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>
375 lines
12 KiB
Markdown
375 lines
12 KiB
Markdown
# NS.E2 — Nudge Rules & Chained Flow
|
|
|
|
**Casus:** Cortex No Show Afhandeling
|
|
**Epic doel:** Twee proactieve nudges bouwen die sequentieel volgen op de no-show registratie, plus de store-uitbreiding die de multi-step state bijhoudt.
|
|
**Geschatte tijd:** ~2 uur
|
|
**Afhankelijkheden:** NS.E1 volledig klaar (CortexIntent type moet bestaan)
|
|
|
|
---
|
|
|
|
## Context & waarschuwingen
|
|
|
|
### Nudge evaluatie wordt NIET aangeroepen vanuit de hoofd-chat flow
|
|
|
|
Dit is het grootste architecturele gat. `evaluateNudge()` wordt momenteel **alleen** aangeroepen vanuit `handleConfirmAction` in `chat-panel.tsx` — dat is de V2 chain flow (feature-flagged). De hoofd-chat SSE flow (`onDone` callback) roept dit **niet** aan.
|
|
|
|
Gevolg: als een gebruiker "patiënt niet verschenen" typt en de AI antwoordt met `register_no_show`, triggert er **geen nudge** via de protocol rules engine.
|
|
|
|
**Oplossing in dit epic:** We voegen nudge-evaluatie toe aan de `onDone` callback van de hoofd-chat flow, maar alleen voor de `register_no_show` intent. Dit is bewust scopebegrensd — geen generieke uitbreiding van de flow.
|
|
|
|
### NudgeSuggestion ID bevat timestamp
|
|
|
|
Nudge IDs worden gegenereerd als `nudge-${rule.id}-${Date.now()}`. We kunnen dus **niet** matchen op het volledige ID in `handleAcceptNudge`. Match op `suggestion.trigger.intent` of de `suggestion.suggestion.rationale` (= `rule.name`).
|
|
|
|
**Gekozen aanpak:** `suggestion.trigger.intent` gebruiken als discriminator.
|
|
- Nudge 1: `trigger.intent === 'register_no_show'`
|
|
- Nudge 2: handmatig geconstrueerd (niet via `evaluateNudge`) — zie NS.E2.S3
|
|
|
|
### ProtocolRule interface vereisten
|
|
|
|
De `ProtocolRule` interface in `nudge.ts` heeft **verplichte** velden die het bouwplan oorspronkelijk wegliet:
|
|
|
|
```typescript
|
|
interface ProtocolRule {
|
|
id: string;
|
|
name: string; // ← verplicht
|
|
trigger: {
|
|
intent: CortexIntent;
|
|
conditions: ProtocolCondition[]; // ← verplicht, mag [] zijn
|
|
};
|
|
suggestion: {
|
|
intent: CortexIntent;
|
|
message: string;
|
|
prefillEntities: (source: ExtractedEntities) => Partial<ExtractedEntities>; // ← FUNCTIE, niet object
|
|
};
|
|
protocol?: ProtocolMetadata;
|
|
priority: NudgePriority;
|
|
enabled: boolean; // ← verplicht
|
|
expiresAfterMs: number;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## NS.E2.S1 — No-show flow state aan store toevoegen
|
|
|
|
**Bestand:** `stores/cortex-store.ts`
|
|
|
|
### Context
|
|
|
|
De no-show flow heeft 5 sequentiële stappen. We slaan de state op in de Zustand store zodat alle componenten er toegang toe hebben.
|
|
|
|
### Wijziging 1 — Types toevoegen (boven `CortexStore` interface)
|
|
|
|
```typescript
|
|
// No-show flow state machine
|
|
export type NoShowStep = 'idle' | 'waiting_cancel' | 'waiting_brief' | 'brief_open' | 'done';
|
|
|
|
export interface NoShowFlowState {
|
|
step: NoShowStep;
|
|
appointmentId: string | null;
|
|
documentId: string | null;
|
|
originalContent: string | null;
|
|
}
|
|
```
|
|
|
|
### Wijziging 2 — State toevoegen aan `CortexStore` interface
|
|
|
|
Voeg toe na `pendingClarification`:
|
|
```typescript
|
|
// No-show flow state
|
|
noShowFlow: NoShowFlowState;
|
|
```
|
|
|
|
Voeg actions toe na bestaande clarification actions:
|
|
```typescript
|
|
// No-show flow actions
|
|
setNoShowStep: (step: NoShowStep) => void;
|
|
setNoShowContext: (ctx: Partial<Omit<NoShowFlowState, 'step'>>) => void;
|
|
resetNoShowFlow: () => void;
|
|
```
|
|
|
|
### Wijziging 3 — Initiële waarde in `initialState`
|
|
|
|
```typescript
|
|
noShowFlow: {
|
|
step: 'idle',
|
|
appointmentId: null,
|
|
documentId: null,
|
|
originalContent: null,
|
|
} as NoShowFlowState,
|
|
```
|
|
|
|
### Wijziging 4 — Actions implementeren (in `create()` body)
|
|
|
|
```typescript
|
|
setNoShowStep: (step) =>
|
|
set(
|
|
(state) => ({ noShowFlow: { ...state.noShowFlow, step } }),
|
|
false,
|
|
'setNoShowStep'
|
|
),
|
|
|
|
setNoShowContext: (ctx) =>
|
|
set(
|
|
(state) => ({ noShowFlow: { ...state.noShowFlow, ...ctx } }),
|
|
false,
|
|
'setNoShowContext'
|
|
),
|
|
|
|
resetNoShowFlow: () =>
|
|
set(
|
|
{
|
|
noShowFlow: {
|
|
step: 'idle',
|
|
appointmentId: null,
|
|
documentId: null,
|
|
originalContent: null,
|
|
},
|
|
},
|
|
false,
|
|
'resetNoShowFlow'
|
|
),
|
|
```
|
|
|
|
### Done criteria
|
|
- `pnpm build` slaagt
|
|
- `useCortexStore(s => s.noShowFlow)` geeft `{ step: 'idle', ... }` terug
|
|
- `useCortexStore(s => s.setNoShowStep)('waiting_cancel')` werkt zonder error
|
|
|
|
---
|
|
|
|
## NS.E2.S2 — Nudge Rule 1: Declarabiliteitscheck
|
|
|
|
**Bestand:** `lib/cortex/nudge.ts`
|
|
|
|
### Wijziging — rule toevoegen aan `PROTOCOL_RULES`
|
|
|
|
Voeg toe **vóór** de bestaande wondzorg-regel (hogere priority verdient eerste positie):
|
|
|
|
```typescript
|
|
{
|
|
id: 'noshow-declarabel-check',
|
|
name: 'No Show declarabiliteitscheck',
|
|
trigger: {
|
|
intent: 'register_no_show',
|
|
conditions: [], // Geen aanvullende condities — altijd triggeren bij no-show
|
|
},
|
|
suggestion: {
|
|
intent: 'cancel_appointment',
|
|
message: 'Ik zie een declarabel consult in de agenda. Volgens inkoopvoorwaarden mag deze afspraak NIET gedeclareerd worden. Wil je dat ik deze annuleer als \'No Show\'?',
|
|
prefillEntities: (_source) => ({}), // Geen prefill nodig — context wordt via API opgehaald
|
|
},
|
|
priority: 'high',
|
|
enabled: true,
|
|
expiresAfterMs: DEFAULT_EXPIRY_MS,
|
|
},
|
|
```
|
|
|
|
**Let op:** Er is geen `protocol` metadata voor deze rule — dat is optioneel en ontbreekt hier bewust (het gaat om een inkoopverplichting, geen klinisch protocol).
|
|
|
|
### Done criteria
|
|
- `evaluateNudge({ intent: 'register_no_show', actionId: 'test', entities: {}, content: '' })` geeft een array terug met één nudge
|
|
- De nudge heeft `priority: 'high'` en `suggestion.intent: 'cancel_appointment'`
|
|
- De message bevat "declarabel"
|
|
|
|
---
|
|
|
|
## NS.E2.S3 — Hoofd-chat flow uitbreiden met nudge trigger
|
|
|
|
**Bestand:** `components/cortex/chat/chat-panel.tsx`
|
|
|
|
### Context
|
|
|
|
De nudge voor no-show moet triggeren ná de AI-response, niet ná een chain action. Dat betekent: uitbreiding van de `onDone` callback in de `ChatInput` `onSend` handler.
|
|
|
|
Momenteel (vereenvoudigd):
|
|
```typescript
|
|
onDone: () => {
|
|
setStreaming(false);
|
|
const parsed = parseActionFromResponse(accumulatedContent);
|
|
if (parsed.action) {
|
|
updateLastMessage(...);
|
|
setPendingAction(parsed.action);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Stap 1 — Store actions ophalen
|
|
|
|
Voeg bovenaan `ChatPanel()` toe:
|
|
```typescript
|
|
const noShowFlow = useCortexStore((s) => s.noShowFlow);
|
|
const setNoShowStep = useCortexStore((s) => s.setNoShowStep);
|
|
const setNoShowContext = useCortexStore((s) => s.setNoShowContext);
|
|
```
|
|
|
|
### Stap 2 — Nudge trigger in `onDone`
|
|
|
|
Voeg toe in de `onDone` callback, **ná** het bestaande `setPendingAction` blok:
|
|
|
|
```typescript
|
|
// No-show flow: trigger nudge na register_no_show intent
|
|
if (parsed.action?.intent === 'register_no_show' && isFeatureEnabled('CORTEX_NUDGE')) {
|
|
const suggestions = evaluateNudge({
|
|
intent: 'register_no_show',
|
|
actionId: crypto.randomUUID(),
|
|
entities: parsed.action.entities,
|
|
content: message, // originele user input
|
|
});
|
|
|
|
suggestions.forEach((suggestion) => {
|
|
addChatMessage({
|
|
type: 'nudge',
|
|
content: suggestion.suggestion.message,
|
|
nudge: suggestion,
|
|
});
|
|
});
|
|
}
|
|
```
|
|
|
|
### Stap 3 — `handleAcceptNudge` uitbreiden met no-show logica
|
|
|
|
De bestaande `handleAcceptNudge` doet: `acceptSuggestion` → `routeIntentToArtifact` → `openArtifact`. We voegen een **vroege exit** toe voor no-show nudges die een aparte flow hebben.
|
|
|
|
Voeg toe aan het begin van `handleAcceptNudge`, vóór de bestaande `routeIntentToArtifact` aanroep:
|
|
|
|
```typescript
|
|
const handleAcceptNudge = useCallback(async (
|
|
suggestionId: string,
|
|
suggestion: ChatMessageType['nudge']
|
|
) => {
|
|
acceptSuggestion(suggestionId);
|
|
|
|
if (!suggestion) return;
|
|
|
|
// --- No-show flow: stap 2 → stap 3 ---
|
|
if (suggestion.trigger.intent === 'register_no_show') {
|
|
await handleNoShowCancelStep(suggestion);
|
|
return; // Vroege exit — geen generieke artifact routing
|
|
}
|
|
|
|
// Bestaande generieke flow (voor alle andere nudges)
|
|
const artifact = routeIntentToArtifact(
|
|
suggestion.suggestion.intent,
|
|
suggestion.suggestion.entities,
|
|
0.9
|
|
);
|
|
if (artifact) {
|
|
openArtifact({ type: artifact.type, prefill: artifact.prefill, title: artifact.title });
|
|
}
|
|
}, [acceptSuggestion, openArtifact, setNoShowStep, setNoShowContext, addChatMessage, activePatient]);
|
|
```
|
|
|
|
**Let op:** `handleAcceptNudge` is nu `async`. Controleer of de prop-definitie in `NudgeChatMessage` dit ondersteunt — zo niet, pas de prop type aan.
|
|
|
|
### Stap 4 — `handleNoShowCancelStep` functie
|
|
|
|
Voeg toe als aparte `useCallback` in `ChatPanel`:
|
|
|
|
```typescript
|
|
const handleNoShowCancelStep = useCallback(async (suggestion: NudgeSuggestion) => {
|
|
setNoShowStep('waiting_cancel');
|
|
|
|
// Voeg verwerking chat message toe
|
|
addChatMessage({ type: 'assistant', content: 'Bezig met annuleren...' });
|
|
|
|
try {
|
|
// Annuleer de afspraak
|
|
const cancelRes = await fetch('/api/cortex/noshow/cancel', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
appointmentId: 'mock-appt-noshow-001', // mock voor nu
|
|
patientId: activePatient?.id ?? 'demo-patient-001',
|
|
}),
|
|
});
|
|
|
|
if (!cancelRes.ok) throw new Error('Cancel mislukt');
|
|
|
|
// Check op concept brief
|
|
const patientId = activePatient?.id ?? 'demo-patient-001';
|
|
const ctxRes = await fetch(`/api/cortex/noshow/context?patientId=${patientId}`);
|
|
const ctx = await ctxRes.json();
|
|
|
|
if (ctx.hasConceptBrief) {
|
|
setNoShowContext({
|
|
documentId: ctx.document.id,
|
|
originalContent: ctx.document.content,
|
|
});
|
|
setNoShowStep('waiting_brief');
|
|
|
|
// Construeer nudge 2 handmatig (niet via evaluateNudge)
|
|
const briefNudge: NudgeSuggestion = {
|
|
id: `nudge-noshow-brief-${Date.now()}`,
|
|
trigger: {
|
|
actionId: 'noshow-cancel-done',
|
|
intent: 'cancel_appointment',
|
|
entities: {},
|
|
},
|
|
suggestion: {
|
|
intent: 'register_no_show', // gebruikt als signaal voor brief-stap
|
|
entities: {},
|
|
message: 'Afspraak geannuleerd. Er staat nog een concept huisartsbrief klaar. Zal ik daar de No Show in verwerken?',
|
|
rationale: 'noshow-brief-check', // gebruikt als discriminator in handleAcceptNudge stap 5
|
|
},
|
|
status: 'pending',
|
|
priority: 'high',
|
|
expiresAt: new Date(Date.now() + 5 * 60 * 1000),
|
|
createdAt: new Date(),
|
|
};
|
|
|
|
addChatMessage({ type: 'nudge', content: briefNudge.suggestion.message, nudge: briefNudge });
|
|
} else {
|
|
setNoShowStep('done');
|
|
addChatMessage({
|
|
type: 'assistant',
|
|
content: 'Afspraak geannuleerd als No Show. Er zijn geen openstaande conceptbrieven gevonden.',
|
|
});
|
|
}
|
|
} catch {
|
|
setNoShowStep('idle');
|
|
addChatMessage({
|
|
type: 'error',
|
|
content: 'Er is iets misgegaan bij het annuleren. Probeer het opnieuw.',
|
|
});
|
|
}
|
|
}, [activePatient, setNoShowStep, setNoShowContext, addChatMessage]);
|
|
```
|
|
|
|
### Stap 5 — `handleAcceptNudge` uitbreiden voor brief-stap
|
|
|
|
In de bestaande `handleAcceptNudge`, voeg een tweede no-show check toe vóór de generieke flow:
|
|
|
|
```typescript
|
|
// --- No-show flow: stap 4 → brief openen ---
|
|
if (suggestion.suggestion.rationale === 'noshow-brief-check') {
|
|
await handleNoShowRescriptStep();
|
|
return;
|
|
}
|
|
```
|
|
|
|
Voeg `handleNoShowRescriptStep` toe als aparte `useCallback` (zie NS.E3.S4 voor de rescript API, die hier aangeroepen wordt). De volledige implementatie staat in NS.E5.S1.
|
|
|
|
### Done criteria
|
|
- Typen "patiënt niet verschenen" → AI antwoordt → nudge 1 verschijnt in chat
|
|
- Klikken `[Ja]` op nudge 1 → cancel API aangeroepen → nudge 2 verschijnt
|
|
- Klikken `[Ja]` op nudge 2 → rescript API aangeroepen → artifact opent (NS.E4)
|
|
- Klikken `[Nee]` op nudge 1 → `dismissSuggestion` → nudge verdwijnt, geen verdere actie
|
|
- `noShowFlow.step` doorloopt correct: `idle` → `waiting_cancel` → `waiting_brief` → `brief_open`
|
|
|
|
---
|
|
|
|
## Validatie na NS.E2
|
|
|
|
```bash
|
|
pnpm build
|
|
pnpm lint
|
|
```
|
|
|
|
**Visuele check:**
|
|
1. Typ `"patiënt niet verschenen"` in de chat
|
|
2. Wacht op AI response
|
|
3. Verwacht: nudge bubble met "Ik zie een declarabel consult..." en `[Ja, annuleer]` / `[Nee]` knoppen
|
|
4. Browser console toont: `[ChatPanel] Nudge suggestions (chat-based): 1`
|