# Technical Document: Choose Your Own Adventure **REVIEW FEEDBACK**: I have read my waythrough the document, and the bones are good. The suggested document centralised the code, and created monster/mamoth code. I have left feedback suggesting where I think we can split it out. It also advised the buildin of BFF endpoints, but until we design the UI we can't suggest that, so I have removed all reference. In general, let's push for a bit more separation between our layers, especially the way LLMs are communicated with over HTTP - the code that makes the call, the code that generates the prompts, and the code that requires the LLM result to do domain-y things are all separate roles, and would all change for different reasons. Let's not get too philosophical or atomic, let's stay pragmatic - but having massive methods or loads of methods on a class was definitely a code smell. This document translates the [design doc](./design-doc-choose-your-own-adventure.md) into a concrete, actionable set of changes for the API. It is intended to be handed directly to an LLM for implementation. --- ## Summary The Choose Your Own Adventure (CYOA) feature generates interactive story content using Claude. A learner creates an adventure with creative parameters (genre, setting, vibes, protagonist). The API generates the first story entry via the Anthropic API, then translates it via DeepL and produces TTS audio via Gemini. The learner reads the entry and chooses from four options at the end. Each choice triggers the next entry generation. Adventures run for a configurable number of entries (default 6). All LLM/translation/TTS work runs through the existing in-process worker queue (`app/worker.py`). --- ## State Machines ### Adventure.status ``` 'awaiting_first_entry' → first entry pipeline completes successfully → 'active' → first entry pipeline errors → 'error' 'active' → decision recorded AND entry_count reaches max_entry_count → 'complete' → subsequent entry pipeline errors → 'error' 'complete' (terminal) 'error' (terminal — user can see it errored; no auto-retry in MVP) ``` ### AdventureEntry.status ``` 'generating' → pipeline completes (story_text set, choices saved, translation saved, audio saved) → 'complete' → any pipeline step throws an unhandled exception → 'error' ``` --- ## Generation Pipeline The pipeline runs inside a worker task (enqueued via `app.worker.enqueue`). The worker processes tasks serially. ### First entry (entry_index = 0) 1. Create `AdventureEntry` row with `status='generating'`, `entry_index=0`, `generated_from_choice_id=None` 2. Build system prompt (see §AnthropicClient) 3. Build initial user message from adventure parameters 4. Call `AnthropicClient.generate_adventure_entry(...)` — returns `(raw_text, usage_dict)` 5. Parse raw_text into `(story_text, choices_raw, gamemaster_notes)` using `parse_llm_response()` 6. Parse `choices_raw` into list of `(label, text)` pairs 7. Update entry: set `story_text`, `gamemaster_notes`, `llm_data`, `status='complete'` 8. Create `AdventureEntryPossibleChoice` rows (one per option, typically 4) 9. Call `DeepLClient.translate(story_text, source_language)` → create `AdventureEntryTranslation` 10. Call `GeminiClient.generate_audio(story_text, voice)` → upload to S3 → create `AdventureEntryAudio` 11. Call `AnthropicClient.generate_adventure_title_and_description(story_text, ...)` → returns `(title, description)` 12. Update adventure: set `title`, `description`, `status='active'` On any exception during steps 2–12: set `entry.status='error'` and `adventure.status='error'`. ### Subsequent entries (entry_index > 0) Same pipeline except: - `generated_from_choice_id` is set to the chosen `AdventureEntryPossibleChoice.id` - Step 3: conversation history is passed to `generate_adventure_entry` (see §Conversation History) - If this entry completes AND `entry_index + 1 == adventure.max_entry_count`: set `adventure.status='complete'`; no choices are created for the final entry - Step 11–12 (title/description) is skipped — already set --- ## Conversation History Each call to `AnthropicClient.generate_adventure_entry` (after the first) must include the full conversation history so the model maintains narrative continuity. Reconstruct it from the stored entries: ``` messages = [ {"role": "user", "content": }, # constructed from adventure params {"role": "assistant", "content": }, {"role": "user", "content": "