# Research Pipeline — Multi-Note Redesign > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Replace the single monolithic research note with a set of focused, topic-driven notes plus an index note that links them — making research output browsable, TTS-friendly, and well-organized. **Architecture:** Two new LLM calls (outline generation + N parallel section syntheses) replace the single large synthesis call. Public API unchanged — callers receive the index note. Fallback to single-note behavior on any outline failure. **Tech Stack:** Python/Quart backend, existing `research.py` service, asyncio.gather for parallelism. --- ## Problem The current pipeline synthesizes one note with a minimum of 2500 words and 6 sections. This creates: - Notes too large to read or listen to comfortably - No way to navigate directly to a specific sub-topic - TTS failures on long prose (8000-char route limit, unbounded sentence buffers) --- ## Pipeline Flow Public signature unchanged: ```python async def run_research_pipeline( topic: str, user_id: int, model: str, buf=None, project_id: int | None = None, ) -> Note: # returns the index note ``` Execution order: ``` 1. Generate sub-queries (unchanged) 2. Search + fetch sources (unchanged) 3. Generate topic outline (NEW — one LLM call → 3–7 section dicts) 4. Synthesize each section note (NEW — parallelized via asyncio.gather) 5. Create all section notes in DB (sequential, tagged ["research"], same project_id) 6. Create index note (NEW — links all sections) 7. Return index note ``` Status messages via `buf.append_event("status", ...)`: - `"Generating outline…"` - `"Writing: [Section Title]…"` (one per section, emitted before synthesis starts) - `"Saving [N] notes…"` No note content is streamed into chat. After the tool call resolves, the LLM writes a brief conversational summary citing the index note title and section count. --- ## Outline Generation New function: `_generate_outline(topic, sources, model) -> list[dict]` Sends all fetched sources to the model with a prompt requesting a JSON array: ```json [ {"title": "Quantum Entanglement: Mechanisms", "focus": "How entanglement works at the physical level"}, {"title": "Quantum Computing Hardware", "focus": "Ion traps, superconducting qubits, photonic approaches"} ] ``` **Prompt requirements:** - Produce 3–7 sections covering distinct aspects of the topic - Titles must work as standalone note titles (no "Overview" or "Introduction" generics) - No overlap between sections - `focus` is one sentence describing what this section should specifically cover **Guardrails:** - Fewer than 3 sections parsed → fall back to single-note synthesis - JSON parse failure → fall back to single-note synthesis - More than 8 sections → truncate to 8 **Model params:** `max_tokens=400, num_ctx=16384` (outline is short) --- ## Section Synthesis New function: `_synthesize_section(section_title, section_focus, sources, model) -> tuple[str, str]` Returns `(title, body_markdown)`. All sections receive all fetched sources. The `section_focus` field in the prompt directs the model to draw only what's relevant to that section's scope. **Prompt requirements:** - 300–600 words of substantive prose - Do NOT include a `# Title` heading (title is set separately) - End with a brief `## Sources` list of relevant URLs from the provided sources - Focus strictly on `section_focus` — ignore source material outside that scope **Model params:** `num_predict=2048, num_ctx=16384` (reduced from 8192 — sufficient for 600 words, prevents rambling) **Parallelism:** All section synthesis calls run via `asyncio.gather`. Wall-clock time stays close to a single synthesis call despite producing N notes. --- ## Note Creation and Index Note **Section notes:** - Tags: `["research"]` - `project_id`: same as passed to pipeline (or None) - Title: from outline `title` field - Created sequentially (avoids DB contention) **Index note:** - Tags: `["research", "research-index"]` - `project_id`: same as section notes - Title: `"Research: [topic]"` - Created last (after all section notes exist) **Index note body format:** ```markdown Research overview for **[topic]** — [YYYY-MM-DD] Generated from [N] web sources across [M] sections. ## Sections - **[Section 1 Title]** — [focus sentence] - **[Section 2 Title]** — [focus sentence] ... *Search for any section title to read it.* ``` The index note is what `run_research_pipeline` returns. The existing `research_topic` tool handler uses `note.id` and `note.title` — both remain valid with the index note. --- ## Error Handling | Scenario | Behaviour | |---|---| | Outline generation raises | Fall back to single-note synthesis (current behaviour) | | Outline JSON unparseable | Fall back to single-note synthesis | | Outline returns < 3 sections | Fall back to single-note synthesis | | Outline returns > 8 sections | Truncate to 8, continue | | A section synthesis raises | Log warning, skip that section; continue with remaining | | All section syntheses fail | Fall back to single-note synthesis | | A section note DB save fails | Log warning, skip from index; index note still created | | No sources fetched | Raise `ValueError` as today — unchanged | The fallback in every case is the current single-note pipeline. Research never silently produces nothing. --- ## What Is NOT Changing - Public function signature of `run_research_pipeline` - Sub-query generation (`_generate_sub_queries`) - SearXNG search and URL fetching - `_search_searxng`, `_search_searxng_images`, `fetch_url_content` - The `research_topic` tool definition and handler in `tools.py` - The `quick_capture` research path - Any frontend component