diff --git a/tools/engine.py b/tools/engine.py index 7fd4013..8db68cd 100644 --- a/tools/engine.py +++ b/tools/engine.py @@ -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: diff --git a/tools/engine.py.tmp b/tools/engine.py.tmp deleted file mode 100644 index 8c9fe42..0000000 --- a/tools/engine.py.tmp +++ /dev/null @@ -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, - ) - diff --git a/tools/engine_lib/prompts.py b/tools/engine_lib/prompts.py index a7ce7f1..0dc0547 100644 --- a/tools/engine_lib/prompts.py +++ b/tools/engine_lib/prompts.py @@ -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. diff --git a/tools/engine_lib/tools_handler.py b/tools/engine_lib/tools_handler.py index 832c33c..aa4471f 100644 --- a/tools/engine_lib/tools_handler.py +++ b/tools/engine_lib/tools_handler.py @@ -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"}}, } diff --git a/tools/engine_lib/validation.py b/tools/engine_lib/validation.py index bbc06c2..66fbe57 100644 --- a/tools/engine_lib/validation.py +++ b/tools/engine_lib/validation.py @@ -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,