project init
This commit is contained in:
289
apps/ai-service/src/pipeline/course-generation.pipeline.ts
Normal file
289
apps/ai-service/src/pipeline/course-generation.pipeline.ts
Normal file
@ -0,0 +1,289 @@
|
||||
import { prisma, GenerationStatus, CourseStatus } from '@coursecraft/database';
|
||||
import { generateUniqueSlug } from '@coursecraft/shared';
|
||||
import { OpenRouterProvider, CourseOutline } from '../providers/openrouter.provider';
|
||||
|
||||
// Logger helper
|
||||
const log = {
|
||||
step: (step: string, msg: string) => {
|
||||
console.log(`\x1b[35m[Pipeline]\x1b[0m \x1b[1m${step}\x1b[0m - ${msg}`);
|
||||
},
|
||||
info: (msg: string, data?: unknown) => {
|
||||
console.log(`\x1b[35m[Pipeline]\x1b[0m ${msg}`, data ? JSON.stringify(data, null, 2) : '');
|
||||
},
|
||||
success: (msg: string) => {
|
||||
console.log(`\x1b[32m[Pipeline] ✓\x1b[0m ${msg}`);
|
||||
},
|
||||
error: (msg: string, error?: unknown) => {
|
||||
console.error(`\x1b[31m[Pipeline] ✗\x1b[0m ${msg}`, error);
|
||||
},
|
||||
progress: (generationId: string, progress: number, step: string) => {
|
||||
console.log(`\x1b[34m[Pipeline]\x1b[0m [${generationId.substring(0, 8)}...] ${progress}% - ${step}`);
|
||||
},
|
||||
};
|
||||
|
||||
export class CourseGenerationPipeline {
|
||||
constructor(private aiProvider: OpenRouterProvider) {
|
||||
log.info('CourseGenerationPipeline initialized');
|
||||
}
|
||||
|
||||
async generateCourse(
|
||||
generationId: string,
|
||||
prompt: string,
|
||||
aiModel: string
|
||||
): Promise<void> {
|
||||
log.step('START', `Beginning course generation`);
|
||||
log.info(`Generation ID: ${generationId}`);
|
||||
log.info(`AI Model: ${aiModel}`);
|
||||
log.info(`Prompt: "${prompt}"`);
|
||||
|
||||
try {
|
||||
// Step 1: Update status to analyzing
|
||||
log.step('STEP 1', 'Analyzing request...');
|
||||
await this.updateProgress(generationId, GenerationStatus.ANALYZING, 10, 'Analyzing your request...');
|
||||
|
||||
// Step 2: Generate clarifying questions
|
||||
log.step('STEP 2', 'Generating clarifying questions...');
|
||||
await this.updateProgress(generationId, GenerationStatus.ASKING_QUESTIONS, 15, 'Generating questions...');
|
||||
|
||||
const questionsResult = await this.aiProvider.generateClarifyingQuestions(prompt, aiModel);
|
||||
log.success(`Generated ${questionsResult.questions.length} clarifying questions`);
|
||||
|
||||
// Step 3: Wait for user answers
|
||||
log.step('STEP 3', 'Waiting for user answers...');
|
||||
await prisma.courseGeneration.update({
|
||||
where: { id: generationId },
|
||||
data: {
|
||||
status: GenerationStatus.WAITING_FOR_ANSWERS,
|
||||
progress: 20,
|
||||
currentStep: 'Waiting for your answers...',
|
||||
questions: questionsResult.questions as unknown as Record<string, unknown>,
|
||||
},
|
||||
});
|
||||
|
||||
log.success('Generation paused - waiting for user answers');
|
||||
// Job will be continued when user answers via continue-generation
|
||||
} catch (error) {
|
||||
log.error('Course generation failed', error);
|
||||
await this.updateProgress(
|
||||
generationId,
|
||||
GenerationStatus.FAILED,
|
||||
0,
|
||||
'Generation failed',
|
||||
{ errorMessage: error instanceof Error ? error.message : 'Unknown error' }
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async continueGeneration(generationId: string, stage: string): Promise<void> {
|
||||
log.step('CONTINUE', `Continuing generation at stage: ${stage}`);
|
||||
log.info(`Generation ID: ${generationId}`);
|
||||
|
||||
const generation = await prisma.courseGeneration.findUnique({
|
||||
where: { id: generationId },
|
||||
});
|
||||
|
||||
if (!generation) {
|
||||
log.error('Generation not found');
|
||||
throw new Error('Generation not found');
|
||||
}
|
||||
|
||||
log.info(`Current status: ${generation.status}`);
|
||||
log.info(`AI Model: ${generation.aiModel}`);
|
||||
|
||||
try {
|
||||
if (stage === 'after-questions') {
|
||||
await this.continueAfterQuestions(generation);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('Continue generation failed', error);
|
||||
await this.updateProgress(
|
||||
generationId,
|
||||
GenerationStatus.FAILED,
|
||||
0,
|
||||
'Generation failed',
|
||||
{ errorMessage: error instanceof Error ? error.message : 'Unknown error' }
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async continueAfterQuestions(generation: {
|
||||
id: string;
|
||||
userId: string;
|
||||
initialPrompt: string;
|
||||
aiModel: string;
|
||||
answers: unknown;
|
||||
}): Promise<void> {
|
||||
const { id: generationId, userId, initialPrompt, aiModel, answers } = generation;
|
||||
|
||||
log.info('User answers received:', answers);
|
||||
|
||||
// Step 4: Research (simulated - in production would use web search)
|
||||
log.step('STEP 4', 'Researching the topic...');
|
||||
await this.updateProgress(generationId, GenerationStatus.RESEARCHING, 30, 'Researching the topic...');
|
||||
await this.delay(2000); // Simulate research time
|
||||
log.success('Research completed');
|
||||
|
||||
// Step 5: Generate outline
|
||||
log.step('STEP 5', 'Generating course outline...');
|
||||
await this.updateProgress(generationId, GenerationStatus.GENERATING_OUTLINE, 50, 'Creating course structure...');
|
||||
|
||||
const outline = await this.aiProvider.generateCourseOutline(
|
||||
initialPrompt,
|
||||
(answers as Record<string, string | string[]>) || {},
|
||||
aiModel
|
||||
);
|
||||
|
||||
log.success(`Course outline generated: "${outline.title}"`);
|
||||
log.info(`Chapters: ${outline.chapters.length}`);
|
||||
|
||||
await prisma.courseGeneration.update({
|
||||
where: { id: generationId },
|
||||
data: {
|
||||
generatedOutline: outline as unknown as Record<string, unknown>,
|
||||
progress: 55,
|
||||
},
|
||||
});
|
||||
|
||||
// Step 6: Generate content
|
||||
log.step('STEP 6', 'Generating course content...');
|
||||
await this.updateProgress(generationId, GenerationStatus.GENERATING_CONTENT, 60, 'Writing course content...');
|
||||
|
||||
// Create course structure first
|
||||
log.info('Creating course structure in database...');
|
||||
const course = await this.createCourseFromOutline(userId, outline);
|
||||
log.success(`Course created with ID: ${course.id}`);
|
||||
|
||||
// Link course to generation
|
||||
await prisma.courseGeneration.update({
|
||||
where: { id: generationId },
|
||||
data: { courseId: course.id },
|
||||
});
|
||||
|
||||
// Generate content for each lesson
|
||||
const totalLessons = outline.chapters.reduce((acc, ch) => acc + ch.lessons.length, 0);
|
||||
let processedLessons = 0;
|
||||
|
||||
log.info(`Total lessons to generate: ${totalLessons}`);
|
||||
|
||||
for (const chapter of course.chapters) {
|
||||
log.info(`Processing chapter: "${chapter.title}" (${chapter.lessons.length} lessons)`);
|
||||
|
||||
for (const lesson of chapter.lessons) {
|
||||
const progress = 60 + Math.floor((processedLessons / totalLessons) * 35);
|
||||
log.progress(generationId, progress, `Writing: ${lesson.title}`);
|
||||
|
||||
await this.updateProgress(
|
||||
generationId,
|
||||
GenerationStatus.GENERATING_CONTENT,
|
||||
progress,
|
||||
`Writing: ${lesson.title}...`
|
||||
);
|
||||
|
||||
// Generate lesson content
|
||||
const lessonContent = await this.aiProvider.generateLessonContent(
|
||||
course.title,
|
||||
chapter.title,
|
||||
lesson.title,
|
||||
{
|
||||
difficulty: outline.difficulty,
|
||||
},
|
||||
aiModel
|
||||
);
|
||||
|
||||
// Update lesson with generated content
|
||||
await prisma.lesson.update({
|
||||
where: { id: lesson.id },
|
||||
data: {
|
||||
content: lessonContent.content as unknown as Record<string, unknown>,
|
||||
},
|
||||
});
|
||||
|
||||
processedLessons++;
|
||||
log.success(`Lesson ${processedLessons}/${totalLessons} completed: "${lesson.title}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// Increment user's course count
|
||||
log.info('Updating user subscription...');
|
||||
await prisma.subscription.update({
|
||||
where: { userId },
|
||||
data: {
|
||||
coursesCreatedThisMonth: {
|
||||
increment: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Step 7: Complete
|
||||
log.step('COMPLETE', 'Course generation finished!');
|
||||
await this.updateProgress(generationId, GenerationStatus.COMPLETED, 100, 'Course completed!');
|
||||
log.success(`Course "${course.title}" is ready!`);
|
||||
}
|
||||
|
||||
private async createCourseFromOutline(userId: string, outline: CourseOutline) {
|
||||
const slug = generateUniqueSlug(outline.title);
|
||||
|
||||
return prisma.course.create({
|
||||
data: {
|
||||
authorId: userId,
|
||||
title: outline.title,
|
||||
description: outline.description,
|
||||
slug,
|
||||
status: CourseStatus.DRAFT,
|
||||
difficulty: outline.difficulty,
|
||||
estimatedHours: outline.estimatedTotalHours,
|
||||
tags: outline.tags,
|
||||
chapters: {
|
||||
create: outline.chapters.map((chapter, chapterIndex) => ({
|
||||
title: chapter.title,
|
||||
description: chapter.description,
|
||||
order: chapterIndex,
|
||||
lessons: {
|
||||
create: chapter.lessons.map((lesson, lessonIndex) => ({
|
||||
title: lesson.title,
|
||||
order: lessonIndex,
|
||||
durationMinutes: lesson.estimatedMinutes,
|
||||
content: null, // Will be filled later
|
||||
})),
|
||||
},
|
||||
})),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
chapters: {
|
||||
include: {
|
||||
lessons: {
|
||||
orderBy: { order: 'asc' },
|
||||
},
|
||||
},
|
||||
orderBy: { order: 'asc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async updateProgress(
|
||||
generationId: string,
|
||||
status: GenerationStatus,
|
||||
progress: number,
|
||||
currentStep: string,
|
||||
additionalData?: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
await prisma.courseGeneration.update({
|
||||
where: { id: generationId },
|
||||
data: {
|
||||
status,
|
||||
progress,
|
||||
currentStep,
|
||||
...(additionalData?.errorMessage && { errorMessage: additionalData.errorMessage as string }),
|
||||
...(status === GenerationStatus.COMPLETED && { completedAt: new Date() }),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user