diff --git a/lyra/tools.py b/lyra/tools.py index e144589..819ab2a 100644 --- a/lyra/tools.py +++ b/lyra/tools.py @@ -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"}}, diff --git a/tests/test_hand_tools.py b/tests/test_hand_tools.py new file mode 100644 index 0000000..6f37b95 --- /dev/null +++ b/tests/test_hand_tools.py @@ -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()