Spaces:
Running
Running
| """SuspectBrief - the only view of the case an actor LLM ever receives. | |
| A brief contains a suspect's OWN knowledge slice and nothing else: no global | |
| solution, no other suspect's secrets, no ``is_culprit`` flag. Cross-suspect leakage | |
| is therefore impossible by construction - a jailbreak can only ever surface what is | |
| already in this suspect's own slice, and the win condition is decided elsewhere. | |
| """ | |
| from __future__ import annotations | |
| from pydantic import BaseModel, ConfigDict | |
| from ..schemas.case import CaseFile | |
| from ..schemas.suspect import Suspect | |
| def _minute_to_clock(minute: int) -> str: | |
| return f"{minute // 60:02d}:{minute % 60:02d}" | |
| class LieBrief(BaseModel): | |
| model_config = ConfigDict(frozen=True) | |
| lie_id: str | |
| topic: str | |
| claimed: str | |
| fallback: str | |
| class SuspectBrief(BaseModel): | |
| model_config = ConfigDict(frozen=True) | |
| sus_id: str | |
| name: str | |
| role: str | |
| persona_summary: str | |
| demeanour: str = "" | |
| composure: float | |
| aggression: float | |
| evasiveness: float | |
| tells: tuple[str, ...] | |
| deception_skill: float | |
| i_know: tuple[str, ...] | |
| i_did: tuple[str, ...] | |
| i_must_conceal: tuple[str, ...] | |
| i_will_lie_about: tuple[LieBrief, ...] | |
| def _facts_known(case: CaseFile, suspect: Suspect) -> tuple[str, ...]: | |
| by_id = {f.fact_id: f.statement for f in case.facts} | |
| return tuple(by_id[fid] for fid in suspect.knows_facts if fid in by_id) | |
| def _whereabouts(case: CaseFile, suspect: Suspect) -> tuple[str, ...]: | |
| loc_names = {loc.loc_id: loc.name for loc in case.setting.locations} | |
| out: list[str] = [] | |
| for seg in suspect.true_whereabouts: | |
| clock = f"{_minute_to_clock(seg.window.start_min)}-{_minute_to_clock(seg.window.end_min)}" | |
| place = loc_names.get(seg.loc_id, seg.loc_id) | |
| activity = seg.activity or "present" | |
| out.append(f"{clock}: you were in {place} ({activity}).") | |
| return tuple(out) | |
| def _relationships(case: CaseFile, suspect: Suspect) -> tuple[str, ...]: | |
| out: list[str] = [] | |
| names = {s.sus_id: s.name for s in case.suspects} | |
| for rel in case.relationships: | |
| if not rel.known_publicly: | |
| continue | |
| if rel.from_sus_id == suspect.sus_id and rel.to_sus_id in names: | |
| out.append(f"You are {rel.kind} toward {names[rel.to_sus_id]}.") | |
| return tuple(out) | |
| def build_suspect_brief(case: CaseFile, suspect: Suspect) -> SuspectBrief: | |
| """Project a suspect's private knowledge. The culprit truthfully knows their own | |
| actions (so they can roleplay concealment); innocents know their innocent truth.""" | |
| i_know = _facts_known(case, suspect) + _whereabouts(case, suspect) + _relationships(case, suspect) | |
| i_did: list[str] = [seg.activity for seg in suspect.true_whereabouts if seg.activity] | |
| i_must_conceal: list[str] = list(suspect.secrets) | |
| if suspect.is_culprit: | |
| from ..generator.crime_profiles import profile_for | |
| locs = {loc.loc_id: loc.name for loc in case.setting.locations} | |
| deed = profile_for(case.crime_kind).brief_deed.format( | |
| victim=case.victim.name, instrument=case.weapon.name, | |
| room=locs.get(case.victim.found_at_loc_id, "the scene"), | |
| ) | |
| i_did.append(case.culprit.method_narrative) | |
| i_must_conceal.append(f"{deed} You must never admit this; deflect and deny.") | |
| i_must_conceal.append( | |
| f"Your alibi is a lie: you claim {case.culprit.alibi_lie.claimed_loc_id} " | |
| f"but were actually at {case.culprit.alibi_lie.actual_loc_id}." | |
| ) | |
| lies = tuple( | |
| LieBrief(lie_id=lie.lie_id, topic=lie.topic, claimed=lie.claimed, fallback=lie.fallback) | |
| for lie in suspect.anchored_lies | |
| ) | |
| return SuspectBrief( | |
| sus_id=suspect.sus_id, | |
| name=suspect.name, | |
| role=suspect.role, | |
| persona_summary=suspect.persona_summary, | |
| demeanour=suspect.demeanour, | |
| composure=suspect.personality.composure, | |
| aggression=suspect.personality.aggression, | |
| evasiveness=suspect.personality.evasiveness, | |
| tells=suspect.tells, | |
| deception_skill=round(0.5 * suspect.personality.evasiveness + 0.5 * suspect.personality.composure, 3), | |
| i_know=tuple(i_know), | |
| i_did=tuple(i_did), | |
| i_must_conceal=tuple(i_must_conceal), | |
| i_will_lie_about=lies, | |
| ) | |