ShubhamSetia's picture
fetch-initial-data-from-llm (#7)
ed151e4
Raw
History Blame Contribute Delete
7.53 kB
from dataclasses import dataclass, field
from datetime import datetime, timezone
from uuid import uuid4
from typing import Any, Literal
from pydantic import BaseModel, Field, ValidationInfo, field_validator
"""
Core data models for the puppet theater simulation.
This module defines the structured objects used throughout a theater session,
including:
- Actors and their state (goals, secrets, mood, props, memory)
- Director decisions that guide story progression
- Actor responses for each scene beat
- Tool requests and tool execution results
- Individual beats in the transcript
- The overall theater session state and configuration
Pydantic models are used where validation is required for LLM-generated
outputs (e.g. actor responses and director decisions), while dataclasses
are used for runtime session state and domain entities.
In short: this file defines the data structures that represent the
story, characters, dialogue, stage actions, and session state of
the puppet theater engine.
"""
BeatType = Literal[
"setup",
"denial_or_contradiction",
"evidence_or_prop",
"secret_reveal",
"chaos_or_intervention",
"finale",
]
SimpleToolValue = str | int | float | bool | None
class ToolRequest(BaseModel):
"""
Represents a request to call a tool.
Pydantic automatically validates the input data when an
instance is created. Ensures the tool name and reason are
non-empty and that the reason remains concise.
"""
tool_name: str
arguments: Any = Field(default_factory=dict)
reason: str
@field_validator("tool_name", "reason")
@classmethod
def require_text(cls, value: str) -> str:
cleaned = " ".join(value.strip().split())
if not cleaned:
raise ValueError("field must not be empty")
return cleaned
@field_validator("reason")
@classmethod
def keep_reason_short(cls, value: str) -> str:
if len(value) > 140:
raise ValueError("reason must be 140 characters or fewer")
return value
class ActorResponse(BaseModel):
"""
Represents a structured response from a puppet actor for one scene beat.
Includes:
- intent: actor's short goal for this beat
- line: dialogue spoken on stage
- emotion: actor's emotional state
- gesture: physical action performed
- stage_effect: environmental/stage effect
- memory_update: optional note carried to future beats
- tool_request: optional request to use a tool
Validators enforce length limits, remove extra whitespace,
and prevent required fields from being empty.
"""
intent: str = Field(default="Keep the scene moving.", description="Short visible actor intention.")
line: str = Field(description="Short, stage-ready puppet dialogue.")
emotion: str
gesture: str
stage_effect: str
memory_update: str = Field(default="", description="Short visible memory note from this beat.")
tool_request: ToolRequest | None = None
@field_validator("intent", "line", "emotion", "gesture", "stage_effect", "memory_update")
@classmethod
def require_text(cls, value: str, info: ValidationInfo) -> str:
cleaned = " ".join(value.strip().split())
if not cleaned and info.field_name != "memory_update":
raise ValueError("field must not be empty")
return cleaned
@field_validator("intent")
@classmethod
def keep_intent_short(cls, value: str) -> str:
if len(value) > 90:
raise ValueError("intent must be 90 characters or fewer")
return value
@field_validator("line")
@classmethod
def keep_line_short(cls, value: str) -> str:
if not value:
raise ValueError("line must not be empty")
if len(value.split()) > 25:
raise ValueError("line must be 25 words or fewer")
if len(value) > 220:
raise ValueError("line must be 220 characters or fewer")
return value
@field_validator("memory_update")
@classmethod
def keep_memory_short(cls, value: str) -> str:
if len(value) > 140:
raise ValueError("memory_update must be 140 characters or fewer")
return value
class DirectorDecision(BaseModel):
next_speaker: str
beat_type: BeatType
instruction: str
stage_effect: str
uses_prop: bool = False
reveal_secret: bool = False
should_end_scene: bool = False
reason_summary: str
@field_validator("next_speaker", "instruction", "stage_effect", "reason_summary")
@classmethod
def require_text(cls, value: str) -> str:
cleaned = " ".join(value.strip().split())
if not cleaned:
raise ValueError("field must not be empty")
return cleaned
@field_validator("instruction", "reason_summary")
@classmethod
def keep_brief(cls, value: str) -> str:
if len(value) > 240:
raise ValueError("field must be 240 characters or fewer")
return value
@dataclass
class Actor:
name: str
avatar: str # Emoji or short label; used if avatar_image_url is unset/invalid.
goal: str
secret: str
speaking_style: str
tools: list[str] = field(default_factory=list)
avatar_image_url: str | None = None # Optional HTTPS portrait for the stage card.
held_prop: str | None = None
mood: str = "ready"
current_goal: str | None = None
goal_progress: str = "Waiting for the curtain."
held_props: list[str] = field(default_factory=list)
secret_status: Literal["hidden", "hinted", "revealed", "resolved"] = "hidden"
recent_memory: list[str] = field(default_factory=list)
@dataclass
class Beat:
speaker: str
intent: str
line: str
emotion: str
gesture: str
stage_effect: str
memory_update: str = ""
tool_request: ToolRequest | None = None
@dataclass
class ToolResult:
tool_name: str
result: str
actor_name: str
reason: str
arguments: dict[str, SimpleToolValue] = field(default_factory=dict)
stage_effect: str | None = None
@dataclass
class TheaterSession:
show_title: str
premise: str
setting: str
actors: list[Actor]
backdrop_image_url: str | None = None # Optional HTTPS image layered behind stage copy.
backdrop_description: str | None = None # LLM minimal art-direction text used to pick backdrop_image_url.
session_id: str = field(default_factory=lambda: uuid4().hex[:12])
created_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
beat_index: int = 0
min_beats: int = 7
target_beats: int = 10
max_beats: int = 12
show_length_mode: str = "standard"
transcript: list[Beat] = field(default_factory=list)
props: list[str] = field(default_factory=list)
latest_prop: str | None = None
latest_audience_action: str | None = None
latest_tool_result: ToolResult | None = None
recent_tool_results: list[ToolResult] = field(default_factory=list)
stage_lighting: str = "warm_spotlight"
director_log: list[str] = field(default_factory=list)
trace_events: list[dict[str, Any] | str] = field(default_factory=list)
finale_requested: bool = False
backend_name: str = "deterministic"
backend_model_id: str | None = None
backend_max_new_tokens: int = 120
backend_temperature: float = 0.75
director_mode: str = "deterministic"
# One-shot: show opening-curtain animation on the first stage render after create.
play_opening_curtain: bool = False