diff --git a/docs/superpowers/specs/2026-03-24-conversational-branching-design.md b/docs/superpowers/specs/2026-03-24-conversational-branching-design.md new file mode 100644 index 00000000..898288dc --- /dev/null +++ b/docs/superpowers/specs/2026-03-24-conversational-branching-design.md @@ -0,0 +1,498 @@ +# Conversational Branching — Design Spec + +> **Date:** 2026-03-24 +> **Status:** Draft +> **Branch:** `feat/conversational-branching` (to be created) +> **Source:** `docs/ConversationalBranching_DataModel_Spec.docx` (original spec) + +--- + +## Executive Summary + +Conversational Branching transforms FlowPilot from a linear AI chat into a branching troubleshooting workspace. Engineers explore multiple diagnostic hypotheses as first-class branches, with full AI context awareness across all paths. + +The design is **additive and removable** — all branching state lives in new tables and nullable columns. If the feature is pulled, drop the tables, remove the columns, and the app works exactly as it does today. + +--- + +## Design Principles + +1. **Extend, don't replace.** Reuse existing tables, services, and infrastructure. No parallel code paths. +2. **Removable.** Every branching artifact (tables, columns, services, components) can be deleted without breaking existing functionality. +3. **Dual-write for backward compat.** New handoff system writes to both `session_handoffs` table AND existing `escalation_package`/`escalated_to_id` fields until the old queue UI is fully migrated. +4. **Branching services never touch the Anthropic SDK.** They assemble context and call `_call_ai` from `assistant_chat_service.py` — the same function `unified_chat_service` already uses. + +--- + +## What This Enables + +1. **Branch Map sidebar** — live tree visualization of all diagnostic paths, with status badges (active, dead end, solved, untried) and click-to-switch. +2. **Fork Cards** — in-chat decision points where FlowPilot suggests multiple hypotheses, each becoming a branch. +3. **Cross-branch AI context** — when exploring Branch 3, FlowPilot knows what was tried on Branches 1 and 2. +4. **Branch revival** — new evidence from one branch can reopen a previously dead-end branch. +5. **Unified handoff (park / escalate)** — single snapshot mechanism with intent toggle. +6. **Three-output resolution package** — PSA ticket notes, KB article draft, and client-facing summary. + +--- + +## Reuse Map + +### What branching reuses (NOT duplicated): + +| Capability | Existing Source | How Branching Uses It | +|---|---|---| +| LLM calls | `_call_ai` in `assistant_chat_service.py` | All branching LLM calls go through this | +| Image content blocks | `_call_ai` multimodal support | Current branch images included in messages via existing format | +| Token counting | `AISessionStep.input_tokens/output_tokens` | Unchanged — tokens tracked per step per branch | +| RAG search | `rag_service.py` | Branch messages use same RAG pipeline | +| Image upload + S3 storage | `file_uploads` table + `storage_service.py` | Extended with 5 new columns, no new table | +| PSA push | `psa_documentation_service.py` | Resolution outputs and handoff notes push through existing service | +| Escalation package | `_build_escalation_package_enhanced()` | HandoffManager reuses this for snapshot generation | +| Escalation queue | Existing `ai_sessions` query + frontend | Dual-write keeps old queue working | +| Session lifecycle | `flowpilot_engine.py` resolve/escalate/pause | Extended, not replaced | + +### What's new: + +| Capability | Notes | +|---|---| +| `session_branches` table | Core branching entity | +| `fork_points` table | Decision point metadata (kept separate for expandability) | +| `session_handoffs` table | Unified park/escalate with history | +| `session_resolution_outputs` table | Three independent output deliverables | +| `BranchManager` service | Branch lifecycle CRUD + context summary generation | +| `BranchAwarePromptBuilder` service | Cross-branch context assembly | +| `HandoffManager` service | Snapshot, assessment, claim, PSA push | +| `ResolutionOutputGenerator` service | PSA notes, KB article, client summary generation | +| AI description pipeline | Async `ai_description` generation on every file upload | + +--- + +## Data Model + +### Modified existing tables + +**`ai_sessions` — add columns:** + +| Column | Type | Default | Notes | +|---|---|---|---| +| `is_branching` | BOOLEAN | FALSE | Whether branching is active | +| `active_branch_id` | UUID FK NULLABLE | NULL | Currently viewed branch | +| `handoff_count` | INTEGER | 0 | Times handed off | +| `total_active_seconds` | INTEGER | 0 | Cumulative active time | +| `total_parked_seconds` | INTEGER | 0 | Cumulative parked time | + +**`ai_session_steps` — add columns:** + +| Column | Type | Default | Notes | +|---|---|---|---| +| `branch_id` | UUID FK NULLABLE | NULL | NULL = pre-branching/root | +| `is_fork_point` | BOOLEAN | FALSE | Whether this step triggered a fork | +| `fork_point_id` | UUID FK NULLABLE | NULL | References `fork_points.id` | + +**`file_uploads` — add columns:** + +| Column | Type | Default | Notes | +|---|---|---|---| +| `ai_description` | TEXT NULLABLE | NULL | AI-generated one-sentence description | +| `extracted_content` | TEXT NULLABLE | NULL | Extracted text from logs/configs | +| `content_summary` | TEXT NULLABLE | NULL | AI summary for long files | +| `uploaded_on_branch_id` | UUID FK NULLABLE | NULL | Which branch the file was uploaded from | +| `uploaded_at_step_id` | UUID FK NULLABLE | NULL | Which step triggered the upload | + +All columns nullable. Existing rows unaffected. `ai_description` is always generated on upload (not just branching sessions) — useful for search, exports, PSA notes, Knowledge Flywheel. + +Also add `'fork'` to `ai_session_steps.step_type` check constraint. + +### New tables + +**`session_branches`** + +| Column | Type | Notes | +|---|---|---| +| `id` | UUID PK | | +| `session_id` | UUID FK → `ai_sessions.id` CASCADE | | +| `parent_branch_id` | UUID FK → self, NULLABLE | NULL = root branch | +| `fork_point_step_id` | UUID FK → `ai_session_steps.id`, NULLABLE | Step where this branch forked | +| `branch_order` | INTEGER | Display order among siblings (1-based) | +| `label` | VARCHAR(200) | "Network connectivity", "Print spooler service" | +| `status` | VARCHAR(20) | `active`, `dead_end`, `solved`, `untried`, `revived` | +| `status_reason` | TEXT NULLABLE | AI-generated reason for status | +| `status_changed_at` | TIMESTAMP NULLABLE | | +| `status_changed_by` | UUID FK NULLABLE | | +| `conversation_messages` | JSONB | LLM message history scoped to this branch | +| `context_summary` | JSONB | `{tried: [], concluded: str, artifacts: []}` | +| `evidence_from_branch_id` | UUID FK NULLABLE | If revived, evidence source | +| `evidence_description` | TEXT NULLABLE | What triggered revival | +| `created_at` | TIMESTAMP | | +| `updated_at` | TIMESTAMP | | + +Indexes: `session_id`, `parent_branch_id`, `(session_id, status)`, `(session_id, branch_order)`. +Check constraints: `status IN (...)`, `branch_order > 0`. + +**`fork_points`** + +| Column | Type | Notes | +|---|---|---| +| `id` | UUID PK | | +| `session_id` | UUID FK → `ai_sessions.id` | | +| `parent_branch_id` | UUID FK → `session_branches.id` | Branch this fork occurs in | +| `trigger_step_id` | UUID FK → `ai_session_steps.id`, NULLABLE | Step that triggered fork | +| `fork_reason` | TEXT | AI explanation | +| `options` | JSONB | `[{label, description, branch_id, status}]` | +| `created_at` | TIMESTAMP | | + +**`session_handoffs`** + +| Column | Type | Notes | +|---|---|---| +| `id` | UUID PK | | +| `session_id` | UUID FK → `ai_sessions.id` | | +| `handed_off_by` | UUID FK → `users.id` | | +| `intent` | VARCHAR(20) | `park` or `escalate` | +| `source_branch_id` | UUID FK NULLABLE | Active branch at handoff | +| `snapshot` | JSONB | Branch map, status, next step, waiting on, watch out | +| `ai_assessment` | TEXT NULLABLE | Diagnostic assessment (escalate only) | +| `ai_assessment_data` | JSONB NULLABLE | `{likely_cause, suggested_steps, confidence}` | +| `artifacts` | JSONB NULLABLE | `[{name, type, reference}]` | +| `engineer_notes` | TEXT NULLABLE | | +| `priority` | VARCHAR(20) | `normal` or `elevated` | +| `claimed_by` | UUID FK NULLABLE | | +| `claimed_at` | TIMESTAMP NULLABLE | | +| `psa_note_pushed` | BOOLEAN DEFAULT FALSE | | +| `psa_note_id` | VARCHAR(100) NULLABLE | | +| `notification_sent` | BOOLEAN DEFAULT FALSE | | +| `created_at` | TIMESTAMP | | + +Check constraints: `intent IN ('park', 'escalate')`, `priority IN ('normal', 'elevated')`. +Dual-write: on create, also populates `ai_sessions.escalation_package` and `escalated_to_id`. + +**`session_resolution_outputs`** + +| Column | Type | Notes | +|---|---|---| +| `id` | UUID PK | | +| `session_id` | UUID FK → `ai_sessions.id` | | +| `output_type` | VARCHAR(30) | `psa_ticket_notes`, `knowledge_base`, `client_summary` | +| `generated_content` | TEXT | AI-generated output | +| `structured_data` | JSONB NULLABLE | For KB: `{symptoms, root_cause, steps, tags}` | +| `edited_content` | TEXT NULLABLE | Engineer's edited version | +| `status` | VARCHAR(20) | `draft`, `approved`, `pushed`, `rejected` | +| `pushed_to` | VARCHAR(50) NULLABLE | `psa`, `kb_library`, `clipboard`, `email` | +| `pushed_at` | TIMESTAMP NULLABLE | | +| `pushed_reference` | VARCHAR(200) NULLABLE | External ID after push | +| `generated_by_model` | VARCHAR(50) | | +| `created_at` | TIMESTAMP | | +| `updated_at` | TIMESTAMP | | + +Constraints: `UNIQUE(session_id, output_type)`, check constraints on `output_type` and `status`. + +### Entity Relationships + +``` +AISession 1──* SessionBranch (session has many branches) +AISession 1──* AISessionStep (unchanged) +AISession 1──* SessionHandoff (can be handed off multiple times) +AISession 1──3 SessionResolutionOutput (one per output type) +SessionBranch 1──* AISessionStep (branch owns steps via branch_id) +SessionBranch 1──* SessionBranch (parent → children, self-referential) +SessionBranch 1──* ForkPoint (branch contains fork points) +ForkPoint 1──* SessionBranch (each option becomes a branch) +SessionHandoff *──1 User (handed_off_by, claimed_by) +FileUpload *──1 SessionBranch (optional, via uploaded_on_branch_id) +``` + +--- + +## Service Layer + +### Integration Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ LAYER 3: BRANCHING SERVICES (NEW) │ +│ │ +│ BranchManager — Fork, switch, mark status, revive, │ +│ tree query, context summary gen │ +│ │ +│ BranchAwarePromptBuilder — Assembles system prompt + │ +│ messages + images with cross- │ +│ branch context, returns dict │ +│ for _call_ai │ +│ │ +│ HandoffManager — Snapshot, AI assessment, claim, │ +│ briefing, PSA push, queue query │ +│ │ +│ ResolutionOutputGen — PSA notes, KB article, client │ +│ summary, push to destination │ +└────────────────────────────┬────────────────────────────────┘ + │ calls _call_ai directly + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ LAYER 2: EXISTING CHAT INFRASTRUCTURE │ +│ │ +│ _call_ai / _call_anthropic_cached │ +│ — Anthropic API, prompt caching, image blocks, MCP │ +│ │ +│ unified_chat_service — Session-level chat (linear) │ +│ flowpilot_engine — Step lifecycle, resolve, escalate │ +│ rag_service — Flow library search │ +│ psa_documentation_service — ConnectWise note push │ +└────────────────────────────┬────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ LAYER 1: INFRASTRUCTURE (EXISTS) │ +│ │ +│ Railway Object Storage — S3 bucket, presigned URLs │ +│ PostgreSQL — All data │ +│ Anthropic API — Claude with Vision │ +└─────────────────────────────────────────────────────────────┘ +``` + +### `services/branch_manager.py` + +Branch lifecycle management. Pure data operations + one LLM call pattern (context summary). + +| Method | What it does | LLM call? | +|---|---|---| +| `create_root_branch(session_id)` | Creates root branch, sets `is_branching=True`, copies `session.conversation_messages` into root branch. Session-level field kept as pre-branching snapshot. | No | +| `create_fork(session_id, parent_branch_id, trigger_step_id, fork_reason, options[])` | Creates `ForkPoint` + N `SessionBranch` rows. Sets `is_fork_point=True` on trigger step. Unexplored options get status `untried`. | No | +| `switch_branch(session_id, target_branch_id)` | Updates `session.active_branch_id`. Returns branch with context. | No | +| `mark_branch_status(branch_id, status, reason)` | Updates status. Generates `context_summary` via `_call_ai`. | Yes — summary | +| `revive_branch(branch_id, evidence_from_branch_id, evidence_description)` | Sets status `revived`, records evidence source, prepends revival context to branch messages. | No | +| `get_branch_tree(session_id)` | Full tree with status, labels, step counts, summaries. | No | +| `build_cross_branch_context(branch_id)` | Reads `context_summary` from all sibling branches, returns formatted text. | No | + +### `services/branch_aware_prompt_builder.py` + +Pure function — takes data, returns assembled prompt components. No DB access, no LLM calls. + +**Single method:** `build(branch, sibling_summaries, session_context, attachments, token_budget)` + +Returns: `{system_prompt: str, history: list[dict], new_message: str, images: list[dict]}` + +Preserves `_call_ai`'s cache breakpoint behavior by separating history from new message. + +**Assembly order:** +1. Session context (~2,000 tokens) — problem summary, domain, client info, PSA data +2. Cross-branch summaries (~3,000 token cap) — prioritized: active > untried > revived > dead_end +3. Revival context — if branch was revived, prepend evidence +4. Attachment descriptions (~1,000 tokens) — `ai_description` from other branches' uploads +5. Branch messages (remaining budget) — last 10-15 turns verbatim, older summarized +6. Token budget enforcement — compress: old messages → dead-end summaries → file content → never drop system prompt, last 5 messages, branch status map + +### `services/handoff_manager.py` + +Unified park/escalate with dual-write backward compatibility. + +| Method | What it does | LLM call? | +|---|---|---| +| `create_handoff(session_id, intent, engineer_notes, user_id)` | Creates `SessionHandoff`. Calls `generate_snapshot()`. If escalate, calls `generate_ai_assessment()`. Dual-writes to `session.escalation_package` + `escalated_to_id`. | Escalate only | +| `generate_snapshot(session_id)` | Serializes branch tree into snapshot JSONB. Reuses `_build_escalation_package_enhanced()` for steps-tried data. | No | +| `generate_ai_assessment(session_id)` | Full session + branch context → diagnostic assessment. | Yes | +| `generate_briefing(handoff_id, claiming_user_id)` | Natural-language handoff summary for claiming engineer. | Yes | +| `claim_session(handoff_id, claiming_user_id)` | Updates `claimed_by/at`, sets session `active`. Dual-writes `escalation_package`. | No | +| `push_to_psa(handoff_id)` | Calls existing `psa_documentation_service`. | No | +| `get_queue(team_id, filters)` | DB query for parked + escalated sessions. | No | + +### `services/resolution_output_generator.py` + +Three LLM calls on resolve, each through `_call_ai`. + +| Method | What it does | LLM call? | +|---|---|---| +| `generate_all(session_id)` | Creates 3 `SessionResolutionOutput` rows. | 3 calls | +| `generate_psa_notes(session_id)` | Structured ticket notes with full branch history. | Yes | +| `generate_kb_article(session_id)` | KB draft — dead-end branches become "rule out first" guidance. `structured_data` has `{symptoms, root_cause, steps, tags}`. | Yes | +| `generate_client_summary(session_id)` | Non-technical summary for end user. | Yes | +| `push_output(output_id, destination)` | Routes: `psa` → `psa_documentation_service`, `kb_library` → flow/step library, `clipboard` → returns content. | No | + +### AI Description Pipeline (upload extension) + +Not a new service — extends the existing upload endpoint in `uploads.py`: + +1. Upload completes, response returned immediately. +2. Background task (via `asyncio.create_task`) calls `_call_ai` with image + prompt: "Describe this in one sentence for a troubleshooting context log." +3. Result written to `file_uploads.ai_description`. +4. For text files: extract content directly, call `_call_ai` for summary if >2,000 tokens. +5. Always runs (not just branching sessions) — useful for search, exports, PSA notes. + +--- + +## API Endpoints + +All under existing `/api` prefix, JWT auth, team-scoped. + +### Branch Management + +| Method | Endpoint | Description | +|---|---|---| +| GET | `/ai-sessions/{id}/branches` | List all branches (tree structure) | +| POST | `/ai-sessions/{id}/branches/fork` | Create fork point with N branches | +| PATCH | `/ai-sessions/{id}/branches/{bid}` | Update branch status | +| POST | `/ai-sessions/{id}/branches/{bid}/switch` | Switch active branch | +| POST | `/ai-sessions/{id}/branches/{bid}/revive` | Revive dead-end with evidence | +| POST | `/ai-sessions/{id}/branches/{bid}/message` | Send message on a specific branch | + +### Handoff (Park / Escalate) + +| Method | Endpoint | Description | +|---|---|---| +| POST | `/ai-sessions/{id}/handoff` | Create handoff (park or escalate) | +| GET | `/ai-sessions/{id}/handoffs` | Handoff history for session | +| POST | `/ai-sessions/{id}/handoffs/{hid}/claim` | Claim a handed-off session | +| GET | `/ai-sessions/queue` | Team queue (parked + escalated) | + +### Resolution Outputs + +| Method | Endpoint | Description | +|---|---|---| +| POST | `/ai-sessions/{id}/resolve` | Resolve + auto-generate 3 outputs | +| GET | `/ai-sessions/{id}/outputs` | Get all resolution outputs | +| PATCH | `/ai-sessions/{id}/outputs/{oid}` | Edit output before pushing | +| POST | `/ai-sessions/{id}/outputs/{oid}/push` | Push to destination | + +Note: endpoints nest under `/ai-sessions` (not `/api/v1/sessions` as the original spec proposed) to match existing routing conventions. + +--- + +## Token Budget Strategy + +### Budget Allocation + +| Context Layer | Budget | Strategy | +|---|---|---| +| System prompt + session context | ~2,000 tokens | Fixed | +| Cross-branch summaries | ~3,000 tokens | Scales with branch count. Each summary ~200-500 tokens. Cap at 3,000. | +| Current branch messages | Remaining budget | Last 10-15 turns verbatim. Older summarized. | +| Attachment descriptions | ~1,000 tokens | Included in cross-branch summaries | + +### Graceful Degradation (in order) + +1. Summarize old current-branch messages (keep last 8-10 verbatim) +2. Trim cross-branch summaries (dead-end → one sentence) +3. Drop file content, keep descriptions +4. **Never drop:** system prompt, problem summary, last 5 messages, branch status map + +### Branch Limits by Plan + +| Plan | Max Branches | Rationale | +|---|---|---| +| Free | 2 | Experience branching, limit cost | +| Pro | 5 | Covers most scenarios | +| Team | 10 | Complex multi-path issues | +| Enterprise | Unlimited | | + +--- + +## Frontend Components + +| Component | Location | Description | +|---|---|---| +| `BranchMap` | `components/session/BranchMap.tsx` | Sidebar tree visualization with status badges | +| `BranchNode` | `components/session/BranchNode.tsx` | Individual node in branch map | +| `ForkCard` | `components/session/ForkCard.tsx` | In-chat fork decision point | +| `BranchTransitionBar` | `components/session/BranchTransitionBar.tsx` | Context bar on branch switch | +| `BranchRevivalCard` | `components/session/BranchRevivalCard.tsx` | Evidence card for revival | +| `HandoffModal` | `components/session/HandoffModal.tsx` | Unified park/escalate modal | +| `ResolutionOutputPanel` | `components/session/ResolutionOutputPanel.tsx` | Three-tab resolution view | +| `SessionQueuePage` | `pages/SessionQueuePage.tsx` | Team queue for parked/escalated | + +| Hook | Location | Description | +|---|---|---| +| `useBranching` | `hooks/useBranching.ts` | Branch state management | +| `useHandoff` | `hooks/useHandoff.ts` | Handoff flow state | +| `useResolutionOutputs` | `hooks/useResolutionOutputs.ts` | Resolution output state | + +| API Client | Location | Description | +|---|---|---| +| `branches.ts` | `api/branches.ts` | Branch API client | +| `handoffs.ts` | `api/handoffs.ts` | Handoff API client | +| `resolutions.ts` | `api/resolutions.ts` | Resolution output API client | + +--- + +## Backend File Locations + +``` +backend/app/ +├── models/ +│ ├── session_branch.py # SessionBranch model +│ ├── fork_point.py # ForkPoint model +│ ├── session_handoff.py # SessionHandoff model +│ └── session_resolution_output.py # SessionResolutionOutput model +├── schemas/ +│ ├── session_branch.py # Pydantic schemas +│ ├── session_handoff.py # Pydantic schemas +│ └── session_resolution.py # Pydantic schemas +├── services/ +│ ├── branch_manager.py # Branch lifecycle +│ ├── branch_aware_prompt_builder.py # Cross-branch context assembly +│ ├── handoff_manager.py # Park/escalate +│ └── resolution_output_generator.py # 3-output generator +├── api/endpoints/ +│ ├── session_branches.py # Branch API router +│ ├── session_handoffs.py # Handoff API router +│ └── session_resolutions.py # Resolution output API router +└── alembic/versions/ + └── xxx_add_branching_tables.py # Single migration +``` + +--- + +## Implementation Phases + +### Phase 1: Data Foundation (est. 2 days) +- Create 4 new models + Pydantic schemas +- Add columns to `ai_sessions`, `ai_session_steps`, `file_uploads` +- Single Alembic migration (all additive) +- Unit tests for model creation and relationships + +### Phase 2: Branch Engine (est. 2-3 days) +- `BranchManager` service +- `BranchAwarePromptBuilder` service +- Branch API endpoints +- Integration with FlowPilot engine (branch_id on step creation) +- Integration tests: create → fork → explore → dead-end → switch → verify cross-branch context + +### Phase 3: Handoff System (est. 1.5-2 days) +- `HandoffManager` service with dual-write +- Handoff API endpoints + queue endpoint +- PSA push integration (reuses existing service) +- Tests: park → verify snapshot. Escalate → verify assessment. Claim from queue. + +### Phase 4: Resolution Outputs (est. 1.5-2 days) +- `ResolutionOutputGenerator` service +- Resolution API endpoints + push logic +- KB article generation with dead-end branch "rule out" guidance +- Tests: resolve multi-branch session → verify 3 outputs → edit → push + +### Phase 5: AI Description Pipeline (est. 0.5 day) +- Extend upload endpoint with async `ai_description` generation +- Add `extracted_content` + `content_summary` for text files +- Tests: upload image → verify `ai_description` populated + +### Phase 6: Frontend (est. 3-4 days) +- Branch Map sidebar (tree vis, status badges, click-to-switch) +- Fork Card component (in-chat decision point) +- Branch transition animation +- Handoff modal (unified park/escalate) +- Session queue page +- Resolution output panel (three-tab view + edit + push) +- Branch revival UI + +--- + +## Removability Checklist + +If this feature is pulled: + +1. Drop tables: `session_branches`, `fork_points`, `session_handoffs`, `session_resolution_outputs` +2. Remove columns from `ai_sessions`: `is_branching`, `active_branch_id`, `handoff_count`, `total_active_seconds`, `total_parked_seconds` +3. Remove columns from `ai_session_steps`: `branch_id`, `is_fork_point`, `fork_point_id` +4. Remove columns from `file_uploads`: `uploaded_on_branch_id`, `uploaded_at_step_id` (keep `ai_description`, `extracted_content`, `content_summary` — useful independently) +5. Remove `'fork'` from step_type check constraint +6. Delete service files: `branch_manager.py`, `branch_aware_prompt_builder.py`, `handoff_manager.py`, `resolution_output_generator.py` +7. Delete endpoint files: `session_branches.py`, `session_handoffs.py`, `session_resolutions.py` +8. Delete frontend components/hooks/API clients listed above +9. Existing escalation flow, upload pipeline, chat service, PSA integration — **all untouched**