/** * Cortex Layer 3: Nudge System * * Proactive suggestions after successful actions based on protocol rules. * MVP implementation with hardcoded rules for demonstration. */ import type { CortexIntent, ExtractedEntities, NudgePriority, NudgeSuggestion, ProtocolMetadata, } from './types'; // ----------------------------------------------------------------------------- // Protocol Rule Types // ----------------------------------------------------------------------------- /** Condition operator for matching */ export type ConditionOperator = 'equals' | 'contains' | 'matches' | 'exists'; /** Single condition to check against entities or content */ export interface ProtocolCondition { /** Field to check - 'content' for the original input text, or entity field */ field: keyof ExtractedEntities | 'content'; /** Comparison operator */ operator: ConditionOperator; /** Value to compare against (not used for 'exists' operator) */ value: string; } /** Protocol rule that triggers a nudge suggestion */ export interface ProtocolRule { /** Unique identifier for the rule */ id: string; /** Human-readable name for the rule */ name: string; /** Trigger conditions */ trigger: { /** Intent that must match */ intent: CortexIntent; /** Additional conditions that must all be met */ conditions: ProtocolCondition[]; }; /** Suggestion to show when rule matches */ suggestion: { /** Suggested follow-up intent */ intent: CortexIntent; /** User-facing message */ message: string; /** Function to prefill entities from source action */ prefillEntities: (source: ExtractedEntities) => Partial; }; /** Protocol metadata for clinical context (optional) */ protocol?: ProtocolMetadata; /** Priority for sorting multiple suggestions */ priority: NudgePriority; /** Whether this rule is active */ enabled: boolean; /** Time in ms before suggestion expires (default 5 minutes) */ expiresAfterMs: number; } // ----------------------------------------------------------------------------- // Condition Checker (Internal) // ----------------------------------------------------------------------------- /** * Check if a single condition is met */ function checkCondition( condition: ProtocolCondition, entities: ExtractedEntities, content?: string ): boolean { // Get the value to check const value = condition.field === 'content' ? content : entities[condition.field as keyof ExtractedEntities]; // Handle null/undefined values if (value === undefined || value === null || value === '') { return condition.operator === 'exists' ? false : false; } const stringValue = String(value); switch (condition.operator) { case 'equals': return stringValue.toLowerCase() === condition.value.toLowerCase(); case 'contains': return stringValue.toLowerCase().includes(condition.value.toLowerCase()); case 'matches': try { return new RegExp(condition.value, 'i').test(stringValue); } catch { // Invalid regex, return false return false; } case 'exists': return true; default: return false; } } // ----------------------------------------------------------------------------- // Nudge Evaluation // ----------------------------------------------------------------------------- /** Input for nudge evaluation */ export interface NudgeEvaluationInput { /** The completed action's intent */ intent: CortexIntent; /** The action's ID */ actionId: string; /** Extracted entities from the action */ entities: ExtractedEntities; /** Original user input text (for content matching) */ content?: string; } /** * Evaluate completed action against protocol rules and generate suggestions * * @param input - The completed action details * @returns Array of NudgeSuggestions sorted by priority (high first) */ export function evaluateNudge(input: NudgeEvaluationInput): NudgeSuggestion[] { const suggestions: NudgeSuggestion[] = []; const now = new Date(); for (const rule of PROTOCOL_RULES) { // Skip disabled rules if (!rule.enabled) continue; // Check intent match if (rule.trigger.intent !== input.intent) continue; // Check all conditions (AND logic) const allConditionsMet = rule.trigger.conditions.every( (cond) => checkCondition(cond, input.entities, input.content) ); if (allConditionsMet) { suggestions.push({ id: `nudge-${rule.id}-${Date.now()}`, trigger: { actionId: input.actionId, intent: input.intent, entities: input.entities, }, suggestion: { intent: rule.suggestion.intent, entities: rule.suggestion.prefillEntities(input.entities), message: rule.suggestion.message, rationale: rule.name, protocol: rule.protocol, }, status: 'pending', priority: rule.priority, expiresAt: new Date(now.getTime() + rule.expiresAfterMs), createdAt: now, }); } } // Sort by priority (high first) const priorityOrder: Record = { high: 0, medium: 1, low: 2, }; return suggestions.sort( (a, b) => priorityOrder[a.priority] - priorityOrder[b.priority] ); } // ----------------------------------------------------------------------------- // MVP Protocol Rules (Hardcoded) // ----------------------------------------------------------------------------- /** Default expiry time: 5 minutes */ const DEFAULT_EXPIRY_MS = 5 * 60 * 1000; /** * MVP Protocol Rules * * For the prototype, we hardcode a few demonstration rules. * In production, these would come from a database or configuration. */ export const PROTOCOL_RULES: ProtocolRule[] = [ { id: 'noshow-declarabel-check', name: 'No Show declarabiliteitscheck', trigger: { intent: 'register_no_show', conditions: [], }, suggestion: { intent: 'cancel_appointment', message: 'Ik zie een declarabel consult in de agenda. Volgens inkoopvoorwaarden mag deze afspraak NIET gedeclareerd worden. Wil je dat ik deze annuleer als \'No Show\'?', prefillEntities: (_source) => ({}), }, priority: 'high', enabled: true, expiresAfterMs: DEFAULT_EXPIRY_MS, }, { id: 'wondzorg-controle', name: 'Wondcontrole na verzorging', trigger: { intent: 'dagnotitie', conditions: [ { field: 'content', operator: 'contains', value: 'wond' }, ], }, suggestion: { intent: 'create_appointment', message: 'Wondcontrole inplannen over 3 dagen?', prefillEntities: (source) => ({ patientName: source.patientName, appointmentType: 'follow-up', }), }, protocol: { name: 'V&VN Richtlijn Wondzorg', reference: 'ยง4.2 Controlefrequentie', rationale: 'Vroege hercontrole na wondverzorging verkleint het risico op infectie en bevordert optimale wondgenezing.', }, priority: 'medium', enabled: true, expiresAfterMs: DEFAULT_EXPIRY_MS, }, { id: 'medicatie-controle', name: 'Medicatie controle na wijziging', trigger: { intent: 'dagnotitie', conditions: [ { field: 'content', operator: 'contains', value: 'medicatie' }, { field: 'content', operator: 'contains', value: 'gewijzigd' }, ], }, suggestion: { intent: 'create_appointment', message: 'Medicatie evaluatie inplannen over 1 week?', prefillEntities: (source) => ({ patientName: source.patientName, appointmentType: 'follow-up', }), }, priority: 'medium', enabled: true, expiresAfterMs: DEFAULT_EXPIRY_MS, }, ];