fix: normalise compatible live reconstruction responses
This commit is contained in:
+77
-16
@@ -5,7 +5,12 @@
|
||||
|
||||
import { getConfig } from "../lib/config.js";
|
||||
import { getProvider } from "../lib/llm/provider.js";
|
||||
import { buildPrompt, PROMPT_VERSIONS, DEFAULT_PROMPT_VERSION } from "../lib/reconstruction/prompt.js";
|
||||
import {
|
||||
buildPrompt,
|
||||
PROMPT_VERSIONS,
|
||||
DEFAULT_PROMPT_VERSION,
|
||||
} from "../lib/reconstruction/prompt.js";
|
||||
import { normaliseAnalysisResponse } from "../lib/reconstruction/compatibility.js";
|
||||
import {
|
||||
reconstructionV2Schema,
|
||||
reconstructionSchema as reconstructionV1Schema,
|
||||
@@ -33,7 +38,10 @@ export async function analyseScenario(scenario, opts = {}) {
|
||||
return buildErrorResponse("Scenario cannot be empty", startTime);
|
||||
}
|
||||
if (trimmed.length > MAX_SCENARIO_LENGTH) {
|
||||
return buildErrorResponse(`Scenario must be under ${MAX_SCENARIO_LENGTH} characters`, startTime);
|
||||
return buildErrorResponse(
|
||||
`Scenario must be under ${MAX_SCENARIO_LENGTH} characters`,
|
||||
startTime,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Configuration check ────────────────────────────
|
||||
@@ -50,18 +58,24 @@ export async function analyseScenario(scenario, opts = {}) {
|
||||
try {
|
||||
promptObj = await buildPrompt(trimmed, promptVersion);
|
||||
} catch (e) {
|
||||
return buildErrorResponse(`Failed to build prompt: ${e.message}`, startTime);
|
||||
return buildErrorResponse(
|
||||
`Failed to build prompt: ${e.message}`,
|
||||
startTime,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Call provider ──────────────────────────────────
|
||||
const provider = getProvider();
|
||||
let rawResponse;
|
||||
try {
|
||||
rawResponse = await provider.generateReconstruction(promptObj.prompt, OLLAMA_MODEL);
|
||||
rawResponse = await provider.generateReconstruction(
|
||||
promptObj.prompt,
|
||||
OLLAMA_MODEL,
|
||||
);
|
||||
} catch (e) {
|
||||
return buildErrorResponse(
|
||||
e.message || "Provider error during analysis",
|
||||
Date.now() - startTime
|
||||
Date.now() - startTime,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -75,16 +89,37 @@ export async function analyseScenario(scenario, opts = {}) {
|
||||
rawResponseStr = String(rawResponse).slice(0, 2000);
|
||||
}
|
||||
|
||||
const compatibility = normaliseAnalysisResponse(rawResponse);
|
||||
const candidateResponse = compatibility.normalised;
|
||||
|
||||
// ── Validate against v0.2 schema (preferred) ──────
|
||||
const resultV2 = tryValidateAgainstSchema(rawResponse, reconstructionV2Schema);
|
||||
const resultV2 = tryValidateAgainstSchema(
|
||||
candidateResponse,
|
||||
reconstructionV2Schema,
|
||||
);
|
||||
if (resultV2.valid) {
|
||||
return buildSuccessResultV2(resultV2.data, OLLAMA_MODEL, duration, promptVersion);
|
||||
return buildSuccessResultV2(
|
||||
resultV2.data,
|
||||
OLLAMA_MODEL,
|
||||
duration,
|
||||
promptVersion,
|
||||
compatibility,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Fallback to v0.1 schema ────────────────────────
|
||||
const resultV1 = tryValidateAgainstSchema(rawResponse, reconstructionV1Schema);
|
||||
const resultV1 = tryValidateAgainstSchema(
|
||||
candidateResponse,
|
||||
reconstructionV1Schema,
|
||||
);
|
||||
if (resultV1.valid) {
|
||||
return buildSuccessResultV1(resultV1.data, OLLAMA_MODEL, duration, promptVersion);
|
||||
return buildSuccessResultV1(
|
||||
resultV1.data,
|
||||
OLLAMA_MODEL,
|
||||
duration,
|
||||
promptVersion,
|
||||
compatibility,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Neither schema matched — partial failure ───────
|
||||
@@ -93,17 +128,23 @@ export async function analyseScenario(scenario, opts = {}) {
|
||||
resultV2.error ?? resultV1.error,
|
||||
OLLAMA_MODEL,
|
||||
duration,
|
||||
promptVersion
|
||||
promptVersion,
|
||||
compatibility,
|
||||
);
|
||||
}
|
||||
|
||||
/** Attempt validation against a Zod schema */
|
||||
function tryValidateAgainstSchema(data, schema) {
|
||||
if (!schema.safeParse) {
|
||||
return { valid: false, error: new Error("Schema does not support safeParse") };
|
||||
return {
|
||||
valid: false,
|
||||
error: new Error("Schema does not support safeParse"),
|
||||
};
|
||||
}
|
||||
const result = schema.safeParse(data);
|
||||
return result.success ? { valid: true, data: result.data } : { valid: false, error: result.error };
|
||||
return result.success
|
||||
? { valid: true, data: result.data }
|
||||
: { valid: false, error: result.error };
|
||||
}
|
||||
|
||||
// ── Result builders ──────────────────────────────────
|
||||
@@ -121,7 +162,15 @@ function buildErrorResponse(message, elapsed, statusCode = 500) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildSuccessResultV2(data, model, duration, version) {
|
||||
function buildCompatibilityDiagnostics(compatibility) {
|
||||
return {
|
||||
compatibilityApplied: compatibility.changesApplied.length > 0,
|
||||
compatibilityChanges: compatibility.changesApplied,
|
||||
compatibilityWarnings: compatibility.warnings,
|
||||
};
|
||||
}
|
||||
|
||||
function buildSuccessResultV2(data, model, duration, version, compatibility) {
|
||||
return {
|
||||
success: true,
|
||||
validationStatus: "valid",
|
||||
@@ -134,10 +183,11 @@ function buildSuccessResultV2(data, model, duration, version) {
|
||||
evidence: data.evidence,
|
||||
nextQuestion: data.nextQuestion,
|
||||
errors: undefined,
|
||||
...buildCompatibilityDiagnostics(compatibility),
|
||||
};
|
||||
}
|
||||
|
||||
function buildSuccessResultV1(data, model, duration, version) {
|
||||
function buildSuccessResultV1(data, model, duration, version, compatibility) {
|
||||
return {
|
||||
success: true,
|
||||
validationStatus: "valid",
|
||||
@@ -150,14 +200,24 @@ function buildSuccessResultV1(data, model, duration, version) {
|
||||
evidence: undefined,
|
||||
nextQuestion: undefined,
|
||||
errors: undefined,
|
||||
...buildCompatibilityDiagnostics(compatibility),
|
||||
};
|
||||
}
|
||||
|
||||
function buildPartialResult(rawResp, error, model, duration, version) {
|
||||
function buildPartialResult(
|
||||
rawResp,
|
||||
error,
|
||||
model,
|
||||
duration,
|
||||
version,
|
||||
compatibility,
|
||||
) {
|
||||
let errors = [];
|
||||
if (error && typeof error.flatten === "function") {
|
||||
errors = error.flatten().fieldErrors
|
||||
? Object.entries(error.flatten().fieldErrors).flatMap(([k, v]) => [`${k}: ${v.join(", ")}`])
|
||||
? Object.entries(error.flatten().fieldErrors).flatMap(([k, v]) => [
|
||||
`${k}: ${v.join(", ")}`,
|
||||
])
|
||||
: [String(error)];
|
||||
} else if (error) {
|
||||
errors = [String(error).slice(0, 500)];
|
||||
@@ -175,6 +235,7 @@ function buildPartialResult(rawResp, error, model, duration, version) {
|
||||
evidence: undefined,
|
||||
nextQuestion: undefined,
|
||||
errors,
|
||||
...buildCompatibilityDiagnostics(compatibility),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user