Skip to content

Commit 04c250e

Browse files
committed
Merge branch 'pull_request_conversation_1' into pull_request_conversation_2
2 parents d73e6d5 + 4b452cd commit 04c250e

File tree

13 files changed

+710
-441
lines changed

13 files changed

+710
-441
lines changed

packages/slackBotFunction/app/core/config.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from typing import Tuple
1212
import boto3
1313
from aws_lambda_powertools import Logger
14+
from aws_lambda_powertools.logging import utils
1415
from aws_lambda_powertools.utilities.parameters import get_parameter
1516
from mypy_boto3_dynamodb.service_resource import Table
1617

@@ -19,7 +20,9 @@
1920

2021
@lru_cache()
2122
def get_logger() -> Logger:
22-
return Logger(service="slackBotFunction")
23+
powertools_logger = Logger(service="slackBotFunction")
24+
utils.copy_config_to_registered_loggers(source_logger=powertools_logger, ignore_log_level=True)
25+
return powertools_logger
2326

2427

2528
# set up logger as its used in other functions

packages/slackBotFunction/app/handler.py

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,9 @@
1111

1212
from app.core.config import get_logger
1313
from app.services.app import get_app
14-
from app.slack.slack_events import process_async_slack_event, process_pull_request_slack_event
14+
from app.slack.slack_events import process_pull_request_slack_event
1515

1616
logger = get_logger()
17-
app = get_app()
1817

1918

2019
@logger.inject_lambda_context(log_event=True, clear_state=True)
@@ -27,18 +26,8 @@ def handler(event: dict, context: LambdaContext) -> dict:
2726
2. Lambda acknowledges immediately and triggers async self-invocation
2827
3. Async invocation processes Bedrock query and responds to Slack
2928
"""
30-
31-
# handle async processing requests
32-
if event.get("async_processing"):
33-
slack_event_data = event.get("slack_event")
34-
if not slack_event_data:
35-
logger.error("Async processing requested but no slack_event provided")
36-
return {"statusCode": 400}
37-
38-
process_async_slack_event(slack_event_data=slack_event_data)
39-
return {"statusCode": 200}
40-
41-
# handle async processing requests
29+
app = get_app(logger=logger)
30+
# handle pull request processing requests
4231
if event.get("pull_request_processing"):
4332
slack_event_data = event.get("slack_event")
4433
if not slack_event_data:
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
from functools import lru_cache
2+
import logging
23

34
from slack_bolt import App
45
from app.core.config import get_ssm_params
5-
from app.core.config import get_logger
66
from app.slack.slack_handlers import setup_handlers
7+
from aws_lambda_powertools import Logger
78

8-
logger = get_logger()
99

10-
11-
@lru_cache()
12-
def get_app() -> App:
10+
@lru_cache
11+
def get_app(logger: Logger) -> App:
1312
bot_token, signing_secret = get_ssm_params()
13+
powertools_logger = logging.getLogger(name=logger.name)
1414

1515
# initialise the Slack app
1616
app = App(
1717
process_before_response=True,
1818
token=bot_token,
1919
signing_secret=signing_secret,
20-
logger=logger,
20+
logger=powertools_logger,
2121
)
2222
setup_handlers(app)
2323
return app

packages/slackBotFunction/app/slack/slack_events.py

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import time
88
import traceback
99
import json
10-
from typing import Any, Dict, Tuple
10+
from typing import Any, Dict
1111
from botocore.exceptions import ClientError
1212
from slack_sdk import WebClient
1313
from app.core.config import (
@@ -25,7 +25,7 @@
2525
)
2626
from app.services.query_reformulator import reformulate_query
2727
from app.services.slack import get_friendly_channel_name, post_error_message
28-
from app.utils.handler_utils import extract_pull_request_id, is_duplicate_event
28+
from app.utils.handler_utils import extract_conversation_context, extract_pull_request_id, is_duplicate_event
2929

3030
logger = get_logger()
3131

@@ -103,17 +103,6 @@ def _mark_qa_feedback_received(conversation_key: str, message_ts: str) -> None:
103103
# ================================================================
104104

105105

106-
def _extract_conversation_context(event: Dict[str, Any]) -> Tuple[str, str, str | None]:
107-
"""Extract conversation key and thread context from event"""
108-
channel = event["channel"]
109-
# Determine conversation context: DM vs channel thread
110-
if event.get("channel_type") == constants.CHANNEL_TYPE_IM:
111-
return f"{constants.DM_PREFIX}{channel}", constants.CONTEXT_TYPE_DM, None # DMs don't use threads
112-
else:
113-
thread_root = event.get("thread_ts", event["ts"])
114-
return f"{constants.THREAD_PREFIX}{channel}#{thread_root}", constants.CONTEXT_TYPE_THREAD, thread_root
115-
116-
117106
def _handle_session_management(
118107
conversation_key: str,
119108
session_data: Dict[str, Any],
@@ -182,23 +171,21 @@ def _create_feedback_blocks(
182171
# ================================================================
183172

184173

185-
def process_async_slack_event(slack_event_data: Dict[str, Any]) -> None:
174+
def process_async_slack_event(event: Dict[str, Any], event_id: str) -> None:
186175
"""
187176
Process Slack events asynchronously after initial acknowledgment
188177
189178
This function handles the actual AI processing that takes longer than Slack's
190179
3-second timeout. It extracts the user query, calls Bedrock, and posts the response.
191180
"""
192-
event = slack_event_data["event"]
193-
event_id = slack_event_data["event_id"]
194181
token = get_bot_token()
195182

196183
client = WebClient(token=token)
197184

198185
try:
199186
user_id = event["user"]
200187
channel = event["channel"]
201-
conversation_key, context_type, thread_ts = _extract_conversation_context(event)
188+
conversation_key, context_type, thread_ts = extract_conversation_context(event)
202189

203190
# Remove Slack user mentions from message text
204191
user_query = re.sub(r"<@[UW][A-Z0-9]+(\|[^>]+)?>", "", event["text"]).strip()

packages/slackBotFunction/app/slack/slack_handlers.py

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@
2323
from app.services.slack import post_error_message
2424
from app.utils.handler_utils import (
2525
conversation_key_and_root,
26+
extract_conversation_context,
2627
extract_pull_request_id,
2728
gate_common,
2829
is_latest_message,
2930
strip_mentions,
30-
trigger_async_processing,
3131
respond_with_eyes,
3232
trigger_pull_request_processing,
3333
)
34-
from app.slack.slack_events import _extract_conversation_context, store_feedback
34+
from app.slack.slack_events import process_async_slack_event, store_feedback
3535

3636
logger = get_logger()
3737

@@ -43,26 +43,29 @@
4343
@lru_cache
4444
def setup_handlers(app: App) -> None:
4545
"""Register handlers. Intentionally minimal—no branching here."""
46-
app.event("app_mention")(mention_handler)
47-
app.event("message")(unified_message_handler)
48-
app.action("feedback_yes")(feedback_handler)
49-
app.action("feedback_no")(feedback_handler)
46+
app.event("app_mention")(ack=respond_to_slack_within_3_seconds, lazy=[mention_handler])
47+
app.event("message")(ack=respond_to_slack_within_3_seconds, lazy=[unified_message_handler])
48+
app.action("feedback_yes")(ack=respond_to_slack_within_3_seconds, lazy=[feedback_handler])
49+
app.action("feedback_no")(ack=respond_to_slack_within_3_seconds, lazy=[feedback_handler])
5050

5151

5252
# ================================================================
5353
# Event and message handlers
5454
# ================================================================
5555

5656

57-
def mention_handler(event: Dict[str, Any], ack: Ack, body: Dict[str, Any], client: WebClient) -> None:
57+
def respond_to_slack_within_3_seconds(event: Dict[str, Any], ack: Ack):
58+
respond_with_eyes(event=event)
59+
logger.debug("Sending ack response")
60+
ack()
61+
62+
63+
def mention_handler(event: Dict[str, Any], body: Dict[str, Any], client: WebClient) -> None:
5864
"""
5965
Channel interactions that mention the bot.
6066
- If text after the mention starts with 'feedback:', store it as additional feedback.
6167
- Otherwise, forward to the async processing pipeline (Q&A).
6268
"""
63-
logger.debug("Sending ack response in mention_handler")
64-
ack()
65-
respond_with_eyes(event=event)
6669
event_id = gate_common(event=event, body=body)
6770
if not event_id:
6871
return
@@ -133,6 +136,8 @@ def thread_message_handler(event: Dict[str, Any], event_id: str, client: WebClie
133136
logger.info(f"Found session for thread: {conversation_key}")
134137
except Exception as e:
135138
logger.error(f"Error checking thread session: {e}", extra={"error": traceback.format_exc()})
139+
_, _, thread_ts = extract_conversation_context(event)
140+
post_error_message(channel=channel_id, thread_ts=thread_ts)
136141
return
137142

138143
logger.info(f"Processing thread message from user {user_id}", extra={"event_id": event_id})
@@ -147,11 +152,8 @@ def thread_message_handler(event: Dict[str, Any], event_id: str, client: WebClie
147152
)
148153

149154

150-
def unified_message_handler(event: Dict[str, Any], ack: Ack, body: Dict[str, Any], client: WebClient) -> None:
155+
def unified_message_handler(event: Dict[str, Any], body: Dict[str, Any], client: WebClient) -> None:
151156
"""Handle all message events - DMs and channel messages"""
152-
logger.debug("Sending ack response")
153-
ack()
154-
respond_with_eyes(event=event)
155157
event_id = gate_common(event=event, body=body)
156158
if not event_id:
157159
return
@@ -165,11 +167,10 @@ def unified_message_handler(event: Dict[str, Any], ack: Ack, body: Dict[str, Any
165167
thread_message_handler(event=event, event_id=event_id, client=client, body=body)
166168

167169

168-
def feedback_handler(ack: Ack, body: Dict[str, Any], client: WebClient) -> None:
170+
def feedback_handler(body: Dict[str, Any], client: WebClient, event: Dict[str, Any]) -> None:
169171
"""Handle feedback button clicks (both positive and negative)."""
170-
logger.debug("Sending ack response")
171-
ack()
172172
try:
173+
channel_id = event["channel"]
173174
action_id = body["actions"][0]["action_id"]
174175
feedback_data = json.loads(body["actions"][0]["value"])
175176

@@ -215,8 +216,12 @@ def feedback_handler(ack: Ack, body: Dict[str, Any], client: WebClient) -> None:
215216
logger.error(f"Feedback storage error: {e}", extra={"error": traceback.format_exc()})
216217
except Exception as e:
217218
logger.error(f"Unexpected feedback error: {e}", extra={"error": traceback.format_exc()})
219+
thread_ts = feedback_data.get("tt")
220+
post_error_message(channel=channel_id, thread_ts=thread_ts)
218221
except Exception as e:
219222
logger.error(f"Error handling feedback: {e}", extra={"error": traceback.format_exc()})
223+
_, _, thread_ts = extract_conversation_context(event)
224+
post_error_message(channel=channel_id, thread_ts=thread_ts)
220225

221226

222227
# ================================================================
@@ -262,6 +267,8 @@ def _common_message_handler(
262267
client.chat_postMessage(**params)
263268
except Exception as e:
264269
logger.error(f"Failed to post channel feedback ack: {e}", extra={"error": traceback.format_exc()})
270+
_, _, thread_ts = extract_conversation_context(event)
271+
post_error_message(channel=channel_id, thread_ts=thread_ts)
265272
return
266273

267274
if message_text.lower().startswith(constants.PULL_REQUEST_PREFIX):
@@ -270,13 +277,12 @@ def _common_message_handler(
270277
trigger_pull_request_processing(pull_request_id=pull_request_id, event=event, event_id=event_id)
271278
except Exception as e:
272279
logger.error(f"Can not find pull request details: {e}", extra={"error": traceback.format_exc()})
273-
_, _, thread_ts = _extract_conversation_context(event)
280+
_, _, thread_ts = extract_conversation_context(event)
274281
post_error_message(channel=channel_id, thread_ts=thread_ts)
275282
return
276283

284+
# note - we dont do post an error message if this fails as its handled by process_async_slack_event
277285
try:
278-
trigger_async_processing(event=event, event_id=event_id)
286+
process_async_slack_event(event=event, event_id=event_id)
279287
except Exception:
280288
logger.error("Error triggering async processing", extra={"error": traceback.format_exc()})
281-
_, _, thread_ts = _extract_conversation_context(event)
282-
post_error_message(channel=channel_id, thread_ts=thread_ts)

packages/slackBotFunction/app/utils/handler_utils.py

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import boto3
1111
from botocore.exceptions import ClientError
1212
from slack_sdk import WebClient
13-
import os
1413
from mypy_boto3_cloudformation.client import CloudFormationClient
1514
from mypy_boto3_lambda.client import LambdaClient
1615

@@ -45,30 +44,6 @@ def is_duplicate_event(event_id: str) -> bool:
4544
return False
4645

4746

48-
def trigger_async_processing(event: Dict[str, Any], event_id: str) -> None:
49-
"""
50-
Trigger asynchronous Lambda invocation to process Slack events
51-
52-
Slack requires responses within 3 seconds, but Bedrock queries can take longer.
53-
This function invokes the same Lambda function asynchronously to handle the
54-
actual AI processing without blocking the initial Slack response.
55-
"""
56-
# incase we fail to re-invoke the lambda we should log an error
57-
lambda_client: LambdaClient = boto3.client("lambda")
58-
try:
59-
logger.debug("Triggering async lambda processing")
60-
lambda_payload = {"async_processing": True, "slack_event": {"event": event, "event_id": event_id}}
61-
lambda_client.invoke(
62-
FunctionName=os.environ["AWS_LAMBDA_FUNCTION_NAME"],
63-
InvocationType="Event",
64-
Payload=json.dumps(lambda_payload),
65-
)
66-
logger.debug("Async processing triggered successfully")
67-
except Exception as e:
68-
logger.error("Failed to trigger async processing", extra={"error": traceback.format_exc()})
69-
raise e
70-
71-
7247
def respond_with_eyes(event: Dict[str, Any]) -> None:
7348
bot_token = get_bot_token()
7449
client = WebClient(token=bot_token)
@@ -172,3 +147,14 @@ def conversation_key_and_root(event: Dict[str, Any]) -> Tuple[str, str]:
172147
if event.get("channel_type") == constants.CHANNEL_TYPE_IM:
173148
return f"{constants.DM_PREFIX}{channel_id}", root
174149
return f"{constants.THREAD_PREFIX}{channel_id}#{root}", root
150+
151+
152+
def extract_conversation_context(event: Dict[str, Any]) -> Tuple[str, str, str | None]:
153+
"""Extract conversation key and thread context from event"""
154+
channel = event["channel"]
155+
# Determine conversation context: DM vs channel thread
156+
if event.get("channel_type") == constants.CHANNEL_TYPE_IM:
157+
return f"{constants.DM_PREFIX}{channel}", constants.CONTEXT_TYPE_DM, None # DMs don't use threads
158+
else:
159+
thread_root = event.get("thread_ts", event["ts"])
160+
return f"{constants.THREAD_PREFIX}{channel}#{thread_root}", constants.CONTEXT_TYPE_THREAD, thread_root

packages/slackBotFunction/tests/conftest.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ def mock_env():
2121
"AWS_REGION": "eu-west-2",
2222
"GUARD_RAIL_ID": "test-guard-id",
2323
"GUARD_RAIL_VERSION": "1",
24-
"AWS_LAMBDA_FUNCTION_NAME": "test-function",
2524
"QUERY_REFORMULATION_MODEL_ID": "test-model",
2625
"QUERY_REFORMULATION_PROMPT_NAME": "test-prompt",
2726
"QUERY_REFORMULATION_PROMPT_VERSION": "DRAFT",

0 commit comments

Comments
 (0)