diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java index c09aa141..0052c7cc 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java @@ -44,6 +44,7 @@ import xiaozhi.modules.agent.entity.AgentEntity; import xiaozhi.modules.agent.entity.AgentTemplateEntity; import xiaozhi.modules.agent.service.AgentChatAudioService; import xiaozhi.modules.agent.service.AgentChatHistoryService; +import xiaozhi.modules.agent.service.AgentChatSummaryService; import xiaozhi.modules.agent.service.AgentContextProviderService; import xiaozhi.modules.agent.service.AgentPluginMappingService; import xiaozhi.modules.agent.service.AgentService; @@ -66,6 +67,7 @@ public class AgentController { private final AgentChatAudioService agentChatAudioService; private final AgentPluginMappingService agentPluginMappingService; private final AgentContextProviderService agentContextProviderService; + private final AgentChatSummaryService agentChatSummaryService; private final RedisUtils redisUtils; @GetMapping("/list") @@ -119,6 +121,27 @@ public class AgentController { return new Result<>(); } + @PostMapping("/chat-summary/{sessionId}/save") + @Operation(summary = "根据会话ID生成聊天记录总结并保存(异步执行)") + public Result generateAndSaveChatSummary(@PathVariable String sessionId) { + try { + // 异步执行总结生成任务,立即返回成功响应 + new Thread(() -> { + try { + agentChatSummaryService.generateAndSaveChatSummary(sessionId); + System.out.println("异步执行会话 " + sessionId + " 的聊天记录总结完成"); + } catch (Exception e) { + System.err.println("异步执行会话 " + sessionId + " 的聊天记录总结失败: " + e.getMessage()); + } + }).start(); + + // 立即返回成功响应,不等待总结生成完成 + return new Result().ok(null); + } catch (Exception e) { + return new Result().error("启动异步总结生成任务失败: " + e.getMessage()); + } + } + @PutMapping("/{id}") @Operation(summary = "更新智能体") @RequiresPermissions("sys:role:normal") @@ -186,6 +209,7 @@ public class AgentController { List result = agentChatHistoryService.getChatHistoryBySessionId(id, sessionId); return new Result>().ok(result); } + @GetMapping("/{id}/chat-history/user") @Operation(summary = "获取智能体聊天记录(用户)") @RequiresPermissions("sys:role:normal") diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentChatSummaryDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentChatSummaryDTO.java new file mode 100644 index 00000000..f6fbe660 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentChatSummaryDTO.java @@ -0,0 +1,45 @@ +package xiaozhi.modules.agent.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 智能体聊天记录总结DTO + */ +@Data +@Schema(description = "智能体聊天记录总结对象") +public class AgentChatSummaryDTO { + + @Schema(description = "会话ID") + private String sessionId; + + @Schema(description = "智能体ID") + private String agentId; + + @Schema(description = "总结内容") + private String summary; + + @Schema(description = "总结状态") + private boolean success; + + @Schema(description = "错误信息") + private String errorMessage; + + public AgentChatSummaryDTO() { + this.success = true; + } + + public AgentChatSummaryDTO(String sessionId, String agentId, String summary) { + this.sessionId = sessionId; + this.agentId = agentId; + this.summary = summary; + this.success = true; + } + + public AgentChatSummaryDTO(String sessionId, String errorMessage) { + this.sessionId = sessionId; + this.errorMessage = errorMessage; + this.success = false; + } + +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatSummaryService.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatSummaryService.java new file mode 100644 index 00000000..418ee9a0 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatSummaryService.java @@ -0,0 +1,15 @@ +package xiaozhi.modules.agent.service; + +/** + * 智能体聊天记录总结服务接口 + */ +public interface AgentChatSummaryService { + + /** + * 根据会话ID生成聊天记录总结并保存到智能体记忆 + * + * @param sessionId 会话ID + * @return 保存结果 + */ + boolean generateAndSaveChatSummary(String sessionId); +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/impl/AgentChatHistoryBizServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/impl/AgentChatHistoryBizServiceImpl.java index 36b59993..8831bb1a 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/impl/AgentChatHistoryBizServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/impl/AgentChatHistoryBizServiceImpl.java @@ -17,6 +17,7 @@ import xiaozhi.modules.agent.entity.AgentChatHistoryEntity; import xiaozhi.modules.agent.entity.AgentEntity; import xiaozhi.modules.agent.service.AgentChatAudioService; import xiaozhi.modules.agent.service.AgentChatHistoryService; +import xiaozhi.modules.agent.service.AgentChatSummaryService; import xiaozhi.modules.agent.service.AgentService; import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService; import xiaozhi.modules.device.entity.DeviceEntity; @@ -36,6 +37,7 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic private final AgentService agentService; private final AgentChatHistoryService agentChatHistoryService; private final AgentChatAudioService agentChatAudioService; + private final AgentChatSummaryService agentChatSummaryService; private final RedisUtils redisUtils; private final DeviceService deviceService; @@ -50,7 +52,8 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic public Boolean report(AgentChatHistoryReportDTO report) { String macAddress = report.getMacAddress(); Byte chatType = report.getChatType(); - Long reportTimeMillis = null != report.getReportTime() ? report.getReportTime() * 1000 : System.currentTimeMillis(); + Long reportTimeMillis = null != report.getReportTime() ? report.getReportTime() * 1000 + : System.currentTimeMillis(); log.info("小智设备聊天上报请求: macAddress={}, type={} reportTime={}", macAddress, chatType, reportTimeMillis); // 根据设备MAC地址查询对应的默认智能体,判断是否需要上报 @@ -105,7 +108,8 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic /** * 组装上报数据 */ - private void saveChatText(AgentChatHistoryReportDTO report, String agentId, String macAddress, String audioId, Long reportTime) { + private void saveChatText(AgentChatHistoryReportDTO report, String agentId, String macAddress, String audioId, + Long reportTime) { // 构建聊天记录实体 AgentChatHistoryEntity entity = AgentChatHistoryEntity.builder() .macAddress(macAddress) diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatSummaryServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatSummaryServiceImpl.java new file mode 100644 index 00000000..f367891f --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatSummaryServiceImpl.java @@ -0,0 +1,423 @@ +package xiaozhi.modules.agent.service.impl; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; + +import lombok.RequiredArgsConstructor; +import xiaozhi.modules.agent.dto.AgentChatHistoryDTO; +import xiaozhi.modules.agent.dto.AgentChatSummaryDTO; +import xiaozhi.modules.agent.dto.AgentMemoryDTO; +import xiaozhi.modules.agent.dto.AgentUpdateDTO; +import xiaozhi.modules.agent.entity.AgentChatHistoryEntity; +import xiaozhi.modules.agent.service.AgentChatHistoryService; +import xiaozhi.modules.agent.service.AgentChatSummaryService; +import xiaozhi.modules.agent.service.AgentService; +import xiaozhi.modules.agent.vo.AgentInfoVO; +import xiaozhi.modules.device.entity.DeviceEntity; +import xiaozhi.modules.device.service.DeviceService; +import xiaozhi.modules.llm.service.LLMService; +import xiaozhi.modules.model.entity.ModelConfigEntity; +import xiaozhi.modules.model.service.ModelConfigService; + +/** + * 智能体聊天记录总结服务实现类 + * 实现Python端mem_local_short.py中的总结逻辑 + */ +@Service +@RequiredArgsConstructor +public class AgentChatSummaryServiceImpl implements AgentChatSummaryService { + + private static final Logger log = LoggerFactory.getLogger(AgentChatSummaryServiceImpl.class); + + private final AgentChatHistoryService agentChatHistoryService; + private final AgentService agentService; + private final DeviceService deviceService; + private final LLMService llmService; + private final ModelConfigService modelConfigService; + + // 总结规则常量 + private static final int MAX_SUMMARY_LENGTH = 1800; // 最大总结长度 + private static final Pattern JSON_PATTERN = Pattern.compile("\\{.*?\\}", Pattern.DOTALL); + private static final Pattern DEVICE_CONTROL_PATTERN = Pattern.compile("设备控制|设备操作|控制设备|设备状态", + Pattern.CASE_INSENSITIVE); + private static final Pattern WEATHER_PATTERN = Pattern.compile("天气|温度|湿度|降雨|气象", Pattern.CASE_INSENSITIVE); + private static final Pattern DATE_PATTERN = Pattern.compile("日期|时间|星期|月份|年份", Pattern.CASE_INSENSITIVE); + + private AgentChatSummaryDTO generateChatSummary(String sessionId) { + try { + System.out.println("开始生成会话 " + sessionId + " 的聊天记录总结"); + + // 1. 根据sessionId获取聊天记录 + List chatHistory = getChatHistoryBySessionId(sessionId); + if (chatHistory == null || chatHistory.isEmpty()) { + return new AgentChatSummaryDTO(sessionId, "未找到该会话的聊天记录"); + } + + // 2. 获取智能体信息 + String agentId = getAgentIdFromSession(sessionId, chatHistory); + if (StringUtils.isBlank(agentId)) { + return new AgentChatSummaryDTO(sessionId, "无法获取智能体信息"); + } + + // 3. 提取关键对话内容 + List meaningfulMessages = extractMeaningfulMessages(chatHistory); + if (meaningfulMessages.isEmpty()) { + return new AgentChatSummaryDTO(sessionId, "没有有效的对话内容可总结"); + } + + // 4. 生成总结(generateSummaryFromMessages方法已包含长度限制逻辑) + String summary = generateSummaryFromMessages(meaningfulMessages, agentId); + + System.out.println("成功生成会话 " + sessionId + " 的聊天记录总结,长度: " + summary.length() + " 字符"); + return new AgentChatSummaryDTO(sessionId, agentId, summary); + + } catch (Exception e) { + System.err.println("生成会话 " + sessionId + " 的聊天记录总结时发生错误: " + e.getMessage()); + return new AgentChatSummaryDTO(sessionId, "生成总结时发生错误: " + e.getMessage()); + } + } + + @Override + public boolean generateAndSaveChatSummary(String sessionId) { + try { + // 1. 生成总结 + AgentChatSummaryDTO summaryDTO = generateChatSummary(sessionId); + if (!summaryDTO.isSuccess()) { + System.err.println("生成总结失败: " + summaryDTO.getErrorMessage()); + return false; + } + + // 2. 获取设备信息(通过会话关联的设备) + DeviceEntity device = getDeviceBySessionId(sessionId); + if (device == null) { + System.err.println("未找到与会话 " + sessionId + " 关联的设备"); + return false; + } + + // 3. 更新智能体记忆 + AgentMemoryDTO memoryDTO = new AgentMemoryDTO(); + memoryDTO.setSummaryMemory(summaryDTO.getSummary()); + + // 调用现有接口更新记忆 + agentService.updateAgentById(device.getAgentId(), + new AgentUpdateDTO() { + { + setSummaryMemory(summaryDTO.getSummary()); + } + }); + + System.out.println("成功保存会话 " + sessionId + " 的聊天记录总结到智能体 " + device.getAgentId()); + return true; + + } catch (Exception e) { + System.err.println("保存会话 " + sessionId + " 的聊天记录总结时发生错误: " + e.getMessage()); + return false; + } + } + + /** + * 根据会话ID获取聊天记录 + */ + private List getChatHistoryBySessionId(String sessionId) { + try { + // 这里需要根据sessionId获取聊天记录 + // 由于现有接口需要agentId,我们需要先找到关联的agentId + String agentId = findAgentIdBySessionId(sessionId); + if (StringUtils.isBlank(agentId)) { + return null; + } + return agentChatHistoryService.getChatHistoryBySessionId(agentId, sessionId); + } catch (Exception e) { + System.err.println("获取会话 " + sessionId + " 的聊天记录失败: " + e.getMessage()); + return null; + } + } + + /** + * 根据会话ID查找关联的智能体ID + */ + private String findAgentIdBySessionId(String sessionId) { + try { + // 查询该会话的第一条记录获取agentId + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.select("agent_id") + .eq("session_id", sessionId) + .last("LIMIT 1"); + + AgentChatHistoryEntity entity = agentChatHistoryService.getOne(wrapper); + return entity != null ? entity.getAgentId() : null; + } catch (Exception e) { + System.err.println("根据会话ID " + sessionId + " 查找智能体ID失败: " + e.getMessage()); + return null; + } + } + + /** + * 从会话中获取智能体ID + */ + private String getAgentIdFromSession(String sessionId, List chatHistory) { + // 直接从数据库查询智能体ID + return findAgentIdBySessionId(sessionId); + } + + /** + * 提取有意义的对话内容(只提取用户消息,排除AI回复) + */ + private List extractMeaningfulMessages(List chatHistory) { + List meaningfulMessages = new ArrayList<>(); + + for (AgentChatHistoryDTO message : chatHistory) { + // 只处理用户消息(chatType = 1) + if (message.getChatType() != null && message.getChatType() == 1) { + String content = extractContentFromMessage(message); + if (isMeaningfulMessage(content)) { + meaningfulMessages.add(content); + } + } + } + + return meaningfulMessages; + } + + /** + * 从消息中提取内容(处理JSON格式) + */ + private String extractContentFromMessage(AgentChatHistoryDTO message) { + String content = message.getContent(); + if (StringUtils.isBlank(content)) { + return ""; + } + + // 处理JSON格式内容(与前端ChatHistoryDialog.vue逻辑一致) + Matcher matcher = JSON_PATTERN.matcher(content); + if (matcher.find()) { + String jsonContent = matcher.group(); + // 简化处理:提取JSON中的文本内容 + return extractTextFromJson(jsonContent); + } + + return content; + } + + /** + * 从JSON中提取文本内容 + */ + private String extractTextFromJson(String jsonContent) { + // 简化处理:提取"content"字段的值 + Pattern contentPattern = Pattern.compile("\"content\"\s*:\s*\"([^\"]*)\""); + Matcher matcher = contentPattern.matcher(jsonContent); + if (matcher.find()) { + return matcher.group(1); + } + return jsonContent; + } + + /** + * 判断是否为有意义的消息 + */ + private boolean isMeaningfulMessage(String content) { + if (StringUtils.isBlank(content)) { + return false; + } + + // 排除设备控制信息 + if (DEVICE_CONTROL_PATTERN.matcher(content).find()) { + return false; + } + + // 排除日期天气等无关内容 + if (WEATHER_PATTERN.matcher(content).find() || DATE_PATTERN.matcher(content).find()) { + return false; + } + + // 排除过短的消息 + return content.length() >= 5; + } + + /** + * 从消息生成总结 + */ + private String generateSummaryFromMessages(List messages, String agentId) { + if (messages.isEmpty()) { + return "本次对话内容较少,没有需要总结的重要信息。"; + } + + // 构建完整的对话内容 + StringBuilder conversation = new StringBuilder(); + for (int i = 0; i < messages.size(); i++) { + conversation.append("消息").append(i + 1).append(": ").append(messages.get(i)).append("\n"); + } + + try { + // 获取当前智能体的历史记忆 + String historyMemory = getCurrentAgentMemory(agentId); + + // 调用LLM服务进行智能总结,传递agentId以获取正确的模型配置 + String summary = callJavaLLMForSummaryWithHistory(conversation.toString(), historyMemory, agentId); + + // 应用总结规则:限制最大长度 + if (summary.length() > MAX_SUMMARY_LENGTH) { + summary = summary.substring(0, MAX_SUMMARY_LENGTH) + "..."; + } + + return summary; + } catch (Exception e) { + System.err.println("调用Java端LLM服务失败: " + e.getMessage()); + throw new RuntimeException("LLM服务不可用,无法生成聊天总结"); + } + } + + /** + * 获取当前智能体的历史记忆 + */ + private String getCurrentAgentMemory(String agentId) { + try { + if (StringUtils.isBlank(agentId)) { + return null; + } + + // 获取智能体信息 + AgentInfoVO agentInfo = agentService.getAgentById(agentId); + if (agentInfo == null) { + return null; + } + + // 返回智能体的当前总结记忆 + return agentInfo.getSummaryMemory(); + } catch (Exception e) { + System.err.println("获取智能体历史记忆失败,agentId: " + agentId + ", 错误: " + e.getMessage()); + return null; + } + } + + /** + * 调用Java端LLM服务进行智能总结(支持历史记忆合并) + */ + private String callJavaLLMForSummaryWithHistory(String conversation, String historyMemory, String agentId) { + try { + // 获取智能体配置,从中提取记忆总结的模型ID + String modelId = getMemorySummaryModelId(agentId); + + if (StringUtils.isBlank(modelId)) { + System.out.println("未找到记忆总结的LLM模型配置,使用默认LLM服务"); + return llmService.generateSummaryWithHistory(conversation, historyMemory, null, null); + } + + // 使用指定的模型ID调用LLM服务(支持历史记忆合并) + String summary = llmService.generateSummaryWithHistory(conversation, historyMemory, null, modelId); + + if (StringUtils.isNotBlank(summary) && !summary.equals("服务暂不可用") && !summary.equals("总结生成失败")) { + return summary; + } + + throw new RuntimeException("Java端LLM服务返回异常: " + summary); + + } catch (Exception e) { + System.err.println("调用Java端LLM服务异常,agentId: " + agentId + ", 错误: " + e.getMessage()); + throw e; + } + } + + /** + * 调用Java端LLM服务进行智能总结 + */ + private String callJavaLLMForSummary(String conversation, String agentId) { + try { + // 获取智能体配置,从中提取记忆总结的模型ID + String modelId = getMemorySummaryModelId(agentId); + + if (StringUtils.isBlank(modelId)) { + System.out.println("未找到记忆总结的LLM模型配置,使用默认LLM服务"); + return llmService.generateSummary(conversation); + } + + // 使用指定的模型ID调用LLM服务 + String summary = llmService.generateSummaryWithModel(conversation, modelId); + + if (StringUtils.isNotBlank(summary) && !summary.equals("服务暂不可用") && !summary.equals("总结生成失败")) { + return summary; + } + + throw new RuntimeException("Java端LLM服务返回异常: " + summary); + + } catch (Exception e) { + System.err.println("调用Java端LLM服务异常,agentId: " + agentId + ", 错误: " + e.getMessage()); + throw e; + } + } + + /** + * 获取记忆总结的LLM模型ID + */ + private String getMemorySummaryModelId(String agentId) { + try { + if (StringUtils.isBlank(agentId)) { + return null; + } + + // 获取智能体信息 + AgentInfoVO agentInfo = agentService.getAgentById(agentId); + if (agentInfo == null) { + return null; + } + + // 获取智能体的记忆模型ID + String memModelId = agentInfo.getMemModelId(); + if (StringUtils.isBlank(memModelId)) { + return null; + } + + // 获取记忆模型配置 + ModelConfigEntity memModelConfig = modelConfigService.getModelByIdFromCache(memModelId); + if (memModelConfig == null || memModelConfig.getConfigJson() == null) { + return null; + } + + // 从记忆模型配置中提取对应的LLM模型ID + Map configMap = memModelConfig.getConfigJson(); + String llmModelId = (String) configMap.get("llm"); + + if (StringUtils.isBlank(llmModelId)) { + // 如果记忆模型没有配置独立的LLM,则使用智能体的默认LLM模型 + return agentInfo.getLlmModelId(); + } + + return llmModelId; + } catch (Exception e) { + System.err.println("获取记忆总结LLM模型ID失败,agentId: " + agentId + ", 错误: " + e.getMessage()); + return null; + } + } + + /** + * 根据会话ID获取设备信息 + */ + private DeviceEntity getDeviceBySessionId(String sessionId) { + try { + // 查询该会话的第一条记录获取macAddress + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.select("mac_address") + .eq("session_id", sessionId) + .last("LIMIT 1"); + + AgentChatHistoryEntity entity = agentChatHistoryService.getOne(wrapper); + if (entity != null && StringUtils.isNotBlank(entity.getMacAddress())) { + return deviceService.getDeviceByMacAddress(entity.getMacAddress()); + } + return null; + } catch (Exception e) { + System.err.println("根据会话ID " + sessionId + " 查找设备信息失败: " + e.getMessage()); + return null; + } + } +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/llm/service/LLMService.java b/main/manager-api/src/main/java/xiaozhi/modules/llm/service/LLMService.java new file mode 100644 index 00000000..a3f10e9f --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/llm/service/LLMService.java @@ -0,0 +1,70 @@ +package xiaozhi.modules.llm.service; + +/** + * LLM服务接口 + * 支持多种大模型调用 + */ +public interface LLMService { + + /** + * 生成聊天记录总结 + * + * @param conversation 对话内容 + * @param promptTemplate 提示词模板 + * @return 总结结果 + */ + String generateSummary(String conversation, String promptTemplate); + + /** + * 生成聊天记录总结(使用默认提示词) + * + * @param conversation 对话内容 + * @return 总结结果 + */ + String generateSummary(String conversation); + + /** + * 生成聊天记录总结(指定模型ID) + * + * @param conversation 对话内容 + * @param modelId 模型ID + * @return 总结结果 + */ + String generateSummaryWithModel(String conversation, String modelId); + + /** + * 生成聊天记录总结(指定模型ID和提示词模板) + * + * @param conversation 对话内容 + * @param promptTemplate 提示词模板 + * @param modelId 模型ID + * @return 总结结果 + */ + String generateSummary(String conversation, String promptTemplate, String modelId); + + /** + * 生成聊天记录总结(包含历史记忆合并) + * + * @param conversation 对话内容 + * @param historyMemory 历史记忆 + * @param promptTemplate 提示词模板 + * @param modelId 模型ID + * @return 总结结果 + */ + String generateSummaryWithHistory(String conversation, String historyMemory, String promptTemplate, String modelId); + + /** + * 检查服务是否可用 + * + * @return 是否可用 + */ + boolean isAvailable(); + + /** + * 检查指定模型的服务是否可用 + * + * @param modelId 模型ID + * @return 是否可用 + */ + boolean isAvailable(String modelId); +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/llm/service/impl/OpenAIStyleLLMServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/llm/service/impl/OpenAIStyleLLMServiceImpl.java new file mode 100644 index 00000000..55fda70d --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/llm/service/impl/OpenAIStyleLLMServiceImpl.java @@ -0,0 +1,305 @@ +package xiaozhi.modules.llm.service.impl; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import lombok.extern.slf4j.Slf4j; +import xiaozhi.modules.llm.service.LLMService; +import xiaozhi.modules.model.entity.ModelConfigEntity; +import xiaozhi.modules.model.service.ModelConfigService; + +/** + * OpenAI风格API的LLM服务实现 + * 支持阿里云、DeepSeek、ChatGLM等兼容OpenAI API的模型 + */ +@Slf4j +@Service +public class OpenAIStyleLLMServiceImpl implements LLMService { + + @Autowired + private ModelConfigService modelConfigService; + + private final RestTemplate restTemplate = new RestTemplate(); + + private static final String DEFAULT_SUMMARY_PROMPT = "你是一个经验丰富的记忆总结者,擅长将对话内容进行总结摘要,遵循以下规则:\n1、总结用户的重要信息,以便在未来的对话中提供更个性化的服务\n2、不要重复总结,不要遗忘之前记忆,除非原来的记忆超过了1800字,否则不要遗忘、不要压缩用户的历史记忆\n3、用户操控的设备音量、播放音乐、天气、退出、不想对话等和用户本身无关的内容,这些信息不需要加入到总结中\n4、聊天内容中的今天的日期时间、今天的天气情况与用户事件无关的数据,这些信息如果当成记忆存储会影响后续对话,这些信息不需要加入到总结中\n5、不要把设备操控的成果结果和失败结果加入到总结中,也不要把用户的一些废话加入到总结中\n6、不要为了总结而总结,如果用户的聊天没有意义,请返回原来的历史记录也是可以的\n7、只需要返回总结摘要,严格控制在1800字内\n8、不要包含代码、xml,不需要解释、注释和说明,保存记忆时仅从对话提取信息,不要混入示例内容\n9、如果提供了历史记忆,请将新对话内容与历史记忆进行智能合并,保留有价值的历史信息,同时添加新的重要信息\n\n历史记忆:\n{history_memory}\n\n新对话内容:\n{conversation}"; + + @Override + public String generateSummary(String conversation) { + return generateSummary(conversation, null, null); + } + + @Override + public String generateSummaryWithModel(String conversation, String modelId) { + return generateSummary(conversation, null, modelId); + } + + @Override + public String generateSummary(String conversation, String promptTemplate, String modelId) { + if (!isAvailable()) { + log.warn("LLM服务不可用,无法生成总结"); + return "LLM服务不可用,无法生成总结"; + } + + try { + // 从智控台获取LLM模型配置 + ModelConfigEntity llmConfig; + if (modelId != null && !modelId.trim().isEmpty()) { + // 通过具体模型ID获取配置 + llmConfig = modelConfigService.getModelByIdFromCache(modelId); + } else { + // 保持向后兼容,使用默认配置 + llmConfig = getDefaultLLMConfig(); + } + + if (llmConfig == null || llmConfig.getConfigJson() == null) { + log.error("未找到可用的LLM模型配置,modelId: {}", modelId); + return "未找到可用的LLM模型配置"; + } + + JSONObject configJson = llmConfig.getConfigJson(); + String baseUrl = configJson.getStr("base_url"); + String model = configJson.getStr("model_name"); + String apiKey = configJson.getStr("api_key"); + Double temperature = configJson.getDouble("temperature"); + Integer maxTokens = configJson.getInt("max_tokens"); + + if (StringUtils.isBlank(baseUrl) || StringUtils.isBlank(apiKey)) { + log.error("LLM配置不完整,baseUrl或apiKey为空"); + return "LLM配置不完整,无法生成总结"; + } + + // 构建提示词 + String prompt = (promptTemplate != null ? promptTemplate : DEFAULT_SUMMARY_PROMPT).replace("{conversation}", + conversation); + + // 构建请求体 + Map requestBody = new HashMap<>(); + requestBody.put("model", model != null ? model : "gpt-3.5-turbo"); + + Map[] messages = new Map[1]; + Map message = new HashMap<>(); + message.put("role", "user"); + message.put("content", prompt); + messages[0] = message; + + requestBody.put("messages", messages); + requestBody.put("temperature", temperature != null ? temperature : 0.7); + requestBody.put("max_tokens", maxTokens != null ? maxTokens : 2000); + + // 发送HTTP请求 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("Authorization", "Bearer " + apiKey); + + HttpEntity> entity = new HttpEntity<>(requestBody, headers); + + // 构建完整的API URL + String apiUrl = baseUrl; + if (!apiUrl.endsWith("/chat/completions")) { + if (!apiUrl.endsWith("/")) { + apiUrl += "/"; + } + apiUrl += "chat/completions"; + } + + ResponseEntity response = restTemplate.exchange( + apiUrl, HttpMethod.POST, entity, String.class); + + if (response.getStatusCode().is2xxSuccessful()) { + JSONObject responseJson = JSONUtil.parseObj(response.getBody()); + JSONArray choices = responseJson.getJSONArray("choices"); + if (choices != null && choices.size() > 0) { + JSONObject choice = choices.getJSONObject(0); + JSONObject messageObj = choice.getJSONObject("message"); + return messageObj.getStr("content"); + } + } else { + log.error("LLM API调用失败,状态码:{},响应:{}", response.getStatusCode(), response.getBody()); + } + } catch (Exception e) { + log.error("调用LLM服务生成总结时发生异常,modelId: {}", modelId, e); + } + + return "生成总结失败,请稍后重试"; + } + + @Override + public String generateSummary(String conversation, String promptTemplate) { + return generateSummary(conversation, promptTemplate, null); + } + + @Override + public String generateSummaryWithHistory(String conversation, String historyMemory, String promptTemplate, + String modelId) { + if (!isAvailable()) { + log.warn("LLM服务不可用,无法生成总结"); + return "LLM服务不可用,无法生成总结"; + } + + try { + // 从智控台获取LLM模型配置 + ModelConfigEntity llmConfig; + if (modelId != null && !modelId.trim().isEmpty()) { + // 通过具体模型ID获取配置 + llmConfig = modelConfigService.getModelByIdFromCache(modelId); + } else { + // 保持向后兼容,使用默认配置 + llmConfig = getDefaultLLMConfig(); + } + + if (llmConfig == null || llmConfig.getConfigJson() == null) { + log.error("未找到可用的LLM模型配置,modelId: {}", modelId); + return "未找到可用的LLM模型配置"; + } + + JSONObject configJson = llmConfig.getConfigJson(); + String baseUrl = configJson.getStr("base_url"); + String model = configJson.getStr("model_name"); + String apiKey = configJson.getStr("api_key"); + + if (StringUtils.isBlank(baseUrl) || StringUtils.isBlank(apiKey)) { + log.error("LLM配置不完整,baseUrl或apiKey为空"); + return "LLM配置不完整,无法生成总结"; + } + + // 构建提示词,包含历史记忆 + String prompt = (promptTemplate != null ? promptTemplate : DEFAULT_SUMMARY_PROMPT) + .replace("{history_memory}", historyMemory != null ? historyMemory : "无历史记忆") + .replace("{conversation}", conversation); + + // 构建请求体 + Map requestBody = new HashMap<>(); + requestBody.put("model", model != null ? model : "gpt-3.5-turbo"); + + Map[] messages = new Map[1]; + Map message = new HashMap<>(); + message.put("role", "user"); + message.put("content", prompt); + messages[0] = message; + + requestBody.put("messages", messages); + requestBody.put("temperature", 0.2); + requestBody.put("max_tokens", 2000); + + // 发送HTTP请求 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("Authorization", "Bearer " + apiKey); + + HttpEntity> entity = new HttpEntity<>(requestBody, headers); + + // 构建完整的API URL + String apiUrl = baseUrl; + if (!apiUrl.endsWith("/chat/completions")) { + if (!apiUrl.endsWith("/")) { + apiUrl += "/"; + } + apiUrl += "chat/completions"; + } + + ResponseEntity response = restTemplate.exchange( + apiUrl, HttpMethod.POST, entity, String.class); + + if (response.getStatusCode().is2xxSuccessful()) { + JSONObject responseJson = JSONUtil.parseObj(response.getBody()); + JSONArray choices = responseJson.getJSONArray("choices"); + if (choices != null && choices.size() > 0) { + JSONObject choice = choices.getJSONObject(0); + JSONObject messageObj = choice.getJSONObject("message"); + return messageObj.getStr("content"); + } + } else { + log.error("LLM API调用失败,状态码:{},响应:{}", response.getStatusCode(), response.getBody()); + } + } catch (Exception e) { + log.error("调用LLM服务生成总结时发生异常,modelId: {}", modelId, e); + } + + return "生成总结失败,请稍后重试"; + } + + @Override + public boolean isAvailable() { + try { + ModelConfigEntity defaultLLMConfig = getDefaultLLMConfig(); + if (defaultLLMConfig == null || defaultLLMConfig.getConfigJson() == null) { + return false; + } + + JSONObject configJson = defaultLLMConfig.getConfigJson(); + String baseUrl = configJson.getStr("base_url"); + String apiKey = configJson.getStr("api_key"); + + return baseUrl != null && !baseUrl.trim().isEmpty() && + apiKey != null && !apiKey.trim().isEmpty(); + } catch (Exception e) { + log.error("检查LLM服务可用性时发生异常:", e); + return false; + } + } + + @Override + public boolean isAvailable(String modelId) { + try { + if (modelId == null || modelId.trim().isEmpty()) { + return isAvailable(); + } + + // 通过具体模型ID获取配置 + ModelConfigEntity modelConfig = modelConfigService.getModelByIdFromCache(modelId); + if (modelConfig == null || modelConfig.getConfigJson() == null) { + log.warn("未找到指定的LLM模型配置,modelId: {}", modelId); + return false; + } + + JSONObject configJson = modelConfig.getConfigJson(); + String baseUrl = configJson.getStr("base_url"); + String apiKey = configJson.getStr("api_key"); + + return baseUrl != null && !baseUrl.trim().isEmpty() && + apiKey != null && !apiKey.trim().isEmpty(); + } catch (Exception e) { + log.error("检查LLM服务可用性时发生异常,modelId: {}", modelId, e); + return false; + } + } + + /** + * 从智控台获取默认的LLM模型配置 + */ + private ModelConfigEntity getDefaultLLMConfig() { + try { + // 获取所有启用的LLM模型配置 + List llmConfigs = modelConfigService.getEnabledModelsByType("LLM"); + if (llmConfigs == null || llmConfigs.isEmpty()) { + return null; + } + + // 优先返回默认配置,如果没有默认配置则返回第一个启用的配置 + for (ModelConfigEntity config : llmConfigs) { + if (config.getIsDefault() != null && config.getIsDefault() == 1) { + return config; + } + } + + return llmConfigs.get(0); + } catch (Exception e) { + log.error("获取LLM模型配置时发生异常:", e); + return null; + } + } +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/service/ModelConfigService.java b/main/manager-api/src/main/java/xiaozhi/modules/model/service/ModelConfigService.java index 95b64b20..9ae10761 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/model/service/ModelConfigService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/service/ModelConfigService.java @@ -55,4 +55,12 @@ public interface ModelConfigService extends BaseService { * @return TTS平台列表(id和modelName) */ List> getTtsPlatformList(); + + /** + * 根据模型类型获取所有启用的模型配置 + * + * @param modelType 模型类型(如:LLM, TTS, ASR等) + * @return 启用的模型配置列表 + */ + List getEnabledModelsByType(String modelType); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelConfigServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelConfigServiceImpl.java index fbfc7723..2a291cf9 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelConfigServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelConfigServiceImpl.java @@ -502,4 +502,22 @@ public class ModelConfigServiceImpl extends BaseServiceImpl> getTtsPlatformList() { return modelConfigDao.getTtsPlatformList(); } + + /** + * 根据模型类型获取所有启用的模型配置 + */ + @Override + public List getEnabledModelsByType(String modelType) { + if (StringUtils.isBlank(modelType)) { + return null; + } + + List entities = modelConfigDao.selectList( + new QueryWrapper() + .eq("model_type", modelType) + .eq("is_enabled", 1) + .orderByAsc("sort")); + + return entities; + } } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java b/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java index 051cbc29..29b9ad2f 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java @@ -89,7 +89,7 @@ public class ShiroConfig { filterMap.put("/config/**", "server"); filterMap.put("/agent/chat-history/report", "server"); filterMap.put("/agent/chat-history/download/**", "anon"); - filterMap.put("/agent/saveMemory/**", "server"); + filterMap.put("/agent/chat-summary/**", "server"); filterMap.put("/agent/play/**", "anon"); filterMap.put("/voiceClone/play/**", "anon"); filterMap.put("/**", "oauth2"); diff --git a/main/manager-web/src/assets/xiaozhi-ai.png b/main/manager-web/src/assets/xiaozhi-ai.png index ef3834af..8a595493 100644 Binary files a/main/manager-web/src/assets/xiaozhi-ai.png and b/main/manager-web/src/assets/xiaozhi-ai.png differ diff --git a/main/manager-web/src/assets/xiaozhi-ai_de.png b/main/manager-web/src/assets/xiaozhi-ai_de.png new file mode 100644 index 00000000..f2c54d80 Binary files /dev/null and b/main/manager-web/src/assets/xiaozhi-ai_de.png differ diff --git a/main/manager-web/src/assets/xiaozhi-ai_en.png b/main/manager-web/src/assets/xiaozhi-ai_en.png new file mode 100644 index 00000000..d35fd499 Binary files /dev/null and b/main/manager-web/src/assets/xiaozhi-ai_en.png differ diff --git a/main/manager-web/src/assets/xiaozhi-ai_vi.png b/main/manager-web/src/assets/xiaozhi-ai_vi.png new file mode 100644 index 00000000..a54913ad Binary files /dev/null and b/main/manager-web/src/assets/xiaozhi-ai_vi.png differ diff --git a/main/manager-web/src/assets/xiaozhi-ai_zh_CN.png b/main/manager-web/src/assets/xiaozhi-ai_zh_CN.png new file mode 100644 index 00000000..ef3834af Binary files /dev/null and b/main/manager-web/src/assets/xiaozhi-ai_zh_CN.png differ diff --git a/main/manager-web/src/assets/xiaozhi-ai_zh_TW.png b/main/manager-web/src/assets/xiaozhi-ai_zh_TW.png new file mode 100644 index 00000000..ac5f59aa Binary files /dev/null and b/main/manager-web/src/assets/xiaozhi-ai_zh_TW.png differ diff --git a/main/manager-web/src/components/HeaderBar.vue b/main/manager-web/src/components/HeaderBar.vue index b274d649..add7e221 100644 --- a/main/manager-web/src/components/HeaderBar.vue +++ b/main/manager-web/src/components/HeaderBar.vue @@ -4,7 +4,7 @@
- +
@@ -257,6 +257,24 @@ export default { return this.$t("language.zhCN"); } }, + // 根据当前语言获取对应的xiaozhi-ai图标 + xiaozhiAiIcon() { + const currentLang = this.currentLanguage; + switch (currentLang) { + case "zh_CN": + return require("@/assets/xiaozhi-ai.png"); + case "zh_TW": + return require("@/assets/xiaozhi-ai_zh_TW.png"); + case "en": + return require("@/assets/xiaozhi-ai_en.png"); + case "de": + return require("@/assets/xiaozhi-ai_de.png"); + case "vi": + return require("@/assets/xiaozhi-ai_vi.png"); + default: + return require("@/assets/xiaozhi-ai.png"); + } + }, // 用户菜单选项 userMenuOptions() { return [ diff --git a/main/manager-web/src/views/login.vue b/main/manager-web/src/views/login.vue index cdcf3754..f9dd0748 100644 --- a/main/manager-web/src/views/login.vue +++ b/main/manager-web/src/views/login.vue @@ -10,7 +10,7 @@ gap: 10px; "> - +