refactor(confidence-engine): extract focused deconstruction seam
This commit is contained in:
@@ -1,9 +1,5 @@
|
||||
import { getProvider, getProviderModelName } from "@/lib/llm/provider";
|
||||
import {
|
||||
buildFocusedDeconstructPrompt,
|
||||
focusedDeconstructJsonSchema,
|
||||
validateFocusedDeconstructSchema,
|
||||
} from "@/lib/graph/focused-investigation";
|
||||
import { focusedDeconstruct } from "@/lib/graph/focused-investigation";
|
||||
import { withAuthenticatedApi } from "@/lib/supabase/api-auth.js";
|
||||
|
||||
async function post(request) {
|
||||
@@ -67,14 +63,6 @@ async function post(request) {
|
||||
}
|
||||
}
|
||||
|
||||
const prompt = buildFocusedDeconstructPrompt({
|
||||
targetLabel: body.targetLabel,
|
||||
targetDescription: body.targetDescription,
|
||||
centralStatement: body.centralStatement,
|
||||
question: body.question,
|
||||
answer: body.answer,
|
||||
});
|
||||
|
||||
const provider = getProvider();
|
||||
const modelName = getProviderModelName();
|
||||
startedAt = Date.now();
|
||||
@@ -84,36 +72,21 @@ async function post(request) {
|
||||
providerMode: process.env.CONFIDENCE_ENGINE_EXPERIMENT_PROVIDER ?? "ollama",
|
||||
startedAt,
|
||||
});
|
||||
const wrapper = await provider.generateReconstruction(
|
||||
prompt,
|
||||
|
||||
const result = await focusedDeconstruct({
|
||||
provider,
|
||||
modelName,
|
||||
focusedDeconstructJsonSchema,
|
||||
);
|
||||
targetNodeId,
|
||||
targetLabel: body.targetLabel,
|
||||
targetDescription: body.targetDescription,
|
||||
centralStatement: body.centralStatement,
|
||||
question: body.question,
|
||||
answer: body.answer,
|
||||
});
|
||||
|
||||
const elapsedMs = Date.now() - startedAt;
|
||||
|
||||
// Unwrap the semantic deconstruction from the provider envelope.
|
||||
const deconstruction = wrapper.response;
|
||||
console.info("[api/focused-investigation/deconstruct] provider success", {
|
||||
targetNodeId,
|
||||
elapsedMs,
|
||||
providerApiPath: wrapper.providerApiPath ?? null,
|
||||
responsePresent: Boolean(deconstruction),
|
||||
responseKeys: deconstruction && typeof deconstruction === "object"
|
||||
? Object.keys(deconstruction)
|
||||
: [],
|
||||
});
|
||||
|
||||
// Validate schema (required fields present, no graph-mutation fields)
|
||||
const validationErrors = validateFocusedDeconstructSchema(deconstruction);
|
||||
console.info("[api/focused-investigation/deconstruct] validation", {
|
||||
targetNodeId,
|
||||
schemaValid: validationErrors.length === 0,
|
||||
responseKeys: deconstruction && typeof deconstruction === "object"
|
||||
? Object.keys(deconstruction)
|
||||
: [],
|
||||
validationErrors,
|
||||
});
|
||||
if (validationErrors.length > 0) {
|
||||
if (!result.success) {
|
||||
console.info("[api/focused-investigation/deconstruct] end", {
|
||||
targetNodeId,
|
||||
status: 502,
|
||||
@@ -123,7 +96,7 @@ async function post(request) {
|
||||
{
|
||||
success: false,
|
||||
error: "Focused deconstruction result did not match expected schema",
|
||||
validationErrors,
|
||||
validationErrors: result.errors,
|
||||
targetNodeId: body.targetNodeId,
|
||||
elapsedMs,
|
||||
},
|
||||
@@ -134,11 +107,11 @@ async function post(request) {
|
||||
const response = Response.json({
|
||||
success: true,
|
||||
targetNodeId: body.targetNodeId,
|
||||
observations: deconstruction.observations,
|
||||
uncertainties: deconstruction.uncertainties,
|
||||
assumptions: deconstruction.assumptions,
|
||||
relationships: deconstruction.relationships,
|
||||
possibleFollowUpQuestions: deconstruction.possibleFollowUpQuestions,
|
||||
observations: result.observations,
|
||||
uncertainties: result.uncertainties,
|
||||
assumptions: result.assumptions,
|
||||
relationships: result.relationships,
|
||||
possibleFollowUpQuestions: result.possibleFollowUpQuestions,
|
||||
elapsedMs,
|
||||
});
|
||||
console.info("[api/focused-investigation/deconstruct] end", {
|
||||
|
||||
@@ -189,4 +189,55 @@ export function validateFocusedDeconstructSchema(result) {
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose a focused answer deconstruction invocation.
|
||||
*
|
||||
* Accepts explicit provider and modelName — does NOT call getProvider or getProviderModelName.
|
||||
* Calls buildFocusedDeconstructPrompt internally to produce the prompt.
|
||||
* Calls provider.generateReconstruction exactly once.
|
||||
* Validates result with validateFocusedDeconstructSchema.
|
||||
* Returns validated semantic payload on success, or { validation: false, errors } on schema failure.
|
||||
*/
|
||||
export function focusedDeconstruct({
|
||||
provider,
|
||||
modelName,
|
||||
targetNodeId,
|
||||
targetLabel,
|
||||
targetDescription,
|
||||
centralStatement,
|
||||
question,
|
||||
answer,
|
||||
}) {
|
||||
const prompt = buildFocusedDeconstructPrompt({
|
||||
targetLabel,
|
||||
targetDescription,
|
||||
centralStatement,
|
||||
question,
|
||||
answer,
|
||||
});
|
||||
|
||||
const wrapper = provider.generateReconstruction(
|
||||
prompt,
|
||||
modelName,
|
||||
focusedDeconstructJsonSchema,
|
||||
);
|
||||
|
||||
const deconstruction = wrapper.response;
|
||||
|
||||
const validationErrors = validateFocusedDeconstructSchema(deconstruction);
|
||||
if (validationErrors.length > 0) {
|
||||
return { success: false, validation: false, errors: validationErrors };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
targetNodeId,
|
||||
observations: deconstruction.observations,
|
||||
uncertainties: deconstruction.uncertainties,
|
||||
assumptions: deconstruction.assumptions,
|
||||
relationships: deconstruction.relationships,
|
||||
possibleFollowUpQuestions: deconstruction.possibleFollowUpQuestions,
|
||||
};
|
||||
}
|
||||
|
||||
export { FOCUSED_ANSWER_SCHEMA_FIELDS, FORBIDDEN_GRAPH_MUTATION_FIELDS };
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
focusedDeconstruct,
|
||||
buildFocusedDeconstructPrompt,
|
||||
validateFocusedDeconstructSchema,
|
||||
focusedDeconstructJsonSchema,
|
||||
} from "@/lib/graph/focused-investigation.js";
|
||||
|
||||
const FAKE_TARGET_NODE_ID = "test-node-001";
|
||||
const FAKE_LABEL = "Market demand signal";
|
||||
const FAKE_DESCRIPTION = "Evidence that customers want this.";
|
||||
const FAKE_CENTRAL = "Should we launch the product now?";
|
||||
const FAKE_QUESTION = "What evidence would clarify market demand?";
|
||||
const FAKE_ANSWER = "Some evidence suggests demand.";
|
||||
|
||||
function makeValidResponse() {
|
||||
return {
|
||||
targetNodeId: "model-returned-001",
|
||||
observations: ["Observation A"],
|
||||
uncertainties: ["Uncertainty B"],
|
||||
assumptions: ["Assumption C"],
|
||||
relationships: [{ from: "A", to: "B", type: "supports" }],
|
||||
possibleFollowUpQuestions: ["What next?"],
|
||||
};
|
||||
}
|
||||
|
||||
function makeInvalidResponse() {
|
||||
return { targetNodeId: "model-returned-001", observations: ["Obs"] };
|
||||
}
|
||||
|
||||
describe("focusedDeconstruct — success", () => {
|
||||
it("calls provider.generateReconstruction exactly once and returns success", () => {
|
||||
let callCount = 0;
|
||||
const fake = { generateReconstruction() { callCount++; return { response: makeValidResponse() }; } };
|
||||
const result = focusedDeconstruct({ provider: fake, modelName: "test-model", targetNodeId: FAKE_TARGET_NODE_ID, targetLabel: FAKE_LABEL, targetDescription: FAKE_DESCRIPTION, centralStatement: FAKE_CENTRAL, question: FAKE_QUESTION, answer: FAKE_ANSWER });
|
||||
expect(callCount).toBe(1);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("preserves observations from provider response", () => {
|
||||
const fake = { generateReconstruction() { return { response: makeValidResponse() }; } };
|
||||
const result = focusedDeconstruct({ provider: fake, modelName: "test-model", targetNodeId: FAKE_TARGET_NODE_ID, targetLabel: FAKE_LABEL, targetDescription: FAKE_DESCRIPTION, centralStatement: FAKE_CENTRAL, question: FAKE_QUESTION, answer: FAKE_ANSWER });
|
||||
expect(Array.isArray(result.observations)).toBe(true);
|
||||
expect(result.observations.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("preserves uncertainties from provider response", () => {
|
||||
const fake = { generateReconstruction() { return { response: makeValidResponse() }; } };
|
||||
const result = focusedDeconstruct({ provider: fake, modelName: "test-model", targetNodeId: FAKE_TARGET_NODE_ID, targetLabel: FAKE_LABEL, targetDescription: FAKE_DESCRIPTION, centralStatement: FAKE_CENTRAL, question: FAKE_QUESTION, answer: FAKE_ANSWER });
|
||||
expect(Array.isArray(result.uncertainties)).toBe(true);
|
||||
expect(result.uncertainties.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("preserves assumptions from provider response", () => {
|
||||
const fake = { generateReconstruction() { return { response: makeValidResponse() }; } };
|
||||
const result = focusedDeconstruct({ provider: fake, modelName: "test-model", targetNodeId: FAKE_TARGET_NODE_ID, targetLabel: FAKE_LABEL, targetDescription: FAKE_DESCRIPTION, centralStatement: FAKE_CENTRAL, question: FAKE_QUESTION, answer: FAKE_ANSWER });
|
||||
expect(Array.isArray(result.assumptions)).toBe(true);
|
||||
expect(result.assumptions.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("preserves relationships from provider response", () => {
|
||||
const fake = { generateReconstruction() { return { response: makeValidResponse() }; } };
|
||||
const result = focusedDeconstruct({ provider: fake, modelName: "test-model", targetNodeId: FAKE_TARGET_NODE_ID, targetLabel: FAKE_LABEL, targetDescription: FAKE_DESCRIPTION, centralStatement: FAKE_CENTRAL, question: FAKE_QUESTION, answer: FAKE_ANSWER });
|
||||
expect(Array.isArray(result.relationships)).toBe(true);
|
||||
expect(result.relationships.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("preserves possibleFollowUpQuestions from provider response", () => {
|
||||
const fake = { generateReconstruction() { return { response: makeValidResponse() }; } };
|
||||
const result = focusedDeconstruct({ provider: fake, modelName: "test-model", targetNodeId: FAKE_TARGET_NODE_ID, targetLabel: FAKE_LABEL, targetDescription: FAKE_DESCRIPTION, centralStatement: FAKE_CENTRAL, question: FAKE_QUESTION, answer: FAKE_ANSWER });
|
||||
expect(Array.isArray(result.possibleFollowUpQuestions)).toBe(true);
|
||||
expect(result.possibleFollowUpQuestions.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("preserves caller target identity (result.targetNodeId === request targetNodeId)", () => {
|
||||
const fake = { generateReconstruction() { return { response: makeValidResponse() }; } };
|
||||
const result = focusedDeconstruct({ provider: fake, modelName: "test-model", targetNodeId: FAKE_TARGET_NODE_ID, targetLabel: FAKE_LABEL, targetDescription: FAKE_DESCRIPTION, centralStatement: FAKE_CENTRAL, question: FAKE_QUESTION, answer: FAKE_ANSWER });
|
||||
expect(result.targetNodeId).toBe(FAKE_TARGET_NODE_ID);
|
||||
});
|
||||
|
||||
it("passes correct prompt to provider", () => {
|
||||
let capturedPrompt = null;
|
||||
const fake = { generateReconstruction(prompt) { capturedPrompt = prompt; return { response: makeValidResponse() }; } };
|
||||
focusedDeconstruct({ provider: fake, modelName: "test-model", targetNodeId: FAKE_TARGET_NODE_ID, targetLabel: FAKE_LABEL, targetDescription: FAKE_DESCRIPTION, centralStatement: FAKE_CENTRAL, question: FAKE_QUESTION, answer: FAKE_ANSWER });
|
||||
const expected = buildFocusedDeconstructPrompt({ targetLabel: FAKE_LABEL, targetDescription: FAKE_DESCRIPTION, centralStatement: FAKE_CENTRAL, question: FAKE_QUESTION, answer: FAKE_ANSWER });
|
||||
expect(capturedPrompt).toBe(expected);
|
||||
});
|
||||
|
||||
it("passes modelName unchanged to provider", () => {
|
||||
let capturedModel = null;
|
||||
const fake = { generateReconstruction(prompt, receivedModel) { capturedModel = receivedModel; return { response: makeValidResponse() }; } };
|
||||
focusedDeconstruct({ provider: fake, modelName: "my-custom-model-v2", targetNodeId: FAKE_TARGET_NODE_ID, targetLabel: FAKE_LABEL, targetDescription: FAKE_DESCRIPTION, centralStatement: FAKE_CENTRAL, question: FAKE_QUESTION, answer: FAKE_ANSWER });
|
||||
expect(capturedModel).toBe("my-custom-model-v2");
|
||||
});
|
||||
|
||||
it("passes focusedDeconstructJsonSchema as the schema argument", () => {
|
||||
let capturedSchema = null;
|
||||
const fake = { generateReconstruction(prompt, modelName, schema) { capturedSchema = schema; return { response: makeValidResponse() }; } };
|
||||
focusedDeconstruct({ provider: fake, modelName: "test-model", targetNodeId: FAKE_TARGET_NODE_ID, targetLabel: FAKE_LABEL, targetDescription: FAKE_DESCRIPTION, centralStatement: FAKE_CENTRAL, question: FAKE_QUESTION, answer: FAKE_ANSWER });
|
||||
expect(capturedSchema).toBe(focusedDeconstructJsonSchema);
|
||||
});
|
||||
})
|
||||
|
||||
describe("focusedDeconstruct — validation failure", () => {
|
||||
it("returns failure result when provider response fails canonical validation", () => {
|
||||
const fake = { generateReconstruction() { return { response: makeInvalidResponse() }; } };
|
||||
const result = focusedDeconstruct({ provider: fake, modelName: "test-model", targetNodeId: FAKE_TARGET_NODE_ID, targetLabel: FAKE_LABEL, targetDescription: FAKE_DESCRIPTION, centralStatement: FAKE_CENTRAL, question: FAKE_QUESTION, answer: FAKE_ANSWER });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("validation errors remain available on failure result", () => {
|
||||
const fake = { generateReconstruction() { return { response: makeInvalidResponse() }; } };
|
||||
const result = focusedDeconstruct({ provider: fake, modelName: "test-model", targetNodeId: FAKE_TARGET_NODE_ID, targetLabel: FAKE_LABEL, targetDescription: FAKE_DESCRIPTION, centralStatement: FAKE_CENTRAL, question: FAKE_QUESTION, answer: FAKE_ANSWER });
|
||||
expect(Array.isArray(result.errors)).toBe(true);
|
||||
expect(result.errors.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("no second provider call occurs on validation failure", () => {
|
||||
let callCount = 0;
|
||||
const fake = { generateReconstruction() { callCount++; return { response: makeInvalidResponse() }; } };
|
||||
focusedDeconstruct({ provider: fake, modelName: "test-model", targetNodeId: FAKE_TARGET_NODE_ID, targetLabel: FAKE_LABEL, targetDescription: FAKE_DESCRIPTION, centralStatement: FAKE_CENTRAL, question: FAKE_QUESTION, answer: FAKE_ANSWER });
|
||||
expect(callCount).toBe(1);
|
||||
});
|
||||
})
|
||||
|
||||
describe("focusedDeconstruct — provider failure", () => {
|
||||
it("propagates error with original identity/metadata intact", () => {
|
||||
const fake = {
|
||||
generateReconstruction() {
|
||||
const err = new Error("Connection refused to ollama endpoint");
|
||||
err.name = "ProviderError";
|
||||
err.providerApiPath = "/api/v1/completions";
|
||||
err.code = "PROVIDER_UNAVAILABLE";
|
||||
throw err;
|
||||
},
|
||||
};
|
||||
expect(() => focusedDeconstruct({ provider: fake, modelName: "test-model", targetNodeId: FAKE_TARGET_NODE_ID, targetLabel: FAKE_LABEL, targetDescription: FAKE_DESCRIPTION, centralStatement: FAKE_CENTRAL, question: FAKE_QUESTION, answer: FAKE_ANSWER })).toThrow("Connection refused to ollama endpoint");
|
||||
});
|
||||
})
|
||||
Reference in New Issue
Block a user