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:
180
CLAUDE.md
Normal file
180
CLAUDE.md
Normal 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`).
|
||||
Reference in New Issue
Block a user