feat(cortex): Epic 0 - Foundation & Context voor V2 architectuur
Implementeert de basis voor de Cortex V2 three-layer architecture. E0.S1 - CortexContext Types (lib/cortex/types.ts): - EscalationReason type voor Reflex → Orchestrator - LocalClassificationResult met ambiguity detection - CortexContext voor AI classificatie context - IntentChain en IntentAction voor multi-intent flows - NudgeSuggestion voor proactieve suggesties - CONFIDENCE_THRESHOLD (0.7) en AMBIGUITY_THRESHOLD (0.1) - getCurrentShift() utility (DRY - herbruikbaar) E0.S2 - Context API (app/api/cortex/context/route.ts): - GET endpoint voor huidige context - Haalt agenda vandaag op uit database - Retourneert CortexContext object E0.S3 - Feature Flags (lib/config/feature-flags.ts): - CORTEX_V2_ENABLED, CORTEX_MULTI_INTENT - CORTEX_NUDGE, CORTEX_LOGGING - isFeatureEnabled() helper - Dev mode: default true E0.S4 - CortexStore V2 (stores/cortex-store.ts): - Types geïmporteerd uit lib/cortex (DRY) - ChatEntities type voor AI responses (string-based) - V2 state: context, activeChain, chainHistory, suggestions - V2 actions: chain management, nudge management, clarification E0.S5 - Classification Logging (lib/cortex/logger.ts): - PII sanitization (BSN, tel, namen, email) - logClassification(), logEscalation(), logNudge() - logPerformance() met threshold checks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,26 +1,58 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import type { Database } from '@/lib/supabase/database.types';
|
||||
import type { VerpleegkundigCategory } from '@/lib/types/report';
|
||||
|
||||
// Import shared types from lib/cortex (DRY - single source of truth)
|
||||
import {
|
||||
getCurrentShift,
|
||||
type CortexIntent,
|
||||
type ShiftType,
|
||||
type ExtractedEntities,
|
||||
type CortexContext,
|
||||
type IntentChain,
|
||||
type IntentAction,
|
||||
type NudgeSuggestion,
|
||||
type ClarificationRequest,
|
||||
} from '@/lib/cortex/types';
|
||||
|
||||
// Re-export for backward compatibility
|
||||
export type { CortexIntent, ShiftType };
|
||||
|
||||
// Database types
|
||||
export type Patient = Database['public']['Tables']['patients']['Row'];
|
||||
|
||||
// Cortex-specific types
|
||||
export type ShiftType = 'nacht' | 'ochtend' | 'middag' | 'avond';
|
||||
|
||||
export type CortexIntent =
|
||||
| 'dagnotitie'
|
||||
| 'zoeken'
|
||||
| 'overdracht'
|
||||
| 'agenda_query'
|
||||
| 'create_appointment'
|
||||
| 'cancel_appointment'
|
||||
| 'reschedule_appointment'
|
||||
| 'unknown';
|
||||
|
||||
// Store-specific types (not in lib/cortex/types.ts)
|
||||
export type BlockType = Exclude<CortexIntent, 'unknown'> | 'fallback' | 'patient-dashboard';
|
||||
|
||||
// Chat entities - simplified version for AI responses (strings, not Dates)
|
||||
// This differs from ExtractedEntities in lib/cortex/types.ts which uses Date objects
|
||||
// All properties are optional to match Zod schema flexibility
|
||||
export interface ChatEntities {
|
||||
patientName?: string;
|
||||
patientId?: string;
|
||||
category?: 'medicatie' | 'adl' | 'gedrag' | 'incident' | 'observatie';
|
||||
content?: string;
|
||||
query?: string;
|
||||
date?: string;
|
||||
time?: string;
|
||||
dateRange?: {
|
||||
start: string;
|
||||
end: string;
|
||||
label: string;
|
||||
};
|
||||
datetime?: {
|
||||
date?: string;
|
||||
time?: string;
|
||||
};
|
||||
appointmentType?: string;
|
||||
location?: string;
|
||||
identifier?: string;
|
||||
newDatetime?: {
|
||||
date?: string;
|
||||
time?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Chat types (v3.0)
|
||||
export type ChatMessageType = 'user' | 'assistant' | 'system' | 'error';
|
||||
|
||||
@@ -29,12 +61,12 @@ export interface ChatMessage {
|
||||
type: ChatMessageType;
|
||||
content: string;
|
||||
timestamp: Date;
|
||||
action?: ChatAction; // Optional action attached to assistant messages
|
||||
action?: ChatAction;
|
||||
}
|
||||
|
||||
export interface ChatAction {
|
||||
intent: CortexIntent;
|
||||
entities: ExtractedEntities;
|
||||
entities: ChatEntities;
|
||||
confidence: number;
|
||||
artifact?: {
|
||||
type: BlockType;
|
||||
@@ -42,20 +74,8 @@ export interface ChatAction {
|
||||
};
|
||||
}
|
||||
|
||||
// Extracted entities from user input
|
||||
export interface ExtractedEntities {
|
||||
patientName?: string;
|
||||
patientId?: string;
|
||||
category?: VerpleegkundigCategory;
|
||||
content?: string;
|
||||
query?: string;
|
||||
date?: string;
|
||||
time?: string;
|
||||
identifier?: string;
|
||||
}
|
||||
|
||||
// Block prefill data
|
||||
export interface BlockPrefillData extends ExtractedEntities {
|
||||
// Block prefill data - uses ChatEntities (string-based) for UI prefilling
|
||||
export interface BlockPrefillData extends ChatEntities {
|
||||
// Additional prefill data specific to blocks
|
||||
}
|
||||
|
||||
@@ -104,6 +124,19 @@ interface CortexStore {
|
||||
openArtifacts: Artifact[];
|
||||
activeArtifactId: string | null;
|
||||
|
||||
// V2 Context state
|
||||
context: CortexContext | null;
|
||||
|
||||
// V2 Intent Chain state
|
||||
activeChain: IntentChain | null;
|
||||
chainHistory: IntentChain[];
|
||||
|
||||
// V2 Nudge Suggestions state
|
||||
suggestions: NudgeSuggestion[];
|
||||
|
||||
// V2 Clarification state
|
||||
pendingClarification: ClarificationRequest | null;
|
||||
|
||||
// Context actions
|
||||
setActivePatient: (patient: Patient | null) => void;
|
||||
setShift: (shift: ShiftType) => void;
|
||||
@@ -134,19 +167,32 @@ interface CortexStore {
|
||||
setStreaming: (streaming: boolean) => void;
|
||||
setPendingAction: (action: ChatAction | null) => void;
|
||||
|
||||
// V2 Context actions
|
||||
setContext: (context: CortexContext) => void;
|
||||
updateContext: (partial: Partial<CortexContext>) => void;
|
||||
|
||||
// V2 Chain actions
|
||||
startChain: (chain: IntentChain) => void;
|
||||
updateActionStatus: (
|
||||
actionId: string,
|
||||
status: IntentAction['status'],
|
||||
error?: IntentAction['error']
|
||||
) => void;
|
||||
completeChain: () => void;
|
||||
|
||||
// V2 Nudge actions
|
||||
addSuggestion: (suggestion: NudgeSuggestion) => void;
|
||||
acceptSuggestion: (suggestionId: string) => void;
|
||||
dismissSuggestion: (suggestionId: string) => void;
|
||||
|
||||
// V2 Clarification actions
|
||||
setPendingClarification: (clarification: ClarificationRequest | null) => void;
|
||||
resolveClarification: (selectedOption: string) => void;
|
||||
|
||||
// Reset
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
// Helper to calculate current shift based on time
|
||||
function getCurrentShift(): ShiftType {
|
||||
const hour = new Date().getHours();
|
||||
if (hour >= 0 && hour < 7) return 'nacht';
|
||||
if (hour >= 7 && hour < 12) return 'ochtend';
|
||||
if (hour >= 12 && hour < 17) return 'middag';
|
||||
return 'avond';
|
||||
}
|
||||
|
||||
// Initial state
|
||||
const initialState = {
|
||||
activePatient: null,
|
||||
@@ -156,14 +202,20 @@ const initialState = {
|
||||
isBlockLoading: false,
|
||||
inputValue: '',
|
||||
isVoiceActive: false,
|
||||
recentActions: [],
|
||||
recentActions: [] as RecentAction[],
|
||||
// Chat state (v3.0)
|
||||
chatMessages: [],
|
||||
chatMessages: [] as ChatMessage[],
|
||||
isStreaming: false,
|
||||
pendingAction: null,
|
||||
pendingAction: null as ChatAction | null,
|
||||
// Artifact state (E4)
|
||||
openArtifacts: [],
|
||||
activeArtifactId: null,
|
||||
openArtifacts: [] as Artifact[],
|
||||
activeArtifactId: null as string | null,
|
||||
// V2 state
|
||||
context: null as CortexContext | null,
|
||||
activeChain: null as IntentChain | null,
|
||||
chainHistory: [] as IntentChain[],
|
||||
suggestions: [] as NudgeSuggestion[],
|
||||
pendingClarification: null as ClarificationRequest | null,
|
||||
};
|
||||
|
||||
// Create the store
|
||||
@@ -368,6 +420,122 @@ export const useCortexStore = create<CortexStore>()(
|
||||
);
|
||||
},
|
||||
|
||||
// V2 Context actions
|
||||
setContext: (context) => set({ context }, false, 'setContext'),
|
||||
|
||||
updateContext: (partial) =>
|
||||
set(
|
||||
(state) => ({
|
||||
context: state.context ? { ...state.context, ...partial } : null,
|
||||
}),
|
||||
false,
|
||||
'updateContext'
|
||||
),
|
||||
|
||||
// V2 Chain actions
|
||||
startChain: (chain) =>
|
||||
set(
|
||||
{
|
||||
activeChain: chain,
|
||||
},
|
||||
false,
|
||||
'startChain'
|
||||
),
|
||||
|
||||
updateActionStatus: (actionId, status, error) =>
|
||||
set(
|
||||
(state) => {
|
||||
if (!state.activeChain) return state;
|
||||
|
||||
const actions = state.activeChain.actions.map((action) =>
|
||||
action.id === actionId
|
||||
? {
|
||||
...action,
|
||||
status,
|
||||
error,
|
||||
...(status === 'executing' ? { startedAt: new Date() } : {}),
|
||||
...(status === 'success' || status === 'failed'
|
||||
? { completedAt: new Date() }
|
||||
: {}),
|
||||
}
|
||||
: action
|
||||
);
|
||||
|
||||
return {
|
||||
activeChain: {
|
||||
...state.activeChain,
|
||||
actions,
|
||||
},
|
||||
};
|
||||
},
|
||||
false,
|
||||
'updateActionStatus'
|
||||
),
|
||||
|
||||
completeChain: () =>
|
||||
set(
|
||||
(state) => {
|
||||
if (!state.activeChain) return state;
|
||||
|
||||
// Move to history
|
||||
const completedChain: IntentChain = {
|
||||
...state.activeChain,
|
||||
status: 'completed',
|
||||
};
|
||||
|
||||
return {
|
||||
activeChain: null,
|
||||
chainHistory: [completedChain, ...state.chainHistory].slice(0, 10), // Keep last 10
|
||||
};
|
||||
},
|
||||
false,
|
||||
'completeChain'
|
||||
),
|
||||
|
||||
// V2 Nudge actions
|
||||
addSuggestion: (suggestion) =>
|
||||
set(
|
||||
(state) => ({
|
||||
suggestions: [...state.suggestions, suggestion],
|
||||
}),
|
||||
false,
|
||||
'addSuggestion'
|
||||
),
|
||||
|
||||
acceptSuggestion: (suggestionId) =>
|
||||
set(
|
||||
(state) => ({
|
||||
suggestions: state.suggestions.map((s) =>
|
||||
s.id === suggestionId ? { ...s, status: 'accepted' as const } : s
|
||||
),
|
||||
}),
|
||||
false,
|
||||
'acceptSuggestion'
|
||||
),
|
||||
|
||||
dismissSuggestion: (suggestionId) =>
|
||||
set(
|
||||
(state) => ({
|
||||
suggestions: state.suggestions.filter((s) => s.id !== suggestionId),
|
||||
}),
|
||||
false,
|
||||
'dismissSuggestion'
|
||||
),
|
||||
|
||||
// V2 Clarification actions
|
||||
setPendingClarification: (clarification) =>
|
||||
set({ pendingClarification: clarification }, false, 'setPendingClarification'),
|
||||
|
||||
resolveClarification: (selectedOption) =>
|
||||
set(
|
||||
(state) => {
|
||||
console.log('[Store] Clarification resolved:', selectedOption);
|
||||
return { pendingClarification: null };
|
||||
},
|
||||
false,
|
||||
'resolveClarification'
|
||||
),
|
||||
|
||||
// Reset
|
||||
reset: () => set(initialState, false, 'reset'),
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user