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

+49 -4
View File
@@ -27,6 +27,30 @@ def ask(prompt: str, system: str = "") -> str:
return get_provider().ask(prompt, system=system)
def _try_repair_json(text: str) -> str | None:
"""Tenta di riparare errori JSON comuni prodotti dagli LLM."""
m = re.search(r'(\{[\s\S]*\})', text)
if not m:
return None
blob = m.group(1)
# Fix 1: chiavi con apici singoli → doppi ("params' " → "params": ")
blob = re.sub(r"(?<!\\)'(params|narration|tool|command|text|character_id|dc|modifier|die|count)"
r"'(?=\s*[:=])", r'"\1":', blob)
# Fix 2: qualsiasi altra chiave con apici singoli
blob = re.sub(r"(?<!\\)'(\w+)'(?=\s*[:=])", r'"\1":', blob)
# Fix 3: trailing comma prima di } o ]
blob = re.sub(r',\s*}', '}', blob)
blob = re.sub(r',\s*\]', ']', blob)
# Fix 4: = al posto di : nelle chiavi
blob = re.sub(r'"(\w+)"\s*=', r'"\1":', blob)
try:
json.loads(blob)
return blob
except json.JSONDecodeError:
return None
def _extract_json(text: str) -> str | None:
for pat in [
r'```json\s*([\s\S]*?)```',
@@ -40,8 +64,27 @@ def _extract_json(text: str) -> str | None:
json.loads(candidate)
return candidate
except json.JSONDecodeError:
# tenta riparazione
repaired = _try_repair_json(candidate)
if repaired:
return repaired
continue
return None
# try repair on full text too
return _try_repair_json(text)
def _strip_json_from_text(text: str) -> str:
"""Rimuove qualsiasi contenuto simile a JSON dal testo narrativo."""
# toglie blocchi {...} completi di tool call
cleaned = re.sub(r'\{"tool":[\s\S]*?"narration":\s*"[^"]*"\s*\}', '', text)
# toglie blocchi JSON parziali o incompleti
cleaned = re.sub(r'\{"tool":[\s\S]*?\}', '', cleaned)
cleaned = re.sub(r'\{\s*"[a-zA-Z_]+"\s*[:=][^}]*\}', '', cleaned)
# pulisce spaziature multiple
cleaned = re.sub(r'\s+', ' ', cleaned).strip()
# toglie virgolette pendenti all'inizio/fine
cleaned = cleaned.strip('"\' ,.')
return cleaned
def parse_tool_call(text: str) -> dict:
@@ -55,10 +98,12 @@ def parse_tool_call(text: str) -> dict:
except json.JSONDecodeError:
pass
tool_match = re.search(r'(?:tool|strumento|chiamo|uso)\s*[":]*\s*(\w+)', text, re.I)
# Fallback: estrae tool name via regex, ma PULISCE la narration dal JSON
tool_match = re.search(r'\b(?:tool|strumento|chiamo|uso)\s*[":]*\s*(\w+)', text, re.I)
narration = _strip_json_from_text(text) if text else ""
if tool_match:
return {"tool": tool_match.group(1), "params": {}, "narration": text[:200]}
return {"tool": "dm_override", "params": {"command": "narrate", "params": {"text": text[:200]}}, "narration": ""}
return {"tool": tool_match.group(1), "params": {}, "narration": narration}
return {"tool": "dm_override", "params": {"command": "narrate", "params": {"text": narration or text[:500]}}, "narration": narration}
if __name__ == "__main__":