ShubhamSetia commited on
Commit
9543536
·
1 Parent(s): e44d065

refactor: add model backend foundation

Browse files
app.py CHANGED
@@ -27,6 +27,7 @@ EMPTY_STAGE = """
27
  EMPTY_TRANSCRIPT = "No show yet. The transcript will appear here."
28
  EMPTY_DIRECTOR_LOG = "No director notes yet."
29
  EMPTY_TRACE = "No trace events yet."
 
30
  PLAYBACK_DELAY_SECONDS = 0.75
31
  PROP_EMOJI = {
32
  "rubber duck": "🐤",
@@ -911,12 +912,23 @@ def render_trace(session: TheaterSession | None) -> str:
911
  return "\n".join(f"- {entry}" for entry in session.trace_events)
912
 
913
 
 
 
 
 
 
 
 
 
 
 
914
  def render_outputs(session: TheaterSession | None):
915
  return (
916
  render_stage(session),
917
  render_transcript(session),
918
  render_director_log(session),
919
  render_trace(session),
 
920
  )
921
 
922
 
@@ -929,6 +941,7 @@ def create_show(premise: str, session: TheaterSession | None):
929
  "No premise yet. Add a premise to raise the curtain.",
930
  EMPTY_DIRECTOR_LOG,
931
  EMPTY_TRACE,
 
932
  )
933
 
934
  session = create_show_from_premise(premise)
@@ -945,12 +958,13 @@ def reset_show():
945
  EMPTY_TRANSCRIPT,
946
  EMPTY_DIRECTOR_LOG,
947
  EMPTY_TRACE,
 
948
  )
949
 
950
 
951
  def advance_one_beat(session: TheaterSession | None):
952
  if session is None:
953
- return None, EMPTY_STAGE, "Create a show before running a beat.", EMPTY_DIRECTOR_LOG, EMPTY_TRACE
954
 
955
  session = run_one_beat(session)
956
  return session, *render_outputs(session)
@@ -958,7 +972,7 @@ def advance_one_beat(session: TheaterSession | None):
958
 
959
  def advance_full_act(session: TheaterSession | None):
960
  if session is None:
961
- yield None, EMPTY_STAGE, "Create a show before running the full act.", EMPTY_DIRECTOR_LOG, EMPTY_TRACE
962
  return
963
 
964
  if session.beat_index >= session.max_beats:
@@ -975,7 +989,7 @@ def advance_full_act(session: TheaterSession | None):
975
 
976
  def throw_audience_prop(session: TheaterSession | None, prop_name: str):
977
  if session is None:
978
- return None, EMPTY_STAGE, "Create a show before throwing a prop.", EMPTY_DIRECTOR_LOG, EMPTY_TRACE
979
 
980
  session = throw_prop(session, prop_name)
981
  return session, *render_outputs(session)
@@ -983,7 +997,7 @@ def throw_audience_prop(session: TheaterSession | None, prop_name: str):
983
 
984
  def summon_audience_actor(session: TheaterSession | None, actor_name: str):
985
  if session is None:
986
- return None, EMPTY_STAGE, "Create a show before summoning an actor.", EMPTY_DIRECTOR_LOG, EMPTY_TRACE
987
 
988
  session = summon_actor(session, actor_name)
989
  return session, *render_outputs(session)
@@ -991,7 +1005,7 @@ def summon_audience_actor(session: TheaterSession | None, actor_name: str):
991
 
992
  def request_audience_finale(session: TheaterSession | None):
993
  if session is None:
994
- return None, EMPTY_STAGE, "Create a show before requesting a finale.", EMPTY_DIRECTOR_LOG, EMPTY_TRACE
995
 
996
  session = request_finale(session)
997
  return session, *render_outputs(session)
@@ -1076,36 +1090,43 @@ with gr.Blocks(title="AI Puppet Theater") as app:
1076
  lines=6,
1077
  interactive=False,
1078
  )
 
 
 
 
 
 
 
1079
 
1080
  create_button.click(
1081
  create_show,
1082
  inputs=[premise_input, session_state],
1083
- outputs=[session_state, stage_output, transcript_output, director_output, trace_output],
1084
  )
1085
  run_one_button.click(
1086
  advance_one_beat,
1087
  inputs=[session_state],
1088
- outputs=[session_state, stage_output, transcript_output, director_output, trace_output],
1089
  )
1090
  run_full_button.click(
1091
  advance_full_act,
1092
  inputs=[session_state],
1093
- outputs=[session_state, stage_output, transcript_output, director_output, trace_output],
1094
  )
1095
  throw_prop_button.click(
1096
  throw_audience_prop,
1097
  inputs=[session_state, prop_input],
1098
- outputs=[session_state, stage_output, transcript_output, director_output, trace_output],
1099
  )
1100
  summon_actor_button.click(
1101
  summon_audience_actor,
1102
  inputs=[session_state, actor_input],
1103
- outputs=[session_state, stage_output, transcript_output, director_output, trace_output],
1104
  )
1105
  request_finale_button.click(
1106
  request_audience_finale,
1107
  inputs=[session_state],
1108
- outputs=[session_state, stage_output, transcript_output, director_output, trace_output],
1109
  )
1110
  reset_button.click(
1111
  reset_show,
@@ -1118,6 +1139,7 @@ with gr.Blocks(title="AI Puppet Theater") as app:
1118
  transcript_output,
1119
  director_output,
1120
  trace_output,
 
1121
  ],
1122
  )
1123
 
 
27
  EMPTY_TRANSCRIPT = "No show yet. The transcript will appear here."
28
  EMPTY_DIRECTOR_LOG = "No director notes yet."
29
  EMPTY_TRACE = "No trace events yet."
30
+ EMPTY_BACKEND = "Active backend: deterministic\nFallback: deterministic safety path enabled"
31
  PLAYBACK_DELAY_SECONDS = 0.75
32
  PROP_EMOJI = {
33
  "rubber duck": "🐤",
 
912
  return "\n".join(f"- {entry}" for entry in session.trace_events)
913
 
914
 
915
+ def render_backend_settings(session: TheaterSession | None) -> str:
916
+ backend_name = session.backend_name if session is not None else "deterministic"
917
+ return (
918
+ f"Active backend: {backend_name}\n"
919
+ "Available backends: deterministic\n"
920
+ "LLM backends: not configured\n"
921
+ "Fallback behavior: invalid model output falls back to deterministic actor lines"
922
+ )
923
+
924
+
925
  def render_outputs(session: TheaterSession | None):
926
  return (
927
  render_stage(session),
928
  render_transcript(session),
929
  render_director_log(session),
930
  render_trace(session),
931
+ render_backend_settings(session),
932
  )
933
 
934
 
 
941
  "No premise yet. Add a premise to raise the curtain.",
942
  EMPTY_DIRECTOR_LOG,
943
  EMPTY_TRACE,
944
+ EMPTY_BACKEND,
945
  )
946
 
947
  session = create_show_from_premise(premise)
 
958
  EMPTY_TRANSCRIPT,
959
  EMPTY_DIRECTOR_LOG,
960
  EMPTY_TRACE,
961
+ EMPTY_BACKEND,
962
  )
963
 
964
 
965
  def advance_one_beat(session: TheaterSession | None):
966
  if session is None:
967
+ return None, EMPTY_STAGE, "Create a show before running a beat.", EMPTY_DIRECTOR_LOG, EMPTY_TRACE, EMPTY_BACKEND
968
 
969
  session = run_one_beat(session)
970
  return session, *render_outputs(session)
 
972
 
973
  def advance_full_act(session: TheaterSession | None):
974
  if session is None:
975
+ yield None, EMPTY_STAGE, "Create a show before running the full act.", EMPTY_DIRECTOR_LOG, EMPTY_TRACE, EMPTY_BACKEND
976
  return
977
 
978
  if session.beat_index >= session.max_beats:
 
989
 
990
  def throw_audience_prop(session: TheaterSession | None, prop_name: str):
991
  if session is None:
992
+ return None, EMPTY_STAGE, "Create a show before throwing a prop.", EMPTY_DIRECTOR_LOG, EMPTY_TRACE, EMPTY_BACKEND
993
 
994
  session = throw_prop(session, prop_name)
995
  return session, *render_outputs(session)
 
997
 
998
  def summon_audience_actor(session: TheaterSession | None, actor_name: str):
999
  if session is None:
1000
+ return None, EMPTY_STAGE, "Create a show before summoning an actor.", EMPTY_DIRECTOR_LOG, EMPTY_TRACE, EMPTY_BACKEND
1001
 
1002
  session = summon_actor(session, actor_name)
1003
  return session, *render_outputs(session)
 
1005
 
1006
  def request_audience_finale(session: TheaterSession | None):
1007
  if session is None:
1008
+ return None, EMPTY_STAGE, "Create a show before requesting a finale.", EMPTY_DIRECTOR_LOG, EMPTY_TRACE, EMPTY_BACKEND
1009
 
1010
  session = request_finale(session)
1011
  return session, *render_outputs(session)
 
1090
  lines=6,
1091
  interactive=False,
1092
  )
1093
+ with gr.Accordion("Backend", open=False):
1094
+ backend_output = gr.Textbox(
1095
+ value=EMPTY_BACKEND,
1096
+ label="Model Settings",
1097
+ lines=4,
1098
+ interactive=False,
1099
+ )
1100
 
1101
  create_button.click(
1102
  create_show,
1103
  inputs=[premise_input, session_state],
1104
+ outputs=[session_state, stage_output, transcript_output, director_output, trace_output, backend_output],
1105
  )
1106
  run_one_button.click(
1107
  advance_one_beat,
1108
  inputs=[session_state],
1109
+ outputs=[session_state, stage_output, transcript_output, director_output, trace_output, backend_output],
1110
  )
1111
  run_full_button.click(
1112
  advance_full_act,
1113
  inputs=[session_state],
1114
+ outputs=[session_state, stage_output, transcript_output, director_output, trace_output, backend_output],
1115
  )
1116
  throw_prop_button.click(
1117
  throw_audience_prop,
1118
  inputs=[session_state, prop_input],
1119
+ outputs=[session_state, stage_output, transcript_output, director_output, trace_output, backend_output],
1120
  )
1121
  summon_actor_button.click(
1122
  summon_audience_actor,
1123
  inputs=[session_state, actor_input],
1124
+ outputs=[session_state, stage_output, transcript_output, director_output, trace_output, backend_output],
1125
  )
1126
  request_finale_button.click(
1127
  request_audience_finale,
1128
  inputs=[session_state],
1129
+ outputs=[session_state, stage_output, transcript_output, director_output, trace_output, backend_output],
1130
  )
1131
  reset_button.click(
1132
  reset_show,
 
1139
  transcript_output,
1140
  director_output,
1141
  trace_output,
1142
+ backend_output,
1143
  ],
1144
  )
1145
 
puppet_theater/__init__.py CHANGED
@@ -1,14 +1,20 @@
1
  from puppet_theater.actions import request_finale, summon_actor, throw_prop
 
2
  from puppet_theater.director import BEAT_ARC, run_full_act, run_one_beat
3
- from puppet_theater.models import Actor, Beat, TheaterSession
4
  from puppet_theater.session import create_show_from_premise
5
 
6
  __all__ = [
7
  "Actor",
 
8
  "BEAT_ARC",
9
  "Beat",
 
 
10
  "TheaterSession",
11
  "create_show_from_premise",
 
 
12
  "request_finale",
13
  "run_full_act",
14
  "run_one_beat",
 
1
  from puppet_theater.actions import request_finale, summon_actor, throw_prop
2
+ from puppet_theater.backends import DeterministicBackend, ModelBackend, generate_actor_response, parse_actor_output
3
  from puppet_theater.director import BEAT_ARC, run_full_act, run_one_beat
4
+ from puppet_theater.models import Actor, ActorResponse, Beat, TheaterSession
5
  from puppet_theater.session import create_show_from_premise
6
 
7
  __all__ = [
8
  "Actor",
9
+ "ActorResponse",
10
  "BEAT_ARC",
11
  "Beat",
12
+ "DeterministicBackend",
13
+ "ModelBackend",
14
  "TheaterSession",
15
  "create_show_from_premise",
16
+ "generate_actor_response",
17
+ "parse_actor_output",
18
  "request_finale",
19
  "run_full_act",
20
  "run_one_beat",
puppet_theater/backends.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+ from dataclasses import dataclass
3
+ import json
4
+ from typing import Any
5
+
6
+ from pydantic import ValidationError
7
+
8
+ from puppet_theater.models import Actor, ActorResponse, TheaterSession
9
+
10
+
11
+ MAX_ACTOR_LINE_CHARS = 220
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class BackendGeneration:
16
+ response: ActorResponse
17
+ backend_name: str
18
+ fallback_used: bool
19
+ validation_status: str
20
+
21
+
22
+ class ModelBackend(ABC):
23
+ name: str = "base"
24
+
25
+ @abstractmethod
26
+ def generate_actor_response(
27
+ self,
28
+ session: TheaterSession,
29
+ beat_type: str,
30
+ speaker: Actor,
31
+ prop: str | None,
32
+ ) -> ActorResponse | dict[str, Any] | str:
33
+ """Return raw or structured actor output for one beat."""
34
+
35
+
36
+ class DeterministicBackend(ModelBackend):
37
+ name = "deterministic"
38
+
39
+ def generate_actor_response(
40
+ self,
41
+ session: TheaterSession,
42
+ beat_type: str,
43
+ speaker: Actor,
44
+ prop: str | None,
45
+ ) -> ActorResponse:
46
+ return deterministic_actor_response(session, beat_type, speaker, prop)
47
+
48
+
49
+ def deterministic_actor_response(
50
+ session: TheaterSession,
51
+ beat_type: str,
52
+ speaker: Actor,
53
+ prop: str | None,
54
+ ) -> ActorResponse:
55
+ return ActorResponse(
56
+ line=_line_for_beat(session, beat_type, speaker, prop),
57
+ emotion=_emotion_for_beat(beat_type),
58
+ gesture=_gesture_for_beat(beat_type),
59
+ stage_effect=_effect_for_beat(beat_type),
60
+ tool_request=None,
61
+ )
62
+
63
+
64
+ def generate_actor_response(
65
+ session: TheaterSession,
66
+ beat_type: str,
67
+ speaker: Actor,
68
+ prop: str | None,
69
+ backend: ModelBackend | None = None,
70
+ ) -> BackendGeneration:
71
+ active_backend = backend or DeterministicBackend()
72
+ raw_output = active_backend.generate_actor_response(session, beat_type, speaker, prop)
73
+ response, validation_status = parse_actor_output(raw_output)
74
+ if response is not None:
75
+ return BackendGeneration(
76
+ response=response,
77
+ backend_name=active_backend.name,
78
+ fallback_used=False,
79
+ validation_status=validation_status,
80
+ )
81
+
82
+ fallback_response = deterministic_actor_response(session, beat_type, speaker, prop)
83
+ return BackendGeneration(
84
+ response=fallback_response,
85
+ backend_name=active_backend.name,
86
+ fallback_used=True,
87
+ validation_status=validation_status,
88
+ )
89
+
90
+
91
+ def parse_actor_output(raw_output: ActorResponse | dict[str, Any] | str) -> tuple[ActorResponse | None, str]:
92
+ parsed = _coerce_actor_output(raw_output)
93
+ if parsed is None:
94
+ return None, "invalid_schema"
95
+
96
+ try:
97
+ response = ActorResponse.model_validate(parsed)
98
+ except ValidationError:
99
+ return None, "invalid_required_fields"
100
+
101
+ if not response.line.strip():
102
+ return None, "invalid_empty_line"
103
+
104
+ if len(response.line) > MAX_ACTOR_LINE_CHARS:
105
+ capped_line = response.line[: MAX_ACTOR_LINE_CHARS - 3].rstrip() + "..."
106
+ response = response.model_copy(update={"line": capped_line})
107
+ return response, "valid_line_capped"
108
+
109
+ return response, "valid"
110
+
111
+
112
+ def _coerce_actor_output(raw_output: ActorResponse | dict[str, Any] | str) -> ActorResponse | dict[str, Any] | None:
113
+ if isinstance(raw_output, ActorResponse):
114
+ return raw_output
115
+ if isinstance(raw_output, dict):
116
+ return raw_output
117
+ if isinstance(raw_output, str):
118
+ text = raw_output.strip()
119
+ if not text:
120
+ return None
121
+ try:
122
+ decoded = json.loads(text)
123
+ except json.JSONDecodeError:
124
+ return None
125
+ return decoded if isinstance(decoded, dict) else None
126
+ return None
127
+
128
+
129
+ def _line_for_beat(
130
+ session: TheaterSession,
131
+ beat_type: str,
132
+ speaker: Actor,
133
+ prop: str | None,
134
+ ) -> str:
135
+ if prop is not None:
136
+ return f"I shall use this {prop} as evidence, a prop, and possibly a tiny emotional support object."
137
+ if beat_type == "setup":
138
+ return f"I see it clearly: {session.premise}, and somehow I am in charge."
139
+ if beat_type == "denial_or_contradiction":
140
+ return "Absolutely not. The premise is innocent, which is exactly what makes it suspicious."
141
+ if beat_type == "evidence_or_prop":
142
+ return "I found a prop with fingerprints, glitter, and a very dramatic attitude."
143
+ if beat_type == "secret_reveal":
144
+ return f"I confess: {speaker.secret}"
145
+ if beat_type == "chaos_or_intervention":
146
+ return "The audience has interrupted with imaginary confetti, so everyone must panic gracefully."
147
+ return "Curtain call! We solved nothing, learned everything, and bowed before the wobble got worse."
148
+
149
+
150
+ def _emotion_for_beat(beat_type: str) -> str:
151
+ return {
152
+ "setup": "curious",
153
+ "denial_or_contradiction": "defensive",
154
+ "evidence_or_prop": "suspicious",
155
+ "secret_reveal": "confessional",
156
+ "chaos_or_intervention": "frantic",
157
+ "finale": "triumphant",
158
+ }[beat_type]
159
+
160
+
161
+ def _gesture_for_beat(beat_type: str) -> str:
162
+ return {
163
+ "setup": "raise_curtain",
164
+ "denial_or_contradiction": "shake_head",
165
+ "evidence_or_prop": "present_prop",
166
+ "secret_reveal": "lean_to_audience",
167
+ "chaos_or_intervention": "flail_politely",
168
+ "finale": "deep_bow",
169
+ }[beat_type]
170
+
171
+
172
+ def _effect_for_beat(beat_type: str) -> str:
173
+ return {
174
+ "setup": "warm_spotlight",
175
+ "denial_or_contradiction": "quick_blackout",
176
+ "evidence_or_prop": "prop_table_glow",
177
+ "secret_reveal": "single_spotlight",
178
+ "chaos_or_intervention": "confetti_rustle",
179
+ "finale": "curtain_fall",
180
+ }[beat_type]
puppet_theater/director.py CHANGED
@@ -1,3 +1,4 @@
 
1
  from puppet_theater.models import Beat, TheaterSession
2
 
3
 
@@ -11,28 +12,6 @@ BEAT_ARC = [
11
  ]
12
 
13
 
14
- def _line_for_beat(
15
- session: TheaterSession,
16
- beat_type: str,
17
- speaker_name: str,
18
- prop: str | None,
19
- ) -> str:
20
- if prop is not None:
21
- return f"I shall use this {prop} as evidence, a prop, and possibly a tiny emotional support object."
22
- if beat_type == "setup":
23
- return f"I see it clearly: {session.premise}, and somehow I am in charge."
24
- if beat_type == "denial_or_contradiction":
25
- return "Absolutely not. The premise is innocent, which is exactly what makes it suspicious."
26
- if beat_type == "evidence_or_prop":
27
- return "I found a prop with fingerprints, glitter, and a very dramatic attitude."
28
- if beat_type == "secret_reveal":
29
- actor = next(actor for actor in session.actors if actor.name == speaker_name)
30
- return f"I confess: {actor.secret}"
31
- if beat_type == "chaos_or_intervention":
32
- return "The audience has interrupted with imaginary confetti, so everyone must panic gracefully."
33
- return "Curtain call! We solved nothing, learned everything, and bowed before the wobble got worse."
34
-
35
-
36
  def run_one_beat(session: TheaterSession | None) -> TheaterSession | None:
37
  if session is None:
38
  return None
@@ -47,16 +26,17 @@ def run_one_beat(session: TheaterSession | None) -> TheaterSession | None:
47
  prop = session.latest_prop
48
  if prop is not None:
49
  speaker.held_prop = prop
50
- line = _line_for_beat(session, beat_type, speaker.name, prop)
51
  session.beat_index += 1
52
 
 
53
  beat = Beat(
54
  speaker=speaker.name,
55
- line=line,
56
- emotion=_emotion_for_beat(beat_type),
57
- gesture=_gesture_for_beat(beat_type),
58
- stage_effect=_effect_for_beat(beat_type),
59
- tool_request=None,
60
  )
61
  session.transcript.append(beat)
62
  if prop is not None:
@@ -66,7 +46,18 @@ def run_one_beat(session: TheaterSession | None) -> TheaterSession | None:
66
  session.director_log.append(
67
  f"Beat {session.beat_index}/{session.max_beats}: {beat_type} assigned to {speaker.name}."
68
  )
 
 
 
 
 
69
  session.trace_events.append(f"beat_added:{session.beat_index}:{beat_type}")
 
 
 
 
 
 
70
 
71
  if beat_type == "finale":
72
  session.beat_index = session.max_beats
@@ -84,36 +75,3 @@ def run_full_act(session: TheaterSession | None) -> TheaterSession | None:
84
  while session.beat_index < session.max_beats:
85
  run_one_beat(session)
86
  return session
87
-
88
-
89
- def _emotion_for_beat(beat_type: str) -> str:
90
- return {
91
- "setup": "curious",
92
- "denial_or_contradiction": "defensive",
93
- "evidence_or_prop": "suspicious",
94
- "secret_reveal": "confessional",
95
- "chaos_or_intervention": "frantic",
96
- "finale": "triumphant",
97
- }[beat_type]
98
-
99
-
100
- def _gesture_for_beat(beat_type: str) -> str:
101
- return {
102
- "setup": "raise_curtain",
103
- "denial_or_contradiction": "shake_head",
104
- "evidence_or_prop": "present_prop",
105
- "secret_reveal": "lean_to_audience",
106
- "chaos_or_intervention": "flail_politely",
107
- "finale": "deep_bow",
108
- }[beat_type]
109
-
110
-
111
- def _effect_for_beat(beat_type: str) -> str:
112
- return {
113
- "setup": "warm_spotlight",
114
- "denial_or_contradiction": "quick_blackout",
115
- "evidence_or_prop": "prop_table_glow",
116
- "secret_reveal": "single_spotlight",
117
- "chaos_or_intervention": "confetti_rustle",
118
- "finale": "curtain_fall",
119
- }[beat_type]
 
1
+ from puppet_theater.backends import generate_actor_response
2
  from puppet_theater.models import Beat, TheaterSession
3
 
4
 
 
12
  ]
13
 
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  def run_one_beat(session: TheaterSession | None) -> TheaterSession | None:
16
  if session is None:
17
  return None
 
26
  prop = session.latest_prop
27
  if prop is not None:
28
  speaker.held_prop = prop
29
+ backend_generation = generate_actor_response(session, beat_type, speaker, prop)
30
  session.beat_index += 1
31
 
32
+ response = backend_generation.response
33
  beat = Beat(
34
  speaker=speaker.name,
35
+ line=response.line,
36
+ emotion=response.emotion,
37
+ gesture=response.gesture,
38
+ stage_effect=response.stage_effect,
39
+ tool_request=response.tool_request,
40
  )
41
  session.transcript.append(beat)
42
  if prop is not None:
 
46
  session.director_log.append(
47
  f"Beat {session.beat_index}/{session.max_beats}: {beat_type} assigned to {speaker.name}."
48
  )
49
+ session.director_log.append(
50
+ "Backend "
51
+ f"{backend_generation.backend_name} returned actor output "
52
+ f"({backend_generation.validation_status}, fallback={backend_generation.fallback_used})."
53
+ )
54
  session.trace_events.append(f"beat_added:{session.beat_index}:{beat_type}")
55
+ session.trace_events.append(
56
+ "backend_result:"
57
+ f"{backend_generation.backend_name}:"
58
+ f"fallback={backend_generation.fallback_used}:"
59
+ f"validation={backend_generation.validation_status}"
60
+ )
61
 
62
  if beat_type == "finale":
63
  session.beat_index = session.max_beats
 
75
  while session.beat_index < session.max_beats:
76
  run_one_beat(session)
77
  return session
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
puppet_theater/models.py CHANGED
@@ -1,5 +1,31 @@
1
  from dataclasses import dataclass, field
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
  @dataclass
5
  class Actor:
@@ -37,3 +63,4 @@ class TheaterSession:
37
  director_log: list[str] = field(default_factory=list)
38
  trace_events: list[str] = field(default_factory=list)
39
  finale_requested: bool = False
 
 
1
  from dataclasses import dataclass, field
2
 
3
+ from pydantic import BaseModel, Field, field_validator
4
+
5
+
6
+ class ActorResponse(BaseModel):
7
+ line: str = Field(description="Short, stage-ready puppet dialogue.")
8
+ emotion: str
9
+ gesture: str
10
+ stage_effect: str
11
+ tool_request: str | None = None
12
+
13
+ @field_validator("line", "emotion", "gesture", "stage_effect")
14
+ @classmethod
15
+ def require_text(cls, value: str) -> str:
16
+ cleaned = " ".join(value.strip().split())
17
+ if not cleaned:
18
+ raise ValueError("field must not be empty")
19
+ return cleaned
20
+
21
+ @field_validator("tool_request")
22
+ @classmethod
23
+ def clean_optional_text(cls, value: str | None) -> str | None:
24
+ if value is None:
25
+ return None
26
+ cleaned = " ".join(value.strip().split())
27
+ return cleaned or None
28
+
29
 
30
  @dataclass
31
  class Actor:
 
63
  director_log: list[str] = field(default_factory=list)
64
  trace_events: list[str] = field(default_factory=list)
65
  finale_requested: bool = False
66
+ backend_name: str = "deterministic"
puppet_theater/prompts.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ACTOR_LINE_PROMPT = """You are writing one short puppet line for AI Puppet Theater.
2
+
3
+ Return only JSON with these fields:
4
+ - line: a short, stage-ready spoken line
5
+ - emotion: one concise emotion label
6
+ - gesture: one concise stage gesture label
7
+ - stage_effect: one concise stage effect label
8
+ - tool_request: null or one concise theatrical tool request
9
+
10
+ Show title: {show_title}
11
+ Premise: {premise}
12
+ Setting: {setting}
13
+ Beat type: {beat_type}
14
+ Speaker: {speaker_name}
15
+ Speaker goal: {speaker_goal}
16
+ Speaker style: {speaker_style}
17
+ Audience action: {audience_action}
18
+ Latest prop: {latest_prop}
19
+ """
20
+
21
+
22
+ DIRECTOR_DECISION_PROMPT = """You are the Director for AI Puppet Theater.
23
+
24
+ Choose the next beat for a short puppet scene. Keep the scene tight, rotate speakers,
25
+ respect finale requests, and avoid exposing actor secrets unless the beat calls for it.
26
+
27
+ Return only JSON with these fields:
28
+ - beat_type
29
+ - speaker_name
30
+ - reason_summary
31
+ - should_end
32
+
33
+ Show title: {show_title}
34
+ Premise: {premise}
35
+ Current beat: {beat_index}
36
+ Maximum beats: {max_beats}
37
+ Available actors: {actor_names}
38
+ Recent transcript: {recent_transcript}
39
+ Audience action: {audience_action}
40
+ Finale requested: {finale_requested}
41
+ """
42
+
43
+
44
+ CASTING_PROMPT = """You are casting a tiny improv puppet show.
45
+
46
+ Create a small ensemble of puppet actors that can perform the premise quickly.
47
+ Return only JSON with an actors array. Each actor must include:
48
+ - name
49
+ - avatar
50
+ - goal
51
+ - secret
52
+ - speaking_style
53
+ - tools
54
+
55
+ Premise: {premise}
56
+ Preferred actor count: {actor_count}
57
+ """
puppet_theater/session.py CHANGED
@@ -61,6 +61,7 @@ def create_show_from_premise(premise: str) -> TheaterSession:
61
 
62
  director_log = [
63
  "Director created a deterministic six-beat show plan.",
 
64
  f"Setting selected: {setting}.",
65
  "Three puppet actors are waiting for the first beat.",
66
  ]
@@ -68,6 +69,7 @@ def create_show_from_premise(premise: str) -> TheaterSession:
68
  "show_created",
69
  "actors_created:3",
70
  "director_plan_created",
 
71
  ]
72
 
73
  return TheaterSession(
@@ -84,4 +86,5 @@ def create_show_from_premise(premise: str) -> TheaterSession:
84
  director_log=director_log,
85
  trace_events=trace_events,
86
  finale_requested=False,
 
87
  )
 
61
 
62
  director_log = [
63
  "Director created a deterministic six-beat show plan.",
64
+ "Active backend: deterministic.",
65
  f"Setting selected: {setting}.",
66
  "Three puppet actors are waiting for the first beat.",
67
  ]
 
69
  "show_created",
70
  "actors_created:3",
71
  "director_plan_created",
72
+ "backend_active:deterministic",
73
  ]
74
 
75
  return TheaterSession(
 
86
  director_log=director_log,
87
  trace_events=trace_events,
88
  finale_requested=False,
89
+ backend_name="deterministic",
90
  )