# Developer Guide: Swift Intent System
**Document:** Developer Guide - Intent Classification & Extension
**Versie:** 1.0
**Datum:** 27-12-2024
**Auteur:** Colin Lit
**Audience:** Developers die Swift intents willen begrijpen of uitbreiden
---
## π Inhoudsopgave
1. [Overzicht](#1-overzicht)
2. [Architectuur](#2-architectuur)
3. [Hoe het werkt](#3-hoe-het-werkt)
4. [Bestaande Intents](#4-bestaande-intents)
5. [Een nieuw intent toevoegen](#5-een-nieuw-intent-toevoegen)
6. [Best Practices](#6-best-practices)
7. [Troubleshooting](#7-troubleshooting)
8. [Testing](#8-testing)
9. [Referenties](#9-referenties)
---
## 1. Overzicht
### Wat is het Intent System?
Het Swift Intent System is een **two-tier classificatie systeem** dat natuurlijke taal input van gebruikers analyseert en vertaalt naar concrete acties binnen de applicatie.
**Doel:**
- Gebruikers kunnen in natuurlijke taal (Nederlands) communiceren met Swift
- Systeem herkent de intentie achter de input
- Opent het juiste artifact (UI component) met de juiste pre-filled data
**Voorbeeld:**
```
User input: "notitie jan medicatie"
β
Intent classification: 'dagnotitie'
β
Entities extracted: { patient: "jan", category: "medicatie" }
β
Action: Open DagnotatieBlock met pre-filled patient + category
```
### Waarom Two-Tier?
| Tier | Methode | Snelheid | Accuracy | Use Case |
|------|---------|----------|----------|----------|
| **Tier 1** | Local regex patterns | <50ms | Hoog voor directe matches | "notitie jan", "afspraken vandaag" |
| **Tier 2** | AI (Claude Haiku) | ~400ms | Hoog voor complexe/ambigue input | "ik wil iets plannen voor volgende week" |
**Voordelen:**
- β
**Snelheid:** 90%+ van queries worden lokaal afgehandeld (<50ms)
- β
**Kosten:** Alleen AI call bij onduidelijke input
- β
**Betrouwbaarheid:** Fallback naar AI als local match faalt
- β
**Flexibiliteit:** Makkelijk om nieuwe patterns toe te voegen
---
## 2. Architectuur
### System Diagram
```
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β USER INPUT β
β "maak afspraak met jan morgen 14:00" β
ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TIER 1: Local Pattern Classifier β
β (lib/swift/intent-classifier.ts) β
β β
β β’ Regex-based pattern matching β
β β’ Client-side execution β
β β’ Returns: { intent, confidence, processingTimeMs } β
ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββ
β Confidence β
β >= 0.8? β
ββββ¬βββββββ¬ββββ
β YES β NO
β β
βββββββββββββ ββββββββββββββββββββββββββββββββββββ
β β
β β
ββββββββββββββββ βββββββββββββββββββββββββββββββββββ
β Use Local β β TIER 2: AI Fallback Classifier β
β Result β β (lib/swift/intent-classifier-ai β
ββββββββ¬ββββββββ β .ts) β
β β β
β β β’ Claude Haiku API call β
β β β’ Server-side only β
β β β’ Advanced entity extraction β
β βββββββββββββββ¬ββββββββββββββββββββ
β β
ββββββββββββββββββββ¬ββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β INTENT ROUTER β
β (swift-store.ts / Chat API) β
β β
β β’ Maps intent β artifact type β
β β’ Applies confidence thresholds β
β β’ Triggers verduidelijkingsvragen bij lage confidence β
ββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ARTIFACT MANAGER β
β β
β β’ Opens appropriate artifact (DagnotatieBlock, etc.) β
β β’ Pre-fills extracted entities β
β β’ Manages artifact lifecycle β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
```
### Core Files
| File | Locatie | Rol | Execution |
|------|---------|-----|-----------|
| **intent-classifier.ts** | `/lib/swift/` | Local pattern matching | Client-side |
| **intent-classifier-ai.ts** | `/lib/swift/` | AI fallback classifier | Server-side |
| **types.ts** | `/lib/swift/` | Type definitions | Both |
| **swift-store.ts** | `/stores/` | State management + routing | Client-side |
| **chat/route.ts** | `/app/api/swift/` | Chat API met streaming | Server-side |
---
## 3. Hoe het werkt
### Step-by-Step Flow
#### Step 1: User Input
```typescript
// User types in chat input
const userInput = "notitie jan medicatie";
```
#### Step 2: Local Classification (Tier 1)
```typescript
// lib/swift/intent-classifier.ts
import { classifyIntent } from '@/lib/swift/intent-classifier';
const result = classifyIntent(userInput);
// Returns:
{
intent: 'dagnotitie',
confidence: 0.9,
matchedPattern: '/^notitie\\s+\\w+/',
processingTimeMs: 12
}
```
**How it works:**
```typescript
const INTENT_PATTERNS = {
dagnotitie: [
{ pattern: /^notitie\s+\w+/i, weight: 0.95 },
{ pattern: /^dagnotitie\b/i, weight: 1.0 },
// ... meer patterns
],
zoeken: [
{ pattern: /^zoek\b/i, weight: 1.0 },
{ pattern: /^wie\s+is\b/i, weight: 1.0 },
// ...
],
// ...
};
// Test all patterns, return highest weight match
for (const [intent, patterns] of Object.entries(INTENT_PATTERNS)) {
for (const { pattern, weight } of patterns) {
if (pattern.test(userInput)) {
// Found a match!
return { intent, confidence: weight };
}
}
}
```
#### Step 3: Confidence Check
```typescript
import { isHighConfidence, shouldUseAIFallback } from '@/lib/swift/intent-classifier';
if (isHighConfidence(result)) {
// confidence >= 0.8
// β Use local result, skip AI
return result;
}
if (shouldUseAIFallback(result.confidence)) {
// confidence < 0.8
// β Trigger AI classification
const aiResult = await classifyIntentWithAI(userInput);
return aiResult;
}
```
#### Step 4a: AI Fallback (Tier 2) - If Needed
```typescript
// lib/swift/intent-classifier-ai.ts (SERVER-SIDE ONLY)
import { classifyIntentWithAI } from '@/lib/swift/intent-classifier-ai';
const aiResult = await classifyIntentWithAI("ik wil een gesprek plannen");
// Returns:
{
intent: 'create_appointment',
confidence: 0.75,
entities: {
patientName: undefined, // Missing!
content: "een gesprek plannen"
},
source: 'ai',
processingTimeMs: 380,
reasoning: "Gebruiker wil afspraak maken, maar patient ontbreekt"
}
```
**How it works:**
1. Sends user input to Claude Haiku API
2. Uses structured system prompt with intent definitions
3. Expects JSON response with intent + entities + confidence
4. Validates response with Zod schema
```typescript
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'x-api-key': process.env.ANTHROPIC_API_KEY,
'anthropic-version': '2023-06-01',
},
body: JSON.stringify({
model: 'claude-3-5-haiku-20241022',
max_tokens: 256,
temperature: 0,
system: INTENT_CLASSIFIER_SYSTEM_PROMPT,
messages: [{ role: 'user', content: `Classificeer: "${input}"` }],
}),
});
const parsed = JSON.parse(response);
const validated = AIIntentResponseSchema.parse(parsed);
```
#### Step 5: Intent Routing
```typescript
// In swift-store.ts or chat API
function handleIntent(intent: SwiftIntent, entities: ExtractedEntities, confidence: number) {
// Confidence too low β ask clarification
if (confidence < 0.7) {
return {
type: 'clarification_needed',
question: generateClarificationQuestion(intent, entities)
};
}
// Route to appropriate artifact
switch (intent) {
case 'dagnotitie':
return openArtifact({
type: 'DagnotatieBlock',
mode: 'create',
prefill: {
patient: entities.patientName,
category: entities.category,
content: entities.content
}
});
case 'zoeken':
return openArtifact({
type: 'ZoekenBlock',
prefill: { query: entities.patientName }
});
case 'agenda_query':
return openArtifact({
type: 'AgendaBlock',
mode: 'list',
prefill: { dateRange: entities.dateRange }
});
// ... etc
}
}
```
#### Step 6: Artifact Opens
```typescript
// Artifact Manager opens the correct component
```
---
## 4. Bestaande Intents
### Intent Overzicht
| Intent | Beschrijving | Artifacts | Entities |
|--------|--------------|-----------|----------|
| **dagnotitie** | Notitie maken voor patiΓ«nt | DagnotatieBlock | patientName, category, content |
| **zoeken** | PatiΓ«nt zoeken | ZoekenBlock, PatientContextCard | patientName |
| **overdracht** | Dienst overdracht genereren | OverdrachtBlock | β |
| **agenda_query** | Afspraken opvragen | AgendaBlock (list) | dateRange |
| **create_appointment** | Nieuwe afspraak maken | AgendaBlock (create) | patient, datetime, type, location |
| **cancel_appointment** | Afspraak annuleren | AgendaBlock (cancel) | identifier |
| **reschedule_appointment** | Afspraak verzetten | AgendaBlock (reschedule) | identifier, newDatetime |
| **unknown** | Niet herkend | Fallback picker | β |
### Pattern Examples
**Dagnotitie:**
```typescript
dagnotitie: [
{ pattern: /^dagnotitie\b/i, weight: 1.0 },
{ pattern: /^notitie\b/i, weight: 1.0 },
{ pattern: /^notitie\s+\w+/i, weight: 0.95 }, // "notitie jan"
{ pattern: /^\w+\s+(medicatie|adl|gedrag|incident)/i, weight: 0.9 }, // "jan medicatie"
]
```
**Zoeken:**
```typescript
zoeken: [
{ pattern: /^zoek\b/i, weight: 1.0 },
{ pattern: /^wie\s+is\b/i, weight: 1.0 }, // "wie is jan"
{ pattern: /^vind\s+\w+/i, weight: 1.0 }, // "vind marie"
{ pattern: /^[A-Z][a-z]+$/i, weight: 0.5 }, // Single capitalized word
]
```
**Agenda Query:**
```typescript
agenda_query: [
{ pattern: /^afspraken?\b/i, weight: 1.0 }, // "afspraken"
{ pattern: /^agenda\b/i, weight: 1.0 }, // "agenda"
{ pattern: /^wat\s+zijn\s+(mijn\s+)?afspraken/i, weight: 1.0 },
{ pattern: /^volgende\s+afspraak/i, weight: 0.95 },
]
```
---
## 5. Een nieuw intent toevoegen
### Voorbeeld: "metingen" intent
Stel je wilt een nieuw intent toevoegen voor het invoeren van vitale metingen (bloeddruk, temperatuur, etc.).
**Gewenste flow:**
```
User: "bloeddruk jan 120/80"
β Intent: 'metingen'
β Entities: { patient: "jan", type: "bloeddruk", value: "120/80" }
β Opens: MetingenBlock met pre-filled data
```
### Step 1: Update Types
**File:** `lib/swift/types.ts`
```typescript
// Add new intent to enum
export type SwiftIntent =
| 'dagnotitie'
| 'zoeken'
| 'overdracht'
| 'agenda_query'
| 'create_appointment'
| 'cancel_appointment'
| 'reschedule_appointment'
| 'metingen' // β NEW
| 'unknown';
// Extend ExtractedEntities
export interface ExtractedEntities {
// Existing fields
patientName?: string;
category?: VerpleegkundigCategory;
content?: string;
// ... other existing fields
// NEW: Metingen-specific entities
metingType?: 'bloeddruk' | 'temperatuur' | 'pols' | 'saturatie' | 'gewicht';
metingValue?: string;
metingUnit?: string;
metingTimestamp?: Date;
}
```
### Step 2: Add Local Patterns
**File:** `lib/swift/intent-classifier.ts`
```typescript
const INTENT_PATTERNS: Record, PatternConfig[]> = {
// ... existing patterns
// NEW: Metingen patterns
metingen: [
// Exact commands
{ pattern: /^meting\b/i, weight: 1.0 },
{ pattern: /^metingen\b/i, weight: 1.0 },
{ pattern: /^vitale\s+functies\b/i, weight: 1.0 },
// Type-specific patterns
{ pattern: /^bloeddruk\b/i, weight: 1.0 },
{ pattern: /^temperatuur\b/i, weight: 1.0 },
{ pattern: /^pols\b/i, weight: 0.95 },
{ pattern: /^saturatie\b/i, weight: 0.95 },
{ pattern: /^gewicht\b/i, weight: 0.95 },
// Pattern: "bloeddruk [naam]" or "[naam] bloeddruk"
{ pattern: /^bloeddruk\s+\w+/i, weight: 0.95 },
{ pattern: /^\w+\s+bloeddruk/i, weight: 0.9 },
// Pattern: "bloeddruk [naam] [waarde]"
{ pattern: /^bloeddruk\s+\w+\s+\d+/i, weight: 0.98 },
// Pattern with value patterns
{ pattern: /\d{2,3}\/\d{2,3}/i, weight: 0.7 }, // "120/80" format
{ pattern: /\d+\s*graden?/i, weight: 0.7 }, // "38 graden"
],
};
```
**Pattern Weight Guidelines:**
- **1.0** = Exact, unambiguous match ("meting", "bloeddruk")
- **0.9-0.95** = Strong match with context ("bloeddruk jan")
- **0.8-0.89** = Good match, minor ambiguity
- **0.7-0.79** = Partial match, needs additional context
- **<0.7** = Weak match, likely needs AI fallback
### Step 3: Update AI System Prompt
**File:** `lib/swift/intent-classifier-ai.ts`
```typescript
const INTENT_CLASSIFIER_SYSTEM_PROMPT = `Je bent een intent classifier voor Swift EPD.
Classificeer de intentie in één van deze categorieën:
**Bestaande intents:**
1. dagnotitie - Notitie maken
2. zoeken - PatiΓ«nt zoeken
3. overdracht - Dienst overdracht
4-7. [agenda intents...]
**Nieuwe intent:**
8. **metingen** - Vitale functies invoeren
Voorbeelden: "bloeddruk jan 120/80", "temperatuur marie 38 graden", "pols invoeren"
Extraheer: patientName, metingType, metingValue, metingUnit
Types: bloeddruk, temperatuur, pols, saturatie, gewicht
Value patterns:
- Bloeddruk: "120/80" (systolisch/diastolisch)
- Temperatuur: "38" of "38 graden" of "38.5"
- Pols: "72" of "72 bpm"
- Saturatie: "98" of "98%"
- Gewicht: "75" of "75 kg"
9. unknown - Onduidelijk
**Response format:**
{
"intent": "...",
"confidence": 0.0-1.0,
"entities": {
"patientName": "jan",
"metingType": "bloeddruk",
"metingValue": "120/80",
"metingUnit": "mmHg" // optioneel
}
}
`;
```
**Update Zod Schema:**
```typescript
const AIIntentResponseSchema = z.object({
intent: z.enum([
'dagnotitie',
'zoeken',
'overdracht',
'agenda_query',
'create_appointment',
'cancel_appointment',
'reschedule_appointment',
'metingen', // β ADD
'unknown'
]),
confidence: z.number().min(0).max(1),
entities: z.object({
patientName: z.string().optional(),
category: z.enum(['medicatie', 'adl', 'gedrag', 'incident', 'observatie']).optional(),
content: z.string().optional(),
// ... existing fields
// NEW
metingType: z.enum(['bloeddruk', 'temperatuur', 'pols', 'saturatie', 'gewicht']).optional(),
metingValue: z.string().optional(),
metingUnit: z.string().optional(),
}).optional(),
reasoning: z.string().optional(),
});
```
### Step 4: Add Intent Routing
**File:** `stores/swift-store.ts` or chat API route
```typescript
function openArtifactForIntent(
intent: SwiftIntent,
entities: ExtractedEntities,
confidence: number
): ArtifactConfig | null {
// Confidence check
if (confidence < 0.7) {
return null; // Trigger clarification
}
switch (intent) {
// ... existing cases
// NEW
case 'metingen':
// Validate required entities
if (!entities.patientName || !entities.metingType) {
return null; // Need more info
}
return {
type: 'MetingenBlock',
mode: 'create',
prefill: {
patient: entities.patientName,
type: entities.metingType,
value: entities.metingValue,
unit: entities.metingUnit,
timestamp: entities.metingTimestamp || new Date()
}
};
default:
return null;
}
}
```
### Step 5: Create the Artifact Component
**File:** `components/swift/artifacts/blocks/metingen-block.tsx`
```typescript
interface MetingenBlockProps {
mode: 'create' | 'view';
prefill?: {
patient?: { id: string; name: string };
type?: MetingType;
value?: string;
unit?: string;
timestamp?: Date;
};
onClose?: () => void;
}
export function MetingenBlock({ mode, prefill, onClose }: MetingenBlockProps) {
const [formData, setFormData] = useState({
patientId: prefill?.patient?.id || '',
type: prefill?.type || 'bloeddruk',
value: prefill?.value || '',
unit: prefill?.unit || getDefaultUnit(prefill?.type),
timestamp: prefill?.timestamp || new Date(),
});
const handleSubmit = async () => {
// Call server action to save measurement
await createMeasurement(formData);
// Show success toast
toast.success('Meting opgeslagen!');
onClose?.();
};
return (
);
}
function getDefaultUnit(type?: MetingType): string {
switch (type) {
case 'bloeddruk': return 'mmHg';
case 'temperatuur': return 'Β°C';
case 'pols': return 'bpm';
case 'saturatie': return '%';
case 'gewicht': return 'kg';
default: return '';
}
}
```
### Step 6: Register Artifact Type
**File:** `stores/swift-store.ts` or artifact registry
```typescript
export type ArtifactType =
| 'DagnotatieBlock'
| 'ZoekenBlock'
| 'PatientContextCard'
| 'OverdrachtBlock'
| 'AgendaBlock'
| 'MetingenBlock' // β ADD
;
// Add to artifact component mapping
const ARTIFACT_COMPONENTS = {
DagnotatieBlock: lazy(() => import('@/components/swift/artifacts/blocks/dagnotitie-block')),
ZoekenBlock: lazy(() => import('@/components/swift/artifacts/blocks/zoeken-block')),
// ... existing
MetingenBlock: lazy(() => import('@/components/swift/artifacts/blocks/metingen-block')), // β ADD
};
```
### Step 7: Test
**Manual Testing Checklist:**
```typescript
// Test cases for 'metingen' intent
// β
Local pattern matches (high confidence)
"bloeddruk jan 120/80" β intent: metingen, conf: 0.98
"temperatuur marie" β intent: metingen, conf: 1.0
"pols" β intent: metingen, conf: 0.95
// β
AI fallback (ambiguous)
"jan zijn bloeddruk invoeren" β AI: intent: metingen, conf: 0.85
"ik wil vitale functies noteren" β AI: intent: metingen, conf: 0.75
// β
Edge cases
"bloeddruk" β Missing patient, show clarification
"jan" β Likely 'zoeken', not 'metingen'
"bloeddruk 120/80" β Missing patient, show clarification
// β
Artifact opens correctly
"bloeddruk jan 120/80" β MetingenBlock opens with prefill
- patient: "jan"
- type: "bloeddruk"
- value: "120/80"
- unit: "mmHg"
// β
Voice input works
[Voice] "bloeddruk jan honderdtwintig tachtig"
β Transcript: "bloeddruk jan honderdtwintig tachtig"
β AI parses: value: "120/80"
β Opens MetingenBlock
```
**Unit Tests:**
```typescript
// __tests__/intent-classifier.test.ts
import { classifyIntent } from '@/lib/swift/intent-classifier';
describe('Intent Classifier - Metingen', () => {
it('should classify "bloeddruk jan 120/80" as metingen', () => {
const result = classifyIntent('bloeddruk jan 120/80');
expect(result.intent).toBe('metingen');
expect(result.confidence).toBeGreaterThanOrEqual(0.9);
});
it('should classify "temperatuur" as metingen', () => {
const result = classifyIntent('temperatuur');
expect(result.intent).toBe('metingen');
expect(result.confidence).toBe(1.0);
});
it('should have low confidence for "jan bloeddruk"', () => {
const result = classifyIntent('jan bloeddruk');
expect(result.intent).toBe('metingen');
expect(result.confidence).toBeLessThan(0.95);
});
});
```
---
## 6. Best Practices
### Pattern Design
**β
DO:**
```typescript
// Clear, specific patterns first
{ pattern: /^bloeddruk\b/i, weight: 1.0 }
{ pattern: /^temperatuur\b/i, weight: 1.0 }
// Then patterns with context
{ pattern: /^bloeddruk\s+\w+/i, weight: 0.95 } // "bloeddruk jan"
// Finally, partial matches
{ pattern: /\d{2,3}\/\d{2,3}/i, weight: 0.7 } // "120/80" format
```
**β DON'T:**
```typescript
// Too broad - will match everything
{ pattern: /./i, weight: 1.0 }
// Too specific - will never match natural language
{ pattern: /^bloeddruk\s+jan\s+120\/80$/i, weight: 1.0 }
// Overlapping high-weight patterns (creates ambiguity)
{ pattern: /^notitie\b/i, weight: 1.0 }
{ pattern: /^notitie\s+/i, weight: 1.0 } // Redundant!
```
### Confidence Weights
**Guidelines:**
| Weight Range | Use Case | Example |
|--------------|----------|---------|
| **1.0** | Exact keyword match, no ambiguity | `/^bloeddruk\b/i` |
| **0.9-0.95** | Strong match with minimal context | `/^bloeddruk\s+\w+/i` |
| **0.8-0.89** | Good match, some context needed | `/^\w+\s+bloeddruk/i` |
| **0.7-0.79** | Partial match, may need AI | `/\d{2,3}\/\d{2,3}/i` |
| **<0.7** | Weak match, definitely needs AI | β |
**Threshold decisions:**
```typescript
if (confidence >= 0.8) {
// High confidence β Use local result
return openArtifact(intent, entities);
}
if (confidence >= 0.7) {
// Medium confidence β Open artifact + confirmation in chat
openArtifact(intent, entities);
return chatResponse("Klopt dit? [intent description]");
}
if (confidence >= 0.5) {
// Low confidence β Ask clarification
return chatResponse(generateClarificationQuestion(intent, entities));
}
// confidence < 0.5
// Very low β Show fallback picker or generic help
return showFallbackPicker();
```
### Entity Extraction
**Local extraction (basic):**
```typescript
// Extract from regex capture groups
const pattern = /^bloeddruk\s+(\w+)\s+(\d+\/\d+)/i;
const match = pattern.exec(input);
if (match) {
return {
intent: 'metingen',
entities: {
patientName: match[1], // "jan"
metingValue: match[2], // "120/80"
}
};
}
```
**AI extraction (advanced):**
Let the AI handle complex parsing:
- Relative dates: "morgen", "volgende week dinsdag"
- Number words: "honderdtwintig tachtig" β "120/80"
- Implicit entities: "deze patiΓ«nt" (uses context)
### Performance Optimization
**β
DO:**
```typescript
// Pre-compile regex patterns (happens at module load)
const PATTERNS = {
bloeddruk: /^bloeddruk\b/i,
temperatuur: /^temperatuur\b/i,
};
// Early exit on perfect match
if (confidence === 1.0) break;
// Cache frequent queries (if applicable)
const cache = new Map();
```
**β DON'T:**
```typescript
// Don't create regex in loop
for (const input of inputs) {
const pattern = new RegExp(`^${keyword}\\b`, 'i'); // β Slow!
}
// Don't make unnecessary AI calls
if (confidence >= 0.8) {
const aiResult = await classifyWithAI(input); // β Waste of time & money
}
```
### Error Handling
**Always validate:**
```typescript
// Validate AI response
try {
const parsed = JSON.parse(aiResponse);
const validated = AIIntentResponseSchema.parse(parsed);
return validated;
} catch (error) {
console.error('AI classification error:', error);
// Fallback to unknown intent
return {
intent: 'unknown',
confidence: 0,
source: 'ai',
};
}
// Validate entity presence before opening artifact
if (!entities.patientName) {
// Ask clarification instead of opening empty artifact
return askClarification("Met welke patiΓ«nt?");
}
```
---
## 7. Troubleshooting
### Common Issues
#### Issue 1: Intent not being recognized
**Symptoms:**
```
User: "bloeddruk jan"
Result: intent: 'unknown', confidence: 0
```
**Debugging:**
```typescript
// 1. Check if pattern exists
console.log(INTENT_PATTERNS.metingen);
// Should show array of patterns
// 2. Test pattern manually
const pattern = /^bloeddruk\b/i;
console.log(pattern.test("bloeddruk jan"));
// Should be true
// 3. Check pattern order (first match wins)
// If pattern is below a more generic pattern, it may never be tested
// 4. Check case sensitivity
const pattern = /^Bloeddruk\b/; // β Won't match "bloeddruk"
const pattern = /^bloeddruk\b/i; // β
Case insensitive
```
**Solution:**
```typescript
// Add or fix pattern
metingen: [
{ pattern: /^bloeddruk\b/i, weight: 1.0 }, // β Ensure this exists
// ...
]
```
#### Issue 2: Wrong intent detected
**Symptoms:**
```
User: "jan bloeddruk"
Result: intent: 'zoeken', confidence: 0.5 // β Should be 'metingen'
```
**Cause:** Pattern for 'zoeken' matches first:
```typescript
zoeken: [
{ pattern: /^[A-Z][a-z]+$/i, weight: 0.5 }, // Matches "jan"
]
```
**Solution:** Add more specific patterns with higher weight:
```typescript
metingen: [
{ pattern: /^\w+\s+bloeddruk/i, weight: 0.9 }, // "jan bloeddruk" β metingen
]
// Or: Lower weight of generic patterns
zoeken: [
{ pattern: /^[A-Z][a-z]+$/i, weight: 0.3 }, // Lower weight
]
```
#### Issue 3: AI fallback not triggering
**Symptoms:**
```
User: "ik wil de bloeddruk invoeren"
Result: intent: 'unknown', confidence: 0 // AI should have helped
```
**Debugging:**
```typescript
// 1. Check confidence threshold
if (result.confidence < 0.8) {
// Should trigger AI
const aiResult = await classifyIntentWithAI(input);
}
// 2. Check API key
console.log(process.env.ANTHROPIC_API_KEY); // Should be set
// 3. Check AI is only called server-side
// AI classifier should NEVER run in browser
```
**Solution:**
```typescript
// Ensure AI fallback is in server-side code only
// app/api/swift/chat/route.ts (β
Server-side)
// components/client-component.tsx (β Client-side, won't work)
```
#### Issue 4: Entities not extracted
**Symptoms:**
```
User: "bloeddruk jan 120/80"
Result: intent: 'metingen', entities: {} // β Should extract patient + value
```
**Cause:** Local classifier only does basic regex matching. Complex entity extraction requires AI or custom parsing.
**Solutions:**
**Option A:** Add local entity extraction:
```typescript
// In intent-classifier.ts
const pattern = /^bloeddruk\s+(\w+)\s+(.+)/i;
const match = pattern.exec(input);
if (match) {
return {
intent: 'metingen',
confidence: 0.95,
entities: {
patientName: match[1], // "jan"
metingValue: match[2], // "120/80"
}
};
}
```
**Option B:** Let AI handle entity extraction:
```typescript
// Trigger AI when entities are crucial
if (result.intent === 'metingen' && !result.entities?.patientName) {
// Local matched intent, but missing entities β Use AI for entity extraction
const aiResult = await classifyIntentWithAI(input);
return {
intent: result.intent,
confidence: result.confidence,
entities: aiResult.entities, // Use AI-extracted entities
};
}
```
#### Issue 5: Slow performance
**Symptoms:**
- Local classification takes >100ms
- UI feels sluggish
**Debugging:**
```typescript
const result = classifyIntent(input);
console.log(`Classification took ${result.processingTimeMs}ms`);
// Target: <50ms
// Acceptable: <100ms
// Slow: >100ms
```
**Common causes:**
1. **Too many patterns** (>100 patterns total)
- Solution: Consolidate similar patterns
2. **Complex regex** (catastrophic backtracking)
```typescript
// β BAD: Can cause exponential backtracking
{ pattern: /^(a+)+b/, weight: 1.0 }
// β
GOOD: Simple, efficient patterns
{ pattern: /^a+b/, weight: 1.0 }
```
3. **Not breaking early on perfect match**
```typescript
// β
Add early exit
if (bestMatch?.confidence === 1.0) break;
```
---
## 8. Testing
### Unit Tests
**File:** `__tests__/intent-classifier.test.ts`
```typescript
import { classifyIntent } from '@/lib/swift/intent-classifier';
describe('Intent Classifier', () => {
describe('Dagnotitie intent', () => {
it('should recognize "notitie jan"', () => {
const result = classifyIntent('notitie jan');
expect(result.intent).toBe('dagnotitie');
expect(result.confidence).toBeGreaterThanOrEqual(0.9);
});
it('should recognize "jan medicatie"', () => {
const result = classifyIntent('jan medicatie');
expect(result.intent).toBe('dagnotitie');
expect(result.confidence).toBeGreaterThanOrEqual(0.8);
});
});
describe('Zoeken intent', () => {
it('should recognize "zoek jan"', () => {
const result = classifyIntent('zoek jan');
expect(result.intent).toBe('zoeken');
expect(result.confidence).toBe(1.0);
});
it('should recognize "wie is jan"', () => {
const result = classifyIntent('wie is jan');
expect(result.intent).toBe('zoeken');
expect(result.confidence).toBe(1.0);
});
});
describe('Performance', () => {
it('should classify in less than 50ms', () => {
const result = classifyIntent('notitie jan medicatie');
expect(result.processingTimeMs).toBeLessThan(50);
});
});
describe('Confidence thresholds', () => {
it('should return high confidence for exact matches', () => {
const result = classifyIntent('dagnotitie');
expect(result.confidence).toBe(1.0);
});
it('should return lower confidence for partial matches', () => {
const result = classifyIntent('jan');
expect(result.confidence).toBeLessThan(0.8);
});
});
});
```
### Integration Tests
**File:** `__tests__/intent-flow.test.ts`
```typescript
import { classifyIntent } from '@/lib/swift/intent-classifier';
import { classifyIntentWithAI } from '@/lib/swift/intent-classifier-ai';
import { openArtifactForIntent } from '@/stores/swift-store';
describe('Intent Flow Integration', () => {
it('should open DagnotatieBlock for "notitie jan medicatie"', async () => {
// Step 1: Classify
const result = classifyIntent('notitie jan medicatie');
expect(result.intent).toBe('dagnotitie');
// Step 2: Route to artifact
const artifact = openArtifactForIntent(
result.intent,
{ patientName: 'jan', category: 'medicatie' },
result.confidence
);
expect(artifact).toEqual({
type: 'DagnotatieBlock',
mode: 'create',
prefill: {
patient: 'jan',
category: 'medicatie',
}
});
});
it('should trigger AI fallback for ambiguous input', async () => {
// Step 1: Local classification (low confidence)
const localResult = classifyIntent('ik wil iets noteren');
expect(localResult.confidence).toBeLessThan(0.8);
// Step 2: AI fallback
const aiResult = await classifyIntentWithAI('ik wil iets noteren');
expect(aiResult.intent).toBe('dagnotitie');
expect(aiResult.confidence).toBeGreaterThan(0.5);
});
});
```
### Manual Test Scenarios
**Checklist:** `docs/swift/manual-test-intents.md`
```markdown
# Manual Test Scenarios - Intent System
## Dagnotitie
- [ ] "notitie jan" β Opens DagnotatieBlock with patient "jan"
- [ ] "jan medicatie" β Opens DagnotatieBlock with patient + category
- [ ] "dagnotitie" β Opens DagnotatieBlock empty
- [ ] "schrijf observatie voor marie" β AI fallback, extracts patient + category
## Zoeken
- [ ] "zoek jan" β Opens ZoekenBlock with query "jan"
- [ ] "wie is marie" β Opens ZoekenBlock
- [ ] "Jan" (single word) β Low confidence, shows options
## Agenda
- [ ] "afspraken vandaag" β Opens AgendaBlock (list, today)
- [ ] "maak afspraak jan morgen 14:00" β Opens AgendaBlock (create, prefilled)
- [ ] "annuleer jan" β Opens AgendaBlock (cancel, shows matches)
## Voice Input
- [ ] [Voice] "notitie jan medicatie" β Transcribes correctly, opens artifact
- [ ] [Voice] "bloeddruk jan honderdtwintig tachtig" β AI parses numbers
## Edge Cases
- [ ] Empty input β intent: unknown
- [ ] Gibberish β intent: unknown, triggers fallback picker
- [ ] Mixed language β AI tries to parse, may ask clarification
```
---
## 9. Referenties
### Code Locations
| Component | File Path |
|-----------|-----------|
| **Local Classifier** | `/lib/swift/intent-classifier.ts` |
| **AI Classifier** | `/lib/swift/intent-classifier-ai.ts` |
| **Type Definitions** | `/lib/swift/types.ts` |
| **State Management** | `/stores/swift-store.ts` |
| **Chat API** | `/app/api/swift/chat/route.ts` |
| **Artifacts** | `/components/swift/artifacts/blocks/` |
### External Resources
- **Regex Testing:** [regex101.com](https://regex101.com/)
- **Claude API Docs:** [docs.anthropic.com](https://docs.anthropic.com)
- **Zod Validation:** [zod.dev](https://zod.dev/)
- **TypeScript Handbook:** [typescriptlang.org](https://www.typescriptlang.org/docs/)
### Related Documentation
- **Swift FO v3.0:** `docs/swift/fo-swift-medical-scribe-v3.md`
- **Agenda FO:** `docs/swift/fo-swift-agenda-planning.md`
- **Bouwplan:** `docs/swift/bouwplan-swift-standalone-module.md`
- **Architecture Decision Records:** `docs/architecture/`
---
## π Quick Reference Card
### Adding a New Intent - Checklist
- [ ] **Step 1:** Add intent to `SwiftIntent` type (`lib/swift/types.ts`)
- [ ] **Step 2:** Extend `ExtractedEntities` with new fields
- [ ] **Step 3:** Add patterns to `INTENT_PATTERNS` (`intent-classifier.ts`)
- [ ] **Step 4:** Update AI system prompt (`intent-classifier-ai.ts`)
- [ ] **Step 5:** Update Zod schema for AI response
- [ ] **Step 6:** Add routing logic (`swift-store.ts`)
- [ ] **Step 7:** Create artifact component (`components/swift/artifacts/blocks/`)
- [ ] **Step 8:** Register artifact type in component mapping
- [ ] **Step 9:** Write unit tests
- [ ] **Step 10:** Manual testing
- [ ] **Step 11:** Update documentation
### Pattern Weight Quick Guide
```typescript
1.0 // "bloeddruk" - exact match
0.95 // "bloeddruk jan" - strong match with context
0.9 // "jan bloeddruk" - good match, reversed order
0.8 // partial match with good context
0.7 // partial match, may need AI
<0.7 // weak match, likely needs AI fallback
```
### Common Regex Patterns
```typescript
// Keywords
/^bloeddruk\b/i // Starts with "bloeddruk"
/\bbloeddruk\b/i // Contains "bloeddruk" (word boundary)
// With patient name
/^bloeddruk\s+(\w+)/i // "bloeddruk jan" - captures "jan"
/^(\w+)\s+bloeddruk/i // "jan bloeddruk" - captures "jan"
// With values
/\d{2,3}\/\d{2,3}/ // "120/80" - blood pressure format
/\d+\s*graden?/i // "38 graden" - temperature
/\d+\s*%/ // "98%" - percentage
// Case insensitive
/pattern/i // 'i' flag = case insensitive
// Word boundary
/\bword\b/ // Only matches whole word "word"
```
---
**Document Version:** 1.0
**Last Updated:** 27-12-2024
**Maintainer:** Swift Development Team
**Questions?** Check the troubleshooting section or reach out to the team.