export const RULE_FALLBACK_CONFIDENCE = 0.72; export const RULE_FALLBACK_REASON = '普通对话默认走轻量聊天'; function asFiniteNumber(value) { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : null; } function bucketTextLength(length) { if (!length) return 'empty'; if (length <= 6) return 'short_le6'; if (length <= 12) return 'medium_le12'; return 'long_gt12'; } export function isRuleFallbackClassification(classification = {}) { if (classification?.ruleFallback === true) return true; const source = String(classification?.source ?? '').trim(); const reason = String(classification?.reason ?? '').trim(); const confidence = asFiniteNumber(classification?.confidence); return source === 'rule' && confidence != null && Math.abs(confidence - RULE_FALLBACK_CONFIDENCE) < 1e-9 && reason.includes(RULE_FALLBACK_REASON); } export function summarizeChatIntentObservation(classification = {}, context = {}) { const text = String(context.text ?? '').trim(); const textLength = text.length; const confidence = asFiniteNumber(classification.confidence); return { route: classification.route ?? null, source: String(classification.source ?? '').trim() || null, confidence, ruleFallback: isRuleFallbackClassification(classification), memoryRecall: context.memoryRecall === true, wouldChangeRoute: Boolean(classification.llmShadow?.wouldChangeRoute), llmShadowRoute: classification.llmShadow?.route ?? null, textLength, textLengthBucket: bucketTextLength(textLength), }; } export function observationFromIntentRoutedData(data = {}, fallbackText = '') { if (data?.observation && typeof data.observation === 'object') { return data.observation; } return summarizeChatIntentObservation(data, { text: fallbackText }); } function increment(map, key) { const normalized = key == null ? '(null)' : String(key); map[normalized] = (map[normalized] || 0) + 1; } export function aggregateChatIntentObservations({ routed = [], escalatedCount = 0, contextInjectedCount = 0, } = {}) { const total = routed.length; const sourceCounts = {}; const routeCounts = {}; const bucketCounts = {}; let ruleFallbackCount = 0; let memoryRecallCount = 0; let memoryRecallNotDirect = 0; let shadowObserved = 0; let wouldChangeRoute = 0; for (const item of routed) { increment(sourceCounts, item.source); increment(routeCounts, item.route); increment(bucketCounts, item.textLengthBucket); if (item.ruleFallback) ruleFallbackCount += 1; if (item.memoryRecall) { memoryRecallCount += 1; if (item.route && item.route !== 'direct_chat') memoryRecallNotDirect += 1; } if (item.llmShadowRoute != null || item.wouldChangeRoute) { shadowObserved += 1; } if (item.wouldChangeRoute) wouldChangeRoute += 1; } const ratio = (count) => (total > 0 ? Number((count / total).toFixed(4)) : 0); return { total, ruleFallbackCount, ruleFallbackRatio: ratio(ruleFallbackCount), escalatedCount, escalationRatio: ratio(escalatedCount), contextInjectedCount, sourceCounts, routeCounts, shortMessageCounts: bucketCounts, memoryRecallCount, memoryRecallDirectRatio: memoryRecallCount > 0 ? Number(((memoryRecallCount - memoryRecallNotDirect) / memoryRecallCount).toFixed(4)) : 1, shadowObserved, wouldChangeRoute, wouldChangeRouteRatio: shadowObserved > 0 ? Number((wouldChangeRoute / shadowObserved).toFixed(4)) : 0, }; }