Files
project-lyra/tests/test_scouting.py
serversdown 9cd962625d feat: scouting desk — proactive villain recall injected before she replies
Phase 2. On every poker-context turn, detect players named or described in the
message and slide their structured history into her prompt as a SCOUTING DESK
note (cite-don't-invent). Fail-safe: any desk error is swallowed, never breaks
the turn; silence is the default.

- poker.villain_recall(id): episodic brief — times/where seen, last seen, notable
  hands (linkable ids), reads, stats (Gap 1: the when/where/which-hand narrative).
- scouting.scout(): named hits (deterministic word match) + descriptor spans
  (regex → resolve_villain). High → surface + "confirm it's the same guy";
  ambiguous → file needs_clarification to the queue instead of interrupting;
  generic → stay silent.
- mind.build_messages wires it in, gated to poker modes (poker_cash/study).

5 tests. Full suite 146 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 22:49:40 +00:00

74 lines
2.7 KiB
Python

"""The scouting desk: named + descriptor recall, ambiguous→queue, generic→silence."""
from __future__ import annotations
import importlib
import numpy as np
import pytest
def _fake_embed(texts):
out = []
for t in texts:
v = np.zeros(64, dtype=np.float32)
for w in t.lower().split():
v[hash(w) % 64] += 1.0
out.append((v if v.any() else np.full(64, 1e-6, dtype=np.float32)).tolist())
return out
@pytest.fixture
def mods(tmp_path, monkeypatch):
monkeypatch.setenv("LYRA_DB_PATH", str(tmp_path / "test.db"))
from lyra import llm
monkeypatch.setattr(llm, "embed", _fake_embed)
import lyra.memory as memory
importlib.reload(memory)
import lyra.poker as poker
importlib.reload(poker)
import lyra.scouting as scouting
importlib.reload(scouting)
return poker, scouting
def test_named_player_surfaces_a_brief(mods):
poker, scouting = mods
sid = poker.start_session(venue="Meadows", stakes="1/3", buy_in=300)
pid = poker.upsert_player("Sleepy John", venue="Meadows", category="reg")
poker._c().execute(
"INSERT INTO player_observations (player_id, session_id, cards, created_at) VALUES (?,?,?,?)",
(pid, sid, "As Ks", poker._now()))
poker._c().commit()
note = scouting.scout("sleepy john just sat down on my left", venue="Meadows")
assert note and "Sleepy John" in note and "SCOUTING DESK" in note
def test_descriptor_high_match_surfaces_with_confirm(mods):
poker, scouting = mods
poker.create_descriptor_villain("neck tattoo sleeve arm", venue="Meadows", category="reg")
note = scouting.scout("the neck tattoo sleeve guy just 3bet me again", venue="Meadows")
assert note and "confirm it's the same guy" in note
def test_ambiguous_descriptor_queues_instead_of_interrupting(mods):
poker, scouting = mods
poker.create_descriptor_villain("neck tattoo sleeve arm", venue="Meadows")
note = scouting.scout("the neck tattoo guy raised", venue="Meadows")
assert note is None # didn't interrupt
q = poker.list_identity_queue()
assert q and q[0]["kind"] == "needs_clarification"
def test_generic_descriptor_stays_silent(mods):
poker, scouting = mods
poker.create_descriptor_villain("neck tattoo sleeve arm", venue="Meadows")
note = scouting.scout("the mid aged white guy with glasses raised", venue="Meadows")
assert note is None
assert poker.list_identity_queue() == [] # no queue spam for a non-identifier
def test_no_player_reference_returns_nothing(mods):
poker, scouting = mods
poker.upsert_player("Sleepy John", venue="Meadows")
assert scouting.scout("i folded pocket kings to a 4bet", venue="Meadows") is None