Files
confidence-engine/scripts/debug-ollama-direct.mjs
T
robbond 956fc2e31e fix: resolve 500 errors from model returning trivial status objects (root cause + v0.2 prompt fix)
Two bugs were causing the model to return {"status":"ok"} / {"status":"ready"}
instead of structured reconstruction data, resulting in POST /api/analyse 500:

1. DOUBLE-WRAPPING BUG (lib/llm/provider.js):
   generateReconstruction() called buildPrompt(scenario) on input that was
   already a fully-built prompt string from analyseScenario(). This wrapped the
   v0.1 prompt (~5000+ chars) in another template layer, producing incomprehensible
   output that the model could not parse as structured JSON.
   Fix: Pass scenario through directly (it is ALREADY a built prompt).

2. MISSING JSON SPEC (prompts/reconstruct-v0.2.md):
   The v0.2 prompt template said 'matching the structure exactly' but never
   defined what that structure was. The model invented its own field names
   (input_classification, reasoning_mode, anchors) with snake_case instead of
   camelCase, which failed Zod validation -> 500 errors.
   Fix: Added explicit JSON schema section with exact key names, enum values,
   and nested structure matching the Zod validation layer.

Additionally:
- Refactored route to use analyseScenario from lib/analysis (centralized)
- Added lib/analysis.js with shared analysis logic
- Updated components to display promptVersion and validation errors
- Added lib/reconstruction/prompt.js v0.1/v0.2 versioning
- Added lib/reconstruction/schema.js v0.2 Zod schemas
- Added debug tool scripts, evaluation results, and comparison findings
2026-08-01 08:57:28 +01:00

219 lines
7.7 KiB
JavaScript

/**
* Debug script: send raw Ollama requests directly, bypassing the application provider.
* Tests /api/chat with format:json and captures request payloads + raw responses.
*/
import { mkdirSync, writeFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const BASE_URL = process.env.OLLAMA_BASE_URL || "http://localhost:11434";
const TIMESTAMP = new Date().toISOString().replace(/[/:]/g, "-");
const RESULTS_DIR = join(__dirname, "..", "provider-debug-results", TIMESTAMP);
mkdirSync(RESULTS_DIR, { recursive: true });
// ============================================================
// Test cases
// ============================================================
const MODEL_A = "qwen-claude:latest";
const MODEL_B = "qwen3.6:35b-a3b";
function getModelList() {
// Check which models are available locally (not via Ollama server)
return { A: MODEL_A, B: MODEL_B };
}
// Test A: Simple text reply to verify model responds normally
const TEST_A = {
label: "A",
description: "Plain instruction test — should return CHAT_WORKS",
system: "You are a normal assistant. Follow the user instruction exactly.",
user: "Reply with exactly: CHAT_WORKS",
};
// Test B: Explicit JSON schema via format field
const TEST_B = {
label: "B",
description: "JSON schema test — should return exact object",
system: null, // uses messages only with format
user: 'Return exactly: {"message": "STRUCTURED_OUTPUT_WORKS"}',
};
// Test C: Minimal reconstruction-style schema
const TEST_C = {
label: "C",
description: "Minimal reconstruction schema — structured output test",
system: null,
user: "Analyse this situation without solving it: Some customers can log in but cannot download invoices. Identify the meaningful difference and ask one useful next question.",
};
const ALL_TESTS = [TEST_A, TEST_B, TEST_C];
// ============================================================
// Helper functions
// ============================================================
async function runChatWithFormat(model, messages, format) {
const body = { model, messages, stream: false, format };
const res = await fetch(`${BASE_URL}/api/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const rawText = await res.text();
let parsed = null;
try { parsed = JSON.parse(rawText); } catch {}
return {
status: res.status,
statusText: res.statusText,
requestPayload: body,
rawResponseText: rawText.slice(0, 5000),
parsedResponse: parsed,
messageContent: parsed?.message?.content ?? null,
thinkingLength: (parsed?.message?.thinking || "").length,
messageContentType: typeof parsed?.message?.content,
responseField: parsed?.response,
};
}
async function runGenerate(model, prompt) {
const body = { model, prompt, stream: false };
const res = await fetch(`${BASE_URL}/api/generate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const rawText = await res.text();
let parsed = null;
try { parsed = JSON.parse(rawText); } catch {}
return {
status: res.status,
requestPayload: body,
rawResponseText: rawText.slice(0, 5000),
parsedResponse: parsed,
responseField: typeof parsed?.response === "string" ? parsed.response : JSON.stringify(parsed),
responseFirst200: (parsed?.response || "").slice(0, 200),
};
}
// ============================================================
// Run tests
// ============================================================
const results = {};
for (const model of [MODEL_A, MODEL_B]) {
console.log(`\n=== Testing model: ${model} ===`);
results[model] = {};
// Check if model is available locally
let available = false;
try {
const tagsRes = await fetch(`${BASE_URL}/api/tags`);
const tagsData = await tagsRes.json();
available = tagsData.models?.some(m => m.name.includes(model.split(":")[0]));
} catch (e) {
console.log(` Warning: could not check model availability: ${e.message}`);
}
if (!available) {
results[model].availability = "NOT_AVAILABLE_ON_SERVER";
console.log(` -> Model ${model} not found on server, skipping`);
continue;
}
console.log(` -> Model available on server\n`);
for (const test of ALL_TESTS) {
const testKey = `test_${test.label}_${model.split(":")[0].replace(/[^a-zA-Z]/g, "_")}`;
console.log(` Running Test ${test.label}: ${test.description}`);
// Chat with format:json
let chatResult;
try {
const messages = [];
if (test.system) {
messages.push({ role: "system", content: test.system });
}
messages.push({ role: "user", content: test.user });
chatResult = await runChatWithFormat(model, messages, "json");
// Try to extract JSON from message.content
let extractedJson = null;
if (typeof chatResult.messageContent === "string") {
try {
extractedJson = JSON.parse(chatResult.messageContent);
} catch {}
}
results[model][testKey] = {
testDescription: test.description,
endpoint: "/api/chat",
format: "json",
hasSystemMessage: !!test.system,
httpStatus: chatResult.status,
messageContentType: chatResult.messageContentType,
messageContentLength: chatResult.messageContent?.length || 0,
thinkingPresent: chatResult.thinkingLength > 0,
parsedContentKeys: extractedJson ? Object.keys(extractedJson) : null,
// If content looks like a status acknowledgment
looksLikeStatusAck: typeof chatResult.messageContent === "string" &&
(chatResult.messageContent.includes('"status"') || chatResult.messageContent.includes('"state"')),
rawPreview: chatResult.messageContent?.slice(0, 300) ?? "(none)",
};
const status = extractedJson ? "JSON_OK" : (chatResult.messageContent ? "TEXT_RESPONSE" : "EMPTY");
console.log(` -> ${status} (HTTP ${chatResult.status}, content type: ${chatResult.messageContentType})`);
if (extractedJson) {
console.log(` JSON keys: ${Object.keys(extractedJson).join(", ")}`);
} else if (chatResult.messageContent) {
console.log(` Content preview: ${(typeof chatResult.messageContent === "string" ? chatResult.messageContent : String(chatResult.messageContent)).slice(0, 150)}...`);
}
} catch (e) {
results[model][testKey] = { error: e.message };
console.log(` -> ERROR: ${e.message}`);
}
// Generate (fallback test)
let generateResult;
try {
const generatePrompt = test.system ? `${test.system}\n\n${test.user}` : test.user;
generateResult = await runGenerate(model, generatePrompt);
results[model][`${testKey}_generate`] = {
endpoint: "/api/generate",
httpStatus: generateResult.status,
responseFirst200: generateResult.responseFirst200,
responseLooksLikeStructuredJSON: generateResult.responseField?.trim().startsWith("{"),
rawPreview: generateResult.responseFirst200,
};
const isJson = generateResult.responseField?.trim().startsWith("{") ? "JSON_START" : "NOT_JSON";
console.log(` -> ${isJson} (HTTP ${generateResult.status})`);
} catch (e) {
results[model][`${testKey}_generate`] = { error: e.message };
console.log(` -> GENERATE ERROR: ${e.message}`);
}
console.log();
}
}
// ============================================================
// Save results
// ============================================================
const saveFile = join(RESULTS_DIR, "debug-results.json");
writeFileSync(saveFile, JSON.stringify(results, null, 2));
console.log(`\nResults saved to: ${saveFile}`);