59 lines
2.7 KiB
JavaScript
59 lines
2.7 KiB
JavaScript
import { app } from 'mu';
|
|
import express from 'express';
|
|
import { loadProfiles } from './src/runner/profile.js';
|
|
import { buildSession, stripStoreError } from './src/db.js';
|
|
import { ask } from './src/llm/loop.js';
|
|
import { mountChat } from './src/chat/index.js';
|
|
import { reportAssistant } from './src/report-assistant.js';
|
|
|
|
const PROFILE_DIR = process.env.PROFILE_DIR || '/config/profiles';
|
|
|
|
// Profiles load fire-and-forget, like waitForDatabase in the bbcdr services:
|
|
// routes are registered synchronously, handlers await the same promise, and a
|
|
// call that lands before the load finishes just waits it out (local file
|
|
// reads). If loading failed, calls answer an error instead of crashing boot.
|
|
let profilesPromise = loadProfiles(PROFILE_DIR);
|
|
profilesPromise.catch(e => console.error('[profiles] loading failed:', e.message));
|
|
|
|
async function whenProfiles() {
|
|
try {
|
|
return await profilesPromise;
|
|
} catch (e) {
|
|
throw new Error('profiles are not available: ' + e.message);
|
|
}
|
|
}
|
|
|
|
// The chat. The turn module is generic (src/chat/ moves between services
|
|
// without edits); reportAssistant is this service's one answer hook: it runs
|
|
// the loop below with the conversation so far and turns a report run into an
|
|
// interim message with a pending file. Step 1's echo hook, kept for tests:
|
|
// answer: async (turn) => { await sleep(5000); return `Je schreef: ${turn.content}`; }
|
|
mountChat(app, { path: '/assistant', answer: reportAssistant(whenProfiles) });
|
|
|
|
// The endpoint. One conversation turn in Dutch, the agent loop runs inside
|
|
// (read the profiles, look up values on the public graph, write a spec,
|
|
// validate and repair it, run the report) and the answer in Dutch comes back
|
|
// with a trace of the tool calls. Follow-ups carry the earlier turns in
|
|
// `history`; the conversation lives on the caller's side.
|
|
// The template parses application/vnd.api+json only; the question comes as
|
|
// plain JSON, so the route parses it itself, like the chat route does.
|
|
app.post('/ask', express.json(), async (req, res) => {
|
|
const { question, history } = req.body || {};
|
|
if (!question) return res.status(400).json({ error: 'no question in the request body' });
|
|
if (!process.env.LLM_BASE_URL) return res.status(503).json({ error: 'no LLM configured (set LLM_BASE_URL)' });
|
|
|
|
try {
|
|
const profiles = await whenProfiles();
|
|
const turns = [
|
|
...(Array.isArray(history) ? history : []),
|
|
{ role: 'user', content: question },
|
|
];
|
|
const session = await buildSession();
|
|
const { text, trace } = await ask(turns, profiles, session);
|
|
res.json({ answer: text, toolCalls: trace });
|
|
} catch (e) {
|
|
console.error('[ask] failed:', e);
|
|
res.status(502).json({ error: stripStoreError(e.message) });
|
|
}
|
|
});
|