fix(send_message): native Slack media uploads for cross-channel/DM sends (#17261)#17348
fix(send_message): native Slack media uploads for cross-channel/DM sends (#17261)#17348briandevans wants to merge 2 commits into
Conversation
…nds (NousResearch#17261) The cross-channel `send_message` tool's `_send_to_platform` had no Slack-with-media branch — Slack fell through to the "non-media platforms" block and dropped attachments with the misleading warning: MEDIA attachments were omitted for slack; native send_message media delivery is currently only supported for telegram, discord, matrix, weixin, signal and yuanbao This is the bug from NousResearch#17261: in-thread agent replies upload media fine because `BasePlatformAdapter._process_message_background` dispatches through the SlackAdapter's `send_image_file` / `send_voice` / `send_video` / `send_document` (all backed by `files_upload_v2`), but cross-channel and DM sends through the `send_message` tool never enter that pipeline. This change: - Adds a `Platform.SLACK and media_files` branch in `_send_to_platform`, modeled on the existing Telegram/Discord branches: media attaches to the final chunk only. - Extends `_send_slack(token, chat_id, message, media_files=None, thread_id=None)` to call `slack_sdk.web.async_client.AsyncWebClient. files_upload_v2` directly when media is present — the same API the SlackAdapter uses internally. The text becomes the first upload's `initial_comment`; subsequent uploads in the same call don't duplicate it. `thread_id` is forwarded as `thread_ts` so in-thread cross-channel sends still land in the right thread. - Updates the "MEDIA attachments were omitted" warning + the media-only-no-text error to list slack as a supported platform. Approach choice: directly using `AsyncWebClient(token=...)` mirrors the one-shot HTTP/SDK style of the other senders in `send_message_tool.py` (no long-lived adapter, no conflict with the running gateway). The `SlackAdapter` itself remains untouched. Tests: - `TestSendSlackMedia` (6 tests): covers single image, thread_ts forwarding, multi-file caption-on-first-only invariant, missing-file error, `SlackApiError` surfacing, and a regression test that the text-only path does NOT import `slack_sdk` (gates the new dep behind `media_files`). - `TestSendToPlatformSlackMedia` (4 tests): covers media branch routing, last-chunk-only attachment for chunked messages, text-only Slack still hits the existing `chat.postMessage` path, and the warning text now lists slack. Regression guard: with `tools/send_message_tool.py` reverted to origin/main (58a6171) but the new tests in place, the three representative tests fail (`test_single_image_upload_uses_files_upload_v2`, `test_media_present_uses_send_slack_with_media`, `test_warning_message_lists_slack_as_supported`); restoring the fix makes all 97 tests in the file pass and the 228 tests across `tests/gateway/test_slack*.py` continue to pass. Fixes NousResearch#17261 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds native Slack media attachment support to the send_message tool’s cross-channel/DM delivery path by routing Slack sends with media_files through files_upload_v2 (matching the gateway Slack adapter behavior), and updates user-facing supported-platform messaging accordingly.
Changes:
- Add a Slack-specific media branch in
_send_to_platformto upload attachments on the final chunk only. - Extend
_send_slackto supportmedia_filesviaslack_sdkAsyncWebClient.files_upload_v2, and forwardthread_tsfor threaded sends. - Add regression tests covering Slack media uploads, chunking behavior, and updated warning/error messages.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
tools/send_message_tool.py |
Adds Slack media routing + files_upload_v2 upload path and updates supported-platform warning/error strings. |
tests/tools/test_send_message_tool.py |
Adds unit tests for Slack media uploads, thread forwarding, chunking behavior, and updated warnings. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if platform == Platform.SLACK and media_files: | ||
| last_result = None | ||
| for i, chunk in enumerate(chunks): | ||
| is_last = (i == len(chunks) - 1) | ||
| result = await _send_slack( | ||
| pconfig.token, | ||
| chat_id, | ||
| chunk, | ||
| media_files=media_files if is_last else [], | ||
| thread_id=thread_id, | ||
| ) |
There was a problem hiding this comment.
This Slack+media branch correctly forwards thread_id into _send_slack, but text-only Slack sends still go through the non-media loop below which calls _send_slack(pconfig.token, chat_id, chunk) without thread_id. As a result, _send_to_platform(..., thread_id=...) is ignored for Slack unless media is present. Consider passing thread_id through in the text-only Slack path as well so threaded cross-channel sends work consistently.
| if media_files: | ||
| try: | ||
| from slack_sdk.web.async_client import AsyncWebClient | ||
| from slack_sdk.errors import SlackApiError | ||
| except ImportError: | ||
| return {"error": "slack_sdk not installed. Run: pip install 'hermes-agent[slack]'"} | ||
|
|
||
| client = AsyncWebClient(token=token) | ||
| last_result = None | ||
| text_consumed = False |
There was a problem hiding this comment.
The files_upload_v2 path creates a fresh AsyncWebClient but doesn’t apply the gateway’s proxy configuration. Text-only Slack sends use resolve_proxy_url() + proxy_kwargs_for_aiohttp(), and SlackAdapter also resolves and applies a proxy to Slack SDK clients (see gateway/platforms/slack.py:_resolve_slack_proxy_url / _apply_slack_proxy). To avoid media uploads failing in proxied environments, apply the same resolved proxy URL to this AsyncWebClient (when supported) before calling files_upload_v2.
… path Addresses both Copilot inline findings on NousResearch#17348: 1. **Proxy not applied to AsyncWebClient (line 1012):** `files_upload_v2` was bypassing the proxy that the rest of the Slack stack uses. Text-only `chat.postMessage` goes through `resolve_proxy_url()` + `proxy_kwargs_for_aiohttp()`, and SlackAdapter applies a NO_PROXY-aware resolver to every Slack SDK client it constructs. The new media upload path needs the same treatment or uploads silently fail in proxied environments. Now imports `_resolve_slack_proxy_url` and `_apply_slack_proxy` from `gateway.platforms.slack` and applies the resolved proxy to the throwaway `AsyncWebClient`. The import is wrapped so the slack_sdk-only code path still works if `gateway.platforms.slack` (which depends on slack_bolt) is unavailable. 2. **Text-only Slack path ignores thread_id (line 541):** `_send_to_platform` accepts `thread_id` but the Slack text-only branch was calling `_send_slack(token, chat_id, chunk)` without passing it. So a caller like `send_message(target='slack:C123:1700000000.1')` would land in the channel root, not the thread. Now forwarded as `thread_ts` (matching the existing media path). Test changes: - Added `test_text_only_slack_forwards_thread_id` regression test for finding 1. - Added `test_proxy_resolved_via_slack_helpers_is_applied_to_client` and `test_no_proxy_resolved_does_not_set_client_proxy` for finding 2 — both verify the SlackAdapter proxy helper is consulted and the client's `.proxy` attribute is only set when a proxy is actually resolved. - Updated `test_slack_messages_are_formatted_before_send` to expect the new `thread_id=None` kwarg (was positional-only assertion). - Loosened `test_text_only_slack_still_uses_text_branch` to assert "no media_files kwarg" rather than "no kwargs at all" since thread_id is now always passed through. All 100 tests in `tests/tools/test_send_message_tool.py` pass; all 247 tests across the Slack adapter suites and `test_send_message_missing_platforms.py` still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@copilot Both findings addressed in commit Finding 1 (line 541 — text-only Slack path drops Finding 2 (line 1012 — All 100 tests in |
|
CI audit — all 37 Same-list verification: this PR shows 37 failures, main shows 37 failures, the only delta is which two members of
The two send_message regression tests added in this PR ( |
|
Closing to keep the queue clean — happy to reopen if this is still useful. |
Summary
send_messageto Slack drops MEDIA attachments and emits "MEDIA attachments were omitted for slack…" even though SlackAdapter has full media support viafiles_upload_v2. This PR adds the missing media branch._send_slacknow uploads files throughslack_sdk.web.async_client.AsyncWebClient.files_upload_v2whenmedia_filesis non-empty, withthread_tsforwarded for in-thread cross-channel sends.Fixes #17261.
The bug
Reporter (#17261) traced it:
BasePlatformAdapter._process_message_background(in-thread agent replies) dispatches MEDIA tags through the SlackAdapter'ssend_image_file/send_voice/send_video/send_document(all callfiles_upload_v2). Thesend_messagetool — used for cross-channel and DM sends — has its own hand-rolled per-platform helpers intools/send_message_tool.pyand just lacked a Slack-with-media branch:_send_to_platformhadif platform == Platform.TELEGRAM/DISCORD/MATRIX and media_files/SIGNAL and media_filesbranches that passmedia_filesthrough to platform-specific helpers.tools/send_message_tool.py:580), which surfaced the warning attools/send_message_tool.py:589-593and called_send_slack(token, chat_id, chunk)— text only.So the result the user saw — "send works in thread, fails when DMing or sending to another channel" — matches the code path exactly.
The fix
Two-part surgical change in tools/send_message_tool.py:
_send_to_platform— add a Slack media branch (mirrors the existing Telegram / Discord branches). Media attaches to the last chunk only so we don't upload duplicates per-chunk on long messages:_send_slack— extend signature to(token, chat_id, message, media_files=None, thread_id=None). Whenmedia_filesis empty, behavior is unchanged (still uses aiohttp +chat.postMessage). When media is present, it usesslack_sdk.web.async_client.AsyncWebClient(token=token).files_upload_v2(...)per file, with the message text carried asinitial_commenton the first upload only (so multi-file sends don't duplicate the caption).thread_idbecomesthread_tsso the upload lands in the right thread when one is specified.The "MEDIA attachments were omitted" warning + the media-only-no-text error were updated to list
slackas supported.Approach choice: directly using
AsyncWebClient(token=...)mirrors the one-shot HTTP/SDK style of the other senders in this file. Going through_send_yuanbao-style "fetch the running gateway adapter" would couple the cross-channel send to the gateway's lifecycle (vs. a clean, restartable HTTP/SDK call) and didn't seem worth the indirection —SlackAdapter._upload_fileitself is just a wrapper around the samefiles_upload_v2API call. TheSlackAdapteris untouched.Test plan
TestSendSlackMedia(6): single image,thread_tsforwarding, multi-file caption-on-first-only invariant, missing-file error,SlackApiErrorsurfacing, and a regression that the text-only path does not importslack_sdk(gates the dep behindmedia_files).TestSendToPlatformSlackMedia(4): media branch routing, last-chunk-only attachment for chunked messages over Slack's 39000-charMAX_MESSAGE_LENGTH, text-only Slack still hitschat.postMessage, and the warning text now lists slack.tests/tools/test_send_message_tool.py(97/97 pass, all preexisting Slack tests still green) andtests/gateway/test_slack*.py(228/228 pass).tools/send_message_tool.pyreverted to58a6171bf(currentorigin/main) but the new tests in place, three representative new tests fail (test_single_image_upload_uses_files_upload_v2,test_media_present_uses_send_slack_with_media,test_warning_message_lists_slack_as_supported); restoring the fix makes them pass.Related
Fixes #17261.