diff --git a/src/lib/curriculumService.js b/src/lib/curriculumService.js index 4816b94..37d5335 100644 --- a/src/lib/curriculumService.js +++ b/src/lib/curriculumService.js @@ -147,34 +147,56 @@ Rules: - Do NOT invent theme or topic references — use only the provided values - Emit via emit_curriculum_schedule tool — no prose`; - // Try generation + // Try generation with a retry mechanism if validation fails let result; - try { - result = await callLLM({ - task: 'curriculum.generate', - tier: 'standard', - system: cachedSystem(SYSTEM_PROMPT), - user: userPrompt, - tools: [EMIT_CURRICULUM_SCHEDULE_TOOL], - toolChoice: { type: 'tool', name: EMIT_CURRICULUM_SCHEDULE_TOOL.name }, - maxTokens: 8192, - temperature: 0, - }); - } catch (err) { - throw new Error(`AI generation failed: ${err.message}`); + let schedule; + let validationErrors = []; + + for (let attempt = 1; attempt <= 2; attempt++) { + let prompt = userPrompt; + if (attempt > 1) { + prompt = `${userPrompt}\n\nWARNING: The previous generation attempt failed validation with the following errors. Please correct them:\n- ${validationErrors.join('\n- ')}`; + } + + try { + result = await callLLM({ + task: 'curriculum.generate', + tier: 'standard', + system: cachedSystem(SYSTEM_PROMPT), + user: prompt, + tools: [EMIT_CURRICULUM_SCHEDULE_TOOL], + toolChoice: { type: 'tool', name: EMIT_CURRICULUM_SCHEDULE_TOOL.name }, + maxTokens: 8192, + temperature: 0, + }); + } catch (err) { + if (attempt === 2) { + throw new Error(`AI generation failed: ${err.message}`); + } + continue; + } + + const emitted = result.toolUses[0]?.input; + if (!emitted || !emitted.weeks) { + validationErrors = ['The AI did not emit a valid curriculum schedule structure.']; + if (attempt === 2) { + throw new Error('The AI did not emit a valid curriculum schedule.'); + } + continue; + } + + schedule = emitted.weeks; + const validation = validateSchedule(schedule, topics); + if (validation.valid) { + validationErrors = []; + break; + } else { + validationErrors = validation.errors; + } } - const emitted = result.toolUses[0]?.input; - if (!emitted || !emitted.weeks) { - throw new Error('The AI did not emit a valid curriculum schedule.'); - } - - const schedule = emitted.weeks; - - // Validate - const validation = validateSchedule(schedule, topics); - if (!validation.valid) { - throw new Error(`Generated schedule failed validation:\n- ${validation.errors.join('\n- ')}`); + if (validationErrors.length > 0) { + throw new Error(`Generated schedule failed validation after retry:\n- ${validationErrors.join('\n- ')}`); } const stats = computeCoverageStats(schedule, topics); @@ -206,15 +228,24 @@ export async function confirmVersion(versionId, adminUserId) { } const currentActive = await db.getActiveCurriculumVersion(); - if (currentActive) { - await db.updateCurriculumVersion(currentActive.id, { status: 'superseded' }); - } - return db.updateCurriculumVersion(versionId, { + // Set the new version to active first to ensure we never have zero active versions + const updated = await db.updateCurriculumVersion(versionId, { status: 'active', confirmed_by: adminUserId, confirmed_at: new Date().toISOString(), }); + + // Supercede old active version gracefully + if (currentActive) { + try { + await db.updateCurriculumVersion(currentActive.id, { status: 'superseded' }); + } catch (e) { + console.warn('[Curriculum] Failed to supersede old active version, but new version was successfully activated:', e.message); + } + } + + return updated; } /** @@ -257,8 +288,9 @@ export async function getCurrentWeekContent(isoWeekNumber) { if (!scheduleWeek) return null; const topics = await db.getTopics(); + const topicMap = new Map(topics.map(t => [t.id, t])); const weekTopics = scheduleWeek.topic_ids - .map(id => topics.find(t => t.id === id)) + .map(id => topicMap.get(id)) .filter(Boolean); return { @@ -333,12 +365,12 @@ export async function enrichTopicsForCurriculum() { for (const update of enrichedBatch) { const original = allTopics.find(t => t.id === update.id); if (original) { - await db.saveTopics([{ + await db.upsertTopic({ ...original, theme: update.theme, complexity_weight: update.complexity_weight, difficulty: update.difficulty - }]); + }); totalEnriched++; } } diff --git a/src/lib/db.js b/src/lib/db.js index eb0fb93..b3dabb2 100644 --- a/src/lib/db.js +++ b/src/lib/db.js @@ -29,6 +29,9 @@ export async function saveTopics(topics) { description: t.description, learning_relevance: t.learning_relevance || 'standard', relevance_locked: t.relevance_locked === true, + theme: t.theme || '', + complexity_weight: t.complexity_weight || 3, + difficulty: t.difficulty || 'intermediate', }, { requestKey: null }); } }