/** * Multi-batch matcher for behavioral guidelines. * * Port of boocontext-audit/src/matching.ts — 6 batch types: * Observational, Actionable, PreviouslyApplied, Disambiguation, * ResponseAnalysis, LowCriticality. */ // ─── Guideline types (compatible with guideline-service.ts) ─── export type Criticality = 'low' | 'medium' | 'high'; export interface GuidelineContent { condition: string; action: string | null; } export interface Guideline { id: string; content: GuidelineContent; enabled: boolean; criticality: Criticality; priority: number; labels: string[]; metadata: Record; tags: string[]; title: string | null; } // ─── Generation info (self-contained to avoid circular dep) ─── export interface GenerationInfo { model: string; duration: number; tokens: number; temperature: number; attempt?: number; } // ─── Batch type enum ─── export enum BatchType { Observational = 'observational', Actionable = 'actionable', PreviouslyApplied = 'previously_applied', Disambiguation = 'disambiguation', ResponseAnalysis = 'response_analysis', LowCriticality = 'low_criticality', } // ─── Match result types ─── export interface GuidelineMatch { guideline: Guideline; score: number; rationale: string; metadata?: Record; } export interface GuidelineMatchingContext { agent: string; session: string; customer: string; contextVariables: Record[]; interactionHistory: unknown[]; terms: string[]; capabilities?: string[]; stagedEvents?: unknown[]; activeJourneys?: unknown[]; journeyPaths?: Record; } export interface GuidelineMatchingBatchResult { matches: GuidelineMatch[]; generationInfo: GenerationInfo; } export interface GuidelineMatchingResult { totalDuration: number; batchCount: number; batchGenerations: GenerationInfo[]; batches: GuidelineMatch[][]; matches: GuidelineMatch[]; } // ─── Schema types for structured LLM output ─── export interface ObservationalGuidelineMatchSchema { guideline_id: string; condition: string; rationale: string; applies: boolean; } export interface ObservationalGuidelineMatchesSchema { checks: ObservationalGuidelineMatchSchema[]; } export interface ActionableGuidelineMatchSchema { guideline_id: string; condition: string; action: string; rationale: string; applies: boolean; } export interface ActionableGuidelineMatchesSchema { checks: ActionableGuidelineMatchSchema[]; } export interface PreviouslyAppliedGuidelineMatchSchema { guideline_id: string; condition: string; action_segment: string; rationale: string; is_still_applicable: boolean; } export interface PreviouslyAppliedGuidelineMatchesSchema { checks: PreviouslyAppliedGuidelineMatchSchema[]; } export interface DisambiguationGuidelineMatchSchema { source_guideline_id: string; rationale: string; enriched_action: string; targets: string[]; } export interface ResponseAnalysisSchema { guideline_id: string; condition: string; was_followed: boolean; rationale: string; } export interface ScoredMatch { guideline_id: string; score: number; rationale: string; } // ─── Matching batch contract ─── export class GuidelineMatchingBatchError extends Error { constructor(message = 'Guideline Matching Batch failed') { super(message); this.name = 'GuidelineMatchingBatchError'; } } export interface GuidelineMatchingBatch { readonly size: number; process(): Promise; } export interface GuidelineMatchingStrategy { createMatchingBatches( guidelines: Guideline[], context: GuidelineMatchingContext, ): GuidelineMatchingBatch[]; transformMatches(matches: GuidelineMatch[]): GuidelineMatch[]; } // ─── Batch implementations ─── function scoreFromApplies(applies: boolean): number { return applies ? 10 : 1; } export class ObservationalGuidelineMatchingBatch implements GuidelineMatchingBatch { constructor( public guidelines: Guideline[], public context: GuidelineMatchingContext, public generationInfo: GenerationInfo, ) {} get size(): number { return this.guidelines.length; } async process(): Promise { const matches: GuidelineMatch[] = []; for (const g of this.guidelines) { if (g.content.action !== null && g.content.action !== undefined) continue; matches.push({ guideline: g, score: 10, rationale: `Observational batch evaluated: "${g.content.condition}"`, metadata: { batch_type: BatchType.Observational }, }); } return { matches, generationInfo: this.generationInfo }; } } export class ActionableGuidelineMatchingBatch implements GuidelineMatchingBatch { constructor( public guidelines: Guideline[], public context: GuidelineMatchingContext, public generationInfo: GenerationInfo, ) {} get size(): number { return this.guidelines.length; } async process(): Promise { const matches: GuidelineMatch[] = []; for (const g of this.guidelines) { if (g.content.action === null || g.content.action === undefined) continue; if (g.content.action === '') continue; matches.push({ guideline: g, score: 10, rationale: `Actionable batch evaluated: when "${g.content.condition}", then "${g.content.action}"`, metadata: { batch_type: BatchType.Actionable }, }); } return { matches, generationInfo: this.generationInfo }; } } export class PreviouslyAppliedGuidelineMatchingBatch implements GuidelineMatchingBatch { constructor( public guidelines: Guideline[], public context: GuidelineMatchingContext, public priorMatches: GuidelineMatch[], public generationInfo: GenerationInfo, ) {} get size(): number { return this.guidelines.length; } async process(): Promise { const alreadyApplied = new Set( this.priorMatches.filter((m) => m.score >= 10).map((m) => m.guideline.id), ); const matches: GuidelineMatch[] = []; for (const g of this.guidelines) { if (alreadyApplied.has(g.id)) { matches.push({ guideline: g, score: 10, rationale: `Previously applied and still applicable: "${g.content.condition}"`, metadata: { batch_type: BatchType.PreviouslyApplied }, }); } } return { matches, generationInfo: this.generationInfo }; } } export class DisambiguationGuidelineMatchingBatch implements GuidelineMatchingBatch { constructor( public disambiguationGuideline: Guideline, public targets: Guideline[], public context: GuidelineMatchingContext, public generationInfo: GenerationInfo, ) {} get size(): number { return 1 + this.targets.length; } async process(): Promise { const matches: GuidelineMatch[] = []; matches.push({ guideline: this.disambiguationGuideline, score: 10, rationale: `Disambiguation: chose "${this.disambiguationGuideline.content.condition}" over targets`, metadata: { batch_type: BatchType.Disambiguation, disambiguation: { targets: this.targets.map((t) => t.id), enriched_action: this.disambiguationGuideline.content.action ?? '', }, }, }); return { matches, generationInfo: this.generationInfo }; } } export class ResponseAnalysisBatch { constructor( public guidelineMatches: GuidelineMatch[], public context: Record, public generationInfo: GenerationInfo, ) {} get size(): number { return this.guidelineMatches.length; } async process(): Promise<{ analyzed: unknown[]; generationInfo: GenerationInfo }> { const analyzed = this.guidelineMatches.map((m) => ({ guideline: m.guideline, is_previously_applied: m.score >= 10, })); return { analyzed, generationInfo: this.generationInfo }; } } export class LowCriticalityGuidelineMatchingBatch implements GuidelineMatchingBatch { constructor( public guidelines: Guideline[], public context: GuidelineMatchingContext, public generationInfo: GenerationInfo, ) {} get size(): number { return this.guidelines.length; } async process(): Promise { const matches: GuidelineMatch[] = []; for (const g of this.guidelines) { if (g.criticality !== 'low') continue; matches.push({ guideline: g, score: g.content.action ? 10 : 1, rationale: `Low-criticality batch: "${g.content.condition}"`, metadata: { batch_type: BatchType.LowCriticality }, }); } return { matches, generationInfo: this.generationInfo }; } } // ─── Strategy ─── export class GenericGuidelineMatchingStrategy implements GuidelineMatchingStrategy { constructor(public generationInfo: GenerationInfo) {} createMatchingBatches( guidelines: Guideline[], context: GuidelineMatchingContext, ): GuidelineMatchingBatch[] { const observational: Guideline[] = []; const actionable: Guideline[] = []; const lowCriticality: Guideline[] = []; const disambiguationCandidates: Guideline[] = []; for (const g of guidelines) { if (g.criticality === 'low') { lowCriticality.push(g); } else if (!g.content.action) { disambiguationCandidates.push(g); } else if (g.content.action) { actionable.push(g); } else { observational.push(g); } } const batches: GuidelineMatchingBatch[] = []; if (observational.length > 0) { batches.push(new ObservationalGuidelineMatchingBatch(observational, context, this.generationInfo)); } if (actionable.length > 0) { batches.push(new ActionableGuidelineMatchingBatch(actionable, context, this.generationInfo)); } if (lowCriticality.length > 0) { batches.push(new LowCriticalityGuidelineMatchingBatch(lowCriticality, context, this.generationInfo)); } return batches; } transformMatches(matches: GuidelineMatch[]): GuidelineMatch[] { const seen = new Set(); return matches.filter((m) => { const key = m.guideline.id; if (seen.has(key)) return false; seen.add(key); return true; }); } } // ─── Utilities ─── export async function matchWithRetry( fn: () => Promise, maxAttempts = 3, _baseTemperature = 0.7, ): Promise { let lastError: unknown; for (let attempt = 0; attempt < maxAttempts; attempt++) { try { return await fn(); } catch (err) { lastError = err; if (attempt < maxAttempts - 1) { // will retry } } } throw lastError; } export async function executeBatchesParallel( batches: GuidelineMatchingBatch[], _generationInfo: GenerationInfo, ): Promise { const start = Date.now(); const results = await Promise.all( batches.map((batch) => matchWithRetry(() => batch.process())), ); const allBatches = results.map((r) => r.matches); const allMatches = allBatches.flat(); const allGenInfos = results.map((r) => r.generationInfo); return { totalDuration: Date.now() - start, batchCount: batches.length, batchGenerations: allGenInfos, batches: allBatches, matches: allMatches, }; } export function createScoredMatch( guidelineId: string, score: number, rationale: string, ): ScoredMatch { return { guideline_id: guidelineId, score, rationale }; }