Skip to content

Commit ba64a4b

Browse files
committed
Merge branch 'main' of https://github.com/NHSDigital/eps-assist-me into AEA-5546-prompting-and-query-reformulation
2 parents 7bea781 + 2f072ae commit ba64a4b

19 files changed

+1611
-767
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: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* eslint-disable @typescript-eslint/no-unused-vars */
1+
22
import {Stack} from "aws-cdk-lib"
33
import {NagPackSuppression, NagSuppressions} from "cdk-nag"
44

@@ -123,7 +123,7 @@ export const nagSuppressions = (stack: Stack) => {
123123
[
124124
{
125125
id: "AwsSolutions-IAM5",
126-
reason: "SlackBot Lambda uses wildcard permissions as recommended by AWS docs for Bedrock prompt management."
126+
reason: "SlackBot Lambda needs wildcard permissions for guardrails, knowledge bases, and function invocation."
127127
}
128128
]
129129
)
@@ -141,9 +141,12 @@ export const nagSuppressions = (stack: Stack) => {
141141
)
142142

143143
// Suppress secrets without rotation
144-
safeAddNagSuppression(
144+
safeAddNagSuppressionGroup(
145145
stack,
146-
"/EpsAssistMeStack/Secrets/SlackBotToken/Secret/Resource",
146+
[
147+
"/EpsAssistMeStack/Secrets/SlackBotToken/Secret/Resource",
148+
"/EpsAssistMeStack/Secrets/SlackBotSigning/Secret/Resource"
149+
],
147150
[
148151
{
149152
id: "AwsSolutions-SMG4",

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
@@ -48,7 +48,7 @@ export class Functions extends Construct {
4848
stackName: props.stackName,
4949
functionName: `${props.stackName}-CreateIndexFunction`,
5050
packageBasePath: "packages/createIndexFunction",
51-
entryPoint: "app.handler.py",
51+
handler: "app.handler.handler",
5252
logRetentionInDays: props.logRetentionInDays,
5353
logLevel: props.logLevel,
5454
environmentVariables: {"INDEX_NAME": props.collectionId},
@@ -60,7 +60,7 @@ export class Functions extends Construct {
6060
stackName: props.stackName,
6161
functionName: `${props.stackName}-SlackBotFunction`,
6262
packageBasePath: "packages/slackBotFunction",
63-
entryPoint: "app.handler.py",
63+
handler: "app.handler.handler",
6464
logRetentionInDays: props.logRetentionInDays,
6565
logLevel: props.logLevel,
6666
additionalPolicies: [props.slackBotManagedPolicy],
@@ -89,7 +89,7 @@ export class Functions extends Construct {
8989
stackName: props.stackName,
9090
functionName: `${props.stackName}-SyncKnowledgeBaseFunction`,
9191
packageBasePath: "packages/syncKnowledgeBaseFunction",
92-
entryPoint: "app.handler.py",
92+
handler: "app.handler.handler",
9393
logRetentionInDays: props.logRetentionInDays,
9494
logLevel: props.logLevel,
9595
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)