fix: JSON repair, WS thread pool, switch to DeepSeek V4 Flash, funnel Lv0

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
This commit is contained in:
truetype committed 2026-06-28 08:18:24 +02:00
1 parent 6b048dc0cc
commit eacc946868
23 files changed
+1786 -61

No files matched your search

+133
View File
@@ -0,0 +1,133 @@
# 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, aree
- `format_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:
1. `parse_tool_call()` fallisce → regex JSON + keyword extraction
2. Se ancora fallisce → `heuristic_fallback.parse_action()`
3. Keyword matching: "attacco" → combat, "muovo" → movement, "d20" → roll_dice
4. 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`: keepalive
- `error`: messaggio errore
### REST API
- `GET /api/campaigns` — lista campagne
- `GET /api/campaign/{id}` — stato completo
- `POST /api/campaign` — crea nuova campagna
- `POST /api/campaign/{id}/character` — aggiunge PG
- `GET /` — frontend
- `GET /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