rapport spec as attachment

This commit is contained in:
felix
2026-09-26 16:54:32 +02:00
parent aec7dc457c
commit b1dad3c6e2
10 changed files with 267 additions and 131 deletions

View File

@@ -1,48 +1,51 @@
// What this service answers a chat turn with: the LLM loop with the
// conversation so far, and a report run becomes an interim message with a
// pending file.
// conversation so far. The service is synchronous per turn: when the answer
// is written, every bijlage already exists. The agreed spec goes on the
// proposal, and once a report runs, its CSV (registered by the runner as a
// real file) and the spec go on the same turn. The prompt never carries the
// Turtle.
import { ask } from './llm/loop.js';
import { storeSpecFile } from './chat/attachment-file.js';
import { SPEC_MEDIA_TYPE } from './chat/vocab.js';
const TEXT = {
making: 'Ik maak het rapport. Het verschijnt hier zodra het klaar is.',
failed: 'Het rapport is mislukt. Stel de vraag opnieuw, of anders geformuleerd.',
};
// The chat mounts before the profiles are in; whenProfiles waits for the load.
export function reportAssistant(whenProfiles) {
return async function answer(turn) {
// One pending file per turn: a second run_report overwrites it before the
// first end callback fires.
let pendingDocument = null;
// The file uri of the executed report, set by the end callback.
let fileUri = null;
// The last spec that checked out; stored as a file for this turn's
// final message.
let spec = null;
const session = {
query: turn.query,
update: turn.update,
async onReportStart({ fileName }) {
const said = await turn.say(TEXT.making, [{ name: fileName, mediaType: 'text/csv' }]);
pendingDocument = said.documents[0].uri;
onSpecValidated(validated) { spec = validated; },
async onReportStart() {
// Feedback while the run goes on; the report itself lands on the
// final message in bijlagen.
await turn.say(TEXT.making);
},
async onReportEnd(error, result) {
const documentUri = pendingDocument;
pendingDocument = null;
if (!documentUri) return;
if (error) {
await turn.drop(documentUri);
if (turn.finished) await turn.say(TEXT.failed);
return;
}
await turn.setUrl(documentUri, downloadHref(result.fileUri));
if (!error && result) fileUri = result.fileUri;
},
};
const profiles = await whenProfiles();
const { text } = await ask([...turn.history, { role: 'user', content: turn.content }], profiles, session);
return text;
// The bijlagen of this turn, all with a real file behind them: the
// executed report's CSV first, next the validated spec.
const attachments = [];
if (fileUri) attachments.push({ uri: fileUri });
if (spec) {
const stored = await storeSpecFile(turn.update, spec);
attachments.push({ ...stored, mediaType: SPEC_MEDIA_TYPE });
}
return { text, attachments };
};
}
// The file service serves /files/<uuid>/download; the logical file URI ends
// in that uuid.
function downloadHref(fileUri) {
return `/files/${fileUri.split('/').pop()}/download`;
}