Spaces:
Running on Zero
Running on Zero
| # 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). | |