feat: add curriculum management service and database integration for 26-week schedule generation
All checks were successful
On Push to Main / test (push) Successful in 32s
On Push to Main / publish (push) Successful in 1m2s
On Push to Main / deploy-dev (push) Successful in 1m32s

This commit is contained in:
RaymondVerhoef
2026-05-24 19:58:18 +02:00
parent c5e23c77cd
commit 10d5066be8
2 changed files with 67 additions and 32 deletions

View File

@@ -147,34 +147,56 @@ Rules:
- Do NOT invent theme or topic references — use only the provided values - Do NOT invent theme or topic references — use only the provided values
- Emit via emit_curriculum_schedule tool — no prose`; - Emit via emit_curriculum_schedule tool — no prose`;
// Try generation // Try generation with a retry mechanism if validation fails
let result; let result;
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 { try {
result = await callLLM({ result = await callLLM({
task: 'curriculum.generate', task: 'curriculum.generate',
tier: 'standard', tier: 'standard',
system: cachedSystem(SYSTEM_PROMPT), system: cachedSystem(SYSTEM_PROMPT),
user: userPrompt, user: prompt,
tools: [EMIT_CURRICULUM_SCHEDULE_TOOL], tools: [EMIT_CURRICULUM_SCHEDULE_TOOL],
toolChoice: { type: 'tool', name: EMIT_CURRICULUM_SCHEDULE_TOOL.name }, toolChoice: { type: 'tool', name: EMIT_CURRICULUM_SCHEDULE_TOOL.name },
maxTokens: 8192, maxTokens: 8192,
temperature: 0, temperature: 0,
}); });
} catch (err) { } catch (err) {
if (attempt === 2) {
throw new Error(`AI generation failed: ${err.message}`); throw new Error(`AI generation failed: ${err.message}`);
} }
continue;
}
const emitted = result.toolUses[0]?.input; const emitted = result.toolUses[0]?.input;
if (!emitted || !emitted.weeks) { 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.'); throw new Error('The AI did not emit a valid curriculum schedule.');
} }
continue;
}
const schedule = emitted.weeks; schedule = emitted.weeks;
// Validate
const validation = validateSchedule(schedule, topics); const validation = validateSchedule(schedule, topics);
if (!validation.valid) { if (validation.valid) {
throw new Error(`Generated schedule failed validation:\n- ${validation.errors.join('\n- ')}`); validationErrors = [];
break;
} else {
validationErrors = validation.errors;
}
}
if (validationErrors.length > 0) {
throw new Error(`Generated schedule failed validation after retry:\n- ${validationErrors.join('\n- ')}`);
} }
const stats = computeCoverageStats(schedule, topics); const stats = computeCoverageStats(schedule, topics);
@@ -206,15 +228,24 @@ export async function confirmVersion(versionId, adminUserId) {
} }
const currentActive = await db.getActiveCurriculumVersion(); 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', status: 'active',
confirmed_by: adminUserId, confirmed_by: adminUserId,
confirmed_at: new Date().toISOString(), 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; if (!scheduleWeek) return null;
const topics = await db.getTopics(); const topics = await db.getTopics();
const topicMap = new Map(topics.map(t => [t.id, t]));
const weekTopics = scheduleWeek.topic_ids const weekTopics = scheduleWeek.topic_ids
.map(id => topics.find(t => t.id === id)) .map(id => topicMap.get(id))
.filter(Boolean); .filter(Boolean);
return { return {
@@ -333,12 +365,12 @@ export async function enrichTopicsForCurriculum() {
for (const update of enrichedBatch) { for (const update of enrichedBatch) {
const original = allTopics.find(t => t.id === update.id); const original = allTopics.find(t => t.id === update.id);
if (original) { if (original) {
await db.saveTopics([{ await db.upsertTopic({
...original, ...original,
theme: update.theme, theme: update.theme,
complexity_weight: update.complexity_weight, complexity_weight: update.complexity_weight,
difficulty: update.difficulty difficulty: update.difficulty
}]); });
totalEnriched++; totalEnriched++;
} }
} }

View File

@@ -29,6 +29,9 @@ export async function saveTopics(topics) {
description: t.description, description: t.description,
learning_relevance: t.learning_relevance || 'standard', learning_relevance: t.learning_relevance || 'standard',
relevance_locked: t.relevance_locked === true, relevance_locked: t.relevance_locked === true,
theme: t.theme || '',
complexity_weight: t.complexity_weight || 3,
difficulty: t.difficulty || 'intermediate',
}, { requestKey: null }); }, { requestKey: null });
} }
} }