Files
triqura-ecd/docs/intent/architecture-swift-cortex-v2.md
colinislit c8aaba657e refactor(swift): update references from Medical Scribe to Swift Assistent
- Updated comments and documentation to reflect the new branding of the chat API and related components.
- Renamed functions and variables to align with the Swift Assistent terminology.
- Enhanced UI components with animations using Framer Motion for a smoother user experience.
- Removed outdated architecture documentation related to the Medical Scribe system.

This change is part of the transition to the Swift Assistent branding, ensuring consistency across the application.
2025-12-29 22:40:34 +01:00

66 KiB

🧠 Swift Cortex V2 - Uitgebreid Architectuurplan

Project: Swift Intent System V2
Versie: 2.0
Datum: 29-12-2025
Status: Technisch Ontwerp


Inhoudsopgave

  1. Executive Summary
  2. Architectuur Overview
  3. Data Models & Types
  4. Layer 1: Reflex Arc
  5. Layer 2: Intent Orchestrator
  6. Layer 3: Safety Net
  7. API Design
  8. Frontend Components
  9. State Management
  10. Implementatie Roadmap
  11. Testing Strategy
  12. Migratie Plan

1. Executive Summary

Huidige Situatie (V1)

Het huidige Swift 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 Swift 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.9)                       │   │
│  │  • Examples: "agenda", "zoek jan", "notitie"                         │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  Decision: Confidence > 0.9 AND single intent? ──► EXECUTE DIRECTLY        │
│            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: SAFETY NET (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   │    │   Net     │
└──────────┘    └─────────┘    └─────────────┘    └──────────┘    └───────────┘
                                     │                  │               │
                                     ▼                  ▼               ▼
                              ┌─────────────┐    ┌──────────┐    ┌───────────┐
                              │   Swift     │    │  Artifact │    │ Suggestion│
                              │   Store     │◄───│  Updates  │    │   Toast   │
                              └─────────────┘    └──────────┘    └───────────┘

3. Data Models & Types

3.1 Core Types (lib/swift/types.ts)

// ============================================================================
// INTENT TYPES
// ============================================================================

export type SwiftIntent =
  | '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 SwiftContext {
  // 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: SwiftIntent;
    patientName?: string;
    timestamp: Date;
  }[];

  // User preferences (adaptive confidence)
  userPreferences?: {
    confirmationLevel: 'always' | 'destructive' | 'never';
    frequentIntents: SwiftIntent[];
  };
}

// ============================================================================
// 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: SwiftIntent;
  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<string, unknown>;
}

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';

// ============================================================================
// SAFETY NET TYPES (NEW in V2)
// ============================================================================

/**
 * A suggestion generated by the Safety Net
 */
export interface SafetySuggestion {
  id: string;
  
  // What triggered this suggestion
  trigger: {
    actionId: string;
    intent: SwiftIntent;
    entities: ExtractedEntities;
  };
  
  // The suggestion itself
  suggestion: {
    intent: SwiftIntent;
    entities: Partial<ExtractedEntities>;
    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: SwiftIntent;
    conditions?: ProtocolCondition[];
  };
  
  // What to suggest
  suggestion: {
    intent: SwiftIntent;
    message: string;
    prefillFrom: (source: ExtractedEntities) => Partial<ExtractedEntities>;
  };
  
  // 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: SwiftIntent;
  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/swift-store.ts additions)

// Add to existing SwiftStore interface

interface SwiftStoreV2 extends SwiftStore {
  // Context (enhanced)
  context: SwiftContext;
  
  // Intent Chain state
  activeChain: IntentChain | null;
  chainHistory: IntentChain[];
  
  // Safety Net state
  pendingSuggestions: SafetySuggestion[];
  suggestionHistory: SafetySuggestion[];
  
  // Actions
  setContext: (context: Partial<SwiftContext>) => void;
  
  // Chain actions
  startChain: (chain: IntentChain) => void;
  updateActionStatus: (chainId: string, actionId: string, status: IntentAction['status']) => void;
  completeChain: (chainId: string) => void;
  
  // Safety Net actions
  addSuggestion: (suggestion: SafetySuggestion) => 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.9
  • 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/swift/reflex-classifier.ts)

/**
 * Layer 1: Reflex Arc
 * 
 * Fast, local pattern matching for simple commands.
 * Escalates to Layer 2 when complexity is detected.
 */

import type { LocalClassificationResult, SwiftIntent } 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<SwiftIntent, Array<{ pattern: RegExp; weight: number }>> = {
  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: SwiftIntent; 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 SwiftIntent,
            confidence: weight,
            pattern: pattern.source,
          };
        }
      }
    }
  }
  
  const processingTimeMs = performance.now() - startTime;
  
  // Step 3: Decide on escalation
  if (!bestMatch || bestMatch.confidence < 0.9) {
    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:

/**
 * Build context for AI classification
 */
export function buildSwiftContext(store: SwiftStore): SwiftContext {
  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/swift/orchestrator.ts)

/**
 * Layer 2: Intent Orchestrator
 * 
 * AI-powered classification with context awareness and multi-intent support.
 */

import Anthropic from '@anthropic-ai/sdk';
import type { 
  SwiftContext, 
  IntentChain, 
  IntentAction,
  AIClassificationResult 
} from './types';

const ORCHESTRATOR_SYSTEM_PROMPT = `Je bent de Intent Orchestrator voor Swift, 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: SwiftContext): 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: SwiftContext
): Promise<AIClassificationResult> {
  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: SwiftIntent;
    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: Safety Net

6.1 Doel

Proactieve suggesties na succesvolle acties op basis van medische protocollen en domeinkennis.

6.2 Protocol Rules

/**
 * Layer 3: Safety Net
 * 
 * Post-action intelligence that suggests follow-up actions
 * based on medical protocols and domain knowledge.
 */

import type { 
  IntentAction, 
  SafetySuggestion, 
  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,
  },
];

// ============================================================================
// SAFETY NET 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 evaluateSafetyNet(
  completedAction: IntentAction
): SafetySuggestion[] {
  const suggestions: SafetySuggestion[] = [];
  
  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: SafetySuggestion = {
      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/intent/classify

// app/api/intent/classify/route.ts (V2)

import { NextRequest, NextResponse } from 'next/server';
import { classifyWithReflex } from '@/lib/swift/reflex-classifier';
import { classifyWithOrchestrator } from '@/lib/swift/orchestrator';
import { evaluateSafetyNet } from '@/lib/swift/safety-net';
import { extractEntities } from '@/lib/swift/entity-extractor';
import type { ClassificationResult, SwiftContext } from '@/lib/swift/types';

// Request schema
interface ClassifyRequest {
  input: string;
  context: SwiftContext;
  options?: {
    forceAI?: boolean;
    skipSafetyNet?: boolean;
  };
}

// Response schema
interface ClassifyResponse {
  chain: IntentChain;
  handledBy: 'reflex' | 'orchestrator';
  
  // Clarification (if needed)
  needsClarification?: boolean;
  clarificationQuestion?: string;
  clarificationOptions?: string[];
  
  // Safety Net suggestions (if any)
  suggestions?: SafetySuggestion[];
  
  // 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/intent/execute

// app/api/intent/execute/route.ts

interface ExecuteRequest {
  chainId: string;
  actionId: string;
  confirmed?: boolean;  // For actions requiring confirmation
}

interface ExecuteResponse {
  success: boolean;
  action: IntentAction;
  suggestions?: SafetySuggestion[];  // From Safety Net
  error?: string;
}

7.3 Context API

Endpoint: GET /api/swift/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
└── SuggestionToast (NEW - Layer 3)

8.2 ActionChainCard Component

// components/swift/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/swift/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: <div className="w-2 h-2 rounded-full bg-slate-300" />,
  confirming: <AlertCircle className="w-4 h-4 text-amber-500" />,
  executing: <Loader2 className="w-4 h-4 text-blue-500 animate-spin" />,
  success: <Check className="w-4 h-4 text-green-500" />,
  failed: <X className="w-4 h-4 text-red-500" />,
  skipped: <X className="w-4 h-4 text-slate-400" />,
};

const INTENT_LABELS: Record<string, string> = {
  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 (
    <div className="bg-white rounded-lg border border-slate-200 overflow-hidden">
      {/* Header */}
      <div className="px-4 py-2 bg-slate-50 border-b border-slate-200">
        <span className="text-sm text-slate-600">
          {chain.actions.length} {chain.actions.length === 1 ? 'actie' : 'acties'} gedetecteerd
        </span>
      </div>
      
      {/* Actions */}
      <div className="divide-y divide-slate-100">
        {chain.actions.map((action, index) => (
          <ActionItem
            key={action.id}
            action={action}
            index={index}
            onConfirm={() => onConfirm(action.id)}
            onSkip={() => onSkip(action.id)}
            onRetry={() => onRetry(action.id)}
          />
        ))}
      </div>
      
      {/* AI Reasoning (collapsible) */}
      {chain.meta.aiReasoning && (
        <details className="px-4 py-2 bg-slate-50 border-t border-slate-200">
          <summary className="text-xs text-slate-500 cursor-pointer">
            AI Redenering
          </summary>
          <p className="mt-1 text-xs text-slate-600">
            {chain.meta.aiReasoning}
          </p>
        </details>
      )}
    </div>
  );
}

function ActionItem({ 
  action, 
  index,
  onConfirm,
  onSkip,
  onRetry
}: { 
  action: IntentAction; 
  index: number;
  onConfirm: () => void;
  onSkip: () => void;
  onRetry: () => void;
}) {
  const [expanded, setExpanded] = useState(false);
  
  return (
    <div className={cn(
      "px-4 py-3",
      action.status === 'confirming' && "bg-amber-50",
      action.status === 'failed' && "bg-red-50",
    )}>
      {/* Main row */}
      <div className="flex items-center gap-3">
        {/* Sequence number */}
        <span className="w-6 h-6 rounded-full bg-slate-100 flex items-center justify-center text-xs font-medium text-slate-600">
          {index + 1}
        </span>
        
        {/* Status icon */}
        {STATUS_ICONS[action.status]}
        
        {/* Intent label */}
        <span className="font-medium text-slate-900">
          {INTENT_LABELS[action.intent] || action.intent}
        </span>
        
        {/* Entity preview */}
        {action.entities.patientName && (
          <span className="text-slate-500">
             {action.entities.patientName}
          </span>
        )}
        
        {/* Confidence badge */}
        <span className={cn(
          "ml-auto text-xs px-2 py-0.5 rounded-full",
          action.confidence >= 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)}%
        </span>
      </div>
      
      {/* Confirmation buttons */}
      {action.status === 'confirming' && action.confirmationMessage && (
        <div className="mt-3 flex items-center gap-2">
          <span className="text-sm text-amber-700">
            {action.confirmationMessage}
          </span>
          <Button size="sm" onClick={onConfirm}>
            Bevestig
          </Button>
          <Button size="sm" variant="ghost" onClick={onSkip}>
            Overslaan
          </Button>
        </div>
      )}
      
      {/* Error state */}
      {action.status === 'failed' && action.error && (
        <div className="mt-3 flex items-center gap-2">
          <span className="text-sm text-red-700">
            {action.error.message}
          </span>
          {action.error.recoverable && (
            <Button size="sm" variant="outline" onClick={onRetry}>
              Opnieuw
            </Button>
          )}
        </div>
      )}
    </div>
  );
}

8.3 SuggestionToast Component (Layer 3 UI)

// components/swift/suggestion-toast.tsx

'use client';

import { useEffect, useState } from 'react';
import { X, Lightbulb, ArrowRight } from 'lucide-react';
import type { SafetySuggestion } from '@/lib/swift/types';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';

interface SuggestionToastProps {
  suggestion: SafetySuggestion;
  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 SuggestionToast({ 
  suggestion, 
  onAccept, 
  onDismiss 
}: SuggestionToastProps) {
  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 (
    <div
      className={cn(
        "fixed bottom-4 right-4 max-w-sm rounded-lg border shadow-lg transition-all duration-300",
        PRIORITY_STYLES[suggestion.priority],
        isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"
      )}
    >
      {/* Progress bar */}
      <div className="absolute top-0 left-0 right-0 h-1 bg-slate-200 rounded-t-lg overflow-hidden">
        <div 
          className="h-full bg-slate-400 transition-all duration-1000"
          style={{ width: `${timeLeft}%` }}
        />
      </div>
      
      {/* Content */}
      <div className="p-4 pt-5">
        <div className="flex items-start gap-3">
          <Lightbulb className="w-5 h-5 text-amber-500 flex-shrink-0 mt-0.5" />
          
          <div className="flex-1">
            <p className="text-sm font-medium text-slate-900">
              {suggestion.suggestion.message}
            </p>
            <p className="mt-1 text-xs text-slate-500">
              {suggestion.suggestion.rationale}
            </p>
          </div>
          
          <button
            onClick={() => onDismiss(suggestion.id)}
            className="text-slate-400 hover:text-slate-600"
          >
            <X className="w-4 h-4" />
          </button>
        </div>
        
        {/* Actions */}
        <div className="mt-3 flex items-center gap-2">
          <Button 
            size="sm" 
            onClick={() => onAccept(suggestion.id)}
            className="gap-1"
          >
            Ja, doe maar
            <ArrowRight className="w-3 h-3" />
          </Button>
          <Button 
            size="sm" 
            variant="ghost"
            onClick={() => onDismiss(suggestion.id)}
          >
            Nee, bedankt
          </Button>
        </div>
      </div>
    </div>
  );
}

8.4 ClarificationCard Component

// components/swift/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 (
    <div className="bg-blue-50 rounded-lg border border-blue-200 p-4">
      <div className="flex items-start gap-3">
        <HelpCircle className="w-5 h-5 text-blue-500 flex-shrink-0 mt-0.5" />
        
        <div>
          <p className="text-sm text-slate-700">{question}</p>
          
          <div className="mt-3 flex flex-wrap gap-2">
            {options.map((option) => (
              <Button
                key={option}
                size="sm"
                variant="outline"
                onClick={() => onSelect(option)}
                className="bg-white"
              >
                {option}
              </Button>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
}

9. State Management

9.1 Enhanced Swift Store

// stores/swift-store.ts (V2 additions)

import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import type {
  SwiftContext,
  IntentChain,
  IntentAction,
  SafetySuggestion,
} from '@/lib/swift/types';

interface SwiftStoreV2 {
  // ============ CONTEXT ============
  context: SwiftContext;
  setContext: (context: Partial<SwiftContext>) => 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;
  
  // ============ SAFETY NET ============
  suggestions: SafetySuggestion[];
  
  addSuggestion: (suggestion: SafetySuggestion) => void;
  acceptSuggestion: (suggestionId: string) => void;
  dismissSuggestion: (suggestionId: string) => void;
  clearExpiredSuggestions: () => void;
  
  // ============ CLARIFICATION ============
  pendingClarification: {
    question: string;
    options: string[];
    originalInput: string;
  } | null;
  
  setClarification: (clarification: SwiftStoreV2['pendingClarification']) => void;
  answerClarification: (answer: string) => void;
}

export const useSwiftStoreV2 = create<SwiftStoreV2>()(
  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: 'swift-store-v2',
        partialize: (state) => ({
          chainHistory: state.chainHistory.slice(0, 10),
        }),
      }
    ),
    { name: 'swift-v2' }
  )
);

10. Implementatie Roadmap

Fase 1: Hybrid Foundation (Week 1-2)

# Task Beschrijving Effort
1.1 Context Types Nieuwe types voor SwiftContext S
1.2 Context API GET /api/swift/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: Safety Net (Week 5-6)

# Task Beschrijving Effort
3.1 Protocol Rules Rule definitions voor wondzorg, medicatie M
3.2 Safety Net Engine evaluateSafetyNet functie M
3.3 SuggestionToast 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

// lib/swift/__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.9);
      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

// __tests__/api/intent-classify.test.ts

describe('POST /api/intent/classify', () => {
  describe('Multi-intent handling', () => {
    it('returns chain with multiple actions', async () => {
      const response = await fetch('/api/intent/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/intent/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

// lib/swift/__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:

// lib/swift/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_SWIFT_V2 === 'true';

export async function classifyIntent(
  input: string,
  context?: SwiftContext
): Promise<ClassificationResult> {
  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

// lib/config/feature-flags.ts

export const FEATURE_FLAGS = {
  // V2 Features
  SWIFT_V2_ENABLED: process.env.NEXT_PUBLIC_SWIFT_V2 === 'true',
  SWIFT_MULTI_INTENT: process.env.NEXT_PUBLIC_SWIFT_MULTI_INTENT === 'true',
  SWIFT_SAFETY_NET: process.env.NEXT_PUBLIC_SWIFT_SAFETY_NET === 'true',
  SWIFT_CONTEXT_INJECTION: process.env.NEXT_PUBLIC_SWIFT_CONTEXT === 'true',
  
  // Rollout percentage (A/B testing)
  SWIFT_V2_ROLLOUT: parseInt(process.env.NEXT_PUBLIC_SWIFT_V2_ROLLOUT || '0', 10),
};

export function isSwiftV2Enabled(userId?: string): boolean {
  if (!FEATURE_FLAGS.SWIFT_V2_ENABLED) return false;
  
  // A/B test based on user ID hash
  if (userId && FEATURE_FLAGS.SWIFT_V2_ROLLOUT < 100) {
    const hash = simpleHash(userId);
    return hash % 100 < FEATURE_FLAGS.SWIFT_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
Safety Net Layer 3 - proactieve suggesties
Entity Geëxtraheerde data (patiëntnaam, datum, etc.)
Artifact UI component voor een specifieke taak

B. Referenties

C. Versie Historie

Versie Datum Auteur Wijzigingen
2.0 29-12-2025 Colin Lit Initieel document