122 lines
4.2 KiB
Python
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,
|
|
})
|