mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-29 05:33:57 +08:00
update:合并最新代码
This commit is contained in:
@@ -1,22 +1,23 @@
|
||||
import asyncio
|
||||
from enum import Enum
|
||||
|
||||
from config.logger import setup_logging
|
||||
import json
|
||||
from plugins_func.register import FunctionRegistry, ActionResponse, Action, ToolType
|
||||
from plugins_func.functions.hass_init import append_devices_to_prompt
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
class FunctionHandler:
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
def __init__(self, conn):
|
||||
self.conn = conn
|
||||
self.config = conn.config
|
||||
self.function_registry = FunctionRegistry()
|
||||
self.register_nessary_functions()
|
||||
self.register_config_functions()
|
||||
self.functions_desc = self.function_registry.get_all_function_desc()
|
||||
func_names = self.current_support_functions()
|
||||
self.modify_plugin_loader_des(func_names)
|
||||
self.finish_init = True
|
||||
|
||||
def modify_plugin_loader_des(self, func_names):
|
||||
if "plugin_loader" not in func_names:
|
||||
@@ -26,9 +27,11 @@ class FunctionHandler:
|
||||
func_names = ",".join(surport_plugins)
|
||||
for function_desc in self.functions_desc:
|
||||
if function_desc["function"]["name"] == "plugin_loader":
|
||||
function_desc["function"]["description"] = function_desc["function"]["description"].replace("[plugins]", func_names)
|
||||
function_desc["function"]["description"] = function_desc["function"][
|
||||
"description"
|
||||
].replace("[plugins]", func_names)
|
||||
break
|
||||
|
||||
|
||||
def upload_functions_desc(self):
|
||||
self.functions_desc = self.function_registry.get_all_function_desc()
|
||||
|
||||
@@ -47,16 +50,19 @@ class FunctionHandler:
|
||||
def register_nessary_functions(self):
|
||||
"""注册必要的函数"""
|
||||
self.function_registry.register_function("handle_exit_intent")
|
||||
self.function_registry.register_function("play_music")
|
||||
self.function_registry.register_function("plugin_loader")
|
||||
self.function_registry.register_function("get_time")
|
||||
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):
|
||||
"""注册配置中的函数,可以不同客户端使用不同的配置"""
|
||||
for func in self.config["Intent"]["function_call"].get("functions", []):
|
||||
self.function_registry.register_function(func)
|
||||
|
||||
"""home assistant需要初始化提示词"""
|
||||
append_devices_to_prompt(self.conn)
|
||||
|
||||
def get_function(self, name):
|
||||
return self.function_registry.get_function(name)
|
||||
|
||||
@@ -65,20 +71,27 @@ class FunctionHandler:
|
||||
function_name = function_call_data["name"]
|
||||
funcItem = self.get_function(function_name)
|
||||
if not funcItem:
|
||||
return ActionResponse(action=Action.NOTFOUND, result="没有找到对应的函数", response="")
|
||||
return ActionResponse(
|
||||
action=Action.NOTFOUND, result="没有找到对应的函数", response=""
|
||||
)
|
||||
func = funcItem.func
|
||||
arguments = function_call_data["arguments"]
|
||||
arguments = json.loads(arguments) if arguments else {}
|
||||
logger.bind(tag=TAG).info(f"调用函数: {function_name}, 参数: {arguments}")
|
||||
if funcItem.type == ToolType.SYSTEM_CTL or funcItem.type == ToolType.IOT_CTL:
|
||||
if (
|
||||
funcItem.type == ToolType.SYSTEM_CTL
|
||||
or funcItem.type == ToolType.IOT_CTL
|
||||
):
|
||||
return func(conn, **arguments)
|
||||
elif funcItem.type == ToolType.WAIT:
|
||||
return func(**arguments)
|
||||
elif funcItem.type == ToolType.CHANGE_SYS_PROMPT:
|
||||
return func(conn, **arguments)
|
||||
else:
|
||||
return ActionResponse(action=Action.NOTFOUND, result="没有找到对应的函数", response="")
|
||||
return ActionResponse(
|
||||
action=Action.NOTFOUND, result="没有找到对应的函数", response=""
|
||||
)
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"处理function call错误: {e}")
|
||||
|
||||
return None
|
||||
return None
|
||||
|
||||
@@ -1,8 +1,102 @@
|
||||
import json
|
||||
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()
|
||||
|
||||
WAKEUP_CONFIG = {
|
||||
"dir": "config/assets/",
|
||||
"file_name": "wakeup_words",
|
||||
"create_time": time.time(),
|
||||
"refresh_time": 10,
|
||||
"words": ["你好小智", "你好啊小智", "小智你好", "小智"],
|
||||
"text": "",
|
||||
}
|
||||
|
||||
|
||||
async def handleHelloMessage(conn):
|
||||
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
|
||||
from core.handle.sendAudioHandle import send_stt_message
|
||||
from core.utils.util import remove_punctuation_and_length
|
||||
from core.utils.dialogue import Message
|
||||
from loguru import logger
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
@@ -12,15 +14,20 @@ async def handle_user_intent(conn, text):
|
||||
# 检查是否有明确的退出命令
|
||||
if await check_direct_exit(conn, text):
|
||||
return True
|
||||
# 4月4日因流式改造暂时关闭唤醒词加速功能
|
||||
# # 检查是否是唤醒词
|
||||
# if await checkWakeupWords(conn, text):
|
||||
# return True
|
||||
|
||||
if conn.use_function_call_mode:
|
||||
# 使用支持function calling的聊天方法,不再进行意图分析
|
||||
return False
|
||||
# 使用LLM进行意图分析
|
||||
intent = await analyze_intent_with_llm(conn, text)
|
||||
if not intent:
|
||||
intent_result = await analyze_intent_with_llm(conn, text)
|
||||
if not intent_result:
|
||||
return False
|
||||
# 处理各种意图
|
||||
return await process_intent_result(conn, intent, text)
|
||||
return await process_intent_result(conn, intent_result, text)
|
||||
|
||||
|
||||
async def check_direct_exit(conn, text):
|
||||
@@ -30,6 +37,7 @@ async def check_direct_exit(conn, text):
|
||||
for cmd in cmd_exit:
|
||||
if text == cmd:
|
||||
logger.bind(tag=TAG).info(f"识别到明确的退出命令: {text}")
|
||||
await send_stt_message(conn, text)
|
||||
await conn.close()
|
||||
return True
|
||||
return False
|
||||
@@ -37,7 +45,7 @@ async def check_direct_exit(conn, text):
|
||||
|
||||
async def analyze_intent_with_llm(conn, text):
|
||||
"""使用LLM分析用户意图"""
|
||||
if not hasattr(conn, 'intent') or not conn.intent:
|
||||
if not hasattr(conn, "intent") or not conn.intent:
|
||||
logger.bind(tag=TAG).warning("意图识别服务未初始化")
|
||||
return None
|
||||
|
||||
@@ -45,52 +53,64 @@ async def analyze_intent_with_llm(conn, text):
|
||||
dialogue = conn.dialogue
|
||||
try:
|
||||
intent_result = await conn.intent.detect_intent(conn, dialogue.dialogue, text)
|
||||
# 尝试解析JSON结果
|
||||
try:
|
||||
intent_data = json.loads(intent_result)
|
||||
if "intent" in intent_data:
|
||||
return intent_data["intent"]
|
||||
except json.JSONDecodeError:
|
||||
# 如果不是JSON格式,尝试直接获取意图文本
|
||||
return intent_result.strip()
|
||||
|
||||
return intent_result
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"意图识别失败: {str(e)}")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def process_intent_result(conn, intent, original_text):
|
||||
async def process_intent_result(conn, intent_result, original_text):
|
||||
"""处理意图识别结果"""
|
||||
# 处理退出意图
|
||||
if "结束聊天" in intent:
|
||||
logger.bind(tag=TAG).info(f"识别到退出意图: {intent}")
|
||||
# 如果是明确的离别意图,发送告别语并关闭连接
|
||||
await send_stt_message(conn, original_text)
|
||||
conn.executor.submit(conn.chat_and_close, original_text)
|
||||
return True
|
||||
try:
|
||||
# 尝试将结果解析为JSON
|
||||
intent_data = json.loads(intent_result)
|
||||
|
||||
# 处理播放音乐意图
|
||||
if "播放音乐" in intent:
|
||||
logger.bind(tag=TAG).info(f"识别到音乐播放意图: {intent}")
|
||||
# 调用play_music函数来播放音乐
|
||||
song_name = extract_text_in_brackets(intent)
|
||||
function_id = str(uuid.uuid4().hex)
|
||||
function_name = "play_music"
|
||||
function_arguments = '{ "song_name": "' + song_name + '" }'
|
||||
# 检查是否有function_call
|
||||
if "function_call" in intent_data:
|
||||
# 直接从意图识别获取了function_call
|
||||
logger.bind(tag=TAG).info(
|
||||
f"检测到function_call格式的意图结果: {intent_data['function_call']['name']}"
|
||||
)
|
||||
function_name = intent_data["function_call"]["name"]
|
||||
if function_name == "continue_chat":
|
||||
return False
|
||||
function_args = None
|
||||
if "arguments" in intent_data["function_call"]:
|
||||
function_args = intent_data["function_call"]["arguments"]
|
||||
# 确保参数是字符串格式的JSON
|
||||
if isinstance(function_args, dict):
|
||||
function_args = json.dumps(function_args)
|
||||
|
||||
function_call_data = {
|
||||
"name": function_name,
|
||||
"id": function_id,
|
||||
"arguments": function_arguments
|
||||
}
|
||||
conn.func_handler.handle_llm_function_call(conn, function_call_data)
|
||||
return True
|
||||
function_call_data = {
|
||||
"name": function_name,
|
||||
"id": str(uuid.uuid4().hex),
|
||||
"arguments": function_args,
|
||||
}
|
||||
|
||||
# 其他意图处理可以在这里扩展
|
||||
await send_stt_message(conn, original_text)
|
||||
|
||||
# 默认返回False,表示继续常规聊天流程
|
||||
return False
|
||||
# 使用executor执行函数调用和结果处理
|
||||
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):
|
||||
@@ -100,10 +120,14 @@ def extract_text_in_brackets(s):
|
||||
:param s: 输入字符串
|
||||
:return: 中括号内的文字,如果不存在则返回空字符串
|
||||
"""
|
||||
left_bracket_index = s.find('[')
|
||||
right_bracket_index = s.find(']')
|
||||
left_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:
|
||||
return s[left_bracket_index + 1:right_bracket_index]
|
||||
if (
|
||||
left_bracket_index != -1
|
||||
and right_bracket_index != -1
|
||||
and left_bracket_index < right_bracket_index
|
||||
):
|
||||
return s[left_bracket_index + 1 : right_bracket_index]
|
||||
else:
|
||||
return ""
|
||||
return ""
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import json
|
||||
import asyncio
|
||||
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__
|
||||
logger = setup_logging()
|
||||
@@ -14,10 +20,13 @@ def wrap_async_function(async_func):
|
||||
try:
|
||||
# 获取连接对象(第一个参数)
|
||||
conn = args[0]
|
||||
if not hasattr(conn, 'loop'):
|
||||
if not hasattr(conn, "loop"):
|
||||
logger.bind(tag=TAG).error("Connection对象没有loop属性")
|
||||
return ActionResponse(Action.ERROR, "Connection对象没有loop属性",
|
||||
"执行操作时出错: Connection对象没有loop属性")
|
||||
return ActionResponse(
|
||||
Action.ERROR,
|
||||
"Connection对象没有loop属性",
|
||||
"执行操作时出错: Connection对象没有loop属性",
|
||||
)
|
||||
|
||||
# 使用conn对象中的事件循环
|
||||
loop = conn.loop
|
||||
@@ -37,11 +46,14 @@ def create_iot_function(device_name, method_name, method_info):
|
||||
根据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:
|
||||
# 打印响应参数
|
||||
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)
|
||||
@@ -51,14 +63,15 @@ def create_iot_function(device_name, method_name, method_info):
|
||||
# 生成结果信息
|
||||
result = f"{device_name}的{method_name}操作执行成功"
|
||||
|
||||
|
||||
# 处理响应中可能的占位符
|
||||
response = response_success
|
||||
# 替换{value}占位符
|
||||
for param_name, param_value in params.items():
|
||||
# 先尝试直接替换参数值
|
||||
if "{" + param_name + "}" in response:
|
||||
response = response.replace("{" + param_name + "}", str(param_value))
|
||||
response = response.replace(
|
||||
"{" + param_name + "}", str(param_value)
|
||||
)
|
||||
|
||||
# 如果有{value}占位符,用相关参数替换
|
||||
if "{value}" in response:
|
||||
@@ -86,7 +99,8 @@ def create_iot_query_function(device_name, prop_name, prop_info):
|
||||
try:
|
||||
# 打印响应参数
|
||||
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)
|
||||
|
||||
@@ -126,7 +140,7 @@ class IotDescriptor:
|
||||
# 根据描述创建属性
|
||||
for key, value in properties.items():
|
||||
property_item = globals()[key] = {}
|
||||
property_item['name'] = key
|
||||
property_item["name"] = key
|
||||
property_item["description"] = value["description"]
|
||||
if value["type"] == "number":
|
||||
property_item["value"] = 0
|
||||
@@ -140,7 +154,7 @@ class IotDescriptor:
|
||||
for key, value in methods.items():
|
||||
method = globals()[key] = {}
|
||||
method["description"] = value["description"]
|
||||
method['name'] = key
|
||||
method["name"] = key
|
||||
for k, v in value["parameters"].items():
|
||||
method[k] = {}
|
||||
method[k]["description"] = v["description"]
|
||||
@@ -177,19 +191,21 @@ def register_device_type(descriptor):
|
||||
"properties": {
|
||||
"response_success": {
|
||||
"type": "string",
|
||||
"description": f"查询成功时的友好回复,必须使用{{value}}作为占位符表示查询到的值"
|
||||
"description": f"查询成功时的友好回复,必须使用{{value}}作为占位符表示查询到的值",
|
||||
},
|
||||
"response_failure": {
|
||||
"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)
|
||||
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
|
||||
|
||||
# 为每个方法创建控制函数
|
||||
@@ -200,22 +216,24 @@ def register_device_type(descriptor):
|
||||
parameters = {
|
||||
param_name: {
|
||||
"type": param_info["type"],
|
||||
"description": param_info["description"]
|
||||
"description": param_info["description"],
|
||||
}
|
||||
for param_name, param_info in method_info["parameters"].items()
|
||||
}
|
||||
|
||||
# 添加响应参数
|
||||
parameters.update({
|
||||
"response_success": {
|
||||
"type": "string",
|
||||
"description": "操作成功时的友好回复,关于该设备的操作结果,设备名称尽量使用description中的名称"
|
||||
},
|
||||
"response_failure": {
|
||||
"type": "string",
|
||||
"description": "操作失败时的友好回复,关于该设备的操作结果,设备名称尽量使用description中的名称"
|
||||
parameters.update(
|
||||
{
|
||||
"response_success": {
|
||||
"type": "string",
|
||||
"description": "操作成功时的友好回复,关于该设备的操作结果,设备名称尽量使用description中的名称",
|
||||
},
|
||||
"response_failure": {
|
||||
"type": "string",
|
||||
"description": "操作失败时的友好回复,关于该设备的操作结果,设备名称尽量使用description中的名称",
|
||||
},
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
# 构建必须参数列表(原有参数 + 响应参数)
|
||||
required_params = list(method_info["parameters"].keys())
|
||||
@@ -229,12 +247,14 @@ def register_device_type(descriptor):
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": parameters,
|
||||
"required": required_params
|
||||
}
|
||||
}
|
||||
"required": required_params,
|
||||
},
|
||||
},
|
||||
}
|
||||
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
|
||||
|
||||
device_type_registry.register_device_type(type_id, functions)
|
||||
@@ -243,13 +263,24 @@ def register_device_type(descriptor):
|
||||
|
||||
# 用于接受前端设备推送的搜索iot描述
|
||||
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
|
||||
|
||||
for descriptor in descriptors:
|
||||
# 创建IOT设备描述符
|
||||
iot_descriptor = IotDescriptor(descriptor["name"], descriptor["description"], descriptor["properties"],
|
||||
descriptor["methods"])
|
||||
iot_descriptor = IotDescriptor(
|
||||
descriptor["name"],
|
||||
descriptor["description"],
|
||||
descriptor["properties"],
|
||||
descriptor["methods"],
|
||||
)
|
||||
conn.iot_descriptors[descriptor["name"]] = iot_descriptor
|
||||
|
||||
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)
|
||||
|
||||
# 在连接级注册设备函数
|
||||
if hasattr(conn, 'func_handler'):
|
||||
if hasattr(conn, "func_handler"):
|
||||
for func_name in device_functions:
|
||||
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
|
||||
|
||||
# 如果注册了新函数,更新function描述列表
|
||||
if functions_changed and hasattr(conn, 'func_handler'):
|
||||
if functions_changed and hasattr(conn, "func_handler"):
|
||||
conn.func_handler.upload_functions_desc()
|
||||
func_names = conn.func_handler.current_support_functions()
|
||||
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):
|
||||
@@ -281,11 +316,15 @@ async def handleIotStatus(conn, states):
|
||||
for k, v in state["state"].items():
|
||||
if property_item["name"] == k:
|
||||
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
|
||||
else:
|
||||
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
|
||||
|
||||
@@ -308,10 +347,14 @@ async def set_iot_status(conn, name, property_name, value):
|
||||
for property_item in iot_descriptor.properties:
|
||||
if property_item["name"] == property_name:
|
||||
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
|
||||
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
|
||||
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:
|
||||
# 找到了方法
|
||||
if method["name"] == method_name:
|
||||
await conn.websocket.send(json.dumps({
|
||||
"type": "iot",
|
||||
"commands": [
|
||||
await conn.websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"name": name,
|
||||
"method": method_name,
|
||||
"parameters": parameters
|
||||
"type": "iot",
|
||||
"commands": [
|
||||
{
|
||||
"name": name,
|
||||
"method": method_name,
|
||||
"parameters": parameters,
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
}))
|
||||
)
|
||||
)
|
||||
return
|
||||
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:
|
||||
await no_voice_close_connect(conn)
|
||||
conn.asr_audio.append(audio)
|
||||
conn.asr_audio = conn.asr_audio[-5:] # 保留最新的5帧音频内容,解决ASR句首丢字问题
|
||||
conn.asr_audio = conn.asr_audio[
|
||||
-10:
|
||||
] # 保留最新的10帧音频内容,解决ASR句首丢字问题
|
||||
return
|
||||
conn.client_no_voice_last_time = 0.0
|
||||
conn.asr_audio.append(audio)
|
||||
@@ -30,10 +32,12 @@ async def handleAudioMessage(conn, audio):
|
||||
conn.client_abort = False
|
||||
conn.asr_server_receive = False
|
||||
# 音频太短了,无法识别
|
||||
if len(conn.asr_audio) < 10:
|
||||
if len(conn.asr_audio) < 15:
|
||||
conn.asr_server_receive = True
|
||||
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}")
|
||||
text_len, _ = remove_punctuation_and_length(text)
|
||||
if text_len > 0:
|
||||
@@ -47,12 +51,12 @@ async def handleAudioMessage(conn, audio):
|
||||
async def startToChat(conn, text):
|
||||
# 首先进行意图分析
|
||||
intent_handled = await handle_user_intent(conn, text)
|
||||
|
||||
|
||||
if intent_handled:
|
||||
# 如果意图已被处理,不再进行聊天
|
||||
conn.asr_server_receive = True
|
||||
return
|
||||
|
||||
|
||||
# 意图未被处理,继续常规聊天流程
|
||||
await send_stt_message(conn, text)
|
||||
if conn.use_function_call_mode:
|
||||
@@ -67,10 +71,17 @@ async def no_voice_close_connect(conn):
|
||||
conn.client_no_voice_last_time = time.time() * 1000
|
||||
else:
|
||||
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)
|
||||
if not conn.close_after_chat and no_voice_time > 1000 * close_connection_no_voice_time:
|
||||
close_connection_no_voice_time = conn.config.get(
|
||||
"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.client_abort = False
|
||||
conn.asr_server_receive = False
|
||||
prompt = "请你以“时间过得真快”未来头,用富有感情、依依不舍的话来结束这场对话吧。"
|
||||
prompt = (
|
||||
"请你以“时间过得真快”未来头,用富有感情、依依不舍的话来结束这场对话吧。"
|
||||
)
|
||||
await startToChat(conn, prompt)
|
||||
|
||||
@@ -2,8 +2,11 @@ from config.logger import setup_logging
|
||||
import json
|
||||
from core.handle.abortHandle import handleAbortMessage
|
||||
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.sendAudioHandle import send_stt_message, send_tts_message
|
||||
from core.handle.iotHandle import handleIotDescriptors, handleIotStatus
|
||||
import asyncio
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
@@ -32,17 +35,31 @@ async def handleTextMessage(conn, message):
|
||||
conn.client_have_voice = True
|
||||
conn.client_voice_stop = True
|
||||
if len(conn.asr_audio) > 0:
|
||||
await handleAudioMessage(conn, b'')
|
||||
await handleAudioMessage(conn, b"")
|
||||
elif msg_json["state"] == "detect":
|
||||
conn.asr_server_receive = False
|
||||
conn.client_have_voice = False
|
||||
conn.asr_audio.clear()
|
||||
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":
|
||||
if "descriptors" in msg_json:
|
||||
await handleIotDescriptors(conn, msg_json["descriptors"])
|
||||
asyncio.create_task(handleIotDescriptors(conn, msg_json["descriptors"]))
|
||||
if "states" in msg_json:
|
||||
await handleIotStatus(conn, msg_json["states"])
|
||||
asyncio.create_task(handleIotStatus(conn, msg_json["states"]))
|
||||
except json.JSONDecodeError:
|
||||
await conn.websocket.send(message)
|
||||
|
||||
Reference in New Issue
Block a user