AstrBot 4.6.1__py3-none-any.whl → 4.7.1__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 (37) hide show
  1. astrbot/core/agent/mcp_client.py +3 -3
  2. astrbot/core/agent/runners/base.py +7 -4
  3. astrbot/core/agent/runners/coze/coze_agent_runner.py +367 -0
  4. astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py +403 -0
  5. astrbot/core/agent/runners/dify/dify_agent_runner.py +336 -0
  6. astrbot/core/{utils → agent/runners/dify}/dify_api_client.py +51 -13
  7. astrbot/core/agent/runners/tool_loop_agent_runner.py +0 -6
  8. astrbot/core/config/default.py +141 -26
  9. astrbot/core/config/i18n_utils.py +110 -0
  10. astrbot/core/core_lifecycle.py +11 -13
  11. astrbot/core/db/po.py +1 -1
  12. astrbot/core/db/sqlite.py +2 -2
  13. astrbot/core/pipeline/process_stage/method/agent_request.py +48 -0
  14. astrbot/core/pipeline/process_stage/method/{llm_request.py → agent_sub_stages/internal.py} +13 -34
  15. astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +202 -0
  16. astrbot/core/pipeline/process_stage/method/star_request.py +1 -1
  17. astrbot/core/pipeline/process_stage/stage.py +8 -5
  18. astrbot/core/pipeline/result_decorate/stage.py +15 -5
  19. astrbot/core/provider/manager.py +43 -41
  20. astrbot/core/star/session_llm_manager.py +0 -107
  21. astrbot/core/star/session_plugin_manager.py +0 -81
  22. astrbot/core/umop_config_router.py +19 -0
  23. astrbot/core/utils/migra_helper.py +73 -0
  24. astrbot/core/utils/shared_preferences.py +1 -28
  25. astrbot/dashboard/routes/chat.py +13 -1
  26. astrbot/dashboard/routes/config.py +20 -16
  27. astrbot/dashboard/routes/knowledge_base.py +0 -156
  28. astrbot/dashboard/routes/session_management.py +311 -606
  29. {astrbot-4.6.1.dist-info → astrbot-4.7.1.dist-info}/METADATA +1 -1
  30. {astrbot-4.6.1.dist-info → astrbot-4.7.1.dist-info}/RECORD +34 -30
  31. {astrbot-4.6.1.dist-info → astrbot-4.7.1.dist-info}/WHEEL +1 -1
  32. astrbot/core/provider/sources/coze_source.py +0 -650
  33. astrbot/core/provider/sources/dashscope_source.py +0 -207
  34. astrbot/core/provider/sources/dify_source.py +0 -285
  35. /astrbot/core/{provider/sources → agent/runners/coze}/coze_api_client.py +0 -0
  36. {astrbot-4.6.1.dist-info → astrbot-4.7.1.dist-info}/entry_points.txt +0 -0
  37. {astrbot-4.6.1.dist-info → astrbot-4.7.1.dist-info}/licenses/LICENSE +0 -0
@@ -345,9 +345,6 @@ class MCPClient:
345
345
 
346
346
  async def cleanup(self):
347
347
  """Clean up resources including old exit stacks from reconnections"""
348
- # Set running_event first to unblock any waiting tasks
349
- self.running_event.set()
350
-
351
348
  # Close current exit stack
352
349
  try:
353
350
  await self.exit_stack.aclose()
@@ -359,6 +356,9 @@ class MCPClient:
359
356
  # Just clear the list to release references
360
357
  self._old_exit_stacks.clear()
361
358
 
359
+ # Set running_event first to unblock any waiting tasks
360
+ self.running_event.set()
361
+
362
362
 
363
363
  class MCPTool(FunctionTool, Generic[TContext]):
364
364
  """A function tool that calls an MCP service."""
@@ -2,13 +2,12 @@ import abc
2
2
  import typing as T
3
3
  from enum import Enum, auto
4
4
 
5
- from astrbot.core.provider import Provider
5
+ from astrbot import logger
6
6
  from astrbot.core.provider.entities import LLMResponse
7
7
 
8
8
  from ..hooks import BaseAgentRunHooks
9
9
  from ..response import AgentResponse
10
10
  from ..run_context import ContextWrapper, TContext
11
- from ..tool_executor import BaseFunctionToolExecutor
12
11
 
13
12
 
14
13
  class AgentState(Enum):
@@ -24,9 +23,7 @@ class BaseAgentRunner(T.Generic[TContext]):
24
23
  @abc.abstractmethod
25
24
  async def reset(
26
25
  self,
27
- provider: Provider,
28
26
  run_context: ContextWrapper[TContext],
29
- tool_executor: BaseFunctionToolExecutor[TContext],
30
27
  agent_hooks: BaseAgentRunHooks[TContext],
31
28
  **kwargs: T.Any,
32
29
  ) -> None:
@@ -60,3 +57,9 @@ class BaseAgentRunner(T.Generic[TContext]):
60
57
  This method should be called after the agent is done.
61
58
  """
62
59
  ...
60
+
61
+ def _transition_state(self, new_state: AgentState) -> None:
62
+ """Transition the agent state."""
63
+ if self._state != new_state:
64
+ logger.debug(f"Agent state transition: {self._state} -> {new_state}")
65
+ self._state = new_state
@@ -0,0 +1,367 @@
1
+ import base64
2
+ import json
3
+ import sys
4
+ import typing as T
5
+
6
+ import astrbot.core.message.components as Comp
7
+ from astrbot import logger
8
+ from astrbot.core import sp
9
+ from astrbot.core.message.message_event_result import MessageChain
10
+ from astrbot.core.provider.entities import (
11
+ LLMResponse,
12
+ ProviderRequest,
13
+ )
14
+
15
+ from ...hooks import BaseAgentRunHooks
16
+ from ...response import AgentResponseData
17
+ from ...run_context import ContextWrapper, TContext
18
+ from ..base import AgentResponse, AgentState, BaseAgentRunner
19
+ from .coze_api_client import CozeAPIClient
20
+
21
+ if sys.version_info >= (3, 12):
22
+ from typing import override
23
+ else:
24
+ from typing_extensions import override
25
+
26
+
27
+ class CozeAgentRunner(BaseAgentRunner[TContext]):
28
+ """Coze Agent Runner"""
29
+
30
+ @override
31
+ async def reset(
32
+ self,
33
+ request: ProviderRequest,
34
+ run_context: ContextWrapper[TContext],
35
+ agent_hooks: BaseAgentRunHooks[TContext],
36
+ provider_config: dict,
37
+ **kwargs: T.Any,
38
+ ) -> None:
39
+ self.req = request
40
+ self.streaming = kwargs.get("streaming", False)
41
+ self.final_llm_resp = None
42
+ self._state = AgentState.IDLE
43
+ self.agent_hooks = agent_hooks
44
+ self.run_context = run_context
45
+
46
+ self.api_key = provider_config.get("coze_api_key", "")
47
+ if not self.api_key:
48
+ raise Exception("Coze API Key 不能为空。")
49
+ self.bot_id = provider_config.get("bot_id", "")
50
+ if not self.bot_id:
51
+ raise Exception("Coze Bot ID 不能为空。")
52
+ self.api_base: str = provider_config.get("coze_api_base", "https://api.coze.cn")
53
+
54
+ if not isinstance(self.api_base, str) or not self.api_base.startswith(
55
+ ("http://", "https://"),
56
+ ):
57
+ raise Exception(
58
+ "Coze API Base URL 格式不正确,必须以 http:// 或 https:// 开头。",
59
+ )
60
+
61
+ self.timeout = provider_config.get("timeout", 120)
62
+ if isinstance(self.timeout, str):
63
+ self.timeout = int(self.timeout)
64
+ self.auto_save_history = provider_config.get("auto_save_history", True)
65
+
66
+ # 创建 API 客户端
67
+ self.api_client = CozeAPIClient(api_key=self.api_key, api_base=self.api_base)
68
+
69
+ # 会话相关缓存
70
+ self.file_id_cache: dict[str, dict[str, str]] = {}
71
+
72
+ @override
73
+ async def step(self):
74
+ """
75
+ 执行 Coze Agent 的一个步骤
76
+ """
77
+ if not self.req:
78
+ raise ValueError("Request is not set. Please call reset() first.")
79
+
80
+ if self._state == AgentState.IDLE:
81
+ try:
82
+ await self.agent_hooks.on_agent_begin(self.run_context)
83
+ except Exception as e:
84
+ logger.error(f"Error in on_agent_begin hook: {e}", exc_info=True)
85
+
86
+ # 开始处理,转换到运行状态
87
+ self._transition_state(AgentState.RUNNING)
88
+
89
+ try:
90
+ # 执行 Coze 请求并处理结果
91
+ async for response in self._execute_coze_request():
92
+ yield response
93
+ except Exception as e:
94
+ logger.error(f"Coze 请求失败:{str(e)}")
95
+ self._transition_state(AgentState.ERROR)
96
+ self.final_llm_resp = LLMResponse(
97
+ role="err", completion_text=f"Coze 请求失败:{str(e)}"
98
+ )
99
+ yield AgentResponse(
100
+ type="err",
101
+ data=AgentResponseData(
102
+ chain=MessageChain().message(f"Coze 请求失败:{str(e)}")
103
+ ),
104
+ )
105
+ finally:
106
+ await self.api_client.close()
107
+
108
+ @override
109
+ async def step_until_done(
110
+ self, max_step: int = 30
111
+ ) -> T.AsyncGenerator[AgentResponse, None]:
112
+ while not self.done():
113
+ async for resp in self.step():
114
+ yield resp
115
+
116
+ async def _execute_coze_request(self):
117
+ """执行 Coze 请求的核心逻辑"""
118
+ prompt = self.req.prompt or ""
119
+ session_id = self.req.session_id or "unknown"
120
+ image_urls = self.req.image_urls or []
121
+ contexts = self.req.contexts or []
122
+ system_prompt = self.req.system_prompt
123
+
124
+ # 用户ID参数
125
+ user_id = session_id
126
+
127
+ # 获取或创建会话ID
128
+ conversation_id = await sp.get_async(
129
+ scope="umo",
130
+ scope_id=user_id,
131
+ key="coze_conversation_id",
132
+ default="",
133
+ )
134
+
135
+ # 构建消息
136
+ additional_messages = []
137
+
138
+ if system_prompt:
139
+ if not self.auto_save_history or not conversation_id:
140
+ additional_messages.append(
141
+ {
142
+ "role": "system",
143
+ "content": system_prompt,
144
+ "content_type": "text",
145
+ },
146
+ )
147
+
148
+ # 处理历史上下文
149
+ if not self.auto_save_history and contexts:
150
+ for ctx in contexts:
151
+ if isinstance(ctx, dict) and "role" in ctx and "content" in ctx:
152
+ # 处理上下文中的图片
153
+ content = ctx["content"]
154
+ if isinstance(content, list):
155
+ # 多模态内容,需要处理图片
156
+ processed_content = []
157
+ for item in content:
158
+ if isinstance(item, dict):
159
+ if item.get("type") == "text":
160
+ processed_content.append(item)
161
+ elif item.get("type") == "image_url":
162
+ # 处理图片上传
163
+ try:
164
+ image_data = item.get("image_url", {})
165
+ url = image_data.get("url", "")
166
+ if url:
167
+ file_id = (
168
+ await self._download_and_upload_image(
169
+ url, session_id
170
+ )
171
+ )
172
+ processed_content.append(
173
+ {
174
+ "type": "file",
175
+ "file_id": file_id,
176
+ "file_url": url,
177
+ }
178
+ )
179
+ except Exception as e:
180
+ logger.warning(f"处理上下文图片失败: {e}")
181
+ continue
182
+
183
+ if processed_content:
184
+ additional_messages.append(
185
+ {
186
+ "role": ctx["role"],
187
+ "content": processed_content,
188
+ "content_type": "object_string",
189
+ }
190
+ )
191
+ else:
192
+ # 纯文本内容
193
+ additional_messages.append(
194
+ {
195
+ "role": ctx["role"],
196
+ "content": content,
197
+ "content_type": "text",
198
+ }
199
+ )
200
+
201
+ # 构建当前消息
202
+ if prompt or image_urls:
203
+ if image_urls:
204
+ # 多模态
205
+ object_string_content = []
206
+ if prompt:
207
+ object_string_content.append({"type": "text", "text": prompt})
208
+
209
+ for url in image_urls:
210
+ # the url is a base64 string
211
+ try:
212
+ image_data = base64.b64decode(url)
213
+ file_id = await self.api_client.upload_file(image_data)
214
+ object_string_content.append(
215
+ {
216
+ "type": "image",
217
+ "file_id": file_id,
218
+ }
219
+ )
220
+ except Exception as e:
221
+ logger.warning(f"处理图片失败 {url}: {e}")
222
+ continue
223
+
224
+ if object_string_content:
225
+ content = json.dumps(object_string_content, ensure_ascii=False)
226
+ additional_messages.append(
227
+ {
228
+ "role": "user",
229
+ "content": content,
230
+ "content_type": "object_string",
231
+ }
232
+ )
233
+ elif prompt:
234
+ # 纯文本
235
+ additional_messages.append(
236
+ {
237
+ "role": "user",
238
+ "content": prompt,
239
+ "content_type": "text",
240
+ },
241
+ )
242
+
243
+ # 执行 Coze API 请求
244
+ accumulated_content = ""
245
+ message_started = False
246
+
247
+ async for chunk in self.api_client.chat_messages(
248
+ bot_id=self.bot_id,
249
+ user_id=user_id,
250
+ additional_messages=additional_messages,
251
+ conversation_id=conversation_id,
252
+ auto_save_history=self.auto_save_history,
253
+ stream=True,
254
+ timeout=self.timeout,
255
+ ):
256
+ event_type = chunk.get("event")
257
+ data = chunk.get("data", {})
258
+
259
+ if event_type == "conversation.chat.created":
260
+ if isinstance(data, dict) and "conversation_id" in data:
261
+ await sp.put_async(
262
+ scope="umo",
263
+ scope_id=user_id,
264
+ key="coze_conversation_id",
265
+ value=data["conversation_id"],
266
+ )
267
+
268
+ if event_type == "conversation.message.delta":
269
+ # 增量消息
270
+ content = data.get("content", "")
271
+ if not content and "delta" in data:
272
+ content = data["delta"].get("content", "")
273
+ if not content and "text" in data:
274
+ content = data.get("text", "")
275
+
276
+ if content:
277
+ accumulated_content += content
278
+ message_started = True
279
+
280
+ # 如果是流式响应,发送增量数据
281
+ if self.streaming:
282
+ yield AgentResponse(
283
+ type="streaming_delta",
284
+ data=AgentResponseData(
285
+ chain=MessageChain().message(content)
286
+ ),
287
+ )
288
+
289
+ elif event_type == "conversation.message.completed":
290
+ # 消息完成
291
+ logger.debug("Coze message completed")
292
+ message_started = True
293
+
294
+ elif event_type == "conversation.chat.completed":
295
+ # 对话完成
296
+ logger.debug("Coze chat completed")
297
+ break
298
+
299
+ elif event_type == "error":
300
+ # 错误处理
301
+ error_msg = data.get("msg", "未知错误")
302
+ error_code = data.get("code", "UNKNOWN")
303
+ logger.error(f"Coze 出现错误: {error_code} - {error_msg}")
304
+ raise Exception(f"Coze 出现错误: {error_code} - {error_msg}")
305
+
306
+ if not message_started and not accumulated_content:
307
+ logger.warning("Coze 未返回任何内容")
308
+ accumulated_content = ""
309
+
310
+ # 创建最终响应
311
+ chain = MessageChain(chain=[Comp.Plain(accumulated_content)])
312
+ self.final_llm_resp = LLMResponse(role="assistant", result_chain=chain)
313
+ self._transition_state(AgentState.DONE)
314
+
315
+ try:
316
+ await self.agent_hooks.on_agent_done(self.run_context, self.final_llm_resp)
317
+ except Exception as e:
318
+ logger.error(f"Error in on_agent_done hook: {e}", exc_info=True)
319
+
320
+ # 返回最终结果
321
+ yield AgentResponse(
322
+ type="llm_result",
323
+ data=AgentResponseData(chain=chain),
324
+ )
325
+
326
+ async def _download_and_upload_image(
327
+ self,
328
+ image_url: str,
329
+ session_id: str | None = None,
330
+ ) -> str:
331
+ """下载图片并上传到 Coze,返回 file_id"""
332
+ import hashlib
333
+
334
+ # 计算哈希实现缓存
335
+ cache_key = hashlib.md5(image_url.encode("utf-8")).hexdigest()
336
+
337
+ if session_id:
338
+ if session_id not in self.file_id_cache:
339
+ self.file_id_cache[session_id] = {}
340
+
341
+ if cache_key in self.file_id_cache[session_id]:
342
+ file_id = self.file_id_cache[session_id][cache_key]
343
+ logger.debug(f"[Coze] 使用缓存的 file_id: {file_id}")
344
+ return file_id
345
+
346
+ try:
347
+ image_data = await self.api_client.download_image(image_url)
348
+ file_id = await self.api_client.upload_file(image_data)
349
+
350
+ if session_id:
351
+ self.file_id_cache[session_id][cache_key] = file_id
352
+ logger.debug(f"[Coze] 图片上传成功并缓存,file_id: {file_id}")
353
+
354
+ return file_id
355
+
356
+ except Exception as e:
357
+ logger.error(f"处理图片失败 {image_url}: {e!s}")
358
+ raise Exception(f"处理图片失败: {e!s}")
359
+
360
+ @override
361
+ def done(self) -> bool:
362
+ """检查 Agent 是否已完成工作"""
363
+ return self._state in (AgentState.DONE, AgentState.ERROR)
364
+
365
+ @override
366
+ def get_final_llm_resp(self) -> LLMResponse | None:
367
+ return self.final_llm_resp