File size: 11,902 Bytes
ed151e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# Learning path: AI Puppet Theater

This guide walks a new contributor from Python and LLM basics through this repository so you can **trace one user action end-to-end** and **change the engine or UI safely**.

Companion docs: [AGENTS.md](../AGENTS.md) (agent and product rules), [README.md](../README.md) (features and architecture images under `assets/`).

---

## Phase 0: Environment and vocabulary

### Run the app

```bash
uv sync
uv run python app.py
# or, for reload while editing:
uv run gradio app.py
```

Open `http://127.0.0.1:7860`. Create a show, use **Run One Beat**, throw a prop, open **Behind the Curtain**. You will see the same concepts the code names: session, beats, director log, trace.

### Python building blocks used here

- **Modules and imports** β€” `app.py` imports a small public API from `puppet_theater` (see [`puppet_theater/__init__.py`](../puppet_theater/__init__.py)).
- **`dataclasses`** β€” mutable β€œworld state”: [`Actor`](../puppet_theater/models.py), [`Beat`](../puppet_theater/models.py), [`TheaterSession`](../puppet_theater/models.py).
- **Pydantic `BaseModel`** β€” validated β€œmessages” often produced from LLM text: [`DirectorDecision`](../puppet_theater/models.py), [`ActorResponse`](../puppet_theater/models.py), [`ToolRequest`](../puppet_theater/models.py).
- **Type hints** β€” e.g. `str | None`, `list[Beat]`.
- **In-place session updates** β€” most functions take `TheaterSession`, mutate it, and return the same object (not a copy).

### LLM vocabulary in this project

- **Prompt** β€” text sent to a model; see [`puppet_theater/prompts.py`](../puppet_theater/prompts.py).
- **Structured output** β€” the app expects JSON-shaped answers, parses them, and validates with Pydantic. Bad JSON or timeouts β†’ **fallback** to deterministic templates so the Space always runs.

---

## Phase 1: The nouns of the show (`models.py`)

Read [`puppet_theater/models.py`](../puppet_theater/models.py) end-to-end.

### TheaterSession vs Beat

| | **`TheaterSession`** | **`Beat`** |
|---|----------------------|------------|
| **Role** | The entire live show: cast, configuration, pacing budget, transcript history, logs, trace. | One row of dialogue / stage moment in the transcript. |
| **Mutability** | Updated every audience action and every beat (beat counter, actors’ mood, etc.). | Created once per beat and appended to `session.transcript`; treat as immutable after append. |
| **Contains** | `actors`, `beat_index`, `min_beats` / `target_beats` / `max_beats`, `transcript`, `latest_prop`, `director_log`, `trace_events`, `backend_name`, `director_mode`, … | `speaker`, `intent`, `line`, `emotion`, `gesture`, `stage_effect`, optional `memory_update` and `tool_request`. |

**Checkpoint:** Explain aloud: β€œThe session is the notebook; each beat is one line the puppets spoke.”

### Pydantic vs dataclass in this file

- **Pydantic** β€” strict validation for fields that might come from model JSON (length limits, non-empty strings).
- **Dataclass** β€” convenient structured bags for runtime state the code controls directly.

Public exports are listed in [`puppet_theater/__init__.py`](../puppet_theater/__init__.py).

---

## Phase 2: Creating a show and audience actions

1. **[`puppet_theater/session.py`](../puppet_theater/session.py)** β€” [`create_show_from_premise`](../puppet_theater/session.py) builds `show_title`, `setting`, the default three [`Actor`](../puppet_theater/models.py) instances, beat budget from [`resolve_show_length`](../puppet_theater/session.py), copies backend/director/temperature settings onto the session, seeds `director_log`, and records trace events (`show_created`, `actors_created`, `director_plan_created`).

2. **[`puppet_theater/actions.py`](../puppet_theater/actions.py)** β€” Audience mutators:
   - [`throw_prop`](../puppet_theater/actions.py) β€” appends to `session.props`, sets `latest_prop`, updates `latest_audience_action` and `director_log`, traces `audience_action`.
   - [`summon_actor`](../puppet_theater/actions.py) β€” appends a new `Actor` (cap `MAX_ACTORS`), or logs a skip if full.
   - [`request_finale`](../puppet_theater/actions.py) β€” sets `finale_requested` so the Director policy can force a finale.

**Checkpoint:** Trace β€œthrow rubber duck” from UI β†’ `throw_prop` β†’ `session.latest_prop` β†’ next Director beat may set `uses_prop=True` (see deterministic policy when `latest_prop` is set in [`DirectorPolicy.decide`](../puppet_theater/director.py)).

---

## Phase 3: One beat β€” the main pipeline (`director.py`)

Read [`puppet_theater/director.py`](../puppet_theater/director.py) with focus on:

- [`story_progress`](../puppet_theater/director.py) / [`story_phase`](../puppet_theater/director.py) β€” pacing helpers from `beat_index` and `target_beats`.
- [`choose_director_decision`](../puppet_theater/director.py) β€” branches on `session.director_mode`: `hf_api` β†’ `HFAPIDirectorPolicy`, `openbmb` β†’ `OpenBMBDirectorPolicy`, else [`DirectorPolicy`](../puppet_theater/director.py) (deterministic).
- [`run_one_beat`](../puppet_theater/director.py) β€” orchestrates one turn:
  1. Skip if `beat_index >= max_beats`.
  2. Call `choose_director_decision` β†’ [`DirectorDecision`](../puppet_theater/models.py).
  3. Resolve speaker; optionally attach `latest_prop` if decision uses prop.
  4. Append director log lines and `add_trace_event(..., "director_decision", ...)`.
  5. [`generate_actor_response`](../puppet_theater/backends.py) (from `backends`) β†’ [`ActorResponse`](../puppet_theater/models.py).
  6. Increment `beat_index`, build [`Beat`](../puppet_theater/models.py), append to `transcript`.
  7. [`apply_actor_state_update`](../puppet_theater/director.py) β€” mood, memory, secret status, held props.
  8. [`run_actor_tool_request`](../puppet_theater/tools.py) β€” optional tool side effects; may adjust `beat.stage_effect`.
  9. Clear `latest_prop` if consumed; more logs and trace (`beat_added`, `actor_response`, …).
  10. If finale: clamp `beat_index` to `max_beats`, set `finale_requested`, trace `scene_completed`.

- [`run_full_act`](../puppet_theater/director.py) β€” `while session.beat_index < session.max_beats: run_one_beat(session)`.

```mermaid
flowchart LR
  subgraph beat [run_one_beat]
    D[choose_director_decision]
    A[generate_actor_response]
    T[append Beat to transcript]
    S[apply_actor_state_update]
    U[run_actor_tool_request]
  end
  Session[TheaterSession]
  Session --> D
  D --> A
  A --> T
  T --> S
  S --> U
  U --> Session
```

**Checkpoint:** Without opening `app.py`, narrate one beat from Director decision through transcript line.

---

## Phase 4: Backends β€” where the LLM lives (`backends.py`)

Read [`puppet_theater/backends.py`](../puppet_theater/backends.py) with this map:

- **`ModelBackend`** β€” abstract [`generate_actor_response`](../puppet_theater/backends.py) per backend.
- **Implementations** β€” [`DeterministicBackend`](../puppet_theater/backends.py) (always works), [`OpenBMBTransformersBackend`](../puppet_theater/backends.py) (local Transformers; ZeroGPU hooks in [`puppet_theater/zerogpu.py`](../puppet_theater/zerogpu.py)), [`HFAPIBackend`](../puppet_theater/backends.py) (hosted inference).
- **[`generate_actor_response`](../puppet_theater/backends.py)** (module-level) β€” picks backend from `session.backend_name`, runs generation, [`parse_actor_output`](../puppet_theater/backends.py), optional repair, then deterministic fallback on failure (mirrors Director policies’ try/validate/fallback pattern).

**Checkpoint:** Why does the demo run without an HF token? Because `backend_name` and `director_mode` default to `deterministic`, which never calls the network.

Optional deep read: [`puppet_theater/prompts.py`](../puppet_theater/prompts.py).

---

## Phase 5: Tools and trace

### Tools ([`puppet_theater/tools.py`](../puppet_theater/tools.py))

- [`ALLOWED_TOOL_NAMES`](../puppet_theater/tools.py) β€” allowlist: `inspect_prop`, `consult_stage_oracle`, `change_lighting`.
- [`validate_tool_request`](../puppet_theater/tools.py) β€” Pydantic + argument shape checks.
- [`run_actor_tool_request`](../puppet_theater/tools.py) β€” traces `tool_requested` / `tool_ignored` / `tool_executed` / `tool_result`, updates `session.latest_tool_result` and `recent_tool_results`.

### Trace ([`puppet_theater/trace.py`](../puppet_theater/trace.py))

- [`add_trace_event`](../puppet_theater/trace.py) β€” appends sanitized dicts to `session.trace_events` (no secrets; path stripping in [`sanitize_value`](../puppet_theater/trace.py)). Each event uses the key **`event_type`** (not `type`) for the event name string.
- [`export_trace`](../puppet_theater/trace.py) / [`render_trace_json`](../puppet_theater/trace.py) / [`write_trace_json_file`](../puppet_theater/trace.py) β€” used by the UI for download and display.

**Checkpoint:** If you add a new `event_type` in `add_trace_event`, you can find it in the trace JSON and in Gradio components wired to `render_trace`.

---

## Phase 6: Gradio UI wiring (`app.py`)

Do **not** read [`app.py`](../app.py) top to bottom (it is large). Use this map.

### `gr.Blocks` handlers β†’ Python wrappers β†’ `puppet_theater`

| UI control | Handler in `app.py` | Calls into `puppet_theater` |
|------------|---------------------|-----------------------------|
| Create show | [`create_show`](../app.py) (~2607) | [`create_show_from_premise`](../puppet_theater/session.py) |
| Run one beat | [`advance_one_beat`](../app.py) (~2691) | [`run_one_beat`](../puppet_theater/director.py) (after [`apply_backend_selection`](../app.py)) |
| Run full act | [`advance_full_act`](../app.py) (~2720) | Loop / yield calling [`run_one_beat`](../puppet_theater/director.py) |
| Throw prop | [`throw_audience_prop`](../app.py) (~2790) | [`throw_prop`](../puppet_theater/actions.py) |
| Summon actor | [`summon_audience_actor`](../app.py) (~2820) | [`summon_actor`](../puppet_theater/actions.py) |
| Request finale | [`request_audience_finale`](../app.py) (~2850) | [`request_finale`](../puppet_theater/actions.py) |
| Warm up OpenBMB | [`warm_up_backend`](../app.py) (~2879) | [`warm_up_openbmb`](../puppet_theater/backends.py) |
| Reset | [`reset_show`](../app.py) (~2663) | Clears state (no package call) |

Event wiring lives near **`create_button.click`**, **`run_one_button.click`**, etc. (~3119–3290 in `app.py` at time of writing; line numbers may shift).

Shared presentation pipeline: [`render_outputs`](../app.py) (~2590) refreshes stage HTML, transcript, TTS payload, director log, trace, backend panel.

**Checkpoint:** Name the handler that runs when **Run One Beat** is clicked and the single core engine function it invokes.

---

## Phase 7: Verify and contribute

### Commands

```bash
uv run python -m py_compile app.py puppet_theater/*.py
uv run pytest
```

### Tests to read first

- [`tests/test_director.py`](../tests/test_director.py) β€” Director and beat behavior.
- [`tests/conftest.py`](../tests/conftest.py) β€” shared fixtures.

### First contribution ideas

- Copyedit a `director_log` string.
- Extend deterministic dialogue or stage effects in [`backends.py`](../puppet_theater/backends.py) / [`director.py`](../puppet_theater/director.py).
- Add a trace field or a test for a prop-heavy beat.
- Document an env var in [README.md](../README.md).

### Optional advanced track

[`finetune/`](../finetune/) β€” LoRA training and eval scripts; separate from the live Gradio beat loop.

---

## Suggested schedule

| Week | Focus |
|------|--------|
| 1 | Phases 0–2 + UI play |
| 2 | Phase 3 (`run_one_beat`) until you can draw the diagram from memory |
| 3 | Phases 4–5 |
| 4 | Phase 6–7 + a small PR |

---

## External references

- Gradio **Blocks** and `.click()` inputs/outputs.
- Pydantic v2 **validators** (`field_validator`).
- Hugging Face **Spaces** environment variables (see README Configuration).