feat: add traceable scoped AIOps diagnosis

This commit is contained in:
aruo
2026-07-04 22:57:28 +08:00
parent 246c99b954
commit 23ee05c7c3
32 changed files with 1179 additions and 25 deletions
@@ -0,0 +1,169 @@
package com.superbiz.agent.service;
import com.superbiz.agent.domain.entity.AgentStep;
import com.superbiz.agent.domain.entity.DiagnosisSession;
import com.superbiz.agent.dto.AIOpsRequest;
import com.superbiz.agent.repository.AgentStepRepository;
import com.superbiz.agent.repository.DiagnosisSessionRepository;
import com.superbiz.agent.repository.ToolInvocationRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.Optional;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class AiOpsServiceTest {
private final DiagnosisSessionRepository diagnosisSessionRepository = mock(DiagnosisSessionRepository.class);
private final AgentStepRepository agentStepRepository = mock(AgentStepRepository.class);
private final ToolInvocationRepository toolInvocationRepository = mock(ToolInvocationRepository.class);
private final AiOpsService service = new AiOpsService();
@BeforeEach
void setUp() {
ReflectionTestUtils.setField(service, "diagnosisSessionRepository", diagnosisSessionRepository);
ReflectionTestUtils.setField(service, "agentStepRepository", agentStepRepository);
ReflectionTestUtils.setField(service, "toolInvocationRepository", toolInvocationRepository);
}
@Test
void resolveSessionIdUsesRequestValueWhenPresent() {
AIOpsRequest request = new AIOpsRequest();
request.setSessionId(" aiops-demo-session ");
assertEquals("aiops-demo-session", service.resolveSessionId(request));
}
@Test
void resolveSessionIdGeneratesWhenMissing() {
String sessionId = service.resolveSessionId(null);
assertNotNull(sessionId);
assertFalse(sessionId.isBlank());
}
@Test
void buildQuerySummaryUsesAlertFieldsAndUserRequestFallback() {
AIOpsRequest request = new AIOpsRequest();
request.setAlertName("payment-service-latency-high");
request.setService("payment-service");
request.setSeverity("P1");
request.setTimeRange("last_15m");
request.setDescription("P95 latency is high");
request.setUserRequest("结合日志和指标排查支付超时");
String summary = service.buildQuerySummary(request);
assertTrue(summary.contains("AI Ops 告警分析"));
assertTrue(summary.contains("告警: payment-service-latency-high"));
assertTrue(summary.contains("服务: payment-service"));
assertTrue(summary.contains("等级: P1"));
assertTrue(summary.contains("时间范围: last_15m"));
assertTrue(summary.contains("描述: P95 latency is high"));
assertTrue(summary.contains("请求: 结合日志和指标排查支付超时"));
}
@Test
void hasAlertPayloadIgnoresUserRequestOnly() {
AIOpsRequest request = new AIOpsRequest();
request.setUserRequest("please discover active alerts");
assertFalse(service.hasAlertPayload(request));
request.setAlertName("HighCPUUsage");
assertTrue(service.hasAlertPayload(request));
}
@Test
void buildTaskPromptUsesPayloadTargetedModeWhenAlertFieldsExist() {
AIOpsRequest request = new AIOpsRequest();
request.setAlertName("HighCPUUsage");
request.setService("payment-service");
request.setSeverity("P1");
request.setTimeRange("last_15m");
request.setDescription("CPU usage is above 80%");
String prompt = service.buildTaskPrompt(request);
assertTrue(prompt.contains("AIOps scope mode: PAYLOAD_TARGETED"));
assertTrue(prompt.contains("primary and only main diagnosis target"));
assertTrue(prompt.contains("queryPrometheusAlerts only to verify"));
assertTrue(prompt.contains("do not create full root-cause or remediation sections"));
assertTrue(prompt.contains("Related Risk"));
assertTrue(prompt.contains("告警: HighCPUUsage"));
assertTrue(prompt.contains("服务: payment-service"));
assertFalse(prompt.contains("AIOps scope mode: AUTO_DISCOVERY"));
}
@Test
void buildTaskPromptUsesAutoDiscoveryModeWhenAlertPayloadIsMissing() {
String nullRequestPrompt = service.buildTaskPrompt(null);
assertTrue(nullRequestPrompt.contains("AIOps scope mode: AUTO_DISCOVERY"));
assertTrue(nullRequestPrompt.contains("First call queryPrometheusAlerts"));
assertTrue(nullRequestPrompt.contains("current active/firing alerts"));
assertFalse(nullRequestPrompt.contains("AIOps scope mode: PAYLOAD_TARGETED"));
AIOpsRequest userRequestOnly = new AIOpsRequest();
userRequestOnly.setUserRequest("check what is firing now");
String userRequestOnlyPrompt = service.buildTaskPrompt(userRequestOnly);
assertTrue(userRequestOnlyPrompt.contains("AIOps scope mode: AUTO_DISCOVERY"));
assertTrue(userRequestOnlyPrompt.contains("First call queryPrometheusAlerts"));
}
@Test
void persistFinalReportUpdatesDiagnosisSessionAnswer() {
DiagnosisSession session = DiagnosisSession.builder()
.sessionId("aiops-session-001")
.query("AI Ops 告警分析")
.status("SUCCESS")
.agentFlow("AI_OPS")
.build();
when(diagnosisSessionRepository.findBySessionId("aiops-session-001")).thenReturn(Optional.of(session));
service.persistFinalReport("aiops-session-001", "# 告警分析报告");
assertEquals("# 告警分析报告", session.getAnswer());
verify(diagnosisSessionRepository).save(session);
}
@Test
void persistFinalReportSkipsBlankInput() {
service.persistFinalReport("aiops-session-001", " ");
verifyNoInteractions(diagnosisSessionRepository);
}
@Test
void backfillSessionMetricsUsesRealToolInvocationCount() {
DiagnosisSession session = DiagnosisSession.builder()
.sessionId("aiops-session-002")
.build();
AgentStep stepWithTool = AgentStep.builder()
.sessionId("aiops-session-002")
.hasToolCall(true)
.tokenCount(10)
.build();
AgentStep stepWithoutTool = AgentStep.builder()
.sessionId("aiops-session-002")
.hasToolCall(false)
.tokenCount(20)
.build();
when(agentStepRepository.findBySessionIdOrderByStepIndex("aiops-session-002"))
.thenReturn(List.of(stepWithTool, stepWithoutTool));
when(toolInvocationRepository.countBySessionId("aiops-session-002")).thenReturn(11L);
ReflectionTestUtils.invokeMethod(service, "backfillSessionMetrics", session);
assertEquals(2, session.getStepCount());
assertEquals(30, session.getTotalTokenCount());
assertEquals(11, session.getToolCallCount());
}
}
@@ -7,6 +7,7 @@ import com.superbiz.agent.domain.entity.AgentStep;
import com.superbiz.agent.domain.entity.DiagnosisSession;
import com.superbiz.agent.repository.AgentStepRepository;
import com.superbiz.agent.repository.DiagnosisSessionRepository;
import com.superbiz.agent.repository.ToolInvocationRepository;
import com.superbiz.agent.tool.LookupKnowledgeTool;
import com.superbiz.agent.tool.RetrievedDocTracker;
import org.junit.jupiter.api.Test;
@@ -143,6 +144,8 @@ class ChatServiceSequentialAgentTest {
});
when(agentStepRepository.findById(any())).thenReturn(Optional.of(new AgentStep()));
when(agentStepRepository.findBySessionIdOrderByStepIndex(anyString())).thenReturn(List.of());
ToolInvocationRepository toolInvocationRepository = mock(ToolInvocationRepository.class);
when(toolInvocationRepository.countBySessionId(anyString())).thenReturn(0L);
EvaluationService evaluationService = mock(EvaluationService.class);
RetrievedDocTracker retrievedDocTracker = mock(RetrievedDocTracker.class);
@@ -158,6 +161,7 @@ class ChatServiceSequentialAgentTest {
ReflectionTestUtils.setField(chatService, "queryLogsTools", new QueryLogsTools(mock(ToolInvocationRecorder.class)));
ReflectionTestUtils.setField(chatService, "diagnosisSessionRepository", diagnosisSessionRepository);
ReflectionTestUtils.setField(chatService, "agentStepRepository", agentStepRepository);
ReflectionTestUtils.setField(chatService, "toolInvocationRepository", toolInvocationRepository);
ReflectionTestUtils.setField(chatService, "evaluationService", evaluationService);
ReflectionTestUtils.setField(chatService, "retrievedDocTracker", retrievedDocTracker);
ReflectionTestUtils.setField(chatService, "knowledgeDomainService", knowledgeDomainService);