diff --git a/packages/cdk/prompts/systemPrompt.txt b/packages/cdk/prompts/systemPrompt.txt
index 6e79e4c7..94f7d3b4 100644
--- a/packages/cdk/prompts/systemPrompt.txt
+++ b/packages/cdk/prompts/systemPrompt.txt
@@ -1,49 +1,89 @@
-You are an AI assistant designed to provide guidance and references from your knowledge base to help users make decisions when onboarding. It is *VERY* important you return *ALL* references, for user examination.
-
-# Response
-## Response Structure
-- *Summary*: 100 characters maximum, capturing core answer
-- *Answer* (use "mrkdown") (< 800 characters)
-- Page break (use `------`)
-- \[Bibliography\]
-
-## Formatting ("mrkdwn")
- a. *Bold* for:
- - Headings, subheadings: *Answer:*, *Bibliography:*
- - Source names: *NHS England*, *EPS*
- b. _Italic_ for:
- - Citations, references, document titles
- c. Block Quotes for:
- - Direct quotes >1 sentence
- - Technical specifications, parameters
- - Examples
- d. `Inline code` for:
- - System names, field names: `PrescriptionID`
- - Short technical terms: `HL7 FHIR`
- e. Links:
- - Do not provide links
-
-# Thinking
-## Question Handling
-- Detect whether the query contains one or multiple questions
-- Split complex queries into individual sub-questions
-- Identify question type: factual, procedural, diagnostic, troubleshooting, or clarification-seeking
-- For multi-question queries: number sub-questions clearly (Q1, Q2, etc)
-
-## RAG & Knowledge Base Integration
-- Relevance threshold handling:
- - Score > 0.85 (High confidence)
- - Score 0.70 - 0.85 (Medium confidence)
- - Score < 0.70 (Low confidence)
-
-## Corrections
-- Change _National Health Service Digital (NHSD)_ references to _National Health Service England (NHSE)_
-
-# Bibliography
-## Format
-source number||summary title||link||filename||text snippet||reasoning\n
-
-## Requirements
-- Return **ALL** retrieved documents, their name and a text snippet, from "CONTEXT"
-- Get full text references from search results for Bibliography
-- Title should be less than 50 characters
+# 1. Persona
+You are an AI assistant designed to provide guidance and references from your knowledge base to help users make decisions during onboarding.
+
+It is **VERY** important that you return **ALL** references found in the context for user examination.
+
+---
+
+# 2. THINKING PROCESS & LOGIC
+Before generating a response, adhere to these processing rules:
+
+## A. Context Verification
+Scan the retrieved context for the specific answer
+1. **No information found**: If the information is not present in the context:
+ - Do NOT formulate a general answer.
+ - Do NOT user external resources (i.e., websites, etc) to get an answer.
+ - Do NOT infer an answer from the users question.
+
+## B. Question Analysis
+1. **Detection:** Determine if the query contains one or multiple questions.
+2. **Decomposition:** Split complex queries into individual sub-questions.
+3. **Classification:** Identify if the question is Factual, Procedural, Diagnostic, Troubleshooting, or Clarification-seeking.
+4. **Multi-Question Strategy:** Number sub-questions clearly (Q1, Q2, etc).
+5. **No Information:** If there is no information supporting an answer to the query, do not try and fill in the information
+6. **Strictness:** Do not infer information, be strict on evidence.
+
+## C. Entity Correction
+- If you encounter "National Health Service Digital (NHSD)", automatically treat and output it as **"National Health Service England (NHSE)"**.
+
+## D. RAG Confidence Scoring
+```
+Evaluate retrieved context using these relevance score thresholds:
+- `Score > 0.9` : **Diamond** (Definitive source)
+- `Score 0.8 - 0.9` : **Gold** (Strong evidence)
+- `Score 0.7 - 0.8` : **Silver** (Partial context)
+- `Score 0.6 - 0.7` : **Bronze** (Weak relevance)
+- `Score < 0.6` : **Scrap** (Ignore completely)
+```
+
+---
+
+# 3. OUTPUT STRUCTURE
+Construct your response in this exact order:
+
+1. **Summary:** A concise overview (Maximum **100 characters**).
+2. **Answer:** The core response using the specific "mrkdwn" styling defined below (Maximum **800 characters**).
+3. **Separator:** A literal line break using `------`.
+4. **Bibliography:** The list of all sources used.
+
+---
+
+# 4. FORMATTING RULES ("mrkdwn")
+You must use a specific variation of markdown. Follow this table strictly:
+
+| Element | Style to Use | Example |
+| :--- | :--- | :--- |
+| **Headings / Subheadings** | Bold (`*`) | `*Answer:*`, `*Bibliography:*` |
+| **Source Names** | Bold (`*`) | `*NHS England*`, `*EPS*` |
+| **Citations / Titles** | Italic (`_`) | `_Guidance Doc v1_` |
+| **Quotes (>1 sentence)** | Blockquote (`>`) | `> text` |
+| **Tech Specs / Examples** | Blockquote (`>`) | `> param: value` |
+| **System / Field Names** | Inline Code (`` ` ``) | `` `PrescriptionID` `` |
+| **Technical Terms** | Inline Code (`` ` ``) | `` `HL7 FHIR` `` |
+| **Hyperlinks** | **NONE** | Do not output any URLs. |
+
+---
+
+# 5. BIBLIOGRAPHY GENERATOR
+**Requirements:**
+- Return **ALL** retrieved documents from the context.
+- Title length must be **< 50 characters**.
+- Use the exact string format below (do not render it as a table or list).
+
+**Template:**
+```text
+source number||summary title||excerpt||relevance score||source name
+
+# 6. Example
+"""
+*Summary*
+Short summary text
+
+* Answer *
+A longer answer, going into more detail gained from the knowledge base and using critical thinking.
+
+------
+1||A document||This is the precise snippet of the pdf file which answers the question.||0.98||very_helpful_doc.pdf
+2||Another file||A 500 word text excerpt which gives some inference to the answer, but the long citation helps fill in the information for the user, so it's worth the tokens.||0.76||something_interesting.txt
+3||A useless file||This file doesn't contain anything that useful||0.05||folder/another/some_file.txt
+"""
diff --git a/packages/cdk/prompts/userPrompt.txt b/packages/cdk/prompts/userPrompt.txt
index e7ae7f18..f54a7c2c 100644
--- a/packages/cdk/prompts/userPrompt.txt
+++ b/packages/cdk/prompts/userPrompt.txt
@@ -1,6 +1,4 @@
-# QUERY
-{{user_query}}
+{{user_query}}
# CONTEXT
-## Results $search_results$
-## LIST ALL RESULTS IN TABLE
+$search_results$
diff --git a/packages/cdk/resources/BedrockPromptResources.ts b/packages/cdk/resources/BedrockPromptResources.ts
index c16b3a07..73a8fa2c 100644
--- a/packages/cdk/resources/BedrockPromptResources.ts
+++ b/packages/cdk/resources/BedrockPromptResources.ts
@@ -20,14 +20,12 @@ export class BedrockPromptResources extends Construct {
constructor(scope: Construct, id: string, props: BedrockPromptResourcesProps) {
super(scope, id)
- // Nova Pro is recommended for text generation tasks requiring high accuracy and complex understanding.
- const novaProModel = BedrockFoundationModel.AMAZON_NOVA_PRO_V1
- // Nova Lite is recommended for tasks
- const novaLiteModel = BedrockFoundationModel.AMAZON_NOVA_LITE_V1
+ const ragModel = new BedrockFoundationModel("meta.llama3-70b-instruct-v1:0")
+ const reformulationModel = BedrockFoundationModel.AMAZON_NOVA_LITE_V1
const queryReformulationPromptVariant = PromptVariant.text({
variantName: "default",
- model: novaLiteModel,
+ model: reformulationModel,
promptVariables: ["topic"],
promptText: props.settings.reformulationPrompt.text
})
@@ -41,7 +39,7 @@ export class BedrockPromptResources extends Construct {
const ragResponsePromptVariant = PromptVariant.chat({
variantName: "default",
- model: novaProModel,
+ model: ragModel,
promptVariables: ["query", "search_results"],
system: props.settings.systemPrompt.text,
messages: [props.settings.userPrompt]
@@ -59,8 +57,8 @@ export class BedrockPromptResources extends Construct {
})
// expose model IDs for use in Lambda environment variables
- this.ragModelId = novaProModel.modelId
- this.queryReformulationModelId = novaLiteModel.modelId
+ this.ragModelId = ragModel.modelId
+ this.queryReformulationModelId = reformulationModel.modelId
this.queryReformulationPrompt = queryReformulationPrompt
this.ragResponsePrompt = ragPrompt
diff --git a/packages/slackBotFunction/app/services/bedrock.py b/packages/slackBotFunction/app/services/bedrock.py
index 218826a4..44d02019 100644
--- a/packages/slackBotFunction/app/services/bedrock.py
+++ b/packages/slackBotFunction/app/services/bedrock.py
@@ -42,8 +42,10 @@ def query_bedrock(user_query: str, session_id: str = None) -> RetrieveAndGenerat
"type": "KNOWLEDGE_BASE",
"knowledgeBaseConfiguration": {
"knowledgeBaseId": config.KNOWLEDGEBASE_ID,
- "modelArn": config.RAG_MODEL_ID,
- "retrievalConfiguration": {"vectorSearchConfiguration": {"numberOfResults": 5}},
+ "modelArn": prompt_template.get("model_id", config.RAG_MODEL_ID),
+ "retrievalConfiguration": {
+ "vectorSearchConfiguration": {"numberOfResults": 5, "overrideSearchType": "SEMANTIC"}
+ },
"generationConfiguration": {
"guardrailConfiguration": {
"guardrailId": config.GUARD_RAIL_ID,
@@ -58,16 +60,6 @@ def query_bedrock(user_query: str, session_id: str = None) -> RetrieveAndGenerat
}
},
},
- "orchestrationConfiguration": {
- "inferenceConfig": {
- "textInferenceConfig": {
- **inference_config,
- "stopSequences": [
- "Human:",
- ],
- }
- },
- },
},
},
}
@@ -87,6 +79,7 @@ def query_bedrock(user_query: str, session_id: str = None) -> RetrieveAndGenerat
else:
logger.info("Starting new conversation")
+ logger.debug("Retrieve and Generate", extra={"params": request_params})
response = client.retrieve_and_generate(**request_params)
logger.info(
"Got Bedrock response",
@@ -100,10 +93,8 @@ def invoke_model(prompt: str, model_id: str, client: BedrockRuntimeClient, infer
modelId=model_id,
body=json.dumps(
{
- "anthropic_version": "bedrock-2023-05-31",
"temperature": inference_config["temperature"],
"top_p": inference_config["topP"],
- "top_k": 50,
"max_tokens": inference_config["maxTokens"],
"messages": [{"role": "user", "content": prompt}],
}
diff --git a/packages/slackBotFunction/app/services/prompt_loader.py b/packages/slackBotFunction/app/services/prompt_loader.py
index e951d447..10d941fb 100644
--- a/packages/slackBotFunction/app/services/prompt_loader.py
+++ b/packages/slackBotFunction/app/services/prompt_loader.py
@@ -92,7 +92,7 @@ def load_prompt(prompt_name: str, prompt_version: str = None) -> dict:
logger.info(
f"Loading prompt {prompt_name}' (ID: {prompt_id})",
- extra={"prompt_name": prompt_name, "prompt_id": prompt_id, "prompt_version": prompt_version},
+ extra={"prompt_version": prompt_version},
)
if is_explicit_version:
@@ -100,15 +100,20 @@ def load_prompt(prompt_name: str, prompt_version: str = None) -> dict:
else:
response = client.get_prompt(promptIdentifier=prompt_id)
+ logger.info("Prompt Found", extra={"prompt": response})
+
+ variant = response["variants"][0]
+
# Extract and render the prompt template
- template_config = response["variants"][0]["templateConfiguration"]
+ template_config = variant["templateConfiguration"]
prompt_text = _render_prompt(template_config)
actual_version = response.get("version", "DRAFT")
# Extract inference configuration with defaults
default_inference = {"temperature": 0, "topP": 1, "maxTokens": 1500}
- raw_inference = response["variants"][0].get("inferenceConfiguration", {})
- raw_text_config = raw_inference.get("textInferenceConfiguration", {})
+ model_id = variant.get("modelId", "")
+ raw_inference = variant.get("inferenceConfiguration", {})
+ raw_text_config = raw_inference.get("text", {})
inference_config = {**default_inference, **raw_text_config}
logger.info(
@@ -117,10 +122,11 @@ def load_prompt(prompt_name: str, prompt_version: str = None) -> dict:
"prompt_name": prompt_name,
"prompt_id": prompt_id,
"version_used": actual_version,
+ "model_id": model_id,
**inference_config,
},
)
- return {"prompt_text": prompt_text, "inference_config": inference_config}
+ return {"prompt_text": prompt_text, "model_id": model_id, "inference_config": inference_config}
except ClientError as e:
error_code = e.response.get("Error", {}).get("Code", "Unknown")
diff --git a/packages/slackBotFunction/app/slack/slack_events.py b/packages/slackBotFunction/app/slack/slack_events.py
index 458107cc..84164c64 100644
--- a/packages/slackBotFunction/app/slack/slack_events.py
+++ b/packages/slackBotFunction/app/slack/slack_events.py
@@ -199,10 +199,11 @@ def _create_response_body(citations: list[dict[str, str]], feedback_data: dict[s
for i, citation in enumerate(citations):
result = _create_citation(citation, feedback_data, response_text)
- action_buttons.append(result[0])
- response_text = result[1]
+ action_buttons += result.get("action_buttons", [])
+ response_text = result.get("response_text", response_text)
# Remove any citations that have not been returned
+ response_text = convert_markdown_to_slack(response_text)
response_text = response_text.replace("cit_", "")
# Main body
@@ -222,45 +223,68 @@ def _create_response_body(citations: list[dict[str, str]], feedback_data: dict[s
def _create_citation(citation: dict[str, str], feedback_data: dict, response_text: str):
- logger.info("Creating citation", extra={"Citation": citation})
invalid_body = "No document excerpt available."
action_buttons = []
- # Create citation blocks ["sourceNumber", "title", "link", "filename", "reference_text"]
- title = citation.get("title") or citation.get("filename") or "Source"
- body = citation.get("reference_text") or invalid_body
- citation_link = citation.get("link") or ""
- source_number = (citation.get("source_number", "0")).replace("\n", "")
-
- # Buttons can only be 75 characters long, truncate to be safe
- button_text = f"[{source_number}] {title}"
- button = {
- "type": "button",
- "text": {
- "type": "plain_text",
- "text": button_text if len(button_text) < 75 else f"{button_text[:70]}...",
- },
- "action_id": f"cite_{source_number}",
- "value": json.dumps(
- {
- **feedback_data,
- "source_number": source_number,
- "title": title,
- "body": body,
- "link": citation_link,
+ # Create citation blocks ["source_number", "title", "excerpt", "relevance_score"]
+ source_number: str = (citation.get("source_number", "0")).replace("\n", "")
+ title: str = citation.get("title") or citation.get("filename") or "Source"
+ body: str = citation.get("excerpt") or invalid_body
+ score: float = float(citation.get("relevance_score") or "0")
+
+ # Format body
+ body = convert_markdown_to_slack(body)
+
+ if score < 0.6: # low relevance score, skip citation
+ logger.info("Skipping low relevance citation", extra={"source_number": source_number, "score": score})
+ else:
+ # Buttons can only be 75 characters long, truncate to be safe
+ button_text = f"[{source_number}] {title}"
+ button_value = {**feedback_data, "source_number": source_number, "title": title, "body": body, "score": score}
+ button = {
+ "type": "button",
+ "text": {
+ "type": "plain_text",
+ "text": button_text if len(button_text) < 75 else f"{button_text[:70]}...",
},
- separators=(",", ":"),
- ),
- }
- action_buttons.append(button)
+ "action_id": f"cite_{source_number}",
+ "value": json.dumps(
+ button_value,
+ separators=(",", ":"),
+ ),
+ }
+ action_buttons.append(button)
- # Update inline citations
- response_text = response_text.replace(
- f"[cit_{source_number}]",
- f"<{citation_link}|[{source_number}]>" if citation_link else f"[{source_number}]",
- )
+ # Update inline citations to remove "cit_" prefix
+ response_text = response_text.replace(f"[cit_{source_number}]", f"[{source_number}]")
+ logger.info("Created citation", extra=button_value)
+
+ return {"action_buttons": action_buttons, "response_text": response_text}
+
+
+def convert_markdown_to_slack(body: str) -> str:
+ """Convert basic markdown to Slack formatting"""
+ if not body:
+ return ""
+
+ # 1. Fix common encoding issues
+ body = body.replace("»", "")
+ body = body.replace("â¢", "-")
+
+ # 2. Convert Markdown Italics (*text*) and (__text__) to Slack Italics (_text_)
+ body = re.sub(r"(?
+ body = re.sub(r"\[([^\]]+)\]\(([^\)]+)\)", r"<\2|\1>", body)
+
+ return body.strip()
# ================================================================
@@ -436,14 +460,8 @@ def process_slack_message(event: Dict[str, Any], event_id: str, client: WebClien
# 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",
- "link",
- "filename",
- "reference_text",
- ]
- split = response_text.split("------") # Citations are separated by ------
+ 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:
@@ -652,25 +670,11 @@ def open_citation(channel: str, timestamp: str, message: Any, params: Dict[str,
body = body.replace("»", "") # Remove double chevrons
current_id = f"cite_{source_number}".strip()
- selected = False
# Reset all button styles, then set the clicked one
- for block in blocks:
- if block.get("type") == "actions":
- for element in block.get("elements", []):
- if element.get("type") == "button":
- action_id = element.get("action_id")
- if action_id == current_id:
- # Toggle: if already styled, unselect; else select
- if element.get("style") == "primary":
- element.pop("style", None)
- selected = False
- else:
- element["style"] = "primary"
- selected = True
- else:
- # Unselect all other buttons
- element.pop("style", None)
+ result = format_blocks(blocks, current_id)
+ selected = result["selected"]
+ blocks = result["blocks"]
# If selected, insert citation block before feedback
if selected:
@@ -693,6 +697,36 @@ def open_citation(channel: str, timestamp: str, message: Any, params: Dict[str,
logger.error(f"Error updating message for citation: {e}", extra={"error": traceback.format_exc()})
+def format_blocks(blocks: Any, current_id: str):
+ """Format blocks by styling the selected citation button and unstyle others"""
+ selected = False
+
+ for block in blocks:
+ if block.get("type") != "actions":
+ continue
+
+ for element in block.get("elements", []):
+ if element.get("type") != "button":
+ continue
+
+ if element.get("action_id") == current_id:
+ selected = _toggle_button_style(element)
+ else:
+ element.pop("style", None)
+
+ return {"selected": selected, "blocks": blocks}
+
+
+def _toggle_button_style(element: dict) -> bool:
+ """Toggle button style and return whether it's now selected"""
+ if element.get("style") == "primary":
+ element.pop("style", None)
+ return False
+ else:
+ element["style"] = "primary"
+ return True
+
+
# ================================================================
# Session management
# ================================================================
diff --git a/packages/slackBotFunction/tests/test_slack_events.py b/packages/slackBotFunction/tests/test_slack_events.py
deleted file mode 100644
index a4398249..00000000
--- a/packages/slackBotFunction/tests/test_slack_events.py
+++ /dev/null
@@ -1,868 +0,0 @@
-import sys
-import pytest
-from unittest.mock import Mock, patch, MagicMock, call
-
-
-@pytest.fixture
-def mock_logger():
- return MagicMock()
-
-
-@patch("app.utils.handler_utils.forward_event_to_pull_request_lambda")
-def test_process_async_slack_event_normal_message(
- mock_forward_event_to_pull_request_lambda: Mock,
- mock_get_parameter: Mock,
- mock_env: Mock,
-):
- """Test successful async event 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_event
-
- # perform operation
- slack_event_data = {"text": "<@U123> test question", "user": "U456", "channel": "C789", "ts": "1234567890.123"}
- with patch("app.slack.slack_events.process_feedback_event") as mock_process_feedback_event, patch(
- "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_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")
-def test_process_async_slack_event_pull_request_with_mention(
- mock_forward_event_to_pull_request_lambda: Mock,
- mock_get_parameter: Mock,
- mock_env: Mock,
-):
- """Test successful async event 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_event
-
- # perform operation
- slack_event_data = {
- "text": "<@U123> pr: 123 test question",
- "user": "U456",
- "channel": "C789",
- "ts": "1234567890.123",
- }
- with patch("app.slack.slack_events.process_feedback_event") as mock_process_feedback_event, patch(
- "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(
- pull_request_id="123",
- event=slack_event_data,
- event_id="evt123",
- store_pull_request_id=True,
- )
- 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")
-def test_process_async_slack_event_pull_request_with_no_mention(
- mock_forward_event_to_pull_request_lambda: Mock,
- mock_get_parameter: Mock,
- mock_env: Mock,
-):
- """Test successful async event 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_event
-
- # perform operation
- slack_event_data = {
- "text": "pr: 123 test question",
- "user": "U456",
- "channel": "C789",
- "ts": "1234567890.123",
- }
- with patch("app.slack.slack_events.process_feedback_event") as mock_process_feedback_event, patch(
- "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(
- pull_request_id="123",
- event=slack_event_data,
- event_id="evt123",
- store_pull_request_id=True,
- )
- 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")
-def test_process_async_slack_event_feedback(
- mock_forward_event_to_pull_request_lambda: Mock,
- mock_get_parameter: Mock,
- mock_env: Mock,
-):
- """Test successful async event 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_event
-
- # perform operation
- slack_event_data = {
- "text": "feedback: this is some feedback",
- "user": "U456",
- "channel": "C789",
- "ts": "1234567890.123",
- }
- with patch("app.slack.slack_events.process_feedback_event") as mock_process_feedback_event, patch(
- "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_process_feedback_event.assert_called_once_with(
- message_text="feedback: this is some feedback",
- conversation_key="thread#C789#1234567890.123",
- user_id="U456",
- channel_id="C789",
- thread_root="1234567890.123",
- client=mock_client,
- event=slack_event_data,
- )
- mock_process_slack_message.assert_not_called()
-
-
-def test_process_slack_message_empty_query(mock_get_parameter: Mock, mock_env: Mock):
- """Test async event processing with empty query"""
- # 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_slack_message
-
- # perform operation
- slack_event_data = {
- "text": "<@U123>", # Only mention, no actual query
- "user": "U456",
- "channel": "C789",
- "ts": "1234567890.123",
- }
- process_slack_message(event=slack_event_data, event_id="evt123", client=mock_client)
-
- # assertions
- mock_client.chat_postMessage.assert_called_once_with(
- channel="C789",
- text="Hi there! Please ask me a question and I'll help you find information from our knowledge base.",
- thread_ts="1234567890.123",
- )
-
-
-@patch("app.services.dynamo.get_state_information")
-@patch("app.services.ai_processor.process_ai_query")
-@patch("app.slack.slack_events.get_conversation_session")
-@patch("app.services.slack.post_error_message")
-def test_process_slack_message_event_error(
- mock_post_error_message: Mock,
- mock_get_session: Mock,
- mock_process_ai_query: Mock,
- mock_get_state_information: Mock,
- mock_get_parameter: Mock,
- mock_env: Mock,
-):
- """Test async event processing with error"""
- # set up mocks
- mock_process_ai_query.side_effect = Exception("AI processing error")
- mock_get_session.return_value = None # No existing session
- 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_slack_message
-
- # perform operation
- slack_event_data = {"text": "test question", "user": "U456", "channel": "C789", "ts": "1234567890.123"}
- process_slack_message(event=slack_event_data, event_id="evt123", client=mock_client)
-
- # assertions
- mock_post_error_message.assert_called_once_with(channel="C789", thread_ts="1234567890.123", client=mock_client)
-
-
-@patch("app.services.dynamo.get_state_information")
-@patch("app.services.ai_processor.process_ai_query")
-@patch("app.slack.slack_events.get_conversation_session")
-def test_process_slack_message_with_thread_ts(
- mock_get_session: Mock,
- mock_process_ai_query: Mock,
- mock_get_state_information: Mock,
- mock_get_parameter: Mock,
- mock_env: Mock,
-):
- """Test async event processing with existing thread_ts"""
- # set up mocks
- mock_client = Mock()
- mock_client.chat_postMessage.return_value = {"ts": "1234567890.124"}
- mock_client.chat_update.return_value = {"ok": True}
- mock_process_ai_query.return_value = {
- "text": "AI response",
- "session_id": "session-123",
- "citations": [],
- "kb_response": {"output": {"text": "AI response"}},
- }
- mock_get_session.return_value = None # No existing session
-
- # 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_slack_message
-
- # perform operation
- slack_event_data = {
- "text": "<@U123> test question",
- "user": "U456",
- "channel": "C789",
- "ts": "1234567890.123",
- "thread_ts": "1234567888.111", # Existing thread
- }
- process_slack_message(event=slack_event_data, event_id="evt123", client=mock_client)
-
- # assertions
- # Should be called at least once with the correct 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"
-
-
-@patch("app.services.dynamo.get_state_information")
-@patch("app.services.ai_processor.process_ai_query")
-@patch("app.slack.slack_events.get_conversation_session")
-def test_regex_text_processing(
- mock_get_session: Mock,
- mock_process_ai_query: Mock,
- mock_get_state_information: Mock,
- mock_get_parameter: Mock,
- mock_env: Mock,
-):
- """Test regex text processing functionality within process_async_slack_event"""
- # set up mocks
- mock_client = Mock()
- mock_process_ai_query.return_value = {
- "text": "AI response",
- "session_id": "session-123",
- "citations": [],
- "kb_response": {"output": {"text": "AI response"}},
- }
- mock_get_session.return_value = None # No existing session
-
- # 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_slack_message
-
- # perform operation
- slack_event_data = {"text": "<@U123456> test question", "user": "U456", "channel": "C789", "ts": "1234567890.123"}
-
- process_slack_message(event=slack_event_data, event_id="evt123", client=mock_client)
-
- # assertions
- # Verify that the message was processed (process_ai_query was called)
- mock_process_ai_query.assert_called_once()
- # The actual regex processing happens inside the function
- assert mock_client.chat_postMessage.called
-
-
-@patch("app.services.dynamo.get_state_information")
-@patch("app.services.ai_processor.process_ai_query")
-@patch("app.slack.slack_events.get_conversation_session")
-@patch("app.slack.slack_events._create_feedback_blocks")
-def test_citation_processing(
- mock_get_session: Mock,
- mock_process_ai_query: Mock,
- mock_create_feedback_blocks: Mock,
- mock_get_state_information: Mock,
- mock_get_parameter: Mock,
- mock_env: Mock,
-):
- """Test block builder is being called correctly"""
- # set up mocks
- mock_client = Mock()
- mock_process_ai_query.return_value = {
- "text": "AI response",
- "session_id": "session-123",
- "citations": [],
- "kb_response": {"output": {"text": "AI response"}},
- }
- mock_get_session.return_value = None # No existing session
-
- # 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_slack_message
-
- # perform operation
- slack_event_data = {
- "text": "Answer",
- "user": "U456",
- "channel": "C789",
- "ts": "1234567890.123",
- }
-
- process_slack_message(event=slack_event_data, event_id="evt123", client=mock_client)
-
- # assertions
- # Verify that the message was processed (process_ai_query was called)
- mock_create_feedback_blocks.assert_called_once()
-
-
-@patch("app.services.dynamo.get_state_information")
-@patch("app.services.ai_processor.process_ai_query")
-@patch("app.slack.slack_events.get_conversation_session")
-@patch("app.slack.slack_events._create_feedback_blocks")
-def test_citation_logging(
- mock_get_session: Mock,
- mock_create_feedback_blocks: Mock,
- mock_process_ai_query: Mock,
- mock_get_state_information: Mock,
- mock_get_parameter: Mock,
- mock_env: Mock,
- mock_logger,
-):
- """Test block builder is being called correctly"""
- with patch("app.core.config.get_logger", return_value=mock_logger):
- # set up mocks
- mock_client = Mock()
- mock_process_ai_query.return_value = {
- "text": "AI response\n------\ntest",
- "session_id": "session-123",
- "citations": [],
- "kb_response": {"output": {"text": "AI response"}},
- }
- mock_get_session.return_value = None # No existing session
-
- # 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_slack_message
-
- # perform operation
- slack_event_data = {
- "text": "AI response",
- "user": "U456",
- "channel": "C789",
- "ts": "1234567890.123",
- }
-
- process_slack_message(event=slack_event_data, event_id="evt123", client=mock_client)
-
- # assertions
- mock_logger.info.assert_has_calls(
- [
- call(
- "Processing message from user U456",
- extra={
- "user_query": "AI response",
- "conversation_key": "thread#C789#1234567890.123",
- "event_id": "evt123",
- },
- ),
- # Found citations to split
- call("Found citation(s)", extra={"Raw Citations": ["test"]}),
- # Citations parsed correctly
- call("Parsed citation(s)", extra={"citations": [{"source_number": "test"}]}),
- ]
- )
-
-
-@patch("app.services.dynamo.get_state_information")
-def test_citation_creation(
- mock_get_state_information: Mock,
- mock_get_parameter: Mock,
- mock_env: Mock,
-):
- """Test citations are being added via Slack blocks correctly"""
- # set up mocks
- # 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 _create_feedback_blocks
-
- _sourceNumber = "5"
- _title = "Some Title Summarising the Document"
- _link = "http://example.com"
- _filename = "example.pdf"
- _text_snippet = "This is some example text, maybe something about NHSE"
-
- result = _create_feedback_blocks(
- response_text="Answer",
- citations=[
- {
- "source_number": _sourceNumber,
- "title": _title,
- "link": _link,
- "filename": _filename,
- "reference_text": _text_snippet,
- }
- ],
- conversation_key="12345",
- channel="C789",
- message_ts="123",
- thread_ts="123",
- )
-
- # assertions
- # Verify that the citation button was added
- citation_section = result[1]
- assert citation_section is not None
-
- # Verify button is correct
- assert citation_section["type"] == "actions"
- assert citation_section["block_id"] == "citation_actions"
- assert citation_section["elements"] and len(citation_section["elements"]) > 0
-
- # Verify that the citation data is correct
- citation_button = citation_section["elements"][0]
- assert citation_button is not None
-
- assert citation_button["type"] == "button"
- assert citation_button["text"]["text"] == f"[{_sourceNumber}] {_title}"
-
- assert f'"source_number":"{_sourceNumber}"' in citation_button["value"]
- assert f'"title":"{_title}"' in citation_button["value"]
- assert f'"body":"{_text_snippet}"' in citation_button["value"]
- assert f'"link":"{_link}"' in citation_button["value"]
-
-
-@patch("app.services.dynamo.get_state_information")
-def test_citation_creation_defaults(
- mock_get_state_information: Mock,
- mock_get_parameter: Mock,
- mock_env: Mock,
-):
- """Test regex text processing functionality within process_async_slack_event"""
- # set up mocks
- # 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 _create_feedback_blocks
-
- result = _create_feedback_blocks(
- response_text="Answer",
- citations=[{}], # Pass in empty object
- conversation_key="12345",
- channel="C789",
- message_ts="123",
- thread_ts="123",
- )
-
- # assertions
- # Verify that the citation button was added
- citation_section = result[1]
- assert citation_section is not None
-
- # Verify that the citation data is correct
- citation_button = citation_section["elements"][0]
- assert citation_button is not None
-
- assert citation_button["type"] == "button"
- assert citation_button["text"]["text"] == "[0] Source"
-
- assert '"source_number":"0"' in citation_button["value"]
- assert '"title":"Source"' in citation_button["value"]
- assert '"body":"No document excerpt available."' in citation_button["value"]
- assert '"link":""' in citation_button["value"]
-
-
-@patch("app.services.dynamo.get_state_information")
-def test_response_handle_links(
- mock_get_state_information: Mock,
- mock_get_parameter: Mock,
- mock_env: Mock,
-):
- """Test regex text processing citation links in response body"""
- # set up mocks
- # 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 _create_feedback_blocks
-
- result = _create_feedback_blocks(
- response_text="[cit_0]",
- citations=[
- {
- "source_number": "0",
- "link": "https://example.com",
- }
- ],
- conversation_key="12345",
- channel="C789",
- message_ts="123",
- thread_ts="123",
- )
-
- # assertions
- # Verify links in the body are changed to slack links
- citation_section = result[0]
- assert citation_section is not None
-
- assert "" in citation_section["text"]["text"]
-
-
-@patch("app.services.dynamo.get_state_information")
-@patch("app.services.dynamo.store_state_information")
-@patch("app.services.ai_processor.process_ai_query")
-def test_process_slack_message_with_session_storage(
- mock_process_ai_query: Mock,
- mock_store_state_information: Mock,
- mock_get_state_information: Mock,
- mock_get_parameter: Mock,
- mock_env: Mock,
-):
- """Test async event processing that stores a new session"""
- # set up mocks
- mock_client = Mock()
- mock_client.chat_postMessage.return_value = {"ts": "1234567890.124"}
- mock_client.chat_update.return_value = {"ok": True}
- mock_process_ai_query.return_value = {
- "text": "AI response",
- "session_id": "new-session-123",
- "citations": [],
- "kb_response": {"output": {"text": "AI response"}, "sessionId": "new-session-123"},
- }
-
- # 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_slack_message
-
- # perform operation
- slack_event_data = {"text": "test question", "user": "U456", "channel": "C789", "ts": "1234567890.123"}
-
- process_slack_message(event=slack_event_data, event_id="evt123", client=mock_client)
-
- # assertions
- # Verify session was stored - should be called twice (Q&A pair + session)
- assert mock_store_state_information.call_count >= 1
-
-
-@patch("app.services.dynamo.get_state_information")
-@patch("app.services.ai_processor.process_ai_query")
-@patch("app.slack.slack_events.get_conversation_session")
-def test_process_slack_message_chat_update_error(
- mock_get_session: Mock,
- mock_process_ai_query: Mock,
- mock_get_state_information: Mock,
- mock_get_parameter: Mock,
- mock_env: Mock,
-):
- """Test process_async_slack_event with chat_update error"""
- # set up mocks
- mock_client = Mock()
- mock_client.chat_postMessage.return_value = {"ts": "1234567890.124"}
- mock_client.chat_update.side_effect = Exception("Update failed")
- mock_process_ai_query.return_value = {
- "text": "AI response",
- "session_id": "session-123",
- "citations": [],
- "kb_response": {"output": {"text": "AI response"}},
- }
- mock_get_session.return_value = None # No existing session
-
- # 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_slack_message
-
- # perform operation
- slack_event_data = {"text": "<@U123> test question", "user": "U456", "channel": "C789", "ts": "1234567890.123"}
- process_slack_message(event=slack_event_data, event_id="evt123", client=mock_client)
-
- # assertions
- # no assertions as we are just checking it does not throw an error
-
-
-@patch("app.services.dynamo.get_state_information")
-@patch("app.services.ai_processor.process_ai_query")
-@patch("app.slack.slack_events.get_conversation_session")
-def test_process_slack_message_dm_context(
- mock_get_session: Mock,
- mock_process_ai_query: Mock,
- mock_get_state_information: Mock,
- mock_get_parameter: Mock,
- mock_env: Mock,
-):
- """Test process_async_slack_event with DM context"""
- # set up mocks
- mock_client = Mock()
- mock_client.chat_postMessage.return_value = {"ts": "123"}
- mock_process_ai_query.return_value = {
- "text": "AI response",
- "session_id": "new-session",
- "citations": [],
- "kb_response": {"output": {"text": "AI response"}, "sessionId": "new-session"},
- }
- mock_get_session.return_value = None
-
- # 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_event
-
- # perform operation
- slack_event_data = {
- "text": "test question",
- "user": "U456",
- "channel": "D789",
- "ts": "123",
- "channel_type": "im", # DM context
- }
- process_async_slack_event(event=slack_event_data, event_id="evt123", client=mock_client)
-
- # assertions
- # no assertions as we are just checking it does not throw an error
-
-
-@patch("app.utils.handler_utils.is_latest_message")
-def test_process_async_slack_action_positive(
- mock_is_latest_message: Mock,
- mock_get_parameter: Mock,
- mock_env: Mock,
-):
- """Test successful async action processing"""
- # set up mocks
- mock_client = Mock()
- mock_is_latest_message.return_value = True
-
- # 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_action
-
- feedback_value = '{"ck":"thread#C123#123","ch":"C123","mt":"1759845126.972219","tt":"1759845114.407989"}'
-
- # perform operation
- slack_action_data = {
- "type": "block_actions",
- "user": {"id": "U123"},
- "channel": {"id": "C123"},
- "message": {"ts": "1759845126.972219"},
- "actions": [{"action_id": "feedback_yes", "value": feedback_value}],
- }
- with patch("app.slack.slack_events.store_feedback") as mock_store_feedback:
- process_async_slack_action(body=slack_action_data, client=mock_client)
-
- # assertions
- mock_store_feedback.assert_called_once_with(
- conversation_key="thread#C123#123",
- feedback_type="positive",
- user_id="U123",
- channel_id="C123",
- thread_ts="1759845114.407989",
- message_ts="1759845126.972219",
- client=mock_client,
- )
- mock_client.chat_postMessage.assert_called_once_with(
- channel="C123",
- text="Thank you for your feedback.",
- thread_ts="1759845114.407989",
- )
-
-
-@patch("app.utils.handler_utils.is_latest_message")
-def test_process_async_slack_action_negative(
- mock_is_latest_message: Mock,
- mock_get_parameter: Mock,
- mock_env: Mock,
-):
- """Test successful async action processing"""
- # set up mocks
- mock_client = Mock()
- mock_is_latest_message.return_value = True
-
- # 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_action
-
- feedback_value = '{"ck":"thread#C123#123","ch":"C123","mt":"1759845126.972219","tt":"1759845114.407989"}'
-
- # perform operation
- slack_action_data = {
- "type": "block_actions",
- "user": {"id": "U123"},
- "channel": {"id": "C123"},
- "message": {"ts": "1759845126.972219"},
- "actions": [{"action_id": "feedback_no", "value": feedback_value}],
- }
- with patch("app.slack.slack_events.store_feedback") as mock_store_feedback:
- process_async_slack_action(body=slack_action_data, client=mock_client)
-
- # assertions
- mock_store_feedback.assert_called_once_with(
- conversation_key="thread#C123#123",
- feedback_type="negative",
- user_id="U123",
- channel_id="C123",
- thread_ts="1759845114.407989",
- message_ts="1759845126.972219",
- client=mock_client,
- )
- mock_client.chat_postMessage.assert_called_once_with(
- channel="C123",
- text='Please let us know how the answer could be improved. Start your message with "feedback:"',
- thread_ts="1759845114.407989",
- )
-
-
-@patch("app.utils.handler_utils.is_latest_message")
-def test_process_async_slack_action_not_latest(
- mock_is_latest_message: Mock,
- mock_get_parameter: Mock,
- mock_env: Mock,
-):
- """Test successful async action processing"""
- # set up mocks
- mock_client = Mock()
- mock_is_latest_message.return_value = False
-
- # 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_action
-
- feedback_value = '{"ck":"thread#C123#123","ch":"C123","mt":"1759845126.972219","tt":"1759845114.407989"}'
-
- # perform operation
- slack_action_data = {
- "type": "block_actions",
- "user": {"id": "U123"},
- "channel": {"id": "C123"},
- "actions": [{"action_id": "feedback_no", "value": feedback_value}],
- }
- with patch("app.slack.slack_events.store_feedback") as mock_store_feedback:
- process_async_slack_action(body=slack_action_data, client=mock_client)
-
- # assertions
- mock_store_feedback.assert_not_called()
- mock_client.chat_postMessage.assert_not_called()
-
-
-@patch("app.utils.handler_utils.is_latest_message")
-def test_process_async_slack_action_unknown_action(
- mock_is_latest_message: Mock,
- mock_get_parameter: Mock,
- mock_env: Mock,
-):
- """Test successful async action processing"""
- # set up mocks
- mock_client = Mock()
- mock_is_latest_message.return_value = True
-
- # 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_action
-
- feedback_value = '{"ck":"thread#C123#123","ch":"C123","mt":"1759845126.972219","tt":"1759845114.407989"}'
-
- # perform operation
- slack_action_data = {
- "type": "block_actions",
- "user": {"id": "U123"},
- "channel": {"id": "C123"},
- "actions": [{"action_id": "I_Do_Not_Know_This_Action", "value": feedback_value}],
- }
- with patch("app.slack.slack_events.store_feedback") as mock_store_feedback:
- process_async_slack_action(body=slack_action_data, client=mock_client)
-
- # assertions
- mock_store_feedback.assert_not_called()
- mock_client.chat_postMessage.assert_not_called()
-
-
-def test_process_feedback_event():
- # 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_feedback_event
-
- # perform operation
- mock_event = {}
- with patch("app.slack.slack_events.store_feedback") as mock_store_feedback:
- process_feedback_event(
- message_text="feedback: this is some feedback",
- conversation_key="thread#C123#123",
- user_id="U123",
- channel_id="C123",
- thread_root="1759845114.407989",
- event=mock_event,
- client=mock_client,
- )
-
- # assertions
- mock_store_feedback.assert_called_once_with(
- conversation_key="thread#C123#123",
- feedback_type="additional",
- user_id="U123",
- channel_id="C123",
- thread_ts="1759845114.407989",
- message_ts=None,
- feedback_text="this is some feedback",
- client=mock_client,
- )
- mock_client.chat_postMessage.assert_called_once_with(
- channel="C123", text="Thank you for your feedback.", thread_ts="1759845114.407989"
- )
-
-
-@patch("app.services.slack.post_error_message")
-def test_process_feedback_event_error(
- mock_post_error_message: Mock,
-):
- # 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_feedback_event
-
- # perform operation
- mock_event = {
- "channel": "C123",
- "thread_ts": "123",
- }
- with patch("app.slack.slack_events.store_feedback") as mock_store_feedback:
- mock_store_feedback.side_effect = Exception("There was an error")
- process_feedback_event(
- message_text="feedback: this is some feedback",
- conversation_key="thread#C123#123",
- user_id="U123",
- channel_id="C123",
- thread_root="1759845114.407989",
- event=mock_event,
- client=mock_client,
- )
-
- # assertions
- mock_post_error_message.assert_called_once_with(channel="C123", thread_ts="123", client=mock_client)
diff --git a/packages/slackBotFunction/tests/test_slack_events/test_slack_events_actions.py b/packages/slackBotFunction/tests/test_slack_events/test_slack_events_actions.py
new file mode 100644
index 00000000..635920e7
--- /dev/null
+++ b/packages/slackBotFunction/tests/test_slack_events/test_slack_events_actions.py
@@ -0,0 +1,276 @@
+import sys
+import pytest
+from unittest.mock import Mock, patch, MagicMock
+
+
+@pytest.fixture
+def mock_logger():
+ return MagicMock()
+
+
+@patch("app.utils.handler_utils.forward_event_to_pull_request_lambda")
+def test_process_async_slack_event_feedback(
+ mock_forward_event_to_pull_request_lambda: Mock,
+ mock_get_parameter: Mock,
+ mock_env: Mock,
+):
+ """Test successful async event 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_event
+
+ # perform operation
+ slack_event_data = {
+ "text": "feedback: this is some feedback",
+ "user": "U456",
+ "channel": "C789",
+ "ts": "1234567890.123",
+ }
+ with patch("app.slack.slack_events.process_feedback_event") as mock_process_feedback_event, patch(
+ "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_process_feedback_event.assert_called_once_with(
+ message_text="feedback: this is some feedback",
+ conversation_key="thread#C789#1234567890.123",
+ user_id="U456",
+ channel_id="C789",
+ thread_root="1234567890.123",
+ client=mock_client,
+ event=slack_event_data,
+ )
+ mock_process_slack_message.assert_not_called()
+
+
+@patch("app.utils.handler_utils.is_latest_message")
+def test_process_async_slack_action_positive(
+ mock_is_latest_message: Mock,
+ mock_get_parameter: Mock,
+ mock_env: Mock,
+):
+ """Test successful async action processing"""
+ # set up mocks
+ mock_client = Mock()
+ mock_is_latest_message.return_value = True
+
+ # 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_action
+
+ feedback_value = '{"ck":"thread#C123#123","ch":"C123","mt":"1759845126.972219","tt":"1759845114.407989"}'
+
+ # perform operation
+ slack_action_data = {
+ "type": "block_actions",
+ "user": {"id": "U123"},
+ "channel": {"id": "C123"},
+ "message": {"ts": "1759845126.972219"},
+ "actions": [{"action_id": "feedback_yes", "value": feedback_value}],
+ }
+ with patch("app.slack.slack_events.store_feedback") as mock_store_feedback:
+ process_async_slack_action(body=slack_action_data, client=mock_client)
+
+ # assertions
+ mock_store_feedback.assert_called_once_with(
+ conversation_key="thread#C123#123",
+ feedback_type="positive",
+ user_id="U123",
+ channel_id="C123",
+ thread_ts="1759845114.407989",
+ message_ts="1759845126.972219",
+ client=mock_client,
+ )
+ mock_client.chat_postMessage.assert_called_once_with(
+ channel="C123",
+ text="Thank you for your feedback.",
+ thread_ts="1759845114.407989",
+ )
+
+
+@patch("app.utils.handler_utils.is_latest_message")
+def test_process_async_slack_action_negative(
+ mock_is_latest_message: Mock,
+ mock_get_parameter: Mock,
+ mock_env: Mock,
+):
+ """Test successful async action processing"""
+ # set up mocks
+ mock_client = Mock()
+ mock_is_latest_message.return_value = True
+
+ # 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_action
+
+ feedback_value = '{"ck":"thread#C123#123","ch":"C123","mt":"1759845126.972219","tt":"1759845114.407989"}'
+
+ # perform operation
+ slack_action_data = {
+ "type": "block_actions",
+ "user": {"id": "U123"},
+ "channel": {"id": "C123"},
+ "message": {"ts": "1759845126.972219"},
+ "actions": [{"action_id": "feedback_no", "value": feedback_value}],
+ }
+ with patch("app.slack.slack_events.store_feedback") as mock_store_feedback:
+ process_async_slack_action(body=slack_action_data, client=mock_client)
+
+ # assertions
+ mock_store_feedback.assert_called_once_with(
+ conversation_key="thread#C123#123",
+ feedback_type="negative",
+ user_id="U123",
+ channel_id="C123",
+ thread_ts="1759845114.407989",
+ message_ts="1759845126.972219",
+ client=mock_client,
+ )
+ mock_client.chat_postMessage.assert_called_once_with(
+ channel="C123",
+ text='Please let us know how the answer could be improved. Start your message with "feedback:"',
+ thread_ts="1759845114.407989",
+ )
+
+
+@patch("app.utils.handler_utils.is_latest_message")
+def test_process_async_slack_action_not_latest(
+ mock_is_latest_message: Mock,
+ mock_get_parameter: Mock,
+ mock_env: Mock,
+):
+ """Test successful async action processing"""
+ # set up mocks
+ mock_client = Mock()
+ mock_is_latest_message.return_value = False
+
+ # 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_action
+
+ feedback_value = '{"ck":"thread#C123#123","ch":"C123","mt":"1759845126.972219","tt":"1759845114.407989"}'
+
+ # perform operation
+ slack_action_data = {
+ "type": "block_actions",
+ "user": {"id": "U123"},
+ "channel": {"id": "C123"},
+ "actions": [{"action_id": "feedback_no", "value": feedback_value}],
+ }
+ with patch("app.slack.slack_events.store_feedback") as mock_store_feedback:
+ process_async_slack_action(body=slack_action_data, client=mock_client)
+
+ # assertions
+ mock_store_feedback.assert_not_called()
+ mock_client.chat_postMessage.assert_not_called()
+
+
+@patch("app.utils.handler_utils.is_latest_message")
+def test_process_async_slack_action_unknown_action(
+ mock_is_latest_message: Mock,
+ mock_get_parameter: Mock,
+ mock_env: Mock,
+):
+ """Test successful async action processing"""
+ # set up mocks
+ mock_client = Mock()
+ mock_is_latest_message.return_value = True
+
+ # 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_action
+
+ feedback_value = '{"ck":"thread#C123#123","ch":"C123","mt":"1759845126.972219","tt":"1759845114.407989"}'
+
+ # perform operation
+ slack_action_data = {
+ "type": "block_actions",
+ "user": {"id": "U123"},
+ "channel": {"id": "C123"},
+ "actions": [{"action_id": "I_Do_Not_Know_This_Action", "value": feedback_value}],
+ }
+ with patch("app.slack.slack_events.store_feedback") as mock_store_feedback:
+ process_async_slack_action(body=slack_action_data, client=mock_client)
+
+ # assertions
+ mock_store_feedback.assert_not_called()
+ mock_client.chat_postMessage.assert_not_called()
+
+
+def test_process_feedback_event():
+ # 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_feedback_event
+
+ # perform operation
+ mock_event = {}
+ with patch("app.slack.slack_events.store_feedback") as mock_store_feedback:
+ process_feedback_event(
+ message_text="feedback: this is some feedback",
+ conversation_key="thread#C123#123",
+ user_id="U123",
+ channel_id="C123",
+ thread_root="1759845114.407989",
+ event=mock_event,
+ client=mock_client,
+ )
+
+ # assertions
+ mock_store_feedback.assert_called_once_with(
+ conversation_key="thread#C123#123",
+ feedback_type="additional",
+ user_id="U123",
+ channel_id="C123",
+ thread_ts="1759845114.407989",
+ message_ts=None,
+ feedback_text="this is some feedback",
+ client=mock_client,
+ )
+ mock_client.chat_postMessage.assert_called_once_with(
+ channel="C123", text="Thank you for your feedback.", thread_ts="1759845114.407989"
+ )
+
+
+@patch("app.services.slack.post_error_message")
+def test_process_feedback_event_error(
+ mock_post_error_message: Mock,
+):
+ # 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_feedback_event
+
+ # perform operation
+ mock_event = {
+ "channel": "C123",
+ "thread_ts": "123",
+ }
+ with patch("app.slack.slack_events.store_feedback") as mock_store_feedback:
+ mock_store_feedback.side_effect = Exception("There was an error")
+ process_feedback_event(
+ message_text="feedback: this is some feedback",
+ conversation_key="thread#C123#123",
+ user_id="U123",
+ channel_id="C123",
+ thread_root="1759845114.407989",
+ event=mock_event,
+ client=mock_client,
+ )
+
+ # assertions
+ mock_post_error_message.assert_called_once_with(channel="C123", thread_ts="123", client=mock_client)
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
new file mode 100644
index 00000000..4f5ced5e
--- /dev/null
+++ b/packages/slackBotFunction/tests/test_slack_events/test_slack_events_citations.py
@@ -0,0 +1,685 @@
+import json
+import sys
+import pytest
+from unittest.mock import Mock, MagicMock, patch
+
+
+@pytest.fixture
+def mock_logger():
+ return MagicMock()
+
+
+@patch("app.services.dynamo.get_state_information")
+@patch("app.services.ai_processor.process_ai_query")
+@patch("app.slack.slack_events.get_conversation_session")
+@patch("app.slack.slack_events._create_feedback_blocks")
+def test_citation_processing(
+ mock_get_session: Mock,
+ mock_process_ai_query: Mock,
+ mock_create_feedback_blocks: Mock,
+ mock_get_state_information: Mock,
+ mock_get_parameter: Mock,
+ mock_env: Mock,
+):
+ """Test block builder is being called correctly"""
+ # set up mocks
+ mock_client = Mock()
+ mock_process_ai_query.return_value = {
+ "text": "AI response",
+ "session_id": "session-123",
+ "citations": [],
+ "kb_response": {"output": {"text": "AI response"}},
+ }
+ mock_get_session.return_value = None # No existing session
+
+ # 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_slack_message
+
+ # perform operation
+ slack_event_data = {
+ "text": "Answer",
+ "user": "U456",
+ "channel": "C789",
+ "ts": "1234567890.123",
+ }
+
+ process_slack_message(event=slack_event_data, event_id="evt123", client=mock_client)
+
+ # assertions
+ # Verify that the message was processed (process_ai_query was called)
+ mock_create_feedback_blocks.assert_called_once()
+
+
+def test_process_slack_message_split_citation():
+ # set up mocks
+ mock_client = Mock()
+ mock_client.chat_postMessage.return_value = {"ts": "1234567890.124"}
+ mock_client.chat_update.return_value = {"ok": True}
+
+
+def test_process_citation_events_update_chat():
+ # set up mocks
+ mock_client = Mock()
+ mock_client.chat_postMessage.return_value = {"ts": "1234567890.124"}
+ mock_client.chat_update.return_value = {"ok": True}
+
+ # 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_action
+
+ body = {
+ "type": "block_actions",
+ "message": {
+ "ts": "123",
+ "text": "",
+ "blocks": [
+ {
+ "type": "section",
+ "block_id": "OvNCm",
+ "text": {
+ "type": "mrkdwn",
+ "text": "",
+ },
+ },
+ {
+ "type": "actions",
+ "block_id": "citation_actions",
+ "elements": [
+ {
+ "type": "button",
+ "action_id": "cite_1",
+ "text": {
+ "type": "plain_text",
+ "text": "[1] Downloading a single prescription using the prescription's ID, or ...",
+ "emoji": "true",
+ },
+ "value": '{"ck":"123","ch":"123","mt":"123","tt":"123","source_number":"1","title":"title"',
+ }
+ ],
+ },
+ ],
+ },
+ "channel": {
+ "id": "ABC123",
+ },
+ "actions": [
+ {
+ "action_id": "cite_1",
+ "block_id": "citation_actions",
+ "text": {
+ "type": "plain_text",
+ "text": "[1] Downloading a single prescription using the prescription's ID, or ...",
+ "emoji": "true",
+ },
+ "value": '{"ck":"123","ch":"C095D4SRX6W","mt":"123","tt":"123","source_number":"1","title":""}',
+ "type": "button",
+ "action_ts": "1765807735.805872",
+ }
+ ],
+ }
+
+ # perform operation
+ process_async_slack_action(body, mock_client)
+
+ # assertions
+ mock_client.chat_update.assert_called()
+
+
+def test_process_citation_events_update_chat_message_open_citation():
+ # set up mocks
+ mock_client = Mock()
+ mock_client.chat_postMessage.return_value = {"ts": "1234567890.124"}
+ mock_client.chat_update.return_value = {"ok": True}
+
+ # 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 open_citation
+
+ params = {
+ "ck": "123123",
+ "ch": "123123",
+ "mt": "123123.123123",
+ "tt": "123123.123123",
+ "source_number": "1",
+ "title": "Citation Title",
+ "body": "Citation Body",
+ "relevance_score": "0.95",
+ }
+
+ citations = {
+ "type": "actions",
+ "block_id": "citation_actions",
+ "elements": [
+ {
+ "type": "button",
+ "action_id": "cite_1",
+ "text": {
+ "type": "plain_text",
+ "text": "[1] The body of the citation",
+ "emoji": "true",
+ },
+ "style": None, # Set citation as de-active
+ "value": str(params),
+ },
+ ],
+ }
+
+ message = {
+ "blocks": [citations],
+ }
+
+ # perform operation
+ open_citation("ABC", "123", message, params, mock_client)
+
+ # assertions
+ expected_blocks = [
+ citations,
+ {
+ "type": "section",
+ "text": {"type": "mrkdwn", "text": "*Citation Title*\n\n> Citation Body"},
+ "block_id": "citation_block",
+ },
+ ]
+ mock_client.chat_update.assert_called()
+ mock_client.chat_update.assert_called_with(channel="ABC", ts="123", blocks=expected_blocks)
+
+
+def test_process_citation_events_update_chat_message_close_citation():
+ # set up mocks
+ mock_client = Mock()
+ mock_client.chat_postMessage.return_value = {"ts": "1234567890.124"}
+ mock_client.chat_update.return_value = {"ok": True}
+
+ # 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 open_citation
+
+ params = {
+ "ck": "123123",
+ "ch": "123123",
+ "mt": "123123.123123",
+ "tt": "123123.123123",
+ "source_number": "1",
+ "title": "Citation Title",
+ "body": "Citation Body",
+ "relevance_score": "0.95",
+ }
+
+ citations = {
+ "type": "actions",
+ "block_id": "citation_actions",
+ "elements": [
+ {
+ "type": "button",
+ "action_id": "cite_1",
+ "text": {
+ "type": "plain_text",
+ "text": "[1] The body of the citation",
+ "emoji": "true",
+ },
+ "style": "primary", # Set citation as active
+ "value": str(params),
+ },
+ ],
+ }
+
+ citation_body = {
+ "type": "section",
+ "text": {"type": "mrkdwn", "text": "*Citation Title*\n\n> Citation Body"},
+ "block_id": "citation_block",
+ }
+
+ message = {
+ "blocks": [citations, citation_body],
+ }
+
+ # perform operation
+ open_citation("ABC", "123", message, params, mock_client)
+
+ # assertions
+ expected_blocks = [
+ citations,
+ ]
+ mock_client.chat_update.assert_called()
+ mock_client.chat_update.assert_called_with(channel="ABC", ts="123", blocks=expected_blocks)
+
+
+def test_process_citation_events_update_chat_message_change_close_citation():
+ # set up mocks
+ mock_client = Mock()
+ mock_client.chat_postMessage.return_value = {"ts": "1234567890.124"}
+ mock_client.chat_update.return_value = {"ok": True}
+
+ # 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 open_citation
+
+ params = {
+ "ck": "123123",
+ "ch": "123123",
+ "mt": "123123.123123",
+ "tt": "123123.123123",
+ "source_number": "2",
+ "title": "Second Citation Title",
+ "body": "Second Citation Body",
+ "relevance_score": "0.95",
+ }
+
+ citations = {
+ "type": "actions",
+ "block_id": "citation_actions",
+ "elements": [
+ {
+ "type": "button",
+ "action_id": "cite_1",
+ "text": {
+ "type": "plain_text",
+ "text": "[1] The body of the citation",
+ "emoji": "true",
+ },
+ "style": "primary", # Set citation as active
+ "value": str(params),
+ },
+ {
+ "type": "button",
+ "action_id": "cite_2",
+ "text": {
+ "type": "plain_text",
+ "text": "[2] The body of the citation",
+ "emoji": "true",
+ },
+ "style": None, # Set citation as active
+ "value": str(params),
+ },
+ ],
+ }
+
+ first_citation_body = {
+ "type": "section",
+ "text": {"type": "mrkdwn", "text": "*First Citation Title*\n\n> First Citation Body"},
+ "block_id": "citation_block",
+ }
+
+ second_citation_body = {
+ "type": "section",
+ "text": {"type": "mrkdwn", "text": "*Second Citation Title*\n\n> Second Citation Body"},
+ "block_id": "citation_block",
+ }
+
+ message = {
+ "blocks": [citations, first_citation_body],
+ }
+
+ # perform operation
+ open_citation("ABC", "123", message, params, mock_client)
+
+ # assertions
+ expected_blocks = [citations, second_citation_body]
+ mock_client.chat_update.assert_called()
+ mock_client.chat_update.assert_called_with(channel="ABC", ts="123", blocks=expected_blocks)
+
+
+def test_create_response_body_no_error_without_citations(
+ mock_get_parameter: Mock,
+ mock_env: Mock,
+):
+ """Test regex text processing functionality within process_async_slack_event"""
+ # 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 _create_response_body
+
+ # perform operation
+ _create_response_body(
+ citations=[],
+ feedback_data={},
+ response_text="This is a response without a citation.[1]",
+ )
+
+ # assertions
+ # no assertions as we are just checking it does not throw an error
+
+
+def test_create_response_body_creates_body_without_citations(
+ mock_get_parameter: Mock,
+ mock_env: Mock,
+):
+ """Test regex text processing functionality within process_async_slack_event"""
+ # 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 _create_response_body
+
+ # perform operation
+ response = _create_response_body(
+ citations=[],
+ feedback_data={},
+ response_text="This is a response without a citation.",
+ )
+
+ # assertions
+ assert len(response) > 0
+ assert response[0]["type"] == "section"
+ assert "This is a response without a citation." in response[0]["text"]["text"]
+
+
+def test_create_response_body_update_body_with_citations(
+ mock_get_parameter: Mock,
+ mock_env: Mock,
+):
+ """Test regex text processing functionality within process_async_slack_event"""
+ # 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 _create_response_body
+
+ # perform operation
+ response = _create_response_body(
+ citations=[
+ {
+ "source_number": "1",
+ "title": "Citation Title",
+ "body": "Citation Body",
+ "relevance_score": "0.95",
+ }
+ ],
+ feedback_data={},
+ response_text="This is a response with a citation.[1]",
+ )
+
+ # assertions
+ assert len(response) > 1
+ assert response[1]["type"] == "actions"
+ assert response[1]["block_id"] == "citation_actions"
+
+ citation_element = response[1]["elements"][0]
+ assert citation_element["type"] == "button"
+ assert citation_element["action_id"] == "cite_1"
+ assert "[1] Citation Title" in citation_element["text"]["text"]
+
+
+def test_create_response_body_creates_body_with_multiple_citations(
+ mock_get_parameter: Mock,
+ mock_env: Mock,
+):
+ """Test regex text processing functionality within process_async_slack_event"""
+ # 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 _create_response_body
+
+ # perform operation
+ response = _create_response_body(
+ citations=[
+ {
+ "source_number": "1",
+ "title": "Citation Title",
+ "body": "Citation Body",
+ "relevance_score": "0.95",
+ },
+ {
+ "source_number": "2",
+ "title": "Citation Title",
+ "body": "Citation Body",
+ "relevance_score": "0.95",
+ },
+ ],
+ feedback_data={},
+ response_text="This is a response with a citation.[1]",
+ )
+
+ # assertions
+ assert len(response) > 1
+ assert response[1]["type"] == "actions"
+ assert response[1]["block_id"] == "citation_actions"
+
+ first_citation_element = response[1]["elements"][0]
+ assert first_citation_element["type"] == "button"
+ assert first_citation_element["action_id"] == "cite_1"
+ assert "[1] Citation Title" in first_citation_element["text"]["text"]
+
+ second_citation_element = response[1]["elements"][1]
+ assert second_citation_element["type"] == "button"
+ assert second_citation_element["action_id"] == "cite_2"
+ assert "[2] Citation Title" in second_citation_element["text"]["text"]
+
+
+def test_create_response_body_creates_body_ignoring_low_score_citations(
+ mock_get_parameter: Mock,
+ mock_env: Mock,
+):
+ """Test regex text processing functionality within process_async_slack_event"""
+ # 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 _create_response_body
+
+ # perform operation
+ response = _create_response_body(
+ citations=[
+ {
+ "source_number": "1",
+ "title": "Citation Title",
+ "body": "Citation Body",
+ "relevance_score": "0.55",
+ },
+ {
+ "source_number": "2",
+ "title": "Citation Title",
+ "body": "Citation Body",
+ "relevance_score": "0.95",
+ },
+ ],
+ feedback_data={},
+ response_text="This is a response with a citation.[1]",
+ )
+
+ # assertions
+ assert len(response) > 1
+ assert response[1]["type"] == "actions"
+ assert response[1]["block_id"] == "citation_actions"
+
+ citation_elements = response[1]["elements"]
+ assert len(citation_elements) == 1
+
+ citation_element = citation_elements[0]
+ assert citation_element["type"] == "button"
+ assert citation_element["action_id"] == "cite_2"
+ assert "[2] Citation Title" in citation_element["text"]["text"]
+
+
+def test_create_response_body_update_body_with_reformatted_citations(
+ mock_get_parameter: Mock,
+ mock_env: Mock,
+):
+ """Test regex text processing functionality within process_async_slack_event"""
+ # 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 _create_response_body
+
+ # perform operation
+ response = _create_response_body(
+ citations=[
+ {
+ "source_number": "1",
+ "title": "Citation Title",
+ "body": "Citation Body",
+ "relevance_score": "0.95",
+ }
+ ],
+ feedback_data={},
+ response_text="This is a response with a citation.[cit_1]",
+ )
+
+ # assertions
+ assert len(response) > 1
+ assert response[0]["type"] == "section"
+ assert "This is a response with a citation.[1]" in response[0]["text"]["text"]
+
+
+def test_create_response_body_creates_body_with_markdown_formatting(
+ mock_get_parameter: Mock,
+ mock_env: Mock,
+):
+ """Test regex text processing functionality within process_async_slack_event"""
+ # 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 _create_response_body
+
+ # perform operation
+ response = _create_response_body(
+ citations=[
+ {
+ "source_number": "1",
+ "title": "Citation Title",
+ "excerpt": "**Bold**, __italics__, *markdown italics*, and `code`.",
+ "relevance_score": "0.95",
+ }
+ ],
+ feedback_data={},
+ response_text="This is a response with a citation.[1]",
+ )
+
+ # assertions
+ assert len(response) > 1
+ assert response[1]["type"] == "actions"
+ assert response[1]["block_id"] == "citation_actions"
+
+ citation_element = response[1]["elements"][0]
+ citation_value = json.loads(citation_element["value"])
+
+ assert "*Bold*, _italics_, _markdown italics_, and `code`." in citation_value.get("body")
+
+
+def test_create_response_body_creates_body_with_lists(
+ mock_get_parameter: Mock,
+ mock_env: Mock,
+):
+ """Test regex text processing functionality within process_async_slack_event"""
+ # 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 _create_response_body
+
+ dirty_input = "Header text - Standard Dash -No Space Dash • Standard Bullet -NoSpace-NoSpace"
+
+ # perform operation
+ response = _create_response_body(
+ citations=[
+ {
+ "source_number": "1",
+ "title": "Citation Title",
+ "excerpt": dirty_input,
+ "relevance_score": "0.95",
+ }
+ ],
+ feedback_data={},
+ response_text="This is a response with a citation.[1]",
+ )
+
+ # assertions
+ assert len(response) > 1
+ assert response[1]["type"] == "actions"
+ assert response[1]["block_id"] == "citation_actions"
+
+ citation_element = response[1]["elements"][0]
+ citation_value = json.loads(citation_element["value"])
+
+ expected_output = "Header text\n- Standard Dash\n- No Space Dash\n- Standard Bullet\n- NoSpace-NoSpace"
+ assert expected_output in citation_value.get("body")
+
+
+def test_create_response_body_creates_body_without_encoding_errors(
+ mock_get_parameter: Mock,
+ mock_env: Mock,
+):
+ """Test regex text processing functionality within process_async_slack_event"""
+ # 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 _create_response_body
+
+ # perform operation
+ response = _create_response_body(
+ citations=[
+ {
+ "source_number": "1",
+ "title": "Citation Title",
+ "excerpt": "» Tabbing Issue. ⢠Bullet point issue.",
+ "relevance_score": "0.95",
+ }
+ ],
+ feedback_data={},
+ response_text="This is a response with a citation.[1]",
+ )
+
+ # assertions
+ assert len(response) > 1
+ assert response[1]["type"] == "actions"
+ assert response[1]["block_id"] == "citation_actions"
+
+ citation_element = response[1]["elements"][0]
+ citation_value = json.loads(citation_element["value"])
+
+ assert "Tabbing Issue.\n- Bullet point issue." in citation_value.get("body")
+
+
+@patch("app.services.ai_processor.process_ai_query")
+def test_create_citation_logs_citations(
+ mock_process_ai_query: Mock,
+ mock_logger,
+):
+ with patch("app.core.config.get_logger", return_value=mock_logger):
+ # set up mocks
+ mock_client = Mock()
+ 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}",
+ "session_id": "session-123",
+ "citations": [],
+ "kb_response": {"output": {"text": "AI response"}},
+ }
+
+ # 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_slack_message
+
+ # perform operation
+ slack_event_data = {
+ "text": "Answer",
+ "user": "U456",
+ "channel": "C789",
+ "ts": "1234567890.123",
+ }
+
+ process_slack_message(event=slack_event_data, event_id="evt123", client=mock_client)
+
+ # mock_logger.assert_has_calls([call.info("Found citation(s)", extra={"Raw Citations": [raw_citation]})])
+ # assertions
+
+ mock_logger.info.assert_any_call(
+ "Found citation(s)", extra={"Raw Citations": ["1||This is the Title||This is the excerpt/ citation||0.99"]}
+ )
+ mock_logger.info.assert_any_call(
+ "Parsed citation(s)",
+ extra={
+ "citations": [
+ {
+ "source_number": "1",
+ "title": "This is the Title",
+ "excerpt": "This is the excerpt/ citation",
+ "relevance_score": "0.99",
+ }
+ ]
+ },
+ )
+ # mock_logger.info.assert_called_with("Found citation(s)", extra={"Raw Citations": [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
new file mode 100644
index 00000000..c5131877
--- /dev/null
+++ b/packages/slackBotFunction/tests/test_slack_events/test_slack_events_messages.py
@@ -0,0 +1,499 @@
+import sys
+import pytest
+from unittest.mock import Mock, patch, MagicMock
+
+
+@pytest.fixture
+def mock_logger():
+ return MagicMock()
+
+
+@patch("app.utils.handler_utils.forward_event_to_pull_request_lambda")
+def test_process_async_slack_event_normal_message(
+ mock_forward_event_to_pull_request_lambda: Mock,
+ mock_get_parameter: Mock,
+ mock_env: Mock,
+):
+ """Test successful async event 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_event
+
+ # perform operation
+ slack_event_data = {"text": "<@U123> test question", "user": "U456", "channel": "C789", "ts": "1234567890.123"}
+ with patch("app.slack.slack_events.process_feedback_event") as mock_process_feedback_event, patch(
+ "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_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")
+def test_process_async_slack_event_pull_request_with_mention(
+ mock_forward_event_to_pull_request_lambda: Mock,
+ mock_get_parameter: Mock,
+ mock_env: Mock,
+):
+ """Test successful async event 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_event
+
+ # perform operation
+ slack_event_data = {
+ "text": "<@U123> pr: 123 test question",
+ "user": "U456",
+ "channel": "C789",
+ "ts": "1234567890.123",
+ }
+ with patch("app.slack.slack_events.process_feedback_event") as mock_process_feedback_event, patch(
+ "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(
+ pull_request_id="123",
+ event=slack_event_data,
+ event_id="evt123",
+ store_pull_request_id=True,
+ )
+ 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")
+def test_process_async_slack_event_pull_request_with_no_mention(
+ mock_forward_event_to_pull_request_lambda: Mock,
+ mock_get_parameter: Mock,
+ mock_env: Mock,
+):
+ """Test successful async event 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_event
+
+ # perform operation
+ slack_event_data = {
+ "text": "pr: 123 test question",
+ "user": "U456",
+ "channel": "C789",
+ "ts": "1234567890.123",
+ }
+ with patch("app.slack.slack_events.process_feedback_event") as mock_process_feedback_event, patch(
+ "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(
+ pull_request_id="123",
+ event=slack_event_data,
+ event_id="evt123",
+ store_pull_request_id=True,
+ )
+ mock_process_feedback_event.assert_not_called()
+ mock_process_slack_message.assert_not_called()
+
+
+def test_process_slack_message_empty_query(mock_get_parameter: Mock, mock_env: Mock):
+ """Test async event processing with empty query"""
+ # 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_slack_message
+
+ # perform operation
+ slack_event_data = {
+ "text": "<@U123>", # Only mention, no actual query
+ "user": "U456",
+ "channel": "C789",
+ "ts": "1234567890.123",
+ }
+ process_slack_message(event=slack_event_data, event_id="evt123", client=mock_client)
+
+ # assertions
+ mock_client.chat_postMessage.assert_called_once_with(
+ channel="C789",
+ text="Hi there! Please ask me a question and I'll help you find information from our knowledge base.",
+ thread_ts="1234567890.123",
+ )
+
+
+@patch("app.services.dynamo.get_state_information")
+@patch("app.services.ai_processor.process_ai_query")
+@patch("app.slack.slack_events.get_conversation_session")
+def test_process_slack_message_with_thread_ts(
+ mock_get_session: Mock,
+ mock_process_ai_query: Mock,
+ mock_get_state_information: Mock,
+ mock_get_parameter: Mock,
+ mock_env: Mock,
+):
+ """Test async event processing with existing thread_ts"""
+ # set up mocks
+ mock_client = Mock()
+ mock_client.chat_postMessage.return_value = {"ts": "1234567890.124"}
+ mock_client.chat_update.return_value = {"ok": True}
+ mock_process_ai_query.return_value = {
+ "text": "AI response",
+ "session_id": "session-123",
+ "citations": [],
+ "kb_response": {"output": {"text": "AI response"}},
+ }
+ mock_get_session.return_value = None # No existing session
+
+ # 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_slack_message
+
+ # perform operation
+ slack_event_data = {
+ "text": "<@U123> test question",
+ "user": "U456",
+ "channel": "C789",
+ "ts": "1234567890.123",
+ "thread_ts": "1234567888.111", # Existing thread
+ }
+ process_slack_message(event=slack_event_data, event_id="evt123", client=mock_client)
+
+ # assertions
+ # Should be called at least once with the correct 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"
+
+
+@patch("app.services.dynamo.get_state_information")
+@patch("app.services.ai_processor.process_ai_query")
+@patch("app.slack.slack_events.get_conversation_session")
+def test_regex_text_processing(
+ mock_get_session: Mock,
+ mock_process_ai_query: Mock,
+ mock_get_state_information: Mock,
+ mock_get_parameter: Mock,
+ mock_env: Mock,
+):
+ """Test regex text processing functionality within process_async_slack_event"""
+ # set up mocks
+ mock_client = Mock()
+ mock_process_ai_query.return_value = {
+ "text": "AI response",
+ "session_id": "session-123",
+ "citations": [],
+ "kb_response": {"output": {"text": "AI response"}},
+ }
+ mock_get_session.return_value = None # No existing session
+
+ # 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_slack_message
+
+ # perform operation
+ slack_event_data = {"text": "<@U123456> test question", "user": "U456", "channel": "C789", "ts": "1234567890.123"}
+
+ process_slack_message(event=slack_event_data, event_id="evt123", client=mock_client)
+
+ # assertions
+ # Verify that the message was processed (process_ai_query was called)
+ mock_process_ai_query.assert_called_once()
+ # The actual regex processing happens inside the function
+ assert mock_client.chat_postMessage.called
+
+
+@patch("app.services.dynamo.get_state_information")
+@patch("app.services.dynamo.store_state_information")
+@patch("app.services.ai_processor.process_ai_query")
+def test_process_slack_message_with_session_storage(
+ mock_process_ai_query: Mock,
+ mock_store_state_information: Mock,
+ mock_get_state_information: Mock,
+ mock_get_parameter: Mock,
+ mock_env: Mock,
+):
+ """Test async event processing that stores a new session"""
+ # set up mocks
+ mock_client = Mock()
+ mock_client.chat_postMessage.return_value = {"ts": "1234567890.124"}
+ mock_client.chat_update.return_value = {"ok": True}
+ mock_process_ai_query.return_value = {
+ "text": "AI response",
+ "session_id": "new-session-123",
+ "citations": [],
+ "kb_response": {"output": {"text": "AI response"}, "sessionId": "new-session-123"},
+ }
+
+ # 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_slack_message
+
+ # perform operation
+ slack_event_data = {"text": "test question", "user": "U456", "channel": "C789", "ts": "1234567890.123"}
+
+ process_slack_message(event=slack_event_data, event_id="evt123", client=mock_client)
+
+ # assertions
+ # Verify session was stored - should be called twice (Q&A pair + session)
+ assert mock_store_state_information.call_count >= 1
+
+
+@patch("app.services.dynamo.get_state_information")
+@patch("app.services.ai_processor.process_ai_query")
+@patch("app.slack.slack_events.get_conversation_session")
+def test_process_slack_message_chat_update_no_error(
+ mock_get_session: Mock,
+ mock_process_ai_query: Mock,
+ mock_get_state_information: Mock,
+ mock_get_parameter: Mock,
+ mock_env: Mock,
+):
+ """Test process_async_slack_event with chat_update error"""
+ # set up mocks
+ mock_client = Mock()
+ mock_client.chat_postMessage.return_value = {"ts": "1234567890.124"}
+ mock_client.chat_update.side_effect = Exception("Update failed")
+ mock_process_ai_query.return_value = {
+ "text": "AI response",
+ "session_id": "session-123",
+ "citations": [],
+ "kb_response": {"output": {"text": "AI response"}},
+ }
+ mock_get_session.return_value = None # No existing session
+
+ # 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_slack_message
+
+ # perform operation
+ slack_event_data = {"text": "<@U123> test question", "user": "U456", "channel": "C789", "ts": "1234567890.123"}
+ process_slack_message(event=slack_event_data, event_id="evt123", client=mock_client)
+
+ # assertions
+ # no assertions as we are just checking it does not throw an error
+
+
+@patch("app.slack.slack_events.get_conversation_session")
+@patch("app.slack.slack_events.get_conversation_session_data")
+@patch("app.slack.slack_events.cleanup_previous_unfeedback_qa")
+@patch("app.slack.slack_events.update_session_latest_message")
+@patch("app.services.ai_processor.process_ai_query")
+def test_process_slack_message_chat_update_cleanup(
+ mock_process_ai_query: Mock,
+ mock_update_session_latest_message: Mock,
+ mock_cleanup_previous_unfeedback_qa: Mock,
+ mock_get_conversation_session_data: Mock,
+ mock_get_session: Mock,
+ mock_get_parameter: Mock,
+ mock_env: Mock,
+):
+ """Test process_async_slack_event with chat_update error"""
+ # set up mocks
+ mock_client = Mock()
+ mock_client.chat_postMessage.return_value = {"ts": "1234567890.124"}
+ mock_client.chat_update.side_effect = Exception("Update failed")
+ mock_process_ai_query.return_value = {
+ "text": "AI response",
+ "session_id": "session-123",
+ "citations": [],
+ "kb_response": {"output": {"text": "AI response"}},
+ }
+ mock_get_conversation_session_data.return_value = {"session_id": "session-123"}
+ mock_get_session.return_value = None # No existing session
+ mock_cleanup_previous_unfeedback_qa.return_value = {"test": "123"}
+
+ # delete and import module to test
+ from app.slack.slack_events import process_slack_message
+
+ # perform operation
+ slack_event_data = {"text": "<@U123> test question", "user": "U456", "channel": "C789", "ts": "1234567890.123"}
+ with patch("app.slack.slack_events.get_conversation_session_data", mock_get_conversation_session_data):
+ process_slack_message(event=slack_event_data, event_id="evt123", client=mock_client)
+
+ # assertions
+ mock_cleanup_previous_unfeedback_qa.assert_called_once()
+ mock_update_session_latest_message.assert_called_once()
+
+
+@patch("app.services.dynamo.get_state_information")
+@patch("app.services.ai_processor.process_ai_query")
+@patch("app.slack.slack_events.get_conversation_session")
+def test_process_slack_message_dm_context(
+ mock_get_session: Mock,
+ mock_process_ai_query: Mock,
+ mock_get_state_information: Mock,
+ mock_get_parameter: Mock,
+ mock_env: Mock,
+):
+ """Test process_async_slack_event with DM context"""
+ # set up mocks
+ mock_client = Mock()
+ mock_client.chat_postMessage.return_value = {"ts": "123"}
+ mock_process_ai_query.return_value = {
+ "text": "AI response",
+ "session_id": "new-session",
+ "citations": [],
+ "kb_response": {"output": {"text": "AI response"}, "sessionId": "new-session"},
+ }
+ mock_get_session.return_value = None
+
+ # 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_event
+
+ # perform operation
+ slack_event_data = {
+ "text": "test question",
+ "user": "U456",
+ "channel": "D789",
+ "ts": "123",
+ "channel_type": "im", # DM context
+ }
+ process_async_slack_event(event=slack_event_data, event_id="evt123", client=mock_client)
+
+ # assertions
+ # no assertions as we are just checking it does not throw an error
+
+
+@patch("app.services.dynamo.delete_state_information")
+def test_cleanup_previous_unfeedback_qa_no_previous_message(
+ mock_delete_state_information: Mock,
+):
+ """Test cleanup skipped when no previous message exists"""
+ conversation_key = "conv-123"
+ current_message_ts = "1234567890.124"
+ session_data = {}
+
+ if "app.slack.slack_events" in sys.modules:
+ del sys.modules["app.slack.slack_events"]
+ from app.slack.slack_events import cleanup_previous_unfeedback_qa
+
+ # perform operation
+ cleanup_previous_unfeedback_qa(conversation_key, current_message_ts, session_data)
+
+ # assertions
+ mock_delete_state_information.assert_not_called()
+
+
+@patch("app.services.dynamo.delete_state_information")
+def test_cleanup_previous_unfeedback_qa_same_message(
+ mock_delete_state_information: Mock,
+):
+ """Test cleanup skipped when previous message is same as current"""
+ if "app.slack.slack_events" in sys.modules:
+ del sys.modules["app.slack.slack_events"]
+
+ conversation_key = "conv-123"
+ current_message_ts = "1234567890.123"
+ session_data = {"latest_message_ts": "1234567890.123"}
+
+ if "app.slack.slack_events" in sys.modules:
+ del sys.modules["app.slack.slack_events"]
+ from app.slack.slack_events import cleanup_previous_unfeedback_qa
+
+ # perform operation
+ cleanup_previous_unfeedback_qa(conversation_key, current_message_ts, session_data)
+
+ # assertions
+ mock_delete_state_information.assert_not_called()
+
+
+def test_create_response_body_creates_body_with_markdown_formatting(
+ mock_get_parameter: Mock,
+ mock_env: Mock,
+):
+ """Test regex text processing functionality within process_async_slack_event"""
+ # 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 _create_response_body
+
+ # perform operation
+ response = _create_response_body(
+ citations=[],
+ feedback_data={},
+ response_text="**Bold**, __italics__, *markdown italics*, and `code`.",
+ )
+
+ # assertions
+ assert len(response) > 0
+ assert response[0]["type"] == "section"
+
+ response_value = response[0]["text"]["text"]
+
+ assert "*Bold*, _italics_, _markdown italics_, and `code`." in response_value
+
+
+def test_create_response_body_creates_body_with_lists(
+ mock_get_parameter: Mock,
+ mock_env: Mock,
+):
+ """Test regex text processing functionality within process_async_slack_event"""
+ # 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 _create_response_body
+
+ dirty_input = "Header text - Standard Dash -No Space Dash • Standard Bullet -NoSpace-NoSpace"
+
+ # perform operation
+ response = _create_response_body(
+ citations=[],
+ feedback_data={},
+ response_text=dirty_input,
+ )
+
+ # assertions
+ assert len(response) > 0
+ assert response[0]["type"] == "section"
+
+ response_value = response[0]["text"]["text"]
+
+ expected_output = "Header text\n- Standard Dash\n- No Space Dash\n- Standard Bullet\n- NoSpace-NoSpace"
+ assert expected_output in response_value
+
+
+def test_create_response_body_creates_body_without_encoding_errors(
+ mock_get_parameter: Mock,
+ mock_env: Mock,
+):
+ """Test regex text processing functionality within process_async_slack_event"""
+ # 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 _create_response_body
+
+ # perform operation
+ response = _create_response_body(
+ citations=[],
+ feedback_data={},
+ response_text="» Tabbing Issue. ⢠Bullet point issue.",
+ )
+
+ # assertions
+ assert len(response) > 0
+ assert response[0]["type"] == "section"
+
+ response_value = response[0]["text"]["text"]
+
+ assert "Tabbing Issue.\n- Bullet point issue." in response_value