/** * Flow Engine — executes saved Workflow flows for incoming chat messages. * * Execution model: * 1. Load all active flows from the DB * 2. For each flow, find its intent node and check if user message matches * 3. If matched, traverse the flow graph from intent → response nodes * 4. Return the response content (or null if no flow matched) */ import { getDb } from "./db"; import { workflowNodes } from "../drizzle/schema"; import { eq } from "drizzle-orm"; export interface FlowResult { content: string; shouldEscalate?: boolean; flowId: string; flowName?: string; } // Intent patterns for the 5 live Support Flows const FLOW_INTENT_PATTERNS: Record = { "check-order-status": [ /\border\s*status\b/i, /\bcheck.*order\b/i, /\bwhere.*my order\b/i, /\border.*update\b/i, /\bmy order\b/i, ], "track-shipment": [ /\btrack.*shipment\b/i, /\btracking\b/i, /\bshipment.*status\b/i, /\bwhere.*package\b/i, /\bdelivery.*status\b/i, /\btrack.*package\b/i, ], "submit-return": [ /\breturn\b/i, /\bRMA\b/i, /\bsend.*back\b/i, /\breturn.*request\b/i, /\brefund\b/i, ], "cancel-order": [ /\bcancel.*order\b/i, /\bcancel.*my order\b/i, /\bstop.*order\b/i, ], "faq-deflection": [ /\bpayment\b/i, /\bwarranty\b/i, /\bhow.*long.*delivery\b/i, /\bshipping.*time\b/i, /\bminimum.*order\b/i, ], }; // Fallback static responses for flows that don't have DB nodes yet const FLOW_STATIC_RESPONSES: Record = { "check-order-status": "To check your order status, please provide your **Sales Order number** (e.g., SO-12345) and I'll look it up for you right away.", "track-shipment": "I can help you track your shipment! Please share your **order number** or **tracking number** and I'll pull up the latest status.", "submit-return": "I can guide you through the return process. Please provide:\n1. Your **order number**\n2. The **item(s)** you want to return\n3. The **reason** for the return\n\nOur return policy allows returns within 30 days of delivery.", "cancel-order": "I can help with order cancellation. Please provide your **order number** and I'll check if it's still within the cancellation window. Note: orders that have already shipped cannot be cancelled.", "faq-deflection": null, // handled by knowledge base }; /** * Detect if the user message matches a flow intent. * Returns the flowId or null. */ export function detectFlowIntent(message: string): string | null { for (const [flowId, patterns] of Object.entries(FLOW_INTENT_PATTERNS)) { if (patterns.some(p => p.test(message))) { return flowId; } } return null; } /** * Execute a flow by ID. Returns the response content or null. * Tries DB-saved nodes first, falls back to static responses. */ export async function executeFlow(flowId: string, _userMessage: string): Promise { // Try to load flow nodes from DB try { const db = await getDb(); if (db) { const nodes = await db.select().from(workflowNodes).where(eq(workflowNodes.workflowId, flowId)); if (nodes.length > 0) { const responseNodes = nodes.filter(n => n.type === "response" && (n.config as any)?.message); if (responseNodes.length > 0) { const escalationNode = nodes.find(n => n.type === "escalation"); const content = responseNodes.map(n => (n.config as any).message).join("\n\n"); return { content, shouldEscalate: !!escalationNode, flowId }; } } } } catch (err) { console.error("[FlowEngine] DB error:", err); } const staticResponse = FLOW_STATIC_RESPONSES[flowId]; if (staticResponse) { return { content: staticResponse, flowId }; } return null; }