LLM fixes: - _try_repair_json() — ripara JSON malformato (apici, virgole trailing) - _strip_json_from_text() — rimuove residui JSON dalla narration - parse_tool_call() — rimuove troncamento 200 caratteri dal fallback DM fixes: - Fallback euristico quando params vuoti (dm.py) - Estrazione narrazione da dm_override/narrate params.params.text Prompt (prompts.py): - Vietati termini tecnici/scientifici/anacronistici (HPLC, laser, ...) - Aggiunto vocabolario fantasy obbligatorio Server (server.py): - dm_cycle in ThreadPoolExecutor per non bloccare WS ping - WebSocketDisconnect gestiti - Rimosso import inesistente db_schema Frontend (index.html): - Nascosti tool name dalla chat - Personaggio -> PG, connessioni aree con nomi Infrastruttura: - Switch da openrouter/free a DeepSeek V4 Flash via OpenCode Go - pyproject.toml, Dockerfile, docker-compose.yml, start.sh - .env auto-load (start.sh) - Funnel Lv0 (funnel.py) — creazione PG livello 0 Docs: architecture, database, setup, game mechanics, codebase ref
5.5 KiB
5.5 KiB
Architettura: Neon Scratch Lounge (On-Premise)
Pattern: Separazione Reasoning/Execution
L'architettura separa nettamente ciò che fa il LLM (ragionamento, narrazione) da ciò che fa il motore Python (meccaniche deterministiche, persistenza).
┌─────────────────────────────────────────────────┐
│ CLIENT (Web UI) │
│ Chat testuale + pannello stato laterale │
└──────────────────┬──────────────────────────────┘
│ WebSocket / HTTP REST
┌──────────────────▼──────────────────────────────┐
│ LLM ORCHESTRATOR (src/engine/dm.py) │
│ - Mantiene contesto sessione │
│ - Invia prompt: regole + stato + azione │
│ - Riceve: narrazione + tool calls │
│ - Fallback: heuristic parser su errore LLM │
└──────────────────┬──────────────────────────────┘
│
┌──────────────────▼──────────────────────────────┐
│ TOOL REGISTRY (src/engine/tools.py) │
│ @tool decorator → registro centralizzato │
│ 10 Approved Tools + dm-override │
└──────────────────┬──────────────────────────────┘
│
┌──────────────────▼──────────────────────────────┐
│ STATO PERSISTENTE (SQLite) │
│ src/engine/db.py — 8 tabelle, CRUD completo │
│ WAL mode, thread-local connections │
└─────────────────────────────────────────────────┘
Flusso del Turno (dm_cycle)
dm_cycle(campaign_id, player_action)
│
├─ 1. GameContext.load(campaign_id)
│ Carica da DB: campagna, PG, log, quest, aree
│ → dataclass GameContext
│
├─ 2. _llm_decision(ctx, action)
│ build_prompt(ctx, action) → prompt enorme
│ llm_ask(prompt, system=SYSTEM_PROMPT)
│ parse_tool_call(raw_llm_response) → {tool, params, narration}
│ SE LLM fallisce → fallback_parse(action, chars)
│
├─ 3. tools.run(tool_name, **params)
│ TOOL_REGISTRY[tool_name](**params)
│ → ToolResult dict
│
├─ 4. Persistenza turno
│ db.advance_turn(campaign_id)
│ db.log_turn(campaign_id, turn, action, result)
│
└─ 5. Response dict
{turn, action, tool, params, narration, mechanics, full_output}
Multi-Provider LLM
ProviderConfig (env vars)
│
├─ kind = "ollama" → OllamaProvider
│ POST {base_url}/api/generate
│ model: gemma2:9b
│
├─ kind = "openrouter" → OpenRouterProvider
│ POST openrouter.ai/api/v1/chat/completions
│ Fallback automatico a modelli free
│
└─ kind = "local" → LocalProvider
POST {base_url}/chat/completions (OpenAI-compatible)
Per inference server locali (vLLM, TGI, llama.cpp)
Prompt Engineering
System Prompt (SYSTEM_PROMPT)
- Stile narrazione: sword & sorcery, italiano, 2-3 frasi
- Regole base: CD, CA, tiri salvezza, mighty deed
- Tool disponibili: JSON con parametri + esempi
- Formato output obbligatorio: JSON
{tool, params, narration}
Context Builder (build_prompt)
format_context(ctx): campagna, PG, cronologia, quest, areeformat_area_context(area_id): descrizione area, atmosfera, features, mostri presenti- Knowledge base completa via DB (non file statici)
Heuristic Fallback
Quando il LLM non produce un tool valido:
parse_tool_call()fallisce → regex JSON + keyword extraction- Se ancora fallisce →
heuristic_fallback.parse_action() - Keyword matching: "attacco" → combat, "muovo" → movement, "d20" → roll_dice
- Fallback finale → dm_override (narrazione pura)
Frontend
WebSocket Protocol
state: stato completo campagna (all'entrata + dopo ogni azione)response: risultato turno (narrazione + tool + meccaniche)action: azione giocatore (input → WebSocket → dm_cycle)ping/pong: keepaliveerror: messaggio errore
REST API
GET /api/campaigns— lista campagneGET /api/campaign/{id}— stato completoPOST /api/campaign— crea nuova campagnaPOST /api/campaign/{id}/character— aggiunge PGGET /— frontendGET /health— health check
Key Design Decisions
Perché SQLite invece di DynamoDB
- Zero dipendenze, on-premise
- WAL mode per concorrenza thread-safe
- Thread-local connections via
threading.local()
Perché @tool decorator invece di enum
- Aggiungere nuovo tool = scrivere funzione + decorator
- Nessun'altra modifica richiesta
- Auto-registrazione in TOOL_REGISTRY
Perché heuristic fallback
- Ollama su CPU è lento e talvolta non rispetta il formato JSON
- Il fallback garantisce che l'azione vada sempre a buon fine
- Keyword parser copre ~80% dei casi d'uso base