Cortex handelt een no-show af vanuit één chatcommando: afspraak annuleren (declarabiliteits-nudge), en de openstaande concept- huisartsbrief wordt via LLM herschreven en ter review aangeboden in een document artifact (human-in-the-loop, PATCH dispatch zet status op verzendklaar). - API-routes: context, rescript, cancel, dispatch - NoShowDocumentBlock: review/edit UI met origineel-vergelijk - Mock-data voor concept huisartsbrief - PRD, FO, bouwplan en epics in docs/intent/noshow-case/ Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
297 lines
9.9 KiB
Markdown
297 lines
9.9 KiB
Markdown
# NS.E1 — Intent Foundation
|
||
|
||
**Casus:** Cortex No Show Afhandeling
|
||
**Epic doel:** `register_no_show` herkenbaar maken door het volledige classificatiesysteem — types, reflex patterns, en orchestrator prompt.
|
||
**Geschatte tijd:** ~1.5 uur
|
||
**Afhankelijkheden:** Geen (dit is de basis voor alle andere epics)
|
||
|
||
---
|
||
|
||
## Context & waarschuwingen
|
||
|
||
### Dubbele BlockType definitie — kritiek
|
||
|
||
Er zijn **twee** `BlockType` definities in de codebase die allebei bijgewerkt moeten worden:
|
||
|
||
| Bestand | Definitie |
|
||
|---|---|
|
||
| `lib/cortex/types.ts` | `Exclude<CortexIntent, 'unknown' \| 'intake_navigeer'> \| 'patient-dashboard'` |
|
||
| `stores/cortex-store.ts` | `Exclude<CortexIntent, 'unknown'> \| 'fallback' \| 'patient-dashboard'` |
|
||
|
||
De store-versie is breder (includeert `intake_navigeer` en `fallback`). Door `register_no_show` toe te voegen aan `CortexIntent` wordt het automatisch onderdeel van beide `BlockType` afgeleidingen — maar alleen als je ook `BLOCK_CONFIGS` bijwerkt.
|
||
|
||
### `INTENT_PATTERNS` is strikt getypeerd
|
||
|
||
`reflex-classifier.ts` regel 27:
|
||
```typescript
|
||
const INTENT_PATTERNS: Record<Exclude<CortexIntent, 'unknown'>, PatternConfig[]> = { ... }
|
||
```
|
||
|
||
Dit betekent: als je `register_no_show` toevoegt aan `CortexIntent`, **eist TypeScript** dat je ook een entry toevoegt aan `INTENT_PATTERNS`. De build faalt anders. Dit is bewust — het voorkomt vergeten patronen.
|
||
|
||
---
|
||
|
||
## NS.E1.S1 — CortexIntent type + BLOCK_CONFIGS uitbreiden
|
||
|
||
**Bestand:** `lib/cortex/types.ts`
|
||
|
||
### Wijziging 1 — CortexIntent union
|
||
|
||
Voeg `'register_no_show'` toe na de intake-intents:
|
||
|
||
```typescript
|
||
export type CortexIntent =
|
||
| 'dagnotitie'
|
||
| 'zoeken'
|
||
| 'overdracht'
|
||
| 'agenda_query'
|
||
| 'create_appointment'
|
||
| 'cancel_appointment'
|
||
| 'reschedule_appointment'
|
||
| 'intake_status'
|
||
| 'intake_navigeer'
|
||
| 'risico_query'
|
||
| 'diagnose_query'
|
||
// No Show casus
|
||
| 'register_no_show'
|
||
| 'unknown';
|
||
```
|
||
|
||
### Wijziging 2 — BLOCK_CONFIGS
|
||
|
||
Voeg een entry toe in `BLOCK_CONFIGS` (het is een `Record<BlockType, BlockConfig>` — alle BlockTypes moeten erin):
|
||
|
||
```typescript
|
||
register_no_show: {
|
||
type: 'register_no_show',
|
||
title: 'No Show Registratie',
|
||
size: 'md',
|
||
icon: 'UserX',
|
||
},
|
||
```
|
||
|
||
Locatie: onderaan de `BLOCK_CONFIGS` definitie, na `diagnose_query`.
|
||
|
||
### Done criteria
|
||
- `pnpm build` slaagt — geen TypeScript errors
|
||
- `register_no_show` is een geldige waarde voor `CortexIntent`
|
||
- `BLOCK_CONFIGS['register_no_show']` bestaat
|
||
|
||
---
|
||
|
||
## NS.E1.S2 — Reflex Classifier patronen toevoegen
|
||
|
||
**Bestand:** `lib/cortex/reflex-classifier.ts`
|
||
|
||
### Context
|
||
|
||
`INTENT_PATTERNS` is getypeerd als `Record<Exclude<CortexIntent, 'unknown'>, PatternConfig[]>`. Na toevoeging van `register_no_show` aan de union **moet** je hier ook een entry toevoegen — anders faalt de build met:
|
||
|
||
```
|
||
Type '{ dagnotitie: ...; ... }' is missing the following properties
|
||
from type 'Record<...>': register_no_show
|
||
```
|
||
|
||
### Wijziging — patronen toevoegen
|
||
|
||
Voeg toe aan `INTENT_PATTERNS`, na het `diagnose_query` blok:
|
||
|
||
```typescript
|
||
// =========================================================================
|
||
// No Show casus
|
||
// =========================================================================
|
||
register_no_show: [
|
||
// Exacte no-show varianten
|
||
{ pattern: /no.?show/i, weight: 1.0 },
|
||
{ pattern: /no show/i, weight: 1.0 },
|
||
|
||
// "Niet verschenen" varianten
|
||
{ pattern: /niet\s+verschenen/i, weight: 0.95 },
|
||
{ pattern: /niet\s+gekomen/i, weight: 0.95 },
|
||
{ pattern: /niet\s+op\s+komen\s+dagen/i, weight: 0.95 },
|
||
|
||
// "Afwezig" + context
|
||
{ pattern: /afwezig\s+(bij|voor)\s+(de\s+)?afspraak/i, weight: 0.9 },
|
||
{ pattern: /pati[eë]nt\s+afwezig/i, weight: 0.85 },
|
||
|
||
// Werkwoordvormen
|
||
{ pattern: /komt?\s+niet\s+(op|naar)/i, weight: 0.85 },
|
||
{ pattern: /is\s+er\s+niet\s+(geweest)?/i, weight: 0.7 },
|
||
],
|
||
```
|
||
|
||
**Toelichting weights:**
|
||
- `1.0` — directe "no show" termen, geen ambiguïteit
|
||
- `0.95` — sterke klinische uitdrukkingen
|
||
- `0.85–0.9` — contextuele varianten
|
||
- `0.7` — vaag ("is er niet geweest") — escaleer naar AI bij twijfel
|
||
|
||
### Escalatie check
|
||
|
||
Controleer of de bestaande escalatiepatronen niet per ongeluk triggeren op no-show input:
|
||
- `"patiënt niet verschenen"` — bevat geen conjuncties → geen multi_intent
|
||
- `"patiënt is er niet"` — bevat geen voornaamwoorden die escaleren → OK
|
||
- `"patiënt niet verschenen morgen"` — bevat `morgen` → triggert `relative_time` escalatie → correct (AI lost dit op)
|
||
|
||
### Done criteria
|
||
- `classifyWithReflex("patiënt is niet verschenen")` geeft `{ intent: 'register_no_show', confidence: 0.95, shouldEscalateToAI: false }`
|
||
- `classifyWithReflex("no show vandaag")` geeft `{ intent: 'register_no_show', confidence: 1.0, shouldEscalateToAI: false }`
|
||
- `classifyWithReflex("patiënt niet verschenen morgen")` geeft `shouldEscalateToAI: true, escalationReason: 'relative_time'`
|
||
- `pnpm build` slaagt — TypeScript tevreden met de nieuwe entry
|
||
|
||
---
|
||
|
||
## NS.E1.S3 — Orchestrator system prompt uitbreiden
|
||
|
||
**Bestand:** `app/api/cortex/chat/route.ts`
|
||
|
||
### Context
|
||
|
||
De chat API bouwt een system prompt via `buildSystemPrompt(context)`. Die prompt bevat een opsomming van alle intents met beschrijvingen. De Orchestrator (Layer 2) én de chat AI (Layer 3) gebruiken allebei deze prompt.
|
||
|
||
Zoek de sectie in de prompt waar intents worden opgesomd — het zal er zo uitzien:
|
||
```
|
||
- dagnotitie: Gebruik wanneer...
|
||
- zoeken: Gebruik wanneer...
|
||
```
|
||
|
||
### Wijziging — intent beschrijving toevoegen
|
||
|
||
Voeg toe aan de intent-opsomming in `buildSystemPrompt`:
|
||
|
||
```
|
||
- register_no_show: Gebruik wanneer de zorgverlener aangeeft dat een patiënt
|
||
niet op de geplande afspraak is verschenen. Signaalwoorden: "no show",
|
||
"niet verschenen", "niet gekomen", "afwezig bij afspraak", "komt niet op".
|
||
Entiteiten: geen specifieke entiteiten nodig — de actieve patiënt en
|
||
huidige context worden gebruikt.
|
||
```
|
||
|
||
### Done criteria
|
||
- System prompt in de chat API bevat `register_no_show` met beschrijving
|
||
- Handmatige test: typ "cliënt was er niet vandaag" in de chat → AI classificeert als `register_no_show` (zichtbaar in console via `[ChatPanel] Action detected:`)
|
||
- Typ "patiënt heeft de afspraak gemist" → zelfde resultaat
|
||
|
||
---
|
||
|
||
---
|
||
|
||
## NS.E1.S4 — `action-parser.ts` uitbreiden
|
||
|
||
**Bestand:** `lib/cortex/action-parser.ts`
|
||
|
||
### Context — waarom dit hier hoort
|
||
|
||
`action-parser.ts` bevat vier hardcoded enums en switches die de volledige lijst van geldige intents bevatten. Ze zijn **niet** afgeleid van `CortexIntent` in `types.ts` — ze zijn handmatig gesynchroniseerd. Dit is het meest kritieke gat: als `register_no_show` hier ontbreekt, faalt de Zod validatie silently op de AI response en wordt `parsed.action` altijd `null`. De nudge triggert dan **nooit** — ook niet na alle andere fixes.
|
||
|
||
### Wijziging 1 — `ActionSchema.intent` enum (regel ~16)
|
||
|
||
Voeg `'register_no_show'` toe aan de `z.enum([...])` lijst:
|
||
|
||
```typescript
|
||
intent: z.enum([
|
||
'dagnotitie',
|
||
'zoeken',
|
||
'overdracht',
|
||
'agenda_query',
|
||
'create_appointment',
|
||
'cancel_appointment',
|
||
'reschedule_appointment',
|
||
'intake_status',
|
||
'intake_navigeer',
|
||
'risico_query',
|
||
'diagnose_query',
|
||
// No Show casus
|
||
'register_no_show',
|
||
'unknown',
|
||
]),
|
||
```
|
||
|
||
### Wijziging 2 — `ActionSchema.artifact.type` enum (regel ~77)
|
||
|
||
Voeg `'register_no_show'` toe aan de artifact type enum:
|
||
|
||
```typescript
|
||
type: z.enum([
|
||
'dagnotitie',
|
||
'zoeken',
|
||
'overdracht',
|
||
'agenda_query',
|
||
'create_appointment',
|
||
'cancel_appointment',
|
||
'reschedule_appointment',
|
||
'intake_status',
|
||
'risico_query',
|
||
'diagnose_query',
|
||
'fallback',
|
||
'patient-dashboard',
|
||
// No Show casus
|
||
'register_no_show',
|
||
]),
|
||
```
|
||
|
||
### Wijziging 3 — `routeIntentToArtifact` switch
|
||
|
||
De switch heeft een `default: return null`. Voeg een expliciete case toe **vóór** de `default`, na het `intake_navigeer` blok:
|
||
|
||
```typescript
|
||
case 'register_no_show':
|
||
// Artifact wordt geopend via de no-show handler in chat-panel.tsx,
|
||
// niet via de generieke routing. Geef null terug zodat de handler
|
||
// de controle houdt.
|
||
return null;
|
||
```
|
||
|
||
**Toelichting:** We returnen bewust `null` — het artifact voor no-show wordt door `handleNoShowRescriptStep` geopend met volledige prefill data (documentId, content, originalContent) die pas beschikbaar is ná de rescript API call. De generieke router heeft die data niet.
|
||
|
||
### Wijziging 4 — `getDefaultConfirmationMessage` switch
|
||
|
||
Voeg toe na het `overdracht` case, vóór de `default`:
|
||
|
||
```typescript
|
||
case 'register_no_show':
|
||
return 'Ik registreer de no show en controleer de agenda op declarabiliteit.';
|
||
```
|
||
|
||
### Wijziging 5 — `getAcceptButtonText` in `nudge-chat-message.tsx`
|
||
|
||
**Bestand:** `components/cortex/chat/nudge-chat-message.tsx`
|
||
|
||
De `getAcceptButtonText` functie bepaalt de knoptekst op basis van `suggestion.suggestion.intent`:
|
||
- Nudge 1 heeft `intent: 'cancel_appointment'` → toont al correct **"Ja, annuleren"**
|
||
- Nudge 2 heeft `intent: 'register_no_show'` → valt op `default: 'Ja, uitvoeren'`
|
||
|
||
Voeg toe in de switch van `getAcceptButtonText`:
|
||
|
||
```typescript
|
||
case 'register_no_show':
|
||
return 'Ja, pas brief aan';
|
||
```
|
||
|
||
### Done criteria
|
||
- `ActionSchema.safeParse({ type: 'action', intent: 'register_no_show', entities: {}, confidence: 0.9 })` geeft `success: true`
|
||
- `routeIntentToArtifact('register_no_show', {}, 0.9)` geeft `null` terug (geen artifact via generieke router)
|
||
- `getDefaultConfirmationMessage('register_no_show', {})` geeft correcte Nederlandse zin
|
||
- Nudge 2 accept-knop toont "Ja, pas brief aan"
|
||
- `pnpm build` slaagt
|
||
|
||
---
|
||
|
||
## Validatie na NS.E1 (alle stories)
|
||
|
||
Run na voltooiing van alle vier stories:
|
||
|
||
```bash
|
||
pnpm build
|
||
pnpm lint
|
||
```
|
||
|
||
Verwacht: geen errors. Als er TypeScript errors zijn over `INTENT_PATTERNS` of `BLOCK_CONFIGS`, controleer dan of beide `Record<>` types volledig zijn bijgewerkt.
|
||
|
||
**Snelle handmatige smoke test:**
|
||
1. Open de app (`pnpm dev`)
|
||
2. Navigeer naar het Cortex dashboard
|
||
3. Typ: `"patiënt niet verschenen"`
|
||
4. Verwacht: AI response met intent `register_no_show` zichtbaar in browser console via `[ChatPanel] Action detected: { intent: 'register_no_show', ... }`
|
||
5. Verwacht: nudge bubble verschijnt (NS.E2 vereist — maar de action parse moet nu wel slagen)
|