#!/usr/bin/env python3 """ parsing.py — LLM response parsing and turn logging for The Chaos engine. Standalone functions — no dependency on GameEngine. """ from __future__ import annotations import json import re from datetime import datetime from typing import Optional from .models import GenerationResult from . import state def parse_response(text: str) -> GenerationResult: """ Parse a full LLM response into a GenerationResult. Extracts the JSON block and splits narrative from it. """ if text.startswith('{"error":'): try: err = json.loads(text).get("error", "Unknown error") except json.JSONDecodeError: err = "Unknown error" return GenerationResult(narrative="", error=err) json_pattern = r"```json\s*\n?(.*?)\n?```" matches = re.findall(json_pattern, text, re.DOTALL) narrative = text data = {} if matches: json_str = matches[-1].strip() narrative = text[: text.rfind("```json")] narrative_lines = [] for line in narrative.splitlines(): if not line.lstrip().startswith('book_log:'): narrative_lines.append(line) narrative = "\n".join(narrative_lines).strip() try: data = json.loads(json_str) except json.JSONDecodeError: pass else: text_stripped = text.strip() if text_stripped.startswith("{") and text_stripped.endswith("}"): try: data = json.loads(text_stripped) narrative = data.get("narrative", "") except json.JSONDecodeError: pass return GenerationResult( narrative=narrative or text, choices=data.get("choices", []), log_entry=data.get("log_entry"), ambience=data.get("ambience"), character_updates=data.get("character_updates"), world_updates=data.get("world_updates"), journal_add=data.get("journal_add", []), journal_done=data.get("journal_done", []), ) def log_turn_details( player_action: str, last_prompt: str, strategy_name: str, die_roll: int, model: str, temperature: float, max_tokens: int, book_log: str, log_entry: str, ambience: Optional[str], tool_calls: list, on_debug=None, ) -> None: """Write structured turn summary to llm.log and fire TUI debug event.""" ts = datetime.now().isoformat() output_chars = len(book_log) output_words = len(book_log.split()) if book_log else 0 applied = len([tc for tc in tool_calls if tc.get("tool") != "finalize_turn"]) state.append_llm_log("") state.append_llm_log(f"┌─ Turn Details — {ts}") state.append_llm_log(f"├─ Input: {player_action}") state.append_llm_log(f"├─ Last Prompt: {last_prompt}") state.append_llm_log(f"├─ Strategy: {strategy_name}") state.append_llm_log(f"├─ Dice: {die_roll} (1d6)") state.append_llm_log(f"├─ Model: {model} | Temp: {temperature} | Tokens: {max_tokens}") state.append_llm_log(f"├─ Output: {output_chars} chars ({output_words} words)") state.append_llm_log(f"├─ Log Entry: {log_entry}") state.append_llm_log(f"├─ Ambience: {ambience or 'None'}") tools_preview = ", ".join(tc.get("tool", "?") for tc in tool_calls) state.append_llm_log(f"├─ Tool Calls: {len(tool_calls)} ({tools_preview})") state.append_llm_log( "└─────────────────────────────────────────────────────────────────────────────────────────┘" ) if on_debug: on_debug("turn_details", { "timestamp": ts, "model": model, "temperature": temperature, "max_tokens": max_tokens, "strategy_name": strategy_name, "die_roll": die_roll, "player_action": player_action, "book_log_chars": output_chars, "book_log_words": output_words, "ambience": ambience, "tool_calls_count": len(tool_calls), "applied_changes_count": applied, "tool_call_results": tool_calls, })