From 9b3e2524ac9a5a1812901789271821fded51ef18 Mon Sep 17 00:00:00 2001 From: chengyongru Date: Mon, 27 Apr 2026 10:58:48 +0800 Subject: [PATCH] fix(agent): resolve relative media paths in MessageTool When deployed with Docker and workspace mounted as a volume, sending media files failed because relative paths (e.g. output/image.png) were not resolved against the workspace directory. The process CWD differs from the workspace in containerized environments, causing os.path.isfile checks to fail in channel handlers. Normalize relative media paths at the MessageTool entry point using get_workspace_path(). --- nanobot/agent/tools/message.py | 11 ++++ tests/tools/test_message_tool.py | 97 ++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/nanobot/agent/tools/message.py b/nanobot/agent/tools/message.py index fe097e11..f35f8fe2 100644 --- a/nanobot/agent/tools/message.py +++ b/nanobot/agent/tools/message.py @@ -1,11 +1,13 @@ """Message tool for sending messages to users.""" +import os from contextvars import ContextVar from typing import Any, Awaitable, Callable from nanobot.agent.tools.base import Tool, tool_parameters from nanobot.agent.tools.schema import ArraySchema, StringSchema, tool_parameters_schema from nanobot.bus.events import OutboundMessage +from nanobot.config.paths import get_workspace_path @tool_parameters( @@ -141,6 +143,15 @@ class MessageTool(Tool): if not self._send_callback: return "Error: Message sending not configured" + if media: + resolved = [] + for p in media: + if p.startswith(("http://", "https://")) or os.path.isabs(p): + resolved.append(p) + else: + resolved.append(str(get_workspace_path() / p)) + media = resolved + metadata = dict(self._default_metadata.get()) if same_target else {} if message_id: metadata["message_id"] = message_id diff --git a/tests/tools/test_message_tool.py b/tests/tools/test_message_tool.py index feff43da..d93219f0 100644 --- a/tests/tools/test_message_tool.py +++ b/tests/tools/test_message_tool.py @@ -1,7 +1,10 @@ +import os + import pytest from nanobot.agent.tools.message import MessageTool from nanobot.bus.events import OutboundMessage +from nanobot.config.paths import get_workspace_path @pytest.mark.asyncio @@ -85,3 +88,97 @@ async def test_message_tool_does_not_inherit_metadata_for_cross_target() -> None await tool.execute(content="channel reply", channel="slack", chat_id="C999") assert sent[0].metadata == {} + + +@pytest.mark.asyncio +async def test_message_tool_resolves_relative_media_paths() -> None: + sent: list[OutboundMessage] = [] + + async def _send(msg: OutboundMessage) -> None: + sent.append(msg) + + tool = MessageTool(send_callback=_send) + + await tool.execute( + content="see attached", + channel="telegram", + chat_id="1", + media=["output/image.png"], + ) + + expected = str(get_workspace_path() / "output/image.png") + assert sent[0].media == [expected] + + +@pytest.mark.asyncio +async def test_message_tool_passes_through_absolute_media_paths() -> None: + sent: list[OutboundMessage] = [] + + async def _send(msg: OutboundMessage) -> None: + sent.append(msg) + + tool = MessageTool(send_callback=_send) + + abs_path = os.path.abspath(os.path.join(os.sep, "tmp", "abs_image.png")) + + await tool.execute( + content="see attached", + channel="telegram", + chat_id="1", + media=[abs_path], + ) + + assert sent[0].media == [abs_path] + + +@pytest.mark.asyncio +async def test_message_tool_passes_through_url_media_paths() -> None: + sent: list[OutboundMessage] = [] + + async def _send(msg: OutboundMessage) -> None: + sent.append(msg) + + tool = MessageTool(send_callback=_send) + + url = "https://example.com/image.png" + + await tool.execute( + content="see attached", + channel="telegram", + chat_id="1", + media=[url], + ) + + assert sent[0].media == [url] + + +@pytest.mark.asyncio +async def test_message_tool_resolves_mixed_media_paths() -> None: + sent: list[OutboundMessage] = [] + + async def _send(msg: OutboundMessage) -> None: + sent.append(msg) + + tool = MessageTool(send_callback=_send) + + abs_path = os.path.abspath(os.path.join(os.sep, "tmp", "absolute.png")) + + await tool.execute( + content="see attached", + channel="telegram", + chat_id="1", + media=[ + "output/relative.png", + abs_path, + "https://example.com/url.png", + "http://example.com/http.png", + ], + ) + + expected_relative = str(get_workspace_path() / "output/relative.png") + assert sent[0].media == [ + expected_relative, + abs_path, + "https://example.com/url.png", + "http://example.com/http.png", + ]