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

@@ -0,0 +1,258 @@
"""
Slack file_share media extraction helpers.
Handles Slack file_share events by:
1. Detecting file_share subtype in Slack events
2. Downloading files from Slack's private download URL using the bot token
3. Uploading files to MinIO with tenant-isolated keys
4. Building MediaAttachment objects with correct media_type, filename, mime_type
Storage key format: {tenant_id}/{agent_id}/{message_id}/{filename}
This module is intentionally import-side-effect free — no boto3 or httpx
imports at the module level to keep import times fast. Both are imported
inside the async function that requires them.
"""
from __future__ import annotations
import logging
from shared.models.message import MediaAttachment, MediaType
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# MIME type → MediaType mapping
# ---------------------------------------------------------------------------
_MIME_TO_MEDIA_TYPE: dict[str, MediaType] = {
# Images
"image/jpeg": MediaType.IMAGE,
"image/jpg": MediaType.IMAGE,
"image/png": MediaType.IMAGE,
"image/gif": MediaType.IMAGE,
"image/webp": MediaType.IMAGE,
"image/bmp": MediaType.IMAGE,
"image/tiff": MediaType.IMAGE,
"image/svg+xml": MediaType.IMAGE,
# Audio
"audio/mpeg": MediaType.AUDIO,
"audio/mp3": MediaType.AUDIO,
"audio/ogg": MediaType.AUDIO,
"audio/wav": MediaType.AUDIO,
"audio/webm": MediaType.AUDIO,
"audio/aac": MediaType.AUDIO,
# Video
"video/mp4": MediaType.VIDEO,
"video/webm": MediaType.VIDEO,
"video/ogg": MediaType.VIDEO,
"video/quicktime": MediaType.VIDEO,
"video/x-msvideo": MediaType.VIDEO,
}
def media_type_from_mime(mime_type: str) -> MediaType:
"""
Map a MIME type string to a MediaType enum value.
Checks explicit MIME map first, then falls back to:
- image/* → IMAGE
- audio/* → AUDIO
- video/* → VIDEO
- everything else → DOCUMENT
Args:
mime_type: MIME type string (e.g. "image/png", "application/pdf").
Returns:
The appropriate MediaType enum value.
"""
if not mime_type:
return MediaType.DOCUMENT
lower = mime_type.lower()
# Check exact match first
if lower in _MIME_TO_MEDIA_TYPE:
return _MIME_TO_MEDIA_TYPE[lower]
# Fall back to prefix match
if lower.startswith("image/"):
return MediaType.IMAGE
if lower.startswith("audio/"):
return MediaType.AUDIO
if lower.startswith("video/"):
return MediaType.VIDEO
# Default: treat as document
return MediaType.DOCUMENT
# ---------------------------------------------------------------------------
# Event detection
# ---------------------------------------------------------------------------
def is_file_share_event(event: dict) -> bool:
"""
Return True if the Slack event is a file_share subtype event.
Args:
event: Slack event dict.
Returns:
True if event["subtype"] == "file_share", else False.
"""
return event.get("subtype") == "file_share"
# ---------------------------------------------------------------------------
# Storage key builder
# ---------------------------------------------------------------------------
def build_slack_storage_key(
tenant_id: str,
agent_id: str,
message_id: str,
filename: str,
) -> str:
"""
Build the MinIO storage key for a Slack file attachment.
Format: ``{tenant_id}/{agent_id}/{message_id}/{filename}``
This key ensures tenant-isolated, message-scoped storage. The agent_id
is included to allow per-agent media lifecycle management in the future.
Args:
tenant_id: Konstruct tenant ID.
agent_id: Agent ID that received the message.
message_id: Slack message timestamp (event ts) or generated UUID.
filename: Original filename from Slack.
Returns:
MinIO object key string.
"""
return f"{tenant_id}/{agent_id}/{message_id}/{filename}"
# ---------------------------------------------------------------------------
# Attachment builder
# ---------------------------------------------------------------------------
def build_attachment_from_slack_file(
file_info: dict,
storage_key: str,
) -> MediaAttachment:
"""
Build a MediaAttachment from Slack file metadata + storage key.
Args:
file_info: Slack file metadata dict (from files.info or inline event).
Expected keys: id, name, mimetype, size.
storage_key: MinIO storage key already written by download_and_store_slack_file.
Returns:
A populated MediaAttachment instance.
"""
mime_type: str = file_info.get("mimetype", "") or ""
filename: str = file_info.get("name", "") or ""
size_bytes: int | None = file_info.get("size")
return MediaAttachment(
media_type=media_type_from_mime(mime_type),
storage_key=storage_key,
mime_type=mime_type or None,
filename=filename or None,
size_bytes=size_bytes,
)
# ---------------------------------------------------------------------------
# Download + upload
# ---------------------------------------------------------------------------
async def download_and_store_slack_file(
tenant_id: str,
agent_id: str,
message_id: str,
file_info: dict,
bot_token: str,
expiry: int = 3600,
) -> tuple[str, str]:
"""
Download a Slack private file and store it in MinIO.
Slack files are only accessible with a valid bot token via
``url_private_download``. This function:
1. Downloads the file using the bot token in the Authorization header
2. Uploads to MinIO with the key ``{tenant_id}/{agent_id}/{message_id}/{filename}``
3. Generates a presigned URL valid for ``expiry`` seconds
Args:
tenant_id: Konstruct tenant ID.
agent_id: Agent ID (for scoped storage key).
message_id: Message ID (for scoped storage key).
file_info: Slack file metadata dict. Must include:
- ``name`` (filename)
- ``mimetype`` (MIME type)
- ``url_private_download`` (Slack private URL)
bot_token: Slack bot token (xoxb-...) for Authorization header.
expiry: Presigned URL expiry in seconds (default: 3600 = 1 hour).
Returns:
Tuple of (storage_key, presigned_url).
"""
import boto3 # type: ignore[import-untyped]
import httpx
from shared.config import settings
filename: str = file_info.get("name", "file") or "file"
mime_type: str = file_info.get("mimetype", "") or ""
download_url: str = file_info.get("url_private_download", "") or ""
storage_key = build_slack_storage_key(tenant_id, agent_id, message_id, filename)
# Download the file from Slack using the bot token
async with httpx.AsyncClient(timeout=httpx.Timeout(60.0)) as client:
response = await client.get(
download_url,
headers={"Authorization": f"Bearer {bot_token}"},
)
response.raise_for_status()
file_bytes = response.content
# Upload to MinIO
s3_client = boto3.client(
"s3",
endpoint_url=settings.minio_endpoint,
aws_access_key_id=settings.minio_access_key,
aws_secret_access_key=settings.minio_secret_key,
region_name="us-east-1", # MinIO ignores region but boto3 requires it
)
s3_client.put_object(
Bucket=settings.minio_media_bucket,
Key=storage_key,
Body=file_bytes,
ContentType=mime_type or "application/octet-stream",
)
# Generate presigned URL
presigned_url: str = s3_client.generate_presigned_url(
"get_object",
Params={"Bucket": settings.minio_media_bucket, "Key": storage_key},
ExpiresIn=expiry,
)
logger.info(
"download_and_store_slack_file: stored tenant=%s key=%s",
tenant_id,
storage_key,
)
return storage_key, presigned_url

View File

@@ -16,6 +16,7 @@ dependencies = [
"python-telegram-bot>=21.0",
"httpx>=0.28.0",
"redis>=5.0.0",
"boto3>=1.35.0",
]
[tool.uv.sources]

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,

View File

@@ -3,7 +3,9 @@ name = "konstruct"
version = "0.1.0"
description = "AI workforce platform — channel-native AI employees for Slack, Teams, and more"
requires-python = ">=3.12"
dependencies = []
dependencies = [
"boto3>=1.42.74",
]
[tool.uv.workspace]
members = [

View File

@@ -0,0 +1,428 @@
"""
Unit tests for Slack file_share media extraction and channel-aware outbound routing.
Tests:
- file_share event detection (file_share subtype identified)
- MediaAttachment creation from Slack file metadata (correct media_type, filename, mime_type)
- MinIO upload key format: {tenant_id}/{agent_id}/{message_id}/{filename}
- Channel-aware outbound routing helper (Slack vs WhatsApp)
- Mock httpx/boto3 calls to avoid real API hits
"""
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from shared.models.message import MediaType
# ---------------------------------------------------------------------------
# Helpers to build Slack event payloads
# ---------------------------------------------------------------------------
def make_file_share_event(
file_id: str = "F12345",
file_name: str = "screenshot.png",
mime_type: str = "image/png",
size: int = 4096,
workspace_id: str = "T-WORKSPACE-1",
channel: str = "C-CHANNEL-1",
user: str = "U-USER-1",
ts: str = "1711234567.000100",
thread_ts: str | None = None,
) -> dict:
"""Build a minimal Slack file_share event dict."""
event: dict = {
"type": "message",
"subtype": "file_share",
"channel": channel,
"channel_type": "channel",
"user": user,
"ts": ts,
"files": [
{
"id": file_id,
"name": file_name,
"mimetype": mime_type,
"size": size,
"url_private_download": f"https://files.slack.com/files-pri/T-xxx/{file_name}",
}
],
"_workspace_id": workspace_id,
"_bot_user_id": "U-BOT-1",
"text": "",
}
if thread_ts:
event["thread_ts"] = thread_ts
return event
# ---------------------------------------------------------------------------
# Tests: file_share detection
# ---------------------------------------------------------------------------
class TestFileShareDetection:
"""Tests that file_share subtypes are correctly identified."""
def test_is_file_share_event_true(self):
"""Event with subtype='file_share' returns True."""
from gateway.channels.slack_media import is_file_share_event
event = make_file_share_event()
assert is_file_share_event(event) is True
def test_is_file_share_event_false_for_regular_message(self):
"""Regular message event (no subtype) returns False."""
from gateway.channels.slack_media import is_file_share_event
event = {"type": "message", "text": "Hello", "user": "U123"}
assert is_file_share_event(event) is False
def test_is_file_share_event_false_for_bot_message(self):
"""Bot message subtype returns False."""
from gateway.channels.slack_media import is_file_share_event
event = {"type": "message", "subtype": "bot_message", "text": "Bot says hi"}
assert is_file_share_event(event) is False
def test_is_file_share_event_false_for_empty_event(self):
"""Empty event dict returns False."""
from gateway.channels.slack_media import is_file_share_event
assert is_file_share_event({}) is False
# ---------------------------------------------------------------------------
# Tests: MediaAttachment creation from Slack file metadata
# ---------------------------------------------------------------------------
class TestMediaAttachmentFromSlackFile:
"""Tests that Slack file metadata is correctly mapped to MediaAttachment."""
def test_image_file_produces_image_media_type(self):
"""image/png MIME type maps to MediaType.IMAGE."""
from gateway.channels.slack_media import media_type_from_mime
assert media_type_from_mime("image/png") == MediaType.IMAGE
def test_jpeg_file_produces_image_media_type(self):
"""image/jpeg MIME type maps to MediaType.IMAGE."""
from gateway.channels.slack_media import media_type_from_mime
assert media_type_from_mime("image/jpeg") == MediaType.IMAGE
def test_gif_file_produces_image_media_type(self):
"""image/gif MIME type maps to MediaType.IMAGE."""
from gateway.channels.slack_media import media_type_from_mime
assert media_type_from_mime("image/gif") == MediaType.IMAGE
def test_pdf_produces_document_media_type(self):
"""application/pdf MIME type maps to MediaType.DOCUMENT."""
from gateway.channels.slack_media import media_type_from_mime
assert media_type_from_mime("application/pdf") == MediaType.DOCUMENT
def test_docx_produces_document_media_type(self):
"""application/vnd.openxmlformats-officedocument... maps to MediaType.DOCUMENT."""
from gateway.channels.slack_media import media_type_from_mime
result = media_type_from_mime(
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
)
assert result == MediaType.DOCUMENT
def test_mp3_produces_audio_media_type(self):
"""audio/mpeg MIME type maps to MediaType.AUDIO."""
from gateway.channels.slack_media import media_type_from_mime
assert media_type_from_mime("audio/mpeg") == MediaType.AUDIO
def test_mp4_produces_video_media_type(self):
"""video/mp4 MIME type maps to MediaType.VIDEO."""
from gateway.channels.slack_media import media_type_from_mime
assert media_type_from_mime("video/mp4") == MediaType.VIDEO
def test_unknown_mime_defaults_to_document(self):
"""Unknown MIME type falls back to MediaType.DOCUMENT."""
from gateway.channels.slack_media import media_type_from_mime
assert media_type_from_mime("application/x-custom") == MediaType.DOCUMENT
def test_build_attachment_from_slack_file(self):
"""MediaAttachment is correctly built from Slack file metadata."""
from gateway.channels.slack_media import build_attachment_from_slack_file
file_info = {
"id": "F12345",
"name": "screenshot.png",
"mimetype": "image/png",
"size": 4096,
"url_private_download": "https://files.slack.com/files-pri/T-xxx/screenshot.png",
}
storage_key = "tenant-1/agent-1/msg-1/screenshot.png"
attachment = build_attachment_from_slack_file(file_info, storage_key)
assert attachment.media_type == MediaType.IMAGE
assert attachment.filename == "screenshot.png"
assert attachment.mime_type == "image/png"
assert attachment.size_bytes == 4096
assert attachment.storage_key == storage_key
def test_build_attachment_pdf(self):
"""PDF file produces DOCUMENT media type attachment."""
from gateway.channels.slack_media import build_attachment_from_slack_file
file_info = {
"id": "F99999",
"name": "report.pdf",
"mimetype": "application/pdf",
"size": 102400,
"url_private_download": "https://files.slack.com/files-pri/T-xxx/report.pdf",
}
storage_key = "tenant-2/agent-2/msg-2/report.pdf"
attachment = build_attachment_from_slack_file(file_info, storage_key)
assert attachment.media_type == MediaType.DOCUMENT
assert attachment.filename == "report.pdf"
assert attachment.mime_type == "application/pdf"
assert attachment.size_bytes == 102400
# ---------------------------------------------------------------------------
# Tests: MinIO storage key format
# ---------------------------------------------------------------------------
class TestMinIOStorageKey:
"""Tests that MinIO storage keys follow the correct format."""
def test_storage_key_format(self):
"""Storage key follows {tenant_id}/{agent_id}/{message_id}/{filename} format."""
from gateway.channels.slack_media import build_slack_storage_key
key = build_slack_storage_key(
tenant_id="tenant-abc",
agent_id="agent-xyz",
message_id="msg-123",
filename="photo.jpg",
)
assert key == "tenant-abc/agent-xyz/msg-123/photo.jpg"
def test_storage_key_with_all_parts(self):
"""Storage key includes all four parts."""
from gateway.channels.slack_media import build_slack_storage_key
key = build_slack_storage_key(
tenant_id="t1",
agent_id="a1",
message_id="m1",
filename="document.pdf",
)
parts = key.split("/")
assert len(parts) == 4
assert parts[0] == "t1"
assert parts[1] == "a1"
assert parts[2] == "m1"
assert parts[3] == "document.pdf"
# ---------------------------------------------------------------------------
# Tests: Download and upload to MinIO (mocked)
# ---------------------------------------------------------------------------
class TestSlackFileDownloadAndUpload:
"""Tests for Slack file download and MinIO upload flow."""
@pytest.mark.asyncio
async def test_download_and_store_slack_file_calls_minio(self):
"""download_and_store_slack_file downloads from Slack and uploads to MinIO."""
from gateway.channels.slack_media import download_and_store_slack_file
file_info = {
"id": "F12345",
"name": "image.png",
"mimetype": "image/png",
"size": 2048,
"url_private_download": "https://files.slack.com/files-pri/T-xxx/image.png",
}
mock_response = MagicMock()
mock_response.content = b"fake-image-bytes"
mock_response.raise_for_status = MagicMock()
mock_s3 = MagicMock()
mock_s3.put_object = MagicMock()
mock_s3.generate_presigned_url = MagicMock(
return_value="http://minio:9000/konstruct-media/tenant-1/agent-1/msg-1/image.png?X-Amz-Signature=abc"
)
with patch("boto3.client") as mock_boto3_client:
mock_boto3_client.return_value = mock_s3
with patch("httpx.AsyncClient") as mock_httpx:
mock_client = AsyncMock()
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=None)
mock_client.get = AsyncMock(return_value=mock_response)
mock_httpx.return_value = mock_client
storage_key, presigned_url = await download_and_store_slack_file(
tenant_id="tenant-1",
agent_id="agent-1",
message_id="msg-1",
file_info=file_info,
bot_token="xoxb-test-token",
)
assert storage_key == "tenant-1/agent-1/msg-1/image.png"
assert "minio:9000" in presigned_url
mock_s3.put_object.assert_called_once()
mock_s3.generate_presigned_url.assert_called_once()
@pytest.mark.asyncio
async def test_download_uses_bot_token_for_authorization(self):
"""Download request includes Slack bot token in Authorization header."""
from gateway.channels.slack_media import download_and_store_slack_file
file_info = {
"id": "F99999",
"name": "doc.pdf",
"mimetype": "application/pdf",
"size": 5000,
"url_private_download": "https://files.slack.com/files-pri/T-xxx/doc.pdf",
}
mock_response = MagicMock()
mock_response.content = b"pdf-content"
mock_response.raise_for_status = MagicMock()
captured_headers: list[dict] = []
async def fake_get(url: str, **kwargs: object) -> MagicMock:
captured_headers.append(dict(kwargs.get("headers", {})))
return mock_response
mock_s3 = MagicMock()
mock_s3.put_object = MagicMock()
mock_s3.generate_presigned_url = MagicMock(
return_value="http://minio:9000/key?sig=xyz"
)
with patch("boto3.client") as mock_boto3_client:
mock_boto3_client.return_value = mock_s3
with patch("httpx.AsyncClient") as mock_httpx:
mock_client = AsyncMock()
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=None)
mock_client.get = AsyncMock(side_effect=fake_get)
mock_httpx.return_value = mock_client
await download_and_store_slack_file(
tenant_id="tenant-1",
agent_id="agent-1",
message_id="msg-1",
file_info=file_info,
bot_token="xoxb-my-bot-token",
)
# At least one request should include the Authorization header
auth_headers = [h.get("Authorization", "") for h in captured_headers]
assert any("xoxb-my-bot-token" in h for h in auth_headers), (
f"Expected bot token in headers, got: {auth_headers}"
)
# ---------------------------------------------------------------------------
# Tests: Channel-aware outbound routing helper
# ---------------------------------------------------------------------------
class TestChannelAwareOutbound:
"""Tests for the send_response helper that routes to Slack or WhatsApp."""
@pytest.mark.asyncio
async def test_slack_channel_calls_chat_update(self):
"""Slack channel routes to _update_slack_placeholder."""
from orchestrator.tasks import _send_response
with patch("orchestrator.tasks._update_slack_placeholder") as mock_update:
mock_update.return_value = None
await _send_response(
channel="slack",
text="Hello from agent",
extras={
"bot_token": "xoxb-test",
"channel_id": "C-123",
"placeholder_ts": "1711234567.000001",
},
)
mock_update.assert_called_once_with(
bot_token="xoxb-test",
channel_id="C-123",
placeholder_ts="1711234567.000001",
text="Hello from agent",
)
@pytest.mark.asyncio
async def test_whatsapp_channel_calls_send_whatsapp_message(self):
"""WhatsApp channel routes to send_whatsapp_message."""
from orchestrator.tasks import _send_response
with patch("orchestrator.tasks.send_whatsapp_message") as mock_wa:
mock_wa.return_value = None
await _send_response(
channel="whatsapp",
text="Reply via WhatsApp",
extras={
"phone_number_id": "12345678901",
"bot_token": "EAAx...",
"wa_id": "5511987654321",
},
)
mock_wa.assert_called_once_with(
phone_number_id="12345678901",
access_token="EAAx...",
recipient_wa_id="5511987654321",
text="Reply via WhatsApp",
)
@pytest.mark.asyncio
async def test_unknown_channel_logs_warning_no_exception(self):
"""Unknown channel type logs a warning but does not raise."""
from orchestrator.tasks import _send_response
# Should not raise, just log and return
await _send_response(
channel="mattermost",
text="Reply via Mattermost",
extras={},
)
@pytest.mark.asyncio
async def test_slack_channel_missing_extras_does_not_raise(self):
"""Slack routing with empty extras logs warning but does not crash."""
from orchestrator.tasks import _send_response
# Should fall back gracefully when extras are missing
await _send_response(
channel="slack",
text="Hello",
extras={},
)
@pytest.mark.asyncio
async def test_whatsapp_channel_missing_extras_does_not_raise(self):
"""WhatsApp routing with empty extras logs warning but does not crash."""
from orchestrator.tasks import _send_response
# Should fall back gracefully when wa_id is missing
await _send_response(
channel="whatsapp",
text="Hello",
extras={},
)

56
uv.lock generated
View File

@@ -305,6 +305,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/87/8bab77b323f16d67be364031220069f79159117dd5e43eeb4be2fef1ac9b/billiard-4.2.4-py3-none-any.whl", hash = "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5", size = 87070, upload-time = "2025-11-30T13:28:47.016Z" },
]
[[package]]
name = "boto3"
version = "1.42.74"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "botocore" },
{ name = "jmespath" },
{ name = "s3transfer" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/ad/16/a264b4da2af99f4a12609b93fea941cce5ec41da14b33ed3fef77a910f0c/boto3-1.42.74-py3-none-any.whl", hash = "sha256:4bf89c044d618fe4435af854ab820f09dd43569c0df15d7beb0398f50b9aa970", size = 140557, upload-time = "2026-03-23T19:34:07.084Z" },
]
[[package]]
name = "botocore"
version = "1.42.74"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jmespath" },
{ name = "python-dateutil" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9d/c7/cab8a14f0b69944bd0dd1fd58559163455b347eeda00bf836e93ce2684e4/botocore-1.42.74.tar.gz", hash = "sha256:9cf5cdffc6c90ed87b0fe184676806182588be0d0df9b363e9fe3e2923ac8e80", size = 15014379, upload-time = "2026-03-23T19:33:57.692Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d3/65/75852e04de5423c9b0c5b88241d0bdea33e6c6f454c88b71377d230216f2/botocore-1.42.74-py3-none-any.whl", hash = "sha256:3a76a8af08b5de82e51a0ae132394e226e15dbf21c8146ac3f7c1f881517a7a7", size = 14688218, upload-time = "2026-03-23T19:33:52.677Z" },
]
[[package]]
name = "celery"
version = "5.6.2"
@@ -1153,6 +1180,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" },
]
[[package]]
name = "jmespath"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" },
]
[[package]]
name = "joblib"
version = "1.5.3"
@@ -1213,6 +1249,9 @@ redis = [
name = "konstruct"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "boto3" },
]
[package.dev-dependencies]
dev = [
@@ -1226,6 +1265,7 @@ dev = [
]
[package.metadata]
requires-dist = [{ name = "boto3", specifier = ">=1.42.74" }]
[package.metadata.requires-dev]
dev = [
@@ -1243,6 +1283,7 @@ name = "konstruct-gateway"
version = "0.1.0"
source = { editable = "packages/gateway" }
dependencies = [
{ name = "boto3" },
{ name = "fastapi", extra = ["standard"] },
{ name = "httpx" },
{ name = "konstruct-orchestrator" },
@@ -1255,6 +1296,7 @@ dependencies = [
[package.metadata]
requires-dist = [
{ name = "boto3", specifier = ">=1.35.0" },
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.0" },
{ name = "httpx", specifier = ">=0.28.0" },
{ name = "konstruct-orchestrator", editable = "packages/orchestrator" },
@@ -1292,6 +1334,7 @@ dependencies = [
{ name = "celery", extra = ["redis"] },
{ name = "fastapi", extra = ["standard"] },
{ name = "httpx" },
{ name = "jsonschema" },
{ name = "konstruct-shared" },
{ name = "sentence-transformers" },
]
@@ -1301,6 +1344,7 @@ requires-dist = [
{ name = "celery", extras = ["redis"], specifier = ">=5.4.0" },
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.0" },
{ name = "httpx", specifier = ">=0.28.0" },
{ name = "jsonschema", specifier = ">=4.26.0" },
{ name = "konstruct-shared", editable = "packages/shared" },
{ name = "sentence-transformers", specifier = ">=3.0.0" },
]
@@ -2660,6 +2704,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8f/e8/726643a3ea68c727da31570bde48c7a10f1aa60eddd628d94078fec586ff/ruff-0.15.7-py3-none-win_arm64.whl", hash = "sha256:18e8d73f1c3fdf27931497972250340f92e8c861722161a9caeb89a58ead6ed2", size = 11023304, upload-time = "2026-03-19T16:26:51.669Z" },
]
[[package]]
name = "s3transfer"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "botocore" },
]
sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" },
]
[[package]]
name = "safetensors"
version = "0.7.0"