35c973df05
She could write everything the HUD shows but not read most of it back (stack, live net, alligator state, scar/confidence entries) — so "what's my live net?" or "what's in my confidence bank?" was a memory guess. - session_state tool returns the same bundle the HUD renders, as a readable summary; added to the Cash toolset. - Cash card tells her the HUD exists, that she and it share the same data, and to answer where-am-I questions from session_state, never memory. - test_modes.py +1; 42 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
511 lines
24 KiB
Python
511 lines
24 KiB
Python
"""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
|
|
import re
|
|
|
|
from lyra import equity, logbus, memory, poker
|
|
|
|
|
|
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"],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
# --- Poker copilot tools -----------------------------------------------------
|
|
|
|
def _start_session(args: dict, ctx: dict) -> str:
|
|
sid = poker.start_session(
|
|
venue=args.get("venue"), stakes=args.get("stakes"),
|
|
game=args.get("game") or "NLH", fmt=args.get("format") or "cash",
|
|
buy_in=args.get("buy_in") or 0, mantra=args.get("mantra"),
|
|
chat_session_id=ctx.get("session_id"),
|
|
)
|
|
logbus.log("info", "poker session started", id=sid, stakes=args.get("stakes"))
|
|
return (f"Session #{sid} started — {args.get('stakes') or '?'} "
|
|
f"{args.get('game') or 'NLH'} at {args.get('venue') or 'unknown'}, "
|
|
f"in for {args.get('buy_in') or 0}.")
|
|
|
|
|
|
def _add_buyin(args: dict, ctx: dict) -> str:
|
|
total = poker.add_buyin(float(args.get("amount") or 0))
|
|
return f"Added {args.get('amount')}. Total in this session: {total:g}."
|
|
|
|
|
|
def _log_stack(args: dict, ctx: dict) -> str:
|
|
try:
|
|
amount = float(args.get("amount"))
|
|
except (TypeError, ValueError):
|
|
return "Give me a number for the stack."
|
|
try:
|
|
st = poker.log_stack(amount)
|
|
except ValueError:
|
|
return "No live session — start one first, then I'll track your stack."
|
|
net = st.get("net")
|
|
return f"Stack ${amount:g} logged" + (f" (net {net:+.0f})." if net is not None else ".")
|
|
|
|
|
|
def _scar_note(args: dict, ctx: dict) -> str:
|
|
content = (args.get("content") or "").strip()
|
|
if not content:
|
|
return "Nothing to log — give me the scar."
|
|
cls = (args.get("classification") or "").strip().lower() or None
|
|
if cls and cls not in ("punt", "cooler", "standard"):
|
|
cls = None
|
|
try:
|
|
poker.log_ritual("scar", content=content, classification=cls,
|
|
hand_id=args.get("hand_id"))
|
|
except ValueError:
|
|
return "No live session — start one and I'll keep the scar notes."
|
|
return f"Scar note logged{f' ({cls})' if cls else ''}."
|
|
|
|
|
|
def _confidence_bank(args: dict, ctx: dict) -> str:
|
|
content = (args.get("content") or "").strip()
|
|
if not content:
|
|
return "Nothing to bank — tell me the good process."
|
|
try:
|
|
poker.log_ritual("confidence", content=content, hand_id=args.get("hand_id"))
|
|
except ValueError:
|
|
return "No live session — start one and I'll run the confidence bank."
|
|
return "Banked. 💰"
|
|
|
|
|
|
def _alligator_blood(args: dict, ctx: dict) -> str:
|
|
on = bool(args.get("on", True))
|
|
try:
|
|
poker.set_alligator(on)
|
|
except ValueError:
|
|
return "No live session to set that on."
|
|
return ("🐊 Alligator Blood ON — hang around, refuse to die, no forced miracles."
|
|
if on else "Alligator Blood off. Back to standard register.")
|
|
|
|
|
|
def _reset_ritual(args: dict, ctx: dict) -> str:
|
|
content = (args.get("content") or "").strip() or None
|
|
try:
|
|
poker.log_ritual("reset", content=content)
|
|
except ValueError:
|
|
return "No live session to reset."
|
|
return "Reset logged. Clean slate — this is a new session in your head."
|
|
|
|
|
|
def _log_hand(args: dict, ctx: dict) -> str:
|
|
fields = {k: args.get(k) for k in poker._HAND_FIELDS if args.get(k) not in (None, "")}
|
|
hid = poker.log_hand(**fields)
|
|
bits = " ".join(str(fields[k]) for k in ("position", "hole_cards") if k in fields)
|
|
return f"Hand #{hid} logged{(' — ' + bits) if bits else ''}."
|
|
|
|
|
|
def _add_read(args: dict, ctx: dict) -> str:
|
|
poker.add_read(
|
|
note=args.get("note") or "", seat=args.get("seat"), name=args.get("name"),
|
|
tendencies=args.get("tendencies"), adjustment=args.get("adjustment"),
|
|
description=args.get("description"), category=args.get("category"),
|
|
venue=args.get("venue"),
|
|
)
|
|
who = f" on {args['name']}" if args.get("name") else ""
|
|
return f"Read logged{who}."
|
|
|
|
|
|
def _end_session(args: dict, ctx: dict) -> str:
|
|
s = poker.end_session(cash_out=float(args.get("cash_out") or 0), mood=args.get("mood"))
|
|
hourly = f", {s['net'] / s['hours']:+.0f}/hr" if s.get("hours") else ""
|
|
logbus.log("info", "poker session closed", id=s["id"], net=s["net"])
|
|
return f"Session #{s['id']} closed — net {s['net']:+.0f} over {s['hours']}h{hourly}."
|
|
|
|
|
|
def _session_state(args: dict, ctx: dict) -> str:
|
|
h = poker.hud()
|
|
if not h:
|
|
return "No live session right now."
|
|
s, st, r = h["session"], h["stack"], h["rituals"]
|
|
L = [f"{s.get('stakes') or '?'} {s.get('game') or ''} @ {s.get('venue') or '?'} "
|
|
f"— {h['stats']['hands_logged']} hands logged"]
|
|
if st.get("current") is not None:
|
|
L.append(f"Stack ${st['current']:g} (in {st['buy_in']:g}, live net {st['net']:+.0f})")
|
|
else:
|
|
L.append(f"Stack not logged yet (in {st['buy_in']:g})")
|
|
L.append("🐊 Alligator Blood is ON" if r["alligator"] else "Alligator Blood: off")
|
|
if r["confidence"]:
|
|
L.append("Confidence bank: " + " | ".join(c["content"] for c in r["confidence"][-4:]))
|
|
if r["scars"]:
|
|
L.append("Scar notes: " + " | ".join(
|
|
sc["content"] + (f" [{sc['classification']}]" if sc.get("classification") else "")
|
|
for sc in r["scars"][-4:]))
|
|
if r["resets"]:
|
|
L.append(f"{len(r['resets'])} reset(s) this session")
|
|
return "\n".join(L)
|
|
|
|
|
|
def _session_stats(args: dict, ctx: dict) -> str:
|
|
st = poker.session_stats()
|
|
if not st:
|
|
return "No session found."
|
|
s = st["session"]
|
|
tags = ", ".join(f"{k}:{v}" for k, v in st["tags"].items()) or "none"
|
|
return (f"Session #{s['id']} ({s.get('stakes')} {s.get('game')} @ {s.get('venue')}): "
|
|
f"in {s.get('buy_in_total'):g}, net {st['net'] if st['net'] is not None else '—'}, "
|
|
f"{st['hands_logged']} hands logged (tags: {tags}).")
|
|
|
|
|
|
def _running_stats(args: dict, ctx: dict) -> str:
|
|
rs = poker.running_stats(stakes=args.get("stakes"), venue=args.get("venue"),
|
|
game=args.get("game"), since=args.get("since"))
|
|
if not rs["sessions"]:
|
|
return "No closed sessions match that filter yet."
|
|
by = " | ".join(f"{k}: {v['net']:+.0f} in {v['hours']:g}h ({v['sessions']})"
|
|
for k, v in rs["by_stake"].items())
|
|
hourly = f" ({rs['per_hour']:+.0f}/hr)" if rs["per_hour"] is not None else ""
|
|
return f"{rs['sessions']} sessions, {rs['hours']:g}h, net {rs['net']:+.0f}{hourly}. By stake: {by}"
|
|
|
|
|
|
def _record_hand(args: dict, ctx: dict) -> str:
|
|
out = poker.record_hand(
|
|
args.get("shorthand") or "", stakes=args.get("stakes"),
|
|
tag=args.get("tag"), lesson=args.get("lesson"),
|
|
)
|
|
if not out["id"]:
|
|
return "I couldn't parse that hand — give it to me again with a little more detail?"
|
|
p = out["parsed"]
|
|
cards = " ".join(p.get("hero_cards") or [])
|
|
logbus.log("info", "hand reconstructed", id=out["id"], hero=p.get("hero_pos"))
|
|
return (f"Hand #{out['id']} reconstructed — {p.get('hero_pos') or '?'} "
|
|
f"{cards}. View/replay it at /hand/{out['id']}")
|
|
|
|
|
|
def _generate_recap(args: dict, ctx: dict) -> str:
|
|
out = poker.generate_recap()
|
|
if not out:
|
|
return "No session to recap yet — start (and ideally finish) one first."
|
|
logbus.log("info", "recap generated", id=out["id"], chars=len(out["markdown"]))
|
|
return (f"Recap written for session #{out['id']} — view or download the .md "
|
|
f"at /recap/{out['id']}")
|
|
|
|
|
|
def _analyze_spot(args: dict, ctx: dict) -> str:
|
|
def cards(s):
|
|
return [c for c in re.split(r"[\s,]+", (s or "").strip()) if c]
|
|
try:
|
|
r = equity.analyze(cards(args.get("hero")), cards(args.get("villain")),
|
|
cards(args.get("board")))
|
|
except equity.EquityError as e:
|
|
return f"(can't compute equity: {e})"
|
|
except Exception as e: # never let a bad spot kill the turn
|
|
return f"(equity error: {e})"
|
|
street = {0: "preflop", 3: "flop", 4: "turn", 5: "river"}.get(len(r["board"]), "")
|
|
L = [f"Board: {' '.join(r['board']) or '(preflop)'}" + (f" — {street}" if street else "")]
|
|
if "hero_hand" in r:
|
|
L.append(f"You ({' '.join(r['hero'])}): {r['hero_hand']}")
|
|
L.append(f"Villain ({' '.join(r['villain'])}): {r['villain_hand']}")
|
|
L.append(f"Currently ahead: {r['ahead']}")
|
|
tie = f" / tie {r['tie_equity']}%" if r.get("tie_equity") else ""
|
|
L.append(f"EQUITY (exact): you {r['hero_equity']}% / villain {r['villain_equity']}%{tie}")
|
|
o = r.get("hero_outs")
|
|
if o:
|
|
L.append(f"Your outs (one card to come): {o['count']}"
|
|
+ (f" — {' '.join(o['cards'])}" if o["count"] else " — drawing dead"))
|
|
return "\n".join(L)
|
|
|
|
|
|
def _player_profile(args: dict, ctx: dict) -> str:
|
|
prof = poker.player_profile(args.get("name") or "")
|
|
if not prof:
|
|
return f"No file on {args.get('name')} yet."
|
|
p = prof["player"]
|
|
L = [p["name"] + (f" ({p['venue']})" if p.get("venue") else "")
|
|
+ (f" [{p['category']}]" if p.get("category") else "")]
|
|
thin = not (p.get("tendencies") or p.get("adjustment")) and not prof.get("stats")
|
|
if thin:
|
|
L.append("⚠ THIN FILE — no standing read on record. Report only the observed "
|
|
"hand(s) below and tell Brian you've barely seen him. Do NOT generalize a style.")
|
|
if p.get("description"):
|
|
L.append(p["description"])
|
|
if p.get("tendencies"):
|
|
L.append(f"Tendencies: {p['tendencies']}")
|
|
if p.get("adjustment"):
|
|
L.append(f"Exploit: {p['adjustment']}")
|
|
s = prof.get("stats")
|
|
if s:
|
|
L.append(f"Stats ({s['hands']} hands): VPIP {s['vpip_pct']}% · PFR {s['pfr_pct']}% · WTSD {s['wtsd_pct']}%")
|
|
elif prof.get("small_sample"):
|
|
L.append(prof["small_sample"])
|
|
if prof.get("showdowns"):
|
|
L.append("Shown down: " + ", ".join(prof["showdowns"][:6]))
|
|
if prof.get("reads"):
|
|
L.append("Notes: " + " | ".join(prof["reads"][:4]))
|
|
if prof.get("recent"):
|
|
L.append("Recent hands: " + " | ".join(prof["recent"][:4]))
|
|
return "\n".join(L)
|
|
|
|
|
|
def _villain_file(args: dict, ctx: dict) -> str:
|
|
vs = poker.get_villain_file(name=args.get("name"), venue=args.get("venue"))
|
|
if not vs:
|
|
return "No villain notes match."
|
|
lines = []
|
|
for v in vs[:8]:
|
|
lines.append(
|
|
f"- {v['name']}" + (f" ({v['venue']})" if v.get("venue") else "")
|
|
+ (f" [{v['category']}]" if v.get("category") else "")
|
|
+ (f": {v['tendencies']}" if v.get("tendencies") else "")
|
|
+ (f" → {v['adjustment']}" if v.get("adjustment") else "")
|
|
)
|
|
return "\n".join(lines)
|
|
|
|
|
|
def _f(name, desc, props, required):
|
|
return {"type": "function", "function": {
|
|
"name": name, "description": desc,
|
|
"parameters": {"type": "object", "properties": props, "required": required}}}
|
|
|
|
|
|
_S = {"type": "string"}
|
|
_N = {"type": "number"}
|
|
|
|
TOOLS.update({
|
|
"start_session": {"handler": _start_session, "spec": _f(
|
|
"start_session",
|
|
"Begin a live poker session. Call when Brian sits down to play.",
|
|
{"venue": {**_S, "description": "Casino/room, e.g. 'Meadows'"},
|
|
"stakes": {**_S, "description": "e.g. '1/3', '2/5'"},
|
|
"game": {**_S, "description": "NLH, PLO, Stud8, Mixed (default NLH)"},
|
|
"format": {**_S, "description": "'cash' or 'tournament' (default cash)"},
|
|
"buy_in": {**_N, "description": "Initial buy-in amount"},
|
|
"mantra": {**_S, "description": "Optional pre-session focus/anchor"}},
|
|
[])},
|
|
"add_buyin": {"handler": _add_buyin, "spec": _f(
|
|
"add_buyin", "Record a rebuy / additional buy-in in the live session.",
|
|
{"amount": {**_N, "description": "Amount added"}}, ["amount"])},
|
|
"log_stack": {"handler": _log_stack, "spec": _f(
|
|
"log_stack",
|
|
"Record Brian's CURRENT total chip stack in the live session. Call whenever "
|
|
"he states his stack ('I'm at 350', 'down to 220', 'stacked off to 900'). "
|
|
"Tracks his stack over time and his live net while he's still sitting.",
|
|
{"amount": {**_N, "description": "Current total chip stack, in dollars"}},
|
|
["amount"])},
|
|
"scar_note": {"handler": _scar_note, "spec": _f(
|
|
"scar_note",
|
|
"Log a SCAR NOTE — a painful or instructive mistake to study later. Use when "
|
|
"Brian punts, gets too attached, or makes a leak — or when he flags one. "
|
|
"Classify honestly: 'punt' (his error), 'cooler' (unavoidable), or 'standard' "
|
|
"(correct play, bad result). The punt-vs-cooler distinction matters to him.",
|
|
{"content": {**_S, "description": "What happened and the lesson, in Brian's terms"},
|
|
"classification": {**_S, "description": "punt | cooler | standard"},
|
|
"hand_id": {**_N, "description": "Linked hand id, if this scar is a logged hand"}},
|
|
["content"])},
|
|
"confidence_bank": {"handler": _confidence_bank, "spec": _f(
|
|
"confidence_bank",
|
|
"Log a CONFIDENCE BANK entry — good PROCESS regardless of result: a disciplined "
|
|
"laydown, clean value bet, catching a leak in real time, sticking to the plan. "
|
|
"Bank it when he does something right, especially when the result didn't reward it.",
|
|
{"content": {**_S, "description": "The disciplined / good-process play to bank"},
|
|
"hand_id": {**_N, "description": "Linked hand id, if applicable"}},
|
|
["content"])},
|
|
"alligator_blood": {"handler": _alligator_blood, "spec": _f(
|
|
"alligator_blood",
|
|
"Toggle ALLIGATOR BLOOD mode — Brian's adversity state: hang around, refuse to "
|
|
"die, don't force miracles, make opponents beat him correctly. Turn it ON when he "
|
|
"invokes it, or SUGGEST it (then turn on if he agrees) when he's card-dead, short, "
|
|
"stuck, or grinding through a downswing. Turn OFF on reset or when he's back in rhythm.",
|
|
{"on": {"type": "boolean", "description": "true to engage, false to stand down"}},
|
|
[])},
|
|
"reset_ritual": {"handler": _reset_ritual, "spec": _f(
|
|
"reset_ritual",
|
|
"Log a RESET — a deliberate mental circuit-breaker after a loss or tilt spike, "
|
|
"treating the rest of the night as a fresh start (the stats stay continuous). "
|
|
"Use when he resets, or when you've talked him through one.",
|
|
{"content": {**_S, "description": "Optional note on what prompted the reset"}},
|
|
[])},
|
|
"log_hand": {"handler": _log_hand, "spec": _f(
|
|
"log_hand",
|
|
"Log a hand in the live session. All fields optional — capture whatever Brian gives you, even terse.",
|
|
{"position": {**_S, "description": "e.g. 'BTN', 'UTG', 'BB'"},
|
|
"hole_cards": {**_S, "description": "e.g. 'AKs', 'JJ', '8d9s'"},
|
|
"board": {**_S, "description": "Final board if known"},
|
|
"preflop": {**_S, "description": "Preflop action narrative"},
|
|
"flop": {**_S, "description": "Flop board + action"},
|
|
"turn": {**_S, "description": "Turn card + action"},
|
|
"river": {**_S, "description": "River card + action"},
|
|
"showdown": {**_S, "description": "Showdown / result detail"},
|
|
"pot": {**_N, "description": "Pot size"},
|
|
"result": {**_N, "description": "Net chips won(+)/lost(-) on the hand"},
|
|
"tag": {**_S, "description": "well_played | leak | cooler | confidence | notable"},
|
|
"lesson": {**_S, "description": "Takeaway/analysis"}},
|
|
[])},
|
|
"add_read": {"handler": _add_read, "spec": _f(
|
|
"add_read",
|
|
"Log a read on an opponent. If you give a name, it's saved to the persistent villain file.",
|
|
{"note": {**_S, "description": "The observation / what they showed down"},
|
|
"name": {**_S, "description": "Player name/handle if known (creates/updates their dossier)"},
|
|
"seat": {**_S, "description": "Seat or relative position"},
|
|
"tendencies": {**_S, "description": "Standing read on how they play"},
|
|
"adjustment": {**_S, "description": "How Brian should exploit them"},
|
|
"description": {**_S, "description": "Physical marker, e.g. 'motorized chair'"},
|
|
"category": {**_S, "description": "feeder | risky | reg | unknown"},
|
|
"venue": {**_S, "description": "Where they play"}},
|
|
["note"])},
|
|
"end_session": {"handler": _end_session, "spec": _f(
|
|
"end_session", "Close the live session: record cashout, compute net + hours.",
|
|
{"cash_out": {**_N, "description": "Final cashout amount"},
|
|
"mood": {**_S, "description": "Mental-game note for the session"}},
|
|
["cash_out"])},
|
|
"session_stats": {"handler": _session_stats, "spec": _f(
|
|
"session_stats", "Get money + hand summary for the current/most-recent session.",
|
|
{}, [])},
|
|
"session_state": {"handler": _session_state, "spec": _f(
|
|
"session_state",
|
|
"Read back the CURRENT live-session state — the same data Brian sees on his HUD: "
|
|
"stack, live net, whether Alligator Blood is on, and the scar notes / "
|
|
"confidence-bank entries so far. Use whenever he asks where he's at, what's in "
|
|
"the bank, his stack or net, or if gator mode is on — answer from THIS, not memory.",
|
|
{}, [])},
|
|
"running_stats": {"handler": _running_stats, "spec": _f(
|
|
"running_stats",
|
|
"Cumulative results across closed sessions (net, $/hr, by stake). Optionally filter.",
|
|
{"stakes": {**_S, "description": "Filter by stakes, e.g. '1/3'"},
|
|
"venue": {**_S, "description": "Filter by venue"},
|
|
"game": {**_S, "description": "Filter by game type"},
|
|
"since": {**_S, "description": "ISO date lower bound, e.g. '2026-06-01'"}},
|
|
[])},
|
|
"record_hand": {"handler": _record_hand, "spec": _f(
|
|
"record_hand",
|
|
"Reconstruct a hand from Brian's rough shorthand into a structured, "
|
|
"replayable hand history. Use when he describes/vomits a hand he wants "
|
|
"saved or to review. Pass his description verbatim as 'shorthand'.",
|
|
{"shorthand": {**_S, "description": "Brian's rough description of the hand, verbatim"},
|
|
"stakes": {**_S, "description": "Stakes if known, e.g. '1/3'"},
|
|
"tag": {**_S, "description": "well_played | leak | cooler | confidence | notable"},
|
|
"lesson": {**_S, "description": "Takeaway, if he stated one"}},
|
|
["shorthand"])},
|
|
"generate_recap": {"handler": _generate_recap, "spec": _f(
|
|
"generate_recap",
|
|
"Write up the full session recap (.md) in Brian's format from the logged "
|
|
"data + this conversation. Use when he asks for the recap/writeup, usually "
|
|
"after ending a session.",
|
|
{}, [])},
|
|
"analyze_spot": {"handler": _analyze_spot, "spec": _f(
|
|
"analyze_spot",
|
|
"Compute EXACT poker equity, what each hand makes, who's ahead, and outs "
|
|
"for a hero-vs-villain spot. ALWAYS use this for any equity / board-reading "
|
|
"/ 'am I ahead' / outs question — never compute it yourself.",
|
|
{"hero": {**_S, "description": "Hero's hole cards, rank+suit letters, e.g. 'Jh Js' (use 'Jx' if a suit is unknown)"},
|
|
"villain": {**_S, "description": "Villain's hole cards, e.g. '6d 5d'"},
|
|
"board": {**_S, "description": "Board cards so far, e.g. '8c 7d Ts' (flop) or '8c 7d Ts 4d' (turn); omit for preflop"}},
|
|
["hero", "villain"])},
|
|
"player_profile": {"handler": _player_profile, "spec": _f(
|
|
"player_profile",
|
|
"Look up everything known about one opponent — dossier, reads, hands "
|
|
"they've shown down, and (once enough hands are logged) inferred stats "
|
|
"like VPIP/PFR. Use when Brian asks what's known about a player.",
|
|
{"name": {**_S, "description": "Player name to look up"}},
|
|
["name"])},
|
|
"get_villain_file": {"handler": _villain_file, "spec": _f(
|
|
"get_villain_file",
|
|
"Pull saved opponent dossiers (the villain file). Filter by name or venue, e.g. before sitting down.",
|
|
{"name": {**_S, "description": "Player name to look up"},
|
|
"venue": {**_S, "description": "Venue to pull the local pool for"}},
|
|
[])},
|
|
})
|
|
|
|
|
|
def specs(allow=None) -> list[dict]:
|
|
"""OpenAI-format tool definitions to offer the model.
|
|
|
|
`allow` (an iterable of tool names, e.g. a mode's allow-list) restricts the
|
|
set; None means every tool. Unknown names in `allow` are ignored.
|
|
"""
|
|
if allow is None:
|
|
return [t["spec"] for t in TOOLS.values()]
|
|
allow = set(allow)
|
|
return [t["spec"] for name, t in TOOLS.items() if name in allow]
|
|
|
|
|
|
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})"
|