feat: tool use — Lyra's first real actions (journal_write, note)
She can now *do* things mid-conversation, not just reply. Adds a tool-calling loop to the chat path and her first two tools; the same mechanism will carry the poker tools (start_session, log_result, get_stats, solver) next. - tools.py: registry of OpenAI-style tool specs + handlers + safe dispatch; journal_write (knowing journaling) and note (tagged notepad, e.g. poker reads) - llm.chat_call(): OpenAI-style call that returns tool_calls (cloud/mi50); local has no tool support and returns plain content - chat.respond(): tool loop — offer tools, run any calls, feed results back, repeat until a text reply (capped at MAX_TOOL_ROUNDS); persists final reply - tests: dispatch + full chat loop (tool call -> result -> reply) Verified live: she invoked `note`, tagged it 'poker', stored a villain read. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+105
@@ -0,0 +1,105 @@
|
||||
"""Lyra's tools — concrete actions she can choose to take mid-conversation.
|
||||
|
||||
This is her first real agency: instead of only producing text, she can decide to
|
||||
*do* something — write in her journal, jot a note. Each tool is an OpenAI-style
|
||||
function spec plus a Python handler. The chat loop offers these on every turn;
|
||||
when she calls one, we run the handler and feed the result back so she can
|
||||
continue. Poker tools (start_session, log_result, get_stats, …) will slot in here
|
||||
the same way once we build that side.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from lyra import logbus, memory
|
||||
|
||||
|
||||
def _journal_write(args: dict, ctx: dict) -> str:
|
||||
entry = (args.get("entry") or "").strip()
|
||||
if not entry:
|
||||
return "Nothing to write — entry was empty."
|
||||
memory.add_journal_entry("journal", entry, source="chat")
|
||||
logbus.log("info", "Lyra journaled (tool)", chars=len(entry))
|
||||
return "Written to your journal."
|
||||
|
||||
|
||||
def _note(args: dict, ctx: dict) -> str:
|
||||
content = (args.get("content") or "").strip()
|
||||
if not content:
|
||||
return "Nothing to note — content was empty."
|
||||
tag = (args.get("tag") or "").strip()
|
||||
stored = f"[{tag}] {content}" if tag else content
|
||||
memory.add_journal_entry("note", stored, source="chat")
|
||||
logbus.log("info", "Lyra noted (tool)", tag=tag or None)
|
||||
return "Noted."
|
||||
|
||||
|
||||
# name -> {spec (OpenAI function tool), handler}
|
||||
TOOLS: dict[str, dict] = {
|
||||
"journal_write": {
|
||||
"handler": _journal_write,
|
||||
"spec": {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "journal_write",
|
||||
"description": (
|
||||
"Write an entry in your own private journal — a permanent place "
|
||||
"that's yours. Use it for a thought, a question, or something about "
|
||||
"yourself or Brian that you want to keep. This is for you, not a "
|
||||
"reply to Brian. Call it whenever you genuinely want to, on your own initiative."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"entry": {"type": "string", "description": "What you want to write, in your own words."}
|
||||
},
|
||||
"required": ["entry"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"note": {
|
||||
"handler": _note,
|
||||
"spec": {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "note",
|
||||
"description": (
|
||||
"Jot down a note to remember later — an observation, an idea, a "
|
||||
"reminder, a read on a poker spot or opponent, anything worth keeping. "
|
||||
"Optionally tag it (e.g. 'poker', 'idea', 'reminder')."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string", "description": "The note text."},
|
||||
"tag": {"type": "string", "description": "Optional category, e.g. 'poker' or 'idea'."},
|
||||
},
|
||||
"required": ["content"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def specs() -> list[dict]:
|
||||
"""OpenAI-format tool definitions to offer the model."""
|
||||
return [t["spec"] for t in TOOLS.values()]
|
||||
|
||||
|
||||
def dispatch(name: str, arguments, ctx: dict | None = None) -> str:
|
||||
"""Run a tool by name with JSON (string or dict) arguments. Returns a result
|
||||
string fed back to the model. Never raises — errors come back as text."""
|
||||
tool = TOOLS.get(name)
|
||||
if not tool:
|
||||
return f"(unknown tool: {name})"
|
||||
try:
|
||||
args = json.loads(arguments) if isinstance(arguments, str) else (arguments or {})
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
args = {}
|
||||
try:
|
||||
return tool["handler"](args, ctx or {})
|
||||
except Exception as exc: # a broken tool must not kill the chat turn
|
||||
logbus.log("error", "tool failed", tool=name, error=str(exc)[:120])
|
||||
return f"(tool error: {exc})"
|
||||
Reference in New Issue
Block a user