diff --git a/README.md b/README.md index c6290698c..9c5760c19 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ [English](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/README.md) | [日本語](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_ja-JP.md) | [한국어](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_ko-KR.md) | [中文](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_zh-CN.md) | [Français](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_fr-FR.md) | [Deutsch](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_de-DE.md) | [Español](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_es-ES.md) | [Italian](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_it-IT.md) | [Norsk](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_nb-NO.md) | [ไทย](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_th-TH.md) | [Bahasa Indonesia](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_id-ID.md) | [Bahasa Melayu](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_ms-MY.md) | [Tiếng Việt](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_vi-VN.md) | [Polski](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_pl-PL.md) -> [!Warning] +> [!Warning] +> > **V2 released. To update, please carefully review the [migration guide](./docs/migration/V1_TO_V2.md).** Without any care, **BOTS FROM V1 WILL BECOME UNUSABLE.** A multilingual chatbot using LLM models provided by [Amazon Bedrock](https://aws.amazon.com/bedrock/) for generative AI. @@ -215,6 +216,109 @@ BedrockChatStack.BackendApiBackendApiUrlXXXXX = https://xxxxx.execute-api.ap-nor BedrockChatStack.FrontendURL = https://xxxxx.cloudfront.net ``` +### Defining Parameters + +You can define parameters for your deployment in two ways: using `cdk.json` or using the type-safe `parameter.ts` file. + +#### Using cdk.json (Traditional Method) + +The traditional way to configure parameters is by editing the `cdk.json` file. This approach is simple but lacks type checking: + +```json +{ + "app": "npx ts-node --prefer-ts-exts bin/bedrock-chat.ts", + "context": { + "bedrockRegion": "us-east-1", + "allowedIpV4AddressRanges": ["0.0.0.0/1", "128.0.0.0/1"], + "enableMistral": false, + "selfSignUpEnabled": true + } +} +``` + +#### Using parameter.ts (Recommended Type-Safe Method) + +For better type safety and developer experience, you can use the `parameter.ts` file to define your parameters: + +```typescript +// Define parameters for the default environment +bedrockChatParams.set("default", { + bedrockRegion: "us-east-1", + allowedIpV4AddressRanges: ["192.168.0.0/16"], + enableMistral: false, + selfSignUpEnabled: true, +}); + +// Define parameters for additional environments +bedrockChatParams.set("dev", { + bedrockRegion: "us-west-2", + allowedIpV4AddressRanges: ["10.0.0.0/8"], + enableRagReplicas: false, // Cost-saving for dev environment +}); + +bedrockChatParams.set("prod", { + bedrockRegion: "us-east-1", + allowedIpV4AddressRanges: ["172.16.0.0/12"], + enableLambdaSnapStart: true, + enableRagReplicas: true, // Enhanced availability for production +}); +``` + +> [!Note] +> Existing users can continue using `cdk.json` without any changes. The `parameter.ts` approach is recommended for new deployments or when you need to manage multiple environments. + +### Deploying Multiple Environments + +You can deploy multiple environments from the same codebase using the `parameter.ts` file and the `-c envName` option. + +#### Prerequisites + +1. Define your environments in `parameter.ts` as shown above +2. Each environment will have its own set of resources with environment-specific prefixes + +#### Deployment Commands + +To deploy a specific environment: + +```bash +# Deploy the dev environment +npx cdk deploy --all -c envName=dev + +# Deploy the prod environment +npx cdk deploy --all -c envName=prod +``` + +If no environment is specified, the "default" environment is used: + +```bash +# Deploy the default environment +npx cdk deploy --all +``` + +#### Important Notes + +1. **Stack Naming**: + + - The main stacks for each environment will be prefixed with the environment name (e.g., `dev-BedrockChatStack`, `prod-BedrockChatStack`) + - However, custom bot stacks (`BrChatKbStack*`) and API publishing stacks (`ApiPublishmentStack*`) do not receive environment prefixes as they are created dynamically at runtime + +2. **Resource Naming**: + + - Only some resources receive environment prefixes in their names (e.g., `dev_ddb_export` table, `dev-FrontendWebAcl`) + - Most resources maintain their original names but are isolated by being in different stacks + +3. **Environment Identification**: + + - All resources are tagged with a `CDKEnvironment` tag containing the environment name + - You can use this tag to identify which environment a resource belongs to + - Example: `CDKEnvironment: dev` or `CDKEnvironment: prod` + +4. **Default Environment Override**: If you define a "default" environment in `parameter.ts`, it will override the settings in `cdk.json`. To continue using `cdk.json`, don't define a "default" environment in `parameter.ts`. + +5. **Environment Requirements**: To create environments other than "default", you must use `parameter.ts`. The `-c envName` option alone is not sufficient without corresponding environment definitions. + +6. **Resource Isolation**: Each environment creates its own set of resources, allowing you to have development, testing, and production environments in the same AWS account without conflicts. + ## Others ### Configure Mistral models support diff --git a/backend/app/usecases/publication.py b/backend/app/usecases/publication.py index 2370b044b..a9c54b6c0 100644 --- a/backend/app/usecases/publication.py +++ b/backend/app/usecases/publication.py @@ -81,7 +81,6 @@ def create_bot_publication(user: User, bot_id: str, bot_publish_input: BotPublis environment_variables["PUBLISHED_API_ID"] = published_api_id # Set environment variables. - # NOTE: default values are set in `cdk/lib/constructs/api-publish-codebuild.ts` if bot_publish_input.throttle.rate_limit is not None: environment_variables["PUBLISHED_API_THROTTLE_RATE_LIMIT"] = str( bot_publish_input.throttle.rate_limit diff --git a/backend/app/utils.py b/backend/app/utils.py index 78531d453..5f0be0abc 100644 --- a/backend/app/utils.py +++ b/backend/app/utils.py @@ -12,9 +12,11 @@ REGION = os.environ.get("REGION", "us-east-1") BEDROCK_REGION = os.environ.get("BEDROCK_REGION", "us-east-1") +ENV_NAME = os.environ.get("ENV_NAME", "default") logger.debug(f"REGION: {REGION}") logger.debug(f"BEDROCK_REGION: {BEDROCK_REGION}") +logger.debug(f"ENV_NAME: {ENV_NAME}") PUBLISH_API_CODEBUILD_PROJECT_NAME = os.environ.get( "PUBLISH_API_CODEBUILD_PROJECT_NAME", "" @@ -216,7 +218,9 @@ def store_api_key_to_secret_manager( # Create new secret if it doesn't exist logger.info(f"Creating new secret: {secret_name}") response = secrets_client.create_secret( - Name=secret_name, SecretString=secret_value + Name=secret_name, + SecretString=secret_value, + Tags=[{"Key": "CDKEnvironment", "Value": ENV_NAME}], ) logger.info(f"Created new secret: {secret_name}") return response["ARN"] diff --git a/cdk/bin/api-publish.ts b/cdk/bin/api-publish.ts index 753376914..d86051e4d 100644 --- a/cdk/bin/api-publish.ts +++ b/cdk/bin/api-publish.ts @@ -3,103 +3,71 @@ import "source-map-support/register"; import * as cdk from "aws-cdk-lib"; import { ApiPublishmentStack } from "../lib/api-publishment-stack"; import * as apigateway from "aws-cdk-lib/aws-apigateway"; +import { resolveApiPublishParameters } from "../lib/utils/parameter-models"; const app = new cdk.App(); -const BEDROCK_REGION = app.node.tryGetContext("bedrockRegion"); +// Get parameters specific to API publishing +const params = resolveApiPublishParameters(); +const sepHyphen = params.envPrefix ? "-" : ""; -// Usage plan for the published API -const PUBLISHED_API_THROTTLE_RATE_LIMIT: number | undefined = - app.node.tryGetContext("publishedApiThrottleRateLimit") - ? Number(app.node.tryGetContext("publishedApiThrottleRateLimit")) - : undefined; -const PUBLISHED_API_THROTTLE_BURST_LIMIT: number | undefined = - app.node.tryGetContext("publishedApiThrottleBurstLimit") - ? Number(app.node.tryGetContext("publishedApiThrottleBurstLimit")) - : undefined; -const PUBLISHED_API_QUOTA_LIMIT: number | undefined = app.node.tryGetContext( - "publishedApiQuotaLimit" -) - ? Number(app.node.tryGetContext("publishedApiQuotaLimit")) - : undefined; -const PUBLISHED_API_QUOTA_PERIOD: "DAY" | "WEEK" | "MONTH" | undefined = - app.node.tryGetContext("publishedApiQuotaPeriod") - ? app.node.tryGetContext("publishedApiQuotaPeriod") - : undefined; -const PUBLISHED_API_DEPLOYMENT_STAGE = app.node.tryGetContext( - "publishedApiDeploymentStage" -); -const PUBLISHED_API_ID: string = app.node.tryGetContext("publishedApiId"); -const PUBLISHED_API_ALLOWED_ORIGINS_STRING: string = app.node.tryGetContext( - "publishedApiAllowedOrigins" -); -const PUBLISHED_API_ALLOWED_ORIGINS: string[] = JSON.parse( - PUBLISHED_API_ALLOWED_ORIGINS_STRING || '["*"]' +// Parse allowed origins +const publishedApiAllowedOrigins = JSON.parse( + params.publishedApiAllowedOrigins || '["*"]' ); -console.log( - `PUBLISHED_API_THROTTLE_RATE_LIMIT: ${PUBLISHED_API_THROTTLE_RATE_LIMIT}` -); -console.log( - `PUBLISHED_API_THROTTLE_BURST_LIMIT: ${PUBLISHED_API_THROTTLE_BURST_LIMIT}` -); -console.log(`PUBLISHED_API_QUOTA_LIMIT: ${PUBLISHED_API_QUOTA_LIMIT}`); -console.log(`PUBLISHED_API_QUOTA_PERIOD: ${PUBLISHED_API_QUOTA_PERIOD}`); -console.log( - `PUBLISHED_API_DEPLOYMENT_STAGE: ${PUBLISHED_API_DEPLOYMENT_STAGE}` -); -console.log(`PUBLISHED_API_ID: ${PUBLISHED_API_ID}`); -console.log(`PUBLISHED_API_ALLOWED_ORIGINS: ${PUBLISHED_API_ALLOWED_ORIGINS}`); +// Log all parameters at once for debugging +console.log("API Publish Parameters:", JSON.stringify(params)); -const webAclArn = cdk.Fn.importValue("PublishedApiWebAclArn"); +const webAclArn = cdk.Fn.importValue( + `${params.envPrefix}${sepHyphen}PublishedApiWebAclArn` +); const conversationTableName = cdk.Fn.importValue( - "BedrockClaudeChatConversationTableName" + `${params.envPrefix}${sepHyphen}BedrockClaudeChatConversationTableName` ); const tableAccessRoleArn = cdk.Fn.importValue( - "BedrockClaudeChatTableAccessRoleArn" + `${params.envPrefix}${sepHyphen}BedrockClaudeChatTableAccessRoleArn` ); const largeMessageBucketName = cdk.Fn.importValue( - "BedrockClaudeChatLargeMessageBucketName" + `${params.envPrefix}${sepHyphen}BedrockClaudeChatLargeMessageBucketName` ); // NOTE: DO NOT change the stack id naming rule. -const publishedApi = new ApiPublishmentStack( - app, - `ApiPublishmentStack${PUBLISHED_API_ID}`, - { - env: { - region: process.env.CDK_DEFAULT_REGION, - }, - bedrockRegion: BEDROCK_REGION, - conversationTableName: conversationTableName, - tableAccessRoleArn: tableAccessRoleArn, - webAclArn: webAclArn, - largeMessageBucketName: largeMessageBucketName, - usagePlan: { - throttle: - PUBLISHED_API_THROTTLE_RATE_LIMIT !== undefined && - PUBLISHED_API_THROTTLE_BURST_LIMIT !== undefined - ? { - rateLimit: PUBLISHED_API_THROTTLE_RATE_LIMIT, - burstLimit: PUBLISHED_API_THROTTLE_BURST_LIMIT, - } - : undefined, - quota: - PUBLISHED_API_QUOTA_LIMIT !== undefined && - PUBLISHED_API_QUOTA_PERIOD !== undefined - ? { - limit: PUBLISHED_API_QUOTA_LIMIT, - period: apigateway.Period[PUBLISHED_API_QUOTA_PERIOD], - } - : undefined, - }, - deploymentStage: PUBLISHED_API_DEPLOYMENT_STAGE, - corsOptions: { - allowOrigins: PUBLISHED_API_ALLOWED_ORIGINS, - allowMethods: apigateway.Cors.ALL_METHODS, - allowHeaders: apigateway.Cors.DEFAULT_HEADERS, - allowCredentials: true, - }, - } -); +new ApiPublishmentStack(app, `ApiPublishmentStack${params.publishedApiId}`, { + env: { + region: process.env.CDK_DEFAULT_REGION, + }, + bedrockRegion: params.bedrockRegion, + conversationTableName: conversationTableName, + tableAccessRoleArn: tableAccessRoleArn, + webAclArn: webAclArn, + largeMessageBucketName: largeMessageBucketName, + usagePlan: { + throttle: + params.publishedApiThrottleRateLimit !== undefined && + params.publishedApiThrottleBurstLimit !== undefined + ? { + rateLimit: params.publishedApiThrottleRateLimit, + burstLimit: params.publishedApiThrottleBurstLimit, + } + : undefined, + quota: + params.publishedApiQuotaLimit !== undefined && + params.publishedApiQuotaPeriod !== undefined + ? { + limit: params.publishedApiQuotaLimit, + period: apigateway.Period[params.publishedApiQuotaPeriod], + } + : undefined, + }, + deploymentStage: params.publishedApiDeploymentStage, + corsOptions: { + allowOrigins: publishedApiAllowedOrigins, + allowMethods: apigateway.Cors.ALL_METHODS, + allowHeaders: apigateway.Cors.DEFAULT_HEADERS, + allowCredentials: true, + }, +}); + +cdk.Tags.of(app).add("CDKEnvironment", params.envName); diff --git a/cdk/bin/bedrock-chat.ts b/cdk/bin/bedrock-chat.ts index f3809caf1..c5ad70183 100644 --- a/cdk/bin/bedrock-chat.ts +++ b/cdk/bin/bedrock-chat.ts @@ -4,58 +4,45 @@ import * as cdk from "aws-cdk-lib"; import { BedrockChatStack } from "../lib/bedrock-chat-stack"; import { BedrockRegionResourcesStack } from "../lib/bedrock-region-resources"; import { FrontendWafStack } from "../lib/frontend-waf-stack"; -import { TIdentityProvider } from "../lib/utils/identity-provider"; import { LogRetentionChecker } from "../rules/log-retention-checker"; +import { getBedrockChatParameters } from "../lib/utils/parameter-models"; +import { bedrockChatParams } from "../parameter"; const app = new cdk.App(); -const BEDROCK_REGION = app.node.tryGetContext("bedrockRegion"); - -// Allowed IP address ranges for this app itself -const ALLOWED_IP_V4_ADDRESS_RANGES: string[] = app.node.tryGetContext( - "allowedIpV4AddressRanges" -); -const ALLOWED_IP_V6_ADDRESS_RANGES: string[] = app.node.tryGetContext( - "allowedIpV6AddressRanges" +// Specify env name by "envName" context variable +// ex) cdk synth -c envName=foo +// If you don't specify the envName context variable, "default" is used. +const params = getBedrockChatParameters( + app, + app.node.tryGetContext("envName"), + bedrockChatParams ); -// Allowed IP address ranges for the published API -const PUBLISHED_API_ALLOWED_IP_V4_ADDRESS_RANGES: string[] = - app.node.tryGetContext("publishedApiAllowedIpV4AddressRanges"); -const PUBLISHED_API_ALLOWED_IP_V6_ADDRESS_RANGES: string[] = - app.node.tryGetContext("publishedApiAllowedIpV6AddressRanges"); -const ALLOWED_SIGN_UP_EMAIL_DOMAINS: string[] = app.node.tryGetContext( - "allowedSignUpEmailDomains" -); -const IDENTITY_PROVIDERS: TIdentityProvider[] = - app.node.tryGetContext("identityProviders"); -const USER_POOL_DOMAIN_PREFIX: string = app.node.tryGetContext( - "userPoolDomainPrefix" -); -const AUTO_JOIN_USER_GROUPS: string[] = - app.node.tryGetContext("autoJoinUserGroups"); +// // Another way, you can iterate over params map to declare multiple environments in single App. +// for (const [k] of bedrockChatParams) { +// const params = getBedrockChatParameters(app, k, bedrockChatParams); +// // Include stack declaration this scope... +// } -const ENABLE_MISTRAL: boolean = app.node.tryGetContext("enableMistral"); -const SELF_SIGN_UP_ENABLED: boolean = - app.node.tryGetContext("selfSignUpEnabled"); -const USE_STAND_BY_REPLICAS: boolean = - app.node.tryGetContext("enableRagReplicas"); -const ENABLE_BEDROCK_CROSS_REGION_INFERENCE: boolean = app.node.tryGetContext( - "enableBedrockCrossRegionInference" -); -const ENABLE_LAMBDA_SNAPSTART: boolean = app.node.tryGetContext("enableLambdaSnapStart"); +const sepHyphen = params.envPrefix ? "-" : ""; // WAF for frontend // 2023/9: Currently, the WAF for CloudFront needs to be created in the North America region (us-east-1), so the stacks are separated // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-wafv2-webacl.html -const waf = new FrontendWafStack(app, `FrontendWafStack`, { - env: { - // account: process.env.CDK_DEFAULT_ACCOUNT, - region: "us-east-1", - }, - allowedIpV4AddressRanges: ALLOWED_IP_V4_ADDRESS_RANGES, - allowedIpV6AddressRanges: ALLOWED_IP_V6_ADDRESS_RANGES, -}); +const waf = new FrontendWafStack( + app, + `${params.envPrefix}${sepHyphen}FrontendWafStack`, + { + env: { + // account: process.env.CDK_DEFAULT_ACCOUNT, + region: "us-east-1", + }, + envPrefix: params.envPrefix, + allowedIpV4AddressRanges: params.allowedIpV4AddressRanges, + allowedIpV6AddressRanges: params.allowedIpV6AddressRanges, + } +); // The region of the LLM model called by the converse API and the region of Guardrail must be in the same region. // CustomBotStack contains Knowledge Bases is deployed in the same region as the LLM model, and source bucket must be in the same region as Knowledge Bases. @@ -63,46 +50,50 @@ const waf = new FrontendWafStack(app, `FrontendWafStack`, { // Ref: https://docs.aws.amazon.com/bedrock/latest/userguide/s3-data-source-connector.html const bedrockRegionResources = new BedrockRegionResourcesStack( app, - `BedrockRegionResourcesStack`, + `${params.envPrefix}${sepHyphen}BedrockRegionResourcesStack`, { env: { // account: process.env.CDK_DEFAULT_ACCOUNT, - region: BEDROCK_REGION, + region: params.bedrockRegion, }, crossRegionReferences: true, } ); -const ALTERNATE_DOMAIN_NAME: string = app.node.tryGetContext("alternateDomainName"); -const HOSTED_ZONE_ID: string = app.node.tryGetContext("hostedZoneId"); - -const chat = new BedrockChatStack(app, `BedrockChatStack`, { - env: { - // account: process.env.CDK_DEFAULT_ACCOUNT, - region: process.env.CDK_DEFAULT_REGION, - }, - crossRegionReferences: true, - bedrockRegion: BEDROCK_REGION, - webAclId: waf.webAclArn.value, - enableIpV6: waf.ipV6Enabled, - identityProviders: IDENTITY_PROVIDERS, - userPoolDomainPrefix: USER_POOL_DOMAIN_PREFIX, - publishedApiAllowedIpV4AddressRanges: - PUBLISHED_API_ALLOWED_IP_V4_ADDRESS_RANGES, - publishedApiAllowedIpV6AddressRanges: - PUBLISHED_API_ALLOWED_IP_V6_ADDRESS_RANGES, - allowedSignUpEmailDomains: ALLOWED_SIGN_UP_EMAIL_DOMAINS, - autoJoinUserGroups: AUTO_JOIN_USER_GROUPS, - enableMistral: ENABLE_MISTRAL, - selfSignUpEnabled: SELF_SIGN_UP_ENABLED, - documentBucket: bedrockRegionResources.documentBucket, - useStandbyReplicas: USE_STAND_BY_REPLICAS, - enableBedrockCrossRegionInference: ENABLE_BEDROCK_CROSS_REGION_INFERENCE, - enableLambdaSnapStart: ENABLE_LAMBDA_SNAPSTART, - alternateDomainName: ALTERNATE_DOMAIN_NAME, - hostedZoneId: HOSTED_ZONE_ID, -}); +const chat = new BedrockChatStack( + app, + `${params.envPrefix}${sepHyphen}BedrockChatStack`, + { + env: { + // account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION, + }, + envName: params.envName, + envPrefix: params.envPrefix, + crossRegionReferences: true, + bedrockRegion: params.bedrockRegion, + webAclId: waf.webAclArn.value, + enableIpV6: waf.ipV6Enabled, + identityProviders: params.identityProviders, + userPoolDomainPrefix: params.userPoolDomainPrefix, + publishedApiAllowedIpV4AddressRanges: + params.publishedApiAllowedIpV4AddressRanges, + publishedApiAllowedIpV6AddressRanges: + params.publishedApiAllowedIpV6AddressRanges, + allowedSignUpEmailDomains: params.allowedSignUpEmailDomains, + autoJoinUserGroups: params.autoJoinUserGroups, + enableMistral: params.enableMistral, + selfSignUpEnabled: params.selfSignUpEnabled, + documentBucket: bedrockRegionResources.documentBucket, + useStandbyReplicas: params.enableRagReplicas, + enableBedrockCrossRegionInference: params.enableBedrockCrossRegionInference, + enableLambdaSnapStart: params.enableLambdaSnapStart, + alternateDomainName: params.alternateDomainName, + hostedZoneId: params.hostedZoneId, + } +); chat.addDependency(waf); chat.addDependency(bedrockRegionResources); cdk.Aspects.of(chat).add(new LogRetentionChecker()); +cdk.Tags.of(app).add("CDKEnvironment", params.envName); diff --git a/cdk/bin/bedrock-custom-bot.ts b/cdk/bin/bedrock-custom-bot.ts index da745e22b..e8a3e3896 100644 --- a/cdk/bin/bedrock-custom-bot.ts +++ b/cdk/bin/bedrock-custom-bot.ts @@ -9,198 +9,252 @@ import { getCrowlingScope, getCrawlingFilters, } from "../lib/utils/bedrock-knowledge-base-args"; +import { BedrockFoundationModel } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock"; +import { ChunkingStrategy } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock/data-sources/chunking"; import { CrawlingFilters, + CrawlingScope, } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock/data-sources/web-crawler-data-source"; +import { Analyzer } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/opensearch-vectorindex"; +import { resolveBedrockCustomBotParameters } from "../lib/utils/parameter-models"; const app = new cdk.App(); -const BEDROCK_REGION = app.node.tryGetContext("bedrockRegion"); +// Get parameters specific to Bedrock Custom Bot +const params = resolveBedrockCustomBotParameters(); -const PK: string = process.env.PK!; -const SK: string = process.env.SK!; -const BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME: string = - process.env.BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME!; -const KNOWLEDGE: string = process.env.KNOWLEDGE!; -const BEDROCK_KNOWLEDGE_BASE: string = process.env.BEDROCK_KNOWLEDGE_BASE!; -const BEDROCK_GUARDRAILS: string = process.env.BEDROCK_GUARDRAILS!; -const USE_STAND_BY_REPLICAS: string = process.env.USE_STAND_BY_REPLICAS!; +// Parse JSON strings into objects +const knowledgeBaseJson = JSON.parse(params.knowledgeBase); +const knowledgeJson = JSON.parse(params.knowledge); +const guardrailsJson = JSON.parse(params.guardrails); -console.log("PK: ", PK); -console.log("SK: ", SK); +// Define interfaces for typed configuration objects +interface BaseConfig { + envName: string; + envPrefix: string; + bedrockRegion: string; + ownerUserId: string; + botId: string; + documentBucketName: string; + useStandbyReplicas: boolean; +} + +interface KnowledgeConfig { + embeddingsModel: BedrockFoundationModel; + parsingModel: BedrockFoundationModel | undefined; + existKnowledgeBaseId?: string; + existingS3Urls: string[]; + sourceUrls: string[]; + instruction?: string; + analyzer?: Analyzer | undefined; +} + +interface ChunkingConfig { + chunkingStrategy: ChunkingStrategy; + maxTokens?: number; + overlapPercentage?: number; + overlapTokens?: number; + maxParentTokenSize?: number; + maxChildTokenSize?: number; + bufferSize?: number; + breakpointPercentileThreshold?: number; +} + +interface GuardrailConfig { + is_guardrail_enabled?: boolean; + hateThreshold?: number; + insultsThreshold?: number; + sexualThreshold?: number; + violenceThreshold?: number; + misconductThreshold?: number; + groundingThreshold?: number; + relevanceThreshold?: number; + guardrailArn?: number; + guardrailVersion?: number; +} + +interface CrawlingConfig { + crawlingScope?: CrawlingScope | undefined; + crawlingFilters: CrawlingFilters; +} + +// Extract and organize configuration by category +const baseConfig: BaseConfig = { + envName: params.envName, + envPrefix: params.envPrefix, + bedrockRegion: params.bedrockRegion, + ownerUserId: params.pk, + botId: params.sk.split("#")[2], + documentBucketName: params.documentBucketName, + useStandbyReplicas: params.useStandByReplicas === true, +}; + +const knowledgeConfig: KnowledgeConfig = { + embeddingsModel: getEmbeddingModel(knowledgeBaseJson.embeddings_model.S), + parsingModel: getParsingModel(knowledgeBaseJson.parsing_model.S), + existKnowledgeBaseId: knowledgeBaseJson.exist_knowledge_base_id?.S, + existingS3Urls: knowledgeJson.s3_urls.L.map((s3Url: any) => s3Url.S), + sourceUrls: knowledgeJson.source_urls.L.map((sourceUrl: any) => sourceUrl.S), + instruction: knowledgeBaseJson.instruction?.S, + analyzer: knowledgeBaseJson.open_search.M.analyzer.M + ? getAnalyzer(knowledgeBaseJson.open_search.M.analyzer.M) + : undefined, +}; + +// Extract chunking configuration +const chunkingParams = { + maxTokens: knowledgeBaseJson.chunking_configuration.M.max_tokens + ? Number(knowledgeBaseJson.chunking_configuration.M.max_tokens.N) + : undefined, + overlapPercentage: knowledgeBaseJson.chunking_configuration.M + .overlap_percentage + ? Number(knowledgeBaseJson.chunking_configuration.M.overlap_percentage.N) + : undefined, + overlapTokens: knowledgeBaseJson.chunking_configuration.M.overlap_tokens + ? Number(knowledgeBaseJson.chunking_configuration.M.overlap_tokens.N) + : undefined, + maxParentTokenSize: knowledgeBaseJson.chunking_configuration.M + .max_parent_token_size + ? Number(knowledgeBaseJson.chunking_configuration.M.max_parent_token_size.N) + : undefined, + maxChildTokenSize: knowledgeBaseJson.chunking_configuration.M + .max_child_token_size + ? Number(knowledgeBaseJson.chunking_configuration.M.max_child_token_size.N) + : undefined, + bufferSize: knowledgeBaseJson.chunking_configuration.M.buffer_size + ? Number(knowledgeBaseJson.chunking_configuration.M.buffer_size.N) + : undefined, + breakpointPercentileThreshold: knowledgeBaseJson.chunking_configuration.M + .breakpoint_percentile_threshold + ? Number( + knowledgeBaseJson.chunking_configuration.M + .breakpoint_percentile_threshold.N + ) + : undefined, +}; + +const chunkingConfig: ChunkingConfig = { + ...chunkingParams, + chunkingStrategy: getChunkingStrategy( + knowledgeBaseJson.chunking_configuration.M.chunking_strategy.S, + knowledgeBaseJson.embeddings_model.S, + chunkingParams + ), +}; + +const crawlingConfig: CrawlingConfig = { + crawlingScope: getCrowlingScope(knowledgeBaseJson.web_crawling_scope.S), + crawlingFilters: getCrawlingFilters(knowledgeBaseJson.web_crawling_filters.M), +}; + +const guardrailConfig: GuardrailConfig = { + is_guardrail_enabled: guardrailsJson.is_guardrail_enabled + ? Boolean(guardrailsJson.is_guardrail_enabled.BOOL) + : undefined, + hateThreshold: guardrailsJson.hate_threshold + ? Number(guardrailsJson.hate_threshold.N) + : undefined, + insultsThreshold: guardrailsJson.insults_threshold + ? Number(guardrailsJson.insults_threshold.N) + : undefined, + sexualThreshold: guardrailsJson.sexual_threshold + ? Number(guardrailsJson.sexual_threshold.N) + : undefined, + violenceThreshold: guardrailsJson.violence_threshold + ? Number(guardrailsJson.violence_threshold.N) + : undefined, + misconductThreshold: guardrailsJson.misconduct_threshold + ? Number(guardrailsJson.misconduct_threshold.N) + : undefined, + groundingThreshold: guardrailsJson.grounding_threshold + ? Number(guardrailsJson.grounding_threshold.N) + : undefined, + relevanceThreshold: guardrailsJson.relevance_threshold + ? Number(guardrailsJson.relevance_threshold.N) + : undefined, + guardrailArn: guardrailsJson.guardrail_arn + ? Number(guardrailsJson.guardrail_arn.N) + : undefined, + guardrailVersion: guardrailsJson.guardrail_version + ? Number(guardrailsJson.guardrail_version.N) + : undefined, +}; + +// Log organized configurations for debugging +console.log("Base Configuration:", JSON.stringify(baseConfig, null, 2)); console.log( - "BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME: ", - BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME + "Knowledge Configuration:", + JSON.stringify( + { + ...knowledgeConfig, + embeddingsModel: knowledgeConfig.embeddingsModel.toString(), + parsingModel: knowledgeConfig.parsingModel?.toString(), + analyzer: knowledgeConfig.analyzer ? "configured" : "undefined", + }, + null, + 2 + ) ); -console.log("KNOWLEDGE: ", KNOWLEDGE); -console.log("BEDROCK_KNOWLEDGE_BASE: ", BEDROCK_KNOWLEDGE_BASE); -console.log("BEDROCK_GUARDRAILS: ", BEDROCK_GUARDRAILS); -console.log("USE_STAND_BY_REPLICAS: ", USE_STAND_BY_REPLICAS); - -const ownerUserId: string = PK; -const botId: string = SK.split("#")[2]; -const knowledgeBase = JSON.parse(BEDROCK_KNOWLEDGE_BASE); -const knowledge = JSON.parse(KNOWLEDGE); -const guardrails = JSON.parse(BEDROCK_GUARDRAILS); -const existingS3Urls: string[] = knowledge.s3_urls.L.map( - (s3Url: any) => s3Url.S +console.log( + "Chunking Configuration:", + JSON.stringify( + { + ...chunkingConfig, + chunkingStrategy: chunkingConfig.chunkingStrategy.toString(), + }, + null, + 2 + ) ); -const sourceUrls: string[] = knowledge.source_urls.L.map( - (sourceUrl: any) => sourceUrl.S +console.log( + "Guardrail Configuration:", + JSON.stringify(guardrailConfig, null, 2) ); -const useStandbyReplicas: boolean = USE_STAND_BY_REPLICAS === "true"; - -console.log("ownerUserId: ", ownerUserId); -console.log("botId: ", botId); -console.log("knowledgeBase: ", knowledgeBase); -console.log("knowledge: ", knowledge); -console.log("guardrails: ", guardrails); -console.log("existingS3Urls: ", existingS3Urls); -console.log("sourceUrls: ", sourceUrls); - -const embeddingsModel = getEmbeddingModel(knowledgeBase.embeddings_model.S); -const parsingModel = getParsingModel(knowledgeBase.parsing_model.S) -const crawlingScope = getCrowlingScope(knowledgeBase.web_crawling_scope.S) -const crawlingFilters: CrawlingFilters = getCrawlingFilters(knowledgeBase.web_crawling_filters.M) -const existKnowledgeBaseId: string | undefined = knowledgeBase.exist_knowledge_base_id.S - ? knowledgeBase.exist_knowledge_base_id.S - : undefined; -const maxTokens: number | undefined = knowledgeBase.chunking_configuration.M.max_tokens - ? Number(knowledgeBase.chunking_configuration.M.max_tokens.N) - : undefined; -const instruction: string | undefined = knowledgeBase.instruction - ? knowledgeBase.instruction.S - : undefined; -const analyzer = knowledgeBase.open_search.M.analyzer.M - ? getAnalyzer(knowledgeBase.open_search.M.analyzer.M) - : undefined; -const overlapPercentage: number | undefined = knowledgeBase.chunking_configuration.M.overlap_percentage - ? Number(knowledgeBase.chunking_configuration.M.overlap_percentage.N) - : undefined; -const overlapTokens: number | undefined = knowledgeBase.chunking_configuration.M.overlap_tokens - ? Number(knowledgeBase.chunking_configuration.M.overlap_tokens.N) - : undefined; -const maxParentTokenSize: number | undefined = knowledgeBase.chunking_configuration.M.max_parent_token_size - ? Number(knowledgeBase.chunking_configuration.M.max_parent_token_size.N) - : undefined; -const maxChildTokenSize: number | undefined = knowledgeBase.chunking_configuration.M.max_child_token_size - ? Number(knowledgeBase.chunking_configuration.M.max_child_token_size.N) - : undefined; -const bufferSize: number | undefined = knowledgeBase.chunking_configuration.M.buffer_size - ? Number(knowledgeBase.chunking_configuration.M.buffer_size.N) - : undefined; -const breakpointPercentileThreshold: number | undefined = knowledgeBase.chunking_configuration.M.breakpoint_percentile_threshold - ? Number(knowledgeBase.chunking_configuration.M.breakpoint_percentile_threshold.N) - : undefined; -const is_guardrail_enabled: boolean | undefined = - guardrails.is_guardrail_enabled - ? Boolean(guardrails.is_guardrail_enabled.BOOL) - : undefined; -const hateThreshold: number | undefined = guardrails.hate_threshold - ? Number(guardrails.hate_threshold.N) - : undefined; -const insultsThreshold: number | undefined = guardrails.insults_threshold - ? Number(guardrails.insults_threshold.N) - : undefined; -const sexualThreshold: number | undefined = guardrails.sexual_threshold - ? Number(guardrails.sexual_threshold.N) - : undefined; -const violenceThreshold: number | undefined = guardrails.violence_threshold - ? Number(guardrails.violence_threshold.N) - : undefined; -const misconductThreshold: number | undefined = guardrails.misconduct_threshold - ? Number(guardrails.misconduct_threshold.N) - : undefined; -const groundingThreshold: number | undefined = guardrails.grounding_threshold - ? Number(guardrails.grounding_threshold.N) - : undefined; -const relevanceThreshold: number | undefined = guardrails.relevance_threshold - ? Number(guardrails.relevance_threshold.N) - : undefined; -const guardrailArn: number | undefined = guardrails.guardrail_arn - ? Number(guardrails.guardrail_arn.N) - : undefined; -const guardrailVersion: number | undefined = guardrails.guardrail_version - ? Number(guardrails.guardrail_version.N) - : undefined; -const chunkingStrategy = getChunkingStrategy( - knowledgeBase.chunking_configuration.M.chunking_strategy.S, - knowledgeBase.embeddings_model.S, - { - maxTokens, - overlapPercentage, - overlapTokens, - maxParentTokenSize, - maxChildTokenSize, - bufferSize, - breakpointPercentileThreshold, - } +console.log( + "Crawling Configuration:", + JSON.stringify( + { + crawlingScope: crawlingConfig.crawlingScope?.toString(), + crawlingFilters: crawlingConfig.crawlingFilters, + }, + null, + 2 + ) ); -console.log("embeddingsModel: ", embeddingsModel); -console.log("chunkingStrategy: ", chunkingStrategy); -console.log("existKnowledgeBaseId: ", existKnowledgeBaseId); -console.log("maxTokens: ", maxTokens); -console.log("instruction: ", instruction); -console.log("is_guardrail_enabled: ", is_guardrail_enabled); -console.log("hateThreshold: ", hateThreshold); -console.log("insultsThreshold: ", insultsThreshold); -console.log("sexualThreshold: ", sexualThreshold); -console.log("violenceThreshold: ", violenceThreshold); -console.log("misconductThreshold: ", misconductThreshold); -console.log("relevanceThreshold: ", relevanceThreshold); -console.log("guardrailArn: ", guardrailArn); -console.log("guardrailVersion: ", guardrailVersion); -console.log("parsingModel: ", parsingModel); -console.log("crawlingScope: ", crawlingScope); - -if (analyzer) { - console.log( - "Analyzer: ", - JSON.stringify(knowledgeBase.open_search.M.analyzer, null, 2) - ); -} else { - console.log("Analyzer is undefined or null."); -} +// Create the stack +new BedrockCustomBotStack(app, `BrChatKbStack${baseConfig.botId}`, { + // Environment configuration + env: { + region: baseConfig.bedrockRegion, + }, -console.log("overlapPercentage: ", overlapPercentage); + // Base configuration + ownerUserId: baseConfig.ownerUserId, + botId: baseConfig.botId, + bedrockClaudeChatDocumentBucketName: baseConfig.documentBucketName, + useStandbyReplicas: baseConfig.useStandbyReplicas, -const bedrockCustomBotStack = new BedrockCustomBotStack( - app, - `BrChatKbStack${botId}`, - { - env: { - // account: process.env.CDK_DEFAULT_ACCOUNT, - region: BEDROCK_REGION, - }, - ownerUserId, - botId, - embeddingsModel, - parsingModel, - crawlingScope, - crawlingFilters, - existKnowledgeBaseId, - bedrockClaudeChatDocumentBucketName: - BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME, - chunkingStrategy, - existingS3Urls, - sourceUrls, - maxTokens, - instruction, - analyzer, - overlapPercentage, - guardrail: { - is_guardrail_enabled, - hateThreshold, - insultsThreshold, - sexualThreshold, - violenceThreshold, - misconductThreshold, - groundingThreshold, - relevanceThreshold, - guardrailArn, - guardrailVersion, - }, - useStandbyReplicas, - } -); + // Knowledge base configuration + embeddingsModel: knowledgeConfig.embeddingsModel, + parsingModel: knowledgeConfig.parsingModel, + existKnowledgeBaseId: knowledgeConfig.existKnowledgeBaseId, + existingS3Urls: knowledgeConfig.existingS3Urls, + sourceUrls: knowledgeConfig.sourceUrls, + instruction: knowledgeConfig.instruction, + analyzer: knowledgeConfig.analyzer, + + // Chunking configuration + chunkingStrategy: chunkingConfig.chunkingStrategy, + maxTokens: chunkingConfig.maxTokens, + overlapPercentage: chunkingConfig.overlapPercentage, + + // Crawling configuration + crawlingScope: crawlingConfig.crawlingScope, + crawlingFilters: crawlingConfig.crawlingFilters, + + // Guardrail configuration + guardrail: guardrailConfig, +}); + +cdk.Tags.of(app).add("CDKEnvironment", baseConfig.envName); diff --git a/cdk/lib/bedrock-chat-stack.ts b/cdk/lib/bedrock-chat-stack.ts index 21a22c8f5..a802bf853 100644 --- a/cdk/lib/bedrock-chat-stack.ts +++ b/cdk/lib/bedrock-chat-stack.ts @@ -25,6 +25,8 @@ import * as path from "path"; import { BedrockCustomBotCodebuild } from "./constructs/bedrock-custom-bot-codebuild"; export interface BedrockChatStackProps extends StackProps { + readonly envName: string; + readonly envPrefix: string; readonly bedrockRegion: string; readonly webAclId: string; readonly identityProviders: TIdentityProvider[]; @@ -51,6 +53,7 @@ export class BedrockChatStack extends cdk.Stack { ...props, }); + const sepHyphen = props.envPrefix ? "-" : ""; const idp = identityProvider(props.identityProviders); const accessLogBucket = new Bucket(this, "AccessLogBucket", { @@ -110,6 +113,9 @@ export class BedrockChatStack extends cdk.Stack { "ApiPublishCodebuild", { sourceBucket, + envName: props.envName, + envPrefix: props.envPrefix, + bedrockRegion: props.bedrockRegion, } ); // CodeBuild used for KnowledgeBase @@ -118,6 +124,9 @@ export class BedrockChatStack extends cdk.Stack { "BedrockKnowledgeBaseCodebuild", { sourceBucket, + envName: props.envName, + envPrefix: props.envPrefix, + bedrockRegion: props.bedrockRegion, } ); @@ -155,11 +164,14 @@ export class BedrockChatStack extends cdk.Stack { }); const usageAnalysis = new UsageAnalysis(this, "UsageAnalysis", { + envPrefix: props.envPrefix, accessLogBucket, sourceDatabase: database, }); const backendApi = new Api(this, "BackendApi", { + envName: props.envName, + envPrefix: props.envPrefix, database: database.table, auth, bedrockRegion: props.bedrockRegion, @@ -227,6 +239,7 @@ export class BedrockChatStack extends cdk.Stack { this, "WebAclForPublishedApi", { + envPrefix: props.envPrefix, allowedIpV4AddressRanges: props.publishedApiAllowedIpV4AddressRanges, allowedIpV6AddressRanges: props.publishedApiAllowedIpV6AddressRanges, } @@ -242,19 +255,19 @@ export class BedrockChatStack extends cdk.Stack { // Outputs for API publication new CfnOutput(this, "PublishedApiWebAclArn", { value: webAclForPublishedApi.webAclArn, - exportName: "PublishedApiWebAclArn", + exportName: `${props.envPrefix}${sepHyphen}PublishedApiWebAclArn`, }); new CfnOutput(this, "ConversationTableName", { value: database.table.tableName, - exportName: "BedrockClaudeChatConversationTableName", + exportName: `${props.envPrefix}${sepHyphen}BedrockClaudeChatConversationTableName`, }); new CfnOutput(this, "TableAccessRoleArn", { value: database.tableAccessRole.roleArn, - exportName: "BedrockClaudeChatTableAccessRoleArn", + exportName: `${props.envPrefix}${sepHyphen}BedrockClaudeChatTableAccessRoleArn`, }); new CfnOutput(this, "LargeMessageBucketName", { value: largeMessageBucket.bucketName, - exportName: "BedrockClaudeChatLargeMessageBucketName", + exportName: `${props.envPrefix}${sepHyphen}BedrockClaudeChatLargeMessageBucketName`, }); } } diff --git a/cdk/lib/bedrock-custom-bot-stack.ts b/cdk/lib/bedrock-custom-bot-stack.ts index 78421c339..aa22ee985 100644 --- a/cdk/lib/bedrock-custom-bot-stack.ts +++ b/cdk/lib/bedrock-custom-bot-stack.ts @@ -1,4 +1,4 @@ -import { CfnOutput, RemovalPolicy, Stack, StackProps} from "aws-cdk-lib"; +import { CfnOutput, RemovalPolicy, Stack, StackProps } from "aws-cdk-lib"; import { Construct } from "constructs"; import { VectorCollection } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/opensearchserverless"; import { @@ -8,27 +8,26 @@ import { import { VectorCollectionStandbyReplicas } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/opensearchserverless"; import * as s3 from "aws-cdk-lib/aws-s3"; import * as iam from "aws-cdk-lib/aws-iam"; -import { - BedrockFoundationModel, -} from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock"; -import { - ChunkingStrategy, -} from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock/data-sources/chunking"; -import { - S3DataSource, -} from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock/data-sources/s3-data-source"; +import { BedrockFoundationModel } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock"; +import { ChunkingStrategy } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock/data-sources/chunking"; +import { S3DataSource } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock/data-sources/s3-data-source"; import { WebCrawlerDataSource, CrawlingScope, CrawlingFilters, } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock/data-sources/web-crawler-data-source"; -import { - ParsingStategy -} from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock/data-sources/parsing"; +import { ParsingStategy } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock/data-sources/parsing"; -import { KnowledgeBase, IKnowledgeBase } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock"; +import { + KnowledgeBase, + IKnowledgeBase, +} from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock"; import { aws_bedrock as bedrock } from "aws-cdk-lib"; -import { AwsCustomResource, PhysicalResourceId, AwsCustomResourcePolicy } from 'aws-cdk-lib/custom-resources'; +import { + AwsCustomResource, + PhysicalResourceId, + AwsCustomResourcePolicy, +} from "aws-cdk-lib/custom-resources"; import { getThreshold } from "./utils/bedrock-guardrails"; const BLOCKED_INPUT_MESSAGE = "this input message is blocked"; @@ -48,23 +47,32 @@ interface BedrockGuardrailProps { } interface BedrockCustomBotStackProps extends StackProps { + // Base configuration readonly ownerUserId: string; readonly botId: string; + readonly bedrockClaudeChatDocumentBucketName: string; + readonly useStandbyReplicas?: boolean; + + // Knowledge base configuration readonly embeddingsModel: BedrockFoundationModel; readonly parsingModel?: BedrockFoundationModel; readonly existKnowledgeBaseId: string | undefined; - readonly bedrockClaudeChatDocumentBucketName: string; - readonly chunkingStrategy: ChunkingStrategy; readonly existingS3Urls: string[]; readonly sourceUrls: string[]; - readonly maxTokens?: number; readonly instruction?: string; readonly analyzer?: Analyzer; + + // Chunking configuration + readonly chunkingStrategy: ChunkingStrategy; + readonly maxTokens?: number; readonly overlapPercentage?: number; - readonly guardrail?: BedrockGuardrailProps; - readonly useStandbyReplicas?: boolean; + + // Crawling configuration readonly crawlingScope?: CrawlingScope; readonly crawlingFilters?: CrawlingFilters; + + // Guardrail configuration + readonly guardrail?: BedrockGuardrailProps; } export class BedrockCustomBotStack extends Stack { @@ -73,7 +81,7 @@ export class BedrockCustomBotStack extends Stack { const { docBucketsAndPrefixes } = this.setupBucketsAndPrefixes(props); - let kb: IKnowledgeBase + let kb: IKnowledgeBase; // if knowledge base arn does not exist if (props.existKnowledgeBaseId == undefined) { @@ -105,7 +113,7 @@ export class BedrockCustomBotStack extends Stack { ], analyzer: props.analyzer, }); - + kb = new KnowledgeBase(this, "KB", { embeddingsModel: props.embeddingsModel, vectorStore: vectorCollection, @@ -121,32 +129,39 @@ export class BedrockCustomBotStack extends Stack { knowledgeBase: kb, dataSourceName: bucket.bucketName, chunkingStrategy: props.chunkingStrategy, - parsingStrategy: props.parsingModel ? ParsingStategy.foundationModel({ - parsingModel: props.parsingModel.asIModel(this), - }) : undefined, + parsingStrategy: props.parsingModel + ? ParsingStategy.foundationModel({ + parsingModel: props.parsingModel.asIModel(this), + }) + : undefined, inclusionPrefixes: inclusionPrefixes, }); - }); + }); // Add Web Crawler Data Sources if (props.sourceUrls.length > 0) { - const webCrawlerDataSource = new WebCrawlerDataSource(this, 'WebCrawlerDataSource', { - knowledgeBase: kb, - sourceUrls: props.sourceUrls, - chunkingStrategy: props.chunkingStrategy, - parsingStrategy: props.parsingModel ? ParsingStategy.foundationModel({ - parsingModel: props.parsingModel.asIModel(this), - }) : undefined, - crawlingScope: props.crawlingScope, - filters: { - excludePatterns: props.crawlingFilters?.excludePatterns, - includePatterns: props.crawlingFilters?.includePatterns, + const webCrawlerDataSource = new WebCrawlerDataSource( + this, + "WebCrawlerDataSource", + { + knowledgeBase: kb, + sourceUrls: props.sourceUrls, + chunkingStrategy: props.chunkingStrategy, + parsingStrategy: props.parsingModel + ? ParsingStategy.foundationModel({ + parsingModel: props.parsingModel.asIModel(this), + }) + : undefined, + crawlingScope: props.crawlingScope, + filters: { + excludePatterns: props.crawlingFilters?.excludePatterns, + includePatterns: props.crawlingFilters?.includePatterns, + }, } - + ); + new CfnOutput(this, "DataSourceIdWebCrawler", { + value: webCrawlerDataSource.dataSourceId, }); - new CfnOutput(this, 'DataSourceIdWebCrawler', { - value: webCrawlerDataSource.dataSourceId - }) } if (props.guardrail?.is_guardrail_enabled == true) { @@ -154,7 +169,7 @@ export class BedrockCustomBotStack extends Stack { let contentPolicyConfigFiltersConfig = []; let contextualGroundingFiltersConfig = []; console.log("props.guardrail: ", props.guardrail); - + if ( props.guardrail.hateThreshold != undefined && props.guardrail.hateThreshold > 0 @@ -165,7 +180,7 @@ export class BedrockCustomBotStack extends Stack { type: "HATE", }); } - + if ( props.guardrail.insultsThreshold != undefined && props.guardrail.insultsThreshold > 0 @@ -176,7 +191,7 @@ export class BedrockCustomBotStack extends Stack { type: "INSULTS", }); } - + if ( props.guardrail.sexualThreshold != undefined && props.guardrail.sexualThreshold > 0 @@ -187,7 +202,7 @@ export class BedrockCustomBotStack extends Stack { type: "SEXUAL", }); } - + if ( props.guardrail.violenceThreshold != undefined && props.guardrail.violenceThreshold > 0 @@ -198,7 +213,7 @@ export class BedrockCustomBotStack extends Stack { type: "VIOLENCE", }); } - + if ( props.guardrail.misconductThreshold != undefined && props.guardrail.misconductThreshold > 0 @@ -209,7 +224,7 @@ export class BedrockCustomBotStack extends Stack { type: "MISCONDUCT", }); } - + if ( props.guardrail.groundingThreshold != undefined && props.guardrail.groundingThreshold > 0 @@ -219,7 +234,7 @@ export class BedrockCustomBotStack extends Stack { type: "GROUNDING", }); } - + if ( props.guardrail.relevanceThreshold != undefined && props.guardrail.relevanceThreshold > 0 @@ -229,7 +244,7 @@ export class BedrockCustomBotStack extends Stack { type: "RELEVANCE", }); } - + console.log( "contentPolicyConfigFiltersConfig: ", contentPolicyConfigFiltersConfig @@ -238,7 +253,7 @@ export class BedrockCustomBotStack extends Stack { "contextualGroundingFiltersConfig: ", contextualGroundingFiltersConfig ); - + // Deploy Guardrail if it contains at least one configuration value if ( contentPolicyConfigFiltersConfig.length > 0 || @@ -276,14 +291,12 @@ export class BedrockCustomBotStack extends Stack { value: dataSource.dataSourceId, }); }); - } - else - { + } else { // if knowledgeBaseArn exists - const getKnowledgeBase = new AwsCustomResource(this, 'GetKnowledgeBase', { + const getKnowledgeBase = new AwsCustomResource(this, "GetKnowledgeBase", { onCreate: { - service: 'bedrock-agent', - action: 'GetKnowledgeBase', + service: "bedrock-agent", + action: "GetKnowledgeBase", parameters: { knowledgeBaseId: props.existKnowledgeBaseId, }, @@ -291,20 +304,20 @@ export class BedrockCustomBotStack extends Stack { }, policy: AwsCustomResourcePolicy.fromStatements([ new iam.PolicyStatement({ - actions: [ - 'bedrock:GetKnowledgeBase', + actions: ["bedrock:GetKnowledgeBase"], + resources: [ + `arn:aws:bedrock:${this.region}:${this.account}:knowledge-base/${props.existKnowledgeBaseId}`, ], - resources: [`arn:aws:bedrock:${this.region}:${this.account}:knowledge-base/${props.existKnowledgeBaseId}`], }), - ]), + ]), }); - const executionRoleArn = getKnowledgeBase.getResponseField('roleArn'); + const executionRoleArn = getKnowledgeBase.getResponseField("roleArn"); - kb = KnowledgeBase.fromKnowledgeBaseAttributes(this, 'MyKnowledgeBase', { + kb = KnowledgeBase.fromKnowledgeBaseAttributes(this, "MyKnowledgeBase", { knowledgeBaseId: props.existKnowledgeBaseId, - executionRoleArn: executionRoleArn - }) + executionRoleArn: executionRoleArn, + }); } new CfnOutput(this, "KnowledgeBaseId", { diff --git a/cdk/lib/constructs/api-publish-codebuild.ts b/cdk/lib/constructs/api-publish-codebuild.ts index 6cdec448b..bbf4c43c3 100644 --- a/cdk/lib/constructs/api-publish-codebuild.ts +++ b/cdk/lib/constructs/api-publish-codebuild.ts @@ -3,10 +3,12 @@ import * as codebuild from "aws-cdk-lib/aws-codebuild"; import * as s3 from "aws-cdk-lib/aws-s3"; import * as iam from "aws-cdk-lib/aws-iam"; import * as logs from "aws-cdk-lib/aws-logs"; -import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager"; import { NagSuppressions } from "cdk-nag"; export interface ApiPublishCodebuildProps { + readonly envName: string; + readonly envPrefix: string; + readonly bedrockRegion: string; readonly sourceBucket: s3.Bucket; } @@ -29,14 +31,9 @@ export class ApiPublishCodebuild extends Construct { privileged: true, }, environmentVariables: { - // Need to be overridden when invoke the project - // PUBLISHED_API_THROTTLE_RATE_LIMIT: { value: undefined }, - // PUBLISHED_API_THROTTLE_BURST_LIMIT: { value: undefined }, - // PUBLISHED_API_QUOTA_LIMIT: { value: undefined }, - // PUBLISHED_API_QUOTA_PERIOD: { value: undefined }, - PUBLISHED_API_DEPLOYMENT_STAGE: { value: "api" }, - PUBLISHED_API_ID: { value: "xy1234" }, - PUBLISHED_API_ALLOWED_ORIGINS: { value: '["*"]' }, + ENV_NAME: { value: props.envName }, + ENV_PREFIX: { value: props.envPrefix }, + BEDROCK_REGION: { value: props.bedrockRegion }, }, buildSpec: codebuild.BuildSpec.fromObject({ version: "0.2", @@ -53,14 +50,7 @@ export class ApiPublishCodebuild extends Construct { "npm ci", // Replace cdk's entrypoint. This is a workaround to avoid the issue that cdk synthesize all stacks. "sed -i 's|bin/bedrock-chat.ts|bin/api-publish.ts|' cdk.json", - `npx cdk deploy --require-approval never ApiPublishmentStack$PUBLISHED_API_ID \\ - -c publishedApiThrottleRateLimit=$PUBLISHED_API_THROTTLE_RATE_LIMIT \\ - -c publishedApiThrottleBurstLimit=$PUBLISHED_API_THROTTLE_BURST_LIMIT \\ - -c publishedApiQuotaLimit=$PUBLISHED_API_QUOTA_LIMIT \\ - -c publishedApiQuotaPeriod=$PUBLISHED_API_QUOTA_PERIOD \\ - -c publishedApiDeploymentStage=$PUBLISHED_API_DEPLOYMENT_STAGE \\ - -c publishedApiId=$PUBLISHED_API_ID \\ - -c publishedApiAllowedOrigins=$PUBLISHED_API_ALLOWED_ORIGINS`, + "npx cdk deploy --require-approval never ApiPublishmentStack$PUBLISHED_API_ID", ], }, }, diff --git a/cdk/lib/constructs/api.ts b/cdk/lib/constructs/api.ts index f29244713..ba2c9721b 100644 --- a/cdk/lib/constructs/api.ts +++ b/cdk/lib/constructs/api.ts @@ -27,6 +27,8 @@ import { excludeDockerImage } from "../constants/docker"; import { PythonFunction } from "@aws-cdk/aws-lambda-python-alpha"; export interface ApiProps { + readonly envName: string; + readonly envPrefix: string; readonly database: ITable; readonly corsAllowOrigins?: string[]; readonly auth: Auth; @@ -185,6 +187,7 @@ export class Api extends Construct { "secretsmanager:RotateSecret", "secretsmanager:CancelRotateSecret", "secretsmanager:UpdateSecret", + "secretsmanager:TagResource", ], resources: [ `arn:aws:secretsmanager:${Stack.of(this).region}:${ @@ -209,6 +212,8 @@ export class Api extends Construct { memorySize: 1024, timeout: Duration.minutes(15), environment: { + ENV_NAME: props.envName, + ENV_PREFIX: props.envPrefix, TABLE_NAME: database.tableName, CORS_ALLOW_ORIGINS: allowOrigins.join(","), USER_POOL_ID: props.auth.userPool.userPoolId, diff --git a/cdk/lib/constructs/bedrock-custom-bot-codebuild.ts b/cdk/lib/constructs/bedrock-custom-bot-codebuild.ts index 44a8d3385..4bb1940ed 100644 --- a/cdk/lib/constructs/bedrock-custom-bot-codebuild.ts +++ b/cdk/lib/constructs/bedrock-custom-bot-codebuild.ts @@ -5,6 +5,9 @@ import * as iam from "aws-cdk-lib/aws-iam"; import { NagSuppressions } from "cdk-nag"; export interface BedrockCustomBotCodebuildProps { + readonly envName: string; + readonly envPrefix: string; + readonly bedrockRegion: string; readonly sourceBucket: s3.Bucket; } @@ -28,14 +31,9 @@ export class BedrockCustomBotCodebuild extends Construct { privileged: true, }, environmentVariables: { - PK: { value: "" }, - SK: { value: "" }, - BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME: { - value: "", - }, - KNOWLEDGE: { value: "" }, - BEDROCK_KNOWLEDGE_BASE: { value: "" }, - BEDROCK_GUARDRAILS: { value: "" }, + ENV_NAME: { value: props.envName }, + ENV_PREFIX: { value: props.envPrefix }, + BEDROCK_REGION: { value: props.bedrockRegion }, }, buildSpec: codebuild.BuildSpec.fromObject({ version: "0.2", diff --git a/cdk/lib/constructs/usage-analysis.ts b/cdk/lib/constructs/usage-analysis.ts index 5f2e5afc2..6e6916b32 100644 --- a/cdk/lib/constructs/usage-analysis.ts +++ b/cdk/lib/constructs/usage-analysis.ts @@ -14,6 +14,7 @@ import * as iam from "aws-cdk-lib/aws-iam"; import * as logs from "aws-cdk-lib/aws-logs"; export interface UsageAnalysisProps { + envPrefix: string; sourceDatabase: Database; accessLogBucket?: s3.Bucket; } @@ -28,10 +29,14 @@ export class UsageAnalysis extends Construct { constructor(scope: Construct, id: string, props: UsageAnalysisProps) { super(scope, id); - const GLUE_DATABASE_NAME = `${Stack.of( - this - ).stackName.toLowerCase()}_usage_analysis`; - const DDB_EXPORT_TABLE_NAME = "ddb_export"; + const safeStackName = Stack.of(this) + .stackName.toLowerCase() + .replace("-", "_"); + + const GLUE_DATABASE_NAME = `${safeStackName}_usage_analysis`; + + const sepUnderscore = props.envPrefix ? "_" : ""; + const DDB_EXPORT_TABLE_NAME = `${props.envPrefix}${sepUnderscore}ddb_export`; // Bucket to export DynamoDB data const ddbBucket = new s3.Bucket(this, "DdbBucket", { @@ -59,7 +64,7 @@ export class UsageAnalysis extends Construct { // Workgroup for Athena const wg = new athena.CfnWorkGroup(this, "Wg", { - name: `${Stack.of(this).stackName.toLowerCase()}_wg`, + name: `${safeStackName}_wg`, description: "Workgroup for Athena", recursiveDeleteOption: true, workGroupConfiguration: { diff --git a/cdk/lib/constructs/webacl-for-published-api.ts b/cdk/lib/constructs/webacl-for-published-api.ts index 3de38f652..fa10f9d4b 100644 --- a/cdk/lib/constructs/webacl-for-published-api.ts +++ b/cdk/lib/constructs/webacl-for-published-api.ts @@ -3,6 +3,7 @@ import * as wafv2 from "aws-cdk-lib/aws-wafv2"; import { CfnOutput } from "aws-cdk-lib"; export interface WebAclForPublishedApiProps { + envPrefix: string; readonly allowedIpV4AddressRanges: string[]; readonly allowedIpV6AddressRanges: string[]; } @@ -12,6 +13,7 @@ export class WebAclForPublishedApi extends Construct { constructor(scope: Construct, id: string, props: WebAclForPublishedApiProps) { super(scope, id); + const sepHyphen = props.envPrefix ? "-" : ""; const rules: wafv2.CfnWebACL.RuleProperty[] = []; if (props.allowedIpV4AddressRanges.length > 0) { @@ -58,7 +60,7 @@ export class WebAclForPublishedApi extends Construct { if (rules.length > 0) { const webAcl = new wafv2.CfnWebACL(this, "WebAcl", { defaultAction: { block: {} }, - name: `ApiWebAcl-${id}`, + name: `${props.envPrefix}${sepHyphen}ApiWebAcl-${id}`, scope: "REGIONAL", visibilityConfig: { cloudWatchMetricsEnabled: true, diff --git a/cdk/lib/frontend-waf-stack.ts b/cdk/lib/frontend-waf-stack.ts index 8ee2a64ae..21eb27235 100644 --- a/cdk/lib/frontend-waf-stack.ts +++ b/cdk/lib/frontend-waf-stack.ts @@ -4,6 +4,7 @@ import * as wafv2 from "aws-cdk-lib/aws-wafv2"; import { Construct } from "constructs"; interface FrontendWafStackProps extends StackProps { + readonly envPrefix: string; readonly allowedIpV4AddressRanges: string[]; readonly allowedIpV6AddressRanges: string[]; } @@ -25,6 +26,7 @@ export class FrontendWafStack extends Stack { constructor(scope: Construct, id: string, props: FrontendWafStackProps) { super(scope, id, props); + const sepHyphen = props.envPrefix ? "-" : ""; const rules: wafv2.CfnWebACL.RuleProperty[] = []; // create Ipset for ACL @@ -83,7 +85,7 @@ export class FrontendWafStack extends Stack { if (rules.length > 0) { const webAcl = new wafv2.CfnWebACL(this, "WebAcl", { defaultAction: { block: {} }, - name: "FrontendWebAcl", + name: `${props.envPrefix}${sepHyphen}FrontendWebAcl`, scope: "CLOUDFRONT", visibilityConfig: { cloudWatchMetricsEnabled: true, diff --git a/cdk/lib/utils/parameter-models.ts b/cdk/lib/utils/parameter-models.ts new file mode 100644 index 000000000..c53853941 --- /dev/null +++ b/cdk/lib/utils/parameter-models.ts @@ -0,0 +1,293 @@ +import { z } from "zod"; +import { TIdentityProvider } from "./identity-provider"; +import { App } from "aws-cdk-lib"; + +/** + * Base parameters schema that is common across all entry points + */ +const BaseParametersSchema = z.object({ + // CDK Environments + envName: z.string().default("default"), + envPrefix: z.string().default(""), + + // Bedrock configuration + bedrockRegion: z.string().default("us-east-1"), +}); + +/** + * Helper function to get environment variables with fallback + * @param name Environment variable name + * @param defaultValue Default value if environment variable is not set + * @returns The environment variable value or default value + */ +function getEnvVar(name: string, defaultValue?: string): string | undefined { + const value = process.env[name]; + return value !== undefined ? value : defaultValue; +} + +/** + * Parameters schema for the main Bedrock Chat application + */ +const BedrockChatParametersSchema = BaseParametersSchema.extend({ + // Bedrock configuration + enableMistral: z.boolean().default(false), + enableBedrockCrossRegionInference: z.boolean().default(true), + + // IP address restrictions + allowedIpV4AddressRanges: z + .array(z.string()) + .default(["0.0.0.0/1", "128.0.0.0/1"]), + allowedIpV6AddressRanges: z + .array(z.string()) + .default([ + "0000:0000:0000:0000:0000:0000:0000:0000/1", + "8000:0000:0000:0000:0000:0000:0000:0000/1", + ]), + publishedApiAllowedIpV4AddressRanges: z + .array(z.string()) + .default(["0.0.0.0/1", "128.0.0.0/1"]), + publishedApiAllowedIpV6AddressRanges: z + .array(z.string()) + .default([ + "0000:0000:0000:0000:0000:0000:0000:0000/1", + "8000:0000:0000:0000:0000:0000:0000:0000/1", + ]), + + // Authentication and user management + identityProviders: z + .unknown() + .transform((val) => + Array.isArray(val) ? (val as TIdentityProvider[]) : [] + ) + .pipe(z.array(z.custom())) + .default([]), + userPoolDomainPrefix: z.string().default(""), + allowedSignUpEmailDomains: z.array(z.string()).default([]), + autoJoinUserGroups: z.array(z.string()).default(["CreatingBotAllowed"]), + selfSignUpEnabled: z.boolean().default(true), + + // Performance and availability + enableRagReplicas: z.boolean().default(true), + enableLambdaSnapStart: z.boolean().default(true), + + // Custom domain configuration + alternateDomainName: z.string().default(""), + hostedZoneId: z.string().default(""), +}); + +/** + * Parameters schema for API publishing + */ +const ApiPublishParametersSchema = BaseParametersSchema.extend({ + // API publishing configuration + publishedApiThrottleRateLimit: z + .string() + .optional() + .transform((val) => (val ? Number(val) : undefined)), + publishedApiThrottleBurstLimit: z + .string() + .optional() + .transform((val) => (val ? Number(val) : undefined)), + publishedApiQuotaLimit: z + .string() + .optional() + .transform((val) => (val ? Number(val) : undefined)), + publishedApiQuotaPeriod: z.enum(["DAY", "WEEK", "MONTH"]).optional(), + publishedApiDeploymentStage: z.string().default("api"), + publishedApiId: z.string().optional(), + publishedApiAllowedOrigins: z.string().default('["*"]'), +}); + +/** + * Parameters schema for Bedrock Custom Bot + */ +const BedrockCustomBotParametersSchema = BaseParametersSchema.extend({ + // Bot configuration + pk: z.string(), + sk: z.string(), + documentBucketName: z.string(), + knowledge: z.string(), + knowledgeBase: z.string(), + guardrails: z.string(), + useStandByReplicas: z + .string() + .optional() + .transform((val) => val === "true") + .default("false"), +}); + +/** + * Type definitions for each parameter set + */ +// Input types (for user input, default values are optional) +export type BaseParametersInput = z.input; +export type BedrockChatParametersInput = z.input< + typeof BedrockChatParametersSchema +>; +export type ApiPublishParametersInput = z.input< + typeof ApiPublishParametersSchema +>; +export type BedrockCustomBotParametersInput = z.input< + typeof BedrockCustomBotParametersSchema +>; + +// Output types (for function returns, all properties are required) +export type BaseParameters = z.infer; +export type BedrockChatParameters = z.infer; +export type ApiPublishParameters = z.infer; +export type BedrockCustomBotParameters = z.infer< + typeof BedrockCustomBotParametersSchema +>; + +/** + * Parse and validate parameters for the main Bedrock Chat application. + * If you omit parametersInput, context parameters and environment variables are used. + * @param app CDK App instance + * @param parametersInput (optional) Input parameters that should be used instead of context parameters + * @returns Validated parameters object + */ +export function resolveBedrockChatParameters( + app: App, + parametersInput?: BedrockChatParametersInput +): BedrockChatParameters { + // If parametersInput is provided, use it directly + if (parametersInput) { + return BedrockChatParametersSchema.parse(parametersInput); + } + + // Get environment variables + const envName = app.node.tryGetContext("envName") || "default"; + const envPrefix = envName === "default" ? "" : envName; + + // Otherwise, get parameters from context + const identityProviders = app.node.tryGetContext("identityProviders"); + + const contextParams = { + envName, + envPrefix, + bedrockRegion: app.node.tryGetContext("bedrockRegion"), + enableMistral: app.node.tryGetContext("enableMistral"), + allowedIpV4AddressRanges: app.node.tryGetContext( + "allowedIpV4AddressRanges" + ), + allowedIpV6AddressRanges: app.node.tryGetContext( + "allowedIpV6AddressRanges" + ), + identityProviders: app.node.tryGetContext("identityProviders"), + userPoolDomainPrefix: app.node.tryGetContext("userPoolDomainPrefix"), + allowedSignUpEmailDomains: app.node.tryGetContext( + "allowedSignUpEmailDomains" + ), + autoJoinUserGroups: app.node.tryGetContext("autoJoinUserGroups"), + selfSignUpEnabled: app.node.tryGetContext("selfSignUpEnabled"), + publishedApiAllowedIpV4AddressRanges: app.node.tryGetContext( + "publishedApiAllowedIpV4AddressRanges" + ), + publishedApiAllowedIpV6AddressRanges: app.node.tryGetContext( + "publishedApiAllowedIpV6AddressRanges" + ), + enableRagReplicas: app.node.tryGetContext("enableRagReplicas"), + enableBedrockCrossRegionInference: app.node.tryGetContext( + "enableBedrockCrossRegionInference" + ), + enableLambdaSnapStart: app.node.tryGetContext("enableLambdaSnapStart"), + alternateDomainName: app.node.tryGetContext("alternateDomainName"), + hostedZoneId: app.node.tryGetContext("hostedZoneId"), + }; + + return BedrockChatParametersSchema.parse(contextParams); +} + +/** + * Get Bedrock Chat parameters based on environment name. + * If you omit envName, "default" is used. + * If you omit parametersInput, context parameters and environment variables are used. + * @param app CDK App instance + * @param envName (optional) Environment name. Used as map key if provided + * @param paramsMap (optional) Map of parameters. If not provided, use context parameters + * @returns Validated parameters object + */ +export function getBedrockChatParameters( + app: App, + envName: string | undefined, + paramsMap: Map +): BedrockChatParameters { + if (envName == undefined) { + if (paramsMap.has("default")) { + // Use parameter.ts instead of context parameters + const params = paramsMap.get("default") || {}; + return resolveBedrockChatParameters(app, { + envName: "default", + envPrefix: "", + ...params, + }); + } else { + // Use CDK context parameters (cdk.json or -c options) + return resolveBedrockChatParameters(app); + } + } else { + // Lookup envName in parameter.ts + if (!paramsMap.has(envName)) { + throw new Error(`Environment ${envName} not found in parameter.ts`); + } + + const params = paramsMap.get(envName) || {}; + const envPrefix = envName === "default" ? "" : envName; + + return resolveBedrockChatParameters(app, { + envName, + envPrefix, + ...params, + }); + } +} + +/** + * Parse and validate parameters for API publishing. + * This function is executed by CDK in CodeBuild launched via the API. + * Therefore, this is not intend to be set values using cdk.json or parameter.ts. + * @returns Validated parameters object from environment variables + */ +export function resolveApiPublishParameters(): ApiPublishParameters { + const envVars = { + envName: getEnvVar("ENV_NAME"), + envPrefix: getEnvVar("ENV_PREFIX"), + bedrockRegion: getEnvVar("BEDROCK_REGION"), + publishedApiThrottleRateLimit: getEnvVar( + "PUBLISHED_API_THROTTLE_RATE_LIMIT" + ), + publishedApiThrottleBurstLimit: getEnvVar( + "PUBLISHED_API_THROTTLE_BURST_LIMIT" + ), + publishedApiQuotaLimit: getEnvVar("PUBLISHED_API_QUOTA_LIMIT"), + publishedApiQuotaPeriod: getEnvVar("PUBLISHED_API_QUOTA_PERIOD"), + publishedApiDeploymentStage: getEnvVar("PUBLISHED_API_DEPLOYMENT_STAGE"), + publishedApiId: getEnvVar("PUBLISHED_API_ID"), + publishedApiAllowedOrigins: getEnvVar("PUBLISHED_API_ALLOWED_ORIGINS"), + }; + + return ApiPublishParametersSchema.parse(envVars); +} + +/** + * Parse and validate parameters for Bedrock Custom Bot. + * This function is executed by CDK in CodeBuild launched via the API. + * Therefore, this is not intend to be set values using cdk.json or parameter.ts. + * @returns Validated parameters object from environment variables + */ +export function resolveBedrockCustomBotParameters(): BedrockCustomBotParameters { + const envVars = { + envName: getEnvVar("ENV_NAME"), + envPrefix: getEnvVar("ENV_PREFIX"), + bedrockRegion: getEnvVar("BEDROCK_REGION"), + pk: getEnvVar("PK"), + sk: getEnvVar("SK"), + documentBucketName: getEnvVar("BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME"), + knowledge: getEnvVar("KNOWLEDGE"), + knowledgeBase: getEnvVar("BEDROCK_KNOWLEDGE_BASE"), + guardrails: getEnvVar("BEDROCK_GUARDRAILS"), + useStandByReplicas: getEnvVar("USE_STAND_BY_REPLICAS"), + }; + + return BedrockCustomBotParametersSchema.parse(envVars); +} diff --git a/cdk/package-lock.json b/cdk/package-lock.json index 783ce47e9..bf0d18b00 100644 --- a/cdk/package-lock.json +++ b/cdk/package-lock.json @@ -30,7 +30,8 @@ "jest": "^29.6.1", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", - "typescript": "~5.1.6" + "typescript": "~5.1.6", + "zod": "^3.24.2" } }, "node_modules/@ampproject/remapping": { @@ -5769,6 +5770,16 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.24.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", + "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/cdk/package.json b/cdk/package.json index 97086bbc8..325eeb45f 100644 --- a/cdk/package.json +++ b/cdk/package.json @@ -18,7 +18,8 @@ "jest": "^29.6.1", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", - "typescript": "~5.1.6" + "typescript": "~5.1.6", + "zod": "^3.24.2" }, "dependencies": { "@aws-cdk/aws-glue-alpha": "^2.155.0-alpha.0", diff --git a/cdk/parameter.ts b/cdk/parameter.ts new file mode 100644 index 000000000..ea1e55192 --- /dev/null +++ b/cdk/parameter.ts @@ -0,0 +1,8 @@ +import { BedrockChatParametersInput } from "./lib/utils/parameter-models"; + +export const bedrockChatParams = new Map(); +// You can define multiple environments and their parameters here +// bedrockChatParams.set("dev", {}); + +// If you define "default" environment here, parameters in cdk.json are ignored +// bedrockChatParams.set("default", {}); diff --git a/cdk/test/cdk.test.ts b/cdk/test/cdk.test.ts index b79aafc6a..4f98413a9 100644 --- a/cdk/test/cdk.test.ts +++ b/cdk/test/cdk.test.ts @@ -36,6 +36,8 @@ describe("Bedrock Chat Stack Test", () => { env: { region: "us-west-2", }, + envName: "test", + envPrefix: "test-", bedrockRegion: "us-east-1", crossRegionReferences: true, webAclId: "", @@ -106,6 +108,8 @@ describe("Bedrock Chat Stack Test", () => { env: { region: "us-west-2", }, + envName: "test", + envPrefix: "test-", bedrockRegion: "us-east-1", crossRegionReferences: true, webAclId: "", @@ -173,6 +177,8 @@ describe("Bedrock Chat Stack Test", () => { env: { region: "us-west-2", }, + envName: "test", + envPrefix: "test-", bedrockRegion: "us-east-1", crossRegionReferences: true, webAclId: "", @@ -219,6 +225,8 @@ describe("Bedrock Chat Stack Test", () => { env: { region: "us-east-1", }, + envName: "test", + envPrefix: "test-", bedrockRegion: "us-east-1", crossRegionReferences: true, webAclId: "", @@ -299,6 +307,8 @@ describe("Bedrock Chat Stack Test", () => { env: { region: "us-east-1", }, + envName: "test", + envPrefix: "test-", bedrockRegion: "us-east-1", crossRegionReferences: true, webAclId: "", diff --git a/cdk/test/utils/parameter-models.test.ts b/cdk/test/utils/parameter-models.test.ts new file mode 100644 index 000000000..323c777b2 --- /dev/null +++ b/cdk/test/utils/parameter-models.test.ts @@ -0,0 +1,672 @@ +import { App } from "aws-cdk-lib"; +import { + resolveBedrockChatParameters, + resolveApiPublishParameters, + resolveBedrockCustomBotParameters, + BedrockChatParametersInput, + getBedrockChatParameters, +} from "../../lib/utils/parameter-models"; +import { ZodError } from "zod"; + +/** + * Test helper function: create CDK App instance + */ +function createTestApp(context = {}) { + return new App({ + autoSynth: false, + context, + }); +} + +describe("resolveBedrockChatParameters", () => { + describe("Parameter Source Selection", () => { + test("should use parametersInput when provided", () => { + // Given + const app = createTestApp(); + const inputParams = { + bedrockRegion: "eu-west-1", + enableMistral: true, + }; + + // When + const result = resolveBedrockChatParameters(app, inputParams); + + // Then + expect(result.bedrockRegion).toBe("eu-west-1"); + expect(result.enableMistral).toBe(true); + }); + + test("should get parameters from context when parametersInput is not provided", () => { + // Given + const app = createTestApp({ + bedrockRegion: "ap-northeast-1", + enableMistral: true, + }); + + // When + const result = resolveBedrockChatParameters(app); + + // Then + expect(result.bedrockRegion).toBe("ap-northeast-1"); + expect(result.enableMistral).toBe(true); + }); + }); + + describe("Parameter Validation", () => { + test("should apply default values when required parameters are missing", () => { + // Given + const app = createTestApp(); + + // When + const result = resolveBedrockChatParameters(app); + + // Then + expect(result.bedrockRegion).toBe("us-east-1"); // default value + expect(result.enableMistral).toBe(false); // default value + expect(result.allowedIpV4AddressRanges).toEqual([ + "0.0.0.0/1", + "128.0.0.0/1", + ]); // default value + }); + + test("should correctly parse all parameters when specified", () => { + // Given + const app = createTestApp(); + const inputParams = { + bedrockRegion: "us-west-2", + enableMistral: true, + allowedIpV4AddressRanges: ["192.168.0.0/16"], + allowedIpV6AddressRanges: ["2001:db8::/32"], + identityProviders: [{ service: "google", secretName: "GoogleSecret" }], + userPoolDomainPrefix: "my-app", + allowedSignUpEmailDomains: ["example.com"], + autoJoinUserGroups: ["Users"], + selfSignUpEnabled: false, + publishedApiAllowedIpV4AddressRanges: ["10.0.0.0/8"], + publishedApiAllowedIpV6AddressRanges: ["2001:db8:1::/48"], + enableRagReplicas: false, + enableBedrockCrossRegionInference: false, + enableLambdaSnapStart: false, + alternateDomainName: "chat.example.com", + hostedZoneId: "Z1234567890", + }; + + // When + const result = resolveBedrockChatParameters(app, inputParams); + + // Then + expect(result.bedrockRegion).toBe("us-west-2"); + expect(result.enableMistral).toBe(true); + expect(result.allowedIpV4AddressRanges).toEqual(["192.168.0.0/16"]); + expect(result.allowedIpV6AddressRanges).toEqual(["2001:db8::/32"]); + expect(result.identityProviders).toEqual([ + { service: "google", secretName: "GoogleSecret" }, + ]); + expect(result.userPoolDomainPrefix).toBe("my-app"); + expect(result.allowedSignUpEmailDomains).toEqual(["example.com"]); + expect(result.autoJoinUserGroups).toEqual(["Users"]); + expect(result.selfSignUpEnabled).toBe(false); + expect(result.publishedApiAllowedIpV4AddressRanges).toEqual([ + "10.0.0.0/8", + ]); + expect(result.publishedApiAllowedIpV6AddressRanges).toEqual([ + "2001:db8:1::/48", + ]); + expect(result.enableRagReplicas).toBe(false); + expect(result.enableBedrockCrossRegionInference).toBe(false); + expect(result.enableLambdaSnapStart).toBe(false); + expect(result.alternateDomainName).toBe("chat.example.com"); + expect(result.hostedZoneId).toBe("Z1234567890"); + }); + + test("should throw ZodError when invalid parameter is specified", () => { + // Given + const app = createTestApp(); + const invalidParams = { + bedrockRegion: 123, // number instead of string + }; + + // When/Then + expect(() => { + resolveBedrockChatParameters(app, invalidParams as any); + }).toThrow(ZodError); + }); + }); + + describe("Special Parameter Handling", () => { + test("should correctly process array parameters", () => { + // Given + const app = createTestApp(); + const inputParams = { + allowedIpV4AddressRanges: ["192.168.1.0/24", "10.0.0.0/8"], + allowedSignUpEmailDomains: ["example.com", "test.com"], + }; + + // When + const result = resolveBedrockChatParameters(app, inputParams); + + // Then + expect(result.allowedIpV4AddressRanges).toEqual([ + "192.168.1.0/24", + "10.0.0.0/8", + ]); + expect(result.allowedSignUpEmailDomains).toEqual([ + "example.com", + "test.com", + ]); + }); + + test("should correctly process boolean parameters", () => { + // Given + const app = createTestApp(); + const inputParams = { + enableMistral: true, + selfSignUpEnabled: false, + enableRagReplicas: true, + enableBedrockCrossRegionInference: false, + enableLambdaSnapStart: true, + }; + + // When + const result = resolveBedrockChatParameters(app, inputParams); + + // Then + expect(result.enableMistral).toBe(true); + expect(result.selfSignUpEnabled).toBe(false); + expect(result.enableRagReplicas).toBe(true); + expect(result.enableBedrockCrossRegionInference).toBe(false); + expect(result.enableLambdaSnapStart).toBe(true); + }); + + test("should apply default value (empty array) when identityProviders is not an array", () => { + // Given + const app = createTestApp({ + identityProviders: "invalid", // string instead of array + }); + + // When + const result = resolveBedrockChatParameters(app); + + // Then + expect(result.identityProviders).toEqual([]); + }); + + test("should pass validation even when identityProviders contains invalid service", () => { + // Given + const app = createTestApp(); + const inputParams = { + identityProviders: [{ service: "invalid", secretName: "Secret" }], + }; + + // When + const result = resolveBedrockChatParameters(app, inputParams); + + // Then + expect(result.identityProviders).toEqual([ + { service: "invalid", secretName: "Secret" }, + ]); + // Note: Actual validation is performed in identityProvider function + }); + }); + + test("should correctly parse parameters mimicking cdk.json context properties", () => { + // Given + const app = createTestApp({ + enableMistral: false, + bedrockRegion: "us-east-1", + allowedIpV4AddressRanges: ["0.0.0.0/1", "128.0.0.0/1"], + allowedIpV6AddressRanges: [ + "0000:0000:0000:0000:0000:0000:0000:0000/1", + "8000:0000:0000:0000:0000:0000:0000:0000/1", + ], + identityProviders: [], + userPoolDomainPrefix: "", + allowedSignUpEmailDomains: [], + autoJoinUserGroups: ["CreatingBotAllowed"], + selfSignUpEnabled: true, + publishedApiAllowedIpV4AddressRanges: ["0.0.0.0/1", "128.0.0.0/1"], + publishedApiAllowedIpV6AddressRanges: [ + "0000:0000:0000:0000:0000:0000:0000:0000/1", + "8000:0000:0000:0000:0000:0000:0000:0000/1", + ], + enableRagReplicas: true, + enableBedrockCrossRegionInference: true, + enableLambdaSnapStart: true, + alternateDomainName: "", + hostedZoneId: "", + }); + + // When + const result = resolveBedrockChatParameters(app); + + // Then + expect(result.bedrockRegion).toBe("us-east-1"); + expect(result.enableMistral).toBe(false); + expect(result.allowedIpV4AddressRanges).toEqual([ + "0.0.0.0/1", + "128.0.0.0/1", + ]); + expect(result.allowedIpV6AddressRanges).toEqual([ + "0000:0000:0000:0000:0000:0000:0000:0000/1", + "8000:0000:0000:0000:0000:0000:0000:0000/1", + ]); + expect(result.identityProviders).toEqual([]); + expect(result.userPoolDomainPrefix).toBe(""); + expect(result.allowedSignUpEmailDomains).toEqual([]); + expect(result.autoJoinUserGroups).toEqual(["CreatingBotAllowed"]); + expect(result.selfSignUpEnabled).toBe(true); + expect(result.publishedApiAllowedIpV4AddressRanges).toEqual([ + "0.0.0.0/1", + "128.0.0.0/1", + ]); + expect(result.publishedApiAllowedIpV6AddressRanges).toEqual([ + "0000:0000:0000:0000:0000:0000:0000:0000/1", + "8000:0000:0000:0000:0000:0000:0000:0000/1", + ]); + expect(result.enableRagReplicas).toBe(true); + expect(result.enableBedrockCrossRegionInference).toBe(true); + expect(result.enableLambdaSnapStart).toBe(true); + expect(result.alternateDomainName).toBe(""); + expect(result.hostedZoneId).toBe(""); + }); +}); + +describe("getBedrockChatParameters", () => { + let app: App; + let paramsMap: Map; + + beforeEach(() => { + app = createTestApp(); + paramsMap = new Map(); + }); + + describe("Environment Name Handling", () => { + test("should use params from paramsMap when default exists and envName is undefined", () => { + // Given + const defaultParams: BedrockChatParametersInput = { + bedrockRegion: "us-west-2", + enableMistral: true, + }; + paramsMap.set("default", defaultParams); + + // When + const result = getBedrockChatParameters(app, undefined, paramsMap); + + // Then + expect(result.envName).toBe("default"); + expect(result.envPrefix).toBe(""); + expect(result.bedrockRegion).toBe("us-west-2"); + expect(result.enableMistral).toBe(true); + }); + + test("should use CDK context when default doesn't exist in paramsMap and envName is undefined", () => { + // Given + app = createTestApp({ + bedrockRegion: "eu-west-1", + enableMistral: true, + }); + + // When + const result = getBedrockChatParameters(app, undefined, paramsMap); + + // Then + expect(result.envName).toBe("default"); + expect(result.envPrefix).toBe(""); + expect(result.bedrockRegion).toBe("eu-west-1"); + expect(result.enableMistral).toBe(true); + }); + + test("should throw error when envName doesn't exist in paramsMap", () => { + // Given + const nonExistentEnvName = "nonexistent"; + + // When/Then + expect(() => { + getBedrockChatParameters(app, nonExistentEnvName, paramsMap); + }).toThrow(`Environment ${nonExistentEnvName} not found in parameter.ts`); + }); + + test("should use params with empty envPrefix when envName is default", () => { + // Given + const defaultParams: BedrockChatParametersInput = { + bedrockRegion: "ap-northeast-1", + }; + paramsMap.set("default", defaultParams); + + // When + const result = getBedrockChatParameters(app, "default", paramsMap); + + // Then + expect(result.envName).toBe("default"); + expect(result.envPrefix).toBe(""); + expect(result.bedrockRegion).toBe("ap-northeast-1"); + }); + + test("should use params with envName as envPrefix for non-default env", () => { + // Given + const devParams: BedrockChatParametersInput = { + bedrockRegion: "ap-southeast-1", + }; + paramsMap.set("dev", devParams); + + // When + const result = getBedrockChatParameters(app, "dev", paramsMap); + + // Then + expect(result.envName).toBe("dev"); + expect(result.envPrefix).toBe("dev"); + expect(result.bedrockRegion).toBe("ap-southeast-1"); + }); + }); + + describe("Parameter Validation", () => { + test("should apply default values for optional parameters", () => { + // Given + const minimalParams: BedrockChatParametersInput = { + bedrockRegion: "us-east-1", + }; + paramsMap.set("minimal", minimalParams); + + // When + const result = getBedrockChatParameters(app, "minimal", paramsMap); + + // Then + expect(result.bedrockRegion).toBe("us-east-1"); + expect(result.enableMistral).toBe(false); + expect(result.enableBedrockCrossRegionInference).toBe(true); + expect(result.enableRagReplicas).toBe(true); + expect(result.enableLambdaSnapStart).toBe(true); + expect(result.selfSignUpEnabled).toBe(true); + expect(result.autoJoinUserGroups).toEqual(["CreatingBotAllowed"]); + expect(result.allowedSignUpEmailDomains).toEqual([]); + expect(result.identityProviders).toEqual([]); + }); + + test("should override default values with provided values", () => { + // Given + const customParams: BedrockChatParametersInput = { + bedrockRegion: "us-east-2", + enableMistral: true, + enableBedrockCrossRegionInference: false, + enableRagReplicas: false, + enableLambdaSnapStart: false, + selfSignUpEnabled: false, + autoJoinUserGroups: ["CustomGroup"], + allowedSignUpEmailDomains: ["example.com"], + }; + paramsMap.set("custom", customParams); + + // When + const result = getBedrockChatParameters(app, "custom", paramsMap); + + // Then + expect(result.bedrockRegion).toBe("us-east-2"); + expect(result.enableMistral).toBe(true); + expect(result.enableBedrockCrossRegionInference).toBe(false); + expect(result.enableRagReplicas).toBe(false); + expect(result.enableLambdaSnapStart).toBe(false); + expect(result.selfSignUpEnabled).toBe(false); + expect(result.autoJoinUserGroups).toEqual(["CustomGroup"]); + expect(result.allowedSignUpEmailDomains).toEqual(["example.com"]); + }); + + test("should use default values when CDK context is empty", () => { + // Given + const emptyParamsMap = new Map(); + + // When + const result = getBedrockChatParameters(app, undefined, emptyParamsMap); + + // Then + expect(result.bedrockRegion).toBe("us-east-1"); + expect(result.enableMistral).toBe(false); + expect(result.enableBedrockCrossRegionInference).toBe(true); + expect(result.enableRagReplicas).toBe(true); + expect(result.enableLambdaSnapStart).toBe(true); + }); + }); +}); + +describe("resolveApiPublishParameters", () => { + describe("Parameter Source Selection", () => { + test("should get parameters from environment variables", () => { + // Given + // Mock environment variables + const originalEnv = process.env; + process.env = { + ...originalEnv, + BEDROCK_REGION: "us-east-1", + PUBLISHED_API_THROTTLE_RATE_LIMIT: "200", + PUBLISHED_API_ALLOWED_ORIGINS: '["https://test.com"]', + }; + + try { + // When + const result = resolveApiPublishParameters(); + + // Then + expect(result.bedrockRegion).toBe("us-east-1"); + expect(result.publishedApiThrottleRateLimit).toBe(200); + expect(result.publishedApiAllowedOrigins).toBe('["https://test.com"]'); + } finally { + // Restore original environment + process.env = originalEnv; + } + }); + }); + + describe("Parameter Validation", () => { + test("should apply default values when required parameters are missing", () => { + // Given + // When + const result = resolveApiPublishParameters(); + + // Then + expect(result.bedrockRegion).toBe("us-east-1"); // default value + expect(result.publishedApiAllowedOrigins).toBe('["*"]'); // default value + expect(result.publishedApiThrottleRateLimit).toBeUndefined(); // optional + }); + + test("should convert string numeric parameters to numbers", () => { + // Given + // Mock environment variables + const originalEnv = process.env; + process.env = { + ...originalEnv, + PUBLISHED_API_THROTTLE_RATE_LIMIT: "100", + PUBLISHED_API_THROTTLE_BURST_LIMIT: "200", + PUBLISHED_API_QUOTA_LIMIT: "1000", + }; + + try { + // When + const result = resolveApiPublishParameters(); + + // Then + expect(result.publishedApiThrottleRateLimit).toBe(100); + expect(result.publishedApiThrottleBurstLimit).toBe(200); + expect(result.publishedApiQuotaLimit).toBe(1000); + } finally { + // Restore original environment + process.env = originalEnv; + } + }); + + test("should correctly parse all parameters when specified", () => { + // Given + // Mock environment variables + const originalEnv = process.env; + process.env = { + ...originalEnv, + BEDROCK_REGION: "us-west-2", + PUBLISHED_API_THROTTLE_RATE_LIMIT: "100", + PUBLISHED_API_THROTTLE_BURST_LIMIT: "200", + PUBLISHED_API_QUOTA_LIMIT: "1000", + }; + + try { + // When + const result = resolveApiPublishParameters(); + + // Then + expect(result.publishedApiThrottleRateLimit).toBe(100); + expect(result.publishedApiThrottleBurstLimit).toBe(200); + expect(result.publishedApiQuotaLimit).toBe(1000); + } finally { + // Restore original environment + process.env = originalEnv; + } + }); + + test("should correctly parse all parameters when specified", () => { + // Given + // Mock environment variables + const originalEnv = process.env; + process.env = { + ...originalEnv, + BEDROCK_REGION: "us-west-2", + PUBLISHED_API_THROTTLE_RATE_LIMIT: "100", + PUBLISHED_API_THROTTLE_BURST_LIMIT: "200", + PUBLISHED_API_QUOTA_LIMIT: "1000", + PUBLISHED_API_QUOTA_PERIOD: "DAY", + PUBLISHED_API_DEPLOYMENT_STAGE: "prod", + PUBLISHED_API_ID: "api123", + PUBLISHED_API_ALLOWED_ORIGINS: + '["https://example.com", "https://test.com"]', + }; + + try { + // When + const result = resolveApiPublishParameters(); + + // Then + expect(result.bedrockRegion).toBe("us-west-2"); + expect(result.publishedApiThrottleRateLimit).toBe(100); + expect(result.publishedApiThrottleBurstLimit).toBe(200); + expect(result.publishedApiQuotaLimit).toBe(1000); + expect(result.publishedApiQuotaPeriod).toBe("DAY"); + expect(result.publishedApiDeploymentStage).toBe("prod"); + expect(result.publishedApiId).toBe("api123"); + expect(result.publishedApiAllowedOrigins).toBe( + '["https://example.com", "https://test.com"]' + ); + } finally { + // Restore original environment + process.env = originalEnv; + } + }); + + test("should throw ZodError when invalid QuotaPeriod is specified", () => { + // Given + // Mock environment variables + const originalEnv = process.env; + process.env = { + ...originalEnv, + PUBLISHED_API_QUOTA_PERIOD: "YEAR", // invalid value + }; + + try { + // When/Then + expect(() => { + resolveApiPublishParameters(); + }).toThrow(ZodError); + } finally { + // Restore original environment + process.env = originalEnv; + } + }); + + test("should handle invalid publishedApiAllowedOrigins format", () => { + // Given + // Mock environment variables + const originalEnv = process.env; + process.env = { + ...originalEnv, + PUBLISHED_API_ALLOWED_ORIGINS: "invalid json format", + }; + + try { + // When + const result = resolveApiPublishParameters(); + + // Then + // Note: The function doesn't validate JSON format, it just passes the string through + expect(result.publishedApiAllowedOrigins).toBe("invalid json format"); + } finally { + // Restore original environment + process.env = originalEnv; + } + }); + }); +}); + +describe("resolveBedrockCustomBotParameters", () => { + describe("Parameter Source Selection", () => { + test("should get parameters from environment variables", () => { + // Given + // Mock environment variables + const originalEnv = process.env; + process.env = { + ...originalEnv, + ENV_NAME: "test-env", + ENV_PREFIX: "test-prefix", + BEDROCK_REGION: "us-east-1", + PK: "env-pk", + SK: "env-sk", + BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME: "env-bucket", + KNOWLEDGE: '{"env": "knowledge"}', + BEDROCK_KNOWLEDGE_BASE: '{"env": "kb"}', + BEDROCK_GUARDRAILS: '{"env": "guardrails"}', + USE_STAND_BY_REPLICAS: "true", + }; + + try { + // When + const result = resolveBedrockCustomBotParameters(); + + // Then + expect(result.bedrockRegion).toBe("us-east-1"); + expect(result.envName).toBe("test-env"); + expect(result.envPrefix).toBe("test-prefix"); + expect(result.pk).toBe("env-pk"); + expect(result.sk).toBe("env-sk"); + expect(result.documentBucketName).toBe("env-bucket"); + expect(result.knowledge).toBe('{"env": "knowledge"}'); + expect(result.knowledgeBase).toBe('{"env": "kb"}'); + expect(result.guardrails).toBe('{"env": "guardrails"}'); + expect(result.useStandByReplicas).toBe(true); + } finally { + // Restore original environment + process.env = originalEnv; + } + }); + }); + + describe("Parameter Validation", () => { + test("should throw ZodError when required parameters are missing", () => { + // Given + // When/Then + expect(() => { + resolveBedrockCustomBotParameters(); + }).toThrow(); + }); + + test("should throw ZodError when invalid parameter is specified", () => { + // Given + // Mock environment variables with invalid type + const originalEnv = process.env; + // Clear all environment variables to ensure required ones are missing + process.env = {}; + + try { + // When/Then + expect(() => { + resolveBedrockCustomBotParameters(); + }).toThrow(); + } finally { + // Restore original environment + process.env = originalEnv; + } + }); + }); +}); diff --git a/docs/ADMINISTRATOR.md b/docs/ADMINISTRATOR.md index 722717b1d..3f6f98e6e 100644 --- a/docs/ADMINISTRATOR.md +++ b/docs/ADMINISTRATOR.md @@ -35,6 +35,20 @@ The admin user must be a member of group called `Admin`, which can be set up via - In user usages, users who have not used the system at all during the specified period will not be listed. +> [!Important] > **Multi-Environment Database Names** +> +> If you're using multiple environments (dev, prod, etc.), the Athena database name will include the environment prefix. Instead of `bedrockchatstack_usage_analysis`, the database name will be: +> +> - For default environment: `bedrockchatstack_usage_analysis` +> - For named environments: `_bedrockchatstack_usage_analysis` (e.g., `dev_bedrockchatstack_usage_analysis`) +> +> Additionally, the table name will include the environment prefix: +> +> - For default environment: `ddb_export` +> - For named environments: `_ddb_export` (e.g., `dev_ddb_export`) +> +> Make sure to adjust your queries accordingly when working with multiple environments. + ## Download conversation data You can query the conversation logs by Athena, using SQL. To download logs, open Athena Query Editor from management console and run SQL. Followings are some example queries which are useful to analyze use-cases. Feedback can be referred in `MessageMap` attribute. @@ -63,6 +77,9 @@ ORDER BY d.datehour DESC; ``` +> [!Note] +> If using a named environment (e.g., "dev"), replace `bedrockchatstack_usage_analysis.ddb_export` with `dev_bedrockchatstack_usage_analysis.dev_ddb_export` in the query above. + ### Query per User ID Edit `user-id` and `datehour`. `user-id` can be referred on Bot Management screen. @@ -89,3 +106,6 @@ WHERE ORDER BY d.datehour DESC; ``` + +> [!Note] +> If using a named environment (e.g., "dev"), replace `bedrockchatstack_usage_analysis.ddb_export` with `dev_bedrockchatstack_usage_analysis.dev_ddb_export` in the query above.