Simplify, improve validations

This commit is contained in:
Dejvino 2026-07-01 21:42:05 +02:00
parent 433be9a4a4
commit c5c40225a3
5 changed files with 6 additions and 238 deletions

View File

@ -10,7 +10,7 @@ from pathlib import Path
from engine_lib.models import TurnResult
from engine_lib import config
from engine_lib.context import build_system_prompt
from engine_lib.validation import validate_action, auto_prompt
from engine_lib.validation import validate_action
from engine_lib.tools_handler import execute_tool, describe_change, extract_tool_calls
from engine_lib.parsing import log_turn_details
from engine_lib import state
@ -61,7 +61,7 @@ class GameEngine:
return TurnResult(
book_log="",
log_entry=f"You can't do that — {reason}.",
user_prompt=f"*Your action \"{player_action}\" was rejected: {reason}*",
user_prompt=f"*Your action:\n\t\"{player_action}\"\nwas rejected:\n\t{reason}*",
)
system = build_system_prompt()
@ -105,7 +105,7 @@ class GameEngine:
book_log = ""
log_entry = None
user_prompt = auto_prompt("")
user_prompt = ""
ambience = None
changes: list[str] = []
errors: list[str] = []
@ -121,8 +121,6 @@ class GameEngine:
if text:
book_log = (book_log + "\n\n" + text) if book_log else text
elif name == "finalize_turn":
if args.get("user_prompt"):
user_prompt = args["user_prompt"]
if args.get("ambience"):
ambience = args["ambience"]
elif name == "player_roll" and on_player_roll:

View File

@ -1,230 +0,0 @@
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,
)

View File

@ -53,7 +53,7 @@ Wrap each action in its own ```tool block:
{"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"}}
{"tool": "finalize_turn", "args": {"ambience": "dungeon"}}
```
You are the sole authority over the game state. The player's action is a **proposal**, not a fact. If their action contradicts the character sheet (e.g. using an item they don't have, spending cash they don't have, claiming stats they don't have), narrate the failure and do NOT call any state-changing tools.

View File

@ -20,7 +20,7 @@ TOOL_REGISTRY: dict[str, dict] = {
"replace_note": {"description": "Replace note by exact match.", "args": {"before": "exact text", "after": "new text"}},
"world_update": {"description": "Replace world state.", "args": {"content": "full world markdown"}},
"journal_update": {"description": "Update TODO/DONE.", "args": {"add": "[...]", "done": "[...]"}},
"finalize_turn": {"description": "End turn.", "args": {"user_prompt": "question for player", "ambience": "soundscape name"}},
"finalize_turn": {"description": "End turn.", "args": {"ambience": "soundscape name"}},
}

View File

@ -66,7 +66,7 @@ def validate_action(
text = call_llm(
[{"role": "user", "content": prompt}],
max_tokens=512,
max_tokens=1024,
temperature=0.2,
label="Action validation",
on_debug=on_debug,