File size: 7,527 Bytes
6bc4afd
67fbecc
 
6bc4afd
67fbecc
da51a9e
50cccdf
9543536
ed151e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9543536
da51a9e
 
 
 
 
 
 
 
 
 
8855e7f
 
 
 
ed151e4
 
 
 
 
 
 
8855e7f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9543536
ed151e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50cccdf
9543536
 
 
 
50cccdf
8855e7f
9543536
50cccdf
9543536
50cccdf
9543536
50cccdf
9543536
 
 
50cccdf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6bc4afd
da51a9e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3d2961d
 
 
bf65d34
 
 
3d2961d
da51a9e
6bc4afd
 
 
ed151e4
6bc4afd
 
 
 
ed151e4
9c70cda
50cccdf
 
 
 
 
 
6bc4afd
 
 
 
 
50cccdf
6bc4afd
 
 
 
50cccdf
8855e7f
 
 
 
 
 
 
 
 
 
 
6bc4afd
 
 
 
 
 
 
 
ed151e4
 
67fbecc
 
6bc4afd
c3e33f1
 
 
aa14d1f
6bc4afd
 
9c70cda
 
8855e7f
 
 
6bc4afd
67fbecc
6bc4afd
9543536
7fdbbb5
017769b
 
3d2961d
202450d
 
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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
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