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:
parent
a0dec0d1d7
commit
8f0f9ff9e6
1 changed files with 23 additions and 6 deletions
|
|
@ -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 prompt = buildGeneratePrompt({ topic, docText, contentType, questionCount, refinement, wordCount, slideCount });
|
||||||
|
|
||||||
var result = await callAI(
|
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 }
|
{ model: model, temperature: 0.4, maxTokens: 6000 }
|
||||||
);
|
);
|
||||||
|
|
||||||
var raw = result.content.trim();
|
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 ──
|
// ── Presentation ──
|
||||||
if (contentType === '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 });
|
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*$/, '');
|
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;
|
var parsed;
|
||||||
try {
|
try {
|
||||||
parsed = JSON.parse(raw);
|
parsed = JSON.parse(raw);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
var match = raw.match(/\{[\s\S]*\}/);
|
// Try extracting from first { to last }
|
||||||
if (match) {
|
var firstBrace = raw.indexOf('{');
|
||||||
try { parsed = JSON.parse(match[0]); }
|
var extractedJson = firstBrace >= 0 ? raw.substring(firstBrace, lastBrace + 1) : null;
|
||||||
catch (e2) { return res.status(500).json({ error: 'AI returned invalid JSON. Try again.' }); }
|
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 {
|
} 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.' });
|
return res.status(500).json({ error: 'AI returned invalid JSON. Try again.' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue