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
This commit is contained in:
@@ -0,0 +1,117 @@
|
|||||||
|
# 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; ~$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."
|
||||||
|
|
||||||
|
**STATUS** — He's narrating logistics (time, venue, table change, waiting for a seat). Acknowledge in 1–2 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.
|
||||||
Reference in New Issue
Block a user