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
- 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++;
}
}

View File

@@ -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 });
}
}