feat: use student questions for AI help

main
chenyuan 3 weeks ago
parent 01eaacc32c
commit c6e0a328b2

@ -3,6 +3,7 @@ package com.sztzjy.linkCommerce.controller.stu;
import com.sztzjy.linkCommerce.config.exception.handler.ServiceException; import com.sztzjy.linkCommerce.config.exception.handler.ServiceException;
import com.sztzjy.linkCommerce.config.security.JwtUser; import com.sztzjy.linkCommerce.config.security.JwtUser;
import com.sztzjy.linkCommerce.config.security.TokenProvider; import com.sztzjy.linkCommerce.config.security.TokenProvider;
import com.sztzjy.linkCommerce.entity.dto.AiTrainingHelpRequest;
import com.sztzjy.linkCommerce.entity.dto.AiTrainingEvaluationView; import com.sztzjy.linkCommerce.entity.dto.AiTrainingEvaluationView;
import com.sztzjy.linkCommerce.service.AiTrainingEvaluationService; import com.sztzjy.linkCommerce.service.AiTrainingEvaluationService;
import com.sztzjy.linkCommerce.util.ResultEntity; import com.sztzjy.linkCommerce.util.ResultEntity;
@ -13,6 +14,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@ -38,10 +40,12 @@ public class AiTrainingEvaluationController {
@PostMapping("/help") @PostMapping("/help")
@ApiOperation("Generate AI learning help") @ApiOperation("Generate AI learning help")
public ResultEntity<AiTrainingEvaluationView> help(@PathVariable String taskKey, HttpServletRequest request) { public ResultEntity<AiTrainingEvaluationView> help(@PathVariable String taskKey,
@RequestBody(required = false) AiTrainingHelpRequest body,
HttpServletRequest request) {
try { try {
return new ResultEntity<>(HttpStatus.OK, "AI help generated", return new ResultEntity<>(HttpStatus.OK, "AI help generated",
aiTrainingEvaluationService.generateHelp(taskKey, currentUser(request))); aiTrainingEvaluationService.generateHelp(taskKey, currentUser(request), body == null ? null : body.getQuestion()));
} catch (ServiceException e) { } catch (ServiceException e) {
return new ResultEntity<>(e.getCode(), e.getMessage()); return new ResultEntity<>(e.getCode(), e.getMessage());
} }

@ -0,0 +1,20 @@
package com.sztzjy.linkCommerce.entity.dto;
public class AiTrainingHelpRequest {
private String question;
public AiTrainingHelpRequest() {
}
public AiTrainingHelpRequest(String question) {
this.question = question;
}
public String getQuestion() {
return question;
}
public void setQuestion(String question) {
this.question = question;
}
}

@ -6,7 +6,7 @@ import com.sztzjy.linkCommerce.entity.dto.AiTrainingEvaluationView;
public interface AiTrainingEvaluationService { public interface AiTrainingEvaluationService {
AiTrainingEvaluationView get(String taskKey, JwtUser user); AiTrainingEvaluationView get(String taskKey, JwtUser user);
AiTrainingEvaluationView generateHelp(String taskKey, JwtUser user); AiTrainingEvaluationView generateHelp(String taskKey, JwtUser user, String question);
AiTrainingEvaluationView generateAssessment(String taskKey, JwtUser user); AiTrainingEvaluationView generateAssessment(String taskKey, JwtUser user);
} }

@ -33,6 +33,7 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
private static final String PROCESSING = "PROCESSING"; private static final String PROCESSING = "PROCESSING";
private static final String SUCCEEDED = "SUCCEEDED"; private static final String SUCCEEDED = "SUCCEEDED";
private static final int MAX_ERROR_LENGTH = 1000; private static final int MAX_ERROR_LENGTH = 1000;
private static final int MAX_HELP_QUESTION_LENGTH = 500;
private final ObjectMapper objectMapper = new ObjectMapper(); private final ObjectMapper objectMapper = new ObjectMapper();
@ -58,10 +59,11 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
} }
@Override @Override
public AiTrainingEvaluationView generateHelp(String taskKey, JwtUser user) { public AiTrainingEvaluationView generateHelp(String taskKey, JwtUser user, String question) {
String normalizedQuestion = normalizeHelpQuestion(question);
EvaluationContext context = resolveContext(taskKey, user, true); EvaluationContext context = resolveContext(taskKey, user, true);
AiTrainingEvaluation evaluation = getOrCreate(context, user); AiTrainingEvaluation evaluation = getOrCreate(context, user);
if (isTerminalOrProcessing(evaluation.getHelpStatus())) { if (PROCESSING.equals(evaluation.getHelpStatus())) {
return toView(evaluation); return toView(evaluation);
} }
if (aiTrainingEvaluationMapper.claimHelp(evaluation.getId(), new Date()) != 1) { if (aiTrainingEvaluationMapper.claimHelp(evaluation.getId(), new Date()) != 1) {
@ -69,9 +71,9 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
} }
String taskSnapshot = taskSnapshot(context.task); String taskSnapshot = taskSnapshot(context.task);
String answerSnapshot = answerSnapshot(context.answer); String answerSnapshot = helpAnswerSnapshot(normalizedQuestion, context.answer);
try { try {
String rawResponse = qwenChatClient.completeJson(helpSystemPrompt(), helpUserPrompt(taskSnapshot, answerSnapshot)); String rawResponse = qwenChatClient.completeJson(helpSystemPrompt(), helpUserPrompt(taskSnapshot, answerSnapshot, normalizedQuestion));
validateHelp(rawResponse); validateHelp(rawResponse);
Date completedAt = new Date(); Date completedAt = new Date();
AiTrainingEvaluation completed = new AiTrainingEvaluation(); AiTrainingEvaluation completed = new AiTrainingEvaluation();
@ -230,6 +232,17 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
return SUCCEEDED.equals(status) || PROCESSING.equals(status); return SUCCEEDED.equals(status) || PROCESSING.equals(status);
} }
private String normalizeHelpQuestion(String question) {
String normalized = StringUtils.trimToEmpty(question);
if (StringUtils.isBlank(normalized)) {
throw new ServiceException(HttpStatus.BAD_REQUEST, "AI help question is required");
}
if (normalized.length() > MAX_HELP_QUESTION_LENGTH) {
throw new ServiceException(HttpStatus.BAD_REQUEST, "AI help question is too long");
}
return normalized;
}
private String taskSnapshot(TrainingTask task) { private String taskSnapshot(TrainingTask task) {
ObjectNode node = objectMapper.createObjectNode(); ObjectNode node = objectMapper.createObjectNode();
node.put("background", task.getBackground()); node.put("background", task.getBackground());
@ -248,6 +261,13 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
return json(node); return json(node);
} }
private String helpAnswerSnapshot(String question, StudentTrainingAnswer answer) {
ObjectNode node = objectMapper.createObjectNode();
node.put("studentQuestion", question);
node.put("studentAnswers", answerSnapshot(answer));
return json(node);
}
private String json(JsonNode node) { private String json(JsonNode node) {
try { try {
return objectMapper.writeValueAsString(node); return objectMapper.writeValueAsString(node);
@ -331,8 +351,8 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
return "You are a training learning assistant. Return only JSON with overallDiagnosis, strengths, improvementAreas, and recommendedActions. Do not include a score."; return "You are a training learning assistant. Return only JSON with overallDiagnosis, strengths, improvementAreas, and recommendedActions. Do not include a score.";
} }
private String helpUserPrompt(String taskSnapshot, String answerSnapshot) { private String helpUserPrompt(String taskSnapshot, String answerSnapshot, String question) {
return "Task snapshot: " + taskSnapshot + "\nStudent answer snapshot: " + answerSnapshot; return "Student question: " + question + "\nTask snapshot: " + taskSnapshot + "\nStudent answer snapshot: " + answerSnapshot;
} }
private String assessmentSystemPrompt() { private String assessmentSystemPrompt() {

@ -61,7 +61,7 @@
UPDATE ai_training_evaluation UPDATE ai_training_evaluation
SET help_status = 'PROCESSING', help_error_message = NULL, update_time = #{updateTime,jdbcType=TIMESTAMP} SET help_status = 'PROCESSING', help_error_message = NULL, update_time = #{updateTime,jdbcType=TIMESTAMP}
WHERE id = #{id,jdbcType=VARCHAR} WHERE id = #{id,jdbcType=VARCHAR}
AND help_status IN ('NOT_STARTED', 'FAILED') AND help_status IN ('NOT_STARTED', 'FAILED', 'SUCCEEDED')
</update> </update>
<update id="claimAssessment"> <update id="claimAssessment">

@ -2,6 +2,7 @@ package com.sztzjy.linkCommerce.controller.stu;
import com.sztzjy.linkCommerce.config.exception.handler.ServiceException; import com.sztzjy.linkCommerce.config.exception.handler.ServiceException;
import com.sztzjy.linkCommerce.config.security.JwtUser; import com.sztzjy.linkCommerce.config.security.JwtUser;
import com.sztzjy.linkCommerce.entity.dto.AiTrainingHelpRequest;
import com.sztzjy.linkCommerce.entity.dto.AiTrainingEvaluationView; import com.sztzjy.linkCommerce.entity.dto.AiTrainingEvaluationView;
import com.sztzjy.linkCommerce.service.AiTrainingEvaluationService; import com.sztzjy.linkCommerce.service.AiTrainingEvaluationService;
import com.sztzjy.linkCommerce.util.ResultEntity; import com.sztzjy.linkCommerce.util.ResultEntity;
@ -23,13 +24,13 @@ class AiTrainingEvaluationControllerTest {
controller.aiTrainingEvaluationService = mock(AiTrainingEvaluationService.class); controller.aiTrainingEvaluationService = mock(AiTrainingEvaluationService.class);
AiTrainingEvaluationView view = new AiTrainingEvaluationView(); AiTrainingEvaluationView view = new AiTrainingEvaluationView();
view.setHelpStatus("SUCCEEDED"); view.setHelpStatus("SUCCEEDED");
when(controller.aiTrainingEvaluationService.generateHelp("task-key", controller.user)).thenReturn(view); when(controller.aiTrainingEvaluationService.generateHelp("task-key", controller.user, "如何区分核心层和有形层?")).thenReturn(view);
ResultEntity<AiTrainingEvaluationView> result = controller.help("task-key", mock(HttpServletRequest.class)); ResultEntity<AiTrainingEvaluationView> result = controller.help("task-key", new AiTrainingHelpRequest("如何区分核心层和有形层?"), mock(HttpServletRequest.class));
assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals(HttpStatus.OK, result.getStatusCode());
assertEquals("SUCCEEDED", result.getBody().getData().getHelpStatus()); assertEquals("SUCCEEDED", result.getBody().getData().getHelpStatus());
verify(controller.aiTrainingEvaluationService).generateHelp("task-key", controller.user); verify(controller.aiTrainingEvaluationService).generateHelp("task-key", controller.user, "如何区分核心层和有形层?");
} }
@Test @Test

@ -42,7 +42,16 @@ class AiTrainingEvaluationServiceImplTest {
void rejectsBlankAllStepAnswers() { void rejectsBlankAllStepAnswers() {
AiTrainingEvaluationServiceImpl service = serviceWithContext(answer(null, " ", "", "\t", null)); AiTrainingEvaluationServiceImpl service = serviceWithContext(answer(null, " ", "", "\t", null));
assertThrows(ServiceException.class, () -> service.generateHelp("new-product-survey", student())); assertThrows(ServiceException.class, () -> service.generateHelp("new-product-survey", student(), "请给我建议"));
verify(service.qwenChatClient, never()).completeJson(any(), any());
}
@Test
void rejectsBlankHelpQuestionBeforeCallingQwen() {
AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null));
assertThrows(ServiceException.class, () -> service.generateHelp("new-product-survey", student(), " "));
verify(service.qwenChatClient, never()).completeJson(any(), any()); verify(service.qwenChatClient, never()).completeJson(any(), any());
} }
@ -56,7 +65,7 @@ class AiTrainingEvaluationServiceImplTest {
when(service.aiTrainingEvaluationMapper.claimHelp(eq("evaluation-1"), any(Date.class))).thenReturn(1); when(service.aiTrainingEvaluationMapper.claimHelp(eq("evaluation-1"), any(Date.class))).thenReturn(1);
when(service.qwenChatClient.completeJson(any(), any())).thenReturn(helpJson()); when(service.qwenChatClient.completeJson(any(), any())).thenReturn(helpJson());
AiTrainingEvaluationView view = service.generateHelp("new-product-survey", student()); AiTrainingEvaluationView view = service.generateHelp("new-product-survey", student(), "如何完善调研证据?");
ArgumentCaptor<AiTrainingEvaluation> saved = ArgumentCaptor.forClass(AiTrainingEvaluation.class); ArgumentCaptor<AiTrainingEvaluation> saved = ArgumentCaptor.forClass(AiTrainingEvaluation.class);
verify(service.aiTrainingEvaluationMapper).completeHelp(saved.capture()); verify(service.aiTrainingEvaluationMapper).completeHelp(saved.capture());
@ -65,9 +74,26 @@ class AiTrainingEvaluationServiceImplTest {
assertTrue(saved.getValue().getHelpTaskSnapshot().contains("objective one")); assertTrue(saved.getValue().getHelpTaskSnapshot().contains("objective one"));
assertTrue(saved.getValue().getHelpTaskSnapshot().contains("task requirement")); assertTrue(saved.getValue().getHelpTaskSnapshot().contains("task requirement"));
assertTrue(saved.getValue().getHelpAnswerSnapshot().contains("step four")); assertTrue(saved.getValue().getHelpAnswerSnapshot().contains("step four"));
assertTrue(saved.getValue().getHelpAnswerSnapshot().contains("如何完善调研证据?"));
assertEquals(helpJson(), saved.getValue().getHelpReportJson()); assertEquals(helpJson(), saved.getValue().getHelpReportJson());
} }
@Test
void helpPromptIncludesStudentQuestion() {
AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null));
AiTrainingEvaluation evaluation = evaluation("NOT_STARTED", "NOT_STARTED");
when(service.aiTrainingEvaluationMapper.findByStudentClassAndTask("stu-1", "class-1", "task-1"))
.thenReturn(null, evaluation);
when(service.aiTrainingEvaluationMapper.claimHelp(eq("evaluation-1"), any(Date.class))).thenReturn(1);
when(service.qwenChatClient.completeJson(any(), any())).thenReturn(helpJson());
service.generateHelp("new-product-survey", student(), "如何区分核心层和有形层?");
ArgumentCaptor<String> userPrompt = ArgumentCaptor.forClass(String.class);
verify(service.qwenChatClient).completeJson(any(), userPrompt.capture());
assertTrue(userPrompt.getValue().contains("Student question: 如何区分核心层和有形层?"));
}
@Test @Test
void assessmentSuccessWritesScoreToAnswer() { void assessmentSuccessWritesScoreToAnswer() {
AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null)); AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null));
@ -138,17 +164,20 @@ class AiTrainingEvaluationServiceImplTest {
} }
@Test @Test
void succeededHelpReturnsStoredReportWithoutCallingQwenAgain() { void succeededHelpCanBeRegeneratedForANewQuestion() {
AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null)); AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null));
AiTrainingEvaluation evaluation = evaluation("SUCCEEDED", "NOT_STARTED"); AiTrainingEvaluation evaluation = evaluation("SUCCEEDED", "NOT_STARTED");
evaluation.setHelpReportJson(helpJson()); evaluation.setHelpReportJson(helpJson());
when(service.aiTrainingEvaluationMapper.findByStudentClassAndTask("stu-1", "class-1", "task-1")).thenReturn(evaluation); when(service.aiTrainingEvaluationMapper.findByStudentClassAndTask("stu-1", "class-1", "task-1")).thenReturn(evaluation);
AiTrainingEvaluationView view = service.generateHelp("new-product-survey", student()); when(service.aiTrainingEvaluationMapper.claimHelp(eq("evaluation-1"), any(Date.class))).thenReturn(1);
when(service.qwenChatClient.completeJson(any(), any())).thenReturn(helpJson());
AiTrainingEvaluationView view = service.generateHelp("new-product-survey", student(), "请根据新问题给建议");
assertEquals("SUCCEEDED", view.getHelpStatus()); assertEquals("SUCCEEDED", view.getHelpStatus());
assertEquals(helpJson(), view.getHelpReportJson()); verify(service.qwenChatClient).completeJson(any(), any());
verify(service.qwenChatClient, never()).completeJson(any(), any()); verify(service.aiTrainingEvaluationMapper).completeHelp(any(AiTrainingEvaluation.class));
} }
@Test @Test
@ -159,7 +188,7 @@ class AiTrainingEvaluationServiceImplTest {
when(service.aiTrainingEvaluationMapper.claimHelp(eq("evaluation-1"), any(Date.class))).thenReturn(1); when(service.aiTrainingEvaluationMapper.claimHelp(eq("evaluation-1"), any(Date.class))).thenReturn(1);
when(service.qwenChatClient.completeJson(any(), any())).thenReturn(helpJson()); when(service.qwenChatClient.completeJson(any(), any())).thenReturn(helpJson());
service.generateHelp("new-product-survey", student()); service.generateHelp("new-product-survey", student(), "请给出改进建议");
verify(service.qwenChatClient).completeJson(any(), any()); verify(service.qwenChatClient).completeJson(any(), any());
verify(service.aiTrainingEvaluationMapper).completeHelp(any(AiTrainingEvaluation.class)); verify(service.aiTrainingEvaluationMapper).completeHelp(any(AiTrainingEvaluation.class));
@ -195,7 +224,7 @@ class AiTrainingEvaluationServiceImplTest {
demo.setDemoMode(true); demo.setDemoMode(true);
demo.setDemoTeachingClassId("class-1"); demo.setDemoTeachingClassId("class-1");
assertThrows(ServiceException.class, () -> service.generateHelp("new-product-survey", demo)); assertThrows(ServiceException.class, () -> service.generateHelp("new-product-survey", demo, "请给建议"));
verify(service.qwenChatClient, never()).completeJson(any(), any()); verify(service.qwenChatClient, never()).completeJson(any(), any());
verify(service.aiTrainingEvaluationMapper, never()).insertIgnore(any()); verify(service.aiTrainingEvaluationMapper, never()).insertIgnore(any());
@ -248,7 +277,7 @@ class AiTrainingEvaluationServiceImplTest {
String xml = readResource("src/main/resources/mappers/AiTrainingEvaluationMapper.xml"); String xml = readResource("src/main/resources/mappers/AiTrainingEvaluationMapper.xml");
assertTrue(xml.contains("INSERT IGNORE INTO ai_training_evaluation")); assertTrue(xml.contains("INSERT IGNORE INTO ai_training_evaluation"));
assertTrue(xml.contains("help_status IN ('NOT_STARTED', 'FAILED')")); assertTrue(xml.contains("help_status IN ('NOT_STARTED', 'FAILED', 'SUCCEEDED')"));
assertTrue(xml.contains("assessment_status IN ('NOT_STARTED', 'FAILED')")); assertTrue(xml.contains("assessment_status IN ('NOT_STARTED', 'FAILED')"));
assertTrue(xml.contains("help_status = 'SUCCEEDED'")); assertTrue(xml.contains("help_status = 'SUCCEEDED'"));
assertTrue(xml.contains("assessment_status = 'SUCCEEDED'")); assertTrue(xml.contains("assessment_status = 'SUCCEEDED'"));

Loading…
Cancel
Save