Files
memind/chat-intent-router.mjs
john cb7a446d3e fix: ensure userVisible metadata on Goosed session replies
Goosed rejects /sessions/:id/reply when user_message.metadata lacks
userVisible. Normalize metadata before submit and during agent orchestration
so page-generation runs no longer fail before streaming starts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-05 11:58:17 +08:00

777 lines
25 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
buildChatSkillPrompt,
extractSelectedChatSkillName,
hasExplicitChatSkillPrompt,
} from './chat-skills.mjs';
import { isDirectChatSessionId } from './direct-chat-service.mjs';
import {
memoryLimitForIntervention,
resolveMemoryInterventionMode,
} from './memory-intervention.mjs';
export const CHAT_INTENT_ROUTE = {
DIRECT_CHAT: 'direct_chat',
AGENT: 'agent_orchestration',
};
const AGENT_ORCHESTRATION_HEADER = '【Memind 任务编排】';
const SKILL_PROMPT_KEYS = {
web: 'web',
search: 'search',
'static-page-publish': 'generate-page',
'form-builder': 'form-builder',
'table-viewer': 'table-viewer',
'product-campaign-page': 'product-campaign-page',
};
const OBVIOUS_AGENT_PATTERNS = [
/\bpublic\/[^\s"'<>]+\.html\b/i,
/MindSpace\/[^/\s]+\/public\/[^\s"'<>]+\.html/i,
/(?:生成|创建|制作|做|写|设计|发布).{0,24}(?:H5|h5|HTML|html|网页|页面|活动页|宣传页|落地页|分享页)/u,
/(?:H5|h5|HTML|html|网页|页面).{0,24}(?:生成|创建|制作|做|写|设计|发布)/u,
/(?:写|做|生成|制作|设计).{0,16}主题(?:页面|文章)/u,
/主题(?:页面|文章).{0,16}(?:写|做|生成|制作|设计)/u,
];
const OBVIOUS_DIRECT_PATTERNS = [
/^(?:你好|您好|在吗|在不在|嗨|hi|hello|hey)[!!。.\s]*$/iu,
/^(?:测试\s*\d*|test\s*\d*)[!!。.\s]*$/iu,
/^[?]+$/u,
];
const MEMORY_RECALL_PATTERNS = [
/你(?:还)?记得(?:我)?/u,
/(?:有没有|是否).{0,6}记住/u,
/你对(?:我)?(?:的)?记忆/u,
/(?:我|之前).{0,16}(?:说过|提到|聊过|告诉)/u,
/(?:我的|之前的)(?:记忆|偏好|计划|目标|想法)/u,
/之前(?:说|提|聊)(?:过|的)/u,
];
export function isMemoryRecallQuestion(text) {
const normalized = String(text ?? '').trim();
if (!normalized) return false;
return MEMORY_RECALL_PATTERNS.some((pattern) => pattern.test(normalized));
}
const DEFAULT_ROUTER_TIMEOUT_MS = 1500;
const DEFAULT_ROUTER_MEMORY_LIMIT = 8;
const DEFAULT_ROUTER_MIN_CONFIDENCE = 0.55;
function envFlag(value, fallback = false) {
const raw = String(value ?? '').trim().toLowerCase();
if (!raw) return fallback;
return ['1', 'true', 'yes', 'on'].includes(raw);
}
function boundedNumber(value, fallback, { min = 0, max = Number.POSITIVE_INFINITY } = {}) {
if (value == null || value === '') return fallback;
const parsed = Number(value);
if (!Number.isFinite(parsed)) return fallback;
return Math.min(max, Math.max(min, parsed));
}
function pickDefined(source, keys) {
return Object.fromEntries(
keys
.filter((key) => source[key] !== undefined)
.map((key) => [key, source[key]]),
);
}
function truncateText(value, maxLength) {
const text = String(value ?? '').replace(/\s+/g, ' ').trim();
if (!text) return '';
return text.length > maxLength ? `${text.slice(0, maxLength)}...` : text;
}
function firstSentence(value, maxLength) {
const text = truncateText(value, maxLength);
if (!text) return '';
const match = text.match(/^(.{12,}?[。.!?])/u);
return truncateText(match?.[1] ?? text, maxLength);
}
function normalizeMemoryText(item) {
if (typeof item === 'string') return { label: null, text: item };
const text = item?.text ?? item?.memory_text ?? item?.memoryText ?? item?.content ?? item?.summary ?? '';
const label = String(item?.label ?? item?.type ?? '').trim() || null;
return { label, text };
}
function withTimeout(promise, timeoutMs, label) {
const timeout = Number(timeoutMs);
if (!Number.isFinite(timeout) || timeout <= 0) return promise;
let timer = null;
const timeoutPromise = new Promise((_, reject) => {
timer = setTimeout(() => {
const err = new Error(`${label} timed out after ${timeout}ms`);
err.code = 'CHAT_INTENT_ROUTER_TIMEOUT';
reject(err);
}, timeout);
});
return Promise.race([promise, timeoutPromise]).finally(() => {
if (timer) clearTimeout(timer);
});
}
function messageDisplayText(message) {
const displayText = message?.metadata?.displayText;
if (typeof displayText === 'string' && displayText.trim()) return displayText.trim();
return extractMessageText(message);
}
function extractMessageText(message) {
const content = message?.content;
if (typeof content === 'string') return content.trim();
if (!Array.isArray(content)) return String(message?.text ?? message?.value ?? '').trim();
return content
.map((item) => {
if (typeof item === 'string') return item;
if (item?.type === 'text') return item.text ?? '';
return '';
})
.join('\n')
.trim();
}
function isTextOnlyUserMessage(message) {
const content = message?.content;
if (!Array.isArray(content)) return Boolean(extractMessageText(message));
if (content.length === 0) return false;
return content.every((item) => {
if (typeof item === 'string') return true;
return item?.type === 'text';
});
}
function readSelectedChatSkillId(message) {
const selected =
message?.metadata?.memindRun?.selectedChatSkill ??
message?.metadata?.selectedChatSkill;
return typeof selected === 'string' && selected.trim() ? selected.trim() : null;
}
function messageHasExplicitChatSkillSelection(message) {
if (!message) return false;
if (readSelectedChatSkillId(message)) return true;
return [extractMessageText(message), messageDisplayText(message)].some((text) =>
hasExplicitChatSkillPrompt(text),
);
}
function buildSkillSelectionClassification(userMessage) {
const selectedSkillId = readSelectedChatSkillId(userMessage);
const suggestedSkill =
selectedSkillId ??
extractSelectedChatSkillName(extractMessageText(userMessage)) ??
extractSelectedChatSkillName(messageDisplayText(userMessage));
return normalizeClassification({
route: CHAT_INTENT_ROUTE.AGENT,
confidence: 1,
reason: '用户已选择 skill',
suggested_skill: suggestedSkill,
agent_brief: suggestedSkill ? `使用 ${suggestedSkill} 执行用户任务` : '执行用户选择的 skill 任务',
}, { source: 'rule' });
}
function buildRouterSystemPrompt(grantedSkills = []) {
const skills = Array.isArray(grantedSkills) ? grantedSkills.filter(Boolean) : [];
return [
'你是 TKMind H5 聊天意图路由器,只负责判断用户消息应该走哪条处理通道。',
'',
'系统有两条通道:',
'1. direct_chat — 纯文字交流:问答、解释、闲聊、总结已有文字、给建议,不需要写文件、生成页面、调工具。',
'2. agent_orchestration — 任务编排 Agent:需要实际执行并产出结果的任务,例如生成/修改 HTML 页面、发布到 MindSpace、搜索实时资料、写代码改仓库、表单收集、数据表格、商品页、docx/长图导出等。',
'',
'判断原则:',
'- 用户只要文字回答,不要求“做出来/发布/生成链接/改文件” → direct_chat',
'- 用户要产出可访问页面、文件、链接,或需要工具/skills → agent_orchestration',
'- 不确定时优先 agent_orchestration,避免漏执行',
'- 记忆线索只用于辅助判断本轮意图,不能替用户扩写新需求',
'',
skills.length ? `当前用户已授权 skills${skills.join(', ')}` : '当前用户未授权额外 skills。',
skills.length
? '若走 agent_orchestration,可在 suggested_skill 中填写最匹配的 skill 名称(须来自上述列表),否则填 null。'
: 'suggested_skill 通常填 null。',
'',
'只输出 JSON,不要 markdown,不要解释:',
'{"route":"direct_chat|agent_orchestration","confidence":0.0,"reason":"一句话","suggested_skill":null,"agent_brief":"给 Agent 的执行要点,direct_chat 时可为空"}',
].join('\n');
}
export function buildRouterContext(resolveResult, {
memoryLimit = 5,
semanticLimit = 3,
goalLimit = 2,
memoryTextLimit = 80,
semanticTextLimit = 60,
goalTextLimit = 60,
behaviorTextLimit = 100,
} = {}) {
const result = resolveResult && typeof resolveResult === 'object' ? resolveResult : {};
const lines = [];
let itemsUsed = 0;
const memories = Array.isArray(result.memories) ? result.memories : [];
const memoryLines = memories
.map((item) => normalizeMemoryText(item))
.map(({ label, text }) => {
const clipped = truncateText(text, memoryTextLimit);
if (!clipped) return '';
return `- ${label ? `[${label}] ` : ''}${clipped}`;
})
.filter(Boolean)
.slice(0, memoryLimit);
if (memoryLines.length) {
lines.push('相关记忆:', ...memoryLines);
itemsUsed += memoryLines.length;
}
const semanticLines = (Array.isArray(result.semanticMemories) ? result.semanticMemories : [])
.map((item) => (typeof item === 'string' ? item : normalizeMemoryText(item).text))
.map((text) => truncateText(text, semanticTextLimit))
.filter(Boolean)
.slice(0, semanticLimit)
.map((text) => `- ${text}`);
if (semanticLines.length) {
lines.push('语义线索:', ...semanticLines);
itemsUsed += semanticLines.length;
}
const behaviorSummary = firstSentence(result.behaviorSummary, behaviorTextLimit);
if (behaviorSummary) {
lines.push(`行为摘要:${behaviorSummary}`);
itemsUsed += 1;
}
const goals = Array.isArray(result.activeGoals) && result.activeGoals.length
? result.activeGoals
: (Array.isArray(result.contextGoals) ? result.contextGoals : []);
const goalLines = goals
.map((text) => truncateText(text, goalTextLimit))
.filter(Boolean)
.slice(0, goalLimit)
.map((text) => `- ${text}`);
if (goalLines.length) {
lines.push('进行中目标:', ...goalLines);
itemsUsed += goalLines.length;
}
return {
content: lines.join('\n').trim(),
itemsUsed,
source: result.source ?? null,
degraded: Boolean(result.degraded),
skipped: Boolean(result.skipped),
reason: result.reason ?? null,
};
}
function buildRouterUserPrompt({ text, routerContext }) {
const context = String(routerContext ?? '').trim() || '无';
return [
'[Router Context]',
context,
'',
'[User]',
text || '(empty)',
].join('\n');
}
function parseRouterJson(reply) {
const text = String(reply ?? '').trim();
if (!text) return null;
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
const candidate = (fenced?.[1] ?? text).trim();
try {
return JSON.parse(candidate);
} catch {
const start = candidate.indexOf('{');
const end = candidate.lastIndexOf('}');
if (start < 0 || end <= start) return null;
try {
return JSON.parse(candidate.slice(start, end + 1));
} catch {
return null;
}
}
}
function normalizeRoute(value) {
const normalized = String(value ?? '').trim().toLowerCase();
if (normalized === CHAT_INTENT_ROUTE.DIRECT_CHAT) return CHAT_INTENT_ROUTE.DIRECT_CHAT;
if (normalized === CHAT_INTENT_ROUTE.AGENT || normalized === 'agent') return CHAT_INTENT_ROUTE.AGENT;
return null;
}
export function resolveChatIntentRouterPolicy({ env = process.env, overrides = {} } = {}) {
const fallbackRoute = normalizeRoute(env?.MEMIND_CHAT_ROUTER_FALLBACK_ROUTE) ?? CHAT_INTENT_ROUTE.AGENT;
return {
enabled: envFlag(env?.MEMIND_CHAT_LLM_ROUTER_ENABLED, false),
modelProviderKeyId: String(env?.MEMIND_CHAT_ROUTER_MODEL_PROVIDER_KEY_ID ?? '').trim() || null,
model: String(env?.MEMIND_CHAT_ROUTER_MODEL ?? '').trim() || null,
modelApiType: String(env?.MEMIND_CHAT_ROUTER_MODEL_API ?? '').trim() || 'chat',
temperature: boundedNumber(env?.MEMIND_CHAT_ROUTER_TEMPERATURE, 0, { min: 0, max: 2 }),
minConfidence: boundedNumber(
env?.MEMIND_CHAT_ROUTER_MIN_CONFIDENCE,
DEFAULT_ROUTER_MIN_CONFIDENCE,
{ min: 0, max: 1 },
),
memoryResolveEnabled: envFlag(env?.MEMIND_CHAT_ROUTER_MEMORY_ENABLED, true),
memoryResolveLimit: Math.round(boundedNumber(
env?.MEMIND_CHAT_ROUTER_MEMORY_LIMIT,
DEFAULT_ROUTER_MEMORY_LIMIT,
{ min: 1, max: 50 },
)),
timeoutMs: Math.round(boundedNumber(
env?.MEMIND_CHAT_ROUTER_TIMEOUT_MS,
DEFAULT_ROUTER_TIMEOUT_MS,
{ min: 0, max: 30_000 },
)),
fallbackRoute,
...overrides,
};
}
function normalizeClassification(raw, { source, fallbackRoute = CHAT_INTENT_ROUTE.AGENT } = {}) {
const route = normalizeRoute(raw?.route) ?? fallbackRoute;
const confidenceRaw = Number(raw?.confidence);
const confidence = Number.isFinite(confidenceRaw)
? Math.min(1, Math.max(0, confidenceRaw))
: source === 'llm' ? 0.7 : 1;
const suggestedSkill = String(raw?.suggested_skill ?? raw?.suggestedSkill ?? '').trim() || null;
const agentBrief = String(raw?.agent_brief ?? raw?.agentBrief ?? '').trim();
const reason = String(raw?.reason ?? '').trim() || (source === 'llm' ? '模型路由判定' : '规则路由判定');
return {
route,
confidence,
reason,
suggestedSkill,
agentBrief,
source,
providerKeyId: raw?.providerKeyId ?? raw?.provider_key_id ?? null,
model: raw?.model ?? null,
memory: raw?.memory ?? null,
};
}
function resolveSkillPrompt(suggestedSkill, grantedSkills = []) {
const skillName = String(suggestedSkill ?? '').trim();
if (!skillName) return '';
if (grantedSkills.length > 0 && !grantedSkills.includes(skillName)) return '';
const promptKey = SKILL_PROMPT_KEYS[skillName];
if (!promptKey) return '';
return buildChatSkillPrompt(promptKey, skillName);
}
export function buildAgentOrchestrationAgentText({
displayText,
classification,
skillPrompt = '',
}) {
const taskBody = String(displayText ?? '').trim();
const lines = [
`${AGENT_ORCHESTRATION_HEADER}以下为用户任务,请使用工具与技能实际执行并产出结果,不要只做文字描述。`,
`路由判定:${classification.reason}`,
classification.agentBrief ? `执行要点:${classification.agentBrief}` : '',
classification.suggestedSkill ? `建议 skill${classification.suggestedSkill}` : '',
skillPrompt,
'',
'用户任务:',
taskBody,
].filter((line, index, all) => line !== '' || index === all.length - 2);
return lines.join('\n');
}
export function applyAgentOrchestrationToUserMessage(userMessage, classification, { grantedSkills = [] } = {}) {
const displayText = messageDisplayText(userMessage);
const skillPrompt = resolveSkillPrompt(classification?.suggestedSkill, grantedSkills);
const agentText = buildAgentOrchestrationAgentText({
displayText,
classification,
skillPrompt,
});
const content = Array.isArray(userMessage?.content)
? userMessage.content.map((item, index) => {
if (index !== 0) return item;
if (typeof item === 'string') return agentText;
if (item?.type === 'text') return { ...item, text: agentText };
return item;
})
: [{ type: 'text', text: agentText }];
const metadata = {
...(userMessage?.metadata ?? {}),
displayText: displayText || undefined,
chatIntentRoute: CHAT_INTENT_ROUTE.AGENT,
chatIntentSource: classification?.source ?? null,
userVisible: userMessage?.metadata?.userVisible ?? true,
agentVisible: userMessage?.metadata?.agentVisible ?? true,
};
return {
...userMessage,
content,
metadata,
};
}
function hasPriorAgentConversation(sessionId, sessionMessageCount) {
if (!sessionId || isDirectChatSessionId(sessionId)) return false;
if (sessionMessageCount == null) return true;
return Number(sessionMessageCount) > 0;
}
export function classifyWithRules({
text,
forceDeepReasoning = false,
toolMode = 'chat',
sessionId = null,
sessionMessageCount = null,
userMessage = null,
includeIntentPatterns = true,
llmRouterEnabled = false,
} = {}) {
const normalized = String(text ?? '').trim();
if (forceDeepReasoning || toolMode !== 'chat') {
return normalizeClassification({
route: CHAT_INTENT_ROUTE.AGENT,
confidence: 1,
reason: forceDeepReasoning ? '用户开启深度推理' : '代码任务模式',
}, { source: 'rule' });
}
if (userMessage && messageHasExplicitChatSkillSelection(userMessage)) {
return buildSkillSelectionClassification(userMessage);
}
if (userMessage && !isTextOnlyUserMessage(userMessage)) {
return normalizeClassification({
route: CHAT_INTENT_ROUTE.AGENT,
confidence: 1,
reason: '消息包含非文本内容',
}, { source: 'rule' });
}
if (
!llmRouterEnabled &&
includeIntentPatterns &&
normalized &&
isMemoryRecallQuestion(normalized)
) {
return normalizeClassification({
route: CHAT_INTENT_ROUTE.DIRECT_CHAT,
confidence: 0.98,
reason: '用户在询问个人记忆或历史对话',
}, { source: 'rule' });
}
if (!llmRouterEnabled && hasPriorAgentConversation(sessionId, sessionMessageCount)) {
return normalizeClassification({
route: CHAT_INTENT_ROUTE.AGENT,
confidence: 1,
reason: '延续已有 Agent 会话',
}, { source: 'rule' });
}
if (
includeIntentPatterns &&
normalized &&
OBVIOUS_AGENT_PATTERNS.some((pattern) => pattern.test(normalized))
) {
return normalizeClassification({
route: CHAT_INTENT_ROUTE.AGENT,
confidence: 0.95,
reason: '明确需要生成或发布页面/文件',
suggested_skill: 'static-page-publish',
agent_brief: '生成或更新 MindSpace 公开页面,并返回可访问链接。',
}, { source: 'rule' });
}
if (
includeIntentPatterns &&
normalized &&
OBVIOUS_DIRECT_PATTERNS.some((pattern) => pattern.test(normalized))
) {
return normalizeClassification({
route: CHAT_INTENT_ROUTE.DIRECT_CHAT,
confidence: 0.95,
reason: '简单寒暄或连通性测试',
}, { source: 'rule' });
}
return null;
}
export function createChatIntentRouter(options = {}) {
const {
llmProviderService,
memoryV2 = null,
env = process.env,
logger = console,
} = options;
const policy = options.policy ?? resolveChatIntentRouterPolicy({
env,
overrides: pickDefined(options, [
'enabled',
'modelProviderKeyId',
'model',
'modelApiType',
'temperature',
'minConfidence',
'memoryResolveEnabled',
'memoryResolveLimit',
'timeoutMs',
'fallbackRoute',
]),
});
function getStatus() {
return {
enabled: Boolean(policy.enabled),
modelProviderKeyId: policy.modelProviderKeyId ?? null,
model: policy.model ?? null,
modelApiType: policy.modelApiType ?? 'chat',
minConfidence: policy.minConfidence,
memoryResolveEnabled: Boolean(policy.memoryResolveEnabled),
memoryResolveLimit: policy.memoryResolveLimit,
timeoutMs: policy.timeoutMs,
fallbackRoute: policy.fallbackRoute,
};
}
function isEnabled() {
return Boolean(policy.enabled && llmProviderService?.createChatCompletion);
}
async function resolveRouterContext({ userId, sessionId, text, forceDeepReasoning = false }) {
const intervention = resolveMemoryInterventionMode({
forceDeepReasoning,
recallQuestion: isMemoryRecallQuestion(text),
});
const limit = memoryLimitForIntervention(intervention, { context: 'router' });
if (
limit <= 0 ||
!policy.memoryResolveEnabled ||
!memoryV2?.resolve ||
!userId
) {
return buildRouterContext(null);
}
try {
const resolved = await withTimeout(
memoryV2.resolve({
userId,
sessionId,
query: text,
limit,
}),
policy.timeoutMs,
'Memory V2 router resolve',
);
return buildRouterContext(resolved);
} catch (err) {
logger?.warn?.(
`[chat-intent-router] memory resolve skipped: ${err instanceof Error ? err.message : err}`,
);
return buildRouterContext({
degraded: true,
reason: 'memory_resolve_failed',
});
}
}
async function classify({
userId = null,
userMessage,
sessionId = null,
sessionMessageCount = null,
toolMode = 'chat',
forceDeepReasoning = false,
grantedSkills = [],
} = {}) {
const text = messageDisplayText(userMessage);
const ruleResult = classifyWithRules({
text,
forceDeepReasoning,
toolMode,
sessionId,
sessionMessageCount,
userMessage,
includeIntentPatterns: false,
llmRouterEnabled: Boolean(policy.enabled),
});
if (ruleResult) return ruleResult;
if (!isEnabled()) {
return normalizeClassification({
route: policy.fallbackRoute,
confidence: 0.5,
reason: '意图路由未启用,走默认通道',
}, { source: 'fallback' });
}
const routerContext = await resolveRouterContext({
userId,
sessionId,
text,
forceDeepReasoning,
});
let completion = null;
try {
completion = await withTimeout(
llmProviderService.createChatCompletion({
providerKeyId: policy.modelProviderKeyId || undefined,
model: policy.model || undefined,
modelApiType: policy.modelApiType || undefined,
temperature: policy.temperature,
messages: [
{ role: 'system', content: buildRouterSystemPrompt(grantedSkills) },
{
role: 'user',
content: buildRouterUserPrompt({
text,
routerContext: routerContext.content,
}),
},
],
}),
policy.timeoutMs,
'Chat intent router',
);
} catch (err) {
return normalizeClassification({
route: policy.fallbackRoute,
confidence: 0,
reason: err instanceof Error ? err.message : '意图路由失败,走默认通道',
memory: routerContext,
}, { source: 'fallback', fallbackRoute: policy.fallbackRoute });
}
if (!completion?.ok) {
return normalizeClassification({
route: policy.fallbackRoute,
confidence: 0,
reason: completion?.message ?? '意图路由失败,走默认通道',
memory: routerContext,
}, { source: 'fallback', fallbackRoute: policy.fallbackRoute });
}
const parsed = parseRouterJson(completion.reply);
if (!parsed) {
return normalizeClassification({
route: policy.fallbackRoute,
confidence: 0,
reason: '意图路由响应无法解析,走默认通道',
memory: routerContext,
}, { source: 'fallback', fallbackRoute: policy.fallbackRoute });
}
const classification = {
...normalizeClassification(parsed, { source: 'llm', fallbackRoute: policy.fallbackRoute }),
providerKeyId: completion.providerKeyId ?? policy.modelProviderKeyId ?? null,
model: completion.model ?? policy.model ?? null,
memory: routerContext,
};
if (
classification.route === CHAT_INTENT_ROUTE.DIRECT_CHAT &&
classification.confidence < policy.minConfidence
) {
return normalizeClassification({
...classification,
route: policy.fallbackRoute,
reason: `${classification.reason}(置信度 ${classification.confidence} 低于阈值,走默认通道)`,
}, { source: 'threshold', fallbackRoute: policy.fallbackRoute });
}
return classification;
}
return {
getStatus,
isEnabled,
classify,
applyAgentOrchestration: applyAgentOrchestrationToUserMessage,
};
}
export function createManagedChatIntentRouter({
llmProviderService,
memoryV2 = null,
configService = null,
env = process.env,
logger = console,
} = {}) {
let activeRouter = null;
let activeFingerprint = null;
let activeMeta = { source: 'env', updatedAt: null, updatedBy: null, configError: null };
async function loadRouterState() {
if (!configService?.getRuntimeState) {
return {
source: 'env',
updatedAt: null,
updatedBy: null,
fingerprint: 'env-only',
effectiveEnv: { ...env },
configError: null,
};
}
try {
const state = await configService.getRuntimeState();
return {
source: state?.source ?? 'admin-db',
updatedAt: state?.updatedAt ?? null,
updatedBy: state?.updatedBy ?? null,
fingerprint: state?.fingerprint ?? `admin-db:${Date.now()}`,
effectiveEnv: { ...env, ...(state?.overrides ?? {}) },
configError: null,
};
} catch (err) {
logger?.warn?.(
`[chat-intent-router] admin config unavailable, using process env: ${err instanceof Error ? err.message : err}`,
);
return {
source: 'env-fallback',
updatedAt: null,
updatedBy: null,
fingerprint: 'env-fallback',
effectiveEnv: { ...env },
configError: err instanceof Error ? err.message : String(err),
};
}
}
async function ensureRouter() {
const state = await loadRouterState();
if (activeRouter && activeFingerprint === state.fingerprint) {
activeMeta = state;
return activeRouter;
}
activeRouter = createChatIntentRouter({
llmProviderService,
memoryV2,
env: state.effectiveEnv,
logger,
});
activeFingerprint = state.fingerprint;
activeMeta = state;
return activeRouter;
}
return {
async getStatus() {
const router = await ensureRouter();
return {
...router.getStatus(),
configSource: activeMeta.source,
configUpdatedAt: activeMeta.updatedAt,
configUpdatedBy: activeMeta.updatedBy,
configError: activeMeta.configError,
};
},
async isEnabled() {
const router = await ensureRouter();
return router.isEnabled();
},
async classify(input = {}) {
const router = await ensureRouter();
return router.classify(input);
},
applyAgentOrchestration: applyAgentOrchestrationToUserMessage,
};
}