Files
2026-09-27 17:17:34 +02:00

85 lines
3.5 KiB
JavaScript

import { app, errorHandler } from 'mu';
import bodyParser from 'body-parser';
import { loadProfiles } from './src/runner/profile.js';
import { readConversation, readHistory, writeMessage, deleteConversation } from './src/chat.js';
import { wantsExecution } from './src/llm/mode.js';
import { executeSpec, refineSpec } from './src/report-assistant.js';
const PROFILE_DIR = process.env.PROFILE_DIR || '/config/profiles';
// The agent that signs the assistant's messages, seeded by the portal's
// chat-assistant migration.
const CHAT_ASSISTANT_URI = process.env.CHAT_ASSISTANT_URI || 'http://data.lblod.info/id/chat-agents/rapportassistent';
const TITLE_LENGTH = 80;
const FAILED = 'Er ging iets mis. Probeer het opnieuw.';
// Profiles load at boot. A broken profile stops the service.
const profiles = loadProfiles(PROFILE_DIR);
// One chat turn. Records the question, answers 202, then writes the
// assistant's answer; the frontend polls the conversation for it. mu's
// query/update run as the caller for the whole turn, also after the 202.
// The template only parses application/vnd.api+json, the chat sends
// application/json.
app.post('/assistant/conversations/:id/turns', bodyParser.json(), async function(req, res) {
const content = String(req.body?.content ?? '').trim();
if (!content) return res.status(400).json({ error: 'content is required' });
let conversation, history, question;
try {
// the access check: the caller's session decides what is readable
conversation = await readConversation(req.params.id);
if (!conversation) return res.status(404).json({ error: 'no such conversation' });
history = await readHistory(conversation.uri, CHAT_ASSISTANT_URI);
question = await writeMessage({
conversationUri: conversation.uri,
content,
maker: conversation.creator,
title: conversation.title ? undefined : content.slice(0, TITLE_LENGTH),
});
} catch (e) {
console.error('[chat] could not record the question:', e);
return res.status(500).json({ error: e.message });
}
res.status(202).json({ id: question.id });
try {
const turns = [...history, { role: 'user', content }];
const answer = await wantsExecution(turns)
? await executeSpec(history, conversation.title, profiles)
: await refineSpec(turns, profiles);
await writeMessage({
conversationUri: conversation.uri,
content: answer.text,
maker: CHAT_ASSISTANT_URI,
attachments: answer.attachments,
});
} catch (e) {
console.error('[chat] turn failed:', e);
try {
await writeMessage({ conversationUri: conversation.uri, content: FAILED, maker: CHAT_ASSISTANT_URI });
} catch (e2) {
console.error('[chat] could not write the failure message:', e2);
}
}
});
// Delete a conversation with everything that hangs off it: the messages,
// their bijlagen (triples and share files) and the reports. The read is the
// access check: the caller's session decides what exists, so another user's
// conversation is a 404.
app.delete('/assistant/conversations/:id', async function(req, res) {
try {
const conversation = await readConversation(req.params.id);
if (!conversation) return res.status(404).json({ error: 'no such conversation' });
const removed = await deleteConversation(conversation.uri);
console.log(`[chat] deleted conversation ${req.params.id}, ${removed} share file(s) removed`);
res.status(204).send();
} catch (e) {
console.error('[chat] could not delete the conversation:', e);
res.status(500).json({ error: e.message });
}
});
app.use(errorHandler);