AstrBot 3.5.6__py3-none-any.whl → 4.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. astrbot/api/__init__.py +16 -4
  2. astrbot/api/all.py +2 -1
  3. astrbot/api/event/__init__.py +5 -6
  4. astrbot/api/event/filter/__init__.py +37 -34
  5. astrbot/api/platform/__init__.py +7 -8
  6. astrbot/api/provider/__init__.py +8 -7
  7. astrbot/api/star/__init__.py +3 -4
  8. astrbot/api/util/__init__.py +2 -2
  9. astrbot/cli/__init__.py +1 -0
  10. astrbot/cli/__main__.py +18 -197
  11. astrbot/cli/commands/__init__.py +6 -0
  12. astrbot/cli/commands/cmd_conf.py +209 -0
  13. astrbot/cli/commands/cmd_init.py +56 -0
  14. astrbot/cli/commands/cmd_plug.py +245 -0
  15. astrbot/cli/commands/cmd_run.py +62 -0
  16. astrbot/cli/utils/__init__.py +18 -0
  17. astrbot/cli/utils/basic.py +76 -0
  18. astrbot/cli/utils/plugin.py +246 -0
  19. astrbot/cli/utils/version_comparator.py +90 -0
  20. astrbot/core/__init__.py +17 -19
  21. astrbot/core/agent/agent.py +14 -0
  22. astrbot/core/agent/handoff.py +38 -0
  23. astrbot/core/agent/hooks.py +30 -0
  24. astrbot/core/agent/mcp_client.py +385 -0
  25. astrbot/core/agent/message.py +175 -0
  26. astrbot/core/agent/response.py +14 -0
  27. astrbot/core/agent/run_context.py +22 -0
  28. astrbot/core/agent/runners/__init__.py +3 -0
  29. astrbot/core/agent/runners/base.py +65 -0
  30. astrbot/core/agent/runners/coze/coze_agent_runner.py +367 -0
  31. astrbot/core/agent/runners/coze/coze_api_client.py +324 -0
  32. astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py +403 -0
  33. astrbot/core/agent/runners/dify/dify_agent_runner.py +336 -0
  34. astrbot/core/agent/runners/dify/dify_api_client.py +195 -0
  35. astrbot/core/agent/runners/tool_loop_agent_runner.py +400 -0
  36. astrbot/core/agent/tool.py +285 -0
  37. astrbot/core/agent/tool_executor.py +17 -0
  38. astrbot/core/astr_agent_context.py +19 -0
  39. astrbot/core/astr_agent_hooks.py +36 -0
  40. astrbot/core/astr_agent_run_util.py +80 -0
  41. astrbot/core/astr_agent_tool_exec.py +246 -0
  42. astrbot/core/astrbot_config_mgr.py +275 -0
  43. astrbot/core/config/__init__.py +2 -2
  44. astrbot/core/config/astrbot_config.py +60 -20
  45. astrbot/core/config/default.py +1972 -453
  46. astrbot/core/config/i18n_utils.py +110 -0
  47. astrbot/core/conversation_mgr.py +285 -75
  48. astrbot/core/core_lifecycle.py +167 -62
  49. astrbot/core/db/__init__.py +305 -102
  50. astrbot/core/db/migration/helper.py +69 -0
  51. astrbot/core/db/migration/migra_3_to_4.py +357 -0
  52. astrbot/core/db/migration/migra_45_to_46.py +44 -0
  53. astrbot/core/db/migration/migra_webchat_session.py +131 -0
  54. astrbot/core/db/migration/shared_preferences_v3.py +48 -0
  55. astrbot/core/db/migration/sqlite_v3.py +497 -0
  56. astrbot/core/db/po.py +259 -55
  57. astrbot/core/db/sqlite.py +773 -528
  58. astrbot/core/db/vec_db/base.py +73 -0
  59. astrbot/core/db/vec_db/faiss_impl/__init__.py +3 -0
  60. astrbot/core/db/vec_db/faiss_impl/document_storage.py +392 -0
  61. astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +93 -0
  62. astrbot/core/db/vec_db/faiss_impl/sqlite_init.sql +17 -0
  63. astrbot/core/db/vec_db/faiss_impl/vec_db.py +204 -0
  64. astrbot/core/event_bus.py +26 -22
  65. astrbot/core/exceptions.py +9 -0
  66. astrbot/core/file_token_service.py +98 -0
  67. astrbot/core/initial_loader.py +19 -10
  68. astrbot/core/knowledge_base/chunking/__init__.py +9 -0
  69. astrbot/core/knowledge_base/chunking/base.py +25 -0
  70. astrbot/core/knowledge_base/chunking/fixed_size.py +59 -0
  71. astrbot/core/knowledge_base/chunking/recursive.py +161 -0
  72. astrbot/core/knowledge_base/kb_db_sqlite.py +301 -0
  73. astrbot/core/knowledge_base/kb_helper.py +642 -0
  74. astrbot/core/knowledge_base/kb_mgr.py +330 -0
  75. astrbot/core/knowledge_base/models.py +120 -0
  76. astrbot/core/knowledge_base/parsers/__init__.py +13 -0
  77. astrbot/core/knowledge_base/parsers/base.py +51 -0
  78. astrbot/core/knowledge_base/parsers/markitdown_parser.py +26 -0
  79. astrbot/core/knowledge_base/parsers/pdf_parser.py +101 -0
  80. astrbot/core/knowledge_base/parsers/text_parser.py +42 -0
  81. astrbot/core/knowledge_base/parsers/url_parser.py +103 -0
  82. astrbot/core/knowledge_base/parsers/util.py +13 -0
  83. astrbot/core/knowledge_base/prompts.py +65 -0
  84. astrbot/core/knowledge_base/retrieval/__init__.py +14 -0
  85. astrbot/core/knowledge_base/retrieval/hit_stopwords.txt +767 -0
  86. astrbot/core/knowledge_base/retrieval/manager.py +276 -0
  87. astrbot/core/knowledge_base/retrieval/rank_fusion.py +142 -0
  88. astrbot/core/knowledge_base/retrieval/sparse_retriever.py +136 -0
  89. astrbot/core/log.py +21 -15
  90. astrbot/core/message/components.py +413 -287
  91. astrbot/core/message/message_event_result.py +35 -24
  92. astrbot/core/persona_mgr.py +192 -0
  93. astrbot/core/pipeline/__init__.py +14 -14
  94. astrbot/core/pipeline/content_safety_check/stage.py +13 -9
  95. astrbot/core/pipeline/content_safety_check/strategies/__init__.py +1 -2
  96. astrbot/core/pipeline/content_safety_check/strategies/baidu_aip.py +13 -14
  97. astrbot/core/pipeline/content_safety_check/strategies/keywords.py +2 -1
  98. astrbot/core/pipeline/content_safety_check/strategies/strategy.py +6 -6
  99. astrbot/core/pipeline/context.py +7 -1
  100. astrbot/core/pipeline/context_utils.py +107 -0
  101. astrbot/core/pipeline/preprocess_stage/stage.py +63 -36
  102. astrbot/core/pipeline/process_stage/method/agent_request.py +48 -0
  103. astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +464 -0
  104. astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +202 -0
  105. astrbot/core/pipeline/process_stage/method/star_request.py +26 -32
  106. astrbot/core/pipeline/process_stage/stage.py +21 -15
  107. astrbot/core/pipeline/process_stage/utils.py +125 -0
  108. astrbot/core/pipeline/rate_limit_check/stage.py +34 -36
  109. astrbot/core/pipeline/respond/stage.py +142 -101
  110. astrbot/core/pipeline/result_decorate/stage.py +124 -57
  111. astrbot/core/pipeline/scheduler.py +21 -16
  112. astrbot/core/pipeline/session_status_check/stage.py +37 -0
  113. astrbot/core/pipeline/stage.py +11 -76
  114. astrbot/core/pipeline/waking_check/stage.py +69 -33
  115. astrbot/core/pipeline/whitelist_check/stage.py +10 -7
  116. astrbot/core/platform/__init__.py +6 -6
  117. astrbot/core/platform/astr_message_event.py +107 -129
  118. astrbot/core/platform/astrbot_message.py +32 -12
  119. astrbot/core/platform/manager.py +62 -18
  120. astrbot/core/platform/message_session.py +30 -0
  121. astrbot/core/platform/platform.py +16 -24
  122. astrbot/core/platform/platform_metadata.py +9 -4
  123. astrbot/core/platform/register.py +12 -7
  124. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +136 -60
  125. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +126 -46
  126. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +63 -31
  127. astrbot/core/platform/sources/dingtalk/dingtalk_event.py +30 -26
  128. astrbot/core/platform/sources/discord/client.py +129 -0
  129. astrbot/core/platform/sources/discord/components.py +139 -0
  130. astrbot/core/platform/sources/discord/discord_platform_adapter.py +473 -0
  131. astrbot/core/platform/sources/discord/discord_platform_event.py +313 -0
  132. astrbot/core/platform/sources/lark/lark_adapter.py +27 -18
  133. astrbot/core/platform/sources/lark/lark_event.py +39 -13
  134. astrbot/core/platform/sources/misskey/misskey_adapter.py +770 -0
  135. astrbot/core/platform/sources/misskey/misskey_api.py +964 -0
  136. astrbot/core/platform/sources/misskey/misskey_event.py +163 -0
  137. astrbot/core/platform/sources/misskey/misskey_utils.py +550 -0
  138. astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +149 -33
  139. astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +41 -26
  140. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +36 -17
  141. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_event.py +3 -1
  142. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +14 -8
  143. astrbot/core/platform/sources/satori/satori_adapter.py +792 -0
  144. astrbot/core/platform/sources/satori/satori_event.py +432 -0
  145. astrbot/core/platform/sources/slack/client.py +164 -0
  146. astrbot/core/platform/sources/slack/slack_adapter.py +416 -0
  147. astrbot/core/platform/sources/slack/slack_event.py +253 -0
  148. astrbot/core/platform/sources/telegram/tg_adapter.py +100 -43
  149. astrbot/core/platform/sources/telegram/tg_event.py +136 -36
  150. astrbot/core/platform/sources/webchat/webchat_adapter.py +72 -22
  151. astrbot/core/platform/sources/webchat/webchat_event.py +46 -22
  152. astrbot/core/platform/sources/webchat/webchat_queue_mgr.py +35 -0
  153. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +926 -0
  154. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +178 -0
  155. astrbot/core/platform/sources/wechatpadpro/xml_data_parser.py +159 -0
  156. astrbot/core/platform/sources/wecom/wecom_adapter.py +169 -27
  157. astrbot/core/platform/sources/wecom/wecom_event.py +162 -77
  158. astrbot/core/platform/sources/wecom/wecom_kf.py +279 -0
  159. astrbot/core/platform/sources/wecom/wecom_kf_message.py +196 -0
  160. astrbot/core/platform/sources/wecom_ai_bot/WXBizJsonMsgCrypt.py +297 -0
  161. astrbot/core/platform/sources/wecom_ai_bot/__init__.py +15 -0
  162. astrbot/core/platform/sources/wecom_ai_bot/ierror.py +19 -0
  163. astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +472 -0
  164. astrbot/core/platform/sources/wecom_ai_bot/wecomai_api.py +417 -0
  165. astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +152 -0
  166. astrbot/core/platform/sources/wecom_ai_bot/wecomai_queue_mgr.py +153 -0
  167. astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +168 -0
  168. astrbot/core/platform/sources/wecom_ai_bot/wecomai_utils.py +209 -0
  169. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +306 -0
  170. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +186 -0
  171. astrbot/core/platform_message_history_mgr.py +49 -0
  172. astrbot/core/provider/__init__.py +2 -3
  173. astrbot/core/provider/entites.py +8 -8
  174. astrbot/core/provider/entities.py +154 -98
  175. astrbot/core/provider/func_tool_manager.py +446 -458
  176. astrbot/core/provider/manager.py +345 -207
  177. astrbot/core/provider/provider.py +188 -73
  178. astrbot/core/provider/register.py +9 -7
  179. astrbot/core/provider/sources/anthropic_source.py +295 -115
  180. astrbot/core/provider/sources/azure_tts_source.py +224 -0
  181. astrbot/core/provider/sources/bailian_rerank_source.py +236 -0
  182. astrbot/core/provider/sources/dashscope_tts.py +138 -14
  183. astrbot/core/provider/sources/edge_tts_source.py +24 -19
  184. astrbot/core/provider/sources/fishaudio_tts_api_source.py +58 -13
  185. astrbot/core/provider/sources/gemini_embedding_source.py +61 -0
  186. astrbot/core/provider/sources/gemini_source.py +310 -132
  187. astrbot/core/provider/sources/gemini_tts_source.py +81 -0
  188. astrbot/core/provider/sources/groq_source.py +15 -0
  189. astrbot/core/provider/sources/gsv_selfhosted_source.py +151 -0
  190. astrbot/core/provider/sources/gsvi_tts_source.py +14 -7
  191. astrbot/core/provider/sources/minimax_tts_api_source.py +159 -0
  192. astrbot/core/provider/sources/openai_embedding_source.py +40 -0
  193. astrbot/core/provider/sources/openai_source.py +241 -145
  194. astrbot/core/provider/sources/openai_tts_api_source.py +18 -7
  195. astrbot/core/provider/sources/sensevoice_selfhosted_source.py +13 -11
  196. astrbot/core/provider/sources/vllm_rerank_source.py +71 -0
  197. astrbot/core/provider/sources/volcengine_tts.py +115 -0
  198. astrbot/core/provider/sources/whisper_api_source.py +18 -13
  199. astrbot/core/provider/sources/whisper_selfhosted_source.py +19 -12
  200. astrbot/core/provider/sources/xinference_rerank_source.py +116 -0
  201. astrbot/core/provider/sources/xinference_stt_provider.py +197 -0
  202. astrbot/core/provider/sources/zhipu_source.py +6 -73
  203. astrbot/core/star/__init__.py +43 -11
  204. astrbot/core/star/config.py +17 -18
  205. astrbot/core/star/context.py +362 -138
  206. astrbot/core/star/filter/__init__.py +4 -3
  207. astrbot/core/star/filter/command.py +111 -35
  208. astrbot/core/star/filter/command_group.py +46 -34
  209. astrbot/core/star/filter/custom_filter.py +6 -5
  210. astrbot/core/star/filter/event_message_type.py +4 -2
  211. astrbot/core/star/filter/permission.py +4 -2
  212. astrbot/core/star/filter/platform_adapter_type.py +45 -12
  213. astrbot/core/star/filter/regex.py +4 -2
  214. astrbot/core/star/register/__init__.py +19 -15
  215. astrbot/core/star/register/star.py +41 -13
  216. astrbot/core/star/register/star_handler.py +236 -86
  217. astrbot/core/star/session_llm_manager.py +280 -0
  218. astrbot/core/star/session_plugin_manager.py +170 -0
  219. astrbot/core/star/star.py +36 -43
  220. astrbot/core/star/star_handler.py +47 -85
  221. astrbot/core/star/star_manager.py +442 -260
  222. astrbot/core/star/star_tools.py +167 -45
  223. astrbot/core/star/updator.py +17 -20
  224. astrbot/core/umop_config_router.py +106 -0
  225. astrbot/core/updator.py +38 -13
  226. astrbot/core/utils/astrbot_path.py +39 -0
  227. astrbot/core/utils/command_parser.py +1 -1
  228. astrbot/core/utils/io.py +119 -60
  229. astrbot/core/utils/log_pipe.py +1 -1
  230. astrbot/core/utils/metrics.py +11 -10
  231. astrbot/core/utils/migra_helper.py +73 -0
  232. astrbot/core/utils/path_util.py +63 -62
  233. astrbot/core/utils/pip_installer.py +37 -15
  234. astrbot/core/utils/session_lock.py +29 -0
  235. astrbot/core/utils/session_waiter.py +19 -20
  236. astrbot/core/utils/shared_preferences.py +174 -34
  237. astrbot/core/utils/t2i/__init__.py +4 -1
  238. astrbot/core/utils/t2i/local_strategy.py +386 -238
  239. astrbot/core/utils/t2i/network_strategy.py +109 -49
  240. astrbot/core/utils/t2i/renderer.py +29 -14
  241. astrbot/core/utils/t2i/template/astrbot_powershell.html +184 -0
  242. astrbot/core/utils/t2i/template_manager.py +111 -0
  243. astrbot/core/utils/tencent_record_helper.py +115 -1
  244. astrbot/core/utils/version_comparator.py +10 -13
  245. astrbot/core/zip_updator.py +112 -65
  246. astrbot/dashboard/routes/__init__.py +20 -13
  247. astrbot/dashboard/routes/auth.py +20 -9
  248. astrbot/dashboard/routes/chat.py +297 -141
  249. astrbot/dashboard/routes/config.py +652 -55
  250. astrbot/dashboard/routes/conversation.py +107 -37
  251. astrbot/dashboard/routes/file.py +26 -0
  252. astrbot/dashboard/routes/knowledge_base.py +1244 -0
  253. astrbot/dashboard/routes/log.py +27 -2
  254. astrbot/dashboard/routes/persona.py +202 -0
  255. astrbot/dashboard/routes/plugin.py +197 -139
  256. astrbot/dashboard/routes/route.py +27 -7
  257. astrbot/dashboard/routes/session_management.py +354 -0
  258. astrbot/dashboard/routes/stat.py +85 -18
  259. astrbot/dashboard/routes/static_file.py +5 -2
  260. astrbot/dashboard/routes/t2i.py +233 -0
  261. astrbot/dashboard/routes/tools.py +184 -120
  262. astrbot/dashboard/routes/update.py +59 -36
  263. astrbot/dashboard/server.py +96 -36
  264. astrbot/dashboard/utils.py +165 -0
  265. astrbot-4.7.0.dist-info/METADATA +294 -0
  266. astrbot-4.7.0.dist-info/RECORD +274 -0
  267. {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/WHEEL +1 -1
  268. astrbot/core/db/plugin/sqlite_impl.py +0 -112
  269. astrbot/core/db/sqlite_init.sql +0 -50
  270. astrbot/core/pipeline/platform_compatibility/stage.py +0 -56
  271. astrbot/core/pipeline/process_stage/method/llm_request.py +0 -606
  272. astrbot/core/platform/sources/gewechat/client.py +0 -806
  273. astrbot/core/platform/sources/gewechat/downloader.py +0 -55
  274. astrbot/core/platform/sources/gewechat/gewechat_event.py +0 -255
  275. astrbot/core/platform/sources/gewechat/gewechat_platform_adapter.py +0 -103
  276. astrbot/core/platform/sources/gewechat/xml_data_parser.py +0 -110
  277. astrbot/core/provider/sources/dashscope_source.py +0 -203
  278. astrbot/core/provider/sources/dify_source.py +0 -281
  279. astrbot/core/provider/sources/llmtuner_source.py +0 -132
  280. astrbot/core/rag/embedding/openai_source.py +0 -20
  281. astrbot/core/rag/knowledge_db_mgr.py +0 -94
  282. astrbot/core/rag/store/__init__.py +0 -9
  283. astrbot/core/rag/store/chroma_db.py +0 -42
  284. astrbot/core/utils/dify_api_client.py +0 -152
  285. astrbot-3.5.6.dist-info/METADATA +0 -249
  286. astrbot-3.5.6.dist-info/RECORD +0 -158
  287. {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/entry_points.txt +0 -0
  288. {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,606 +0,0 @@
1
- """
2
- 本地 Agent 模式的 LLM 调用 Stage
3
- """
4
-
5
- import traceback
6
- import asyncio
7
- import json
8
- from typing import Union, AsyncGenerator
9
- from ...context import PipelineContext
10
- from ..stage import Stage
11
- from astrbot.core.platform.astr_message_event import AstrMessageEvent
12
- from astrbot.core.message.message_event_result import (
13
- MessageEventResult,
14
- ResultContentType,
15
- MessageChain,
16
- )
17
- from astrbot.core.message.components import Image
18
- from astrbot.core import logger
19
- from astrbot.core.utils.metrics import Metric
20
- from astrbot.core.provider.entities import (
21
- ProviderRequest,
22
- LLMResponse,
23
- ToolCallMessageSegment,
24
- AssistantMessageSegment,
25
- ToolCallsResult,
26
- )
27
- from astrbot.core.star.star_handler import star_handlers_registry, EventType
28
- from astrbot.core.star.star import star_map
29
- from mcp.types import (
30
- TextContent,
31
- ImageContent,
32
- EmbeddedResource,
33
- TextResourceContents,
34
- BlobResourceContents,
35
- )
36
-
37
-
38
- class LLMRequestSubStage(Stage):
39
- async def initialize(self, ctx: PipelineContext) -> None:
40
- self.ctx = ctx
41
- self.bot_wake_prefixs = ctx.astrbot_config["wake_prefix"] # list
42
- self.provider_wake_prefix = ctx.astrbot_config["provider_settings"][
43
- "wake_prefix"
44
- ] # str
45
- self.max_context_length = ctx.astrbot_config["provider_settings"][
46
- "max_context_length"
47
- ] # int
48
- self.dequeue_context_length = min(
49
- max(1, ctx.astrbot_config["provider_settings"]["dequeue_context_length"]),
50
- self.max_context_length - 1,
51
- ) # int
52
- self.streaming_response = ctx.astrbot_config["provider_settings"][
53
- "streaming_response"
54
- ] # bool
55
-
56
- for bwp in self.bot_wake_prefixs:
57
- if self.provider_wake_prefix.startswith(bwp):
58
- logger.info(
59
- f"识别 LLM 聊天额外唤醒前缀 {self.provider_wake_prefix} 以机器人唤醒前缀 {bwp} 开头,已自动去除。"
60
- )
61
- self.provider_wake_prefix = self.provider_wake_prefix[len(bwp) :]
62
-
63
- self.conv_manager = ctx.plugin_manager.context.conversation_manager
64
-
65
- async def process(
66
- self, event: AstrMessageEvent, _nested: bool = False
67
- ) -> Union[None, AsyncGenerator[None, None]]:
68
- req: ProviderRequest = None
69
-
70
- provider = self.ctx.plugin_manager.context.get_using_provider()
71
- if provider is None:
72
- return
73
-
74
- if event.get_extra("provider_request"):
75
- req = event.get_extra("provider_request")
76
- assert isinstance(req, ProviderRequest), (
77
- "provider_request 必须是 ProviderRequest 类型。"
78
- )
79
-
80
- if req.conversation:
81
- all_contexts = json.loads(req.conversation.history)
82
- req.contexts = self._process_tool_message_pairs(
83
- all_contexts, remove_tags=True
84
- )
85
-
86
- else:
87
- req = ProviderRequest(prompt="", image_urls=[])
88
- if self.provider_wake_prefix:
89
- if not event.message_str.startswith(self.provider_wake_prefix):
90
- return
91
- req.prompt = event.message_str[len(self.provider_wake_prefix) :]
92
- req.func_tool = self.ctx.plugin_manager.context.get_llm_tool_manager()
93
- for comp in event.message_obj.message:
94
- if isinstance(comp, Image):
95
- image_path = await comp.convert_to_file_path()
96
- req.image_urls.append(image_path)
97
-
98
- # 获取对话上下文
99
- conversation_id = await self.conv_manager.get_curr_conversation_id(
100
- event.unified_msg_origin
101
- )
102
- if not conversation_id:
103
- conversation_id = await self.conv_manager.new_conversation(
104
- event.unified_msg_origin
105
- )
106
- conversation = await self.conv_manager.get_conversation(
107
- event.unified_msg_origin, conversation_id
108
- )
109
- if not conversation:
110
- conversation_id = await self.conv_manager.new_conversation(
111
- event.unified_msg_origin
112
- )
113
- conversation = await self.conv_manager.get_conversation(
114
- event.unified_msg_origin, conversation_id
115
- )
116
- req.conversation = conversation
117
- req.contexts = json.loads(conversation.history)
118
-
119
- event.set_extra("provider_request", req)
120
-
121
- if not req.prompt and not req.image_urls:
122
- return
123
-
124
- # 执行请求 LLM 前事件钩子。
125
- # 装饰 system_prompt 等功能
126
- # 获取当前平台ID
127
- platform_id = event.get_platform_id()
128
- handlers = star_handlers_registry.get_handlers_by_event_type(
129
- EventType.OnLLMRequestEvent, platform_id=platform_id
130
- )
131
- for handler in handlers:
132
- try:
133
- logger.debug(
134
- f"hook(on_llm_request) -> {star_map[handler.handler_module_path].name} - {handler.handler_name}"
135
- )
136
- await handler.handler(event, req)
137
- except BaseException:
138
- logger.error(traceback.format_exc())
139
-
140
- if event.is_stopped():
141
- logger.info(
142
- f"{star_map[handler.handler_module_path].name} - {handler.handler_name} 终止了事件传播。"
143
- )
144
- return
145
-
146
- if isinstance(req.contexts, str):
147
- req.contexts = json.loads(req.contexts)
148
-
149
- # max context length
150
- if (
151
- self.max_context_length != -1 # -1 为不限制
152
- and len(req.contexts) // 2 > self.max_context_length
153
- ):
154
- logger.debug("上下文长度超过限制,将截断。")
155
- req.contexts = req.contexts[
156
- -(self.max_context_length - self.dequeue_context_length + 1) * 2 :
157
- ]
158
- # 找到第一个role 为 user 的索引,确保上下文格式正确
159
- index = next(
160
- (
161
- i
162
- for i, item in enumerate(req.contexts)
163
- if item.get("role") == "user"
164
- ),
165
- None,
166
- )
167
- if index is not None and index > 0:
168
- req.contexts = req.contexts[index:]
169
-
170
- # session_id
171
- if not req.session_id:
172
- req.session_id = event.unified_msg_origin
173
-
174
- async def requesting(req: ProviderRequest):
175
- try:
176
- need_loop = True
177
- while need_loop:
178
- need_loop = False
179
- logger.debug(f"提供商请求 Payload: {req}")
180
-
181
- final_llm_response = None
182
-
183
- if self.streaming_response:
184
- stream = provider.text_chat_stream(**req.__dict__)
185
- async for llm_response in stream:
186
- if llm_response.is_chunk:
187
- if llm_response.result_chain:
188
- yield llm_response.result_chain # MessageChain
189
- else:
190
- yield MessageChain().message(
191
- llm_response.completion_text
192
- )
193
- else:
194
- final_llm_response = llm_response
195
- else:
196
- final_llm_response = await provider.text_chat(
197
- **req.__dict__
198
- ) # 请求 LLM
199
-
200
- if not final_llm_response:
201
- raise Exception("LLM response is None.")
202
-
203
- # 执行 LLM 响应后的事件钩子。
204
- handlers = star_handlers_registry.get_handlers_by_event_type(
205
- EventType.OnLLMResponseEvent
206
- )
207
- for handler in handlers:
208
- try:
209
- logger.debug(
210
- f"hook(on_llm_response) -> {star_map[handler.handler_module_path].name} - {handler.handler_name}"
211
- )
212
- await handler.handler(event, final_llm_response)
213
- except BaseException:
214
- logger.error(traceback.format_exc())
215
-
216
- if event.is_stopped():
217
- logger.info(
218
- f"{star_map[handler.handler_module_path].name} - {handler.handler_name} 终止了事件传播。"
219
- )
220
- return
221
-
222
- if self.streaming_response:
223
- # 流式输出的处理
224
- async for result in self._handle_llm_stream_response(
225
- event, req, final_llm_response
226
- ):
227
- if isinstance(result, ProviderRequest):
228
- # 有函数工具调用并且返回了结果,我们需要再次请求 LLM
229
- req = result
230
- need_loop = True
231
- else:
232
- yield
233
- else:
234
- # 非流式输出的处理
235
- async for result in self._handle_llm_response(
236
- event, req, final_llm_response
237
- ):
238
- if isinstance(result, ProviderRequest):
239
- # 有函数工具调用并且返回了结果,我们需要再次请求 LLM
240
- req = result
241
- need_loop = True
242
- else:
243
- yield
244
-
245
- asyncio.create_task(
246
- Metric.upload(
247
- llm_tick=1,
248
- model_name=provider.get_model(),
249
- provider_type=provider.meta().type,
250
- )
251
- )
252
-
253
- # 保存到历史记录
254
- await self._save_to_history(event, req, final_llm_response)
255
-
256
- except BaseException as e:
257
- logger.error(traceback.format_exc())
258
- event.set_result(
259
- MessageEventResult().message(
260
- f"AstrBot 请求失败。\n错误类型: {type(e).__name__}\n错误信息: {str(e)}"
261
- )
262
- )
263
-
264
- if not self.streaming_response:
265
- event.set_extra("tool_call_result", None)
266
- async for _ in requesting(req):
267
- yield
268
- else:
269
- event.set_result(
270
- MessageEventResult()
271
- .set_result_content_type(ResultContentType.STREAMING_RESULT)
272
- .set_async_stream(requesting(req))
273
- )
274
- # 这里使用yield来暂停当前阶段,等待流式输出完成后继续处理
275
- yield
276
-
277
- if event.get_extra("tool_call_result"):
278
- event.set_result(event.get_extra("tool_call_result"))
279
- event.set_extra("tool_call_result", None)
280
- yield
281
-
282
- # 暂时直接发出去
283
- if img_b64 := event.get_extra("tool_call_img_respond"):
284
- await event.send(MessageChain(chain=[Image.fromBase64(img_b64)]))
285
- event.set_extra("tool_call_img_respond", None)
286
- yield
287
-
288
- async def _handle_llm_response(
289
- self,
290
- event: AstrMessageEvent,
291
- req: ProviderRequest,
292
- llm_response: LLMResponse,
293
- ) -> AsyncGenerator[Union[None, ProviderRequest], None]:
294
- """处理非流式 LLM 响应。
295
-
296
- Returns:
297
- AsyncGenerator[Union[None, ProviderRequest], None]: 如果返回 ProviderRequest,表示需要再次调用 LLM
298
-
299
- Yields:
300
- Iterator[Union[None, ProviderRequest]]: 将 event 交付给下一个 stage 或者返回 ProviderRequest 表示需要再次调用 LLM
301
- """
302
- if llm_response.role == "assistant":
303
- # text completion
304
- if llm_response.result_chain:
305
- event.set_result(
306
- MessageEventResult(
307
- chain=llm_response.result_chain.chain
308
- ).set_result_content_type(ResultContentType.LLM_RESULT)
309
- )
310
- else:
311
- event.set_result(
312
- MessageEventResult()
313
- .message(llm_response.completion_text)
314
- .set_result_content_type(ResultContentType.LLM_RESULT)
315
- )
316
- elif llm_response.role == "err":
317
- event.set_result(
318
- MessageEventResult().message(
319
- f"AstrBot 请求失败。\n错误信息: {llm_response.completion_text}"
320
- )
321
- )
322
- elif llm_response.role == "tool":
323
- # 处理函数工具调用
324
- async for result in self._handle_function_tools(event, req, llm_response):
325
- yield result
326
-
327
- async def _handle_llm_stream_response(
328
- self,
329
- event: AstrMessageEvent,
330
- req: ProviderRequest,
331
- llm_response: LLMResponse,
332
- ) -> AsyncGenerator[Union[None, ProviderRequest], None]:
333
- """处理流式 LLM 响应。
334
-
335
- 专门用于处理流式输出完成后的响应,与非流式响应处理分离。
336
-
337
- Returns:
338
- AsyncGenerator[Union[None, ProviderRequest], None]: 如果返回 ProviderRequest,表示需要再次调用 LLM
339
-
340
- Yields:
341
- Iterator[Union[None, ProviderRequest]]: 将 event 交付给下一个 stage 或者返回 ProviderRequest 表示需要再次调用 LLM
342
- """
343
- if llm_response.role == "assistant":
344
- # text completion
345
- if llm_response.result_chain:
346
- event.set_result(
347
- MessageEventResult(
348
- chain=llm_response.result_chain.chain
349
- ).set_result_content_type(ResultContentType.STREAMING_FINISH)
350
- )
351
- else:
352
- event.set_result(
353
- MessageEventResult()
354
- .message(llm_response.completion_text)
355
- .set_result_content_type(ResultContentType.STREAMING_FINISH)
356
- )
357
- elif llm_response.role == "err":
358
- event.set_result(
359
- MessageEventResult().message(
360
- f"AstrBot 请求失败。\n错误信息: {llm_response.completion_text}"
361
- )
362
- )
363
- elif llm_response.role == "tool":
364
- # 处理函数工具调用
365
- async for result in self._handle_function_tools(event, req, llm_response):
366
- yield result
367
-
368
- async def _handle_function_tools(
369
- self,
370
- event: AstrMessageEvent,
371
- req: ProviderRequest,
372
- llm_response: LLMResponse,
373
- ) -> AsyncGenerator[Union[None, ProviderRequest], None]:
374
- """处理函数工具调用。
375
-
376
- Returns:
377
- AsyncGenerator[Union[None, ProviderRequest], None]: 如果返回 ProviderRequest,表示需要再次调用 LLM
378
- """
379
- # function calling
380
- tool_call_result: list[ToolCallMessageSegment] = []
381
- logger.info(
382
- f"触发 {len(llm_response.tools_call_name)} 个函数调用: {llm_response.tools_call_name}"
383
- )
384
- for func_tool_name, func_tool_args, func_tool_id in zip(
385
- llm_response.tools_call_name,
386
- llm_response.tools_call_args,
387
- llm_response.tools_call_ids,
388
- ):
389
- try:
390
- func_tool = req.func_tool.get_func(func_tool_name)
391
- if func_tool.origin == "mcp":
392
- logger.info(
393
- f"从 MCP 服务 {func_tool.mcp_server_name} 调用工具函数:{func_tool.name},参数:{func_tool_args}"
394
- )
395
- client = req.func_tool.mcp_client_dict[func_tool.mcp_server_name]
396
- res = await client.session.call_tool(func_tool.name, func_tool_args)
397
- if res:
398
- # TODO 仅对ImageContent | EmbeddedResource进行了简单的Fallback
399
- if isinstance(res.content[0], TextContent):
400
- tool_call_result.append(
401
- ToolCallMessageSegment(
402
- role="tool",
403
- tool_call_id=func_tool_id,
404
- content=res.content[0].text,
405
- )
406
- )
407
- elif isinstance(res.content[0], ImageContent):
408
- tool_call_result.append(
409
- ToolCallMessageSegment(
410
- role="tool",
411
- tool_call_id=func_tool_id,
412
- content="返回了图片(已直接发送给用户)",
413
- )
414
- )
415
- event.set_extra(
416
- "tool_call_img_respond",
417
- res.content[0].data,
418
- )
419
- elif isinstance(res.content[0], EmbeddedResource):
420
- resource = res.content[0].resource
421
- if isinstance(resource, TextResourceContents):
422
- tool_call_result.append(
423
- ToolCallMessageSegment(
424
- role="tool",
425
- tool_call_id=func_tool_id,
426
- content=resource.text,
427
- )
428
- )
429
- elif (
430
- isinstance(resource, BlobResourceContents)
431
- and resource.mimeType
432
- and resource.mimeType.startswith("image/")
433
- ):
434
- tool_call_result.append(
435
- ToolCallMessageSegment(
436
- role="tool",
437
- tool_call_id=func_tool_id,
438
- content="返回了图片(已直接发送给用户)",
439
- )
440
- )
441
- event.set_extra(
442
- "tool_call_img_respond",
443
- res.content[0].data,
444
- )
445
- else:
446
- tool_call_result.append(
447
- ToolCallMessageSegment(
448
- role="tool",
449
- tool_call_id=func_tool_id,
450
- content="返回的数据类型不受支持",
451
- )
452
- )
453
- else:
454
- # 获取处理器,过滤掉平台不兼容的处理器
455
- platform_id = event.get_platform_id()
456
- star_md = star_map.get(func_tool.handler_module_path)
457
- if (
458
- star_md
459
- and platform_id in star_md.supported_platforms
460
- and not star_md.supported_platforms[platform_id]
461
- ):
462
- logger.debug(
463
- f"处理器 {func_tool_name}({star_md.name}) 在当前平台不兼容或者被禁用,跳过执行"
464
- )
465
- # 直接跳过,不添加任何消息到tool_call_result
466
- continue
467
-
468
- logger.info(
469
- f"调用工具函数:{func_tool_name},参数:{func_tool_args}"
470
- )
471
- # 尝试调用工具函数
472
- wrapper = self._call_handler(
473
- self.ctx, event, func_tool.handler, **func_tool_args
474
- )
475
- async for resp in wrapper:
476
- if resp is not None: # 有 return 返回
477
- tool_call_result.append(
478
- ToolCallMessageSegment(
479
- role="tool",
480
- tool_call_id=func_tool_id,
481
- content=resp,
482
- )
483
- )
484
- else:
485
- res = event.get_result()
486
- if res and res.chain:
487
- event.set_extra("tool_call_result", res)
488
- yield # 有生成器返回
489
- event.clear_result() # 清除上一个 handler 的结果
490
- except BaseException as e:
491
- logger.warning(traceback.format_exc())
492
- tool_call_result.append(
493
- ToolCallMessageSegment(
494
- role="tool",
495
- tool_call_id=func_tool_id,
496
- content=f"error: {str(e)}",
497
- )
498
- )
499
- if tool_call_result:
500
- # 函数调用结果
501
- req.func_tool = None # 暂时不支持递归工具调用
502
- assistant_msg_seg = AssistantMessageSegment(
503
- role="assistant", tool_calls=llm_response.to_openai_tool_calls()
504
- )
505
- # 在多轮 Tool 调用的情况下,这里始终保持最新的 Tool 调用结果,减少上下文长度。
506
- req.tool_calls_result = ToolCallsResult(
507
- tool_calls_info=assistant_msg_seg,
508
- tool_calls_result=tool_call_result,
509
- )
510
- yield req # 再次执行 LLM 请求
511
- else:
512
- if llm_response.completion_text:
513
- event.set_result(
514
- MessageEventResult().message(llm_response.completion_text)
515
- )
516
-
517
- async def _save_to_history(
518
- self, event: AstrMessageEvent, req: ProviderRequest, llm_response: LLMResponse
519
- ):
520
- if not req or not req.conversation or not llm_response:
521
- return
522
-
523
- if llm_response.role == "assistant":
524
- # 文本回复
525
- contexts = req.contexts.copy()
526
- contexts.append(await req.assemble_context())
527
-
528
- # 记录并标记函数调用结果
529
- if req.tool_calls_result:
530
- tool_calls_messages = req.tool_calls_result.to_openai_messages()
531
-
532
- # 添加标记
533
- for message in tool_calls_messages:
534
- message["_tool_call_history"] = True
535
-
536
- processed_tool_messages = self._process_tool_message_pairs(
537
- tool_calls_messages, remove_tags=False
538
- )
539
-
540
- contexts.extend(processed_tool_messages)
541
-
542
- contexts.append(
543
- {"role": "assistant", "content": llm_response.completion_text}
544
- )
545
- contexts_to_save = list(
546
- filter(lambda item: "_no_save" not in item, contexts)
547
- )
548
- await self.conv_manager.update_conversation(
549
- event.unified_msg_origin, req.conversation.cid, history=contexts_to_save
550
- )
551
-
552
- def _process_tool_message_pairs(self, messages, remove_tags=True):
553
- """处理工具调用消息,确保assistant和tool消息成对出现
554
-
555
- Args:
556
- messages (list): 消息列表
557
- remove_tags (bool): 是否移除_tool_call_history标记
558
-
559
- Returns:
560
- list: 处理后的消息列表,保证了assistant和对应tool消息的成对出现
561
- """
562
- result = []
563
- i = 0
564
-
565
- while i < len(messages):
566
- current_msg = messages[i]
567
-
568
- # 普通消息直接添加
569
- if "_tool_call_history" not in current_msg:
570
- result.append(current_msg.copy() if remove_tags else current_msg)
571
- i += 1
572
- continue
573
-
574
- # 工具调用消息成对处理
575
- if current_msg.get("role") == "assistant" and "tool_calls" in current_msg:
576
- assistant_msg = current_msg.copy()
577
-
578
- if remove_tags and "_tool_call_history" in assistant_msg:
579
- del assistant_msg["_tool_call_history"]
580
-
581
- related_tools = []
582
- j = i + 1
583
- while (
584
- j < len(messages)
585
- and messages[j].get("role") == "tool"
586
- and "_tool_call_history" in messages[j]
587
- ):
588
- tool_msg = messages[j].copy()
589
-
590
- if remove_tags:
591
- del tool_msg["_tool_call_history"]
592
-
593
- related_tools.append(tool_msg)
594
- j += 1
595
-
596
- # 成对的时候添加到结果
597
- if related_tools:
598
- result.append(assistant_msg)
599
- result.extend(related_tools)
600
-
601
- i = j # 跳过已处理
602
- else:
603
- # 单独的tool消息
604
- i += 1
605
-
606
- return result