import json import logging import os from services.llm import LLMError, send_message_with_model, send_message logger = logging.getLogger(__name__) FACTS_MODEL = os.getenv("RPG_FACTS_MODEL", "").strip() or "deepseek/deepseek-chat-v3" FACTS_SYSTEM = """Extract stable facts from the conversation. Return ONLY valid JSON (no markdown), as an array of short strings. Rules: - Facts must be durable (names, relations, inventory, locations, world rules). - Do not include ephemeral actions unless they change state. - Avoid duplicates. - Keep each fact <= 120 chars. Example output: ["User name is Alex", "We are in a ruined castle", "NPC Mira distrusts the user"]""" def merge_facts(existing_json: str, new_facts: list[str], limit: int = 80) -> str: try: existing = json.loads(existing_json or "[]") if not isinstance(existing, list): existing = [] except json.JSONDecodeError: existing = [] seen = {str(x).strip() for x in existing if str(x).strip()} merged = [str(x).strip() for x in existing if str(x).strip()] for f in new_facts: s = str(f).strip() if not s or s in seen: continue seen.add(s) merged.append(s) if len(merged) > limit: merged = merged[-limit:] return json.dumps(merged, ensure_ascii=False) async def extract_facts(context_messages: list[dict]) -> list[str]: # Build a compact transcript transcript = "\n".join( f"{m.get('role')}: {m.get('content','')}".strip() for m in context_messages if m.get("role") in ("user", "assistant") )[-6000:] messages = [ {"role": "system", "content": FACTS_SYSTEM}, {"role": "user", "content": transcript}, ] try: raw = await ( send_message_with_model(messages, FACTS_MODEL) if FACTS_MODEL else send_message(messages) ) except LLMError as e: logger.warning("extract_facts LLM failed (model=%s): %s", FACTS_MODEL or "SYSTEM", e) return [] except Exception as e: logger.warning("extract_facts unexpected error: %s", e) return [] try: data = json.loads(raw.strip()) if isinstance(data, list): return [str(x) for x in data][:40] except Exception: return [] return [] def facts_to_prompt(facts_json: str, max_items: int = 20) -> str: try: facts = json.loads(facts_json or "[]") if not isinstance(facts, list): return "" except json.JSONDecodeError: return "" facts = [str(x).strip() for x in facts if str(x).strip()] if not facts: return "" block = "\n".join(f"- {x}" for x in facts[-max_items:]) return f"--- Facts (persistent memory) ---\n{block}\n---"