docs: flow runtime design spec — config-driven WhatsApp conversation engine

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) <noreply@anthropic.com>
This commit is contained in:
2026-04-20 18:18:17 +05:30
parent 6a3834a7eb
commit 4549241b78

View File

@@ -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<string, string>; // 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<string, any>;
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)