diff --git a/README.md b/README.md index c436692..36de7fa 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ nodes columns share, on every step; `rep:collect`) → CSV → register file ## The profile: a map of the data -A profile lists the kinds of things in the data (entities), what each one +A profile (using SHACL + some extensions) lists the kinds of things in the data (entities), what each one has (fields), and which fields lead to other entities (links). The app writes it; the service ships no profile and knows no vocabulary. @@ -285,6 +285,7 @@ The validator (`src/runner/check.js`) runs on every proposal and again before every run. It returns the first problem, in words the LLM can act on. On top of the rules above, it checks that: +- the Turtle holds one `rep:ReportSpec`, no more: one answer is one report; - every step of every path exists in the profile; - no path is longer than `MAX_PATH_DEPTH` steps; - a date or number test ends on a value field, not on a link; @@ -504,4 +505,4 @@ non-admin callers. Opening this up means adding the type to their group first. ## What it will not do No counting, no sorting, no top-N, no comparing two subjects, one class per -report, suggestions stop at the public graph. \ No newline at end of file +report, suggestions stop at the public graph. diff --git a/src/llm/loop.js b/src/llm/loop.js index 2ce38a7..adda16b 100644 --- a/src/llm/loop.js +++ b/src/llm/loop.js @@ -48,6 +48,9 @@ Rules: applies. Say that in Dutch in your proposal, so the user can check it. - Only offer what validate_spec accepted. Before you suggest an alternative, write it and validate it; if it does not validate, say it cannot be done. +- One answer carries one spec, and one spec is one report on one profile. If + the question needs two reports, say so, propose the first, and offer the + second as the next step. - You cannot count, sort, take the first N, or compare two subjects. Say so. - The report's title is the dct:title you write in the spec. There is no other title input. @@ -101,7 +104,13 @@ export async function ask(turns, profiles) { } // validate_spec answers "ok" (plus notes) when the spec checks out; // that spec becomes the bijlage of the answer. - if (call.function.name === 'validate_spec' && /^ok(\n|$)/.test(result)) spec = args.spec; + // One answer carries one spec: say so when this one replaces another. + if (call.function.name === 'validate_spec' && /^ok(\n|$)/.test(result)) { + if (spec && spec !== args.spec) { + result += '\nThis spec replaces the one you validated before. Your answer carries only this one.'; + } + spec = args.spec; + } messages.push({ role: 'tool', tool_call_id: call.id, content: result }); } } diff --git a/src/runner/spec.js b/src/runner/spec.js index a2a0bfc..51a2b18 100644 --- a/src/runner/spec.js +++ b/src/runner/spec.js @@ -13,8 +13,13 @@ export function parseSpec(turtle) { const quads = parser.parse(turtle); const store = new N3.Store(quads); - const specNode = store.getQuads(null, RDF + 'type', REP + 'ReportSpec')[0]?.subject; - if (!specNode) throw new Error('spec has no rep:ReportSpec'); + const specNodes = store.getQuads(null, RDF + 'type', REP + 'ReportSpec').map(q => q.subject); + if (!specNodes.length) throw new Error('spec has no rep:ReportSpec'); + // One spec is one report. A second one would pass unchecked and never run. + if (specNodes.length > 1) { + throw new Error(`one spec per answer. This Turtle has ${specNodes.length} rep:ReportSpec. Keep one, and offer the other as the next report.`); + } + const specNode = specNodes[0]; const one = (s, pred) => store.getQuads(s, pred, null)[0]?.object || null;