# WhatsApp Flow Runtime — Design Spec ## Goal Config-driven conversation engine that reads flow definitions (JSON) and executes them at runtime. Replaces the hardcoded system prompt + tools in `messaging.service.ts`. Hospital admins define flows via API/file — no code changes needed. ## Architecture ``` Inbound WhatsApp message → MessagingController (existing) → FlowExecutionService (NEW — replaces MessagingService AI logic) → Load/create FlowSession from Redis → Match flow by trigger (or resume existing session) → Walk forward through Groups → Blocks → Pause at InputBlock, resume on next message → Send messages via MessagingProvider (existing) → Call tools via ToolRegistry (NEW) → Reply sent to patient ``` ## Flow Definition Schema ```typescript type Flow = { id: string; name: string; description: string; trigger: FlowTrigger; groups: Group[]; edges: Edge[]; variables: VariableDefinition[]; version: number; status: 'draft' | 'published'; }; type FlowTrigger = | { type: 'message'; conditions?: { keywords?: string[]; regex?: string } } | { type: 'default' }; type VariableDefinition = { id: string; name: string; type: 'string' | 'number' | 'boolean' | 'object' | 'array'; defaultValue?: any; }; ``` ## Groups and Edges ```typescript type Group = { id: string; title: string; blocks: Block[]; }; type Edge = { id: string; from: { blockId: string; conditionId?: string }; to: { groupId: string; blockId?: string }; }; ``` ## Block Types ```typescript type Block = | MessageBlock | InputBlock | ConditionBlock | SetVariableBlock | ToolCallBlock | AIBlock | JumpBlock; // Send text/list/buttons to patient type MessageBlock = { id: string; type: 'message'; content: | { format: 'text'; text: string } | { format: 'buttons'; text: string; buttons: { id: string; title: string }[] } | { format: 'list'; text: string; buttonText: string; sections: { title: string; rows: { id: string; title: string; description?: string }[] }[] }; }; // Wait for patient reply type InputBlock = { id: string; type: 'input'; inputType: 'text' | 'interactive_reply' | 'any'; variableId: string; validation?: { regex?: string; errorMessage?: string }; }; // Branch based on variable value type ConditionBlock = { id: string; type: 'condition'; conditions: { id: string; variableId: string; operator: 'equals' | 'contains' | 'exists' | 'not_exists' | 'gt' | 'lt' | 'starts_with'; value?: string; }[]; }; // Assign/transform a variable type SetVariableBlock = { id: string; type: 'set_variable'; variableId: string; value: string; expression?: 'extract_id'; }; // Execute a registered tool type ToolCallBlock = { id: string; type: 'tool_call'; toolName: string; inputs: Record; // values support {{variables}} outputVariableId?: string; }; // Generate dynamic LLM response type AIBlock = { id: string; type: 'ai'; prompt: string; // supports {{variables}} outputVariableId?: string; sendToPatient: boolean; }; // Jump to another group type JumpBlock = { id: string; type: 'jump'; targetGroupId: string; }; ``` ## Session State (Redis) ```typescript type FlowSession = { flowId: string; currentGroupId: string; currentBlockIndex: number; variables: Record; history: ConversationEntry[]; startedAt: number; lastActiveAt: number; }; ``` Key: `wa:flow:{phone}`, TTL: 24 hours (WhatsApp session window). ## Execution Loop ``` On inbound message: 1. Load session from Redis (or create new → match flow by trigger) 2. If paused at InputBlock → store reply in variable, advance 3. Walk forward: - MessageBlock → send via provider, advance - InputBlock → save session, STOP (wait for next message) - ConditionBlock → evaluate, follow matching edge (or fall through) - SetVariableBlock → assign value, advance - ToolCallBlock → execute tool, store result, advance - AIBlock → call LLM, optionally send, advance - JumpBlock → jump to target group - End of group → follow outgoing edge to next group 4. If no more blocks/edges → flow complete, clear session ``` ## Tool Registry Existing tools from messaging.service.ts become registered tools: | Tool Name | Description | Inputs | Output | |---|---|---|---| | resolve_caller | Phone → Lead + Patient | phone | { leadId, patientId, isNew, name } | | send_department_list | Send interactive department list | (none — reads from platform) | { departments[] } | | send_doctor_list | Send interactive doctor list | department | { doctors[] } | | send_slot_list | Send time slots for doctor+date | doctorId, doctorName, date | { slots[] } | | send_confirm_buttons | Send confirm/cancel buttons | summary | { sent: true } | | book_appointment | Book appointment (with conflict check) | patientName, phoneNumber, department, doctorName, scheduledAt, reason | { booked, appointmentId } | | lookup_appointments | Check existing appointments | patientId? | { appointments[] } | | create_lead | Create lead + patient | name, phoneNumber, interest | { leadId } | ## Example Flow: Appointment Booking ``` Group: "Greeting" (g1) → AIBlock: greet using patient name + context → MessageBlock: buttons ["Book Appointment", "Check Appointment", "Ask a Question"] → InputBlock: store in {{intent}} Edges: g1 → ConditionBlock routes to g2 (book) / g7 (check) / g8 (question) Group: "Department Selection" (g2) → ToolCallBlock: send_department_list → InputBlock: store in {{selectedDepartment}} Edge: g2 → g3 Group: "Doctor Selection" (g3) → ToolCallBlock: send_doctor_list, input: department={{selectedDepartment}} → InputBlock: store in {{selectedDoctor}} → SetVariableBlock: extract doctorId from {{selectedDoctor}} Edge: g3 → g4 Group: "Date Selection" (g4) → MessageBlock: "When would you like to visit?" → MessageBlock: buttons ["Tomorrow", "Day After", "Choose Date"] → InputBlock: store in {{dateChoice}} → ConditionBlock: tomorrow → SetVariable, day_after → SetVariable, else → AI parse Edge: g4 → g5 Group: "Slot Selection" (g5) → ToolCallBlock: send_slot_list, inputs: doctorId={{doctorId}}, date={{selectedDate}} → InputBlock: store in {{selectedSlot}} Edge: g5 → g6 Group: "Confirmation" (g6) → MessageBlock: buttons ["Confirm", "Cancel"], summary text → InputBlock: store in {{confirmation}} → ConditionBlock: confirm → g7, cancel → g8 Edges: confirm → "Booking" group, cancel → "Cancelled" group Group: "Booking" (g7) → ToolCallBlock: book_appointment with all collected variables → MessageBlock: confirmation with reference number Group: "Cancelled" (g8) → MessageBlock: "No problem! Let me know if you need anything else." ``` ## File Structure (Implementation) ``` src/messaging/ ├── flow/ │ ├── flow-types.ts — All types above │ ├── flow-execution.service.ts — Main execution loop │ ├── flow-session.service.ts — Redis session CRUD │ ├── flow-store.service.ts — Load/save flow definitions (file/Redis) │ ├── flow-variable.service.ts — Variable interpolation + expressions │ ├── tool-registry.ts — Tool name → handler mapping │ └── default-flows/ │ └── appointment-booking.json — Seeded default flow ├── providers/ (existing, unchanged) ├── messaging.module.ts — Wire new services ├── messaging.controller.ts — Unchanged (webhook still here) ├── messaging.service.ts — Delegates to FlowExecutionService └── types.ts — Existing types (unchanged) ``` ## Migration Path 1. Build FlowExecutionService alongside existing MessagingService 2. Seed default appointment-booking.json (equivalent to current hardcoded flow) 3. MessagingService checks: if flow config exists → delegate to FlowExecutionService, else → current AI behavior (backward compatible) 4. Once validated, remove hardcoded AI flow from MessagingService ## Not in Scope - Visual builder UI (future, maybe never) - Flow versioning/rollback (v2) - Flow analytics/metrics (v2) - Multi-flow routing (v2 — for now, one active flow per trigger type)