Add more logging. Selectable LLM strategy.

This commit is contained in:
Dejvino
2026-06-29 22:59:45 +02:00
parent 25fb5fd729
commit 91b1b35cfa
7 changed files with 975 additions and 61 deletions
+312 -11
View File
@@ -13,6 +13,7 @@ import json
import re
import sys
from dataclasses import dataclass, field
import random
from datetime import date, datetime
from pathlib import Path
from string import Template
@@ -203,6 +204,10 @@ class GameEngine:
def max_tokens(self) -> int:
return self.config.get("llm", {}).get("max_tokens", 512)
@property
def timeout(self) -> int:
return self.config.get("llm", {}).get("timeout", 120)
def _set_llm_env(self) -> None:
"""Set provider-specific env vars for litellm."""
prefix = self.model.split("/")[0].upper()
@@ -374,7 +379,7 @@ class GameEngine:
messages=messages,
temperature=self.temperature,
stream=False,
timeout=60,
timeout=self.timeout,
)
text = response.choices[0].message.content or ""
except Exception as e:
@@ -420,7 +425,7 @@ class GameEngine:
messages=messages,
temperature=self.temperature,
stream=True,
timeout=60,
timeout=self.timeout,
)
full_text = ""
for chunk in response:
@@ -881,10 +886,14 @@ class GameEngine:
messages=messages,
temperature=self.temperature,
stream=False,
timeout=60,
timeout=self.timeout,
max_tokens=max_tokens or self.max_tokens,
)
text = response.choices[0].message.content or ""
content = getattr(response.choices[0].message, 'content', None) or ""
reasoning = getattr(response.choices[0].message, 'reasoning_content', None) or ""
if reasoning and reasoning not in content:
self._append_llm_log(f"\n--- {label} [reasoning] ---\n{reasoning}")
text = content or reasoning
self._append_llm_log(f"\n--- {label} ---\n{text}")
return text
except Exception as e:
@@ -1197,6 +1206,226 @@ class GameEngine:
changes=changes,
)
def generate_with_tools_single(
self,
player_action: str | None = None,
last_prompt: str | None = None,
on_thought: callable = None,
on_action: callable = None,
on_player_roll: callable = None,
on_debug: callable = None,
) -> TurnResult:
"""
Single-call generation using tools.
Uses a single LLM call with all tools available — LLM outputs
narrative + tool blocks in one go. No retry loop.
"""
from datetime import datetime
self._append_llm_log(f"\n{'='*60}")
self._append_llm_log(f"=== Turn — {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ===")
self._append_llm_log(f"{'='*60}")
if player_action:
self._append_llm_log(f"Player: {player_action}")
elif last_prompt:
self._append_llm_log(f"Resume from: {last_prompt[:120]}")
strategy_name = "tools"
if on_action:
on_action(f"LLM: {self.model} | temp={self.temperature} | tokens={self.max_tokens} | strategy={strategy_name}")
if on_debug:
on_debug("config", {"model": self.model, "temperature": self.temperature, "max_tokens": self.max_tokens, "strategy": strategy_name})
die_roll = random.randint(1, 6)
self._append_llm_log(f"Dice: {die_roll} (1d6)")
# Build system prompt that instructs LLM to use tools for changes
system = """You are an RPG dungeon master. The player just took an action.
Output ONLY ```tool blocks — no prose, no reasoning, no explanation outside tool blocks. Every piece of output must be in a tool block.
Use these tools to perform every action. Wrap each in its own ```tool block:
```tool
{"tool": "narrative", "args": {"text": "The full vivid narrative prose goes here."}}
```
```tool
{"tool": "modify_vitals", "args": {"current_hp": 5, "cash": 45}}
```
```tool
{"tool": "modify_traits", "args": {"dex": 15}}
```
```tool
{"tool": "add_to_inventory", "args": {"item": "Silver key"}}
```
```tool
{"tool": "remove_from_inventory", "args": {"item": "Torches (10)"}}
```
```tool
{"tool": "replace_gear", "args": {"before": "Mace (1d6+1)", "after": "Mace (1d6+2, sharpened)"}}
```
```tool
{"tool": "add_note", "args": {"note": "Found a hidden passage under the temple"}}
```
```tool
{"tool": "replace_note", "args": {"before": "Old note text", "after": "New note text"}}
```
```tool
{"tool": "world_update", "args": {"content": "# The World\n\n...full new world state..."}}
```
```tool
{"tool": "journal_update", "args": {"add": ["Investigate the mine"], "done": ["Defeat the demon"]}}
```
```tool
{"tool": "finalize_turn", "args": {"user_prompt": "What do you do?", "ambience": "dungeon"}}
```
"""
system += PROSE_PROMPT.substitute(
character=self._read_file(CHAR_PATH) or "*No character sheet.*",
world=self._truncate_world(self._read_file(WORLD_PATH) or "") or "*No world state.*",
log=self._read_recent_log(),
story=self._read_recent_book(),
)
user = self.build_user_message(
player_action=player_action,
last_prompt=last_prompt,
)
user += f"\n\n*A die is cast: **{die_roll}** (1d6).*"
start_time = datetime.now()
self._set_llm_env()
self._append_llm_log(f"\n[TOOL] Single call — {len(system)} chars system, {len(user)} chars user")
self._append_llm_log(f"System preview: {system.split('\n')[0][:80]}...")
self._append_llm_log(f"User preview: {user.split('\n')[0][:80]}...")
text = self._call_llm(
[{"role": "system", "content": system},
{"role": "user", "content": user}],
label="Single tool call",
max_tokens=4096,
on_debug=on_debug,
)
total_elapsed = (datetime.now() - start_time).total_seconds() * 1000
self._append_llm_log(f"\n[TOOL] got {len(text)} chars in {total_elapsed:.1f}ms")
if not text or not text.strip():
return TurnResult(error="Single tool call returned empty response")
raw = text.strip()
book_log = ""
changes_block = ""
log_entry = None
user_prompt = self._auto_prompt("")
ambience = None
tool_calls = []
# Extract tool blocks — ignore everything outside them
import re
tool_pattern = r"```tool\s*\n?(.*?)\n?```"
matches = re.findall(tool_pattern, text, re.DOTALL)
if matches:
for block in matches:
block = block.strip()
try:
tc = json.loads(block)
tool_calls.append(tc)
name = tc.get("tool", "unknown")
args = tc.get("args", {})
self._append_llm_log(f"\n[EXTRACT] {name}: {json.dumps(args)[:100]}")
if name == "narrative":
book_log = args.get("text", book_log)
elif name == "finalize_turn":
if args.get("user_prompt"):
user_prompt = args["user_prompt"]
if args.get("ambience"):
ambience = args["ambience"]
except json.JSONDecodeError as e:
self._append_llm_log(f"\n[EXTRACT] bad JSON: {e}")
continue
# Generate log entry from narrative (first sentence, trimmed)
log_entry = None
if book_log:
clean = re.sub(r'\s+', ' ', book_log).strip()
first_sentence = re.split(r'(?<=[.!?])\s+', clean)
if first_sentence:
log_entry = first_sentence[0].strip()[:200]
else:
log_entry = clean[:200]
self._append_llm_log(f"\n[SUMMARY] \"{log_entry}\"")
# Apply changes (exclude narrative and finalize_turn)
extr_start = datetime.now()
changes = []
phase3_errors = []
for tc in tool_calls:
name = tc.get("tool", "unknown")
args = tc.get("args", {})
if name in ("finalize_turn", "narrative"):
continue
result = self._execute_tool(name, args)
if result.startswith("**Error:") or result.startswith("Tool error") or result.startswith("Unknown"):
phase3_errors.append(f"{name}: {result}")
else:
desc = self._describe_change(name, args)
if desc:
changes.append(desc)
apply_elapsed = (datetime.now() - extr_start).total_seconds() * 1000
self._append_llm_log(f"\n[APPLY] {len(changes)} changes in {apply_elapsed:.1f}ms")
else:
# No tool blocks found — fallback to book_log and apply changes
self._append_llm_log(f"\n[TOOL] no tool blocks found")
tool_calls = []
changes = []
phase3_errors = []
elapsed = (datetime.now() - start_time).total_seconds() * 1000
# ── Finalize ──────────────────────────────────────────────────────
if on_action:
on_action("Turn complete")
if on_debug:
applied = len([tc for tc in tool_calls if tc.get("tool") != "finalize_turn"])
on_debug("phase_done", {
"book_log_chars": len(book_log),
"log_entry": log_entry,
"user_prompt": user_prompt,
"ambience": ambience,
"extract_errors": phase3_errors or None,
"total_elapsed_ms": elapsed,
"tool_calls_count": len(tool_calls),
"applied_changes_count": applied,
"tool_call_results": tool_calls,
})
self._log_turn_details(
player_action=player_action or last_prompt or "",
last_prompt=last_prompt or "",
strategy_name=strategy_name,
die_roll=die_roll,
model=self.model,
temperature=self.temperature,
max_tokens=self.max_tokens,
book_log=book_log,
log_entry=log_entry or "",
ambience=ambience,
tool_calls=tool_calls,
on_debug=on_debug,
)
return TurnResult(
book_log=book_log,
log_entry=log_entry,
user_prompt=user_prompt,
ambience=ambience,
debug_info="; ".join(phase3_errors) if phase3_errors else "",
changes=changes,
)
@staticmethod
def _strip_tool_blocks(text: str) -> str:
"""Remove ```tool, ```json, finalize_turn blocks from narrative text."""
@@ -1370,6 +1599,80 @@ class GameEngine:
with open(log_path, "a") as f:
f.write(entry.strip() + "\n")
def _log_turn_details(
self,
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:
"""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"])
self._append_llm_log("")
self._append_llm_log(
f"┌─ Turn Details — {ts}"
)
self._append_llm_log(
f"├─ Input: {player_action}"
)
self._append_llm_log(
f"├─ Last Prompt: {last_prompt}"
)
self._append_llm_log(
f"├─ Strategy: {strategy_name}"
)
self._append_llm_log(
f"├─ Dice: {die_roll} (1d6)"
)
self._append_llm_log(
f"├─ Model: {model} | Temp: {temperature} | Tokens: {max_tokens}"
)
self._append_llm_log(
f"├─ Output: {output_chars} chars ({output_words} words)"
)
self._append_llm_log(
f"├─ Log Entry: {log_entry}"
)
self._append_llm_log(
f"├─ Ambience: {ambience or 'None'}"
)
tools_preview = ", ".join(tc.get("tool", "?") for tc in tool_calls)
self._append_llm_log(
f"├─ Tool Calls: {len(tool_calls)} ({tools_preview})"
)
self._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,
})
def _append_llm_log(self, text: str) -> None:
"""Append raw LLM activity to llm.log for debugging."""
LLM_LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
@@ -1458,20 +1761,18 @@ def main():
args = parser.parse_args()
engine = GameEngine()
result = engine.generate(
result = engine.generate_with_tools_single(
player_action=args.action,
last_narrative=args.last,
last_prompt=args.last,
)
if result.error:
print(f"ERROR: {result.error}", file=sys.stderr)
sys.exit(1)
print(result.narrative)
if result.choices:
print("\n--- Choices ---")
for c in result.choices:
print(f" [{c}]")
print(result.book_log)
if result.user_prompt:
print(f"\n{result.user_prompt}")
if result.log_entry:
print(f"\n[Log] {result.log_entry}")
if result.ambience:
+230
View File
@@ -0,0 +1,230 @@
def generate_with_tools_single(
self,
player_action: str | None = None,
last_prompt: str | None = None,
on_thought: callable = None,
on_action: callable = None,
on_player_roll: callable = None,
on_debug: callable = None,
) -> TurnResult:
"""
Single-call generation using tools.
Uses a single LLM call with all tools available — LLM outputs
narrative + tool blocks in one go. No retry loop.
"""
from datetime import datetime
self._append_llm_log(f"\n{'='*60}")
self._append_llm_log(f"=== Turn — {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ===")
self._append_llm_log(f"{'='*60}")
if player_action:
self._append_llm_log(f"Player: {player_action}")
elif last_prompt:
self._append_llm_log(f"Resume from: {last_prompt[:120]}")
strategy_name = "tools"
if on_action:
on_action(f"LLM: {self.model} | temp={self.temperature} | tokens={self.max_tokens} | strategy={strategy_name}")
if on_debug:
on_debug("config", {"model": self.model, "temperature": self.temperature, "max_tokens": self.max_tokens, "strategy": strategy_name})
die_roll = random.randint(1, 6)
self._append_llm_log(f"Dice: {die_roll} (1d6)")
# Build system prompt that instructs LLM to use tools for changes
system = """You are an RPG dungeon master. The player just took an action.
Narrate the outcome in engaging, vivid prose. Use tools for any mechanics (rolls, damage, state changes). Only use ```tool blocks — no prose output.
Use these tools to perform every action. Wrap each in its own ```tool block:
```tool
{"tool": "modify_vitals", "args": {"current_hp": 5, "cash": 45}}
```
```tool
{"tool": "modify_traits", "args": {"dex": 15}}
```
```tool
{"tool": "add_to_inventory", "args": {"item": "Silver key"}}
```
```tool
{"tool": "remove_from_inventory", "args": {"item": "Torches (10)"}}
```
```tool
{"tool": "replace_gear", "args": {"before": "Mace (1d6+1)", "after": "Mace (1d6+2, sharpened)"}}
```
```tool
{"tool": "add_note", "args": {"note": "Found a hidden passage under the temple"}}
```
```tool
{"tool": "replace_note", "args": {"before": "Old note text", "after": "New note text"}}
```
```tool
{"tool": "world_update", "args": {"content": "# The World\n\n...full new world state..."}}
```
```tool
{"tool": "journal_update", "args": {"add": ["Investigate the mine"], "done": ["Defeat the demon"]}}
```
```tool
{"tool": "finalize_turn", "args": {"user_prompt": "What do you do?", "ambience": "dungeon"}}
```
"""
system += PROSE_PROMPT.substitute(
character=self._read_file(CHAR_PATH) or "*No character sheet.*",
world=self._truncate_world(self._read_file(WORLD_PATH) or "") or "*No world state.*",
log=self._read_recent_log(),
story=self._read_recent_book(),
)
user = self.build_user_message(
player_action=player_action,
last_prompt=last_prompt,
)
user += f"\n\n*A die is cast: **{die_roll}** (1d6).*"
start_time = datetime.now()
self._set_llm_env()
self._append_llm_log(f"\n[TOOL] Single call — {len(system)} chars system, {len(user)} chars user")
self._append_llm_log(f"System preview: {system.split('\n')[0][:80]}...")
self._append_llm_log(f"User preview: {user.split('\n')[0][:80]}...")
text = self._call_llm(
[{"role": "system", "content": system},
{"role": "user", "content": user}],
label="Single tool call",
max_tokens=4096,
on_debug=on_debug,
)
total_elapsed = (datetime.now() - start_time).total_seconds() * 1000
self._append_llm_log(f"\n[TOOL] got {len(text)} chars in {total_elapsed:.1f}ms")
if not text or not text.strip():
return TurnResult(error="Single tool call returned empty response")
raw = text.strip()
book_log = ""
changes_block = ""
log_entry = None
user_prompt = self._auto_prompt("")
ambience = None
tool_calls = []
# Extract tool blocks
import re
tool_pattern = r"```tool\s*\n?(.*?)\n?```"
matches = re.findall(tool_pattern, text, re.DOTALL)
if matches:
for block in matches:
block = block.strip()
if '"tool": "finalize_turn"' in block:
continue
try:
tc = json.loads(block)
tool_calls.append(tc)
name = tc.get("tool", "unknown")
args = tc.get("args", {})
self._append_llm_log(f"\n[EXTRACT] {name}: {json.dumps(args)[:100]}")
except json.JSONDecodeError as e:
self._append_llm_log(f"\n[EXTRACT] bad JSON: {e}")
continue
# Separate narrative and changes
parts = raw.split("### Changes", 1)
if len(parts) == 2:
book_log = parts[0].strip()
changes_block = "### Changes" + parts[1]
else:
book_log = raw
# Try to extract log entry and user prompt from finalize_turn
for tc in tool_calls:
if tc.get("tool") == "finalize_turn":
if tc.get("args", {}).get("user_prompt"):
user_prompt = tc["args"]["user_prompt"]
if tc.get("args", {}).get("ambience"):
ambience = tc["args"]["ambience"]
break
# Summarize
sum_start = datetime.now()
sum_text = self._call_llm([
{"role": "user", "content": f"Summarize this story into one log line:\n\n{book_log}"}],
label="Summarize",
max_tokens=256,
on_debug=on_debug,
)
sum_elapsed = (datetime.now() - sum_start).total_seconds() * 1000
if sum_text:
log_entry = sum_text.strip()
self._append_llm_log(f"\n[SUMMARY] \"{log_entry}\" in {sum_elapsed:.1f}ms")
# Apply changes
extr_start = datetime.now()
changes = []
phase3_errors = []
for tc in tool_calls:
name = tc.get("tool", "unknown")
args = tc.get("args", {})
if name == "finalize_turn":
continue
result = self._execute_tool(name, args)
if result.startswith("**Error:") or result.startswith("Tool error") or result.startswith("Unknown"):
phase3_errors.append(f"{name}: {result}")
else:
desc = self._describe_change(name, args)
if desc:
changes.append(desc)
apply_elapsed = (datetime.now() - extr_start).total_seconds() * 1000
self._append_llm_log(f"\n[APPLY] {len(changes)} changes in {apply_elapsed:.1f}ms")
else:
# No tool blocks found — fallback to book_log and apply changes
self._append_llm_log(f"\n[TOOL] no tool blocks found")
tool_calls = []
changes = []
phase3_errors = []
elapsed = (datetime.now() - start_time).total_seconds() * 1000
# ── Finalize ──────────────────────────────────────────────────────
if on_action:
on_action("Turn complete")
if on_debug:
applied = len([tc for tc in tool_calls if tc.get("tool") != "finalize_turn"])
on_debug("phase_done", {
"book_log_chars": len(book_log),
"log_entry": log_entry,
"user_prompt": user_prompt,
"ambience": ambience,
"extract_errors": phase3_errors or None,
"total_elapsed_ms": elapsed,
"tool_calls_count": len(tool_calls),
"applied_changes_count": applied,
"tool_call_results": tool_calls,
})
self._log_turn_details(
player_action=player_action or last_prompt or "",
last_prompt=last_prompt or "",
strategy_name=strategy_name,
die_roll=die_roll,
model=self.model,
temperature=self.temperature,
max_tokens=self.max_tokens,
book_log=book_log,
log_entry=log_entry or "",
ambience=ambience,
tool_calls=tool_calls,
on_debug=on_debug,
)
return TurnResult(
book_log=book_log,
log_entry=log_entry,
user_prompt=user_prompt,
ambience=ambience,
debug_info="; ".join(phase3_errors) if phase3_errors else "",
changes=changes,
)
+86 -35
View File
@@ -24,7 +24,7 @@ from rich.markdown import Markdown as RichMarkdown
from rich.theme import Theme
# ── Game engine ─────────────────────────────────────────
from engine import GameEngine, GenerationResult, TurnResult
from engine import GameEngine, GenerationResult, TurnResult, LLM_LOG_PATH
# ── Optional miniaudio ────────────────────────────────────
try:
@@ -81,41 +81,28 @@ def _populate_if_empty():
content = LOG_PATH.read_text().strip()
if content and len(content.splitlines()) > 2:
return
prev = _previous_log()
if prev:
lines = prev.read_text().splitlines()
lines[0] = f"# Session Log — {TODAY}"
LOG_PATH.write_text('\n'.join(lines) + '\n')
def clear_llm_log():
"""Clear llm.log at start of app."""
LLM_LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
LLM_LOG_PATH.write_text("")
def _previous_log():
entries = sorted(LOG_DIR.glob('*.md'))
today_name = LOG_PATH.name
for e in reversed(entries):
if e.name != today_name:
return e
return None
def read_todo():
"""Read TODO items from journal.md."""
if not JOURNAL_PATH.exists():
return ["—— No journal yet ——"]
lines = JOURNAL_PATH.read_text().splitlines()
in_todo = False
todo = []
for l in lines:
if l.strip().lstrip('#').strip().startswith('TODO'):
in_todo = True
continue
if l.strip().startswith('#') and in_todo:
break
if in_todo and l.strip():
todo.append(l.strip().lstrip('- '))
return todo or ["—— All done! ——"]
lines = LOG_PATH.read_text().splitlines()
return [l for l in lines if l.strip() and not l.startswith('#')][-n:]
def read_log_tail(n=200):
"""Read the tail of the session log."""
if not LOG_PATH.exists():
return []
lines = LOG_PATH.read_text().splitlines()
return [l for l in lines if l.strip() and not l.startswith('#')][-n:]
return [l for l in lines if l.strip() and not l.startswith("#")][-n:]
def status_summary():
if not CHAR_PATH.exists():
@@ -346,6 +333,8 @@ class AutoStatic(Static):
raise NotImplementedError
def on_mount(self):
clear_llm_log()
ensure_log()
self.load()
self.set_interval(REFRESH_SECS, self.load)
@@ -710,6 +699,7 @@ class ChaosTUI(App):
yield Button("♫", id="mute-btn", classes="mute-button")
def on_mount(self):
clear_llm_log()
ensure_log()
self.console._theme = MARKDOWN_THEME
self._init_book()
@@ -810,7 +800,7 @@ class ChaosTUI(App):
t.start()
def _run_generation(self, player_action: str | None) -> None:
"""Worker thread: calls engine.generate_with_tools() and posts result back."""
"""Worker thread: calls engine.generate_with_tools() or generate_with_tools_single() based on configured strategy."""
import traceback
last_prompt = self._last_prompt if self._last_prompt else None
@@ -824,14 +814,25 @@ class ChaosTUI(App):
self.call_from_thread(self._on_debug, event_type, data)
try:
result = self.engine.generate_with_tools(
player_action=player_action,
last_prompt=last_prompt,
on_thought=on_thought,
on_action=on_action,
on_player_roll=self._on_player_roll,
on_debug=on_debug,
)
strategy = self.engine.config.get("llm", {}).get("strategy", "tools")
if strategy == "tools":
result = self.engine.generate_with_tools_single(
player_action=player_action,
last_prompt=last_prompt,
on_thought=on_thought,
on_action=on_action,
on_player_roll=self._on_player_roll,
on_debug=on_debug,
)
else:
result = self.engine.generate_with_tools(
player_action=player_action,
last_prompt=last_prompt,
on_thought=on_thought,
on_action=on_action,
on_player_roll=self._on_player_roll,
on_debug=on_debug,
)
except Exception as e:
tb = traceback.format_exc()
self.call_from_thread(self._on_generation_error, e, tb)
@@ -939,6 +940,12 @@ class ChaosTUI(App):
self._append_debug(f" ambience: {data['ambience']}")
if data.get("extract_errors"):
self._append_debug(f" extract errors: {data['extract_errors']}")
elif event_type == "config":
model = data.get("model", "?")
temp = data.get("temperature", "?")
tokens = data.get("max_tokens", "?")
strat = data.get("strategy", "?")
self._append_debug(f"▸ LLM: {model} | temp={temp} | max_tokens={tokens} | strategy={strat}")
elif event_type == "tool_call":
tool = data.get("tool", "?")
args = data.get("args", {})
@@ -953,6 +960,32 @@ class ChaosTUI(App):
label = data.get("label", "")
err = data.get("error", "")
self._append_debug(f" ✖ LLM error [{label}]: {err}")
elif event_type == "turn_details":
ts = data.get("timestamp", "")
model = data.get("model", "?")
temp = data.get("temperature", "?")
tokens = data.get("max_tokens", "?")
strat = data.get("strategy_name", "?")
dice = data.get("die_roll", "?")
input_chars = len(data.get("player_action", ""))
output_chars = data.get("book_log_chars", 0)
words = data.get("book_log_words", 0)
ambience = data.get("ambience", "None")
tool_count = data.get("tool_calls_count", 0)
applied = data.get("applied_changes_count", 0)
total_ms = data.get("total_elapsed_ms", 0)
apply_ms = data.get("apply_elapsed_ms", 0)
self._append_debug(f" ━━━ Turn ━━━ {ts}")
self._append_debug(f" LLM: {model} | temp={temp} | tokens={tokens} | strategy={strat}")
self._append_debug(f" Dice: {dice} | Input: {input_chars} chars | Output: {output_chars} chars ({words} words)")
self._append_debug(f" Ambience: {ambience}")
self._append_debug(f" Tools: {tool_count} (applied: {applied})")
self._append_debug(f" Time: {total_ms:.0f}ms total — {apply_ms:.0f}ms apply")
self._append_debug(f" ━━━ Details ━━━")
for tc in data.get("tool_call_results", []):
tool = tc.get("tool", "?")
args = tc.get("args", {})
self._append_debug(f" 🔧 {tool}: {json.dumps(args)[:120]}")
def _on_player_roll(self, dice: str, reason: str) -> str:
"""Called from worker thread. Shows roll popup, blocks until player responds."""
@@ -1159,6 +1192,24 @@ class ChaosTUI(App):
self._save_settings()
def read_todo():
"""Read TODO items from journal.md."""
if not JOURNAL_PATH.exists():
return ["—— No journal yet ——"]
lines = JOURNAL_PATH.read_text().splitlines()
in_todo = False
todo = []
for l in lines:
if l.strip().lstrip("#").strip().startswith("TODO"):
in_todo = True
continue
if l.strip().startswith("#") and in_todo:
break
if in_todo and l.strip():
todo.append(l.strip().lstrip("- "))
return todo or ["—— All done! ——"]
def main():
import argparse
parser = argparse.ArgumentParser(description="The Chaos TUI")
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""Test that all module imports work correctly."""
import sys
import os
import ast
def check_missing_imports():
"""Check for missing imports that would cause NameError."""
errors = []
# Check engine.py
engine_path = os.path.join(os.path.dirname(__file__), 'engine.py')
with open(engine_path, 'r') as f:
engine_content = f.read()
# Parse the file to find all names used
tree = ast.parse(engine_content)
# Collect all names that are used (not defined)
names_used = set()
for node in ast.walk(tree):
if isinstance(node, ast.Name):
names_used.add(node.id)
# Check for common missing imports
common_modules = {
'random',
're',
'json',
'traceback',
'datetime',
'time',
'os',
'sys',
'pathlib',
'functools',
'collections',
'typing',
'io',
'string',
}
for module in common_modules:
if module in names_used and not hasattr(sys.modules.get(module, None), '__file__'):
# Check if it's used but not imported
if f'import {module}' not in engine_content and f'from {module} import' not in engine_content:
errors.append(f"Missing import: {module}")
return errors
if __name__ == '__main__':
errors = check_missing_imports()
if errors:
print("ERROR: Missing imports detected:")
for error in errors:
print(f" - {error}")
sys.exit(1)
else:
print("✓ All imports present")
sys.exit(0)
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""Test that the engine module can be imported without errors."""
import sys
import os
import traceback
def test_engine_import():
"""Test that the engine module imports without errors."""
errors = []
try:
# Add the tools directory to the path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# Import the engine module
import engine
print(f"✓ Engine module imported successfully")
# Check for common runtime errors
if not hasattr(engine, 'GameEngine'):
errors.append("GameEngine class not found")
else:
print(f"✓ GameEngine class found")
# Check that generate_with_tools_single exists
if hasattr(engine.GameEngine, 'generate_with_tools_single'):
print(f"✓ generate_with_tools_single method found")
else:
errors.append("generate_with_tools_single method not found")
except ImportError as e:
errors.append(f"Import error: {e}")
except AttributeError as e:
errors.append(f"Attribute error: {e}")
except Exception as e:
errors.append(f"Unexpected error: {e}\n{traceback.format_exc()}")
return errors
if __name__ == '__main__':
errors = test_engine_import()
if errors:
print("ERROR: Runtime errors detected:")
for error in errors:
print(f" - {error}")
sys.exit(1)
else:
sys.exit(0)