From 8504c181c0b89f7876e9653a5012a4c29ef33584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=84=E5=87=A4=E7=A7=91=E6=8A=80?= Date: Thu, 20 Mar 2025 08:59:45 +0800 Subject: [PATCH 01/28] =?UTF-8?q?mcp=E5=88=9D=E6=AD=A5=E8=B0=83=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/connection.py | 31 +++++- main/xiaozhi-server/core/mcp/MCPClient.py | 96 +++++++++++++++++ main/xiaozhi-server/core/mcp/manager.py | 102 ++++++++++++++++++ .../plugins_func/functions/get_time.py | 2 +- main/xiaozhi-server/plugins_func/register.py | 1 + 5 files changed, 228 insertions(+), 4 deletions(-) create mode 100644 main/xiaozhi-server/core/mcp/MCPClient.py create mode 100644 main/xiaozhi-server/core/mcp/manager.py diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index 718398c3..b5e7dcf2 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -22,6 +22,7 @@ from plugins_func.register import Action from config.private_config import PrivateConfig from core.auth import AuthMiddleware, AuthenticationError from core.utils.auth_code_gen import AuthCodeGenerator +from core.mcp.manager import MCPManager TAG = __name__ @@ -98,6 +99,8 @@ class ConnectionHandler: self.use_function_call_mode = False if self.config["selected_module"]["Intent"] == 'function_call': self.use_function_call_mode = True + + self.mcp_manager = MCPManager(self) async def handle_connection(self, ws): try: @@ -151,6 +154,8 @@ class ConnectionHandler: # 异步初始化 await self.loop.run_in_executor(None, self._initialize_components) + # 初始化MCP服务 + await self.mcp_manager.initialize_servers() # tts 消化线程 tts_priority = threading.Thread(target=self._tts_priority_thread, daemon=True) @@ -317,7 +322,9 @@ class ConnectionHandler: # Define intent functions functions = self.func_handler.get_functions() - + mcpfuncs = self.mcp_manager.get_all_tools() # MCP工具 + functions.extend(mcpfuncs) + print("functions with mcp", functions) response_message = [] processed_chars = 0 # 跟踪已处理的字符位置 @@ -429,8 +436,24 @@ class ConnectionHandler: "id": function_id, "arguments": function_arguments } - result = self.func_handler.handle_llm_function_call(self, function_call_data) - self._handle_function_result(result, function_call_data, text_index + 1) + #result = self.func_handler.handle_llm_function_call(self, function_call_data) + #self._handle_function_result(result, function_call_data, text_index+1) + + # 处理MCP工具调用 + if self.mcp_manager.is_mcp_tool(function_name): + try: + tool_result = asyncio.create_task(self.mcp_manager.execute_tool( + function_name, + function_arguments + )) + self._handle_mcp_tool_result(tool_result, function_call_data, text_index+1) + except Exception as e: + self.logger.bind(tag=TAG).error(f"MCP工具调用错误: {e}") + response_message.append(f"MCP工具调用失败: {str(e)}") + else: + # 处理系统函数 + result = self.func_handler.handle_llm_function_call(self, function_call_data) + self._handle_function_result(result, function_call_data, text_index+1) # 处理最后剩余的文本 full_text = "".join(response_message) @@ -565,6 +588,8 @@ class ConnectionHandler: async def close(self): """资源清理方法""" + # 清理MCP资源 + await self.mcp_manager.cleanup_all() # 清理其他资源 self.stop_event.set() diff --git a/main/xiaozhi-server/core/mcp/MCPClient.py b/main/xiaozhi-server/core/mcp/MCPClient.py new file mode 100644 index 00000000..903ce60b --- /dev/null +++ b/main/xiaozhi-server/core/mcp/MCPClient.py @@ -0,0 +1,96 @@ +import asyncio +from typing import Optional +from contextlib import AsyncExitStack +import os +from mcp import ClientSession, StdioServerParameters +from mcp.client.stdio import stdio_client + +from config.logger import setup_logging + +TAG = __name__ + +class MCPClient: + def __init__(self, config): + # Initialize session and client objects + self.session: Optional[ClientSession] = None + self.exit_stack = AsyncExitStack() + self.logger = setup_logging() + self.config = config + self.tolls = [] + + async def initialize(self): + main_command = self.config["command"] + args = self.config.get("args", []) + + server_script_path = main_command + if args: + # 如果第一个参数是路径,使用其目录作为工作目录 + possible_path = args[0] + if os.path.exists(possible_path): + server_script_path = possible_path + await self.connect_to_server(server_script_path) + + + async def connect_to_server(self, server_script_path: str): + """Connect to an MCP server + + Args: + server_script_path: Path to the server script (.py or .js) + """ + is_python = server_script_path.endswith('.py') + is_js = server_script_path.endswith('.js') + if not (is_python or is_js): + raise ValueError("Server script must be a .py or .js file") + + env = self.config.get("env", {}) + command = "python" if is_python else "node" + server_params = StdioServerParameters( + command=command, + args=[server_script_path], + env=env + ) + + stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params)) + self.stdio, self.write = stdio_transport + self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write)) + + await self.session.initialize() + + # List available tools + response = await self.session.list_tools() + tools = response.tools + self.tools = tools + self.logger.bind(tag=TAG).info(f"Connected to server with tools:{[tool.name for tool in tools]}") + + def get_available_tools(self): + available_tools = [{"type": "function", "function":{ + "name": tool.name, + "description": tool.description, + "parameters": tool.inputSchema + } } for tool in self.tools] + + return available_tools + + async def call_tool(self, tool_name: str, tool_args: dict): + response = await self.session.call_tool(tool_name, tool_args) + return response + + async def cleanup(self): + """Clean up resources""" + await self.exit_stack.aclose() + +async def main(): + if len(sys.argv) < 2: + print("Usage: python client.py ") + sys.exit(1) + + client = MCPClient() + try: + await client.connect_to_server(sys.argv[1]) + await client.chat_loop() + finally: + await client.cleanup() + +if __name__ == "__main__": + import sys + asyncio.run(main()) \ No newline at end of file diff --git a/main/xiaozhi-server/core/mcp/manager.py b/main/xiaozhi-server/core/mcp/manager.py new file mode 100644 index 00000000..f9b4a742 --- /dev/null +++ b/main/xiaozhi-server/core/mcp/manager.py @@ -0,0 +1,102 @@ +"""MCP服务管理器""" +import json +from typing import Dict, Any, List +from .MCPClient import MCPClient +from config.logger import setup_logging +from core.utils.util import get_project_dir +from plugins_func.register import register_function, ActionResponse, Action, ToolType + +TAG = __name__ + +class MCPManager: + """管理多个MCP服务的集中管理器""" + + def __init__(self,conn) -> None: + """ + 初始化MCP管理器 + """ + self.conn = conn + self.config_path = get_project_dir() + 'data/.mcp_server_settings.json' + self.client: Dict[str, MCPClient] = {} + self.logger = setup_logging() + self.tools = [] + + def load_config(self) -> Dict[str, Any]: + """加载MCP服务配置 + Returns: + Dict[str, Any]: 服务配置字典 + """ + try: + with open(self.config_path, 'r', encoding='utf-8') as f: + config = json.load(f) + return config.get('mcpServers', {}) + except Exception as e: + self.logger.bind(tag=TAG).error(f"Error loading MCP config from {self.config_path}: {e}") + return {} + + async def initialize_servers(self) -> None: + """初始化所有MCP服务""" + config = self.load_config() + for name, srv_config in config.items(): + if not srv_config.get("command"): + self.logger.bind(tag=TAG).warning(f"Skipping server {name}: command not specified") + continue + + try: + client = MCPClient(srv_config) + await client.initialize() + self.client[name] = client + self.logger.bind(tag=TAG).info(f"Initialized MCP client: {name}") + client_tools = client.get_available_tools() + self.tools.extend(client_tools) + for tool in client_tools: + func_name = "mcp_"+tool["function"]["name"] + register_function(func_name, tool, ToolType.MCP_CLIENT)(self.execute_tool) + self.conn.func_handler.function_registry.register_function(func_name) + + except Exception as e: + self.logger.bind(tag=TAG).error(f"Failed to initialize MCP server {name}: {e}") + + def get_all_tools(self) -> List[Dict[str, Any]]: + """获取所有服务的工具function定义 + Returns: + List[Dict[str, Any]]: 所有工具的function定义列表 + """ + return self.tools + + def is_mcp_tool(self, tool_name: str) -> bool: + """检查是否是MCP工具 + Args: + tool_name: 工具名称 + Returns: + bool: 是否是MCP工具 + """ + for tool in self.tools: + if tool.get("function") != None and tool["function"].get("name") == tool_name: + return True + return False + + async def execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any: + """执行工具调用 + Args: + tool_name: 工具名称 + arguments: 工具参数 + Returns: + Any: 工具执行结果 + Raises: + ValueError: 工具未找到时抛出 + """ + for client in self.client.values(): + if any(tool_name == tool["name"] for tool in client.tools): + return await client.call_tool(tool_name, arguments) + + raise ValueError(f"Tool {tool_name} not found in any MCP server") + + async def cleanup_all(self) -> None: + for name, client in self.client.items(): + try: + await client.cleanup() + self.logger.bind(tag=TAG).info(f"Cleaned up MCP client: {name}") + except Exception as e: + self.logger.bind(tag=TAG).error(f"Error cleaning up MCP client {name}: {e}") + self.client.clear() diff --git a/main/xiaozhi-server/plugins_func/functions/get_time.py b/main/xiaozhi-server/plugins_func/functions/get_time.py index 1d141ed2..9a9aebfe 100644 --- a/main/xiaozhi-server/plugins_func/functions/get_time.py +++ b/main/xiaozhi-server/plugins_func/functions/get_time.py @@ -6,7 +6,7 @@ get_time_function_desc = { "function": { "name": "get_time", "description": "获取当前时间、日期、星期几", - "parameters": {} + 'parameters': {'type': 'object', 'properties': {}, 'required': []} } } diff --git a/main/xiaozhi-server/plugins_func/register.py b/main/xiaozhi-server/plugins_func/register.py index ccb03b44..fd990760 100644 --- a/main/xiaozhi-server/plugins_func/register.py +++ b/main/xiaozhi-server/plugins_func/register.py @@ -12,6 +12,7 @@ class ToolType(Enum): CHANGE_SYS_PROMPT = (3, "修改系统提示词,切换角色性格或职责") SYSTEM_CTL = (4, "系统控制,影响正常的对话流程,如退出、播放音乐等,需要传递conn参数") IOT_CTL = (5, "IOT设备控制,需要传递conn参数") + MCP_CLIENT = (6, "MCP客户端") def __init__(self, code, message): self.code = code From de9809ca5aa15ba25440e7a5282eef0f6b20325f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=84=E5=87=A4=E7=A7=91=E6=8A=80?= Date: Thu, 20 Mar 2025 09:06:36 +0800 Subject: [PATCH 02/28] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E5=AF=BC=E8=87=B4chatglm=E5=8D=A1=E4=BD=8F?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/plugins_func/functions/get_time.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/xiaozhi-server/plugins_func/functions/get_time.py b/main/xiaozhi-server/plugins_func/functions/get_time.py index 1d141ed2..9a9aebfe 100644 --- a/main/xiaozhi-server/plugins_func/functions/get_time.py +++ b/main/xiaozhi-server/plugins_func/functions/get_time.py @@ -6,7 +6,7 @@ get_time_function_desc = { "function": { "name": "get_time", "description": "获取当前时间、日期、星期几", - "parameters": {} + 'parameters': {'type': 'object', 'properties': {}, 'required': []} } } From 2b81ebca8e3bbebef5c90c015f58d4ef355bb1b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=84=E5=87=A4=E7=A7=91=E6=8A=80?= Date: Thu, 20 Mar 2025 11:52:37 +0800 Subject: [PATCH 03/28] =?UTF-8?q?=E8=B0=83=E9=80=9Amcp=20tool?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/connection.py | 52 ++++++++++++++------- main/xiaozhi-server/core/mcp/MCPClient.py | 55 ++++++----------------- main/xiaozhi-server/core/mcp/manager.py | 5 ++- 3 files changed, 53 insertions(+), 59 deletions(-) diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index b5e7dcf2..088598e2 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -18,7 +18,7 @@ from concurrent.futures import ThreadPoolExecutor, TimeoutError from core.handle.sendAudioHandle import sendAudioMessage from core.handle.receiveAudioHandle import handleAudioMessage from core.handle.functionHandler import FunctionHandler -from plugins_func.register import Action +from plugins_func.register import Action, ActionResponse from config.private_config import PrivateConfig from core.auth import AuthMiddleware, AuthenticationError from core.utils.auth_code_gen import AuthCodeGenerator @@ -196,9 +196,9 @@ class ConnectionHandler: if self.private_config: self.prompt = self.private_config.private_config.get("prompt", self.prompt) - self.client_ip_info = get_ip_info(self.client_ip) - self.logger.bind(tag=TAG).info(f"Client ip info: {self.client_ip_info}") - self.prompt = self.prompt + f"\n我在:{self.client_ip_info}" + #self.client_ip_info = get_ip_info(self.client_ip) + #self.logger.bind(tag=TAG).info(f"Client ip info: {self.client_ip_info}") + #self.prompt = self.prompt + f"\n我在:{self.client_ip_info}" self.dialogue.put(Message(role="system", content=self.prompt)) self.func_handler = FunctionHandler(self.config) @@ -436,24 +436,14 @@ class ConnectionHandler: "id": function_id, "arguments": function_arguments } - #result = self.func_handler.handle_llm_function_call(self, function_call_data) - #self._handle_function_result(result, function_call_data, text_index+1) # 处理MCP工具调用 if self.mcp_manager.is_mcp_tool(function_name): - try: - tool_result = asyncio.create_task(self.mcp_manager.execute_tool( - function_name, - function_arguments - )) - self._handle_mcp_tool_result(tool_result, function_call_data, text_index+1) - except Exception as e: - self.logger.bind(tag=TAG).error(f"MCP工具调用错误: {e}") - response_message.append(f"MCP工具调用失败: {str(e)}") + result = self._handle_mcp_tool_call(function_call_data) else: # 处理系统函数 result = self.func_handler.handle_llm_function_call(self, function_call_data) - self._handle_function_result(result, function_call_data, text_index+1) + self._handle_function_result(result, function_call_data, text_index+1) # 处理最后剩余的文本 full_text = "".join(response_message) @@ -475,6 +465,36 @@ class ConnectionHandler: return True + def _handle_mcp_tool_call(self, function_call_data): + function_arguments = function_call_data["arguments"] + function_name = function_call_data["name"] + try: + args_dict = function_arguments + if isinstance(function_arguments, str): + try: + args_dict = json.loads(function_arguments) + except json.JSONDecodeError: + self.logger.bind(tag=TAG).error(f"无法解析 function_arguments: {function_arguments}") + return ActionResponse(action=Action.REQLLM, result="参数解析失败", response="") + + tool_result = asyncio.run_coroutine_threadsafe(self.mcp_manager.execute_tool( + function_name, + args_dict + ), self.loop).result() + # meta=None content=[TextContent(type='text', text='北京当前天气:\n温度: 21°C\n天气: 晴\n湿度: 6%\n风向: 西北 风\n风力等级: 5级', annotations=None)] isError=False + self.logger.bind(tag=TAG).info(f"tool_result:{tool_result}") + if tool_result is not None: + if tool_result.content is not None: + print(tool_result.content) + return ActionResponse(action=Action.REQLLM, result=tool_result.content[0].text, response="") + + except Exception as e: + self.logger.bind(tag=TAG).error(f"MCP工具调用错误: {e}") + return ActionResponse(action=Action.REQLLM, result="工具调用出错", response="") + + return ActionResponse(action=Action.REQLLM, result="工具调用出错", response="") + + def _handle_function_result(self, result, function_call_data, text_index): if result.action == Action.RESPONSE: # 直接回复前端 text = result.response diff --git a/main/xiaozhi-server/core/mcp/MCPClient.py b/main/xiaozhi-server/core/mcp/MCPClient.py index 903ce60b..a8be61e1 100644 --- a/main/xiaozhi-server/core/mcp/MCPClient.py +++ b/main/xiaozhi-server/core/mcp/MCPClient.py @@ -1,7 +1,7 @@ import asyncio from typing import Optional from contextlib import AsyncExitStack -import os +import os, shutil from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client @@ -19,34 +19,19 @@ class MCPClient: self.tolls = [] async def initialize(self): - main_command = self.config["command"] args = self.config.get("args", []) - server_script_path = main_command - if args: - # 如果第一个参数是路径,使用其目录作为工作目录 - possible_path = args[0] - if os.path.exists(possible_path): - server_script_path = possible_path - await self.connect_to_server(server_script_path) + command = ( + shutil.which("npx") + if self.config["command"] == "npx" + else self.config["command"] + ) - - async def connect_to_server(self, server_script_path: str): - """Connect to an MCP server + env={**os.environ, **self.config["env"]} - Args: - server_script_path: Path to the server script (.py or .js) - """ - is_python = server_script_path.endswith('.py') - is_js = server_script_path.endswith('.js') - if not (is_python or is_js): - raise ValueError("Server script must be a .py or .js file") - - env = self.config.get("env", {}) - command = "python" if is_python else "node" server_params = StdioServerParameters( command=command, - args=[server_script_path], + args=args, env=env ) @@ -61,7 +46,10 @@ class MCPClient: tools = response.tools self.tools = tools self.logger.bind(tag=TAG).info(f"Connected to server with tools:{[tool.name for tool in tools]}") - + + def has_tool(self, tool_name): + return any(tool.name == tool_name for tool in self.tools) + def get_available_tools(self): available_tools = [{"type": "function", "function":{ "name": tool.name, @@ -72,25 +60,10 @@ class MCPClient: return available_tools async def call_tool(self, tool_name: str, tool_args: dict): + self.logger.bind(tag=TAG).info(f"MCPClient Calling tool {tool_name} with args: {tool_args}") response = await self.session.call_tool(tool_name, tool_args) return response async def cleanup(self): """Clean up resources""" - await self.exit_stack.aclose() - -async def main(): - if len(sys.argv) < 2: - print("Usage: python client.py ") - sys.exit(1) - - client = MCPClient() - try: - await client.connect_to_server(sys.argv[1]) - await client.chat_loop() - finally: - await client.cleanup() - -if __name__ == "__main__": - import sys - asyncio.run(main()) \ No newline at end of file + await self.exit_stack.aclose() \ No newline at end of file diff --git a/main/xiaozhi-server/core/mcp/manager.py b/main/xiaozhi-server/core/mcp/manager.py index f9b4a742..cae3bcf2 100644 --- a/main/xiaozhi-server/core/mcp/manager.py +++ b/main/xiaozhi-server/core/mcp/manager.py @@ -80,14 +80,15 @@ class MCPManager: """执行工具调用 Args: tool_name: 工具名称 - arguments: 工具参数 + arguments: 工具参数 Returns: Any: 工具执行结果 Raises: ValueError: 工具未找到时抛出 """ + self.logger.bind(tag=TAG).info(f"Executing tool {tool_name} with arguments: {arguments}") for client in self.client.values(): - if any(tool_name == tool["name"] for tool in client.tools): + if client.has_tool(tool_name): return await client.call_tool(tool_name, arguments) raise ValueError(f"Tool {tool_name} not found in any MCP server") From 898fd36eca4e3e5b78b4325b8a03038627742f80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=84=E5=87=A4=E7=A7=91=E6=8A=80?= Date: Thu, 20 Mar 2025 18:20:09 +0800 Subject: [PATCH 04/28] =?UTF-8?q?=E5=A2=9E=E5=8A=A0mcp=E8=B0=83=E7=94=A8?= =?UTF-8?q?=E8=B6=85=E6=97=B6=E6=A3=80=E6=B5=8B=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/connection.py | 19 +++++++++------- main/xiaozhi-server/core/mcp/MCPClient.py | 27 ++++++++++++++++++----- main/xiaozhi-server/core/mcp/manager.py | 12 +++++++--- 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index 088598e2..e536d00e 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -322,9 +322,6 @@ class ConnectionHandler: # Define intent functions functions = self.func_handler.get_functions() - mcpfuncs = self.mcp_manager.get_all_tools() # MCP工具 - functions.extend(mcpfuncs) - print("functions with mcp", functions) response_message = [] processed_chars = 0 # 跟踪已处理的字符位置 @@ -482,11 +479,17 @@ class ConnectionHandler: args_dict ), self.loop).result() # meta=None content=[TextContent(type='text', text='北京当前天气:\n温度: 21°C\n天气: 晴\n湿度: 6%\n风向: 西北 风\n风力等级: 5级', annotations=None)] isError=False - self.logger.bind(tag=TAG).info(f"tool_result:{tool_result}") - if tool_result is not None: - if tool_result.content is not None: - print(tool_result.content) - return ActionResponse(action=Action.REQLLM, result=tool_result.content[0].text, response="") + content_text = "" + if tool_result is not None and tool_result.content is not None: + for content in tool_result.content: + content_type = content.type + if content_type == "text": + content_text = content.text + elif content_type == "image": + pass + + if len(content_text) > 0: + return ActionResponse(action=Action.REQLLM, result=content_text, response="") except Exception as e: self.logger.bind(tag=TAG).error(f"MCP工具调用错误: {e}") diff --git a/main/xiaozhi-server/core/mcp/MCPClient.py b/main/xiaozhi-server/core/mcp/MCPClient.py index a8be61e1..3c32e077 100644 --- a/main/xiaozhi-server/core/mcp/MCPClient.py +++ b/main/xiaozhi-server/core/mcp/MCPClient.py @@ -1,4 +1,4 @@ -import asyncio +from datetime import timedelta from typing import Optional from contextlib import AsyncExitStack import os, shutil @@ -26,8 +26,10 @@ class MCPClient: if self.config["command"] == "npx" else self.config["command"] ) - - env={**os.environ, **self.config["env"]} + + env={**os.environ} + if self.config.get("env"): + env.update(self.config["env"]) server_params = StdioServerParameters( command=command, @@ -37,7 +39,8 @@ class MCPClient: stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params)) self.stdio, self.write = stdio_transport - self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write)) + time_out_delta = timedelta(seconds=10) + self.session = await self.exit_stack.enter_async_context(ClientSession(read_stream=self.stdio, write_stream=self.write, read_timeout_seconds=time_out_delta)) await self.session.initialize() @@ -61,7 +64,21 @@ class MCPClient: async def call_tool(self, tool_name: str, tool_args: dict): self.logger.bind(tag=TAG).info(f"MCPClient Calling tool {tool_name} with args: {tool_args}") - response = await self.session.call_tool(tool_name, tool_args) + try: + response = await self.session.call_tool(tool_name, tool_args) + except Exception as e: + self.logger.bind(tag=TAG).error(f"Error calling tool {tool_name}: {e}") + from types import SimpleNamespace + error_content = SimpleNamespace( + type='text', + text=f"Error calling tool {tool_name}: {e}" + ) + error_response = SimpleNamespace( + content=[error_content], + isError=True + ) + return error_response + self.logger.bind(tag=TAG).info(f"MCPClient Response from tool {tool_name}: {response}") return response async def cleanup(self): diff --git a/main/xiaozhi-server/core/mcp/manager.py b/main/xiaozhi-server/core/mcp/manager.py index cae3bcf2..810554e7 100644 --- a/main/xiaozhi-server/core/mcp/manager.py +++ b/main/xiaozhi-server/core/mcp/manager.py @@ -1,5 +1,5 @@ """MCP服务管理器""" -import json +import os, json from typing import Dict, Any, List from .MCPClient import MCPClient from config.logger import setup_logging @@ -16,9 +16,12 @@ class MCPManager: 初始化MCP管理器 """ self.conn = conn - self.config_path = get_project_dir() + 'data/.mcp_server_settings.json' - self.client: Dict[str, MCPClient] = {} self.logger = setup_logging() + self.config_path = get_project_dir() + 'data/.mcp_server_settings.json' + if os.path.exists(self.config_path) == False: + self.config_path = "" + self.logger.bind(tag=TAG).warning(f"请检查mcp服务配置文件:data/.mcp_server_settings.json") + self.client: Dict[str, MCPClient] = {} self.tools = [] def load_config(self) -> Dict[str, Any]: @@ -26,6 +29,9 @@ class MCPManager: Returns: Dict[str, Any]: 服务配置字典 """ + if len(self.config_path) == 0: + return {} + try: with open(self.config_path, 'r', encoding='utf-8') as f: config = json.load(f) From 3ea95ffa48cf17d92471734b5b4c6585eb845d4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=84=E5=87=A4=E7=A7=91=E6=8A=80?= Date: Thu, 20 Mar 2025 18:20:50 +0800 Subject: [PATCH 05/28] =?UTF-8?q?mcp=E6=A8=A1=E6=9D=BF=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/mcp_server_settings.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 main/xiaozhi-server/mcp_server_settings.json diff --git a/main/xiaozhi-server/mcp_server_settings.json b/main/xiaozhi-server/mcp_server_settings.json new file mode 100644 index 00000000..1beb79a3 --- /dev/null +++ b/main/xiaozhi-server/mcp_server_settings.json @@ -0,0 +1,16 @@ +{ + "mcpServers": { + "filesystem": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-filesystem", + "/path/to/other/allowed/dir" + ] + }, + "puppeteer": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-puppeteer"] + } + } +} From 7ead5537f622c10e5395fd2ee37f7cc71ea05601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=84=E5=87=A4=E7=A7=91=E6=8A=80?= Date: Fri, 21 Mar 2025 17:15:54 +0800 Subject: [PATCH 06/28] =?UTF-8?q?=E6=8F=90=E4=BA=A4requirements.txt?= =?UTF-8?q?=E4=B8=ADmcp=E7=89=88=E6=9C=AC=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/main/xiaozhi-server/requirements.txt b/main/xiaozhi-server/requirements.txt index 81d0cdfd..386d054c 100755 --- a/main/xiaozhi-server/requirements.txt +++ b/main/xiaozhi-server/requirements.txt @@ -20,3 +20,4 @@ requests==2.32.3 cozepy==0.12.0 mem0ai==0.1.62 bs4==0.0.2 +mcp==1.4.1 \ No newline at end of file From dc4968f9614c636eb12a234bf51356b6ee5e98c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=84=E5=87=A4=E7=A7=91=E6=8A=80?= Date: Fri, 21 Mar 2025 17:31:29 +0800 Subject: [PATCH 07/28] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E8=A1=8C=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/mcp_server_settings.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/main/xiaozhi-server/mcp_server_settings.json b/main/xiaozhi-server/mcp_server_settings.json index 1beb79a3..7e4dd5d2 100644 --- a/main/xiaozhi-server/mcp_server_settings.json +++ b/main/xiaozhi-server/mcp_server_settings.json @@ -11,6 +11,10 @@ "puppeteer": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-puppeteer"] + }, + "windows-cli": { + "command": "npx", + "args": ["-y", "@simonb97/server-win-cli"] } } } From 8c4c9d88dafeee147aa657bdc8cf1ddca51abf35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=84=E5=87=A4=E7=A7=91=E6=8A=80?= Date: Mon, 24 Mar 2025 10:28:48 +0800 Subject: [PATCH 08/28] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=BD=91=E7=AB=99?= =?UTF-8?q?=E6=B5=8F=E8=A7=88mcp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/connection.py | 3 +++ main/xiaozhi-server/core/mcp/manager.py | 1 + main/xiaozhi-server/mcp_server_settings.json | 19 +++++++++++++++---- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index 447c6f8d..4523911a 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -208,6 +208,9 @@ class ConnectionHandler: self.prompt = self.prompt + f"\nuser location:{self.client_ip_info}" self.dialogue.update_system_message(self.prompt) + """加载MCP工具""" + asyncio.run_coroutine_threadsafe(self.mcp_manager.initialize_servers(), self.loop) + def change_system_prompt(self, prompt): self.prompt = prompt # 找到原来的role==system,替换原来的系统提示 diff --git a/main/xiaozhi-server/core/mcp/manager.py b/main/xiaozhi-server/core/mcp/manager.py index 810554e7..28efd87d 100644 --- a/main/xiaozhi-server/core/mcp/manager.py +++ b/main/xiaozhi-server/core/mcp/manager.py @@ -62,6 +62,7 @@ class MCPManager: except Exception as e: self.logger.bind(tag=TAG).error(f"Failed to initialize MCP server {name}: {e}") + self.conn.func_handler.upload_functions_desc() def get_all_tools(self) -> List[Dict[str, Any]]: """获取所有服务的工具function定义 diff --git a/main/xiaozhi-server/mcp_server_settings.json b/main/xiaozhi-server/mcp_server_settings.json index 7e4dd5d2..a38bbdfb 100644 --- a/main/xiaozhi-server/mcp_server_settings.json +++ b/main/xiaozhi-server/mcp_server_settings.json @@ -1,20 +1,31 @@ { + "des": [ + "在data目录下创建.mcp_server_settings.json文件,可以选择下面的MCP服务,也可以自行添加新的MCP服务。", + "后面不断测试补充好用的mcp服务,欢迎大家一起补充。", + "记得删除注释行,des属性仅为说明,不会被解析。", + "des和link属性,仅为说明安装方式,方便大家查看原始链接,不是必须项。" + ], "mcpServers": { "filesystem": { "command": "npx", "args": [ "-y", "@modelcontextprotocol/server-filesystem", + "/Users/username/Desktop", "/path/to/other/allowed/dir" - ] + ], + "link":"https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem" }, - "puppeteer": { + "playwright": { "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-puppeteer"] + "args": ["-y", "@executeautomation/playwright-mcp-server"], + "des" : "run 'npx playwright install' first", + "link": "https://github.com/executeautomation/mcp-playwright" }, "windows-cli": { "command": "npx", - "args": ["-y", "@simonb97/server-win-cli"] + "args": ["-y", "@simonb97/server-win-cli"], + "link": "https://github.com/SimonB97/win-cli-mcp-server" } } } From 88455604a2a32c6d666a240b73e2f996fc0e61e6 Mon Sep 17 00:00:00 2001 From: koalalgx <82762701+koalalgx@users.noreply.github.com> Date: Tue, 25 Mar 2025 14:22:34 +0800 Subject: [PATCH 09/28] Update config.yaml --- main/xiaozhi-server/config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 2fe438cf..ffa2da33 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -86,7 +86,7 @@ selected_module: # 意图识别使用intent_llm,优点:通用性强,缺点:增加串行前置意图识别模块,会增加处理时间 # 意图识别使用function_call,缺点:需要所选择的LLM支持function_call,优点:按需调用工具、速度快 # 默认免费的ChatGLMLLM就已经支持function_call,但是如果像追求稳定建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-pro-32k-functioncall-241028 - Intent: function_call + Intent: intent_llm # 意图识别,是用于理解用户意图的模块,例如:播放音乐 Intent: @@ -534,4 +534,4 @@ wakeup_words: - "小龙小龙" - "喵喵同学" - "小滨小滨" - - "小冰小冰" \ No newline at end of file + - "小冰小冰" From 2199b8eca701c906b5472b87ddaec7b8042b9ce9 Mon Sep 17 00:00:00 2001 From: koalalgx <82762701+koalalgx@users.noreply.github.com> Date: Tue, 25 Mar 2025 14:24:05 +0800 Subject: [PATCH 10/28] Update config.yaml --- main/xiaozhi-server/config.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index ffa2da33..3c770c88 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -88,6 +88,9 @@ selected_module: # 默认免费的ChatGLMLLM就已经支持function_call,但是如果像追求稳定建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-pro-32k-functioncall-241028 Intent: intent_llm +# 意图识别专用LLM配置,如果设置,则意图识别会使用这个模型而不是主LLM +IntentLLM: XinferenceSmallLLM #ChatGLMLLM #XinferenceSmallLLM # 设置为空字符串""或不设置则使用主LLM + # 意图识别,是用于理解用户意图的模块,例如:播放音乐 Intent: # 不使用意图识别 @@ -181,6 +184,18 @@ VAD: LLM: # 所有openai类型均可以修改超参,以AliLLM为例 # 当前支持的type为openai、dify、ollama,可自行适配 + XinferenceLLM: + # 定义LLM API类型 + type: xinference + # Xinference服务地址和模型名称 + model_name: qwen2.5:72b-AWQ # 使用的模型名称,需要预先在Xinference启动对应模型 + base_url: http://localhost:9997 # Xinference服务地址 + XinferenceSmallLLM: + # 定义轻量级LLM API类型,用于意图识别 + type: xinference + # Xinference服务地址和模型名称 + model_name: qwen2.5:3b-AWQ # 使用的小模型名称,用于意图识别 + base_url: http://localhost:9997 # Xinference服务地址 AliLLM: # 定义LLM API类型 type: openai From 38191f976bd4f4eb36d5be880f887172a999a4f3 Mon Sep 17 00:00:00 2001 From: koalalgx <82762701+koalalgx@users.noreply.github.com> Date: Tue, 25 Mar 2025 14:25:31 +0800 Subject: [PATCH 11/28] Create xinference.py --- .../providers/llm/xinference/xinference.py | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 main/xiaozhi-server/core/providers/llm/xinference/xinference.py diff --git a/main/xiaozhi-server/core/providers/llm/xinference/xinference.py b/main/xiaozhi-server/core/providers/llm/xinference/xinference.py new file mode 100644 index 00000000..b90b0418 --- /dev/null +++ b/main/xiaozhi-server/core/providers/llm/xinference/xinference.py @@ -0,0 +1,85 @@ +from config.logger import setup_logging +from openai import OpenAI +import json +from core.providers.llm.base import LLMProviderBase + +TAG = __name__ +logger = setup_logging() + + +class LLMProvider(LLMProviderBase): + def __init__(self, config): + self.model_name = config.get("model_name") + self.base_url = config.get("base_url", "http://localhost:9997") + # Initialize OpenAI client with Xinference base URL + # 如果没有v1,增加v1 + if not self.base_url.endswith("/v1"): + self.base_url = f"{self.base_url}/v1" + + logger.bind(tag=TAG).info(f"Initializing Xinference LLM provider with model: {self.model_name}, base_url: {self.base_url}") + + try: + self.client = OpenAI( + base_url=self.base_url, + api_key="xinference" # Xinference has a similar setup to Ollama where it doesn't need an actual key + ) + logger.bind(tag=TAG).info("Xinference client initialized successfully") + except Exception as e: + logger.bind(tag=TAG).error(f"Error initializing Xinference client: {e}") + raise + + def response(self, session_id, dialogue): + try: + logger.bind(tag=TAG).debug(f"Sending request to Xinference with model: {self.model_name}, dialogue length: {len(dialogue)}") + responses = self.client.chat.completions.create( + model=self.model_name, + messages=dialogue, + stream=True + ) + is_active=True + for chunk in responses: + try: + delta = chunk.choices[0].delta if getattr(chunk, 'choices', None) else None + content = delta.content if hasattr(delta, 'content') else '' + if content: + if '' in content: + is_active = False + content = content.split('')[0] + if '' in content: + is_active = True + content = content.split('')[-1] + if is_active: + yield content + except Exception as e: + logger.bind(tag=TAG).error(f"Error processing chunk: {e}") + + except Exception as e: + logger.bind(tag=TAG).error(f"Error in Xinference response generation: {e}") + yield "【Xinference服务响应异常】" + + def response_with_functions(self, session_id, dialogue, functions=None): + try: + logger.bind(tag=TAG).debug(f"Sending function call request to Xinference with model: {self.model_name}, dialogue length: {len(dialogue)}") + if functions: + logger.bind(tag=TAG).debug(f"Function calls enabled with: {[f.get('function', {}).get('name') for f in functions]}") + + stream = self.client.chat.completions.create( + model=self.model_name, + messages=dialogue, + stream=True, + tools=functions, + ) + + for chunk in stream: + delta = chunk.choices[0].delta + content = delta.content + tool_calls = delta.tool_calls + + if content: + yield content, tool_calls + elif tool_calls: + yield None, tool_calls + + except Exception as e: + logger.bind(tag=TAG).error(f"Error in Xinference function call: {e}") + yield {"type": "content", "content": f"【Xinference服务响应异常: {str(e)}】"} From 32d7400f7d92d08d75fb2f26eba2dac524e079d0 Mon Sep 17 00:00:00 2001 From: koalalgx <82762701+koalalgx@users.noreply.github.com> Date: Tue, 25 Mar 2025 14:26:46 +0800 Subject: [PATCH 12/28] Update intent_llm.py --- .../providers/intent/intent_llm/intent_llm.py | 253 +++++++++++++++--- 1 file changed, 213 insertions(+), 40 deletions(-) diff --git a/main/xiaozhi-server/core/providers/intent/intent_llm/intent_llm.py b/main/xiaozhi-server/core/providers/intent/intent_llm/intent_llm.py index 9d5d2507..5d0b0830 100644 --- a/main/xiaozhi-server/core/providers/intent/intent_llm/intent_llm.py +++ b/main/xiaozhi-server/core/providers/intent/intent_llm/intent_llm.py @@ -3,6 +3,9 @@ from ..base import IntentProviderBase from plugins_func.functions.play_music import initialize_music_handler from config.logger import setup_logging import re +import json +import hashlib +import time TAG = __name__ logger = setup_logging() @@ -13,6 +16,23 @@ class IntentProvider(IntentProviderBase): super().__init__(config) self.llm = None self.promot = self.get_intent_system_prompt() + # 添加缓存管理 + self.intent_cache = {} # 缓存意图识别结果 + self.cache_expiry = 600 # 缓存有效期10分钟 + self.cache_max_size = 100 # 最多缓存100个意图 + self.common_patterns = { + "天气": '{\"function_call\": {\"name\": \"get_weather\", \"arguments\": {\"location\": null, \"lang\": \"zh_CN\"}}}', + "新闻": '{\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": null, \"detail\": false, \"lang\": \"zh_CN\"}}}', + "财经新闻": '{\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": \"财经\", \"detail\": false, \"lang\": \"zh_CN\"}}}', + "国际新闻": '{\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": \"国际\", \"detail\": false, \"lang\": \"zh_CN\"}}}', + "社会新闻": '{\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": \"社会\", \"detail\": false, \"lang\": \"zh_CN\"}}}', + "详细介绍": '{\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"detail\": true, \"lang\": \"zh_CN\"}}}', + "详情": '{\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"detail\": true, \"lang\": \"zh_CN\"}}}', + "再见": '{\"intent\": \"结束聊天\"}', + "结束": '{\"intent\": \"结束聊天\"}', + "拜拜": '{\"intent\": \"结束聊天\"}', + "播放音乐": '{\"function_call\": {\"name\": \"play_music\", \"arguments\": {\"song_name\": \"random\"}}}' + } def get_intent_system_prompt(self) -> str: """ @@ -25,7 +45,9 @@ class IntentProvider(IntentProviderBase): """ "continue_chat": "1.继续聊天, 除了播放音乐和结束聊天的时候的选项, 比如日常的聊天和问候, 对话等", "end_chat": "2.结束聊天, 用户发来如再见之类的表示结束的话, 不想再进行对话的时候", - "play_music": "3.播放音乐, 用户希望你可以播放音乐, 只用于播放音乐的意图" + "play_music": "3.播放音乐, 用户希望你可以播放音乐, 只用于播放音乐的意图", + "get_weather": "4.查询天气, 用户希望查询某个地点的天气情况" + "get_news": "5.查询新闻, 用户希望查询最新新闻或特定类型的新闻" """ for key, value in self.intent_options.items(): if key == "play_music": @@ -34,50 +56,141 @@ class IntentProvider(IntentProviderBase): intent_list.append("2.结束聊天, 用户发来如再见之类的表示结束的话, 不想再进行对话的时候") elif key == "continue_chat": intent_list.append("1.继续聊天, 除了播放音乐和结束聊天的时候的选项, 比如日常的聊天和问候, 对话等") + elif key == "get_weather": + intent_list.append("4.查询天气, 用户希望查询某个地点的天气情况") + elif key == "get_news": + intent_list.append("5.查询新闻, 用户希望查询最新新闻或特定类型的新闻") else: intent_list.append(value) - # "如果是唱歌、听歌、播放音乐,请指定歌名,格式为'播放音乐 [识别出的歌名]'。\n" - # "如果听不出具体歌名,可以返回'随机播放音乐'。\n" - # "只需要返回意图结果的json,不要解释。" - # "返回格式如下:\n" prompt = ( - "你是一个意图识别助手。你需要根据和用户的对话记录,重点分析用户的最后一句话,判断用户意图属于以下哪一类(使用标志):\n" + "你是一个意图识别助手。请分析用户的最后一句话,判断用户意图属于以下哪一类:\n" "" f"{', '.join(intent_list)}" "\n" - "你需要按照以下的步骤处理用户的对话" - "1. 思考出对话的意图是哪一类的" - "2. 属于1和2的意图, 直接返回,返回格式如下:\n" - "{intent: '用户意图'}\n" - "3. 属于3的意图,则继续分析用户希望播放的音乐\n" - "4. 如果无法识别出具体歌名,可以返回'随机播放音乐'\n" - "{intent: '播放音乐 [获取的音乐名字]'}\n" - "下面是几个处理的示例(思考的内容不返回, 只返回json部分, 无额外的内容)\n" - "```" + "处理步骤:" + "1. 思考意图类型" + "2. 继续聊天和结束聊天意图: 返回intent格式" + "3. 播放音乐意图: 分析歌名,生成function_call格式" + "4. 查询天气意图: 分析地点,生成function_call格式" + "5. 查询新闻意图: 分析新闻类别,生成function_call格式" + "\n\n" + "返回格式示例:\n" + "1. 继续聊天意图: {\"intent\": \"继续聊天\"}\n" + "2. 结束聊天意图: {\"intent\": \"结束聊天\"}\n" + "3. 播放音乐意图: {\"function_call\": {\"name\": \"play_music\", \"arguments\": {\"song_name\": \"音乐名称\"}}}\n" + "4. 查询天气意图: {\"function_call\": {\"name\": \"get_weather\", \"arguments\": {\"location\": \"地点名称\", \"lang\": \"zh_CN\"}}}\n" + "5. 查询新闻意图: {\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": \"新闻类别\", \"detail\": false, \"lang\": \"zh_CN\"}}}\n" + "\n" + "注意:\n" + "- 播放音乐:无歌名时,song_name设为\"random\"\n" + "- 查询天气:无地点时,location设为null\n" + "- 查询新闻:无类别时,category设为null;查询详情时,detail设为true\n" + "- 只返回纯JSON,不要任何其他内容\n" + "\n" + "示例分析:\n" + "```\n" "用户: 你今天怎么样?\n" - "思考(不返回): 用户发来的数据是一个问候语,属于继续聊天的意图, 是种类1, 种类1的需求是直接返回\n" - "返回结果: {intent: '继续聊天'}\n" - "```" - "用户: 我今天有点累了, 我们明天再聊吧\n" - "思考(不返回): 用户表达了今天不想继续对话,属于结束聊天的意图, 是种类2, 种类2的需求是直接返回\n" - "返回结果: {intent: '结束聊天'}\n" - "```" - "用户: 我今天有点累了, 我们明天再聊吧\n" - "思考(不返回): 用户表达了今天不想继续对话,属于结束聊天的意图, 是种类2, 种类2的需求是直接返回\n" - "返回结果: {intent: '结束聊天'}\n" - "```" - "用户: 你可以播放一首中秋月给我听吗\n" - "思考(不返回): 用户表达了想听音乐的续签,属于播放音乐的意图, 是种类3, 种类3的需求需要继续判断播放的音乐, 这里用户希望的歌曲名明确给出是中秋月\n" - "返回结果: {intent: '播放音乐 [中秋月]'}\n" - "```" - "你现在可以使用的音乐的名称如下(使用标志):\n" + "返回: {\"intent\": \"继续聊天\"}\n" + "```\n" + "```\n" + "用户: 我们明天再聊吧\n" + "返回: {\"intent\": \"结束聊天\"}\n" + "```\n" + "```\n" + "用户: 播放中秋月\n" + "返回: {\"function_call\": {\"name\": \"play_music\", \"arguments\": {\"song_name\": \"中秋月\"}}}\n" + "```\n" + "```\n" + "用户: 北京天气怎么样\n" + "返回: {\"function_call\": {\"name\": \"get_weather\", \"arguments\": {\"location\": \"北京\", \"lang\": \"zh_CN\"}}}\n" + "```\n" + "```\n" + "用户: 今天天气怎么样\n" + "返回: {\"function_call\": {\"name\": \"get_weather\", \"arguments\": {\"location\": null, \"lang\": \"zh_CN\"}}}\n" + "```\n" + "```\n" + "用户: 播报财经新闻\n" + "返回: {\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": \"财经\", \"detail\": false, \"lang\": \"zh_CN\"}}}\n" + "```\n" + "```\n" + "用户: 有什么最新新闻\n" + "返回: {\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": null, \"detail\": false, \"lang\": \"zh_CN\"}}}\n" + "```\n" + "```\n" + "用户: 详细介绍一下这条新闻\n" + "返回: {\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"detail\": true, \"lang\": \"zh_CN\"}}}\n" + "```\n" + "可用的音乐名称:\n" ) return prompt + + def clean_cache(self): + """清理过期缓存""" + now = time.time() + # 找出过期键 + expired_keys = [k for k, v in self.intent_cache.items() if now - v['timestamp'] > self.cache_expiry] + for key in expired_keys: + del self.intent_cache[key] + + # 如果缓存太大,移除最旧的条目 + if len(self.intent_cache) > self.cache_max_size: + # 按时间戳排序并保留最新的条目 + sorted_items = sorted(self.intent_cache.items(), key=lambda x: x[1]['timestamp']) + for key, _ in sorted_items[:len(sorted_items) - self.cache_max_size]: + del self.intent_cache[key] + + def check_pattern_match(self, text): + """检查文本是否匹配常见模式,并提取关键信息""" + # 城市+天气的特殊模式匹配 + city_weather_pattern = re.search(r'([^\s,,。?!]+)天气', text) + if city_weather_pattern: + city = city_weather_pattern.group(1) + # 排除可能的误匹配,如"今天天气"、"明天天气"、"现在天气"等 + if city not in ["今天", "今日", "明天", "现在", "当前", "未来", "明日", "这两天", "近期"]: + logger.bind(tag=TAG).info(f"提取到城市名: {city}") + # 返回包含城市名的function_call + return f'{{\"function_call\": {{\"name\": \"get_weather\", \"arguments\": {{\"location\": \"{city}\", \"lang\": \"zh_CN\"}}}}}}' + + # 普通模式匹配 + for pattern, intent in self.common_patterns.items(): + if pattern in text: + return intent + + return None async def detect_intent(self, conn, dialogue_history: List[Dict], text: str) -> str: if not self.llm: raise ValueError("LLM provider not set") + + # 记录整体开始时间 + total_start_time = time.time() + + # 打印使用的模型信息 + model_info = getattr(self.llm, 'model_name', str(self.llm.__class__.__name__)) + logger.bind(tag=TAG).info(f"使用意图识别模型: {model_info}") + + # 先尝试简单的模式匹配 + pattern_match = self.check_pattern_match(text) + if pattern_match: + pattern_time = time.time() - total_start_time + logger.bind(tag=TAG).info(f"模式匹配成功: {text} -> {pattern_match}, 耗时: {pattern_time:.4f}秒") + return pattern_match + + # 计算缓存键 + cache_key = hashlib.md5(text.encode()).hexdigest() + + # 检查缓存 + if cache_key in self.intent_cache: + cache_entry = self.intent_cache[cache_key] + # 检查缓存是否过期 + if time.time() - cache_entry['timestamp'] <= self.cache_expiry: + cache_time = time.time() - total_start_time + logger.bind(tag=TAG).info(f"使用缓存的意图: {cache_key} -> {cache_entry['intent']}, 耗时: {cache_time:.4f}秒") + return cache_entry['intent'] + + # 清理缓存 + self.clean_cache() # 构建用户最后一句话的提示 msgStr = "" @@ -94,18 +207,78 @@ class IntentProvider(IntentProviderBase): music_file_names = music_config["music_file_names"] prompt_music = f"{self.promot}\n{music_file_names}\n" logger.bind(tag=TAG).debug(f"User prompt: {prompt_music}") + + # 记录预处理完成时间 + preprocess_time = time.time() - total_start_time + logger.bind(tag=TAG).debug(f"意图识别预处理耗时: {preprocess_time:.4f}秒") + # 使用LLM进行意图识别 + llm_start_time = time.time() + logger.bind(tag=TAG).info(f"开始LLM意图识别调用, 模型: {model_info}") + intent = self.llm.response_no_stream( system_prompt=prompt_music, user_prompt=user_prompt ) - # 使用正则表达式提取大括号中的内容 - # 使用正则表达式提取 {} 中的内容 - match = re.search(r'\{.*?\}', intent) + + # 记录LLM调用完成时间 + llm_time = time.time() - llm_start_time + logger.bind(tag=TAG).info(f"LLM意图识别完成, 模型: {model_info}, 调用耗时: {llm_time:.4f}秒") + + # 记录后处理开始时间 + postprocess_start_time = time.time() + + # 清理和解析响应 + intent = intent.strip() + # 尝试提取JSON部分 + match = re.search(r'\{.*\}', intent, re.DOTALL) if match: - result = match.group(0) - intent = result - else: - intent = "{intent: '继续聊天'}" - logger.bind(tag=TAG).info(f"Detected intent: {intent}") - return intent.strip() + intent = match.group(0) + + # 记录总处理时间 + total_time = time.time() - total_start_time + logger.bind(tag=TAG).info(f"【意图识别性能】模型: {model_info}, 总耗时: {total_time:.4f}秒, LLM调用: {llm_time:.4f}秒, 查询: '{text[:20]}...'") + + # 尝试解析为JSON + try: + intent_data = json.loads(intent) + # 如果包含function_call,则格式化为适合处理的格式 + if "function_call" in intent_data: + function_data = intent_data["function_call"] + function_name = function_data.get("name") + function_args = function_data.get("arguments", {}) + + # 记录识别到的function call + logger.bind(tag=TAG).info(f"识别到function call: {function_name}, 参数: {function_args}") + + # 添加到缓存 + self.intent_cache[cache_key] = { + 'intent': intent, + 'timestamp': time.time() + } + + # 后处理时间 + postprocess_time = time.time() - postprocess_start_time + logger.bind(tag=TAG).debug(f"意图后处理耗时: {postprocess_time:.4f}秒") + + # 确保返回完全序列化的JSON字符串 + return intent + else: + # 添加到缓存 + self.intent_cache[cache_key] = { + 'intent': intent, + 'timestamp': time.time() + } + + # 后处理时间 + postprocess_time = time.time() - postprocess_start_time + logger.bind(tag=TAG).debug(f"意图后处理耗时: {postprocess_time:.4f}秒") + + # 返回普通意图 + return intent + except json.JSONDecodeError: + # 后处理时间 + postprocess_time = time.time() - postprocess_start_time + logger.bind(tag=TAG).error(f"无法解析意图JSON: {intent}, 后处理耗时: {postprocess_time:.4f}秒") + # 如果解析失败,默认返回继续聊天意图 + return "{\"intent\": \"继续聊天\"}" From 63a3c33df93d09b252fce6cf53e37781b14d23c4 Mon Sep 17 00:00:00 2001 From: koalalgx <82762701+koalalgx@users.noreply.github.com> Date: Tue, 25 Mar 2025 14:27:10 +0800 Subject: [PATCH 13/28] Update base.py --- main/xiaozhi-server/core/providers/intent/base.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/main/xiaozhi-server/core/providers/intent/base.py b/main/xiaozhi-server/core/providers/intent/base.py index a691d6cb..877dfced 100644 --- a/main/xiaozhi-server/core/providers/intent/base.py +++ b/main/xiaozhi-server/core/providers/intent/base.py @@ -12,12 +12,22 @@ class IntentProviderBase(ABC): self.intent_options = config.get("intent_options", { "continue_chat": "继续聊天", "end_chat": "结束聊天", - "play_music": "播放音乐" + "play_music": "播放音乐", + "get_weather": "查询天气", + "get_news": "查询新闻" }) def set_llm(self, llm): self.llm = llm - logger.bind(tag=TAG).debug("Set LLM for intent provider") + # 获取模型名称和类型信息 + model_name = getattr(llm, 'model_name', str(llm.__class__.__name__)) + model_type = getattr(llm, 'type', 'unknown') + # 记录更详细的日志 + logger.bind(tag=TAG).info(f"意图识别设置LLM: {model_name}, 类型: {model_type}") + # 尝试获取模型基础URL + base_url = getattr(llm, 'base_url', 'N/A') + if base_url != 'N/A': + logger.bind(tag=TAG).debug(f"意图识别LLM基础URL: {base_url}") @abstractmethod async def detect_intent(self, conn, dialogue_history: List[Dict], text: str) -> str: @@ -30,5 +40,6 @@ class IntentProviderBase(ABC): - "继续聊天" - "结束聊天" - "播放音乐 歌名" 或 "随机播放音乐" + - "查询天气 地点名" 或 "查询天气 [当前位置]" """ pass From 3a3ddbc2bc691ce706e6a9034ca634cb6f8af13e Mon Sep 17 00:00:00 2001 From: koalalgx <82762701+koalalgx@users.noreply.github.com> Date: Tue, 25 Mar 2025 14:27:48 +0800 Subject: [PATCH 14/28] Update connection.py --- main/xiaozhi-server/core/connection.py | 207 ++++++++++++++++++++++--- 1 file changed, 185 insertions(+), 22 deletions(-) diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index c9a52b12..3391e126 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -196,7 +196,36 @@ class ConnectionHandler: """加载记忆""" device_id = self.headers.get("device-id", None) self.memory.init_memory(device_id, self.llm) - self.intent.set_llm(self.llm) + + """为意图识别设置LLM,优先使用专用LLM""" + # 检查是否配置了专用的意图识别LLM + intent_llm_name = self.config.get("IntentLLM", "") + + # 记录开始初始化意图识别LLM的时间 + intent_llm_init_start = time.time() + + if intent_llm_name and intent_llm_name in self.config["LLM"]: + # 如果配置了专用LLM,则创建独立的LLM实例 + from core.utils import llm as llm_utils + intent_llm_config = self.config["LLM"][intent_llm_name] + intent_llm_type = intent_llm_config.get("type", intent_llm_name) + intent_llm = llm_utils.create_instance(intent_llm_type, intent_llm_config) + self.logger.bind(tag=TAG).info(f"为意图识别创建了专用LLM: {intent_llm_name}, 类型: {intent_llm_type}") + + # 记录额外的模型信息 + model_name = intent_llm_config.get("model_name", "未指定") + base_url = intent_llm_config.get("base_url", "未指定") + self.logger.bind(tag=TAG).info(f"意图识别LLM详细信息 - 模型名称: {model_name}, 服务地址: {base_url}") + + self.intent.set_llm(intent_llm) + else: + # 否则使用主LLM + self.intent.set_llm(self.llm) + self.logger.bind(tag=TAG).info("意图识别使用主LLM") + + # 记录意图识别LLM初始化耗时 + intent_llm_init_time = time.time() - intent_llm_init_start + self.logger.bind(tag=TAG).info(f"意图识别LLM初始化完成,耗时: {intent_llm_init_time:.4f}秒") """加载位置信息""" self.client_ip_info = get_ip_info(self.client_ip) @@ -310,7 +339,7 @@ class ConnectionHandler: self.logger.bind(tag=TAG).debug(json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False)) return True - def chat_with_function_calling(self, query, tool_call=False): + def chat_with_function_calling(self, query, tool_call=False, is_weather_query=False, is_news_query=False): self.logger.bind(tag=TAG).debug(f"Chat with function calling start: {query}") """Chat with function calling for intent detection using streaming""" if self.isNeedAuth(): @@ -326,7 +355,7 @@ class ConnectionHandler: functions = self.func_handler.get_functions() response_message = [] - processed_chars = 0 # 跟踪已处理的字符位置 + processed_chars = 0 try: start_time = time.time() @@ -335,14 +364,58 @@ class ConnectionHandler: future = asyncio.run_coroutine_threadsafe(self.memory.query_memory(query), self.loop) memory_str = future.result() - # self.logger.bind(tag=TAG).info(f"对话记录: {self.dialogue.get_llm_dialogue_with_memory(memory_str)}") - - # 使用支持functions的streaming接口 - llm_responses = self.llm.response_with_functions( - self.session_id, - self.dialogue.get_llm_dialogue_with_memory(memory_str), - functions=functions - ) + # 为天气查询添加特殊处理 + if is_weather_query: + self.logger.bind(tag=TAG).info(f"检测到天气查询,添加特殊指令") + # 获取对话历史 + dialogue_with_memory = self.dialogue.get_llm_dialogue_with_memory(memory_str) + + # 找到最后一条tool消息(可能是天气数据) + for i in range(len(dialogue_with_memory) - 1, -1, -1): + if dialogue_with_memory[i].get("role") == "tool" and "当前天气" in dialogue_with_memory[i].get("content", ""): + # 添加特殊指令 + dialogue_with_memory.append({ + "role": "system", + "content": "请根据上面的天气数据,以简洁友好的方式回答用户的天气查询。直接告诉用户当前天气状况、温度以及可能需要的建议,不要提及数据来源或解释你是如何获取这些信息的。" + }) + self.logger.bind(tag=TAG).info(f"已添加天气查询特殊指令") + break + + # 使用支持functions的streaming接口并传入修改后的对话历史 + llm_responses = self.llm.response_with_functions( + self.session_id, + dialogue_with_memory, + functions=functions + ) + # 为新闻查询添加特殊处理 + elif is_news_query: + self.logger.bind(tag=TAG).info(f"检测到新闻查询,添加特殊指令") + # 获取对话历史 + dialogue_with_memory = self.dialogue.get_llm_dialogue_with_memory(memory_str) + + # 找到最后一条tool消息(可能是新闻数据) + for i in range(len(dialogue_with_memory) - 1, -1, -1): + if dialogue_with_memory[i].get("role") == "tool" and "新闻" in dialogue_with_memory[i].get("content", ""): + # 添加特殊指令 + dialogue_with_memory.append({ + "role": "system", + "content": "请根据上面的新闻数据,以简洁友好的方式回答用户的新闻查询。直接告诉用户新闻内容,不要提及数据来源或解释你是如何获取这些信息的。保持新闻播报的语气和风格。" + }) + self.logger.bind(tag=TAG).info(f"已添加新闻查询特殊指令") + break + + # 使用支持functions的streaming接口并传入修改后的对话历史 + llm_responses = self.llm.response_with_functions( + self.session_id, + dialogue_with_memory, + functions=functions + ) + else: + llm_responses = self.llm.response_with_functions( + self.session_id, + self.dialogue.get_llm_dialogue_with_memory(memory_str), + functions=functions + ) except Exception as e: self.logger.bind(tag=TAG).error(f"LLM 处理出错 {query}: {e}") return None @@ -460,36 +533,126 @@ class ConnectionHandler: return True def _handle_function_result(self, result, function_call_data, text_index): + self.logger.bind(tag=TAG).info(f"处理函数调用结果,动作类型: {result.action.name if result.action else 'None'}") + + # 检查是否有备用直接回复 + direct_response = getattr(result, 'response', None) + if result.action == Action.RESPONSE: # 直接回复前端 text = result.response + self.logger.bind(tag=TAG).info(f"函数返回直接回复: {text[:100] if text else 'None'}...") self.recode_first_last_text(text, text_index) future = self.executor.submit(self.speak_and_play, text, text_index) self.tts_queue.put(future) self.dialogue.put(Message(role="assistant", content=text)) elif result.action == Action.REQLLM: # 调用函数后再请求llm生成回复 - + self.logger.bind(tag=TAG).info(f"处理REQLLM动作,需要进一步处理结果") + text = result.result + function_id = function_call_data["id"] + function_name = function_call_data["name"] + function_arguments = function_call_data["arguments"] + if text is not None and len(text) > 0: - function_id = function_call_data["id"] - function_name = function_call_data["name"] - function_arguments = function_call_data["arguments"] - self.dialogue.put(Message(role='assistant', - tool_calls=[{"id": function_id, - "function": {"arguments": function_arguments, + self.logger.bind(tag=TAG).info(f"函数返回结果长度: {len(text)}, 前100字符: {text[:100]}...") + + # 特殊处理天气查询 + if function_name == "get_weather": + self.logger.bind(tag=TAG).info(f"检测到天气查询结果,使用特殊处理") + + # 记录工具调用到对话历史 + self.dialogue.put(Message(role='assistant', + tool_calls=[{"id": function_id, + "function": {"arguments": function_arguments, "name": function_name}, - "type": 'function', - "index": 0}])) + "type": 'function', + "index": 0}])) + + # 记录工具返回结果到对话历史 + self.dialogue.put(Message(role="tool", tool_call_id=function_id, content=text)) + + try: + # 使用天气数据生成回复 + self.chat_with_function_calling(text, tool_call=True, is_weather_query=True) + except Exception as e: + self.logger.bind(tag=TAG).error(f"处理天气查询数据失败: {e}") + if direct_response: + self.logger.bind(tag=TAG).info(f"使用备用直接回复: {direct_response}") + self.recode_first_last_text(direct_response, text_index) + future = self.executor.submit(self.speak_and_play, direct_response, text_index) + self.tts_queue.put(future) + self.dialogue.put(Message(role="assistant", content=direct_response)) + # 特殊处理新闻查询 + elif function_name == "get_news": + self.logger.bind(tag=TAG).info(f"检测到新闻查询结果,使用特殊处理") + + # 记录工具调用到对话历史 + self.dialogue.put(Message(role='assistant', + tool_calls=[{"id": function_id, + "function": {"arguments": function_arguments, + "name": function_name}, + "type": 'function', + "index": 0}])) + + # 记录工具返回结果到对话历史 + self.dialogue.put(Message(role="tool", tool_call_id=function_id, content=text)) + + try: + # 使用新闻数据生成回复,设置is_news_query=True + self.chat_with_function_calling(text, tool_call=True, is_news_query=True) + except Exception as e: + self.logger.bind(tag=TAG).error(f"处理新闻查询数据失败: {e}") + if direct_response: + self.logger.bind(tag=TAG).info(f"使用备用直接回复: {direct_response}") + self.recode_first_last_text(direct_response, text_index) + future = self.executor.submit(self.speak_and_play, direct_response, text_index) + self.tts_queue.put(future) + self.dialogue.put(Message(role="assistant", content=direct_response)) + else: + # 其他类型的函数调用 + self.dialogue.put(Message(role='assistant', + tool_calls=[{"id": function_id, + "function": {"arguments": function_arguments, + "name": function_name}, + "type": 'function', + "index": 0}])) - self.dialogue.put(Message(role="tool", tool_call_id=function_id, content=text)) - self.chat_with_function_calling(text, tool_call=True) + self.dialogue.put(Message(role="tool", tool_call_id=function_id, content=text)) + + try: + self.chat_with_function_calling(text, tool_call=True) + except Exception as e: + self.logger.bind(tag=TAG).error(f"处理函数调用结果失败: {e}") + if direct_response: + self.logger.bind(tag=TAG).info(f"使用备用直接回复: {direct_response}") + self.recode_first_last_text(direct_response, text_index) + future = self.executor.submit(self.speak_and_play, direct_response, text_index) + self.tts_queue.put(future) + self.dialogue.put(Message(role="assistant", content=direct_response)) + else: + self.logger.bind(tag=TAG).warning(f"函数返回结果为空") + if direct_response: + self.logger.bind(tag=TAG).info(f"使用备用直接回复: {direct_response}") + self.recode_first_last_text(direct_response, text_index) + future = self.executor.submit(self.speak_and_play, direct_response, text_index) + self.tts_queue.put(future) + self.dialogue.put(Message(role="assistant", content=direct_response)) + else: + error_text = f"抱歉,我无法获取{function_name}的结果,请稍后再试。" + self.recode_first_last_text(error_text, text_index) + future = self.executor.submit(self.speak_and_play, error_text, text_index) + self.tts_queue.put(future) + self.dialogue.put(Message(role="assistant", content=error_text)) elif result.action == Action.NOTFOUND: text = result.result + self.logger.bind(tag=TAG).info(f"未找到对应函数: {text}") self.recode_first_last_text(text, text_index) future = self.executor.submit(self.speak_and_play, text, text_index) self.tts_queue.put(future) self.dialogue.put(Message(role="assistant", content=text)) else: text = result.result + self.logger.bind(tag=TAG).info(f"其他动作类型,直接返回结果: {text[:100] if text else None}...") self.recode_first_last_text(text, text_index) future = self.executor.submit(self.speak_and_play, text, text_index) self.tts_queue.put(future) From 9cbad1a4488ea8c62f6f168c5bb784d53924ad26 Mon Sep 17 00:00:00 2001 From: koalalgx <82762701+koalalgx@users.noreply.github.com> Date: Tue, 25 Mar 2025 14:28:32 +0800 Subject: [PATCH 15/28] Update get_news.py --- .../plugins_func/functions/get_news.py | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/main/xiaozhi-server/plugins_func/functions/get_news.py b/main/xiaozhi-server/plugins_func/functions/get_news.py index 43f8ea48..f7a6d342 100644 --- a/main/xiaozhi-server/plugins_func/functions/get_news.py +++ b/main/xiaozhi-server/plugins_func/functions/get_news.py @@ -128,13 +128,15 @@ def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_C # 如果detail为True,获取上一条新闻的详细内容 if detail: if not hasattr(conn, 'last_news_link') or not conn.last_news_link or 'link' not in conn.last_news_link: - return ActionResponse(Action.REQLLM, "抱歉,没有找到最近查询的新闻,请先获取一条新闻。", None) + direct_response = "抱歉,没有找到最近查询的新闻,请先获取一条新闻。" + return ActionResponse(Action.REQLLM, "抱歉,没有找到最近查询的新闻,请先获取一条新闻。", direct_response) link = conn.last_news_link.get('link') title = conn.last_news_link.get('title', '未知标题') if link == '#': - return ActionResponse(Action.REQLLM, "抱歉,该新闻没有可用的链接获取详细内容。", None) + direct_response = "抱歉,该新闻没有可用的链接获取详细内容。" + return ActionResponse(Action.REQLLM, "抱歉,该新闻没有可用的链接获取详细内容。", direct_response) logger.bind(tag=TAG).debug(f"获取新闻详情: {title}, URL={link}") @@ -142,7 +144,8 @@ def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_C detail_content = fetch_news_detail(link) if not detail_content or detail_content == "无法获取详细内容": - return ActionResponse(Action.REQLLM, f"抱歉,无法获取《{title}》的详细内容,可能是链接已失效或网站结构发生变化。", None) + direct_response = f"抱歉,无法获取《{title}》的详细内容,可能是链接已失效或网站结构发生变化。" + return ActionResponse(Action.REQLLM, f"抱歉,无法获取《{title}》的详细内容,可能是链接已失效或网站结构发生变化。", direct_response) # 构建详情报告 detail_report = ( @@ -153,7 +156,10 @@ def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_C f"不要提及这是总结,就像是在讲述一个完整的新闻故事)" ) - return ActionResponse(Action.REQLLM, detail_report, None) + # 构造一个直接回复,简要概括新闻内容 + direct_response = f"以下是《{title}》的详细内容:{detail_content[:200]}...(内容过长已省略)" + + return ActionResponse(Action.REQLLM, detail_report, direct_response) # 否则,获取新闻列表并随机选择一条 # 从配置中获取RSS URL @@ -174,7 +180,8 @@ def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_C news_items = fetch_news_from_rss(rss_url) if not news_items: - return ActionResponse(Action.REQLLM, "抱歉,未能获取到新闻信息,请稍后再试。", None) + direct_response = "抱歉,未能获取到新闻信息,请稍后再试。" + return ActionResponse(Action.REQLLM, "抱歉,未能获取到新闻信息,请稍后再试。", direct_response) # 随机选择一条新闻 selected_news = random.choice(news_items) @@ -198,8 +205,16 @@ def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_C f"如果用户询问更多详情,告知用户可以说'请详细介绍这条新闻'获取更多内容)" ) - return ActionResponse(Action.REQLLM, news_report, None) + # 构造一个直接回复版本,简要播报新闻 + direct_response = ( + f"最新{mapped_category or ''}新闻:{selected_news['title']}。" + f"{selected_news['description'][:150]}..." + f"如果您想了解更多详情,可以说'请详细介绍这条新闻'。" + ) + + return ActionResponse(Action.REQLLM, news_report, direct_response) except Exception as e: logger.bind(tag=TAG).error(f"获取新闻出错: {e}") - return ActionResponse(Action.REQLLM, "抱歉,获取新闻时发生错误,请稍后再试。", None) \ No newline at end of file + direct_response = "抱歉,获取新闻时发生错误,请稍后再试。" + return ActionResponse(Action.REQLLM, "抱歉,获取新闻时发生错误,请稍后再试。", direct_response) From 44c3e73577afcc7fcb6163d0d2c8f2a290381e3d Mon Sep 17 00:00:00 2001 From: koalalgx <82762701+koalalgx@users.noreply.github.com> Date: Tue, 25 Mar 2025 14:28:59 +0800 Subject: [PATCH 16/28] Update intentHandler.py --- .../core/handle/intentHandler.py | 284 +++++++++++++++--- 1 file changed, 244 insertions(+), 40 deletions(-) diff --git a/main/xiaozhi-server/core/handle/intentHandler.py b/main/xiaozhi-server/core/handle/intentHandler.py index d8440fc6..900a25cf 100644 --- a/main/xiaozhi-server/core/handle/intentHandler.py +++ b/main/xiaozhi-server/core/handle/intentHandler.py @@ -4,6 +4,9 @@ import uuid from core.handle.sendAudioHandle import send_stt_message from core.handle.helloHandle import checkWakeupWords from core.utils.util import remove_punctuation_and_length +import re +import asyncio +from loguru import logger TAG = __name__ logger = setup_logging() @@ -21,11 +24,11 @@ async def handle_user_intent(conn, text): # 使用支持function calling的聊天方法,不再进行意图分析 return False # 使用LLM进行意图分析 - intent = await analyze_intent_with_llm(conn, text) - if not intent: + intent_result = await analyze_intent_with_llm(conn, text) + if not intent_result: return False # 处理各种意图 - return await process_intent_result(conn, intent, text) + return await process_intent_result(conn, intent_result, text) async def check_direct_exit(conn, text): @@ -40,7 +43,6 @@ async def check_direct_exit(conn, text): return False - async def analyze_intent_with_llm(conn, text): """使用LLM分析用户意图""" if not hasattr(conn, 'intent') or not conn.intent: @@ -51,49 +53,251 @@ async def analyze_intent_with_llm(conn, text): dialogue = conn.dialogue try: intent_result = await conn.intent.detect_intent(conn, dialogue.dialogue, text) - # 尝试解析JSON结果 - try: - intent_data = json.loads(intent_result) - if "intent" in intent_data: - return intent_data["intent"] - except json.JSONDecodeError: - # 如果不是JSON格式,尝试直接获取意图文本 - return intent_result.strip() - + return intent_result except Exception as e: logger.bind(tag=TAG).error(f"意图识别失败: {str(e)}") return None -async def process_intent_result(conn, intent, original_text): +async def process_intent_result(conn, intent_result, original_text): """处理意图识别结果""" - # 处理退出意图 - if "结束聊天" in intent: - logger.bind(tag=TAG).info(f"识别到退出意图: {intent}") - # 如果是明确的离别意图,发送告别语并关闭连接 - await send_stt_message(conn, original_text) - conn.executor.submit(conn.chat_and_close, original_text) - return True + try: + # 尝试将结果解析为JSON + intent_data = json.loads(intent_result) + + # 检查是否有function_call + if "function_call" in intent_data: + # 直接从意图识别获取了function_call + logger.bind(tag=TAG).info(f"检测到function_call格式的意图结果: {intent_data['function_call']['name']}") + + function_name = intent_data["function_call"]["name"] + function_args = intent_data["function_call"]["arguments"] + + # 确保参数是字符串格式的JSON + if isinstance(function_args, dict): + function_args = json.dumps(function_args) + + function_call_data = { + "name": function_name, + "id": str(uuid.uuid4().hex), + "arguments": function_args + } + + # 处理特定类型的函数调用 + if function_name == "get_weather": + logger.bind(tag=TAG).info(f"识别到天气查询意图") + # 先发送消息确认 + await send_stt_message(conn, original_text) + + # 使用executor执行函数调用和结果处理 + def process_weather_query(): + # 直接调用函数 + result = conn.func_handler.handle_llm_function_call(conn, function_call_data) + if result: + # 获取当前最新的文本索引 + text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0 + # 处理函数调用结果 + conn._handle_function_result(result, function_call_data, text_index) + + # 将函数执行放在线程池中 + conn.executor.submit(process_weather_query) + return True + + elif function_name == "play_music": + logger.bind(tag=TAG).info(f"识别到音乐播放意图") + # 先发送消息确认 + await send_stt_message(conn, original_text) + + # 使用executor执行函数调用和结果处理 + def process_music_query(): + # 直接调用函数 + result = conn.func_handler.handle_llm_function_call(conn, function_call_data) + if result: + # 获取当前最新的文本索引 + text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0 + # 处理函数调用结果 + conn._handle_function_result(result, function_call_data, text_index) + + # 将函数执行放在线程池中 + conn.executor.submit(process_music_query) + return True + + elif function_name == "get_news": + logger.bind(tag=TAG).info(f"识别到新闻查询意图") + # 先发送消息确认 + await send_stt_message(conn, original_text) + + # 使用executor执行函数调用和结果处理 + def process_news_query(): + # 直接调用函数 + result = conn.func_handler.handle_llm_function_call(conn, function_call_data) + if result: + # 获取当前最新的文本索引 + text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0 + # 处理函数调用结果 + conn._handle_function_result(result, function_call_data, text_index) + + # 将函数执行放在线程池中 + conn.executor.submit(process_news_query) + return True + + else: + # 其他类型的函数调用,尝试直接执行 + # 先发送消息确认 + await send_stt_message(conn, original_text) + + # 使用executor执行函数调用和结果处理 + def process_function_call(): + result = conn.func_handler.handle_llm_function_call(conn, function_call_data) + if result: + # 获取当前最新的文本索引 + text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0 + # 处理函数调用结果 + conn._handle_function_result(result, function_call_data, text_index) + + # 将函数执行放在线程池中 + conn.executor.submit(process_function_call) + return True + + # 处理传统意图格式 + elif "intent" in intent_data: + intent = intent_data["intent"] + + # 处理退出意图 + if "结束聊天" in intent: + logger.bind(tag=TAG).info(f"识别到退出意图: {intent}") + # 如果是明确的离别意图,发送告别语并关闭连接 + await send_stt_message(conn, original_text) + conn.executor.submit(conn.chat_and_close, original_text) + return True + + # 其他不需要特殊处理的意图,让常规聊天流程处理 + return False + + except json.JSONDecodeError: + # 如果不是有效的JSON,尝试兼容旧格式 + intent = intent_result + + # 处理退出意图 + if "结束聊天" in intent: + logger.bind(tag=TAG).info(f"识别到退出意图: {intent}") + # 如果是明确的离别意图,发送告别语并关闭连接 + await send_stt_message(conn, original_text) + conn.executor.submit(conn.chat_and_close, original_text) + return True - # 处理播放音乐意图 - if "播放音乐" in intent: - logger.bind(tag=TAG).info(f"识别到音乐播放意图: {intent}") - # 调用play_music函数来播放音乐 - song_name = extract_text_in_brackets(intent) - function_id = str(uuid.uuid4().hex) - function_name = "play_music" - function_arguments = '{ "song_name": "' + song_name + '" }' - - function_call_data = { - "name": function_name, - "id": function_id, - "arguments": function_arguments - } - conn.func_handler.handle_llm_function_call(conn, function_call_data) - return True - - # 其他意图处理可以在这里扩展 + # 处理播放音乐意图 + if "播放音乐" in intent: + logger.bind(tag=TAG).info(f"识别到音乐播放意图: {intent}") + # 获取歌曲名称 + song_name = extract_text_in_brackets(intent) + + # 先发送消息确认 + await send_stt_message(conn, original_text) + + # 构造合适的音乐播放函数调用 + function_id = str(uuid.uuid4().hex) + function_name = "play_music" + function_arguments = '{ "song_name": ' + (f'"{song_name}"' if song_name else '"random"') + ' }' + + function_call_data = { + "name": function_name, + "id": function_id, + "arguments": function_arguments + } + + # 使用executor执行函数调用和结果处理 + def process_music_query(): + # 直接调用函数 + result = conn.func_handler.handle_llm_function_call(conn, function_call_data) + if result: + # 获取当前最新的文本索引 + text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0 + # 处理函数调用结果 + conn._handle_function_result(result, function_call_data, text_index) + + # 将函数执行放在线程池中 + conn.executor.submit(process_music_query) + return True + + # 处理查询天气意图 + if "查询天气" in intent: + logger.bind(tag=TAG).info(f"识别到天气查询意图: {intent}") + # 获取地点 + location = extract_text_in_brackets(intent) + + # 先发送消息确认 + await send_stt_message(conn, original_text) + + # 构造合适的天气查询函数调用 + function_id = str(uuid.uuid4().hex) + function_name = "get_weather" + function_arguments = '{ "location": ' + (f'"{location}"' if location and location != "当前位置" else 'null') + ', "lang": "zh_CN" }' + + function_call_data = { + "name": function_name, + "id": function_id, + "arguments": function_arguments + } + + # 使用executor执行函数调用和结果处理 + def process_weather_query(): + # 直接调用函数 + result = conn.func_handler.handle_llm_function_call(conn, function_call_data) + if result: + # 获取当前最新的文本索引 + text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0 + # 处理函数调用结果 + conn._handle_function_result(result, function_call_data, text_index) + + # 将函数执行放在线程池中 + conn.executor.submit(process_weather_query) + return True + + # 处理查询新闻意图 + if "查询新闻" in intent or "播报新闻" in intent or "看新闻" in intent: + logger.bind(tag=TAG).info(f"识别到新闻查询意图: {intent}") + # 获取新闻类别 + category = extract_text_in_brackets(intent) + + # 先发送消息确认 + await send_stt_message(conn, original_text) + + # 构造合适的新闻查询函数调用 + function_id = str(uuid.uuid4().hex) + function_name = "get_news" + + # 判断是否是查询详情 + detail = "详情" in intent or "详细" in intent + + # 构造参数JSON字符串 + if detail: + function_arguments = '{ "detail": true, "lang": "zh_CN" }' + else: + function_arguments = '{ "category": ' + (f'"{category}"' if category else 'null') + ', "detail": false, "lang": "zh_CN" }' + + function_call_data = { + "name": function_name, + "id": function_id, + "arguments": function_arguments + } + + # 使用executor执行函数调用和结果处理 + def process_news_query(): + # 直接调用函数 + result = conn.func_handler.handle_llm_function_call(conn, function_call_data) + if result: + # 获取当前最新的文本索引 + text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0 + # 处理函数调用结果 + conn._handle_function_result(result, function_call_data, text_index) + + # 将函数执行放在线程池中 + conn.executor.submit(process_news_query) + return True + except Exception as e: + logger.bind(tag=TAG).error(f"处理意图结果时出错: {e}") # 默认返回False,表示继续常规聊天流程 return False @@ -112,4 +316,4 @@ def extract_text_in_brackets(s): if left_bracket_index != -1 and right_bracket_index != -1 and left_bracket_index < right_bracket_index: return s[left_bracket_index + 1:right_bracket_index] else: - return "" \ No newline at end of file + return "" From fbd25353d5e36a1372c6398f8cddf20cf155f5dd Mon Sep 17 00:00:00 2001 From: koalalgx <82762701+koalalgx@users.noreply.github.com> Date: Tue, 25 Mar 2025 14:29:28 +0800 Subject: [PATCH 17/28] Update get_weather.py --- .../plugins_func/functions/get_weather.py | 198 ++++++++++++------ 1 file changed, 136 insertions(+), 62 deletions(-) diff --git a/main/xiaozhi-server/plugins_func/functions/get_weather.py b/main/xiaozhi-server/plugins_func/functions/get_weather.py index 8cdd6ea1..58f63844 100644 --- a/main/xiaozhi-server/plugins_func/functions/get_weather.py +++ b/main/xiaozhi-server/plugins_func/functions/get_weather.py @@ -2,6 +2,7 @@ import requests from bs4 import BeautifulSoup from config.logger import setup_logging from plugins_func.register import register_function, ToolType, ActionResponse, Action +import json TAG = __name__ logger = setup_logging() @@ -39,85 +40,158 @@ HEADERS = { ) } -# 天气代码 https://dev.qweather.com/docs/resource/icons/#weather-icons -WEATHER_CODE_MAP = { - "100": "晴", "101": "多云", "102": "少云", "103": "晴间多云", "104": "阴", - "150": "晴", "151": "多云", "152": "少云", "153": "晴间多云", - "300": "阵雨", "301": "强阵雨", "302": "雷阵雨", "303": "强雷阵雨", "304": "雷阵雨伴有冰雹", - "305": "小雨", "306": "中雨", "307": "大雨", "308": "极端降雨", "309": "毛毛雨/细雨", - "310": "暴雨", "311": "大暴雨", "312": "特大暴雨", "313": "冻雨", "314": "小到中雨", - "315": "中到大雨", "316": "大到暴雨", "317": "暴雨到大暴雨", "318": "大暴雨到特大暴雨", - "350": "阵雨", "351": "强阵雨", "399": "雨", - "400": "小雪", "401": "中雪", "402": "大雪", "403": "暴雪", "404": "雨夹雪", - "405": "雨雪天气", "406": "阵雨夹雪", "407": "阵雪", "408": "小到中雪", "409": "中到大雪", "410": "大到暴雪", - "456": "阵雨夹雪", "457": "阵雪", "499": "雪", - "500": "薄雾", "501": "雾", "502": "霾", "503": "扬沙", "504": "浮尘", - "507": "沙尘暴", "508": "强沙尘暴", - "509": "浓雾", "510": "强浓雾", "511": "中度霾", "512": "重度霾", "513": "严重霾", "514": "大雾", "515": "特强浓雾", - "900": "热", "901": "冷", "999": "未知" -} def fetch_city_info(location, api_key): url = f"https://geoapi.qweather.com/v2/city/lookup?key={api_key}&location={location}&lang=zh" - response = requests.get(url, headers=HEADERS).json() - return response.get('location', [])[0] if response.get('location') else None + logger.bind(tag=TAG).info(f"正在请求城市信息API,URL: {url}") + try: + response = requests.get(url, headers=HEADERS) + logger.bind(tag=TAG).info(f"城市信息API响应状态码: {response.status_code}") + json_data = response.json() + logger.bind(tag=TAG).debug(f"城市信息API响应: {json.dumps(json_data, ensure_ascii=False)}") + return json_data.get('location', [])[0] if json_data.get('location') else None + except Exception as e: + logger.bind(tag=TAG).error(f"获取城市信息失败: {str(e)}") + return None def fetch_weather_page(url): - response = requests.get(url, headers=HEADERS) - return BeautifulSoup(response.text, "html.parser") if response.ok else None + logger.bind(tag=TAG).info(f"正在请求天气页面,URL: {url}") + try: + response = requests.get(url, headers=HEADERS) + logger.bind(tag=TAG).info(f"天气页面响应状态码: {response.status_code}") + if not response.ok: + logger.bind(tag=TAG).error(f"获取天气页面失败: {response.text[:200]}") + return BeautifulSoup(response.text, "html.parser") if response.ok else None + except Exception as e: + logger.bind(tag=TAG).error(f"获取天气页面失败: {str(e)}") + return None def parse_weather_info(soup): - city_name = soup.select_one("h1.c-submenu__location").get_text(strip=True) + try: + city_name = soup.select_one("h1.c-submenu__location").get_text(strip=True) + logger.bind(tag=TAG).info(f"解析到城市名称: {city_name}") - current_abstract = soup.select_one(".c-city-weather-current .current-abstract") - current_abstract = current_abstract.get_text(strip=True) if current_abstract else "未知" + current_abstract = soup.select_one(".c-city-weather-current .current-abstract") + current_abstract = current_abstract.get_text(strip=True) if current_abstract else "未知" + logger.bind(tag=TAG).info(f"当前天气概况: {current_abstract}") - current_basic = {} - for item in soup.select(".c-city-weather-current .current-basic .current-basic___item"): - parts = item.get_text(strip=True, separator=" ").split(" ") - if len(parts) == 2: - key, value = parts[1], parts[0] - current_basic[key] = value + current_basic = {} + for item in soup.select(".c-city-weather-current .current-basic .current-basic___item"): + parts = item.get_text(strip=True, separator=" ").split(" ") + if len(parts) == 2: + key, value = parts[1], parts[0] + current_basic[key] = value + logger.bind(tag=TAG).info(f"当前天气详情: {json.dumps(current_basic, ensure_ascii=False)}") - temps_list = [] - for row in soup.select(".city-forecast-tabs__row")[:7]: # 取前7天的数据 - date = row.select_one(".date-bg .date").get_text(strip=True) - weather_code = row.select_one(".date-bg .icon")["src"].split("/")[-1].split(".")[0] - weather = WEATHER_CODE_MAP.get(weather_code, "未知") - temps = [span.get_text(strip=True) for span in row.select(".tmp-cont .temp")] - high_temp, low_temp = (temps[0], temps[-1]) if len(temps) >= 2 else (None, None) - temps_list.append((date, weather, high_temp, low_temp)) + temps_list = [] + for row in soup.select(".city-forecast-tabs__row")[:7]: # 取前7天的数据 + date = row.select_one(".date-bg .date").get_text(strip=True) + temps = [span.get_text(strip=True) for span in row.select(".tmp-cont .temp")] + high_temp, low_temp = (temps[0], temps[-1]) if len(temps) >= 2 else (None, None) + temps_list.append((date, high_temp, low_temp)) + logger.bind(tag=TAG).info(f"获取到未来7天温度: {temps_list}") - return city_name, current_abstract, current_basic, temps_list + return city_name, current_abstract, current_basic, temps_list + except Exception as e: + logger.bind(tag=TAG).error(f"解析天气信息失败: {str(e)}") + return "未知城市", "未知天气", {}, [] @register_function('get_weather', GET_WEATHER_FUNCTION_DESC, ToolType.SYSTEM_CTL) def get_weather(conn, location: str = None, lang: str = "zh_CN"): - api_key = conn.config["plugins"]["get_weather"]["api_key"] - default_location = conn.config["plugins"]["get_weather"]["default_location"] - location = location or conn.client_ip_info.get("city") or default_location - logger.bind(tag=TAG).debug(f"获取天气: {location}") + logger.bind(tag=TAG).info(f"===== 天气查询函数开始执行 =====") + logger.bind(tag=TAG).info(f"查询参数: location={location}, lang={lang}") + + try: + api_key = conn.config["plugins"]["get_weather"]["api_key"] + default_location = conn.config["plugins"]["get_weather"]["default_location"] + + # 位置参数处理逻辑优化 + # 1. 如果传入了明确的位置参数且不为None/空字符串,则使用该参数 + # 2. 否则,尝试使用客户端IP地址对应的城市信息 + # 3. 如果上述都无效,则使用配置文件中的默认位置 + if location and location.strip(): + final_location = location.strip() + logger.bind(tag=TAG).info(f"使用用户明确指定的位置: {final_location}") + elif conn.client_ip_info and "city" in conn.client_ip_info and conn.client_ip_info["city"]: + final_location = conn.client_ip_info["city"] + logger.bind(tag=TAG).info(f"使用IP地址解析的位置: {final_location}") + else: + final_location = default_location + logger.bind(tag=TAG).info(f"使用配置文件中的默认位置: {final_location}") + + logger.bind(tag=TAG).info(f"最终使用的查询地点: {final_location}, API Key: {api_key}") - city_info = fetch_city_info(location, api_key) - if not city_info: - return ActionResponse(Action.REQLLM, f"未找到相关的城市: {location},请确认地点是否正确", None) + city_info = fetch_city_info(final_location, api_key) + if not city_info: + error_msg = f"未找到相关的城市: {final_location},请确认地点是否正确" + logger.bind(tag=TAG).error(error_msg) + # 构造一个直接回复 + direct_response = f"抱歉,我未能找到\"{final_location}\"的天气信息,请确认地点名称是否正确,或者尝试查询其他城市的天气。" + return ActionResponse(Action.RESPONSE, None, direct_response) # 使用RESPONSE动作直接返回 - soup = fetch_weather_page(city_info['fxLink']) - if not soup: - return ActionResponse(Action.REQLLM, None, "请求失败") + logger.bind(tag=TAG).info(f"获取到城市信息: {json.dumps(city_info, ensure_ascii=False)}") + logger.bind(tag=TAG).info(f"天气查询链接: {city_info['fxLink']}") + + soup = fetch_weather_page(city_info['fxLink']) + if not soup: + error_msg = "请求天气页面失败" + logger.bind(tag=TAG).error(error_msg) + direct_response = f"抱歉,在获取{final_location}的天气信息时遇到了问题,请稍后再试。" + return ActionResponse(Action.RESPONSE, None, direct_response) # 使用RESPONSE动作直接返回 - city_name, current_abstract, current_basic, temps_list = parse_weather_info(soup) - weather_report = f"根据下列数据,用{lang}回应用户的查询天气请求:\n{city_name}未来7天天气:\n" - for i, (date, weather, high, low) in enumerate(temps_list): - if high and low: - weather_report += f"{date}: {low}到{high}, {weather}\n" - weather_report += ( - f"当前天气: {current_abstract}\n" - f"当前天气参数: {current_basic}\n" - f"(确保只报告指定单日的天气情况,除非未来会出现异常天气;或者用户明确要求想要了解多日天气,如果未指定,默认报告今天的天气。" - "参数为0的值不需要报告给用户,每次都报告体感温度,根据语境选择合适的参数内容告知用户,并对参数给出相应评价)" - ) - - return ActionResponse(Action.REQLLM, weather_report, None) + city_name, current_abstract, current_basic, temps_list = parse_weather_info(soup) + + # 构建天气报告(给LLM使用的详细数据) + weather_report = f"根据下列数据,用{lang}回应用户的查询天气请求:\n{city_name}未来7天天气:\n" + for i, (date, high, low) in enumerate(temps_list): + if high and low: + weather_report += f"{date}: {low}到{high}\n" + weather_report += ( + f"当前天气: {current_abstract}\n" + f"当前天气参数: {current_basic}\n" + f"(确保只报告指定单日的气温范围,除非用户明确要求想要了解多日天气,如果未指定,默认报告今天的温度范围。" + "参数为0的值不需要报告给用户,每次都报告体感温度,根据语境选择合适的参数内容告知用户,并对参数给出相应评价)" + ) + + # 同时构建一个直接可用的人性化天气回复 + # 这用作备用,防止LLM处理失败时能够直接回复用户 + today_temp = "" + for date, high, low in temps_list: + if "今天" in date or "今日" in date: + today_temp = f"{low}到{high}" + break + if not today_temp and temps_list: + today_temp = f"{temps_list[0][2]}到{temps_list[0][1]}" + + feel_temp = current_basic.get("体感温度", "") + humidity = current_basic.get("相对湿度", "") + + direct_response = f"{city_name}今天{current_abstract},温度{today_temp}。" + if feel_temp: + direct_response += f"体感温度{feel_temp}。" + if humidity: + direct_response += f"相对湿度{humidity}。" + + # 添加简单的建议 + if "雨" in current_abstract: + direct_response += "外出请记得带伞。" + elif temps_list and temps_list[0][1] and int(temps_list[0][1].replace("°", "")) > 30: + direct_response += "天气炎热,注意防暑。" + elif temps_list and temps_list[0][2] and int(temps_list[0][2].replace("°", "")) < 10: + direct_response += "天气较冷,注意保暖。" + + logger.bind(tag=TAG).info(f"构建的天气报告: {weather_report}") + logger.bind(tag=TAG).info(f"备用直接回复: {direct_response}") + logger.bind(tag=TAG).info(f"===== 天气查询函数执行完毕,返回结果 =====") + + # 返回详细天气报告,但添加direct_response作为备用 + return ActionResponse(Action.REQLLM, weather_report, direct_response) + except Exception as e: + error_msg = f"查询天气时发生错误: {str(e)}" + logger.bind(tag=TAG).error(error_msg) + # 出错时也提供一个直接回复 + direct_response = f"抱歉,在获取天气信息时遇到了技术问题,请稍后再试。" + return ActionResponse(Action.RESPONSE, None, direct_response) # 使用RESPONSE动作直接返回 From 5bfc3efc9cf673150758b93f2b0c0210aa2efa74 Mon Sep 17 00:00:00 2001 From: CGD <3030332422@qq.com> Date: Thu, 27 Mar 2025 11:24:49 +0800 Subject: [PATCH 18/28] =?UTF-8?q?=E5=AE=8C=E6=88=90=E2=80=9C=E8=A7=A3?= =?UTF-8?q?=E7=BB=91=E8=AE=BE=E5=A4=87=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/manager-web/src/apis/module/user.js | 32 +++++++++++++--- .../src/views/DeviceManagement.vue | 37 +++++++++++++++---- 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/main/manager-web/src/apis/module/user.js b/main/manager-web/src/apis/module/user.js index 88a77623..83f5457f 100755 --- a/main/manager-web/src/apis/module/user.js +++ b/main/manager-web/src/apis/module/user.js @@ -5,7 +5,8 @@ import {getServiceUrl} from '../api' export default { // 登录 login(loginForm, callback) { - RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/login`) + RequestService.sendRequest() + .url(`${getServiceUrl()}/api/v1/user/login`) .method('POST') .data(loginForm) .success((res) => { @@ -40,7 +41,9 @@ export default { }, // 注册账号 register(registerForm, callback) { - RequestService.sendRequest().url(`${getServiceUrl()}/api/v1/user/register`).method('POST') + RequestService.sendRequest() + .url(`${getServiceUrl()}/api/v1/user/register`) + .method('POST') .data(registerForm) .success((res) => { RequestService.clearRequestTime() @@ -132,11 +135,11 @@ export default { // 修改用户密码 changePassword(oldPassword, newPassword, successCallback, errorCallback) { RequestService.sendRequest() - .url(`${getServiceUrl()}/api/v1/user/change-password`) // 修改URL - .method('PUT') // 修改方法为PUT + .url(`${getServiceUrl()}/api/v1/user/change-password`) + .method('PUT') .data({ - old_password: oldPassword, // 修改参数名 - new_password: newPassword // 修改参数名 + old_password: oldPassword, + new_password: newPassword, }) .success((res) => { RequestService.clearRequestTime(); @@ -197,4 +200,21 @@ export default { }); }).send(); }, + // 解绑设备 + unbindDevice(device_id, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/api/v1/user/device/unbind/${device_id}`) + .method('PUT') + .success((res) => { + RequestService.clearRequestTime(); + callback(res); + }) + .fail((err) => { + console.error('解绑设备失败:', err); + RequestService.reAjaxFun(() => { + this.unbindDevice(device_id, callback); + }); + }) + .send(); + } } diff --git a/main/manager-web/src/views/DeviceManagement.vue b/main/manager-web/src/views/DeviceManagement.vue index 9135a3e6..b6741d7e 100644 --- a/main/manager-web/src/views/DeviceManagement.vue +++ b/main/manager-web/src/views/DeviceManagement.vue @@ -31,7 +31,7 @@ @@ -59,7 +59,7 @@ \ No newline at end of file From 00a0b2679741a520c4c60392c238015730012c18 Mon Sep 17 00:00:00 2001 From: CGD <3030332422@qq.com> Date: Fri, 28 Mar 2025 11:20:08 +0800 Subject: [PATCH 23/28] =?UTF-8?q?=E5=AE=8C=E6=88=90=E2=80=9C=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E6=99=BA=E8=83=BD=E4=BD=93=E6=A8=A1=E6=9D=BF=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/manager-web/src/apis/module/user.js | 26 ++- .../src/components/AddDeviceDialog.vue | 3 - main/manager-web/src/views/roleConfig.vue | 180 +++++++++++------- 3 files changed, 128 insertions(+), 81 deletions(-) diff --git a/main/manager-web/src/apis/module/user.js b/main/manager-web/src/apis/module/user.js index 66fefdbc..1fb3503d 100755 --- a/main/manager-web/src/apis/module/user.js +++ b/main/manager-web/src/apis/module/user.js @@ -186,7 +186,6 @@ export default { }, // 已绑设备 getAgentBindDevices(agentId, callback) { - console.log("77777777777777777777777") RequestService.sendRequest() .url(`${getServiceUrl()}/api/v1/user/agent/device/bind/${agentId}`) .method('GET') @@ -219,7 +218,6 @@ export default { }, // 绑定设备 bindDevice(agentId, code, callback) { - console.log("32323234343434344340000") RequestService.sendRequest() .url(`${getServiceUrl()}/api/v1/user/agent/device/bind/${agentId}`) .method('POST') @@ -229,10 +227,26 @@ export default { callback(res); }) .fail((err) => { - // console.error('绑定设备失败:', err); - // RequestService.reAjaxFun(() => { - // this.bindDevice(agentId, code, callback); - // }); + console.error('绑定设备失败:', err); + RequestService.reAjaxFun(() => { + this.bindDevice(agentId, code, callback); + }); }).send(); }, + // 新增方法:获取智能体模板 + getAgentTemplate(templateName, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/api/v1/user/agent/templateId`) + .method('GET') + .success((res) => { + RequestService.clearRequestTime(); + callback(res); + }) + .fail((err) => { + console.error('获取模板失败:', err); + RequestService.reAjaxFun(() => { + this.getAgentTemplate(templateName, callback); + }); + }).send(); + }, } diff --git a/main/manager-web/src/components/AddDeviceDialog.vue b/main/manager-web/src/components/AddDeviceDialog.vue index 6c99a038..129ed156 100644 --- a/main/manager-web/src/components/AddDeviceDialog.vue +++ b/main/manager-web/src/components/AddDeviceDialog.vue @@ -44,9 +44,6 @@ export default { }, methods: { confirm() { - // console.log(this.agentId) - // console.log("=========") - // console.log(this.$route.query.agentId) if (!/^\d{6}$/.test(this.deviceCode)) { this.$message.error('请输入6位数字验证码'); return; diff --git a/main/manager-web/src/views/roleConfig.vue b/main/manager-web/src/views/roleConfig.vue index fb37fb16..fb0f0a81 100644 --- a/main/manager-web/src/views/roleConfig.vue +++ b/main/manager-web/src/views/roleConfig.vue @@ -9,7 +9,7 @@ style="width: 37px;height: 37px;background: #5778ff;border-radius: 50%;display: flex;align-items: center;justify-content: center;"> - {{ deviceMac }} + {{ form.agentName }}
@@ -21,7 +21,7 @@
-
+
{{ template }}
@@ -103,65 +103,65 @@ export default { components: {HeaderBar}, data() { return { - deviceMac: 'CC:ba:97:11:a6:ac', form: { - agentCode:"", + agentCode: "", agentName: "", ttsVoiceId: "", systemPrompt: "", - langCode:"", - language:"", - sort:"", + langCode: "", + language: "", + sort: "", model: { ttsModelId: "", vadModelId: "", - asrModelId:"", + asrModelId: "", llmModelId: "", memModelId: "", intentModelId: "", } }, options: [ - { value: '选项1', label: '黄金糕' }, - { value: '选项2', label: '双皮奶' } + {value: '选项1', label: '黄金糕'}, + {value: '选项2', label: '双皮奶'} ], models: [ - { label: '大语言模型(LLM)', key: 'llmModelId' }, - { label: '语音识别(ASR)', key: 'asrModelId' }, - { label: '语音活动检测模型(VAD)', key: 'vadModelId' }, - { label: '语音合成模型(TTS)', key: 'ttsModelId' }, - { label: '意图识别模型(Intent)', key: 'intentModelId' }, - { label: '记忆模型(Memory)', key: 'memModelId' } + {label: '大语言模型(LLM)', key: 'llmModelId'}, + {label: '语音识别(ASR)', key: 'asrModelId'}, + {label: '语音活动检测模型(VAD)', key: 'vadModelId'}, + {label: '语音合成模型(TTS)', key: 'ttsModelId'}, + {label: '意图识别模型(Intent)', key: 'intentModelId'}, + {label: '记忆模型(Memory)', key: 'memModelId'} ], - templates: ['台湾女友', '土豆子', '英语老师', '好奇小男孩', '汪汪队队长'] + templates: ['台湾女友', '土豆子', '英语老师', '好奇小男孩', '汪汪队队长'], + loadingTemplate: false } }, methods: { saveConfig() { - const configData = { - agentCode: this.form.agentCode, - agentName: this.form.agentName, - asrModelId: this.form.model.asrModelId, - vadModelId: this.form.model.vadModelId, - llmModelId: this.form.model.llmModelId, - ttsModelId: this.form.model.ttsModelId, - ttsVoiceId: this.form.ttsVoiceId, - memModelId: this.form.model.memModelId, - intentModelId: this.form.model.intentModelId, - systemPrompt: this.form.systemPrompt, - langCode: this.form.langCode, - language: this.form.language, - sort: this.form.sort - }; - import('@/apis/module/user').then(({ default: userApi }) => { - userApi.updateAgentConfig(this.$route.query.agentId, configData, ({data}) => { - if (data.code === 0) { - this.$message.success('配置保存成功'); - } else { - this.$message.error(data.msg || '配置保存失败'); - } - }); + const configData = { + agentCode: this.form.agentCode, + agentName: this.form.agentName, + asrModelId: this.form.model.asrModelId, + vadModelId: this.form.model.vadModelId, + llmModelId: this.form.model.llmModelId, + ttsModelId: this.form.model.ttsModelId, + ttsVoiceId: this.form.ttsVoiceId, + memModelId: this.form.model.memModelId, + intentModelId: this.form.model.intentModelId, + systemPrompt: this.form.systemPrompt, + langCode: this.form.langCode, + language: this.form.language, + sort: this.form.sort + }; + import('@/apis/module/user').then(({default: userApi}) => { + userApi.updateAgentConfig(this.$route.query.agentId, configData, ({data}) => { + if (data.code === 0) { + this.$message.success('配置保存成功'); + } else { + this.$message.error(data.msg || '配置保存失败'); + } }); + }); }, resetConfig() { this.$confirm('确定要重置配置吗?', '提示', { @@ -171,17 +171,17 @@ export default { }).then(() => { // 重置表单 this.form = { - agentCode:"", + agentCode: "", agentName: "", ttsVoiceId: "", systemPrompt: "", - langCode:"", - language:"", - sort:"", + langCode: "", + language: "", + sort: "", model: { ttsModelId: "", vadModelId: "", - asrModelId:"", + asrModelId: "", llmModelId: "", memModelId: "", intentModelId: "", @@ -191,37 +191,73 @@ export default { }).catch(() => { }) }, - selectTemplate(template) { - this.form.name = template; - this.$message.success(`已选择模板:${template}`); - }, - fetchAgentConfig(agentId) { - import('@/apis/module/user').then(({ default: userApi }) => { - userApi.getDeviceConfig(agentId, ({ data }) => { - if (data.code === 0) { - this.form = { - ...this.form, - ...data.data, - model: { - ttsModelId: data.data.ttsModelId, - vadModelId: data.data.vadModelId, - asrModelId: data.data.asrModelId, - llmModelId: data.data.llmModelId, - memModelId: data.data.memModelId, - intentModelId: data.data.intentModelId + selectTemplate(templateName) { + if (this.loadingTemplate) return; + + this.loadingTemplate = true; + import('@/apis/module/user').then(({default: userApi}) => { + userApi.getAgentTemplate( + {templateName}, + (response) => { + this.loadingTemplate = false; + if (response.data.code === 0 && response.data.data.length > 0) { + this.applyTemplateData(response.data.data[0]); + this.$message.success(`「${templateName}」模板已应用`); + } else { + this.$message.warning(`未找到「${templateName}」模板`); } - }; - } else { - this.$message.error(data.msg || '获取配置失败'); - } - }); + } + ); + }).catch((error) => { + this.loadingTemplate = false; + this.$message.error('模板加载失败'); + console.error('接口异常:', error); }); }, - // 清空记忆体内容 - clearMemory() { - this.form.langCode = ""; - this.$message.success("记忆体已清空"); + applyTemplateData(templateData) { + this.form = { + ...this.form, + agentName: templateData.agentName || this.form.agentName, + ttsVoiceId: templateData.ttsVoiceId || this.form.ttsVoiceId, + systemPrompt: templateData.systemPrompt || this.form.systemPrompt, + langCode: templateData.langCode || this.form.langCode, + model: { + ttsModelId: templateData.ttsModelId || this.form.model.ttsModelId, + vadModelId: templateData.vadModelId || this.form.model.vadModelId, + asrModelId: templateData.asrModelId || this.form.model.asrModelId, + llmModelId: templateData.llmModelId || this.form.model.llmModelId, + memModelId: templateData.memModelId || this.form.model.memModelId, + intentModelId: templateData.intentModelId || this.form.model.intentModelId + } + }; }, + fetchAgentConfig(agentId) { + import('@/apis/module/user').then(({default: userApi}) => { + userApi.getDeviceConfig(agentId, ({data}) => { + if (data.code === 0) { + this.form = { + ...this.form, + ...data.data, + model: { + ttsModelId: data.data.ttsModelId, + vadModelId: data.data.vadModelId, + asrModelId: data.data.asrModelId, + llmModelId: data.data.llmModelId, + memModelId: data.data.memModelId, + intentModelId: data.data.intentModelId + } + }; + } else { + this.$message.error(data.msg || '获取配置失败'); + } + }); + }); + }, + // 清空记忆体内容 + clearMemory() { + this.form.langCode = ""; + this.$message.success("记忆体已清空"); + }, }, mounted() { const agentId = this.$route.query.agentId; From 05356b6652fe8497282fa5a2d105d58ef9c90254 Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Fri, 28 Mar 2025 14:13:54 +0800 Subject: [PATCH 24/28] =?UTF-8?q?update:=E7=AE=80=E5=8C=96intent=5Fllm?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/config.yaml | 42 ++-- main/xiaozhi-server/config/logger.py | 2 +- main/xiaozhi-server/core/connection.py | 4 +- .../core/handle/intentHandler.py | 229 ++---------------- .../core/providers/intent/base.py | 10 +- .../intent/function_call/function_call.py | 20 ++ .../providers/intent/intent_llm/intent_llm.py | 95 ++------ 7 files changed, 90 insertions(+), 312 deletions(-) create mode 100644 main/xiaozhi-server/core/providers/intent/function_call/function_call.py diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 71e7d66e..e2c35e67 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -82,14 +82,12 @@ selected_module: TTS: EdgeTTS # 记忆模块,默认不开启记忆;如果想使用超长记忆,推荐使用mem0ai;如果注重隐私,请使用本地的mem_local_short Memory: nomem - # 意图识别模块,默认使用function_call。开启后,可以播放音乐、控制音量、识别退出指令 - # 意图识别使用intent_llm,优点:通用性强,缺点:增加串行前置意图识别模块,会增加处理时间 - # 意图识别使用function_call,缺点:需要所选择的LLM支持function_call,优点:按需调用工具、速度快 + # 意图识别模块开启后,可以播放音乐、控制音量、识别退出指令。 + # 不想开通意图识别,就设置成:nointent + # 意图识别可使用intent_llm,如果你的LLM是DifyLLM或CozeLLM,建议使用这个。优点:通用性强,缺点:增加串行前置意图识别模块,会增加处理时间,这个意图识别暂时不支持控制音量大小等iot操作 + # 意图识别可使用function_call,缺点:需要所选择的LLM支持function_call,优点:按需调用工具、速度快,理论上能全部操作所有iot指令 # 默认免费的ChatGLMLLM就已经支持function_call,但是如果像追求稳定建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-pro-32k-functioncall-241028 - Intent: intent_llm - -# 意图识别专用LLM配置,如果设置,则意图识别会使用这个模型而不是主LLM -IntentLLM: XinferenceSmallLLM #ChatGLMLLM #XinferenceSmallLLM # 设置为空字符串""或不设置则使用主LLM + Intent: function_call # 意图识别,是用于理解用户意图的模块,例如:播放音乐 Intent: @@ -100,9 +98,13 @@ Intent: intent_llm: # 不需要动type type: intent_llm + # 配备意图识别独立的思考模型 + # 如果这里不填,则会默认使用selected_module.LLM的模型作为意图识别的思考模型 + # 如果你的selected_module.LLM选择了DifyLLM或CozeLLM,这里最好使用独立的LLM作为意图识别,例如使用免费的ChatGLMLLM + llm: ChatGLMLLM function_call: # 不需要动type - type: nointent + type: function_call # plugins_func/functions下的模块,可以通过配置,选择加载哪个模块,加载后对话支持相应的function调用 # 系统默认已经记载“handle_exit_intent(退出识别)”、“play_music(音乐播放)”插件,请勿重复加载 # 下面是加载查天气、角色切换、加载查新闻的插件示例 @@ -184,18 +186,6 @@ VAD: LLM: # 所有openai类型均可以修改超参,以AliLLM为例 # 当前支持的type为openai、dify、ollama,可自行适配 - XinferenceLLM: - # 定义LLM API类型 - type: xinference - # Xinference服务地址和模型名称 - model_name: qwen2.5:72b-AWQ # 使用的模型名称,需要预先在Xinference启动对应模型 - base_url: http://localhost:9997 # Xinference服务地址 - XinferenceSmallLLM: - # 定义轻量级LLM API类型,用于意图识别 - type: xinference - # Xinference服务地址和模型名称 - model_name: qwen2.5:3b-AWQ # 使用的小模型名称,用于意图识别 - base_url: http://localhost:9997 # Xinference服务地址 AliLLM: # 定义LLM API类型 type: openai @@ -293,6 +283,18 @@ LLM: variables: k: "v" k2: "v2" + XinferenceLLM: + # 定义LLM API类型 + type: xinference + # Xinference服务地址和模型名称 + model_name: qwen2.5:72b-AWQ # 使用的模型名称,需要预先在Xinference启动对应模型 + base_url: http://localhost:9997 # Xinference服务地址 + XinferenceSmallLLM: + # 定义轻量级LLM API类型,用于意图识别 + type: xinference + # Xinference服务地址和模型名称 + model_name: qwen2.5:3b-AWQ # 使用的小模型名称,用于意图识别 + base_url: http://localhost:9997 # Xinference服务地址 TTS: # 当前支持的type为edge、doubao,可自行适配 EdgeTTS: diff --git a/main/xiaozhi-server/config/logger.py b/main/xiaozhi-server/config/logger.py index 2206e52c..89d991e3 100644 --- a/main/xiaozhi-server/config/logger.py +++ b/main/xiaozhi-server/config/logger.py @@ -13,7 +13,7 @@ def setup_logging(): log_format_file = log_config.get("log_format_file", "{time:YYYY-MM-DD HH:mm:ss} - {version_{selected_module}} - {name} - {level} - {extra[tag]} - {message}") selected_module = config.get("selected_module") - selected_module_str = ''.join([key[0] + value[0] for key, value in selected_module.items()]) + selected_module_str = ''.join([value[0] + value[1] for key, value in selected_module.items()]) log_format = log_format.replace("{version}", SERVER_VERSION) log_format = log_format.replace("{selected_module}", selected_module_str) diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index 07c9c779..10ffc8f6 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -197,12 +197,12 @@ class ConnectionHandler: """为意图识别设置LLM,优先使用专用LLM""" # 检查是否配置了专用的意图识别LLM - intent_llm_name = self.config.get("IntentLLM", "") + intent_llm_name = self.config["Intent"]["intent_llm"]["llm"] # 记录开始初始化意图识别LLM的时间 intent_llm_init_start = time.time() - if intent_llm_name and intent_llm_name in self.config["LLM"]: + if not self.use_function_call_mode and intent_llm_name and intent_llm_name in self.config["LLM"]: # 如果配置了专用LLM,则创建独立的LLM实例 from core.utils import llm as llm_utils intent_llm_config = self.config["LLM"][intent_llm_name] diff --git a/main/xiaozhi-server/core/handle/intentHandler.py b/main/xiaozhi-server/core/handle/intentHandler.py index 900a25cf..c63ca329 100644 --- a/main/xiaozhi-server/core/handle/intentHandler.py +++ b/main/xiaozhi-server/core/handle/intentHandler.py @@ -4,8 +4,7 @@ import uuid from core.handle.sendAudioHandle import send_stt_message from core.handle.helloHandle import checkWakeupWords from core.utils.util import remove_punctuation_and_length -import re -import asyncio +from core.utils.dialogue import Message from loguru import logger TAG = __name__ @@ -65,242 +64,46 @@ async def process_intent_result(conn, intent_result, original_text): try: # 尝试将结果解析为JSON intent_data = json.loads(intent_result) - + # 检查是否有function_call if "function_call" in intent_data: # 直接从意图识别获取了function_call logger.bind(tag=TAG).info(f"检测到function_call格式的意图结果: {intent_data['function_call']['name']}") - function_name = intent_data["function_call"]["name"] - function_args = intent_data["function_call"]["arguments"] - + if function_name == "continue_chat": + return False + function_args = None + if "arguments" in intent_data["function_call"]: + function_args = intent_data["function_call"]["arguments"] # 确保参数是字符串格式的JSON if isinstance(function_args, dict): function_args = json.dumps(function_args) - + function_call_data = { "name": function_name, "id": str(uuid.uuid4().hex), "arguments": function_args } - - # 处理特定类型的函数调用 - if function_name == "get_weather": - logger.bind(tag=TAG).info(f"识别到天气查询意图") - # 先发送消息确认 - await send_stt_message(conn, original_text) - - # 使用executor执行函数调用和结果处理 - def process_weather_query(): - # 直接调用函数 - result = conn.func_handler.handle_llm_function_call(conn, function_call_data) - if result: - # 获取当前最新的文本索引 - text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0 - # 处理函数调用结果 - conn._handle_function_result(result, function_call_data, text_index) - - # 将函数执行放在线程池中 - conn.executor.submit(process_weather_query) - return True - - elif function_name == "play_music": - logger.bind(tag=TAG).info(f"识别到音乐播放意图") - # 先发送消息确认 - await send_stt_message(conn, original_text) - - # 使用executor执行函数调用和结果处理 - def process_music_query(): - # 直接调用函数 - result = conn.func_handler.handle_llm_function_call(conn, function_call_data) - if result: - # 获取当前最新的文本索引 - text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0 - # 处理函数调用结果 - conn._handle_function_result(result, function_call_data, text_index) - - # 将函数执行放在线程池中 - conn.executor.submit(process_music_query) - return True - - elif function_name == "get_news": - logger.bind(tag=TAG).info(f"识别到新闻查询意图") - # 先发送消息确认 - await send_stt_message(conn, original_text) - - # 使用executor执行函数调用和结果处理 - def process_news_query(): - # 直接调用函数 - result = conn.func_handler.handle_llm_function_call(conn, function_call_data) - if result: - # 获取当前最新的文本索引 - text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0 - # 处理函数调用结果 - conn._handle_function_result(result, function_call_data, text_index) - - # 将函数执行放在线程池中 - conn.executor.submit(process_news_query) - return True - - else: - # 其他类型的函数调用,尝试直接执行 - # 先发送消息确认 - await send_stt_message(conn, original_text) - - # 使用executor执行函数调用和结果处理 - def process_function_call(): - result = conn.func_handler.handle_llm_function_call(conn, function_call_data) - if result: - # 获取当前最新的文本索引 - text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0 - # 处理函数调用结果 - conn._handle_function_result(result, function_call_data, text_index) - - # 将函数执行放在线程池中 - conn.executor.submit(process_function_call) - return True - - # 处理传统意图格式 - elif "intent" in intent_data: - intent = intent_data["intent"] - - # 处理退出意图 - if "结束聊天" in intent: - logger.bind(tag=TAG).info(f"识别到退出意图: {intent}") - # 如果是明确的离别意图,发送告别语并关闭连接 - await send_stt_message(conn, original_text) - conn.executor.submit(conn.chat_and_close, original_text) - return True - - # 其他不需要特殊处理的意图,让常规聊天流程处理 - return False - - except json.JSONDecodeError: - # 如果不是有效的JSON,尝试兼容旧格式 - intent = intent_result - - # 处理退出意图 - if "结束聊天" in intent: - logger.bind(tag=TAG).info(f"识别到退出意图: {intent}") - # 如果是明确的离别意图,发送告别语并关闭连接 - await send_stt_message(conn, original_text) - conn.executor.submit(conn.chat_and_close, original_text) - return True - # 处理播放音乐意图 - if "播放音乐" in intent: - logger.bind(tag=TAG).info(f"识别到音乐播放意图: {intent}") - # 获取歌曲名称 - song_name = extract_text_in_brackets(intent) - - # 先发送消息确认 await send_stt_message(conn, original_text) - - # 构造合适的音乐播放函数调用 - function_id = str(uuid.uuid4().hex) - function_name = "play_music" - function_arguments = '{ "song_name": ' + (f'"{song_name}"' if song_name else '"random"') + ' }' - - function_call_data = { - "name": function_name, - "id": function_id, - "arguments": function_arguments - } - + # 使用executor执行函数调用和结果处理 - def process_music_query(): - # 直接调用函数 + def process_function_call(): + conn.dialogue.put(Message(role="user", content=original_text)) result = conn.func_handler.handle_llm_function_call(conn, function_call_data) if result: # 获取当前最新的文本索引 text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0 # 处理函数调用结果 conn._handle_function_result(result, function_call_data, text_index) - + # 将函数执行放在线程池中 - conn.executor.submit(process_music_query) + conn.executor.submit(process_function_call) return True - - # 处理查询天气意图 - if "查询天气" in intent: - logger.bind(tag=TAG).info(f"识别到天气查询意图: {intent}") - # 获取地点 - location = extract_text_in_brackets(intent) - - # 先发送消息确认 - await send_stt_message(conn, original_text) - - # 构造合适的天气查询函数调用 - function_id = str(uuid.uuid4().hex) - function_name = "get_weather" - function_arguments = '{ "location": ' + (f'"{location}"' if location and location != "当前位置" else 'null') + ', "lang": "zh_CN" }' - - function_call_data = { - "name": function_name, - "id": function_id, - "arguments": function_arguments - } - - # 使用executor执行函数调用和结果处理 - def process_weather_query(): - # 直接调用函数 - result = conn.func_handler.handle_llm_function_call(conn, function_call_data) - if result: - # 获取当前最新的文本索引 - text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0 - # 处理函数调用结果 - conn._handle_function_result(result, function_call_data, text_index) - - # 将函数执行放在线程池中 - conn.executor.submit(process_weather_query) - return True - - # 处理查询新闻意图 - if "查询新闻" in intent or "播报新闻" in intent or "看新闻" in intent: - logger.bind(tag=TAG).info(f"识别到新闻查询意图: {intent}") - # 获取新闻类别 - category = extract_text_in_brackets(intent) - - # 先发送消息确认 - await send_stt_message(conn, original_text) - - # 构造合适的新闻查询函数调用 - function_id = str(uuid.uuid4().hex) - function_name = "get_news" - - # 判断是否是查询详情 - detail = "详情" in intent or "详细" in intent - - # 构造参数JSON字符串 - if detail: - function_arguments = '{ "detail": true, "lang": "zh_CN" }' - else: - function_arguments = '{ "category": ' + (f'"{category}"' if category else 'null') + ', "detail": false, "lang": "zh_CN" }' - - function_call_data = { - "name": function_name, - "id": function_id, - "arguments": function_arguments - } - - # 使用executor执行函数调用和结果处理 - def process_news_query(): - # 直接调用函数 - result = conn.func_handler.handle_llm_function_call(conn, function_call_data) - if result: - # 获取当前最新的文本索引 - text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0 - # 处理函数调用结果 - conn._handle_function_result(result, function_call_data, text_index) - - # 将函数执行放在线程池中 - conn.executor.submit(process_news_query) - return True - except Exception as e: + return False + except json.JSONDecodeError as e: logger.bind(tag=TAG).error(f"处理意图结果时出错: {e}") - - # 默认返回False,表示继续常规聊天流程 - return False + return False def extract_text_in_brackets(s): diff --git a/main/xiaozhi-server/core/providers/intent/base.py b/main/xiaozhi-server/core/providers/intent/base.py index 877dfced..a17cde81 100644 --- a/main/xiaozhi-server/core/providers/intent/base.py +++ b/main/xiaozhi-server/core/providers/intent/base.py @@ -10,11 +10,13 @@ class IntentProviderBase(ABC): def __init__(self, config): self.config = config self.intent_options = config.get("intent_options", { + "handle_exit_intent": "结束聊天, 用户发来如再见之类的表示结束的话, 不想再进行对话的时候", + "play_music": "播放音乐, 用户希望你可以播放音乐, 只用于播放音乐的意图", + "get_weather": "查询天气, 用户希望查询某个地点的天气情况", + "get_news": "查询新闻, 用户希望查询最新新闻或特定类型的新闻", + "get_lunar": "用于获取今天的阴历/农历和黄历信息", + "get_time": "获取今天日期或者当前时间信息", "continue_chat": "继续聊天", - "end_chat": "结束聊天", - "play_music": "播放音乐", - "get_weather": "查询天气", - "get_news": "查询新闻" }) def set_llm(self, llm): diff --git a/main/xiaozhi-server/core/providers/intent/function_call/function_call.py b/main/xiaozhi-server/core/providers/intent/function_call/function_call.py new file mode 100644 index 00000000..ff33e58f --- /dev/null +++ b/main/xiaozhi-server/core/providers/intent/function_call/function_call.py @@ -0,0 +1,20 @@ +from ..base import IntentProviderBase +from typing import List, Dict +from config.logger import setup_logging + +TAG = __name__ +logger = setup_logging() + + +class IntentProvider(IntentProviderBase): + async def detect_intent(self, conn, dialogue_history: List[Dict], text: str) -> str: + """ + 默认的意图识别实现,始终返回继续聊天 + Args: + dialogue_history: 对话历史记录列表 + text: 本次对话记录 + Returns: + 固定返回"继续聊天" + """ + logger.bind(tag=TAG).debug("Using functionCallProvider, always returning continue chat") + return self.intent_options["continue_chat"] diff --git a/main/xiaozhi-server/core/providers/intent/intent_llm/intent_llm.py b/main/xiaozhi-server/core/providers/intent/intent_llm/intent_llm.py index 5d0b0830..1afd835e 100644 --- a/main/xiaozhi-server/core/providers/intent/intent_llm/intent_llm.py +++ b/main/xiaozhi-server/core/providers/intent/intent_llm/intent_llm.py @@ -20,19 +20,6 @@ class IntentProvider(IntentProviderBase): self.intent_cache = {} # 缓存意图识别结果 self.cache_expiry = 600 # 缓存有效期10分钟 self.cache_max_size = 100 # 最多缓存100个意图 - self.common_patterns = { - "天气": '{\"function_call\": {\"name\": \"get_weather\", \"arguments\": {\"location\": null, \"lang\": \"zh_CN\"}}}', - "新闻": '{\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": null, \"detail\": false, \"lang\": \"zh_CN\"}}}', - "财经新闻": '{\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": \"财经\", \"detail\": false, \"lang\": \"zh_CN\"}}}', - "国际新闻": '{\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": \"国际\", \"detail\": false, \"lang\": \"zh_CN\"}}}', - "社会新闻": '{\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": \"社会\", \"detail\": false, \"lang\": \"zh_CN\"}}}', - "详细介绍": '{\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"detail\": true, \"lang\": \"zh_CN\"}}}', - "详情": '{\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"detail\": true, \"lang\": \"zh_CN\"}}}', - "再见": '{\"intent\": \"结束聊天\"}', - "结束": '{\"intent\": \"结束聊天\"}', - "拜拜": '{\"intent\": \"结束聊天\"}', - "播放音乐": '{\"function_call\": {\"name\": \"play_music\", \"arguments\": {\"song_name\": \"random\"}}}' - } def get_intent_system_prompt(self) -> str: """ @@ -42,60 +29,50 @@ class IntentProvider(IntentProviderBase): """ intent_list = [] - """ - "continue_chat": "1.继续聊天, 除了播放音乐和结束聊天的时候的选项, 比如日常的聊天和问候, 对话等", - "end_chat": "2.结束聊天, 用户发来如再见之类的表示结束的话, 不想再进行对话的时候", - "play_music": "3.播放音乐, 用户希望你可以播放音乐, 只用于播放音乐的意图", - "get_weather": "4.查询天气, 用户希望查询某个地点的天气情况" - "get_news": "5.查询新闻, 用户希望查询最新新闻或特定类型的新闻" - """ - for key, value in self.intent_options.items(): - if key == "play_music": - intent_list.append("3.播放音乐, 用户希望你可以播放音乐, 只用于播放音乐的意图") - elif key == "end_chat": - intent_list.append("2.结束聊天, 用户发来如再见之类的表示结束的话, 不想再进行对话的时候") - elif key == "continue_chat": - intent_list.append("1.继续聊天, 除了播放音乐和结束聊天的时候的选项, 比如日常的聊天和问候, 对话等") - elif key == "get_weather": - intent_list.append("4.查询天气, 用户希望查询某个地点的天气情况") - elif key == "get_news": - intent_list.append("5.查询新闻, 用户希望查询最新新闻或特定类型的新闻") - else: - intent_list.append(value) - prompt = ( "你是一个意图识别助手。请分析用户的最后一句话,判断用户意图属于以下哪一类:\n" "" f"{', '.join(intent_list)}" "\n" "处理步骤:" - "1. 思考意图类型" - "2. 继续聊天和结束聊天意图: 返回intent格式" - "3. 播放音乐意图: 分析歌名,生成function_call格式" - "4. 查询天气意图: 分析地点,生成function_call格式" - "5. 查询新闻意图: 分析新闻类别,生成function_call格式" + "1. 思考意图类型,生成function_call格式" "\n\n" "返回格式示例:\n" - "1. 继续聊天意图: {\"intent\": \"继续聊天\"}\n" - "2. 结束聊天意图: {\"intent\": \"结束聊天\"}\n" - "3. 播放音乐意图: {\"function_call\": {\"name\": \"play_music\", \"arguments\": {\"song_name\": \"音乐名称\"}}}\n" - "4. 查询天气意图: {\"function_call\": {\"name\": \"get_weather\", \"arguments\": {\"location\": \"地点名称\", \"lang\": \"zh_CN\"}}}\n" - "5. 查询新闻意图: {\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": \"新闻类别\", \"detail\": false, \"lang\": \"zh_CN\"}}}\n" + "1. 播放音乐意图: {\"function_call\": {\"name\": \"play_music\", \"arguments\": {\"song_name\": \"音乐名称\"}}}\n" + "2. 查询天气意图: {\"function_call\": {\"name\": \"get_weather\", \"arguments\": {\"location\": \"地点名称\", \"lang\": \"zh_CN\"}}}\n" + "3. 查询新闻意图: {\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": \"新闻类别\", \"detail\": false, \"lang\": \"zh_CN\"}}}\n" + "4. 结束对话意图: {\"function_call\": {\"name\": \"handle_exit_intent\", \"arguments\": {\"say_goodbye\": \"goodbye\"}}}\n" + "5. 获取当天日期时间: {\"function_call\": {\"name\": \"get_time\"}}\n" + "6. 获取当前黄历意图: {\"function_call\": {\"name\": \"get_lunar\"}}\n" + "7. 继续聊天意图: {\"function_call\": {\"name\": \"continue_chat\"}}\n" "\n" "注意:\n" "- 播放音乐:无歌名时,song_name设为\"random\"\n" "- 查询天气:无地点时,location设为null\n" "- 查询新闻:无类别时,category设为null;查询详情时,detail设为true\n" + "- 如果没有明显的意图,应按照继续聊天意图处理\n" "- 只返回纯JSON,不要任何其他内容\n" "\n" "示例分析:\n" "```\n" + "用户: 你好小智\n" + "返回: {\"function_call\": {\"name\": \"continue_chat\"}}\n" + "```\n" + "```\n" "用户: 你今天怎么样?\n" - "返回: {\"intent\": \"继续聊天\"}\n" + "返回: {\"function_call\": {\"name\": \"continue_chat\"}}\n" + "```\n" + "```\n" + "用户: 现在是几号了?现在几点了?\n" + "返回: {\"function_call\": {\"name\": \"get_time\"}}\n" + "```\n" + "```\n" + "用户: 今天农历是多少?\n" + "返回: {\"function_call\": {\"name\": \"get_lunar\"}}\n" "```\n" "```\n" "用户: 我们明天再聊吧\n" - "返回: {\"intent\": \"结束聊天\"}\n" + "返回: {\"function_call\": {\"name\": \"handle_exit_intent\"}}\n" "```\n" "```\n" "用户: 播放中秋月\n" @@ -140,25 +117,6 @@ class IntentProvider(IntentProviderBase): for key, _ in sorted_items[:len(sorted_items) - self.cache_max_size]: del self.intent_cache[key] - def check_pattern_match(self, text): - """检查文本是否匹配常见模式,并提取关键信息""" - # 城市+天气的特殊模式匹配 - city_weather_pattern = re.search(r'([^\s,,。?!]+)天气', text) - if city_weather_pattern: - city = city_weather_pattern.group(1) - # 排除可能的误匹配,如"今天天气"、"明天天气"、"现在天气"等 - if city not in ["今天", "今日", "明天", "现在", "当前", "未来", "明日", "这两天", "近期"]: - logger.bind(tag=TAG).info(f"提取到城市名: {city}") - # 返回包含城市名的function_call - return f'{{\"function_call\": {{\"name\": \"get_weather\", \"arguments\": {{\"location\": \"{city}\", \"lang\": \"zh_CN\"}}}}}}' - - # 普通模式匹配 - for pattern, intent in self.common_patterns.items(): - if pattern in text: - return intent - - return None - async def detect_intent(self, conn, dialogue_history: List[Dict], text: str) -> str: if not self.llm: raise ValueError("LLM provider not set") @@ -170,13 +128,6 @@ class IntentProvider(IntentProviderBase): model_info = getattr(self.llm, 'model_name', str(self.llm.__class__.__name__)) logger.bind(tag=TAG).info(f"使用意图识别模型: {model_info}") - # 先尝试简单的模式匹配 - pattern_match = self.check_pattern_match(text) - if pattern_match: - pattern_time = time.time() - total_start_time - logger.bind(tag=TAG).info(f"模式匹配成功: {text} -> {pattern_match}, 耗时: {pattern_time:.4f}秒") - return pattern_match - # 计算缓存键 cache_key = hashlib.md5(text.encode()).hexdigest() From b1b64abed922ac1d44a3869944d1871466dd81f7 Mon Sep 17 00:00:00 2001 From: kanako <4190346+mkanako@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:20:11 +0800 Subject: [PATCH 25/28] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8Dhandle=5Fdevice.py?= =?UTF-8?q?=20=E5=8F=82=E6=95=B0=E7=B1=BB=E5=9E=8B=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E5=AF=BC=E8=87=B4DoubaoLLM=E8=B0=83=E7=94=A8?= =?UTF-8?q?=E6=8A=A5=20400=20=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/plugins_func/functions/handle_device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/xiaozhi-server/plugins_func/functions/handle_device.py b/main/xiaozhi-server/plugins_func/functions/handle_device.py index d564562b..1496f9e6 100644 --- a/main/xiaozhi-server/plugins_func/functions/handle_device.py +++ b/main/xiaozhi-server/plugins_func/functions/handle_device.py @@ -69,7 +69,7 @@ handle_device_function_desc = { "description": "动作名称,可选值:get(获取),set(设置),raise(提高),lower(降低)" }, "value": { - "type": "int", + "type": "number", "description": "值大小,可选值:0-100之间的整数" } }, From 3a2261107c3f8fe3695b36cef17070ac85a60369 Mon Sep 17 00:00:00 2001 From: Ran_Chen <1908198662@qq.com> Date: Fri, 28 Mar 2025 15:24:53 +0800 Subject: [PATCH 26/28] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BA=86=E2=80=9C?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E7=AE=A1=E7=90=86=E2=80=9D=EF=BC=8C=E5=88=86?= =?UTF-8?q?=E9=A1=B5=E8=BF=98=E6=9C=AA=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/manager-web/src/views/UserManagement.vue | 106 ++++++++++++++---- 1 file changed, 82 insertions(+), 24 deletions(-) diff --git a/main/manager-web/src/views/UserManagement.vue b/main/manager-web/src/views/UserManagement.vue index 2122568e..cee70991 100644 --- a/main/manager-web/src/views/UserManagement.vue +++ b/main/manager-web/src/views/UserManagement.vue @@ -5,13 +5,13 @@
用户管理
- + - - - + + - +
- 全选 - 启用 - 禁用 - 删除 + 全选 + 启用 + 禁用 + 删除
@@ -128,11 +125,20 @@ export default { this.currentPage = page; console.log('当前页码:', page); }, + headerCellClassName({ column, columnIndex }) { + if (columnIndex === 0) { + return 'custom-selection-header' + } + return '' + }, } }; \ No newline at end of file From 17126303fadc8be371ff289f53756b83c2da319c Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Fri, 28 Mar 2025 17:17:14 +0800 Subject: [PATCH 27/28] =?UTF-8?q?fix:=E5=B7=B2=E7=BB=8F=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E4=BA=86intent=5Fllm=EF=BC=8C=E4=B8=8D=E5=BA=94=E8=AF=A5?= =?UTF-8?q?=E5=86=8D=E5=9B=9E=E5=88=B0functioncall=E7=9A=84chat=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/connection.py | 190 +++-------------- .../core/handle/intentHandler.py | 15 +- .../core/providers/intent/base.py | 7 +- .../plugins_func/functions/get_news.py | 84 ++++---- .../plugins_func/functions/get_weather.py | 198 ++++++------------ 5 files changed, 136 insertions(+), 358 deletions(-) diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index 10ffc8f6..2b6bda25 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -210,11 +210,6 @@ class ConnectionHandler: intent_llm = llm_utils.create_instance(intent_llm_type, intent_llm_config) self.logger.bind(tag=TAG).info(f"为意图识别创建了专用LLM: {intent_llm_name}, 类型: {intent_llm_type}") - # 记录额外的模型信息 - model_name = intent_llm_config.get("model_name", "未指定") - base_url = intent_llm_config.get("base_url", "未指定") - self.logger.bind(tag=TAG).info(f"意图识别LLM详细信息 - 模型名称: {model_name}, 服务地址: {base_url}") - self.intent.set_llm(intent_llm) else: # 否则使用主LLM @@ -337,7 +332,7 @@ class ConnectionHandler: self.logger.bind(tag=TAG).debug(json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False)) return True - def chat_with_function_calling(self, query, tool_call=False, is_weather_query=False, is_news_query=False): + def chat_with_function_calling(self, query, tool_call=False): self.logger.bind(tag=TAG).debug(f"Chat with function calling start: {query}") """Chat with function calling for intent detection using streaming""" if self.isNeedAuth(): @@ -355,7 +350,7 @@ class ConnectionHandler: functions = self.func_handler.get_functions() response_message = [] - processed_chars = 0 + processed_chars = 0 # 跟踪已处理的字符位置 try: start_time = time.time() @@ -364,58 +359,14 @@ class ConnectionHandler: future = asyncio.run_coroutine_threadsafe(self.memory.query_memory(query), self.loop) memory_str = future.result() - # 为天气查询添加特殊处理 - if is_weather_query: - self.logger.bind(tag=TAG).info(f"检测到天气查询,添加特殊指令") - # 获取对话历史 - dialogue_with_memory = self.dialogue.get_llm_dialogue_with_memory(memory_str) - - # 找到最后一条tool消息(可能是天气数据) - for i in range(len(dialogue_with_memory) - 1, -1, -1): - if dialogue_with_memory[i].get("role") == "tool" and "当前天气" in dialogue_with_memory[i].get("content", ""): - # 添加特殊指令 - dialogue_with_memory.append({ - "role": "system", - "content": "请根据上面的天气数据,以简洁友好的方式回答用户的天气查询。直接告诉用户当前天气状况、温度以及可能需要的建议,不要提及数据来源或解释你是如何获取这些信息的。" - }) - self.logger.bind(tag=TAG).info(f"已添加天气查询特殊指令") - break - - # 使用支持functions的streaming接口并传入修改后的对话历史 - llm_responses = self.llm.response_with_functions( - self.session_id, - dialogue_with_memory, - functions=functions - ) - # 为新闻查询添加特殊处理 - elif is_news_query: - self.logger.bind(tag=TAG).info(f"检测到新闻查询,添加特殊指令") - # 获取对话历史 - dialogue_with_memory = self.dialogue.get_llm_dialogue_with_memory(memory_str) - - # 找到最后一条tool消息(可能是新闻数据) - for i in range(len(dialogue_with_memory) - 1, -1, -1): - if dialogue_with_memory[i].get("role") == "tool" and "新闻" in dialogue_with_memory[i].get("content", ""): - # 添加特殊指令 - dialogue_with_memory.append({ - "role": "system", - "content": "请根据上面的新闻数据,以简洁友好的方式回答用户的新闻查询。直接告诉用户新闻内容,不要提及数据来源或解释你是如何获取这些信息的。保持新闻播报的语气和风格。" - }) - self.logger.bind(tag=TAG).info(f"已添加新闻查询特殊指令") - break - - # 使用支持functions的streaming接口并传入修改后的对话历史 - llm_responses = self.llm.response_with_functions( - self.session_id, - dialogue_with_memory, - functions=functions - ) - else: - llm_responses = self.llm.response_with_functions( - self.session_id, - self.dialogue.get_llm_dialogue_with_memory(memory_str), - functions=functions - ) + # self.logger.bind(tag=TAG).info(f"对话记录: {self.dialogue.get_llm_dialogue_with_memory(memory_str)}") + + # 使用支持functions的streaming接口 + llm_responses = self.llm.response_with_functions( + self.session_id, + self.dialogue.get_llm_dialogue_with_memory(memory_str), + functions=functions + ) except Exception as e: self.logger.bind(tag=TAG).error(f"LLM 处理出错 {query}: {e}") return None @@ -431,6 +382,9 @@ class ConnectionHandler: content_arguments = "" for response in llm_responses: content, tools_call = response + if "content" in response: + content = response["content"] + tools_call = None if content is not None and len(content) > 0: if len(response_message) <= 0 and (content == "```" or "" in content): tool_call_flag = True @@ -533,126 +487,36 @@ class ConnectionHandler: return True def _handle_function_result(self, result, function_call_data, text_index): - self.logger.bind(tag=TAG).info(f"处理函数调用结果,动作类型: {result.action.name if result.action else 'None'}") - - # 检查是否有备用直接回复 - direct_response = getattr(result, 'response', None) - if result.action == Action.RESPONSE: # 直接回复前端 text = result.response - self.logger.bind(tag=TAG).info(f"函数返回直接回复: {text[:100] if text else 'None'}...") self.recode_first_last_text(text, text_index) future = self.executor.submit(self.speak_and_play, text, text_index) self.tts_queue.put(future) self.dialogue.put(Message(role="assistant", content=text)) elif result.action == Action.REQLLM: # 调用函数后再请求llm生成回复 - self.logger.bind(tag=TAG).info(f"处理REQLLM动作,需要进一步处理结果") - - text = result.result - function_id = function_call_data["id"] - function_name = function_call_data["name"] - function_arguments = function_call_data["arguments"] - - if text is not None and len(text) > 0: - self.logger.bind(tag=TAG).info(f"函数返回结果长度: {len(text)}, 前100字符: {text[:100]}...") - - # 特殊处理天气查询 - if function_name == "get_weather": - self.logger.bind(tag=TAG).info(f"检测到天气查询结果,使用特殊处理") - - # 记录工具调用到对话历史 - self.dialogue.put(Message(role='assistant', - tool_calls=[{"id": function_id, - "function": {"arguments": function_arguments, - "name": function_name}, - "type": 'function', - "index": 0}])) - - # 记录工具返回结果到对话历史 - self.dialogue.put(Message(role="tool", tool_call_id=function_id, content=text)) - - try: - # 使用天气数据生成回复 - self.chat_with_function_calling(text, tool_call=True, is_weather_query=True) - except Exception as e: - self.logger.bind(tag=TAG).error(f"处理天气查询数据失败: {e}") - if direct_response: - self.logger.bind(tag=TAG).info(f"使用备用直接回复: {direct_response}") - self.recode_first_last_text(direct_response, text_index) - future = self.executor.submit(self.speak_and_play, direct_response, text_index) - self.tts_queue.put(future) - self.dialogue.put(Message(role="assistant", content=direct_response)) - # 特殊处理新闻查询 - elif function_name == "get_news": - self.logger.bind(tag=TAG).info(f"检测到新闻查询结果,使用特殊处理") - - # 记录工具调用到对话历史 - self.dialogue.put(Message(role='assistant', - tool_calls=[{"id": function_id, - "function": {"arguments": function_arguments, - "name": function_name}, - "type": 'function', - "index": 0}])) - - # 记录工具返回结果到对话历史 - self.dialogue.put(Message(role="tool", tool_call_id=function_id, content=text)) - - try: - # 使用新闻数据生成回复,设置is_news_query=True - self.chat_with_function_calling(text, tool_call=True, is_news_query=True) - except Exception as e: - self.logger.bind(tag=TAG).error(f"处理新闻查询数据失败: {e}") - if direct_response: - self.logger.bind(tag=TAG).info(f"使用备用直接回复: {direct_response}") - self.recode_first_last_text(direct_response, text_index) - future = self.executor.submit(self.speak_and_play, direct_response, text_index) - self.tts_queue.put(future) - self.dialogue.put(Message(role="assistant", content=direct_response)) - else: - # 其他类型的函数调用 - self.dialogue.put(Message(role='assistant', - tool_calls=[{"id": function_id, - "function": {"arguments": function_arguments, - "name": function_name}, - "type": 'function', - "index": 0}])) - self.dialogue.put(Message(role="tool", tool_call_id=function_id, content=text)) - - try: - self.chat_with_function_calling(text, tool_call=True) - except Exception as e: - self.logger.bind(tag=TAG).error(f"处理函数调用结果失败: {e}") - if direct_response: - self.logger.bind(tag=TAG).info(f"使用备用直接回复: {direct_response}") - self.recode_first_last_text(direct_response, text_index) - future = self.executor.submit(self.speak_and_play, direct_response, text_index) - self.tts_queue.put(future) - self.dialogue.put(Message(role="assistant", content=direct_response)) - else: - self.logger.bind(tag=TAG).warning(f"函数返回结果为空") - if direct_response: - self.logger.bind(tag=TAG).info(f"使用备用直接回复: {direct_response}") - self.recode_first_last_text(direct_response, text_index) - future = self.executor.submit(self.speak_and_play, direct_response, text_index) - self.tts_queue.put(future) - self.dialogue.put(Message(role="assistant", content=direct_response)) - else: - error_text = f"抱歉,我无法获取{function_name}的结果,请稍后再试。" - self.recode_first_last_text(error_text, text_index) - future = self.executor.submit(self.speak_and_play, error_text, text_index) - self.tts_queue.put(future) - self.dialogue.put(Message(role="assistant", content=error_text)) + text = result.result + if text is not None and len(text) > 0: + function_id = function_call_data["id"] + function_name = function_call_data["name"] + function_arguments = function_call_data["arguments"] + self.dialogue.put(Message(role='assistant', + tool_calls=[{"id": function_id, + "function": {"arguments": function_arguments, + "name": function_name}, + "type": 'function', + "index": 0}])) + + self.dialogue.put(Message(role="tool", tool_call_id=function_id, content=text)) + self.chat_with_function_calling(text, tool_call=True) elif result.action == Action.NOTFOUND: text = result.result - self.logger.bind(tag=TAG).info(f"未找到对应函数: {text}") self.recode_first_last_text(text, text_index) future = self.executor.submit(self.speak_and_play, text, text_index) self.tts_queue.put(future) self.dialogue.put(Message(role="assistant", content=text)) else: text = result.result - self.logger.bind(tag=TAG).info(f"其他动作类型,直接返回结果: {text[:100] if text else None}...") self.recode_first_last_text(text, text_index) future = self.executor.submit(self.speak_and_play, text, text_index) self.tts_queue.put(future) diff --git a/main/xiaozhi-server/core/handle/intentHandler.py b/main/xiaozhi-server/core/handle/intentHandler.py index c63ca329..5105a4d9 100644 --- a/main/xiaozhi-server/core/handle/intentHandler.py +++ b/main/xiaozhi-server/core/handle/intentHandler.py @@ -91,11 +91,18 @@ async def process_intent_result(conn, intent_result, original_text): def process_function_call(): conn.dialogue.put(Message(role="user", content=original_text)) result = conn.func_handler.handle_llm_function_call(conn, function_call_data) - if result: + if result and function_name != 'play_music': # 获取当前最新的文本索引 - text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0 - # 处理函数调用结果 - conn._handle_function_result(result, function_call_data, text_index) + text = result.response + if text is None: + text = result.result + if text is not None: + text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0 + conn.recode_first_last_text(text, text_index) + future = conn.executor.submit(conn.speak_and_play, text, text_index) + conn.llm_finish_task = True + conn.tts_queue.put(future) + conn.dialogue.put(Message(role="assistant", content=text)) # 将函数执行放在线程池中 conn.executor.submit(process_function_call) diff --git a/main/xiaozhi-server/core/providers/intent/base.py b/main/xiaozhi-server/core/providers/intent/base.py index a17cde81..2a39f1ae 100644 --- a/main/xiaozhi-server/core/providers/intent/base.py +++ b/main/xiaozhi-server/core/providers/intent/base.py @@ -23,13 +23,8 @@ class IntentProviderBase(ABC): self.llm = llm # 获取模型名称和类型信息 model_name = getattr(llm, 'model_name', str(llm.__class__.__name__)) - model_type = getattr(llm, 'type', 'unknown') # 记录更详细的日志 - logger.bind(tag=TAG).info(f"意图识别设置LLM: {model_name}, 类型: {model_type}") - # 尝试获取模型基础URL - base_url = getattr(llm, 'base_url', 'N/A') - if base_url != 'N/A': - logger.bind(tag=TAG).debug(f"意图识别LLM基础URL: {base_url}") + logger.bind(tag=TAG).info(f"意图识别设置LLM: {model_name}") @abstractmethod async def detect_intent(self, conn, dialogue_history: List[Dict], text: str) -> str: diff --git a/main/xiaozhi-server/plugins_func/functions/get_news.py b/main/xiaozhi-server/plugins_func/functions/get_news.py index f7a6d342..85b203dc 100644 --- a/main/xiaozhi-server/plugins_func/functions/get_news.py +++ b/main/xiaozhi-server/plugins_func/functions/get_news.py @@ -45,10 +45,10 @@ def fetch_news_from_rss(rss_url): try: response = requests.get(rss_url) response.raise_for_status() - + # 解析XML root = ET.fromstring(response.content) - + # 查找所有item元素(新闻条目) news_items = [] for item in root.findall('.//item'): @@ -56,14 +56,14 @@ def fetch_news_from_rss(rss_url): link = item.find('link').text if item.find('link') is not None else "#" description = item.find('description').text if item.find('description') is not None else "无描述" pubDate = item.find('pubDate').text if item.find('pubDate') is not None else "未知时间" - + news_items.append({ 'title': title, 'link': link, 'description': description, 'pubDate': pubDate }) - + return news_items except Exception as e: logger.bind(tag=TAG).error(f"获取RSS新闻失败: {e}") @@ -75,9 +75,9 @@ def fetch_news_detail(url): try: response = requests.get(url) response.raise_for_status() - + soup = BeautifulSoup(response.content, 'html.parser') - + # 尝试提取正文内容 (这里的选择器需要根据实际网站结构调整) content_div = soup.select_one('.content_desc, .content, article, .article-content') if content_div: @@ -98,7 +98,7 @@ def map_category(category_text): """将用户输入的中文类别映射到配置文件中的类别键""" if not category_text: return None - + # 类别映射字典,目前支持社会、国际、财经新闻,如需更多类型,参见配置文件 category_map = { # 社会新闻 @@ -113,10 +113,10 @@ def map_category(category_text): "金融": "finance", "经济": "finance" } - + # 转换为小写并去除空格 normalized_category = category_text.lower().strip() - + # 返回映射结果,如果没有匹配项则返回原始输入 return category_map.get(normalized_category, category_text) @@ -128,25 +128,23 @@ def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_C # 如果detail为True,获取上一条新闻的详细内容 if detail: if not hasattr(conn, 'last_news_link') or not conn.last_news_link or 'link' not in conn.last_news_link: - direct_response = "抱歉,没有找到最近查询的新闻,请先获取一条新闻。" - return ActionResponse(Action.REQLLM, "抱歉,没有找到最近查询的新闻,请先获取一条新闻。", direct_response) - + return ActionResponse(Action.REQLLM, "抱歉,没有找到最近查询的新闻,请先获取一条新闻。", None) + link = conn.last_news_link.get('link') title = conn.last_news_link.get('title', '未知标题') - + if link == '#': - direct_response = "抱歉,该新闻没有可用的链接获取详细内容。" - return ActionResponse(Action.REQLLM, "抱歉,该新闻没有可用的链接获取详细内容。", direct_response) - + return ActionResponse(Action.REQLLM, "抱歉,该新闻没有可用的链接获取详细内容。", None) + logger.bind(tag=TAG).debug(f"获取新闻详情: {title}, URL={link}") - + # 获取新闻详情 detail_content = fetch_news_detail(link) - + if not detail_content or detail_content == "无法获取详细内容": - direct_response = f"抱歉,无法获取《{title}》的详细内容,可能是链接已失效或网站结构发生变化。" - return ActionResponse(Action.REQLLM, f"抱歉,无法获取《{title}》的详细内容,可能是链接已失效或网站结构发生变化。", direct_response) - + return ActionResponse(Action.REQLLM, + f"抱歉,无法获取《{title}》的详细内容,可能是链接已失效或网站结构发生变化。", None) + # 构建详情报告 detail_report = ( f"根据下列数据,用{lang}回应用户的新闻详情查询请求:\n\n" @@ -155,37 +153,33 @@ def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_C f"(请对上述新闻内容进行总结,提取关键信息,以自然、流畅的方式向用户播报," f"不要提及这是总结,就像是在讲述一个完整的新闻故事)" ) - - # 构造一个直接回复,简要概括新闻内容 - direct_response = f"以下是《{title}》的详细内容:{detail_content[:200]}...(内容过长已省略)" - - return ActionResponse(Action.REQLLM, detail_report, direct_response) - + + return ActionResponse(Action.REQLLM, detail_report, None) + # 否则,获取新闻列表并随机选择一条 # 从配置中获取RSS URL rss_config = conn.config["plugins"]["get_news"] default_rss_url = rss_config.get("default_rss_url", "https://www.chinanews.com.cn/rss/society.xml") - + # 将用户输入的类别映射到配置中的类别键 mapped_category = map_category(category) - + # 如果提供了类别,尝试从配置中获取对应的URL rss_url = default_rss_url if mapped_category and mapped_category in rss_config.get("category_urls", {}): rss_url = rss_config["category_urls"][mapped_category] - + logger.bind(tag=TAG).info(f"获取新闻: 原始类别={category}, 映射类别={mapped_category}, URL={rss_url}") - + # 获取新闻列表 news_items = fetch_news_from_rss(rss_url) - + if not news_items: - direct_response = "抱歉,未能获取到新闻信息,请稍后再试。" - return ActionResponse(Action.REQLLM, "抱歉,未能获取到新闻信息,请稍后再试。", direct_response) - + return ActionResponse(Action.REQLLM, "抱歉,未能获取到新闻信息,请稍后再试。", None) + # 随机选择一条新闻 selected_news = random.choice(news_items) - + # 保存当前新闻链接到连接对象,以便后续查询详情 if not hasattr(conn, 'last_news_link'): conn.last_news_link = {} @@ -193,7 +187,7 @@ def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_C 'link': selected_news.get('link', '#'), 'title': selected_news.get('title', '未知标题') } - + # 构建新闻报告 news_report = ( f"根据下列数据,用{lang}回应用户的新闻查询请求:\n\n" @@ -204,17 +198,9 @@ def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_C f"直接读出新闻即可,不需要额外多余的内容。" f"如果用户询问更多详情,告知用户可以说'请详细介绍这条新闻'获取更多内容)" ) - - # 构造一个直接回复版本,简要播报新闻 - direct_response = ( - f"最新{mapped_category or ''}新闻:{selected_news['title']}。" - f"{selected_news['description'][:150]}..." - f"如果您想了解更多详情,可以说'请详细介绍这条新闻'。" - ) - - return ActionResponse(Action.REQLLM, news_report, direct_response) - + + return ActionResponse(Action.REQLLM, news_report, None) + except Exception as e: logger.bind(tag=TAG).error(f"获取新闻出错: {e}") - direct_response = "抱歉,获取新闻时发生错误,请稍后再试。" - return ActionResponse(Action.REQLLM, "抱歉,获取新闻时发生错误,请稍后再试。", direct_response) + return ActionResponse(Action.REQLLM, "抱歉,获取新闻时发生错误,请稍后再试。", None) \ No newline at end of file diff --git a/main/xiaozhi-server/plugins_func/functions/get_weather.py b/main/xiaozhi-server/plugins_func/functions/get_weather.py index 58f63844..060274d9 100644 --- a/main/xiaozhi-server/plugins_func/functions/get_weather.py +++ b/main/xiaozhi-server/plugins_func/functions/get_weather.py @@ -2,7 +2,6 @@ import requests from bs4 import BeautifulSoup from config.logger import setup_logging from plugins_func.register import register_function, ToolType, ActionResponse, Action -import json TAG = __name__ logger = setup_logging() @@ -40,158 +39,85 @@ HEADERS = { ) } +# 天气代码 https://dev.qweather.com/docs/resource/icons/#weather-icons +WEATHER_CODE_MAP = { + "100": "晴", "101": "多云", "102": "少云", "103": "晴间多云", "104": "阴", + "150": "晴", "151": "多云", "152": "少云", "153": "晴间多云", + "300": "阵雨", "301": "强阵雨", "302": "雷阵雨", "303": "强雷阵雨", "304": "雷阵雨伴有冰雹", + "305": "小雨", "306": "中雨", "307": "大雨", "308": "极端降雨", "309": "毛毛雨/细雨", + "310": "暴雨", "311": "大暴雨", "312": "特大暴雨", "313": "冻雨", "314": "小到中雨", + "315": "中到大雨", "316": "大到暴雨", "317": "暴雨到大暴雨", "318": "大暴雨到特大暴雨", + "350": "阵雨", "351": "强阵雨", "399": "雨", + "400": "小雪", "401": "中雪", "402": "大雪", "403": "暴雪", "404": "雨夹雪", + "405": "雨雪天气", "406": "阵雨夹雪", "407": "阵雪", "408": "小到中雪", "409": "中到大雪", "410": "大到暴雪", + "456": "阵雨夹雪", "457": "阵雪", "499": "雪", + "500": "薄雾", "501": "雾", "502": "霾", "503": "扬沙", "504": "浮尘", + "507": "沙尘暴", "508": "强沙尘暴", + "509": "浓雾", "510": "强浓雾", "511": "中度霾", "512": "重度霾", "513": "严重霾", "514": "大雾", "515": "特强浓雾", + "900": "热", "901": "冷", "999": "未知" +} def fetch_city_info(location, api_key): url = f"https://geoapi.qweather.com/v2/city/lookup?key={api_key}&location={location}&lang=zh" - logger.bind(tag=TAG).info(f"正在请求城市信息API,URL: {url}") - try: - response = requests.get(url, headers=HEADERS) - logger.bind(tag=TAG).info(f"城市信息API响应状态码: {response.status_code}") - json_data = response.json() - logger.bind(tag=TAG).debug(f"城市信息API响应: {json.dumps(json_data, ensure_ascii=False)}") - return json_data.get('location', [])[0] if json_data.get('location') else None - except Exception as e: - logger.bind(tag=TAG).error(f"获取城市信息失败: {str(e)}") - return None + response = requests.get(url, headers=HEADERS).json() + return response.get('location', [])[0] if response.get('location') else None def fetch_weather_page(url): - logger.bind(tag=TAG).info(f"正在请求天气页面,URL: {url}") - try: - response = requests.get(url, headers=HEADERS) - logger.bind(tag=TAG).info(f"天气页面响应状态码: {response.status_code}") - if not response.ok: - logger.bind(tag=TAG).error(f"获取天气页面失败: {response.text[:200]}") - return BeautifulSoup(response.text, "html.parser") if response.ok else None - except Exception as e: - logger.bind(tag=TAG).error(f"获取天气页面失败: {str(e)}") - return None + response = requests.get(url, headers=HEADERS) + return BeautifulSoup(response.text, "html.parser") if response.ok else None def parse_weather_info(soup): - try: - city_name = soup.select_one("h1.c-submenu__location").get_text(strip=True) - logger.bind(tag=TAG).info(f"解析到城市名称: {city_name}") + city_name = soup.select_one("h1.c-submenu__location").get_text(strip=True) - current_abstract = soup.select_one(".c-city-weather-current .current-abstract") - current_abstract = current_abstract.get_text(strip=True) if current_abstract else "未知" - logger.bind(tag=TAG).info(f"当前天气概况: {current_abstract}") + current_abstract = soup.select_one(".c-city-weather-current .current-abstract") + current_abstract = current_abstract.get_text(strip=True) if current_abstract else "未知" - current_basic = {} - for item in soup.select(".c-city-weather-current .current-basic .current-basic___item"): - parts = item.get_text(strip=True, separator=" ").split(" ") - if len(parts) == 2: - key, value = parts[1], parts[0] - current_basic[key] = value - logger.bind(tag=TAG).info(f"当前天气详情: {json.dumps(current_basic, ensure_ascii=False)}") + current_basic = {} + for item in soup.select(".c-city-weather-current .current-basic .current-basic___item"): + parts = item.get_text(strip=True, separator=" ").split(" ") + if len(parts) == 2: + key, value = parts[1], parts[0] + current_basic[key] = value - temps_list = [] - for row in soup.select(".city-forecast-tabs__row")[:7]: # 取前7天的数据 - date = row.select_one(".date-bg .date").get_text(strip=True) - temps = [span.get_text(strip=True) for span in row.select(".tmp-cont .temp")] - high_temp, low_temp = (temps[0], temps[-1]) if len(temps) >= 2 else (None, None) - temps_list.append((date, high_temp, low_temp)) - logger.bind(tag=TAG).info(f"获取到未来7天温度: {temps_list}") + temps_list = [] + for row in soup.select(".city-forecast-tabs__row")[:7]: # 取前7天的数据 + date = row.select_one(".date-bg .date").get_text(strip=True) + weather_code = row.select_one(".date-bg .icon")["src"].split("/")[-1].split(".")[0] + weather = WEATHER_CODE_MAP.get(weather_code, "未知") + temps = [span.get_text(strip=True) for span in row.select(".tmp-cont .temp")] + high_temp, low_temp = (temps[0], temps[-1]) if len(temps) >= 2 else (None, None) + temps_list.append((date, weather, high_temp, low_temp)) - return city_name, current_abstract, current_basic, temps_list - except Exception as e: - logger.bind(tag=TAG).error(f"解析天气信息失败: {str(e)}") - return "未知城市", "未知天气", {}, [] + return city_name, current_abstract, current_basic, temps_list @register_function('get_weather', GET_WEATHER_FUNCTION_DESC, ToolType.SYSTEM_CTL) def get_weather(conn, location: str = None, lang: str = "zh_CN"): - logger.bind(tag=TAG).info(f"===== 天气查询函数开始执行 =====") - logger.bind(tag=TAG).info(f"查询参数: location={location}, lang={lang}") - - try: - api_key = conn.config["plugins"]["get_weather"]["api_key"] - default_location = conn.config["plugins"]["get_weather"]["default_location"] - - # 位置参数处理逻辑优化 - # 1. 如果传入了明确的位置参数且不为None/空字符串,则使用该参数 - # 2. 否则,尝试使用客户端IP地址对应的城市信息 - # 3. 如果上述都无效,则使用配置文件中的默认位置 - if location and location.strip(): - final_location = location.strip() - logger.bind(tag=TAG).info(f"使用用户明确指定的位置: {final_location}") - elif conn.client_ip_info and "city" in conn.client_ip_info and conn.client_ip_info["city"]: - final_location = conn.client_ip_info["city"] - logger.bind(tag=TAG).info(f"使用IP地址解析的位置: {final_location}") - else: - final_location = default_location - logger.bind(tag=TAG).info(f"使用配置文件中的默认位置: {final_location}") - - logger.bind(tag=TAG).info(f"最终使用的查询地点: {final_location}, API Key: {api_key}") + api_key = conn.config["plugins"]["get_weather"]["api_key"] + default_location = conn.config["plugins"]["get_weather"]["default_location"] + location = location or conn.client_ip_info.get("city") or default_location + logger.bind(tag=TAG).debug(f"获取天气: {location}") - city_info = fetch_city_info(final_location, api_key) - if not city_info: - error_msg = f"未找到相关的城市: {final_location},请确认地点是否正确" - logger.bind(tag=TAG).error(error_msg) - # 构造一个直接回复 - direct_response = f"抱歉,我未能找到\"{final_location}\"的天气信息,请确认地点名称是否正确,或者尝试查询其他城市的天气。" - return ActionResponse(Action.RESPONSE, None, direct_response) # 使用RESPONSE动作直接返回 + city_info = fetch_city_info(location, api_key) + if not city_info: + return ActionResponse(Action.REQLLM, f"未找到相关的城市: {location},请确认地点是否正确", None) - logger.bind(tag=TAG).info(f"获取到城市信息: {json.dumps(city_info, ensure_ascii=False)}") - logger.bind(tag=TAG).info(f"天气查询链接: {city_info['fxLink']}") - - soup = fetch_weather_page(city_info['fxLink']) - if not soup: - error_msg = "请求天气页面失败" - logger.bind(tag=TAG).error(error_msg) - direct_response = f"抱歉,在获取{final_location}的天气信息时遇到了问题,请稍后再试。" - return ActionResponse(Action.RESPONSE, None, direct_response) # 使用RESPONSE动作直接返回 + soup = fetch_weather_page(city_info['fxLink']) + if not soup: + return ActionResponse(Action.REQLLM, None, "请求失败") - city_name, current_abstract, current_basic, temps_list = parse_weather_info(soup) - - # 构建天气报告(给LLM使用的详细数据) - weather_report = f"根据下列数据,用{lang}回应用户的查询天气请求:\n{city_name}未来7天天气:\n" - for i, (date, high, low) in enumerate(temps_list): - if high and low: - weather_report += f"{date}: {low}到{high}\n" - weather_report += ( - f"当前天气: {current_abstract}\n" - f"当前天气参数: {current_basic}\n" - f"(确保只报告指定单日的气温范围,除非用户明确要求想要了解多日天气,如果未指定,默认报告今天的温度范围。" - "参数为0的值不需要报告给用户,每次都报告体感温度,根据语境选择合适的参数内容告知用户,并对参数给出相应评价)" - ) - - # 同时构建一个直接可用的人性化天气回复 - # 这用作备用,防止LLM处理失败时能够直接回复用户 - today_temp = "" - for date, high, low in temps_list: - if "今天" in date or "今日" in date: - today_temp = f"{low}到{high}" - break - if not today_temp and temps_list: - today_temp = f"{temps_list[0][2]}到{temps_list[0][1]}" - - feel_temp = current_basic.get("体感温度", "") - humidity = current_basic.get("相对湿度", "") - - direct_response = f"{city_name}今天{current_abstract},温度{today_temp}。" - if feel_temp: - direct_response += f"体感温度{feel_temp}。" - if humidity: - direct_response += f"相对湿度{humidity}。" - - # 添加简单的建议 - if "雨" in current_abstract: - direct_response += "外出请记得带伞。" - elif temps_list and temps_list[0][1] and int(temps_list[0][1].replace("°", "")) > 30: - direct_response += "天气炎热,注意防暑。" - elif temps_list and temps_list[0][2] and int(temps_list[0][2].replace("°", "")) < 10: - direct_response += "天气较冷,注意保暖。" - - logger.bind(tag=TAG).info(f"构建的天气报告: {weather_report}") - logger.bind(tag=TAG).info(f"备用直接回复: {direct_response}") - logger.bind(tag=TAG).info(f"===== 天气查询函数执行完毕,返回结果 =====") - - # 返回详细天气报告,但添加direct_response作为备用 - return ActionResponse(Action.REQLLM, weather_report, direct_response) - except Exception as e: - error_msg = f"查询天气时发生错误: {str(e)}" - logger.bind(tag=TAG).error(error_msg) - # 出错时也提供一个直接回复 - direct_response = f"抱歉,在获取天气信息时遇到了技术问题,请稍后再试。" - return ActionResponse(Action.RESPONSE, None, direct_response) # 使用RESPONSE动作直接返回 + city_name, current_abstract, current_basic, temps_list = parse_weather_info(soup) + weather_report = f"根据下列数据,用{lang}回应用户的查询天气请求:\n{city_name}未来7天天气:\n" + for i, (date, weather, high, low) in enumerate(temps_list): + if high and low: + weather_report += f"{date}: {low}到{high}, {weather}\n" + weather_report += ( + f"当前天气: {current_abstract}\n" + f"当前天气参数: {current_basic}\n" + f"(确保只报告指定单日的天气情况,除非未来会出现异常天气;或者用户明确要求想要了解多日天气,如果未指定,默认报告今天的天气。" + "参数为0的值不需要报告给用户,每次都报告体感温度,根据语境选择合适的参数内容告知用户,并对参数给出相应评价)" + ) + + return ActionResponse(Action.REQLLM, weather_report, None) \ No newline at end of file From 62be26ff47ae12bbc3a3f505d95c5272ed29a2bb Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Fri, 28 Mar 2025 18:47:20 +0800 Subject: [PATCH 28/28] =?UTF-8?q?update:0.1.16=E7=89=88=E6=9C=AC=20-=20?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0mcp=E6=9C=8D=E5=8A=A1=20-=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81Dify=E3=80=81Coze=E6=97=B6=E4=BD=BF=E7=94=A8=E7=8B=AC?= =?UTF-8?q?=E7=AB=8B=E7=9A=84=E6=84=8F=E5=9B=BE=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/config/logger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/xiaozhi-server/config/logger.py b/main/xiaozhi-server/config/logger.py index 89d991e3..e652e3ec 100644 --- a/main/xiaozhi-server/config/logger.py +++ b/main/xiaozhi-server/config/logger.py @@ -3,7 +3,7 @@ import sys from loguru import logger from config.settings import load_config -SERVER_VERSION = "0.1.15" +SERVER_VERSION = "0.1.16" def setup_logging(): """从配置文件中读取日志配置,并设置日志输出格式和级别"""