Files
memind/chat-intent-observation.mjs
T
John a30b62b987
Memind CI / Test, build, and release guards (push) Has been cancelled
feat(chat-intent): observe routing, keep discuss-first on chat, and escalate short follow-ups
Close the chat-session dead zone without flipping production flags: record 0.72 fallbacks, stop C-2 from treating「先聊聊」as execute, and promote「不对你去执行」after a completed direct turn on both h5direct and date sessions.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-19 13:13:17 +08:00

108 lines
3.5 KiB
JavaScript

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,
};
}