Spaces:
Running
Running
Case Zero - initial public release (fully local: Qwen2.5-1.5B via llama.cpp + Supertonic, custom pixel-noir SPA via gradio.Server)
414dc55 | """The Director - deterministic adjudication of an interrogation turn. | |
| This is the mechanical authority. The model's ``internal`` block is advisory; whether | |
| a lie is caught, a contradiction fires, or a fact is genuinely revealed is decided | |
| here by comparing presented evidence and claimed reveals against ground truth. A | |
| table lookup, never a second LLM call - so the per-turn path is exactly one model | |
| call, and a jailbroken confession in prose changes nothing here. | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| from ..schemas.case import CaseFile | |
| from ..schemas.enums import Relevance | |
| from ..schemas.interrogation import InterrogationTurn | |
| from ..schemas.suspect import Suspect | |
| from .game_state import SuspectState | |
| from .relevance import assess_relevance | |
| _STRESS_ON_BREAK = 0.40 | |
| _STRESS_ON_DIRECT = 0.15 | |
| _STRESS_ON_TANGENTIAL = 0.05 | |
| _STRESS_DECAY = 0.95 | |
| _RAPPORT_NEUTRAL = 0.02 | |
| _RAPPORT_ON_BREAK = -0.12 | |
| class Adjudication: | |
| relevance: Relevance | |
| is_contradiction: bool | |
| broken_lie_id: str | None | |
| revealed_fact_ids: tuple[str, ...] | |
| stress_after: float | |
| rapport_after: float | |
| def _clamp(value: float, low: float, high: float) -> float: | |
| return max(low, min(high, value)) | |
| def _grounded_reveals( | |
| suspect: Suspect, turn: InterrogationTurn, already: frozenset[str] | |
| ) -> tuple[str, ...]: | |
| """Accept a model-claimed reveal only if the suspect actually knows that fact and | |
| it is not one they must conceal. Keeps reveals grounded in the suspect's slice.""" | |
| known = set(suspect.knows_facts) | |
| concealed = set(suspect.must_lie_about) | |
| out: list[str] = [] | |
| for fid in turn.internal.revealed_fact_ids: | |
| if fid in known and fid not in concealed and fid not in already: | |
| out.append(fid) | |
| return tuple(out) | |
| def adjudicate( | |
| case: CaseFile, | |
| suspect: Suspect, | |
| state: SuspectState, | |
| turn: InterrogationTurn, | |
| presented_clue_id: str | None, | |
| ) -> Adjudication: | |
| result = assess_relevance(case, suspect, presented_clue_id) | |
| is_contradiction = False | |
| broken_lie_id: str | None = None | |
| reveals: list[str] = list(_grounded_reveals(suspect, turn, state.revealed_fact_ids)) | |
| if result.relevance is Relevance.BREAKING: | |
| lie_id = result.broken_lie_id | |
| # Fire once: dedup by the anchored lie when known, else by the presented clue. | |
| if lie_id is not None: | |
| already_fired = lie_id in state.broken_lie_ids | |
| else: | |
| already_fired = presented_clue_id is not None and presented_clue_id in state.evidence_shown | |
| if not already_fired: | |
| is_contradiction = True | |
| broken_lie_id = lie_id | |
| if lie_id is not None: | |
| lie = next((x for x in suspect.anchored_lies if x.lie_id == lie_id), None) | |
| if lie and lie.truth_ref and lie.truth_ref not in reveals: | |
| reveals.append(lie.truth_ref) | |
| stress = state.stress * _STRESS_DECAY | |
| if result.relevance is Relevance.BREAKING: | |
| stress += _STRESS_ON_BREAK | |
| elif result.relevance is Relevance.DIRECT: | |
| stress += _STRESS_ON_DIRECT | |
| elif result.relevance is Relevance.TANGENTIAL: | |
| stress += _STRESS_ON_TANGENTIAL | |
| rapport = state.rapport + (_RAPPORT_ON_BREAK if is_contradiction else _RAPPORT_NEUTRAL) | |
| return Adjudication( | |
| relevance=result.relevance, | |
| is_contradiction=is_contradiction, | |
| broken_lie_id=broken_lie_id, | |
| revealed_fact_ids=tuple(reveals), | |
| stress_after=_clamp(stress, 0.0, 1.0), | |
| rapport_after=_clamp(rapport, -1.0, 1.0), | |
| ) | |