feat: poker copilot — structured session/hand/villain tracking + stats
The real upgrade over the ChatGPT prose-recap workflow: structured data capture via tools Lyra drives during a live session, with stats computed from real data. - lyra/poker.py: domain pack (separate from core memory) — poker_sessions, poker_hands, persistent poker_players (villain file) + player_reads; functions for session lifecycle (start/buyin/end with net+hours), tolerant hand logging, villain upsert/reads, and session/running stats ($/hr, by stake/venue/game) - tools.py: 8 poker tools wired into the chat tool loop (start_session, add_buyin, log_hand, add_read, end_session, session_stats, running_stats, get_villain_file) — partial/terse input tolerated - import/: Brian's real .md session-log format (reference for the phase-2 recap) - tests: lifecycle/net math, partial hand logging, villain upsert, running stats, tool dispatch Verified live: a full talk-through session persisted as structured rows (session +240, AKs hand, seat-5 read) — she drove the tools from natural chat. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+315
@@ -0,0 +1,315 @@
|
||||
"""Poker domain pack: structured session / hand / villain storage + stats.
|
||||
|
||||
This is the poker-specific data layer — kept separate from the domain-agnostic
|
||||
core memory so Lyra-the-agent stays general. It records real structured data
|
||||
(money, hands, opponents) during a live session via tools Lyra calls, and
|
||||
computes stats from that data. The narrative .md recap is generated on top of
|
||||
this, not instead of it.
|
||||
|
||||
Tables live in the same SQLite file as everything else (one DB), created lazily.
|
||||
Most tool-facing functions default to the current *live* session so Lyra rarely
|
||||
needs to pass an id around.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from lyra import memory
|
||||
|
||||
_SCHEMA = """
|
||||
CREATE TABLE IF NOT EXISTS poker_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
started_at TEXT NOT NULL,
|
||||
ended_at TEXT,
|
||||
venue TEXT,
|
||||
game TEXT, -- NLH, PLO, Stud8, Mixed, ...
|
||||
stakes TEXT, -- "1/3", "2/5"
|
||||
format TEXT, -- cash | tournament
|
||||
buy_in_total REAL NOT NULL DEFAULT 0,
|
||||
cash_out REAL,
|
||||
net REAL,
|
||||
hours REAL,
|
||||
mantra TEXT,
|
||||
mood TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'live', -- live | closed
|
||||
recap_md TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS poker_hands (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id INTEGER NOT NULL,
|
||||
at TEXT NOT NULL,
|
||||
position TEXT,
|
||||
hole_cards TEXT,
|
||||
board TEXT,
|
||||
preflop TEXT,
|
||||
flop TEXT,
|
||||
turn TEXT,
|
||||
river TEXT,
|
||||
showdown TEXT,
|
||||
pot REAL,
|
||||
result REAL,
|
||||
stack_after REAL,
|
||||
tag TEXT, -- well_played | leak | cooler | confidence | notable
|
||||
lesson TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_hands_session ON poker_hands(session_id);
|
||||
|
||||
-- Persistent villain file — survives across sessions/venues.
|
||||
CREATE TABLE IF NOT EXISTS poker_players (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
venue TEXT,
|
||||
description TEXT,
|
||||
tendencies TEXT,
|
||||
adjustment TEXT,
|
||||
category TEXT, -- feeder | risky | reg | unknown
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Per-session observations (the live 'reads'); player_id links to the file.
|
||||
CREATE TABLE IF NOT EXISTS player_reads (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id INTEGER,
|
||||
player_id INTEGER,
|
||||
seat TEXT,
|
||||
note TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
"""
|
||||
|
||||
_ensured_for = None
|
||||
|
||||
|
||||
def _c():
|
||||
"""Shared connection with poker tables ensured (re-ensures after reconnect)."""
|
||||
global _ensured_for
|
||||
conn = memory._connection()
|
||||
if _ensured_for is not conn:
|
||||
conn.executescript(_SCHEMA)
|
||||
_ensured_for = conn
|
||||
return conn
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
# --- sessions ---
|
||||
|
||||
def start_session(venue: str | None = None, stakes: str | None = None,
|
||||
game: str = "NLH", fmt: str = "cash",
|
||||
buy_in: float = 0.0, mantra: str | None = None) -> int:
|
||||
"""Open a new live session. Returns its id."""
|
||||
conn = _c()
|
||||
with conn:
|
||||
cur = conn.execute(
|
||||
"INSERT INTO poker_sessions "
|
||||
"(started_at, venue, game, stakes, format, buy_in_total, mantra, status) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, 'live')",
|
||||
(_now(), venue, game, stakes, fmt, float(buy_in or 0), mantra),
|
||||
)
|
||||
return int(cur.lastrowid)
|
||||
|
||||
|
||||
def live_session() -> dict | None:
|
||||
"""The current open session, if any."""
|
||||
r = _c().execute(
|
||||
"SELECT * FROM poker_sessions WHERE status = 'live' ORDER BY id DESC LIMIT 1"
|
||||
).fetchone()
|
||||
return dict(r) if r else None
|
||||
|
||||
|
||||
def _resolve(session_id: int | None) -> int | None:
|
||||
if session_id is not None:
|
||||
return session_id
|
||||
live = live_session()
|
||||
return live["id"] if live else None
|
||||
|
||||
|
||||
def add_buyin(amount: float, session_id: int | None = None) -> float:
|
||||
"""Add a buy-in/rebuy to a session. Returns the new total in."""
|
||||
sid = _resolve(session_id)
|
||||
if sid is None:
|
||||
raise ValueError("no live session")
|
||||
conn = _c()
|
||||
with conn:
|
||||
conn.execute(
|
||||
"UPDATE poker_sessions SET buy_in_total = buy_in_total + ? WHERE id = ?",
|
||||
(float(amount), sid),
|
||||
)
|
||||
return float(_c().execute(
|
||||
"SELECT buy_in_total FROM poker_sessions WHERE id = ?", (sid,)
|
||||
).fetchone()["buy_in_total"])
|
||||
|
||||
|
||||
def end_session(cash_out: float, mood: str | None = None,
|
||||
session_id: int | None = None) -> dict:
|
||||
"""Close a session: record cashout, compute net + hours. Returns the row."""
|
||||
sid = _resolve(session_id)
|
||||
if sid is None:
|
||||
raise ValueError("no live session")
|
||||
row = _c().execute("SELECT * FROM poker_sessions WHERE id = ?", (sid,)).fetchone()
|
||||
ended = _now()
|
||||
hours = (datetime.fromisoformat(ended) - datetime.fromisoformat(row["started_at"])).total_seconds() / 3600
|
||||
net = float(cash_out) - float(row["buy_in_total"])
|
||||
conn = _c()
|
||||
with conn:
|
||||
conn.execute(
|
||||
"UPDATE poker_sessions SET ended_at = ?, cash_out = ?, net = ?, hours = ?, "
|
||||
"mood = COALESCE(?, mood), status = 'closed' WHERE id = ?",
|
||||
(ended, float(cash_out), net, round(hours, 2), mood, sid),
|
||||
)
|
||||
return dict(_c().execute("SELECT * FROM poker_sessions WHERE id = ?", (sid,)).fetchone())
|
||||
|
||||
|
||||
# --- hands ---
|
||||
|
||||
_HAND_FIELDS = ("position", "hole_cards", "board", "preflop", "flop", "turn",
|
||||
"river", "showdown", "pot", "result", "stack_after", "tag", "lesson")
|
||||
|
||||
|
||||
def log_hand(session_id: int | None = None, **fields) -> int:
|
||||
"""Record a hand. All fields optional/partial — terse logging is fine."""
|
||||
sid = _resolve(session_id)
|
||||
if sid is None:
|
||||
raise ValueError("no live session")
|
||||
cols = ["session_id", "at"]
|
||||
vals: list = [sid, _now()]
|
||||
for f in _HAND_FIELDS:
|
||||
if fields.get(f) not in (None, ""):
|
||||
cols.append(f)
|
||||
vals.append(fields[f])
|
||||
conn = _c()
|
||||
with conn:
|
||||
cur = conn.execute(
|
||||
f"INSERT INTO poker_hands ({', '.join(cols)}) VALUES ({', '.join('?' * len(cols))})",
|
||||
vals,
|
||||
)
|
||||
return int(cur.lastrowid)
|
||||
|
||||
|
||||
def list_hands(session_id: int | None = None) -> list[dict]:
|
||||
sid = _resolve(session_id)
|
||||
if sid is None:
|
||||
return []
|
||||
return [dict(r) for r in _c().execute(
|
||||
"SELECT * FROM poker_hands WHERE session_id = ? ORDER BY id", (sid,)
|
||||
).fetchall()]
|
||||
|
||||
|
||||
# --- villain file ---
|
||||
|
||||
def upsert_player(name: str, venue: str | None = None, description: str | None = None,
|
||||
tendencies: str | None = None, adjustment: str | None = None,
|
||||
category: str | None = None) -> int:
|
||||
"""Create or update a player in the persistent villain file (matched by name)."""
|
||||
conn = _c()
|
||||
existing = conn.execute(
|
||||
"SELECT id FROM poker_players WHERE name = ? COLLATE NOCASE", (name,)
|
||||
).fetchone()
|
||||
with conn:
|
||||
if existing:
|
||||
pid = existing["id"]
|
||||
# only overwrite fields that were provided
|
||||
for col, val in (("venue", venue), ("description", description),
|
||||
("tendencies", tendencies), ("adjustment", adjustment),
|
||||
("category", category)):
|
||||
if val not in (None, ""):
|
||||
conn.execute(f"UPDATE poker_players SET {col} = ? WHERE id = ?", (val, pid))
|
||||
conn.execute("UPDATE poker_players SET updated_at = ? WHERE id = ?", (_now(), pid))
|
||||
return int(pid)
|
||||
cur = conn.execute(
|
||||
"INSERT INTO poker_players (name, venue, description, tendencies, adjustment, "
|
||||
"category, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
(name, venue, description, tendencies, adjustment, category, _now()),
|
||||
)
|
||||
return int(cur.lastrowid)
|
||||
|
||||
|
||||
def add_read(note: str, seat: str | None = None, name: str | None = None,
|
||||
session_id: int | None = None, **player_fields) -> int:
|
||||
"""Log a live read. If `name` is given, upsert the player and link the read."""
|
||||
sid = _resolve(session_id)
|
||||
pid = None
|
||||
if name:
|
||||
pid = upsert_player(name, **{k: v for k, v in player_fields.items()
|
||||
if k in ("venue", "description", "tendencies",
|
||||
"adjustment", "category")})
|
||||
conn = _c()
|
||||
with conn:
|
||||
cur = conn.execute(
|
||||
"INSERT INTO player_reads (session_id, player_id, seat, note, created_at) "
|
||||
"VALUES (?, ?, ?, ?, ?)",
|
||||
(sid, pid, seat, note, _now()),
|
||||
)
|
||||
return int(cur.lastrowid)
|
||||
|
||||
|
||||
def get_villain_file(name: str | None = None, venue: str | None = None) -> list[dict]:
|
||||
"""Pull villain dossiers, optionally filtered by name or venue."""
|
||||
sql = "SELECT * FROM poker_players"
|
||||
where, params = [], []
|
||||
if name:
|
||||
where.append("name LIKE ?")
|
||||
params.append(f"%{name}%")
|
||||
if venue:
|
||||
where.append("venue LIKE ?")
|
||||
params.append(f"%{venue}%")
|
||||
if where:
|
||||
sql += " WHERE " + " AND ".join(where)
|
||||
sql += " ORDER BY updated_at DESC"
|
||||
return [dict(r) for r in _c().execute(sql, params).fetchall()]
|
||||
|
||||
|
||||
# --- stats ---
|
||||
|
||||
def session_stats(session_id: int | None = None) -> dict:
|
||||
"""Money + hand summary for one session."""
|
||||
sid = _resolve(session_id)
|
||||
if sid is None:
|
||||
return {}
|
||||
s = _c().execute("SELECT * FROM poker_sessions WHERE id = ?", (sid,)).fetchone()
|
||||
if not s:
|
||||
return {}
|
||||
s = dict(s)
|
||||
hands = list_hands(sid)
|
||||
tags: dict[str, int] = {}
|
||||
for h in hands:
|
||||
if h.get("tag"):
|
||||
tags[h["tag"]] = tags.get(h["tag"], 0) + 1
|
||||
hourly = round(s["net"] / s["hours"], 2) if s.get("net") is not None and s.get("hours") else None
|
||||
return {
|
||||
"session": s, "hands_logged": len(hands), "tags": tags,
|
||||
"net": s.get("net"), "hours": s.get("hours"), "per_hour": hourly,
|
||||
}
|
||||
|
||||
|
||||
def running_stats(stakes: str | None = None, venue: str | None = None,
|
||||
game: str | None = None, since: str | None = None) -> dict:
|
||||
"""Cumulative stats over closed sessions, optionally filtered."""
|
||||
sql = "SELECT net, hours, stakes, venue, game FROM poker_sessions WHERE status = 'closed' AND net IS NOT NULL"
|
||||
params: list = []
|
||||
for col, val in (("stakes", stakes), ("venue", venue), ("game", game)):
|
||||
if val:
|
||||
sql += f" AND {col} = ?"
|
||||
params.append(val)
|
||||
if since:
|
||||
sql += " AND started_at >= ?"
|
||||
params.append(since)
|
||||
rows = [dict(r) for r in _c().execute(sql, params).fetchall()]
|
||||
sessions = len(rows)
|
||||
net = round(sum(r["net"] or 0 for r in rows), 2)
|
||||
hours = round(sum(r["hours"] or 0 for r in rows), 2)
|
||||
by_stake: dict[str, dict] = {}
|
||||
for r in rows:
|
||||
k = r["stakes"] or "?"
|
||||
b = by_stake.setdefault(k, {"sessions": 0, "net": 0.0, "hours": 0.0})
|
||||
b["sessions"] += 1
|
||||
b["net"] = round(b["net"] + (r["net"] or 0), 2)
|
||||
b["hours"] = round(b["hours"] + (r["hours"] or 0), 2)
|
||||
return {
|
||||
"sessions": sessions, "net": net, "hours": hours,
|
||||
"per_hour": round(net / hours, 2) if hours else None,
|
||||
"by_stake": by_stake,
|
||||
}
|
||||
+158
-1
@@ -11,7 +11,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from lyra import logbus, memory
|
||||
from lyra import logbus, memory, poker
|
||||
|
||||
|
||||
def _journal_write(args: dict, ctx: dict) -> str:
|
||||
@@ -83,6 +83,163 @@ TOOLS: dict[str, dict] = {
|
||||
}
|
||||
|
||||
|
||||
# --- 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"),
|
||||
)
|
||||
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_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_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 _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_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.",
|
||||
{}, [])},
|
||||
"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'"}},
|
||||
[])},
|
||||
"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() -> list[dict]:
|
||||
"""OpenAI-format tool definitions to offer the model."""
|
||||
return [t["spec"] for t in TOOLS.values()]
|
||||
|
||||
Reference in New Issue
Block a user