Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ logs/
.pytype/
.vscode
.cursor
.claude/
AGENTS.md
4 changes: 2 additions & 2 deletions listeners/events/entity_details_requested.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ def entity_details_requested_callback(event: dict, client: WebClient, logger: lo
json=payload,
)
except SlackResponseError as e:
logger.error(f"Failed to fetch or parse sample data. Error details: {str(e)}", exc_info=e)
logger.error(f"Failed to fetch or parse sample data. Error details: {e}", exc_info=e)
except Exception as e:
logger.error(
f"An unexpected error occurred handling entity_details_requested event: {type(e).__name__} - {str(e)}",
f"An unexpected error occurred handling entity_details_requested event: {type(e).__name__} - {e}",
exc_info=e,
)
52 changes: 13 additions & 39 deletions listeners/filters.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,16 @@
from dataclasses import dataclass
from enum import Enum
from typing import List, Optional


class FilterType(Enum):
MULTI_SELECT = "multi_select"
TOGGLE = "toggle"


@dataclass
class FilterOptions:
name: str
value: str


@dataclass
class Filter:
name: str
display_name: str
type: FilterType
display_name_plural: Optional[str] = None
options: Optional[List[FilterOptions]] = None


LANGUAGES_FILTER = Filter(
name="languages",
display_name="Language",
display_name_plural="Languages",
type=FilterType.MULTI_SELECT.value,
options=[
FilterOptions(name="Python", value="python"),
FilterOptions(name="Java", value="java"),
FilterOptions(name="JavaScript", value="javascript"),
FilterOptions(name="TypeScript", value="typescript"),
LANGUAGES_FILTER = {
"name": "languages",
"display_name": "Language",
"display_name_plural": "Languages",
"type": "multi_select",
"options": [
{"name": "Python", "value": "python"},
{"name": "Java", "value": "java"},
{"name": "JavaScript", "value": "javascript"},
{"name": "TypeScript", "value": "typescript"},
],
)

TEMPLATES_FILTER = Filter(name="template", display_name="Templates", type=FilterType.TOGGLE.value)
}

TEMPLATES_FILTER = {"name": "template", "display_name": "Templates", "type": "toggle"}

SAMPLES_FILTER = Filter(name="sample", display_name="Samples", type=FilterType.TOGGLE.value)
SAMPLES_FILTER = {"name": "sample", "display_name": "Samples", "type": "toggle"}
4 changes: 2 additions & 2 deletions listeners/functions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@


def register(app: App):
app.function("search", auto_acknowledge=False)(search_step_callback)
app.function("filters", auto_acknowledge=False)(filters_step_callback)
app.function("search", auto_acknowledge=False, ack_timeout=10)(search_step_callback)
app.function("filters", auto_acknowledge=False, ack_timeout=10)(filters_step_callback)
26 changes: 6 additions & 20 deletions listeners/functions/filters.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,24 @@
import logging
from dataclasses import asdict
from typing import Dict

from slack_bolt import Ack, Complete, Fail

from listeners.filters import LANGUAGES_FILTER, SAMPLES_FILTER, TEMPLATES_FILTER

FILTER_PROCESSING_ERROR_MSG = (
"We encountered an issue processing filter results. Please try again or contact the app owner if the problem persists."
)

def filter_none(items: Dict):
return {k: v for k, v in items if v is not None}


def filters_step_callback(ack: Ack, inputs: dict, fail: Fail, complete: Complete, logger: logging.Logger):
try:
user_context = inputs.get("user_context", {})
logger.debug(f"User {user_context.get('id')} executing filter request")

complete(
outputs={
"filters": [
asdict(LANGUAGES_FILTER, dict_factory=filter_none),
asdict(TEMPLATES_FILTER, dict_factory=filter_none),
asdict(SAMPLES_FILTER, dict_factory=filter_none),
]
}
)
complete(outputs={"filters": [LANGUAGES_FILTER, TEMPLATES_FILTER, SAMPLES_FILTER]})
except Exception as e:
logger.error(
f"Unexpected error occurred while processing filter request: {type(e).__name__} - {str(e)}",
f"Unexpected error occurred while processing filter request: {type(e).__name__} - {e}",
exc_info=e,
)
fail(error=FILTER_PROCESSING_ERROR_MSG)
fail(
error="We encountered an issue processing filter results. "
"Please try again or contact the app owner if the problem persists."
)
finally:
ack()
49 changes: 8 additions & 41 deletions listeners/functions/search.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,10 @@
import logging
from typing import List, NotRequired, Optional, TypedDict

from slack_bolt import Ack, Complete, Fail
from slack_sdk import WebClient

from listeners.sample_data_service import SlackResponseError, fetch_sample_data

SEARCH_PROCESSING_ERROR_MSG = (
"We encountered an issue processing your search results. "
"Please try again or contact the app owner if the problem persists."
)


class EntityReference(TypedDict):
id: str
type: Optional[str]


class SearchResult(TypedDict):
title: str
description: str
link: str
date_updated: str
external_ref: EntityReference
content: NotRequired[str]


def search_step_callback(
ack: Ack,
Expand All @@ -42,27 +22,14 @@ def search_step_callback(

samples = response.get("samples", [])

results: List[SearchResult] = [
{
"title": sample["title"],
"description": sample["description"],
"link": sample["link"],
"date_updated": sample["date_updated"],
"external_ref": sample["external_ref"],
**({"content": sample["content"]} if "content" in sample else {}),
}
for sample in samples
]

complete(outputs={"search_results": results})
complete(outputs={"search_results": samples})
except SlackResponseError as e:
logger.error(f"Failed to fetch or parse sample data. Error details: {e}", exc_info=e)
fail(
error="We encountered an issue processing your search results. "
"Please try again or contact the app owner if the problem persists."
)
except Exception as e:
if isinstance(e, SlackResponseError):
logger.error(f"Failed to fetch or parse sample data. Error details: {str(e)}", exc_info=e)
fail(error=SEARCH_PROCESSING_ERROR_MSG)
else:
logger.error(
f"Unexpected error occurred while processing search request: {type(e).__name__} - {str(e)}",
exc_info=e,
)
logger.error(f"Unexpected error processing search request: {type(e).__name__} - {e}", exc_info=e)
finally:
ack()
16 changes: 7 additions & 9 deletions listeners/sample_data_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@


class SlackResponseError(Exception):
def __init__(self, message: str):
super().__init__(message)
self.name = "SlackResponseError"
pass


def fetch_sample_data(client: WebClient, query: str = None, filters: dict = None, logger: logging.Logger = None):
Expand All @@ -19,18 +17,18 @@ def fetch_sample_data(client: WebClient, query: str = None, filters: dict = None
if filters:
selected_filters = {}

languages = filters.get(LANGUAGES_FILTER.name, [])
templates = filters.get(TEMPLATES_FILTER.name, False)
samples = filters.get(SAMPLES_FILTER.name, False)
languages = filters.get(LANGUAGES_FILTER["name"], [])
templates = filters.get(TEMPLATES_FILTER["name"], False)
samples = filters.get(SAMPLES_FILTER["name"], False)

if languages:
selected_filters[LANGUAGES_FILTER.name] = languages
selected_filters[LANGUAGES_FILTER["name"]] = languages

if templates ^ samples:
if templates:
selected_filters["type"] = TEMPLATES_FILTER.name
selected_filters["type"] = TEMPLATES_FILTER["name"]
elif samples:
selected_filters["type"] = SAMPLES_FILTER.name
selected_filters["type"] = SAMPLES_FILTER["name"]

if selected_filters:
params["filters"] = selected_filters
Expand Down
5 changes: 1 addition & 4 deletions tests/listeners/functions/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from slack_bolt import Ack, Complete, Fail

from listeners.functions.filters import FILTER_PROCESSING_ERROR_MSG, filters_step_callback
from listeners.functions.filters import filters_step_callback


class TestFilters:
Expand Down Expand Up @@ -84,7 +84,4 @@ def test_filters_step_callback_unexpected_exception(self):
)

self.mock_fail.assert_called_once()
call_args = self.mock_fail.call_args
assert call_args.kwargs["error"] == FILTER_PROCESSING_ERROR_MSG

self.mock_ack.assert_called_once()
13 changes: 7 additions & 6 deletions tests/listeners/functions/test_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from slack_sdk import WebClient

from listeners.filters import LANGUAGES_FILTER, SAMPLES_FILTER, TEMPLATES_FILTER
from listeners.functions.search import SEARCH_PROCESSING_ERROR_MSG, search_step_callback
from listeners.functions.search import search_step_callback
from listeners.sample_data_service import SlackResponseError


Expand Down Expand Up @@ -41,7 +41,7 @@ def setup_method(self):
def test_search_step_callback_success(self, mock_fetch_sample_data):
mock_fetch_sample_data.return_value = self.mock_sample_data

filters = {LANGUAGES_FILTER.name: ["python"]}
filters = {LANGUAGES_FILTER["name"]: ["python"]}

inputs = {"query": "test query", "filters": filters}

Expand Down Expand Up @@ -74,7 +74,11 @@ def test_search_step_callback_success(self, mock_fetch_sample_data):
def test_search_step_callback_multiple_filter_types(self, mock_fetch_sample_data):
mock_fetch_sample_data.return_value = self.mock_sample_data

filters = {TEMPLATES_FILTER.name: True, SAMPLES_FILTER.name: True, LANGUAGES_FILTER.name: ["python", "javascript"]}
filters = {
TEMPLATES_FILTER["name"]: True,
SAMPLES_FILTER["name"]: True,
LANGUAGES_FILTER["name"]: ["python", "javascript"],
}

inputs = {"query": "test query", "filters": filters}

Expand Down Expand Up @@ -135,9 +139,6 @@ def test_search_step_callback_slack_response_error(self, mock_fetch_sample_data)
)

self.mock_fail.assert_called_once()
call_args = self.mock_fail.call_args
assert call_args.kwargs["error"] == SEARCH_PROCESSING_ERROR_MSG

self.mock_complete.assert_not_called()
self.mock_ack.assert_called_once()

Expand Down
19 changes: 11 additions & 8 deletions tests/listeners/test_sample_data_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def test_fetch_sample_data_with_query(self):
assert result == self.mock_response

def test_fetch_sample_data_with_languages_filter(self):
filters = {LANGUAGES_FILTER.name: ["python", "javascript"]}
filters = {LANGUAGES_FILTER["name"]: ["python", "javascript"]}

result = fetch_sample_data(client=self.mock_client, query="test query", filters=filters, logger=self.mock_logger)

Expand All @@ -57,41 +57,44 @@ def test_fetch_sample_data_with_languages_filter(self):
assert result == self.mock_response

def test_fetch_sample_data_with_templates_filter(self):
filters = {TEMPLATES_FILTER.name: True}
filters = {TEMPLATES_FILTER["name"]: True}

result = fetch_sample_data(client=self.mock_client, query="test query", filters=filters, logger=self.mock_logger)

self.mock_client.api_call.assert_called_once_with(
API_METHOD, params={"query": "test query", "filters": {"type": TEMPLATES_FILTER.name}}
API_METHOD, params={"query": "test query", "filters": {"type": TEMPLATES_FILTER["name"]}}
)

assert result == self.mock_response

def test_fetch_sample_data_with_samples_filter(self):
filters = {SAMPLES_FILTER.name: True}
filters = {SAMPLES_FILTER["name"]: True}

result = fetch_sample_data(client=self.mock_client, query="test query", filters=filters, logger=self.mock_logger)

self.mock_client.api_call.assert_called_once_with(
API_METHOD, params={"query": "test query", "filters": {"type": SAMPLES_FILTER.name}}
API_METHOD, params={"query": "test query", "filters": {"type": SAMPLES_FILTER["name"]}}
)

assert result == self.mock_response

def test_fetch_sample_data_with_combined_filters(self):
filters = {LANGUAGES_FILTER.name: ["python"], TEMPLATES_FILTER.name: True}
filters = {LANGUAGES_FILTER["name"]: ["python"], TEMPLATES_FILTER["name"]: True}

result = fetch_sample_data(client=self.mock_client, query="test query", filters=filters, logger=self.mock_logger)

self.mock_client.api_call.assert_called_once_with(
API_METHOD,
params={"query": "test query", "filters": {LANGUAGES_FILTER.name: ["python"], "type": TEMPLATES_FILTER.name}},
params={
"query": "test query",
"filters": {LANGUAGES_FILTER["name"]: ["python"], "type": TEMPLATES_FILTER["name"]},
},
)

assert result == self.mock_response

def test_fetch_sample_data_with_both_template_and_sample(self):
filters = {TEMPLATES_FILTER.name: True, SAMPLES_FILTER.name: True}
filters = {TEMPLATES_FILTER["name"]: True, SAMPLES_FILTER["name"]: True}

result = fetch_sample_data(client=self.mock_client, query="test query", filters=filters, logger=self.mock_logger)

Expand Down