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-
211import os
222import json
233import boto3
244import logging
255from slack_bolt import App
266from 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 ==================
3012def 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.
5340bot_token_parameter = os .environ ["SLACK_BOT_TOKEN_PARAMETER" ]
5441signing_secret_parameter = os .environ ["SLACK_SIGNING_SECRET_PARAMETER" ]
5542
56- # Retrieve the parameters from SSM Parameter Store
5743bot_token = get_parameter (bot_token_parameter )
5844signing_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.
6149app = 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 ==================
6857SLACK_SLASH_COMMAND = os .environ ["SLACK_SLASH_COMMAND" ]
6958KNOWLEDGEBASE_ID = os .environ ["KNOWLEDGEBASE_ID" ]
7059RAG_MODEL_ID = os .environ ["RAG_MODEL_ID" ]
7160AWS_REGION = os .environ ["AWS_REGION" ]
7261GUARD_RAIL_ID = os .environ ["GUARD_RAIL_ID" ]
7362GUARD_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
8070def 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 ==================
8879def 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"\n Processing 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 ==================
11296def 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 ==================
135135def 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
175175app .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.
181183SlackRequestHandler .clear_all_log_handlers ()
182184logging .basicConfig (format = "%(asctime)s %(message)s" , level = logging .DEBUG )
183185
184186
185- # Lambda handler method.
187+ # ================== AWS Lambda Entrypoint ==================
186188def 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 )
0 commit comments