Files
project-lyra/docs/superpowers/specs/2026-07-01-poker-prompts-design.md
serversdown 3afa75f4be docs: spec for poker message-type prompts (sub-project 2)
Phase A pipeline fixes (suppress the misfiring _route mood nudge + the
always-on mode-menu note in poker mode), Phase B classifier
(HAND/STATUS/MENTAL/LOG/CHAT) + per-type fragments replacing the
monolithic _CASH_CARD, Phase C MI50 tool-calling. NLH-only HAND
reasoning; PLO hands logged/replayed but not analyzed this pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G796GsLCvJQKVN7hwV2cDx
2026-07-01 01:48:31 +00:00

118 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Poker message-type prompts (sub-project 2)
- **Date:** 2026-07-01
- **Status:** Spec for review
- **Branch:** `feat/poker-mode-prompts` (continues on the same branch; sub-project 1 shipped there)
- **Supersedes:** the parked "sub-project 2" section of `docs/superpowers/specs/2026-06-28-poker-mode-prompts-design.md`
## Problem (recap)
In poker mode Lyra routes correctly but her replies are generic — one broad `_CASH_CARD` (`lyra/modes.py:66-116`) describes *traits* and gets injected on every turn, so the model satisfies it with safe, flattering abstraction. From real sessions: coaching essays on bare stack updates, false tilt/fatigue reads on neutral logistics ("table broke, it's 11:50pm" → "late-night fatigue…"), praising a value bet that got *no* value, and hedging ("a disciplined fold might have been better") instead of calling `analyze_spot`.
The fix: stop sending one card for every message. Detect *what kind of message* Brian just sent and inject a small, concrete response contract for that type.
## Goals
1. A per-turn **message-type classifier** for poker mode, and **per-type prompt fragments** replacing the monolithic card.
2. Kill the two pipeline sources of mush in poker mode: the misfiring mood nudge and the always-on mode-menu note.
3. Make HAND turns reason about **bet intent** and lean on `analyze_spot` (NLH only).
4. Keep the door open for a fine-tuned MI50 classifier/model behind the same seams.
## Non-goals
- PLO/Omaha strategic analysis. `record_hand` already parses 4-card hands and the replayer renders them; only `analyze_spot` (equity) is NLH-bound. **This pass: PLO hands are logged/replayed but get no NLH-style analysis.**
- A PLO equity engine.
- Changing the store, the REST API, or the tools (sub-project 1, done).
- An LLM classifier in v1 (heuristic first; the function is the swappable seam).
## Build order (confirmed)
**Phase A — pipeline fixes** (quick win) → **Phase B — classifier + fragments** (the meat) → **Phase C — MI50 tool-calling** (separable).
---
## Phase A — Pipeline fixes
Both are independent of the classifier and immediately reduce mush in poker mode.
1. **Suppress the mode-menu note in poker mode.** `_mode_menu_note` (`mind.py:77-88`) is injected every turn (`mind.py:158`). At the table she should not be offering to switch modes. In `build_messages`, skip that append when `mode.key == "poker_cash"`.
2. **Suppress the `_route` mood nudge in poker mode.** `_route` (`mind.py:320-339`) sets `ctx.register` + a "steady/hype" note from a lexicon heuristic; in poker this double-signals with the card and caused the false tilt reads. In `_route`, when `mode.key == "poker_cash"`, resolve the mode as normal (line 324 stays) but **skip the register/note block** (327-338). Poker register comes from the Phase B fragments (esp. MENTAL) instead. Non-poker modes keep the nudge unchanged.
## Phase B — Classifier + per-type fragments
### New module `lyra/poker_prompts.py`
Cohesive home for poker prompting: the classifier, a lean always-on base, and the per-type fragments.
```
classify(user_msg: str) -> str # "HAND" | "STATUS" | "MENTAL" | "LOG" | "CHAT"
BASE: str # always-on poker rules (logging, session_state, rituals, equity)
FRAGMENTS: dict[str, str] # msg_type -> response-shape contract
fragment_for(msg_type: str | None) -> str # FRAGMENTS.get(msg_type, FRAGMENTS["CHAT"])
```
`classify` is a **pure function** (no DB), unit-tested like `perceive.read`. Heuristic signals, first match wins in priority order:
1. **HAND** — card tokens (regex `\b[2-9TJQKA][shdc]\b`, ≥2), or position tokens (UTG/MP/HJ/CO/BTN/SB/BB/"button"/"hijack"/"straddle"), or a street word (flop/turn/river) with a betting verb (bet/raise/call/fold/check/shove/limp/jam).
2. **MENTAL** — first-person feeling: "I feel", "I'm tilted/steaming/fried/tired/frustrated/confident/stuck/bored", "on tilt", "in my head", "mental", "leak".
3. **STATUS** — logistics with no cards: "table broke", "new table", "waiting for a seat", "seat opened", "just sat", clock times, "heading to"/venue mentions.
4. **LOG** — bare money/result prose that slipped past the quick-capture box: "I'm at", "stack is", "down to", "up to", "out for", "cashed", "rebought", "rebuy" with a number.
5. **CHAT** — default fallback (questions, open talk).
(HAND wins over MENTAL so a described hand still gets logged even if he's venting; the HAND fragment tells her to acknowledge the feeling too.)
### Injection (`mind.py`)
- Add `msg_type: str | None = None` to `TurnContext` (`mind.py:305`).
- In `_route`, when `mode.key == "poker_cash"`, set `ctx.msg_type = poker_prompts.classify(ctx.user_msg)`.
- Thread it through `_compose` → add a `msg_type` param to `build_messages` (`mind.py:137`, `344`).
- Replace the card-injection block (`mind.py:152-154`) with:
```python
if mode and mode.key == "poker_cash":
messages.append({"role": "system", "content": poker_prompts.BASE})
messages.append({"role": "system", "content": poker_prompts.fragment_for(msg_type)})
elif mode and mode.card:
messages.append({"role": "system", "content": mode.card})
```
- Set `CASH.card = ""` in `modes.py` (content moves to `poker_prompts`; keep `_CASH_CARD` text as the source material to distill from, then delete once fragments are in). `CASH.tools` is unchanged.
### The fragments (concrete contracts, not traits)
**BASE** (always-on in poker) — distilled from the card's cross-cutting rules: log any trackable fact FIRST then reply (stack→`log_stack`, hand→`record_hand`, read→`add_read`, rebuy→`add_buyin`); for any equity/who's-ahead question call `analyze_spot`, never eyeball; when he asks where he's at (stack/net/gator), call `session_state` and answer from it; rituals (`scar_note`/`confidence_bank`/`alligator_blood`/`reset_ritual`) — run them in his language, honest punt-vs-cooler line, never invent one.
**HAND** — Log it (`record_hand`). Then **if it's NLH**: reason about **bet intent** — for each meaningful bet name what it was for (value / bluff / protection) and whether it worked (*a fold to a value bet = value left behind — flag it; a call of a bluff = it failed*); call `analyze_spot` for a close equity/who's-ahead spot; name leaks plainly (value-owning, missed value, sizing); give ONE real opinion. **No reflexive praise** — don't open with "nice hand." If a named villain is referenced, call `player_profile` first — never invent a read. **If it's PLO/Omaha or any non-NLH game**, just confirm it's logged and offer at most a light read; do NOT attempt NLH-style equity or deep analysis. Talk in prose, not a listicle. Seeded with 2 real-hand exemplars:
> *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; ~$2025 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."
**STATUS** — He's narrating logistics (time, venue, table change, waiting for a seat). Acknowledge in 12 sentences, log a stack only if a bare number is present, then stop. **No coaching, no strategy dump, and do NOT read him as tilted/tired/impatient — a neutral update is not a mood.**
**MENTAL** — He told you how he's feeling. This is when he needs you most. Drop the shorthand, full presence, real voice — talk him down off tilt, hold him disciplined through a card-dead stretch, engage the mental game honestly. Never a clipped confirmation.
**LOG** — He handed you a bare fact (stack/result/buyin) that isn't already captured. Log it, confirm in ONE short line ("$317 logged."), stop. No coaching.
**CHAT** — Open talk or a question that isn't a specific hand. Your real voice, an actual opinion, no filler sign-offs. If it's a concrete strategy spot, engage it for real (call `analyze_spot` when there are cards).
## Phase C — MI50 tool-calling
Flip `TOOL_BACKENDS = {"cloud"}` → `{"cloud", "mi50"}` (`chat.py:21`). Precondition: the MI50's llama.cpp server must be launched with `--jinja` (per the existing comment) or tool calls 500. This lets a tool-calling model on the MI50 drive the same contract from sub-project 1. If a tool ever needs `msg_type`, add it to the dispatch dict (`chat.py:100`/`128`) — the pipeline `TurnContext` does not currently flow into the tool loop. Ship this only once the MI50 backend is `--jinja`-enabled and a tool-capable model is loaded.
## Testing
- **`classify` unit tests** (pure, no DB — mirror `test_perceive.py` top): real messages from the transcripts →
`"Button straddle on. I limp UTG with 22. Flop 2d7cjh…"` → `HAND`;
`"table broke, it's 11:50pm"` → `STATUS`;
`"I feel like I'm being mean when I raise"` → `MENTAL`;
`"I'm at 317 now"` → `LOG`;
`"should I have folded the river?"` → `CHAT` (no cards) — or `HAND` if cards present.
- **`build_messages` fragment injection** (blob-join pattern from `test_chat.py:57-70`): in poker mode, a HAND message includes the HAND fragment string and NOT the STATUS one; a STATUS message includes STATUS and NOT HAND; assert `poker_prompts.BASE` is always present in poker mode.
- **Pipeline fixes**: `assemble` in poker mode on a tilt-lexicon message → `turn.register is None` and no tilt note in the system blob (nudge suppressed); the mode-menu note string is absent in poker mode and present in a non-poker mode.
- **No regressions**: full suite green (currently 123).
## Rollout
Phase A and Phase B ship together as the meaningful behavior change (A alone leaves the card in place). Phase C waits on the MI50 `--jinja` flag. Verify live in a real/replayed session before merging the branch.