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
+79
-27
@@ -1,13 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
import uvicorn # noqa: F401 — needed by main()
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
from src.engine import db
|
||||
from src.engine.config import load_from_env
|
||||
@@ -51,11 +53,16 @@ def get_campaign_state(campaign_id: int):
|
||||
for sa in ctx.areas:
|
||||
aid = sa["area_id"]
|
||||
full = area_map.get(aid, {})
|
||||
raw_conns = full.get("connections", [])
|
||||
conn_names = []
|
||||
for cid in raw_conns:
|
||||
linked = area_map.get(cid)
|
||||
conn_names.append(linked["name"] if linked else cid)
|
||||
enriched.append({
|
||||
"id": aid,
|
||||
"name": full.get("name", aid),
|
||||
"description": full.get("description", "")[:200],
|
||||
"connections": full.get("connections", []),
|
||||
"connections": conn_names,
|
||||
"visited": bool(sa.get("visited")),
|
||||
"features": full.get("features", [])[:3],
|
||||
})
|
||||
@@ -78,32 +85,51 @@ def create_campaign(name: str = "Nuova Avventura", adventure: str = ""):
|
||||
areas = db.get_all_areas()
|
||||
if areas:
|
||||
db.set_area_state(camp_id, areas[0]["id"], visited=True)
|
||||
return {"campaign_id": camp_id}
|
||||
for pg in genera_funnel(4):
|
||||
_create_lv0_character(camp_id, pg)
|
||||
return {"campaign_id": camp_id, "funnel": True}
|
||||
|
||||
|
||||
def _create_lv0_character(campaign_id: int, pg: dict) -> int:
|
||||
char_id = db.create_character(
|
||||
campaign_id,
|
||||
name=pg["name"],
|
||||
char_class=f"Lv0 ({pg['occupation']})",
|
||||
occupation=pg["occupation"],
|
||||
)
|
||||
db.update_character(
|
||||
char_id,
|
||||
level=0,
|
||||
str=pg["str"],
|
||||
agi=pg["agi"],
|
||||
sta=pg["sta"],
|
||||
per=pg["per"],
|
||||
int=pg["int"],
|
||||
lck=pg["lck"],
|
||||
hp_current=pg["hp_current"],
|
||||
hp_max=pg["hp_max"],
|
||||
ac=pg["ac"],
|
||||
armor_type=pg["armor_type"],
|
||||
weapon=pg["weapon"],
|
||||
equipment=f'["{pg["weapon"]}", "{pg["belonging"]}"]',
|
||||
)
|
||||
return char_id
|
||||
|
||||
|
||||
@app.post("/api/campaign/{campaign_id}/character")
|
||||
def add_character(
|
||||
campaign_id: int,
|
||||
name: str = "Eroe",
|
||||
name: str = "",
|
||||
char_class: str = "",
|
||||
occupation: str = "",
|
||||
):
|
||||
char_id = db.create_character(campaign_id, name, char_class, occupation)
|
||||
# Default stats for a lv0/lv1 character
|
||||
db.update_character(
|
||||
char_id,
|
||||
level=1,
|
||||
str=12,
|
||||
agi=11,
|
||||
sta=12,
|
||||
per=10,
|
||||
int=10,
|
||||
lck=10,
|
||||
hp_current=8,
|
||||
hp_max=8,
|
||||
ac=12,
|
||||
)
|
||||
return {"character_id": char_id}
|
||||
if name and char_class:
|
||||
char_id = db.create_character(campaign_id, name, char_class, occupation)
|
||||
db.update_character(char_id, level=1, str=12, agi=11, sta=12, per=10, int=10, lck=10, hp_current=8, hp_max=8, ac=12)
|
||||
return {"character_id": char_id}
|
||||
pg = genera_pg_lv0(name)
|
||||
char_id = _create_lv0_character(campaign_id, pg)
|
||||
return {"character_id": char_id, "level": 0, "name": pg["name"], "occupation": pg["occupation"]}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -111,6 +137,9 @@ def add_character(
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
_executor = ThreadPoolExecutor(max_workers=4)
|
||||
|
||||
|
||||
@app.websocket("/ws/{campaign_id}")
|
||||
async def game_ws(ws: WebSocket, campaign_id: int):
|
||||
await ws.accept()
|
||||
@@ -126,7 +155,11 @@ async def game_ws(ws: WebSocket, campaign_id: int):
|
||||
await ws.send_json({"type": "state", "campaign": _build_state(campaign_id)})
|
||||
|
||||
while True:
|
||||
raw = await ws.receive_text()
|
||||
try:
|
||||
raw = await ws.receive_text()
|
||||
except WebSocketDisconnect:
|
||||
break
|
||||
|
||||
try:
|
||||
msg = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
@@ -138,7 +171,12 @@ async def game_ws(ws: WebSocket, campaign_id: int):
|
||||
continue
|
||||
|
||||
try:
|
||||
out = dm_cycle(campaign_id, player_action)
|
||||
# dm_cycle è bloccante (chiamata HTTP al LLM) —
|
||||
# la eseguiamo in un thread worker per non bloccare
|
||||
# il loop asyncio e tenere vivi i ping WebSocket
|
||||
out = await asyncio.get_event_loop().run_in_executor(
|
||||
_executor, dm_cycle, campaign_id, player_action
|
||||
)
|
||||
await ws.send_json({
|
||||
"type": "response",
|
||||
"turn": out["turn"],
|
||||
@@ -150,13 +188,22 @@ async def game_ws(ws: WebSocket, campaign_id: int):
|
||||
})
|
||||
except Exception as e:
|
||||
log.exception("DM cycle failed")
|
||||
await ws.send_json({"type": "error", "message": str(e)})
|
||||
try:
|
||||
await ws.send_json({"type": "error", "message": str(e)})
|
||||
except WebSocketDisconnect:
|
||||
break
|
||||
|
||||
# Send updated state after action
|
||||
await ws.send_json({"type": "state", "campaign": _build_state(campaign_id)})
|
||||
try:
|
||||
await ws.send_json({"type": "state", "campaign": _build_state(campaign_id)})
|
||||
except WebSocketDisconnect:
|
||||
break
|
||||
|
||||
elif msg.get("type") == "ping":
|
||||
await ws.send_json({"type": "pong"})
|
||||
try:
|
||||
await ws.send_json({"type": "pong"})
|
||||
except WebSocketDisconnect:
|
||||
break
|
||||
|
||||
|
||||
def _build_state(campaign_id: int) -> dict:
|
||||
@@ -167,11 +214,16 @@ def _build_state(campaign_id: int) -> dict:
|
||||
for sa in ctx.areas:
|
||||
aid = sa["area_id"]
|
||||
full = area_map.get(aid, {})
|
||||
raw_conns = full.get("connections", [])
|
||||
conn_names = []
|
||||
for cid in raw_conns:
|
||||
linked = area_map.get(cid)
|
||||
conn_names.append(linked["name"] if linked else cid)
|
||||
enriched.append({
|
||||
"id": aid,
|
||||
"name": full.get("name", aid),
|
||||
"visited": bool(sa.get("visited")),
|
||||
"connections": full.get("connections", []),
|
||||
"connections": conn_names,
|
||||
})
|
||||
return {
|
||||
"turn": ctx.turn,
|
||||
|
||||
Reference in new issue
Block a user