Dodaj CLAUDE.md: vodič za pravljenje novih mini-app-ova

Detaljan vodič (orijentacija, dva procesa, arhitektura plugina, AppSpec/AppTool/
AppContext, inline vs background, korak-po-korak skelet novog app-a sa admin
Blueprint-om, i zlatna pravila: jezgro app-agnostično, admin bez openai, tanak
AppContext, smoke test poslije svakog commita).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LzKpYaEsuWDevuNnvSjcFW
This commit is contained in:
idriz
2026-09-11 14:20:13 +02:00
parent e74c617228
commit 00b4525588

180
CLAUDE.md Normal file
View File

@@ -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 <noreply@anthropic.com>` (+ 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/<ime>/`. 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. <tvoj return> ...` ili `GREŠKA. ...`). Handler samo vrati rezultat-tekst.
### `schema` (OpenAI Realtime function tool)
```python
SCHEMA = {
"type": "function",
"name": "<ime_alata>", # 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", <stanje>) 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/<ime>/`.
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.<ime> 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`).