AstrBot 4.10.1__py3-none-any.whl → 4.10.3__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/builtin_stars/astrbot/long_term_memory.py +186 -0
- astrbot/builtin_stars/astrbot/main.py +128 -0
- astrbot/builtin_stars/astrbot/metadata.yaml +4 -0
- astrbot/builtin_stars/astrbot/process_llm_request.py +245 -0
- astrbot/builtin_stars/builtin_commands/commands/__init__.py +31 -0
- astrbot/builtin_stars/builtin_commands/commands/admin.py +77 -0
- astrbot/builtin_stars/builtin_commands/commands/alter_cmd.py +173 -0
- astrbot/builtin_stars/builtin_commands/commands/conversation.py +366 -0
- astrbot/builtin_stars/builtin_commands/commands/help.py +88 -0
- astrbot/builtin_stars/builtin_commands/commands/llm.py +20 -0
- astrbot/builtin_stars/builtin_commands/commands/persona.py +142 -0
- astrbot/builtin_stars/builtin_commands/commands/plugin.py +120 -0
- astrbot/builtin_stars/builtin_commands/commands/provider.py +329 -0
- astrbot/builtin_stars/builtin_commands/commands/setunset.py +36 -0
- astrbot/builtin_stars/builtin_commands/commands/sid.py +36 -0
- astrbot/builtin_stars/builtin_commands/commands/t2i.py +23 -0
- astrbot/builtin_stars/builtin_commands/commands/tool.py +31 -0
- astrbot/builtin_stars/builtin_commands/commands/tts.py +36 -0
- astrbot/builtin_stars/builtin_commands/commands/utils/rst_scene.py +26 -0
- astrbot/builtin_stars/builtin_commands/main.py +237 -0
- astrbot/builtin_stars/builtin_commands/metadata.yaml +4 -0
- astrbot/builtin_stars/python_interpreter/main.py +537 -0
- astrbot/builtin_stars/python_interpreter/metadata.yaml +4 -0
- astrbot/builtin_stars/python_interpreter/requirements.txt +1 -0
- astrbot/builtin_stars/python_interpreter/shared/api.py +22 -0
- astrbot/builtin_stars/reminder/main.py +266 -0
- astrbot/builtin_stars/reminder/metadata.yaml +4 -0
- astrbot/builtin_stars/session_controller/main.py +114 -0
- astrbot/builtin_stars/session_controller/metadata.yaml +5 -0
- astrbot/builtin_stars/web_searcher/engines/__init__.py +111 -0
- astrbot/builtin_stars/web_searcher/engines/bing.py +30 -0
- astrbot/builtin_stars/web_searcher/engines/sogo.py +52 -0
- astrbot/builtin_stars/web_searcher/main.py +436 -0
- astrbot/builtin_stars/web_searcher/metadata.yaml +4 -0
- astrbot/cli/__init__.py +1 -1
- astrbot/core/agent/message.py +9 -0
- astrbot/core/agent/runners/tool_loop_agent_runner.py +2 -1
- astrbot/core/backup/__init__.py +26 -0
- astrbot/core/backup/constants.py +77 -0
- astrbot/core/backup/exporter.py +476 -0
- astrbot/core/backup/importer.py +761 -0
- astrbot/core/config/default.py +1 -1
- astrbot/core/log.py +1 -1
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +1 -1
- astrbot/core/pipeline/waking_check/stage.py +2 -1
- astrbot/core/provider/entities.py +32 -9
- astrbot/core/provider/provider.py +3 -1
- astrbot/core/provider/sources/anthropic_source.py +80 -27
- astrbot/core/provider/sources/fishaudio_tts_api_source.py +14 -6
- astrbot/core/provider/sources/gemini_source.py +75 -26
- astrbot/core/provider/sources/openai_source.py +68 -25
- astrbot/core/star/command_management.py +45 -4
- astrbot/core/star/context.py +1 -1
- astrbot/core/star/star_manager.py +11 -13
- astrbot/core/utils/astrbot_path.py +34 -0
- astrbot/dashboard/routes/__init__.py +2 -0
- astrbot/dashboard/routes/backup.py +589 -0
- astrbot/dashboard/routes/command.py +2 -1
- astrbot/dashboard/routes/log.py +44 -10
- astrbot/dashboard/server.py +8 -1
- {astrbot-4.10.1.dist-info → astrbot-4.10.3.dist-info}/METADATA +2 -2
- {astrbot-4.10.1.dist-info → astrbot-4.10.3.dist-info}/RECORD +65 -26
- {astrbot-4.10.1.dist-info → astrbot-4.10.3.dist-info}/WHEEL +0 -0
- {astrbot-4.10.1.dist-info → astrbot-4.10.3.dist-info}/entry_points.txt +0 -0
- {astrbot-4.10.1.dist-info → astrbot-4.10.3.dist-info}/licenses/LICENSE +0 -0
astrbot/core/config/default.py
CHANGED
astrbot/core/log.py
CHANGED
|
@@ -58,7 +58,7 @@ def is_plugin_path(pathname):
|
|
|
58
58
|
return False
|
|
59
59
|
|
|
60
60
|
norm_path = os.path.normpath(pathname)
|
|
61
|
-
return ("data/plugins" in norm_path) or ("
|
|
61
|
+
return ("data/plugins" in norm_path) or ("astrbot/builtin_stars/" in norm_path)
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
def get_short_level_name(level_name):
|
|
@@ -390,7 +390,7 @@ class InternalAgentSubStage(Stage):
|
|
|
390
390
|
return
|
|
391
391
|
|
|
392
392
|
req.prompt = event.message_str[len(provider_wake_prefix) :]
|
|
393
|
-
# func_tool selection 现在已经转移到
|
|
393
|
+
# func_tool selection 现在已经转移到 astrbot/builtin_stars/astrbot 插件中进行选择。
|
|
394
394
|
# req.func_tool = self.ctx.plugin_manager.context.get_llm_tool_manager()
|
|
395
395
|
for comp in event.message_obj.message:
|
|
396
396
|
if isinstance(comp, Image):
|
|
@@ -136,7 +136,8 @@ class WakingCheckStage(Stage):
|
|
|
136
136
|
):
|
|
137
137
|
if (
|
|
138
138
|
self.disable_builtin_commands
|
|
139
|
-
and handler.handler_module_path
|
|
139
|
+
and handler.handler_module_path
|
|
140
|
+
== "astrbot.builtin_stars.builtin_commands.main"
|
|
140
141
|
):
|
|
141
142
|
logger.debug("skipping builtin command")
|
|
142
143
|
continue
|
|
@@ -14,6 +14,7 @@ import astrbot.core.message.components as Comp
|
|
|
14
14
|
from astrbot import logger
|
|
15
15
|
from astrbot.core.agent.message import (
|
|
16
16
|
AssistantMessageSegment,
|
|
17
|
+
ContentPart,
|
|
17
18
|
ToolCall,
|
|
18
19
|
ToolCallMessageSegment,
|
|
19
20
|
)
|
|
@@ -92,6 +93,8 @@ class ProviderRequest:
|
|
|
92
93
|
"""会话 ID"""
|
|
93
94
|
image_urls: list[str] = field(default_factory=list)
|
|
94
95
|
"""图片 URL 列表"""
|
|
96
|
+
extra_user_content_parts: list[ContentPart] = field(default_factory=list)
|
|
97
|
+
"""额外的用户消息内容部分列表,用于在用户消息后添加额外的内容块(如系统提醒、指令等)。支持 dict 或 ContentPart 对象"""
|
|
95
98
|
func_tool: ToolSet | None = None
|
|
96
99
|
"""可用的函数工具"""
|
|
97
100
|
contexts: list[dict] = field(default_factory=list)
|
|
@@ -166,13 +169,23 @@ class ProviderRequest:
|
|
|
166
169
|
|
|
167
170
|
async def assemble_context(self) -> dict:
|
|
168
171
|
"""将请求(prompt 和 image_urls)包装成 OpenAI 的消息格式。"""
|
|
172
|
+
# 构建内容块列表
|
|
173
|
+
content_blocks = []
|
|
174
|
+
|
|
175
|
+
# 1. 用户原始发言(OpenAI 建议:用户发言在前)
|
|
176
|
+
if self.prompt and self.prompt.strip():
|
|
177
|
+
content_blocks.append({"type": "text", "text": self.prompt})
|
|
178
|
+
elif self.image_urls:
|
|
179
|
+
# 如果没有文本但有图片,添加占位文本
|
|
180
|
+
content_blocks.append({"type": "text", "text": "[图片]"})
|
|
181
|
+
|
|
182
|
+
# 2. 额外的内容块(系统提醒、指令等)
|
|
183
|
+
if self.extra_user_content_parts:
|
|
184
|
+
for part in self.extra_user_content_parts:
|
|
185
|
+
content_blocks.append(part.model_dump())
|
|
186
|
+
|
|
187
|
+
# 3. 图片内容
|
|
169
188
|
if self.image_urls:
|
|
170
|
-
user_content = {
|
|
171
|
-
"role": "user",
|
|
172
|
-
"content": [
|
|
173
|
-
{"type": "text", "text": self.prompt if self.prompt else "[图片]"},
|
|
174
|
-
],
|
|
175
|
-
}
|
|
176
189
|
for image_url in self.image_urls:
|
|
177
190
|
if image_url.startswith("http"):
|
|
178
191
|
image_path = await download_image_by_url(image_url)
|
|
@@ -185,11 +198,21 @@ class ProviderRequest:
|
|
|
185
198
|
if not image_data:
|
|
186
199
|
logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。")
|
|
187
200
|
continue
|
|
188
|
-
|
|
201
|
+
content_blocks.append(
|
|
189
202
|
{"type": "image_url", "image_url": {"url": image_data}},
|
|
190
203
|
)
|
|
191
|
-
|
|
192
|
-
|
|
204
|
+
|
|
205
|
+
# 只有当只有一个来自 prompt 的文本块且没有额外内容块时,才降级为简单格式以保持向后兼容
|
|
206
|
+
if (
|
|
207
|
+
len(content_blocks) == 1
|
|
208
|
+
and content_blocks[0]["type"] == "text"
|
|
209
|
+
and not self.extra_user_content_parts
|
|
210
|
+
and not self.image_urls
|
|
211
|
+
):
|
|
212
|
+
return {"role": "user", "content": content_blocks[0]["text"]}
|
|
213
|
+
|
|
214
|
+
# 否则返回多模态格式
|
|
215
|
+
return {"role": "user", "content": content_blocks}
|
|
193
216
|
|
|
194
217
|
async def _encode_image_bs64(self, image_url: str) -> str:
|
|
195
218
|
"""将图片转换为 base64"""
|
|
@@ -4,7 +4,7 @@ import os
|
|
|
4
4
|
from collections.abc import AsyncGenerator
|
|
5
5
|
from typing import TypeAlias, Union
|
|
6
6
|
|
|
7
|
-
from astrbot.core.agent.message import Message
|
|
7
|
+
from astrbot.core.agent.message import ContentPart, Message
|
|
8
8
|
from astrbot.core.agent.tool import ToolSet
|
|
9
9
|
from astrbot.core.provider.entities import (
|
|
10
10
|
LLMResponse,
|
|
@@ -103,6 +103,7 @@ class Provider(AbstractProvider):
|
|
|
103
103
|
system_prompt: str | None = None,
|
|
104
104
|
tool_calls_result: ToolCallsResult | list[ToolCallsResult] | None = None,
|
|
105
105
|
model: str | None = None,
|
|
106
|
+
extra_user_content_parts: list[ContentPart] | None = None,
|
|
106
107
|
**kwargs,
|
|
107
108
|
) -> LLMResponse:
|
|
108
109
|
"""获得 LLM 的文本对话结果。会使用当前的模型进行对话。
|
|
@@ -114,6 +115,7 @@ class Provider(AbstractProvider):
|
|
|
114
115
|
tools: tool set
|
|
115
116
|
contexts: 上下文,和 prompt 二选一使用
|
|
116
117
|
tool_calls_result: 回传给 LLM 的工具调用结果。参考: https://platform.openai.com/docs/guides/function-calling
|
|
118
|
+
extra_user_content_parts: 额外的内容块列表,用于在用户消息后添加额外的文本块(如系统提醒、指令等)
|
|
117
119
|
kwargs: 其他参数
|
|
118
120
|
|
|
119
121
|
Notes:
|
|
@@ -11,6 +11,7 @@ from anthropic.types.usage import Usage
|
|
|
11
11
|
|
|
12
12
|
from astrbot import logger
|
|
13
13
|
from astrbot.api.provider import Provider
|
|
14
|
+
from astrbot.core.agent.message import ContentPart, ImageURLPart, TextPart
|
|
14
15
|
from astrbot.core.provider.entities import LLMResponse, TokenUsage
|
|
15
16
|
from astrbot.core.provider.func_tool_manager import ToolSet
|
|
16
17
|
from astrbot.core.utils.io import download_image_by_url
|
|
@@ -68,7 +69,7 @@ class ProviderAnthropic(Provider):
|
|
|
68
69
|
blocks = []
|
|
69
70
|
if isinstance(message["content"], str):
|
|
70
71
|
blocks.append({"type": "text", "text": message["content"]})
|
|
71
|
-
if "tool_calls" in message:
|
|
72
|
+
if "tool_calls" in message and isinstance(message["tool_calls"], list):
|
|
72
73
|
for tool_call in message["tool_calls"]:
|
|
73
74
|
blocks.append( # noqa: PERF401
|
|
74
75
|
{
|
|
@@ -132,6 +133,9 @@ class ProviderAnthropic(Provider):
|
|
|
132
133
|
|
|
133
134
|
extra_body = self.provider_config.get("custom_extra_body", {})
|
|
134
135
|
|
|
136
|
+
if "max_tokens" not in payloads:
|
|
137
|
+
payloads["max_tokens"] = 1024
|
|
138
|
+
|
|
135
139
|
completion = await self.client.messages.create(
|
|
136
140
|
**payloads, stream=False, extra_body=extra_body
|
|
137
141
|
)
|
|
@@ -181,6 +185,9 @@ class ProviderAnthropic(Provider):
|
|
|
181
185
|
usage = TokenUsage()
|
|
182
186
|
extra_body = self.provider_config.get("custom_extra_body", {})
|
|
183
187
|
|
|
188
|
+
if "max_tokens" not in payloads:
|
|
189
|
+
payloads["max_tokens"] = 1024
|
|
190
|
+
|
|
184
191
|
async with self.client.messages.stream(
|
|
185
192
|
**payloads, extra_body=extra_body
|
|
186
193
|
) as stream:
|
|
@@ -296,13 +303,16 @@ class ProviderAnthropic(Provider):
|
|
|
296
303
|
system_prompt=None,
|
|
297
304
|
tool_calls_result=None,
|
|
298
305
|
model=None,
|
|
306
|
+
extra_user_content_parts=None,
|
|
299
307
|
**kwargs,
|
|
300
308
|
) -> LLMResponse:
|
|
301
309
|
if contexts is None:
|
|
302
310
|
contexts = []
|
|
303
311
|
new_record = None
|
|
304
312
|
if prompt is not None:
|
|
305
|
-
new_record = await self.assemble_context(
|
|
313
|
+
new_record = await self.assemble_context(
|
|
314
|
+
prompt, image_urls, extra_user_content_parts
|
|
315
|
+
)
|
|
306
316
|
context_query = self._ensure_message_to_dicts(contexts)
|
|
307
317
|
if new_record:
|
|
308
318
|
context_query.append(new_record)
|
|
@@ -342,21 +352,24 @@ class ProviderAnthropic(Provider):
|
|
|
342
352
|
|
|
343
353
|
async def text_chat_stream(
|
|
344
354
|
self,
|
|
345
|
-
prompt,
|
|
355
|
+
prompt=None,
|
|
346
356
|
session_id=None,
|
|
347
|
-
image_urls
|
|
357
|
+
image_urls=None,
|
|
348
358
|
func_tool=None,
|
|
349
|
-
contexts
|
|
359
|
+
contexts=None,
|
|
350
360
|
system_prompt=None,
|
|
351
361
|
tool_calls_result=None,
|
|
352
362
|
model=None,
|
|
363
|
+
extra_user_content_parts=None,
|
|
353
364
|
**kwargs,
|
|
354
365
|
):
|
|
355
366
|
if contexts is None:
|
|
356
367
|
contexts = []
|
|
357
368
|
new_record = None
|
|
358
369
|
if prompt is not None:
|
|
359
|
-
new_record = await self.assemble_context(
|
|
370
|
+
new_record = await self.assemble_context(
|
|
371
|
+
prompt, image_urls, extra_user_content_parts
|
|
372
|
+
)
|
|
360
373
|
context_query = self._ensure_message_to_dicts(contexts)
|
|
361
374
|
if new_record:
|
|
362
375
|
context_query.append(new_record)
|
|
@@ -388,15 +401,15 @@ class ProviderAnthropic(Provider):
|
|
|
388
401
|
async for llm_response in self._query_stream(payloads, func_tool):
|
|
389
402
|
yield llm_response
|
|
390
403
|
|
|
391
|
-
async def assemble_context(
|
|
404
|
+
async def assemble_context(
|
|
405
|
+
self,
|
|
406
|
+
text: str,
|
|
407
|
+
image_urls: list[str] | None = None,
|
|
408
|
+
extra_user_content_parts: list[ContentPart] | None = None,
|
|
409
|
+
):
|
|
392
410
|
"""组装上下文,支持文本和图片"""
|
|
393
|
-
if not image_urls:
|
|
394
|
-
return {"role": "user", "content": text}
|
|
395
|
-
|
|
396
|
-
content = []
|
|
397
|
-
content.append({"type": "text", "text": text})
|
|
398
411
|
|
|
399
|
-
|
|
412
|
+
async def resolve_image_url(image_url: str) -> dict | None:
|
|
400
413
|
if image_url.startswith("http"):
|
|
401
414
|
image_path = await download_image_by_url(image_url)
|
|
402
415
|
image_data = await self.encode_image_bs64(image_path)
|
|
@@ -408,28 +421,68 @@ class ProviderAnthropic(Provider):
|
|
|
408
421
|
|
|
409
422
|
if not image_data:
|
|
410
423
|
logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。")
|
|
411
|
-
|
|
424
|
+
return None
|
|
412
425
|
|
|
413
426
|
# Get mime type for the image
|
|
414
427
|
mime_type, _ = guess_type(image_url)
|
|
415
428
|
if not mime_type:
|
|
416
429
|
mime_type = "image/jpeg" # Default to JPEG if can't determine
|
|
417
430
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
"
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
"
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
),
|
|
429
|
-
},
|
|
431
|
+
return {
|
|
432
|
+
"type": "image",
|
|
433
|
+
"source": {
|
|
434
|
+
"type": "base64",
|
|
435
|
+
"media_type": mime_type,
|
|
436
|
+
"data": (
|
|
437
|
+
image_data.split("base64,")[1]
|
|
438
|
+
if "base64," in image_data
|
|
439
|
+
else image_data
|
|
440
|
+
),
|
|
430
441
|
},
|
|
431
|
-
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
content = []
|
|
432
445
|
|
|
446
|
+
# 1. 用户原始发言(OpenAI 建议:用户发言在前)
|
|
447
|
+
if text:
|
|
448
|
+
content.append({"type": "text", "text": text})
|
|
449
|
+
elif image_urls:
|
|
450
|
+
# 如果没有文本但有图片,添加占位文本
|
|
451
|
+
content.append({"type": "text", "text": "[图片]"})
|
|
452
|
+
elif extra_user_content_parts:
|
|
453
|
+
# 如果只有额外内容块,也需要添加占位文本
|
|
454
|
+
content.append({"type": "text", "text": " "})
|
|
455
|
+
|
|
456
|
+
# 2. 额外的内容块(系统提醒、指令等)
|
|
457
|
+
if extra_user_content_parts:
|
|
458
|
+
for block in extra_user_content_parts:
|
|
459
|
+
if isinstance(block, TextPart):
|
|
460
|
+
content.append({"type": "text", "text": block.text})
|
|
461
|
+
elif isinstance(block, ImageURLPart):
|
|
462
|
+
image_dict = await resolve_image_url(block.image_url.url)
|
|
463
|
+
if image_dict:
|
|
464
|
+
content.append(image_dict)
|
|
465
|
+
else:
|
|
466
|
+
raise ValueError(f"不支持的额外内容块类型: {type(block)}")
|
|
467
|
+
|
|
468
|
+
# 3. 图片内容
|
|
469
|
+
if image_urls:
|
|
470
|
+
for image_url in image_urls:
|
|
471
|
+
image_dict = await resolve_image_url(image_url)
|
|
472
|
+
if image_dict:
|
|
473
|
+
content.append(image_dict)
|
|
474
|
+
|
|
475
|
+
# 如果只有主文本且没有额外内容块和图片,返回简单格式以保持向后兼容
|
|
476
|
+
if (
|
|
477
|
+
text
|
|
478
|
+
and not extra_user_content_parts
|
|
479
|
+
and not image_urls
|
|
480
|
+
and len(content) == 1
|
|
481
|
+
and content[0]["type"] == "text"
|
|
482
|
+
):
|
|
483
|
+
return {"role": "user", "content": content[0]["text"]}
|
|
484
|
+
|
|
485
|
+
# 否则返回多模态格式
|
|
433
486
|
return {"role": "user", "content": content}
|
|
434
487
|
|
|
435
488
|
async def encode_image_bs64(self, image_url: str) -> str:
|
|
@@ -56,10 +56,14 @@ class ProviderFishAudioTTSAPI(TTSProvider):
|
|
|
56
56
|
"api_base",
|
|
57
57
|
"https://api.fish-audio.cn/v1",
|
|
58
58
|
)
|
|
59
|
+
try:
|
|
60
|
+
self.timeout: int = int(provider_config.get("timeout", 20))
|
|
61
|
+
except ValueError:
|
|
62
|
+
self.timeout = 20
|
|
59
63
|
self.headers = {
|
|
60
64
|
"Authorization": f"Bearer {self.chosen_api_key}",
|
|
61
65
|
}
|
|
62
|
-
self.set_model(provider_config
|
|
66
|
+
self.set_model(provider_config.get("model", None))
|
|
63
67
|
|
|
64
68
|
async def _get_reference_id_by_character(self, character: str) -> str | None:
|
|
65
69
|
"""获取角色的reference_id
|
|
@@ -135,17 +139,21 @@ class ProviderFishAudioTTSAPI(TTSProvider):
|
|
|
135
139
|
path = os.path.join(temp_dir, f"fishaudio_tts_api_{uuid.uuid4()}.wav")
|
|
136
140
|
self.headers["content-type"] = "application/msgpack"
|
|
137
141
|
request = await self._generate_request(text)
|
|
138
|
-
async with AsyncClient(base_url=self.api_base).stream(
|
|
142
|
+
async with AsyncClient(base_url=self.api_base, timeout=self.timeout).stream(
|
|
139
143
|
"POST",
|
|
140
144
|
"/tts",
|
|
141
145
|
headers=self.headers,
|
|
142
146
|
content=ormsgpack.packb(request, option=ormsgpack.OPT_SERIALIZE_PYDANTIC),
|
|
143
147
|
) as response:
|
|
144
|
-
if response.
|
|
148
|
+
if response.status_code == 200 and response.headers.get(
|
|
149
|
+
"content-type", ""
|
|
150
|
+
).startswith("audio/"):
|
|
145
151
|
with open(path, "wb") as f:
|
|
146
152
|
async for chunk in response.aiter_bytes():
|
|
147
153
|
f.write(chunk)
|
|
148
154
|
return path
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
raise Exception(
|
|
155
|
+
error_bytes = await response.aread()
|
|
156
|
+
error_text = error_bytes.decode("utf-8", errors="replace")[:1024]
|
|
157
|
+
raise Exception(
|
|
158
|
+
f"Fish Audio API请求失败: 状态码 {response.status_code}, 响应内容: {error_text}"
|
|
159
|
+
)
|
|
@@ -13,6 +13,7 @@ from google.genai.errors import APIError
|
|
|
13
13
|
import astrbot.core.message.components as Comp
|
|
14
14
|
from astrbot import logger
|
|
15
15
|
from astrbot.api.provider import Provider
|
|
16
|
+
from astrbot.core.agent.message import ContentPart, ImageURLPart, TextPart
|
|
16
17
|
from astrbot.core.message.message_event_result import MessageChain
|
|
17
18
|
from astrbot.core.provider.entities import LLMResponse, TokenUsage
|
|
18
19
|
from astrbot.core.provider.func_tool_manager import ToolSet
|
|
@@ -680,13 +681,16 @@ class ProviderGoogleGenAI(Provider):
|
|
|
680
681
|
system_prompt=None,
|
|
681
682
|
tool_calls_result=None,
|
|
682
683
|
model=None,
|
|
684
|
+
extra_user_content_parts=None,
|
|
683
685
|
**kwargs,
|
|
684
686
|
) -> LLMResponse:
|
|
685
687
|
if contexts is None:
|
|
686
688
|
contexts = []
|
|
687
689
|
new_record = None
|
|
688
690
|
if prompt is not None:
|
|
689
|
-
new_record = await self.assemble_context(
|
|
691
|
+
new_record = await self.assemble_context(
|
|
692
|
+
prompt, image_urls, extra_user_content_parts
|
|
693
|
+
)
|
|
690
694
|
context_query = self._ensure_message_to_dicts(contexts)
|
|
691
695
|
if new_record:
|
|
692
696
|
context_query.append(new_record)
|
|
@@ -732,13 +736,16 @@ class ProviderGoogleGenAI(Provider):
|
|
|
732
736
|
system_prompt=None,
|
|
733
737
|
tool_calls_result=None,
|
|
734
738
|
model=None,
|
|
739
|
+
extra_user_content_parts=None,
|
|
735
740
|
**kwargs,
|
|
736
741
|
) -> AsyncGenerator[LLMResponse, None]:
|
|
737
742
|
if contexts is None:
|
|
738
743
|
contexts = []
|
|
739
744
|
new_record = None
|
|
740
745
|
if prompt is not None:
|
|
741
|
-
new_record = await self.assemble_context(
|
|
746
|
+
new_record = await self.assemble_context(
|
|
747
|
+
prompt, image_urls, extra_user_content_parts
|
|
748
|
+
)
|
|
742
749
|
context_query = self._ensure_message_to_dicts(contexts)
|
|
743
750
|
if new_record:
|
|
744
751
|
context_query.append(new_record)
|
|
@@ -797,33 +804,75 @@ class ProviderGoogleGenAI(Provider):
|
|
|
797
804
|
self.chosen_api_key = key
|
|
798
805
|
self._init_client()
|
|
799
806
|
|
|
800
|
-
async def assemble_context(
|
|
807
|
+
async def assemble_context(
|
|
808
|
+
self,
|
|
809
|
+
text: str,
|
|
810
|
+
image_urls: list[str] | None = None,
|
|
811
|
+
extra_user_content_parts: list[ContentPart] | None = None,
|
|
812
|
+
):
|
|
801
813
|
"""组装上下文。"""
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
814
|
+
|
|
815
|
+
async def resolve_image_part(image_url: str) -> dict | None:
|
|
816
|
+
if image_url.startswith("http"):
|
|
817
|
+
image_path = await download_image_by_url(image_url)
|
|
818
|
+
image_data = await self.encode_image_bs64(image_path)
|
|
819
|
+
elif image_url.startswith("file:///"):
|
|
820
|
+
image_path = image_url.replace("file:///", "")
|
|
821
|
+
image_data = await self.encode_image_bs64(image_path)
|
|
822
|
+
else:
|
|
823
|
+
image_data = await self.encode_image_bs64(image_url)
|
|
824
|
+
if not image_data:
|
|
825
|
+
logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。")
|
|
826
|
+
return None
|
|
827
|
+
return {
|
|
828
|
+
"type": "image_url",
|
|
829
|
+
"image_url": {"url": image_data},
|
|
806
830
|
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
831
|
+
|
|
832
|
+
# 构建内容块列表
|
|
833
|
+
content_blocks = []
|
|
834
|
+
|
|
835
|
+
# 1. 用户原始发言(OpenAI 建议:用户发言在前)
|
|
836
|
+
if text:
|
|
837
|
+
content_blocks.append({"type": "text", "text": text})
|
|
838
|
+
elif image_urls:
|
|
839
|
+
# 如果没有文本但有图片,添加占位文本
|
|
840
|
+
content_blocks.append({"type": "text", "text": "[图片]"})
|
|
841
|
+
elif extra_user_content_parts:
|
|
842
|
+
# 如果只有额外内容块,也需要添加占位文本
|
|
843
|
+
content_blocks.append({"type": "text", "text": " "})
|
|
844
|
+
|
|
845
|
+
# 2. 额外的内容块(系统提醒、指令等)
|
|
846
|
+
if extra_user_content_parts:
|
|
847
|
+
for part in extra_user_content_parts:
|
|
848
|
+
if isinstance(part, TextPart):
|
|
849
|
+
content_blocks.append({"type": "text", "text": part.text})
|
|
850
|
+
elif isinstance(part, ImageURLPart):
|
|
851
|
+
image_part = await resolve_image_part(part.image_url.url)
|
|
852
|
+
if image_part:
|
|
853
|
+
content_blocks.append(image_part)
|
|
814
854
|
else:
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
855
|
+
raise ValueError(f"不支持的额外内容块类型: {type(part)}")
|
|
856
|
+
|
|
857
|
+
# 3. 图片内容
|
|
858
|
+
if image_urls:
|
|
859
|
+
for image_url in image_urls:
|
|
860
|
+
image_part = await resolve_image_part(image_url)
|
|
861
|
+
if image_part:
|
|
862
|
+
content_blocks.append(image_part)
|
|
863
|
+
|
|
864
|
+
# 如果只有主文本且没有额外内容块和图片,返回简单格式以保持向后兼容
|
|
865
|
+
if (
|
|
866
|
+
text
|
|
867
|
+
and not extra_user_content_parts
|
|
868
|
+
and not image_urls
|
|
869
|
+
and len(content_blocks) == 1
|
|
870
|
+
and content_blocks[0]["type"] == "text"
|
|
871
|
+
):
|
|
872
|
+
return {"role": "user", "content": content_blocks[0]["text"]}
|
|
873
|
+
|
|
874
|
+
# 否则返回多模态格式
|
|
875
|
+
return {"role": "user", "content": content_blocks}
|
|
827
876
|
|
|
828
877
|
async def encode_image_bs64(self, image_url: str) -> str:
|
|
829
878
|
"""将图片转换为 base64"""
|
|
@@ -17,7 +17,7 @@ from openai.types.completion_usage import CompletionUsage
|
|
|
17
17
|
import astrbot.core.message.components as Comp
|
|
18
18
|
from astrbot import logger
|
|
19
19
|
from astrbot.api.provider import Provider
|
|
20
|
-
from astrbot.core.agent.message import Message
|
|
20
|
+
from astrbot.core.agent.message import ContentPart, ImageURLPart, Message, TextPart
|
|
21
21
|
from astrbot.core.agent.tool import ToolSet
|
|
22
22
|
from astrbot.core.message.message_event_result import MessageChain
|
|
23
23
|
from astrbot.core.provider.entities import LLMResponse, TokenUsage, ToolCallsResult
|
|
@@ -348,6 +348,7 @@ class ProviderOpenAIOfficial(Provider):
|
|
|
348
348
|
system_prompt: str | None = None,
|
|
349
349
|
tool_calls_result: ToolCallsResult | list[ToolCallsResult] | None = None,
|
|
350
350
|
model: str | None = None,
|
|
351
|
+
extra_user_content_parts: list[ContentPart] | None = None,
|
|
351
352
|
**kwargs,
|
|
352
353
|
) -> tuple:
|
|
353
354
|
"""准备聊天所需的有效载荷和上下文"""
|
|
@@ -355,7 +356,9 @@ class ProviderOpenAIOfficial(Provider):
|
|
|
355
356
|
contexts = []
|
|
356
357
|
new_record = None
|
|
357
358
|
if prompt is not None:
|
|
358
|
-
new_record = await self.assemble_context(
|
|
359
|
+
new_record = await self.assemble_context(
|
|
360
|
+
prompt, image_urls, extra_user_content_parts
|
|
361
|
+
)
|
|
359
362
|
context_query = self._ensure_message_to_dicts(contexts)
|
|
360
363
|
if new_record:
|
|
361
364
|
context_query.append(new_record)
|
|
@@ -476,6 +479,7 @@ class ProviderOpenAIOfficial(Provider):
|
|
|
476
479
|
system_prompt=None,
|
|
477
480
|
tool_calls_result=None,
|
|
478
481
|
model=None,
|
|
482
|
+
extra_user_content_parts=None,
|
|
479
483
|
**kwargs,
|
|
480
484
|
) -> LLMResponse:
|
|
481
485
|
payloads, context_query = await self._prepare_chat_payload(
|
|
@@ -485,6 +489,7 @@ class ProviderOpenAIOfficial(Provider):
|
|
|
485
489
|
system_prompt,
|
|
486
490
|
tool_calls_result,
|
|
487
491
|
model=model,
|
|
492
|
+
extra_user_content_parts=extra_user_content_parts,
|
|
488
493
|
**kwargs,
|
|
489
494
|
)
|
|
490
495
|
|
|
@@ -624,33 +629,71 @@ class ProviderOpenAIOfficial(Provider):
|
|
|
624
629
|
self,
|
|
625
630
|
text: str,
|
|
626
631
|
image_urls: list[str] | None = None,
|
|
632
|
+
extra_user_content_parts: list[ContentPart] | None = None,
|
|
627
633
|
) -> dict:
|
|
628
634
|
"""组装成符合 OpenAI 格式的 role 为 user 的消息段"""
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
635
|
+
|
|
636
|
+
async def resolve_image_part(image_url: str) -> dict | None:
|
|
637
|
+
if image_url.startswith("http"):
|
|
638
|
+
image_path = await download_image_by_url(image_url)
|
|
639
|
+
image_data = await self.encode_image_bs64(image_path)
|
|
640
|
+
elif image_url.startswith("file:///"):
|
|
641
|
+
image_path = image_url.replace("file:///", "")
|
|
642
|
+
image_data = await self.encode_image_bs64(image_path)
|
|
643
|
+
else:
|
|
644
|
+
image_data = await self.encode_image_bs64(image_url)
|
|
645
|
+
if not image_data:
|
|
646
|
+
logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。")
|
|
647
|
+
return None
|
|
648
|
+
return {
|
|
649
|
+
"type": "image_url",
|
|
650
|
+
"image_url": {"url": image_data},
|
|
633
651
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
652
|
+
|
|
653
|
+
# 构建内容块列表
|
|
654
|
+
content_blocks = []
|
|
655
|
+
|
|
656
|
+
# 1. 用户原始发言(OpenAI 建议:用户发言在前)
|
|
657
|
+
if text:
|
|
658
|
+
content_blocks.append({"type": "text", "text": text})
|
|
659
|
+
elif image_urls:
|
|
660
|
+
# 如果没有文本但有图片,添加占位文本
|
|
661
|
+
content_blocks.append({"type": "text", "text": "[图片]"})
|
|
662
|
+
elif extra_user_content_parts:
|
|
663
|
+
# 如果只有额外内容块,也需要添加占位文本
|
|
664
|
+
content_blocks.append({"type": "text", "text": " "})
|
|
665
|
+
|
|
666
|
+
# 2. 额外的内容块(系统提醒、指令等)
|
|
667
|
+
if extra_user_content_parts:
|
|
668
|
+
for part in extra_user_content_parts:
|
|
669
|
+
if isinstance(part, TextPart):
|
|
670
|
+
content_blocks.append({"type": "text", "text": part.text})
|
|
671
|
+
elif isinstance(part, ImageURLPart):
|
|
672
|
+
image_part = await resolve_image_part(part.image_url.url)
|
|
673
|
+
if image_part:
|
|
674
|
+
content_blocks.append(image_part)
|
|
641
675
|
else:
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
676
|
+
raise ValueError(f"不支持的额外内容块类型: {type(part)}")
|
|
677
|
+
|
|
678
|
+
# 3. 图片内容
|
|
679
|
+
if image_urls:
|
|
680
|
+
for image_url in image_urls:
|
|
681
|
+
image_part = await resolve_image_part(image_url)
|
|
682
|
+
if image_part:
|
|
683
|
+
content_blocks.append(image_part)
|
|
684
|
+
|
|
685
|
+
# 如果只有主文本且没有额外内容块和图片,返回简单格式以保持向后兼容
|
|
686
|
+
if (
|
|
687
|
+
text
|
|
688
|
+
and not extra_user_content_parts
|
|
689
|
+
and not image_urls
|
|
690
|
+
and len(content_blocks) == 1
|
|
691
|
+
and content_blocks[0]["type"] == "text"
|
|
692
|
+
):
|
|
693
|
+
return {"role": "user", "content": content_blocks[0]["text"]}
|
|
694
|
+
|
|
695
|
+
# 否则返回多模态格式
|
|
696
|
+
return {"role": "user", "content": content_blocks}
|
|
654
697
|
|
|
655
698
|
async def encode_image_bs64(self, image_url: str) -> str:
|
|
656
699
|
"""将图片转换为 base64"""
|