feat: archive mvp demo trace acceptance

This commit is contained in:
zhuyongxin
2026-07-03 16:25:00 +08:00
parent 5b827fe90e
commit 6919092b83
24 changed files with 1343 additions and 2 deletions
@@ -0,0 +1,24 @@
package com.superbiz.agent.controller;
import com.superbiz.agent.dto.DiagnosisTraceResponse;
import com.superbiz.agent.dto.Result;
import com.superbiz.agent.service.DiagnosisTraceService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/diagnosis")
@RequiredArgsConstructor
public class DiagnosisTraceController {
private final DiagnosisTraceService diagnosisTraceService;
@GetMapping("/{sessionId}/trace")
public ResponseEntity<Result<DiagnosisTraceResponse>> getTrace(@PathVariable String sessionId) {
return ResponseEntity.ok(Result.success(diagnosisTraceService.getTrace(sessionId)));
}
}
@@ -0,0 +1,102 @@
package com.superbiz.agent.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DiagnosisTraceResponse {
private SessionTrace session;
private List<AgentStepTrace> steps;
private List<ToolInvocationTrace> toolInvocations;
private TraceSummary summary;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class SessionTrace {
private Long id;
private String sessionId;
private String query;
private String status;
private String agentFlow;
private Integer totalDurationMs;
private Integer totalTokenCount;
private Integer stepCount;
private Integer toolCallCount;
private String answer;
private String selfEvaluationRaw;
private Map<String, Object> selfEvaluation;
private String feedback;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class AgentStepTrace {
private Long id;
private String sessionId;
private Integer stepIndex;
private String agentName;
private String modelInput;
private String modelOutput;
private String thought;
private Boolean hasToolCall;
private Integer durationMs;
private Integer tokenCount;
private LocalDateTime createdAt;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class ToolInvocationTrace {
private Long id;
private String sessionId;
private Long stepId;
private String toolName;
private String inputParamsRaw;
private Map<String, Object> inputParams;
private String outputPreview;
private Integer outputLength;
private String retrievalLayer;
private Integer l0MatchCount;
private Integer l1MatchCount;
private Boolean truncated;
private String relevanceLevel;
private String dedupReason;
private String retrievalDetailsRaw;
private Map<String, Object> retrievalDetails;
private Integer durationMs;
private Boolean success;
private String errorMessage;
private LocalDateTime createdAt;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class TraceSummary {
private int persistedStepCount;
private int returnedStepCount;
private int persistedToolCallCount;
private int returnedToolCallCount;
private boolean hasVerifierEvaluation;
private boolean hasFeedback;
}
}
@@ -416,6 +416,9 @@ public class ChatService {
answer = extractStateText(stateOptional, "executor_feedback");
VerifierContextHolder.setExecutorFinalAnswer(answer);
String verifierOutput = extractStateText(stateOptional, "verifier_output");
if ((verifierOutput == null || verifierOutput.isBlank()) && answer != null && !answer.isBlank()) {
verifierOutput = invokeVerifierFallback(verifier, question, round, config);
}
finalDecision = parseVerifierDecision(verifierOutput, round);
logger.debug("Supervisor round {} finished: plannerPlanLength={}, answerLength={}, verifierOutputLength={}",
round,
@@ -588,6 +591,17 @@ public class ChatService {
return input.toString();
}
private String invokeVerifierFallback(ReactAgent verifier, String question, int round, RunnableConfig config) {
try {
logger.warn("Supervisor round {} finished without verifier_output, invoking chat_verifier fallback", round);
return verifier.call("请基于 executor_final_answer 和 tool_trace_summary 输出 verifier JSON。原始问题:" + question, config)
.getText();
} catch (Exception e) {
logger.error("chat_verifier fallback 执行失败", e);
return null;
}
}
private VerifierDecision parseVerifierDecision(String verifierOutput, int round) {
if (verifierOutput == null || verifierOutput.isBlank()) {
return null;
@@ -0,0 +1,136 @@
package com.superbiz.agent.service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.superbiz.agent.domain.entity.AgentStep;
import com.superbiz.agent.domain.entity.DiagnosisSession;
import com.superbiz.agent.domain.entity.ToolInvocation;
import com.superbiz.agent.dto.DiagnosisTraceResponse;
import com.superbiz.agent.exception.SessionNotFoundException;
import com.superbiz.agent.repository.AgentStepRepository;
import com.superbiz.agent.repository.DiagnosisSessionRepository;
import com.superbiz.agent.repository.ToolInvocationRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
@Service
@RequiredArgsConstructor
public class DiagnosisTraceService {
private static final TypeReference<Map<String, Object>> JSON_MAP_TYPE = new TypeReference<>() {
};
private final DiagnosisSessionRepository diagnosisSessionRepository;
private final AgentStepRepository agentStepRepository;
private final ToolInvocationRepository toolInvocationRepository;
private final ObjectMapper objectMapper;
public DiagnosisTraceResponse getTrace(String sessionId) {
DiagnosisSession session = diagnosisSessionRepository.findBySessionId(sessionId)
.orElseThrow(() -> new SessionNotFoundException(sessionId));
List<AgentStep> steps = agentStepRepository.findBySessionIdOrderByStepIndex(sessionId);
List<ToolInvocation> toolInvocations = toolInvocationRepository.findBySessionIdOrderByIdAsc(sessionId);
return DiagnosisTraceResponse.builder()
.session(toSessionTrace(session))
.steps(steps.stream().map(this::toAgentStepTrace).toList())
.toolInvocations(toolInvocations.stream().map(this::toToolInvocationTrace).toList())
.summary(toSummary(session, steps, toolInvocations))
.build();
}
private DiagnosisTraceResponse.SessionTrace toSessionTrace(DiagnosisSession session) {
return DiagnosisTraceResponse.SessionTrace.builder()
.id(session.getId())
.sessionId(session.getSessionId())
.query(session.getQuery())
.status(session.getStatus())
.agentFlow(session.getAgentFlow())
.totalDurationMs(session.getTotalDurationMs())
.totalTokenCount(session.getTotalTokenCount())
.stepCount(session.getStepCount())
.toolCallCount(session.getToolCallCount())
.answer(session.getAnswer())
.selfEvaluationRaw(session.getSelfEvaluation())
.selfEvaluation(parseJsonObject(session.getSelfEvaluation()))
.feedback(session.getFeedback())
.createdAt(session.getCreatedAt())
.updatedAt(session.getUpdatedAt())
.build();
}
private DiagnosisTraceResponse.AgentStepTrace toAgentStepTrace(AgentStep step) {
return DiagnosisTraceResponse.AgentStepTrace.builder()
.id(step.getId())
.sessionId(step.getSessionId())
.stepIndex(step.getStepIndex())
.agentName(step.getAgentName())
.modelInput(step.getModelInput())
.modelOutput(step.getModelOutput())
.thought(step.getThought())
.hasToolCall(step.getHasToolCall())
.durationMs(step.getDurationMs())
.tokenCount(step.getTokenCount())
.createdAt(step.getCreatedAt())
.build();
}
private DiagnosisTraceResponse.ToolInvocationTrace toToolInvocationTrace(ToolInvocation invocation) {
return DiagnosisTraceResponse.ToolInvocationTrace.builder()
.id(invocation.getId())
.sessionId(invocation.getSessionId())
.stepId(invocation.getStepId())
.toolName(invocation.getToolName())
.inputParamsRaw(invocation.getInputParams())
.inputParams(parseJsonObject(invocation.getInputParams()))
.outputPreview(invocation.getOutputPreview())
.outputLength(invocation.getOutputLength())
.retrievalLayer(invocation.getRetrievalLayer())
.l0MatchCount(invocation.getL0MatchCount())
.l1MatchCount(invocation.getL1MatchCount())
.truncated(invocation.getIsTruncated())
.relevanceLevel(invocation.getRelevanceLevel())
.dedupReason(invocation.getDedupReason())
.retrievalDetailsRaw(invocation.getRetrievalDetails())
.retrievalDetails(parseJsonObject(invocation.getRetrievalDetails()))
.durationMs(invocation.getDurationMs())
.success(invocation.getSuccess())
.errorMessage(invocation.getErrorMessage())
.createdAt(invocation.getCreatedAt())
.build();
}
private DiagnosisTraceResponse.TraceSummary toSummary(
DiagnosisSession session,
List<AgentStep> steps,
List<ToolInvocation> toolInvocations
) {
Map<String, Object> selfEvaluation = parseJsonObject(session.getSelfEvaluation());
return DiagnosisTraceResponse.TraceSummary.builder()
.persistedStepCount(defaultInt(session.getStepCount()))
.returnedStepCount(steps.size())
.persistedToolCallCount(defaultInt(session.getToolCallCount()))
.returnedToolCallCount(toolInvocations.size())
.hasVerifierEvaluation(selfEvaluation != null && selfEvaluation.containsKey("verifier_evaluation"))
.hasFeedback(session.getFeedback() != null && !session.getFeedback().isBlank())
.build();
}
private Map<String, Object> parseJsonObject(String json) {
if (json == null || json.isBlank()) {
return null;
}
try {
return objectMapper.readValue(json, JSON_MAP_TYPE);
} catch (Exception ignored) {
return null;
}
}
private int defaultInt(Integer value) {
return value == null ? 0 : value;
}
}