From e482ad591c9527da6d0c91e8cb16192f02f06ed4 Mon Sep 17 00:00:00 2001 From: serversdown Date: Sun, 5 Jul 2026 03:28:10 +0000 Subject: [PATCH] feat(prompting): harden the poker classifier against real-world phrasings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Probed the heuristic against live-style messages and fixed 6 real gaps: - action verbs missed -ing forms ("TAG's been limping") → no READ - the descriptor-subject regex required words between "the" and the noun, so "the whale called" missed → now handles bare "the " - player departures ("TAG busted", "TAG left") weren't TABLE → added _DEPART (gated to non-first-person so "I busted, heading home" isn't a roster op) - the bare word "stack" made strategy questions ("should I stack off?") classify as LOG → LOG now needs a number/result word AND excludes questions - thin MENTAL lexicon → added coolered/sick/brutal/run bad/etc. 21-case probe (13 former misses + 8 regression guards) all green; folded into the suite as 5 hardening tests. Full suite 201 green. classify() stays the swappable seam for an LLM/MI50 upgrade later. Co-Authored-By: Claude Opus 4.8 (1M context) --- lyra/poker_prompts.py | 39 ++++++++++++++++++++++++------------- tests/test_poker_prompts.py | 33 +++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/lyra/poker_prompts.py b/lyra/poker_prompts.py index 0807f43..236d454 100644 --- a/lyra/poker_prompts.py +++ b/lyra/poker_prompts.py @@ -27,13 +27,20 @@ _QUESTION = re.compile(r"\?\s*$|^\s*(?:should|would|could|was|were|is|are|do|did # 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). +# 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)?|call(?:ed|s)?|rais(?:e|ed|es)|bet(?:s)?|check(?:ed|s)?|" - r"fold(?:ed|s)?|shov(?:e|ed|es)|jam(?:med|s)?|3-?bet(?:s|ted)?|4-?bet(?:s|ted)?|" - r"open(?:ed|s)?|straddl(?:e|ed|es)|stack(?:ed|s)?|flat(?:ted|s)?|donk(?:ed|s)?)\b", + 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). @@ -54,13 +61,17 @@ _TABLE = re.compile( _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)\b", + 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"(?:\$?\d{2,5})\b|\b(?:stack|down to|up to|out for|cashed|rebought|rebuy|buy ?in|" - r"felted|stuck|booked)\b", re.I, + 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( @@ -97,8 +108,10 @@ def _read_subject(msg: str, low: str, roster_handles) -> bool: 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 - # "the neck-tattoo guy 3bet" — a descriptor subject. - if re.search(r"\bthe .+?\b(?:guy|reg|kid|player|villain|man|woman|lady|fish|whale|nit|lag|maniac)\b", low): + # 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 @@ -118,8 +131,8 @@ def classify(user_msg: str, roster_handles=()) -> str: # 2) HAND — Brian's hand (first-person card/position/street content). if _looks_like_hand(low, msg): return "HAND" - # 3) TABLE — roster ops with a tool action. - if _TABLE.search(low): + # 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): @@ -127,8 +140,8 @@ def classify(user_msg: str, roster_handles=()) -> str: # 5) STATUS — pure logistics, no cards, no roster action. if _STATUS.search(low): return "STATUS" - # 6) LOG — bare money/result fact. - if _MONEY.search(low): + # 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" diff --git a/tests/test_poker_prompts.py b/tests/test_poker_prompts.py index 10c483e..1cfd189 100644 --- a/tests/test_poker_prompts.py +++ b/tests/test_poker_prompts.py @@ -81,3 +81,36 @@ def test_fragment_for_maps_each_type(): 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 " + 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"