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
@@ -0,0 +1,400 @@
1
+ import sys
2
+ import traceback
3
+ import typing as T
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
15
+ from astrbot.core.message.message_event_result import (
16
+ MessageChain,
17
+ )
18
+ from astrbot.core.provider.entities import (
19
+ LLMResponse,
20
+ ProviderRequest,
21
+ ToolCallsResult,
22
+ )
23
+ from astrbot.core.provider.provider import Provider
24
+
25
+ from ..hooks import BaseAgentRunHooks
26
+ from ..message import AssistantMessageSegment, Message, 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
31
+
32
+ if sys.version_info >= (3, 12):
33
+ from typing import override
34
+ else:
35
+ from typing_extensions import override
36
+
37
+
38
+ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
39
+ @override
40
+ async def reset(
41
+ self,
42
+ provider: Provider,
43
+ request: ProviderRequest,
44
+ run_context: ContextWrapper[TContext],
45
+ tool_executor: BaseFunctionToolExecutor[TContext],
46
+ agent_hooks: BaseAgentRunHooks[TContext],
47
+ **kwargs: T.Any,
48
+ ) -> None:
49
+ self.req = request
50
+ self.streaming = kwargs.get("streaming", False)
51
+ self.provider = provider
52
+ self.final_llm_resp = None
53
+ self._state = AgentState.IDLE
54
+ self.tool_executor = tool_executor
55
+ self.agent_hooks = agent_hooks
56
+ self.run_context = run_context
57
+
58
+ messages = []
59
+ # append existing messages in the run context
60
+ for msg in request.contexts:
61
+ messages.append(Message.model_validate(msg))
62
+ if request.prompt is not None:
63
+ m = await request.assemble_context()
64
+ messages.append(Message.model_validate(m))
65
+ if request.system_prompt:
66
+ messages.insert(
67
+ 0,
68
+ Message(role="system", content=request.system_prompt),
69
+ )
70
+ self.run_context.messages = messages
71
+
72
+ async def _iter_llm_responses(self) -> T.AsyncGenerator[LLMResponse, None]:
73
+ """Yields chunks *and* a final LLMResponse."""
74
+ if self.streaming:
75
+ stream = self.provider.text_chat_stream(**self.req.__dict__)
76
+ async for resp in stream: # type: ignore
77
+ yield resp
78
+ else:
79
+ yield await self.provider.text_chat(**self.req.__dict__)
80
+
81
+ @override
82
+ async def step(self):
83
+ """Process a single step of the agent.
84
+ This method should return the result of the step.
85
+ """
86
+ if not self.req:
87
+ raise ValueError("Request is not set. Please call reset() first.")
88
+
89
+ if self._state == AgentState.IDLE:
90
+ try:
91
+ await self.agent_hooks.on_agent_begin(self.run_context)
92
+ except Exception as e:
93
+ logger.error(f"Error in on_agent_begin hook: {e}", exc_info=True)
94
+
95
+ # 开始处理,转换到运行状态
96
+ self._transition_state(AgentState.RUNNING)
97
+ llm_resp_result = None
98
+
99
+ async for llm_response in self._iter_llm_responses():
100
+ assert isinstance(llm_response, LLMResponse)
101
+ if llm_response.is_chunk:
102
+ if llm_response.result_chain:
103
+ yield AgentResponse(
104
+ type="streaming_delta",
105
+ data=AgentResponseData(chain=llm_response.result_chain),
106
+ )
107
+ elif llm_response.completion_text:
108
+ yield AgentResponse(
109
+ type="streaming_delta",
110
+ data=AgentResponseData(
111
+ chain=MessageChain().message(llm_response.completion_text),
112
+ ),
113
+ )
114
+ elif llm_response.reasoning_content:
115
+ yield AgentResponse(
116
+ type="streaming_delta",
117
+ data=AgentResponseData(
118
+ chain=MessageChain(type="reasoning").message(
119
+ llm_response.reasoning_content,
120
+ ),
121
+ ),
122
+ )
123
+ continue
124
+ llm_resp_result = llm_response
125
+ break # got final response
126
+
127
+ if not llm_resp_result:
128
+ return
129
+
130
+ # 处理 LLM 响应
131
+ llm_resp = llm_resp_result
132
+
133
+ if llm_resp.role == "err":
134
+ # 如果 LLM 响应错误,转换到错误状态
135
+ self.final_llm_resp = llm_resp
136
+ self._transition_state(AgentState.ERROR)
137
+ yield AgentResponse(
138
+ type="err",
139
+ data=AgentResponseData(
140
+ chain=MessageChain().message(
141
+ f"LLM 响应错误: {llm_resp.completion_text or '未知错误'}",
142
+ ),
143
+ ),
144
+ )
145
+
146
+ if not llm_resp.tools_call_name:
147
+ # 如果没有工具调用,转换到完成状态
148
+ self.final_llm_resp = llm_resp
149
+ self._transition_state(AgentState.DONE)
150
+ # record the final assistant message
151
+ self.run_context.messages.append(
152
+ Message(
153
+ role="assistant",
154
+ content=llm_resp.completion_text or "",
155
+ ),
156
+ )
157
+ try:
158
+ await self.agent_hooks.on_agent_done(self.run_context, llm_resp)
159
+ except Exception as e:
160
+ logger.error(f"Error in on_agent_done hook: {e}", exc_info=True)
161
+
162
+ # 返回 LLM 结果
163
+ if llm_resp.result_chain:
164
+ yield AgentResponse(
165
+ type="llm_result",
166
+ data=AgentResponseData(chain=llm_resp.result_chain),
167
+ )
168
+ elif llm_resp.completion_text:
169
+ yield AgentResponse(
170
+ type="llm_result",
171
+ data=AgentResponseData(
172
+ chain=MessageChain().message(llm_resp.completion_text),
173
+ ),
174
+ )
175
+
176
+ # 如果有工具调用,还需处理工具调用
177
+ if llm_resp.tools_call_name:
178
+ tool_call_result_blocks = []
179
+ for tool_call_name in llm_resp.tools_call_name:
180
+ yield AgentResponse(
181
+ type="tool_call",
182
+ data=AgentResponseData(
183
+ chain=MessageChain(type="tool_call").message(
184
+ f"🔨 调用工具: {tool_call_name}"
185
+ ),
186
+ ),
187
+ )
188
+ async for result in self._handle_function_tools(self.req, llm_resp):
189
+ if isinstance(result, list):
190
+ tool_call_result_blocks = result
191
+ elif isinstance(result, MessageChain):
192
+ result.type = "tool_call_result"
193
+ yield AgentResponse(
194
+ type="tool_call_result",
195
+ data=AgentResponseData(chain=result),
196
+ )
197
+ # 将结果添加到上下文中
198
+ tool_calls_result = ToolCallsResult(
199
+ tool_calls_info=AssistantMessageSegment(
200
+ tool_calls=llm_resp.to_openai_to_calls_model(),
201
+ content=llm_resp.completion_text,
202
+ ),
203
+ tool_calls_result=tool_call_result_blocks,
204
+ )
205
+ # record the assistant message with tool calls
206
+ self.run_context.messages.extend(
207
+ tool_calls_result.to_openai_messages_model()
208
+ )
209
+
210
+ self.req.append_tool_calls_result(tool_calls_result)
211
+
212
+ async def step_until_done(
213
+ self, max_step: int
214
+ ) -> T.AsyncGenerator[AgentResponse, None]:
215
+ """Process steps until the agent is done."""
216
+ step_count = 0
217
+ while not self.done() and step_count < max_step:
218
+ step_count += 1
219
+ async for resp in self.step():
220
+ yield resp
221
+
222
+ async def _handle_function_tools(
223
+ self,
224
+ req: ProviderRequest,
225
+ llm_response: LLMResponse,
226
+ ) -> T.AsyncGenerator[MessageChain | list[ToolCallMessageSegment], None]:
227
+ """处理函数工具调用。"""
228
+ tool_call_result_blocks: list[ToolCallMessageSegment] = []
229
+ logger.info(f"Agent 使用工具: {llm_response.tools_call_name}")
230
+
231
+ # 执行函数调用
232
+ for func_tool_name, func_tool_args, func_tool_id in zip(
233
+ llm_response.tools_call_name,
234
+ llm_response.tools_call_args,
235
+ llm_response.tools_call_ids,
236
+ ):
237
+ try:
238
+ if not req.func_tool:
239
+ return
240
+ func_tool = req.func_tool.get_func(func_tool_name)
241
+ logger.info(f"使用工具:{func_tool_name},参数:{func_tool_args}")
242
+
243
+ if not func_tool:
244
+ logger.warning(f"未找到指定的工具: {func_tool_name},将跳过。")
245
+ tool_call_result_blocks.append(
246
+ ToolCallMessageSegment(
247
+ role="tool",
248
+ tool_call_id=func_tool_id,
249
+ content=f"error: 未找到工具 {func_tool_name}",
250
+ ),
251
+ )
252
+ continue
253
+
254
+ valid_params = {} # 参数过滤:只传递函数实际需要的参数
255
+
256
+ # 获取实际的 handler 函数
257
+ if func_tool.handler:
258
+ logger.debug(
259
+ f"工具 {func_tool_name} 期望的参数: {func_tool.parameters}",
260
+ )
261
+ if func_tool.parameters and func_tool.parameters.get("properties"):
262
+ expected_params = set(func_tool.parameters["properties"].keys())
263
+
264
+ valid_params = {
265
+ k: v
266
+ for k, v in func_tool_args.items()
267
+ if k in expected_params
268
+ }
269
+
270
+ # 记录被忽略的参数
271
+ ignored_params = set(func_tool_args.keys()) - set(
272
+ valid_params.keys(),
273
+ )
274
+ if ignored_params:
275
+ logger.warning(
276
+ f"工具 {func_tool_name} 忽略非期望参数: {ignored_params}",
277
+ )
278
+ else:
279
+ # 如果没有 handler(如 MCP 工具),使用所有参数
280
+ valid_params = func_tool_args
281
+
282
+ try:
283
+ await self.agent_hooks.on_tool_start(
284
+ self.run_context,
285
+ func_tool,
286
+ valid_params,
287
+ )
288
+ except Exception as e:
289
+ logger.error(f"Error in on_tool_start hook: {e}", exc_info=True)
290
+
291
+ executor = self.tool_executor.execute(
292
+ tool=func_tool,
293
+ run_context=self.run_context,
294
+ **valid_params, # 只传递有效的参数
295
+ )
296
+
297
+ _final_resp: CallToolResult | None = None
298
+ async for resp in executor: # type: ignore
299
+ if isinstance(resp, CallToolResult):
300
+ res = resp
301
+ _final_resp = resp
302
+ if isinstance(res.content[0], TextContent):
303
+ tool_call_result_blocks.append(
304
+ ToolCallMessageSegment(
305
+ role="tool",
306
+ tool_call_id=func_tool_id,
307
+ content=res.content[0].text,
308
+ ),
309
+ )
310
+ yield MessageChain().message(res.content[0].text)
311
+ elif isinstance(res.content[0], ImageContent):
312
+ tool_call_result_blocks.append(
313
+ ToolCallMessageSegment(
314
+ role="tool",
315
+ tool_call_id=func_tool_id,
316
+ content="返回了图片(已直接发送给用户)",
317
+ ),
318
+ )
319
+ yield MessageChain(type="tool_direct_result").base64_image(
320
+ res.content[0].data,
321
+ )
322
+ elif isinstance(res.content[0], EmbeddedResource):
323
+ resource = res.content[0].resource
324
+ if isinstance(resource, TextResourceContents):
325
+ tool_call_result_blocks.append(
326
+ ToolCallMessageSegment(
327
+ role="tool",
328
+ tool_call_id=func_tool_id,
329
+ content=resource.text,
330
+ ),
331
+ )
332
+ yield MessageChain().message(resource.text)
333
+ elif (
334
+ isinstance(resource, BlobResourceContents)
335
+ and resource.mimeType
336
+ and resource.mimeType.startswith("image/")
337
+ ):
338
+ tool_call_result_blocks.append(
339
+ ToolCallMessageSegment(
340
+ role="tool",
341
+ tool_call_id=func_tool_id,
342
+ content="返回了图片(已直接发送给用户)",
343
+ ),
344
+ )
345
+ yield MessageChain(
346
+ type="tool_direct_result",
347
+ ).base64_image(resource.blob)
348
+ else:
349
+ tool_call_result_blocks.append(
350
+ ToolCallMessageSegment(
351
+ role="tool",
352
+ tool_call_id=func_tool_id,
353
+ content="返回的数据类型不受支持",
354
+ ),
355
+ )
356
+ yield MessageChain().message("返回的数据类型不受支持。")
357
+
358
+ elif resp is None:
359
+ # Tool 直接请求发送消息给用户
360
+ # 这里我们将直接结束 Agent Loop。
361
+ # 发送消息逻辑在 ToolExecutor 中处理了。
362
+ logger.warning(
363
+ f"{func_tool_name} 没有没有返回值或者将结果直接发送给用户,此工具调用不会被记录到历史中。"
364
+ )
365
+ self._transition_state(AgentState.DONE)
366
+ else:
367
+ # 不应该出现其他类型
368
+ logger.warning(
369
+ f"Tool 返回了不支持的类型: {type(resp)},将忽略。",
370
+ )
371
+
372
+ try:
373
+ await self.agent_hooks.on_tool_end(
374
+ self.run_context,
375
+ func_tool,
376
+ func_tool_args,
377
+ _final_resp,
378
+ )
379
+ except Exception as e:
380
+ logger.error(f"Error in on_tool_end hook: {e}", exc_info=True)
381
+ except Exception as e:
382
+ logger.warning(traceback.format_exc())
383
+ tool_call_result_blocks.append(
384
+ ToolCallMessageSegment(
385
+ role="tool",
386
+ tool_call_id=func_tool_id,
387
+ content=f"error: {e!s}",
388
+ ),
389
+ )
390
+
391
+ # 处理函数调用响应
392
+ if tool_call_result_blocks:
393
+ yield tool_call_result_blocks
394
+
395
+ def done(self) -> bool:
396
+ """检查 Agent 是否已完成工作"""
397
+ return self._state in (AgentState.DONE, AgentState.ERROR)
398
+
399
+ def get_final_llm_resp(self) -> LLMResponse | None:
400
+ return self.final_llm_resp
@@ -0,0 +1,285 @@
1
+ from collections.abc import Awaitable, Callable
2
+ from typing import Any, Generic
3
+
4
+ import jsonschema
5
+ import mcp
6
+ from deprecated import deprecated
7
+ from pydantic import Field, model_validator
8
+ from pydantic.dataclasses import dataclass
9
+
10
+ from .run_context import ContextWrapper, TContext
11
+
12
+ ParametersType = dict[str, Any]
13
+ ToolExecResult = str | mcp.types.CallToolResult
14
+
15
+
16
+ @dataclass
17
+ class ToolSchema:
18
+ """A class representing the schema of a tool for function calling."""
19
+
20
+ name: str
21
+ """The name of the tool."""
22
+
23
+ description: str
24
+ """The description of the tool."""
25
+
26
+ parameters: ParametersType
27
+ """The parameters of the tool, in JSON Schema format."""
28
+
29
+ @model_validator(mode="after")
30
+ def validate_parameters(self) -> "ToolSchema":
31
+ jsonschema.validate(
32
+ self.parameters, jsonschema.Draft202012Validator.META_SCHEMA
33
+ )
34
+ return self
35
+
36
+
37
+ @dataclass
38
+ class FunctionTool(ToolSchema, Generic[TContext]):
39
+ """A callable tool, for function calling."""
40
+
41
+ handler: Callable[..., Awaitable[Any]] | None = None
42
+ """a callable that implements the tool's functionality. It should be an async function."""
43
+
44
+ handler_module_path: str | None = None
45
+ """
46
+ The module path of the handler function. This is empty when the origin is mcp.
47
+ This field must be retained, as the handler will be wrapped in functools.partial during initialization,
48
+ causing the handler's __module__ to be functools
49
+ """
50
+ active: bool = True
51
+ """
52
+ Whether the tool is active. This field is a special field for AstrBot.
53
+ You can ignore it when integrating with other frameworks.
54
+ """
55
+
56
+ def __repr__(self):
57
+ return f"FuncTool(name={self.name}, parameters={self.parameters}, description={self.description})"
58
+
59
+ async def call(self, context: ContextWrapper[TContext], **kwargs) -> ToolExecResult:
60
+ """Run the tool with the given arguments. The handler field has priority."""
61
+ raise NotImplementedError(
62
+ "FunctionTool.call() must be implemented by subclasses or set a handler."
63
+ )
64
+
65
+
66
+ @dataclass
67
+ class ToolSet:
68
+ """A set of function tools that can be used in function calling.
69
+
70
+ This class provides methods to add, remove, and retrieve tools, as well as
71
+ convert the tools to different API formats (OpenAI, Anthropic, Google GenAI).
72
+ """
73
+
74
+ tools: list[FunctionTool] = Field(default_factory=list)
75
+
76
+ def empty(self) -> bool:
77
+ """Check if the tool set is empty."""
78
+ return len(self.tools) == 0
79
+
80
+ def add_tool(self, tool: FunctionTool):
81
+ """Add a tool to the set."""
82
+ # 检查是否已存在同名工具
83
+ for i, existing_tool in enumerate(self.tools):
84
+ if existing_tool.name == tool.name:
85
+ self.tools[i] = tool
86
+ return
87
+ self.tools.append(tool)
88
+
89
+ def remove_tool(self, name: str):
90
+ """Remove a tool by its name."""
91
+ self.tools = [tool for tool in self.tools if tool.name != name]
92
+
93
+ def get_tool(self, name: str) -> FunctionTool | None:
94
+ """Get a tool by its name."""
95
+ for tool in self.tools:
96
+ if tool.name == name:
97
+ return tool
98
+ return None
99
+
100
+ @deprecated(reason="Use add_tool() instead", version="4.0.0")
101
+ def add_func(
102
+ self,
103
+ name: str,
104
+ func_args: list,
105
+ desc: str,
106
+ handler: Callable[..., Awaitable[Any]],
107
+ ):
108
+ """Add a function tool to the set."""
109
+ params = {
110
+ "type": "object", # hard-coded here
111
+ "properties": {},
112
+ }
113
+ for param in func_args:
114
+ params["properties"][param["name"]] = {
115
+ "type": param["type"],
116
+ "description": param["description"],
117
+ }
118
+ _func = FunctionTool(
119
+ name=name,
120
+ parameters=params,
121
+ description=desc,
122
+ handler=handler,
123
+ )
124
+ self.add_tool(_func)
125
+
126
+ @deprecated(reason="Use remove_tool() instead", version="4.0.0")
127
+ def remove_func(self, name: str):
128
+ """Remove a function tool by its name."""
129
+ self.remove_tool(name)
130
+
131
+ @deprecated(reason="Use get_tool() instead", version="4.0.0")
132
+ def get_func(self, name: str) -> FunctionTool | None:
133
+ """Get all function tools."""
134
+ return self.get_tool(name)
135
+
136
+ @property
137
+ def func_list(self) -> list[FunctionTool]:
138
+ """Get the list of function tools."""
139
+ return self.tools
140
+
141
+ def openai_schema(self, omit_empty_parameter_field: bool = False) -> list[dict]:
142
+ """Convert tools to OpenAI API function calling schema format."""
143
+ result = []
144
+ for tool in self.tools:
145
+ func_def = {
146
+ "type": "function",
147
+ "function": {
148
+ "name": tool.name,
149
+ "description": tool.description,
150
+ },
151
+ }
152
+
153
+ if (
154
+ tool.parameters and tool.parameters.get("properties")
155
+ ) or not omit_empty_parameter_field:
156
+ func_def["function"]["parameters"] = tool.parameters
157
+
158
+ result.append(func_def)
159
+ return result
160
+
161
+ def anthropic_schema(self) -> list[dict]:
162
+ """Convert tools to Anthropic API format."""
163
+ result = []
164
+ for tool in self.tools:
165
+ input_schema = {"type": "object"}
166
+ if tool.parameters:
167
+ input_schema["properties"] = tool.parameters.get("properties", {})
168
+ input_schema["required"] = tool.parameters.get("required", [])
169
+ tool_def = {
170
+ "name": tool.name,
171
+ "description": tool.description,
172
+ "input_schema": input_schema,
173
+ }
174
+ result.append(tool_def)
175
+ return result
176
+
177
+ def google_schema(self) -> dict:
178
+ """Convert tools to Google GenAI API format."""
179
+
180
+ def convert_schema(schema: dict) -> dict:
181
+ """Convert schema to Gemini API format."""
182
+ supported_types = {
183
+ "string",
184
+ "number",
185
+ "integer",
186
+ "boolean",
187
+ "array",
188
+ "object",
189
+ "null",
190
+ }
191
+ supported_formats = {
192
+ "string": {"enum", "date-time"},
193
+ "integer": {"int32", "int64"},
194
+ "number": {"float", "double"},
195
+ }
196
+
197
+ if "anyOf" in schema:
198
+ return {"anyOf": [convert_schema(s) for s in schema["anyOf"]]}
199
+
200
+ result = {}
201
+
202
+ if "type" in schema and schema["type"] in supported_types:
203
+ result["type"] = schema["type"]
204
+ if "format" in schema and schema["format"] in supported_formats.get(
205
+ result["type"],
206
+ set(),
207
+ ):
208
+ result["format"] = schema["format"]
209
+ else:
210
+ result["type"] = "null"
211
+
212
+ support_fields = {
213
+ "title",
214
+ "description",
215
+ "enum",
216
+ "minimum",
217
+ "maximum",
218
+ "maxItems",
219
+ "minItems",
220
+ "nullable",
221
+ "required",
222
+ }
223
+ result.update({k: schema[k] for k in support_fields if k in schema})
224
+
225
+ if "properties" in schema:
226
+ properties = {}
227
+ for key, value in schema["properties"].items():
228
+ prop_value = convert_schema(value)
229
+ if "default" in prop_value:
230
+ del prop_value["default"]
231
+ properties[key] = prop_value
232
+
233
+ if properties:
234
+ result["properties"] = properties
235
+
236
+ if "items" in schema:
237
+ result["items"] = convert_schema(schema["items"])
238
+
239
+ return result
240
+
241
+ tools = []
242
+ for tool in self.tools:
243
+ d: dict[str, Any] = {
244
+ "name": tool.name,
245
+ "description": tool.description,
246
+ }
247
+ if tool.parameters:
248
+ d["parameters"] = convert_schema(tool.parameters)
249
+ tools.append(d)
250
+
251
+ declarations = {}
252
+ if tools:
253
+ declarations["function_declarations"] = tools
254
+ return declarations
255
+
256
+ @deprecated(reason="Use openai_schema() instead", version="4.0.0")
257
+ def get_func_desc_openai_style(self, omit_empty_parameter_field: bool = False):
258
+ return self.openai_schema(omit_empty_parameter_field)
259
+
260
+ @deprecated(reason="Use anthropic_schema() instead", version="4.0.0")
261
+ def get_func_desc_anthropic_style(self):
262
+ return self.anthropic_schema()
263
+
264
+ @deprecated(reason="Use google_schema() instead", version="4.0.0")
265
+ def get_func_desc_google_genai_style(self):
266
+ return self.google_schema()
267
+
268
+ def names(self) -> list[str]:
269
+ """获取所有工具的名称列表"""
270
+ return [tool.name for tool in self.tools]
271
+
272
+ def __len__(self):
273
+ return len(self.tools)
274
+
275
+ def __bool__(self):
276
+ return len(self.tools) > 0
277
+
278
+ def __iter__(self):
279
+ return iter(self.tools)
280
+
281
+ def __repr__(self):
282
+ return f"ToolSet(tools={self.tools})"
283
+
284
+ def __str__(self):
285
+ return f"ToolSet(tools={self.tools})"
@@ -0,0 +1,17 @@
1
+ from collections.abc import AsyncGenerator
2
+ from typing import Any, Generic
3
+
4
+ import mcp
5
+
6
+ from .run_context import ContextWrapper, TContext
7
+ from .tool import FunctionTool
8
+
9
+
10
+ class BaseFunctionToolExecutor(Generic[TContext]):
11
+ @classmethod
12
+ async def execute(
13
+ cls,
14
+ tool: FunctionTool,
15
+ run_context: ContextWrapper[TContext],
16
+ **tool_args,
17
+ ) -> AsyncGenerator[Any | mcp.types.CallToolResult, None]: ...