diff --git a/src/config/index.ts b/src/config/index.ts index 9e173ad..e405919 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -29,6 +29,9 @@ const envSchema = z.object({ // Rate limiting RATE_LIMIT_MAX: z.coerce.number().default(100), RATE_LIMIT_WINDOW_MS: z.coerce.number().default(60_000), + + // AI service (chainlearn-ai) used for quiz generation + AI_SERVICE_URL: z.string().url().default("http://localhost:8000"), }); export type Env = z.infer; diff --git a/src/modules/quizzes/ai-client.ts b/src/modules/quizzes/ai-client.ts new file mode 100644 index 0000000..58f0acb --- /dev/null +++ b/src/modules/quizzes/ai-client.ts @@ -0,0 +1,50 @@ +import { config } from "../../config/index.js"; +import { logger } from "../../utils/logger.js"; + +interface AiQuizQuestion { + prompt: string; + options: string[]; + correct_index: number; +} + +interface AiQuizResponse { + quiz_id: string; + questions: AiQuizQuestion[]; +} + +export type AiDifficulty = "beginner" | "intermediate" | "advanced"; + +export interface GenerateQuizFromAIParams { + userId: string; + courseId: string; + moduleId: string; + difficulty: AiDifficulty; + numQuestions: number; +} + +export async function generateQuizFromAI( + params: GenerateQuizFromAIParams +): Promise { + const response = await fetch(`${config.AI_SERVICE_URL}/generate-quiz`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + user_id: params.userId, + course_id: params.courseId, + module_id: params.moduleId, + difficulty: params.difficulty, + num_questions: params.numQuestions, + }), + }); + + if (!response.ok) { + logger.error( + { status: response.status }, + "AI service quiz generation failed" + ); + throw new Error(`AI service returned ${response.status}`); + } + + const data = (await response.json()) as AiQuizResponse; + return data.questions; +} diff --git a/src/modules/quizzes/quiz.service.ts b/src/modules/quizzes/quiz.service.ts index 9396e82..c59b586 100644 --- a/src/modules/quizzes/quiz.service.ts +++ b/src/modules/quizzes/quiz.service.ts @@ -5,6 +5,7 @@ import { NotFoundError, ForbiddenError, ConflictError } from "../../utils/errors import { withLock } from "../../utils/lock.js"; import { createQuizProof } from "../../stellar/signatures.js"; import { logger } from "../../utils/logger.js"; +import { generateQuizFromAI } from "./ai-client.js"; import type { GenerateQuizBody, SubmitQuizBody, @@ -17,9 +18,10 @@ const PASSING_PERCENTAGE = 70; export class QuizService { /** - * Generate a quiz for a given course/module. - * In a real system this would call an AI service; here we return - * a pre-generated quiz or fetch from the database. + * Generate a quiz for a given course/module. Calls the chainlearn-ai + * service for fresh questions and falls back to a fixed placeholder set + * if the service is unreachable. Existing quizzes for the same user + + * module are returned as-is so a refresh doesn't regenerate. */ async generateQuiz( userId: string, @@ -64,11 +66,34 @@ export class QuizService { }; } - // Generate new quiz (placeholder — would call AI service) - const generatedQuestions = this.createPlaceholderQuestions( - data.courseId, - data.moduleId - ); + // Generate new quiz via the chainlearn-ai service. Fall back to the + // hardcoded placeholder set if the AI service is unreachable so the + // dashboard never breaks on a transient outage. + let generatedQuestions; + try { + const aiQuestions = await generateQuizFromAI({ + userId, + courseId: data.courseId, + moduleId: data.moduleId, + difficulty: data.difficulty ?? "beginner", + numQuestions: data.numQuestions ?? 5, + }); + generatedQuestions = aiQuestions.map((q, i) => ({ + id: `q${i + 1}`, + text: q.prompt, + options: q.options, + correctIndex: q.correct_index, + })); + } catch (err) { + logger.warn( + { err }, + "AI service unavailable, falling back to placeholder questions" + ); + generatedQuestions = this.createPlaceholderQuestions( + data.courseId, + data.moduleId + ); + } const [quiz] = await db .insert(quizzes) diff --git a/src/modules/quizzes/quiz.types.ts b/src/modules/quizzes/quiz.types.ts index c1b44b8..41157a8 100644 --- a/src/modules/quizzes/quiz.types.ts +++ b/src/modules/quizzes/quiz.types.ts @@ -5,6 +5,8 @@ import { z } from "zod"; export const generateQuizSchema = z.object({ courseId: z.string().uuid("Invalid course ID"), moduleId: z.string().min(1, "Module ID is required"), + difficulty: z.enum(["beginner", "intermediate", "advanced"]).optional(), + numQuestions: z.coerce.number().int().min(1).max(20).optional(), }); export const submitQuizSchema = z.object({