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
|
- 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++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user