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.
Files changed (288) hide show
  1. astrbot/api/__init__.py +16 -4
  2. astrbot/api/all.py +2 -1
  3. astrbot/api/event/__init__.py +5 -6
  4. astrbot/api/event/filter/__init__.py +37 -34
  5. astrbot/api/platform/__init__.py +7 -8
  6. astrbot/api/provider/__init__.py +8 -7
  7. astrbot/api/star/__init__.py +3 -4
  8. astrbot/api/util/__init__.py +2 -2
  9. astrbot/cli/__init__.py +1 -0
  10. astrbot/cli/__main__.py +18 -197
  11. astrbot/cli/commands/__init__.py +6 -0
  12. astrbot/cli/commands/cmd_conf.py +209 -0
  13. astrbot/cli/commands/cmd_init.py +56 -0
  14. astrbot/cli/commands/cmd_plug.py +245 -0
  15. astrbot/cli/commands/cmd_run.py +62 -0
  16. astrbot/cli/utils/__init__.py +18 -0
  17. astrbot/cli/utils/basic.py +76 -0
  18. astrbot/cli/utils/plugin.py +246 -0
  19. astrbot/cli/utils/version_comparator.py +90 -0
  20. astrbot/core/__init__.py +17 -19
  21. astrbot/core/agent/agent.py +14 -0
  22. astrbot/core/agent/handoff.py +38 -0
  23. astrbot/core/agent/hooks.py +30 -0
  24. astrbot/core/agent/mcp_client.py +385 -0
  25. astrbot/core/agent/message.py +175 -0
  26. astrbot/core/agent/response.py +14 -0
  27. astrbot/core/agent/run_context.py +22 -0
  28. astrbot/core/agent/runners/__init__.py +3 -0
  29. astrbot/core/agent/runners/base.py +65 -0
  30. astrbot/core/agent/runners/coze/coze_agent_runner.py +367 -0
  31. astrbot/core/agent/runners/coze/coze_api_client.py +324 -0
  32. astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py +403 -0
  33. astrbot/core/agent/runners/dify/dify_agent_runner.py +336 -0
  34. astrbot/core/agent/runners/dify/dify_api_client.py +195 -0
  35. astrbot/core/agent/runners/tool_loop_agent_runner.py +400 -0
  36. astrbot/core/agent/tool.py +285 -0
  37. astrbot/core/agent/tool_executor.py +17 -0
  38. astrbot/core/astr_agent_context.py +19 -0
  39. astrbot/core/astr_agent_hooks.py +36 -0
  40. astrbot/core/astr_agent_run_util.py +80 -0
  41. astrbot/core/astr_agent_tool_exec.py +246 -0
  42. astrbot/core/astrbot_config_mgr.py +275 -0
  43. astrbot/core/config/__init__.py +2 -2
  44. astrbot/core/config/astrbot_config.py +60 -20
  45. astrbot/core/config/default.py +1972 -453
  46. astrbot/core/config/i18n_utils.py +110 -0
  47. astrbot/core/conversation_mgr.py +285 -75
  48. astrbot/core/core_lifecycle.py +167 -62
  49. astrbot/core/db/__init__.py +305 -102
  50. astrbot/core/db/migration/helper.py +69 -0
  51. astrbot/core/db/migration/migra_3_to_4.py +357 -0
  52. astrbot/core/db/migration/migra_45_to_46.py +44 -0
  53. astrbot/core/db/migration/migra_webchat_session.py +131 -0
  54. astrbot/core/db/migration/shared_preferences_v3.py +48 -0
  55. astrbot/core/db/migration/sqlite_v3.py +497 -0
  56. astrbot/core/db/po.py +259 -55
  57. astrbot/core/db/sqlite.py +773 -528
  58. astrbot/core/db/vec_db/base.py +73 -0
  59. astrbot/core/db/vec_db/faiss_impl/__init__.py +3 -0
  60. astrbot/core/db/vec_db/faiss_impl/document_storage.py +392 -0
  61. astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +93 -0
  62. astrbot/core/db/vec_db/faiss_impl/sqlite_init.sql +17 -0
  63. astrbot/core/db/vec_db/faiss_impl/vec_db.py +204 -0
  64. astrbot/core/event_bus.py +26 -22
  65. astrbot/core/exceptions.py +9 -0
  66. astrbot/core/file_token_service.py +98 -0
  67. astrbot/core/initial_loader.py +19 -10
  68. astrbot/core/knowledge_base/chunking/__init__.py +9 -0
  69. astrbot/core/knowledge_base/chunking/base.py +25 -0
  70. astrbot/core/knowledge_base/chunking/fixed_size.py +59 -0
  71. astrbot/core/knowledge_base/chunking/recursive.py +161 -0
  72. astrbot/core/knowledge_base/kb_db_sqlite.py +301 -0
  73. astrbot/core/knowledge_base/kb_helper.py +642 -0
  74. astrbot/core/knowledge_base/kb_mgr.py +330 -0
  75. astrbot/core/knowledge_base/models.py +120 -0
  76. astrbot/core/knowledge_base/parsers/__init__.py +13 -0
  77. astrbot/core/knowledge_base/parsers/base.py +51 -0
  78. astrbot/core/knowledge_base/parsers/markitdown_parser.py +26 -0
  79. astrbot/core/knowledge_base/parsers/pdf_parser.py +101 -0
  80. astrbot/core/knowledge_base/parsers/text_parser.py +42 -0
  81. astrbot/core/knowledge_base/parsers/url_parser.py +103 -0
  82. astrbot/core/knowledge_base/parsers/util.py +13 -0
  83. astrbot/core/knowledge_base/prompts.py +65 -0
  84. astrbot/core/knowledge_base/retrieval/__init__.py +14 -0
  85. astrbot/core/knowledge_base/retrieval/hit_stopwords.txt +767 -0
  86. astrbot/core/knowledge_base/retrieval/manager.py +276 -0
  87. astrbot/core/knowledge_base/retrieval/rank_fusion.py +142 -0
  88. astrbot/core/knowledge_base/retrieval/sparse_retriever.py +136 -0
  89. astrbot/core/log.py +21 -15
  90. astrbot/core/message/components.py +413 -287
  91. astrbot/core/message/message_event_result.py +35 -24
  92. astrbot/core/persona_mgr.py +192 -0
  93. astrbot/core/pipeline/__init__.py +14 -14
  94. astrbot/core/pipeline/content_safety_check/stage.py +13 -9
  95. astrbot/core/pipeline/content_safety_check/strategies/__init__.py +1 -2
  96. astrbot/core/pipeline/content_safety_check/strategies/baidu_aip.py +13 -14
  97. astrbot/core/pipeline/content_safety_check/strategies/keywords.py +2 -1
  98. astrbot/core/pipeline/content_safety_check/strategies/strategy.py +6 -6
  99. astrbot/core/pipeline/context.py +7 -1
  100. astrbot/core/pipeline/context_utils.py +107 -0
  101. astrbot/core/pipeline/preprocess_stage/stage.py +63 -36
  102. astrbot/core/pipeline/process_stage/method/agent_request.py +48 -0
  103. astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +464 -0
  104. astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +202 -0
  105. astrbot/core/pipeline/process_stage/method/star_request.py +26 -32
  106. astrbot/core/pipeline/process_stage/stage.py +21 -15
  107. astrbot/core/pipeline/process_stage/utils.py +125 -0
  108. astrbot/core/pipeline/rate_limit_check/stage.py +34 -36
  109. astrbot/core/pipeline/respond/stage.py +142 -101
  110. astrbot/core/pipeline/result_decorate/stage.py +124 -57
  111. astrbot/core/pipeline/scheduler.py +21 -16
  112. astrbot/core/pipeline/session_status_check/stage.py +37 -0
  113. astrbot/core/pipeline/stage.py +11 -76
  114. astrbot/core/pipeline/waking_check/stage.py +69 -33
  115. astrbot/core/pipeline/whitelist_check/stage.py +10 -7
  116. astrbot/core/platform/__init__.py +6 -6
  117. astrbot/core/platform/astr_message_event.py +107 -129
  118. astrbot/core/platform/astrbot_message.py +32 -12
  119. astrbot/core/platform/manager.py +62 -18
  120. astrbot/core/platform/message_session.py +30 -0
  121. astrbot/core/platform/platform.py +16 -24
  122. astrbot/core/platform/platform_metadata.py +9 -4
  123. astrbot/core/platform/register.py +12 -7
  124. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +136 -60
  125. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +126 -46
  126. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +63 -31
  127. astrbot/core/platform/sources/dingtalk/dingtalk_event.py +30 -26
  128. astrbot/core/platform/sources/discord/client.py +129 -0
  129. astrbot/core/platform/sources/discord/components.py +139 -0
  130. astrbot/core/platform/sources/discord/discord_platform_adapter.py +473 -0
  131. astrbot/core/platform/sources/discord/discord_platform_event.py +313 -0
  132. astrbot/core/platform/sources/lark/lark_adapter.py +27 -18
  133. astrbot/core/platform/sources/lark/lark_event.py +39 -13
  134. astrbot/core/platform/sources/misskey/misskey_adapter.py +770 -0
  135. astrbot/core/platform/sources/misskey/misskey_api.py +964 -0
  136. astrbot/core/platform/sources/misskey/misskey_event.py +163 -0
  137. astrbot/core/platform/sources/misskey/misskey_utils.py +550 -0
  138. astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +149 -33
  139. astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +41 -26
  140. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +36 -17
  141. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_event.py +3 -1
  142. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +14 -8
  143. astrbot/core/platform/sources/satori/satori_adapter.py +792 -0
  144. astrbot/core/platform/sources/satori/satori_event.py +432 -0
  145. astrbot/core/platform/sources/slack/client.py +164 -0
  146. astrbot/core/platform/sources/slack/slack_adapter.py +416 -0
  147. astrbot/core/platform/sources/slack/slack_event.py +253 -0
  148. astrbot/core/platform/sources/telegram/tg_adapter.py +100 -43
  149. astrbot/core/platform/sources/telegram/tg_event.py +136 -36
  150. astrbot/core/platform/sources/webchat/webchat_adapter.py +72 -22
  151. astrbot/core/platform/sources/webchat/webchat_event.py +46 -22
  152. astrbot/core/platform/sources/webchat/webchat_queue_mgr.py +35 -0
  153. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +926 -0
  154. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +178 -0
  155. astrbot/core/platform/sources/wechatpadpro/xml_data_parser.py +159 -0
  156. astrbot/core/platform/sources/wecom/wecom_adapter.py +169 -27
  157. astrbot/core/platform/sources/wecom/wecom_event.py +162 -77
  158. astrbot/core/platform/sources/wecom/wecom_kf.py +279 -0
  159. astrbot/core/platform/sources/wecom/wecom_kf_message.py +196 -0
  160. astrbot/core/platform/sources/wecom_ai_bot/WXBizJsonMsgCrypt.py +297 -0
  161. astrbot/core/platform/sources/wecom_ai_bot/__init__.py +15 -0
  162. astrbot/core/platform/sources/wecom_ai_bot/ierror.py +19 -0
  163. astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +472 -0
  164. astrbot/core/platform/sources/wecom_ai_bot/wecomai_api.py +417 -0
  165. astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +152 -0
  166. astrbot/core/platform/sources/wecom_ai_bot/wecomai_queue_mgr.py +153 -0
  167. astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +168 -0
  168. astrbot/core/platform/sources/wecom_ai_bot/wecomai_utils.py +209 -0
  169. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +306 -0
  170. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +186 -0
  171. astrbot/core/platform_message_history_mgr.py +49 -0
  172. astrbot/core/provider/__init__.py +2 -3
  173. astrbot/core/provider/entites.py +8 -8
  174. astrbot/core/provider/entities.py +154 -98
  175. astrbot/core/provider/func_tool_manager.py +446 -458
  176. astrbot/core/provider/manager.py +345 -207
  177. astrbot/core/provider/provider.py +188 -73
  178. astrbot/core/provider/register.py +9 -7
  179. astrbot/core/provider/sources/anthropic_source.py +295 -115
  180. astrbot/core/provider/sources/azure_tts_source.py +224 -0
  181. astrbot/core/provider/sources/bailian_rerank_source.py +236 -0
  182. astrbot/core/provider/sources/dashscope_tts.py +138 -14
  183. astrbot/core/provider/sources/edge_tts_source.py +24 -19
  184. astrbot/core/provider/sources/fishaudio_tts_api_source.py +58 -13
  185. astrbot/core/provider/sources/gemini_embedding_source.py +61 -0
  186. astrbot/core/provider/sources/gemini_source.py +310 -132
  187. astrbot/core/provider/sources/gemini_tts_source.py +81 -0
  188. astrbot/core/provider/sources/groq_source.py +15 -0
  189. astrbot/core/provider/sources/gsv_selfhosted_source.py +151 -0
  190. astrbot/core/provider/sources/gsvi_tts_source.py +14 -7
  191. astrbot/core/provider/sources/minimax_tts_api_source.py +159 -0
  192. astrbot/core/provider/sources/openai_embedding_source.py +40 -0
  193. astrbot/core/provider/sources/openai_source.py +241 -145
  194. astrbot/core/provider/sources/openai_tts_api_source.py +18 -7
  195. astrbot/core/provider/sources/sensevoice_selfhosted_source.py +13 -11
  196. astrbot/core/provider/sources/vllm_rerank_source.py +71 -0
  197. astrbot/core/provider/sources/volcengine_tts.py +115 -0
  198. astrbot/core/provider/sources/whisper_api_source.py +18 -13
  199. astrbot/core/provider/sources/whisper_selfhosted_source.py +19 -12
  200. astrbot/core/provider/sources/xinference_rerank_source.py +116 -0
  201. astrbot/core/provider/sources/xinference_stt_provider.py +197 -0
  202. astrbot/core/provider/sources/zhipu_source.py +6 -73
  203. astrbot/core/star/__init__.py +43 -11
  204. astrbot/core/star/config.py +17 -18
  205. astrbot/core/star/context.py +362 -138
  206. astrbot/core/star/filter/__init__.py +4 -3
  207. astrbot/core/star/filter/command.py +111 -35
  208. astrbot/core/star/filter/command_group.py +46 -34
  209. astrbot/core/star/filter/custom_filter.py +6 -5
  210. astrbot/core/star/filter/event_message_type.py +4 -2
  211. astrbot/core/star/filter/permission.py +4 -2
  212. astrbot/core/star/filter/platform_adapter_type.py +45 -12
  213. astrbot/core/star/filter/regex.py +4 -2
  214. astrbot/core/star/register/__init__.py +19 -15
  215. astrbot/core/star/register/star.py +41 -13
  216. astrbot/core/star/register/star_handler.py +236 -86
  217. astrbot/core/star/session_llm_manager.py +280 -0
  218. astrbot/core/star/session_plugin_manager.py +170 -0
  219. astrbot/core/star/star.py +36 -43
  220. astrbot/core/star/star_handler.py +47 -85
  221. astrbot/core/star/star_manager.py +442 -260
  222. astrbot/core/star/star_tools.py +167 -45
  223. astrbot/core/star/updator.py +17 -20
  224. astrbot/core/umop_config_router.py +106 -0
  225. astrbot/core/updator.py +38 -13
  226. astrbot/core/utils/astrbot_path.py +39 -0
  227. astrbot/core/utils/command_parser.py +1 -1
  228. astrbot/core/utils/io.py +119 -60
  229. astrbot/core/utils/log_pipe.py +1 -1
  230. astrbot/core/utils/metrics.py +11 -10
  231. astrbot/core/utils/migra_helper.py +73 -0
  232. astrbot/core/utils/path_util.py +63 -62
  233. astrbot/core/utils/pip_installer.py +37 -15
  234. astrbot/core/utils/session_lock.py +29 -0
  235. astrbot/core/utils/session_waiter.py +19 -20
  236. astrbot/core/utils/shared_preferences.py +174 -34
  237. astrbot/core/utils/t2i/__init__.py +4 -1
  238. astrbot/core/utils/t2i/local_strategy.py +386 -238
  239. astrbot/core/utils/t2i/network_strategy.py +109 -49
  240. astrbot/core/utils/t2i/renderer.py +29 -14
  241. astrbot/core/utils/t2i/template/astrbot_powershell.html +184 -0
  242. astrbot/core/utils/t2i/template_manager.py +111 -0
  243. astrbot/core/utils/tencent_record_helper.py +115 -1
  244. astrbot/core/utils/version_comparator.py +10 -13
  245. astrbot/core/zip_updator.py +112 -65
  246. astrbot/dashboard/routes/__init__.py +20 -13
  247. astrbot/dashboard/routes/auth.py +20 -9
  248. astrbot/dashboard/routes/chat.py +297 -141
  249. astrbot/dashboard/routes/config.py +652 -55
  250. astrbot/dashboard/routes/conversation.py +107 -37
  251. astrbot/dashboard/routes/file.py +26 -0
  252. astrbot/dashboard/routes/knowledge_base.py +1244 -0
  253. astrbot/dashboard/routes/log.py +27 -2
  254. astrbot/dashboard/routes/persona.py +202 -0
  255. astrbot/dashboard/routes/plugin.py +197 -139
  256. astrbot/dashboard/routes/route.py +27 -7
  257. astrbot/dashboard/routes/session_management.py +354 -0
  258. astrbot/dashboard/routes/stat.py +85 -18
  259. astrbot/dashboard/routes/static_file.py +5 -2
  260. astrbot/dashboard/routes/t2i.py +233 -0
  261. astrbot/dashboard/routes/tools.py +184 -120
  262. astrbot/dashboard/routes/update.py +59 -36
  263. astrbot/dashboard/server.py +96 -36
  264. astrbot/dashboard/utils.py +165 -0
  265. astrbot-4.7.0.dist-info/METADATA +294 -0
  266. astrbot-4.7.0.dist-info/RECORD +274 -0
  267. {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/WHEEL +1 -1
  268. astrbot/core/db/plugin/sqlite_impl.py +0 -112
  269. astrbot/core/db/sqlite_init.sql +0 -50
  270. astrbot/core/pipeline/platform_compatibility/stage.py +0 -56
  271. astrbot/core/pipeline/process_stage/method/llm_request.py +0 -606
  272. astrbot/core/platform/sources/gewechat/client.py +0 -806
  273. astrbot/core/platform/sources/gewechat/downloader.py +0 -55
  274. astrbot/core/platform/sources/gewechat/gewechat_event.py +0 -255
  275. astrbot/core/platform/sources/gewechat/gewechat_platform_adapter.py +0 -103
  276. astrbot/core/platform/sources/gewechat/xml_data_parser.py +0 -110
  277. astrbot/core/provider/sources/dashscope_source.py +0 -203
  278. astrbot/core/provider/sources/dify_source.py +0 -281
  279. astrbot/core/provider/sources/llmtuner_source.py +0 -132
  280. astrbot/core/rag/embedding/openai_source.py +0 -20
  281. astrbot/core/rag/knowledge_db_mgr.py +0 -94
  282. astrbot/core/rag/store/__init__.py +0 -9
  283. astrbot/core/rag/store/chroma_db.py +0 -42
  284. astrbot/core/utils/dify_api_client.py +0 -152
  285. astrbot-3.5.6.dist-info/METADATA +0 -249
  286. astrbot-3.5.6.dist-info/RECORD +0 -158
  287. {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/entry_points.txt +0 -0
  288. {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 asyncio
6
- import logging
7
+ from collections.abc import Awaitable, Callable
8
+ from typing import Any
7
9
 
8
- from typing import Dict, List, Awaitable, Literal, Any
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
- try:
16
- import mcp
17
- from mcp.client.sse import sse_client
18
- except (ModuleNotFoundError, ImportError):
19
- logger.warning("警告: 缺少依赖库 'mcp',将无法使用 MCP 服务。")
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
- @dataclass
33
- class FuncTool:
34
- """
35
- 用于描述一个函数调用工具。
36
- """
37
-
38
- name: str
39
- parameters: Dict
40
- description: str
41
- handler: Awaitable = None
42
- """处理函数, origin 为 mcp 时,这个为空"""
43
- handler_module_path: str = None
44
- """处理函数的模块路径,当 origin 为 mcp 时,这个为空
45
-
46
- 必须要保留这个字段, handler 在初始化会被 functools.partial 包装,导致 handler 的 __module__ 为 functools
47
- """
48
- active: bool = True
49
- """是否激活"""
50
-
51
- origin: Literal["local", "mcp"] = "local"
52
- """函数工具的来源, local 为本地函数工具, mcp 为 MCP 服务"""
53
-
54
- # MCP 相关字段
55
- mcp_server_name: str = None
56
- """MCP 服务名称,当 origin 为 mcp 时有效"""
57
- mcp_client: MCPClient = None
58
- """MCP 客户端,当 origin 为 mcp 时有效"""
59
-
60
- def __repr__(self):
61
- return f"FuncTool(name={self.name}, parameters={self.parameters}, description={self.description}, active={self.active}, origin={self.origin})"
62
-
63
- async def execute(self, **args) -> Any:
64
- """执行函数调用"""
65
- if self.origin == "local":
66
- if not self.handler:
67
- raise Exception(f"Local function {self.name} has no handler")
68
- return await self.handler(**args)
69
- elif self.origin == "mcp":
70
- if not self.mcp_client or not self.mcp_client.session:
71
- raise Exception(f"MCP client for {self.name} is not available")
72
- # 使用name属性而不是额外的mcp_tool_name
73
- if ":" in self.name:
74
- # 如果名字是格式为 mcp:server:tool_name,提取实际的工具名
75
- actual_tool_name = self.name.split(":")[-1]
76
- return await self.mcp_client.session.call_tool(actual_tool_name, args)
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
- return await self.mcp_client.session.call_tool(self.name, args)
79
- else:
80
- raise Exception(f"Unknown function origin: {self.origin}")
81
-
82
-
83
- class MCPClient:
84
- def __init__(self):
85
- # Initialize session and client objects
86
- self.session: Optional[mcp.ClientSession] = None
87
- self.exit_stack = AsyncExitStack()
88
-
89
- self.name = None
90
- self.active: bool = True
91
- self.tools: List[mcp.Tool] = []
92
- self.server_errlogs: List[str] = []
93
-
94
- async def connect_to_server(self, mcp_server_config: dict, name: str):
95
- """连接到 MCP 服务器
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: List[FuncTool] = []
162
- """内部加载的 func tools"""
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.mcp_service_queue = asyncio.Queue()
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
- params = {
190
- "type": "object", # hard-coded here
191
- "properties": {},
192
- }
193
- for param in func_args:
194
- params["properties"][param["name"]] = {
195
- "type": param["type"],
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
- async def _init_mcp_clients(self) -> None:
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
- current_dir = os.path.dirname(os.path.abspath(__file__))
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: Dict[str, Dict] = json.load(
253
- open(mcp_json_file, "r", encoding="utf-8")
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.keys():
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, name: str, cfg: dict, event: asyncio.Event
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
- import traceback
324
-
325
- traceback.print_exc()
326
- logger.error(f"初始化 MCP 客户端 {name} 失败: {e}")
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
- try:
331
- # 先清理之前的客户端,如果存在
332
- if name in self.mcp_client_dict:
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
- # MCP 工具转换为 FuncTool 并添加到 func_list
350
- for tool in mcp_client.tools:
351
- func_tool = FuncTool(
352
- name=tool.name,
353
- parameters=tool.inputSchema,
354
- description=tool.description,
355
- origin="mcp",
356
- mcp_server_name=name,
357
- mcp_client=mcp_client,
358
- )
359
- self.func_list.append(func_tool)
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
- logger.info(f"已连接 MCP 服务 {name}, Tools: {tool_names}")
362
- return
363
- except Exception as e:
364
- import traceback
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
- logger.error(traceback.format_exc())
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 self.mcp_client_dict[name].cleanup()
379
- del self.mcp_client_dict[name]
286
+ await client.cleanup()
380
287
  except Exception as e:
381
- logger.info(f"清空 MCP 客户端资源 {name}: {e}。")
382
- # 移除关联的FuncTool
383
- self.func_list = [
384
- f
385
- for f in self.func_list
386
- if not (f.origin == "mcp" and f.mcp_server_name == name)
387
- ]
388
- logger.info(f"已关闭 MCP 服务 {name}")
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 get_func_desc_openai_style(self, omit_empty_parameter_field=False) -> list:
391
- """
392
- 获得 OpenAI API 风格的**已经激活**的工具描述
393
- """
394
- _l = []
395
- # 处理所有工具(包括本地和MCP工具)
396
- for f in self.func_list:
397
- if not f.active:
398
- continue
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
- def get_func_desc_anthropic_style(self) -> list:
415
- """
416
- 获得 Anthropic API 风格的**已经激活**的工具描述
417
- """
418
- tools = []
419
- for f in self.func_list:
420
- if not f.active:
421
- continue
422
-
423
- # Convert internal format to Anthropic style
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
- # Gemini API 支持的数据类型和格式
443
- supported_types = {
444
- "string",
445
- "number",
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
- def convert_schema(schema: dict) -> dict:
459
- """转换 schema 为 Gemini API 格式"""
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
- # 如果 schema 包含 anyOf,则只返回 anyOf 字段
462
- if "anyOf" in schema:
463
- return {"anyOf": [convert_schema(s) for s in schema["anyOf"]]}
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
- result = {}
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
- if "type" in schema and schema["type"] in supported_types:
468
- result["type"] = schema["type"]
469
- if "format" in schema and schema["format"] in supported_formats.get(
470
- result["type"], set()
471
- ):
472
- result["format"] = schema["format"]
473
- else:
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
- if "properties" in schema:
491
- properties = {}
492
- for key, value in schema["properties"].items():
493
- prop_value = convert_schema(value)
494
- if "default" in prop_value:
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
- if properties: # 只在有非空属性时添加
499
- result["properties"] = properties
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
- if "items" in schema:
502
- result["items"] = convert_schema(schema["items"])
424
+ def deactivate_llm_tool(self, name: str) -> bool:
425
+ """停用一个已经注册的函数调用工具。
503
426
 
504
- return result
427
+ Returns:
428
+ 如果没找到,会返回 False
505
429
 
506
- tools = [
507
- {
508
- "name": f.name,
509
- "description": f.description,
510
- **({"parameters": convert_schema(f.parameters)}),
511
- }
512
- for f in self.func_list
513
- if f.active
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
- declarations = {}
517
- if tools:
518
- declarations["function_declarations"] = tools
519
- return declarations
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
- async def func_call(self, question: str, session_id: str, provider) -> tuple:
522
- _l = []
523
- for f in self.func_list:
524
- if not f.active:
525
- continue
526
- _l.append(
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
- func_definition = json.dumps(_l, ensure_ascii=False)
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
- prompt = textwrap.dedent(f"""
536
- ROLE:
537
- 你是一个 Function calling AI Agent, 你的任务是将用户的提问转化为函数调用。
480
+ return True
481
+ return False
538
482
 
539
- TOOLS:
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
- {func_definition}
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
- LIMIT:
545
- 1. 你返回的内容应当能够被 Python 的 json 模块解析的 Json 格式字符串。
546
- 2. 你的 Json 返回的格式如下:`[{{"name": "<func_name>", "args": <arg_dict>}}, ...]`。参数根据上面提供的函数列表中的参数来填写。
547
- 3. 允许必要时返回多个函数调用,但需保证这些函数调用的顺序正确。
548
- 4. 如果用户的提问中不需要用到给定的函数,请直接返回 `{{"res": False}}`。
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
- EXAMPLE:
551
- 1. `用户提问`:请问一下天气怎么样? `函数调用`:[{{"name": "get_weather", "args": {{"city": "北京"}}}}]
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
- 用户的提问是:{question}
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
- _c = 0
557
- while _c < 3:
558
- try:
559
- res = await provider.text_chat(prompt, session_id)
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
- async def terminate(self):
596
- for name in self.mcp_client_dict.keys():
597
- await self._terminate_mcp_client(name)
598
- logger.debug(f"清理 MCP 客户端 {name} 资源")
584
+
585
+ # alias
586
+ FuncCall = FunctionToolManager