File size: 1,718 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
"""The dual-output contract for one suspect turn.

``spoken`` is a distinct JSON string node, so hidden state physically cannot leak
into the dialogue shown to the player. ``internal`` is parsed for flavour and UI
cues only - it is ADVISORY. The deterministic director (engine.director) is the
sole authority on whether a lie was caught or a fact was truly revealed.
"""

from __future__ import annotations

from pydantic import BaseModel, ConfigDict, Field

from ..constants import DECEPTION_MAX, DECEPTION_MIN
from .enums import EvidenceReaction, Intent


class InternalState(BaseModel):
    model_config = ConfigDict(frozen=True, extra="ignore")

    # Ordered first so the small model "reasons" before committing to a line.
    private_reasoning: str = ""
    intent: Intent = Intent.DEFLECT
    addressed_topic: str = ""
    is_lying: bool = False
    active_lie_id: str | None = None
    lie_refs: tuple[str, ...] = ()
    evidence_reaction: EvidenceReaction = EvidenceReaction.NONE
    deception_level: int = Field(default=0, ge=DECEPTION_MIN, le=DECEPTION_MAX)
    stress: float = Field(default=0.0, ge=0.0, le=1.0)
    revealed_fact_ids: tuple[str, ...] = ()
    contradicted_by_evidence: bool = False
    consistency_flag: bool = True
    slip: bool = False


class InterrogationTurn(BaseModel):
    model_config = ConfigDict(frozen=True, extra="ignore")

    spoken: str
    narration: str = ""
    internal: InternalState = InternalState()

    @staticmethod
    def safe_default(line: str = "I have nothing more to say about that.") -> InterrogationTurn:
        """A valid fallback turn used when generation or validation fails."""
        return InterrogationTurn(spoken=line, internal=InternalState())