- 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.
66 KiB
🧠 Swift Cortex V2 - Uitgebreid Architectuurplan
Project: Swift Intent System V2
Versie: 2.0
Datum: 29-12-2025
Status: Technisch Ontwerp
Inhoudsopgave
- Executive Summary
- Architectuur Overview
- Data Models & Types
- Layer 1: Reflex Arc
- Layer 2: Intent Orchestrator
- Layer 3: Safety Net
- API Design
- Frontend Components
- State Management
- Implementatie Roadmap
- Testing Strategy
- 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
- FO Swift Intent System V2
- UX Simulatie Next Level
- UX Evaluatie Schaalbaarheid
- Architecture Proposal V2
C. Versie Historie
| Versie | Datum | Auteur | Wijzigingen |
|---|---|---|---|
| 2.0 | 29-12-2025 | Colin Lit | Initieel document |