feat: end-to-end choice question support in assessment pipeline
- Data pathway: flow options through questions, answerKey in graph state - Interviewer: format MULTIPLE_CHOICE with A/B/C/D options - Grader: instant choice scoring (zero LLM), compare correctAnswer - AssessmentView: render choice buttons vs textarea based on questionType - Security: sanitizeStateForClient strips correctAnswer/judgment/answerKey - Bank detection: check PUBLISHED items (not PUBLISHED bank status) - Batch UI: select all / batch approve / batch reject on detail view
This commit is contained in:
@@ -230,6 +230,7 @@ ${existingQuestionsText ? `- Repeating previous questions: ${existingQuestionsTe
|
||||
return {
|
||||
id: (existingQuestions.length + 1).toString(),
|
||||
questionText: q.question_text,
|
||||
questionType: 'SHORT_ANSWER',
|
||||
keyPoints: q.key_points,
|
||||
difficulty: q.difficulty,
|
||||
basis: q.basis,
|
||||
|
||||
@@ -67,6 +67,31 @@ export const graderNode = async (
|
||||
return { currentQuestionIndex: currentQuestionIndex + 1 };
|
||||
}
|
||||
|
||||
const isChoice = currentQuestion.questionType === 'MULTIPLE_CHOICE';
|
||||
const answerKey = (state.questionAnswerKey as any)?.[currentQuestion.id];
|
||||
|
||||
if (isChoice || answerKey?.correctAnswer) {
|
||||
const expectedAnswer = answerKey?.correctAnswer || currentQuestion.correctAnswer;
|
||||
const userAnswer = (lastUserMessage.content as string).trim();
|
||||
const isCorrect = userAnswer.toUpperCase() === expectedAnswer?.toUpperCase();
|
||||
|
||||
console.log('[GraderNode] Choice grading:', { userAnswer, expectedAnswer, isCorrect });
|
||||
|
||||
const feedback = isCorrect ? '✅ 正确' : `❌ 错误,正确答案是 ${expectedAnswer}`;
|
||||
const feedbackMessage = new AIMessage(
|
||||
{ content: `Score: ${isCorrect ? 10 : 0}\nFeedback: ${feedback}` } as any,
|
||||
);
|
||||
|
||||
return {
|
||||
messages: [feedbackMessage],
|
||||
feedbackHistory: [feedbackMessage],
|
||||
scores: { [currentQuestion.id || currentQuestionIndex.toString()]: isCorrect ? 10 : 0 },
|
||||
shouldFollowUp: false,
|
||||
followUpCount: 0,
|
||||
currentQuestionIndex: currentQuestionIndex + 1,
|
||||
};
|
||||
}
|
||||
|
||||
const systemPromptZh = `你是一位专业的考官。
|
||||
请根据以下问题和关键点对用户的回答进行评分。
|
||||
|
||||
|
||||
@@ -33,8 +33,6 @@ export const interviewerNode = async (
|
||||
|
||||
const currentQuestion = questions[currentQuestionIndex];
|
||||
|
||||
// If it's a follow-up, we add a prefix to the label later.
|
||||
// If we've run out of questions and no follow-up requested, we shouldn't be here, but let's be safe.
|
||||
if (currentQuestionIndex >= questions.length) {
|
||||
return { shouldFollowUp: false };
|
||||
}
|
||||
@@ -49,12 +47,10 @@ export const interviewerNode = async (
|
||||
state.feedbackHistory &&
|
||||
state.feedbackHistory.length > 0
|
||||
) {
|
||||
// Construct a follow-up prompt based on last feedback
|
||||
const lastFeedbackMsg =
|
||||
state.feedbackHistory[state.feedbackHistory.length - 1];
|
||||
const feedbackText = lastFeedbackMsg.content.toString();
|
||||
|
||||
// Extract the "Feedback: ..." part if possible, otherwise use whole text
|
||||
const feedbackMatch = feedbackText.match(
|
||||
/(?:Feedback|反馈|フィードバック): ([\s\S]*)/i,
|
||||
);
|
||||
@@ -74,8 +70,22 @@ export const interviewerNode = async (
|
||||
: 'Based on the feedback above, please provide more specific details:';
|
||||
|
||||
prompt = `${followUpLabel}\n\n${specificFeedback}\n\n${followUpInstruction}`;
|
||||
} else if (currentQuestion.questionType === 'MULTIPLE_CHOICE' && currentQuestion.options?.length > 0) {
|
||||
const label = isZh
|
||||
? `问题 ${currentQuestionIndex + 1}`
|
||||
: isJa
|
||||
? `質問 ${currentQuestionIndex + 1}`
|
||||
: `Question ${currentQuestionIndex + 1}`;
|
||||
|
||||
const optionsText = currentQuestion.options.join('\n');
|
||||
const instruction = isZh
|
||||
? '请选择一个选项(输入字母 A/B/C/D)'
|
||||
: isJa
|
||||
? '選択肢から1つ選んでください(A/B/C/Dを入力)'
|
||||
: 'Please select one option (enter A, B, C, or D)';
|
||||
|
||||
prompt = `${label}: ${currentQuestion.questionText}\n\n${optionsText}\n\n${instruction}`;
|
||||
} else {
|
||||
// Standard question presentation
|
||||
const label = isZh
|
||||
? `问题 ${currentQuestionIndex + 1}`
|
||||
: isJa
|
||||
|
||||
@@ -119,6 +119,15 @@ export const EvaluationAnnotation = Annotation.Root({
|
||||
keywords: Annotation<string[] | undefined>({
|
||||
reducer: (prev, next) => next ?? prev,
|
||||
}),
|
||||
|
||||
/**
|
||||
* Answer key for bank questions: id → { correctAnswer, judgment, followupHints }.
|
||||
* Used by grader for instant choice scoring and open-question anchoring.
|
||||
* NOT sent to frontend.
|
||||
*/
|
||||
questionAnswerKey: Annotation<Record<string, any> | undefined>({
|
||||
reducer: (prev, next) => next ?? prev,
|
||||
}),
|
||||
});
|
||||
|
||||
export type EvaluationState = typeof EvaluationAnnotation.State;
|
||||
|
||||
Reference in New Issue
Block a user