from __future__ import annotations import contextvars import json import re from src.engine.providers import LLMProvider, create_provider _provider_var: contextvars.ContextVar[LLMProvider | None] = contextvars.ContextVar("provider", default=None) def get_provider() -> LLMProvider: provider = _provider_var.get() if provider is None: from src.engine.config import load_from_env provider = create_provider(load_from_env()) _provider_var.set(provider) return provider def set_provider(provider: LLMProvider) -> None: _provider_var.set(provider) 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"(? str | None: for pat in [ r'```json\s*([\s\S]*?)```', r'```\s*([\s\S]*?)```', r'(\{[\s\S]*\})', ]: m = re.search(pat, text) if m: candidate = m.group(1).strip() try: json.loads(candidate) return candidate except json.JSONDecodeError: # tenta riparazione repaired = _try_repair_json(candidate) if repaired: return repaired continue # 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: text = text.strip() blob = _extract_json(text) if blob: try: data = json.loads(blob) if "tool" in data: return data except json.JSONDecodeError: pass # 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": narration} return {"tool": "dm_override", "params": {"command": "narrate", "params": {"text": narration or text[:500]}}, "narration": narration} if __name__ == "__main__": test_actions = [ "Tiro un d20 per vedere se sento rumori", "Attacco l'orco con la mia ascia!", "Esploro il corridoio buio", ] from src.engine.prompts import SYSTEM_PROMPT as _DM_PROMPT for a in test_actions: print(f"\n>>> Giocatore: {a}") try: resp = ask(a, system=_DM_PROMPT) print(f"LLM: {resp[:300]}") parsed = parse_tool_call(resp) print(f"Parser: {json.dumps(parsed, indent=2)}") except Exception as e: print(f"ERRORE: {e}")