Merge pull request #2742 from xinnan-tech/manager-web-logo-i18n
add:登录、注册、首页、忘记密码页的多语言logo显示判断
@@ -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<Void> 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<Void>().ok(null);
|
||||
} catch (Exception e) {
|
||||
return new Result<Void>().error("启动异步总结生成任务失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@Operation(summary = "更新智能体")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
@@ -186,6 +209,7 @@ public class AgentController {
|
||||
List<AgentChatHistoryDTO> result = agentChatHistoryService.getChatHistoryBySessionId(id, sessionId);
|
||||
return new Result<List<AgentChatHistoryDTO>>().ok(result);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/chat-history/user")
|
||||
@Operation(summary = "获取智能体聊天记录(用户)")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package xiaozhi.modules.agent.service;
|
||||
|
||||
/**
|
||||
* 智能体聊天记录总结服务接口
|
||||
*/
|
||||
public interface AgentChatSummaryService {
|
||||
|
||||
/**
|
||||
* 根据会话ID生成聊天记录总结并保存到智能体记忆
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
* @return 保存结果
|
||||
*/
|
||||
boolean generateAndSaveChatSummary(String sessionId);
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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<AgentChatHistoryDTO> 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<String> 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<AgentChatHistoryDTO> 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<AgentChatHistoryEntity> 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<AgentChatHistoryDTO> chatHistory) {
|
||||
// 直接从数据库查询智能体ID
|
||||
return findAgentIdBySessionId(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取有意义的对话内容(只提取用户消息,排除AI回复)
|
||||
*/
|
||||
private List<String> extractMeaningfulMessages(List<AgentChatHistoryDTO> chatHistory) {
|
||||
List<String> 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<String> 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<String, Object> 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<AgentChatHistoryEntity> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("model", model != null ? model : "gpt-3.5-turbo");
|
||||
|
||||
Map<String, Object>[] messages = new Map[1];
|
||||
Map<String, Object> 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<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
// 构建完整的API URL
|
||||
String apiUrl = baseUrl;
|
||||
if (!apiUrl.endsWith("/chat/completions")) {
|
||||
if (!apiUrl.endsWith("/")) {
|
||||
apiUrl += "/";
|
||||
}
|
||||
apiUrl += "chat/completions";
|
||||
}
|
||||
|
||||
ResponseEntity<String> 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<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("model", model != null ? model : "gpt-3.5-turbo");
|
||||
|
||||
Map<String, Object>[] messages = new Map[1];
|
||||
Map<String, Object> 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<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
// 构建完整的API URL
|
||||
String apiUrl = baseUrl;
|
||||
if (!apiUrl.endsWith("/chat/completions")) {
|
||||
if (!apiUrl.endsWith("/")) {
|
||||
apiUrl += "/";
|
||||
}
|
||||
apiUrl += "chat/completions";
|
||||
}
|
||||
|
||||
ResponseEntity<String> 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<ModelConfigEntity> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,4 +55,12 @@ public interface ModelConfigService extends BaseService<ModelConfigEntity> {
|
||||
* @return TTS平台列表(id和modelName)
|
||||
*/
|
||||
List<Map<String, Object>> getTtsPlatformList();
|
||||
|
||||
/**
|
||||
* 根据模型类型获取所有启用的模型配置
|
||||
*
|
||||
* @param modelType 模型类型(如:LLM, TTS, ASR等)
|
||||
* @return 启用的模型配置列表
|
||||
*/
|
||||
List<ModelConfigEntity> getEnabledModelsByType(String modelType);
|
||||
}
|
||||
|
||||
@@ -502,4 +502,22 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
public List<Map<String, Object>> getTtsPlatformList() {
|
||||
return modelConfigDao.getTtsPlatformList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模型类型获取所有启用的模型配置
|
||||
*/
|
||||
@Override
|
||||
public List<ModelConfigEntity> getEnabledModelsByType(String modelType) {
|
||||
if (StringUtils.isBlank(modelType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<ModelConfigEntity> entities = modelConfigDao.selectList(
|
||||
new QueryWrapper<ModelConfigEntity>()
|
||||
.eq("model_type", modelType)
|
||||
.eq("is_enabled", 1)
|
||||
.orderByAsc("sort"));
|
||||
|
||||
return entities;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
@@ -4,7 +4,7 @@
|
||||
<!-- 左侧元素 -->
|
||||
<div class="header-left" @click="goHome">
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" class="logo-img" />
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" class="brand-img" />
|
||||
<img loading="lazy" alt="" :src="xiaozhiAiIcon" class="brand-img" />
|
||||
</div>
|
||||
|
||||
<!-- 中间导航菜单 -->
|
||||
@@ -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 [
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
gap: 10px;
|
||||
">
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px; height: 45px" />
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" style="height: 18px" />
|
||||
<img loading="lazy" alt="" :src="xiaozhiAiIcon" style="height: 18px" />
|
||||
</div>
|
||||
</el-header>
|
||||
<div class="login-person">
|
||||
@@ -192,6 +192,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");
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<el-header>
|
||||
<div style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;" />
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" style="height: 18px;" />
|
||||
<img loading="lazy" alt="" :src="xiaozhiAiIcon" style="height: 18px;" />
|
||||
</div>
|
||||
</el-header>
|
||||
<div class="login-person">
|
||||
@@ -108,7 +108,7 @@
|
||||
<div style="font-size: 14px;color: #979db1;">
|
||||
{{ $t('register.agreeTo') }}
|
||||
<div style="display: inline-block;color: #5778FF;cursor: pointer;">{{ $t('register.userAgreement') }}</div>
|
||||
{{ $t('register.and') }}
|
||||
{{ $t('login.and') }}
|
||||
<div style="display: inline-block;color: #5778FF;cursor: pointer;">{{ $t('register.privacyPolicy') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -127,6 +127,7 @@ import Api from '@/apis/api';
|
||||
import VersionFooter from '@/components/VersionFooter.vue';
|
||||
import { getUUID, goToPage, showDanger, showSuccess, sm2Encrypt, validateMobile } from '@/utils';
|
||||
import { mapState } from 'vuex';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
// 导入语言切换功能
|
||||
|
||||
@@ -142,6 +143,28 @@ export default {
|
||||
mobileAreaList: state => state.pubConfig.mobileAreaList,
|
||||
sm2PublicKey: state => state.pubConfig.sm2PublicKey,
|
||||
}),
|
||||
// 获取当前语言
|
||||
currentLanguage() {
|
||||
return i18n.locale || "zh_CN";
|
||||
},
|
||||
// 根据当前语言获取对应的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");
|
||||
}
|
||||
},
|
||||
canSendMobileCaptcha() {
|
||||
return this.countdown === 0 && validateMobile(this.form.mobile, this.form.areaCode);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<el-header>
|
||||
<div style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;" />
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" style="height: 18px;" />
|
||||
<img loading="lazy" alt="" :src="xiaozhiAiIcon" style="height: 18px;" />
|
||||
</div>
|
||||
</el-header>
|
||||
<div class="login-person">
|
||||
@@ -83,7 +83,7 @@
|
||||
<div style="font-size: 14px;color: #979db1;">
|
||||
{{ $t('retrievePassword.agreeTo') }}
|
||||
<div style="display: inline-block;color: #5778FF;cursor: pointer;">{{ $t('register.userAgreement') }}</div>
|
||||
{{ $t('register.and') }}
|
||||
{{ $t('login.and') }}
|
||||
<div style="display: inline-block;color: #5778FF;cursor: pointer;">{{ $t('register.privacyPolicy') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -103,6 +103,7 @@ import Api from '@/apis/api';
|
||||
import VersionFooter from '@/components/VersionFooter.vue';
|
||||
import { getUUID, goToPage, showDanger, showSuccess, validateMobile, sm2Encrypt } from '@/utils';
|
||||
import { mapState } from 'vuex';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
// 导入语言切换功能
|
||||
import { changeLanguage } from '@/i18n';
|
||||
@@ -118,6 +119,28 @@ export default {
|
||||
mobileAreaList: state => state.pubConfig.mobileAreaList,
|
||||
sm2PublicKey: state => state.pubConfig.sm2PublicKey
|
||||
}),
|
||||
// 获取当前语言
|
||||
currentLanguage() {
|
||||
return i18n.locale || "zh_CN";
|
||||
},
|
||||
// 根据当前语言获取对应的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");
|
||||
}
|
||||
},
|
||||
canSendMobileCaptcha() {
|
||||
return this.countdown === 0 && validateMobile(this.form.mobile, this.form.areaCode);
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ class ManageApiClient:
|
||||
async def _ensure_async_client(cls):
|
||||
"""确保异步客户端已创建(为每个事件循环创建独立的客户端)"""
|
||||
import asyncio
|
||||
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
loop_id = id(loop)
|
||||
@@ -115,6 +116,7 @@ class ManageApiClient:
|
||||
async def _execute_async_request(cls, method: str, endpoint: str, **kwargs) -> Dict:
|
||||
"""带重试机制的异步请求执行器"""
|
||||
import asyncio
|
||||
|
||||
retry_count = 0
|
||||
|
||||
while retry_count <= cls.max_retries:
|
||||
@@ -138,6 +140,7 @@ class ManageApiClient:
|
||||
def safe_close(cls):
|
||||
"""安全关闭所有异步连接池"""
|
||||
import asyncio
|
||||
|
||||
for client in list(cls._async_clients.values()):
|
||||
try:
|
||||
asyncio.run(client.aclose())
|
||||
@@ -149,7 +152,9 @@ class ManageApiClient:
|
||||
|
||||
async def get_server_config() -> Optional[Dict]:
|
||||
"""获取服务器基础配置"""
|
||||
return await ManageApiClient._instance._execute_async_request("POST", "/config/server-base")
|
||||
return await ManageApiClient._instance._execute_async_request(
|
||||
"POST", "/config/server-base"
|
||||
)
|
||||
|
||||
|
||||
async def get_agent_models(
|
||||
@@ -167,17 +172,15 @@ async def get_agent_models(
|
||||
)
|
||||
|
||||
|
||||
async def save_mem_local_short(mac_address: str, short_momery: str) -> Optional[Dict]:
|
||||
async def generate_and_save_chat_summary(session_id: str) -> Optional[Dict]:
|
||||
"""生成并保存聊天记录总结"""
|
||||
try:
|
||||
return await ManageApiClient._instance._execute_async_request(
|
||||
"PUT",
|
||||
f"/agent/saveMemory/" + mac_address,
|
||||
json={
|
||||
"summaryMemory": short_momery,
|
||||
},
|
||||
"POST",
|
||||
f"/agent/chat-summary/{session_id}/save",
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"存储短期记忆到服务器失败: {e}")
|
||||
print(f"生成并保存聊天记录总结失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -244,7 +244,9 @@ class ConnectionHandler:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.run_until_complete(
|
||||
self.memory.save_memory(self.dialogue.dialogue)
|
||||
self.memory.save_memory(
|
||||
self.dialogue.dialogue, self.session_id
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"保存记忆失败: {e}")
|
||||
|
||||
@@ -14,7 +14,7 @@ class MemoryProviderBase(ABC):
|
||||
self.llm = llm
|
||||
|
||||
@abstractmethod
|
||||
async def save_memory(self, msgs):
|
||||
async def save_memory(self, msgs, session_id=None):
|
||||
"""Save a new memory for specific role and return memory ID"""
|
||||
print("this is base func", msgs)
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ class MemoryProvider(MemoryProviderBase):
|
||||
logger.bind(tag=TAG).error(f"详细错误: {traceback.format_exc()}")
|
||||
self.use_mem0 = False
|
||||
|
||||
async def save_memory(self, msgs):
|
||||
async def save_memory(self, msgs, session_id=None):
|
||||
if not self.use_mem0:
|
||||
return None
|
||||
if len(msgs) < 2:
|
||||
@@ -41,9 +41,7 @@ class MemoryProvider(MemoryProviderBase):
|
||||
for message in msgs
|
||||
if message.role != "system"
|
||||
]
|
||||
result = self.client.add(
|
||||
messages, user_id=self.role_id
|
||||
)
|
||||
result = self.client.add(messages, user_id=self.role_id)
|
||||
logger.bind(tag=TAG).debug(f"Save memory result: {result}")
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"保存记忆失败: {str(e)}")
|
||||
|
||||
@@ -4,7 +4,7 @@ import json
|
||||
import os
|
||||
import yaml
|
||||
from config.config_loader import get_project_dir
|
||||
from config.manage_api_client import save_mem_local_short
|
||||
from config.manage_api_client import generate_and_save_chat_summary
|
||||
import asyncio
|
||||
from core.utils.util import check_model_key
|
||||
|
||||
@@ -75,18 +75,6 @@ short_term_memory_prompt = """
|
||||
```
|
||||
"""
|
||||
|
||||
short_term_memory_prompt_only_content = """
|
||||
你是一个经验丰富的记忆总结者,擅长将对话内容进行总结摘要,遵循以下规则:
|
||||
1、总结user的重要信息,以便在未来的对话中提供更个性化的服务
|
||||
2、不要重复总结,不要遗忘之前记忆,除非原来的记忆超过了1800字内,否则不要遗忘、不要压缩用户的历史记忆
|
||||
3、用户操控的设备音量、播放音乐、天气、退出、不想对话等和用户本身无关的内容,这些信息不需要加入到总结中
|
||||
4、聊天内容中的今天的日期时间、今天的天气情况与用户事件无关的数据,这些信息如果当成记忆存储会影响后序对话,这些信息不需要加入到总结中
|
||||
5、不要把设备操控的成果结果和失败结果加入到总结中,也不要把用户的一些废话加入到总结中
|
||||
6、不要为了总结而总结,如果用户的聊天没有意义,请返回原来的历史记录也是可以的
|
||||
7、只需要返回总结摘要,严格控制在1800字内
|
||||
8、不要包含代码、xml,不需要解释、注释和说明,保存记忆时仅从对话提取信息,不要混入示例内容
|
||||
"""
|
||||
|
||||
|
||||
def extract_json_data(json_code):
|
||||
start = json_code.find("```json")
|
||||
@@ -144,7 +132,7 @@ class MemoryProvider(MemoryProviderBase):
|
||||
with open(self.memory_path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(all_memory, f, allow_unicode=True)
|
||||
|
||||
async def save_memory(self, msgs):
|
||||
async def save_memory(self, msgs, session_id=None):
|
||||
# 打印使用的模型信息
|
||||
model_info = getattr(self.llm, "model_name", str(self.llm.__class__.__name__))
|
||||
logger.bind(tag=TAG).debug(f"使用记忆保存模型: {model_info}")
|
||||
@@ -188,20 +176,12 @@ class MemoryProvider(MemoryProviderBase):
|
||||
except Exception as e:
|
||||
print("Error:", e)
|
||||
else:
|
||||
result = self.llm.response_no_stream(
|
||||
short_term_memory_prompt_only_content,
|
||||
msgStr,
|
||||
max_tokens=2000,
|
||||
temperature=0.2,
|
||||
)
|
||||
# 使用异步版本,需要在事件循环中运行
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
loop.create_task(save_mem_local_short(self.role_id, result))
|
||||
except RuntimeError:
|
||||
# 如果没有运行中的事件循环,创建一个新的
|
||||
asyncio.run(save_mem_local_short(self.role_id, result))
|
||||
logger.bind(tag=TAG).info(f"Save memory successful - Role: {self.role_id}")
|
||||
# 当save_to_file为False时,调用Java端的聊天记录总结接口
|
||||
summary_id = session_id if session_id else self.role_id
|
||||
await generate_and_save_chat_summary(summary_id)
|
||||
logger.bind(tag=TAG).info(
|
||||
f"Save memory successful - Role: {self.role_id}, Session: {session_id}"
|
||||
)
|
||||
|
||||
return self.short_memory
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ class MemoryProvider(MemoryProviderBase):
|
||||
def __init__(self, config, summary_memory=None):
|
||||
super().__init__(config)
|
||||
|
||||
async def save_memory(self, msgs):
|
||||
async def save_memory(self, msgs, session_id=None):
|
||||
logger.bind(tag=TAG).debug("nomem mode: No memory saving is performed.")
|
||||
return None
|
||||
|
||||
|
||||