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).
|