--- phase: 02-agent-features plan: "05" subsystem: orchestrator-media tags: [multimodal, media, slack, whatsapp, minio, vision, llm] dependency_graph: requires: ["02-02", "02-03"] provides: ["multimodal-llm-messages", "slack-media-extraction", "whatsapp-outbound-routing"] affects: ["orchestrator/tasks.py", "orchestrator/agents/builder.py", "gateway/channels/slack_media.py"] tech_stack: added: ["boto3>=1.35.0 (gateway dependency)"] patterns: - "OpenAI multipart content format: {role: user, content: [{type: text}, {type: image_url}]}" - "Vision model detection via regex pattern matching on model name" - "Channel-aware outbound routing via _send_response() dispatcher" - "TDD: RED (failing tests) -> GREEN (passing implementation)" key_files: created: - packages/gateway/gateway/channels/slack_media.py - tests/unit/test_slack_media.py - tests/unit/test_multimodal_messages.py modified: - packages/orchestrator/orchestrator/tasks.py - packages/orchestrator/orchestrator/agents/builder.py - packages/gateway/pyproject.toml key_decisions: - "boto3 added to gateway pyproject.toml — not previously installed despite whatsapp.py using it via local import; uv add --directory installs at workspace root level" - "boto3.client patched at import site in tests (patch('boto3.client')) — boto3 is a local import inside async functions, not a module-level attribute" - "build_messages_with_media() wraps build_messages_with_memory() — media enrichment is additive, not a replacement, preserving all memory context" - "AUDIO/VIDEO attachments text-referenced only in v1 — image_url blocks are image-only in OpenAI/LiteLLM format" - "Non-vision model graceful degradation: IMAGE -> '[Image attached: filename]' appended to text content" metrics: duration: "~25 minutes" completed_date: "2026-03-23" tasks_completed: 2 files_changed: 6 --- # Phase 2 Plan 05: Cross-Channel Media Support and Multimodal LLM Interpretation Summary Multimodal media pipeline wired end-to-end: Slack file_share events download to MinIO, WhatsApp responses routed via send_whatsapp_message, and LLM prompts enriched with image_url content blocks for vision-capable models. ## What Was Built ### Task 1: Slack file_share extraction and channel-aware outbound routing (commit: 9dd7c48) **New: `packages/gateway/gateway/channels/slack_media.py`** Provides the complete Slack file_share pipeline: - `is_file_share_event(event)` — detects `subtype == "file_share"` - `media_type_from_mime(mime_type)` — maps MIME types to MediaType enum (IMAGE/DOCUMENT/AUDIO/VIDEO) - `build_slack_storage_key(tenant_id, agent_id, message_id, filename)` — generates `{tenant_id}/{agent_id}/{message_id}/{filename}` storage key - `build_attachment_from_slack_file(file_info, storage_key)` — creates MediaAttachment from Slack file metadata - `download_and_store_slack_file(...)` — async: downloads via bot token, uploads to MinIO, returns (storage_key, presigned_url) **Updated: `packages/orchestrator/orchestrator/tasks.py`** Added `_send_response(channel, text, extras)` helper function for channel-aware outbound routing: - `channel == "slack"` → calls `_update_slack_placeholder()` with `bot_token`, `channel_id`, `placeholder_ts` from extras - `channel == "whatsapp"` → calls `send_whatsapp_message()` with `phone_number_id`, `bot_token` (access_token), `wa_id` from extras - Unknown channels → logs warning and returns (no crash) Added `send_whatsapp_message` import from `gateway.channels.whatsapp` at module top. **Updated: `packages/gateway/pyproject.toml`** Added `boto3>=1.35.0` as an explicit gateway dependency (was used via local import in whatsapp.py but never declared). ### Task 2: Multimodal LLM interpretation with image_url content blocks (commit: 669c0b5) **Updated: `packages/orchestrator/orchestrator/agents/builder.py`** Added three new functions: 1. `supports_vision(model_name: str) -> bool` - Strips provider prefixes (`anthropic/`, `openai/`) before matching - Returns True for: claude-3*, gpt-4o*, gpt-4-vision*, gemini-pro-vision*, gemini-1.5*, gemini-2* - Returns False for: gpt-3.5-turbo, gpt-4, ollama/llama3, etc. 2. `generate_presigned_url(storage_key: str, expiry: int = 3600) -> str` - Uses boto3 S3 client with MinIO endpoint from shared settings - Default 1-hour expiry - Returns presigned GET URL string 3. `build_messages_with_media(agent, current_message, media_attachments, recent_messages, relevant_context) -> list[dict]` - Extends `build_messages_with_memory()` with media injection - IMAGE + vision model → multipart content list with `image_url` blocks (`detail: "auto"`) - IMAGE + non-vision model → appends `[Image attached: filename]` text - DOCUMENT → appends `[Document attached: filename - presigned_url]` text (any model) - AUDIO/VIDEO → appends `[Audio/Video attached: filename]` text - Missing storage_key → skipped gracefully (logged at DEBUG) - Memory context (recent + relevant) fully preserved alongside media ## Tests | File | Tests | Coverage | |------|-------|----------| | `tests/unit/test_slack_media.py` | 23 | file_share detection, MIME mapping, storage key format, MinIO upload (mocked), bot token auth, channel-aware routing | | `tests/unit/test_multimodal_messages.py` | 27 | supports_vision (14 models), presigned URL (expiry, key), build_messages_with_media (8 scenarios) | | **Total new tests** | **50** | | Full suite: 308 tests, all passing. ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 3 - Blocking] boto3 not installed despite plan assumption** - **Found during:** Task 1 (GREEN phase) — test patching `boto3.client` failed with `ModuleNotFoundError` - **Issue:** Plan stated "boto3 may already be installed from Plan 02-03" — it was used via local import in whatsapp.py but never declared as a dependency, so it wasn't installed in the venv - **Fix:** Added `boto3>=1.35.0` to `packages/gateway/pyproject.toml` dependencies and ran `uv add boto3` to install at workspace root level - **Files modified:** `packages/gateway/pyproject.toml`, `pyproject.toml` (lockfile update), `uv.lock` - **Commit:** 9dd7c48 **2. [Rule 1 - Bug] boto3 test patch target mismatch** - **Found during:** Task 1 (GREEN phase) — `patch("gateway.channels.slack_media.boto3")` failed with `AttributeError: does not have the attribute 'boto3'` - **Issue:** boto3 is imported inside the async function (`import boto3` as a local import), not at module level. Patching the module attribute doesn't work for local imports. - **Fix:** Changed test patch target to `patch("boto3.client")` which patches the actual boto3 module's client factory — works regardless of where the import happens - **Files modified:** `tests/unit/test_slack_media.py` - **Commit:** 9dd7c48 ## Self-Check: PASSED Files created/verified: - FOUND: packages/gateway/gateway/channels/slack_media.py - FOUND: tests/unit/test_slack_media.py - FOUND: tests/unit/test_multimodal_messages.py Commits verified: - FOUND: 9dd7c48 feat(02-05): Slack file_share extraction and channel-aware outbound routing - FOUND: 669c0b5 feat(02-05): multimodal LLM interpretation with image_url content blocks Test suite: 308/308 passed