refactor(disambiguation): own F1 clarification policy (#27)
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { createDisambiguationGate } = require("../disambiguation/index.js");
|
||||
|
||||
|
||||
function setupClarification({
|
||||
phase = 1,
|
||||
knownTypes = new Set(["concept_clarified", "ambiguity_open", "value_grounded"]),
|
||||
minimumPhases = {
|
||||
concept_clarified: 1,
|
||||
ambiguity_open: 1,
|
||||
value_grounded: 3,
|
||||
},
|
||||
} = {}) {
|
||||
const calls = [];
|
||||
const gate = createDisambiguationGate({
|
||||
workflow: {
|
||||
phase: (_ctx, session) => {
|
||||
calls.push(["phase", session]);
|
||||
return { number: phase };
|
||||
},
|
||||
describe: (_ctx, session) => {
|
||||
calls.push(["describe", session]);
|
||||
return "F1";
|
||||
},
|
||||
decisionTypes: () => {
|
||||
calls.push(["decisionTypes"]);
|
||||
return knownTypes;
|
||||
},
|
||||
decisionMinimumPhases: () => {
|
||||
calls.push(["decisionMinimumPhases"]);
|
||||
return minimumPhases;
|
||||
},
|
||||
advance: () => ({ advanced: false }),
|
||||
},
|
||||
session: { mutate: () => null },
|
||||
ledger: {
|
||||
record: () => null,
|
||||
},
|
||||
reviewer: {
|
||||
buildSelect: (input) => {
|
||||
calls.push(["build", input]);
|
||||
return { type: "ui_request", widget: "select", ...input };
|
||||
},
|
||||
isReserved: (label) => ["Altro", "Other"].includes(label),
|
||||
},
|
||||
toTextResult: (text) => ({ content: [{ type: "text", text }] }),
|
||||
});
|
||||
return { gate, calls };
|
||||
}
|
||||
|
||||
|
||||
const OPTIONS = [
|
||||
{
|
||||
id: "procedure",
|
||||
label: "Procedura clinica",
|
||||
recommended: true,
|
||||
decision: {
|
||||
type: "concept_clarified",
|
||||
subject: "ablazione",
|
||||
detail: "procedura clinica",
|
||||
rationale: "scelta dal reviewer",
|
||||
},
|
||||
},
|
||||
{ id: "ask", label: "Serve un altro chiarimento" },
|
||||
{ id: "other", label: "Altro" },
|
||||
];
|
||||
|
||||
|
||||
test("Disambiguation normalizes stringified F1 options", () => {
|
||||
const { gate } = setupClarification();
|
||||
|
||||
assert.deepEqual(
|
||||
gate.prepareClarificationArguments({ session: "s1", options: JSON.stringify(OPTIONS) }),
|
||||
{ session: "s1", options: OPTIONS },
|
||||
);
|
||||
assert.deepEqual(
|
||||
gate.prepareClarificationArguments({ session: "s1", options: "not-json" }),
|
||||
{ session: "s1", options: "not-json" },
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
test("Disambiguation validates and presents F1 clarification options", () => {
|
||||
const { gate, calls } = setupClarification();
|
||||
|
||||
const prepared = gate.prepareClarification({}, {
|
||||
session: "s1",
|
||||
title: "Che cosa significa ablazione?",
|
||||
intro: "Una sola interpretazione può essere corretta.",
|
||||
options: OPTIONS,
|
||||
advance: true,
|
||||
}, "u1");
|
||||
|
||||
assert.deepEqual(calls.map(([kind]) => kind), [
|
||||
"decisionTypes", "phase", "decisionMinimumPhases", "describe", "build",
|
||||
]);
|
||||
assert.deepEqual(prepared, {
|
||||
session: "s1",
|
||||
options: OPTIONS,
|
||||
advance: true,
|
||||
widget: {
|
||||
type: "ui_request",
|
||||
widget: "select",
|
||||
id: "u1",
|
||||
phase: "F1",
|
||||
title: "Che cosa significa ablazione?",
|
||||
intro: "Una sola interpretazione può essere corretta.",
|
||||
recommended: "procedure",
|
||||
options: [
|
||||
{ id: "procedure", label: "Procedura clinica" },
|
||||
{ id: "ask", label: "Serve un altro chiarimento" },
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
test("Disambiguation rejects an unknown F1 decision type before presentation", () => {
|
||||
const { gate, calls } = setupClarification();
|
||||
const invalidOptions = [{
|
||||
id: "invalid",
|
||||
label: "Invalida",
|
||||
decision: { type: "not_a_decision", subject: "x" },
|
||||
}];
|
||||
|
||||
const prepared = gate.prepareClarification({}, {
|
||||
session: "s1",
|
||||
title: "Domanda",
|
||||
options: invalidOptions,
|
||||
}, "u1");
|
||||
|
||||
assert.deepEqual(calls.map(([kind]) => kind), ["decisionTypes"]);
|
||||
assert.equal(
|
||||
prepared.result.content[0].text,
|
||||
"Tipo di decisione 'not_a_decision' non valido. Tipi ammessi: " +
|
||||
"concept_clarified, ambiguity_open, value_grounded. Correggi e riprova.",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
test("Disambiguation rejects a decision that belongs to a later phase", () => {
|
||||
const { gate, calls } = setupClarification();
|
||||
const futureOptions = [{
|
||||
id: "grounded",
|
||||
label: "Valore verificato",
|
||||
decision: { type: "value_grounded", subject: "stato" },
|
||||
}];
|
||||
|
||||
const prepared = gate.prepareClarification({}, {
|
||||
session: "s1",
|
||||
title: "Domanda",
|
||||
options: futureOptions,
|
||||
}, "u1");
|
||||
|
||||
assert.deepEqual(calls.map(([kind]) => kind), [
|
||||
"decisionTypes", "phase", "decisionMinimumPhases",
|
||||
]);
|
||||
assert.equal(
|
||||
prepared.result.content[0].text,
|
||||
"Tipo 'value_grounded' ammesso dalla Fase 3, sessione alla Fase 1. " +
|
||||
"Chiudi prima la fase corrente.",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
test("Disambiguation translates F1 choices into domain decisions or clarification requests", () => {
|
||||
const { gate } = setupClarification();
|
||||
const prepared = { options: OPTIONS };
|
||||
|
||||
const decision = gate.resolveClarification(prepared, { choices: ["procedure"] });
|
||||
assert.deepEqual(decision.decision, OPTIONS[0].decision);
|
||||
assert.match(decision.text, /Decisione registrata \(concept_clarified\): Procedura clinica\./);
|
||||
assert.match(decision.text, /tht session show/);
|
||||
|
||||
assert.deepEqual(
|
||||
gate.resolveClarification(prepared, { choices: ["ask"] }),
|
||||
{ text: "Scelta del reviewer: Serve un altro chiarimento" },
|
||||
);
|
||||
assert.deepEqual(
|
||||
gate.resolveClarification(prepared, { choices: ["missing"] }),
|
||||
{ text: "Scelta del reviewer: missing" },
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
test("Disambiguation never translates F1 control responses into decisions", () => {
|
||||
const { gate } = setupClarification();
|
||||
const prepared = { options: OPTIONS };
|
||||
|
||||
assert.deepEqual(
|
||||
gate.resolveClarification(prepared, { control: "freetext", text: "un altro significato" }),
|
||||
{ text: "Altro (reviewer): un altro significato" },
|
||||
);
|
||||
assert.deepEqual(
|
||||
gate.resolveClarification(prepared, { control: "back" }),
|
||||
{ text: "Il reviewer vuole tornare indietro." },
|
||||
);
|
||||
assert.deepEqual(
|
||||
gate.resolveClarification(prepared, { control: "exit" }),
|
||||
{ text: "Il reviewer vuole uscire." },
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user