Files
dcc-rpg/docs/architecture.md
T
truetype eacc946868 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
2026-06-28 08:18:24 +02:00

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, 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