feat(chat): cut over to single SSE endpoint
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
package com.superbiz.agent.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/** Central limits and bounded executor settings for the production Chat Harness graph. */
|
||||
@Getter
|
||||
@Setter
|
||||
@ConfigurationProperties(prefix = "harness.chat")
|
||||
public class ChatHarnessProperties {
|
||||
|
||||
private int workerCorePoolSize = 2;
|
||||
private int workerMaxPoolSize = 8;
|
||||
private int workerQueueCapacity = 64;
|
||||
private int modelCorePoolSize = 4;
|
||||
private int modelMaxPoolSize = 8;
|
||||
private int modelQueueCapacity = 32;
|
||||
private String toolKeyPrefix = "superbiz:harness:tool-call";
|
||||
private Duration runTimeout = Duration.ofMinutes(5);
|
||||
private Duration sseTimeout = Duration.ofMinutes(5);
|
||||
private Duration canonicalTtl = Duration.ofHours(2);
|
||||
private long canonicalMaxRecordBytes = 1_048_576;
|
||||
private long canonicalMaxAgentResultBytes = 65_536;
|
||||
private int maxModelCalls = 24;
|
||||
private int maxToolCalls = 24;
|
||||
private int maxCallsPerTool = 8;
|
||||
private long maxInputTokens = 100_000;
|
||||
private long maxOutputTokens = 100_000;
|
||||
private long maxTotalTokens = 200_000;
|
||||
private long maxRunBytes = 1_000_000;
|
||||
private long diagnosisMaxQueryBytes = 16_384;
|
||||
private long diagnosisMaxPreviousTurnBytes = 16_384;
|
||||
private long diagnosisMaxInputBytes = 49_152;
|
||||
private long diagnosisMaxDraftBytes = 49_152;
|
||||
private long routerMaxInputBytes = 16_384;
|
||||
private long routerMaxOutputBytes = 4_096;
|
||||
private Duration routerAttemptTimeout = Duration.ofSeconds(10);
|
||||
private Duration routerTotalTimeout = Duration.ofSeconds(25);
|
||||
private long systemMaxInputBytes = 16_384;
|
||||
private long systemMaxOutputBytes = 16_384;
|
||||
private Duration systemTimeout = Duration.ofSeconds(30);
|
||||
private long knowledgeMaxModelInputBytes = 32_768;
|
||||
private long knowledgeMaxModelOutputBytes = 24_576;
|
||||
private Duration knowledgeModelTimeout = Duration.ofSeconds(30);
|
||||
private long semanticMaxInputBytes = 100_000;
|
||||
private long semanticMaxOutputBytes = 10_000;
|
||||
private Duration semanticAttemptTimeout = Duration.ofSeconds(20);
|
||||
private Duration semanticTotalTimeout = Duration.ofSeconds(45);
|
||||
private long repairMaxInputBytes = 100_000;
|
||||
private long repairMaxOutputBytes = 48_000;
|
||||
private Duration repairTimeout = Duration.ofSeconds(20);
|
||||
|
||||
public void validate() {
|
||||
positive(workerCorePoolSize, "workerCorePoolSize");
|
||||
positive(workerMaxPoolSize, "workerMaxPoolSize");
|
||||
positive(workerQueueCapacity, "workerQueueCapacity");
|
||||
positive(modelCorePoolSize, "modelCorePoolSize");
|
||||
positive(modelMaxPoolSize, "modelMaxPoolSize");
|
||||
positive(modelQueueCapacity, "modelQueueCapacity");
|
||||
if (workerCorePoolSize > workerMaxPoolSize || modelCorePoolSize > modelMaxPoolSize) {
|
||||
throw new IllegalArgumentException("executor core size must not exceed max size");
|
||||
}
|
||||
positive(canonicalMaxRecordBytes, "canonicalMaxRecordBytes");
|
||||
positive(canonicalMaxAgentResultBytes, "canonicalMaxAgentResultBytes");
|
||||
if (canonicalMaxAgentResultBytes > canonicalMaxRecordBytes) {
|
||||
throw new IllegalArgumentException("canonical agent result exceeds record limit");
|
||||
}
|
||||
positive(maxModelCalls, "maxModelCalls");
|
||||
positive(maxToolCalls, "maxToolCalls");
|
||||
positive(maxCallsPerTool, "maxCallsPerTool");
|
||||
positive(maxInputTokens, "maxInputTokens");
|
||||
positive(maxOutputTokens, "maxOutputTokens");
|
||||
positive(maxTotalTokens, "maxTotalTokens");
|
||||
positive(maxRunBytes, "maxRunBytes");
|
||||
positive(diagnosisMaxQueryBytes, "diagnosisMaxQueryBytes");
|
||||
positive(diagnosisMaxPreviousTurnBytes, "diagnosisMaxPreviousTurnBytes");
|
||||
positive(diagnosisMaxInputBytes, "diagnosisMaxInputBytes");
|
||||
positive(diagnosisMaxDraftBytes, "diagnosisMaxDraftBytes");
|
||||
positive(routerMaxInputBytes, "routerMaxInputBytes");
|
||||
positive(routerMaxOutputBytes, "routerMaxOutputBytes");
|
||||
positive(systemMaxInputBytes, "systemMaxInputBytes");
|
||||
positive(systemMaxOutputBytes, "systemMaxOutputBytes");
|
||||
positive(knowledgeMaxModelInputBytes, "knowledgeMaxModelInputBytes");
|
||||
positive(knowledgeMaxModelOutputBytes, "knowledgeMaxModelOutputBytes");
|
||||
positive(semanticMaxInputBytes, "semanticMaxInputBytes");
|
||||
positive(semanticMaxOutputBytes, "semanticMaxOutputBytes");
|
||||
positive(repairMaxInputBytes, "repairMaxInputBytes");
|
||||
positive(repairMaxOutputBytes, "repairMaxOutputBytes");
|
||||
requirePositive(runTimeout, "runTimeout");
|
||||
requirePositive(sseTimeout, "sseTimeout");
|
||||
requirePositive(canonicalTtl, "canonicalTtl");
|
||||
requirePositive(routerAttemptTimeout, "routerAttemptTimeout");
|
||||
requirePositive(routerTotalTimeout, "routerTotalTimeout");
|
||||
requirePositive(systemTimeout, "systemTimeout");
|
||||
requirePositive(knowledgeModelTimeout, "knowledgeModelTimeout");
|
||||
requirePositive(semanticAttemptTimeout, "semanticAttemptTimeout");
|
||||
requirePositive(semanticTotalTimeout, "semanticTotalTimeout");
|
||||
requirePositive(repairTimeout, "repairTimeout");
|
||||
if (routerTotalTimeout.compareTo(routerAttemptTimeout) < 0
|
||||
|| semanticTotalTimeout.compareTo(semanticAttemptTimeout) < 0) {
|
||||
throw new IllegalArgumentException("total timeout must not be shorter than attempt timeout");
|
||||
}
|
||||
}
|
||||
|
||||
private static void positive(long value, String name) {
|
||||
if (value <= 0) {
|
||||
throw new IllegalArgumentException(name + " must be positive");
|
||||
}
|
||||
}
|
||||
|
||||
private static void requirePositive(Duration value, String name) {
|
||||
if (value == null || value.isZero() || value.isNegative()) {
|
||||
throw new IllegalArgumentException(name + " must be positive");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
package com.superbiz.agent.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.superbiz.agent.config.MysqlToolProperties.DataSourceProperties;
|
||||
import com.superbiz.agent.harness.agent.HarnessEvidenceTools;
|
||||
import com.superbiz.agent.harness.agent.DiagnosisAgentFactory;
|
||||
import com.superbiz.agent.harness.agent.DiagnosisAgentLimits;
|
||||
import com.superbiz.agent.harness.agent.DiagnosisAgentUseCase;
|
||||
import com.superbiz.agent.harness.application.ChatApplicationUseCase;
|
||||
import com.superbiz.agent.harness.application.executor.DiagnosisChatExecutor;
|
||||
import com.superbiz.agent.harness.application.executor.KnowledgeQueryExecutor;
|
||||
import com.superbiz.agent.harness.application.executor.KnowledgeQueryLimits;
|
||||
import com.superbiz.agent.harness.application.executor.SingleTurnExecutorLimits;
|
||||
import com.superbiz.agent.harness.application.executor.SystemChatExecutor;
|
||||
import com.superbiz.agent.harness.application.persistence.ChatRunStore;
|
||||
import com.superbiz.agent.harness.application.persistence.PreviousTurnLimits;
|
||||
import com.superbiz.agent.harness.application.persistence.PublishedResultPolicy;
|
||||
import com.superbiz.agent.harness.application.routing.IntentRouter;
|
||||
import com.superbiz.agent.harness.application.routing.IntentRouterLimits;
|
||||
import com.superbiz.agent.harness.core.DiagnosisHarnessCore;
|
||||
import com.superbiz.agent.harness.core.RunBudgetLimits;
|
||||
import com.superbiz.agent.harness.guard.evidence.EvidenceGuard;
|
||||
import com.superbiz.agent.harness.guard.semantic.GuardModelCall;
|
||||
import com.superbiz.agent.harness.guard.semantic.SemanticGuard;
|
||||
import com.superbiz.agent.harness.guard.semantic.SemanticGuardLimits;
|
||||
import com.superbiz.agent.harness.release.DiagnosisReleaseUseCase;
|
||||
import com.superbiz.agent.harness.release.EvidenceRepair;
|
||||
import com.superbiz.agent.harness.release.EvidenceRepairLimits;
|
||||
import com.superbiz.agent.harness.release.SafeFallbackFactory;
|
||||
import com.superbiz.agent.harness.retry.HarnessRetryExecutor;
|
||||
import com.superbiz.agent.harness.retry.HarnessRetryPolicies;
|
||||
import com.superbiz.agent.harness.tool.adapter.MysqlToolAdapter;
|
||||
import com.superbiz.agent.harness.tool.adapter.QueryLogsToolAdapter;
|
||||
import com.superbiz.agent.harness.tool.adapter.RagToolAdapter;
|
||||
import com.superbiz.agent.harness.tool.boundary.ToolBoundary;
|
||||
import com.superbiz.agent.harness.tool.mysql.JdbcMysqlReadOnlyExecutor;
|
||||
import com.superbiz.agent.harness.tool.mysql.MysqlDataSourceDefinition;
|
||||
import com.superbiz.agent.harness.tool.mysql.MysqlReadOnlyExecutor;
|
||||
import com.superbiz.agent.harness.tool.mysql.MysqlResultProjector;
|
||||
import com.superbiz.agent.harness.tool.mysql.MysqlSqlValidator;
|
||||
import com.superbiz.agent.harness.tool.projection.QueryLogsResultProjector;
|
||||
import com.superbiz.agent.harness.tool.projection.RagResultProjector;
|
||||
import com.superbiz.agent.harness.tool.projection.ToolProjectionLimits;
|
||||
import com.superbiz.agent.harness.tool.store.CanonicalInvocationLimits;
|
||||
import com.superbiz.agent.harness.tool.store.CanonicalInvocationStore;
|
||||
import com.superbiz.agent.harness.tool.store.RedisCanonicalInvocationStore;
|
||||
import com.superbiz.agent.harness.tool.store.ToolCallKeyFactory;
|
||||
import com.superbiz.agent.harness.application.DiagnosisOperation;
|
||||
import com.superbiz.agent.harness.application.KnowledgeQueryOperation;
|
||||
import com.superbiz.agent.harness.application.SystemChatOperation;
|
||||
import com.superbiz.agent.harness.application.IntentRouting;
|
||||
import com.superbiz.agent.hook.AgentLoggingHook;
|
||||
import com.superbiz.agent.repository.AgentStepRepository;
|
||||
import com.superbiz.agent.tool.LookupKnowledgeTool;
|
||||
import com.superbiz.agent.agent.tool.QueryLogsTools;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.time.Clock;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(ChatHarnessProperties.class)
|
||||
public class HarnessChatConfiguration {
|
||||
|
||||
@Bean
|
||||
public Clock harnessClock() {
|
||||
return Clock.systemUTC();
|
||||
}
|
||||
|
||||
@Bean(destroyMethod = "shutdown")
|
||||
@Qualifier("chatWorkerExecutor")
|
||||
public ThreadPoolExecutor chatWorkerExecutor(ChatHarnessProperties properties) {
|
||||
properties.validate();
|
||||
return boundedExecutor(properties.getWorkerCorePoolSize(), properties.getWorkerMaxPoolSize(),
|
||||
properties.getWorkerQueueCapacity(), "chat-worker-");
|
||||
}
|
||||
|
||||
@Bean(destroyMethod = "shutdown")
|
||||
@Qualifier("harnessModelExecutor")
|
||||
public ThreadPoolExecutor harnessModelExecutor(ChatHarnessProperties properties) {
|
||||
properties.validate();
|
||||
return boundedExecutor(properties.getModelCorePoolSize(), properties.getModelMaxPoolSize(),
|
||||
properties.getModelQueueCapacity(), "harness-model-");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DiagnosisHarnessCore diagnosisHarnessCore(ChatHarnessProperties properties, Clock harnessClock) {
|
||||
properties.validate();
|
||||
return new DiagnosisHarnessCore(harnessClock, () -> UUID.randomUUID().toString(),
|
||||
properties.getRunTimeout(),
|
||||
new RunBudgetLimits(properties.getMaxModelCalls(), properties.getMaxToolCalls(),
|
||||
properties.getMaxCallsPerTool(), properties.getMaxInputTokens(),
|
||||
properties.getMaxOutputTokens(), properties.getMaxTotalTokens(), properties.getMaxRunBytes()),
|
||||
HarnessRetryPolicies.strict());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public HarnessRetryExecutor harnessRetryExecutor(DiagnosisHarnessCore core) {
|
||||
return new HarnessRetryExecutor(core);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GuardModelCall guardModelCall(DiagnosisHarnessCore core, ChatModel chatModel,
|
||||
@Qualifier("harnessModelExecutor") ThreadPoolExecutor executor) {
|
||||
return new GuardModelCall(core, chatModel, executor);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CanonicalInvocationLimits canonicalInvocationLimits(ChatHarnessProperties properties) {
|
||||
return new CanonicalInvocationLimits(properties.getCanonicalTtl(),
|
||||
properties.getCanonicalMaxRecordBytes(), properties.getCanonicalMaxAgentResultBytes());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ToolCallKeyFactory toolCallKeyFactory(ChatHarnessProperties properties) {
|
||||
return new ToolCallKeyFactory(properties.getToolKeyPrefix());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CanonicalInvocationStore canonicalInvocationStore(
|
||||
RedisTemplate<String, Object> redisTemplate, ObjectMapper objectMapper,
|
||||
CanonicalInvocationLimits limits) {
|
||||
return new RedisCanonicalInvocationStore(redisTemplate, objectMapper, limits);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ToolBoundary toolBoundary(DiagnosisHarnessCore core, ToolCallKeyFactory keyFactory,
|
||||
CanonicalInvocationStore store, ObjectMapper objectMapper, Clock clock) {
|
||||
return new ToolBoundary(core, keyFactory, store, objectMapper, clock);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ToolProjectionLimits toolProjectionLimits() {
|
||||
return ToolProjectionLimits.defaults();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RagResultProjector ragResultProjector(ObjectMapper mapper, ToolProjectionLimits limits) {
|
||||
return new RagResultProjector(mapper, limits);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public QueryLogsResultProjector queryLogsResultProjector(ObjectMapper mapper, ToolProjectionLimits limits) {
|
||||
return new QueryLogsResultProjector(mapper, limits);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RagToolAdapter ragToolAdapter(ToolBoundary boundary, ObjectMapper mapper,
|
||||
RagResultProjector projector, LookupKnowledgeTool legacy) {
|
||||
return new RagToolAdapter(boundary, mapper, projector, legacy::lookupKnowledge);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public QueryLogsToolAdapter queryLogsToolAdapter(ToolBoundary boundary, ObjectMapper mapper,
|
||||
QueryLogsResultProjector projector,
|
||||
ObjectProvider<QueryLogsTools> legacy, Clock clock) {
|
||||
return new QueryLogsToolAdapter(boundary, mapper, projector,
|
||||
(region, topic, query, limit) -> {
|
||||
QueryLogsTools tools = legacy.getIfAvailable();
|
||||
if (tools == null) {
|
||||
return "{\"success\":false,\"logs\":[],\"total\":0}";
|
||||
}
|
||||
return tools.queryLogs(region, topic, query, limit);
|
||||
}, clock);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Map<String, DataSource> harnessMysqlDataSources(MysqlToolProperties properties) {
|
||||
Map<String, DataSource> result = new java.util.LinkedHashMap<>();
|
||||
for (Map.Entry<String, DataSourceProperties> entry : properties.getDataSources().entrySet()) {
|
||||
DataSourceProperties source = entry.getValue();
|
||||
if (source.getJdbcUrl() == null || source.getJdbcUrl().isBlank()) {
|
||||
continue;
|
||||
}
|
||||
org.springframework.jdbc.datasource.DriverManagerDataSource dataSource =
|
||||
new org.springframework.jdbc.datasource.DriverManagerDataSource();
|
||||
dataSource.setUrl(source.getJdbcUrl());
|
||||
dataSource.setUsername(source.getUsername());
|
||||
dataSource.setPassword(source.getPassword());
|
||||
result.put(entry.getKey(), dataSource);
|
||||
}
|
||||
return Map.copyOf(result);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Map<String, MysqlDataSourceDefinition> harnessMysqlDefinitions(MysqlToolProperties properties) {
|
||||
return properties.definitions();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MysqlSqlValidator mysqlSqlValidator(Map<String, MysqlDataSourceDefinition> definitions) {
|
||||
return new MysqlSqlValidator(definitions);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MysqlReadOnlyExecutor mysqlReadOnlyExecutor(
|
||||
@Qualifier("harnessMysqlDataSources") Map<String, DataSource> dataSources, Clock clock) {
|
||||
return new JdbcMysqlReadOnlyExecutor(dataSources, clock);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MysqlResultProjector mysqlResultProjector(ObjectMapper mapper) {
|
||||
return new MysqlResultProjector(mapper);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MysqlToolAdapter mysqlToolAdapter(ToolBoundary boundary, ObjectMapper mapper,
|
||||
MysqlSqlValidator validator, MysqlReadOnlyExecutor executor,
|
||||
MysqlResultProjector projector) {
|
||||
return new MysqlToolAdapter(boundary, mapper, validator, executor, projector);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public HarnessEvidenceTools harnessEvidenceTools(RagToolAdapter rag, QueryLogsToolAdapter logs,
|
||||
MysqlToolAdapter mysql) {
|
||||
return HarnessEvidenceTools.fromAdapters(rag, logs, mysql);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DiagnosisAgentFactory diagnosisAgentFactory(ChatModel chatModel, DiagnosisHarnessCore core,
|
||||
HarnessEvidenceTools tools, ObjectMapper mapper,
|
||||
AgentStepRepository steps) {
|
||||
return new DiagnosisAgentFactory(chatModel, core, tools, mapper,
|
||||
List.of(new AgentLoggingHook(steps, DiagnosisAgentFactory.AGENT_NAME)));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DiagnosisAgentUseCase diagnosisAgentUseCase(DiagnosisHarnessCore core,
|
||||
DiagnosisAgentFactory factory,
|
||||
ObjectMapper mapper,
|
||||
ChatHarnessProperties properties) {
|
||||
return new DiagnosisAgentUseCase(core, factory, mapper, new DiagnosisAgentLimits(
|
||||
properties.getDiagnosisMaxQueryBytes(), properties.getDiagnosisMaxPreviousTurnBytes(),
|
||||
properties.getDiagnosisMaxInputBytes(), properties.getDiagnosisMaxDraftBytes()));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public EvidenceGuard evidenceGuard(CanonicalInvocationStore store, ToolCallKeyFactory keyFactory,
|
||||
ObjectMapper mapper) {
|
||||
return new EvidenceGuard(store, keyFactory, mapper);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public EvidenceRepair evidenceRepair(DiagnosisHarnessCore core, HarnessRetryExecutor retries,
|
||||
GuardModelCall modelCall, ObjectMapper mapper,
|
||||
ChatHarnessProperties properties) {
|
||||
return new EvidenceRepair(core, retries, modelCall, mapper,
|
||||
new EvidenceRepairLimits(properties.getRepairMaxInputBytes(), properties.getRepairMaxOutputBytes(),
|
||||
properties.getRepairTimeout()),
|
||||
attempt -> { });
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SemanticGuard semanticGuard(DiagnosisHarnessCore core, HarnessRetryExecutor retries,
|
||||
GuardModelCall modelCall, ObjectMapper mapper,
|
||||
ChatHarnessProperties properties) {
|
||||
return new SemanticGuard(core, retries, modelCall, mapper,
|
||||
new SemanticGuardLimits(properties.getSemanticMaxInputBytes(), properties.getSemanticMaxOutputBytes(),
|
||||
properties.getSemanticAttemptTimeout(), properties.getSemanticTotalTimeout()),
|
||||
attempt -> { });
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DiagnosisReleaseUseCase diagnosisReleaseUseCase(EvidenceGuard evidenceGuard,
|
||||
EvidenceRepair repair,
|
||||
SemanticGuard semanticGuard) {
|
||||
return new DiagnosisReleaseUseCase(evidenceGuard, repair, semanticGuard, new SafeFallbackFactory());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PublishedResultPolicy publishedResultPolicy() {
|
||||
return new PublishedResultPolicy(PreviousTurnLimits.defaults());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IntentRouting intentRouter(DiagnosisHarnessCore core, HarnessRetryExecutor retries,
|
||||
GuardModelCall modelCall, ObjectMapper mapper,
|
||||
ChatHarnessProperties properties) {
|
||||
return new IntentRouter(core, retries, modelCall, mapper,
|
||||
new IntentRouterLimits(properties.getRouterMaxInputBytes(), properties.getRouterMaxOutputBytes(),
|
||||
properties.getRouterAttemptTimeout(), properties.getRouterTotalTimeout()),
|
||||
attempt -> { });
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SystemChatOperation systemChatOperation(DiagnosisHarnessCore core, GuardModelCall modelCall,
|
||||
ObjectMapper mapper, ChatHarnessProperties properties) {
|
||||
return new SystemChatExecutor(core, modelCall,
|
||||
new SingleTurnExecutorLimits(properties.getSystemMaxInputBytes(),
|
||||
properties.getSystemMaxOutputBytes(), properties.getSystemTimeout()));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public KnowledgeQueryOperation knowledgeQueryOperation(DiagnosisHarnessCore core,
|
||||
HarnessEvidenceTools tools,
|
||||
GuardModelCall modelCall,
|
||||
ObjectMapper mapper,
|
||||
ChatHarnessProperties properties) {
|
||||
return new KnowledgeQueryExecutor(core, tools, modelCall, mapper, () -> UUID.randomUUID().toString(),
|
||||
new KnowledgeQueryLimits(properties.getKnowledgeMaxModelInputBytes(),
|
||||
properties.getKnowledgeMaxModelOutputBytes(), properties.getKnowledgeModelTimeout()));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DiagnosisOperation diagnosisOperation(DiagnosisAgentUseCase agent,
|
||||
DiagnosisReleaseUseCase release,
|
||||
PublishedResultPolicy policy) {
|
||||
return new DiagnosisChatExecutor(agent, release, policy);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ChatApplicationUseCase chatApplicationUseCase(DiagnosisHarnessCore core,
|
||||
ChatRunStore runStore,
|
||||
IntentRouting router,
|
||||
SystemChatOperation system,
|
||||
KnowledgeQueryOperation knowledge,
|
||||
DiagnosisOperation diagnosis,
|
||||
ObjectMapper mapper) {
|
||||
return new ChatApplicationUseCase(core, () -> UUID.randomUUID().toString(), runStore,
|
||||
router, system, knowledge, diagnosis, mapper);
|
||||
}
|
||||
|
||||
private static ThreadPoolExecutor boundedExecutor(int core, int max, int queue, String prefix) {
|
||||
return new ThreadPoolExecutor(core, max, 60, TimeUnit.SECONDS,
|
||||
new ArrayBlockingQueue<>(queue), runnable -> {
|
||||
Thread thread = new Thread(runnable);
|
||||
thread.setName(prefix + thread.getId());
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
}, new ThreadPoolExecutor.AbortPolicy());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.superbiz.agent.controller;
|
||||
|
||||
import com.alibaba.cloud.ai.graph.OverAllState;
|
||||
import com.superbiz.agent.dto.AIOpsRequest;
|
||||
import com.superbiz.agent.service.AiOpsService;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
/** Preserves the legacy AiOps SSE protocol independently from Chat. */
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class AiOpsController {
|
||||
|
||||
private final AiOpsService aiOpsService;
|
||||
private final ThreadPoolExecutor workerExecutor;
|
||||
|
||||
public AiOpsController(AiOpsService aiOpsService,
|
||||
@Qualifier("chatWorkerExecutor") ThreadPoolExecutor workerExecutor) {
|
||||
this.aiOpsService = aiOpsService;
|
||||
this.workerExecutor = workerExecutor;
|
||||
}
|
||||
|
||||
@PostMapping(value = "/ai_ops", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public SseEmitter aiOps(@RequestBody(required = false) AIOpsRequest request) {
|
||||
SseEmitter emitter = new SseEmitter(600_000L);
|
||||
String sessionId = aiOpsService.resolveSessionId(request);
|
||||
String runId = aiOpsService.newRunId();
|
||||
|
||||
emitter.onTimeout(emitter::complete);
|
||||
try {
|
||||
workerExecutor.execute(() -> executeAiOps(request, sessionId, runId, emitter));
|
||||
} catch (RejectedExecutionException rejected) {
|
||||
emitter.completeWithError(rejected);
|
||||
}
|
||||
return emitter;
|
||||
}
|
||||
|
||||
private void executeAiOps(AIOpsRequest request, String sessionId, String runId, SseEmitter emitter) {
|
||||
try {
|
||||
emitter.send(SseEmitter.event().name("message")
|
||||
.data(SseMessage.metadata(sessionId, runId), MediaType.APPLICATION_JSON));
|
||||
emitter.send(SseEmitter.event().name("message")
|
||||
.data(SseMessage.content("正在读取告警并拆解任务...\n"), MediaType.APPLICATION_JSON));
|
||||
|
||||
Optional<OverAllState> state = aiOpsService.executeAiOpsAnalysis(request, sessionId, runId);
|
||||
if (state.isEmpty()) {
|
||||
emitter.send(SseEmitter.event().name("message")
|
||||
.data(SseMessage.error("多 Agent 编排未获取到有效结果"), MediaType.APPLICATION_JSON));
|
||||
emitter.complete();
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<String> report = aiOpsService.extractFinalReport(state.get());
|
||||
if (report.isPresent()) {
|
||||
aiOpsService.persistFinalReport(sessionId, runId, report.get(), request);
|
||||
emitter.send(SseEmitter.event().name("message")
|
||||
.data(SseMessage.content(report.get()), MediaType.APPLICATION_JSON));
|
||||
} else {
|
||||
emitter.send(SseEmitter.event().name("message")
|
||||
.data(SseMessage.content("多 Agent 流程已完成,但未能生成最终报告。"), MediaType.APPLICATION_JSON));
|
||||
}
|
||||
emitter.send(SseEmitter.event().name("message")
|
||||
.data(SseMessage.done(), MediaType.APPLICATION_JSON));
|
||||
emitter.complete();
|
||||
} catch (Exception exception) {
|
||||
try {
|
||||
emitter.send(SseEmitter.event().name("message")
|
||||
.data(SseMessage.error("AI Ops 流程失败"), MediaType.APPLICATION_JSON));
|
||||
} catch (IOException ignored) {
|
||||
// Client has already gone away.
|
||||
}
|
||||
emitter.completeWithError(exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class SseMessage {
|
||||
private String type;
|
||||
private String data;
|
||||
private String sessionId;
|
||||
private String runId;
|
||||
|
||||
public static SseMessage content(String data) {
|
||||
SseMessage message = new SseMessage();
|
||||
message.type = "content";
|
||||
message.data = data;
|
||||
return message;
|
||||
}
|
||||
|
||||
public static SseMessage metadata(String sessionId, String runId) {
|
||||
SseMessage message = new SseMessage();
|
||||
message.type = "metadata";
|
||||
message.sessionId = sessionId;
|
||||
message.runId = runId;
|
||||
return message;
|
||||
}
|
||||
|
||||
public static SseMessage error(String data) {
|
||||
SseMessage message = new SseMessage();
|
||||
message.type = "error";
|
||||
message.data = data;
|
||||
return message;
|
||||
}
|
||||
|
||||
public static SseMessage done() {
|
||||
SseMessage message = new SseMessage();
|
||||
message.type = "done";
|
||||
return message;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,521 +1,90 @@
|
||||
package com.superbiz.agent.controller;
|
||||
|
||||
import com.alibaba.cloud.ai.graph.OverAllState;
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.superbiz.agent.config.ChatHarnessProperties;
|
||||
import com.superbiz.agent.controller.sse.ChatSseSession;
|
||||
import com.superbiz.agent.harness.application.ChatApplicationException;
|
||||
import com.superbiz.agent.harness.application.ChatApplicationRequest;
|
||||
import com.superbiz.agent.harness.application.ChatApplicationResult;
|
||||
import com.superbiz.agent.harness.application.ChatApplicationUseCase;
|
||||
import com.superbiz.agent.harness.application.ChatFailureCode;
|
||||
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;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
/**
|
||||
* 统一 API 控制器
|
||||
* 适配前端接口需求
|
||||
*/
|
||||
/** HTTP and SSE protocol adapter for Chat. */
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class ChatController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ChatController.class);
|
||||
private final ChatApplicationUseCase chatApplication;
|
||||
private final ThreadPoolExecutor chatWorkerExecutor;
|
||||
private final ChatHarnessProperties harnessProperties;
|
||||
|
||||
@Autowired
|
||||
private AiOpsService aiOpsService;
|
||||
|
||||
@Autowired
|
||||
private ChatService chatService;
|
||||
public ChatController(ChatApplicationUseCase chatApplication,
|
||||
@Qualifier("chatWorkerExecutor") ThreadPoolExecutor chatWorkerExecutor,
|
||||
ChatHarnessProperties harnessProperties) {
|
||||
this.chatApplication = chatApplication;
|
||||
this.chatWorkerExecutor = chatWorkerExecutor;
|
||||
this.harnessProperties = harnessProperties;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private DiagnosisTraceService diagnosisTraceService;
|
||||
@PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public ResponseEntity<SseEmitter> chat(@RequestBody ChatRequest request) {
|
||||
if (request == null || request.getQuestion() == null || request.getQuestion().isBlank()) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private SessionManager sessionManager;
|
||||
SseEmitter emitter = new SseEmitter(harnessProperties.getSseTimeout().toMillis());
|
||||
ChatSseSession session = new ChatSseSession(emitter);
|
||||
emitter.onTimeout(session::disconnect);
|
||||
emitter.onError(ignored -> session.disconnect());
|
||||
emitter.onCompletion(session::disconnect);
|
||||
|
||||
@Autowired(required = false)
|
||||
private ToolCallbackProvider tools;
|
||||
|
||||
private final ExecutorService executor = Executors.newCachedThreadPool();
|
||||
|
||||
// 最大历史消息窗口大小(成对计算:用户消息+AI回复=1对)
|
||||
private static final int MAX_WINDOW_SIZE = 6;
|
||||
|
||||
@Value("${session.ttl-seconds:3600}")
|
||||
private long sessionTtlSeconds;
|
||||
|
||||
/**
|
||||
* 普通对话接口(支持工具调用)
|
||||
* 与 /chat_react 逻辑一致,但直接返回完整结果而非流式输出
|
||||
*/
|
||||
@PostMapping("/chat")
|
||||
public ResponseEntity<ApiResponse<ChatResponse>> chat(@RequestBody ChatRequest request) {
|
||||
try {
|
||||
logger.info("收到对话请求 - SessionId: {}, Question: {}", request.getId(), request.getQuestion());
|
||||
|
||||
// 参数校验
|
||||
if (request.getQuestion() == null || request.getQuestion().trim().isEmpty()) {
|
||||
logger.warn("问题内容为空");
|
||||
return ResponseEntity.ok(ApiResponse.success(ChatResponse.error("问题内容不能为空")));
|
||||
}
|
||||
|
||||
// 获取或创建会话
|
||||
SessionContext session = getOrCreateSession(request.getId());
|
||||
|
||||
// 获取历史消息
|
||||
List<Map<String, String>> history = session.getMessageHistorySnapshot();
|
||||
logger.info("会话历史消息对数: {}", history.size() / 2);
|
||||
|
||||
// 获取注入的 ChatModel
|
||||
ChatModel chatModel = chatService.getChatModel();
|
||||
|
||||
// 记录可用工具
|
||||
chatService.logAvailableTools();
|
||||
|
||||
ToolCallback[] toolCallbacks = tools != null ? tools.getToolCallbacks() : new ToolCallback[0];
|
||||
|
||||
// 根据问题复杂度自动选择单 Agent 或多 Agent
|
||||
logger.info("开始 ReactAgent 对话(支持自动工具调用)");
|
||||
ChatService.ChatResult result = chatService.executeChatWithStrategy(chatModel, toolCallbacks,
|
||||
request.getQuestion(), history, session.getSessionId());
|
||||
String fullAnswer = result.answer();
|
||||
|
||||
// 更新会话历史
|
||||
session.addChatMessagePair(request.getQuestion(), fullAnswer, MAX_WINDOW_SIZE);
|
||||
sessionManager.updateSession(session);
|
||||
chatService.syncChatSessionMetadata(session.getSessionId(), session.getMessagePairCount());
|
||||
logger.info("已更新会话历史 - SessionId: {}, 当前消息对数: {}",
|
||||
session.getSessionId(), session.getMessagePairCount());
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(ChatResponse.success(fullAnswer, result.sessionId(), result.runId())));
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("对话失败", e);
|
||||
return ResponseEntity.ok(ApiResponse.success(ChatResponse.error(e.getMessage())));
|
||||
chatWorkerExecutor.execute(() -> executeChat(request, session));
|
||||
} catch (RejectedExecutionException rejected) {
|
||||
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build();
|
||||
}
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.TEXT_EVENT_STREAM)
|
||||
.body(emitter);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空会话历史
|
||||
*/
|
||||
@PostMapping("/chat/clear")
|
||||
public ResponseEntity<ApiResponse<String>> clearChatHistory(@RequestBody ClearRequest request) {
|
||||
private void executeChat(ChatRequest request, ChatSseSession session) {
|
||||
try {
|
||||
logger.info("收到清空会话历史请求 - SessionId: {}", request.getId());
|
||||
|
||||
if (request.getId() == null || request.getId().isEmpty()) {
|
||||
return ResponseEntity.ok(ApiResponse.error("会话ID不能为空"));
|
||||
}
|
||||
|
||||
Optional<SessionContext> session = sessionManager.getSession(request.getId());
|
||||
if (session.isPresent()) {
|
||||
SessionContext context = session.get();
|
||||
context.clearMessageHistory();
|
||||
sessionManager.updateSession(context);
|
||||
return ResponseEntity.ok(ApiResponse.success("会话历史已清空"));
|
||||
} else {
|
||||
return ResponseEntity.ok(ApiResponse.error("会话不存在"));
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("清空会话历史失败", e);
|
||||
return ResponseEntity.ok(ApiResponse.error(e.getMessage()));
|
||||
ChatApplicationResult result = chatApplication.execute(
|
||||
new ChatApplicationRequest(request.getQuestion(), request.getId()), session);
|
||||
session.complete(result);
|
||||
} catch (ChatApplicationException exception) {
|
||||
session.fail(exception);
|
||||
} catch (RuntimeException exception) {
|
||||
session.fail(ChatFailureCode.INTERNAL_FAILURE,
|
||||
"当前暂时无法处理该请求,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话接口(SSE 流式模式)
|
||||
* 与 /chat 使用同一条 ChatService 策略链路,区别仅在于通过 SSE 分块返回最终答案。
|
||||
*/
|
||||
@PostMapping(value = "/chat_stream", produces = "text/event-stream;charset=UTF-8")
|
||||
public SseEmitter chatStream(@RequestBody ChatRequest request) {
|
||||
SseEmitter emitter = new SseEmitter(300000L); // 5分钟超时
|
||||
|
||||
// 参数校验
|
||||
if (request.getQuestion() == null || request.getQuestion().trim().isEmpty()) {
|
||||
logger.warn("问题内容为空");
|
||||
try {
|
||||
emitter.send(SseEmitter.event().name("message").data(SseMessage.error("问题内容不能为空"), MediaType.APPLICATION_JSON));
|
||||
emitter.complete();
|
||||
} catch (IOException e) {
|
||||
emitter.completeWithError(e);
|
||||
}
|
||||
return emitter;
|
||||
}
|
||||
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
logger.info("收到 ReactAgent 对话请求 - SessionId: {}, Question: {}", request.getId(), request.getQuestion());
|
||||
|
||||
// 获取或创建会话
|
||||
SessionContext session = getOrCreateSession(request.getId());
|
||||
|
||||
// 获取历史消息
|
||||
List<Map<String, String>> history = session.getMessageHistorySnapshot();
|
||||
logger.info("ReactAgent 会话历史消息对数: {}", history.size() / 2);
|
||||
|
||||
// 获取注入的 ChatModel
|
||||
ChatModel chatModel = chatService.getChatModel();
|
||||
|
||||
// 记录可用工具
|
||||
chatService.logAvailableTools();
|
||||
|
||||
ToolCallback[] toolCallbacks = tools != null ? tools.getToolCallbacks() : new ToolCallback[0];
|
||||
|
||||
logger.info("开始统一 ChatService 对话(SSE 分块返回)");
|
||||
ChatService.ChatResult result = chatService.executeChatWithStrategy(chatModel, toolCallbacks,
|
||||
request.getQuestion(), history, session.getSessionId());
|
||||
String fullAnswer = result.answer() == null ? "" : result.answer();
|
||||
logger.info("统一 ChatService 对话完成 - SessionId: {}, 答案长度: {}",
|
||||
result.sessionId(), fullAnswer.length());
|
||||
|
||||
session.addChatMessagePair(request.getQuestion(), fullAnswer, MAX_WINDOW_SIZE);
|
||||
sessionManager.updateSession(session);
|
||||
logger.info("已更新会话历史 - SessionId: {}, 当前消息对数: {}",
|
||||
session.getSessionId(), session.getMessagePairCount());
|
||||
|
||||
sendContentChunks(emitter, fullAnswer);
|
||||
emitter.send(SseEmitter.event()
|
||||
.name("message")
|
||||
.data(SseMessage.done(), MediaType.APPLICATION_JSON));
|
||||
emitter.complete();
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("ReactAgent 对话初始化失败", e);
|
||||
try {
|
||||
emitter.send(SseEmitter.event()
|
||||
.name("message")
|
||||
.data(SseMessage.error(e.getMessage()), MediaType.APPLICATION_JSON));
|
||||
} catch (IOException ex) {
|
||||
logger.error("发送错误消息失败", ex);
|
||||
}
|
||||
emitter.completeWithError(e);
|
||||
}
|
||||
});
|
||||
|
||||
return emitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 智能运维接口(SSE 流式模式)- 自动分析告警并生成运维报告
|
||||
* 无需用户输入,自动执行告警分析流程
|
||||
*/
|
||||
@PostMapping(value = "/ai_ops", produces = "text/event-stream;charset=UTF-8")
|
||||
public SseEmitter aiOps(@RequestBody(required = false) AIOpsRequest request) {
|
||||
SseEmitter emitter = new SseEmitter(600000L); // 10分钟超时(告警分析可能较慢)
|
||||
String sessionId = aiOpsService.resolveSessionId(request);
|
||||
String runId = aiOpsService.newRunId();
|
||||
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
logger.info("收到 AI 智能运维请求 - SessionId: {}, RunId: {}, 启动多 Agent 协作流程", sessionId, runId);
|
||||
|
||||
ChatModel chatModel = chatService.getChatModel();
|
||||
|
||||
ToolCallback[] toolCallbacks = tools != null ? tools.getToolCallbacks() : new ToolCallback[0];
|
||||
|
||||
emitter.send(SseEmitter.event().name("message").data(SseMessage.metadata(sessionId, runId), MediaType.APPLICATION_JSON));
|
||||
emitter.send(SseEmitter.event().name("message").data(SseMessage.content("正在读取告警并拆解任务...\n")));
|
||||
|
||||
// 调用 AiOpsService 执行分析流程
|
||||
Optional<OverAllState> overAllStateOptional = aiOpsService.executeAiOpsAnalysis(chatModel, toolCallbacks, request, sessionId, runId);
|
||||
|
||||
if (overAllStateOptional.isEmpty()) {
|
||||
emitter.send(SseEmitter.event().name("message")
|
||||
.data(SseMessage.error("多 Agent 编排未获取到有效结果"), MediaType.APPLICATION_JSON));
|
||||
emitter.complete();
|
||||
return;
|
||||
}
|
||||
|
||||
OverAllState state = overAllStateOptional.get();
|
||||
logger.info("AI Ops 编排完成,开始提取最终报告...");
|
||||
|
||||
// 提取最终报告
|
||||
Optional<String> finalReportOptional = aiOpsService.extractFinalReport(state);
|
||||
|
||||
// 输出最终报告
|
||||
if (finalReportOptional.isPresent()) {
|
||||
String finalReportText = finalReportOptional.get();
|
||||
logger.info("提取到 Planner 最终报告,长度: {}", finalReportText.length());
|
||||
aiOpsService.persistFinalReport(sessionId, runId, finalReportText, request);
|
||||
|
||||
// 发送分隔线
|
||||
emitter.send(SseEmitter.event().name("message")
|
||||
.data(SseMessage.content("\n\n" + "=".repeat(60) + "\n"), MediaType.APPLICATION_JSON));
|
||||
|
||||
// 发送完整的告警分析报告
|
||||
emitter.send(SseEmitter.event().name("message")
|
||||
.data(SseMessage.content("📋 **告警分析报告**\n\n"), MediaType.APPLICATION_JSON));
|
||||
|
||||
int chunkSize = 50;
|
||||
for (int i = 0; i < finalReportText.length(); i += chunkSize) {
|
||||
int end = Math.min(i + chunkSize, finalReportText.length());
|
||||
String chunk = finalReportText.substring(i, end);
|
||||
|
||||
emitter.send(SseEmitter.event().name("message")
|
||||
.data(SseMessage.content(chunk), MediaType.APPLICATION_JSON));
|
||||
}
|
||||
|
||||
// 发送结束分隔线
|
||||
emitter.send(SseEmitter.event().name("message")
|
||||
.data(SseMessage.content("\n" + "=".repeat(60) + "\n\n"), MediaType.APPLICATION_JSON));
|
||||
|
||||
logger.info("最终报告已完整输出");
|
||||
} else {
|
||||
logger.warn("未能提取到 Planner 最终报告");
|
||||
emitter.send(SseEmitter.event().name("message")
|
||||
.data(SseMessage.content("⚠️ 多 Agent 流程已完成,但未能生成最终报告。"), MediaType.APPLICATION_JSON));
|
||||
}
|
||||
|
||||
emitter.send(SseEmitter.event().name("message").data(SseMessage.done(), MediaType.APPLICATION_JSON));
|
||||
emitter.complete();
|
||||
logger.info("AI Ops 多 Agent 编排完成");
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("AI Ops 多 Agent 协作失败", e);
|
||||
try {
|
||||
emitter.send(SseEmitter.event().name("message")
|
||||
.data(SseMessage.error("AI Ops 流程失败: " + e.getMessage()), MediaType.APPLICATION_JSON));
|
||||
} catch (IOException ex) {
|
||||
logger.error("发送错误消息失败", ex);
|
||||
}
|
||||
emitter.completeWithError(e);
|
||||
}
|
||||
});
|
||||
|
||||
return emitter;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取会话信息
|
||||
*/
|
||||
@GetMapping("/chat/session/{sessionId}")
|
||||
public ResponseEntity<ApiResponse<SessionInfoResponse>> getSessionInfo(@PathVariable String sessionId) {
|
||||
try {
|
||||
logger.info("收到获取会话信息请求 - SessionId: {}", sessionId);
|
||||
|
||||
Optional<SessionContext> session = sessionManager.getSession(sessionId);
|
||||
if (session.isPresent()) {
|
||||
SessionContext context = session.get();
|
||||
SessionInfoResponse response = new SessionInfoResponse();
|
||||
response.setSessionId(sessionId);
|
||||
response.setMessagePairCount(context.getMessagePairCount());
|
||||
response.setCreateTime(toEpochMillis(context.getCreatedAt()));
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
} else {
|
||||
return ResponseEntity.ok(ApiResponse.error("会话不存在"));
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("获取会话信息失败", e);
|
||||
return ResponseEntity.ok(ApiResponse.error(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@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) {
|
||||
String resolvedSessionId = (sessionId == null || sessionId.isEmpty())
|
||||
? UUID.randomUUID().toString()
|
||||
: sessionId;
|
||||
return sessionManager.getSession(resolvedSessionId)
|
||||
.orElseGet(() -> {
|
||||
SessionContext context = SessionContext.builder()
|
||||
.sessionId(resolvedSessionId)
|
||||
.status("ACTIVE")
|
||||
.ttl(sessionTtlSeconds)
|
||||
.build();
|
||||
sessionManager.createSession(context, sessionTtlSeconds);
|
||||
return context;
|
||||
});
|
||||
}
|
||||
|
||||
private long toEpochMillis(LocalDateTime time) {
|
||||
if (time == null) {
|
||||
return 0L;
|
||||
}
|
||||
return time.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
|
||||
}
|
||||
|
||||
private void sendContentChunks(SseEmitter emitter, String content) throws IOException {
|
||||
if (content == null || content.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
int chunkSize = 80;
|
||||
for (int i = 0; i < content.length(); i += chunkSize) {
|
||||
int end = Math.min(i + chunkSize, content.length());
|
||||
emitter.send(SseEmitter.event()
|
||||
.name("message")
|
||||
.data(SseMessage.content(content.substring(i, end)), MediaType.APPLICATION_JSON));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 聊天请求
|
||||
*/
|
||||
@Setter
|
||||
@Getter
|
||||
@Setter
|
||||
public static class ChatRequest {
|
||||
@com.fasterxml.jackson.annotation.JsonProperty(value = "Id")
|
||||
@com.fasterxml.jackson.annotation.JsonAlias({"id", "ID"})
|
||||
private String Id;
|
||||
|
||||
@com.fasterxml.jackson.annotation.JsonProperty(value = "Question")
|
||||
@com.fasterxml.jackson.annotation.JsonAlias({"question", "QUESTION"})
|
||||
private String Question;
|
||||
|
||||
@JsonProperty("Id")
|
||||
@JsonAlias({"id", "ID"})
|
||||
private String id;
|
||||
@JsonProperty("Question")
|
||||
@JsonAlias({"question", "QUESTION"})
|
||||
private String question;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空会话请求
|
||||
*/
|
||||
@Setter
|
||||
@Getter
|
||||
public static class ClearRequest {
|
||||
@com.fasterxml.jackson.annotation.JsonProperty(value = "Id")
|
||||
@com.fasterxml.jackson.annotation.JsonAlias({"id", "ID"})
|
||||
private String Id;
|
||||
}
|
||||
|
||||
// ==================== 内部类 ====================
|
||||
|
||||
/**
|
||||
* 会话信息响应
|
||||
*/
|
||||
@Setter
|
||||
@Getter
|
||||
public static class SessionInfoResponse {
|
||||
private String sessionId;
|
||||
private int messagePairCount;
|
||||
private long createTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一聊天响应格式
|
||||
* 适用于所有普通返回模式的对话接口
|
||||
*/
|
||||
@Setter
|
||||
@Getter
|
||||
public static class ChatResponse {
|
||||
private boolean success;
|
||||
private String answer;
|
||||
private String errorMessage;
|
||||
private String sessionId;
|
||||
private String runId;
|
||||
|
||||
public static ChatResponse success(String answer, String sessionId, String runId) {
|
||||
ChatResponse response = new ChatResponse();
|
||||
response.setSuccess(true);
|
||||
response.setAnswer(answer);
|
||||
response.setSessionId(sessionId);
|
||||
response.setRunId(runId);
|
||||
return response;
|
||||
}
|
||||
|
||||
public static ChatResponse error(String errorMessage) {
|
||||
ChatResponse response = new ChatResponse();
|
||||
response.setSuccess(false);
|
||||
response.setErrorMessage(errorMessage);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一 SSE 流式消息格式
|
||||
* 适用于所有 SSE 流式返回模式的对话接口
|
||||
*/
|
||||
@Setter
|
||||
@Getter
|
||||
public static class SseMessage {
|
||||
private String type; // metadata: 元数据, content: 内容块, error: 错误, done: 完成
|
||||
private String data;
|
||||
private String sessionId;
|
||||
private String runId;
|
||||
|
||||
public static SseMessage content(String data) {
|
||||
SseMessage message = new SseMessage();
|
||||
message.setType("content");
|
||||
message.setData(data);
|
||||
return message;
|
||||
}
|
||||
|
||||
public static SseMessage session(String sessionId) {
|
||||
SseMessage message = new SseMessage();
|
||||
message.setType("session");
|
||||
message.setData(sessionId);
|
||||
return message;
|
||||
}
|
||||
|
||||
public static SseMessage metadata(String sessionId, String runId) {
|
||||
SseMessage message = new SseMessage();
|
||||
message.setType("metadata");
|
||||
message.setSessionId(sessionId);
|
||||
message.setRunId(runId);
|
||||
return message;
|
||||
}
|
||||
|
||||
public static SseMessage error(String errorMessage) {
|
||||
SseMessage message = new SseMessage();
|
||||
message.setType("error");
|
||||
message.setData(errorMessage);
|
||||
return message;
|
||||
}
|
||||
|
||||
public static SseMessage done() {
|
||||
SseMessage message = new SseMessage();
|
||||
message.setType("done");
|
||||
message.setData(null);
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class ApiResponse<T> {
|
||||
private int code;
|
||||
private String message;
|
||||
private T data;
|
||||
|
||||
public static <T> ApiResponse<T> success(T data) {
|
||||
ApiResponse<T> response = new ApiResponse<>();
|
||||
response.setCode(200);
|
||||
response.setMessage("success");
|
||||
response.setData(data);
|
||||
return response;
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> error(String message) {
|
||||
ApiResponse<T> response = new ApiResponse<>();
|
||||
response.setCode(500);
|
||||
response.setMessage(message);
|
||||
return response;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.superbiz.agent.controller;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.superbiz.agent.dto.DiagnosisTraceResponse;
|
||||
import com.superbiz.agent.service.DiagnosisTraceService;
|
||||
import com.superbiz.agent.service.session.SessionManager;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
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.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Legacy Chat session management endpoints, isolated from the Chat execution adapter. */
|
||||
@RestController
|
||||
@RequestMapping("/api/chat")
|
||||
public class ChatSessionController {
|
||||
|
||||
private final DiagnosisTraceService diagnosisTraceService;
|
||||
private final SessionManager sessionManager;
|
||||
|
||||
public ChatSessionController(DiagnosisTraceService diagnosisTraceService, SessionManager sessionManager) {
|
||||
this.diagnosisTraceService = diagnosisTraceService;
|
||||
this.sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
@PostMapping("/clear")
|
||||
public ResponseEntity<ApiResponse<String>> clearChatHistory(@RequestBody ClearRequest request) {
|
||||
if (request == null || request.getId() == null || request.getId().isBlank()) {
|
||||
return ResponseEntity.ok(ApiResponse.error("会话ID不能为空"));
|
||||
}
|
||||
try {
|
||||
Optional<com.superbiz.agent.domain.model.SessionContext> session =
|
||||
sessionManager.getSession(request.getId());
|
||||
if (session.isEmpty()) {
|
||||
return ResponseEntity.ok(ApiResponse.error("会话不存在"));
|
||||
}
|
||||
var context = session.get();
|
||||
context.clearMessageHistory();
|
||||
sessionManager.updateSession(context);
|
||||
return ResponseEntity.ok(ApiResponse.success("会话历史已清空"));
|
||||
} catch (RuntimeException exception) {
|
||||
return ResponseEntity.ok(ApiResponse.error("会话历史清理失败"));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/session/{sessionId}")
|
||||
public ResponseEntity<ApiResponse<SessionInfoResponse>> getSessionInfo(@PathVariable String sessionId) {
|
||||
try {
|
||||
Optional<com.superbiz.agent.domain.model.SessionContext> session = sessionManager.getSession(sessionId);
|
||||
if (session.isEmpty()) {
|
||||
return ResponseEntity.ok(ApiResponse.error("会话不存在"));
|
||||
}
|
||||
var context = session.get();
|
||||
SessionInfoResponse response = new SessionInfoResponse();
|
||||
response.setSessionId(sessionId);
|
||||
response.setMessagePairCount(context.getMessagePairCount());
|
||||
response.setCreateTime(toEpochMillis(context.getCreatedAt()));
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
} catch (RuntimeException exception) {
|
||||
return ResponseEntity.ok(ApiResponse.error("会话读取失败"));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/session/{sessionId}/runs")
|
||||
public ResponseEntity<ApiResponse<List<DiagnosisTraceResponse.RunSummary>>> listSessionRuns(
|
||||
@PathVariable String sessionId) {
|
||||
return ResponseEntity.ok(ApiResponse.success(diagnosisTraceService.listRunSummaries(sessionId)));
|
||||
}
|
||||
|
||||
private long toEpochMillis(LocalDateTime time) {
|
||||
return time == null ? 0L : time.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class ClearRequest {
|
||||
@JsonProperty("Id")
|
||||
@JsonAlias({"id", "ID"})
|
||||
private String id;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class SessionInfoResponse {
|
||||
private String sessionId;
|
||||
private int messagePairCount;
|
||||
private long createTime;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class ApiResponse<T> {
|
||||
private int code;
|
||||
private String message;
|
||||
private T data;
|
||||
|
||||
public static <T> ApiResponse<T> success(T data) {
|
||||
ApiResponse<T> response = new ApiResponse<>();
|
||||
response.code = 200;
|
||||
response.message = "success";
|
||||
response.data = data;
|
||||
return response;
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> error(String message) {
|
||||
ApiResponse<T> response = new ApiResponse<>();
|
||||
response.code = 500;
|
||||
response.message = message;
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.superbiz.agent.controller.sse;
|
||||
|
||||
final class ChatSseDisconnectedException extends RuntimeException {
|
||||
|
||||
ChatSseDisconnectedException(Throwable cause) {
|
||||
super("Chat SSE connection is no longer writable", cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.superbiz.agent.controller.sse;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.superbiz.agent.harness.application.ChatApplicationContent;
|
||||
import com.superbiz.agent.harness.application.ChatApplicationStatus;
|
||||
import com.superbiz.agent.harness.application.ChatContentType;
|
||||
import com.superbiz.agent.harness.application.ChatFailureCode;
|
||||
import com.superbiz.agent.harness.contract.ReleaseOutcome;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public interface ChatSseEvent {
|
||||
|
||||
record Metadata(
|
||||
@JsonProperty("session_id") String sessionId,
|
||||
@JsonProperty("run_id") String runId) implements ChatSseEvent {
|
||||
public Metadata {
|
||||
requireText(sessionId, "sessionId");
|
||||
requireText(runId, "runId");
|
||||
}
|
||||
}
|
||||
|
||||
record Status(String code, String message) implements ChatSseEvent {
|
||||
public Status {
|
||||
requireText(code, "code");
|
||||
requireText(message, "message");
|
||||
}
|
||||
|
||||
public static Status from(ChatApplicationStatus status) {
|
||||
Objects.requireNonNull(status, "status must not be null");
|
||||
return new Status(status.name(), status.message());
|
||||
}
|
||||
}
|
||||
|
||||
record Content(
|
||||
@JsonProperty("content_type") ChatContentType contentType,
|
||||
@JsonProperty("payload") ChatApplicationContent payload) implements ChatSseEvent {
|
||||
public Content {
|
||||
Objects.requireNonNull(contentType, "contentType must not be null");
|
||||
Objects.requireNonNull(payload, "payload must not be null");
|
||||
if (contentType != payload.contentType()) {
|
||||
throw new IllegalArgumentException("contentType does not match payload");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
record Failure(ChatFailureCode code, String message) implements ChatSseEvent {
|
||||
public Failure {
|
||||
Objects.requireNonNull(code, "code must not be null");
|
||||
requireText(message, "message");
|
||||
}
|
||||
}
|
||||
|
||||
record Done(ReleaseOutcome outcome) implements ChatSseEvent {
|
||||
public Done {
|
||||
Objects.requireNonNull(outcome, "outcome must not be null");
|
||||
if (outcome == ReleaseOutcome.CANCELLED) {
|
||||
throw new IllegalArgumentException("CANCELLED is not a public SSE outcome");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireText(String value, String name) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException(name + " must not be blank");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.superbiz.agent.controller.sse;
|
||||
|
||||
import com.superbiz.agent.harness.application.ChatApplicationException;
|
||||
import com.superbiz.agent.harness.application.ChatApplicationObserver;
|
||||
import com.superbiz.agent.harness.application.ChatApplicationResult;
|
||||
import com.superbiz.agent.harness.application.ChatApplicationStatus;
|
||||
import com.superbiz.agent.harness.application.ChatFailureCode;
|
||||
import com.superbiz.agent.harness.application.ChatRunControl;
|
||||
import com.superbiz.agent.harness.contract.ReleaseOutcome;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class ChatSseSession implements ChatApplicationObserver {
|
||||
|
||||
private enum State {
|
||||
NEW,
|
||||
OPEN,
|
||||
TERMINAL,
|
||||
DISCONNECTED
|
||||
}
|
||||
|
||||
private final ChatSseSink sink;
|
||||
private State state = State.NEW;
|
||||
private ChatRunControl runControl;
|
||||
|
||||
public ChatSseSession(SseEmitter emitter) {
|
||||
this(new SseEmitterChatSink(emitter));
|
||||
}
|
||||
|
||||
ChatSseSession(ChatSseSink sink) {
|
||||
this.sink = Objects.requireNonNull(sink, "sink must not be null");
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onStarted(ChatRunControl control) {
|
||||
Objects.requireNonNull(control, "runControl must not be null");
|
||||
if (runControl != null) {
|
||||
throw new IllegalStateException("Run control was already published");
|
||||
}
|
||||
runControl = control;
|
||||
if (state == State.DISCONNECTED) {
|
||||
control.cancelClientDisconnect();
|
||||
return;
|
||||
}
|
||||
requireState(State.NEW);
|
||||
send("metadata", new ChatSseEvent.Metadata(control.sessionId(), control.runId()));
|
||||
state = State.OPEN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onStatus(ChatApplicationStatus status) {
|
||||
if (state == State.DISCONNECTED) {
|
||||
return;
|
||||
}
|
||||
requireState(State.OPEN);
|
||||
send("status", ChatSseEvent.Status.from(status));
|
||||
}
|
||||
|
||||
public synchronized boolean complete(ChatApplicationResult result) {
|
||||
Objects.requireNonNull(result, "result must not be null");
|
||||
if (state == State.DISCONNECTED || state == State.TERMINAL) {
|
||||
return false;
|
||||
}
|
||||
requireState(State.OPEN);
|
||||
requireMatchingRun(result);
|
||||
send("content", new ChatSseEvent.Content(result.contentType(), result.content()));
|
||||
send("done", new ChatSseEvent.Done(result.outcome()));
|
||||
state = State.TERMINAL;
|
||||
sink.complete();
|
||||
return true;
|
||||
}
|
||||
|
||||
public synchronized boolean fail(ChatApplicationException failure) {
|
||||
Objects.requireNonNull(failure, "failure must not be null");
|
||||
return fail(failure.code(), failure.getMessage());
|
||||
}
|
||||
|
||||
public synchronized boolean fail(ChatFailureCode code, String message) {
|
||||
if (state == State.DISCONNECTED || state == State.TERMINAL) {
|
||||
return false;
|
||||
}
|
||||
if (state == State.NEW) {
|
||||
state = State.TERMINAL;
|
||||
sink.complete();
|
||||
return false;
|
||||
}
|
||||
requireState(State.OPEN);
|
||||
send("failure", new ChatSseEvent.Failure(code, message));
|
||||
send("done", new ChatSseEvent.Done(ReleaseOutcome.FAILED));
|
||||
state = State.TERMINAL;
|
||||
sink.complete();
|
||||
return true;
|
||||
}
|
||||
|
||||
public synchronized boolean disconnect() {
|
||||
if (state == State.DISCONNECTED || state == State.TERMINAL) {
|
||||
return false;
|
||||
}
|
||||
state = State.DISCONNECTED;
|
||||
return runControl != null && runControl.cancelClientDisconnect();
|
||||
}
|
||||
|
||||
public synchronized boolean terminal() {
|
||||
return state == State.TERMINAL;
|
||||
}
|
||||
|
||||
private void requireMatchingRun(ChatApplicationResult result) {
|
||||
if (runControl == null
|
||||
|| !runControl.sessionId().equals(result.sessionId())
|
||||
|| !runControl.runId().equals(result.runId())) {
|
||||
disconnect();
|
||||
throw new IllegalStateException("Application result does not match SSE Run metadata");
|
||||
}
|
||||
}
|
||||
|
||||
private void send(String name, ChatSseEvent payload) {
|
||||
try {
|
||||
sink.send(name, payload);
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
disconnect();
|
||||
throw new ChatSseDisconnectedException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void requireState(State expected) {
|
||||
if (state != expected) {
|
||||
throw new IllegalStateException(
|
||||
"Invalid Chat SSE state: expected " + expected + " but was " + state);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.superbiz.agent.controller.sse;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
interface ChatSseSink {
|
||||
|
||||
void send(String name, Object payload) throws IOException;
|
||||
|
||||
void complete();
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.superbiz.agent.controller.sse;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
final class SseEmitterChatSink implements ChatSseSink {
|
||||
|
||||
private final SseEmitter emitter;
|
||||
|
||||
SseEmitterChatSink(SseEmitter emitter) {
|
||||
this.emitter = Objects.requireNonNull(emitter, "emitter must not be null");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(String name, Object payload) throws IOException {
|
||||
emitter.send(SseEmitter.event()
|
||||
.name(name)
|
||||
.data(payload, MediaType.APPLICATION_JSON));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void complete() {
|
||||
emitter.complete();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -32,7 +32,6 @@ public class JpaChatRunStore implements ChatRunStore {
|
||||
private final ObjectReader publishedReader;
|
||||
private final PublishedResultPolicy publishedPolicy;
|
||||
|
||||
@Autowired
|
||||
public JpaChatRunStore(ChatSessionRepository chatSessions,
|
||||
DiagnosisRunRepository runs,
|
||||
ObjectMapper objectMapper) {
|
||||
@@ -40,6 +39,7 @@ public class JpaChatRunStore implements ChatRunStore {
|
||||
new PublishedResultPolicy(PreviousTurnLimits.defaults()));
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public JpaChatRunStore(ChatSessionRepository chatSessions,
|
||||
DiagnosisRunRepository runs,
|
||||
ObjectMapper objectMapper,
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.ai.chat.messages.AssistantMessage;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.superbiz.agent.config.AiOpsPromptProperties;
|
||||
@@ -47,6 +48,12 @@ public class AiOpsService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AiOpsService.class);
|
||||
|
||||
@Autowired
|
||||
private ChatModel chatModel;
|
||||
|
||||
@Autowired(required = false)
|
||||
private ToolCallbackProvider toolCallbackProvider;
|
||||
|
||||
@Autowired
|
||||
private DateTimeTools dateTimeTools;
|
||||
|
||||
@@ -157,6 +164,15 @@ public class AiOpsService {
|
||||
}
|
||||
}
|
||||
|
||||
/** Internal dependency-owning overload used by the protocol-only Controller. */
|
||||
public Optional<OverAllState> executeAiOpsAnalysis(AIOpsRequest request,
|
||||
String sessionId,
|
||||
String runId) throws GraphRunnerException {
|
||||
ToolCallback[] callbacks = toolCallbackProvider == null
|
||||
? new ToolCallback[0] : toolCallbackProvider.getToolCallbacks();
|
||||
return executeAiOpsAnalysis(chatModel, callbacks, request, sessionId, runId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从多 Agent 执行状态中提取最终报告文本。
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user