feat(trace): add run-scoped trace reads
This commit is contained in:
@@ -5,8 +5,10 @@ import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import com.superbiz.agent.domain.model.SessionContext;
|
||||
import com.superbiz.agent.dto.AIOpsRequest;
|
||||
import com.superbiz.agent.dto.DiagnosisTraceResponse;
|
||||
import com.superbiz.agent.service.AiOpsService;
|
||||
import com.superbiz.agent.service.ChatService;
|
||||
import com.superbiz.agent.service.DiagnosisTraceService;
|
||||
import com.superbiz.agent.service.session.SessionManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -43,6 +45,9 @@ public class ChatController {
|
||||
@Autowired
|
||||
private ChatService chatService;
|
||||
|
||||
@Autowired
|
||||
private DiagnosisTraceService diagnosisTraceService;
|
||||
|
||||
@Autowired
|
||||
private SessionManager sessionManager;
|
||||
|
||||
@@ -325,6 +330,12 @@ public class ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/chat/session/{sessionId}/runs")
|
||||
public ResponseEntity<ApiResponse<List<DiagnosisTraceResponse.RunSummary>>> listSessionRuns(
|
||||
@PathVariable String sessionId) {
|
||||
return ResponseEntity.ok(ApiResponse.success(diagnosisTraceService.listRunSummaries(sessionId)));
|
||||
}
|
||||
|
||||
// ==================== 辅助方法 ====================
|
||||
|
||||
private SessionContext getOrCreateSession(String sessionId) {
|
||||
|
||||
@@ -7,6 +7,7 @@ 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.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@@ -18,7 +19,10 @@ 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)));
|
||||
public ResponseEntity<Result<DiagnosisTraceResponse>> getTrace(
|
||||
@PathVariable String sessionId,
|
||||
@RequestParam(required = false) String runId
|
||||
) {
|
||||
return ResponseEntity.ok(Result.success(diagnosisTraceService.getTrace(sessionId, runId)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,28 @@ import java.util.Map;
|
||||
@AllArgsConstructor
|
||||
public class DiagnosisTraceResponse {
|
||||
|
||||
private String runId;
|
||||
private ChatSessionTrace chatSession;
|
||||
private SessionTrace session;
|
||||
private RunTrace run;
|
||||
private List<AgentStepTrace> steps;
|
||||
private List<ToolInvocationTrace> toolInvocations;
|
||||
private TraceSummary summary;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class ChatSessionTrace {
|
||||
private Long id;
|
||||
private String sessionId;
|
||||
private String status;
|
||||
private Integer messagePairCount;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime lastActiveAt;
|
||||
private LocalDateTime expiresAt;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@@ -42,6 +59,29 @@ public class DiagnosisTraceResponse {
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class RunTrace {
|
||||
private Long id;
|
||||
private String runId;
|
||||
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
|
||||
@@ -49,6 +89,7 @@ public class DiagnosisTraceResponse {
|
||||
public static class AgentStepTrace {
|
||||
private Long id;
|
||||
private String sessionId;
|
||||
private String runId;
|
||||
private Integer stepIndex;
|
||||
private String agentName;
|
||||
private String modelInput;
|
||||
@@ -67,6 +108,7 @@ public class DiagnosisTraceResponse {
|
||||
public static class ToolInvocationTrace {
|
||||
private Long id;
|
||||
private String sessionId;
|
||||
private String runId;
|
||||
private Long stepId;
|
||||
private String toolName;
|
||||
private String inputParamsRaw;
|
||||
@@ -92,6 +134,7 @@ public class DiagnosisTraceResponse {
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class TraceSummary {
|
||||
private String resolvedRunId;
|
||||
private int persistedStepCount;
|
||||
private int returnedStepCount;
|
||||
private int persistedToolCallCount;
|
||||
@@ -100,4 +143,21 @@ public class DiagnosisTraceResponse {
|
||||
private boolean hasAiOpsRuleEvaluation;
|
||||
private boolean hasFeedback;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class RunSummary {
|
||||
private String runId;
|
||||
private String sessionId;
|
||||
private String query;
|
||||
private String status;
|
||||
private String agentFlow;
|
||||
private String answerPreview;
|
||||
private Integer stepCount;
|
||||
private Integer toolCallCount;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,15 @@ 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.ChatSession;
|
||||
import com.superbiz.agent.domain.entity.DiagnosisRun;
|
||||
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.ChatSessionRepository;
|
||||
import com.superbiz.agent.repository.DiagnosisRunRepository;
|
||||
import com.superbiz.agent.repository.DiagnosisSessionRepository;
|
||||
import com.superbiz.agent.repository.ToolInvocationRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -25,18 +29,76 @@ public class DiagnosisTraceService {
|
||||
};
|
||||
|
||||
private final DiagnosisSessionRepository diagnosisSessionRepository;
|
||||
private final ChatSessionRepository chatSessionRepository;
|
||||
private final DiagnosisRunRepository diagnosisRunRepository;
|
||||
private final AgentStepRepository agentStepRepository;
|
||||
private final ToolInvocationRepository toolInvocationRepository;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public DiagnosisTraceResponse getTrace(String sessionId) {
|
||||
return getTrace(sessionId, null);
|
||||
}
|
||||
|
||||
public DiagnosisTraceResponse getTrace(String sessionId, String runId) {
|
||||
if (runId != null && !runId.isBlank()) {
|
||||
DiagnosisRun run = diagnosisRunRepository.findBySessionIdAndRunId(sessionId, runId)
|
||||
.orElseThrow(() -> buildRunLookupException(sessionId, runId));
|
||||
return buildRunTraceResponse(sessionId, run);
|
||||
}
|
||||
|
||||
return diagnosisRunRepository.findFirstBySessionIdOrderByCreatedAtDescIdDesc(sessionId)
|
||||
.map(run -> buildRunTraceResponse(sessionId, run))
|
||||
.orElseGet(() -> buildLegacyTraceResponse(sessionId));
|
||||
}
|
||||
|
||||
public List<DiagnosisTraceResponse.RunSummary> listRunSummaries(String sessionId) {
|
||||
List<DiagnosisRun> runs = diagnosisRunRepository.findBySessionIdOrderByCreatedAtDescIdDesc(sessionId);
|
||||
if (runs.isEmpty() && chatSessionRepository.findBySessionId(sessionId).isEmpty()) {
|
||||
throw new SessionNotFoundException(sessionId);
|
||||
}
|
||||
return runs.stream()
|
||||
.map(this::toRunSummary)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private RuntimeException buildRunLookupException(String sessionId, String runId) {
|
||||
if (diagnosisRunRepository.findByRunId(runId).isPresent()) {
|
||||
return new IllegalArgumentException("runId does not belong to sessionId: " + runId);
|
||||
}
|
||||
return new SessionNotFoundException(sessionId, "Run not found: " + runId);
|
||||
}
|
||||
|
||||
private DiagnosisTraceResponse buildRunTraceResponse(String sessionId, DiagnosisRun run) {
|
||||
List<AgentStep> steps = orderStepsForTrace(agentStepRepository.findByRunIdOrderByStepIndex(run.getRunId()));
|
||||
List<ToolInvocation> toolInvocations = toolInvocationRepository.findByRunIdOrderByIdAsc(run.getRunId());
|
||||
DiagnosisTraceResponse.RunTrace runTrace = toRunTrace(run);
|
||||
|
||||
return DiagnosisTraceResponse.builder()
|
||||
.runId(run.getRunId())
|
||||
.chatSession(chatSessionRepository.findBySessionId(sessionId)
|
||||
.map(this::toChatSessionTrace)
|
||||
.orElse(null))
|
||||
.session(toSessionTrace(runTrace))
|
||||
.run(runTrace)
|
||||
.steps(steps.stream().map(this::toAgentStepTrace).toList())
|
||||
.toolInvocations(toolInvocations.stream().map(this::toToolInvocationTrace).toList())
|
||||
.summary(toSummary(runTrace, steps, toolInvocations))
|
||||
.build();
|
||||
}
|
||||
|
||||
private DiagnosisTraceResponse buildLegacyTraceResponse(String sessionId) {
|
||||
DiagnosisSession session = diagnosisSessionRepository.findBySessionId(sessionId)
|
||||
.orElseThrow(() -> new SessionNotFoundException(sessionId));
|
||||
List<AgentStep> steps = orderStepsForTrace(agentStepRepository.findBySessionId(sessionId));
|
||||
List<ToolInvocation> toolInvocations = toolInvocationRepository.findBySessionIdOrderByIdAsc(sessionId);
|
||||
|
||||
return DiagnosisTraceResponse.builder()
|
||||
.runId(null)
|
||||
.chatSession(chatSessionRepository.findBySessionId(sessionId)
|
||||
.map(this::toChatSessionTrace)
|
||||
.orElse(null))
|
||||
.session(toSessionTrace(session))
|
||||
.run(null)
|
||||
.steps(steps.stream().map(this::toAgentStepTrace).toList())
|
||||
.toolInvocations(toolInvocations.stream().map(this::toToolInvocationTrace).toList())
|
||||
.summary(toSummary(session, steps, toolInvocations))
|
||||
@@ -52,6 +114,18 @@ public class DiagnosisTraceService {
|
||||
.toList();
|
||||
}
|
||||
|
||||
private DiagnosisTraceResponse.ChatSessionTrace toChatSessionTrace(ChatSession chatSession) {
|
||||
return DiagnosisTraceResponse.ChatSessionTrace.builder()
|
||||
.id(chatSession.getId())
|
||||
.sessionId(chatSession.getSessionId())
|
||||
.status(chatSession.getStatus())
|
||||
.messagePairCount(chatSession.getMessagePairCount())
|
||||
.createdAt(chatSession.getCreatedAt())
|
||||
.lastActiveAt(chatSession.getLastActiveAt())
|
||||
.expiresAt(chatSession.getExpiresAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
private DiagnosisTraceResponse.SessionTrace toSessionTrace(DiagnosisSession session) {
|
||||
return DiagnosisTraceResponse.SessionTrace.builder()
|
||||
.id(session.getId())
|
||||
@@ -72,10 +146,52 @@ public class DiagnosisTraceService {
|
||||
.build();
|
||||
}
|
||||
|
||||
private DiagnosisTraceResponse.SessionTrace toSessionTrace(DiagnosisTraceResponse.RunTrace run) {
|
||||
return DiagnosisTraceResponse.SessionTrace.builder()
|
||||
.id(run.getId())
|
||||
.sessionId(run.getSessionId())
|
||||
.query(run.getQuery())
|
||||
.status(run.getStatus())
|
||||
.agentFlow(run.getAgentFlow())
|
||||
.totalDurationMs(run.getTotalDurationMs())
|
||||
.totalTokenCount(run.getTotalTokenCount())
|
||||
.stepCount(run.getStepCount())
|
||||
.toolCallCount(run.getToolCallCount())
|
||||
.answer(run.getAnswer())
|
||||
.selfEvaluationRaw(run.getSelfEvaluationRaw())
|
||||
.selfEvaluation(run.getSelfEvaluation())
|
||||
.feedback(run.getFeedback())
|
||||
.createdAt(run.getCreatedAt())
|
||||
.updatedAt(run.getUpdatedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
private DiagnosisTraceResponse.RunTrace toRunTrace(DiagnosisRun run) {
|
||||
return DiagnosisTraceResponse.RunTrace.builder()
|
||||
.id(run.getId())
|
||||
.runId(run.getRunId())
|
||||
.sessionId(run.getSessionId())
|
||||
.query(run.getQuery())
|
||||
.status(run.getStatus())
|
||||
.agentFlow(run.getAgentFlow())
|
||||
.totalDurationMs(run.getTotalDurationMs())
|
||||
.totalTokenCount(run.getTotalTokenCount())
|
||||
.stepCount(run.getStepCount())
|
||||
.toolCallCount(run.getToolCallCount())
|
||||
.answer(run.getAnswer())
|
||||
.selfEvaluationRaw(run.getSelfEvaluation())
|
||||
.selfEvaluation(parseJsonObject(run.getSelfEvaluation()))
|
||||
.feedback(run.getFeedback())
|
||||
.createdAt(run.getCreatedAt())
|
||||
.updatedAt(run.getUpdatedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
private DiagnosisTraceResponse.AgentStepTrace toAgentStepTrace(AgentStep step) {
|
||||
return DiagnosisTraceResponse.AgentStepTrace.builder()
|
||||
.id(step.getId())
|
||||
.sessionId(step.getSessionId())
|
||||
.runId(step.getRunId())
|
||||
.stepIndex(step.getStepIndex())
|
||||
.agentName(step.getAgentName())
|
||||
.modelInput(step.getModelInput())
|
||||
@@ -92,6 +208,7 @@ public class DiagnosisTraceService {
|
||||
return DiagnosisTraceResponse.ToolInvocationTrace.builder()
|
||||
.id(invocation.getId())
|
||||
.sessionId(invocation.getSessionId())
|
||||
.runId(invocation.getRunId())
|
||||
.stepId(invocation.getStepId())
|
||||
.toolName(invocation.getToolName())
|
||||
.inputParamsRaw(invocation.getInputParams())
|
||||
@@ -113,6 +230,21 @@ public class DiagnosisTraceService {
|
||||
.build();
|
||||
}
|
||||
|
||||
private DiagnosisTraceResponse.RunSummary toRunSummary(DiagnosisRun run) {
|
||||
return DiagnosisTraceResponse.RunSummary.builder()
|
||||
.runId(run.getRunId())
|
||||
.sessionId(run.getSessionId())
|
||||
.query(run.getQuery())
|
||||
.status(run.getStatus())
|
||||
.agentFlow(run.getAgentFlow())
|
||||
.answerPreview(preview(run.getAnswer(), 160))
|
||||
.stepCount(run.getStepCount())
|
||||
.toolCallCount(run.getToolCallCount())
|
||||
.createdAt(run.getCreatedAt())
|
||||
.updatedAt(run.getUpdatedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
private DiagnosisTraceResponse.TraceSummary toSummary(
|
||||
DiagnosisSession session,
|
||||
List<AgentStep> steps,
|
||||
@@ -120,6 +252,7 @@ public class DiagnosisTraceService {
|
||||
) {
|
||||
Map<String, Object> selfEvaluation = parseJsonObject(session.getSelfEvaluation());
|
||||
return DiagnosisTraceResponse.TraceSummary.builder()
|
||||
.resolvedRunId(null)
|
||||
.persistedStepCount(defaultInt(session.getStepCount()))
|
||||
.returnedStepCount(steps.size())
|
||||
.persistedToolCallCount(defaultInt(session.getToolCallCount()))
|
||||
@@ -130,6 +263,24 @@ public class DiagnosisTraceService {
|
||||
.build();
|
||||
}
|
||||
|
||||
private DiagnosisTraceResponse.TraceSummary toSummary(
|
||||
DiagnosisTraceResponse.RunTrace run,
|
||||
List<AgentStep> steps,
|
||||
List<ToolInvocation> toolInvocations
|
||||
) {
|
||||
Map<String, Object> selfEvaluation = run.getSelfEvaluation();
|
||||
return DiagnosisTraceResponse.TraceSummary.builder()
|
||||
.resolvedRunId(run.getRunId())
|
||||
.persistedStepCount(defaultInt(run.getStepCount()))
|
||||
.returnedStepCount(steps.size())
|
||||
.persistedToolCallCount(defaultInt(run.getToolCallCount()))
|
||||
.returnedToolCallCount(toolInvocations.size())
|
||||
.hasVerifierEvaluation(selfEvaluation != null && selfEvaluation.containsKey("verifier_evaluation"))
|
||||
.hasAiOpsRuleEvaluation(selfEvaluation != null && selfEvaluation.containsKey("aiops_rule_evaluation"))
|
||||
.hasFeedback(run.getFeedback() != null && !run.getFeedback().isBlank())
|
||||
.build();
|
||||
}
|
||||
|
||||
private Map<String, Object> parseJsonObject(String json) {
|
||||
if (json == null || json.isBlank()) {
|
||||
return null;
|
||||
@@ -144,4 +295,14 @@ public class DiagnosisTraceService {
|
||||
private int defaultInt(Integer value) {
|
||||
return value == null ? 0 : value;
|
||||
}
|
||||
|
||||
private String preview(String text, int maxLength) {
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
if (text.length() <= maxLength) {
|
||||
return text;
|
||||
}
|
||||
return text.substring(0, maxLength);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user