import json import os from services.llm import send_message_with_model import logging logger = logging.getLogger(__name__) NARRATOR_MODEL = os.getenv("RPG_NARRATOR_MODEL", "").strip() or "deepseek/deepseek-chat-v3" NARRATOR_PRE_SYSTEM = """You are the System/Narrator of an RPG chat. You do NOT roleplay as the character. You update the status quo and enforce mechanics. Return ONLY valid JSON (no markdown): { "directives": ["short imperative rules for the next character reply"], "resolution_text": "what actually happens as the result of the user's described action (1-2 sentences)", "status_quo_update": "optional short update about the world's state after applying outcome", "choices": [{"id":"a","label":"..."}, ...] } Rules: - directives must be actionable constraints (tone, consequences, new obstacles). - resolution_text must be consistent with roll/outcome and should not contradict established facts. - If outcome is failure/critical failure, impose meaningful complication. - If outcome is success/critical success, allow progress and reward. - Keep it short.""" NARRATOR_POST_SYSTEM = """You are the System/Narrator of an RPG chat. After the character replied, update persistent status quo and facts. Return ONLY valid JSON (no markdown): { "status_quo_update": "what changed in the world/state (short)", "facts": ["durable facts only"], "choices": [{"id":"a","label":"..."}, ...] } Rules: - status_quo_update should be 1-3 sentences. - facts must be stable and non-duplicative. - choices optional (0-4).""" async def narrator_pre( persona_name: str, context: str, global_plot: str, facts_block: str, roll: int, outcome: str, ) -> dict: user = ( f"Persona: {persona_name}\n" f"Roll d20={roll}\nOutcome={outcome}\n\n" f"Global plot:\n{global_plot}\n\n" f"Facts:\n{facts_block}\n\n" f"Recent context:\n{context}\n" ) raw = await send_message_with_model( [{"role": "system", "content": NARRATOR_PRE_SYSTEM}, {"role": "user", "content": user}], NARRATOR_MODEL, ) cleaned = raw.strip() if cleaned.startswith("```"): cleaned = cleaned.split("\n", 1)[1] if "\n" in cleaned else cleaned if cleaned.endswith("```"): cleaned = cleaned.rsplit("```", 1)[0] cleaned = cleaned.strip() try: data = json.loads(cleaned) if isinstance(data, dict): return data except Exception: logger.warning("Narrator-pre JSON parse failed. Raw=%.500s", raw) return {"directives": [], "status_quo_update": "", "choices": []} async def narrator_post( persona_name: str, context: str, global_plot: str, facts_block: str, ) -> dict: user = ( f"Persona: {persona_name}\n\n" f"Global plot:\n{global_plot}\n\n" f"Facts:\n{facts_block}\n\n" f"Recent context:\n{context}\n" ) raw = await send_message_with_model( [{"role": "system", "content": NARRATOR_POST_SYSTEM}, {"role": "user", "content": user}], NARRATOR_MODEL, ) cleaned = raw.strip() if cleaned.startswith("```"): cleaned = cleaned.split("\n", 1)[1] if "\n" in cleaned else cleaned if cleaned.endswith("```"): cleaned = cleaned.rsplit("```", 1)[0] cleaned = cleaned.strip() try: data = json.loads(cleaned) if isinstance(data, dict): return data except Exception: logger.warning("Narrator-post JSON parse failed. Raw=%.500s", raw) return {"status_quo_update": "", "facts": [], "choices": []}