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
@@ -1,650 +0,0 @@
1
- import base64
2
- import hashlib
3
- import json
4
- import os
5
- from collections.abc import AsyncGenerator
6
-
7
- import astrbot.core.message.components as Comp
8
- from astrbot import logger
9
- from astrbot.api.provider import Provider
10
- from astrbot.core.message.message_event_result import MessageChain
11
- from astrbot.core.provider.entities import LLMResponse
12
-
13
- from ..register import register_provider_adapter
14
- from .coze_api_client import CozeAPIClient
15
-
16
-
17
- @register_provider_adapter("coze", "Coze (扣子) 智能体适配器")
18
- class ProviderCoze(Provider):
19
- def __init__(
20
- self,
21
- provider_config,
22
- provider_settings,
23
- ) -> None:
24
- super().__init__(
25
- provider_config,
26
- provider_settings,
27
- )
28
- self.api_key = provider_config.get("coze_api_key", "")
29
- if not self.api_key:
30
- raise Exception("Coze API Key 不能为空。")
31
- self.bot_id = provider_config.get("bot_id", "")
32
- if not self.bot_id:
33
- raise Exception("Coze Bot ID 不能为空。")
34
- self.api_base: str = provider_config.get("coze_api_base", "https://api.coze.cn")
35
-
36
- if not isinstance(self.api_base, str) or not self.api_base.startswith(
37
- ("http://", "https://"),
38
- ):
39
- raise Exception(
40
- "Coze API Base URL 格式不正确,必须以 http:// 或 https:// 开头。",
41
- )
42
-
43
- self.timeout = provider_config.get("timeout", 120)
44
- if isinstance(self.timeout, str):
45
- self.timeout = int(self.timeout)
46
- self.auto_save_history = provider_config.get("auto_save_history", True)
47
- self.conversation_ids: dict[str, str] = {}
48
- self.file_id_cache: dict[str, dict[str, str]] = {}
49
-
50
- # 创建 API 客户端
51
- self.api_client = CozeAPIClient(api_key=self.api_key, api_base=self.api_base)
52
-
53
- def _generate_cache_key(self, data: str, is_base64: bool = False) -> str:
54
- """生成统一的缓存键
55
-
56
- Args:
57
- data: 图片数据或路径
58
- is_base64: 是否是 base64 数据
59
-
60
- Returns:
61
- str: 缓存键
62
-
63
- """
64
- try:
65
- if is_base64 and data.startswith("data:image/"):
66
- try:
67
- header, encoded = data.split(",", 1)
68
- image_bytes = base64.b64decode(encoded)
69
- cache_key = hashlib.md5(image_bytes).hexdigest()
70
- return cache_key
71
- except Exception:
72
- cache_key = hashlib.md5(encoded.encode("utf-8")).hexdigest()
73
- return cache_key
74
- elif data.startswith(("http://", "https://")):
75
- # URL图片,使用URL作为缓存键
76
- cache_key = hashlib.md5(data.encode("utf-8")).hexdigest()
77
- return cache_key
78
- else:
79
- clean_path = (
80
- data.split("_")[0]
81
- if "_" in data and len(data.split("_")) >= 3
82
- else data
83
- )
84
-
85
- if os.path.exists(clean_path):
86
- with open(clean_path, "rb") as f:
87
- file_content = f.read()
88
- cache_key = hashlib.md5(file_content).hexdigest()
89
- return cache_key
90
- cache_key = hashlib.md5(clean_path.encode("utf-8")).hexdigest()
91
- return cache_key
92
-
93
- except Exception as e:
94
- cache_key = hashlib.md5(data.encode("utf-8")).hexdigest()
95
- logger.debug(f"[Coze] 异常文件缓存键: {cache_key}, error={e}")
96
- return cache_key
97
-
98
- async def _upload_file(
99
- self,
100
- file_data: bytes,
101
- session_id: str | None = None,
102
- cache_key: str | None = None,
103
- ) -> str:
104
- """上传文件到 Coze 并返回 file_id"""
105
- # 使用 API 客户端上传文件
106
- file_id = await self.api_client.upload_file(file_data)
107
-
108
- # 缓存 file_id
109
- if session_id and cache_key:
110
- if session_id not in self.file_id_cache:
111
- self.file_id_cache[session_id] = {}
112
- self.file_id_cache[session_id][cache_key] = file_id
113
- logger.debug(f"[Coze] 图片上传成功并缓存,file_id: {file_id}")
114
-
115
- return file_id
116
-
117
- async def _download_and_upload_image(
118
- self,
119
- image_url: str,
120
- session_id: str | None = None,
121
- ) -> str:
122
- """下载图片并上传到 Coze,返回 file_id"""
123
- # 计算哈希实现缓存
124
- cache_key = self._generate_cache_key(image_url) if session_id else None
125
-
126
- if session_id and cache_key:
127
- if session_id not in self.file_id_cache:
128
- self.file_id_cache[session_id] = {}
129
-
130
- if cache_key in self.file_id_cache[session_id]:
131
- file_id = self.file_id_cache[session_id][cache_key]
132
- return file_id
133
-
134
- try:
135
- image_data = await self.api_client.download_image(image_url)
136
-
137
- file_id = await self._upload_file(image_data, session_id, cache_key)
138
-
139
- if session_id and cache_key:
140
- self.file_id_cache[session_id][cache_key] = file_id
141
-
142
- return file_id
143
-
144
- except Exception as e:
145
- logger.error(f"处理图片失败 {image_url}: {e!s}")
146
- raise Exception(f"处理图片失败: {e!s}")
147
-
148
- async def _process_context_images(
149
- self,
150
- content: str | list,
151
- session_id: str,
152
- ) -> str:
153
- """处理上下文中的图片内容,将 base64 图片上传并替换为 file_id"""
154
- try:
155
- if isinstance(content, str):
156
- return content
157
-
158
- processed_content = []
159
- if session_id not in self.file_id_cache:
160
- self.file_id_cache[session_id] = {}
161
-
162
- for item in content:
163
- if not isinstance(item, dict):
164
- processed_content.append(item)
165
- continue
166
- if item.get("type") == "text":
167
- processed_content.append(item)
168
- elif item.get("type") == "image_url":
169
- # 处理图片逻辑
170
- if "file_id" in item:
171
- # 已经有 file_id
172
- logger.debug(f"[Coze] 图片已有file_id: {item['file_id']}")
173
- processed_content.append(item)
174
- else:
175
- # 获取图片数据
176
- image_data = ""
177
- if "image_url" in item and isinstance(item["image_url"], dict):
178
- image_data = item["image_url"].get("url", "")
179
- elif "data" in item:
180
- image_data = item.get("data", "")
181
- elif "url" in item:
182
- image_data = item.get("url", "")
183
-
184
- if not image_data:
185
- continue
186
- # 计算哈希用于缓存
187
- cache_key = self._generate_cache_key(
188
- image_data,
189
- is_base64=image_data.startswith("data:image/"),
190
- )
191
-
192
- # 检查缓存
193
- if cache_key in self.file_id_cache[session_id]:
194
- file_id = self.file_id_cache[session_id][cache_key]
195
- processed_content.append(
196
- {"type": "image", "file_id": file_id},
197
- )
198
- else:
199
- # 上传图片并缓存
200
- if image_data.startswith("data:image/"):
201
- # base64 处理
202
- _, encoded = image_data.split(",", 1)
203
- image_bytes = base64.b64decode(encoded)
204
- file_id = await self._upload_file(
205
- image_bytes,
206
- session_id,
207
- cache_key,
208
- )
209
- elif image_data.startswith(("http://", "https://")):
210
- # URL 图片
211
- file_id = await self._download_and_upload_image(
212
- image_data,
213
- session_id,
214
- )
215
- # 为URL图片也添加缓存
216
- self.file_id_cache[session_id][cache_key] = file_id
217
- elif os.path.exists(image_data):
218
- # 本地文件
219
- with open(image_data, "rb") as f:
220
- image_bytes = f.read()
221
- file_id = await self._upload_file(
222
- image_bytes,
223
- session_id,
224
- cache_key,
225
- )
226
- else:
227
- logger.warning(
228
- f"无法处理的图片格式: {image_data[:50]}...",
229
- )
230
- continue
231
-
232
- processed_content.append(
233
- {"type": "image", "file_id": file_id},
234
- )
235
-
236
- result = json.dumps(processed_content, ensure_ascii=False)
237
- return result
238
- except Exception as e:
239
- logger.error(f"处理上下文图片失败: {e!s}")
240
- if isinstance(content, str):
241
- return content
242
- return json.dumps(content, ensure_ascii=False)
243
-
244
- async def text_chat(
245
- self,
246
- prompt: str,
247
- session_id=None,
248
- image_urls=None,
249
- func_tool=None,
250
- contexts=None,
251
- system_prompt=None,
252
- tool_calls_result=None,
253
- model=None,
254
- **kwargs,
255
- ) -> LLMResponse:
256
- """文本对话, 内部使用流式接口实现非流式
257
-
258
- Args:
259
- prompt (str): 用户提示词
260
- session_id (str): 会话ID
261
- image_urls (List[str]): 图片URL列表
262
- func_tool (FuncCall): 函数调用工具(不支持)
263
- contexts (List): 上下文列表
264
- system_prompt (str): 系统提示语
265
- tool_calls_result (ToolCallsResult | List[ToolCallsResult]): 工具调用结果(不支持)
266
- model (str): 模型名称(不支持)
267
-
268
- Returns:
269
- LLMResponse: LLM响应对象
270
-
271
- """
272
- accumulated_content = ""
273
- final_response = None
274
-
275
- async for llm_response in self.text_chat_stream(
276
- prompt=prompt,
277
- session_id=session_id,
278
- image_urls=image_urls,
279
- func_tool=func_tool,
280
- contexts=contexts,
281
- system_prompt=system_prompt,
282
- tool_calls_result=tool_calls_result,
283
- model=model,
284
- **kwargs,
285
- ):
286
- if llm_response.is_chunk:
287
- if llm_response.completion_text:
288
- accumulated_content += llm_response.completion_text
289
- else:
290
- final_response = llm_response
291
-
292
- if final_response:
293
- return final_response
294
-
295
- if accumulated_content:
296
- chain = MessageChain(chain=[Comp.Plain(accumulated_content)])
297
- return LLMResponse(role="assistant", result_chain=chain)
298
- return LLMResponse(role="assistant", completion_text="")
299
-
300
- async def text_chat_stream(
301
- self,
302
- prompt: str,
303
- session_id=None,
304
- image_urls=None,
305
- func_tool=None,
306
- contexts=None,
307
- system_prompt=None,
308
- tool_calls_result=None,
309
- model=None,
310
- **kwargs,
311
- ) -> AsyncGenerator[LLMResponse, None]:
312
- """流式对话接口"""
313
- # 用户ID参数(参考文档, 可以自定义)
314
- user_id = session_id or kwargs.get("user", "default_user")
315
-
316
- # 获取或创建会话ID
317
- conversation_id = self.conversation_ids.get(user_id)
318
-
319
- # 构建消息
320
- additional_messages = []
321
-
322
- if system_prompt:
323
- if not self.auto_save_history or not conversation_id:
324
- additional_messages.append(
325
- {
326
- "role": "system",
327
- "content": system_prompt,
328
- "content_type": "text",
329
- },
330
- )
331
-
332
- contexts = self._ensure_message_to_dicts(contexts)
333
- if not self.auto_save_history and contexts:
334
- # 如果关闭了自动保存历史,传入上下文
335
- for ctx in contexts:
336
- if isinstance(ctx, dict) and "role" in ctx and "content" in ctx:
337
- content = ctx["content"]
338
- content_type = ctx.get("content_type", "text")
339
-
340
- # 处理可能包含图片的上下文
341
- if (
342
- content_type == "object_string"
343
- or (isinstance(content, str) and content.startswith("["))
344
- or (
345
- isinstance(content, list)
346
- and any(
347
- isinstance(item, dict)
348
- and item.get("type") == "image_url"
349
- for item in content
350
- )
351
- )
352
- ):
353
- processed_content = await self._process_context_images(
354
- content,
355
- user_id,
356
- )
357
- additional_messages.append(
358
- {
359
- "role": ctx["role"],
360
- "content": processed_content,
361
- "content_type": "object_string",
362
- },
363
- )
364
- else:
365
- # 纯文本
366
- additional_messages.append(
367
- {
368
- "role": ctx["role"],
369
- "content": (
370
- content
371
- if isinstance(content, str)
372
- else json.dumps(content, ensure_ascii=False)
373
- ),
374
- "content_type": "text",
375
- },
376
- )
377
- else:
378
- logger.info(f"[Coze] 跳过格式不正确的上下文: {ctx}")
379
-
380
- if prompt or image_urls:
381
- if image_urls:
382
- # 多模态
383
- object_string_content = []
384
- if prompt:
385
- object_string_content.append({"type": "text", "text": prompt})
386
-
387
- for url in image_urls:
388
- try:
389
- if url.startswith(("http://", "https://")):
390
- # 网络图片
391
- file_id = await self._download_and_upload_image(
392
- url,
393
- user_id,
394
- )
395
- else:
396
- # 本地文件或 base64
397
- if url.startswith("data:image/"):
398
- # base64
399
- _, encoded = url.split(",", 1)
400
- image_data = base64.b64decode(encoded)
401
- cache_key = self._generate_cache_key(
402
- url,
403
- is_base64=True,
404
- )
405
- file_id = await self._upload_file(
406
- image_data,
407
- user_id,
408
- cache_key,
409
- )
410
- # 本地文件
411
- elif os.path.exists(url):
412
- with open(url, "rb") as f:
413
- image_data = f.read()
414
- # 用文件路径和修改时间来缓存
415
- file_stat = os.stat(url)
416
- cache_key = self._generate_cache_key(
417
- f"{url}_{file_stat.st_mtime}_{file_stat.st_size}",
418
- is_base64=False,
419
- )
420
- file_id = await self._upload_file(
421
- image_data,
422
- user_id,
423
- cache_key,
424
- )
425
- else:
426
- logger.warning(f"图片文件不存在: {url}")
427
- continue
428
-
429
- object_string_content.append(
430
- {
431
- "type": "image",
432
- "file_id": file_id,
433
- },
434
- )
435
- except Exception as e:
436
- logger.error(f"处理图片失败 {url}: {e!s}")
437
- continue
438
-
439
- if object_string_content:
440
- content = json.dumps(object_string_content, ensure_ascii=False)
441
- additional_messages.append(
442
- {
443
- "role": "user",
444
- "content": content,
445
- "content_type": "object_string",
446
- },
447
- )
448
- # 纯文本
449
- elif prompt:
450
- additional_messages.append(
451
- {
452
- "role": "user",
453
- "content": prompt,
454
- "content_type": "text",
455
- },
456
- )
457
-
458
- try:
459
- accumulated_content = ""
460
- message_started = False
461
-
462
- async for chunk in self.api_client.chat_messages(
463
- bot_id=self.bot_id,
464
- user_id=user_id,
465
- additional_messages=additional_messages,
466
- conversation_id=conversation_id,
467
- auto_save_history=self.auto_save_history,
468
- stream=True,
469
- timeout=self.timeout,
470
- ):
471
- event_type = chunk.get("event")
472
- data = chunk.get("data", {})
473
-
474
- if event_type == "conversation.chat.created":
475
- if isinstance(data, dict) and "conversation_id" in data:
476
- self.conversation_ids[user_id] = data["conversation_id"]
477
-
478
- elif event_type == "conversation.message.delta":
479
- if isinstance(data, dict):
480
- content = data.get("content", "")
481
- if not content and "delta" in data:
482
- content = data["delta"].get("content", "")
483
- if not content and "text" in data:
484
- content = data.get("text", "")
485
-
486
- if content:
487
- message_started = True
488
- accumulated_content += content
489
- yield LLMResponse(
490
- role="assistant",
491
- completion_text=content,
492
- is_chunk=True,
493
- )
494
-
495
- elif event_type == "conversation.message.completed":
496
- if isinstance(data, dict):
497
- msg_type = data.get("type")
498
- if msg_type == "answer" and data.get("role") == "assistant":
499
- final_content = data.get("content", "")
500
- if not accumulated_content and final_content:
501
- chain = MessageChain(chain=[Comp.Plain(final_content)])
502
- yield LLMResponse(
503
- role="assistant",
504
- result_chain=chain,
505
- is_chunk=False,
506
- )
507
-
508
- elif event_type == "conversation.chat.completed":
509
- if accumulated_content:
510
- chain = MessageChain(chain=[Comp.Plain(accumulated_content)])
511
- yield LLMResponse(
512
- role="assistant",
513
- result_chain=chain,
514
- is_chunk=False,
515
- )
516
- break
517
-
518
- elif event_type == "done":
519
- break
520
-
521
- elif event_type == "error":
522
- error_msg = (
523
- data.get("message", "未知错误")
524
- if isinstance(data, dict)
525
- else str(data)
526
- )
527
- logger.error(f"Coze 流式响应错误: {error_msg}")
528
- yield LLMResponse(
529
- role="err",
530
- completion_text=f"Coze 错误: {error_msg}",
531
- is_chunk=False,
532
- )
533
- break
534
-
535
- if not message_started and not accumulated_content:
536
- yield LLMResponse(
537
- role="assistant",
538
- completion_text="LLM 未响应任何内容。",
539
- is_chunk=False,
540
- )
541
- elif message_started and accumulated_content:
542
- chain = MessageChain(chain=[Comp.Plain(accumulated_content)])
543
- yield LLMResponse(
544
- role="assistant",
545
- result_chain=chain,
546
- is_chunk=False,
547
- )
548
-
549
- except Exception as e:
550
- logger.error(f"Coze 流式请求失败: {e!s}")
551
- yield LLMResponse(
552
- role="err",
553
- completion_text=f"Coze 流式请求失败: {e!s}",
554
- is_chunk=False,
555
- )
556
-
557
- async def forget(self, session_id: str):
558
- """清空指定会话的上下文"""
559
- user_id = session_id
560
- conversation_id = self.conversation_ids.get(user_id)
561
-
562
- if user_id in self.file_id_cache:
563
- self.file_id_cache.pop(user_id, None)
564
-
565
- if not conversation_id:
566
- return True
567
-
568
- try:
569
- response = await self.api_client.clear_context(conversation_id)
570
-
571
- if "code" in response and response["code"] == 0:
572
- self.conversation_ids.pop(user_id, None)
573
- return True
574
- logger.warning(f"清空 Coze 会话上下文失败: {response}")
575
- return False
576
-
577
- except Exception as e:
578
- logger.error(f"清空 Coze 会话失败: {e!s}")
579
- return False
580
-
581
- async def get_current_key(self):
582
- """获取当前API Key"""
583
- return self.api_key
584
-
585
- async def set_key(self, key: str):
586
- """设置新的API Key"""
587
- raise NotImplementedError("Coze 适配器不支持设置 API Key。")
588
-
589
- async def get_models(self):
590
- """获取可用模型列表"""
591
- return [f"bot_{self.bot_id}"]
592
-
593
- def get_model(self):
594
- """获取当前模型"""
595
- return f"bot_{self.bot_id}"
596
-
597
- def set_model(self, model: str):
598
- """设置模型(在Coze中是Bot ID)"""
599
- if model.startswith("bot_"):
600
- self.bot_id = model[4:]
601
- else:
602
- self.bot_id = model
603
-
604
- async def get_human_readable_context(
605
- self,
606
- session_id: str,
607
- page: int = 1,
608
- page_size: int = 10,
609
- ):
610
- """获取人类可读的上下文历史"""
611
- user_id = session_id
612
- conversation_id = self.conversation_ids.get(user_id)
613
-
614
- if not conversation_id:
615
- return []
616
-
617
- try:
618
- data = await self.api_client.get_message_list(
619
- conversation_id=conversation_id,
620
- order="desc",
621
- limit=page_size,
622
- offset=(page - 1) * page_size,
623
- )
624
-
625
- if data.get("code") != 0:
626
- logger.warning(f"获取 Coze 消息历史失败: {data}")
627
- return []
628
-
629
- messages = data.get("data", {}).get("messages", [])
630
-
631
- readable_history = []
632
- for msg in messages:
633
- role = msg.get("role", "unknown")
634
- content = msg.get("content", "")
635
- msg_type = msg.get("type", "")
636
-
637
- if role == "user":
638
- readable_history.append(f"用户: {content}")
639
- elif role == "assistant" and msg_type == "answer":
640
- readable_history.append(f"助手: {content}")
641
-
642
- return readable_history
643
-
644
- except Exception as e:
645
- logger.error(f"获取 Coze 消息历史失败: {e!s}")
646
- return []
647
-
648
- async def terminate(self):
649
- """清理资源"""
650
- await self.api_client.close()