feat: archive mvp demo trace acceptance
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: mvp-demo
|
||||
|
||||
server:
|
||||
port: 9900
|
||||
|
||||
prometheus:
|
||||
mock-enabled: true
|
||||
timeout: 5
|
||||
|
||||
cls:
|
||||
mock-enabled: true
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
com.superbiz.agent: DEBUG
|
||||
com.alibaba.cloud: INFO
|
||||
|
||||
mvp:
|
||||
demo:
|
||||
name: payment-timeout-trace
|
||||
description: Repeatable MVP flow for chat diagnosis, tool evidence, verifier evaluation, trace query, and feedback.
|
||||
@@ -85,6 +85,43 @@ class ChatServiceSupervisorAgentTest {
|
||||
assertEquals(List.of("chat_planner", "chat_executor", "chat_verifier", "FINISH"), chatModel.decisions);
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeChatComplexInvokesVerifierFallbackWhenSupervisorSkipsVerifier() throws Exception {
|
||||
ChatService chatService = createChatService();
|
||||
ScriptedChatModel chatModel = new ScriptedChatModel(
|
||||
List.of("chat_planner", "chat_executor", "FINISH"),
|
||||
"""
|
||||
{
|
||||
"verdict": "PASS",
|
||||
"groundedness_score": 1.0,
|
||||
"critical_fact_count": 1,
|
||||
"facts_checked": [
|
||||
{
|
||||
"fact": "executor answer generated",
|
||||
"is_critical": true,
|
||||
"verification": "direct_evidence",
|
||||
"detail": "covered by fallback verifier",
|
||||
"evidence_refs": []
|
||||
}
|
||||
],
|
||||
"rationale": "fallback verifier pass"
|
||||
}
|
||||
"""
|
||||
);
|
||||
|
||||
ChatService.ChatResult result = chatService.executeChatComplex(
|
||||
chatModel,
|
||||
new ToolCallback[0],
|
||||
"请分析订单支付超时的原因,并给出修复建议",
|
||||
List.of(),
|
||||
"supervisor-verifier-fallback-session"
|
||||
);
|
||||
|
||||
assertEquals("EXECUTOR_FINAL_ANSWER", result.answer());
|
||||
assertEquals(List.of("chat_planner", "chat_executor", "FINISH"), chatModel.decisions);
|
||||
assertTrue(chatModel.sawVerifierPrompt);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildMethodToolsArrayIncludesLogsAndMetricsWhenAvailable() {
|
||||
ChatService chatService = new ChatService();
|
||||
@@ -153,7 +190,7 @@ class ChatServiceSupervisorAgentTest {
|
||||
}
|
||||
|
||||
private static final class ScriptedChatModel implements ChatModel {
|
||||
private final List<String> decisionScript = List.of("chat_planner", "chat_executor", "chat_verifier", "FINISH");
|
||||
private final List<String> decisionScript;
|
||||
private final java.util.ArrayList<String> decisions = new java.util.ArrayList<>();
|
||||
private int decisionIndex;
|
||||
private String promptText = "";
|
||||
@@ -161,7 +198,7 @@ class ChatServiceSupervisorAgentTest {
|
||||
private final String verifierOutput;
|
||||
|
||||
private ScriptedChatModel() {
|
||||
this("""
|
||||
this(List.of("chat_planner", "chat_executor", "chat_verifier", "FINISH"), """
|
||||
{
|
||||
"verdict": "PASS",
|
||||
"groundedness_score": 1.0,
|
||||
@@ -181,6 +218,11 @@ class ChatServiceSupervisorAgentTest {
|
||||
}
|
||||
|
||||
private ScriptedChatModel(String verifierOutput) {
|
||||
this(List.of("chat_planner", "chat_executor", "chat_verifier", "FINISH"), verifierOutput);
|
||||
}
|
||||
|
||||
private ScriptedChatModel(List<String> decisionScript, String verifierOutput) {
|
||||
this.decisionScript = decisionScript;
|
||||
this.verifierOutput = verifierOutput;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.superbiz.agent.service;
|
||||
|
||||
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 org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class DiagnosisTraceServiceTest {
|
||||
|
||||
private final DiagnosisSessionRepository diagnosisSessionRepository = mock(DiagnosisSessionRepository.class);
|
||||
private final AgentStepRepository agentStepRepository = mock(AgentStepRepository.class);
|
||||
private final ToolInvocationRepository toolInvocationRepository = mock(ToolInvocationRepository.class);
|
||||
private final DiagnosisTraceService service = new DiagnosisTraceService(
|
||||
diagnosisSessionRepository,
|
||||
agentStepRepository,
|
||||
toolInvocationRepository,
|
||||
new ObjectMapper()
|
||||
);
|
||||
|
||||
@Test
|
||||
void getTraceAggregatesSessionStepsAndTools() {
|
||||
String sessionId = "trace-session-001";
|
||||
LocalDateTime now = LocalDateTime.of(2026, 7, 3, 14, 30);
|
||||
DiagnosisSession session = DiagnosisSession.builder()
|
||||
.id(1L)
|
||||
.sessionId(sessionId)
|
||||
.query("payment timeout")
|
||||
.status("SUCCESS")
|
||||
.agentFlow("COMPLEX")
|
||||
.totalDurationMs(1200)
|
||||
.totalTokenCount(300)
|
||||
.stepCount(2)
|
||||
.toolCallCount(1)
|
||||
.answer("restart payment gateway pool")
|
||||
.selfEvaluation("{\"verifier_evaluation\":{\"verdict\":\"PASS\"}}")
|
||||
.feedback("useful")
|
||||
.createdAt(now)
|
||||
.updatedAt(now)
|
||||
.build();
|
||||
AgentStep step = AgentStep.builder()
|
||||
.id(10L)
|
||||
.sessionId(sessionId)
|
||||
.stepIndex(1)
|
||||
.agentName("chat_executor")
|
||||
.modelInput("input")
|
||||
.modelOutput("output")
|
||||
.thought("executor finished")
|
||||
.hasToolCall(true)
|
||||
.durationMs(500)
|
||||
.tokenCount(100)
|
||||
.createdAt(now)
|
||||
.build();
|
||||
ToolInvocation invocation = ToolInvocation.builder()
|
||||
.id(20L)
|
||||
.sessionId(sessionId)
|
||||
.stepId(10L)
|
||||
.toolName("lookup_knowledge")
|
||||
.inputParams("{\"query\":\"ERR_TIMEOUT\"}")
|
||||
.outputPreview("payment timeout doc")
|
||||
.outputLength(19)
|
||||
.retrievalLayer("L0")
|
||||
.l0MatchCount(1)
|
||||
.l1MatchCount(0)
|
||||
.isTruncated(false)
|
||||
.relevanceLevel("HIGHLY_RELEVANT")
|
||||
.dedupReason("FIRST_HIT")
|
||||
.retrievalDetails("{\"documents\":[\"payment-errors.md\"]}")
|
||||
.durationMs(80)
|
||||
.success(true)
|
||||
.createdAt(now)
|
||||
.build();
|
||||
|
||||
when(diagnosisSessionRepository.findBySessionId(sessionId)).thenReturn(Optional.of(session));
|
||||
when(agentStepRepository.findBySessionIdOrderByStepIndex(sessionId)).thenReturn(List.of(step));
|
||||
when(toolInvocationRepository.findBySessionIdOrderByIdAsc(sessionId)).thenReturn(List.of(invocation));
|
||||
|
||||
DiagnosisTraceResponse response = service.getTrace(sessionId);
|
||||
|
||||
assertEquals(sessionId, response.getSession().getSessionId());
|
||||
assertEquals("payment timeout", response.getSession().getQuery());
|
||||
assertEquals("PASS", ((java.util.Map<?, ?>) response.getSession()
|
||||
.getSelfEvaluation()
|
||||
.get("verifier_evaluation")).get("verdict"));
|
||||
assertEquals(1, response.getSteps().size());
|
||||
assertEquals("chat_executor", response.getSteps().get(0).getAgentName());
|
||||
assertEquals(1, response.getToolInvocations().size());
|
||||
assertEquals("ERR_TIMEOUT", response.getToolInvocations().get(0).getInputParams().get("query"));
|
||||
assertEquals(2, response.getSummary().getPersistedStepCount());
|
||||
assertEquals(1, response.getSummary().getReturnedStepCount());
|
||||
assertEquals(1, response.getSummary().getPersistedToolCallCount());
|
||||
assertEquals(1, response.getSummary().getReturnedToolCallCount());
|
||||
assertTrue(response.getSummary().isHasVerifierEvaluation());
|
||||
assertTrue(response.getSummary().isHasFeedback());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTraceThrowsWhenSessionMissing() {
|
||||
String sessionId = "missing-session";
|
||||
when(diagnosisSessionRepository.findBySessionId(sessionId)).thenReturn(Optional.empty());
|
||||
|
||||
assertThrows(SessionNotFoundException.class, () -> service.getTrace(sessionId));
|
||||
|
||||
verify(diagnosisSessionRepository).findBySessionId(sessionId);
|
||||
verifyNoInteractions(agentStepRepository, toolInvocationRepository);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user