case0 / src /case_zero /suspects /memory.py
HusseinEid's picture
Case Zero - initial public release (fully local: Qwen2.5-1.5B via llama.cpp + Supertonic, custom pixel-noir SPA via gradio.Server)
414dc55
raw
history blame
2.2 kB
"""Compact memory for a suspect prompt.
Two cheap, bounded sources keep a small model consistent without a vector store:
a structured ledger (what has been established) and a rolling transcript buffer.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from ..constants import ROLLING_BUFFER_TURNS
from ..schemas.case import CaseFile
from ..schemas.suspect import Suspect
if TYPE_CHECKING: # avoid a runtime cycle: engine imports the suspects layer
from ..engine.game_state import SuspectState
def _fact_lookup(case: CaseFile) -> dict[str, str]:
return {f.fact_id: f.statement for f in case.facts}
def ledger_text(case: CaseFile, suspect: Suspect, state: SuspectState) -> str:
facts = _fact_lookup(case)
lines: list[str] = []
if state.revealed_fact_ids:
admitted = "; ".join(sorted(facts.get(fid, fid) for fid in state.revealed_fact_ids))
lines.append(f"You have already admitted: {admitted}")
if state.evidence_shown:
shown = "; ".join(
sorted(_safe_clue_name(case, cid) for cid in state.evidence_shown)
)
lines.append(f"The detective has already shown you: {shown}")
if state.broken_lie_ids:
topics = "; ".join(
sorted(
lie.topic
for lie in suspect.anchored_lies
if lie.lie_id in state.broken_lie_ids
)
)
lines.append(f"You have already been caught lying about: {topics}. Do not repeat that lie.")
if state.stress >= 0.7:
lines.append("You are visibly rattled and close to breaking.")
elif state.stress >= 0.4:
lines.append("You are tense and guarded.")
return "\n".join(lines) if lines else "Nothing has been established yet in this interrogation."
def buffer_text(state: SuspectState, n: int = ROLLING_BUFFER_TURNS) -> str:
recent = state.transcript[-n:]
if not recent:
return "(no questions asked yet)"
return "\n".join(f'Detective: "{e.question}"\nYou: "{e.answer}"' for e in recent)
def _safe_clue_name(case: CaseFile, clue_id: str) -> str:
try:
return case.clue(clue_id).name
except KeyError:
return clue_id