5c63175a3c
Decompose into two sub-projects: (1) harden poker.py into a standalone logging service with a complete REST API, a first-class documented tool/API contract, and a human UI to log/edit/correct everything — usable by Brian alone, zero LLM dependency; (2) wire Lyra in as a client (classifier + message-type prompts), parked. Contract is first-class because it's the shared seam for the cloud model, a fine-tuned MI50 poker model, RTO, the human UI, and a future MCP wrap. MCP deferred until a second host app exists. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G796GsLCvJQKVN7hwV2cDx
159 lines
12 KiB
Markdown
159 lines
12 KiB
Markdown
# Poker logging service + message-type prompts
|
||
|
||
- **Date:** 2026-06-28
|
||
- **Status:** Sub-project 1 spec ready for review; sub-project 2 parked.
|
||
- **Branch:** `feat/poker-mode-prompts`
|
||
|
||
## Origin
|
||
|
||
This started as "make Lyra's poker replies less generic" (message-type-specific prompts). During design we decided to **build the logging tool first** as a standalone system of record with a clean API and a human-usable UI, then wire Lyra in as a *client* of it. Rationale:
|
||
|
||
- Brian can log and **correct** data himself, independent of whether Lyra parsed it right (she mislabeled "Dave the rock" vs "Dave the mechanic" mid-session).
|
||
- The data stops being hostage to the agent. Lyra becomes one client among potentially several.
|
||
- It's reusable: RTO (the solver) and a **fine-tuned poker model on the MI50** could consume the same hand/session data through the same contract.
|
||
|
||
## Decomposition
|
||
|
||
Two sub-projects, built and shipped in order.
|
||
|
||
### Sub-project 1 — Poker logging service *(this spec)*
|
||
Harden `lyra/poker.py` into a well-bounded store, expose a **complete REST API** over it, define a **stable, documented tool/API contract**, and build the human UI to log/edit/correct everything. Fully usable by Brian alone, zero LLM dependency.
|
||
|
||
### Sub-project 2 — Lyra wiring *(parked; summarized at the end)*
|
||
Message-type classifier + type-specific prompt fragments; Lyra's tools call the sub-project 1 service. Separately, enabling tool-calling on the MI50 backend so a fine-tuned poker model can drive the same contract.
|
||
|
||
**Why the contract is first-class:** in every design (in-process, REST, MCP) the *model* never calls the API directly — it emits a tool-call and the host app executes it. So what lets the cloud model, the MI50 fine-tune, RTO, and a human UI all interoperate is a single **stable tool/API schema** (operation names + JSON arg schemas). That contract is the training target for the fine-tune and the seam for every backend. MCP is deferred: it's a thin wrap over the same service, worth adding only when a *second host application* appears.
|
||
|
||
---
|
||
|
||
# Sub-project 1 — Poker logging service
|
||
|
||
## Goals
|
||
|
||
1. A complete API surface over the poker data model — create/read/update/delete for every entity, not just the few edit/delete endpoints exposed today.
|
||
2. A single **documented, versioned tool/API contract** that the REST API, Lyra's LLM tools, the human UI, a future MCP wrapper, and the MI50 fine-tune all share.
|
||
3. A human UI to **log** (fast capture) and **edit/correct** (fix Lyra's mistakes) every entity.
|
||
4. The 2nd input box (stack quick-capture) and the iOS-PWA bottom safe-area fix.
|
||
5. Pure data capture never touches the LLM.
|
||
|
||
## Non-goals
|
||
|
||
- Lyra's classifier / prompt fragments (sub-project 2).
|
||
- Enabling tools on the MI50 backend (sub-project 2).
|
||
- MCP wrapper (deferred until a second host app exists).
|
||
- Itemized buy-in history — buy-ins stay a single `buy_in_total` scalar.
|
||
- Rewriting the SQLite schema; we build on the existing tables.
|
||
|
||
## Current state (what exists)
|
||
|
||
- **Store & logic:** `lyra/poker.py` — schema at `poker.py:21` (tables `poker_sessions`, `poker_hands`, `poker_stack_log`, `poker_rituals`, `poker_players`, `player_reads`, `player_observations`). Functions: `start_session` (157), `add_buyin` (389), `log_stack` (407), `stack_state` (446), `update_session` (362), `end_session` (515), `log_hand` (541, flat/no-LLM), `record_hand` (770, LLM-parses shorthand), `add_read` (1010), `hud` (1245).
|
||
- **Exposed endpoints (`lyra/web/server.py`):** `GET /session/data` (hud), `PATCH /session/{id}`, `DELETE /session/entry/{kind}/{id}`, `GET/DELETE /history`, `GET /hand/{id}/data`, `POST /hand/{id}/reconstruct`, `GET /hands/data`, `GET /recap/...`. **No** direct create endpoint for stack/buyin/hand/read/session — those are reachable only through chat → tool-calling.
|
||
- **UI:** `index.html` (chat), `session.html` (live HUD: stack card + sparkline + a PATCH-based edit form via `saveEdit()` at `session.html:192`, and `del(kind,id)` at `211`), `history.html`, `hand.html`.
|
||
- **Tool specs:** `lyra/tools.py` already defines arg schemas for each operation (`_f(...)` specs, `tools.py:469-658`) — the embryo of the contract.
|
||
|
||
## Design
|
||
|
||
### 1. Store layer — harden `poker.py`
|
||
|
||
Keep the existing functions and tables; tighten the module into a clean service boundary so both the REST layer and Lyra's tools call the *same* functions. Each operation: validates input, resolves the target session (`_resolve`), writes, returns a consistent dict. No behavior change to existing callers; this is consolidation, not a rewrite.
|
||
|
||
### 2. The tool/API contract *(first-class deliverable)*
|
||
|
||
A single source-of-truth document + schema defining every operation: name, purpose, JSON arg schema, return shape, and which REST route + which LLM tool map to it. Versioned (e.g. `contract_version: 1`). Lives at `docs/POKER_API.md` (or a machine-readable `poker_contract.py` that both the REST routes and `tools.py` specs derive from — preferred, so they can't drift).
|
||
|
||
Operations (the canonical set):
|
||
|
||
| Operation | Args | Entity |
|
||
|---|---|---|
|
||
| `start_session` | venue, stakes, game, format, buy_in, mantra | session |
|
||
| `update_session` | venue, stakes, game, format, buy_in_total, cash_out, mantra, mood | session |
|
||
| `end_session` | cash_out, mood | session |
|
||
| `delete_session` | id | session |
|
||
| `log_stack` | amount, note | stack entry |
|
||
| `delete_stack` | id | stack entry |
|
||
| `add_buyin` | amount | session (increments buy_in_total) |
|
||
| `log_hand` | position, hole_cards, board, streets…, pot, result, tag, lesson | hand |
|
||
| `record_hand` | shorthand (LLM-parsed) | hand |
|
||
| `update_hand` | id, any hand field | hand |
|
||
| `delete_hand` | id | hand |
|
||
| `add_read` | note, name, seat, tendencies, adjustment, category, venue | player/read |
|
||
| `update_read` / `update_player` | id, fields | player/read |
|
||
| `delete_read` | id | player/read |
|
||
| rituals: `scar_note`, `confidence_bank`, `alligator_blood`, `reset_ritual` | … | ritual |
|
||
|
||
### 3. REST API — complete the surface (`lyra/web/server.py`)
|
||
|
||
Add the missing **create/update** routes so the human UI (and any non-LLM client) can do everything:
|
||
|
||
- `POST /session/stack` → `log_stack(amount, note?)`; server-stamped time; returns `stack_state()`.
|
||
- `POST /session/buyin` → `add_buyin(amount)`; returns `buy_in_total`.
|
||
- `POST /session` → `start_session(...)`.
|
||
- `POST /session/hand` → `log_hand(...)` (flat) and/or `record_hand(shorthand)`.
|
||
- `PATCH /hand/{id}` → `update_hand(...)`; `DELETE /hand/{id}`.
|
||
- `POST /session/read` → `add_read(...)`; `PATCH /read/{id}`; `DELETE` via existing entry-delete.
|
||
- Keep existing `PATCH /session/{id}`, `DELETE /session/entry/{kind}/{id}`, `GET /session/data`.
|
||
|
||
All return `{ok, ...}` and a clear error on "no live session." Routes are thin wrappers over the store, mirroring the contract one-to-one.
|
||
|
||
### 4. Human UI — log + edit/correct
|
||
|
||
**Fast capture:**
|
||
- **2nd input box** (`index.html`): slim row **below the message input, above the bottom nav icons**. Type a number → `POST /session/stack` → time-stamped, sparkline updates, a one-line confirmation drops into the **Live Log**. **No chat message, no LLM call.** Stack-only in v1. Tolerates `$685`/`685`.
|
||
- **HUD widget** (`session.html`, in the Stack card at `:280`, mirroring `saveEdit()` at `:192`): stack field (`POST /session/stack`), buy-in field (`POST /session/buyin`), cash-out field (existing PATCH).
|
||
|
||
**Edit / correct (fix Lyra's mistakes):**
|
||
- Edit any session field (exists via the PATCH edit form — verify coverage).
|
||
- Hands list with edit + delete (`hand.html` + new PATCH/DELETE) — fix mislabeled villains, wrong board, wrong result.
|
||
- Reads/players list with edit + delete — rename "Dave the rock" ≠ "Dave the mechanic", fix tendencies.
|
||
- Stack entries deletable (exists via `del('stack', id)`) — verify.
|
||
|
||
### 5. iOS-PWA bottom safe-area fix
|
||
|
||
The empty band below the nav icons is a safe-area issue (likely `100vh` not accounting for `env(safe-area-inset-bottom)` / the home indicator). Fix the layout container + bottom nav CSS so the app fills the viewport with the icons seated above the home indicator. Use the `building-ios-pwas` skill at implementation time.
|
||
|
||
## Testing / verification
|
||
|
||
- **Contract conformance:** a test asserting each REST route and each `tools.py` spec matches the canonical contract (names, required args) — catches drift between the human API and the LLM API.
|
||
- **Endpoint round-trips:** create → read → update → delete for stack, buyin, hand, read against a test session; assert rows written, time stamped, `stack_state()`/`hud()` reflect changes; assert clean error with no live session.
|
||
- **UI manual pass:** log a stack via the 2nd box and confirm it lands in Live Log + sparkline without a chat reply; edit a hand's villain and confirm persistence; delete a bad read.
|
||
- **PWA:** on the iOS PWA, confirm the bottom gap is gone and the 2nd input box sits above the nav with the keyboard open.
|
||
|
||
---
|
||
|
||
# Sub-project 2 — Lyra wiring *(parked)*
|
||
|
||
Detail preserved here; gets its own spec → plan after sub-project 1 is MVP'd.
|
||
|
||
## Why it exists (diagnosis from real sessions)
|
||
|
||
Evidence from `sess-dff2s91c` (2026-06-27 Meadows, 2026-06-28 Wheeling):
|
||
|
||
- **Coaching essay on every turn, including pure data** — `Stack=$685` drew 4–6 sentences of "keep that momentum rolling." (Sub-project 1's dumb capture removes these from the LLM entirely.)
|
||
- **False tilt/fatigue reads** — "table broke, it's 11:50pm" → repeated "late-night fatigue… mental reset"; Brian: *"you seem to be reading me as tilted."* Cause: the `_route` mood nudge (`mind.py:328`) firing on non-mood messages.
|
||
- **No bet-intent reasoning** — a value bet ($40, full house) that folded out 88 was praised as "the power of representing something stronger." It was value *lost*, not a successful rep.
|
||
- **Eyeballs instead of `analyze_spot`** — 77 multiway got "a disciplined fold might have been better," no math, violating the persona's "never eyeball poker math" rule.
|
||
- **Even her sharp reads leak bad logic** — the Connie read included "limp-checking in position" (contradictory).
|
||
|
||
Root cause: one broad per-turn card (`_CASH_CARD`, `modes.py:66`) describes traits; the model satisfies trait language with safe abstraction.
|
||
|
||
## Planned approach
|
||
|
||
- **Classifier** (`lyra/poker_classify.py`): `classify(message) -> HAND | STATUS | MENTAL | LOG | CHAT`. Heuristic v1 (card-token regex, position/street keywords, feeling phrases, time/venue), swappable for an LLM/MI50 classifier behind the same signature. Ambiguous → CHAT.
|
||
- **Pipeline:** a `_classify` step in `mind.PIPELINE` sets `ctx.msg_type` (poker mode only); `build_messages` injects the **type fragment** in the seat now held by `mode.card` (`mind.py:152`) instead of the whole `_CASH_CARD`.
|
||
- **Fragments** (`lyra/poker_prompts.py`, `POKER_FRAGMENTS`): concrete rules + response shape per type.
|
||
- **HAND:** log it → `analyze_spot` if close → reason about **bet intent** (value/bluff/protection — did it work? a fold to a value bet = value lost, flag it) → name leaks (value-owning, missed value, sizing) → one opinion. No reflexive praise. Pull `player_profile` before referencing a villain. Seeded with 2 real-hand exemplars (the $40 value bet; the 77 spot).
|
||
- **STATUS:** 1–2 sentences, no coaching, no tilt-reading.
|
||
- **MENTAL:** full presence (already works — preserve).
|
||
- **LOG:** one line.
|
||
- **CHAT:** full voice, real opinion.
|
||
- **Pipeline fixes:** kill the misfiring `_route` mood nudge (`mind.py:328`); trim the always-on mode-menu note (`mind.py:77`) in poker mode.
|
||
- **MI50 tool-calling:** enable tools on the MI50 backend (`chat.py` `TOOL_BACKENDS = {"cloud"}` at `:21`) so a fine-tuned, tool-calling poker model can drive the contract. Requires the fine-tune to emit the contract's tool-call format.
|
||
|
||
## HAND exemplars (preserved for sub-project 2)
|
||
|
||
> *Brian:* "Flopped bottom set with 22, turned a full house, bet $40 on the river, he folded 88."
|
||
> *Lyra:* "That's a value bet that got no value — 88 was never folding to a smaller stab. $40 into that pot was too much; ~$20–25 gets called by the over-pair and worse two-pairs. The hand's a monster, the leak is the sizing."
|
||
|
||
> *Brian:* "77 multiway, flop 4h5c3c, HJ bets 75, BTN shoves 74, I call. Turn/river check through, HJ wins with 64."
|
||
> *Lyra:* "Against a bet and a shove on 4-5-3 you're drawing thin — sets, two pair, and the made wheel are all ahead, and you block almost none of it. The stack-depth read (he only had ~150 behind) is real, but that's a reason to fold and wait, not to call off light. This is the value-owning spot you flagged yourself."
|