diff --git a/packages/cdk/nagSuppressions.ts b/packages/cdk/nagSuppressions.ts
index 9f3c61b1..d0f9220f 100644
--- a/packages/cdk/nagSuppressions.ts
+++ b/packages/cdk/nagSuppressions.ts
@@ -45,6 +45,22 @@ export const nagSuppressions = (stack: Stack) => {
safeAddNagSuppression(
stack,
"/EpsAssistMeStack/Apis/EpsAssistApiGateway/ApiGateway/Default/slack/events/POST/Resource",
+ [
+ {
+ id: "AwsSolutions-APIG4",
+ reason: "Slack event endpoint is intentionally unauthenticated."
+ },
+ {
+ id: "AwsSolutions-COG4",
+ reason: "Cognito not required for this public endpoint."
+ }
+ ]
+ )
+
+ // Suppress unauthenticated API route warnings
+ safeAddNagSuppression(
+ stack,
+ "/EpsAssistMeStack/Apis/EpsAssistApiGateway/ApiGateway/Default/slack/commands/POST/Resource",
[
{
id: "AwsSolutions-APIG4",
diff --git a/packages/cdk/resources/Apis.ts b/packages/cdk/resources/Apis.ts
index 39702ddb..1ea5475d 100644
--- a/packages/cdk/resources/Apis.ts
+++ b/packages/cdk/resources/Apis.ts
@@ -37,6 +37,17 @@ export class Apis extends Construct {
lambdaFunction: props.functions.slackBot
})
+ // Create the '/slack/commands' POST endpoint for Slack Events API
+ // This endpoint will handle slash commands, such as /test
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const slackCommandsEndpoint = new LambdaEndpoint(this, "SlackCommandsEndpoint", {
+ parentResource: slackResource,
+ resourceName: "commands",
+ method: HttpMethod.POST,
+ restApiGatewayRole: apiGateway.role,
+ lambdaFunction: props.functions.slackBot
+ })
+
this.apis = {
api: apiGateway
}
diff --git a/packages/cdk/stacks/EpsAssistMeStack.ts b/packages/cdk/stacks/EpsAssistMeStack.ts
index 24bb3ee7..fd0c99cb 100644
--- a/packages/cdk/stacks/EpsAssistMeStack.ts
+++ b/packages/cdk/stacks/EpsAssistMeStack.ts
@@ -216,6 +216,12 @@ export class EpsAssistMeStack extends Stack {
description: "Slack Events API endpoint for @mentions and direct messages"
})
+ // Output: SlackBot Endpoint
+ new CfnOutput(this, "SlackBotCommandsEndpoint", {
+ value: `https://${apis.apis["api"].api.domainName?.domainName}/slack/commands`,
+ description: "Slack Commands API endpoint for slash commands"
+ })
+
// Output: Bedrock Prompt ARN
new CfnOutput(this, "QueryReformulationPromptArn", {
value: bedrockPromptResources.queryReformulationPrompt.promptArn,
diff --git a/packages/slackBotFunction/app/handler.py b/packages/slackBotFunction/app/handler.py
index e7a3426a..099816bf 100644
--- a/packages/slackBotFunction/app/handler.py
+++ b/packages/slackBotFunction/app/handler.py
@@ -19,7 +19,11 @@
create_error_response,
)
from app.services.app import get_app
-from app.slack.slack_events import process_pull_request_slack_action, process_pull_request_slack_event
+from app.slack.slack_events import (
+ process_pull_request_slack_action,
+ process_pull_request_slack_command,
+ process_pull_request_slack_event,
+)
logger = get_logger()
@@ -46,19 +50,30 @@ def handler(event: dict, context: LambdaContext) -> dict:
return handle_direct_invocation(event, context)
app = get_app(logger=logger)
+ error_msg = "Pull request processing requested but no slack_event provided"
# handle pull request processing requests
if event.get("pull_request_event"):
slack_event_data = event.get("slack_event")
if not slack_event_data:
- logger.error("Pull request processing requested but no slack_event provided")
+ logger.error(error_msg)
return {"statusCode": 400}
process_pull_request_slack_event(slack_event_data=slack_event_data)
return {"statusCode": 200}
+
+ if event.get("pull_request_command"):
+ slack_body_data = event.get("slack_event")
+ if not slack_body_data:
+ logger.error(error_msg)
+ return {"statusCode": 400}
+
+ process_pull_request_slack_command(slack_command_data=slack_body_data)
+ return {"statusCode": 200}
+
if event.get("pull_request_action"):
slack_body_data = event.get("slack_body")
if not slack_body_data:
- logger.error("Pull request processing requested but no slack_event provided")
+ logger.error(error_msg)
return {"statusCode": 400}
process_pull_request_slack_action(slack_body_data=slack_body_data)
diff --git a/packages/slackBotFunction/app/services/sample_questions.py b/packages/slackBotFunction/app/services/sample_questions.py
new file mode 100644
index 00000000..9d757cfb
--- /dev/null
+++ b/packages/slackBotFunction/app/services/sample_questions.py
@@ -0,0 +1,64 @@
+# flake8: noqa: E501
+# fmt: off
+class SampleQuestionBank:
+ """A collection of sample questions for testing purposes."""
+
+ def __init__(self):
+ self.questions = []
+ # Append data as tuples: (id, text) just to make it easier to read for users.
+ self.questions.append((0, "can a prescribed item have more than one endorsement or is it expected that a single item has no more than one endorsement?")) # noqa: E501
+ self.questions.append((1, "for the non-repudiation screen, Note - is this the note to pharmacy? note to patient?")) # noqa: E501
+ self.questions.append((2, """We are getting an error from DSS "Unauthorised". We have checked the credentials and they appear to be correct. What could be the cause? Note that, the CIS2 key is different to the key used in the EPS application (which has the signing api attached to it)""")) # noqa: E501
+ self.questions.append((3, """Can you clarify this requirement?
+
+"The connecting system must monitor the sending of prescription order request and alert the NHSE service desk if requests fail to send as expected"a)Is this meant to be an automated process, or should an option be given to the user to send an e-mail?
+b)What will be the e-mail used?
+c)Any certain format needed for the report?
+d)Should this be done only after 2 hours of retries that failed?""")) # noqa: E501
+ self.questions.append((4, "how do we find if a patient is a cross-border patient?")) # noqa: E501
+ self.questions.append((5, "When a prescription has only one prescribed item and the prescriber cancels the prescribed item will the prescription be downloaded when a bulk download request is made to /FHIR/R4/Task/$release?")) # noqa: E501
+ self.questions.append((6, "When I have authenticated with a smartcard using CIS2 over an HSCN connection what happens when the smartcard is removed?")) # noqa: E501
+ self.questions.append((7, "direct me to the web page that has the details on how to uninstall Oberthur SR1")) # noqa: E501
+ self.questions.append((8, """Should the Dispensing system send a Claim notification message when all the prescription items on the prescription have expired, and if it should then what value should be sent for the overall prescription form status for the FHIAR API extension "https://fhir.nhs.uk/StructureDefinition/Extension-EPS-TaskBusinessStatus"?""")) # noqa: E501
+ self.questions.append((9, """For an EPS FHIR API Dispense Notification message, when should the value "on-hold" be used for resource: { "resourceType": "MedicationDispense", "status"} for a prescription item ?""")) # noqa: E501
+ self.questions.append((10, "confirm if dispensing systems are required to include an option for the user to select the MedicationDispenseReasoncodes of 004, 005, and 009")) # noqa: E501
+ self.questions.append((11, "how many lines can a prescription have?")) # noqa: E501
+ self.questions.append((12, "Is extension:controlledDrugSchedule.value.userSelected. used for EPS?")) # noqa: E501
+ self.questions.append((13, "Can we use dosageInstruction.timing.repeat.boundsPeriod.start or ...dosageInstruction.timing.repeat.boundsPeriod.end?")) # noqa: E501
+ self.questions.append((14, "show me example of prescription successfully created response within the Electronic Prescribing System (EPS), where a unique identifier for the prescription, often referred to as the prescription ID is shown")) # noqa: E501
+ self.questions.append((15, "please provide a documentation link where i find the specific format of prescribing token?")) # noqa: E501
+ self.questions.append((16, """I have a query related to cancelling Repeat Dispensing (RD) courses. We would like to know what the Spine allows in terms of cancelling particular issues of a RD course.For example, let's say a patient has an ongoing RD course which originally had 6 issues. Let's say that issue 1 has been dispensed (status 'Dispensed'), and issue 2 has been pulled down from the Spine by the pharmacist (status 'With dispenser'). The remaining issues 3-6 are still on spine.Can the 'Cancel' request sent to Spine handle the following scenarios? And what will happen if so?
+
+Doctor requests to cancel all issues, 1-6
+Doctor requests to cancel all remaining issues on Spine, 3-6""")) # noqa: E501
+ self.questions.append((17, "Does a Cancel request for a medication that has been downloaded include the pharmacy details of the pharmacy that had downloaded the medication in the response")) # noqa: E501
+ self.questions.append((18, """For MedicationRequest "entry. resource. groupIdentifier. extension. valueIdentifier" what is the Long-form Prescription ID?""")) # noqa: E501
+ self.questions.append((19, "for the non-repudiation screen, do we use the patient friendly text for dosage information?")) # noqa: E501
+ self.questions.append((20, "Can an API have multiple callback URLs")) # noqa: E501
+
+ def get_questions(self, start: int, end: int) -> list[tuple[int, str]]:
+ """
+ Pulls a selection of questions
+ """
+ # Must be integers
+ if not isinstance(start, int):
+ raise TypeError(f"'start' must be an integer, got {type(start).__name__}")
+
+ if not isinstance(end, int):
+ raise TypeError(f"'end' must be an integer, got {type(end).__name__}")
+
+ # Must be in valid range
+ if start < 0:
+ raise ValueError("'start' cannot be negative")
+
+ if end < 0 or end < start:
+ raise ValueError("'end' must be non-negative and greater than or equal to 'start'")
+
+ if end > len(self.questions) - 1:
+ raise ValueError(f"'end' must be less than {len(self.questions)}")
+
+ # Extract only the text (index 1) from the tuple
+ return list(self.questions[start : end + 1])
+
+ def add_questions(self, question_text: str):
+ self.questions.append((len(self.questions), question_text))
diff --git a/packages/slackBotFunction/app/slack/slack_events.py b/packages/slackBotFunction/app/slack/slack_events.py
index cce33cbd..851f4999 100644
--- a/packages/slackBotFunction/app/slack/slack_events.py
+++ b/packages/slackBotFunction/app/slack/slack_events.py
@@ -24,11 +24,13 @@
update_state_information,
)
+from app.services.sample_questions import SampleQuestionBank
from app.services.slack import get_friendly_channel_name, post_error_message
from app.utils.handler_utils import (
conversation_key_and_root,
extract_pull_request_id,
- forward_event_to_pull_request_lambda,
+ extract_test_command_params,
+ forward_to_pull_request_lambda,
is_duplicate_event,
is_latest_message,
strip_mentions,
@@ -39,6 +41,8 @@
logger = get_logger()
+processing_error_message = "Error processing message"
+
# ================================================================
# Privacy and Q&A management helpers
@@ -145,16 +149,11 @@ def _handle_session_management(
def _create_feedback_blocks(
response_text: str,
citations: list[dict[str, str]],
- conversation_key: str,
- channel: str,
- message_ts: str,
- thread_ts: str,
+ feedback_data: dict[str, str],
) -> list[dict[str, Any]]:
"""Create Slack blocks with feedback buttons"""
- # Create compact feedback payload for button actions
- feedback_data = {"ck": conversation_key, "ch": channel, "mt": message_ts}
- if thread_ts: # Only include thread_ts for channel threads, not DMs
- feedback_data["tt"] = thread_ts
+ if feedback_data.get("thread_ts"): # Only include thread_ts for channel threads, not DMs
+ feedback_data["tt"] = feedback_data["thread_ts"]
feedback_value = json.dumps(feedback_data, separators=(",", ":"))
# Main response block
@@ -397,8 +396,13 @@ def process_async_slack_event(event: Dict[str, Any], event_id: str, client: WebC
if message_text.lower().startswith(constants.PULL_REQUEST_PREFIX):
try:
pull_request_id, _ = extract_pull_request_id(text=message_text)
- forward_event_to_pull_request_lambda(
- pull_request_id=pull_request_id, event=event, event_id=event_id, store_pull_request_id=True
+ forward_to_pull_request_lambda(
+ body={},
+ pull_request_id=pull_request_id,
+ event=event,
+ event_id=event_id,
+ store_pull_request_id=True,
+ type="event",
)
except Exception as e:
logger.error(f"Can not find pull request details: {e}", extra={"error": traceback.format_exc()})
@@ -418,6 +422,18 @@ def process_async_slack_event(event: Dict[str, Any], event_id: str, client: WebC
process_slack_message(event=event, event_id=event_id, client=client)
+def process_async_slack_command(command: Dict[str, Any], client: WebClient) -> None:
+ logger.debug("Processing async Slack command", extra={"command": command})
+
+ try:
+ command_arg = command.get("command", "").strip()
+ if command_arg == "/test":
+ process_command_test(command=command, client=client)
+ except Exception as e:
+ logger.error(f"Error processing test command: {e}", extra={"error": traceback.format_exc()})
+ post_error_message(channel=command["channel_id"], thread_ts=None, client=client)
+
+
def process_slack_message(event: Dict[str, Any], event_id: str, client: WebClient) -> None:
"""
Process Slack events asynchronously after initial acknowledgment
@@ -450,49 +466,33 @@ def process_slack_message(event: Dict[str, Any], event_id: str, client: WebClien
session_data = get_conversation_session_data(conversation_key)
session_id = session_data.get("session_id") if session_data else None
- ai_response = process_ai_query(user_query, session_id)
- kb_response = ai_response["kb_response"]
- response_text = ai_response["text"]
-
- # Split out citation block if present
- # Citations are not returned in the object without using `$output_format_instructions$` which overrides the
- # system prompt. Instead, pull out and format the citations in the prompt manually
- prompt_value_keys = ["source_number", "title", "excerpt", "relevance_score"]
- split = response_text.split("------") # Citations are separated from main body by ------
-
- citations: list[dict[str, str]] = []
- if len(split) != 1:
- response_text = split[0]
- citation_block = split[1]
- raw_citations = []
- raw_citations = re.compile(r"]*>(.*?)", re.DOTALL | re.IGNORECASE).findall(citation_block)
- if len(raw_citations) > 0:
- logger.info("Found citation(s)", extra={"Raw Citations": raw_citations})
- citations = [dict(zip(prompt_value_keys, citation.split("||"))) for citation in raw_citations]
- logger.info("Parsed citation(s)", extra={"citations": citations})
-
# Post the answer (plain) to get message_ts
- post_params = {"channel": channel, "text": response_text}
+ post_params = {"channel": channel, "text": ":spinner:"}
if thread_ts: # Only add thread_ts for channel threads, not DMs
post_params["thread_ts"] = thread_ts
post = client.chat_postMessage(**post_params)
message_ts = post["ts"]
+ # Create compact feedback payload for button actions
+ feedback_data = {"channel": channel, "message_ts": message_ts, "thread_ts": thread_ts}
+
+ # Call Bedrock to process the user query
+ kb_response, response_text, blocks = process_formatted_bedrock_query(
+ user_query=user_query, session_id=session_id, feedback_data={**feedback_data, "ck": conversation_key}
+ )
+
_handle_session_management(
- conversation_key=conversation_key,
+ **feedback_data,
session_data=session_data,
session_id=session_id,
kb_response=kb_response,
user_id=user_id,
- channel=channel,
- thread_ts=thread_ts,
- message_ts=message_ts,
+ conversation_key=conversation_key,
)
# Store Q&A pair for feedback correlation
store_qa_pair(conversation_key, user_query, response_text, message_ts, kb_response.get("sessionId"), user_id)
- blocks = _create_feedback_blocks(response_text, citations, conversation_key, channel, message_ts, thread_ts)
try:
client.chat_update(channel=channel, ts=message_ts, text=response_text, blocks=blocks)
except Exception as e:
@@ -520,7 +520,22 @@ def process_pull_request_slack_event(slack_event_data: Dict[str, Any]) -> None:
process_async_slack_event(event=event, event_id=event_id, client=client)
except Exception:
# we cant post a reply to slack for this error as we may not have details about where to post it
- logger.error("Error processing message", extra={"event_id": event_id, "error": traceback.format_exc()})
+ logger.error(processing_error_message, extra={"event_id": event_id, "error": traceback.format_exc()})
+
+
+def process_pull_request_slack_command(slack_command_data: Dict[str, Any]) -> None:
+ # separate function to process pull requests so that we can ensure we store session information
+ logger.debug(
+ "Processing pull request slack command", extra={"slack_command_data": slack_command_data}
+ ) # Removed line after debugging
+ try:
+ command = slack_command_data["event"]
+ token = get_bot_token()
+ client = WebClient(token=token)
+ process_async_slack_command(command=command, client=client)
+ except Exception:
+ # we cant post a reply to slack for this error as we may not have details about where to post it
+ logger.error(processing_error_message, extra={"error": traceback.format_exc()})
def process_pull_request_slack_action(slack_body_data: Dict[str, Any]) -> None:
@@ -531,7 +546,7 @@ def process_pull_request_slack_action(slack_body_data: Dict[str, Any]) -> None:
process_async_slack_action(body=slack_body_data, client=client)
except Exception:
# we cant post a reply to slack for this error as we may not have details about where to post it
- logger.error("Error processing message", extra={"error": traceback.format_exc()})
+ logger.error(processing_error_message, extra={"error": traceback.format_exc()})
def log_query_stats(user_query: str, event: Dict[str, Any], channel: str, client: WebClient, thread_ts: str) -> None:
@@ -646,6 +661,36 @@ def store_feedback(
logger.error(f"Error storing feedback: {e}", extra={"error": traceback.format_exc()})
+def process_formatted_bedrock_query(
+ user_query: str, session_id: str | None, feedback_data: Dict[str, Any]
+) -> Dict[str, Any]:
+ """Process the user query with Bedrock and return the response dict"""
+ ai_response = process_ai_query(user_query, session_id)
+ kb_response = ai_response["kb_response"]
+ response_text = ai_response["text"]
+
+ # Split out citation block if present
+ # Citations are not returned in the object without using `$output_format_instructions$` which overrides the
+ # system prompt. Instead, pull out and format the citations in the prompt manually
+ prompt_value_keys = ["source_number", "title", "excerpt", "relevance_score"]
+ split = response_text.split("------") # Citations are separated from main body by ------
+
+ citations: list[dict[str, str]] = []
+ if len(split) != 1:
+ response_text = split[0]
+ citation_block = split[1]
+ raw_citations = []
+ raw_citations = re.compile(r"]*>(.*?)", re.DOTALL | re.IGNORECASE).findall(citation_block)
+ if len(raw_citations) > 0:
+ logger.info("Found citation(s)", extra={"Raw Citations": raw_citations})
+ citations = [dict(zip(prompt_value_keys, citation.split("||"))) for citation in raw_citations]
+ logger.info("Parsed citation(s)", extra={"citations": citations})
+
+ blocks = _create_feedback_blocks(response_text, citations, feedback_data)
+
+ return kb_response, response_text, blocks
+
+
def open_citation(channel: str, timestamp: str, message: Any, params: Dict[str, Any], client: WebClient) -> None:
"""Open citation - update/replace message to include citation content"""
logger.info("Opening citation", extra={"channel": channel, "timestamp": timestamp})
@@ -724,6 +769,93 @@ def _toggle_button_style(element: dict) -> bool:
return True
+# ================================================================
+# Command management
+# ================================================================
+def process_command_test(command: Dict[str, Any], client: WebClient) -> None:
+ if "help" in command.get("text"):
+ process_command_test_help(command=command, client=client)
+ else:
+ process_command_test_response(command=command, client=client)
+
+
+def process_command_test_response(command: Dict[str, Any], client: WebClient) -> None:
+ # Initial acknowledgment
+ post_params = {
+ "channel": command["channel_id"],
+ "text": "Initialising tests...\n",
+ }
+ client.chat_meMessage(**post_params)
+
+ # Extract parameters
+ params = extract_test_command_params(command.get("text"))
+
+ pr = params.get("pr", "").strip()
+ pr = f"pr: {pr}" if pr else ""
+
+ start = int(params.get("start", 0))
+ end = int(params.get("end", 20))
+ logger.info("Test command parameters", extra={"pr": pr, "start": start, "end": end})
+
+ # Retrieve sample questions
+ test_questions = SampleQuestionBank().get_questions(start=start, end=end)
+ logger.info("Retrieved test questions", extra={"count": len(test_questions)})
+
+ # Post each test question
+ for question in test_questions:
+ # Update message to make it more user-friendly
+ post_params["text"] = f"Question {question[0]}:\n> {question[1].replace('\n', '\n> ')}\n"
+ response = client.chat_postMessage(**post_params)
+
+ # Process as normal message
+ slack_message = {**response.data, "text": f"{pr} {question[1]}"}
+ logger.debug("Processing test question", extra={"slack_message": slack_message})
+
+ message_ts = response.get("ts")
+ channel = response.get("channel")
+
+ feedback_data = {
+ "ck": None,
+ "ch": channel,
+ "mt": message_ts,
+ "thread_ts": message_ts,
+ }
+
+ _, response_text, blocks = process_formatted_bedrock_query(question[1], None, feedback_data)
+ try:
+ client.chat_postMessage(channel=channel, thread_ts=message_ts, text=response_text, blocks=blocks)
+ except Exception as e:
+ logger.error(
+ f"Failed to attach feedback buttons: {e}",
+ extra={"event_id": None, "message_ts": message_ts, "error": traceback.format_exc()},
+ )
+
+
+def process_command_test_help(command: Dict[str, Any], client: WebClient) -> None:
+ help_text = """
+ Certainly! Here is some help testing me!
+
+ You can use the `/test` command to send sample test questions to the bot.
+ The command supports parameters to specify the range of questions or have me target a specific pull request.
+
+ - Usage:
+ - /test [pr: ] [q-]
+
+ - Parameters:
+ - : (optional) Specify a pull request ID to associate the questions with.
+ - : (optional) The starting and ending index of the sample questions (default is 0-20).
+ - : The ending index of the sample questions (default is 20).
+
+ - Examples:
+ - /test --> Sends questions 0 to 20
+ - /test q15 --> Sends question 15 only
+ - /test q10-25 --> Sends questions 10 to 25
+ - /test pr:12345 --> Sends questions 0 to 20 for pull request 12345
+ - /test pr:12345 q5-15 --> Sends questions 5 to 15 for pull request 12345
+ """
+ client.chat_meMessage(channel=command["channel_id"], text=help_text)
+
+
# ================================================================
# Session management
# ================================================================
diff --git a/packages/slackBotFunction/app/slack/slack_handlers.py b/packages/slackBotFunction/app/slack/slack_handlers.py
index 2100be2d..bf78111b 100644
--- a/packages/slackBotFunction/app/slack/slack_handlers.py
+++ b/packages/slackBotFunction/app/slack/slack_handlers.py
@@ -8,10 +8,11 @@
"""
import json
+import time
from functools import lru_cache
import traceback
from typing import Any, Dict
-from slack_bolt import Ack, App
+from slack_bolt import Ack, App, Say
from slack_sdk import WebClient
from app.core.config import (
get_logger,
@@ -19,13 +20,13 @@
from app.utils.handler_utils import (
conversation_key_and_root,
extract_session_pull_request_id,
- forward_action_to_pull_request_lambda,
- forward_event_to_pull_request_lambda,
+ extract_test_command_params,
+ forward_to_pull_request_lambda,
gate_common,
respond_with_eyes,
should_reply_to_message,
)
-from app.slack.slack_events import process_async_slack_action, process_async_slack_event
+from app.slack.slack_events import process_async_slack_action, process_async_slack_event, process_async_slack_command
logger = get_logger()
@@ -43,6 +44,7 @@ def setup_handlers(app: App) -> None:
app.action("feedback_no")(ack=respond_to_action, lazy=[feedback_handler])
for i in range(1, 10):
app.action(f"cite_{i}")(ack=respond_to_action, lazy=[feedback_handler])
+ app.command("/test")(ack=respond_to_command, lazy=[command_handler])
# ================================================================
@@ -54,13 +56,19 @@ def setup_handlers(app: App) -> None:
def respond_to_events(event: Dict[str, Any], ack: Ack, client: WebClient):
if should_reply_to_message(event, client):
respond_with_eyes(event=event, client=client)
- logger.debug("Sending ack response")
+ logger.debug("Sending ack response for event")
ack()
# ack function for actions where we just send an ack response back
def respond_to_action(ack: Ack):
- logger.debug("Sending ack response")
+ logger.debug("Sending ack response for action")
+ ack()
+
+
+# ack function for commands where we just send an ack response back
+def respond_to_command(ack: Ack, say: Say):
+ logger.debug("Sending ack response for command")
ack()
@@ -77,7 +85,14 @@ def feedback_handler(body: Dict[str, Any], client: WebClient) -> None:
f"Feedback in pull request session {session_pull_request_id}",
extra={"session_pull_request_id": session_pull_request_id},
)
- forward_action_to_pull_request_lambda(body=body, pull_request_id=session_pull_request_id)
+ forward_to_pull_request_lambda(
+ body=body,
+ event=None,
+ event_id="",
+ store_pull_request_id=False,
+ pull_request_id=session_pull_request_id,
+ type="action",
+ )
return
process_async_slack_action(body=body, client=client)
except Exception as e:
@@ -117,8 +132,13 @@ def unified_message_handler(client: WebClient, event: Dict[str, Any], body: Dict
f"Message in pull request session {session_pull_request_id} from user {user_id}",
extra={"session_pull_request_id": session_pull_request_id},
)
- forward_event_to_pull_request_lambda(
- event=event, pull_request_id=session_pull_request_id, event_id=event_id, store_pull_request_id=False
+ forward_to_pull_request_lambda(
+ body=body,
+ event=event,
+ pull_request_id=session_pull_request_id,
+ event_id=event_id,
+ store_pull_request_id=False,
+ type="event",
)
return
@@ -126,4 +146,34 @@ def unified_message_handler(client: WebClient, event: Dict[str, Any], body: Dict
try:
process_async_slack_event(event=event, event_id=event_id, client=client)
except Exception:
- logger.error("Error triggering async processing", extra={"error": traceback.format_exc()})
+ logger.error("Error triggering async processing for event", extra={"error": traceback.format_exc()})
+
+
+def command_handler(body: Dict[str, Any], command: Dict[str, Any], client: WebClient) -> None:
+ """Handle /test command to prompt the bot to respond."""
+ logger.info("Received command from user", extra={"body": body, "command": command, "client": client})
+ if not command:
+ logger.error("Invalid command payload")
+ return
+
+ user_id = command.get("user_id")
+ session_pull_request_id = extract_test_command_params(command.get("text")).get("pr")
+ if session_pull_request_id:
+ logger.info(
+ f"Command in pull request session {session_pull_request_id} from user {user_id}",
+ extra={"session_pull_request_id": session_pull_request_id},
+ )
+ forward_to_pull_request_lambda(
+ body=body,
+ event=command,
+ pull_request_id=session_pull_request_id,
+ event_id=f"/command-{time.time()}",
+ store_pull_request_id=False,
+ type="command",
+ )
+ return
+
+ try:
+ process_async_slack_command(command, client)
+ except Exception:
+ logger.error("Error triggering async processing for command", extra={"error": traceback.format_exc()})
diff --git a/packages/slackBotFunction/app/utils/handler_utils.py b/packages/slackBotFunction/app/utils/handler_utils.py
index f75b1cf1..ca756172 100644
--- a/packages/slackBotFunction/app/utils/handler_utils.py
+++ b/packages/slackBotFunction/app/utils/handler_utils.py
@@ -67,20 +67,20 @@ def get_pull_request_lambda_arn(pull_request_id: str) -> str:
raise e
-def forward_event_to_pull_request_lambda(
- pull_request_id: str, event: Dict[str, Any], event_id: str, store_pull_request_id: bool
+def forward_to_pull_request_lambda(
+ body: Dict[str, Any],
+ event: Dict[str, Any],
+ event_id: str,
+ pull_request_id: str,
+ store_pull_request_id: bool,
+ type: str,
) -> None:
lambda_client: LambdaClient = boto3.client("lambda")
try:
pull_request_lambda_arn = get_pull_request_lambda_arn(pull_request_id=pull_request_id)
- # strip pull request prefix and id from message text
- message_text = event["text"]
- _, extracted_message = extract_pull_request_id(message_text)
- event["text"] = extracted_message
-
- lambda_payload = {"pull_request_event": True, "slack_event": {"event": event, "event_id": event_id}}
+ lambda_payload = get_forward_payload(body=body, event=event, event_id=event_id, type=type)
logger.debug(
- "Forwarding event to pull request lambda",
+ f"Forwarding '{type}' to pull request lambda",
extra={"lambda_arn": pull_request_lambda_arn, "lambda_payload": lambda_payload},
)
lambda_client.invoke(
@@ -94,27 +94,22 @@ def forward_event_to_pull_request_lambda(
store_state_information(item=item)
except Exception as e:
- logger.error("Failed to trigger pull request lambda", extra={"error": traceback.format_exc()})
+ logger.error("Failed to forward request to pull request lambda", extra={"error": traceback.format_exc()})
raise e
-def forward_action_to_pull_request_lambda(body: Dict[str, Any], pull_request_id: str) -> None:
- lambda_client: LambdaClient = boto3.client("lambda")
- try:
- pull_request_lambda_arn = get_pull_request_lambda_arn(pull_request_id=pull_request_id)
- lambda_payload = {"pull_request_action": True, "slack_body": body}
- logger.debug(
- "Forwarding action to pull request lambda",
- extra={"lambda_arn": pull_request_lambda_arn, "lambda_payload": lambda_payload},
- )
- lambda_client.invoke(
- FunctionName=pull_request_lambda_arn, InvocationType="Event", Payload=json.dumps(lambda_payload)
- )
- logger.info("Triggered pull request lambda", extra={"lambda_arn": pull_request_lambda_arn})
+def get_forward_payload(body: Dict[str, Any], event: Dict[str, Any], event_id: str, type: str) -> Dict[str, Any]:
+ if type == "action":
+ return {"pull_request_action": True, "slack_body": body}
- except Exception as e:
- logger.error("Failed to forward request to pull request lambda", extra={"error": traceback.format_exc()})
- raise e
+ if event_id is None or event["text"] is None:
+ logger.error("Missing required fields to forward pull request event")
+ return None
+
+ message_text = event["text"]
+ _, extracted_message = extract_pull_request_id(message_text)
+ event["text"] = extracted_message
+ return {"pull_request_event": True, "slack_event": {"event": event, "event_id": event_id}}
def is_latest_message(conversation_key: str, message_ts: str) -> bool:
@@ -178,6 +173,34 @@ def extract_pull_request_id(text: str) -> Tuple[str | None, str]:
return None, text.strip()
+def extract_test_command_params(text: str) -> Dict[str, str]:
+ """
+ Extract parameters from the /test command text.
+
+ Expected format: /test pr: 123 q1-2
+ """
+ params = {
+ "pr": "",
+ "start": "0",
+ "end": "20",
+ }
+ prefix = re.escape(constants.PULL_REQUEST_PREFIX) # safely escape for regex
+ pr_pattern = rf"{prefix}\s*(\d+)\b"
+ q_pattern = r"\bq-?(\d+)(?:-(\d+))?"
+
+ pr_match = re.match(pr_pattern, text, flags=re.IGNORECASE)
+ if pr_match:
+ params["pr"] = pr_match.group(1)
+
+ q_match = re.search(q_pattern, text, flags=re.IGNORECASE)
+ if q_match:
+ params["start"] = q_match.group(1)
+ params["end"] = q_match.group(2) if q_match.group(2) else q_match.group(1)
+
+ logger.debug("Extracted test command parameters", extra={"params": params})
+ return params
+
+
def conversation_key_and_root(event: Dict[str, Any]) -> Tuple[str, str]:
"""
Build a stable conversation scope and its root timestamp.
@@ -294,6 +317,7 @@ def should_reply_to_message(event: Dict[str, Any], client: WebClient = None) ->
- Message is in a group chat (channel_type == 'group') but not in a thread
- Message is in a channel or group thread where the bot was not initially mentioned
"""
+ logger.debug("Checking if should reply to message", extra={"event": event, "client": client})
# we don't reply to non-threaded messages in group chats
if event.get("channel_type") == "group" and event.get("type") == "message" and event.get("thread_ts") is None:
diff --git a/packages/slackBotFunction/tests/example_command.json b/packages/slackBotFunction/tests/example_command.json
new file mode 100644
index 00000000..d506414c
--- /dev/null
+++ b/packages/slackBotFunction/tests/example_command.json
@@ -0,0 +1,36 @@
+{
+ "body": {
+ "token": "QxKSL5PxaCmAd9hhII4pIbCe",
+ "team_id": "TRK2Y2T1U",
+ "team_domain": "nhsdigital-platforms",
+ "channel_id": "D0A0BQDBCA0",
+ "channel_name": "directmessage",
+ "user_id": "U09TSP13P9P",
+ "user_name": "kieran.wilkinson4",
+ "command": "/test",
+ "text": "pr: 261",
+ "api_app_id": "A09FAPJQUG7",
+ "is_enterprise_install": "false",
+ "enterprise_id": "E02KFHR77DL",
+ "enterprise_name": "NHS England",
+ "response_url": "https://hooks.slack.com/commands/TRK2Y2T1U/10233876396961/KLkYSgUizMbBlRyUal0ZW5cc",
+ "trigger_id": "10224545323140.869100095062.9f251bf0cd683d4306291c2547a77e0f"
+ },
+ "command": {
+ "token": "QxKSL5PxaCmAd9hhII4pIbCe",
+ "team_id": "TRK2Y2T1U",
+ "team_domain": "nhsdigital-platforms",
+ "channel_id": "D0A0BQDBCA0",
+ "channel_name": "directmessage",
+ "user_id": "U09TSP13P9P",
+ "user_name": "kieran.wilkinson4",
+ "command": "/test",
+ "text": "pr: 261",
+ "api_app_id": "A09FAPJQUG7",
+ "is_enterprise_install": "false",
+ "enterprise_id": "E02KFHR77DL",
+ "enterprise_name": "NHS England",
+ "response_url": "https://hooks.slack.com/commands/TRK2Y2T1U/10233876396961/KLkYSgUizMbBlRyUal0ZW5cc",
+ "trigger_id": "10224545323140.869100095062.9f251bf0cd683d4306291c2547a77e0f"
+ }
+}
diff --git a/packages/slackBotFunction/tests/test_forward_to_lambda.py b/packages/slackBotFunction/tests/test_forward_to_lambda.py
index 20672462..e4eccaf7 100644
--- a/packages/slackBotFunction/tests/test_forward_to_lambda.py
+++ b/packages/slackBotFunction/tests/test_forward_to_lambda.py
@@ -25,14 +25,19 @@ def client_side_effect(service_name, *args, **kwargs):
# delete and import module to test
if "app.utils.handler_utils" in sys.modules:
del sys.modules["app.utils.handler_utils"]
- from app.utils.handler_utils import forward_event_to_pull_request_lambda
+ from app.utils.handler_utils import forward_to_pull_request_lambda
# perform operation
event_data = {"test": "data", "channel": "C123", "ts": "12345.6789", "text": "foo_bar"}
with patch("app.utils.handler_utils.get_pull_request_lambda_arn", return_value="output_SlackBotLambdaArn"):
- forward_event_to_pull_request_lambda(
- pull_request_id="123", event=event_data, event_id="evt123", store_pull_request_id=True
+ forward_to_pull_request_lambda(
+ body={},
+ pull_request_id="123",
+ event=event_data,
+ event_id="evt123",
+ store_pull_request_id=True,
+ type="event",
)
# assertions
@@ -72,14 +77,19 @@ def client_side_effect(service_name, *args, **kwargs):
# delete and import module to test
if "app.utils.handler_utils" in sys.modules:
del sys.modules["app.utils.handler_utils"]
- from app.utils.handler_utils import forward_event_to_pull_request_lambda
+ from app.utils.handler_utils import forward_to_pull_request_lambda
# perform operation
event_data = {"test": "data", "channel": "C123", "ts": "12345.6789", "text": "foo_bar"}
with patch("app.utils.handler_utils.get_pull_request_lambda_arn", return_value="output_SlackBotLambdaArn"):
- forward_event_to_pull_request_lambda(
- pull_request_id="123", event=event_data, event_id="evt123", store_pull_request_id=False
+ forward_to_pull_request_lambda(
+ body={},
+ pull_request_id="123",
+ event=event_data,
+ event_id="evt123",
+ store_pull_request_id=False,
+ type="event",
)
# assertions
@@ -115,15 +125,20 @@ def client_side_effect(service_name, *args, **kwargs):
# delete and import module to test
if "app.utils.handler_utils" in sys.modules:
del sys.modules["app.utils.handler_utils"]
- from app.utils.handler_utils import forward_event_to_pull_request_lambda
+ from app.utils.handler_utils import forward_to_pull_request_lambda
# perform operation
event_data = {"test": "data"}
with patch("app.utils.handler_utils.get_pull_request_lambda_arn") as mock_get_pull_request_lambda_arn:
mock_get_pull_request_lambda_arn.side_effect = Exception("Error getting lambda arn")
with pytest.raises(Exception):
- forward_event_to_pull_request_lambda(
- pull_request_id="123", event=event_data, event_id="evt123", store_pull_request_id=False
+ forward_to_pull_request_lambda(
+ body={},
+ pull_request_id="123",
+ event=event_data,
+ event_id="evt123",
+ store_pull_request_id=False,
+ type="event",
)
# assertions
@@ -149,14 +164,21 @@ def client_side_effect(service_name, *args, **kwargs):
# delete and import module to test
if "app.utils.handler_utils" in sys.modules:
del sys.modules["app.utils.handler_utils"]
- from app.utils.handler_utils import forward_action_to_pull_request_lambda
+ from app.utils.handler_utils import forward_to_pull_request_lambda
# perform operation
mock_body = {"type": "block_actions", "user": {"id": "U123"}, "actions": []}
with patch("app.utils.handler_utils.get_pull_request_lambda_arn") as mock_get_pull_request_lambda_arn:
mock_get_pull_request_lambda_arn.side_effect = Exception("Error getting lambda arn")
with pytest.raises(Exception):
- forward_action_to_pull_request_lambda(pull_request_id="123", body=mock_body)
+ forward_to_pull_request_lambda(
+ pull_request_id="123",
+ body=mock_body,
+ type="action",
+ event=None,
+ event_id="",
+ store_pull_request_id=False,
+ )
# assertions
@@ -183,13 +205,15 @@ def client_side_effect(service_name, *args, **kwargs):
# delete and import module to test
if "app.utils.handler_utils" in sys.modules:
del sys.modules["app.utils.handler_utils"]
- from app.utils.handler_utils import forward_action_to_pull_request_lambda
+ from app.utils.handler_utils import forward_to_pull_request_lambda
# perform operation
mock_body = {"type": "block_actions", "user": {"id": "U123"}, "actions": []}
with patch("app.utils.handler_utils.get_pull_request_lambda_arn", return_value="output_SlackBotLambdaArn"):
- forward_action_to_pull_request_lambda(pull_request_id="123", body=mock_body)
+ forward_to_pull_request_lambda(
+ pull_request_id="123", body=mock_body, type="action", event=None, event_id="", store_pull_request_id=False
+ )
# assertions
expected_lambda_payload = {
@@ -198,6 +222,8 @@ def client_side_effect(service_name, *args, **kwargs):
}
mock_lambda_client.invoke.assert_called_once_with(
- FunctionName="output_SlackBotLambdaArn", InvocationType="Event", Payload=json.dumps(expected_lambda_payload)
+ FunctionName="output_SlackBotLambdaArn",
+ InvocationType="Event",
+ Payload=json.dumps(expected_lambda_payload),
)
mock_store_state_information.assert_not_called()
diff --git a/packages/slackBotFunction/tests/test_slack_events/test_slack_events_actions.py b/packages/slackBotFunction/tests/test_slack_actions.py
similarity index 97%
rename from packages/slackBotFunction/tests/test_slack_events/test_slack_events_actions.py
rename to packages/slackBotFunction/tests/test_slack_actions.py
index 635920e7..a931ca66 100644
--- a/packages/slackBotFunction/tests/test_slack_events/test_slack_events_actions.py
+++ b/packages/slackBotFunction/tests/test_slack_actions.py
@@ -8,9 +8,9 @@ def mock_logger():
return MagicMock()
-@patch("app.utils.handler_utils.forward_event_to_pull_request_lambda")
+@patch("app.utils.handler_utils.forward_to_pull_request_lambda")
def test_process_async_slack_event_feedback(
- mock_forward_event_to_pull_request_lambda: Mock,
+ mock_forward_to_pull_request_lambda: Mock,
mock_get_parameter: Mock,
mock_env: Mock,
):
@@ -34,7 +34,7 @@ def test_process_async_slack_event_feedback(
"app.slack.slack_events.process_slack_message"
) as mock_process_slack_message:
process_async_slack_event(event=slack_event_data, event_id="evt123", client=mock_client)
- mock_forward_event_to_pull_request_lambda.assert_not_called()
+ mock_forward_to_pull_request_lambda.assert_not_called()
mock_process_feedback_event.assert_called_once_with(
message_text="feedback: this is some feedback",
conversation_key="thread#C789#1234567890.123",
diff --git a/packages/slackBotFunction/tests/test_slack_commands.py b/packages/slackBotFunction/tests/test_slack_commands.py
new file mode 100644
index 00000000..cb79d736
--- /dev/null
+++ b/packages/slackBotFunction/tests/test_slack_commands.py
@@ -0,0 +1,235 @@
+import sys
+import pytest
+from unittest.mock import Mock, patch, MagicMock
+
+
+@pytest.fixture
+def mock_logger():
+ return MagicMock()
+
+
+@patch("app.utils.handler_utils.forward_to_pull_request_lambda")
+def test_process_slack_command(
+ mock_forward_to_pull_request_lambda: Mock,
+):
+ """Test successful command processing"""
+ # set up mocks
+ mock_client = Mock()
+
+ # delete and import module to test
+ if "app.slack.slack_events" in sys.modules:
+ del sys.modules["app.slack.slack_events"]
+ from app.slack.slack_events import process_async_slack_command
+
+ # perform operation
+ slack_command_data = {
+ "text": "",
+ "user": "U456",
+ "channel": "C789",
+ "ts": "1234567890.123",
+ "command": "/test",
+ }
+ with patch("app.slack.slack_events.process_command_test_response") as mock_process_command_test_response, patch(
+ "app.slack.slack_events.process_slack_message"
+ ) as mock_process_slack_message, patch(
+ "app.slack.slack_events.process_async_slack_event"
+ ) as mock_process_async_slack_event:
+ process_async_slack_command(command=slack_command_data, client=mock_client)
+ mock_forward_to_pull_request_lambda.assert_not_called()
+ mock_process_command_test_response.assert_called_once_with(
+ command={**slack_command_data},
+ client=mock_client,
+ )
+ mock_process_slack_message.assert_not_called()
+ mock_process_async_slack_event.assert_not_called()
+
+
+@patch("app.services.ai_processor.process_ai_query")
+def test_process_slack_command_test_questions_default(
+ mock_process_ai_query: Mock,
+):
+ """Test successful command processing"""
+ # set up mocks
+ mock_client = Mock()
+
+ # delete and import module to test
+ if "app.slack.slack_events" in sys.modules:
+ del sys.modules["app.slack.slack_events"]
+ from app.slack.slack_events import process_async_slack_command
+
+ # perform operation
+ slack_command_data = {
+ "text": "",
+ "user": "U456",
+ "channel_id": "C789",
+ "ts": "1234567890.123",
+ "command": "/test",
+ }
+
+ mock_response = MagicMock()
+ mock_response.data = {}
+ mock_response.get.side_effect = lambda k: {"thread_ts": "1234567890.123456", "channel": "C12345678"}.get(k)
+ mock_client.chat_postMessage.return_value = mock_response
+
+ mock_process_ai_query.return_value = {"text": "ai response", "session_id": None, "citations": [], "kb_response": {}}
+
+ # perform operation
+ process_async_slack_command(command=slack_command_data, client=mock_client)
+
+ # assertions
+ mock_client.chat_postMessage.assert_called()
+ assert (
+ mock_client.chat_postMessage.call_count == 42
+ ) # 21 Tests - Posts once with question information, then replies with answer
+
+
+@patch("app.services.ai_processor.process_ai_query")
+def test_process_slack_command_test_questions_single_question(
+ mock_process_ai_query: Mock,
+):
+ """Test successful command processing"""
+ # set up mocks
+ mock_client = Mock()
+
+ # delete and import module to test
+ if "app.slack.slack_events" in sys.modules:
+ del sys.modules["app.slack.slack_events"]
+ from app.slack.slack_events import process_async_slack_command
+
+ # perform operation
+ slack_command_data = {
+ "text": "q2",
+ "user": "U456",
+ "channel_id": "C789",
+ "ts": "1234567890.123",
+ "command": "/test",
+ }
+
+ mock_response = MagicMock()
+ mock_response.data = {}
+ mock_response.get.side_effect = lambda k: {"thread_ts": "1234567890.123456", "channel": "C12345678"}.get(k)
+ mock_client.chat_postMessage.return_value = mock_response
+
+ mock_process_ai_query.return_value = {"text": "ai response", "session_id": None, "citations": [], "kb_response": {}}
+
+ # perform operation
+ process_async_slack_command(command=slack_command_data, client=mock_client)
+
+ # assertions
+ mock_client.chat_postMessage.assert_called()
+ assert (
+ mock_client.chat_postMessage.call_count == 2
+ ) # 1 Test - Posts once with question information, then replies with answer
+
+
+@patch("app.services.ai_processor.process_ai_query")
+def test_process_slack_command_test_questions_two_questions(
+ mock_process_ai_query: Mock,
+):
+ """Test successful command processing"""
+ # set up mocks
+ mock_client = Mock()
+
+ # delete and import module to test
+ if "app.slack.slack_events" in sys.modules:
+ del sys.modules["app.slack.slack_events"]
+ from app.slack.slack_events import process_async_slack_command
+
+ # perform operation
+ slack_command_data = {
+ "text": "q2-3",
+ "user": "U456",
+ "channel_id": "C789",
+ "ts": "1234567890.123",
+ "command": "/test",
+ }
+
+ mock_client.chat_postMessage.return_value = {}
+
+ mock_process_ai_query.return_value = {"text": "ai response", "session_id": None, "citations": [], "kb_response": {}}
+
+ # perform operation
+ process_async_slack_command(command=slack_command_data, client=mock_client)
+
+ # assertions
+ mock_client.chat_postMessage.assert_called()
+ assert mock_client.chat_postMessage.call_count == 2
+
+
+@patch("app.services.ai_processor.process_ai_query")
+def test_process_slack_command_test_questions_too_many_questions_error(
+ mock_process_ai_query: Mock,
+ mock_logger: Mock,
+):
+ """Test successful command processing"""
+ # set up mocks
+ with patch("app.core.config.get_logger", return_value=mock_logger):
+ mock_client = Mock()
+
+ # delete and import module to test
+ if "app.slack.slack_events" in sys.modules:
+ del sys.modules["app.slack.slack_events"]
+ from app.slack.slack_events import process_async_slack_command
+
+ # perform operation
+ slack_command_data = {
+ "text": "q0-100",
+ "user": "U456",
+ "channel_id": "C789",
+ "ts": "1234567890.123",
+ "command": "/test",
+ }
+
+ mock_client.chat_postMessage.return_value = {}
+
+ mock_process_ai_query.return_value = {
+ "text": "ai response",
+ "session_id": None,
+ "citations": [],
+ "kb_response": {},
+ }
+
+ # with pytest.raises(ValueError, match="'end' must be less than 21"):
+ # perform operation
+ process_async_slack_command(command=slack_command_data, client=mock_client)
+
+ # assertions
+ mock_client.chat_postMessage.asset_not_called()
+
+ mock_logger.error.assert_called_once()
+
+
+@patch("app.services.ai_processor.process_ai_query")
+def test_process_slack_command_test_help(
+ mock_process_ai_query: Mock,
+):
+ """Test successful command processing"""
+ # set up mocks
+ mock_client = Mock()
+
+ # delete and import module to test
+ if "app.slack.slack_events" in sys.modules:
+ del sys.modules["app.slack.slack_events"]
+ from app.slack.slack_events import process_async_slack_command
+
+ # perform operation
+ slack_command_data = {
+ "text": "help",
+ "user": "U456",
+ "channel_id": "C789",
+ "ts": "1234567890.123",
+ "command": "/test",
+ }
+
+ mock_response = MagicMock()
+ mock_response.data = {}
+ mock_client.chat_postMessage.return_value = mock_response
+
+ mock_process_ai_query.return_value = {"text": "ai response", "session_id": None, "citations": [], "kb_response": {}}
+
+ # perform operation
+ process_async_slack_command(command=slack_command_data, client=mock_client)
+
+ # assertions
+ mock_client.chat_meMessage.assert_called_once()
+ mock_client.chat_postMessage.assert_not_called()
diff --git a/packages/slackBotFunction/tests/test_slack_events/test_slack_events_citations.py b/packages/slackBotFunction/tests/test_slack_events/test_slack_events_citations.py
index 8b256e0f..f054b483 100644
--- a/packages/slackBotFunction/tests/test_slack_events/test_slack_events_citations.py
+++ b/packages/slackBotFunction/tests/test_slack_events/test_slack_events_citations.py
@@ -24,6 +24,7 @@ def test_citation_processing(
"""Test block builder is being called correctly"""
# set up mocks
mock_client = Mock()
+ mock_client.chat_postMessage.return_value = {"ts": ""}
mock_process_ai_query.return_value = {
"text": "AI response",
"session_id": "session-123",
@@ -640,6 +641,7 @@ def test_create_citation_logs_citations(
with patch("app.core.config.get_logger", return_value=mock_logger):
# set up mocks
mock_client = Mock()
+ mock_client.chat_postMessage.return_value = {"ts": ""}
raw_citation = "1||This is the Title||This is the excerpt/ citation||0.99"
mock_process_ai_query.return_value = {
"text": "AI response" + "------" + f"{raw_citation}",
diff --git a/packages/slackBotFunction/tests/test_slack_events/test_slack_events_messages.py b/packages/slackBotFunction/tests/test_slack_events/test_slack_events_messages.py
index cd180566..56b673ea 100644
--- a/packages/slackBotFunction/tests/test_slack_events/test_slack_events_messages.py
+++ b/packages/slackBotFunction/tests/test_slack_events/test_slack_events_messages.py
@@ -8,9 +8,9 @@ def mock_logger():
return MagicMock()
-@patch("app.utils.handler_utils.forward_event_to_pull_request_lambda")
+@patch("app.utils.handler_utils.forward_to_pull_request_lambda")
def test_process_async_slack_event_normal_message(
- mock_forward_event_to_pull_request_lambda: Mock,
+ mock_forward_to_pull_request_lambda: Mock,
mock_get_parameter: Mock,
mock_env: Mock,
):
@@ -29,16 +29,16 @@ def test_process_async_slack_event_normal_message(
"app.slack.slack_events.process_slack_message"
) as mock_process_slack_message:
process_async_slack_event(event=slack_event_data, event_id="evt123", client=mock_client)
- mock_forward_event_to_pull_request_lambda.assert_not_called()
+ mock_forward_to_pull_request_lambda.assert_not_called()
mock_process_feedback_event.assert_not_called()
mock_process_slack_message.assert_called_once_with(
event=slack_event_data, event_id="evt123", client=mock_client
)
-@patch("app.utils.handler_utils.forward_event_to_pull_request_lambda")
+@patch("app.utils.handler_utils.forward_to_pull_request_lambda")
def test_process_async_slack_event_pull_request_with_mention(
- mock_forward_event_to_pull_request_lambda: Mock,
+ mock_forward_to_pull_request_lambda: Mock,
mock_get_parameter: Mock,
mock_env: Mock,
):
@@ -62,19 +62,21 @@ def test_process_async_slack_event_pull_request_with_mention(
"app.slack.slack_events.process_slack_message"
) as mock_process_slack_message:
process_async_slack_event(event=slack_event_data, event_id="evt123", client=mock_client)
- mock_forward_event_to_pull_request_lambda.assert_called_once_with(
+ mock_forward_to_pull_request_lambda.assert_called_once_with(
+ body={},
pull_request_id="123",
event=slack_event_data,
event_id="evt123",
store_pull_request_id=True,
+ type="event",
)
mock_process_feedback_event.assert_not_called()
mock_process_slack_message.assert_not_called()
-@patch("app.utils.handler_utils.forward_event_to_pull_request_lambda")
+@patch("app.utils.handler_utils.forward_to_pull_request_lambda")
def test_process_async_slack_event_pull_request_with_no_mention(
- mock_forward_event_to_pull_request_lambda: Mock,
+ mock_forward_to_pull_request_lambda: Mock,
mock_get_parameter: Mock,
mock_env: Mock,
):
@@ -98,11 +100,13 @@ def test_process_async_slack_event_pull_request_with_no_mention(
"app.slack.slack_events.process_slack_message"
) as mock_process_slack_message:
process_async_slack_event(event=slack_event_data, event_id="evt123", client=mock_client)
- mock_forward_event_to_pull_request_lambda.assert_called_once_with(
+ mock_forward_to_pull_request_lambda.assert_called_once_with(
+ body={},
pull_request_id="123",
event=slack_event_data,
event_id="evt123",
store_pull_request_id=True,
+ type="event",
)
mock_process_feedback_event.assert_not_called()
mock_process_slack_message.assert_not_called()
@@ -178,7 +182,7 @@ def test_process_slack_message_with_thread_ts(
assert mock_client.chat_postMessage.call_count >= 1
first_call = mock_client.chat_postMessage.call_args_list[0]
assert first_call[1]["thread_ts"] == "1234567888.111"
- assert first_call[1]["text"] == "AI response"
+ assert first_call[1]["text"] == ":spinner:"
@patch("app.services.dynamo.get_state_information")
@@ -194,6 +198,7 @@ def test_regex_text_processing(
"""Test regex text processing functionality within process_async_slack_event"""
# set up mocks
mock_client = Mock()
+ mock_client.chat_postMessage.return_value = {"ts": ""}
mock_process_ai_query.return_value = {
"text": "AI response",
"session_id": "session-123",
@@ -236,6 +241,7 @@ def test_process_slack_message_with_session_storage(
mock_client.chat_update.return_value = {"ok": True}
mock_process_ai_query.return_value = {
"text": "AI response",
+ "ck": "thread#123",
"session_id": "new-session-123",
"citations": [],
"kb_response": {"output": {"text": "AI response"}, "sessionId": "new-session-123"},
@@ -247,7 +253,13 @@ def test_process_slack_message_with_session_storage(
from app.slack.slack_events import process_slack_message
# perform operation
- slack_event_data = {"text": "test question", "user": "U456", "channel": "C789", "ts": "1234567890.123"}
+ slack_event_data = {
+ "text": "test question",
+ "user": "U456",
+ "channel": "C789",
+ "ts": "1234567890.123",
+ "event_ts": "123",
+ }
process_slack_message(event=slack_event_data, event_id="evt123", client=mock_client)
diff --git a/packages/slackBotFunction/tests/test_slack_handlers.py b/packages/slackBotFunction/tests/test_slack_handlers.py
index c4dfa9ef..3844fce1 100644
--- a/packages/slackBotFunction/tests/test_slack_handlers.py
+++ b/packages/slackBotFunction/tests/test_slack_handlers.py
@@ -5,11 +5,11 @@
# unified message handler
@patch("app.slack.slack_events.process_async_slack_event")
@patch("app.utils.handler_utils.extract_session_pull_request_id")
-@patch("app.utils.handler_utils.forward_event_to_pull_request_lambda")
+@patch("app.utils.handler_utils.forward_to_pull_request_lambda")
@patch("app.utils.handler_utils.gate_common")
def test_unified_message_handler_successful_call(
mock_gate_common: Mock,
- mock_forward_event_to_pull_request_lambda: Mock,
+ mock_forward_to_pull_request_lambda: Mock,
mock_extract_session_pull_request_id: Mock,
mock_process_async_slack_event: Mock,
mock_get_parameter: Mock,
@@ -39,16 +39,16 @@ def test_unified_message_handler_successful_call(
# assertions
mock_process_async_slack_event.assert_called_once_with(event=mock_event, event_id="evt123", client=mock_client)
- mock_forward_event_to_pull_request_lambda.assert_not_called()
+ mock_forward_to_pull_request_lambda.assert_not_called()
@patch("app.slack.slack_events.process_async_slack_event")
@patch("app.utils.handler_utils.extract_session_pull_request_id")
-@patch("app.utils.handler_utils.forward_event_to_pull_request_lambda")
+@patch("app.utils.handler_utils.forward_to_pull_request_lambda")
@patch("app.utils.handler_utils.gate_common")
def test_unified_message_handler_messages_with_no_thread_are_dropped(
mock_gate_common: Mock,
- mock_forward_event_to_pull_request_lambda: Mock,
+ mock_forward_to_pull_request_lambda: Mock,
mock_extract_session_pull_request_id: Mock,
mock_process_async_slack_event: Mock,
mock_get_parameter: Mock,
@@ -78,16 +78,16 @@ def test_unified_message_handler_messages_with_no_thread_are_dropped(
# assertions
mock_process_async_slack_event.assert_not_called()
- mock_forward_event_to_pull_request_lambda.assert_not_called()
+ mock_forward_to_pull_request_lambda.assert_not_called()
@patch("app.slack.slack_events.process_async_slack_event")
@patch("app.utils.handler_utils.extract_session_pull_request_id")
-@patch("app.utils.handler_utils.forward_event_to_pull_request_lambda")
+@patch("app.utils.handler_utils.forward_to_pull_request_lambda")
@patch("app.utils.handler_utils.gate_common")
def test_unified_message_handler_pull_request_call(
mock_gate_common: Mock,
- mock_forward_event_to_pull_request_lambda: Mock,
+ mock_forward_to_pull_request_lambda: Mock,
mock_extract_session_pull_request_id: Mock,
mock_process_async_slack_event: Mock,
mock_get_parameter: Mock,
@@ -116,8 +116,13 @@ def test_unified_message_handler_pull_request_call(
unified_message_handler(event=mock_event, body=mock_body, client=mock_client)
# assertions
- mock_forward_event_to_pull_request_lambda.assert_called_once_with(
- event=mock_event, pull_request_id="123", event_id="evt123", store_pull_request_id=False
+ mock_forward_to_pull_request_lambda.assert_called_once_with(
+ body=mock_body,
+ event=mock_event,
+ pull_request_id="123",
+ event_id="evt123",
+ store_pull_request_id=False,
+ type="event",
)
mock_process_async_slack_event.assert_not_called()
@@ -125,9 +130,9 @@ def test_unified_message_handler_pull_request_call(
# feedback action handler
@patch("app.slack.slack_events.process_async_slack_action")
@patch("app.utils.handler_utils.extract_session_pull_request_id")
-@patch("app.utils.handler_utils.forward_action_to_pull_request_lambda")
+@patch("app.utils.handler_utils.forward_to_pull_request_lambda")
def test_feedback_handler_success(
- mock_forward_action_to_pull_request_lambda: Mock,
+ mock_forward_to_pull_request_lambda: Mock,
mock_extract_session_pull_request_id: Mock,
mock_process_async_slack_action: Mock,
mock_get_parameter: Mock,
@@ -160,14 +165,14 @@ def test_feedback_handler_success(
body=mock_body,
client=mock_client,
)
- mock_forward_action_to_pull_request_lambda.assert_not_called()
+ mock_forward_to_pull_request_lambda.assert_not_called()
@patch("app.slack.slack_events.process_async_slack_action")
@patch("app.utils.handler_utils.extract_session_pull_request_id")
-@patch("app.utils.handler_utils.forward_action_to_pull_request_lambda")
+@patch("app.utils.handler_utils.forward_to_pull_request_lambda")
def test_feedback_handler_pull_request(
- mock_forward_action_to_pull_request_lambda: Mock,
+ mock_forward_to_pull_request_lambda: Mock,
mock_extract_session_pull_request_id: Mock,
mock_process_async_slack_action: Mock,
mock_get_parameter: Mock,
@@ -196,18 +201,22 @@ def test_feedback_handler_pull_request(
feedback_handler(body=mock_body, client=mock_client)
# assertions
- mock_forward_action_to_pull_request_lambda.assert_called_once_with(
+ mock_forward_to_pull_request_lambda.assert_called_once_with(
body=mock_body,
pull_request_id="123",
+ type="action",
+ event=None,
+ event_id="",
+ store_pull_request_id=False,
)
mock_process_async_slack_action.assert_not_called()
@patch("app.slack.slack_events.open_citation")
@patch("app.utils.handler_utils.extract_session_pull_request_id")
-@patch("app.utils.handler_utils.forward_action_to_pull_request_lambda")
+@patch("app.utils.handler_utils.forward_to_pull_request_lambda")
def test_citation(
- mock_forward_action_to_pull_request_lambda: Mock,
+ mock_forward_to_pull_request_lambda: Mock,
mock_extract_session_pull_request_id: Mock,
mock_open_citation: Mock,
mock_get_parameter: Mock,
@@ -241,4 +250,4 @@ def test_citation(
# assertions
mock_open_citation.assert_called_once()
- mock_forward_action_to_pull_request_lambda.assert_not_called()
+ mock_forward_to_pull_request_lambda.assert_not_called()