AstrBot 3.5.6__py3-none-any.whl → 4.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- astrbot/api/__init__.py +16 -4
- astrbot/api/all.py +2 -1
- astrbot/api/event/__init__.py +5 -6
- astrbot/api/event/filter/__init__.py +37 -34
- astrbot/api/platform/__init__.py +7 -8
- astrbot/api/provider/__init__.py +8 -7
- astrbot/api/star/__init__.py +3 -4
- astrbot/api/util/__init__.py +2 -2
- astrbot/cli/__init__.py +1 -0
- astrbot/cli/__main__.py +18 -197
- astrbot/cli/commands/__init__.py +6 -0
- astrbot/cli/commands/cmd_conf.py +209 -0
- astrbot/cli/commands/cmd_init.py +56 -0
- astrbot/cli/commands/cmd_plug.py +245 -0
- astrbot/cli/commands/cmd_run.py +62 -0
- astrbot/cli/utils/__init__.py +18 -0
- astrbot/cli/utils/basic.py +76 -0
- astrbot/cli/utils/plugin.py +246 -0
- astrbot/cli/utils/version_comparator.py +90 -0
- astrbot/core/__init__.py +17 -19
- astrbot/core/agent/agent.py +14 -0
- astrbot/core/agent/handoff.py +38 -0
- astrbot/core/agent/hooks.py +30 -0
- astrbot/core/agent/mcp_client.py +385 -0
- astrbot/core/agent/message.py +175 -0
- astrbot/core/agent/response.py +14 -0
- astrbot/core/agent/run_context.py +22 -0
- astrbot/core/agent/runners/__init__.py +3 -0
- astrbot/core/agent/runners/base.py +65 -0
- astrbot/core/agent/runners/coze/coze_agent_runner.py +367 -0
- astrbot/core/agent/runners/coze/coze_api_client.py +324 -0
- astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py +403 -0
- astrbot/core/agent/runners/dify/dify_agent_runner.py +336 -0
- astrbot/core/agent/runners/dify/dify_api_client.py +195 -0
- astrbot/core/agent/runners/tool_loop_agent_runner.py +400 -0
- astrbot/core/agent/tool.py +285 -0
- astrbot/core/agent/tool_executor.py +17 -0
- astrbot/core/astr_agent_context.py +19 -0
- astrbot/core/astr_agent_hooks.py +36 -0
- astrbot/core/astr_agent_run_util.py +80 -0
- astrbot/core/astr_agent_tool_exec.py +246 -0
- astrbot/core/astrbot_config_mgr.py +275 -0
- astrbot/core/config/__init__.py +2 -2
- astrbot/core/config/astrbot_config.py +60 -20
- astrbot/core/config/default.py +1972 -453
- astrbot/core/config/i18n_utils.py +110 -0
- astrbot/core/conversation_mgr.py +285 -75
- astrbot/core/core_lifecycle.py +167 -62
- astrbot/core/db/__init__.py +305 -102
- astrbot/core/db/migration/helper.py +69 -0
- astrbot/core/db/migration/migra_3_to_4.py +357 -0
- astrbot/core/db/migration/migra_45_to_46.py +44 -0
- astrbot/core/db/migration/migra_webchat_session.py +131 -0
- astrbot/core/db/migration/shared_preferences_v3.py +48 -0
- astrbot/core/db/migration/sqlite_v3.py +497 -0
- astrbot/core/db/po.py +259 -55
- astrbot/core/db/sqlite.py +773 -528
- astrbot/core/db/vec_db/base.py +73 -0
- astrbot/core/db/vec_db/faiss_impl/__init__.py +3 -0
- astrbot/core/db/vec_db/faiss_impl/document_storage.py +392 -0
- astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +93 -0
- astrbot/core/db/vec_db/faiss_impl/sqlite_init.sql +17 -0
- astrbot/core/db/vec_db/faiss_impl/vec_db.py +204 -0
- astrbot/core/event_bus.py +26 -22
- astrbot/core/exceptions.py +9 -0
- astrbot/core/file_token_service.py +98 -0
- astrbot/core/initial_loader.py +19 -10
- astrbot/core/knowledge_base/chunking/__init__.py +9 -0
- astrbot/core/knowledge_base/chunking/base.py +25 -0
- astrbot/core/knowledge_base/chunking/fixed_size.py +59 -0
- astrbot/core/knowledge_base/chunking/recursive.py +161 -0
- astrbot/core/knowledge_base/kb_db_sqlite.py +301 -0
- astrbot/core/knowledge_base/kb_helper.py +642 -0
- astrbot/core/knowledge_base/kb_mgr.py +330 -0
- astrbot/core/knowledge_base/models.py +120 -0
- astrbot/core/knowledge_base/parsers/__init__.py +13 -0
- astrbot/core/knowledge_base/parsers/base.py +51 -0
- astrbot/core/knowledge_base/parsers/markitdown_parser.py +26 -0
- astrbot/core/knowledge_base/parsers/pdf_parser.py +101 -0
- astrbot/core/knowledge_base/parsers/text_parser.py +42 -0
- astrbot/core/knowledge_base/parsers/url_parser.py +103 -0
- astrbot/core/knowledge_base/parsers/util.py +13 -0
- astrbot/core/knowledge_base/prompts.py +65 -0
- astrbot/core/knowledge_base/retrieval/__init__.py +14 -0
- astrbot/core/knowledge_base/retrieval/hit_stopwords.txt +767 -0
- astrbot/core/knowledge_base/retrieval/manager.py +276 -0
- astrbot/core/knowledge_base/retrieval/rank_fusion.py +142 -0
- astrbot/core/knowledge_base/retrieval/sparse_retriever.py +136 -0
- astrbot/core/log.py +21 -15
- astrbot/core/message/components.py +413 -287
- astrbot/core/message/message_event_result.py +35 -24
- astrbot/core/persona_mgr.py +192 -0
- astrbot/core/pipeline/__init__.py +14 -14
- astrbot/core/pipeline/content_safety_check/stage.py +13 -9
- astrbot/core/pipeline/content_safety_check/strategies/__init__.py +1 -2
- astrbot/core/pipeline/content_safety_check/strategies/baidu_aip.py +13 -14
- astrbot/core/pipeline/content_safety_check/strategies/keywords.py +2 -1
- astrbot/core/pipeline/content_safety_check/strategies/strategy.py +6 -6
- astrbot/core/pipeline/context.py +7 -1
- astrbot/core/pipeline/context_utils.py +107 -0
- astrbot/core/pipeline/preprocess_stage/stage.py +63 -36
- astrbot/core/pipeline/process_stage/method/agent_request.py +48 -0
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +464 -0
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +202 -0
- astrbot/core/pipeline/process_stage/method/star_request.py +26 -32
- astrbot/core/pipeline/process_stage/stage.py +21 -15
- astrbot/core/pipeline/process_stage/utils.py +125 -0
- astrbot/core/pipeline/rate_limit_check/stage.py +34 -36
- astrbot/core/pipeline/respond/stage.py +142 -101
- astrbot/core/pipeline/result_decorate/stage.py +124 -57
- astrbot/core/pipeline/scheduler.py +21 -16
- astrbot/core/pipeline/session_status_check/stage.py +37 -0
- astrbot/core/pipeline/stage.py +11 -76
- astrbot/core/pipeline/waking_check/stage.py +69 -33
- astrbot/core/pipeline/whitelist_check/stage.py +10 -7
- astrbot/core/platform/__init__.py +6 -6
- astrbot/core/platform/astr_message_event.py +107 -129
- astrbot/core/platform/astrbot_message.py +32 -12
- astrbot/core/platform/manager.py +62 -18
- astrbot/core/platform/message_session.py +30 -0
- astrbot/core/platform/platform.py +16 -24
- astrbot/core/platform/platform_metadata.py +9 -4
- astrbot/core/platform/register.py +12 -7
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +136 -60
- astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +126 -46
- astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +63 -31
- astrbot/core/platform/sources/dingtalk/dingtalk_event.py +30 -26
- astrbot/core/platform/sources/discord/client.py +129 -0
- astrbot/core/platform/sources/discord/components.py +139 -0
- astrbot/core/platform/sources/discord/discord_platform_adapter.py +473 -0
- astrbot/core/platform/sources/discord/discord_platform_event.py +313 -0
- astrbot/core/platform/sources/lark/lark_adapter.py +27 -18
- astrbot/core/platform/sources/lark/lark_event.py +39 -13
- astrbot/core/platform/sources/misskey/misskey_adapter.py +770 -0
- astrbot/core/platform/sources/misskey/misskey_api.py +964 -0
- astrbot/core/platform/sources/misskey/misskey_event.py +163 -0
- astrbot/core/platform/sources/misskey/misskey_utils.py +550 -0
- astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +149 -33
- astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +41 -26
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +36 -17
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_event.py +3 -1
- astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +14 -8
- astrbot/core/platform/sources/satori/satori_adapter.py +792 -0
- astrbot/core/platform/sources/satori/satori_event.py +432 -0
- astrbot/core/platform/sources/slack/client.py +164 -0
- astrbot/core/platform/sources/slack/slack_adapter.py +416 -0
- astrbot/core/platform/sources/slack/slack_event.py +253 -0
- astrbot/core/platform/sources/telegram/tg_adapter.py +100 -43
- astrbot/core/platform/sources/telegram/tg_event.py +136 -36
- astrbot/core/platform/sources/webchat/webchat_adapter.py +72 -22
- astrbot/core/platform/sources/webchat/webchat_event.py +46 -22
- astrbot/core/platform/sources/webchat/webchat_queue_mgr.py +35 -0
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +926 -0
- astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +178 -0
- astrbot/core/platform/sources/wechatpadpro/xml_data_parser.py +159 -0
- astrbot/core/platform/sources/wecom/wecom_adapter.py +169 -27
- astrbot/core/platform/sources/wecom/wecom_event.py +162 -77
- astrbot/core/platform/sources/wecom/wecom_kf.py +279 -0
- astrbot/core/platform/sources/wecom/wecom_kf_message.py +196 -0
- astrbot/core/platform/sources/wecom_ai_bot/WXBizJsonMsgCrypt.py +297 -0
- astrbot/core/platform/sources/wecom_ai_bot/__init__.py +15 -0
- astrbot/core/platform/sources/wecom_ai_bot/ierror.py +19 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +472 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_api.py +417 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +152 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_queue_mgr.py +153 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +168 -0
- astrbot/core/platform/sources/wecom_ai_bot/wecomai_utils.py +209 -0
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +306 -0
- astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +186 -0
- astrbot/core/platform_message_history_mgr.py +49 -0
- astrbot/core/provider/__init__.py +2 -3
- astrbot/core/provider/entites.py +8 -8
- astrbot/core/provider/entities.py +154 -98
- astrbot/core/provider/func_tool_manager.py +446 -458
- astrbot/core/provider/manager.py +345 -207
- astrbot/core/provider/provider.py +188 -73
- astrbot/core/provider/register.py +9 -7
- astrbot/core/provider/sources/anthropic_source.py +295 -115
- astrbot/core/provider/sources/azure_tts_source.py +224 -0
- astrbot/core/provider/sources/bailian_rerank_source.py +236 -0
- astrbot/core/provider/sources/dashscope_tts.py +138 -14
- astrbot/core/provider/sources/edge_tts_source.py +24 -19
- astrbot/core/provider/sources/fishaudio_tts_api_source.py +58 -13
- astrbot/core/provider/sources/gemini_embedding_source.py +61 -0
- astrbot/core/provider/sources/gemini_source.py +310 -132
- astrbot/core/provider/sources/gemini_tts_source.py +81 -0
- astrbot/core/provider/sources/groq_source.py +15 -0
- astrbot/core/provider/sources/gsv_selfhosted_source.py +151 -0
- astrbot/core/provider/sources/gsvi_tts_source.py +14 -7
- astrbot/core/provider/sources/minimax_tts_api_source.py +159 -0
- astrbot/core/provider/sources/openai_embedding_source.py +40 -0
- astrbot/core/provider/sources/openai_source.py +241 -145
- astrbot/core/provider/sources/openai_tts_api_source.py +18 -7
- astrbot/core/provider/sources/sensevoice_selfhosted_source.py +13 -11
- astrbot/core/provider/sources/vllm_rerank_source.py +71 -0
- astrbot/core/provider/sources/volcengine_tts.py +115 -0
- astrbot/core/provider/sources/whisper_api_source.py +18 -13
- astrbot/core/provider/sources/whisper_selfhosted_source.py +19 -12
- astrbot/core/provider/sources/xinference_rerank_source.py +116 -0
- astrbot/core/provider/sources/xinference_stt_provider.py +197 -0
- astrbot/core/provider/sources/zhipu_source.py +6 -73
- astrbot/core/star/__init__.py +43 -11
- astrbot/core/star/config.py +17 -18
- astrbot/core/star/context.py +362 -138
- astrbot/core/star/filter/__init__.py +4 -3
- astrbot/core/star/filter/command.py +111 -35
- astrbot/core/star/filter/command_group.py +46 -34
- astrbot/core/star/filter/custom_filter.py +6 -5
- astrbot/core/star/filter/event_message_type.py +4 -2
- astrbot/core/star/filter/permission.py +4 -2
- astrbot/core/star/filter/platform_adapter_type.py +45 -12
- astrbot/core/star/filter/regex.py +4 -2
- astrbot/core/star/register/__init__.py +19 -15
- astrbot/core/star/register/star.py +41 -13
- astrbot/core/star/register/star_handler.py +236 -86
- astrbot/core/star/session_llm_manager.py +280 -0
- astrbot/core/star/session_plugin_manager.py +170 -0
- astrbot/core/star/star.py +36 -43
- astrbot/core/star/star_handler.py +47 -85
- astrbot/core/star/star_manager.py +442 -260
- astrbot/core/star/star_tools.py +167 -45
- astrbot/core/star/updator.py +17 -20
- astrbot/core/umop_config_router.py +106 -0
- astrbot/core/updator.py +38 -13
- astrbot/core/utils/astrbot_path.py +39 -0
- astrbot/core/utils/command_parser.py +1 -1
- astrbot/core/utils/io.py +119 -60
- astrbot/core/utils/log_pipe.py +1 -1
- astrbot/core/utils/metrics.py +11 -10
- astrbot/core/utils/migra_helper.py +73 -0
- astrbot/core/utils/path_util.py +63 -62
- astrbot/core/utils/pip_installer.py +37 -15
- astrbot/core/utils/session_lock.py +29 -0
- astrbot/core/utils/session_waiter.py +19 -20
- astrbot/core/utils/shared_preferences.py +174 -34
- astrbot/core/utils/t2i/__init__.py +4 -1
- astrbot/core/utils/t2i/local_strategy.py +386 -238
- astrbot/core/utils/t2i/network_strategy.py +109 -49
- astrbot/core/utils/t2i/renderer.py +29 -14
- astrbot/core/utils/t2i/template/astrbot_powershell.html +184 -0
- astrbot/core/utils/t2i/template_manager.py +111 -0
- astrbot/core/utils/tencent_record_helper.py +115 -1
- astrbot/core/utils/version_comparator.py +10 -13
- astrbot/core/zip_updator.py +112 -65
- astrbot/dashboard/routes/__init__.py +20 -13
- astrbot/dashboard/routes/auth.py +20 -9
- astrbot/dashboard/routes/chat.py +297 -141
- astrbot/dashboard/routes/config.py +652 -55
- astrbot/dashboard/routes/conversation.py +107 -37
- astrbot/dashboard/routes/file.py +26 -0
- astrbot/dashboard/routes/knowledge_base.py +1244 -0
- astrbot/dashboard/routes/log.py +27 -2
- astrbot/dashboard/routes/persona.py +202 -0
- astrbot/dashboard/routes/plugin.py +197 -139
- astrbot/dashboard/routes/route.py +27 -7
- astrbot/dashboard/routes/session_management.py +354 -0
- astrbot/dashboard/routes/stat.py +85 -18
- astrbot/dashboard/routes/static_file.py +5 -2
- astrbot/dashboard/routes/t2i.py +233 -0
- astrbot/dashboard/routes/tools.py +184 -120
- astrbot/dashboard/routes/update.py +59 -36
- astrbot/dashboard/server.py +96 -36
- astrbot/dashboard/utils.py +165 -0
- astrbot-4.7.0.dist-info/METADATA +294 -0
- astrbot-4.7.0.dist-info/RECORD +274 -0
- {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/WHEEL +1 -1
- astrbot/core/db/plugin/sqlite_impl.py +0 -112
- astrbot/core/db/sqlite_init.sql +0 -50
- astrbot/core/pipeline/platform_compatibility/stage.py +0 -56
- astrbot/core/pipeline/process_stage/method/llm_request.py +0 -606
- astrbot/core/platform/sources/gewechat/client.py +0 -806
- astrbot/core/platform/sources/gewechat/downloader.py +0 -55
- astrbot/core/platform/sources/gewechat/gewechat_event.py +0 -255
- astrbot/core/platform/sources/gewechat/gewechat_platform_adapter.py +0 -103
- astrbot/core/platform/sources/gewechat/xml_data_parser.py +0 -110
- astrbot/core/provider/sources/dashscope_source.py +0 -203
- astrbot/core/provider/sources/dify_source.py +0 -281
- astrbot/core/provider/sources/llmtuner_source.py +0 -132
- astrbot/core/rag/embedding/openai_source.py +0 -20
- astrbot/core/rag/knowledge_db_mgr.py +0 -94
- astrbot/core/rag/store/__init__.py +0 -9
- astrbot/core/rag/store/chroma_db.py +0 -42
- astrbot/core/utils/dify_api_client.py +0 -152
- astrbot-3.5.6.dist-info/METADATA +0 -249
- astrbot-3.5.6.dist-info/RECORD +0 -158
- {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/entry_points.txt +0 -0
- {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import copy
|
|
2
5
|
import json
|
|
3
|
-
import textwrap
|
|
4
6
|
import os
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
+
from collections.abc import Awaitable, Callable
|
|
8
|
+
from typing import Any
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
from dataclasses import dataclass
|
|
10
|
-
from typing import Optional
|
|
11
|
-
from contextlib import AsyncExitStack
|
|
12
|
-
from astrbot import logger
|
|
13
|
-
from astrbot.core.utils.log_pipe import LogPipe
|
|
10
|
+
import aiohttp
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
from astrbot import logger
|
|
13
|
+
from astrbot.core import sp
|
|
14
|
+
from astrbot.core.agent.mcp_client import MCPClient, MCPTool
|
|
15
|
+
from astrbot.core.agent.tool import FunctionTool, ToolSet
|
|
16
|
+
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
|
20
17
|
|
|
21
18
|
DEFAULT_MCP_CONFIG = {"mcpServers": {}}
|
|
22
19
|
|
|
@@ -28,153 +25,122 @@ SUPPORTED_TYPES = [
|
|
|
28
25
|
"boolean",
|
|
29
26
|
] # json schema 支持的数据类型
|
|
30
27
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"""
|
|
35
|
-
|
|
36
|
-
"""
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
""
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
28
|
+
PY_TO_JSON_TYPE = {
|
|
29
|
+
"int": "number",
|
|
30
|
+
"float": "number",
|
|
31
|
+
"bool": "boolean",
|
|
32
|
+
"str": "string",
|
|
33
|
+
"dict": "object",
|
|
34
|
+
"list": "array",
|
|
35
|
+
"tuple": "array",
|
|
36
|
+
"set": "array",
|
|
37
|
+
}
|
|
38
|
+
# alias
|
|
39
|
+
FuncTool = FunctionTool
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _prepare_config(config: dict) -> dict:
|
|
43
|
+
"""准备配置,处理嵌套格式"""
|
|
44
|
+
if config.get("mcpServers"):
|
|
45
|
+
first_key = next(iter(config["mcpServers"]))
|
|
46
|
+
config = config["mcpServers"][first_key]
|
|
47
|
+
config.pop("active", None)
|
|
48
|
+
return config
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def _quick_test_mcp_connection(config: dict) -> tuple[bool, str]:
|
|
52
|
+
"""快速测试 MCP 服务器可达性"""
|
|
53
|
+
import aiohttp
|
|
54
|
+
|
|
55
|
+
cfg = _prepare_config(config.copy())
|
|
56
|
+
|
|
57
|
+
url = cfg["url"]
|
|
58
|
+
headers = cfg.get("headers", {})
|
|
59
|
+
timeout = cfg.get("timeout", 10)
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
async with aiohttp.ClientSession() as session:
|
|
63
|
+
if cfg.get("transport") == "streamable_http":
|
|
64
|
+
test_payload = {
|
|
65
|
+
"jsonrpc": "2.0",
|
|
66
|
+
"method": "initialize",
|
|
67
|
+
"id": 0,
|
|
68
|
+
"params": {
|
|
69
|
+
"protocolVersion": "2024-11-05",
|
|
70
|
+
"capabilities": {},
|
|
71
|
+
"clientInfo": {"name": "test-client", "version": "1.2.3"},
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
async with session.post(
|
|
75
|
+
url,
|
|
76
|
+
headers={
|
|
77
|
+
**headers,
|
|
78
|
+
"Content-Type": "application/json",
|
|
79
|
+
"Accept": "application/json, text/event-stream",
|
|
80
|
+
},
|
|
81
|
+
json=test_payload,
|
|
82
|
+
timeout=aiohttp.ClientTimeout(total=timeout),
|
|
83
|
+
) as response:
|
|
84
|
+
if response.status == 200:
|
|
85
|
+
return True, ""
|
|
86
|
+
return False, f"HTTP {response.status}: {response.reason}"
|
|
77
87
|
else:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
如果 `url` 参数存在,则使用 SSE 的方式连接到 MCP 服务。
|
|
98
|
-
|
|
99
|
-
Args:
|
|
100
|
-
mcp_server_config (dict): Configuration for the MCP server. See https://modelcontextprotocol.io/quickstart/server
|
|
101
|
-
"""
|
|
102
|
-
cfg = mcp_server_config.copy()
|
|
103
|
-
if "mcpServers" in cfg and len(cfg["mcpServers"]) > 0:
|
|
104
|
-
key_0 = list(cfg["mcpServers"].keys())[0]
|
|
105
|
-
cfg = cfg["mcpServers"][key_0]
|
|
106
|
-
cfg.pop("active", None) # Remove active flag from config
|
|
107
|
-
|
|
108
|
-
if "url" in cfg:
|
|
109
|
-
# SSE transport method
|
|
110
|
-
self._streams_context = sse_client(url=cfg["url"])
|
|
111
|
-
streams = await self._streams_context.__aenter__()
|
|
112
|
-
|
|
113
|
-
# Create a new client session
|
|
114
|
-
# self.session = await self._session_context.__aenter__()
|
|
115
|
-
self.session = await self.exit_stack.enter_async_context(
|
|
116
|
-
mcp.ClientSession(*streams)
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
else:
|
|
120
|
-
server_params = mcp.StdioServerParameters(
|
|
121
|
-
**cfg,
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
def callback(msg: str):
|
|
125
|
-
# 处理 MCP 服务的错误日志
|
|
126
|
-
self.server_errlogs.append(msg)
|
|
127
|
-
|
|
128
|
-
stdio_transport = await self.exit_stack.enter_async_context(
|
|
129
|
-
mcp.stdio_client(
|
|
130
|
-
server_params,
|
|
131
|
-
errlog=LogPipe(
|
|
132
|
-
level=logging.ERROR,
|
|
133
|
-
logger=logger,
|
|
134
|
-
identifier=f"MCPServer-{name}",
|
|
135
|
-
callback=callback,
|
|
136
|
-
),
|
|
137
|
-
),
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
# Create a new client session
|
|
141
|
-
self.session = await self.exit_stack.enter_async_context(
|
|
142
|
-
mcp.ClientSession(*stdio_transport)
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
await self.session.initialize()
|
|
146
|
-
|
|
147
|
-
async def list_tools_and_save(self) -> mcp.ListToolsResult:
|
|
148
|
-
"""List all tools from the server and save them to self.tools"""
|
|
149
|
-
response = await self.session.list_tools()
|
|
150
|
-
logger.debug(f"MCP server {self.name} list tools response: {response}")
|
|
151
|
-
self.tools = response.tools
|
|
152
|
-
return response
|
|
153
|
-
|
|
154
|
-
async def cleanup(self):
|
|
155
|
-
"""Clean up resources"""
|
|
156
|
-
await self.exit_stack.aclose()
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
class FuncCall:
|
|
88
|
+
async with session.get(
|
|
89
|
+
url,
|
|
90
|
+
headers={
|
|
91
|
+
**headers,
|
|
92
|
+
"Accept": "application/json, text/event-stream",
|
|
93
|
+
},
|
|
94
|
+
timeout=aiohttp.ClientTimeout(total=timeout),
|
|
95
|
+
) as response:
|
|
96
|
+
if response.status == 200:
|
|
97
|
+
return True, ""
|
|
98
|
+
return False, f"HTTP {response.status}: {response.reason}"
|
|
99
|
+
|
|
100
|
+
except asyncio.TimeoutError:
|
|
101
|
+
return False, f"连接超时: {timeout}秒"
|
|
102
|
+
except Exception as e:
|
|
103
|
+
return False, f"{e!s}"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class FunctionToolManager:
|
|
160
107
|
def __init__(self) -> None:
|
|
161
|
-
self.func_list:
|
|
162
|
-
|
|
163
|
-
self.mcp_client_dict: Dict[str, MCPClient] = {}
|
|
108
|
+
self.func_list: list[FuncTool] = []
|
|
109
|
+
self.mcp_client_dict: dict[str, MCPClient] = {}
|
|
164
110
|
"""MCP 服务列表"""
|
|
165
|
-
self.
|
|
166
|
-
"""用于外部控制 MCP 服务的启停"""
|
|
167
|
-
self.mcp_client_event: Dict[str, asyncio.Event] = {}
|
|
111
|
+
self.mcp_client_event: dict[str, asyncio.Event] = {}
|
|
168
112
|
|
|
169
113
|
def empty(self) -> bool:
|
|
170
114
|
return len(self.func_list) == 0
|
|
171
115
|
|
|
116
|
+
def spec_to_func(
|
|
117
|
+
self,
|
|
118
|
+
name: str,
|
|
119
|
+
func_args: list[dict],
|
|
120
|
+
desc: str,
|
|
121
|
+
handler: Callable[..., Awaitable[Any]],
|
|
122
|
+
) -> FuncTool:
|
|
123
|
+
params = {
|
|
124
|
+
"type": "object", # hard-coded here
|
|
125
|
+
"properties": {},
|
|
126
|
+
}
|
|
127
|
+
for param in func_args:
|
|
128
|
+
p = copy.deepcopy(param)
|
|
129
|
+
p.pop("name", None)
|
|
130
|
+
params["properties"][param["name"]] = p
|
|
131
|
+
return FuncTool(
|
|
132
|
+
name=name,
|
|
133
|
+
parameters=params,
|
|
134
|
+
description=desc,
|
|
135
|
+
handler=handler,
|
|
136
|
+
)
|
|
137
|
+
|
|
172
138
|
def add_func(
|
|
173
139
|
self,
|
|
174
140
|
name: str,
|
|
175
141
|
func_args: list,
|
|
176
142
|
desc: str,
|
|
177
|
-
handler: Awaitable,
|
|
143
|
+
handler: Callable[..., Awaitable[Any]],
|
|
178
144
|
) -> None:
|
|
179
145
|
"""添加函数调用工具
|
|
180
146
|
|
|
@@ -186,40 +152,34 @@ class FuncCall:
|
|
|
186
152
|
# check if the tool has been added before
|
|
187
153
|
self.remove_func(name)
|
|
188
154
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
"description": param["description"],
|
|
197
|
-
}
|
|
198
|
-
_func = FuncTool(
|
|
199
|
-
name=name,
|
|
200
|
-
parameters=params,
|
|
201
|
-
description=desc,
|
|
202
|
-
handler=handler,
|
|
155
|
+
self.func_list.append(
|
|
156
|
+
self.spec_to_func(
|
|
157
|
+
name=name,
|
|
158
|
+
func_args=func_args,
|
|
159
|
+
desc=desc,
|
|
160
|
+
handler=handler,
|
|
161
|
+
),
|
|
203
162
|
)
|
|
204
|
-
self.func_list.append(_func)
|
|
205
163
|
logger.info(f"添加函数调用工具: {name}")
|
|
206
164
|
|
|
207
165
|
def remove_func(self, name: str) -> None:
|
|
208
|
-
"""
|
|
209
|
-
删除一个函数调用工具。
|
|
210
|
-
"""
|
|
166
|
+
"""删除一个函数调用工具。"""
|
|
211
167
|
for i, f in enumerate(self.func_list):
|
|
212
168
|
if f.name == name:
|
|
213
169
|
self.func_list.pop(i)
|
|
214
170
|
break
|
|
215
171
|
|
|
216
|
-
def get_func(self, name) -> FuncTool:
|
|
172
|
+
def get_func(self, name) -> FuncTool | None:
|
|
217
173
|
for f in self.func_list:
|
|
218
174
|
if f.name == name:
|
|
219
175
|
return f
|
|
220
|
-
return None
|
|
221
176
|
|
|
222
|
-
|
|
177
|
+
def get_full_tool_set(self) -> ToolSet:
|
|
178
|
+
"""获取完整工具集"""
|
|
179
|
+
tool_set = ToolSet(self.func_list.copy())
|
|
180
|
+
return tool_set
|
|
181
|
+
|
|
182
|
+
async def init_mcp_clients(self) -> None:
|
|
223
183
|
"""从项目根目录读取 mcp_server.json 文件,初始化 MCP 服务列表。文件格式如下:
|
|
224
184
|
```
|
|
225
185
|
{
|
|
@@ -238,8 +198,7 @@ class FuncCall:
|
|
|
238
198
|
}
|
|
239
199
|
```
|
|
240
200
|
"""
|
|
241
|
-
|
|
242
|
-
data_dir = os.path.abspath(os.path.join(current_dir, "../../../data"))
|
|
201
|
+
data_dir = get_astrbot_data_path()
|
|
243
202
|
|
|
244
203
|
mcp_json_file = os.path.join(data_dir, "mcp_server.json")
|
|
245
204
|
if not os.path.exists(mcp_json_file):
|
|
@@ -249,342 +208,372 @@ class FuncCall:
|
|
|
249
208
|
logger.info(f"未找到 MCP 服务配置文件,已创建默认配置文件 {mcp_json_file}")
|
|
250
209
|
return
|
|
251
210
|
|
|
252
|
-
mcp_server_json_obj:
|
|
253
|
-
open(mcp_json_file,
|
|
211
|
+
mcp_server_json_obj: dict[str, dict] = json.load(
|
|
212
|
+
open(mcp_json_file, encoding="utf-8"),
|
|
254
213
|
)["mcpServers"]
|
|
255
214
|
|
|
256
|
-
for name in mcp_server_json_obj
|
|
215
|
+
for name in mcp_server_json_obj:
|
|
257
216
|
cfg = mcp_server_json_obj[name]
|
|
258
217
|
if cfg.get("active", True):
|
|
259
218
|
event = asyncio.Event()
|
|
260
219
|
asyncio.create_task(
|
|
261
|
-
self._init_mcp_client_task_wrapper(name, cfg, event)
|
|
220
|
+
self._init_mcp_client_task_wrapper(name, cfg, event),
|
|
262
221
|
)
|
|
263
222
|
self.mcp_client_event[name] = event
|
|
264
223
|
|
|
265
|
-
async def mcp_service_selector(self):
|
|
266
|
-
"""为了避免在不同异步任务中控制 MCP 服务导致的报错,整个项目统一通过这个 Task 来控制
|
|
267
|
-
|
|
268
|
-
使用 self.mcp_service_queue.put_nowait() 来控制 MCP 服务的启停,数据格式如下:
|
|
269
|
-
|
|
270
|
-
{"type": "init"} 初始化所有MCP客户端
|
|
271
|
-
|
|
272
|
-
{"type": "init", "name": "mcp_server_name", "cfg": {...}} 初始化指定的MCP客户端
|
|
273
|
-
|
|
274
|
-
{"type": "terminate"} 终止所有MCP客户端
|
|
275
|
-
|
|
276
|
-
{"type": "terminate", "name": "mcp_server_name"} 终止指定的MCP客户端
|
|
277
|
-
"""
|
|
278
|
-
while True:
|
|
279
|
-
data = await self.mcp_service_queue.get()
|
|
280
|
-
if data["type"] == "init":
|
|
281
|
-
if "name" in data:
|
|
282
|
-
event = asyncio.Event()
|
|
283
|
-
asyncio.create_task(
|
|
284
|
-
self._init_mcp_client_task_wrapper(
|
|
285
|
-
data["name"], data["cfg"], event
|
|
286
|
-
)
|
|
287
|
-
)
|
|
288
|
-
self.mcp_client_event[data["name"]] = event
|
|
289
|
-
else:
|
|
290
|
-
await self._init_mcp_clients()
|
|
291
|
-
elif data["type"] == "terminate":
|
|
292
|
-
if "name" in data:
|
|
293
|
-
# await self._terminate_mcp_client(data["name"])
|
|
294
|
-
if data["name"] in self.mcp_client_event:
|
|
295
|
-
self.mcp_client_event[data["name"]].set()
|
|
296
|
-
self.mcp_client_event.pop(data["name"], None)
|
|
297
|
-
self.func_list = [
|
|
298
|
-
f
|
|
299
|
-
for f in self.func_list
|
|
300
|
-
if not (
|
|
301
|
-
f.origin == "mcp" and f.mcp_server_name == data["name"]
|
|
302
|
-
)
|
|
303
|
-
]
|
|
304
|
-
else:
|
|
305
|
-
for name in self.mcp_client_dict.keys():
|
|
306
|
-
# await self._terminate_mcp_client(name)
|
|
307
|
-
# self.mcp_client_event[name].set()
|
|
308
|
-
if name in self.mcp_client_event:
|
|
309
|
-
self.mcp_client_event[name].set()
|
|
310
|
-
self.mcp_client_event.pop(name, None)
|
|
311
|
-
self.func_list = [f for f in self.func_list if f.origin != "mcp"]
|
|
312
|
-
|
|
313
224
|
async def _init_mcp_client_task_wrapper(
|
|
314
|
-
self,
|
|
225
|
+
self,
|
|
226
|
+
name: str,
|
|
227
|
+
cfg: dict,
|
|
228
|
+
event: asyncio.Event,
|
|
229
|
+
ready_future: asyncio.Future | None = None,
|
|
315
230
|
) -> None:
|
|
316
231
|
"""初始化 MCP 客户端的包装函数,用于捕获异常"""
|
|
317
232
|
try:
|
|
318
233
|
await self._init_mcp_client(name, cfg)
|
|
234
|
+
tools = await self.mcp_client_dict[name].list_tools_and_save()
|
|
235
|
+
if ready_future and not ready_future.done():
|
|
236
|
+
# tell the caller we are ready
|
|
237
|
+
ready_future.set_result(tools)
|
|
319
238
|
await event.wait()
|
|
320
239
|
logger.info(f"收到 MCP 客户端 {name} 终止信号")
|
|
321
|
-
await self._terminate_mcp_client(name)
|
|
322
240
|
except Exception as e:
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
241
|
+
logger.error(f"初始化 MCP 客户端 {name} 失败", exc_info=True)
|
|
242
|
+
if ready_future and not ready_future.done():
|
|
243
|
+
ready_future.set_exception(e)
|
|
244
|
+
finally:
|
|
245
|
+
# 无论如何都能清理
|
|
246
|
+
await self._terminate_mcp_client(name)
|
|
327
247
|
|
|
328
248
|
async def _init_mcp_client(self, name: str, config: dict) -> None:
|
|
329
249
|
"""初始化单个MCP客户端"""
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
await self._terminate_mcp_client(name)
|
|
334
|
-
|
|
335
|
-
mcp_client = MCPClient()
|
|
336
|
-
mcp_client.name = name
|
|
337
|
-
self.mcp_client_dict[name] = mcp_client
|
|
338
|
-
await mcp_client.connect_to_server(config, name)
|
|
339
|
-
tools_res = await mcp_client.list_tools_and_save()
|
|
340
|
-
tool_names = [tool.name for tool in tools_res.tools]
|
|
341
|
-
|
|
342
|
-
# 移除该MCP服务之前的工具(如有)
|
|
343
|
-
self.func_list = [
|
|
344
|
-
f
|
|
345
|
-
for f in self.func_list
|
|
346
|
-
if not (f.origin == "mcp" and f.mcp_server_name == name)
|
|
347
|
-
]
|
|
250
|
+
# 先清理之前的客户端,如果存在
|
|
251
|
+
if name in self.mcp_client_dict:
|
|
252
|
+
await self._terminate_mcp_client(name)
|
|
348
253
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
254
|
+
mcp_client = MCPClient()
|
|
255
|
+
mcp_client.name = name
|
|
256
|
+
self.mcp_client_dict[name] = mcp_client
|
|
257
|
+
await mcp_client.connect_to_server(config, name)
|
|
258
|
+
tools_res = await mcp_client.list_tools_and_save()
|
|
259
|
+
logger.debug(f"MCP server {name} list tools response: {tools_res}")
|
|
260
|
+
tool_names = [tool.name for tool in tools_res.tools]
|
|
261
|
+
|
|
262
|
+
# 移除该MCP服务之前的工具(如有)
|
|
263
|
+
self.func_list = [
|
|
264
|
+
f
|
|
265
|
+
for f in self.func_list
|
|
266
|
+
if not (isinstance(f, MCPTool) and f.mcp_server_name == name)
|
|
267
|
+
]
|
|
360
268
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
269
|
+
# 将 MCP 工具转换为 FuncTool 并添加到 func_list
|
|
270
|
+
for tool in mcp_client.tools:
|
|
271
|
+
func_tool = MCPTool(
|
|
272
|
+
mcp_tool=tool,
|
|
273
|
+
mcp_client=mcp_client,
|
|
274
|
+
mcp_server_name=name,
|
|
275
|
+
)
|
|
276
|
+
self.func_list.append(func_tool)
|
|
365
277
|
|
|
366
|
-
|
|
367
|
-
logger.error(f"初始化 MCP 客户端 {name} 失败: {e}")
|
|
368
|
-
# 发生错误时确保客户端被清理
|
|
369
|
-
if name in self.mcp_client_dict:
|
|
370
|
-
await self._terminate_mcp_client(name)
|
|
371
|
-
return
|
|
278
|
+
logger.info(f"已连接 MCP 服务 {name}, Tools: {tool_names}")
|
|
372
279
|
|
|
373
280
|
async def _terminate_mcp_client(self, name: str) -> None:
|
|
374
281
|
"""关闭并清理MCP客户端"""
|
|
375
282
|
if name in self.mcp_client_dict:
|
|
283
|
+
client = self.mcp_client_dict[name]
|
|
376
284
|
try:
|
|
377
285
|
# 关闭MCP连接
|
|
378
|
-
await
|
|
379
|
-
del self.mcp_client_dict[name]
|
|
286
|
+
await client.cleanup()
|
|
380
287
|
except Exception as e:
|
|
381
|
-
logger.
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
288
|
+
logger.error(f"清空 MCP 客户端资源 {name}: {e}。")
|
|
289
|
+
finally:
|
|
290
|
+
# Remove client from dict after cleanup attempt (successful or not)
|
|
291
|
+
self.mcp_client_dict.pop(name, None)
|
|
292
|
+
# 移除关联的FuncTool
|
|
293
|
+
self.func_list = [
|
|
294
|
+
f
|
|
295
|
+
for f in self.func_list
|
|
296
|
+
if not (isinstance(f, MCPTool) and f.mcp_server_name == name)
|
|
297
|
+
]
|
|
298
|
+
logger.info(f"已关闭 MCP 服务 {name}")
|
|
299
|
+
|
|
300
|
+
@staticmethod
|
|
301
|
+
async def test_mcp_server_connection(config: dict) -> list[str]:
|
|
302
|
+
if "url" in config:
|
|
303
|
+
success, error_msg = await _quick_test_mcp_connection(config)
|
|
304
|
+
if not success:
|
|
305
|
+
raise Exception(error_msg)
|
|
306
|
+
|
|
307
|
+
mcp_client = MCPClient()
|
|
308
|
+
try:
|
|
309
|
+
logger.debug(f"testing MCP server connection with config: {config}")
|
|
310
|
+
await mcp_client.connect_to_server(config, "test")
|
|
311
|
+
tools_res = await mcp_client.list_tools_and_save()
|
|
312
|
+
tool_names = [tool.name for tool in tools_res.tools]
|
|
313
|
+
finally:
|
|
314
|
+
logger.debug("Cleaning up MCP client after testing connection.")
|
|
315
|
+
await mcp_client.cleanup()
|
|
316
|
+
return tool_names
|
|
389
317
|
|
|
390
|
-
def
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
func_ = {
|
|
400
|
-
"type": "function",
|
|
401
|
-
"function": {
|
|
402
|
-
"name": f.name,
|
|
403
|
-
# "parameters": f.parameters,
|
|
404
|
-
"description": f.description,
|
|
405
|
-
},
|
|
406
|
-
}
|
|
407
|
-
func_["function"]["parameters"] = f.parameters
|
|
408
|
-
if not f.parameters.get("properties") and omit_empty_parameter_field:
|
|
409
|
-
# 如果 properties 为空,并且 omit_empty_parameter_field 为 True,则删除 parameters 字段
|
|
410
|
-
del func_["function"]["parameters"]
|
|
411
|
-
_l.append(func_)
|
|
412
|
-
return _l
|
|
318
|
+
async def enable_mcp_server(
|
|
319
|
+
self,
|
|
320
|
+
name: str,
|
|
321
|
+
config: dict,
|
|
322
|
+
event: asyncio.Event | None = None,
|
|
323
|
+
ready_future: asyncio.Future | None = None,
|
|
324
|
+
timeout: int = 30,
|
|
325
|
+
) -> None:
|
|
326
|
+
"""Enable_mcp_server a new MCP server to the manager and initialize it.
|
|
413
327
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
tool = {
|
|
425
|
-
"name": f.name,
|
|
426
|
-
"description": f.description,
|
|
427
|
-
"input_schema": {
|
|
428
|
-
"type": "object",
|
|
429
|
-
"properties": f.parameters.get("properties", {}),
|
|
430
|
-
# Keep the required field from the original parameters if it exists
|
|
431
|
-
"required": f.parameters.get("required", []),
|
|
432
|
-
},
|
|
433
|
-
}
|
|
434
|
-
tools.append(tool)
|
|
435
|
-
return tools
|
|
328
|
+
Args:
|
|
329
|
+
name (str): The name of the MCP server.
|
|
330
|
+
config (dict): Configuration for the MCP server.
|
|
331
|
+
event (asyncio.Event): Event to signal when the MCP client is ready.
|
|
332
|
+
ready_future (asyncio.Future): Future to signal when the MCP client is ready.
|
|
333
|
+
timeout (int): Timeout for the initialization.
|
|
334
|
+
|
|
335
|
+
Raises:
|
|
336
|
+
TimeoutError: If the initialization does not complete within the specified timeout.
|
|
337
|
+
Exception: If there is an error during initialization.
|
|
436
338
|
|
|
437
|
-
def get_func_desc_google_genai_style(self) -> dict:
|
|
438
|
-
"""
|
|
439
|
-
获得 Google GenAI API 风格的**已经激活**的工具描述
|
|
440
339
|
"""
|
|
340
|
+
if not event:
|
|
341
|
+
event = asyncio.Event()
|
|
342
|
+
if not ready_future:
|
|
343
|
+
ready_future = asyncio.Future()
|
|
344
|
+
if name in self.mcp_client_dict:
|
|
345
|
+
return
|
|
346
|
+
asyncio.create_task(
|
|
347
|
+
self._init_mcp_client_task_wrapper(name, config, event, ready_future),
|
|
348
|
+
)
|
|
349
|
+
try:
|
|
350
|
+
await asyncio.wait_for(ready_future, timeout=timeout)
|
|
351
|
+
finally:
|
|
352
|
+
self.mcp_client_event[name] = event
|
|
441
353
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
"integer",
|
|
447
|
-
"boolean",
|
|
448
|
-
"array",
|
|
449
|
-
"object",
|
|
450
|
-
"null",
|
|
451
|
-
}
|
|
452
|
-
supported_formats = {
|
|
453
|
-
"string": {"enum", "date-time"},
|
|
454
|
-
"integer": {"int32", "int64"},
|
|
455
|
-
"number": {"float", "double"},
|
|
456
|
-
}
|
|
354
|
+
if ready_future.done() and ready_future.exception():
|
|
355
|
+
exc = ready_future.exception()
|
|
356
|
+
if exc is not None:
|
|
357
|
+
raise exc
|
|
457
358
|
|
|
458
|
-
|
|
459
|
-
|
|
359
|
+
async def disable_mcp_server(
|
|
360
|
+
self,
|
|
361
|
+
name: str | None = None,
|
|
362
|
+
timeout: float = 10,
|
|
363
|
+
) -> None:
|
|
364
|
+
"""Disable an MCP server by its name.
|
|
460
365
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
366
|
+
Args:
|
|
367
|
+
name (str): The name of the MCP server to disable. If None, ALL MCP servers will be disabled.
|
|
368
|
+
timeout (int): Timeout.
|
|
464
369
|
|
|
465
|
-
|
|
370
|
+
"""
|
|
371
|
+
if name:
|
|
372
|
+
if name not in self.mcp_client_event:
|
|
373
|
+
return
|
|
374
|
+
client = self.mcp_client_dict.get(name)
|
|
375
|
+
self.mcp_client_event[name].set()
|
|
376
|
+
if not client:
|
|
377
|
+
return
|
|
378
|
+
client_running_event = client.running_event
|
|
379
|
+
try:
|
|
380
|
+
await asyncio.wait_for(client_running_event.wait(), timeout=timeout)
|
|
381
|
+
finally:
|
|
382
|
+
self.mcp_client_event.pop(name, None)
|
|
383
|
+
self.func_list = [
|
|
384
|
+
f
|
|
385
|
+
for f in self.func_list
|
|
386
|
+
if not (isinstance(f, MCPTool) and f.mcp_server_name == name)
|
|
387
|
+
]
|
|
388
|
+
else:
|
|
389
|
+
running_events = [
|
|
390
|
+
client.running_event.wait() for client in self.mcp_client_dict.values()
|
|
391
|
+
]
|
|
392
|
+
for key, event in self.mcp_client_event.items():
|
|
393
|
+
event.set()
|
|
394
|
+
# waiting for all clients to finish
|
|
395
|
+
try:
|
|
396
|
+
await asyncio.wait_for(asyncio.gather(*running_events), timeout=timeout)
|
|
397
|
+
finally:
|
|
398
|
+
self.mcp_client_event.clear()
|
|
399
|
+
self.mcp_client_dict.clear()
|
|
400
|
+
self.func_list = [
|
|
401
|
+
f for f in self.func_list if not isinstance(f, MCPTool)
|
|
402
|
+
]
|
|
466
403
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
# 暂时指定默认为null
|
|
475
|
-
result["type"] = "null"
|
|
476
|
-
|
|
477
|
-
support_fields = {
|
|
478
|
-
"title",
|
|
479
|
-
"description",
|
|
480
|
-
"enum",
|
|
481
|
-
"minimum",
|
|
482
|
-
"maximum",
|
|
483
|
-
"maxItems",
|
|
484
|
-
"minItems",
|
|
485
|
-
"nullable",
|
|
486
|
-
"required",
|
|
487
|
-
}
|
|
488
|
-
result.update({k: schema[k] for k in support_fields if k in schema})
|
|
404
|
+
def get_func_desc_openai_style(self, omit_empty_parameter_field=False) -> list:
|
|
405
|
+
"""获得 OpenAI API 风格的**已经激活**的工具描述"""
|
|
406
|
+
tools = [f for f in self.func_list if f.active]
|
|
407
|
+
toolset = ToolSet(tools)
|
|
408
|
+
return toolset.openai_schema(
|
|
409
|
+
omit_empty_parameter_field=omit_empty_parameter_field,
|
|
410
|
+
)
|
|
489
411
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
del prop_value["default"]
|
|
496
|
-
properties[key] = prop_value
|
|
412
|
+
def get_func_desc_anthropic_style(self) -> list:
|
|
413
|
+
"""获得 Anthropic API 风格的**已经激活**的工具描述"""
|
|
414
|
+
tools = [f for f in self.func_list if f.active]
|
|
415
|
+
toolset = ToolSet(tools)
|
|
416
|
+
return toolset.anthropic_schema()
|
|
497
417
|
|
|
498
|
-
|
|
499
|
-
|
|
418
|
+
def get_func_desc_google_genai_style(self) -> dict:
|
|
419
|
+
"""获得 Google GenAI API 风格的**已经激活**的工具描述"""
|
|
420
|
+
tools = [f for f in self.func_list if f.active]
|
|
421
|
+
toolset = ToolSet(tools)
|
|
422
|
+
return toolset.google_schema()
|
|
500
423
|
|
|
501
|
-
|
|
502
|
-
|
|
424
|
+
def deactivate_llm_tool(self, name: str) -> bool:
|
|
425
|
+
"""停用一个已经注册的函数调用工具。
|
|
503
426
|
|
|
504
|
-
|
|
427
|
+
Returns:
|
|
428
|
+
如果没找到,会返回 False
|
|
505
429
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
430
|
+
"""
|
|
431
|
+
func_tool = self.get_func(name)
|
|
432
|
+
if func_tool is not None:
|
|
433
|
+
func_tool.active = False
|
|
434
|
+
|
|
435
|
+
inactivated_llm_tools: list = sp.get(
|
|
436
|
+
"inactivated_llm_tools",
|
|
437
|
+
[],
|
|
438
|
+
scope="global",
|
|
439
|
+
scope_id="global",
|
|
440
|
+
)
|
|
441
|
+
if name not in inactivated_llm_tools:
|
|
442
|
+
inactivated_llm_tools.append(name)
|
|
443
|
+
sp.put(
|
|
444
|
+
"inactivated_llm_tools",
|
|
445
|
+
inactivated_llm_tools,
|
|
446
|
+
scope="global",
|
|
447
|
+
scope_id="global",
|
|
448
|
+
)
|
|
515
449
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
450
|
+
return True
|
|
451
|
+
return False
|
|
452
|
+
|
|
453
|
+
# 因为不想解决循环引用,所以这里直接传入 star_map 先了...
|
|
454
|
+
def activate_llm_tool(self, name: str, star_map: dict) -> bool:
|
|
455
|
+
func_tool = self.get_func(name)
|
|
456
|
+
if func_tool is not None:
|
|
457
|
+
if func_tool.handler_module_path in star_map:
|
|
458
|
+
if not star_map[func_tool.handler_module_path].activated:
|
|
459
|
+
raise ValueError(
|
|
460
|
+
f"此函数调用工具所属的插件 {star_map[func_tool.handler_module_path].name} 已被禁用,请先在管理面板启用再激活此工具。",
|
|
461
|
+
)
|
|
520
462
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
"name": f.name,
|
|
529
|
-
"parameters": f.parameters,
|
|
530
|
-
"description": f.description,
|
|
531
|
-
}
|
|
463
|
+
func_tool.active = True
|
|
464
|
+
|
|
465
|
+
inactivated_llm_tools: list = sp.get(
|
|
466
|
+
"inactivated_llm_tools",
|
|
467
|
+
[],
|
|
468
|
+
scope="global",
|
|
469
|
+
scope_id="global",
|
|
532
470
|
)
|
|
533
|
-
|
|
471
|
+
if name in inactivated_llm_tools:
|
|
472
|
+
inactivated_llm_tools.remove(name)
|
|
473
|
+
sp.put(
|
|
474
|
+
"inactivated_llm_tools",
|
|
475
|
+
inactivated_llm_tools,
|
|
476
|
+
scope="global",
|
|
477
|
+
scope_id="global",
|
|
478
|
+
)
|
|
534
479
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
你是一个 Function calling AI Agent, 你的任务是将用户的提问转化为函数调用。
|
|
480
|
+
return True
|
|
481
|
+
return False
|
|
538
482
|
|
|
539
|
-
|
|
540
|
-
|
|
483
|
+
@property
|
|
484
|
+
def mcp_config_path(self):
|
|
485
|
+
data_dir = get_astrbot_data_path()
|
|
486
|
+
return os.path.join(data_dir, "mcp_server.json")
|
|
541
487
|
|
|
542
|
-
|
|
488
|
+
def load_mcp_config(self):
|
|
489
|
+
if not os.path.exists(self.mcp_config_path):
|
|
490
|
+
# 配置文件不存在,创建默认配置
|
|
491
|
+
os.makedirs(os.path.dirname(self.mcp_config_path), exist_ok=True)
|
|
492
|
+
with open(self.mcp_config_path, "w", encoding="utf-8") as f:
|
|
493
|
+
json.dump(DEFAULT_MCP_CONFIG, f, ensure_ascii=False, indent=4)
|
|
494
|
+
return DEFAULT_MCP_CONFIG
|
|
543
495
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
496
|
+
try:
|
|
497
|
+
with open(self.mcp_config_path, encoding="utf-8") as f:
|
|
498
|
+
return json.load(f)
|
|
499
|
+
except Exception as e:
|
|
500
|
+
logger.error(f"加载 MCP 配置失败: {e}")
|
|
501
|
+
return DEFAULT_MCP_CONFIG
|
|
549
502
|
|
|
550
|
-
|
|
551
|
-
|
|
503
|
+
def save_mcp_config(self, config: dict):
|
|
504
|
+
try:
|
|
505
|
+
with open(self.mcp_config_path, "w", encoding="utf-8") as f:
|
|
506
|
+
json.dump(config, f, ensure_ascii=False, indent=4)
|
|
507
|
+
return True
|
|
508
|
+
except Exception as e:
|
|
509
|
+
logger.error(f"保存 MCP 配置失败: {e}")
|
|
510
|
+
return False
|
|
511
|
+
|
|
512
|
+
async def sync_modelscope_mcp_servers(self, access_token: str) -> None:
|
|
513
|
+
"""从 ModelScope 平台同步 MCP 服务器配置"""
|
|
514
|
+
base_url = "https://www.modelscope.cn/openapi/v1"
|
|
515
|
+
url = f"{base_url}/mcp/servers/operational"
|
|
516
|
+
headers = {
|
|
517
|
+
"Authorization": f"Bearer {access_token.strip()}",
|
|
518
|
+
"Content-Type": "application/json",
|
|
519
|
+
}
|
|
552
520
|
|
|
553
|
-
|
|
554
|
-
|
|
521
|
+
try:
|
|
522
|
+
async with aiohttp.ClientSession() as session:
|
|
523
|
+
async with session.get(url, headers=headers) as response:
|
|
524
|
+
if response.status == 200:
|
|
525
|
+
data = await response.json()
|
|
526
|
+
mcp_server_list = data.get("data", {}).get(
|
|
527
|
+
"mcp_server_list",
|
|
528
|
+
[],
|
|
529
|
+
)
|
|
530
|
+
local_mcp_config = self.load_mcp_config()
|
|
531
|
+
|
|
532
|
+
synced_count = 0
|
|
533
|
+
for server in mcp_server_list:
|
|
534
|
+
server_name = server["name"]
|
|
535
|
+
operational_urls = server.get("operational_urls", [])
|
|
536
|
+
if not operational_urls:
|
|
537
|
+
continue
|
|
538
|
+
url_info = operational_urls[0]
|
|
539
|
+
server_url = url_info.get("url")
|
|
540
|
+
if not server_url:
|
|
541
|
+
continue
|
|
542
|
+
# 添加到配置中(同名会覆盖)
|
|
543
|
+
local_mcp_config["mcpServers"][server_name] = {
|
|
544
|
+
"url": server_url,
|
|
545
|
+
"transport": "sse",
|
|
546
|
+
"active": True,
|
|
547
|
+
"provider": "modelscope",
|
|
548
|
+
}
|
|
549
|
+
synced_count += 1
|
|
550
|
+
|
|
551
|
+
if synced_count > 0:
|
|
552
|
+
self.save_mcp_config(local_mcp_config)
|
|
553
|
+
tasks = []
|
|
554
|
+
for server in mcp_server_list:
|
|
555
|
+
name = server["name"]
|
|
556
|
+
tasks.append(
|
|
557
|
+
self.enable_mcp_server(
|
|
558
|
+
name=name,
|
|
559
|
+
config=local_mcp_config["mcpServers"][name],
|
|
560
|
+
),
|
|
561
|
+
)
|
|
562
|
+
await asyncio.gather(*tasks)
|
|
563
|
+
logger.info(
|
|
564
|
+
f"从 ModelScope 同步了 {synced_count} 个 MCP 服务器",
|
|
565
|
+
)
|
|
566
|
+
else:
|
|
567
|
+
logger.warning("没有找到可用的 ModelScope MCP 服务器")
|
|
568
|
+
else:
|
|
569
|
+
raise Exception(
|
|
570
|
+
f"ModelScope API 请求失败: HTTP {response.status}",
|
|
571
|
+
)
|
|
555
572
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
if res.find("```") != -1:
|
|
561
|
-
res = res[res.find("```json") + 7 : res.rfind("```")]
|
|
562
|
-
res = json.loads(res)
|
|
563
|
-
break
|
|
564
|
-
except Exception as e:
|
|
565
|
-
_c += 1
|
|
566
|
-
if _c == 3:
|
|
567
|
-
raise e
|
|
568
|
-
if "The message you submitted was too long" in str(e):
|
|
569
|
-
raise e
|
|
570
|
-
|
|
571
|
-
if "res" in res and not res["res"]:
|
|
572
|
-
return "", False
|
|
573
|
-
|
|
574
|
-
tool_call_result = []
|
|
575
|
-
for tool in res:
|
|
576
|
-
# 说明有函数调用
|
|
577
|
-
func_name = tool["name"]
|
|
578
|
-
args = tool["args"]
|
|
579
|
-
# 调用函数
|
|
580
|
-
func_tool = self.get_func(func_name)
|
|
581
|
-
if not func_tool:
|
|
582
|
-
raise Exception(f"Request function {func_name} not found.")
|
|
583
|
-
|
|
584
|
-
ret = await func_tool.execute(**args)
|
|
585
|
-
if ret:
|
|
586
|
-
tool_call_result.append(str(ret))
|
|
587
|
-
return tool_call_result, True
|
|
573
|
+
except aiohttp.ClientError as e:
|
|
574
|
+
raise Exception(f"网络连接错误: {e!s}")
|
|
575
|
+
except Exception as e:
|
|
576
|
+
raise Exception(f"同步 ModelScope MCP 服务器时发生错误: {e!s}")
|
|
588
577
|
|
|
589
578
|
def __str__(self):
|
|
590
579
|
return str(self.func_list)
|
|
@@ -592,7 +581,6 @@ class FuncCall:
|
|
|
592
581
|
def __repr__(self):
|
|
593
582
|
return str(self.func_list)
|
|
594
583
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
logger.debug(f"清理 MCP 客户端 {name} 资源")
|
|
584
|
+
|
|
585
|
+
# alias
|
|
586
|
+
FuncCall = FunctionToolManager
|