feat: add curriculum management service and database integration for 26-week schedule generation
This commit is contained in:
@@ -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;
|
||||
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: userPrompt,
|
||||
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;
|
||||
}
|
||||
|
||||
const schedule = emitted.weeks;
|
||||
|
||||
// Validate
|
||||
schedule = emitted.weeks;
|
||||
const validation = validateSchedule(schedule, topics);
|
||||
if (!validation.valid) {
|
||||
throw new Error(`Generated schedule failed validation:\n- ${validation.errors.join('\n- ')}`);
|
||||
if (validation.valid) {
|
||||
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);
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user