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,19 @@
1
+ from pydantic import Field
2
+ from pydantic.dataclasses import dataclass
3
+
4
+ from astrbot.core.agent.run_context import ContextWrapper
5
+ from astrbot.core.platform.astr_message_event import AstrMessageEvent
6
+ from astrbot.core.star.context import Context
7
+
8
+
9
+ @dataclass(config={"arbitrary_types_allowed": True})
10
+ class AstrAgentContext:
11
+ context: Context
12
+ """The star context instance"""
13
+ event: AstrMessageEvent
14
+ """The message event associated with the agent context."""
15
+ extra: dict[str, str] = Field(default_factory=dict)
16
+ """Customized extra data."""
17
+
18
+
19
+ AgentContextWrapper = ContextWrapper[AstrAgentContext]
@@ -0,0 +1,36 @@
1
+ from typing import Any
2
+
3
+ from mcp.types import CallToolResult
4
+
5
+ from astrbot.core.agent.hooks import BaseAgentRunHooks
6
+ from astrbot.core.agent.run_context import ContextWrapper
7
+ from astrbot.core.agent.tool import FunctionTool
8
+ from astrbot.core.astr_agent_context import AstrAgentContext
9
+ from astrbot.core.pipeline.context_utils import call_event_hook
10
+ from astrbot.core.star.star_handler import EventType
11
+
12
+
13
+ class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]):
14
+ async def on_agent_done(self, run_context, llm_response):
15
+ # 执行事件钩子
16
+ await call_event_hook(
17
+ run_context.context.event,
18
+ EventType.OnLLMResponseEvent,
19
+ llm_response,
20
+ )
21
+
22
+ async def on_tool_end(
23
+ self,
24
+ run_context: ContextWrapper[AstrAgentContext],
25
+ tool: FunctionTool[Any],
26
+ tool_args: dict | None,
27
+ tool_result: CallToolResult | None,
28
+ ):
29
+ run_context.context.event.clear_result()
30
+
31
+
32
+ class EmptyAgentHooks(BaseAgentRunHooks[AstrAgentContext]):
33
+ pass
34
+
35
+
36
+ MAIN_AGENT_HOOKS = MainAgentHooks()
@@ -0,0 +1,80 @@
1
+ import traceback
2
+ from collections.abc import AsyncGenerator
3
+
4
+ from astrbot.core import logger
5
+ from astrbot.core.agent.runners.tool_loop_agent_runner import ToolLoopAgentRunner
6
+ from astrbot.core.astr_agent_context import AstrAgentContext
7
+ from astrbot.core.message.message_event_result import (
8
+ MessageChain,
9
+ MessageEventResult,
10
+ ResultContentType,
11
+ )
12
+
13
+ AgentRunner = ToolLoopAgentRunner[AstrAgentContext]
14
+
15
+
16
+ async def run_agent(
17
+ agent_runner: AgentRunner,
18
+ max_step: int = 30,
19
+ show_tool_use: bool = True,
20
+ stream_to_general: bool = False,
21
+ show_reasoning: bool = False,
22
+ ) -> AsyncGenerator[MessageChain | None, None]:
23
+ step_idx = 0
24
+ astr_event = agent_runner.run_context.context.event
25
+ while step_idx < max_step:
26
+ step_idx += 1
27
+ try:
28
+ async for resp in agent_runner.step():
29
+ if astr_event.is_stopped():
30
+ return
31
+ if resp.type == "tool_call_result":
32
+ msg_chain = resp.data["chain"]
33
+ if msg_chain.type == "tool_direct_result":
34
+ # tool_direct_result 用于标记 llm tool 需要直接发送给用户的内容
35
+ await astr_event.send(resp.data["chain"])
36
+ continue
37
+ # 对于其他情况,暂时先不处理
38
+ continue
39
+ elif resp.type == "tool_call":
40
+ if agent_runner.streaming:
41
+ # 用来标记流式响应需要分节
42
+ yield MessageChain(chain=[], type="break")
43
+ if show_tool_use:
44
+ await astr_event.send(resp.data["chain"])
45
+ continue
46
+
47
+ if stream_to_general and resp.type == "streaming_delta":
48
+ continue
49
+
50
+ if stream_to_general or not agent_runner.streaming:
51
+ content_typ = (
52
+ ResultContentType.LLM_RESULT
53
+ if resp.type == "llm_result"
54
+ else ResultContentType.GENERAL_RESULT
55
+ )
56
+ astr_event.set_result(
57
+ MessageEventResult(
58
+ chain=resp.data["chain"].chain,
59
+ result_content_type=content_typ,
60
+ ),
61
+ )
62
+ yield
63
+ astr_event.clear_result()
64
+ elif resp.type == "streaming_delta":
65
+ chain = resp.data["chain"]
66
+ if chain.type == "reasoning" and not show_reasoning:
67
+ # display the reasoning content only when configured
68
+ continue
69
+ yield resp.data["chain"] # MessageChain
70
+ if agent_runner.done():
71
+ break
72
+
73
+ except Exception as e:
74
+ logger.error(traceback.format_exc())
75
+ err_msg = f"\n\nAstrBot 请求失败。\n错误类型: {type(e).__name__}\n错误信息: {e!s}\n\n请在控制台查看和分享错误详情。\n"
76
+ if agent_runner.streaming:
77
+ yield MessageChain().message(err_msg)
78
+ else:
79
+ astr_event.set_result(MessageEventResult().message(err_msg))
80
+ return
@@ -0,0 +1,246 @@
1
+ import asyncio
2
+ import inspect
3
+ import traceback
4
+ import typing as T
5
+
6
+ import mcp
7
+
8
+ from astrbot import logger
9
+ from astrbot.core.agent.handoff import HandoffTool
10
+ from astrbot.core.agent.mcp_client import MCPTool
11
+ from astrbot.core.agent.run_context import ContextWrapper
12
+ from astrbot.core.agent.tool import FunctionTool, ToolSet
13
+ from astrbot.core.agent.tool_executor import BaseFunctionToolExecutor
14
+ from astrbot.core.astr_agent_context import AstrAgentContext
15
+ from astrbot.core.message.message_event_result import (
16
+ CommandResult,
17
+ MessageChain,
18
+ MessageEventResult,
19
+ )
20
+ from astrbot.core.provider.register import llm_tools
21
+
22
+
23
+ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
24
+ @classmethod
25
+ async def execute(cls, tool, run_context, **tool_args):
26
+ """执行函数调用。
27
+
28
+ Args:
29
+ event (AstrMessageEvent): 事件对象, 当 origin 为 local 时必须提供。
30
+ **kwargs: 函数调用的参数。
31
+
32
+ Returns:
33
+ AsyncGenerator[None | mcp.types.CallToolResult, None]
34
+
35
+ """
36
+ if isinstance(tool, HandoffTool):
37
+ async for r in cls._execute_handoff(tool, run_context, **tool_args):
38
+ yield r
39
+ return
40
+
41
+ elif isinstance(tool, MCPTool):
42
+ async for r in cls._execute_mcp(tool, run_context, **tool_args):
43
+ yield r
44
+ return
45
+
46
+ else:
47
+ async for r in cls._execute_local(tool, run_context, **tool_args):
48
+ yield r
49
+ return
50
+
51
+ @classmethod
52
+ async def _execute_handoff(
53
+ cls,
54
+ tool: HandoffTool,
55
+ run_context: ContextWrapper[AstrAgentContext],
56
+ **tool_args,
57
+ ):
58
+ input_ = tool_args.get("input")
59
+
60
+ # make toolset for the agent
61
+ tools = tool.agent.tools
62
+ if tools:
63
+ toolset = ToolSet()
64
+ for t in tools:
65
+ if isinstance(t, str):
66
+ _t = llm_tools.get_func(t)
67
+ if _t:
68
+ toolset.add_tool(_t)
69
+ elif isinstance(t, FunctionTool):
70
+ toolset.add_tool(t)
71
+ else:
72
+ toolset = None
73
+
74
+ ctx = run_context.context.context
75
+ event = run_context.context.event
76
+ umo = event.unified_msg_origin
77
+ prov_id = await ctx.get_current_chat_provider_id(umo)
78
+ llm_resp = await ctx.tool_loop_agent(
79
+ event=event,
80
+ chat_provider_id=prov_id,
81
+ prompt=input_,
82
+ system_prompt=tool.agent.instructions,
83
+ tools=toolset,
84
+ max_steps=30,
85
+ run_hooks=tool.agent.run_hooks,
86
+ )
87
+ yield mcp.types.CallToolResult(
88
+ content=[mcp.types.TextContent(type="text", text=llm_resp.completion_text)]
89
+ )
90
+
91
+ @classmethod
92
+ async def _execute_local(
93
+ cls,
94
+ tool: FunctionTool,
95
+ run_context: ContextWrapper[AstrAgentContext],
96
+ **tool_args,
97
+ ):
98
+ event = run_context.context.event
99
+ if not event:
100
+ raise ValueError("Event must be provided for local function tools.")
101
+
102
+ is_override_call = False
103
+ for ty in type(tool).mro():
104
+ if "call" in ty.__dict__ and ty.__dict__["call"] is not FunctionTool.call:
105
+ is_override_call = True
106
+ break
107
+
108
+ # 检查 tool 下有没有 run 方法
109
+ if not tool.handler and not hasattr(tool, "run") and not is_override_call:
110
+ raise ValueError("Tool must have a valid handler or override 'run' method.")
111
+
112
+ awaitable = None
113
+ method_name = ""
114
+ if tool.handler:
115
+ awaitable = tool.handler
116
+ method_name = "decorator_handler"
117
+ elif is_override_call:
118
+ awaitable = tool.call
119
+ method_name = "call"
120
+ elif hasattr(tool, "run"):
121
+ awaitable = getattr(tool, "run")
122
+ method_name = "run"
123
+ if awaitable is None:
124
+ raise ValueError("Tool must have a valid handler or override 'run' method.")
125
+
126
+ wrapper = call_local_llm_tool(
127
+ context=run_context,
128
+ handler=awaitable,
129
+ method_name=method_name,
130
+ **tool_args,
131
+ )
132
+ while True:
133
+ try:
134
+ resp = await asyncio.wait_for(
135
+ anext(wrapper),
136
+ timeout=run_context.tool_call_timeout,
137
+ )
138
+ if resp is not None:
139
+ if isinstance(resp, mcp.types.CallToolResult):
140
+ yield resp
141
+ else:
142
+ text_content = mcp.types.TextContent(
143
+ type="text",
144
+ text=str(resp),
145
+ )
146
+ yield mcp.types.CallToolResult(content=[text_content])
147
+ else:
148
+ # NOTE: Tool 在这里直接请求发送消息给用户
149
+ # TODO: 是否需要判断 event.get_result() 是否为空?
150
+ # 如果为空,则说明没有发送消息给用户,并且返回值为空,将返回一个特殊的 TextContent,其内容如"工具没有返回内容"
151
+ if res := run_context.context.event.get_result():
152
+ if res.chain:
153
+ try:
154
+ await event.send(
155
+ MessageChain(
156
+ chain=res.chain,
157
+ type="tool_direct_result",
158
+ )
159
+ )
160
+ except Exception as e:
161
+ logger.error(
162
+ f"Tool 直接发送消息失败: {e}",
163
+ exc_info=True,
164
+ )
165
+ yield None
166
+ except asyncio.TimeoutError:
167
+ raise Exception(
168
+ f"tool {tool.name} execution timeout after {run_context.tool_call_timeout} seconds.",
169
+ )
170
+ except StopAsyncIteration:
171
+ break
172
+
173
+ @classmethod
174
+ async def _execute_mcp(
175
+ cls,
176
+ tool: FunctionTool,
177
+ run_context: ContextWrapper[AstrAgentContext],
178
+ **tool_args,
179
+ ):
180
+ res = await tool.call(run_context, **tool_args)
181
+ if not res:
182
+ return
183
+ yield res
184
+
185
+
186
+ async def call_local_llm_tool(
187
+ context: ContextWrapper[AstrAgentContext],
188
+ handler: T.Callable[..., T.Awaitable[T.Any]],
189
+ method_name: str,
190
+ *args,
191
+ **kwargs,
192
+ ) -> T.AsyncGenerator[T.Any, None]:
193
+ """执行本地 LLM 工具的处理函数并处理其返回结果"""
194
+ ready_to_call = None # 一个协程或者异步生成器
195
+
196
+ trace_ = None
197
+
198
+ event = context.context.event
199
+
200
+ try:
201
+ if method_name == "run" or method_name == "decorator_handler":
202
+ ready_to_call = handler(event, *args, **kwargs)
203
+ elif method_name == "call":
204
+ ready_to_call = handler(context, *args, **kwargs)
205
+ else:
206
+ raise ValueError(f"未知的方法名: {method_name}")
207
+ except ValueError as e:
208
+ logger.error(f"调用本地 LLM 工具时出错: {e}", exc_info=True)
209
+ except TypeError:
210
+ logger.error("处理函数参数不匹配,请检查 handler 的定义。", exc_info=True)
211
+ except Exception as e:
212
+ trace_ = traceback.format_exc()
213
+ logger.error(f"调用本地 LLM 工具时出错: {e}\n{trace_}")
214
+
215
+ if not ready_to_call:
216
+ return
217
+
218
+ if inspect.isasyncgen(ready_to_call):
219
+ _has_yielded = False
220
+ try:
221
+ async for ret in ready_to_call:
222
+ # 这里逐步执行异步生成器, 对于每个 yield 返回的 ret, 执行下面的代码
223
+ # 返回值只能是 MessageEventResult 或者 None(无返回值)
224
+ _has_yielded = True
225
+ if isinstance(ret, (MessageEventResult, CommandResult)):
226
+ # 如果返回值是 MessageEventResult, 设置结果并继续
227
+ event.set_result(ret)
228
+ yield
229
+ else:
230
+ # 如果返回值是 None, 则不设置结果并继续
231
+ # 继续执行后续阶段
232
+ yield ret
233
+ if not _has_yielded:
234
+ # 如果这个异步生成器没有执行到 yield 分支
235
+ yield
236
+ except Exception as e:
237
+ logger.error(f"Previous Error: {trace_}")
238
+ raise e
239
+ elif inspect.iscoroutine(ready_to_call):
240
+ # 如果只是一个协程, 直接执行
241
+ ret = await ready_to_call
242
+ if isinstance(ret, (MessageEventResult, CommandResult)):
243
+ event.set_result(ret)
244
+ yield
245
+ else:
246
+ yield ret
@@ -0,0 +1,275 @@
1
+ import os
2
+ import uuid
3
+ from typing import TypedDict, TypeVar
4
+
5
+ from astrbot.core import AstrBotConfig, logger
6
+ from astrbot.core.config.astrbot_config import ASTRBOT_CONFIG_PATH
7
+ from astrbot.core.config.default import DEFAULT_CONFIG
8
+ from astrbot.core.platform.message_session import MessageSession
9
+ from astrbot.core.umop_config_router import UmopConfigRouter
10
+ from astrbot.core.utils.astrbot_path import get_astrbot_config_path
11
+ from astrbot.core.utils.shared_preferences import SharedPreferences
12
+
13
+ _VT = TypeVar("_VT")
14
+
15
+
16
+ class ConfInfo(TypedDict):
17
+ """Configuration information for a specific session or platform."""
18
+
19
+ id: str # UUID of the configuration or "default"
20
+ name: str
21
+ path: str # File name to the configuration file
22
+
23
+
24
+ DEFAULT_CONFIG_CONF_INFO = ConfInfo(
25
+ id="default",
26
+ name="default",
27
+ path=ASTRBOT_CONFIG_PATH,
28
+ )
29
+
30
+
31
+ class AstrBotConfigManager:
32
+ """A class to manage the system configuration of AstrBot, aka ACM"""
33
+
34
+ def __init__(
35
+ self,
36
+ default_config: AstrBotConfig,
37
+ ucr: UmopConfigRouter,
38
+ sp: SharedPreferences,
39
+ ):
40
+ self.sp = sp
41
+ self.ucr = ucr
42
+ self.confs: dict[str, AstrBotConfig] = {}
43
+ """uuid / "default" -> AstrBotConfig"""
44
+ self.confs["default"] = default_config
45
+ self.abconf_data = None
46
+ self._load_all_configs()
47
+
48
+ def _get_abconf_data(self) -> dict:
49
+ """获取所有的 abconf 数据"""
50
+ if self.abconf_data is None:
51
+ self.abconf_data = self.sp.get(
52
+ "abconf_mapping",
53
+ {},
54
+ scope="global",
55
+ scope_id="global",
56
+ )
57
+ return self.abconf_data
58
+
59
+ def _load_all_configs(self):
60
+ """Load all configurations from the shared preferences."""
61
+ abconf_data = self._get_abconf_data()
62
+ self.abconf_data = abconf_data
63
+ for uuid_, meta in abconf_data.items():
64
+ filename = meta["path"]
65
+ conf_path = os.path.join(get_astrbot_config_path(), filename)
66
+ if os.path.exists(conf_path):
67
+ conf = AstrBotConfig(config_path=conf_path)
68
+ self.confs[uuid_] = conf
69
+ else:
70
+ logger.warning(
71
+ f"Config file {conf_path} for UUID {uuid_} does not exist, skipping.",
72
+ )
73
+ continue
74
+
75
+ def _load_conf_mapping(self, umo: str | MessageSession) -> ConfInfo:
76
+ """获取指定 umo 的配置文件 uuid, 如果不存在则返回默认配置(返回 "default")
77
+
78
+ Returns:
79
+ ConfInfo: 包含配置文件的 uuid, 路径和名称等信息, 是一个 dict 类型
80
+
81
+ """
82
+ # uuid -> { "path": str, "name": str }
83
+ abconf_data = self._get_abconf_data()
84
+
85
+ if isinstance(umo, MessageSession):
86
+ umo = str(umo)
87
+ else:
88
+ try:
89
+ umo = str(MessageSession.from_str(umo)) # validate
90
+ except Exception:
91
+ return DEFAULT_CONFIG_CONF_INFO
92
+
93
+ conf_id = self.ucr.get_conf_id_for_umop(umo)
94
+ if conf_id:
95
+ meta = abconf_data.get(conf_id)
96
+ if meta and isinstance(meta, dict):
97
+ # the bind relation between umo and conf is defined in ucr now, so we remove "umop" here
98
+ meta.pop("umop", None)
99
+ return ConfInfo(**meta, id=conf_id)
100
+
101
+ return DEFAULT_CONFIG_CONF_INFO
102
+
103
+ def _save_conf_mapping(
104
+ self,
105
+ abconf_path: str,
106
+ abconf_id: str,
107
+ abconf_name: str | None = None,
108
+ ) -> None:
109
+ """保存配置文件的映射关系"""
110
+ abconf_data = self.sp.get(
111
+ "abconf_mapping",
112
+ {},
113
+ scope="global",
114
+ scope_id="global",
115
+ )
116
+ random_word = abconf_name or uuid.uuid4().hex[:8]
117
+ abconf_data[abconf_id] = {
118
+ "path": abconf_path,
119
+ "name": random_word,
120
+ }
121
+ self.sp.put("abconf_mapping", abconf_data, scope="global", scope_id="global")
122
+ self.abconf_data = abconf_data
123
+
124
+ def get_conf(self, umo: str | MessageSession | None) -> AstrBotConfig:
125
+ """获取指定 umo 的配置文件。如果不存在,则 fallback 到默认配置文件。"""
126
+ if not umo:
127
+ return self.confs["default"]
128
+ if isinstance(umo, MessageSession):
129
+ umo = f"{umo.platform_id}:{umo.message_type}:{umo.session_id}"
130
+
131
+ uuid_ = self._load_conf_mapping(umo)["id"]
132
+
133
+ conf = self.confs.get(uuid_)
134
+ if not conf:
135
+ conf = self.confs["default"] # default MUST exists
136
+
137
+ return conf
138
+
139
+ @property
140
+ def default_conf(self) -> AstrBotConfig:
141
+ """获取默认配置文件"""
142
+ return self.confs["default"]
143
+
144
+ def get_conf_info(self, umo: str | MessageSession) -> ConfInfo:
145
+ """获取指定 umo 的配置文件元数据"""
146
+ if isinstance(umo, MessageSession):
147
+ umo = f"{umo.platform_id}:{umo.message_type}:{umo.session_id}"
148
+
149
+ return self._load_conf_mapping(umo)
150
+
151
+ def get_conf_list(self) -> list[ConfInfo]:
152
+ """获取所有配置文件的元数据列表"""
153
+ conf_list = []
154
+ abconf_mapping = self._get_abconf_data()
155
+ for uuid_, meta in abconf_mapping.items():
156
+ if not isinstance(meta, dict):
157
+ continue
158
+ meta.pop("umop", None)
159
+ conf_list.append(ConfInfo(**meta, id=uuid_))
160
+ conf_list.append(DEFAULT_CONFIG_CONF_INFO)
161
+ return conf_list
162
+
163
+ def create_conf(
164
+ self,
165
+ config: dict = DEFAULT_CONFIG,
166
+ name: str | None = None,
167
+ ) -> str:
168
+ conf_uuid = str(uuid.uuid4())
169
+ conf_file_name = f"abconf_{conf_uuid}.json"
170
+ conf_path = os.path.join(get_astrbot_config_path(), conf_file_name)
171
+ conf = AstrBotConfig(config_path=conf_path, default_config=config)
172
+ conf.save_config()
173
+ self._save_conf_mapping(conf_file_name, conf_uuid, abconf_name=name)
174
+ self.confs[conf_uuid] = conf
175
+ return conf_uuid
176
+
177
+ def delete_conf(self, conf_id: str) -> bool:
178
+ """删除指定配置文件
179
+
180
+ Args:
181
+ conf_id: 配置文件的 UUID
182
+
183
+ Returns:
184
+ bool: 删除是否成功
185
+
186
+ Raises:
187
+ ValueError: 如果试图删除默认配置文件
188
+
189
+ """
190
+ if conf_id == "default":
191
+ raise ValueError("不能删除默认配置文件")
192
+
193
+ # 从映射中移除
194
+ abconf_data = self.sp.get(
195
+ "abconf_mapping",
196
+ {},
197
+ scope="global",
198
+ scope_id="global",
199
+ )
200
+ if conf_id not in abconf_data:
201
+ logger.warning(f"配置文件 {conf_id} 不存在于映射中")
202
+ return False
203
+
204
+ # 获取配置文件路径
205
+ conf_path = os.path.join(
206
+ get_astrbot_config_path(),
207
+ abconf_data[conf_id]["path"],
208
+ )
209
+
210
+ # 删除配置文件
211
+ try:
212
+ if os.path.exists(conf_path):
213
+ os.remove(conf_path)
214
+ logger.info(f"已删除配置文件: {conf_path}")
215
+ except Exception as e:
216
+ logger.error(f"删除配置文件 {conf_path} 失败: {e}")
217
+ return False
218
+
219
+ # 从内存中移除
220
+ if conf_id in self.confs:
221
+ del self.confs[conf_id]
222
+
223
+ # 从映射中移除
224
+ del abconf_data[conf_id]
225
+ self.sp.put("abconf_mapping", abconf_data, scope="global", scope_id="global")
226
+ self.abconf_data = abconf_data
227
+
228
+ logger.info(f"成功删除配置文件 {conf_id}")
229
+ return True
230
+
231
+ def update_conf_info(self, conf_id: str, name: str | None = None) -> bool:
232
+ """更新配置文件信息
233
+
234
+ Args:
235
+ conf_id: 配置文件的 UUID
236
+ name: 新的配置文件名称 (可选)
237
+
238
+ Returns:
239
+ bool: 更新是否成功
240
+
241
+ """
242
+ if conf_id == "default":
243
+ raise ValueError("不能更新默认配置文件的信息")
244
+
245
+ abconf_data = self.sp.get(
246
+ "abconf_mapping",
247
+ {},
248
+ scope="global",
249
+ scope_id="global",
250
+ )
251
+ if conf_id not in abconf_data:
252
+ logger.warning(f"配置文件 {conf_id} 不存在于映射中")
253
+ return False
254
+
255
+ # 更新名称
256
+ if name is not None:
257
+ abconf_data[conf_id]["name"] = name
258
+
259
+ # 保存更新
260
+ self.sp.put("abconf_mapping", abconf_data, scope="global", scope_id="global")
261
+ self.abconf_data = abconf_data
262
+ logger.info(f"成功更新配置文件 {conf_id} 的信息")
263
+ return True
264
+
265
+ def g(
266
+ self,
267
+ umo: str | None = None,
268
+ key: str | None = None,
269
+ default: _VT = None,
270
+ ) -> _VT:
271
+ """获取配置项。umo 为 None 时使用默认配置"""
272
+ if umo is None:
273
+ return self.confs["default"].get(key, default)
274
+ conf = self.get_conf(umo)
275
+ return conf.get(key, default)
@@ -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
  ]