535 lines
22 KiB
JavaScript
535 lines
22 KiB
JavaScript
/**
|
|
* Reusable live-focused-deconstruction experiment helper.
|
|
*
|
|
* Makes one real focused-deconstruction call using the same production
|
|
* semantic operation as /api/focused-investigation/deconstruct:
|
|
*
|
|
* buildFocusedDeconstructPrompt -> provider.generateReconstruction -> validateFocusedDeconstructSchema
|
|
*
|
|
* Inputs are the minimum fields required by buildFocusedDeconstructPrompt.
|
|
* Relies on environment variables OLLAMA_BASE_URL and OLLAMA_MODEL being set.
|
|
*/
|
|
|
|
import dotenv from "dotenv";
|
|
import { buildFocusedDeconstructPrompt, validateFocusedDeconstructSchema } from "@/lib/graph/focused-investigation.js";
|
|
import { getProvider } from "@/lib/llm/provider.js";
|
|
|
|
// Load configured OLLAMA_MODEL / OLLAMA_BASE_URL (same mechanism as all known-good live tests)
|
|
dotenv.config({ path: ".env.local" });
|
|
|
|
// ── shared execution seam (extracted from existing body) ────────────────
|
|
|
|
/**
|
|
* Execute a focused-deconstruct prompt through the canonical live machinery.
|
|
*
|
|
* Caller supplies: prompt, targetNodeId, optional injected provider.
|
|
* This function owns: provider resolution, model invocation, timing, validation, result construction.
|
|
*/
|
|
async function _executeFocusedPrompt({ prompt, targetNodeId, provider }) {
|
|
const actualProvider = provider ?? getProvider();
|
|
|
|
const ollamaModel = process.env.OLLAMA_MODEL;
|
|
if (!ollamaModel) throw new Error("OLLAMA_MODEL is not set in environment");
|
|
|
|
const startedAt = Date.now();
|
|
const raw = await actualProvider.generateReconstruction(prompt, ollamaModel);
|
|
const elapsedMs = Date.now() - startedAt;
|
|
|
|
const validationErrors = validateFocusedDeconstructSchema(raw);
|
|
if (validationErrors.length > 0) {
|
|
throw new Error(
|
|
"Focused deconstruction result did not match expected schema:\n" +
|
|
validationErrors.map((e) => " - " + e).join("\n")
|
|
);
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
targetNodeId,
|
|
observations: raw.observations,
|
|
uncertainties: raw.uncertainties,
|
|
assumptions: raw.assumptions,
|
|
relationships: raw.relationships,
|
|
possibleFollowUpQuestions: raw.possibleFollowUpQuestions,
|
|
elapsedMs,
|
|
};
|
|
}
|
|
|
|
// ── existing API (preserved — unchanged behaviour) ──────────────────────
|
|
|
|
/**
|
|
* Run one live focused-deconstruction experiment.
|
|
*
|
|
* @param {object} params
|
|
* @param {string} params.targetNodeId - the target node ID under investigation
|
|
* @param {string} params.targetLabel - label of the target node
|
|
* @param {string} params.targetDescription - description of the target node
|
|
* @param {string} params.centralStatement - the case's central statement
|
|
* @param {string} params.question - the exact real production question
|
|
* @param {string} params.answer - the exact real production answer
|
|
* @param {object} [params.provider] - optional injected provider (for test isolation)
|
|
* @returns {object} validated result + timing details
|
|
*/
|
|
export async function runLiveFocusedDeconstructExperiment(params) {
|
|
const { targetNodeId, targetLabel, targetDescription, centralStatement, question, answer, provider } = params;
|
|
|
|
const prompt = buildFocusedDeconstructPrompt({
|
|
targetLabel, targetDescription, centralStatement, question, answer,
|
|
});
|
|
|
|
return _executeFocusedPrompt({ prompt, targetNodeId, provider });
|
|
}
|
|
|
|
// ── new supplied-prompt API ──────────────────────────────────────────────
|
|
|
|
/**
|
|
* Execute an already-built focused-deconstruct prompt through the canonical
|
|
* live machinery.
|
|
*
|
|
* The caller supplies a pre-constructed prompt string; this function owns:
|
|
* - configured OLLAMA_MODEL reading
|
|
* - provider creation/invocation
|
|
* - elapsed timing
|
|
* - focused-deconstruct schema validation
|
|
* - structured result return
|
|
*
|
|
* @param {object} params
|
|
* @param {string} params.prompt - the already-built prompt to send
|
|
* @param {string} [params.targetNodeId] - original graph node ID (preserved in result)
|
|
* @param {object} [params.provider] - optional injected provider (for test isolation)
|
|
* @returns {object} validated result + timing details
|
|
*/
|
|
export async function runLiveFocusedPromptExperiment({ prompt, targetNodeId, provider }) {
|
|
return _executeFocusedPrompt({ prompt, targetNodeId, provider });
|
|
}
|
|
|
|
// ── minimal frontier result contract (EXP37) ────────────────────────────
|
|
|
|
/**
|
|
* Validate that a focused-deconstruction result contains exactly one
|
|
* uncertainty and one follow-up question — the minimal frontier output.
|
|
* Returns an array of errors (empty = valid).
|
|
*/
|
|
function validateMinimalFrontierResult(raw) {
|
|
const errors = [];
|
|
|
|
if (!Array.isArray(raw.uncertainties)) {
|
|
errors.push("uncertainties must be an array");
|
|
} else if (raw.uncertainties.length !== 1) {
|
|
errors.push("uncertainties must contain exactly one element, got " + raw.uncertainties.length);
|
|
} else if (typeof raw.uncertainties[0] !== "string" || raw.uncertainties[0].trim() === "") {
|
|
errors.push("uncertainties[0] must be a non-empty string");
|
|
}
|
|
|
|
if (!Array.isArray(raw.possibleFollowUpQuestions)) {
|
|
errors.push("possibleFollowUpQuestions must be an array");
|
|
} else if (raw.possibleFollowUpQuestions.length !== 1) {
|
|
errors.push("possibleFollowUpQuestions must contain exactly one element, got " + raw.possibleFollowUpQuestions.length);
|
|
} else if (typeof raw.possibleFollowUpQuestions[0] !== "string" || raw.possibleFollowUpQuestions[0].trim() === "") {
|
|
errors.push("possibleFollowUpQuestions[0] must be a non-empty string");
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
// ── new frontier API (EXP37 minimal runner) ──────────────────────────────
|
|
|
|
/**
|
|
* Execute an already-built prompt and validate the returned payload
|
|
* against the minimal two-field frontier contract.
|
|
*
|
|
* Returns only: uncertainties, possibleFollowUpQuestions, elapsedMs, success.
|
|
* Does NOT require or reconstruct observations, assumptions, relationships,
|
|
* or targetNodeId.
|
|
*
|
|
* @param {object} params
|
|
* @param {string} params.prompt - the already-built prompt to send
|
|
* @param {object} [params.provider] - optional injected provider (for test isolation)
|
|
* @returns {object} validated result + timing details
|
|
*/
|
|
export async function runLiveFocusedFrontierPromptExperiment({ prompt, provider }) {
|
|
const actualProvider = provider ?? getProvider();
|
|
|
|
const ollamaModel = process.env.OLLAMA_MODEL;
|
|
if (!ollamaModel) throw new Error("OLLAMA_MODEL is not set in environment");
|
|
|
|
const startedAt = Date.now();
|
|
const raw = await actualProvider.generateReconstruction(prompt, ollamaModel);
|
|
const elapsedMs = Date.now() - startedAt;
|
|
|
|
const validationErrors = validateMinimalFrontierResult(raw);
|
|
if (validationErrors.length > 0) {
|
|
throw new Error(
|
|
"Focused frontier result did not match expected minimal contract:\n" +
|
|
validationErrors.map((e) => " - " + e).join("\n")
|
|
);
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
uncertainties: raw.uncertainties,
|
|
possibleFollowUpQuestions: raw.possibleFollowUpQuestions,
|
|
elapsedMs,
|
|
};
|
|
}
|
|
|
|
// ── three-field observation-frontier result contract (EXP38) ────────────
|
|
|
|
/**
|
|
* Validate that a focused-deconstruction result contains observations + one
|
|
* uncertainty + one follow-up question — the minimal observation-grounded
|
|
* frontier output for EXP38.
|
|
* Returns an array of errors (empty = valid).
|
|
*/
|
|
function validateObservationFrontierResult(raw) {
|
|
const errors = [];
|
|
|
|
// observations: required, non-empty array of non-empty strings
|
|
if (!Array.isArray(raw.observations)) {
|
|
errors.push("observations must be an array");
|
|
} else if (raw.observations.length === 0) {
|
|
errors.push("observations must contain at least one element");
|
|
} else {
|
|
for (let i = 0; i < raw.observations.length; i++) {
|
|
if (typeof raw.observations[i] !== "string" || raw.observations[i].trim() === "") {
|
|
errors.push(`observations[${i}] must be a non-empty string`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// uncertainties: exactly one
|
|
if (!Array.isArray(raw.uncertainties)) {
|
|
errors.push("uncertainties must be an array");
|
|
} else if (raw.uncertainties.length !== 1) {
|
|
errors.push("uncertainties must contain exactly one element, got " + raw.uncertainties.length);
|
|
} else if (typeof raw.uncertainties[0] !== "string" || raw.uncertainties[0].trim() === "") {
|
|
errors.push("uncertainties[0] must be a non-empty string");
|
|
}
|
|
|
|
// possibleFollowUpQuestions: exactly one
|
|
if (!Array.isArray(raw.possibleFollowUpQuestions)) {
|
|
errors.push("possibleFollowUpQuestions must be an array");
|
|
} else if (raw.possibleFollowUpQuestions.length !== 1) {
|
|
errors.push("possibleFollowUpQuestions must contain exactly one element, got " + raw.possibleFollowUpQuestions.length);
|
|
} else if (typeof raw.possibleFollowUpQuestions[0] !== "string" || raw.possibleFollowUpQuestions[0].trim() === "") {
|
|
errors.push("possibleFollowUpQuestions[0] must be a non-empty string");
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
// ── observation-frontier runner (EXP38) ──────────────────────────────────
|
|
|
|
/**
|
|
* Execute an already-built prompt and validate the returned payload
|
|
* against the three-field observation-frontier contract.
|
|
*
|
|
* Requires: observations (array of non-empty strings), uncertainties (exactly 1),
|
|
* possibleFollowUpQuestions (exactly 1).
|
|
* Does NOT require assumptions, relationships, or targetNodeId.
|
|
*
|
|
* @param {object} params
|
|
* @param {string} params.prompt - the already-built prompt to send
|
|
* @param {object} [params.provider] - optional injected provider (for test isolation)
|
|
* @returns {object} validated result + timing details
|
|
*/
|
|
export async function runLiveFocusedObservationFrontierPromptExperiment({ prompt, provider }) {
|
|
const actualProvider = provider ?? getProvider();
|
|
|
|
const ollamaModel = process.env.OLLAMA_MODEL;
|
|
if (!ollamaModel) throw new Error("OLLAMA_MODEL is not set in environment");
|
|
|
|
const startedAt = Date.now();
|
|
const raw = await actualProvider.generateReconstruction(prompt, ollamaModel);
|
|
const elapsedMs = Date.now() - startedAt;
|
|
|
|
const validationErrors = validateObservationFrontierResult(raw);
|
|
if (validationErrors.length > 0) {
|
|
throw new Error(
|
|
"Focused observation-frontier result did not match expected three-field contract:\n" +
|
|
validationErrors.map((e) => " - " + e).join("\n")
|
|
);
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
observations: raw.observations,
|
|
uncertainties: raw.uncertainties,
|
|
possibleFollowUpQuestions: raw.possibleFollowUpQuestions,
|
|
elapsedMs,
|
|
};
|
|
}
|
|
|
|
// ── observation+assumption-frontier result contract (EXP46) ──────────────
|
|
|
|
/**
|
|
* Validate that a focused-deconstruction result contains observations (at least one)
|
|
* + assumptions (zero or more) + one uncertainty + one follow-up question.
|
|
* Returns an array of errors (empty = valid).
|
|
*/
|
|
function validateObservationAssumptionFrontierResult(raw) {
|
|
const errors = [];
|
|
|
|
// observations: required, at least one non-empty string
|
|
if (!Array.isArray(raw.observations)) {
|
|
errors.push("observations must be an array");
|
|
} else if (raw.observations.length === 0) {
|
|
errors.push("observations must contain at least one element");
|
|
} else {
|
|
for (let i = 0; i < raw.observations.length; i++) {
|
|
if (typeof raw.observations[i] !== "string" || raw.observations[i].trim() === "") {
|
|
errors.push(`observations[${i}] must be a non-empty string`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// assumptions: zero or more, each a non-empty string if present
|
|
if (!Array.isArray(raw.assumptions)) {
|
|
errors.push("assumptions must be an array");
|
|
} else {
|
|
for (let i = 0; i < raw.assumptions.length; i++) {
|
|
if (typeof raw.assumptions[i] !== "string" || raw.assumptions[i].trim() === "") {
|
|
errors.push(`assumptions[${i}] must be a non-empty string`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// uncertainties: exactly one
|
|
if (!Array.isArray(raw.uncertainties)) {
|
|
errors.push("uncertainties must be an array");
|
|
} else if (raw.uncertainties.length !== 1) {
|
|
errors.push("uncertainties must contain exactly one element, got " + raw.uncertainties.length);
|
|
} else if (typeof raw.uncertainties[0] !== "string" || raw.uncertainties[0].trim() === "") {
|
|
errors.push("uncertainties[0] must be a non-empty string");
|
|
}
|
|
|
|
// possibleFollowUpQuestions: exactly one
|
|
if (!Array.isArray(raw.possibleFollowUpQuestions)) {
|
|
errors.push("possibleFollowUpQuestions must be an array");
|
|
} else if (raw.possibleFollowUpQuestions.length !== 1) {
|
|
errors.push("possibleFollowUpQuestions must contain exactly one element, got " + raw.possibleFollowUpQuestions.length);
|
|
} else if (typeof raw.possibleFollowUpQuestions[0] !== "string" || raw.possibleFollowUpQuestions[0].trim() === "") {
|
|
errors.push("possibleFollowUpQuestions[0] must be a non-empty string");
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
// ── observation+assumption-frontier runner (EXP46) ──────────────────────
|
|
|
|
/**
|
|
* Execute an already-built prompt and validate the returned payload
|
|
* against the observation+assumption-frontier contract.
|
|
*
|
|
* Requires: observations (array of non-empty strings with at least one),
|
|
* assumptions (zero or more non-empty strings, [] is valid),
|
|
* uncertainties (exactly 1), possibleFollowUpQuestions (exactly 1).
|
|
* Does NOT require relationships, targetNodeId, or any other fields.
|
|
*
|
|
* @param {object} params
|
|
* @param {string} params.prompt - the already-built prompt to send
|
|
* @param {object} [params.provider] - optional injected provider (for test isolation)
|
|
* @returns {object} validated result + timing details
|
|
*/
|
|
export async function runLiveFocusedObservationAssumptionFrontierPromptExperiment({ prompt, provider }) {
|
|
const actualProvider = provider ?? getProvider();
|
|
|
|
const ollamaModel = process.env.OLLAMA_MODEL;
|
|
if (!ollamaModel) throw new Error("OLLAMA_MODEL is not set in environment");
|
|
|
|
const startedAt = Date.now();
|
|
const raw = await actualProvider.generateReconstruction(prompt, ollamaModel);
|
|
const elapsedMs = Date.now() - startedAt;
|
|
|
|
const validationErrors = validateObservationAssumptionFrontierResult(raw);
|
|
if (validationErrors.length > 0) {
|
|
throw new Error(
|
|
"Focused observation+assumption-frontier result did not match expected contract:\n" +
|
|
validationErrors.map((e) => " - " + e).join("\n")
|
|
);
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
observations: raw.observations,
|
|
assumptions: raw.assumptions,
|
|
uncertainties: raw.uncertainties,
|
|
possibleFollowUpQuestions: raw.possibleFollowUpQuestions,
|
|
elapsedMs,
|
|
};
|
|
}
|
|
|
|
// ── re-exports for convenience ──────────────────────────────────────────
|
|
|
|
export { buildFocusedDeconstructPrompt, validateFocusedDeconstructSchema };
|
|
|
|
// ── relationship-frontier result contract (EXP44) ──────────────────────
|
|
|
|
/**
|
|
* Validate that a focused-deconstruction result contains relationships + one
|
|
* uncertainty + one follow-up question — the relationship-frontier output.
|
|
* Returns an array of errors (empty = valid).
|
|
*/
|
|
function validateRelationshipFrontierResult(raw) {
|
|
const errors = [];
|
|
|
|
// relationships: required, at least one non-empty string
|
|
if (!Array.isArray(raw.relationships)) {
|
|
errors.push("relationships must be an array");
|
|
} else if (raw.relationships.length === 0) {
|
|
errors.push("relationships must contain at least one element");
|
|
} else {
|
|
for (let i = 0; i < raw.relationships.length; i++) {
|
|
if (typeof raw.relationships[i] !== "string" || raw.relationships[i].trim() === "") {
|
|
errors.push(`relationships[${i}] must be a non-empty string`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// uncertainties: exactly one
|
|
if (!Array.isArray(raw.uncertainties)) {
|
|
errors.push("uncertainties must be an array");
|
|
} else if (raw.uncertainties.length !== 1) {
|
|
errors.push("uncertainties must contain exactly one element, got " + raw.uncertainties.length);
|
|
} else if (typeof raw.uncertainties[0] !== "string" || raw.uncertainties[0].trim() === "") {
|
|
errors.push("uncertainties[0] must be a non-empty string");
|
|
}
|
|
|
|
// possibleFollowUpQuestions: exactly one
|
|
if (!Array.isArray(raw.possibleFollowUpQuestions)) {
|
|
errors.push("possibleFollowUpQuestions must be an array");
|
|
} else if (raw.possibleFollowUpQuestions.length !== 1) {
|
|
errors.push("possibleFollowUpQuestions must contain exactly one element, got " + raw.possibleFollowUpQuestions.length);
|
|
} else if (typeof raw.possibleFollowUpQuestions[0] !== "string" || raw.possibleFollowUpQuestions[0].trim() === "") {
|
|
errors.push("possibleFollowUpQuestions[0] must be a non-empty string");
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
// ── relationship-frontier runner (EXP44) ───────────────────────────────
|
|
|
|
/**
|
|
* Execute an already-built prompt and validate the returned payload
|
|
* against the relationship-frontier contract.
|
|
*
|
|
* Requires: relationships (array of non-empty strings with at least one),
|
|
* uncertainties (exactly 1), possibleFollowUpQuestions (exactly 1).
|
|
* Does NOT require observations, assumptions, or targetNodeId.
|
|
*
|
|
* @param {object} params
|
|
* @param {string} params.prompt - the already-built prompt to send
|
|
* @param {object} [params.provider] - optional injected provider (for test isolation)
|
|
* @returns {object} validated result + timing details
|
|
*/
|
|
export async function runLiveFocusedRelationshipFrontierPromptExperiment({ prompt, provider }) {
|
|
const actualProvider = provider ?? getProvider();
|
|
|
|
const ollamaModel = process.env.OLLAMA_MODEL;
|
|
if (!ollamaModel) throw new Error("OLLAMA_MODEL is not set in environment");
|
|
|
|
const startedAt = Date.now();
|
|
const raw = await actualProvider.generateReconstruction(prompt, ollamaModel);
|
|
const elapsedMs = Date.now() - startedAt;
|
|
|
|
const validationErrors = validateRelationshipFrontierResult(raw);
|
|
if (validationErrors.length > 0) {
|
|
throw new Error(
|
|
"Focused relationship-frontier result did not match expected contract:\n" +
|
|
validationErrors.map((e) => " - " + e).join("\n")
|
|
);
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
relationships: raw.relationships,
|
|
uncertainties: raw.uncertainties,
|
|
possibleFollowUpQuestions: raw.possibleFollowUpQuestions,
|
|
elapsedMs,
|
|
};
|
|
}
|
|
|
|
// ── assumption-frontier result contract (EXP45) ────────────────────────
|
|
|
|
/**
|
|
* Validate that a focused-deconstruction result contains assumptions (zero or more)
|
|
* + one uncertainty + one follow-up question — the assumption-frontier output.
|
|
* Returns an array of errors (empty = valid).
|
|
*/
|
|
function validateAssumptionFrontierResult(raw) {
|
|
const errors = [];
|
|
|
|
// assumptions: zero or more, each a non-empty string if present
|
|
if (!Array.isArray(raw.assumptions)) {
|
|
errors.push("assumptions must be an array");
|
|
} else {
|
|
for (let i = 0; i < raw.assumptions.length; i++) {
|
|
if (typeof raw.assumptions[i] !== "string" || raw.assumptions[i].trim() === "") {
|
|
errors.push(`assumptions[${i}] must be a non-empty string`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// uncertainties: exactly one
|
|
if (!Array.isArray(raw.uncertainties)) {
|
|
errors.push("uncertainties must be an array");
|
|
} else if (raw.uncertainties.length !== 1) {
|
|
errors.push("uncertainties must contain exactly one element, got " + raw.uncertainties.length);
|
|
} else if (typeof raw.uncertainties[0] !== "string" || raw.uncertainties[0].trim() === "") {
|
|
errors.push("uncertainties[0] must be a non-empty string");
|
|
}
|
|
|
|
// possibleFollowUpQuestions: exactly one
|
|
if (!Array.isArray(raw.possibleFollowUpQuestions)) {
|
|
errors.push("possibleFollowUpQuestions must be an array");
|
|
} else if (raw.possibleFollowUpQuestions.length !== 1) {
|
|
errors.push("possibleFollowUpQuestions must contain exactly one element, got " + raw.possibleFollowUpQuestions.length);
|
|
} else if (typeof raw.possibleFollowUpQuestions[0] !== "string" || raw.possibleFollowUpQuestions[0].trim() === "") {
|
|
errors.push("possibleFollowUpQuestions[0] must be a non-empty string");
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
// ── assumption-frontier runner (EXP45) ────────────────────────────────
|
|
|
|
/**
|
|
* Execute an already-built prompt and validate the returned payload
|
|
* against the assumption-frontier contract.
|
|
*
|
|
* Requires: assumptions (zero or more non-empty strings, [] is valid),
|
|
* uncertainties (exactly 1), possibleFollowUpQuestions (exactly 1).
|
|
* Does NOT require observations, relationships, or targetNodeId.
|
|
*
|
|
* @param {object} params
|
|
* @param {string} params.prompt - the already-built prompt to send
|
|
* @param {object} [params.provider] - optional injected provider (for test isolation)
|
|
* @returns {object} validated result + timing details
|
|
*/
|
|
export async function runLiveFocusedAssumptionFrontierPromptExperiment({ prompt, provider }) {
|
|
const actualProvider = provider ?? getProvider();
|
|
|
|
const ollamaModel = process.env.OLLAMA_MODEL;
|
|
if (!ollamaModel) throw new Error("OLLAMA_MODEL is not set in environment");
|
|
|
|
const startedAt = Date.now();
|
|
const raw = await actualProvider.generateReconstruction(prompt, ollamaModel);
|
|
const elapsedMs = Date.now() - startedAt;
|
|
|
|
const validationErrors = validateAssumptionFrontierResult(raw);
|
|
if (validationErrors.length > 0) {
|
|
throw new Error(
|
|
"Focused assumption-frontier result did not match expected contract:\n" +
|
|
validationErrors.map((e) => " - " + e).join("\n")
|
|
);
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
assumptions: raw.assumptions,
|
|
uncertainties: raw.uncertainties,
|
|
possibleFollowUpQuestions: raw.possibleFollowUpQuestions,
|
|
elapsedMs,
|
|
};
|
|
}
|