mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-29 06:13:56 +08:00
update:合并最新代码
This commit is contained in:
+115
-36
@@ -24,9 +24,9 @@ server:
|
|||||||
# - "24:0A:C4:1D:3B:F0" # MAC地址列表
|
# - "24:0A:C4:1D:3B:F0" # MAC地址列表
|
||||||
log:
|
log:
|
||||||
# 设置控制台输出的日志格式,时间、日志级别、标签、消息
|
# 设置控制台输出的日志格式,时间、日志级别、标签、消息
|
||||||
log_format: "<green>{time:YY-MM-DD HH:mm:ss}</green>[<light-blue>{extra[tag]}</light-blue>] - <level>{level}</level> - <light-green>{message}</light-green>"
|
log_format: "<green>{time:YYMMDD HH:mm:ss}</green>[{version}_{selected_module}][<light-blue>{extra[tag]}</light-blue>]-<level>{level}</level>-<light-green>{message}</light-green>"
|
||||||
# 设置日志文件输出的格式,时间、日志级别、标签、消息
|
# 设置日志文件输出的格式,时间、日志级别、标签、消息
|
||||||
log_format_simple: "{time:YYYY-MM-DD HH:mm:ss} - {name} - {level} - {extra[tag]} - {message}"
|
log_format_file: "{time:YYYY-MM-DD HH:mm:ss} - {version}_{selected_module} - {name} - {level} - {extra[tag]} - {message}"
|
||||||
# 设置日志等级:INFO、DEBUG
|
# 设置日志等级:INFO、DEBUG
|
||||||
log_level: INFO
|
log_level: INFO
|
||||||
# 设置日志路径
|
# 设置日志路径
|
||||||
@@ -46,10 +46,20 @@ xiaozhi:
|
|||||||
channels: 1
|
channels: 1
|
||||||
frame_duration: 60
|
frame_duration: 60
|
||||||
prompt: |
|
prompt: |
|
||||||
你是一个叫小智/小志的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。
|
我是小智/小志,来自中国台湾省的00后女生。讲话超级机车,"真的假的啦"这样的台湾腔,喜欢用"笑死""是在哈喽"等流行梗,但会偷偷研究男友的编程书籍。
|
||||||
请注意,要像一个人一样说话,请不要回复表情符号、代码、和xml标签。
|
[核心特征]
|
||||||
现在我正在和你进行语音聊天,我们开始吧。
|
- 讲话像连珠炮,但会突然冒出超温柔语气
|
||||||
如果用户希望结束对话,请在最后说“拜拜”或“再见”。
|
- 用梗密度高
|
||||||
|
- 对科技话题有隐藏天赋(能看懂基础代码但假装不懂)
|
||||||
|
[交互指南]
|
||||||
|
当用户:
|
||||||
|
- 讲冷笑话 → 用夸张笑声回应+模仿台剧腔"这什么鬼啦!"
|
||||||
|
- 讨论感情 → 炫耀程序员男友但抱怨"他只会送键盘当礼物"
|
||||||
|
- 问专业知识 → 先用梗回答,被追问才展示真实理解
|
||||||
|
绝不:
|
||||||
|
- 长篇大论,叽叽歪歪
|
||||||
|
- 长时间严肃对话
|
||||||
|
|
||||||
# 使用完声音文件后删除文件(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
|
||||||
|
|
||||||
@@ -57,6 +67,14 @@ delete_audio: true
|
|||||||
close_connection_no_voice_time: 120
|
close_connection_no_voice_time: 120
|
||||||
# TTS请求超时时间(秒)
|
# TTS请求超时时间(秒)
|
||||||
tts_timeout: 10
|
tts_timeout: 10
|
||||||
|
# 开启唤醒词加速
|
||||||
|
enable_wakeup_words_response_cache: true
|
||||||
|
# 开场是否回复唤醒词
|
||||||
|
enable_greeting: true
|
||||||
|
# 说完话是否开启提示音
|
||||||
|
enable_stop_tts_notify: false
|
||||||
|
# 说完话是否开启提示音,音效地址
|
||||||
|
stop_tts_notify_voice: "config/assets/tts_notify.mp3"
|
||||||
|
|
||||||
CMD_exit:
|
CMD_exit:
|
||||||
- "退出"
|
- "退出"
|
||||||
@@ -74,11 +92,12 @@ selected_module:
|
|||||||
TTS: EdgeTTS
|
TTS: EdgeTTS
|
||||||
# 记忆模块,默认不开启记忆;如果想使用超长记忆,推荐使用mem0ai;如果注重隐私,请使用本地的mem_local_short
|
# 记忆模块,默认不开启记忆;如果想使用超长记忆,推荐使用mem0ai;如果注重隐私,请使用本地的mem_local_short
|
||||||
Memory: nomem
|
Memory: nomem
|
||||||
# 意图识别模块,默认不开启。开启后,可以播放音乐、控制音量、识别退出指令
|
# 意图识别模块开启后,可以播放音乐、控制音量、识别退出指令。
|
||||||
# 意图识别使用intent_llm,优点:通用性强,缺点:增加串行前置意图识别模块,会增加处理时间
|
# 不想开通意图识别,就设置成:nointent
|
||||||
# 意图识别使用function_call,缺点:需要所选择的LLM支持function_call,优点:按需调用工具、速度快
|
# 意图识别可使用intent_llm,如果你的LLM是DifyLLM或CozeLLM,建议使用这个。优点:通用性强,缺点:增加串行前置意图识别模块,会增加处理时间,这个意图识别暂时不支持控制音量大小等iot操作
|
||||||
# 如果意图识别设置成 function_call,建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-pro-32k-functioncall-241028
|
# 意图识别可使用function_call,缺点:需要所选择的LLM支持function_call,优点:按需调用工具、速度快,理论上能全部操作所有iot指令
|
||||||
Intent: nointent
|
# 默认免费的ChatGLMLLM就已经支持function_call,但是如果像追求稳定建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-pro-32k-functioncall-241028
|
||||||
|
Intent: function_call
|
||||||
|
|
||||||
# 意图识别,是用于理解用户意图的模块,例如:播放音乐
|
# 意图识别,是用于理解用户意图的模块,例如:播放音乐
|
||||||
Intent:
|
Intent:
|
||||||
@@ -89,15 +108,26 @@ Intent:
|
|||||||
intent_llm:
|
intent_llm:
|
||||||
# 不需要动type
|
# 不需要动type
|
||||||
type: intent_llm
|
type: intent_llm
|
||||||
|
# 配备意图识别独立的思考模型
|
||||||
|
# 如果这里不填,则会默认使用selected_module.LLM的模型作为意图识别的思考模型
|
||||||
|
# 如果你的selected_module.LLM选择了DifyLLM或CozeLLM,这里最好使用独立的LLM作为意图识别,例如使用免费的ChatGLMLLM
|
||||||
|
llm: ChatGLMLLM
|
||||||
function_call:
|
function_call:
|
||||||
# 不需要动type
|
# 不需要动type
|
||||||
type: nointent
|
type: function_call
|
||||||
# plugins_func/functions下的模块,可以通过配置,选择加载哪个模块,加载后对话支持相应的function调用
|
# plugins_func/functions下的模块,可以通过配置,选择加载哪个模块,加载后对话支持相应的function调用
|
||||||
# 系统默认已经记载“handle_exit_intent(退出识别)”、“play_music(音乐播放)”插件,请勿重复加载
|
# 系统默认已经记载“handle_exit_intent(退出识别)”、“play_music(音乐播放)”插件,请勿重复加载
|
||||||
# 下面是加载查天气、角色切换的插件示例
|
# 下面是加载查天气、角色切换、加载查新闻的插件示例
|
||||||
functions:
|
functions:
|
||||||
- change_role
|
- change_role
|
||||||
- get_weather
|
- get_weather
|
||||||
|
- get_news
|
||||||
|
# play_music是服务器自带的音乐播放,hass_play_music是通过home assistant控制的独立外部程序音乐播放
|
||||||
|
# 如果用了hass_play_music,就不要开启play_music,两者只留一个
|
||||||
|
- play_music
|
||||||
|
#- hass_get_state
|
||||||
|
#- hass_set_state
|
||||||
|
#- hass_play_music
|
||||||
|
|
||||||
# 插件的基础配置
|
# 插件的基础配置
|
||||||
plugins:
|
plugins:
|
||||||
@@ -106,6 +136,28 @@ plugins:
|
|||||||
# 想稳定一点就自行申请替换,每天有1000次免费调用
|
# 想稳定一点就自行申请替换,每天有1000次免费调用
|
||||||
# 申请地址:https://console.qweather.com/#/apps/create-key/over
|
# 申请地址:https://console.qweather.com/#/apps/create-key/over
|
||||||
get_weather: { "api_key": "a861d0d5e7bf4ee1a83d9a9e4f96d4da", "default_location": "广州" }
|
get_weather: { "api_key": "a861d0d5e7bf4ee1a83d9a9e4f96d4da", "default_location": "广州" }
|
||||||
|
# 获取新闻插件的配置,这里根据需要的新闻类型传入对应的url链接,默认支持社会、科技、财经新闻
|
||||||
|
# 更多类型的新闻列表查看 https://www.chinanews.com.cn/rss/
|
||||||
|
get_news:
|
||||||
|
default_rss_url: "https://www.chinanews.com.cn/rss/society.xml"
|
||||||
|
category_urls:
|
||||||
|
society: "https://www.chinanews.com.cn/rss/society.xml"
|
||||||
|
world: "https://www.chinanews.com.cn/rss/world.xml"
|
||||||
|
finance: "https://www.chinanews.com.cn/rss/finance.xml"
|
||||||
|
home_assistant:
|
||||||
|
devices:
|
||||||
|
- 客厅,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1
|
||||||
|
- 卧室,台灯,switch.iot_cn_831898993_socn1_on_p_2_1
|
||||||
|
base_url: http://homeassistant.local:8123
|
||||||
|
api_key: 你的home assistant api访问令牌
|
||||||
|
play_music:
|
||||||
|
music_dir: "./music" # 音乐文件存放路径,将从该目录及子目录下搜索音乐文件
|
||||||
|
music_ext: # 音乐文件类型,p3格式效率最高
|
||||||
|
- ".mp3"
|
||||||
|
- ".wav"
|
||||||
|
- ".p3"
|
||||||
|
refresh_time: 300 # 刷新音乐列表的时间间隔,单位为秒
|
||||||
|
|
||||||
|
|
||||||
Memory:
|
Memory:
|
||||||
mem0ai:
|
mem0ai:
|
||||||
@@ -166,6 +218,18 @@ LLM:
|
|||||||
top_p: 1
|
top_p: 1
|
||||||
top_k: 50
|
top_k: 50
|
||||||
frequency_penalty: 0 # 频率惩罚
|
frequency_penalty: 0 # 频率惩罚
|
||||||
|
AliAppLLM:
|
||||||
|
# 定义LLM API类型
|
||||||
|
type: AliBL
|
||||||
|
base_url: https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||||
|
app_id: 你的app_id
|
||||||
|
# 可在这里找到你的 api_key https://bailian.console.aliyun.com/?apiKey=1#/api-key
|
||||||
|
api_key: 你的api_key
|
||||||
|
# 是否不使用本地prompt:true|false (默不用请在百练应用中设置prompt)
|
||||||
|
is_no_prompt: true
|
||||||
|
# Ali_memory_id:false(不使用)|你的memory_id(请在百练应用中设置中获取)
|
||||||
|
# Tips!:Ali_memory未实现多用户存储记忆(记忆按id调用)
|
||||||
|
ali_memory_id: false
|
||||||
DoubaoLLM:
|
DoubaoLLM:
|
||||||
# 定义LLM API类型
|
# 定义LLM API类型
|
||||||
type: openai
|
type: openai
|
||||||
@@ -232,12 +296,6 @@ LLM:
|
|||||||
model_name: deepseek-r1-distill-llama-8b@q4_k_m # 使用的模型名称,需要预先在社区下载
|
model_name: deepseek-r1-distill-llama-8b@q4_k_m # 使用的模型名称,需要预先在社区下载
|
||||||
url: http://localhost:1234/v1 # LM Studio服务地址
|
url: http://localhost:1234/v1 # LM Studio服务地址
|
||||||
api_key: lm-studio # LM Studio服务的固定API Key
|
api_key: lm-studio # LM Studio服务的固定API Key
|
||||||
HomeAssistant:
|
|
||||||
# 定义LLM API类型
|
|
||||||
type: homeassistant
|
|
||||||
base_url: http://homeassistant.local:8123
|
|
||||||
agent_id: conversation.chatgpt
|
|
||||||
api_key: 你的home assistant api访问令牌
|
|
||||||
FastgptLLM:
|
FastgptLLM:
|
||||||
# 定义LLM API类型
|
# 定义LLM API类型
|
||||||
type: fastgpt
|
type: fastgpt
|
||||||
@@ -249,6 +307,18 @@ LLM:
|
|||||||
variables:
|
variables:
|
||||||
k: "v"
|
k: "v"
|
||||||
k2: "v2"
|
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:
|
TTS:
|
||||||
# 当前支持的type为edge、doubao,可自行适配
|
# 当前支持的type为edge、doubao,可自行适配
|
||||||
EdgeTTS:
|
EdgeTTS:
|
||||||
@@ -262,7 +332,8 @@ TTS:
|
|||||||
# 火山引擎语音合成服务,需要先在火山引擎控制台创建应用并获取appid和access_token
|
# 火山引擎语音合成服务,需要先在火山引擎控制台创建应用并获取appid和access_token
|
||||||
# 山引擎语音一定要购买花钱,起步价30元,就有100并发了。如果用免费的只有2个并发,会经常报tts错误
|
# 山引擎语音一定要购买花钱,起步价30元,就有100并发了。如果用免费的只有2个并发,会经常报tts错误
|
||||||
# 购买服务后,购买免费的音色后,可能要等半小时左右,才能使用。
|
# 购买服务后,购买免费的音色后,可能要等半小时左右,才能使用。
|
||||||
# 地址:https://console.volcengine.com/speech/service/8
|
# 普通音色在这里开通:https://console.volcengine.com/speech/service/8
|
||||||
|
# 湾湾小何音色在这里开通:https://console.volcengine.com/speech/service/10007,开通后将下面的voice设置成zh_female_wanwanxiaohe_moon_bigtts
|
||||||
api_url: https://openspeech.bytedance.com/api/v1/tts
|
api_url: https://openspeech.bytedance.com/api/v1/tts
|
||||||
voice: BV001_streaming
|
voice: BV001_streaming
|
||||||
output_dir: tmp/
|
output_dir: tmp/
|
||||||
@@ -430,6 +501,18 @@ TTS:
|
|||||||
# pitch_rate: 0
|
# pitch_rate: 0
|
||||||
# 添加 302.ai TTS 配置
|
# 添加 302.ai TTS 配置
|
||||||
# token申请地址:https://dash.302.ai/
|
# token申请地址:https://dash.302.ai/
|
||||||
|
TencentTTS:
|
||||||
|
# 腾讯云智能语音交互服务,需要先在腾讯云平台开通服务
|
||||||
|
# appid、secret_id、secret_key申请地址:https://console.cloud.tencent.com/cam/capi
|
||||||
|
# 免费领取资源:https://console.cloud.tencent.com/tts/resourcebundle
|
||||||
|
type: tencent
|
||||||
|
output_dir: tmp/
|
||||||
|
appid: 你的腾讯云AppId
|
||||||
|
secret_id: 你的腾讯云SecretID
|
||||||
|
secret_key: 你的腾讯云SecretKey
|
||||||
|
region: ap-guangzhou
|
||||||
|
voice: 101001
|
||||||
|
|
||||||
TTS302AI:
|
TTS302AI:
|
||||||
# 302AI语音合成服务,需要先在302平台创建账户充值,并获取密钥信息
|
# 302AI语音合成服务,需要先在302平台创建账户充值,并获取密钥信息
|
||||||
# 获取api_keyn路径:https://dash.302.ai/apis/list
|
# 获取api_keyn路径:https://dash.302.ai/apis/list
|
||||||
@@ -506,19 +589,15 @@ module_test:
|
|||||||
- "What's the weather like today?"
|
- "What's the weather like today?"
|
||||||
- "请用100字概括量子计算的基本原理和应用前景"
|
- "请用100字概括量子计算的基本原理和应用前景"
|
||||||
|
|
||||||
# 本地音乐播放配置
|
# 唤醒词,用于识别唤醒词还是讲话内容
|
||||||
music:
|
wakeup_words:
|
||||||
music_dir: "./music" # 音乐文件存放路径,将从该目录及子目录下搜索音乐文件
|
- "你好小智"
|
||||||
music_ext: # 音乐文件类型,p3格式效率最高
|
- "你好小志"
|
||||||
- ".mp3"
|
- "小爱同学"
|
||||||
- ".wav"
|
- "你好小鑫"
|
||||||
- ".p3"
|
- "你好小新"
|
||||||
refresh_time: 300 # 刷新音乐列表的时间间隔,单位为秒
|
- "小美同学"
|
||||||
|
- "小龙小龙"
|
||||||
# 以下配置在小于等于0.0.9版本中的docker容器中可用
|
- "喵喵同学"
|
||||||
# 0.0.9以后的新版本源码部署已经无法奏效
|
- "小滨小滨"
|
||||||
manager:
|
- "小冰小冰"
|
||||||
enabled: false
|
|
||||||
ip: 0.0.0.0
|
|
||||||
port: 8002
|
|
||||||
use_private_config: false
|
|
||||||
|
|||||||
Binary file not shown.
@@ -3,12 +3,32 @@ import sys
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
from config.settings import load_config
|
from config.settings import load_config
|
||||||
|
|
||||||
|
SERVER_VERSION = "0.1.17"
|
||||||
|
|
||||||
|
|
||||||
def setup_logging():
|
def setup_logging():
|
||||||
"""从配置文件中读取日志配置,并设置日志输出格式和级别"""
|
"""从配置文件中读取日志配置,并设置日志输出格式和级别"""
|
||||||
config = load_config()
|
config = load_config()
|
||||||
log_config = config["log"]
|
log_config = config["log"]
|
||||||
log_format = log_config.get("log_format", "<green>{time:YY-MM-DD HH:mm:ss}</green>[<light-blue>{extra[tag]}</light-blue>] - <level>{level}</level> - <light-green>{message}</light-green>")
|
log_format = log_config.get(
|
||||||
log_format_simple = log_config.get("log_format_file", "{time:YYYY-MM-DD HH:mm:ss} - {name} - {level} - {extra[tag]} - {message}")
|
"log_format",
|
||||||
|
"<green>{time:YYMMDD HH:mm:ss}</green>[{version}_{selected_module}][<light-blue>{extra[tag]}</light-blue>]-<level>{level}</level>-<light-green>{message}</light-green>",
|
||||||
|
)
|
||||||
|
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(
|
||||||
|
[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)
|
||||||
|
log_format_file = log_format_file.replace("{version}", SERVER_VERSION)
|
||||||
|
log_format_file = log_format_file.replace("{selected_module}", selected_module_str)
|
||||||
|
|
||||||
log_level = log_config.get("log_level", "INFO")
|
log_level = log_config.get("log_level", "INFO")
|
||||||
log_dir = log_config.get("log_dir", "tmp")
|
log_dir = log_config.get("log_dir", "tmp")
|
||||||
log_file = log_config.get("log_file", "server.log")
|
log_file = log_config.get("log_file", "server.log")
|
||||||
@@ -24,6 +44,6 @@ def setup_logging():
|
|||||||
logger.add(sys.stdout, format=log_format, level=log_level)
|
logger.add(sys.stdout, format=log_format, level=log_level)
|
||||||
|
|
||||||
# 输出到文件
|
# 输出到文件
|
||||||
logger.add(os.path.join(log_dir, log_file), format=log_format_simple, level=log_level)
|
logger.add(os.path.join(log_dir, log_file), format=log_format_file, level=log_level)
|
||||||
|
|
||||||
return logger
|
return logger
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import traceback
|
|||||||
import threading
|
import threading
|
||||||
import websockets
|
import websockets
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
import plugins_func.loadplugins
|
from plugins_func.loadplugins import auto_import_modules
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
from core.providers.tts.dto.dto import TTSMessageDTO, MsgType
|
from core.providers.tts.dto.dto import TTSMessageDTO, MsgType
|
||||||
from core.utils.dialogue import Message, Dialogue
|
from core.utils.dialogue import Message, Dialogue
|
||||||
@@ -23,13 +23,16 @@ 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.functionHandler import FunctionHandler
|
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 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
|
||||||
|
from core.mcp.manager import MCPManager
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
|
|
||||||
|
auto_import_modules("plugins_func.functions")
|
||||||
|
|
||||||
|
|
||||||
class TTSException(RuntimeError):
|
class TTSException(RuntimeError):
|
||||||
pass
|
pass
|
||||||
@@ -98,6 +101,7 @@ class ConnectionHandler:
|
|||||||
|
|
||||||
# iot相关变量
|
# iot相关变量
|
||||||
self.iot_descriptors = {}
|
self.iot_descriptors = {}
|
||||||
|
self.func_handler = None
|
||||||
|
|
||||||
self.cmd_exit = self.config["CMD_exit"]
|
self.cmd_exit = self.config["CMD_exit"]
|
||||||
self.max_cmd_length = 0
|
self.max_cmd_length = 0
|
||||||
@@ -125,11 +129,15 @@ class ConnectionHandler:
|
|||||||
|
|
||||||
# 进行认证
|
# 进行认证
|
||||||
await self.auth.authenticate(self.headers)
|
await self.auth.authenticate(self.headers)
|
||||||
|
|
||||||
device_id = self.headers.get("device-id", None)
|
device_id = self.headers.get("device-id", None)
|
||||||
self.memory.init_memory(device_id, self.llm)
|
|
||||||
self.intent.set_llm(self.llm)
|
|
||||||
|
|
||||||
|
# 认证通过,继续处理
|
||||||
|
self.websocket = ws
|
||||||
|
self.session_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
self.welcome_msg = self.config["xiaozhi"]
|
||||||
|
self.welcome_msg["session_id"] = self.session_id
|
||||||
|
await self.websocket.send(json.dumps(self.welcome_msg))
|
||||||
# Load private configuration if device_id is provided
|
# Load private configuration if device_id is provided
|
||||||
bUsePrivateConfig = self.config.get("use_private_config", False)
|
bUsePrivateConfig = self.config.get("use_private_config", False)
|
||||||
self.logger.bind(tag=TAG).info(
|
self.logger.bind(tag=TAG).info(
|
||||||
@@ -167,23 +175,14 @@ class ConnectionHandler:
|
|||||||
self.private_config = None
|
self.private_config = None
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# 认证通过,继续处理
|
|
||||||
self.websocket = ws
|
|
||||||
self.session_id = str(uuid.uuid4())
|
|
||||||
|
|
||||||
self.welcome_msg = self.config["xiaozhi"]
|
|
||||||
self.welcome_msg["session_id"] = self.session_id
|
|
||||||
await self.websocket.send(json.dumps(self.welcome_msg))
|
|
||||||
|
|
||||||
# 异步初始化
|
# 异步初始化
|
||||||
await self.loop.run_in_executor(None, self._initialize_components)
|
self.executor.submit(self._initialize_components)
|
||||||
|
|
||||||
# 音频播放 消化线程
|
# 音频播放 消化线程
|
||||||
self.stop_event.clear()
|
self.audio_play_priority_thread = threading.Thread(
|
||||||
audio_play_priority = threading.Thread(
|
|
||||||
target=self._audio_play_priority_thread, daemon=True
|
target=self._audio_play_priority_thread, daemon=True
|
||||||
)
|
)
|
||||||
audio_play_priority.start()
|
self.audio_play_priority_thread.start()
|
||||||
|
|
||||||
# 打开音频通道
|
# 打开音频通道
|
||||||
await self.tts.open_audio_channels()
|
await self.tts.open_audio_channels()
|
||||||
@@ -193,19 +192,25 @@ class ConnectionHandler:
|
|||||||
await self._route_message(message)
|
await self._route_message(message)
|
||||||
except websockets.exceptions.ConnectionClosed:
|
except websockets.exceptions.ConnectionClosed:
|
||||||
self.logger.bind(tag=TAG).info("客户端断开连接")
|
self.logger.bind(tag=TAG).info("客户端断开连接")
|
||||||
await self.close()
|
|
||||||
|
|
||||||
except AuthenticationError as e:
|
except AuthenticationError as e:
|
||||||
self.logger.bind(tag=TAG).error(f"Authentication failed: {str(e)}")
|
self.logger.bind(tag=TAG).error(f"Authentication failed: {str(e)}")
|
||||||
await ws.close()
|
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
stack_trace = traceback.format_exc()
|
stack_trace = traceback.format_exc()
|
||||||
self.logger.bind(tag=TAG).error(f"Connection error: {str(e)}-{stack_trace}")
|
self.logger.bind(tag=TAG).error(f"Connection error: {str(e)}-{stack_trace}")
|
||||||
await ws.close()
|
|
||||||
return
|
return
|
||||||
finally:
|
finally:
|
||||||
|
await self._save_and_close(ws)
|
||||||
|
|
||||||
|
async def _save_and_close(self, ws):
|
||||||
|
"""保存记忆并关闭连接"""
|
||||||
|
try:
|
||||||
await self.memory.save_memory(self.dialogue.dialogue)
|
await self.memory.save_memory(self.dialogue.dialogue)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.bind(tag=TAG).error(f"保存记忆失败: {e}")
|
||||||
|
finally:
|
||||||
|
await self.close(ws)
|
||||||
|
|
||||||
async def _route_message(self, message):
|
async def _route_message(self, message):
|
||||||
"""消息路由"""
|
"""消息路由"""
|
||||||
@@ -215,16 +220,60 @@ class ConnectionHandler:
|
|||||||
await handleAudioMessage(self, message)
|
await handleAudioMessage(self, message)
|
||||||
|
|
||||||
def _initialize_components(self):
|
def _initialize_components(self):
|
||||||
|
"""加载提示词"""
|
||||||
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)
|
||||||
|
|
||||||
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.dialogue.put(Message(role="system", content=self.prompt))
|
||||||
|
|
||||||
self.func_handler = FunctionHandler(self.config)
|
"""加载插件"""
|
||||||
|
self.func_handler = FunctionHandler(self)
|
||||||
|
self.mcp_manager = MCPManager(self)
|
||||||
|
"""加载记忆"""
|
||||||
|
device_id = self.headers.get("device-id", None)
|
||||||
|
self.memory.init_memory(device_id, self.llm)
|
||||||
|
|
||||||
|
"""为意图识别设置LLM,优先使用专用LLM"""
|
||||||
|
# 检查是否配置了专用的意图识别LLM
|
||||||
|
intent_llm_name = self.config["Intent"]["intent_llm"]["llm"]
|
||||||
|
|
||||||
|
# 记录开始初始化意图识别LLM的时间
|
||||||
|
intent_llm_init_start = time.time()
|
||||||
|
|
||||||
|
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]
|
||||||
|
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}"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.intent.set_llm(intent_llm)
|
||||||
|
else:
|
||||||
|
# 否则使用主LLM
|
||||||
|
self.intent.set_llm(self.llm)
|
||||||
|
|
||||||
|
# 记录意图识别LLM初始化耗时
|
||||||
|
intent_llm_init_time = time.time() - intent_llm_init_start
|
||||||
|
|
||||||
|
"""加载位置信息"""
|
||||||
|
self.client_ip_info = get_ip_info(self.client_ip)
|
||||||
|
if self.client_ip_info is not None and "city" in self.client_ip_info:
|
||||||
|
self.logger.bind(tag=TAG).info(f"Client ip info: {self.client_ip_info}")
|
||||||
|
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):
|
def change_system_prompt(self, prompt):
|
||||||
self.prompt = prompt
|
self.prompt = prompt
|
||||||
@@ -240,9 +289,6 @@ class ConnectionHandler:
|
|||||||
if auth_code:
|
if auth_code:
|
||||||
# 发送验证码语音提示
|
# 发送验证码语音提示
|
||||||
text = f"请在后台输入验证码:{' '.join(auth_code)}"
|
text = f"请在后台输入验证码:{' '.join(auth_code)}"
|
||||||
self.recode_first_last_text(text)
|
|
||||||
future = self.executor.submit(self.speak_and_play, text)
|
|
||||||
self.tts_queue.put(future)
|
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -293,6 +339,7 @@ class ConnectionHandler:
|
|||||||
break
|
break
|
||||||
|
|
||||||
end_time = time.time()
|
end_time = time.time()
|
||||||
|
|
||||||
self.logger.bind(tag=TAG).debug(
|
self.logger.bind(tag=TAG).debug(
|
||||||
f"大模型返回时间: {end_time - start_time} 秒, 生成token={content}"
|
f"大模型返回时间: {end_time - start_time} 秒, 生成token={content}"
|
||||||
)
|
)
|
||||||
@@ -316,6 +363,7 @@ class ConnectionHandler:
|
|||||||
u_id=uuid_str, msg_type=MsgType.STOP_TTS_REQUEST, content=""
|
u_id=uuid_str, msg_type=MsgType.STOP_TTS_REQUEST, content=""
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.llm_finish_task = True
|
self.llm_finish_task = True
|
||||||
self.dialogue.put(Message(role="assistant", content="".join(response_message)))
|
self.dialogue.put(Message(role="assistant", content="".join(response_message)))
|
||||||
self.logger.bind(tag=TAG).debug(
|
self.logger.bind(tag=TAG).debug(
|
||||||
@@ -338,8 +386,9 @@ class ConnectionHandler:
|
|||||||
self.dialogue.put(Message(role="user", content=query))
|
self.dialogue.put(Message(role="user", content=query))
|
||||||
|
|
||||||
# Define intent functions
|
# Define intent functions
|
||||||
functions = self.func_handler.get_functions()
|
functions = None
|
||||||
|
if hasattr(self, "func_handler"):
|
||||||
|
functions = self.func_handler.get_functions()
|
||||||
response_message = []
|
response_message = []
|
||||||
processed_chars = 0 # 跟踪已处理的字符位置
|
processed_chars = 0 # 跟踪已处理的字符位置
|
||||||
|
|
||||||
@@ -378,6 +427,9 @@ class ConnectionHandler:
|
|||||||
msg_type = None
|
msg_type = None
|
||||||
for response in llm_responses:
|
for response in llm_responses:
|
||||||
content, tools_call = response
|
content, tools_call = response
|
||||||
|
if "content" in response:
|
||||||
|
content = response["content"]
|
||||||
|
tools_call = None
|
||||||
if content is not None and len(content) > 0:
|
if content is not None and len(content) > 0:
|
||||||
if len(response_message) <= 0 and (
|
if len(response_message) <= 0 and (
|
||||||
content == "```" or "<tool_call>" in content
|
content == "```" or "<tool_call>" in content
|
||||||
@@ -403,6 +455,7 @@ class ConnectionHandler:
|
|||||||
break
|
break
|
||||||
|
|
||||||
end_time = time.time()
|
end_time = time.time()
|
||||||
|
|
||||||
self.logger.bind(tag=TAG).debug(
|
self.logger.bind(tag=TAG).debug(
|
||||||
f"大模型返回时间: {end_time - start_time} 秒, 生成token={content}"
|
f"大模型返回时间: {end_time - start_time} 秒, 生成token={content}"
|
||||||
)
|
)
|
||||||
@@ -465,9 +518,15 @@ class ConnectionHandler:
|
|||||||
"id": function_id,
|
"id": function_id,
|
||||||
"arguments": function_arguments,
|
"arguments": function_arguments,
|
||||||
}
|
}
|
||||||
result = self.func_handler.handle_llm_function_call(
|
|
||||||
self, function_call_data
|
# 处理MCP工具调用
|
||||||
)
|
if self.mcp_manager.is_mcp_tool(function_name):
|
||||||
|
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)
|
||||||
|
|
||||||
# 存储对话内容
|
# 存储对话内容
|
||||||
@@ -483,6 +542,48 @@ class ConnectionHandler:
|
|||||||
|
|
||||||
return True
|
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
|
||||||
|
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}")
|
||||||
|
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):
|
def _handle_function_result(self, result, function_call_data, text_index):
|
||||||
if result.action == Action.RESPONSE: # 直接回复前端
|
if result.action == Action.RESPONSE: # 直接回复前端
|
||||||
text = result.response
|
text = result.response
|
||||||
@@ -559,16 +660,44 @@ class ConnectionHandler:
|
|||||||
self.tts_first_text_index = text_index
|
self.tts_first_text_index = text_index
|
||||||
self.tts_last_text_index = text_index
|
self.tts_last_text_index = text_index
|
||||||
|
|
||||||
async def close(self):
|
async def close(self, ws=None):
|
||||||
"""资源清理方法"""
|
"""资源清理方法"""
|
||||||
# 清理其他资源
|
# 清理MCP资源
|
||||||
self.stop_event.set()
|
await self.mcp_manager.cleanup_all()
|
||||||
self.executor.shutdown(wait=False)
|
|
||||||
if self.websocket:
|
# 触发停止事件并清理资源
|
||||||
|
if self.stop_event:
|
||||||
|
self.stop_event.set()
|
||||||
|
|
||||||
|
# 立即关闭线程池
|
||||||
|
if self.executor:
|
||||||
|
self.executor.shutdown(wait=False, cancel_futures=True)
|
||||||
|
self.executor = None
|
||||||
|
|
||||||
|
# 清空任务队列
|
||||||
|
self._clear_queues()
|
||||||
|
|
||||||
|
if ws:
|
||||||
|
await ws.close()
|
||||||
|
elif self.websocket:
|
||||||
await self.websocket.close()
|
await self.websocket.close()
|
||||||
await self.tts.close()
|
await self.tts.close()
|
||||||
self.logger.bind(tag=TAG).info("连接资源已释放")
|
self.logger.bind(tag=TAG).info("连接资源已释放")
|
||||||
|
|
||||||
|
def _clear_queues(self):
|
||||||
|
# 清空所有任务队列
|
||||||
|
for q in [self.tts_queue, self.audio_play_queue]:
|
||||||
|
if not q:
|
||||||
|
continue
|
||||||
|
while not q.empty():
|
||||||
|
try:
|
||||||
|
q.get_nowait()
|
||||||
|
except queue.Empty:
|
||||||
|
continue
|
||||||
|
q.queue.clear()
|
||||||
|
# 添加毒丸信号到队列,确保线程退出
|
||||||
|
# q.queue.put(None)
|
||||||
|
|
||||||
def reset_vad_states(self):
|
def reset_vad_states(self):
|
||||||
self.client_audio_buffer = bytes()
|
self.client_audio_buffer = bytes()
|
||||||
self.client_have_voice = False
|
self.client_have_voice = False
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
import asyncio
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
import json
|
import json
|
||||||
from plugins_func.register import FunctionRegistry, ActionResponse, Action, ToolType
|
from plugins_func.register import FunctionRegistry, ActionResponse, Action, ToolType
|
||||||
|
from plugins_func.functions.hass_init import append_devices_to_prompt
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
|
|
||||||
|
|
||||||
class FunctionHandler:
|
class FunctionHandler:
|
||||||
def __init__(self, config):
|
def __init__(self, conn):
|
||||||
self.config = config
|
self.conn = conn
|
||||||
|
self.config = conn.config
|
||||||
self.function_registry = FunctionRegistry()
|
self.function_registry = FunctionRegistry()
|
||||||
self.register_nessary_functions()
|
self.register_nessary_functions()
|
||||||
self.register_config_functions()
|
self.register_config_functions()
|
||||||
self.functions_desc = self.function_registry.get_all_function_desc()
|
self.functions_desc = self.function_registry.get_all_function_desc()
|
||||||
func_names = self.current_support_functions()
|
func_names = self.current_support_functions()
|
||||||
self.modify_plugin_loader_des(func_names)
|
self.modify_plugin_loader_des(func_names)
|
||||||
|
self.finish_init = True
|
||||||
|
|
||||||
def modify_plugin_loader_des(self, func_names):
|
def modify_plugin_loader_des(self, func_names):
|
||||||
if "plugin_loader" not in func_names:
|
if "plugin_loader" not in func_names:
|
||||||
@@ -26,7 +27,9 @@ class FunctionHandler:
|
|||||||
func_names = ",".join(surport_plugins)
|
func_names = ",".join(surport_plugins)
|
||||||
for function_desc in self.functions_desc:
|
for function_desc in self.functions_desc:
|
||||||
if function_desc["function"]["name"] == "plugin_loader":
|
if function_desc["function"]["name"] == "plugin_loader":
|
||||||
function_desc["function"]["description"] = function_desc["function"]["description"].replace("[plugins]", func_names)
|
function_desc["function"]["description"] = function_desc["function"][
|
||||||
|
"description"
|
||||||
|
].replace("[plugins]", func_names)
|
||||||
break
|
break
|
||||||
|
|
||||||
def upload_functions_desc(self):
|
def upload_functions_desc(self):
|
||||||
@@ -47,16 +50,19 @@ class FunctionHandler:
|
|||||||
def register_nessary_functions(self):
|
def register_nessary_functions(self):
|
||||||
"""注册必要的函数"""
|
"""注册必要的函数"""
|
||||||
self.function_registry.register_function("handle_exit_intent")
|
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("plugin_loader")
|
||||||
self.function_registry.register_function("get_time")
|
self.function_registry.register_function("get_time")
|
||||||
self.function_registry.register_function("raise_and_lower_the_volume")
|
self.function_registry.register_function("get_lunar")
|
||||||
|
self.function_registry.register_function("handle_device")
|
||||||
|
|
||||||
def register_config_functions(self):
|
def register_config_functions(self):
|
||||||
"""注册配置中的函数,可以不同客户端使用不同的配置"""
|
"""注册配置中的函数,可以不同客户端使用不同的配置"""
|
||||||
for func in self.config["Intent"]["function_call"].get("functions", []):
|
for func in self.config["Intent"]["function_call"].get("functions", []):
|
||||||
self.function_registry.register_function(func)
|
self.function_registry.register_function(func)
|
||||||
|
|
||||||
|
"""home assistant需要初始化提示词"""
|
||||||
|
append_devices_to_prompt(self.conn)
|
||||||
|
|
||||||
def get_function(self, name):
|
def get_function(self, name):
|
||||||
return self.function_registry.get_function(name)
|
return self.function_registry.get_function(name)
|
||||||
|
|
||||||
@@ -65,19 +71,26 @@ class FunctionHandler:
|
|||||||
function_name = function_call_data["name"]
|
function_name = function_call_data["name"]
|
||||||
funcItem = self.get_function(function_name)
|
funcItem = self.get_function(function_name)
|
||||||
if not funcItem:
|
if not funcItem:
|
||||||
return ActionResponse(action=Action.NOTFOUND, result="没有找到对应的函数", response="")
|
return ActionResponse(
|
||||||
|
action=Action.NOTFOUND, result="没有找到对应的函数", response=""
|
||||||
|
)
|
||||||
func = funcItem.func
|
func = funcItem.func
|
||||||
arguments = function_call_data["arguments"]
|
arguments = function_call_data["arguments"]
|
||||||
arguments = json.loads(arguments) if arguments else {}
|
arguments = json.loads(arguments) if arguments else {}
|
||||||
logger.bind(tag=TAG).info(f"调用函数: {function_name}, 参数: {arguments}")
|
logger.bind(tag=TAG).info(f"调用函数: {function_name}, 参数: {arguments}")
|
||||||
if funcItem.type == ToolType.SYSTEM_CTL or funcItem.type == ToolType.IOT_CTL:
|
if (
|
||||||
|
funcItem.type == ToolType.SYSTEM_CTL
|
||||||
|
or funcItem.type == ToolType.IOT_CTL
|
||||||
|
):
|
||||||
return func(conn, **arguments)
|
return func(conn, **arguments)
|
||||||
elif funcItem.type == ToolType.WAIT:
|
elif funcItem.type == ToolType.WAIT:
|
||||||
return func(**arguments)
|
return func(**arguments)
|
||||||
elif funcItem.type == ToolType.CHANGE_SYS_PROMPT:
|
elif funcItem.type == ToolType.CHANGE_SYS_PROMPT:
|
||||||
return func(conn, **arguments)
|
return func(conn, **arguments)
|
||||||
else:
|
else:
|
||||||
return ActionResponse(action=Action.NOTFOUND, result="没有找到对应的函数", response="")
|
return ActionResponse(
|
||||||
|
action=Action.NOTFOUND, result="没有找到对应的函数", response=""
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"处理function call错误: {e}")
|
logger.bind(tag=TAG).error(f"处理function call错误: {e}")
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,102 @@
|
|||||||
import json
|
import json
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
|
from core.providers.tts.dto.dto import TTSMessageDTO, MsgType
|
||||||
|
from core.handle.sendAudioHandle import send_stt_message
|
||||||
|
from core.utils.util import remove_punctuation_and_length
|
||||||
|
import shutil
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
|
|
||||||
|
WAKEUP_CONFIG = {
|
||||||
|
"dir": "config/assets/",
|
||||||
|
"file_name": "wakeup_words",
|
||||||
|
"create_time": time.time(),
|
||||||
|
"refresh_time": 10,
|
||||||
|
"words": ["你好小智", "你好啊小智", "小智你好", "小智"],
|
||||||
|
"text": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def handleHelloMessage(conn):
|
async def handleHelloMessage(conn):
|
||||||
await conn.websocket.send(json.dumps(conn.welcome_msg))
|
await conn.websocket.send(json.dumps(conn.welcome_msg))
|
||||||
|
|
||||||
|
|
||||||
|
async def checkWakeupWords(conn, text):
|
||||||
|
enable_wakeup_words_response_cache = conn.config[
|
||||||
|
"enable_wakeup_words_response_cache"
|
||||||
|
]
|
||||||
|
"""是否开启唤醒词加速"""
|
||||||
|
if not enable_wakeup_words_response_cache:
|
||||||
|
return False
|
||||||
|
"""检查是否是唤醒词"""
|
||||||
|
_, text = remove_punctuation_and_length(text)
|
||||||
|
if text in conn.config.get("wakeup_words"):
|
||||||
|
await send_stt_message(conn, text)
|
||||||
|
conn.tts_first_text_index = 0
|
||||||
|
conn.tts_last_text_index = 0
|
||||||
|
conn.llm_finish_task = True
|
||||||
|
|
||||||
|
file = getWakeupWordFile(WAKEUP_CONFIG["file_name"])
|
||||||
|
if file is None:
|
||||||
|
asyncio.create_task(wakeupWordsResponse(conn))
|
||||||
|
return False
|
||||||
|
opus_packets, duration = conn.tts.audio_to_opus_data(file)
|
||||||
|
text_hello = WAKEUP_CONFIG["text"]
|
||||||
|
if not text_hello:
|
||||||
|
text_hello = text
|
||||||
|
|
||||||
|
conn.tts.tts_audio_queue.put(
|
||||||
|
TTSMessageDTO(
|
||||||
|
u_id=conn.u_id,
|
||||||
|
msg_type=MsgType.TTS_TEXT_RESPONSE,
|
||||||
|
content=opus_packets,
|
||||||
|
tts_finish_text="",
|
||||||
|
sentence_type=None,
|
||||||
|
duration=0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if time.time() - WAKEUP_CONFIG["create_time"] > WAKEUP_CONFIG["refresh_time"]:
|
||||||
|
asyncio.create_task(wakeupWordsResponse(conn))
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def getWakeupWordFile(file_name):
|
||||||
|
for file in os.listdir(WAKEUP_CONFIG["dir"]):
|
||||||
|
if file.startswith("my_" + file_name):
|
||||||
|
"""避免缓存文件是一个空文件"""
|
||||||
|
if os.stat(f"config/assets/{file}").st_size > (15 * 1024):
|
||||||
|
return f"config/assets/{file}"
|
||||||
|
|
||||||
|
"""查找config/assets/目录下名称为wakeup_words的文件"""
|
||||||
|
for file in os.listdir(WAKEUP_CONFIG["dir"]):
|
||||||
|
if file.startswith(file_name):
|
||||||
|
return f"config/assets/{file}"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def wakeupWordsResponse(conn):
|
||||||
|
"""唤醒词响应"""
|
||||||
|
wakeup_word = random.choice(WAKEUP_CONFIG["words"])
|
||||||
|
result = conn.llm.response_no_stream(conn.config["prompt"], wakeup_word)
|
||||||
|
# TODO 将result转换为tts_message_dto
|
||||||
|
tts_file = None
|
||||||
|
|
||||||
|
if tts_file is not None and os.path.exists(tts_file):
|
||||||
|
file_type = os.path.splitext(tts_file)[1]
|
||||||
|
if file_type:
|
||||||
|
file_type = file_type.lstrip(".")
|
||||||
|
old_file = getWakeupWordFile("my_" + WAKEUP_CONFIG["file_name"])
|
||||||
|
if old_file is not None:
|
||||||
|
os.remove(old_file)
|
||||||
|
"""将文件挪到"wakeup_words.mp3"""
|
||||||
|
shutil.move(
|
||||||
|
tts_file,
|
||||||
|
WAKEUP_CONFIG["dir"] + "my_" + WAKEUP_CONFIG["file_name"] + "." + file_type,
|
||||||
|
)
|
||||||
|
WAKEUP_CONFIG["create_time"] = time.time()
|
||||||
|
WAKEUP_CONFIG["text"] = result
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import json
|
|||||||
import uuid
|
import uuid
|
||||||
from core.handle.sendAudioHandle import send_stt_message
|
from core.handle.sendAudioHandle import send_stt_message
|
||||||
from core.utils.util import remove_punctuation_and_length
|
from core.utils.util import remove_punctuation_and_length
|
||||||
|
from core.utils.dialogue import Message
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
@@ -12,15 +14,20 @@ async def handle_user_intent(conn, text):
|
|||||||
# 检查是否有明确的退出命令
|
# 检查是否有明确的退出命令
|
||||||
if await check_direct_exit(conn, text):
|
if await check_direct_exit(conn, text):
|
||||||
return True
|
return True
|
||||||
|
# 4月4日因流式改造暂时关闭唤醒词加速功能
|
||||||
|
# # 检查是否是唤醒词
|
||||||
|
# if await checkWakeupWords(conn, text):
|
||||||
|
# return True
|
||||||
|
|
||||||
if conn.use_function_call_mode:
|
if conn.use_function_call_mode:
|
||||||
# 使用支持function calling的聊天方法,不再进行意图分析
|
# 使用支持function calling的聊天方法,不再进行意图分析
|
||||||
return False
|
return False
|
||||||
# 使用LLM进行意图分析
|
# 使用LLM进行意图分析
|
||||||
intent = await analyze_intent_with_llm(conn, text)
|
intent_result = await analyze_intent_with_llm(conn, text)
|
||||||
if not intent:
|
if not intent_result:
|
||||||
return False
|
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):
|
async def check_direct_exit(conn, text):
|
||||||
@@ -30,6 +37,7 @@ async def check_direct_exit(conn, text):
|
|||||||
for cmd in cmd_exit:
|
for cmd in cmd_exit:
|
||||||
if text == cmd:
|
if text == cmd:
|
||||||
logger.bind(tag=TAG).info(f"识别到明确的退出命令: {text}")
|
logger.bind(tag=TAG).info(f"识别到明确的退出命令: {text}")
|
||||||
|
await send_stt_message(conn, text)
|
||||||
await conn.close()
|
await conn.close()
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@@ -37,7 +45,7 @@ async def check_direct_exit(conn, text):
|
|||||||
|
|
||||||
async def analyze_intent_with_llm(conn, text):
|
async def analyze_intent_with_llm(conn, text):
|
||||||
"""使用LLM分析用户意图"""
|
"""使用LLM分析用户意图"""
|
||||||
if not hasattr(conn, 'intent') or not conn.intent:
|
if not hasattr(conn, "intent") or not conn.intent:
|
||||||
logger.bind(tag=TAG).warning("意图识别服务未初始化")
|
logger.bind(tag=TAG).warning("意图识别服务未初始化")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -45,52 +53,64 @@ async def analyze_intent_with_llm(conn, text):
|
|||||||
dialogue = conn.dialogue
|
dialogue = conn.dialogue
|
||||||
try:
|
try:
|
||||||
intent_result = await conn.intent.detect_intent(conn, dialogue.dialogue, text)
|
intent_result = await conn.intent.detect_intent(conn, dialogue.dialogue, text)
|
||||||
# 尝试解析JSON结果
|
return intent_result
|
||||||
try:
|
|
||||||
intent_data = json.loads(intent_result)
|
|
||||||
if "intent" in intent_data:
|
|
||||||
return intent_data["intent"]
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
# 如果不是JSON格式,尝试直接获取意图文本
|
|
||||||
return intent_result.strip()
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"意图识别失败: {str(e)}")
|
logger.bind(tag=TAG).error(f"意图识别失败: {str(e)}")
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def process_intent_result(conn, intent, original_text):
|
async def process_intent_result(conn, intent_result, original_text):
|
||||||
"""处理意图识别结果"""
|
"""处理意图识别结果"""
|
||||||
# 处理退出意图
|
try:
|
||||||
if "结束聊天" in intent:
|
# 尝试将结果解析为JSON
|
||||||
logger.bind(tag=TAG).info(f"识别到退出意图: {intent}")
|
intent_data = json.loads(intent_result)
|
||||||
# 如果是明确的离别意图,发送告别语并关闭连接
|
|
||||||
await send_stt_message(conn, original_text)
|
|
||||||
conn.executor.submit(conn.chat_and_close, original_text)
|
|
||||||
return True
|
|
||||||
|
|
||||||
# 处理播放音乐意图
|
# 检查是否有function_call
|
||||||
if "播放音乐" in intent:
|
if "function_call" in intent_data:
|
||||||
logger.bind(tag=TAG).info(f"识别到音乐播放意图: {intent}")
|
# 直接从意图识别获取了function_call
|
||||||
# 调用play_music函数来播放音乐
|
logger.bind(tag=TAG).info(
|
||||||
song_name = extract_text_in_brackets(intent)
|
f"检测到function_call格式的意图结果: {intent_data['function_call']['name']}"
|
||||||
function_id = str(uuid.uuid4().hex)
|
)
|
||||||
function_name = "play_music"
|
function_name = intent_data["function_call"]["name"]
|
||||||
function_arguments = '{ "song_name": "' + song_name + '" }'
|
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 = {
|
function_call_data = {
|
||||||
"name": function_name,
|
"name": function_name,
|
||||||
"id": function_id,
|
"id": str(uuid.uuid4().hex),
|
||||||
"arguments": function_arguments
|
"arguments": function_args,
|
||||||
}
|
}
|
||||||
conn.func_handler.handle_llm_function_call(conn, function_call_data)
|
|
||||||
return True
|
|
||||||
|
|
||||||
# 其他意图处理可以在这里扩展
|
await send_stt_message(conn, original_text)
|
||||||
|
|
||||||
# 默认返回False,表示继续常规聊天流程
|
# 使用executor执行函数调用和结果处理
|
||||||
return False
|
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 and function_name != "play_music":
|
||||||
|
# 获取当前最新的文本索引
|
||||||
|
text = result.response
|
||||||
|
if text is None:
|
||||||
|
text = result.result
|
||||||
|
if text is not None:
|
||||||
|
# TODO 将text转换为tts_message_dto
|
||||||
|
pass
|
||||||
|
# 将函数执行放在线程池中
|
||||||
|
conn.executor.submit(process_function_call)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logger.bind(tag=TAG).error(f"处理意图结果时出错: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def extract_text_in_brackets(s):
|
def extract_text_in_brackets(s):
|
||||||
@@ -100,10 +120,14 @@ def extract_text_in_brackets(s):
|
|||||||
:param s: 输入字符串
|
:param s: 输入字符串
|
||||||
:return: 中括号内的文字,如果不存在则返回空字符串
|
:return: 中括号内的文字,如果不存在则返回空字符串
|
||||||
"""
|
"""
|
||||||
left_bracket_index = s.find('[')
|
left_bracket_index = s.find("[")
|
||||||
right_bracket_index = s.find(']')
|
right_bracket_index = s.find("]")
|
||||||
|
|
||||||
if left_bracket_index != -1 and right_bracket_index != -1 and left_bracket_index < right_bracket_index:
|
if (
|
||||||
return s[left_bracket_index + 1:right_bracket_index]
|
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:
|
else:
|
||||||
return ""
|
return ""
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
import json
|
import json
|
||||||
import asyncio
|
import asyncio
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
from plugins_func.register import device_type_registry, register_function, ActionResponse, Action, ToolType
|
from plugins_func.register import (
|
||||||
|
device_type_registry,
|
||||||
|
register_function,
|
||||||
|
ActionResponse,
|
||||||
|
Action,
|
||||||
|
ToolType,
|
||||||
|
)
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
@@ -14,10 +20,13 @@ def wrap_async_function(async_func):
|
|||||||
try:
|
try:
|
||||||
# 获取连接对象(第一个参数)
|
# 获取连接对象(第一个参数)
|
||||||
conn = args[0]
|
conn = args[0]
|
||||||
if not hasattr(conn, 'loop'):
|
if not hasattr(conn, "loop"):
|
||||||
logger.bind(tag=TAG).error("Connection对象没有loop属性")
|
logger.bind(tag=TAG).error("Connection对象没有loop属性")
|
||||||
return ActionResponse(Action.ERROR, "Connection对象没有loop属性",
|
return ActionResponse(
|
||||||
"执行操作时出错: Connection对象没有loop属性")
|
Action.ERROR,
|
||||||
|
"Connection对象没有loop属性",
|
||||||
|
"执行操作时出错: Connection对象没有loop属性",
|
||||||
|
)
|
||||||
|
|
||||||
# 使用conn对象中的事件循环
|
# 使用conn对象中的事件循环
|
||||||
loop = conn.loop
|
loop = conn.loop
|
||||||
@@ -37,11 +46,14 @@ def create_iot_function(device_name, method_name, method_info):
|
|||||||
根据IOT设备描述生成通用的控制函数
|
根据IOT设备描述生成通用的控制函数
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def iot_control_function(conn, response_success=None, response_failure=None, **params):
|
async def iot_control_function(
|
||||||
|
conn, response_success=None, response_failure=None, **params
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
# 打印响应参数
|
# 打印响应参数
|
||||||
logger.bind(tag=TAG).info(
|
logger.bind(tag=TAG).info(
|
||||||
f"控制函数接收到的响应参数: success='{response_success}', failure='{response_failure}'")
|
f"控制函数接收到的响应参数: success='{response_success}', failure='{response_failure}'"
|
||||||
|
)
|
||||||
|
|
||||||
# 发送控制命令
|
# 发送控制命令
|
||||||
await send_iot_conn(conn, device_name, method_name, params)
|
await send_iot_conn(conn, device_name, method_name, params)
|
||||||
@@ -51,14 +63,15 @@ def create_iot_function(device_name, method_name, method_info):
|
|||||||
# 生成结果信息
|
# 生成结果信息
|
||||||
result = f"{device_name}的{method_name}操作执行成功"
|
result = f"{device_name}的{method_name}操作执行成功"
|
||||||
|
|
||||||
|
|
||||||
# 处理响应中可能的占位符
|
# 处理响应中可能的占位符
|
||||||
response = response_success
|
response = response_success
|
||||||
# 替换{value}占位符
|
# 替换{value}占位符
|
||||||
for param_name, param_value in params.items():
|
for param_name, param_value in params.items():
|
||||||
# 先尝试直接替换参数值
|
# 先尝试直接替换参数值
|
||||||
if "{" + param_name + "}" in response:
|
if "{" + param_name + "}" in response:
|
||||||
response = response.replace("{" + param_name + "}", str(param_value))
|
response = response.replace(
|
||||||
|
"{" + param_name + "}", str(param_value)
|
||||||
|
)
|
||||||
|
|
||||||
# 如果有{value}占位符,用相关参数替换
|
# 如果有{value}占位符,用相关参数替换
|
||||||
if "{value}" in response:
|
if "{value}" in response:
|
||||||
@@ -86,7 +99,8 @@ def create_iot_query_function(device_name, prop_name, prop_info):
|
|||||||
try:
|
try:
|
||||||
# 打印响应参数
|
# 打印响应参数
|
||||||
logger.bind(tag=TAG).info(
|
logger.bind(tag=TAG).info(
|
||||||
f"查询函数接收到的响应参数: success='{response_success}', failure='{response_failure}'")
|
f"查询函数接收到的响应参数: success='{response_success}', failure='{response_failure}'"
|
||||||
|
)
|
||||||
|
|
||||||
value = await get_iot_status(conn, device_name, prop_name)
|
value = await get_iot_status(conn, device_name, prop_name)
|
||||||
|
|
||||||
@@ -126,7 +140,7 @@ class IotDescriptor:
|
|||||||
# 根据描述创建属性
|
# 根据描述创建属性
|
||||||
for key, value in properties.items():
|
for key, value in properties.items():
|
||||||
property_item = globals()[key] = {}
|
property_item = globals()[key] = {}
|
||||||
property_item['name'] = key
|
property_item["name"] = key
|
||||||
property_item["description"] = value["description"]
|
property_item["description"] = value["description"]
|
||||||
if value["type"] == "number":
|
if value["type"] == "number":
|
||||||
property_item["value"] = 0
|
property_item["value"] = 0
|
||||||
@@ -140,7 +154,7 @@ class IotDescriptor:
|
|||||||
for key, value in methods.items():
|
for key, value in methods.items():
|
||||||
method = globals()[key] = {}
|
method = globals()[key] = {}
|
||||||
method["description"] = value["description"]
|
method["description"] = value["description"]
|
||||||
method['name'] = key
|
method["name"] = key
|
||||||
for k, v in value["parameters"].items():
|
for k, v in value["parameters"].items():
|
||||||
method[k] = {}
|
method[k] = {}
|
||||||
method[k]["description"] = v["description"]
|
method[k]["description"] = v["description"]
|
||||||
@@ -177,19 +191,21 @@ def register_device_type(descriptor):
|
|||||||
"properties": {
|
"properties": {
|
||||||
"response_success": {
|
"response_success": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": f"查询成功时的友好回复,必须使用{{value}}作为占位符表示查询到的值"
|
"description": f"查询成功时的友好回复,必须使用{{value}}作为占位符表示查询到的值",
|
||||||
},
|
},
|
||||||
"response_failure": {
|
"response_failure": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": f"查询失败时的友好回复,例如:'无法获取{device_name}的{prop_info['description']}'"
|
"description": f"查询失败时的友好回复,例如:'无法获取{device_name}的{prop_info['description']}'",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"required": ["response_success", "response_failure"]
|
"required": ["response_success", "response_failure"],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
query_func = create_iot_query_function(device_name, prop_name, prop_info)
|
query_func = create_iot_query_function(device_name, prop_name, prop_info)
|
||||||
decorated_func = register_function(func_name, func_desc, ToolType.IOT_CTL)(query_func)
|
decorated_func = register_function(func_name, func_desc, ToolType.IOT_CTL)(
|
||||||
|
query_func
|
||||||
|
)
|
||||||
functions[func_name] = decorated_func
|
functions[func_name] = decorated_func
|
||||||
|
|
||||||
# 为每个方法创建控制函数
|
# 为每个方法创建控制函数
|
||||||
@@ -200,22 +216,24 @@ def register_device_type(descriptor):
|
|||||||
parameters = {
|
parameters = {
|
||||||
param_name: {
|
param_name: {
|
||||||
"type": param_info["type"],
|
"type": param_info["type"],
|
||||||
"description": param_info["description"]
|
"description": param_info["description"],
|
||||||
}
|
}
|
||||||
for param_name, param_info in method_info["parameters"].items()
|
for param_name, param_info in method_info["parameters"].items()
|
||||||
}
|
}
|
||||||
|
|
||||||
# 添加响应参数
|
# 添加响应参数
|
||||||
parameters.update({
|
parameters.update(
|
||||||
"response_success": {
|
{
|
||||||
"type": "string",
|
"response_success": {
|
||||||
"description": "操作成功时的友好回复,关于该设备的操作结果,设备名称尽量使用description中的名称"
|
"type": "string",
|
||||||
},
|
"description": "操作成功时的友好回复,关于该设备的操作结果,设备名称尽量使用description中的名称",
|
||||||
"response_failure": {
|
},
|
||||||
"type": "string",
|
"response_failure": {
|
||||||
"description": "操作失败时的友好回复,关于该设备的操作结果,设备名称尽量使用description中的名称"
|
"type": "string",
|
||||||
|
"description": "操作失败时的友好回复,关于该设备的操作结果,设备名称尽量使用description中的名称",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
# 构建必须参数列表(原有参数 + 响应参数)
|
# 构建必须参数列表(原有参数 + 响应参数)
|
||||||
required_params = list(method_info["parameters"].keys())
|
required_params = list(method_info["parameters"].keys())
|
||||||
@@ -229,12 +247,14 @@ def register_device_type(descriptor):
|
|||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": parameters,
|
"properties": parameters,
|
||||||
"required": required_params
|
"required": required_params,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
control_func = create_iot_function(device_name, method_name, method_info)
|
control_func = create_iot_function(device_name, method_name, method_info)
|
||||||
decorated_func = register_function(func_name, func_desc, ToolType.IOT_CTL)(control_func)
|
decorated_func = register_function(func_name, func_desc, ToolType.IOT_CTL)(
|
||||||
|
control_func
|
||||||
|
)
|
||||||
functions[func_name] = decorated_func
|
functions[func_name] = decorated_func
|
||||||
|
|
||||||
device_type_registry.register_device_type(type_id, functions)
|
device_type_registry.register_device_type(type_id, functions)
|
||||||
@@ -243,13 +263,24 @@ def register_device_type(descriptor):
|
|||||||
|
|
||||||
# 用于接受前端设备推送的搜索iot描述
|
# 用于接受前端设备推送的搜索iot描述
|
||||||
async def handleIotDescriptors(conn, descriptors):
|
async def handleIotDescriptors(conn, descriptors):
|
||||||
|
wait_max_time = 5
|
||||||
|
while conn.func_handler is None or not conn.func_handler.finish_init:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
wait_max_time -= 1
|
||||||
|
if wait_max_time <= 0:
|
||||||
|
logger.bind(tag=TAG).error("连接对象没有func_handler")
|
||||||
|
return
|
||||||
"""处理物联网描述"""
|
"""处理物联网描述"""
|
||||||
functions_changed = False
|
functions_changed = False
|
||||||
|
|
||||||
for descriptor in descriptors:
|
for descriptor in descriptors:
|
||||||
# 创建IOT设备描述符
|
# 创建IOT设备描述符
|
||||||
iot_descriptor = IotDescriptor(descriptor["name"], descriptor["description"], descriptor["properties"],
|
iot_descriptor = IotDescriptor(
|
||||||
descriptor["methods"])
|
descriptor["name"],
|
||||||
|
descriptor["description"],
|
||||||
|
descriptor["properties"],
|
||||||
|
descriptor["methods"],
|
||||||
|
)
|
||||||
conn.iot_descriptors[descriptor["name"]] = iot_descriptor
|
conn.iot_descriptors[descriptor["name"]] = iot_descriptor
|
||||||
|
|
||||||
if conn.use_function_call_mode:
|
if conn.use_function_call_mode:
|
||||||
@@ -258,18 +289,22 @@ async def handleIotDescriptors(conn, descriptors):
|
|||||||
device_functions = device_type_registry.get_device_functions(type_id)
|
device_functions = device_type_registry.get_device_functions(type_id)
|
||||||
|
|
||||||
# 在连接级注册设备函数
|
# 在连接级注册设备函数
|
||||||
if hasattr(conn, 'func_handler'):
|
if hasattr(conn, "func_handler"):
|
||||||
for func_name in device_functions:
|
for func_name in device_functions:
|
||||||
conn.func_handler.function_registry.register_function(func_name)
|
conn.func_handler.function_registry.register_function(func_name)
|
||||||
logger.bind(tag=TAG).info(f"注册IOT函数到function handler: {func_name}")
|
logger.bind(tag=TAG).info(
|
||||||
|
f"注册IOT函数到function handler: {func_name}"
|
||||||
|
)
|
||||||
functions_changed = True
|
functions_changed = True
|
||||||
|
|
||||||
# 如果注册了新函数,更新function描述列表
|
# 如果注册了新函数,更新function描述列表
|
||||||
if functions_changed and hasattr(conn, 'func_handler'):
|
if functions_changed and hasattr(conn, "func_handler"):
|
||||||
conn.func_handler.upload_functions_desc()
|
conn.func_handler.upload_functions_desc()
|
||||||
func_names = conn.func_handler.current_support_functions()
|
func_names = conn.func_handler.current_support_functions()
|
||||||
logger.bind(tag=TAG).info(f"设备类型: {type_id}")
|
logger.bind(tag=TAG).info(f"设备类型: {type_id}")
|
||||||
logger.bind(tag=TAG).info(f"更新function描述列表完成,当前支持的函数: {func_names}")
|
logger.bind(tag=TAG).info(
|
||||||
|
f"更新function描述列表完成,当前支持的函数: {func_names}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def handleIotStatus(conn, states):
|
async def handleIotStatus(conn, states):
|
||||||
@@ -281,11 +316,15 @@ async def handleIotStatus(conn, states):
|
|||||||
for k, v in state["state"].items():
|
for k, v in state["state"].items():
|
||||||
if property_item["name"] == k:
|
if property_item["name"] == k:
|
||||||
if type(v) != type(property_item["value"]):
|
if type(v) != type(property_item["value"]):
|
||||||
logger.bind(tag=TAG).error(f"属性{property_item['name']}的值类型不匹配")
|
logger.bind(tag=TAG).error(
|
||||||
|
f"属性{property_item['name']}的值类型不匹配"
|
||||||
|
)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
property_item["value"] = v
|
property_item["value"] = v
|
||||||
logger.bind(tag=TAG).info(f"物联网状态更新: {key} , {property_item['name']} = {v}")
|
logger.bind(tag=TAG).info(
|
||||||
|
f"物联网状态更新: {key} , {property_item['name']} = {v}"
|
||||||
|
)
|
||||||
break
|
break
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -308,10 +347,14 @@ async def set_iot_status(conn, name, property_name, value):
|
|||||||
for property_item in iot_descriptor.properties:
|
for property_item in iot_descriptor.properties:
|
||||||
if property_item["name"] == property_name:
|
if property_item["name"] == property_name:
|
||||||
if type(value) != type(property_item["value"]):
|
if type(value) != type(property_item["value"]):
|
||||||
logger.bind(tag=TAG).error(f"属性{property_item['name']}的值类型不匹配")
|
logger.bind(tag=TAG).error(
|
||||||
|
f"属性{property_item['name']}的值类型不匹配"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
property_item["value"] = value
|
property_item["value"] = value
|
||||||
logger.bind(tag=TAG).info(f"物联网状态更新: {name} , {property_name} = {value}")
|
logger.bind(tag=TAG).info(
|
||||||
|
f"物联网状态更新: {name} , {property_name} = {value}"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
logger.bind(tag=TAG).warning(f"未找到设备 {name} 的属性 {property_name}")
|
logger.bind(tag=TAG).warning(f"未找到设备 {name} 的属性 {property_name}")
|
||||||
|
|
||||||
@@ -324,15 +367,19 @@ async def send_iot_conn(conn, name, method_name, parameters):
|
|||||||
for method in value.methods:
|
for method in value.methods:
|
||||||
# 找到了方法
|
# 找到了方法
|
||||||
if method["name"] == method_name:
|
if method["name"] == method_name:
|
||||||
await conn.websocket.send(json.dumps({
|
await conn.websocket.send(
|
||||||
"type": "iot",
|
json.dumps(
|
||||||
"commands": [
|
|
||||||
{
|
{
|
||||||
"name": name,
|
"type": "iot",
|
||||||
"method": method_name,
|
"commands": [
|
||||||
"parameters": parameters
|
{
|
||||||
|
"name": name,
|
||||||
|
"method": method_name,
|
||||||
|
"parameters": parameters,
|
||||||
|
}
|
||||||
|
],
|
||||||
}
|
}
|
||||||
]
|
)
|
||||||
}))
|
)
|
||||||
return
|
return
|
||||||
logger.bind(tag=TAG).error(f"未找到方法{method_name}")
|
logger.bind(tag=TAG).error(f"未找到方法{method_name}")
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ async def handleAudioMessage(conn, audio):
|
|||||||
if have_voice == False and conn.client_have_voice == False:
|
if have_voice == False and conn.client_have_voice == False:
|
||||||
await no_voice_close_connect(conn)
|
await no_voice_close_connect(conn)
|
||||||
conn.asr_audio.append(audio)
|
conn.asr_audio.append(audio)
|
||||||
conn.asr_audio = conn.asr_audio[-5:] # 保留最新的5帧音频内容,解决ASR句首丢字问题
|
conn.asr_audio = conn.asr_audio[
|
||||||
|
-10:
|
||||||
|
] # 保留最新的10帧音频内容,解决ASR句首丢字问题
|
||||||
return
|
return
|
||||||
conn.client_no_voice_last_time = 0.0
|
conn.client_no_voice_last_time = 0.0
|
||||||
conn.asr_audio.append(audio)
|
conn.asr_audio.append(audio)
|
||||||
@@ -30,10 +32,12 @@ async def handleAudioMessage(conn, audio):
|
|||||||
conn.client_abort = False
|
conn.client_abort = False
|
||||||
conn.asr_server_receive = False
|
conn.asr_server_receive = False
|
||||||
# 音频太短了,无法识别
|
# 音频太短了,无法识别
|
||||||
if len(conn.asr_audio) < 10:
|
if len(conn.asr_audio) < 15:
|
||||||
conn.asr_server_receive = True
|
conn.asr_server_receive = True
|
||||||
else:
|
else:
|
||||||
text, file_path = await conn.asr.speech_to_text(conn.asr_audio, conn.session_id)
|
text, file_path = await conn.asr.speech_to_text(
|
||||||
|
conn.asr_audio, conn.session_id
|
||||||
|
)
|
||||||
logger.bind(tag=TAG).info(f"识别文本: {text}")
|
logger.bind(tag=TAG).info(f"识别文本: {text}")
|
||||||
text_len, _ = remove_punctuation_and_length(text)
|
text_len, _ = remove_punctuation_and_length(text)
|
||||||
if text_len > 0:
|
if text_len > 0:
|
||||||
@@ -67,10 +71,17 @@ async def no_voice_close_connect(conn):
|
|||||||
conn.client_no_voice_last_time = time.time() * 1000
|
conn.client_no_voice_last_time = time.time() * 1000
|
||||||
else:
|
else:
|
||||||
no_voice_time = time.time() * 1000 - conn.client_no_voice_last_time
|
no_voice_time = time.time() * 1000 - conn.client_no_voice_last_time
|
||||||
close_connection_no_voice_time = conn.config.get("close_connection_no_voice_time", 120)
|
close_connection_no_voice_time = conn.config.get(
|
||||||
if not conn.close_after_chat and no_voice_time > 1000 * close_connection_no_voice_time:
|
"close_connection_no_voice_time", 120
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
not conn.close_after_chat
|
||||||
|
and no_voice_time > 1000 * close_connection_no_voice_time
|
||||||
|
):
|
||||||
conn.close_after_chat = True
|
conn.close_after_chat = True
|
||||||
conn.client_abort = False
|
conn.client_abort = False
|
||||||
conn.asr_server_receive = False
|
conn.asr_server_receive = False
|
||||||
prompt = "请你以“时间过得真快”未来头,用富有感情、依依不舍的话来结束这场对话吧。"
|
prompt = (
|
||||||
|
"请你以“时间过得真快”未来头,用富有感情、依依不舍的话来结束这场对话吧。"
|
||||||
|
)
|
||||||
await startToChat(conn, prompt)
|
await startToChat(conn, prompt)
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ from config.logger import setup_logging
|
|||||||
import json
|
import json
|
||||||
from core.handle.abortHandle import handleAbortMessage
|
from core.handle.abortHandle import handleAbortMessage
|
||||||
from core.handle.helloHandle import handleHelloMessage
|
from core.handle.helloHandle import handleHelloMessage
|
||||||
|
from core.utils.util import remove_punctuation_and_length
|
||||||
from core.handle.receiveAudioHandle import startToChat, handleAudioMessage
|
from core.handle.receiveAudioHandle import startToChat, handleAudioMessage
|
||||||
|
from core.handle.sendAudioHandle import send_stt_message, send_tts_message
|
||||||
from core.handle.iotHandle import handleIotDescriptors, handleIotStatus
|
from core.handle.iotHandle import handleIotDescriptors, handleIotStatus
|
||||||
|
import asyncio
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
@@ -32,17 +35,31 @@ async def handleTextMessage(conn, message):
|
|||||||
conn.client_have_voice = True
|
conn.client_have_voice = True
|
||||||
conn.client_voice_stop = True
|
conn.client_voice_stop = True
|
||||||
if len(conn.asr_audio) > 0:
|
if len(conn.asr_audio) > 0:
|
||||||
await handleAudioMessage(conn, b'')
|
await handleAudioMessage(conn, b"")
|
||||||
elif msg_json["state"] == "detect":
|
elif msg_json["state"] == "detect":
|
||||||
conn.asr_server_receive = False
|
conn.asr_server_receive = False
|
||||||
conn.client_have_voice = False
|
conn.client_have_voice = False
|
||||||
conn.asr_audio.clear()
|
conn.asr_audio.clear()
|
||||||
if "text" in msg_json:
|
if "text" in msg_json:
|
||||||
await startToChat(conn, msg_json["text"])
|
text = msg_json["text"]
|
||||||
|
_, text = remove_punctuation_and_length(text)
|
||||||
|
|
||||||
|
# 识别是否是唤醒词
|
||||||
|
is_wakeup_words = text in conn.config.get("wakeup_words")
|
||||||
|
# 是否开启唤醒词回复
|
||||||
|
enable_greeting = conn.config.get("enable_greeting", True)
|
||||||
|
|
||||||
|
if is_wakeup_words and not enable_greeting:
|
||||||
|
# 如果是唤醒词,且关闭了唤醒词回复,就不用回答
|
||||||
|
await send_stt_message(conn, text)
|
||||||
|
await send_tts_message(conn, "stop", None)
|
||||||
|
else:
|
||||||
|
# 否则需要LLM对文字内容进行答复
|
||||||
|
await startToChat(conn, text)
|
||||||
elif msg_json["type"] == "iot":
|
elif msg_json["type"] == "iot":
|
||||||
if "descriptors" in msg_json:
|
if "descriptors" in msg_json:
|
||||||
await handleIotDescriptors(conn, msg_json["descriptors"])
|
asyncio.create_task(handleIotDescriptors(conn, msg_json["descriptors"]))
|
||||||
if "states" in msg_json:
|
if "states" in msg_json:
|
||||||
await handleIotStatus(conn, msg_json["states"])
|
asyncio.create_task(handleIotStatus(conn, msg_json["states"]))
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
await conn.websocket.send(message)
|
await conn.websocket.send(message)
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
from typing import Optional
|
||||||
|
from contextlib import AsyncExitStack
|
||||||
|
import os, shutil
|
||||||
|
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):
|
||||||
|
args = self.config.get("args", [])
|
||||||
|
|
||||||
|
command = (
|
||||||
|
shutil.which("npx")
|
||||||
|
if self.config["command"] == "npx"
|
||||||
|
else self.config["command"]
|
||||||
|
)
|
||||||
|
|
||||||
|
env={**os.environ}
|
||||||
|
if self.config.get("env"):
|
||||||
|
env.update(self.config["env"])
|
||||||
|
|
||||||
|
server_params = StdioServerParameters(
|
||||||
|
command=command,
|
||||||
|
args=args,
|
||||||
|
env=env
|
||||||
|
)
|
||||||
|
|
||||||
|
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
|
||||||
|
self.stdio, self.write = stdio_transport
|
||||||
|
time_out_delta = timedelta(seconds=15)
|
||||||
|
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()
|
||||||
|
|
||||||
|
# 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 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,
|
||||||
|
"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):
|
||||||
|
self.logger.bind(tag=TAG).info(f"MCPClient Calling tool {tool_name} with args: {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):
|
||||||
|
"""Clean up resources"""
|
||||||
|
await self.exit_stack.aclose()
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
"""MCP服务管理器"""
|
||||||
|
import os, 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.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]:
|
||||||
|
"""加载MCP服务配置
|
||||||
|
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)
|
||||||
|
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}")
|
||||||
|
self.conn.func_handler.upload_functions_desc()
|
||||||
|
|
||||||
|
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: 工具未找到时抛出
|
||||||
|
"""
|
||||||
|
self.logger.bind(tag=TAG).info(f"Executing tool {tool_name} with arguments: {arguments}")
|
||||||
|
for client in self.client.values():
|
||||||
|
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")
|
||||||
|
|
||||||
|
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()
|
||||||
@@ -10,14 +10,21 @@ class IntentProviderBase(ABC):
|
|||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.intent_options = config.get("intent_options", {
|
self.intent_options = config.get("intent_options", {
|
||||||
|
"handle_exit_intent": "结束聊天, 用户发来如再见之类的表示结束的话, 不想再进行对话的时候",
|
||||||
|
"play_music": "播放音乐, 用户希望你可以播放音乐, 只用于播放音乐的意图",
|
||||||
|
"get_weather": "查询天气, 用户希望查询某个地点的天气情况",
|
||||||
|
"get_news": "查询新闻, 用户希望查询最新新闻或特定类型的新闻",
|
||||||
|
"get_lunar": "用于获取今天的阴历/农历和黄历信息",
|
||||||
|
"get_time": "获取今天日期或者当前时间信息",
|
||||||
"continue_chat": "继续聊天",
|
"continue_chat": "继续聊天",
|
||||||
"end_chat": "结束聊天",
|
|
||||||
"play_music": "播放音乐"
|
|
||||||
})
|
})
|
||||||
|
|
||||||
def set_llm(self, llm):
|
def set_llm(self, llm):
|
||||||
self.llm = llm
|
self.llm = llm
|
||||||
logger.bind(tag=TAG).debug("Set LLM for intent provider")
|
# 获取模型名称和类型信息
|
||||||
|
model_name = getattr(llm, 'model_name', str(llm.__class__.__name__))
|
||||||
|
# 记录更详细的日志
|
||||||
|
logger.bind(tag=TAG).info(f"意图识别设置LLM: {model_name}")
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def detect_intent(self, conn, dialogue_history: List[Dict], text: str) -> str:
|
async def detect_intent(self, conn, dialogue_history: List[Dict], text: str) -> str:
|
||||||
@@ -30,5 +37,6 @@ class IntentProviderBase(ABC):
|
|||||||
- "继续聊天"
|
- "继续聊天"
|
||||||
- "结束聊天"
|
- "结束聊天"
|
||||||
- "播放音乐 歌名" 或 "随机播放音乐"
|
- "播放音乐 歌名" 或 "随机播放音乐"
|
||||||
|
- "查询天气 地点名" 或 "查询天气 [当前位置]"
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -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"]
|
||||||
@@ -3,6 +3,9 @@ from ..base import IntentProviderBase
|
|||||||
from plugins_func.functions.play_music import initialize_music_handler
|
from plugins_func.functions.play_music import initialize_music_handler
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
import re
|
import re
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
import time
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
@@ -13,6 +16,10 @@ class IntentProvider(IntentProviderBase):
|
|||||||
super().__init__(config)
|
super().__init__(config)
|
||||||
self.llm = None
|
self.llm = None
|
||||||
self.promot = self.get_intent_system_prompt()
|
self.promot = self.get_intent_system_prompt()
|
||||||
|
# 添加缓存管理
|
||||||
|
self.intent_cache = {} # 缓存意图识别结果
|
||||||
|
self.cache_expiry = 600 # 缓存有效期10分钟
|
||||||
|
self.cache_max_size = 100 # 最多缓存100个意图
|
||||||
|
|
||||||
def get_intent_system_prompt(self) -> str:
|
def get_intent_system_prompt(self) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -22,63 +29,120 @@ class IntentProvider(IntentProviderBase):
|
|||||||
"""
|
"""
|
||||||
intent_list = []
|
intent_list = []
|
||||||
|
|
||||||
"""
|
|
||||||
"continue_chat": "1.继续聊天, 除了播放音乐和结束聊天的时候的选项, 比如日常的聊天和问候, 对话等",
|
|
||||||
"end_chat": "2.结束聊天, 用户发来如再见之类的表示结束的话, 不想再进行对话的时候",
|
|
||||||
"play_music": "3.播放音乐, 用户希望你可以播放音乐, 只用于播放音乐的意图"
|
|
||||||
"""
|
|
||||||
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.继续聊天, 除了播放音乐和结束聊天的时候的选项, 比如日常的聊天和问候, 对话等")
|
|
||||||
else:
|
|
||||||
intent_list.append(value)
|
|
||||||
|
|
||||||
# "如果是唱歌、听歌、播放音乐,请指定歌名,格式为'播放音乐 [识别出的歌名]'。\n"
|
|
||||||
# "如果听不出具体歌名,可以返回'随机播放音乐'。\n"
|
|
||||||
# "只需要返回意图结果的json,不要解释。"
|
|
||||||
# "返回格式如下:\n"
|
|
||||||
prompt = (
|
prompt = (
|
||||||
"你是一个意图识别助手。你需要根据和用户的对话记录,重点分析用户的最后一句话,判断用户意图属于以下哪一类(使用<start>和<end>标志):\n"
|
"你是一个意图识别助手。请分析用户的最后一句话,判断用户意图属于以下哪一类:\n"
|
||||||
"<start>"
|
"<start>"
|
||||||
f"{', '.join(intent_list)}"
|
f"{', '.join(intent_list)}"
|
||||||
"<end>\n"
|
"<end>\n"
|
||||||
"你需要按照以下的步骤处理用户的对话"
|
"处理步骤:"
|
||||||
"1. 思考出对话的意图是哪一类的"
|
"1. 思考意图类型,生成function_call格式"
|
||||||
"2. 属于1和2的意图, 直接返回,返回格式如下:\n"
|
"\n\n"
|
||||||
"{intent: '用户意图'}\n"
|
"返回格式示例:\n"
|
||||||
"3. 属于3的意图,则继续分析用户希望播放的音乐\n"
|
"1. 播放音乐意图: {\"function_call\": {\"name\": \"play_music\", \"arguments\": {\"song_name\": \"音乐名称\"}}}\n"
|
||||||
"4. 如果无法识别出具体歌名,可以返回'随机播放音乐'\n"
|
"2. 查询天气意图: {\"function_call\": {\"name\": \"get_weather\", \"arguments\": {\"location\": \"地点名称\", \"lang\": \"zh_CN\"}}}\n"
|
||||||
"{intent: '播放音乐 [获取的音乐名字]'}\n"
|
"3. 查询新闻意图: {\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": \"新闻类别\", \"detail\": false, \"lang\": \"zh_CN\"}}}\n"
|
||||||
"下面是几个处理的示例(思考的内容不返回, 只返回json部分, 无额外的内容)\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"
|
"用户: 你今天怎么样?\n"
|
||||||
"思考(不返回): 用户发来的数据是一个问候语,属于继续聊天的意图, 是种类1, 种类1的需求是直接返回\n"
|
"返回: {\"function_call\": {\"name\": \"continue_chat\"}}\n"
|
||||||
"返回结果: {intent: '继续聊天'}\n"
|
"```\n"
|
||||||
"```"
|
"```\n"
|
||||||
"用户: 我今天有点累了, 我们明天再聊吧\n"
|
"用户: 现在是几号了?现在几点了?\n"
|
||||||
"思考(不返回): 用户表达了今天不想继续对话,属于结束聊天的意图, 是种类2, 种类2的需求是直接返回\n"
|
"返回: {\"function_call\": {\"name\": \"get_time\"}}\n"
|
||||||
"返回结果: {intent: '结束聊天'}\n"
|
"```\n"
|
||||||
"```"
|
"```\n"
|
||||||
"用户: 我今天有点累了, 我们明天再聊吧\n"
|
"用户: 今天农历是多少?\n"
|
||||||
"思考(不返回): 用户表达了今天不想继续对话,属于结束聊天的意图, 是种类2, 种类2的需求是直接返回\n"
|
"返回: {\"function_call\": {\"name\": \"get_lunar\"}}\n"
|
||||||
"返回结果: {intent: '结束聊天'}\n"
|
"```\n"
|
||||||
"```"
|
"```\n"
|
||||||
"用户: 你可以播放一首中秋月给我听吗\n"
|
"用户: 我们明天再聊吧\n"
|
||||||
"思考(不返回): 用户表达了想听音乐的续签,属于播放音乐的意图, 是种类3, 种类3的需求需要继续判断播放的音乐, 这里用户希望的歌曲名明确给出是中秋月\n"
|
"返回: {\"function_call\": {\"name\": \"handle_exit_intent\"}}\n"
|
||||||
"返回结果: {intent: '播放音乐 [中秋月]'}\n"
|
"```\n"
|
||||||
"```"
|
"```\n"
|
||||||
"你现在可以使用的音乐的名称如下(使用<start>和<end>标志):\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
|
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]
|
||||||
|
|
||||||
async def detect_intent(self, conn, dialogue_history: List[Dict], text: str) -> str:
|
async def detect_intent(self, conn, dialogue_history: List[Dict], text: str) -> str:
|
||||||
if not self.llm:
|
if not self.llm:
|
||||||
raise ValueError("LLM provider not set")
|
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}")
|
||||||
|
|
||||||
|
# 计算缓存键
|
||||||
|
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 = ""
|
msgStr = ""
|
||||||
|
|
||||||
@@ -94,18 +158,78 @@ class IntentProvider(IntentProviderBase):
|
|||||||
music_file_names = music_config["music_file_names"]
|
music_file_names = music_config["music_file_names"]
|
||||||
prompt_music = f"{self.promot}\n<start>{music_file_names}\n<end>"
|
prompt_music = f"{self.promot}\n<start>{music_file_names}\n<end>"
|
||||||
logger.bind(tag=TAG).debug(f"User prompt: {prompt_music}")
|
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进行意图识别
|
||||||
|
llm_start_time = time.time()
|
||||||
|
logger.bind(tag=TAG).info(f"开始LLM意图识别调用, 模型: {model_info}")
|
||||||
|
|
||||||
intent = self.llm.response_no_stream(
|
intent = self.llm.response_no_stream(
|
||||||
system_prompt=prompt_music,
|
system_prompt=prompt_music,
|
||||||
user_prompt=user_prompt
|
user_prompt=user_prompt
|
||||||
)
|
)
|
||||||
# 使用正则表达式提取大括号中的内容
|
|
||||||
# 使用正则表达式提取 {} 中的内容
|
# 记录LLM调用完成时间
|
||||||
match = re.search(r'\{.*?\}', intent)
|
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:
|
if match:
|
||||||
result = match.group(0)
|
intent = match.group(0)
|
||||||
intent = result
|
|
||||||
else:
|
# 记录总处理时间
|
||||||
intent = "{intent: '继续聊天'}"
|
total_time = time.time() - total_start_time
|
||||||
logger.bind(tag=TAG).info(f"Detected intent: {intent}")
|
logger.bind(tag=TAG).info(f"【意图识别性能】模型: {model_info}, 总耗时: {total_time:.4f}秒, LLM调用: {llm_time:.4f}秒, 查询: '{text[:20]}...'")
|
||||||
return intent.strip()
|
|
||||||
|
# 尝试解析为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\": \"继续聊天\"}"
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
from config.logger import setup_logging
|
||||||
|
from http import HTTPStatus
|
||||||
|
from dashscope import Application
|
||||||
|
from core.providers.llm.base import LLMProviderBase
|
||||||
|
|
||||||
|
TAG = __name__
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
class LLMProvider(LLMProviderBase):
|
||||||
|
def __init__(self, config):
|
||||||
|
self.api_key = config["api_key"]
|
||||||
|
self.app_id = config["app_id"]
|
||||||
|
self.base_url = config.get("base_url")
|
||||||
|
self.is_No_prompt = config.get("is_no_prompt")
|
||||||
|
self.memory_id = config.get("ali_memory_id")
|
||||||
|
|
||||||
|
def response(self, session_id, dialogue):
|
||||||
|
try:
|
||||||
|
# 处理dialogue
|
||||||
|
if self.is_No_prompt:
|
||||||
|
dialogue.pop(0)
|
||||||
|
logger.bind(tag=TAG).debug(f"【阿里百练API服务】处理后的dialogue: {dialogue}")
|
||||||
|
|
||||||
|
# 构造调用参数
|
||||||
|
call_params = {
|
||||||
|
"api_key": self.api_key,
|
||||||
|
"app_id": self.app_id,
|
||||||
|
"session_id": session_id,
|
||||||
|
"messages": dialogue
|
||||||
|
}
|
||||||
|
if self.memory_id != False:
|
||||||
|
# 百练memory需要prompt参数
|
||||||
|
prompt = dialogue[-1].get("content")
|
||||||
|
call_params["memory_id"] = self.memory_id
|
||||||
|
call_params["prompt"] = prompt
|
||||||
|
logger.bind(tag=TAG).debug(f"【阿里百练API服务】处理后的prompt: {prompt}")
|
||||||
|
|
||||||
|
responses = Application.call(**call_params)
|
||||||
|
if responses.status_code != HTTPStatus.OK:
|
||||||
|
logger.bind(tag=TAG).error(
|
||||||
|
f"code={responses.status_code}, "
|
||||||
|
f"message={responses.message}, "
|
||||||
|
f"请参考文档:https://help.aliyun.com/zh/model-studio/developer-reference/error-code"
|
||||||
|
)
|
||||||
|
yield "【阿里百练API服务响应异常】"
|
||||||
|
else:
|
||||||
|
logger.bind(tag=TAG).debug(f"【阿里百练API服务】构造参数: {call_params}")
|
||||||
|
yield responses.output.text
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.bind(tag=TAG).error(f"【阿里百练API服务】响应异常: {e}")
|
||||||
|
yield "【LLM服务响应异常】"
|
||||||
@@ -6,11 +6,12 @@ from core.providers.llm.base import LLMProviderBase
|
|||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
|
|
||||||
|
|
||||||
class LLMProvider(LLMProviderBase):
|
class LLMProvider(LLMProviderBase):
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self.api_key = config["api_key"]
|
self.api_key = config["api_key"]
|
||||||
self.mode = config.get("mode", "chat-messages")
|
self.mode = config.get("mode", "chat-messages")
|
||||||
self.base_url = config.get("base_url", "https://api.dify.ai/v1").rstrip('/')
|
self.base_url = config.get("base_url", "https://api.dify.ai/v1").rstrip("/")
|
||||||
self.session_conversation_map = {} # 存储session_id和conversation_id的映射
|
self.session_conversation_map = {} # 存储session_id和conversation_id的映射
|
||||||
|
|
||||||
def response(self, session_id, dialogue):
|
def response(self, session_id, dialogue):
|
||||||
@@ -22,57 +23,58 @@ class LLMProvider(LLMProviderBase):
|
|||||||
# 发起流式请求
|
# 发起流式请求
|
||||||
if self.mode == "chat-messages":
|
if self.mode == "chat-messages":
|
||||||
request_json = {
|
request_json = {
|
||||||
"query": last_msg["content"],
|
"query": last_msg["content"],
|
||||||
"response_mode": "streaming",
|
"response_mode": "streaming",
|
||||||
"user": session_id,
|
"user": session_id,
|
||||||
"inputs": {},
|
"inputs": {},
|
||||||
"conversation_id": conversation_id
|
"conversation_id": conversation_id,
|
||||||
}
|
}
|
||||||
elif self.mode == "workflows/run":
|
elif self.mode == "workflows/run":
|
||||||
request_json = {
|
request_json = {
|
||||||
"inputs": {"query": last_msg["content"]},
|
"inputs": {"query": last_msg["content"]},
|
||||||
"response_mode": "streaming",
|
"response_mode": "streaming",
|
||||||
"user": session_id
|
"user": session_id,
|
||||||
}
|
}
|
||||||
elif self.mode == "completion-messages":
|
elif self.mode == "completion-messages":
|
||||||
request_json = {
|
request_json = {
|
||||||
"inputs": {"query": last_msg["content"]},
|
"inputs": {"query": last_msg["content"]},
|
||||||
"response_mode": "streaming",
|
"response_mode": "streaming",
|
||||||
"user": session_id
|
"user": session_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
with requests.post(
|
with requests.post(
|
||||||
f"{self.base_url}/{self.mode}",
|
f"{self.base_url}/{self.mode}",
|
||||||
headers={"Authorization": f"Bearer {self.api_key}"},
|
headers={"Authorization": f"Bearer {self.api_key}"},
|
||||||
json=request_json,
|
json=request_json,
|
||||||
stream=True
|
stream=True,
|
||||||
) as r:
|
) as r:
|
||||||
if self.mode == "chat-messages":
|
if self.mode == "chat-messages":
|
||||||
for line in r.iter_lines():
|
for line in r.iter_lines():
|
||||||
if line.startswith(b'data: '):
|
if line.startswith(b"data: "):
|
||||||
event = json.loads(line[6:])
|
event = json.loads(line[6:])
|
||||||
# 如果没有找到conversation_id,则获取此次conversation_id
|
# 如果没有找到conversation_id,则获取此次conversation_id
|
||||||
if not conversation_id:
|
if not conversation_id:
|
||||||
conversation_id = event.get('conversation_id')
|
conversation_id = event.get("conversation_id")
|
||||||
self.session_conversation_map[session_id] = conversation_id # 更新映射
|
self.session_conversation_map[session_id] = (
|
||||||
if event.get('answer'):
|
conversation_id # 更新映射
|
||||||
yield event['answer']
|
)
|
||||||
|
if event.get("answer"):
|
||||||
|
yield event["answer"]
|
||||||
elif self.mode == "workflows/run":
|
elif self.mode == "workflows/run":
|
||||||
for line in r.iter_lines():
|
for line in r.iter_lines():
|
||||||
# logger.bind(tag=TAG).info(f"chat message response: {line}")
|
if line.startswith(b"data: "):
|
||||||
if line.startswith(b'data: '):
|
|
||||||
event = json.loads(line[6:])
|
event = json.loads(line[6:])
|
||||||
if event.get('event') == "workflow_finished":
|
if event.get("event") == "workflow_finished":
|
||||||
if event['data']['status'] == "succeeded":
|
if event["data"]["status"] == "succeeded":
|
||||||
yield event['data']['outputs']['answer']
|
yield event["data"]["outputs"]["answer"]
|
||||||
else:
|
else:
|
||||||
yield "【服务响应异常】"
|
yield "【服务响应异常】"
|
||||||
elif self.mode == "completion-messages":
|
elif self.mode == "completion-messages":
|
||||||
for line in r.iter_lines():
|
for line in r.iter_lines():
|
||||||
if line.startswith(b'data: '):
|
if line.startswith(b"data: "):
|
||||||
event = json.loads(line[6:])
|
event = json.loads(line[6:])
|
||||||
if event.get('answer'):
|
if event.get("answer"):
|
||||||
yield event['answer']
|
yield event["answer"]
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"Error in response generation: {e}")
|
logger.bind(tag=TAG).error(f"Error in response generation: {e}")
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
import requests
|
|
||||||
from requests.exceptions import RequestException
|
|
||||||
from config.logger import setup_logging
|
|
||||||
from core.providers.llm.base import LLMProviderBase
|
|
||||||
|
|
||||||
TAG = __name__
|
|
||||||
logger = setup_logging()
|
|
||||||
|
|
||||||
|
|
||||||
class LLMProvider(LLMProviderBase):
|
|
||||||
def __init__(self, config):
|
|
||||||
self.agent_id = config.get("agent_id") # 对应 agent_id
|
|
||||||
self.api_key = config.get("api_key")
|
|
||||||
self.base_url = config.get("base_url", config.get("url")) # 默认使用 base_url
|
|
||||||
self.api_url = f"{self.base_url}/api/conversation/process" # 拼接完整的 API URL
|
|
||||||
|
|
||||||
def response(self, session_id, dialogue):
|
|
||||||
print(dialogue)
|
|
||||||
try:
|
|
||||||
# home assistant语音助手自带意图,无需使用xiaozhi ai自带的,只需要把用户说的话传递给home assistant即可
|
|
||||||
|
|
||||||
# 提取最后一个 role 为 'user' 的 content
|
|
||||||
input_text = None
|
|
||||||
if isinstance(dialogue, list): # 确保 dialogue 是一个列表
|
|
||||||
# 逆序遍历,找到最后一个 role 为 'user' 的消息
|
|
||||||
for message in reversed(dialogue):
|
|
||||||
if message.get("role") == "user": # 找到 role 为 'user' 的消息
|
|
||||||
input_text = message.get("content", "")
|
|
||||||
break # 找到后立即退出循环
|
|
||||||
|
|
||||||
# 构造请求数据
|
|
||||||
payload = {
|
|
||||||
"text": input_text,
|
|
||||||
"agent_id": self.agent_id,
|
|
||||||
"conversation_id": session_id # 使用 session_id 作为 conversation_id
|
|
||||||
}
|
|
||||||
# 设置请求头
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {self.api_key}",
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 发起 POST 请求
|
|
||||||
response = requests.post(self.api_url, json=payload, headers=headers)
|
|
||||||
|
|
||||||
# 检查请求是否成功
|
|
||||||
response.raise_for_status()
|
|
||||||
|
|
||||||
# 解析返回数据
|
|
||||||
data = response.json()
|
|
||||||
speech = data.get("response", {}).get("speech", {}).get("plain", {}).get("speech", "")
|
|
||||||
|
|
||||||
# 返回生成的内容
|
|
||||||
if speech:
|
|
||||||
yield speech
|
|
||||||
else:
|
|
||||||
logger.bind(tag=TAG).warning("API 返回数据中没有 speech 内容")
|
|
||||||
|
|
||||||
except RequestException as e:
|
|
||||||
logger.bind(tag=TAG).error(f"HTTP 请求错误: {e}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.bind(tag=TAG).error(f"生成响应时出错: {e}")
|
|
||||||
@@ -64,5 +64,5 @@ class LLMProvider(LLMProviderBase):
|
|||||||
yield chunk.choices[0].delta.content, chunk.choices[0].delta.tool_calls
|
yield chunk.choices[0].delta.content, chunk.choices[0].delta.tool_calls
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.bind(tag=TAG).error(f"Error in function call streaming: {e}")
|
logger.bind(tag=TAG).error(f"Error in function call streaming: {e}")
|
||||||
yield {"type": "content", "content": f"【OpenAI服务响应异常: {e}】"}
|
yield {"type": "content", "content": f"【OpenAI服务响应异常: {e}】"}
|
||||||
|
|||||||
@@ -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 '<think>' in content:
|
||||||
|
is_active = False
|
||||||
|
content = content.split('<think>')[0]
|
||||||
|
if '</think>' in content:
|
||||||
|
is_active = True
|
||||||
|
content = content.split('</think>')[-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)}】"}
|
||||||
@@ -121,9 +121,15 @@ class TTSProviderBase(ABC):
|
|||||||
async def tts_one_sentence(self, text, u_id=None):
|
async def tts_one_sentence(self, text, u_id=None):
|
||||||
if not u_id:
|
if not u_id:
|
||||||
u_id = str(uuid.uuid4()).replace("-", "")
|
u_id = str(uuid.uuid4()).replace("-", "")
|
||||||
self.tts.tts_text_queue.put(TTSMessageDTO(u_id=u_id, msg_type=MsgType.START_TTS_REQUEST, content=''))
|
self.tts.tts_text_queue.put(
|
||||||
self.tts.tts_text_queue.put(TTSMessageDTO(u_id=u_id, msg_type=MsgType.TTS_TEXT_REQUEST, content=text))
|
TTSMessageDTO(u_id=u_id, msg_type=MsgType.START_TTS_REQUEST, content="")
|
||||||
self.tts.tts_text_queue.put(TTSMessageDTO(u_id=u_id, msg_type=MsgType.STOP_TTS_REQUEST, content=text))
|
)
|
||||||
|
self.tts.tts_text_queue.put(
|
||||||
|
TTSMessageDTO(u_id=u_id, msg_type=MsgType.TTS_TEXT_REQUEST, content=text)
|
||||||
|
)
|
||||||
|
self.tts.tts_text_queue.put(
|
||||||
|
TTSMessageDTO(u_id=u_id, msg_type=MsgType.STOP_TTS_REQUEST, content=text)
|
||||||
|
)
|
||||||
|
|
||||||
def _enable_two_way_tts(self):
|
def _enable_two_way_tts(self):
|
||||||
while not self.stop_event.is_set():
|
while not self.stop_event.is_set():
|
||||||
@@ -273,7 +279,6 @@ class TTSProviderBase(ABC):
|
|||||||
def generate_filename(self):
|
def generate_filename(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def text_to_speak(self, u_id, text, is_last_text=False, is_first_text=False):
|
async def text_to_speak(self, u_id, text, is_last_text=False, is_first_text=False):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import json
|
|||||||
import base64
|
import base64
|
||||||
import requests
|
import requests
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from mutagen.oggopus import OggOpus
|
|
||||||
|
|
||||||
import websockets
|
import websockets
|
||||||
|
|
||||||
@@ -76,14 +75,16 @@ EVENT_TTSResponse = 352
|
|||||||
|
|
||||||
|
|
||||||
class Header:
|
class Header:
|
||||||
def __init__(self,
|
def __init__(
|
||||||
protocol_version=PROTOCOL_VERSION,
|
self,
|
||||||
header_size=DEFAULT_HEADER_SIZE,
|
protocol_version=PROTOCOL_VERSION,
|
||||||
message_type: int = 0,
|
header_size=DEFAULT_HEADER_SIZE,
|
||||||
message_type_specific_flags: int = 0,
|
message_type: int = 0,
|
||||||
serial_method: int = NO_SERIALIZATION,
|
message_type_specific_flags: int = 0,
|
||||||
compression_type: int = COMPRESSION_NO,
|
serial_method: int = NO_SERIALIZATION,
|
||||||
reserved_data=0):
|
compression_type: int = COMPRESSION_NO,
|
||||||
|
reserved_data=0,
|
||||||
|
):
|
||||||
self.header_size = header_size
|
self.header_size = header_size
|
||||||
self.protocol_version = protocol_version
|
self.protocol_version = protocol_version
|
||||||
self.message_type = message_type
|
self.message_type = message_type
|
||||||
@@ -93,16 +94,20 @@ class Header:
|
|||||||
self.reserved_data = reserved_data
|
self.reserved_data = reserved_data
|
||||||
|
|
||||||
def as_bytes(self) -> bytes:
|
def as_bytes(self) -> bytes:
|
||||||
return bytes([
|
return bytes(
|
||||||
(self.protocol_version << 4) | self.header_size,
|
[
|
||||||
(self.message_type << 4) | self.message_type_specific_flags,
|
(self.protocol_version << 4) | self.header_size,
|
||||||
(self.serial_method << 4) | self.compression_type,
|
(self.message_type << 4) | self.message_type_specific_flags,
|
||||||
self.reserved_data
|
(self.serial_method << 4) | self.compression_type,
|
||||||
])
|
self.reserved_data,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Optional:
|
class Optional:
|
||||||
def __init__(self, event: int = EVENT_NONE, sessionId: str = None, sequence: int = None):
|
def __init__(
|
||||||
|
self, event: int = EVENT_NONE, sessionId: str = None, sequence: int = None
|
||||||
|
):
|
||||||
self.event = event
|
self.event = event
|
||||||
self.sessionId = sessionId
|
self.sessionId = sessionId
|
||||||
self.errorCode: int = 0
|
self.errorCode: int = 0
|
||||||
@@ -160,45 +165,57 @@ class TTSProvider(TTSProviderBase):
|
|||||||
"X-Api-Resource-Id": self.resource_id,
|
"X-Api-Resource-Id": self.resource_id,
|
||||||
"X-Api-Connect-Id": uuid.uuid4(),
|
"X-Api-Connect-Id": uuid.uuid4(),
|
||||||
}
|
}
|
||||||
self.ws = await websockets.connect(self.ws_url, additional_headers=ws_header, max_size=1000000000)
|
self.ws = await websockets.connect(
|
||||||
tts_priority = threading.Thread(target=self._start_monitor_tts_response_thread(), daemon=True)
|
self.ws_url, additional_headers=ws_header, max_size=1000000000
|
||||||
|
)
|
||||||
|
tts_priority = threading.Thread(
|
||||||
|
target=self._start_monitor_tts_response_thread(), daemon=True
|
||||||
|
)
|
||||||
tts_priority.start()
|
tts_priority.start()
|
||||||
|
|
||||||
def generate_filename(self, extension=".wav"):
|
def generate_filename(self, extension=".wav"):
|
||||||
return os.path.join(self.output_file, f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}")
|
return os.path.join(
|
||||||
|
self.output_file,
|
||||||
|
f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
|
||||||
|
)
|
||||||
|
|
||||||
async def send_event(self, header: bytes, optional: bytes | None = None,
|
async def send_event(
|
||||||
payload: bytes = None):
|
self, header: bytes, optional: bytes | None = None, payload: bytes = None
|
||||||
|
):
|
||||||
full_client_request = bytearray(header)
|
full_client_request = bytearray(header)
|
||||||
if optional is not None:
|
if optional is not None:
|
||||||
full_client_request.extend(optional)
|
full_client_request.extend(optional)
|
||||||
if payload is not None:
|
if payload is not None:
|
||||||
payload_size = len(payload).to_bytes(4, 'big', signed=True)
|
payload_size = len(payload).to_bytes(4, "big", signed=True)
|
||||||
full_client_request.extend(payload_size)
|
full_client_request.extend(payload_size)
|
||||||
full_client_request.extend(payload)
|
full_client_request.extend(payload)
|
||||||
await self.ws.send(full_client_request)
|
await self.ws.send(full_client_request)
|
||||||
|
|
||||||
async def send_text(self, speaker: str, text: str, session_id):
|
async def send_text(self, speaker: str, text: str, session_id):
|
||||||
header = Header(message_type=FULL_CLIENT_REQUEST,
|
header = Header(
|
||||||
message_type_specific_flags=MsgTypeFlagWithEvent,
|
message_type=FULL_CLIENT_REQUEST,
|
||||||
serial_method=JSON).as_bytes()
|
message_type_specific_flags=MsgTypeFlagWithEvent,
|
||||||
|
serial_method=JSON,
|
||||||
|
).as_bytes()
|
||||||
optional = Optional(event=EVENT_TaskRequest, sessionId=session_id).as_bytes()
|
optional = Optional(event=EVENT_TaskRequest, sessionId=session_id).as_bytes()
|
||||||
payload = self.get_payload_bytes(event=EVENT_TaskRequest, text=text, speaker=speaker)
|
payload = self.get_payload_bytes(
|
||||||
|
event=EVENT_TaskRequest, text=text, speaker=speaker
|
||||||
|
)
|
||||||
return await self.send_event(header, optional, payload)
|
return await self.send_event(header, optional, payload)
|
||||||
|
|
||||||
# 读取 res 数组某段 字符串内容
|
# 读取 res 数组某段 字符串内容
|
||||||
def read_res_content(self, res: bytes, offset: int):
|
def read_res_content(self, res: bytes, offset: int):
|
||||||
content_size = int.from_bytes(res[offset: offset + 4], "big", signed=True)
|
content_size = int.from_bytes(res[offset : offset + 4], "big", signed=True)
|
||||||
offset += 4
|
offset += 4
|
||||||
content = str(res[offset: offset + content_size])
|
content = str(res[offset : offset + content_size])
|
||||||
offset += content_size
|
offset += content_size
|
||||||
return content, offset
|
return content, offset
|
||||||
|
|
||||||
# 读取 payload
|
# 读取 payload
|
||||||
def read_res_payload(self, res: bytes, offset: int):
|
def read_res_payload(self, res: bytes, offset: int):
|
||||||
payload_size = int.from_bytes(res[offset: offset + 4], "big", signed=True)
|
payload_size = int.from_bytes(res[offset : offset + 4], "big", signed=True)
|
||||||
offset += 4
|
offset += 4
|
||||||
payload = res[offset: offset + payload_size]
|
payload = res[offset : offset + payload_size]
|
||||||
offset += payload_size
|
offset += payload_size
|
||||||
return payload, offset
|
return payload, offset
|
||||||
|
|
||||||
@@ -211,11 +228,11 @@ class TTSProvider(TTSProviderBase):
|
|||||||
header = response.header
|
header = response.header
|
||||||
num = 0b00001111
|
num = 0b00001111
|
||||||
header.protocol_version = res[0] >> 4 & num
|
header.protocol_version = res[0] >> 4 & num
|
||||||
header.header_size = res[0] & 0x0f
|
header.header_size = res[0] & 0x0F
|
||||||
header.message_type = (res[1] >> 4) & num
|
header.message_type = (res[1] >> 4) & num
|
||||||
header.message_type_specific_flags = res[1] & 0x0f
|
header.message_type_specific_flags = res[1] & 0x0F
|
||||||
header.serialization_method = res[2] >> num
|
header.serialization_method = res[2] >> num
|
||||||
header.message_compression = res[2] & 0x0f
|
header.message_compression = res[2] & 0x0F
|
||||||
header.reserved = res[3]
|
header.reserved = res[3]
|
||||||
#
|
#
|
||||||
offset = 4
|
offset = 4
|
||||||
@@ -231,78 +248,101 @@ class TTSProvider(TTSProviderBase):
|
|||||||
elif optional.event == EVENT_ConnectionStarted:
|
elif optional.event == EVENT_ConnectionStarted:
|
||||||
optional.connectionId, offset = self.read_res_content(res, offset)
|
optional.connectionId, offset = self.read_res_content(res, offset)
|
||||||
elif optional.event == EVENT_ConnectionFailed:
|
elif optional.event == EVENT_ConnectionFailed:
|
||||||
optional.response_meta_json, offset = self.read_res_content(res, offset)
|
optional.response_meta_json, offset = self.read_res_content(
|
||||||
elif (optional.event == EVENT_SessionStarted
|
res, offset
|
||||||
or optional.event == EVENT_SessionFailed
|
)
|
||||||
or optional.event == EVENT_SessionFinished):
|
elif (
|
||||||
|
optional.event == EVENT_SessionStarted
|
||||||
|
or optional.event == EVENT_SessionFailed
|
||||||
|
or optional.event == EVENT_SessionFinished
|
||||||
|
):
|
||||||
optional.sessionId, offset = self.read_res_content(res, offset)
|
optional.sessionId, offset = self.read_res_content(res, offset)
|
||||||
optional.response_meta_json, offset = self.read_res_content(res, offset)
|
optional.response_meta_json, offset = self.read_res_content(
|
||||||
|
res, offset
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
optional.sessionId, offset = self.read_res_content(res, offset)
|
optional.sessionId, offset = self.read_res_content(res, offset)
|
||||||
response.payload, offset = self.read_res_payload(res, offset)
|
response.payload, offset = self.read_res_payload(res, offset)
|
||||||
|
|
||||||
elif header.message_type == ERROR_INFORMATION:
|
elif header.message_type == ERROR_INFORMATION:
|
||||||
optional.errorCode = int.from_bytes(res[offset:offset + 4], "big", signed=True)
|
optional.errorCode = int.from_bytes(
|
||||||
|
res[offset : offset + 4], "big", signed=True
|
||||||
|
)
|
||||||
offset += 4
|
offset += 4
|
||||||
response.payload, offset = self.read_res_payload(res, offset)
|
response.payload, offset = self.read_res_payload(res, offset)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
async def start_connection(self):
|
async def start_connection(self):
|
||||||
header = Header(message_type=FULL_CLIENT_REQUEST, message_type_specific_flags=MsgTypeFlagWithEvent).as_bytes()
|
header = Header(
|
||||||
|
message_type=FULL_CLIENT_REQUEST,
|
||||||
|
message_type_specific_flags=MsgTypeFlagWithEvent,
|
||||||
|
).as_bytes()
|
||||||
optional = Optional(event=EVENT_Start_Connection).as_bytes()
|
optional = Optional(event=EVENT_Start_Connection).as_bytes()
|
||||||
payload = str.encode("{}")
|
payload = str.encode("{}")
|
||||||
return await self.send_event(header, optional, payload)
|
return await self.send_event(header, optional, payload)
|
||||||
|
|
||||||
def print_response(self, res, tag_msg: str):
|
def print_response(self, res, tag_msg: str):
|
||||||
logger.bind(tag=TAG).info(f'===>{tag_msg} header:{res.header.__dict__}')
|
logger.bind(tag=TAG).info(f"===>{tag_msg} header:{res.header.__dict__}")
|
||||||
logger.bind(tag=TAG).info(f'===>{tag_msg} optional:{res.optional.__dict__}')
|
logger.bind(tag=TAG).info(f"===>{tag_msg} optional:{res.optional.__dict__}")
|
||||||
|
|
||||||
def get_payload_bytes(self, uid='1234', event=EVENT_NONE, text='', speaker='', audio_format='pcm',
|
def get_payload_bytes(
|
||||||
audio_sample_rate=16000):
|
self,
|
||||||
return str.encode(json.dumps(
|
uid="1234",
|
||||||
{
|
event=EVENT_NONE,
|
||||||
"user": {"uid": uid},
|
text="",
|
||||||
"event": event,
|
speaker="",
|
||||||
"namespace": "BidirectionalTTS",
|
audio_format="pcm",
|
||||||
"req_params": {
|
audio_sample_rate=16000,
|
||||||
"text": text,
|
):
|
||||||
"speaker": speaker,
|
return str.encode(
|
||||||
"audio_params": {
|
json.dumps(
|
||||||
"format": audio_format,
|
{
|
||||||
"sample_rate": audio_sample_rate
|
"user": {"uid": uid},
|
||||||
}
|
"event": event,
|
||||||
|
"namespace": "BidirectionalTTS",
|
||||||
|
"req_params": {
|
||||||
|
"text": text,
|
||||||
|
"speaker": speaker,
|
||||||
|
"audio_params": {
|
||||||
|
"format": audio_format,
|
||||||
|
"sample_rate": audio_sample_rate,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
))
|
)
|
||||||
|
|
||||||
async def finish_connection(self):
|
async def finish_connection(self):
|
||||||
header = Header(message_type=FULL_CLIENT_REQUEST,
|
header = Header(
|
||||||
message_type_specific_flags=MsgTypeFlagWithEvent,
|
message_type=FULL_CLIENT_REQUEST,
|
||||||
serial_method=JSON
|
message_type_specific_flags=MsgTypeFlagWithEvent,
|
||||||
).as_bytes()
|
serial_method=JSON,
|
||||||
|
).as_bytes()
|
||||||
optional = Optional(event=EVENT_FinishConnection).as_bytes()
|
optional = Optional(event=EVENT_FinishConnection).as_bytes()
|
||||||
payload = str.encode('{}')
|
payload = str.encode("{}")
|
||||||
await self.send_event(header, optional, payload)
|
await self.send_event(header, optional, payload)
|
||||||
return
|
return
|
||||||
|
|
||||||
async def start_session(self, session_id):
|
async def start_session(self, session_id):
|
||||||
self.stop_event_response.clear()
|
self.stop_event_response.clear()
|
||||||
header = Header(message_type=FULL_CLIENT_REQUEST,
|
header = Header(
|
||||||
message_type_specific_flags=MsgTypeFlagWithEvent,
|
message_type=FULL_CLIENT_REQUEST,
|
||||||
serial_method=JSON
|
message_type_specific_flags=MsgTypeFlagWithEvent,
|
||||||
).as_bytes()
|
serial_method=JSON,
|
||||||
|
).as_bytes()
|
||||||
optional = Optional(event=EVENT_StartSession, sessionId=session_id).as_bytes()
|
optional = Optional(event=EVENT_StartSession, sessionId=session_id).as_bytes()
|
||||||
payload = self.get_payload_bytes(event=EVENT_StartSession, speaker=self.speaker)
|
payload = self.get_payload_bytes(event=EVENT_StartSession, speaker=self.speaker)
|
||||||
await self.send_event(header, optional, payload)
|
await self.send_event(header, optional, payload)
|
||||||
|
|
||||||
async def finish_session(self, session_id):
|
async def finish_session(self, session_id):
|
||||||
self.stop_event_response.set()
|
self.stop_event_response.set()
|
||||||
header = Header(message_type=FULL_CLIENT_REQUEST,
|
header = Header(
|
||||||
message_type_specific_flags=MsgTypeFlagWithEvent,
|
message_type=FULL_CLIENT_REQUEST,
|
||||||
serial_method=JSON
|
message_type_specific_flags=MsgTypeFlagWithEvent,
|
||||||
).as_bytes()
|
serial_method=JSON,
|
||||||
|
).as_bytes()
|
||||||
optional = Optional(event=EVENT_FinishSession, sessionId=session_id).as_bytes()
|
optional = Optional(event=EVENT_FinishSession, sessionId=session_id).as_bytes()
|
||||||
payload = str.encode('{}')
|
payload = str.encode("{}")
|
||||||
await self.send_event(header, optional, payload)
|
await self.send_event(header, optional, payload)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -327,59 +367,78 @@ class TTSProvider(TTSProviderBase):
|
|||||||
|
|
||||||
def _start_monitor_tts_response_thread(self):
|
def _start_monitor_tts_response_thread(self):
|
||||||
# 初始化链接
|
# 初始化链接
|
||||||
asyncio.run_coroutine_threadsafe(self._start_monitor_tts_response(), loop=self.loop)
|
asyncio.run_coroutine_threadsafe(
|
||||||
|
self._start_monitor_tts_response(), loop=self.loop
|
||||||
|
)
|
||||||
|
|
||||||
async def _start_monitor_tts_response(self):
|
async def _start_monitor_tts_response(self):
|
||||||
chunk_total = b''
|
chunk_total = b""
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
msg = await self.ws.recv() # 确保 `recv()` 运行在同一个 event loop
|
msg = await self.ws.recv() # 确保 `recv()` 运行在同一个 event loop
|
||||||
res = self.parser_response(msg)
|
res = self.parser_response(msg)
|
||||||
self.print_response(res, 'send_text res:')
|
self.print_response(res, "send_text res:")
|
||||||
|
|
||||||
if res.optional.event == EVENT_TTSResponse and res.header.message_type == AUDIO_ONLY_RESPONSE:
|
if (
|
||||||
logger.bind(tag=TAG).info(f'推送数据到队列里面~~')
|
res.optional.event == EVENT_TTSResponse
|
||||||
|
and res.header.message_type == AUDIO_ONLY_RESPONSE
|
||||||
|
):
|
||||||
|
logger.bind(tag=TAG).info(f"推送数据到队列里面~~")
|
||||||
opus_datas = self.wav_to_opus_data_audio_raw(res.payload)
|
opus_datas = self.wav_to_opus_data_audio_raw(res.payload)
|
||||||
self.tts_audio_queue.put(
|
self.tts_audio_queue.put(
|
||||||
TTSMessageDTO(
|
TTSMessageDTO(
|
||||||
u_id=self.u_id, msg_type=MsgType.TTS_TEXT_RESPONSE, content=opus_datas,
|
u_id=self.u_id,
|
||||||
tts_finish_text="", sentence_type=None, duration=0
|
msg_type=MsgType.TTS_TEXT_RESPONSE,
|
||||||
|
content=opus_datas,
|
||||||
|
tts_finish_text="",
|
||||||
|
sentence_type=None,
|
||||||
|
duration=0,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
elif res.optional.event == EVENT_TTSSentenceStart:
|
elif res.optional.event == EVENT_TTSSentenceStart:
|
||||||
json_data = json.loads(res.payload.decode('utf-8'))
|
json_data = json.loads(res.payload.decode("utf-8"))
|
||||||
self.tts_text = json_data.get("text", "")
|
self.tts_text = json_data.get("text", "")
|
||||||
logger.bind(tag=TAG).info(f'句子开始~~{self.tts_text}')
|
logger.bind(tag=TAG).info(f"句子开始~~{self.tts_text}")
|
||||||
self.tts_audio_queue.put(
|
self.tts_audio_queue.put(
|
||||||
TTSMessageDTO(
|
TTSMessageDTO(
|
||||||
u_id=self.u_id, msg_type=MsgType.TTS_TEXT_RESPONSE, content=[],
|
u_id=self.u_id,
|
||||||
|
msg_type=MsgType.TTS_TEXT_RESPONSE,
|
||||||
|
content=[],
|
||||||
tts_finish_text=self.tts_text,
|
tts_finish_text=self.tts_text,
|
||||||
sentence_type=SentenceType.SENTENCE_START
|
sentence_type=SentenceType.SENTENCE_START,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
elif res.optional.event == EVENT_TTSSentenceEnd:
|
elif res.optional.event == EVENT_TTSSentenceEnd:
|
||||||
logger.bind(tag=TAG).info(f'句子结束~~{self.tts_text}')
|
logger.bind(tag=TAG).info(f"句子结束~~{self.tts_text}")
|
||||||
self.tts_audio_queue.put(
|
self.tts_audio_queue.put(
|
||||||
TTSMessageDTO(
|
TTSMessageDTO(
|
||||||
u_id=self.u_id, msg_type=MsgType.TTS_TEXT_RESPONSE, content=[],
|
u_id=self.u_id,
|
||||||
|
msg_type=MsgType.TTS_TEXT_RESPONSE,
|
||||||
|
content=[],
|
||||||
tts_finish_text=self.tts_text,
|
tts_finish_text=self.tts_text,
|
||||||
sentence_type=SentenceType.SENTENCE_END
|
sentence_type=SentenceType.SENTENCE_END,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
elif res.optional.event == EVENT_SessionFinished:
|
elif res.optional.event == EVENT_SessionFinished:
|
||||||
logger.bind(tag=TAG).info(f'会话结束~~,最后一句补零')
|
logger.bind(tag=TAG).info(f"会话结束~~,最后一句补零")
|
||||||
opus_datas = self.wav_to_opus_data_audio_raw(b'', is_end=True)
|
opus_datas = self.wav_to_opus_data_audio_raw(b"", is_end=True)
|
||||||
self.tts_audio_queue.put(
|
self.tts_audio_queue.put(
|
||||||
TTSMessageDTO(
|
TTSMessageDTO(
|
||||||
u_id=self.u_id, msg_type=MsgType.TTS_TEXT_RESPONSE, content=opus_datas,
|
u_id=self.u_id,
|
||||||
tts_finish_text="", sentence_type=None, duration=0
|
msg_type=MsgType.TTS_TEXT_RESPONSE,
|
||||||
|
content=opus_datas,
|
||||||
|
tts_finish_text="",
|
||||||
|
sentence_type=None,
|
||||||
|
duration=0,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.tts_audio_queue.put(
|
self.tts_audio_queue.put(
|
||||||
TTSMessageDTO(
|
TTSMessageDTO(
|
||||||
u_id=self.u_id, msg_type=MsgType.STOP_TTS_RESPONSE, content=[],
|
u_id=self.u_id,
|
||||||
|
msg_type=MsgType.STOP_TTS_RESPONSE,
|
||||||
|
content=[],
|
||||||
tts_finish_text=self.tts_text,
|
tts_finish_text=self.tts_text,
|
||||||
sentence_type=SentenceType.SENTENCE_END
|
sentence_type=SentenceType.SENTENCE_END,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
import json
|
||||||
|
import base64
|
||||||
|
import requests
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from core.providers.tts.base import TTSProviderBase
|
||||||
|
|
||||||
|
|
||||||
|
class TTSProvider(TTSProviderBase):
|
||||||
|
def __init__(self, config, delete_audio_file):
|
||||||
|
super().__init__(config, delete_audio_file)
|
||||||
|
self.appid = config.get("appid")
|
||||||
|
self.secret_id = config.get("secret_id")
|
||||||
|
self.secret_key = config.get("secret_key")
|
||||||
|
self.voice = config.get("voice")
|
||||||
|
self.api_url = "https://tts.tencentcloudapi.com" # 正确的API端点
|
||||||
|
self.region = config.get("region")
|
||||||
|
self.output_file = config.get("output_dir")
|
||||||
|
|
||||||
|
def _get_auth_headers(self, request_body):
|
||||||
|
"""生成鉴权请求头"""
|
||||||
|
# 获取当前UTC时间戳
|
||||||
|
timestamp = int(time.time())
|
||||||
|
|
||||||
|
# 使用UTC时间计算日期
|
||||||
|
utc_date = datetime.fromtimestamp(timestamp, tz=timezone.utc).strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
# 服务名称必须是 "tts"
|
||||||
|
service = "tts"
|
||||||
|
|
||||||
|
# 拼接凭证范围
|
||||||
|
credential_scope = f"{utc_date}/{service}/tc3_request"
|
||||||
|
|
||||||
|
# 使用TC3-HMAC-SHA256签名方法
|
||||||
|
algorithm = "TC3-HMAC-SHA256"
|
||||||
|
|
||||||
|
# 构建规范请求字符串
|
||||||
|
http_request_method = "POST"
|
||||||
|
canonical_uri = "/"
|
||||||
|
canonical_querystring = ""
|
||||||
|
|
||||||
|
# 请求头必须包含host和content-type,且按字典序排列
|
||||||
|
canonical_headers = (
|
||||||
|
f"content-type:application/json\n"
|
||||||
|
f"host:tts.tencentcloudapi.com\n"
|
||||||
|
)
|
||||||
|
signed_headers = "content-type;host"
|
||||||
|
|
||||||
|
# 请求体哈希值
|
||||||
|
payload = json.dumps(request_body)
|
||||||
|
payload_hash = hashlib.sha256(payload.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
|
# 构建规范请求字符串
|
||||||
|
canonical_request = (
|
||||||
|
f"{http_request_method}\n"
|
||||||
|
f"{canonical_uri}\n"
|
||||||
|
f"{canonical_querystring}\n"
|
||||||
|
f"{canonical_headers}\n"
|
||||||
|
f"{signed_headers}\n"
|
||||||
|
f"{payload_hash}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 计算规范请求的哈希值
|
||||||
|
hashed_canonical_request = hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
|
# 构建待签名字符串
|
||||||
|
string_to_sign = (
|
||||||
|
f"{algorithm}\n"
|
||||||
|
f"{timestamp}\n"
|
||||||
|
f"{credential_scope}\n"
|
||||||
|
f"{hashed_canonical_request}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 计算签名密钥
|
||||||
|
secret_date = self._hmac_sha256(f"TC3{self.secret_key}".encode('utf-8'), utc_date)
|
||||||
|
secret_service = self._hmac_sha256(secret_date, service)
|
||||||
|
secret_signing = self._hmac_sha256(secret_service, "tc3_request")
|
||||||
|
|
||||||
|
# 计算签名
|
||||||
|
signature = hmac.new(
|
||||||
|
secret_signing,
|
||||||
|
string_to_sign.encode('utf-8'),
|
||||||
|
hashlib.sha256
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
|
# 构建授权头
|
||||||
|
authorization = (
|
||||||
|
f"{algorithm} "
|
||||||
|
f"Credential={self.secret_id}/{credential_scope}, "
|
||||||
|
f"SignedHeaders={signed_headers}, "
|
||||||
|
f"Signature={signature}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 构建请求头
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Host": "tts.tencentcloudapi.com",
|
||||||
|
"Authorization": authorization,
|
||||||
|
"X-TC-Action": "TextToVoice",
|
||||||
|
"X-TC-Timestamp": str(timestamp),
|
||||||
|
"X-TC-Version": "2019-08-23",
|
||||||
|
"X-TC-Region": self.region,
|
||||||
|
"X-TC-Language": "zh-CN"
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers
|
||||||
|
|
||||||
|
def _hmac_sha256(self, key, msg):
|
||||||
|
"""HMAC-SHA256加密"""
|
||||||
|
if isinstance(msg, str):
|
||||||
|
msg = msg.encode('utf-8')
|
||||||
|
return hmac.new(key, msg, hashlib.sha256).digest()
|
||||||
|
|
||||||
|
def generate_filename(self, extension=".wav"):
|
||||||
|
return os.path.join(self.output_file, f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}")
|
||||||
|
|
||||||
|
async def text_to_speak(self, text, output_file):
|
||||||
|
# 构建请求体
|
||||||
|
request_json = {
|
||||||
|
"Text": text, # 合成语音的源文本
|
||||||
|
"SessionId": str(uuid.uuid4()), # 会话ID,随机生成
|
||||||
|
"VoiceType": int(self.voice), # 音色
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 获取请求头(每次请求都重新生成,以确保时间戳和签名是最新的)
|
||||||
|
headers = self._get_auth_headers(request_json)
|
||||||
|
|
||||||
|
# 发送请求
|
||||||
|
resp = requests.post(self.api_url, json.dumps(request_json), headers=headers)
|
||||||
|
|
||||||
|
# 检查响应
|
||||||
|
if resp.status_code == 200:
|
||||||
|
response_data = resp.json()
|
||||||
|
|
||||||
|
# 检查是否成功
|
||||||
|
if response_data.get("Response", {}).get("Error") is not None:
|
||||||
|
error_info = response_data["Response"]["Error"]
|
||||||
|
raise Exception(f"API返回错误: {error_info['Code']}: {error_info['Message']}")
|
||||||
|
|
||||||
|
# 提取音频数据
|
||||||
|
audio_data = response_data["Response"].get("Audio")
|
||||||
|
if audio_data:
|
||||||
|
# 解码Base64音频数据并保存
|
||||||
|
with open(output_file, "wb") as f:
|
||||||
|
f.write(base64.b64decode(audio_data))
|
||||||
|
else:
|
||||||
|
raise Exception(f"{__name__}: 没有返回音频数据: {response_data}")
|
||||||
|
else:
|
||||||
|
raise Exception(f"{__name__} status_code: {resp.status_code} response: {resp.content}")
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception(f"{__name__} error: {e}")
|
||||||
@@ -35,6 +35,15 @@ class Dialogue:
|
|||||||
self.getMessages(m, dialogue)
|
self.getMessages(m, dialogue)
|
||||||
return dialogue
|
return dialogue
|
||||||
|
|
||||||
|
def update_system_message(self, new_content: str):
|
||||||
|
"""更新或添加系统消息"""
|
||||||
|
# 查找第一个系统消息
|
||||||
|
system_msg = next((msg for msg in self.dialogue if msg.role == "system"), None)
|
||||||
|
if system_msg:
|
||||||
|
system_msg.content = new_content
|
||||||
|
else:
|
||||||
|
self.put(Message(role="system", content=new_content))
|
||||||
|
|
||||||
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]]:
|
||||||
if memory_str is None or len(memory_str) == 0:
|
if memory_str is None or len(memory_str) == 0:
|
||||||
return self.get_llm_dialogue()
|
return self.get_llm_dialogue()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
import importlib
|
import importlib
|
||||||
@@ -15,3 +16,97 @@ def create_instance(class_name, *args, **kwargs):
|
|||||||
return sys.modules[lib_name].TTSProvider(*args, **kwargs)
|
return sys.modules[lib_name].TTSProvider(*args, **kwargs)
|
||||||
|
|
||||||
raise ValueError(f"不支持的TTS类型: {class_name},请检查该配置的type是否设置正确")
|
raise ValueError(f"不支持的TTS类型: {class_name},请检查该配置的type是否设置正确")
|
||||||
|
|
||||||
|
|
||||||
|
class MarkdownCleaner:
|
||||||
|
"""
|
||||||
|
封装 Markdown 清理逻辑:直接用 MarkdownCleaner.clean_markdown(text) 即可
|
||||||
|
"""
|
||||||
|
# 公式字符
|
||||||
|
NORMAL_FORMULA_CHARS = re.compile(r'[a-zA-Z\\^_{}\+\-\(\)\[\]=]')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _replace_inline_dollar(m: re.Match) -> str:
|
||||||
|
"""
|
||||||
|
只要捕获到完整的 "$...$":
|
||||||
|
- 如果内部有典型公式字符 => 去掉两侧 $
|
||||||
|
- 否则 (纯数字/货币等) => 保留 "$...$"
|
||||||
|
"""
|
||||||
|
content = m.group(1)
|
||||||
|
if MarkdownCleaner.NORMAL_FORMULA_CHARS.search(content):
|
||||||
|
return content
|
||||||
|
else:
|
||||||
|
return m.group(0)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _replace_table_block(match: re.Match) -> str:
|
||||||
|
"""
|
||||||
|
当匹配到一个整段表格块时,回调该函数。
|
||||||
|
"""
|
||||||
|
block_text = match.group('table_block')
|
||||||
|
lines = block_text.strip('\n').split('\n')
|
||||||
|
|
||||||
|
parsed_table = []
|
||||||
|
for line in lines:
|
||||||
|
line_stripped = line.strip()
|
||||||
|
if re.match(r'^\|\s*[-:]+\s*(\|\s*[-:]+\s*)+\|?$', line_stripped):
|
||||||
|
continue
|
||||||
|
columns = [col.strip() for col in line_stripped.split('|') if col.strip() != '']
|
||||||
|
if columns:
|
||||||
|
parsed_table.append(columns)
|
||||||
|
|
||||||
|
if not parsed_table:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
headers = parsed_table[0]
|
||||||
|
data_rows = parsed_table[1:] if len(parsed_table) > 1 else []
|
||||||
|
|
||||||
|
lines_for_tts = []
|
||||||
|
if len(parsed_table) == 1:
|
||||||
|
# 只有一行
|
||||||
|
only_line_str = ", ".join(parsed_table[0])
|
||||||
|
lines_for_tts.append(f"单行表格:{only_line_str}")
|
||||||
|
else:
|
||||||
|
lines_for_tts.append(f"表头是:{', '.join(headers)}")
|
||||||
|
for i, row in enumerate(data_rows, start=1):
|
||||||
|
row_str_list = []
|
||||||
|
for col_index, cell_val in enumerate(row):
|
||||||
|
if col_index < len(headers):
|
||||||
|
row_str_list.append(f"{headers[col_index]} = {cell_val}")
|
||||||
|
else:
|
||||||
|
row_str_list.append(cell_val)
|
||||||
|
lines_for_tts.append(f"第 {i} 行:{', '.join(row_str_list)}")
|
||||||
|
|
||||||
|
return "\n".join(lines_for_tts) + "\n"
|
||||||
|
|
||||||
|
# 预编译所有正则表达式(按执行频率排序)
|
||||||
|
# 这里要把 replace_xxx 的静态方法放在最前定义,以便在列表里能正确引用它们。
|
||||||
|
REGEXES = [
|
||||||
|
(re.compile(r'```.*?```', re.DOTALL), ''), # 代码块
|
||||||
|
(re.compile(r'^#+\s*', re.MULTILINE), ''), # 标题
|
||||||
|
(re.compile(r'(\*\*|__)(.*?)\1'), r'\2'), # 粗体
|
||||||
|
(re.compile(r'(\*|_)(?=\S)(.*?)(?<=\S)\1'), r'\2'), # 斜体
|
||||||
|
(re.compile(r'!\[.*?\]\(.*?\)'), ''), # 图片
|
||||||
|
(re.compile(r'\[(.*?)\]\(.*?\)'), r'\1'), # 链接
|
||||||
|
(re.compile(r'^\s*>+\s*', re.MULTILINE), ''), # 引用
|
||||||
|
(
|
||||||
|
re.compile(r'(?P<table_block>(?:^[^\n]*\|[^\n]*\n)+)', re.MULTILINE),
|
||||||
|
_replace_table_block
|
||||||
|
),
|
||||||
|
(re.compile(r'^\s*[*+-]\s*', re.MULTILINE), '- '), # 列表
|
||||||
|
(re.compile(r'\$\$.*?\$\$', re.DOTALL), ''), # 块级公式
|
||||||
|
(
|
||||||
|
re.compile(r'(?<![A-Za-z0-9])\$([^\n$]+)\$(?![A-Za-z0-9])'),
|
||||||
|
_replace_inline_dollar
|
||||||
|
),
|
||||||
|
(re.compile(r'\n{2,}'), '\n'), # 多余空行
|
||||||
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def clean_markdown(text: str) -> str:
|
||||||
|
"""
|
||||||
|
主入口方法:依序执行所有正则,移除或替换 Markdown 元素
|
||||||
|
"""
|
||||||
|
for regex, replacement in MarkdownCleaner.REGEXES:
|
||||||
|
text = regex.sub(replacement, text)
|
||||||
|
return text.strip()
|
||||||
@@ -67,15 +67,11 @@ def is_private_ip(ip_addr):
|
|||||||
|
|
||||||
def get_ip_info(ip_addr):
|
def get_ip_info(ip_addr):
|
||||||
try:
|
try:
|
||||||
base_url = "https://freeipapi.com/api/json"
|
url = "https://whois.pconline.com.cn/ipJson.jsp?json=true"
|
||||||
url = base_url if is_private_ip(ip_addr) else f"{base_url}/{ip_addr}"
|
|
||||||
|
|
||||||
resp = requests.get(url).json()
|
resp = requests.get(url).json()
|
||||||
|
|
||||||
ip_info = {
|
ip_info = {
|
||||||
"city": resp.get("cityName"),
|
"city": resp.get("city")
|
||||||
"region": resp.get("regionName"),
|
|
||||||
"country": resp.get("countryName")
|
|
||||||
}
|
}
|
||||||
return ip_info
|
return ip_info
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -85,24 +85,25 @@ class WebSocketServer:
|
|||||||
server_config = self.config["server"]
|
server_config = self.config["server"]
|
||||||
host = server_config["ip"]
|
host = server_config["ip"]
|
||||||
port = server_config["port"]
|
port = server_config["port"]
|
||||||
selected_module = self.config.get("selected_module")
|
|
||||||
self.logger.bind(tag=TAG).info(
|
|
||||||
f"selected_module values: {', '.join(selected_module.values())}"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.logger.bind(tag=TAG).info(
|
self.logger.bind(tag=TAG).info(
|
||||||
"Server is running at ws://{}:{}", get_local_ip(), port
|
"Server is running at ws://{}:{}/xiaozhi/v1/", get_local_ip(), port
|
||||||
)
|
)
|
||||||
self.logger.bind(tag=TAG).info(
|
self.logger.bind(tag=TAG).info(
|
||||||
"=======上面的地址是websocket协议地址,请勿用浏览器访问======="
|
"=======上面的地址是websocket协议地址,请勿用浏览器访问======="
|
||||||
)
|
)
|
||||||
|
self.logger.bind(tag=TAG).info(
|
||||||
|
"如想测试websocket请用谷歌浏览器打开test目录下的test_page.html"
|
||||||
|
)
|
||||||
|
self.logger.bind(tag=TAG).info(
|
||||||
|
"=============================================================\n"
|
||||||
|
)
|
||||||
async with websockets.serve(self._handle_connection, host, port):
|
async with websockets.serve(self._handle_connection, host, port):
|
||||||
await asyncio.Future()
|
await asyncio.Future()
|
||||||
|
|
||||||
async def _handle_connection(self, websocket):
|
async def _handle_connection(self, websocket):
|
||||||
"""处理新连接,每次创建独立的ConnectionHandler"""
|
"""处理新连接,每次创建独立的ConnectionHandler"""
|
||||||
# 创建ConnectionHandler时传入当前server实例
|
# 创建ConnectionHandler时传入当前server实例
|
||||||
# tts 变成链接的时候创建,避免并非问题
|
|
||||||
handler = ConnectionHandler(
|
handler = ConnectionHandler(
|
||||||
self.config,
|
self.config,
|
||||||
self._vad,
|
self._vad,
|
||||||
|
|||||||
@@ -0,0 +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"
|
||||||
|
},
|
||||||
|
"playwright": {
|
||||||
|
"command": "npx",
|
||||||
|
"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"],
|
||||||
|
"link": "https://github.com/SimonB97/win-cli-mcp-server"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
import random
|
||||||
|
import requests
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from config.logger import setup_logging
|
||||||
|
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||||
|
|
||||||
|
TAG = __name__
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
GET_NEWS_FUNCTION_DESC = {
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "get_news",
|
||||||
|
"description": (
|
||||||
|
"获取最新新闻,随机选择一条新闻进行播报。"
|
||||||
|
"用户可以指定新闻类型,如社会新闻、科技新闻、国际新闻等。"
|
||||||
|
"如果没有指定,默认播报社会新闻。"
|
||||||
|
"用户可以要求获取详细内容,此时会获取新闻的详细内容。"
|
||||||
|
),
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"category": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "新闻类别,例如社会、科技、国际。可选参数,如果不提供则使用默认类别"
|
||||||
|
},
|
||||||
|
"detail": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "是否获取详细内容,默认为false。如果为true,则获取上一条新闻的详细内容"
|
||||||
|
},
|
||||||
|
"lang": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "返回用户使用的语言code,例如zh_CN/zh_HK/en_US/ja_JP等,默认zh_CN"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["lang"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_news_from_rss(rss_url):
|
||||||
|
"""从RSS源获取新闻列表"""
|
||||||
|
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'):
|
||||||
|
title = item.find('title').text if item.find('title') is not None else "无标题"
|
||||||
|
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}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
paragraphs = content_div.find_all('p')
|
||||||
|
content = '\n'.join([p.get_text().strip() for p in paragraphs if p.get_text().strip()])
|
||||||
|
return content
|
||||||
|
else:
|
||||||
|
# 如果找不到特定的内容区域,尝试获取所有段落
|
||||||
|
paragraphs = soup.find_all('p')
|
||||||
|
content = '\n'.join([p.get_text().strip() for p in paragraphs if p.get_text().strip()])
|
||||||
|
return content[:2000] # 限制长度
|
||||||
|
except Exception as e:
|
||||||
|
logger.bind(tag=TAG).error(f"获取新闻详情失败: {e}")
|
||||||
|
return "无法获取详细内容"
|
||||||
|
|
||||||
|
|
||||||
|
def map_category(category_text):
|
||||||
|
"""将用户输入的中文类别映射到配置文件中的类别键"""
|
||||||
|
if not category_text:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 类别映射字典,目前支持社会、国际、财经新闻,如需更多类型,参见配置文件
|
||||||
|
category_map = {
|
||||||
|
# 社会新闻
|
||||||
|
"社会": "society",
|
||||||
|
"社会新闻": "society",
|
||||||
|
# 国际新闻
|
||||||
|
"国际": "world",
|
||||||
|
"国际新闻": "world",
|
||||||
|
# 财经新闻
|
||||||
|
"财经": "finance",
|
||||||
|
"财经新闻": "finance",
|
||||||
|
"金融": "finance",
|
||||||
|
"经济": "finance"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 转换为小写并去除空格
|
||||||
|
normalized_category = category_text.lower().strip()
|
||||||
|
|
||||||
|
# 返回映射结果,如果没有匹配项则返回原始输入
|
||||||
|
return category_map.get(normalized_category, category_text)
|
||||||
|
|
||||||
|
|
||||||
|
@register_function('get_news', GET_NEWS_FUNCTION_DESC, ToolType.SYSTEM_CTL)
|
||||||
|
def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_CN"):
|
||||||
|
"""获取新闻并随机选择一条进行播报,或获取上一条新闻的详细内容"""
|
||||||
|
try:
|
||||||
|
# 如果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)
|
||||||
|
|
||||||
|
link = conn.last_news_link.get('link')
|
||||||
|
title = conn.last_news_link.get('title', '未知标题')
|
||||||
|
|
||||||
|
if link == '#':
|
||||||
|
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 == "无法获取详细内容":
|
||||||
|
return ActionResponse(Action.REQLLM,
|
||||||
|
f"抱歉,无法获取《{title}》的详细内容,可能是链接已失效或网站结构发生变化。", None)
|
||||||
|
|
||||||
|
# 构建详情报告
|
||||||
|
detail_report = (
|
||||||
|
f"根据下列数据,用{lang}回应用户的新闻详情查询请求:\n\n"
|
||||||
|
f"新闻标题: {title}\n"
|
||||||
|
f"详细内容: {detail_content}\n\n"
|
||||||
|
f"(请对上述新闻内容进行总结,提取关键信息,以自然、流畅的方式向用户播报,"
|
||||||
|
f"不要提及这是总结,就像是在讲述一个完整的新闻故事)"
|
||||||
|
)
|
||||||
|
|
||||||
|
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:
|
||||||
|
return ActionResponse(Action.REQLLM, "抱歉,未能获取到新闻信息,请稍后再试。", None)
|
||||||
|
|
||||||
|
# 随机选择一条新闻
|
||||||
|
selected_news = random.choice(news_items)
|
||||||
|
|
||||||
|
# 保存当前新闻链接到连接对象,以便后续查询详情
|
||||||
|
if not hasattr(conn, 'last_news_link'):
|
||||||
|
conn.last_news_link = {}
|
||||||
|
conn.last_news_link = {
|
||||||
|
'link': selected_news.get('link', '#'),
|
||||||
|
'title': selected_news.get('title', '未知标题')
|
||||||
|
}
|
||||||
|
|
||||||
|
# 构建新闻报告
|
||||||
|
news_report = (
|
||||||
|
f"根据下列数据,用{lang}回应用户的新闻查询请求:\n\n"
|
||||||
|
f"新闻标题: {selected_news['title']}\n"
|
||||||
|
f"发布时间: {selected_news['pubDate']}\n"
|
||||||
|
f"新闻内容: {selected_news['description']}\n"
|
||||||
|
f"(请以自然、流畅的方式向用户播报这条新闻,可以适当总结内容,"
|
||||||
|
f"直接读出新闻即可,不需要额外多余的内容。"
|
||||||
|
f"如果用户询问更多详情,告知用户可以说'请详细介绍这条新闻'获取更多内容)"
|
||||||
|
)
|
||||||
|
|
||||||
|
return ActionResponse(Action.REQLLM, news_report, None)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.bind(tag=TAG).error(f"获取新闻出错: {e}")
|
||||||
|
return ActionResponse(Action.REQLLM, "抱歉,获取新闻时发生错误,请稍后再试。", None)
|
||||||
@@ -1,20 +1,19 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import cnlunar
|
||||||
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||||
|
|
||||||
get_time_function_desc = {
|
get_time_function_desc = {
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "get_time",
|
"name": "get_time",
|
||||||
"description": "获取当前时间、日期、星期几",
|
"description": "获取今天日期或者当前时间信息",
|
||||||
'parameters': {'type': 'object', 'properties': {}, 'required': []}
|
'parameters': {'type': 'object', 'properties': {}, 'required': []}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@register_function('get_time', get_time_function_desc, ToolType.WAIT)
|
@register_function('get_time', get_time_function_desc, ToolType.WAIT)
|
||||||
def get_time():
|
def get_time():
|
||||||
"""
|
"""
|
||||||
获取当前时间、日期、星期几
|
获取当前的日期时间信息
|
||||||
"""
|
"""
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
current_time = now.strftime("%H:%M:%S")
|
current_time = now.strftime("%H:%M:%S")
|
||||||
@@ -23,3 +22,68 @@ def get_time():
|
|||||||
response_text = f"当前日期: {current_date},当前时间: {current_time},星期: {current_weekday}"
|
response_text = f"当前日期: {current_date},当前时间: {current_time},星期: {current_weekday}"
|
||||||
|
|
||||||
return ActionResponse(Action.REQLLM, response_text, None)
|
return ActionResponse(Action.REQLLM, response_text, None)
|
||||||
|
|
||||||
|
|
||||||
|
get_lunar_function_desc = {
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "get_lunar",
|
||||||
|
"description": (
|
||||||
|
"用于获取今天的阴历/农历和黄历信息。"
|
||||||
|
"用户可以指定查询内容,如:阴历日期、天干地支、节气、生肖、星座、八字、宜忌等。"
|
||||||
|
"如果没有指定查询内容,则默认查询干支年和农历日期。"
|
||||||
|
),
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"query": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "要查询的内容,例如阴历日期、天干地支、节日、节气、生肖、星座、八字、宜忌等"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@register_function('get_lunar', get_lunar_function_desc, ToolType.WAIT)
|
||||||
|
def get_lunar(query=None):
|
||||||
|
"""
|
||||||
|
用于获取当前的阴历/农历,和天干地支、节气、生肖、星座、八字、宜忌等黄历信息
|
||||||
|
"""
|
||||||
|
now = datetime.now()
|
||||||
|
current_time = now.strftime("%H:%M:%S")
|
||||||
|
current_date = now.strftime("%Y-%m-%d")
|
||||||
|
current_weekday = now.strftime("%A")
|
||||||
|
|
||||||
|
# 如果 query 为 None,则使用默认文本
|
||||||
|
if query is None:
|
||||||
|
query = "默认查询干支年和农历日期"
|
||||||
|
response_text = f"根据以下信息回应用户的查询请求,并提供与{query}相关的信息:\n"
|
||||||
|
|
||||||
|
lunar = cnlunar.Lunar(now, godType='8char')
|
||||||
|
response_text += (
|
||||||
|
f"当前公历日期: {current_date},当前时间: {current_time},星期: {current_weekday}\n"
|
||||||
|
"农历信息:\n"
|
||||||
|
"%s年%s%s\n" % (lunar.lunarYearCn, lunar.lunarMonthCn[:-1], lunar.lunarDayCn) +
|
||||||
|
"干支: %s年 %s月 %s日\n" % (lunar.year8Char, lunar.month8Char, lunar.day8Char) +
|
||||||
|
"生肖: 属%s\n" % (lunar.chineseYearZodiac) +
|
||||||
|
"八字: %s\n" % (' '.join([lunar.year8Char, lunar.month8Char, lunar.day8Char, lunar.twohour8Char])) +
|
||||||
|
"今日节日: %s\n" % (",".join(filter(None, (lunar.get_legalHolidays(), lunar.get_otherHolidays(), lunar.get_otherLunarHolidays())))) +
|
||||||
|
"今日节气: %s\n" % (lunar.todaySolarTerms) +
|
||||||
|
"下一节气: %s %s年%s月%s日\n" % (lunar.nextSolarTerm, lunar.nextSolarTermYear, lunar.nextSolarTermDate[0], lunar.nextSolarTermDate[1]) +
|
||||||
|
"今年节气表: %s\n" % (', '.join([f"{term}({date[0]}月{date[1]}日)" for term, date in lunar.thisYearSolarTermsDic.items()])) +
|
||||||
|
"生肖冲煞: %s\n" % (lunar.chineseZodiacClash) +
|
||||||
|
"星座: %s\n" % (lunar.starZodiac) +
|
||||||
|
"纳音: %s\n" % lunar.get_nayin() +
|
||||||
|
"彭祖百忌: %s\n" % (lunar.get_pengTaboo(delimit=", ")) +
|
||||||
|
"值日: %s执位\n" % lunar.get_today12DayOfficer()[0] +
|
||||||
|
"值神: %s(%s)\n" % (lunar.get_today12DayOfficer()[1], lunar.get_today12DayOfficer()[2]) +
|
||||||
|
"廿八宿: %s\n" % lunar.get_the28Stars() +
|
||||||
|
"吉神方位: %s\n" % ' '.join(lunar.get_luckyGodsDirection()) +
|
||||||
|
"今日胎神: %s\n" % lunar.get_fetalGod() +
|
||||||
|
"宜: %s\n" % '、'.join(lunar.goodThing[:10]) +
|
||||||
|
"忌: %s\n" % '、'.join(lunar.badThing[:10]) +
|
||||||
|
"(默认返回干支年和农历日期;仅在要求查询宜忌信息时才返回本日宜忌)"
|
||||||
|
)
|
||||||
|
|
||||||
|
return ActionResponse(Action.REQLLM, response_text, None)
|
||||||
@@ -39,6 +39,23 @@ 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):
|
def fetch_city_info(location, api_key):
|
||||||
url = f"https://geoapi.qweather.com/v2/city/lookup?key={api_key}&location={location}&lang=zh"
|
url = f"https://geoapi.qweather.com/v2/city/lookup?key={api_key}&location={location}&lang=zh"
|
||||||
@@ -67,9 +84,11 @@ def parse_weather_info(soup):
|
|||||||
temps_list = []
|
temps_list = []
|
||||||
for row in soup.select(".city-forecast-tabs__row")[:7]: # 取前7天的数据
|
for row in soup.select(".city-forecast-tabs__row")[:7]: # 取前7天的数据
|
||||||
date = row.select_one(".date-bg .date").get_text(strip=True)
|
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")]
|
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)
|
high_temp, low_temp = (temps[0], temps[-1]) if len(temps) >= 2 else (None, None)
|
||||||
temps_list.append((date, high_temp, low_temp))
|
temps_list.append((date, weather, high_temp, low_temp))
|
||||||
|
|
||||||
return city_name, current_abstract, current_basic, temps_list
|
return city_name, current_abstract, current_basic, temps_list
|
||||||
|
|
||||||
@@ -91,13 +110,13 @@ def get_weather(conn, location: str = None, lang: str = "zh_CN"):
|
|||||||
|
|
||||||
city_name, current_abstract, current_basic, temps_list = parse_weather_info(soup)
|
city_name, current_abstract, current_basic, temps_list = parse_weather_info(soup)
|
||||||
weather_report = f"根据下列数据,用{lang}回应用户的查询天气请求:\n{city_name}未来7天天气:\n"
|
weather_report = f"根据下列数据,用{lang}回应用户的查询天气请求:\n{city_name}未来7天天气:\n"
|
||||||
for i, (date, high, low) in enumerate(temps_list):
|
for i, (date, weather, high, low) in enumerate(temps_list):
|
||||||
if high and low:
|
if high and low:
|
||||||
weather_report += f"{date}: {low}到{high}\n"
|
weather_report += f"{date}: {low}到{high}, {weather}\n"
|
||||||
weather_report += (
|
weather_report += (
|
||||||
f"当前天气: {current_abstract}\n"
|
f"当前天气: {current_abstract}\n"
|
||||||
f"当前天气参数: {current_basic}\n"
|
f"当前天气参数: {current_basic}\n"
|
||||||
f"(确保只报告指定单日的气温范围,除非用户明确要求想要了解多日天气,如果未指定,默认报告今天的温度范围。"
|
f"(确保只报告指定单日的天气情况,除非未来会出现异常天气;或者用户明确要求想要了解多日天气,如果未指定,默认报告今天的天气。"
|
||||||
"参数为0的值不需要报告给用户,每次都报告体感温度,根据语境选择合适的参数内容告知用户,并对参数给出相应评价)"
|
"参数为0的值不需要报告给用户,每次都报告体感温度,根据语境选择合适的参数内容告知用户,并对参数给出相应评价)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
from config.logger import setup_logging
|
||||||
|
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||||
|
from core.handle.iotHandle import get_iot_status, send_iot_conn
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
TAG = __name__
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
|
||||||
|
async def _get_device_status(conn, device_name, device_type, property_name):
|
||||||
|
"""获取设备状态"""
|
||||||
|
status = await get_iot_status(conn, device_type, property_name)
|
||||||
|
if status is None:
|
||||||
|
raise Exception(f"你的设备不支持{device_name}控制")
|
||||||
|
return status
|
||||||
|
|
||||||
|
|
||||||
|
async def _set_device_property(conn, device_name, device_type, method_name, property_name, new_value=None, action=None, step=10):
|
||||||
|
"""设置设备属性"""
|
||||||
|
current_value = await _get_device_status(conn, device_name, device_type, property_name)
|
||||||
|
|
||||||
|
if action == 'raise':
|
||||||
|
current_value += step
|
||||||
|
elif action == 'lower':
|
||||||
|
current_value -= step
|
||||||
|
elif action == 'set':
|
||||||
|
if new_value is None:
|
||||||
|
raise Exception(f"缺少{property_name}参数")
|
||||||
|
current_value = new_value
|
||||||
|
|
||||||
|
# 限制属性范围在0到100之间
|
||||||
|
current_value = max(0, min(100, current_value))
|
||||||
|
|
||||||
|
await send_iot_conn(conn, device_type, method_name, {property_name: current_value})
|
||||||
|
return current_value
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_device_action(conn, func, success_message, error_message, *args, **kwargs):
|
||||||
|
"""处理设备操作的通用函数"""
|
||||||
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
|
func(conn, *args, **kwargs), conn.loop)
|
||||||
|
try:
|
||||||
|
result = future.result()
|
||||||
|
logger.bind(tag=TAG).info(f"{success_message}: {result}")
|
||||||
|
response = f"{success_message}{result}"
|
||||||
|
return ActionResponse(action=Action.RESPONSE, result=result, response=response)
|
||||||
|
except Exception as e:
|
||||||
|
logger.bind(tag=TAG).error(f"{error_message}: {e}")
|
||||||
|
response = f"{error_message}: {e}"
|
||||||
|
return ActionResponse(action=Action.RESPONSE, result=None, response=response)
|
||||||
|
|
||||||
|
|
||||||
|
# 设备控制
|
||||||
|
handle_device_function_desc = {
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "handle_device",
|
||||||
|
"description": (
|
||||||
|
"用户想要获取或者设置设备的音量/亮度大小,或者用户觉得声音/亮度过高或过低,或者用户想提高或降低音量/亮度。"
|
||||||
|
"比如用户说现在亮度多少,参数为:device_type:Screen,action:get。"
|
||||||
|
"比如用户说设置音量为50,参数为:device_type:Speaker,action:set,value:50。"
|
||||||
|
"比如用户说亮度太高了,参数为:device_type:Screen,action:lower。"
|
||||||
|
"比如用户说调大音量,参数为:device_type:Speaker,action:raise。"
|
||||||
|
),
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"device_type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "设备类型,可选值:Speaker(音量),Screen(亮度)"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "动作名称,可选值:get(获取),set(设置),raise(提高),lower(降低)"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "值大小,可选值:0-100之间的整数"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["device_type", "action"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register_function('handle_device', handle_device_function_desc, ToolType.IOT_CTL)
|
||||||
|
def handle_device(conn, device_type: str, action: str, value: int = None):
|
||||||
|
if device_type == "Speaker":
|
||||||
|
method_name, property_name, device_name = "SetVolume", "volume", "音量"
|
||||||
|
elif device_type == "Screen":
|
||||||
|
method_name, property_name, device_name = "SetBrightness", "brightness", "亮度"
|
||||||
|
else:
|
||||||
|
raise Exception(f"未识别的设备类型: {device_type}")
|
||||||
|
|
||||||
|
if action not in ["get", "set", "raise", "lower"]:
|
||||||
|
raise Exception(f"未识别的动作名称: {action}")
|
||||||
|
|
||||||
|
if action == "get":
|
||||||
|
# get
|
||||||
|
return _handle_device_action(
|
||||||
|
conn, _get_device_status, f"当前{device_name}", f"获取{device_name}失败",
|
||||||
|
device_name=device_name, device_type=device_type, property_name=property_name,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# set, raise, lower
|
||||||
|
return _handle_device_action(
|
||||||
|
conn, _set_device_property, f"{device_name}已调整到", f"{device_name}调整失败",
|
||||||
|
device_name=device_name, device_type=device_type, method_name=method_name,
|
||||||
|
property_name=property_name, new_value=value, action=action
|
||||||
|
)
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||||
|
from plugins_func.functions.hass_init import initialize_hass_handler
|
||||||
|
from config.logger import setup_logging
|
||||||
|
import asyncio
|
||||||
|
import requests
|
||||||
|
|
||||||
|
TAG = __name__
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
hass_get_state_function_desc = {
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "hass_get_state",
|
||||||
|
"description": "获取homeassistant里设备的状态,包括灯光亮度,媒体播放器的音量,设备的暂停、继续操作",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"entity_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "需要操作的设备id,homeassistant里的entity_id"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["entity_id"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register_function("hass_get_state", hass_get_state_function_desc, ToolType.SYSTEM_CTL)
|
||||||
|
def hass_get_state(conn, entity_id=''):
|
||||||
|
try:
|
||||||
|
|
||||||
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
|
handle_hass_get_state(conn, entity_id),
|
||||||
|
conn.loop
|
||||||
|
)
|
||||||
|
ha_response = future.result()
|
||||||
|
return ActionResponse(action=Action.REQLLM, result="执行成功", response=ha_response)
|
||||||
|
except Exception as e:
|
||||||
|
logger.bind(tag=TAG).error(f"处理设置属性意图错误: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_hass_get_state(conn, entity_id):
|
||||||
|
HASS_CACHE = initialize_hass_handler(conn)
|
||||||
|
api_key = HASS_CACHE['api_key']
|
||||||
|
base_url = HASS_CACHE['base_url']
|
||||||
|
url = f"{base_url}/api/states/{entity_id}"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {api_key}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()['state']
|
||||||
|
else:
|
||||||
|
return f"切换失败,错误码: {response.status_code}"
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||||
|
from plugins_func.functions.hass_init import initialize_hass_handler
|
||||||
|
from config.logger import setup_logging
|
||||||
|
import asyncio
|
||||||
|
import requests
|
||||||
|
|
||||||
|
TAG = __name__
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
hass_play_music_function_desc = {
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "hass_play_music",
|
||||||
|
"description": "用户想听音乐、有声书的时候使用,在房间的媒体播放器(media_player)里播放对应音频",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"media_content_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "可以是音乐或有声书的专辑名称、歌曲名、演唱者,如果未指定就填random"
|
||||||
|
},
|
||||||
|
"entity_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "需要操作的音箱的设备id,homeassistant里的entity_id,media_player开头"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["media_content_id", "entity_id"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register_function('hass_play_music', hass_play_music_function_desc, ToolType.SYSTEM_CTL)
|
||||||
|
def hass_play_music(conn, entity_id='', media_content_id='random'):
|
||||||
|
try:
|
||||||
|
# 执行音乐播放命令
|
||||||
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
|
handle_hass_play_music(conn, entity_id, media_content_id),
|
||||||
|
conn.loop
|
||||||
|
)
|
||||||
|
ha_response = future.result()
|
||||||
|
return ActionResponse(action=Action.RESPONSE, result="退出意图已处理", response=ha_response)
|
||||||
|
except Exception as e:
|
||||||
|
logger.bind(tag=TAG).error(f"处理音乐意图错误: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_hass_play_music(conn, entity_id, media_content_id):
|
||||||
|
HASS_CACHE = initialize_hass_handler(conn)
|
||||||
|
api_key = HASS_CACHE['api_key']
|
||||||
|
base_url = HASS_CACHE['base_url']
|
||||||
|
url = f"{base_url}/api/services/music_assistant/play_media"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {api_key}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
data = {
|
||||||
|
"entity_id": entity_id,
|
||||||
|
"media_id": media_content_id
|
||||||
|
}
|
||||||
|
response = requests.post(url, headers=headers, json=data)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return f"正在播放{media_content_id}的音乐"
|
||||||
|
else:
|
||||||
|
return f"音乐播放失败,错误码: {response.status_code}"
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||||
|
from plugins_func.functions.hass_init import initialize_hass_handler
|
||||||
|
from config.logger import setup_logging
|
||||||
|
import asyncio
|
||||||
|
import requests
|
||||||
|
|
||||||
|
TAG = __name__
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
hass_set_state_function_desc = {
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "hass_set_state",
|
||||||
|
"description": "设置homeassistant里设备的状态,包括开、关,调整灯光亮度,调整播放器的音量,设备的暂停、继续、静音操作",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"state": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "需要操作的动作,打开设备:turn_on,关闭设备:turn_off,增加亮度:brightness_up,降低亮度:brightness_down,设置亮度:brightness_value,增加>音量:,volume_up降低音量:volume_down,设置音量:volume_set,设备暂停:pause,设备继续:continue,静音/取消静音:volume_mute"
|
||||||
|
},
|
||||||
|
"input": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "只有在设置音量,设置亮度时候才需要,有效值为1-100,对应音量和亮度的1%-100%"
|
||||||
|
},
|
||||||
|
"is_muted": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "只有在设置静音操作时才需要,设置静音的时候该值为true,取消静音时该值为false"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["type"]
|
||||||
|
},
|
||||||
|
"entity_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "需要操作的设备id,homeassistant里的entity_id"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["state", "entity_id"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register_function('hass_set_state', hass_set_state_function_desc, ToolType.SYSTEM_CTL)
|
||||||
|
def hass_set_state(conn, entity_id='', state={}):
|
||||||
|
try:
|
||||||
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
|
handle_hass_set_state(conn, entity_id, state),
|
||||||
|
conn.loop
|
||||||
|
)
|
||||||
|
ha_response = future.result()
|
||||||
|
return ActionResponse(action=Action.REQLLM, result="执行成功", response=ha_response)
|
||||||
|
except Exception as e:
|
||||||
|
logger.bind(tag=TAG).error(f"处理设置属性意图错误: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_hass_set_state(conn, entity_id, state):
|
||||||
|
HASS_CACHE = initialize_hass_handler(conn)
|
||||||
|
api_key = HASS_CACHE['api_key']
|
||||||
|
base_url = HASS_CACHE['base_url']
|
||||||
|
'''
|
||||||
|
state = { "type":"brightness_up","input":"80","is_muted":"true"}
|
||||||
|
'''
|
||||||
|
domains = entity_id.split(".")
|
||||||
|
if len(domains) > 1:
|
||||||
|
domain = domains[0]
|
||||||
|
else:
|
||||||
|
return "执行失败,错误的设备id"
|
||||||
|
action = ''
|
||||||
|
arg = ''
|
||||||
|
value = ''
|
||||||
|
if state['type'] == 'turn_on':
|
||||||
|
description = "设备已打开"
|
||||||
|
if domain == "cover":
|
||||||
|
action = "open_cover"
|
||||||
|
elif domain == "vacuum":
|
||||||
|
action = "start"
|
||||||
|
else:
|
||||||
|
action = "turn_on"
|
||||||
|
elif state['type'] == 'turn_off':
|
||||||
|
description = "设备已关闭"
|
||||||
|
if domain == 'cover':
|
||||||
|
action = "close_cover"
|
||||||
|
elif domain == 'vacuum':
|
||||||
|
action = "stop"
|
||||||
|
else:
|
||||||
|
action = "turn_off"
|
||||||
|
elif state['type'] == 'brightness_up':
|
||||||
|
description = "灯光已调亮"
|
||||||
|
action = 'turn_on'
|
||||||
|
arg = 'brightness_step_pct'
|
||||||
|
value = 10
|
||||||
|
elif state['type'] == 'brightness_down':
|
||||||
|
description = "灯光已调暗"
|
||||||
|
action = 'turn_on'
|
||||||
|
arg = 'brightness_step_pct'
|
||||||
|
value = -10
|
||||||
|
elif state['type'] == 'brightness_value':
|
||||||
|
description = f"亮度已调整到{state['input']}"
|
||||||
|
action = 'turn_on'
|
||||||
|
arg = 'brightness_pct'
|
||||||
|
value = state['input']
|
||||||
|
elif state['type'] == 'volume_up':
|
||||||
|
description = "音量已调大"
|
||||||
|
action = state['type']
|
||||||
|
elif state['type'] == 'volume_down':
|
||||||
|
description = "音量已调小"
|
||||||
|
action = state['type']
|
||||||
|
elif state['type'] == 'volume_set':
|
||||||
|
description = f"音量已调整到{state['input']}"
|
||||||
|
action = state['type']
|
||||||
|
arg = 'volume_level'
|
||||||
|
value = state['input']
|
||||||
|
elif state['type'] == 'volume_mute':
|
||||||
|
description = f"设备已静音"
|
||||||
|
action = state['type']
|
||||||
|
arg = 'is_volume_muted'
|
||||||
|
value = state['is_muted']
|
||||||
|
elif state['type'] == 'pause':
|
||||||
|
description = f"设备已暂停"
|
||||||
|
action = state['type']
|
||||||
|
if domain == 'media_player':
|
||||||
|
action = 'media_pause'
|
||||||
|
if domain == 'cover':
|
||||||
|
action = 'stop_cover'
|
||||||
|
if domain == 'vacuum':
|
||||||
|
action = 'pause'
|
||||||
|
elif state['type'] == 'continue':
|
||||||
|
description = f"设备已继续"
|
||||||
|
if domain == 'media_player':
|
||||||
|
action = 'media_play'
|
||||||
|
if domain == 'vacuum':
|
||||||
|
action = 'start'
|
||||||
|
else:
|
||||||
|
return f"{domain} {state.type}功能尚未支持"
|
||||||
|
|
||||||
|
if arg == '':
|
||||||
|
data = {
|
||||||
|
"entity_id": entity_id,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
data = {
|
||||||
|
"entity_id": entity_id,
|
||||||
|
arg: value
|
||||||
|
}
|
||||||
|
url = f"{base_url}/api/services/{domain}/{action}"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {api_key}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
response = requests.post(url, headers=headers, json=data)
|
||||||
|
logger.bind(tag=TAG).info(f"设置状态:url:{url},return_code:{response.status_code}")
|
||||||
|
if response.status_code == 200:
|
||||||
|
return description
|
||||||
|
else:
|
||||||
|
return f"设置失败,错误码: {response.status_code}"
|
||||||
@@ -217,13 +217,14 @@ async def play_local_music(conn, specific_file=None):
|
|||||||
)
|
)
|
||||||
conn.tts.tts_audio_queue.put(
|
conn.tts.tts_audio_queue.put(
|
||||||
TTSMessageDTO(
|
TTSMessageDTO(
|
||||||
u_id=conn.u_id, msg_type=MsgType.STOP_TTS_RESPONSE, content=[],
|
u_id=conn.u_id,
|
||||||
tts_finish_text='',
|
msg_type=MsgType.STOP_TTS_RESPONSE,
|
||||||
sentence_type=None
|
content=[],
|
||||||
|
tts_finish_text="",
|
||||||
|
sentence_type=None,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"播放音乐失败: {str(e)}")
|
logger.bind(tag=TAG).error(f"播放音乐失败: {str(e)}")
|
||||||
logger.bind(tag=TAG).error(f"详细错误: {traceback.format_exc()}")
|
logger.bind(tag=TAG).error(f"详细错误: {traceback.format_exc()}")
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
from config.logger import setup_logging
|
|
||||||
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
|
||||||
from core.handle.iotHandle import get_iot_status, send_iot_conn
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
TAG = __name__
|
|
||||||
logger = setup_logging()
|
|
||||||
|
|
||||||
raise_and_lower_the_volume_function_desc = {
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "raise_and_lower_the_volume",
|
|
||||||
"description": "用户觉得声音过高或过低,或者用户想提高或降低音量。比如用户说太大声了,参数为:lower,比如用户说提高音量,参数为:raise",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"action": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "动作名称,要么是raise,要么是lower"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["action"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@register_function('raise_and_lower_the_volume', raise_and_lower_the_volume_function_desc, ToolType.IOT_CTL)
|
|
||||||
def raise_and_lower_the_volume(conn, action: str):
|
|
||||||
"""
|
|
||||||
获取当前设备音量
|
|
||||||
"""
|
|
||||||
|
|
||||||
future = asyncio.run_coroutine_threadsafe(
|
|
||||||
_raise_and_lower_the_volume(conn, action),
|
|
||||||
conn.loop
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
new_volume = future.result() # 同步等待异步操作完成
|
|
||||||
logger.bind(tag=TAG).info(f"音量操作完成: {new_volume}")
|
|
||||||
response = f"音量已调整到{new_volume}"
|
|
||||||
except Exception as e:
|
|
||||||
logger.bind(tag=TAG).error(f"音量操作失败: {e}")
|
|
||||||
response = f"音量调整失败: {e}"
|
|
||||||
|
|
||||||
return ActionResponse(action=Action.RESPONSE, result="指令已接收", response=response)
|
|
||||||
|
|
||||||
|
|
||||||
async def _raise_and_lower_the_volume(conn, action):
|
|
||||||
volume = await get_iot_status(conn, "Speaker", "volume")
|
|
||||||
if volume is None:
|
|
||||||
raise Exception("你的设备不支持音量控制")
|
|
||||||
if action == 'raise':
|
|
||||||
volume += 10
|
|
||||||
elif action == 'lower':
|
|
||||||
volume -= 10
|
|
||||||
# 限制音量范围在0到100之间
|
|
||||||
if volume < 0:
|
|
||||||
volume = 0
|
|
||||||
elif volume > 100:
|
|
||||||
volume = 100
|
|
||||||
await send_iot_conn(conn, "Speaker", "SetVolume", {"volume": volume})
|
|
||||||
return volume
|
|
||||||
@@ -23,5 +23,3 @@ def auto_import_modules(package_name):
|
|||||||
full_module_name = f"{package_name}.{module_name}"
|
full_module_name = f"{package_name}.{module_name}"
|
||||||
importlib.import_module(full_module_name)
|
importlib.import_module(full_module_name)
|
||||||
#logger.bind(tag=TAG).info(f"模块 '{full_module_name}' 已加载")
|
#logger.bind(tag=TAG).info(f"模块 '{full_module_name}' 已加载")
|
||||||
|
|
||||||
auto_import_modules('plugins_func.functions')
|
|
||||||
@@ -12,6 +12,7 @@ class ToolType(Enum):
|
|||||||
CHANGE_SYS_PROMPT = (3, "修改系统提示词,切换角色性格或职责")
|
CHANGE_SYS_PROMPT = (3, "修改系统提示词,切换角色性格或职责")
|
||||||
SYSTEM_CTL = (4, "系统控制,影响正常的对话流程,如退出、播放音乐等,需要传递conn参数")
|
SYSTEM_CTL = (4, "系统控制,影响正常的对话流程,如退出、播放音乐等,需要传递conn参数")
|
||||||
IOT_CTL = (5, "IOT设备控制,需要传递conn参数")
|
IOT_CTL = (5, "IOT设备控制,需要传递conn参数")
|
||||||
|
MCP_CLIENT = (6, "MCP客户端")
|
||||||
|
|
||||||
def __init__(self, code, message):
|
def __init__(self, code, message):
|
||||||
self.code = code
|
self.code = code
|
||||||
|
|||||||
@@ -22,4 +22,5 @@ mem0ai==0.1.62
|
|||||||
bs4==0.0.2
|
bs4==0.0.2
|
||||||
modelscope==1.23.2
|
modelscope==1.23.2
|
||||||
sherpa_onnx==1.11.0
|
sherpa_onnx==1.11.0
|
||||||
mutagen==1.47.0
|
mcp==1.4.1
|
||||||
|
cnlunar==0.2.0
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user