implemenation on the bot
This commit is contained in:
97
services/ai/index.js
Normal file
97
services/ai/index.js
Normal file
@@ -0,0 +1,97 @@
|
||||
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));
|
||||
});
|
||||
Reference in New Issue
Block a user