from __future__ import annotations from pathlib import Path from typing import Any from unittest.mock import AsyncMock, MagicMock import pytest from nanobot.agent.loop import AgentLoop from nanobot.agent.tools.image_generation import ImageGenerationToolConfig from nanobot.bus.events import InboundMessage from nanobot.bus.queue import MessageBus from nanobot.config.loader import set_config_path from nanobot.config.schema import ProviderConfig, ToolsConfig from nanobot.providers.base import LLMResponse, ToolCallRequest from nanobot.providers.image_generation import GeneratedImageResponse PNG_DATA_URL = ( "data:image/png;base64," "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=" ) class FakeImageClient: def __init__(self, **kwargs: Any) -> None: pass async def generate(self, **kwargs: Any) -> GeneratedImageResponse: return GeneratedImageResponse(images=[PNG_DATA_URL], content="", raw={}) @pytest.mark.asyncio async def test_outbound_no_longer_carries_generated_media( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: """Media delivery is now the LLM's responsibility via the message tool.""" set_config_path(tmp_path / "config.json") monkeypatch.setattr( "nanobot.agent.tools.image_generation.get_image_gen_provider", lambda name: FakeImageClient if name == "openrouter" else None, ) provider = MagicMock() provider.get_default_model.return_value = "test-model" provider.generation.max_tokens = 4096 provider.chat_with_retry = AsyncMock( side_effect=[ LLMResponse( content="", finish_reason="tool_calls", tool_calls=[ ToolCallRequest( id="call_img", name="generate_image", arguments={"prompt": "draw a tiny icon"}, ) ], ), LLMResponse(content="Done", finish_reason="stop"), ] ) provider.chat_stream_with_retry = AsyncMock() loop = AgentLoop( bus=MessageBus(), provider=provider, workspace=tmp_path, model="test-model", tools_config=ToolsConfig( image_generation=ImageGenerationToolConfig(enabled=True), ), image_generation_provider_config=ProviderConfig(api_key="sk-or-test"), ) loop.consolidator.maybe_consolidate_by_tokens = AsyncMock(return_value=False) # type: ignore[method-assign] result = await loop._process_message( InboundMessage( channel="websocket", sender_id="user", chat_id="chat-image", content="draw an icon", ) ) assert result is not None assert result.content == "Done" # OutboundMessage no longer carries generated media — # the LLM sends images via the message tool instead. assert result.media == []