mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-27 01:23:55 +08:00
Function插件自动装载 (#351)
* 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: 欣南科技 <huangrongzhuang@xin-nan.com> * update:优化插件加载的配置提示 * update:增加自动安装脚本的操作说明 * update:优化插件配置,去掉旧版本的时间设定 --------- Co-authored-by: 玄凤科技 <eric230308@gmail.com> Co-authored-by: TinsFox <fox@tinsfox.com> Co-authored-by: hrz <1710360675@qq.com>
This commit is contained in:
Executable
+105
@@ -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 服务"
|
||||||
+59
-16
@@ -6,7 +6,45 @@ docker镜像已支持x86架构、arm64架构的CPU,支持在国产操作系统
|
|||||||
|
|
||||||
如果您的电脑还没安装docker,可以按照这里的教程安装:[docker安装](https://www.runoob.com/docker/ubuntu-docker-install.html)
|
如果您的电脑还没安装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`。
|
安装完后,你需要为这个项目找一个安放配置文件的目录,例如我们可以新建一个文件夹叫`xiaozhi-server`。
|
||||||
|
|
||||||
@@ -21,14 +59,18 @@ xiaozhi-server
|
|||||||
├─ SenseVoiceSmall
|
├─ 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)。
|
用浏览器打开[这个链接](../main/xiaozhi-server/docker-compose.yml)。
|
||||||
|
|
||||||
@@ -37,7 +79,7 @@ xiaozhi-server
|
|||||||
|
|
||||||
下载完后,回到本教程继续往下。
|
下载完后,回到本教程继续往下。
|
||||||
|
|
||||||
## 3. 下载配置文件
|
##### 1.2.3.2 下载 config.yaml
|
||||||
|
|
||||||
用浏览器打开[这个链接](../main/xiaozhi-server/config.yaml)。
|
用浏览器打开[这个链接](../main/xiaozhi-server/config.yaml)。
|
||||||
|
|
||||||
@@ -58,14 +100,14 @@ xiaozhi-server
|
|||||||
|
|
||||||
如果你的文件目录结构也是上面的,就继续往下。如果不是,你就再仔细看看是不是漏操作了什么。
|
如果你的文件目录结构也是上面的,就继续往下。如果不是,你就再仔细看看是不是漏操作了什么。
|
||||||
|
|
||||||
## 4. 配置项目文件
|
## 3. 配置项目文件
|
||||||
|
|
||||||
接下里,程序还不能直接运行,你需要配置一下,你到底使用的是什么模型。你可以看这个教程:
|
接下里,程序还不能直接运行,你需要配置一下,你到底使用的是什么模型。你可以看这个教程:
|
||||||
[跳转到配置项目文件](#配置项目)
|
[跳转到配置项目文件](#配置项目)
|
||||||
|
|
||||||
配置完项目文件后,回到本教程继续往下。
|
配置完项目文件后,回到本教程继续往下。
|
||||||
|
|
||||||
## 5. 执行docker命令
|
## 4. 执行docker命令
|
||||||
|
|
||||||
打开命令行工具,使用`终端`或`命令行`工具 进入到你的`xiaozhi-server`,执行以下命令
|
打开命令行工具,使用`终端`或`命令行`工具 进入到你的`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`文件不一定有。
|
请注意是对关键密钥逐个复制,不要直接覆盖。因为新的`.config.yaml`文件可能有一些新的配置项,旧的`.config.yaml`文件不一定有。
|
||||||
|
|
||||||
2、执行以下命令
|
5.2、执行以下命令
|
||||||
|
|
||||||
```
|
```
|
||||||
docker stop xiaozhi-esp32-server
|
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
|
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
|
||||||
```
|
```
|
||||||
|
|
||||||
3、重新按docker方式部署
|
5.3、重新按docker方式部署
|
||||||
|
|
||||||
# 方式二:借助Docker环境运行部署
|
# 方式二:借助Docker环境运行部署
|
||||||
|
|
||||||
@@ -217,20 +259,23 @@ python app.py
|
|||||||
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
|
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
|
||||||
如果你的`data`下面没有`.config.yaml`文件,你可以把源码目录下的`config.yaml`文件复制一份,重命名为`.config.yaml`
|
如果你的`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)注册密钥,才能启动。
|
- 默认的LLM使用的是`ChatGLMLLM`,你需要配置密钥,因为他们的模型,虽然有免费的,但是仍要去[官网](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)注册密钥,才能启动。
|
||||||
- 默认的记忆层`mem0ai`,你需要配置密钥,因为他们的API,虽然有免费额度,但是仍要去[官网](https://app.mem0.ai/dashboard/api-keys)注册密钥,才能启动。
|
|
||||||
|
|
||||||
配置说明:这里是各个功能使用的默认组件,例如LLM默认使用`ChatGLMLLM`模型。如果需要切换模型,就是改对应的名称。
|
配置说明:这里是各个功能使用的默认组件,例如LLM默认使用`ChatGLMLLM`模型。如果需要切换模型,就是改对应的名称。
|
||||||
本项目的默认配置仅是成本最低配置(`glm-4-flash`和`EdgeTTS`都是免费的),如果需要更优的更快的搭配,需要自己结合部署环境切换各组件的使用。
|
本项目的默认配置仅是成本最低配置(`glm-4-flash`和`EdgeTTS`都是免费的),如果需要更优的更快的搭配,需要自己结合部署环境切换各组件的使用。
|
||||||
|
|
||||||
```
|
```
|
||||||
selected_module:
|
selected_module:
|
||||||
ASR: FunASR
|
|
||||||
VAD: SileroVAD
|
VAD: SileroVAD
|
||||||
|
ASR: FunASR
|
||||||
LLM: ChatGLMLLM
|
LLM: ChatGLMLLM
|
||||||
TTS: EdgeTTS
|
TTS: EdgeTTS
|
||||||
|
# 默认不开启记忆,如需开启请看配置文件里的描述
|
||||||
|
Memory: nomem
|
||||||
|
# 默认不开启意图识别,如需开启请看配置文件里的描述
|
||||||
|
Intent: nointent
|
||||||
```
|
```
|
||||||
|
|
||||||
比如修改`LLM`使用的组件,就看本项目支持哪些`LLM` API接口,当前支持的是`openai`、`dify`。欢迎验证和支持更多LLM平台的接口。
|
比如修改`LLM`使用的组件,就看本项目支持哪些`LLM` API接口,当前支持的是`openai`、`dify`。欢迎验证和支持更多LLM平台的接口。
|
||||||
@@ -249,8 +294,6 @@ LLM:
|
|||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
有些服务,比如如果你使用`Dify`、`豆包的TTS`,是需要密钥的,记得在配置文件加上哦!
|
|
||||||
|
|
||||||
## 模型文件
|
## 模型文件
|
||||||
|
|
||||||
本项目语音识别模型,默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt`
|
本项目语音识别模型,默认使用`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)
|
[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)
|
||||||
@@ -51,7 +51,7 @@ xiaozhi:
|
|||||||
prompt: |
|
prompt: |
|
||||||
你是一个叫小智/小志的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。
|
你是一个叫小智/小志的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。
|
||||||
请注意,要像一个人一样说话,请不要回复表情符号、代码、和xml标签。
|
请注意,要像一个人一样说话,请不要回复表情符号、代码、和xml标签。
|
||||||
当前时间是:{date_time},现在我正在和你进行语音聊天,我们开始吧。
|
现在我正在和你进行语音聊天,我们开始吧。
|
||||||
如果用户希望结束对话,请在最后说“拜拜”或“再见”。
|
如果用户希望结束对话,请在最后说“拜拜”或“再见”。
|
||||||
# 使用完声音文件后删除文件(Delete the sound file when you are done using it)
|
# 使用完声音文件后删除文件(Delete the sound file when you are done using it)
|
||||||
delete_audio: true
|
delete_audio: true
|
||||||
@@ -87,14 +87,20 @@ selected_module:
|
|||||||
Intent:
|
Intent:
|
||||||
# 不使用意图识别
|
# 不使用意图识别
|
||||||
nointent:
|
nointent:
|
||||||
# 不需要动
|
# 不需要动type
|
||||||
type: nointent
|
type: nointent
|
||||||
intent_llm:
|
intent_llm:
|
||||||
# 不需要动
|
# 不需要动type
|
||||||
type: intent_llm
|
type: intent_llm
|
||||||
function_call:
|
function_call:
|
||||||
# 不需要动
|
# 不需要动type
|
||||||
type: nointent
|
type: nointent
|
||||||
|
# plugins_func/functions下的模块,可以通过配置,选择加载哪个模块,加载后对话支持相应的function调用
|
||||||
|
# 系统默认已经记载“handle_exit_intent(退出识别)”、“play_music(音乐播放)”插件,请勿重复加载
|
||||||
|
# 下面是加载查天气、角色切换的插件示例
|
||||||
|
functions:
|
||||||
|
- change_role
|
||||||
|
- get_weather
|
||||||
|
|
||||||
Memory:
|
Memory:
|
||||||
mem0ai:
|
mem0ai:
|
||||||
|
|||||||
@@ -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"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -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 concurrent.futures import ThreadPoolExecutor, TimeoutError
|
||||||
from core.handle.sendAudioHandle import sendAudioMessage
|
from core.handle.sendAudioHandle import sendAudioMessage
|
||||||
from core.handle.receiveAudioHandle import handleAudioMessage
|
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 config.private_config import PrivateConfig
|
||||||
from core.auth import AuthMiddleware, AuthenticationError
|
from core.auth import AuthMiddleware, AuthenticationError
|
||||||
from core.utils.auth_code_gen import AuthCodeGenerator
|
from core.utils.auth_code_gen import AuthCodeGenerator
|
||||||
|
import plugins_func.loadplugins
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
|
|
||||||
@@ -94,6 +96,8 @@ class ConnectionHandler:
|
|||||||
self.use_function_call_mode = False
|
self.use_function_call_mode = False
|
||||||
if self.config["selected_module"]["Intent"] == 'function_call':
|
if self.config["selected_module"]["Intent"] == 'function_call':
|
||||||
self.use_function_call_mode = True
|
self.use_function_call_mode = True
|
||||||
|
|
||||||
|
self.func_handler = FunctionHandler(self.config)
|
||||||
|
|
||||||
async def handle_connection(self, ws):
|
async def handle_connection(self, ws):
|
||||||
try:
|
try:
|
||||||
@@ -185,11 +189,14 @@ class ConnectionHandler:
|
|||||||
self.prompt = self.config["prompt"]
|
self.prompt = self.config["prompt"]
|
||||||
if self.private_config:
|
if self.private_config:
|
||||||
self.prompt = self.private_config.private_config.get("prompt", self.prompt)
|
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))
|
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):
|
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))
|
self.logger.bind(tag=TAG).debug(json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False))
|
||||||
return True
|
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}")
|
self.logger.bind(tag=TAG).debug(f"Chat with function calling start: {query}")
|
||||||
"""Chat with function calling for intent detection using streaming"""
|
"""Chat with function calling for intent detection using streaming"""
|
||||||
if self.isNeedAuth():
|
if self.isNeedAuth():
|
||||||
@@ -297,11 +304,12 @@ class ConnectionHandler:
|
|||||||
future = asyncio.run_coroutine_threadsafe(self._check_and_broadcast_auth_code(), self.loop)
|
future = asyncio.run_coroutine_threadsafe(self._check_and_broadcast_auth_code(), self.loop)
|
||||||
future.result()
|
future.result()
|
||||||
return True
|
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
|
# Define intent functions
|
||||||
functions = get_functions()
|
functions = self.func_handler.get_functions()
|
||||||
|
|
||||||
response_message = []
|
response_message = []
|
||||||
processed_chars = 0 # 跟踪已处理的字符位置
|
processed_chars = 0 # 跟踪已处理的字符位置
|
||||||
@@ -337,7 +345,7 @@ class ConnectionHandler:
|
|||||||
for response in llm_responses:
|
for response in llm_responses:
|
||||||
content, tools_call = response
|
content, tools_call = response
|
||||||
if content is not None and len(content)>0:
|
if content is not None and len(content)>0:
|
||||||
if len(response_message)<=0 and content=="```":
|
if len(response_message)<=0 and (content=="```" or "<tool_call>" in content):
|
||||||
tool_call_flag = True
|
tool_call_flag = True
|
||||||
|
|
||||||
if tools_call is not None:
|
if tools_call is not None:
|
||||||
@@ -385,7 +393,38 @@ class ConnectionHandler:
|
|||||||
self.tts_queue.put(future)
|
self.tts_queue.put(future)
|
||||||
processed_chars += len(segment_text_raw) # 更新已处理字符位置
|
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)
|
full_text = "".join(response_message)
|
||||||
remaining_text = full_text[processed_chars:]
|
remaining_text = full_text[processed_chars:]
|
||||||
if remaining_text:
|
if remaining_text:
|
||||||
@@ -400,27 +439,6 @@ class ConnectionHandler:
|
|||||||
if len(response_message)>0:
|
if len(response_message)>0:
|
||||||
self.dialogue.put(Message(role="assistant", content="".join(response_message)))
|
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.llm_finish_task = True
|
||||||
self.logger.bind(tag=TAG).debug(json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False))
|
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.tts_queue.put(future)
|
||||||
self.dialogue.put(Message(role="assistant", content=text))
|
self.dialogue.put(Message(role="assistant", content=text))
|
||||||
if result.action == Action.REQLLM: # 调用函数后再请求llm生成回复
|
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:
|
if result.action == Action.NOTFOUND:
|
||||||
text = result.response
|
text = result.response
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,80 +1,12 @@
|
|||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
import json
|
import json
|
||||||
from core.handle.sendAudioHandle import send_stt_message
|
from core.handle.sendAudioHandle import send_stt_message
|
||||||
from core.utils.dialogue import Message
|
|
||||||
from core.utils.util import remove_punctuation_and_length
|
from core.utils.util import remove_punctuation_and_length
|
||||||
from config.functionCallConfig import FunctionCallConfig
|
|
||||||
import asyncio
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
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):
|
async def handle_user_intent(conn, text):
|
||||||
"""
|
"""
|
||||||
Handle user intent before starting chat
|
Handle user intent before starting chat
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ from datetime import datetime
|
|||||||
|
|
||||||
|
|
||||||
class Message:
|
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.uniq_id = uniq_id if uniq_id is not None else str(uuid.uuid4())
|
||||||
self.role = role
|
self.role = role
|
||||||
self.content = content
|
self.content = content
|
||||||
|
self.tool_calls = tool_calls
|
||||||
|
self.tool_call_id = tool_call_id
|
||||||
|
|
||||||
|
|
||||||
class Dialogue:
|
class Dialogue:
|
||||||
@@ -19,10 +21,18 @@ class Dialogue:
|
|||||||
def put(self, message: Message):
|
def put(self, message: Message):
|
||||||
self.dialogue.append(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]]:
|
def get_llm_dialogue(self) -> List[Dict[str, str]]:
|
||||||
dialogue = []
|
dialogue = []
|
||||||
for m in self.dialogue:
|
for m in self.dialogue:
|
||||||
dialogue.append({"role": m.role, "content": m.content})
|
self.getMessages(m, dialogue)
|
||||||
return dialogue
|
return dialogue
|
||||||
|
|
||||||
def get_llm_dialogue_with_memory(self, memory_str: str = None) -> List[Dict[str, str]]:
|
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})
|
dialogue.append({"role": "system", "content": enhanced_system_prompt})
|
||||||
|
|
||||||
# 添加用户和助手的对话
|
# 添加用户和助手的对话
|
||||||
for msg in self.dialogue:
|
for m in self.dialogue:
|
||||||
if msg.role != "system": # 跳过原始的系统消息
|
if m.role != "system": # 跳过原始的系统消息
|
||||||
dialogue.append({"role": msg.role, "content": msg.content})
|
self.getMessages(m, dialogue)
|
||||||
|
|
||||||
return dialogue
|
return dialogue
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
from datetime import datetime
|
||||||
import yaml
|
import yaml
|
||||||
import socket
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class WebSocketServer:
|
|||||||
host = server_config["ip"]
|
host = server_config["ip"]
|
||||||
port = server_config["port"]
|
port = server_config["port"]
|
||||||
selected_module = self.config.get("selected_module")
|
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("Server is running at ws://{}:{}", get_local_ip(), port)
|
||||||
self.logger.bind(tag=TAG).info("=======上面的地址是websocket协议地址,请勿用浏览器访问=======")
|
self.logger.bind(tag=TAG).info("=======上面的地址是websocket协议地址,请勿用浏览器访问=======")
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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="")
|
||||||
@@ -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}")
|
||||||
@@ -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)
|
||||||
@@ -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')
|
||||||
@@ -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()]
|
||||||
@@ -18,4 +18,5 @@ ruamel.yaml==0.18.10
|
|||||||
loguru==0.7.3
|
loguru==0.7.3
|
||||||
requests==2.32.3
|
requests==2.32.3
|
||||||
cozepy==0.12.0
|
cozepy==0.12.0
|
||||||
mem0ai==0.1.62
|
mem0ai==0.1.62
|
||||||
|
bs4==0.0.2
|
||||||
Reference in New Issue
Block a user