From 4549241b78d5a7acb14f2b1b2f3d062f3b6dc401 Mon Sep 17 00:00:00 2001 From: saridsa2 Date: Mon, 20 Apr 2026 18:18:17 +0530 Subject: [PATCH] =?UTF-8?q?docs:=20flow=20runtime=20design=20spec=20?= =?UTF-8?q?=E2=80=94=20config-driven=20WhatsApp=20conversation=20engine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Groups + Blocks model adapted from Typebot. Execution loop pauses at InputBlocks, resumes on next message. Tool registry bridges existing tools. Session state in Redis with 24h TTL. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/specs/2026-04-20-flow-runtime-design.md | 270 +++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 docs/specs/2026-04-20-flow-runtime-design.md diff --git a/docs/specs/2026-04-20-flow-runtime-design.md b/docs/specs/2026-04-20-flow-runtime-design.md new file mode 100644 index 0000000..f703a54 --- /dev/null +++ b/docs/specs/2026-04-20-flow-runtime-design.md @@ -0,0 +1,270 @@ +# 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)