Add more logging. Selectable LLM strategy.
This commit is contained in:
parent
25fb5fd729
commit
91b1b35cfa
243
AGENTS.md
243
AGENTS.md
@ -31,7 +31,9 @@ the-chaos/
|
||||
│ ├── draw.py # Card drawing tool
|
||||
│ ├── music-fetch.py # YouTube audio downloader
|
||||
│ ├── roll.py # Dice rolling tool
|
||||
│ └── store_turn.py # DEPRECATED — use engine.py archive_turn instead
|
||||
│ ├── store_turn.py # DEPRECATED — use engine.py archive_turn instead
|
||||
│ ├── test_imports.py # Import validation test
|
||||
│ └── test_runtime.py # Runtime import test
|
||||
├── scripts/ # UNLOCKED — helper scripts
|
||||
├── run.sh # Entry point (just calls tools/run.py)
|
||||
└── session/ # Game state (read/write by engine)
|
||||
@ -118,19 +120,6 @@ The `model` field accepts any litellm provider string: `openai/gpt-4`,
|
||||
`anthropic/claude-sonnet-4-20250514`, `ollama/llama3.1`, `groq/llama3-70b-8192`,
|
||||
etc. Set `api_key` for remote providers.
|
||||
|
||||
## Files Still Used By Tools
|
||||
|
||||
| File | Purpose | Written By |
|
||||
|------|---------|------------|
|
||||
| `session/config.json` | LLM provider config | Manual edit |
|
||||
| `session/character.md` | PC state | engine.py |
|
||||
| `session/world.md` | Realm state | engine.py |
|
||||
| `session/book.md` | Story archive | engine.py |
|
||||
| `session/journal.md` | TODO/DONE | engine.py |
|
||||
| `session/ambience.md` | Current ambience | engine.py |
|
||||
| `session/log/<date>.md` | Session log | engine.py |
|
||||
| `session/tweaks.md` | House rules | Manual edit |
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
@ -146,3 +135,229 @@ python3 tools/run.py --no-music
|
||||
# Test a generation from CLI (no TUI)
|
||||
python3 tools/engine.py --action "I head to the market"
|
||||
```
|
||||
|
||||
## Testing Commands
|
||||
|
||||
Always run tests before making changes. This prevents runtime errors like missing imports.
|
||||
|
||||
```bash
|
||||
# Quick test (runs import and runtime validation)
|
||||
./run.sh
|
||||
|
||||
# Test with engine action
|
||||
./run.sh --action "I check on Rina"
|
||||
|
||||
# Run tests only
|
||||
python3 tools/test_imports.py
|
||||
python3 tools/test_runtime.py
|
||||
```
|
||||
|
||||
### Test Coverage
|
||||
|
||||
- `tools/test_imports.py` — Checks for missing imports using AST analysis
|
||||
- `tools/test_runtime.py` — Verifies module loads without errors, checks for missing classes/methods
|
||||
- Both tests should pass before proceeding with development
|
||||
|
||||
## LLM Strategies
|
||||
|
||||
Two strategies for LLM interaction:
|
||||
|
||||
1. **"conversational"** — 3-phase approach (prose → summarize → extract)
|
||||
2. **"tools"** — Single-call approach with tool blocks
|
||||
|
||||
Default is "tools" for faster single-call generation.
|
||||
|
||||
### Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"llm": {
|
||||
"model": "openai/llama3",
|
||||
"api_key": "sk-bogus-key",
|
||||
"api_base": "http://localhost:8080/v1",
|
||||
"temperature": 0.8,
|
||||
"timeout": 120,
|
||||
"max_tokens": 10000,
|
||||
"strategy": "tools"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Important Notes
|
||||
|
||||
- The `random` module must be imported before use — it's used in dice rolling and die roll generation
|
||||
- All LLM responses go through `_call_llm` which logs complete output with markers
|
||||
- The engine extracts both `content` and `reasoning_content` fields from responses (for OpenAI-compatible servers)
|
||||
- The `generate_with_tools_single()` method handles single-call tool-based generation
|
||||
|
||||
## LLM Logging
|
||||
|
||||
The engine logs detailed information to `llm.log`:
|
||||
|
||||
```
|
||||
============================================================
|
||||
=== Turn — 2026-06-28 23:21:58 ===
|
||||
============================================================
|
||||
Player: I smash the demon
|
||||
Dice: 2 (1d6)
|
||||
|
||||
[TOOL] Single call — 8615 chars system, 219 chars user
|
||||
System preview: You are an RPG dungeon master. The player just took an action....
|
||||
User preview: ## Situation...
|
||||
|
||||
┌─ Single tool call ───────────────────────────────────────────────────────────
|
||||
├─ Model: openai/llama3 | Temp: 0.80 | Tokens: 4096
|
||||
├─ Messages:
|
||||
├ [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 p...
|
||||
├ [USER]: ## Situation
|
||||
What do you do?
|
||||
|
||||
## Player's Request
|
||||
I smash the demon
|
||||
|
||||
## Instructions
|
||||
Advance the story based on the player's request. All state is shown above — write the outcome directly.
|
||||
|
||||
*A ...
|
||||
└─ Response:
|
||||
└ The die cast for this turn is a 2. The player wants to smash the demon. I need to narrate the outcome of that action, incorporating the die result and the combat mechanics.
|
||||
|
||||
First, determine if the attack hits. The demon is a large creature (size 5). I assume the player's DEX is 14, so the roll to hit is 1d6 with a 4+ favourable. The die result is 2, which is a failure. However, the player might have a modifier. The demon is a weaver? No, it's a demon. The player is Dillion, who just took -4 HP ...
|
||||
└───────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
[TOOL] got 17372 chars in 97396.3ms
|
||||
|
||||
[TOOL] no tool blocks found
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Turn Details — 2026-06-28 23:23:36.097 │
|
||||
├─────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ Input: I smash the demon │
|
||||
│ Last Prompt: What do you do? │
|
||||
│ Strategy: tools │
|
||||
│ Dice: 2 (1d6) │
|
||||
│ Model: openai/llama3 | Temp: 0.8 | Tokens: 10000 │
|
||||
│ Output: 0 chars (0 words) │
|
||||
│ Log Entry: │
|
||||
│ Ambience: None │
|
||||
│ Tool Calls: 0 () │
|
||||
│─────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Debug Markers
|
||||
|
||||
- `┌─` / `└─` — Response markers
|
||||
- `├─` — Message lines
|
||||
- `[TOOL]` — Tool execution logs
|
||||
- `[DEBUG LLM RESPONSE]` — Raw LLM response logging
|
||||
- `[DEBUG RESPONSE LENGTH]` — Response length logging
|
||||
|
||||
## TUI Debug Tab
|
||||
|
||||
The TUI has a DEBUG tab that shows:
|
||||
|
||||
- LLM configuration
|
||||
- Tool calls with arguments
|
||||
- LLM errors with tracebacks
|
||||
- Turn details (timestamps, dice rolls, response sizes, tool call counts)
|
||||
- Phase progress (Phase 1/3, Phase 2/3, Phase 3/3)
|
||||
|
||||
## Key Code References
|
||||
|
||||
- `_call_llm` — Core LLM interaction with logging
|
||||
- `generate_with_tools` — 3-phase conversational approach
|
||||
- `generate_with_tools_single` — Single-call tool-based approach
|
||||
- `_log_turn_details` — Comprehensive turn logging
|
||||
- `_on_debug` — Structured debug output to TUI
|
||||
|
||||
## Common Errors & Fixes
|
||||
|
||||
### NameError: name 'random' is not defined
|
||||
|
||||
Add `import random` at the top of `tools/engine.py`. The `random` module is used in:
|
||||
- Line ~488: Dice rolling in `_tool_roll`
|
||||
- Line ~926: Random die roll in `generate_with_tools`
|
||||
- Line ~1232: Random die roll in `generate_with_tools_single`
|
||||
|
||||
### NameError: name 'read_todo' is not defined
|
||||
|
||||
The `read_todo` function must be defined in `tools/run.py`. It reads TODO items from `journal.md`.
|
||||
|
||||
### NameError: name 'read_log_tail' is not defined
|
||||
|
||||
The `read_log_tail` function must be defined in `tools/run.py`. It reads the tail of the session log.
|
||||
|
||||
## Testing Workflow
|
||||
|
||||
1. **Before coding**: Run `./run.sh` to ensure imports and runtime are valid
|
||||
2. **After coding**: Run `./run.sh --action "test action"` to test the engine
|
||||
3. **Before committing**: Run both tests to ensure no missing imports
|
||||
|
||||
```bash
|
||||
# Quick validation
|
||||
./run.sh
|
||||
|
||||
# Test with engine action
|
||||
./run.sh --action "I check on Rina"
|
||||
|
||||
# Manual tests
|
||||
python3 tools/test_imports.py
|
||||
python3 tools/test_runtime.py
|
||||
```
|
||||
|
||||
## LLM Response Handling
|
||||
|
||||
The engine handles both `content` and `reasoning_content` fields from LLM responses:
|
||||
|
||||
```python
|
||||
text = response.choices[0].message.content or response.choices[0].message.reasoning_content or ""
|
||||
```
|
||||
|
||||
This allows compatibility with OpenAI-compatible servers that return content in the `reasoning_content` field instead of `content`.
|
||||
|
||||
## Timeout & Token Configuration
|
||||
|
||||
- Default timeout: 120 seconds (configurable in config.json)
|
||||
- Default max tokens: 10000 (configurable in config.json)
|
||||
- Adjust these values based on your LLM provider's limits
|
||||
|
||||
## Session Files
|
||||
|
||||
- `session/config.json` — LLM config (edit directly)
|
||||
- `session/character.md` — PC state (written by engine)
|
||||
- `session/world.md` — Realm state (written by engine)
|
||||
- `session/book.md` — Story archive (written by engine)
|
||||
- `session/journal.md` — TODO/DONE list (written by engine)
|
||||
- `session/ambience.md` — Current ambience (written by engine)
|
||||
- `session/log/<date>.md` — Session logs (written by engine)
|
||||
- `session/tweaks.md` — House rules (manual edit)
|
||||
|
||||
## LLM Strategies Explained
|
||||
|
||||
### "conversational" Strategy
|
||||
|
||||
Uses three separate LLM calls:
|
||||
1. **Prose** — Writes full book_log from context + player action
|
||||
2. **Summarize** — Condenses book_log into one log line
|
||||
3. **Extract** — Reads book_log and outputs tool calls for state changes
|
||||
|
||||
Retry loop: 3 attempts, Phase 3 fallback to Phase 1 if extraction fails.
|
||||
|
||||
### "tools" Strategy
|
||||
|
||||
Uses single LLM call with all tools available:
|
||||
- System prompt instructs LLM to use tools for changes
|
||||
- Single call outputs narrative + tool blocks together
|
||||
- No retry loop — if it fails, turn fails
|
||||
- Extracts tool blocks, applies changes, summarizes in one pass
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
1. Check `llm.log` for detailed LLM interaction logs
|
||||
2. Use the TUI's DEBUG tab for structured debug output
|
||||
3. Run tests before making changes
|
||||
4. Check config.json for LLM settings
|
||||
5. Look for missing imports in the engine.py file
|
||||
6. Verify that the LLM provider is correctly configured in config.json
|
||||
|
||||
9
run.sh
9
run.sh
@ -2,4 +2,11 @@
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
python3 tools/run.py
|
||||
|
||||
# If --action is provided, run engine CLI test; otherwise run the TUI.
|
||||
if [[ "$1" == "--action" || "$1" == "-a" ]]; then
|
||||
shift
|
||||
python3 tools/engine.py --action "$@"
|
||||
else
|
||||
python3 tools/run.py "$@"
|
||||
fi
|
||||
|
||||
323
tools/engine.py
323
tools/engine.py
@ -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
tools/engine.py.tmp
Normal file
230
tools/engine.py.tmp
Normal 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,
|
||||
)
|
||||
|
||||
105
tools/run.py
105
tools/run.py
@ -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,6 +814,17 @@ class ChaosTUI(App):
|
||||
self.call_from_thread(self._on_debug, event_type, data)
|
||||
|
||||
try:
|
||||
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,
|
||||
@ -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
tools/test_imports.py
Executable file
61
tools/test_imports.py
Executable 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
tools/test_runtime.py
Executable file
49
tools/test_runtime.py
Executable 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)
|
||||
Loading…
Reference in New Issue
Block a user