New documents: - prd-cortex-v2.md: Product Requirements Document with MVP scope, user flows, success criteria, and roadmap - to-cortex-v2.md: Technical Design with three-layer architecture, API design, error handling, and logging Renamed files (swift → cortex): - architecture-swift-cortex-v2.md → architecture-cortex-v2.md - fo-swift-intent-system-v2.md → fo-cortex-intent-system-v2.md - haalbaarheidsanalyse-swift-cortex-v2.md → haalbaarheidsanalyse-cortex-v2.md - review-swift-cortex-v2.md → review-cortex-v2.md Updated content: - All swift references replaced with cortex - FO: confidence threshold 0.9 → 0.7 (industry standard) - TO v1.1: Added error handling, input sanitization, improved suïcidaliteit regex, model version note - Legacy docs: Added note about swift → cortex rename 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
44 KiB
Technisch Ontwerp (TO) – Cortex V2
Projectnaam: Cortex Intent System V2 Versie: v1.1 Datum: 30-12-2025 Auteur: Colin Lit (Antigravity AI)
1. Doel en relatie met PRD en FO
Doel van dit document: Dit Technisch Ontwerp (TO) beschrijft hoe het Cortex V2 systeem technisch wordt gebouwd. Het document vertaalt de functionele specificaties uit het FO naar concrete architectuur, techstack, datamodellen en implementatiedetails.
Relatie met andere documenten:
- FO:
fo-cortex-intent-system-v2.md- Beschrijft het wat en waarom - Architectuur:
architecture-cortex-v2.md- High-level architectuurvisie - MVP Scope:
mvp-userstories-intent-system.md- User stories en acceptatiecriteria - Haalbaarheid:
haalbaarheidsanalyse-cortex-v2.md- Gap analyse en effort schatting
Kernprincipe:
"We stoppen met optimaliseren voor milliseconden en starten met optimaliseren voor intelligentie."
2. Technische Architectuur Overzicht
2.1 The Three-Layer Cortex Model
┌─────────────────────────────────────────────────────────────────────────────┐
│ USER INPUT │
│ "Zeg Jan af en maak notitie: grieperig" │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ LAYER 1: REFLEX ARC [<20ms] │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ • Local Regex Pattern Matching │ │
│ │ • High-confidence simple commands only (>= 0.7) │ │
│ │ • Examples: "agenda", "zoek jan", "notitie" │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ Decision: Confidence >= 0.7 AND no escalation triggers? → EXECUTE │
│ Otherwise → Pass to Layer 2 │
└─────────────────────────────────────────────────────────────────────────────┘
│
[Complex/Multi-intent/Low confidence/Ambiguous]
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ LAYER 2: INTENT ORCHESTRATOR (AI CORTEX) [~400ms] │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ • Claude 3.5 Haiku (fast, cheap, smart) │ │
│ │ • Context injection: ActivePatient, CurrentView, Agenda, History │ │
│ │ • Multi-intent parsing: splits "X en Y" into action chain │ │
│ │ • Entity disambiguation: "hij" → active patient │ │
│ │ • Clarification questions if truly ambiguous │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ Output: IntentChain { actions: [Action1, Action2, ...] } │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ EXECUTION ENGINE │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ • Sequential or parallel action execution │ │
│ │ • Confirmation dialogs for destructive actions │ │
│ │ • Rollback support for failed chains │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
[Action Completed]
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ LAYER 3: NUDGE (POST-ACTION INTELLIGENCE) [async] │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ • Protocol Rules Engine: medical domain knowledge │ │
│ │ • Trigger evaluation: "Does this action warrant a follow-up?" │ │
│ │ • Suggestion generation: "Wondcontrole inplannen?" │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ Output: Suggestion Toast / Follow-up Card │
└─────────────────────────────────────────────────────────────────────────────┘
2.2 Data Flow Diagram
┌──────────┐ ┌─────────┐ ┌─────────────┐ ┌──────────┐ ┌───────────┐
│ Speech │───►│ Input │───►│ Classify │───►│ Execute │───►│ Nudge │
│ /Text │ │ Buffer │ │ (L1/L2) │ │ Chain │ │ Engine │
└──────────┘ └─────────┘ └─────────────┘ └──────────┘ └───────────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌──────────┐ ┌───────────┐
│ Cortex │ │ Artifact │ │ Suggestion│
│ Store │◄───│ Updates │ │ Toast │
└─────────────┘ └──────────┘ └───────────┘
3. Techstack Selectie
| Component | Technologie | Argumentatie | Alternatieven |
|---|---|---|---|
| Frontend | Next.js 15 | React framework, App Router, TypeScript | SvelteKit, Remix |
| Backend | Next.js API Routes | Co-located met frontend, Server Actions | Express, FastAPI |
| Database | Supabase (PostgreSQL) | Realtime, RLS, auth included | Firebase, PlanetScale |
| AI Model | Claude 3.5 Haiku | Snel (~400ms), goedkoop, Nederlands | GPT-4o-mini, Gemini Flash |
| State Management | Zustand | Lightweight, devtools, persist | Redux, Jotai |
| Styling | TailwindCSS | Utility-first, shadcn/ui compatibel | styled-components |
| Hosting | Vercel | Zero-config Next.js, edge functions | Netlify, Railway |
4. Datamodel
4.1 Core Types (lib/cortex/types.ts)
// ============================================================================
// THRESHOLDS & CONSTANTS
// ============================================================================
/** Minimum score om lokaal af te handelen (industry standard: 0.7) */
export const CONFIDENCE_THRESHOLD = 0.7;
/** Minimum verschil tussen top-2 scores om niet-ambigue te zijn */
export const AMBIGUITY_THRESHOLD = 0.1;
// ============================================================================
// INTENT TYPES
// ============================================================================
export type CortexIntent =
| 'dagnotitie'
| 'zoeken'
| 'overdracht'
| 'agenda_query'
| 'create_appointment'
| 'cancel_appointment'
| 'reschedule_appointment'
| 'unknown';
export type EscalationReason =
| 'low_confidence'
| 'ambiguous'
| 'multi_intent_detected'
| 'needs_context'
| 'relative_time';
// ============================================================================
// CONTEXT TYPES
// ============================================================================
export interface CortexContext {
activePatient: {
id: string;
name: string;
recentNotes?: string[];
upcomingAppointments?: {
date: Date;
type: string;
}[];
} | null;
currentView: 'dashboard' | 'patient-detail' | 'agenda' | 'reports' | 'chat';
shift: ShiftType;
currentTime: Date;
agendaToday: {
time: string;
patientName: string;
patientId: string;
type: string;
}[];
recentIntents: {
intent: CortexIntent;
patientName?: string;
timestamp: Date;
}[];
userPreferences?: {
confirmationLevel: 'always' | 'destructive' | 'never';
frequentIntents: CortexIntent[];
};
}
// ============================================================================
// CLASSIFICATION RESULT TYPES
// ============================================================================
export interface LocalClassificationResult {
intent: CortexIntent;
confidence: number;
/** Op-één-na-beste match voor ambiguity detection */
secondBestIntent?: CortexIntent;
secondBestConfidence?: number;
matchedPattern?: string;
processingTimeMs: number;
shouldEscalateToAI: boolean;
escalationReason?: EscalationReason;
}
// ============================================================================
// INTENT CHAIN (Multi-Intent Support)
// ============================================================================
export interface IntentChain {
id: string;
originalInput: string;
createdAt: Date;
actions: IntentAction[];
status: 'pending' | 'executing' | 'completed' | 'partial' | 'failed';
meta: {
source: 'local' | 'ai';
processingTimeMs: number;
aiReasoning?: string;
};
}
export interface IntentAction {
id: string;
sequence: number;
intent: CortexIntent;
confidence: number;
entities: ExtractedEntities;
status: 'pending' | 'confirming' | 'executing' | 'success' | 'failed' | 'skipped';
requiresConfirmation: boolean;
confirmationMessage?: string;
error?: {
code: string;
message: string;
recoverable: boolean;
};
startedAt?: Date;
completedAt?: Date;
}
// ============================================================================
// ENTITY TYPES
// ============================================================================
export interface ExtractedEntities {
patientName?: string;
patientId?: string;
patientResolution?: 'explicit' | 'context' | 'pronoun';
category?: VerpleegkundigCategory;
content?: string;
severity?: 'low' | 'medium' | 'high';
query?: string;
dateRange?: DateRange;
datetime?: {
date: Date;
time: string;
isRelative: boolean;
};
appointmentType?: AppointmentType;
location?: 'praktijk' | 'online' | 'thuis';
identifier?: AppointmentIdentifier;
newDatetime?: {
date: Date;
time: string;
};
_raw?: Record<string, unknown>;
}
// ============================================================================
// NUDGE TYPES
// ============================================================================
export interface NudgeSuggestion {
id: string;
trigger: {
actionId: string;
intent: CortexIntent;
entities: ExtractedEntities;
};
suggestion: {
intent: CortexIntent;
entities: Partial<ExtractedEntities>;
message: string;
rationale: string;
};
status: 'pending' | 'accepted' | 'dismissed' | 'expired';
priority: 'low' | 'medium' | 'high';
expiresAt?: Date;
createdAt: Date;
}
export interface ProtocolRule {
id: string;
name: string;
description: string;
trigger: {
intent: CortexIntent;
conditions?: ProtocolCondition[];
};
suggestion: {
intent: CortexIntent;
message: string;
prefillFrom: (source: ExtractedEntities) => Partial<ExtractedEntities>;
};
priority: 'low' | 'medium' | 'high';
category: 'medicatie' | 'wondzorg' | 'veiligheid' | 'administratief';
enabled: boolean;
}
4.2 Database Schema (Logging)
-- Classification logs for analytics
CREATE TABLE classification_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id UUID NOT NULL,
-- Input
input TEXT NOT NULL,
input_length INTEGER NOT NULL,
-- Classification result
layer TEXT NOT NULL CHECK (layer IN ('reflex', 'cortex')),
intent TEXT NOT NULL,
confidence NUMERIC(3,2) NOT NULL,
-- Reflex specifics
second_best_intent TEXT,
second_best_confidence NUMERIC(3,2),
matched_pattern TEXT,
-- Escalation
escalated BOOLEAN NOT NULL DEFAULT false,
escalation_reason TEXT,
-- Cortex specifics
tokens_used INTEGER,
ai_model TEXT,
ai_reasoning TEXT,
-- Performance
processing_time_ms INTEGER NOT NULL,
-- Context (geanonimiseerd)
has_active_patient BOOLEAN NOT NULL DEFAULT false,
current_view TEXT,
-- Outcome
action_executed BOOLEAN,
user_corrected BOOLEAN,
corrected_intent TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Index voor analytics queries
CREATE INDEX idx_classification_logs_layer ON classification_logs(layer);
CREATE INDEX idx_classification_logs_escalated ON classification_logs(escalated);
CREATE INDEX idx_classification_logs_created_at ON classification_logs(created_at);
5. API Ontwerp
5.1 Intent Classification API
Endpoint: POST /api/cortex/classify
// Request
interface ClassifyRequest {
input: string;
context: CortexContext;
options?: {
forceAI?: boolean;
skipLogging?: boolean;
};
}
// Response
interface ClassifyResponse {
chain: IntentChain;
handledBy: 'reflex' | 'orchestrator';
needsClarification?: boolean;
clarificationQuestion?: string;
clarificationOptions?: string[];
suggestions?: NudgeSuggestion[];
debug?: {
localResult?: LocalClassificationResult;
aiResult?: AIClassificationResult;
};
}
5.2 Action Execution API
Endpoint: POST /api/cortex/execute
interface ExecuteRequest {
chainId: string;
actionId: string;
confirmed?: boolean;
}
interface ExecuteResponse {
success: boolean;
action: IntentAction;
suggestions?: NudgeSuggestion[];
error?: string;
}
5.3 Context API
Endpoint: GET /api/cortex/context
Returns current context for AI classification:
- Active patient met recent notes
- Today's agenda
- Recent actions
- Current shift
interface ContextResponse {
context: CortexContext;
timestamp: Date;
}
5.4 API Response Codes
| Code | Betekenis | Wanneer |
|---|---|---|
| 200 | OK | Succesvolle classificatie/executie |
| 400 | Bad Request | Ongeldige input |
| 401 | Unauthorized | Niet ingelogd |
| 422 | Unprocessable | AI kon input niet verwerken |
| 429 | Too Many Requests | Rate limit (100 req/min) |
| 500 | Server Error | Interne fout |
| 503 | Service Unavailable | AI provider down |
6. Security & Compliance
6.1 Security Checklist
- Authentication: Supabase Auth (session-based)
- Authorization: Row Level Security (RLS) policies
- Data Encryption: At rest (PostgreSQL), in transit (HTTPS)
- Input Validation: Zod schemas op alle endpoints
- Rate Limiting: Vercel Edge Functions (100 req/min)
- CORS: Restrictive origins (alleen eigen domein)
- Secrets Management: Environment variables
6.2 Data Privacy (AVG/GDPR)
- Data minimalisatie: Classification logs bevatten geen PII
- Context anonimisatie: Alleen boolean
has_active_patient, geen namen - Audit trail: Logging van alle classificaties voor debugging
- Right to deletion: Logs kunnen per sessie verwijderd worden
6.3 NEN7510 Overwegingen
- Toegangscontrole per rol (behandelaar, manager, admin)
- Medische dossier toegang gelogd
- AI-verwerking valt onder "verwerker" regels
7. Layer 1: Reflex Arc
7.1 Doel
Razendsnelle (<20ms) afhandeling van simpele, eenduidige commando's met hoge confidence.
7.2 Escalatie Criteria
Layer 1 (Reflex) escaleert naar Layer 2 (Cortex) wanneer één of meer van de volgende triggers aanwezig is:
1. Lage Confidence
- Trigger: Beste match scoort < 0.7
- Reden: Systeem is niet zeker genoeg
- Voorbeeld: "doe iets met de planning" → score 0.45
2. Ambigue Match
- Trigger: Verschil tussen #1 en #2 score < 0.1
- Reden: Systeem twijfelt tussen twee intents
- Voorbeeld: "plan wondzorg" → create_appointment (0.72) vs dagnotitie (0.68)
3. Multi-Intent Signaalwoorden
- Trigger: Input bevat conjuncties die meerdere acties suggereren
- Detectie:
/\b(en|daarna|ook|eerst|dan|vervolgens)\b/i - Reden: Waarschijnlijk meerdere intents in één zin
- Voorbeeld: "Zeg Jan af en maak notitie"
4. Context-Afhankelijke Woorden
- Trigger: Input bevat pronouns of verwijswoorden
- Detectie:
/\b(hij|zij|hem|haar|zijn|die|deze|dat|dezelfde)\b/i - Reden: AI moet context raadplegen om te resolven
- Voorbeeld: "Maak notitie voor hem" → wie is "hem"?
5. Relatieve Tijdsaanduidingen
- Trigger: Input bevat relatieve tijd die berekening vereist
- Detectie:
/\b(morgen|overmorgen|volgende week|over \d+ dagen?|vanmiddag|vanavond)\b/i - Reden: Datum moet berekend worden op basis van huidige tijd
- Voorbeeld: "Plan afspraak morgen 14:00"
7.3 Implementatie
// lib/cortex/reflex-classifier.ts
import type { LocalClassificationResult, CortexIntent, EscalationReason } from './types';
import { CONFIDENCE_THRESHOLD, AMBIGUITY_THRESHOLD } from './types';
// Multi-intent signaalwoorden
const MULTI_INTENT_SIGNALS = /\b(en|daarna|ook|eerst|dan|vervolgens)\b/i;
// Context-afhankelijke woorden (pronouns, verwijzingen)
const CONTEXT_SIGNALS = /\b(hij|zij|hem|haar|zijn|die|deze|dat|dezelfde)\b/i;
// Relatieve tijdsaanduidingen
const RELATIVE_TIME_SIGNALS = /\b(morgen|overmorgen|volgende week|over \d+ dagen?|vanmiddag|vanavond)\b/i;
// Intent patterns met weights
const REFLEX_PATTERNS: Record<CortexIntent, 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.85 },
{ pattern: /^(medicatie|adl|gedrag|incident|observatie)\s+\w+/i, weight: 0.8 },
],
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.85 },
],
agenda_query: [
{ pattern: /^agenda\s*(vandaag)?$/i, weight: 1.0 },
{ pattern: /^afspraken\s*(vandaag|deze week)?$/i, weight: 0.95 },
{ pattern: /^wat\s+staat\s+er\s+vandaag/i, weight: 0.85 },
],
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.8 },
{ pattern: /^plan\s+(intake|afspraak)\b/i, weight: 0.8 },
],
cancel_appointment: [
{ pattern: /^annuleer\s+afspraak\b/i, weight: 0.85 },
{ pattern: /^zeg\s+\w+\s+af\b/i, weight: 0.8 },
],
reschedule_appointment: [
{ pattern: /^verzet\s+afspraak\b/i, weight: 0.8 },
{ pattern: /^verplaats\s+\w+\s+naar\b/i, weight: 0.75 },
],
unknown: [],
};
/**
* Detecteer escalatie triggers in de input
*/
function detectEscalationTriggers(input: string): {
shouldEscalate: boolean;
reasons: EscalationReason[];
} {
const reasons: EscalationReason[] = [];
if (MULTI_INTENT_SIGNALS.test(input)) {
reasons.push('multi_intent_detected');
}
if (CONTEXT_SIGNALS.test(input)) {
reasons.push('needs_context');
}
if (RELATIVE_TIME_SIGNALS.test(input)) {
reasons.push('relative_time');
}
return {
shouldEscalate: reasons.length > 0,
reasons
};
}
/**
* Classify input using local patterns
*/
export function classifyWithReflex(input: string): LocalClassificationResult {
const startTime = performance.now();
const trimmedInput = input.trim();
// STAP 1: Check escalatie triggers
const triggers = detectEscalationTriggers(trimmedInput);
if (triggers.shouldEscalate) {
return {
intent: 'unknown',
confidence: 0,
processingTimeMs: performance.now() - startTime,
shouldEscalateToAI: true,
escalationReason: triggers.reasons[0],
};
}
// STAP 2: Pattern matching - verzamel alle matches
const matches: Array<{ intent: CortexIntent; confidence: number; pattern: string }> = [];
for (const [intent, patterns] of Object.entries(REFLEX_PATTERNS)) {
for (const { pattern, weight } of patterns) {
if (pattern.test(trimmedInput)) {
matches.push({
intent: intent as CortexIntent,
confidence: weight,
pattern: pattern.source,
});
}
}
}
// Sorteer op confidence (hoogste eerst)
matches.sort((a, b) => b.confidence - a.confidence);
const processingTimeMs = performance.now() - startTime;
const bestMatch = matches[0];
const secondBestMatch = matches[1];
// STAP 3: Geen match gevonden
if (!bestMatch) {
return {
intent: 'unknown',
confidence: 0,
processingTimeMs,
shouldEscalateToAI: true,
escalationReason: 'low_confidence',
};
}
// STAP 4: Confidence check
if (bestMatch.confidence < CONFIDENCE_THRESHOLD) {
return {
intent: bestMatch.intent,
confidence: bestMatch.confidence,
matchedPattern: bestMatch.pattern,
processingTimeMs,
shouldEscalateToAI: true,
escalationReason: 'low_confidence',
};
}
// STAP 5: Ambiguity check
if (secondBestMatch &&
bestMatch.confidence - secondBestMatch.confidence < AMBIGUITY_THRESHOLD) {
return {
intent: bestMatch.intent,
confidence: bestMatch.confidence,
secondBestIntent: secondBestMatch.intent,
secondBestConfidence: secondBestMatch.confidence,
matchedPattern: bestMatch.pattern,
processingTimeMs,
shouldEscalateToAI: true,
escalationReason: 'ambiguous',
};
}
// STAP 6: High confidence, unambiguous match - handle locally
return {
intent: bestMatch.intent,
confidence: bestMatch.confidence,
matchedPattern: bestMatch.pattern,
processingTimeMs,
shouldEscalateToAI: false,
};
}
8. Layer 2: Intent Orchestrator
8.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
8.2 AI Model Selectie
| Criterium | Claude 3.5 Haiku | GPT-4o-mini | Gemini Flash |
|---|---|---|---|
| Latency | ~400ms | ~500ms | ~300ms |
| Cost/1K tokens | $0.25 | $0.15 | $0.075 |
| Dutch quality | Excellent | Good | Good |
| JSON reliability | Excellent | Good | Medium |
Keuze: Claude 3.5 Haiku vanwege superieure Nederlandse taalverwerking en JSON output betrouwbaarheid.
Model Versie Note: We gebruiken
claude-3-5-haiku-20241022. Check periodiek de Anthropic model docs voor nieuwere versies.
8.3 System Prompt
const ORCHESTRATOR_SYSTEM_PROMPT = `Je bent de Intent Orchestrator voor Cortex, een Nederlands EPD systeem.
## Je Taak
Analyseer de gebruikersinput en extraheer ALLE intenties, ook als er meerdere zijn.
## Context die je krijgt
- Actieve patiënt: wie de gebruiker momenteel bekijkt
- Agenda vandaag: afspraken voor vandaag
- Recente acties: wat de gebruiker net deed
- Huidige weergave: waar in de app de gebruiker is
## Intent Types
1. **dagnotitie** - Notitie/rapportage maken
2. **zoeken** - Patiënt zoeken
3. **overdracht** - Dienst overdracht
4. **agenda_query** - Agenda bekijken
5. **create_appointment** - Afspraak maken
6. **cancel_appointment** - Afspraak annuleren
7. **reschedule_appointment** - Afspraak verzetten
## Multi-Intent Detectie
Let op woorden als: "en", "daarna", "ook", "eerst", "dan", "vervolgens"
## 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 (geen markdown):
{
"actions": [...],
"reasoning": "...",
"needsClarification": false
}`;
8.4 Implementatie
// lib/cortex/orchestrator.ts
import { classifyWithReflex } from './reflex-classifier';
/**
* Classify with AI Orchestrator, with graceful fallback to Reflex on failure
*/
export async function classifyWithOrchestrator(
input: string,
context: CortexContext
): Promise<AIClassificationResult> {
const startTime = performance.now();
try {
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 : '';
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,
};
} catch (error) {
// Graceful degradation: fallback to Reflex-only
console.error('[Cortex] AI Orchestrator failed, falling back to Reflex:', error);
const reflexResult = classifyWithReflex(input);
const processingTimeMs = performance.now() - startTime;
// Build minimal chain from Reflex result
const fallbackChain: IntentChain = {
id: crypto.randomUUID(),
originalInput: input,
createdAt: new Date(),
actions: reflexResult.intent !== 'unknown' ? [{
id: crypto.randomUUID(),
sequence: 1,
intent: reflexResult.intent,
confidence: reflexResult.confidence,
entities: {},
status: 'pending',
requiresConfirmation: false,
}] : [],
status: reflexResult.intent !== 'unknown' ? 'pending' : 'failed',
meta: {
source: 'local', // Indicate fallback
processingTimeMs,
aiReasoning: 'AI unavailable - fallback to local classification',
},
};
return {
chain: fallbackChain,
model: 'fallback-reflex',
tokensUsed: 0,
processingTimeMs,
needsClarification: false,
};
}
}
9. Layer 3: Nudge Engine
9.1 Doel
Proactieve suggesties na succesvolle acties op basis van medische protocollen.
9.2 Protocol Rules (MVP)
// lib/cortex/nudge.ts
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,
},
// Crisis Protocol
{
id: 'crisis-signalering',
name: 'Suïcidaliteit signalering',
description: 'Bij signalen van suïcidaliteit crisisprotocol checken',
trigger: {
intent: 'dagnotitie',
conditions: [
{
field: 'content',
operator: 'matches',
// Specifiekere regex om false positives te voorkomen (bijv. "doodmoe")
// Matcht: suïcide, zelfmoord, doodswens, wil dood, uitzichtloos, geen zin meer
// Matcht NIET: doodmoe, doodsaai, etc.
value: /suïcid|zelfmoord|doodswens|\bwil\s+dood\b|levenseinde|uitzichtloos|geen\s+zin\s+(meer|in)/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,
},
];
/**
* Note: Protocol rules zijn hardcoded voor MVP.
* Post-MVP: overweeg een admin UI of database-driven rules.
*
* Regex validatie tips:
* - Test altijd met edge cases ("doodmoe", "doodsaai")
* - Gebruik word boundaries (\b) waar mogelijk
* - Overweeg NLP/AI voor complexere matching in productie
*/
10. Logging & Metrics
10.1 Classification Log Structure
Elke classificatie wordt gelogd voor analyse en verbetering:
// lib/cortex/logger.ts
interface ClassificationLog {
// Identificatie
id: string;
timestamp: Date;
sessionId: string;
// Input
input: string;
inputLength: number;
// Classificatie resultaat
layer: 'reflex' | 'cortex';
intent: CortexIntent;
confidence: number;
// Bij Reflex
secondBestIntent?: CortexIntent;
secondBestConfidence?: number;
matchedPattern?: string;
// Escalatie info
escalated: boolean;
escalationReason?: EscalationReason;
// Bij Cortex (AI)
tokensUsed?: number;
aiModel?: string;
aiReasoning?: string;
// Performance
processingTimeMs: number;
// Context (geanonimiseerd)
hasActivePatient: boolean;
currentView: string;
// Outcome (later in te vullen)
actionExecuted?: boolean;
userCorrected?: boolean;
correctedIntent?: CortexIntent;
}
10.2 Metrics om te monitoren
| Metric | Doel | Actie bij afwijking |
|---|---|---|
| Reflex hit rate | >70% lokaal afgehandeld | Threshold tunen of regex uitbreiden |
| Escalation reasons | Verdeling monitoren | Veel 'ambiguous' → patterns verbeteren |
| AI latency p95 | <800ms | Model of prompt optimaliseren |
| User corrections | <5% | Intent definities of training verbeteren |
10.3 Privacy: Input Sanitization
Let op: Raw input kan PII bevatten (bijv. "notitie voor Jan Jansen medicatie"). We sanitizen input voordat we loggen naar productie.
// lib/cortex/sanitize.ts
/**
* Verwijdert potentiële PII uit input voor logging
* - Namen (2+ woorden met hoofdletters)
* - BSN-achtige nummers (9 cijfers)
* - Telefoonnummers
*/
export function sanitizeForLogging(input: string): string {
return input
// Vervang potentiële namen (Hoofdletter + woord patronen)
.replace(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)+\b/g, '[NAAM]')
// Vervang BSN-achtige nummers
.replace(/\b\d{9}\b/g, '[BSN]')
// Vervang telefoonnummers
.replace(/\b(?:06|0\d{2})[-\s]?\d{7,8}\b/g, '[TELEFOON]')
// Vervang email adressen
.replace(/\b[\w.-]+@[\w.-]+\.\w+\b/g, '[EMAIL]');
}
10.4 Log implementatie
// lib/cortex/logger.ts
import { ClassificationLog } from './types';
import { sanitizeForLogging } from './sanitize';
export function logClassification(log: ClassificationLog): void {
// Development: console (raw input OK voor debugging)
if (process.env.NODE_ENV === 'development') {
console.log('[Cortex Classification]', {
input: log.input.substring(0, 50),
layer: log.layer,
intent: log.intent,
confidence: log.confidence,
escalated: log.escalated,
escalationReason: log.escalationReason,
timeMs: log.processingTimeMs,
});
}
// Production: sanitize input voordat we loggen
if (process.env.NODE_ENV === 'production') {
const sanitizedLog = {
...log,
input: sanitizeForLogging(log.input),
};
// Fire-and-forget naar Supabase
fetch('/api/cortex/log', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(sanitizedLog),
}).catch(console.error);
}
}
11. Frontend Components
11.1 Component Hierarchy
CommandCenter (v3.0)
├── ContextBar
├── ChatPanel
│ ├── ChatMessages
│ │ ├── UserMessage
│ │ ├── AssistantMessage
│ │ │ └── ActionChainCard (NEW)
│ │ │ ├── ActionItem
│ │ │ ├── ActionItem
│ │ │ └── ConfirmationDialog
│ │ └── ClarificationCard (NEW)
│ └── ChatInput
├── ArtifactArea
│ ├── ArtifactTabs
│ └── ArtifactContainer
└── NudgeToast (NEW - Layer 3)
11.2 ActionChainCard Component
// components/cortex/chat/action-chain-card.tsx
interface ActionChainCardProps {
chain: IntentChain;
onConfirm: (actionId: string) => void;
onSkip: (actionId: string) => void;
onRetry: (actionId: string) => void;
}
// Toont meerdere acties in één card met:
// - Sequence numbers (1, 2, 3...)
// - Status icons (pending, executing, success, failed)
// - Confidence badges
// - Confirmation buttons voor destructieve acties
// - Collapsible AI reasoning
11.3 NudgeToast Component
// components/cortex/command-center/nudge-toast.tsx
interface NudgeToastProps {
suggestion: NudgeSuggestion;
onAccept: (suggestionId: string) => void;
onDismiss: (suggestionId: string) => void;
}
// Toont proactieve suggestie met:
// - Progress bar countdown (5 minuten)
// - Priority-based styling (red/amber/blue)
// - Accept/Dismiss buttons
// - Rationale tekst
12. State Management
12.1 Cortex Store Extensions
// stores/cortex-store.ts (V2 additions)
interface CortexStoreV2 extends CortexStore {
// Context
context: CortexContext;
setContext: (context: Partial<CortexContext>) => void;
// Intent Chains
activeChain: IntentChain | null;
chainHistory: IntentChain[];
startChain: (chain: IntentChain) => void;
updateActionStatus: (chainId: string, actionId: string, status: IntentAction['status']) => void;
completeChain: (chainId: string) => void;
// Nudge
suggestions: NudgeSuggestion[];
addSuggestion: (suggestion: NudgeSuggestion) => void;
acceptSuggestion: (suggestionId: string) => void;
dismissSuggestion: (suggestionId: string) => void;
// Clarification
pendingClarification: {
question: string;
options: string[];
originalInput: string;
} | null;
setClarification: (clarification: ...) => void;
answerClarification: (answer: string) => void;
}
13. Performance & Scalability
13.1 Performance Targets
| Metric | Target | Gemeten |
|---|---|---|
| Reflex classificatie | <20ms | ~5ms |
| AI classificatie | <800ms | ~400ms |
| Page load (FCP) | <2s | - |
| API response (excl. AI) | <500ms | - |
13.2 Caching Strategie
- Context API: 30 seconden cache (agenda wijzigt niet snel)
- AI responses: Geen cache (context-afhankelijk)
- Protocol rules: In-memory (statisch)
13.3 Scalability
- Frontend: Vercel Edge Network (CDN)
- Backend: Serverless functions (auto-scaling)
- Database: Supabase (vertical scaling)
- AI: Queue systeem voor batch processing (toekomst)
14. Deployment & CI/CD
14.1 Omgevingen
| Omgeving | URL | Branch | Doel |
|---|---|---|---|
| Development | localhost:3000 | - | Lokaal ontwikkelen |
| Preview | *.vercel.app | PR branches | Review builds |
| Production | app.example.com | main | Live applicatie |
14.2 Feature Flags
// lib/config/feature-flags.ts
export const FEATURE_FLAGS = {
CORTEX_V2_ENABLED: process.env.NEXT_PUBLIC_CORTEX_V2 === 'true',
CORTEX_MULTI_INTENT: process.env.NEXT_PUBLIC_CORTEX_MULTI_INTENT === 'true',
CORTEX_NUDGE: process.env.NEXT_PUBLIC_CORTEX_NUDGE === 'true',
CORTEX_LOGGING: process.env.NEXT_PUBLIC_CORTEX_LOGGING === 'true',
};
14.3 Deployment Checklist
- Environment variables set (Vercel dashboard)
- Feature flags configured
- Database migrations run
- Smoke tests passed
- Monitoring configured
15. Testing Strategy
15.1 Test Categorieën
| Type | Scope | Tools |
|---|---|---|
| Unit | Reflex classifier, entity extractor | Vitest |
| Integration | API endpoints, AI responses | Vitest + MSW |
| E2E | Complete flows | Playwright |
15.2 Test Zinnen Dataset
{
"single_intent": [
{ "input": "notitie jan medicatie", "expected": ["dagnotitie"] },
{ "input": "zoek marie", "expected": ["zoeken"] },
{ "input": "agenda vandaag", "expected": ["agenda_query"] }
],
"multi_intent": [
{
"input": "Zeg Jan af en maak notitie dat hij griep heeft",
"expected": ["cancel_appointment", "dagnotitie"]
}
],
"context_dependent": [
{
"input": "Maak notitie voor hem",
"context": { "activePatient": { "name": "Piet" } },
"expected": ["dagnotitie"],
"entities": [{ "patientResolution": "pronoun" }]
}
],
"ambiguous": [
{
"input": "Plan wondzorg",
"shouldEscalate": true
}
]
}
16. Risico's & Technische Mitigatie
| Risico | Impact | Kans | Mitigatie |
|---|---|---|---|
| AI API down | Hoog | Laag | Graceful degradation naar Reflex-only |
| Hallucinated intents | Middel | Middel | Strikte JSON schema, fallback to unknown |
| Performance degradatie | Middel | Laag | Monitoring, caching, threshold tuning |
| Privacy breach (logs) | Hoog | Laag | Anonimisatie, geen PII in logs |
| Cost overrun (AI) | Middel | Middel | Token limits, usage monitoring |
17. Implementatie Roadmap
Fase 1: Foundation (3-4 dagen)
| Task | Effort | Status |
|---|---|---|
| CortexContext types | S | Pending |
| GET /api/cortex/context endpoint | M | Pending |
| Reflex complexity detection | S | Pending |
| Feature flags setup | S | Pending |
Deliverable: Context beschikbaar, backward compatible
Fase 2: Multi-Intent (4-5 dagen)
| Task | Effort | Status |
|---|---|---|
| IntentChain types | M | Pending |
| Orchestrator AI prompt | M | Pending |
| ActionChainCard component | L | Pending |
| Store chain state | M | Pending |
Deliverable: "Zeg Jan af en maak notitie" werkt
Fase 3: Nudge MVP (2-3 dagen)
| Task | Effort | Status |
|---|---|---|
| Protocol rules (wondzorg) | S | Pending |
| evaluateNudge functie | M | Pending |
| NudgeToast component | M | Pending |
Deliverable: Proactieve suggestie demo
Fase 4: Polish (2-3 dagen)
| Task | Effort | Status |
|---|---|---|
| ClarificationCard | S | Pending |
| Error handling | M | Pending |
| Logging infrastructure | M | Pending |
Deliverable: Demo-ready prototype
Totaal: 11-15 werkdagen
18. Bijlagen & Referenties
A. Gerelateerde Documenten
| Document | Locatie |
|---|---|
| FO Intent System V2 | docs/intent/fo-cortex-intent-system-v2.md |
| Architectuur V2 | docs/intent/architecture-cortex-v2.md |
| MVP User Stories | docs/intent/mvp-userstories-intent-system.md |
| Haalbaarheidsanalyse | docs/intent/haalbaarheidsanalyse-cortex-v2.md |
B. Tech Documentatie
- Next.js: https://nextjs.org/docs
- Supabase: https://supabase.com/docs
- Anthropic: https://docs.anthropic.com
- Zustand: https://docs.pmnd.rs/zustand
C. Versie Historie
| Versie | Datum | Auteur | Wijzigingen |
|---|---|---|---|
| 1.0 | 30-12-2025 | Colin Lit | Initieel document met sanity check aanbevelingen verwerkt |