Model resolution (from oh-my-openagent/model-core): 6-step priority resolution pipeline (UI select -> user config -> category default -> user fallback -> policy chain -> system default), provider fallback chains, fuzzy model matching, error classification, provider-specific model ID transforms. 14 files, zero runtime deps. Multi-batch matcher (from boocontext-audit): 6 batch types (Observational, Actionable, PreviouslyApplied, Disambiguation, ResponseAnalysis, LowCriticality) for behavioral guideline evaluation. RelationalResolver with iterative convergence (DEPENDS_ON, PRIORITIZES, ENTAILS, TAG_ALL, TAG_PRIORITIZES). SchematicGenerator abstract class with retry and execution plans. 4 files.
436 lines
11 KiB
TypeScript
436 lines
11 KiB
TypeScript
/**
|
|
* 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<string, unknown>;
|
|
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<string, unknown>;
|
|
}
|
|
|
|
export interface GuidelineMatchingContext {
|
|
agent: string;
|
|
session: string;
|
|
customer: string;
|
|
contextVariables: Record<string, string>[];
|
|
interactionHistory: unknown[];
|
|
terms: string[];
|
|
capabilities?: string[];
|
|
stagedEvents?: unknown[];
|
|
activeJourneys?: unknown[];
|
|
journeyPaths?: Record<string, unknown>;
|
|
}
|
|
|
|
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<GuidelineMatchingBatchResult>;
|
|
}
|
|
|
|
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<GuidelineMatchingBatchResult> {
|
|
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<GuidelineMatchingBatchResult> {
|
|
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<GuidelineMatchingBatchResult> {
|
|
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<GuidelineMatchingBatchResult> {
|
|
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<string, unknown>,
|
|
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<GuidelineMatchingBatchResult> {
|
|
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<string>();
|
|
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<T>(
|
|
fn: () => Promise<T>,
|
|
maxAttempts = 3,
|
|
_baseTemperature = 0.7,
|
|
): Promise<T> {
|
|
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<GuidelineMatchingResult> {
|
|
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 };
|
|
}
|