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
This commit is contained in:
@@ -1,15 +1,8 @@
|
||||
const categoryLabels = {
|
||||
observations: "Direct Observations",
|
||||
reportedClaims: "Reported Claims",
|
||||
assumptions: "Unsupported Assumptions",
|
||||
entities: "Entities",
|
||||
transitions: "Transitions",
|
||||
expectedButMissing: "Expected But Missing",
|
||||
presentButUnexpected: "Present But Unexpected",
|
||||
contradictions: "Contradictions",
|
||||
openUncertainties: "Open Uncertainties",
|
||||
};
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
|
||||
// ── Confidence badge (shared) ────────────────────────
|
||||
const confidenceColor = {
|
||||
low: "text-red-600 bg-red-50 border-red-200",
|
||||
medium: "text-yellow-700 bg-yellow-50 border-yellow-200",
|
||||
@@ -22,26 +15,210 @@ const ConfidenceBadge = ({ level }) => (
|
||||
</span>
|
||||
);
|
||||
|
||||
function ItemList({ items, renderExtra }) {
|
||||
if (!items?.length) return <p className="text-sm italic text-gray-400">None identified</p>;
|
||||
|
||||
// ── Evidence type labels (shared) ───────────────────
|
||||
const evidenceTypeLabels = {
|
||||
direct_observation: "Direct Observation",
|
||||
reported_statement: "Reported Statement",
|
||||
interpretation: "Interpretation",
|
||||
assumption: "Assumption",
|
||||
inferred_relationship: "Inferred Relationship",
|
||||
};
|
||||
|
||||
const importanceColors = {
|
||||
incidental: "text-gray-500 bg-gray-50 border-gray-200",
|
||||
supporting: "text-blue-700 bg-blue-50 border-blue-200",
|
||||
important: "text-orange-700 bg-orange-50 border-orange-200",
|
||||
critical: "text-red-800 bg-red-50 border-red-300 font-semibold",
|
||||
};
|
||||
|
||||
const importanceLabels = {
|
||||
incidental: "Incidental",
|
||||
supporting: "Supporting",
|
||||
important: "Important",
|
||||
critical: "Critical",
|
||||
};
|
||||
|
||||
// ── Input classification display ────────────────────
|
||||
function ClassificationDisplay({ classification }) {
|
||||
if (!classification) return null;
|
||||
const p = classification.primaryType || classification.primary_type;
|
||||
const sec = classification.secondaryTypes || classification.secondary_types || [];
|
||||
const modes = classification.reasoningModes || classification.reasoning_modes || [];
|
||||
|
||||
// Normalize camelCase to snake_case for display if needed
|
||||
const primaryLabel = String(p).replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
const secLabels = sec.map((s) => s.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()));
|
||||
const modeLabels = modes.map((m) => m.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()));
|
||||
|
||||
return (
|
||||
<ul className="space-y-2">
|
||||
{items.map((item) => (
|
||||
<li key={item.id} className="rounded border border-gray-200 bg-white px-3 py-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-xs text-gray-400">#{item.id}</span>
|
||||
<ConfidenceBadge level={item.confidence} />
|
||||
</div>
|
||||
<p className="mt-1">{item.description}</p>
|
||||
{renderExtra && renderExtra(item)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="rounded-lg border border-blue-200 bg-blue-50 p-4">
|
||||
<h3 className="mb-2 text-sm font-semibold text-blue-700">Input Classification</h3>
|
||||
<dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1.5 text-sm">
|
||||
<dt className="text-blue-500">Primary type</dt>
|
||||
<dd className="font-medium">{primaryLabel}</dd>
|
||||
{secLabels.length > 0 && (
|
||||
<>
|
||||
<dt className="text-blue-500 pt-1">Secondary types</dt>
|
||||
<dd>{secLabels.join(" · ")}</dd>
|
||||
</>
|
||||
)}
|
||||
{modeLabels.length > 0 && (
|
||||
<>
|
||||
<dt className="text-blue-500 pt-1">Reasoning modes</dt>
|
||||
<dd>{modeLabels.join(" · ")}</dd>
|
||||
</>
|
||||
)}
|
||||
<dt className="text-blue-500 pt-1">Classification reason</dt>
|
||||
<dd className="italic">{classification.classificationReason || classification.classification_reason}</dd>
|
||||
<dt className="text-blue-500 pt-1">Confidence</dt>
|
||||
<dd><ConfidenceBadge level={classification.confidence} /></dd>
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Reconstruction summary ──────────────────────────
|
||||
function SummaryDisplay({ reconstruction }) {
|
||||
if (!reconstruction?.summary) return null;
|
||||
const summary = reconstruction.summary || reconstruction.Summary;
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
||||
<h3 className="mb-2 text-sm font-semibold text-gray-600">Reconstruction Summary</h3>
|
||||
<p className="text-sm leading-relaxed">{summary}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Generic item list (used for multiple sections) ──
|
||||
function ItemList({ title, items, renderExtra }) {
|
||||
const count = items?.length;
|
||||
if (!count) return null; // hide empty sections entirely
|
||||
|
||||
const itemsArr = Array.isArray(items) ? items : [items];
|
||||
|
||||
return (
|
||||
<div className="mb-4 rounded-lg border border-gray-200 bg-white p-4">
|
||||
<h3 className="mb-2 text-sm font-semibold text-gray-600">{title} ({count})</h3>
|
||||
<ul className="space-y-2">
|
||||
{itemsArr.map((item, idx) => (
|
||||
<li key={item.id || `${title}-${idx}`} className="rounded border border-gray-200 bg-white px-3 py-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
{item.id && <span className="font-mono text-xs text-gray-400">#{item.id}</span>}
|
||||
{item.confidence && <ConfidenceBadge level={item.confidence} />}
|
||||
{item.importance && (
|
||||
<span className={`inline-block rounded-full border px-2 py-0.5 text-xs font-medium ${importanceColors[item.importance] || "text-gray-600 bg-gray-100"}`}>
|
||||
{importanceLabels[item.importance]}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1">{item.description}</p>
|
||||
{renderExtra && renderExtra(item)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Plausible interpretations ───────────────────────
|
||||
function InterpretationsDisplay({ interpretations }) {
|
||||
if (!interpretations?.length) return null;
|
||||
const arr = Array.isArray(interpretations) ? interpretations : [interpretations];
|
||||
|
||||
return (
|
||||
<div className="mb-4 rounded-lg border border-indigo-200 bg-indigo-50 p-4">
|
||||
<h3 className="mb-2 text-sm font-semibold text-indigo-700">Plausible Interpretations ({arr.length})</h3>
|
||||
<ul className="space-y-3">
|
||||
{arr.map((interp, idx) => (
|
||||
<li key={interp.id || `${idx}`} className="rounded border border-indigo-200 bg-white px-3 py-2.5 text-sm leading-relaxed">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-medium text-indigo-600">{interp.description}</span>
|
||||
{interp.confidence && <ConfidenceBadge level={interp.confidence} />}
|
||||
</div>
|
||||
{interp.supportingEvidenceIds?.length > 0 && (
|
||||
<p className="text-xs text-gray-500">Supporting evidence: {interp.supportingEvidenceIds.join(", ")}</p>
|
||||
)}
|
||||
{interp.assumptionsRequired?.length > 0 && (
|
||||
<p className="text-xs italic text-gray-500">Requires assumptions: {interp.assumptionsRequired.join("; ")}</p>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Next question (prominent) ───────────────────────
|
||||
function NextQuestionDisplay({ question }) {
|
||||
if (!question?.question) return null;
|
||||
const q = question.question || question.Question;
|
||||
const targets = question.targets || question.Targets || [];
|
||||
const reason = question.reason || question.Reason || "";
|
||||
const value = question.expectedInformationValue || question.expected_information_value || "medium";
|
||||
|
||||
const valueLabel = { low: "Low", medium: "Medium", high: "High" }[value] || "Medium";
|
||||
const valueColor = { low: "bg-yellow-100 text-yellow-800", medium: "bg-blue-100 text-blue-800", high: "bg-green-100 text-green-800" }[value] || "";
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border-2 border-green-300 bg-green-50 p-5">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<h3 className="text-sm font-bold text-green-800">Next Question</h3>
|
||||
<span className={`rounded-full px-2 py-0.5 text-xs font-medium ${valueColor}`}>{valueLabel} value</span>
|
||||
</div>
|
||||
<p className="mb-2 text-base font-medium text-gray-900">{q}</p>
|
||||
{targets.length > 0 && (
|
||||
<p className="text-sm text-gray-600">Targets: {targets.join(", ")}</p>
|
||||
)}
|
||||
{reason && <p className="text-sm italic text-gray-500">Because: {reason}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Evidence list ───────────────────────────────────
|
||||
function EvidenceDisplay({ evidence }) {
|
||||
if (!evidence?.length) return null;
|
||||
const arr = Array.isArray(evidence) ? evidence : [evidence];
|
||||
|
||||
const evidenceLabels = {
|
||||
direct_observation: "👁 Direct Observation",
|
||||
reported_statement: "🗣 Reported Statement",
|
||||
interpretation: "💡 Interpretation",
|
||||
assumption: "❓ Assumption",
|
||||
inferred_relationship: "🔗 Inferred Relationship",
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-4 rounded-lg border border-gray-200 bg-white p-4">
|
||||
<h3 className="mb-2 text-sm font-semibold text-gray-600">Supporting Evidence ({arr.length})</h3>
|
||||
<ul className="space-y-2">
|
||||
{arr.map((item, idx) => (
|
||||
<li key={item.id || `${idx}`} className="rounded border border-gray-200 bg-white px-3 py-2 text-sm leading-relaxed">
|
||||
<div className="flex items-center gap-2 mb-0.5 flex-wrap">
|
||||
{item.id && <span className="font-mono text-xs text-gray-400">#{item.id}</span>}
|
||||
<span className={`inline-block rounded px-1.5 py-0.5 text-[10px] font-medium ${importanceColors[item.importance] || "text-gray-600 bg-gray-100"}`}>
|
||||
{importanceLabels[item.importance]}
|
||||
</span>
|
||||
<span className="inline-block rounded px-1.5 py-0.5 text-[10px] font-medium bg-gray-100 text-gray-700">
|
||||
{evidenceLabels[item.evidenceType] || item.evidenceType}
|
||||
</span>
|
||||
{item.confidence && <ConfidenceBadge level={item.confidence} />}
|
||||
</div>
|
||||
<p className="text-sm">{item.description}</p>
|
||||
{(item.source || item.attribution) && (
|
||||
<p className="mt-0.5 text-xs text-gray-400">Source: {item.source || item.attribution}</p>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Main component ──────────────────────────────────
|
||||
export default function ReconstructionView({ reconstruction, partial }) {
|
||||
// Handle both v0.2 direct object and wrapped result formats
|
||||
const data = reconstruction;
|
||||
|
||||
if (partial) {
|
||||
return (
|
||||
<div className="rounded-lg border border-yellow-300 bg-yellow-50 px-4 py-3 text-sm text-yellow-800">
|
||||
@@ -50,21 +227,74 @@ export default function ReconstructionView({ reconstruction, partial }) {
|
||||
);
|
||||
}
|
||||
|
||||
const categories = Object.entries(categoryLabels).map(([key, label]) => ({
|
||||
key,
|
||||
label,
|
||||
items: reconstruction[key],
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<h2 className="mb-3 text-lg font-semibold">Reconstruction</h2>
|
||||
{categories.map(({ key, label, items }) => (
|
||||
<div key={key} className="mb-4 rounded border border-gray-200 bg-white p-4">
|
||||
<h3 className="mb-2 text-sm font-medium text-gray-600">{label}</h3>
|
||||
<ItemList items={items} />
|
||||
</div>
|
||||
))}
|
||||
<div className="space-y-4">
|
||||
{/* Classification first */}
|
||||
{data.inputClassification && (
|
||||
<ClassificationDisplay classification={data.inputClassification} />
|
||||
)}
|
||||
|
||||
{/* Summary */}
|
||||
{data.reconstruction?.summary && (
|
||||
<SummaryDisplay reconstruction={data.reconstruction} />
|
||||
)}
|
||||
|
||||
{/* Key differences */}
|
||||
{data.reconstruction?.differences && (
|
||||
<ItemList title="Key Differences" items={data.reconstruction.differences} />
|
||||
)}
|
||||
|
||||
{/* Unexplained transitions */}
|
||||
{data.reconstruction?.unexplainedTransitions && data.reconstruction.unexplainedTransitions.length > 0 && (
|
||||
<ItemList title="Unexplained Transitions" items={data.reconstruction.unexplainedTransitions} renderExtra={(i) => (
|
||||
i.entity && <p className="mt-1 text-xs text-gray-500">Entity: {i.entity}</p>
|
||||
)} />
|
||||
)}
|
||||
|
||||
{/* Contradictions */}
|
||||
{data.reconstruction?.contradictions && data.reconstruction.contradictions.length > 0 && (
|
||||
<ItemList title="Contradictions" items={data.reconstruction.contradictions} />
|
||||
)}
|
||||
|
||||
{/* Important unknowns */}
|
||||
{data.reconstruction?.importantUnknowns && data.reconstruction.importantUnknowns.length > 0 && (
|
||||
<ItemList title="Important Unknowns" items={data.reconstruction.importantUnknowns} />
|
||||
)}
|
||||
|
||||
{/* Plausible interpretations */}
|
||||
{data.reconstruction?.plausibleInterpretations && data.reconstruction.plausibleInterpretations.length > 0 && (
|
||||
<InterpretationsDisplay interpretations={data.reconstruction.plausibleInterpretations} />
|
||||
)}
|
||||
|
||||
{/* Secondary reconstruction categories (actors, systems, etc.) */}
|
||||
{data.reconstruction?.actors && data.reconstruction.actors.length > 0 && (
|
||||
<ItemList title="Actors" items={data.reconstruction.actors} />
|
||||
)}
|
||||
{data.reconstruction?.systemsOrObjects && data.reconstruction.systemsOrObjects.length > 0 && (
|
||||
<ItemList title="Systems / Objects" items={data.reconstruction.systemsOrObjects} />
|
||||
)}
|
||||
{data.reconstruction?.expectedStates && data.reconstruction.expectedStates.length > 0 && (
|
||||
<ItemList title="Expected States" items={data.reconstruction.expectedStates} />
|
||||
)}
|
||||
{data.reconstruction?.observedStates && data.reconstruction.observedStates.length > 0 && (
|
||||
<ItemList title="Observed States" items={data.reconstruction.observedStates} />
|
||||
)}
|
||||
{data.reconstruction?.knownTransitions && data.reconstruction.knownTransitions.length > 0 && (
|
||||
<ItemList title="Known Transitions" items={data.reconstruction.knownTransitions} renderExtra={(i) => (
|
||||
<div className="mt-1 text-xs text-gray-500">
|
||||
{i.entity && <span>Entity: {i.entity} · </span>}
|
||||
From "{i.previousState}" → To "{i.currentState}" ({i.explanationStatus})
|
||||
</div>
|
||||
)} />
|
||||
)}
|
||||
|
||||
{/* Next question — prominent */}
|
||||
<NextQuestionDisplay question={data.nextQuestion} />
|
||||
|
||||
{/* Evidence */}
|
||||
{data.evidence && (
|
||||
<EvidenceDisplay evidence={data.evidence} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user