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 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.' });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue