diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1f47dc9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,180 @@ +# 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`).