grader.node.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. import { ChatOpenAI } from "@langchain/openai";
  2. import { SystemMessage, HumanMessage, AIMessage } from "@langchain/core/messages";
  3. import { RunnableConfig } from "@langchain/core/runnables";
  4. import { EvaluationState } from "../state";
  5. /**
  6. * Node responsible for grading the user's answer and deciding if a follow-up is needed.
  7. */
  8. export const graderNode = async (
  9. state: EvaluationState,
  10. config?: RunnableConfig
  11. ): Promise<Partial<EvaluationState>> => {
  12. const { model } = (config?.configurable as any) || {};
  13. const { questions, currentQuestionIndex, messages } = state;
  14. console.log("[GraderNode] Entering node...", {
  15. currentIndex: currentQuestionIndex,
  16. numMessages: messages?.length
  17. });
  18. if (!model) {
  19. throw new Error("Missing model in node configuration");
  20. }
  21. const currentQuestion = questions[currentQuestionIndex];
  22. const lastUserMessage = messages[messages.length - 1];
  23. if (!(lastUserMessage instanceof HumanMessage)) {
  24. return {};
  25. }
  26. const systemPrompt = `You are an expert examiner.
  27. Grade the user's answer based on the following question and key points.
  28. IMPORTANT: You MUST provide the feedback in the following language: ${state.language || 'zh'}.
  29. QUESTION: ${currentQuestion.questionText}
  30. EXPECTED KEY POINTS: ${currentQuestion.keyPoints.join(", ")}
  31. Evaluate:
  32. 1. Accuracy: Did they cover the key points correctly?
  33. 2. Completeness: Did they miss anything important?
  34. 3. Depth: Is the explanation sufficient?
  35. Provide:
  36. 1. A score from 0 to 10.
  37. 2. Constructive feedback.
  38. 3. A boolean flag 'should_follow_up' if the answer is incomplete or unclear and needs further clarification.
  39. Format your response as JSON:
  40. {
  41. "score": 8,
  42. "feedback": "...",
  43. "should_follow_up": false
  44. }`;
  45. const response = await model.invoke([
  46. new SystemMessage(systemPrompt),
  47. new HumanMessage((lastUserMessage as HumanMessage).content as string),
  48. ]);
  49. try {
  50. const result = JSON.parse(response.content as string);
  51. console.log("[GraderNode] AI Grade Result:", result);
  52. const isZh = state.language === 'zh';
  53. const isJa = state.language === 'ja';
  54. const scoreLabel = isZh ? '得分' : isJa ? 'スコア' : 'Score';
  55. const feedbackLabel = isZh ? '反馈' : isJa ? 'フィードバック' : 'Feedback';
  56. const feedbackMessage = new AIMessage(`${scoreLabel}: ${result.score}/10\n\n${feedbackLabel}: ${result.feedback}`);
  57. const newScores = { ...state.scores, [currentQuestion.id || currentQuestionIndex]: result.score };
  58. let shouldFollowUp = result.should_follow_up;
  59. const currentFollowUpCount = state.followUpCount || 0;
  60. // Breakout logic:
  61. // 1. Max 1 follow-up per question
  62. // 2. If score is decent (>= 8), don't follow up
  63. // 3. If answer is short "don't know", don't follow up
  64. const userContent = (lastUserMessage.content as string).trim().toLowerCase();
  65. const saysIDontKnow = userContent.length < 10 && (
  66. userContent.includes("不知道") ||
  67. userContent.includes("不会") ||
  68. userContent.includes("don't know") ||
  69. userContent.includes("no idea")
  70. );
  71. if (currentFollowUpCount >= 1 || result.score >= 8 || saysIDontKnow) {
  72. shouldFollowUp = false;
  73. }
  74. return {
  75. feedbackHistory: [feedbackMessage],
  76. scores: newScores,
  77. shouldFollowUp: shouldFollowUp,
  78. followUpCount: shouldFollowUp ? currentFollowUpCount + 1 : 0,
  79. currentQuestionIndex: shouldFollowUp ? currentQuestionIndex : currentQuestionIndex + 1,
  80. };
  81. } catch (error) {
  82. console.error("Failed to parse grade from AI response:", error);
  83. return {
  84. feedbackHistory: [new AIMessage("I had some trouble grading that, but let's move on.")],
  85. currentQuestionIndex: currentQuestionIndex + 1,
  86. };
  87. }
  88. };