diff --git a/.claude/skills/generate-whatsapp-flow.md b/.claude/skills/generate-whatsapp-flow.md new file mode 100644 index 0000000..c360dea --- /dev/null +++ b/.claude/skills/generate-whatsapp-flow.md @@ -0,0 +1,203 @@ +# Generate WhatsApp Flow + +Generate a config-driven WhatsApp conversation flow JSON for the Helix Engage flow runtime engine. + +## When to use + +When the user asks to create a new WhatsApp flow, chatbot flow, or conversation automation — e.g., "create a WhatsApp flow for prescription refills", "build a feedback collection flow", "add a lab report flow". + +## Flow Runtime Architecture + +The flow engine reads JSON flow definitions from `src/messaging/flow/default-flows/` and executes them at runtime. Each flow is a graph of **Groups** (containers) containing **Blocks** (steps), connected by **Edges**. + +### Execution Model + +``` +Inbound WhatsApp message → match flow by trigger → create/resume session +→ walk forward through Groups → Blocks: + MessageBlock → send text/buttons/list to patient + InputBlock → PAUSE, wait for next message + ConditionBlock → evaluate variable, follow matching edge + SetVariableBlock → assign/transform variable + ToolCallBlock → call registered tool + AIBlock → generate LLM response + JumpBlock → jump to another group +→ End of group → follow outgoing edge → next group +→ No more edges → flow complete, session cleared +``` + +Session state stored in Redis with 24h TTL. Per-phone execution lock prevents concurrent flows. + +### Flow JSON Schema + +```typescript +type Flow = { + id: string; // "flow-{kebab-name}" + name: string; // Human-readable name + description: string; // Admin-facing description + trigger: FlowTrigger; // What starts this flow + groups: Group[]; // Ordered containers of blocks + edges: Edge[]; // Connections between blocks/groups + variables: VariableDefinition[];// Flow-scoped variables + version: number; // Start at 1 + status: 'draft' | 'published'; // Only published flows execute +}; + +type FlowTrigger = + | { type: 'message'; conditions?: { keywords?: string[]; regex?: string } } + | { type: 'default' }; // Catch-all when no other flow matches + +type Group = { + id: string; // "g1", "g2", etc. + title: string; // "Greeting", "Department Selection" + blocks: Block[]; // Executed in order +}; + +type Edge = { + id: string; // "e1", "e2", etc. + from: { blockId: string; conditionId?: string }; + to: { groupId: string; blockId?: string }; +}; + +type VariableDefinition = { + id: string; // "v1", "v2", etc. + name: string; // "selectedDepartment" + type: 'string' | 'number' | 'boolean' | 'object' | 'array'; + defaultValue?: any; +}; +``` + +### Block Types + +```typescript +// Send text, buttons, or list to patient +type MessageBlock = { + id: string; type: 'message'; + content: + | { format: 'text'; text: string } // Supports {{variables}} + | { format: 'buttons'; text: string; buttons: { id: string; title: string }[] } // Max 3 buttons, title max 20 chars + | { format: 'list'; text: string; buttonText: string; sections: { title: string; rows: { id: string; title: string; description?: string }[] }[] }; // Section title max 24 chars, row title max 24 chars, max 10 rows total +}; + +// Wait for patient reply — PAUSES execution +type InputBlock = { + id: string; type: 'input'; + inputType: 'text' | 'interactive_reply' | 'any'; + variableId: string; // Store reply in this variable + validation?: { regex?: string; errorMessage?: string }; +}; + +// Branch based on variable value +type ConditionBlock = { + id: string; type: 'condition'; + conditions: { + id: string; // "c1" — used in edge.from.conditionId + variableId: string; + operator: 'equals' | 'contains' | 'exists' | 'not_exists' | 'gt' | 'lt' | 'starts_with'; + value?: string; // Supports {{variables}} + }[]; +}; + +// Assign or transform a variable +type SetVariableBlock = { + id: string; type: 'set_variable'; + variableId: string; + value: string; + expression?: 'extract_id' | 'extract_datetime' | 'date_tomorrow' | 'date_day_after'; + // extract_id: "doc:{uuid}:{name}" → uuid (second segment) + // extract_datetime: "slot:{id}:{datetime}" → datetime (third+ segments, rejoined with :) + // date_tomorrow/date_day_after: computes date string YYYY-MM-DD +}; + +// Execute a registered tool +type ToolCallBlock = { + id: string; type: 'tool_call'; + toolName: string; // Must be a registered tool (see below) + inputs: Record; // Values support {{variables}} and {{var.field}} dot notation + outputVariableId?: string; +}; + +// Generate dynamic LLM response +type AIBlock = { + id: string; type: 'ai'; + prompt: string; // Supports {{variables}} + outputVariableId?: string; + sendToPatient: boolean; // true = send as WhatsApp message +}; + +// Jump to another group +type JumpBlock = { + id: string; type: 'jump'; + targetGroupId: string; +}; +``` + +### Available Tools (ToolRegistry) + +| Tool Name | Description | Inputs | Output | +|---|---|---|---| +| `resolve_caller` | Phone → Lead + Patient | phone? (defaults to current) | { leadId, patientId, isNew, phone } | +| `send_department_list` | Interactive department list | (none) | { sent, departments[] } | +| `send_doctor_list` | Interactive doctor list | department | { sent, count } | +| `send_slot_list` | Time slots for doctor+date | doctorId, doctorName, date | { sent, slots } | +| `send_confirm_buttons` | Confirm/Cancel buttons | summary | { sent } | +| `book_appointment` | Book with conflict check | patientName, phoneNumber, department, doctorName, scheduledAt, reason | { booked, appointmentId, reference } | +| `lookup_appointments` | Check existing appointments | (none — uses current caller) | { appointments[] } | +| `send_appointment_qr` | Generate and send QR code | appointmentId, reference, patientName, doctorName, department, scheduledAt | { sent, qrUrl } | + +### System Variables (auto-injected) + +| Variable | Description | +|---|---| +| `_initialMessage` | The first message the patient sent | +| `_senderName` | WhatsApp profile name | +| `_phone` | Phone number (E.164 without +) | +| `_callerName` | Resolved patient name from platform | +| `_leadId` | Lead ID if exists | +| `_patientId` | Patient ID if exists | +| `_isNew` | true if no prior records | + +### Variable Interpolation + +- `{{variableName}}` — simple substitution +- `{{result.fieldName}}` — dot notation for object fields (e.g., `{{bookingResult.appointmentId}}`) +- Interactive reply IDs stored in `variableId`, display titles in `variableId_title` + +### WhatsApp Constraints + +- Button title: max 20 characters +- List section title: max 24 characters +- List row title: max 24 characters +- List row description: max 72 characters +- Max 3 buttons per message +- Max 10 list rows total across all sections +- No markdown in text messages (plain text only) +- Interactive messages only work within 24h session window + +## How to Generate + +1. **Ask the user** what the flow should do — purpose, steps, what data to collect +2. **Design the groups** — each logical phase is a group (Greeting, Selection, Confirmation, etc.) +3. **Define variables** — what data flows through the conversation +4. **Build blocks** — MessageBlocks for output, InputBlocks to pause for reply, ConditionBlocks for branching, ToolCallBlocks for platform operations, AIBlocks for dynamic responses +5. **Wire edges** — connect groups via edges, condition edges for branching +6. **Write the JSON** to `src/messaging/flow/default-flows/{flow-name}.json` +7. **Register new tools** if needed in `src/messaging/flow/tool-registry.ts` + +## Reference + +See `src/messaging/flow/default-flows/appointment-booking.json` for a complete working example with: +- AI greeting +- Intent routing (book / check / question) +- Interactive lists (departments, doctors, slots) +- Date selection with custom date AI parsing +- Confirmation buttons +- Booking with conflict check +- QR code generation + +## Deployment + +After creating the flow JSON: +1. `npm run build` — verifies the JSON is copied to dist (via nest-cli.json assets) +2. Deploy to EC2 — the flow store auto-seeds on first run if `data/flows/` is empty +3. If updating an existing flow: `docker exec sidecar cp /app/dist/.../flow.json /app/data/flows/flow-id.json && docker compose restart sidecar`