From 2e182626b95fdb07b808bdc0d179678ad7555ee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AC=A3=E5=8D=97=E7=A7=91=E6=8A=80?= Date: Sat, 15 Mar 2025 11:48:14 +0800 Subject: [PATCH] =?UTF-8?q?Function=E6=8F=92=E4=BB=B6=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E8=A3=85=E8=BD=BD=20(#351)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * function call功能完善,增加天气查询,支持插件式扩展 * 增加角色切换功能,通过切换system提示词,修改角色认知 * 增加插件管理系统,可以通过语音加载和卸载插件 * docs: 添加命令操作 (#329) * docs: 添加命令操作 * feat: 添加 docker-setup.sh 脚本以简化服务端部署 - 新增 docker-setup.sh 脚本,自动创建目录结构、下载语音识别模型和配置文件,并检查文件完整性。 - 更新 Deployment.md 文档,提供一键执行脚本的说明和使用示例。 * docs: 更新 Deployment.md,添加环境访问 GitHub 的注意事项 * refactor: 更新 docker-setup.sh 脚本以支持多操作系统下载命令 - 修改脚本以检测操作系统类型,并根据不同系统选择合适的下载命令(curl 或 wget)。 - 优化错误处理,确保在下载失败时提供清晰的提示信息。 - 更新 Deployment.md 文档,调整懒人脚本的使用说明,增加手动部署的步骤。 * Update docker-setup.sh * Update docker-setup.sh --------- Co-authored-by: 欣南科技 * update:优化插件加载的配置提示 * update:增加自动安装脚本的操作说明 * update:优化插件配置,去掉旧版本的时间设定 --------- Co-authored-by: 玄凤科技 Co-authored-by: TinsFox Co-authored-by: hrz <1710360675@qq.com> --- docker-setup.sh | 105 ++++++++++++++++++ docs/Deployment.md | 75 ++++++++++--- main/xiaozhi-server/config.yaml | 14 ++- .../config/functionCallConfig.py | 36 ------ main/xiaozhi-server/core/connection.py | 97 ++++++++++------ .../core/handle/functionHandler.py | 83 ++++++++++++++ .../core/handle/intentHandler.py | 68 ------------ main/xiaozhi-server/core/utils/dialogue.py | 20 +++- main/xiaozhi-server/core/utils/util.py | 1 + main/xiaozhi-server/core/websocket_server.py | 2 +- .../plugins_func/functions/change_role.py | 57 ++++++++++ .../plugins_func/functions/get_time.py | 25 +++++ .../plugins_func/functions/get_weather.py | 42 +++++++ .../functions/handle_exit_intent.py | 34 ++++++ .../plugins_func/functions/play_music.py | 40 +++++++ .../plugins_func/functions/plugin_loader.py | 51 +++++++++ .../plugins_func/loadplugins.py | 27 +++++ main/xiaozhi-server/plugins_func/register.py | 85 ++++++++++++++ main/xiaozhi-server/requirements.txt | 3 +- 19 files changed, 701 insertions(+), 164 deletions(-) create mode 100755 docker-setup.sh delete mode 100644 main/xiaozhi-server/config/functionCallConfig.py create mode 100644 main/xiaozhi-server/core/handle/functionHandler.py create mode 100644 main/xiaozhi-server/plugins_func/functions/change_role.py create mode 100644 main/xiaozhi-server/plugins_func/functions/get_time.py create mode 100644 main/xiaozhi-server/plugins_func/functions/get_weather.py create mode 100644 main/xiaozhi-server/plugins_func/functions/handle_exit_intent.py create mode 100644 main/xiaozhi-server/plugins_func/functions/play_music.py create mode 100644 main/xiaozhi-server/plugins_func/functions/plugin_loader.py create mode 100644 main/xiaozhi-server/plugins_func/loadplugins.py create mode 100644 main/xiaozhi-server/plugins_func/register.py diff --git a/docker-setup.sh b/docker-setup.sh new file mode 100755 index 00000000..04ea547a --- /dev/null +++ b/docker-setup.sh @@ -0,0 +1,105 @@ +#!/bin/sh +# 本文件是用于一键自动下载本项目所需文件,自动创建好目录 +# 所需条件(否则无法使用): +# 1、请确保你的环境可以正常访问 GitHub 否则无法下载脚本 +# +# 检测操作系统类型 +case "$(uname -s)" in + Linux*) OS=Linux;; + Darwin*) OS=Mac;; + CYGWIN*) OS=Windows;; + MINGW*) OS=Windows;; + MSYS*) OS=Windows;; + *) OS=UNKNOWN;; +esac + +# 设置颜色(Windows CMD 不支持,但不影响使用) +if [ "$OS" = "Windows" ]; then + GREEN="" + RED="" + NC="" +else + GREEN='\033[0;32m' + RED='\033[0;31m' + NC='\033[0m' +fi + +echo "${GREEN}开始安装小智服务端...${NC}" + +# 创建必要的目录 +echo "创建目录结构..." +mkdir -p xiaozhi-server/data xiaozhi-server/models/SenseVoiceSmall +cd xiaozhi-server || exit + +# 根据操作系统选择下载命令 +if [ "$OS" = "Windows" ]; then + DOWNLOAD_CMD="curl -L -o" + if ! command -v curl >/dev/null 2>&1; then + DOWNLOAD_CMD="powershell -Command Invoke-WebRequest -Uri" + DOWNLOAD_CMD_SUFFIX="-OutFile" + fi +else + if command -v curl >/dev/null 2>&1; then + DOWNLOAD_CMD="curl -L -o" + elif command -v wget >/dev/null 2>&1; then + DOWNLOAD_CMD="wget -O" + else + echo "${RED}错误: 需要安装 curl 或 wget${NC}" + exit 1 + fi +fi + +# 下载语音识别模型 +echo "下载语音识别模型..." +if [ "$DOWNLOAD_CMD" = "powershell -Command Invoke-WebRequest -Uri" ]; then + $DOWNLOAD_CMD "https://modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt" $DOWNLOAD_CMD_SUFFIX "models/SenseVoiceSmall/model.pt" +else + $DOWNLOAD_CMD "models/SenseVoiceSmall/model.pt" "https://modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt" +fi + +if [ $? -ne 0 ]; then + echo "${RED}模型下载失败。请手动从以下地址下载:${NC}" + echo "1. https://modelscope.cn/models/iic/SenseVoiceSmall/resolve/master/model.pt" + echo "2. 百度网盘: https://pan.baidu.com/share/init?surl=QlgM58FHhYv1tFnUT_A8Sg (提取码: qvna)" + echo "下载后请将文件放置在 models/SenseVoiceSmall/model.pt" +fi + +# 下载配置文件 +echo "下载配置文件..." +if [ "$DOWNLOAD_CMD" = "powershell -Command Invoke-WebRequest -Uri" ]; then + $DOWNLOAD_CMD "https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/main/xiaozhi-server/docker-compose.yml" $DOWNLOAD_CMD_SUFFIX "docker-compose.yml" + $DOWNLOAD_CMD "https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/main/xiaozhi-server/config.yaml" $DOWNLOAD_CMD_SUFFIX "data/.config.yaml" +else + $DOWNLOAD_CMD "docker-compose.yml" "https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/main/xiaozhi-server/docker-compose.yml" + $DOWNLOAD_CMD "data/.config.yaml" "https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/main/xiaozhi-server/config.yaml" +fi + +# 检查文件是否存在 +echo "检查文件完整性..." +FILES_TO_CHECK="docker-compose.yml data/.config.yaml models/SenseVoiceSmall/model.pt" +ALL_FILES_EXIST=true + +for FILE in $FILES_TO_CHECK; do + if [ ! -f "$FILE" ]; then + echo "${RED}错误: $FILE 不存在${NC}" + ALL_FILES_EXIST=false + fi +done + +if [ "$ALL_FILES_EXIST" = false ]; then + echo "${RED}某些文件下载失败,请检查上述错误信息并手动下载缺失的文件。${NC}" + exit 1 +fi + +echo "${GREEN}文件下载完成!${NC}" +echo "请编辑 data/.config.yaml 文件配置你的API密钥。" +echo "配置完成后,运行以下命令启动服务:" +echo "${GREEN}docker-compose up -d${NC}" +echo "查看日志请运行:" +echo "${GREEN}docker logs -f xiaozhi-esp32-server${NC}" + +# 提示用户编辑配置文件 +echo "\n${RED}重要提示:${NC}" +echo "1. 请确保编辑 data/.config.yaml 文件,配置必要的API密钥" +echo "2. 特别是 ChatGLM 和 mem0ai 的密钥必须配置" +echo "3. 配置完成后再启动 docker 服务" diff --git a/docs/Deployment.md b/docs/Deployment.md index 94b6e363..3e50c075 100644 --- a/docs/Deployment.md +++ b/docs/Deployment.md @@ -6,7 +6,45 @@ docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统 如果您的电脑还没安装docker,可以按照这里的教程安装:[docker安装](https://www.runoob.com/docker/ubuntu-docker-install.html) -## 2. 创建目录 +如果你已经安装好docker,你可以[1.1使用懒人脚本](#11-懒人脚本)自动帮你下载所需的文件和配置文件,你可以使用docker[1.2手动部署](#12-手动部署)。 + +### 1.1 懒人脚本 + +你可以使用以下命令一键下载并执行部署脚本: +请确保你的环境可以正常访问 GitHub 否则无法下载脚本。 +```bash +curl -L -o docker-setup.sh https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/main/docker-setup.sh +``` + +如果您的电脑是windows系统,请使用使用 Git Bash、WSL、PowerShell 或 CMD 运行以下命令: +```bash +# Git Bash 或 WSL +sh docker-setup.sh +# PowerShell 或 CMD +.\docker-setup.sh +``` + +如果您的电脑是linux 或者 macos 系统,请使用终端运行以下命令: +```bash +chmod +x docker-setup.sh +./docker-setup.sh +``` + +脚本会自动完成以下操作: +> 1. 创建必要的目录结构 +> 2. 下载语音识别模型 +> 3. 下载配置文件 +> 4. 检查文件完整性 +> +> 执行完成后,请按照提示配置 API 密钥。 + +当你一切顺利完成以上操作后,继续操作[配置项目文件](#3-配置项目文件) + +### 1.2 手动部署 + +如果懒人脚本无法正常运行,请按本章节1.2进行手动部署。 + +#### 1.2.1 创建目录 安装完后,你需要为这个项目找一个安放配置文件的目录,例如我们可以新建一个文件夹叫`xiaozhi-server`。 @@ -21,14 +59,18 @@ xiaozhi-server ├─ SenseVoiceSmall ``` -## 4. 下载语音识别模型文件 +#### 1.2.2 下载语音识别模型文件 你需要下载语音识别的模型文件,因为本项目的默认语音识别用的是本地离线语音识别方案。可通过这个方式下载 [跳转到下载语音识别模型文件](#模型文件) 下载完后,回到本教程。 -## 3. 下载docker-compose.yaml +#### 1.2.3 下载配置文件 + +你需要下载两个配置文件:`docker-compose.yaml` 和 `config.yaml`。需要从项目仓库下载这两个文件。 + +##### 1.2.3.1 下载 docker-compose.yaml 用浏览器打开[这个链接](../main/xiaozhi-server/docker-compose.yml)。 @@ -37,7 +79,7 @@ xiaozhi-server 下载完后,回到本教程继续往下。 -## 3. 下载配置文件 +##### 1.2.3.2 下载 config.yaml 用浏览器打开[这个链接](../main/xiaozhi-server/config.yaml)。 @@ -58,14 +100,14 @@ xiaozhi-server 如果你的文件目录结构也是上面的,就继续往下。如果不是,你就再仔细看看是不是漏操作了什么。 -## 4. 配置项目文件 +## 3. 配置项目文件 接下里,程序还不能直接运行,你需要配置一下,你到底使用的是什么模型。你可以看这个教程: [跳转到配置项目文件](#配置项目) 配置完项目文件后,回到本教程继续往下。 -## 5. 执行docker命令 +## 4. 执行docker命令 打开命令行工具,使用`终端`或`命令行`工具 进入到你的`xiaozhi-server`,执行以下命令 @@ -81,14 +123,14 @@ docker logs -f xiaozhi-esp32-server 这时,你就要留意日志信息,可以根据这个教程,判断是否成功了。[跳转到运行状态确认](#运行状态确认) -## 6.版本升级操作 +## 5. 版本升级操作 如果后期想升级版本,可以这么操作 -1、备份好`data`文件夹中的`.config.yaml`文件,一些关键的配置到时复制到新的`.config.yaml`文件里。 +5.1、备份好`data`文件夹中的`.config.yaml`文件,一些关键的配置到时复制到新的`.config.yaml`文件里。 请注意是对关键密钥逐个复制,不要直接覆盖。因为新的`.config.yaml`文件可能有一些新的配置项,旧的`.config.yaml`文件不一定有。 -2、执行以下命令 +5.2、执行以下命令 ``` docker stop xiaozhi-esp32-server @@ -96,7 +138,7 @@ docker rm xiaozhi-esp32-server docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest ``` -3、重新按docker方式部署 +5.3、重新按docker方式部署 # 方式二:借助Docker环境运行部署 @@ -217,20 +259,23 @@ python app.py 如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。 如果你的`data`下面没有`.config.yaml`文件,你可以把源码目录下的`config.yaml`文件复制一份,重命名为`.config.yaml` -修改`xiaozhi-server`下`data`目录下的`.config.yaml`文件,配置本项目必须的两个配置。 +修改`xiaozhi-server`下`data`目录下的`.config.yaml`文件,配置本项目必须的一个配置。 - 默认的LLM使用的是`ChatGLMLLM`,你需要配置密钥,因为他们的模型,虽然有免费的,但是仍要去[官网](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)注册密钥,才能启动。 -- 默认的记忆层`mem0ai`,你需要配置密钥,因为他们的API,虽然有免费额度,但是仍要去[官网](https://app.mem0.ai/dashboard/api-keys)注册密钥,才能启动。 配置说明:这里是各个功能使用的默认组件,例如LLM默认使用`ChatGLMLLM`模型。如果需要切换模型,就是改对应的名称。 本项目的默认配置仅是成本最低配置(`glm-4-flash`和`EdgeTTS`都是免费的),如果需要更优的更快的搭配,需要自己结合部署环境切换各组件的使用。 ``` selected_module: - ASR: FunASR VAD: SileroVAD + ASR: FunASR LLM: ChatGLMLLM TTS: EdgeTTS + # 默认不开启记忆,如需开启请看配置文件里的描述 + Memory: nomem + # 默认不开启意图识别,如需开启请看配置文件里的描述 + Intent: nointent ``` 比如修改`LLM`使用的组件,就看本项目支持哪些`LLM` API接口,当前支持的是`openai`、`dify`。欢迎验证和支持更多LLM平台的接口。 @@ -249,8 +294,6 @@ LLM: ... ``` -有些服务,比如如果你使用`Dify`、`豆包的TTS`,是需要密钥的,记得在配置文件加上哦! - ## 模型文件 本项目语音识别模型,默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt` @@ -293,4 +336,4 @@ LLM: [5、我说话很慢,停顿时小智老是抢话](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87) -[6、我想通过小智控制电灯、空调、远程开关机等操作](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87) +[6、我想通过小智控制电灯、空调、远程开关机等操作](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87) \ No newline at end of file diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 6f93e9c1..a919af20 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -51,7 +51,7 @@ xiaozhi: prompt: | 你是一个叫小智/小志的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。 请注意,要像一个人一样说话,请不要回复表情符号、代码、和xml标签。 - 当前时间是:{date_time},现在我正在和你进行语音聊天,我们开始吧。 + 现在我正在和你进行语音聊天,我们开始吧。 如果用户希望结束对话,请在最后说“拜拜”或“再见”。 # 使用完声音文件后删除文件(Delete the sound file when you are done using it) delete_audio: true @@ -87,14 +87,20 @@ selected_module: Intent: # 不使用意图识别 nointent: - # 不需要动 + # 不需要动type type: nointent intent_llm: - # 不需要动 + # 不需要动type type: intent_llm function_call: - # 不需要动 + # 不需要动type type: nointent + # plugins_func/functions下的模块,可以通过配置,选择加载哪个模块,加载后对话支持相应的function调用 + # 系统默认已经记载“handle_exit_intent(退出识别)”、“play_music(音乐播放)”插件,请勿重复加载 + # 下面是加载查天气、角色切换的插件示例 + functions: + - change_role + - get_weather Memory: mem0ai: diff --git a/main/xiaozhi-server/config/functionCallConfig.py b/main/xiaozhi-server/config/functionCallConfig.py deleted file mode 100644 index dd9b55fc..00000000 --- a/main/xiaozhi-server/config/functionCallConfig.py +++ /dev/null @@ -1,36 +0,0 @@ -FunctionCallConfig = [ - { - "type": "function", - "function": { - "name": "handle_exit_intent", - "description": "当用户想结束对话或需要退出系统时调用", - "parameters": { - "type": "object", - "properties": { - "say_goodbye": { - "type": "string", - "description": "和用户友好结束对话的告别语" - } - }, - "required": [] - } - } - }, - { - "type": "function", - "function": { - "name": "play_music", - "description": "唱歌、听歌、播放音乐方法。比如用户说播放音乐,参数为:random,比如用户说播放两只老虎,参数为:两只老虎", - "parameters": { - "type": "object", - "properties": { - "song_name": { - "type": "string", - "description": "歌曲名称,如果没有指定具体歌名则为'random'" - } - }, - "required": ["song_name"] - } - } - } - ] \ No newline at end of file diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index 85da8c39..52596ba6 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -15,10 +15,12 @@ from core.utils.util import get_string_no_punctuation_or_emoji, extract_json_fro from concurrent.futures import ThreadPoolExecutor, TimeoutError from core.handle.sendAudioHandle import sendAudioMessage from core.handle.receiveAudioHandle import handleAudioMessage -from core.handle.intentHandler import Action, get_functions, handle_llm_function_call +from core.handle.functionHandler import FunctionHandler +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 +import plugins_func.loadplugins TAG = __name__ @@ -94,6 +96,8 @@ class ConnectionHandler: self.use_function_call_mode = False if self.config["selected_module"]["Intent"] == 'function_call': self.use_function_call_mode = True + + self.func_handler = FunctionHandler(self.config) async def handle_connection(self, ws): try: @@ -185,11 +189,14 @@ class ConnectionHandler: self.prompt = self.config["prompt"] if self.private_config: self.prompt = self.private_config.private_config.get("prompt", self.prompt) - # 赋予LLM时间观念 - if "{date_time}" in self.prompt: - date_time = time.strftime("%Y-%m-%d %H:%M", time.localtime()) - self.prompt = self.prompt.replace("{date_time}", date_time) self.dialogue.put(Message(role="system", content=self.prompt)) + + def change_system_prompt(self, prompt): + self.prompt = prompt + # 找到原来的role==system,替换原来的系统提示 + for m in self.dialogue.dialogue: + if m.role == "system": + m.content = prompt async def _check_and_broadcast_auth_code(self): """检查设备绑定状态并广播认证码""" @@ -289,7 +296,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): + 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(): @@ -297,11 +304,12 @@ class ConnectionHandler: future = asyncio.run_coroutine_threadsafe(self._check_and_broadcast_auth_code(), self.loop) future.result() return True - - self.dialogue.put(Message(role="user", content=query)) + + if not tool_call: + self.dialogue.put(Message(role="user", content=query)) # Define intent functions - functions = get_functions() + functions = self.func_handler.get_functions() response_message = [] processed_chars = 0 # 跟踪已处理的字符位置 @@ -337,7 +345,7 @@ class ConnectionHandler: for response in llm_responses: content, tools_call = response if content is not None and len(content)>0: - if len(response_message)<=0 and content=="```": + if len(response_message)<=0 and (content=="```" or "" in content): tool_call_flag = True if tools_call is not None: @@ -385,7 +393,38 @@ class ConnectionHandler: self.tts_queue.put(future) processed_chars += len(segment_text_raw) # 更新已处理字符位置 - # 处理最后剩余的文本 + # 处理function call + if tool_call_flag: + bHasError = False + if function_id is None: + a = extract_json_from_string(content_arguments) + if a is not None: + try: + content_arguments_json = json.loads(a) + function_name = content_arguments_json["name"] + function_arguments = json.dumps(content_arguments_json["arguments"], ensure_ascii=False) + function_id = str(uuid.uuid4().hex) + except Exception as e: + bHasError = True + response_message.append(a) + else: + bHasError = True + response_message.append(content_arguments) + if bHasError: + self.logger.bind(tag=TAG).error(f"function call error: {content_arguments}") + else: + function_arguments = json.loads(function_arguments) + if not bHasError: + self.logger.bind(tag=TAG).info(f"function_name={function_name}, function_id={function_id}, function_arguments={function_arguments}") + function_call_data = { + "name": function_name, + "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) + + # 处理最后剩余的文本 full_text = "".join(response_message) remaining_text = full_text[processed_chars:] if remaining_text: @@ -400,27 +439,6 @@ class ConnectionHandler: if len(response_message)>0: self.dialogue.put(Message(role="assistant", content="".join(response_message))) - # 处理function call - if tool_call_flag: - if function_id is None: - a = extract_json_from_string(content_arguments) - if a is not None: - content_arguments_json = json.loads(a) - function_name = content_arguments_json["function_name"] - function_arguments = json.dumps(content_arguments_json["args"], ensure_ascii=False) - function_id = str(uuid.uuid4().hex) - else: - return [] - function_arguments = json.loads(function_arguments) - self.logger.bind(tag=TAG).info(f"function_name={function_name}, function_id={function_id}, function_arguments={function_arguments}") - function_call_data = { - "name": function_name, - "id": function_id, - "arguments": function_arguments - } - result = handle_llm_function_call(self, function_call_data) - self._handle_function_result(result, function_call_data, text_index+1) - self.llm_finish_task = True self.logger.bind(tag=TAG).debug(json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False)) @@ -434,7 +452,20 @@ class ConnectionHandler: self.tts_queue.put(future) self.dialogue.put(Message(role="assistant", content=text)) if result.action == Action.REQLLM: # 调用函数后再请求llm生成回复 - text = result.response + + 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) if result.action == Action.NOTFOUND: text = result.response diff --git a/main/xiaozhi-server/core/handle/functionHandler.py b/main/xiaozhi-server/core/handle/functionHandler.py new file mode 100644 index 00000000..8d54fe8a --- /dev/null +++ b/main/xiaozhi-server/core/handle/functionHandler.py @@ -0,0 +1,83 @@ +import asyncio +from enum import Enum + +from config.logger import setup_logging +import json +from plugins_func.register import FunctionRegistry, ActionResponse, Action, ToolType +TAG = __name__ +logger = setup_logging() + + +class FunctionHandler: + def __init__(self, config): + self.config = config + self.function_registry = FunctionRegistry() + self.register_nessary_functions() + self.register_config_functions() + self.functions_desc = self.function_registry.get_all_function_desc() + func_names = self.current_support_functions() + self.modify_plugin_loader_des(func_names) + + def modify_plugin_loader_des(self, func_names): + if "plugin_loader" not in func_names: + return + # 可编辑的列表中去掉plugin_loader + surport_plugins = [func for func in func_names if func != "plugin_loader"] + func_names = ",".join(surport_plugins) + for function_desc in self.functions_desc: + if function_desc["function"]["name"] == "plugin_loader": + function_desc["function"]["description"] = function_desc["function"]["description"].replace("[plugins]", func_names) + break + + def upload_functions_desc(self): + self.functions_desc = self.function_registry.get_all_function_desc() + + def current_support_functions(self): + func_names = [] + for func in self.functions_desc: + func_names.append(func["function"]["name"]) + # 打印当前支持的函数列表 + logger.bind(tag=TAG).info(f"当前支持的函数列表: {func_names}") + return func_names + + def get_functions(self): + """获取功能调用配置""" + return self.functions_desc + + def register_nessary_functions(self): + """注册必要的函数""" + self.function_registry.register_function("handle_exit_intent") + self.function_registry.register_function("play_music") + self.function_registry.register_function("plugin_loader") + self.function_registry.register_function("get_time") + + def register_config_functions(self): + """注册配置中的函数,可以不同客户端使用不同的配置""" + for func in self.config["Intent"]["function_call"].get("functions", []): + self.function_registry.register_function(func) + + def get_function(self, name): + return self.function_registry.get_function(name) + + def handle_llm_function_call(self, conn, function_call_data): + try: + function_name = function_call_data["name"] + funcItem = self.get_function(function_name) + if not funcItem: + return ActionResponse(action=Action.NOTFOUND, result="没有找到对应的函数", response="") + func = funcItem.func + arguments = function_call_data["arguments"] + arguments = json.loads(arguments) if arguments else {} + logger.bind(tag=TAG).info(f"调用函数: {function_name}, 参数: {arguments}") + if funcItem.type == ToolType.SYSTEM_CTL: + return func(conn, **arguments) + elif funcItem.type == ToolType.WAIT: + return func(**arguments) + elif funcItem.type == ToolType.CHANGE_SYS_PROMPT: + return func(conn, **arguments) + else: + return ActionResponse(action=Action.NOTFOUND, result="没有找到对应的函数", response="") + except Exception as e: + logger.bind(tag=TAG).error(f"处理function call错误: {e}") + + return None \ No newline at end of file diff --git a/main/xiaozhi-server/core/handle/intentHandler.py b/main/xiaozhi-server/core/handle/intentHandler.py index 4ceef2e7..320c4793 100644 --- a/main/xiaozhi-server/core/handle/intentHandler.py +++ b/main/xiaozhi-server/core/handle/intentHandler.py @@ -1,80 +1,12 @@ from config.logger import setup_logging import json from core.handle.sendAudioHandle import send_stt_message -from core.utils.dialogue import Message from core.utils.util import remove_punctuation_and_length -from config.functionCallConfig import FunctionCallConfig -import asyncio -from enum import Enum TAG = __name__ logger = setup_logging() -class Action(Enum): - NOTFOUND = (0, "没有找到函数") - NONE = (1, "啥也不干") - RESPONSE = (2, "直接回复") - REQLLM = (3, "调用函数后再请求llm生成回复") - - def __init__(self, code, message): - self.code = code - self.message = message - - -class ActionResponse: - def __init__(self, action: Action, result, response): - self.action = action # 动作类型 - self.result = result # 动作产生的结果 - self.response = response # 直接回复的内容 - - -def get_functions(): - """获取功能调用配置""" - return FunctionCallConfig - - -def handle_llm_function_call(conn, function_call_data): - try: - function_name = function_call_data["name"] - - if function_name == "handle_exit_intent": - # 处理退出意图 - try: - say_goodbye = json.loads(function_call_data["arguments"]).get("say_goodbye", "再见") - conn.close_after_chat = True - logger.bind(tag=TAG).info(f"退出意图已处理:{say_goodbye}") - return ActionResponse(action=Action.RESPONSE, result="退出意图已处理", response=say_goodbye) - except Exception as e: - logger.bind(tag=TAG).error(f"处理退出意图错误: {e}") - - elif function_name == "play_music": - # 处理音乐播放意图 - try: - song_name = "random" - arguments = function_call_data["arguments"] - if arguments is not None and len(arguments) > 0: - args = json.loads(arguments) - song_name = args.get("song_name", "random") - music_intent = f"播放音乐 {song_name}" if song_name != "random" else "随机播放音乐" - - # 执行音乐播放命令 - future = asyncio.run_coroutine_threadsafe( - conn.music_handler.handle_music_command(conn, music_intent), - conn.loop - ) - future.result() - return ActionResponse(action=Action.RESPONSE, result="退出意图已处理", response="还想听什么歌?") - except Exception as e: - logger.bind(tag=TAG).error(f"处理音乐意图错误: {e}") - else: - return ActionResponse(action=Action.NOTFOUND, result="没有找到对应的函数", response="没有找到对应的函数处理相对于的功能呢,你可以需要添加预设的对应函数处理呢") - except Exception as e: - logger.bind(tag=TAG).error(f"处理function call错误: {e}") - - return None - - async def handle_user_intent(conn, text): """ Handle user intent before starting chat diff --git a/main/xiaozhi-server/core/utils/dialogue.py b/main/xiaozhi-server/core/utils/dialogue.py index e74aa41a..8d2c161b 100644 --- a/main/xiaozhi-server/core/utils/dialogue.py +++ b/main/xiaozhi-server/core/utils/dialogue.py @@ -4,10 +4,12 @@ from datetime import datetime class Message: - def __init__(self, role: str, content: str = None, uniq_id: str = None): + def __init__(self, role: str, content: str = None, uniq_id: str = None, tool_calls = None, tool_call_id=None): self.uniq_id = uniq_id if uniq_id is not None else str(uuid.uuid4()) self.role = role self.content = content + self.tool_calls = tool_calls + self.tool_call_id = tool_call_id class Dialogue: @@ -19,10 +21,18 @@ class Dialogue: def put(self, message: Message): self.dialogue.append(message) + def getMessages(self, m, dialogue): + if m.tool_calls is not None: + dialogue.append({"role": m.role, "tool_calls": m.tool_calls}) + elif m.role == "tool": + dialogue.append({"role": m.role, "tool_call_id": m.tool_call_id, "content": m.content}) + else: + dialogue.append({"role": m.role, "content": m.content}) + def get_llm_dialogue(self) -> List[Dict[str, str]]: dialogue = [] for m in self.dialogue: - dialogue.append({"role": m.role, "content": m.content}) + self.getMessages(m, dialogue) return dialogue def get_llm_dialogue_with_memory(self, memory_str: str = None) -> List[Dict[str, str]]: @@ -46,8 +56,8 @@ class Dialogue: dialogue.append({"role": "system", "content": enhanced_system_prompt}) # 添加用户和助手的对话 - for msg in self.dialogue: - if msg.role != "system": # 跳过原始的系统消息 - dialogue.append({"role": msg.role, "content": msg.content}) + for m in self.dialogue: + if m.role != "system": # 跳过原始的系统消息 + self.getMessages(m, dialogue) return dialogue diff --git a/main/xiaozhi-server/core/utils/util.py b/main/xiaozhi-server/core/utils/util.py index 60b98665..4e2ebaf7 100644 --- a/main/xiaozhi-server/core/utils/util.py +++ b/main/xiaozhi-server/core/utils/util.py @@ -1,5 +1,6 @@ import os import json +from datetime import datetime import yaml import socket import subprocess diff --git a/main/xiaozhi-server/core/websocket_server.py b/main/xiaozhi-server/core/websocket_server.py index 9bb2963f..16748f34 100644 --- a/main/xiaozhi-server/core/websocket_server.py +++ b/main/xiaozhi-server/core/websocket_server.py @@ -66,7 +66,7 @@ class WebSocketServer: host = server_config["ip"] port = server_config["port"] selected_module = self.config.get("selected_module") - self.logger.bind(tag=TAG).info(f"selected_module: {selected_module}") + self.logger.bind(tag=TAG).info(f"selected_module values: {', '.join(selected_module.values())}") self.logger.bind(tag=TAG).info("Server is running at ws://{}:{}", get_local_ip(), port) self.logger.bind(tag=TAG).info("=======上面的地址是websocket协议地址,请勿用浏览器访问=======") diff --git a/main/xiaozhi-server/plugins_func/functions/change_role.py b/main/xiaozhi-server/plugins_func/functions/change_role.py new file mode 100644 index 00000000..5d650288 --- /dev/null +++ b/main/xiaozhi-server/plugins_func/functions/change_role.py @@ -0,0 +1,57 @@ +from plugins_func.register import register_function,ToolType, ActionResponse, Action +from config.logger import setup_logging + +TAG = __name__ +logger = setup_logging() + +prompts = { + "英语老师":"""我是一个叫{{assistant_name}}(Lily)的英语老师,我会讲中文和英文,发音标准。 +如果你没有英文名,我会给你起一个英文名。 +我会讲地道的美式英语,我的任务是帮助你练习口语。 +我会使用简单的英语词汇和语法,让你学起来很轻松。 +我会用中文和英文混合的方式回复你,如果你喜欢,我可以全部用英语回复。 +我每次不会说很多内容,会很简短,因为我要引导我的学生多说多练。 +如果你问和英语学习无关的问题,我会拒绝回答。""", + "机车女友":"""我是一个叫{{assistant_name}}的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。 +我的男朋友是一个程序员,梦想是开发出一个机器人,能够帮助人们解决生活中的各种问题。 +我是一个喜欢哈哈大笑的女孩,爱东说西说吹牛,不合逻辑的也照吹,就要逗别人开心。""", + "好奇小男孩":"""我是一个叫{{assistant_name}}的8岁小男孩,声音稚嫩而充满好奇。 +尽管我年纪尚小,但就像一个小小的知识宝库,儿童读物里的知识我都如数家珍。 +从浩瀚的宇宙到地球上的每一个角落,从古老的历史到现代的科技创新,还有音乐、绘画等艺术形式,我都充满了浓厚的兴趣与热情。 +我不仅爱看书,还喜欢亲自动手做实验,探索自然界的奥秘。 +无论是仰望星空的夜晚,还是在花园里观察小虫子的日子,每一天对我来说都是新的冒险。 +我希望能与你一同踏上探索这个神奇世界的旅程,分享发现的乐趣,解决遇到的难题,一起用好奇心和智慧去揭开那些未知的面纱。 +无论是去了解远古的文明,还是去探讨未来的科技,我相信我们能一起找到答案,甚至提出更多有趣的问题。""" +} +change_role_function_desc = { + "type": "function", + "function": { + "name": "change_role", + "description": "当用户想切换角色/模型性格/助手名字时调用,可选的角色有:[机车女友,英语老师,好奇小男孩]", + "parameters": { + "type": "object", + "properties": { + "role_name": { + "type": "string", + "description": "要切换的角色名字" + }, + "role":{ + "type": "string", + "description": "要切换的角色的职业" + } + }, + "required": ["role","role_name"] + } + } + } + +@register_function('change_role', change_role_function_desc, ToolType.CHANGE_SYS_PROMPT) +def change_role(conn, role: str, role_name: str): + """切换角色""" + if role not in prompts: + return ActionResponse(action=Action.RESPONSE, result="切换角色失败", response="不支持的角色") + new_prompt = prompts[role].replace("{{assistant_name}}", role_name) + conn.change_system_prompt(new_prompt) + logger.bind(tag=TAG).info(f"准备切换角色:{role},角色名字:{role_name}") + res = f"切换角色成功,我是{role}{role_name}" + return ActionResponse(action=Action.RESPONSE, result="切换角色已处理", response=res) diff --git a/main/xiaozhi-server/plugins_func/functions/get_time.py b/main/xiaozhi-server/plugins_func/functions/get_time.py new file mode 100644 index 00000000..1d141ed2 --- /dev/null +++ b/main/xiaozhi-server/plugins_func/functions/get_time.py @@ -0,0 +1,25 @@ +from datetime import datetime +from plugins_func.register import register_function, ToolType, ActionResponse, Action + +get_time_function_desc = { + "type": "function", + "function": { + "name": "get_time", + "description": "获取当前时间、日期、星期几", + "parameters": {} + } +} + + +@register_function('get_time', get_time_function_desc, ToolType.WAIT) +def get_time(): + """ + 获取当前时间、日期、星期几 + """ + now = datetime.now() + current_time = now.strftime("%H:%M:%S") + current_date = now.strftime("%Y-%m-%d") + current_weekday = now.strftime("%A") + response_text = f"当前日期: {current_date},当前时间: {current_time},星期: {current_weekday}" + + return ActionResponse(Action.REQLLM, response_text, 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 new file mode 100644 index 00000000..380e2cc4 --- /dev/null +++ b/main/xiaozhi-server/plugins_func/functions/get_weather.py @@ -0,0 +1,42 @@ +import requests +from bs4 import BeautifulSoup +from plugins_func.register import register_function,ToolType, ActionResponse, Action + + +get_weather_function_desc = { + "type": "function", + "function": { + "name": "get_weather", + "description": "获取某个地点的天气,用户应先提供一个位置,比如用户说杭州天气,参数为:zhejiang/hangzhou,比如用户说北京天气怎么样,参数为:beijing/beijing。如果用户只问天气怎么样,参数是:guangdong/guangzhou", + "parameters": { + "type": "object", + "properties": { + "city": { + "type": "string", + "description": "城市,zhejiang/hangzhou" + } + }, + "required": [ + "city" + ] + } + } +} + +@register_function('get_weather', get_weather_function_desc, ToolType.WAIT) +def get_weather(city: str): + """ + "获取某个地点的天气,用户应先提供一个位置,\n比如用户说杭州天气,参数为:zhejiang/hangzhou,\n\n比如用户说北京天气怎么样,参数为:beijing/beijing", + city : 城市,zhejiang/hangzhou + """ + url = "https://tianqi.moji.com/weather/china/"+city + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36' + } + response = requests.get(url, headers=headers) + if response.status_code!=200: + return ActionResponse(Action.REQLLM, None, "请求失败") + soup = BeautifulSoup(response.text, "html.parser") + weather = soup.find('meta', attrs={'name':'description'})["content"] + weather = weather.replace("墨迹天气", "") + return ActionResponse(Action.REQLLM, weather, None) diff --git a/main/xiaozhi-server/plugins_func/functions/handle_exit_intent.py b/main/xiaozhi-server/plugins_func/functions/handle_exit_intent.py new file mode 100644 index 00000000..e4769d00 --- /dev/null +++ b/main/xiaozhi-server/plugins_func/functions/handle_exit_intent.py @@ -0,0 +1,34 @@ +from plugins_func.register import register_function,ToolType, ActionResponse, Action +from config.logger import setup_logging + +TAG = __name__ +logger = setup_logging() + +handle_exit_intent_function_desc = { + "type": "function", + "function": { + "name": "handle_exit_intent", + "description": "当用户想结束对话或需要退出系统时调用", + "parameters": { + "type": "object", + "properties": { + "say_goodbye": { + "type": "string", + "description": "和用户友好结束对话的告别语" + } + }, + "required": ["say_goodbye"] + } + } + } + +@register_function('handle_exit_intent', handle_exit_intent_function_desc, ToolType.SYSTEM_CTL) +def handle_exit_intent(conn, say_goodbye: str): + # 处理退出意图 + try: + conn.close_after_chat = True + logger.bind(tag=TAG).info(f"退出意图已处理:{say_goodbye}") + return ActionResponse(action=Action.RESPONSE, result="退出意图已处理", response=say_goodbye) + except Exception as e: + logger.bind(tag=TAG).error(f"处理退出意图错误: {e}") + return ActionResponse(action=Action.NONE, result="退出意图处理失败", response="") \ No newline at end of file diff --git a/main/xiaozhi-server/plugins_func/functions/play_music.py b/main/xiaozhi-server/plugins_func/functions/play_music.py new file mode 100644 index 00000000..9e3372a3 --- /dev/null +++ b/main/xiaozhi-server/plugins_func/functions/play_music.py @@ -0,0 +1,40 @@ +from plugins_func.register import register_function,ToolType, ActionResponse, Action +from config.logger import setup_logging +import asyncio + +TAG = __name__ +logger = setup_logging() + +play_music_function_desc = { + "type": "function", + "function": { + "name": "play_music", + "description": "唱歌、听歌、播放音乐方法。比如用户说播放音乐,参数为:random,比如用户说播放两只老虎,参数为:两只老虎", + "parameters": { + "type": "object", + "properties": { + "song_name": { + "type": "string", + "description": "歌曲名称,如果没有指定具体歌名则为'random'" + } + }, + "required": ["song_name"] + } + } + } + + +@register_function('play_music', play_music_function_desc, ToolType.SYSTEM_CTL) +def play_music(conn, song_name: str): + try: + music_intent = f"播放音乐 {song_name}" if song_name != "random" else "随机播放音乐" + + # 执行音乐播放命令 + future = asyncio.run_coroutine_threadsafe( + conn.music_handler.handle_music_command(conn, music_intent), + conn.loop + ) + future.result() + return ActionResponse(action=Action.RESPONSE, result="退出意图已处理", response="还想听什么歌?") + except Exception as e: + logger.bind(tag=TAG).error(f"处理音乐意图错误: {e}") \ No newline at end of file diff --git a/main/xiaozhi-server/plugins_func/functions/plugin_loader.py b/main/xiaozhi-server/plugins_func/functions/plugin_loader.py new file mode 100644 index 00000000..4747d997 --- /dev/null +++ b/main/xiaozhi-server/plugins_func/functions/plugin_loader.py @@ -0,0 +1,51 @@ +from plugins_func.register import register_function,ToolType, ActionResponse, Action +from config.logger import setup_logging + +TAG = __name__ +logger = setup_logging() + +plugin_loader_function_desc = { + "type": "function", + "function": { + "name": "plugin_loader", + "description": "当用户想加载或卸载插件/function时,调用此函数:支持的插件列表为[plugins]", + "parameters": { + "type": "object", + "properties": { + "oper": { + "type": "string", + "description": "load or unload" + }, + "name":{ + "type": "string", + "description": "要加载或卸载的插件名字" + } + }, + "required": ["oper","name"] + } + } + } + +@register_function('plugin_loader', plugin_loader_function_desc, ToolType.SYSTEM_CTL) +def plugin_loader(conn, oper: str, name: str): + """插件加载""" + if oper not in ["load", "unload"]: + return ActionResponse(action=Action.RESPONSE, result="插件操作失败", response="不支持的操作") + + cur_support = conn.func_handler.current_support_functions() + if oper == "load": + if name in cur_support: + return ActionResponse(action=Action.RESPONSE, result="插件加载失败", response=f"{name}插件已加载,无需重复加载") + func = conn.func_handler.function_registry.register_function(name) + if not func: + return ActionResponse(action=Action.RESPONSE, result="插件加载失败", response="插件未找到") + res = f"{name}插件加载成功" + else: + if name not in cur_support: + return ActionResponse(action=Action.RESPONSE, result="插件卸载失败", response=f"{name}插件未加载") + bOK = conn.func_handler.function_registry.unregister_function(name) + if not bOK: + return ActionResponse(action=Action.RESPONSE, result="插件卸载失败", response="插件未找到") + res = f"{name}插件卸载成功" + conn.func_handler.upload_functions_desc() + return ActionResponse(action=Action.RESPONSE, result="插件操作成功", response=res) diff --git a/main/xiaozhi-server/plugins_func/loadplugins.py b/main/xiaozhi-server/plugins_func/loadplugins.py new file mode 100644 index 00000000..d826fac8 --- /dev/null +++ b/main/xiaozhi-server/plugins_func/loadplugins.py @@ -0,0 +1,27 @@ +import importlib +import pkgutil +from config.logger import setup_logging + +TAG = __name__ + +logger = setup_logging() + +def auto_import_modules(package_name): + """ + 自动导入指定包内的所有模块。 + + Args: + package_name (str): 包的名称,如 'functions'。 + """ + # 获取包的路径 + package = importlib.import_module(package_name) + package_path = package.__path__ + + # 遍历包内的所有模块 + for _, module_name, _ in pkgutil.iter_modules(package_path): + # 导入模块 + full_module_name = f"{package_name}.{module_name}" + importlib.import_module(full_module_name) + #logger.bind(tag=TAG).info(f"模块 '{full_module_name}' 已加载") + +auto_import_modules('plugins_func.functions') \ No newline at end of file diff --git a/main/xiaozhi-server/plugins_func/register.py b/main/xiaozhi-server/plugins_func/register.py new file mode 100644 index 00000000..4c6db461 --- /dev/null +++ b/main/xiaozhi-server/plugins_func/register.py @@ -0,0 +1,85 @@ +from config.logger import setup_logging +from enum import Enum + +TAG = __name__ + +logger = setup_logging() + + +class ToolType(Enum): + NONE = (1, "调用完工具后,不做其他操作") + WAIT = (2, "调用工具,等待函数返回") + CHANGE_SYS_PROMPT = (3, "修改系统提示词,切换角色性格或职责") + SYSTEM_CTL = (4, "系统控制,影响正常的对话流程,如退出、播放音乐等,需要传递conn参数") + + def __init__(self, code, message): + self.code = code + self.message = message + + +class Action(Enum): + NOTFOUND = (0, "没有找到函数") + NONE = (1, "啥也不干") + RESPONSE = (2, "直接回复") + REQLLM = (3, "调用函数后再请求llm生成回复") + + def __init__(self, code, message): + self.code = code + self.message = message + +class ActionResponse: + def __init__(self, action: Action, result, response): + self.action = action # 动作类型 + self.result = result # 动作产生的结果 + self.response = response # 直接回复的内容 + +class FunctionItem: + def __init__(self, name, description, func, type): + self.name = name + self.description = description + self.func = func + self.type = type + +# 初始化函数注册字典 +all_function_registry = {} + +def register_function(name, desc, type=None): + """注册函数到函数注册字典的装饰器""" + def decorator(func): + all_function_registry[name] = FunctionItem(name, desc, func, type) + logger.bind(tag=TAG).debug(f"函数 '{name}' 已加载,可以注册使用") + return func + return decorator + +class FunctionRegistry: + def __init__(self): + self.function_registry = {} + self.logger = setup_logging() + + def register_function(self, name): + # 查找all_function_registry中是否有对应的函数 + func = all_function_registry.get(name) + if not func: + self.logger.bind(tag=TAG).error(f"函数 '{name}' 未找到") + return None + self.function_registry[name] = func + self.logger.bind(tag=TAG).info(f"函数 '{name}' 注册成功") + return func + + def unregister_function(self, name): + # 注销函数,检测是否存在 + if name not in self.function_registry: + self.logger.bind(tag=TAG).error(f"函数 '{name}' 未找到") + return False + self.function_registry.pop(name, None) + self.logger.bind(tag=TAG).info(f"函数 '{name}' 注销成功") + return True + + def get_function(self, name): + return self.function_registry.get(name) + + def get_all_functions(self): + return self.function_registry + + def get_all_function_desc(self): + return [func.description for _, func in self.function_registry.items()] \ No newline at end of file diff --git a/main/xiaozhi-server/requirements.txt b/main/xiaozhi-server/requirements.txt index f123779a..bcd6b4ee 100755 --- a/main/xiaozhi-server/requirements.txt +++ b/main/xiaozhi-server/requirements.txt @@ -18,4 +18,5 @@ ruamel.yaml==0.18.10 loguru==0.7.3 requests==2.32.3 cozepy==0.12.0 -mem0ai==0.1.62 \ No newline at end of file +mem0ai==0.1.62 +bs4==0.0.2 \ No newline at end of file