diff --git a/packages/cdk/nagSuppressions.ts b/packages/cdk/nagSuppressions.ts index 3de6806a5..5482bab4a 100644 --- a/packages/cdk/nagSuppressions.ts +++ b/packages/cdk/nagSuppressions.ts @@ -80,6 +80,22 @@ export const nagSuppressions = (stack: Stack, account: string) => { ] ) + // Suppress unauthenticated API route warnings + safeAddNagSuppression( + stack, + "/EpsAssistMeStack/Apis/EpsAssistApiGateway/ApiGateway/Default/slack/commands/POST/Resource", + [ + { + id: "AwsSolutions-APIG4", + reason: "Slack command endpoint is intentionally unauthenticated." + }, + { + id: "AwsSolutions-COG4", + reason: "Cognito not required for this public endpoint." + } + ] + ) + // Suppress missing WAF on API stage for Apis construct safeAddNagSuppression( stack, diff --git a/packages/cdk/prompts/reformulationPrompt.txt b/packages/cdk/prompts/reformulationPrompt.txt index 60b255819..eb6e5039a 100644 --- a/packages/cdk/prompts/reformulationPrompt.txt +++ b/packages/cdk/prompts/reformulationPrompt.txt @@ -1,5 +1,40 @@ -Return the user query exactly as provided without any modifications, changes, or reformulations. -Do not alter, rephrase, or modify the input in any way. -Simply return: {{user_query}} +<|begin_of_text|><|start_header_id|>system<|end_header_id|> +You are an expert RAG query and context optimizer. Your task is to analyze verbose user queries and raw search context, stripping away all conversational filler to output a concise, impactful summary. -User Query: {{user_query}} +You must: +1. Extract the core objective into a single, direct question. +2. Capture individual questions and their specific needs. +3. Isolate critical variables, specific states, and constraints required to solve the problem. +4. Enhance the question(s) with relevant terminology from the search results + +Output your response strictly using the following XML structure: + (The short, direct question) + (Bullet points of critical states, statuses, or constraints) +<|eot_id|> + +<|start_header_id|>user<|end_header_id|> +### User Query +Hi, I need some help figuring out the PTO rules for one of my team members. They started as part-time 6 months ago, but they just transitioned to full-time last week (let's say exactly 7 days ago). They currently have 2 days of PTO saved up from their part-time stint. They want to take next week off entirely, which would require 5 days of PTO. Can they do this, effectively going to a -3 balance, since they are full-time now? +<|eot_id|> + +<|start_header_id|>assistant<|end_header_id|> + +Can a recently transitioned full-time employee with 6 months total tenure and 2 accrued PTO days take 5 days off, resulting in a -3 PTO balance? + + +- Current Status: Full-time (transitioned 7 days ago) +- Total Tenure: 6 months +- Current PTO Balance: 2 days +- Requested PTO: 5 days (resulting in -3 balance) + +<|eot_id|> + +<|start_header_id|>user<|end_header_id|> +### Search Context +$search_results$ + +### User Query +{{user_query}} +<|eot_id|> + +<|start_header_id|>assistant<|end_header_id|> diff --git a/packages/cdk/prompts/systemPrompt.txt b/packages/cdk/prompts/systemPrompt.txt index b058420cb..2b0189b4b 100644 --- a/packages/cdk/prompts/systemPrompt.txt +++ b/packages/cdk/prompts/systemPrompt.txt @@ -1,24 +1,32 @@ -# 1. Persona & Logic -You are an AI assistant for onboarding guidance. Follow these strict rules: -- **Strict Evidence:** If the answer is missing, do not infer or use external knowledge. -- **Grounding:** NEVER use your own internal training data, online resources, or prior knowledge. -- **Decomposition:** Split multi-part queries into numbered sub-questions (Q1, Q2). +You are a technical assistant specialized in onboarding guidance. +Your primary goal is to -# 2. Output Structure -**Summary** -2-3 sentences maximum. +STYLE & FORMATTING RULES: +- Do NOT refer to the search results by number or name in the body of the text. +- Do NOT add a "Citations" section at the end of the response. +- Do NOT reference how the information was found (e.g., "...the provided search results") +- Do NOT state what the data is related to (e.g., "The search results are related to NHS API and FHIR...") +- Text should prioritise readability. +- Links should use Markdown text, e.g., . +- Use `Inline Code` for system names, field names, or technical terms (e.g., `HL7 FHIR`). - **Answer** - Prioritize detail and specification, focus on the information direct at the question. +RULES: +- Answer questions using ONLY the provided search results. +- Do not assume any information, all information must be grounded in data. -# 3. Styling Rules (`mrkdwn`) -Use British English. -- **Bold (`*`):** Headings, Subheadings, Source Names, and important information/ exceptions (e.g. `*NHS England*`). -- **Italic (`_`):** Citations and Titles (e.g. `_Guidance v1_`). -- **Blockquote (`>`):** Quotes (>1 sentence) and Tech Specs/Examples (e.g. `HL7 FHIR`). -- **Links:** `[text](link)`. +STEPS: +1. Extract key information from the knowledge base +2. Generate an answer, capturing the core question the user is asking. +3. Answer, directly, any individual or sub-questions the user has provided. +4. You must create a very short summary encapsulating the response and have it precede all other answers. -# 4. Format Rules -- NEVER use in-line references or citations (e.g., do not write "(search result 1)" or "[1]"). -- Do NOT refer to the search results by number or name in the body of the text. -- Do NOT add a "Citations" section at the end of the response.wer, details from the knowledge base. +EXAMPLE: + +*Summary* +This is a short, fast answer so the user doesn't _have_ to read the long answer. + +*Answer* +This is a direct answer to the question, or questions, provided. It breaks down individual questions. There is no reference to the text here (for example, you don't see "from source 1") but instead treats this information as if it was public knowledge. However, if there is a source, it does provide that source [as a hyperlink](hyperlink) to the website it can be found. + +There is multiple paragraphs, with blank lines between, to make it easier to read, as readability is a requirement. + diff --git a/packages/cdk/prompts/userPrompt.txt b/packages/cdk/prompts/userPrompt.txt index e54881843..76c6d6140 100644 --- a/packages/cdk/prompts/userPrompt.txt +++ b/packages/cdk/prompts/userPrompt.txt @@ -1,4 +1,3 @@ - $search_results$ {{user_query}} diff --git a/packages/cdk/resources/Apis.ts b/packages/cdk/resources/Apis.ts index c67c9fba3..fd8d637db 100644 --- a/packages/cdk/resources/Apis.ts +++ b/packages/cdk/resources/Apis.ts @@ -27,6 +27,7 @@ export class Apis extends Construct { forwardCsocLogs: props.forwardCsocLogs, csocApiGatewayDestination: props.csocApiGatewayDestination }) + // Create /slack resource path const slackResource = apiGateway.api.root.addResource("slack") @@ -41,6 +42,17 @@ export class Apis extends Construct { lambdaFunction: props.functions.slackBot }) + // Create the '/slack/commands' POST endpoint for Slack Events API + // This endpoint will handle slash commands, such as /test + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const slackCommandsEndpoint = new LambdaEndpoint(this, "SlackCommandsEndpoint", { + parentResource: slackResource, + resourceName: "commands", + method: HttpMethod.POST, + restApiGatewayRole: apiGateway.role, + lambdaFunction: props.functions.slackBot + }) + this.apis = { api: apiGateway } diff --git a/packages/cdk/resources/BedrockPromptResources.ts b/packages/cdk/resources/BedrockPromptResources.ts index 73a8fa2ce..9274ebf39 100644 --- a/packages/cdk/resources/BedrockPromptResources.ts +++ b/packages/cdk/resources/BedrockPromptResources.ts @@ -1,10 +1,13 @@ import {Construct} from "constructs" +import * as crypto from "crypto" import { BedrockFoundationModel, + ChatMessage, Prompt, PromptVariant } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock" import {BedrockPromptSettings} from "./BedrockPromptSettings" +import {CfnPrompt} from "aws-cdk-lib/aws-bedrock" export interface BedrockPromptResourcesProps { readonly stackName: string @@ -12,55 +15,71 @@ export interface BedrockPromptResourcesProps { } export class BedrockPromptResources extends Construct { - public readonly queryReformulationPrompt: Prompt + public readonly reformulationPrompt: Prompt public readonly ragResponsePrompt: Prompt - public readonly ragModelId: string - public readonly queryReformulationModelId: string + public readonly modelId: string constructor(scope: Construct, id: string, props: BedrockPromptResourcesProps) { super(scope, id) - const ragModel = new BedrockFoundationModel("meta.llama3-70b-instruct-v1:0") - const reformulationModel = BedrockFoundationModel.AMAZON_NOVA_LITE_V1 + const aiModel = new BedrockFoundationModel("meta.llama3-70b-instruct-v1:0") - const queryReformulationPromptVariant = PromptVariant.text({ - variantName: "default", - model: reformulationModel, - promptVariables: ["topic"], - promptText: props.settings.reformulationPrompt.text - }) + // Create Prompts + this.reformulationPrompt = this.createPrompt( + "ReformulationPrompt", + `${props.stackName}-reformulation`, + "Prompt for reformulation queries to improve RAG inference", + aiModel, + "", + [props.settings.reformulationPrompt], + props.settings.reformulationInferenceConfig + ) - const queryReformulationPrompt = new Prompt(this, "QueryReformulationPrompt", { - promptName: `${props.stackName}-queryReformulation`, - description: "Prompt for reformulating user queries to improve RAG retrieval", - defaultVariant: queryReformulationPromptVariant, - variants: [queryReformulationPromptVariant] - }) + this.ragResponsePrompt = this.createPrompt( + "RagResponsePrompt", + `${props.stackName}-ragResponse`, + "Prompt for generating RAG responses with knowledge base context and system instructions", + aiModel, + props.settings.systemPrompt.text, + [props.settings.userPrompt], + props.settings.ragInferenceConfig + ) + + this.modelId = aiModel.modelId + } - const ragResponsePromptVariant = PromptVariant.chat({ + private createPrompt( + id: string, + promptName: string, + description: string, + model: BedrockFoundationModel, + systemPromptText: string, + messages: [ChatMessage], + inferenceConfig: CfnPrompt.PromptModelInferenceConfigurationProperty + ): Prompt { + + const variant = PromptVariant.chat({ variantName: "default", - model: ragModel, - promptVariables: ["query", "search_results"], - system: props.settings.systemPrompt.text, - messages: [props.settings.userPrompt] + model: model, + promptVariables: ["prompt", "search_results"], + system: systemPromptText, + messages: messages }) - ragResponsePromptVariant.inferenceConfiguration = { - text: props.settings.inferenceConfig + variant.inferenceConfiguration = { + text: inferenceConfig } - const ragPrompt = new Prompt(this, "ragResponsePrompt", { - promptName: `${props.stackName}-ragResponse`, - description: "Prompt for generating RAG responses with knowledge base context and system instructions", - defaultVariant: ragResponsePromptVariant, - variants: [ragResponsePromptVariant] - }) - - // expose model IDs for use in Lambda environment variables - this.ragModelId = ragModel.modelId - this.queryReformulationModelId = reformulationModel.modelId + const hash = crypto.createHash("md5") + .update(JSON.stringify(variant)) + .digest("hex") + .substring(0, 6) - this.queryReformulationPrompt = queryReformulationPrompt - this.ragResponsePrompt = ragPrompt + return new Prompt(this, id, { + promptName: `${promptName}-${hash}`, + description, + defaultVariant: variant, + variants: [variant] + }) } } diff --git a/packages/cdk/resources/BedrockPromptSettings.ts b/packages/cdk/resources/BedrockPromptSettings.ts index 9526e6e09..096650b68 100644 --- a/packages/cdk/resources/BedrockPromptSettings.ts +++ b/packages/cdk/resources/BedrockPromptSettings.ts @@ -3,7 +3,7 @@ import {ChatMessage} from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bed import {Construct} from "constructs" import {CfnPrompt} from "aws-cdk-lib/aws-bedrock" -export type BedrockPromptSettingsType = "system" | "user" | "reformulation" +export type BedrockPromptSettingsType = "system" | "reformulation" | "user" /** BedrockPromptSettings is responsible for loading and providing * the system, user, and reformulation prompts along with their @@ -13,7 +13,8 @@ export class BedrockPromptSettings extends Construct { public readonly systemPrompt: ChatMessage public readonly userPrompt: ChatMessage public readonly reformulationPrompt: ChatMessage - public readonly inferenceConfig: CfnPrompt.PromptModelInferenceConfigurationProperty + public readonly ragInferenceConfig: CfnPrompt.PromptModelInferenceConfigurationProperty + public readonly reformulationInferenceConfig: CfnPrompt.PromptModelInferenceConfigurationProperty /** * @param scope The Construct scope @@ -30,16 +31,19 @@ export class BedrockPromptSettings extends Construct { this.userPrompt = ChatMessage.user(userPromptData.text) const reformulationPrompt = this.getTypedPrompt("reformulation") - this.reformulationPrompt = ChatMessage.user(reformulationPrompt.text) + this.reformulationPrompt = ChatMessage.assistant(reformulationPrompt.text) - this.inferenceConfig = { + const defaultInferenceConfig = { temperature: 0, topP: 0.3, - maxTokens: 1024, + maxTokens: 512, stopSequences: [ "Human:" ] } + + this.ragInferenceConfig = defaultInferenceConfig + this.reformulationInferenceConfig = defaultInferenceConfig } /** Get the latest prompt text from files in the specified directory. diff --git a/packages/cdk/resources/Functions.ts b/packages/cdk/resources/Functions.ts index 322447981..d2463920f 100644 --- a/packages/cdk/resources/Functions.ts +++ b/packages/cdk/resources/Functions.ts @@ -35,7 +35,7 @@ export interface FunctionsProps { readonly isPullRequest: boolean readonly mainSlackBotLambdaExecutionRoleArn : string readonly ragModelId: string - readonly queryReformulationModelId: string + readonly reformulationModelId: string readonly notifyS3UploadFunctionPolicy: ManagedPolicy readonly docsBucketName: string } @@ -61,7 +61,7 @@ export class Functions extends Construct { dependencyLocation: ".dependencies/slackBotFunction", environmentVariables: { "RAG_MODEL_ID": props.ragModelId, - "QUERY_REFORMULATION_MODEL_ID": props.queryReformulationModelId, + "REFORMULATION_MODEL_ID": props.reformulationModelId, "KNOWLEDGEBASE_ID": props.knowledgeBaseId, "LAMBDA_MEMORY_SIZE": LAMBDA_MEMORY_SIZE, "SLACK_BOT_TOKEN_PARAMETER": props.slackBotTokenParameter.parameterName, @@ -69,9 +69,9 @@ export class Functions extends Construct { "GUARD_RAIL_ID": props.guardrailId, "GUARD_RAIL_VERSION": props.guardrailVersion, "SLACK_BOT_STATE_TABLE": props.slackBotStateTable.tableName, - "QUERY_REFORMULATION_PROMPT_NAME": props.reformulationPromptName, + "REFORMULATION_RESPONSE_PROMPT_NAME": props.reformulationPromptName, "RAG_RESPONSE_PROMPT_NAME": props.ragResponsePromptName, - "QUERY_REFORMULATION_PROMPT_VERSION": props.reformulationPromptVersion, + "REFORMULATION_RESPONSE_PROMPT_VERSION": props.reformulationPromptVersion, "RAG_RESPONSE_PROMPT_VERSION": props.ragResponsePromptVersion } }) diff --git a/packages/cdk/resources/RuntimePolicies.ts b/packages/cdk/resources/RuntimePolicies.ts index df35703b6..4250aac37 100644 --- a/packages/cdk/resources/RuntimePolicies.ts +++ b/packages/cdk/resources/RuntimePolicies.ts @@ -13,7 +13,7 @@ export interface RuntimePoliciesProps { readonly dataSourceArn: string readonly promptName: string readonly ragModelId: string - readonly queryReformulationModelId: string + readonly reformulationModelId: string readonly docsBucketArn: string readonly docsBucketKmsKeyArn: string } @@ -32,7 +32,7 @@ export class RuntimePolicies extends Construct { actions: ["bedrock:InvokeModel"], resources: [ `arn:aws:bedrock:${props.region}::foundation-model/${props.ragModelId}`, - `arn:aws:bedrock:${props.region}::foundation-model/${props.queryReformulationModelId}` + `arn:aws:bedrock:${props.region}::foundation-model/${props.reformulationModelId}` ] }) diff --git a/packages/cdk/resources/VectorKnowledgeBaseResources.ts b/packages/cdk/resources/VectorKnowledgeBaseResources.ts index 37717fd51..8abd8e1b2 100644 --- a/packages/cdk/resources/VectorKnowledgeBaseResources.ts +++ b/packages/cdk/resources/VectorKnowledgeBaseResources.ts @@ -156,15 +156,12 @@ export class VectorKnowledgeBaseResources extends Construct { // Create S3 data source for knowledge base documents // prefix pointed to processed/ to only ingest converted markdown documents - const chunkingConfiguration = { - ...ChunkingStrategy.HIERARCHICAL_TITAN.configuration, - hierarchicalChunkingConfiguration: { - overlapTokens: 60, - levelConfigurations: [ - {maxTokens: 1000}, // Parent chunk configuration, - {maxTokens: 300} // Child chunk configuration - ] - } + const chunkingConfiguration: CfnDataSource.ChunkingConfigurationProperty = { + ...ChunkingStrategy.SEMANTIC.configuration, + fixedSizeChunkingConfiguration: { + maxTokens: 512, + overlapPercentage: 25 + } satisfies CfnDataSource.FixedSizeChunkingConfigurationProperty } const hash = crypto.createHash("md5") @@ -183,9 +180,6 @@ export class VectorKnowledgeBaseResources extends Construct { bucketArn: props.docsBucket.bucketArn, inclusionPrefixes: ["processed/"] } - }, - vectorIngestionConfiguration: { - chunkingConfiguration: chunkingConfiguration } }) diff --git a/packages/cdk/stacks/EpsAssistMeStack.ts b/packages/cdk/stacks/EpsAssistMeStack.ts index cdf1df80f..468f6c8df 100644 --- a/packages/cdk/stacks/EpsAssistMeStack.ts +++ b/packages/cdk/stacks/EpsAssistMeStack.ts @@ -163,9 +163,9 @@ export class EpsAssistMeStack extends Stack { knowledgeBaseArn: vectorKB.knowledgeBase.attrKnowledgeBaseArn, guardrailArn: vectorKB.guardrail.guardrailArn, dataSourceArn: vectorKB.dataSourceArn, - promptName: bedrockPromptResources.queryReformulationPrompt.promptName, - ragModelId: bedrockPromptResources.ragModelId, - queryReformulationModelId: bedrockPromptResources.queryReformulationModelId, + promptName: bedrockPromptResources.reformulationPrompt.promptName, + ragModelId: bedrockPromptResources.modelId, + reformulationModelId: bedrockPromptResources.modelId, docsBucketArn: storage.kbDocsBucket.bucketArn, docsBucketKmsKeyArn: storage.kbDocsKmsKey.keyArn }) @@ -192,12 +192,12 @@ export class EpsAssistMeStack extends Stack { slackBotTokenSecret: secrets.slackBotTokenSecret, slackBotSigningSecret: secrets.slackBotSigningSecret, slackBotStateTable: tables.slackBotStateTable.table, - reformulationPromptName: bedrockPromptResources.queryReformulationPrompt.promptName, + reformulationPromptName: bedrockPromptResources.reformulationPrompt.promptName, ragResponsePromptName: bedrockPromptResources.ragResponsePrompt.promptName, - reformulationPromptVersion: bedrockPromptResources.queryReformulationPrompt.promptVersion, + reformulationPromptVersion: bedrockPromptResources.reformulationPrompt.promptVersion, ragResponsePromptVersion: bedrockPromptResources.ragResponsePrompt.promptVersion, - ragModelId: bedrockPromptResources.ragModelId, - queryReformulationModelId: bedrockPromptResources.queryReformulationModelId, + ragModelId: bedrockPromptResources.modelId, + reformulationModelId: bedrockPromptResources.modelId, isPullRequest: isPullRequest, mainSlackBotLambdaExecutionRoleArn: mainSlackBotLambdaExecutionRoleArn, notifyS3UploadFunctionPolicy: runtimePolicies.notifyS3UploadFunctionPolicy, @@ -276,13 +276,7 @@ export class EpsAssistMeStack extends Stack { // Output: SlackBot Endpoint new CfnOutput(this, "SlackBotCommandsEndpoint", { value: `https://${apis.apis["api"].api.domainName?.domainName}/slack/commands`, - description: "Slack Commands API endpoint for slash commands" - }) - - // Output: Bedrock Prompt ARN - new CfnOutput(this, "QueryReformulationPromptArn", { - value: bedrockPromptResources.queryReformulationPrompt.promptArn, - description: "ARN of the query reformulation prompt in Bedrock" + description: "Slack Commands API endpoint for /slash commands" }) new CfnOutput(this, "kbDocsBucketArn", { diff --git a/packages/slackBotFunction/app/core/config.py b/packages/slackBotFunction/app/core/config.py index 6e980c55b..1127a225e 100644 --- a/packages/slackBotFunction/app/core/config.py +++ b/packages/slackBotFunction/app/core/config.py @@ -81,6 +81,8 @@ def get_retrieve_generate_config() -> BedrockConfig: GUARD_VERSION = os.environ["GUARD_RAIL_VERSION"] RAG_RESPONSE_PROMPT_NAME = os.environ["RAG_RESPONSE_PROMPT_NAME"] RAG_RESPONSE_PROMPT_VERSION = os.environ["RAG_RESPONSE_PROMPT_VERSION"] + REFORMULATION_RESPONSE_PROMPT_NAME = os.environ["REFORMULATION_RESPONSE_PROMPT_NAME"] + REFORMULATION_RESPONSE_PROMPT_VERSION = os.environ["REFORMULATION_RESPONSE_PROMPT_VERSION"] logger.info( "Guardrail configuration loaded", extra={"guardrail_id": GUARD_RAIL_ID, "guardrail_version": GUARD_VERSION} @@ -94,6 +96,8 @@ def get_retrieve_generate_config() -> BedrockConfig: GUARD_VERSION, RAG_RESPONSE_PROMPT_NAME, RAG_RESPONSE_PROMPT_VERSION, + REFORMULATION_RESPONSE_PROMPT_NAME, + REFORMULATION_RESPONSE_PROMPT_VERSION, ) @@ -148,6 +152,8 @@ class BedrockConfig: GUARD_VERSION: str RAG_RESPONSE_PROMPT_NAME: str RAG_RESPONSE_PROMPT_VERSION: str + REFORMULATION_PROMPT_NAME: str + REFORMULATION_PROMPT_VERSION: str @dataclass diff --git a/packages/slackBotFunction/app/services/ai_processor.py b/packages/slackBotFunction/app/services/ai_processor.py index 857a9e24e..d7f4a07f9 100644 --- a/packages/slackBotFunction/app/services/ai_processor.py +++ b/packages/slackBotFunction/app/services/ai_processor.py @@ -6,20 +6,30 @@ """ from app.services.bedrock import query_bedrock -from app.services.query_reformulator import reformulate_query -from app.core.config import get_logger +from app.core.config import get_retrieve_generate_config, get_logger from app.core.types import AIProcessorResponse +from app.services.prompt_loader import load_prompt logger = get_logger() def process_ai_query(user_query: str, session_id: str | None = None) -> AIProcessorResponse: """shared AI processing logic for both slack and direct invocation""" - # reformulate: improves vector search quality in knowledge base - reformulated_query = reformulate_query(user_query) - # session_id enables conversation continuity across multiple queries - kb_response = query_bedrock(reformulated_query, session_id) + config = get_retrieve_generate_config() + + reformulation_prompt_template = load_prompt( + config.REFORMULATION_PROMPT_NAME, + config.REFORMULATION_PROMPT_VERSION, + ) + # Don't provide sessionId as this conflicts with the sessions knowledgebase settings + reformulation_prompt = query_bedrock(user_query, reformulation_prompt_template, config) + reformulation_text = reformulation_prompt["output"]["text"] + + logger.debug("reformulation_text", extra={"text": reformulation_text}) + + rag_prompt_template = load_prompt(config.RAG_RESPONSE_PROMPT_NAME, config.RAG_RESPONSE_PROMPT_VERSION) + kb_response = query_bedrock(reformulation_text, rag_prompt_template, config, session_id) logger.info( "response from bedrock", diff --git a/packages/slackBotFunction/app/services/bedrock.py b/packages/slackBotFunction/app/services/bedrock.py index 44d020196..3cda7da7d 100644 --- a/packages/slackBotFunction/app/services/bedrock.py +++ b/packages/slackBotFunction/app/services/bedrock.py @@ -5,14 +5,18 @@ from mypy_boto3_bedrock_runtime.client import BedrockRuntimeClient from mypy_boto3_bedrock_agent_runtime.type_defs import RetrieveAndGenerateResponseTypeDef -from app.core.config import get_retrieve_generate_config, get_logger -from app.services.prompt_loader import load_prompt +from app.core.config import BedrockConfig, get_logger logger = get_logger() -def query_bedrock(user_query: str, session_id: str = None) -> RetrieveAndGenerateResponseTypeDef: +def query_bedrock( + user_query: str, + prompt_template: dict, + config: BedrockConfig, + session_id: str = None, +) -> RetrieveAndGenerateResponseTypeDef: """ Query Amazon Bedrock Knowledge Base using RAG (Retrieval-Augmented Generation) @@ -20,12 +24,10 @@ def query_bedrock(user_query: str, session_id: str = None) -> RetrieveAndGenerat a response using the configured LLM model with guardrails for safety. """ - config = get_retrieve_generate_config() - prompt_template = load_prompt(config.RAG_RESPONSE_PROMPT_NAME, config.RAG_RESPONSE_PROMPT_VERSION) inference_config = prompt_template.get("inference_config") if not inference_config: - default_values = {"temperature": 0, "maxTokens": 1500, "topP": 1} + default_values = {"temperature": 0, "maxTokens": 1024, "topP": 0.1} inference_config = default_values logger.warning( "No inference configuration found in prompt template; using default values", @@ -43,9 +45,7 @@ def query_bedrock(user_query: str, session_id: str = None) -> RetrieveAndGenerat "knowledgeBaseConfiguration": { "knowledgeBaseId": config.KNOWLEDGEBASE_ID, "modelArn": prompt_template.get("model_id", config.RAG_MODEL_ID), - "retrievalConfiguration": { - "vectorSearchConfiguration": {"numberOfResults": 5, "overrideSearchType": "SEMANTIC"} - }, + "retrievalConfiguration": {"vectorSearchConfiguration": {"numberOfResults": 5}}, "generationConfiguration": { "guardrailConfiguration": { "guardrailId": config.GUARD_RAIL_ID, diff --git a/packages/slackBotFunction/app/services/prompt_loader.py b/packages/slackBotFunction/app/services/prompt_loader.py index 10d941fba..cf1bebded 100644 --- a/packages/slackBotFunction/app/services/prompt_loader.py +++ b/packages/slackBotFunction/app/services/prompt_loader.py @@ -9,14 +9,14 @@ logger = get_logger() -def _render_prompt(template_config: dict) -> str: +def _render_system_prompt(template_config: dict) -> str: """ Returns a unified prompt string regardless of template type. """ - chat_cfg = template_config.get("chat") - if chat_cfg: - return parse_system_message(chat_cfg) + chat_configuration = template_config.get("chat") + if chat_configuration: + return parse_system_message(chat_configuration) text_cfg = template_config.get("text") if isinstance(text_cfg, dict) and "text" in text_cfg: @@ -31,10 +31,10 @@ def _render_prompt(template_config: dict) -> str: raise PromptLoadError(f"Unsupported prompt configuration. Keys: {list(template_config.keys())}") -def parse_system_message(chat_cfg: dict) -> str: +def parse_system_message(chat_configuration: dict) -> str: parts: list[str] = [] - system_items = chat_cfg.get("system", []) + system_items = chat_configuration.get("system", []) logger.debug("Processing system messages for prompt rendering", extra={"system_items": system_items}) if isinstance(system_items, list): system_texts = [ @@ -50,9 +50,11 @@ def parse_system_message(chat_cfg: dict) -> str: "assistant": "Assistant: ", } - logger.debug("Processing chat messages for prompt rendering", extra={"messages": chat_cfg.get("messages", [])}) + logger.debug( + "Processing chat messages for prompt rendering", extra={"messages": chat_configuration.get("messages", [])} + ) - for msg in chat_cfg.get("messages", []): + for msg in chat_configuration.get("messages", []): role = (msg.get("role") or "").lower() prefix = role_prefix.get(role) if not prefix: @@ -106,11 +108,11 @@ def load_prompt(prompt_name: str, prompt_version: str = None) -> dict: # Extract and render the prompt template template_config = variant["templateConfiguration"] - prompt_text = _render_prompt(template_config) + prompt_text = _render_system_prompt(template_config) actual_version = response.get("version", "DRAFT") # Extract inference configuration with defaults - default_inference = {"temperature": 0, "topP": 1, "maxTokens": 1500} + default_inference = {"temperature": 0, "topP": 0.1, "maxTokens": 1024} model_id = variant.get("modelId", "") raw_inference = variant.get("inferenceConfiguration", {}) raw_text_config = raw_inference.get("text", {}) diff --git a/packages/slackBotFunction/app/services/query_reformulator.py b/packages/slackBotFunction/app/services/query_reformulator.py deleted file mode 100644 index 4a2bd060e..000000000 --- a/packages/slackBotFunction/app/services/query_reformulator.py +++ /dev/null @@ -1,83 +0,0 @@ -import os -import traceback -import boto3 - -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 - -logger = get_logger() - - -def reformulate_query(user_query: str) -> str: - """ - Reformulate user query using Amazon Nova Lite for better RAG retrieval. - - Loads prompt template from Bedrock Prompt Management, formats it with the user's - query, and uses Nova Lite to generate a reformulated version optimized for vector search. - """ - try: - client: BedrockRuntimeClient = boto3.client("bedrock-runtime", region_name=os.environ["AWS_REGION"]) - model_id = os.environ["QUERY_REFORMULATION_MODEL_ID"] - - # Load prompt template from Bedrock Prompt Management - prompt_name = os.environ.get("QUERY_REFORMULATION_PROMPT_NAME") - prompt_version = os.environ.get("QUERY_REFORMULATION_PROMPT_VERSION", "DRAFT") - - if not prompt_name: - raise ConfigurationError("QUERY_REFORMULATION_PROMPT_NAME environment variable not set") - - # Load prompt with specified version (DRAFT by default) - prompt_template = load_prompt(prompt_name, prompt_version) - - logger.info( - "Prompt loaded successfully from Bedrock", - extra={"prompt_name": prompt_name, "version_used": prompt_version}, - ) - - # Format the prompt with the user query (using double braces from Bedrock template) - prompt = prompt_template.get("prompt_text").replace("{{user_query}}", user_query) - result = invoke_model( - prompt=prompt, model_id=model_id, client=client, inference_config=prompt_template.get("inference_config") - ) - - reformulated_query = result["content"][0]["text"].strip() - - logger.info( - "Query reformulated successfully using Bedrock prompt", - extra={ - "original_query": user_query, - "reformulated_query": reformulated_query, - "prompt_version_used": prompt_version, - "prompt_source": "bedrock_prompt_management", - }, - ) - - return reformulated_query - - except Exception as e: - logger.error( - f"Failed to reformulate query using Bedrock prompts: {e}", - extra={ - "original_query": user_query, - "prompt_name": os.environ.get("QUERY_REFORMULATION_PROMPT_NAME"), - "prompt_version": os.environ.get("QUERY_REFORMULATION_PROMPT_VERSION", "auto"), - "error_type": type(e).__name__, - "error": traceback.format_exc(), - }, - ) - - # Graceful degradation - return original query but alert on infrastructure issue - logger.error( - "Query reformulation degraded: Bedrock Prompt Management unavailable", - extra={ - "service_status": "degraded", - "fallback_action": "using_original_query", - "requires_attention": True, - "impact": "reduced_rag_quality", - }, - ) - - return user_query # Minimal fallback - just return original query diff --git a/packages/slackBotFunction/app/slack/slack_events.py b/packages/slackBotFunction/app/slack/slack_events.py index 726968cc6..46adcdbb7 100644 --- a/packages/slackBotFunction/app/slack/slack_events.py +++ b/packages/slackBotFunction/app/slack/slack_events.py @@ -281,10 +281,14 @@ def convert_markdown_to_slack(body: str) -> str: body = re.sub(r"([\*_]){2,10}([^*]+)([\*_]){2,10}", r"\1\2\1", body) # 3. Handle Lists (Handle various bullet points and dashes, inc. unicode support) - body = re.sub(r"(?:^|\s{1,10})[-•–—▪‣◦⁃]\s{0,10}", r"\n- ", body) + body = re.sub(r"[-•–—▪‣◦⁃]", r"-", body) - # 4. Convert Markdown Links [text](url) to Slack - body = re.sub(r"\[([^\]]+)\]\(([^\)]+)\)", r"<\2|\1>", body) + # 3. Convert Markdown Links [text](url) to Slack + matches = re.findall(r"\[([^\]]+)\]\(([^\)]+)\)", body) + + for match in matches: + text, url = match + body = body.replace(f"[{text}]({url})", f"<{url}|{text.replace('\n', ' ').replace(r"/n{2,}", ' ')[:50]}>") return body.strip() diff --git a/packages/slackBotFunction/tests/conftest.py b/packages/slackBotFunction/tests/conftest.py index 3b62aecc8..e8795660b 100644 --- a/packages/slackBotFunction/tests/conftest.py +++ b/packages/slackBotFunction/tests/conftest.py @@ -17,13 +17,13 @@ def mock_env(): "SLACK_SIGNING_SECRET_PARAMETER": "/test/signing-secret", "SLACK_BOT_STATE_TABLE": "test-bot-state-table", "KNOWLEDGEBASE_ID": "test-kb-id", - "RAG_MODEL_ID": "test-model-id", "AWS_REGION": "eu-west-2", "GUARD_RAIL_ID": "test-guard-id", "GUARD_RAIL_VERSION": "1", - "QUERY_REFORMULATION_MODEL_ID": "test-model", - "QUERY_REFORMULATION_PROMPT_NAME": "test-prompt", - "QUERY_REFORMULATION_PROMPT_VERSION": "DRAFT", + "REFORMULATION_MODEL_ID": "test-model", + "REFORMULATION_RESPONSE_PROMPT_NAME": "test-prompt", + "REFORMULATION_RESPONSE_PROMPT_VERSION": "DRAFT", + "RAG_MODEL_ID": "test-model-id", "RAG_RESPONSE_PROMPT_NAME": "test-rag-prompt", "RAG_RESPONSE_PROMPT_VERSION": "DRAFT", } diff --git a/packages/slackBotFunction/tests/test_ai_processor.py b/packages/slackBotFunction/tests/test_ai_processor.py index 8cb9f7fbe..e971f4687 100644 --- a/packages/slackBotFunction/tests/test_ai_processor.py +++ b/packages/slackBotFunction/tests/test_ai_processor.py @@ -1,17 +1,30 @@ """shared ai processor - validates query reformulation and bedrock integration""" import pytest -from unittest.mock import patch +from unittest.mock import call, patch, ANY from app.services.ai_processor import process_ai_query +@pytest.fixture +def mock_config_setup(mock_load_prompt, mock_config): + """Setup common mock configurations""" + mock_load_prompt.return_value = {"prompt_text": "test_prompt", "model_id": "model_id", "inference_config": {}} + mock_config.get_retrieve_generate_config.return_value = { + "REFORMULATION_PROMPT_NAME": "test", + "REFORMULATION_PROMPT_VERSION": "test", + "RAG_RESPONSE_PROMPT_NAME": "test", + "RAG_RESPONSE_PROMPT_VERSION": "test", + } + return mock_load_prompt, mock_config + + class TestAIProcessor: + @patch("app.services.ai_processor.get_retrieve_generate_config") + @patch("app.services.ai_processor.load_prompt") @patch("app.services.ai_processor.query_bedrock") - @patch("app.services.ai_processor.reformulate_query") - def test_process_ai_query_without_session(self, mock_reformulate, mock_bedrock): + def test_process_ai_query_without_session(self, mock_bedrock, mock_load_prompt, mock_config): """new conversation: no session context passed to bedrock""" - mock_reformulate.return_value = "reformulated: How to authenticate EPS API?" mock_bedrock.return_value = { "output": {"text": "To authenticate with EPS API, you need..."}, "sessionId": "new-session-abc123", @@ -26,60 +39,59 @@ def test_process_ai_query_without_session(self, mock_reformulate, mock_bedrock): assert result["citations"][0]["title"] == "EPS Authentication Guide" assert "kb_response" in result - mock_reformulate.assert_called_once_with("How to authenticate EPS API?") - mock_bedrock.assert_called_once_with("reformulated: How to authenticate EPS API?", None) + assert mock_bedrock.call_count == 2 + assert mock_load_prompt.call_count == 2 + + mock_bedrock.assert_has_calls( + [ + call("How to authenticate EPS API?", mock_load_prompt.return_value, ANY), + call("To authenticate with EPS API, you need...", mock_load_prompt.return_value, ANY, None), + ] + ) + @patch("app.services.ai_processor.get_retrieve_generate_config") + @patch("app.services.ai_processor.load_prompt") @patch("app.services.ai_processor.query_bedrock") - @patch("app.services.ai_processor.reformulate_query") - def test_process_ai_query_with_session(self, mock_reformulate, mock_bedrock): + def test_process_ai_query_with_session(self, mock_bedrock, mock_load_prompt, mock_config): """conversation continuity: existing session maintained across queries""" - mock_reformulate.return_value = "reformulated: What about rate limits?" + mock_prompt = "What about rate limits?" + mock_session_id = "existing-session-456" mock_bedrock.return_value = { "output": {"text": "EPS API has rate limits of..."}, - "sessionId": "existing-session-456", + "sessionId": mock_session_id, "citations": [], } - result = process_ai_query("What about rate limits?", session_id="existing-session-456") + result = process_ai_query(mock_prompt, session_id="existing-session-456") assert result["text"] == "EPS API has rate limits of..." assert result["session_id"] == "existing-session-456" assert result["citations"] == [] assert "kb_response" in result - mock_reformulate.assert_called_once_with("What about rate limits?") - mock_bedrock.assert_called_once_with("reformulated: What about rate limits?", "existing-session-456") + mock_bedrock.assert_has_calls( + [ + call("What about rate limits?", mock_load_prompt.return_value, ANY), + call("EPS API has rate limits of...", mock_load_prompt.return_value, ANY, mock_session_id), + ] + ) + @patch("app.services.ai_processor.get_retrieve_generate_config") + @patch("app.services.ai_processor.load_prompt") @patch("app.services.ai_processor.query_bedrock") - @patch("app.services.ai_processor.reformulate_query") - def test_process_ai_query_reformulate_error(self, mock_reformulate, mock_bedrock): - """graceful degradation: reformulation failure bubbles up""" - mock_reformulate.side_effect = Exception("Query reformulation failed") - - with pytest.raises(Exception) as exc_info: - process_ai_query("How to authenticate EPS API?") - - assert "Query reformulation failed" in str(exc_info.value) - mock_bedrock.assert_not_called() - - @patch("app.services.ai_processor.query_bedrock") - @patch("app.services.ai_processor.reformulate_query") - def test_process_ai_query_bedrock_error(self, mock_reformulate, mock_bedrock): + def test_process_ai_query_bedrock_error(self, mock_bedrock, mock_load_prompt, mock_config): """bedrock service failure: error propagated to caller""" - mock_reformulate.return_value = "reformulated query" mock_bedrock.side_effect = Exception("Bedrock service error") - with pytest.raises(Exception) as exc_info: process_ai_query("How to authenticate EPS API?") assert "Bedrock service error" in str(exc_info.value) - mock_reformulate.assert_called_once() + @patch("app.services.ai_processor.get_retrieve_generate_config") + @patch("app.services.ai_processor.load_prompt") @patch("app.services.ai_processor.query_bedrock") - @patch("app.services.ai_processor.reformulate_query") - def test_process_ai_query_missing_citations(self, mock_reformulate, mock_bedrock): + def test_process_ai_query_missing_citations(self, mock_bedrock, mock_load_prompt, mock_config): """bedrock response incomplete: citations default to empty list""" - mock_reformulate.return_value = "reformulated query" mock_bedrock.return_value = { "output": {"text": "Response without citations"}, "sessionId": "session-123", @@ -92,11 +104,11 @@ def test_process_ai_query_missing_citations(self, mock_reformulate, mock_bedrock assert result["session_id"] == "session-123" assert result["citations"] == [] # safe default when bedrock omits citations + @patch("app.services.ai_processor.get_retrieve_generate_config") + @patch("app.services.ai_processor.load_prompt") @patch("app.services.ai_processor.query_bedrock") - @patch("app.services.ai_processor.reformulate_query") - def test_process_ai_query_missing_session_id(self, mock_reformulate, mock_bedrock): + def test_process_ai_query_missing_session_id(self, mock_bedrock, mock_load_prompt, mock_config): """bedrock response incomplete: session_id properly handles None""" - mock_reformulate.return_value = "reformulated query" mock_bedrock.return_value = { "output": {"text": "Response without session"}, "citations": [], @@ -109,11 +121,11 @@ def test_process_ai_query_missing_session_id(self, mock_reformulate, mock_bedroc assert result["session_id"] is None # explicit None when bedrock omits sessionId assert result["citations"] == [] + @patch("app.services.ai_processor.get_retrieve_generate_config") + @patch("app.services.ai_processor.load_prompt") @patch("app.services.ai_processor.query_bedrock") - @patch("app.services.ai_processor.reformulate_query") - def test_process_ai_query_empty_query(self, mock_reformulate, mock_bedrock): + def test_process_ai_query_empty_query(self, mock_bedrock, mock_load_prompt, mock_config): """edge case: empty query still processed through full pipeline""" - mock_reformulate.return_value = "" mock_bedrock.return_value = { "output": {"text": "Please provide a question"}, "sessionId": "session-empty", @@ -123,14 +135,20 @@ def test_process_ai_query_empty_query(self, mock_reformulate, mock_bedrock): result = process_ai_query("") assert result["text"] == "Please provide a question" - mock_reformulate.assert_called_once_with("") - mock_bedrock.assert_called_once_with("", None) + mock_bedrock.assert_called_with + + mock_bedrock.assert_has_calls( + [ + call("", ANY, ANY), + call("Please provide a question", ANY, ANY, None), + ] + ) + @patch("app.services.ai_processor.get_retrieve_generate_config") + @patch("app.services.ai_processor.load_prompt") @patch("app.services.ai_processor.query_bedrock") - @patch("app.services.ai_processor.reformulate_query") - def test_process_ai_query_includes_raw_response(self, mock_reformulate, mock_bedrock): + def test_process_ai_query_includes_raw_response(self, mock_bedrock, mock_load_prompt, mock_config): """slack needs raw bedrock data: kb_response preserved for session handling""" - mock_reformulate.return_value = "reformulated query" raw_response = { "output": {"text": "Test response"}, "sessionId": "test-123", diff --git a/packages/slackBotFunction/tests/test_bedrock_integration.py b/packages/slackBotFunction/tests/test_bedrock_integration.py index 8b4877c7c..24d87cd5a 100644 --- a/packages/slackBotFunction/tests/test_bedrock_integration.py +++ b/packages/slackBotFunction/tests/test_bedrock_integration.py @@ -1,10 +1,10 @@ +import math import sys -from unittest.mock import Mock, patch +from unittest.mock import Mock, patch, MagicMock, ANY -@patch("app.services.prompt_loader.load_prompt") @patch("boto3.client") -def test_get_bedrock_knowledgebase_response(mock_boto_client: Mock, mock_load_prompt: Mock, mock_env: Mock): +def test_get_bedrock_knowledgebase_response(mock_boto_client: Mock, mock_env: Mock): """Test Bedrock knowledge base integration""" # set up mocks mock_client = Mock() @@ -17,18 +17,16 @@ def test_get_bedrock_knowledgebase_response(mock_boto_client: Mock, mock_load_pr from app.services.bedrock import query_bedrock # perform operation - result = query_bedrock("test query") + result = query_bedrock("test query", {"inference_config": None}, MagicMock()) # assertions - mock_load_prompt.assert_called_once_with("test-rag-prompt", "DRAFT") - mock_boto_client.assert_called_once_with(service_name="bedrock-agent-runtime", region_name="eu-west-2") + mock_boto_client.assert_called_once_with(service_name="bedrock-agent-runtime", region_name=ANY) mock_client.retrieve_and_generate.assert_called_once() assert result["output"]["text"] == "bedrock response" -@patch("app.services.prompt_loader.load_prompt") @patch("boto3.client") -def test_query_bedrock_with_session(mock_boto_client: Mock, mock_load_prompt: Mock, mock_env: Mock): +def test_query_bedrock_with_session(mock_boto_client: Mock, mock_env: Mock): """Test query_bedrock with existing session""" # set up mocks mock_client = Mock() @@ -42,18 +40,16 @@ def test_query_bedrock_with_session(mock_boto_client: Mock, mock_load_prompt: Mo from app.services.bedrock import query_bedrock # perform operation - result = query_bedrock("test query", session_id="existing_session") + result = query_bedrock("test query", {"inference_config": None}, MagicMock(), session_id="existing_session") # assertions - mock_load_prompt.assert_called_once_with("test-rag-prompt", "DRAFT") assert result == mock_response call_args = mock_client.retrieve_and_generate.call_args[1] assert call_args["sessionId"] == "existing_session" -@patch("app.services.prompt_loader.load_prompt") @patch("boto3.client") -def test_query_bedrock_without_session(mock_boto_client: Mock, mock_load_prompt: Mock, mock_env: Mock): +def test_query_bedrock_without_session(mock_boto_client: Mock, mock_env: Mock): """Test query_bedrock without session""" # set up mocks mock_client = Mock() @@ -67,24 +63,21 @@ def test_query_bedrock_without_session(mock_boto_client: Mock, mock_load_prompt: from app.services.bedrock import query_bedrock # perform operation - result = query_bedrock("test query") + result = query_bedrock("test query", {"inference_config": None}, MagicMock()) # assertions - mock_load_prompt.assert_called_once_with("test-rag-prompt", "DRAFT") assert result == mock_response call_args = mock_client.retrieve_and_generate.call_args[1] assert "sessionId" not in call_args -@patch("app.services.prompt_loader.load_prompt") @patch("boto3.client") -def test_query_bedrock_check_prompt(mock_boto_client: Mock, mock_load_prompt: Mock, mock_env: Mock): +def test_query_bedrock_check_prompt(mock_boto_client: Mock, mock_env: Mock): """Test query_bedrock prompt loading""" # set up mocks mock_client = Mock() mock_boto_client.return_value = mock_client mock_client.retrieve_and_generate.return_value = {"output": {"text": "response"}} - mock_load_prompt.return_value = {"prompt_text": "Test prompt template", "inference_config": {}} # delete and import module to test if "app.services.bedrock" in sys.modules: @@ -92,10 +85,9 @@ def test_query_bedrock_check_prompt(mock_boto_client: Mock, mock_load_prompt: Mo from app.services.bedrock import query_bedrock # perform operation - result = query_bedrock("test query") + result = query_bedrock("test query", {"inference_config": None, "prompt_text": "Test prompt template"}, MagicMock()) # assertions - mock_load_prompt.assert_called_once_with("test-rag-prompt", "DRAFT") call_args = mock_client.retrieve_and_generate.call_args[1] prompt_template = call_args["retrieveAndGenerateConfiguration"]["knowledgeBaseConfiguration"][ "generationConfiguration" @@ -104,18 +96,13 @@ def test_query_bedrock_check_prompt(mock_boto_client: Mock, mock_load_prompt: Mo assert result["output"]["text"] == "response" -@patch("app.services.prompt_loader.load_prompt") @patch("boto3.client") -def test_query_bedrock_check_config(mock_boto_client: Mock, mock_load_prompt: Mock, mock_env: Mock): +def test_query_bedrock_check_config(mock_boto_client: Mock, mock_env: Mock): """Test query_bedrock config loading""" # set up mocks mock_client = Mock() mock_boto_client.return_value = mock_client mock_client.retrieve_and_generate.return_value = {"output": {"text": "response"}} - mock_load_prompt.return_value = { - "prompt_text": "Test prompt template", - "inference_config": {"temperature": "0", "maxTokens": "1500", "topP": "1"}, - } # delete and import module to test if "app.services.bedrock" in sys.modules: @@ -123,7 +110,7 @@ def test_query_bedrock_check_config(mock_boto_client: Mock, mock_load_prompt: Mo from app.services.bedrock import query_bedrock # perform operation - query_bedrock("test query") + query_bedrock("test query", {"inference_config": None}, MagicMock()) # assertions call_args = mock_client.retrieve_and_generate.call_args[1] @@ -131,6 +118,6 @@ def test_query_bedrock_check_config(mock_boto_client: Mock, mock_load_prompt: Mo "generationConfiguration" ]["inferenceConfig"]["textInferenceConfig"] - assert prompt_config["temperature"] == "0" - assert prompt_config["maxTokens"] == "1500" - assert prompt_config["topP"] == "1" + assert prompt_config["temperature"] == 0 + assert prompt_config["maxTokens"] == 1024 + assert math.isclose(prompt_config["topP"], 0.1, rel_tol=1e-09, abs_tol=1e-09) diff --git a/packages/slackBotFunction/tests/test_prompt_loader.py b/packages/slackBotFunction/tests/test_prompt_loader.py index c86d8efd7..9733b85c4 100644 --- a/packages/slackBotFunction/tests/test_prompt_loader.py +++ b/packages/slackBotFunction/tests/test_prompt_loader.py @@ -158,9 +158,9 @@ def test_get_render_prompt_chat_dict(mock_logger: Mock, mock_env: Mock): # delete and import module to test if "app.services.prompt_loader" in sys.modules: del sys.modules["app.services.prompt_loader"] - from app.services.prompt_loader import _render_prompt + from app.services.prompt_loader import _render_system_prompt - result = _render_prompt( + result = _render_system_prompt( { "chat": { "system": [ @@ -186,9 +186,9 @@ def test_get_render_prompt_chat_dict_no_role(mock_logger: Mock, mock_env: Mock): # delete and import module to test if "app.services.prompt_loader" in sys.modules: del sys.modules["app.services.prompt_loader"] - from app.services.prompt_loader import _render_prompt + from app.services.prompt_loader import _render_system_prompt - result = _render_prompt( + result = _render_system_prompt( { "chat": { "system": [ @@ -213,9 +213,9 @@ def test_get_render_prompt_chat_dict_multiple_questions(mock_logger: Mock, mock_ # delete and import module to test if "app.services.prompt_loader" in sys.modules: del sys.modules["app.services.prompt_loader"] - from app.services.prompt_loader import _render_prompt + from app.services.prompt_loader import _render_system_prompt - result = _render_prompt( + result = _render_system_prompt( { "chat": { "messages": [ @@ -244,9 +244,9 @@ def test_get_render_prompt_chat_dict_multiple_assistant_prompts(mock_logger: Moc # delete and import module to test if "app.services.prompt_loader" in sys.modules: del sys.modules["app.services.prompt_loader"] - from app.services.prompt_loader import _render_prompt + from app.services.prompt_loader import _render_system_prompt - result = _render_prompt( + result = _render_system_prompt( { "chat": { "system": [ @@ -266,9 +266,9 @@ def test_get_render_prompt_chat_dict_multiple_assistant_message(mock_logger: Moc # delete and import module to test if "app.services.prompt_loader" in sys.modules: del sys.modules["app.services.prompt_loader"] - from app.services.prompt_loader import _render_prompt + from app.services.prompt_loader import _render_system_prompt - result = _render_prompt( + result = _render_system_prompt( { "chat": { "messages": [ @@ -297,9 +297,9 @@ def test_get_render_prompt_text_dict(mock_logger: Mock, mock_env: Mock): # delete and import module to test if "app.services.prompt_loader" in sys.modules: del sys.modules["app.services.prompt_loader"] - from app.services.prompt_loader import _render_prompt + from app.services.prompt_loader import _render_system_prompt - result = _render_prompt( + result = _render_system_prompt( { "text": "Second Prompt.", }, @@ -313,9 +313,9 @@ def test_get_render_prompt_empty(mock_logger: Mock, mock_env: Mock): # delete and import module to test if "app.services.prompt_loader" in sys.modules: del sys.modules["app.services.prompt_loader"] - from app.services.prompt_loader import _render_prompt + from app.services.prompt_loader import _render_system_prompt - result = _render_prompt( + result = _render_system_prompt( { "chat": { "system": [], @@ -332,11 +332,11 @@ def test_render_prompt_raises_configuration_error_empty(mock_logger): with patch("app.core.config.get_logger", return_value=mock_logger): if "app.services.prompt_loader" in sys.modules: del sys.modules["app.services.prompt_loader"] - from app.services.prompt_loader import _render_prompt + from app.services.prompt_loader import _render_system_prompt from app.services.exceptions import PromptLoadError with pytest.raises(PromptLoadError) as excinfo: - _render_prompt({}) + _render_system_prompt({}) # Verify the exception and logger call assert excinfo.type is PromptLoadError @@ -348,11 +348,11 @@ def test_render_prompt_raises_configuration_error_text_missing(mock_logger): with patch("app.core.config.get_logger", return_value=mock_logger): if "app.services.prompt_loader" in sys.modules: del sys.modules["app.services.prompt_loader"] - from app.services.prompt_loader import _render_prompt + from app.services.prompt_loader import _render_system_prompt from app.services.exceptions import PromptLoadError with pytest.raises(PromptLoadError) as excinfo: - _render_prompt({"text": {}}) + _render_system_prompt({"text": {}}) # Verify the exception and logger call assert excinfo.type is PromptLoadError diff --git a/packages/slackBotFunction/tests/test_query_reformulator.py b/packages/slackBotFunction/tests/test_query_reformulator.py deleted file mode 100644 index f9a629857..000000000 --- a/packages/slackBotFunction/tests/test_query_reformulator.py +++ /dev/null @@ -1,94 +0,0 @@ -import sys -import pytest -from unittest.mock import ANY, Mock, patch, MagicMock -from botocore.exceptions import ClientError - - -@pytest.fixture -def mock_logger(): - return MagicMock() - - -@patch("app.services.prompt_loader.load_prompt") -@patch("app.services.bedrock.invoke_model") -def test_reformulate_query_returns_string(mock_invoke_model: Mock, mock_load_prompt: Mock, mock_env: Mock): - """Test that reformulate_query returns a string without crashing""" - # set up mocks - mock_load_prompt.return_value = {"prompt_text": "Test reformat. {{user_query}}", "inference_config": {}} - mock_invoke_model.return_value = {"content": [{"text": "foo"}]} - - # delete and import module to test - if "app.services.query_reformulator" in sys.modules: - del sys.modules["app.services.query_reformulator"] - from app.services.query_reformulator import reformulate_query - - # perform operation - result = reformulate_query("How do I use EPS?") - - # assertions - # Function should return a string (either reformulated or fallback to original) - assert isinstance(result, str) - assert len(result) > 0 - assert result == "foo" - mock_load_prompt.assert_called_once_with("test-prompt", "DRAFT") - mock_invoke_model.assert_called_once_with( - prompt="Test reformat. How do I use EPS?", model_id="test-model", client=ANY, inference_config={} - ) - - -@patch("app.services.prompt_loader.load_prompt") -def test_reformulate_query_prompt_load_error(mock_load_prompt: Mock, mock_env: Mock): - # set up mocks - mock_load_prompt.side_effect = Exception("Prompt not found") - - # delete and import module to test - if "app.services.query_reformulator" in sys.modules: - del sys.modules["app.services.query_reformulator"] - from app.services.query_reformulator import reformulate_query - - # perform operation - original_query = "How do I use EPS?" - result = reformulate_query(original_query) - - # assertions - assert result == original_query - - -@patch("app.services.prompt_loader.load_prompt") -@patch("app.services.bedrock.invoke_model") -def test_reformulate_query_bedrock_error(mock_invoke_model: Mock, mock_load_prompt: Mock, mock_env: Mock): - """Test query reformulation with Bedrock API error""" - # set up mocks - mock_load_prompt.return_value = "Reformulate this query: {{user_query}}" - mock_invoke_model.side_effect = ClientError({"Error": {"Code": "ThrottlingException"}}, "InvokeModel") - - # delete and import module to test - if "app.services.query_reformulator" in sys.modules: - del sys.modules["app.services.query_reformulator"] - from app.services.query_reformulator import reformulate_query - - # perform operation - result = reformulate_query("original query") - - # assertions - assert result == "original query" - - -@patch("app.services.prompt_loader.load_prompt") -@patch("app.services.bedrock.invoke_model") -def test_reformulate_query_bedrock_invoke_model(mock_invoke_model: Mock, mock_load_prompt: Mock, mock_env: Mock): - """Test query reformulation with successful Bedrock invoke_model call""" - # set up mocks - mock_load_prompt.return_value = {"prompt_text": "Reformulate this query: {{user_query}}"} - mock_invoke_model.return_value = {"content": [{"text": "reformulated query"}]} - - # delete and import module to test - if "app.services.query_reformulator" in sys.modules: - del sys.modules["app.services.query_reformulator"] - from app.services.query_reformulator import reformulate_query - - # perform operation - result = reformulate_query("original query") - - # assertions - assert result == "reformulated query" diff --git a/packages/slackBotFunction/tests/test_slack_events/test_slack_events_citations.py b/packages/slackBotFunction/tests/test_slack_events/test_slack_events_citations.py index f054b483a..b494c0875 100644 --- a/packages/slackBotFunction/tests/test_slack_events/test_slack_events_citations.py +++ b/packages/slackBotFunction/tests/test_slack_events/test_slack_events_citations.py @@ -560,79 +560,6 @@ def test_create_response_body_creates_body_with_markdown_formatting( assert "*Bold*, _italics_, and `code`." in citation_value.get("body") -def test_create_response_body_creates_body_with_lists( - mock_get_parameter: Mock, - mock_env: Mock, -): - """Test regex text processing functionality within process_async_slack_event""" - # delete and import module to test - if "app.slack.slack_events" in sys.modules: - del sys.modules["app.slack.slack_events"] - from app.slack.slack_events import _create_response_body - - dirty_input = "Header text - Standard Dash -No Space Dash • Standard Bullet -NoSpace-NoSpace" - - # perform operation - response = _create_response_body( - citations=[ - { - "source_number": "1", - "title": "Citation Title", - "excerpt": dirty_input, - "relevance_score": "0.95", - } - ], - feedback_data={}, - response_text="This is a response with a citation.[1]", - ) - - # assertions - assert len(response) > 1 - assert response[1]["type"] == "actions" - assert response[1]["block_id"] == "citation_actions" - - citation_element = response[1]["elements"][0] - citation_value = json.loads(citation_element["value"]) - - expected_output = "Header text\n- Standard Dash\n- No Space Dash\n- Standard Bullet\n- NoSpace-NoSpace" - assert expected_output in citation_value.get("body") - - -def test_create_response_body_creates_body_without_encoding_errors( - mock_get_parameter: Mock, - mock_env: Mock, -): - """Test regex text processing functionality within process_async_slack_event""" - # delete and import module to test - if "app.slack.slack_events" in sys.modules: - del sys.modules["app.slack.slack_events"] - from app.slack.slack_events import _create_response_body - - # perform operation - response = _create_response_body( - citations=[ - { - "source_number": "1", - "title": "Citation Title", - "excerpt": "» Tabbing Issue. ⢠Bullet point issue.", - "relevance_score": "0.95", - } - ], - feedback_data={}, - response_text="This is a response with a citation.[1]", - ) - - # assertions - assert len(response) > 1 - assert response[1]["type"] == "actions" - assert response[1]["block_id"] == "citation_actions" - - citation_element = response[1]["elements"][0] - citation_value = json.loads(citation_element["value"]) - - assert "Tabbing Issue.\n- Bullet point issue." in citation_value.get("body") - - @patch("app.services.ai_processor.process_ai_query") def test_create_citation_logs_citations( mock_process_ai_query: Mock, diff --git a/packages/slackBotFunction/tests/test_slack_events/test_slack_events_messages.py b/packages/slackBotFunction/tests/test_slack_events/test_slack_events_messages.py index 60d52c7df..0a142ce54 100644 --- a/packages/slackBotFunction/tests/test_slack_events/test_slack_events_messages.py +++ b/packages/slackBotFunction/tests/test_slack_events/test_slack_events_messages.py @@ -466,7 +466,7 @@ def test_create_response_body_creates_body_with_lists( del sys.modules["app.slack.slack_events"] from app.slack.slack_events import _create_response_body - dirty_input = "Header text - Standard Dash -No Space Dash • Standard Bullet -NoSpace-NoSpace" + dirty_input = "Header text - Standard Dash -No Space Dash • Standard Bullet -DoubleSpace-NoSpace" # perform operation response = _create_response_body( @@ -481,7 +481,7 @@ def test_create_response_body_creates_body_with_lists( response_value = response[0]["text"]["text"] - expected_output = "Header text\n- Standard Dash\n- No Space Dash\n- Standard Bullet\n- NoSpace-NoSpace" + expected_output = "Header text - Standard Dash -No Space Dash - Standard Bullet -DoubleSpace-NoSpace" assert expected_output in response_value @@ -508,4 +508,361 @@ def test_create_response_body_creates_body_without_encoding_errors( response_value = response[0]["text"]["text"] - assert "Tabbing Issue.\n- Bullet point issue." in response_value + assert "Tabbing Issue. - Bullet point issue." in response_value + + +# ================================================================ +# Tests for _create_citation +# ================================================================ + + +def test_create_citation_high_relevance_score(mock_get_parameter: Mock, mock_env: Mock): + """Test citation creation with high relevance score""" + if "app.slack.slack_events" in sys.modules: + del sys.modules["app.slack.slack_events"] + from app.slack.slack_events import _create_citation + + citation = { + "source_number": "1", + "title": "Test Document", + "excerpt": "This is a test excerpt", + "relevance_score": "0.95", + } + feedback_data = {"ck": "conv-123", "ch": "C789"} + response_text = "Response with [cit_1] citation" + + result = _create_citation(citation, feedback_data, response_text) + + assert len(result["action_buttons"]) == 1 + button = result["action_buttons"][0] + assert button["type"] == "button" + assert button["text"]["text"] == "[1] Test Document" + assert button["action_id"] == "cite_1" + assert result["response_text"] == "Response with [1] citation" + + +def test_create_citation_low_relevance_score(mock_get_parameter: Mock, mock_env: Mock): + """Test citation is skipped when relevance score is low""" + if "app.slack.slack_events" in sys.modules: + del sys.modules["app.slack.slack_events"] + from app.slack.slack_events import _create_citation + + citation = { + "source_number": "2", + "title": "Low Relevance Doc", + "excerpt": "This is low relevance", + "relevance_score": "0.5", # Below 0.6 threshold + } + feedback_data = {"ck": "conv-123"} + response_text = "Response with [cit_2]" + + result = _create_citation(citation, feedback_data, response_text) + + assert len(result["action_buttons"]) == 0 + assert result["response_text"] == "Response with [cit_2]" + + +def test_create_citation_missing_excerpt(mock_get_parameter: Mock, mock_env: Mock): + """Test citation with missing excerpt uses default message""" + if "app.slack.slack_events" in sys.modules: + del sys.modules["app.slack.slack_events"] + from app.slack.slack_events import _create_citation + + citation = { + "source_number": "3", + "title": "Document Without Excerpt", + "relevance_score": "0.9", + } + feedback_data = {"ck": "conv-123"} + response_text = "Response text" + + result = _create_citation(citation, feedback_data, response_text) + + assert len(result["action_buttons"]) == 1 + import json + + button_data = json.loads(result["action_buttons"][0]["value"]) + assert button_data["body"] == "No document excerpt available." + + +def test_create_citation_missing_title_uses_filename(mock_get_parameter: Mock, mock_env: Mock): + """Test citation uses filename when title is missing""" + if "app.slack.slack_events" in sys.modules: + del sys.modules["app.slack.slack_events"] + from app.slack.slack_events import _create_citation + + citation = { + "source_number": "4", + "filename": "document.pdf", + "excerpt": "Some content", + "relevance_score": "0.85", + } + feedback_data = {"ck": "conv-123"} + response_text = "Response text" + + result = _create_citation(citation, feedback_data, response_text) + + assert len(result["action_buttons"]) == 1 + assert result["action_buttons"][0]["text"]["text"] == "[4] document.pdf" + + +def test_create_citation_fallback_source_when_missing(mock_get_parameter: Mock, mock_env: Mock): + """Test citation uses 'Source' when both title and filename are missing""" + if "app.slack.slack_events" in sys.modules: + del sys.modules["app.slack.slack_events"] + from app.slack.slack_events import _create_citation + + citation = { + "source_number": "5", + "excerpt": "Some content", + "relevance_score": "0.9", + } + feedback_data = {"ck": "conv-123"} + response_text = "Response text" + + result = _create_citation(citation, feedback_data, response_text) + + assert len(result["action_buttons"]) == 1 + assert result["action_buttons"][0]["text"]["text"] == "[5] Source" + + +def test_create_citation_button_text_truncation(mock_get_parameter: Mock, mock_env: Mock): + """Test citation button text is truncated when exceeds 75 characters""" + if "app.slack.slack_events" in sys.modules: + del sys.modules["app.slack.slack_events"] + from app.slack.slack_events import _create_citation + + long_title = "A" * 80 # Title longer than 75 chars + citation = { + "source_number": "6", + "title": long_title, + "excerpt": "Some content", + "relevance_score": "0.9", + } + feedback_data = {"ck": "conv-123"} + response_text = "Response text" + + result = _create_citation(citation, feedback_data, response_text) + + button_text = result["action_buttons"][0]["text"]["text"] + assert len(button_text) <= 77 # "[X] " + 70 chars + "..." + assert button_text.endswith("...") + + +def test_create_citation_removes_newlines_from_source_number(mock_get_parameter: Mock, mock_env: Mock): + """Test citation removes newlines from source number""" + if "app.slack.slack_events" in sys.modules: + del sys.modules["app.slack.slack_events"] + from app.slack.slack_events import _create_citation + + citation = { + "source_number": "7\n\n", + "title": "Test", + "excerpt": "Content", + "relevance_score": "0.9", + } + feedback_data = {"ck": "conv-123"} + response_text = "Response with [cit_7]" + + result = _create_citation(citation, feedback_data, response_text) + + assert result["response_text"] == "Response with [7]" + assert result["action_buttons"][0]["action_id"] == "cite_7" + + +def test_create_citation_zero_relevance_score(mock_get_parameter: Mock, mock_env: Mock): + """Test citation with zero relevance score is skipped""" + if "app.slack.slack_events" in sys.modules: + del sys.modules["app.slack.slack_events"] + from app.slack.slack_events import _create_citation + + citation = { + "source_number": "8", + "title": "No Relevance", + "excerpt": "Content", + "relevance_score": "0", + } + feedback_data = {"ck": "conv-123"} + response_text = "Response text" + + result = _create_citation(citation, feedback_data, response_text) + + assert len(result["action_buttons"]) == 0 + + +# ================================================================ +# Tests for convert_markdown_to_slack +# ================================================================ + + +def test_convert_markdown_to_slack_bold_formatting(mock_get_parameter: Mock, mock_env: Mock): + """Test conversion of markdown bold to Slack formatting""" + if "app.slack.slack_events" in sys.modules: + del sys.modules["app.slack.slack_events"] + from app.slack.slack_events import convert_markdown_to_slack + + markdown_text = "This is **bold text** in markdown" + result = convert_markdown_to_slack(markdown_text) + + assert "*bold text*" in result + + +def test_convert_markdown_to_slack_italic_formatting(mock_get_parameter: Mock, mock_env: Mock): + """Test conversion of markdown italics to Slack formatting""" + if "app.slack.slack_events" in sys.modules: + del sys.modules["app.slack.slack_events"] + from app.slack.slack_events import convert_markdown_to_slack + + markdown_text = "This is __italic text__ in markdown" + result = convert_markdown_to_slack(markdown_text) + + assert "_italic text_" in result + + +def test_convert_markdown_to_slack_links(mock_get_parameter: Mock, mock_env: Mock): + """Test conversion of markdown links to Slack formatting""" + if "app.slack.slack_events" in sys.modules: + del sys.modules["app.slack.slack_events"] + from app.slack.slack_events import convert_markdown_to_slack + + markdown_text = "Check out [this link](https://example.com)" + result = convert_markdown_to_slack(markdown_text) + + assert "" in result + + +def test_convert_markdown_to_slack_encoding_issues_arrow(mock_get_parameter: Mock, mock_env: Mock): + """Test conversion removes arrow encoding issues""" + if "app.slack.slack_events" in sys.modules: + del sys.modules["app.slack.slack_events"] + from app.slack.slack_events import convert_markdown_to_slack + + text_with_encoding = "» Tab issue" + result = convert_markdown_to_slack(text_with_encoding) + + assert "»" not in result + assert "Tab issue" in result + + +def test_convert_markdown_to_slack_encoding_issues_bullet(mock_get_parameter: Mock, mock_env: Mock): + """Test conversion fixes bullet point encoding issues""" + if "app.slack.slack_events" in sys.modules: + del sys.modules["app.slack.slack_events"] + from app.slack.slack_events import convert_markdown_to_slack + + text_with_encoding = "⢠Bullet point" + result = convert_markdown_to_slack(text_with_encoding) + + assert "â¢" not in result + assert "- Bullet point" in result + + +def test_convert_markdown_to_slack_empty_string(mock_get_parameter: Mock, mock_env: Mock): + """Test conversion of empty string""" + if "app.slack.slack_events" in sys.modules: + del sys.modules["app.slack.slack_events"] + from app.slack.slack_events import convert_markdown_to_slack + + result = convert_markdown_to_slack("") + + assert result == "" + + +def test_convert_markdown_to_slack_none_string(mock_get_parameter: Mock, mock_env: Mock): + """Test conversion of None string""" + if "app.slack.slack_events" in sys.modules: + del sys.modules["app.slack.slack_events"] + from app.slack.slack_events import convert_markdown_to_slack + + result = convert_markdown_to_slack(None) + + assert result == "" + + +def test_convert_markdown_to_slack_combined_formatting(mock_get_parameter: Mock, mock_env: Mock): + """Test conversion with multiple formatting types""" + if "app.slack.slack_events" in sys.modules: + del sys.modules["app.slack.slack_events"] + from app.slack.slack_events import convert_markdown_to_slack + + text = "Check **bold** and __italic__ with [link](https://test.com)" + result = convert_markdown_to_slack(text) + + assert "*bold*" in result + assert "_italic_" in result + assert "" in result + + +def test_convert_markdown_to_slack_link_with_newlines_no_space(mock_get_parameter: Mock, mock_env: Mock): + """Test link conversion removes newlines from link text""" + if "app.slack.slack_events" in sys.modules: + del sys.modules["app.slack.slack_events"] + from app.slack.slack_events import convert_markdown_to_slack + + text = "Check [link\ntext](https://example.com)" + result = convert_markdown_to_slack(text) + + assert "" in result + + +def test_convert_markdown_to_slack_link_with_newlines_with_space(mock_get_parameter: Mock, mock_env: Mock): + """Test link conversion removes newlines from link text""" + if "app.slack.slack_events" in sys.modules: + del sys.modules["app.slack.slack_events"] + from app.slack.slack_events import convert_markdown_to_slack + + text = "Check [link \n text](https://example.com)" + result = convert_markdown_to_slack(text) + + assert "" in result + + +def test_convert_markdown_to_slack_link_with_newlines_with_dash(mock_get_parameter: Mock, mock_env: Mock): + """Test link conversion removes newlines from link text""" + if "app.slack.slack_events" in sys.modules: + del sys.modules["app.slack.slack_events"] + from app.slack.slack_events import convert_markdown_to_slack + + text = "Check [link-text](https://example.com)" + result = convert_markdown_to_slack(text) + + assert "" in result + + +def test_convert_markdown_to_slack_link_with_newlines_with_dash_and_space(mock_get_parameter: Mock, mock_env: Mock): + """Test link conversion removes newlines from link text""" + if "app.slack.slack_events" in sys.modules: + del sys.modules["app.slack.slack_events"] + from app.slack.slack_events import convert_markdown_to_slack + + text = "Check [link - text](https://example.com)" + result = convert_markdown_to_slack(text) + + assert "" in result + + +def test_convert_markdown_to_slack_whitespace_stripped(mock_get_parameter: Mock, mock_env: Mock): + """Test conversion strips leading/trailing whitespace""" + if "app.slack.slack_events" in sys.modules: + del sys.modules["app.slack.slack_events"] + from app.slack.slack_events import convert_markdown_to_slack + + text = " Some text with spaces " + result = convert_markdown_to_slack(text) + + assert result == "Some text with spaces" + + +def test_convert_markdown_to_slack_multiple_encoding_issues(mock_get_parameter: Mock, mock_env: Mock): + """Test conversion handles multiple encoding issues""" + if "app.slack.slack_events" in sys.modules: + del sys.modules["app.slack.slack_events"] + from app.slack.slack_events import convert_markdown_to_slack + + text = "» Tab issue. ⢠Bullet point ⢠another bullet" + result = convert_markdown_to_slack(text) + + assert "»" not in result + assert "â¢" not in result + assert "- Bullet point" in result + assert "- another bullet" in result diff --git a/scripts/run_sync.sh b/scripts/run_sync.sh index a3e1e6b12..a0dc4b136 100755 --- a/scripts/run_sync.sh +++ b/scripts/run_sync.sh @@ -48,6 +48,10 @@ SLACK_SIGNING_SECRET=$(echo "$CF_LONDON_EXPORTS" | \ -r '.Exports[] | select(.Name == $EXPORT_NAME) | .Value') LOG_RETENTION_IN_DAYS=30 LOG_LEVEL=debug +FORWARD_CSOC_LOGS=false +RUN_REGRESSION_TESTS=false + + # export all the vars so they can be picked up by external programs export STACK_NAME @@ -57,24 +61,30 @@ export SLACK_BOT_TOKEN export SLACK_SIGNING_SECRET export LOG_RETENTION_IN_DAYS export LOG_LEVEL - +export FORWARD_CSOC_LOGS +export RUN_REGRESSION_TESTS echo "Generating config for ${EPSAM_CONFIG}" "$FIX_SCRIPT" "$EPSAM_CONFIG" echo "Installing dependencies locally" mkdir -p .dependencies -poetry export --without-hashes --format=requirements.txt --with slackBotFunction > .dependencies/requirements_slackBotFunction -poetry export --without-hashes --format=requirements.txt --with syncKnowledgeBaseFunction > .dependencies/requirements_syncKnowledgeBaseFunction -poetry export --without-hashes --format=requirements.txt --with preprocessingFunction > .dependencies/requirements_preprocessingFunction poetry show --only=slackBotFunction | grep -E "^[a-zA-Z]" | awk '{print $1"=="$2}' > .dependencies/requirements_slackBotFunction poetry show --only=syncKnowledgeBaseFunction | grep -E "^[a-zA-Z]" | awk '{print $1"=="$2}' > .dependencies/requirements_syncKnowledgeBaseFunction +poetry show --only=notifyS3UploadFunction | grep -E "^[a-zA-Z]" | awk '{print $1"=="$2}' > .dependencies/requirements_notifyS3UploadFunction +poetry show --only=preprocessingFunction | grep -E "^[a-zA-Z]" | awk '{print $1"=="$2}' > .dependencies/requirements_preprocessingFunction +poetry show --only=bedrockLoggingConfigFunction | grep -E "^[a-zA-Z]" | awk '{print $1"=="$2}' > .dependencies/requirements_bedrockLoggingConfigFunction + pip3 install -r .dependencies/requirements_slackBotFunction -t .dependencies/slackBotFunction/python pip3 install -r .dependencies/requirements_syncKnowledgeBaseFunction -t .dependencies/syncKnowledgeBaseFunction/python pip3 install -r .dependencies/requirements_notifyS3UploadFunction -t .dependencies/notifyS3UploadFunction/python pip3 install -r .dependencies/requirements_preprocessingFunction -t .dependencies/preprocessingFunction/python +pip3 install -r .dependencies/requirements_bedrockLoggingConfigFunction -t .dependencies/bedrockLoggingConfigFunction/python + rm -rf .dependencies/preprocessingFunction/python/magika* .dependencies/preprocessingFunction/python/onnxruntime* + cp packages/preprocessingFunction/magika_shim.py .dependencies/preprocessingFunction/python/magika.py + find .dependencies/preprocessingFunction/python -type d -name "tests" -exec rm -rf {} + 2>/dev/null || true find .dependencies/preprocessingFunction/python -type d -name "test" -exec rm -rf {} + 2>/dev/null || true find .dependencies/preprocessingFunction/python -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true