import logging import uuid from sqlalchemy.ext.asyncio import AsyncSession from app.llm.context import build_transcript_text, count_messages_since from app.models.world_state import WorldState logger = logging.getLogger("app.llm.tools.world_state") # After this many new messages without an update_world_state call, force a dedicated # summarization call instead of relying on the DM to remember — see run_dm_turn(). AUTO_REFRESH_MESSAGE_THRESHOLD = 10 SUMMARIZER_SYSTEM_PROMPT = ( "Du fasst den bisherigen Verlauf eines laufenden D&D-Text-Adventures kompakt zusammen. " "Antworte NUR mit der Zusammenfassung selbst — keine Einleitung, keine Meta-Kommentare. " "Schreibe in der Sprache des Transkripts." ) TOOL_SCHEMA = { "name": "update_world_state", "description": ( "Persist a compact, up-to-date recap of everything that matters long-term for this game: " "key NPCs and their status, current location, open plot threads, important party/inventory " "state. The raw chat history you see is only a recent window — this summary is what carries " "everything older forward. Call it regularly (at least on every scene change or major " "development), not just at the end. Send the full current picture each time, not a diff — " "this replaces whatever was stored before." ), "parameters": { "type": "object", "properties": { "summary": { "type": "string", "description": ( "The complete current world-state recap, in the language you're playing in. " "A few short paragraphs or a bullet list, not a full transcript." ), }, }, "required": ["summary"], }, } async def update(session: AsyncSession, game_id: uuid.UUID, tool_input: dict) -> dict: state = await session.get(WorldState, game_id) if state is None: state = WorldState(game_id=game_id, summary=tool_input["summary"]) session.add(state) else: state.summary = tool_input["summary"] await session.commit() return {"summary": state.summary} async def get_summary(session: AsyncSession, game_id: uuid.UUID) -> str | None: state = await session.get(WorldState, game_id) return state.summary if state is not None and state.summary else None async def refresh_if_due(session: AsyncSession, game_id: uuid.UUID, client, model: str) -> bool: """Hard backstop for update_world_state: if AUTO_REFRESH_MESSAGE_THRESHOLD new messages have piled up since the summary was last touched (by the DM's tool call or a prior auto-refresh), force a dedicated summarization call instead of trusting the DM to remember. Returns whether it actually refreshed.""" state = await session.get(WorldState, game_id) since = state.updated_at if state is not None else None new_count = await count_messages_since(session, game_id, since) if new_count < AUTO_REFRESH_MESSAGE_THRESHOLD: return False transcript = await build_transcript_text(session, game_id, since=since) if not transcript: return False prior_summary = state.summary if state is not None and state.summary else "(noch keine)" user_prompt = ( f"Bisherige Zusammenfassung:\n{prior_summary}\n\n" f"Neue Ereignisse seither:\n{transcript}\n\n" "Schreibe die vollständige, aktuelle Zusammenfassung neu (nicht nur die neuen Ereignisse " "anhängen): wichtige NPCs und ihr Status, aktueller Ort, offene Handlungsstränge, " "Party-/Inventarstand. Ein paar kurze Absätze oder Stichpunkte, kein Transkript." ) try: response = await client.chat.completions.create( model=model, max_tokens=800, messages=[ {"role": "system", "content": SUMMARIZER_SYSTEM_PROMPT}, {"role": "user", "content": user_prompt}, ], ) summary = response.choices[0].message.content except Exception as exc: # noqa: BLE001 logger.warning("Auto world-state summarization failed for game %s: %s", game_id, exc) return False if not summary: return False if state is None: state = WorldState(game_id=game_id, summary=summary) session.add(state) else: state.summary = summary await session.commit() return True