695 lines
28 KiB
JavaScript
695 lines
28 KiB
JavaScript
const SAFE_ROUTES = new Set([
|
|
"cached_answer",
|
|
"predefined_answer",
|
|
"main_llm",
|
|
"clarification",
|
|
"refusal",
|
|
"unavailable"
|
|
]);
|
|
|
|
class GateProvider {
|
|
constructor({ getConfig, runtime, lookupRepo, lookupCorrection, lookupKnowledge, groundKnowledgeLinks, cache, metrics }) {
|
|
Object.assign(this, { getConfig, runtime, lookupRepo, lookupCorrection, lookupKnowledge, groundKnowledgeLinks, cache, metrics });
|
|
this.recentPrompts = new Map();
|
|
}
|
|
|
|
async route({ message, knowledgeQuery = "", user, role, scope, originContext, onStage = () => {} }) {
|
|
const started = Date.now();
|
|
const cfg = this.getConfig();
|
|
const gate = cfg.gate || {};
|
|
const prepared = stripForcePrefix(message, gate.force_prefix);
|
|
const context = {
|
|
message: prepared.message,
|
|
knowledge_query: knowledgeQuery || prepared.message,
|
|
role,
|
|
platform: originContext?.platform || originContext?.origin || "webui"
|
|
};
|
|
const requestClass = classifyRequestType(context.message, { role, scope });
|
|
onStage("deterministic");
|
|
const forceReason = prepared.forced
|
|
? "explicit_force_prefix"
|
|
: this.isRepeat(context, user?.id, scope, gate)
|
|
? "repeat_prompt_force"
|
|
: null;
|
|
this.remember(context.message, user?.id, scope, gate);
|
|
const knowledge = this.lookupEarlyKnowledge({
|
|
...context,
|
|
knowledge_query: context.knowledge_query,
|
|
user,
|
|
scope,
|
|
originContext
|
|
});
|
|
|
|
if (forceReason) {
|
|
return this.finish({
|
|
route: "main_llm",
|
|
confidence: 1,
|
|
reason_code: forceReason,
|
|
message: context.message,
|
|
forced: true,
|
|
request_class: requestClass,
|
|
deterministic_ms: Date.now() - started,
|
|
gate_ms: 0,
|
|
knowledge_context: knowledge.blocks,
|
|
knowledge_diagnostics: knowledge.diagnostics
|
|
}, started, context);
|
|
}
|
|
|
|
if (isSensitiveRequest(context.message)) {
|
|
return this.finish({
|
|
route: "main_llm",
|
|
confidence: 1,
|
|
reason_code: "sensitive_or_user_specific",
|
|
message: context.message,
|
|
request_class: requestClass,
|
|
deterministic_ms: Date.now() - started,
|
|
gate_ms: 0,
|
|
knowledge_context: knowledge.blocks,
|
|
knowledge_diagnostics: knowledge.diagnostics
|
|
}, started, context);
|
|
}
|
|
|
|
const reviewed = gate.predefined_enabled !== false
|
|
? this.lookupCorrection?.({
|
|
...context,
|
|
origin: originContext?.origin || context.platform
|
|
})
|
|
: null;
|
|
if (reviewed) {
|
|
return this.finish({
|
|
route: "predefined_answer",
|
|
confidence: reviewed.score,
|
|
reason_code: `approved_${reviewed.target}`,
|
|
message: context.message,
|
|
answer: {
|
|
text: reviewed.corrected_answer,
|
|
links: reviewed.expected_link
|
|
? [{ label: reviewed.route_alias || "Open verified Lumi page", href: reviewed.expected_link }]
|
|
: [],
|
|
source: { type: "approved_correction", id: reviewed.id },
|
|
safe: true
|
|
},
|
|
request_class: requestClass,
|
|
deterministic_ms: Date.now() - started,
|
|
gate_ms: 0
|
|
}, started, context);
|
|
}
|
|
|
|
if (knowledge.blocks.length) {
|
|
const groundedLink = this.groundKnowledgeLinks?.("", knowledge.blocks, context.message);
|
|
if (groundedLink?.diagnostics?.grounded) {
|
|
return this.finish({
|
|
route: "predefined_answer",
|
|
confidence: 1,
|
|
reason_code: "verified_okf_link",
|
|
message: context.message,
|
|
answer: {
|
|
text: groundedLink.text,
|
|
links: groundedLink.links,
|
|
source: { type: "approved_okf" },
|
|
safe: true
|
|
},
|
|
request_class: requestClass,
|
|
deterministic_ms: Date.now() - started,
|
|
gate_ms: 0,
|
|
knowledge_context: knowledge.blocks,
|
|
knowledge_diagnostics: knowledge.diagnostics
|
|
}, started, context);
|
|
}
|
|
const fastStarted = Date.now();
|
|
onStage("gating");
|
|
const fastAnswer = await this.answerFromKnowledge(context, knowledge.blocks);
|
|
if (fastAnswer && !isComplexOrAmbiguous(context.message)) {
|
|
return this.finish({
|
|
route: "predefined_answer",
|
|
confidence: 0.95,
|
|
reason_code: "verified_okf_fast_answer",
|
|
message: context.message,
|
|
answer: {
|
|
text: fastAnswer,
|
|
links: [],
|
|
source: { type: "approved_okf" },
|
|
safe: true
|
|
},
|
|
request_class: requestClass,
|
|
deterministic_ms: fastStarted - started,
|
|
gate_ms: Date.now() - fastStarted,
|
|
knowledge_context: knowledge.blocks,
|
|
knowledge_diagnostics: knowledge.diagnostics,
|
|
knowledge_sufficient: true
|
|
}, started, context);
|
|
}
|
|
return this.finish({
|
|
route: "main_llm",
|
|
confidence: 1,
|
|
reason_code: "okf_requires_main_llm",
|
|
message: context.message,
|
|
request_class: requestClass,
|
|
deterministic_ms: Date.now() - started,
|
|
gate_ms: Date.now() - fastStarted,
|
|
knowledge_context: knowledge.blocks,
|
|
knowledge_diagnostics: knowledge.diagnostics,
|
|
knowledge_sufficient: Boolean(fastAnswer)
|
|
}, started, context);
|
|
}
|
|
|
|
const cached = gate.predefined_enabled !== false ? this.cache?.get(context) : null;
|
|
if (cached) {
|
|
return this.finish({
|
|
route: "cached_answer",
|
|
confidence: 1,
|
|
reason_code: "exact_cache_hit",
|
|
message: context.message,
|
|
answer: cached,
|
|
request_class: requestClass,
|
|
deterministic_ms: Date.now() - started,
|
|
gate_ms: 0
|
|
}, started, context);
|
|
}
|
|
|
|
const repoAnswer = gate.predefined_enabled !== false
|
|
? this.lookupRepo?.(context.message) || null
|
|
: null;
|
|
if (isExactPredefinedQuery(context.message, repoAnswer)) {
|
|
const answer = {
|
|
text: repoAnswer.text,
|
|
links: repoAnswer.links || [],
|
|
source: repoAnswer.source || null,
|
|
safe: true
|
|
};
|
|
this.cache?.set(context, answer);
|
|
return this.finish({
|
|
route: "predefined_answer",
|
|
confidence: 1,
|
|
reason_code: `exact_verified_${repoAnswer.type}`,
|
|
message: context.message,
|
|
answer,
|
|
request_class: "navigation_help",
|
|
deterministic_ms: Date.now() - started,
|
|
gate_ms: 0
|
|
}, started, context);
|
|
}
|
|
|
|
if (isComplexOrAmbiguous(context.message)) {
|
|
return this.finish({
|
|
route: "main_llm",
|
|
confidence: 1,
|
|
reason_code: "deterministic_complexity_escalation",
|
|
message: context.message,
|
|
request_class: requestClass,
|
|
deterministic_ms: Date.now() - started,
|
|
gate_ms: 0
|
|
}, started, context);
|
|
}
|
|
|
|
if (isSimpleKnowledgeLookup(context.message)) {
|
|
return this.finish({
|
|
route: "main_llm",
|
|
confidence: 0.86,
|
|
reason_code: "simple_knowledge_lookup",
|
|
message: context.message,
|
|
request_class: requestClass,
|
|
deterministic_ms: Date.now() - started,
|
|
gate_ms: 0
|
|
}, started, context);
|
|
}
|
|
|
|
const deterministicMs = Date.now() - started;
|
|
const gateStarted = Date.now();
|
|
onStage("gating");
|
|
let classification;
|
|
try {
|
|
classification = await this.classify(context);
|
|
} catch (error) {
|
|
classification = {
|
|
route: "main_llm",
|
|
confidence: 0,
|
|
reason_code: isTimeoutError(error) ? "gate_timeout_escalated" : "gate_error_escalated",
|
|
gate_error: error.message
|
|
};
|
|
}
|
|
|
|
const normalized = normalizeDecision(classification);
|
|
const mainThreshold = Math.max(0.1, Math.min(0.95, Number(gate.main_llm_threshold) || 0.72));
|
|
const highThreshold = Math.max(0.5, Math.min(0.99, Number(gate.high_confidence_threshold) || 0.88));
|
|
let decision = normalized;
|
|
|
|
if (["refusal", "unavailable"].includes(decision.route) && decision.confidence < highThreshold) {
|
|
decision = {
|
|
route: "main_llm",
|
|
confidence: decision.confidence,
|
|
reason_code: "terminal_route_low_confidence"
|
|
};
|
|
} else if (["cached_answer", "predefined_answer"].includes(decision.route)) {
|
|
decision = {
|
|
route: "main_llm",
|
|
confidence: decision.confidence,
|
|
reason_code: "gate_cannot_authorize_predefined"
|
|
};
|
|
} else if (
|
|
(decision.confidence < mainThreshold || !SAFE_ROUTES.has(decision.route)) &&
|
|
!["gate_timeout_escalated", "gate_error_escalated"].includes(decision.reason_code)
|
|
) {
|
|
decision = {
|
|
route: "main_llm",
|
|
confidence: decision.confidence,
|
|
reason_code: "low_confidence"
|
|
};
|
|
} else if (decision.route === "refusal") {
|
|
decision.answer = {
|
|
text: cfg.instructions?.out_of_scope_response || "I cannot help with that request.",
|
|
links: [],
|
|
safe: true
|
|
};
|
|
} else if (decision.route === "unavailable") {
|
|
decision.answer = {
|
|
text: cfg.commands?.unavailable_message || "Lumi Assistant is currently unavailable.",
|
|
links: [],
|
|
safe: true
|
|
};
|
|
} else if (decision.route === "clarification") {
|
|
decision.route = "main_llm";
|
|
decision.reason_code = "clarification_requires_main_llm";
|
|
}
|
|
|
|
return this.finish({
|
|
...decision,
|
|
message: context.message,
|
|
request_class: requestClass,
|
|
deterministic_ms: deterministicMs,
|
|
gate_ms: Date.now() - gateStarted
|
|
}, started, context);
|
|
}
|
|
|
|
async classify(context) {
|
|
if (this.runtime.status().state !== "running") throw new Error("Gate runtime is unavailable.");
|
|
const timeoutMs = Math.max(1000, Math.min(5000, Number(this.getConfig().gate?.timeout_ms) || 3000));
|
|
const prompt = [
|
|
"Classify only. JSON only.",
|
|
"Routes: main_llm, refusal, unavailable.",
|
|
"Escalate uncertainty or complexity to main_llm.",
|
|
'{"route":"main_llm","confidence":0.0,"reason_code":"short_code"}'
|
|
].join("\n");
|
|
const result = await withTimeout(this.runtime.infer([
|
|
{ role: "system", content: prompt },
|
|
{ role: "user", content: String(context.message).slice(0, 1000) }
|
|
], 64, timeoutMs), timeoutMs);
|
|
return parseDecision(result.choices?.[0]?.message?.content);
|
|
}
|
|
|
|
lookupEarlyKnowledge(context) {
|
|
if (typeof this.lookupKnowledge !== "function") return { blocks: [], diagnostics: null };
|
|
try {
|
|
const result = this.lookupKnowledge(context);
|
|
if (result && typeof result === "object" && !Array.isArray(result)) {
|
|
return {
|
|
blocks: normalizeKnowledgeBlocks(result.blocks ?? result.context ?? result.items ?? []),
|
|
diagnostics: result.diagnostics || null
|
|
};
|
|
}
|
|
return { blocks: normalizeKnowledgeBlocks(result), diagnostics: null };
|
|
} catch {
|
|
return { blocks: [], diagnostics: null };
|
|
}
|
|
}
|
|
|
|
async answerFromKnowledge(context, blocks) {
|
|
if (this.runtime.status().state !== "running") return null;
|
|
const timeoutMs = Math.max(1000, Math.min(5000, Number(this.getConfig().gate?.timeout_ms) || 3000));
|
|
const evidence = prepareFastKnowledgeEvidence(blocks, context.message);
|
|
const prompt = [
|
|
"Use only the VERIFIED FACTS below.",
|
|
"Write a short, natural, satisfying reply of 1 to 3 complete sentences.",
|
|
"Answer directly, then add one or two useful distinguishing details when available.",
|
|
"For identity questions, prefer roles, relationships, aliases, activities, and named channels. Do not merely repeat the first sentence.",
|
|
"Resolve pronouns using the RESOLVED REQUEST. Do not mention facts, context, OKF, files, verification, workflows, or databases.",
|
|
"Do not print raw URLs unless the user asks for a link. Never invent details, routes, or links.",
|
|
"If the facts do not answer the request, output only INSUFFICIENT.",
|
|
`RESOLVED REQUEST:\n${String(context.knowledge_query || context.message).slice(0, 700)}`,
|
|
`VERIFIED FACTS:\n${evidence}`
|
|
].join("\n");
|
|
try {
|
|
const result = await withTimeout(this.runtime.infer([
|
|
{ role: "system", content: prompt },
|
|
{ role: "user", content: context.message }
|
|
], 256, timeoutMs), timeoutMs);
|
|
const rawAnswer = result.choices?.[0]?.message?.content;
|
|
const answer = parseKnowledgeAnswer(rawAnswer, blocks, context.message);
|
|
if (answer) return answer;
|
|
const groundedDraft = parseKnowledgeAnswer(rawAnswer, blocks, context.message, { requireSatisfying: false });
|
|
if (!groundedDraft) return null;
|
|
const personRelationshipQuestion = isPersonRelationshipQuestion(context.message);
|
|
const repairPrompt = (personRelationshipQuestion ? [
|
|
"Using only the verified facts, finish the sentence start with one useful detail and output the complete sentence.",
|
|
"Preserve the relationship in the sentence start. Add a role, activity, or alias; do not invent anything.",
|
|
`SENTENCE START:\n${groundedDraft.replace(/[.!?]+$/, "").slice(0, 240)}, who`
|
|
] : [
|
|
"The first answer was correct but too abrupt.",
|
|
"Write one natural sentence of 10 to 24 words using only the verified facts.",
|
|
"Answer directly and add at least one different supporting role, relationship, activity, alias, or named channel."
|
|
]).concat([
|
|
"Do not mention context, OKF, files, verification, or missing information. Do not invent anything.",
|
|
`RESOLVED REQUEST:\n${String(context.knowledge_query || context.message).slice(0, 700)}`,
|
|
`VERIFIED FACTS:\n${evidence}`
|
|
]).join("\n");
|
|
const repaired = await withTimeout(this.runtime.infer([
|
|
{ role: "system", content: repairPrompt },
|
|
{ role: "user", content: context.message }
|
|
], 192, timeoutMs), timeoutMs);
|
|
return parseKnowledgeAnswer(repaired.choices?.[0]?.message?.content, blocks, context.message);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
isRepeat(context, userId, scope, gate) {
|
|
const configuredWindow = Number(gate.repeat_force_window_seconds);
|
|
const windowMs = Math.max(0, Number.isFinite(configuredWindow) ? configuredWindow : 90) * 1000;
|
|
if (!windowMs) return false;
|
|
const key = `${userId || "anonymous"}:${scope || "assistant"}`;
|
|
const rows = (this.recentPrompts.get(key) || []).filter((entry) => Date.now() - entry.at <= windowMs);
|
|
const threshold = Math.max(0.5, Math.min(1, Number(gate.similarity_threshold) || 0.86));
|
|
return rows.some((entry) => similarity(entry.message, context.message) >= threshold);
|
|
}
|
|
|
|
remember(message, userId, scope, gate) {
|
|
const key = `${userId || "anonymous"}:${scope || "assistant"}`;
|
|
const configuredWindow = Number(gate.repeat_force_window_seconds);
|
|
const windowMs = Math.max(1, Number.isFinite(configuredWindow) ? configuredWindow : 90) * 1000;
|
|
const rows = (this.recentPrompts.get(key) || [])
|
|
.filter((entry) => Date.now() - entry.at <= windowMs)
|
|
.slice(-9);
|
|
rows.push({ message, at: Date.now() });
|
|
this.recentPrompts.set(key, rows);
|
|
}
|
|
|
|
finish(decision, started, context) {
|
|
const output = {
|
|
route: decision.route,
|
|
confidence: Number(decision.confidence) || 0,
|
|
reason_code: decision.reason_code || "unspecified",
|
|
answer: decision.answer || null,
|
|
message: decision.message || context.message,
|
|
gate_error: decision.gate_error ? String(decision.gate_error).slice(0, 300) : null,
|
|
forced: Boolean(decision.forced),
|
|
request_class: normalizeRequestClass(decision.request_class),
|
|
deterministic_ms: Math.max(0, Number(decision.deterministic_ms) || 0),
|
|
gate_ms: Math.max(0, Number(decision.gate_ms) || 0),
|
|
duration_ms: Date.now() - started
|
|
};
|
|
Object.defineProperties(output, {
|
|
knowledge_context: {
|
|
value: normalizeKnowledgeBlocks(decision.knowledge_context),
|
|
enumerable: false
|
|
},
|
|
knowledge_diagnostics: {
|
|
value: decision.knowledge_diagnostics || null,
|
|
enumerable: false
|
|
}
|
|
});
|
|
output.knowledge_match_count = output.knowledge_context.length;
|
|
output.knowledge_sufficient = Boolean(decision.knowledge_sufficient || decision.answer?.source?.type === "approved_okf");
|
|
this.metrics.record({
|
|
kind: "gate_decision",
|
|
status: "success",
|
|
route_used: output.route,
|
|
confidence: output.confidence,
|
|
reason_code: output.reason_code,
|
|
gate_error: output.gate_error,
|
|
request_class: output.request_class,
|
|
route_class: output.request_class,
|
|
deterministic_ms: output.deterministic_ms,
|
|
gate_ms: output.gate_ms,
|
|
duration_ms: output.duration_ms,
|
|
okf_checked: typeof this.lookupKnowledge === "function",
|
|
okf_match_count: output.knowledge_match_count,
|
|
okf_sufficient: output.knowledge_sufficient,
|
|
platform: context.platform
|
|
});
|
|
return output;
|
|
}
|
|
}
|
|
|
|
function normalizeKnowledgeBlocks(value) {
|
|
return (Array.isArray(value) ? value : value == null ? [] : [value])
|
|
.map((block) => String(block || "").trim())
|
|
.filter(Boolean)
|
|
.slice(0, 2);
|
|
}
|
|
|
|
function prepareFastKnowledgeEvidence(blocks = [], message = "") {
|
|
const needsRelationshipContext = /\b(?:he|him|his|she|her|hers|they|them|their|theirs|fianc(?:e|ee|é|ée)?|wife|husband|partner|relationship|related)\b/i
|
|
.test(String(message || ""));
|
|
const selectedBlocks = normalizeKnowledgeBlocks(blocks).slice(0, needsRelationshipContext ? 2 : 1);
|
|
return selectedBlocks.map((rawBlock) => {
|
|
const heading = rawBlock.match(/(?:^|;\s*)heading=([^;\n]+)/im)?.[1] ||
|
|
rawBlock.match(/^OKF entry:\s*(.+)$/im)?.[1] || "Relevant knowledge";
|
|
const factsMarker = rawBlock.match(/(?:^|\n)Facts:\s*\n/i);
|
|
let facts = factsMarker
|
|
? rawBlock.slice((factsMarker.index || 0) + factsMarker[0].length)
|
|
: rawBlock;
|
|
facts = facts
|
|
.replace(/\[([^\]]+)\]\(https?:\/\/[^)\s]+\)/gi, "$1")
|
|
.replace(/\bis a known community contact in Lumi's local community knowledge\b/gi, "is a Lumi community contact")
|
|
.replace(/When users ask how to contact[\s\S]*?unless a verified internal Lumi workflow is available\./gi, "The recommended contact channel is the community Discord server.")
|
|
.replace(/^Links:\s*$/gim, "Named channels:")
|
|
.replace(/\n{3,}/g, "\n\n")
|
|
.trim();
|
|
return `Section: ${heading}\n${facts}`;
|
|
}).join("\n\n").slice(0, 2300);
|
|
}
|
|
|
|
function parseKnowledgeAnswer(value, blocks = [], message = "", { requireSatisfying = true } = {}) {
|
|
const cleaned = String(value || "")
|
|
.replace(/<think>[\s\S]*?<\/think>/gi, "")
|
|
.trim();
|
|
const marker = cleaned.match(/(?:^|\n)\s*(ANSWER|INSUFFICIENT)\s*:?[ \t]*(?:\n|$)?/i);
|
|
if (marker?.[1]?.toUpperCase() === "INSUFFICIENT" || /^\s*INSUFFICIENT\b/i.test(cleaned)) return null;
|
|
let answer = marker?.[1]?.toUpperCase() === "ANSWER"
|
|
? cleaned.slice((marker.index || 0) + marker[0].length).trim()
|
|
: cleaned;
|
|
if (!answer || answer.length > 1800) return null;
|
|
if (!hasKnowledgeSupport(answer, blocks)) return null;
|
|
if (requireSatisfying && !isSatisfyingFastAnswer(answer, message)) return null;
|
|
const allowedUrls = new Set(extractHttpUrls(blocks.join("\n")).map(normalizeUrl).filter(Boolean));
|
|
answer = answer.replace(/https?:\/\/[^\s<>()\]]+/gi, (match) => {
|
|
const punctuation = match.match(/[.,;:!?]+$/)?.[0] || "";
|
|
const raw = punctuation ? match.slice(0, -punctuation.length) : match;
|
|
return allowedUrls.has(normalizeUrl(raw)) ? match : "[unsupported link removed]";
|
|
});
|
|
return answer;
|
|
}
|
|
|
|
function isSatisfyingFastAnswer(answer, message) {
|
|
const words = String(answer || "").trim().split(/\s+/).filter(Boolean);
|
|
const identityQuestion = /^\s*who(?:'s|\s+is|\s+are)\b/i.test(String(message || ""));
|
|
const relationshipQuestion = isPersonRelationshipQuestion(message) || /\b(?:name|nickname|alias)\b/i.test(String(message || ""));
|
|
if (identityQuestion && !relationshipQuestion && words.length < 8) return false;
|
|
if (identityQuestion && relationshipQuestion && words.length < 6) return false;
|
|
if (isPersonRelationshipQuestion(message) && !answersRequestedRelationship(answer, message)) return false;
|
|
return words.length >= 3;
|
|
}
|
|
|
|
function isPersonRelationshipQuestion(message) {
|
|
return /\b(?:fianc(?:e|ee|é|ée)?|wife|husband|partner|owner)\b/i.test(String(message || ""));
|
|
}
|
|
|
|
function answersRequestedRelationship(answer, message) {
|
|
const response = String(answer || "");
|
|
const request = String(message || "");
|
|
if (/\bfianc(?:e|ee|é|ée)?\b/i.test(request)) return /\b(?:fianc(?:e|ee|é|ée)?|engaged)\b/i.test(response);
|
|
if (/\b(?:wife|husband)\b/i.test(request)) return /\b(?:wife|husband|spouse|married)\b/i.test(response);
|
|
if (/\bpartner\b/i.test(request)) return /\bpartner\b/i.test(response);
|
|
if (/\bowner\b/i.test(request)) return /\bown(?:er|s|ed|ing)?\b/i.test(response);
|
|
return true;
|
|
}
|
|
|
|
function hasKnowledgeSupport(answer, blocks) {
|
|
const ignored = new Set([
|
|
"about", "also", "and", "are", "but", "for", "from", "have", "he", "her", "hers", "him", "his", "into", "its", "only", "she", "that", "the", "their", "them", "they", "this", "through", "use", "was", "were", "who", "with", "you", "your"
|
|
]);
|
|
const tokens = (value) => String(value || "").toLowerCase().split(/[^a-z0-9]+/)
|
|
.filter((token) => token.length >= 3 && !ignored.has(token));
|
|
const answerTokens = [...new Set(tokens(answer))];
|
|
if (!answerTokens.length) return false;
|
|
const evidenceTokens = new Set(tokens(blocks.join("\n")));
|
|
const supported = answerTokens.filter((token) => evidenceTokens.has(token)).length;
|
|
return supported >= Math.min(3, answerTokens.length) && supported / answerTokens.length >= 0.35;
|
|
}
|
|
|
|
function extractHttpUrls(value) {
|
|
return String(value || "").match(/https?:\/\/[^\s<>()\]]+/gi) || [];
|
|
}
|
|
|
|
function normalizeUrl(value) {
|
|
try {
|
|
const url = new URL(String(value || "").replace(/[.,;:!?]+$/, ""));
|
|
if (!['http:', 'https:'].includes(url.protocol)) return "";
|
|
url.hash = "";
|
|
if (url.pathname.length > 1) url.pathname = url.pathname.replace(/\/+$/, "");
|
|
return url.href;
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function parseDecision(value) {
|
|
const text = String(value || "").trim();
|
|
const match = text.match(/\{[\s\S]*\}/);
|
|
if (!match) throw new Error("Gate model returned invalid JSON.");
|
|
return JSON.parse(match[0]);
|
|
}
|
|
|
|
function normalizeDecision(value = {}) {
|
|
return {
|
|
route: SAFE_ROUTES.has(value.route) ? value.route : "main_llm",
|
|
confidence: Math.max(0, Math.min(1, Number(value.confidence) || 0)),
|
|
reason_code: /^[a-z0-9_]{2,80}$/.test(String(value.reason_code || ""))
|
|
? value.reason_code
|
|
: "invalid_reason_code",
|
|
gate_error: value.gate_error ? String(value.gate_error).slice(0, 300) : null
|
|
};
|
|
}
|
|
|
|
function stripForcePrefix(message, prefix = "force ai:") {
|
|
const text = String(message || "").trim();
|
|
const configured = String(prefix || "").trim();
|
|
if (!configured || !text.toLowerCase().startsWith(configured.toLowerCase())) {
|
|
return { message: text, forced: false };
|
|
}
|
|
return { message: text.slice(configured.length).trim() || text, forced: true };
|
|
}
|
|
|
|
function isSensitiveRequest(message) {
|
|
const text = String(message || "");
|
|
if (/\b(token|password|secret|credential|api key|private key)\b/i.test(text)) return true;
|
|
const personal = /\b(my|mine|our|ours|their|theirs|this user|user id|username)\b/i.test(text) &&
|
|
/\b(balance|inventory|economy|points|currency|database|file|permission|role|account|profile)\b/i.test(text);
|
|
const action = /\b(delete|remove|ban|timeout|moderate|transfer|pay|give|set|change|edit|execute|run|install|grant|revoke|reset)\b/i.test(text) &&
|
|
/\b(balance|inventory|economy|points|currency|database|file|api|permission|role|user|account|plugin|command)\b/i.test(text);
|
|
return personal || action;
|
|
}
|
|
|
|
function isCacheSafeRepoAnswer(answer) {
|
|
if (!answer?.text) return false;
|
|
if (answer.type === "route") return answer.source?.confidence === "high";
|
|
return ["contact", "unknown"].includes(answer.type);
|
|
}
|
|
|
|
function isExactPredefinedQuery(message, answer) {
|
|
if (!isCacheSafeRepoAnswer(answer)) return false;
|
|
if (isComplexOrAmbiguous(message)) return false;
|
|
if (answer.type === "contact") return true;
|
|
if (answer.type !== "route" || answer.source?.confidence !== "high") return false;
|
|
return /\b(where|open|find|navigate|page|screen|menu|settings?|configuration|wizard|location)\b/i
|
|
.test(String(message || ""));
|
|
}
|
|
|
|
function isComplexOrAmbiguous(message) {
|
|
const text = String(message || "");
|
|
if (text.length > 500 || text.split(/\s+/).length > 70) return true;
|
|
return /\b(why|explain|debug|diagnos|troubleshoot|fix|error|failed|failure|code|javascript|python|implement|design|compare|analy[sz]e|step by step|multi[- ]?step|architecture|configure and|set up and|what should|this|that|it)\b/i
|
|
.test(text);
|
|
}
|
|
|
|
function isSimpleKnowledgeLookup(message) {
|
|
const text = String(message || "").trim();
|
|
if (!text || text.length > 180 || text.split(/\s+/).length > 18) return false;
|
|
if (/\b(who|what)\s+(?:are|r)\s+you\b|\byour\s+(?:name|identity)\b/i.test(text)) return false;
|
|
return (
|
|
/^(?:who|what)\s+(?:is|are|was|were)\s+["'`]?[\p{L}\p{N}_ .'-]{2,80}["'`]?\??$/iu.test(text) ||
|
|
/^(?:tell me about|describe|identify)\s+["'`]?[\p{L}\p{N}_ .'-]{2,80}["'`]?\??$/iu.test(text)
|
|
);
|
|
}
|
|
|
|
function isTimeoutError(error) {
|
|
return error?.name === "TimeoutError" || error?.name === "AbortError" || /timed?\s*out|timeout/i.test(error?.message || "");
|
|
}
|
|
|
|
function classifyRequestType(message, { role = "user", scope = "assistant" } = {}) {
|
|
const text = String(message || "").trim();
|
|
if (hasRouteReference(text)) {
|
|
return "navigation_help";
|
|
}
|
|
if (/\b(explicitly|please|give|write|provide|show)\b[\s\S]{0,60}\b(long|detailed|comprehensive|thorough|in[- ]depth)\b|\b(full (analysis|report|guide|explanation)|in detail|very detailed|long answer)\b/i.test(text)) {
|
|
return "explicit_long";
|
|
}
|
|
if (/\b(custom command|javascript|python|code block|function run\s*\(|def run\s*\(|implement|write code|script)\b/i.test(text)) {
|
|
return "code_custom_command";
|
|
}
|
|
if (
|
|
role === "admin" &&
|
|
(scope === "model_test" || /\b(debug|diagnos|troubleshoot|stack trace|runtime|backend|database|logs?|metrics?|configuration|config|error|failed|failure)\b/i.test(text))
|
|
) {
|
|
return "admin_debug";
|
|
}
|
|
if (/\b(where|open|find|navigate|page|screen|menu|settings?|configuration|wizard|location|link|path)\b/i.test(text)) {
|
|
return "navigation_help";
|
|
}
|
|
return "simple_answer";
|
|
}
|
|
|
|
function hasRouteReference(text) {
|
|
return /\b(?:GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s+\/[^\s]+/i.test(text) ||
|
|
/\b(?:route|webroute|web route|endpoint|request|api)\b[\s\S]{0,80}\/[a-z0-9_/-]+/i.test(text) ||
|
|
/\/(?:admin|api|setup|auth|plugins|commands|feedback|stats|pages)(?:\/[a-z0-9_/-]*)?\b/i.test(text);
|
|
}
|
|
|
|
function normalizeRequestClass(value) {
|
|
return [
|
|
"navigation_help",
|
|
"simple_answer",
|
|
"code_custom_command",
|
|
"admin_debug",
|
|
"explicit_long"
|
|
].includes(value) ? value : "simple_answer";
|
|
}
|
|
|
|
function withTimeout(promise, timeoutMs) {
|
|
let timer;
|
|
const timeout = new Promise((_, reject) => {
|
|
timer = setTimeout(() => {
|
|
reject(Object.assign(new Error(`Gate timed out after ${timeoutMs}ms.`), { name: "TimeoutError" }));
|
|
}, timeoutMs);
|
|
});
|
|
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
}
|
|
|
|
function similarity(left, right) {
|
|
const a = tokens(left);
|
|
const b = tokens(right);
|
|
if (!a.size || !b.size) return 0;
|
|
let intersection = 0;
|
|
for (const token of a) if (b.has(token)) intersection += 1;
|
|
return intersection / (a.size + b.size - intersection);
|
|
}
|
|
|
|
function tokens(value) {
|
|
const ignored = new Set([
|
|
"a", "an", "are", "can", "could", "do", "find", "for", "how", "i", "in", "is",
|
|
"me", "of", "please", "the", "this", "to", "where", "would", "you"
|
|
]);
|
|
return new Set(
|
|
String(value || "").toLowerCase().split(/[^a-z0-9]+/)
|
|
.filter((token) => token && !ignored.has(token))
|
|
);
|
|
}
|
|
|
|
module.exports = {
|
|
GateProvider,
|
|
parseDecision,
|
|
stripForcePrefix,
|
|
isSensitiveRequest,
|
|
similarity,
|
|
isCacheSafeRepoAnswer,
|
|
isExactPredefinedQuery,
|
|
isComplexOrAmbiguous,
|
|
isSimpleKnowledgeLookup,
|
|
isSatisfyingFastAnswer,
|
|
prepareFastKnowledgeEvidence,
|
|
classifyRequestType,
|
|
normalizeRequestClass,
|
|
hasRouteReference,
|
|
withTimeout
|
|
};
|