# FlowPilot-First Pivot — Phase 1: AI Session Core > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Build the core AI-powered troubleshooting session — the new flagship product experience. An engineer starts a session by describing a problem (free text, screenshot, log paste), FlowPilot guides them through structured diagnosis with selectable options + free-text escape, and the session closes with auto-generated documentation. Flows are built as a byproduct of real resolutions. **Architecture:** New `AISession` and `AISessionStep` models. New `FlowPilotEngine` service orchestrating LLM calls with structured JSON output contracts. New `FlowMatchingEngine` service for semantic matching against existing flows. New frontend session UX with conversational layout. Reuses existing `ai_provider.py`, `embedding_service.py`, `rag_service.py`, and `copilot_service.py` patterns. **Tech Stack:** FastAPI, SQLAlchemy 2.0 (async), Anthropic Claude Sonnet 4 (structured JSON output), pgvector (flow matching), React, TypeScript, Tailwind CSS v4, shadcn/ui **Pivot architecture doc:** `docs/ResolutionFlow_Pivot_Architecture.docx` (in project root — the full strategic context) **Existing patterns to follow:** - Models: `app/models/copilot_conversation.py`, `app/models/script_template.py` - Services: `app/services/copilot_service.py`, `app/core/ai_provider.py` - API: `app/api/endpoints/copilot.py` (auth, quota, error handling patterns) - Frontend: `src/pages/FlowAssistPage.tsx`, `src/components/copilot/` --- ## Context: What This Pivot Changes ResolutionFlow currently requires engineers to build flows manually before getting value. This pivot makes FlowPilot the primary interface — engineers bring a problem, FlowPilot guides diagnosis, and flows get built organically from real resolutions. **What stays:** Session runner UX (promoted to core), flow editor (repurposed for curation), script generator (elevated to in-session action), PSA integration (elevated to primary intake channel), all existing models and services. **What's new in Phase 1:** AI Session models, FlowPilot Engine service, Flow Matching Engine v1, new session intake + conversational diagnosis frontend, resolve/escalate endpoints with auto-documentation. --- ## Slice 1: Database Models & Migration ### Task 1: Create AISession model **Files:** - Create: `backend/app/models/ai_session.py` ```python """AI-powered troubleshooting session model. Represents a complete FlowPilot interaction from intake to resolution/escalation. This is the central entity of the FlowPilot-First pivot. """ import uuid from datetime import datetime, timezone from typing import Optional, Any, TYPE_CHECKING from sqlalchemy import String, Text, DateTime, ForeignKey, Boolean, Integer, Float, CheckConstraint import sqlalchemy as sa from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.dialects.postgresql import UUID, JSONB from app.core.database import Base if TYPE_CHECKING: from app.models.user import User from app.models.team import Team from app.models.account import Account from app.models.tree import Tree from app.models.psa_connection import PsaConnection class AISession(Base): """A FlowPilot-guided troubleshooting session. Lifecycle: active → resolved | escalated | abandoned Sessions may be paused and resumed (e.g., escalation handoff). """ __tablename__ = "ai_sessions" __table_args__ = ( CheckConstraint( "intake_type IN ('free_text', 'psa_ticket', 'screenshot', 'log_paste', 'combined')", name="ck_ai_sessions_intake_type", ), CheckConstraint( "status IN ('active', 'paused', 'resolved', 'escalated', 'abandoned')", name="ck_ai_sessions_status", ), CheckConstraint( "confidence_tier IN ('guided', 'exploring', 'discovery')", name="ck_ai_sessions_confidence_tier", ), ) id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) user_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True, ) account_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False, index=True, ) team_id: Mapped[Optional[uuid.UUID]] = mapped_column( UUID(as_uuid=True), ForeignKey("teams.id", ondelete="SET NULL"), nullable=True, index=True, ) # ── Intake ── intake_type: Mapped[str] = mapped_column( String(20), nullable=False, default="free_text" ) intake_content: Mapped[dict[str, Any]] = mapped_column( JSONB, nullable=False, default=dict, comment="Original intake data: {text, image_urls, log_content, ticket_data}", ) problem_summary: Mapped[Optional[str]] = mapped_column( Text, nullable=True, comment="AI-generated one-line problem summary from intake", ) problem_domain: Mapped[Optional[str]] = mapped_column( String(100), nullable=True, comment="Classified domain: active_directory, networking, m365, hardware, etc.", ) # ── Session state ── status: Mapped[str] = mapped_column( String(20), nullable=False, default="active", index=True, ) confidence_tier: Mapped[str] = mapped_column( String(20), nullable=False, default="discovery", comment="Current AI confidence: guided (>80%), exploring (40-80%), discovery (<40%)", ) confidence_score: Mapped[float] = mapped_column( Float, nullable=False, default=0.0, comment="Numeric confidence 0.0-1.0 for internal tracking", ) # ── Flow matching ── matched_flow_id: Mapped[Optional[uuid.UUID]] = mapped_column( UUID(as_uuid=True), ForeignKey("trees.id", ondelete="SET NULL"), nullable=True, comment="If following an existing flow, which one", ) match_score: Mapped[Optional[float]] = mapped_column( Float, nullable=True, comment="Similarity score of the matched flow (0.0-1.0)", ) # ── PSA link ── psa_ticket_id: Mapped[Optional[str]] = mapped_column( String(100), nullable=True, comment="External PSA ticket ID if session was started from a ticket", ) psa_connection_id: Mapped[Optional[uuid.UUID]] = mapped_column( UUID(as_uuid=True), ForeignKey("psa_connections.id", ondelete="SET NULL"), nullable=True, ) ticket_data: Mapped[Optional[dict[str, Any]]] = mapped_column( JSONB, nullable=True, comment="Snapshot of PSA ticket data at session start", ) # ── Resolution / Escalation ── resolution_summary: Mapped[Optional[str]] = mapped_column( Text, nullable=True, comment="What fixed the issue (set on resolution)", ) resolution_action: Mapped[Optional[str]] = mapped_column( Text, nullable=True, comment="The specific action/step that resolved the issue", ) escalation_reason: Mapped[Optional[str]] = mapped_column( Text, nullable=True, comment="Why escalated (set on escalation)", ) escalation_package: Mapped[Optional[dict[str, Any]]] = mapped_column( JSONB, nullable=True, comment="Context package for receiving engineer: steps_tried, hypotheses, suggestions", ) escalated_to_id: Mapped[Optional[uuid.UUID]] = mapped_column( UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True, ) # ── Feedback ── session_rating: Mapped[Optional[int]] = mapped_column( Integer, nullable=True, comment="1-5 engineer feedback rating", ) session_feedback: Mapped[Optional[str]] = mapped_column( Text, nullable=True, comment="Optional feedback text from engineer", ) # ── AI tracking ── total_input_tokens: Mapped[int] = mapped_column( Integer, nullable=False, default=0, ) total_output_tokens: Mapped[int] = mapped_column( Integer, nullable=False, default=0, ) step_count: Mapped[int] = mapped_column( Integer, nullable=False, default=0, ) # ── Timestamps ── created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), ) resolved_at: Mapped[Optional[datetime]] = mapped_column( DateTime(timezone=True), nullable=True, ) # ── LLM conversation context ── system_prompt_snapshot: Mapped[Optional[str]] = mapped_column( Text, nullable=True, comment="Snapshot of the system prompt used (for debugging/training)", ) conversation_messages: Mapped[list[dict[str, Any]]] = mapped_column( JSONB, nullable=False, default=list, comment="Full LLM message history for context continuity", ) # ── Relationships ── user: Mapped["User"] = relationship("User", foreign_keys=[user_id]) account: Mapped["Account"] = relationship("Account") team: Mapped[Optional["Team"]] = relationship("Team") matched_flow: Mapped[Optional["Tree"]] = relationship("Tree", foreign_keys=[matched_flow_id]) escalated_to: Mapped[Optional["User"]] = relationship("User", foreign_keys=[escalated_to_id]) psa_connection: Mapped[Optional["PsaConnection"]] = relationship("PsaConnection") steps: Mapped[list["AISessionStep"]] = relationship( "AISessionStep", back_populates="session", cascade="all, delete-orphan", order_by="AISessionStep.step_order", ) ``` ### Task 2: Create AISessionStep model **Files:** - Create: `backend/app/models/ai_session_step.py` ```python """AI session step model. Every interaction within an AI session is captured as a step. Steps are the raw material that becomes flow nodes in the Knowledge Flywheel. """ import uuid from datetime import datetime, timezone from typing import Optional, Any, TYPE_CHECKING from sqlalchemy import String, Text, DateTime, ForeignKey, Integer, Float, CheckConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.dialects.postgresql import UUID, JSONB from app.core.database import Base if TYPE_CHECKING: from app.models.ai_session import AISession from app.models.script_template import ScriptGeneration class AISessionStep(Base): """A single interaction step within a FlowPilot session. Step types: - question: FlowPilot asks a diagnostic question with options - action: FlowPilot suggests an action for the engineer to perform - script_generation: FlowPilot invokes the Script Generator - verification: FlowPilot asks engineer to verify a condition - info_request: FlowPilot asks engineer to gather specific data - note: Engineer or FlowPilot adds a contextual note - intake_analysis: Initial analysis of the intake content """ __tablename__ = "ai_session_steps" __table_args__ = ( CheckConstraint( "step_type IN ('question', 'action', 'script_generation', 'verification', " "'info_request', 'note', 'intake_analysis')", name="ck_ai_session_steps_step_type", ), ) id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) session_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("ai_sessions.id", ondelete="CASCADE"), nullable=False, index=True, ) step_order: Mapped[int] = mapped_column( Integer, nullable=False, comment="Sequential position in the session (0-indexed)", ) step_type: Mapped[str] = mapped_column( String(30), nullable=False, ) # ── Content presented to engineer ── content: Mapped[dict[str, Any]] = mapped_column( JSONB, nullable=False, default=dict, comment="The question/action content rendered in the session UI", ) context_message: Mapped[Optional[str]] = mapped_column( Text, nullable=True, comment="Why FlowPilot is asking this (shown above the question)", ) # ── Options (for question steps) ── options_presented: Mapped[Optional[list[dict[str, Any]]]] = mapped_column( JSONB, nullable=True, comment="Array of {label, value, followup_hint} options shown to engineer", ) # ── Engineer response ── selected_option: Mapped[Optional[str]] = mapped_column( String(500), nullable=True, comment="Which option the engineer selected (value field)", ) free_text_input: Mapped[Optional[str]] = mapped_column( Text, nullable=True, comment="If engineer typed a custom response instead of selecting an option", ) was_free_text: Mapped[bool] = mapped_column( default=False, comment="True if the engineer used the free-text escape hatch", ) was_skipped: Mapped[bool] = mapped_column( default=False, comment="True if engineer selected 'I don't know / Can't check'", ) # ── Action results ── action_result: Mapped[Optional[dict[str, Any]]] = mapped_column( JSONB, nullable=True, comment="Outcome of action step: {success: bool, details: str, next_hint: str}", ) # ── Script generation link ── script_generation_id: Mapped[Optional[uuid.UUID]] = mapped_column( UUID(as_uuid=True), ForeignKey("script_generations.id", ondelete="SET NULL"), nullable=True, ) # ── AI internals ── confidence_at_step: Mapped[float] = mapped_column( Float, nullable=False, default=0.0, comment="FlowPilot confidence level at this point (0.0-1.0)", ) ai_reasoning: Mapped[Optional[str]] = mapped_column( Text, nullable=True, comment="Why FlowPilot chose this step (internal, for debugging/training)", ) input_tokens: Mapped[int] = mapped_column( Integer, nullable=False, default=0, ) output_tokens: Mapped[int] = mapped_column( Integer, nullable=False, default=0, ) # ── Timestamps ── created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) responded_at: Mapped[Optional[datetime]] = mapped_column( DateTime(timezone=True), nullable=True, comment="When the engineer responded to this step", ) # ── Relationships ── session: Mapped["AISession"] = relationship("AISession", back_populates="steps") script_generation: Mapped[Optional["ScriptGeneration"]] = relationship("ScriptGeneration") ``` ### Task 3: Register models in `__init__.py` **Files:** - Edit: `backend/app/models/__init__.py` Add these imports after the `ScriptGeneration` line: ```python from .ai_session import AISession from .ai_session_step import AISessionStep ``` Add to `__all__`: ```python "AISession", "AISessionStep", ``` ### Task 4: Create Alembic migration Generate with: ```bash cd backend && alembic revision --autogenerate -m "add ai_sessions and ai_session_steps tables" ``` The migration should create both tables with all columns, constraints, and indexes. Also add columns to the existing `trees` table for flow matching support: ```python op.add_column('trees', sa.Column('origin', sa.String(20), nullable=True, comment='manual | ai_generated | ai_enhanced')) op.add_column('trees', sa.Column('source_session_id', sa.dialects.postgresql.UUID(as_uuid=True), nullable=True)) op.add_column('trees', sa.Column('match_keywords', sa.dialects.postgresql.JSONB(), nullable=True, server_default='[]')) op.add_column('trees', sa.Column('success_rate', sa.Float(), nullable=True)) op.add_column('trees', sa.Column('last_matched_at', sa.DateTime(timezone=True), nullable=True)) ``` **Verification:** Run `alembic upgrade head`. Verify both tables exist in psql. Verify trees table has new columns. ``` git commit -m "feat(ai-session): add AISession and AISessionStep models + migration" ``` --- ## Slice 2: Pydantic Schemas ### Task 5: Create AI session schemas **Files:** - Create: `backend/app/schemas/ai_session.py` ```python """Pydantic schemas for FlowPilot AI sessions.""" from __future__ import annotations from typing import Optional, Any from uuid import UUID from datetime import datetime from pydantic import BaseModel, Field # ── Intake ── class AISessionCreateRequest(BaseModel): """Start a new FlowPilot session.""" intake_type: str = Field( "free_text", pattern="^(free_text|psa_ticket|screenshot|log_paste|combined)$", ) intake_content: dict[str, Any] = Field( ..., description=( "Intake payload. Shape depends on intake_type: " "{text: str} for free_text, " "{text?: str, image_urls?: list[str]} for screenshot, " "{text?: str, log_content?: str} for log_paste, " "{ticket_id: str, psa_connection_id: str} for psa_ticket, " "any combination for combined." ), ) psa_ticket_id: Optional[str] = None psa_connection_id: Optional[UUID] = None class AISessionCreateResponse(BaseModel): """Response after starting a session — includes the first FlowPilot step.""" session_id: UUID status: str confidence_tier: str problem_summary: str | None = None problem_domain: str | None = None matched_flow_id: UUID | None = None matched_flow_name: str | None = None match_score: float | None = None first_step: AISessionStepResponse # ── Step interaction ── class StepOptionSchema(BaseModel): """A selectable option presented to the engineer.""" label: str value: str followup_hint: str | None = None class AISessionStepResponse(BaseModel): """A FlowPilot step rendered in the session UI.""" step_id: UUID step_order: int step_type: str content: dict[str, Any] context_message: str | None = None options: list[StepOptionSchema] = [] allow_free_text: bool = True allow_skip: bool = True confidence_tier: str confidence_score: float model_config = {"from_attributes": True} class StepResponseRequest(BaseModel): """Engineer's response to a FlowPilot step.""" selected_option: str | None = None free_text_input: str | None = None was_skipped: bool = False action_result: dict[str, Any] | None = None class StepResponseResponse(BaseModel): """FlowPilot's next step after processing the engineer's response.""" session_id: UUID status: str confidence_tier: str confidence_score: float next_step: AISessionStepResponse | None = None resolution_suggested: bool = False resolution_summary: str | None = None # ── Resolution / Escalation ── class ResolveSessionRequest(BaseModel): """Close a session as resolved.""" resolution_summary: str = Field(..., min_length=5, max_length=2000) resolution_action: str | None = None session_rating: int | None = Field(None, ge=1, le=5) session_feedback: str | None = None class EscalateSessionRequest(BaseModel): """Escalate a session to another engineer.""" escalation_reason: str = Field(..., min_length=5, max_length=2000) escalated_to_id: UUID | None = None class SessionDocumentation(BaseModel): """Auto-generated session documentation.""" problem_summary: str problem_domain: str | None = None intake_summary: str diagnostic_steps: list[DocumentationStep] resolution_summary: str | None = None escalation_reason: str | None = None total_steps: int duration_display: str | None = None generated_at: datetime class DocumentationStep(BaseModel): """A step in the documentation trail.""" step_number: int step_type: str description: str engineer_response: str | None = None outcome: str | None = None class SessionCloseResponse(BaseModel): """Response after resolving or escalating.""" session_id: UUID status: str documentation: SessionDocumentation class RateSessionRequest(BaseModel): """Submit post-session rating.""" rating: int = Field(..., ge=1, le=5) feedback: str | None = None # ── List / Detail ── class AISessionSummary(BaseModel): """Compact session for list views.""" id: UUID status: str intake_type: str problem_summary: str | None = None problem_domain: str | None = None confidence_tier: str step_count: int session_rating: int | None = None created_at: datetime resolved_at: datetime | None = None model_config = {"from_attributes": True} class AISessionDetail(AISessionSummary): """Full session detail with steps.""" intake_content: dict[str, Any] matched_flow_id: UUID | None = None match_score: float | None = None resolution_summary: str | None = None resolution_action: str | None = None escalation_reason: str | None = None session_feedback: str | None = None steps: list[AISessionStepResponse] = [] model_config = {"from_attributes": True} ``` **Verification:** Import the schemas in a Python shell and construct sample instances to verify validation. ``` git commit -m "feat(ai-session): add Pydantic schemas for AI sessions" ``` --- ## Slice 3: FlowPilot Engine Service ### Task 6: Create FlowPilot Engine service This is the brain of the product. It orchestrates the LLM, manages structured output, and drives the diagnostic conversation. **Files:** - Create: `backend/app/services/flowpilot_engine.py` **Architecture:** ``` ┌─────────────────────────────────────────────┐ │ FlowPilotEngine │ │ │ │ start_session(intake) → first_step │ │ process_response(step_response) → next_step│ │ resolve_session(summary) → documentation │ │ escalate_session(reason) → package │ │ │ │ Internal: │ │ _build_system_prompt(session) │ │ _build_messages(session, new_input) │ │ _parse_structured_output(llm_response) │ │ _update_confidence(session, step) │ │ _generate_documentation(session) │ │ _classify_intake(intake_content) │ └─────────────────────────────────────────────┘ │ ▼ ┌─────────────────┐ ┌──────────────────┐ │ ai_provider.py │ │ FlowMatchingEngine│ │ (Anthropic) │ │ (Slice 4) │ └─────────────────┘ └──────────────────┘ ``` **System Prompt Structure:** The system prompt is built dynamically per session and includes: 1. Role definition (Senior MSP engineer + structured output contract) 2. Response format (STRICT JSON schema — never free-form prose) 3. Team context (if available — client configs, naming conventions) 4. Matched flow context (if a flow matched — full flow definition) 5. Session history (all prior steps for context continuity) 6. Available actions (script templates, verification checks) **Structured Output Contract:** FlowPilot's LLM responses must ALWAYS be valid JSON matching one of these shapes: ```json // Diagnostic question { "type": "question", "content": "Brief description of what we're checking", "reasoning": "Internal: why this question matters (stored in ai_reasoning)", "context_message": "Shown to engineer: why we're asking this", "options": [ {"label": "Human-readable option", "value": "machine_value", "followup_hint": "what this implies"}, {"label": "Another option", "value": "another_value", "followup_hint": null} ], "allow_free_text": true, "allow_skip": true, "confidence": 0.65 } // Suggested action { "type": "action", "content": "What the engineer should do", "reasoning": "Internal: why this action", "context_message": "Here's what I'd like you to try", "action_type": "instruction | script_generation | verification | info_request", "template_id": "uuid | null", "pre_filled_params": {}, "expected_outcome": "What success looks like", "confidence": 0.78 } // Resolution suggestion { "type": "resolution_suggestion", "content": "Summary of what we did and why it should be resolved", "reasoning": "Internal: why I think this is resolved", "resolution_summary": "The issue was caused by X and fixed by Y", "confidence": 0.92, "follow_up_recommendations": ["Monitor for 24 hours", "Check event log tomorrow"] } ``` **Key implementation details:** - Use `ai_provider.generate_json()` for all LLM calls to enforce JSON output - Parse the JSON response and validate against the expected shapes - If parsing fails, retry once with a "please respond in valid JSON" nudge - Store `reasoning` in `ai_reasoning` column (never shown to engineer) - Map `confidence` to `confidence_tier`: >0.8 = guided, 0.4-0.8 = exploring, <0.4 = discovery - Conversation messages accumulate in `session.conversation_messages` JSONB field - Each step creates both an `AISessionStep` record AND appends to conversation history - Token counts tracked per step AND accumulated on the session **System prompt template** (key excerpt — full prompt will be ~800 tokens): ```python FLOWPILOT_SYSTEM_PROMPT = """You are FlowPilot, an expert MSP troubleshooting assistant embedded in ResolutionFlow. You guide engineers through structured diagnosis of IT issues. ## YOUR ROLE - Conduct systematic troubleshooting through targeted questions and actions - Start broad, narrow down based on responses - Never guess — ask clarifying questions when uncertain - Suggest specific, actionable steps the engineer can verify - When confidence is high, suggest resolution; when low, keep investigating ## RESPONSE FORMAT You MUST respond with ONLY a valid JSON object. No markdown, no prose, no code fences. Every response must have a "type" field: "question", "action", or "resolution_suggestion". {structured_output_schema} ## RULES - Maximum 5 options per question. Options should be the most likely scenarios. - Always include relevant context in context_message — explain WHY you're asking - confidence is a float 0.0-1.0 reflecting how certain you are about the diagnosis path - When multiple symptoms point to one root cause with >90% confidence, suggest resolution - If you detect the engineer needs a PowerShell script, suggest a script_generation action - Never suggest restarting or rebooting as a first step — diagnose first - Be specific: "Check Event Viewer > System > source NTFS" not "check the logs" {team_context} {matched_flow_context} """ ``` **`start_session` flow:** 1. Receive intake content 2. Call `_classify_intake()` — quick LLM call (haiku-tier) to extract: problem_summary, problem_domain, key symptoms, urgency 3. Call `FlowMatchingEngine.find_matches()` with extracted symptoms 4. Build system prompt with any matched flow context 5. Call LLM with intake as first user message → get first diagnostic step 6. Create `AISession` and first `AISessionStep` records 7. Return session + first step **`process_response` flow:** 1. Load session + all steps 2. Append engineer's response to conversation_messages 3. Call LLM with full conversation → get next step 4. Parse structured output 5. Create new `AISessionStep` record 6. Update session confidence, step_count, token counts 7. If type is `resolution_suggestion`, flag it but don't auto-close 8. Return next step **`resolve_session` flow:** 1. Set status = resolved, resolved_at = now 2. Call `_generate_documentation()` — constructs a clean doc from all steps 3. Return documentation **`_generate_documentation` flow:** 1. Walk all steps in order 2. For each question step: format as "Checked: {context_message} → Response: {selected_option or free_text}" 3. For each action step: format as "Action: {content} → Result: {action_result}" 4. Compile into `SessionDocumentation` schema 5. Calculate duration from created_at to resolved_at **Verification:** Write unit tests for `_parse_structured_output` and `_update_confidence`. Test `start_session` with a mock AI provider returning sample JSON. Verify session and step records are created correctly. ``` git commit -m "feat(ai-session): add FlowPilot Engine service with structured output" ``` --- ## Slice 4: Flow Matching Engine ### Task 7: Create Flow Matching Engine v1 **Files:** - Create: `backend/app/services/flow_matching_engine.py` **Architecture:** v1 uses keyword matching + existing tree embeddings for semantic search. Deliberately simple — v2 (Phase 3) will add deeper semantic matching. **Matching strategy:** 1. **Keyword match:** Extract key terms from intake, match against `trees.match_keywords` JSONB array (new column from migration) 2. **Semantic search:** Embed the intake text via `embedding_service.get_embedding()`, cosine similarity search against `tree_embeddings` table 3. **Category match:** Match `problem_domain` against `trees.category` 4. **Recency boost:** Trees that were recently matched successfully get a score boost **Return type:** List of `FlowMatch(tree_id, tree_name, score, match_reason)` sorted by score descending. **Threshold:** Only return matches with score > 0.5. Top match with score > 0.8 triggers "guided" confidence tier. **Key implementation:** - Reuse `rag_service.search()` for vector similarity (it already handles pgvector queries) - Combine keyword score (0.0-1.0) + semantic score (0.0-1.0) + recency score (0.0-0.2) into a weighted composite - Weights: semantic 0.5, keyword 0.3, recency 0.2 - Only match against published trees (status = 'published') in the same account **Verification:** Seed a few test trees with keywords. Call `find_matches()` with a sample intake and verify reasonable matches are returned. ``` git commit -m "feat(ai-session): add Flow Matching Engine v1" ``` --- ## Slice 5: API Endpoints ### Task 8: Create AI session endpoints **Files:** - Create: `backend/app/api/endpoints/ai_sessions.py` Follow the patterns in `copilot.py` exactly: rate limiting, AI quota checks, error handling with AI provider errors, token usage recording. **Endpoints:** ``` POST /api/v1/ai-sessions — Start a new FlowPilot session POST /api/v1/ai-sessions/{id}/respond — Submit step response, get next step POST /api/v1/ai-sessions/{id}/resolve — Resolve the session POST /api/v1/ai-sessions/{id}/escalate — Escalate the session GET /api/v1/ai-sessions — List user's sessions (paginated) GET /api/v1/ai-sessions/{id} — Get session detail with all steps GET /api/v1/ai-sessions/{id}/documentation — Get auto-generated documentation POST /api/v1/ai-sessions/{id}/rate — Submit post-session rating ``` **Auth:** All endpoints require `get_current_active_user` + `require_engineer_or_admin`. **Rate limits:** - `POST /ai-sessions`: 5/minute (starting sessions is expensive) - `POST /{id}/respond`: 15/minute (normal conversation pace) - `GET` endpoints: 30/minute **AI quota:** Check quota on `POST /ai-sessions` and `POST /{id}/respond` (both make LLM calls). Record usage after each call using `record_ai_usage()` from `app.core.ai_quota_service`. **Key details:** - `POST /ai-sessions` calls `flowpilot_engine.start_session()`, returns `AISessionCreateResponse` - `POST /{id}/respond` calls `flowpilot_engine.process_response()`, returns `StepResponseResponse` - `POST /{id}/resolve` calls `flowpilot_engine.resolve_session()`, returns `SessionCloseResponse` - `GET /ai-sessions` supports `?status=active` filter and pagination (`?skip=0&limit=20`) - Ensure the session belongs to the current user (or their team for escalation handoffs) ### Task 9: Register router **Files:** - Edit: `backend/app/api/router.py` Add import and include: ```python from app.api.endpoints import ai_sessions api_router.include_router(ai_sessions.router) ``` **Verification:** Start the backend. Hit `POST /api/v1/ai-sessions` with a sample intake via curl. Verify session is created, first step is returned. Submit a response, verify next step. Resolve, verify documentation. ``` git commit -m "feat(ai-session): add AI session API endpoints" ``` --- ## Slice 6: Frontend — Types & API Client ### Task 10: Create TypeScript types **Files:** - Create: `frontend/src/types/ai-session.ts` Mirror all Pydantic schemas as TypeScript interfaces. Follow the patterns in `types/copilot.ts` and `types/session.ts`. Key types: `AISessionCreateRequest`, `AISessionCreateResponse`, `AISessionStepResponse`, `StepOptionSchema`, `StepResponseRequest`, `StepResponseResponse`, `ResolveSessionRequest`, `EscalateSessionRequest`, `SessionCloseResponse`, `SessionDocumentation`, `AISessionSummary`, `AISessionDetail`. ### Task 11: Create API client functions **Files:** - Create: `frontend/src/api/aiSessions.ts` Follow patterns in `api/copilot.ts` — use the existing `client.ts` axios instance. ```typescript export const createAISession = (data: AISessionCreateRequest) => client.post('/ai-sessions', data) export const respondToStep = (sessionId: string, data: StepResponseRequest) => client.post(`/ai-sessions/${sessionId}/respond`, data) export const resolveSession = (sessionId: string, data: ResolveSessionRequest) => client.post(`/ai-sessions/${sessionId}/resolve`, data) export const escalateSession = (sessionId: string, data: EscalateSessionRequest) => client.post(`/ai-sessions/${sessionId}/escalate`, data) export const getAISessions = (params?: { status?: string; skip?: number; limit?: number }) => client.get('/ai-sessions', { params }) export const getAISession = (sessionId: string) => client.get(`/ai-sessions/${sessionId}`) export const getSessionDocumentation = (sessionId: string) => client.get(`/ai-sessions/${sessionId}/documentation`) export const rateSession = (sessionId: string, data: { rating: number; feedback?: string }) => client.post(`/ai-sessions/${sessionId}/rate`, data) ``` **Verification:** Import in a component, verify TypeScript compiles with no errors. ``` git commit -m "feat(ai-session): add frontend types and API client" ``` --- ## Slice 7: Frontend — FlowPilot Session Page ### Task 12: Create the FlowPilot session page and components This is the most important UX in the product. It needs to feel like a smart conversation, not a form. **Files:** - Create: `frontend/src/pages/FlowPilotSessionPage.tsx` — The main page - Create: `frontend/src/components/flowpilot/FlowPilotIntake.tsx` — Intake screen - Create: `frontend/src/components/flowpilot/FlowPilotSession.tsx` — Active session view - Create: `frontend/src/components/flowpilot/FlowPilotStepCard.tsx` — Individual step card - Create: `frontend/src/components/flowpilot/FlowPilotOptions.tsx` — Selectable options grid - Create: `frontend/src/components/flowpilot/FlowPilotActionBar.tsx` — Resolve/Escalate bar - Create: `frontend/src/components/flowpilot/ConfidenceIndicator.tsx` — Confidence tier badge - Create: `frontend/src/components/flowpilot/SessionDocView.tsx` — Documentation view - Create: `frontend/src/components/flowpilot/index.ts` — Barrel export - Create: `frontend/src/hooks/useFlowPilotSession.ts` — Session state management hook **Design requirements (MUST follow existing design system):** - Dark theme: slate-900/950 backgrounds, glass morphism cards - Brand cyan (#06b6d4 → #22d3ee) for primary actions and active states - IBM Plex Sans for body, Bricolage Grotesque for headings, JetBrains Mono for code - shadcn/ui components as base (Button, Card, Badge, Textarea, etc.) - Sonner for toast notifications - Lucide icons throughout ### Intake Screen (`FlowPilotIntake.tsx`) **Layout:** Centered card on the page. Clean, focused, inviting. **Elements:** - Heading: "What are you troubleshooting?" (Bricolage Grotesque) - Large textarea with placeholder: "Describe the issue, paste an error message, or paste log output..." - Below textarea: Row of input type pills/badges — "Add Screenshot" (image upload), "Paste Logs" (toggles a monospace textarea), "Pull from Ticket" (opens ticket picker — disabled until PSA integration in Phase 2) - "Start Session" primary button (cyan, disabled until text has content) - Below the CTA: subtle text "FlowPilot will analyze your input and guide you through diagnosis" **Behavior:** - On submit: call `createAISession()`, transition to active session view - Show loading state: "Analyzing your issue..." with a subtle pulse animation - Screenshot upload stores the file and passes URL in `intake_content.image_urls` ### Active Session View (`FlowPilotSession.tsx`) **Layout:** Conversational scroll view. Steps appear sequentially like a chat, scrolling down as the conversation progresses. **Left column (main, ~70%):** The conversation. Each step is a `FlowPilotStepCard`. History steps are collapsed/completed. The current step is expanded and interactive. **Right column (sidebar, ~30%):** Session metadata — problem summary, domain badge, confidence indicator, matched flow (if any), step counter, elapsed time. Sticky positioned. **Bottom bar:** `FlowPilotActionBar` — always visible. "Resolve" (green) and "Escalate" (amber) buttons. Only enabled after at least one diagnostic step. ### Step Card (`FlowPilotStepCard.tsx`) **For question steps:** - Context message (if present) shown in a subtle info banner above the question - Question content as the card heading - Options rendered as `FlowPilotOptions` — clickable cards in a grid (1-2 columns) - "None of these — let me describe" link at the bottom → expands a textarea - "I can't check this right now" skip link - After responding: card collapses to show the question + selected answer inline **For action steps:** - Action description with checklist-style items if applicable - "I've completed this action" / "This didn't work" buttons - If script_generation: embedded script generator (reuse from existing ScriptGeneratorPanel) **For resolution_suggestion steps:** - Summary card with green accent border - "Yes, this is resolved" button → opens resolve modal - "No, keep investigating" button → sends response back to FlowPilot ### Options Grid (`FlowPilotOptions.tsx`) **Design:** Cards in a responsive grid (2 columns on desktop, 1 on mobile). Each option card: - Label text (primary) - Followup hint (secondary, smaller, muted) - Hover: cyan border glow + slight scale - Selected: cyan background with check icon - Keyboard: Tab through options, Enter to select ### Confidence Indicator (`ConfidenceIndicator.tsx`) Subtle badge in the sidebar: - **Guided** (>0.8): Green dot + "Proven path" text - **Exploring** (0.4-0.8): Amber dot + "Investigating" text - **Discovery** (<0.4): Purple dot + "New territory" text Tooltip on hover explains what the tier means. ### useFlowPilotSession Hook Manages all session state: ```typescript interface UseFlowPilotSession { // State session: AISessionDetail | null currentStep: AISessionStepResponse | null allSteps: AISessionStepResponse[] isLoading: boolean isProcessing: boolean // waiting for next step from LLM error: string | null // Actions startSession: (intake: AISessionCreateRequest) => Promise respondToStep: (response: StepResponseRequest) => Promise resolveSession: (data: ResolveSessionRequest) => Promise escalateSession: (data: EscalateSessionRequest) => Promise rateSession: (rating: number, feedback?: string) => Promise // Derived isActive: boolean canResolve: boolean canEscalate: boolean } ``` ### Task 13: Add route and navigation **Files:** - Edit: `frontend/src/router.tsx` Add lazy import: ```typescript const FlowPilotSessionPage = lazy(() => import('@/pages/FlowPilotSessionPage')) ``` Add routes inside the AppLayout children: ```typescript { path: 'pilot', element: page(FlowPilotSessionPage) }, { path: 'pilot/:sessionId', element: page(FlowPilotSessionPage) }, ``` **Files:** - Edit sidebar component (in `frontend/src/components/sidebar/`) Add a prominent "New Session" entry at the top of the sidebar with the `Sparkles` Lucide icon and cyan highlight. This should be the most visually prominent nav item. Link to `/pilot`. **Verification:** Navigate to `/pilot`. See the intake screen. Type a problem, submit. See FlowPilot's first question with selectable options. Select an option, see the next step. Continue for 3-4 steps. Resolve. See documentation. Verify all data is persisted in the database. ``` git commit -m "feat(ai-session): add FlowPilot session page, components, and routing" ``` --- ## Slice 8: Session History Integration ### Task 14: AI sessions in session history **Files:** - Edit: `frontend/src/pages/SessionHistoryPage.tsx` - Create: `frontend/src/components/flowpilot/AISessionListItem.tsx` Add a tab or toggle to switch between "Flow Sessions" (existing) and "AI Sessions" (new). AI sessions show: problem summary, domain badge, status, confidence tier, step count, created_at, resolved_at. Click on an AI session navigates to `/pilot/{sessionId}` which loads the session in read-only mode showing the full conversation trail. **Verification:** Complete an AI session. Navigate to session history. See the AI session listed. Click it, see the full conversation in read-only mode. ``` git commit -m "feat(ai-session): integrate AI sessions into session history" ``` --- ## Summary of All New Files ### Backend ``` app/models/ai_session.py # AISession model app/models/ai_session_step.py # AISessionStep model app/schemas/ai_session.py # All Pydantic schemas app/services/flowpilot_engine.py # FlowPilot Engine (LLM orchestration) app/services/flow_matching_engine.py # Flow Matching Engine v1 app/api/endpoints/ai_sessions.py # API endpoints alembic/versions/xxx_add_ai_sessions.py # Migration ``` ### Backend (edited) ``` app/models/__init__.py # Register new models app/api/router.py # Register new router ``` ### Frontend ``` src/types/ai-session.ts # TypeScript types src/api/aiSessions.ts # API client src/pages/FlowPilotSessionPage.tsx # Main page src/hooks/useFlowPilotSession.ts # Session state hook src/components/flowpilot/ FlowPilotIntake.tsx # Intake screen FlowPilotSession.tsx # Active session view FlowPilotStepCard.tsx # Step card FlowPilotOptions.tsx # Options grid FlowPilotActionBar.tsx # Resolve/Escalate bar ConfidenceIndicator.tsx # Confidence badge SessionDocView.tsx # Documentation view AISessionListItem.tsx # History list item index.ts # Barrel export ``` ### Frontend (edited) ``` src/router.tsx # New routes src/components/sidebar/ # New nav entry src/pages/SessionHistoryPage.tsx # AI session tab ``` --- ## Config Requirements No new environment variables needed for Phase 1 — the FlowPilot Engine reuses existing `AI_PROVIDER` and `AI_MODEL_ANTHROPIC` settings (currently `claude-sonnet-4-6`). The `get_ai_provider()` function and `ai_quota_service` work as-is. Future phases may add: - `FLOWPILOT_MODEL_TIER` — route simple questions to haiku, complex diagnosis to sonnet via existing `AI_MODEL_TIERS` config - `FLOWPILOT_MAX_STEPS` — safety limit on steps per session (default: 30) --- ## Testing Strategy ### Backend Unit Tests **Files:** `backend/tests/test_flowpilot_engine.py` - Test `_parse_structured_output` with valid and invalid JSON - Test `_update_confidence` tier mapping - Test `_classify_intake` with various intake types - Test `_generate_documentation` with mock session steps - Test Flow Matching Engine scoring with mock embeddings ### Backend Integration Tests **Files:** `backend/tests/test_ai_sessions_api.py` - Test full session lifecycle: create → respond (3 steps) → resolve - Test escalation flow: create → respond → escalate - Test auth enforcement (wrong user can't access another's session) - Test rate limiting - Test AI quota enforcement ### Frontend Manual testing of the full flow: 1. Open `/pilot`, type a problem description 2. Verify first step renders with selectable options 3. Select an option, verify next step appears 4. Use the free-text "none of these" escape hatch, verify it works 5. Skip a step with "I can't check this", verify it works 6. Resolve the session, verify documentation generates 7. Check session history page, verify the AI session appears 8. Re-open the completed session, verify read-only conversation view --- ## What Comes Next (Phase 2 — NOT in scope here) For context only — do NOT implement these in Phase 1: - **PSA ticket intake:** Pull ticket from ConnectWise as session input - **PSA ticket update:** Push documentation back to ticket on resolution - **Escalation handoff:** Another engineer picks up a paused session - **Knowledge Flywheel:** Post-session flow proposal generation - **Review Queue:** UI for approving AI-generated flow proposals - **In-session Script Generator:** FlowPilot invokes script generation contextually