Files
triqura-ecd/lib/swift/action-parser.ts
colinislit 9b85448a43 feat(swift): implementeer intent detection & action parsing (E3.S4)
Epic 3 Story 4 compleet: AI action objects worden geparsed en gevalideerd.

E3.S4 - Intent Detection in Response (5 SP)
- Action parser met JSON extraction uit markdown code blocks
- Zod validatie voor action schema (intent, entities, confidence, artifact)
- Confidence-based artifact opening (≥0.7 threshold)
- Visual feedback met action badges in chat messages
- Console logging voor debugging

Nieuwe Components:
- lib/swift/action-parser.ts (149 regels)
  - parseActionFromResponse(): Extract & validate JSON actions
  - extractJsonBlock(): Regex matching voor ```json blocks
  - removeJsonBlocks(): Clean text zonder JSON
  - shouldOpenArtifact(): Confidence check (≥0.7)
  - getConfidenceLabel(): "Zeer zeker", "Redelijk zeker", etc.
  - validateArtifactType(): Intent/artifact type matching

Components Updated:
- components/swift/chat/chat-panel.tsx
  - Action parsing in onDone callback
  - setPendingAction voor artifact opening (E3.S6)
  - Console logging: "[ChatPanel] Action detected"

- components/swift/chat/chat-message.tsx
  - Action badge met Sparkles icon
  - Intent label (Dagnotitie, Patiënt zoeken, Overdracht)
  - CheckCircle icon voor high confidence (≥0.7)
  - Confidence label display

- stores/swift-store.ts
  - updateLastMessage(): Optional action parameter
  - Type-safe action assignment (null/undefined handling)

Features:
- JSON extraction via regex: /```json\s*\n([\s\S]*?)\n```/
- Zod schema: type, intent, entities, confidence, artifact
- Confidence thresholds: >0.9, 0.7-0.9, 0.5-0.7, <0.5
- Visual feedback: Sparkles + CheckCircle icons
- Cleaned text content (JSON removed from display)
- Action stored in message.action en pendingAction state

Action Schema:
```typescript
{
  type: "action",
  intent: "dagnotitie" | "zoeken" | "overdracht" | "unknown",
  entities: {
    patientName?: string,
    patientId?: string,
    category?: "medicatie" | "adl" | "gedrag" | "incident" | "observatie",
    content?: string,
    query?: string
  },
  confidence: number, // 0-1
  artifact?: {
    type: BlockType,
    prefill: Record<string, any>
  }
}
```

Build Status:
-  pnpm build succesvol (geen type errors)
- Alleen bekende warnings (Supabase realtime, useCallback)

Voortgang: 46 SP / 85 SP (54%) - E3.S4 compleet

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 16:11:43 +01:00

149 lines
3.9 KiB
TypeScript

/**
* Action Parser for Swift Medical Scribe
*
* Parses JSON action objects from AI responses and validates them.
*
* Epic: E3 (Chat API & Medical Scribe)
* Story: E3.S4 (Intent detection in response)
*/
import { z } from 'zod';
import type { ChatAction, SwiftIntent, BlockType } from '@/stores/swift-store';
// Validation schema for action objects
const ActionSchema = z.object({
type: z.literal('action'),
intent: z.enum(['dagnotitie', 'zoeken', 'overdracht', 'unknown']),
entities: z.object({
patientName: z.string().optional(),
patientId: z.string().optional(),
category: z.enum(['medicatie', 'adl', 'gedrag', 'incident', 'observatie']).optional(),
content: z.string().optional(),
query: z.string().optional(), // For zoeken intent
}),
confidence: z.number().min(0).max(1),
artifact: z
.object({
type: z.enum(['dagnotitie', 'zoeken', 'overdracht', 'fallback']),
prefill: z.record(z.string(), z.any()),
})
.optional(),
});
export interface ParsedActionResult {
action: ChatAction | null;
textContent: string; // Text without JSON block
rawJson?: string; // Raw JSON string if found
}
/**
* Extract JSON code block from markdown text
* Looks for ```json ... ``` blocks
*/
function extractJsonBlock(text: string): string | null {
// Match ```json ... ``` blocks (with newlines)
const jsonBlockRegex = /```json\s*\n([\s\S]*?)\n```/;
const match = text.match(jsonBlockRegex);
if (match && match[1]) {
return match[1].trim();
}
return null;
}
/**
* Remove JSON code blocks from text
*/
function removeJsonBlocks(text: string): string {
return text.replace(/```json\s*\n[\s\S]*?\n```/g, '').trim();
}
/**
* Parse action object from AI response
*
* Extracts and validates JSON action objects from markdown code blocks.
*
* @param responseText - Full AI response text
* @returns Parsed action (if valid), cleaned text content, and raw JSON
*/
export function parseActionFromResponse(responseText: string): ParsedActionResult {
// Extract JSON block from markdown
const jsonString = extractJsonBlock(responseText);
if (!jsonString) {
return {
action: null,
textContent: responseText,
};
}
// Try to parse JSON
let parsedJson: unknown;
try {
parsedJson = JSON.parse(jsonString);
} catch (error) {
console.error('Failed to parse JSON action:', error);
return {
action: null,
textContent: responseText,
rawJson: jsonString,
};
}
// Validate against schema
const validation = ActionSchema.safeParse(parsedJson);
if (!validation.success) {
console.error('Action validation failed:', validation.error);
return {
action: null,
textContent: removeJsonBlocks(responseText),
rawJson: jsonString,
};
}
// Valid action found
const action: ChatAction = {
intent: validation.data.intent,
entities: validation.data.entities,
confidence: validation.data.confidence,
artifact: validation.data.artifact,
};
return {
action,
textContent: removeJsonBlocks(responseText),
rawJson: jsonString,
};
}
/**
* Check if confidence is high enough to open artifact
*/
export function shouldOpenArtifact(confidence: number): boolean {
return confidence >= 0.7;
}
/**
* Get user-friendly confidence label
*/
export function getConfidenceLabel(confidence: number): string {
if (confidence >= 0.9) return 'Zeer zeker';
if (confidence >= 0.7) return 'Redelijk zeker';
if (confidence >= 0.5) return 'Onzeker';
return 'Zeer onzeker';
}
/**
* Validate that artifact type matches intent
*/
export function validateArtifactType(intent: SwiftIntent, artifactType?: BlockType): boolean {
if (!artifactType) return true; // No artifact is valid
// Intent should match artifact type (except for 'unknown' and 'fallback')
if (intent === 'unknown') return artifactType === 'fallback';
return intent === artifactType;
}