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.
- astrbot/core/agent/mcp_client.py +3 -3
- astrbot/core/agent/runners/base.py +7 -4
- astrbot/core/agent/runners/coze/coze_agent_runner.py +367 -0
- astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py +403 -0
- astrbot/core/agent/runners/dify/dify_agent_runner.py +336 -0
- astrbot/core/{utils → agent/runners/dify}/dify_api_client.py +51 -13
- astrbot/core/agent/runners/tool_loop_agent_runner.py +0 -6
- astrbot/core/config/default.py +141 -26
- astrbot/core/config/i18n_utils.py +110 -0
- astrbot/core/core_lifecycle.py +11 -13
- astrbot/core/db/po.py +1 -1
- astrbot/core/db/sqlite.py +2 -2
- astrbot/core/pipeline/process_stage/method/agent_request.py +48 -0
- astrbot/core/pipeline/process_stage/method/{llm_request.py → agent_sub_stages/internal.py} +13 -34
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +202 -0
- astrbot/core/pipeline/process_stage/method/star_request.py +1 -1
- astrbot/core/pipeline/process_stage/stage.py +8 -5
- astrbot/core/pipeline/result_decorate/stage.py +15 -5
- astrbot/core/provider/manager.py +43 -41
- astrbot/core/star/session_llm_manager.py +0 -107
- astrbot/core/star/session_plugin_manager.py +0 -81
- astrbot/core/umop_config_router.py +19 -0
- astrbot/core/utils/migra_helper.py +73 -0
- astrbot/core/utils/shared_preferences.py +1 -28
- astrbot/dashboard/routes/chat.py +13 -1
- astrbot/dashboard/routes/config.py +20 -16
- astrbot/dashboard/routes/knowledge_base.py +0 -156
- astrbot/dashboard/routes/session_management.py +311 -606
- {astrbot-4.6.1.dist-info → astrbot-4.7.1.dist-info}/METADATA +1 -1
- {astrbot-4.6.1.dist-info → astrbot-4.7.1.dist-info}/RECORD +34 -30
- {astrbot-4.6.1.dist-info → astrbot-4.7.1.dist-info}/WHEEL +1 -1
- astrbot/core/provider/sources/coze_source.py +0 -650
- astrbot/core/provider/sources/dashscope_source.py +0 -207
- astrbot/core/provider/sources/dify_source.py +0 -285
- /astrbot/core/{provider/sources → agent/runners/coze}/coze_api_client.py +0 -0
- {astrbot-4.6.1.dist-info → astrbot-4.7.1.dist-info}/entry_points.txt +0 -0
- {astrbot-4.6.1.dist-info → astrbot-4.7.1.dist-info}/licenses/LICENSE +0 -0
astrbot/core/agent/mcp_client.py
CHANGED
|
@@ -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
|
|
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
|