Skip to content

Commit 43f94ee

Browse files
committed
Merge branch 'main' of https://github.com/NHSDigital/eps-assist-me into dependabot/pip/boto3-stubs-1.40.39
2 parents 227f68f + d6946c2 commit 43f94ee

38 files changed

+3511
-2552
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ repos:
6565
entry: bash
6666
args:
6767
- -c
68-
- 'docker run -v "$(pwd):/src" git-secrets --pre_commit_hook'
68+
- 'docker run -v "$LOCAL_WORKSPACE_FOLDER:/src" git-secrets --pre_commit_hook'
6969
language: system
7070

7171
fail_fast: true

package-lock.json

Lines changed: 388 additions & 387 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,20 @@
1919
"devDependencies": {
2020
"@semantic-release/changelog": "^6.0.3",
2121
"@semantic-release/release-notes-generator": "^14.1.0",
22-
"@types/aws-lambda": "^8.10.152",
22+
"@types/aws-lambda": "^8.10.153",
2323
"@types/jest": "^30.0.0",
24-
"@types/node": "^24.5.2",
25-
"@typescript-eslint/eslint-plugin": "^8.44.1",
24+
"@types/node": "^24.6.2",
25+
"@typescript-eslint/eslint-plugin": "^8.45.0",
2626
"@typescript-eslint/parser": "^8.44.1",
2727
"eslint": "^9.36.0",
2828
"eslint-plugin-import-newlines": "^1.4.0",
29-
"jest": "^30.1.3",
29+
"jest": "^30.2.0",
3030
"jest-junit": "^16.0.0",
3131
"license-checker": "^25.0.1",
3232
"semantic-release": "^24.2.9",
3333
"ts-jest": "^29.4.4",
3434
"ts-node": "^10.9.2",
35-
"typescript": "^5.9.2"
35+
"typescript": "^5.9.3"
3636
},
3737
"dependencies": {
3838
"conventional-changelog-eslint": "^6.0.0",

packages/cdk/constructs/LambdaFunction.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const insightsLayerArn = "arn:aws:lambda:eu-west-2:580247275435:layer:LambdaInsi
3737
export class LambdaFunction extends Construct {
3838
public readonly executionPolicy: ManagedPolicy
3939
public readonly function: LambdaFunctionResource
40+
public readonly executionRole: Role
4041

4142
public constructor(scope: Construct, id: string, props: LambdaFunctionProps) {
4243
super(scope, id)
@@ -169,5 +170,6 @@ export class LambdaFunction extends Construct {
169170
// Export Lambda function and execution policy for use by other constructs
170171
this.function = lambdaFunction
171172
this.executionPolicy = executionManagedPolicy
173+
this.executionRole = role
172174
}
173175
}

packages/cdk/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@
1212
"dependencies": {
1313
"@aws-cdk/aws-lambda-python-alpha": "^2.205.0-alpha.0",
1414
"@cdklabs/generative-ai-cdk-constructs": "^0.1.309",
15-
"aws-cdk-lib": "^2.217.0",
16-
"cdk-nag": "^2.37.38",
15+
"aws-cdk-lib": "^2.219.0",
16+
"cdk-nag": "^2.37.42",
1717
"constructs": "^10.4.2",
1818
"source-map-support": "^0.5.21"
1919
},
2020
"devDependencies": {
21-
"@types/node": "^24.5.2",
22-
"aws-cdk": "^2.1029.3"
21+
"@types/node": "^24.6.2",
22+
"aws-cdk": "^2.1029.4"
2323
}
2424
}

packages/cdk/resources/Apis.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {HttpMethod} from "aws-cdk-lib/aws-lambda"
77
export interface ApisProps {
88
readonly stackName: string
99
readonly logRetentionInDays: number
10-
readonly enableMutalTls: boolean
1110
functions: {[key: string]: LambdaFunction}
1211
}
1312

packages/cdk/resources/Functions.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {Construct} from "constructs"
22
import {LambdaFunction} from "../constructs/LambdaFunction"
3-
import {ManagedPolicy} from "aws-cdk-lib/aws-iam"
3+
import {ManagedPolicy, PolicyStatement, Role} from "aws-cdk-lib/aws-iam"
44
import {StringParameter} from "aws-cdk-lib/aws-ssm"
55
import {Secret} from "aws-cdk-lib/aws-secretsmanager"
66
import {TableV2} from "aws-cdk-lib/aws-dynamodb"
@@ -35,10 +35,13 @@ export interface FunctionsProps {
3535
readonly slackBotSigningSecret: Secret
3636
readonly slackBotStateTable: TableV2
3737
readonly promptName: string
38+
readonly isPullRequest: boolean
39+
readonly mainSlackBotLambdaExecutionRoleArn : string
3840
}
3941

4042
export class Functions extends Construct {
41-
public readonly functions: {[key: string]: LambdaFunction}
43+
public readonly slackBotLambda: LambdaFunction
44+
public readonly syncKnowledgeBaseFunction: LambdaFunction
4245

4346
constructor(scope: Construct, id: string, props: FunctionsProps) {
4447
super(scope, id)
@@ -73,6 +76,30 @@ export class Functions extends Construct {
7376
props.slackBotTokenSecret.grantRead(slackBotLambda.function)
7477
props.slackBotSigningSecret.grantRead(slackBotLambda.function)
7578

79+
if (props.isPullRequest) {
80+
const mainSlackBotLambdaExecutionRole = Role.fromRoleArn(
81+
this,
82+
"mainRoleArn",
83+
props.mainSlackBotLambdaExecutionRoleArn, {
84+
mutable: true
85+
})
86+
87+
const executeSlackBotPolicy = new ManagedPolicy(this, "ExecuteSlackBotPolicy", {
88+
description: "foo",
89+
statements: [
90+
new PolicyStatement({
91+
actions: [
92+
"lambda:invokeFunction"
93+
],
94+
resources: [
95+
slackBotLambda.function.functionArn
96+
]
97+
})
98+
]
99+
})
100+
mainSlackBotLambdaExecutionRole.addManagedPolicy(executeSlackBotPolicy)
101+
}
102+
76103
// Lambda function to sync knowledge base on S3 events
77104
const syncKnowledgeBaseFunction = new LambdaFunction(this, "SyncKnowledgeBaseFunction", {
78105
stackName: props.stackName,
@@ -89,9 +116,7 @@ export class Functions extends Construct {
89116
additionalPolicies: [props.syncKnowledgeBaseManagedPolicy]
90117
})
91118

92-
this.functions = {
93-
slackBot: slackBotLambda,
94-
syncKnowledgeBase: syncKnowledgeBaseFunction
95-
}
119+
this.slackBotLambda = slackBotLambda
120+
this.syncKnowledgeBaseFunction = syncKnowledgeBaseFunction
96121
}
97122
}

packages/cdk/resources/RuntimePolicies.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,15 @@ export class RuntimePolicies extends Construct {
135135
resources: [props.slackBotStateTableKmsKeyArn]
136136
})
137137

138+
const slackBotDescribeCfStacks = new PolicyStatement({
139+
actions: [
140+
"cloudformation:DescribeStacks"
141+
],
142+
resources: [
143+
`arn:aws:cloudformation:eu-west-2:${props.account}:stack/epsam-pr-*`
144+
]
145+
})
146+
138147
this.slackBotPolicy = new ManagedPolicy(this, "SlackBotPolicy", {
139148
description: "Policy for SlackBot Lambda to access Bedrock, SSM, Lambda, DynamoDB, and KMS",
140149
statements: [
@@ -145,7 +154,8 @@ export class RuntimePolicies extends Construct {
145154
slackBotLambdaPolicy,
146155
slackBotGuardrailPolicy,
147156
slackBotDynamoDbPolicy,
148-
slackBotKmsPolicy
157+
slackBotKmsPolicy,
158+
slackBotDescribeCfStacks
149159
]
150160
})
151161

packages/cdk/stacks/EpsAssistMeStack.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import {
22
App,
33
Stack,
44
StackProps,
5-
CfnOutput
5+
CfnOutput,
6+
Fn
67
} from "aws-cdk-lib"
78
import {nagSuppressions} from "../nagSuppressions"
89
import {Apis} from "../resources/Apis"
@@ -30,6 +31,9 @@ export class EpsAssistMeStack extends Stack {
3031
public constructor(scope: App, id: string, props: EpsAssistMeStackProps) {
3132
super(scope, id, props)
3233

34+
// imports
35+
const mainSlackBotLambdaExecutionRoleArn = Fn.importValue("epsam:lambda:SlackBot:ExecutionRole:Arn")
36+
3337
// Get variables from context
3438
const region = Stack.of(this).region
3539
const account = Stack.of(this).account
@@ -131,7 +135,9 @@ export class EpsAssistMeStack extends Stack {
131135
slackBotTokenSecret: secrets.slackBotTokenSecret,
132136
slackBotSigningSecret: secrets.slackBotSigningSecret,
133137
slackBotStateTable: tables.slackBotStateTable.table,
134-
promptName: bedrockPromptResources.queryReformulationPrompt.promptName
138+
promptName: bedrockPromptResources.queryReformulationPrompt.promptName,
139+
isPullRequest: isPullRequest,
140+
mainSlackBotLambdaExecutionRoleArn: mainSlackBotLambdaExecutionRoleArn
135141
})
136142

137143
// Create vector index after Functions are created
@@ -147,16 +153,15 @@ export class EpsAssistMeStack extends Stack {
147153
// Add S3 notification to trigger sync Lambda function
148154
new S3LambdaNotification(this, "S3LambdaNotification", {
149155
bucket: storage.kbDocsBucket.bucket,
150-
lambdaFunction: functions.functions.syncKnowledgeBase.function
156+
lambdaFunction: functions.syncKnowledgeBaseFunction.function
151157
})
152158

153159
// Create Apis and pass the Lambda function
154160
const apis = new Apis(this, "Apis", {
155161
stackName: props.stackName,
156162
logRetentionInDays,
157-
enableMutalTls: false,
158163
functions: {
159-
slackBot: functions.functions.slackBot
164+
slackBot: functions.slackBotLambda
160165
}
161166
})
162167

@@ -180,6 +185,12 @@ export class EpsAssistMeStack extends Stack {
180185
value: storage.kbDocsBucket.bucket.bucketName,
181186
exportName: `${props.stackName}:kbDocsBucket:Name`
182187
})
188+
189+
new CfnOutput(this, "SlackBotLambdaRoleArn", {
190+
value: functions.slackBotLambda.executionRole.roleArn,
191+
exportName: `${props.stackName}:lambda:SlackBot:ExecutionRole:Arn`
192+
})
193+
183194
if (isPullRequest) {
184195
new CfnOutput(this, "VERSION_NUMBER", {
185196
value: props.version,

packages/slackBotFunction/app/core/config.py

Lines changed: 59 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,24 @@
88
import os
99
import json
1010
import traceback
11+
from typing import Tuple
1112
import boto3
1213
from aws_lambda_powertools import Logger
14+
from aws_lambda_powertools.logging import utils
1315
from aws_lambda_powertools.utilities.parameters import get_parameter
1416
from mypy_boto3_dynamodb.service_resource import Table
1517

18+
# we use lru_cache for lots of configs so they are cached
19+
1620

1721
@lru_cache()
1822
def get_logger() -> Logger:
19-
return Logger(service="slackBotFunction")
23+
powertools_logger = Logger(service="slackBotFunction")
24+
utils.copy_config_to_registered_loggers(source_logger=powertools_logger, ignore_log_level=True)
25+
return powertools_logger
2026

2127

28+
# set up logger as its used in other functions
2229
logger = get_logger()
2330

2431

@@ -30,7 +37,7 @@ def get_slack_bot_state_table() -> Table:
3037

3138

3239
@lru_cache()
33-
def get_ssm_params():
40+
def get_ssm_params() -> Tuple[str, str]:
3441
bot_token_parameter = os.environ["SLACK_BOT_TOKEN_PARAMETER"]
3542
signing_secret_parameter = os.environ["SLACK_SIGNING_SECRET_PARAMETER"]
3643
try:
@@ -57,13 +64,35 @@ def get_ssm_params():
5764
return bot_token, signing_secret
5865

5966

67+
@lru_cache
68+
def get_bot_token() -> str:
69+
bot_token, _ = get_ssm_params()
70+
return bot_token
71+
72+
73+
@lru_cache
74+
def get_guardrail_config() -> Tuple[str, str, str, str, str]:
75+
# Bedrock configuration from environment
76+
KNOWLEDGEBASE_ID = os.environ["KNOWLEDGEBASE_ID"]
77+
RAG_MODEL_ID = os.environ["RAG_MODEL_ID"]
78+
AWS_REGION = os.environ["AWS_REGION"]
79+
GUARD_RAIL_ID = os.environ["GUARD_RAIL_ID"]
80+
GUARD_VERSION = os.environ["GUARD_RAIL_VERSION"]
81+
82+
logger.info(
83+
"Guardrail configuration loaded", extra={"guardrail_id": GUARD_RAIL_ID, "guardrail_version": GUARD_VERSION}
84+
)
85+
return KNOWLEDGEBASE_ID, RAG_MODEL_ID, AWS_REGION, GUARD_RAIL_ID, GUARD_VERSION
86+
87+
6088
@dataclass
6189
class Constants:
6290
FEEDBACK_PREFIX: str
6391
CONTEXT_TYPE_DM: str
6492
CONTEXT_TYPE_THREAD: str
6593
CHANNEL_TYPE_IM: str
6694
SESSION_SK: str
95+
PULL_REQUEST_SK: str
6796
DEDUP_SK: str
6897
EVENT_PREFIX: str
6998
FEEDBACK_PREFIX_KEY: str
@@ -74,6 +103,7 @@ class Constants:
74103
TTL_EVENT_DEDUP: int
75104
TTL_FEEDBACK: int
76105
TTL_SESSION: int
106+
PULL_REQUEST_PREFIX: str
77107

78108

79109
constants = Constants(
@@ -82,6 +112,7 @@ class Constants:
82112
CONTEXT_TYPE_THREAD="thread",
83113
CHANNEL_TYPE_IM="im",
84114
SESSION_SK="session",
115+
PULL_REQUEST_SK="pull_request",
85116
DEDUP_SK="dedup",
86117
EVENT_PREFIX="event#",
87118
FEEDBACK_PREFIX_KEY="feedback#",
@@ -92,45 +123,32 @@ class Constants:
92123
TTL_EVENT_DEDUP=3600, # 1 hour
93124
TTL_FEEDBACK=7776000, # 90 days
94125
TTL_SESSION=2592000, # 30 days
126+
PULL_REQUEST_PREFIX="pr:",
95127
)
96128

97129

98-
@lru_cache
99-
def get_bot_token():
100-
bot_token, _ = get_ssm_params()
101-
return bot_token
102-
103-
104-
@lru_cache()
105-
def get_bot_messages():
106-
107-
# Bot response messages
108-
BOT_MESSAGES = {
109-
"empty_query": "Hi there! Please ask me a question and I'll help you find information from our knowledge base.",
110-
"error_response": "Sorry, an error occurred while processing your request. Please try again later.",
111-
"feedback_positive_thanks": "Thank you for your feedback.",
112-
"feedback_negative_thanks": (
113-
'Please let us know how the answer could be improved. Start your message with "feedback:"'
114-
),
115-
"feedback_thanks": "Thank you for your feedback.",
116-
"feedback_prompt": "Was this helpful?",
117-
"feedback_yes": "Yes",
118-
"feedback_no": "No",
119-
}
120-
121-
return BOT_MESSAGES
122-
123-
124-
@lru_cache
125-
def get_guardrail_config():
126-
# Bedrock configuration from environment
127-
KNOWLEDGEBASE_ID = os.environ["KNOWLEDGEBASE_ID"]
128-
RAG_MODEL_ID = os.environ["RAG_MODEL_ID"]
129-
AWS_REGION = os.environ["AWS_REGION"]
130-
GUARD_RAIL_ID = os.environ["GUARD_RAIL_ID"]
131-
GUARD_VERSION = os.environ["GUARD_RAIL_VERSION"]
132-
133-
logger.info(
134-
"Guardrail configuration loaded", extra={"guardrail_id": GUARD_RAIL_ID, "guardrail_version": GUARD_VERSION}
135-
)
136-
return KNOWLEDGEBASE_ID, RAG_MODEL_ID, AWS_REGION, GUARD_RAIL_ID, GUARD_VERSION
130+
@dataclass
131+
class BotMessages:
132+
EMPTY_QUERY: str
133+
ERROR_RESPONSE: str
134+
FEEDBACK_POSITIVE_THANKS: str
135+
FEEDBACK_NEGATIVE_THANKS: str
136+
FEEDBACK_THANKS: str
137+
FEEDBACK_PROMPT: str
138+
FEEDBACK_YES: str
139+
FEEDBACK_NO: str
140+
141+
142+
# Bot response messages
143+
bot_messages = BotMessages(
144+
EMPTY_QUERY="Hi there! Please ask me a question and I'll help you find information from our knowledge base.",
145+
ERROR_RESPONSE="Sorry, an error occurred while processing your request. Please try again later.",
146+
FEEDBACK_POSITIVE_THANKS="Thank you for your feedback.",
147+
FEEDBACK_NEGATIVE_THANKS=(
148+
'Please let us know how the answer could be improved. Start your message with "feedback:"'
149+
),
150+
FEEDBACK_THANKS="Thank you for your feedback.",
151+
FEEDBACK_PROMPT="Was this helpful?",
152+
FEEDBACK_YES="Yes",
153+
FEEDBACK_NO="No",
154+
)

0 commit comments

Comments
 (0)