AstrBot 4.5.0__py3-none-any.whl → 4.5.2__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 (244) hide show
  1. astrbot/api/__init__.py +10 -11
  2. astrbot/api/event/__init__.py +5 -6
  3. astrbot/api/event/filter/__init__.py +37 -36
  4. astrbot/api/platform/__init__.py +7 -8
  5. astrbot/api/provider/__init__.py +7 -7
  6. astrbot/api/star/__init__.py +3 -4
  7. astrbot/api/util/__init__.py +2 -2
  8. astrbot/cli/__main__.py +5 -5
  9. astrbot/cli/commands/__init__.py +3 -3
  10. astrbot/cli/commands/cmd_conf.py +19 -16
  11. astrbot/cli/commands/cmd_init.py +3 -2
  12. astrbot/cli/commands/cmd_plug.py +8 -10
  13. astrbot/cli/commands/cmd_run.py +5 -6
  14. astrbot/cli/utils/__init__.py +6 -6
  15. astrbot/cli/utils/basic.py +14 -14
  16. astrbot/cli/utils/plugin.py +24 -15
  17. astrbot/cli/utils/version_comparator.py +10 -12
  18. astrbot/core/__init__.py +8 -6
  19. astrbot/core/agent/agent.py +3 -2
  20. astrbot/core/agent/handoff.py +6 -2
  21. astrbot/core/agent/hooks.py +9 -6
  22. astrbot/core/agent/mcp_client.py +50 -15
  23. astrbot/core/agent/message.py +168 -0
  24. astrbot/core/agent/response.py +2 -1
  25. astrbot/core/agent/run_context.py +2 -3
  26. astrbot/core/agent/runners/base.py +10 -13
  27. astrbot/core/agent/runners/tool_loop_agent_runner.py +52 -51
  28. astrbot/core/agent/tool.py +60 -41
  29. astrbot/core/agent/tool_executor.py +9 -3
  30. astrbot/core/astr_agent_context.py +3 -1
  31. astrbot/core/astrbot_config_mgr.py +29 -9
  32. astrbot/core/config/__init__.py +2 -2
  33. astrbot/core/config/astrbot_config.py +28 -26
  34. astrbot/core/config/default.py +44 -6
  35. astrbot/core/conversation_mgr.py +105 -36
  36. astrbot/core/core_lifecycle.py +68 -54
  37. astrbot/core/db/__init__.py +33 -18
  38. astrbot/core/db/migration/helper.py +18 -13
  39. astrbot/core/db/migration/migra_3_to_4.py +53 -34
  40. astrbot/core/db/migration/migra_45_to_46.py +1 -1
  41. astrbot/core/db/migration/shared_preferences_v3.py +2 -1
  42. astrbot/core/db/migration/sqlite_v3.py +26 -23
  43. astrbot/core/db/po.py +27 -18
  44. astrbot/core/db/sqlite.py +74 -45
  45. astrbot/core/db/vec_db/base.py +10 -14
  46. astrbot/core/db/vec_db/faiss_impl/document_storage.py +90 -77
  47. astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +9 -3
  48. astrbot/core/db/vec_db/faiss_impl/vec_db.py +36 -31
  49. astrbot/core/event_bus.py +8 -6
  50. astrbot/core/file_token_service.py +6 -5
  51. astrbot/core/initial_loader.py +7 -5
  52. astrbot/core/knowledge_base/chunking/__init__.py +1 -3
  53. astrbot/core/knowledge_base/chunking/base.py +1 -0
  54. astrbot/core/knowledge_base/chunking/fixed_size.py +2 -0
  55. astrbot/core/knowledge_base/chunking/recursive.py +16 -10
  56. astrbot/core/knowledge_base/kb_db_sqlite.py +50 -48
  57. astrbot/core/knowledge_base/kb_helper.py +30 -17
  58. astrbot/core/knowledge_base/kb_mgr.py +6 -7
  59. astrbot/core/knowledge_base/models.py +10 -4
  60. astrbot/core/knowledge_base/parsers/__init__.py +3 -5
  61. astrbot/core/knowledge_base/parsers/base.py +1 -0
  62. astrbot/core/knowledge_base/parsers/markitdown_parser.py +2 -1
  63. astrbot/core/knowledge_base/parsers/pdf_parser.py +2 -1
  64. astrbot/core/knowledge_base/parsers/text_parser.py +1 -0
  65. astrbot/core/knowledge_base/parsers/util.py +1 -1
  66. astrbot/core/knowledge_base/retrieval/__init__.py +6 -8
  67. astrbot/core/knowledge_base/retrieval/manager.py +17 -14
  68. astrbot/core/knowledge_base/retrieval/rank_fusion.py +7 -3
  69. astrbot/core/knowledge_base/retrieval/sparse_retriever.py +11 -5
  70. astrbot/core/log.py +21 -13
  71. astrbot/core/message/components.py +123 -217
  72. astrbot/core/message/message_event_result.py +24 -24
  73. astrbot/core/persona_mgr.py +20 -11
  74. astrbot/core/pipeline/__init__.py +7 -7
  75. astrbot/core/pipeline/content_safety_check/stage.py +13 -9
  76. astrbot/core/pipeline/content_safety_check/strategies/__init__.py +1 -2
  77. astrbot/core/pipeline/content_safety_check/strategies/baidu_aip.py +12 -13
  78. astrbot/core/pipeline/content_safety_check/strategies/keywords.py +1 -0
  79. astrbot/core/pipeline/content_safety_check/strategies/strategy.py +6 -6
  80. astrbot/core/pipeline/context.py +4 -1
  81. astrbot/core/pipeline/context_utils.py +77 -7
  82. astrbot/core/pipeline/preprocess_stage/stage.py +12 -9
  83. astrbot/core/pipeline/process_stage/method/llm_request.py +125 -72
  84. astrbot/core/pipeline/process_stage/method/star_request.py +19 -17
  85. astrbot/core/pipeline/process_stage/stage.py +13 -10
  86. astrbot/core/pipeline/process_stage/utils.py +6 -5
  87. astrbot/core/pipeline/rate_limit_check/stage.py +37 -36
  88. astrbot/core/pipeline/respond/stage.py +23 -20
  89. astrbot/core/pipeline/result_decorate/stage.py +31 -23
  90. astrbot/core/pipeline/scheduler.py +12 -8
  91. astrbot/core/pipeline/session_status_check/stage.py +12 -8
  92. astrbot/core/pipeline/stage.py +10 -4
  93. astrbot/core/pipeline/waking_check/stage.py +24 -18
  94. astrbot/core/pipeline/whitelist_check/stage.py +10 -7
  95. astrbot/core/platform/__init__.py +6 -6
  96. astrbot/core/platform/astr_message_event.py +76 -110
  97. astrbot/core/platform/astrbot_message.py +11 -13
  98. astrbot/core/platform/manager.py +16 -15
  99. astrbot/core/platform/message_session.py +5 -3
  100. astrbot/core/platform/platform.py +16 -24
  101. astrbot/core/platform/platform_metadata.py +4 -4
  102. astrbot/core/platform/register.py +8 -8
  103. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +23 -15
  104. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +51 -33
  105. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +47 -29
  106. astrbot/core/platform/sources/dingtalk/dingtalk_event.py +7 -3
  107. astrbot/core/platform/sources/discord/client.py +9 -6
  108. astrbot/core/platform/sources/discord/components.py +18 -14
  109. astrbot/core/platform/sources/discord/discord_platform_adapter.py +45 -30
  110. astrbot/core/platform/sources/discord/discord_platform_event.py +38 -30
  111. astrbot/core/platform/sources/lark/lark_adapter.py +23 -17
  112. astrbot/core/platform/sources/lark/lark_event.py +21 -14
  113. astrbot/core/platform/sources/misskey/misskey_adapter.py +107 -67
  114. astrbot/core/platform/sources/misskey/misskey_api.py +153 -129
  115. astrbot/core/platform/sources/misskey/misskey_event.py +20 -15
  116. astrbot/core/platform/sources/misskey/misskey_utils.py +74 -62
  117. astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +63 -44
  118. astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +41 -26
  119. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +36 -17
  120. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_event.py +3 -1
  121. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +12 -7
  122. astrbot/core/platform/sources/satori/satori_adapter.py +56 -38
  123. astrbot/core/platform/sources/satori/satori_event.py +34 -25
  124. astrbot/core/platform/sources/slack/client.py +11 -9
  125. astrbot/core/platform/sources/slack/slack_adapter.py +52 -36
  126. astrbot/core/platform/sources/slack/slack_event.py +34 -24
  127. astrbot/core/platform/sources/telegram/tg_adapter.py +38 -18
  128. astrbot/core/platform/sources/telegram/tg_event.py +32 -18
  129. astrbot/core/platform/sources/webchat/webchat_adapter.py +27 -17
  130. astrbot/core/platform/sources/webchat/webchat_event.py +14 -10
  131. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +115 -120
  132. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +9 -8
  133. astrbot/core/platform/sources/wechatpadpro/xml_data_parser.py +15 -16
  134. astrbot/core/platform/sources/wecom/wecom_adapter.py +35 -18
  135. astrbot/core/platform/sources/wecom/wecom_event.py +55 -48
  136. astrbot/core/platform/sources/wecom/wecom_kf.py +34 -44
  137. astrbot/core/platform/sources/wecom/wecom_kf_message.py +26 -10
  138. astrbot/core/platform/sources/wecom_ai_bot/WXBizJsonMsgCrypt.py +18 -10
  139. astrbot/core/platform/sources/wecom_ai_bot/__init__.py +3 -5
  140. astrbot/core/platform/sources/wecom_ai_bot/ierror.py +0 -1
  141. astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +61 -37
  142. astrbot/core/platform/sources/wecom_ai_bot/wecomai_api.py +67 -28
  143. astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +8 -9
  144. astrbot/core/platform/sources/wecom_ai_bot/wecomai_queue_mgr.py +18 -9
  145. astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +14 -12
  146. astrbot/core/platform/sources/wecom_ai_bot/wecomai_utils.py +22 -12
  147. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +40 -26
  148. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +47 -45
  149. astrbot/core/platform_message_history_mgr.py +5 -3
  150. astrbot/core/provider/__init__.py +2 -3
  151. astrbot/core/provider/entites.py +8 -8
  152. astrbot/core/provider/entities.py +61 -75
  153. astrbot/core/provider/func_tool_manager.py +59 -55
  154. astrbot/core/provider/manager.py +40 -22
  155. astrbot/core/provider/provider.py +72 -46
  156. astrbot/core/provider/register.py +7 -7
  157. astrbot/core/provider/sources/anthropic_source.py +48 -30
  158. astrbot/core/provider/sources/azure_tts_source.py +17 -13
  159. astrbot/core/provider/sources/coze_api_client.py +27 -17
  160. astrbot/core/provider/sources/coze_source.py +104 -87
  161. astrbot/core/provider/sources/dashscope_source.py +18 -11
  162. astrbot/core/provider/sources/dashscope_tts.py +36 -23
  163. astrbot/core/provider/sources/dify_source.py +25 -20
  164. astrbot/core/provider/sources/edge_tts_source.py +21 -17
  165. astrbot/core/provider/sources/fishaudio_tts_api_source.py +22 -14
  166. astrbot/core/provider/sources/gemini_embedding_source.py +12 -13
  167. astrbot/core/provider/sources/gemini_source.py +72 -58
  168. astrbot/core/provider/sources/gemini_tts_source.py +8 -6
  169. astrbot/core/provider/sources/gsv_selfhosted_source.py +17 -14
  170. astrbot/core/provider/sources/gsvi_tts_source.py +11 -7
  171. astrbot/core/provider/sources/minimax_tts_api_source.py +50 -40
  172. astrbot/core/provider/sources/openai_embedding_source.py +6 -8
  173. astrbot/core/provider/sources/openai_source.py +102 -69
  174. astrbot/core/provider/sources/openai_tts_api_source.py +14 -6
  175. astrbot/core/provider/sources/sensevoice_selfhosted_source.py +13 -11
  176. astrbot/core/provider/sources/vllm_rerank_source.py +10 -4
  177. astrbot/core/provider/sources/volcengine_tts.py +38 -31
  178. astrbot/core/provider/sources/whisper_api_source.py +14 -12
  179. astrbot/core/provider/sources/whisper_selfhosted_source.py +15 -11
  180. astrbot/core/provider/sources/xinference_rerank_source.py +116 -0
  181. astrbot/core/provider/sources/xinference_stt_provider.py +197 -0
  182. astrbot/core/star/__init__.py +16 -11
  183. astrbot/core/star/config.py +10 -15
  184. astrbot/core/star/context.py +109 -84
  185. astrbot/core/star/filter/__init__.py +4 -3
  186. astrbot/core/star/filter/command.py +30 -28
  187. astrbot/core/star/filter/command_group.py +27 -24
  188. astrbot/core/star/filter/custom_filter.py +6 -5
  189. astrbot/core/star/filter/event_message_type.py +4 -2
  190. astrbot/core/star/filter/permission.py +4 -2
  191. astrbot/core/star/filter/platform_adapter_type.py +4 -2
  192. astrbot/core/star/filter/regex.py +4 -2
  193. astrbot/core/star/register/__init__.py +19 -19
  194. astrbot/core/star/register/star.py +6 -2
  195. astrbot/core/star/register/star_handler.py +96 -73
  196. astrbot/core/star/session_llm_manager.py +48 -14
  197. astrbot/core/star/session_plugin_manager.py +29 -15
  198. astrbot/core/star/star.py +1 -2
  199. astrbot/core/star/star_handler.py +13 -8
  200. astrbot/core/star/star_manager.py +151 -59
  201. astrbot/core/star/star_tools.py +44 -37
  202. astrbot/core/star/updator.py +10 -10
  203. astrbot/core/umop_config_router.py +10 -4
  204. astrbot/core/updator.py +13 -5
  205. astrbot/core/utils/astrbot_path.py +3 -5
  206. astrbot/core/utils/dify_api_client.py +33 -15
  207. astrbot/core/utils/io.py +66 -42
  208. astrbot/core/utils/log_pipe.py +1 -1
  209. astrbot/core/utils/metrics.py +7 -7
  210. astrbot/core/utils/path_util.py +15 -16
  211. astrbot/core/utils/pip_installer.py +5 -5
  212. astrbot/core/utils/session_waiter.py +19 -20
  213. astrbot/core/utils/shared_preferences.py +45 -20
  214. astrbot/core/utils/t2i/__init__.py +4 -1
  215. astrbot/core/utils/t2i/network_strategy.py +35 -26
  216. astrbot/core/utils/t2i/renderer.py +11 -5
  217. astrbot/core/utils/t2i/template_manager.py +14 -15
  218. astrbot/core/utils/tencent_record_helper.py +19 -13
  219. astrbot/core/utils/version_comparator.py +10 -13
  220. astrbot/core/zip_updator.py +43 -40
  221. astrbot/dashboard/routes/__init__.py +18 -18
  222. astrbot/dashboard/routes/auth.py +10 -8
  223. astrbot/dashboard/routes/chat.py +30 -21
  224. astrbot/dashboard/routes/config.py +92 -75
  225. astrbot/dashboard/routes/conversation.py +46 -39
  226. astrbot/dashboard/routes/file.py +4 -2
  227. astrbot/dashboard/routes/knowledge_base.py +47 -40
  228. astrbot/dashboard/routes/log.py +9 -4
  229. astrbot/dashboard/routes/persona.py +19 -16
  230. astrbot/dashboard/routes/plugin.py +69 -55
  231. astrbot/dashboard/routes/route.py +3 -1
  232. astrbot/dashboard/routes/session_management.py +130 -116
  233. astrbot/dashboard/routes/stat.py +34 -34
  234. astrbot/dashboard/routes/t2i.py +15 -12
  235. astrbot/dashboard/routes/tools.py +47 -52
  236. astrbot/dashboard/routes/update.py +32 -28
  237. astrbot/dashboard/server.py +30 -26
  238. astrbot/dashboard/utils.py +8 -4
  239. {astrbot-4.5.0.dist-info → astrbot-4.5.2.dist-info}/METADATA +4 -2
  240. astrbot-4.5.2.dist-info/RECORD +261 -0
  241. astrbot-4.5.0.dist-info/RECORD +0 -258
  242. {astrbot-4.5.0.dist-info → astrbot-4.5.2.dist-info}/WHEEL +0 -0
  243. {astrbot-4.5.0.dist-info → astrbot-4.5.2.dist-info}/entry_points.txt +0 -0
  244. {astrbot-4.5.0.dist-info → astrbot-4.5.2.dist-info}/licenses/LICENSE +0 -0
@@ -1,31 +1,33 @@
1
1
  import sys
2
2
  import traceback
3
3
  import typing as T
4
- from .base import BaseAgentRunner, AgentResponse, AgentState
5
- from ..hooks import BaseAgentRunHooks
6
- from ..tool_executor import BaseFunctionToolExecutor
7
- from ..run_context import ContextWrapper, TContext
8
- from ..response import AgentResponseData
9
- from astrbot.core.provider.provider import Provider
4
+
5
+ from mcp.types import (
6
+ BlobResourceContents,
7
+ CallToolResult,
8
+ EmbeddedResource,
9
+ ImageContent,
10
+ TextContent,
11
+ TextResourceContents,
12
+ )
13
+
14
+ from astrbot import logger
10
15
  from astrbot.core.message.message_event_result import (
11
16
  MessageChain,
12
17
  )
13
18
  from astrbot.core.provider.entities import (
14
- ProviderRequest,
15
19
  LLMResponse,
16
- ToolCallMessageSegment,
17
- AssistantMessageSegment,
20
+ ProviderRequest,
18
21
  ToolCallsResult,
19
22
  )
20
- from mcp.types import (
21
- TextContent,
22
- ImageContent,
23
- EmbeddedResource,
24
- TextResourceContents,
25
- BlobResourceContents,
26
- CallToolResult,
27
- )
28
- from astrbot import logger
23
+ from astrbot.core.provider.provider import Provider
24
+
25
+ from ..hooks import BaseAgentRunHooks
26
+ from ..message import AssistantMessageSegment, ToolCallMessageSegment
27
+ from ..response import AgentResponseData
28
+ from ..run_context import ContextWrapper, TContext
29
+ from ..tool_executor import BaseFunctionToolExecutor
30
+ from .base import AgentResponse, AgentState, BaseAgentRunner
29
31
 
30
32
  if sys.version_info >= (3, 12):
31
33
  from typing import override
@@ -70,8 +72,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
70
72
 
71
73
  @override
72
74
  async def step(self):
73
- """
74
- Process a single step of the agent.
75
+ """Process a single step of the agent.
75
76
  This method should return the result of the step.
76
77
  """
77
78
  if not self.req:
@@ -99,7 +100,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
99
100
  yield AgentResponse(
100
101
  type="streaming_delta",
101
102
  data=AgentResponseData(
102
- chain=MessageChain().message(llm_response.completion_text)
103
+ chain=MessageChain().message(llm_response.completion_text),
103
104
  ),
104
105
  )
105
106
  continue
@@ -120,8 +121,8 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
120
121
  type="err",
121
122
  data=AgentResponseData(
122
123
  chain=MessageChain().message(
123
- f"LLM 响应错误: {llm_resp.completion_text or '未知错误'}"
124
- )
124
+ f"LLM 响应错误: {llm_resp.completion_text or '未知错误'}",
125
+ ),
125
126
  ),
126
127
  )
127
128
 
@@ -144,7 +145,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
144
145
  yield AgentResponse(
145
146
  type="llm_result",
146
147
  data=AgentResponseData(
147
- chain=MessageChain().message(llm_resp.completion_text)
148
+ chain=MessageChain().message(llm_resp.completion_text),
148
149
  ),
149
150
  )
150
151
 
@@ -155,7 +156,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
155
156
  yield AgentResponse(
156
157
  type="tool_call",
157
158
  data=AgentResponseData(
158
- chain=MessageChain().message(f"🔨 调用工具: {tool_call_name}")
159
+ chain=MessageChain().message(f"🔨 调用工具: {tool_call_name}"),
159
160
  ),
160
161
  )
161
162
  async for result in self._handle_function_tools(self.req, llm_resp):
@@ -169,8 +170,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
169
170
  # 将结果添加到上下文中
170
171
  tool_calls_result = ToolCallsResult(
171
172
  tool_calls_info=AssistantMessageSegment(
172
- role="assistant",
173
- tool_calls=llm_resp.to_openai_tool_calls(),
173
+ tool_calls=llm_resp.to_openai_to_calls_model(),
174
174
  content=llm_resp.completion_text,
175
175
  ),
176
176
  tool_calls_result=tool_call_result_blocks,
@@ -205,7 +205,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
205
205
  role="tool",
206
206
  tool_call_id=func_tool_id,
207
207
  content=f"error: 未找到工具 {func_tool_name}",
208
- )
208
+ ),
209
209
  )
210
210
  continue
211
211
 
@@ -214,7 +214,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
214
214
  # 获取实际的 handler 函数
215
215
  if func_tool.handler:
216
216
  logger.debug(
217
- f"工具 {func_tool_name} 期望的参数: {func_tool.parameters}"
217
+ f"工具 {func_tool_name} 期望的参数: {func_tool.parameters}",
218
218
  )
219
219
  if func_tool.parameters and func_tool.parameters.get("properties"):
220
220
  expected_params = set(func_tool.parameters["properties"].keys())
@@ -227,20 +227,21 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
227
227
 
228
228
  # 记录被忽略的参数
229
229
  ignored_params = set(func_tool_args.keys()) - set(
230
- valid_params.keys()
230
+ valid_params.keys(),
231
231
  )
232
232
  if ignored_params:
233
233
  logger.warning(
234
- f"工具 {func_tool_name} 忽略非期望参数: {ignored_params}"
234
+ f"工具 {func_tool_name} 忽略非期望参数: {ignored_params}",
235
235
  )
236
236
  else:
237
237
  # 如果没有 handler(如 MCP 工具),使用所有参数
238
238
  valid_params = func_tool_args
239
- logger.warning(f"工具 {func_tool_name} 没有 handler,使用所有参数")
240
239
 
241
240
  try:
242
241
  await self.agent_hooks.on_tool_start(
243
- self.run_context, func_tool, valid_params
242
+ self.run_context,
243
+ func_tool,
244
+ valid_params,
244
245
  )
245
246
  except Exception as e:
246
247
  logger.error(f"Error in on_tool_start hook: {e}", exc_info=True)
@@ -262,7 +263,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
262
263
  role="tool",
263
264
  tool_call_id=func_tool_id,
264
265
  content=res.content[0].text,
265
- )
266
+ ),
266
267
  )
267
268
  yield MessageChain().message(res.content[0].text)
268
269
  elif isinstance(res.content[0], ImageContent):
@@ -271,10 +272,10 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
271
272
  role="tool",
272
273
  tool_call_id=func_tool_id,
273
274
  content="返回了图片(已直接发送给用户)",
274
- )
275
+ ),
275
276
  )
276
277
  yield MessageChain(type="tool_direct_result").base64_image(
277
- res.content[0].data
278
+ res.content[0].data,
278
279
  )
279
280
  elif isinstance(res.content[0], EmbeddedResource):
280
281
  resource = res.content[0].resource
@@ -284,7 +285,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
284
285
  role="tool",
285
286
  tool_call_id=func_tool_id,
286
287
  content=resource.text,
287
- )
288
+ ),
288
289
  )
289
290
  yield MessageChain().message(resource.text)
290
291
  elif (
@@ -297,10 +298,10 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
297
298
  role="tool",
298
299
  tool_call_id=func_tool_id,
299
300
  content="返回了图片(已直接发送给用户)",
300
- )
301
+ ),
301
302
  )
302
303
  yield MessageChain(
303
- type="tool_direct_result"
304
+ type="tool_direct_result",
304
305
  ).base64_image(resource.blob)
305
306
  else:
306
307
  tool_call_result_blocks.append(
@@ -308,41 +309,41 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
308
309
  role="tool",
309
310
  tool_call_id=func_tool_id,
310
311
  content="返回的数据类型不受支持",
311
- )
312
+ ),
312
313
  )
313
314
  yield MessageChain().message("返回的数据类型不受支持。")
314
315
 
315
316
  elif resp is None:
316
317
  # Tool 直接请求发送消息给用户
317
318
  # 这里我们将直接结束 Agent Loop。
319
+ # 发送消息逻辑在 ToolExecutor 中处理了。
320
+ logger.warning(
321
+ f"{func_tool_name} 没有没有返回值或者将结果直接发送给用户,此工具调用不会被记录到历史中。"
322
+ )
318
323
  self._transition_state(AgentState.DONE)
319
- if res := self.run_context.event.get_result():
320
- if res.chain:
321
- yield MessageChain(
322
- chain=res.chain, type="tool_direct_result"
323
- )
324
324
  else:
325
325
  # 不应该出现其他类型
326
326
  logger.warning(
327
- f"Tool 返回了不支持的类型: {type(resp)},将忽略。"
327
+ f"Tool 返回了不支持的类型: {type(resp)},将忽略。",
328
328
  )
329
329
 
330
330
  try:
331
331
  await self.agent_hooks.on_tool_end(
332
- self.run_context, func_tool, func_tool_args, _final_resp
332
+ self.run_context,
333
+ func_tool,
334
+ func_tool_args,
335
+ _final_resp,
333
336
  )
334
337
  except Exception as e:
335
338
  logger.error(f"Error in on_tool_end hook: {e}", exc_info=True)
336
-
337
- self.run_context.event.clear_result()
338
339
  except Exception as e:
339
340
  logger.warning(traceback.format_exc())
340
341
  tool_call_result_blocks.append(
341
342
  ToolCallMessageSegment(
342
343
  role="tool",
343
344
  tool_call_id=func_tool_id,
344
- content=f"error: {str(e)}",
345
- )
345
+ content=f"error: {e!s}",
346
+ ),
346
347
  )
347
348
 
348
349
  # 处理函数调用响应
@@ -1,55 +1,75 @@
1
- from dataclasses import dataclass
1
+ from collections.abc import Awaitable, Callable
2
+ from typing import Any, Generic
3
+
4
+ import jsonschema
5
+ import mcp
2
6
  from deprecated import deprecated
3
- from typing import Awaitable, Callable, Literal, Any, Optional
4
- from .mcp_client import MCPClient
7
+ from pydantic import model_validator
8
+ from pydantic.dataclasses import dataclass
9
+
10
+ from .run_context import ContextWrapper, TContext
11
+
12
+ ParametersType = dict[str, Any]
5
13
 
6
14
 
7
15
  @dataclass
8
- class FunctionTool:
9
- """A class representing a function tool that can be used in function calling."""
16
+ class ToolSchema:
17
+ """A class representing the schema of a tool for function calling."""
10
18
 
11
19
  name: str
12
- parameters: dict | None = None
13
- description: str | None = None
20
+ """The name of the tool."""
21
+
22
+ description: str
23
+ """The description of the tool."""
24
+
25
+ parameters: ParametersType
26
+ """The parameters of the tool, in JSON Schema format."""
27
+
28
+ @model_validator(mode="after")
29
+ def validate_parameters(self) -> "ToolSchema":
30
+ jsonschema.validate(
31
+ self.parameters, jsonschema.Draft202012Validator.META_SCHEMA
32
+ )
33
+ return self
34
+
35
+
36
+ @dataclass
37
+ class FunctionTool(ToolSchema, Generic[TContext]):
38
+ """A callable tool, for function calling."""
39
+
14
40
  handler: Callable[..., Awaitable[Any]] | None = None
15
- """处理函数, origin mcp 时,这个为空"""
16
- handler_module_path: str | None = None
17
- """处理函数的模块路径,当 origin 为 mcp 时,这个为空
41
+ """a callable that implements the tool's functionality. It should be an async function."""
18
42
 
19
- 必须要保留这个字段, handler 在初始化会被 functools.partial 包装,导致 handler 的 __module__ 为 functools
43
+ handler_module_path: str | None = None
44
+ """
45
+ The module path of the handler function. This is empty when the origin is mcp.
46
+ This field must be retained, as the handler will be wrapped in functools.partial during initialization,
47
+ causing the handler's __module__ to be functools
20
48
  """
21
49
  active: bool = True
22
- """是否激活"""
23
-
24
- origin: Literal["local", "mcp"] = "local"
25
- """函数工具的来源, local 为本地函数工具, mcp 为 MCP 服务"""
26
-
27
- # MCP 相关字段
28
- mcp_server_name: str | None = None
29
- """MCP 服务名称,当 origin 为 mcp 时有效"""
30
- mcp_client: MCPClient | None = None
31
- """MCP 客户端,当 origin 为 mcp 时有效"""
50
+ """
51
+ Whether the tool is active. This field is a special field for AstrBot.
52
+ You can ignore it when integrating with other frameworks.
53
+ """
32
54
 
33
55
  def __repr__(self):
34
- return f"FuncTool(name={self.name}, parameters={self.parameters}, description={self.description}, active={self.active}, origin={self.origin})"
35
-
36
- def __dict__(self) -> dict[str, Any]:
37
- """将 FunctionTool 转换为字典格式"""
38
- return {
39
- "name": self.name,
40
- "parameters": self.parameters,
41
- "description": self.description,
42
- "active": self.active,
43
- "origin": self.origin,
44
- "mcp_server_name": self.mcp_server_name,
45
- }
56
+ return f"FuncTool(name={self.name}, parameters={self.parameters}, description={self.description})"
57
+
58
+ async def call(
59
+ self, context: ContextWrapper[TContext], **kwargs
60
+ ) -> str | mcp.types.CallToolResult:
61
+ """Run the tool with the given arguments. The handler field has priority."""
62
+ raise NotImplementedError(
63
+ "FunctionTool.call() must be implemented by subclasses or set a handler."
64
+ )
46
65
 
47
66
 
48
67
  class ToolSet:
49
68
  """A set of function tools that can be used in function calling.
50
69
 
51
70
  This class provides methods to add, remove, and retrieve tools, as well as
52
- convert the tools to different API formats (OpenAI, Anthropic, Google GenAI)."""
71
+ convert the tools to different API formats (OpenAI, Anthropic, Google GenAI).
72
+ """
53
73
 
54
74
  def __init__(self, tools: list[FunctionTool] | None = None):
55
75
  self.tools: list[FunctionTool] = tools or []
@@ -71,7 +91,7 @@ class ToolSet:
71
91
  """Remove a tool by its name."""
72
92
  self.tools = [tool for tool in self.tools if tool.name != name]
73
93
 
74
- def get_tool(self, name: str) -> Optional[FunctionTool]:
94
+ def get_tool(self, name: str) -> FunctionTool | None:
75
95
  """Get a tool by its name."""
76
96
  for tool in self.tools:
77
97
  if tool.name == name:
@@ -132,10 +152,8 @@ class ToolSet:
132
152
  }
133
153
 
134
154
  if (
135
- tool.parameters
136
- and tool.parameters.get("properties")
137
- or not omit_empty_parameter_field
138
- ):
155
+ tool.parameters and tool.parameters.get("properties")
156
+ ) or not omit_empty_parameter_field:
139
157
  func_def["function"]["parameters"] = tool.parameters
140
158
 
141
159
  result.append(func_def)
@@ -185,7 +203,8 @@ class ToolSet:
185
203
  if "type" in schema and schema["type"] in supported_types:
186
204
  result["type"] = schema["type"]
187
205
  if "format" in schema and schema["format"] in supported_formats.get(
188
- result["type"], set()
206
+ result["type"],
207
+ set(),
189
208
  ):
190
209
  result["format"] = schema["format"]
191
210
  else:
@@ -222,7 +241,7 @@ class ToolSet:
222
241
 
223
242
  tools = []
224
243
  for tool in self.tools:
225
- d = {
244
+ d: dict[str, Any] = {
226
245
  "name": tool.name,
227
246
  "description": tool.description,
228
247
  }
@@ -1,11 +1,17 @@
1
+ from collections.abc import AsyncGenerator
2
+ from typing import Any, Generic
3
+
1
4
  import mcp
2
- from typing import Any, Generic, AsyncGenerator
3
- from .run_context import TContext, ContextWrapper
5
+
6
+ from .run_context import ContextWrapper, TContext
4
7
  from .tool import FunctionTool
5
8
 
6
9
 
7
10
  class BaseFunctionToolExecutor(Generic[TContext]):
8
11
  @classmethod
9
12
  async def execute(
10
- cls, tool: FunctionTool, run_context: ContextWrapper[TContext], **tool_args
13
+ cls,
14
+ tool: FunctionTool,
15
+ run_context: ContextWrapper[TContext],
16
+ **tool_args,
11
17
  ) -> AsyncGenerator[Any | mcp.types.CallToolResult, None]: ...
@@ -1,4 +1,6 @@
1
1
  from dataclasses import dataclass
2
+
3
+ from astrbot.core.platform.astr_message_event import AstrMessageEvent
2
4
  from astrbot.core.provider import Provider
3
5
  from astrbot.core.provider.entities import ProviderRequest
4
6
 
@@ -9,4 +11,4 @@ class AstrAgentContext:
9
11
  first_provider_request: ProviderRequest
10
12
  curr_provider_request: ProviderRequest
11
13
  streaming: bool
12
- tool_call_timeout: int = 60 # Default tool call timeout in seconds
14
+ event: AstrMessageEvent
@@ -1,13 +1,14 @@
1
1
  import os
2
2
  import uuid
3
+ from typing import TypedDict, TypeVar
4
+
3
5
  from astrbot.core import AstrBotConfig, logger
4
- from astrbot.core.utils.shared_preferences import SharedPreferences
5
6
  from astrbot.core.config.astrbot_config import ASTRBOT_CONFIG_PATH
6
7
  from astrbot.core.config.default import DEFAULT_CONFIG
7
8
  from astrbot.core.platform.message_session import MessageSession
8
9
  from astrbot.core.umop_config_router import UmopConfigRouter
9
10
  from astrbot.core.utils.astrbot_path import get_astrbot_config_path
10
- from typing import TypeVar, TypedDict
11
+ from astrbot.core.utils.shared_preferences import SharedPreferences
11
12
 
12
13
  _VT = TypeVar("_VT")
13
14
 
@@ -48,7 +49,10 @@ class AstrBotConfigManager:
48
49
  """获取所有的 abconf 数据"""
49
50
  if self.abconf_data is None:
50
51
  self.abconf_data = self.sp.get(
51
- "abconf_mapping", {}, scope="global", scope_id="global"
52
+ "abconf_mapping",
53
+ {},
54
+ scope="global",
55
+ scope_id="global",
52
56
  )
53
57
  return self.abconf_data
54
58
 
@@ -64,7 +68,7 @@ class AstrBotConfigManager:
64
68
  self.confs[uuid_] = conf
65
69
  else:
66
70
  logger.warning(
67
- f"Config file {conf_path} for UUID {uuid_} does not exist, skipping."
71
+ f"Config file {conf_path} for UUID {uuid_} does not exist, skipping.",
68
72
  )
69
73
  continue
70
74
 
@@ -73,6 +77,7 @@ class AstrBotConfigManager:
73
77
 
74
78
  Returns:
75
79
  ConfInfo: 包含配置文件的 uuid, 路径和名称等信息, 是一个 dict 类型
80
+
76
81
  """
77
82
  # uuid -> { "path": str, "name": str }
78
83
  abconf_data = self._get_abconf_data()
@@ -103,7 +108,10 @@ class AstrBotConfigManager:
103
108
  ) -> None:
104
109
  """保存配置文件的映射关系"""
105
110
  abconf_data = self.sp.get(
106
- "abconf_mapping", {}, scope="global", scope_id="global"
111
+ "abconf_mapping",
112
+ {},
113
+ scope="global",
114
+ scope_id="global",
107
115
  )
108
116
  random_word = abconf_name or uuid.uuid4().hex[:8]
109
117
  abconf_data[abconf_id] = {
@@ -177,13 +185,17 @@ class AstrBotConfigManager:
177
185
 
178
186
  Raises:
179
187
  ValueError: 如果试图删除默认配置文件
188
+
180
189
  """
181
190
  if conf_id == "default":
182
191
  raise ValueError("不能删除默认配置文件")
183
192
 
184
193
  # 从映射中移除
185
194
  abconf_data = self.sp.get(
186
- "abconf_mapping", {}, scope="global", scope_id="global"
195
+ "abconf_mapping",
196
+ {},
197
+ scope="global",
198
+ scope_id="global",
187
199
  )
188
200
  if conf_id not in abconf_data:
189
201
  logger.warning(f"配置文件 {conf_id} 不存在于映射中")
@@ -191,7 +203,8 @@ class AstrBotConfigManager:
191
203
 
192
204
  # 获取配置文件路径
193
205
  conf_path = os.path.join(
194
- get_astrbot_config_path(), abconf_data[conf_id]["path"]
206
+ get_astrbot_config_path(),
207
+ abconf_data[conf_id]["path"],
195
208
  )
196
209
 
197
210
  # 删除配置文件
@@ -224,12 +237,16 @@ class AstrBotConfigManager:
224
237
 
225
238
  Returns:
226
239
  bool: 更新是否成功
240
+
227
241
  """
228
242
  if conf_id == "default":
229
243
  raise ValueError("不能更新默认配置文件的信息")
230
244
 
231
245
  abconf_data = self.sp.get(
232
- "abconf_mapping", {}, scope="global", scope_id="global"
246
+ "abconf_mapping",
247
+ {},
248
+ scope="global",
249
+ scope_id="global",
233
250
  )
234
251
  if conf_id not in abconf_data:
235
252
  logger.warning(f"配置文件 {conf_id} 不存在于映射中")
@@ -246,7 +263,10 @@ class AstrBotConfigManager:
246
263
  return True
247
264
 
248
265
  def g(
249
- self, umo: str | None = None, key: str | None = None, default: _VT = None
266
+ self,
267
+ umo: str | None = None,
268
+ key: str | None = None,
269
+ default: _VT = None,
250
270
  ) -> _VT:
251
271
  """获取配置项。umo 为 None 时使用默认配置"""
252
272
  if umo is None:
@@ -1,9 +1,9 @@
1
- from .default import DEFAULT_CONFIG, VERSION, DB_PATH
2
1
  from .astrbot_config import *
2
+ from .default import DB_PATH, DEFAULT_CONFIG, VERSION
3
3
 
4
4
  __all__ = [
5
+ "DB_PATH",
5
6
  "DEFAULT_CONFIG",
6
7
  "VERSION",
7
- "DB_PATH",
8
8
  "AstrBotConfig",
9
9
  ]
@@ -1,11 +1,12 @@
1
- import os
1
+ import enum
2
2
  import json
3
3
  import logging
4
- import enum
5
- from .default import DEFAULT_CONFIG, DEFAULT_VALUE_MAP
6
- from typing import Dict
4
+ import os
5
+
7
6
  from astrbot.core.utils.astrbot_path import get_astrbot_data_path
8
7
 
8
+ from .default import DEFAULT_CONFIG, DEFAULT_VALUE_MAP
9
+
9
10
  ASTRBOT_CONFIG_PATH = os.path.join(get_astrbot_data_path(), "cmd_config.json")
10
11
  logger = logging.getLogger("astrbot")
11
12
 
@@ -27,7 +28,7 @@ class AstrBotConfig(dict):
27
28
  self,
28
29
  config_path: str = ASTRBOT_CONFIG_PATH,
29
30
  default_config: dict = DEFAULT_CONFIG,
30
- schema: dict = None,
31
+ schema: dict | None = None,
31
32
  ):
32
33
  super().__init__()
33
34
 
@@ -45,7 +46,7 @@ class AstrBotConfig(dict):
45
46
  json.dump(default_config, f, indent=4, ensure_ascii=False)
46
47
  object.__setattr__(self, "first_deploy", True) # 标记第一次部署
47
48
 
48
- with open(config_path, "r", encoding="utf-8-sig") as f:
49
+ with open(config_path, encoding="utf-8-sig") as f:
49
50
  conf_str = f.read()
50
51
  conf = json.loads(conf_str)
51
52
 
@@ -65,7 +66,7 @@ class AstrBotConfig(dict):
65
66
  for k, v in schema.items():
66
67
  if v["type"] not in DEFAULT_VALUE_MAP:
67
68
  raise TypeError(
68
- f"不受支持的配置类型 {v['type']}。支持的类型有:{DEFAULT_VALUE_MAP.keys()}"
69
+ f"不受支持的配置类型 {v['type']}。支持的类型有:{DEFAULT_VALUE_MAP.keys()}",
69
70
  )
70
71
  if "default" in v:
71
72
  default = v["default"]
@@ -82,7 +83,7 @@ class AstrBotConfig(dict):
82
83
 
83
84
  return conf
84
85
 
85
- def check_config_integrity(self, refer_conf: Dict, conf: Dict, path=""):
86
+ def check_config_integrity(self, refer_conf: dict, conf: dict, path=""):
86
87
  """检查配置完整性,如果有新的配置项或顺序不一致则返回 True"""
87
88
  has_new = False
88
89
 
@@ -97,27 +98,28 @@ class AstrBotConfig(dict):
97
98
  logger.info(f"检查到配置项 {path_} 不存在,已插入默认值 {value}")
98
99
  new_conf[key] = value
99
100
  has_new = True
100
- else:
101
- if conf[key] is None:
102
- # 配置项为 None,使用默认值
101
+ elif conf[key] is None:
102
+ # 配置项为 None,使用默认值
103
+ new_conf[key] = value
104
+ has_new = True
105
+ elif isinstance(value, dict):
106
+ # 递归检查子配置项
107
+ if not isinstance(conf[key], dict):
108
+ # 类型不匹配,使用默认值
103
109
  new_conf[key] = value
104
110
  has_new = True
105
- elif isinstance(value, dict):
106
- # 递归检查子配置项
107
- if not isinstance(conf[key], dict):
108
- # 类型不匹配,使用默认值
109
- new_conf[key] = value
110
- has_new = True
111
- else:
112
- # 递归检查并同步顺序
113
- child_has_new = self.check_config_integrity(
114
- value, conf[key], path + "." + key if path else key
115
- )
116
- new_conf[key] = conf[key]
117
- has_new |= child_has_new
118
111
  else:
119
- # 直接使用现有配置
112
+ # 递归检查并同步顺序
113
+ child_has_new = self.check_config_integrity(
114
+ value,
115
+ conf[key],
116
+ path + "." + key if path else key,
117
+ )
120
118
  new_conf[key] = conf[key]
119
+ has_new |= child_has_new
120
+ else:
121
+ # 直接使用现有配置
122
+ new_conf[key] = conf[key]
121
123
 
122
124
  # 检查是否存在参考配置中没有的配置项
123
125
  for key in list(conf.keys()):
@@ -140,7 +142,7 @@ class AstrBotConfig(dict):
140
142
 
141
143
  return has_new
142
144
 
143
- def save_config(self, replace_config: Dict = None):
145
+ def save_config(self, replace_config: dict | None = None):
144
146
  """将配置写入文件
145
147
 
146
148
  如果传入 replace_config,则将配置替换为 replace_config