Skip to content

Commit afe9123

Browse files
committed
finish refactor
1 parent a884fc5 commit afe9123

File tree

15 files changed

+1202
-1078
lines changed

15 files changed

+1202
-1078
lines changed

packages/slackBotFunction/app/core/config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import boto3
1111
from aws_lambda_powertools import Logger
1212
from aws_lambda_powertools.utilities.parameters import get_parameter
13+
from mypy_boto3_dynamodb.service_resource import Table
1314

1415

1516
@lru_cache()
@@ -21,7 +22,7 @@ def get_logger():
2122

2223

2324
@lru_cache()
24-
def get_slack_bot_state_table():
25+
def get_slack_bot_state_table() -> Table:
2526
# DynamoDB table for deduplication and session storage
2627
dynamodb = boto3.resource("dynamodb")
2728
return dynamodb.Table(os.environ["SLACK_BOT_STATE_TABLE"])
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import boto3
2+
from mypy_boto3_bedrock_agent_runtime import AgentsforBedrockRuntimeClient
3+
from app.core.config import get_guardrail_config, get_logger
4+
5+
6+
logger = get_logger()
7+
8+
9+
def query_bedrock(user_query, session_id=None):
10+
"""
11+
Query Amazon Bedrock Knowledge Base using RAG (Retrieval-Augmented Generation)
12+
13+
This function retrieves relevant documents from the knowledge base and generates
14+
a response using the configured LLM model with guardrails for safety.
15+
"""
16+
17+
KNOWLEDGEBASE_ID, RAG_MODEL_ID, AWS_REGION, GUARD_RAIL_ID, GUARD_VERSION = get_guardrail_config()
18+
client: AgentsforBedrockRuntimeClient = boto3.client(
19+
service_name="bedrock-agent-runtime",
20+
region_name=AWS_REGION,
21+
)
22+
request_params = {
23+
"input": {"text": user_query},
24+
"retrieveAndGenerateConfiguration": {
25+
"type": "KNOWLEDGE_BASE",
26+
"knowledgeBaseConfiguration": {
27+
"knowledgeBaseId": KNOWLEDGEBASE_ID,
28+
"modelArn": RAG_MODEL_ID,
29+
"generationConfiguration": {
30+
"guardrailConfiguration": {
31+
"guardrailId": GUARD_RAIL_ID,
32+
"guardrailVersion": GUARD_VERSION,
33+
}
34+
},
35+
},
36+
},
37+
}
38+
39+
# Include session ID for conversation continuity across messages
40+
if session_id:
41+
request_params["sessionId"] = session_id
42+
logger.info("Using existing session", extra={"session_id": session_id})
43+
else:
44+
logger.info("Starting new conversation")
45+
46+
response = client.retrieve_and_generate(**request_params)
47+
logger.info(
48+
"Got Bedrock response",
49+
extra={"session_id": response.get("sessionId"), "has_citations": len(response.get("citations", [])) > 0},
50+
)
51+
return response
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from app.core.config import get_slack_bot_state_table
2+
3+
4+
def get_state_information(key):
5+
table = get_slack_bot_state_table()
6+
table.get_item(Key=key)
7+
8+
9+
def store_state_information(item, condition):
10+
table = get_slack_bot_state_table()
11+
if condition:
12+
table.put_item(Item=item, ConditionExpression=condition)
13+
else:
14+
table.put_item(Item=item)
15+
16+
17+
def update_state_information(key, update_expression, expression_attribute_values):
18+
table = get_slack_bot_state_table()
19+
table.update_item(
20+
Key=key, UpdateExpression=update_expression, ExpressionAttributeValues=expression_attribute_values
21+
)
22+
23+
24+
def delete_state_information(pk, sk, condition):
25+
table = get_slack_bot_state_table()
26+
table.delete_item(
27+
Key={"pk": pk, "sk": sk},
28+
ConditionExpression=condition,
29+
)

packages/slackBotFunction/app/slack/slack_events.py

Lines changed: 34 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import time
88
import traceback
99
import json
10-
import boto3
1110
from botocore.exceptions import ClientError
1211
from slack_sdk import WebClient
1312
from app.core.config import (
@@ -23,10 +22,14 @@
2322
TTL_FEEDBACK,
2423
TTL_SESSION,
2524
get_bot_messages,
26-
get_guardrail_config,
27-
get_slack_bot_state_table,
2825
get_logger,
29-
get_table,
26+
)
27+
from app.services.bedrock import query_bedrock
28+
from app.services.dynamo import (
29+
delete_state_information,
30+
get_state_information,
31+
store_state_information,
32+
update_state_information,
3033
)
3134
from app.services.query_reformulator import reformulate_query
3235

@@ -47,10 +50,8 @@ def cleanup_previous_unfeedback_qa(conversation_key, current_message_ts, session
4750
return
4851

4952
# Atomically delete Q&A only if no feedback received
50-
table = get_table()
51-
table.delete_item(
52-
Key={"pk": f"qa#{conversation_key}#{previous_message_ts}", "sk": "turn"},
53-
ConditionExpression="attribute_not_exists(feedback_received)",
53+
delete_state_information(
54+
f"qa#{conversation_key}#{previous_message_ts}", "turn", "attribute_not_exists(feedback_received)"
5455
)
5556
logger.info("Deleted unfeedback Q&A for privacy", extra={"message_ts": previous_message_ts})
5657

@@ -68,20 +69,18 @@ def store_qa_pair(conversation_key, user_query, bot_response, message_ts, sessio
6869
Store Q&A pair for feedback correlation
6970
"""
7071
try:
71-
table = get_table()
72-
table.put_item(
73-
Item={
74-
"pk": f"qa#{conversation_key}#{message_ts}",
75-
"sk": "turn",
76-
"user_query": user_query[:1000] if user_query else None,
77-
"bot_response": bot_response[:2000] if bot_response else None,
78-
"session_id": session_id,
79-
"user_id": user_id,
80-
"message_ts": message_ts,
81-
"created_at": int(time.time()),
82-
"ttl": int(time.time()) + TTL_FEEDBACK,
83-
}
84-
)
72+
item = {
73+
"pk": f"qa#{conversation_key}#{message_ts}",
74+
"sk": "turn",
75+
"user_query": user_query[:1000] if user_query else None,
76+
"bot_response": bot_response[:2000] if bot_response else None,
77+
"session_id": session_id,
78+
"user_id": user_id,
79+
"message_ts": message_ts,
80+
"created_at": int(time.time()),
81+
"ttl": int(time.time()) + TTL_FEEDBACK,
82+
}
83+
store_state_information(item)
8584
logger.info("Stored Q&A pair", extra={"conversation_key": conversation_key, "message_ts": message_ts})
8685
except Exception as e:
8786
logger.error("Failed to store Q&A pair", extra={"error": str(e)})
@@ -92,11 +91,10 @@ def _mark_qa_feedback_received(conversation_key, message_ts):
9291
Mark Q&A record as having received feedback to prevent deletion
9392
"""
9493
try:
95-
table = get_table()
96-
table.update_item(
97-
Key={"pk": f"qa#{conversation_key}#{message_ts}", "sk": "turn"},
98-
UpdateExpression="SET feedback_received = :val",
99-
ExpressionAttributeValues={":val": True},
94+
update_state_information(
95+
{"pk": f"qa#{conversation_key}#{message_ts}", "sk": "turn"},
96+
"SET feedback_received = :val",
97+
{":val": True},
10098
)
10199
except Exception as e:
102100
logger.error("Error marking Q&A feedback received", extra={"error": str(e)})
@@ -147,7 +145,7 @@ def _create_feedback_blocks(response_text, conversation_key, channel, message_ts
147145
if thread_ts: # Only include thread_ts for channel threads, not DMs
148146
feedback_data["tt"] = thread_ts
149147
feedback_value = json.dumps(feedback_data, separators=(",", ":"))
150-
148+
BOT_MESSAGES = get_bot_messages()
151149
return [
152150
{"type": "section", "text": {"type": "mrkdwn", "text": response_text}},
153151
{"type": "section", "text": {"type": "plain_text", "text": BOT_MESSAGES["feedback_prompt"]}},
@@ -327,11 +325,7 @@ def store_feedback(
327325
if feedback_text:
328326
feedback_item["feedback_text"] = feedback_text[:4000]
329327

330-
table = get_slack_bot_state_table()
331-
if condition:
332-
table.put_item(Item=feedback_item, ConditionExpression=condition)
333-
else:
334-
table.put_item(Item=feedback_item)
328+
store_state_information(feedback_item, condition)
335329

336330
# Mark Q&A as having received feedback to prevent deletion
337331
if message_ts:
@@ -372,8 +366,7 @@ def get_conversation_session_data(conversation_key):
372366
Get full session data for this conversation
373367
"""
374368
try:
375-
slack_bot_state_table = get_slack_bot_state_table()
376-
response = slack_bot_state_table.get_item(Key={"pk": conversation_key, "sk": "session"})
369+
response = get_state_information({"pk": conversation_key, "sk": "session"})
377370
if "Item" in response:
378371
logger.info("Found existing session", extra={"conversation_key": conversation_key})
379372
return response["Item"]
@@ -388,7 +381,7 @@ def get_latest_message_ts(conversation_key):
388381
Get latest message timestamp from session
389382
"""
390383
try:
391-
response = table.get_item(Key={"pk": conversation_key, "sk": SESSION_SK})
384+
response = get_state_information({"pk": conversation_key, "sk": SESSION_SK})
392385
if "Item" in response:
393386
return response["Item"].get("latest_message_ts")
394387
return None
@@ -421,8 +414,7 @@ def store_conversation_session(
421414
if latest_message_ts:
422415
item["latest_message_ts"] = latest_message_ts
423416

424-
table = get_slack_bot_state_table()
425-
table.put_item(Item=item)
417+
store_state_information(item)
426418
logger.info("Stored session", extra={"session_id": session_id, "conversation_key": conversation_key})
427419
except Exception:
428420
logger.error("Error storing session", extra={"error": traceback.format_exc()})
@@ -433,61 +425,10 @@ def update_session_latest_message(conversation_key, message_ts):
433425
Update session with latest message timestamp
434426
"""
435427
try:
436-
table = get_slack_bot_state_table()
437-
table.update_item(
438-
Key={"pk": conversation_key, "sk": SESSION_SK},
439-
UpdateExpression="SET latest_message_ts = :ts",
440-
ExpressionAttributeValues={":ts": message_ts},
428+
update_state_information(
429+
{"pk": conversation_key, "sk": SESSION_SK},
430+
"SET latest_message_ts = :ts",
431+
{":ts": message_ts},
441432
)
442433
except Exception as e:
443434
logger.error("Error updating session latest message", extra={"error": str(e)})
444-
445-
446-
# ================================================================
447-
# Bedrock integration
448-
# ================================================================
449-
450-
451-
def query_bedrock(user_query, session_id=None):
452-
"""
453-
Query Amazon Bedrock Knowledge Base using RAG (Retrieval-Augmented Generation)
454-
455-
This function retrieves relevant documents from the knowledge base and generates
456-
a response using the configured LLM model with guardrails for safety.
457-
"""
458-
459-
KNOWLEDGEBASE_ID, RAG_MODEL_ID, AWS_REGION, GUARD_RAIL_ID, GUARD_VERSION = get_guardrail_config()
460-
client = boto3.client(
461-
service_name="bedrock-agent-runtime",
462-
region_name=AWS_REGION,
463-
)
464-
request_params = {
465-
"input": {"text": user_query},
466-
"retrieveAndGenerateConfiguration": {
467-
"type": "KNOWLEDGE_BASE",
468-
"knowledgeBaseConfiguration": {
469-
"knowledgeBaseId": KNOWLEDGEBASE_ID,
470-
"modelArn": RAG_MODEL_ID,
471-
"generationConfiguration": {
472-
"guardrailConfiguration": {
473-
"guardrailId": GUARD_RAIL_ID,
474-
"guardrailVersion": GUARD_VERSION,
475-
}
476-
},
477-
},
478-
},
479-
}
480-
481-
# Include session ID for conversation continuity across messages
482-
if session_id:
483-
request_params["sessionId"] = session_id
484-
logger.info("Using existing session", extra={"session_id": session_id})
485-
else:
486-
logger.info("Starting new conversation")
487-
488-
response = client.retrieve_and_generate(**request_params)
489-
logger.info(
490-
"Got Bedrock response",
491-
extra={"session_id": response.get("sessionId"), "has_citations": len(response.get("citations", [])) > 0},
492-
)
493-
return response

packages/slackBotFunction/app/slack/slack_handlers.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
SESSION_SK,
2121
DM_PREFIX,
2222
THREAD_PREFIX,
23-
get_slack_bot_state_table,
2423
)
24+
from app.services.dynamo import get_state_information
2525
from app.utils.handler_utils import is_duplicate_event, trigger_async_processing, respond_with_eyes
2626
from app.slack.slack_events import store_feedback
2727

@@ -197,8 +197,7 @@ def thread_message_handler(event, event_id, client):
197197

198198
conversation_key = f"{THREAD_PREFIX}{channel_id}#{thread_root}"
199199
try:
200-
table = get_slack_bot_state_table()
201-
resp = table.get_item(Key={"pk": conversation_key, "sk": SESSION_SK})
200+
resp = get_state_information({"pk": conversation_key, "sk": SESSION_SK})
202201
if "Item" not in resp:
203202
logger.info(f"No session found for thread: {conversation_key}")
204203
return # not a bot-owned thread; ignore
@@ -236,6 +235,7 @@ def thread_message_handler(event, event_id, client):
236235
return
237236

238237
# Follow-up in a bot-owned thread (no re-mention required)
238+
bot_token = get_bot_token()
239239
trigger_async_processing({"event": event, "event_id": event_id, "bot_token": bot_token})
240240

241241

@@ -330,8 +330,7 @@ def setup_handlers(app):
330330
def _is_latest_message(conversation_key, message_ts):
331331
"""Check if message_ts is the latest bot message using session data"""
332332
try:
333-
table = get_slack_bot_state_table()
334-
response = table.get_item(Key={"pk": conversation_key, "sk": SESSION_SK})
333+
response = get_state_information({"pk": conversation_key, "sk": SESSION_SK})
335334
if "Item" in response:
336335
latest_message_ts = response["Item"].get("latest_message_ts")
337336
return latest_message_ts == message_ts

packages/slackBotFunction/app/utils/handler_utils.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
import boto3
99
from botocore.exceptions import ClientError
1010
from slack_sdk import WebClient
11-
from app.core.config import get_slack_bot_state_table, get_logger
11+
from app.core.config import get_logger
1212
import os
1313

14+
from app.services.dynamo import store_state_information
15+
1416
logger = get_logger()
1517

1618

@@ -23,10 +25,9 @@ def is_duplicate_event(event_id):
2325
"""
2426
try:
2527
ttl = int(time.time()) + 3600 # 1 hour TTL
26-
slack_bot_state_table = get_slack_bot_state_table()
27-
slack_bot_state_table.put_item(
28-
Item={"pk": f"event#{event_id}", "sk": "dedup", "ttl": ttl, "timestamp": int(time.time())},
29-
ConditionExpression="attribute_not_exists(pk)",
28+
store_state_information(
29+
{"pk": f"event#{event_id}", "sk": "dedup", "ttl": ttl, "timestamp": int(time.time())},
30+
"attribute_not_exists(pk)",
3031
)
3132
return False # Not a duplicate
3233
except ClientError as e:

packages/slackBotFunction/tests/conftest.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ def mock_env():
2222
"GUARD_RAIL_ID": "test-guard-id",
2323
"GUARD_RAIL_VERSION": "1",
2424
"AWS_LAMBDA_FUNCTION_NAME": "test-function",
25+
"QUERY_REFORMULATION_MODEL_ID": "test-model",
26+
"QUERY_REFORMULATION_PROMPT_NAME": "test-prompt",
27+
"QUERY_REFORMULATION_PROMPT_VERSION": "DRAFT",
2528
}
2629
env_vars["AWS_DEFAULT_REGION"] = env_vars["AWS_REGION"]
2730
with patch.dict(os.environ, env_vars, clear=False):
@@ -51,7 +54,7 @@ def fake_get_parameter(name, *args, **kwargs):
5154

5255
@pytest.fixture
5356
def mock_slack_app():
54-
with patch("app.services.app.App") as mock_app_cls:
57+
with patch("slack_bolt.App") as mock_app_cls:
5558
mock_instance = MagicMock()
5659
mock_app_cls.return_value = mock_instance
5760
yield mock_instance

0 commit comments

Comments
 (0)