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:
1 parent
6b048dc0cc
commit
eacc946868
23 files changed
+1786
-61
No files matched your search
@@ -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
|
||||
Reference in new issue
Block a user