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,464 @@
1
+ """本地 Agent 模式的 LLM 调用 Stage"""
2
+
3
+ import asyncio
4
+ import copy
5
+ import json
6
+ from collections.abc import AsyncGenerator
7
+
8
+ from astrbot.core import logger
9
+ from astrbot.core.agent.tool import ToolSet
10
+ from astrbot.core.astr_agent_context import AstrAgentContext
11
+ from astrbot.core.conversation_mgr import Conversation
12
+ from astrbot.core.message.components import Image
13
+ from astrbot.core.message.message_event_result import (
14
+ MessageChain,
15
+ MessageEventResult,
16
+ ResultContentType,
17
+ )
18
+ from astrbot.core.platform.astr_message_event import AstrMessageEvent
19
+ from astrbot.core.provider import Provider
20
+ from astrbot.core.provider.entities import (
21
+ LLMResponse,
22
+ ProviderRequest,
23
+ )
24
+ from astrbot.core.star.star_handler import EventType, star_map
25
+ from astrbot.core.utils.metrics import Metric
26
+ from astrbot.core.utils.session_lock import session_lock_manager
27
+
28
+ from .....astr_agent_context import AgentContextWrapper
29
+ from .....astr_agent_hooks import MAIN_AGENT_HOOKS
30
+ from .....astr_agent_run_util import AgentRunner, run_agent
31
+ from .....astr_agent_tool_exec import FunctionToolExecutor
32
+ from ....context import PipelineContext, call_event_hook
33
+ from ...stage import Stage
34
+ from ...utils import KNOWLEDGE_BASE_QUERY_TOOL, retrieve_knowledge_base
35
+
36
+
37
+ class InternalAgentSubStage(Stage):
38
+ async def initialize(self, ctx: PipelineContext) -> None:
39
+ self.ctx = ctx
40
+ conf = ctx.astrbot_config
41
+ settings = conf["provider_settings"]
42
+ self.max_context_length = settings["max_context_length"] # int
43
+ self.dequeue_context_length: int = min(
44
+ max(1, settings["dequeue_context_length"]),
45
+ self.max_context_length - 1,
46
+ )
47
+ self.streaming_response: bool = settings["streaming_response"]
48
+ self.unsupported_streaming_strategy: str = settings[
49
+ "unsupported_streaming_strategy"
50
+ ]
51
+ self.max_step: int = settings.get("max_agent_step", 30)
52
+ self.tool_call_timeout: int = settings.get("tool_call_timeout", 60)
53
+ if isinstance(self.max_step, bool): # workaround: #2622
54
+ self.max_step = 30
55
+ self.show_tool_use: bool = settings.get("show_tool_use_status", True)
56
+ self.show_reasoning = settings.get("display_reasoning_text", False)
57
+ self.kb_agentic_mode: bool = conf.get("kb_agentic_mode", False)
58
+
59
+ self.conv_manager = ctx.plugin_manager.context.conversation_manager
60
+
61
+ def _select_provider(self, event: AstrMessageEvent):
62
+ """选择使用的 LLM 提供商"""
63
+ sel_provider = event.get_extra("selected_provider")
64
+ _ctx = self.ctx.plugin_manager.context
65
+ if sel_provider and isinstance(sel_provider, str):
66
+ provider = _ctx.get_provider_by_id(sel_provider)
67
+ if not provider:
68
+ logger.error(f"未找到指定的提供商: {sel_provider}。")
69
+ return provider
70
+
71
+ return _ctx.get_using_provider(umo=event.unified_msg_origin)
72
+
73
+ async def _get_session_conv(self, event: AstrMessageEvent) -> Conversation:
74
+ umo = event.unified_msg_origin
75
+ conv_mgr = self.conv_manager
76
+
77
+ # 获取对话上下文
78
+ cid = await conv_mgr.get_curr_conversation_id(umo)
79
+ if not cid:
80
+ cid = await conv_mgr.new_conversation(umo, event.get_platform_id())
81
+ conversation = await conv_mgr.get_conversation(umo, cid)
82
+ if not conversation:
83
+ cid = await conv_mgr.new_conversation(umo, event.get_platform_id())
84
+ conversation = await conv_mgr.get_conversation(umo, cid)
85
+ if not conversation:
86
+ raise RuntimeError("无法创建新的对话。")
87
+ return conversation
88
+
89
+ async def _apply_kb(
90
+ self,
91
+ event: AstrMessageEvent,
92
+ req: ProviderRequest,
93
+ ):
94
+ """Apply knowledge base context to the provider request"""
95
+ if not self.kb_agentic_mode:
96
+ if req.prompt is None:
97
+ return
98
+ try:
99
+ kb_result = await retrieve_knowledge_base(
100
+ query=req.prompt,
101
+ umo=event.unified_msg_origin,
102
+ context=self.ctx.plugin_manager.context,
103
+ )
104
+ if not kb_result:
105
+ return
106
+ if req.system_prompt is not None:
107
+ req.system_prompt += (
108
+ f"\n\n[Related Knowledge Base Results]:\n{kb_result}"
109
+ )
110
+ except Exception as e:
111
+ logger.error(f"Error occurred while retrieving knowledge base: {e}")
112
+ else:
113
+ if req.func_tool is None:
114
+ req.func_tool = ToolSet()
115
+ req.func_tool.add_tool(KNOWLEDGE_BASE_QUERY_TOOL)
116
+
117
+ def _truncate_contexts(
118
+ self,
119
+ contexts: list[dict],
120
+ ) -> list[dict]:
121
+ """截断上下文列表,确保不超过最大长度"""
122
+ if self.max_context_length == -1:
123
+ return contexts
124
+
125
+ if len(contexts) // 2 <= self.max_context_length:
126
+ return contexts
127
+
128
+ truncated_contexts = contexts[
129
+ -(self.max_context_length - self.dequeue_context_length + 1) * 2 :
130
+ ]
131
+ # 找到第一个role 为 user 的索引,确保上下文格式正确
132
+ index = next(
133
+ (
134
+ i
135
+ for i, item in enumerate(truncated_contexts)
136
+ if item.get("role") == "user"
137
+ ),
138
+ None,
139
+ )
140
+ if index is not None and index > 0:
141
+ truncated_contexts = truncated_contexts[index:]
142
+
143
+ return truncated_contexts
144
+
145
+ def _modalities_fix(
146
+ self,
147
+ provider: Provider,
148
+ req: ProviderRequest,
149
+ ):
150
+ """检查提供商的模态能力,清理请求中的不支持内容"""
151
+ if req.image_urls:
152
+ provider_cfg = provider.provider_config.get("modalities", ["image"])
153
+ if "image" not in provider_cfg:
154
+ logger.debug(f"用户设置提供商 {provider} 不支持图像,清空图像列表。")
155
+ req.image_urls = []
156
+ if req.func_tool:
157
+ provider_cfg = provider.provider_config.get("modalities", ["tool_use"])
158
+ # 如果模型不支持工具使用,但请求中包含工具列表,则清空。
159
+ if "tool_use" not in provider_cfg:
160
+ logger.debug(
161
+ f"用户设置提供商 {provider} 不支持工具使用,清空工具列表。",
162
+ )
163
+ req.func_tool = None
164
+
165
+ def _plugin_tool_fix(
166
+ self,
167
+ event: AstrMessageEvent,
168
+ req: ProviderRequest,
169
+ ):
170
+ """根据事件中的插件设置,过滤请求中的工具列表"""
171
+ if event.plugins_name is not None and req.func_tool:
172
+ new_tool_set = ToolSet()
173
+ for tool in req.func_tool.tools:
174
+ mp = tool.handler_module_path
175
+ if not mp:
176
+ continue
177
+ plugin = star_map.get(mp)
178
+ if not plugin:
179
+ continue
180
+ if plugin.name in event.plugins_name or plugin.reserved:
181
+ new_tool_set.add_tool(tool)
182
+ req.func_tool = new_tool_set
183
+
184
+ async def _handle_webchat(
185
+ self,
186
+ event: AstrMessageEvent,
187
+ req: ProviderRequest,
188
+ prov: Provider,
189
+ ):
190
+ """处理 WebChat 平台的特殊情况,包括第一次 LLM 对话时总结对话内容生成 title"""
191
+ if not req.conversation:
192
+ return
193
+ conversation = await self.conv_manager.get_conversation(
194
+ event.unified_msg_origin,
195
+ req.conversation.cid,
196
+ )
197
+ if conversation and not req.conversation.title:
198
+ messages = json.loads(conversation.history)
199
+ latest_pair = messages[-2:]
200
+ if not latest_pair:
201
+ return
202
+ content = latest_pair[0].get("content", "")
203
+ if isinstance(content, list):
204
+ # 多模态
205
+ text_parts = []
206
+ for item in content:
207
+ if isinstance(item, dict):
208
+ if item.get("type") == "text":
209
+ text_parts.append(item.get("text", ""))
210
+ elif item.get("type") == "image":
211
+ text_parts.append("[图片]")
212
+ elif isinstance(item, str):
213
+ text_parts.append(item)
214
+ cleaned_text = "User: " + " ".join(text_parts).strip()
215
+ elif isinstance(content, str):
216
+ cleaned_text = "User: " + content.strip()
217
+ else:
218
+ return
219
+ logger.debug(f"WebChat 对话标题生成请求,清理后的文本: {cleaned_text}")
220
+ llm_resp = await prov.text_chat(
221
+ system_prompt="You are expert in summarizing user's query.",
222
+ prompt=(
223
+ f"Please summarize the following query of user:\n"
224
+ f"{cleaned_text}\n"
225
+ "Only output the summary within 10 words, DO NOT INCLUDE any other text."
226
+ "You must use the same language as the user."
227
+ "If you think the dialog is too short to summarize, only output a special mark: `<None>`"
228
+ ),
229
+ )
230
+ if llm_resp and llm_resp.completion_text:
231
+ title = llm_resp.completion_text.strip()
232
+ if not title or "<None>" in title:
233
+ return
234
+ await self.conv_manager.update_conversation_title(
235
+ unified_msg_origin=event.unified_msg_origin,
236
+ title=title,
237
+ conversation_id=req.conversation.cid,
238
+ )
239
+
240
+ async def _save_to_history(
241
+ self,
242
+ event: AstrMessageEvent,
243
+ req: ProviderRequest,
244
+ llm_response: LLMResponse | None,
245
+ ):
246
+ if (
247
+ not req
248
+ or not req.conversation
249
+ or not llm_response
250
+ or llm_response.role != "assistant"
251
+ ):
252
+ return
253
+
254
+ if not llm_response.completion_text and not req.tool_calls_result:
255
+ logger.debug("LLM 响应为空,不保存记录。")
256
+ return
257
+
258
+ if req.contexts is None:
259
+ req.contexts = []
260
+
261
+ # 历史上下文
262
+ messages = copy.deepcopy(req.contexts)
263
+ # 这一轮对话请求的用户输入
264
+ messages.append(await req.assemble_context())
265
+ # 这一轮对话的 LLM 响应
266
+ if req.tool_calls_result:
267
+ if not isinstance(req.tool_calls_result, list):
268
+ messages.extend(req.tool_calls_result.to_openai_messages())
269
+ elif isinstance(req.tool_calls_result, list):
270
+ for tcr in req.tool_calls_result:
271
+ messages.extend(tcr.to_openai_messages())
272
+ messages.append({"role": "assistant", "content": llm_response.completion_text})
273
+ messages = list(filter(lambda item: "_no_save" not in item, messages))
274
+ await self.conv_manager.update_conversation(
275
+ event.unified_msg_origin,
276
+ req.conversation.cid,
277
+ history=messages,
278
+ )
279
+
280
+ def _fix_messages(self, messages: list[dict]) -> list[dict]:
281
+ """验证并且修复上下文"""
282
+ fixed_messages = []
283
+ for message in messages:
284
+ if message.get("role") == "tool":
285
+ # tool block 前面必须要有 user 和 assistant block
286
+ if len(fixed_messages) < 2:
287
+ # 这种情况可能是上下文被截断导致的
288
+ # 我们直接将之前的上下文都清空
289
+ fixed_messages = []
290
+ else:
291
+ fixed_messages.append(message)
292
+ else:
293
+ fixed_messages.append(message)
294
+ return fixed_messages
295
+
296
+ async def process(
297
+ self, event: AstrMessageEvent, provider_wake_prefix: str
298
+ ) -> AsyncGenerator[None, None]:
299
+ req: ProviderRequest | None = None
300
+
301
+ provider = self._select_provider(event)
302
+ if provider is None:
303
+ return
304
+ if not isinstance(provider, Provider):
305
+ logger.error(f"选择的提供商类型无效({type(provider)}),跳过 LLM 请求处理。")
306
+ return
307
+
308
+ streaming_response = self.streaming_response
309
+ if (enable_streaming := event.get_extra("enable_streaming")) is not None:
310
+ streaming_response = bool(enable_streaming)
311
+
312
+ logger.debug("ready to request llm provider")
313
+ async with session_lock_manager.acquire_lock(event.unified_msg_origin):
314
+ logger.debug("acquired session lock for llm request")
315
+ if event.get_extra("provider_request"):
316
+ req = event.get_extra("provider_request")
317
+ assert isinstance(req, ProviderRequest), (
318
+ "provider_request 必须是 ProviderRequest 类型。"
319
+ )
320
+
321
+ if req.conversation:
322
+ req.contexts = json.loads(req.conversation.history)
323
+
324
+ else:
325
+ req = ProviderRequest()
326
+ req.prompt = ""
327
+ req.image_urls = []
328
+ if sel_model := event.get_extra("selected_model"):
329
+ req.model = sel_model
330
+ if provider_wake_prefix and not event.message_str.startswith(
331
+ provider_wake_prefix
332
+ ):
333
+ return
334
+
335
+ req.prompt = event.message_str[len(provider_wake_prefix) :]
336
+ # func_tool selection 现在已经转移到 packages/astrbot 插件中进行选择。
337
+ # req.func_tool = self.ctx.plugin_manager.context.get_llm_tool_manager()
338
+ for comp in event.message_obj.message:
339
+ if isinstance(comp, Image):
340
+ image_path = await comp.convert_to_file_path()
341
+ req.image_urls.append(image_path)
342
+
343
+ conversation = await self._get_session_conv(event)
344
+ req.conversation = conversation
345
+ req.contexts = json.loads(conversation.history)
346
+
347
+ event.set_extra("provider_request", req)
348
+
349
+ if not req.prompt and not req.image_urls:
350
+ return
351
+
352
+ # call event hook
353
+ if await call_event_hook(event, EventType.OnLLMRequestEvent, req):
354
+ return
355
+
356
+ # apply knowledge base feature
357
+ await self._apply_kb(event, req)
358
+
359
+ # fix contexts json str
360
+ if isinstance(req.contexts, str):
361
+ req.contexts = json.loads(req.contexts)
362
+
363
+ # truncate contexts to fit max length
364
+ if req.contexts:
365
+ req.contexts = self._truncate_contexts(req.contexts)
366
+ self._fix_messages(req.contexts)
367
+
368
+ # session_id
369
+ if not req.session_id:
370
+ req.session_id = event.unified_msg_origin
371
+
372
+ # check provider modalities, if provider does not support image/tool_use, clear them in request.
373
+ self._modalities_fix(provider, req)
374
+
375
+ # filter tools, only keep tools from this pipeline's selected plugins
376
+ self._plugin_tool_fix(event, req)
377
+
378
+ stream_to_general = (
379
+ self.unsupported_streaming_strategy == "turn_off"
380
+ and not event.platform_meta.support_streaming_message
381
+ )
382
+ # 备份 req.contexts
383
+ backup_contexts = copy.deepcopy(req.contexts)
384
+
385
+ # run agent
386
+ agent_runner = AgentRunner()
387
+ logger.debug(
388
+ f"handle provider[id: {provider.provider_config['id']}] request: {req}",
389
+ )
390
+ astr_agent_ctx = AstrAgentContext(
391
+ context=self.ctx.plugin_manager.context,
392
+ event=event,
393
+ )
394
+ await agent_runner.reset(
395
+ provider=provider,
396
+ request=req,
397
+ run_context=AgentContextWrapper(
398
+ context=astr_agent_ctx,
399
+ tool_call_timeout=self.tool_call_timeout,
400
+ ),
401
+ tool_executor=FunctionToolExecutor(),
402
+ agent_hooks=MAIN_AGENT_HOOKS,
403
+ streaming=streaming_response,
404
+ )
405
+
406
+ if streaming_response and not stream_to_general:
407
+ # 流式响应
408
+ event.set_result(
409
+ MessageEventResult()
410
+ .set_result_content_type(ResultContentType.STREAMING_RESULT)
411
+ .set_async_stream(
412
+ run_agent(
413
+ agent_runner,
414
+ self.max_step,
415
+ self.show_tool_use,
416
+ show_reasoning=self.show_reasoning,
417
+ ),
418
+ ),
419
+ )
420
+ yield
421
+ if agent_runner.done():
422
+ if final_llm_resp := agent_runner.get_final_llm_resp():
423
+ if final_llm_resp.completion_text:
424
+ chain = (
425
+ MessageChain()
426
+ .message(final_llm_resp.completion_text)
427
+ .chain
428
+ )
429
+ elif final_llm_resp.result_chain:
430
+ chain = final_llm_resp.result_chain.chain
431
+ else:
432
+ chain = MessageChain().chain
433
+ event.set_result(
434
+ MessageEventResult(
435
+ chain=chain,
436
+ result_content_type=ResultContentType.STREAMING_FINISH,
437
+ ),
438
+ )
439
+ else:
440
+ async for _ in run_agent(
441
+ agent_runner,
442
+ self.max_step,
443
+ self.show_tool_use,
444
+ stream_to_general,
445
+ show_reasoning=self.show_reasoning,
446
+ ):
447
+ yield
448
+
449
+ # 恢复备份的 contexts
450
+ req.contexts = backup_contexts
451
+
452
+ await self._save_to_history(event, req, agent_runner.get_final_llm_resp())
453
+
454
+ # 异步处理 WebChat 特殊情况
455
+ if event.get_platform_name() == "webchat":
456
+ asyncio.create_task(self._handle_webchat(event, req, provider))
457
+
458
+ asyncio.create_task(
459
+ Metric.upload(
460
+ llm_tick=1,
461
+ model_name=agent_runner.provider.get_model(),
462
+ provider_type=agent_runner.provider.meta().type,
463
+ ),
464
+ )
@@ -0,0 +1,202 @@
1
+ import asyncio
2
+ from collections.abc import AsyncGenerator
3
+ from typing import TYPE_CHECKING
4
+
5
+ from astrbot.core import logger
6
+ from astrbot.core.agent.runners.coze.coze_agent_runner import CozeAgentRunner
7
+ from astrbot.core.agent.runners.dashscope.dashscope_agent_runner import (
8
+ DashscopeAgentRunner,
9
+ )
10
+ from astrbot.core.agent.runners.dify.dify_agent_runner import DifyAgentRunner
11
+ from astrbot.core.message.components import Image
12
+ from astrbot.core.message.message_event_result import (
13
+ MessageChain,
14
+ MessageEventResult,
15
+ ResultContentType,
16
+ )
17
+
18
+ if TYPE_CHECKING:
19
+ from astrbot.core.agent.runners.base import BaseAgentRunner
20
+ from astrbot.core.platform.astr_message_event import AstrMessageEvent
21
+ from astrbot.core.provider.entities import (
22
+ ProviderRequest,
23
+ )
24
+ from astrbot.core.star.star_handler import EventType
25
+ from astrbot.core.utils.metrics import Metric
26
+
27
+ from .....astr_agent_context import AgentContextWrapper, AstrAgentContext
28
+ from .....astr_agent_hooks import MAIN_AGENT_HOOKS
29
+ from ....context import PipelineContext, call_event_hook
30
+ from ...stage import Stage
31
+
32
+ AGENT_RUNNER_TYPE_KEY = {
33
+ "dify": "dify_agent_runner_provider_id",
34
+ "coze": "coze_agent_runner_provider_id",
35
+ "dashscope": "dashscope_agent_runner_provider_id",
36
+ }
37
+
38
+
39
+ async def run_third_party_agent(
40
+ runner: "BaseAgentRunner",
41
+ stream_to_general: bool = False,
42
+ ) -> AsyncGenerator[MessageChain | None, None]:
43
+ """
44
+ 运行第三方 agent runner 并转换响应格式
45
+ 类似于 run_agent 函数,但专门处理第三方 agent runner
46
+ """
47
+ try:
48
+ async for resp in runner.step_until_done(max_step=30): # type: ignore[misc]
49
+ if resp.type == "streaming_delta":
50
+ if stream_to_general:
51
+ continue
52
+ yield resp.data["chain"]
53
+ elif resp.type == "llm_result":
54
+ if stream_to_general:
55
+ yield resp.data["chain"]
56
+ except Exception as e:
57
+ logger.error(f"Third party agent runner error: {e}")
58
+ err_msg = (
59
+ f"\nAstrBot 请求失败。\n错误类型: {type(e).__name__}\n"
60
+ f"错误信息: {e!s}\n\n请在控制台查看和分享错误详情。\n"
61
+ )
62
+ yield MessageChain().message(err_msg)
63
+
64
+
65
+ class ThirdPartyAgentSubStage(Stage):
66
+ async def initialize(self, ctx: PipelineContext) -> None:
67
+ self.ctx = ctx
68
+ self.conf = ctx.astrbot_config
69
+ self.runner_type = self.conf["provider_settings"]["agent_runner_type"]
70
+ self.prov_id = self.conf["provider_settings"].get(
71
+ AGENT_RUNNER_TYPE_KEY.get(self.runner_type, ""),
72
+ "",
73
+ )
74
+ settings = ctx.astrbot_config["provider_settings"]
75
+ self.streaming_response: bool = settings["streaming_response"]
76
+ self.unsupported_streaming_strategy: str = settings[
77
+ "unsupported_streaming_strategy"
78
+ ]
79
+
80
+ async def process(
81
+ self, event: AstrMessageEvent, provider_wake_prefix: str
82
+ ) -> AsyncGenerator[None, None]:
83
+ req: ProviderRequest | None = None
84
+
85
+ if provider_wake_prefix and not event.message_str.startswith(
86
+ provider_wake_prefix
87
+ ):
88
+ return
89
+
90
+ self.prov_cfg: dict = next(
91
+ (p for p in self.conf["provider"] if p["id"] == self.prov_id),
92
+ {},
93
+ )
94
+ if not self.prov_id or not self.prov_cfg:
95
+ logger.error(
96
+ "Third Party Agent Runner provider ID is not configured properly."
97
+ )
98
+ return
99
+
100
+ # make provider request
101
+ req = ProviderRequest()
102
+ req.session_id = event.unified_msg_origin
103
+ req.prompt = event.message_str[len(provider_wake_prefix) :]
104
+ for comp in event.message_obj.message:
105
+ if isinstance(comp, Image):
106
+ image_path = await comp.convert_to_base64()
107
+ req.image_urls.append(image_path)
108
+
109
+ if not req.prompt and not req.image_urls:
110
+ return
111
+
112
+ # call event hook
113
+ if await call_event_hook(event, EventType.OnLLMRequestEvent, req):
114
+ return
115
+
116
+ if self.runner_type == "dify":
117
+ runner = DifyAgentRunner[AstrAgentContext]()
118
+ elif self.runner_type == "coze":
119
+ runner = CozeAgentRunner[AstrAgentContext]()
120
+ elif self.runner_type == "dashscope":
121
+ runner = DashscopeAgentRunner[AstrAgentContext]()
122
+ else:
123
+ raise ValueError(
124
+ f"Unsupported third party agent runner type: {self.runner_type}",
125
+ )
126
+
127
+ astr_agent_ctx = AstrAgentContext(
128
+ context=self.ctx.plugin_manager.context,
129
+ event=event,
130
+ )
131
+
132
+ streaming_response = self.streaming_response
133
+ if (enable_streaming := event.get_extra("enable_streaming")) is not None:
134
+ streaming_response = bool(enable_streaming)
135
+
136
+ stream_to_general = (
137
+ self.unsupported_streaming_strategy == "turn_off"
138
+ and not event.platform_meta.support_streaming_message
139
+ )
140
+
141
+ await runner.reset(
142
+ request=req,
143
+ run_context=AgentContextWrapper(
144
+ context=astr_agent_ctx,
145
+ tool_call_timeout=60,
146
+ ),
147
+ agent_hooks=MAIN_AGENT_HOOKS,
148
+ provider_config=self.prov_cfg,
149
+ streaming=streaming_response,
150
+ )
151
+
152
+ if streaming_response and not stream_to_general:
153
+ # 流式响应
154
+ event.set_result(
155
+ MessageEventResult()
156
+ .set_result_content_type(ResultContentType.STREAMING_RESULT)
157
+ .set_async_stream(
158
+ run_third_party_agent(
159
+ runner,
160
+ stream_to_general=False,
161
+ ),
162
+ ),
163
+ )
164
+ yield
165
+ if runner.done():
166
+ final_resp = runner.get_final_llm_resp()
167
+ if final_resp and final_resp.result_chain:
168
+ event.set_result(
169
+ MessageEventResult(
170
+ chain=final_resp.result_chain.chain or [],
171
+ result_content_type=ResultContentType.STREAMING_FINISH,
172
+ ),
173
+ )
174
+ else:
175
+ # 非流式响应或转换为普通响应
176
+ async for _ in run_third_party_agent(
177
+ runner,
178
+ stream_to_general=stream_to_general,
179
+ ):
180
+ yield
181
+
182
+ final_resp = runner.get_final_llm_resp()
183
+
184
+ if not final_resp or not final_resp.result_chain:
185
+ logger.warning("Agent Runner 未返回最终结果。")
186
+ return
187
+
188
+ event.set_result(
189
+ MessageEventResult(
190
+ chain=final_resp.result_chain.chain or [],
191
+ result_content_type=ResultContentType.LLM_RESULT,
192
+ ),
193
+ )
194
+ yield
195
+
196
+ asyncio.create_task(
197
+ Metric.upload(
198
+ llm_tick=1,
199
+ model_name=self.runner_type,
200
+ provider_type=self.runner_type,
201
+ ),
202
+ )