// ==============================|| 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'; };