Simplify, improve validations
This commit is contained in:
parent
433be9a4a4
commit
c5c40225a3
@ -10,7 +10,7 @@ from pathlib import Path
|
|||||||
from engine_lib.models import TurnResult
|
from engine_lib.models import TurnResult
|
||||||
from engine_lib import config
|
from engine_lib import config
|
||||||
from engine_lib.context import build_system_prompt
|
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.tools_handler import execute_tool, describe_change, extract_tool_calls
|
||||||
from engine_lib.parsing import log_turn_details
|
from engine_lib.parsing import log_turn_details
|
||||||
from engine_lib import state
|
from engine_lib import state
|
||||||
@ -61,7 +61,7 @@ class GameEngine:
|
|||||||
return TurnResult(
|
return TurnResult(
|
||||||
book_log="",
|
book_log="",
|
||||||
log_entry=f"You can't do that — {reason}.",
|
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()
|
system = build_system_prompt()
|
||||||
@ -105,7 +105,7 @@ class GameEngine:
|
|||||||
|
|
||||||
book_log = ""
|
book_log = ""
|
||||||
log_entry = None
|
log_entry = None
|
||||||
user_prompt = auto_prompt("")
|
user_prompt = ""
|
||||||
ambience = None
|
ambience = None
|
||||||
changes: list[str] = []
|
changes: list[str] = []
|
||||||
errors: list[str] = []
|
errors: list[str] = []
|
||||||
@ -121,8 +121,6 @@ class GameEngine:
|
|||||||
if text:
|
if text:
|
||||||
book_log = (book_log + "\n\n" + text) if book_log else text
|
book_log = (book_log + "\n\n" + text) if book_log else text
|
||||||
elif name == "finalize_turn":
|
elif name == "finalize_turn":
|
||||||
if args.get("user_prompt"):
|
|
||||||
user_prompt = args["user_prompt"]
|
|
||||||
if args.get("ambience"):
|
if args.get("ambience"):
|
||||||
ambience = args["ambience"]
|
ambience = args["ambience"]
|
||||||
elif name == "player_roll" and on_player_roll:
|
elif name == "player_roll" and on_player_roll:
|
||||||
|
|||||||
@ -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,
|
|
||||||
)
|
|
||||||
|
|
||||||
@ -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": "journal_update", "args": {"add": ["Investigate the mine"], "done": ["Defeat the demon"]}}
|
||||||
```
|
```
|
||||||
```tool
|
```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.
|
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.
|
||||||
|
|||||||
@ -20,7 +20,7 @@ TOOL_REGISTRY: dict[str, dict] = {
|
|||||||
"replace_note": {"description": "Replace note by exact match.", "args": {"before": "exact text", "after": "new text"}},
|
"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"}},
|
"world_update": {"description": "Replace world state.", "args": {"content": "full world markdown"}},
|
||||||
"journal_update": {"description": "Update TODO/DONE.", "args": {"add": "[...]", "done": "[...]"}},
|
"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"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -66,7 +66,7 @@ def validate_action(
|
|||||||
|
|
||||||
text = call_llm(
|
text = call_llm(
|
||||||
[{"role": "user", "content": prompt}],
|
[{"role": "user", "content": prompt}],
|
||||||
max_tokens=512,
|
max_tokens=1024,
|
||||||
temperature=0.2,
|
temperature=0.2,
|
||||||
label="Action validation",
|
label="Action validation",
|
||||||
on_debug=on_debug,
|
on_debug=on_debug,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user