|
|
@@ -0,0 +1,281 @@
|
|
|
+package com.emoon.openplatform.service.impl;
|
|
|
+
|
|
|
+import cn.hutool.core.util.IdUtil;
|
|
|
+import cn.hutool.core.util.StrUtil;
|
|
|
+import cn.hutool.json.JSONUtil;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
|
|
+import com.emoon.common.core.exception.exception.ServiceException;
|
|
|
+import com.emoon.mcp.domain.AiConversation;
|
|
|
+import com.emoon.mcp.engine.AgentEngine;
|
|
|
+import com.emoon.mcp.engine.AgentEngineFactory;
|
|
|
+import com.emoon.mcp.engine.AgentRequest;
|
|
|
+import com.emoon.mcp.engine.AgentResponse;
|
|
|
+import com.emoon.mcp.mapper.AiConversationMapper;
|
|
|
+import com.emoon.openplatform.domain.dto.request.AgentChatRequest;
|
|
|
+import com.emoon.openplatform.domain.dto.resp.AgentChatResponse;
|
|
|
+import com.emoon.openplatform.enums.SystemEnum;
|
|
|
+import com.emoon.openplatform.service.IAgentChatService;
|
|
|
+import com.emoon.system.domain.AiAgentApp;
|
|
|
+import com.emoon.system.domain.AiAgentEngineConfig;
|
|
|
+import com.emoon.system.domain.AiUsageLog;
|
|
|
+import com.emoon.system.mapper.AiAgentAppMapper;
|
|
|
+import com.emoon.system.mapper.AiAgentEngineConfigMapper;
|
|
|
+import com.emoon.system.mapper.AiUsageLogMapper;
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.scheduling.annotation.Async;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
|
|
+
|
|
|
+import java.io.IOException;
|
|
|
+import java.util.Date;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 对话编排实现(核心类)
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+@RequiredArgsConstructor
|
|
|
+public class AgentChatServiceImpl implements IAgentChatService {
|
|
|
+
|
|
|
+ private final AgentEngineFactory agentEngineFactory;
|
|
|
+ private final AiAgentAppMapper aiAgentAppMapper;
|
|
|
+ private final AiAgentEngineConfigMapper aiAgentEngineConfigMapper;
|
|
|
+ private final AiConversationMapper aiConversationMapper;
|
|
|
+ private final AiUsageLogMapper aiUsageLogMapper;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public AgentChatResponse chat(AgentChatRequest request, Long projectId, String tenantId) {
|
|
|
+ // 1. 查询智能体配置
|
|
|
+ AgentContext ctx = prepareContext(request, projectId, tenantId);
|
|
|
+
|
|
|
+ // 2. 获取引擎并执行对话
|
|
|
+ AgentEngine engine = agentEngineFactory.getEngine(ctx.engineType());
|
|
|
+ AgentRequest agentRequest = buildAgentRequest(request, ctx);
|
|
|
+ AgentResponse response = engine.chat(agentRequest);
|
|
|
+
|
|
|
+ // 3. 会话管理
|
|
|
+ String conversationId = handleConversation(request, ctx, response, tenantId);
|
|
|
+
|
|
|
+ // 4. 异步写入使用日志
|
|
|
+ asyncLogUsage(ctx, response, tenantId);
|
|
|
+
|
|
|
+ // 5. 转换响应
|
|
|
+ return toResponse(response, conversationId);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void chatStream(AgentChatRequest request, Long projectId, String tenantId, SseEmitter emitter) {
|
|
|
+ try {
|
|
|
+ // 1. 查询智能体配置
|
|
|
+ AgentContext ctx = prepareContext(request, projectId, tenantId);
|
|
|
+
|
|
|
+ // 2. 获取引擎
|
|
|
+ AgentEngine engine = agentEngineFactory.getEngine(ctx.engineType());
|
|
|
+ AgentRequest agentRequest = buildAgentRequest(request, ctx);
|
|
|
+
|
|
|
+ // 会话 ID 用于流式响应
|
|
|
+ final String[] conversationIdHolder = {request.getConversationId()};
|
|
|
+ if (StrUtil.isBlank(conversationIdHolder[0])) {
|
|
|
+ conversationIdHolder[0] = IdUtil.fastSimpleUUID();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 流式对话
|
|
|
+ engine.chatStream(agentRequest, chunk -> {
|
|
|
+ try {
|
|
|
+ AgentChatResponse resp = toResponse(chunk, conversationIdHolder[0]);
|
|
|
+ emitter.send(SseEmitter.event().data(JSONUtil.toJsonStr(resp)));
|
|
|
+
|
|
|
+ // 最后一个 chunk 处理会话和日志
|
|
|
+ if (Boolean.TRUE.equals(chunk.getFinished())) {
|
|
|
+ handleConversation(request, ctx, chunk, tenantId);
|
|
|
+ asyncLogUsage(ctx, chunk, tenantId);
|
|
|
+ }
|
|
|
+ } catch (IOException e) {
|
|
|
+ log.warn("SSE 发送失败(客户端可能断连): {}", e.getMessage());
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ emitter.complete();
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("流式对话异常: {}", e.getMessage(), e);
|
|
|
+ emitter.completeWithError(e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 准备对话上下文
|
|
|
+ */
|
|
|
+ private AgentContext prepareContext(AgentChatRequest request, Long projectId, String tenantId) {
|
|
|
+ // 查询智能体
|
|
|
+ AiAgentApp app = aiAgentAppMapper.selectOne(
|
|
|
+ new LambdaQueryWrapper<AiAgentApp>()
|
|
|
+ .eq(AiAgentApp::getAgentId, request.getAgentId())
|
|
|
+ .eq(AiAgentApp::getDelFlag, "0")
|
|
|
+ );
|
|
|
+ if (app == null) {
|
|
|
+ throw new ServiceException(SystemEnum.AGENT_NOT_FOUND.getMessage(), SystemEnum.AGENT_NOT_FOUND.getCode());
|
|
|
+ }
|
|
|
+ if (!"0".equals(app.getStatus())) {
|
|
|
+ throw new ServiceException(SystemEnum.AGENT_DISABLED.getMessage(), SystemEnum.AGENT_DISABLED.getCode());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查询引擎配置
|
|
|
+ AiAgentEngineConfig config = aiAgentEngineConfigMapper.selectOne(
|
|
|
+ new LambdaQueryWrapper<AiAgentEngineConfig>()
|
|
|
+ .eq(AiAgentEngineConfig::getId, app.getEngineConfigId())
|
|
|
+ .eq(AiAgentEngineConfig::getDelFlag, "0")
|
|
|
+ );
|
|
|
+ if (config == null) {
|
|
|
+ throw new ServiceException(SystemEnum.ENGINE_CONFIG_NOT_FOUND.getMessage(), SystemEnum.ENGINE_CONFIG_NOT_FOUND.getCode());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查询现有会话
|
|
|
+ AiConversation conversation = null;
|
|
|
+ if (StrUtil.isNotBlank(request.getConversationId())) {
|
|
|
+ conversation = aiConversationMapper.selectOne(
|
|
|
+ new LambdaQueryWrapper<AiConversation>()
|
|
|
+ .eq(AiConversation::getConversationId, request.getConversationId())
|
|
|
+ );
|
|
|
+ if (conversation == null) {
|
|
|
+ throw new ServiceException(SystemEnum.CONVERSATION_NOT_FOUND.getMessage(), SystemEnum.CONVERSATION_NOT_FOUND.getCode());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return new AgentContext(app, config, conversation, projectId);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建 AgentRequest
|
|
|
+ */
|
|
|
+ private AgentRequest buildAgentRequest(AgentChatRequest request, AgentContext ctx) {
|
|
|
+ return AgentRequest.builder()
|
|
|
+ .agentId(request.getAgentId())
|
|
|
+ .conversationId(ctx.conversation != null ? ctx.conversation.getExternalConversationId() : null)
|
|
|
+ .query(request.getQuery())
|
|
|
+ .userId(request.getUserId())
|
|
|
+ .engineConfig(ctx.config.getConfigJson())
|
|
|
+ .inputs(request.getInputs())
|
|
|
+ .files(request.getFiles())
|
|
|
+ .build();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理会话(新建或更新)
|
|
|
+ */
|
|
|
+ private String handleConversation(AgentChatRequest request, AgentContext ctx, AgentResponse response, String tenantId) {
|
|
|
+ String conversationId = request.getConversationId();
|
|
|
+
|
|
|
+ if (StrUtil.isBlank(conversationId)) {
|
|
|
+ // 新建会话
|
|
|
+ conversationId = IdUtil.fastSimpleUUID();
|
|
|
+ AiConversation conversation = new AiConversation();
|
|
|
+ conversation.setProjectId(ctx.projectId.intValue());
|
|
|
+ conversation.setAgentId(ctx.app.getId());
|
|
|
+ conversation.setConversationId(conversationId);
|
|
|
+ conversation.setConversationName(truncate(request.getQuery(), 50));
|
|
|
+ conversation.setUserId(request.getUserId());
|
|
|
+ conversation.setEngineType(ctx.config.getEngineType());
|
|
|
+ conversation.setExternalConversationId(response.getConversationId());
|
|
|
+ conversation.setStatus("active");
|
|
|
+ conversation.setMessageCount(1);
|
|
|
+ conversation.setTotalTokens(getTotalTokens(response));
|
|
|
+ conversation.setLastMessageTime(new Date());
|
|
|
+ conversation.setTenantId(tenantId);
|
|
|
+ aiConversationMapper.insert(conversation);
|
|
|
+ } else {
|
|
|
+ // 更新会话
|
|
|
+ aiConversationMapper.update(null,
|
|
|
+ new LambdaUpdateWrapper<AiConversation>()
|
|
|
+ .eq(AiConversation::getConversationId, conversationId)
|
|
|
+ .setSql("message_count = message_count + 1")
|
|
|
+ .setSql("total_tokens = total_tokens + " + getTotalTokens(response))
|
|
|
+ .set(AiConversation::getLastMessageTime, new Date())
|
|
|
+ .set(AiConversation::getExternalConversationId, response.getConversationId())
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return conversationId;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 异步写入使用日志
|
|
|
+ */
|
|
|
+ @Async
|
|
|
+ public void asyncLogUsage(AgentContext ctx, AgentResponse response, String tenantId) {
|
|
|
+ try {
|
|
|
+ if (response.getUsage() == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ AiUsageLog usageLog = new AiUsageLog();
|
|
|
+ usageLog.setAgentId(ctx.app.getId());
|
|
|
+ usageLog.setEngineType(ctx.config.getEngineType());
|
|
|
+ usageLog.setPromptTokens(response.getUsage().getPromptTokens());
|
|
|
+ usageLog.setCompletionTokens(response.getUsage().getCompletionTokens());
|
|
|
+ usageLog.setTotalTokens(response.getUsage().getTotalTokens());
|
|
|
+ usageLog.setTenantId(tenantId);
|
|
|
+ aiUsageLogMapper.insert(usageLog);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("写入使用日志失败: {}", e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 转换为响应对象
|
|
|
+ */
|
|
|
+ private AgentChatResponse toResponse(AgentResponse response, String conversationId) {
|
|
|
+ AgentChatResponse.Usage usage = null;
|
|
|
+ if (response.getUsage() != null) {
|
|
|
+ usage = AgentChatResponse.Usage.builder()
|
|
|
+ .promptTokens(response.getUsage().getPromptTokens())
|
|
|
+ .completionTokens(response.getUsage().getCompletionTokens())
|
|
|
+ .totalTokens(response.getUsage().getTotalTokens())
|
|
|
+ .build();
|
|
|
+ }
|
|
|
+
|
|
|
+ return AgentChatResponse.builder()
|
|
|
+ .reply(response.getReply())
|
|
|
+ .cardKey(response.getCardKey())
|
|
|
+ .cardData(response.getCardData())
|
|
|
+ .conversationId(conversationId)
|
|
|
+ .messageId(response.getMessageId())
|
|
|
+ .usage(usage)
|
|
|
+ .finished(response.getFinished())
|
|
|
+ .build();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取 token 消耗
|
|
|
+ */
|
|
|
+ private int getTotalTokens(AgentResponse response) {
|
|
|
+ if (response.getUsage() == null || response.getUsage().getTotalTokens() == null) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ return response.getUsage().getTotalTokens();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 截断字符串
|
|
|
+ */
|
|
|
+ private String truncate(String str, int maxLen) {
|
|
|
+ if (StrUtil.isBlank(str)) {
|
|
|
+ return "新会话";
|
|
|
+ }
|
|
|
+ return str.length() > maxLen ? str.substring(0, maxLen) : str;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 对话上下文
|
|
|
+ */
|
|
|
+ private record AgentContext(
|
|
|
+ AiAgentApp app,
|
|
|
+ AiAgentEngineConfig config,
|
|
|
+ AiConversation conversation,
|
|
|
+ Long projectId
|
|
|
+ ) {
|
|
|
+ String engineType() {
|
|
|
+ return config.getEngineType();
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|