# CLAUDE.md — vodič za rad na idrizu Idriz je glasovni pomoćnik za djecu (OpenAI Realtime, izlaz na printer + govor). Komunikacija sa korisnikom je **isključivo na bosanskom** (ijekavica), i sam idriz priča samo bosanski. Kod, komentari i promptovi su na bosanskom. **Repo:** `~/src/idriz`, backup na Gitea `ssh://git@192.168.88.2:221/senaduka/idriz.git` (grana `main`). NE piši ni u jedan drugi repo. Sav aplikacioni kod ide u ovaj repo. Commit trailer: `Co-Authored-By: Claude Opus 4.8 ` (+ Claude-Session). ## Dva procesa (korisnički systemd servisi) - **idriz.service** — asistent (asyncio, OpenAI Realtime). venv: `~/src/idriz/.venv`. Ima `openai`. Pokreće `python -m idriz`. - **idriz-admin.service** — LAN web panel (Flask + waitress) na `:8080`, PAM login. venv: `~/src/idriz/admin/.venv`. **NEMA `openai`.** Pokreće `admin/app.py`. Restart nakon izmjena i smoke test: ``` systemctl --user restart idriz idriz-admin systemctl --user is-active idriz idriz-admin journalctl --user -u idriz -n 15 --no-pager # traži "povezan na ..." i greške curl -s -o /dev/null -w "%{http_code}\n" http://192.168.1.10:8080/login # 200 ``` ## Arhitektura: mini-app-ovi (plugini) Jezgro `idriz/assistant.py` je **app-agnostično**: audio petlje, realtime sesija, generički pokretač alata (`_run_inline` / `_run_background`), `notify_model`, `_nudge`. Prompt, alati, pozdrav i dispatch se **sklapaju iz registra app-ova** — jezgro ne sadrži nijedno ime app-a. Svaki app je folder u `idriz/apps//`. Registruje se na dva mjesta: - `idriz/apps/__init__.py` → `load_apps()` (za asistenta), - `admin/app.py` → `ADMIN_APPS` (samo ako app ima admin stranicu). Tipovi su u `idriz/apps/base.py`: - `AppSpec(name, title, instructions="", greeting="", tools=[])` - `AppTool(schema, handler, mode="inline")` — `handler` je `async (ctx, args) -> str` - `AppContext` (uski most ka runtime-u; **držati ga tankim**): - `ctx.client` — `AsyncOpenAI` - `ctx.cfg` — `Config` (npr. `ctx.cfg.game_model`, `text_model`, `image_model`, `out_dir`, `printer`, `dry_run`, `max_pages`) - `ctx.get_state(key)` / `ctx.set_state(key, val)` — per-razgovor stanje app-a - `await ctx.notify(text)` — ubaci sistemsku poruku modelu i traži novi odgovor - `await ctx.nudge(instructions)` — kratko podstakni model (npr. filler) - `ctx.background(coro)` — pokreni praćeni pozadinski task ### `mode`: inline vs background - **inline** — brzo (≲ par sekundi). Rezultat handlera ide ODMAH nazad modelu kao `function_call_output` i model ga izgovori. Koristi za igre, brze upite. Handler MORA biti brz; ako treba dug posao, pokreni ga preko `ctx.background(...)` i handler odmah vrati kratku potvrdu (vidi superheroji izbor→auto-presuda). - **background** — dug posao (štampanje, generisanje slike). Runtime modelu odmah kaže "radim na tome", pa kad handler završi pošalje rezultat preko `notify_model` (`GOTOVO. ...` ili `GREŠKA. ...`). Handler samo vrati rezultat-tekst. ### `schema` (OpenAI Realtime function tool) ```python SCHEMA = { "type": "function", "name": "", # jedinstveno u cijelom sistemu "description": "Šta radi i KADA ga zvati. Piši jasno, na bosanskom.", "parameters": { "type": "object", "properties": { "arg": {"type": "string", "description": "..."} }, "required": ["arg"], }, } ``` ## Kako napraviti NOVI app (korak po korak) Primjer: app `kviz`. 1) Napravi folder i prazan `__init__.py` (mora biti lagan — bez `openai`): ``` mkdir -p idriz/apps/kviz touch idriz/apps/kviz/__init__.py ``` 2) `idriz/apps/kviz/manifest.py` — prompt fragment, sheme, handleri, `SPEC`: ```python """Mini-app 'Kviz'.""" from ..base import AppContext, AppSpec, AppTool INSTRUCTIONS = """KVIZ (alat: kviz_start): ... opiši djetetu razumljivo tok i pravila ... - (pravila, kad zvati alat, kako se ponašati)""" START_SCHEMA = { "type": "function", "name": "kviz_start", "description": "Započinje kviz. Pozvati kad dijete kaže da želi kviz.", "parameters": {"type": "object", "properties": { "tema": {"type": "string", "description": "Tema kviza."}}, "required": ["tema"]}, } async def start(ctx: AppContext, args: dict) -> str: # ctx.set_state("kviz", ) ako treba pamtiti kroz razgovor # ctx.client / ctx.cfg.text_model za LLM pozive return "Kviz je počeo! ..." # inline: model odmah izgovori SPEC = AppSpec( name="kviz", title="Kviz", instructions=INSTRUCTIONS, greeting="igrati kviz", # ubaci se u pozdrav ("a možeš i: ..., igrati kviz") tools=[AppTool(START_SCHEMA, start, "inline")], ) ``` 3) Registruj u `idriz/apps/__init__.py` (LIJENI uvoz unutar funkcije!): ```python def load_apps(): from .bojanka import manifest as bojanka from .sazetak import manifest as sazetak from .superheroji import manifest as superheroji from .kviz import manifest as kviz # <— return [sazetak.SPEC, bojanka.SPEC, superheroji.SPEC, kviz.SPEC] ``` 4) (Opciono) Logika/stanje: `logic.py` (smije `openai`) i/ili `store.py`. **`store.py` mora ostati bez `openai`** ako ga uvozi admin. Za trajno stanje/config koristi JSON pod `~/.local/share/idriz/` ili `~/.config/idriz/`, uz env-override imena putanje (vidi `superheroji/store.py`: `IDRIZ_HEROES_PATH`, `IDRIZ_GAME_CONFIG_PATH`). 5) (Opciono) Admin stranica: `idriz/apps/kviz/admin.py` (samo lagani uvozi): ```python from flask import Blueprint, render_template, request, redirect, url_for, flash from . import store # bez openai! BP = Blueprint("kviz", __name__, template_folder="templates", url_prefix="/kviz") NAV = {"endpoint": "kviz.index", "page": "kviz", "title": "Kviz", "icon": "❓"} @BP.route("/", methods=["GET", "POST"]) def index(): # POST: sačuvaj config; GET: render return render_template("kviz.html", active_page="kviz") ``` Template ide u `idriz/apps/kviz/templates/kviz.html` i `{% extends "base.html" %}` (base je u `admin/templates/`; auth štiti globalni `before_request`). Zatim u `admin/app.py`: ```python from idriz.apps.kviz import admin as _kviz_admin ADMIN_APPS = [_superheroji_admin, _kviz_admin] ``` 6) Testiraj (bez štampanja/pravih poziva gdje se može), pa commit + smoke test: ``` .venv/bin/python -c "import os; os.environ.setdefault('OPENAI_API_KEY','x'); \ from idriz.config import Config; from idriz.assistant import Assistant; \ a=Assistant(Config()); print([t['name'] for t in a.tool_schemas])" admin/.venv/bin/python -c "import sys,pathlib; sys.path.insert(0,'.'); import app; print('admin ok')" systemctl --user restart idriz idriz-admin && systemctl --user is-active idriz idriz-admin ``` ## Zlatna pravila (da se ne pogriješi) 1. **Jezgro ostaje app-agnostično.** Ne dodaji app-imena ni app-prompt u `assistant.py`/`INSTRUCTIONS`. Sve app-specifično ide u `apps//`. 2. **Admin nema `openai`.** Šta admin uvozi (`admin.py`, `store.py`, `apps/__init__.py`, `apps/base.py`) NE smije povlačiti `openai`. Teški dio (`manifest.py`, `logic.py`) uvozi samo asistent. Zato je uvoz manifesta u `load_apps()` LIJEN. Provjera (iz `~/src/idriz`): `admin/.venv/bin/python -c "import sys; sys.path.insert(0,'.'); from idriz.apps. import store; print('openai' not in sys.modules)"` → mora `True`. 3. **`AppContext` drži tankim** — par metoda. Ako počne bujati, to je znak da nešto pripada samom app-u, a ne mostu. 4. **inline handler mora biti brz.** Dug posao → `ctx.background(...)` + kratka potvrda odmah; rezultat kasnije preko `ctx.notify(...)`. 5. **Ime alata jedinstveno** u cijelom sistemu (dispatch je globalni rječnik). 6. **Poslije SVAKOG commita:** restart servisa + smoke test (import, dispatch, `is-active`, admin 200). Radi u malim koracima. 7. **Realtime specifičnosti:** jedan odgovor u isto vrijeme — programski `response.create` ide kroz `notify_model`/`_nudge` koji čekaju `_wait_idle`. Ne šalji sirovi `response.create` iz app-a. 8. Bosanski svugdje; sadržaj primjeren djeci. ## Mreža / hardver (ukratko) IP mašine `192.168.1.10`; Gitea `192.168.88.2:221` (drugi subnet, ruta preko `192.168.1.1`). Printer: CUPS `idriz-printer`. Detalji u memoriji (`~/.claude/projects/-home-idriz/memory/`, indeks `MEMORY.md`).