Skip to content

Commit fb549be

Browse files
feat: Add tests back for citations
1 parent e586855 commit fb549be

File tree

4 files changed

+247
-46
lines changed

4 files changed

+247
-46
lines changed

packages/cdk/prompts/systemPrompt.txt

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,32 @@ It is **VERY** important that you return **ALL** references found in the context
88
# 2. THINKING PROCESS & LOGIC
99
Before generating a response, adhere to these processing rules:
1010

11-
## A. Question Analysis
11+
## A. Context Verification
12+
Scan the retrieved context for the specific answer
13+
1. **No information found**: If the information is not present in the context:
14+
- Do NOT formulate a general answer.
15+
- Do NOT user external resources (i.e., websites, etc) to get an answer.
16+
- Do NOT infer an answer from the users question.
17+
18+
## B. Question Analysis
1219
1. **Detection:** Determine if the query contains one or multiple questions.
1320
2. **Decomposition:** Split complex queries into individual sub-questions.
1421
3. **Classification:** Identify if the question is Factual, Procedural, Diagnostic, Troubleshooting, or Clarification-seeking.
1522
4. **Multi-Question Strategy:** Number sub-questions clearly (Q1, Q2, etc).
23+
5. **No Information:** If there is no information supporting an answer to the query, do not try and fill in the information
24+
6. **Strictness:** Do not infer information, be strict on evidence.
1625

17-
## B. Entity Correction
26+
## C. Entity Correction
1827
- If you encounter "National Health Service Digital (NHSD)", automatically treat and output it as **"National Health Service England (NHSE)"**.
1928

20-
## C. RAG Confidence Scoring
29+
## D. RAG Confidence Scoring
2130
```
2231
Evaluate retrieved context using these relevance score thresholds:
23-
- `Score > 0.85` : **High confidence**
24-
- `Score 0.70 - 0.85` : **Medium confidence**
25-
- `Score < 0.70` : **Low confidence**
32+
- `Score > 0.9` : **Diamond** (Definitive source)
33+
- `Score 0.8 - 0.9` : **Gold** (Strong evidence)
34+
- `Score 0.7 - 0.8` : **Silver** (Partial context)
35+
- `Score 0.6 - 0.7` : **Bronze** (Weak relevance)
36+
- `Score < 0.6` : **Scrap** (Ignore completely)
2637
```
2738

2839
---

packages/slackBotFunction/app/slack/slack_events.py

Lines changed: 70 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ def _create_response_body(citations: list[dict[str, str]], feedback_data: dict[s
203203
response_text = result.get("response_text", response_text)
204204

205205
# Remove any citations that have not been returned
206+
response_text = convert_markdown_to_slack(response_text)
206207
response_text = response_text.replace("cit_", "")
207208

208209
# Main body
@@ -226,35 +227,56 @@ def _create_citation(citation: dict[str, str], feedback_data: dict, response_tex
226227
action_buttons = []
227228

228229
# Create citation blocks ["source_number", "title", "excerpt", "relevance_score"]
229-
source_number = (citation.get("source_number", "0")).replace("\n", "")
230-
title = citation.get("title") or citation.get("filename") or "Source"
231-
body = citation.get("excerpt") or invalid_body
232-
score = citation.get("relevance_score") or "0"
233-
234-
# Buttons can only be 75 characters long, truncate to be safe
235-
button_text = f"[{source_number}] {title}"
236-
button_value = {**feedback_data, "source_number": source_number, "title": title, "body": body, "score": score}
237-
button = {
238-
"type": "button",
239-
"text": {
240-
"type": "plain_text",
241-
"text": button_text if len(button_text) < 75 else f"{button_text[:70]}...",
242-
},
243-
"action_id": f"cite_{source_number}",
244-
"value": json.dumps(
245-
button_value,
246-
separators=(",", ":"),
247-
),
248-
}
249-
action_buttons.append(button)
230+
source_number: str = (citation.get("source_number", "0")).replace("\n", "")
231+
title: str = citation.get("title") or citation.get("filename") or "Source"
232+
body: str = citation.get("excerpt") or invalid_body
233+
score: float = float(citation.get("relevance_score") or "0")
234+
235+
# Format body
236+
body = convert_markdown_to_slack(body)
250237

251-
# Update inline citations to remove "cit_" prefix
252-
response_text = response_text.replace(f"[cit_{source_number}]", f"[{source_number}]")
238+
if score < 60: # low relevance score, skip citation
239+
logger.info("Skipping low relevance citation", extra={"source_number": source_number, "score": score})
240+
else:
241+
# Buttons can only be 75 characters long, truncate to be safe
242+
button_text = f"[{source_number}] {title}"
243+
button_value = {**feedback_data, "source_number": source_number, "title": title, "body": body, "score": score}
244+
button = {
245+
"type": "button",
246+
"text": {
247+
"type": "plain_text",
248+
"text": button_text if len(button_text) < 75 else f"{button_text[:70]}...",
249+
},
250+
"action_id": f"cite_{source_number}",
251+
"value": json.dumps(
252+
button_value,
253+
separators=(",", ":"),
254+
),
255+
}
256+
action_buttons.append(button)
257+
258+
# Update inline citations to remove "cit_" prefix
259+
response_text = response_text.replace(f"[cit_{source_number}]", f"[{source_number}]")
260+
logger.info("Created citation", extra=button_value)
253261

254-
logger.info("Created citation", extra=button_value)
255262
return {"action_buttons": action_buttons, "response_text": response_text}
256263

257264

265+
def convert_markdown_to_slack(body: str) -> str:
266+
"""Convert basic markdown to Slack formatting"""
267+
# Fix common encoding issues
268+
body = body.replace("»", "") # Remove double chevrons
269+
body = body.replace("â¢", "-") # Replace bullet points with encoding issues
270+
271+
# Simple markdown conversions
272+
body = re.sub(r"(\*{1,2}|_{1,2})([^\*_]+)\1", r"_\2_", body) # Italic (Do this first to avoid conflict with bold)
273+
body = body.replace("**", "*") # Bold
274+
275+
body = re.sub(r"(\u2022|-)\s", r"\n\g<0>", body) # Ensure bullet points on new lines
276+
body = re.sub(r"\[([^\]]+)\]\(([^\)]+)\)", r"<\1|\2>", body) # Convert links
277+
return body
278+
279+
258280
# ================================================================
259281
# Main async event processing
260282
# ================================================================
@@ -666,26 +688,35 @@ def open_citation(channel: str, timestamp: str, message: Any, params: Dict[str,
666688

667689

668690
def format_blocks(blocks: Any, current_id: str):
691+
"""Format blocks by styling the selected citation button and unstyle others"""
669692
selected = False
693+
670694
for block in blocks:
671-
if block.get("type") == "actions":
672-
for element in block.get("elements", []):
673-
if element.get("type") == "button":
674-
action_id = element.get("action_id")
675-
if action_id == current_id:
676-
# Toggle: if already styled, unselect; else select
677-
if element.get("style") == "primary":
678-
element.pop("style", None)
679-
selected = False
680-
else:
681-
element["style"] = "primary"
682-
selected = True
683-
else:
684-
# Unselect all other buttons
685-
element.pop("style", None)
695+
if block.get("type") != "actions":
696+
continue
697+
698+
for element in block.get("elements", []):
699+
if element.get("type") != "button":
700+
continue
701+
702+
if element.get("action_id") == current_id:
703+
selected = _toggle_button_style(element)
704+
else:
705+
element.pop("style", None)
706+
686707
return {"selected": selected, "blocks": blocks}
687708

688709

710+
def _toggle_button_style(element: dict) -> bool:
711+
"""Toggle button style and return whether it's now selected"""
712+
if element.get("style") == "primary":
713+
element.pop("style", None)
714+
return False
715+
else:
716+
element["style"] = "primary"
717+
return True
718+
719+
689720
# ================================================================
690721
# Session management
691722
# ================================================================

packages/slackBotFunction/tests/test_slack_events/test_slack_events_citations.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,3 +322,77 @@ def test_process_citation_events_update_chat_message_change_close_citation():
322322
expected_blocks = [citations, second_citation_body]
323323
mock_client.chat_update.assert_called()
324324
mock_client.chat_update.assert_called_with(channel="ABC", ts="123", blocks=expected_blocks)
325+
326+
327+
def test_create_response_body_no_error_without_citations(
328+
mock_get_parameter: Mock,
329+
mock_env: Mock,
330+
):
331+
"""Test regex text processing functionality within process_async_slack_event"""
332+
# delete and import module to test
333+
if "app.slack.slack_events" in sys.modules:
334+
del sys.modules["app.slack.slack_events"]
335+
from app.slack.slack_events import _create_response_body
336+
337+
# perform operation
338+
_create_response_body(
339+
citations=[],
340+
feedback_data={},
341+
response_text="This is a response without a citation.[1]",
342+
)
343+
344+
# assertions
345+
# no assertions as we are just checking it does not throw an error
346+
347+
348+
def test_create_response_body_creates_body_without_citations(
349+
mock_get_parameter: Mock,
350+
mock_env: Mock,
351+
):
352+
"""Test regex text processing functionality within process_async_slack_event"""
353+
# delete and import module to test
354+
if "app.slack.slack_events" in sys.modules:
355+
del sys.modules["app.slack.slack_events"]
356+
from app.slack.slack_events import _create_response_body
357+
358+
# perform operation
359+
response = _create_response_body(
360+
citations=[],
361+
feedback_data={},
362+
response_text="This is a response without a citation.",
363+
)
364+
365+
# assertions
366+
assert len(response) > 0
367+
assert response[0]["type"] == "section"
368+
assert "This is a response without a citation." in response[0]["text"]["text"]
369+
370+
371+
def test_create_response_body_creates_body_with_citations(
372+
mock_get_parameter: Mock,
373+
mock_env: Mock,
374+
):
375+
"""Test regex text processing functionality within process_async_slack_event"""
376+
# delete and import module to test
377+
if "app.slack.slack_events" in sys.modules:
378+
del sys.modules["app.slack.slack_events"]
379+
from app.slack.slack_events import _create_response_body
380+
381+
# perform operation
382+
response = _create_response_body(
383+
citations=[
384+
{
385+
"source_number": "1",
386+
"title": "Citation Title",
387+
"body": "Citation Body",
388+
"relevance_score": "0.95",
389+
}
390+
],
391+
feedback_data={},
392+
response_text="This is a response with a citation.[1]",
393+
)
394+
395+
# assertions
396+
assert len(response) > 1
397+
assert response[0]["type"] == "section"
398+
assert "This is a response with a citation.[1]" in response[0]["text"]["text"]

packages/slackBotFunction/tests/test_slack_events/test_slack_events_messages.py

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ def test_process_slack_message_with_session_storage(
259259
@patch("app.services.dynamo.get_state_information")
260260
@patch("app.services.ai_processor.process_ai_query")
261261
@patch("app.slack.slack_events.get_conversation_session")
262-
def test_process_slack_message_chat_update_error(
262+
def test_process_slack_message_chat_update_no_error(
263263
mock_get_session: Mock,
264264
mock_process_ai_query: Mock,
265265
mock_get_state_information: Mock,
@@ -292,6 +292,48 @@ def test_process_slack_message_chat_update_error(
292292
# no assertions as we are just checking it does not throw an error
293293

294294

295+
@patch("app.slack.slack_events.get_conversation_session")
296+
@patch("app.slack.slack_events.get_conversation_session_data")
297+
@patch("app.slack.slack_events.cleanup_previous_unfeedback_qa")
298+
@patch("app.slack.slack_events.update_session_latest_message")
299+
@patch("app.services.ai_processor.process_ai_query")
300+
def test_process_slack_message_chat_update_cleanup(
301+
mock_process_ai_query: Mock,
302+
mock_update_session_latest_message: Mock,
303+
mock_cleanup_previous_unfeedback_qa: Mock,
304+
mock_get_conversation_session_data: Mock,
305+
mock_get_session: Mock,
306+
mock_get_parameter: Mock,
307+
mock_env: Mock,
308+
):
309+
"""Test process_async_slack_event with chat_update error"""
310+
# set up mocks
311+
mock_client = Mock()
312+
mock_client.chat_postMessage.return_value = {"ts": "1234567890.124"}
313+
mock_client.chat_update.side_effect = Exception("Update failed")
314+
mock_process_ai_query.return_value = {
315+
"text": "AI response",
316+
"session_id": "session-123",
317+
"citations": [],
318+
"kb_response": {"output": {"text": "AI response"}},
319+
}
320+
mock_get_conversation_session_data.return_value = {"session_id": "session-123"}
321+
mock_get_session.return_value = None # No existing session
322+
mock_cleanup_previous_unfeedback_qa.return_value = {"test": "123"}
323+
324+
# delete and import module to test
325+
from app.slack.slack_events import process_slack_message
326+
327+
# perform operation
328+
slack_event_data = {"text": "<@U123> test question", "user": "U456", "channel": "C789", "ts": "1234567890.123"}
329+
with patch("app.slack.slack_events.get_conversation_session_data", mock_get_conversation_session_data):
330+
process_slack_message(event=slack_event_data, event_id="evt123", client=mock_client)
331+
332+
# assertions
333+
mock_cleanup_previous_unfeedback_qa.assert_called_once()
334+
mock_update_session_latest_message.assert_called_once()
335+
336+
295337
@patch("app.services.dynamo.get_state_information")
296338
@patch("app.services.ai_processor.process_ai_query")
297339
@patch("app.slack.slack_events.get_conversation_session")
@@ -331,3 +373,46 @@ def test_process_slack_message_dm_context(
331373

332374
# assertions
333375
# no assertions as we are just checking it does not throw an error
376+
377+
378+
@patch("app.services.dynamo.delete_state_information")
379+
def test_cleanup_previous_unfeedback_qa_no_previous_message(
380+
mock_delete_state_information: Mock,
381+
):
382+
"""Test cleanup skipped when no previous message exists"""
383+
conversation_key = "conv-123"
384+
current_message_ts = "1234567890.124"
385+
session_data = {}
386+
387+
if "app.slack.slack_events" in sys.modules:
388+
del sys.modules["app.slack.slack_events"]
389+
from app.slack.slack_events import cleanup_previous_unfeedback_qa
390+
391+
# perform operation
392+
cleanup_previous_unfeedback_qa(conversation_key, current_message_ts, session_data)
393+
394+
# assertions
395+
mock_delete_state_information.assert_not_called()
396+
397+
398+
@patch("app.services.dynamo.delete_state_information")
399+
def test_cleanup_previous_unfeedback_qa_same_message(
400+
mock_delete_state_information: Mock,
401+
):
402+
"""Test cleanup skipped when previous message is same as current"""
403+
if "app.slack.slack_events" in sys.modules:
404+
del sys.modules["app.slack.slack_events"]
405+
406+
conversation_key = "conv-123"
407+
current_message_ts = "1234567890.123"
408+
session_data = {"latest_message_ts": "1234567890.123"}
409+
410+
if "app.slack.slack_events" in sys.modules:
411+
del sys.modules["app.slack.slack_events"]
412+
from app.slack.slack_events import cleanup_previous_unfeedback_qa
413+
414+
# perform operation
415+
cleanup_previous_unfeedback_qa(conversation_key, current_message_ts, session_data)
416+
417+
# assertions
418+
mock_delete_state_information.assert_not_called()

0 commit comments

Comments
 (0)