merge: add AI help question flow

main
chenyuan 3 weeks ago
commit 0e354b3abb

@ -3,6 +3,7 @@ package com.sztzjy.linkCommerce.controller.stu;
import com.sztzjy.linkCommerce.config.exception.handler.ServiceException;
import com.sztzjy.linkCommerce.config.security.JwtUser;
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.service.AiTrainingEvaluationService;
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.PathVariable;
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.RestController;
@ -38,10 +40,12 @@ public class AiTrainingEvaluationController {
@PostMapping("/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 {
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) {
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 {
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);
}

@ -33,6 +33,7 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
private static final String PROCESSING = "PROCESSING";
private static final String SUCCEEDED = "SUCCEEDED";
private static final int MAX_ERROR_LENGTH = 1000;
private static final int MAX_HELP_QUESTION_LENGTH = 500;
private final ObjectMapper objectMapper = new ObjectMapper();
@ -58,10 +59,11 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
}
@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);
AiTrainingEvaluation evaluation = getOrCreate(context, user);
if (isTerminalOrProcessing(evaluation.getHelpStatus())) {
if (PROCESSING.equals(evaluation.getHelpStatus())) {
return toView(evaluation);
}
if (aiTrainingEvaluationMapper.claimHelp(evaluation.getId(), new Date()) != 1) {
@ -69,9 +71,9 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
}
String taskSnapshot = taskSnapshot(context.task);
String answerSnapshot = answerSnapshot(context.answer);
String answerSnapshot = helpAnswerSnapshot(normalizedQuestion, context.answer);
try {
String rawResponse = qwenChatClient.completeJson(helpSystemPrompt(), helpUserPrompt(taskSnapshot, answerSnapshot));
String rawResponse = qwenChatClient.completeJson(helpSystemPrompt(), helpUserPrompt(taskSnapshot, answerSnapshot, normalizedQuestion));
validateHelp(rawResponse);
Date completedAt = new Date();
AiTrainingEvaluation completed = new AiTrainingEvaluation();
@ -230,6 +232,17 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
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) {
ObjectNode node = objectMapper.createObjectNode();
node.put("background", task.getBackground());
@ -248,6 +261,13 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
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) {
try {
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.";
}
private String helpUserPrompt(String taskSnapshot, String answerSnapshot) {
return "Task snapshot: " + taskSnapshot + "\nStudent answer snapshot: " + answerSnapshot;
private String helpUserPrompt(String taskSnapshot, String answerSnapshot, String question) {
return "Student question: " + question + "\nTask snapshot: " + taskSnapshot + "\nStudent answer snapshot: " + answerSnapshot;
}
private String assessmentSystemPrompt() {

@ -61,7 +61,7 @@
UPDATE ai_training_evaluation
SET help_status = 'PROCESSING', help_error_message = NULL, update_time = #{updateTime,jdbcType=TIMESTAMP}
WHERE id = #{id,jdbcType=VARCHAR}
AND help_status IN ('NOT_STARTED', 'FAILED')
AND help_status IN ('NOT_STARTED', 'FAILED', 'SUCCEEDED')
</update>
<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.security.JwtUser;
import com.sztzjy.linkCommerce.entity.dto.AiTrainingHelpRequest;
import com.sztzjy.linkCommerce.entity.dto.AiTrainingEvaluationView;
import com.sztzjy.linkCommerce.service.AiTrainingEvaluationService;
import com.sztzjy.linkCommerce.util.ResultEntity;
@ -23,13 +24,13 @@ class AiTrainingEvaluationControllerTest {
controller.aiTrainingEvaluationService = mock(AiTrainingEvaluationService.class);
AiTrainingEvaluationView view = new AiTrainingEvaluationView();
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("SUCCEEDED", result.getBody().getData().getHelpStatus());
verify(controller.aiTrainingEvaluationService).generateHelp("task-key", controller.user);
verify(controller.aiTrainingEvaluationService).generateHelp("task-key", controller.user, "如何区分核心层和有形层?");
}
@Test

@ -42,7 +42,16 @@ class AiTrainingEvaluationServiceImplTest {
void rejectsBlankAllStepAnswers() {
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());
}
@ -56,7 +65,7 @@ class AiTrainingEvaluationServiceImplTest {
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());
AiTrainingEvaluationView view = service.generateHelp("new-product-survey", student(), "如何完善调研证据?");
ArgumentCaptor<AiTrainingEvaluation> saved = ArgumentCaptor.forClass(AiTrainingEvaluation.class);
verify(service.aiTrainingEvaluationMapper).completeHelp(saved.capture());
@ -65,9 +74,26 @@ class AiTrainingEvaluationServiceImplTest {
assertTrue(saved.getValue().getHelpTaskSnapshot().contains("objective one"));
assertTrue(saved.getValue().getHelpTaskSnapshot().contains("task requirement"));
assertTrue(saved.getValue().getHelpAnswerSnapshot().contains("step four"));
assertTrue(saved.getValue().getHelpAnswerSnapshot().contains("如何完善调研证据?"));
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
void assessmentSuccessWritesScoreToAnswer() {
AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null));
@ -138,17 +164,20 @@ class AiTrainingEvaluationServiceImplTest {
}
@Test
void succeededHelpReturnsStoredReportWithoutCallingQwenAgain() {
void succeededHelpCanBeRegeneratedForANewQuestion() {
AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null));
AiTrainingEvaluation evaluation = evaluation("SUCCEEDED", "NOT_STARTED");
evaluation.setHelpReportJson(helpJson());
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(helpJson(), view.getHelpReportJson());
verify(service.qwenChatClient, never()).completeJson(any(), any());
verify(service.qwenChatClient).completeJson(any(), any());
verify(service.aiTrainingEvaluationMapper).completeHelp(any(AiTrainingEvaluation.class));
}
@Test
@ -159,7 +188,7 @@ class AiTrainingEvaluationServiceImplTest {
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());
service.generateHelp("new-product-survey", student(), "请给出改进建议");
verify(service.qwenChatClient).completeJson(any(), any());
verify(service.aiTrainingEvaluationMapper).completeHelp(any(AiTrainingEvaluation.class));
@ -195,7 +224,7 @@ class AiTrainingEvaluationServiceImplTest {
demo.setDemoMode(true);
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.aiTrainingEvaluationMapper, never()).insertIgnore(any());
@ -248,7 +277,7 @@ class AiTrainingEvaluationServiceImplTest {
String xml = readResource("src/main/resources/mappers/AiTrainingEvaluationMapper.xml");
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("help_status = 'SUCCEEDED'"));
assertTrue(xml.contains("assessment_status = 'SUCCEEDED'"));

Loading…
Cancel
Save