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.
|
||||
- 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
|
||||
|
||||
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 {object} [opts]
|
||||
* @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
|
||||
*/
|
||||
export async function analyseScenario(scenario, opts = {}) {
|
||||
@@ -70,14 +72,15 @@ export async function analyseScenario(scenario, opts = {}) {
|
||||
}
|
||||
|
||||
// ── Call provider ──────────────────────────────────
|
||||
const provider = getProvider();
|
||||
const provider = opts.reconstructionProvider ?? getProvider();
|
||||
const reconstructionModelName = opts.reconstructionModelName ?? OLLAMA_MODEL;
|
||||
let rawResponse;
|
||||
let providerApiPath;
|
||||
let providerExecution;
|
||||
try {
|
||||
const providerResult = await provider.generateReconstruction(
|
||||
promptObj.prompt,
|
||||
OLLAMA_MODEL,
|
||||
reconstructionModelName,
|
||||
);
|
||||
if (
|
||||
providerResult &&
|
||||
@@ -122,7 +125,7 @@ export async function analyseScenario(scenario, opts = {}) {
|
||||
if (resultV2.valid) {
|
||||
return buildSuccessResultV2(
|
||||
resultV2.data,
|
||||
OLLAMA_MODEL,
|
||||
reconstructionModelName,
|
||||
duration,
|
||||
promptVersion,
|
||||
compatibility,
|
||||
@@ -137,7 +140,7 @@ export async function analyseScenario(scenario, opts = {}) {
|
||||
if (resultV1.valid) {
|
||||
return buildSuccessResultV1(
|
||||
resultV1.data,
|
||||
OLLAMA_MODEL,
|
||||
reconstructionModelName,
|
||||
duration,
|
||||
promptVersion,
|
||||
compatibility,
|
||||
@@ -148,7 +151,7 @@ export async function analyseScenario(scenario, opts = {}) {
|
||||
return buildPartialResult(
|
||||
rawResponseStr,
|
||||
resultV2.error ?? resultV1.error,
|
||||
OLLAMA_MODEL,
|
||||
reconstructionModelName,
|
||||
duration,
|
||||
promptVersion,
|
||||
compatibility,
|
||||
|
||||
@@ -359,7 +359,14 @@ export async function startCase(body, dependencies = {}) {
|
||||
}
|
||||
|
||||
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) {
|
||||
return {
|
||||
|
||||
@@ -53,7 +53,7 @@ function readScenarioInput(argv) {
|
||||
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 ──
|
||||
const hadEnvVar = process.env.RECONSTRUCTION_EXPERIMENT_INSTRUCTION != null;
|
||||
const previousEnvValue = process.env.RECONSTRUCTION_EXPERIMENT_INSTRUCTION;
|
||||
@@ -61,10 +61,10 @@ async function runStartCaseExperiment(scenarioInput, experimentInstruction) {
|
||||
process.env.RECONSTRUCTION_EXPERIMENT_INSTRUCTION = experimentInstruction;
|
||||
}
|
||||
|
||||
let startCase;
|
||||
let startCase = options.startCase;
|
||||
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.
|
||||
startCase = async (body) => {
|
||||
// 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: [] } },
|
||||
};
|
||||
};
|
||||
} else {
|
||||
} else if (!startCase) {
|
||||
const baseUrl = assertRequiredEnv("OLLAMA_BASE_URL");
|
||||
const model = assertRequiredEnv("OLLAMA_MODEL");
|
||||
|
||||
@@ -99,7 +99,10 @@ async function runStartCaseExperiment(scenarioInput, experimentInstruction) {
|
||||
let result;
|
||||
const endToEndStartedAt = Date.now();
|
||||
try {
|
||||
result = await startCase(scenarioInput);
|
||||
result = await startCase(scenarioInput, {
|
||||
reconstructionProvider: options.reconstructionProvider,
|
||||
reconstructionModelName: options.reconstructionModelName,
|
||||
});
|
||||
} finally {
|
||||
// Clean up experiment env var after execution regardless of outcome
|
||||
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.
|
||||
// Used only for deterministic apparatus verification of the import path.
|
||||
if (process.env.START_CASE_EXPERIMENT_HELPER_IMPORT_ONLY === "1") {
|
||||
@@ -183,4 +187,9 @@ async function runStartCaseExperiment(scenarioInput, experimentInstruction) {
|
||||
console.log(JSON.stringify(failure));
|
||||
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 () => {
|
||||
const { startCase } = await import("@/lib/graph/orchestrator.js");
|
||||
|
||||
|
||||
@@ -112,6 +112,35 @@ describe("normaliseAnalysisResponse", () => {
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const providerError = new Error("Provider failed");
|
||||
providerError.providerApiPath = "/api/chat";
|
||||
|
||||
@@ -14,10 +14,12 @@ import { execFile } from "child_process";
|
||||
import { writeFile, unlink } from "fs/promises";
|
||||
import { join, dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { createRequire } from "module";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = join(__dirname, "..", "..");
|
||||
const helperPath = join(rootDir, "scripts", "start-case-experiment-helper.cjs");
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
function runHelper(args = [], envOverrides = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -128,6 +130,34 @@ describe("start-case-experiment-helper.cjs apparatus", () => {
|
||||
// ── 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 () => {
|
||||
const result = await runHelper(["Success test scenario"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
||||
Reference in New Issue
Block a user