fix: record_hand recovers when the model uses log_hand's field schema
Live-session bug: the chat model called record_hand but filled log_hand's granular fields (position/hole_cards/board/streets), leaving `shorthand` empty → the parser got nothing → "I couldn't parse that hand". The fragment/tool choice was correct; only the argument shape was wrong. - _record_hand now reconstructs a parseable description from the granular fields when `shorthand` is empty (_shorthand_from_fields), so it works regardless of which schema the model uses. Explicit `shorthand` still passes verbatim. - Sharpened record_hand's spec: pass the ENTIRE hand as ONE `shorthand` string, not split fields (that's log_hand); shorthand is required + non-empty. 4 tests. Full suite 210 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+26
-3
@@ -444,9 +444,29 @@ def _running_stats(args: dict, ctx: dict) -> str:
|
||||
return f"{rs['sessions']} sessions, {rs['hours']:g}h, net {rs['net']:+.0f}{hourly}. By stake: {by}"
|
||||
|
||||
|
||||
def _shorthand_from_fields(args: dict) -> str:
|
||||
"""Rebuild a hand description from log_hand-style granular fields. The chat model
|
||||
sometimes calls record_hand with those fields (position/hole_cards/board/streets)
|
||||
and leaves `shorthand` empty — so we reconstruct a parseable description from
|
||||
whatever it did pass, instead of failing on an empty shorthand."""
|
||||
parts = []
|
||||
pos, hole = args.get("position"), args.get("hole_cards")
|
||||
if pos or hole:
|
||||
parts.append(f"Hero {pos or '?'} with {hole or 'unknown'}")
|
||||
for st in ("preflop", "flop", "turn", "river", "showdown"):
|
||||
if args.get(st):
|
||||
parts.append(f"{st.capitalize()}: {args[st]}")
|
||||
if args.get("board"):
|
||||
parts.append(f"Board: {args['board']}")
|
||||
if args.get("result") is not None:
|
||||
parts.append(f"Hero net: {args['result']}")
|
||||
return ". ".join(str(p).strip() for p in parts if str(p).strip())
|
||||
|
||||
|
||||
def _record_hand(args: dict, ctx: dict) -> str:
|
||||
shorthand = (args.get("shorthand") or "").strip() or _shorthand_from_fields(args)
|
||||
out = poker.record_hand(
|
||||
args.get("shorthand") or "", stakes=args.get("stakes"),
|
||||
shorthand, stakes=args.get("stakes"),
|
||||
tag=args.get("tag"), lesson=args.get("lesson"),
|
||||
)
|
||||
if not out["id"]:
|
||||
@@ -759,8 +779,11 @@ TOOLS.update({
|
||||
"record_hand",
|
||||
"Reconstruct a hand from Brian's rough shorthand into a structured, "
|
||||
"replayable hand history. Use when he describes/vomits a hand he wants "
|
||||
"saved or to review. Pass his description verbatim as 'shorthand'.",
|
||||
{"shorthand": {**_S, "description": "Brian's rough description of the hand, verbatim"},
|
||||
"saved or to review. Pass his ENTIRE description as ONE string in `shorthand` "
|
||||
"— do NOT split it into position/board/street fields (that's log_hand). "
|
||||
"`shorthand` is required and must be non-empty.",
|
||||
{"shorthand": {**_S, "description": "Brian's whole hand description as one verbatim "
|
||||
"string, e.g. 'UTG with 9h6h, raise 15, BTN calls, flop 8h7h5s...'"},
|
||||
"stakes": {**_S, "description": "Stakes if known, e.g. '1/3'"},
|
||||
"tag": {**_S, "description": "well_played | leak | cooler | confidence | notable"},
|
||||
"lesson": {**_S, "description": "Takeaway, if he stated one"}},
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
"""record_hand tolerance: recover when the model calls it with log_hand's fields."""
|
||||
from __future__ import annotations
|
||||
|
||||
from lyra import tools
|
||||
|
||||
_GRANULAR = {
|
||||
"position": "UTG", "hole_cards": "9h6h", "board": "8h7h5s 5h Kc",
|
||||
"preflop": "raised to 15, BTN calls", "flop": "bet 25, BTN calls",
|
||||
"turn": "bet 50, BTN raises to 150, call", "river": "check, BTN all in, snap call",
|
||||
"showdown": "BTN shows 55 for quads, hero shows straight flush", "result": 300,
|
||||
"tag": "notable", "lesson": "rare straight flush over quads",
|
||||
}
|
||||
|
||||
|
||||
def test_shorthand_from_fields_builds_a_parseable_description():
|
||||
s = tools._shorthand_from_fields(_GRANULAR)
|
||||
assert "UTG with 9h6h" in s
|
||||
assert "Preflop:" in s and "River:" in s and "Board: 8h7h5s 5h Kc" in s
|
||||
assert "Hero net: 300" in s
|
||||
|
||||
|
||||
def test_record_hand_recovers_from_granular_fields(monkeypatch):
|
||||
# The model called record_hand with log_hand's schema (no `shorthand`). The
|
||||
# handler must reconstruct one and pass it to poker.record_hand, not fail empty.
|
||||
seen = {}
|
||||
|
||||
def fake_record_hand(shorthand, stakes=None, tag=None, lesson=None, backend=None):
|
||||
seen["shorthand"] = shorthand
|
||||
return {"id": 42, "parsed": {"hero_involved": True, "hero_pos": "UTG",
|
||||
"hero_cards": ["9h", "6h"]}, "linked": 0}
|
||||
|
||||
monkeypatch.setattr(tools.poker, "record_hand", fake_record_hand)
|
||||
out = tools.dispatch("record_hand", _GRANULAR, {})
|
||||
assert "UTG with 9h6h" in seen["shorthand"] # reconstructed, not empty
|
||||
assert "#42" in out and "couldn't parse" not in out
|
||||
|
||||
|
||||
def test_record_hand_still_prefers_explicit_shorthand(monkeypatch):
|
||||
seen = {}
|
||||
|
||||
def fake_record_hand(shorthand, stakes=None, tag=None, lesson=None, backend=None):
|
||||
seen["shorthand"] = shorthand
|
||||
return {"id": 7, "parsed": {"hero_involved": True, "hero_pos": "BTN",
|
||||
"hero_cards": ["As", "Ks"]}, "linked": 0}
|
||||
|
||||
monkeypatch.setattr(tools.poker, "record_hand", fake_record_hand)
|
||||
tools.dispatch("record_hand", {"shorthand": "BTN AKs, I open, everyone folds"}, {})
|
||||
assert seen["shorthand"] == "BTN AKs, I open, everyone folds" # verbatim, not rebuilt
|
||||
|
||||
|
||||
def test_record_hand_empty_call_still_fails_gracefully(monkeypatch):
|
||||
monkeypatch.setattr(tools.poker, "record_hand",
|
||||
lambda *a, **k: {"id": None, "parsed": None})
|
||||
out = tools.dispatch("record_hand", {}, {})
|
||||
assert "couldn't parse" in out.lower()
|
||||
Reference in New Issue
Block a user