splinter-keep/tools/engine_lib/parsing.py
2026-06-30 20:03:53 +02:00

122 lines
4.2 KiB
Python

#!/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,
})