# 🧠 Cortex V2 - Uitgebreid Architectuurplan **Project:** Cortex Intent System V2 **Versie:** 2.0 **Datum:** 29-12-2025 **Status:** Technisch Ontwerp --- ## Inhoudsopgave 1. [Executive Summary](#1-executive-summary) 2. [Architectuur Overview](#2-architectuur-overview) 3. [Data Models & Types](#3-data-models--types) 4. [Layer 1: Reflex Arc](#4-layer-1-reflex-arc) 5. [Layer 2: Intent Orchestrator](#5-layer-2-intent-orchestrator) 6. [Layer 3: Nudge](#6-layer-3-nudge) 7. [API Design](#7-api-design) 8. [Frontend Components](#8-frontend-components) 9. [State Management](#9-state-management) 10. [Implementatie Roadmap](#10-implementatie-roadmap) 11. [Testing Strategy](#11-testing-strategy) 12. [Migratie Plan](#12-migratie-plan) --- ## 1. Executive Summary ### Huidige Situatie (V1) Het huidige Cortex systeem is **reactief**: gebruiker geeft commando β†’ systeem voert uit. Het werkt met single-intent classificatie en heeft een two-tier architectuur (lokaal regex + AI fallback). ### Doel V2: "The Cortex" Transformatie naar een **agentic systeem** dat: - **Multi-intents** begrijpt ("Zeg Jan af **en** maak notitie") - **Context-aware** is (snapt wie "hij" is, wat "morgen" betekent) - **Proactief** suggesties geeft (na wondzorg β†’ "Controle inplannen?") - **Nooit** "Ik snap het niet" zegt (altijd een poging tot begrip) ### Kernprincipe > "We stoppen met optimaliseren voor milliseconden en starten met optimaliseren voor intelligentie." --- ## 2. Architectuur Overview ### 2.1 The Three-Layer Cortex Model ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ USER INPUT β”‚ β”‚ "Zeg Jan af en maak notitie: grieperig" β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ LAYER 1: REFLEX ARC [<20ms] β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β€’ Local Regex Pattern Matching β”‚ β”‚ β”‚ β”‚ β€’ High-confidence simple commands only (>=0.7) β”‚ β”‚ β”‚ β”‚ β€’ Examples: "agenda", "zoek jan", "notitie" β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ Decision: Confidence >= 0.7 AND no escalation triggers? ──► EXECUTE β”‚ β”‚ Otherwise ──► Pass to Layer 2 β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ [Complex/Multi-intent/Low confidence] β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ LAYER 2: INTENT ORCHESTRATOR (AI CORTEX) [~400ms] β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β€’ Claude 3.5 Haiku (fast, cheap, smart) β”‚ β”‚ β”‚ β”‚ β€’ Context injection: ActivePatient, CurrentView, Agenda, History β”‚ β”‚ β”‚ β”‚ β€’ Multi-intent parsing: splits "X en Y" into action chain β”‚ β”‚ β”‚ β”‚ β€’ Entity disambiguation: "hij" β†’ active patient β”‚ β”‚ β”‚ β”‚ β€’ Clarification questions if truly ambiguous β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ Output: IntentChain { actions: [Action1, Action2, ...] } β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ EXECUTION ENGINE β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β€’ Sequential or parallel action execution β”‚ β”‚ β”‚ β”‚ β€’ Confirmation dialogs for destructive actions β”‚ β”‚ β”‚ β”‚ β€’ Rollback support for failed chains β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ [Action Completed] β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ LAYER 3: NUDGE (POST-ACTION INTELLIGENCE) [async] β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β€’ Protocol Rules Engine: medical domain knowledge β”‚ β”‚ β”‚ β”‚ β€’ Trigger evaluation: "Does this action warrant a follow-up?" β”‚ β”‚ β”‚ β”‚ β€’ Suggestion generation: "Wondcontrole inplannen?" β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ Output: Suggestion Toast / Follow-up Card β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ### 2.2 Data Flow Diagram ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Speech │───►│ Input │───►│ Classify │───►│ Execute │───►│ Safety β”‚ β”‚ /Text β”‚ β”‚ Buffer β”‚ β”‚ (L1/L2) β”‚ β”‚ Chain β”‚ β”‚ Nudge β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β–Ό β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Cortex β”‚ β”‚ Artifact β”‚ β”‚ Suggestionβ”‚ β”‚ Store │◄───│ Updates β”‚ β”‚ Toast β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` --- ## 3. Data Models & Types ### 3.1 Core Types (lib/cortex/types.ts) ```typescript // ============================================================================ // INTENT TYPES // ============================================================================ export type CortexIntent = | 'dagnotitie' | 'zoeken' | 'overdracht' | 'agenda_query' | 'create_appointment' | 'cancel_appointment' | 'reschedule_appointment' | 'unknown'; // ============================================================================ // CONTEXT TYPES (NEW in V2) // ============================================================================ /** * Full context passed to AI for intelligent classification */ export interface CortexContext { // Active patient (if any) activePatient: { id: string; name: string; recentNotes?: string[]; // Last 3 note summaries upcomingAppointments?: { date: Date; type: string; }[]; } | null; // Current UI state currentView: 'dashboard' | 'patient-detail' | 'agenda' | 'reports' | 'chat'; // Time context shift: ShiftType; currentTime: Date; // Today's agenda (for disambiguation) agendaToday: { time: string; patientName: string; patientId: string; type: string; }[]; // Recent intents (for continuity) recentIntents: { intent: CortexIntent; patientName?: string; timestamp: Date; }[]; // User preferences (adaptive confidence) userPreferences?: { confirmationLevel: 'always' | 'destructive' | 'never'; frequentIntents: CortexIntent[]; }; } // ============================================================================ // INTENT CHAIN (Multi-Intent Support - NEW in V2) // ============================================================================ /** * A chain of actions derived from a single user input */ export interface IntentChain { id: string; originalInput: string; createdAt: Date; // Array of actions to execute actions: IntentAction[]; // Overall chain status status: 'pending' | 'executing' | 'completed' | 'partial' | 'failed'; // Processing metadata meta: { source: 'local' | 'ai'; processingTimeMs: number; aiReasoning?: string; }; } /** * A single action within an intent chain */ export interface IntentAction { id: string; sequence: number; // Order in chain (1, 2, 3...) // Classification intent: CortexIntent; confidence: number; // Extracted data entities: ExtractedEntities; // Execution state status: 'pending' | 'confirming' | 'executing' | 'success' | 'failed' | 'skipped'; // Confirmation handling requiresConfirmation: boolean; confirmationMessage?: string; // Error handling error?: { code: string; message: string; recoverable: boolean; }; // Timing startedAt?: Date; completedAt?: Date; } // ============================================================================ // ENTITY TYPES (Enhanced) // ============================================================================ export interface ExtractedEntities { // Patient reference patientName?: string; patientId?: string; patientResolution?: 'explicit' | 'context' | 'pronoun'; // How we found the patient // Dagnotitie category?: VerpleegkundigCategory; content?: string; severity?: 'low' | 'medium' | 'high'; // Sentiment analysis // Search query?: string; // Agenda dateRange?: DateRange; datetime?: { date: Date; time: string; isRelative: boolean; // "morgen" vs "15 januari" }; appointmentType?: AppointmentType; location?: 'praktijk' | 'online' | 'thuis'; // Reschedule identifier?: AppointmentIdentifier; newDatetime?: { date: Date; time: string; }; // Raw AI extraction (for debugging) _raw?: Record; } export interface DateRange { start: Date; end: Date; label: 'vandaag' | 'morgen' | 'deze week' | 'volgende week' | 'custom'; } export interface AppointmentIdentifier { type: 'patient' | 'time' | 'both' | 'id'; patientName?: string; patientId?: string; time?: string; date?: Date; encounterId?: string; } export type AppointmentType = | 'intake' | 'behandeling' | 'follow-up' | 'telefonisch' | 'huisbezoek' | 'online' | 'crisis' | 'overig'; // ============================================================================ // NUDGE TYPES (NEW in V2) // ============================================================================ /** * A suggestion generated by the Nudge layer */ export interface NudgeSuggestion { id: string; // What triggered this suggestion trigger: { actionId: string; intent: CortexIntent; entities: ExtractedEntities; }; // The suggestion itself suggestion: { intent: CortexIntent; entities: Partial; message: string; // "Wondcontrole inplannen over 3 dagen?" rationale: string; // "Bij wondzorg hoort standaard een controle" }; // UI state status: 'pending' | 'accepted' | 'dismissed' | 'expired'; // Priority and timing priority: 'low' | 'medium' | 'high'; expiresAt?: Date; createdAt: Date; } /** * Protocol rule for generating suggestions */ export interface ProtocolRule { id: string; name: string; description: string; // When to trigger trigger: { intent: CortexIntent; conditions?: ProtocolCondition[]; }; // What to suggest suggestion: { intent: CortexIntent; message: string; prefillFrom: (source: ExtractedEntities) => Partial; }; // Rule metadata priority: 'low' | 'medium' | 'high'; category: 'medicatie' | 'wondzorg' | 'veiligheid' | 'administratief'; enabled: boolean; } export interface ProtocolCondition { field: keyof ExtractedEntities; operator: 'equals' | 'contains' | 'exists' | 'matches'; value?: string | RegExp; } // ============================================================================ // CLASSIFICATION RESULT TYPES // ============================================================================ /** * Result from Layer 1 (Local Reflex) */ export interface LocalClassificationResult { intent: CortexIntent; confidence: number; matchedPattern?: string; processingTimeMs: number; // Decision shouldEscalateToAI: boolean; escalationReason?: 'low_confidence' | 'multi_intent_detected' | 'needs_context'; } /** * Result from Layer 2 (AI Orchestrator) */ export interface AIClassificationResult { chain: IntentChain; // AI metadata model: string; tokensUsed: number; processingTimeMs: number; // Clarification (if needed) needsClarification: boolean; clarificationQuestion?: string; clarificationOptions?: string[]; } /** * Combined classification result */ export interface ClassificationResult { // The final intent chain chain: IntentChain; // Which layer handled it handledBy: 'reflex' | 'orchestrator'; // Timing totalProcessingTimeMs: number; // Debug info debug?: { localResult?: LocalClassificationResult; aiResult?: AIClassificationResult; }; } ``` ### 3.2 Store Types (stores/cortex-store.ts additions) ```typescript // Add to existing CortexStore interface interface CortexStoreV2 extends CortexStore { // Context (enhanced) context: CortexContext; // Intent Chain state activeChain: IntentChain | null; chainHistory: IntentChain[]; // Nudge state pendingSuggestions: NudgeSuggestion[]; suggestionHistory: NudgeSuggestion[]; // Actions setContext: (context: Partial) => void; // Chain actions startChain: (chain: IntentChain) => void; updateActionStatus: (chainId: string, actionId: string, status: IntentAction['status']) => void; completeChain: (chainId: string) => void; // Nudge actions addSuggestion: (suggestion: NudgeSuggestion) => void; acceptSuggestion: (suggestionId: string) => void; dismissSuggestion: (suggestionId: string) => void; } ``` --- ## 4. Layer 1: Reflex Arc ### 4.1 Doel Razendsnelle (<20ms) afhandeling van **simpele, eenduidige commando's** met hoge confidence. ### 4.2 Wanneer Layer 1 afhandelt - Confidence >= 0.7 - Geen "en", "daarna", "ook" (multi-intent signals) - Geen context-afhankelijke woorden ("hij", "haar", "die afspraak") - Geen tijdsrelaties die interpretatie nodig hebben ### 4.3 Implementatie (lib/cortex/reflex-classifier.ts) ```typescript /** * Layer 1: Reflex Arc * * Fast, local pattern matching for simple commands. * Escalates to Layer 2 when complexity is detected. */ import type { LocalClassificationResult, CortexIntent } from './types'; // Multi-intent signal words const MULTI_INTENT_SIGNALS = [ /\ben\b/i, // "X en Y" /\bdaarna\b/i, // "X daarna Y" /\book\b/i, // "X ook Y" /\beerst\b/i, // "eerst X dan Y" /\bdan\b/i, // "X dan Y" /\bvervolgens\b/i, ]; // Context-dependent words (need AI to resolve) const CONTEXT_SIGNALS = [ /\bhij\b/i, /\bzij\b/i, /\bhaar\b/i, /\bhem\b/i, /\bdie\b/i, // "die afspraak" /\bdeze\b/i, // "deze patiΓ«nt" /\bdezelfde\b/i, ]; // Intent patterns with weights const REFLEX_PATTERNS: Record> = { dagnotitie: [ { pattern: /^dagnotitie\b/i, weight: 1.0 }, { pattern: /^notitie\s+\w+\s+(medicatie|adl|gedrag|incident|observatie)\b/i, weight: 0.95 }, { pattern: /^notitie\s+\w+/i, weight: 0.9 }, { pattern: /^(medicatie|adl|gedrag|incident|observatie)\s+\w+/i, weight: 0.85 }, ], zoeken: [ { pattern: /^zoek\s+\w+$/i, weight: 1.0 }, { pattern: /^vind\s+\w+$/i, weight: 1.0 }, { pattern: /^wie\s+is\s+\w+/i, weight: 0.95 }, { pattern: /^dossier\s+\w+$/i, weight: 0.9 }, ], agenda_query: [ { pattern: /^agenda\s*(vandaag|morgen)?$/i, weight: 1.0 }, { pattern: /^afspraken\s*(vandaag|morgen|deze week)?$/i, weight: 0.95 }, { pattern: /^wat\s+staat\s+er\s+(vandaag|morgen)/i, weight: 0.9 }, ], overdracht: [ { pattern: /^overdracht$/i, weight: 1.0 }, { pattern: /^dienst\s+afronden$/i, weight: 0.95 }, ], create_appointment: [ { pattern: /^maak\s+afspraak\b/i, weight: 0.85 }, { pattern: /^plan\s+(intake|afspraak)\b/i, weight: 0.85 }, ], cancel_appointment: [ { pattern: /^annuleer\s+afspraak\b/i, weight: 0.9 }, { pattern: /^zeg\s+\w+\s+af\b/i, weight: 0.85 }, ], reschedule_appointment: [ { pattern: /^verzet\s+afspraak\b/i, weight: 0.85 }, { pattern: /^verplaats\s+\w+\s+naar\b/i, weight: 0.8 }, ], unknown: [], }; /** * Check if input contains signals that require AI processing */ function detectComplexity(input: string): { hasMultiIntent: boolean; hasContextDependency: boolean; signals: string[]; } { const signals: string[] = []; const hasMultiIntent = MULTI_INTENT_SIGNALS.some(pattern => { if (pattern.test(input)) { signals.push(`multi-intent: ${pattern.source}`); return true; } return false; }); const hasContextDependency = CONTEXT_SIGNALS.some(pattern => { if (pattern.test(input)) { signals.push(`context: ${pattern.source}`); return true; } return false; }); return { hasMultiIntent, hasContextDependency, signals }; } /** * Classify input using local patterns */ export function classifyWithReflex(input: string): LocalClassificationResult { const startTime = performance.now(); const trimmedInput = input.trim(); // Step 1: Check for complexity signals const complexity = detectComplexity(trimmedInput); if (complexity.hasMultiIntent) { return { intent: 'unknown', confidence: 0, processingTimeMs: performance.now() - startTime, shouldEscalateToAI: true, escalationReason: 'multi_intent_detected', }; } if (complexity.hasContextDependency) { return { intent: 'unknown', confidence: 0, processingTimeMs: performance.now() - startTime, shouldEscalateToAI: true, escalationReason: 'needs_context', }; } // Step 2: Pattern matching let bestMatch: { intent: CortexIntent; confidence: number; pattern: string } | null = null; for (const [intent, patterns] of Object.entries(REFLEX_PATTERNS)) { for (const { pattern, weight } of patterns) { if (pattern.test(trimmedInput)) { if (!bestMatch || weight > bestMatch.confidence) { bestMatch = { intent: intent as CortexIntent, confidence: weight, pattern: pattern.source, }; } } } } const processingTimeMs = performance.now() - startTime; // Step 3: Decide on escalation if (!bestMatch || bestMatch.confidence < CONFIDENCE_THRESHOLD) { return { intent: bestMatch?.intent || 'unknown', confidence: bestMatch?.confidence || 0, matchedPattern: bestMatch?.pattern, processingTimeMs, shouldEscalateToAI: true, escalationReason: 'low_confidence', }; } // High confidence match - handle locally return { intent: bestMatch.intent, confidence: bestMatch.confidence, matchedPattern: bestMatch.pattern, processingTimeMs, shouldEscalateToAI: false, }; } ``` --- ## 5. Layer 2: Intent Orchestrator ### 5.1 Doel AI-gedreven analyse voor: - **Complexe zinnen** met lage confidence - **Multi-intents** ("X en Y") - **Context-afhankelijke** verwijzingen ("hij", "die afspraak") - **Disambiguation** bij twijfel ### 5.2 Context Injection De AI krijgt altijd volledige context mee: ```typescript /** * Build context for AI classification */ export function buildCortexContext(store: CortexStore): CortexContext { return { activePatient: store.activePatient ? { id: store.activePatient.id, name: `${store.activePatient.name_given[0]} ${store.activePatient.name_family}`, } : null, currentView: determineCurrentView(), shift: store.shift, currentTime: new Date(), agendaToday: [], // Populated from API recentIntents: store.recentActions.slice(0, 5).map(a => ({ intent: a.intent, patientName: a.patientName, timestamp: a.timestamp, })), }; } ``` ### 5.3 Implementatie (lib/cortex/orchestrator.ts) ```typescript /** * Layer 2: Intent Orchestrator * * AI-powered classification with context awareness and multi-intent support. */ import Anthropic from '@anthropic-ai/sdk'; import type { CortexContext, IntentChain, IntentAction, AIClassificationResult } from './types'; const ORCHESTRATOR_SYSTEM_PROMPT = `Je bent de Intent Orchestrator voor Cortex, een Nederlands EPD systeem. ## Je Taak Analyseer de gebruikersinput en extraheer ALLE intenties, ook als er meerdere zijn. ## Context die je krijgt - Actieve patiΓ«nt: wie de gebruiker momenteel bekijkt - Agenda vandaag: afspraken voor vandaag - Recente acties: wat de gebruiker net deed - Huidige weergave: waar in de app de gebruiker is ## Intent Types 1. **dagnotitie** - Notitie/rapportage maken 2. **zoeken** - PatiΓ«nt zoeken 3. **overdracht** - Dienst overdracht 4. **agenda_query** - Agenda bekijken 5. **create_appointment** - Afspraak maken 6. **cancel_appointment** - Afspraak annuleren 7. **reschedule_appointment** - Afspraak verzetten ## Multi-Intent Detectie Let op woorden als: "en", "daarna", "ook", "eerst", "dan", "vervolgens" Voorbeeld: - "Zeg Jan af en maak notitie: ziek" β†’ 2 intents - "Plan intake marie morgen 14:00" β†’ 1 intent ## Pronoun Resolution Gebruik de context om "hij/zij/die" op te lossen: - Als er een actieve patiΓ«nt is, verwijst "hij/zij" daar waarschijnlijk naar - "Die afspraak" verwijst naar de meest recente genoemde afspraak ## Output Format Antwoord ALLEEN met valid JSON: { "actions": [ { "intent": "cancel_appointment", "confidence": 0.95, "entities": { "patientName": "Jan", "patientResolution": "explicit" }, "requiresConfirmation": true, "confirmationMessage": "Afspraak van Jan annuleren?" }, { "intent": "dagnotitie", "confidence": 0.9, "entities": { "patientName": "Jan", "patientResolution": "context", "content": "ziek" }, "requiresConfirmation": false } ], "reasoning": "Gebruiker wil twee dingen: afspraak annuleren EN notitie maken. 'Jan' expliciet genoemd voor annulering, impliciet voor notitie.", "needsClarification": false } Als je twijfelt, stel een verduidelijkingsvraag: { "actions": [], "needsClarification": true, "clarificationQuestion": "Bedoel je een notitie maken of een afspraak inplannen?", "clarificationOptions": ["Notitie maken", "Afspraak inplannen"] }`; /** * Format context for AI prompt */ function formatContextForPrompt(context: CortexContext): string { const lines: string[] = []; // Active patient if (context.activePatient) { lines.push(`πŸ§‘ Actieve patiΓ«nt: ${context.activePatient.name} (ID: ${context.activePatient.id})`); } else { lines.push(`πŸ§‘ Actieve patiΓ«nt: Geen`); } // Current view lines.push(`πŸ“ Huidige weergave: ${context.currentView}`); // Time lines.push(`⏰ Tijd: ${context.currentTime.toLocaleTimeString('nl-NL')} (${context.shift}dienst)`); // Today's agenda if (context.agendaToday.length > 0) { lines.push(`πŸ“… Agenda vandaag:`); context.agendaToday.slice(0, 5).forEach(apt => { lines.push(` - ${apt.time}: ${apt.patientName} (${apt.type})`); }); } // Recent intents if (context.recentIntents.length > 0) { lines.push(`πŸ• Recente acties:`); context.recentIntents.slice(0, 3).forEach(ri => { lines.push(` - ${ri.intent}${ri.patientName ? ` (${ri.patientName})` : ''}`); }); } return lines.join('\n'); } /** * Classify with AI Orchestrator */ export async function classifyWithOrchestrator( input: string, context: CortexContext ): Promise { const startTime = performance.now(); const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY, }); const contextPrompt = formatContextForPrompt(context); const response = await anthropic.messages.create({ model: 'claude-3-5-haiku-20241022', max_tokens: 512, temperature: 0, system: ORCHESTRATOR_SYSTEM_PROMPT, messages: [ { role: 'user', content: `## Context ${contextPrompt} ## Input "${input}" Analyseer en extraheer alle intenties.`, }, ], }); const processingTimeMs = performance.now() - startTime; const rawText = response.content[0].type === 'text' ? response.content[0].text : ''; // Parse JSON response const parsed = parseAIResponse(rawText); // Build IntentChain const chain: IntentChain = { id: crypto.randomUUID(), originalInput: input, createdAt: new Date(), actions: parsed.actions.map((action, index) => ({ id: crypto.randomUUID(), sequence: index + 1, intent: action.intent, confidence: action.confidence, entities: action.entities, status: 'pending', requiresConfirmation: action.requiresConfirmation ?? false, confirmationMessage: action.confirmationMessage, })), status: 'pending', meta: { source: 'ai', processingTimeMs, aiReasoning: parsed.reasoning, }, }; return { chain, model: 'claude-3-5-haiku-20241022', tokensUsed: response.usage.input_tokens + response.usage.output_tokens, processingTimeMs, needsClarification: parsed.needsClarification ?? false, clarificationQuestion: parsed.clarificationQuestion, clarificationOptions: parsed.clarificationOptions, }; } /** * Parse AI JSON response with error handling */ function parseAIResponse(rawText: string): { actions: Array<{ intent: CortexIntent; confidence: number; entities: ExtractedEntities; requiresConfirmation?: boolean; confirmationMessage?: string; }>; reasoning?: string; needsClarification?: boolean; clarificationQuestion?: string; clarificationOptions?: string[]; } { // Strip markdown code blocks let jsonText = rawText.trim(); if (jsonText.startsWith('```json')) { jsonText = jsonText.slice(7); } if (jsonText.startsWith('```')) { jsonText = jsonText.slice(3); } if (jsonText.endsWith('```')) { jsonText = jsonText.slice(0, -3); } try { return JSON.parse(jsonText.trim()); } catch (error) { console.error('Failed to parse AI response:', error, rawText); return { actions: [{ intent: 'unknown', confidence: 0.3, entities: {}, }], reasoning: 'Failed to parse AI response', }; } } ``` --- ## 6. Layer 3: Nudge ### 6.1 Doel Proactieve suggesties na succesvolle acties op basis van medische protocollen en domeinkennis. ### 6.2 Protocol Rules ```typescript /** * Layer 3: Nudge * * Post-action intelligence that suggests follow-up actions * based on medical protocols and domain knowledge. */ import type { IntentAction, NudgeSuggestion, ProtocolRule, ExtractedEntities } from './types'; // ============================================================================ // PROTOCOL RULES DATABASE // ============================================================================ export const PROTOCOL_RULES: ProtocolRule[] = [ // Wondzorg Protocol { id: 'wondzorg-controle', name: 'Wondcontrole na verzorging', description: 'Bij wondzorg hoort standaard een vervolgcontrole', trigger: { intent: 'dagnotitie', conditions: [ { field: 'category', operator: 'equals', value: 'adl' }, { field: 'content', operator: 'contains', value: 'wond' }, ], }, suggestion: { intent: 'create_appointment', message: 'Wondcontrole inplannen over 3 dagen?', prefillFrom: (source) => ({ patientName: source.patientName, patientId: source.patientId, appointmentType: 'follow-up', content: 'Wondcontrole', }), }, priority: 'medium', category: 'wondzorg', enabled: true, }, // Medicatie Protocol { id: 'medicatie-evaluatie', name: 'Evaluatie na medicatiestart', description: 'Nieuwe medicatie vereist evaluatie na 2 weken', trigger: { intent: 'dagnotitie', conditions: [ { field: 'category', operator: 'equals', value: 'medicatie' }, { field: 'content', operator: 'matches', value: /start|gestart|nieuw/i }, ], }, suggestion: { intent: 'create_appointment', message: 'Medicatie-evaluatie inplannen over 2 weken?', prefillFrom: (source) => ({ patientName: source.patientName, patientId: source.patientId, appointmentType: 'follow-up', content: 'Medicatie-evaluatie', }), }, priority: 'medium', category: 'medicatie', enabled: true, }, // Incident Protocol { id: 'incident-melding', name: 'Incidentmelding', description: 'Bij ernstige incidenten altijd MIC-melding overwegen', trigger: { intent: 'dagnotitie', conditions: [ { field: 'category', operator: 'equals', value: 'incident' }, { field: 'severity', operator: 'equals', value: 'high' }, ], }, suggestion: { intent: 'dagnotitie', message: 'MIC-melding aanmaken voor dit incident?', prefillFrom: (source) => ({ patientName: source.patientName, patientId: source.patientId, category: 'incident', content: `MIC: ${source.content}`, }), }, priority: 'high', category: 'veiligheid', enabled: true, }, // Crisis Protocol (van de UX simulatie) { id: 'crisis-signalering', name: 'SuΓ―cidaliteit signalering', description: 'Bij signalen van suΓ―cidaliteit crisisprotocol checken', trigger: { intent: 'dagnotitie', conditions: [ { field: 'content', operator: 'matches', value: /suΓ―cid|zelfmoord|dood|uitzichtloos|geen zin/i }, ], }, suggestion: { intent: 'unknown', // Special action: show crisis protocol message: 'Crisisprotocol raadplegen? Signaleringsplan updaten?', prefillFrom: (source) => ({ patientName: source.patientName, patientId: source.patientId, }), }, priority: 'high', category: 'veiligheid', enabled: true, }, // Afspraak verzet Protocol { id: 'afspraak-bellen', name: 'PatiΓ«nt informeren', description: 'Bij annulering patiΓ«nt informeren', trigger: { intent: 'cancel_appointment', conditions: [], }, suggestion: { intent: 'dagnotitie', message: 'Bellen genoteerd? PatiΓ«nt geΓ―nformeerd over annulering?', prefillFrom: (source) => ({ patientName: source.patientName, patientId: source.patientId, category: 'observatie', content: 'Telefonisch geΓ―nformeerd over geannuleerde afspraak', }), }, priority: 'low', category: 'administratief', enabled: true, }, ]; // ============================================================================ // NUDGE ENGINE // ============================================================================ /** * Check if a condition matches the entities */ function checkCondition( condition: ProtocolCondition, entities: ExtractedEntities ): boolean { const value = entities[condition.field]; switch (condition.operator) { case 'equals': return value === condition.value; case 'contains': return typeof value === 'string' && typeof condition.value === 'string' && value.toLowerCase().includes(condition.value.toLowerCase()); case 'exists': return value !== undefined && value !== null && value !== ''; case 'matches': return typeof value === 'string' && condition.value instanceof RegExp && condition.value.test(value); default: return false; } } /** * Evaluate all protocol rules against a completed action */ export function evaluateNudge( completedAction: IntentAction ): NudgeSuggestion[] { const suggestions: NudgeSuggestion[] = []; for (const rule of PROTOCOL_RULES) { if (!rule.enabled) continue; // Check if trigger intent matches if (rule.trigger.intent !== completedAction.intent) continue; // Check all conditions const conditionsMet = !rule.trigger.conditions || rule.trigger.conditions.every(cond => checkCondition(cond, completedAction.entities) ); if (!conditionsMet) continue; // Generate suggestion const suggestion: NudgeSuggestion = { id: crypto.randomUUID(), trigger: { actionId: completedAction.id, intent: completedAction.intent, entities: completedAction.entities, }, suggestion: { intent: rule.suggestion.intent, entities: rule.suggestion.prefillFrom(completedAction.entities), message: rule.suggestion.message, rationale: rule.description, }, status: 'pending', priority: rule.priority, expiresAt: new Date(Date.now() + 5 * 60 * 1000), // 5 minutes createdAt: new Date(), }; suggestions.push(suggestion); } // Sort by priority (high first) return suggestions.sort((a, b) => { const priorityOrder = { high: 0, medium: 1, low: 2 }; return priorityOrder[a.priority] - priorityOrder[b.priority]; }); } ``` --- ## 7. API Design ### 7.1 Intent Classification API (Enhanced) **Endpoint:** `POST /api/cortex/classify` ```typescript // app/api/cortex/classify/route.ts (V2) import { NextRequest, NextResponse } from 'next/server'; import { classifyWithReflex } from '@/lib/cortex/reflex-classifier'; import { classifyWithOrchestrator } from '@/lib/cortex/orchestrator'; import { evaluateNudge } from '@/lib/cortex/nudge'; import { extractEntities } from '@/lib/cortex/entity-extractor'; import type { ClassificationResult, CortexContext } from '@/lib/cortex/types'; // Request schema interface ClassifyRequest { input: string; context: CortexContext; options?: { forceAI?: boolean; skipSafetyNet?: boolean; }; } // Response schema interface ClassifyResponse { chain: IntentChain; handledBy: 'reflex' | 'orchestrator'; // Clarification (if needed) needsClarification?: boolean; clarificationQuestion?: string; clarificationOptions?: string[]; // Nudge suggestions (if any) suggestions?: NudgeSuggestion[]; // Debug info (dev only) debug?: object; } export async function POST(request: NextRequest) { const startTime = performance.now(); try { const body: ClassifyRequest = await request.json(); const { input, context, options } = body; // Step 1: Try Reflex Arc (Layer 1) const reflexResult = classifyWithReflex(input); let result: ClassificationResult; if (reflexResult.shouldEscalateToAI || options?.forceAI) { // Step 2: Escalate to Orchestrator (Layer 2) const aiResult = await classifyWithOrchestrator(input, context); result = { chain: aiResult.chain, handledBy: 'orchestrator', totalProcessingTimeMs: performance.now() - startTime, debug: process.env.NODE_ENV === 'development' ? { reflexResult, aiResult: { ...aiResult, chain: undefined }, } : undefined, }; // Handle clarification if (aiResult.needsClarification) { return NextResponse.json({ ...result, needsClarification: true, clarificationQuestion: aiResult.clarificationQuestion, clarificationOptions: aiResult.clarificationOptions, }); } } else { // Handle locally with Reflex Arc const entities = extractEntities(input, reflexResult.intent); result = { chain: { id: crypto.randomUUID(), originalInput: input, createdAt: new Date(), actions: [{ id: crypto.randomUUID(), sequence: 1, intent: reflexResult.intent, confidence: reflexResult.confidence, entities, status: 'pending', requiresConfirmation: false, }], status: 'pending', meta: { source: 'local', processingTimeMs: reflexResult.processingTimeMs, }, }, handledBy: 'reflex', totalProcessingTimeMs: performance.now() - startTime, }; } return NextResponse.json(result); } catch (error) { console.error('Classification error:', error); return NextResponse.json( { error: 'Classificatie mislukt' }, { status: 500 } ); } } ``` ### 7.2 Action Execution API **Endpoint:** `POST /api/cortex/execute` ```typescript // app/api/cortex/execute/route.ts interface ExecuteRequest { chainId: string; actionId: string; confirmed?: boolean; // For actions requiring confirmation } interface ExecuteResponse { success: boolean; action: IntentAction; suggestions?: NudgeSuggestion[]; // From Nudge error?: string; } ``` ### 7.3 Context API **Endpoint:** `GET /api/cortex/context` Returns current context for AI classification: - Active patient - Today's agenda - Recent actions - Current shift --- ## 8. Frontend Components ### 8.1 Component Hierarchy ``` CommandCenter (v3.0) β”œβ”€β”€ ContextBar β”œβ”€β”€ ChatPanel β”‚ β”œβ”€β”€ ChatMessages β”‚ β”‚ β”œβ”€β”€ UserMessage β”‚ β”‚ β”œβ”€β”€ AssistantMessage β”‚ β”‚ β”‚ └── ActionChainCard (NEW) β”‚ β”‚ β”‚ β”œβ”€β”€ ActionItem β”‚ β”‚ β”‚ β”œβ”€β”€ ActionItem β”‚ β”‚ β”‚ └── ConfirmationDialog β”‚ β”‚ └── ClarificationCard (NEW) β”‚ └── ChatInput β”œβ”€β”€ ArtifactArea β”‚ β”œβ”€β”€ ArtifactTabs β”‚ └── ArtifactContainer └── NudgeToast (NEW - Layer 3) ``` ### 8.2 ActionChainCard Component ```tsx // components/cortex/chat/action-chain-card.tsx 'use client'; import { useState } from 'react'; import { Check, X, Loader2, AlertCircle } from 'lucide-react'; import type { IntentChain, IntentAction } from '@/lib/cortex/types'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; interface ActionChainCardProps { chain: IntentChain; onConfirm: (actionId: string) => void; onSkip: (actionId: string) => void; onRetry: (actionId: string) => void; } const STATUS_ICONS = { pending:
, confirming: , executing: , success: , failed: , skipped: , }; const INTENT_LABELS: Record = { dagnotitie: 'Notitie', zoeken: 'Zoeken', overdracht: 'Overdracht', agenda_query: 'Agenda', create_appointment: 'Afspraak maken', cancel_appointment: 'Afspraak annuleren', reschedule_appointment: 'Afspraak verzetten', }; export function ActionChainCard({ chain, onConfirm, onSkip, onRetry }: ActionChainCardProps) { return (
{/* Header */}
{chain.actions.length} {chain.actions.length === 1 ? 'actie' : 'acties'} gedetecteerd
{/* Actions */}
{chain.actions.map((action, index) => ( onConfirm(action.id)} onSkip={() => onSkip(action.id)} onRetry={() => onRetry(action.id)} /> ))}
{/* AI Reasoning (collapsible) */} {chain.meta.aiReasoning && (
AI Redenering

{chain.meta.aiReasoning}

)}
); } function ActionItem({ action, index, onConfirm, onSkip, onRetry }: { action: IntentAction; index: number; onConfirm: () => void; onSkip: () => void; onRetry: () => void; }) { const [expanded, setExpanded] = useState(false); return (
{/* Main row */}
{/* Sequence number */} {index + 1} {/* Status icon */} {STATUS_ICONS[action.status]} {/* Intent label */} {INTENT_LABELS[action.intent] || action.intent} {/* Entity preview */} {action.entities.patientName && ( β€’ {action.entities.patientName} )} {/* Confidence badge */} = 0.9 && "bg-green-100 text-green-700", action.confidence >= 0.7 && action.confidence < 0.9 && "bg-amber-100 text-amber-700", action.confidence < 0.7 && "bg-red-100 text-red-700", )}> {Math.round(action.confidence * 100)}%
{/* Confirmation buttons */} {action.status === 'confirming' && action.confirmationMessage && (
{action.confirmationMessage}
)} {/* Error state */} {action.status === 'failed' && action.error && (
{action.error.message} {action.error.recoverable && ( )}
)}
); } ``` ### 8.3 NudgeToast Component (Layer 3 UI) ```tsx // components/cortex/command-center/nudge-toast.tsx 'use client'; import { useEffect, useState } from 'react'; import { X, Lightbulb, ArrowRight } from 'lucide-react'; import type { NudgeSuggestion } from '@/lib/cortex/types'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; interface NudgeToastProps { suggestion: NudgeSuggestion; onAccept: (suggestionId: string) => void; onDismiss: (suggestionId: string) => void; } const PRIORITY_STYLES = { high: 'border-red-200 bg-red-50', medium: 'border-amber-200 bg-amber-50', low: 'border-blue-200 bg-blue-50', }; export function NudgeToast({ suggestion, onAccept, onDismiss }: NudgeToastProps) { const [isVisible, setIsVisible] = useState(false); const [timeLeft, setTimeLeft] = useState(100); // Animate in useEffect(() => { const timer = setTimeout(() => setIsVisible(true), 100); return () => clearTimeout(timer); }, []); // Countdown timer useEffect(() => { if (!suggestion.expiresAt) return; const interval = setInterval(() => { const now = Date.now(); const expires = new Date(suggestion.expiresAt!).getTime(); const total = expires - new Date(suggestion.createdAt).getTime(); const remaining = expires - now; if (remaining <= 0) { onDismiss(suggestion.id); } else { setTimeLeft(Math.round((remaining / total) * 100)); } }, 1000); return () => clearInterval(interval); }, [suggestion, onDismiss]); return (
{/* Progress bar */}
{/* Content */}

{suggestion.suggestion.message}

{suggestion.suggestion.rationale}

{/* Actions */}
); } ``` ### 8.4 ClarificationCard Component ```tsx // components/cortex/chat/clarification-card.tsx 'use client'; import { HelpCircle } from 'lucide-react'; import { Button } from '@/components/ui/button'; interface ClarificationCardProps { question: string; options: string[]; onSelect: (option: string) => void; } export function ClarificationCard({ question, options, onSelect }: ClarificationCardProps) { return (

{question}

{options.map((option) => ( ))}
); } ``` --- ## 9. State Management ### 9.1 Enhanced Cortex Store ```typescript // stores/cortex-store.ts (V2 additions) import { create } from 'zustand'; import { devtools, persist } from 'zustand/middleware'; import type { CortexContext, IntentChain, IntentAction, NudgeSuggestion, } from '@/lib/cortex/types'; interface CortexStoreV2 { // ============ CONTEXT ============ context: CortexContext; setContext: (context: Partial) => void; // ============ INTENT CHAINS ============ activeChain: IntentChain | null; chainHistory: IntentChain[]; // Chain actions startChain: (chain: IntentChain) => void; updateActionStatus: ( chainId: string, actionId: string, status: IntentAction['status'], error?: IntentAction['error'] ) => void; confirmAction: (chainId: string, actionId: string) => void; skipAction: (chainId: string, actionId: string) => void; completeChain: (chainId: string) => void; // ============ NUDGE ============ suggestions: NudgeSuggestion[]; addSuggestion: (suggestion: NudgeSuggestion) => void; acceptSuggestion: (suggestionId: string) => void; dismissSuggestion: (suggestionId: string) => void; clearExpiredSuggestions: () => void; // ============ CLARIFICATION ============ pendingClarification: { question: string; options: string[]; originalInput: string; } | null; setClarification: (clarification: CortexStoreV2['pendingClarification']) => void; answerClarification: (answer: string) => void; } export const useCortexStoreV2 = create()( devtools( persist( (set, get) => ({ // Context context: { activePatient: null, currentView: 'dashboard', shift: 'ochtend', currentTime: new Date(), agendaToday: [], recentIntents: [], }, setContext: (partial) => set((state) => ({ context: { ...state.context, ...partial }, })), // Chains activeChain: null, chainHistory: [], startChain: (chain) => set({ activeChain: chain, }), updateActionStatus: (chainId, actionId, status, error) => set((state) => { if (state.activeChain?.id !== chainId) return state; const actions = state.activeChain.actions.map(action => action.id === actionId ? { ...action, status, error, ...(status === 'executing' ? { startedAt: new Date() } : {}), ...(status === 'success' || status === 'failed' ? { completedAt: new Date() } : {}), } : action ); return { activeChain: { ...state.activeChain, actions, }, }; }), confirmAction: (chainId, actionId) => set((state) => { if (state.activeChain?.id !== chainId) return state; return { activeChain: { ...state.activeChain, actions: state.activeChain.actions.map(action => action.id === actionId ? { ...action, status: 'executing' as const } : action ), }, }; }), skipAction: (chainId, actionId) => set((state) => { if (state.activeChain?.id !== chainId) return state; return { activeChain: { ...state.activeChain, actions: state.activeChain.actions.map(action => action.id === actionId ? { ...action, status: 'skipped' as const } : action ), }, }; }), completeChain: (chainId) => set((state) => { if (state.activeChain?.id !== chainId) return state; const completedChain = { ...state.activeChain, status: 'completed' as const, }; return { activeChain: null, chainHistory: [completedChain, ...state.chainHistory].slice(0, 20), }; }), // Suggestions suggestions: [], addSuggestion: (suggestion) => set((state) => ({ suggestions: [...state.suggestions, suggestion], })), acceptSuggestion: (suggestionId) => set((state) => ({ suggestions: state.suggestions.map(s => s.id === suggestionId ? { ...s, status: 'accepted' as const } : s ), })), dismissSuggestion: (suggestionId) => set((state) => ({ suggestions: state.suggestions.filter(s => s.id !== suggestionId), })), clearExpiredSuggestions: () => set((state) => ({ suggestions: state.suggestions.filter(s => !s.expiresAt || new Date(s.expiresAt) > new Date() ), })), // Clarification pendingClarification: null, setClarification: (clarification) => set({ pendingClarification: clarification, }), answerClarification: (answer) => { const { pendingClarification } = get(); if (!pendingClarification) return; // Re-classify with the clarified input // This would trigger a new classification request set({ pendingClarification: null }); }, }), { name: 'cortex-store-v2', partialize: (state) => ({ chainHistory: state.chainHistory.slice(0, 10), }), } ), { name: 'cortex-v2' } ) ); ``` --- ## 10. Implementatie Roadmap ### Fase 1: Hybrid Foundation (Week 1-2) | # | Task | Beschrijving | Effort | |---|------|--------------|--------| | 1.1 | Context Types | Nieuwe types voor CortexContext | S | | 1.2 | Context API | GET /api/cortex/context endpoint | M | | 1.3 | Context Injection | Update AI classifier om context te ontvangen | M | | 1.4 | Reflex Complexity Detection | Multi-intent en context signals detectie | S | | 1.5 | Clarification UI | ClarificationCard component | S | **Deliverable:** AI begrijpt context en geeft verduidelijkingsvragen ### Fase 2: Multi-Intent Support (Week 3-4) | # | Task | Beschrijving | Effort | |---|------|--------------|--------| | 2.1 | IntentChain Types | Nieuwe types voor chains en actions | M | | 2.2 | Orchestrator Prompt | AI prompt voor multi-intent parsing | M | | 2.3 | Chain Store | Zustand state voor chains | M | | 2.4 | ActionChainCard | UI voor meerdere acties | L | | 2.5 | Chain Execution | Sequential action execution met confirmations | L | **Deliverable:** "Zeg Jan af en maak notitie" werkt ### Fase 3: Nudge (Week 5-6) | # | Task | Beschrijving | Effort | |---|------|--------------|--------| | 3.1 | Protocol Rules | Rule definitions voor wondzorg, medicatie | M | | 3.2 | Nudge Engine | evaluateNudge functie | M | | 3.3 | NudgeToast | UI component met timer | M | | 3.4 | Suggestion Flow | Accept/dismiss handling | S | | 3.5 | Protocol Admin | Admin UI voor regels (optional) | L | **Deliverable:** Proactieve suggesties na acties ### Fase 4: Polish & Optimization (Week 7-8) | # | Task | Beschrijving | Effort | |---|------|--------------|--------| | 4.1 | Error Handling | Graceful degradation, rollbacks | M | | 4.2 | Performance | Caching, parallel requests | M | | 4.3 | Analytics | Telemetry voor classificatie success | S | | 4.4 | Testing | E2E tests voor hele flow | L | | 4.5 | Documentation | API docs, user guide | S | --- ## 11. Testing Strategy ### 11.1 Unit Tests ```typescript // lib/cortex/__tests__/reflex-classifier.test.ts import { classifyWithReflex } from '../reflex-classifier'; describe('Reflex Classifier', () => { describe('Simple intents (should NOT escalate)', () => { it('handles "notitie jan medicatie"', () => { const result = classifyWithReflex('notitie jan medicatie'); expect(result.intent).toBe('dagnotitie'); expect(result.confidence).toBeGreaterThanOrEqual(0.7); expect(result.shouldEscalateToAI).toBe(false); }); it('handles "agenda vandaag"', () => { const result = classifyWithReflex('agenda vandaag'); expect(result.intent).toBe('agenda_query'); expect(result.shouldEscalateToAI).toBe(false); }); }); describe('Complex intents (SHOULD escalate)', () => { it('escalates multi-intent: "zeg jan af en maak notitie"', () => { const result = classifyWithReflex('zeg jan af en maak notitie'); expect(result.shouldEscalateToAI).toBe(true); expect(result.escalationReason).toBe('multi_intent_detected'); }); it('escalates context-dependent: "maak notitie voor hem"', () => { const result = classifyWithReflex('maak notitie voor hem'); expect(result.shouldEscalateToAI).toBe(true); expect(result.escalationReason).toBe('needs_context'); }); it('escalates low confidence', () => { const result = classifyWithReflex('help jan met iets'); expect(result.shouldEscalateToAI).toBe(true); expect(result.escalationReason).toBe('low_confidence'); }); }); }); ``` ### 11.2 Integration Tests ```typescript // __tests__/api/intent-classify.test.ts describe('POST /api/cortex/classify', () => { describe('Multi-intent handling', () => { it('returns chain with multiple actions', async () => { const response = await fetch('/api/cortex/classify', { method: 'POST', body: JSON.stringify({ input: 'Zeg de afspraak van Jan af en maak een notitie dat hij ziek is', context: mockContext, }), }); const data = await response.json(); expect(data.chain.actions).toHaveLength(2); expect(data.chain.actions[0].intent).toBe('cancel_appointment'); expect(data.chain.actions[1].intent).toBe('dagnotitie'); }); }); describe('Context resolution', () => { it('resolves "hij" to active patient', async () => { const response = await fetch('/api/cortex/classify', { method: 'POST', body: JSON.stringify({ input: 'Maak notitie voor hem', context: { ...mockContext, activePatient: { id: '123', name: 'Jan de Vries' }, }, }), }); const data = await response.json(); expect(data.chain.actions[0].entities.patientName).toBe('Jan de Vries'); expect(data.chain.actions[0].entities.patientResolution).toBe('pronoun'); }); }); }); ``` ### 11.3 Test Zinnen Dataset ```json // lib/cortex/__tests__/test-sentences.json { "single_intent": [ { "input": "notitie jan medicatie", "expected": ["dagnotitie"] }, { "input": "zoek marie", "expected": ["zoeken"] }, { "input": "agenda morgen", "expected": ["agenda_query"] } ], "multi_intent": [ { "input": "Zeg Jan af en maak notitie dat hij griep heeft", "expected": ["cancel_appointment", "dagnotitie"], "entities": [ { "patientName": "Jan" }, { "patientName": "Jan", "content": "griep" } ] }, { "input": "Plan intake marie morgen 14:00 en bel haar huisarts", "expected": ["create_appointment", "dagnotitie"], "entities": [ { "patientName": "Marie", "time": "14:00" }, { "patientName": "Marie" } ] } ], "context_dependent": [ { "input": "Maak notitie voor hem", "context": { "activePatient": { "name": "Piet" } }, "expected": ["dagnotitie"], "entities": [{ "patientName": "Piet", "patientResolution": "pronoun" }] } ], "ambiguous": [ { "input": "Plan wondzorg", "shouldClarify": true, "clarificationOptions": ["Notitie maken", "Afspraak inplannen"] } ] } ``` --- ## 12. Migratie Plan ### 12.1 Backward Compatibility Het V2 systeem moet naast V1 kunnen draaien tijdens de migratie: ```typescript // lib/cortex/intent-classifier-adapter.ts import { classifyIntent as classifyV1 } from './intent-classifier'; import { classifyWithReflex } from './reflex-classifier'; import { classifyWithOrchestrator } from './orchestrator'; const USE_V2 = process.env.NEXT_PUBLIC_CORTEX_V2 === 'true'; export async function classifyIntent( input: string, context?: CortexContext ): Promise { if (!USE_V2) { // V1 path - single intent const result = classifyV1(input); return { chain: { id: crypto.randomUUID(), originalInput: input, actions: [{ id: crypto.randomUUID(), sequence: 1, intent: result.intent, confidence: result.confidence, entities: {}, status: 'pending', requiresConfirmation: false, }], status: 'pending', meta: { source: 'local', processingTimeMs: result.processingTimeMs }, createdAt: new Date(), }, handledBy: 'reflex', totalProcessingTimeMs: result.processingTimeMs, }; } // V2 path - full cortex const reflex = classifyWithReflex(input); if (!reflex.shouldEscalateToAI) { // Handle locally return buildLocalResult(input, reflex); } // Escalate to AI return classifyWithOrchestrator(input, context!); } ``` ### 12.2 Feature Flags ```typescript // lib/config/feature-flags.ts export const FEATURE_FLAGS = { // V2 Features CORTEX_V2_ENABLED: process.env.NEXT_PUBLIC_CORTEX_V2 === 'true', CORTEX_MULTI_INTENT: process.env.NEXT_PUBLIC_CORTEX_MULTI_INTENT === 'true', CORTEX_NUDGE: process.env.NEXT_PUBLIC_CORTEX_NUDGE === 'true', CORTEX_CONTEXT_INJECTION: process.env.NEXT_PUBLIC_CORTEX_CONTEXT === 'true', // Rollout percentage (A/B testing) CORTEX_V2_ROLLOUT: parseInt(process.env.NEXT_PUBLIC_CORTEX_V2_ROLLOUT || '0', 10), }; export function isCortexV2Enabled(userId?: string): boolean { if (!FEATURE_FLAGS.CORTEX_V2_ENABLED) return false; // A/B test based on user ID hash if (userId && FEATURE_FLAGS.CORTEX_V2_ROLLOUT < 100) { const hash = simpleHash(userId); return hash % 100 < FEATURE_FLAGS.CORTEX_V2_ROLLOUT; } return true; } ``` ### 12.3 Rollout Strategie | Week | Rollout % | Features | Monitoring | |------|-----------|----------|------------| | 1 | 0% (dev only) | Context injection | Error rates | | 2 | 10% | + Multi-intent | Classification accuracy | | 3 | 25% | + Safety Net | Suggestion acceptance rate | | 4 | 50% | Full V2 | User feedback | | 5 | 75% | Full V2 | Performance metrics | | 6 | 100% | Full V2 | Final review | --- ## Bijlagen ### A. Glossary | Term | Definitie | |------|-----------| | **Intent** | De actie die de gebruiker wil uitvoeren (bijv. "dagnotitie") | | **IntentChain** | Lijst van intents geëxtraheerd uit één uiting | | **Reflex Arc** | Layer 1 - snelle lokale pattern matching | | **Orchestrator** | Layer 2 - AI-gedreven classificatie | | **Nudge** | Layer 3 - proactieve suggesties | | **Entity** | Geëxtraheerde data (patiëntnaam, datum, etc.) | | **Artifact** | UI component voor een specifieke taak | ### B. Referenties - [FO Cortex Intent System V2](./fo-cortex-intent-system-v2.md) - [UX Simulatie Next Level](./ux-simulation-intent-next-level.md) - [UX Evaluatie Schaalbaarheid](./ux-evaluation-intent-scalability.md) - [Architecture Proposal V2](./intent-architecture-v2-proposal.md) ### C. Versie Historie | Versie | Datum | Auteur | Wijzigingen | |--------|-------|--------|-------------| | 2.0 | 29-12-2025 | Colin Lit | Initieel document |