File size: 4,044 Bytes
414dc55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
"""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:
        i_did.append(case.culprit.method_narrative)
        i_must_conceal.append(
            f"You killed {case.victim.name}. 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,
    )