diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py index d848aba40..1452bdfae 100644 --- a/astrbot/core/astr_main_agent.py +++ b/astrbot/core/astr_main_agent.py @@ -752,6 +752,7 @@ async def _process_quote_message( plugin_context: Context, quoted_message_settings: QuotedMessageParserSettings = DEFAULT_QUOTED_MESSAGE_SETTINGS, config: MainAgentBuildConfig | None = None, + main_provider_supports_image: bool = False, ) -> None: quote = None for comp in event.message_obj.message: @@ -781,13 +782,21 @@ async def _process_quote_message( image_seg = comp break - if image_seg: + if image_seg and main_provider_supports_image: + logger.debug( + "Skipping quote image captioning because the main provider supports image input." + ) + elif image_seg and not img_cap_prov_id: + logger.debug( + "No dedicated image caption provider configured. " + "Skipping quote image captioning." + ) + elif image_seg: try: prov = None path = None compress_path = None - if img_cap_prov_id: - prov = plugin_context.get_provider_by_id(img_cap_prov_id) + prov = plugin_context.get_provider_by_id(img_cap_prov_id) if prov is None: prov = plugin_context.get_using_provider(event.unified_msg_origin) @@ -876,6 +885,7 @@ async def _decorate_llm_request( req: ProviderRequest, plugin_context: Context, config: MainAgentBuildConfig, + provider: Provider | None = None, ) -> None: cfg = config.provider_settings or plugin_context.get_config( umo=event.unified_msg_origin @@ -883,11 +893,15 @@ async def _decorate_llm_request( _apply_prompt_prefix(req, cfg) + main_provider_supports_image = provider is not None and _provider_supports_modality( + provider, "image" + ) + if req.conversation: await _ensure_persona_and_skills(req, cfg, plugin_context, event) img_cap_prov_id: str = cfg.get("default_image_caption_provider_id") or "" - if img_cap_prov_id and req.image_urls: + if img_cap_prov_id and req.image_urls and not main_provider_supports_image: await _ensure_img_caption( event, req, @@ -905,6 +919,7 @@ async def _decorate_llm_request( plugin_context, quoted_message_settings, config, + main_provider_supports_image=main_provider_supports_image, ) tz = config.timezone @@ -1418,7 +1433,7 @@ async def build_main_agent( else: return None - await _decorate_llm_request(event, req, plugin_context, config) + await _decorate_llm_request(event, req, plugin_context, config, provider=provider) await _apply_kb(event, req, plugin_context, config) diff --git a/tests/unit/test_astr_main_agent.py b/tests/unit/test_astr_main_agent.py index da1c9d966..304eda637 100644 --- a/tests/unit/test_astr_main_agent.py +++ b/tests/unit/test_astr_main_agent.py @@ -1085,6 +1085,61 @@ class TestBuildMainAgent: assert result is not None + @pytest.mark.asyncio + async def test_build_main_agent_skips_caption_when_main_provider_supports_images( + self, mock_event, mock_context, mock_provider + ): + """Test image-capable chat providers receive quoted images directly.""" + module = ama + mock_image = Image(file="file:///tmp/quoted.jpg") + mock_reply = Reply( + id="reply-1", + chain=[Plain(text="quoted text"), mock_image], + sender_nickname="", + message_str="quoted text", + ) + mock_event.message_obj.message = [Plain(text="Hello"), mock_reply] + + mock_context.get_provider_by_id.return_value = None + mock_context.get_using_provider.return_value = mock_provider + mock_context.get_config.return_value = {} + + conv_mgr = mock_context.conversation_manager + _setup_conversation_for_build(conv_mgr) + + with ( + patch("astrbot.core.astr_main_agent.AgentRunner") as mock_runner_cls, + patch("astrbot.core.astr_main_agent.AstrAgentContext"), + patch.object( + Image, + "convert_to_file_path", + AsyncMock(return_value="/tmp/quoted.jpg"), + ), + ): + mock_runner = MagicMock() + mock_runner.reset = AsyncMock() + mock_runner_cls.return_value = mock_runner + + result = await module.build_main_agent( + event=mock_event, + plugin_context=mock_context, + config=module.MainAgentBuildConfig( + tool_call_timeout=60, + provider_settings={ + "default_image_caption_provider_id": "caption-provider", + }, + ), + provider=mock_provider, + ) + + assert result is not None + assert result.provider_request.image_urls == ["/tmp/quoted.jpg"] + assert not any( + "Image Caption" in part.text or "" in part.text + for part in result.provider_request.extra_user_content_parts + ) + mock_provider.text_chat.assert_not_called() + @pytest.mark.asyncio async def test_build_main_agent_uses_image_fallback_provider( self, mock_event, mock_context