Files
course-craft-service/apps/ai-service/src/pipeline/course-generation.pipeline.ts
2026-02-06 14:53:52 +00:00

297 lines
9.6 KiB
TypeScript

import { prisma, 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 Prisma.InputJsonValue,
},
});
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 Prisma.InputJsonValue,
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 Prisma.InputJsonValue,
},
});
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,
groups: {
create: {
name: 'Основная группа',
description: 'Обсуждение курса и вопросы преподавателю',
isDefault: true,
},
},
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: Prisma.JsonNull, // 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 != null ? { errorMessage: additionalData.errorMessage as string } : {}),
...(status === GenerationStatus.COMPLETED ? { completedAt: new Date() } : {}),
},
});
}
private delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}