Files
doormilxpress_astryx/src/lib/assistant/ragRouter.js
2026-08-26 15:01:15 +05:30

67 lines
2.8 KiB
JavaScript

// ==============================|| Doormile AI — semantic routing client ||============================== //
//
// Talks to the retrieval sidecar (services/ai) to decide WHICH QUESTION was
// asked. It never returns data — every figure still comes from the intent's own
// deterministic run(), through the same typed API functions the pages use.
//
// The whole module is optional by design:
//
// • REACT_APP_AI_URL unset → disabled, regex matcher only (production today)
// • sidecar unreachable → disabled for this call, regex matcher
// • slow → aborted at ROUTE_TIMEOUT_MS, regex matcher
// • low confidence → not used, regex matcher
//
// Today's behaviour is the floor. This can raise it, never lower it.
const BASE = (typeof import.meta !== 'undefined' && import.meta.env?.VITE_AI_URL) || (typeof process !== 'undefined' && process.env?.REACT_APP_AI_URL) || '';
const ROUTE_TIMEOUT_MS = 400;
export const isRagEnabled = () => Boolean(BASE);
// Once the sidecar has failed we stop hammering it on every keystroke-fast
// question. Re-armed after a cool-off so a restarted container is picked up
// without a page reload.
let disabledUntil = 0;
const COOL_OFF_MS = 30000;
const post = async (path, body) => {
if (!BASE || Date.now() < disabledUntil) return null;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), ROUTE_TIMEOUT_MS);
try {
const res = await fetch(`${BASE}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
signal: controller.signal
});
if (!res.ok) throw new Error(`${res.status}`);
return await res.json();
} catch {
// Any failure — offline, timeout, 5xx — is silent by design. The operator
// gets the deterministic answer; they should never see plumbing.
disabledUntil = Date.now() + COOL_OFF_MS;
return null;
} finally {
clearTimeout(timer);
}
};
// Returns { intentId, confidence, score, margin, alternatives } or null.
export const routeQuestion = (text) => post('/route', { text });
// Documentation passages, verbatim with attribution. No generation step —
// summarising would need a hosted model (assistant/CLAUDE.md §2) and would let
// a paraphrase drift from what the doc actually says.
export const askDocs = (text) => post('/ask', { text });
// A semantic near-miss must never open a create form. Write intents require
// high confidence AND corroboration from the deterministic trigger, so the
// worst case is that the operator types the phrase the regex already knows.
export const isRouteTrustworthy = (routed) => {
if (!routed?.intentId) return false;
if (routed.isWrite) return routed.confidence === 'high';
return routed.confidence === 'high' || routed.confidence === 'medium';
};