mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-05-18 20:08:19 +00:00
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>
271 lines
8.5 KiB
Markdown
271 lines
8.5 KiB
Markdown
# 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)
|