experiment(confidence-engine): expose reconstruction provider seam
This commit is contained in:
@@ -122,6 +122,12 @@
|
|||||||
- An experiment-only OpenAI Responses API provider apparatus exists for `gpt-5.6-terra`, using the same current v0.5 prompt and canonical reconstruction schema. Production provider selection remains Ollama/Qwen.
|
- An experiment-only OpenAI Responses API provider apparatus exists for `gpt-5.6-terra`, using the same current v0.5 prompt and canonical reconstruction schema. Production provider selection remains Ollama/Qwen.
|
||||||
- No live OpenAI calls have occurred. Next boundary: a bounded live semantic comparison, not production migration.
|
- No live OpenAI calls have occurred. Next boundary: a bounded live semantic comparison, not production migration.
|
||||||
|
|
||||||
|
## Reconstruction provider injection seam
|
||||||
|
|
||||||
|
- OpenAI provider apparatus exists at `860ee6f`; the canonical helper was previously blocked because `startCase()` and `analyseScenario()` created providers internally.
|
||||||
|
- An optional reconstruction-provider injection seam now carries an experiment provider through the same production analysis path. Production callers still default to configured Ollama/Qwen through `getProvider()`.
|
||||||
|
- No live calls occurred. Next boundary: exactly one live OpenAI fixed-scenario run through the canonical helper; this is not a production provider migration.
|
||||||
|
|
||||||
## Current product architecture
|
## Current product architecture
|
||||||
|
|
||||||
Three distinct routes, not a single page:
|
Three distinct routes, not a single page:
|
||||||
|
|||||||
+8
-5
@@ -23,6 +23,8 @@ const MAX_SCENARIO_LENGTH = 10000;
|
|||||||
* @param {string} scenario - The scenario text to analyse
|
* @param {string} scenario - The scenario text to analyse
|
||||||
* @param {object} [opts]
|
* @param {object} [opts]
|
||||||
* @param {"v0.1" | "v0.2"} [opts.promptVersion="v0.2"] - Prompt version to use
|
* @param {"v0.1" | "v0.2"} [opts.promptVersion="v0.2"] - Prompt version to use
|
||||||
|
* @param {{ generateReconstruction: Function }} [opts.reconstructionProvider] - Experiment-only reconstruction provider override
|
||||||
|
* @param {string} [opts.reconstructionModelName] - Experiment-only model override for an injected provider
|
||||||
* @returns {Promise<object>} Analysis result with diagnostics
|
* @returns {Promise<object>} Analysis result with diagnostics
|
||||||
*/
|
*/
|
||||||
export async function analyseScenario(scenario, opts = {}) {
|
export async function analyseScenario(scenario, opts = {}) {
|
||||||
@@ -70,14 +72,15 @@ export async function analyseScenario(scenario, opts = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Call provider ──────────────────────────────────
|
// ── Call provider ──────────────────────────────────
|
||||||
const provider = getProvider();
|
const provider = opts.reconstructionProvider ?? getProvider();
|
||||||
|
const reconstructionModelName = opts.reconstructionModelName ?? OLLAMA_MODEL;
|
||||||
let rawResponse;
|
let rawResponse;
|
||||||
let providerApiPath;
|
let providerApiPath;
|
||||||
let providerExecution;
|
let providerExecution;
|
||||||
try {
|
try {
|
||||||
const providerResult = await provider.generateReconstruction(
|
const providerResult = await provider.generateReconstruction(
|
||||||
promptObj.prompt,
|
promptObj.prompt,
|
||||||
OLLAMA_MODEL,
|
reconstructionModelName,
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
providerResult &&
|
providerResult &&
|
||||||
@@ -122,7 +125,7 @@ export async function analyseScenario(scenario, opts = {}) {
|
|||||||
if (resultV2.valid) {
|
if (resultV2.valid) {
|
||||||
return buildSuccessResultV2(
|
return buildSuccessResultV2(
|
||||||
resultV2.data,
|
resultV2.data,
|
||||||
OLLAMA_MODEL,
|
reconstructionModelName,
|
||||||
duration,
|
duration,
|
||||||
promptVersion,
|
promptVersion,
|
||||||
compatibility,
|
compatibility,
|
||||||
@@ -137,7 +140,7 @@ export async function analyseScenario(scenario, opts = {}) {
|
|||||||
if (resultV1.valid) {
|
if (resultV1.valid) {
|
||||||
return buildSuccessResultV1(
|
return buildSuccessResultV1(
|
||||||
resultV1.data,
|
resultV1.data,
|
||||||
OLLAMA_MODEL,
|
reconstructionModelName,
|
||||||
duration,
|
duration,
|
||||||
promptVersion,
|
promptVersion,
|
||||||
compatibility,
|
compatibility,
|
||||||
@@ -148,7 +151,7 @@ export async function analyseScenario(scenario, opts = {}) {
|
|||||||
return buildPartialResult(
|
return buildPartialResult(
|
||||||
rawResponseStr,
|
rawResponseStr,
|
||||||
resultV2.error ?? resultV1.error,
|
resultV2.error ?? resultV1.error,
|
||||||
OLLAMA_MODEL,
|
reconstructionModelName,
|
||||||
duration,
|
duration,
|
||||||
promptVersion,
|
promptVersion,
|
||||||
compatibility,
|
compatibility,
|
||||||
|
|||||||
@@ -359,7 +359,14 @@ export async function startCase(body, dependencies = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { scenario, promptVersion } = parsedRequest.data;
|
const { scenario, promptVersion } = parsedRequest.data;
|
||||||
const analysis = await analyseScenario(scenario, { promptVersion });
|
const analysisOptions = { promptVersion };
|
||||||
|
if (dependencies.reconstructionProvider) {
|
||||||
|
analysisOptions.reconstructionProvider = dependencies.reconstructionProvider;
|
||||||
|
}
|
||||||
|
if (dependencies.reconstructionModelName) {
|
||||||
|
analysisOptions.reconstructionModelName = dependencies.reconstructionModelName;
|
||||||
|
}
|
||||||
|
const analysis = await analyseScenario(scenario, analysisOptions);
|
||||||
|
|
||||||
if (!analysis.success) {
|
if (!analysis.success) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ function readScenarioInput(argv) {
|
|||||||
return { scenario: argv.slice(startIdx).join(" ") };
|
return { scenario: argv.slice(startIdx).join(" ") };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runStartCaseExperiment(scenarioInput, experimentInstruction) {
|
async function runStartCaseExperiment(scenarioInput, experimentInstruction, options = {}) {
|
||||||
// ── Experiment seam bridge: supply instruction to production path ──
|
// ── Experiment seam bridge: supply instruction to production path ──
|
||||||
const hadEnvVar = process.env.RECONSTRUCTION_EXPERIMENT_INSTRUCTION != null;
|
const hadEnvVar = process.env.RECONSTRUCTION_EXPERIMENT_INSTRUCTION != null;
|
||||||
const previousEnvValue = process.env.RECONSTRUCTION_EXPERIMENT_INSTRUCTION;
|
const previousEnvValue = process.env.RECONSTRUCTION_EXPERIMENT_INSTRUCTION;
|
||||||
@@ -61,10 +61,10 @@ async function runStartCaseExperiment(scenarioInput, experimentInstruction) {
|
|||||||
process.env.RECONSTRUCTION_EXPERIMENT_INSTRUCTION = experimentInstruction;
|
process.env.RECONSTRUCTION_EXPERIMENT_INSTRUCTION = experimentInstruction;
|
||||||
}
|
}
|
||||||
|
|
||||||
let startCase;
|
let startCase = options.startCase;
|
||||||
const isMock = process.env.START_CASE_EXPERIMENT_HELPER_MOCK === "1";
|
const isMock = process.env.START_CASE_EXPERIMENT_HELPER_MOCK === "1";
|
||||||
|
|
||||||
if (isMock) {
|
if (!startCase && isMock) {
|
||||||
// Deterministic mode: skip environment checks and use inline test double.
|
// Deterministic mode: skip environment checks and use inline test double.
|
||||||
startCase = async (body) => {
|
startCase = async (body) => {
|
||||||
// Deterministic inline test double — no live model calls.
|
// Deterministic inline test double — no live model calls.
|
||||||
@@ -81,7 +81,7 @@ async function runStartCaseExperiment(scenarioInput, experimentInstruction) {
|
|||||||
diagnostics: { validationStatus: "mock", modelName: "inline-test-double", graphReferenceValidation: { valid: true, errors: [] } },
|
diagnostics: { validationStatus: "mock", modelName: "inline-test-double", graphReferenceValidation: { valid: true, errors: [] } },
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
} else {
|
} else if (!startCase) {
|
||||||
const baseUrl = assertRequiredEnv("OLLAMA_BASE_URL");
|
const baseUrl = assertRequiredEnv("OLLAMA_BASE_URL");
|
||||||
const model = assertRequiredEnv("OLLAMA_MODEL");
|
const model = assertRequiredEnv("OLLAMA_MODEL");
|
||||||
|
|
||||||
@@ -99,7 +99,10 @@ async function runStartCaseExperiment(scenarioInput, experimentInstruction) {
|
|||||||
let result;
|
let result;
|
||||||
const endToEndStartedAt = Date.now();
|
const endToEndStartedAt = Date.now();
|
||||||
try {
|
try {
|
||||||
result = await startCase(scenarioInput);
|
result = await startCase(scenarioInput, {
|
||||||
|
reconstructionProvider: options.reconstructionProvider,
|
||||||
|
reconstructionModelName: options.reconstructionModelName,
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
// Clean up experiment env var after execution regardless of outcome
|
// Clean up experiment env var after execution regardless of outcome
|
||||||
if (experimentInstruction && !hadEnvVar) {
|
if (experimentInstruction && !hadEnvVar) {
|
||||||
@@ -132,7 +135,8 @@ async function runStartCaseExperiment(scenarioInput, experimentInstruction) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
(async () => {
|
if (require.main === module) {
|
||||||
|
(async () => {
|
||||||
// Import-only mode: prove real production seam without invoking startCase.
|
// Import-only mode: prove real production seam without invoking startCase.
|
||||||
// Used only for deterministic apparatus verification of the import path.
|
// Used only for deterministic apparatus verification of the import path.
|
||||||
if (process.env.START_CASE_EXPERIMENT_HELPER_IMPORT_ONLY === "1") {
|
if (process.env.START_CASE_EXPERIMENT_HELPER_IMPORT_ONLY === "1") {
|
||||||
@@ -183,4 +187,9 @@ async function runStartCaseExperiment(scenarioInput, experimentInstruction) {
|
|||||||
console.log(JSON.stringify(failure));
|
console.log(JSON.stringify(failure));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { runStartCaseExperiment };
|
||||||
|
|
||||||
|
module.exports = { runStartCaseExperiment };
|
||||||
|
|||||||
@@ -356,6 +356,24 @@ describe("lib/graph/orchestrator startCase", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("forwards an injected reconstruction provider to analyseScenario", async () => {
|
||||||
|
mockAnalyseScenario.mockResolvedValue(makeAnalysisResult());
|
||||||
|
const reconstructionProvider = { generateReconstruction: vi.fn() };
|
||||||
|
const { startCase } = await import("@/lib/graph/orchestrator.js");
|
||||||
|
|
||||||
|
const result = await startCase(
|
||||||
|
{ scenario: "Scenario text" },
|
||||||
|
{ reconstructionProvider, reconstructionModelName: "experiment-model" },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(mockAnalyseScenario).toHaveBeenCalledWith("Scenario text", {
|
||||||
|
promptVersion: undefined,
|
||||||
|
reconstructionProvider,
|
||||||
|
reconstructionModelName: "experiment-model",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects invalid request input without throwing", async () => {
|
it("rejects invalid request input without throwing", async () => {
|
||||||
const { startCase } = await import("@/lib/graph/orchestrator.js");
|
const { startCase } = await import("@/lib/graph/orchestrator.js");
|
||||||
|
|
||||||
|
|||||||
@@ -112,6 +112,35 @@ describe("normaliseAnalysisResponse", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("analyseScenario compatibility", () => {
|
describe("analyseScenario compatibility", () => {
|
||||||
|
it("uses an injected reconstruction provider with the production prompt path", async () => {
|
||||||
|
const injectedProvider = {
|
||||||
|
generateReconstruction: vi.fn().mockResolvedValue({
|
||||||
|
inputClassification: { primaryType: "other", classificationReason: "test", confidence: "low" },
|
||||||
|
reconstruction: {
|
||||||
|
summary: "summary", actors: [], systemsOrObjects: [], expectedStates: [], observedStates: [],
|
||||||
|
differences: [], knownTransitions: [], unexplainedTransitions: [], contradictions: [],
|
||||||
|
importantUnknowns: [], plausibleInterpretations: [],
|
||||||
|
},
|
||||||
|
evidence: [],
|
||||||
|
nextQuestion: { id: "q1", question: "What next?", targets: [], reason: "test", expectedInformationValue: "low" },
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const { analyseScenario } = await import("@/lib/analysis.js");
|
||||||
|
|
||||||
|
const result = await analyseScenario("Scenario text", {
|
||||||
|
promptVersion: "v0.3",
|
||||||
|
reconstructionProvider: injectedProvider,
|
||||||
|
reconstructionModelName: "experiment-model",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(injectedProvider.generateReconstruction).toHaveBeenCalledWith(
|
||||||
|
"prompt",
|
||||||
|
"experiment-model",
|
||||||
|
);
|
||||||
|
expect(mockGenerateReconstruction).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("preserves an attempted provider API path on provider failure", async () => {
|
it("preserves an attempted provider API path on provider failure", async () => {
|
||||||
const providerError = new Error("Provider failed");
|
const providerError = new Error("Provider failed");
|
||||||
providerError.providerApiPath = "/api/chat";
|
providerError.providerApiPath = "/api/chat";
|
||||||
|
|||||||
@@ -14,10 +14,12 @@ import { execFile } from "child_process";
|
|||||||
import { writeFile, unlink } from "fs/promises";
|
import { writeFile, unlink } from "fs/promises";
|
||||||
import { join, dirname } from "path";
|
import { join, dirname } from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
|
import { createRequire } from "module";
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const rootDir = join(__dirname, "..", "..");
|
const rootDir = join(__dirname, "..", "..");
|
||||||
const helperPath = join(rootDir, "scripts", "start-case-experiment-helper.cjs");
|
const helperPath = join(rootDir, "scripts", "start-case-experiment-helper.cjs");
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
|
||||||
function runHelper(args = [], envOverrides = {}) {
|
function runHelper(args = [], envOverrides = {}) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -128,6 +130,34 @@ describe("start-case-experiment-helper.cjs apparatus", () => {
|
|||||||
// ── D — Success output ──────────────────────────────────────────
|
// ── D — Success output ──────────────────────────────────────────
|
||||||
|
|
||||||
describe("D — success output", () => {
|
describe("D — success output", () => {
|
||||||
|
it("forwards an explicit reconstruction provider through the startCase path", async () => {
|
||||||
|
const { runStartCaseExperiment } = require(helperPath);
|
||||||
|
const reconstructionProvider = { generateReconstruction() {} };
|
||||||
|
const startCase = async (input, dependencies) => {
|
||||||
|
expect(input).toEqual({ scenario: "Provider seam scenario" });
|
||||||
|
expect(dependencies).toMatchObject({
|
||||||
|
reconstructionProvider,
|
||||||
|
reconstructionModelName: "gpt-5.6-terra",
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
situationGraph: { nodes: [], edges: [] },
|
||||||
|
assessment: null,
|
||||||
|
selectedQuestion: null,
|
||||||
|
summary: "test",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await runStartCaseExperiment(
|
||||||
|
{ scenario: "Provider seam scenario" },
|
||||||
|
null,
|
||||||
|
{ reconstructionProvider, reconstructionModelName: "gpt-5.6-terra", startCase },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.summary).toBe("test");
|
||||||
|
});
|
||||||
|
|
||||||
it("exit code is 0", async () => {
|
it("exit code is 0", async () => {
|
||||||
const result = await runHelper(["Success test scenario"]);
|
const result = await runHelper(["Success test scenario"]);
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
|
|||||||
Reference in New Issue
Block a user