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>
12 KiB
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:
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)
// 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:
// No-show flow state
noShowFlow: NoShowFlowState;
Voeg actions toe na bestaande clarification actions:
// No-show flow actions
setNoShowStep: (step: NoShowStep) => void;
setNoShowContext: (ctx: Partial<Omit<NoShowFlowState, 'step'>>) => void;
resetNoShowFlow: () => void;
Wijziging 3 — Initiële waarde in initialState
noShowFlow: {
step: 'idle',
appointmentId: null,
documentId: null,
originalContent: null,
} as NoShowFlowState,
Wijziging 4 — Actions implementeren (in create() body)
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 buildslaagtuseCortexStore(s => s.noShowFlow)geeft{ step: 'idle', ... }teruguseCortexStore(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):
{
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'ensuggestion.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):
onDone: () => {
setStreaming(false);
const parsed = parseActionFromResponse(accumulatedContent);
if (parsed.action) {
updateLastMessage(...);
setPendingAction(parsed.action);
}
}
Stap 1 — Store actions ophalen
Voeg bovenaan ChatPanel() toe:
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:
// 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:
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:
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:
// --- 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.stepdoorloopt correct:idle→waiting_cancel→waiting_brief→brief_open
Validatie na NS.E2
pnpm build
pnpm lint
Visuele check:
- Typ
"patiënt niet verschenen"in de chat - Wacht op AI response
- Verwacht: nudge bubble met "Ik zie een declarabel consult..." en
[Ja, annuleer]/[Nee]knoppen - Browser console toont:
[ChatPanel] Nudge suggestions (chat-based): 1