Simpler tool calls with examples
This commit is contained in:
parent
84b53cfd0c
commit
e74dd07699
214
tools/engine.py
214
tools/engine.py
@ -420,55 +420,18 @@ class GameEngine:
|
||||
# ── Tool Infrastructure ────────────────────────────────────────────
|
||||
|
||||
TOOL_REGISTRY: dict[str, dict] = {
|
||||
"read_file": {
|
||||
"description": "Read a game state file.",
|
||||
"args": {"file": "character | world | book | log | journal"},
|
||||
},
|
||||
"roll": {
|
||||
"description": "Roll dice and return the outcome.",
|
||||
"args": {"dice": "e.g. 1d6, 2d6", "modifier": "optional +N or -N"},
|
||||
},
|
||||
"think": {
|
||||
"description": "Internal reasoning shown in the game status bar.",
|
||||
"args": {"thought": "Your reasoning."},
|
||||
},
|
||||
"player_roll": {
|
||||
"description": "Ask the player to physically roll dice and enter the result.",
|
||||
"args": {"dice": "e.g. 2d6+1", "reason": "Why the roll is needed (shown to player)"},
|
||||
},
|
||||
"character_get": {
|
||||
"description": "Read the full character sheet.",
|
||||
"args": {},
|
||||
},
|
||||
"character_update": {
|
||||
"description": "Replace the character sheet with a new full version (ONLY if HP/cash/gear/stats changed).",
|
||||
"args": {"content": "Full character sheet markdown"},
|
||||
},
|
||||
"world_get": {
|
||||
"description": "Read the full world state.",
|
||||
"args": {},
|
||||
},
|
||||
"world_update": {
|
||||
"description": "Replace the world state with a new full version (ONLY if NPCs/locations/threads changed).",
|
||||
"args": {"content": "Full world state markdown"},
|
||||
},
|
||||
"journal_get": {
|
||||
"description": "Read the journal (TODO / DONE).",
|
||||
"args": {},
|
||||
},
|
||||
"journal_update": {
|
||||
"description": "Add or complete journal entries.",
|
||||
"args": {"add": "Optional: list of new TODO items", "done": "Optional: list of completed items"},
|
||||
},
|
||||
"finalize_turn": {
|
||||
"description": "Complete the turn.",
|
||||
"args": {
|
||||
"book_log": "[Required] Full narrative — appended to story book (permanent record)",
|
||||
"user_prompt": "[Required] Short prompt for player — NOT recorded, 1-3 sentences",
|
||||
"log_entry": "[Optional] One-sentence summary",
|
||||
"ambience": "[Optional] Soundscape name",
|
||||
},
|
||||
},
|
||||
"roll": {"description": "Roll dice.", "args": {"dice": "1d6", "modifier": "+1"}},
|
||||
"player_roll": {"description": "Ask player to roll.", "args": {"dice": "1d6", "reason": "why"}},
|
||||
"modify_traits": {"description": "Change STR/DEX/WIL.", "args": {"str": "optional", "dex": "optional", "wil": "optional"}},
|
||||
"modify_vitals": {"description": "Change HP, cash, weapon, armour.", "args": {"current_hp": "optional", "max_hp": "optional", "cash": "optional", "weapon": "optional", "armour": "optional"}},
|
||||
"add_to_inventory": {"description": "Add item to gear.", "args": {"item": "item name and stats"}},
|
||||
"remove_from_inventory": {"description": "Remove item from gear.", "args": {"item": "exact item text"}},
|
||||
"replace_gear": {"description": "Replace gear by exact match.", "args": {"before": "exact text", "after": "new text"}},
|
||||
"add_note": {"description": "Add note to sheet.", "args": {"note": "note content"}},
|
||||
"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"}},
|
||||
}
|
||||
|
||||
def _tool_think(self, args: dict) -> str:
|
||||
@ -510,20 +473,98 @@ class GameEngine:
|
||||
mod_str = f" {'+' if mod >= 0 else ''}{mod}" if mod != 0 else ""
|
||||
return f"Roll: {dice_str}{mod_str} → [{', '.join(str(r) for r in rolls)}] = {total}"
|
||||
|
||||
def _tool_character_get(self, args: dict) -> str:
|
||||
return self._read_file(CHAR_PATH) or "*Character sheet is empty.*"
|
||||
def _patch_character(self, pattern: str, repl: str, count: int = 1, flags: int = 0) -> str:
|
||||
"""Apply a regex replacement to character.md. Returns error msg or empty string."""
|
||||
text = CHAR_PATH.read_text()
|
||||
new, n = re.subn(pattern, repl, text, count=count, flags=flags)
|
||||
if n == 0:
|
||||
return f"**Error:** pattern not found:\n{pattern}"
|
||||
CHAR_PATH.write_text(new)
|
||||
return ""
|
||||
|
||||
def _tool_character_update(self, args: dict) -> str:
|
||||
content = (args or {}).get("content", "")
|
||||
if not content:
|
||||
return "**Error:** `content` is required."
|
||||
if not self._validate_update_size("character", content, CHAR_PATH):
|
||||
return "**Error:** Update rejected — content is too short (likely a partial paste)."
|
||||
CHAR_PATH.write_text(content.strip() + "\n")
|
||||
return "Character sheet updated."
|
||||
def _tool_modify_traits(self, args: dict) -> str:
|
||||
errors = []
|
||||
for stat in ("str", "dex", "wil"):
|
||||
val = args.get(stat)
|
||||
if val is not None:
|
||||
err = self._patch_character(
|
||||
rf"^(- \*\*{stat.upper()}:\*\*\s*)\d+", rf"\g<1>{val}", count=1, flags=re.MULTILINE
|
||||
)
|
||||
if err:
|
||||
errors.append(err)
|
||||
return "; ".join(errors) if errors else "Traits updated."
|
||||
|
||||
def _tool_world_get(self, args: dict) -> str:
|
||||
return self._read_file(WORLD_PATH) or "*World state is empty.*"
|
||||
def _tool_modify_vitals(self, args: dict) -> str:
|
||||
errors = []
|
||||
for field, label in [("current_hp", "Current Health"), ("max_hp", "Max Health"),
|
||||
("cash", "Cash"), ("weapon", "Weapon"), ("armour", "Armour")]:
|
||||
val = args.get(field)
|
||||
if val is not None:
|
||||
err = self._patch_character(
|
||||
rf"^(- \*\*{label}:\*\*\s*).*", rf"\g<1>{val}", count=1, flags=re.MULTILINE
|
||||
)
|
||||
if err:
|
||||
errors.append(err)
|
||||
return "; ".join(errors) if errors else "Vitals updated."
|
||||
|
||||
def _tool_add_to_inventory(self, args: dict) -> str:
|
||||
item = (args or {}).get("item", "")
|
||||
if not item:
|
||||
return "**Error:** `item` is required."
|
||||
text = CHAR_PATH.read_text()
|
||||
if item in text:
|
||||
return f"Item already in inventory: {item}"
|
||||
# Insert after last gear item or after "## Gear" header
|
||||
gear_section = re.search(r"^## Gear\n", text, re.MULTILINE)
|
||||
if gear_section:
|
||||
insert_at = gear_section.end()
|
||||
text = text[:insert_at] + f"- {item}\n" + text[insert_at:]
|
||||
else:
|
||||
text += f"\n## Gear\n- {item}\n"
|
||||
CHAR_PATH.write_text(text)
|
||||
return f"Added to inventory: {item}"
|
||||
|
||||
def _tool_remove_from_inventory(self, args: dict) -> str:
|
||||
item = (args or {}).get("item", "")
|
||||
if not item:
|
||||
return "**Error:** `item` is required."
|
||||
err = self._patch_character(rf"^- {re.escape(item)}\n?", "", count=1, flags=re.MULTILINE)
|
||||
if err:
|
||||
return f"**Error:** item not found: {item}"
|
||||
return f"Removed from inventory: {item}"
|
||||
|
||||
def _tool_replace_gear(self, args: dict) -> str:
|
||||
before = (args or {}).get("before", "")
|
||||
after = (args or {}).get("after", "")
|
||||
if not before or not after:
|
||||
return "**Error:** `before` and `after` are required."
|
||||
err = self._patch_character(rf"^- {re.escape(before)}", f"- {after}", count=1, flags=re.MULTILINE)
|
||||
if err:
|
||||
return f"**Error:** gear not found: {before}"
|
||||
return f"Gear replaced: {before} → {after}"
|
||||
|
||||
def _tool_add_note(self, args: dict) -> str:
|
||||
note = (args or {}).get("note", "")
|
||||
if not note:
|
||||
return "**Error:** `note` is required."
|
||||
text = CHAR_PATH.read_text()
|
||||
notes_section = re.search(r"^## Notes & Scribbles\n", text, re.MULTILINE)
|
||||
if notes_section:
|
||||
text = text[:notes_section.end()] + f"- {note}\n" + text[notes_section.end():]
|
||||
else:
|
||||
text += f"\n## Notes & Scribbles\n- {note}\n"
|
||||
CHAR_PATH.write_text(text)
|
||||
return f"Note added: {note}"
|
||||
|
||||
def _tool_replace_note(self, args: dict) -> str:
|
||||
before = (args or {}).get("before", "")
|
||||
after = (args or {}).get("after", "")
|
||||
if not before or not after:
|
||||
return "**Error:** `before` and `after` are required."
|
||||
err = self._patch_character(rf"^- {re.escape(before)}", f"- {after}", count=1, flags=re.MULTILINE)
|
||||
if err:
|
||||
return f"**Error:** note not found: {before}"
|
||||
return f"Note replaced."
|
||||
|
||||
def _tool_world_update(self, args: dict) -> str:
|
||||
content = (args or {}).get("content", "")
|
||||
@ -534,9 +575,6 @@ class GameEngine:
|
||||
WORLD_PATH.write_text(content.strip() + "\n")
|
||||
return "World state updated."
|
||||
|
||||
def _tool_journal_get(self, args: dict) -> str:
|
||||
return self._read_file(JOURNAL_PATH) or "*Journal is empty.*"
|
||||
|
||||
def _tool_journal_update(self, args: dict) -> str:
|
||||
add = (args or {}).get("add", [])
|
||||
done = (args or {}).get("done", [])
|
||||
@ -583,20 +621,35 @@ class GameEngine:
|
||||
elif tool_name == "player_roll":
|
||||
dice = (args or {}).get("dice", "1d6")
|
||||
desc = f"asking you to roll {dice}"
|
||||
elif tool_name == "modify_traits":
|
||||
desc = "updating traits"
|
||||
elif tool_name == "modify_vitals":
|
||||
desc = "updating vitals"
|
||||
elif tool_name == "add_to_inventory":
|
||||
desc = "adding item to inventory"
|
||||
elif tool_name == "remove_from_inventory":
|
||||
desc = "removing item from inventory"
|
||||
elif tool_name == "replace_gear":
|
||||
desc = "replacing gear"
|
||||
elif tool_name == "add_note":
|
||||
desc = "adding note"
|
||||
elif tool_name == "replace_note":
|
||||
desc = "replacing note"
|
||||
else:
|
||||
desc = f"using {tool_name}"
|
||||
return f"DM is {desc}..."
|
||||
|
||||
def _execute_tool(self, tool_name: str, args: dict) -> str:
|
||||
fn_map = {
|
||||
"read_file": self._tool_read_file,
|
||||
"roll": self._tool_roll,
|
||||
"think": self._tool_think,
|
||||
"character_get": self._tool_character_get,
|
||||
"character_update": self._tool_character_update,
|
||||
"world_get": self._tool_world_get,
|
||||
"modify_traits": self._tool_modify_traits,
|
||||
"modify_vitals": self._tool_modify_vitals,
|
||||
"add_to_inventory": self._tool_add_to_inventory,
|
||||
"remove_from_inventory": self._tool_remove_from_inventory,
|
||||
"replace_gear": self._tool_replace_gear,
|
||||
"add_note": self._tool_add_note,
|
||||
"replace_note": self._tool_replace_note,
|
||||
"world_update": self._tool_world_update,
|
||||
"journal_get": self._tool_journal_get,
|
||||
"journal_update": self._tool_journal_update,
|
||||
}
|
||||
fn = fn_map.get(tool_name)
|
||||
@ -813,17 +866,22 @@ class GameEngine:
|
||||
for attempt in range(3):
|
||||
text = self._call_llm([
|
||||
{"role": "user", "content":
|
||||
f"Read the story and compare with current state. Output tool calls for changes:\n\n"
|
||||
f"Read the story and compare with current state. Output tool calls for any changes:\n\n"
|
||||
f"## Current Character\n{current_char}\n\n"
|
||||
f"## Current World\n{current_world}\n\n"
|
||||
f"## Story\n{book_log}\n\n"
|
||||
f"Output tool blocks for changes only. Include the FULL updated content:\n"
|
||||
f"- character_update — content: full new sheet if HP/cash/gear/stats changed\n"
|
||||
f"- world_update — content: full new world if NPCs/locations/threads changed\n"
|
||||
f"- journal_update — add: [...], done: [...]\n"
|
||||
f"- finalize_turn — user_prompt (question for player), ambience (soundscape)\n\n"
|
||||
f"Wrap each in ```tool:\n"
|
||||
f"```tool\n{{\"tool\": \"character_update\", \"args\": {{\"content\": \"# Character\\n...\"}}}}\n```"}
|
||||
f"Output ```tool blocks for changes only. Examples:\n\n"
|
||||
f"```tool\n{{\"tool\": \"modify_vitals\", \"args\": {{\"current_hp\": 5, \"cash\": 45}}}}\n```\n"
|
||||
f"```tool\n{{\"tool\": \"modify_traits\", \"args\": {{\"dex\": 15}}}}\n```\n"
|
||||
f"```tool\n{{\"tool\": \"add_to_inventory\", \"args\": {{\"item\": \"Silver key\"}}}}\n```\n"
|
||||
f"```tool\n{{\"tool\": \"remove_from_inventory\", \"args\": {{\"item\": \"Torches (10)\"}}}}\n```\n"
|
||||
f"```tool\n{{\"tool\": \"replace_gear\", \"args\": {{\"before\": \"Mace (1d6+1)\", \"after\": \"Mace (1d6+2, sharpened)\"}}}}\n```\n"
|
||||
f"```tool\n{{\"tool\": \"add_note\", \"args\": {{\"note\": \"Found a hidden passage under the temple\"}}}}\n```\n"
|
||||
f"```tool\n{{\"tool\": \"replace_note\", \"args\": {{\"before\": \"Old note text\", \"after\": \"New note text\"}}}}\n```\n"
|
||||
f"```tool\n{{\"tool\": \"world_update\", \"args\": {{\"content\": \"# The World\\n\\n...full new world state...\"}}}}\n```\n"
|
||||
f"```tool\n{{\"tool\": \"journal_update\", \"args\": {{\"add\": [\"Investigate the mine\"], \"done\": [\"Defeat the demon\"]}}}}\n```\n"
|
||||
f"```tool\n{{\"tool\": \"finalize_turn\", \"args\": {{\"user_prompt\": \"What do you do?\", \"ambience\": \"dungeon\"}}}}\n```\n\n"
|
||||
f"Only output tools for things that actually changed. Omit unchanged fields."}
|
||||
], label=f"Extract attempt {attempt + 1}")
|
||||
|
||||
if not text or not text.strip():
|
||||
|
||||
Loading…
Reference in New Issue
Block a user