route.ts 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. /**
  2. * Quiz Grading API
  3. *
  4. * POST: Receives a text question + user answer, calls LLM for scoring and feedback.
  5. * Used for short-answer (text) questions that cannot be graded locally.
  6. */
  7. import { NextRequest } from 'next/server';
  8. import { callLLM } from '@/lib/ai/llm';
  9. import { createLogger } from '@/lib/logger';
  10. import { apiError, apiSuccess } from '@/lib/server/api-response';
  11. import { resolveModelFromHeaders } from '@/lib/server/resolve-model';
  12. const log = createLogger('Quiz Grade');
  13. interface GradeRequest {
  14. question: string;
  15. userAnswer: string;
  16. points: number;
  17. commentPrompt?: string;
  18. language?: string;
  19. }
  20. interface GradeResponse {
  21. score: number;
  22. comment: string;
  23. }
  24. export async function POST(req: NextRequest) {
  25. let questionSnippet: string | undefined;
  26. let resolvedPoints: number | undefined;
  27. try {
  28. const body = (await req.json()) as GradeRequest;
  29. const { question, userAnswer, points, commentPrompt, language } = body;
  30. questionSnippet = question?.substring(0, 60);
  31. resolvedPoints = points;
  32. if (!question || !userAnswer) {
  33. return apiError('MISSING_REQUIRED_FIELD', 400, 'question and userAnswer are required');
  34. }
  35. // Validate points is a positive finite number
  36. if (!points || !Number.isFinite(points) || points <= 0) {
  37. return apiError('INVALID_REQUEST', 400, 'points must be a positive number');
  38. }
  39. // Resolve model from request headers
  40. const { model: languageModel } = await resolveModelFromHeaders(req);
  41. const isZh = language === 'zh-CN';
  42. const systemPrompt = isZh
  43. ? `你是一位专业的教育评估专家。请根据题目和学生答案进行评分并给出简短评语。
  44. 必须以如下 JSON 格式回复(不要包含其他内容):
  45. {"score": <0到${points}的整数>, "comment": "<一两句评语>"}`
  46. : `You are a professional educational assessor. Grade the student's answer and provide brief feedback.
  47. You must reply in the following JSON format only (no other content):
  48. {"score": <integer from 0 to ${points}>, "comment": "<one or two sentences of feedback>"}`;
  49. const userPrompt = isZh
  50. ? `题目:${question}
  51. 满分:${points}分
  52. ${commentPrompt ? `评分要点:${commentPrompt}\n` : ''}学生答案:${userAnswer}`
  53. : `Question: ${question}
  54. Full marks: ${points} points
  55. ${commentPrompt ? `Grading guidance: ${commentPrompt}\n` : ''}Student answer: ${userAnswer}`;
  56. const result = await callLLM(
  57. {
  58. model: languageModel,
  59. system: systemPrompt,
  60. prompt: userPrompt,
  61. },
  62. 'quiz-grade',
  63. );
  64. // Parse the LLM response as JSON
  65. const text = result.text.trim();
  66. let gradeResult: GradeResponse;
  67. try {
  68. // Try to extract JSON from the response
  69. const jsonMatch = text.match(/\{[\s\S]*\}/);
  70. if (!jsonMatch) throw new Error('No JSON found');
  71. const parsed = JSON.parse(jsonMatch[0]);
  72. gradeResult = {
  73. score: Math.max(0, Math.min(points, Math.round(Number(parsed.score)))),
  74. comment: String(parsed.comment || ''),
  75. };
  76. } catch {
  77. // Fallback: give partial credit with a generic comment
  78. gradeResult = {
  79. score: Math.round(points * 0.5),
  80. comment: isZh
  81. ? '已作答,请参考标准答案。'
  82. : 'Answer received. Please refer to the standard answer.',
  83. };
  84. }
  85. return apiSuccess({ ...gradeResult });
  86. } catch (error) {
  87. log.error(
  88. `Quiz grading failed [question="${questionSnippet ?? 'unknown'}...", points=${resolvedPoints ?? 'unknown'}]:`,
  89. error,
  90. );
  91. return apiError('INTERNAL_ERROR', 500, 'Failed to grade answer');
  92. }
  93. }