project init

This commit is contained in:
2026-02-06 02:17:59 +03:00
commit b9d9b9ed17
129 changed files with 22835 additions and 0 deletions

View File

@ -0,0 +1,108 @@
import dotenv from 'dotenv';
import path from 'path';
// Load .env from project root
dotenv.config({ path: path.resolve(__dirname, '../../../.env') });
import { Worker, Job } from 'bullmq';
import Redis from 'ioredis';
import { CourseGenerationPipeline } from './pipeline/course-generation.pipeline';
import { OpenRouterProvider } from './providers/openrouter.provider';
// Logger helper
const log = {
info: (msg: string) => console.log(`\x1b[36m[AI Service]\x1b[0m ${msg}`),
success: (msg: string) => console.log(`\x1b[32m[AI Service] ✓\x1b[0m ${msg}`),
error: (msg: string, error?: unknown) => console.error(`\x1b[31m[AI Service] ✗\x1b[0m ${msg}`, error || ''),
job: (action: string, job: Job) => {
console.log(`\x1b[33m[AI Service]\x1b[0m ${action}: ${job.name} (ID: ${job.id})`);
console.log(`\x1b[90m Data: ${JSON.stringify(job.data)}\x1b[0m`);
},
};
// Check environment variables
log.info('Checking environment variables...');
log.info(`REDIS_URL: ${process.env.REDIS_URL || 'redis://localhost:6395 (default)'}`);
log.info(`OPENROUTER_API_KEY: ${process.env.OPENROUTER_API_KEY ? 'SET' : 'NOT SET'}`);
log.info(`OPENROUTER_BASE_URL: ${process.env.OPENROUTER_BASE_URL || 'https://openrouter.ai/api/v1 (default)'}`);
const redisConnection = new Redis(process.env.REDIS_URL || 'redis://localhost:6395', {
maxRetriesPerRequest: null,
});
redisConnection.on('connect', () => log.success('Connected to Redis'));
redisConnection.on('error', (err) => log.error('Redis connection error', err));
let openRouterProvider: OpenRouterProvider;
let pipeline: CourseGenerationPipeline;
try {
openRouterProvider = new OpenRouterProvider();
pipeline = new CourseGenerationPipeline(openRouterProvider);
} catch (error) {
log.error('Failed to initialize AI providers', error);
process.exit(1);
}
// Worker for course generation
const worker = new Worker(
'course-generation',
async (job) => {
log.job('Processing job', job);
const startTime = Date.now();
try {
let result;
if (job.name === 'generate-course') {
const { generationId, prompt, aiModel } = job.data;
result = await pipeline.generateCourse(generationId, prompt, aiModel);
} else if (job.name === 'continue-generation') {
const { generationId, stage } = job.data;
result = await pipeline.continueGeneration(generationId, stage);
} else {
log.error(`Unknown job name: ${job.name}`);
throw new Error(`Unknown job name: ${job.name}`);
}
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
log.success(`Job ${job.name} completed in ${duration}s`);
return result;
} catch (error) {
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
log.error(`Job ${job.name} failed after ${duration}s`, error);
throw error;
}
},
{
connection: redisConnection,
concurrency: 5,
}
);
worker.on('completed', (job) => {
log.success(`Job ${job.id} completed successfully`);
});
worker.on('failed', (job, error) => {
log.error(`Job ${job?.id} failed: ${error.message}`);
if (error.stack) {
console.error(`\x1b[90m${error.stack}\x1b[0m`);
}
});
worker.on('error', (error) => {
log.error('Worker error', error);
});
console.log('');
console.log('🤖 ═══════════════════════════════════════════════════════');
console.log('🤖 AI Service started and listening for jobs...');
console.log('🤖 ═══════════════════════════════════════════════════════');
console.log('');
// Graceful shutdown
process.on('SIGTERM', async () => {
log.info('Shutting down AI Service...');
await worker.close();
await redisConnection.quit();
process.exit(0);
});

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

View File

@ -0,0 +1,393 @@
import OpenAI from 'openai';
import { z } from 'zod';
// Logger helper
const log = {
info: (msg: string, data?: unknown) => {
console.log(`\x1b[36m[AI Provider]\x1b[0m ${msg}`, data ? JSON.stringify(data, null, 2) : '');
},
success: (msg: string, data?: unknown) => {
console.log(`\x1b[32m[AI Provider] ✓\x1b[0m ${msg}`, data ? JSON.stringify(data, null, 2) : '');
},
error: (msg: string, error?: unknown) => {
console.error(`\x1b[31m[AI Provider] ✗\x1b[0m ${msg}`, error);
},
debug: (msg: string, data?: unknown) => {
if (process.env.DEBUG === 'true') {
console.log(`\x1b[90m[AI Provider DEBUG]\x1b[0m ${msg}`, data ? JSON.stringify(data, null, 2) : '');
}
},
request: (method: string, model: string) => {
console.log(`\x1b[33m[AI Provider] →\x1b[0m Calling ${method} with model: ${model}`);
},
response: (method: string, tokens?: { prompt?: number; completion?: number }) => {
const tokenInfo = tokens ? ` (tokens: ${tokens.prompt || '?'}/${tokens.completion || '?'})` : '';
console.log(`\x1b[33m[AI Provider] ←\x1b[0m ${method} completed${tokenInfo}`);
},
};
// Course outline schema for structured output
const CourseOutlineSchema = z.object({
title: z.string(),
description: z.string(),
chapters: z.array(
z.object({
title: z.string(),
description: z.string(),
lessons: z.array(
z.object({
title: z.string(),
estimatedMinutes: z.number(),
})
),
})
),
estimatedTotalHours: z.number(),
difficulty: z.enum(['beginner', 'intermediate', 'advanced']),
tags: z.array(z.string()),
});
const ClarifyingQuestionItemSchema = z.object({
id: z.string().optional(),
question: z.string().optional(),
text: z.string().optional(),
type: z.enum(['single_choice', 'multiple_choice', 'text']).optional(),
options: z.array(z.string()).optional(),
required: z.boolean().optional(),
}).refine((q) => typeof (q.question ?? q.text) === 'string', { message: 'question or text required' });
const ClarifyingQuestionsSchema = z.object({
questions: z.array(ClarifyingQuestionItemSchema).transform((items) =>
items.map((q, i) => ({
id: q.id ?? `q_${i}`,
question: (q.question ?? q.text ?? '').trim() || `Вопрос ${i + 1}`,
type: (q.type ?? 'text') as 'single_choice' | 'multiple_choice' | 'text',
options: q.options,
required: q.required ?? true,
}))
),
});
const LessonContentSchema = z.object({
content: z.object({
type: z.literal('doc'),
content: z.array(z.any()),
}),
});
export type CourseOutline = z.infer<typeof CourseOutlineSchema>;
export type ClarifyingQuestions = z.infer<typeof ClarifyingQuestionsSchema>;
export type LessonContent = z.infer<typeof LessonContentSchema>;
export class OpenRouterProvider {
private client: OpenAI;
private maxRetries = 3;
private retryDelay = 2000;
constructor() {
const apiKey = process.env.OPENROUTER_API_KEY;
const baseURL = process.env.OPENROUTER_BASE_URL || 'https://openrouter.ai/api/v1';
log.info('Initializing OpenRouter provider...');
log.info(`Base URL: ${baseURL}`);
log.info(`API Key: ${apiKey ? apiKey.substring(0, 15) + '...' : 'NOT SET'}`);
if (!apiKey) {
log.error('OPENROUTER_API_KEY environment variable is required');
throw new Error('OPENROUTER_API_KEY environment variable is required');
}
this.client = new OpenAI({
baseURL,
apiKey,
timeout: 120000, // 2 minute timeout
maxRetries: 3,
defaultHeaders: {
'HTTP-Referer': process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000',
'X-Title': 'CourseCraft',
},
});
log.success('OpenRouter provider initialized');
}
private async withRetry<T>(fn: () => Promise<T>, operation: string): Promise<T> {
let lastError: Error | null = null;
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
return await fn();
} catch (error: any) {
lastError = error;
log.error(`${operation} attempt ${attempt}/${this.maxRetries} failed: ${error.message}`);
if (attempt < this.maxRetries) {
const delay = this.retryDelay * attempt;
log.info(`Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw lastError;
}
async generateClarifyingQuestions(
prompt: string,
model: string
): Promise<ClarifyingQuestions> {
log.request('generateClarifyingQuestions', model);
log.info(`User prompt: "${prompt.substring(0, 100)}${prompt.length > 100 ? '...' : ''}"`);
const systemPrompt = `Ты - эксперт по созданию образовательных курсов.
Пользователь хочет создать курс. Твоя задача - задать уточняющие вопросы,
чтобы лучше понять его потребности и создать максимально релевантный курс.
Сгенерируй 3-5 вопросов. Обязательно включи вопрос про объём курса:
- Короткий (2-4 главы, введение в тему)
- Средний (4-7 глав, хорошее покрытие)
- Длинный / полный (6-12 глав, глубокое погружение)
Остальные вопросы: целевая аудитория, глубина материала, специфические темы.
Ответь в формате JSON.`;
return this.withRetry(async () => {
const response = await this.client.chat.completions.create({
model,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: `Запрос пользователя: "${prompt}"` },
],
response_format: { type: 'json_object' },
temperature: 0.7,
});
log.response('generateClarifyingQuestions', {
prompt: response.usage?.prompt_tokens,
completion: response.usage?.completion_tokens,
});
const content = response.choices[0].message.content;
log.debug('Raw AI response:', content);
if (!content) {
log.error('Empty response from AI');
throw new Error('Empty response from AI');
}
const parsed = JSON.parse(content);
const validated = ClarifyingQuestionsSchema.parse(parsed);
log.success(`Generated ${validated.questions.length} clarifying questions`);
log.info('Questions:', validated.questions.map(q => q.question));
return validated;
}, 'generateClarifyingQuestions');
}
async generateCourseOutline(
prompt: string,
answers: Record<string, string | string[]>,
model: string
): Promise<CourseOutline> {
log.request('generateCourseOutline', model);
log.info(`Prompt: "${prompt.substring(0, 100)}${prompt.length > 100 ? '...' : ''}"`);
log.info('User answers:', answers);
const systemPrompt = `Ты - эксперт по созданию образовательных курсов.
Создай структуру курса на основе запроса пользователя и его ответов на уточняющие вопросы.
ОБЪЁМ КУРСА (соблюдай строго по ответам пользователя):
- Если пользователь выбрал короткий курс / введение: 24 главы, в каждой 24 урока. estimatedTotalHours: 28.
- Если средний курс: 47 глав, в каждой 35 уроков. estimatedTotalHours: 820.
- Если длинный / полный курс: 612 глав, в каждой 48 уроков. estimatedTotalHours: 1540.
- Если объём не указан — предложи средний (46 глав по 35 уроков).
Укажи примерное время на каждый урок (estimatedMinutes: 1045). estimatedTotalHours = сумма уроков.
Определи сложность (beginner|intermediate|advanced). Добавь релевантные теги.
Ответь в формате JSON со структурой:
{
"title": "Название курса",
"description": "Подробное описание курса",
"chapters": [
{
"title": "Название главы",
"description": "Описание главы",
"lessons": [
{ "title": "Название урока", "estimatedMinutes": 25 }
]
}
],
"estimatedTotalHours": 20,
"difficulty": "beginner|intermediate|advanced",
"tags": ["тег1", "тег2"]
}`;
const userMessage = `Запрос: "${prompt}"
Ответы пользователя на уточняющие вопросы:
${Object.entries(answers)
.map(([key, value]) => `- ${key}: ${Array.isArray(value) ? value.join(', ') : value}`)
.join('\n')}`;
return this.withRetry(async () => {
const response = await this.client.chat.completions.create({
model,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userMessage },
],
response_format: { type: 'json_object' },
temperature: 0.7,
});
log.response('generateCourseOutline', {
prompt: response.usage?.prompt_tokens,
completion: response.usage?.completion_tokens,
});
const content = response.choices[0].message.content;
log.debug('Raw AI response:', content);
if (!content) {
log.error('Empty response from AI');
throw new Error('Empty response from AI');
}
const parsed = JSON.parse(content);
const validated = CourseOutlineSchema.parse(parsed);
const totalLessons = validated.chapters.reduce((acc, ch) => acc + ch.lessons.length, 0);
log.success(`Generated course outline: "${validated.title}"`);
log.info(`Structure: ${validated.chapters.length} chapters, ${totalLessons} lessons`);
log.info(`Difficulty: ${validated.difficulty}, Est. hours: ${validated.estimatedTotalHours}`);
return validated;
}, 'generateCourseOutline');
}
async generateLessonContent(
courseTitle: string,
chapterTitle: string,
lessonTitle: string,
context: {
difficulty: string;
targetAudience?: string;
},
model: string
): Promise<LessonContent> {
log.request('generateLessonContent', model);
log.info(`Generating content for: "${lessonTitle}" (Chapter: "${chapterTitle}")`);
const systemPrompt = `Ты - эксперт по созданию образовательного контента.
Пиши содержание урока СРАЗУ в форматированном виде — в формате TipTap JSON. Каждый блок контента должен быть правильным узлом TipTap (heading, paragraph, bulletList, orderedList, blockquote, codeBlock, image).
ФОРМАТИРОВАНИЕ (используй обязательно):
- Заголовки: { "type": "heading", "attrs": { "level": 1|2|3 }, "content": [{ "type": "text", "text": "..." }] }
- Параграфы: { "type": "paragraph", "content": [{ "type": "text", "text": "..." }] } — для выделения используй "marks": [{ "type": "bold" }] или [{ "type": "italic" }]
- Списки: bulletList > listItem > paragraph; orderedList > listItem > paragraph
- Цитаты: { "type": "blockquote", "content": [{ "type": "paragraph", "content": [...] }] }
- Код: { "type": "codeBlock", "attrs": { "language": "javascript"|"python"|"text" }, "content": [{ "type": "text", "text": "код" }] }
- Mermaid-диаграммы: { "type": "codeBlock", "attrs": { "language": "mermaid" }, "content": [{ "type": "text", "text": "graph LR\\n A --> B" }] } — вставляй где уместно (схемы, процессы, связи)
- Картинки не генерируй (src нельзя выдумывать); если нужна иллюстрация — опиши в тексте или предложи место для изображения
ОБЪЁМ: зависит от темы. Короткий урок: 300600 слов (35 блоков). Средний: 6001200 слов. Длинный: 12002500 слов. Структура: заголовок, введение, 24 секции с подзаголовками, примеры/код/списки, резюме.
Уровень: ${context.difficulty}. ${context.targetAudience ? `Аудитория: ${context.targetAudience}` : ''}
Ответь только валидным JSON:
{
"content": {
"type": "doc",
"content": [
{ "type": "heading", "attrs": { "level": 1 }, "content": [{ "type": "text", "text": "Заголовок" }] },
{ "type": "paragraph", "content": [{ "type": "text", "text": "Текст..." }] }
]
}
}`;
return this.withRetry(async () => {
const response = await this.client.chat.completions.create({
model,
messages: [
{ role: 'system', content: systemPrompt },
{
role: 'user',
content: `Курс: "${courseTitle}"
Глава: "${chapterTitle}"
Урок: "${lessonTitle}"
Создай содержание урока сразу в TipTap JSON: заголовки (heading), параграфы (paragraph), списки (bulletList/orderedList), цитаты (blockquote), блоки кода (codeBlock) и при необходимости Mermaid-диаграммы (codeBlock с "language": "mermaid"). Объём — по теме урока (короткий/средний/длинный).`,
},
],
response_format: { type: 'json_object' },
temperature: 0.7,
max_tokens: 8000,
});
log.response('generateLessonContent', {
prompt: response.usage?.prompt_tokens,
completion: response.usage?.completion_tokens,
});
const content = response.choices[0].message.content;
log.debug('Raw AI response:', content?.substring(0, 200) + '...');
if (!content) {
log.error('Empty response from AI');
throw new Error('Empty response from AI');
}
const parsed = JSON.parse(content);
const validated = LessonContentSchema.parse(parsed);
log.success(`Generated content for lesson: "${lessonTitle}"`);
return validated;
}, `generateLessonContent: ${lessonTitle}`);
}
async rewriteText(
text: string,
instruction: string,
model: string
): Promise<string> {
const response = await this.client.chat.completions.create({
model,
messages: [
{
role: 'system',
content:
'Ты - редактор образовательного контента. Переработай текст согласно инструкции пользователя.',
},
{
role: 'user',
content: `Исходный текст:
"${text}"
Инструкция: ${instruction}
Верни только переработанный текст без дополнительных пояснений.`,
},
],
temperature: 0.7,
});
return response.choices[0].message.content || text;
}
async chat(
messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>,
model: string
): Promise<string> {
const response = await this.client.chat.completions.create({
model,
messages,
temperature: 0.7,
});
return response.choices[0].message.content || '';
}
}