feat: hand-history reconstruction + replayable table viewer

Brian's idea: vomit rough shorthand, Lyra rebuilds it into a structured,
replayable hand history.

- poker.parse_hand(): focused LLM pass turning shorthand into a canonical hand
  JSON (positions, stacks, hero cards, chronological actions w/ board reveals,
  result); store_hand_history() persists JSON + extracted flat fields;
  record_hand() = parse+store; standalone hands attach to a 'Hand Reviews' session
- poker_hands gains a `structured` JSON column (ALTER-migrated for existing DBs)
- record_hand tool wired into chat: "log this hand: ..." -> reconstructed + a
  /hand/{id} link
- web: GET /hand/{id} viewer + /hand/{id}/data — a felt table with seats placed
  around the oval (hero at bottom), hole cards, progressive board reveal, and
  prev/next/end step-through of the action with running pot
- tests: store/get roundtrip, record_hand tool (stubbed parse)

Verified live: parsed a real AKs hand (BTN, 14 actions, full board) end to end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 23:11:46 +00:00
parent 16f3442640
commit 9491951da0
5 changed files with 412 additions and 3 deletions
+10 -1
View File
@@ -18,7 +18,7 @@ from fastapi import FastAPI, Request
from fastapi.responses import FileResponse, StreamingResponse
from fastapi.staticfiles import StaticFiles
from lyra import chat, logbus, memory, self_state, summary
from lyra import chat, logbus, memory, poker, self_state, summary
from lyra.llm import Backend
@@ -141,6 +141,15 @@ def create_app() -> FastAPI:
async def journal_data(limit: int = 300) -> dict:
return {"entries": memory.list_journal(limit=limit)}
@app.get("/hand/{hand_id}")
async def hand_page(hand_id: int) -> FileResponse:
"""Replayable hand-history viewer."""
return FileResponse(str(_STATIC / "hand.html"))
@app.get("/hand/{hand_id}/data")
async def hand_data(hand_id: int) -> dict:
return poker.get_hand(hand_id) or {}
@app.get("/stream/logs")
async def stream_logs(request: Request) -> StreamingResponse:
"""Live activity feed: replay the recent buffer, then stream new events."""