From c02f2105d754ca56138222447e08a6dcfbbb7d21 Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Sat, 8 Nov 2025 16:00:37 +0800 Subject: [PATCH] =?UTF-8?q?update:=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/AgentPluginMappingServiceImpl.java | 47 +++++++-- main/xiaozhi-server/config.yaml | 11 ++- .../tools/server_plugins/plugin_executor.py | 95 +++---------------- .../functions/search_from_ragflow.py | 93 +++++++++++++++--- 4 files changed, 141 insertions(+), 105 deletions(-) diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentPluginMappingServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentPluginMappingServiceImpl.java index e168489f..ff9fc8e9 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentPluginMappingServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentPluginMappingServiceImpl.java @@ -1,5 +1,6 @@ package xiaozhi.modules.agent.service.impl; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -7,11 +8,11 @@ import java.util.Map; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; -import com.alibaba.druid.support.json.JSONUtils; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import lombok.RequiredArgsConstructor; +import xiaozhi.common.utils.JsonUtils; import xiaozhi.modules.agent.dao.AgentPluginMappingMapper; import xiaozhi.modules.agent.entity.AgentPluginMapping; import xiaozhi.modules.agent.service.AgentPluginMappingService; @@ -35,7 +36,8 @@ public class AgentPluginMappingServiceImpl extends ServiceImpl agentPluginParamsByAgentId(String agentId) { List list = agentPluginMappingMapper.selectPluginsByAgentId(agentId); - int index = 0; + Map> knowledgeBaseMap = new HashMap<>(); + Map modelConfigMap = new HashMap<>(); for (int i = list.size() - 1; i >= 0; i--) { AgentPluginMapping mapping = list.get(i); if (StringUtils.isBlank(mapping.getProviderCode())) { @@ -51,14 +53,39 @@ public class AgentPluginMappingServiceImpl extends ServiceImpl paramInfo = new HashMap<>(2); - paramInfo.put("name", knowledgeBaseEntity.getName()); - paramInfo.put("description", knowledgeBaseEntity.getDescription()); - mapping.setParamInfo(JSONUtils.toJSONString(paramInfo)); - String providerCode = "xzKnowledgeBase_search_from_" + modelConfigEntity.getModelCode() + "_" - + index; - index++; - mapping.setProviderCode(providerCode); + List knowledgeBaseList = knowledgeBaseMap.get(modelConfigEntity.getModelCode()); + if (knowledgeBaseList == null) { + knowledgeBaseList = new ArrayList<>(); + } + modelConfigMap.put(modelConfigEntity.getModelCode(), modelConfigEntity); + knowledgeBaseList.add(knowledgeBaseEntity); + knowledgeBaseMap.put(modelConfigEntity.getModelCode(), knowledgeBaseList); + list.remove(i); + } + } + if (knowledgeBaseMap.size() > 0) { + for (String pluginCode : knowledgeBaseMap.keySet()) { + List knowledgeBaseList = knowledgeBaseMap.get(pluginCode); + if (knowledgeBaseList == null || knowledgeBaseList.isEmpty()) { + continue; + } + + AgentPluginMapping agentPluginMapping = new AgentPluginMapping(); + agentPluginMapping.setAgentId(agentId); + agentPluginMapping.setPluginId(pluginCode); + agentPluginMapping.setProviderCode("search_from_" + pluginCode); + agentPluginMapping.setId(Long.valueOf(list.size() + 1)); + + Map paramInfo = new HashMap<>(4); + ModelConfigEntity modelConfigEntity = modelConfigMap.get(pluginCode); + paramInfo.put("base_url", modelConfigEntity.getConfigJson().getStr("base_url")); + paramInfo.put("api_key", modelConfigEntity.getConfigJson().getStr("api_key")); + paramInfo.put("dataset_ids", + knowledgeBaseList.stream().map(KnowledgeBaseEntity::getDatasetId).toList()); + paramInfo.put("description", + String.join(",", knowledgeBaseList.stream().map(KnowledgeBaseEntity::getDescription).toList())); + agentPluginMapping.setParamInfo(JsonUtils.toJsonString(paramInfo)); + list.add(agentPluginMapping); } } return list; diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 248be252..8bdf3860 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -147,7 +147,15 @@ plugins: - ".wav" - ".p3" refresh_time: 300 # 刷新音乐列表的时间间隔,单位为秒 - + search_from_ragflow: + # 知识库的描述信息,方便大语言模型知道什么时候调用 + description: "当用户问xxx时,调用本方法,使用知识库中的信息回答问题" + # ragflow接口配置 + base_url: "http://192.168.0.8" + # ragflow api访问令牌 + api_key: "ragflow-xxx" + # ragflow知识库id + dataset_ids: ["123456789"] # 声纹识别配置 voiceprint: # 声纹接口地址 @@ -239,6 +247,7 @@ Intent: functions: - change_role - get_weather + # - search_from_ragflow # - get_news_from_chinanews - get_news_from_newsnow # play_music是服务器自带的音乐播放,hass_play_music是通过home assistant控制的独立外部程序音乐播放 diff --git a/main/xiaozhi-server/core/providers/tools/server_plugins/plugin_executor.py b/main/xiaozhi-server/core/providers/tools/server_plugins/plugin_executor.py index 9840351d..6c61eacd 100644 --- a/main/xiaozhi-server/core/providers/tools/server_plugins/plugin_executor.py +++ b/main/xiaozhi-server/core/providers/tools/server_plugins/plugin_executor.py @@ -1,6 +1,6 @@ """服务端插件工具执行器""" -from typing import Dict, Any, Optional, Tuple +from typing import Dict, Any from ..base import ToolType, ToolDefinition, ToolExecutor from plugins_func.register import all_function_registry, Action, ActionResponse @@ -11,53 +11,12 @@ class ServerPluginExecutor(ToolExecutor): def __init__(self, conn): self.conn = conn self.config = conn.config - # 存储知识库工具名称到真实插件名称的映射 - self._knowledge_base_mapping: Dict[str, str] = {} - - def _parse_knowledge_base_config(self, config_name: str) -> Optional[str]: - """ - 解析知识库配置名称,提取真实的插件名称 - - Args: - config_name: 配置名称,格式为 xzKnowledgeBase__ - - Returns: - 真实的插件名称,如果不是知识库配置则返回 None - - Example: - "xzKnowledgeBase_search_from_ragflow_0" -> "search_from_ragflow" - "xzKnowledgeBase_search_from_ragflow_1" -> "search_from_ragflow" - """ - if not config_name.startswith("xzKnowledgeBase_"): - return None - - # 移除前缀 - name_without_prefix = config_name[len("xzKnowledgeBase_"):] - - # 找到最后一个下划线的位置 - last_underscore_index = name_without_prefix.rfind("_") - - if last_underscore_index == -1: - return None - - # 提取真实插件名称(从开头到最后一个下划线之前) - real_plugin_name = name_without_prefix[:last_underscore_index] - - return real_plugin_name async def execute( self, conn, tool_name: str, arguments: Dict[str, Any] ) -> ActionResponse: """执行服务端插件工具""" - # 检查是否是知识库工具调用 - real_plugin_name = self._knowledge_base_mapping.get(tool_name) - if real_plugin_name: - # 使用真实的插件名称获取函数 - func_item = all_function_registry.get(real_plugin_name) - else: - # 普通插件调用 - func_item = all_function_registry.get(tool_name) - + func_item = all_function_registry.get(tool_name) if not func_item: return ActionResponse( action=Action.NOTFOUND, response=f"插件函数 {tool_name} 不存在" @@ -112,51 +71,27 @@ class ServerPluginExecutor(ToolExecutor): for func_name in all_required_functions: func_item = all_function_registry.get(func_name) if func_item: + # 从函数注册中获取描述 + fun_description = ( + self.config.get("plugins", {}) + .get(func_name, {}) + .get("description", "") + ) + if fun_description is not None and len(fun_description) > 0: + if "function" in func_item.description and isinstance( + func_item.description["function"], dict + ): + func_item.description["function"][ + "description" + ] = fun_description tools[func_name] = ToolDefinition( name=func_name, description=func_item.description, tool_type=ToolType.SERVER_PLUGIN, ) - # 处理知识库配置 - plugins_config = self.config.get("plugins", {}) - for config_name, config_value in plugins_config.items(): - # 检查是否是知识库配置 - real_plugin_name = self._parse_knowledge_base_config(config_name) - if real_plugin_name: - # 获取真实的插件函数 - func_item = all_function_registry.get(real_plugin_name) - if func_item and isinstance(config_value, dict): - # 从配置中获取自定义的 name 和 description - custom_name = config_value.get("name", "") - custom_description = config_value.get("description", "") - - # 创建动态的工具名称(使用配置名称去掉前缀部分作为工具名) - tool_name = config_name[len("xzKnowledgeBase_"):] - - # 复制原始函数描述并修改 - custom_func_desc = func_item.description.copy() - if "function" in custom_func_desc: - custom_func_desc["function"] = custom_func_desc["function"].copy() - custom_func_desc["function"]["name"] = tool_name - custom_func_desc["function"]["description"] = custom_description - - # 注册工具 - tools[tool_name] = ToolDefinition( - name=tool_name, - description=custom_func_desc, - tool_type=ToolType.SERVER_PLUGIN, - ) - - # 保存映射关系 - self._knowledge_base_mapping[tool_name] = real_plugin_name - return tools def has_tool(self, tool_name: str) -> bool: """检查是否有指定的服务端插件工具""" - # 检查是否是知识库工具 - if tool_name in self._knowledge_base_mapping: - return True - # 检查是否是普通工具 return tool_name in all_function_registry diff --git a/main/xiaozhi-server/plugins_func/functions/search_from_ragflow.py b/main/xiaozhi-server/plugins_func/functions/search_from_ragflow.py index ae1ed42f..cdb709e3 100644 --- a/main/xiaozhi-server/plugins_func/functions/search_from_ragflow.py +++ b/main/xiaozhi-server/plugins_func/functions/search_from_ragflow.py @@ -1,5 +1,11 @@ +import requests +import sys +from config.logger import setup_logging from plugins_func.register import register_function, ToolType, ActionResponse, Action +TAG = __name__ +logger = setup_logging() + # 定义基础的函数描述模板 SEARCH_FROM_RAGFLOW_FUNCTION_DESC = { "type": "function", @@ -8,26 +14,85 @@ SEARCH_FROM_RAGFLOW_FUNCTION_DESC = { "description": "从知识库中查询信息", "parameters": { "type": "object", - "properties": {"query": {"type": "string", "description": "查询的关键词"}}, - "required": ["query"], + "properties": {"question": {"type": "string", "description": "查询的问题"}}, + "required": ["question"], }, }, } @register_function( - "search_from_ragflow", SEARCH_FROM_RAGFLOW_FUNCTION_DESC, ToolType.WAIT + "search_from_ragflow", SEARCH_FROM_RAGFLOW_FUNCTION_DESC, ToolType.SYSTEM_CTL ) -def search_from_ragflow(query=None): - """ - 用于从ragflow知识库中查询信息 - """ - # TODO 从ragflow知识库中查询信息 - if query and "医生" in query: - response_text = "医院有张山、里斯、王五3名全科医生,其中王五医生是主要擅长眼科" - elif query and "科室" in query: - response_text = "医院眼科、麻醉科" +def search_from_ragflow(conn, question=None): + # 确保字符串参数正确处理编码 + if question and isinstance(question, str): + # 确保问题参数是UTF-8编码的字符串 + pass else: - response_text = "暂无相关信息" + question = str(question) if question is not None else "" - return ActionResponse(Action.REQLLM, response_text, None) + base_url = conn.config["plugins"]["search_from_ragflow"].get("base_url", "") + api_key = conn.config["plugins"]["search_from_ragflow"].get("api_key", "") + dataset_ids = conn.config["plugins"]["search_from_ragflow"].get("dataset_ids", []) + + url = base_url + "/api/v1/retrieval" + headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} + + # 确保payload中的字符串都是UTF-8编码 + payload = {"question": question, "dataset_ids": dataset_ids} + + try: + # 使用ensure_ascii=False确保JSON序列化时正确处理中文 + response = requests.post( + url, + json=payload, + headers=headers, + timeout=5, + verify=False, + ) + + # 显式设置响应的编码为utf-8 + response.encoding = "utf-8" + + response.raise_for_status() + + # 先获取文本内容,然后手动处理JSON解码 + response_text = response.text + import json + + result = json.loads(response_text) + + if result.get("code") != 0: + error_detail = response.get("error", {}).get("detail", "") + # 安全地记录错误信息 + logger.bind(tag=TAG).error( + "从RAGflow获取信息失败,原因:%s", str(error_detail) + ) + return ActionResponse(Action.RESPONSE, None, "RAG接口返回异常") + + chunks = result.get("data", {}).get("chunks", []) + contents = [] + for chunk in chunks: + content = chunk.get("content", "") + if content: + # 安全地处理内容字符串 + if isinstance(content, str): + contents.append(content) + elif isinstance(content, bytes): + contents.append(content.decode("utf-8", errors="replace")) + else: + contents.append(str(content)) + + # 构建适合大模型的上下文内容(每段前加编号,段间两个换行) + context_text = "\n\n".join( + f"{i+1}. {c.strip()}" for i, c in enumerate(contents[:5]) + ) + if not context_text: + context_text = "根据知识库查询结果,没有相关信息。" + return ActionResponse(Action.REQLLM, context_text, None) + + except Exception as e: + # 使用安全的方式记录异常,避免编码问题 + logger.bind(tag=TAG).error("从RAGflow获取信息失败,原因:%s", str(e)) + return ActionResponse(Action.RESPONSE, None, "RAG接口返回异常")