Skip to content

Commit ba1e222

Browse files
xunxiingzouyonghe
andauthored
fix: handle video attachment for llm (#7679)
* fix: handle video attachment for llm * fix: harden llm video attachment handling --------- Co-authored-by: 邹永赫 <1259085392@qq.com>
1 parent 0068960 commit ba1e222

File tree

3 files changed

+173
-2
lines changed

3 files changed

+173
-2
lines changed

astrbot/core/astr_main_agent.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
TOOL_CALL_PROMPT_SKILLS_LIKE_MODE,
3030
)
3131
from astrbot.core.conversation_mgr import Conversation
32-
from astrbot.core.message.components import File, Image, Record, Reply
32+
from astrbot.core.message.components import File, Image, Record, Reply, Video
3333
from astrbot.core.persona_error_reply import (
3434
extract_persona_custom_error_message_from_persona,
3535
set_persona_custom_error_message_on_event,
@@ -592,6 +592,33 @@ def _append_quoted_audio_attachment(req: ProviderRequest, audio_path: str) -> No
592592
)
593593

594594

595+
async def _append_video_attachment(
596+
req: ProviderRequest,
597+
video: Video,
598+
*,
599+
quoted: bool = False,
600+
) -> None:
601+
try:
602+
video_path = await video.convert_to_file_path()
603+
except Exception as exc: # noqa: BLE001
604+
if quoted:
605+
logger.error("Error processing quoted video attachment: %s", exc)
606+
else:
607+
logger.error("Error processing video attachment: %s", exc)
608+
return
609+
610+
video_name = os.path.basename(video_path)
611+
if quoted:
612+
text = (
613+
f"[Video Attachment in quoted message: "
614+
f"name {video_name}, path {video_path}]"
615+
)
616+
else:
617+
text = f"[Video Attachment: name {video_name}, path {video_path}]"
618+
619+
req.extra_user_content_parts.append(TextPart(text=text))
620+
621+
595622
def _get_quoted_message_parser_settings(
596623
provider_settings: dict[str, object] | None,
597624
) -> QuotedMessageParserSettings:
@@ -1278,6 +1305,8 @@ async def build_main_agent(
12781305
text=f"[File Attachment: name {file_name}, path {file_path}]"
12791306
)
12801307
)
1308+
elif isinstance(comp, Video):
1309+
await _append_video_attachment(req, comp)
12811310
# quoted message attachments
12821311
reply_comps = [
12831312
comp for comp in event.message_obj.message if isinstance(comp, Reply)
@@ -1316,6 +1345,8 @@ async def build_main_agent(
13161345
)
13171346
)
13181347
)
1348+
elif isinstance(reply_comp, Video):
1349+
await _append_video_attachment(req, reply_comp, quoted=True)
13191350

13201351
# Fallback quoted image extraction for reply-id-only payloads, or when
13211352
# embedded reply chain only contains placeholders (e.g. [Forward Message], [Image]).

tests/unit/test_astr_main_agent.py

Lines changed: 141 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from astrbot.core.agent.mcp_client import MCPTool
1010
from astrbot.core.agent.tool import FunctionTool, ToolSet
1111
from astrbot.core.conversation_mgr import Conversation
12-
from astrbot.core.message.components import File, Image, Plain, Reply
12+
from astrbot.core.message.components import File, Image, Plain, Reply, Video
1313
from astrbot.core.platform.astr_message_event import AstrMessageEvent
1414
from astrbot.core.platform.platform_metadata import PlatformMetadata
1515
from astrbot.core.provider import Provider
@@ -1067,6 +1067,146 @@ async def test_build_main_agent_with_images(
10671067

10681068
assert result is not None
10691069

1070+
@pytest.mark.asyncio
1071+
async def test_build_main_agent_with_video_attachment(
1072+
self, mock_event, mock_context, mock_provider
1073+
):
1074+
"""Test building main agent with video attachments."""
1075+
module = ama
1076+
mock_video = Video(file="file:///path/to/video.mp4")
1077+
mock_event.message_obj.message = [mock_video]
1078+
1079+
mock_context.get_provider_by_id.return_value = None
1080+
mock_context.get_using_provider.return_value = mock_provider
1081+
mock_context.get_config.return_value = {}
1082+
1083+
conv_mgr = mock_context.conversation_manager
1084+
_setup_conversation_for_build(conv_mgr)
1085+
1086+
with (
1087+
patch("astrbot.core.astr_main_agent.AgentRunner") as mock_runner_cls,
1088+
patch("astrbot.core.astr_main_agent.AstrAgentContext"),
1089+
):
1090+
mock_runner = MagicMock()
1091+
mock_runner.reset = AsyncMock()
1092+
mock_runner_cls.return_value = mock_runner
1093+
1094+
result = await module.build_main_agent(
1095+
event=mock_event,
1096+
plugin_context=mock_context,
1097+
config=module.MainAgentBuildConfig(tool_call_timeout=60),
1098+
)
1099+
1100+
assert result is not None
1101+
assert [
1102+
part.text for part in result.provider_request.extra_user_content_parts
1103+
] == ["[Video Attachment: name video.mp4, path path/to/video.mp4]"]
1104+
1105+
@pytest.mark.asyncio
1106+
async def test_build_main_agent_with_quoted_video_attachment(
1107+
self, mock_event, mock_context, mock_provider
1108+
):
1109+
"""Test building main agent with quoted video attachments."""
1110+
module = ama
1111+
mock_video = Video(file="file:///path/to/quoted-video.mp4")
1112+
mock_reply = Reply(
1113+
id="reply-1",
1114+
chain=[mock_video],
1115+
sender_nickname="",
1116+
message_str="quoted message",
1117+
)
1118+
mock_event.message_obj.message = [Plain(text="Hello"), mock_reply]
1119+
1120+
mock_context.get_provider_by_id.return_value = None
1121+
mock_context.get_using_provider.return_value = mock_provider
1122+
mock_context.get_config.return_value = {}
1123+
1124+
conv_mgr = mock_context.conversation_manager
1125+
_setup_conversation_for_build(conv_mgr)
1126+
1127+
with (
1128+
patch("astrbot.core.astr_main_agent.AgentRunner") as mock_runner_cls,
1129+
patch("astrbot.core.astr_main_agent.AstrAgentContext"),
1130+
):
1131+
mock_runner = MagicMock()
1132+
mock_runner.reset = AsyncMock()
1133+
mock_runner_cls.return_value = mock_runner
1134+
1135+
result = await module.build_main_agent(
1136+
event=mock_event,
1137+
plugin_context=mock_context,
1138+
config=module.MainAgentBuildConfig(tool_call_timeout=60),
1139+
)
1140+
1141+
assert result is not None
1142+
assert (
1143+
"[Video Attachment in quoted message: "
1144+
"name quoted-video.mp4, path path/to/quoted-video.mp4]"
1145+
) in [part.text for part in result.provider_request.extra_user_content_parts]
1146+
1147+
@pytest.mark.asyncio
1148+
async def test_build_main_agent_skips_video_attachment_when_conversion_fails(
1149+
self, mock_event, mock_context, mock_provider
1150+
):
1151+
"""Test video attachment failures do not abort request construction."""
1152+
module = ama
1153+
mock_video = Video(file="file:///path/to/direct.mp4")
1154+
mock_quoted_video = Video(file="file:///path/to/quoted.mp4")
1155+
mock_reply = Reply(
1156+
id="reply-1",
1157+
chain=[mock_quoted_video],
1158+
sender_nickname="",
1159+
message_str="quoted message",
1160+
)
1161+
mock_event.message_obj.message = [mock_video, mock_reply]
1162+
1163+
mock_context.get_provider_by_id.return_value = None
1164+
mock_context.get_using_provider.return_value = mock_provider
1165+
mock_context.get_config.return_value = {}
1166+
1167+
conv_mgr = mock_context.conversation_manager
1168+
_setup_conversation_for_build(conv_mgr)
1169+
1170+
async def _raise_video_conversion_error(self):
1171+
if self.file.endswith("direct.mp4"):
1172+
raise RuntimeError("direct")
1173+
raise RuntimeError("quoted")
1174+
1175+
with (
1176+
patch("astrbot.core.astr_main_agent.AgentRunner") as mock_runner_cls,
1177+
patch("astrbot.core.astr_main_agent.AstrAgentContext"),
1178+
patch("astrbot.core.astr_main_agent.logger") as mock_logger,
1179+
patch.object(
1180+
Video,
1181+
"convert_to_file_path",
1182+
AsyncMock(side_effect=_raise_video_conversion_error),
1183+
),
1184+
):
1185+
mock_runner = MagicMock()
1186+
mock_runner.reset = AsyncMock()
1187+
mock_runner_cls.return_value = mock_runner
1188+
1189+
result = await module.build_main_agent(
1190+
event=mock_event,
1191+
plugin_context=mock_context,
1192+
config=module.MainAgentBuildConfig(tool_call_timeout=60),
1193+
)
1194+
1195+
assert result is not None
1196+
assert not any(
1197+
"Video Attachment" in part.text
1198+
for part in result.provider_request.extra_user_content_parts
1199+
)
1200+
assert mock_logger.error.call_count == 2
1201+
assert (
1202+
"Error processing video attachment"
1203+
in mock_logger.error.call_args_list[0][0][0]
1204+
)
1205+
assert (
1206+
"Error processing quoted video attachment"
1207+
in mock_logger.error.call_args_list[1][0][0]
1208+
)
1209+
10701210
@pytest.mark.asyncio
10711211
async def test_build_main_agent_no_prompt_no_images(
10721212
self, mock_event, mock_context, mock_provider

video-fix.patch

5.11 KB
Binary file not shown.

0 commit comments

Comments
 (0)