feat: poker phase 2 — session recap (.md) generation, export, hands browser
Completes the poker copilot loop: talk through a session -> structured capture
-> generated writeup in Brian's format, remembered + exportable.
- poker.generate_recap(): LLM produces Brian's .md log (Session Header, Money
Flow, Overview, Timeline, Key Hands w/ assessments, Villain Notes, Confidence
Bank, Scar Notes, Mental Game, Final Assessment) from the session's structured
data + the linked chat conversation; stored on poker_sessions.recap_md
- sessions now capture chat_session_id (via tool ctx) to pull the right convo;
list_recent_hands() for browsing
- generate_recap tool ("write up the recap")
- web: /recap/{id} (renders the md) + /recap/{id}/download (.md attachment) +
/hands browser (recent hands -> /hand/{id}); nav links added (desktop + mobile)
- tests: recap generation (stubbed), recent-hands listing
Verified live: recap for the Meadows session rendered + downloaded; all pages 200.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+122
-11
@@ -33,8 +33,9 @@ CREATE TABLE IF NOT EXISTS poker_sessions (
|
||||
hours REAL,
|
||||
mantra TEXT,
|
||||
mood TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'live', -- live | closed
|
||||
recap_md TEXT
|
||||
status TEXT NOT NULL DEFAULT 'live', -- live | closed | review
|
||||
recap_md TEXT,
|
||||
chat_session_id TEXT -- links to the chat where it was played, for recap
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS poker_hands (
|
||||
@@ -91,10 +92,12 @@ def _c():
|
||||
if _ensured_for is not conn:
|
||||
conn.executescript(_SCHEMA)
|
||||
# Add columns introduced after a DB already had the tables (no-op if present).
|
||||
try:
|
||||
conn.execute("ALTER TABLE poker_hands ADD COLUMN structured TEXT")
|
||||
except Exception:
|
||||
pass
|
||||
for ddl in ("ALTER TABLE poker_hands ADD COLUMN structured TEXT",
|
||||
"ALTER TABLE poker_sessions ADD COLUMN chat_session_id TEXT"):
|
||||
try:
|
||||
conn.execute(ddl)
|
||||
except Exception:
|
||||
pass
|
||||
_ensured_for = conn
|
||||
return conn
|
||||
|
||||
@@ -106,20 +109,25 @@ def _now() -> str:
|
||||
# --- 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:
|
||||
game: str = "NLH", fmt: str = "cash", buy_in: float = 0.0,
|
||||
mantra: str | None = None, chat_session_id: 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),
|
||||
"(started_at, venue, game, stakes, format, buy_in_total, mantra, status, chat_session_id) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, 'live', ?)",
|
||||
(_now(), venue, game, stakes, fmt, float(buy_in or 0), mantra, chat_session_id),
|
||||
)
|
||||
return int(cur.lastrowid)
|
||||
|
||||
|
||||
def get_session(session_id: int) -> dict | None:
|
||||
r = _c().execute("SELECT * FROM poker_sessions WHERE id = ?", (session_id,)).fetchone()
|
||||
return dict(r) if r else None
|
||||
|
||||
|
||||
def live_session() -> dict | None:
|
||||
"""The current open session, if any."""
|
||||
r = _c().execute(
|
||||
@@ -326,6 +334,109 @@ def get_hand(hand_id: int) -> dict | None:
|
||||
return d
|
||||
|
||||
|
||||
def list_recent_hands(limit: int = 60) -> list[dict]:
|
||||
"""Recent recorded hands with their session's venue/stakes, for browsing."""
|
||||
rows = _c().execute(
|
||||
"SELECT h.id, h.position, h.hole_cards, h.board, h.result, h.tag, h.at, "
|
||||
"h.lesson, s.venue AS venue, s.stakes AS stakes "
|
||||
"FROM poker_hands h LEFT JOIN poker_sessions s ON s.id = h.session_id "
|
||||
"ORDER BY h.id DESC LIMIT ?", (limit,),
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
# --- session recap (.md generation on top of structured data + conversation) ---
|
||||
|
||||
_RECAP_PROMPT = """You are writing Brian's structured poker session log in Markdown, in his \
|
||||
established format, from the session DATA and CONVERSATION provided. Output ONLY the Markdown \
|
||||
— no preamble, no code fences.
|
||||
|
||||
Use these sections (skip any with no material; don't pad):
|
||||
|
||||
# YYYY-MM-DD — <venue + game/stakes>
|
||||
## Session Header
|
||||
* Date / Casino / Game & stakes / Start–End / Buy-in(s) / Cash-out / Net result
|
||||
## Money Flow
|
||||
(totals; break out by variant if multiple games were played)
|
||||
## Session Overview
|
||||
(1-2 short narrative paragraphs)
|
||||
## Timeline
|
||||
(bullets of how it went)
|
||||
## Key Hands
|
||||
(### per notable hand — Action recap → brief analysis → **Assessment:** Well Played / Leak Candidate / Cooler / Confidence Bank)
|
||||
## Table Dynamics & Villain Notes
|
||||
(### per opponent — profile + exploit)
|
||||
## Confidence Bank
|
||||
(disciplined / good process plays)
|
||||
## Scar Notes
|
||||
(mistakes and study points)
|
||||
## Mental Game Notes
|
||||
## Final Assessment
|
||||
(overall quality of play; biggest strength; biggest thing to improve; did the result match decision quality?)
|
||||
|
||||
Base everything on the actual data and conversation — do NOT invent hands, villains, or results. \
|
||||
Address Brian as "you" or "Brian", coach-to-player. Be concise but complete."""
|
||||
|
||||
|
||||
def _resolve_recap(session_id: int | None) -> int | None:
|
||||
if session_id is not None:
|
||||
return session_id
|
||||
live = live_session()
|
||||
if live:
|
||||
return live["id"]
|
||||
r = _c().execute(
|
||||
"SELECT id FROM poker_sessions WHERE status = 'closed' ORDER BY id DESC LIMIT 1"
|
||||
).fetchone()
|
||||
return int(r["id"]) if r else None
|
||||
|
||||
|
||||
def _hand_line(h: dict) -> str:
|
||||
bits = [h.get("position"), h.get("hole_cards"),
|
||||
(f"board {h['board']}") if h.get("board") else None,
|
||||
(f"result {h['result']:+g}") if h.get("result") is not None else None,
|
||||
(f"[{h['tag']}]") if h.get("tag") else None, h.get("lesson")]
|
||||
return " | ".join(str(b) for b in bits if b)
|
||||
|
||||
|
||||
def generate_recap(session_id: int | None = None, backend: str | None = None) -> dict | None:
|
||||
"""Generate Brian's .md recap from a session's structured data + conversation, store it."""
|
||||
backend = backend or "cloud"
|
||||
sid = _resolve_recap(session_id)
|
||||
if sid is None:
|
||||
return None
|
||||
s = get_session(sid)
|
||||
hands = list_hands(sid)
|
||||
reads = [dict(r) for r in _c().execute(
|
||||
"SELECT seat, note FROM player_reads WHERE session_id = ?", (sid,)).fetchall()]
|
||||
stats = session_stats(sid)
|
||||
|
||||
convo = ""
|
||||
if s.get("chat_session_id"):
|
||||
exs = [e for e in memory.history(s["chat_session_id"])
|
||||
if (e.created_at or "") >= (s.get("started_at") or "")]
|
||||
convo = "\n".join(f"{e.role}: {e.content}" for e in exs)[-12000:]
|
||||
|
||||
body = (
|
||||
"SESSION DATA:\n"
|
||||
f"- venue: {s.get('venue')} | game: {s.get('game')} | stakes: {s.get('stakes')} | format: {s.get('format')}\n"
|
||||
f"- started: {s.get('started_at')} | ended: {s.get('ended_at')} | hours: {s.get('hours')}\n"
|
||||
f"- buy-in total: {s.get('buy_in_total')} | cash out: {s.get('cash_out')} | net: {s.get('net')}\n"
|
||||
f"- mantra: {s.get('mantra')} | mood: {s.get('mood')} | "
|
||||
f"{stats.get('per_hour')}/hr | hands logged: {stats.get('hands_logged')} | tags: {stats.get('tags')}\n\n"
|
||||
"HANDS:\n" + ("\n".join("- " + _hand_line(h) for h in hands) or "(none logged)") + "\n\n"
|
||||
"READS:\n" + ("\n".join(f"- seat {r.get('seat')}: {r['note']}" for r in reads) or "(none)") + "\n\n"
|
||||
"CONVERSATION DURING SESSION:\n" + (convo or "(none captured)")
|
||||
)
|
||||
md = llm.complete(
|
||||
[{"role": "system", "content": _RECAP_PROMPT}, {"role": "user", "content": body}],
|
||||
backend=backend,
|
||||
)
|
||||
conn = _c()
|
||||
with conn:
|
||||
conn.execute("UPDATE poker_sessions SET recap_md = ? WHERE id = ?", (md, sid))
|
||||
return {"id": sid, "markdown": md}
|
||||
|
||||
|
||||
# --- villain file ---
|
||||
|
||||
def upsert_player(name: str, venue: str | None = None, description: str | None = None,
|
||||
|
||||
Reference in New Issue
Block a user