From 872ed9d78d8b80c557b2f141353cf8651ddc97f7 Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Thu, 12 Feb 2026 19:06:28 +0530 Subject: [PATCH 01/12] add image download feature --- backend/apps/ai/flows/assistant.py | 9 +++++++-- backend/apps/slack/common/handlers/ai.py | 20 +++++++++++++++---- backend/apps/slack/events/app_mention.py | 25 +++++++++++++++++++++++- backend/apps/slack/utils.py | 23 ++++++++++++++++++++++ 4 files changed, 70 insertions(+), 7 deletions(-) diff --git a/backend/apps/ai/flows/assistant.py b/backend/apps/ai/flows/assistant.py index 1490af34d9..75ce3ed1e4 100644 --- a/backend/apps/ai/flows/assistant.py +++ b/backend/apps/ai/flows/assistant.py @@ -48,7 +48,11 @@ def normalize_channel_id(channel_id: str) -> str: def process_query( # noqa: PLR0911 - query: str, channel_id: str | None = None, *, is_app_mention: bool = False + query: str, + images: list[str] | None = None, + channel_id: str | None = None, + *, + is_app_mention: bool = False, ) -> str | None: """Process query using multi-agent architecture. @@ -56,6 +60,7 @@ def process_query( # noqa: PLR0911 Args: query: User's question + images (list[str] | None): A list of base64 encoded image data URIs. channel_id: Optional Slack channel ID where the query originated is_app_mention: Whether this is an explicit app mention (vs channel monitored message) @@ -64,12 +69,12 @@ def process_query( # noqa: PLR0911 (for channel monitored messages with low confidence) """ + _ = images try: # Step 1: Route to appropriate expert agent router_result = route(query) intent = router_result["intent"] confidence = router_result["confidence"] - logger.info( "Query routed", extra={ diff --git a/backend/apps/slack/common/handlers/ai.py b/backend/apps/slack/common/handlers/ai.py index c743577c60..fd68bdc63c 100644 --- a/backend/apps/slack/common/handlers/ai.py +++ b/backend/apps/slack/common/handlers/ai.py @@ -12,12 +12,17 @@ def get_blocks( - query: str, channel_id: str | None = None, *, is_app_mention: bool = False + query: str, + images: list[str] | None = None, + channel_id: str | None = None, + *, + is_app_mention: bool = False, ) -> list[dict]: """Get AI response blocks. Args: query (str): The user's question. + images (list[str] | None): A list of base64 encoded image data URIs. channel_id (str | None): The Slack channel ID where the query originated. is_app_mention (bool): Whether this is an explicit app mention. @@ -26,7 +31,7 @@ def get_blocks( """ ai_response = process_ai_query( - query.strip(), channel_id=channel_id, is_app_mention=is_app_mention + query.strip(), images=images, channel_id=channel_id, is_app_mention=is_app_mention ) if ai_response: @@ -37,12 +42,17 @@ def get_blocks( def process_ai_query( - query: str, channel_id: str | None = None, *, is_app_mention: bool = False + query: str, + images: list[str] | None = None, + channel_id: str | None = None, + *, + is_app_mention: bool = False, ) -> str | None: """Process the AI query using CrewAI flow. Args: query (str): The user's question. + images (list[str] | None): A list of base64 encoded image data URIs. channel_id (str | None): The Slack channel ID where the query originated. is_app_mention (bool): Whether this is an explicit app mention. @@ -51,7 +61,9 @@ def process_ai_query( """ try: - return process_query(query, channel_id=channel_id, is_app_mention=is_app_mention) + return process_query( + query, images=images, channel_id=channel_id, is_app_mention=is_app_mention + ) except Exception: logger.exception("Failed to process AI query") return None diff --git a/backend/apps/slack/events/app_mention.py b/backend/apps/slack/events/app_mention.py index 294082729c..b10c5a5cf8 100644 --- a/backend/apps/slack/events/app_mention.py +++ b/backend/apps/slack/events/app_mention.py @@ -1,13 +1,18 @@ """Slack app mention event handler.""" +import base64 import logging from apps.slack.common.handlers.ai import get_blocks from apps.slack.events.event import EventBase from apps.slack.models import Conversation +from apps.slack.utils import download_file logger = logging.getLogger(__name__) +ALLOWED_MIMETYPES = ("image/jpeg", "image/png", "image/webp") +MAX_IMAGE_SIZE = 2 * 1024 * 1024 # 2 MB + class AppMention(EventBase): """Handles app mention events when the bot is mentioned in a channel.""" @@ -17,6 +22,7 @@ class AppMention(EventBase): def handle_event(self, event, client): """Handle an incoming app mention event.""" channel_id = event.get("channel") + files = event.get("files", []) text = event.get("text", "") if not Conversation.objects.filter( @@ -26,6 +32,12 @@ def handle_event(self, event, client): logger.warning("NestBot AI Assistant is not enabled for this conversation.") return + images_raw = [ + file + for file in files + if file.get("mimetype") in ALLOWED_MIMETYPES and file.get("size") <= MAX_IMAGE_SIZE + ] + query = text for mention in event.get("blocks", []): if mention.get("type") == "rich_text": @@ -82,8 +94,19 @@ def handle_event(self, event, client): }, ) + image_uris = [] + for image in images_raw: + content = download_file(image.get("url_private"), client.token) + if content: + image_uri = ( + f"data:{image.get('mimetype')};base64,{base64.b64encode(content).decode()}" + ) + image_uris.append(image_uri) + # Get AI response and post it - reply_blocks = get_blocks(query=query, channel_id=channel_id, is_app_mention=True) + reply_blocks = get_blocks( + query=query, images=image_uris, channel_id=channel_id, is_app_mention=True + ) client.chat_postMessage( channel=channel_id, blocks=reply_blocks, diff --git a/backend/apps/slack/utils.py b/backend/apps/slack/utils.py index c04c5c00eb..1bcbeade89 100644 --- a/backend/apps/slack/utils.py +++ b/backend/apps/slack/utils.py @@ -8,6 +8,8 @@ from html import escape as escape_html from typing import TYPE_CHECKING +import requests + if TYPE_CHECKING: # pragma: no cover from django.db.models import QuerySet @@ -17,6 +19,27 @@ logger: logging.Logger = logging.getLogger(__name__) +def download_file(url: str, token: str) -> bytes | None: + """Download Slack file. + + Args: + url (str): The url of the file. + token (str): The slack bot token. + + Returns: + bytes or None: The downloaded file content, or None if download failed. + + """ + try: + response = requests.get(url, headers={"Authorization": f"Bearer {token}"}, timeout=30) + response.raise_for_status() + except requests.RequestException as e: + logger.exception("Failed to download Slack file", extra={"error": str(e)}) + return None + + return response.content + + def escape(content: str) -> str: """Escape HTML content. From 071deec948660315b52f60b2b1c3a310a08a3bb2 Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Thu, 12 Feb 2026 22:44:43 +0530 Subject: [PATCH 02/12] update the flow to extract image information --- backend/apps/ai/flows/assistant.py | 19 ++++++++++++++++- backend/apps/common/open_ai.py | 34 +++++++++++++++++++++++++++++- backend/apps/slack/MANIFEST.yaml | 1 + 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/backend/apps/ai/flows/assistant.py b/backend/apps/ai/flows/assistant.py index 75ce3ed1e4..e96a319fdb 100644 --- a/backend/apps/ai/flows/assistant.py +++ b/backend/apps/ai/flows/assistant.py @@ -15,6 +15,7 @@ from apps.ai.agents.rag import create_rag_agent from apps.ai.common.intent import Intent from apps.ai.router import route +from apps.common.open_ai import OpenAi from apps.slack.constants import ( OWASP_COMMUNITY_CHANNEL_ID, ) @@ -23,6 +24,12 @@ CONFIDENCE_THRESHOLD = 0.7 +IMAGE_DESCRIPTION_PROMPT = ( + "Describe what is shown in these images. Focus on any text, " + "error messages, code snippets, UI elements, or technical details. " + "Be concise." +) + def normalize_channel_id(channel_id: str) -> str: """Normalize channel ID by removing # prefix if present. @@ -69,8 +76,18 @@ def process_query( # noqa: PLR0911 (for channel monitored messages with low confidence) """ - _ = images try: + if images: + image_context = ( + OpenAi(model="gpt-4o") + .set_prompt(IMAGE_DESCRIPTION_PROMPT) + .set_input(query) + .set_images(images) + .complete() + ) + if image_context: + query = f"{query}\n\nImage context: {image_context}" + # Step 1: Route to appropriate expert agent router_result = route(query) intent = router_result["intent"] diff --git a/backend/apps/common/open_ai.py b/backend/apps/common/open_ai.py index 2488da2f72..d1bcbbe5db 100644 --- a/backend/apps/common/open_ai.py +++ b/backend/apps/common/open_ai.py @@ -3,9 +3,14 @@ from __future__ import annotations import logging +from typing import TYPE_CHECKING import openai from django.conf import settings +from openai.types.chat import ChatCompletionContentPartParam + +if TYPE_CHECKING: + from openai.types.chat import ChatCompletionContentPartParam logger: logging.Logger = logging.getLogger(__name__) @@ -29,10 +34,25 @@ def __init__( timeout=30, # In seconds. ) + self.images: list[str] = [] self.max_tokens = max_tokens self.model = model self.temperature = temperature + def set_images(self, images: list[str]) -> OpenAi: + """Set images data URI. + + Args: + images (list[str]): A list of base64 encoded image data URIs. + + Returns: + OpenAi: The current instance. + + """ + self.images = images + + return self + def set_input(self, content: str) -> OpenAi: """Set system role content. @@ -75,6 +95,18 @@ def set_prompt(self, content: str) -> OpenAi: return self + @property + def user_content(self) -> list[ChatCompletionContentPartParam]: + """User message content. + + Returns: + list[dict]: User message content. + + """ + content: list[ChatCompletionContentPartParam] = [{"type": "text", "text": self.input}] + content.extend({"type": "image_url", "image_url": {"url": uri}} for uri in self.images) + return content + def complete(self) -> str | None: """Get API response. @@ -91,7 +123,7 @@ def complete(self) -> str | None: max_tokens=self.max_tokens, messages=[ {"role": "system", "content": self.prompt}, - {"role": "user", "content": self.input}, + {"role": "user", "content": self.user_content}, ], model=self.model, temperature=self.temperature, diff --git a/backend/apps/slack/MANIFEST.yaml b/backend/apps/slack/MANIFEST.yaml index 6d7f45c3f7..8cbd9abf97 100644 --- a/backend/apps/slack/MANIFEST.yaml +++ b/backend/apps/slack/MANIFEST.yaml @@ -114,6 +114,7 @@ oauth_config: - channels:read - chat:write - commands + - files:read - groups:read - groups:write - im:history From b93da112ddc8bd053e912d4c0f0b7917d8ecae3b Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Fri, 13 Feb 2026 00:49:58 +0530 Subject: [PATCH 03/12] add tests for changes --- backend/tests/apps/ai/flows/__init__.py | 0 backend/tests/apps/ai/flows/assistant_test.py | 91 ++++++++ backend/tests/apps/common/open_ai_test.py | 51 +++++ .../apps/slack/common/handlers/ai_test.py | 93 ++++++-- .../apps/slack/events/app_mention_test.py | 210 ++++++++++++++---- backend/tests/apps/slack/utils_test.py | 59 +++++ 6 files changed, 439 insertions(+), 65 deletions(-) create mode 100644 backend/tests/apps/ai/flows/__init__.py create mode 100644 backend/tests/apps/ai/flows/assistant_test.py diff --git a/backend/tests/apps/ai/flows/__init__.py b/backend/tests/apps/ai/flows/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/tests/apps/ai/flows/assistant_test.py b/backend/tests/apps/ai/flows/assistant_test.py new file mode 100644 index 0000000000..34941c347a --- /dev/null +++ b/backend/tests/apps/ai/flows/assistant_test.py @@ -0,0 +1,91 @@ +"""Tests for assistant flow image context enrichment.""" + +from unittest.mock import MagicMock, patch + +from apps.ai.flows.assistant import process_query + + +class TestProcessQueryImageEnrichment: + """Test cases for image context enrichment in process_query.""" + + @patch("apps.ai.flows.assistant.route") + @patch("apps.ai.flows.assistant.execute_task") + @patch("apps.ai.flows.assistant.OpenAi") + def test_process_query_with_images_enriches_query( + self, mock_openai_cls, mock_execute_task, mock_route + ): + """Test that images trigger OpenAI vision call and enrich the query.""" + mock_openai_instance = MagicMock() + mock_openai_instance.set_prompt.return_value = mock_openai_instance + mock_openai_instance.set_input.return_value = mock_openai_instance + mock_openai_instance.set_images.return_value = mock_openai_instance + mock_openai_instance.complete.return_value = "A screenshot of a login page" + mock_openai_cls.return_value = mock_openai_instance + + mock_route.return_value = {"intent": "rag", "confidence": 0.9} + mock_execute_task.return_value = "Response" + + images = ["data:image/png;base64,abc123"] + + process_query("What is this?", images=images) + + mock_openai_cls.assert_called_once_with(model="gpt-4o") + mock_openai_instance.set_images.assert_called_once_with(images) + mock_openai_instance.complete.assert_called_once() + + # Verify the enriched query is passed to route + enriched_query = mock_route.call_args[0][0] + assert "Image context: A screenshot of a login page" in enriched_query + assert "What is this?" in enriched_query + + @patch("apps.ai.flows.assistant.route") + @patch("apps.ai.flows.assistant.execute_task") + @patch("apps.ai.flows.assistant.OpenAi") + def test_process_query_with_images_vision_failure( + self, mock_openai_cls, mock_execute_task, mock_route + ): + """Test that failed vision call proceeds with original query.""" + mock_openai_instance = MagicMock() + mock_openai_instance.set_prompt.return_value = mock_openai_instance + mock_openai_instance.set_input.return_value = mock_openai_instance + mock_openai_instance.set_images.return_value = mock_openai_instance + mock_openai_instance.complete.return_value = None + mock_openai_cls.return_value = mock_openai_instance + + mock_route.return_value = {"intent": "rag", "confidence": 0.9} + mock_execute_task.return_value = "Response" + + process_query("What is this?", images=["data:image/png;base64,abc"]) + + # Original query should be passed without enrichment + enriched_query = mock_route.call_args[0][0] + assert enriched_query == "What is this?" + assert "Image context" not in enriched_query + + @patch("apps.ai.flows.assistant.route") + @patch("apps.ai.flows.assistant.execute_task") + @patch("apps.ai.flows.assistant.OpenAi") + def test_process_query_without_images_skips_vision( + self, mock_openai_cls, mock_execute_task, mock_route + ): + """Test that no images means no vision API call.""" + mock_route.return_value = {"intent": "rag", "confidence": 0.9} + mock_execute_task.return_value = "Response" + + process_query("What is OWASP?") + + mock_openai_cls.assert_not_called() + + @patch("apps.ai.flows.assistant.route") + @patch("apps.ai.flows.assistant.execute_task") + @patch("apps.ai.flows.assistant.OpenAi") + def test_process_query_empty_images_skips_vision( + self, mock_openai_cls, mock_execute_task, mock_route + ): + """Test that empty images list means no vision API call.""" + mock_route.return_value = {"intent": "rag", "confidence": 0.9} + mock_execute_task.return_value = "Response" + + process_query("What is OWASP?", images=[]) + + mock_openai_cls.assert_not_called() diff --git a/backend/tests/apps/common/open_ai_test.py b/backend/tests/apps/common/open_ai_test.py index c4fface6c7..e02e875ded 100644 --- a/backend/tests/apps/common/open_ai_test.py +++ b/backend/tests/apps/common/open_ai_test.py @@ -30,6 +30,14 @@ def test_init(self, mock_openai, mock_settings): assert instance.model == DEFAULT_MODEL assert instance.temperature == DEFAULT_TEMPERATURE + def test_set_images(self, openai_instance): + """Test set_images stores images and returns self for chaining.""" + images = ["data:image/png;base64,abc123"] + result = openai_instance.set_images(images) + + assert result is openai_instance + assert openai_instance.images == images + @pytest.mark.parametrize( ("input_content", "expected_input"), [("Test input content", "Test input content")] ) @@ -64,3 +72,46 @@ def test_complete_general_exception(self, mock_openai, mock_logger, openai_insta mock_logger.exception.assert_called_once_with( "An error occurred during OpenAI API request." ) + + def test_user_content_text_only(self, openai_instance): + """Test user_content returns text-only content when no images.""" + openai_instance.set_input("What is OWASP?") + + content = openai_instance.user_content + + assert isinstance(content, list) + assert len(content) == 1 + assert content[0] == {"type": "text", "text": "What is OWASP?"} + + def test_user_content_with_images(self, openai_instance): + """Test user_content returns multimodal content list with images.""" + openai_instance.set_input("What is in this image?") + openai_instance.set_images(["data:image/png;base64,abc123"]) + + content = openai_instance.user_content + + assert isinstance(content, list) + assert len(content) == 2 + assert content[0] == {"type": "text", "text": "What is in this image?"} + assert content[1] == { + "type": "image_url", + "image_url": {"url": "data:image/png;base64,abc123"}, + } + + def test_user_content_with_multiple_images(self, openai_instance): + """Test user_content with multiple images.""" + openai_instance.set_input("Describe these images") + openai_instance.set_images( + [ + "data:image/png;base64,img1", + "data:image/jpeg;base64,img2", + ] + ) + + content = openai_instance.user_content + + assert isinstance(content, list) + assert len(content) == 3 + assert content[0] == {"type": "text", "text": "Describe these images"} + assert content[1]["type"] == "image_url" + assert content[2]["type"] == "image_url" diff --git a/backend/tests/apps/slack/common/handlers/ai_test.py b/backend/tests/apps/slack/common/handlers/ai_test.py index aa218fa290..95e6175641 100644 --- a/backend/tests/apps/slack/common/handlers/ai_test.py +++ b/backend/tests/apps/slack/common/handlers/ai_test.py @@ -29,7 +29,9 @@ def test_get_blocks_with_successful_response(self, mock_markdown, mock_process_a result = get_blocks(query) - mock_process_ai_query.assert_called_once_with(query.strip()) + mock_process_ai_query.assert_called_once_with( + query.strip(), images=None, channel_id=None, is_app_mention=False + ) mock_markdown.assert_called_once_with(ai_response) assert result == [expected_block] @@ -45,7 +47,9 @@ def test_get_blocks_with_no_response(self, mock_get_error_blocks, mock_process_a result = get_blocks(query) - mock_process_ai_query.assert_called_once_with(query.strip()) + mock_process_ai_query.assert_called_once_with( + query.strip(), images=None, channel_id=None, is_app_mention=False + ) mock_get_error_blocks.assert_called_once() assert result == error_blocks @@ -61,10 +65,34 @@ def test_get_blocks_with_empty_response(self, mock_get_error_blocks, mock_proces result = get_blocks(query) - mock_process_ai_query.assert_called_once_with(query.strip()) + mock_process_ai_query.assert_called_once_with( + query.strip(), images=None, channel_id=None, is_app_mention=False + ) mock_get_error_blocks.assert_called_once() assert result == error_blocks + @patch("apps.slack.common.handlers.ai.process_ai_query") + @patch("apps.slack.common.handlers.ai.markdown") + def test_get_blocks_with_images(self, mock_markdown, mock_process_ai_query): + """Test get_blocks passes images through to process_ai_query.""" + query = "What is in this image?" + images = ["data:image/png;base64,abc123"] + ai_response = "The image shows a security dashboard." + expected_block = { + "type": "section", + "text": {"type": "mrkdwn", "text": ai_response}, + } + + mock_process_ai_query.return_value = ai_response + mock_markdown.return_value = expected_block + + result = get_blocks(query, images=images) + + mock_process_ai_query.assert_called_once_with( + query.strip(), images=images, channel_id=None, is_app_mention=False + ) + assert result == [expected_block] + @patch("apps.slack.common.handlers.ai.process_query") def test_process_ai_query_success(self, mock_process_query): """Test successful AI query processing.""" @@ -75,7 +103,9 @@ def test_process_ai_query_success(self, mock_process_query): result = process_ai_query(query) - mock_process_query.assert_called_once_with(query) + mock_process_query.assert_called_once_with( + query, images=None, channel_id=None, is_app_mention=False + ) assert result == expected_response @patch("apps.slack.common.handlers.ai.process_query") @@ -87,7 +117,9 @@ def test_process_ai_query_failure(self, mock_process_query): result = process_ai_query(query) - mock_process_query.assert_called_once_with(query) + mock_process_query.assert_called_once_with( + query, images=None, channel_id=None, is_app_mention=False + ) assert result is None @patch("apps.slack.common.handlers.ai.process_query") @@ -99,7 +131,9 @@ def test_process_ai_query_returns_none(self, mock_process_query): result = process_ai_query(query) - mock_process_query.assert_called_once_with(query) + mock_process_query.assert_called_once_with( + query, images=None, channel_id=None, is_app_mention=False + ) assert result is None @patch("apps.slack.common.handlers.ai.process_query") @@ -111,9 +145,42 @@ def test_process_ai_query_non_owasp_question(self, mock_process_query): result = process_ai_query(query) - mock_process_query.assert_called_once_with(query) + mock_process_query.assert_called_once_with( + query, images=None, channel_id=None, is_app_mention=False + ) assert result == get_default_response() + @patch("apps.slack.common.handlers.ai.process_query") + def test_process_ai_query_with_images(self, mock_process_query): + """Test process_ai_query passes images through to process_query.""" + query = "Describe this image" + images = ["data:image/jpeg;base64,img_data"] + expected_response = "The image shows..." + + mock_process_query.return_value = expected_response + + result = process_ai_query(query, images=images) + + mock_process_query.assert_called_once_with( + query, images=images, channel_id=None, is_app_mention=False + ) + assert result == expected_response + + @patch("apps.slack.common.handlers.ai.process_query") + def test_process_ai_query_with_images_and_channel(self, mock_process_query): + """Test process_ai_query forwards images along with channel_id and is_app_mention.""" + query = "What is this?" + images = ["data:image/png;base64,abc"] + channel_id = "C123456" + + mock_process_query.return_value = "Response" + + process_ai_query(query, images=images, channel_id=channel_id, is_app_mention=True) + + mock_process_query.assert_called_once_with( + query, images=images, channel_id=channel_id, is_app_mention=True + ) + @patch("apps.slack.common.handlers.ai.markdown") def test_get_error_blocks(self, mock_markdown): """Test error blocks generation.""" @@ -131,15 +198,3 @@ def test_get_error_blocks(self, mock_markdown): mock_markdown.assert_called_once_with(expected_error_message) assert result == [expected_block] - - def test_get_blocks_strips_whitespace(self): - """Test that get_blocks properly strips whitespace from query.""" - with patch("apps.slack.common.handlers.ai.process_ai_query") as mock_process_ai_query: - mock_process_ai_query.return_value = None - with patch("apps.slack.common.handlers.ai.get_error_blocks") as mock_get_error_blocks: - mock_get_error_blocks.return_value = [] - - query_with_whitespace = " What is OWASP? " - get_blocks(query_with_whitespace) - - mock_process_ai_query.assert_called_once_with("What is OWASP?") diff --git a/backend/tests/apps/slack/events/app_mention_test.py b/backend/tests/apps/slack/events/app_mention_test.py index dbd7c35c8a..a9ac1f8052 100644 --- a/backend/tests/apps/slack/events/app_mention_test.py +++ b/backend/tests/apps/slack/events/app_mention_test.py @@ -6,6 +6,8 @@ from apps.slack.events.app_mention import AppMention +TEST_BOT_TOKEN = "xoxb-test-token" # noqa: S105 + class TestAppMention: """Test cases for AppMention event handler.""" @@ -54,75 +56,65 @@ def test_handle_event_ai_disabled( mock_get_blocks.assert_not_called() mock_client.chat_postMessage.assert_not_called() + @patch("apps.slack.events.app_mention.download_file") @patch("apps.slack.events.app_mention.Conversation") @patch("apps.slack.events.app_mention.get_blocks") - def test_handle_event_no_query(self, mock_get_blocks, mock_conversation, handler, mock_client): - """Test that handler returns early when no query is found.""" - mock_conversation.objects.filter.return_value.exists.return_value = True - - event = { - "channel": "C123456", - "text": "", - "ts": "1234567890.123456", - } - - handler.handle_event(event, mock_client) - - mock_get_blocks.assert_not_called() - mock_client.chat_postMessage.assert_not_called() - - @patch("apps.slack.events.app_mention.Conversation") - @patch("apps.slack.events.app_mention.get_blocks") - def test_handle_event_success(self, mock_get_blocks, mock_conversation, handler, mock_client): - """Test successful handling of app mention event.""" + def test_handle_event_filters_unsupported_mimetypes( + self, mock_get_blocks, mock_conversation, mock_download_file, handler, mock_client + ): + """Test that files with unsupported MIME types are filtered out.""" mock_conversation.objects.filter.return_value.exists.return_value = True mock_get_blocks.return_value = [{"type": "section", "text": {"text": "Response"}}] + mock_client.token = TEST_BOT_TOKEN event = { "channel": "C123456", - "text": "What is OWASP?", + "text": "Check this file", "ts": "1234567890.123456", + "files": [ + { + "mimetype": "application/pdf", + "size": 1024, + "url_private": "https://files.slack.com/doc.pdf", + }, + ], } handler.handle_event(event, mock_client) - mock_client.reactions_add.assert_called_once_with( - channel="C123456", - timestamp="1234567890.123456", - name="eyes", - ) - mock_get_blocks.assert_called_once_with(query="What is OWASP?") - mock_client.chat_postMessage.assert_called_once_with( - channel="C123456", - blocks=[{"type": "section", "text": {"text": "Response"}}], - text="What is OWASP?", - thread_ts="1234567890.123456", - ) + mock_download_file.assert_not_called() + call_kwargs = mock_get_blocks.call_args.kwargs + assert call_kwargs["images"] == [] + @patch("apps.slack.events.app_mention.download_file") @patch("apps.slack.events.app_mention.Conversation") @patch("apps.slack.events.app_mention.get_blocks") - def test_handle_event_with_thread_ts( - self, mock_get_blocks, mock_conversation, handler, mock_client + def test_handle_event_filters_oversized_images( + self, mock_get_blocks, mock_conversation, mock_download_file, handler, mock_client ): - """Test handling event with thread_ts.""" + """Test that images exceeding MAX_IMAGE_SIZE are filtered out.""" mock_conversation.objects.filter.return_value.exists.return_value = True mock_get_blocks.return_value = [{"type": "section", "text": {"text": "Response"}}] + mock_client.token = TEST_BOT_TOKEN event = { "channel": "C123456", - "text": "What is OWASP?", + "text": "Check this image", "ts": "1234567890.123456", - "thread_ts": "1234567890.000000", + "files": [ + { + "mimetype": "image/png", + "size": 3 * 1024 * 1024, # 3MB, exceeds 2MB limit + "url_private": "https://files.slack.com/big.png", + }, + ], } handler.handle_event(event, mock_client) - mock_client.chat_postMessage.assert_called_once_with( - channel="C123456", - blocks=[{"type": "section", "text": {"text": "Response"}}], - text="What is OWASP?", - thread_ts="1234567890.000000", - ) + mock_download_file.assert_not_called() + call_kwargs = mock_get_blocks.call_args.kwargs + assert call_kwargs["images"] == [] @patch("apps.slack.events.app_mention.Conversation") @patch("apps.slack.events.app_mention.get_blocks") @@ -154,7 +146,9 @@ def test_handle_event_extract_query_from_blocks( handler.handle_event(event, mock_client) - mock_get_blocks.assert_called_once_with(query="What is OWASP?") + mock_get_blocks.assert_called_once_with( + query="What is OWASP?", images=[], channel_id="C123456", is_app_mention=True + ) @patch("apps.slack.events.app_mention.Conversation") @patch("apps.slack.events.app_mention.get_blocks") @@ -187,7 +181,9 @@ def test_handle_event_extract_query_from_blocks_multiple_elements( handler.handle_event(event, mock_client) - mock_get_blocks.assert_called_once_with(query="What is OWASP?") + mock_get_blocks.assert_called_once_with( + query="What is OWASP?", images=[], channel_id="C123456", is_app_mention=True + ) @patch("apps.slack.events.app_mention.Conversation") @patch("apps.slack.events.app_mention.get_blocks") @@ -212,7 +208,9 @@ def test_handle_event_blocks_not_rich_text( handler.handle_event(event, mock_client) - mock_get_blocks.assert_called_once_with(query="What is OWASP?") + mock_get_blocks.assert_called_once_with( + query="What is OWASP?", images=[], channel_id="C123456", is_app_mention=True + ) @patch("apps.slack.events.app_mention.Conversation") @patch("apps.slack.events.app_mention.get_blocks") @@ -277,3 +275,123 @@ def test_handle_event_reaction_exception( mock_client.reactions_add.assert_called_once() mock_get_blocks.assert_called_once() mock_client.chat_postMessage.assert_called_once() + + @patch("apps.slack.events.app_mention.download_file") + @patch("apps.slack.events.app_mention.Conversation") + @patch("apps.slack.events.app_mention.get_blocks") + def test_handle_event_with_image_files( + self, mock_get_blocks, mock_conversation, mock_download_file, handler, mock_client + ): + """Test that image files are downloaded, base64 encoded, and passed to get_blocks.""" + mock_conversation.objects.filter.return_value.exists.return_value = True + mock_get_blocks.return_value = [{"type": "section", "text": {"text": "Response"}}] + mock_download_file.return_value = b"\x89PNG\r\n" + mock_client.token = TEST_BOT_TOKEN + + event = { + "channel": "C123456", + "text": "What is in this image?", + "ts": "1234567890.123456", + "files": [ + { + "mimetype": "image/png", + "size": 1024, + "url_private": "https://files.slack.com/image.png", + }, + ], + } + + handler.handle_event(event, mock_client) + + mock_download_file.assert_called_once_with( + "https://files.slack.com/image.png", TEST_BOT_TOKEN + ) + call_kwargs = mock_get_blocks.call_args.kwargs + assert len(call_kwargs["images"]) == 1 + assert call_kwargs["images"][0].startswith("data:image/png;base64,") + + @patch("apps.slack.events.app_mention.download_file") + @patch("apps.slack.events.app_mention.Conversation") + @patch("apps.slack.events.app_mention.get_blocks") + def test_handle_event_image_download_failure( + self, mock_get_blocks, mock_conversation, mock_download_file, handler, mock_client + ): + """Test that failed image downloads are skipped gracefully.""" + mock_conversation.objects.filter.return_value.exists.return_value = True + mock_get_blocks.return_value = [{"type": "section", "text": {"text": "Response"}}] + mock_download_file.return_value = None + mock_client.token = TEST_BOT_TOKEN + + event = { + "channel": "C123456", + "text": "What is in this image?", + "ts": "1234567890.123456", + "files": [ + { + "mimetype": "image/jpeg", + "size": 500, + "url_private": "https://files.slack.com/broken.jpg", + }, + ], + } + + handler.handle_event(event, mock_client) + + mock_download_file.assert_called_once() + call_kwargs = mock_get_blocks.call_args.kwargs + assert call_kwargs["images"] == [] + + @patch("apps.slack.events.app_mention.Conversation") + @patch("apps.slack.events.app_mention.get_blocks") + def test_handle_event_success(self, mock_get_blocks, mock_conversation, handler, mock_client): + """Test successful handling of app mention event.""" + mock_conversation.objects.filter.return_value.exists.return_value = True + mock_get_blocks.return_value = [{"type": "section", "text": {"text": "Response"}}] + + event = { + "channel": "C123456", + "text": "What is OWASP?", + "ts": "1234567890.123456", + } + + handler.handle_event(event, mock_client) + + mock_client.reactions_add.assert_called_once_with( + channel="C123456", + timestamp="1234567890.123456", + name="eyes", + ) + mock_get_blocks.assert_called_once_with( + query="What is OWASP?", images=[], channel_id="C123456", is_app_mention=True + ) + mock_client.chat_postMessage.assert_called_once_with( + channel="C123456", + blocks=[{"type": "section", "text": {"text": "Response"}}], + text="What is OWASP?", + thread_ts="1234567890.123456", + ) + + @patch("apps.slack.events.app_mention.Conversation") + @patch("apps.slack.events.app_mention.get_blocks") + def test_handle_event_with_thread_ts( + self, mock_get_blocks, mock_conversation, handler, mock_client + ): + """Test handling event with thread_ts.""" + mock_conversation.objects.filter.return_value.exists.return_value = True + mock_get_blocks.return_value = [{"type": "section", "text": {"text": "Response"}}] + + event = { + "channel": "C123456", + "text": "What is OWASP?", + "ts": "1234567890.123456", + "thread_ts": "1234567890.000000", + } + + handler.handle_event(event, mock_client) + + mock_client.chat_postMessage.assert_called_once_with( + channel="C123456", + blocks=[{"type": "section", "text": {"text": "Response"}}], + text="What is OWASP?", + thread_ts="1234567890.000000", + ) diff --git a/backend/tests/apps/slack/utils_test.py b/backend/tests/apps/slack/utils_test.py index d2236ee61e..a69abcbb44 100644 --- a/backend/tests/apps/slack/utils_test.py +++ b/backend/tests/apps/slack/utils_test.py @@ -8,6 +8,7 @@ from apps.owasp.utils.news import get_news_data from apps.owasp.utils.staff import get_staff_data from apps.slack.utils import ( + download_file, escape, get_text, strip_markdown, @@ -139,6 +140,64 @@ def test_blocks_to_text(input_blocks, expected_output): assert get_text(input_blocks) == expected_output +def test_download_file_success(monkeypatch): + """Test successful file download.""" + mock_response = Mock() + mock_response.content = b"image-bytes" + mock_response.raise_for_status = Mock() + + monkeypatch.setattr("apps.slack.utils.requests.get", Mock(return_value=mock_response)) + + result = download_file("https://files.slack.com/image.png", "xoxb-token") + + assert result == b"image-bytes" + + +def test_download_file_http_error(monkeypatch): + """Test download returns None on HTTP error.""" + import requests as req + + mock_response = Mock() + mock_response.raise_for_status.side_effect = req.HTTPError("404") + + monkeypatch.setattr("apps.slack.utils.requests.get", Mock(return_value=mock_response)) + + result = download_file("https://files.slack.com/image.png", "xoxb-token") + + assert result is None + + +def test_download_file_request_exception(monkeypatch): + """Test download returns None on request exception.""" + import requests as req + + monkeypatch.setattr( + "apps.slack.utils.requests.get", Mock(side_effect=req.ConnectionError("timeout")) + ) + + result = download_file("https://files.slack.com/image.png", "xoxb-token") + + assert result is None + + +def test_download_file_sends_auth_header(monkeypatch): + """Test that download_file sends the correct authorization header.""" + mock_response = Mock() + mock_response.content = b"image-bytes" + mock_response.raise_for_status = Mock() + mock_get = Mock(return_value=mock_response) + + monkeypatch.setattr("apps.slack.utils.requests.get", mock_get) + + download_file("https://files.slack.com/image.png", "xoxb-test-token") + + mock_get.assert_called_once_with( + "https://files.slack.com/image.png", + headers={"Authorization": "Bearer xoxb-test-token"}, + timeout=30, + ) + + @pytest.mark.parametrize( ("input_content", "expected_output"), [ From a762c7093403cd3ef3aca8197eec33be878e1b65 Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Fri, 13 Feb 2026 00:52:59 +0530 Subject: [PATCH 04/12] add unrelated tests --- .../slack/common/question_detector_test.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/backend/tests/apps/slack/common/question_detector_test.py b/backend/tests/apps/slack/common/question_detector_test.py index 9ecc1b91aa..ecb8298734 100644 --- a/backend/tests/apps/slack/common/question_detector_test.py +++ b/backend/tests/apps/slack/common/question_detector_test.py @@ -24,13 +24,21 @@ def _mock_openai(self, monkeypatch): monkeypatch.setattr("openai.OpenAI", MagicMock(return_value=mock_client)) - # Mock the Retriever class - mock_retriever = MagicMock() - mock_retriever.retrieve.return_value = [] + # Mock the embedder + mock_embedder = MagicMock() + mock_embedder.embed_query.return_value = [0.0] * 1536 monkeypatch.setattr( - "apps.slack.common.question_detector.Retriever", MagicMock(return_value=mock_retriever) + "apps.slack.common.question_detector.get_embedder", + MagicMock(return_value=mock_embedder), ) + # Mock Chunk.objects for _retrieve_chunks + mock_chunk_qs = MagicMock() + mock_chunk_qs.annotate.return_value.order_by.return_value.__getitem__ = MagicMock( + return_value=[] + ) + monkeypatch.setattr("apps.slack.common.question_detector.Chunk.objects", mock_chunk_qs) + monkeypatch.setattr( "apps.slack.common.question_detector.Prompt.get_slack_question_detector_prompt", lambda: "System prompt with {context}", @@ -70,7 +78,7 @@ def test_init(self, detector): # Test that detector initializes properly assert detector is not None assert hasattr(detector, "openai_client") - assert hasattr(detector, "retriever") + assert hasattr(detector, "embedder") def test_is_owasp_question_true_cases(self, detector, sample_context_chunks, monkeypatch): """Test cases that should be detected as OWASP questions.""" @@ -178,7 +186,7 @@ def test_mocked_initialization(self): # Test that detector initializes properly assert detector is not None assert hasattr(detector, "openai_client") - assert hasattr(detector, "retriever") + assert hasattr(detector, "embedder") def test_class_constants(self, detector): """Test that class constants are properly defined.""" From b659b4bc6abec244bfc01dc959c051287c5f9493 Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Fri, 13 Feb 2026 01:00:16 +0530 Subject: [PATCH 05/12] cubic and coderabbit suggestions --- backend/apps/common/open_ai.py | 3 +-- backend/apps/slack/events/app_mention.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/apps/common/open_ai.py b/backend/apps/common/open_ai.py index d1bcbbe5db..7ad65b3aa7 100644 --- a/backend/apps/common/open_ai.py +++ b/backend/apps/common/open_ai.py @@ -7,7 +7,6 @@ import openai from django.conf import settings -from openai.types.chat import ChatCompletionContentPartParam if TYPE_CHECKING: from openai.types.chat import ChatCompletionContentPartParam @@ -100,7 +99,7 @@ def user_content(self) -> list[ChatCompletionContentPartParam]: """User message content. Returns: - list[dict]: User message content. + list[ChatCompletionContentPartParam]: User message content. """ content: list[ChatCompletionContentPartParam] = [{"type": "text", "text": self.input}] diff --git a/backend/apps/slack/events/app_mention.py b/backend/apps/slack/events/app_mention.py index b10c5a5cf8..c2c34dade9 100644 --- a/backend/apps/slack/events/app_mention.py +++ b/backend/apps/slack/events/app_mention.py @@ -35,7 +35,7 @@ def handle_event(self, event, client): images_raw = [ file for file in files - if file.get("mimetype") in ALLOWED_MIMETYPES and file.get("size") <= MAX_IMAGE_SIZE + if file.get("mimetype") in ALLOWED_MIMETYPES and file.get("size", 0) <= MAX_IMAGE_SIZE ] query = text From 275439232f31a5885c28b54a54b6e68f883fb738 Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Fri, 13 Feb 2026 01:15:24 +0530 Subject: [PATCH 06/12] cubic bot suggestions --- backend/apps/slack/events/app_mention.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/apps/slack/events/app_mention.py b/backend/apps/slack/events/app_mention.py index c2c34dade9..a57dc589a1 100644 --- a/backend/apps/slack/events/app_mention.py +++ b/backend/apps/slack/events/app_mention.py @@ -35,7 +35,8 @@ def handle_event(self, event, client): images_raw = [ file for file in files - if file.get("mimetype") in ALLOWED_MIMETYPES and file.get("size", 0) <= MAX_IMAGE_SIZE + if file.get("mimetype") in ALLOWED_MIMETYPES + and file.get("size", float("inf")) <= MAX_IMAGE_SIZE ] query = text From f0db6715e31206e67a4be687ae4cbdefdfb56a87 Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Fri, 13 Feb 2026 01:15:42 +0530 Subject: [PATCH 07/12] add unrelated tests --- .../tests/apps/slack/services/message_auto_reply_test.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/tests/apps/slack/services/message_auto_reply_test.py b/backend/tests/apps/slack/services/message_auto_reply_test.py index 898c52dba2..f5e73d9f9c 100644 --- a/backend/tests/apps/slack/services/message_auto_reply_test.py +++ b/backend/tests/apps/slack/services/message_auto_reply_test.py @@ -76,8 +76,13 @@ def test_generate_ai_reply_success( ts=mock_message.slack_message_id, limit=1, ) - mock_process_ai_query.assert_called_once_with(query=mock_message.text) - mock_get_blocks.assert_called_once_with("OWASP is a security organization...") + mock_process_ai_query.assert_called_once_with( + query=mock_message.text, channel_id=mock_message.conversation.slack_channel_id + ) + mock_get_blocks.assert_called_once_with( + "OWASP is a security organization...", + channel_id=mock_message.conversation.slack_channel_id, + ) mock_client.chat_postMessage.assert_called_once_with( channel=mock_message.conversation.slack_channel_id, blocks=[ From 2478fe1c21175f70d74d6485248bc9b3166b7103 Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Fri, 13 Feb 2026 01:32:04 +0530 Subject: [PATCH 08/12] refactor code --- backend/apps/ai/common/constants.py | 1 + backend/apps/ai/flows/assistant.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/apps/ai/common/constants.py b/backend/apps/ai/common/constants.py index 363cf47422..548d8b619f 100644 --- a/backend/apps/ai/common/constants.py +++ b/backend/apps/ai/common/constants.py @@ -5,6 +5,7 @@ DEFAULT_MAX_ITERATIONS = 3 DEFAULT_REASONING_MODEL = "gpt-4o" DEFAULT_SIMILARITY_THRESHOLD = 0.1 +DEFAULT_VISION_MODEL = "gpt-4o-mini" DELIMITER = "\n\n" GITHUB_REQUEST_INTERVAL_SECONDS = 0.5 MIN_REQUEST_INTERVAL_SECONDS = 1.2 diff --git a/backend/apps/ai/flows/assistant.py b/backend/apps/ai/flows/assistant.py index e96a319fdb..9e00aaa1a5 100644 --- a/backend/apps/ai/flows/assistant.py +++ b/backend/apps/ai/flows/assistant.py @@ -13,6 +13,7 @@ from apps.ai.agents.contribution import create_contribution_agent from apps.ai.agents.project import create_project_agent from apps.ai.agents.rag import create_rag_agent +from apps.ai.common.constants import DEFAULT_VISION_MODEL from apps.ai.common.intent import Intent from apps.ai.router import route from apps.common.open_ai import OpenAi @@ -79,7 +80,7 @@ def process_query( # noqa: PLR0911 try: if images: image_context = ( - OpenAi(model="gpt-4o") + OpenAi(model=DEFAULT_VISION_MODEL) .set_prompt(IMAGE_DESCRIPTION_PROMPT) .set_input(query) .set_images(images) @@ -92,6 +93,7 @@ def process_query( # noqa: PLR0911 router_result = route(query) intent = router_result["intent"] confidence = router_result["confidence"] + logger.info( "Query routed", extra={ From 8d51ec00ac2941c16651495f7b3f23a38b716996 Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Fri, 13 Feb 2026 01:36:09 +0530 Subject: [PATCH 09/12] use delimiter --- backend/apps/ai/flows/assistant.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/apps/ai/flows/assistant.py b/backend/apps/ai/flows/assistant.py index 9e00aaa1a5..011407a437 100644 --- a/backend/apps/ai/flows/assistant.py +++ b/backend/apps/ai/flows/assistant.py @@ -13,7 +13,7 @@ from apps.ai.agents.contribution import create_contribution_agent from apps.ai.agents.project import create_project_agent from apps.ai.agents.rag import create_rag_agent -from apps.ai.common.constants import DEFAULT_VISION_MODEL +from apps.ai.common.constants import DEFAULT_VISION_MODEL, DELIMITER from apps.ai.common.intent import Intent from apps.ai.router import route from apps.common.open_ai import OpenAi @@ -87,7 +87,7 @@ def process_query( # noqa: PLR0911 .complete() ) if image_context: - query = f"{query}\n\nImage context: {image_context}" + query = f"{query}{DELIMITER}Image context: {image_context}" # Step 1: Route to appropriate expert agent router_result = route(query) From 79ec9e8695c6ca91155ff5866db6d42e44e3b0ea Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Fri, 13 Feb 2026 01:41:27 +0530 Subject: [PATCH 10/12] fix tests --- backend/tests/apps/ai/flows/assistant_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/tests/apps/ai/flows/assistant_test.py b/backend/tests/apps/ai/flows/assistant_test.py index 34941c347a..6a8ff8bb6b 100644 --- a/backend/tests/apps/ai/flows/assistant_test.py +++ b/backend/tests/apps/ai/flows/assistant_test.py @@ -29,7 +29,6 @@ def test_process_query_with_images_enriches_query( process_query("What is this?", images=images) - mock_openai_cls.assert_called_once_with(model="gpt-4o") mock_openai_instance.set_images.assert_called_once_with(images) mock_openai_instance.complete.assert_called_once() From d2d29f3ee586a7e44f952e3f27b39c3f2ff3bd1e Mon Sep 17 00:00:00 2001 From: Rudransh Shrivastava Date: Fri, 13 Feb 2026 01:47:07 +0530 Subject: [PATCH 11/12] add None checks --- backend/apps/slack/utils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/apps/slack/utils.py b/backend/apps/slack/utils.py index 1bcbeade89..4c0d4e3840 100644 --- a/backend/apps/slack/utils.py +++ b/backend/apps/slack/utils.py @@ -30,6 +30,11 @@ def download_file(url: str, token: str) -> bytes | None: bytes or None: The downloaded file content, or None if download failed. """ + if not url: + return None + if not token: + return None + try: response = requests.get(url, headers={"Authorization": f"Bearer {token}"}, timeout=30) response.raise_for_status() From af1e7a5b1d7e3625df9c80aa9d54be96b6a36b7f Mon Sep 17 00:00:00 2001 From: Arkadii Yakovets Date: Fri, 13 Feb 2026 18:50:02 -0800 Subject: [PATCH 12/12] Update code --- backend/apps/common/open_ai.py | 1 + backend/apps/slack/events/app_mention.py | 4 ++-- backend/apps/slack/utils.py | 4 +--- backend/tests/apps/common/open_ai_test.py | 12 +++++++++--- backend/tests/apps/slack/common/handlers/ai_test.py | 2 +- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/backend/apps/common/open_ai.py b/backend/apps/common/open_ai.py index 7ad65b3aa7..045ef024d2 100644 --- a/backend/apps/common/open_ai.py +++ b/backend/apps/common/open_ai.py @@ -104,6 +104,7 @@ def user_content(self) -> list[ChatCompletionContentPartParam]: """ content: list[ChatCompletionContentPartParam] = [{"type": "text", "text": self.input}] content.extend({"type": "image_url", "image_url": {"url": uri}} for uri in self.images) + return content def complete(self) -> str | None: diff --git a/backend/apps/slack/events/app_mention.py b/backend/apps/slack/events/app_mention.py index a57dc589a1..37529f5df3 100644 --- a/backend/apps/slack/events/app_mention.py +++ b/backend/apps/slack/events/app_mention.py @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) -ALLOWED_MIMETYPES = ("image/jpeg", "image/png", "image/webp") +ALLOWED_MIMETYPES = {"image/jpeg", "image/png", "image/webp"} MAX_IMAGE_SIZE = 2 * 1024 * 1024 # 2 MB @@ -104,7 +104,7 @@ def handle_event(self, event, client): ) image_uris.append(image_uri) - # Get AI response and post it + # Get AI response and post it. reply_blocks = get_blocks( query=query, images=image_uris, channel_id=channel_id, is_app_mention=True ) diff --git a/backend/apps/slack/utils.py b/backend/apps/slack/utils.py index 4c0d4e3840..909622dd59 100644 --- a/backend/apps/slack/utils.py +++ b/backend/apps/slack/utils.py @@ -30,9 +30,7 @@ def download_file(url: str, token: str) -> bytes | None: bytes or None: The downloaded file content, or None if download failed. """ - if not url: - return None - if not token: + if not url or not token: return None try: diff --git a/backend/tests/apps/common/open_ai_test.py b/backend/tests/apps/common/open_ai_test.py index e02e875ded..a2fe77dd5f 100644 --- a/backend/tests/apps/common/open_ai_test.py +++ b/backend/tests/apps/common/open_ai_test.py @@ -92,10 +92,13 @@ def test_user_content_with_images(self, openai_instance): assert isinstance(content, list) assert len(content) == 2 - assert content[0] == {"type": "text", "text": "What is in this image?"} + assert content[0] == { + "text": "What is in this image?", + "type": "text", + } assert content[1] == { - "type": "image_url", "image_url": {"url": "data:image/png;base64,abc123"}, + "type": "image_url", } def test_user_content_with_multiple_images(self, openai_instance): @@ -112,6 +115,9 @@ def test_user_content_with_multiple_images(self, openai_instance): assert isinstance(content, list) assert len(content) == 3 - assert content[0] == {"type": "text", "text": "Describe these images"} + assert content[0] == { + "text": "Describe these images", + "type": "text", + } assert content[1]["type"] == "image_url" assert content[2]["type"] == "image_url" diff --git a/backend/tests/apps/slack/common/handlers/ai_test.py b/backend/tests/apps/slack/common/handlers/ai_test.py index 95e6175641..dbf0324c8e 100644 --- a/backend/tests/apps/slack/common/handlers/ai_test.py +++ b/backend/tests/apps/slack/common/handlers/ai_test.py @@ -79,8 +79,8 @@ def test_get_blocks_with_images(self, mock_markdown, mock_process_ai_query): images = ["data:image/png;base64,abc123"] ai_response = "The image shows a security dashboard." expected_block = { - "type": "section", "text": {"type": "mrkdwn", "text": ai_response}, + "type": "section", } mock_process_ai_query.return_value = ai_response