f-id / src /id /models.py
marcodsn's picture
Initial Gradio Space
0423b99
Raw
History Blame Contribute Delete
6.82 kB
"""Pydantic models for world artifacts, runtime state, claims, and events.
These mirror the YAML frontmatter / JSON shapes described in PLAN.md sections
6, 7, and 8. Artifacts are authored as markdown-with-frontmatter; these models
validate the machine-readable parts after parsing.
"""
from __future__ import annotations
from datetime import UTC, datetime
from enum import StrEnum
from typing import Any, Literal
from pydantic import BaseModel, Field
def utcnow_iso() -> str:
"""ISO-8601 UTC timestamp used across ledgers and events."""
return datetime.now(UTC).isoformat()
# --------------------------------------------------------------------------
# World artifacts (Section 6)
# --------------------------------------------------------------------------
class TimelineSlice(BaseModel):
"""One row of the structured movement schedule — the spine of the case."""
time_slice: str
character: str
location: str
action: str
observed_by: list[str] = Field(default_factory=list)
class Timeline(BaseModel):
slices: list[TimelineSlice] = Field(default_factory=list)
def for_character(self, name: str) -> list[TimelineSlice]:
return [s for s in self.slices if s.character == name]
def slice_by_id(self, time_slice: str) -> list[TimelineSlice]:
return [s for s in self.slices if s.time_slice == time_slice]
class EnvObject(BaseModel):
"""An authored object/detail in the environment with its true state."""
id: str
location: str
description_true: str
evidential: bool = False
clue: str | None = None
visible_by_default: bool = True
class ClueNode(BaseModel):
"""A discoverable clue node with unlock dependencies and exonerations."""
id: str
reveals: str
sources: list[str] = Field(default_factory=list)
unlocks: list[str] = Field(default_factory=list)
exonerates: list[str] = Field(default_factory=list)
required_for_solution: bool = False
# Optional prerequisites; if absent, derived from other nodes' `unlocks`.
requires: list[str] = Field(default_factory=list)
CrackBehavior = Literal[
"deflect", "silence", "anger", "lawyer_up", "leave", "partial_confess"
]
SecretKind = Literal["guilty", "innocent"]
Role = Literal["victim", "suspect", "witness", "accomplice"]
class KnowledgeBoundary(BaseModel):
witnessed: list[str] = Field(default_factory=list)
topics_known: list[str] = Field(default_factory=list)
topics_unknowable: list[str] = Field(default_factory=list)
class CharacterCard(BaseModel):
"""Full character model. Frontmatter (machine) + prose (voice)."""
name: str
role: Role = "suspect"
guilty: bool = False
truth: str = ""
knows: KnowledgeBoundary = Field(default_factory=KnowledgeBoundary)
cover: str = ""
never_admit: list[str] = Field(default_factory=list)
cracks_when: str = ""
crack_behavior: CrackBehavior = "deflect"
tells: list[str] = Field(default_factory=list)
secret_kind: SecretKind = "guilty"
exoneration: str | None = None
# Prose voice section (after frontmatter).
voice: str = ""
class Alibi(BaseModel):
"""A pre-authored shared lie rehearsed by accomplices."""
id: str
characters: list[str] = Field(default_factory=list)
agreed_facts: str = ""
agreed_timeline: str = ""
cover_per_character: dict[str, str] = Field(default_factory=dict)
# The points where the alibi is actually false vs. the real timeline.
seams: list[str] = Field(default_factory=list)
body: str = ""
class Solution(BaseModel):
"""GROUND TRUTH. Engine-only; never shown to the player pre-accusation."""
culprit: str
means: str
motive: str
opportunity: str
true_sequence: str = ""
body: str = ""
class WorldMeta(BaseModel):
world_id: str
created: str = Field(default_factory=utcnow_iso)
seed: str = ""
one_line: str = ""
setting: str = ""
twist_tag: str = ""
play_count: int = 0
title: str = ""
# --------------------------------------------------------------------------
# Runtime state (Section 7)
# --------------------------------------------------------------------------
Polarity = Literal["affirm", "deny", "neutral"]
TruthValue = Literal["true", "false", "unknown"]
class Claim(BaseModel):
"""A structured proposition extracted from a character utterance."""
claim_id: str
topic: str
proposition: str
turn: int
polarity: Polarity = "neutral"
engine_truth_value: TruthValue = "unknown"
class LedgerEntry(BaseModel):
"""One committed utterance: verbatim + structured claims."""
turn: int
raw: str
claims: list[Claim] = Field(default_factory=list)
ts: str = Field(default_factory=utcnow_iso)
class CharacterLedger(BaseModel):
character: str
entries: list[LedgerEntry] = Field(default_factory=list)
@property
def claims(self) -> list[Claim]:
out: list[Claim] = []
for e in self.entries:
out.extend(e.claims)
return out
class TranscriptEvent(BaseModel):
"""One ordered line in the session transcript."""
turn: int
kind: Literal[
"player",
"character",
"world",
"crack",
"confront",
"accuse",
"system",
]
actor: str = "" # character/world name, or "player"
text: str
ts: str = Field(default_factory=utcnow_iso)
meta: dict[str, Any] = Field(default_factory=dict)
class SessionStatus(StrEnum):
active = "active"
solved = "solved"
closed = "closed"
class AccusationResult(BaseModel):
culprit_named: str
culprit_correct: bool
means_supported: bool
motive_supported: bool
opportunity_supported: bool
grade: Literal["solved", "right_culprit_unproven", "wrong"]
debrief: str = ""
missed_clues: list[str] = Field(default_factory=list)
class SessionState(BaseModel):
session_id: str
world_id: str
started_at: str = Field(default_factory=utcnow_iso)
status: SessionStatus = SessionStatus.active
turn: int = 0
discovered_clues: list[str] = Field(default_factory=list)
cracked: list[str] = Field(default_factory=list) # characters in crack state
accusation: AccusationResult | None = None
# --------------------------------------------------------------------------
# Token ledger (Section 4)
# --------------------------------------------------------------------------
class UsageRecord(BaseModel):
ts: str = Field(default_factory=utcnow_iso)
world_id: str = ""
session_id: str = ""
task: str = ""
tier: str = ""
provider: str = ""
model: str = ""
prompt_tokens: int = 0
completion_tokens: int = 0
total_tokens: int = 0
ok: bool = True
retries: int = 0