67 lines
2.8 KiB
JavaScript
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';
|
|
};
|