Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 7 additions & 7 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@
"AmazonWebServices.aws-toolkit-vscode",
"redhat.vscode-yaml",
"ms-python.python",
"ms-python.black-formatter",
"ms-python.flake8",
"charliermarsh.ruff",
"eamodio.gitlens",
"github.vscode-pull-request-github",
"42crunch.vscode-openapi",
Expand All @@ -52,12 +51,13 @@
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": false,
"python.linting.pylintEnabled": false,
"python.linting.flake8Enabled": true,
"python.linting.enabled": true, // required to format on save
"python.formatting.provider": "black",
"black-formatter.args": ["--line-length=120"],
"python.linting.enabled": false, // Disable old linting since we're using ruff
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.codeActionsOnSave": {
"source.organizeImports.ruff": "explicit",
"source.fixAll.ruff": "explicit"
}
},
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
"editor.formatOnPaste": false, // required
Expand Down
5 changes: 0 additions & 5 deletions .flake8

This file was deleted.

17 changes: 7 additions & 10 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,14 @@ repos:
name: Check pipelines configuration
files: ^(.github)

- repo: https://github.com/pycqa/flake8
rev: "7ef0350a439c93166bc8ba89fcc3de6a9a664e6c" # release 6.1.0
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.8
hooks:
- id: flake8

- repo: https://github.com/psf/black
rev: "24.10.0"
hooks:
- id: black
language_version: python3
args: [--line-length=120]
# Run the linter
- id: ruff-check
args: [--fix]
# Run the formatter
- id: ruff-format

- repo: local
hooks:
Expand Down
2 changes: 1 addition & 1 deletion .vscode/eps-assist-me.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@
"AmazonWebServices.aws-toolkit-vscode",
"redhat.vscode-yaml",
"ms-python.python",
"ms-python.flake8",
"charliermarsh.ruff",
"eamodio.gitlens",
"github.vscode-pull-request-github",
"orta.vscode-jest",
Expand Down
12 changes: 5 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ git-secrets-docker-setup:
export LOCAL_WORKSPACE_FOLDER=$(pwd)
docker build -f https://raw.githubusercontent.com/NHSDigital/eps-workflow-quality-checks/refs/tags/v4.0.4/dockerfiles/nhsd-git-secrets.dockerfile -t git-secrets .

lint: lint-githubactions lint-githubaction-scripts lint-black lint-flake8 lint-node
lint: lint-githubactions lint-githubaction-scripts lint-python lint-node

lint-node:
npm run lint --workspace packages/cdk
Expand All @@ -39,11 +39,9 @@ lint-githubaction-scripts:
shellcheck ./scripts/*.sh
shellcheck .github/scripts/*.sh

lint-black:
poetry run black .

lint-flake8:
poetry run flake8 .
lint-python:
poetry run ruff check .
poetry run ruff format --check .

test:
cd packages/slackBotFunction && PYTHONPATH=. COVERAGE_FILE=coverage/.coverage poetry run python -m pytest
Expand Down Expand Up @@ -128,5 +126,5 @@ cdk-diff:
cdk-watch:
./scripts/run_sync.sh

sync-docs:
sync-docs:
./scripts/sync_docs.sh
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,7 @@ These are used to do common commands related to cdk
#### Linting and testing

- `lint` Runs all linting checks
- `lint-black` Runs black formatter on Python code.
- `lint-flake8` Runs flake8 linter on Python code.
- `lint-python` Runs ruff linting and formatting checks on Python code.
- `lint-githubactions` Lints the repository's GitHub Actions workflows.
- `lint-githubaction-scripts` Lints all shell scripts in `.github/scripts` using ShellCheck.
- `cfn-guard` Runs cfn-guard against CDK resources.
Expand Down
20 changes: 10 additions & 10 deletions packages/slackBotFunction/app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
Sets up all the AWS and Slack connections we need.
"""

from dataclasses import dataclass
from functools import lru_cache
import os
import json
import os
import traceback
from typing import Tuple
from dataclasses import dataclass
from functools import lru_cache

import boto3
from aws_lambda_powertools import Logger
from aws_lambda_powertools.logging import utils
Expand All @@ -18,7 +18,7 @@
# we use lru_cache for lots of configs so they are cached


@lru_cache()
@lru_cache
def get_logger() -> Logger:
powertools_logger = Logger(service="slackBotFunction")
utils.copy_config_to_registered_loggers(source_logger=powertools_logger, ignore_log_level=True)
Expand All @@ -29,15 +29,15 @@ def get_logger() -> Logger:
logger = get_logger()


@lru_cache()
@lru_cache
def get_slack_bot_state_table() -> Table:
# DynamoDB table for deduplication and session storage
dynamodb = boto3.resource("dynamodb")
return dynamodb.Table(os.environ["SLACK_BOT_STATE_TABLE"])


@lru_cache()
def get_ssm_params() -> Tuple[str, str]:
@lru_cache
def get_ssm_params() -> tuple[str, str]:
bot_token_parameter = os.environ["SLACK_BOT_TOKEN_PARAMETER"]
signing_secret_parameter = os.environ["SLACK_SIGNING_SECRET_PARAMETER"]
try:
Expand All @@ -57,7 +57,7 @@ def get_ssm_params() -> Tuple[str, str]:
raise ValueError("Missing required parameters: token or secret in Parameter Store values")

except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in Parameter Store: {e}")
raise ValueError(f"Invalid JSON in Parameter Store: {e}") from e
except Exception:
logger.error("Configuration error", extra={"error": traceback.format_exc()})
raise
Expand All @@ -71,7 +71,7 @@ def get_bot_token() -> str:


@lru_cache
def get_guardrail_config() -> Tuple[str, str, str, str, str]:
def get_guardrail_config() -> tuple[str, str, str, str, str]:
# Bedrock configuration from environment
KNOWLEDGEBASE_ID = os.environ["KNOWLEDGEBASE_ID"]
RAG_MODEL_ID = os.environ["RAG_MODEL_ID"]
Expand Down
2 changes: 1 addition & 1 deletion packages/slackBotFunction/app/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
2. Processes async operations when invoked by itself to avoid timeouts
"""

from slack_bolt.adapter.aws_lambda import SlackRequestHandler
from aws_lambda_powertools.utilities.typing import LambdaContext
from slack_bolt.adapter.aws_lambda import SlackRequestHandler

from app.core.config import get_logger
from app.services.app import get_app
Expand Down
5 changes: 3 additions & 2 deletions packages/slackBotFunction/app/services/app.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from functools import lru_cache
import logging
from functools import lru_cache

from aws_lambda_powertools import Logger
from slack_bolt import App

from app.core.config import get_ssm_params
from app.slack.slack_handlers import setup_handlers
from aws_lambda_powertools import Logger


@lru_cache
Expand Down
4 changes: 2 additions & 2 deletions packages/slackBotFunction/app/services/bedrock.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import json
from typing import Any

import boto3
from mypy_boto3_bedrock_agent_runtime import AgentsforBedrockRuntimeClient
from mypy_boto3_bedrock_runtime.client import BedrockRuntimeClient
from mypy_boto3_bedrock_agent_runtime.type_defs import RetrieveAndGenerateResponseTypeDef
from mypy_boto3_bedrock_runtime.client import BedrockRuntimeClient

from app.core.config import get_guardrail_config, get_logger


logger = get_logger()


Expand Down
6 changes: 4 additions & 2 deletions packages/slackBotFunction/app/services/dynamo.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from typing import Any
from app.core.config import get_logger, get_slack_bot_state_table
from time import time
from typing import Any

from mypy_boto3_dynamodb.type_defs import GetItemOutputTableTypeDef

from app.core.config import get_logger, get_slack_bot_state_table

logger = get_logger()


Expand Down
10 changes: 6 additions & 4 deletions packages/slackBotFunction/app/services/prompt_loader.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import os
import traceback

import boto3
from botocore.exceptions import ClientError
from app.core.config import get_logger
from app.services.exceptions import PromptNotFoundError, PromptLoadError
from mypy_boto3_bedrock_agent import AgentsforBedrockClient

from app.core.config import get_logger
from app.services.exceptions import PromptLoadError, PromptNotFoundError

logger = get_logger()


Expand Down Expand Up @@ -66,14 +68,14 @@ def load_prompt(prompt_name: str, prompt_version: str = None) -> str:
)
raise PromptLoadError(
f"Failed to load prompt '{prompt_name}' version '{prompt_version}': {error_code} - {error_message}"
)
) from e

except Exception as e:
logger.error(
"Unexpected error loading prompt",
extra={"prompt_name": prompt_name, "error_type": type(e).__name__, "error": traceback.format_exc()},
)
raise PromptLoadError(f"Unexpected error loading prompt '{prompt_name}': {e}")
raise PromptLoadError(f"Unexpected error loading prompt '{prompt_name}': {e}") from e


def get_prompt_id_from_name(client: AgentsforBedrockClient, prompt_name: str) -> str | None:
Expand Down
6 changes: 4 additions & 2 deletions packages/slackBotFunction/app/services/query_reformulator.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import os
import traceback

import boto3
from mypy_boto3_bedrock_runtime.client import BedrockRuntimeClient

from app.core.config import get_logger
from app.services.bedrock import invoke_model
from .prompt_loader import load_prompt

from .exceptions import ConfigurationError
from mypy_boto3_bedrock_runtime.client import BedrockRuntimeClient
from .prompt_loader import load_prompt

logger = get_logger()

Expand Down
2 changes: 1 addition & 1 deletion packages/slackBotFunction/app/services/slack.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import traceback

from slack_sdk import WebClient
from app.core.config import bot_messages, get_logger

from app.core.config import bot_messages, get_logger

logger = get_logger()

Expand Down
28 changes: 15 additions & 13 deletions packages/slackBotFunction/app/slack/slack_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
Handles conversation memory, Bedrock queries, and responding back to Slack
"""

import json
import re
import time
import traceback
import json
from typing import Any, Dict
from typing import Any

from botocore.exceptions import ClientError
from slack_sdk import WebClient

from app.core.config import (
bot_messages,
constants,
Expand Down Expand Up @@ -43,7 +45,7 @@


def cleanup_previous_unfeedback_qa(
conversation_key: str, current_message_ts: str, session_data: Dict[str, Any]
conversation_key: str, current_message_ts: str, session_data: dict[str, Any]
) -> None:
"""Delete previous Q&A pair if no feedback received using atomic operation"""
try:
Expand Down Expand Up @@ -112,9 +114,9 @@ def _mark_qa_feedback_received(conversation_key: str, message_ts: str) -> None:

def _handle_session_management(
conversation_key: str,
session_data: Dict[str, Any],
session_data: dict[str, Any],
session_id: str,
kb_response: Dict[str, Any],
kb_response: dict[str, Any],
user_id: str,
channel: str,
thread_ts: str,
Expand Down Expand Up @@ -184,7 +186,7 @@ def process_feedback_event(
channel_id: str,
thread_root: str,
client: WebClient,
event: Dict[str, Any],
event: dict[str, Any],
) -> None:
feedback_text = message_text.split(":", 1)[1].strip() if ":" in message_text else ""
try:
Expand All @@ -208,7 +210,7 @@ def process_feedback_event(
post_error_message(channel=channel_id, thread_ts=thread_ts, client=client)


def process_async_slack_action(body: Dict[str, Any], client: WebClient) -> None:
def process_async_slack_action(body: dict[str, Any], client: WebClient) -> None:
try:
channel_id = body["channel"]["id"]
action_id = body["actions"][0]["action_id"]
Expand Down Expand Up @@ -261,7 +263,7 @@ def process_async_slack_action(body: Dict[str, Any], client: WebClient) -> None:
logger.error(f"Error handling feedback: {e}", extra={"error": traceback.format_exc()})


def process_async_slack_event(event: Dict[str, Any], event_id: str, client: WebClient) -> None:
def process_async_slack_event(event: dict[str, Any], event_id: str, client: WebClient) -> None:
logger.debug("Processing async Slack event", extra={"event_id": event_id, "event": event})
original_message_text = (event.get("text") or "").strip()
message_text = strip_mentions(message_text=original_message_text)
Expand Down Expand Up @@ -293,7 +295,7 @@ def process_async_slack_event(event: Dict[str, Any], event_id: str, client: WebC
process_slack_message(event=event, event_id=event_id, client=client)


def process_slack_message(event: Dict[str, Any], event_id: str, client: WebClient) -> None:
def process_slack_message(event: dict[str, Any], event_id: str, client: WebClient) -> None:
"""
Process Slack events asynchronously after initial acknowledgment

Expand Down Expand Up @@ -369,7 +371,7 @@ def process_slack_message(event: Dict[str, Any], event_id: str, client: WebClien
post_error_message(channel=channel, thread_ts=thread_ts, client=client)


def process_pull_request_slack_event(slack_event_data: Dict[str, Any]) -> None:
def process_pull_request_slack_event(slack_event_data: dict[str, Any]) -> None:
# separate function to process pull requests so that we can ensure we store session information
try:
event_id = slack_event_data["event_id"]
Expand All @@ -384,7 +386,7 @@ def process_pull_request_slack_event(slack_event_data: Dict[str, Any]) -> None:
logger.error("Error processing message", extra={"event_id": event_id, "error": traceback.format_exc()})


def process_pull_request_slack_action(slack_body_data: Dict[str, Any]) -> None:
def process_pull_request_slack_action(slack_body_data: dict[str, Any]) -> None:
# separate function to process pull requests so that we can ensure we store session information
try:
token = get_bot_token()
Expand All @@ -395,7 +397,7 @@ def process_pull_request_slack_action(slack_body_data: Dict[str, Any]) -> None:
logger.error("Error processing message", extra={"error": traceback.format_exc()})


def log_query_stats(user_query: str, event: Dict[str, Any], channel: str, client: WebClient, thread_ts: str) -> None:
def log_query_stats(user_query: str, event: dict[str, Any], channel: str, client: WebClient, thread_ts: str) -> None:
query_length = len(user_query)
start_time = float(event["event_ts"])
end_time = time.time()
Expand Down Expand Up @@ -520,7 +522,7 @@ def get_conversation_session(conversation_key: str) -> str | None:
return session_data.get("session_id") if session_data else None


def get_conversation_session_data(conversation_key: str) -> Dict[str, Any]:
def get_conversation_session_data(conversation_key: str) -> dict[str, Any]:
"""
Get full session data for this conversation
"""
Expand Down
Loading
Loading