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
8.2 KiB
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. Imaopenai. Pokrećepython -m idriz. - idriz-admin.service — LAN web panel (Flask + waitress) na
:8080, PAM login. venv:~/src/idriz/admin/.venv. NEMAopenai. Pokrećeadmin/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")—handlerjeasync (ctx, args) -> strAppContext(uski most ka runtime-u; držati ga tankim):ctx.client—AsyncOpenAIctx.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-aawait ctx.notify(text)— ubaci sistemsku poruku modelu i traži novi odgovorawait 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_outputi model ga izgovori. Koristi za igre, brze upite. Handler MORA biti brz; ako treba dug posao, pokreni ga prekoctx.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> ...iliGREŠKA. ...). Handler samo vrati rezultat-tekst.
schema (OpenAI Realtime function tool)
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.
- Napravi folder i prazan
__init__.py(mora biti lagan — bezopenai):
mkdir -p idriz/apps/kviz
touch idriz/apps/kviz/__init__.py
idriz/apps/kviz/manifest.py— prompt fragment, sheme, handleri,SPEC:
"""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")],
)
- Registruj u
idriz/apps/__init__.py(LIJENI uvoz unutar funkcije!):
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]
-
(Opciono) Logika/stanje:
logic.py(smijeopenai) i/ilistore.py.store.pymora ostati bezopenaiako ga uvozi admin. Za trajno stanje/config koristi JSON pod~/.local/share/idriz/ili~/.config/idriz/, uz env-override imena putanje (vidisuperheroji/store.py:IDRIZ_HEROES_PATH,IDRIZ_GAME_CONFIG_PATH). -
(Opciono) Admin stranica:
idriz/apps/kviz/admin.py(samo lagani uvozi):
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:
from idriz.apps.kviz import admin as _kviz_admin
ADMIN_APPS = [_superheroji_admin, _kviz_admin]
- 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)
- Jezgro ostaje app-agnostično. Ne dodaji app-imena ni app-prompt u
assistant.py/INSTRUCTIONS. Sve app-specifično ide uapps/<ime>/. - Admin nema
openai. Šta admin uvozi (admin.py,store.py,apps/__init__.py,apps/base.py) NE smije povlačitiopenai. Teški dio (manifest.py,logic.py) uvozi samo asistent. Zato je uvoz manifesta uload_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)"→ moraTrue. AppContextdrži tankim — par metoda. Ako počne bujati, to je znak da nešto pripada samom app-u, a ne mostu.- inline handler mora biti brz. Dug posao →
ctx.background(...)+ kratka potvrda odmah; rezultat kasnije prekoctx.notify(...). - Ime alata jedinstveno u cijelom sistemu (dispatch je globalni rječnik).
- Poslije SVAKOG commita: restart servisa + smoke test (import, dispatch,
is-active, admin 200). Radi u malim koracima. - Realtime specifičnosti: jedan odgovor u isto vrijeme — programski
response.createide kroznotify_model/_nudgekoji čekaju_wait_idle. Ne šalji siroviresponse.createiz app-a. - 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).