|
|
|
@ -21,6 +21,7 @@ import com.sztzjy.linkCommerce.mapper.TrainingTaskClassConfigMapper;
|
|
|
|
import com.sztzjy.linkCommerce.service.AiTrainingEvaluationService;
|
|
|
|
import com.sztzjy.linkCommerce.service.AiTrainingEvaluationService;
|
|
|
|
import com.sztzjy.linkCommerce.service.TrainingKnowledgeService;
|
|
|
|
import com.sztzjy.linkCommerce.service.TrainingKnowledgeService;
|
|
|
|
import com.sztzjy.linkCommerce.training.knowledge.EffectiveTrainingTaskContent;
|
|
|
|
import com.sztzjy.linkCommerce.training.knowledge.EffectiveTrainingTaskContent;
|
|
|
|
|
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
|
import org.apache.commons.lang3.StringUtils;
|
|
|
|
import org.apache.commons.lang3.StringUtils;
|
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
|
import org.springframework.http.HttpStatus;
|
|
|
|
import org.springframework.http.HttpStatus;
|
|
|
|
@ -34,11 +35,16 @@ import java.util.Date;
|
|
|
|
import java.util.List;
|
|
|
|
import java.util.List;
|
|
|
|
import java.util.UUID;
|
|
|
|
import java.util.UUID;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Slf4j
|
|
|
|
@Service
|
|
|
|
@Service
|
|
|
|
public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationService {
|
|
|
|
public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationService {
|
|
|
|
private static final String NOT_STARTED = "NOT_STARTED";
|
|
|
|
private static final String NOT_STARTED = "NOT_STARTED";
|
|
|
|
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 String FAILURE_MESSAGE = "AI服务暂时不可用,请稍后重试";
|
|
|
|
|
|
|
|
// 与 AiTrainingEvaluationMapper.xml 中 claimHelp/claimAssessment 的 INTERVAL 10 MINUTE 保持一致
|
|
|
|
|
|
|
|
private static final long PROCESSING_TIMEOUT_MILLIS = 10 * 60 * 1000L;
|
|
|
|
|
|
|
|
private static final int LOGGED_RAW_RESPONSE_LENGTH = 500;
|
|
|
|
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 static final int MAX_HELP_QUESTION_LENGTH = 500;
|
|
|
|
private static final int MAX_STEP_CONTEXT_LENGTH = 2000;
|
|
|
|
private static final int MAX_STEP_CONTEXT_LENGTH = 2000;
|
|
|
|
@ -82,7 +88,7 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
|
|
|
|
String normalizedStepContext = normalizeStepContext(stepContext);
|
|
|
|
String normalizedStepContext = normalizeStepContext(stepContext);
|
|
|
|
EvaluationContext context = resolveContext(taskKey, user, true);
|
|
|
|
EvaluationContext context = resolveContext(taskKey, user, true);
|
|
|
|
AiTrainingEvaluation evaluation = getOrCreate(context, user);
|
|
|
|
AiTrainingEvaluation evaluation = getOrCreate(context, user);
|
|
|
|
if (PROCESSING.equals(evaluation.getHelpStatus())) {
|
|
|
|
if (PROCESSING.equals(evaluation.getHelpStatus()) && !isProcessingTimedOut(evaluation)) {
|
|
|
|
return toView(evaluation);
|
|
|
|
return toView(evaluation);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (aiTrainingEvaluationMapper.claimHelp(evaluation.getId(), new Date()) != 1) {
|
|
|
|
if (aiTrainingEvaluationMapper.claimHelp(evaluation.getId(), new Date()) != 1) {
|
|
|
|
@ -110,7 +116,7 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
|
|
|
|
completed.setAssessmentStatus(evaluation.getAssessmentStatus());
|
|
|
|
completed.setAssessmentStatus(evaluation.getAssessmentStatus());
|
|
|
|
return toView(completed);
|
|
|
|
return toView(completed);
|
|
|
|
} catch (RuntimeException e) {
|
|
|
|
} catch (RuntimeException e) {
|
|
|
|
aiTrainingEvaluationMapper.failHelp(evaluation.getId(), cappedMessage(e), new Date());
|
|
|
|
recordHelpFailure(evaluation.getId(), e);
|
|
|
|
throw asServiceException(e);
|
|
|
|
throw asServiceException(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -119,7 +125,7 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
|
|
|
|
public AiTrainingEvaluationView generateAssessment(String taskKey, JwtUser user) {
|
|
|
|
public AiTrainingEvaluationView generateAssessment(String taskKey, JwtUser user) {
|
|
|
|
EvaluationContext context = resolveContext(taskKey, user, true);
|
|
|
|
EvaluationContext context = resolveContext(taskKey, user, true);
|
|
|
|
AiTrainingEvaluation evaluation = getOrCreate(context, user);
|
|
|
|
AiTrainingEvaluation evaluation = getOrCreate(context, user);
|
|
|
|
if (PROCESSING.equals(evaluation.getAssessmentStatus())) {
|
|
|
|
if (PROCESSING.equals(evaluation.getAssessmentStatus()) && !isProcessingTimedOut(evaluation)) {
|
|
|
|
return toView(evaluation);
|
|
|
|
return toView(evaluation);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
String answerSnapshot = answerSnapshot(context.answer);
|
|
|
|
String answerSnapshot = answerSnapshot(context.answer);
|
|
|
|
@ -159,7 +165,7 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
|
|
|
|
completed.setAssessmentStatus(SUCCEEDED);
|
|
|
|
completed.setAssessmentStatus(SUCCEEDED);
|
|
|
|
return toView(completed);
|
|
|
|
return toView(completed);
|
|
|
|
} catch (RuntimeException e) {
|
|
|
|
} catch (RuntimeException e) {
|
|
|
|
aiTrainingEvaluationMapper.failAssessment(evaluation.getId(), cappedMessage(e), rawResponse, new Date());
|
|
|
|
recordAssessmentFailure(evaluation.getId(), e, rawResponse);
|
|
|
|
throw asServiceException(e);
|
|
|
|
throw asServiceException(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -548,16 +554,57 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
|
|
|
|
return view;
|
|
|
|
return view;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private String cappedMessage(RuntimeException exception) {
|
|
|
|
/**
|
|
|
|
String message = StringUtils.defaultIfBlank(exception.getMessage(), "AI evaluation failed");
|
|
|
|
* 落库的失败原因:只保存我们自己抛出的 ServiceException 文案,
|
|
|
|
return StringUtils.left(message, MAX_ERROR_LENGTH);
|
|
|
|
* 其余异常(数据库、网络等)的原文只写日志,避免把 SQL 等长文本存进库或带给前端。
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private String storedFailureMessage(RuntimeException exception) {
|
|
|
|
|
|
|
|
if (exception instanceof ServiceException && StringUtils.isNotBlank(exception.getMessage())) {
|
|
|
|
|
|
|
|
return StringUtils.left(exception.getMessage(), MAX_ERROR_LENGTH);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return FAILURE_MESSAGE;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private ServiceException asServiceException(RuntimeException exception) {
|
|
|
|
/**
|
|
|
|
if (exception instanceof ServiceException) {
|
|
|
|
* 记录失败状态本身出错时不能再往外抛:否则会盖掉真正的失败原因,
|
|
|
|
return (ServiceException) exception;
|
|
|
|
* 并让记录一直停在 PROCESSING,学生无法重试。
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private void recordHelpFailure(String evaluationId, RuntimeException cause) {
|
|
|
|
|
|
|
|
log.warn("AI help failed, evaluationId={}", evaluationId, cause);
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
aiTrainingEvaluationMapper.failHelp(evaluationId, storedFailureMessage(cause), new Date());
|
|
|
|
|
|
|
|
} catch (RuntimeException recordError) {
|
|
|
|
|
|
|
|
log.error("Could not record AI help failure, evaluationId={}", evaluationId, recordError);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private void recordAssessmentFailure(String evaluationId, RuntimeException cause, String rawResponse) {
|
|
|
|
|
|
|
|
log.warn("AI assessment failed, evaluationId={}", evaluationId, cause);
|
|
|
|
|
|
|
|
String message = storedFailureMessage(cause);
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
aiTrainingEvaluationMapper.failAssessment(evaluationId, message, rawResponse, new Date());
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
} catch (RuntimeException recordError) {
|
|
|
|
|
|
|
|
log.warn("Could not record AI assessment failure with the raw response, retrying without it, evaluationId={}, rawResponse={}",
|
|
|
|
|
|
|
|
evaluationId, StringUtils.left(rawResponse, LOGGED_RAW_RESPONSE_LENGTH), recordError);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return new ServiceException(HttpStatus.BAD_GATEWAY, "AI evaluation failed");
|
|
|
|
try {
|
|
|
|
|
|
|
|
aiTrainingEvaluationMapper.failAssessment(evaluationId, message, null, new Date());
|
|
|
|
|
|
|
|
} catch (RuntimeException recordError) {
|
|
|
|
|
|
|
|
log.error("Could not record AI assessment failure, evaluationId={}", evaluationId, recordError);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private boolean isProcessingTimedOut(AiTrainingEvaluation evaluation) {
|
|
|
|
|
|
|
|
Date updatedAt = evaluation.getUpdateTime();
|
|
|
|
|
|
|
|
return updatedAt != null && System.currentTimeMillis() - updatedAt.getTime() > PROCESSING_TIMEOUT_MILLIS;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private ServiceException asServiceException(RuntimeException exception) {
|
|
|
|
|
|
|
|
HttpStatus status = exception instanceof ServiceException && ((ServiceException) exception).getCode() != null
|
|
|
|
|
|
|
|
? ((ServiceException) exception).getCode()
|
|
|
|
|
|
|
|
: HttpStatus.BAD_GATEWAY;
|
|
|
|
|
|
|
|
return new ServiceException(status, FAILURE_MESSAGE);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static class AssessmentCriterionPlan {
|
|
|
|
private static class AssessmentCriterionPlan {
|
|
|
|
|