mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-25 08:33:53 +08:00
Merge branch 'newsnow' into add_local_tts_change_voice
This commit is contained in:
@@ -104,7 +104,8 @@ wakeup_words:
|
||||
- "喵喵同学"
|
||||
- "小滨小滨"
|
||||
- "小冰小冰"
|
||||
|
||||
# MCP接入点地址
|
||||
mcp_endpoint: 你的接入点 websocket地址
|
||||
# 插件的基础配置
|
||||
plugins:
|
||||
# 获取天气插件的配置,这里填写你的api_key
|
||||
@@ -117,11 +118,15 @@ plugins:
|
||||
# 更多类型的新闻列表查看 https://www.chinanews.com.cn/rss/
|
||||
get_news_from_chinanews:
|
||||
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"
|
||||
get_news_from_newsnow: {"url": "https://newsnow.busiyi.world/api/s?id="}
|
||||
society_rss_url: "https://www.chinanews.com.cn/rss/society.xml"
|
||||
world_rss_url: "https://www.chinanews.com.cn/rss/world.xml"
|
||||
finance_rss_url: "https://www.chinanews.com.cn/rss/finance.xml"
|
||||
get_news_from_newsnow:
|
||||
url: "https://newsnow.busiyi.world/api/s?id="
|
||||
news_sources:
|
||||
thepaper: "澎湃新闻"
|
||||
baidu: "百度热搜"
|
||||
cls-depth: "财联社"
|
||||
home_assistant:
|
||||
devices:
|
||||
- 客厅,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1
|
||||
@@ -766,4 +771,4 @@ TTS:
|
||||
# 支持声音克隆,可自行上传音频,填入voice参数,voice参数为空时,使用默认声音
|
||||
access_token: "U4YdYXVfpwWnk2t5Gp822zWPCuORyeJL"
|
||||
voice: "OUeAo1mhq6IBExi"
|
||||
output_dir: tmp/
|
||||
output_dir: tmp/
|
||||
|
||||
@@ -5,7 +5,7 @@ from config.config_loader import load_config
|
||||
from config.settings import check_config_file
|
||||
from datetime import datetime
|
||||
|
||||
SERVER_VERSION = "0.5.5"
|
||||
SERVER_VERSION = "0.6.1"
|
||||
_logger_initialized = False
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ def setup_logging():
|
||||
# 输出到文件 - 统一目录,按大小轮转
|
||||
# 日志文件完整路径
|
||||
log_file_path = os.path.join(log_dir, log_file)
|
||||
|
||||
|
||||
# 添加日志处理器
|
||||
logger.add(
|
||||
log_file_path,
|
||||
@@ -146,14 +146,14 @@ def update_module_string(selected_module_str):
|
||||
level=log_config.get("log_level", "INFO"),
|
||||
filter=formatter,
|
||||
)
|
||||
|
||||
|
||||
# 更新文件日志配置 - 统一目录,按大小轮转
|
||||
log_dir = log_config.get("log_dir", "tmp")
|
||||
log_file = log_config.get("log_file", "server.log")
|
||||
|
||||
|
||||
# 日志文件完整路径
|
||||
log_file_path = os.path.join(log_dir, log_file)
|
||||
|
||||
|
||||
logger.add(
|
||||
log_file_path,
|
||||
format=log_format_file,
|
||||
|
||||
@@ -8,6 +8,7 @@ from config.config_loader import get_private_config_from_api
|
||||
from core.utils.auth import AuthToken
|
||||
import base64
|
||||
from typing import Tuple, Optional
|
||||
from plugins_func.register import Action
|
||||
|
||||
TAG = __name__
|
||||
|
||||
@@ -55,11 +56,7 @@ class VisionHandler:
|
||||
device_id = request.headers.get("Device-Id", "")
|
||||
client_id = request.headers.get("Client-Id", "")
|
||||
if device_id != token_device_id:
|
||||
return web.Response(
|
||||
text=json.dumps(self._create_error_response("设备ID与token不匹配")),
|
||||
content_type="application/json",
|
||||
status=401,
|
||||
)
|
||||
raise ValueError("设备ID与token不匹配")
|
||||
# 解析multipart/form-data请求
|
||||
reader = await request.multipart()
|
||||
|
||||
@@ -126,7 +123,8 @@ class VisionHandler:
|
||||
|
||||
return_json = {
|
||||
"success": True,
|
||||
"result": result,
|
||||
"action": Action.RESPONSE.name,
|
||||
"response": result,
|
||||
}
|
||||
|
||||
response = web.Response(
|
||||
|
||||
@@ -10,7 +10,6 @@ import threading
|
||||
import traceback
|
||||
import subprocess
|
||||
import websockets
|
||||
from core.handle.mcpHandle import call_mcp_tool
|
||||
from core.utils.util import (
|
||||
extract_json_from_string,
|
||||
check_vad_update,
|
||||
@@ -18,7 +17,6 @@ from core.utils.util import (
|
||||
filter_sensitive_info,
|
||||
)
|
||||
from typing import Dict, Any
|
||||
from core.mcp.manager import MCPManager
|
||||
from core.utils.modules_initialize import (
|
||||
initialize_modules,
|
||||
initialize_tts,
|
||||
@@ -30,7 +28,7 @@ from concurrent.futures import ThreadPoolExecutor
|
||||
from core.utils.dialogue import Message, Dialogue
|
||||
from core.providers.asr.dto.dto import InterfaceType
|
||||
from core.handle.textHandle import handleTextMessage
|
||||
from core.handle.functionHandler import FunctionHandler
|
||||
from core.providers.tools.unified_tool_handler import UnifiedToolHandler
|
||||
from plugins_func.loadplugins import auto_import_modules
|
||||
from plugins_func.register import Action, ActionResponse
|
||||
from core.auth import AuthMiddleware, AuthenticationError
|
||||
@@ -112,8 +110,7 @@ class ConnectionHandler:
|
||||
# vad相关变量
|
||||
self.client_audio_buffer = bytearray()
|
||||
self.client_have_voice = False
|
||||
self.client_have_voice_last_time = 0.0
|
||||
self.client_no_voice_last_time = 0.0
|
||||
self.last_activity_time = 0.0 # 统一的活动时间戳(毫秒)
|
||||
self.client_voice_stop = False
|
||||
|
||||
# asr相关变量
|
||||
@@ -144,10 +141,10 @@ class ConnectionHandler:
|
||||
self.load_function_plugin = False
|
||||
self.intent_type = "nointent"
|
||||
|
||||
self.timeout_task = None
|
||||
self.timeout_seconds = (
|
||||
int(self.config.get("close_connection_no_voice_time", 120)) + 60
|
||||
) # 在原来第一道关闭的基础上加60秒,进行二道关闭
|
||||
self.timeout_task = None
|
||||
|
||||
# {"mcp":true} 表示启用MCP功能
|
||||
self.features = None
|
||||
@@ -188,6 +185,9 @@ class ConnectionHandler:
|
||||
self.websocket = ws
|
||||
self.device_id = self.headers.get("device-id", None)
|
||||
|
||||
# 初始化活动时间戳
|
||||
self.last_activity_time = time.time() * 1000
|
||||
|
||||
# 启动超时检查任务
|
||||
self.timeout_task = asyncio.create_task(self._check_timeout())
|
||||
|
||||
@@ -242,18 +242,10 @@ class ConnectionHandler:
|
||||
# 立即关闭连接,不等待记忆保存完成
|
||||
await self.close(ws)
|
||||
|
||||
async def reset_timeout(self):
|
||||
"""重置超时计时器"""
|
||||
if self.timeout_task:
|
||||
self.timeout_task.cancel()
|
||||
self.timeout_task = asyncio.create_task(self._check_timeout())
|
||||
|
||||
async def _route_message(self, message):
|
||||
"""消息路由"""
|
||||
# 重置超时计时器
|
||||
await self.reset_timeout()
|
||||
|
||||
if isinstance(message, str):
|
||||
self.last_activity_time = time.time() * 1000
|
||||
await handleTextMessage(self, message)
|
||||
elif isinstance(message, bytes):
|
||||
if self.vad is None:
|
||||
@@ -425,10 +417,12 @@ class ConnectionHandler:
|
||||
init_asr = check_asr_update(self.common_config, private_config)
|
||||
|
||||
if init_vad:
|
||||
self.config["VAD"] = private_config["VAD"]
|
||||
self.config["selected_module"]["VAD"] = private_config["selected_module"][
|
||||
"VAD"
|
||||
]
|
||||
if init_asr:
|
||||
self.config["ASR"] = private_config["ASR"]
|
||||
self.config["selected_module"]["ASR"] = private_config["selected_module"][
|
||||
"ASR"
|
||||
]
|
||||
@@ -453,9 +447,17 @@ class ConnectionHandler:
|
||||
if private_config.get("Intent", None) is not None:
|
||||
init_intent = True
|
||||
self.config["Intent"] = private_config["Intent"]
|
||||
self.config["selected_module"]["Intent"] = private_config[
|
||||
"selected_module"
|
||||
]["Intent"]
|
||||
model_intent = private_config.get("selected_module", {}).get("Intent", {})
|
||||
self.config["selected_module"]["Intent"] = model_intent
|
||||
# 加载插件配置
|
||||
if model_intent != "Intent_nointent":
|
||||
plugin_from_server = private_config.get("plugins", {})
|
||||
for plugin, config_str in plugin_from_server.items():
|
||||
plugin_from_server[plugin] = json.loads(config_str)
|
||||
self.config["plugins"] = plugin_from_server
|
||||
self.config["Intent"][self.config["selected_module"]["Intent"]][
|
||||
"functions"
|
||||
] = plugin_from_server.keys()
|
||||
if private_config.get("prompt", None) is not None:
|
||||
self.config["prompt"] = private_config["prompt"]
|
||||
if private_config.get("summaryMemory", None) is not None:
|
||||
@@ -464,6 +466,8 @@ class ConnectionHandler:
|
||||
self.max_output_size = int(private_config["device_max_output_size"])
|
||||
if private_config.get("chat_history_conf", None) is not None:
|
||||
self.chat_history_conf = int(private_config["chat_history_conf"])
|
||||
if private_config.get("mcp_endpoint", None) is not None:
|
||||
self.config["mcp_endpoint"] = private_config["mcp_endpoint"]
|
||||
try:
|
||||
modules = initialize_modules(
|
||||
self.logger,
|
||||
@@ -575,14 +579,12 @@ class ConnectionHandler:
|
||||
self.intent.set_llm(self.llm)
|
||||
self.logger.bind(tag=TAG).info("使用主LLM作为意图识别模型")
|
||||
|
||||
"""加载插件"""
|
||||
self.func_handler = FunctionHandler(self)
|
||||
self.mcp_manager = MCPManager(self)
|
||||
"""加载统一工具处理器"""
|
||||
self.func_handler = UnifiedToolHandler(self)
|
||||
|
||||
"""加载MCP工具"""
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.mcp_manager.initialize_servers(), self.loop
|
||||
)
|
||||
# 异步初始化工具处理器
|
||||
if hasattr(self, "loop") and self.loop:
|
||||
asyncio.run_coroutine_threadsafe(self.func_handler._initialize(), self.loop)
|
||||
|
||||
def change_system_prompt(self, prompt):
|
||||
self.prompt = prompt
|
||||
@@ -600,12 +602,6 @@ class ConnectionHandler:
|
||||
functions = None
|
||||
if self.intent_type == "function_call" and hasattr(self, "func_handler"):
|
||||
functions = self.func_handler.get_functions()
|
||||
if hasattr(self, "mcp_client"):
|
||||
mcp_tools = self.mcp_client.get_available_tools()
|
||||
if mcp_tools is not None and len(mcp_tools) > 0:
|
||||
if functions is None:
|
||||
functions = []
|
||||
functions.extend(mcp_tools)
|
||||
response_message = []
|
||||
|
||||
try:
|
||||
@@ -617,8 +613,7 @@ class ConnectionHandler:
|
||||
)
|
||||
memory_str = future.result()
|
||||
|
||||
uuid_str = str(uuid.uuid4()).replace("-", "")
|
||||
self.sentence_id = uuid_str
|
||||
self.sentence_id = str(uuid.uuid4().hex)
|
||||
|
||||
if self.intent_type == "function_call" and functions is not None:
|
||||
# 使用支持functions的streaming接口
|
||||
@@ -723,37 +718,13 @@ class ConnectionHandler:
|
||||
"arguments": function_arguments,
|
||||
}
|
||||
|
||||
# 处理Server端MCP工具调用
|
||||
if self.mcp_manager.is_mcp_tool(function_name):
|
||||
result = self._handle_mcp_tool_call(function_call_data)
|
||||
elif hasattr(self, "mcp_client") and self.mcp_client.has_tool(
|
||||
function_name
|
||||
):
|
||||
# 如果是小智端MCP工具调用
|
||||
self.logger.bind(tag=TAG).debug(
|
||||
f"调用小智端MCP工具: {function_name}, 参数: {function_arguments}"
|
||||
)
|
||||
try:
|
||||
result = asyncio.run_coroutine_threadsafe(
|
||||
call_mcp_tool(
|
||||
self, self.mcp_client, function_name, function_arguments
|
||||
),
|
||||
self.loop,
|
||||
).result()
|
||||
self.logger.bind(tag=TAG).debug(f"MCP工具调用结果: {result}")
|
||||
result = ActionResponse(
|
||||
action=Action.REQLLM, result=result, response=""
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"MCP工具调用失败: {e}")
|
||||
result = ActionResponse(
|
||||
action=Action.REQLLM, result="MCP工具调用失败", response=""
|
||||
)
|
||||
else:
|
||||
# 处理系统函数
|
||||
result = self.func_handler.handle_llm_function_call(
|
||||
# 使用统一工具处理器处理所有工具调用
|
||||
result = asyncio.run_coroutine_threadsafe(
|
||||
self.func_handler.handle_llm_function_call(
|
||||
self, function_call_data
|
||||
)
|
||||
),
|
||||
self.loop,
|
||||
).result()
|
||||
self._handle_function_result(result, function_call_data)
|
||||
|
||||
# 存储对话内容
|
||||
@@ -776,48 +747,6 @@ class ConnectionHandler:
|
||||
|
||||
return True
|
||||
|
||||
def _handle_mcp_tool_call(self, function_call_data):
|
||||
function_arguments = function_call_data["arguments"]
|
||||
function_name = function_call_data["name"]
|
||||
try:
|
||||
args_dict = function_arguments
|
||||
if isinstance(function_arguments, str):
|
||||
try:
|
||||
args_dict = json.loads(function_arguments)
|
||||
except json.JSONDecodeError:
|
||||
self.logger.bind(tag=TAG).error(
|
||||
f"无法解析 function_arguments: {function_arguments}"
|
||||
)
|
||||
return ActionResponse(
|
||||
action=Action.REQLLM, result="参数解析失败", response=""
|
||||
)
|
||||
|
||||
tool_result = asyncio.run_coroutine_threadsafe(
|
||||
self.mcp_manager.execute_tool(function_name, args_dict), self.loop
|
||||
).result()
|
||||
# meta=None content=[TextContent(type='text', text='北京当前天气:\n温度: 21°C\n天气: 晴\n湿度: 6%\n风向: 西北 风\n风力等级: 5级', annotations=None)] isError=False
|
||||
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):
|
||||
if result.action == Action.RESPONSE: # 直接回复前端
|
||||
text = result.response
|
||||
@@ -857,7 +786,7 @@ class ConnectionHandler:
|
||||
)
|
||||
self.chat(text, tool_call=True)
|
||||
elif result.action == Action.NOTFOUND or result.action == Action.ERROR:
|
||||
text = result.result
|
||||
text = result.response if result.response else result.result
|
||||
self.tts.tts_one_sentence(self, ContentType.TEXT, content_detail=text)
|
||||
self.dialogue.put(Message(role="assistant", content=text))
|
||||
else:
|
||||
@@ -912,9 +841,9 @@ class ConnectionHandler:
|
||||
self.timeout_task.cancel()
|
||||
self.timeout_task = None
|
||||
|
||||
# 清理MCP资源
|
||||
if hasattr(self, "mcp_manager") and self.mcp_manager:
|
||||
await self.mcp_manager.cleanup_all()
|
||||
# 清理工具处理器资源
|
||||
if hasattr(self, "func_handler") and self.func_handler:
|
||||
await self.func_handler.cleanup()
|
||||
|
||||
# 触发停止事件
|
||||
if self.stop_event:
|
||||
@@ -966,7 +895,6 @@ class ConnectionHandler:
|
||||
def reset_vad_states(self):
|
||||
self.client_audio_buffer = bytearray()
|
||||
self.client_have_voice = False
|
||||
self.client_have_voice_last_time = 0
|
||||
self.client_voice_stop = False
|
||||
self.logger.bind(tag=TAG).debug("VAD states reset.")
|
||||
|
||||
@@ -985,10 +913,18 @@ class ConnectionHandler:
|
||||
"""检查连接超时"""
|
||||
try:
|
||||
while not self.stop_event.is_set():
|
||||
await asyncio.sleep(self.timeout_seconds)
|
||||
if not self.stop_event.is_set():
|
||||
self.logger.bind(tag=TAG).info("连接超时,准备关闭")
|
||||
await self.close(self.websocket)
|
||||
break
|
||||
# 检查是否超时(只有在时间戳已初始化的情况下)
|
||||
if self.last_activity_time > 0.0:
|
||||
current_time = time.time() * 1000
|
||||
if (
|
||||
current_time - self.last_activity_time
|
||||
> self.timeout_seconds * 1000
|
||||
):
|
||||
if not self.stop_event.is_set():
|
||||
self.logger.bind(tag=TAG).info("连接超时,准备关闭")
|
||||
await self.close(self.websocket)
|
||||
break
|
||||
# 每10秒检查一次,避免过于频繁
|
||||
await asyncio.sleep(10)
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"超时检查任务出错: {e}")
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
from config.logger import setup_logging
|
||||
import json
|
||||
from plugins_func.register import (
|
||||
FunctionRegistry,
|
||||
ActionResponse,
|
||||
Action,
|
||||
ToolType,
|
||||
DeviceTypeRegistry,
|
||||
)
|
||||
from plugins_func.functions.hass_init import append_devices_to_prompt
|
||||
|
||||
TAG = __name__
|
||||
|
||||
|
||||
class FunctionHandler:
|
||||
def __init__(self, conn):
|
||||
self.conn = conn
|
||||
self.config = conn.config
|
||||
self.device_type_registry = DeviceTypeRegistry()
|
||||
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:
|
||||
return
|
||||
# 可编辑的列表中去掉plugin_loader
|
||||
surport_plugins = [func for func in func_names if func != "plugin_loader"]
|
||||
func_names = ",".join(surport_plugins)
|
||||
for function_desc in self.functions_desc:
|
||||
if function_desc["function"]["name"] == "plugin_loader":
|
||||
function_desc["function"]["description"] = function_desc["function"][
|
||||
"description"
|
||||
].replace("[plugins]", func_names)
|
||||
break
|
||||
|
||||
def upload_functions_desc(self):
|
||||
self.functions_desc = self.function_registry.get_all_function_desc()
|
||||
|
||||
def current_support_functions(self):
|
||||
func_names = []
|
||||
for func in self.functions_desc:
|
||||
func_names.append(func["function"]["name"])
|
||||
# 打印当前支持的函数列表
|
||||
self.conn.logger.bind(tag=TAG, session_id=self.conn.session_id).info(
|
||||
f"当前支持的函数列表: {func_names}"
|
||||
)
|
||||
return func_names
|
||||
|
||||
def get_functions(self):
|
||||
"""获取功能调用配置"""
|
||||
return self.functions_desc
|
||||
|
||||
def register_nessary_functions(self):
|
||||
"""注册必要的函数"""
|
||||
self.function_registry.register_function("handle_exit_intent")
|
||||
self.function_registry.register_function("plugin_loader")
|
||||
self.function_registry.register_function("get_time")
|
||||
self.function_registry.register_function("get_lunar")
|
||||
# self.function_registry.register_function("handle_speaker_volume_or_screen_brightness")
|
||||
|
||||
def register_config_functions(self):
|
||||
"""注册配置中的函数,可以不同客户端使用不同的配置"""
|
||||
for func in self.config["Intent"][self.config["selected_module"]["Intent"]].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)
|
||||
|
||||
def handle_llm_function_call(self, conn, function_call_data):
|
||||
try:
|
||||
function_name = function_call_data["name"]
|
||||
funcItem = self.get_function(function_name)
|
||||
if not funcItem:
|
||||
return ActionResponse(
|
||||
action=Action.NOTFOUND, result="没有找到对应的函数", response=""
|
||||
)
|
||||
func = funcItem.func
|
||||
arguments = function_call_data["arguments"]
|
||||
arguments = json.loads(arguments) if arguments else {}
|
||||
self.conn.logger.bind(tag=TAG).debug(
|
||||
f"调用函数: {function_name}, 参数: {arguments}"
|
||||
)
|
||||
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=""
|
||||
)
|
||||
except Exception as e:
|
||||
self.conn.logger.bind(tag=TAG).error(f"处理function call错误: {e}")
|
||||
|
||||
return None
|
||||
@@ -7,7 +7,7 @@ from core.utils.util import audio_to_data
|
||||
from core.handle.sendAudioHandle import sendAudioMessage, send_stt_message
|
||||
from core.utils.util import remove_punctuation_and_length, opus_datas_to_wav_bytes
|
||||
from core.providers.tts.dto.dto import ContentType, SentenceType
|
||||
from core.handle.mcpHandle import (
|
||||
from core.providers.tools.device_mcp import (
|
||||
MCPClient,
|
||||
send_mcp_initialize_message,
|
||||
send_mcp_tools_list_request,
|
||||
|
||||
@@ -6,7 +6,7 @@ from core.handle.helloHandle import checkWakeupWords
|
||||
from core.utils.util import remove_punctuation_and_length
|
||||
from core.providers.tts.dto.dto import ContentType
|
||||
from core.utils.dialogue import Message
|
||||
from core.handle.mcpHandle import call_mcp_tool
|
||||
from core.providers.tools.device_mcp import call_mcp_tool
|
||||
from plugins_func.register import Action, ActionResponse
|
||||
from loguru import logger
|
||||
|
||||
@@ -84,9 +84,11 @@ async def process_intent_result(conn, intent_result, original_text):
|
||||
if not funcItem:
|
||||
conn.func_handler.function_registry.register_function("play_music")
|
||||
|
||||
function_args = None
|
||||
function_args = {}
|
||||
if "arguments" in intent_data["function_call"]:
|
||||
function_args = intent_data["function_call"]["arguments"]
|
||||
if function_args is None:
|
||||
function_args = {}
|
||||
# 确保参数是字符串格式的JSON
|
||||
if isinstance(function_args, dict):
|
||||
function_args = json.dumps(function_args)
|
||||
@@ -104,36 +106,18 @@ async def process_intent_result(conn, intent_result, original_text):
|
||||
def process_function_call():
|
||||
conn.dialogue.put(Message(role="user", content=original_text))
|
||||
|
||||
# 处理Server端MCP工具调用
|
||||
if conn.mcp_manager.is_mcp_tool(function_name):
|
||||
result = conn._handle_mcp_tool_call(function_call_data)
|
||||
elif hasattr(conn, "mcp_client") and conn.mcp_client.has_tool(
|
||||
function_name
|
||||
):
|
||||
# 如果是小智端MCP工具调用
|
||||
conn.logger.bind(tag=TAG).debug(
|
||||
f"调用小智端MCP工具: {function_name}, 参数: {function_args}"
|
||||
)
|
||||
try:
|
||||
result = asyncio.run_coroutine_threadsafe(
|
||||
call_mcp_tool(
|
||||
conn, conn.mcp_client, function_name, function_args
|
||||
),
|
||||
conn.loop,
|
||||
).result()
|
||||
conn.logger.bind(tag=TAG).debug(f"MCP工具调用结果: {result}")
|
||||
result = ActionResponse(
|
||||
action=Action.REQLLM, result=result, response=""
|
||||
)
|
||||
except Exception as e:
|
||||
conn.logger.bind(tag=TAG).error(f"MCP工具调用失败: {e}")
|
||||
result = ActionResponse(
|
||||
action=Action.REQLLM, result="MCP工具调用失败", response=""
|
||||
)
|
||||
else:
|
||||
# 处理系统函数
|
||||
result = conn.func_handler.handle_llm_function_call(
|
||||
conn, function_call_data
|
||||
# 使用统一工具处理器处理所有工具调用
|
||||
try:
|
||||
result = asyncio.run_coroutine_threadsafe(
|
||||
conn.func_handler.handle_llm_function_call(
|
||||
conn, function_call_data
|
||||
),
|
||||
conn.loop,
|
||||
).result()
|
||||
except Exception as e:
|
||||
conn.logger.bind(tag=TAG).error(f"工具调用失败: {e}")
|
||||
result = ActionResponse(
|
||||
action=Action.ERROR, result=str(e), response=str(e)
|
||||
)
|
||||
|
||||
if result:
|
||||
|
||||
@@ -1,422 +0,0 @@
|
||||
import json
|
||||
import asyncio
|
||||
from plugins_func.register import (
|
||||
FunctionItem,
|
||||
register_device_function,
|
||||
ActionResponse,
|
||||
Action,
|
||||
ToolType,
|
||||
)
|
||||
|
||||
TAG = __name__
|
||||
|
||||
|
||||
def wrap_async_function(async_func):
|
||||
"""包装异步函数为同步函数"""
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
# 获取连接对象(第一个参数)
|
||||
conn = args[0]
|
||||
if not hasattr(conn, "loop"):
|
||||
conn.logger.bind(tag=TAG).error("Connection对象没有loop属性")
|
||||
return ActionResponse(
|
||||
Action.ERROR,
|
||||
"Connection对象没有loop属性",
|
||||
"执行操作时出错: Connection对象没有loop属性",
|
||||
)
|
||||
|
||||
# 使用conn对象中的事件循环
|
||||
loop = conn.loop
|
||||
# 在conn的事件循环中运行异步函数
|
||||
future = asyncio.run_coroutine_threadsafe(async_func(*args, **kwargs), loop)
|
||||
# 等待结果返回
|
||||
return future.result()
|
||||
except Exception as e:
|
||||
conn.logger.bind(tag=TAG).error(f"运行异步函数时出错: {e}")
|
||||
return ActionResponse(Action.ERROR, str(e), f"执行操作时出错: {e}")
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def create_iot_function(device_name, method_name, method_info):
|
||||
"""
|
||||
根据IOT设备描述生成通用的控制函数
|
||||
"""
|
||||
|
||||
async def iot_control_function(
|
||||
conn, response_success=None, response_failure=None, **params
|
||||
):
|
||||
try:
|
||||
# 设置默认响应消息
|
||||
if not response_success:
|
||||
response_success = "操作成功"
|
||||
if not response_failure:
|
||||
response_failure = "操作失败"
|
||||
|
||||
# 打印响应参数
|
||||
conn.logger.bind(tag=TAG).debug(
|
||||
f"控制函数接收到的响应参数: success='{response_success}', failure='{response_failure}'"
|
||||
)
|
||||
|
||||
# 发送控制命令
|
||||
await send_iot_conn(conn, device_name, method_name, params)
|
||||
# 等待一小段时间让状态更新
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# 生成结果信息
|
||||
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)
|
||||
)
|
||||
|
||||
# 如果有{value}占位符,用相关参数替换
|
||||
if "{value}" in response:
|
||||
response = response.replace("{value}", str(param_value))
|
||||
break
|
||||
|
||||
return ActionResponse(Action.RESPONSE, result, response)
|
||||
except Exception as e:
|
||||
conn.logger.bind(tag=TAG).error(
|
||||
f"执行{device_name}的{method_name}操作失败: {e}"
|
||||
)
|
||||
|
||||
# 操作失败时使用大模型提供的失败响应
|
||||
response = response_failure
|
||||
|
||||
return ActionResponse(Action.ERROR, str(e), response)
|
||||
|
||||
return wrap_async_function(iot_control_function)
|
||||
|
||||
|
||||
def create_iot_query_function(device_name, prop_name, prop_info):
|
||||
"""
|
||||
根据IOT设备属性创建查询函数
|
||||
"""
|
||||
|
||||
async def iot_query_function(conn, response_success=None, response_failure=None):
|
||||
try:
|
||||
# 打印响应参数
|
||||
conn.logger.bind(tag=TAG).info(
|
||||
f"查询函数接收到的响应参数: success='{response_success}', failure='{response_failure}'"
|
||||
)
|
||||
|
||||
value = await get_iot_status(conn, device_name, prop_name)
|
||||
|
||||
# 查询成功,生成结果
|
||||
if value is not None:
|
||||
# 使用大模型提供的成功响应,并替换其中的占位符
|
||||
response = response_success.replace("{value}", str(value))
|
||||
|
||||
return ActionResponse(Action.RESPONSE, str(value), response)
|
||||
else:
|
||||
# 查询失败,使用大模型提供的失败响应
|
||||
response = response_failure
|
||||
|
||||
return ActionResponse(Action.ERROR, f"属性{prop_name}不存在", response)
|
||||
except Exception as e:
|
||||
conn.logger.bind(tag=TAG).error(
|
||||
f"查询{device_name}的{prop_name}时出错: {e}"
|
||||
)
|
||||
|
||||
# 查询出错时使用大模型提供的失败响应
|
||||
response = response_failure
|
||||
|
||||
return ActionResponse(Action.ERROR, str(e), response)
|
||||
|
||||
return wrap_async_function(iot_query_function)
|
||||
|
||||
|
||||
class IotDescriptor:
|
||||
"""
|
||||
A class to represent an IoT descriptor.
|
||||
"""
|
||||
|
||||
def __init__(self, name, description, properties, methods):
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.properties = []
|
||||
self.methods = []
|
||||
|
||||
# 根据描述创建属性
|
||||
if properties is not None:
|
||||
for key, value in properties.items():
|
||||
property_item = {}
|
||||
property_item["name"] = key
|
||||
property_item["description"] = value["description"]
|
||||
if value["type"] == "number":
|
||||
property_item["value"] = 0
|
||||
elif value["type"] == "boolean":
|
||||
property_item["value"] = False
|
||||
else:
|
||||
property_item["value"] = ""
|
||||
self.properties.append(property_item)
|
||||
|
||||
# 根据描述创建方法
|
||||
if methods is not None:
|
||||
for key, value in methods.items():
|
||||
method = {}
|
||||
method["description"] = value["description"]
|
||||
method["name"] = key
|
||||
# 检查方法是否有参数
|
||||
if "parameters" in value:
|
||||
method["parameters"] = {}
|
||||
for k, v in value["parameters"].items():
|
||||
method["parameters"][k] = {
|
||||
"description": v["description"],
|
||||
"type": v["type"],
|
||||
}
|
||||
self.methods.append(method)
|
||||
|
||||
|
||||
def register_device_type(descriptor, device_type_registry):
|
||||
"""注册设备类型及其功能"""
|
||||
device_name = descriptor["name"]
|
||||
type_id = device_type_registry.generate_device_type_id(descriptor)
|
||||
|
||||
# 如果该类型已注册,直接返回类型ID
|
||||
if type_id in device_type_registry.type_functions:
|
||||
return type_id
|
||||
|
||||
functions = {}
|
||||
|
||||
# 为每个属性创建查询函数
|
||||
for prop_name, prop_info in descriptor["properties"].items():
|
||||
func_name = f"get_{device_name.lower()}_{prop_name.lower()}"
|
||||
func_desc = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": func_name,
|
||||
"description": f"查询{descriptor['description']}的{prop_info['description']}",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"response_success": {
|
||||
"type": "string",
|
||||
"description": f"查询成功时的友好回复,必须使用{{value}}作为占位符表示查询到的值",
|
||||
},
|
||||
"response_failure": {
|
||||
"type": "string",
|
||||
"description": f"查询失败时的友好回复,例如:'无法获取{device_name}的{prop_info['description']}'",
|
||||
},
|
||||
},
|
||||
"required": ["response_success", "response_failure"],
|
||||
},
|
||||
},
|
||||
}
|
||||
query_func = create_iot_query_function(device_name, prop_name, prop_info)
|
||||
decorated_func = register_device_function(
|
||||
func_name, func_desc, ToolType.IOT_CTL
|
||||
)(query_func)
|
||||
functions[func_name] = FunctionItem(
|
||||
func_name, func_desc, decorated_func, ToolType.IOT_CTL
|
||||
)
|
||||
|
||||
# 为每个方法创建控制函数
|
||||
for method_name, method_info in descriptor["methods"].items():
|
||||
func_name = f"{device_name.lower()}_{method_name.lower()}"
|
||||
|
||||
# 创建参数字典,添加原有参数
|
||||
parameters = {}
|
||||
required_params = []
|
||||
|
||||
# 如果方法有参数,则添加参数信息
|
||||
if "parameters" in method_info:
|
||||
parameters = {
|
||||
param_name: {
|
||||
"type": param_info["type"],
|
||||
"description": param_info["description"],
|
||||
}
|
||||
for param_name, param_info in method_info["parameters"].items()
|
||||
}
|
||||
required_params = list(method_info["parameters"].keys())
|
||||
|
||||
# 添加响应参数
|
||||
parameters.update(
|
||||
{
|
||||
"response_success": {
|
||||
"type": "string",
|
||||
"description": "操作成功时的友好回复,关于该设备的操作结果,设备名称尽量使用description中的名称",
|
||||
},
|
||||
"response_failure": {
|
||||
"type": "string",
|
||||
"description": "操作失败时的友好回复,关于该设备的操作结果,设备名称尽量使用description中的名称",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
# 构建必须参数列表(原有参数 + 响应参数)
|
||||
required_params.extend(["response_success", "response_failure"])
|
||||
|
||||
func_desc = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": func_name,
|
||||
"description": f"{descriptor['description']} - {method_info['description']}",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": parameters,
|
||||
"required": required_params,
|
||||
},
|
||||
},
|
||||
}
|
||||
control_func = create_iot_function(device_name, method_name, method_info)
|
||||
decorated_func = register_device_function(
|
||||
func_name, func_desc, ToolType.IOT_CTL
|
||||
)(control_func)
|
||||
functions[func_name] = FunctionItem(
|
||||
func_name, func_desc, decorated_func, ToolType.IOT_CTL
|
||||
)
|
||||
|
||||
device_type_registry.register_device_type(type_id, functions)
|
||||
return type_id
|
||||
|
||||
|
||||
# 用于接受前端设备推送的搜索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:
|
||||
conn.logger.bind(tag=TAG).debug("连接对象没有func_handler")
|
||||
return
|
||||
"""处理物联网描述"""
|
||||
functions_changed = False
|
||||
|
||||
for descriptor in descriptors:
|
||||
# 如果descriptor没有properties和methods,则直接跳过
|
||||
if "properties" not in descriptor and "methods" not in descriptor:
|
||||
continue
|
||||
|
||||
# 处理缺失properties的情况
|
||||
if "properties" not in descriptor:
|
||||
descriptor["properties"] = {}
|
||||
# 从methods中提取所有参数作为properties
|
||||
if "methods" in descriptor:
|
||||
for method_name, method_info in descriptor["methods"].items():
|
||||
if "parameters" in method_info:
|
||||
for param_name, param_info in method_info["parameters"].items():
|
||||
# 将参数信息转换为属性信息
|
||||
descriptor["properties"][param_name] = {
|
||||
"description": param_info["description"],
|
||||
"type": param_info["type"],
|
||||
}
|
||||
|
||||
# 创建IOT设备描述符
|
||||
iot_descriptor = IotDescriptor(
|
||||
descriptor["name"],
|
||||
descriptor["description"],
|
||||
descriptor["properties"],
|
||||
descriptor["methods"],
|
||||
)
|
||||
conn.iot_descriptors[descriptor["name"]] = iot_descriptor
|
||||
|
||||
if conn.load_function_plugin:
|
||||
# 注册或获取设备类型
|
||||
device_type_registry = conn.func_handler.device_type_registry
|
||||
type_id = register_device_type(descriptor, device_type_registry)
|
||||
device_functions = device_type_registry.get_device_functions(type_id)
|
||||
|
||||
# 在连接级注册设备函数
|
||||
if hasattr(conn, "func_handler"):
|
||||
for func_name, func_item in device_functions.items():
|
||||
conn.func_handler.function_registry.register_function(
|
||||
func_name, func_item
|
||||
)
|
||||
conn.logger.bind(tag=TAG).info(
|
||||
f"注册IOT函数到function handler: {func_name}"
|
||||
)
|
||||
functions_changed = True
|
||||
|
||||
# 如果注册了新函数,更新function描述列表
|
||||
if functions_changed and hasattr(conn, "func_handler"):
|
||||
conn.func_handler.upload_functions_desc()
|
||||
func_names = conn.func_handler.current_support_functions()
|
||||
conn.logger.bind(tag=TAG).info(f"设备类型: {type_id}")
|
||||
conn.logger.bind(tag=TAG).info(
|
||||
f"更新function描述列表完成,当前支持的函数: {func_names}"
|
||||
)
|
||||
|
||||
|
||||
async def handleIotStatus(conn, states):
|
||||
"""处理物联网状态"""
|
||||
for state in states:
|
||||
for key, value in conn.iot_descriptors.items():
|
||||
if key == state["name"]:
|
||||
for property_item in value.properties:
|
||||
for k, v in state["state"].items():
|
||||
if property_item["name"] == k:
|
||||
if type(v) != type(property_item["value"]):
|
||||
conn.logger.bind(tag=TAG).error(
|
||||
f"属性{property_item['name']}的值类型不匹配"
|
||||
)
|
||||
break
|
||||
else:
|
||||
property_item["value"] = v
|
||||
conn.logger.bind(tag=TAG).info(
|
||||
f"物联网状态更新: {key} , {property_item['name']} = {v}"
|
||||
)
|
||||
break
|
||||
break
|
||||
|
||||
|
||||
async def get_iot_status(conn, name, property_name):
|
||||
"""获取物联网状态"""
|
||||
for key, value in conn.iot_descriptors.items():
|
||||
if key == name:
|
||||
for property_item in value.properties:
|
||||
if property_item["name"] == property_name:
|
||||
return property_item["value"]
|
||||
conn.logger.bind(tag=TAG).warning(f"未找到设备 {name} 的属性 {property_name}")
|
||||
return None
|
||||
|
||||
|
||||
async def set_iot_status(conn, name, property_name, value):
|
||||
"""设置物联网状态"""
|
||||
for key, iot_descriptor in conn.iot_descriptors.items():
|
||||
if key == name:
|
||||
for property_item in iot_descriptor.properties:
|
||||
if property_item["name"] == property_name:
|
||||
if type(value) != type(property_item["value"]):
|
||||
conn.logger.bind(tag=TAG).error(
|
||||
f"属性{property_item['name']}的值类型不匹配"
|
||||
)
|
||||
return
|
||||
property_item["value"] = value
|
||||
conn.logger.bind(tag=TAG).info(
|
||||
f"物联网状态更新: {name} , {property_name} = {value}"
|
||||
)
|
||||
return
|
||||
conn.logger.bind(tag=TAG).warning(f"未找到设备 {name} 的属性 {property_name}")
|
||||
|
||||
|
||||
async def send_iot_conn(conn, name, method_name, parameters):
|
||||
"""发送物联网指令"""
|
||||
for key, value in conn.iot_descriptors.items():
|
||||
if key == name:
|
||||
# 找到了设备
|
||||
for method in value.methods:
|
||||
# 找到了方法
|
||||
if method["name"] == method_name:
|
||||
# 构建命令对象
|
||||
command = {
|
||||
"name": name,
|
||||
"method": method_name,
|
||||
}
|
||||
|
||||
# 只有当参数不为空时才添加parameters字段
|
||||
if parameters:
|
||||
command["parameters"] = parameters
|
||||
send_message = json.dumps({"type": "iot", "commands": [command]})
|
||||
await conn.websocket.send(send_message)
|
||||
conn.logger.bind(tag=TAG).info(f"发送物联网指令: {send_message}")
|
||||
return
|
||||
conn.logger.bind(tag=TAG).error(f"未找到方法{method_name}")
|
||||
@@ -66,12 +66,11 @@ async def startToChat(conn, text):
|
||||
|
||||
async def no_voice_close_connect(conn, have_voice):
|
||||
if have_voice:
|
||||
conn.client_no_voice_last_time = 0.0
|
||||
conn.last_activity_time = time.time() * 1000
|
||||
return
|
||||
if conn.client_no_voice_last_time == 0.0:
|
||||
conn.client_no_voice_last_time = time.time() * 1000
|
||||
else:
|
||||
no_voice_time = time.time() * 1000 - conn.client_no_voice_last_time
|
||||
# 只有在已经初始化过时间戳的情况下才进行超时检查
|
||||
if conn.last_activity_time > 0.0:
|
||||
no_voice_time = time.time() * 1000 - conn.last_activity_time
|
||||
close_connection_no_voice_time = int(
|
||||
conn.config.get("close_connection_no_voice_time", 120)
|
||||
)
|
||||
|
||||
@@ -92,10 +92,8 @@ async def sendAudio(conn, audios, pre_buffer=True):
|
||||
if conn.client_abort:
|
||||
break
|
||||
|
||||
# 每分钟重置一次计时器
|
||||
if time.perf_counter() - last_reset_time > 60:
|
||||
await conn.reset_timeout()
|
||||
last_reset_time = time.perf_counter()
|
||||
# 重置没有声音的状态
|
||||
conn.last_activity_time = time.time() * 1000
|
||||
|
||||
# 计算预期发送时间
|
||||
expected_time = start_time + (play_position / 1000)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import json
|
||||
from core.handle.abortHandle import handleAbortMessage
|
||||
from core.handle.helloHandle import handleHelloMessage
|
||||
from core.handle.mcpHandle import handle_mcp_message
|
||||
from core.providers.tools.device_mcp import handle_mcp_message
|
||||
from core.utils.util import remove_punctuation_and_length, filter_sensitive_info
|
||||
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.providers.tools.device_iot import handleIotDescriptors, handleIotStatus
|
||||
from core.handle.reportHandle import enqueue_asr_report
|
||||
import asyncio
|
||||
|
||||
@@ -77,7 +77,7 @@ async def handleTextMessage(conn, message):
|
||||
if "states" in msg_json:
|
||||
asyncio.create_task(handleIotStatus(conn, msg_json["states"]))
|
||||
elif msg_json["type"] == "mcp":
|
||||
conn.logger.bind(tag=TAG).info(f"收到mcp消息:{message}")
|
||||
conn.logger.bind(tag=TAG).info(f"收到mcp消息:{message[:100]}")
|
||||
if "payload" in msg_json:
|
||||
asyncio.create_task(
|
||||
handle_mcp_message(conn, conn.mcp_client, msg_json["payload"])
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
"""MCP服务管理器"""
|
||||
|
||||
import asyncio
|
||||
import os, json
|
||||
from typing import Dict, Any, List
|
||||
from .MCPClient import MCPClient
|
||||
from plugins_func.register import register_function, ToolType
|
||||
from config.config_loader import get_project_dir
|
||||
|
||||
TAG = __name__
|
||||
|
||||
|
||||
class MCPManager:
|
||||
"""管理多个MCP服务的集中管理器"""
|
||||
|
||||
def __init__(self, conn) -> None:
|
||||
"""
|
||||
初始化MCP管理器
|
||||
"""
|
||||
self.conn = conn
|
||||
self.config_path = get_project_dir() + "data/.mcp_server_settings.json"
|
||||
if os.path.exists(self.config_path) == False:
|
||||
self.config_path = ""
|
||||
self.conn.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.conn.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") and not srv_config.get("url"):
|
||||
self.conn.logger.bind(tag=TAG).warning(
|
||||
f"Skipping server {name}: neither command nor url specified"
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
client = MCPClient(srv_config)
|
||||
await client.initialize()
|
||||
self.client[name] = client
|
||||
self.conn.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.conn.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.conn.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:
|
||||
"""依次关闭所有 MCPClient,不让异常阻断整体流程。"""
|
||||
for name, client in list(self.client.items()):
|
||||
try:
|
||||
await asyncio.wait_for(client.cleanup(), timeout=20)
|
||||
self.conn.logger.bind(tag=TAG).info(f"MCP client closed: {name}")
|
||||
except (asyncio.TimeoutError, Exception) as e:
|
||||
self.conn.logger.bind(tag=TAG).error(
|
||||
f"Error closing MCP client {name}: {e}"
|
||||
)
|
||||
self.client.clear()
|
||||
@@ -93,9 +93,7 @@ class ASRProvider(ASRProviderBase):
|
||||
|
||||
# 检查初始化响应
|
||||
if "code" in result and result["code"] != 1000:
|
||||
error_msg = f"ASR服务初始化失败: {result.get('payload_msg', {}).get('message', '未知错误')}"
|
||||
if "payload_msg" in result:
|
||||
error_msg += f"\n详细错误信息: {json.dumps(result['payload_msg'], ensure_ascii=False)}"
|
||||
error_msg = f"ASR服务初始化失败: {result.get('payload_msg', {}).get('error', '未知错误')}"
|
||||
logger.bind(tag=TAG).error(error_msg)
|
||||
raise Exception(error_msg)
|
||||
|
||||
@@ -256,7 +254,6 @@ class ASRProvider(ASRProviderBase):
|
||||
"X-Api-Access-Key": self.access_token,
|
||||
"X-Api-Resource-Id": "volc.bigasr.sauc.duration",
|
||||
"X-Api-Connect-Id": str(uuid.uuid4()),
|
||||
"Host": "openspeech.bytedance.com",
|
||||
}
|
||||
|
||||
def generate_header(
|
||||
@@ -309,9 +306,14 @@ class ASRProvider(ASRProviderBase):
|
||||
|
||||
# 如果是错误响应
|
||||
if message_type == 0x0F: # SERVER_ERROR_RESPONSE
|
||||
code = int.from_bytes(header[4:8], "big", signed=False)
|
||||
error_msg = res[8:].decode("utf-8")
|
||||
return {"code": code, "error": error_msg}
|
||||
code = int.from_bytes(res[4:8], "big", signed=False)
|
||||
msg_length = int.from_bytes(res[8:12], "big", signed=False)
|
||||
error_msg = json.loads(res[12:].decode("utf-8"))
|
||||
return {
|
||||
"code": code,
|
||||
"msg_length": msg_length,
|
||||
"payload_msg": error_msg,
|
||||
}
|
||||
|
||||
# 获取JSON数据(跳过12字节头部)
|
||||
try:
|
||||
|
||||
@@ -76,6 +76,14 @@ class IntentProvider(IntentProviderBase):
|
||||
'返回: {"function_call": {"name": "get_battery_level", "arguments": {"response_success": "当前电池电量为{value}%", "response_failure": "无法获取Battery的当前电量百分比"}}}\n'
|
||||
"```\n"
|
||||
"```\n"
|
||||
"用户: 当前屏幕亮度是多少?\n"
|
||||
'返回: {"function_call": {"name": "self_screen_get_brightness"}}\n'
|
||||
"```\n"
|
||||
"```\n"
|
||||
"用户: 设置屏幕亮度为50%\n"
|
||||
'返回: {"function_call": {"name": "self_screen_set_brightness", "arguments": {"brightness": 50}}}\n'
|
||||
"```\n"
|
||||
"```\n"
|
||||
"用户: 我想结束对话\n"
|
||||
'返回: {"function_call": {"name": "handle_exit_intent", "arguments": {"say_goodbye": "goodbye"}}}\n'
|
||||
"```\n"
|
||||
@@ -87,6 +95,10 @@ class IntentProvider(IntentProviderBase):
|
||||
"1. 只返回JSON格式,不要包含任何其他文字\n"
|
||||
'2. 如果没有找到匹配的函数,返回{"function_call": {"name": "continue_chat"}}\n'
|
||||
"3. 确保返回的JSON格式正确,包含所有必要的字段\n"
|
||||
"特殊说明:\n"
|
||||
"- 当用户单次输入包含多个指令时(如'打开灯并且调高音量')\n"
|
||||
"- 请返回多个function_call组成的JSON数组\n"
|
||||
"- 示例:{'function_calls': [{name:'light_on'}, {name:'volume_up'}]}"
|
||||
)
|
||||
return prompt
|
||||
|
||||
@@ -164,7 +176,11 @@ class IntentProvider(IntentProviderBase):
|
||||
music_file_names = music_config["music_file_names"]
|
||||
prompt_music = f"{self.promot}\n<musicNames>{music_file_names}\n</musicNames>"
|
||||
|
||||
devices = conn.config["plugins"]["home_assistant"].get("devices", [])
|
||||
home_assistant_cfg = conn.config["plugins"].get("home_assistant")
|
||||
if home_assistant_cfg:
|
||||
devices = home_assistant_cfg.get("devices", [])
|
||||
else:
|
||||
devices = []
|
||||
if len(devices) > 0:
|
||||
hass_prompt = "\n下面是我家智能设备列表(位置,设备名,entity_id),可以通过homeassistant控制\n"
|
||||
for device in devices:
|
||||
|
||||
@@ -23,7 +23,9 @@ class LLMProvider(LLMProviderBase):
|
||||
self.bot_id = str(config.get("bot_id"))
|
||||
self.user_id = str(config.get("user_id"))
|
||||
self.session_conversation_map = {} # 存储session_id和conversation_id的映射
|
||||
check_model_key("CozeLLM", self.personal_access_token)
|
||||
model_key_msg = check_model_key("CozeLLM", self.personal_access_token)
|
||||
if model_key_msg:
|
||||
logger.bind(tag=TAG).error(model_key_msg)
|
||||
|
||||
def response(self, session_id, dialogue, **kwargs):
|
||||
coze_api_token = self.personal_access_token
|
||||
|
||||
@@ -15,7 +15,9 @@ class LLMProvider(LLMProviderBase):
|
||||
self.mode = config.get("mode", "chat-messages")
|
||||
self.base_url = config.get("base_url", "https://api.dify.ai/v1").rstrip("/")
|
||||
self.session_conversation_map = {} # 存储session_id和conversation_id的映射
|
||||
check_model_key("DifyLLM", self.api_key)
|
||||
model_key_msg = check_model_key("DifyLLM", self.api_key)
|
||||
if model_key_msg:
|
||||
logger.bind(tag=TAG).error(model_key_msg)
|
||||
|
||||
def response(self, session_id, dialogue, **kwargs):
|
||||
try:
|
||||
|
||||
@@ -14,7 +14,9 @@ class LLMProvider(LLMProviderBase):
|
||||
self.base_url = config.get("base_url")
|
||||
self.detail = config.get("detail", False)
|
||||
self.variables = config.get("variables", {})
|
||||
check_model_key("FastGPTLLM", self.api_key)
|
||||
model_key_msg = check_model_key("FastGPTLLM", self.api_key)
|
||||
if model_key_msg:
|
||||
logger.bind(tag=TAG).error(model_key_msg)
|
||||
|
||||
def response(self, session_id, dialogue, **kwargs):
|
||||
try:
|
||||
|
||||
@@ -73,8 +73,9 @@ class LLMProvider(LLMProviderBase):
|
||||
http_proxy = cfg.get("http_proxy")
|
||||
https_proxy = cfg.get("https_proxy")
|
||||
|
||||
if not check_model_key("LLM", self.api_key):
|
||||
raise ValueError("无效的Gemini API Key,请检查是否配置正确")
|
||||
model_key_msg = check_model_key("LLM", self.api_key)
|
||||
if model_key_msg:
|
||||
log.bind(tag=TAG).error(model_key_msg)
|
||||
|
||||
if http_proxy or https_proxy:
|
||||
log.bind(tag=TAG).info(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import httpx
|
||||
import openai
|
||||
from openai.types import CompletionUsage
|
||||
from config.logger import setup_logging
|
||||
@@ -16,26 +17,36 @@ class LLMProvider(LLMProviderBase):
|
||||
self.base_url = config.get("base_url")
|
||||
else:
|
||||
self.base_url = config.get("url")
|
||||
# 增加timeout的配置项,单位为秒
|
||||
timeout = config.get("timeout", 300)
|
||||
self.timeout = int(timeout) if timeout else 300
|
||||
|
||||
param_defaults = {
|
||||
"max_tokens": (500, int),
|
||||
"temperature": (0.7, lambda x: round(float(x), 1)),
|
||||
"top_p": (1.0, lambda x: round(float(x), 1)),
|
||||
"frequency_penalty": (0, lambda x: round(float(x), 1))
|
||||
"frequency_penalty": (0, lambda x: round(float(x), 1)),
|
||||
}
|
||||
|
||||
for param, (default, converter) in param_defaults.items():
|
||||
value = config.get(param)
|
||||
try:
|
||||
setattr(self, param, converter(value) if value not in (None, "") else default)
|
||||
setattr(
|
||||
self,
|
||||
param,
|
||||
converter(value) if value not in (None, "") else default,
|
||||
)
|
||||
except (ValueError, TypeError):
|
||||
setattr(self, param, default)
|
||||
|
||||
logger.debug(
|
||||
f"意图识别参数初始化: {self.temperature}, {self.max_tokens}, {self.top_p}, {self.frequency_penalty}")
|
||||
f"意图识别参数初始化: {self.temperature}, {self.max_tokens}, {self.top_p}, {self.frequency_penalty}"
|
||||
)
|
||||
|
||||
check_model_key("LLM", self.api_key)
|
||||
self.client = openai.OpenAI(api_key=self.api_key, base_url=self.base_url)
|
||||
model_key_msg = check_model_key("LLM", self.api_key)
|
||||
if model_key_msg:
|
||||
logger.bind(tag=TAG).error(model_key_msg)
|
||||
self.client = openai.OpenAI(api_key=self.api_key, base_url=self.base_url, timeout=httpx.Timeout(self.timeout))
|
||||
|
||||
def response(self, session_id, dialogue, **kwargs):
|
||||
try:
|
||||
@@ -46,7 +57,9 @@ class LLMProvider(LLMProviderBase):
|
||||
max_tokens=kwargs.get("max_tokens", self.max_tokens),
|
||||
temperature=kwargs.get("temperature", self.temperature),
|
||||
top_p=kwargs.get("top_p", self.top_p),
|
||||
frequency_penalty=kwargs.get("frequency_penalty", self.frequency_penalty),
|
||||
frequency_penalty=kwargs.get(
|
||||
"frequency_penalty", self.frequency_penalty
|
||||
),
|
||||
)
|
||||
|
||||
is_active = True
|
||||
@@ -84,12 +97,14 @@ class LLMProvider(LLMProviderBase):
|
||||
for chunk in stream:
|
||||
# 检查是否存在有效的choice且content不为空
|
||||
if getattr(chunk, "choices", None):
|
||||
yield chunk.choices[0].delta.content, chunk.choices[0].delta.tool_calls
|
||||
yield chunk.choices[0].delta.content, chunk.choices[
|
||||
0
|
||||
].delta.tool_calls
|
||||
# 存在 CompletionUsage 消息时,生成 Token 消耗 log
|
||||
elif isinstance(getattr(chunk, 'usage', None), CompletionUsage):
|
||||
usage_info = getattr(chunk, 'usage', None)
|
||||
elif isinstance(getattr(chunk, "usage", None), CompletionUsage):
|
||||
usage_info = getattr(chunk, "usage", None)
|
||||
logger.bind(tag=TAG).info(
|
||||
f"Token 消耗:输入 {getattr(usage_info, 'prompt_tokens', '未知')},"
|
||||
f"Token 消耗:输入 {getattr(usage_info, 'prompt_tokens', '未知')},"
|
||||
f"输出 {getattr(usage_info, 'completion_tokens', '未知')},"
|
||||
f"共计 {getattr(usage_info, 'total_tokens', '未知')}"
|
||||
)
|
||||
|
||||
@@ -12,10 +12,6 @@ class MemoryProviderBase(ABC):
|
||||
|
||||
def set_llm(self, llm):
|
||||
self.llm = llm
|
||||
# 获取模型名称和类型信息
|
||||
model_name = getattr(llm, "model_name", str(llm.__class__.__name__))
|
||||
# 记录更详细的日志
|
||||
logger.bind(tag=TAG).info(f"记忆总结设置LLM: {model_name}")
|
||||
|
||||
@abstractmethod
|
||||
async def save_memory(self, msgs):
|
||||
|
||||
@@ -12,12 +12,14 @@ class MemoryProvider(MemoryProviderBase):
|
||||
super().__init__(config)
|
||||
self.api_key = config.get("api_key", "")
|
||||
self.api_version = config.get("api_version", "v1.1")
|
||||
have_key = check_model_key("Mem0ai", self.api_key)
|
||||
if not have_key:
|
||||
model_key_msg = check_model_key("Mem0ai", self.api_key)
|
||||
if model_key_msg:
|
||||
logger.bind(tag=TAG).error(model_key_msg)
|
||||
self.use_mem0 = False
|
||||
return
|
||||
else:
|
||||
self.use_mem0 = True
|
||||
|
||||
try:
|
||||
self.client = MemoryClient(api_key=self.api_key)
|
||||
logger.bind(tag=TAG).info("成功连接到 Mem0ai 服务")
|
||||
|
||||
@@ -5,6 +5,7 @@ import os
|
||||
import yaml
|
||||
from config.config_loader import get_project_dir
|
||||
from config.manage_api_client import save_mem_local_short
|
||||
from core.utils.util import check_model_key
|
||||
|
||||
|
||||
short_term_memory_prompt = """
|
||||
@@ -145,6 +146,10 @@ class MemoryProvider(MemoryProviderBase):
|
||||
# 打印使用的模型信息
|
||||
model_info = getattr(self.llm, "model_name", str(self.llm.__class__.__name__))
|
||||
logger.bind(tag=TAG).debug(f"使用记忆保存模型: {model_info}")
|
||||
api_key = getattr(self.llm, "api_key", None)
|
||||
memory_key_msg = check_model_key("记忆总结专用LLM", api_key)
|
||||
if memory_key_msg:
|
||||
logger.bind(tag=TAG).error(memory_key_msg)
|
||||
if self.llm is None:
|
||||
logger.bind(tag=TAG).error("LLM is not set for memory provider")
|
||||
return None
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
"""基础工具定义模块"""
|
||||
|
||||
from .tool_types import ToolType, ToolDefinition
|
||||
from .tool_executor import ToolExecutor
|
||||
|
||||
__all__ = ["ToolType", "ToolDefinition", "ToolExecutor"]
|
||||
@@ -0,0 +1,27 @@
|
||||
"""工具执行器基类定义"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Any
|
||||
from .tool_types import ToolDefinition
|
||||
from plugins_func.register import ActionResponse
|
||||
|
||||
|
||||
class ToolExecutor(ABC):
|
||||
"""工具执行器抽象基类"""
|
||||
|
||||
@abstractmethod
|
||||
async def execute(
|
||||
self, conn, tool_name: str, arguments: Dict[str, Any]
|
||||
) -> ActionResponse:
|
||||
"""执行工具调用"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_tools(self) -> Dict[str, ToolDefinition]:
|
||||
"""获取该执行器管理的所有工具"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def has_tool(self, tool_name: str) -> bool:
|
||||
"""检查是否有指定工具"""
|
||||
pass
|
||||
@@ -0,0 +1,27 @@
|
||||
"""工具系统的类型定义"""
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional
|
||||
from plugins_func.register import Action
|
||||
|
||||
|
||||
class ToolType(Enum):
|
||||
"""工具类型枚举"""
|
||||
|
||||
SERVER_PLUGIN = "server_plugin" # 服务端插件
|
||||
SERVER_MCP = "server_mcp" # 服务端MCP
|
||||
DEVICE_IOT = "device_iot" # 设备端IoT
|
||||
DEVICE_MCP = "device_mcp" # 设备端MCP
|
||||
MCP_ENDPOINT = "mcp_endpoint" # MCP接入点
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolDefinition:
|
||||
"""工具定义"""
|
||||
|
||||
name: str # 工具名称
|
||||
description: Dict[str, Any] # 工具描述(OpenAI函数调用格式)
|
||||
tool_type: ToolType # 工具类型
|
||||
parameters: Optional[Dict[str, Any]] = None # 额外参数
|
||||
@@ -0,0 +1,12 @@
|
||||
"""设备端IoT工具模块"""
|
||||
|
||||
from .iot_descriptor import IotDescriptor
|
||||
from .iot_handler import handleIotDescriptors, handleIotStatus
|
||||
from .iot_executor import DeviceIoTExecutor
|
||||
|
||||
__all__ = [
|
||||
"IotDescriptor",
|
||||
"handleIotDescriptors",
|
||||
"handleIotStatus",
|
||||
"DeviceIoTExecutor",
|
||||
]
|
||||
@@ -0,0 +1,46 @@
|
||||
"""IoT设备描述符定义"""
|
||||
|
||||
from config.logger import setup_logging
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
class IotDescriptor:
|
||||
"""IoT设备描述符"""
|
||||
|
||||
def __init__(self, name, description, properties, methods):
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.properties = []
|
||||
self.methods = []
|
||||
|
||||
# 根据描述创建属性
|
||||
if properties is not None:
|
||||
for key, value in properties.items():
|
||||
property_item = {}
|
||||
property_item["name"] = key
|
||||
property_item["description"] = value["description"]
|
||||
if value["type"] == "number":
|
||||
property_item["value"] = 0
|
||||
elif value["type"] == "boolean":
|
||||
property_item["value"] = False
|
||||
else:
|
||||
property_item["value"] = ""
|
||||
self.properties.append(property_item)
|
||||
|
||||
# 根据描述创建方法
|
||||
if methods is not None:
|
||||
for key, value in methods.items():
|
||||
method = {}
|
||||
method["description"] = value["description"]
|
||||
method["name"] = key
|
||||
# 检查方法是否有参数
|
||||
if "parameters" in value:
|
||||
method["parameters"] = {}
|
||||
for k, v in value["parameters"].items():
|
||||
method["parameters"][k] = {
|
||||
"description": v["description"],
|
||||
"type": v["type"],
|
||||
}
|
||||
self.methods.append(method)
|
||||
@@ -0,0 +1,238 @@
|
||||
"""设备端IoT工具执行器"""
|
||||
|
||||
import json
|
||||
import asyncio
|
||||
from typing import Dict, Any
|
||||
from ..base import ToolType, ToolDefinition, ToolExecutor
|
||||
from plugins_func.register import Action, ActionResponse
|
||||
|
||||
|
||||
class DeviceIoTExecutor(ToolExecutor):
|
||||
"""设备端IoT工具执行器"""
|
||||
|
||||
def __init__(self, conn):
|
||||
self.conn = conn
|
||||
self.iot_tools: Dict[str, ToolDefinition] = {}
|
||||
|
||||
async def execute(
|
||||
self, conn, tool_name: str, arguments: Dict[str, Any]
|
||||
) -> ActionResponse:
|
||||
"""执行设备端IoT工具"""
|
||||
if not self.has_tool(tool_name):
|
||||
return ActionResponse(
|
||||
action=Action.NOTFOUND, response=f"IoT工具 {tool_name} 不存在"
|
||||
)
|
||||
|
||||
try:
|
||||
# 解析工具名称,获取设备名和操作类型
|
||||
if tool_name.startswith("get_"):
|
||||
# 查询操作:get_devicename_property
|
||||
parts = tool_name.split("_", 2)
|
||||
if len(parts) >= 3:
|
||||
device_name = parts[1]
|
||||
property_name = parts[2]
|
||||
|
||||
value = await self._get_iot_status(device_name, property_name)
|
||||
if value is not None:
|
||||
# 处理响应模板
|
||||
response_success = arguments.get(
|
||||
"response_success", "查询成功:{value}"
|
||||
)
|
||||
response = response_success.replace("{value}", str(value))
|
||||
|
||||
return ActionResponse(
|
||||
action=Action.RESPONSE,
|
||||
response=response,
|
||||
)
|
||||
else:
|
||||
response_failure = arguments.get(
|
||||
"response_failure", f"无法获取{device_name}的状态"
|
||||
)
|
||||
return ActionResponse(
|
||||
action=Action.ERROR, response=response_failure
|
||||
)
|
||||
else:
|
||||
# 控制操作:devicename_method
|
||||
parts = tool_name.split("_", 1)
|
||||
if len(parts) >= 2:
|
||||
device_name = parts[0]
|
||||
method_name = parts[1]
|
||||
|
||||
# 提取控制参数(排除响应参数)
|
||||
control_params = {
|
||||
k: v
|
||||
for k, v in arguments.items()
|
||||
if k not in ["response_success", "response_failure"]
|
||||
}
|
||||
|
||||
# 发送IoT控制命令
|
||||
await self._send_iot_command(
|
||||
device_name, method_name, control_params
|
||||
)
|
||||
|
||||
# 等待状态更新
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
response_success = arguments.get("response_success", "操作成功")
|
||||
|
||||
# 处理响应中的占位符
|
||||
for param_name, param_value in control_params.items():
|
||||
placeholder = "{" + param_name + "}"
|
||||
if placeholder in response_success:
|
||||
response_success = response_success.replace(
|
||||
placeholder, str(param_value)
|
||||
)
|
||||
if "{value}" in response_success:
|
||||
response_success = response_success.replace(
|
||||
"{value}", str(param_value)
|
||||
)
|
||||
break
|
||||
|
||||
return ActionResponse(
|
||||
action=Action.REQLLM,
|
||||
result=response_success,
|
||||
)
|
||||
|
||||
return ActionResponse(action=Action.ERROR, response="无法解析IoT工具名称")
|
||||
|
||||
except Exception as e:
|
||||
response_failure = arguments.get("response_failure", "操作失败")
|
||||
return ActionResponse(action=Action.ERROR, response=response_failure)
|
||||
|
||||
async def _get_iot_status(self, device_name: str, property_name: str):
|
||||
"""获取IoT设备状态"""
|
||||
for key, value in self.conn.iot_descriptors.items():
|
||||
if key.lower() == device_name.lower():
|
||||
for property_item in value.properties:
|
||||
if property_item["name"].lower() == property_name.lower():
|
||||
return property_item["value"]
|
||||
return None
|
||||
|
||||
async def _send_iot_command(
|
||||
self, device_name: str, method_name: str, parameters: Dict[str, Any]
|
||||
):
|
||||
"""发送IoT控制命令"""
|
||||
for key, value in self.conn.iot_descriptors.items():
|
||||
if key.lower() == device_name.lower():
|
||||
for method in value.methods:
|
||||
if method["name"].lower() == method_name.lower():
|
||||
command = {
|
||||
"name": key,
|
||||
"method": method["name"],
|
||||
}
|
||||
|
||||
if parameters:
|
||||
command["parameters"] = parameters
|
||||
|
||||
send_message = json.dumps(
|
||||
{"type": "iot", "commands": [command]}
|
||||
)
|
||||
await self.conn.websocket.send(send_message)
|
||||
return
|
||||
|
||||
raise Exception(f"未找到设备{device_name}的方法{method_name}")
|
||||
|
||||
def register_iot_tools(self, descriptors: list):
|
||||
"""注册IoT工具"""
|
||||
for descriptor in descriptors:
|
||||
device_name = descriptor["name"]
|
||||
device_desc = descriptor["description"]
|
||||
|
||||
# 注册查询工具
|
||||
if "properties" in descriptor:
|
||||
for prop_name, prop_info in descriptor["properties"].items():
|
||||
tool_name = f"get_{device_name.lower()}_{prop_name.lower()}"
|
||||
|
||||
tool_desc = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_name,
|
||||
"description": f"查询{device_desc}的{prop_info['description']}",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"response_success": {
|
||||
"type": "string",
|
||||
"description": f"查询成功时的友好回复,必须使用{{value}}作为占位符表示查询到的值",
|
||||
},
|
||||
"response_failure": {
|
||||
"type": "string",
|
||||
"description": f"查询失败时的友好回复",
|
||||
},
|
||||
},
|
||||
"required": ["response_success", "response_failure"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
self.iot_tools[tool_name] = ToolDefinition(
|
||||
name=tool_name,
|
||||
description=tool_desc,
|
||||
tool_type=ToolType.DEVICE_IOT,
|
||||
)
|
||||
|
||||
# 注册控制工具
|
||||
if "methods" in descriptor:
|
||||
for method_name, method_info in descriptor["methods"].items():
|
||||
tool_name = f"{device_name.lower()}_{method_name.lower()}"
|
||||
|
||||
# 构建参数
|
||||
parameters = {}
|
||||
required_params = []
|
||||
|
||||
# 添加方法的原始参数
|
||||
if "parameters" in method_info:
|
||||
parameters.update(
|
||||
{
|
||||
param_name: {
|
||||
"type": param_info["type"],
|
||||
"description": param_info["description"],
|
||||
}
|
||||
for param_name, param_info in method_info[
|
||||
"parameters"
|
||||
].items()
|
||||
}
|
||||
)
|
||||
required_params.extend(method_info["parameters"].keys())
|
||||
|
||||
# 添加响应参数
|
||||
parameters.update(
|
||||
{
|
||||
"response_success": {
|
||||
"type": "string",
|
||||
"description": "操作成功时的友好回复",
|
||||
},
|
||||
"response_failure": {
|
||||
"type": "string",
|
||||
"description": "操作失败时的友好回复",
|
||||
},
|
||||
}
|
||||
)
|
||||
required_params.extend(["response_success", "response_failure"])
|
||||
|
||||
tool_desc = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_name,
|
||||
"description": f"{device_desc} - {method_info['description']}",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": parameters,
|
||||
"required": required_params,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
self.iot_tools[tool_name] = ToolDefinition(
|
||||
name=tool_name,
|
||||
description=tool_desc,
|
||||
tool_type=ToolType.DEVICE_IOT,
|
||||
)
|
||||
|
||||
def get_tools(self) -> Dict[str, ToolDefinition]:
|
||||
"""获取所有设备端IoT工具"""
|
||||
return self.iot_tools.copy()
|
||||
|
||||
def has_tool(self, tool_name: str) -> bool:
|
||||
"""检查是否有指定的设备端IoT工具"""
|
||||
return tool_name in self.iot_tools
|
||||
@@ -0,0 +1,83 @@
|
||||
"""IoT设备支持模块,提供IoT设备描述符和状态处理"""
|
||||
|
||||
import asyncio
|
||||
from config.logger import setup_logging
|
||||
from .iot_descriptor import IotDescriptor
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
async def handleIotDescriptors(conn, descriptors):
|
||||
"""处理物联网描述"""
|
||||
wait_max_time = 5
|
||||
while (
|
||||
not hasattr(conn, "func_handler")
|
||||
or 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).debug("连接对象没有func_handler")
|
||||
return
|
||||
|
||||
functions_changed = False
|
||||
|
||||
for descriptor in descriptors:
|
||||
# 如果descriptor没有properties和methods,则直接跳过
|
||||
if "properties" not in descriptor and "methods" not in descriptor:
|
||||
continue
|
||||
|
||||
# 处理缺失properties的情况
|
||||
if "properties" not in descriptor:
|
||||
descriptor["properties"] = {}
|
||||
# 从methods中提取所有参数作为properties
|
||||
if "methods" in descriptor:
|
||||
for method_name, method_info in descriptor["methods"].items():
|
||||
if "parameters" in method_info:
|
||||
for param_name, param_info in method_info["parameters"].items():
|
||||
# 将参数信息转换为属性信息
|
||||
descriptor["properties"][param_name] = {
|
||||
"description": param_info["description"],
|
||||
"type": param_info["type"],
|
||||
}
|
||||
|
||||
# 创建IOT设备描述符
|
||||
iot_descriptor = IotDescriptor(
|
||||
descriptor["name"],
|
||||
descriptor["description"],
|
||||
descriptor["properties"],
|
||||
descriptor["methods"],
|
||||
)
|
||||
conn.iot_descriptors[descriptor["name"]] = iot_descriptor
|
||||
functions_changed = True
|
||||
|
||||
# 如果注册了新函数,更新function描述列表
|
||||
if functions_changed and hasattr(conn, "func_handler"):
|
||||
# 注册IoT工具到统一工具处理器
|
||||
await conn.func_handler.register_iot_tools(descriptors)
|
||||
|
||||
conn.func_handler.current_support_functions()
|
||||
|
||||
|
||||
async def handleIotStatus(conn, states):
|
||||
"""处理物联网状态"""
|
||||
for state in states:
|
||||
for key, value in conn.iot_descriptors.items():
|
||||
if key == state["name"]:
|
||||
for property_item in value.properties:
|
||||
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']}的值类型不匹配"
|
||||
)
|
||||
break
|
||||
else:
|
||||
property_item["value"] = v
|
||||
logger.bind(tag=TAG).info(
|
||||
f"物联网状态更新: {key} , {property_item['name']} = {v}"
|
||||
)
|
||||
break
|
||||
break
|
||||
@@ -0,0 +1,21 @@
|
||||
"""设备端MCP工具模块"""
|
||||
|
||||
from .mcp_client import MCPClient
|
||||
from .mcp_handler import (
|
||||
send_mcp_message,
|
||||
handle_mcp_message,
|
||||
send_mcp_initialize_message,
|
||||
send_mcp_tools_list_request,
|
||||
call_mcp_tool,
|
||||
)
|
||||
from .mcp_executor import DeviceMCPExecutor
|
||||
|
||||
__all__ = [
|
||||
"MCPClient",
|
||||
"send_mcp_message",
|
||||
"handle_mcp_message",
|
||||
"send_mcp_initialize_message",
|
||||
"send_mcp_tools_list_request",
|
||||
"call_mcp_tool",
|
||||
"DeviceMCPExecutor",
|
||||
]
|
||||
@@ -0,0 +1,93 @@
|
||||
"""设备端MCP客户端定义"""
|
||||
|
||||
import asyncio
|
||||
from concurrent.futures import Future
|
||||
from core.utils.util import sanitize_tool_name
|
||||
from config.logger import setup_logging
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
class MCPClient:
|
||||
"""设备端MCP客户端,用于管理MCP状态和工具"""
|
||||
|
||||
def __init__(self):
|
||||
self.tools = {} # sanitized_name -> tool_data
|
||||
self.name_mapping = {}
|
||||
self.ready = False
|
||||
self.call_results = {} # To store Futures for tool call responses
|
||||
self.next_id = 1
|
||||
self.lock = asyncio.Lock()
|
||||
self._cached_available_tools = None # Cache for get_available_tools
|
||||
|
||||
def has_tool(self, name: str) -> bool:
|
||||
return name in self.tools
|
||||
|
||||
def get_available_tools(self) -> list:
|
||||
# Check if the cache is valid
|
||||
if self._cached_available_tools is not None:
|
||||
return self._cached_available_tools
|
||||
|
||||
# If cache is not valid, regenerate the list
|
||||
result = []
|
||||
for tool_name, tool_data in self.tools.items():
|
||||
function_def = {
|
||||
"name": tool_name,
|
||||
"description": tool_data["description"],
|
||||
"parameters": {
|
||||
"type": tool_data["inputSchema"].get("type", "object"),
|
||||
"properties": tool_data["inputSchema"].get("properties", {}),
|
||||
"required": tool_data["inputSchema"].get("required", []),
|
||||
},
|
||||
}
|
||||
result.append({"type": "function", "function": function_def})
|
||||
|
||||
self._cached_available_tools = result # Store the generated list in cache
|
||||
return result
|
||||
|
||||
async def is_ready(self) -> bool:
|
||||
async with self.lock:
|
||||
return self.ready
|
||||
|
||||
async def set_ready(self, status: bool):
|
||||
async with self.lock:
|
||||
self.ready = status
|
||||
|
||||
async def add_tool(self, tool_data: dict):
|
||||
async with self.lock:
|
||||
sanitized_name = sanitize_tool_name(tool_data["name"])
|
||||
self.tools[sanitized_name] = tool_data
|
||||
self.name_mapping[sanitized_name] = tool_data["name"]
|
||||
self._cached_available_tools = (
|
||||
None # Invalidate the cache when a tool is added
|
||||
)
|
||||
|
||||
async def get_next_id(self) -> int:
|
||||
async with self.lock:
|
||||
current_id = self.next_id
|
||||
self.next_id += 1
|
||||
return current_id
|
||||
|
||||
async def register_call_result_future(self, id: int, future: Future):
|
||||
async with self.lock:
|
||||
self.call_results[id] = future
|
||||
|
||||
async def resolve_call_result(self, id: int, result: any):
|
||||
async with self.lock:
|
||||
if id in self.call_results:
|
||||
future = self.call_results.pop(id)
|
||||
if not future.done():
|
||||
future.set_result(result)
|
||||
|
||||
async def reject_call_result(self, id: int, exception: Exception):
|
||||
async with self.lock:
|
||||
if id in self.call_results:
|
||||
future = self.call_results.pop(id)
|
||||
if not future.done():
|
||||
future.set_exception(exception)
|
||||
|
||||
async def cleanup_call_result(self, id: int):
|
||||
async with self.lock:
|
||||
if id in self.call_results:
|
||||
self.call_results.pop(id)
|
||||
@@ -0,0 +1,89 @@
|
||||
"""设备端MCP工具执行器"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from ..base import ToolType, ToolDefinition, ToolExecutor
|
||||
from plugins_func.register import Action, ActionResponse
|
||||
from .mcp_handler import call_mcp_tool
|
||||
|
||||
|
||||
class DeviceMCPExecutor(ToolExecutor):
|
||||
"""设备端MCP工具执行器"""
|
||||
|
||||
def __init__(self, conn):
|
||||
self.conn = conn
|
||||
|
||||
async def execute(
|
||||
self, conn, tool_name: str, arguments: Dict[str, Any]
|
||||
) -> ActionResponse:
|
||||
"""执行设备端MCP工具"""
|
||||
if not hasattr(conn, "mcp_client") or not conn.mcp_client:
|
||||
return ActionResponse(
|
||||
action=Action.ERROR,
|
||||
response="设备端MCP客户端未初始化",
|
||||
)
|
||||
|
||||
if not await conn.mcp_client.is_ready():
|
||||
return ActionResponse(
|
||||
action=Action.ERROR,
|
||||
response="设备端MCP客户端未准备就绪",
|
||||
)
|
||||
|
||||
try:
|
||||
# 转换参数为JSON字符串
|
||||
import json
|
||||
|
||||
args_str = json.dumps(arguments) if arguments else "{}"
|
||||
|
||||
# 调用设备端MCP工具
|
||||
result = await call_mcp_tool(conn, conn.mcp_client, tool_name, args_str)
|
||||
|
||||
resultJson = None
|
||||
if isinstance(result, str):
|
||||
try:
|
||||
resultJson = json.loads(result)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
# 视觉大模型不经过二次LLM处理
|
||||
if (
|
||||
resultJson is not None
|
||||
and isinstance(resultJson, dict)
|
||||
and "action" in resultJson
|
||||
):
|
||||
return ActionResponse(
|
||||
action=Action[resultJson["action"]],
|
||||
response=resultJson.get("response", ""),
|
||||
)
|
||||
|
||||
return ActionResponse(action=Action.REQLLM, result=str(result))
|
||||
|
||||
except ValueError as e:
|
||||
return ActionResponse(action=Action.NOTFOUND, response=str(e))
|
||||
except Exception as e:
|
||||
return ActionResponse(action=Action.ERROR, response=str(e))
|
||||
|
||||
def get_tools(self) -> Dict[str, ToolDefinition]:
|
||||
"""获取所有设备端MCP工具"""
|
||||
if not hasattr(self.conn, "mcp_client") or not self.conn.mcp_client:
|
||||
return {}
|
||||
|
||||
tools = {}
|
||||
mcp_tools = self.conn.mcp_client.get_available_tools()
|
||||
|
||||
for tool in mcp_tools:
|
||||
func_def = tool.get("function", {})
|
||||
tool_name = func_def.get("name", "")
|
||||
|
||||
if tool_name:
|
||||
tools[tool_name] = ToolDefinition(
|
||||
name=tool_name, description=tool, tool_type=ToolType.DEVICE_MCP
|
||||
)
|
||||
|
||||
return tools
|
||||
|
||||
def has_tool(self, tool_name: str) -> bool:
|
||||
"""检查是否有指定的设备端MCP工具"""
|
||||
if not hasattr(self.conn, "mcp_client") or not self.conn.mcp_client:
|
||||
return False
|
||||
|
||||
return self.conn.mcp_client.has_tool(tool_name)
|
||||
+33
-32
@@ -1,14 +1,19 @@
|
||||
"""设备端MCP客户端支持模块"""
|
||||
|
||||
import json
|
||||
import asyncio
|
||||
import re
|
||||
from concurrent.futures import Future
|
||||
from core.utils.util import get_vision_url, sanitize_tool_name
|
||||
from core.utils.auth import AuthToken
|
||||
from config.logger import setup_logging
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
class MCPClient:
|
||||
"""MCPClient,用于管理MCP状态和工具"""
|
||||
"""设备端MCP客户端,用于管理MCP状态和工具"""
|
||||
|
||||
def __init__(self):
|
||||
self.tools = {} # sanitized_name -> tool_data
|
||||
@@ -94,24 +99,24 @@ class MCPClient:
|
||||
async def send_mcp_message(conn, payload: dict):
|
||||
"""Helper to send MCP messages, encapsulating common logic."""
|
||||
if not conn.features.get("mcp"):
|
||||
conn.logger.bind(tag=TAG).warning("客户端不支持MCP,无法发送MCP消息")
|
||||
logger.bind(tag=TAG).warning("客户端不支持MCP,无法发送MCP消息")
|
||||
return
|
||||
|
||||
message = json.dumps({"type": "mcp", "payload": payload})
|
||||
|
||||
try:
|
||||
await conn.websocket.send(message)
|
||||
conn.logger.bind(tag=TAG).info(f"成功发送MCP消息: {message}")
|
||||
logger.bind(tag=TAG).info(f"成功发送MCP消息: {message}")
|
||||
except Exception as e:
|
||||
conn.logger.bind(tag=TAG).error(f"发送MCP消息失败: {e}")
|
||||
logger.bind(tag=TAG).error(f"发送MCP消息失败: {e}")
|
||||
|
||||
|
||||
async def handle_mcp_message(conn, mcp_client: MCPClient, payload: dict):
|
||||
"""处理MCP消息,包括初始化、工具列表和工具调用响应等"""
|
||||
conn.logger.bind(tag=TAG).info(f"处理MCP消息: {payload}")
|
||||
logger.bind(tag=TAG).info(f"处理MCP消息: {str(payload)[:100]}")
|
||||
|
||||
if not isinstance(payload, dict):
|
||||
conn.logger.bind(tag=TAG).error("MCP消息缺少payload字段或格式错误")
|
||||
logger.bind(tag=TAG).error("MCP消息缺少payload字段或格式错误")
|
||||
return
|
||||
|
||||
# Handle result
|
||||
@@ -121,32 +126,32 @@ async def handle_mcp_message(conn, mcp_client: MCPClient, payload: dict):
|
||||
|
||||
# Check for tool call response first
|
||||
if msg_id in mcp_client.call_results:
|
||||
conn.logger.bind(tag=TAG).debug(
|
||||
logger.bind(tag=TAG).debug(
|
||||
f"收到工具调用响应,ID: {msg_id}, 结果: {result}"
|
||||
)
|
||||
await mcp_client.resolve_call_result(msg_id, result)
|
||||
return
|
||||
|
||||
if msg_id == 1: # mcpInitializeID
|
||||
conn.logger.bind(tag=TAG).debug("收到MCP初始化响应")
|
||||
logger.bind(tag=TAG).debug("收到MCP初始化响应")
|
||||
server_info = result.get("serverInfo")
|
||||
if isinstance(server_info, dict):
|
||||
name = server_info.get("name")
|
||||
version = server_info.get("version")
|
||||
conn.logger.bind(tag=TAG).info(
|
||||
logger.bind(tag=TAG).info(
|
||||
f"客户端MCP服务器信息: name={name}, version={version}"
|
||||
)
|
||||
return
|
||||
|
||||
elif msg_id == 2: # mcpToolsListID
|
||||
conn.logger.bind(tag=TAG).debug("收到MCP工具列表响应")
|
||||
logger.bind(tag=TAG).debug("收到MCP工具列表响应")
|
||||
if isinstance(result, dict) and "tools" in result:
|
||||
tools_data = result["tools"]
|
||||
if not isinstance(tools_data, list):
|
||||
conn.logger.bind(tag=TAG).error("工具列表格式错误")
|
||||
logger.bind(tag=TAG).error("工具列表格式错误")
|
||||
return
|
||||
|
||||
conn.logger.bind(tag=TAG).info(
|
||||
logger.bind(tag=TAG).info(
|
||||
f"客户端设备支持的工具数量: {len(tools_data)}"
|
||||
)
|
||||
|
||||
@@ -172,7 +177,7 @@ async def handle_mcp_message(conn, mcp_client: MCPClient, payload: dict):
|
||||
"inputSchema": input_schema,
|
||||
}
|
||||
await mcp_client.add_tool(new_tool)
|
||||
conn.logger.bind(tag=TAG).debug(f"客户端工具 #{i+1}: {name}")
|
||||
logger.bind(tag=TAG).debug(f"客户端工具 #{i+1}: {name}")
|
||||
|
||||
# 替换所有工具描述中的工具名称
|
||||
for tool_data in mcp_client.tools.values():
|
||||
@@ -190,24 +195,27 @@ async def handle_mcp_message(conn, mcp_client: MCPClient, payload: dict):
|
||||
|
||||
next_cursor = result.get("nextCursor", "")
|
||||
if next_cursor:
|
||||
conn.logger.bind(tag=TAG).info(
|
||||
f"有更多工具,nextCursor: {next_cursor}"
|
||||
)
|
||||
logger.bind(tag=TAG).info(f"有更多工具,nextCursor: {next_cursor}")
|
||||
await send_mcp_tools_list_continue_request(conn, next_cursor)
|
||||
else:
|
||||
await mcp_client.set_ready(True)
|
||||
conn.logger.bind(tag=TAG).info("所有工具已获取,MCP客户端准备就绪")
|
||||
logger.bind(tag=TAG).info("所有工具已获取,MCP客户端准备就绪")
|
||||
|
||||
# 刷新工具缓存,确保MCP工具被包含在函数列表中
|
||||
if hasattr(conn, "func_handler") and conn.func_handler:
|
||||
conn.func_handler.tool_manager.refresh_tools()
|
||||
conn.func_handler.current_support_functions()
|
||||
return
|
||||
|
||||
# Handle method calls (requests from the client)
|
||||
elif "method" in payload:
|
||||
method = payload["method"]
|
||||
conn.logger.bind(tag=TAG).info(f"收到MCP客户端请求: {method}")
|
||||
logger.bind(tag=TAG).info(f"收到MCP客户端请求: {method}")
|
||||
|
||||
elif "error" in payload:
|
||||
error_data = payload["error"]
|
||||
error_msg = error_data.get("message", "未知错误")
|
||||
conn.logger.bind(tag=TAG).error(f"收到MCP错误响应: {error_msg}")
|
||||
logger.bind(tag=TAG).error(f"收到MCP错误响应: {error_msg}")
|
||||
|
||||
msg_id = int(payload.get("id", 0))
|
||||
if msg_id in mcp_client.call_results:
|
||||
@@ -216,9 +224,6 @@ async def handle_mcp_message(conn, mcp_client: MCPClient, payload: dict):
|
||||
)
|
||||
|
||||
|
||||
# --- Outgoing MCP Messages ---
|
||||
|
||||
|
||||
async def send_mcp_initialize_message(conn):
|
||||
"""发送MCP初始化消息"""
|
||||
|
||||
@@ -250,7 +255,7 @@ async def send_mcp_initialize_message(conn):
|
||||
},
|
||||
},
|
||||
}
|
||||
conn.logger.bind(tag=TAG).info("发送MCP初始化消息")
|
||||
logger.bind(tag=TAG).info("发送MCP初始化消息")
|
||||
await send_mcp_message(conn, payload)
|
||||
|
||||
|
||||
@@ -261,7 +266,7 @@ async def send_mcp_tools_list_request(conn):
|
||||
"id": 2, # mcpToolsListID
|
||||
"method": "tools/list",
|
||||
}
|
||||
conn.logger.bind(tag=TAG).debug("发送MCP工具列表请求")
|
||||
logger.bind(tag=TAG).debug("发送MCP工具列表请求")
|
||||
await send_mcp_message(conn, payload)
|
||||
|
||||
|
||||
@@ -273,7 +278,7 @@ async def send_mcp_tools_list_continue_request(conn, cursor: str):
|
||||
"method": "tools/list",
|
||||
"params": {"cursor": cursor},
|
||||
}
|
||||
conn.logger.bind(tag=TAG).info(f"发送带cursor的MCP工具列表请求: {cursor}")
|
||||
logger.bind(tag=TAG).info(f"发送带cursor的MCP工具列表请求: {cursor}")
|
||||
await send_mcp_message(conn, payload)
|
||||
|
||||
|
||||
@@ -307,8 +312,6 @@ async def call_mcp_tool(
|
||||
# 如果解析失败,尝试合并多个JSON对象
|
||||
try:
|
||||
# 使用正则表达式匹配所有JSON对象
|
||||
import re
|
||||
|
||||
json_objects = re.findall(r"\{[^{}]*\}", args)
|
||||
if len(json_objects) > 1:
|
||||
# 合并所有JSON对象
|
||||
@@ -327,7 +330,7 @@ async def call_mcp_tool(
|
||||
else:
|
||||
raise ValueError(f"参数JSON解析失败: {args}")
|
||||
except Exception as e:
|
||||
conn.logger.bind(tag=TAG).error(
|
||||
logger.bind(tag=TAG).error(
|
||||
f"参数JSON解析失败: {str(e)}, 原始参数: {args}"
|
||||
)
|
||||
raise ValueError(f"参数JSON解析失败: {str(e)}")
|
||||
@@ -353,15 +356,13 @@ async def call_mcp_tool(
|
||||
"params": {"name": actual_name, "arguments": arguments},
|
||||
}
|
||||
|
||||
conn.logger.bind(tag=TAG).info(
|
||||
f"发送客户端mcp工具调用请求: {actual_name},参数: {args}"
|
||||
)
|
||||
logger.bind(tag=TAG).info(f"发送客户端mcp工具调用请求: {actual_name},参数: {args}")
|
||||
await send_mcp_message(conn, payload)
|
||||
|
||||
try:
|
||||
# Wait for response or timeout
|
||||
raw_result = await asyncio.wait_for(result_future, timeout=timeout)
|
||||
conn.logger.bind(tag=TAG).info(
|
||||
logger.bind(tag=TAG).info(
|
||||
f"客户端mcp工具调用 {actual_name} 成功,原始结果: {raw_result}"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
"""MCP接入点工具模块"""
|
||||
|
||||
from .mcp_endpoint_executor import MCPEndpointExecutor
|
||||
from .mcp_endpoint_client import MCPEndpointClient
|
||||
from .mcp_endpoint_handler import (
|
||||
connect_mcp_endpoint,
|
||||
send_mcp_endpoint_initialize,
|
||||
send_mcp_endpoint_notification,
|
||||
send_mcp_endpoint_tools_list,
|
||||
call_mcp_endpoint_tool,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"MCPEndpointExecutor",
|
||||
"MCPEndpointClient",
|
||||
"connect_mcp_endpoint",
|
||||
"send_mcp_endpoint_initialize",
|
||||
"send_mcp_endpoint_notification",
|
||||
"send_mcp_endpoint_tools_list",
|
||||
"call_mcp_endpoint_tool",
|
||||
]
|
||||
@@ -0,0 +1,112 @@
|
||||
"""MCP接入点客户端定义"""
|
||||
|
||||
import asyncio
|
||||
from concurrent.futures import Future
|
||||
from core.utils.util import sanitize_tool_name
|
||||
from config.logger import setup_logging
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
class MCPEndpointClient:
|
||||
"""MCP接入点客户端,用于管理MCP接入点状态和工具"""
|
||||
|
||||
def __init__(self, conn=None):
|
||||
self.conn = conn
|
||||
self.tools = {} # sanitized_name -> tool_data
|
||||
self.name_mapping = {}
|
||||
self.ready = False
|
||||
self.call_results = {} # To store Futures for tool call responses
|
||||
self.next_id = 1
|
||||
self.lock = asyncio.Lock()
|
||||
self._cached_available_tools = None # Cache for get_available_tools
|
||||
self.websocket = None # WebSocket连接
|
||||
|
||||
def has_tool(self, name: str) -> bool:
|
||||
return name in self.tools
|
||||
|
||||
def get_available_tools(self) -> list:
|
||||
# Check if the cache is valid
|
||||
if self._cached_available_tools is not None:
|
||||
return self._cached_available_tools
|
||||
|
||||
# If cache is not valid, regenerate the list
|
||||
result = []
|
||||
for tool_name, tool_data in self.tools.items():
|
||||
function_def = {
|
||||
"name": tool_name,
|
||||
"description": tool_data["description"],
|
||||
"parameters": {
|
||||
"type": tool_data["inputSchema"].get("type", "object"),
|
||||
"properties": tool_data["inputSchema"].get("properties", {}),
|
||||
"required": tool_data["inputSchema"].get("required", []),
|
||||
},
|
||||
}
|
||||
result.append({"type": "function", "function": function_def})
|
||||
|
||||
self._cached_available_tools = result # Store the generated list in cache
|
||||
return result
|
||||
|
||||
async def is_ready(self) -> bool:
|
||||
async with self.lock:
|
||||
return self.ready
|
||||
|
||||
async def set_ready(self, status: bool):
|
||||
async with self.lock:
|
||||
self.ready = status
|
||||
|
||||
async def add_tool(self, tool_data: dict):
|
||||
async with self.lock:
|
||||
sanitized_name = sanitize_tool_name(tool_data["name"])
|
||||
self.tools[sanitized_name] = tool_data
|
||||
self.name_mapping[sanitized_name] = tool_data["name"]
|
||||
self._cached_available_tools = (
|
||||
None # Invalidate the cache when a tool is added
|
||||
)
|
||||
|
||||
async def get_next_id(self) -> int:
|
||||
async with self.lock:
|
||||
current_id = self.next_id
|
||||
self.next_id += 1
|
||||
return current_id
|
||||
|
||||
async def register_call_result_future(self, id: int, future: Future):
|
||||
async with self.lock:
|
||||
self.call_results[id] = future
|
||||
|
||||
async def resolve_call_result(self, id: int, result: any):
|
||||
async with self.lock:
|
||||
if id in self.call_results:
|
||||
future = self.call_results.pop(id)
|
||||
if not future.done():
|
||||
future.set_result(result)
|
||||
|
||||
async def reject_call_result(self, id: int, exception: Exception):
|
||||
async with self.lock:
|
||||
if id in self.call_results:
|
||||
future = self.call_results.pop(id)
|
||||
if not future.done():
|
||||
future.set_exception(exception)
|
||||
|
||||
async def cleanup_call_result(self, id: int):
|
||||
async with self.lock:
|
||||
if id in self.call_results:
|
||||
self.call_results.pop(id)
|
||||
|
||||
def set_websocket(self, websocket):
|
||||
"""设置WebSocket连接"""
|
||||
self.websocket = websocket
|
||||
|
||||
async def send_message(self, message: str):
|
||||
"""发送消息到MCP接入点"""
|
||||
if self.websocket:
|
||||
await self.websocket.send(message)
|
||||
else:
|
||||
raise RuntimeError("WebSocket连接未建立")
|
||||
|
||||
async def close(self):
|
||||
"""关闭WebSocket连接"""
|
||||
if self.websocket:
|
||||
await self.websocket.close()
|
||||
self.websocket = None
|
||||
@@ -0,0 +1,97 @@
|
||||
"""MCP接入点工具执行器"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from ..base import ToolType, ToolDefinition, ToolExecutor
|
||||
from plugins_func.register import Action, ActionResponse
|
||||
from .mcp_endpoint_handler import call_mcp_endpoint_tool
|
||||
|
||||
|
||||
class MCPEndpointExecutor(ToolExecutor):
|
||||
"""MCP接入点工具执行器"""
|
||||
|
||||
def __init__(self, conn):
|
||||
self.conn = conn
|
||||
|
||||
async def execute(
|
||||
self, conn, tool_name: str, arguments: Dict[str, Any]
|
||||
) -> ActionResponse:
|
||||
"""执行MCP接入点工具"""
|
||||
if not hasattr(conn, "mcp_endpoint_client") or not conn.mcp_endpoint_client:
|
||||
return ActionResponse(
|
||||
action=Action.ERROR,
|
||||
response="MCP接入点客户端未初始化",
|
||||
)
|
||||
|
||||
if not await conn.mcp_endpoint_client.is_ready():
|
||||
return ActionResponse(
|
||||
action=Action.ERROR,
|
||||
response="MCP接入点客户端未准备就绪",
|
||||
)
|
||||
|
||||
try:
|
||||
# 转换参数为JSON字符串
|
||||
import json
|
||||
|
||||
args_str = json.dumps(arguments) if arguments else "{}"
|
||||
|
||||
# 调用MCP接入点工具
|
||||
result = await call_mcp_endpoint_tool(
|
||||
conn.mcp_endpoint_client, tool_name, args_str
|
||||
)
|
||||
|
||||
resultJson = None
|
||||
if isinstance(result, str):
|
||||
try:
|
||||
resultJson = json.loads(result)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
# 视觉大模型不经过二次LLM处理
|
||||
if (
|
||||
resultJson is not None
|
||||
and isinstance(resultJson, dict)
|
||||
and "action" in resultJson
|
||||
):
|
||||
return ActionResponse(
|
||||
action=Action[resultJson["action"]],
|
||||
response=resultJson.get("response", ""),
|
||||
)
|
||||
|
||||
return ActionResponse(action=Action.REQLLM, result=str(result))
|
||||
|
||||
except ValueError as e:
|
||||
return ActionResponse(action=Action.NOTFOUND, response=str(e))
|
||||
except Exception as e:
|
||||
return ActionResponse(action=Action.ERROR, response=str(e))
|
||||
|
||||
def get_tools(self) -> Dict[str, ToolDefinition]:
|
||||
"""获取所有MCP接入点工具"""
|
||||
if (
|
||||
not hasattr(self.conn, "mcp_endpoint_client")
|
||||
or not self.conn.mcp_endpoint_client
|
||||
):
|
||||
return {}
|
||||
|
||||
tools = {}
|
||||
mcp_tools = self.conn.mcp_endpoint_client.get_available_tools()
|
||||
|
||||
for tool in mcp_tools:
|
||||
func_def = tool.get("function", {})
|
||||
tool_name = func_def.get("name", "")
|
||||
|
||||
if tool_name:
|
||||
tools[tool_name] = ToolDefinition(
|
||||
name=tool_name, description=tool, tool_type=ToolType.MCP_ENDPOINT
|
||||
)
|
||||
|
||||
return tools
|
||||
|
||||
def has_tool(self, tool_name: str) -> bool:
|
||||
"""检查是否有指定的MCP接入点工具"""
|
||||
if (
|
||||
not hasattr(self.conn, "mcp_endpoint_client")
|
||||
or not self.conn.mcp_endpoint_client
|
||||
):
|
||||
return False
|
||||
|
||||
return self.conn.mcp_endpoint_client.has_tool(tool_name)
|
||||
@@ -0,0 +1,390 @@
|
||||
"""MCP接入点处理器"""
|
||||
|
||||
import json
|
||||
import asyncio
|
||||
import re
|
||||
import websockets
|
||||
from config.logger import setup_logging
|
||||
from .mcp_endpoint_client import MCPEndpointClient
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
async def connect_mcp_endpoint(mcp_endpoint_url: str, conn=None) -> MCPEndpointClient:
|
||||
"""连接到MCP接入点"""
|
||||
if not mcp_endpoint_url or "你的" in mcp_endpoint_url or mcp_endpoint_url == "null":
|
||||
return None
|
||||
|
||||
try:
|
||||
websocket = await websockets.connect(mcp_endpoint_url)
|
||||
|
||||
mcp_client = MCPEndpointClient(conn)
|
||||
mcp_client.set_websocket(websocket)
|
||||
|
||||
# 启动消息监听器
|
||||
asyncio.create_task(_message_listener(mcp_client))
|
||||
|
||||
# 发送初始化消息
|
||||
await send_mcp_endpoint_initialize(mcp_client)
|
||||
|
||||
# 发送初始化完成通知
|
||||
await send_mcp_endpoint_notification(mcp_client, "notifications/initialized")
|
||||
|
||||
# 获取工具列表
|
||||
await send_mcp_endpoint_tools_list(mcp_client)
|
||||
|
||||
logger.bind(tag=TAG).info("MCP接入点连接成功")
|
||||
return mcp_client
|
||||
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"连接MCP接入点失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def _message_listener(mcp_client: MCPEndpointClient):
|
||||
"""监听MCP接入点消息"""
|
||||
try:
|
||||
async for message in mcp_client.websocket:
|
||||
await handle_mcp_endpoint_message(mcp_client, message)
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
logger.bind(tag=TAG).info("MCP接入点连接已关闭")
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"MCP接入点消息监听器错误: {e}")
|
||||
finally:
|
||||
await mcp_client.set_ready(False)
|
||||
|
||||
|
||||
async def handle_mcp_endpoint_message(mcp_client: MCPEndpointClient, message: str):
|
||||
"""处理MCP接入点消息"""
|
||||
try:
|
||||
payload = json.loads(message)
|
||||
logger.bind(tag=TAG).debug(f"收到MCP接入点消息: {payload}")
|
||||
|
||||
if not isinstance(payload, dict):
|
||||
logger.bind(tag=TAG).error("MCP接入点消息格式错误")
|
||||
return
|
||||
|
||||
# Handle result
|
||||
if "result" in payload:
|
||||
result = payload["result"]
|
||||
# 安全地获取消息ID,如果为None则使用0
|
||||
msg_id_raw = payload.get("id")
|
||||
msg_id = int(msg_id_raw) if msg_id_raw is not None else 0
|
||||
|
||||
# Check for tool call response first
|
||||
if msg_id in mcp_client.call_results:
|
||||
logger.bind(tag=TAG).debug(
|
||||
f"收到工具调用响应,ID: {msg_id}, 结果: {result}"
|
||||
)
|
||||
await mcp_client.resolve_call_result(msg_id, result)
|
||||
return
|
||||
|
||||
if msg_id == 1: # mcpInitializeID
|
||||
logger.bind(tag=TAG).debug("收到MCP接入点初始化响应")
|
||||
if result is not None and isinstance(result, dict):
|
||||
server_info = result.get("serverInfo")
|
||||
if isinstance(server_info, dict):
|
||||
name = server_info.get("name")
|
||||
version = server_info.get("version")
|
||||
logger.bind(tag=TAG).info(
|
||||
f"MCP接入点服务器信息: name={name}, version={version}"
|
||||
)
|
||||
else:
|
||||
logger.bind(tag=TAG).warning(
|
||||
"MCP接入点初始化响应结果为空或格式错误"
|
||||
)
|
||||
return
|
||||
|
||||
elif msg_id == 2: # mcpToolsListID
|
||||
logger.bind(tag=TAG).debug("收到MCP接入点工具列表响应")
|
||||
if (
|
||||
result is not None
|
||||
and isinstance(result, dict)
|
||||
and "tools" in result
|
||||
):
|
||||
tools_data = result["tools"]
|
||||
if not isinstance(tools_data, list):
|
||||
logger.bind(tag=TAG).error("工具列表格式错误")
|
||||
return
|
||||
|
||||
logger.bind(tag=TAG).info(
|
||||
f"MCP接入点支持的工具数量: {len(tools_data)}"
|
||||
)
|
||||
|
||||
for i, tool in enumerate(tools_data):
|
||||
if not isinstance(tool, dict):
|
||||
continue
|
||||
|
||||
name = tool.get("name", "")
|
||||
description = tool.get("description", "")
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
}
|
||||
|
||||
if "inputSchema" in tool and isinstance(
|
||||
tool["inputSchema"], dict
|
||||
):
|
||||
schema = tool["inputSchema"]
|
||||
input_schema["type"] = schema.get("type", "object")
|
||||
input_schema["properties"] = schema.get("properties", {})
|
||||
input_schema["required"] = [
|
||||
s
|
||||
for s in schema.get("required", [])
|
||||
if isinstance(s, str)
|
||||
]
|
||||
|
||||
new_tool = {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"inputSchema": input_schema,
|
||||
}
|
||||
await mcp_client.add_tool(new_tool)
|
||||
logger.bind(tag=TAG).debug(f"MCP接入点工具 #{i+1}: {name}")
|
||||
|
||||
# 替换所有工具描述中的工具名称
|
||||
for tool_data in mcp_client.tools.values():
|
||||
if "description" in tool_data:
|
||||
description = tool_data["description"]
|
||||
# 遍历所有工具名称进行替换
|
||||
for (
|
||||
sanitized_name,
|
||||
original_name,
|
||||
) in mcp_client.name_mapping.items():
|
||||
description = description.replace(
|
||||
original_name, sanitized_name
|
||||
)
|
||||
tool_data["description"] = description
|
||||
|
||||
next_cursor = (
|
||||
result.get("nextCursor", "") if result is not None else ""
|
||||
)
|
||||
if next_cursor:
|
||||
logger.bind(tag=TAG).info(
|
||||
f"有更多工具,nextCursor: {next_cursor}"
|
||||
)
|
||||
await send_mcp_endpoint_tools_list_continue(
|
||||
mcp_client, next_cursor
|
||||
)
|
||||
else:
|
||||
await mcp_client.set_ready(True)
|
||||
logger.bind(tag=TAG).info(
|
||||
"所有MCP接入点工具已获取,客户端准备就绪"
|
||||
)
|
||||
|
||||
# 刷新工具缓存,确保MCP接入点工具被包含在函数列表中
|
||||
if (
|
||||
hasattr(mcp_client, "conn")
|
||||
and mcp_client.conn
|
||||
and hasattr(mcp_client.conn, "func_handler")
|
||||
and mcp_client.conn.func_handler
|
||||
):
|
||||
mcp_client.conn.func_handler.tool_manager.refresh_tools()
|
||||
mcp_client.conn.func_handler.current_support_functions()
|
||||
|
||||
logger.bind(tag=TAG).info(
|
||||
f"MCP接入点工具获取完成,共 {len(mcp_client.tools)} 个工具"
|
||||
)
|
||||
else:
|
||||
logger.bind(tag=TAG).warning(
|
||||
"MCP接入点工具列表响应结果为空或格式错误"
|
||||
)
|
||||
return
|
||||
|
||||
# Handle method calls (requests from the endpoint)
|
||||
elif "method" in payload:
|
||||
method = payload["method"]
|
||||
logger.bind(tag=TAG).info(f"收到MCP接入点请求: {method}")
|
||||
|
||||
elif "error" in payload:
|
||||
error_data = payload["error"]
|
||||
error_msg = error_data.get("message", "未知错误")
|
||||
logger.bind(tag=TAG).error(f"收到MCP接入点错误响应: {error_msg}")
|
||||
|
||||
# 安全地获取消息ID,如果为None则使用0
|
||||
msg_id_raw = payload.get("id")
|
||||
msg_id = int(msg_id_raw) if msg_id_raw is not None else 0
|
||||
|
||||
if msg_id in mcp_client.call_results:
|
||||
await mcp_client.reject_call_result(
|
||||
msg_id, Exception(f"MCP接入点错误: {error_msg}")
|
||||
)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.bind(tag=TAG).error(f"MCP接入点消息JSON解析失败: {e}")
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"处理MCP接入点消息时出错: {e}")
|
||||
import traceback
|
||||
|
||||
logger.bind(tag=TAG).error(f"错误详情: {traceback.format_exc()}")
|
||||
|
||||
|
||||
async def send_mcp_endpoint_initialize(mcp_client: MCPEndpointClient):
|
||||
"""发送MCP接入点初始化消息"""
|
||||
payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1, # mcpInitializeID
|
||||
"method": "initialize",
|
||||
"params": {
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {
|
||||
"roots": {"listChanged": True},
|
||||
"sampling": {},
|
||||
},
|
||||
"clientInfo": {
|
||||
"name": "XiaozhiMCPEndpointClient",
|
||||
"version": "1.0.0",
|
||||
},
|
||||
},
|
||||
}
|
||||
message = json.dumps(payload)
|
||||
logger.bind(tag=TAG).info("发送MCP接入点初始化消息")
|
||||
await mcp_client.send_message(message)
|
||||
|
||||
|
||||
async def send_mcp_endpoint_notification(mcp_client: MCPEndpointClient, method: str):
|
||||
"""发送MCP接入点通知消息"""
|
||||
payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": method,
|
||||
"params": {},
|
||||
}
|
||||
message = json.dumps(payload)
|
||||
logger.bind(tag=TAG).debug(f"发送MCP接入点通知: {method}")
|
||||
await mcp_client.send_message(message)
|
||||
|
||||
|
||||
async def send_mcp_endpoint_tools_list(mcp_client: MCPEndpointClient):
|
||||
"""发送MCP接入点工具列表请求"""
|
||||
payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2, # mcpToolsListID
|
||||
"method": "tools/list",
|
||||
}
|
||||
message = json.dumps(payload)
|
||||
logger.bind(tag=TAG).debug("发送MCP接入点工具列表请求")
|
||||
await mcp_client.send_message(message)
|
||||
|
||||
|
||||
async def send_mcp_endpoint_tools_list_continue(
|
||||
mcp_client: MCPEndpointClient, cursor: str
|
||||
):
|
||||
"""发送带有cursor的MCP接入点工具列表请求"""
|
||||
payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2, # mcpToolsListID (same ID for continuation)
|
||||
"method": "tools/list",
|
||||
"params": {"cursor": cursor},
|
||||
}
|
||||
message = json.dumps(payload)
|
||||
logger.bind(tag=TAG).info(f"发送带cursor的MCP接入点工具列表请求: {cursor}")
|
||||
await mcp_client.send_message(message)
|
||||
|
||||
|
||||
async def call_mcp_endpoint_tool(
|
||||
mcp_client: MCPEndpointClient, tool_name: str, args: str = "{}", timeout: int = 30
|
||||
):
|
||||
"""
|
||||
调用指定的MCP接入点工具,并等待响应
|
||||
"""
|
||||
if not await mcp_client.is_ready():
|
||||
raise RuntimeError("MCP接入点客户端尚未准备就绪")
|
||||
|
||||
if not mcp_client.has_tool(tool_name):
|
||||
raise ValueError(f"工具 {tool_name} 不存在")
|
||||
|
||||
tool_call_id = await mcp_client.get_next_id()
|
||||
result_future = asyncio.Future()
|
||||
await mcp_client.register_call_result_future(tool_call_id, result_future)
|
||||
|
||||
# 处理参数
|
||||
try:
|
||||
if isinstance(args, str):
|
||||
# 确保字符串是有效的JSON
|
||||
if not args.strip():
|
||||
arguments = {}
|
||||
else:
|
||||
try:
|
||||
# 尝试直接解析
|
||||
arguments = json.loads(args)
|
||||
except json.JSONDecodeError:
|
||||
# 如果解析失败,尝试合并多个JSON对象
|
||||
try:
|
||||
# 使用正则表达式匹配所有JSON对象
|
||||
json_objects = re.findall(r"\{[^{}]*\}", args)
|
||||
if len(json_objects) > 1:
|
||||
# 合并所有JSON对象
|
||||
merged_dict = {}
|
||||
for json_str in json_objects:
|
||||
try:
|
||||
obj = json.loads(json_str)
|
||||
if isinstance(obj, dict):
|
||||
merged_dict.update(obj)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if merged_dict:
|
||||
arguments = merged_dict
|
||||
else:
|
||||
raise ValueError(f"无法解析任何有效的JSON对象: {args}")
|
||||
else:
|
||||
raise ValueError(f"参数JSON解析失败: {args}")
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(
|
||||
f"参数JSON解析失败: {str(e)}, 原始参数: {args}"
|
||||
)
|
||||
raise ValueError(f"参数JSON解析失败: {str(e)}")
|
||||
elif isinstance(args, dict):
|
||||
arguments = args
|
||||
else:
|
||||
raise ValueError(f"参数类型错误,期望字符串或字典,实际类型: {type(args)}")
|
||||
|
||||
# 确保参数是字典类型
|
||||
if not isinstance(arguments, dict):
|
||||
raise ValueError(f"参数必须是字典类型,实际类型: {type(arguments)}")
|
||||
|
||||
except Exception as e:
|
||||
if not isinstance(e, ValueError):
|
||||
raise ValueError(f"参数处理失败: {str(e)}")
|
||||
raise e
|
||||
|
||||
actual_name = mcp_client.name_mapping.get(tool_name, tool_name)
|
||||
payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": tool_call_id,
|
||||
"method": "tools/call",
|
||||
"params": {"name": actual_name, "arguments": arguments},
|
||||
}
|
||||
|
||||
message = json.dumps(payload)
|
||||
logger.bind(tag=TAG).info(f"发送MCP接入点工具调用请求: {actual_name},参数: {args}")
|
||||
await mcp_client.send_message(message)
|
||||
|
||||
try:
|
||||
# Wait for response or timeout
|
||||
raw_result = await asyncio.wait_for(result_future, timeout=timeout)
|
||||
logger.bind(tag=TAG).info(
|
||||
f"MCP接入点工具调用 {actual_name} 成功,原始结果: {raw_result}"
|
||||
)
|
||||
|
||||
if isinstance(raw_result, dict):
|
||||
if raw_result.get("isError") is True:
|
||||
error_msg = raw_result.get(
|
||||
"error", "工具调用返回错误,但未提供具体错误信息"
|
||||
)
|
||||
raise RuntimeError(f"工具调用错误: {error_msg}")
|
||||
|
||||
content = raw_result.get("content")
|
||||
if isinstance(content, list) and len(content) > 0:
|
||||
if isinstance(content[0], dict) and "text" in content[0]:
|
||||
# 直接返回文本内容,不进行JSON解析
|
||||
return content[0]["text"]
|
||||
# 如果结果不是预期的格式,将其转换为字符串
|
||||
return str(raw_result)
|
||||
except asyncio.TimeoutError:
|
||||
await mcp_client.cleanup_call_result(tool_call_id)
|
||||
raise TimeoutError("工具调用请求超时")
|
||||
except Exception as e:
|
||||
await mcp_client.cleanup_call_result(tool_call_id)
|
||||
raise e
|
||||
@@ -0,0 +1,7 @@
|
||||
"""服务端MCP工具模块"""
|
||||
|
||||
from .mcp_manager import ServerMCPManager
|
||||
from .mcp_executor import ServerMCPExecutor
|
||||
from .mcp_client import ServerMCPClient
|
||||
|
||||
__all__ = ["ServerMCPManager", "ServerMCPExecutor", "ServerMCPClient"]
|
||||
+76
-11
@@ -1,7 +1,12 @@
|
||||
"""服务端MCP客户端"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import asyncio, os, shutil, concurrent.futures
|
||||
import asyncio
|
||||
import os
|
||||
import shutil
|
||||
import concurrent.futures
|
||||
from contextlib import AsyncExitStack
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
@@ -14,8 +19,15 @@ from core.utils.util import sanitize_tool_name
|
||||
TAG = __name__
|
||||
|
||||
|
||||
class MCPClient:
|
||||
class ServerMCPClient:
|
||||
"""服务端MCP客户端,用于连接和管理MCP服务"""
|
||||
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
"""初始化服务端MCP客户端
|
||||
|
||||
Args:
|
||||
config: MCP服务配置字典
|
||||
"""
|
||||
self.logger = setup_logging()
|
||||
self.config = config
|
||||
|
||||
@@ -24,21 +36,26 @@ class MCPClient:
|
||||
self._shutdown_evt = asyncio.Event()
|
||||
|
||||
self.session: Optional[ClientSession] = None
|
||||
self.tools: List = [] # original tool objects
|
||||
self.tools: List = [] # 原始工具对象
|
||||
self.tools_dict: Dict[str, Any] = {}
|
||||
self.name_mapping: Dict[str, str] = {}
|
||||
|
||||
async def initialize(self):
|
||||
"""初始化MCP客户端连接"""
|
||||
if self._worker_task:
|
||||
return
|
||||
self._worker_task = asyncio.create_task(self._worker(), name="MCPClientWorker")
|
||||
|
||||
self._worker_task = asyncio.create_task(
|
||||
self._worker(), name="ServerMCPClientWorker"
|
||||
)
|
||||
await self._ready_evt.wait()
|
||||
|
||||
self.logger.bind(tag=TAG).info(
|
||||
f"Connected, tools = {[name for name in self.name_mapping.values()]}"
|
||||
f"服务端MCP客户端已连接,可用工具: {[name for name in self.name_mapping.values()]}"
|
||||
)
|
||||
|
||||
async def cleanup(self):
|
||||
"""清理MCP客户端资源"""
|
||||
if not self._worker_task:
|
||||
return
|
||||
|
||||
@@ -46,14 +63,27 @@ class MCPClient:
|
||||
try:
|
||||
await asyncio.wait_for(self._worker_task, timeout=20)
|
||||
except (asyncio.TimeoutError, Exception) as e:
|
||||
self.logger.bind(tag=TAG).error(f"worker shutdown err: {e}")
|
||||
self.logger.bind(tag=TAG).error(f"服务端MCP客户端关闭错误: {e}")
|
||||
finally:
|
||||
self._worker_task = None
|
||||
|
||||
def has_tool(self, name: str) -> bool:
|
||||
"""检查是否包含指定工具
|
||||
|
||||
Args:
|
||||
name: 工具名称
|
||||
|
||||
Returns:
|
||||
bool: 是否包含该工具
|
||||
"""
|
||||
return name in self.tools_dict
|
||||
|
||||
def get_available_tools(self):
|
||||
def get_available_tools(self) -> List[Dict[str, Any]]:
|
||||
"""获取所有可用工具的定义
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 工具定义列表
|
||||
"""
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
@@ -66,9 +96,21 @@ class MCPClient:
|
||||
for name, tool in self.tools_dict.items()
|
||||
]
|
||||
|
||||
async def call_tool(self, name: str, args: dict):
|
||||
async def call_tool(self, name: str, args: dict) -> Any:
|
||||
"""调用指定工具
|
||||
|
||||
Args:
|
||||
name: 工具名称
|
||||
args: 工具参数
|
||||
|
||||
Returns:
|
||||
Any: 工具执行结果
|
||||
|
||||
Raises:
|
||||
RuntimeError: 客户端未初始化时抛出
|
||||
"""
|
||||
if not self.session:
|
||||
raise RuntimeError("MCPClient not initialized")
|
||||
raise RuntimeError("服务端MCP客户端未初始化")
|
||||
|
||||
real_name = self.name_mapping.get(name, name)
|
||||
loop = self._worker_task.get_loop()
|
||||
@@ -80,7 +122,29 @@ class MCPClient:
|
||||
fut: concurrent.futures.Future = asyncio.run_coroutine_threadsafe(coro, loop)
|
||||
return await asyncio.wrap_future(fut)
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
"""检查MCP客户端是否连接正常
|
||||
|
||||
Returns:
|
||||
bool: 如果客户端已连接并正常工作,返回True,否则返回False
|
||||
"""
|
||||
# 检查工作任务是否存在
|
||||
if self._worker_task is None:
|
||||
return False
|
||||
|
||||
# 检查工作任务是否已经完成或取消
|
||||
if self._worker_task.done():
|
||||
return False
|
||||
|
||||
# 检查会话是否存在
|
||||
if self.session is None:
|
||||
return False
|
||||
|
||||
# 所有检查都通过,连接正常
|
||||
return True
|
||||
|
||||
async def _worker(self):
|
||||
"""MCP客户端工作协程"""
|
||||
async with AsyncExitStack() as stack:
|
||||
try:
|
||||
# 建立 StdioClient
|
||||
@@ -100,6 +164,7 @@ class MCPClient:
|
||||
stdio_client(params)
|
||||
)
|
||||
read_stream, write_stream = stdio_r, stdio_w
|
||||
|
||||
# 建立SSEClient
|
||||
elif "url" in self.config:
|
||||
if "API_ACCESS_TOKEN" in self.config:
|
||||
@@ -114,7 +179,7 @@ class MCPClient:
|
||||
read_stream, write_stream = sse_r, sse_w
|
||||
|
||||
else:
|
||||
raise ValueError("MCPClient config must include 'command' or 'url'")
|
||||
raise ValueError("MCP客户端配置必须包含'command'或'url'")
|
||||
|
||||
self.session = await stack.enter_async_context(
|
||||
ClientSession(
|
||||
@@ -138,6 +203,6 @@ class MCPClient:
|
||||
await self._shutdown_evt.wait()
|
||||
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"worker error: {e}")
|
||||
self.logger.bind(tag=TAG).error(f"服务端MCP客户端工作协程错误: {e}")
|
||||
self._ready_evt.set()
|
||||
raise
|
||||
@@ -0,0 +1,89 @@
|
||||
"""服务端MCP工具执行器"""
|
||||
|
||||
from typing import Dict, Any, Optional
|
||||
from ..base import ToolType, ToolDefinition, ToolExecutor
|
||||
from plugins_func.register import Action, ActionResponse
|
||||
from .mcp_manager import ServerMCPManager
|
||||
|
||||
|
||||
class ServerMCPExecutor(ToolExecutor):
|
||||
"""服务端MCP工具执行器"""
|
||||
|
||||
def __init__(self, conn):
|
||||
self.conn = conn
|
||||
self.mcp_manager: Optional[ServerMCPManager] = None
|
||||
self._initialized = False
|
||||
|
||||
async def initialize(self):
|
||||
"""初始化MCP管理器"""
|
||||
if not self._initialized:
|
||||
self.mcp_manager = ServerMCPManager(self.conn)
|
||||
await self.mcp_manager.initialize_servers()
|
||||
self._initialized = True
|
||||
|
||||
async def execute(
|
||||
self, conn, tool_name: str, arguments: Dict[str, Any]
|
||||
) -> ActionResponse:
|
||||
"""执行服务端MCP工具"""
|
||||
if not self._initialized or not self.mcp_manager:
|
||||
return ActionResponse(
|
||||
action=Action.ERROR,
|
||||
response="MCP管理器未初始化",
|
||||
)
|
||||
|
||||
try:
|
||||
# 移除mcp_前缀(如果有)
|
||||
actual_tool_name = tool_name
|
||||
if tool_name.startswith("mcp_"):
|
||||
actual_tool_name = tool_name[4:]
|
||||
|
||||
result = await self.mcp_manager.execute_tool(actual_tool_name, arguments)
|
||||
|
||||
return ActionResponse(action=Action.REQLLM, result=str(result))
|
||||
|
||||
except ValueError as e:
|
||||
return ActionResponse(
|
||||
action=Action.NOTFOUND,
|
||||
response=str(e),
|
||||
)
|
||||
except Exception as e:
|
||||
return ActionResponse(
|
||||
action=Action.ERROR,
|
||||
response=str(e),
|
||||
)
|
||||
|
||||
def get_tools(self) -> Dict[str, ToolDefinition]:
|
||||
"""获取所有服务端MCP工具"""
|
||||
if not self._initialized or not self.mcp_manager:
|
||||
return {}
|
||||
|
||||
tools = {}
|
||||
mcp_tools = self.mcp_manager.get_all_tools()
|
||||
|
||||
for tool in mcp_tools:
|
||||
func_def = tool.get("function", {})
|
||||
tool_name = func_def.get("name", "")
|
||||
if tool_name == "":
|
||||
continue
|
||||
tools[tool_name] = ToolDefinition(
|
||||
name=tool_name, description=tool, tool_type=ToolType.SERVER_MCP
|
||||
)
|
||||
|
||||
return tools
|
||||
|
||||
def has_tool(self, tool_name: str) -> bool:
|
||||
"""检查是否有指定的服务端MCP工具"""
|
||||
if not self._initialized or not self.mcp_manager:
|
||||
return False
|
||||
|
||||
# 移除mcp_前缀(如果有)
|
||||
actual_tool_name = tool_name
|
||||
if tool_name.startswith("mcp_"):
|
||||
actual_tool_name = tool_name[4:]
|
||||
|
||||
return self.mcp_manager.is_mcp_tool(actual_tool_name)
|
||||
|
||||
async def cleanup(self):
|
||||
"""清理MCP连接"""
|
||||
if self.mcp_manager:
|
||||
await self.mcp_manager.cleanup_all()
|
||||
@@ -0,0 +1,158 @@
|
||||
"""服务端MCP管理器"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import json
|
||||
from typing import Dict, Any, List
|
||||
from config.config_loader import get_project_dir
|
||||
from config.logger import setup_logging
|
||||
from .mcp_client import ServerMCPClient
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
class ServerMCPManager:
|
||||
"""管理多个服务端MCP服务的集中管理器"""
|
||||
|
||||
def __init__(self, conn) -> None:
|
||||
"""初始化MCP管理器"""
|
||||
self.conn = conn
|
||||
self.config_path = get_project_dir() + "data/.mcp_server_settings.json"
|
||||
if not os.path.exists(self.config_path):
|
||||
self.config_path = ""
|
||||
logger.bind(tag=TAG).warning(
|
||||
f"请检查mcp服务配置文件:data/.mcp_server_settings.json"
|
||||
)
|
||||
self.clients: Dict[str, ServerMCPClient] = {}
|
||||
self.tools = []
|
||||
|
||||
def load_config(self) -> Dict[str, Any]:
|
||||
"""加载MCP服务配置"""
|
||||
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:
|
||||
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") and not srv_config.get("url"):
|
||||
logger.bind(tag=TAG).warning(
|
||||
f"Skipping server {name}: neither command nor url specified"
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
# 初始化服务端MCP客户端
|
||||
logger.bind(tag=TAG).info(f"初始化服务端MCP客户端: {name}")
|
||||
client = ServerMCPClient(srv_config)
|
||||
await client.initialize()
|
||||
self.clients[name] = client
|
||||
client_tools = client.get_available_tools()
|
||||
self.tools.extend(client_tools)
|
||||
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(
|
||||
f"Failed to initialize MCP server {name}: {e}"
|
||||
)
|
||||
|
||||
# 输出当前支持的服务端MCP工具列表
|
||||
if hasattr(self.conn, "func_handler") and self.conn.func_handler:
|
||||
self.conn.func_handler.current_support_functions()
|
||||
|
||||
def get_all_tools(self) -> List[Dict[str, Any]]:
|
||||
"""获取所有服务的工具function定义"""
|
||||
return self.tools
|
||||
|
||||
def is_mcp_tool(self, tool_name: str) -> bool:
|
||||
"""检查是否是MCP工具"""
|
||||
for tool in self.tools:
|
||||
if (
|
||||
tool.get("function") is not 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:
|
||||
"""执行工具调用,失败时会尝试重新连接"""
|
||||
logger.bind(tag=TAG).info(f"执行服务端MCP工具 {tool_name},参数: {arguments}")
|
||||
|
||||
max_retries = 3 # 最大重试次数
|
||||
retry_interval = 2 # 重试间隔(秒)
|
||||
|
||||
# 找到对应的客户端
|
||||
client_name = None
|
||||
target_client = None
|
||||
for name, client in self.clients.items():
|
||||
if client.has_tool(tool_name):
|
||||
client_name = name
|
||||
target_client = client
|
||||
break
|
||||
|
||||
if not target_client:
|
||||
raise ValueError(f"工具 {tool_name} 在任意MCP服务中未找到")
|
||||
|
||||
# 带重试机制的工具调用
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
return await target_client.call_tool(tool_name, arguments)
|
||||
except Exception as e:
|
||||
# 最后一次尝试失败时直接抛出异常
|
||||
if attempt == max_retries - 1:
|
||||
raise
|
||||
|
||||
logger.bind(tag=TAG).warning(
|
||||
f"执行工具 {tool_name} 失败 (尝试 {attempt+1}/{max_retries}): {e}"
|
||||
)
|
||||
|
||||
# 尝试重新连接
|
||||
logger.bind(tag=TAG).info(
|
||||
f"重试前尝试重新连接 MCP 客户端 {client_name}"
|
||||
)
|
||||
try:
|
||||
# 关闭旧的连接
|
||||
await target_client.cleanup()
|
||||
|
||||
# 重新初始化客户端
|
||||
config = self.load_config()
|
||||
if client_name in config:
|
||||
client = ServerMCPClient(config[client_name])
|
||||
await client.initialize()
|
||||
self.clients[client_name] = client
|
||||
target_client = client
|
||||
logger.bind(tag=TAG).info(
|
||||
f"成功重新连接 MCP 客户端: {client_name}"
|
||||
)
|
||||
else:
|
||||
logger.bind(tag=TAG).error(
|
||||
f"Cannot reconnect MCP client {client_name}: config not found"
|
||||
)
|
||||
except Exception as reconnect_error:
|
||||
logger.bind(tag=TAG).error(
|
||||
f"Failed to reconnect MCP client {client_name}: {reconnect_error}"
|
||||
)
|
||||
|
||||
# 等待一段时间再重试
|
||||
await asyncio.sleep(retry_interval)
|
||||
|
||||
async def cleanup_all(self) -> None:
|
||||
"""关闭所有 MCP客户端"""
|
||||
for name, client in list(self.clients.items()):
|
||||
try:
|
||||
if hasattr(client, "cleanup"):
|
||||
await asyncio.wait_for(client.cleanup(), timeout=20)
|
||||
logger.bind(tag=TAG).info(f"服务端MCP客户端已关闭: {name}")
|
||||
except (asyncio.TimeoutError, Exception) as e:
|
||||
logger.bind(tag=TAG).error(f"关闭服务端MCP客户端 {name} 时出错: {e}")
|
||||
self.clients.clear()
|
||||
@@ -0,0 +1,5 @@
|
||||
"""服务端插件工具模块"""
|
||||
|
||||
from .plugin_executor import ServerPluginExecutor
|
||||
|
||||
__all__ = ["ServerPluginExecutor"]
|
||||
@@ -0,0 +1,84 @@
|
||||
"""服务端插件工具执行器"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from ..base import ToolType, ToolDefinition, ToolExecutor
|
||||
from plugins_func.register import all_function_registry, Action, ActionResponse
|
||||
|
||||
|
||||
class ServerPluginExecutor(ToolExecutor):
|
||||
"""服务端插件工具执行器"""
|
||||
|
||||
def __init__(self, conn):
|
||||
self.conn = conn
|
||||
self.config = conn.config
|
||||
|
||||
async def execute(
|
||||
self, conn, tool_name: str, arguments: Dict[str, Any]
|
||||
) -> ActionResponse:
|
||||
"""执行服务端插件工具"""
|
||||
func_item = all_function_registry.get(tool_name)
|
||||
if not func_item:
|
||||
return ActionResponse(
|
||||
action=Action.NOTFOUND, response=f"插件函数 {tool_name} 不存在"
|
||||
)
|
||||
|
||||
try:
|
||||
# 根据工具类型决定如何调用
|
||||
if hasattr(func_item, "type"):
|
||||
func_type = func_item.type
|
||||
if func_type.code in [4, 5]: # SYSTEM_CTL, IOT_CTL (需要conn参数)
|
||||
result = func_item.func(conn, **arguments)
|
||||
elif func_type.code == 2: # WAIT
|
||||
result = func_item.func(**arguments)
|
||||
elif func_type.code == 3: # CHANGE_SYS_PROMPT
|
||||
result = func_item.func(conn, **arguments)
|
||||
else:
|
||||
result = func_item.func(**arguments)
|
||||
else:
|
||||
# 默认不传conn参数
|
||||
result = func_item.func(**arguments)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
return ActionResponse(
|
||||
action=Action.ERROR,
|
||||
response=str(e),
|
||||
)
|
||||
|
||||
def get_tools(self) -> Dict[str, ToolDefinition]:
|
||||
"""获取所有注册的服务端插件工具"""
|
||||
tools = {}
|
||||
|
||||
# 获取必要的函数
|
||||
necessary_functions = ["handle_exit_intent", "get_time", "get_lunar"]
|
||||
|
||||
# 获取配置中的函数
|
||||
config_functions = self.config["Intent"][
|
||||
self.config["selected_module"]["Intent"]
|
||||
].get("functions", [])
|
||||
|
||||
# 转换为列表
|
||||
if not isinstance(config_functions, list):
|
||||
try:
|
||||
config_functions = list(config_functions)
|
||||
except TypeError:
|
||||
config_functions = []
|
||||
|
||||
# 合并所有需要的函数
|
||||
all_required_functions = list(set(necessary_functions + config_functions))
|
||||
|
||||
for func_name in all_required_functions:
|
||||
func_item = all_function_registry.get(func_name)
|
||||
if func_item:
|
||||
tools[func_name] = ToolDefinition(
|
||||
name=func_name,
|
||||
description=func_item.description,
|
||||
tool_type=ToolType.SERVER_PLUGIN,
|
||||
)
|
||||
|
||||
return tools
|
||||
|
||||
def has_tool(self, tool_name: str) -> bool:
|
||||
"""检查是否有指定的服务端插件工具"""
|
||||
return tool_name in all_function_registry
|
||||
@@ -0,0 +1,235 @@
|
||||
"""统一工具处理器"""
|
||||
|
||||
import json
|
||||
from typing import Dict, List, Any, Optional
|
||||
from config.logger import setup_logging
|
||||
from plugins_func.loadplugins import auto_import_modules
|
||||
|
||||
from .base import ToolType
|
||||
from plugins_func.register import Action, ActionResponse
|
||||
from .unified_tool_manager import ToolManager
|
||||
from .server_plugins import ServerPluginExecutor
|
||||
from .server_mcp import ServerMCPExecutor
|
||||
from .device_iot import DeviceIoTExecutor
|
||||
from .device_mcp import DeviceMCPExecutor
|
||||
from .mcp_endpoint import MCPEndpointExecutor
|
||||
|
||||
|
||||
class UnifiedToolHandler:
|
||||
"""统一工具处理器"""
|
||||
|
||||
def __init__(self, conn):
|
||||
self.conn = conn
|
||||
self.config = conn.config
|
||||
self.logger = setup_logging()
|
||||
|
||||
# 创建工具管理器
|
||||
self.tool_manager = ToolManager(conn)
|
||||
|
||||
# 创建各类执行器
|
||||
self.server_plugin_executor = ServerPluginExecutor(conn)
|
||||
self.server_mcp_executor = ServerMCPExecutor(conn)
|
||||
self.device_iot_executor = DeviceIoTExecutor(conn)
|
||||
self.device_mcp_executor = DeviceMCPExecutor(conn)
|
||||
self.mcp_endpoint_executor = MCPEndpointExecutor(conn)
|
||||
|
||||
# 注册执行器
|
||||
self.tool_manager.register_executor(
|
||||
ToolType.SERVER_PLUGIN, self.server_plugin_executor
|
||||
)
|
||||
self.tool_manager.register_executor(
|
||||
ToolType.SERVER_MCP, self.server_mcp_executor
|
||||
)
|
||||
self.tool_manager.register_executor(
|
||||
ToolType.DEVICE_IOT, self.device_iot_executor
|
||||
)
|
||||
self.tool_manager.register_executor(
|
||||
ToolType.DEVICE_MCP, self.device_mcp_executor
|
||||
)
|
||||
self.tool_manager.register_executor(
|
||||
ToolType.MCP_ENDPOINT, self.mcp_endpoint_executor
|
||||
)
|
||||
|
||||
# 初始化标志
|
||||
self.finish_init = False
|
||||
|
||||
async def _initialize(self):
|
||||
"""异步初始化"""
|
||||
try:
|
||||
# 自动导入插件模块
|
||||
auto_import_modules("plugins_func.functions")
|
||||
|
||||
# 初始化服务端MCP
|
||||
await self.server_mcp_executor.initialize()
|
||||
|
||||
# 初始化MCP接入点
|
||||
await self._initialize_mcp_endpoint()
|
||||
|
||||
# 初始化Home Assistant(如果需要)
|
||||
self._initialize_home_assistant()
|
||||
|
||||
self.finish_init = True
|
||||
self.logger.info("统一工具处理器初始化完成")
|
||||
|
||||
# 输出当前支持的所有工具列表
|
||||
self.current_support_functions()
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"统一工具处理器初始化失败: {e}")
|
||||
|
||||
async def _initialize_mcp_endpoint(self):
|
||||
"""初始化MCP接入点"""
|
||||
try:
|
||||
from .mcp_endpoint import connect_mcp_endpoint
|
||||
|
||||
# 从配置中获取MCP接入点URL
|
||||
mcp_endpoint_url = self.config.get("mcp_endpoint", "")
|
||||
|
||||
if (
|
||||
mcp_endpoint_url
|
||||
and "你的" not in mcp_endpoint_url
|
||||
and mcp_endpoint_url != "null"
|
||||
):
|
||||
self.logger.info(f"正在初始化MCP接入点: {mcp_endpoint_url}")
|
||||
mcp_endpoint_client = await connect_mcp_endpoint(
|
||||
mcp_endpoint_url, self.conn
|
||||
)
|
||||
|
||||
if mcp_endpoint_client:
|
||||
# 将MCP接入点客户端保存到连接对象中
|
||||
self.conn.mcp_endpoint_client = mcp_endpoint_client
|
||||
self.logger.info("MCP接入点初始化成功")
|
||||
else:
|
||||
self.logger.warning("MCP接入点初始化失败")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"初始化MCP接入点失败: {e}")
|
||||
|
||||
def _initialize_home_assistant(self):
|
||||
"""初始化Home Assistant提示词"""
|
||||
try:
|
||||
from plugins_func.functions.hass_init import append_devices_to_prompt
|
||||
|
||||
append_devices_to_prompt(self.conn)
|
||||
except ImportError:
|
||||
pass # 忽略导入错误
|
||||
except Exception as e:
|
||||
self.logger.error(f"初始化Home Assistant失败: {e}")
|
||||
|
||||
def get_functions(self) -> List[Dict[str, Any]]:
|
||||
"""获取所有工具的函数描述"""
|
||||
return self.tool_manager.get_function_descriptions()
|
||||
|
||||
def current_support_functions(self) -> List[str]:
|
||||
"""获取当前支持的函数名称列表"""
|
||||
func_names = self.tool_manager.get_supported_tool_names()
|
||||
self.logger.info(f"当前支持的函数列表: {func_names}")
|
||||
return func_names
|
||||
|
||||
def upload_functions_desc(self):
|
||||
"""刷新函数描述列表"""
|
||||
self.tool_manager.refresh_tools()
|
||||
self.logger.info("函数描述列表已刷新")
|
||||
|
||||
def has_tool(self, tool_name: str) -> bool:
|
||||
"""检查是否有指定工具"""
|
||||
return self.tool_manager.has_tool(tool_name)
|
||||
|
||||
async def handle_llm_function_call(
|
||||
self, conn, function_call_data: Dict[str, Any]
|
||||
) -> Optional[ActionResponse]:
|
||||
"""处理LLM函数调用"""
|
||||
try:
|
||||
# 处理多函数调用
|
||||
if "function_calls" in function_call_data:
|
||||
responses = []
|
||||
for call in function_call_data["function_calls"]:
|
||||
result = await self.tool_manager.execute_tool(
|
||||
call["name"], call.get("arguments", {})
|
||||
)
|
||||
responses.append(result)
|
||||
return self._combine_responses(responses)
|
||||
|
||||
# 处理单函数调用
|
||||
function_name = function_call_data["name"]
|
||||
arguments = function_call_data.get("arguments", {})
|
||||
|
||||
# 如果arguments是字符串,尝试解析为JSON
|
||||
if isinstance(arguments, str):
|
||||
try:
|
||||
arguments = json.loads(arguments) if arguments else {}
|
||||
except json.JSONDecodeError:
|
||||
self.logger.error(f"无法解析函数参数: {arguments}")
|
||||
return ActionResponse(
|
||||
action=Action.ERROR,
|
||||
response="无法解析函数参数",
|
||||
)
|
||||
|
||||
self.logger.debug(f"调用函数: {function_name}, 参数: {arguments}")
|
||||
|
||||
# 执行工具调用
|
||||
result = await self.tool_manager.execute_tool(function_name, arguments)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"处理function call错误: {e}")
|
||||
return ActionResponse(action=Action.ERROR, response=str(e))
|
||||
|
||||
def _combine_responses(self, responses: List[ActionResponse]) -> ActionResponse:
|
||||
"""合并多个函数调用的响应"""
|
||||
if not responses:
|
||||
return ActionResponse(action=Action.NONE, response="无响应")
|
||||
|
||||
# 如果有任何错误,返回第一个错误
|
||||
for response in responses:
|
||||
if response.action == Action.ERROR:
|
||||
return response
|
||||
|
||||
# 合并所有成功的响应
|
||||
contents = []
|
||||
responses_text = []
|
||||
|
||||
for response in responses:
|
||||
if response.content:
|
||||
contents.append(response.content)
|
||||
if response.response:
|
||||
responses_text.append(response.response)
|
||||
|
||||
# 确定最终的动作类型
|
||||
final_action = Action.RESPONSE
|
||||
for response in responses:
|
||||
if response.action == Action.REQLLM:
|
||||
final_action = Action.REQLLM
|
||||
break
|
||||
|
||||
return ActionResponse(
|
||||
action=final_action,
|
||||
result="; ".join(contents) if contents else None,
|
||||
response="; ".join(responses_text) if responses_text else None,
|
||||
)
|
||||
|
||||
async def register_iot_tools(self, descriptors: List[Dict[str, Any]]):
|
||||
"""注册IoT设备工具"""
|
||||
self.device_iot_executor.register_iot_tools(descriptors)
|
||||
self.tool_manager.refresh_tools()
|
||||
self.logger.info(f"注册了{len(descriptors)}个IoT设备的工具")
|
||||
|
||||
def get_tool_statistics(self) -> Dict[str, int]:
|
||||
"""获取工具统计信息"""
|
||||
return self.tool_manager.get_tool_statistics()
|
||||
|
||||
async def cleanup(self):
|
||||
"""清理资源"""
|
||||
try:
|
||||
await self.server_mcp_executor.cleanup()
|
||||
|
||||
# 清理MCP接入点连接
|
||||
if (
|
||||
hasattr(self.conn, "mcp_endpoint_client")
|
||||
and self.conn.mcp_endpoint_client
|
||||
):
|
||||
await self.conn.mcp_endpoint_client.close()
|
||||
|
||||
self.logger.info("工具处理器清理完成")
|
||||
except Exception as e:
|
||||
self.logger.error(f"工具处理器清理失败: {e}")
|
||||
@@ -0,0 +1,124 @@
|
||||
"""统一工具管理器"""
|
||||
|
||||
from typing import Dict, List, Optional, Any
|
||||
from config.logger import setup_logging
|
||||
from plugins_func.register import Action, ActionResponse
|
||||
from .base import ToolType, ToolDefinition, ToolExecutor
|
||||
|
||||
|
||||
class ToolManager:
|
||||
"""统一工具管理器,管理所有类型的工具"""
|
||||
|
||||
def __init__(self, conn):
|
||||
self.conn = conn
|
||||
self.logger = setup_logging()
|
||||
self.executors: Dict[ToolType, ToolExecutor] = {}
|
||||
self._cached_tools: Optional[Dict[str, ToolDefinition]] = None
|
||||
self._cached_function_descriptions: Optional[List[Dict[str, Any]]] = None
|
||||
|
||||
def register_executor(self, tool_type: ToolType, executor: ToolExecutor):
|
||||
"""注册工具执行器"""
|
||||
self.executors[tool_type] = executor
|
||||
self._invalidate_cache()
|
||||
self.logger.info(f"注册工具执行器: {tool_type.value}")
|
||||
|
||||
def _invalidate_cache(self):
|
||||
"""使缓存失效"""
|
||||
self._cached_tools = None
|
||||
self._cached_function_descriptions = None
|
||||
|
||||
def get_all_tools(self) -> Dict[str, ToolDefinition]:
|
||||
"""获取所有工具定义"""
|
||||
if self._cached_tools is not None:
|
||||
return self._cached_tools
|
||||
|
||||
all_tools = {}
|
||||
for tool_type, executor in self.executors.items():
|
||||
try:
|
||||
tools = executor.get_tools()
|
||||
for name, definition in tools.items():
|
||||
if name in all_tools:
|
||||
self.logger.warning(f"工具名称冲突: {name}")
|
||||
all_tools[name] = definition
|
||||
except Exception as e:
|
||||
self.logger.error(f"获取{tool_type.value}工具时出错: {e}")
|
||||
|
||||
self._cached_tools = all_tools
|
||||
return all_tools
|
||||
|
||||
def get_function_descriptions(self) -> List[Dict[str, Any]]:
|
||||
"""获取所有工具的函数描述(OpenAI格式)"""
|
||||
if self._cached_function_descriptions is not None:
|
||||
return self._cached_function_descriptions
|
||||
|
||||
descriptions = []
|
||||
tools = self.get_all_tools()
|
||||
for tool_definition in tools.values():
|
||||
descriptions.append(tool_definition.description)
|
||||
|
||||
self._cached_function_descriptions = descriptions
|
||||
return descriptions
|
||||
|
||||
def has_tool(self, tool_name: str) -> bool:
|
||||
"""检查是否存在指定工具"""
|
||||
tools = self.get_all_tools()
|
||||
return tool_name in tools
|
||||
|
||||
def get_tool_type(self, tool_name: str) -> Optional[ToolType]:
|
||||
"""获取工具类型"""
|
||||
tools = self.get_all_tools()
|
||||
tool_def = tools.get(tool_name)
|
||||
return tool_def.tool_type if tool_def else None
|
||||
|
||||
async def execute_tool(
|
||||
self, tool_name: str, arguments: Dict[str, Any]
|
||||
) -> ActionResponse:
|
||||
"""执行工具调用"""
|
||||
try:
|
||||
# 查找工具类型
|
||||
tool_type = self.get_tool_type(tool_name)
|
||||
if not tool_type:
|
||||
return ActionResponse(
|
||||
action=Action.NOTFOUND,
|
||||
response=f"工具 {tool_name} 不存在",
|
||||
)
|
||||
|
||||
# 获取对应的执行器
|
||||
executor = self.executors.get(tool_type)
|
||||
if not executor:
|
||||
return ActionResponse(
|
||||
action=Action.ERROR,
|
||||
response=f"工具类型 {tool_type.value} 的执行器未注册",
|
||||
)
|
||||
|
||||
# 执行工具
|
||||
self.logger.info(f"执行工具: {tool_name},参数: {arguments}")
|
||||
result = await executor.execute(self.conn, tool_name, arguments)
|
||||
self.logger.debug(f"工具执行结果: {result}")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"执行工具 {tool_name} 时出错: {e}")
|
||||
return ActionResponse(action=Action.ERROR, response=str(e))
|
||||
|
||||
def get_supported_tool_names(self) -> List[str]:
|
||||
"""获取所有支持的工具名称"""
|
||||
tools = self.get_all_tools()
|
||||
return list(tools.keys())
|
||||
|
||||
def refresh_tools(self):
|
||||
"""刷新工具缓存"""
|
||||
self._invalidate_cache()
|
||||
self.logger.info("工具缓存已刷新")
|
||||
|
||||
def get_tool_statistics(self) -> Dict[str, int]:
|
||||
"""获取工具统计信息"""
|
||||
stats = {}
|
||||
for tool_type, executor in self.executors.items():
|
||||
try:
|
||||
tools = executor.get_tools()
|
||||
stats[tool_type.value] = len(tools)
|
||||
except Exception as e:
|
||||
self.logger.error(f"获取{tool_type.value}工具统计时出错: {e}")
|
||||
stats[tool_type.value] = 0
|
||||
return stats
|
||||
@@ -43,7 +43,6 @@ class TTSProviderBase(ABC):
|
||||
self.tts_text_buff = []
|
||||
self.punctuations = (
|
||||
"。",
|
||||
".",
|
||||
"?",
|
||||
"?",
|
||||
"!",
|
||||
@@ -59,7 +58,6 @@ class TTSProviderBase(ABC):
|
||||
"、",
|
||||
",",
|
||||
"。",
|
||||
".",
|
||||
"?",
|
||||
"?",
|
||||
"!",
|
||||
@@ -171,7 +169,7 @@ class TTSProviderBase(ABC):
|
||||
)
|
||||
)
|
||||
# 对于单句的文本,进行分段处理
|
||||
segments = re.split(r'([。!?!?;;\n])', content_detail)
|
||||
segments = re.split(r"([。!?!?;;\n])", content_detail)
|
||||
for seg in segments:
|
||||
self.tts_text_queue.put(
|
||||
TTSMessageDTO(
|
||||
|
||||
@@ -37,7 +37,9 @@ class TTSProvider(TTSProviderBase):
|
||||
self.api_url = config.get("api_url")
|
||||
self.authorization = config.get("authorization")
|
||||
self.header = {"Authorization": f"{self.authorization}{self.access_token}"}
|
||||
check_model_key("TTS", self.access_token)
|
||||
model_key_msg = check_model_key("TTS", self.access_token)
|
||||
if model_key_msg:
|
||||
logger.bind(tag=TAG).error(model_key_msg)
|
||||
|
||||
async def text_to_speak(self, text, output_file):
|
||||
request_json = {
|
||||
|
||||
@@ -94,8 +94,9 @@ class TTSProvider(TTSProviderBase):
|
||||
self.format = config.get("response_format", "wav")
|
||||
self.audio_file_type = config.get("response_format", "wav")
|
||||
self.api_key = config.get("api_key", "YOUR_API_KEY")
|
||||
have_key = check_model_key("FishSpeech TTS", self.api_key)
|
||||
if not have_key:
|
||||
model_key_msg = check_model_key("FishSpeech TTS", self.api_key)
|
||||
if model_key_msg:
|
||||
logger.bind(tag=TAG).error(model_key_msg)
|
||||
return
|
||||
self.normalize = str(config.get("normalize", True)).lower() in (
|
||||
"true",
|
||||
|
||||
@@ -12,6 +12,8 @@ from core.utils.util import check_model_key
|
||||
from core.providers.tts.base import TTSProviderBase
|
||||
from core.handle.abortHandle import handleAbortMessage
|
||||
from core.providers.tts.dto.dto import SentenceType, ContentType, InterfaceType
|
||||
from asyncio import Task
|
||||
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
@@ -141,6 +143,7 @@ class TTSProvider(TTSProviderBase):
|
||||
super().__init__(config, delete_audio_file)
|
||||
self.ws = None
|
||||
self.interface_type = InterfaceType.DUAL_STREAM
|
||||
self._monitor_task = None # 监听任务引用
|
||||
self.appId = config.get("appid")
|
||||
self.access_token = config.get("access_token")
|
||||
self.cluster = config.get("cluster")
|
||||
@@ -157,7 +160,9 @@ class TTSProvider(TTSProviderBase):
|
||||
self.opus_encoder = opus_encoder_utils.OpusEncoderUtils(
|
||||
sample_rate=16000, channels=1, frame_size_ms=60
|
||||
)
|
||||
check_model_key("TTS", self.access_token)
|
||||
model_key_msg = check_model_key("TTS", self.access_token)
|
||||
if model_key_msg:
|
||||
logger.bind(tag=TAG).error(model_key_msg)
|
||||
|
||||
async def open_audio_channels(self, conn):
|
||||
try:
|
||||
@@ -202,6 +207,10 @@ class TTSProvider(TTSProviderBase):
|
||||
if message.sentence_type == SentenceType.FIRST:
|
||||
# 初始化参数
|
||||
try:
|
||||
if not getattr(self.conn, "sentence_id", None):
|
||||
self.conn.sentence_id = uuid.uuid4().hex
|
||||
logger.bind(tag=TAG).info(f"自动生成新的 会话ID: {self.conn.sentence_id}")
|
||||
|
||||
logger.bind(tag=TAG).info("开始启动TTS会话...")
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
self.start_session(self.conn.sentence_id),
|
||||
@@ -264,10 +273,9 @@ class TTSProvider(TTSProviderBase):
|
||||
try:
|
||||
# 建立新连接
|
||||
if self.ws is None:
|
||||
await handleAbortMessage(self.conn)
|
||||
logger.bind(tag=TAG).error(f"WebSocket连接不存在,终止发送文本")
|
||||
logger.bind(tag=TAG).warning(f"WebSocket连接不存在,终止发送文本")
|
||||
return
|
||||
|
||||
|
||||
# 过滤Markdown
|
||||
filtered_text = MarkdownCleaner.clean_markdown(text)
|
||||
|
||||
@@ -287,6 +295,25 @@ class TTSProvider(TTSProviderBase):
|
||||
async def start_session(self, session_id):
|
||||
logger.bind(tag=TAG).info(f"开始会话~~{session_id}")
|
||||
try:
|
||||
task = self._monitor_task
|
||||
if (
|
||||
task is not None
|
||||
and isinstance(task, Task)
|
||||
and not task.done()
|
||||
):
|
||||
logger.bind(tag=TAG).info("等待上一个监听任务结束...")
|
||||
if self.ws is not None:
|
||||
logger.bind(tag=TAG).info("强制关闭上一个WebSocket连接以唤醒监听任务...")
|
||||
try:
|
||||
await self.ws.close()
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).warning(f"关闭上一个ws异常: {e}")
|
||||
self.ws = None
|
||||
try:
|
||||
await asyncio.wait_for(task, timeout=8)
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).warning(f"等待监听任务异常: {e}")
|
||||
self._monitor_task = None
|
||||
# 建立新连接
|
||||
await self._ensure_connection()
|
||||
|
||||
@@ -457,6 +484,8 @@ class TTSProvider(TTSProviderBase):
|
||||
except:
|
||||
pass
|
||||
self.ws = None
|
||||
# 监听任务退出时清理引用
|
||||
self._monitor_task = None
|
||||
|
||||
async def send_event(
|
||||
self,
|
||||
|
||||
@@ -166,7 +166,9 @@ class TTSProvider(TTSProviderBase):
|
||||
) as resp:
|
||||
|
||||
if resp.status != 200:
|
||||
logger.error(f"TTS请求失败: {resp.status}, {await resp.text()}")
|
||||
logger.bind(tag=TAG).error(
|
||||
f"TTS请求失败: {resp.status}, {await resp.text()}"
|
||||
)
|
||||
self.tts_audio_queue.put((SentenceType.LAST, [], None))
|
||||
return
|
||||
|
||||
@@ -229,7 +231,7 @@ class TTSProvider(TTSProviderBase):
|
||||
self._process_before_stop_play_files()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"TTS请求异常: {e}")
|
||||
logger.bind(tag=TAG).error(f"TTS请求异常: {e}")
|
||||
self.tts_audio_queue.put((SentenceType.LAST, [], None))
|
||||
|
||||
def to_tts(self, text: str) -> list:
|
||||
@@ -263,7 +265,7 @@ class TTSProvider(TTSProviderBase):
|
||||
self.api_url, params=params, headers=headers, timeout=5
|
||||
) as response:
|
||||
if response.status_code != 200:
|
||||
logger.error(
|
||||
logger.bind(tag=TAG).error(
|
||||
f"TTS请求失败: {response.status_code}, {response.text}"
|
||||
)
|
||||
return []
|
||||
@@ -299,5 +301,5 @@ class TTSProvider(TTSProviderBase):
|
||||
return opus_datas
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"TTS请求异常: {e}")
|
||||
logger.bind(tag=TAG).error(f"TTS请求异常: {e}")
|
||||
return []
|
||||
|
||||
@@ -25,7 +25,9 @@ class TTSProvider(TTSProviderBase):
|
||||
self.speed = float(speed) if speed else 1.0
|
||||
|
||||
self.output_file = config.get("output_dir", "tmp/")
|
||||
check_model_key("TTS", self.api_key)
|
||||
model_key_msg = check_model_key("TTS", self.api_key)
|
||||
if model_key_msg:
|
||||
logger.bind(tag=TAG).error(model_key_msg)
|
||||
|
||||
async def text_to_speak(self, text, output_file):
|
||||
headers = {
|
||||
|
||||
@@ -35,6 +35,10 @@ class VADProvider(VADProviderBase):
|
||||
pcm_frame = self.decoder.decode(opus_packet, 960)
|
||||
conn.client_audio_buffer.extend(pcm_frame) # 将新数据加入缓冲区
|
||||
|
||||
# 确保帧计数器存在
|
||||
if not hasattr(conn, "client_voice_frame_count"):
|
||||
conn.client_voice_frame_count = 0
|
||||
|
||||
# 处理缓冲区中的完整帧(每次处理512采样点)
|
||||
client_have_voice = False
|
||||
while len(conn.client_audio_buffer) >= 512 * 2:
|
||||
@@ -50,18 +54,24 @@ class VADProvider(VADProviderBase):
|
||||
# 检测语音活动
|
||||
with torch.no_grad():
|
||||
speech_prob = self.model(audio_tensor, 16000).item()
|
||||
client_have_voice = speech_prob >= self.vad_threshold
|
||||
is_voice = speech_prob >= self.vad_threshold
|
||||
|
||||
if is_voice:
|
||||
conn.client_voice_frame_count += 1
|
||||
else:
|
||||
conn.client_voice_frame_count = 0
|
||||
|
||||
# 只有连续4帧检测到语音才认为有语音
|
||||
client_have_voice = conn.client_voice_frame_count >= 4
|
||||
|
||||
# 如果之前有声音,但本次没有声音,且与上次有声音的时间差已经超过了静默阈值,则认为已经说完一句话
|
||||
if conn.client_have_voice and not client_have_voice:
|
||||
stop_duration = (
|
||||
time.time() * 1000 - conn.client_have_voice_last_time
|
||||
)
|
||||
stop_duration = time.time() * 1000 - conn.last_activity_time
|
||||
if stop_duration >= self.silence_threshold_ms:
|
||||
conn.client_voice_stop = True
|
||||
if client_have_voice:
|
||||
conn.client_have_voice = True
|
||||
conn.client_have_voice_last_time = time.time() * 1000
|
||||
conn.last_activity_time = time.time() * 1000
|
||||
|
||||
return client_have_voice
|
||||
except opuslib_next.OpusError as e:
|
||||
|
||||
@@ -34,7 +34,9 @@ class VLLMProvider(VLLMProviderBase):
|
||||
except (ValueError, TypeError):
|
||||
setattr(self, param, default)
|
||||
|
||||
check_model_key("VLLM", self.api_key)
|
||||
model_key_msg = check_model_key("VLLM", self.api_key)
|
||||
if model_key_msg:
|
||||
logger.bind(tag=TAG).error(model_key_msg)
|
||||
self.client = openai.OpenAI(api_key=self.api_key, base_url=self.base_url)
|
||||
|
||||
def response(self, question, base64_image):
|
||||
|
||||
@@ -1,12 +1,80 @@
|
||||
import jwt
|
||||
import time
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional, Tuple
|
||||
from typing import Tuple, Optional
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.hazmat.primitives import padding
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
import base64
|
||||
|
||||
|
||||
class AuthToken:
|
||||
def __init__(self, secret_key: str):
|
||||
self.secret_key = secret_key
|
||||
self.secret_key = secret_key.encode() # 转换为字节
|
||||
# 从密钥派生固定长度的加密密钥 (32字节 for AES-256)
|
||||
self.encryption_key = self._derive_key(32)
|
||||
|
||||
def _derive_key(self, length: int) -> bytes:
|
||||
"""派生固定长度的密钥"""
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
|
||||
# 使用固定盐值(实际生产环境应使用随机盐)
|
||||
salt = b"fixed_salt_placeholder" # 生产环境应改为随机生成
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=length,
|
||||
salt=salt,
|
||||
iterations=100000,
|
||||
backend=default_backend(),
|
||||
)
|
||||
return kdf.derive(self.secret_key)
|
||||
|
||||
def _encrypt_payload(self, payload: dict) -> str:
|
||||
"""使用AES-GCM加密整个payload"""
|
||||
# 将payload转换为JSON字符串
|
||||
payload_json = json.dumps(payload)
|
||||
|
||||
# 生成随机IV
|
||||
iv = os.urandom(12)
|
||||
# 创建加密器
|
||||
cipher = Cipher(
|
||||
algorithms.AES(self.encryption_key),
|
||||
modes.GCM(iv),
|
||||
backend=default_backend(),
|
||||
)
|
||||
encryptor = cipher.encryptor()
|
||||
|
||||
# 加密并生成标签
|
||||
ciphertext = encryptor.update(payload_json.encode()) + encryptor.finalize()
|
||||
tag = encryptor.tag
|
||||
|
||||
# 组合 IV + 密文 + 标签
|
||||
encrypted_data = iv + ciphertext + tag
|
||||
return base64.urlsafe_b64encode(encrypted_data).decode()
|
||||
|
||||
def _decrypt_payload(self, encrypted_data: str) -> dict:
|
||||
"""解密AES-GCM加密的payload"""
|
||||
# 解码Base64
|
||||
data = base64.urlsafe_b64decode(encrypted_data.encode())
|
||||
# 拆分组件
|
||||
iv = data[:12]
|
||||
tag = data[-16:]
|
||||
ciphertext = data[12:-16]
|
||||
|
||||
# 创建解密器
|
||||
cipher = Cipher(
|
||||
algorithms.AES(self.encryption_key),
|
||||
modes.GCM(iv, tag),
|
||||
backend=default_backend(),
|
||||
)
|
||||
decryptor = cipher.decryptor()
|
||||
|
||||
# 解密
|
||||
plaintext = decryptor.update(ciphertext) + decryptor.finalize()
|
||||
return json.loads(plaintext.decode())
|
||||
|
||||
def generate_token(self, device_id: str) -> str:
|
||||
"""
|
||||
@@ -17,11 +85,17 @@ class AuthToken:
|
||||
# 设置过期时间为1小时后
|
||||
expire_time = datetime.now(timezone.utc) + timedelta(hours=1)
|
||||
|
||||
# 创建payload
|
||||
# 创建原始payload
|
||||
payload = {"device_id": device_id, "exp": expire_time.timestamp()}
|
||||
|
||||
# 加密整个payload
|
||||
encrypted_payload = self._encrypt_payload(payload)
|
||||
|
||||
# 创建外层payload,包含加密数据
|
||||
outer_payload = {"data": encrypted_payload}
|
||||
|
||||
# 使用JWT进行编码
|
||||
token = jwt.encode(payload, self.secret_key, algorithm="HS256")
|
||||
token = jwt.encode(outer_payload, self.secret_key, algorithm="HS256")
|
||||
return token
|
||||
|
||||
def verify_token(self, token: str) -> Tuple[bool, Optional[str]]:
|
||||
@@ -31,13 +105,22 @@ class AuthToken:
|
||||
:return: (是否有效, 设备ID)
|
||||
"""
|
||||
try:
|
||||
# 解码token
|
||||
payload = jwt.decode(token, self.secret_key, algorithms=["HS256"])
|
||||
# 先验证外层JWT(签名和过期时间)
|
||||
outer_payload = jwt.decode(token, self.secret_key, algorithms=["HS256"])
|
||||
|
||||
# 检查是否过期
|
||||
if payload["exp"] < time.time():
|
||||
# 解密内层payload
|
||||
inner_payload = self._decrypt_payload(outer_payload["data"])
|
||||
|
||||
# 再次检查过期时间(双重验证)
|
||||
if inner_payload["exp"] < time.time():
|
||||
return False, None
|
||||
|
||||
return True, payload["device_id"]
|
||||
return True, inner_payload["device_id"]
|
||||
|
||||
except jwt.InvalidTokenError:
|
||||
return False, None
|
||||
except json.JSONDecodeError:
|
||||
return False, None
|
||||
except Exception as e: # 捕获其他可能的错误
|
||||
print(f"Token verification failed: {str(e)}")
|
||||
return False, None
|
||||
|
||||
@@ -186,10 +186,8 @@ def remove_punctuation_and_length(text):
|
||||
|
||||
def check_model_key(modelType, modelKey):
|
||||
if "你" in modelKey:
|
||||
raise ValueError(
|
||||
"你还没配置" + modelType + "的密钥,请检查一下所使用的LLM是否配置了密钥"
|
||||
)
|
||||
return True
|
||||
return f"配置错误: {modelType} 的 API key 未设置,当前值为: {modelKey}"
|
||||
return None
|
||||
|
||||
|
||||
def parse_string_to_list(value, separator=";"):
|
||||
@@ -785,7 +783,9 @@ def audio_bytes_to_data(audio_bytes, file_type, is_opus=True):
|
||||
return p3.decode_opus_from_bytes(audio_bytes)
|
||||
else:
|
||||
# 其他格式用pydub
|
||||
audio = AudioSegment.from_file(BytesIO(audio_bytes), format=file_type, parameters=["-nostdin"])
|
||||
audio = AudioSegment.from_file(
|
||||
BytesIO(audio_bytes), format=file_type, parameters=["-nostdin"]
|
||||
)
|
||||
audio = audio.set_channels(1).set_frame_rate(16000).set_sample_width(2)
|
||||
duration = len(audio) / 1000.0
|
||||
raw_data = audio.raw_data
|
||||
@@ -838,11 +838,11 @@ def opus_datas_to_wav_bytes(opus_datas, sample_rate=16000, channels=1):
|
||||
pcm = decoder.decode(opus_frame, frame_size)
|
||||
pcm_datas.append(pcm)
|
||||
|
||||
pcm_bytes = b''.join(pcm_datas)
|
||||
pcm_bytes = b"".join(pcm_datas)
|
||||
|
||||
# 写入wav字节流
|
||||
wav_buffer = BytesIO()
|
||||
with wave.open(wav_buffer, 'wb') as wf:
|
||||
with wave.open(wav_buffer, "wb") as wf:
|
||||
wf.setnchannels(channels)
|
||||
wf.setsampwidth(2) # 16bit
|
||||
wf.setframerate(sample_rate)
|
||||
|
||||
@@ -52,7 +52,7 @@ services:
|
||||
volumes:
|
||||
# 配置文件目录
|
||||
- ./uploadfile:/uploadfile
|
||||
|
||||
# 数据库模块
|
||||
xiaozhi-esp32-server-db:
|
||||
image: mysql:latest
|
||||
container_name: xiaozhi-esp32-server-db
|
||||
@@ -73,6 +73,7 @@ services:
|
||||
- MYSQL_ROOT_PASSWORD=123456
|
||||
- MYSQL_DATABASE=xiaozhi_esp32_server
|
||||
- MYSQL_INITDB_ARGS="--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci"
|
||||
# redis模块
|
||||
xiaozhi-esp32-server-redis:
|
||||
image: redis
|
||||
expose:
|
||||
|
||||
@@ -120,16 +120,16 @@ def map_category(category_text):
|
||||
# 类别映射字典,目前支持社会、国际、财经新闻,如需更多类型,参见配置文件
|
||||
category_map = {
|
||||
# 社会新闻
|
||||
"社会": "society",
|
||||
"社会新闻": "society",
|
||||
"社会": "society_rss_url",
|
||||
"社会新闻": "society_rss_url",
|
||||
# 国际新闻
|
||||
"国际": "world",
|
||||
"国际新闻": "world",
|
||||
"国际": "world_rss_url",
|
||||
"国际新闻": "world_rss_url",
|
||||
# 财经新闻
|
||||
"财经": "finance",
|
||||
"财经新闻": "finance",
|
||||
"金融": "finance",
|
||||
"经济": "finance",
|
||||
"财经": "finance_rss_url",
|
||||
"财经新闻": "finance_rss_url",
|
||||
"金融": "finance_rss_url",
|
||||
"经济": "finance_rss_url",
|
||||
}
|
||||
|
||||
# 转换为小写并去除空格
|
||||
@@ -205,8 +205,8 @@ def get_news_from_chinanews(
|
||||
|
||||
# 如果提供了类别,尝试从配置中获取对应的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]
|
||||
if mapped_category and mapped_category in rss_config:
|
||||
rss_url = rss_config[mapped_category]
|
||||
|
||||
logger.bind(tag=TAG).info(
|
||||
f"获取新闻: 原始类别={category}, 映射类别={mapped_category}, URL={rss_url}"
|
||||
|
||||
@@ -8,38 +8,61 @@ from markitdown import MarkItDown
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
# 新闻来源字典,包含名称和对应的API ID
|
||||
NEWS_SOURCES = {
|
||||
# 默认新闻来源字典,当配置中没有指定时使用
|
||||
DEFAULT_NEWS_SOURCES = {
|
||||
"thepaper": "澎湃新闻",
|
||||
"baidu": "百度热搜",
|
||||
"cls-depth": "财联社",
|
||||
}
|
||||
|
||||
|
||||
# 动态生成新闻源描述
|
||||
def generate_news_sources_description():
|
||||
sources_desc = []
|
||||
for source_id, source_name in NEWS_SOURCES.items():
|
||||
sources_desc.append(f"{source_name}({source_id})")
|
||||
return "、".join(sources_desc)
|
||||
def get_news_sources_from_config(conn):
|
||||
"""从配置中获取新闻源字典"""
|
||||
try:
|
||||
# 尝试从插件配置中获取新闻源
|
||||
if (conn.config.get("plugins") and
|
||||
conn.config["plugins"].get("get_news_from_newsnow") and
|
||||
conn.config["plugins"]["get_news_from_newsnow"].get("news_sources")):
|
||||
|
||||
# 如果配置中是字符串,尝试解析JSON
|
||||
news_sources_config = conn.config["plugins"]["get_news_from_newsnow"]["news_sources"]
|
||||
if isinstance(news_sources_config, str):
|
||||
try:
|
||||
return json.loads(news_sources_config)
|
||||
except json.JSONDecodeError:
|
||||
logger.bind(tag=TAG).warning("新闻源配置JSON格式错误,使用默认配置")
|
||||
return DEFAULT_NEWS_SOURCES
|
||||
elif isinstance(news_sources_config, dict):
|
||||
return news_sources_config
|
||||
else:
|
||||
logger.bind(tag=TAG).warning("新闻源配置格式错误,使用默认配置")
|
||||
return DEFAULT_NEWS_SOURCES
|
||||
else:
|
||||
logger.bind(tag=TAG).debug("未找到新闻源配置,使用默认配置")
|
||||
return DEFAULT_NEWS_SOURCES
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"获取新闻源配置失败: {e},使用默认配置")
|
||||
return DEFAULT_NEWS_SOURCES
|
||||
|
||||
|
||||
# 静态函数描述,使用默认新闻源生成描述
|
||||
GET_NEWS_FROM_NEWSNOW_FUNCTION_DESC = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_news_from_newsnow",
|
||||
"description": (
|
||||
"获取最新新闻,随机选择一条新闻进行播报。"
|
||||
f"用户可以选择不同的新闻源,如{generate_news_sources_description()}等。"
|
||||
"用户可以选择不同的新闻源,如澎湃新闻(thepaper)、百度热搜(baidu)、财联社(cls-depth)等。"
|
||||
"如果没有指定,默认从澎湃新闻获取。"
|
||||
"用户可以要求获取详细内容,此时会获取新闻的详细内容。"
|
||||
"注意:实际可用的新闻源取决于系统配置。"
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"source": {
|
||||
"type": "string",
|
||||
"description": f"新闻源,例如{generate_news_sources_description()}等。可选参数,如果不提供则使用默认新闻源",
|
||||
"description": "新闻源,例如thepaper、baidu、cls-depth等。可选参数,如果不提供则使用默认新闻源",
|
||||
},
|
||||
"detail": {
|
||||
"type": "boolean",
|
||||
@@ -115,6 +138,9 @@ def get_news_from_newsnow(
|
||||
):
|
||||
"""获取新闻并随机选择一条进行播报,或获取上一条新闻的详细内容"""
|
||||
try:
|
||||
# 获取当前配置的新闻源
|
||||
news_sources = get_news_sources_from_config(conn)
|
||||
|
||||
# 如果detail为True,获取上一条新闻的详细内容
|
||||
detail = str(detail).lower() == "true"
|
||||
if detail:
|
||||
@@ -132,7 +158,7 @@ def get_news_from_newsnow(
|
||||
url = conn.last_newsnow_link.get("url")
|
||||
title = conn.last_newsnow_link.get("title", "未知标题")
|
||||
source_id = conn.last_newsnow_link.get("source_id", "thepaper")
|
||||
source_name = NEWS_SOURCES.get(source_id, "未知来源")
|
||||
source_name = news_sources.get(source_id, "未知来源")
|
||||
|
||||
if not url or url == "#":
|
||||
return ActionResponse(
|
||||
@@ -167,11 +193,11 @@ def get_news_from_newsnow(
|
||||
|
||||
# 否则,获取新闻列表并随机选择一条
|
||||
# 验证新闻源是否有效,如果无效则使用默认源
|
||||
if source not in NEWS_SOURCES:
|
||||
if source not in news_sources:
|
||||
logger.bind(tag=TAG).warning(f"无效的新闻源: {source},使用默认源thepaper")
|
||||
source = "thepaper"
|
||||
|
||||
source_name = NEWS_SOURCES.get(source, "澎湃新闻")
|
||||
source_name = news_sources.get(source, "澎湃新闻")
|
||||
logger.bind(tag=TAG).info(f"获取新闻: 新闻源={source}({source_name})")
|
||||
|
||||
# 获取新闻列表
|
||||
|
||||
@@ -1,157 +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()
|
||||
|
||||
|
||||
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_speaker_volume_or_screen_brightness",
|
||||
"description": (
|
||||
"用户想要获取或者设置设备的音量/亮度大小,或者用户觉得声音/亮度过高或过低,或者用户想提高或降低音量/亮度。\n"
|
||||
"**严格限制**:仅当用户明确操作 **Speaker(音量)或Screen(亮度)** 时才能调用此函数!\n"
|
||||
"对于其他设备(如AC、Battery、Switch等),请不要调用此函数,而是继续正常的对话。\n\n"
|
||||
"示例:\n"
|
||||
"- 用户说『现在亮度多少』 → 调用函数:device_type: Screen, action: get\n"
|
||||
"- 用户说『设置音量为50』 → 调用函数:device_type: Speaker, action: set, value: 50\n"
|
||||
"- 用户说『亮度太高了』 → 调用函数:device_type: Screen, action: lower\n"
|
||||
"- 用户说『调大音量』 → 调用函数:device_type: Speaker, action: raise\n\n"
|
||||
"**拒绝调用示例**(应继续对话而非调用本函数):\n"
|
||||
"- 用户说『空调调低一度』 → 不调用(设备类型为AC)\n"
|
||||
"- 用户说『开关灯』 → 不调用(设备类型为Switch)\n"
|
||||
"- 用户说『电量多少』 → 不调用(设备类型为Battery)\n"
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"device_type": {
|
||||
"type": "string",
|
||||
"description": "设备类型,**严格限定为Speaker(音量)或Screen(亮度)**,其他设备类型禁止调用此函数",
|
||||
"enum": ["Speaker", "Screen"],
|
||||
},
|
||||
"action": {
|
||||
"type": "string",
|
||||
"description": "动作名称,可选值:get(获取),set(设置),raise(提高),lower(降低)",
|
||||
},
|
||||
"value": {
|
||||
"type": "integer",
|
||||
"description": "值大小,可选值:0-100之间的整数",
|
||||
},
|
||||
},
|
||||
"required": ["device_type", "action"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@register_function(
|
||||
"handle_speaker_volume_or_screen_brightness",
|
||||
handle_device_function_desc,
|
||||
ToolType.IOT_CTL,
|
||||
)
|
||||
def handle_speaker_volume_or_screen_brightness(
|
||||
conn, device_type: str, action: str, value: int = None
|
||||
):
|
||||
# 检查value是否为中文值
|
||||
if (
|
||||
value is not None
|
||||
and isinstance(value, str)
|
||||
and any("\u4e00" <= char <= "\u9fff" for char in str(value))
|
||||
):
|
||||
raise Exception(
|
||||
f"请直接告诉我要将{'音量' if device_type=='Speaker' else '亮度'}调整成多少"
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
@@ -17,57 +17,78 @@ hass_get_state_function_desc = {
|
||||
"properties": {
|
||||
"entity_id": {
|
||||
"type": "string",
|
||||
"description": "需要操作的设备id,homeassistant里的entity_id"
|
||||
"description": "需要操作的设备id,homeassistant里的entity_id",
|
||||
}
|
||||
},
|
||||
"required": ["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=''):
|
||||
def hass_get_state(conn, entity_id=""):
|
||||
try:
|
||||
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
handle_hass_get_state(conn, entity_id),
|
||||
conn.loop
|
||||
handle_hass_get_state(conn, entity_id), conn.loop
|
||||
)
|
||||
ha_response = future.result()
|
||||
return ActionResponse( Action.REQLLM, ha_response , None )
|
||||
return ActionResponse(Action.REQLLM, ha_response, None)
|
||||
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']
|
||||
ha_config = initialize_hass_handler(conn)
|
||||
api_key = ha_config.get("api_key")
|
||||
base_url = ha_config.get("base_url")
|
||||
url = f"{base_url}/api/states/{entity_id}"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
||||
response = requests.get(url, headers=headers)
|
||||
if response.status_code == 200:
|
||||
responsetext = '设备状态:' + response.json()['state'] + ' '
|
||||
responsetext = "设备状态:" + response.json()["state"] + " "
|
||||
logger.bind(tag=TAG).info(f"api返回内容: {response.json()}")
|
||||
|
||||
if 'media_title' in response.json()['attributes']:
|
||||
responsetext = responsetext+ '正在播放的是:'+str(response.json()['attributes']['media_title'])+' '
|
||||
if 'volume_level' in response.json()['attributes']:
|
||||
responsetext = responsetext+ '音量是:'+str(response.json()['attributes']['volume_level'])+' '
|
||||
if 'color_temp_kelvin' in response.json()['attributes']:
|
||||
responsetext = responsetext+ '色温是:'+str(response.json()['attributes']['color_temp_kelvin'])+' '
|
||||
if 'rgb_color' in response.json()['attributes']:
|
||||
responsetext = responsetext+ 'rgb颜色是:'+str(response.json()['attributes']['rgb_color'])+' '
|
||||
if 'brightness' in response.json()['attributes']:
|
||||
responsetext = responsetext+ '亮度是:'+str(response.json()['attributes']['brightness'])+' '
|
||||
if "media_title" in response.json()["attributes"]:
|
||||
responsetext = (
|
||||
responsetext
|
||||
+ "正在播放的是:"
|
||||
+ str(response.json()["attributes"]["media_title"])
|
||||
+ " "
|
||||
)
|
||||
if "volume_level" in response.json()["attributes"]:
|
||||
responsetext = (
|
||||
responsetext
|
||||
+ "音量是:"
|
||||
+ str(response.json()["attributes"]["volume_level"])
|
||||
+ " "
|
||||
)
|
||||
if "color_temp_kelvin" in response.json()["attributes"]:
|
||||
responsetext = (
|
||||
responsetext
|
||||
+ "色温是:"
|
||||
+ str(response.json()["attributes"]["color_temp_kelvin"])
|
||||
+ " "
|
||||
)
|
||||
if "rgb_color" in response.json()["attributes"]:
|
||||
responsetext = (
|
||||
responsetext
|
||||
+ "rgb颜色是:"
|
||||
+ str(response.json()["attributes"]["rgb_color"])
|
||||
+ " "
|
||||
)
|
||||
if "brightness" in response.json()["attributes"]:
|
||||
responsetext = (
|
||||
responsetext
|
||||
+ "亮度是:"
|
||||
+ str(response.json()["attributes"]["brightness"])
|
||||
+ " "
|
||||
)
|
||||
logger.bind(tag=TAG).info(f"查询返回内容: {responsetext}")
|
||||
return responsetext
|
||||
#return response.json()['attributes']
|
||||
#response.attributes
|
||||
# return response.json()['attributes']
|
||||
# response.attributes
|
||||
|
||||
else:
|
||||
return f"切换失败,错误码: {response.status_code}"
|
||||
|
||||
@@ -4,40 +4,49 @@ from core.utils.util import check_model_key
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
HASS_CACHE = {}
|
||||
|
||||
|
||||
def append_devices_to_prompt(conn):
|
||||
if conn.intent_type == "function_call":
|
||||
funcs = conn.config["Intent"][conn.config["selected_module"]["Intent"]].get(
|
||||
"functions", []
|
||||
)
|
||||
|
||||
config_source = (
|
||||
"home_assistant"
|
||||
if conn.config["plugins"].get("home_assistant")
|
||||
else "hass_get_state"
|
||||
)
|
||||
|
||||
if "hass_get_state" in funcs or "hass_set_state" in funcs:
|
||||
prompt = "\n下面是我家智能设备列表(位置,设备名,entity_id),可以通过homeassistant控制\n"
|
||||
devices = conn.config["plugins"]["home_assistant"].get("devices", [])
|
||||
if len(devices) == 0:
|
||||
return
|
||||
for device in devices:
|
||||
prompt += device + "\n"
|
||||
conn.prompt += prompt
|
||||
deviceStr = conn.config["plugins"].get(config_source, {}).get("devices", "")
|
||||
conn.prompt += prompt + deviceStr + "\n"
|
||||
# 更新提示词
|
||||
conn.dialogue.update_system_message(conn.prompt)
|
||||
|
||||
|
||||
def initialize_hass_handler(conn):
|
||||
global HASS_CACHE
|
||||
if HASS_CACHE == {}:
|
||||
if conn.load_function_plugin:
|
||||
funcs = conn.config["Intent"][conn.config["selected_module"]["Intent"]].get(
|
||||
"functions", []
|
||||
)
|
||||
if "hass_get_state" in funcs or "hass_set_state" in funcs:
|
||||
HASS_CACHE["base_url"] = conn.config["plugins"]["home_assistant"].get(
|
||||
"base_url"
|
||||
)
|
||||
HASS_CACHE["api_key"] = conn.config["plugins"]["home_assistant"].get(
|
||||
"api_key"
|
||||
)
|
||||
ha_config = {}
|
||||
if not conn.load_function_plugin:
|
||||
return ha_config
|
||||
|
||||
check_model_key("home_assistant", HASS_CACHE["api_key"])
|
||||
return HASS_CACHE
|
||||
# 确定配置来源
|
||||
config_source = (
|
||||
"home_assistant"
|
||||
if conn.config["plugins"].get("home_assistant")
|
||||
else "hass_get_state"
|
||||
)
|
||||
if not conn.config["plugins"].get(config_source):
|
||||
return ha_config
|
||||
|
||||
# 统一获取配置
|
||||
plugin_config = conn.config["plugins"][config_source]
|
||||
ha_config["base_url"] = plugin_config.get("base_url")
|
||||
ha_config["api_key"] = plugin_config.get("api_key")
|
||||
|
||||
# 统一检查API密钥
|
||||
model_key_msg = check_model_key("home_assistant", ha_config.get("api_key"))
|
||||
if model_key_msg:
|
||||
logger.bind(tag=TAG).error(model_key_msg)
|
||||
|
||||
return ha_config
|
||||
|
||||
@@ -17,46 +17,43 @@ hass_play_music_function_desc = {
|
||||
"properties": {
|
||||
"media_content_id": {
|
||||
"type": "string",
|
||||
"description": "可以是音乐或有声书的专辑名称、歌曲名、演唱者,如果未指定就填random"
|
||||
"description": "可以是音乐或有声书的专辑名称、歌曲名、演唱者,如果未指定就填random",
|
||||
},
|
||||
"entity_id": {
|
||||
"type": "string",
|
||||
"description": "需要操作的音箱的设备id,homeassistant里的entity_id,media_player开头"
|
||||
}
|
||||
"description": "需要操作的音箱的设备id,homeassistant里的entity_id,media_player开头",
|
||||
},
|
||||
},
|
||||
"required": ["media_content_id", "entity_id"]
|
||||
}
|
||||
}
|
||||
"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'):
|
||||
@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
|
||||
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)
|
||||
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']
|
||||
ha_config = initialize_hass_handler(conn)
|
||||
api_key = ha_config.get("api_key")
|
||||
base_url = ha_config.get("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
|
||||
}
|
||||
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}的音乐"
|
||||
|
||||
@@ -20,40 +20,40 @@ hass_set_state_function_desc = {
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "需要操作的动作,打开设备:turn_on,关闭设备:turn_off,增加亮度:brightness_up,降低亮度:brightness_down,设置亮度:brightness_value,增加音量:volume_up,降低音量:volume_down,设置音量:volume_set,设置色温:set_kelvin,设置颜色:set_color,设备暂停:pause,设备继续:continue,静音/取消静音:volume_mute"
|
||||
"description": "需要操作的动作,打开设备:turn_on,关闭设备:turn_off,增加亮度:brightness_up,降低亮度:brightness_down,设置亮度:brightness_value,增加音量:volume_up,降低音量:volume_down,设置音量:volume_set,设置色温:set_kelvin,设置颜色:set_color,设备暂停:pause,设备继续:continue,静音/取消静音:volume_mute",
|
||||
},
|
||||
"input": {
|
||||
"type": "integer",
|
||||
"description": "只有在设置音量,设置亮度时候才需要,有效值为1-100,对应音量和亮度的1%-100%"
|
||||
"description": "只有在设置音量,设置亮度时候才需要,有效值为1-100,对应音量和亮度的1%-100%",
|
||||
},
|
||||
"is_muted": {
|
||||
"type": "string",
|
||||
"description": "只有在设置静音操作时才需要,设置静音的时候该值为true,取消静音时该值为false"
|
||||
"description": "只有在设置静音操作时才需要,设置静音的时候该值为true,取消静音时该值为false",
|
||||
},
|
||||
"rgb_color": {
|
||||
"type": "array",
|
||||
"items": {"type": "integer"},
|
||||
"description": "只有在设置颜色时需要,这里填目标颜色的rgb值",
|
||||
},
|
||||
"rgb_color":{
|
||||
"type":"list",
|
||||
"description": "只有在设置颜色时需要,这里填目标颜色的rgb值"
|
||||
}
|
||||
},
|
||||
"required": ["type"]
|
||||
"required": ["type"],
|
||||
},
|
||||
"entity_id": {
|
||||
"type": "string",
|
||||
"description": "需要操作的设备id,homeassistant里的entity_id"
|
||||
}
|
||||
"description": "需要操作的设备id,homeassistant里的entity_id",
|
||||
},
|
||||
},
|
||||
"required": ["state", "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={}):
|
||||
@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
|
||||
handle_hass_set_state(conn, entity_id, state), conn.loop
|
||||
)
|
||||
ha_response = future.result()
|
||||
return ActionResponse(Action.REQLLM, ha_response, None)
|
||||
@@ -62,21 +62,21 @@ def hass_set_state(conn, entity_id='', state={}):
|
||||
|
||||
|
||||
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']
|
||||
'''
|
||||
ha_config = initialize_hass_handler(conn)
|
||||
api_key = ha_config.get("api_key")
|
||||
base_url = ha_config.get("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':
|
||||
action = ""
|
||||
arg = ""
|
||||
value = ""
|
||||
if state["type"] == "turn_on":
|
||||
description = "设备已打开"
|
||||
if domain == "cover":
|
||||
action = "open_cover"
|
||||
@@ -84,91 +84,87 @@ async def handle_hass_set_state(conn, entity_id, state):
|
||||
action = "start"
|
||||
else:
|
||||
action = "turn_on"
|
||||
elif state['type'] == 'turn_off':
|
||||
elif state["type"] == "turn_off":
|
||||
description = "设备已关闭"
|
||||
if domain == 'cover':
|
||||
if domain == "cover":
|
||||
action = "close_cover"
|
||||
elif domain == 'vacuum':
|
||||
elif domain == "vacuum":
|
||||
action = "stop"
|
||||
else:
|
||||
action = "turn_off"
|
||||
elif state['type'] == 'brightness_up':
|
||||
elif state["type"] == "brightness_up":
|
||||
description = "灯光已调亮"
|
||||
action = 'turn_on'
|
||||
arg = 'brightness_step_pct'
|
||||
action = "turn_on"
|
||||
arg = "brightness_step_pct"
|
||||
value = 10
|
||||
elif state['type'] == 'brightness_down':
|
||||
elif state["type"] == "brightness_down":
|
||||
description = "灯光已调暗"
|
||||
action = 'turn_on'
|
||||
arg = 'brightness_step_pct'
|
||||
action = "turn_on"
|
||||
arg = "brightness_step_pct"
|
||||
value = -10
|
||||
elif state['type'] == 'brightness_value':
|
||||
elif state["type"] == "brightness_value":
|
||||
description = f"亮度已调整到{state['input']}"
|
||||
action = 'turn_on'
|
||||
arg = 'brightness_pct'
|
||||
value = state['input']
|
||||
elif state['type'] == 'set_color':
|
||||
action = "turn_on"
|
||||
arg = "brightness_pct"
|
||||
value = state["input"]
|
||||
elif state["type"] == "set_color":
|
||||
description = f"颜色已调整到{state['rgb_color']}"
|
||||
action = 'turn_on'
|
||||
arg = 'rgb_color'
|
||||
value = state['rgb_color']
|
||||
elif state['type'] == 'set_kelvin':
|
||||
action = "turn_on"
|
||||
arg = "rgb_color"
|
||||
value = state["rgb_color"]
|
||||
elif state["type"] == "set_kelvin":
|
||||
description = f"色温已调整到{state['input']}K"
|
||||
action = 'turn_on'
|
||||
arg = 'kelvin'
|
||||
value = state['input']
|
||||
elif state['type'] == 'volume_up':
|
||||
action = "turn_on"
|
||||
arg = "kelvin"
|
||||
value = state["input"]
|
||||
elif state["type"] == "volume_up":
|
||||
description = "音量已调大"
|
||||
action = state['type']
|
||||
elif state['type'] == 'volume_down':
|
||||
action = state["type"]
|
||||
elif state["type"] == "volume_down":
|
||||
description = "音量已调小"
|
||||
action = state['type']
|
||||
elif state['type'] == 'volume_set':
|
||||
action = state["type"]
|
||||
elif state["type"] == "volume_set":
|
||||
description = f"音量已调整到{state['input']}"
|
||||
action = state['type']
|
||||
arg = 'volume_level'
|
||||
value = state['input']
|
||||
if state['input'] >= 1:
|
||||
value = state['input']/100
|
||||
elif state['type'] == 'volume_mute':
|
||||
action = state["type"]
|
||||
arg = "volume_level"
|
||||
value = state["input"]
|
||||
if state["input"] >= 1:
|
||||
value = state["input"] / 100
|
||||
elif state["type"] == "volume_mute":
|
||||
description = f"设备已静音"
|
||||
action = state['type']
|
||||
arg = 'is_volume_muted'
|
||||
value = state['is_muted']
|
||||
elif state['type'] == 'pause':
|
||||
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':
|
||||
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'
|
||||
if domain == "media_player":
|
||||
action = "media_play"
|
||||
if domain == "vacuum":
|
||||
action = "start"
|
||||
else:
|
||||
return f"{domain} {state.type}功能尚未支持"
|
||||
|
||||
if arg == '':
|
||||
if arg == "":
|
||||
data = {
|
||||
"entity_id": entity_id,
|
||||
}
|
||||
else:
|
||||
data = {
|
||||
"entity_id": entity_id,
|
||||
arg: value
|
||||
}
|
||||
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"
|
||||
}
|
||||
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
||||
response = requests.post(url, headers=headers, json=data)
|
||||
logger.bind(tag=TAG).info(f"设置状态:{description},url:{url},return_code:{response.status_code}")
|
||||
logger.bind(tag=TAG).info(
|
||||
f"设置状态:{description},url:{url},return_code:{response.status_code}"
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return description
|
||||
else:
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||
|
||||
plugin_loader_function_desc = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "plugin_loader",
|
||||
"description": "当用户想加载或卸载插件/function时,调用此函数:支持的插件列表为[plugins]",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"oper": {"type": "string", "description": "load or unload"},
|
||||
"name": {"type": "string", "description": "要加载或卸载的插件名字"},
|
||||
},
|
||||
"required": ["oper", "name"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@register_function("plugin_loader", plugin_loader_function_desc, ToolType.SYSTEM_CTL)
|
||||
def plugin_loader(conn, oper: str, name: str):
|
||||
"""插件加载"""
|
||||
if oper not in ["load", "unload"]:
|
||||
return ActionResponse(
|
||||
action=Action.RESPONSE, result="插件操作失败", response="不支持的操作"
|
||||
)
|
||||
|
||||
cur_support = conn.func_handler.current_support_functions()
|
||||
if oper == "load":
|
||||
if name in cur_support:
|
||||
return ActionResponse(
|
||||
action=Action.RESPONSE,
|
||||
result="插件加载失败",
|
||||
response=f"{name}插件已加载,无需重复加载",
|
||||
)
|
||||
func = conn.func_handler.function_registry.register_function(name)
|
||||
if not func:
|
||||
return ActionResponse(
|
||||
action=Action.RESPONSE, result="插件加载失败", response="插件未找到"
|
||||
)
|
||||
res = f"{name}插件加载成功"
|
||||
else:
|
||||
if name not in cur_support:
|
||||
return ActionResponse(
|
||||
action=Action.RESPONSE,
|
||||
result="插件卸载失败",
|
||||
response=f"{name}插件未加载",
|
||||
)
|
||||
bOK = conn.func_handler.function_registry.unregister_function(name)
|
||||
if not bOK:
|
||||
return ActionResponse(
|
||||
action=Action.RESPONSE, result="插件卸载失败", response="插件未找到"
|
||||
)
|
||||
res = f"{name}插件卸载成功"
|
||||
conn.func_handler.upload_functions_desc()
|
||||
return ActionResponse(action=Action.RESPONSE, result="插件操作成功", response=res)
|
||||
@@ -35,7 +35,7 @@ class Action(Enum):
|
||||
|
||||
|
||||
class ActionResponse:
|
||||
def __init__(self, action: Action, result, response):
|
||||
def __init__(self, action: Action, result=None, response=None):
|
||||
self.action = action # 动作类型
|
||||
self.result = result # 动作产生的结果
|
||||
self.response = response # 直接回复的内容
|
||||
|
||||
@@ -30,7 +30,7 @@ baidu-aip==4.16.13
|
||||
chardet==5.2.0
|
||||
aioconsole==0.8.1
|
||||
markitdown==0.1.1
|
||||
mcp-proxy==0.6.0
|
||||
mcp-proxy==0.8.0
|
||||
PyJWT==2.8.0
|
||||
psutil==7.0.0
|
||||
portalocker==2.10.1
|
||||
Reference in New Issue
Block a user