import express from 'express'; import cors from 'cors'; import { warmUp } from './embed.js'; import { INTENTS_COLLECTION, DOCS_COLLECTION, query, health } from './collections.js'; import { classify } from './confidence.js'; // ==============================|| Doormile AI โ€” retrieval sidecar ||============================== // // // Two jobs, and neither of them is "produce a number": // // POST /route text -> which intent is this? (+ how sure) // POST /ask text -> which documentation passages answer this? // // The console then runs its OWN deterministic intent code to produce every // figure. Nothing this service returns is ever read by an operator as data. const app = express(); app.use(cors()); app.use(express.json({ limit: '64kb' })); app.get('/health', async (_req, res) => { try { const model = await warmUp(); const store = await health(); res.json({ ok: true, model, ...store }); } catch (err) { res.status(503).json({ ok: false, error: err.message }); } }); app.post('/route', async (req, res) => { const text = String(req.body?.text || '').trim(); if (!text) return res.status(400).json({ error: 'text is required' }); try { const hits = await query(INTENTS_COLLECTION, text, 5); const { confidence, score, margin } = classify(hits); // Distinct intents only โ€” five phrasings of the same intent in the top-5 // is a strong signal, not five alternatives to offer the operator. const seen = new Set(); const alternatives = []; for (const h of hits) { const id = h.metadata?.intentId; if (!id || seen.has(id)) continue; seen.add(id); alternatives.push({ intentId: id, score: h.score, example: h.document }); } return res.json({ intentId: alternatives[0]?.intentId ?? null, isWrite: Boolean(hits[0]?.metadata?.isWrite), confidence, score: Number(score.toFixed(4)), margin: Number(margin.toFixed(4)), matchedExample: hits[0]?.document ?? null, // Only meaningful when confidence is low โ€” that is when the panel should // offer these as buttons instead of picking one. alternatives: alternatives.slice(1, 3) }); } catch (err) { return res.status(503).json({ error: err.message }); } }); app.post('/ask', async (req, res) => { const text = String(req.body?.text || '').trim(); if (!text) return res.status(400).json({ error: 'text is required' }); try { const hits = await query(DOCS_COLLECTION, text, 4); // Passages are returned verbatim with attribution. There is no generation // step: summarising would need a hosted model (CLAUDE.md ยง2) and would let // a paraphrase drift from what the document actually says. res.json({ chunks: hits .filter((h) => h.score > 0.35) .map((h) => ({ text: h.document, source: h.metadata?.source, heading: h.metadata?.heading, score: Number(h.score.toFixed(4)) })) }); } catch (err) { res.status(503).json({ error: err.message }); } }); const port = Number(process.env.PORT) || 8787; app.listen(port, () => { console.log(`[doormile-ai] listening on :${port}`); // Pay the model load at boot rather than on an operator's first question. warmUp() .then((m) => console.log(`[doormile-ai] model ready: ${m.model} (${m.dimensions}d)`)) .catch((e) => console.error('[doormile-ai] model failed to load:', e.message)); });