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>
This commit is contained in:
@@ -1511,6 +1511,45 @@ def get_villain_file(name: str | None = None, venue: str | None = None) -> list[
|
||||
return [dict(r) for r in _c().execute(sql, params).fetchall()]
|
||||
|
||||
|
||||
def villain_recall(player_id: int) -> dict | None:
|
||||
"""Episodic recall for one villain: who, how often/where seen, last seen, the
|
||||
notable hands against him (with ids to link), reads, and stats. This is the
|
||||
when/where/which-hand narrative the scouting desk surfaces (Gap 1)."""
|
||||
p = _c().execute("SELECT * FROM poker_players WHERE id = ?", (player_id,)).fetchone()
|
||||
if not p:
|
||||
return None
|
||||
p = dict(p)
|
||||
obs = [dict(r) for r in _c().execute(
|
||||
"SELECT o.*, s.venue AS s_venue, s.started_at AS s_at FROM player_observations o "
|
||||
"LEFT JOIN poker_sessions s ON s.id = o.session_id WHERE o.player_id = ? "
|
||||
"ORDER BY o.id DESC", (player_id,)
|
||||
).fetchall()]
|
||||
reads = [dict(r) for r in _c().execute(
|
||||
"SELECT note, created_at FROM player_reads WHERE player_id = ? ORDER BY id DESC LIMIT 8",
|
||||
(player_id,)
|
||||
).fetchall()]
|
||||
sessions_seen = sorted({o["session_id"] for o in obs if o.get("session_id")} |
|
||||
{r_["session_id"] for r_ in _c().execute(
|
||||
"SELECT session_id FROM player_reads WHERE player_id = ?",
|
||||
(player_id,)).fetchall() if r_["session_id"]})
|
||||
last_at = max([o.get("s_at") or o.get("created_at") for o in obs] +
|
||||
[r["created_at"] for r in reads] + [p.get("updated_at")], default=None)
|
||||
# Notable hands: showdowns / anything with cards, most recent first, linkable.
|
||||
notable = [{"hand_id": o["hand_id"], "session_id": o.get("session_id"),
|
||||
"when": o.get("s_at") or o.get("created_at"), "cards": o.get("cards"),
|
||||
"summary": o.get("summary")}
|
||||
for o in obs if o.get("hand_id")][:6]
|
||||
prof = player_profile(p["name"]) or {}
|
||||
return {
|
||||
"player": p, "named": bool(p.get("named")),
|
||||
"times_seen": len(sessions_seen), "sessions_seen": sessions_seen,
|
||||
"last_seen": last_at, "notable_hands": notable,
|
||||
"reads": [r["note"] for r in reads],
|
||||
"stats": prof.get("stats"), "observations": len(obs),
|
||||
"descriptors": p.get("descriptors"),
|
||||
}
|
||||
|
||||
|
||||
# --- stats ---
|
||||
|
||||
def session_stats(session_id: int | None = None) -> dict:
|
||||
|
||||
Reference in New Issue
Block a user