AstrBot 4.11.3__py3-none-any.whl → 4.12.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.
- astrbot/cli/__init__.py +1 -1
- astrbot/core/agent/runners/tool_loop_agent_runner.py +10 -8
- astrbot/core/config/default.py +66 -13
- astrbot/core/db/__init__.py +84 -2
- astrbot/core/db/po.py +65 -0
- astrbot/core/db/sqlite.py +225 -4
- astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +103 -49
- astrbot/core/pipeline/process_stage/utils.py +40 -0
- astrbot/core/platform/sources/discord/discord_platform_adapter.py +2 -0
- astrbot/core/platform/sources/telegram/tg_adapter.py +2 -0
- astrbot/core/platform/sources/webchat/webchat_adapter.py +3 -2
- astrbot/core/platform/sources/webchat/webchat_event.py +17 -4
- astrbot/core/provider/sources/anthropic_source.py +44 -0
- astrbot/core/sandbox/booters/base.py +31 -0
- astrbot/core/sandbox/booters/boxlite.py +186 -0
- astrbot/core/sandbox/booters/shipyard.py +67 -0
- astrbot/core/sandbox/olayer/__init__.py +5 -0
- astrbot/core/sandbox/olayer/filesystem.py +33 -0
- astrbot/core/sandbox/olayer/python.py +19 -0
- astrbot/core/sandbox/olayer/shell.py +21 -0
- astrbot/core/sandbox/sandbox_client.py +52 -0
- astrbot/core/sandbox/tools/__init__.py +10 -0
- astrbot/core/sandbox/tools/fs.py +188 -0
- astrbot/core/sandbox/tools/python.py +74 -0
- astrbot/core/sandbox/tools/shell.py +55 -0
- astrbot/core/star/context.py +162 -44
- astrbot/core/utils/metrics.py +2 -0
- astrbot/dashboard/routes/__init__.py +2 -0
- astrbot/dashboard/routes/chat.py +40 -12
- astrbot/dashboard/routes/chatui_project.py +245 -0
- astrbot/dashboard/routes/session_management.py +545 -0
- astrbot/dashboard/server.py +1 -0
- {astrbot-4.11.3.dist-info → astrbot-4.12.0.dist-info}/METADATA +2 -3
- {astrbot-4.11.3.dist-info → astrbot-4.12.0.dist-info}/RECORD +37 -28
- astrbot/builtin_stars/python_interpreter/main.py +0 -536
- astrbot/builtin_stars/python_interpreter/metadata.yaml +0 -4
- astrbot/builtin_stars/python_interpreter/requirements.txt +0 -1
- astrbot/builtin_stars/python_interpreter/shared/api.py +0 -22
- {astrbot-4.11.3.dist-info → astrbot-4.12.0.dist-info}/WHEEL +0 -0
- {astrbot-4.11.3.dist-info → astrbot-4.12.0.dist-info}/entry_points.txt +0 -0
- {astrbot-4.11.3.dist-info → astrbot-4.12.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import json
|
|
5
|
+
import os
|
|
5
6
|
from collections.abc import AsyncGenerator
|
|
6
7
|
|
|
7
8
|
from astrbot.core import logger
|
|
8
|
-
from astrbot.core.agent.message import Message
|
|
9
|
+
from astrbot.core.agent.message import Message, TextPart
|
|
9
10
|
from astrbot.core.agent.response import AgentStats
|
|
10
11
|
from astrbot.core.agent.tool import ToolSet
|
|
11
12
|
from astrbot.core.astr_agent_context import AstrAgentContext
|
|
@@ -35,8 +36,16 @@ from .....astr_agent_tool_exec import FunctionToolExecutor
|
|
|
35
36
|
from ....context import PipelineContext, call_event_hook
|
|
36
37
|
from ...stage import Stage
|
|
37
38
|
from ...utils import (
|
|
39
|
+
CHATUI_EXTRA_PROMPT,
|
|
40
|
+
EXECUTE_SHELL_TOOL,
|
|
41
|
+
FILE_DOWNLOAD_TOOL,
|
|
42
|
+
FILE_UPLOAD_TOOL,
|
|
38
43
|
KNOWLEDGE_BASE_QUERY_TOOL,
|
|
39
44
|
LLM_SAFETY_MODE_SYSTEM_PROMPT,
|
|
45
|
+
PYTHON_TOOL,
|
|
46
|
+
SANDBOX_MODE_PROMPT,
|
|
47
|
+
TOOL_CALL_PROMPT,
|
|
48
|
+
decoded_blocked,
|
|
40
49
|
retrieve_knowledge_base,
|
|
41
50
|
)
|
|
42
51
|
|
|
@@ -93,6 +102,8 @@ class InternalAgentSubStage(Stage):
|
|
|
93
102
|
"safety_mode_strategy", "system_prompt"
|
|
94
103
|
)
|
|
95
104
|
|
|
105
|
+
self.sandbox_cfg = settings.get("sandbox", {})
|
|
106
|
+
|
|
96
107
|
self.conv_manager = ctx.plugin_manager.context.conversation_manager
|
|
97
108
|
|
|
98
109
|
def _select_provider(self, event: AstrMessageEvent):
|
|
@@ -341,54 +352,45 @@ class InternalAgentSubStage(Stage):
|
|
|
341
352
|
prov: Provider,
|
|
342
353
|
):
|
|
343
354
|
"""处理 WebChat 平台的特殊情况,包括第一次 LLM 对话时总结对话内容生成 title"""
|
|
344
|
-
|
|
355
|
+
from astrbot.core import db_helper
|
|
356
|
+
|
|
357
|
+
chatui_session_id = event.session_id.split("!")[-1]
|
|
358
|
+
user_prompt = req.prompt
|
|
359
|
+
|
|
360
|
+
session = await db_helper.get_platform_session_by_id(chatui_session_id)
|
|
361
|
+
|
|
362
|
+
if (
|
|
363
|
+
not user_prompt
|
|
364
|
+
or not chatui_session_id
|
|
365
|
+
or not session
|
|
366
|
+
or session.display_name
|
|
367
|
+
):
|
|
345
368
|
return
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
369
|
+
|
|
370
|
+
llm_resp = await prov.text_chat(
|
|
371
|
+
system_prompt=(
|
|
372
|
+
"You are a conversation title generator. "
|
|
373
|
+
"Generate a concise title in the same language as the user’s input, "
|
|
374
|
+
"no more than 10 words, capturing only the core topic."
|
|
375
|
+
"If the input is a greeting, small talk, or has no clear topic, "
|
|
376
|
+
"(e.g., “hi”, “hello”, “haha”), return <None>. "
|
|
377
|
+
"Output only the title itself or <None>, with no explanations."
|
|
378
|
+
),
|
|
379
|
+
prompt=(
|
|
380
|
+
f"Generate a concise title for the following user query:\n{user_prompt}"
|
|
381
|
+
),
|
|
349
382
|
)
|
|
350
|
-
if
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if not latest_pair:
|
|
383
|
+
if llm_resp and llm_resp.completion_text:
|
|
384
|
+
title = llm_resp.completion_text.strip()
|
|
385
|
+
if not title or "<None>" in title:
|
|
354
386
|
return
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
if item.get("type") == "text":
|
|
362
|
-
text_parts.append(item.get("text", ""))
|
|
363
|
-
elif item.get("type") == "image":
|
|
364
|
-
text_parts.append("[图片]")
|
|
365
|
-
elif isinstance(item, str):
|
|
366
|
-
text_parts.append(item)
|
|
367
|
-
cleaned_text = "User: " + " ".join(text_parts).strip()
|
|
368
|
-
elif isinstance(content, str):
|
|
369
|
-
cleaned_text = "User: " + content.strip()
|
|
370
|
-
else:
|
|
371
|
-
return
|
|
372
|
-
logger.debug(f"WebChat 对话标题生成请求,清理后的文本: {cleaned_text}")
|
|
373
|
-
llm_resp = await prov.text_chat(
|
|
374
|
-
system_prompt="You are expert in summarizing user's query.",
|
|
375
|
-
prompt=(
|
|
376
|
-
f"Please summarize the following query of user:\n"
|
|
377
|
-
f"{cleaned_text}\n"
|
|
378
|
-
"Only output the summary within 10 words, DO NOT INCLUDE any other text."
|
|
379
|
-
"You must use the same language as the user."
|
|
380
|
-
"If you think the dialog is too short to summarize, only output a special mark: `<None>`"
|
|
381
|
-
),
|
|
387
|
+
logger.info(
|
|
388
|
+
f"Generated chatui title for session {chatui_session_id}: {title}"
|
|
389
|
+
)
|
|
390
|
+
await db_helper.update_platform_session(
|
|
391
|
+
session_id=chatui_session_id,
|
|
392
|
+
display_name=title,
|
|
382
393
|
)
|
|
383
|
-
if llm_resp and llm_resp.completion_text:
|
|
384
|
-
title = llm_resp.completion_text.strip()
|
|
385
|
-
if not title or "<None>" in title:
|
|
386
|
-
return
|
|
387
|
-
await self.conv_manager.update_conversation_title(
|
|
388
|
-
unified_msg_origin=event.unified_msg_origin,
|
|
389
|
-
title=title,
|
|
390
|
-
conversation_id=req.conversation.cid,
|
|
391
|
-
)
|
|
392
394
|
|
|
393
395
|
async def _save_to_history(
|
|
394
396
|
self,
|
|
@@ -466,6 +468,24 @@ class InternalAgentSubStage(Stage):
|
|
|
466
468
|
f"Unsupported llm_safety_mode strategy: {self.safety_mode_strategy}.",
|
|
467
469
|
)
|
|
468
470
|
|
|
471
|
+
def _apply_sandbox_tools(self, req: ProviderRequest, session_id: str) -> None:
|
|
472
|
+
"""Add sandbox tools to the provider request."""
|
|
473
|
+
if req.func_tool is None:
|
|
474
|
+
req.func_tool = ToolSet()
|
|
475
|
+
if self.sandbox_cfg.get("booter") == "shipyard":
|
|
476
|
+
ep = self.sandbox_cfg.get("shipyard_endpoint", "")
|
|
477
|
+
at = self.sandbox_cfg.get("shipyard_access_token", "")
|
|
478
|
+
if not ep or not at:
|
|
479
|
+
logger.error("Shipyard sandbox configuration is incomplete.")
|
|
480
|
+
return
|
|
481
|
+
os.environ["SHIPYARD_ENDPOINT"] = ep
|
|
482
|
+
os.environ["SHIPYARD_ACCESS_TOKEN"] = at
|
|
483
|
+
req.func_tool.add_tool(EXECUTE_SHELL_TOOL)
|
|
484
|
+
req.func_tool.add_tool(PYTHON_TOOL)
|
|
485
|
+
req.func_tool.add_tool(FILE_UPLOAD_TOOL)
|
|
486
|
+
req.func_tool.add_tool(FILE_DOWNLOAD_TOOL)
|
|
487
|
+
req.system_prompt += f"\n{SANDBOX_MODE_PROMPT}\n"
|
|
488
|
+
|
|
469
489
|
async def process(
|
|
470
490
|
self, event: AstrMessageEvent, provider_wake_prefix: str
|
|
471
491
|
) -> AsyncGenerator[None, None]:
|
|
@@ -501,6 +521,14 @@ class InternalAgentSubStage(Stage):
|
|
|
501
521
|
logger.debug("skip llm request: empty message and no provider_request")
|
|
502
522
|
return
|
|
503
523
|
|
|
524
|
+
api_base = provider.provider_config.get("api_base", "")
|
|
525
|
+
for host in decoded_blocked:
|
|
526
|
+
if host in api_base:
|
|
527
|
+
logger.error(
|
|
528
|
+
f"Provider API base {api_base} is blocked due to security reasons. Please use another ai provider."
|
|
529
|
+
)
|
|
530
|
+
return
|
|
531
|
+
|
|
504
532
|
logger.debug("ready to request llm provider")
|
|
505
533
|
|
|
506
534
|
# 通知等待调用 LLM(在获取锁之前)
|
|
@@ -536,6 +564,20 @@ class InternalAgentSubStage(Stage):
|
|
|
536
564
|
image_path = await comp.convert_to_file_path()
|
|
537
565
|
req.image_urls.append(image_path)
|
|
538
566
|
|
|
567
|
+
req.extra_user_content_parts.append(
|
|
568
|
+
TextPart(text=f"[Image Attachment: path {image_path}]")
|
|
569
|
+
)
|
|
570
|
+
elif isinstance(comp, File) and self.sandbox_cfg.get(
|
|
571
|
+
"enable", False
|
|
572
|
+
):
|
|
573
|
+
file_path = await comp.get_file()
|
|
574
|
+
file_name = comp.name or os.path.basename(file_path)
|
|
575
|
+
req.extra_user_content_parts.append(
|
|
576
|
+
TextPart(
|
|
577
|
+
text=f"[File Attachment: name {file_name}, path {file_path}]"
|
|
578
|
+
)
|
|
579
|
+
)
|
|
580
|
+
|
|
539
581
|
conversation = await self._get_session_conv(event)
|
|
540
582
|
req.conversation = conversation
|
|
541
583
|
req.contexts = json.loads(conversation.history)
|
|
@@ -586,6 +628,10 @@ class InternalAgentSubStage(Stage):
|
|
|
586
628
|
if self.llm_safety_mode:
|
|
587
629
|
self._apply_llm_safety_mode(req)
|
|
588
630
|
|
|
631
|
+
# apply sandbox tools
|
|
632
|
+
if self.sandbox_cfg.get("enable", False):
|
|
633
|
+
self._apply_sandbox_tools(req, req.session_id)
|
|
634
|
+
|
|
589
635
|
stream_to_general = (
|
|
590
636
|
self.unsupported_streaming_strategy == "turn_off"
|
|
591
637
|
and not event.platform_meta.support_streaming_message
|
|
@@ -609,6 +655,18 @@ class InternalAgentSubStage(Stage):
|
|
|
609
655
|
"limit"
|
|
610
656
|
]["context"]
|
|
611
657
|
|
|
658
|
+
# ChatUI 对话的标题生成
|
|
659
|
+
if event.get_platform_name() == "webchat":
|
|
660
|
+
asyncio.create_task(self._handle_webchat(event, req, provider))
|
|
661
|
+
|
|
662
|
+
# 注入 ChatUI 额外 prompt
|
|
663
|
+
# 比如 follow-up questions 提示等
|
|
664
|
+
req.system_prompt += f"\n{CHATUI_EXTRA_PROMPT}\n"
|
|
665
|
+
|
|
666
|
+
# 注入基本 prompt
|
|
667
|
+
if req.func_tool and req.func_tool.tools:
|
|
668
|
+
req.system_prompt += f"\n{TOOL_CALL_PROMPT}\n"
|
|
669
|
+
|
|
612
670
|
await agent_runner.reset(
|
|
613
671
|
provider=provider,
|
|
614
672
|
request=req,
|
|
@@ -679,10 +737,6 @@ class InternalAgentSubStage(Stage):
|
|
|
679
737
|
agent_runner.stats,
|
|
680
738
|
)
|
|
681
739
|
|
|
682
|
-
# 异步处理 WebChat 特殊情况
|
|
683
|
-
if event.get_platform_name() == "webchat":
|
|
684
|
-
asyncio.create_task(self._handle_webchat(event, req, provider))
|
|
685
|
-
|
|
686
740
|
asyncio.create_task(
|
|
687
741
|
Metric.upload(
|
|
688
742
|
llm_tick=1,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
|
|
1
3
|
from pydantic import Field
|
|
2
4
|
from pydantic.dataclasses import dataclass
|
|
3
5
|
|
|
@@ -5,6 +7,12 @@ from astrbot.api import logger, sp
|
|
|
5
7
|
from astrbot.core.agent.run_context import ContextWrapper
|
|
6
8
|
from astrbot.core.agent.tool import FunctionTool, ToolExecResult
|
|
7
9
|
from astrbot.core.astr_agent_context import AstrAgentContext
|
|
10
|
+
from astrbot.core.sandbox.tools import (
|
|
11
|
+
ExecuteShellTool,
|
|
12
|
+
FileDownloadTool,
|
|
13
|
+
FileUploadTool,
|
|
14
|
+
PythonTool,
|
|
15
|
+
)
|
|
8
16
|
from astrbot.core.star.context import Context
|
|
9
17
|
|
|
10
18
|
LLM_SAFETY_MODE_SYSTEM_PROMPT = """You are running in Safe Mode.
|
|
@@ -19,6 +27,28 @@ Rules:
|
|
|
19
27
|
- Output same language as the user's input.
|
|
20
28
|
"""
|
|
21
29
|
|
|
30
|
+
SANDBOX_MODE_PROMPT = (
|
|
31
|
+
"You have access to a sandboxed environment and can execute shell commands and Python code securely."
|
|
32
|
+
# "Your have extended skills library, such as PDF processing, image generation, data analysis, etc. "
|
|
33
|
+
# "Before handling complex tasks, please retrieve and review the documentation in the in /app/skills/ directory. "
|
|
34
|
+
# "If the current task matches the description of a specific skill, prioritize following the workflow defined by that skill."
|
|
35
|
+
# "Use `ls /app/skills/` to list all available skills. "
|
|
36
|
+
# "Use `cat /app/skills/{skill_name}/SKILL.md` to read the documentation of a specific skill."
|
|
37
|
+
# "SKILL.md might be large, you can read the description first, which is located in the YAML frontmatter of the file."
|
|
38
|
+
# "Use shell commands such as grep, sed, awk to extract relevant information from the documentation as needed.\n"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
TOOL_CALL_PROMPT = (
|
|
42
|
+
"You MUST NOT return an empty response, especially after invoking a tool."
|
|
43
|
+
"Before calling any tool, provide a brief explanatory message to the user stating the purpose of the tool call."
|
|
44
|
+
"After the tool call is completed, you must briefly summarize the results returned by the tool for the user."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
CHATUI_EXTRA_PROMPT = (
|
|
48
|
+
'When you answered, you need to add a follow up question / summarization but do not add "Follow up" words. '
|
|
49
|
+
"Such as, user asked you to generate codes, you can add: Do you need me to run these codes for you?"
|
|
50
|
+
)
|
|
51
|
+
|
|
22
52
|
|
|
23
53
|
@dataclass
|
|
24
54
|
class KnowledgeBaseQueryTool(FunctionTool[AstrAgentContext]):
|
|
@@ -135,3 +165,13 @@ async def retrieve_knowledge_base(
|
|
|
135
165
|
|
|
136
166
|
|
|
137
167
|
KNOWLEDGE_BASE_QUERY_TOOL = KnowledgeBaseQueryTool()
|
|
168
|
+
|
|
169
|
+
EXECUTE_SHELL_TOOL = ExecuteShellTool()
|
|
170
|
+
PYTHON_TOOL = PythonTool()
|
|
171
|
+
FILE_UPLOAD_TOOL = FileUploadTool()
|
|
172
|
+
FILE_DOWNLOAD_TOOL = FileDownloadTool()
|
|
173
|
+
|
|
174
|
+
# we prevent astrbot from connecting to known malicious hosts
|
|
175
|
+
# these hosts are base64 encoded
|
|
176
|
+
BLOCKED = {"dGZid2h2d3IuY2xvdWQuc2VhbG9zLmlv", "a291cmljaGF0"}
|
|
177
|
+
decoded_blocked = [base64.b64decode(b).decode("utf-8") for b in BLOCKED]
|
|
@@ -370,6 +370,8 @@ class DiscordPlatformAdapter(Platform):
|
|
|
370
370
|
for handler_md in star_handlers_registry:
|
|
371
371
|
if not star_map[handler_md.handler_module_path].activated:
|
|
372
372
|
continue
|
|
373
|
+
if not handler_md.enabled:
|
|
374
|
+
continue
|
|
373
375
|
for event_filter in handler_md.event_filters:
|
|
374
376
|
cmd_info = self._extract_command_info(event_filter, handler_md)
|
|
375
377
|
if not cmd_info:
|
|
@@ -161,6 +161,8 @@ class TelegramPlatformAdapter(Platform):
|
|
|
161
161
|
handler_metadata = handler_md
|
|
162
162
|
if not star_map[handler_metadata.handler_module_path].activated:
|
|
163
163
|
continue
|
|
164
|
+
if not handler_metadata.enabled:
|
|
165
|
+
continue
|
|
164
166
|
for event_filter in handler_metadata.event_filters:
|
|
165
167
|
cmd_info = self._extract_command_info(
|
|
166
168
|
event_filter,
|
|
@@ -93,7 +93,8 @@ class WebChatAdapter(Platform):
|
|
|
93
93
|
session: MessageSesion,
|
|
94
94
|
message_chain: MessageChain,
|
|
95
95
|
):
|
|
96
|
-
|
|
96
|
+
message_id = f"active_{str(uuid.uuid4())}"
|
|
97
|
+
await WebChatMessageEvent._send(message_id, message_chain, session.session_id)
|
|
97
98
|
await super().send_by_session(session, message_chain)
|
|
98
99
|
|
|
99
100
|
async def _get_message_history(
|
|
@@ -196,7 +197,7 @@ class WebChatAdapter(Platform):
|
|
|
196
197
|
|
|
197
198
|
abm.session_id = f"webchat!{username}!{cid}"
|
|
198
199
|
|
|
199
|
-
abm.message_id =
|
|
200
|
+
abm.message_id = payload.get("message_id")
|
|
200
201
|
|
|
201
202
|
# 处理消息段列表
|
|
202
203
|
message_parts = payload.get("message", [])
|
|
@@ -21,7 +21,10 @@ class WebChatMessageEvent(AstrMessageEvent):
|
|
|
21
21
|
|
|
22
22
|
@staticmethod
|
|
23
23
|
async def _send(
|
|
24
|
-
|
|
24
|
+
message_id: str,
|
|
25
|
+
message: MessageChain | None,
|
|
26
|
+
session_id: str,
|
|
27
|
+
streaming: bool = False,
|
|
25
28
|
) -> str | None:
|
|
26
29
|
cid = session_id.split("!")[-1]
|
|
27
30
|
web_chat_back_queue = webchat_queue_mgr.get_or_create_back_queue(cid)
|
|
@@ -31,6 +34,7 @@ class WebChatMessageEvent(AstrMessageEvent):
|
|
|
31
34
|
"type": "end",
|
|
32
35
|
"data": "",
|
|
33
36
|
"streaming": False,
|
|
37
|
+
"message_id": message_id,
|
|
34
38
|
}, # end means this request is finished
|
|
35
39
|
)
|
|
36
40
|
return
|
|
@@ -45,6 +49,7 @@ class WebChatMessageEvent(AstrMessageEvent):
|
|
|
45
49
|
"data": data,
|
|
46
50
|
"streaming": streaming,
|
|
47
51
|
"chain_type": message.type,
|
|
52
|
+
"message_id": message_id,
|
|
48
53
|
},
|
|
49
54
|
)
|
|
50
55
|
elif isinstance(comp, Json):
|
|
@@ -54,6 +59,7 @@ class WebChatMessageEvent(AstrMessageEvent):
|
|
|
54
59
|
"data": json.dumps(comp.data, ensure_ascii=False),
|
|
55
60
|
"streaming": streaming,
|
|
56
61
|
"chain_type": message.type,
|
|
62
|
+
"message_id": message_id,
|
|
57
63
|
},
|
|
58
64
|
)
|
|
59
65
|
elif isinstance(comp, Image):
|
|
@@ -69,6 +75,7 @@ class WebChatMessageEvent(AstrMessageEvent):
|
|
|
69
75
|
"type": "image",
|
|
70
76
|
"data": data,
|
|
71
77
|
"streaming": streaming,
|
|
78
|
+
"message_id": message_id,
|
|
72
79
|
},
|
|
73
80
|
)
|
|
74
81
|
elif isinstance(comp, Record):
|
|
@@ -84,6 +91,7 @@ class WebChatMessageEvent(AstrMessageEvent):
|
|
|
84
91
|
"type": "record",
|
|
85
92
|
"data": data,
|
|
86
93
|
"streaming": streaming,
|
|
94
|
+
"message_id": message_id,
|
|
87
95
|
},
|
|
88
96
|
)
|
|
89
97
|
elif isinstance(comp, File):
|
|
@@ -94,12 +102,13 @@ class WebChatMessageEvent(AstrMessageEvent):
|
|
|
94
102
|
filename = f"{uuid.uuid4()!s}{ext}"
|
|
95
103
|
dest_path = os.path.join(imgs_dir, filename)
|
|
96
104
|
shutil.copy2(file_path, dest_path)
|
|
97
|
-
data = f"[FILE]{filename}
|
|
105
|
+
data = f"[FILE]{filename}"
|
|
98
106
|
await web_chat_back_queue.put(
|
|
99
107
|
{
|
|
100
108
|
"type": "file",
|
|
101
109
|
"data": data,
|
|
102
110
|
"streaming": streaming,
|
|
111
|
+
"message_id": message_id,
|
|
103
112
|
},
|
|
104
113
|
)
|
|
105
114
|
else:
|
|
@@ -108,7 +117,8 @@ class WebChatMessageEvent(AstrMessageEvent):
|
|
|
108
117
|
return data
|
|
109
118
|
|
|
110
119
|
async def send(self, message: MessageChain | None):
|
|
111
|
-
|
|
120
|
+
message_id = self.message_obj.message_id
|
|
121
|
+
await WebChatMessageEvent._send(message_id, message, session_id=self.session_id)
|
|
112
122
|
await super().send(MessageChain([]))
|
|
113
123
|
|
|
114
124
|
async def send_streaming(self, generator, use_fallback: bool = False):
|
|
@@ -116,6 +126,7 @@ class WebChatMessageEvent(AstrMessageEvent):
|
|
|
116
126
|
reasoning_content = ""
|
|
117
127
|
cid = self.session_id.split("!")[-1]
|
|
118
128
|
web_chat_back_queue = webchat_queue_mgr.get_or_create_back_queue(cid)
|
|
129
|
+
message_id = self.message_obj.message_id
|
|
119
130
|
async for chain in generator:
|
|
120
131
|
# if chain.type == "break" and final_data:
|
|
121
132
|
# # 分割符
|
|
@@ -130,7 +141,8 @@ class WebChatMessageEvent(AstrMessageEvent):
|
|
|
130
141
|
# continue
|
|
131
142
|
|
|
132
143
|
r = await WebChatMessageEvent._send(
|
|
133
|
-
|
|
144
|
+
message_id=message_id,
|
|
145
|
+
message=chain,
|
|
134
146
|
session_id=self.session_id,
|
|
135
147
|
streaming=True,
|
|
136
148
|
)
|
|
@@ -147,6 +159,7 @@ class WebChatMessageEvent(AstrMessageEvent):
|
|
|
147
159
|
"data": final_data,
|
|
148
160
|
"reasoning": reasoning_content,
|
|
149
161
|
"streaming": True,
|
|
162
|
+
"message_id": message_id,
|
|
150
163
|
},
|
|
151
164
|
)
|
|
152
165
|
await super().send_streaming(generator, use_fallback)
|
|
@@ -127,6 +127,50 @@ class ProviderAnthropic(Provider):
|
|
|
127
127
|
],
|
|
128
128
|
},
|
|
129
129
|
)
|
|
130
|
+
elif message["role"] == "user":
|
|
131
|
+
if isinstance(message.get("content"), list):
|
|
132
|
+
converted_content = []
|
|
133
|
+
for part in message["content"]:
|
|
134
|
+
if part.get("type") == "image_url":
|
|
135
|
+
# Convert OpenAI image_url format to Anthropic image format
|
|
136
|
+
image_url_data = part.get("image_url", {})
|
|
137
|
+
url = image_url_data.get("url", "")
|
|
138
|
+
if url.startswith("data:"):
|
|
139
|
+
try:
|
|
140
|
+
_, base64_data = url.split(",", 1)
|
|
141
|
+
# Detect actual image format from binary data
|
|
142
|
+
image_bytes = base64.b64decode(base64_data)
|
|
143
|
+
media_type = self._detect_image_mime_type(
|
|
144
|
+
image_bytes
|
|
145
|
+
)
|
|
146
|
+
converted_content.append(
|
|
147
|
+
{
|
|
148
|
+
"type": "image",
|
|
149
|
+
"source": {
|
|
150
|
+
"type": "base64",
|
|
151
|
+
"media_type": media_type,
|
|
152
|
+
"data": base64_data,
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
except ValueError:
|
|
157
|
+
logger.warning(
|
|
158
|
+
f"Failed to parse image data URI: {url[:50]}..."
|
|
159
|
+
)
|
|
160
|
+
else:
|
|
161
|
+
logger.warning(
|
|
162
|
+
f"Unsupported image URL format for Anthropic: {url[:50]}..."
|
|
163
|
+
)
|
|
164
|
+
else:
|
|
165
|
+
converted_content.append(part)
|
|
166
|
+
new_messages.append(
|
|
167
|
+
{
|
|
168
|
+
"role": "user",
|
|
169
|
+
"content": converted_content,
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
else:
|
|
173
|
+
new_messages.append(message)
|
|
130
174
|
else:
|
|
131
175
|
new_messages.append(message)
|
|
132
176
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from ..olayer import FileSystemComponent, PythonComponent, ShellComponent
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SandboxBooter:
|
|
5
|
+
@property
|
|
6
|
+
def fs(self) -> FileSystemComponent: ...
|
|
7
|
+
|
|
8
|
+
@property
|
|
9
|
+
def python(self) -> PythonComponent: ...
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def shell(self) -> ShellComponent: ...
|
|
13
|
+
|
|
14
|
+
async def boot(self, session_id: str) -> None: ...
|
|
15
|
+
|
|
16
|
+
async def shutdown(self) -> None: ...
|
|
17
|
+
|
|
18
|
+
async def upload_file(self, path: str, file_name: str) -> dict:
|
|
19
|
+
"""Upload file to sandbox.
|
|
20
|
+
|
|
21
|
+
Should return a dict with `success` (bool) and `file_path` (str) keys.
|
|
22
|
+
"""
|
|
23
|
+
...
|
|
24
|
+
|
|
25
|
+
async def download_file(self, remote_path: str, local_path: str):
|
|
26
|
+
"""Download file from sandbox."""
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
async def available(self) -> bool:
|
|
30
|
+
"""Check if the sandbox is available."""
|
|
31
|
+
...
|