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,60 +1,115 @@
1
- from typing import List
1
+ import base64
2
+ import json
3
+ from collections.abc import AsyncGenerator
2
4
  from mimetypes import guess_type
3
5
 
6
+ import anthropic
4
7
  from anthropic import AsyncAnthropic
5
8
  from anthropic.types import Message
6
9
 
7
- from astrbot.core.utils.io import download_image_by_url
8
- from astrbot.core.db import BaseDatabase
9
- from astrbot.api.provider import Provider, Personality
10
10
  from astrbot import logger
11
- from astrbot.core.provider.func_tool_manager import FuncCall
11
+ from astrbot.api.provider import Provider
12
+ from astrbot.core.provider.entities import LLMResponse
13
+ from astrbot.core.provider.func_tool_manager import ToolSet
14
+ from astrbot.core.utils.io import download_image_by_url
15
+
12
16
  from ..register import register_provider_adapter
13
- from astrbot.core.message.message_event_result import MessageChain
14
- from astrbot.core.provider.entities import LLMResponse, ToolCallsResult
15
- from .openai_source import ProviderOpenAIOfficial
16
17
 
17
18
 
18
19
  @register_provider_adapter(
19
- "anthropic_chat_completion", "Anthropic Claude API 提供商适配器"
20
+ "anthropic_chat_completion",
21
+ "Anthropic Claude API 提供商适配器",
20
22
  )
21
- class ProviderAnthropic(ProviderOpenAIOfficial):
23
+ class ProviderAnthropic(Provider):
22
24
  def __init__(
23
25
  self,
24
- provider_config: dict,
25
- provider_settings: dict,
26
- db_helper: BaseDatabase,
27
- persistant_history=True,
28
- default_persona: Personality = None,
26
+ provider_config,
27
+ provider_settings,
29
28
  ) -> None:
30
- # Skip OpenAI's __init__ and call Provider's __init__ directly
31
- Provider.__init__(
32
- self,
29
+ super().__init__(
33
30
  provider_config,
34
31
  provider_settings,
35
- persistant_history,
36
- db_helper,
37
- default_persona,
38
32
  )
39
33
 
40
- self.chosen_api_key = None
41
- self.api_keys: List = provider_config.get("key", [])
42
- self.chosen_api_key = self.api_keys[0] if len(self.api_keys) > 0 else None
34
+ self.chosen_api_key: str = ""
35
+ self.api_keys: list = super().get_keys()
36
+ self.chosen_api_key = self.api_keys[0] if len(self.api_keys) > 0 else ""
43
37
  self.base_url = provider_config.get("api_base", "https://api.anthropic.com")
44
38
  self.timeout = provider_config.get("timeout", 120)
45
39
  if isinstance(self.timeout, str):
46
40
  self.timeout = int(self.timeout)
47
41
 
48
42
  self.client = AsyncAnthropic(
49
- api_key=self.chosen_api_key, timeout=self.timeout, base_url=self.base_url
43
+ api_key=self.chosen_api_key,
44
+ timeout=self.timeout,
45
+ base_url=self.base_url,
50
46
  )
51
47
 
52
48
  self.set_model(provider_config["model_config"]["model"])
53
49
 
54
- async def _query(self, payloads: dict, tools: FuncCall) -> LLMResponse:
50
+ def _prepare_payload(self, messages: list[dict]):
51
+ """准备 Anthropic API 的请求 payload
52
+
53
+ Args:
54
+ messages: OpenAI 格式的消息列表,包含用户输入和系统提示等信息
55
+ Returns:
56
+ system_prompt: 系统提示内容
57
+ new_messages: 处理后的消息列表,去除系统提示
58
+
59
+ """
60
+ system_prompt = ""
61
+ new_messages = []
62
+ for message in messages:
63
+ if message["role"] == "system":
64
+ system_prompt = message["content"]
65
+ elif message["role"] == "assistant":
66
+ blocks = []
67
+ if isinstance(message["content"], str):
68
+ blocks.append({"type": "text", "text": message["content"]})
69
+ if "tool_calls" in message:
70
+ for tool_call in message["tool_calls"]:
71
+ blocks.append( # noqa: PERF401
72
+ {
73
+ "type": "tool_use",
74
+ "name": tool_call["function"]["name"],
75
+ "input": (
76
+ json.loads(tool_call["function"]["arguments"])
77
+ if isinstance(
78
+ tool_call["function"]["arguments"],
79
+ str,
80
+ )
81
+ else tool_call["function"]["arguments"]
82
+ ),
83
+ "id": tool_call["id"],
84
+ },
85
+ )
86
+ new_messages.append(
87
+ {
88
+ "role": "assistant",
89
+ "content": blocks,
90
+ },
91
+ )
92
+ elif message["role"] == "tool":
93
+ new_messages.append(
94
+ {
95
+ "role": "user",
96
+ "content": [
97
+ {
98
+ "type": "tool_result",
99
+ "tool_use_id": message["tool_call_id"],
100
+ "content": message["content"],
101
+ },
102
+ ],
103
+ },
104
+ )
105
+ else:
106
+ new_messages.append(message)
107
+
108
+ return system_prompt, new_messages
109
+
110
+ async def _query(self, payloads: dict, tools: ToolSet | None) -> LLMResponse:
55
111
  if tools:
56
- tool_list = tools.get_func_desc_anthropic_style()
57
- if tool_list:
112
+ if tool_list := tools.get_func_desc_anthropic_style():
58
113
  payloads["tools"] = tool_list
59
114
 
60
115
  completion = await self.client.messages.create(**payloads, stream=False)
@@ -64,68 +119,169 @@ class ProviderAnthropic(ProviderOpenAIOfficial):
64
119
 
65
120
  if len(completion.content) == 0:
66
121
  raise Exception("API 返回的 completion 为空。")
67
- # TODO: 如果进行函数调用,思维链被截断,用户可能需要思维链的内容
68
- # 选最后一条消息,如果要进行函数调用,anthropic会先返回文本消息的思维链,然后再返回函数调用请求
69
- content = completion.content[-1]
70
-
71
- llm_response = LLMResponse("assistant")
72
-
73
- if content.type == "text":
74
- # text completion
75
- completion_text = str(content.text).strip()
76
- # llm_response.completion_text = completion_text
77
- llm_response.result_chain = MessageChain().message(completion_text)
78
-
79
- # Anthropic每次只返回一个函数调用
80
- if completion.stop_reason == "tool_use":
81
- # tools call (function calling)
82
- args_ls = []
83
- func_name_ls = []
84
- tool_use_ids = []
85
- func_name_ls.append(content.name)
86
- args_ls.append(content.input)
87
- tool_use_ids.append(content.id)
88
- llm_response.role = "tool"
89
- llm_response.tools_call_args = args_ls
90
- llm_response.tools_call_name = func_name_ls
91
- llm_response.tools_call_ids = tool_use_ids
92
122
 
93
- if not llm_response.completion_text and not llm_response.tools_call_args:
94
- logger.error(f"API 返回的 completion 无法解析:{completion}。")
95
- raise Exception(f"API 返回的 completion 无法解析:{completion}。")
123
+ llm_response = LLMResponse(role="assistant")
96
124
 
97
- llm_response.raw_completion = completion
125
+ for content_block in completion.content:
126
+ if content_block.type == "text":
127
+ completion_text = str(content_block.text).strip()
128
+ llm_response.completion_text = completion_text
129
+
130
+ if content_block.type == "tool_use":
131
+ llm_response.tools_call_args.append(content_block.input)
132
+ llm_response.tools_call_name.append(content_block.name)
133
+ llm_response.tools_call_ids.append(content_block.id)
134
+ # TODO(Soulter): 处理 end_turn 情况
135
+ if not llm_response.completion_text and not llm_response.tools_call_args:
136
+ raise Exception(f"Anthropic API 返回的 completion 无法解析:{completion}。")
98
137
 
99
138
  return llm_response
100
139
 
140
+ async def _query_stream(
141
+ self,
142
+ payloads: dict,
143
+ tools: ToolSet | None,
144
+ ) -> AsyncGenerator[LLMResponse, None]:
145
+ if tools:
146
+ if tool_list := tools.get_func_desc_anthropic_style():
147
+ payloads["tools"] = tool_list
148
+
149
+ # 用于累积工具调用信息
150
+ tool_use_buffer = {}
151
+ # 用于累积最终结果
152
+ final_text = ""
153
+ final_tool_calls = []
154
+
155
+ async with self.client.messages.stream(**payloads) as stream:
156
+ assert isinstance(stream, anthropic.AsyncMessageStream)
157
+ async for event in stream:
158
+ if event.type == "content_block_start":
159
+ if event.content_block.type == "text":
160
+ # 文本块开始
161
+ yield LLMResponse(
162
+ role="assistant",
163
+ completion_text="",
164
+ is_chunk=True,
165
+ )
166
+ elif event.content_block.type == "tool_use":
167
+ # 工具使用块开始,初始化缓冲区
168
+ tool_use_buffer[event.index] = {
169
+ "id": event.content_block.id,
170
+ "name": event.content_block.name,
171
+ "input": {},
172
+ }
173
+
174
+ elif event.type == "content_block_delta":
175
+ if event.delta.type == "text_delta":
176
+ # 文本增量
177
+ final_text += event.delta.text
178
+ yield LLMResponse(
179
+ role="assistant",
180
+ completion_text=event.delta.text,
181
+ is_chunk=True,
182
+ )
183
+ elif event.delta.type == "input_json_delta":
184
+ # 工具调用参数增量
185
+ if event.index in tool_use_buffer:
186
+ # 累积 JSON 输入
187
+ if "input_json" not in tool_use_buffer[event.index]:
188
+ tool_use_buffer[event.index]["input_json"] = ""
189
+ tool_use_buffer[event.index]["input_json"] += (
190
+ event.delta.partial_json
191
+ )
192
+
193
+ elif event.type == "content_block_stop":
194
+ # 内容块结束
195
+ if event.index in tool_use_buffer:
196
+ # 解析完整的工具调用
197
+ tool_info = tool_use_buffer[event.index]
198
+ try:
199
+ if "input_json" in tool_info:
200
+ tool_info["input"] = json.loads(tool_info["input_json"])
201
+
202
+ # 添加到最终结果
203
+ final_tool_calls.append(
204
+ {
205
+ "id": tool_info["id"],
206
+ "name": tool_info["name"],
207
+ "input": tool_info["input"],
208
+ },
209
+ )
210
+
211
+ yield LLMResponse(
212
+ role="tool",
213
+ completion_text="",
214
+ tools_call_args=[tool_info["input"]],
215
+ tools_call_name=[tool_info["name"]],
216
+ tools_call_ids=[tool_info["id"]],
217
+ is_chunk=True,
218
+ )
219
+ except json.JSONDecodeError:
220
+ # JSON 解析失败,跳过这个工具调用
221
+ logger.warning(f"工具调用参数 JSON 解析失败: {tool_info}")
222
+
223
+ # 清理缓冲区
224
+ del tool_use_buffer[event.index]
225
+
226
+ # 返回最终的完整结果
227
+ final_response = LLMResponse(
228
+ role="assistant",
229
+ completion_text=final_text,
230
+ is_chunk=False,
231
+ )
232
+
233
+ if final_tool_calls:
234
+ final_response.tools_call_args = [
235
+ call["input"] for call in final_tool_calls
236
+ ]
237
+ final_response.tools_call_name = [call["name"] for call in final_tool_calls]
238
+ final_response.tools_call_ids = [call["id"] for call in final_tool_calls]
239
+
240
+ yield final_response
241
+
101
242
  async def text_chat(
102
243
  self,
103
- prompt: str,
104
- session_id: str = None,
105
- image_urls: List[str] = [],
106
- func_tool: FuncCall = None,
107
- contexts=[],
244
+ prompt=None,
245
+ session_id=None,
246
+ image_urls=None,
247
+ func_tool=None,
248
+ contexts=None,
108
249
  system_prompt=None,
109
- tool_calls_result: ToolCallsResult = None,
250
+ tool_calls_result=None,
251
+ model=None,
110
252
  **kwargs,
111
253
  ) -> LLMResponse:
112
- if not prompt:
113
- prompt = "<image>"
254
+ if contexts is None:
255
+ contexts = []
256
+ new_record = None
257
+ if prompt is not None:
258
+ new_record = await self.assemble_context(prompt, image_urls)
259
+ context_query = self._ensure_message_to_dicts(contexts)
260
+ if new_record:
261
+ context_query.append(new_record)
114
262
 
115
- new_record = await self.assemble_context(prompt, image_urls)
116
- context_query = [*contexts, new_record]
263
+ if system_prompt:
264
+ context_query.insert(0, {"role": "system", "content": system_prompt})
117
265
 
118
266
  for part in context_query:
119
267
  if "_no_save" in part:
120
268
  del part["_no_save"]
121
269
 
270
+ # tool calls result
122
271
  if tool_calls_result:
123
- # 暂时这样写。
124
- prompt += f"Here are the related results via using tools: {str(tool_calls_result.tool_calls_result)}"
272
+ if not isinstance(tool_calls_result, list):
273
+ context_query.extend(tool_calls_result.to_openai_messages())
274
+ else:
275
+ for tcr in tool_calls_result:
276
+ context_query.extend(tcr.to_openai_messages())
277
+
278
+ system_prompt, new_messages = self._prepare_payload(context_query)
125
279
 
126
280
  model_config = self.provider_config.get("model_config", {})
281
+ model_config["model"] = model or self.get_model()
282
+
283
+ payloads = {"messages": new_messages, **model_config}
127
284
 
128
- payloads = {"messages": context_query, **model_config}
129
285
  # Anthropic has a different way of handling system prompts
130
286
  if system_prompt:
131
287
  payloads["system"] = system_prompt
@@ -133,32 +289,9 @@ class ProviderAnthropic(ProviderOpenAIOfficial):
133
289
  llm_response = None
134
290
  try:
135
291
  llm_response = await self._query(payloads, func_tool)
136
-
137
292
  except Exception as e:
138
- if "maximum context length" in str(e):
139
- retry_cnt = 20
140
- while retry_cnt > 0:
141
- logger.warning(
142
- f"上下文长度超过限制。尝试弹出最早的记录然后重试。当前记录条数: {len(context_query)}"
143
- )
144
- try:
145
- await self.pop_record(context_query)
146
- response = await self.client.messages.create(
147
- messages=context_query, **model_config
148
- )
149
- llm_response = LLMResponse("assistant")
150
- llm_response.result_chain = MessageChain().message(response.content[0].text)
151
- llm_response.raw_completion = response
152
- return llm_response
153
- except Exception as e:
154
- if "maximum context length" in str(e):
155
- retry_cnt -= 1
156
- else:
157
- raise e
158
- return LLMResponse("err", "err: 请尝试 /reset 清除会话记录。")
159
- else:
160
- logger.error(f"发生了错误。Provider 配置如下: {model_config}")
161
- raise e
293
+ logger.error(f"发生了错误。Provider 配置如下: {model_config}")
294
+ raise e
162
295
 
163
296
  return llm_response
164
297
 
@@ -171,25 +304,47 @@ class ProviderAnthropic(ProviderOpenAIOfficial):
171
304
  contexts=...,
172
305
  system_prompt=None,
173
306
  tool_calls_result=None,
307
+ model=None,
174
308
  **kwargs,
175
309
  ):
176
- # raise NotImplementedError("This method is not implemented yet.")
177
- # 调用 text_chat 模拟流式
178
- llm_response = await self.text_chat(
179
- prompt=prompt,
180
- session_id=session_id,
181
- image_urls=image_urls,
182
- func_tool=func_tool,
183
- contexts=contexts,
184
- system_prompt=system_prompt,
185
- tool_calls_result=tool_calls_result,
186
- )
187
- llm_response.is_chunk = True
188
- yield llm_response
189
- llm_response.is_chunk = False
190
- yield llm_response
310
+ if contexts is None:
311
+ contexts = []
312
+ new_record = None
313
+ if prompt is not None:
314
+ new_record = await self.assemble_context(prompt, image_urls)
315
+ context_query = self._ensure_message_to_dicts(contexts)
316
+ if new_record:
317
+ context_query.append(new_record)
318
+ if system_prompt:
319
+ context_query.insert(0, {"role": "system", "content": system_prompt})
320
+
321
+ for part in context_query:
322
+ if "_no_save" in part:
323
+ del part["_no_save"]
191
324
 
192
- async def assemble_context(self, text: str, image_urls: List[str] = None):
325
+ # tool calls result
326
+ if tool_calls_result:
327
+ if not isinstance(tool_calls_result, list):
328
+ context_query.extend(tool_calls_result.to_openai_messages())
329
+ else:
330
+ for tcr in tool_calls_result:
331
+ context_query.extend(tcr.to_openai_messages())
332
+
333
+ system_prompt, new_messages = self._prepare_payload(context_query)
334
+
335
+ model_config = self.provider_config.get("model_config", {})
336
+ model_config["model"] = model or self.get_model()
337
+
338
+ payloads = {"messages": new_messages, **model_config}
339
+
340
+ # Anthropic has a different way of handling system prompts
341
+ if system_prompt:
342
+ payloads["system"] = system_prompt
343
+
344
+ async for llm_response in self._query_stream(payloads, func_tool):
345
+ yield llm_response
346
+
347
+ async def assemble_context(self, text: str, image_urls: list[str] | None = None):
193
348
  """组装上下文,支持文本和图片"""
194
349
  if not image_urls:
195
350
  return {"role": "user", "content": text}
@@ -222,11 +377,36 @@ class ProviderAnthropic(ProviderOpenAIOfficial):
222
377
  "source": {
223
378
  "type": "base64",
224
379
  "media_type": mime_type,
225
- "data": image_data.split("base64,")[1]
226
- if "base64," in image_data
227
- else image_data,
380
+ "data": (
381
+ image_data.split("base64,")[1]
382
+ if "base64," in image_data
383
+ else image_data
384
+ ),
228
385
  },
229
- }
386
+ },
230
387
  )
231
388
 
232
389
  return {"role": "user", "content": content}
390
+
391
+ async def encode_image_bs64(self, image_url: str) -> str:
392
+ """将图片转换为 base64"""
393
+ if image_url.startswith("base64://"):
394
+ return image_url.replace("base64://", "data:image/jpeg;base64,")
395
+ with open(image_url, "rb") as f:
396
+ image_bs64 = base64.b64encode(f.read()).decode("utf-8")
397
+ return "data:image/jpeg;base64," + image_bs64
398
+ return ""
399
+
400
+ def get_current_key(self) -> str:
401
+ return self.chosen_api_key
402
+
403
+ async def get_models(self) -> list[str]:
404
+ models_str = []
405
+ models = await self.client.models.list()
406
+ models = sorted(models.data, key=lambda x: x.id)
407
+ for model in models:
408
+ models_str.append(model.id)
409
+ return models_str
410
+
411
+ def set_key(self, key: str):
412
+ self.chosen_api_key = key