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:
2026-06-18 00:36:52 +00:00
parent fc06b24528
commit 7b65f81d7e
7 changed files with 353 additions and 12 deletions
+16
View File
@@ -90,6 +90,7 @@ def _start_session(args: dict, ctx: dict) -> str:
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 '?'} "
@@ -163,6 +164,15 @@ def _record_hand(args: dict, ctx: dict) -> str:
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 _villain_file(args: dict, ctx: dict) -> str:
vs = poker.get_villain_file(name=args.get("name"), venue=args.get("venue"))
if not vs:
@@ -255,6 +265,12 @@ TOOLS.update({
"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.",
{}, [])},
"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.",