← back to Fleet Rag
server.js
112 lines
'use strict';
const express = require('express');
const { buildIndex, loadVectors } = require('./lib/index');
const { embed, generate, cosineSim } = require('./lib/embed');
const PORT = process.env.PORT || 9710;
const app = express();
app.use(express.json());
// ── in-memory vector cache ────────────────────────────────────────────────────
let vectorCache = [];
let indexing = false;
function refreshCache() {
vectorCache = loadVectors();
console.log(`[fleet-rag] cache loaded: ${vectorCache.length} vectors`);
}
// Load on start if vectors file already exists
refreshCache();
// ── routes ────────────────────────────────────────────────────────────────────
app.get('/healthz', (_req, res) => {
res.json({
status: 'ok',
vectors: vectorCache.length,
indexing,
port: PORT,
});
});
app.post('/index', async (_req, res) => {
if (indexing) {
return res.status(409).json({ error: 'indexing already in progress' });
}
indexing = true;
const logs = [];
const log = msg => { logs.push(msg); process.stdout.write(msg + '\n'); };
try {
const result = await buildIndex(log);
refreshCache();
res.json({ ok: true, ...result, logs });
} catch (e) {
res.status(500).json({ error: e.message, logs });
} finally {
indexing = false;
}
});
app.post('/ask', async (req, res) => {
const question = (req.body && req.body.question) ? String(req.body.question).trim() : '';
if (!question) return res.status(400).json({ error: 'question is required' });
if (vectorCache.length === 0) {
return res.status(503).json({ error: 'No index loaded. POST /index first.' });
}
try {
// 1. Embed the question
const qVec = await embed(question);
// 2. Cosine sim against all vectors — find top 5
const scored = vectorCache.map(v => ({
...v,
score: cosineSim(qVec, v.embedding),
}));
scored.sort((a, b) => b.score - a.score);
const topK = scored.slice(0, 5);
// 3. Build context for LLM
const context = topK.map((c, i) =>
`[${i + 1}] (${c.source_type} — ${c.source_path})\n${c.chunk_text}`
).join('\n\n---\n\n');
const prompt = `/no_think
You are Fleet-RAG, an expert assistant for Steve's local dev fleet.
Answer the following question using ONLY the context below.
Be concise and direct. Include the service name and its path/port when relevant.
If the answer is not in the context, say "I don't have enough information in the index."
QUESTION: ${question}
CONTEXT:
${context}
ANSWER:`;
// 4. Generate answer
const answer = await generate(prompt);
// 5. Return answer + cited sources
const sources = topK.map(c => ({
source_type: c.source_type,
source_path: c.source_path,
score: Math.round(c.score * 1000) / 1000,
preview: c.chunk_text.slice(0, 120),
}));
res.json({ answer: answer.trim(), sources });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// ── start ─────────────────────────────────────────────────────────────────────
app.listen(PORT, () => {
console.log(`[fleet-rag] listening on http://localhost:${PORT}`);
console.log(`[fleet-rag] endpoints: GET /healthz POST /index POST /ask`);
});