Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
f0f698d
add a waiter function
anthony-nhs Sep 10, 2025
8e6db46
Merge remote-tracking branch 'origin/main' into really_wait_for_index
anthony-nhs Sep 10, 2025
569946b
wildcard permissions
anthony-nhs Sep 10, 2025
ef03525
refactor
anthony-nhs Sep 10, 2025
2dae3bb
package it
anthony-nhs Sep 10, 2025
e79598a
really fix permissions
anthony-nhs Sep 10, 2025
fd66277
wildcard
anthony-nhs Sep 10, 2025
6121210
always work
anthony-nhs Sep 10, 2025
5a6f50e
really return ok
anthony-nhs Sep 10, 2025
7f3e787
correctly set log level
anthony-nhs Sep 10, 2025
fd2f661
different resource
anthony-nhs Sep 10, 2025
81df1f3
skip qc
anthony-nhs Sep 10, 2025
dd1ef5b
no region
anthony-nhs Sep 10, 2025
c4f3f4a
do it a different way
anthony-nhs Sep 10, 2025
c764324
format arn
anthony-nhs Sep 10, 2025
a05ebc3
fix the arn
anthony-nhs Sep 11, 2025
69b92fb
fix nag
anthony-nhs Sep 11, 2025
2877bd4
wildcard
anthony-nhs Sep 11, 2025
d2aeadf
better error
anthony-nhs Sep 12, 2025
8ab8401
use correct params
anthony-nhs Sep 12, 2025
2358cfe
better error logging
anthony-nhs Sep 12, 2025
192fa32
try getting the index
anthony-nhs Sep 12, 2025
2fc4e4f
correct policy
anthony-nhs Sep 12, 2025
97f0e2c
do it a different way
anthony-nhs Sep 12, 2025
3586749
fix env
anthony-nhs Sep 12, 2025
4853c67
fix it
anthony-nhs Sep 12, 2025
61a3ca9
remove requirements files
anthony-nhs Sep 13, 2025
be4323a
add plugin
anthony-nhs Sep 13, 2025
32755d1
fix it
anthony-nhs Sep 13, 2025
0d15dd0
remove indexwaiter
anthony-nhs Sep 13, 2025
bdd56fa
update supressions
anthony-nhs Sep 13, 2025
6e2ed15
Merge remote-tracking branch 'origin/main' into really_wait_for_index
anthony-nhs Sep 15, 2025
547bab4
tests in vscode
anthony-nhs Sep 15, 2025
520cc33
refactor test
anthony-nhs Sep 15, 2025
f3ceac6
sonarqube findings
anthony-nhs Sep 15, 2025
f59b747
export bucket
anthony-nhs Sep 15, 2025
984f764
update gitallowed
anthony-nhs Sep 15, 2025
a21d888
better logging
anthony-nhs Sep 15, 2025
78c6019
refactor log again
anthony-nhs Sep 15, 2025
321aaf1
pass logger to app
anthony-nhs Sep 15, 2025
f0da200
more refactor
anthony-nhs Sep 15, 2025
1f6f318
refactor
anthony-nhs Sep 16, 2025
da6f066
respond with eyes
anthony-nhs Sep 16, 2025
dad9a44
update readme
anthony-nhs Sep 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
"ms-python.flake8",
"eamodio.gitlens",
"github.vscode-pull-request-github",
"orta.vscode-jest",
"42crunch.vscode-openapi",
"mermade.openapi-lint",
"dbaeumer.vscode-eslint",
Expand All @@ -51,10 +50,7 @@
"python.analysis.autoSearchPaths": true,
"python.analysis.extraPaths": [],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"python.testing.pytestArgs": [
"packages/slackBotFunction",
],
"python.testing.pytestEnabled": false,
"python.linting.pylintEnabled": false,
"python.linting.flake8Enabled": true,
"python.linting.enabled": true, // required to format on save
Expand Down
1 change: 1 addition & 0 deletions .gitallowed
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ token = slack_event_data\["bot_token"\]
client = WebClient\(token=token\)
client = WebClient\(token=slack_event_data\["bot_token"\]\)
context accountId=123456789012
.*:sample_docs/.*
6 changes: 4 additions & 2 deletions .github/workflows/cdk_package_code.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,10 @@ jobs:

- name: Build Python Lambda Functions
run: |
pip3 install -r packages/slackBotFunction/requirements.txt -t packages/slackBotFunction
pip3 install -r packages/syncKnowledgeBaseFunction/requirements.txt -t packages/syncKnowledgeBaseFunction
poetry export --without-hashes --format=requirements.txt --with slackBotFunction > requirements_slackBotFunction
poetry export --without-hashes --format=requirements.txt --with syncKnowledgeBaseFunction > requirements_syncKnowledgeBaseFunction
pip3 install -r requirements_slackBotFunction -t packages/slackBotFunction
pip3 install -r requirements_syncKnowledgeBaseFunction -t packages/syncKnowledgeBaseFunction

- name: 'Tar files'
run: |
Expand Down
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
nodejs 22.12.0
python 3.13.3
poetry 1.8.3
poetry 2.1.4
shellcheck 0.10.0
direnv 2.32.2
actionlint 1.7.3
Expand Down
10 changes: 7 additions & 3 deletions .vscode/eps-assist-me.code-workspace
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"folders": [
{
"name": "eps-assist-me-monorepo",
"name": "eps-assist-me",
"path": ".."
},
{
Expand All @@ -19,7 +19,10 @@
],
"settings": {
"jest.disabledWorkspaceFolders": [
"eps-assist-me"
"eps-assist-me",
"packages/cdk",
"packages/slackBotFunction",
"packages/syncKnowledgeBaseFunction"
],
"files.exclude": {
"packages/": true
Expand Down Expand Up @@ -103,7 +106,8 @@
"jest.jestCommandLine": "NODE_OPTIONS=--experimental-vm-modules node_modules/.bin/jest --no-cache",
"jest.nodeEnv": {
"POWERTOOLS_DEV": true
}
},
"python.defaultInterpreterPath": "/workspaces/eps-assist-me/.venv/bin/python"
},
"extensions": {
"recommendations": [
Expand Down
12 changes: 7 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ guard-%:
install: install-python install-hooks install-node

install-python:
poetry install
cd packages/slackBotFunction && pip install -r requirements.txt && pip install -r requirements-test.txt
cd packages/syncKnowledgeBaseFunction && pip install -r requirements.txt && pip install -r requirements-test.txt
poetry sync --all-groups

install-hooks: install-python
poetry run pre-commit install --install-hooks --overwrite
Expand Down Expand Up @@ -48,8 +46,8 @@ lint-flake8:
poetry run flake8 .

test:
cd packages/slackBotFunction && PYTHONPATH=. COVERAGE_FILE=coverage/.coverage python -m pytest
cd packages/syncKnowledgeBaseFunction && PYTHONPATH=. COVERAGE_FILE=coverage/.coverage python -m pytest
cd packages/slackBotFunction && PYTHONPATH=. COVERAGE_FILE=coverage/.coverage poetry run python -m pytest
cd packages/syncKnowledgeBaseFunction && PYTHONPATH=. COVERAGE_FILE=coverage/.coverage poetry run python -m pytest

clean:
rm -rf packages/cdk/coverage
Expand All @@ -60,6 +58,7 @@ clean:
rm -rf packages/syncKnowledgeBaseFunction/.coverage
rm -rf cdk.out
rm -rf .build
find . -name '.pytest_cache' -type d -prune -exec rm -rf '{}' +

deep-clean: clean
rm -rf .venv
Expand Down Expand Up @@ -137,3 +136,6 @@ cdk-watch: guard-STACK_NAME
--context logRetentionInDays=$$LOG_RETENTION_IN_DAYS \
--context slackBotToken=$$SLACK_BOT_TOKEN \
--context slackSigningSecret=$$SLACK_SIGNING_SECRET

sync-docs:
./scripts/sync_docs.sh
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ packages/
│ │ └── RestApiGateway/ # API Gateway specific constructs
│ ├── resources/ # AWS resource definitions
│ └── stacks/ # CDK stack definitions
├── sample_docs/ # Contains sample docs for testing purposes. These should not be used for real usage
├── slackBotFunction/ # Lambda function for Slack bot integration
│ ├── app/ # Application code
│ │ ├── config/ # Configuration and environment variables
Expand Down Expand Up @@ -171,6 +172,7 @@ These are used to do common commands related to cdk
- `git-secrets-docker-setup` Sets up git-secrets Docker container.
- `pre-commit` Runs pre-commit hooks on all files.
- `test` Runs unit tests for Lambda functions.
- `sync-docs` Runs a script to sync sample docs to s3 bucket for a pull request. Useful for setting up a stack for testing

#### Compiling

Expand Down
4 changes: 2 additions & 2 deletions packages/cdk/constructs/LambdaFunction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export interface LambdaFunctionProps {
readonly functionName: string
readonly packageBasePath: string
readonly handler: string
readonly environmentVariables: {[key: string]: string}
readonly environmentVariables?: {[key: string]: string}
readonly additionalPolicies?: Array<IManagedPolicy>
readonly logRetentionInDays: number
readonly logLevel: string
Expand Down Expand Up @@ -126,7 +126,7 @@ export class LambdaFunction extends Construct {
role,
environment: {
...props.environmentVariables,
LOG_LEVEL: props.logLevel
POWERTOOLS_LOG_LEVEL: props.logLevel
},
logGroup,
layers: [insightsLambdaLayer]
Expand Down
1 change: 1 addition & 0 deletions packages/cdk/nagSuppressions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ export const nagSuppressions = (stack: Stack) => {
]
)
})

}

const safeAddNagSuppression = (stack: Stack, path: string, suppressions: Array<NagPackSuppression>) => {
Expand Down
2 changes: 0 additions & 2 deletions packages/cdk/resources/VectorIndex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@ export class VectorIndex extends Construct {
}
})

// Ensure collection exists before creating index
cfnIndex.node.addDependency(props.collection)
this.cfnIndex = cfnIndex
}
}
9 changes: 9 additions & 0 deletions packages/cdk/stacks/EpsAssistMeStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,15 @@ export class EpsAssistMeStack extends Stack {
description: "ARN of the query reformulation prompt in Bedrock"
})

new CfnOutput(this, "kbDocsBucketArn", {
value: storage.kbDocsBucket.bucket.bucketArn,
exportName: `${props.stackName}:kbDocsBucket:Arn`
})
new CfnOutput(this, "kbDocsBucketName", {
value: storage.kbDocsBucket.bucket.bucketName,
exportName: `${props.stackName}:kbDocsBucket:Name`
})

// Final CDK Nag Suppressions
nagSuppressions(this)
}
Expand Down
3 changes: 3 additions & 0 deletions packages/slackBotFunction/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"python.testing.pytestEnabled": true
}
107 changes: 64 additions & 43 deletions packages/slackBotFunction/app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,65 +3,86 @@
Sets up all the AWS and Slack connections we need.
"""

from functools import lru_cache
import os
import json
import traceback
import boto3
from slack_bolt import App
from aws_lambda_powertools import Logger
from aws_lambda_powertools.utilities.parameters import get_parameter


# set up logging
logger = Logger(service="slackBotFunction")
@lru_cache()
def get_logger():
return Logger(service="slackBotFunction")

# DynamoDB table for deduplication and session storage
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(os.environ["SLACK_BOT_STATE_TABLE"])

# get Slack credentials from Parameter Store
bot_token_parameter = os.environ["SLACK_BOT_TOKEN_PARAMETER"]
signing_secret_parameter = os.environ["SLACK_SIGNING_SECRET_PARAMETER"]
logger = get_logger()

try:
bot_token_raw = get_parameter(bot_token_parameter, decrypt=True)
signing_secret_raw = get_parameter(signing_secret_parameter, decrypt=True)

if not bot_token_raw or not signing_secret_raw:
raise ValueError("Missing required parameters from Parameter Store")
@lru_cache()
def get_slack_bot_state_table():
# DynamoDB table for deduplication and session storage
dynamodb = boto3.resource("dynamodb")
return dynamodb.Table(os.environ["SLACK_BOT_STATE_TABLE"])

bot_token_data = json.loads(bot_token_raw)
signing_secret_data = json.loads(signing_secret_raw)

bot_token = bot_token_data.get("token")
signing_secret = signing_secret_data.get("secret")
@lru_cache()
def get_ssm_params():
bot_token_parameter = os.environ["SLACK_BOT_TOKEN_PARAMETER"]
signing_secret_parameter = os.environ["SLACK_SIGNING_SECRET_PARAMETER"]
try:
bot_token_raw = get_parameter(bot_token_parameter, decrypt=True)
signing_secret_raw = get_parameter(signing_secret_parameter, decrypt=True)

if not bot_token or not signing_secret:
raise ValueError("Missing required parameters: token or secret in Parameter Store values")
if not bot_token_raw or not signing_secret_raw:
raise ValueError("Missing required parameters from Parameter Store")

except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in Parameter Store: {e}")
except Exception as e:
logger.error("Configuration error", extra={"error": str(e)})
raise
bot_token_data = json.loads(bot_token_raw)
signing_secret_data = json.loads(signing_secret_raw)

# initialise the Slack app
app = App(
process_before_response=True,
token=bot_token,
signing_secret=signing_secret,
)
bot_token = bot_token_data.get("token")
signing_secret = signing_secret_data.get("secret")

# Bedrock configuration from environment
KNOWLEDGEBASE_ID = os.environ["KNOWLEDGEBASE_ID"]
RAG_MODEL_ID = os.environ["RAG_MODEL_ID"]
AWS_REGION = os.environ["AWS_REGION"]
GUARD_RAIL_ID = os.environ["GUARD_RAIL_ID"]
GUARD_VERSION = os.environ["GUARD_RAIL_VERSION"]
if not bot_token or not signing_secret:
raise ValueError("Missing required parameters: token or secret in Parameter Store values")

logger.info("Guardrail configuration loaded", extra={"guardrail_id": GUARD_RAIL_ID, "guardrail_version": GUARD_VERSION})
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in Parameter Store: {e}")
except Exception:
logger.error("Configuration error", extra={"error": traceback.format_exc()})
raise
return bot_token, signing_secret

# Bot response messages
BOT_MESSAGES = {
"empty_query": "Hi there! Please ask me a question and I'll help you find information from our knowledge base.",
"error_response": "Sorry, an error occurred while processing your request. Please try again later.",
}

@lru_cache
def get_bot_token():
bot_token, _ = get_ssm_params()
return bot_token


@lru_cache()
def get_bot_messages():

# Bot response messages
BOT_MESSAGES = {
"empty_query": "Hi there! Please ask me a question and I'll help you find information from our knowledge base.",
"error_response": "Sorry, an error occurred while processing your request. Please try again later.",
}

return BOT_MESSAGES


@lru_cache
def get_guardrail_config():
# Bedrock configuration from environment
KNOWLEDGEBASE_ID = os.environ["KNOWLEDGEBASE_ID"]
RAG_MODEL_ID = os.environ["RAG_MODEL_ID"]
AWS_REGION = os.environ["AWS_REGION"]
GUARD_RAIL_ID = os.environ["GUARD_RAIL_ID"]
GUARD_VERSION = os.environ["GUARD_RAIL_VERSION"]

logger.info(
"Guardrail configuration loaded", extra={"guardrail_id": GUARD_RAIL_ID, "guardrail_version": GUARD_VERSION}
)
return KNOWLEDGEBASE_ID, RAG_MODEL_ID, AWS_REGION, GUARD_RAIL_ID, GUARD_VERSION
11 changes: 7 additions & 4 deletions packages/slackBotFunction/app/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
from slack_bolt.adapter.aws_lambda import SlackRequestHandler
from aws_lambda_powertools.utilities.typing import LambdaContext

from app.core.config import app, logger
from app.core.config import get_logger
from app.services.app import get_app
from app.slack.slack_events import process_async_slack_event
from app.slack.slack_handlers import setup_handlers

# register event handlers with the app
setup_handlers(app)
logger = get_logger()


@logger.inject_lambda_context(log_event=True, clear_state=True)
def handler(event: dict, context: LambdaContext) -> dict:
"""
Main Lambda entry point - routes between Slack webhook and async processing
Expand All @@ -26,6 +26,9 @@ def handler(event: dict, context: LambdaContext) -> dict:
2. Lambda acknowledges immediately and triggers async self-invocation
3. Async invocation processes Bedrock query and responds to Slack
"""
# register event handlers with the app
app = get_app()

logger.info("Lambda invoked", extra={"is_async": event.get("async_processing", False)})

# handle async processing requests
Expand Down
23 changes: 23 additions & 0 deletions packages/slackBotFunction/app/services/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from functools import lru_cache

from slack_bolt import App
from app.core.config import get_ssm_params
from app.core.config import get_logger
from app.slack.slack_handlers import setup_handlers

logger = get_logger()


@lru_cache()
def get_app():
bot_token, signing_secret = get_ssm_params()

# initialise the Slack app
app = App(
process_before_response=True,
token=bot_token,
signing_secret=signing_secret,
logger=logger,
)
setup_handlers(app)
return app
Loading