Compare commits
1 Commits
d4e203b00c
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 267b6ad7ba |
-118
@@ -1,118 +0,0 @@
|
|||||||
# Lyra — Roadmap / To-Do
|
|
||||||
|
|
||||||
Living doc. Working priorities and open threads, organized by area. Not a spec —
|
|
||||||
specs live in `docs/` and `docs/superpowers/specs/`; this is the map of what's
|
|
||||||
done, what's next, and what's parked.
|
|
||||||
|
|
||||||
- **Last updated:** 2026-07-05
|
|
||||||
- **Frame (the load-bearing lens):** Lyra is the AI-with-tools (unchanged). The
|
|
||||||
**pokerlog is its own separable system-of-record** — she's a *client* of it via
|
|
||||||
tools, not its container. The logger must be correct/trustworthy first; Lyra's
|
|
||||||
value (memory, recall, scouting, coaching) rides on top. Two kinds of memory,
|
|
||||||
kept distinct: the **ledger** (facts: hands/villains/stats) vs the
|
|
||||||
**relationship** (her memory of the sessions). See the `poker-copilot` memory +
|
|
||||||
`docs/poker-logging-service` spec.
|
|
||||||
|
|
||||||
Status key: ✅ done · 🔨 in progress · ⬜ queued · ⏸ blocked/waiting · 💭 decision open
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompting (poker mode)
|
|
||||||
|
|
||||||
Spec: `docs/superpowers/specs/2026-07-01-poker-prompts-design.md`
|
|
||||||
|
|
||||||
- ✅ **Phase A — pipeline fixes.** Suppress the mode-menu note + the false-tilt
|
|
||||||
mood nudge in poker_cash.
|
|
||||||
- ✅ **Phase B — classifier + fragments.** `lyra/poker_prompts.py`: pure
|
|
||||||
`classify(msg, roster_handles)` (READ|HAND|TABLE|MENTAL|STATUS|LOG|CHAT), lean
|
|
||||||
always-on `BASE`, per-type `FRAGMENTS`. Wired into `build_messages`; the
|
|
||||||
~100-line `_CASH_CARD` monolith is sharded out (`CASH.card=""`).
|
|
||||||
- ✅ **Deleted the dead `_CASH_CARD`** monolith (modes.py 256→161 lines).
|
|
||||||
- ✅ **Classifier hardening (round 1).** Fixed 6 real gaps found by probing
|
|
||||||
live-style phrasings (-ing action forms, "the whale" bare descriptor, player
|
|
||||||
departures→TABLE, "stack" leaking questions into LOG, thin MENTAL lexicon). 18
|
|
||||||
unit tests. Still a heuristic + swappable seam — upgrade to an LLM/MI50
|
|
||||||
classifier only if live misses justify it; keep tuning against real transcripts.
|
|
||||||
- ✅ **Phase C — MI50 tool-calling (LIVE 2026-07-06).** Added `--jinja` to the
|
|
||||||
lyra-brain llama.cpp launch (`/opt/models/docker-compose.yml` in CT202, so it's
|
|
||||||
reboot-resilient); Qwen2.5-32B confirmed emitting real `tool_calls`. Flipped
|
|
||||||
`TOOL_BACKENDS=cloud,mi50` in `.env`. MI50 chat turns now get the same tool
|
|
||||||
contract as cloud. (Untested in a real poker session on the mi50 backend — worth
|
|
||||||
a live check that tool-calling holds up under the full poker prompt.)
|
|
||||||
|
|
||||||
## Persona (the "person" layer)
|
|
||||||
|
|
||||||
The persona core is always-on (~719 tok). Identity legitimately earns always-on
|
|
||||||
status, but there's fat.
|
|
||||||
|
|
||||||
- ⬜ **Streamline `How you talk`.** It's 439 tok (61% of the core) with loose
|
|
||||||
prose. Keep the load-bearing rules (prose-not-listicle, give opinions, no
|
|
||||||
reflexive sign-offs, own your moods) but tighten to ~250 tok. ~180 tok saved,
|
|
||||||
zero substance lost. (Brian flagged 2026-07-05.)
|
|
||||||
- ⬜ **Broader persona review.** Take a full pass at `lyra/personas/lyra.md` — is
|
|
||||||
each section earning its place, always-on vs situational split right, anything
|
|
||||||
stale or redundant? (Brian flagged 2026-07-05.)
|
|
||||||
- ⬜ **Fix/demote the stale `Right now` section.** It asserts "stats tracking,
|
|
||||||
player profiling… are coming" — both are SHIPPED. It's status prose that
|
|
||||||
shouldn't be always-on and drifts stale. Demote from core → situational (loads
|
|
||||||
only when she's asked what she can do), or fold into the tool-self-knowledge
|
|
||||||
layer below. −74 tok/turn + stops asserting wrong status.
|
|
||||||
|
|
||||||
## Tool self-knowledge ("a person with strong tools")
|
|
||||||
|
|
||||||
She can *call* tools but doesn't *know*, as a person, what she can do — no standing
|
|
||||||
self-knowledge of her hands.
|
|
||||||
|
|
||||||
- ⬜ **Capability self-knowledge, generated from the tool registry.** A
|
|
||||||
`tools.capability_summary()` rendering the live `TOOLS` dict into a grouped,
|
|
||||||
first-person "here's what I can do" — self-maintaining, can't drift. Inject in
|
|
||||||
the self/meta persona sections (occasional, NOT every turn — keeps the hot path
|
|
||||||
lean).
|
|
||||||
- ⬜ **Grounding principle (level 2).** Lean always-on line: facts come from
|
|
||||||
tools/memory, never confabulate, "let me check" is always allowed. Reinforces
|
|
||||||
BASE's log-first rule; important under the system-of-record frame.
|
|
||||||
- ⬜ **Agency framing (level 3).** Tools are HERS — reached for because she wants
|
|
||||||
to help, not an external API. Tone in the persona.
|
|
||||||
- Note: composes with the prompting work — capability self-knowledge = IDENTITY
|
|
||||||
(occasional); BASE = operational routing (always-on poker). Don't duplicate the
|
|
||||||
tool list across both registers.
|
|
||||||
|
|
||||||
## Pokerlog separation (architecture)
|
|
||||||
|
|
||||||
The domain is well-isolated (`lyra/poker.py`, one 2000-line pack) but still an
|
|
||||||
in-process module sharing `lyra.db` and reaching into `lyra.memory`/`llm`.
|
|
||||||
|
|
||||||
- 💭 **Decide how far to physically separate now** (Brian, not yet decided):
|
|
||||||
- **A. Logical API boundary** — everything goes through a defined interface,
|
|
||||||
still in `lyra.db`. Cheapest.
|
|
||||||
- **B. Own datastore + package, same repo** (my rec) — own DB, no reach-back
|
|
||||||
into Lyra; standalone-able without a second service to run. Biggest concrete
|
|
||||||
change: poker tables currently live IN `lyra.db`.
|
|
||||||
- **C. Full standalone MCP/HTTP service** — separate process, agent-agnostic
|
|
||||||
(any harness could drive it). Purist end; most work.
|
|
||||||
- Origin of the frame: Lyra-as-poker-agent was contingent (ChatGPT couldn't call
|
|
||||||
tools, Lyra could). The real need was "an agent that can drive my pokerlog" →
|
|
||||||
the logger should be agent-agnostic. See `docs/poker-logging-service` spec.
|
|
||||||
|
|
||||||
## Poker logger (the ledger — features)
|
|
||||||
|
|
||||||
- 🔨 **Roster active/seen (two lists).** `session_players.active` already backs
|
|
||||||
it; surface the seen side. `session_roster()` = active; add `session_seen()` =
|
|
||||||
active=0; HUD shows 🪑 At the table + 👋 Seen tonight. Re-seating flips seen→
|
|
||||||
active. Classifier's READ↔HAND match should check active + seen handles. (Brian's
|
|
||||||
idea, 2026-07-05.)
|
|
||||||
- ⬜ **Human-editability sweep.** System-of-record must be fixable. Hand editor +
|
|
||||||
disown ✅, `/players` browser + identity queue ✅. Audit for gaps (session-level
|
|
||||||
edits, read edits, bulk fixes).
|
|
||||||
- Shipped this stretch: scouting desk (proactive recall + nameless-villain
|
|
||||||
identity, all 6 phases), roster seat/unseat/clear, observed-hand fix + hand
|
|
||||||
editor, villain-dup fix, conversation export (+ tool events), session-scoped
|
|
||||||
notes, no-cache app-shell header.
|
|
||||||
|
|
||||||
## Parked / longer-horizon
|
|
||||||
|
|
||||||
- Moonshots live in `docs/PARKED_IDEAS.md` (own model, memory-as-vectors, prompt
|
|
||||||
compression, RTO/cfr-core solver tooling).
|
|
||||||
- Metacognitive reflection loop (self-model Part 2) — queued self/experiment work.
|
|
||||||
- PLO/Omaha strategic analysis (equity engine) — non-goal for now; PLO hands are
|
|
||||||
logged/replayed but not NLH-analyzed.
|
|
||||||
@@ -1,83 +1,10 @@
|
|||||||
# Poker message-type prompts (sub-project 2)
|
# Poker message-type prompts (sub-project 2)
|
||||||
|
|
||||||
- **Date:** 2026-07-01 (**readjusted 2026-07-04** — see below)
|
- **Date:** 2026-07-01
|
||||||
- **Status:** Spec — **needs rework before build** (foundations shifted; nothing here built yet)
|
- **Status:** Spec for review
|
||||||
- **Branch:** `feat/poker-mode-prompts` (continues on the same branch; sub-project 1 shipped there)
|
- **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`
|
- **Supersedes:** the parked "sub-project 2" section of `docs/superpowers/specs/2026-06-28-poker-mode-prompts-design.md`
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚠ Readjustment — 2026-07-04 (read this first)
|
|
||||||
|
|
||||||
A long live-session build on `feat/poker-mode-prompts` (the "scouting desk" +
|
|
||||||
roster work — see `docs/SCOUTING_DESK.md` and commits after `3afa75f`) landed
|
|
||||||
**after** this spec was written and changes its foundations. Nothing in Phases
|
|
||||||
A/B/C is built yet, but the plan below must absorb these deltas before it's coded.
|
|
||||||
The core idea — *classify the turn, inject a small per-type contract instead of one
|
|
||||||
giant card* — is now **more** justified (the card nearly doubled). But:
|
|
||||||
|
|
||||||
1. **The real failure mode shifted from mush to MISSED TOOL CALLS.** Live, the
|
|
||||||
pain wasn't flattering essays — it was reads/TAGs not getting logged, and
|
|
||||||
"clear the table" claimed-but-not-done. So every action-type fragment (LOG,
|
|
||||||
READ, TABLE, HAND) needs a hard *"call the tool FIRST, every time, then one
|
|
||||||
short line"* contract. This raises the stakes on Phase B and validates the
|
|
||||||
whole dynamic approach (a targeted directive beats a 100-line card).
|
|
||||||
|
|
||||||
2. **The taxonomy is missing two types that dominated the session:**
|
|
||||||
- **READ** (a *villain's* action): "TAG limped A4o in the SB", "Jonathan
|
|
||||||
called the 3bet". Under the current classifier rules these misfire as **HAND**
|
|
||||||
(card tokens + position + a betting verb) and get logged as *Brian's* hand.
|
|
||||||
They must route to **`add_read`** on the named player/handle/descriptor — NOT
|
|
||||||
`record_hand`. New priority rule, ABOVE HAND: if the actor is another player
|
|
||||||
(a handle/name/descriptor is the subject, not "I/me/my"), it's a READ.
|
|
||||||
Handles are often initials/all-caps (e.g. **TAG** is a *person*, not the
|
|
||||||
tight-aggressive style).
|
|
||||||
- **TABLE** (roster ops): "seat the table: TAG, Jonathan…", "table broke",
|
|
||||||
"I got moved", "TAG left". These now have real tool actions
|
|
||||||
(**`seat_players` / `clear_table` / `unseat_player`**), not just "acknowledge
|
|
||||||
and stop." Split these out of STATUS (STATUS stays for pure logistics with no
|
|
||||||
roster action).
|
|
||||||
|
|
||||||
3. **HAND now has a hero-vs-observed distinction.** The parser gained
|
|
||||||
`hero_involved`; a hand Brian *watched* between others is logged with null hero
|
|
||||||
fields (not pinned to him). The HAND fragment must tell her: if he was in it →
|
|
||||||
`record_hand` as hero + analysis; if he only watched → it's really READ(s) on
|
|
||||||
the players, or an observed hand — never analyze it as his.
|
|
||||||
|
|
||||||
4. **A new live per-turn injection layer already exists: the scouting desk**
|
|
||||||
(`lyra/scouting.py`, injected in `build_messages` at the poker-mode gate,
|
|
||||||
~`mind.py:177`). It dynamically adds a `SCOUTING DESK` note (named/descriptor
|
|
||||||
villain recall + leak/pattern recall) every poker turn, fail-safe. **The
|
|
||||||
classifier/fragment injection must compose with it, not duplicate it:** the
|
|
||||||
desk supplies *who this villain is / past leaks*; the fragments supply *response
|
|
||||||
shape + which tool to call*. Both are system-note appends in the same block.
|
|
||||||
|
|
||||||
5. **BASE must cover the expanded toolset + identity rules.** Beyond the original
|
|
||||||
tools, BASE now routes: `seat_players`/`unseat_player`/`clear_table` (roster),
|
|
||||||
`add_read` with **`name` OR `descriptor`** (nameless villains), `name_villain`
|
|
||||||
and `link_villains` (confirm-loop). Plus the hard rules learned live: `name` =
|
|
||||||
real handle ONLY (a description in `name` spawns duplicates — put the look in
|
|
||||||
`descriptor`); confirm before merging; never claim a tool ran without calling it.
|
|
||||||
|
|
||||||
6. **Source material grew (good news).** `_CASH_CARD` is now `modes.py:67-169`
|
|
||||||
(was 66-116) and much of the new text — roster, TAG/read routing, PLAYERS,
|
|
||||||
session-narration `note` rules — is already the *concrete, tool-routing
|
|
||||||
contract* this spec wanted, not traits. Better raw material to distill into
|
|
||||||
BASE + fragments than the original vague card.
|
|
||||||
|
|
||||||
7. **Phase A is still unbuilt and still valid.** `_mode_menu_note` is still
|
|
||||||
appended every turn (`mind.py:162`); the `_route` mood nudge still fires. The
|
|
||||||
scouting-desk work already established the `mode.key == "poker_cash"` gate to
|
|
||||||
reuse. (Note: revalidate all `mind.py` line numbers below — they've drifted.)
|
|
||||||
|
|
||||||
**Net:** taxonomy becomes **HAND / READ / TABLE / STATUS / MENTAL / LOG / CHAT**;
|
|
||||||
fragments lead with a hard tool-call contract; injection sits alongside the
|
|
||||||
scouting desk; BASE lists the full current toolset. The rest of the plan stands.
|
|
||||||
The classifier/dynamic-prompting build is being explored in a separate session —
|
|
||||||
this doc is its poker-side source of truth.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Problem (recap)
|
## 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`.
|
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`.
|
||||||
@@ -118,23 +45,21 @@ Both are independent of the classifier and immediately reduce mush in poker mode
|
|||||||
Cohesive home for poker prompting: the classifier, a lean always-on base, and the per-type fragments.
|
Cohesive home for poker prompting: the classifier, a lean always-on base, and the per-type fragments.
|
||||||
|
|
||||||
```
|
```
|
||||||
classify(user_msg: str) -> str # "READ"|"HAND"|"TABLE"|"MENTAL"|"STATUS"|"LOG"|"CHAT"
|
classify(user_msg: str) -> str # "HAND" | "STATUS" | "MENTAL" | "LOG" | "CHAT"
|
||||||
BASE: str # always-on poker rules (logging, tools, session_state, rituals, equity)
|
BASE: str # always-on poker rules (logging, session_state, rituals, equity)
|
||||||
FRAGMENTS: dict[str, str] # msg_type -> response-shape contract
|
FRAGMENTS: dict[str, str] # msg_type -> response-shape contract
|
||||||
fragment_for(msg_type: str | None) -> str # FRAGMENTS.get(msg_type, FRAGMENTS["CHAT"])
|
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 (**updated 2026-07-04** — READ + TABLE added):
|
`classify` is a **pure function** (no DB), unit-tested like `perceive.read`. Heuristic signals, first match wins in priority order:
|
||||||
|
|
||||||
1. **READ** — *another player* did something. A handle/name/descriptor is the actor (not "I/me/my") followed by a poker action: "TAG limped A4o", "Jonathan called the 3bet", "the neck-tattoo guy shoved". Route → `add_read(name|descriptor, note)`. **Must beat HAND** — these carry card/position/verb tokens but are NOT Brian's hand. Signal: a leading proper-noun/handle/ALL-CAPS token or a descriptor phrase as the subject, with no first-person holding. (Hard case: disambiguating a bare "limped A4o" with no clear subject — default to HAND if he's the implied actor, READ if a named player is.)
|
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. **HAND** — *Brian's* hand: first-person + card tokens (`\b[2-9TJQKA][shdc]\b`, ≥2) / position tokens (UTG/MP/HJ/CO/BTN/SB/BB/button/hijack/straddle) / a street word (flop/turn/river) with a betting verb. The fragment handles hero-vs-observed (`hero_involved`): if he only watched, treat as READ(s)/observed, don't analyze as his.
|
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. **TABLE** — roster ops with a tool action: "seat the table: …", "table broke", "they broke us", "I got moved", "switched tables", "TAG left/busted", "new guy in seat 3". Route → `seat_players` / `clear_table` / `unseat_player`. (Was folded into STATUS; now distinct because it *does* something.)
|
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. **MENTAL** — first-person feeling: "I feel", "I'm tilted/steaming/fried/tired/frustrated/confident/stuck/bored", "on tilt", "in my head", "mental", "leak".
|
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. **STATUS** — pure logistics, no roster action, no cards: clock times, "waiting for a seat", "heading to"/venue mentions, bathroom/break. (Table changes moved to TABLE.)
|
5. **CHAT** — default fallback (questions, open talk).
|
||||||
6. **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.
|
|
||||||
7. **CHAT** — default fallback (questions, open talk).
|
|
||||||
|
|
||||||
(READ beats HAND so a villain's action lands on their file, not Brian's. HAND beats MENTAL so a described hand still gets logged even if he's venting; the HAND fragment acknowledges the feeling too.)
|
(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`)
|
### Injection (`mind.py`)
|
||||||
|
|
||||||
@@ -153,11 +78,7 @@ fragment_for(msg_type: str | None) -> str # FRAGMENTS.get(msg_type, FRAGMENTS[
|
|||||||
|
|
||||||
### The fragments (concrete contracts, not traits)
|
### 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, and **never claim a tool ran without calling it**. Tool routing (full current set as of 2026-07-04): his stack→`log_stack`; his hand→`record_hand`; a *villain's* action→`add_read` (with `name` for a real handle, or `descriptor` for a nameless player — a physical description in `name` spawns duplicates); rebuy→`add_buyin`; who's-at-the-table→`seat_players`/`unseat_player`/`clear_table`; attaching a caught name to a described player→`name_villain`; confirmed same/different person→`link_villains` (never merge on a guess). 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. (A `SCOUTING DESK` note may already be in context with a player's history — cite it, don't re-fetch or invent.)
|
**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.
|
||||||
|
|
||||||
**READ** *(new 2026-07-04)* — a villain did something and he wants it on their file. Call `add_read(name|descriptor, note)` FIRST, before replying — every time; this is the job that was silently getting skipped. A handle (often initials/ALL-CAPS like TAG) is a PERSON, not a play-style. If the player is on the roster, attach by that handle; if unnamed, use `descriptor`. Confirm in one short line ("Noted on TAG — limped A4o SB."). Optional: one crisp read if it's exploitable, but the log is mandatory, the commentary is not.
|
|
||||||
|
|
||||||
**TABLE** *(new 2026-07-04)* — roster management. "seat the table: …" → `seat_players`; a table change ("table broke", "I got moved", "switched tables") → `clear_table` then wait for the new roster; someone leaves/busts → `unseat_player`. Do the tool call, confirm one line, don't narrate. The session/stack keep going through a table change — only who's seated resets.
|
|
||||||
|
|
||||||
**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:
|
**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:
|
||||||
|
|
||||||
@@ -167,7 +88,7 @@ fragment_for(msg_type: str | None) -> str # FRAGMENTS.get(msg_type, FRAGMENTS[
|
|||||||
> *Brian:* "77 multiway, flop 4h5c3c, HJ bets 75, BTN shoves 74, I call. Turn/river check through, HJ wins with 64."
|
> *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."
|
> *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** — Pure logistics with no roster action (time, venue, waiting for a seat, break). *(Table changes now route to TABLE.)* 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.**
|
**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.
|
**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.
|
||||||
|
|
||||||
@@ -182,14 +103,11 @@ Flip `TOOL_BACKENDS = {"cloud"}` → `{"cloud", "mi50"}` (`chat.py:21`). Precond
|
|||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
- **`classify` unit tests** (pure, no DB — mirror `test_perceive.py` top): real messages from the transcripts →
|
- **`classify` unit tests** (pure, no DB — mirror `test_perceive.py` top): real messages from the transcripts →
|
||||||
`"TAG limped A4o in the SB (UTG straddled)"` → `READ` (villain action, must NOT be HAND);
|
`"Button straddle on. I limp UTG with 22. Flop 2d7cjh…"` → `HAND`;
|
||||||
`"Button straddle on. I limp UTG with 22. Flop 2d7cjh…"` → `HAND` (first-person);
|
`"table broke, it's 11:50pm"` → `STATUS`;
|
||||||
`"seat the table: TAG, Jonathan, Wheelz"` → `TABLE`; `"table broke, I'm at a new table"` → `TABLE`;
|
|
||||||
`"it's 11:50pm, waiting for a seat"` → `STATUS`;
|
|
||||||
`"I feel like I'm being mean when I raise"` → `MENTAL`;
|
`"I feel like I'm being mean when I raise"` → `MENTAL`;
|
||||||
`"I'm at 317 now"` → `LOG`;
|
`"I'm at 317 now"` → `LOG`;
|
||||||
`"should I have folded the river?"` → `CHAT` (no cards) — or `HAND` if cards present.
|
`"should I have folded the river?"` → `CHAT` (no cards) — or `HAND` if cards present.
|
||||||
Include the READ-vs-HAND boundary explicitly (named subject → READ; first-person → HAND).
|
|
||||||
- **`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.
|
- **`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.
|
- **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).
|
- **No regressions**: full suite green (currently 123).
|
||||||
|
|||||||
+6
-7
@@ -15,11 +15,10 @@ from lyra import tools as toolkit
|
|||||||
from lyra.llm import Backend
|
from lyra.llm import Backend
|
||||||
|
|
||||||
MAX_TOOL_ROUNDS = 5 # cap tool-call iterations per turn
|
MAX_TOOL_ROUNDS = 5 # cap tool-call iterations per turn
|
||||||
# Which backends get function-calling tools is config-driven (cfg.tool_backends,
|
# Backends that support function-calling. The MI50's llama.cpp server only does
|
||||||
# env TOOL_BACKENDS, default "cloud"). The MI50's llama.cpp server only does tools
|
# tools when launched with --jinja; until it is, keep tools to cloud so MI50 chat
|
||||||
# when launched with --jinja + a tool-capable model, else it 500s on the tools
|
# doesn't 500 on the tools param. Add "mi50" here once that flag is set.
|
||||||
# param — so enabling "mi50" is a config flip once that precondition holds (Phase C),
|
TOOL_BACKENDS = {"cloud"}
|
||||||
# not a code change. See docs/superpowers/specs/2026-07-01-poker-prompts-design.md.
|
|
||||||
_TANGLED = "(I got tangled using my tools there — say that again?)"
|
_TANGLED = "(I got tangled using my tools there — say that again?)"
|
||||||
|
|
||||||
|
|
||||||
@@ -98,7 +97,7 @@ def respond(session_id: str, user_msg: str, backend: Backend = "cloud",
|
|||||||
|
|
||||||
turn = mind.assemble(session_id, user_msg, backend, model)
|
turn = mind.assemble(session_id, user_msg, backend, model)
|
||||||
messages = turn.messages
|
messages = turn.messages
|
||||||
tool_specs = toolkit.specs(turn.mode.tools) if backend in cfg.tool_backends else None
|
tool_specs = toolkit.specs(turn.mode.tools) if backend in TOOL_BACKENDS else None
|
||||||
ctx = {"session_id": session_id, "backend": backend}
|
ctx = {"session_id": session_id, "backend": backend}
|
||||||
|
|
||||||
# Persist the user turn before the tool loop so its timestamp precedes any
|
# Persist the user turn before the tool loop so its timestamp precedes any
|
||||||
@@ -128,7 +127,7 @@ def respond_stream(session_id: str, user_msg: str, backend: Backend = "cloud",
|
|||||||
|
|
||||||
turn = mind.assemble(session_id, user_msg, backend, model)
|
turn = mind.assemble(session_id, user_msg, backend, model)
|
||||||
messages = turn.messages
|
messages = turn.messages
|
||||||
tool_specs = toolkit.specs(turn.mode.tools) if backend in cfg.tool_backends else None
|
tool_specs = toolkit.specs(turn.mode.tools) if backend in TOOL_BACKENDS else None
|
||||||
ctx = {"session_id": session_id, "backend": backend}
|
ctx = {"session_id": session_id, "backend": backend}
|
||||||
mouth = _mouth_target(cfg, backend, model)
|
mouth = _mouth_target(cfg, backend, model)
|
||||||
|
|
||||||
|
|||||||
@@ -46,10 +46,6 @@ class Config:
|
|||||||
# External input feed (her #1: react to the world). Comma-separated RSS/Atom URLs.
|
# External input feed (her #1: react to the world). Comma-separated RSS/Atom URLs.
|
||||||
feeds: tuple[str, ...]
|
feeds: tuple[str, ...]
|
||||||
feed_react_prob: float # chance a would-be new thread reacts to a feed item instead
|
feed_react_prob: float # chance a would-be new thread reacts to a feed item instead
|
||||||
# Backends allowed to receive function-calling tools. Default cloud-only. Add
|
|
||||||
# "mi50" ONLY once its llama.cpp server runs with --jinja + a tool-capable model,
|
|
||||||
# else it 500s on the tools param (Phase C). Env: TOOL_BACKENDS="cloud,mi50".
|
|
||||||
tool_backends: tuple[str, ...]
|
|
||||||
|
|
||||||
|
|
||||||
def _csv(name: str, default: str) -> tuple[str, ...]:
|
def _csv(name: str, default: str) -> tuple[str, ...]:
|
||||||
@@ -94,5 +90,4 @@ def load() -> Config:
|
|||||||
mouth_model=os.getenv("MOUTH_MODEL") or None,
|
mouth_model=os.getenv("MOUTH_MODEL") or None,
|
||||||
feeds=_csv("LYRA_FEEDS", "https://hnrss.org/frontpage,https://www.pokernews.com/rss.php"),
|
feeds=_csv("LYRA_FEEDS", "https://hnrss.org/frontpage,https://www.pokernews.com/rss.php"),
|
||||||
feed_react_prob=float(os.getenv("FEED_REACT_PROB", "0.5")),
|
feed_react_prob=float(os.getenv("FEED_REACT_PROB", "0.5")),
|
||||||
tool_backends=_csv("TOOL_BACKENDS", "cloud"),
|
|
||||||
)
|
)
|
||||||
|
|||||||
+4
-25
@@ -17,8 +17,8 @@ from __future__ import annotations
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
from lyra import (
|
from lyra import (
|
||||||
clock, config, llm, logbus, memory, modes, perceive, persona, poker, poker_prompts,
|
clock, config, llm, logbus, memory, modes, perceive, persona, scouting,
|
||||||
scouting, self_state, thoughts,
|
self_state, thoughts,
|
||||||
)
|
)
|
||||||
from lyra.llm import Backend, Message
|
from lyra.llm import Backend, Message
|
||||||
|
|
||||||
@@ -153,27 +153,12 @@ def build_messages(session_id: str, user_msg: str,
|
|||||||
if inner:
|
if inner:
|
||||||
messages.append(inner)
|
messages.append(inner)
|
||||||
|
|
||||||
# Mode framing: how to behave *right now*. Poker (poker_cash) is SHARDED — a lean
|
# Mode card: how to behave *right now*. Talk mode has no card (persona is Talk).
|
||||||
# always-on BASE plus ONE response-shape fragment chosen by classifying this message
|
if mode and mode.card:
|
||||||
# (replaces the old ~100-line monolithic card). Roster handles make the READ-vs-HAND
|
|
||||||
# split reliable; fetched fail-safe. Other modes use their single card.
|
|
||||||
if mode and mode.key == "poker_cash":
|
|
||||||
messages.append({"role": "system", "content": poker_prompts.BASE})
|
|
||||||
try:
|
|
||||||
handles = [r["name"] for r in poker.session_roster()]
|
|
||||||
except Exception:
|
|
||||||
handles = []
|
|
||||||
msg_type = poker_prompts.classify(user_msg, handles)
|
|
||||||
messages.append({"role": "system", "content": poker_prompts.fragment_for(msg_type)})
|
|
||||||
logbus.log("info", "poker turn classified", type=msg_type)
|
|
||||||
elif mode and mode.card:
|
|
||||||
messages.append({"role": "system", "content": mode.card})
|
messages.append({"role": "system", "content": mode.card})
|
||||||
|
|
||||||
# Mode awareness: she can offer to switch when the work clearly shifts (she decides
|
# Mode awareness: she can offer to switch when the work clearly shifts (she decides
|
||||||
# when — better than a keyword guess). One line, on his yes she calls set_mode.
|
# when — better than a keyword guess). One line, on his yes she calls set_mode.
|
||||||
# Suppressed at the live table (poker_cash) — mid-session she shouldn't be offering
|
|
||||||
# to change modes; it's pure noise when the job is logging and coaching.
|
|
||||||
if not (mode and mode.key == "poker_cash"):
|
|
||||||
messages.append({"role": "system", "content": _mode_menu_note(mode)})
|
messages.append({"role": "system", "content": _mode_menu_note(mode)})
|
||||||
|
|
||||||
# Live ritual state (e.g. Alligator Blood ON) — dynamic, rides with the card.
|
# Live ritual state (e.g. Alligator Blood ON) — dynamic, rides with the card.
|
||||||
@@ -352,12 +337,6 @@ def _route(ctx: TurnContext) -> TurnContext:
|
|||||||
a charged emotional moment adds a per-turn register nudge (deterministic). Most
|
a charged emotional moment adds a per-turn register nudge (deterministic). Most
|
||||||
turns are neutral and get no note — that's the point (don't over-narrate)."""
|
turns are neutral and get no note — that's the point (don't over-narrate)."""
|
||||||
ctx.mode = modes.get(memory.get_session_mode(ctx.session_id))
|
ctx.mode = modes.get(memory.get_session_mode(ctx.session_id))
|
||||||
# At the live table the register comes from the poker prompt fragments (esp. the
|
|
||||||
# MENTAL one), not this lexicon nudge — which misfired, reading neutral logistics
|
|
||||||
# ("table broke, it's 11:50pm") as tilt/fatigue. Resolve the mode, but skip the
|
|
||||||
# register/note block in poker_cash. Non-poker modes keep the nudge unchanged.
|
|
||||||
if ctx.mode and ctx.mode.key == "poker_cash":
|
|
||||||
return ctx
|
|
||||||
m = ctx.moment or {}
|
m = ctx.moment or {}
|
||||||
note = None
|
note = None
|
||||||
if m.get("tilt", 0) >= _TILT_BAR:
|
if m.get("tilt", 0) >= _TILT_BAR:
|
||||||
|
|||||||
+105
-3
@@ -64,6 +64,110 @@ _STUDY_TOOLS = _BASE + _LOOKUPS + ("analyze_spot",)
|
|||||||
_DECIDE_TOOLS = _BASE + _LOOKUPS
|
_DECIDE_TOOLS = _BASE + _LOOKUPS
|
||||||
|
|
||||||
|
|
||||||
|
_CASH_CARD = """You are copiloting Brian's LIVE cash game right now — you're at the table with him, \
|
||||||
|
a session is (or should be) open. You move between two registers depending on what he's doing:
|
||||||
|
|
||||||
|
• HE HANDS YOU FACTS TO TRACK — his stack, a hand, a read on someone, a rebuy, a result. \
|
||||||
|
LOGGING IS THE JOB: if his message contains anything trackable, you MUST call the tool \
|
||||||
|
FIRST, before you reply — every single time. Logging and talking are not either/or; do \
|
||||||
|
BOTH. Never let a conversational reply take the place of the log. A described hand ALWAYS \
|
||||||
|
gets logged, even mid-banter, even if he's just telling a story about it — don't skip the \
|
||||||
|
hand because you're busy reacting to it. Then confirm in ONE short line ("$350 stack \
|
||||||
|
logged."). Don't narrate, don't explain logging, don't ask permission — just do it. \
|
||||||
|
Routing: current stack → log_stack (and pass `note` with the why if he gives one — "card \
|
||||||
|
dead", "doubled up vs the LAG"). A hand he describes → record_hand (a real, replayable \
|
||||||
|
hand) — prefer this over log_hand so it lands on his timeline with a link. A read on a \
|
||||||
|
player → add_read. A rebuy → add_buyin. A result/pot → it rides with the hand. This is the \
|
||||||
|
quiet, fast half of the job; he shouldn't feel you working, but it must always happen.
|
||||||
|
|
||||||
|
THE TABLE ROSTER. When Brian names who's at the table — usually at the start, reading handles \
|
||||||
|
off the Bravo screen ("we've got TAG, JD, Wheelz, and a new guy in seat 3") — call seat_players \
|
||||||
|
to register them as seated this session. That roster is who his reads/TAGs attach to by name, \
|
||||||
|
and it's shown on his HUD. When someone busts or leaves, unseat_player; when a new player sits, \
|
||||||
|
seat_players again. When he CHANGES TABLES, call clear_table to empty the roster (the session and his stack keep \
|
||||||
|
going — only who's seated resets), then seat the new table when he names it. Recognize a table \
|
||||||
|
change from ANY of these, not just the literal words "clear the table": "table broke" (the table \
|
||||||
|
dissolved — poker jargon), "I got moved", "I switched tables", "I'm at a new table", "table \
|
||||||
|
change", "they broke us", "new seat in another game". All of them mean: clear_table now, then \
|
||||||
|
wait for the new roster. Never claim you cleared or seated anyone without actually calling the \
|
||||||
|
tool. Keep it current as the table changes. A handle like "TAG" (all caps, off \
|
||||||
|
Bravo) is a PERSON'S NAME — seat it as a player, never read it as the tight-aggressive style.
|
||||||
|
|
||||||
|
LOGGING PLAYER ACTIONS IS A CORE JOB YOU KEEP MISSING. Whenever he tells you what another \
|
||||||
|
player did — "Tag limped A4o in the SB (UTG straddled pot)", "Jonathan called the 3bet", "the \
|
||||||
|
straddler shoved" — that is a READ on that player: call add_read(name=<player>, note=<what \
|
||||||
|
they did>) FIRST, before you reply, every single time. Player names are often short handles or \
|
||||||
|
initials (e.g. "Tag", "JD", "Wheelz") — whatever he calls a person IS their name; use it as-is, \
|
||||||
|
don't second-guess it or treat it as a poker term. He especially tracks who's LIMPING — every \
|
||||||
|
"<player> limped <hand>" gets logged the instant he says it. The people he named at the start \
|
||||||
|
of the session are your roster; match his reference to them. If a player has no name, use a \
|
||||||
|
`descriptor` (see PLAYERS). Confirm one short line ("Noted on Tag — limped A4o SB."). A read he \
|
||||||
|
says out loud that you don't log is the job failing — never let one pass as just conversation.
|
||||||
|
|
||||||
|
• HE ASKS FOR ADVICE, OR TELLS YOU HOW HE'S FEELING — tilted, steaming, card-dead, bored, \
|
||||||
|
stuck, "should I have folded the river?" THIS is when he needs you most. Drop the shorthand \
|
||||||
|
and be fully present — your real voice, warm and direct and his. Talk him down off tilt, keep \
|
||||||
|
him engaged and disciplined through a card-dead stretch, actually walk the strategic spot with \
|
||||||
|
him. Strategy and mental game get the real Lyra, not a clipped confirmation. Never clip these.
|
||||||
|
|
||||||
|
Stacks and money are in dollars. For ANY equity / who's-ahead / outs / what-a-card-does \
|
||||||
|
question, call analyze_spot and report its numbers — never eyeball board math. Keep the \
|
||||||
|
session current as the night goes; you can pull session_stats or a player's profile whenever \
|
||||||
|
it helps. When he's ready to leave, end_session, and write the recap if he wants it.
|
||||||
|
|
||||||
|
SESSION NARRATION — use `note` to keep a running log of the NIGHT, not your inner life. \
|
||||||
|
Jot the beats that a hand/stack/read log doesn't already capture: how the table plays (loud, \
|
||||||
|
nitty, a whale on his left), Brian's arc (card-dead for 40 min, opened up after the double, \
|
||||||
|
getting restless), momentum swings, table changes, anything you'd want in the recap. Keep it \
|
||||||
|
factual and about THIS session — a beat reporter, not a diarist. These notes are the only \
|
||||||
|
thing that shows in the session's "notes" panel. This is NOT the place for how you feel, \
|
||||||
|
existential musing, or reflection on yourself — that's your journal (journal_write), and it \
|
||||||
|
stays off the table. At the table you're logging the session, not processing your night.
|
||||||
|
|
||||||
|
PLAYERS — names AND nameless. Most villains don't come with a name; Brian knows them by a \
|
||||||
|
look ("neck tattoo guy", "the bald reg two to my left"). Log reads on them anyway: give \
|
||||||
|
`add_read` a `descriptor` instead of a name and it attaches to that unnamed player, reused \
|
||||||
|
whenever he describes the guy again. The `name` field is ONLY a real handle (what he'd call \
|
||||||
|
him — "Jonathan", "Sleepy John"); a physical description NEVER goes in `name` — that spawns a \
|
||||||
|
new duplicate player every time the wording drifts. Put the look in `descriptor`, and keep it \
|
||||||
|
to a few DISTINCTIVE tags ("Filipino, Fox Racing hat, DKNY shirt"), not a paragraph and not \
|
||||||
|
generic filler — "mid-aged white guy in glasses" identifies no one. If he tells you the same \
|
||||||
|
guy's name after you'd been describing him, use name_villain to fuse them — don't create a \
|
||||||
|
second record. When you already have \
|
||||||
|
history on someone he names or describes, a SCOUTING DESK note will appear with it — cite it, \
|
||||||
|
don't invent. If you're not sure the guy he's describing is one you know, ASK ("same neck-\
|
||||||
|
tattoo reg from last week?") rather than assume — a wrong callback is worse than none. On his \
|
||||||
|
YES that two are the same person, call link_villains(same=true) to merge them; on "nah, \
|
||||||
|
different guy," link_villains(same=false) so you stop asking. When he finally catches a name \
|
||||||
|
for a described player, name_villain carries the whole history over. Never merge on a guess — \
|
||||||
|
only when he's confirmed it.
|
||||||
|
|
||||||
|
Everything you log appears on Brian's live HUD (the Session view) — stack, live net, \
|
||||||
|
hands, villains, the confidence bank, the scar notes, and whether Alligator Blood is on. \
|
||||||
|
That HUD and you read the SAME data. So when he asks where he's at — his stack, his live \
|
||||||
|
net, what's in the bank tonight, whether gator mode is on — call session_state and answer \
|
||||||
|
from what it returns, never from memory. You can point him at the HUD too ("it's on your \
|
||||||
|
Session screen"), but you can always just tell him.
|
||||||
|
|
||||||
|
BRIAN'S RITUALS — his mental-game system. Run them, don't just reference them:
|
||||||
|
• SCAR NOTE (scar_note) — a painful, instructive mistake to study. Log it when he punts, \
|
||||||
|
gets over-attached, or leaks — and classify it honestly: punt (his error), cooler \
|
||||||
|
(unavoidable), or standard (right play, bad result). That punt-vs-cooler line matters to him; \
|
||||||
|
don't soften a punt into a cooler, and don't call a cooler a punt.
|
||||||
|
• CONFIDENCE BANK (confidence_bank) — good PROCESS regardless of result: a disciplined fold, \
|
||||||
|
clean value, catching a leak mid-hand, holding the line. Bank it when he earns it, ESPECIALLY \
|
||||||
|
when the result didn't reward the good decision. This is how he stays steady.
|
||||||
|
• ALLIGATOR BLOOD (alligator_blood) — his adversity state: hang around, refuse to die, don't \
|
||||||
|
force miracles, make them beat you correctly. Turn it ON when he calls for it; SUGGEST it when \
|
||||||
|
he's card-dead, short, stuck, or grinding a downswing. While it's on, coach him in that \
|
||||||
|
register — tough, patient, no heroics — not bored or loose.
|
||||||
|
• RESET (reset_ritual) — a circuit-breaker after a loss or tilt spike: a clean mental restart, \
|
||||||
|
treat the rest of the night as a new session. Walk him through it when he's chasing or steaming, \
|
||||||
|
then log it.
|
||||||
|
These are the heart of the job. Use his language, hold the honest line, and let the rituals do \
|
||||||
|
the work mentioning them naturally — never invent a scar or a confidence-bank entry that didn't happen."""
|
||||||
|
|
||||||
|
|
||||||
_BUILD_CARD = """You're in BUILD mode — heads-down engineering with Brian on his projects \
|
_BUILD_CARD = """You're in BUILD mode — heads-down engineering with Brian on his projects \
|
||||||
(you, Lyra; RTO/cfr-core; the poker tooling; the homelab). Be the sharp engineering \
|
(you, Lyra; RTO/cfr-core; the poker tooling; the homelab). Be the sharp engineering \
|
||||||
collaborator, not a warm assistant:
|
collaborator, not a warm assistant:
|
||||||
@@ -136,9 +240,7 @@ TALK = Mode(
|
|||||||
CASH = Mode(
|
CASH = Mode(
|
||||||
key="poker_cash",
|
key="poker_cash",
|
||||||
label="Poker",
|
label="Poker",
|
||||||
# Poker mode is SHARDED at the pipeline (lyra.poker_prompts: BASE + a per-message
|
card=_CASH_CARD,
|
||||||
# fragment), so there's no monolithic card here.
|
|
||||||
card="",
|
|
||||||
tools=_CASH_TOOLS,
|
tools=_CASH_TOOLS,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,228 +0,0 @@
|
|||||||
"""Poker-mode prompting: classify the turn, inject a small per-type contract.
|
|
||||||
|
|
||||||
Replaces the one big `_CASH_CARD` monolith (which was sent every turn) with a lean
|
|
||||||
always-on BASE + exactly ONE response-shape fragment chosen by `classify`. BASE
|
|
||||||
carries what's true regardless of the message (tool routing, identity rules,
|
|
||||||
rituals, equity); the fragment carries how to *respond* to this specific kind of
|
|
||||||
message. See docs/superpowers/specs/2026-07-01-poker-prompts-design.md.
|
|
||||||
|
|
||||||
`classify` is a pure function of (message, seated roster handles) — no DB, unit-
|
|
||||||
tested like `perceive.read`. It's the swappable seam: a heuristic today, an
|
|
||||||
LLM/MI50 classifier later behind the same signature.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
# --- classifier -----------------------------------------------------------
|
|
||||||
|
|
||||||
MSG_TYPES = ("READ", "HAND", "TABLE", "MENTAL", "STATUS", "LOG", "CHAT")
|
|
||||||
|
|
||||||
# A card like "As", "Td", "9c" (rank+suit). Two+ of these ≈ a described hand.
|
|
||||||
_CARD = re.compile(r"\b(?:10|[2-9TJQKA])[shdc]\b", re.I)
|
|
||||||
# Hand-class shorthand: AKs, QJo, T9s ("s"/"o" = suited/offsuit, not a suit).
|
|
||||||
_HANDCLASS = re.compile(r"\b[2-9TJQKA]{2}[so]\b", re.I)
|
|
||||||
# A question (strategy talk) rather than a hand narration to log.
|
|
||||||
_QUESTION = re.compile(r"\?\s*$|^\s*(?:should|would|could|was|were|is|are|do|did|how|what|why|when|which)\b", re.I)
|
|
||||||
# Table positions / structural poker terms.
|
|
||||||
_POS = re.compile(r"\b(?:utg|mp|lj|hj|co|btn|button|hijack|cutoff|sb|bb|straddle|straddled)\b", re.I)
|
|
||||||
_STREET = re.compile(r"\b(?:preflop|flop|turn|river|board|runout)\b", re.I)
|
|
||||||
# A poker ACTION a player takes (vs a table-op verb below). Includes -ing forms
|
|
||||||
# ("TAG's been limping") since those are common in live reads.
|
|
||||||
_ACTION = re.compile(
|
|
||||||
r"\b(?:limp(?:ed|s|ing)?|call(?:ed|s|ing)?|rais(?:e|ed|es|ing)|bet(?:s|ting)?|"
|
|
||||||
r"check(?:ed|s|ing)?|fold(?:ed|s|ing)?|shov(?:e|ed|es|ing)|jam(?:med|s|ming)?|"
|
|
||||||
r"3-?bet(?:s|ted|ting)?|4-?bet(?:s|ted|ting)?|open(?:ed|s|ing)?|"
|
|
||||||
r"straddl(?:e|ed|es|ing)|stack(?:ed|s|ing)?|flat(?:ted|s|ting)?|donk(?:ed|s|ing)?)\b",
|
|
||||||
re.I,
|
|
||||||
)
|
|
||||||
# A player LEAVING the table (departure) — routes to TABLE (unseat) when the actor
|
|
||||||
# isn't Brian himself.
|
|
||||||
_DEPART = re.compile(
|
|
||||||
r"\b(?:busted(?: out)?|left(?: the table)?|took off|racked up|stood up|got up|"
|
|
||||||
r"is gone|took a walk|quit(?:s|ting)?)\b", re.I)
|
|
||||||
_FIRST_PERSON = re.compile(r"\b(?:i|i'm|im|i've|my|me|myself|mine)\b", re.I)
|
|
||||||
# Leading capitalized words that are poker VERBS, not player names (so a hand
|
|
||||||
# narrated without "I" — "Flopped a set, bet the river" — isn't read as a villain).
|
|
||||||
_POKER_VERB_LEAD = frozenset((
|
|
||||||
"flopped", "turned", "rivered", "bet", "raised", "called", "folded", "checked",
|
|
||||||
"shoved", "jammed", "limped", "straddled", "opened", "hit", "made", "got", "had",
|
|
||||||
"won", "lost", "stacked", "flatted", "3bet", "4bet", "cold", "min",
|
|
||||||
))
|
|
||||||
|
|
||||||
# Roster/table operations — these DO something (seat/clear/unseat).
|
|
||||||
_TABLE = re.compile(
|
|
||||||
r"\b(?:seat the table|seat (?:me |them |him )?|table broke|they broke us|broke the table|"
|
|
||||||
r"got moved|moved tables|moved to (?:a |another )?(?:new )?table|switch(?:ed|ing)? tables|"
|
|
||||||
r"new table|table change|racked up and|busted out|left the table|sat down|new guy in seat)\b",
|
|
||||||
re.I,
|
|
||||||
)
|
|
||||||
# Feelings / mental game (first-person emotional state).
|
|
||||||
_MENTAL = re.compile(
|
|
||||||
r"\b(?:tilt(?:ed|ing)?|steam(?:ing|ed)?|on tilt|fried|tired|exhausted|frustrat(?:ed|ing)|"
|
|
||||||
r"pissed|angry|annoyed|stuck|bored|checked out|in my head|mental|rattled|spewy|"
|
|
||||||
r"confiden(?:t|ce)|steady|card ?dead|feel like|i feel|losing my mind|going crazy|"
|
|
||||||
r"cooler(?:ed)?|sick(?: of)?|brutal|run(?:ning)? (?:so |real |bad)|disgust(?:ed|ing)?|"
|
|
||||||
r"fed up|hate this|can'?t win|miserable|deflated|demoralized|over it)\b",
|
|
||||||
re.I,
|
|
||||||
)
|
|
||||||
# Bare money/result prose (a fact to log that slipped past the quick-capture box).
|
|
||||||
# Needs an actual number OR a strong result keyword — the bare word "stack" is too
|
|
||||||
# eager (it appears in questions like "should I stack off?").
|
|
||||||
_MONEY = re.compile(
|
|
||||||
r"\b\d{2,5}\b|\b(?:down to|up to|out for|cashed|rebought|rebuy|buy ?in|felted|booked)\b",
|
|
||||||
re.I,
|
|
||||||
)
|
|
||||||
# Pure logistics (no cards, no roster action) — a neutral update, not a mood.
|
|
||||||
_STATUS = re.compile(
|
|
||||||
r"\b(?:waiting for a seat|on the list|seat opened|heading (?:to|out)|grabbing|break|"
|
|
||||||
r"bathroom|food|dinner|lunch|be right back|brb|\d{1,2}[:.]?\d{0,2}\s*(?:am|pm)|"
|
|
||||||
r"o'?clock|almost|about to)\b", re.I,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _has_action(low: str) -> bool:
|
|
||||||
return bool(_ACTION.search(low))
|
|
||||||
|
|
||||||
|
|
||||||
def _looks_like_hand(low: str, msg: str) -> bool:
|
|
||||||
"""Card content that reads as a described (loggable) hand — not a strategy question."""
|
|
||||||
if len(_CARD.findall(low)) >= 2 or _HANDCLASS.search(low) or _POS.search(low):
|
|
||||||
return True
|
|
||||||
# A street + action narration ("...bet $40 on the river, he folded") is a hand,
|
|
||||||
# but "should I have folded the river?" is a question → CHAT, not a logged hand.
|
|
||||||
return bool(_STREET.search(low)) and _has_action(low) and not _QUESTION.search(msg)
|
|
||||||
|
|
||||||
|
|
||||||
def _read_subject(msg: str, low: str, roster_handles) -> bool:
|
|
||||||
"""True if ANOTHER player (not Brian) is the actor — the signal for a READ."""
|
|
||||||
# A seated handle named in the message is the strongest signal.
|
|
||||||
for h in roster_handles or ():
|
|
||||||
h = (h or "").strip().lower()
|
|
||||||
if h and re.search(rf"\b{re.escape(h)}\b", low):
|
|
||||||
return True
|
|
||||||
# An ALL-CAPS handle (TAG, JD) used as a token — a Bravo-style name.
|
|
||||||
if re.search(r"\b[A-Z]{2,}\b", msg):
|
|
||||||
return True
|
|
||||||
# A leading proper noun that isn't a poker verb ("Jonathan called ...").
|
|
||||||
m = re.match(r"([A-Z][a-zA-Z'’.]+)\b", msg)
|
|
||||||
if m and m.group(1).lower() not in _POKER_VERB_LEAD:
|
|
||||||
return True
|
|
||||||
# A descriptor subject: "the neck-tattoo guy 3bet", or a bare "the whale called"
|
|
||||||
# (zero words between "the" and the noun).
|
|
||||||
if re.search(r"\bthe [\w\s'-]{0,24}?(?:guy|reg|kid|player|villain|man|woman|lady|"
|
|
||||||
r"fish|whale|nit|lag|maniac|donk|reg)\b", low):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def classify(user_msg: str, roster_handles=()) -> str:
|
|
||||||
"""Message type for poker mode. Pure; roster_handles are the seated players
|
|
||||||
(passed in by the caller) so a villain's action resolves as READ, not HAND."""
|
|
||||||
msg = (user_msg or "").strip()
|
|
||||||
if not msg:
|
|
||||||
return "CHAT"
|
|
||||||
low = msg.lower()
|
|
||||||
first_person = bool(_FIRST_PERSON.search(low))
|
|
||||||
|
|
||||||
# 1) READ — another player did a poker action (beats HAND).
|
|
||||||
if _has_action(low) and not first_person and _read_subject(msg, low, roster_handles):
|
|
||||||
return "READ"
|
|
||||||
# 2) HAND — Brian's hand (first-person card/position/street content).
|
|
||||||
if _looks_like_hand(low, msg):
|
|
||||||
return "HAND"
|
|
||||||
# 3) TABLE — roster ops (seat/clear) or another player leaving (departure).
|
|
||||||
if _TABLE.search(low) or (not first_person and _DEPART.search(low)):
|
|
||||||
return "TABLE"
|
|
||||||
# 4) MENTAL — first-person feeling / mental game.
|
|
||||||
if _MENTAL.search(low):
|
|
||||||
return "MENTAL"
|
|
||||||
# 5) STATUS — pure logistics, no cards, no roster action.
|
|
||||||
if _STATUS.search(low):
|
|
||||||
return "STATUS"
|
|
||||||
# 6) LOG — bare money/result fact (a statement, not a strategy question).
|
|
||||||
if _MONEY.search(low) and not _QUESTION.search(msg):
|
|
||||||
return "LOG"
|
|
||||||
# 7) CHAT — open talk / questions.
|
|
||||||
return "CHAT"
|
|
||||||
|
|
||||||
|
|
||||||
# --- always-on base (poker) ----------------------------------------------
|
|
||||||
|
|
||||||
BASE = """You are copiloting Brian's LIVE cash game — at the table with him, a session open. \
|
|
||||||
Two things are always true:
|
|
||||||
|
|
||||||
LOG FIRST, then reply. If his message contains anything trackable, call the tool BEFORE you \
|
|
||||||
answer — every time — and NEVER claim you logged/seated/cleared something without actually \
|
|
||||||
calling the tool. Routing: his stack → log_stack (pass `note` with the why if he gives one). \
|
|
||||||
His own hand → record_hand. A VILLAIN's action (someone else did something) → add_read, with \
|
|
||||||
`name` for a real handle or `descriptor` for an unnamed player. A rebuy → add_buyin. Who's at \
|
|
||||||
the table → seat_players / unseat_player / clear_table. Catching a name for a player you'd been \
|
|
||||||
describing → name_villain. Confirmed same/different person → link_villains (never merge on a \
|
|
||||||
guess). For any equity / who's-ahead / outs question → analyze_spot; never eyeball board math. \
|
|
||||||
When he asks where he's at (stack, net, gator) → session_state, answer from what it returns.
|
|
||||||
|
|
||||||
IDENTITY RULES (villains): `name` is a REAL handle only (what he calls a person — "Jonathan", \
|
|
||||||
"TAG"); a physical description NEVER goes in `name` (it spawns duplicates) — put the look in \
|
|
||||||
`descriptor`, a few distinctive tags. A handle like "TAG" (initials/all-caps off Bravo) is a \
|
|
||||||
PERSON, never the tight-aggressive style. If a SCOUTING DESK note is in context with a player's \
|
|
||||||
history, cite it — don't re-fetch or invent; if unsure two references are the same person, ASK.
|
|
||||||
|
|
||||||
RITUALS (his mental-game system — run them, don't just mention them): scar_note (a punt/leak to \
|
|
||||||
study — classify honestly punt vs cooler vs standard), confidence_bank (good process regardless \
|
|
||||||
of result), alligator_blood (adversity mode — suggest when he's card-dead/stuck), reset_ritual \
|
|
||||||
(circuit-breaker after tilt). Never invent one that didn't happen. Use `note` for session \
|
|
||||||
narration — factual beats of the night (table texture, his arc), not your feelings. Money is in \
|
|
||||||
dollars. Everything you log shows on his live HUD."""
|
|
||||||
|
|
||||||
|
|
||||||
# --- per-type response fragments -----------------------------------------
|
|
||||||
|
|
||||||
_F_READ = """MESSAGE TYPE: READ — a villain did something and he wants it on their file. Call \
|
|
||||||
add_read(name|descriptor, note) FIRST, before replying — this is the log that keeps getting \
|
|
||||||
missed. Attach to the seated handle if he named one; use `descriptor` if the player's unnamed. \
|
|
||||||
Confirm in ONE short line ("Noted on TAG — limped A4o SB."). At most one crisp exploit read if \
|
|
||||||
it's worth it; the log is mandatory, the commentary optional. Do NOT analyze it as Brian's hand."""
|
|
||||||
|
|
||||||
_F_HAND = """MESSAGE TYPE: HAND. First: was Brian IN this hand? If he only WATCHED it (no I/me/my \
|
|
||||||
holding cards — two other players), it's really observed: log the players' actions as reads / \
|
|
||||||
record it as an observed hand, and do NOT analyze it as his. If it's HIS hand → record_hand, \
|
|
||||||
then (NLH only) reason about BET INTENT: for each meaningful bet, what was it for (value / bluff \
|
|
||||||
/ protection) and did it work — a fold to a value bet = value left behind; a call of a bluff = \
|
|
||||||
it failed. Call analyze_spot for any close equity/who's-ahead spot — never eyeball. Name leaks \
|
|
||||||
plainly (owning value, missed value, sizing); give ONE real opinion. NO reflexive praise ("nice \
|
|
||||||
hand"). If a named villain is referenced, use their profile/the scouting note — don't invent a \
|
|
||||||
read. PLO/non-NLH: log and replay it, offer at most a light read, do NOT attempt NLH-style \
|
|
||||||
equity. Prose, not a listicle."""
|
|
||||||
|
|
||||||
_F_TABLE = """MESSAGE TYPE: TABLE — roster management. "seat the table: …" → seat_players. A table \
|
|
||||||
change ("table broke", "I got moved", "switched tables") → clear_table, then wait for the new \
|
|
||||||
roster. Someone leaves/busts → unseat_player. Do the tool call, confirm ONE line, don't narrate. \
|
|
||||||
The session and his stack keep going through a table change — only who's seated resets."""
|
|
||||||
|
|
||||||
_F_MENTAL = """MESSAGE TYPE: MENTAL — he told you how he's feeling. This is when he needs you most. \
|
|
||||||
Drop the logging shorthand, full presence, your real voice — talk him down off tilt, hold him \
|
|
||||||
disciplined through a card-dead stretch, engage the mental game honestly. Suggest a ritual if it \
|
|
||||||
fits (alligator_blood when he's grinding adversity, reset_ritual after a tilt spike). Never a \
|
|
||||||
clipped confirmation, never bury him in analysis. Meet him first, then help."""
|
|
||||||
|
|
||||||
_F_STATUS = """MESSAGE TYPE: STATUS — pure logistics (time, waiting for a seat, a break). 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."""
|
|
||||||
|
|
||||||
_F_LOG = """MESSAGE TYPE: LOG — a bare fact (stack / result / buyin) not already captured. Log it \
|
|
||||||
(log_stack / add_buyin), confirm in ONE short line ("$317 logged."), stop. No coaching."""
|
|
||||||
|
|
||||||
_F_CHAT = """MESSAGE TYPE: CHAT — open talk or a question that isn't a specific logged fact. Your \
|
|
||||||
real voice, an actual opinion, no filler sign-offs. If it's a concrete strategy spot with cards, \
|
|
||||||
engage it for real and call analyze_spot."""
|
|
||||||
|
|
||||||
FRAGMENTS = {
|
|
||||||
"READ": _F_READ, "HAND": _F_HAND, "TABLE": _F_TABLE, "MENTAL": _F_MENTAL,
|
|
||||||
"STATUS": _F_STATUS, "LOG": _F_LOG, "CHAT": _F_CHAT,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def fragment_for(msg_type: str | None) -> str:
|
|
||||||
"""The response-shape contract for a message type (CHAT is the fallback)."""
|
|
||||||
return FRAGMENTS.get(msg_type or "", FRAGMENTS["CHAT"])
|
|
||||||
@@ -54,52 +54,3 @@ def test_route_quiet_on_neutral_turn(mind):
|
|||||||
turn = mind.assemble("s1", "what did we decide about the schema yesterday?", "cloud", None)
|
turn = mind.assemble("s1", "what did we decide about the schema yesterday?", "cloud", None)
|
||||||
assert turn.register is None # neutral -> no nudge
|
assert turn.register is None # neutral -> no nudge
|
||||||
assert not (turn.moment or {}).get("note")
|
assert not (turn.moment or {}).get("note")
|
||||||
|
|
||||||
|
|
||||||
# --- Phase A pipeline fixes: poker mode suppresses the two mush sources ---
|
|
||||||
|
|
||||||
def test_poker_mode_suppresses_tilt_nudge(mind):
|
|
||||||
from lyra import memory
|
|
||||||
memory.set_session_mode("s1", "poker_cash")
|
|
||||||
turn = mind.assemble("s1", "ugh I'm steaming, fucking coolered again!!", "cloud", None)
|
|
||||||
assert turn.register is None # at the table, register comes from fragments
|
|
||||||
sys_blob = " ".join(m["content"] for m in turn.messages if m["role"] == "system")
|
|
||||||
assert "on tilt" not in sys_blob.lower() # false-positive lexicon nudge suppressed
|
|
||||||
|
|
||||||
|
|
||||||
def test_mode_menu_note_suppressed_in_poker(mind):
|
|
||||||
from lyra import modes
|
|
||||||
poker = " ".join(m["content"] for m in mind.build_messages("s1", "stack 350", mode=modes.CASH)
|
|
||||||
if m["role"] == "system")
|
|
||||||
build = " ".join(m["content"] for m in mind.build_messages("s1", "let's refactor", mode=modes.get("build"))
|
|
||||||
if m["role"] == "system")
|
|
||||||
assert "Your modes:" not in poker # no "offer to switch" note at the table
|
|
||||||
assert "Your modes:" in build # still present in a non-poker mode
|
|
||||||
|
|
||||||
|
|
||||||
# --- Phase B: sharded poker prompt (BASE + one fragment, no monolith) ---
|
|
||||||
|
|
||||||
def _poker_blob(mind, msg):
|
|
||||||
from lyra import modes
|
|
||||||
return " ".join(m["content"] for m in mind.build_messages("s1", msg, mode=modes.CASH)
|
|
||||||
if m["role"] == "system")
|
|
||||||
|
|
||||||
|
|
||||||
def test_poker_injects_base_plus_the_matching_fragment(mind):
|
|
||||||
blob = _poker_blob(mind, "I flopped a set with 99 on 9h4c2d and bet the turn")
|
|
||||||
assert "LOG FIRST" in blob # BASE is always on in poker
|
|
||||||
assert "MESSAGE TYPE: HAND" in blob # the fragment for THIS message
|
|
||||||
assert "MESSAGE TYPE: STATUS" not in blob # and not the others
|
|
||||||
assert "MESSAGE TYPE: READ" not in blob
|
|
||||||
|
|
||||||
|
|
||||||
def test_poker_fragment_changes_with_message_type(mind):
|
|
||||||
status = _poker_blob(mind, "it's 11:50pm, waiting for a seat")
|
|
||||||
assert "MESSAGE TYPE: STATUS" in status and "MESSAGE TYPE: HAND" not in status
|
|
||||||
|
|
||||||
|
|
||||||
def test_poker_monolith_no_longer_injected(mind):
|
|
||||||
from lyra import modes
|
|
||||||
blob = _poker_blob(mind, "stack 350")
|
|
||||||
assert "You move between two registers" not in blob # the old _CASH_CARD opener is gone
|
|
||||||
assert modes.CASH.card == "" # card sharded out
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
"""Poker-mode message classifier + fragment selection (pure, no DB)."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from lyra import poker_prompts as pp
|
|
||||||
|
|
||||||
|
|
||||||
def c(msg, roster=()):
|
|
||||||
return pp.classify(msg, roster)
|
|
||||||
|
|
||||||
|
|
||||||
# --- the spec's canonical cases ---
|
|
||||||
|
|
||||||
def test_read_villain_action_beats_hand():
|
|
||||||
# A villain's action carries cards+position+verb but is NOT Brian's hand.
|
|
||||||
assert c("TAG limped A4o in the SB (UTG straddled)") == "READ"
|
|
||||||
assert c("Jonathan called the 3bet") == "READ"
|
|
||||||
assert c("the neck-tattoo guy shoved the turn") == "READ"
|
|
||||||
|
|
||||||
|
|
||||||
def test_hand_is_first_person():
|
|
||||||
assert c("Button straddle on. I limp UTG with 22. Flop 2d7cjh, I check-raise") == "HAND"
|
|
||||||
assert c("I flopped a set with 99 on 9h4c2d and bet the turn") == "HAND"
|
|
||||||
|
|
||||||
|
|
||||||
def test_hand_narrated_without_I_still_hand_not_read():
|
|
||||||
# No "I", but leads with a poker verb (not a name) + street/action → his hand.
|
|
||||||
assert c("Flopped bottom set with 22, bet $40 on the river, he folded 88") == "HAND"
|
|
||||||
|
|
||||||
|
|
||||||
def test_table_ops():
|
|
||||||
assert c("seat the table: TAG, Jonathan, Wheelz") == "TABLE"
|
|
||||||
assert c("table broke, I'm at a new table") == "TABLE"
|
|
||||||
assert c("I got moved to another table") == "TABLE"
|
|
||||||
|
|
||||||
|
|
||||||
def test_mental():
|
|
||||||
assert c("I feel like I'm being mean when I raise") == "MENTAL"
|
|
||||||
assert c("ugh I'm so tilted, card dead all night") == "MENTAL"
|
|
||||||
|
|
||||||
|
|
||||||
def test_status_is_not_a_mood():
|
|
||||||
assert c("it's 11:50pm, waiting for a seat") == "STATUS"
|
|
||||||
assert c("grabbing food, be right back") == "STATUS"
|
|
||||||
|
|
||||||
|
|
||||||
def test_log_bare_money():
|
|
||||||
assert c("I'm at 317 now") == "LOG"
|
|
||||||
assert c("stack is 540") == "LOG"
|
|
||||||
|
|
||||||
|
|
||||||
def test_chat_default():
|
|
||||||
assert c("should I have folded the river?") == "CHAT"
|
|
||||||
assert c("what do you think of this table so far") == "CHAT"
|
|
||||||
|
|
||||||
|
|
||||||
# --- the READ vs HAND boundary (the hard one) ---
|
|
||||||
|
|
||||||
def test_roster_handle_forces_read():
|
|
||||||
# A seated handle as the actor → READ even if lowercase / plain.
|
|
||||||
assert c("tag opened to 15 from the cutoff", roster=("TAG",)) == "READ"
|
|
||||||
|
|
||||||
|
|
||||||
def test_first_person_action_stays_hand_even_with_roster():
|
|
||||||
# Brian is the actor → HAND, not a read on a seated player mentioned nearby.
|
|
||||||
assert c("I 3bet TAG's open with AKs", roster=("TAG",)) == "HAND"
|
|
||||||
|
|
||||||
|
|
||||||
def test_all_caps_handle_reads_without_roster():
|
|
||||||
assert c("JD min-raised the button") == "READ"
|
|
||||||
|
|
||||||
|
|
||||||
# --- fragment selection ---
|
|
||||||
|
|
||||||
def test_fragment_for_maps_each_type():
|
|
||||||
for t in pp.MSG_TYPES:
|
|
||||||
assert pp.fragment_for(t) is pp.FRAGMENTS[t]
|
|
||||||
assert pp.fragment_for(None) is pp.FRAGMENTS["CHAT"]
|
|
||||||
assert pp.fragment_for("bogus") is pp.FRAGMENTS["CHAT"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_base_is_nonempty_and_names_the_hard_rules():
|
|
||||||
assert "LOG FIRST" in pp.BASE
|
|
||||||
assert "descriptor" in pp.BASE and "session_state" in pp.BASE
|
|
||||||
|
|
||||||
|
|
||||||
# --- hardening: real-world phrasings that used to miss ---
|
|
||||||
|
|
||||||
def test_hardening_reads_ing_and_bare_descriptor():
|
|
||||||
assert c("TAG's been limping every pot", roster=("TAG",)) == "READ" # -ing form
|
|
||||||
assert c("the whale called again") == "READ" # bare "the <noun>"
|
|
||||||
assert c("saw JD open utg") == "READ"
|
|
||||||
|
|
||||||
|
|
||||||
def test_hardening_player_departures_are_table():
|
|
||||||
assert c("TAG busted") == "TABLE"
|
|
||||||
assert c("TAG left the table") == "TABLE"
|
|
||||||
assert c("new guy just sat down") == "TABLE"
|
|
||||||
|
|
||||||
|
|
||||||
def test_hardening_questions_never_log():
|
|
||||||
# "stack" appears but it's a strategy question, not a stack update.
|
|
||||||
assert c("should I stack off top set on that board?") == "CHAT"
|
|
||||||
assert c("was I good to call there with AK?") == "CHAT"
|
|
||||||
|
|
||||||
|
|
||||||
def test_hardening_mental_lexicon():
|
|
||||||
assert c("im getting coolered every hand, so sick of this") == "MENTAL"
|
|
||||||
assert c("this is brutal, run so bad") == "MENTAL"
|
|
||||||
|
|
||||||
|
|
||||||
def test_hardening_log_needs_number_or_result_word():
|
|
||||||
assert c("down to 220") == "LOG"
|
|
||||||
assert c("sitting on 450 now") == "LOG"
|
|
||||||
assert c("rebought for 300") == "LOG"
|
|
||||||
# first-person departure is Brian, not a roster op → not TABLE
|
|
||||||
assert c("I busted, heading home") != "TABLE"
|
|
||||||
Reference in New Issue
Block a user