"""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})"