49b88af3cc
The real upgrade over the ChatGPT prose-recap workflow: structured data capture via tools Lyra drives during a live session, with stats computed from real data. - lyra/poker.py: domain pack (separate from core memory) — poker_sessions, poker_hands, persistent poker_players (villain file) + player_reads; functions for session lifecycle (start/buyin/end with net+hours), tolerant hand logging, villain upsert/reads, and session/running stats ($/hr, by stake/venue/game) - tools.py: 8 poker tools wired into the chat tool loop (start_session, add_buyin, log_hand, add_read, end_session, session_stats, running_stats, get_villain_file) — partial/terse input tolerated - import/: Brian's real .md session-log format (reference for the phase-2 recap) - tests: lifecycle/net math, partial hand logging, villain upsert, running stats, tool dispatch Verified live: a full talk-through session persisted as structured rows (session +240, AKs hand, seat-5 read) — she drove the tools from natural chat. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
75 lines
2.9 KiB
Python
75 lines
2.9 KiB
Python
"""Poker domain: structured session/hand/villain storage + stats, and the tools."""
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture
|
|
def lyra(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("LYRA_DB_PATH", str(tmp_path / "test.db"))
|
|
from lyra import llm
|
|
monkeypatch.setattr(llm, "embed", lambda texts: [[0.1, 0.2, 0.3] for _ in texts])
|
|
import lyra.memory as memory
|
|
importlib.reload(memory)
|
|
import lyra.poker as poker
|
|
importlib.reload(poker) # rebind to the reloaded memory + reset its schema flag
|
|
return poker
|
|
|
|
|
|
def test_session_lifecycle_and_net(lyra):
|
|
poker = lyra
|
|
sid = poker.start_session(venue="Meadows", stakes="1/3", buy_in=400)
|
|
assert poker.live_session()["id"] == sid
|
|
poker.add_buyin(500) # rebuy -> total 900
|
|
s = poker.end_session(cash_out=627)
|
|
assert s["buy_in_total"] == 900
|
|
assert s["net"] == pytest.approx(-273)
|
|
assert s["status"] == "closed"
|
|
assert poker.live_session() is None # closed -> no live session
|
|
|
|
|
|
def test_log_hand_partial_fields(lyra):
|
|
poker = lyra
|
|
poker.start_session(stakes="1/3", buy_in=300)
|
|
hid = poker.log_hand(position="BTN", hole_cards="AKs", result=120, tag="confidence")
|
|
hands = poker.list_hands()
|
|
assert len(hands) == 1 and hands[0]["id"] == hid
|
|
assert hands[0]["hole_cards"] == "AKs" and hands[0]["result"] == 120
|
|
assert hands[0]["board"] is None # unspecified fields stay null
|
|
|
|
|
|
def test_villain_file_upsert_and_read(lyra):
|
|
poker = lyra
|
|
poker.start_session(venue="Meadows", stakes="1/3", buy_in=300)
|
|
poker.add_read("limp-called K4s UTG", name="Sleepy John", seat="3",
|
|
tendencies="loose-passive, jackpot dreamer", category="feeder", venue="Meadows")
|
|
# update the same player
|
|
poker.add_read("cold-called a 3-bet with A2o", name="sleepy john")
|
|
file = poker.get_villain_file(name="Sleepy")
|
|
assert len(file) == 1 # matched by name, not duplicated
|
|
assert file[0]["category"] == "feeder"
|
|
|
|
|
|
def test_running_stats(lyra):
|
|
poker = lyra
|
|
s1 = poker.start_session(stakes="1/3", buy_in=300)
|
|
poker.end_session(540, session_id=s1)
|
|
s2 = poker.start_session(stakes="1/3", buy_in=400)
|
|
poker.end_session(300, session_id=s2)
|
|
rs = poker.running_stats(stakes="1/3")
|
|
assert rs["sessions"] == 2
|
|
assert rs["net"] == pytest.approx(140) # +240 then -100
|
|
assert "1/3" in rs["by_stake"]
|
|
|
|
|
|
def test_poker_tools_dispatch(lyra):
|
|
from lyra import tools
|
|
assert "started" in tools.dispatch("start_session", {"stakes": "1/3", "buy_in": 300})
|
|
assert "logged" in tools.dispatch("log_hand", {"position": "CO", "hole_cards": "QQ"})
|
|
assert "closed" in tools.dispatch("end_session", {"cash_out": 500})
|
|
# the poker tools are offered to the model
|
|
names = {s["function"]["name"] for s in tools.specs()}
|
|
assert {"start_session", "log_hand", "end_session", "running_stats", "get_villain_file"} <= names
|