Skip to content

Commit 2c5b039

Browse files
committed
Add WebClient and SlackApiError classes to slackBotFunction
1 parent 6620867 commit 2c5b039

File tree

2 files changed

+75
-68
lines changed

2 files changed

+75
-68
lines changed

packages/slackBotFunction/app.py

Lines changed: 73 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,151 +1,148 @@
1-
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2-
# SPDX-License-Identifier: MIT-0.
3-
4-
"""
5-
AWS Lambda hosted Slack ChatBot integration to Amazon Bedrock Knowledge Base.
6-
Expects Slack Bot Slash Command given by the SLACK_SLASH_COMMAND param and presents
7-
a user query to the Bedrock Knowledge Base described by the KNOWLEDGEBASE_ID parameter.
8-
9-
The user query is used in a Bedrock KB ReteriveandGenerate API call and the KB
10-
response is presented to the user in Slack.
11-
12-
Slack integration based on SlackBolt library and examples given at:
13-
https://github.com/slackapi/bolt-python/blob/main/examples/aws_lambda/lazy_aws_lambda.py
14-
"""
15-
16-
__version__ = "0.0.1"
17-
__status__ = "Development"
18-
__copyright__ = "Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved."
19-
__author__ = "Dean Colcott <https://www.linkedin.com/in/deancolcott/>"
20-
211
import os
222
import json
233
import boto3
244
import logging
255
from slack_bolt import App
266
from slack_bolt.adapter.aws_lambda import SlackRequestHandler
7+
from slack_sdk import WebClient
8+
from slack_sdk.errors import SlackApiError
279

2810

29-
# Get params from SSM
11+
# ================== Helper: AWS SSM Parameter Store ==================
3012
def get_parameter(parameter_name):
13+
"""
14+
Retrieve a parameter value from AWS SSM Parameter Store.
15+
If the value is a JSON string, return the first value.
16+
Returns raw string otherwise.
17+
Raises on any error.
18+
"""
3119
ssm = boto3.client("ssm")
3220
try:
3321
response = ssm.get_parameter(Name=parameter_name, WithDecryption=True)
34-
# Parse the JSON string from the parameter
3522
parameter_value = response["Parameter"]["Value"]
3623

37-
# Remove the JSON structure and extract just the value
3824
try:
25+
# Attempt to parse value as JSON and return the first item.
3926
json_value = json.loads(parameter_value)
40-
# Get the first value from the dictionary
4127
value = next(iter(json_value.values()))
4228
return value
4329
except (json.JSONDecodeError, StopIteration):
44-
# If parsing fails or dictionary is empty, return the raw value
30+
# Return as string if not valid JSON.
4531
return parameter_value
4632

4733
except Exception as e:
48-
print(f"Error getting parameter {parameter_name}: {str(e)}")
49-
raise e
34+
logging.error(f"Error getting parameter {parameter_name}: {str(e)}")
35+
raise
5036

5137

52-
# Get parameter names from environment variables
38+
# ================== Load Sensitive Configuration ==================
39+
# Retrieve Slack bot token and signing secret from environment/SSM.
5340
bot_token_parameter = os.environ["SLACK_BOT_TOKEN_PARAMETER"]
5441
signing_secret_parameter = os.environ["SLACK_SIGNING_SECRET_PARAMETER"]
5542

56-
# Retrieve the parameters from SSM Parameter Store
5743
bot_token = get_parameter(bot_token_parameter)
5844
signing_secret = get_parameter(signing_secret_parameter)
5945

60-
# Initialize Slack app
46+
47+
# ================== Initialize Slack Bolt App ==================
48+
# Set process_before_response=True to ensure middleware (like logging) runs before Slack gets a response.
6149
app = App(
6250
process_before_response=True,
6351
token=bot_token,
6452
signing_secret=signing_secret,
6553
)
6654

67-
# Get the expected slack and AWS account params to local vars.
55+
56+
# ================== Load Other Config from Environment ==================
6857
SLACK_SLASH_COMMAND = os.environ["SLACK_SLASH_COMMAND"]
6958
KNOWLEDGEBASE_ID = os.environ["KNOWLEDGEBASE_ID"]
7059
RAG_MODEL_ID = os.environ["RAG_MODEL_ID"]
7160
AWS_REGION = os.environ["AWS_REGION"]
7261
GUARD_RAIL_ID = os.environ["GUARD_RAIL_ID"]
7362
GUARD_VERSION = os.environ["GUARD_RAIL_VERSION"]
7463

75-
print(f"GR_ID,{GUARD_RAIL_ID}")
76-
print(f"GR_V, {GUARD_VERSION}")
64+
logging.info(f"GUARD_RAIL_ID: {GUARD_RAIL_ID}")
65+
logging.info(f"GUARD_VERSION: {GUARD_VERSION}")
7766

7867

68+
# ================== Middleware: Log All Incoming Slack Requests ==================
7969
@app.middleware
8070
def log_request(logger, body, next):
8171
"""
82-
SlackBolt library logging.
72+
Log the entire incoming Slack event body for debugging/audit purposes.
8373
"""
8474
logger.debug(body)
8575
return next()
8676

8777

78+
# ================== Immediate Ack Handler for Slash Command ==================
8879
def respond_to_slack_within_3_seconds(body, ack):
8980
"""
90-
Slack Bot Slash Command requires an Ack response within 3 seconds or it
91-
messages an operation timeout error to the user in the chat thread.
92-
93-
The SlackBolt library provides a Async Ack function then re-invokes this Lambda
94-
to LazyLoad the process_command_request command that calls the Bedrock KB ReteriveandGenerate API.
95-
96-
This function is called initially to acknowledge the Slack Slash command within 3 secs.
81+
Immediately acknowledge the incoming slash command to Slack
82+
(must respond in <3 seconds or Slack will show a timeout error).
83+
Main processing happens asynchronously in the lazy handler.
9784
"""
9885
try:
9986
user_query = body["text"]
10087
logging.info(
101-
f"${SLACK_SLASH_COMMAND} - Acknowledging command: {SLACK_SLASH_COMMAND} - User Query: {user_query}\n"
88+
f"Acknowledging slash command {SLACK_SLASH_COMMAND} - User Query: {user_query}"
10289
)
103-
ack(f"\n${SLACK_SLASH_COMMAND} - Processing Request: {user_query}")
104-
90+
ack(f"\nProcessing Request: {user_query}")
10591
except Exception as err:
106-
print(f"${SLACK_SLASH_COMMAND} - Error: {err}")
107-
respond(
108-
f"${SLACK_SLASH_COMMAND} - Sorry an error occurred. Please try again later. Error: {err}"
109-
)
92+
logging.error(f"Ack handler error: {err}")
11093

11194

95+
# ================== Main Business Logic for Slash Command ==================
11296
def process_command_request(respond, body):
11397
"""
114-
Receive the Slack Slash Command user query and proxy the query to Bedrock Knowledge base ReteriveandGenerate API
115-
and return the response to Slack to be presented in the users chat thread.
98+
Handle user slash command asynchronously (runs after immediate ack).
99+
- Calls Bedrock Knowledge Base with the user's question.
100+
- Posts answer as a reply in thread (if present) or as a standalone message.
116101
"""
117102
try:
118-
# Get the user query
119103
user_query = body["text"]
104+
channel_id = body["channel_id"]
105+
user_id = body["user_id"]
106+
# Use thread_ts for thread replies, or fallback to message_ts
107+
thread_ts = body.get("thread_ts") or body.get("message_ts")
108+
120109
logging.info(
121-
f"${SLACK_SLASH_COMMAND} - Responding to command: {SLACK_SLASH_COMMAND} - User Query: {user_query}"
110+
f"Processing command: {SLACK_SLASH_COMMAND} - User Query: {user_query}"
122111
)
123112

124113
kb_response = get_bedrock_knowledgebase_response(user_query)
125114
response_text = kb_response["output"]["text"]
126-
respond(f"\n${SLACK_SLASH_COMMAND} - Response: {response_text}\n")
127115

116+
client = WebClient(token=bot_token)
117+
118+
# Prepare payload: reply in thread if thread_ts is provided.
119+
message_payload = {
120+
"channel": channel_id,
121+
"text": f"*Question from <@{user_id}>:*\n{user_query}\n\n*Answer:*\n{response_text}"
122+
}
123+
if thread_ts:
124+
message_payload["thread_ts"] = thread_ts
125+
126+
client.chat_postMessage(**message_payload)
127+
128+
except SlackApiError as e:
129+
logging.error(f"Slack API error posting message: {e.response['error']}")
128130
except Exception as err:
129-
print(f"${SLACK_SLASH_COMMAND} - Error: {err}")
130-
respond(
131-
f"${SLACK_SLASH_COMMAND} - Sorry an error occurred. Please try again later. Error: {err}"
132-
)
131+
logging.error(f"Handler error: {err}")
133132

134133

134+
# ================== Bedrock Knowledge Base Interaction ==================
135135
def get_bedrock_knowledgebase_response(user_query):
136136
"""
137-
Get and return the Bedrock Knowledge Base ReteriveAndGenerate response.
138-
Do all init tasks here instead of globally as initial invocation of this lambda
139-
provides Slack required ack in 3 sec. It doesn't trigger any bedrock functions and is
140-
time sensitive.
137+
Query the AWS Bedrock Knowledge Base RetrieveAndGenerate API using the user's question.
138+
Loads all Bedrock client config inside the function (avoids Lambda cold start delays in ack handler).
139+
Requires the following global variables: AWS_REGION, GUARD_RAIL_ID, GUARD_VERSION, KNOWLEDGEBASE_ID, RAG_MODEL_ID.
141140
"""
142-
# Initialise the bedrock-runtime client (in default / running region).
143141
client = boto3.client(
144142
service_name="bedrock-agent-runtime",
145143
region_name=AWS_REGION,
146144
)
147145

148-
# Create the RetrieveAndGenerateCommand input with the user query.
149146
input = {
150147
"text": user_query,
151148
}
@@ -171,19 +168,28 @@ def get_bedrock_knowledgebase_response(user_query):
171168
return response
172169

173170

174-
# Init the Slack Slash '/' command handler.
171+
# ================== Slash Command Registration ==================
172+
# Register the Slack slash command handler with Bolt:
173+
# - ack handler responds immediately (must be <3s)
174+
# - lazy handler processes the command asynchronously
175175
app.command(SLACK_SLASH_COMMAND)(
176176
ack=respond_to_slack_within_3_seconds,
177177
lazy=[process_command_request],
178178
)
179179

180-
# Init the Slack Bolt logger and log handlers.
180+
181+
# ================== Logging Setup ==================
182+
# Remove default handlers and configure root logger for DEBUG output.
181183
SlackRequestHandler.clear_all_log_handlers()
182184
logging.basicConfig(format="%(asctime)s %(message)s", level=logging.DEBUG)
183185

184186

185-
# Lambda handler method.
187+
# ================== AWS Lambda Entrypoint ==================
186188
def handler(event, context):
187-
print(f"${SLACK_SLASH_COMMAND} - Event: {event}\n")
189+
"""
190+
AWS Lambda entrypoint: Handles Slack requests via Slack Bolt's AWS adapter.
191+
This function is called by AWS Lambda when the function is invoked.
192+
"""
193+
logging.info(f"{SLACK_SLASH_COMMAND} - Event: {event}")
188194
slack_handler = SlackRequestHandler(app=app)
189195
return slack_handler.handle(event, context)

packages/slackBotFunction/requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
slack-bolt==1.21.0
1+
slack-bolt==1.22.0
2+
slack-sdk==3.34.0
23
boto3
34
requests
45
opensearch-py==2.4.2

0 commit comments

Comments
 (0)