Skip to content

Commit 47321e9

Browse files
committed
fix: avoid duplicate quoted image captions
1 parent 32cfcbf commit 47321e9

2 files changed

Lines changed: 85 additions & 1 deletion

File tree

astrbot/core/astr_main_agent.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,7 @@ async def _process_quote_message(
776776
quoted_message_settings: QuotedMessageParserSettings = DEFAULT_QUOTED_MESSAGE_SETTINGS,
777777
config: MainAgentBuildConfig | None = None,
778778
main_provider_supports_image: bool = False,
779+
skip_quote_image_caption: bool = False,
779780
) -> None:
780781
quote = None
781782
for comp in event.message_obj.message:
@@ -805,7 +806,11 @@ async def _process_quote_message(
805806
image_seg = comp
806807
break
807808

808-
if image_seg and main_provider_supports_image:
809+
if image_seg and skip_quote_image_caption:
810+
logger.debug(
811+
"Skipping quote image captioning because image captioning already handled this request."
812+
)
813+
elif image_seg and main_provider_supports_image:
809814
logger.debug(
810815
"Skipping quote image captioning because the main provider supports image input."
811816
)
@@ -923,6 +928,7 @@ async def _decorate_llm_request(
923928
await _ensure_persona_and_skills(req, cfg, plugin_context, event)
924929

925930
img_cap_prov_id: str = cfg.get("default_image_caption_provider_id") or ""
931+
quote_images_already_captioned = False
926932
if img_cap_prov_id and req.image_urls and not main_provider_supports_image:
927933
await _ensure_img_caption(
928934
event,
@@ -931,6 +937,12 @@ async def _decorate_llm_request(
931937
plugin_context,
932938
img_cap_prov_id,
933939
)
940+
quote_images_already_captioned = any(
941+
"<image_caption>" in getattr(part, "text", "")
942+
for part in req.extra_user_content_parts
943+
)
944+
else:
945+
quote_images_already_captioned = False
934946

935947
img_cap_prov_id = cfg.get("default_image_caption_provider_id") or ""
936948
quoted_message_settings = _get_quoted_message_parser_settings(cfg)
@@ -942,6 +954,7 @@ async def _decorate_llm_request(
942954
quoted_message_settings,
943955
config,
944956
main_provider_supports_image=main_provider_supports_image,
957+
skip_quote_image_caption=quote_images_already_captioned,
945958
)
946959

947960
tz = config.timezone

tests/unit/test_astr_main_agent.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,6 +1247,77 @@ async def test_build_main_agent_skips_caption_when_main_provider_supports_images
12471247
)
12481248
mock_provider.text_chat.assert_not_called()
12491249

1250+
@pytest.mark.asyncio
1251+
async def test_build_main_agent_does_not_caption_quoted_image_twice(
1252+
self, mock_event, mock_context
1253+
):
1254+
"""Quoted images should not be captioned again after request image captioning."""
1255+
module = ama
1256+
text_provider = MagicMock(spec=Provider)
1257+
text_provider.provider_config = {
1258+
"id": "text-provider",
1259+
"modalities": ["text", "tool_use"],
1260+
}
1261+
text_provider.get_model.return_value = "text-model"
1262+
1263+
caption_provider = MagicMock(spec=Provider)
1264+
caption_provider.text_chat = AsyncMock(
1265+
return_value=MagicMock(completion_text="quoted image caption")
1266+
)
1267+
1268+
mock_reply = Reply(
1269+
id="reply-1",
1270+
chain=[Plain(text="quoted text"), Image(file="file:///tmp/quoted.jpg")],
1271+
sender_nickname="Alice",
1272+
message_str="quoted text",
1273+
)
1274+
mock_event.message_obj.message = [Plain(text="Hello"), mock_reply]
1275+
1276+
mock_context.get_provider_by_id.return_value = caption_provider
1277+
mock_context.get_using_provider.return_value = text_provider
1278+
mock_context.get_config.return_value = {}
1279+
1280+
conv_mgr = mock_context.conversation_manager
1281+
_setup_conversation_for_build(conv_mgr)
1282+
1283+
with (
1284+
patch("astrbot.core.astr_main_agent.AgentRunner") as mock_runner_cls,
1285+
patch("astrbot.core.astr_main_agent.AstrAgentContext"),
1286+
patch.object(
1287+
Image,
1288+
"convert_to_file_path",
1289+
AsyncMock(return_value="/tmp/quoted.jpg"),
1290+
),
1291+
patch(
1292+
"astrbot.core.astr_main_agent._compress_image_for_provider",
1293+
AsyncMock(side_effect=lambda path, _settings: path),
1294+
),
1295+
):
1296+
mock_runner = MagicMock()
1297+
mock_runner.reset = AsyncMock()
1298+
mock_runner_cls.return_value = mock_runner
1299+
1300+
result = await module.build_main_agent(
1301+
event=mock_event,
1302+
plugin_context=mock_context,
1303+
config=module.MainAgentBuildConfig(
1304+
tool_call_timeout=60,
1305+
provider_settings={
1306+
"default_image_caption_provider_id": "caption-provider",
1307+
},
1308+
),
1309+
provider=text_provider,
1310+
)
1311+
1312+
assert result is not None
1313+
assert caption_provider.text_chat.await_count == 1
1314+
1315+
extra_text = "\n".join(
1316+
part.text for part in result.provider_request.extra_user_content_parts
1317+
)
1318+
assert "<image_caption>quoted image caption</image_caption>" in extra_text
1319+
assert "[Image Caption in quoted message]" not in extra_text
1320+
12501321
@pytest.mark.asyncio
12511322
async def test_build_main_agent_uses_image_fallback_provider(
12521323
self, mock_event, mock_context

0 commit comments

Comments
 (0)