feat(02-05): Slack file_share extraction and channel-aware outbound routing

- Add gateway/channels/slack_media.py with is_file_share_event, media_type_from_mime,
  build_slack_storage_key, build_attachment_from_slack_file, download_and_store_slack_file
- Add _send_response() helper to orchestrator/tasks.py for channel-aware dispatch
  (Slack -> chat.update, WhatsApp -> send_whatsapp_message)
- Add send_whatsapp_message import to orchestrator/tasks.py for WhatsApp outbound
- Add boto3>=1.35.0 to gateway dependencies for MinIO S3 client
- Add 23 unit tests in test_slack_media.py (TDD)
This commit is contained in:
2026-03-23 15:06:45 -06:00
parent eba6c85188
commit 9dd7c481a3
6 changed files with 809 additions and 1 deletions

View File

@@ -44,6 +44,7 @@ import json
import logging
import uuid
from gateway.channels.whatsapp import send_whatsapp_message
from orchestrator.main import app
from shared.models.message import KonstructMessage
@@ -524,6 +525,68 @@ def _extract_tool_name_from_confirmation(confirmation_message: str) -> str:
return "unknown_tool"
async def _send_response(
channel: str,
text: str,
extras: dict,
) -> None:
"""
Channel-aware outbound routing — dispatch a response to the correct channel.
Checks ``channel`` and routes to:
- ``slack``: calls ``_update_slack_placeholder`` to replace the "Thinking..." message
- ``whatsapp``: calls ``send_whatsapp_message`` via Meta Cloud API
- other channels: logs a warning and returns (no-op for unsupported channels)
Args:
channel: Channel name from KonstructMessage.channel (e.g. "slack", "whatsapp").
text: Response text to send.
extras: Channel-specific metadata dict.
For Slack: ``bot_token``, ``channel_id``, ``placeholder_ts``
For WhatsApp: ``phone_number_id``, ``bot_token`` (access_token), ``wa_id``
"""
if channel == "slack":
bot_token: str = extras.get("bot_token", "") or ""
channel_id: str = extras.get("channel_id", "") or ""
placeholder_ts: str = extras.get("placeholder_ts", "") or ""
if not channel_id or not placeholder_ts:
logger.warning(
"_send_response: Slack channel missing channel_id or placeholder_ts in extras"
)
return
await _update_slack_placeholder(
bot_token=bot_token,
channel_id=channel_id,
placeholder_ts=placeholder_ts,
text=text,
)
elif channel == "whatsapp":
phone_number_id: str = extras.get("phone_number_id", "") or ""
access_token: str = extras.get("bot_token", "") or ""
wa_id: str = extras.get("wa_id", "") or ""
if not phone_number_id or not wa_id:
logger.warning(
"_send_response: WhatsApp channel missing phone_number_id or wa_id in extras"
)
return
await send_whatsapp_message(
phone_number_id=phone_number_id,
access_token=access_token,
recipient_wa_id=wa_id,
text=text,
)
else:
logger.warning(
"_send_response: unsupported channel=%r — response not delivered", channel
)
async def _update_slack_placeholder(
bot_token: str,
channel_id: str,