Skip to content

Commit 2f072ae

Browse files
New: [AEA-5547] - managing chat history (#21)
## Summary - ✨ New Feature - 🤖 Infra / Ops Update ### Details this PR introduces conversation management for the Slack bot and breaks down the old monolithic setup into cleaner, more modular components **Conversation Management** - updates DynamoDB table (`SlackBotStateTable`) to track sessions + handle event deduping - conversation memory via Bedrock session management → bot can keep context across multiple messages - works in both DMs and threaded channel convos - TTL cleanup: 30 days for sessions, 1 hour for dedupe entries (TODO: discuss what we want to set these values as) **Refactor** - split up the single `app.py` into separate modules: - `app/main.py` → Lambda entry + routing - `app/core/config.py` → config + AWS service wiring - `app/util/slack_events.py` → Bedrock query + convo logic - `app/util/slack_handlers.py` → Slack handlers + dedupe - split up the single test_app.py into separate modules: (WIP) - unit tests - integration test? **Infra** - DynamoDB table with KMS encryption - IAM updated for DynamoDB + KMS permissions - new env vars for table access --------- Co-authored-by: Kris Szlapa <[email protected]>
1 parent 020677a commit 2f072ae

19 files changed

+1574
-579
lines changed

packages/cdk/constructs/DynamoDbTable.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import {Construct} from "constructs"
22
import {RemovalPolicy} from "aws-cdk-lib"
33
import {
4-
TableV2,
54
AttributeType,
65
Billing,
7-
TableEncryptionV2
6+
TableEncryptionV2,
7+
TableV2
88
} from "aws-cdk-lib/aws-dynamodb"
99
import {Key} from "aws-cdk-lib/aws-kms"
1010

@@ -14,6 +14,10 @@ export interface DynamoDbTableProps {
1414
name: string
1515
type: AttributeType
1616
}
17+
readonly sortKey?: {
18+
name: string
19+
type: AttributeType
20+
}
1721
readonly timeToLiveAttribute?: string
1822
}
1923

@@ -29,14 +33,18 @@ export class DynamoDbTable extends Construct {
2933
description: `KMS key for ${props.tableName} DynamoDB table encryption`,
3034
removalPolicy: RemovalPolicy.DESTROY
3135
})
36+
3237
this.kmsKey.addAlias(`alias/${props.tableName}-dynamodb-key`)
3338

3439
this.table = new TableV2(this, props.tableName, {
3540
tableName: props.tableName,
3641
partitionKey: props.partitionKey,
42+
sortKey: props.sortKey,
3743
billing: Billing.onDemand(),
3844
timeToLiveAttribute: props.timeToLiveAttribute,
39-
pointInTimeRecovery: true,
45+
pointInTimeRecoverySpecification: {
46+
pointInTimeRecoveryEnabled: true
47+
},
4048
removalPolicy: RemovalPolicy.DESTROY,
4149
encryption: TableEncryptionV2.customerManagedKey(this.kmsKey)
4250
})

packages/cdk/constructs/LambdaFunction.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export interface LambdaFunctionProps {
2323
readonly stackName: string
2424
readonly functionName: string
2525
readonly packageBasePath: string
26-
readonly entryPoint: string
26+
readonly handler: string
2727
readonly environmentVariables: {[key: string]: string}
2828
readonly additionalPolicies?: Array<IManagedPolicy>
2929
readonly role?: Role
@@ -132,7 +132,7 @@ export class LambdaFunction extends Construct {
132132
memorySize: 256,
133133
timeout: Duration.seconds(50),
134134
architecture: Architecture.X86_64,
135-
handler: "app.handler.handler",
135+
handler: props.handler,
136136
code: Code.fromAsset(props.packageBasePath),
137137
role,
138138
environment: {

packages/cdk/nagSuppressions.ts

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
1-
/* eslint-disable @typescript-eslint/no-unused-vars */
1+
22
import {Stack} from "aws-cdk-lib"
33
import {NagPackSuppression, NagSuppressions} from "cdk-nag"
44

55
export const nagSuppressions = (stack: Stack) => {
66
const stackName = stack.node.tryGetContext("stackName") || "epsam"
7-
const account = Stack.of(stack).account
87
// Suppress granular wildcard on log stream for SlackBot Lambda
98
safeAddNagSuppression(
109
stack,
1110
"/EpsAssistMeStack/Functions/SlackBotLambda/LambdaPutLogsManagedPolicy/Resource",
1211
[
1312
{
1413
id: "AwsSolutions-IAM5",
15-
reason: "Wildcard permissions for log stream access are required and scoped appropriately.",
16-
appliesTo: [
17-
"Resource::<FunctionsSlackBotLambdaLambdaLogGroup3597D783.Arn>:log-stream:*"
18-
]
14+
reason: "Wildcard permissions for log stream access are required and scoped appropriately."
1915
}
2016
]
2117
)
@@ -27,10 +23,7 @@ export const nagSuppressions = (stack: Stack) => {
2723
[
2824
{
2925
id: "AwsSolutions-IAM5",
30-
reason: "Wildcard permissions are required for log stream access under known paths.",
31-
appliesTo: [
32-
"Resource::<FunctionsCreateIndexFunctionLambdaLogGroupB45008DF.Arn>:log-stream:*"
33-
]
26+
reason: "Wildcard permissions are required for log stream access under known paths."
3427
}
3528
]
3629
)
@@ -121,11 +114,7 @@ export const nagSuppressions = (stack: Stack) => {
121114
[
122115
{
123116
id: "AwsSolutions-IAM5",
124-
reason: "Lambda needs access to all OpenSearch collections and indexes to create and manage indexes.",
125-
appliesTo: [
126-
`Resource::arn:aws:aoss:eu-west-2:${account}:collection/*`,
127-
`Resource::arn:aws:aoss:eu-west-2:${account}:index/*`
128-
]
117+
reason: "Lambda needs access to all OpenSearch collections and indexes to create and manage indexes."
129118
}
130119
]
131120
)
@@ -137,12 +126,7 @@ export const nagSuppressions = (stack: Stack) => {
137126
[
138127
{
139128
id: "AwsSolutions-IAM5",
140-
reason: "SlackBot Lambda needs wildcard access for Lambda functions (self-invocation) and KMS operations.",
141-
appliesTo: [
142-
`Resource::arn:aws:lambda:eu-west-2:${account}:function:*`,
143-
"Action::kms:GenerateDataKey*",
144-
"Action::kms:ReEncrypt*"
145-
]
129+
reason: "SlackBot Lambda needs wildcard permissions for guardrails, knowledge bases, and function invocation."
146130
}
147131
]
148132
)
@@ -160,13 +144,16 @@ export const nagSuppressions = (stack: Stack) => {
160144
)
161145

162146
// Suppress secrets without rotation
163-
safeAddNagSuppression(
147+
safeAddNagSuppressionGroup(
164148
stack,
165-
"/EpsAssistMeStack/Secrets/SlackBotToken/Secret/Resource",
149+
[
150+
"/EpsAssistMeStack/Secrets/SlackBotToken/Secret/Resource",
151+
"/EpsAssistMeStack/Secrets/SlackBotSigning/Secret/Resource"
152+
],
166153
[
167154
{
168155
id: "AwsSolutions-SMG4",
169-
reason: "Slack bot token rotation is handled manually as part of the Slack app configuration process."
156+
reason: "Slack secrets rotation is handled manually as part of the Slack app configuration process."
170157
}
171158
]
172159
)

packages/cdk/resources/DatabaseTables.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ export class DatabaseTables extends Construct {
1515
this.slackBotStateTable = new DynamoDbTable(this, "SlackBotStateTable", {
1616
tableName: `${props.stackName}-SlackBotState`,
1717
partitionKey: {
18-
name: "eventId",
18+
name: "pk",
19+
type: AttributeType.STRING
20+
},
21+
sortKey: {
22+
name: "sk",
1923
type: AttributeType.STRING
2024
},
2125
timeToLiveAttribute: "ttl"

packages/cdk/resources/Functions.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class Functions extends Construct {
4444
stackName: props.stackName,
4545
functionName: `${props.stackName}-CreateIndexFunction`,
4646
packageBasePath: "packages/createIndexFunction",
47-
entryPoint: "app.handler.py",
47+
handler: "app.handler.handler",
4848
logRetentionInDays: props.logRetentionInDays,
4949
logLevel: props.logLevel,
5050
environmentVariables: {"INDEX_NAME": props.collectionId},
@@ -56,7 +56,7 @@ export class Functions extends Construct {
5656
stackName: props.stackName,
5757
functionName: `${props.stackName}-SlackBotFunction`,
5858
packageBasePath: "packages/slackBotFunction",
59-
entryPoint: "app.handler.py",
59+
handler: "app.handler.handler",
6060
logRetentionInDays: props.logRetentionInDays,
6161
logLevel: props.logLevel,
6262
additionalPolicies: [props.slackBotManagedPolicy],
@@ -82,7 +82,7 @@ export class Functions extends Construct {
8282
stackName: props.stackName,
8383
functionName: `${props.stackName}-SyncKnowledgeBaseFunction`,
8484
packageBasePath: "packages/syncKnowledgeBaseFunction",
85-
entryPoint: "app.handler.py",
85+
handler: "app.handler.handler",
8686
logRetentionInDays: props.logRetentionInDays,
8787
logLevel: props.logLevel,
8888
environmentVariables: {

packages/slackBotFunction/app/config/config.py

Lines changed: 0 additions & 51 deletions
This file was deleted.
File renamed without changes.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""
2+
Core configuration for the Slack bot.
3+
Sets up all the AWS and Slack connections we need.
4+
"""
5+
6+
import os
7+
import json
8+
import boto3
9+
from slack_bolt import App
10+
from aws_lambda_powertools import Logger
11+
from aws_lambda_powertools.utilities.parameters import get_parameter
12+
13+
14+
# set up logging
15+
logger = Logger(service="slackBotFunction")
16+
17+
# DynamoDB table for deduplication and session storage
18+
dynamodb = boto3.resource("dynamodb")
19+
table = dynamodb.Table(os.environ["SLACK_BOT_STATE_TABLE"])
20+
21+
# get Slack credentials from Parameter Store
22+
bot_token_parameter = os.environ["SLACK_BOT_TOKEN_PARAMETER"]
23+
signing_secret_parameter = os.environ["SLACK_SIGNING_SECRET_PARAMETER"]
24+
25+
try:
26+
bot_token_raw = get_parameter(bot_token_parameter, decrypt=True)
27+
signing_secret_raw = get_parameter(signing_secret_parameter, decrypt=True)
28+
29+
if not bot_token_raw or not signing_secret_raw:
30+
raise ValueError("Missing required parameters from Parameter Store")
31+
32+
bot_token_data = json.loads(bot_token_raw)
33+
signing_secret_data = json.loads(signing_secret_raw)
34+
35+
bot_token = bot_token_data.get("token")
36+
signing_secret = signing_secret_data.get("secret")
37+
38+
if not bot_token or not signing_secret:
39+
raise ValueError("Missing required parameters: token or secret in Parameter Store values")
40+
41+
except json.JSONDecodeError as e:
42+
raise ValueError(f"Invalid JSON in Parameter Store: {e}")
43+
except Exception as e:
44+
logger.error(f"Configuration error: {e}")
45+
raise
46+
47+
# initialise the Slack app
48+
app = App(
49+
process_before_response=True,
50+
token=bot_token,
51+
signing_secret=signing_secret,
52+
)
53+
54+
# Bedrock configuration from environment
55+
KNOWLEDGEBASE_ID = os.environ["KNOWLEDGEBASE_ID"]
56+
RAG_MODEL_ID = os.environ["RAG_MODEL_ID"]
57+
AWS_REGION = os.environ["AWS_REGION"]
58+
GUARD_RAIL_ID = os.environ["GUARD_RAIL_ID"]
59+
GUARD_VERSION = os.environ["GUARD_RAIL_VERSION"]
60+
61+
logger.info(f"Guardrail ID: {GUARD_RAIL_ID}, Version: {GUARD_VERSION}")
62+
63+
# Bot response messages
64+
BOT_MESSAGES = {
65+
"empty_query": "Hi there! Please ask me a question and I'll help you find information from our knowledge base.",
66+
"error_response": "Sorry, an error occurred while processing your request. Please try again later.",
67+
}

packages/slackBotFunction/app/handler.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,16 @@
77
"""
88

99
from slack_bolt.adapter.aws_lambda import SlackRequestHandler
10-
from aws_lambda_powertools import Logger
1110
from aws_lambda_powertools.utilities.typing import LambdaContext
12-
from app.config.config import app
11+
12+
from app.core.config import app, logger
1313
from app.slack.slack_events import process_async_slack_event
1414
from app.slack.slack_handlers import setup_handlers
1515

16-
# Register Slack event handlers (@mentions, DMs, etc.)
16+
# register event handlers with the app
1717
setup_handlers(app)
1818

19-
logger = Logger(service="slackBotFunction")
20-
2119

22-
@logger.inject_lambda_context
2320
def handler(event: dict, context: LambdaContext) -> dict:
2421
"""
2522
Main Lambda entry point - routes between Slack webhook and async processing
@@ -29,15 +26,18 @@ def handler(event: dict, context: LambdaContext) -> dict:
2926
2. Lambda acknowledges immediately and triggers async self-invocation
3027
3. Async invocation processes Bedrock query and responds to Slack
3128
"""
32-
logger.info("Lambda invoked for Slack bot", extra={"event": event})
29+
logger.info("Lambda invoked", extra={"is_async": event.get("async_processing", False)})
3330

34-
# Route 2: Async processing path (self-invoked)
31+
# handle async processing requests
3532
if event.get("async_processing"):
36-
# Process the actual AI query without time constraints
37-
process_async_slack_event(event["slack_event"])
33+
slack_event_data = event.get("slack_event")
34+
if not slack_event_data:
35+
logger.error("Async processing requested but no slack_event provided")
36+
return {"statusCode": 400}
37+
38+
process_async_slack_event(slack_event_data)
3839
return {"statusCode": 200}
3940

40-
# Route 1: Slack webhook path (via API Gateway)
41-
# Handle initial Slack event, acknowledge quickly, trigger async processing
41+
# handle Slack webhook requests
4242
slack_handler = SlackRequestHandler(app=app)
4343
return slack_handler.handle(event, context)

0 commit comments

Comments
 (0)