Fix AI invalid JSON: add system prompt + robust JSON extraction

Opus 4.6 and other large models add preamble/thinking text before
JSON output. Fix: add system message enforcing JSON-only, strip
leading text before first {, trim trailing text after last }, and
log raw output on parse failure for debugging.
This commit is contained in:
Daniel Onyejesi 2026-03-24 17:52:49 -04:00
parent a0dec0d1d7
commit 8f0f9ff9e6

View file

@ -202,11 +202,19 @@ router.post('/ai-generate', upload.single('file'), async function(req, res) {
var prompt = buildGeneratePrompt({ topic, docText, contentType, questionCount, refinement, wordCount, slideCount });
var result = await callAI(
[{ role: 'user', content: prompt }],
[
{ role: 'system', content: 'You are a medical education content generator. Return ONLY the requested JSON or Marp markdown — no preamble, no commentary, no code fences, no thinking. Start your response with { or --- as appropriate.' },
{ role: 'user', content: prompt }
],
{ model: model, temperature: 0.4, maxTokens: 6000 }
);
var raw = result.content.trim();
// Strip any leading text before the first { or --- (models sometimes add preamble)
if (contentType !== 'presentation' || parseInt(questionCount) > 0) {
var jsonStart = raw.indexOf('{');
if (jsonStart > 0) raw = raw.substring(jsonStart);
}
// ── Presentation ──
if (contentType === 'presentation') {
@ -228,18 +236,27 @@ router.post('/ai-generate', upload.single('file'), async function(req, res) {
return res.json({ success: true, contentType: 'presentation', marpMarkdown: marpMd, questions: [], model: result.model, docLength: docText.length });
}
// Strip code fences if model added them anyway
// Strip code fences and any trailing text after JSON
raw = raw.replace(/^```(?:json)?\s*/i, '').replace(/\s*```\s*$/, '');
// Trim trailing non-JSON text (e.g. "Here is the JSON..." after closing brace)
var lastBrace = raw.lastIndexOf('}');
if (lastBrace !== -1 && lastBrace < raw.length - 1) raw = raw.substring(0, lastBrace + 1);
var parsed;
try {
parsed = JSON.parse(raw);
} catch (e) {
var match = raw.match(/\{[\s\S]*\}/);
if (match) {
try { parsed = JSON.parse(match[0]); }
catch (e2) { return res.status(500).json({ error: 'AI returned invalid JSON. Try again.' }); }
// Try extracting from first { to last }
var firstBrace = raw.indexOf('{');
var extractedJson = firstBrace >= 0 ? raw.substring(firstBrace, lastBrace + 1) : null;
if (extractedJson) {
try { parsed = JSON.parse(extractedJson); }
catch (e2) {
console.error('[LearningAI] JSON parse failed. Raw (first 500):', raw.substring(0, 500));
return res.status(500).json({ error: 'AI returned invalid JSON. Try again.' });
}
} else {
console.error('[LearningAI] No JSON found. Raw (first 500):', raw.substring(0, 500));
return res.status(500).json({ error: 'AI returned invalid JSON. Try again.' });
}
}