72 lines
3.2 KiB
JavaScript
72 lines
3.2 KiB
JavaScript
// What the assistant answers a chat turn with: { text, attachments }. When
|
|
// the answer is written, every bijlage already exists.
|
|
//
|
|
// Refine: the LLM proposes a spec; the last spec that checked out goes on
|
|
// the answer as a bijlage. The prompt never carries the Turtle.
|
|
//
|
|
// Execute: no LLM runs. The last spec bijlage in the conversation is the
|
|
// agreed spec; it is read from the share and fed straight to the runner.
|
|
// The answer is fixed text. When the run cannot happen or fails, the answer
|
|
// is a fixed failure message: nothing is retried and no spec is rewritten.
|
|
import { uuid } from 'mu';
|
|
import { ask } from './llm/loop.js';
|
|
import { parseSpec } from './runner/spec.js';
|
|
import { checkSpec } from './runner/check.js';
|
|
import { run, registerReport, slug } from './runner/run.js';
|
|
import { storeSpecFile, readSpecFile, writeShareFile, registerFile,
|
|
SPEC_MEDIA_TYPE, SPEC_TYPE, RESULT_TYPE } from './chat.js';
|
|
|
|
const TEXT = {
|
|
executed: 'Het rapport is uitgevoerd. De bijlagen staan erbij.',
|
|
noSpec: 'Er is nog geen voorstel om uit te voeren. Beschrijf eerst welk rapport je wilt.',
|
|
invalid: 'Het voorstel is niet meer geldig. Vraag een nieuw voorstel.',
|
|
failed: 'Het rapport kon niet uitgevoerd worden. Probeer het later opnieuw.',
|
|
noAnswer: 'Er kwam geen antwoord. Probeer het opnieuw.',
|
|
};
|
|
|
|
// An empty answer (a reasoning model can end with no text) gets a fixed
|
|
// text and no spec: only a spec the user saw explained may run.
|
|
export async function refineSpec(turns, profiles) {
|
|
const { text, spec } = await ask(turns, profiles);
|
|
if (!text.trim()) return { text: TEXT.noAnswer };
|
|
const attachments = spec ? [await storeSpecFile(spec)] : [];
|
|
return { text: text.trim(), attachments };
|
|
}
|
|
|
|
export async function executeSpec(history, conversationTitle, profiles) {
|
|
const attachment = lastSpecAttachment(history);
|
|
const content = attachment && readSpecFile(attachment.name);
|
|
if (!content) return { text: TEXT.noSpec };
|
|
|
|
try {
|
|
const spec = parseSpec(content);
|
|
const error = checkSpec(spec, profiles);
|
|
if (error) {
|
|
console.error(`[reports] the agreed spec is not valid: ${error}`);
|
|
return { text: TEXT.invalid };
|
|
}
|
|
|
|
// The conversation's title (named by the first question) names the
|
|
// file; the share name is unique, so a second run never overwrites the
|
|
// first. The report itself keeps the dct:title of the spec.
|
|
const shareName = `${uuid()}.csv`;
|
|
writeShareFile(shareName, await run(spec, profiles.get(spec.profileUri)));
|
|
const csvFile = await registerFile(shareName, 'text/csv', RESULT_TYPE, `${slug(conversationTitle)}.csv`);
|
|
await registerReport(spec.title, csvFile);
|
|
|
|
// Same physical spec file, registered as a bijlage for this message.
|
|
const specFile = await registerFile(attachment.name, SPEC_MEDIA_TYPE, SPEC_TYPE);
|
|
|
|
return { text: TEXT.executed, attachments: [csvFile, specFile] };
|
|
} catch (e) {
|
|
console.error('[reports] execution failed:', e);
|
|
return { text: TEXT.failed };
|
|
}
|
|
}
|
|
|
|
function lastSpecAttachment(history) {
|
|
const specs = history.flatMap(message => message.attachments)
|
|
.filter(a => a.mediaType === SPEC_MEDIA_TYPE && a.name?.startsWith('specificatie-'));
|
|
return specs.at(-1) || null;
|
|
}
|