Files
triqura-ecd/lib/swift/intent-classifier.ts
colinislit db036a3d92 feat(swift): E2 Intent Classification voltooid
Epic 2 - Intent Classification (10 SP):
- E2.S1: Local classifier met regex patterns (<50ms)
- E2.S2: Entity extraction (patientName, category, content)
- E2.S3: AI fallback met Claude Haiku bij confidence <0.8
- E2.S4: POST /api/intent/classify API route

Two-tier systeem:
1. Lokale regex classificatie (snel, offline)
2. AI fallback voor lage confidence of complexe input

Bouwplan bijgewerkt naar v1.4 (31/68 SP, 46% done)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Colin Lit <colin@example.com>
2025-12-23 22:59:30 +01:00

190 lines
5.6 KiB
TypeScript

/**
* Local Intent Classifier
*
* Fast regex-based intent classification for Swift.
* Target: <50ms response time.
*/
import type { SwiftIntent } from './types';
export interface ClassificationResult {
intent: SwiftIntent;
confidence: number;
matchedPattern?: string;
processingTimeMs: number;
}
interface PatternConfig {
pattern: RegExp;
weight: number; // Higher weight = higher confidence
}
// Intent patterns with weights
// Weight 1.0 = exact match, 0.8 = strong match, 0.6 = partial match
const INTENT_PATTERNS: Record<Exclude<SwiftIntent, 'unknown'>, PatternConfig[]> = {
dagnotitie: [
// Exact commands
{ pattern: /^dagnotitie\b/i, weight: 1.0 },
{ pattern: /^notitie\b/i, weight: 1.0 },
{ pattern: /^nieuwe?\s+notitie\b/i, weight: 1.0 },
// Pattern: "notitie [naam]" or "[naam] notitie"
{ pattern: /^notitie\s+\w+/i, weight: 0.95 },
{ pattern: /^\w+\s+notitie\b/i, weight: 0.85 },
// Pattern: "[naam] [categorie]" (e.g., "jan medicatie")
{ pattern: /^\w+\s+(medicatie|adl|gedrag|incident|observatie)\b/i, weight: 0.9 },
// Pattern: "notitie [naam] [categorie]"
{ pattern: /^notitie\s+\w+\s+(medicatie|adl|gedrag|incident|observatie)\b/i, weight: 1.0 },
// Categorie-first patterns
{ pattern: /^(medicatie|adl|gedrag|incident|observatie)\s+\w+/i, weight: 0.85 },
{ pattern: /^(medicatie|adl|gedrag|incident|observatie)\b/i, weight: 0.7 },
// Schrijf patterns
{ pattern: /^schrijf\b/i, weight: 0.8 },
{ pattern: /^rapporteer\b/i, weight: 0.8 },
{ pattern: /^registreer\b/i, weight: 0.8 },
],
zoeken: [
// Exact commands
{ pattern: /^zoek\b/i, weight: 1.0 },
{ pattern: /^vind\b/i, weight: 1.0 },
{ pattern: /^zoeken\b/i, weight: 1.0 },
// Question patterns
{ pattern: /^wie\s+is\b/i, weight: 1.0 },
{ pattern: /^waar\s+is\b/i, weight: 0.9 },
{ pattern: /^welke\s+pati[eë]nt/i, weight: 0.9 },
// Pattern: "zoek [naam]"
{ pattern: /^zoek\s+\w+/i, weight: 1.0 },
{ pattern: /^vind\s+\w+/i, weight: 1.0 },
// Info requests
{ pattern: /^info\s+\w+/i, weight: 0.8 },
{ pattern: /^gegevens\s+\w+/i, weight: 0.8 },
{ pattern: /^dossier\s+\w+/i, weight: 0.85 },
// Partial name lookups (single word that could be a name)
{ pattern: /^[A-Z][a-z]+$/i, weight: 0.5 }, // Single capitalized word
],
overdracht: [
// Exact commands
{ pattern: /^overdracht\b/i, weight: 1.0 },
{ pattern: /^dienst\s*overdracht\b/i, weight: 1.0 },
// Dienst patterns
{ pattern: /^dienst\s+(klaar|afronden|be[eë]indigen)\b/i, weight: 1.0 },
{ pattern: /^einde?\s+dienst\b/i, weight: 1.0 },
{ pattern: /^dienst\s+einde?\b/i, weight: 1.0 },
// Question patterns
{ pattern: /^wat\s+moet\s+ik\s+weten\b/i, weight: 1.0 },
{ pattern: /^wat\s+is\s+er\s+gebeurd\b/i, weight: 0.9 },
{ pattern: /^updates?\b/i, weight: 0.7 },
{ pattern: /^samenvatting\b/i, weight: 0.85 },
// Start dienst
{ pattern: /^start\s+dienst\b/i, weight: 0.9 },
{ pattern: /^begin\s+dienst\b/i, weight: 0.9 },
{ pattern: /^nieuwe?\s+dienst\b/i, weight: 0.85 },
],
};
// Help patterns (separate, always check)
const HELP_PATTERNS: PatternConfig[] = [
{ pattern: /^help\b/i, weight: 1.0 },
{ pattern: /^hulp\b/i, weight: 1.0 },
{ pattern: /^\?\s*$/i, weight: 1.0 },
{ pattern: /^wat\s+kan\s+(ik|je|swift)\b/i, weight: 1.0 },
{ pattern: /^hoe\s+werkt\b/i, weight: 0.9 },
{ pattern: /^voorbeelden?\b/i, weight: 0.9 },
];
/**
* Classify user input into an intent using local regex patterns.
* Fast, runs entirely client-side.
*/
export function classifyIntent(input: string): ClassificationResult {
const startTime = performance.now();
const trimmedInput = input.trim();
// Empty input
if (!trimmedInput) {
return {
intent: 'unknown',
confidence: 0,
processingTimeMs: performance.now() - startTime,
};
}
// Check for help first
for (const { pattern, weight } of HELP_PATTERNS) {
if (pattern.test(trimmedInput)) {
return {
intent: 'unknown', // Help is handled separately, return unknown to trigger help UI
confidence: weight,
matchedPattern: pattern.toString(),
processingTimeMs: performance.now() - startTime,
};
}
}
// Find best matching intent
let bestMatch: { intent: SwiftIntent; confidence: number; pattern: string } | null = null;
for (const [intent, patterns] of Object.entries(INTENT_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.toString(),
};
}
// If we found a perfect match, we can stop
if (weight === 1.0) break;
}
}
// Early exit on perfect match
if (bestMatch?.confidence === 1.0) break;
}
const processingTimeMs = performance.now() - startTime;
if (bestMatch) {
return {
intent: bestMatch.intent,
confidence: bestMatch.confidence,
matchedPattern: bestMatch.pattern,
processingTimeMs,
};
}
// No match found
return {
intent: 'unknown',
confidence: 0,
processingTimeMs,
};
}
/**
* Check if classification confidence is high enough to proceed without AI fallback.
*/
export function isHighConfidence(result: ClassificationResult): boolean {
return result.confidence >= 0.8;
}
/**
* Check if we should show the fallback picker.
*/
export function shouldShowFallback(result: ClassificationResult): boolean {
return result.intent === 'unknown' || result.confidence < 0.5;
}