From 9e73932f27ba46cc5aa9e77e6fe8602060da765b Mon Sep 17 00:00:00 2001 From: Yukinobu Mine Date: Tue, 14 Oct 2025 12:40:02 +0900 Subject: [PATCH 1/9] Implementation of 'Pool pattern' for Bedrock knowledge bases, excluding Step Functions. - Backend - Add `type` to `BedrockKnowledgeBaseModel`. - `dedicated` or `shared` or null - Add `BedrockKnowledgeBaseType` and `BedrockKnowledgeBaseHash` for sparse index of `BotTable`. - When `shared` is selected, dedicated Knowledge Base is not created. - Frontend - Add 'Pool Pattern' selection for Knowledge settings in `BotKbEditPage`. --- backend/app/repositories/custom_bot.py | 65 ++- backend/app/repositories/models/custom_bot.py | 29 ++ .../app/repositories/models/custom_bot_kb.py | 3 +- backend/app/routes/schemas/bot.py | 10 + backend/app/routes/schemas/bot_kb.py | 2 + backend/app/vector_search.py | 7 + .../fetch_stack_output.py | 25 +- .../store_knowledge_base_id.py | 45 +- .../guardrails/store_guardrail_arn.py | 41 +- .../test_repositories/test_custom_bot.py | 5 + .../test_repositories/test_models/test_bot.py | 1 + .../test_repositories/utils/bot_factory.py | 1 + cdk/bin/bedrock-custom-bot.ts | 7 + cdk/lib/bedrock-chat-stack.ts | 1 - cdk/lib/bedrock-custom-bot-stack.ts | 440 ++++++++-------- cdk/lib/constructs/database.ts | 12 + cdk/lib/constructs/websocket.ts | 5 +- cdk/lib/utils/bedrock-knowledge-base-args.ts | 22 + cdk/package-lock.json | 184 +++---- cdk/test/cdk.test.ts | 7 + .../features/knowledgeBase/constants/index.ts | 1 + .../knowledgeBase/pages/BotKbEditPage.tsx | 478 ++++++++++-------- .../features/knowledgeBase/types/index.d.ts | 3 + frontend/src/i18n/en/index.ts | 3 + frontend/src/i18n/ja/index.ts | 3 + 25 files changed, 781 insertions(+), 619 deletions(-) diff --git a/backend/app/repositories/custom_bot.py b/backend/app/repositories/custom_bot.py index 84cc2af59..c766967e8 100644 --- a/backend/app/repositories/custom_bot.py +++ b/backend/app/repositories/custom_bot.py @@ -1,12 +1,9 @@ import base64 +from hashlib import md5 import json import logging -import os -from datetime import datetime from decimal import Decimal as decimal -from typing import Union -import boto3 from app.config import DEFAULT_GENERATION_CONFIG from app.repositories.common import ( TRANSACTION_BATCH_READ_SIZE, @@ -96,6 +93,18 @@ def store_bot(custom_bot: BotModel): item["IsStarred"] = "TRUE" if custom_bot.bedrock_knowledge_base: item["BedrockKnowledgeBase"] = custom_bot.bedrock_knowledge_base.model_dump() + if custom_bot.bedrock_knowledge_base.type is not None: + item["BedrockKnowledgeBaseType"] = custom_bot.bedrock_knowledge_base.type + item["BedrockKnowledgeBaseHash"] = ( + base64.b32encode( + md5( + custom_bot.bedrock_knowledge_base.model_dump_json().encode() + ).digest() + ) + .decode() + .rstrip("=") + ) + if custom_bot.bedrock_guardrails: item["GuardrailsParams"] = custom_bot.bedrock_guardrails.model_dump() @@ -142,6 +151,7 @@ def update_bot( "ConversationQuickStarters = :conversation_quick_starters, " "ActiveModels = :active_models" ) + remove_attributes: list[str] = [] expression_attribute_values = { ":title": title, @@ -160,18 +170,60 @@ def update_bot( ":active_models": active_models.model_dump(), # type: ignore[attr-defined] } if bedrock_knowledge_base: + if ( + bedrock_knowledge_base.exist_knowledge_base_id is not None + or ( + len(knowledge.source_urls) == 0 + and len(knowledge.sitemap_urls) == 0 + and len(knowledge.filenames) == 0 + and len(knowledge.s3_urls) == 0 + ) + ): + bedrock_knowledge_base.type = None + bedrock_knowledge_base.knowledge_base_id = None + + elif bedrock_knowledge_base.type is None: + bedrock_knowledge_base.type = "dedicated" + update_expression += ", BedrockKnowledgeBase = :bedrock_knowledge_base" expression_attribute_values[":bedrock_knowledge_base"] = ( bedrock_knowledge_base.model_dump() ) + if bedrock_knowledge_base.type is not None: + update_expression += ( + ", BedrockKnowledgeBaseType = :bedrock_knowledge_base_type" + ", BedrockKnowledgeBaseHash = :bedrock_knowledge_base_hash" + ) + expression_attribute_values[":bedrock_knowledge_base_type"] = ( + bedrock_knowledge_base.type + ) + expression_attribute_values[":bedrock_knowledge_base_hash"] = ( + base64.b32encode( + md5(bedrock_knowledge_base.model_dump_json().encode()).digest() + ) + .decode() + .rstrip("=") + ) + + else: + remove_attributes.append("BedrockKnowledgeBaseType") + remove_attributes.append("BedrockKnowledgeBaseHash") + if bedrock_guardrails: + if not bedrock_guardrails.is_guardrail_enabled: + bedrock_guardrails.guardrail_arn = "" + bedrock_guardrails.guardrail_version = "" + update_expression += ", GuardrailsParams = :bedrock_guardrails" expression_attribute_values[":bedrock_guardrails"] = ( bedrock_guardrails.model_dump() ) try: + if len(remove_attributes) > 0: + update_expression += " REMOVE " + ", ".join(remove_attributes) + response = table.update_item( Key={"PK": owner_user_id, "SK": compose_sk(bot_id, "bot")}, UpdateExpression=update_expression, @@ -336,7 +388,10 @@ def update_alias_star_status(user_id: str, original_bot_id: str, starred: bool): def update_knowledge_base_id( - user_id: str, bot_id: str, knowledge_base_id: str, data_source_ids: list[str] + user_id: str, + bot_id: str, + knowledge_base_id: str | None, + data_source_ids: list[str] | None, ): table = get_bot_table_client() logger.info(f"Updating knowledge base id for bot: {bot_id}") diff --git a/backend/app/repositories/models/custom_bot.py b/backend/app/repositories/models/custom_bot.py index 4feae1fe7..bcccaae52 100644 --- a/backend/app/repositories/models/custom_bot.py +++ b/backend/app/repositories/models/custom_bot.py @@ -444,6 +444,35 @@ def validate_shared_scope(self) -> Self: ) return self + @model_validator(mode="after") + def validate_knowledge_base_type(self) -> Self: + if self.bedrock_knowledge_base is not None: + if ( + self.bedrock_knowledge_base.exist_knowledge_base_id is not None + or ( + len(self.knowledge.source_urls) == 0 + and len(self.knowledge.sitemap_urls) == 0 + and len(self.knowledge.filenames) == 0 + and len(self.knowledge.s3_urls) == 0 + ) + ): + self.bedrock_knowledge_base.type = None + self.bedrock_knowledge_base.knowledge_base_id = None + + elif self.bedrock_knowledge_base.type is None: + self.bedrock_knowledge_base.type = "dedicated" + + return self + + @model_validator(mode="after") + def validate_guardrails(self) -> Self: + if self.bedrock_guardrails is not None: + if not self.bedrock_guardrails.is_guardrail_enabled: + self.bedrock_guardrails.guardrail_arn = "" + self.bedrock_guardrails.guardrail_version = "" + + return self + @field_validator("published_api_stack_name", mode="after") def validate_published_api_stack_name( cls, value: str | None, info: ValidationInfo diff --git a/backend/app/repositories/models/custom_bot_kb.py b/backend/app/repositories/models/custom_bot_kb.py index 1d4c0dd60..3d52a0829 100644 --- a/backend/app/repositories/models/custom_bot_kb.py +++ b/backend/app/repositories/models/custom_bot_kb.py @@ -9,7 +9,7 @@ type_os_tokenizer, type_kb_resource_type, ) -from typing import Self +from typing import Literal from pydantic import BaseModel, validator, model_validator @@ -74,6 +74,7 @@ class BedrockAgentGetKnowledgeBaseResponse(BaseModel): class BedrockKnowledgeBaseModel(BaseModel): + type: Literal["dedicated", "shared"] | None = None embeddings_model: type_kb_embeddings_model open_search: OpenSearchParamsModel chunking_configuration: ( diff --git a/backend/app/routes/schemas/bot.py b/backend/app/routes/schemas/bot.py index 14d64da3d..5cb78f23c 100644 --- a/backend/app/routes/schemas/bot.py +++ b/backend/app/routes/schemas/bot.py @@ -351,6 +351,16 @@ def is_embedding_required(self, current_bot_model: BotModel) -> bool: else: return True + if ( + self.bedrock_knowledge_base is not None + and current_bot_model.bedrock_knowledge_base is not None + ): + if ( + self.bedrock_knowledge_base.type + != current_bot_model.bedrock_knowledge_base.type + ): + return True + return False diff --git a/backend/app/routes/schemas/bot_kb.py b/backend/app/routes/schemas/bot_kb.py index 37c29ffdf..869c34e02 100644 --- a/backend/app/routes/schemas/bot_kb.py +++ b/backend/app/routes/schemas/bot_kb.py @@ -89,6 +89,7 @@ class WebCrawlingFilters(BaseSchema): class BedrockKnowledgeBaseInput(BaseSchema): + type: Literal["dedicated", "shared"] | None = None embeddings_model: type_kb_embeddings_model open_search: OpenSearchParams chunking_configuration: ( @@ -109,6 +110,7 @@ class BedrockKnowledgeBaseInput(BaseSchema): class BedrockKnowledgeBaseOutput(BaseSchema): + type: Literal["dedicated", "shared"] | None = None embeddings_model: type_kb_embeddings_model open_search: OpenSearchParams chunking_configuration: ( diff --git a/backend/app/vector_search.py b/backend/app/vector_search.py index b6048e6d7..9d9d41025 100644 --- a/backend/app/vector_search.py +++ b/backend/app/vector_search.py @@ -103,6 +103,13 @@ def _bedrock_knowledge_base_search(bot: BotModel, query: str) -> list[SearchResu } }, } + if bot.bedrock_knowledge_base.type == "shared": + retrieve_parameter["retrievalConfiguration"]["vectorSearchConfiguration"]["filter"] = { # type: ignore + "listContains": { + "key": "tenants", + "value": f"BOT#{bot.id}", + }, + } # Omit overrideSearchType parameter if needed def omit_override_search_type_parameter( diff --git a/backend/embedding_statemachine/bedrock_knowledge_base/fetch_stack_output.py b/backend/embedding_statemachine/bedrock_knowledge_base/fetch_stack_output.py index 6f34ff194..e330cbd53 100644 --- a/backend/embedding_statemachine/bedrock_knowledge_base/fetch_stack_output.py +++ b/backend/embedding_statemachine/bedrock_knowledge_base/fetch_stack_output.py @@ -12,18 +12,18 @@ class StackItem(TypedDict): KnowledgeBaseId: str DataSourceId: str - GuardrailArn: str - GuardrailVersion: str PK: str SK: str -class StackResult(TypedDict): +class StackOutput(TypedDict): KnowledgeBaseId: str items: List[StackItem] + GuardrailArn: str + GuardrailVersion: str -def handler(event: Dict[str, str], context: Any) -> StackResult: +def handler(event: Dict[str, str], context: Any) -> StackOutput: print(event) pk = event["pk"] sk = event["sk"] @@ -39,9 +39,12 @@ def handler(event: Dict[str, str], context: Any) -> StackResult: knowledge_base_id = None data_source_ids: List[str] = [] - guardrail_arn = None - guardrail_version = None - result: StackResult = {"KnowledgeBaseId": "", "items": []} + result: StackOutput = { + "KnowledgeBaseId": "", + "items": [], + "GuardrailArn": "", + "GuardrailVersion": "", + } for output in outputs: if output["OutputKey"] == "KnowledgeBaseId": @@ -50,19 +53,15 @@ def handler(event: Dict[str, str], context: Any) -> StackResult: elif output["OutputKey"].startswith("DataSource"): data_source_ids.append(output["OutputValue"]) elif output["OutputKey"] == "GuardrailArn": - guardrail_arn = output["OutputValue"] + result["GuardrailArn"] = output["OutputValue"] elif output["OutputKey"] == "GuardrailVersion": - guardrail_version = output["OutputValue"] + result["GuardrailVersion"] = output["OutputValue"] for data_source_id in data_source_ids: result["items"].append( { "KnowledgeBaseId": knowledge_base_id or "", "DataSourceId": data_source_id, - "GuardrailArn": guardrail_arn if guardrail_arn is not None else "", - "GuardrailVersion": ( - guardrail_version if guardrail_version is not None else "" - ), "PK": pk, "SK": sk, } diff --git a/backend/embedding_statemachine/bedrock_knowledge_base/store_knowledge_base_id.py b/backend/embedding_statemachine/bedrock_knowledge_base/store_knowledge_base_id.py index 91adea2b4..00ba8016a 100644 --- a/backend/embedding_statemachine/bedrock_knowledge_base/store_knowledge_base_id.py +++ b/backend/embedding_statemachine/bedrock_knowledge_base/store_knowledge_base_id.py @@ -1,29 +1,26 @@ import logging -import os from typing import List from app.repositories.common import decompose_sk from app.repositories.custom_bot import update_knowledge_base_id -from app.routes.schemas.bot import type_sync_status -from reretry import retry from typing_extensions import TypedDict logger = logging.getLogger() logger.setLevel(logging.INFO) -class Items(TypedDict): +class StackItem(TypedDict): KnowledgeBaseId: str DataSourceId: str - GuardrailArn: str - GuardrailVersion: str PK: str SK: str class StackOutput(TypedDict): KnowledgeBaseId: str - items: List[Items] + items: List[StackItem] + GuardrailArn: str + GuardrailVersion: str def handler(event, context): @@ -32,20 +29,30 @@ def handler(event, context): sk = event["sk"] stack_output: StackOutput = event["stack_output"] - kb_id = ( - stack_output["KnowledgeBaseId"] if "KnowledgeBaseId" in stack_output else None - ) - if not kb_id: - raise ValueError("KnowledgeBaseId not found in stack outputs") + knowledge_base_id: str | None + data_source_ids: list[str] | None + + # Check if stack_output is valid + if not stack_output: + logger.warning("No stack_output received") + knowledge_base_id = None + data_source_ids = None + + else: + kb_id = stack_output.get("KnowledgeBaseId") + + # Filter out None values and ensure all elements are strings + ds_ids = [ + item["DataSourceId"] + for item in stack_output.get("items", []) + if item.get("DataSourceId") + ] - # Filter out None values and ensure all elements are strings - data_source_ids: List[str] = [ - item["DataSourceId"] - for item in stack_output.get("items", []) - if item.get("DataSourceId") - ] + knowledge_base_id = kb_id if kb_id else None + data_source_ids = ds_ids if kb_id else None user_id = pk bot_id = decompose_sk(sk) - update_knowledge_base_id(user_id, bot_id, kb_id, data_source_ids) + if knowledge_base_id is not None: + update_knowledge_base_id(user_id, bot_id, knowledge_base_id, data_source_ids) diff --git a/backend/embedding_statemachine/guardrails/store_guardrail_arn.py b/backend/embedding_statemachine/guardrails/store_guardrail_arn.py index 8bfbf0a48..1a0ee290e 100644 --- a/backend/embedding_statemachine/guardrails/store_guardrail_arn.py +++ b/backend/embedding_statemachine/guardrails/store_guardrail_arn.py @@ -1,23 +1,16 @@ -import json import logging -import os from typing import List, TypedDict -import boto3 from app.repositories.common import decompose_sk from app.repositories.custom_bot import update_guardrails_params -from app.routes.schemas.bot import type_sync_status -from reretry import retry logger = logging.getLogger() logger.setLevel(logging.INFO) -class Items(TypedDict): +class StackItem(TypedDict): KnowledgeBaseId: str DataSourceId: str - GuardrailArn: str - GuardrailVersion: str PK: str SK: str @@ -30,17 +23,19 @@ class StackOutput(TypedDict): { 'KnowledgeBaseId': 'MNOPQRSTUVWX', 'DataSourceId': 'YZABCDEFGHI', - 'GuardrailArn': 'arn:aws:bedrock:us-east-1:123456789012:guardrail/abcdefghijkl', - 'GuardrailVersion': 'DRAFT', 'PK': '7801e3f0-40b1-70da-2e13-652d4adce1c3', - 'SK': '7801e3f0-40b1-70da-2e13-652d4adce1c3#BOT#01JKWE8RP6YWNX9SKFSCCNS73Z' + 'SK': 'BOT#01JKWE8RP6YWNX9SKFSCCNS73Z' } - ] + ], + 'GuardrailArn': 'arn:aws:bedrock:us-east-1:123456789012:guardrail/abcdefghijkl', + 'GuardrailVersion': 'DRAFT', } """ KnowledgeBaseId: str - items: List[Items] + items: List[StackItem] + GuardrailArn: str + GuardrailVersion: str def handler(event, context): @@ -49,22 +44,18 @@ def handler(event, context): sk = event["sk"] stack_output: StackOutput = event["stack_output"] - # Check if stack_output is valid and has at least one item - if ( - not stack_output - or not isinstance(stack_output["items"], list) - or len(stack_output["items"]) == 0 - ): - logger.warning("Empty or invalid stack_output received") + # Check if stack_output is valid + if not stack_output: + logger.warning("No stack_output received") guardrail_arn = "" guardrail_version = "" + else: - # Access the first item directly since we know it exists - first_output = stack_output["items"][0] - guardrail_arn = first_output.get("GuardrailArn", "") - guardrail_version = first_output.get("GuardrailVersion", "") + guardrail_arn = stack_output.get("GuardrailArn", "") + guardrail_version = stack_output.get("GuardrailVersion", "") user_id = pk bot_id = decompose_sk(sk) - update_guardrails_params(user_id, bot_id, guardrail_arn, guardrail_version) + if guardrail_arn: + update_guardrails_params(user_id, bot_id, guardrail_arn, guardrail_version) diff --git a/backend/tests/test_repositories/test_custom_bot.py b/backend/tests/test_repositories/test_custom_bot.py index 51dc7d614..73bac81fa 100644 --- a/backend/tests/test_repositories/test_custom_bot.py +++ b/backend/tests/test_repositories/test_custom_bot.py @@ -89,6 +89,7 @@ def test_store_and_find_bot(self): ConversationQuickStarterModel(title="QS title", example="QS example") ], bedrock_knowledge_base=BedrockKnowledgeBaseModel( + type="dedicated", embeddings_model="titan_v2", open_search=OpenSearchParamsModel( analyzer=AnalyzerParamsModel( @@ -159,6 +160,7 @@ def test_store_and_find_bot(self): self.assertEqual(len(bot.conversation_quick_starters), 1) self.assertEqual(bot.conversation_quick_starters[0].title, "QS title") self.assertEqual(bot.conversation_quick_starters[0].example, "QS example") + self.assertEqual(bot.bedrock_knowledge_base.type, "dedicated") self.assertEqual(bot.bedrock_knowledge_base.embeddings_model, "titan_v2") self.assertEqual( bot.bedrock_knowledge_base.chunking_configuration.max_tokens, 2000 @@ -275,6 +277,7 @@ def test_update_knowledge_base_id(self): False, "user1", bedrock_knowledge_base=BedrockKnowledgeBaseModel( + type="dedicated", embeddings_model="titan_v2", open_search=OpenSearchParamsModel( analyzer=AnalyzerParamsModel( @@ -339,6 +342,7 @@ def test_update_bot(self): ConversationQuickStarterModel(title="QS title", example="QS example") ], bedrock_knowledge_base=BedrockKnowledgeBaseModel( + type="dedicated", embeddings_model="titan_v2", open_search=OpenSearchParamsModel( analyzer=AnalyzerParamsModel( @@ -397,6 +401,7 @@ def test_update_bot(self): self.assertEqual(bot.conversation_quick_starters[0].title, "QS title") self.assertEqual(bot.conversation_quick_starters[0].example, "QS example") + self.assertEqual(bot.bedrock_knowledge_base.type, "dedicated") self.assertEqual(bot.bedrock_knowledge_base.embeddings_model, "titan_v2") self.assertEqual( bot.bedrock_knowledge_base.chunking_configuration.max_tokens, 2500 diff --git a/backend/tests/test_repositories/test_models/test_bot.py b/backend/tests/test_repositories/test_models/test_bot.py index 9493fd52c..4c798c299 100644 --- a/backend/tests/test_repositories/test_models/test_bot.py +++ b/backend/tests/test_repositories/test_models/test_bot.py @@ -128,6 +128,7 @@ def setUp(self) -> None: ], bedrock_knowledge_base=( BedrockKnowledgeBaseModel( + type="dedicated", embeddings_model="titan_v2", open_search=OpenSearchParamsModel(analyzer=None), chunking_configuration=None, diff --git a/backend/tests/test_repositories/utils/bot_factory.py b/backend/tests/test_repositories/utils/bot_factory.py index 5403e07cd..610de1b93 100644 --- a/backend/tests/test_repositories/utils/bot_factory.py +++ b/backend/tests/test_repositories/utils/bot_factory.py @@ -124,6 +124,7 @@ def _create_test_bot_model( ), bedrock_knowledge_base=( BedrockKnowledgeBaseModel( + type="dedicated", embeddings_model="titan_v2", open_search=OpenSearchParamsModel(analyzer=None), chunking_configuration=None, diff --git a/cdk/bin/bedrock-custom-bot.ts b/cdk/bin/bedrock-custom-bot.ts index 71d7206bc..00bb53013 100644 --- a/cdk/bin/bedrock-custom-bot.ts +++ b/cdk/bin/bedrock-custom-bot.ts @@ -2,6 +2,7 @@ import "source-map-support/register"; import * as cdk from "aws-cdk-lib"; import { BedrockCustomBotStack } from "../lib/bedrock-custom-bot-stack"; import { + getKnowledgeBaseType, getEmbeddingModel, getChunkingStrategy, getAnalyzer, @@ -40,10 +41,12 @@ interface BaseConfig { } interface KnowledgeConfig { + knowledgeBaseType: "dedicated" | "shared" | undefined; embeddingsModel: BedrockFoundationModel; parsingModel: BedrockFoundationModel | undefined; existKnowledgeBaseId?: string; existingS3Urls: string[]; + filenames: string[]; sourceUrls: string[]; instruction?: string; analyzer?: Analyzer | undefined; @@ -90,10 +93,12 @@ const baseConfig: BaseConfig = { }; const knowledgeConfig: KnowledgeConfig = { + knowledgeBaseType: getKnowledgeBaseType(knowledgeBaseJson.type), 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), + filenames: knowledgeJson.filenames.L.map((filename: any) => filename.S), sourceUrls: knowledgeJson.source_urls.L.map((sourceUrl: any) => sourceUrl.S), instruction: knowledgeBaseJson.instruction?.S, analyzer: knowledgeBaseJson.open_search.M.analyzer.M @@ -236,10 +241,12 @@ new BedrockCustomBotStack(app, `BrChatKbStack${baseConfig.botId}`, { enableRagReplicas: baseConfig.enableRagReplicas, // Knowledge base configuration + knowledgeBaseType: knowledgeConfig.knowledgeBaseType, embeddingsModel: knowledgeConfig.embeddingsModel, parsingModel: knowledgeConfig.parsingModel, existKnowledgeBaseId: knowledgeConfig.existKnowledgeBaseId, existingS3Urls: knowledgeConfig.existingS3Urls, + filenames: knowledgeConfig.filenames, sourceUrls: knowledgeConfig.sourceUrls, instruction: knowledgeConfig.instruction, analyzer: knowledgeConfig.analyzer, diff --git a/cdk/lib/bedrock-chat-stack.ts b/cdk/lib/bedrock-chat-stack.ts index fbb218466..25bbea288 100644 --- a/cdk/lib/bedrock-chat-stack.ts +++ b/cdk/lib/bedrock-chat-stack.ts @@ -264,7 +264,6 @@ export class BedrockChatStack extends cdk.Stack { const websocket = new WebSocket(this, "WebSocket", { accessLogBucket, database, - websocketSessionTable: database.websocketSessionTable, auth, bedrockRegion: props.bedrockRegion, largeMessageBucket, diff --git a/cdk/lib/bedrock-custom-bot-stack.ts b/cdk/lib/bedrock-custom-bot-stack.ts index c6747b266..7f5935eef 100644 --- a/cdk/lib/bedrock-custom-bot-stack.ts +++ b/cdk/lib/bedrock-custom-bot-stack.ts @@ -8,7 +8,7 @@ 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 { BedrockFoundationModel, VectorStoreType } 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 { @@ -16,11 +16,10 @@ import { 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 { ParsingStrategy } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock/data-sources/parsing"; import { - KnowledgeBase, - IKnowledgeBase, + VectorKnowledgeBase, } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock"; import { aws_bedrock as bedrock } from "aws-cdk-lib"; import { @@ -54,10 +53,12 @@ interface BedrockCustomBotStackProps extends StackProps { readonly enableRagReplicas?: boolean; // Knowledge base configuration + readonly knowledgeBaseType: "dedicated" | "shared" | undefined; readonly embeddingsModel: BedrockFoundationModel; readonly parsingModel?: BedrockFoundationModel; readonly existKnowledgeBaseId: string | undefined; readonly existingS3Urls: string[]; + readonly filenames: string[]; readonly sourceUrls: string[]; readonly instruction?: string; readonly analyzer?: Analyzer; @@ -81,216 +82,105 @@ export class BedrockCustomBotStack extends Stack { const { docBucketsAndPrefixes } = this.setupBucketsAndPrefixes(props); - let kb: IKnowledgeBase; - // if knowledge base arn does not exist if (props.existKnowledgeBaseId == undefined) { - const vectorCollection = new VectorCollection(this, "KBVectors", { - collectionName: `kb-${props.botId.slice(0, 20).toLowerCase()}`, - standbyReplicas: - props.enableRagReplicas === true - ? VectorCollectionStandbyReplicas.ENABLED - : VectorCollectionStandbyReplicas.DISABLED, - }); - const vectorIndex = new VectorIndex(this, "KBIndex", { - collection: vectorCollection, - // DO NOT CHANGE THIS VALUE - indexName: "bedrock-knowledge-base-default-index", - // DO NOT CHANGE THIS VALUE - vectorField: "bedrock-knowledge-base-default-vector", - vectorDimensions: props.embeddingsModel.vectorDimensions!, - mappings: [ - { - mappingField: "AMAZON_BEDROCK_TEXT_CHUNK", - dataType: "text", - filterable: true, - }, - { - mappingField: "AMAZON_BEDROCK_METADATA", - dataType: "text", - filterable: false, - }, - ], - analyzer: props.analyzer, - }); - - kb = new KnowledgeBase(this, "KB", { - embeddingsModel: props.embeddingsModel, - vectorStore: vectorCollection, - vectorIndex: vectorIndex, - instruction: props.instruction, - }); + if ((props.knowledgeBaseType == null || props.knowledgeBaseType === "dedicated") + && (docBucketsAndPrefixes.length > 0 || props.sourceUrls.length > 0) + ) { + const vectorCollection = new VectorCollection(this, "VectorCollection", { + standbyReplicas: + props.enableRagReplicas === true + ? VectorCollectionStandbyReplicas.ENABLED + : VectorCollectionStandbyReplicas.DISABLED, + }); + const vectorIndex = new VectorIndex(this, "VectorIndex", { + collection: vectorCollection, + // DO NOT CHANGE THIS VALUE + indexName: "bedrock-knowledge-base-default-index", + // DO NOT CHANGE THIS VALUE + vectorField: "bedrock-knowledge-base-default-vector", + vectorDimensions: props.embeddingsModel.vectorDimensions!, + precision: "float", + distanceType: "l2", + mappings: [ + { + mappingField: "AMAZON_BEDROCK_TEXT_CHUNK", + dataType: "text", + filterable: true, + }, + { + mappingField: "AMAZON_BEDROCK_METADATA", + dataType: "text", + filterable: false, + }, + ], + analyzer: props.analyzer, + }); + vectorIndex.node.addDependency(vectorCollection); - const dataSources = docBucketsAndPrefixes.map(({ bucket, prefix }) => { - bucket.grantRead(kb.role); - const inclusionPrefixes = prefix === "" ? undefined : [prefix]; - return new S3DataSource(this, `DataSource${prefix}`, { - bucket: bucket, - knowledgeBase: kb, - dataSourceName: bucket.bucketName, - chunkingStrategy: props.chunkingStrategy, - parsingStrategy: props.parsingModel - ? ParsingStategy.foundationModel({ - parsingModel: props.parsingModel.asIModel(this), - }) - : undefined, - inclusionPrefixes: inclusionPrefixes, + const kb = new VectorKnowledgeBase(this, "KnowledgeBase", { + embeddingsModel: props.embeddingsModel, + vectorStore: vectorCollection, + vectorIndex: vectorIndex, + instruction: props.instruction, + }); + new CfnOutput(this, "KnowledgeBaseId", { + value: kb.knowledgeBaseId, + }); + new CfnOutput(this, "KnowledgeBaseArn", { + value: kb.knowledgeBaseArn, }); - }); - // Add Web Crawler Data Sources - if (props.sourceUrls.length > 0) { - const webCrawlerDataSource = new WebCrawlerDataSource( - this, - "WebCrawlerDataSource", - { + const dataSources = docBucketsAndPrefixes.map(({ bucket, prefix }) => { + bucket.grantRead(kb.role); + const inclusionPrefixes = prefix === "" ? undefined : [prefix]; + return new S3DataSource(this, `DataSource${prefix}`, { + bucket: bucket, knowledgeBase: kb, - sourceUrls: props.sourceUrls, + dataSourceName: bucket.bucketName, chunkingStrategy: props.chunkingStrategy, parsingStrategy: props.parsingModel - ? ParsingStategy.foundationModel({ - parsingModel: props.parsingModel.asIModel(this), + ? ParsingStrategy.foundationModel({ + parsingModel: props.parsingModel, }) : undefined, - crawlingScope: props.crawlingScope, - filters: { - excludePatterns: props.crawlingFilters?.excludePatterns, - includePatterns: props.crawlingFilters?.includePatterns, - }, - } - ); - new CfnOutput(this, "DataSourceIdWebCrawler", { - value: webCrawlerDataSource.dataSourceId, - }); - } - - if (props.guardrail?.is_guardrail_enabled == true) { - // Use only parameters with a value greater than or equal to 0 - let contentPolicyConfigFiltersConfig = []; - let contextualGroundingFiltersConfig = []; - console.log("props.guardrail: ", props.guardrail); - - if ( - props.guardrail.hateThreshold != undefined && - props.guardrail.hateThreshold > 0 - ) { - contentPolicyConfigFiltersConfig.push({ - inputStrength: getThreshold(props.guardrail.hateThreshold), - outputStrength: getThreshold(props.guardrail.hateThreshold), - type: "HATE", - }); - } - - if ( - props.guardrail.insultsThreshold != undefined && - props.guardrail.insultsThreshold > 0 - ) { - contentPolicyConfigFiltersConfig.push({ - inputStrength: getThreshold(props.guardrail.insultsThreshold), - outputStrength: getThreshold(props.guardrail.insultsThreshold), - type: "INSULTS", - }); - } - - if ( - props.guardrail.sexualThreshold != undefined && - props.guardrail.sexualThreshold > 0 - ) { - contentPolicyConfigFiltersConfig.push({ - inputStrength: getThreshold(props.guardrail.sexualThreshold), - outputStrength: getThreshold(props.guardrail.sexualThreshold), - type: "SEXUAL", + inclusionPrefixes: inclusionPrefixes, }); - } - - if ( - props.guardrail.violenceThreshold != undefined && - props.guardrail.violenceThreshold > 0 - ) { - contentPolicyConfigFiltersConfig.push({ - inputStrength: getThreshold(props.guardrail.violenceThreshold), - outputStrength: getThreshold(props.guardrail.violenceThreshold), - type: "VIOLENCE", - }); - } - - if ( - props.guardrail.misconductThreshold != undefined && - props.guardrail.misconductThreshold > 0 - ) { - contentPolicyConfigFiltersConfig.push({ - inputStrength: getThreshold(props.guardrail.misconductThreshold), - outputStrength: getThreshold(props.guardrail.misconductThreshold), - type: "MISCONDUCT", - }); - } - - if ( - props.guardrail.groundingThreshold != undefined && - props.guardrail.groundingThreshold > 0 - ) { - contextualGroundingFiltersConfig.push({ - threshold: props.guardrail.groundingThreshold!, - type: "GROUNDING", - }); - } - - if ( - props.guardrail.relevanceThreshold != undefined && - props.guardrail.relevanceThreshold > 0 - ) { - contextualGroundingFiltersConfig.push({ - threshold: props.guardrail.relevanceThreshold!, - type: "RELEVANCE", - }); - } + }); - console.log( - "contentPolicyConfigFiltersConfig: ", - contentPolicyConfigFiltersConfig - ); - console.log( - "contextualGroundingFiltersConfig: ", - contextualGroundingFiltersConfig - ); - - // Deploy Guardrail if it contains at least one configuration value - if ( - contentPolicyConfigFiltersConfig.length > 0 || - contextualGroundingFiltersConfig.length > 0 - ) { - const guardrail = new bedrock.CfnGuardrail(this, "Guardrail", { - name: props.botId, - blockedInputMessaging: BLOCKED_INPUT_MESSAGE, - blockedOutputsMessaging: BLOCKED_OUTPUT_MESSAGE, - contentPolicyConfig: - contentPolicyConfigFiltersConfig.length > 0 - ? { - filtersConfig: contentPolicyConfigFiltersConfig, - } - : undefined, - contextualGroundingPolicyConfig: - contextualGroundingFiltersConfig.length > 0 - ? { - filtersConfig: contextualGroundingFiltersConfig, - } + // 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 + ? ParsingStrategy.foundationModel({ + parsingModel: props.parsingModel, + }) : undefined, - }); - new CfnOutput(this, "GuardrailArn", { - value: guardrail.attrGuardrailArn, - }); - new CfnOutput(this, "GuardrailVersion", { - value: guardrail.attrVersion, + crawlingScope: props.crawlingScope, + filters: { + excludePatterns: props.crawlingFilters?.excludePatterns, + includePatterns: props.crawlingFilters?.includePatterns, + }, + } + ); + new CfnOutput(this, "DataSourceIdWebCrawler", { + value: webCrawlerDataSource.dataSourceId, }); } - } - // This output is used by Sfn to synchronize KB data. - dataSources.forEach((dataSource, index) => { - new CfnOutput(this, `DataSource${index}`, { - value: dataSource.dataSourceId, + // This output is used by Sfn to synchronize KB data. + dataSources.forEach((dataSource, index) => { + new CfnOutput(this, `DataSource${index}`, { + value: dataSource.dataSourceId, + }); }); - }); + } } else { // if knowledgeBaseArn exists const getKnowledgeBase = new AwsCustomResource(this, "GetKnowledgeBase", { @@ -314,18 +204,140 @@ export class BedrockCustomBotStack extends Stack { const executionRoleArn = getKnowledgeBase.getResponseField("roleArn"); - kb = KnowledgeBase.fromKnowledgeBaseAttributes(this, "MyKnowledgeBase", { + const kb = VectorKnowledgeBase.fromKnowledgeBaseAttributes(this, "MyKnowledgeBase", { + vectorStoreType: VectorStoreType.OPENSEARCH_SERVERLESS, knowledgeBaseId: props.existKnowledgeBaseId, executionRoleArn: executionRoleArn, }); + new CfnOutput(this, "KnowledgeBaseId", { + value: kb.knowledgeBaseId, + }); + new CfnOutput(this, "KnowledgeBaseArn", { + value: kb.knowledgeBaseArn, + }); + } + + if (props.guardrail?.is_guardrail_enabled == true) { + // Use only parameters with a value greater than or equal to 0 + let contentPolicyConfigFiltersConfig = []; + let contextualGroundingFiltersConfig = []; + console.log("props.guardrail: ", props.guardrail); + + if ( + props.guardrail.hateThreshold != undefined && + props.guardrail.hateThreshold > 0 + ) { + contentPolicyConfigFiltersConfig.push({ + inputStrength: getThreshold(props.guardrail.hateThreshold), + outputStrength: getThreshold(props.guardrail.hateThreshold), + type: "HATE", + }); + } + + if ( + props.guardrail.insultsThreshold != undefined && + props.guardrail.insultsThreshold > 0 + ) { + contentPolicyConfigFiltersConfig.push({ + inputStrength: getThreshold(props.guardrail.insultsThreshold), + outputStrength: getThreshold(props.guardrail.insultsThreshold), + type: "INSULTS", + }); + } + + if ( + props.guardrail.sexualThreshold != undefined && + props.guardrail.sexualThreshold > 0 + ) { + contentPolicyConfigFiltersConfig.push({ + inputStrength: getThreshold(props.guardrail.sexualThreshold), + outputStrength: getThreshold(props.guardrail.sexualThreshold), + type: "SEXUAL", + }); + } + + if ( + props.guardrail.violenceThreshold != undefined && + props.guardrail.violenceThreshold > 0 + ) { + contentPolicyConfigFiltersConfig.push({ + inputStrength: getThreshold(props.guardrail.violenceThreshold), + outputStrength: getThreshold(props.guardrail.violenceThreshold), + type: "VIOLENCE", + }); + } + + if ( + props.guardrail.misconductThreshold != undefined && + props.guardrail.misconductThreshold > 0 + ) { + contentPolicyConfigFiltersConfig.push({ + inputStrength: getThreshold(props.guardrail.misconductThreshold), + outputStrength: getThreshold(props.guardrail.misconductThreshold), + type: "MISCONDUCT", + }); + } + + if ( + props.guardrail.groundingThreshold != undefined && + props.guardrail.groundingThreshold > 0 + ) { + contextualGroundingFiltersConfig.push({ + threshold: props.guardrail.groundingThreshold!, + type: "GROUNDING", + }); + } + + if ( + props.guardrail.relevanceThreshold != undefined && + props.guardrail.relevanceThreshold > 0 + ) { + contextualGroundingFiltersConfig.push({ + threshold: props.guardrail.relevanceThreshold!, + type: "RELEVANCE", + }); + } + + console.log( + "contentPolicyConfigFiltersConfig: ", + contentPolicyConfigFiltersConfig + ); + console.log( + "contextualGroundingFiltersConfig: ", + contextualGroundingFiltersConfig + ); + + // Deploy Guardrail if it contains at least one configuration value + if ( + contentPolicyConfigFiltersConfig.length > 0 || + contextualGroundingFiltersConfig.length > 0 + ) { + const guardrail = new bedrock.CfnGuardrail(this, "Guardrail", { + name: props.botId, + blockedInputMessaging: BLOCKED_INPUT_MESSAGE, + blockedOutputsMessaging: BLOCKED_OUTPUT_MESSAGE, + contentPolicyConfig: + contentPolicyConfigFiltersConfig.length > 0 + ? { + filtersConfig: contentPolicyConfigFiltersConfig, + } + : undefined, + contextualGroundingPolicyConfig: + contextualGroundingFiltersConfig.length > 0 + ? { + filtersConfig: contextualGroundingFiltersConfig, + } + : undefined, + }); + new CfnOutput(this, "GuardrailArn", { + value: guardrail.attrGuardrailArn, + }); + new CfnOutput(this, "GuardrailVersion", { + value: guardrail.attrVersion, + }); + } } - new CfnOutput(this, "KnowledgeBaseId", { - value: kb.knowledgeBaseId, - }); - new CfnOutput(this, "KnowledgeBaseArn", { - value: kb.knowledgeBaseArn, - }); new CfnOutput(this, "OwnerUserId", { value: props.ownerUserId, }); @@ -349,15 +361,17 @@ export class BedrockCustomBotStack extends Stack { */ const docBucketsAndPrefixes: { bucket: s3.IBucket; prefix: string }[] = []; - // Always add the default bucket with its default prefix - docBucketsAndPrefixes.push({ - bucket: s3.Bucket.fromBucketName( - this, - props.bedrockClaudeChatDocumentBucketName, - props.bedrockClaudeChatDocumentBucketName - ), - prefix: `${props.ownerUserId}/${props.botId}/documents/`, - }); + // Add the default bucket with its default prefix if needed. + if (props.filenames && props.filenames.length > 0) { + docBucketsAndPrefixes.push({ + bucket: s3.Bucket.fromBucketName( + this, + props.bedrockClaudeChatDocumentBucketName, + props.bedrockClaudeChatDocumentBucketName + ), + prefix: `${props.ownerUserId}/${props.botId}/documents/`, + }); + } if (props.existingS3Urls && props.existingS3Urls.length > 0) { props.existingS3Urls.forEach((url) => { diff --git a/cdk/lib/constructs/database.ts b/cdk/lib/constructs/database.ts index 9faa9c89c..a559e8360 100644 --- a/cdk/lib/constructs/database.ts +++ b/cdk/lib/constructs/database.ts @@ -79,6 +79,18 @@ export class Database extends Construct { indexName: "ItemTypeIndex", partitionKey: { name: "ItemType", type: AttributeType.STRING }, }); + // GSI-4 + botTable.addGlobalSecondaryIndex({ + indexName: "KnowledgeBaseTypeIndex", + partitionKey: { + name: "BedrockKnowledgeBaseType", + type: AttributeType.STRING, + }, + sortKey: { + name: "BedrockKnowledgeBaseHash", + type: AttributeType.STRING, + }, + }); const tableAccessRole = new Role(this, "TableAccessRole", { assumedBy: new AccountPrincipal(Stack.of(this).account), diff --git a/cdk/lib/constructs/websocket.ts b/cdk/lib/constructs/websocket.ts index fc49ccbec..ee2c631ed 100644 --- a/cdk/lib/constructs/websocket.ts +++ b/cdk/lib/constructs/websocket.ts @@ -20,7 +20,6 @@ export interface WebSocketProps { readonly auth: Auth; readonly bedrockRegion: string; readonly documentBucket: s3.IBucket; - readonly websocketSessionTable: ITable; readonly largeMessageBucket: s3.IBucket; readonly accessLogBucket?: s3.Bucket; readonly enableBedrockCrossRegionInference: boolean; @@ -100,7 +99,7 @@ export class WebSocket extends Construct { ); largePayloadSupportBucket.grantRead(handlerRole); - props.websocketSessionTable.grantReadWriteData(handlerRole); + database.websocketSessionTable.grantReadWriteData(handlerRole); props.largeMessageBucket.grantReadWrite(handlerRole); props.documentBucket.grantRead(handlerRole); @@ -125,7 +124,7 @@ export class WebSocket extends Construct { TABLE_ACCESS_ROLE_ARN: tableAccessRole.roleArn, LARGE_MESSAGE_BUCKET: props.largeMessageBucket.bucketName, LARGE_PAYLOAD_SUPPORT_BUCKET: largePayloadSupportBucket.bucketName, - WEBSOCKET_SESSION_TABLE_NAME: props.websocketSessionTable.tableName, + WEBSOCKET_SESSION_TABLE_NAME: database.websocketSessionTable.tableName, ENABLE_BEDROCK_CROSS_REGION_INFERENCE: props.enableBedrockCrossRegionInference.toString(), }, diff --git a/cdk/lib/utils/bedrock-knowledge-base-args.ts b/cdk/lib/utils/bedrock-knowledge-base-args.ts index facd17bb5..8f67104ac 100644 --- a/cdk/lib/utils/bedrock-knowledge-base-args.ts +++ b/cdk/lib/utils/bedrock-knowledge-base-args.ts @@ -28,6 +28,28 @@ interface SemanticOptions { readonly breakpointPercentileThreshold: number; } +export const getKnowledgeBaseType = ( + type: unknown +): "dedicated" | "shared" | undefined => { + if (type == null || typeof(type) !== "object") { + return undefined; + } + + if ("S" in type) { + const value = type["S"]; + switch(value) { + case "dedicated": + case "shared": + return value; + + default: + return undefined; + } + } + + return undefined; +}; + export const getEmbeddingModel = ( embeddingsModel: string ): BedrockFoundationModel => { diff --git a/cdk/package-lock.json b/cdk/package-lock.json index 553f1c850..426ac65cc 100644 --- a/cdk/package-lock.json +++ b/cdk/package-lock.json @@ -48,9 +48,9 @@ } }, "node_modules/@aws-cdk/asset-awscli-v1": { - "version": "2.2.232", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.232.tgz", - "integrity": "sha512-x9aFQG9gA+RgGj9bGB+WC6y1Nq2/Y8R2yXFoKWoQZOet8PRFJ8M5/FeXoh9XmdWI4weJVctLU4WTIve6rOvPtA==", + "version": "2.2.242", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.242.tgz", + "integrity": "sha512-4c1bAy2ISzcdKXYS1k4HYZsNrgiwbiDzj36ybwFVxEWZXVAP0dimQTCaB9fxu7sWzEjw3d+eaw6Fon+QTfTIpQ==", "license": "Apache-2.0" }, "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { @@ -85,9 +85,9 @@ } }, "node_modules/@aws-cdk/cloud-assembly-schema": { - "version": "41.2.0", - "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-41.2.0.tgz", - "integrity": "sha512-JaulVS6z9y5+u4jNmoWbHZRs9uGOnmn/ktXygNWKNu1k6lF3ad4so3s18eRu15XCbUIomxN9WPYT6Ehh7hzONw==", + "version": "48.14.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-48.14.0.tgz", + "integrity": "sha512-mo1MaPKsNVh3QSuosREcwIh8TrVZ9deHVLAjzbjkQuTxb/7LGqkf1E5HHxlAt6Z0+z+DZt7dTPIJ92NcR0tjDQ==", "bundleDependencies": [ "jsonschema", "semver" @@ -95,10 +95,10 @@ "license": "Apache-2.0", "dependencies": { "jsonschema": "~1.4.1", - "semver": "^7.7.1" + "semver": "^7.7.2" }, "engines": { - "node": ">= 14.15.0" + "node": ">= 18.0.0" } }, "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { @@ -110,7 +110,7 @@ } }, "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { - "version": "7.7.1", + "version": "7.7.2", "inBundle": true, "license": "ISC", "bin": { @@ -125,7 +125,6 @@ "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", @@ -141,7 +140,6 @@ "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -154,7 +152,6 @@ "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" @@ -168,7 +165,6 @@ "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" @@ -182,7 +178,6 @@ "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", @@ -197,7 +192,6 @@ "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" } @@ -207,7 +201,6 @@ "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", @@ -219,7 +212,6 @@ "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -232,7 +224,6 @@ "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" @@ -246,7 +237,6 @@ "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" @@ -329,7 +319,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.693.0.tgz", "integrity": "sha512-QEynrBC26x6TG9ZMzApR/kZ3lmt4lEIs2D+cHuDxt6fDGzahBUsQFBwJqhizzsM97JJI5YvmJhmihoYjdSSaXA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -485,7 +474,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.693.0.tgz", "integrity": "sha512-v6Z/kWmLFqRLDPEwl9hJGhtTgIFHjZugSfF1Yqffdxf4n1AWgtHS7qSegakuMyN5pP4K2tvUD8qHJ+gGe2Bw2A==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "3.692.0", "@smithy/core": "^2.5.2", @@ -508,7 +496,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.693.0.tgz", "integrity": "sha512-hMUZaRSF7+iBKZfBHNLihFs9zvpM1CB8MBOTnTp5NGCVkRYF3SB2LH+Kcippe0ats4qCyB1eEoyQX99rERp2iQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/core": "3.693.0", "@aws-sdk/types": "3.692.0", @@ -525,7 +512,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.693.0.tgz", "integrity": "sha512-sL8MvwNJU7ZpD7/d2VVb3by1GknIJUxzTIgYtVkDVA/ojo+KRQSSHxcj0EWWXF5DTSh2Tm+LrEug3y1ZyKHsDA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/core": "3.693.0", "@aws-sdk/types": "3.692.0", @@ -547,7 +533,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.693.0.tgz", "integrity": "sha512-kvaa4mXhCCOuW7UQnBhYqYfgWmwy7WSBSDClutwSLPZvgrhYj2l16SD2lN4IfYdxARYMJJ1lFYp3/jJG/9Yk4Q==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/core": "3.693.0", "@aws-sdk/credential-provider-env": "3.693.0", @@ -574,7 +559,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.693.0.tgz", "integrity": "sha512-42WMsBjTNnjYxYuM3qD/Nq+8b7UdMopUq5OduMDxoM3mFTV6PXMMnfI4Z1TNnR4tYRvPXAnuNltF6xmjKbSJRA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/credential-provider-env": "3.693.0", "@aws-sdk/credential-provider-http": "3.693.0", @@ -598,7 +582,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.693.0.tgz", "integrity": "sha512-cvxQkrTWHHjeHrPlj7EWXPnFSq8x7vMx+Zn1oTsMpCY445N9KuzjfJTkmNGwU2GT6rSZI9/0MM02aQvl5bBBTQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/core": "3.693.0", "@aws-sdk/types": "3.692.0", @@ -616,7 +599,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.693.0.tgz", "integrity": "sha512-479UlJxY+BFjj3pJFYUNC0DCMrykuG7wBAXfsvZqQxKUa83DnH5Q1ID/N2hZLkxjGd4ZW0AC3lTOMxFelGzzpQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/client-sso": "3.693.0", "@aws-sdk/core": "3.693.0", @@ -636,7 +618,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.693.0.tgz", "integrity": "sha512-8LB210Pr6VeCiSb2hIra+sAH4KUBLyGaN50axHtIgufVK8jbKIctTZcVY5TO9Se+1107TsruzeXS7VeqVdJfFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/core": "3.693.0", "@aws-sdk/types": "3.692.0", @@ -656,7 +637,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/endpoint-cache/-/endpoint-cache-3.693.0.tgz", "integrity": "sha512-/zK0ZZncBf5FbTfo8rJMcQIXXk4Ibhe5zEMiwFNivVPR2uNC0+oqfwXz7vjxwY0t6BPE3Bs4h9uFEz4xuGCY6w==", "license": "Apache-2.0", - "peer": true, "dependencies": { "mnemonist": "0.38.3", "tslib": "^2.6.2" @@ -670,7 +650,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.693.0.tgz", "integrity": "sha512-OG8WM8OzYuAt3Ueb8TZoBgA+vqNgPaXksHhiy8SFTQxNamSMMRvKrDSBbdUuV96mq0lcJq1mFgJ4oRXJ1HPh6A==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/endpoint-cache": "3.693.0", "@aws-sdk/types": "3.692.0", @@ -688,7 +667,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.693.0.tgz", "integrity": "sha512-BCki6sAZ5jYwIN/t3ElCiwerHad69ipHwPsDCxJQyeiOnJ8HG+lEpnVIfrnI8A0fLQNSF3Gtx6ahfBpKiv1Oug==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "3.692.0", "@smithy/protocol-http": "^4.1.6", @@ -704,7 +682,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.693.0.tgz", "integrity": "sha512-dXnXDPr+wIiJ1TLADACI1g9pkSB21KkMIko2u4CJ2JCBoxi5IqeTnVoa6YcC8GdFNVRl+PorZ3Zqfmf1EOTC6w==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "3.692.0", "@smithy/types": "^3.7.0", @@ -719,7 +696,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.693.0.tgz", "integrity": "sha512-0LDmM+VxXp0u3rG0xQRWD/q6Ubi7G8I44tBPahevD5CaiDZTkmNTrVUf0VEJgVe0iCKBppACMBDkLB0/ETqkFw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "3.692.0", "@smithy/protocol-http": "^4.1.6", @@ -735,7 +711,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.693.0.tgz", "integrity": "sha512-/KUq/KEpFFbQmNmpp7SpAtFAdViquDfD2W0QcG07zYBfz9MwE2ig48ALynXm5sMpRmnG7sJXjdvPtTsSVPfkiw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/core": "3.693.0", "@aws-sdk/types": "3.692.0", @@ -754,7 +729,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.693.0.tgz", "integrity": "sha512-YLUkMsUY0GLW/nfwlZ69cy1u07EZRmsv8Z9m0qW317/EZaVx59hcvmcvb+W4bFqj5E8YImTjoGfE4cZ0F9mkyw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "3.692.0", "@smithy/node-config-provider": "^3.1.10", @@ -772,7 +746,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.693.0.tgz", "integrity": "sha512-nDBTJMk1l/YmFULGfRbToOA2wjf+FkQT4dMgYCv+V9uSYsMzQj8A7Tha2dz9yv4vnQgYaEiErQ8d7HVyXcVEoA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "3.692.0", "@smithy/property-provider": "^3.1.9", @@ -792,7 +765,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.692.0.tgz", "integrity": "sha512-RpNvzD7zMEhiKgmlxGzyXaEcg2khvM7wd5sSHVapOcrde1awQSOMGI4zKBQ+wy5TnDfrm170ROz/ERLYtrjPZA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^3.7.0", "tslib": "^2.6.2" @@ -821,7 +793,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.693.0.tgz", "integrity": "sha512-eo4F6DRQ/kxS3gxJpLRv+aDNy76DxQJL5B3DPzpr9Vkq0ygVoi4GT5oIZLVaAVIJmi6k5qq9dLsYZfWLUxJJSg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "3.692.0", "@smithy/types": "^3.7.0", @@ -837,7 +808,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.693.0.tgz", "integrity": "sha512-ttrag6haJLWABhLqtg1Uf+4LgHWIMOVSYL+VYZmAp2v4PUGOwWmWQH0Zk8RM7YuQcLfH/EoR72/Yxz6A4FKcuw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -850,7 +820,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.693.0.tgz", "integrity": "sha512-6EUfuKOujtddy18OLJUaXfKBgs+UcbZ6N/3QV4iOkubCUdeM1maIqs++B9bhCbWeaeF5ORizJw5FTwnyNjE/mw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "3.692.0", "@smithy/types": "^3.7.0", @@ -863,7 +832,6 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.693.0.tgz", "integrity": "sha512-td0OVX8m5ZKiXtecIDuzY3Y3UZIzvxEr57Hp21NOwieqKCG2UeyQWWeGPv0FQaU7dpTkvFmVNI+tx9iB8V/Nhg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/middleware-user-agent": "3.693.0", "@aws-sdk/types": "3.692.0", @@ -912,6 +880,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.9.tgz", "integrity": "sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.5", @@ -1368,24 +1337,39 @@ "dev": true }, "node_modules/@cdklabs/generative-ai-cdk-constructs": { - "version": "0.1.275", - "resolved": "https://registry.npmjs.org/@cdklabs/generative-ai-cdk-constructs/-/generative-ai-cdk-constructs-0.1.275.tgz", - "integrity": "sha512-k91LaO4jhP4Y4YhZmO0qGxHkrmXFM7C0B2lllYKMQl0MgEB1/pOq1ccKHfoo0vjXPYX3J0TYk4PkxkrII3N4NQ==", + "version": "0.1.310", + "resolved": "https://registry.npmjs.org/@cdklabs/generative-ai-cdk-constructs/-/generative-ai-cdk-constructs-0.1.310.tgz", + "integrity": "sha512-81njM9LN226ehtsCo4aqj4YhfBncB+gEi3tHKuskBERI32eYp7Cakh8oP4doZIoQLHE3uSy9g68+3SsDw9Ehog==", "bundleDependencies": [ + "@aws-cdk/aws-lambda-python-alpha", "deepmerge" ], + "license": "Apache-2.0", "dependencies": { - "cdk-nag": "^2.29.8", + "@aws-cdk/aws-lambda-python-alpha": "2.219.0-alpha.0", + "cdk-nag": "^2.37.47", "deepmerge": "^4.3.1" }, "engines": { - "node": ">= 18.12.0 <= 20.x" + "node": ">= 18.12.0 <= 22.x" }, "peerDependencies": { - "aws-cdk-lib": "^2.162.1", + "aws-cdk-lib": "^2.219.0", "constructs": "^10.3.0" } }, + "node_modules/@cdklabs/generative-ai-cdk-constructs/node_modules/@aws-cdk/aws-lambda-python-alpha": { + "version": "2.219.0-alpha.0", + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.219.0", + "constructs": "^10.0.0" + } + }, "node_modules/@cdklabs/generative-ai-cdk-constructs/node_modules/deepmerge": { "version": "4.3.1", "inBundle": true, @@ -1802,7 +1786,6 @@ "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.8.tgz", "integrity": "sha512-+3DOBcUn5/rVjlxGvUPKc416SExarAQ+Qe0bqk30YSUjbepwpS7QN0cyKUSifvLJhdMZ0WPzPP5ymut0oonrpQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^3.7.1", "tslib": "^2.6.2" @@ -1816,7 +1799,6 @@ "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-3.0.12.tgz", "integrity": "sha512-YAJP9UJFZRZ8N+UruTeq78zkdjUHmzsY62J4qKWZ4SXB4QXJ/+680EfXXgkYA2xj77ooMqtUY9m406zGNqwivQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/node-config-provider": "^3.1.11", "@smithy/types": "^3.7.1", @@ -1833,7 +1815,6 @@ "resolved": "https://registry.npmjs.org/@smithy/core/-/core-2.5.3.tgz", "integrity": "sha512-96uW8maifUSmehaeW7uydWn7wBc98NEeNI3zN8vqakGpyCQgzyJaA64Z4FCOUmAdCJkhppd/7SZ798Fo4Xx37g==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/middleware-serde": "^3.0.10", "@smithy/protocol-http": "^4.1.7", @@ -1853,7 +1834,6 @@ "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.2.7.tgz", "integrity": "sha512-cEfbau+rrWF8ylkmmVAObOmjbTIzKyUC5TkBL58SbLywD0RCBC4JAUKbmtSm2w5KUJNRPGgpGFMvE2FKnuNlWQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/node-config-provider": "^3.1.11", "@smithy/property-provider": "^3.1.10", @@ -1870,7 +1850,6 @@ "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-4.1.1.tgz", "integrity": "sha512-bH7QW0+JdX0bPBadXt8GwMof/jz0H28I84hU1Uet9ISpzUqXqRQ3fEZJ+ANPOhzSEczYvANNl3uDQDYArSFDtA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/protocol-http": "^4.1.7", "@smithy/querystring-builder": "^3.0.10", @@ -1884,7 +1863,6 @@ "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-3.0.10.tgz", "integrity": "sha512-3zWGWCHI+FlJ5WJwx73Mw2llYR8aflVyZN5JhoqLxbdPZi6UyKSdCeXAWJw9ja22m6S6Tzz1KZ+kAaSwvydi0g==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^3.7.1", "@smithy/util-buffer-from": "^3.0.0", @@ -1900,7 +1878,6 @@ "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-3.0.10.tgz", "integrity": "sha512-Lp2L65vFi+cj0vFMu2obpPW69DU+6O5g3086lmI4XcnRCG8PxvpWC7XyaVwJCxsZFzueHjXnrOH/E0pl0zikfA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^3.7.1", "tslib": "^2.6.2" @@ -1911,7 +1888,6 @@ "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz", "integrity": "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -1924,7 +1900,6 @@ "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.12.tgz", "integrity": "sha512-1mDEXqzM20yywaMDuf5o9ue8OkJ373lSPbaSjyEvkWdqELhFMyNNgKGWL/rCSf4KME8B+HlHKuR8u9kRj8HzEQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/protocol-http": "^4.1.7", "@smithy/types": "^3.7.1", @@ -1939,7 +1914,6 @@ "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.2.3.tgz", "integrity": "sha512-Hdl9296i/EMptaX7agrSzJZDiz5Y8XPUeBbctTmMtnCguGpqfU3jVsTUan0VLaOhsnquqWLL8Bl5HrlbVGT1og==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/core": "^2.5.3", "@smithy/middleware-serde": "^3.0.10", @@ -1959,7 +1933,6 @@ "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.27.tgz", "integrity": "sha512-H3J/PjJpLL7Tt+fxDKiOD25sMc94YetlQhCnYeNmina2LZscAdu0ZEZPas/kwePHABaEtqp7hqa5S4UJgMs1Tg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/node-config-provider": "^3.1.11", "@smithy/protocol-http": "^4.1.7", @@ -1980,7 +1953,6 @@ "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.10.tgz", "integrity": "sha512-MnAuhh+dD14F428ubSJuRnmRsfOpxSzvRhaGVTvd/lrUDE3kxzCCmH8lnVTvoNQnV2BbJ4c15QwZ3UdQBtFNZA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^3.7.1", "tslib": "^2.6.2" @@ -1994,7 +1966,6 @@ "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-3.0.10.tgz", "integrity": "sha512-grCHyoiARDBBGPyw2BeicpjgpsDFWZZxptbVKb3CRd/ZA15F/T6rZjCCuBUjJwdck1nwUuIxYtsS4H9DDpbP5w==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^3.7.1", "tslib": "^2.6.2" @@ -2008,7 +1979,6 @@ "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.11.tgz", "integrity": "sha512-URq3gT3RpDikh/8MBJUB+QGZzfS7Bm6TQTqoh4CqE8NBuyPkWa5eUXj0XFcFfeZVgg3WMh1u19iaXn8FvvXxZw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/property-provider": "^3.1.10", "@smithy/shared-ini-file-loader": "^3.1.11", @@ -2024,7 +1994,6 @@ "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.3.1.tgz", "integrity": "sha512-fr+UAOMGWh6bn4YSEezBCpJn9Ukp9oR4D32sCjCo7U81evE11YePOQ58ogzyfgmjIO79YeOdfXXqr0jyhPQeMg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/abort-controller": "^3.1.8", "@smithy/protocol-http": "^4.1.7", @@ -2041,7 +2010,6 @@ "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.10.tgz", "integrity": "sha512-n1MJZGTorTH2DvyTVj+3wXnd4CzjJxyXeOgnTlgNVFxaaMeT4OteEp4QrzF8p9ee2yg42nvyVK6R/awLCakjeQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^3.7.1", "tslib": "^2.6.2" @@ -2055,7 +2023,6 @@ "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.7.tgz", "integrity": "sha512-FP2LepWD0eJeOTm0SjssPcgqAlDFzOmRXqXmGhfIM52G7Lrox/pcpQf6RP4F21k0+O12zaqQt5fCDOeBtqY6Cg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^3.7.1", "tslib": "^2.6.2" @@ -2069,7 +2036,6 @@ "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.10.tgz", "integrity": "sha512-nT9CQF3EIJtIUepXQuBFb8dxJi3WVZS3XfuDksxSCSn+/CzZowRLdhDn+2acbBv8R6eaJqPupoI/aRFIImNVPQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^3.7.1", "@smithy/util-uri-escape": "^3.0.0", @@ -2084,7 +2050,6 @@ "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-3.0.10.tgz", "integrity": "sha512-Oa0XDcpo9SmjhiDD9ua2UyM3uU01ZTuIrNdZvzwUTykW1PM8o2yJvMh1Do1rY5sUQg4NDV70dMi0JhDx4GyxuQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^3.7.1", "tslib": "^2.6.2" @@ -2098,7 +2063,6 @@ "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.10.tgz", "integrity": "sha512-zHe642KCqDxXLuhs6xmHVgRwy078RfqxP2wRDpIyiF8EmsWXptMwnMwbVa50lw+WOGNrYm9zbaEg0oDe3PTtvQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^3.7.1" }, @@ -2111,7 +2075,6 @@ "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.11.tgz", "integrity": "sha512-AUdrIZHFtUgmfSN4Gq9nHu3IkHMa1YDcN+s061Nfm+6pQ0mJy85YQDB0tZBCmls0Vuj22pLwDPmL92+Hvfwwlg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^3.7.1", "tslib": "^2.6.2" @@ -2125,7 +2088,6 @@ "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-4.2.3.tgz", "integrity": "sha512-pPSQQ2v2vu9vc8iew7sszLd0O09I5TRc5zhY71KA+Ao0xYazIG+uLeHbTJfIWGO3BGVLiXjUr3EEeCcEQLjpWQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/is-array-buffer": "^3.0.0", "@smithy/protocol-http": "^4.1.7", @@ -2145,7 +2107,6 @@ "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.4.4.tgz", "integrity": "sha512-dPGoJuSZqvirBq+yROapBcHHvFjChoAQT8YPWJ820aPHHiowBlB3RL1Q4kPT1hx0qKgJuf+HhyzKi5Gbof4fNA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/core": "^2.5.3", "@smithy/middleware-endpoint": "^3.2.3", @@ -2164,7 +2125,6 @@ "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.1.tgz", "integrity": "sha512-XKLcLXZY7sUQgvvWyeaL/qwNPp6V3dWcUjqrQKjSb+tzYiCy340R/c64LV5j+Tnb2GhmunEX0eou+L+m2hJNYA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -2177,7 +2137,6 @@ "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.10.tgz", "integrity": "sha512-j90NUalTSBR2NaZTuruEgavSdh8MLirf58LoGSk4AtQfyIymogIhgnGUU2Mga2bkMkpSoC9gxb74xBXL5afKAQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/querystring-parser": "^3.0.10", "@smithy/types": "^3.7.1", @@ -2189,7 +2148,6 @@ "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-3.0.0.tgz", "integrity": "sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/util-buffer-from": "^3.0.0", "@smithy/util-utf8": "^3.0.0", @@ -2204,7 +2162,6 @@ "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-3.0.0.tgz", "integrity": "sha512-cbjJs2A1mLYmqmyVl80uoLTJhAcfzMOyPgjwAYusWKMdLeNtzmMz9YxNl3/jRLoxSS3wkqkf0jwNdtXWtyEBaQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" } @@ -2214,7 +2171,6 @@ "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-3.0.0.tgz", "integrity": "sha512-Tj7pZ4bUloNUP6PzwhN7K386tmSmEET9QtQg0TgdNOnxhZvCssHji+oZTUIuzxECRfG8rdm2PMw2WCFs6eIYkA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -2227,7 +2183,6 @@ "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/is-array-buffer": "^3.0.0", "tslib": "^2.6.2" @@ -2241,7 +2196,6 @@ "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-3.0.0.tgz", "integrity": "sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -2254,7 +2208,6 @@ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.27.tgz", "integrity": "sha512-GV8NvPy1vAGp7u5iD/xNKUxCorE4nQzlyl057qRac+KwpH5zq8wVq6rE3lPPeuFLyQXofPN6JwxL1N9ojGapiQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/property-provider": "^3.1.10", "@smithy/smithy-client": "^3.4.4", @@ -2271,7 +2224,6 @@ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.27.tgz", "integrity": "sha512-7+4wjWfZqZxZVJvDutO+i1GvL6bgOajEkop4FuR6wudFlqBiqwxw3HoH6M9NgeCd37km8ga8NPp2JacQEtAMPg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/config-resolver": "^3.0.12", "@smithy/credential-provider-imds": "^3.2.7", @@ -2290,7 +2242,6 @@ "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-2.1.6.tgz", "integrity": "sha512-mFV1t3ndBh0yZOJgWxO9J/4cHZVn5UG1D8DeCc6/echfNkeEJWu9LD7mgGH5fHrEdR7LDoWw7PQO6QiGpHXhgA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/node-config-provider": "^3.1.11", "@smithy/types": "^3.7.1", @@ -2305,7 +2256,6 @@ "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-3.0.0.tgz", "integrity": "sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -2318,7 +2268,6 @@ "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.10.tgz", "integrity": "sha512-eJO+/+RsrG2RpmY68jZdwQtnfsxjmPxzMlQpnHKjFPwrYqvlcT+fHdT+ZVwcjlWSrByOhGr9Ff2GG17efc192A==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^3.7.1", "tslib": "^2.6.2" @@ -2332,7 +2281,6 @@ "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.10.tgz", "integrity": "sha512-1l4qatFp4PiU6j7UsbasUHL2VU023NRB/gfaa1M0rDqVrRN4g3mCArLRyH3OuktApA4ye+yjWQHjdziunw2eWA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/service-error-classification": "^3.0.10", "@smithy/types": "^3.7.1", @@ -2347,7 +2295,6 @@ "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.3.1.tgz", "integrity": "sha512-Ff68R5lJh2zj+AUTvbAU/4yx+6QPRzg7+pI7M1FbtQHcRIp7xvguxVsQBKyB3fwiOwhAKu0lnNyYBaQfSW6TNw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/fetch-http-handler": "^4.1.1", "@smithy/node-http-handler": "^3.3.1", @@ -2367,7 +2314,6 @@ "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-3.0.0.tgz", "integrity": "sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -2380,7 +2326,6 @@ "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/util-buffer-from": "^3.0.0", "tslib": "^2.6.2" @@ -2394,7 +2339,6 @@ "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-3.1.9.tgz", "integrity": "sha512-/aMXPANhMOlMPjfPtSrDfPeVP8l56SJlz93xeiLmhLe5xvlXA5T3abZ2ilEsDEPeY9T/wnN/vNGn9wa1SbufWA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/abort-controller": "^3.1.8", "@smithy/types": "^3.7.1", @@ -2516,7 +2460,8 @@ "version": "20.4.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.2.tgz", "integrity": "sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@types/prettier": { "version": "2.7.3", @@ -2534,8 +2479,7 @@ "version": "9.0.8", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/yargs": { "version": "17.0.24", @@ -2641,9 +2585,9 @@ } }, "node_modules/aws-cdk": { - "version": "2.1020.2", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1020.2.tgz", - "integrity": "sha512-yWdt3dJh4aPm1VNyEgfG3lozGrvddw0i7avt+Cl9KOYixmisQtAg39/aZqzVVqjzVZVEanXmz+tlhzzh75Z69A==", + "version": "2.1030.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1030.0.tgz", + "integrity": "sha512-jYgOy1Hqx8cOTWW9On9xpypXLecjOqSZ4X2q5U0Gzd14xI+HLmpaRJV5ILJ8vYrLKVbqjhiog0pdxAC7vwF9uQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2657,9 +2601,9 @@ } }, "node_modules/aws-cdk-lib": { - "version": "2.189.1", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.189.1.tgz", - "integrity": "sha512-9JU0yUr2iRTJ1oCPrHyx7hOtBDWyUfyOcdb6arlumJnMcQr2cyAMASY8HuAXHc8Y10ipVp8dRTW+J4/132IIYA==", + "version": "2.219.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.219.0.tgz", + "integrity": "sha512-Rq1/f3exfFEWee1znNq8yvR1TuRQ4xQZz3JNkliBW9dFwyrDe7l/dmlAf6DVvB3nuiZAaUS+vh4ua1LZ7Ec8kg==", "bundleDependencies": [ "@balena/dockerignore", "case", @@ -2674,24 +2618,25 @@ "mime-types" ], "license": "Apache-2.0", + "peer": true, "dependencies": { - "@aws-cdk/asset-awscli-v1": "^2.2.229", + "@aws-cdk/asset-awscli-v1": "2.2.242", "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", - "@aws-cdk/cloud-assembly-schema": "^41.0.0", + "@aws-cdk/cloud-assembly-schema": "^48.6.0", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", - "fs-extra": "^11.3.0", + "fs-extra": "^11.3.1", "ignore": "^5.3.2", "jsonschema": "^1.5.0", "mime-types": "^2.1.35", "minimatch": "^3.1.2", "punycode": "^2.3.1", - "semver": "^7.7.1", + "semver": "^7.7.2", "table": "^6.9.0", "yaml": "1.10.2" }, "engines": { - "node": ">= 14.15.0" + "node": ">= 18.0.0" }, "peerDependencies": { "constructs": "^10.0.0" @@ -2753,7 +2698,7 @@ "license": "MIT" }, "node_modules/aws-cdk-lib/node_modules/brace-expansion": { - "version": "1.1.11", + "version": "1.1.12", "inBundle": true, "license": "MIT", "dependencies": { @@ -2801,7 +2746,7 @@ "license": "MIT" }, "node_modules/aws-cdk-lib/node_modules/fast-uri": { - "version": "3.0.6", + "version": "3.1.0", "funding": [ { "type": "github", @@ -2816,7 +2761,7 @@ "license": "BSD-3-Clause" }, "node_modules/aws-cdk-lib/node_modules/fs-extra": { - "version": "11.3.0", + "version": "11.3.1", "inBundle": true, "license": "MIT", "dependencies": { @@ -2855,7 +2800,7 @@ "license": "MIT" }, "node_modules/aws-cdk-lib/node_modules/jsonfile": { - "version": "6.1.0", + "version": "6.2.0", "inBundle": true, "license": "MIT", "dependencies": { @@ -2925,7 +2870,7 @@ } }, "node_modules/aws-cdk-lib/node_modules/semver": { - "version": "7.7.1", + "version": "7.7.2", "inBundle": true, "license": "ISC", "bin": { @@ -3107,8 +3052,7 @@ "version": "2.11.0", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/brace-expansion": { "version": "1.1.12", @@ -3152,6 +3096,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001503", "electron-to-chromium": "^1.4.431", @@ -3242,11 +3187,13 @@ } }, "node_modules/cdk-nag": { - "version": "2.29.11", - "resolved": "https://registry.npmjs.org/cdk-nag/-/cdk-nag-2.29.11.tgz", - "integrity": "sha512-qB+WV8svr3f5yRid5AfZ97lZUfO+GzfY5aPH+wLNHJ3f1v9M7kUphzhebhwjBXpDwpLr+hvMnMFW73zQliaTAw==", + "version": "2.37.51", + "resolved": "https://registry.npmjs.org/cdk-nag/-/cdk-nag-2.37.51.tgz", + "integrity": "sha512-oqlbHxxHQaSOg/9ME4f/S2uSHidBwPVTIfgGhaZPGM20HpGE+cv2YqTB5ID3qKL5/fcpSNDPF7nosU84W2N7nQ==", + "license": "Apache-2.0", + "peer": true, "peerDependencies": { - "aws-cdk-lib": "^2.156.0", + "aws-cdk-lib": "^2.176.0", "constructs": "^10.0.5" } }, @@ -3354,6 +3301,7 @@ "version": "10.3.0", "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.3.0.tgz", "integrity": "sha512-vbK8i3rIb/xwZxSpTjz3SagHn1qq9BChLEfy5Hf6fB3/2eFbrwt2n9kHwQcS0CPTRBesreeAcsJfMq2229FnbQ==", + "peer": true, "engines": { "node": ">= 16.14.0" } @@ -3591,7 +3539,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "strnum": "^1.0.5" }, @@ -3947,6 +3894,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.6.1.tgz", "integrity": "sha512-Nirw5B4nn69rVUZtemCQhwxOBhm0nsp3hmtF4rzCeWD7BkjAXRIji7xWQfnTNbz9g0aVsBX6aZK3n+23LM6uDw==", "dev": true, + "peer": true, "dependencies": { "@jest/core": "^29.6.1", "@jest/types": "^29.6.1", @@ -4717,7 +4665,6 @@ "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.3.tgz", "integrity": "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw==", "license": "MIT", - "peer": true, "dependencies": { "obliterator": "^1.6.1" } @@ -4771,8 +4718,7 @@ "version": "1.6.1", "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-1.6.1.tgz", "integrity": "sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/once": { "version": "1.4.0", @@ -5215,8 +5161,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/supports-color": { "version": "7.2.0", @@ -5355,6 +5300,7 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", "dev": true, + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -5425,6 +5371,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5472,7 +5419,6 @@ "https://github.com/sponsors/ctavan" ], "license": "MIT", - "peer": true, "bin": { "uuid": "dist/bin/uuid" } diff --git a/cdk/test/cdk.test.ts b/cdk/test/cdk.test.ts index ec48d82bc..626b40fe6 100644 --- a/cdk/test/cdk.test.ts +++ b/cdk/test/cdk.test.ts @@ -6,6 +6,7 @@ import { getEmbeddingModel, getChunkingStrategy, getAnalyzer, + getKnowledgeBaseType, } from "../lib/utils/bedrock-knowledge-base-args"; import { BedrockCustomBotStack } from "../lib/bedrock-custom-bot-stack"; import { BedrockRegionResourcesStack } from "../lib/bedrock-region-resources"; @@ -630,6 +631,9 @@ describe("Bedrock Knowledge Base Stack", () => { }; const BEDROCK_KNOWLEDGE_BASE = { + type: { + S: "dedicated", + }, chunking_strategy: { S: "fixed_size", }, @@ -690,6 +694,7 @@ describe("Bedrock Knowledge Base Stack", () => { (s3Url: any) => s3Url.S ); + const knowledgeBaseType = getKnowledgeBaseType(knowledgeBase.type); const embeddingsModel = getEmbeddingModel(knowledgeBase.embeddings_model.S); const chunkingStrategy = getChunkingStrategy( knowledgeBase.chunking_strategy.S, @@ -715,12 +720,14 @@ describe("Bedrock Knowledge Base Stack", () => { embeddingsModel, bedrockClaudeChatDocumentBucketName: BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME, + knowledgeBaseType, chunkingStrategy, existingS3Urls, maxTokens, instruction, analyzer, overlapPercentage, + filenames: knowledge.filenames.L.map((filename: any) => filename.S), sourceUrls: knowledge.source_urls.L.map((sourceUrl: any) => sourceUrl.S), existKnowledgeBaseId: undefined, }); diff --git a/frontend/src/features/knowledgeBase/constants/index.ts b/frontend/src/features/knowledgeBase/constants/index.ts index 5082aa649..0ada105ff 100644 --- a/frontend/src/features/knowledgeBase/constants/index.ts +++ b/frontend/src/features/knowledgeBase/constants/index.ts @@ -48,6 +48,7 @@ export const DEFAULT_OPENSEARCH_ANALYZER: { } as const; export const DEFAULT_BEDROCK_KNOWLEDGEBASE: BedrockKnowledgeBase = { + type: "dedicated", knowledgeBaseId: null, existKnowledgeBaseId: null, embeddingsModel: 'cohere_multilingual_v3', diff --git a/frontend/src/features/knowledgeBase/pages/BotKbEditPage.tsx b/frontend/src/features/knowledgeBase/pages/BotKbEditPage.tsx index 1c4dedb96..977cf7d3c 100644 --- a/frontend/src/features/knowledgeBase/pages/BotKbEditPage.tsx +++ b/frontend/src/features/knowledgeBase/pages/BotKbEditPage.tsx @@ -18,7 +18,7 @@ import { ConversationQuickStarter, ActiveModels, } from '../../../@types/bot'; -import { ParsingModel } from '../types'; +import { BedrockKnowledgeBaseType, ParsingModel } from '../types'; import { ulid } from 'ulid'; import { EDGE_GENERATION_PARAMS, @@ -128,9 +128,32 @@ const BotKbEditPage: React.FC = () => { string | null >(null); const [knowledgeBaseType, setKnowledgeBaseType] = useState< - 'new' | 'existing' + 'new' | 'shared' | 'existing' >('new'); + const bedrockKnowledgeBaseType = useMemo(() => { + if (existKnowledgeBaseId != null) { + return undefined; + } + switch (knowledgeBaseType) { + case 'new': { + if (files.length === 0 && urls.length === 0 && s3Urls.length === 0) { + return undefined; + } + return 'dedicated'; + } + case 'shared': { + if (files.length === 0) { + return undefined; + } + return 'shared'; + } + case 'existing': { + return undefined; + } + } + }, [existKnowledgeBaseId, knowledgeBaseType, files, urls, s3Urls]); + // When loading an existing bot that already has a knowledge base id(s), // default the radio selection to 'existing' so the UI reflects the bot state. useEffect(() => { @@ -155,6 +178,17 @@ const BotKbEditPage: React.FC = () => { const [relevanceThreshold, setRelevanceThreshold] = useState(0); const [guardrailArn, setGuardrailArn] = useState(''); const [guardrailVersion, setGuardrailVersion] = useState(''); + + const isGuardrailEnabled = useMemo(() => ( + hateThreshold > 0 || + insultsThreshold > 0 || + sexualThreshold > 0 || + violenceThreshold > 0 || + misconductThreshold > 0 || + groundingThreshold > 0 || + relevanceThreshold > 0 + ), [hateThreshold, insultsThreshold, sexualThreshold, violenceThreshold, misconductThreshold, groundingThreshold, relevanceThreshold]); + const [parsingModel, setParsingModel] = useState( undefined ); @@ -482,6 +516,9 @@ const BotKbEditPage: React.FC = () => { setS3Urls( bot.knowledge.s3Urls.length === 0 ? [''] : bot.knowledge.s3Urls ); + if (bot.bedrockKnowledgeBase.type === 'shared') { + setKnowledgeBaseType('shared'); + } setFiles( bot.knowledge.filenames.map((filename) => ({ filename, @@ -1256,7 +1293,8 @@ const BotKbEditPage: React.FC = () => { (qs) => qs.title !== '' && qs.example !== '' ), bedrockKnowledgeBase: { - knowledgeBaseId, + type: bedrockKnowledgeBaseType, + knowledgeBaseId: null, existKnowledgeBaseId, embeddingsModel, chunkingConfiguration: (() => { @@ -1280,14 +1318,7 @@ const BotKbEditPage: React.FC = () => { webCrawlingFilters, }, bedrockGuardrails: { - isGuardrailEnabled: - hateThreshold > 0 || - insultsThreshold > 0 || - sexualThreshold > 0 || - violenceThreshold > 0 || - misconductThreshold > 0 || - groundingThreshold > 0 || - relevanceThreshold > 0, + isGuardrailEnabled, hateThreshold: hateThreshold, insultsThreshold: insultsThreshold, sexualThreshold: sexualThreshold, @@ -1387,7 +1418,8 @@ const BotKbEditPage: React.FC = () => { (qs) => qs.title !== '' && qs.example !== '' ), bedrockKnowledgeBase: { - knowledgeBaseId, + type: bedrockKnowledgeBaseType, + knowledgeBaseId: (bedrockKnowledgeBaseType != null) ? knowledgeBaseId : null, existKnowledgeBaseId, embeddingsModel, chunkingConfiguration: (() => { @@ -1411,14 +1443,7 @@ const BotKbEditPage: React.FC = () => { webCrawlingFilters, }, bedrockGuardrails: { - isGuardrailEnabled: - hateThreshold > 0 || - insultsThreshold > 0 || - sexualThreshold > 0 || - violenceThreshold > 0 || - misconductThreshold > 0 || - groundingThreshold > 0 || - relevanceThreshold > 0, + isGuardrailEnabled, hateThreshold: hateThreshold, insultsThreshold: insultsThreshold, sexualThreshold: sexualThreshold, @@ -1426,8 +1451,8 @@ const BotKbEditPage: React.FC = () => { misconductThreshold: misconductThreshold, groundingThreshold: groundingThreshold, relevanceThreshold: relevanceThreshold, - guardrailArn: guardrailArn, - guardrailVersion: guardrailVersion, + guardrailArn: (isGuardrailEnabled) ? guardrailArn : '', + guardrailVersion: (isGuardrailEnabled) ? guardrailVersion : '', }, activeModels, }) @@ -1569,6 +1594,15 @@ const BotKbEditPage: React.FC = () => { )} onChange={() => setKnowledgeBaseType('new')} /> + setKnowledgeBaseType('shared')} + /> { ); } - if (knowledgeBaseType === 'new') { + if (knowledgeBaseType === 'new' || knowledgeBaseType === 'shared') { return (
@@ -1641,174 +1675,54 @@ const BotKbEditPage: React.FC = () => {
-
-
- {t('bot.label.s3url')} -
-
- {t('bot.help.knowledge.s3url')} -
-
- {s3Urls.map((s3Url, idx) => ( -
- { - onChangeS3Url(s, idx); - }} - errorMessage={errorMessages[`s3Urls-${idx}`]} - /> - { - onClickRemoveS3Url(idx); - }}> - - -
- ))} -
-
- -
-
- -
-
- {t('bot.label.url')} -
-
- {t('bot.help.knowledge.url')} -
-
- {urls.map((url, idx) => ( -
- { - onChangeUrls(s, idx); - }} - errorMessage={errorMessages[`urls-${idx}`]} - /> - { - onClickRemoveUrls(idx); - }}> - - -
- ))} -
-
- -
- - -
- { + setWebCrawlingScope(val as WebCrawlingScope); + }} + disabled={disabledKnowledgeEdit} + /> +
+ +
+
+ {t( + 'knowledgeBaseSettings.webCrawlerConfig.includePatterns.label' + )} +
+
+ {t( + 'knowledgeBaseSettings.webCrawlerConfig.includePatterns.hint' + )} +
+
+ {webCrawlingFilters.includePatterns.map( + (pattern, idx) => ( +
+ { + onChangeIncludePattern(s, idx); + }} + /> + { + onClickRemoveIncludePattern(idx); + }}> + + +
+ ) + )} +
+
+ +
+
+ +
+
+ {t( + 'knowledgeBaseSettings.webCrawlerConfig.excludePatterns.label' + )} +
+
+ {t( + 'knowledgeBaseSettings.webCrawlerConfig.excludePatterns.hint' + )} +
+
+ {webCrawlingFilters.excludePatterns.map( + (pattern, idx) => ( +
+ { + onChangeExcludePattern(s, idx); + }} + /> + { + onClickRemoveExcludePattern(idx); + }}> + + +
+ ) + )} +
+
+ +
+
+
- - + + )} ); } diff --git a/frontend/src/features/knowledgeBase/types/index.d.ts b/frontend/src/features/knowledgeBase/types/index.d.ts index 54867ee4e..51c4ec8a0 100644 --- a/frontend/src/features/knowledgeBase/types/index.d.ts +++ b/frontend/src/features/knowledgeBase/types/index.d.ts @@ -1,4 +1,7 @@ +export type BedrockKnowledgeBaseType = "dedicated" | "shared" | undefined; + export type BedrockKnowledgeBase = { + type?: BedrockKnowledgeBaseType; knowledgeBaseId: string | null; existKnowledgeBaseId: string | null; dataSourceIds?: string[]; // only present after bot is ready diff --git a/frontend/src/i18n/en/index.ts b/frontend/src/i18n/en/index.ts index b74a144e1..23e43cdac 100644 --- a/frontend/src/i18n/en/index.ts +++ b/frontend/src/i18n/en/index.ts @@ -944,6 +944,9 @@ How would you categorize this email?`, createNewKb: { label: 'Create New Knowledge Base', }, + useSharedKb: { + label: 'Pool Pattern', + }, existing: { label: 'Use your existing knowledge base', }, diff --git a/frontend/src/i18n/ja/index.ts b/frontend/src/i18n/ja/index.ts index 5bde31d11..a6ef8c966 100644 --- a/frontend/src/i18n/ja/index.ts +++ b/frontend/src/i18n/ja/index.ts @@ -950,6 +950,9 @@ const translation: typeof en = { createNewKb: { label: '新規のナレッジを作成する', }, + useSharedKb: { + label: 'Pool Pattern', + }, existing: { label: '外部のナレッジ(Knowledge Base)を利用する', }, From 1f8e31cd7f92704ba96fe102f6187a211a781bba Mon Sep 17 00:00:00 2001 From: Yukinobu Mine Date: Fri, 17 Oct 2025 14:36:16 +0900 Subject: [PATCH 2/9] Refactor Step Functions state machine for Knowledge Bases updates. --- backend/app/repositories/custom_bot.py | 109 +- backend/app/repositories/models/custom_bot.py | 91 +- .../bootstrap_state_machine.py | 127 + .../fetch_stack_output.py | 70 - .../finalize_custom_bot_build.py | 84 + .../finalize_shared_knowledge_bases_build.py | 68 + .../store_knowledge_base_id.py | 58 - .../update_bot_status.py | 43 +- .../guardrails/store_guardrail_arn.py | 61 - cdk/bin/bedrock-custom-bot.ts | 85 +- cdk/bin/bedrock-shared-knowledge-bases.ts | 165 ++ .../index.ts | 26 + .../package-lock.json | 2185 +++++++++++++++++ .../package.json | 21 + .../tsconfig.json | 51 + cdk/lib/bedrock-chat-stack.ts | 14 + cdk/lib/bedrock-custom-bot-stack.ts | 2 +- .../bedrock-shared-knowledge-bases-stack.ts | 157 ++ cdk/lib/constructs/api-publish-codebuild.ts | 2 +- cdk/lib/constructs/api.ts | 3 + .../bedrock-custom-bot-codebuild.ts | 4 +- ...edrock-shared-knowledge-bases-codebuild.ts | 80 + cdk/lib/constructs/database.ts | 8 + cdk/lib/constructs/embedding.ts | 444 ++-- cdk/lib/utils/bedrock-knowledge-base-args.ts | 109 +- cdk/lib/utils/parameter-models.ts | 51 +- 26 files changed, 3498 insertions(+), 620 deletions(-) create mode 100644 backend/embedding_statemachine/bedrock_knowledge_base/bootstrap_state_machine.py delete mode 100644 backend/embedding_statemachine/bedrock_knowledge_base/fetch_stack_output.py create mode 100644 backend/embedding_statemachine/bedrock_knowledge_base/finalize_custom_bot_build.py create mode 100644 backend/embedding_statemachine/bedrock_knowledge_base/finalize_shared_knowledge_bases_build.py delete mode 100644 backend/embedding_statemachine/bedrock_knowledge_base/store_knowledge_base_id.py delete mode 100644 backend/embedding_statemachine/guardrails/store_guardrail_arn.py create mode 100644 cdk/bin/bedrock-shared-knowledge-bases.ts create mode 100644 cdk/lambda/knowledge-base-custom-transformation/index.ts create mode 100644 cdk/lambda/knowledge-base-custom-transformation/package-lock.json create mode 100644 cdk/lambda/knowledge-base-custom-transformation/package.json create mode 100644 cdk/lambda/knowledge-base-custom-transformation/tsconfig.json create mode 100644 cdk/lib/bedrock-shared-knowledge-bases-stack.ts create mode 100644 cdk/lib/constructs/bedrock-shared-knowledge-bases-codebuild.ts diff --git a/backend/app/repositories/custom_bot.py b/backend/app/repositories/custom_bot.py index c766967e8..ef36b2adf 100644 --- a/backend/app/repositories/custom_bot.py +++ b/backend/app/repositories/custom_bot.py @@ -98,7 +98,13 @@ def store_bot(custom_bot: BotModel): item["BedrockKnowledgeBaseHash"] = ( base64.b32encode( md5( - custom_bot.bedrock_knowledge_base.model_dump_json().encode() + custom_bot.bedrock_knowledge_base.model_dump_json( + exclude={ + "knowledge_base_id", + "exist_knowledge_base_id", + "data_source_ids", + } + ).encode() ).digest() ) .decode() @@ -170,14 +176,11 @@ def update_bot( ":active_models": active_models.model_dump(), # type: ignore[attr-defined] } if bedrock_knowledge_base: - if ( - bedrock_knowledge_base.exist_knowledge_base_id is not None - or ( - len(knowledge.source_urls) == 0 - and len(knowledge.sitemap_urls) == 0 - and len(knowledge.filenames) == 0 - and len(knowledge.s3_urls) == 0 - ) + if bedrock_knowledge_base.exist_knowledge_base_id is not None or ( + len(knowledge.source_urls) == 0 + and len(knowledge.sitemap_urls) == 0 + and len(knowledge.filenames) == 0 + and len(knowledge.s3_urls) == 0 ): bedrock_knowledge_base.type = None bedrock_knowledge_base.knowledge_base_id = None @@ -200,7 +203,15 @@ def update_bot( ) expression_attribute_values[":bedrock_knowledge_base_hash"] = ( base64.b32encode( - md5(bedrock_knowledge_base.model_dump_json().encode()).digest() + md5( + bedrock_knowledge_base.model_dump_json( + exclude={ + "knowledge_base_id", + "exist_knowledge_base_id", + "data_source_ids", + } + ).encode() + ).digest() ) .decode() .rstrip("=") @@ -733,83 +744,9 @@ def find_bot_by_id(bot_id: str) -> BotModel: if len(response["Items"]) == 0: raise RecordNotFoundError(f"Bot with id {bot_id} not found") - item = response["Items"][0] + items = response["Items"] - bot = BotModel( - id=item["BotId"], - owner_user_id=item["PK"], - title=item["Title"], - description=item["Description"], - instruction=item["Instruction"], - create_time=float(item["CreateTime"]), - last_used_time=float(item.get("LastUsedTime", item["CreateTime"])), - # Note: SharedScope is set to None for private shared_scope to use sparse index - shared_scope=item.get("SharedScope", "private"), - shared_status=item["SharedStatus"], - allowed_cognito_groups=item.get("AllowedCognitoGroups", []), - allowed_cognito_users=item.get("AllowedCognitoUsers", []), - # Note: IsStarred is set to False for non-starred bots to use sparse index - is_starred=item.get("IsStarred", False), - generation_params=GenerationParamsModel.model_validate( - { - **item.get("GenerationParams", DEFAULT_GENERATION_CONFIG), - # For backward compatibility - "reasoning_params": item.get("GenerationParams", {}).get( - "reasoning_params", - { - "budget_tokens": DEFAULT_GENERATION_CONFIG["reasoning_params"]["budget_tokens"], # type: ignore - }, - ), - } - ), - agent=( - AgentModel.model_validate(item["AgentData"]) - if "AgentData" in item - else AgentModel(tools=[]) - ), - knowledge=KnowledgeModel( - **{**item["Knowledge"], "s3_urls": item["Knowledge"].get("s3_urls", [])} - ), - prompt_caching_enabled=item.get("PromptCachingEnabled", True), - sync_status=item["SyncStatus"], - sync_status_reason=item["SyncStatusReason"], - sync_last_exec_id=item["LastExecId"], - published_api_stack_name=item.get("ApiPublishmentStackName", None), - published_api_datetime=item.get("ApiPublishedDatetime", None), - published_api_codebuild_id=item.get("ApiPublishCodeBuildId", None), - display_retrieved_chunks=item.get("DisplayRetrievedChunks", False), - conversation_quick_starters=item.get("ConversationQuickStarters", []), - bedrock_knowledge_base=( - BedrockKnowledgeBaseModel( - **{ - **item["BedrockKnowledgeBase"], - "chunking_configuration": item["BedrockKnowledgeBase"].get( - "chunking_configuration", None - ), - "parsing_model": item["BedrockKnowledgeBase"].get( - "parsing_model", "disabled" - ), - } - ) - if "BedrockKnowledgeBase" in item - else None - ), - bedrock_guardrails=( - BedrockGuardrailsModel(**item["GuardrailsParams"]) - if "GuardrailsParams" in item - else None - ), - active_models=( - ActiveModelsModel.model_validate(item.get("ActiveModels")) - if item.get("ActiveModels") - else default_active_models # for backward compatibility - ), - usage_stats=( - UsageStatsModel.model_validate(item.get("UsageStats")) - if item.get("UsageStats") - else UsageStatsModel(usage_count=0) # for backward compatibility - ), - ) + bot = BotModel.from_dynamo_item(items[0]) logger.info(f"Found bot: {bot}") return bot diff --git a/backend/app/repositories/models/custom_bot.py b/backend/app/repositories/models/custom_bot.py index bcccaae52..07e479eeb 100644 --- a/backend/app/repositories/models/custom_bot.py +++ b/backend/app/repositories/models/custom_bot.py @@ -447,14 +447,11 @@ def validate_shared_scope(self) -> Self: @model_validator(mode="after") def validate_knowledge_base_type(self) -> Self: if self.bedrock_knowledge_base is not None: - if ( - self.bedrock_knowledge_base.exist_knowledge_base_id is not None - or ( - len(self.knowledge.source_urls) == 0 - and len(self.knowledge.sitemap_urls) == 0 - and len(self.knowledge.filenames) == 0 - and len(self.knowledge.s3_urls) == 0 - ) + if self.bedrock_knowledge_base.exist_knowledge_base_id is not None or ( + len(self.knowledge.source_urls) == 0 + and len(self.knowledge.sitemap_urls) == 0 + and len(self.knowledge.filenames) == 0 + and len(self.knowledge.s3_urls) == 0 ): self.bedrock_knowledge_base.type = None self.bedrock_knowledge_base.knowledge_base_id = None @@ -634,6 +631,84 @@ def from_input( usage_stats=UsageStatsModel(usage_count=0), ) + @classmethod + def from_dynamo_item(cls, item: dict) -> Self: + return BotModel( + id=item["BotId"], + owner_user_id=item["PK"], + title=item["Title"], + description=item["Description"], + instruction=item["Instruction"], + create_time=float(item["CreateTime"]), + last_used_time=float(item.get("LastUsedTime", item["CreateTime"])), + # Note: SharedScope is set to None for private shared_scope to use sparse index + shared_scope=item.get("SharedScope", "private"), + shared_status=item["SharedStatus"], + allowed_cognito_groups=item.get("AllowedCognitoGroups", []), + allowed_cognito_users=item.get("AllowedCognitoUsers", []), + # Note: IsStarred is set to False for non-starred bots to use sparse index + is_starred=item.get("IsStarred", False), + generation_params=GenerationParamsModel.model_validate( + { + **item.get("GenerationParams", DEFAULT_GENERATION_CONFIG), + # For backward compatibility + "reasoning_params": item.get("GenerationParams", {}).get( + "reasoning_params", + { + "budget_tokens": DEFAULT_GENERATION_CONFIG["reasoning_params"]["budget_tokens"], # type: ignore + }, + ), + } + ), + agent=( + AgentModel.model_validate(item["AgentData"]) + if "AgentData" in item + else AgentModel(tools=[]) + ), + knowledge=KnowledgeModel( + **{**item["Knowledge"], "s3_urls": item["Knowledge"].get("s3_urls", [])} + ), + prompt_caching_enabled=item.get("PromptCachingEnabled", True), + sync_status=item["SyncStatus"], + sync_status_reason=item["SyncStatusReason"], + sync_last_exec_id=item["LastExecId"], + published_api_stack_name=item.get("ApiPublishmentStackName", None), + published_api_datetime=item.get("ApiPublishedDatetime", None), + published_api_codebuild_id=item.get("ApiPublishCodeBuildId", None), + display_retrieved_chunks=item.get("DisplayRetrievedChunks", False), + conversation_quick_starters=item.get("ConversationQuickStarters", []), + bedrock_knowledge_base=( + BedrockKnowledgeBaseModel( + **{ + **item["BedrockKnowledgeBase"], + "chunking_configuration": item["BedrockKnowledgeBase"].get( + "chunking_configuration", None + ), + "parsing_model": item["BedrockKnowledgeBase"].get( + "parsing_model", "disabled" + ), + } + ) + if "BedrockKnowledgeBase" in item + else None + ), + bedrock_guardrails=( + BedrockGuardrailsModel(**item["GuardrailsParams"]) + if "GuardrailsParams" in item + else None + ), + active_models=( + ActiveModelsModel.model_validate(item.get("ActiveModels")) + if item.get("ActiveModels") + else default_active_models # for backward compatibility + ), + usage_stats=( + UsageStatsModel.model_validate(item.get("UsageStats")) + if item.get("UsageStats") + else UsageStatsModel(usage_count=0) # for backward compatibility + ), + ) + def to_output(self) -> BotOutput: return BotOutput( id=self.id, diff --git a/backend/embedding_statemachine/bedrock_knowledge_base/bootstrap_state_machine.py b/backend/embedding_statemachine/bedrock_knowledge_base/bootstrap_state_machine.py new file mode 100644 index 000000000..539652c7e --- /dev/null +++ b/backend/embedding_statemachine/bedrock_knowledge_base/bootstrap_state_machine.py @@ -0,0 +1,127 @@ +from typing import TypedDict +from boto3.dynamodb.conditions import Key + +from app.repositories.common import get_bot_table_client +from app.repositories.models.custom_bot import BotModel, BedrockKnowledgeBaseModel + + +def handler(event, context): + bots = find_queued_bots() + shared_knowledge_bases = find_shared_knowledge_bases() + return { + "Bots": [ + { + "BotId": bot.id, + "OwnerUserId": bot.owner_user_id, + "Knowledge": bot.knowledge.model_dump(), + "KnowledgeBaseHash": knowledge_base_hash, + "KnowledgeBase": ( + bot.bedrock_knowledge_base.model_dump( + exclude={ + "knowledge_base_id", + "exist_knowledge_base_id", + "data_source_ids", + } + ) + if bot.bedrock_knowledge_base is not None + and bot.bedrock_knowledge_base.type == "dedicated" + else {} + ), + "Guardrails": ( + bot.bedrock_guardrails.model_dump() + if bot.bedrock_guardrails is not None + and bot.bedrock_guardrails.is_guardrail_enabled + else {} + ), + } + for bot, knowledge_base_hash in bots + ], + "SharedKnowledgeBases": [ + { + "KnowledgeBaseHash": shared_knowledge_base["knowledge_base_hash"], + "KnowledgeBase": shared_knowledge_base["knowledge_base"].model_dump( + exclude={ + "knowledge_base_id", + "exist_knowledge_base_id", + "data_source_ids", + } + ), + "Bots": [ + { + "OwnerUserId": bot.owner_user_id, + "BotId": bot.id, + } + for bot in shared_knowledge_base["bots"] + ], + } + for shared_knowledge_base in shared_knowledge_bases + ], + } + + +def find_queued_bots() -> list[tuple[BotModel, str | None]]: + bot_table = get_bot_table_client() + + bots: list[tuple[BotModel, str | None]] = [] + query_params = { + "IndexName": "SyncStatusIndex", + "KeyConditionExpression": Key("SyncStatus").eq("QUEUED"), + } + while True: + response = bot_table.query(**query_params) + items = response["Items"] + bots.extend( + (BotModel.from_dynamo_item(item), item.get("BedrockKnowledgeBaseHash")) + for item in items + ) + + last_evaluated_key = response.get("LastEvaluatedKey") + if last_evaluated_key is None: + break + + query_params["ExclusiveStartKey"] = last_evaluated_key + + return bots + + +class SharedKnowledgeBase(TypedDict): + knowledge_base_hash: str + knowledge_base: BedrockKnowledgeBaseModel + bots: list[BotModel] + + +def find_shared_knowledge_bases() -> list[SharedKnowledgeBase]: + bot_table = get_bot_table_client() + + knowledge_bases: dict[str, SharedKnowledgeBase] = {} + query_params = { + "IndexName": "KnowledgeBaseTypeIndex", + "KeyConditionExpression": Key("BedrockKnowledgeBaseType").eq("shared"), + } + while True: + response = bot_table.query(**query_params) + items = response["Items"] + for item in items: + bot = BotModel.from_dynamo_item(item) + knowledge_base_hash = item.get("BedrockKnowledgeBaseHash") + if ( + bot.bedrock_knowledge_base is not None + and knowledge_base_hash is not None + ): + if knowledge_base_hash not in knowledge_bases: + knowledge_bases[knowledge_base_hash] = { + "knowledge_base_hash": knowledge_base_hash, + "knowledge_base": bot.bedrock_knowledge_base, + "bots": [bot], + } + + else: + knowledge_bases[knowledge_base_hash]["bots"].append(bot) + + last_evaluated_key = response.get("LastEvaluatedKey") + if last_evaluated_key is None: + break + + query_params["ExclusiveStartKey"] = last_evaluated_key + + return list(knowledge_bases.values()) diff --git a/backend/embedding_statemachine/bedrock_knowledge_base/fetch_stack_output.py b/backend/embedding_statemachine/bedrock_knowledge_base/fetch_stack_output.py deleted file mode 100644 index e330cbd53..000000000 --- a/backend/embedding_statemachine/bedrock_knowledge_base/fetch_stack_output.py +++ /dev/null @@ -1,70 +0,0 @@ -import os -from typing import Any, Dict, List, TypedDict - -import boto3 -from app.repositories.common import decompose_sk - -BEDROCK_REGION = os.environ.get("BEDROCK_REGION") - -cf_client = boto3.client("cloudformation", BEDROCK_REGION) - - -class StackItem(TypedDict): - KnowledgeBaseId: str - DataSourceId: str - PK: str - SK: str - - -class StackOutput(TypedDict): - KnowledgeBaseId: str - items: List[StackItem] - GuardrailArn: str - GuardrailVersion: str - - -def handler(event: Dict[str, str], context: Any) -> StackOutput: - print(event) - pk = event["pk"] - sk = event["sk"] - - bot_id = decompose_sk(sk) - - # Note: stack naming rule is defined on: - # cdk/bin/bedrock-knowledge-base.ts - stack_name = f"BrChatKbStack{bot_id}" - - response = cf_client.describe_stacks(StackName=stack_name) - outputs = response["Stacks"][0]["Outputs"] - - knowledge_base_id = None - data_source_ids: List[str] = [] - result: StackOutput = { - "KnowledgeBaseId": "", - "items": [], - "GuardrailArn": "", - "GuardrailVersion": "", - } - - for output in outputs: - if output["OutputKey"] == "KnowledgeBaseId": - knowledge_base_id = output["OutputValue"] - result["KnowledgeBaseId"] = knowledge_base_id - elif output["OutputKey"].startswith("DataSource"): - data_source_ids.append(output["OutputValue"]) - elif output["OutputKey"] == "GuardrailArn": - result["GuardrailArn"] = output["OutputValue"] - elif output["OutputKey"] == "GuardrailVersion": - result["GuardrailVersion"] = output["OutputValue"] - - for data_source_id in data_source_ids: - result["items"].append( - { - "KnowledgeBaseId": knowledge_base_id or "", - "DataSourceId": data_source_id, - "PK": pk, - "SK": sk, - } - ) - - return result diff --git a/backend/embedding_statemachine/bedrock_knowledge_base/finalize_custom_bot_build.py b/backend/embedding_statemachine/bedrock_knowledge_base/finalize_custom_bot_build.py new file mode 100644 index 000000000..4ba40180d --- /dev/null +++ b/backend/embedding_statemachine/bedrock_knowledge_base/finalize_custom_bot_build.py @@ -0,0 +1,84 @@ +import os +from typing import Any, Dict, List, TypedDict, NotRequired + +import boto3 +from app.repositories.custom_bot import ( + update_knowledge_base_id, + update_guardrails_params, +) + +BEDROCK_REGION = os.environ.get("BEDROCK_REGION") + +cf_client = boto3.client("cloudformation", BEDROCK_REGION) + + +class DataSource(TypedDict): + KnowledgeBaseId: str + DataSourceId: str + + +class Bot(TypedDict): + OwnerUserId: str + BotId: str + + +class StackOutput(TypedDict): + DataSources: List[DataSource] + Bots: list[Bot] + + +def handler(event, context) -> StackOutput: + print(event) + user_id = event["OwnerUserId"] + bot_id = event["BotId"] + + # Note: stack naming rule is defined on: + # cdk/bin/bedrock-custom-bot.ts + stack_name = f"BrChatKbStack{bot_id}" + + response = cf_client.describe_stacks(StackName=stack_name) + outputs = response["Stacks"][0]["Outputs"] + + knowledge_base_id = None + data_source_ids: List[str] = [] + + guardrail_arn = None + guardrail_version = None + + for output in outputs: + if output["OutputKey"] == "KnowledgeBaseId": + knowledge_base_id = output["OutputValue"] + + elif output["OutputKey"].startswith("DataSource"): + data_source_ids.append(output["OutputValue"]) + + elif output["OutputKey"] == "GuardrailArn": + guardrail_arn = output["OutputValue"] + + elif output["OutputKey"] == "GuardrailVersion": + guardrail_version = output["OutputValue"] + + result: StackOutput = { + "DataSources": [], + "Bots": [ + { + "OwnerUserId": user_id, + "BotId": bot_id, + }, + ], + } + + if knowledge_base_id: + result["DataSources"].extend( + { + "KnowledgeBaseId": knowledge_base_id, + "DataSourceId": data_source_id, + } + for data_source_id in data_source_ids + ) + update_knowledge_base_id(user_id, bot_id, knowledge_base_id, data_source_ids) + + if guardrail_arn and guardrail_version: + update_guardrails_params(user_id, bot_id, guardrail_arn, guardrail_version) + + return result diff --git a/backend/embedding_statemachine/bedrock_knowledge_base/finalize_shared_knowledge_bases_build.py b/backend/embedding_statemachine/bedrock_knowledge_base/finalize_shared_knowledge_bases_build.py new file mode 100644 index 000000000..20a1ed686 --- /dev/null +++ b/backend/embedding_statemachine/bedrock_knowledge_base/finalize_shared_knowledge_bases_build.py @@ -0,0 +1,68 @@ +import os +from typing import Any, Dict, List, TypedDict, NotRequired + +import boto3 +from app.repositories.custom_bot import ( + update_knowledge_base_id, + update_guardrails_params, +) + +BEDROCK_REGION = os.environ.get("BEDROCK_REGION") + +cf_client = boto3.client("cloudformation", BEDROCK_REGION) + + +class DataSource(TypedDict): + KnowledgeBaseId: str + DataSourceId: str + + +class StackOutput(TypedDict): + DataSources: List[DataSource] + + +def handler(event, context) -> StackOutput: + print(event) + shared_knowledge_bases = event["SharedKnowledgeBases"] + + # Note: stack naming rule is defined on: + # cdk/bin/bedrock-shared-knowledge-bases.ts + stack_name = "BrChatSharedKbStack" + + response = cf_client.describe_stacks(StackName=stack_name) + outputs = response["Stacks"][0]["Outputs"] + print(outputs) + + result: StackOutput = { + "DataSources": [], + } + + for shared_knowledge_base in shared_knowledge_bases: + knowledge_base_hash = shared_knowledge_base["KnowledgeBaseHash"] + + knowledge_base_id = None + data_source_ids: List[str] = [] + + for output in outputs: + if output["OutputKey"] == f"KnowledgeBaseId{knowledge_base_hash}": + knowledge_base_id = output["OutputValue"] + + elif output["OutputKey"].startswith(f"DataSource{knowledge_base_hash}"): + data_source_ids.append(output["OutputValue"]) + + if knowledge_base_id: + result["DataSources"].extend( + { + "KnowledgeBaseId": knowledge_base_id, + "DataSourceId": data_source_id, + } + for data_source_id in data_source_ids + ) + + bots = shared_knowledge_base["Bots"] + for bot in bots: + update_knowledge_base_id( + bot["OwnerUserId"], bot["BotId"], knowledge_base_id, data_source_ids + ) + + return result diff --git a/backend/embedding_statemachine/bedrock_knowledge_base/store_knowledge_base_id.py b/backend/embedding_statemachine/bedrock_knowledge_base/store_knowledge_base_id.py deleted file mode 100644 index 00ba8016a..000000000 --- a/backend/embedding_statemachine/bedrock_knowledge_base/store_knowledge_base_id.py +++ /dev/null @@ -1,58 +0,0 @@ -import logging -from typing import List - -from app.repositories.common import decompose_sk -from app.repositories.custom_bot import update_knowledge_base_id -from typing_extensions import TypedDict - -logger = logging.getLogger() -logger.setLevel(logging.INFO) - - -class StackItem(TypedDict): - KnowledgeBaseId: str - DataSourceId: str - PK: str - SK: str - - -class StackOutput(TypedDict): - KnowledgeBaseId: str - items: List[StackItem] - GuardrailArn: str - GuardrailVersion: str - - -def handler(event, context): - logger.info(f"Event: {event}") - pk = event["pk"] - sk = event["sk"] - stack_output: StackOutput = event["stack_output"] - - knowledge_base_id: str | None - data_source_ids: list[str] | None - - # Check if stack_output is valid - if not stack_output: - logger.warning("No stack_output received") - knowledge_base_id = None - data_source_ids = None - - else: - kb_id = stack_output.get("KnowledgeBaseId") - - # Filter out None values and ensure all elements are strings - ds_ids = [ - item["DataSourceId"] - for item in stack_output.get("items", []) - if item.get("DataSourceId") - ] - - knowledge_base_id = kb_id if kb_id else None - data_source_ids = ds_ids if kb_id else None - - user_id = pk - bot_id = decompose_sk(sk) - - if knowledge_base_id is not None: - update_knowledge_base_id(user_id, bot_id, knowledge_base_id, data_source_ids) diff --git a/backend/embedding_statemachine/bedrock_knowledge_base/update_bot_status.py b/backend/embedding_statemachine/bedrock_knowledge_base/update_bot_status.py index 1e88571eb..c450858ff 100644 --- a/backend/embedding_statemachine/bedrock_knowledge_base/update_bot_status.py +++ b/backend/embedding_statemachine/bedrock_knowledge_base/update_bot_status.py @@ -1,10 +1,8 @@ import json import logging -import os -from typing import Literal import boto3 -from app.repositories.common import compose_sk, decompose_sk, get_bot_table_client +from app.repositories.common import compose_sk, get_bot_table_client from app.routes.schemas.bot import type_sync_status from reretry import retry @@ -38,27 +36,33 @@ def update_sync_status( def extract_from_cause(cause_str: str) -> tuple: - logger.debug(f"Extracting PK and SK from cause: {cause_str}") + logger.debug(f"Extracting OWNER_USER_ID and BOT_ID from cause: {cause_str}") cause = json.loads(cause_str) logger.debug(f"Cause: {cause}") environment_variables = cause["Build"]["Environment"]["EnvironmentVariables"] logger.debug(f"Environment variables: {environment_variables}") - pk = next( - (item["Value"] for item in environment_variables if item["Name"] == "PK"), None + user_id = next( + ( + item["Value"] + for item in environment_variables + if item["Name"] == "OWNER_USER_ID" + ), + None, ) - sk = next( - (item["Value"] for item in environment_variables if item["Name"] == "SK"), None + bot_id = next( + (item["Value"] for item in environment_variables if item["Name"] == "BOT_ID"), + None, ) - if not pk or not sk: + if not user_id or not bot_id: raise ValueError("PK or SK not found in cause.") build_arn = cause["Build"].get("Arn", "") - logger.debug(f"PK: {pk}, SK: {sk}, Build ARN: {build_arn}") + logger.debug(f"PK: {user_id}, SK: {bot_id}, Build ARN: {build_arn}") - return pk, sk, build_arn + return user_id, bot_id, build_arn def handler(event, context): @@ -68,35 +72,32 @@ def handler(event, context): ingestion_job = event.get("ingestion_job", None) # Initialize variables - pk: str - sk: str + user_id: str + bot_id: str sync_status: type_sync_status sync_status_reason: str last_exec_id: str if cause: # UpdateSymcStatusFailed - pk, sk, build_arn = extract_from_cause(cause) + user_id, bot_id, build_arn = extract_from_cause(cause) sync_status = "FAILED" sync_status_reason = cause last_exec_id = build_arn elif ingestion_job: # UpdateSymcStatusFailedForIngestion - pk = event["pk"] - sk = event["sk"] + user_id = event["user_id"] + bot_id = event["bot_id"] sync_status = "FAILED" sync_status_reason = str(ingestion_job["ingestionJob"]["failureReasons"]) last_exec_id = ingestion_job["ingestionJob"]["ingestionJobId"] else: - pk = event["pk"] - sk = event["sk"] + user_id = event["user_id"] + bot_id = event["bot_id"] sync_status = event["sync_status"] sync_status_reason = event.get("sync_status_reason", "") last_exec_id = event.get("last_exec_id", "") - user_id = pk - bot_id = decompose_sk(sk) - logger.info( f"Updating sync status for bot {bot_id} of user {user_id} to {sync_status} with reason: {sync_status_reason}" ) diff --git a/backend/embedding_statemachine/guardrails/store_guardrail_arn.py b/backend/embedding_statemachine/guardrails/store_guardrail_arn.py deleted file mode 100644 index 1a0ee290e..000000000 --- a/backend/embedding_statemachine/guardrails/store_guardrail_arn.py +++ /dev/null @@ -1,61 +0,0 @@ -import logging -from typing import List, TypedDict - -from app.repositories.common import decompose_sk -from app.repositories.custom_bot import update_guardrails_params - -logger = logging.getLogger() -logger.setLevel(logging.INFO) - - -class StackItem(TypedDict): - KnowledgeBaseId: str - DataSourceId: str - PK: str - SK: str - - -class StackOutput(TypedDict): - """ - 'stack_output': { - 'KnowledgeBaseId': 'ABCDEFGHIJKL', - 'items': [ - { - 'KnowledgeBaseId': 'MNOPQRSTUVWX', - 'DataSourceId': 'YZABCDEFGHI', - 'PK': '7801e3f0-40b1-70da-2e13-652d4adce1c3', - 'SK': 'BOT#01JKWE8RP6YWNX9SKFSCCNS73Z' - } - ], - 'GuardrailArn': 'arn:aws:bedrock:us-east-1:123456789012:guardrail/abcdefghijkl', - 'GuardrailVersion': 'DRAFT', - } - """ - - KnowledgeBaseId: str - items: List[StackItem] - GuardrailArn: str - GuardrailVersion: str - - -def handler(event, context): - logger.info(f"Event: {event}") - pk = event["pk"] - sk = event["sk"] - stack_output: StackOutput = event["stack_output"] - - # Check if stack_output is valid - if not stack_output: - logger.warning("No stack_output received") - guardrail_arn = "" - guardrail_version = "" - - else: - guardrail_arn = stack_output.get("GuardrailArn", "") - guardrail_version = stack_output.get("GuardrailVersion", "") - - user_id = pk - bot_id = decompose_sk(sk) - - if guardrail_arn: - update_guardrails_params(user_id, bot_id, guardrail_arn, guardrail_version) diff --git a/cdk/bin/bedrock-custom-bot.ts b/cdk/bin/bedrock-custom-bot.ts index 00bb53013..43281e73b 100644 --- a/cdk/bin/bedrock-custom-bot.ts +++ b/cdk/bin/bedrock-custom-bot.ts @@ -86,102 +86,95 @@ const baseConfig: BaseConfig = { envName: params.envName, envPrefix: params.envPrefix, bedrockRegion: params.bedrockRegion, - ownerUserId: params.pk, - botId: params.sk.split("#")[1], + ownerUserId: params.ownerUserId, + botId: params.botId, documentBucketName: params.documentBucketName, enableRagReplicas: params.enableRagReplicas === true, }; const knowledgeConfig: KnowledgeConfig = { knowledgeBaseType: getKnowledgeBaseType(knowledgeBaseJson.type), - 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), - filenames: knowledgeJson.filenames.L.map((filename: any) => filename.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) + embeddingsModel: getEmbeddingModel(knowledgeBaseJson.embeddings_model), + parsingModel: getParsingModel(knowledgeBaseJson.parsing_model), + existKnowledgeBaseId: knowledgeBaseJson.exist_knowledge_base_id, + existingS3Urls: knowledgeJson.s3_urls.map((s3Url: any) => s3Url), + filenames: knowledgeJson.filenames.map((filename: any) => filename), + sourceUrls: knowledgeJson.source_urls.map((sourceUrl: any) => sourceUrl), + instruction: knowledgeBaseJson.instruction, + analyzer: knowledgeBaseJson.open_search?.analyzer + ? getAnalyzer(knowledgeBaseJson.open_search.analyzer) : undefined, }; // Extract chunking configuration const chunkingParams = { - maxTokens: knowledgeBaseJson.chunking_configuration.M.max_tokens - ? Number(knowledgeBaseJson.chunking_configuration.M.max_tokens.N) + maxTokens: knowledgeBaseJson.chunking_configuration?.max_tokens + ? knowledgeBaseJson.chunking_configuration.max_tokens : undefined, - overlapPercentage: knowledgeBaseJson.chunking_configuration.M - .overlap_percentage - ? Number(knowledgeBaseJson.chunking_configuration.M.overlap_percentage.N) + overlapPercentage: knowledgeBaseJson.chunking_configuration?.overlap_percentage + ? knowledgeBaseJson.chunking_configuration.overlap_percentage : undefined, - overlapTokens: knowledgeBaseJson.chunking_configuration.M.overlap_tokens - ? Number(knowledgeBaseJson.chunking_configuration.M.overlap_tokens.N) + overlapTokens: knowledgeBaseJson.chunking_configuration?.overlap_tokens + ? knowledgeBaseJson.chunking_configuration.overlap_tokens : undefined, - maxParentTokenSize: knowledgeBaseJson.chunking_configuration.M - .max_parent_token_size - ? Number(knowledgeBaseJson.chunking_configuration.M.max_parent_token_size.N) + maxParentTokenSize: knowledgeBaseJson.chunking_configuration?.max_parent_token_size + ? knowledgeBaseJson.chunking_configuration.max_parent_token_size : undefined, - maxChildTokenSize: knowledgeBaseJson.chunking_configuration.M - .max_child_token_size - ? Number(knowledgeBaseJson.chunking_configuration.M.max_child_token_size.N) + maxChildTokenSize: knowledgeBaseJson.chunking_configuration?.max_child_token_size + ? knowledgeBaseJson.chunking_configuration.max_child_token_size : undefined, - bufferSize: knowledgeBaseJson.chunking_configuration.M.buffer_size - ? Number(knowledgeBaseJson.chunking_configuration.M.buffer_size.N) + bufferSize: knowledgeBaseJson.chunking_configuration?.buffer_size + ? knowledgeBaseJson.chunking_configuration.buffer_size : undefined, - breakpointPercentileThreshold: knowledgeBaseJson.chunking_configuration.M - .breakpoint_percentile_threshold - ? Number( - knowledgeBaseJson.chunking_configuration.M - .breakpoint_percentile_threshold.N - ) + breakpointPercentileThreshold: knowledgeBaseJson.chunking_configuration?.breakpoint_percentile_threshold + ? knowledgeBaseJson.chunking_configuration.breakpoint_percentile_threshold : undefined, }; const chunkingConfig: ChunkingConfig = { ...chunkingParams, chunkingStrategy: getChunkingStrategy( - knowledgeBaseJson.chunking_configuration.M.chunking_strategy.S, - knowledgeBaseJson.embeddings_model.S, + knowledgeBaseJson.chunking_configuration?.chunking_strategy, + knowledgeBaseJson.embeddings_model, chunkingParams ), }; const crawlingConfig: CrawlingConfig = { - crawlingScope: getCrowlingScope(knowledgeBaseJson.web_crawling_scope.S), - crawlingFilters: getCrawlingFilters(knowledgeBaseJson.web_crawling_filters.M), + crawlingScope: getCrowlingScope(knowledgeBaseJson.web_crawling_scope), + crawlingFilters: getCrawlingFilters(knowledgeBaseJson.web_crawling_filters), }; const guardrailConfig: GuardrailConfig = { is_guardrail_enabled: guardrailsJson.is_guardrail_enabled - ? Boolean(guardrailsJson.is_guardrail_enabled.BOOL) + ? guardrailsJson.is_guardrail_enabled : undefined, hateThreshold: guardrailsJson.hate_threshold - ? Number(guardrailsJson.hate_threshold.N) + ? guardrailsJson.hate_threshold : undefined, insultsThreshold: guardrailsJson.insults_threshold - ? Number(guardrailsJson.insults_threshold.N) + ? guardrailsJson.insults_threshold : undefined, sexualThreshold: guardrailsJson.sexual_threshold - ? Number(guardrailsJson.sexual_threshold.N) + ? guardrailsJson.sexual_threshold : undefined, violenceThreshold: guardrailsJson.violence_threshold - ? Number(guardrailsJson.violence_threshold.N) + ? guardrailsJson.violence_threshold : undefined, misconductThreshold: guardrailsJson.misconduct_threshold - ? Number(guardrailsJson.misconduct_threshold.N) + ? guardrailsJson.misconduct_threshold : undefined, groundingThreshold: guardrailsJson.grounding_threshold - ? Number(guardrailsJson.grounding_threshold.N) + ? guardrailsJson.grounding_threshold : undefined, relevanceThreshold: guardrailsJson.relevance_threshold - ? Number(guardrailsJson.relevance_threshold.N) + ? guardrailsJson.relevance_threshold : undefined, guardrailArn: guardrailsJson.guardrail_arn - ? Number(guardrailsJson.guardrail_arn.N) + ? guardrailsJson.guardrail_arn : undefined, guardrailVersion: guardrailsJson.guardrail_version - ? Number(guardrailsJson.guardrail_version.N) + ? guardrailsJson.guardrail_version : undefined, }; diff --git a/cdk/bin/bedrock-shared-knowledge-bases.ts b/cdk/bin/bedrock-shared-knowledge-bases.ts new file mode 100644 index 000000000..f3f902ff9 --- /dev/null +++ b/cdk/bin/bedrock-shared-knowledge-bases.ts @@ -0,0 +1,165 @@ +import "source-map-support/register"; +import * as cdk from "aws-cdk-lib"; +import { BedrockSharedKnowledgeBasesStack } from "../lib/bedrock-shared-knowledge-bases-stack"; +import { + getEmbeddingModel, + getChunkingStrategy, + getAnalyzer, + getParsingModel, +} 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 { Analyzer } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/opensearch-vectorindex"; +import { resolveBedrockSharedKnowledgeBasesParameters } from "../lib/utils/parameter-models"; + +const app = new cdk.App(); + +// Get parameters specific to Bedrock Custom Bot +const params = resolveBedrockSharedKnowledgeBasesParameters(); + +// Parse JSON strings into objects +const sharedKnowledgeBasesJson = JSON.parse(params.sharedKnowledgeBases); + +// Define interfaces for typed configuration objects +interface BaseConfig { + envName: string; + envPrefix: string; + bedrockRegion: string; + documentBucketName: string; + enableRagReplicas: boolean; +} + +interface KnowledgeConfig { + knowledgeBaseHash: string; + embeddingsModel: BedrockFoundationModel; + parsingModel: BedrockFoundationModel | undefined; + instruction?: string; + analyzer?: Analyzer | undefined; +} + +interface ChunkingConfig { + chunkingStrategy: ChunkingStrategy; + maxTokens?: number; + overlapPercentage?: number; + overlapTokens?: number; + maxParentTokenSize?: number; + maxChildTokenSize?: number; + bufferSize?: number; + breakpointPercentileThreshold?: number; +} + +// Extract and organize configuration by category +const baseConfig: BaseConfig = { + envName: params.envName, + envPrefix: params.envPrefix, + bedrockRegion: params.bedrockRegion, + documentBucketName: params.documentBucketName, + enableRagReplicas: params.enableRagReplicas === true, +}; + +const knowledgeBases = sharedKnowledgeBasesJson.map((sharedKnowledgeBase: any) => { + const knowledgeBaseJson = sharedKnowledgeBase.KnowledgeBase; + const knowledgeBaseHash = sharedKnowledgeBase.KnowledgeBaseHash; + const knowledgeConfig: KnowledgeConfig = { + knowledgeBaseHash: knowledgeBaseHash, + embeddingsModel: getEmbeddingModel(knowledgeBaseJson.embeddings_model), + parsingModel: getParsingModel(knowledgeBaseJson.parsing_model), + instruction: knowledgeBaseJson.instruction, + analyzer: knowledgeBaseJson.open_search?.analyzer + ? getAnalyzer(knowledgeBaseJson.open_search.analyzer) + : undefined, + }; + + // Extract chunking configuration + const chunkingParams = { + maxTokens: knowledgeBaseJson.chunking_configuration?.max_tokens + ? knowledgeBaseJson.chunking_configuration.max_tokens + : undefined, + overlapPercentage: knowledgeBaseJson.chunking_configuration?.overlap_percentage + ? knowledgeBaseJson.chunking_configuration.overlap_percentage + : undefined, + overlapTokens: knowledgeBaseJson.chunking_configuration?.overlap_tokens + ? knowledgeBaseJson.chunking_configuration.overlap_tokens + : undefined, + maxParentTokenSize: knowledgeBaseJson.chunking_configuration?.max_parent_token_size + ? knowledgeBaseJson.chunking_configuration.max_parent_token_size + : undefined, + maxChildTokenSize: knowledgeBaseJson.chunking_configuration?.max_child_token_size + ? knowledgeBaseJson.chunking_configuration.max_child_token_size + : undefined, + bufferSize: knowledgeBaseJson.chunking_configuration?.buffer_size + ? knowledgeBaseJson.chunking_configuration.buffer_size + : undefined, + breakpointPercentileThreshold: knowledgeBaseJson.chunking_configuration?.breakpoint_percentile_threshold + ? knowledgeBaseJson.chunking_configuration.breakpoint_percentile_threshold + : undefined, + }; + + const chunkingConfig: ChunkingConfig = { + ...chunkingParams, + chunkingStrategy: getChunkingStrategy( + knowledgeBaseJson.chunking_configuration?.chunking_strategy, + knowledgeBaseJson.embeddings_model, + chunkingParams + ), + }; + + // Log organized configurations for debugging + console.log("Base Configuration:", JSON.stringify(baseConfig, null, 2)); + console.log( + "Knowledge Configuration:", + JSON.stringify( + { + ...knowledgeConfig, + embeddingsModel: knowledgeConfig.embeddingsModel.toString(), + parsingModel: knowledgeConfig.parsingModel?.toString(), + analyzer: knowledgeConfig.analyzer ? "configured" : "undefined", + }, + null, + 2 + ) + ); + console.log( + "Chunking Configuration:", + JSON.stringify( + { + ...chunkingConfig, + chunkingStrategy: chunkingConfig.chunkingStrategy.toString(), + }, + null, + 2 + ) + ); + return { + knowledgeConfig, + chunkingConfig, + }; +}); + +// Create the stack +new BedrockSharedKnowledgeBasesStack(app, "BrChatSharedKbStack", { + // Environment configuration + env: { + region: baseConfig.bedrockRegion, + }, + + // Base configuration + documentBucketName: baseConfig.documentBucketName, + enableRagReplicas: baseConfig.enableRagReplicas, + + knowledgeBases: knowledgeBases.map((knowledgeBase: any) => ({ + // Knowledge base configuration + knowledgeBaseHash: knowledgeBase.knowledgeConfig.knowledgeBaseHash, + embeddingsModel: knowledgeBase.knowledgeConfig.embeddingsModel, + parsingModel: knowledgeBase.knowledgeConfig.parsingModel, + instruction: knowledgeBase.knowledgeConfig.instruction, + analyzer: knowledgeBase.knowledgeConfig.analyzer, + + // Chunking configuration + chunkingStrategy: knowledgeBase.chunkingConfig.chunkingStrategy, + maxTokens: knowledgeBase.chunkingConfig.maxTokens, + overlapPercentage: knowledgeBase.chunkingConfig.overlapPercentage, + })), +}); + +cdk.Tags.of(app).add("CDKEnvironment", baseConfig.envName); diff --git a/cdk/lambda/knowledge-base-custom-transformation/index.ts b/cdk/lambda/knowledge-base-custom-transformation/index.ts new file mode 100644 index 000000000..ff1dc74a8 --- /dev/null +++ b/cdk/lambda/knowledge-base-custom-transformation/index.ts @@ -0,0 +1,26 @@ +import { type Handler } from 'aws-lambda'; + +export const handler: Handler = async (event, context) => { + console.log(`Event: ${JSON.stringify(event)}`); + + const inputFiles = event.inputFiles as any[]; + + return { + outputFiles: inputFiles.map(file => { + const originalFileLocation = file.originalFileLocation; + const s3Uri = new URL(originalFileLocation.s3_location.uri); + const botId = s3Uri.pathname.match(/^\/(?[^/]+)\/(?[^/]+)\/documents\/(?[^/]+)$/)?.groups?.botId; + return { + originalFileLocation, + ...(botId != null ? { + fileMetadata: { + tenants: [ + `BOT#${botId}`, + ], + }, + } : undefined), + contentBatches: file.contentBatches, + }; + }), + }; +}; diff --git a/cdk/lambda/knowledge-base-custom-transformation/package-lock.json b/cdk/lambda/knowledge-base-custom-transformation/package-lock.json new file mode 100644 index 000000000..d7c2aca5a --- /dev/null +++ b/cdk/lambda/knowledge-base-custom-transformation/package-lock.json @@ -0,0 +1,2185 @@ +{ + "name": "transformation", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "transformation", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@aws-sdk/client-s3": "^3.901.0" + }, + "devDependencies": { + "@types/aws-lambda": "^8.10.155", + "@types/node": "^24.6.2", + "esbuild": "^0.25.10", + "typescript": "^5.9.3" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.901.0.tgz", + "integrity": "sha512-wyKhZ51ur1tFuguZ6PgrUsot9KopqD0Tmxw8O8P/N3suQDxFPr0Yo7Y77ezDRDZQ95Ml3C0jlvx79HCo8VxdWA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.901.0", + "@aws-sdk/credential-provider-node": "3.901.0", + "@aws-sdk/middleware-bucket-endpoint": "3.901.0", + "@aws-sdk/middleware-expect-continue": "3.901.0", + "@aws-sdk/middleware-flexible-checksums": "3.901.0", + "@aws-sdk/middleware-host-header": "3.901.0", + "@aws-sdk/middleware-location-constraint": "3.901.0", + "@aws-sdk/middleware-logger": "3.901.0", + "@aws-sdk/middleware-recursion-detection": "3.901.0", + "@aws-sdk/middleware-sdk-s3": "3.901.0", + "@aws-sdk/middleware-ssec": "3.901.0", + "@aws-sdk/middleware-user-agent": "3.901.0", + "@aws-sdk/region-config-resolver": "3.901.0", + "@aws-sdk/signature-v4-multi-region": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@aws-sdk/util-endpoints": "3.901.0", + "@aws-sdk/util-user-agent-browser": "3.901.0", + "@aws-sdk/util-user-agent-node": "3.901.0", + "@aws-sdk/xml-builder": "3.901.0", + "@smithy/config-resolver": "^4.3.0", + "@smithy/core": "^3.14.0", + "@smithy/eventstream-serde-browser": "^4.2.0", + "@smithy/eventstream-serde-config-resolver": "^4.3.0", + "@smithy/eventstream-serde-node": "^4.2.0", + "@smithy/fetch-http-handler": "^5.3.0", + "@smithy/hash-blob-browser": "^4.2.0", + "@smithy/hash-node": "^4.2.0", + "@smithy/hash-stream-node": "^4.2.0", + "@smithy/invalid-dependency": "^4.2.0", + "@smithy/md5-js": "^4.2.0", + "@smithy/middleware-content-length": "^4.2.0", + "@smithy/middleware-endpoint": "^4.3.0", + "@smithy/middleware-retry": "^4.4.0", + "@smithy/middleware-serde": "^4.2.0", + "@smithy/middleware-stack": "^4.2.0", + "@smithy/node-config-provider": "^4.3.0", + "@smithy/node-http-handler": "^4.3.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/smithy-client": "^4.7.0", + "@smithy/types": "^4.6.0", + "@smithy/url-parser": "^4.2.0", + "@smithy/util-base64": "^4.2.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.0", + "@smithy/util-defaults-mode-browser": "^4.2.0", + "@smithy/util-defaults-mode-node": "^4.2.0", + "@smithy/util-endpoints": "^3.2.0", + "@smithy/util-middleware": "^4.2.0", + "@smithy/util-retry": "^4.2.0", + "@smithy/util-stream": "^4.4.0", + "@smithy/util-utf8": "^4.2.0", + "@smithy/util-waiter": "^4.2.0", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.901.0.tgz", + "integrity": "sha512-sGyDjjkJ7ppaE+bAKL/Q5IvVCxtoyBIzN+7+hWTS/mUxWJ9EOq9238IqmVIIK6sYNIzEf9yhobfMARasPYVTNg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.901.0", + "@aws-sdk/middleware-host-header": "3.901.0", + "@aws-sdk/middleware-logger": "3.901.0", + "@aws-sdk/middleware-recursion-detection": "3.901.0", + "@aws-sdk/middleware-user-agent": "3.901.0", + "@aws-sdk/region-config-resolver": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@aws-sdk/util-endpoints": "3.901.0", + "@aws-sdk/util-user-agent-browser": "3.901.0", + "@aws-sdk/util-user-agent-node": "3.901.0", + "@smithy/config-resolver": "^4.3.0", + "@smithy/core": "^3.14.0", + "@smithy/fetch-http-handler": "^5.3.0", + "@smithy/hash-node": "^4.2.0", + "@smithy/invalid-dependency": "^4.2.0", + "@smithy/middleware-content-length": "^4.2.0", + "@smithy/middleware-endpoint": "^4.3.0", + "@smithy/middleware-retry": "^4.4.0", + "@smithy/middleware-serde": "^4.2.0", + "@smithy/middleware-stack": "^4.2.0", + "@smithy/node-config-provider": "^4.3.0", + "@smithy/node-http-handler": "^4.3.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/smithy-client": "^4.7.0", + "@smithy/types": "^4.6.0", + "@smithy/url-parser": "^4.2.0", + "@smithy/util-base64": "^4.2.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.0", + "@smithy/util-defaults-mode-browser": "^4.2.0", + "@smithy/util-defaults-mode-node": "^4.2.0", + "@smithy/util-endpoints": "^3.2.0", + "@smithy/util-middleware": "^4.2.0", + "@smithy/util-retry": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.901.0.tgz", + "integrity": "sha512-brKAc3y64tdhyuEf+OPIUln86bRTqkLgb9xkd6kUdIeA5+qmp/N6amItQz+RN4k4O3kqkCPYnAd3LonTKluobw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.901.0", + "@aws-sdk/xml-builder": "3.901.0", + "@smithy/core": "^3.14.0", + "@smithy/node-config-provider": "^4.3.0", + "@smithy/property-provider": "^4.2.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/signature-v4": "^5.3.0", + "@smithy/smithy-client": "^4.7.0", + "@smithy/types": "^4.6.0", + "@smithy/util-base64": "^4.2.0", + "@smithy/util-middleware": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.901.0.tgz", + "integrity": "sha512-5hAdVl3tBuARh3zX5MLJ1P/d+Kr5kXtDU3xm1pxUEF4xt2XkEEpwiX5fbkNkz2rbh3BCt2gOHsAbh6b3M7n+DA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@smithy/property-provider": "^4.2.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.901.0.tgz", + "integrity": "sha512-Ggr7+0M6QZEsrqRkK7iyJLf4LkIAacAxHz9c4dm9hnDdU7vqrlJm6g73IxMJXWN1bIV7IxfpzB11DsRrB/oNjQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@smithy/fetch-http-handler": "^5.3.0", + "@smithy/node-http-handler": "^4.3.0", + "@smithy/property-provider": "^4.2.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/smithy-client": "^4.7.0", + "@smithy/types": "^4.6.0", + "@smithy/util-stream": "^4.4.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.901.0.tgz", + "integrity": "sha512-zxadcDS0hNJgv8n4hFYJNOXyfjaNE1vvqIiF/JzZSQpSSYXzCd+WxXef5bQh+W3giDtRUmkvP5JLbamEFjZKyw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.901.0", + "@aws-sdk/credential-provider-env": "3.901.0", + "@aws-sdk/credential-provider-http": "3.901.0", + "@aws-sdk/credential-provider-process": "3.901.0", + "@aws-sdk/credential-provider-sso": "3.901.0", + "@aws-sdk/credential-provider-web-identity": "3.901.0", + "@aws-sdk/nested-clients": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@smithy/credential-provider-imds": "^4.2.0", + "@smithy/property-provider": "^4.2.0", + "@smithy/shared-ini-file-loader": "^4.3.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.901.0.tgz", + "integrity": "sha512-dPuFzMF7L1s/lQyT3wDxqLe82PyTH+5o1jdfseTEln64LJMl0ZMWaKX/C1UFNDxaTd35Cgt1bDbjjAWHMiKSFQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.901.0", + "@aws-sdk/credential-provider-http": "3.901.0", + "@aws-sdk/credential-provider-ini": "3.901.0", + "@aws-sdk/credential-provider-process": "3.901.0", + "@aws-sdk/credential-provider-sso": "3.901.0", + "@aws-sdk/credential-provider-web-identity": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@smithy/credential-provider-imds": "^4.2.0", + "@smithy/property-provider": "^4.2.0", + "@smithy/shared-ini-file-loader": "^4.3.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.901.0.tgz", + "integrity": "sha512-/IWgmgM3Cl1wTdJA5HqKMAojxLkYchh5kDuphApxKhupLu6Pu0JBOHU8A5GGeFvOycyaVwosod6zDduINZxe+A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@smithy/property-provider": "^4.2.0", + "@smithy/shared-ini-file-loader": "^4.3.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.901.0.tgz", + "integrity": "sha512-SjmqZQHmqFSET7+6xcZgtH7yEyh5q53LN87GqwYlJZ6KJ5oNw11acUNEhUOL1xTSJEvaWqwTIkS2zqrzLcM9bw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.901.0", + "@aws-sdk/core": "3.901.0", + "@aws-sdk/token-providers": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@smithy/property-provider": "^4.2.0", + "@smithy/shared-ini-file-loader": "^4.3.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.901.0.tgz", + "integrity": "sha512-NYjy/6NLxH9m01+pfpB4ql8QgAorJcu8tw69kzHwUd/ql6wUDTbC7HcXqtKlIwWjzjgj2BKL7j6SyFapgCuafA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.901.0", + "@aws-sdk/nested-clients": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@smithy/property-provider": "^4.2.0", + "@smithy/shared-ini-file-loader": "^4.3.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.901.0.tgz", + "integrity": "sha512-mPF3N6eZlVs9G8aBSzvtoxR1RZqMo1aIwR+X8BAZSkhfj55fVF2no4IfPXfdFO3I66N+zEQ8nKoB0uTATWrogQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.901.0", + "@aws-sdk/util-arn-parser": "3.893.0", + "@smithy/node-config-provider": "^4.3.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/types": "^4.6.0", + "@smithy/util-config-provider": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.901.0.tgz", + "integrity": "sha512-bwq9nj6MH38hlJwOY9QXIDwa6lI48UsaZpaXbdD71BljEIRlxDzfB4JaYb+ZNNK7RIAdzsP/K05mJty6KJAQHw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.901.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.901.0.tgz", + "integrity": "sha512-63lcKfggVUFyXhE4SsFXShCTCyh7ZHEqXLyYEL4DwX+VWtxutf9t9m3fF0TNUYDE8eEGWiRXhegj8l4FjuW+wA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/node-config-provider": "^4.3.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/types": "^4.6.0", + "@smithy/util-middleware": "^4.2.0", + "@smithy/util-stream": "^4.4.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.901.0.tgz", + "integrity": "sha512-yWX7GvRmqBtbNnUW7qbre3GvZmyYwU0WHefpZzDTYDoNgatuYq6LgUIQ+z5C04/kCRoFkAFrHag8a3BXqFzq5A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.901.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.901.0.tgz", + "integrity": "sha512-MuCS5R2ngNoYifkVt05CTULvYVWX0dvRT0/Md4jE3a0u0yMygYy31C1zorwfE/SUgAQXyLmUx8ATmPp9PppImQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.901.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.901.0.tgz", + "integrity": "sha512-UoHebjE7el/tfRo8/CQTj91oNUm+5Heus5/a4ECdmWaSCHCS/hXTsU3PTTHAY67oAQR8wBLFPfp3mMvXjB+L2A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.901.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.901.0.tgz", + "integrity": "sha512-Wd2t8qa/4OL0v/oDpCHHYkgsXJr8/ttCxrvCKAt0H1zZe2LlRhY9gpDVKqdertfHrHDj786fOvEQA28G1L75Dg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.901.0", + "@aws/lambda-invoke-store": "^0.0.1", + "@smithy/protocol-http": "^5.3.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.901.0.tgz", + "integrity": "sha512-prgjVC3fDT2VIlmQPiw/cLee8r4frTam9GILRUVQyDdNtshNwV3MiaSCLzzQJjKJlLgnBLNUHJCSmvUVtg+3iA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@aws-sdk/util-arn-parser": "3.893.0", + "@smithy/core": "^3.14.0", + "@smithy/node-config-provider": "^4.3.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/signature-v4": "^5.3.0", + "@smithy/smithy-client": "^4.7.0", + "@smithy/types": "^4.6.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.0", + "@smithy/util-stream": "^4.4.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.901.0.tgz", + "integrity": "sha512-YiLLJmA3RvjL38mFLuu8fhTTGWtp2qT24VqpucgfoyziYcTgIQkJJmKi90Xp6R6/3VcArqilyRgM1+x8i/em+Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.901.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.901.0.tgz", + "integrity": "sha512-Zby4F03fvD9xAgXGPywyk4bC1jCbnyubMEYChLYohD+x20ULQCf+AimF/Btn7YL+hBpzh1+RmqmvZcx+RgwgNQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@aws-sdk/util-endpoints": "3.901.0", + "@smithy/core": "^3.14.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.901.0.tgz", + "integrity": "sha512-feAAAMsVwctk2Tms40ONybvpfJPLCmSdI+G+OTrNpizkGLNl6ik2Ng2RzxY6UqOfN8abqKP/DOUj1qYDRDG8ag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.901.0", + "@aws-sdk/middleware-host-header": "3.901.0", + "@aws-sdk/middleware-logger": "3.901.0", + "@aws-sdk/middleware-recursion-detection": "3.901.0", + "@aws-sdk/middleware-user-agent": "3.901.0", + "@aws-sdk/region-config-resolver": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@aws-sdk/util-endpoints": "3.901.0", + "@aws-sdk/util-user-agent-browser": "3.901.0", + "@aws-sdk/util-user-agent-node": "3.901.0", + "@smithy/config-resolver": "^4.3.0", + "@smithy/core": "^3.14.0", + "@smithy/fetch-http-handler": "^5.3.0", + "@smithy/hash-node": "^4.2.0", + "@smithy/invalid-dependency": "^4.2.0", + "@smithy/middleware-content-length": "^4.2.0", + "@smithy/middleware-endpoint": "^4.3.0", + "@smithy/middleware-retry": "^4.4.0", + "@smithy/middleware-serde": "^4.2.0", + "@smithy/middleware-stack": "^4.2.0", + "@smithy/node-config-provider": "^4.3.0", + "@smithy/node-http-handler": "^4.3.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/smithy-client": "^4.7.0", + "@smithy/types": "^4.6.0", + "@smithy/url-parser": "^4.2.0", + "@smithy/util-base64": "^4.2.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.0", + "@smithy/util-defaults-mode-browser": "^4.2.0", + "@smithy/util-defaults-mode-node": "^4.2.0", + "@smithy/util-endpoints": "^3.2.0", + "@smithy/util-middleware": "^4.2.0", + "@smithy/util-retry": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.901.0.tgz", + "integrity": "sha512-7F0N888qVLHo4CSQOsnkZ4QAp8uHLKJ4v3u09Ly5k4AEStrSlFpckTPyUx6elwGL+fxGjNE2aakK8vEgzzCV0A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.901.0", + "@smithy/node-config-provider": "^4.3.0", + "@smithy/types": "^4.6.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.901.0.tgz", + "integrity": "sha512-2IWxbll/pRucp1WQkHi2W5E2SVPGBvk4Is923H7gpNksbVFws18ItjMM8ZpGm44cJEoy1zR5gjhLFklatpuoOw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/signature-v4": "^5.3.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.901.0.tgz", + "integrity": "sha512-pJEr1Ggbc/uVTDqp9IbNu9hdr0eQf3yZix3s4Nnyvmg4xmJSGAlbPC9LrNr5u3CDZoc8Z9CuLrvbP4MwYquNpQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.901.0", + "@aws-sdk/nested-clients": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@smithy/property-provider": "^4.2.0", + "@smithy/shared-ini-file-loader": "^4.3.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.901.0.tgz", + "integrity": "sha512-FfEM25hLEs4LoXsLXQ/q6X6L4JmKkKkbVFpKD4mwfVHtRVQG6QxJiCPcrkcPISquiy6esbwK2eh64TWbiD60cg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.893.0.tgz", + "integrity": "sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.901.0.tgz", + "integrity": "sha512-5nZP3hGA8FHEtKvEQf4Aww5QZOkjLW1Z+NixSd+0XKfHvA39Ah5sZboScjLx0C9kti/K3OGW1RCx5K9Zc3bZqg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.901.0", + "@smithy/types": "^4.6.0", + "@smithy/url-parser": "^4.2.0", + "@smithy/util-endpoints": "^3.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.893.0.tgz", + "integrity": "sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.901.0.tgz", + "integrity": "sha512-Ntb6V/WFI21Ed4PDgL/8NSfoZQQf9xzrwNgiwvnxgAl/KvAvRBgQtqj5gHsDX8Nj2YmJuVoHfH9BGjL9VQ4WNg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.901.0", + "@smithy/types": "^4.6.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.901.0.tgz", + "integrity": "sha512-l59KQP5TY7vPVUfEURc7P5BJKuNg1RSsAKBQW7LHLECXjLqDUbo2SMLrexLBEoArSt6E8QOrIN0C8z/0Xk0jYw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.901.0", + "@aws-sdk/types": "3.901.0", + "@smithy/node-config-provider": "^4.3.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.901.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.901.0.tgz", + "integrity": "sha512-pxFCkuAP7Q94wMTNPAwi6hEtNrp/BdFf+HOrIEeFQsk4EoOmpKY3I6S+u6A9Wg295J80Kh74LqDWM22ux3z6Aw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.6.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.0.1.tgz", + "integrity": "sha512-ORHRQ2tmvnBXc8t/X9Z8IcSbBA4xTLKuN873FopzklHMeqBst7YG0d+AX97inkvDX+NChYtSr+qGfcqGFaI8Zw==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.0.tgz", + "integrity": "sha512-PLUYa+SUKOEZtXFURBu/CNxlsxfaFGxSBPcStL13KpVeVWIfdezWyDqkz7iDLmwnxojXD0s5KzuB5HGHvt4Aeg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.0.tgz", + "integrity": "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.0.tgz", + "integrity": "sha512-HNbGWdyTfSM1nfrZKQjYTvD8k086+M8s1EYkBUdGC++lhxegUp2HgNf5RIt6oOGVvsC26hBCW/11tv8KbwLn/Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-base64": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.3.0.tgz", + "integrity": "sha512-9oH+n8AVNiLPK/iK/agOsoWfrKZ3FGP3502tkksd6SRsKMYiu7AFX0YXo6YBADdsAj7C+G/aLKdsafIJHxuCkQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.0", + "@smithy/types": "^4.6.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.14.0.tgz", + "integrity": "sha512-XJ4z5FxvY/t0Dibms/+gLJrI5niRoY0BCmE02fwmPcRYFPI4KI876xaE79YGWIKnEslMbuQPsIEsoU/DXa0DoA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.2.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/types": "^4.6.0", + "@smithy/util-base64": "^4.2.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-middleware": "^4.2.0", + "@smithy/util-stream": "^4.4.0", + "@smithy/util-utf8": "^4.2.0", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.0.tgz", + "integrity": "sha512-SOhFVvFH4D5HJZytb0bLKxCrSnwcqPiNlrw+S4ZXjMnsC+o9JcUQzbZOEQcA8yv9wJFNhfsUiIUKiEnYL68Big==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.0", + "@smithy/property-provider": "^4.2.0", + "@smithy/types": "^4.6.0", + "@smithy/url-parser": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.0.tgz", + "integrity": "sha512-XE7CtKfyxYiNZ5vz7OvyTf1osrdbJfmUy+rbh+NLQmZumMGvY0mT0Cq1qKSfhrvLtRYzMsOBuRpi10dyI0EBPg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.6.0", + "@smithy/util-hex-encoding": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.0.tgz", + "integrity": "sha512-U53p7fcrk27k8irLhOwUu+UYnBqsXNLKl1XevOpsxK3y1Lndk8R7CSiZV6FN3fYFuTPuJy5pP6qa/bjDzEkRvA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.0.tgz", + "integrity": "sha512-uwx54t8W2Yo9Jr3nVF5cNnkAAnMCJ8Wrm+wDlQY6rY/IrEgZS3OqagtCu/9ceIcZFQ1zVW/zbN9dxb5esuojfA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.0.tgz", + "integrity": "sha512-yjM2L6QGmWgJjVu/IgYd6hMzwm/tf4VFX0lm8/SvGbGBwc+aFl3hOzvO/e9IJ2XI+22Tx1Zg3vRpFRs04SWFcg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.0.tgz", + "integrity": "sha512-C3jxz6GeRzNyGKhU7oV656ZbuHY93mrfkT12rmjDdZch142ykjn8do+VOkeRNjSGKw01p4g+hdalPYPhmMwk1g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^4.2.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.0.tgz", + "integrity": "sha512-BG3KSmsx9A//KyIfw+sqNmWFr1YBUr+TwpxFT7yPqAk0yyDh7oSNgzfNH7pS6OC099EGx2ltOULvumCFe8bcgw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.0", + "@smithy/querystring-builder": "^4.2.0", + "@smithy/types": "^4.6.0", + "@smithy/util-base64": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.0.tgz", + "integrity": "sha512-MWmrRTPqVKpN8NmxmJPTeQuhewTt8Chf+waB38LXHZoA02+BeWYVQ9ViAwHjug8m7lQb1UWuGqp3JoGDOWvvuA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/chunked-blob-reader": "^5.2.0", + "@smithy/chunked-blob-reader-native": "^4.2.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.0.tgz", + "integrity": "sha512-ugv93gOhZGysTctZh9qdgng8B+xO0cj+zN0qAZ+Sgh7qTQGPOJbMdIuyP89KNfUyfAqFSNh5tMvC+h2uCpmTtA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.6.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.0.tgz", + "integrity": "sha512-8dELAuGv+UEjtzrpMeNBZc1sJhO8GxFVV/Yh21wE35oX4lOE697+lsMHBoUIFAUuYkTMIeu0EuJSEsH7/8Y+UQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.6.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.0.tgz", + "integrity": "sha512-ZmK5X5fUPAbtvRcUPtk28aqIClVhbfcmfoS4M7UQBTnDdrNxhsrxYVv0ZEl5NaPSyExsPWqL4GsPlRvtlwg+2A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.0.tgz", + "integrity": "sha512-LFEPniXGKRQArFmDQ3MgArXlClFJMsXDteuQQY8WG1/zzv6gVSo96+qpkuu1oJp4MZsKrwchY0cuAoPKzEbaNA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.6.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.0.tgz", + "integrity": "sha512-6ZAnwrXFecrA4kIDOcz6aLBhU5ih2is2NdcZtobBDSdSHtE9a+MThB5uqyK4XXesdOCvOcbCm2IGB95birTSOQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.0.tgz", + "integrity": "sha512-jFVjuQeV8TkxaRlcCNg0GFVgg98tscsmIrIwRFeC74TIUyLE3jmY9xgc1WXrPQYRjQNK3aRoaIk6fhFRGOIoGw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.14.0", + "@smithy/middleware-serde": "^4.2.0", + "@smithy/node-config-provider": "^4.3.0", + "@smithy/shared-ini-file-loader": "^4.3.0", + "@smithy/types": "^4.6.0", + "@smithy/url-parser": "^4.2.0", + "@smithy/util-middleware": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.0.tgz", + "integrity": "sha512-yaVBR0vQnOnzex45zZ8ZrPzUnX73eUC8kVFaAAbn04+6V7lPtxn56vZEBBAhgS/eqD6Zm86o6sJs6FuQVoX5qg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/service-error-classification": "^4.2.0", + "@smithy/smithy-client": "^4.7.0", + "@smithy/types": "^4.6.0", + "@smithy/util-middleware": "^4.2.0", + "@smithy/util-retry": "^4.2.0", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.0.tgz", + "integrity": "sha512-rpTQ7D65/EAbC6VydXlxjvbifTf4IH+sADKg6JmAvhkflJO2NvDeyU9qsWUNBelJiQFcXKejUHWRSdmpJmEmiw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.0.tgz", + "integrity": "sha512-G5CJ//eqRd9OARrQu9MK1H8fNm2sMtqFh6j8/rPozhEL+Dokpvi1Og+aCixTuwDAGZUkJPk6hJT5jchbk/WCyg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.0.tgz", + "integrity": "sha512-5QgHNuWdT9j9GwMPPJCKxy2KDxZ3E5l4M3/5TatSZrqYVoEiqQrDfAq8I6KWZw7RZOHtVtCzEPdYz7rHZixwcA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.0", + "@smithy/shared-ini-file-loader": "^4.3.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.3.0.tgz", + "integrity": "sha512-RHZ/uWCmSNZ8cneoWEVsVwMZBKy/8123hEpm57vgGXA3Irf/Ja4v9TVshHK2ML5/IqzAZn0WhINHOP9xl+Qy6Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/querystring-builder": "^4.2.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.0.tgz", + "integrity": "sha512-rV6wFre0BU6n/tx2Ztn5LdvEdNZ2FasQbPQmDOPfV9QQyDmsCkOAB0osQjotRCQg+nSKFmINhyda0D3AnjSBJw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.0.tgz", + "integrity": "sha512-6POSYlmDnsLKb7r1D3SVm7RaYW6H1vcNcTWGWrF7s9+2noNYvUsm7E4tz5ZQ9HXPmKn6Hb67pBDRIjrT4w/d7Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.0.tgz", + "integrity": "sha512-Q4oFD0ZmI8yJkiPPeGUITZj++4HHYCW3pYBYfIobUCkYpI6mbkzmG1MAQQ3lJYYWj3iNqfzOenUZu+jqdPQ16A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.6.0", + "@smithy/util-uri-escape": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.0.tgz", + "integrity": "sha512-BjATSNNyvVbQxOOlKse0b0pSezTWGMvA87SvoFoFlkRsKXVsN3bEtjCxvsNXJXfnAzlWFPaT9DmhWy1vn0sNEA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.0.tgz", + "integrity": "sha512-Ylv1ttUeKatpR0wEOMnHf1hXMktPUMObDClSWl2TpCVT4DwtJhCeighLzSLbgH3jr5pBNM0LDXT5yYxUvZ9WpA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.6.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.3.0.tgz", + "integrity": "sha512-VCUPPtNs+rKWlqqntX0CbVvWyjhmX30JCtzO+s5dlzzxrvSfRh5SY0yxnkirvc1c80vdKQttahL71a9EsdolSQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.0.tgz", + "integrity": "sha512-MKNyhXEs99xAZaFhm88h+3/V+tCRDQ+PrDzRqL0xdDpq4gjxcMmf5rBA3YXgqZqMZ/XwemZEurCBQMfxZOWq/g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/types": "^4.6.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-middleware": "^4.2.0", + "@smithy/util-uri-escape": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.7.0.tgz", + "integrity": "sha512-3BDx/aCCPf+kkinYf5QQhdQ9UAGihgOVqI3QO5xQfSaIWvUE4KYLtiGRWsNe1SR7ijXC0QEPqofVp5Sb0zC8xQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.14.0", + "@smithy/middleware-endpoint": "^4.3.0", + "@smithy/middleware-stack": "^4.2.0", + "@smithy/protocol-http": "^5.3.0", + "@smithy/types": "^4.6.0", + "@smithy/util-stream": "^4.4.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.6.0.tgz", + "integrity": "sha512-4lI9C8NzRPOv66FaY1LL1O/0v0aLVrq/mXP/keUa9mJOApEeae43LsLd2kZRUJw91gxOQfLIrV3OvqPgWz1YsA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.0.tgz", + "integrity": "sha512-AlBmD6Idav2ugmoAL6UtR6ItS7jU5h5RNqLMZC7QrLCoITA9NzIN3nx9GWi8g4z1pfWh2r9r96SX/jHiNwPJ9A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.2.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.2.0.tgz", + "integrity": "sha512-+erInz8WDv5KPe7xCsJCp+1WCjSbah9gWcmUXc9NqmhyPx59tf7jqFz+za1tRG1Y5KM1Cy1rWCcGypylFp4mvA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", + "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.0.tgz", + "integrity": "sha512-U8q1WsSZFjXijlD7a4wsDQOvOwV+72iHSfq1q7VD+V75xP/pdtm0WIGuaFJ3gcADDOKj2MIBn4+zisi140HEnQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", + "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.2.0.tgz", + "integrity": "sha512-qzHp7ZDk1Ba4LDwQVCNp90xPGqSu7kmL7y5toBpccuhi3AH7dcVBIT/pUxYcInK4jOy6FikrcTGq5wxcka8UaQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.0", + "@smithy/smithy-client": "^4.7.0", + "@smithy/types": "^4.6.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.0.tgz", + "integrity": "sha512-FxUHS3WXgx3bTWR6yQHNHHkQHZm/XKIi/CchTnKvBulN6obWpcbzJ6lDToXn+Wp0QlVKd7uYAz2/CTw1j7m+Kg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.3.0", + "@smithy/credential-provider-imds": "^4.2.0", + "@smithy/node-config-provider": "^4.3.0", + "@smithy/property-provider": "^4.2.0", + "@smithy/smithy-client": "^4.7.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.0.tgz", + "integrity": "sha512-TXeCn22D56vvWr/5xPqALc9oO+LN+QpFjrSM7peG/ckqEPoI3zaKZFp+bFwfmiHhn5MGWPaLCqDOJPPIixk9Wg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", + "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.0.tgz", + "integrity": "sha512-u9OOfDa43MjagtJZ8AapJcmimP+K2Z7szXn8xbty4aza+7P1wjFmy2ewjSbhEiYQoW1unTlOAIV165weYAaowA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.0.tgz", + "integrity": "sha512-BWSiuGbwRnEE2SFfaAZEX0TqaxtvtSYPM/J73PFVm+A29Fg1HTPiYFb8TmX1DXp4hgcdyJcNQmprfd5foeORsg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.2.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.4.0.tgz", + "integrity": "sha512-vtO7ktbixEcrVzMRmpQDnw/Ehr9UWjBvSJ9fyAbadKkC4w5Cm/4lMO8cHz8Ysb8uflvQUNRcuux/oNHKPXkffg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.0", + "@smithy/node-http-handler": "^4.3.0", + "@smithy/types": "^4.6.0", + "@smithy/util-base64": "^4.2.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", + "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.0.tgz", + "integrity": "sha512-0Z+nxUU4/4T+SL8BCNN4ztKdQjToNvUYmkF1kXO5T7Yz3Gafzh0HeIG6mrkN8Fz3gn9hSyxuAT+6h4vM+iQSBQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.0", + "@smithy/types": "^4.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", + "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@types/aws-lambda": { + "version": "8.10.155", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.155.tgz", + "integrity": "sha512-wd1XgoL0gy/ybo7WozUKQBd+IOgUkdfG6uUGI0fQOTEq06FBFdO7tmPDSxgjkFkl8GlfApvk5TvqZlAl0g+Lbg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.6.2.tgz", + "integrity": "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.13.0" + } + }, + "node_modules/bowser": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", + "integrity": "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz", + "integrity": "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/cdk/lambda/knowledge-base-custom-transformation/package.json b/cdk/lambda/knowledge-base-custom-transformation/package.json new file mode 100644 index 000000000..9b36a8f5f --- /dev/null +++ b/cdk/lambda/knowledge-base-custom-transformation/package.json @@ -0,0 +1,21 @@ +{ + "name": "transformation", + "version": "1.0.0", + "description": "", + "license": "ISC", + "author": "", + "type": "module", + "main": "index.js", + "scripts": { + "build": "esbuild index.ts --bundle --minify --sourcemap --platform=node --target=es2022 --outfile=dist/index.js" + }, + "devDependencies": { + "@types/aws-lambda": "^8.10.155", + "@types/node": "^24.6.2", + "esbuild": "^0.25.10", + "typescript": "^5.9.3" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.901.0" + } +} diff --git a/cdk/lambda/knowledge-base-custom-transformation/tsconfig.json b/cdk/lambda/knowledge-base-custom-transformation/tsconfig.json new file mode 100644 index 000000000..a5cab2b46 --- /dev/null +++ b/cdk/lambda/knowledge-base-custom-transformation/tsconfig.json @@ -0,0 +1,51 @@ +{ + // Visit https://aka.ms/tsconfig to read more about this file + "compilerOptions": { + // File Layout + // "rootDir": "./src", + // "outDir": "./dist", + + // Environment Settings + // See also https://aka.ms/tsconfig/module + "module": "nodenext", + "target": "ES2022", + "lib": ["esnext"], + "types": ["node"], + + // Other Outputs + "preserveConstEnums": true, + "noEmit": true, + "sourceMap": false, + "declaration": true, + "declarationMap": true, + + // Stricter Typechecking Options + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + + // Style Options + // "noImplicitReturns": true, + // "noImplicitOverride": true, + // "noUnusedLocals": true, + // "noUnusedParameters": true, + // "noFallthroughCasesInSwitch": true, + // "noPropertyAccessFromIndexSignature": true, + + // Recommended Options + "strict": true, + "jsx": "react-jsx", + // "verbatimModuleSyntax": true, + "isolatedModules": true, + "noUncheckedSideEffectImports": true, + "moduleDetection": "force", + "skipLibCheck": true, + + "moduleResolution": "nodenext", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + }, + "exclude": [ + "node_modules", + "**/*.test.ts" + ] +} diff --git a/cdk/lib/bedrock-chat-stack.ts b/cdk/lib/bedrock-chat-stack.ts index 25bbea288..01254af57 100644 --- a/cdk/lib/bedrock-chat-stack.ts +++ b/cdk/lib/bedrock-chat-stack.ts @@ -25,6 +25,7 @@ import * as iam from "aws-cdk-lib/aws-iam"; import * as logs from "aws-cdk-lib/aws-logs"; import * as path from "path"; import { BedrockCustomBotCodebuild } from "./constructs/bedrock-custom-bot-codebuild"; +import { BedrockSharedKnowledgeBasesCodebuild } from "./constructs/bedrock-shared-knowledge-bases-codebuild"; import { BotStore, Language } from "./constructs/bot-store"; import { Duration } from "aws-cdk-lib"; @@ -141,6 +142,17 @@ export class BedrockChatStack extends cdk.Stack { bedrockRegion: props.bedrockRegion, } ); + // CodeBuild used for KnowledgeBase + const bedrockSharedKnowledgeBasesCodebuild = new BedrockSharedKnowledgeBasesCodebuild( + this, + "BedrockSharedKnowledgeBasesCodebuild", + { + sourceBucket, + envName: props.envName, + envPrefix: props.envPrefix, + bedrockRegion: props.bedrockRegion, + } + ); const frontend = new Frontend(this, "Frontend", { accessLogBucket, @@ -217,6 +229,7 @@ export class BedrockChatStack extends cdk.Stack { documentBucket: props.documentBucket, apiPublishProject: apiPublishCodebuild.project, bedrockCustomBotProject: bedrockCustomBotCodebuild.project, + bedrockSharedKnowledgeBasesProject: bedrockSharedKnowledgeBasesCodebuild.project, usageAnalysis, largeMessageBucket, enableBedrockCrossRegionInference: @@ -298,6 +311,7 @@ export class BedrockChatStack extends cdk.Stack { database, documentBucket: props.documentBucket, bedrockCustomBotProject: bedrockCustomBotCodebuild.project, + bedrockSharedKnowledgeBasesProject: bedrockSharedKnowledgeBasesCodebuild.project, enableRagReplicas: props.enableRagReplicas, }); diff --git a/cdk/lib/bedrock-custom-bot-stack.ts b/cdk/lib/bedrock-custom-bot-stack.ts index 7f5935eef..c7b5f90c1 100644 --- a/cdk/lib/bedrock-custom-bot-stack.ts +++ b/cdk/lib/bedrock-custom-bot-stack.ts @@ -84,7 +84,7 @@ export class BedrockCustomBotStack extends Stack { // if knowledge base arn does not exist if (props.existKnowledgeBaseId == undefined) { - if ((props.knowledgeBaseType == null || props.knowledgeBaseType === "dedicated") + if (props.knowledgeBaseType === "dedicated" && (docBucketsAndPrefixes.length > 0 || props.sourceUrls.length > 0) ) { const vectorCollection = new VectorCollection(this, "VectorCollection", { diff --git a/cdk/lib/bedrock-shared-knowledge-bases-stack.ts b/cdk/lib/bedrock-shared-knowledge-bases-stack.ts new file mode 100644 index 000000000..417800281 --- /dev/null +++ b/cdk/lib/bedrock-shared-knowledge-bases-stack.ts @@ -0,0 +1,157 @@ +import { CfnOutput, Duration, RemovalPolicy, Stack, StackProps } from "aws-cdk-lib"; +import { Construct } from "constructs"; +import * as lambdaNodeJs from 'aws-cdk-lib/aws-lambda-nodejs'; +import { VectorCollection } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/opensearchserverless"; +import { + Analyzer, + VectorIndex, +} from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/opensearch-vectorindex"; +import { VectorCollectionStandbyReplicas } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/opensearchserverless"; +import * as s3 from "aws-cdk-lib/aws-s3"; +import { BedrockFoundationModel, CustomTransformation } 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 { ParsingStrategy } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock/data-sources/parsing"; + +import { + VectorKnowledgeBase, +} from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock"; + +import * as path from "path"; + +interface BedrockSharedKnowledgeBasesStackProps extends StackProps { + // Base configuration + readonly documentBucketName: string; + readonly enableRagReplicas?: boolean; + + readonly knowledgeBases: Omit[]; +} + +export class BedrockSharedKnowledgeBasesStack extends Stack { + constructor(scope: Construct, id: string, props: BedrockSharedKnowledgeBasesStackProps) { + super(scope, id, props); + + const bucket = s3.Bucket.fromBucketName( + this, + props.documentBucketName, + props.documentBucketName + ); + + props.knowledgeBases.forEach(knowledgeBase => { + const hash = knowledgeBase.knowledgeBaseHash; + const sharedKnowledgeBase = new SharedKnowledgeBase(this, `KnowledgeBase${hash}`, { + documentBucket: bucket, + enableRagReplicas: props.enableRagReplicas, + ...knowledgeBase, + }); + new CfnOutput(this, `KnowledgeBaseId${hash}`, { + value: sharedKnowledgeBase.kb.knowledgeBaseId, + }); + new CfnOutput(this, `KnowledgeBaseArn${hash}`, { + value: sharedKnowledgeBase.kb.knowledgeBaseArn, + }); + + // This output is used by Sfn to synchronize KB data. + new CfnOutput(this, `DataSource${hash}0`, { + value: sharedKnowledgeBase.dataSource.dataSourceId, + }); + }); + } +} + +interface BedrockKnowledgeBaseProps { + // Base configuration + readonly documentBucket: s3.IBucket; + readonly enableRagReplicas?: boolean; + + // Knowledge base configuration + readonly knowledgeBaseHash: string; + readonly embeddingsModel: BedrockFoundationModel; + readonly parsingModel?: BedrockFoundationModel; + readonly instruction?: string; + readonly analyzer?: Analyzer; + + // Chunking configuration + readonly chunkingStrategy: ChunkingStrategy; + readonly maxTokens?: number; + readonly overlapPercentage?: number; +} + +class SharedKnowledgeBase extends Construct { + readonly kb: VectorKnowledgeBase; + readonly dataSource: S3DataSource; + + constructor(scope: Construct, id: string, props: BedrockKnowledgeBaseProps) { + super(scope, id); + + const vectorCollection = new VectorCollection(this, "VectorCollection", { + standbyReplicas: + props.enableRagReplicas === true + ? VectorCollectionStandbyReplicas.ENABLED + : VectorCollectionStandbyReplicas.DISABLED, + }); + const vectorIndex = new VectorIndex(this, "VectorIndex", { + collection: vectorCollection, + // DO NOT CHANGE THIS VALUE + indexName: "bedrock-knowledge-base-default-index", + // DO NOT CHANGE THIS VALUE + vectorField: "bedrock-knowledge-base-default-vector", + vectorDimensions: props.embeddingsModel.vectorDimensions!, + precision: "float", + distanceType: "l2", + mappings: [ + { + mappingField: "AMAZON_BEDROCK_TEXT_CHUNK", + dataType: "text", + filterable: true, + }, + { + mappingField: "AMAZON_BEDROCK_METADATA", + dataType: "text", + filterable: false, + }, + ], + analyzer: props.analyzer, + }); + vectorIndex.node.addDependency(vectorCollection); + + const tempBucket = new s3.Bucket(this, 'TempBucket', { + enforceSSL: true, + autoDeleteObjects: true, + removalPolicy: RemovalPolicy.DESTROY, + }); + const transformationFunction = new lambdaNodeJs.NodejsFunction(this, 'TransformationFunction', { + entry: path.resolve(__dirname, '../lambda/knowledge-base-custom-transformation/index.ts'), + depsLockFilePath: path.resolve(__dirname, '../lambda/knowledge-base-custom-transformation/package-lock.json'), + + timeout: Duration.minutes(15), + }); + props.documentBucket.grantReadWrite(transformationFunction); + tempBucket.grantReadWrite(transformationFunction); + + this.kb = new VectorKnowledgeBase(this, "KnowledgeBase", { + embeddingsModel: props.embeddingsModel, + vectorStore: vectorCollection, + vectorIndex: vectorIndex, + instruction: props.instruction, + }); + tempBucket.grantReadWrite(this.kb.role); + + props.documentBucket.grantRead(this.kb.role); + this.dataSource = new S3DataSource(this, `DocumentBucketDataSource`, { + bucket: props.documentBucket, + knowledgeBase: this.kb, + dataSourceName: props.documentBucket.bucketName, + chunkingStrategy: props.chunkingStrategy, + parsingStrategy: props.parsingModel + ? ParsingStrategy.foundationModel({ + parsingModel: props.parsingModel, + }) + : undefined, + customTransformation: CustomTransformation.lambda({ + lambdaFunction: transformationFunction, + s3BucketUri: tempBucket.s3UrlForObject(), + }), + }); + } +} diff --git a/cdk/lib/constructs/api-publish-codebuild.ts b/cdk/lib/constructs/api-publish-codebuild.ts index 114c4f2b8..980dde4a1 100644 --- a/cdk/lib/constructs/api-publish-codebuild.ts +++ b/cdk/lib/constructs/api-publish-codebuild.ts @@ -38,7 +38,7 @@ export class ApiPublishCodebuild extends Construct { BEDROCK_REGION: { value: props.bedrockRegion }, }, buildSpec: codebuild.BuildSpec.fromObject({ - version: "0.2", + version: 0.2, phases: { install: { "runtime-versions": { diff --git a/cdk/lib/constructs/api.ts b/cdk/lib/constructs/api.ts index 7cb59029e..bcf19610a 100644 --- a/cdk/lib/constructs/api.ts +++ b/cdk/lib/constructs/api.ts @@ -38,6 +38,7 @@ export interface ApiProps { readonly largeMessageBucket: IBucket; readonly apiPublishProject: codebuild.IProject; readonly bedrockCustomBotProject: codebuild.IProject; + readonly bedrockSharedKnowledgeBasesProject: codebuild.IProject; readonly usageAnalysis?: UsageAnalysis; readonly enableBedrockCrossRegionInference: boolean; readonly enableLambdaSnapStart: boolean; @@ -85,6 +86,7 @@ export class Api extends Construct { resources: [ props.apiPublishProject.projectArn, props.bedrockCustomBotProject.projectArn, + props.bedrockSharedKnowledgeBasesProject.projectArn, ], }) ); @@ -108,6 +110,7 @@ export class Api extends Construct { resources: [ props.apiPublishProject.projectArn, props.bedrockCustomBotProject.projectArn, + props.bedrockSharedKnowledgeBasesProject.projectArn, ], }) ); diff --git a/cdk/lib/constructs/bedrock-custom-bot-codebuild.ts b/cdk/lib/constructs/bedrock-custom-bot-codebuild.ts index 3f82767c5..90c96eab0 100644 --- a/cdk/lib/constructs/bedrock-custom-bot-codebuild.ts +++ b/cdk/lib/constructs/bedrock-custom-bot-codebuild.ts @@ -36,7 +36,7 @@ export class BedrockCustomBotCodebuild extends Construct { BEDROCK_REGION: { value: props.bedrockRegion }, }, buildSpec: codebuild.BuildSpec.fromObject({ - version: "0.2", + version: 0.2, phases: { install: { "runtime-versions": { @@ -48,8 +48,6 @@ export class BedrockCustomBotCodebuild extends Construct { commands: [ "cd cdk", "npm ci", - // Extract BOT_ID from SK. Note that SK is given like BOT# - `export BOT_ID=$(echo $SK | awk -F'#' '{print $2}')`, // 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/bedrock-custom-bot.ts|' cdk.json", `npx cdk deploy --require-approval never BrChatKbStack$BOT_ID`, diff --git a/cdk/lib/constructs/bedrock-shared-knowledge-bases-codebuild.ts b/cdk/lib/constructs/bedrock-shared-knowledge-bases-codebuild.ts new file mode 100644 index 000000000..82bebc19f --- /dev/null +++ b/cdk/lib/constructs/bedrock-shared-knowledge-bases-codebuild.ts @@ -0,0 +1,80 @@ +import { Construct } from "constructs"; +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 { NagSuppressions } from "cdk-nag"; + +export interface BedrockSharedKnowledgeBasesCodebuildProps { + readonly envName: string; + readonly envPrefix: string; + readonly bedrockRegion: string; + readonly sourceBucket: s3.Bucket; +} + +export class BedrockSharedKnowledgeBasesCodebuild extends Construct { + public readonly project: codebuild.Project; + + constructor(scope: Construct, id: string, props: BedrockSharedKnowledgeBasesCodebuildProps) { + super(scope, id); + + const sourceBucket = props.sourceBucket; + const project = new codebuild.Project(this, "Project", { + source: codebuild.Source.s3({ + bucket: sourceBucket, + path: "", + }), + environment: { + buildImage: codebuild.LinuxBuildImage.STANDARD_7_0, + privileged: true, + }, + environmentVariables: { + ENV_NAME: { value: props.envName }, + ENV_PREFIX: { value: props.envPrefix }, + BEDROCK_REGION: { value: props.bedrockRegion }, + }, + buildSpec: codebuild.BuildSpec.fromObject({ + version: 0.2, + phases: { + install: { + "runtime-versions": { + nodejs: "18", + }, + "on-failure": "ABORT", + }, + build: { + commands: [ + "cd cdk", + "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/bedrock-shared-knowledge-bases.ts|' cdk.json", + "npx cdk deploy --require-approval never BrChatSharedKbStack", + ], + }, + }, + }), + }); + sourceBucket.grantRead(project.role!); + + // Allow `cdk deploy` + project.role!.addToPrincipalPolicy( + new iam.PolicyStatement({ + actions: ["sts:AssumeRole"], + resources: ["arn:aws:iam::*:role/cdk-*"], + }) + ); + + NagSuppressions.addResourceSuppressions(project, [ + { + id: "AwsPrototyping-CodeBuildProjectKMSEncryptedArtifacts", + reason: + "default: The AWS-managed CMK for Amazon Simple Storage Service (Amazon S3) is used.", + }, + { + id: "AwsPrototyping-CodeBuildProjectPrivilegedModeDisabled", + reason: "for running on the docker daemon on the docker container", + }, + ]); + + this.project = project; + } +} diff --git a/cdk/lib/constructs/database.ts b/cdk/lib/constructs/database.ts index a559e8360..00adbe46b 100644 --- a/cdk/lib/constructs/database.ts +++ b/cdk/lib/constructs/database.ts @@ -91,6 +91,14 @@ export class Database extends Construct { type: AttributeType.STRING, }, }); + // GSI-5 + botTable.addGlobalSecondaryIndex({ + indexName: "SyncStatusIndex", + partitionKey: { + name: "SyncStatus", + type: AttributeType.STRING, + }, + }); const tableAccessRole = new Role(this, "TableAccessRole", { assumedBy: new AccountPrincipal(Stack.of(this).account), diff --git a/cdk/lib/constructs/embedding.ts b/cdk/lib/constructs/embedding.ts index ff15e39e8..65e2b5f4a 100644 --- a/cdk/lib/constructs/embedding.ts +++ b/cdk/lib/constructs/embedding.ts @@ -2,8 +2,6 @@ import { Construct } from "constructs"; import * as path from "path"; import { Duration, RemovalPolicy, Stack } from "aws-cdk-lib"; import * as iam from "aws-cdk-lib/aws-iam"; -import { ITable } from "aws-cdk-lib/aws-dynamodb"; -import { CfnPipe } from "aws-cdk-lib/aws-pipes"; import * as logs from "aws-cdk-lib/aws-logs"; import { IBucket } from "aws-cdk-lib/aws-s3"; import * as lambda from "aws-cdk-lib/aws-lambda"; @@ -25,16 +23,16 @@ export interface EmbeddingProps { readonly bedrockRegion: string; readonly documentBucket: IBucket; readonly bedrockCustomBotProject: codebuild.IProject; + readonly bedrockSharedKnowledgeBasesProject: codebuild.IProject; readonly enableRagReplicas: boolean; } export class Embedding extends Construct { readonly removalHandler: IFunction; private _updateSyncStatusHandler: IFunction; - private _fetchStackOutputHandler: IFunction; - private _StoreKnowledgeBaseIdHandler: IFunction; - private _StoreGuardrailArnHandler: IFunction; - private _pipeRole: iam.Role; + private _bootstrapStateMachineHandler: IFunction; + private _finalizeCustomBotBuildHandler: IFunction; + private _finalizeSharedKnowledgeBasesBuildHandler: IFunction; private _stateMachine: sfn.StateMachine; private _removalHandler: IFunction; @@ -43,7 +41,6 @@ export class Embedding extends Construct { this.setupStateMachineHandlers(props) .setupStateMachine(props) - .setupEventBridgePipe(props) .setupRemovalHandler(props); this.removalHandler = this._removalHandler; @@ -117,9 +114,9 @@ export class Embedding extends Construct { } ); - this._fetchStackOutputHandler = new DockerImageFunction( + this._bootstrapStateMachineHandler = new DockerImageFunction( this, - "FetchStackOutputHandler", + "BootstrapStateMachineHandler", { code: DockerImageCode.fromImageAsset( path.join(__dirname, "../../../backend"), @@ -127,7 +124,7 @@ export class Embedding extends Construct { platform: Platform.LINUX_AMD64, file: "lambda.Dockerfile", cmd: [ - "embedding_statemachine.bedrock_knowledge_base.fetch_stack_output.handler", + "embedding_statemachine.bedrock_knowledge_base.bootstrap_state_machine.handler", ], exclude: [...excludeDockerImage], } @@ -136,14 +133,17 @@ export class Embedding extends Construct { timeout: Duration.minutes(1), role: handlerRole, environment: { - BEDROCK_REGION: props.bedrockRegion, + ACCOUNT: Stack.of(this).account, + REGION: Stack.of(this).region, + BOT_TABLE_NAME: props.database.botTable.tableName, + TABLE_ACCESS_ROLE_ARN: props.database.tableAccessRole.roleArn, }, logRetention: logs.RetentionDays.THREE_MONTHS, } ); - this._StoreKnowledgeBaseIdHandler = new DockerImageFunction( + this._finalizeCustomBotBuildHandler = new DockerImageFunction( this, - "StoreKnowledgeBaseIdHandler", + "FinalizeCustomBotBuildHandler", { code: DockerImageCode.fromImageAsset( path.join(__dirname, "../../../backend"), @@ -151,27 +151,28 @@ export class Embedding extends Construct { platform: Platform.LINUX_AMD64, file: "lambda.Dockerfile", cmd: [ - "embedding_statemachine.bedrock_knowledge_base.store_knowledge_base_id.handler", + "embedding_statemachine.bedrock_knowledge_base.finalize_custom_bot_build.handler", ], exclude: [...excludeDockerImage], } ), memorySize: 512, timeout: Duration.minutes(1), + role: handlerRole, environment: { ACCOUNT: Stack.of(this).account, REGION: Stack.of(this).region, + BEDROCK_REGION: props.bedrockRegion, CONVERSATION_TABLE_NAME: props.database.conversationTable.tableName, BOT_TABLE_NAME: props.database.botTable.tableName, TABLE_ACCESS_ROLE_ARN: props.database.tableAccessRole.roleArn, }, - role: handlerRole, logRetention: logs.RetentionDays.THREE_MONTHS, } ); - this._StoreGuardrailArnHandler = new DockerImageFunction( + this._finalizeSharedKnowledgeBasesBuildHandler = new DockerImageFunction( this, - "StoreGuardrailArnHandler", + "FinalizeSharedKnowledgeBasesBuildHandler", { code: DockerImageCode.fromImageAsset( path.join(__dirname, "../../../backend"), @@ -179,21 +180,22 @@ export class Embedding extends Construct { platform: Platform.LINUX_AMD64, file: "lambda.Dockerfile", cmd: [ - "embedding_statemachine.guardrails.store_guardrail_arn.handler", + "embedding_statemachine.bedrock_knowledge_base.finalize_shared_knowledge_bases_build.handler", ], exclude: [...excludeDockerImage], } ), memorySize: 512, timeout: Duration.minutes(1), + role: handlerRole, environment: { ACCOUNT: Stack.of(this).account, REGION: Stack.of(this).region, + BEDROCK_REGION: props.bedrockRegion, CONVERSATION_TABLE_NAME: props.database.conversationTable.tableName, BOT_TABLE_NAME: props.database.botTable.tableName, TABLE_ACCESS_ROLE_ARN: props.database.tableAccessRole.roleArn, }, - role: handlerRole, logRetention: logs.RetentionDays.THREE_MONTHS, } ); @@ -201,17 +203,169 @@ export class Embedding extends Construct { } private setupStateMachine(props: EmbeddingProps): this { - const extractFirstElement = new sfn.Pass(this, "ExtractFirstElement", { + const bootstrapStateMachine = new tasks.LambdaInvoke(this, "BootstrapStateMachine", { + lambdaFunction: this._bootstrapStateMachineHandler, + resultSelector: { + Bots: sfn.JsonPath.objectAt("$.Payload.Bots"), + SharedKnowledgeBases: sfn.JsonPath.objectAt("$.Payload.SharedKnowledgeBases"), + }, + }); + + const buildSharedKnowledgeBases = new tasks.CodeBuildStartBuild(this, "BuildSharedKnowledgeBases", { + project: props.bedrockSharedKnowledgeBasesProject, + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + environmentVariablesOverride: { + SHARED_KNOWLEDGE_BASES: { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: sfn.JsonPath.stringAt("States.JsonToString($.SharedKnowledgeBases)"), + }, + // Bucket name provisioned by the bedrock stack + BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME: { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: props.documentBucket.bucketName, + }, + ENABLE_RAG_REPLICAS: { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: props.enableRagReplicas.toString(), + }, + }, + resultPath: "$.Build", + }); + + const updateSyncStatusRunning = this.createUpdateSyncStatusTask( + "UpdateSyncStatusRunning", + "RUNNING" + ); + + const updateSyncStatusSucceeded = this.createUpdateSyncStatusTask( + "UpdateSyncStatusSuccess", + "SUCCEEDED", + "Knowledge base sync succeeded" + ); + + // const updateSyncStatusFailed = new tasks.LambdaInvoke( + // this, + // "UpdateSyncStatusFailed", + // { + // lambdaFunction: this._updateSyncStatusHandler, + // payload: sfn.TaskInput.fromObject({ + // "cause.$": "$.Cause", + // }), + // resultPath: sfn.JsonPath.DISCARD, + // } + // ); + + // const fallback = updateSyncStatusFailed.next( + // new sfn.Fail(this, "Fail", { + // cause: "Knowledge base sync failed", + // error: "Knowledge base sync failed", + // }) + // ); + // buildSharedKnowledgeBases.addCatch(fallback); + + const finalizeSharedKnowledgeBasesBuild = new tasks.LambdaInvoke(this, "FinalizeSharedKnowledgeBasesBuild", { + lambdaFunction: this._finalizeSharedKnowledgeBasesBuildHandler, + resultSelector: { + DataSources: sfn.JsonPath.objectAt("$.Payload.DataSources"), + }, + resultPath: "$.StackOutput", + }); + // finalizeSharedKnowledgeBasesBuild.addCatch(fallback); + + const startIngestionJobForSharedKnowledgeBases = new tasks.CallAwsServiceCrossRegion(this, "StartIngestionJobstartIngestionJobForSharedKnowledgeBases", { + service: "bedrock-agent", + action: "startIngestionJob", + iamAction: "bedrock:StartIngestionJob", + region: props.bedrockRegion, + parameters: { + dataSourceId: sfn.JsonPath.stringAt("$.DataSourceId"), + knowledgeBaseId: sfn.JsonPath.stringAt("$.KnowledgeBaseId"), + }, + // Ref: https://docs.aws.amazon.com/ja_jp/service-authorization/latest/reference/list_amazonbedrock.html#amazonbedrock-knowledge-base + iamResources: [ + `arn:${Stack.of(this).partition}:bedrock:${props.bedrockRegion}:${ + Stack.of(this).account + }:knowledge-base/*`, + ], + resultPath: "$.IngestionJob", + }); + + const getIngestionJobForSharedKnowledgeBases = new tasks.CallAwsServiceCrossRegion(this, "GetIngestionJobForSharedKnowledgeBases", { + service: "bedrock-agent", + action: "getIngestionJob", + iamAction: "bedrock:GetIngestionJob", + region: props.bedrockRegion, parameters: { - "dynamodb.$": "$[0].dynamodb", - "eventID.$": "$[0].eventID", - "eventName.$": "$[0].eventName", - "eventSource.$": "$[0].eventSource", - "eventVersion.$": "$[0].eventVersion", - "awsRegion.$": "$[0].awsRegion", - "eventSourceARN.$": "$[0].eventSourceARN", + dataSourceId: sfn.JsonPath.stringAt( + "$.IngestionJob.ingestionJob.dataSourceId" + ), + knowledgeBaseId: sfn.JsonPath.stringAt( + "$.IngestionJob.ingestionJob.knowledgeBaseId" + ), + ingestionJobId: sfn.JsonPath.stringAt( + "$.IngestionJob.ingestionJob.ingestionJobId" + ), }, - resultPath: "$", + // Ref: https://docs.aws.amazon.com/ja_jp/service-authorization/latest/reference/list_amazonbedrock.html#amazonbedrock-knowledge-base + iamResources: [ + `arn:${Stack.of(this).partition}:bedrock:${props.bedrockRegion}:${ + Stack.of(this).account + }:knowledge-base/*`, + ], + resultPath: "$.IngestionJob", + }); + + const waitTaskForSharedKnowledgeBases = new sfn.Wait(this, "WaitSecondsForSharedKnowledgeBases", { + time: sfn.WaitTime.duration(Duration.seconds(3)), + }); + + const checkIngestionJobStatusForSharedKnowledgeBases = new sfn.Choice(this, "CheckIngestionJobStatusForSharedKnowledgeBases") + .when( + sfn.Condition.stringEquals( + "$.IngestionJob.ingestionJob.status", + "COMPLETE" + ), + new sfn.Pass(this, "IngestionJobCompletedForSharedKnowledgeBases") + ) + .when( + sfn.Condition.stringEquals( + "$.IngestionJob.ingestionJob.status", + "FAILED" + ), + new sfn.Fail(this, "IngestionFailForSharedKnowledgeBases", { + cause: "Ingestion job failed", + error: "Ingestion job failed", + }) + // new tasks.LambdaInvoke(this, "UpdateSyncStatusFailedForIngestion", { + // lambdaFunction: this._updateSyncStatusHandler, + // payload: sfn.TaskInput.fromObject({ + // pk: sfn.JsonPath.stringAt("$.PK"), + // sk: sfn.JsonPath.stringAt("$.SK"), + // ingestion_job: sfn.JsonPath.stringAt("$.IngestionJob"), + // }), + // resultPath: sfn.JsonPath.DISCARD, + // }) + // .next( + // new sfn.Fail(this, "IngestionFail", { + // cause: "Ingestion job failed", + // error: "Ingestion job failed", + // }) + // ) + ) + .otherwise(waitTaskForSharedKnowledgeBases.next(getIngestionJobForSharedKnowledgeBases)); + + const mapIngestionJobsForSharedKnowledgeBases = new sfn.Map(this, "MapIngestionJobsForSharedKnowledgeBases", { + inputPath: "$.StackOutput.DataSources", + resultPath: sfn.JsonPath.DISCARD, + maxConcurrency: 1, + }).itemProcessor( + startIngestionJobForSharedKnowledgeBases + .next(getIngestionJobForSharedKnowledgeBases) + .next(checkIngestionJobStatusForSharedKnowledgeBases) + ); + + const mapBots = new sfn.Map(this, "MapBots", { + itemsPath: "$.Bots", }); const startCustomBotBuild = new tasks.CodeBuildStartBuild( @@ -221,13 +375,13 @@ export class Embedding extends Construct { project: props.bedrockCustomBotProject, integrationPattern: sfn.IntegrationPattern.RUN_JOB, environmentVariablesOverride: { - PK: { + OWNER_USER_ID: { type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, - value: sfn.JsonPath.stringAt("$.dynamodb.NewImage.PK.S"), + value: sfn.JsonPath.stringAt("$.OwnerUserId"), }, - SK: { + BOT_ID: { type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, - value: sfn.JsonPath.stringAt("$.dynamodb.NewImage.SK.S"), + value: sfn.JsonPath.stringAt("$.BotId"), }, // Bucket name provisioned by the bedrock stack BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME: { @@ -238,20 +392,20 @@ export class Embedding extends Construct { KNOWLEDGE: { type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, value: sfn.JsonPath.stringAt( - "States.JsonToString($.dynamodb.NewImage.Knowledge.M)" + "States.JsonToString($.Knowledge)" ), }, // Bedrock Knowledge Base configuration - BEDROCK_KNOWLEDGE_BASE: { + KNOWLEDGE_BASE: { type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, value: sfn.JsonPath.stringAt( - "States.JsonToString($.dynamodb.NewImage.BedrockKnowledgeBase.M)" + "States.JsonToString($.KnowledgeBase)" ), }, - BEDROCK_GUARDRAILS: { + GUARDRAILS: { type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, value: sfn.JsonPath.stringAt( - "States.JsonToString($.dynamodb.NewImage.GuardrailsParams.M)" + "States.JsonToString($.Guardrails)" ), }, ENABLE_RAG_REPLICAS: { @@ -262,77 +416,17 @@ export class Embedding extends Construct { resultPath: "$.Build", } ); + // startCustomBotBuild.addCatch(fallback); - const updateSyncStatusRunning = this.createUpdateSyncStatusTask( - "UpdateSyncStatusRunning", - "RUNNING" - ); - - const updateSyncStatusSucceeded = this.createUpdateSyncStatusTask( - "UpdateSyncStatusSuccess", - "SUCCEEDED", - "Knowledge base sync succeeded" - ); - - const updateSyncStatusFailed = new tasks.LambdaInvoke( - this, - "UpdateSyncStatusFailed", - { - lambdaFunction: this._updateSyncStatusHandler, - payload: sfn.TaskInput.fromObject({ - "cause.$": "$.Cause", - }), - resultPath: sfn.JsonPath.DISCARD, - } - ); - - const fallback = updateSyncStatusFailed.next( - new sfn.Fail(this, "Fail", { - cause: "Knowledge base sync failed", - error: "Knowledge base sync failed", - }) - ); - startCustomBotBuild.addCatch(fallback); - - const fetchStackOutput = new tasks.LambdaInvoke(this, "FetchStackOutput", { - lambdaFunction: this._fetchStackOutputHandler, - payload: sfn.TaskInput.fromObject({ - "pk.$": "$.dynamodb.NewImage.PK.S", - "sk.$": "$.dynamodb.NewImage.SK.S", - }), + const finalizeCustomBotBuild = new tasks.LambdaInvoke(this, "FinalizeCustomBotBuild", { + lambdaFunction: this._finalizeCustomBotBuildHandler, + resultSelector: { + DataSources: sfn.JsonPath.objectAt("$.Payload.DataSources"), + Bots: sfn.JsonPath.objectAt("$.Payload.Bots"), + }, resultPath: "$.StackOutput", }); - fetchStackOutput.addCatch(fallback); - - const storeKnowledgeBaseId = new tasks.LambdaInvoke( - this, - "StoreKnowledgeBaseId", - { - lambdaFunction: this._StoreKnowledgeBaseIdHandler, - payload: sfn.TaskInput.fromObject({ - "pk.$": "$.dynamodb.NewImage.PK.S", - "sk.$": "$.dynamodb.NewImage.SK.S", - "stack_output.$": "$.StackOutput.Payload", - }), - resultPath: sfn.JsonPath.DISCARD, - } - ); - storeKnowledgeBaseId.addCatch(fallback); - - const storeGuardrailArn = new tasks.LambdaInvoke( - this, - "StoreGuardrailArn", - { - lambdaFunction: this._StoreGuardrailArnHandler, - payload: sfn.TaskInput.fromObject({ - "pk.$": "$.dynamodb.NewImage.PK.S", - "sk.$": "$.dynamodb.NewImage.SK.S", - "stack_output.$": "$.StackOutput.Payload", - }), - resultPath: sfn.JsonPath.DISCARD, - } - ); - storeGuardrailArn.addCatch(fallback); + // finalizeCustomBotBuild.addCatch(fallback); const startIngestionJob = new tasks.CallAwsServiceCrossRegion( this, @@ -405,39 +499,56 @@ export class Embedding extends Construct { "$.IngestionJob.ingestionJob.status", "FAILED" ), - new tasks.LambdaInvoke(this, "UpdateSyncStatusFailedForIngestion", { - lambdaFunction: this._updateSyncStatusHandler, - payload: sfn.TaskInput.fromObject({ - pk: sfn.JsonPath.stringAt("$.PK"), - sk: sfn.JsonPath.stringAt("$.SK"), - ingestion_job: sfn.JsonPath.stringAt("$.IngestionJob"), - }), - resultPath: sfn.JsonPath.DISCARD, - }).next( - new sfn.Fail(this, "IngestionFail", { - cause: "Ingestion job failed", - error: "Ingestion job failed", - }) - ) + new sfn.Fail(this, "IngestionFail", { + cause: "Ingestion job failed", + error: "Ingestion job failed", + }) + // new tasks.LambdaInvoke(this, "UpdateSyncStatusFailedForIngestion", { + // lambdaFunction: this._updateSyncStatusHandler, + // payload: sfn.TaskInput.fromObject({ + // pk: sfn.JsonPath.stringAt("$.PK"), + // sk: sfn.JsonPath.stringAt("$.SK"), + // ingestion_job: sfn.JsonPath.stringAt("$.IngestionJob"), + // }), + // resultPath: sfn.JsonPath.DISCARD, + // }) + // .next( + // new sfn.Fail(this, "IngestionFail", { + // cause: "Ingestion job failed", + // error: "Ingestion job failed", + // }) + // ) ) .otherwise(waitTask.next(getIngestionJob)); const mapIngestionJobs = new sfn.Map(this, "MapIngestionJobs", { - inputPath: "$.StackOutput.Payload.items", + inputPath: "$.StackOutput.DataSources", resultPath: sfn.JsonPath.DISCARD, maxConcurrency: 1, }).itemProcessor( startIngestionJob.next(getIngestionJob).next(checkIngestionJobStatus) ); - const definition = extractFirstElement - .next(updateSyncStatusRunning) - .next(startCustomBotBuild) - .next(fetchStackOutput) - .next(storeKnowledgeBaseId) - .next(storeGuardrailArn) - .next(mapIngestionJobs) - .next(updateSyncStatusSucceeded); + const definition = bootstrapStateMachine + .next(buildSharedKnowledgeBases) + .next(finalizeSharedKnowledgeBasesBuild) + .next(mapIngestionJobsForSharedKnowledgeBases) + .next( + mapBots.itemProcessor( + updateSyncStatusRunning + .next(startCustomBotBuild) + .next(finalizeCustomBotBuild) + .next(mapIngestionJobs) + .next(updateSyncStatusSucceeded) + ) + ) + + // const definition = extractFirstElement + // .next(updateSyncStatusRunning) + // .next(startCustomBotBuild) + // .next(finalizeCustomBotBuild) + // .next(mapIngestionJobs) + // .next(updateSyncStatusSucceeded); this._stateMachine = new sfn.StateMachine(this, "StateMachine", { definitionBody: sfn.DefinitionBody.fromChainable(definition), @@ -445,79 +556,6 @@ export class Embedding extends Construct { return this; } - private setupEventBridgePipe(props: EmbeddingProps): this { - if (!this._stateMachine) { - throw new Error( - "State machine must be set up before setting up the EventBridge pipe" - ); - } - - const pipeLogGroup = new logs.LogGroup(this, "PipeLogGroup", { - removalPolicy: RemovalPolicy.DESTROY, - retention: logs.RetentionDays.ONE_WEEK, - }); - this._pipeRole = new iam.Role(this, "PipeRole", { - assumedBy: new iam.ServicePrincipal("pipes.amazonaws.com"), - }); - this._pipeRole.addToPolicy( - new iam.PolicyStatement({ - actions: [ - "dynamodb:DescribeStream", - "dynamodb:GetRecords", - "dynamodb:GetShardIterator", - "dynamodb:ListStreams", - ], - resources: [props.database.botTable.tableStreamArn!], - }) - ); - this._pipeRole.addToPolicy( - new iam.PolicyStatement({ - actions: ["states:StartExecution"], - resources: [this._stateMachine.stateMachineArn], - }) - ); - this._pipeRole.addToPolicy( - new iam.PolicyStatement({ - actions: ["logs:CreateLogStream", "logs:PutLogEvents"], - resources: [pipeLogGroup.logGroupArn], - }) - ); - - new CfnPipe(this, "Pipe", { - source: props.database.botTable.tableStreamArn!, - sourceParameters: { - dynamoDbStreamParameters: { - batchSize: 1, - startingPosition: "LATEST", - maximumRetryAttempts: 1, - }, - filterCriteria: { - filters: [ - { - pattern: - '{"dynamodb":{"NewImage":{"SyncStatus":{"S":[{"prefix":"QUEUED"}]}}}}', - }, - ], - }, - }, - target: this._stateMachine.stateMachineArn, - targetParameters: { - stepFunctionStateMachineParameters: { - invocationType: "FIRE_AND_FORGET", - }, - }, - logConfiguration: { - cloudwatchLogsLogDestination: { - logGroupArn: pipeLogGroup.logGroupArn, - }, - level: "INFO", - }, - roleArn: this._pipeRole.roleArn, - }); - - return this; - } - private setupRemovalHandler(props: EmbeddingProps): this { const removeHandlerRole = new iam.Role(this, "RemovalHandlerRole", { assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"), @@ -623,8 +661,8 @@ export class Embedding extends Construct { lastExecIdPath?: string ): tasks.LambdaInvoke { const payload: { [key: string]: any } = { - "pk.$": "$.dynamodb.NewImage.PK.S", - "sk.$": "$.dynamodb.NewImage.SK.S", + "user_id.$": "$.OwnerUserId", + "bot_id.$": "$.BotId", sync_status: syncStatus, sync_status_reason: syncStatusReason || "", }; diff --git a/cdk/lib/utils/bedrock-knowledge-base-args.ts b/cdk/lib/utils/bedrock-knowledge-base-args.ts index 8f67104ac..a04d58249 100644 --- a/cdk/lib/utils/bedrock-knowledge-base-args.ts +++ b/cdk/lib/utils/bedrock-knowledge-base-args.ts @@ -31,28 +31,26 @@ interface SemanticOptions { export const getKnowledgeBaseType = ( type: unknown ): "dedicated" | "shared" | undefined => { - if (type == null || typeof(type) !== "object") { + if (type == null || typeof(type) !== "string") { return undefined; } - if ("S" in type) { - const value = type["S"]; - switch(value) { - case "dedicated": - case "shared": - return value; + switch(type) { + case "dedicated": + case "shared": + return type; - default: - return undefined; - } + default: + return undefined; } - - return undefined; }; export const getEmbeddingModel = ( - embeddingsModel: string + embeddingsModel?: string ): BedrockFoundationModel => { + if (embeddingsModel == null) { + return BedrockFoundationModel.TITAN_EMBED_TEXT_V2_1024; + } switch (embeddingsModel) { case "titan_v2": return BedrockFoundationModel.TITAN_EMBED_TEXT_V2_1024; @@ -64,8 +62,11 @@ export const getEmbeddingModel = ( }; export const getParsingModel = ( - parsingModel: string + parsingModel?: string ): BedrockFoundationModel | undefined => { + if (parsingModel == null) { + return undefined; + } switch (parsingModel) { case "anthropic.claude-3-5-sonnet-v1": return BedrockFoundationModel.ANTHROPIC_CLAUDE_3_5_SONNET_V1_0; @@ -79,9 +80,11 @@ export const getParsingModel = ( } export const getCrowlingScope = ( - web_crawling_scope: string + web_crawling_scope?: string ): CrawlingScope | undefined => { - + if (web_crawling_scope == null) { + return CrawlingScope.DEFAULT; + } switch(web_crawling_scope) { case "DEFAULT": return CrawlingScope.DEFAULT @@ -95,16 +98,20 @@ export const getCrowlingScope = ( } export const getCrawlingFilters =( - web_crawling_filters: any + web_crawling_filters?: any ): CrawlingFilters => { - const regularJson = unmarshall(web_crawling_filters); - console.log(`regularJson: ${JSON.stringify(regularJson)}`) + if (web_crawling_filters == null) { + return { + excludePatterns: [], + includePatterns: [], + }; + } let excludePatterns = undefined let includePatterns = undefined - if (regularJson.exclude_patterns.length > 0 && regularJson.exclude_patterns[0] != "") excludePatterns = regularJson.exclude_patterns - if (regularJson.include_patterns.length > 0 && regularJson.include_patterns[0] != "") includePatterns = regularJson.include_patterns + if (web_crawling_filters.exclude_patterns.length > 0 && web_crawling_filters.exclude_patterns[0] != "") excludePatterns = web_crawling_filters.exclude_patterns + if (web_crawling_filters.include_patterns.length > 0 && web_crawling_filters.include_patterns[0] != "") includePatterns = web_crawling_filters.include_patterns return { excludePatterns, @@ -113,10 +120,13 @@ export const getCrawlingFilters =( } export const getChunkingStrategy = ( - chunkingStrategy: string, - embeddingsModel: string, + chunkingStrategy?: string, + embeddingsModel?: string, options?: Partial ): ChunkingStrategy => { + if (chunkingStrategy == undefined) { + return ChunkingStrategy.DEFAULT; + } switch (chunkingStrategy) { case "default": return ChunkingStrategy.DEFAULT; @@ -136,7 +146,7 @@ export const getChunkingStrategy = ( maxChildTokenSize: options.maxChildTokenSize }); } - return embeddingsModel === 'titan_v2' ? ChunkingStrategy.HIERARCHICAL_TITAN : ChunkingStrategy.HIERARCHICAL_COHERE; + return (embeddingsModel == null || embeddingsModel === 'titan_v2') ? ChunkingStrategy.HIERARCHICAL_TITAN : ChunkingStrategy.HIERARCHICAL_COHERE; case "semantic": // Check that it is not explicitly undefined because bufferSize is set to 0, it will be created with the default value even if other parameters changed. if (options?.maxTokens !== undefined && options?.bufferSize !== undefined && options?.breakpointPercentileThreshold !== undefined) { @@ -157,63 +167,50 @@ export const getChunkingStrategy = ( export const getAnalyzer = (analyzer: any): Analyzer | undefined => { // Example of analyzer: // { - // "character_filters": { - // "L": [ - // { - // "S": "icu_normalizer" - // } - // ] - // }, - // "token_filters": { - // "L": [ - // { - // "S": "kuromoji_baseform" - // }, - // { - // "S": "kuromoji_part_of_speech" - // } - // ] - // }, - // "tokenizer": { - // "S": "kuromoji_tokenizer" - // } + // "character_filters": [ + // "icu_normalizer" + // ], + // "token_filters": [ + // "kuromoji_baseform", + // "kuromoji_part_of_speech" + // ], + // "tokenizer": "kuromoji_tokenizer" // } console.log("getAnalyzer: analyzer: ", analyzer); if ( !analyzer || - !analyzer.character_filters || - !analyzer.character_filters.L + !analyzer.character_filters ) { return undefined; } const characterFilters: CharacterFilterType[] = - analyzer.character_filters.L.map((filter: any) => { - switch (filter.S) { + analyzer.character_filters.map((filter: any) => { + switch (filter) { case "icu_normalizer": return CharacterFilterType.ICU_NORMALIZER; default: - throw new Error(`Unknown character filter: ${filter.S}`); + throw new Error(`Unknown character filter: ${filter}`); } }); const tokenizer: TokenizerType = (() => { - if (!analyzer.tokenizer || !analyzer.tokenizer.S) { - throw new Error(`Tokenizer is not defined`); + if (!analyzer.tokenizer) { + return TokenizerType.KUROMOJI_TOKENIZER; } - switch (analyzer.tokenizer.S) { + switch (analyzer.tokenizer) { case "kuromoji_tokenizer": return TokenizerType.KUROMOJI_TOKENIZER; case "icu_tokenizer": return TokenizerType.ICU_TOKENIZER; default: - throw new Error(`Unknown tokenizer: ${analyzer.tokenizer.S}`); + throw new Error(`Unknown tokenizer: ${analyzer.tokenizer}`); } })(); const tokenFilters: TokenFilterType[] = - analyzer.token_filters?.L.map((filter: any) => { - switch (filter.S) { + analyzer.token_filters?.map((filter: any) => { + switch (filter) { case "kuromoji_baseform": return TokenFilterType.KUROMOJI_BASEFORM; case "kuromoji_part_of_speech": @@ -229,7 +226,7 @@ export const getAnalyzer = (analyzer: any): Analyzer | undefined => { case "icu_folding": return TokenFilterType.ICU_FOLDING; default: - throw new Error(`Unknown token filter: ${filter.S}`); + throw new Error(`Unknown token filter: ${filter}`); } }) || []; diff --git a/cdk/lib/utils/parameter-models.ts b/cdk/lib/utils/parameter-models.ts index 3f8060667..0ac597b54 100644 --- a/cdk/lib/utils/parameter-models.ts +++ b/cdk/lib/utils/parameter-models.ts @@ -146,8 +146,8 @@ const ApiPublishParametersSchema = BaseParametersSchema.extend({ */ const BedrockCustomBotParametersSchema = BaseParametersSchema.extend({ // Bot configuration - pk: z.string(), - sk: z.string(), + ownerUserId: z.string(), + botId: z.string(), documentBucketName: z.string(), knowledge: z.string(), knowledgeBase: z.string(), @@ -159,6 +159,20 @@ const BedrockCustomBotParametersSchema = BaseParametersSchema.extend({ .default("false"), }); +/** + * Parameters schema for shared Knowledge Bases + */ +const BedrockSharedKnowledgeBasesParametersSchema = BaseParametersSchema.extend({ + // Knowledge Base configuration + sharedKnowledgeBases: z.string(), + documentBucketName: z.string(), + enableRagReplicas: z + .string() + .optional() + .transform((val) => val === "true") + .default("false"), +}); + /** * Type definitions for each parameter set */ @@ -173,6 +187,9 @@ export type ApiPublishParametersInput = z.input< export type BedrockCustomBotParametersInput = z.input< typeof BedrockCustomBotParametersSchema >; +export type BedrockSharedKnowledgeBasesarametersInput = z.input< + typeof BedrockCustomBotParametersSchema +>; // Output types (for function returns, all properties are required) export type BaseParameters = z.infer; @@ -181,6 +198,9 @@ export type ApiPublishParameters = z.infer; export type BedrockCustomBotParameters = z.infer< typeof BedrockCustomBotParametersSchema >; +export type BedrockSharedKnowledgeBasesParameters = z.infer< + typeof BedrockSharedKnowledgeBasesParametersSchema +>; /** * Parse and validate parameters for the main Bedrock Chat application. @@ -333,14 +353,33 @@ export function resolveBedrockCustomBotParameters(): BedrockCustomBotParameters envName: getEnvVar("ENV_NAME"), envPrefix: getEnvVar("ENV_PREFIX"), bedrockRegion: getEnvVar("BEDROCK_REGION"), - pk: getEnvVar("PK"), - sk: getEnvVar("SK"), + ownerUserId: getEnvVar("OWNER_USER_ID"), + botId: getEnvVar("BOT_ID"), documentBucketName: getEnvVar("BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME"), knowledge: getEnvVar("KNOWLEDGE"), - knowledgeBase: getEnvVar("BEDROCK_KNOWLEDGE_BASE"), - guardrails: getEnvVar("BEDROCK_GUARDRAILS"), + knowledgeBase: getEnvVar("KNOWLEDGE_BASE"), + guardrails: getEnvVar("GUARDRAILS"), enableRagReplicas: getEnvVar("ENABLE_RAG_REPLICAS"), }; return BedrockCustomBotParametersSchema.parse(envVars); } + +/** + * Parse and validate parameters for shared Knowledge Bases. + * 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 resolveBedrockSharedKnowledgeBasesParameters(): BedrockSharedKnowledgeBasesParameters { + const envVars = { + envName: getEnvVar("ENV_NAME"), + envPrefix: getEnvVar("ENV_PREFIX"), + bedrockRegion: getEnvVar("BEDROCK_REGION"), + sharedKnowledgeBases: getEnvVar("SHARED_KNOWLEDGE_BASES"), + documentBucketName: getEnvVar("BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME"), + enableRagReplicas: getEnvVar("ENABLE_RAG_REPLICAS"), + }; + + return BedrockSharedKnowledgeBasesParametersSchema.parse(envVars); +} From ceb55c45e560e01c33a055665ace66364d8a60a5 Mon Sep 17 00:00:00 2001 From: Yukinobu Mine Date: Thu, 23 Oct 2025 18:50:04 +0900 Subject: [PATCH 3/9] Refactor embedding state machine. --- backend/app/repositories/custom_bot.py | 68 +- .../app/repositories/models/custom_bot_kb.py | 21 + backend/app/usecases/bot.py | 37 +- backend/app/utils.py | 33 +- .../bootstrap_state_machine.py | 220 +++-- .../finalize_custom_bot_build.py | 50 +- .../finalize_shared_knowledge_bases_build.py | 80 +- .../synchronize_data_source.py | 193 ++++ backend/poetry.lock | 924 +++++++++--------- backend/pyproject.toml | 2 +- cdk/lib/bedrock-chat-stack.ts | 19 +- cdk/lib/constructs/api.ts | 6 +- cdk/lib/constructs/database.ts | 12 - cdk/lib/constructs/embedding.ts | 324 ++---- .../knowledgeBase/pages/BotKbEditPage.tsx | 20 +- 15 files changed, 1141 insertions(+), 868 deletions(-) create mode 100644 backend/embedding_statemachine/bedrock_knowledge_base/synchronize_data_source.py diff --git a/backend/app/repositories/custom_bot.py b/backend/app/repositories/custom_bot.py index ef36b2adf..ec166e586 100644 --- a/backend/app/repositories/custom_bot.py +++ b/backend/app/repositories/custom_bot.py @@ -1,5 +1,4 @@ import base64 -from hashlib import md5 import json import logging from decimal import Decimal as decimal @@ -93,23 +92,6 @@ def store_bot(custom_bot: BotModel): item["IsStarred"] = "TRUE" if custom_bot.bedrock_knowledge_base: item["BedrockKnowledgeBase"] = custom_bot.bedrock_knowledge_base.model_dump() - if custom_bot.bedrock_knowledge_base.type is not None: - item["BedrockKnowledgeBaseType"] = custom_bot.bedrock_knowledge_base.type - item["BedrockKnowledgeBaseHash"] = ( - base64.b32encode( - md5( - custom_bot.bedrock_knowledge_base.model_dump_json( - exclude={ - "knowledge_base_id", - "exist_knowledge_base_id", - "data_source_ids", - } - ).encode() - ).digest() - ) - .decode() - .rstrip("=") - ) if custom_bot.bedrock_guardrails: item["GuardrailsParams"] = custom_bot.bedrock_guardrails.model_dump() @@ -193,34 +175,6 @@ def update_bot( bedrock_knowledge_base.model_dump() ) - if bedrock_knowledge_base.type is not None: - update_expression += ( - ", BedrockKnowledgeBaseType = :bedrock_knowledge_base_type" - ", BedrockKnowledgeBaseHash = :bedrock_knowledge_base_hash" - ) - expression_attribute_values[":bedrock_knowledge_base_type"] = ( - bedrock_knowledge_base.type - ) - expression_attribute_values[":bedrock_knowledge_base_hash"] = ( - base64.b32encode( - md5( - bedrock_knowledge_base.model_dump_json( - exclude={ - "knowledge_base_id", - "exist_knowledge_base_id", - "data_source_ids", - } - ).encode() - ).digest() - ) - .decode() - .rstrip("=") - ) - - else: - remove_attributes.append("BedrockKnowledgeBaseType") - remove_attributes.append("BedrockKnowledgeBaseHash") - if bedrock_guardrails: if not bedrock_guardrails.is_guardrail_enabled: bedrock_guardrails.guardrail_arn = "" @@ -752,6 +706,28 @@ def find_bot_by_id(bot_id: str) -> BotModel: return bot +def find_queued_bots() -> list[BotModel]: + """Find all 'QUEUED' bots.""" + bot_table = get_bot_table_client() + bots: list[BotModel] = [] + query_params = { + "IndexName": "SyncStatusIndex", + "KeyConditionExpression": Key("SyncStatus").eq("QUEUED"), + } + while True: + response = bot_table.query(**query_params) + items = response["Items"] + bots.extend(BotModel.from_dynamo_item(item) for item in items) + + last_evaluated_key = response.get("LastEvaluatedKey") + if last_evaluated_key is None: + break + + query_params["ExclusiveStartKey"] = last_evaluated_key + + return bots + + def find_pinned_public_bots() -> list[BotMeta]: """Find all pinned bots.""" table = get_bot_table_client() diff --git a/backend/app/repositories/models/custom_bot_kb.py b/backend/app/repositories/models/custom_bot_kb.py index 3d52a0829..0517864fc 100644 --- a/backend/app/repositories/models/custom_bot_kb.py +++ b/backend/app/repositories/models/custom_bot_kb.py @@ -1,3 +1,6 @@ +import base64 +from hashlib import md5 + from app.routes.schemas.bot_kb import ( type_kb_chunking_strategy, type_kb_embeddings_model, @@ -94,3 +97,21 @@ class BedrockKnowledgeBaseModel(BaseModel): web_crawling_filters: WebCrawlingFiltersModel = WebCrawlingFiltersModel( exclude_patterns=[], include_patterns=[] ) + + +def calc_knowledge_base_hash(knowledge_base: BedrockKnowledgeBaseModel) -> str: + return ( + base64.b32encode( + md5( + knowledge_base.model_dump_json( + exclude={ + "knowledge_base_id", + "exist_knowledge_base_id", + "data_source_ids", + } + ).encode() + ).digest() + ) + .decode() + .rstrip("=") + ) diff --git a/backend/app/usecases/bot.py b/backend/app/usecases/bot.py index b47524524..f32fe63c1 100644 --- a/backend/app/usecases/bot.py +++ b/backend/app/usecases/bot.py @@ -80,9 +80,8 @@ delete_file_from_s3, delete_files_with_prefix_from_s3, generate_presigned_url, - get_current_time, move_file_in_s3, - store_api_key_to_secret_manager, + start_embedding_state_machine, ) logger = logging.getLogger(__name__) @@ -145,6 +144,14 @@ def create_new_bot(user: User, bot_input: BotInput) -> BotOutput: new_bot = BotModel.from_input(bot_input, owner_user_id=user.id, knowledge=knowledge) store_bot(new_bot) + if new_bot.sync_status == "QUEUED": + start_embedding_state_machine( + user_id=user.id, + bot_id=new_bot.id, + added_filenames=[], + deleted_filenames=[], + ) + return new_bot.to_output() @@ -163,29 +170,31 @@ def modify_owned_bot( sitemap_urls = [] filenames = [] s3_urls = [] - sync_status: type_sync_status = "QUEUED" + added_filenames = [] + unchanged_filenames = [] + deleted_filenames = [] if modify_input.knowledge: source_urls = modify_input.knowledge.source_urls sitemap_urls = modify_input.knowledge.sitemap_urls s3_urls = modify_input.knowledge.s3_urls + added_filenames = modify_input.knowledge.added_filenames + unchanged_filenames = modify_input.knowledge.unchanged_filenames + deleted_filenames = modify_input.knowledge.deleted_filenames # Commit changes to S3 _update_s3_documents_by_diff( user.id, bot_id, - modify_input.knowledge.added_filenames, - modify_input.knowledge.deleted_filenames, + added_filenames, + deleted_filenames, ) # Delete files from upload temp directory delete_files_with_prefix_from_s3( DOCUMENT_BUCKET, compose_upload_temp_s3_prefix(bot.owner_user_id, bot_id) ) - filenames = ( - modify_input.knowledge.added_filenames - + modify_input.knowledge.unchanged_filenames - ) + filenames = added_filenames + unchanged_filenames generation_params = ( GenerationParamsModel( @@ -205,7 +214,7 @@ def modify_owned_bot( # if knowledge is not updated, skip embeding process. # 'sync_status = "QUEUED"' will execute embeding process and update dynamodb record. # 'sync_status= "SUCCEEDED"' will update only dynamodb record. - sync_status = ( + sync_status: type_sync_status = ( "QUEUED" if modify_input.is_embedding_required(bot) or modify_input.is_guardrails_update_required(bot) @@ -272,6 +281,14 @@ def modify_owned_bot( ), ) + if sync_status == "QUEUED": + start_embedding_state_machine( + user_id=user.id, + bot_id=bot.id, + added_filenames=added_filenames, + deleted_filenames=deleted_filenames, + ) + return BotModifyOutput( id=bot_id, title=modify_input.title, diff --git a/backend/app/utils.py b/backend/app/utils.py index e32aeb116..447bcae91 100644 --- a/backend/app/utils.py +++ b/backend/app/utils.py @@ -2,14 +2,14 @@ import logging import os from datetime import datetime -from typing import Any, Literal +from typing import Literal import boto3 -from app.repositories.models.custom_bot_guardrails import BedrockGuardrailsModel -from app.user import User from botocore.client import Config from botocore.exceptions import ClientError +from app.user import User + logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) @@ -25,6 +25,7 @@ "PUBLISH_API_CODEBUILD_PROJECT_NAME", "" ) USER_POOL_ID = os.environ.get("USER_POOL_ID", "") +EMBEDDING_STATE_MACHINE_ARN = os.environ.get("EMBEDDING_STATE_MACHINE_ARN") def snake_to_camel(snake_str): @@ -312,3 +313,29 @@ def delete_api_key_from_secret_manager(user_id: str, bot_id: str, prefix: str) - except ClientError as e: logger.error(f"Error accessing Secrets Manager: {e}") raise + + +def start_embedding_state_machine( + user_id: str, + bot_id: str, + added_filenames: list[str], + deleted_filenames: list[str], +): + client = boto3.client("stepfunctions") + client.start_execution( + stateMachineArn=EMBEDDING_STATE_MACHINE_ARN, + input=json.dumps( + { + "QueuedBots": [ + { + "OwnerUserId": user_id, + "BotId": bot_id, + "Files": { + "Added": added_filenames, + "Deleted": deleted_filenames, + }, + }, + ], + } + ), + ) diff --git a/backend/embedding_statemachine/bedrock_knowledge_base/bootstrap_state_machine.py b/backend/embedding_statemachine/bedrock_knowledge_base/bootstrap_state_machine.py index 539652c7e..bdd3badd0 100644 --- a/backend/embedding_statemachine/bedrock_knowledge_base/bootstrap_state_machine.py +++ b/backend/embedding_statemachine/bedrock_knowledge_base/bootstrap_state_machine.py @@ -1,127 +1,201 @@ -from typing import TypedDict -from boto3.dynamodb.conditions import Key +from typing import TypedDict, NotRequired +from boto3.dynamodb.conditions import Attr -from app.repositories.common import get_bot_table_client -from app.repositories.models.custom_bot import BotModel, BedrockKnowledgeBaseModel +from app.repositories.custom_bot import ( + find_bot_by_id, + find_queued_bots, + get_bot_table_client, +) +from app.repositories.models.custom_bot import BotModel +from app.repositories.models.custom_bot_kb import ( + BedrockKnowledgeBaseModel, + calc_knowledge_base_hash, +) def handler(event, context): - bots = find_queued_bots() - shared_knowledge_bases = find_shared_knowledge_bases() + queued_bots_from_event = event.get("QueuedBots") + if queued_bots_from_event is not None: + queued_bots = get_queued_bots_from_event(queued_bots_from_event) + + else: + queued_bots = get_queued_bots() + + shared_knowledge_bases = find_shared_knowledge_bases(queued_bots) + return { - "Bots": [ + "QueuedBots": [ { - "BotId": bot.id, - "OwnerUserId": bot.owner_user_id, - "Knowledge": bot.knowledge.model_dump(), - "KnowledgeBaseHash": knowledge_base_hash, + "OwnerUserId": queued_bot["bot"].owner_user_id, + "BotId": queued_bot["bot"].id, + **( + { + "Files": queued_bot["files"], + } + if "files" in queued_bot + else {} + ), + "Knowledge": queued_bot["bot"].knowledge.model_dump(), + "KnowledgeBaseHash": ( + calc_knowledge_base_hash(queued_bot["bot"].bedrock_knowledge_base) + if queued_bot["bot"].bedrock_knowledge_base is not None + else None + ), "KnowledgeBase": ( - bot.bedrock_knowledge_base.model_dump( + queued_bot["bot"].bedrock_knowledge_base.model_dump( exclude={ "knowledge_base_id", "exist_knowledge_base_id", "data_source_ids", } ) - if bot.bedrock_knowledge_base is not None - and bot.bedrock_knowledge_base.type == "dedicated" + if queued_bot["bot"].bedrock_knowledge_base is not None + and queued_bot["bot"].bedrock_knowledge_base.type == "dedicated" else {} ), "Guardrails": ( - bot.bedrock_guardrails.model_dump() - if bot.bedrock_guardrails is not None - and bot.bedrock_guardrails.is_guardrail_enabled + queued_bot["bot"].bedrock_guardrails.model_dump() + if queued_bot["bot"].bedrock_guardrails is not None + and queued_bot["bot"].bedrock_guardrails.is_guardrail_enabled else {} ), } - for bot, knowledge_base_hash in bots - ], - "SharedKnowledgeBases": [ - { - "KnowledgeBaseHash": shared_knowledge_base["knowledge_base_hash"], - "KnowledgeBase": shared_knowledge_base["knowledge_base"].model_dump( - exclude={ - "knowledge_base_id", - "exist_knowledge_base_id", - "data_source_ids", - } - ), - "Bots": [ - { - "OwnerUserId": bot.owner_user_id, - "BotId": bot.id, - } - for bot in shared_knowledge_base["bots"] - ], - } - for shared_knowledge_base in shared_knowledge_bases + for queued_bot in queued_bots ], + "SharedKnowledgeBases": { + "KnowledgeBases": [ + { + "KnowledgeBaseHash": shared_knowledge_base["knowledge_base_hash"], + "KnowledgeBase": shared_knowledge_base["knowledge_base"].model_dump( + exclude={ + "knowledge_base_id", + "exist_knowledge_base_id", + "data_source_ids", + } + ), + } + for shared_knowledge_base in shared_knowledge_bases + ], + "QueuedBotsForKnowledgeBases": [ + { + "KnowledgeBaseHash": shared_knowledge_base["knowledge_base_hash"], + "Bots": [ + { + "OwnerUserId": queued_bot["bot"].owner_user_id, + "BotId": queued_bot["bot"].id, + **( + { + "Files": queued_bot["files"], + } + if "files" in queued_bot + else {} + ), + } + for queued_bot in shared_knowledge_base["queued_bots"] + ], + } + for shared_knowledge_base in shared_knowledge_bases + ], + }, } -def find_queued_bots() -> list[tuple[BotModel, str | None]]: - bot_table = get_bot_table_client() +class BotFiles(TypedDict): + Added: list[str] + Deleted: list[str] - bots: list[tuple[BotModel, str | None]] = [] - query_params = { - "IndexName": "SyncStatusIndex", - "KeyConditionExpression": Key("SyncStatus").eq("QUEUED"), - } - while True: - response = bot_table.query(**query_params) - items = response["Items"] - bots.extend( - (BotModel.from_dynamo_item(item), item.get("BedrockKnowledgeBaseHash")) - for item in items - ) - last_evaluated_key = response.get("LastEvaluatedKey") - if last_evaluated_key is None: - break +class QueuedBot(TypedDict): + bot: BotModel + files: NotRequired[BotFiles] - query_params["ExclusiveStartKey"] = last_evaluated_key - return bots +def get_queued_bots_from_event(queued_bots_from_event: list[dict]) -> list[QueuedBot]: + result: list[QueuedBot] = [] + for queued_bot in queued_bots_from_event: + user_id = queued_bot.get("OwnerUserId") + bot_id = queued_bot.get("BotId") + if user_id and bot_id: + bot = find_bot_by_id(bot_id) + files = queued_bot.get("Files", {}) + + added_files = files.get("Added", []) + deleted_files = files.get("Deleted", []) + if added_files or deleted_files: + result.append( + { + "bot": bot, + "files": { + "Added": added_files, + "Deleted": deleted_files, + }, + } + ) + + else: + result.append( + { + "bot": bot, + } + ) + + return result + + +def get_queued_bots() -> list[QueuedBot]: + bots = find_queued_bots() + return [ + { + "bot": bot, + } + for bot in bots + ] class SharedKnowledgeBase(TypedDict): knowledge_base_hash: str knowledge_base: BedrockKnowledgeBaseModel - bots: list[BotModel] + queued_bots: list[QueuedBot] -def find_shared_knowledge_bases() -> list[SharedKnowledgeBase]: +def find_shared_knowledge_bases( + queued_bots: list[QueuedBot], +) -> list[SharedKnowledgeBase]: bot_table = get_bot_table_client() + scan_params = { + "FilterExpression": Attr("BedrockKnowledgeBase.type").eq("shared"), + } + queued_bots_dict = dict( + (queued_bot["bot"].id, queued_bot) for queued_bot in queued_bots + ) knowledge_bases: dict[str, SharedKnowledgeBase] = {} - query_params = { - "IndexName": "KnowledgeBaseTypeIndex", - "KeyConditionExpression": Key("BedrockKnowledgeBaseType").eq("shared"), - } while True: - response = bot_table.query(**query_params) + response = bot_table.scan(**scan_params) items = response["Items"] for item in items: bot = BotModel.from_dynamo_item(item) - knowledge_base_hash = item.get("BedrockKnowledgeBaseHash") - if ( - bot.bedrock_knowledge_base is not None - and knowledge_base_hash is not None - ): + if bot.bedrock_knowledge_base is not None: + knowledge_base_hash = calc_knowledge_base_hash( + bot.bedrock_knowledge_base + ) if knowledge_base_hash not in knowledge_bases: knowledge_bases[knowledge_base_hash] = { "knowledge_base_hash": knowledge_base_hash, "knowledge_base": bot.bedrock_knowledge_base, - "bots": [bot], + "queued_bots": [], } - else: - knowledge_bases[knowledge_base_hash]["bots"].append(bot) + queued_bot = queued_bots_dict.get(bot.id) + if queued_bot is not None: + knowledge_bases[knowledge_base_hash]["queued_bots"].append( + queued_bot + ) last_evaluated_key = response.get("LastEvaluatedKey") if last_evaluated_key is None: break - query_params["ExclusiveStartKey"] = last_evaluated_key + scan_params["ExclusiveStartKey"] = last_evaluated_key return list(knowledge_bases.values()) diff --git a/backend/embedding_statemachine/bedrock_knowledge_base/finalize_custom_bot_build.py b/backend/embedding_statemachine/bedrock_knowledge_base/finalize_custom_bot_build.py index 4ba40180d..8aec9cdc3 100644 --- a/backend/embedding_statemachine/bedrock_knowledge_base/finalize_custom_bot_build.py +++ b/backend/embedding_statemachine/bedrock_knowledge_base/finalize_custom_bot_build.py @@ -1,5 +1,5 @@ import os -from typing import Any, Dict, List, TypedDict, NotRequired +from typing import List, TypedDict import boto3 from app.repositories.custom_bot import ( @@ -9,12 +9,20 @@ BEDROCK_REGION = os.environ.get("BEDROCK_REGION") -cf_client = boto3.client("cloudformation", BEDROCK_REGION) +cfn = boto3.client("cloudformation", BEDROCK_REGION) + + +class DataSourceFiles(TypedDict): + OwnerUserId: str + BotId: str + Added: list[str] + Deleted: list[str] class DataSource(TypedDict): KnowledgeBaseId: str DataSourceId: str + Files: list[DataSourceFiles] class Bot(TypedDict): @@ -31,13 +39,16 @@ def handler(event, context) -> StackOutput: print(event) user_id = event["OwnerUserId"] bot_id = event["BotId"] + files = event.get("Files") # Note: stack naming rule is defined on: # cdk/bin/bedrock-custom-bot.ts stack_name = f"BrChatKbStack{bot_id}" - response = cf_client.describe_stacks(StackName=stack_name) - outputs = response["Stacks"][0]["Outputs"] + response = cfn.describe_stacks(StackName=stack_name) + outputs = response["Stacks"][0].get("Outputs") + if not outputs: + raise ValueError(f"No outputs found in CloudFormation stack '{stack_name}'") knowledge_base_id = None data_source_ids: List[str] = [] @@ -46,17 +57,20 @@ def handler(event, context) -> StackOutput: guardrail_version = None for output in outputs: - if output["OutputKey"] == "KnowledgeBaseId": - knowledge_base_id = output["OutputValue"] + key = output.get("OutputKey") + value = output.get("OutputValue") + if key and value: + if key == "KnowledgeBaseId": + knowledge_base_id = value - elif output["OutputKey"].startswith("DataSource"): - data_source_ids.append(output["OutputValue"]) + elif key.startswith("DataSource"): + data_source_ids.append(value) - elif output["OutputKey"] == "GuardrailArn": - guardrail_arn = output["OutputValue"] + elif key == "GuardrailArn": + guardrail_arn = value - elif output["OutputKey"] == "GuardrailVersion": - guardrail_version = output["OutputValue"] + elif key == "GuardrailVersion": + guardrail_version = value result: StackOutput = { "DataSources": [], @@ -73,6 +87,18 @@ def handler(event, context) -> StackOutput: { "KnowledgeBaseId": knowledge_base_id, "DataSourceId": data_source_id, + "Files": ( + [ + { + "OwnerUserId": user_id, + "BotId": bot_id, + "Added": files["Added"], + "Deleted": files["Deleted"], + } + ] + if files is not None + else [] + ), } for data_source_id in data_source_ids ) diff --git a/backend/embedding_statemachine/bedrock_knowledge_base/finalize_shared_knowledge_bases_build.py b/backend/embedding_statemachine/bedrock_knowledge_base/finalize_shared_knowledge_bases_build.py index 20a1ed686..8f5c97372 100644 --- a/backend/embedding_statemachine/bedrock_knowledge_base/finalize_shared_knowledge_bases_build.py +++ b/backend/embedding_statemachine/bedrock_knowledge_base/finalize_shared_knowledge_bases_build.py @@ -1,20 +1,25 @@ import os -from typing import Any, Dict, List, TypedDict, NotRequired +from typing import List, TypedDict import boto3 -from app.repositories.custom_bot import ( - update_knowledge_base_id, - update_guardrails_params, -) +from app.repositories.custom_bot import update_knowledge_base_id BEDROCK_REGION = os.environ.get("BEDROCK_REGION") -cf_client = boto3.client("cloudformation", BEDROCK_REGION) +cfn = boto3.client("cloudformation", BEDROCK_REGION) + + +class DataSourceFiles(TypedDict): + OwnerUserId: str + BotId: str + Added: list[str] + Deleted: list[str] class DataSource(TypedDict): KnowledgeBaseId: str DataSourceId: str + Files: list[DataSourceFiles] class StackOutput(TypedDict): @@ -23,46 +28,71 @@ class StackOutput(TypedDict): def handler(event, context) -> StackOutput: print(event) - shared_knowledge_bases = event["SharedKnowledgeBases"] + queued_bots_for_knowledge_bases = event["SharedKnowledgeBases"][ + "QueuedBotsForKnowledgeBases" + ] # Note: stack naming rule is defined on: # cdk/bin/bedrock-shared-knowledge-bases.ts stack_name = "BrChatSharedKbStack" - response = cf_client.describe_stacks(StackName=stack_name) - outputs = response["Stacks"][0]["Outputs"] + response = cfn.describe_stacks(StackName=stack_name) + outputs = response["Stacks"][0].get("Outputs") + if not outputs: + raise ValueError(f"No outputs found in CloudFormation stack '{stack_name}'") + print(outputs) result: StackOutput = { "DataSources": [], } - for shared_knowledge_base in shared_knowledge_bases: - knowledge_base_hash = shared_knowledge_base["KnowledgeBaseHash"] + for queued_bots_for_knowledge_base in queued_bots_for_knowledge_bases: + knowledge_base_hash = queued_bots_for_knowledge_base["KnowledgeBaseHash"] knowledge_base_id = None data_source_ids: List[str] = [] for output in outputs: - if output["OutputKey"] == f"KnowledgeBaseId{knowledge_base_hash}": - knowledge_base_id = output["OutputValue"] + key = output.get("OutputKey") + value = output.get("OutputValue") + if key and value: + if key == f"KnowledgeBaseId{knowledge_base_hash}": + knowledge_base_id = value - elif output["OutputKey"].startswith(f"DataSource{knowledge_base_hash}"): - data_source_ids.append(output["OutputValue"]) + elif key.startswith(f"DataSource{knowledge_base_hash}"): + data_source_ids.append(value) if knowledge_base_id: - result["DataSources"].extend( - { - "KnowledgeBaseId": knowledge_base_id, - "DataSourceId": data_source_id, - } - for data_source_id in data_source_ids - ) - - bots = shared_knowledge_base["Bots"] + bots = queued_bots_for_knowledge_base["Bots"] + data_source_files: list[DataSourceFiles] = [] for bot in bots: + user_id = bot["OwnerUserId"] + bot_id = bot["BotId"] + files = bot.get("Files") + if files is not None: + data_source_files.append( + { + "OwnerUserId": user_id, + "BotId": bot_id, + "Added": files["Added"], + "Deleted": files["Deleted"], + } + ) + update_knowledge_base_id( - bot["OwnerUserId"], bot["BotId"], knowledge_base_id, data_source_ids + user_id, bot_id, knowledge_base_id, data_source_ids ) + result["DataSources"].extend( + ( + { + "KnowledgeBaseId": knowledge_base_id, + "DataSourceId": data_source_id, + "Files": data_source_files, + } + for data_source_id in data_source_ids + ) + ) + return result diff --git a/backend/embedding_statemachine/bedrock_knowledge_base/synchronize_data_source.py b/backend/embedding_statemachine/bedrock_knowledge_base/synchronize_data_source.py new file mode 100644 index 000000000..f3f903f08 --- /dev/null +++ b/backend/embedding_statemachine/bedrock_knowledge_base/synchronize_data_source.py @@ -0,0 +1,193 @@ +import os +from itertools import islice + +from app.utils import get_bedrock_agent_client + +DOCUMENT_BUCKET = os.environ.get("DOCUMENT_BUCKET") +bedrock_agent = get_bedrock_agent_client() + + +def handler(event, context): + match event["Action"]: + case "Ingest": + return handle_ingest(event) + + case "Check": + return handle_check(event) + + case _ as e: + raise Exception(f"Unknown action {e}") + + +def handle_ingest(event): + knowledge_base_id = event["KnowledgeBaseId"] + data_source_id = event["DataSourceId"] + get_data_source_response = bedrock_agent.get_data_source( + knowledgeBaseId=knowledge_base_id, + dataSourceId=data_source_id, + ) + data_source_type = get_data_source_response["dataSource"][ + "dataSourceConfiguration" + ]["type"] + + data_source_files = event.get("Files") + if data_source_type == "S3" and data_source_files: + for data_source_file in data_source_files: + user_id = data_source_file["OwnerUserId"] + bot_id = data_source_file["BotId"] + + added_files = data_source_file["Added"] + for i in range(0, len(added_files), 10): + bedrock_agent.ingest_knowledge_base_documents( + knowledgeBaseId=knowledge_base_id, + dataSourceId=data_source_id, + documents=[ + { + "content": { + "dataSourceType": "S3", + "s3": { + "s3Location": { + "uri": f"s3://{DOCUMENT_BUCKET}/{user_id}/{bot_id}/documents/{file}", + }, + }, + }, + } + for file in islice(added_files, i, i + 10) + ], + ) + + deleted_files = data_source_file["Deleted"] + for i in range(0, len(deleted_files), 10): + bedrock_agent.delete_knowledge_base_documents( + knowledgeBaseId=knowledge_base_id, + dataSourceId=data_source_id, + documentIdentifiers=[ + { + "dataSourceType": "S3", + "s3": { + "uri": f"s3://{DOCUMENT_BUCKET}/{user_id}/{bot_id}/documents/{file}", + }, + } + for file in islice(deleted_files, i, i + 10) + ], + ) + + return { + "KnowledgeBaseId": knowledge_base_id, + "DataSourceId": data_source_id, + "Files": data_source_files, + "IngestionJobId": None, + } + + else: + start_job_response = bedrock_agent.start_ingestion_job( + knowledgeBaseId=knowledge_base_id, + dataSourceId=data_source_id, + ) + ingestion_job_id = start_job_response["ingestionJob"]["ingestionJobId"] + + return { + "KnowledgeBaseId": knowledge_base_id, + "DataSourceId": data_source_id, + "Files": None, + "IngestionJobId": ingestion_job_id, + } + + +class RetryException(Exception): + pass + + +def handle_check(event): + ingestion_job = event["IngestionJob"] + knowledge_base_id = ingestion_job["KnowledgeBaseId"] + data_source_id = ingestion_job["DataSourceId"] + data_source_files = ingestion_job.get("Files") + if data_source_files: + for data_source_file in data_source_files: + user_id = data_source_file["OwnerUserId"] + bot_id = data_source_file["BotId"] + + added_files = data_source_file["Added"] + for i in range(0, len(added_files), 10): + get_documents_response = bedrock_agent.get_knowledge_base_documents( + knowledgeBaseId=knowledge_base_id, + dataSourceId=data_source_id, + documentIdentifiers=[ + { + "dataSourceType": "S3", + "s3": { + "uri": f"s3://{DOCUMENT_BUCKET}/{user_id}/{bot_id}/documents/{file}", + }, + } + for file in islice(added_files, i, i + 10) + ], + ) + for document in get_documents_response["documentDetails"]: + status = document["status"] + uri = document["identifier"].get("s3", {})["uri"] + match status: + case "INDEXED": + pass + + case ( + "PENDING" | "STARTING" | "IN_PROGRESS" | "PARTIALLY_INDEXED" + ): + raise RetryException() + + case _: + raise Exception(f"File {uri}: Bad status '{status}'.") + + deleted_files = data_source_file["Deleted"] + for i in range(0, len(deleted_files), 10): + get_documents_response = bedrock_agent.get_knowledge_base_documents( + knowledgeBaseId=knowledge_base_id, + dataSourceId=data_source_id, + documentIdentifiers=[ + { + "dataSourceType": "S3", + "s3": { + "uri": f"s3://{DOCUMENT_BUCKET}/{user_id}/{bot_id}/documents/{file}", + }, + } + for file in islice(deleted_files, i, i + 10) + ], + ) + for document in get_documents_response["documentDetails"]: + status = document["status"] + uri = document["identifier"].get("s3", {})["uri"] + match status: + case "NOT_FOUND": + pass + + case "PENDING" | "DELETING" | "DELETE_IN_PROGRESS": + raise RetryException() + + case _: + raise Exception(f"File '{uri}': Bad status '{status}'.") + + return + + ingestion_job_id = ingestion_job.get("IngestionJobId") + if ingestion_job_id: + get_job_response = bedrock_agent.get_ingestion_job( + knowledgeBaseId=knowledge_base_id, + dataSourceId=data_source_id, + ingestionJobId=ingestion_job_id, + ) + status = get_job_response["ingestionJob"]["status"] + match status: + case "COMPLETE": + pass + + case "STARTING" | "IN_PROGRESS": + raise RetryException() + + case _: + raise Exception( + f"Ingestion Job '{ingestion_job_id}': Bad status '{status}'." + ) + + return + + raise Exception("Invalid parameters.") diff --git a/backend/poetry.lock b/backend/poetry.lock index e343c5e7a..5f8cdee9a 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "annotated-types" @@ -104,464 +104,474 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "boto3" -version = "1.37.29" +version = "1.40.56" description = "The AWS SDK for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "boto3-1.37.29-py3-none-any.whl", hash = "sha256:869979050e2cf6f5461503e0f1c8f226e47ec02802e88a2210f085ec22485945"}, - {file = "boto3-1.37.29.tar.gz", hash = "sha256:5702e38356b93c56ed2a27e17f7664d791f1fe2eafd58ae6ab3853b2804cadd2"}, + {file = "boto3-1.40.56-py3-none-any.whl", hash = "sha256:8985a840d57671aa3c6124b0c178e79be97e3447de4b5819156071793f82ee5c"}, + {file = "boto3-1.40.56.tar.gz", hash = "sha256:c1afdb04dd27418fc58400434ab8e05998bb452b69c428168d9ada344fe6b93e"}, ] [package.dependencies] -botocore = ">=1.37.29,<1.38.0" +botocore = ">=1.40.56,<1.41.0" jmespath = ">=0.7.1,<2.0.0" -s3transfer = ">=0.11.0,<0.12.0" +s3transfer = ">=0.14.0,<0.15.0" [package.extras] crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "boto3-stubs" -version = "1.37.29" -description = "Type annotations for boto3 1.37.29 generated with mypy-boto3-builder 8.10.1" +version = "1.40.56" +description = "Type annotations for boto3 1.40.56 generated with mypy-boto3-builder 8.11.0" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "boto3_stubs-1.37.29-py3-none-any.whl", hash = "sha256:a3471040c098c4e82a87fafeb38deb66eb4966950a771c62eba0bf36834f69d6"}, - {file = "boto3_stubs-1.37.29.tar.gz", hash = "sha256:36444606a7c1c10c9700dde590f7afb134546065553f761f36207c1feb847e0b"}, + {file = "boto3_stubs-1.40.56-py3-none-any.whl", hash = "sha256:638398e9b58769db8d050f93cd81aab8213e6a617f13dcd1632c4b09c870c732"}, + {file = "boto3_stubs-1.40.56.tar.gz", hash = "sha256:e5644523eff7f50bade5e9f4a24544b40f849e9051949e3501203a20627f4263"}, ] [package.dependencies] -boto3 = {version = "1.37.29", optional = true, markers = "extra == \"boto3\""} +boto3 = {version = "1.40.56", optional = true, markers = "extra == \"boto3\""} botocore-stubs = "*" -mypy-boto3-bedrock = {version = ">=1.37.0,<1.38.0", optional = true, markers = "extra == \"bedrock\""} -mypy-boto3-bedrock-agent-runtime = {version = ">=1.37.0,<1.38.0", optional = true, markers = "extra == \"bedrock-agent-runtime\""} -mypy-boto3-bedrock-runtime = {version = ">=1.37.0,<1.38.0", optional = true, markers = "extra == \"bedrock-runtime\""} +mypy-boto3-bedrock = {version = ">=1.40.0,<1.41.0", optional = true, markers = "extra == \"bedrock\""} +mypy-boto3-bedrock-agent = {version = ">=1.40.0,<1.41.0", optional = true, markers = "extra == \"bedrock-agent\""} +mypy-boto3-bedrock-agent-runtime = {version = ">=1.40.0,<1.41.0", optional = true, markers = "extra == \"bedrock-agent-runtime\""} +mypy-boto3-bedrock-runtime = {version = ">=1.40.0,<1.41.0", optional = true, markers = "extra == \"bedrock-runtime\""} +mypy-boto3-cloudformation = {version = ">=1.40.0,<1.41.0", optional = true, markers = "extra == \"cloudformation\""} types-s3transfer = "*" [package.extras] -accessanalyzer = ["mypy-boto3-accessanalyzer (>=1.37.0,<1.38.0)"] -account = ["mypy-boto3-account (>=1.37.0,<1.38.0)"] -acm = ["mypy-boto3-acm (>=1.37.0,<1.38.0)"] -acm-pca = ["mypy-boto3-acm-pca (>=1.37.0,<1.38.0)"] -all = ["mypy-boto3-accessanalyzer (>=1.37.0,<1.38.0)", "mypy-boto3-account (>=1.37.0,<1.38.0)", "mypy-boto3-acm (>=1.37.0,<1.38.0)", "mypy-boto3-acm-pca (>=1.37.0,<1.38.0)", "mypy-boto3-amp (>=1.37.0,<1.38.0)", "mypy-boto3-amplify (>=1.37.0,<1.38.0)", "mypy-boto3-amplifybackend (>=1.37.0,<1.38.0)", "mypy-boto3-amplifyuibuilder (>=1.37.0,<1.38.0)", "mypy-boto3-apigateway (>=1.37.0,<1.38.0)", "mypy-boto3-apigatewaymanagementapi (>=1.37.0,<1.38.0)", "mypy-boto3-apigatewayv2 (>=1.37.0,<1.38.0)", "mypy-boto3-appconfig (>=1.37.0,<1.38.0)", "mypy-boto3-appconfigdata (>=1.37.0,<1.38.0)", "mypy-boto3-appfabric (>=1.37.0,<1.38.0)", "mypy-boto3-appflow (>=1.37.0,<1.38.0)", "mypy-boto3-appintegrations (>=1.37.0,<1.38.0)", "mypy-boto3-application-autoscaling (>=1.37.0,<1.38.0)", "mypy-boto3-application-insights (>=1.37.0,<1.38.0)", "mypy-boto3-application-signals (>=1.37.0,<1.38.0)", "mypy-boto3-applicationcostprofiler (>=1.37.0,<1.38.0)", "mypy-boto3-appmesh (>=1.37.0,<1.38.0)", "mypy-boto3-apprunner (>=1.37.0,<1.38.0)", "mypy-boto3-appstream (>=1.37.0,<1.38.0)", "mypy-boto3-appsync (>=1.37.0,<1.38.0)", "mypy-boto3-apptest (>=1.37.0,<1.38.0)", "mypy-boto3-arc-zonal-shift (>=1.37.0,<1.38.0)", "mypy-boto3-artifact (>=1.37.0,<1.38.0)", "mypy-boto3-athena (>=1.37.0,<1.38.0)", "mypy-boto3-auditmanager (>=1.37.0,<1.38.0)", "mypy-boto3-autoscaling (>=1.37.0,<1.38.0)", "mypy-boto3-autoscaling-plans (>=1.37.0,<1.38.0)", "mypy-boto3-b2bi (>=1.37.0,<1.38.0)", "mypy-boto3-backup (>=1.37.0,<1.38.0)", "mypy-boto3-backup-gateway (>=1.37.0,<1.38.0)", "mypy-boto3-backupsearch (>=1.37.0,<1.38.0)", "mypy-boto3-batch (>=1.37.0,<1.38.0)", "mypy-boto3-bcm-data-exports (>=1.37.0,<1.38.0)", "mypy-boto3-bcm-pricing-calculator (>=1.37.0,<1.38.0)", "mypy-boto3-bedrock (>=1.37.0,<1.38.0)", "mypy-boto3-bedrock-agent (>=1.37.0,<1.38.0)", "mypy-boto3-bedrock-agent-runtime (>=1.37.0,<1.38.0)", "mypy-boto3-bedrock-data-automation (>=1.37.0,<1.38.0)", "mypy-boto3-bedrock-data-automation-runtime (>=1.37.0,<1.38.0)", "mypy-boto3-bedrock-runtime (>=1.37.0,<1.38.0)", "mypy-boto3-billing (>=1.37.0,<1.38.0)", "mypy-boto3-billingconductor (>=1.37.0,<1.38.0)", "mypy-boto3-braket (>=1.37.0,<1.38.0)", "mypy-boto3-budgets (>=1.37.0,<1.38.0)", "mypy-boto3-ce (>=1.37.0,<1.38.0)", "mypy-boto3-chatbot (>=1.37.0,<1.38.0)", "mypy-boto3-chime (>=1.37.0,<1.38.0)", "mypy-boto3-chime-sdk-identity (>=1.37.0,<1.38.0)", "mypy-boto3-chime-sdk-media-pipelines (>=1.37.0,<1.38.0)", "mypy-boto3-chime-sdk-meetings (>=1.37.0,<1.38.0)", "mypy-boto3-chime-sdk-messaging (>=1.37.0,<1.38.0)", "mypy-boto3-chime-sdk-voice (>=1.37.0,<1.38.0)", "mypy-boto3-cleanrooms (>=1.37.0,<1.38.0)", "mypy-boto3-cleanroomsml (>=1.37.0,<1.38.0)", "mypy-boto3-cloud9 (>=1.37.0,<1.38.0)", "mypy-boto3-cloudcontrol (>=1.37.0,<1.38.0)", "mypy-boto3-clouddirectory (>=1.37.0,<1.38.0)", "mypy-boto3-cloudformation (>=1.37.0,<1.38.0)", "mypy-boto3-cloudfront (>=1.37.0,<1.38.0)", "mypy-boto3-cloudfront-keyvaluestore (>=1.37.0,<1.38.0)", "mypy-boto3-cloudhsm (>=1.37.0,<1.38.0)", "mypy-boto3-cloudhsmv2 (>=1.37.0,<1.38.0)", "mypy-boto3-cloudsearch (>=1.37.0,<1.38.0)", "mypy-boto3-cloudsearchdomain (>=1.37.0,<1.38.0)", "mypy-boto3-cloudtrail (>=1.37.0,<1.38.0)", "mypy-boto3-cloudtrail-data (>=1.37.0,<1.38.0)", "mypy-boto3-cloudwatch (>=1.37.0,<1.38.0)", "mypy-boto3-codeartifact (>=1.37.0,<1.38.0)", "mypy-boto3-codebuild (>=1.37.0,<1.38.0)", "mypy-boto3-codecatalyst (>=1.37.0,<1.38.0)", "mypy-boto3-codecommit (>=1.37.0,<1.38.0)", "mypy-boto3-codeconnections (>=1.37.0,<1.38.0)", "mypy-boto3-codedeploy (>=1.37.0,<1.38.0)", "mypy-boto3-codeguru-reviewer (>=1.37.0,<1.38.0)", "mypy-boto3-codeguru-security (>=1.37.0,<1.38.0)", "mypy-boto3-codeguruprofiler (>=1.37.0,<1.38.0)", "mypy-boto3-codepipeline (>=1.37.0,<1.38.0)", "mypy-boto3-codestar-connections (>=1.37.0,<1.38.0)", "mypy-boto3-codestar-notifications (>=1.37.0,<1.38.0)", "mypy-boto3-cognito-identity (>=1.37.0,<1.38.0)", "mypy-boto3-cognito-idp (>=1.37.0,<1.38.0)", "mypy-boto3-cognito-sync (>=1.37.0,<1.38.0)", "mypy-boto3-comprehend (>=1.37.0,<1.38.0)", "mypy-boto3-comprehendmedical (>=1.37.0,<1.38.0)", "mypy-boto3-compute-optimizer (>=1.37.0,<1.38.0)", "mypy-boto3-config (>=1.37.0,<1.38.0)", "mypy-boto3-connect (>=1.37.0,<1.38.0)", "mypy-boto3-connect-contact-lens (>=1.37.0,<1.38.0)", "mypy-boto3-connectcampaigns (>=1.37.0,<1.38.0)", "mypy-boto3-connectcampaignsv2 (>=1.37.0,<1.38.0)", "mypy-boto3-connectcases (>=1.37.0,<1.38.0)", "mypy-boto3-connectparticipant (>=1.37.0,<1.38.0)", "mypy-boto3-controlcatalog (>=1.37.0,<1.38.0)", "mypy-boto3-controltower (>=1.37.0,<1.38.0)", "mypy-boto3-cost-optimization-hub (>=1.37.0,<1.38.0)", "mypy-boto3-cur (>=1.37.0,<1.38.0)", "mypy-boto3-customer-profiles (>=1.37.0,<1.38.0)", "mypy-boto3-databrew (>=1.37.0,<1.38.0)", "mypy-boto3-dataexchange (>=1.37.0,<1.38.0)", "mypy-boto3-datapipeline (>=1.37.0,<1.38.0)", "mypy-boto3-datasync (>=1.37.0,<1.38.0)", "mypy-boto3-datazone (>=1.37.0,<1.38.0)", "mypy-boto3-dax (>=1.37.0,<1.38.0)", "mypy-boto3-deadline (>=1.37.0,<1.38.0)", "mypy-boto3-detective (>=1.37.0,<1.38.0)", "mypy-boto3-devicefarm (>=1.37.0,<1.38.0)", "mypy-boto3-devops-guru (>=1.37.0,<1.38.0)", "mypy-boto3-directconnect (>=1.37.0,<1.38.0)", "mypy-boto3-discovery (>=1.37.0,<1.38.0)", "mypy-boto3-dlm (>=1.37.0,<1.38.0)", "mypy-boto3-dms (>=1.37.0,<1.38.0)", "mypy-boto3-docdb (>=1.37.0,<1.38.0)", "mypy-boto3-docdb-elastic (>=1.37.0,<1.38.0)", "mypy-boto3-drs (>=1.37.0,<1.38.0)", "mypy-boto3-ds (>=1.37.0,<1.38.0)", "mypy-boto3-ds-data (>=1.37.0,<1.38.0)", "mypy-boto3-dsql (>=1.37.0,<1.38.0)", "mypy-boto3-dynamodb (>=1.37.0,<1.38.0)", "mypy-boto3-dynamodbstreams (>=1.37.0,<1.38.0)", "mypy-boto3-ebs (>=1.37.0,<1.38.0)", "mypy-boto3-ec2 (>=1.37.0,<1.38.0)", "mypy-boto3-ec2-instance-connect (>=1.37.0,<1.38.0)", "mypy-boto3-ecr (>=1.37.0,<1.38.0)", "mypy-boto3-ecr-public (>=1.37.0,<1.38.0)", "mypy-boto3-ecs (>=1.37.0,<1.38.0)", "mypy-boto3-efs (>=1.37.0,<1.38.0)", "mypy-boto3-eks (>=1.37.0,<1.38.0)", "mypy-boto3-eks-auth (>=1.37.0,<1.38.0)", "mypy-boto3-elasticache (>=1.37.0,<1.38.0)", "mypy-boto3-elasticbeanstalk (>=1.37.0,<1.38.0)", "mypy-boto3-elastictranscoder (>=1.37.0,<1.38.0)", "mypy-boto3-elb (>=1.37.0,<1.38.0)", "mypy-boto3-elbv2 (>=1.37.0,<1.38.0)", "mypy-boto3-emr (>=1.37.0,<1.38.0)", "mypy-boto3-emr-containers (>=1.37.0,<1.38.0)", "mypy-boto3-emr-serverless (>=1.37.0,<1.38.0)", "mypy-boto3-entityresolution (>=1.37.0,<1.38.0)", "mypy-boto3-es (>=1.37.0,<1.38.0)", "mypy-boto3-events (>=1.37.0,<1.38.0)", "mypy-boto3-evidently (>=1.37.0,<1.38.0)", "mypy-boto3-finspace (>=1.37.0,<1.38.0)", "mypy-boto3-finspace-data (>=1.37.0,<1.38.0)", "mypy-boto3-firehose (>=1.37.0,<1.38.0)", "mypy-boto3-fis (>=1.37.0,<1.38.0)", "mypy-boto3-fms (>=1.37.0,<1.38.0)", "mypy-boto3-forecast (>=1.37.0,<1.38.0)", "mypy-boto3-forecastquery (>=1.37.0,<1.38.0)", "mypy-boto3-frauddetector (>=1.37.0,<1.38.0)", "mypy-boto3-freetier (>=1.37.0,<1.38.0)", "mypy-boto3-fsx (>=1.37.0,<1.38.0)", "mypy-boto3-gamelift (>=1.37.0,<1.38.0)", "mypy-boto3-gameliftstreams (>=1.37.0,<1.38.0)", "mypy-boto3-geo-maps (>=1.37.0,<1.38.0)", "mypy-boto3-geo-places (>=1.37.0,<1.38.0)", "mypy-boto3-geo-routes (>=1.37.0,<1.38.0)", "mypy-boto3-glacier (>=1.37.0,<1.38.0)", "mypy-boto3-globalaccelerator (>=1.37.0,<1.38.0)", "mypy-boto3-glue (>=1.37.0,<1.38.0)", "mypy-boto3-grafana (>=1.37.0,<1.38.0)", "mypy-boto3-greengrass (>=1.37.0,<1.38.0)", "mypy-boto3-greengrassv2 (>=1.37.0,<1.38.0)", "mypy-boto3-groundstation (>=1.37.0,<1.38.0)", "mypy-boto3-guardduty (>=1.37.0,<1.38.0)", "mypy-boto3-health (>=1.37.0,<1.38.0)", "mypy-boto3-healthlake (>=1.37.0,<1.38.0)", "mypy-boto3-iam (>=1.37.0,<1.38.0)", "mypy-boto3-identitystore (>=1.37.0,<1.38.0)", "mypy-boto3-imagebuilder (>=1.37.0,<1.38.0)", "mypy-boto3-importexport (>=1.37.0,<1.38.0)", "mypy-boto3-inspector (>=1.37.0,<1.38.0)", "mypy-boto3-inspector-scan (>=1.37.0,<1.38.0)", "mypy-boto3-inspector2 (>=1.37.0,<1.38.0)", "mypy-boto3-internetmonitor (>=1.37.0,<1.38.0)", "mypy-boto3-invoicing (>=1.37.0,<1.38.0)", "mypy-boto3-iot (>=1.37.0,<1.38.0)", "mypy-boto3-iot-data (>=1.37.0,<1.38.0)", "mypy-boto3-iot-jobs-data (>=1.37.0,<1.38.0)", "mypy-boto3-iot-managed-integrations (>=1.37.0,<1.38.0)", "mypy-boto3-iotanalytics (>=1.37.0,<1.38.0)", "mypy-boto3-iotdeviceadvisor (>=1.37.0,<1.38.0)", "mypy-boto3-iotevents (>=1.37.0,<1.38.0)", "mypy-boto3-iotevents-data (>=1.37.0,<1.38.0)", "mypy-boto3-iotfleethub (>=1.37.0,<1.38.0)", "mypy-boto3-iotfleetwise (>=1.37.0,<1.38.0)", "mypy-boto3-iotsecuretunneling (>=1.37.0,<1.38.0)", "mypy-boto3-iotsitewise (>=1.37.0,<1.38.0)", "mypy-boto3-iotthingsgraph (>=1.37.0,<1.38.0)", "mypy-boto3-iottwinmaker (>=1.37.0,<1.38.0)", "mypy-boto3-iotwireless (>=1.37.0,<1.38.0)", "mypy-boto3-ivs (>=1.37.0,<1.38.0)", "mypy-boto3-ivs-realtime (>=1.37.0,<1.38.0)", "mypy-boto3-ivschat (>=1.37.0,<1.38.0)", "mypy-boto3-kafka (>=1.37.0,<1.38.0)", "mypy-boto3-kafkaconnect (>=1.37.0,<1.38.0)", "mypy-boto3-kendra (>=1.37.0,<1.38.0)", "mypy-boto3-kendra-ranking (>=1.37.0,<1.38.0)", "mypy-boto3-keyspaces (>=1.37.0,<1.38.0)", "mypy-boto3-kinesis (>=1.37.0,<1.38.0)", "mypy-boto3-kinesis-video-archived-media (>=1.37.0,<1.38.0)", "mypy-boto3-kinesis-video-media (>=1.37.0,<1.38.0)", "mypy-boto3-kinesis-video-signaling (>=1.37.0,<1.38.0)", "mypy-boto3-kinesis-video-webrtc-storage (>=1.37.0,<1.38.0)", "mypy-boto3-kinesisanalytics (>=1.37.0,<1.38.0)", "mypy-boto3-kinesisanalyticsv2 (>=1.37.0,<1.38.0)", "mypy-boto3-kinesisvideo (>=1.37.0,<1.38.0)", "mypy-boto3-kms (>=1.37.0,<1.38.0)", "mypy-boto3-lakeformation (>=1.37.0,<1.38.0)", "mypy-boto3-lambda (>=1.37.0,<1.38.0)", "mypy-boto3-launch-wizard (>=1.37.0,<1.38.0)", "mypy-boto3-lex-models (>=1.37.0,<1.38.0)", "mypy-boto3-lex-runtime (>=1.37.0,<1.38.0)", "mypy-boto3-lexv2-models (>=1.37.0,<1.38.0)", "mypy-boto3-lexv2-runtime (>=1.37.0,<1.38.0)", "mypy-boto3-license-manager (>=1.37.0,<1.38.0)", "mypy-boto3-license-manager-linux-subscriptions (>=1.37.0,<1.38.0)", "mypy-boto3-license-manager-user-subscriptions (>=1.37.0,<1.38.0)", "mypy-boto3-lightsail (>=1.37.0,<1.38.0)", "mypy-boto3-location (>=1.37.0,<1.38.0)", "mypy-boto3-logs (>=1.37.0,<1.38.0)", "mypy-boto3-lookoutequipment (>=1.37.0,<1.38.0)", "mypy-boto3-lookoutmetrics (>=1.37.0,<1.38.0)", "mypy-boto3-lookoutvision (>=1.37.0,<1.38.0)", "mypy-boto3-m2 (>=1.37.0,<1.38.0)", "mypy-boto3-machinelearning (>=1.37.0,<1.38.0)", "mypy-boto3-macie2 (>=1.37.0,<1.38.0)", "mypy-boto3-mailmanager (>=1.37.0,<1.38.0)", "mypy-boto3-managedblockchain (>=1.37.0,<1.38.0)", "mypy-boto3-managedblockchain-query (>=1.37.0,<1.38.0)", "mypy-boto3-marketplace-agreement (>=1.37.0,<1.38.0)", "mypy-boto3-marketplace-catalog (>=1.37.0,<1.38.0)", "mypy-boto3-marketplace-deployment (>=1.37.0,<1.38.0)", "mypy-boto3-marketplace-entitlement (>=1.37.0,<1.38.0)", "mypy-boto3-marketplace-reporting (>=1.37.0,<1.38.0)", "mypy-boto3-marketplacecommerceanalytics (>=1.37.0,<1.38.0)", "mypy-boto3-mediaconnect (>=1.37.0,<1.38.0)", "mypy-boto3-mediaconvert (>=1.37.0,<1.38.0)", "mypy-boto3-medialive (>=1.37.0,<1.38.0)", "mypy-boto3-mediapackage (>=1.37.0,<1.38.0)", "mypy-boto3-mediapackage-vod (>=1.37.0,<1.38.0)", "mypy-boto3-mediapackagev2 (>=1.37.0,<1.38.0)", "mypy-boto3-mediastore (>=1.37.0,<1.38.0)", "mypy-boto3-mediastore-data (>=1.37.0,<1.38.0)", "mypy-boto3-mediatailor (>=1.37.0,<1.38.0)", "mypy-boto3-medical-imaging (>=1.37.0,<1.38.0)", "mypy-boto3-memorydb (>=1.37.0,<1.38.0)", "mypy-boto3-meteringmarketplace (>=1.37.0,<1.38.0)", "mypy-boto3-mgh (>=1.37.0,<1.38.0)", "mypy-boto3-mgn (>=1.37.0,<1.38.0)", "mypy-boto3-migration-hub-refactor-spaces (>=1.37.0,<1.38.0)", "mypy-boto3-migrationhub-config (>=1.37.0,<1.38.0)", "mypy-boto3-migrationhuborchestrator (>=1.37.0,<1.38.0)", "mypy-boto3-migrationhubstrategy (>=1.37.0,<1.38.0)", "mypy-boto3-mq (>=1.37.0,<1.38.0)", "mypy-boto3-mturk (>=1.37.0,<1.38.0)", "mypy-boto3-mwaa (>=1.37.0,<1.38.0)", "mypy-boto3-neptune (>=1.37.0,<1.38.0)", "mypy-boto3-neptune-graph (>=1.37.0,<1.38.0)", "mypy-boto3-neptunedata (>=1.37.0,<1.38.0)", "mypy-boto3-network-firewall (>=1.37.0,<1.38.0)", "mypy-boto3-networkflowmonitor (>=1.37.0,<1.38.0)", "mypy-boto3-networkmanager (>=1.37.0,<1.38.0)", "mypy-boto3-networkmonitor (>=1.37.0,<1.38.0)", "mypy-boto3-notifications (>=1.37.0,<1.38.0)", "mypy-boto3-notificationscontacts (>=1.37.0,<1.38.0)", "mypy-boto3-oam (>=1.37.0,<1.38.0)", "mypy-boto3-observabilityadmin (>=1.37.0,<1.38.0)", "mypy-boto3-omics (>=1.37.0,<1.38.0)", "mypy-boto3-opensearch (>=1.37.0,<1.38.0)", "mypy-boto3-opensearchserverless (>=1.37.0,<1.38.0)", "mypy-boto3-opsworks (>=1.37.0,<1.38.0)", "mypy-boto3-opsworkscm (>=1.37.0,<1.38.0)", "mypy-boto3-organizations (>=1.37.0,<1.38.0)", "mypy-boto3-osis (>=1.37.0,<1.38.0)", "mypy-boto3-outposts (>=1.37.0,<1.38.0)", "mypy-boto3-panorama (>=1.37.0,<1.38.0)", "mypy-boto3-partnercentral-selling (>=1.37.0,<1.38.0)", "mypy-boto3-payment-cryptography (>=1.37.0,<1.38.0)", "mypy-boto3-payment-cryptography-data (>=1.37.0,<1.38.0)", "mypy-boto3-pca-connector-ad (>=1.37.0,<1.38.0)", "mypy-boto3-pca-connector-scep (>=1.37.0,<1.38.0)", "mypy-boto3-pcs (>=1.37.0,<1.38.0)", "mypy-boto3-personalize (>=1.37.0,<1.38.0)", "mypy-boto3-personalize-events (>=1.37.0,<1.38.0)", "mypy-boto3-personalize-runtime (>=1.37.0,<1.38.0)", "mypy-boto3-pi (>=1.37.0,<1.38.0)", "mypy-boto3-pinpoint (>=1.37.0,<1.38.0)", "mypy-boto3-pinpoint-email (>=1.37.0,<1.38.0)", "mypy-boto3-pinpoint-sms-voice (>=1.37.0,<1.38.0)", "mypy-boto3-pinpoint-sms-voice-v2 (>=1.37.0,<1.38.0)", "mypy-boto3-pipes (>=1.37.0,<1.38.0)", "mypy-boto3-polly (>=1.37.0,<1.38.0)", "mypy-boto3-pricing (>=1.37.0,<1.38.0)", "mypy-boto3-privatenetworks (>=1.37.0,<1.38.0)", "mypy-boto3-proton (>=1.37.0,<1.38.0)", "mypy-boto3-qapps (>=1.37.0,<1.38.0)", "mypy-boto3-qbusiness (>=1.37.0,<1.38.0)", "mypy-boto3-qconnect (>=1.37.0,<1.38.0)", "mypy-boto3-qldb (>=1.37.0,<1.38.0)", "mypy-boto3-qldb-session (>=1.37.0,<1.38.0)", "mypy-boto3-quicksight (>=1.37.0,<1.38.0)", "mypy-boto3-ram (>=1.37.0,<1.38.0)", "mypy-boto3-rbin (>=1.37.0,<1.38.0)", "mypy-boto3-rds (>=1.37.0,<1.38.0)", "mypy-boto3-rds-data (>=1.37.0,<1.38.0)", "mypy-boto3-redshift (>=1.37.0,<1.38.0)", "mypy-boto3-redshift-data (>=1.37.0,<1.38.0)", "mypy-boto3-redshift-serverless (>=1.37.0,<1.38.0)", "mypy-boto3-rekognition (>=1.37.0,<1.38.0)", "mypy-boto3-repostspace (>=1.37.0,<1.38.0)", "mypy-boto3-resiliencehub (>=1.37.0,<1.38.0)", "mypy-boto3-resource-explorer-2 (>=1.37.0,<1.38.0)", "mypy-boto3-resource-groups (>=1.37.0,<1.38.0)", "mypy-boto3-resourcegroupstaggingapi (>=1.37.0,<1.38.0)", "mypy-boto3-robomaker (>=1.37.0,<1.38.0)", "mypy-boto3-rolesanywhere (>=1.37.0,<1.38.0)", "mypy-boto3-route53 (>=1.37.0,<1.38.0)", "mypy-boto3-route53-recovery-cluster (>=1.37.0,<1.38.0)", "mypy-boto3-route53-recovery-control-config (>=1.37.0,<1.38.0)", "mypy-boto3-route53-recovery-readiness (>=1.37.0,<1.38.0)", "mypy-boto3-route53domains (>=1.37.0,<1.38.0)", "mypy-boto3-route53profiles (>=1.37.0,<1.38.0)", "mypy-boto3-route53resolver (>=1.37.0,<1.38.0)", "mypy-boto3-rum (>=1.37.0,<1.38.0)", "mypy-boto3-s3 (>=1.37.0,<1.38.0)", "mypy-boto3-s3control (>=1.37.0,<1.38.0)", "mypy-boto3-s3outposts (>=1.37.0,<1.38.0)", "mypy-boto3-s3tables (>=1.37.0,<1.38.0)", "mypy-boto3-sagemaker (>=1.37.0,<1.38.0)", "mypy-boto3-sagemaker-a2i-runtime (>=1.37.0,<1.38.0)", "mypy-boto3-sagemaker-edge (>=1.37.0,<1.38.0)", "mypy-boto3-sagemaker-featurestore-runtime (>=1.37.0,<1.38.0)", "mypy-boto3-sagemaker-geospatial (>=1.37.0,<1.38.0)", "mypy-boto3-sagemaker-metrics (>=1.37.0,<1.38.0)", "mypy-boto3-sagemaker-runtime (>=1.37.0,<1.38.0)", "mypy-boto3-savingsplans (>=1.37.0,<1.38.0)", "mypy-boto3-scheduler (>=1.37.0,<1.38.0)", "mypy-boto3-schemas (>=1.37.0,<1.38.0)", "mypy-boto3-sdb (>=1.37.0,<1.38.0)", "mypy-boto3-secretsmanager (>=1.37.0,<1.38.0)", "mypy-boto3-security-ir (>=1.37.0,<1.38.0)", "mypy-boto3-securityhub (>=1.37.0,<1.38.0)", "mypy-boto3-securitylake (>=1.37.0,<1.38.0)", "mypy-boto3-serverlessrepo (>=1.37.0,<1.38.0)", "mypy-boto3-service-quotas (>=1.37.0,<1.38.0)", "mypy-boto3-servicecatalog (>=1.37.0,<1.38.0)", "mypy-boto3-servicecatalog-appregistry (>=1.37.0,<1.38.0)", "mypy-boto3-servicediscovery (>=1.37.0,<1.38.0)", "mypy-boto3-ses (>=1.37.0,<1.38.0)", "mypy-boto3-sesv2 (>=1.37.0,<1.38.0)", "mypy-boto3-shield (>=1.37.0,<1.38.0)", "mypy-boto3-signer (>=1.37.0,<1.38.0)", "mypy-boto3-simspaceweaver (>=1.37.0,<1.38.0)", "mypy-boto3-sms (>=1.37.0,<1.38.0)", "mypy-boto3-sms-voice (>=1.37.0,<1.38.0)", "mypy-boto3-snow-device-management (>=1.37.0,<1.38.0)", "mypy-boto3-snowball (>=1.37.0,<1.38.0)", "mypy-boto3-sns (>=1.37.0,<1.38.0)", "mypy-boto3-socialmessaging (>=1.37.0,<1.38.0)", "mypy-boto3-sqs (>=1.37.0,<1.38.0)", "mypy-boto3-ssm (>=1.37.0,<1.38.0)", "mypy-boto3-ssm-contacts (>=1.37.0,<1.38.0)", "mypy-boto3-ssm-incidents (>=1.37.0,<1.38.0)", "mypy-boto3-ssm-quicksetup (>=1.37.0,<1.38.0)", "mypy-boto3-ssm-sap (>=1.37.0,<1.38.0)", "mypy-boto3-sso (>=1.37.0,<1.38.0)", "mypy-boto3-sso-admin (>=1.37.0,<1.38.0)", "mypy-boto3-sso-oidc (>=1.37.0,<1.38.0)", "mypy-boto3-stepfunctions (>=1.37.0,<1.38.0)", "mypy-boto3-storagegateway (>=1.37.0,<1.38.0)", "mypy-boto3-sts (>=1.37.0,<1.38.0)", "mypy-boto3-supplychain (>=1.37.0,<1.38.0)", "mypy-boto3-support (>=1.37.0,<1.38.0)", "mypy-boto3-support-app (>=1.37.0,<1.38.0)", "mypy-boto3-swf (>=1.37.0,<1.38.0)", "mypy-boto3-synthetics (>=1.37.0,<1.38.0)", "mypy-boto3-taxsettings (>=1.37.0,<1.38.0)", "mypy-boto3-textract (>=1.37.0,<1.38.0)", "mypy-boto3-timestream-influxdb (>=1.37.0,<1.38.0)", "mypy-boto3-timestream-query (>=1.37.0,<1.38.0)", "mypy-boto3-timestream-write (>=1.37.0,<1.38.0)", "mypy-boto3-tnb (>=1.37.0,<1.38.0)", "mypy-boto3-transcribe (>=1.37.0,<1.38.0)", "mypy-boto3-transfer (>=1.37.0,<1.38.0)", "mypy-boto3-translate (>=1.37.0,<1.38.0)", "mypy-boto3-trustedadvisor (>=1.37.0,<1.38.0)", "mypy-boto3-verifiedpermissions (>=1.37.0,<1.38.0)", "mypy-boto3-voice-id (>=1.37.0,<1.38.0)", "mypy-boto3-vpc-lattice (>=1.37.0,<1.38.0)", "mypy-boto3-waf (>=1.37.0,<1.38.0)", "mypy-boto3-waf-regional (>=1.37.0,<1.38.0)", "mypy-boto3-wafv2 (>=1.37.0,<1.38.0)", "mypy-boto3-wellarchitected (>=1.37.0,<1.38.0)", "mypy-boto3-wisdom (>=1.37.0,<1.38.0)", "mypy-boto3-workdocs (>=1.37.0,<1.38.0)", "mypy-boto3-workmail (>=1.37.0,<1.38.0)", "mypy-boto3-workmailmessageflow (>=1.37.0,<1.38.0)", "mypy-boto3-workspaces (>=1.37.0,<1.38.0)", "mypy-boto3-workspaces-thin-client (>=1.37.0,<1.38.0)", "mypy-boto3-workspaces-web (>=1.37.0,<1.38.0)", "mypy-boto3-xray (>=1.37.0,<1.38.0)"] -amp = ["mypy-boto3-amp (>=1.37.0,<1.38.0)"] -amplify = ["mypy-boto3-amplify (>=1.37.0,<1.38.0)"] -amplifybackend = ["mypy-boto3-amplifybackend (>=1.37.0,<1.38.0)"] -amplifyuibuilder = ["mypy-boto3-amplifyuibuilder (>=1.37.0,<1.38.0)"] -apigateway = ["mypy-boto3-apigateway (>=1.37.0,<1.38.0)"] -apigatewaymanagementapi = ["mypy-boto3-apigatewaymanagementapi (>=1.37.0,<1.38.0)"] -apigatewayv2 = ["mypy-boto3-apigatewayv2 (>=1.37.0,<1.38.0)"] -appconfig = ["mypy-boto3-appconfig (>=1.37.0,<1.38.0)"] -appconfigdata = ["mypy-boto3-appconfigdata (>=1.37.0,<1.38.0)"] -appfabric = ["mypy-boto3-appfabric (>=1.37.0,<1.38.0)"] -appflow = ["mypy-boto3-appflow (>=1.37.0,<1.38.0)"] -appintegrations = ["mypy-boto3-appintegrations (>=1.37.0,<1.38.0)"] -application-autoscaling = ["mypy-boto3-application-autoscaling (>=1.37.0,<1.38.0)"] -application-insights = ["mypy-boto3-application-insights (>=1.37.0,<1.38.0)"] -application-signals = ["mypy-boto3-application-signals (>=1.37.0,<1.38.0)"] -applicationcostprofiler = ["mypy-boto3-applicationcostprofiler (>=1.37.0,<1.38.0)"] -appmesh = ["mypy-boto3-appmesh (>=1.37.0,<1.38.0)"] -apprunner = ["mypy-boto3-apprunner (>=1.37.0,<1.38.0)"] -appstream = ["mypy-boto3-appstream (>=1.37.0,<1.38.0)"] -appsync = ["mypy-boto3-appsync (>=1.37.0,<1.38.0)"] -apptest = ["mypy-boto3-apptest (>=1.37.0,<1.38.0)"] -arc-zonal-shift = ["mypy-boto3-arc-zonal-shift (>=1.37.0,<1.38.0)"] -artifact = ["mypy-boto3-artifact (>=1.37.0,<1.38.0)"] -athena = ["mypy-boto3-athena (>=1.37.0,<1.38.0)"] -auditmanager = ["mypy-boto3-auditmanager (>=1.37.0,<1.38.0)"] -autoscaling = ["mypy-boto3-autoscaling (>=1.37.0,<1.38.0)"] -autoscaling-plans = ["mypy-boto3-autoscaling-plans (>=1.37.0,<1.38.0)"] -b2bi = ["mypy-boto3-b2bi (>=1.37.0,<1.38.0)"] -backup = ["mypy-boto3-backup (>=1.37.0,<1.38.0)"] -backup-gateway = ["mypy-boto3-backup-gateway (>=1.37.0,<1.38.0)"] -backupsearch = ["mypy-boto3-backupsearch (>=1.37.0,<1.38.0)"] -batch = ["mypy-boto3-batch (>=1.37.0,<1.38.0)"] -bcm-data-exports = ["mypy-boto3-bcm-data-exports (>=1.37.0,<1.38.0)"] -bcm-pricing-calculator = ["mypy-boto3-bcm-pricing-calculator (>=1.37.0,<1.38.0)"] -bedrock = ["mypy-boto3-bedrock (>=1.37.0,<1.38.0)"] -bedrock-agent = ["mypy-boto3-bedrock-agent (>=1.37.0,<1.38.0)"] -bedrock-agent-runtime = ["mypy-boto3-bedrock-agent-runtime (>=1.37.0,<1.38.0)"] -bedrock-data-automation = ["mypy-boto3-bedrock-data-automation (>=1.37.0,<1.38.0)"] -bedrock-data-automation-runtime = ["mypy-boto3-bedrock-data-automation-runtime (>=1.37.0,<1.38.0)"] -bedrock-runtime = ["mypy-boto3-bedrock-runtime (>=1.37.0,<1.38.0)"] -billing = ["mypy-boto3-billing (>=1.37.0,<1.38.0)"] -billingconductor = ["mypy-boto3-billingconductor (>=1.37.0,<1.38.0)"] -boto3 = ["boto3 (==1.37.29)"] -braket = ["mypy-boto3-braket (>=1.37.0,<1.38.0)"] -budgets = ["mypy-boto3-budgets (>=1.37.0,<1.38.0)"] -ce = ["mypy-boto3-ce (>=1.37.0,<1.38.0)"] -chatbot = ["mypy-boto3-chatbot (>=1.37.0,<1.38.0)"] -chime = ["mypy-boto3-chime (>=1.37.0,<1.38.0)"] -chime-sdk-identity = ["mypy-boto3-chime-sdk-identity (>=1.37.0,<1.38.0)"] -chime-sdk-media-pipelines = ["mypy-boto3-chime-sdk-media-pipelines (>=1.37.0,<1.38.0)"] -chime-sdk-meetings = ["mypy-boto3-chime-sdk-meetings (>=1.37.0,<1.38.0)"] -chime-sdk-messaging = ["mypy-boto3-chime-sdk-messaging (>=1.37.0,<1.38.0)"] -chime-sdk-voice = ["mypy-boto3-chime-sdk-voice (>=1.37.0,<1.38.0)"] -cleanrooms = ["mypy-boto3-cleanrooms (>=1.37.0,<1.38.0)"] -cleanroomsml = ["mypy-boto3-cleanroomsml (>=1.37.0,<1.38.0)"] -cloud9 = ["mypy-boto3-cloud9 (>=1.37.0,<1.38.0)"] -cloudcontrol = ["mypy-boto3-cloudcontrol (>=1.37.0,<1.38.0)"] -clouddirectory = ["mypy-boto3-clouddirectory (>=1.37.0,<1.38.0)"] -cloudformation = ["mypy-boto3-cloudformation (>=1.37.0,<1.38.0)"] -cloudfront = ["mypy-boto3-cloudfront (>=1.37.0,<1.38.0)"] -cloudfront-keyvaluestore = ["mypy-boto3-cloudfront-keyvaluestore (>=1.37.0,<1.38.0)"] -cloudhsm = ["mypy-boto3-cloudhsm (>=1.37.0,<1.38.0)"] -cloudhsmv2 = ["mypy-boto3-cloudhsmv2 (>=1.37.0,<1.38.0)"] -cloudsearch = ["mypy-boto3-cloudsearch (>=1.37.0,<1.38.0)"] -cloudsearchdomain = ["mypy-boto3-cloudsearchdomain (>=1.37.0,<1.38.0)"] -cloudtrail = ["mypy-boto3-cloudtrail (>=1.37.0,<1.38.0)"] -cloudtrail-data = ["mypy-boto3-cloudtrail-data (>=1.37.0,<1.38.0)"] -cloudwatch = ["mypy-boto3-cloudwatch (>=1.37.0,<1.38.0)"] -codeartifact = ["mypy-boto3-codeartifact (>=1.37.0,<1.38.0)"] -codebuild = ["mypy-boto3-codebuild (>=1.37.0,<1.38.0)"] -codecatalyst = ["mypy-boto3-codecatalyst (>=1.37.0,<1.38.0)"] -codecommit = ["mypy-boto3-codecommit (>=1.37.0,<1.38.0)"] -codeconnections = ["mypy-boto3-codeconnections (>=1.37.0,<1.38.0)"] -codedeploy = ["mypy-boto3-codedeploy (>=1.37.0,<1.38.0)"] -codeguru-reviewer = ["mypy-boto3-codeguru-reviewer (>=1.37.0,<1.38.0)"] -codeguru-security = ["mypy-boto3-codeguru-security (>=1.37.0,<1.38.0)"] -codeguruprofiler = ["mypy-boto3-codeguruprofiler (>=1.37.0,<1.38.0)"] -codepipeline = ["mypy-boto3-codepipeline (>=1.37.0,<1.38.0)"] -codestar-connections = ["mypy-boto3-codestar-connections (>=1.37.0,<1.38.0)"] -codestar-notifications = ["mypy-boto3-codestar-notifications (>=1.37.0,<1.38.0)"] -cognito-identity = ["mypy-boto3-cognito-identity (>=1.37.0,<1.38.0)"] -cognito-idp = ["mypy-boto3-cognito-idp (>=1.37.0,<1.38.0)"] -cognito-sync = ["mypy-boto3-cognito-sync (>=1.37.0,<1.38.0)"] -comprehend = ["mypy-boto3-comprehend (>=1.37.0,<1.38.0)"] -comprehendmedical = ["mypy-boto3-comprehendmedical (>=1.37.0,<1.38.0)"] -compute-optimizer = ["mypy-boto3-compute-optimizer (>=1.37.0,<1.38.0)"] -config = ["mypy-boto3-config (>=1.37.0,<1.38.0)"] -connect = ["mypy-boto3-connect (>=1.37.0,<1.38.0)"] -connect-contact-lens = ["mypy-boto3-connect-contact-lens (>=1.37.0,<1.38.0)"] -connectcampaigns = ["mypy-boto3-connectcampaigns (>=1.37.0,<1.38.0)"] -connectcampaignsv2 = ["mypy-boto3-connectcampaignsv2 (>=1.37.0,<1.38.0)"] -connectcases = ["mypy-boto3-connectcases (>=1.37.0,<1.38.0)"] -connectparticipant = ["mypy-boto3-connectparticipant (>=1.37.0,<1.38.0)"] -controlcatalog = ["mypy-boto3-controlcatalog (>=1.37.0,<1.38.0)"] -controltower = ["mypy-boto3-controltower (>=1.37.0,<1.38.0)"] -cost-optimization-hub = ["mypy-boto3-cost-optimization-hub (>=1.37.0,<1.38.0)"] -cur = ["mypy-boto3-cur (>=1.37.0,<1.38.0)"] -customer-profiles = ["mypy-boto3-customer-profiles (>=1.37.0,<1.38.0)"] -databrew = ["mypy-boto3-databrew (>=1.37.0,<1.38.0)"] -dataexchange = ["mypy-boto3-dataexchange (>=1.37.0,<1.38.0)"] -datapipeline = ["mypy-boto3-datapipeline (>=1.37.0,<1.38.0)"] -datasync = ["mypy-boto3-datasync (>=1.37.0,<1.38.0)"] -datazone = ["mypy-boto3-datazone (>=1.37.0,<1.38.0)"] -dax = ["mypy-boto3-dax (>=1.37.0,<1.38.0)"] -deadline = ["mypy-boto3-deadline (>=1.37.0,<1.38.0)"] -detective = ["mypy-boto3-detective (>=1.37.0,<1.38.0)"] -devicefarm = ["mypy-boto3-devicefarm (>=1.37.0,<1.38.0)"] -devops-guru = ["mypy-boto3-devops-guru (>=1.37.0,<1.38.0)"] -directconnect = ["mypy-boto3-directconnect (>=1.37.0,<1.38.0)"] -discovery = ["mypy-boto3-discovery (>=1.37.0,<1.38.0)"] -dlm = ["mypy-boto3-dlm (>=1.37.0,<1.38.0)"] -dms = ["mypy-boto3-dms (>=1.37.0,<1.38.0)"] -docdb = ["mypy-boto3-docdb (>=1.37.0,<1.38.0)"] -docdb-elastic = ["mypy-boto3-docdb-elastic (>=1.37.0,<1.38.0)"] -drs = ["mypy-boto3-drs (>=1.37.0,<1.38.0)"] -ds = ["mypy-boto3-ds (>=1.37.0,<1.38.0)"] -ds-data = ["mypy-boto3-ds-data (>=1.37.0,<1.38.0)"] -dsql = ["mypy-boto3-dsql (>=1.37.0,<1.38.0)"] -dynamodb = ["mypy-boto3-dynamodb (>=1.37.0,<1.38.0)"] -dynamodbstreams = ["mypy-boto3-dynamodbstreams (>=1.37.0,<1.38.0)"] -ebs = ["mypy-boto3-ebs (>=1.37.0,<1.38.0)"] -ec2 = ["mypy-boto3-ec2 (>=1.37.0,<1.38.0)"] -ec2-instance-connect = ["mypy-boto3-ec2-instance-connect (>=1.37.0,<1.38.0)"] -ecr = ["mypy-boto3-ecr (>=1.37.0,<1.38.0)"] -ecr-public = ["mypy-boto3-ecr-public (>=1.37.0,<1.38.0)"] -ecs = ["mypy-boto3-ecs (>=1.37.0,<1.38.0)"] -efs = ["mypy-boto3-efs (>=1.37.0,<1.38.0)"] -eks = ["mypy-boto3-eks (>=1.37.0,<1.38.0)"] -eks-auth = ["mypy-boto3-eks-auth (>=1.37.0,<1.38.0)"] -elasticache = ["mypy-boto3-elasticache (>=1.37.0,<1.38.0)"] -elasticbeanstalk = ["mypy-boto3-elasticbeanstalk (>=1.37.0,<1.38.0)"] -elastictranscoder = ["mypy-boto3-elastictranscoder (>=1.37.0,<1.38.0)"] -elb = ["mypy-boto3-elb (>=1.37.0,<1.38.0)"] -elbv2 = ["mypy-boto3-elbv2 (>=1.37.0,<1.38.0)"] -emr = ["mypy-boto3-emr (>=1.37.0,<1.38.0)"] -emr-containers = ["mypy-boto3-emr-containers (>=1.37.0,<1.38.0)"] -emr-serverless = ["mypy-boto3-emr-serverless (>=1.37.0,<1.38.0)"] -entityresolution = ["mypy-boto3-entityresolution (>=1.37.0,<1.38.0)"] -es = ["mypy-boto3-es (>=1.37.0,<1.38.0)"] -essential = ["mypy-boto3-cloudformation (>=1.37.0,<1.38.0)", "mypy-boto3-dynamodb (>=1.37.0,<1.38.0)", "mypy-boto3-ec2 (>=1.37.0,<1.38.0)", "mypy-boto3-lambda (>=1.37.0,<1.38.0)", "mypy-boto3-rds (>=1.37.0,<1.38.0)", "mypy-boto3-s3 (>=1.37.0,<1.38.0)", "mypy-boto3-sqs (>=1.37.0,<1.38.0)"] -events = ["mypy-boto3-events (>=1.37.0,<1.38.0)"] -evidently = ["mypy-boto3-evidently (>=1.37.0,<1.38.0)"] -finspace = ["mypy-boto3-finspace (>=1.37.0,<1.38.0)"] -finspace-data = ["mypy-boto3-finspace-data (>=1.37.0,<1.38.0)"] -firehose = ["mypy-boto3-firehose (>=1.37.0,<1.38.0)"] -fis = ["mypy-boto3-fis (>=1.37.0,<1.38.0)"] -fms = ["mypy-boto3-fms (>=1.37.0,<1.38.0)"] -forecast = ["mypy-boto3-forecast (>=1.37.0,<1.38.0)"] -forecastquery = ["mypy-boto3-forecastquery (>=1.37.0,<1.38.0)"] -frauddetector = ["mypy-boto3-frauddetector (>=1.37.0,<1.38.0)"] -freetier = ["mypy-boto3-freetier (>=1.37.0,<1.38.0)"] -fsx = ["mypy-boto3-fsx (>=1.37.0,<1.38.0)"] -full = ["boto3-stubs-full (>=1.37.0,<1.38.0)"] -gamelift = ["mypy-boto3-gamelift (>=1.37.0,<1.38.0)"] -gameliftstreams = ["mypy-boto3-gameliftstreams (>=1.37.0,<1.38.0)"] -geo-maps = ["mypy-boto3-geo-maps (>=1.37.0,<1.38.0)"] -geo-places = ["mypy-boto3-geo-places (>=1.37.0,<1.38.0)"] -geo-routes = ["mypy-boto3-geo-routes (>=1.37.0,<1.38.0)"] -glacier = ["mypy-boto3-glacier (>=1.37.0,<1.38.0)"] -globalaccelerator = ["mypy-boto3-globalaccelerator (>=1.37.0,<1.38.0)"] -glue = ["mypy-boto3-glue (>=1.37.0,<1.38.0)"] -grafana = ["mypy-boto3-grafana (>=1.37.0,<1.38.0)"] -greengrass = ["mypy-boto3-greengrass (>=1.37.0,<1.38.0)"] -greengrassv2 = ["mypy-boto3-greengrassv2 (>=1.37.0,<1.38.0)"] -groundstation = ["mypy-boto3-groundstation (>=1.37.0,<1.38.0)"] -guardduty = ["mypy-boto3-guardduty (>=1.37.0,<1.38.0)"] -health = ["mypy-boto3-health (>=1.37.0,<1.38.0)"] -healthlake = ["mypy-boto3-healthlake (>=1.37.0,<1.38.0)"] -iam = ["mypy-boto3-iam (>=1.37.0,<1.38.0)"] -identitystore = ["mypy-boto3-identitystore (>=1.37.0,<1.38.0)"] -imagebuilder = ["mypy-boto3-imagebuilder (>=1.37.0,<1.38.0)"] -importexport = ["mypy-boto3-importexport (>=1.37.0,<1.38.0)"] -inspector = ["mypy-boto3-inspector (>=1.37.0,<1.38.0)"] -inspector-scan = ["mypy-boto3-inspector-scan (>=1.37.0,<1.38.0)"] -inspector2 = ["mypy-boto3-inspector2 (>=1.37.0,<1.38.0)"] -internetmonitor = ["mypy-boto3-internetmonitor (>=1.37.0,<1.38.0)"] -invoicing = ["mypy-boto3-invoicing (>=1.37.0,<1.38.0)"] -iot = ["mypy-boto3-iot (>=1.37.0,<1.38.0)"] -iot-data = ["mypy-boto3-iot-data (>=1.37.0,<1.38.0)"] -iot-jobs-data = ["mypy-boto3-iot-jobs-data (>=1.37.0,<1.38.0)"] -iot-managed-integrations = ["mypy-boto3-iot-managed-integrations (>=1.37.0,<1.38.0)"] -iotanalytics = ["mypy-boto3-iotanalytics (>=1.37.0,<1.38.0)"] -iotdeviceadvisor = ["mypy-boto3-iotdeviceadvisor (>=1.37.0,<1.38.0)"] -iotevents = ["mypy-boto3-iotevents (>=1.37.0,<1.38.0)"] -iotevents-data = ["mypy-boto3-iotevents-data (>=1.37.0,<1.38.0)"] -iotfleethub = ["mypy-boto3-iotfleethub (>=1.37.0,<1.38.0)"] -iotfleetwise = ["mypy-boto3-iotfleetwise (>=1.37.0,<1.38.0)"] -iotsecuretunneling = ["mypy-boto3-iotsecuretunneling (>=1.37.0,<1.38.0)"] -iotsitewise = ["mypy-boto3-iotsitewise (>=1.37.0,<1.38.0)"] -iotthingsgraph = ["mypy-boto3-iotthingsgraph (>=1.37.0,<1.38.0)"] -iottwinmaker = ["mypy-boto3-iottwinmaker (>=1.37.0,<1.38.0)"] -iotwireless = ["mypy-boto3-iotwireless (>=1.37.0,<1.38.0)"] -ivs = ["mypy-boto3-ivs (>=1.37.0,<1.38.0)"] -ivs-realtime = ["mypy-boto3-ivs-realtime (>=1.37.0,<1.38.0)"] -ivschat = ["mypy-boto3-ivschat (>=1.37.0,<1.38.0)"] -kafka = ["mypy-boto3-kafka (>=1.37.0,<1.38.0)"] -kafkaconnect = ["mypy-boto3-kafkaconnect (>=1.37.0,<1.38.0)"] -kendra = ["mypy-boto3-kendra (>=1.37.0,<1.38.0)"] -kendra-ranking = ["mypy-boto3-kendra-ranking (>=1.37.0,<1.38.0)"] -keyspaces = ["mypy-boto3-keyspaces (>=1.37.0,<1.38.0)"] -kinesis = ["mypy-boto3-kinesis (>=1.37.0,<1.38.0)"] -kinesis-video-archived-media = ["mypy-boto3-kinesis-video-archived-media (>=1.37.0,<1.38.0)"] -kinesis-video-media = ["mypy-boto3-kinesis-video-media (>=1.37.0,<1.38.0)"] -kinesis-video-signaling = ["mypy-boto3-kinesis-video-signaling (>=1.37.0,<1.38.0)"] -kinesis-video-webrtc-storage = ["mypy-boto3-kinesis-video-webrtc-storage (>=1.37.0,<1.38.0)"] -kinesisanalytics = ["mypy-boto3-kinesisanalytics (>=1.37.0,<1.38.0)"] -kinesisanalyticsv2 = ["mypy-boto3-kinesisanalyticsv2 (>=1.37.0,<1.38.0)"] -kinesisvideo = ["mypy-boto3-kinesisvideo (>=1.37.0,<1.38.0)"] -kms = ["mypy-boto3-kms (>=1.37.0,<1.38.0)"] -lakeformation = ["mypy-boto3-lakeformation (>=1.37.0,<1.38.0)"] -lambda = ["mypy-boto3-lambda (>=1.37.0,<1.38.0)"] -launch-wizard = ["mypy-boto3-launch-wizard (>=1.37.0,<1.38.0)"] -lex-models = ["mypy-boto3-lex-models (>=1.37.0,<1.38.0)"] -lex-runtime = ["mypy-boto3-lex-runtime (>=1.37.0,<1.38.0)"] -lexv2-models = ["mypy-boto3-lexv2-models (>=1.37.0,<1.38.0)"] -lexv2-runtime = ["mypy-boto3-lexv2-runtime (>=1.37.0,<1.38.0)"] -license-manager = ["mypy-boto3-license-manager (>=1.37.0,<1.38.0)"] -license-manager-linux-subscriptions = ["mypy-boto3-license-manager-linux-subscriptions (>=1.37.0,<1.38.0)"] -license-manager-user-subscriptions = ["mypy-boto3-license-manager-user-subscriptions (>=1.37.0,<1.38.0)"] -lightsail = ["mypy-boto3-lightsail (>=1.37.0,<1.38.0)"] -location = ["mypy-boto3-location (>=1.37.0,<1.38.0)"] -logs = ["mypy-boto3-logs (>=1.37.0,<1.38.0)"] -lookoutequipment = ["mypy-boto3-lookoutequipment (>=1.37.0,<1.38.0)"] -lookoutmetrics = ["mypy-boto3-lookoutmetrics (>=1.37.0,<1.38.0)"] -lookoutvision = ["mypy-boto3-lookoutvision (>=1.37.0,<1.38.0)"] -m2 = ["mypy-boto3-m2 (>=1.37.0,<1.38.0)"] -machinelearning = ["mypy-boto3-machinelearning (>=1.37.0,<1.38.0)"] -macie2 = ["mypy-boto3-macie2 (>=1.37.0,<1.38.0)"] -mailmanager = ["mypy-boto3-mailmanager (>=1.37.0,<1.38.0)"] -managedblockchain = ["mypy-boto3-managedblockchain (>=1.37.0,<1.38.0)"] -managedblockchain-query = ["mypy-boto3-managedblockchain-query (>=1.37.0,<1.38.0)"] -marketplace-agreement = ["mypy-boto3-marketplace-agreement (>=1.37.0,<1.38.0)"] -marketplace-catalog = ["mypy-boto3-marketplace-catalog (>=1.37.0,<1.38.0)"] -marketplace-deployment = ["mypy-boto3-marketplace-deployment (>=1.37.0,<1.38.0)"] -marketplace-entitlement = ["mypy-boto3-marketplace-entitlement (>=1.37.0,<1.38.0)"] -marketplace-reporting = ["mypy-boto3-marketplace-reporting (>=1.37.0,<1.38.0)"] -marketplacecommerceanalytics = ["mypy-boto3-marketplacecommerceanalytics (>=1.37.0,<1.38.0)"] -mediaconnect = ["mypy-boto3-mediaconnect (>=1.37.0,<1.38.0)"] -mediaconvert = ["mypy-boto3-mediaconvert (>=1.37.0,<1.38.0)"] -medialive = ["mypy-boto3-medialive (>=1.37.0,<1.38.0)"] -mediapackage = ["mypy-boto3-mediapackage (>=1.37.0,<1.38.0)"] -mediapackage-vod = ["mypy-boto3-mediapackage-vod (>=1.37.0,<1.38.0)"] -mediapackagev2 = ["mypy-boto3-mediapackagev2 (>=1.37.0,<1.38.0)"] -mediastore = ["mypy-boto3-mediastore (>=1.37.0,<1.38.0)"] -mediastore-data = ["mypy-boto3-mediastore-data (>=1.37.0,<1.38.0)"] -mediatailor = ["mypy-boto3-mediatailor (>=1.37.0,<1.38.0)"] -medical-imaging = ["mypy-boto3-medical-imaging (>=1.37.0,<1.38.0)"] -memorydb = ["mypy-boto3-memorydb (>=1.37.0,<1.38.0)"] -meteringmarketplace = ["mypy-boto3-meteringmarketplace (>=1.37.0,<1.38.0)"] -mgh = ["mypy-boto3-mgh (>=1.37.0,<1.38.0)"] -mgn = ["mypy-boto3-mgn (>=1.37.0,<1.38.0)"] -migration-hub-refactor-spaces = ["mypy-boto3-migration-hub-refactor-spaces (>=1.37.0,<1.38.0)"] -migrationhub-config = ["mypy-boto3-migrationhub-config (>=1.37.0,<1.38.0)"] -migrationhuborchestrator = ["mypy-boto3-migrationhuborchestrator (>=1.37.0,<1.38.0)"] -migrationhubstrategy = ["mypy-boto3-migrationhubstrategy (>=1.37.0,<1.38.0)"] -mq = ["mypy-boto3-mq (>=1.37.0,<1.38.0)"] -mturk = ["mypy-boto3-mturk (>=1.37.0,<1.38.0)"] -mwaa = ["mypy-boto3-mwaa (>=1.37.0,<1.38.0)"] -neptune = ["mypy-boto3-neptune (>=1.37.0,<1.38.0)"] -neptune-graph = ["mypy-boto3-neptune-graph (>=1.37.0,<1.38.0)"] -neptunedata = ["mypy-boto3-neptunedata (>=1.37.0,<1.38.0)"] -network-firewall = ["mypy-boto3-network-firewall (>=1.37.0,<1.38.0)"] -networkflowmonitor = ["mypy-boto3-networkflowmonitor (>=1.37.0,<1.38.0)"] -networkmanager = ["mypy-boto3-networkmanager (>=1.37.0,<1.38.0)"] -networkmonitor = ["mypy-boto3-networkmonitor (>=1.37.0,<1.38.0)"] -notifications = ["mypy-boto3-notifications (>=1.37.0,<1.38.0)"] -notificationscontacts = ["mypy-boto3-notificationscontacts (>=1.37.0,<1.38.0)"] -oam = ["mypy-boto3-oam (>=1.37.0,<1.38.0)"] -observabilityadmin = ["mypy-boto3-observabilityadmin (>=1.37.0,<1.38.0)"] -omics = ["mypy-boto3-omics (>=1.37.0,<1.38.0)"] -opensearch = ["mypy-boto3-opensearch (>=1.37.0,<1.38.0)"] -opensearchserverless = ["mypy-boto3-opensearchserverless (>=1.37.0,<1.38.0)"] -opsworks = ["mypy-boto3-opsworks (>=1.37.0,<1.38.0)"] -opsworkscm = ["mypy-boto3-opsworkscm (>=1.37.0,<1.38.0)"] -organizations = ["mypy-boto3-organizations (>=1.37.0,<1.38.0)"] -osis = ["mypy-boto3-osis (>=1.37.0,<1.38.0)"] -outposts = ["mypy-boto3-outposts (>=1.37.0,<1.38.0)"] -panorama = ["mypy-boto3-panorama (>=1.37.0,<1.38.0)"] -partnercentral-selling = ["mypy-boto3-partnercentral-selling (>=1.37.0,<1.38.0)"] -payment-cryptography = ["mypy-boto3-payment-cryptography (>=1.37.0,<1.38.0)"] -payment-cryptography-data = ["mypy-boto3-payment-cryptography-data (>=1.37.0,<1.38.0)"] -pca-connector-ad = ["mypy-boto3-pca-connector-ad (>=1.37.0,<1.38.0)"] -pca-connector-scep = ["mypy-boto3-pca-connector-scep (>=1.37.0,<1.38.0)"] -pcs = ["mypy-boto3-pcs (>=1.37.0,<1.38.0)"] -personalize = ["mypy-boto3-personalize (>=1.37.0,<1.38.0)"] -personalize-events = ["mypy-boto3-personalize-events (>=1.37.0,<1.38.0)"] -personalize-runtime = ["mypy-boto3-personalize-runtime (>=1.37.0,<1.38.0)"] -pi = ["mypy-boto3-pi (>=1.37.0,<1.38.0)"] -pinpoint = ["mypy-boto3-pinpoint (>=1.37.0,<1.38.0)"] -pinpoint-email = ["mypy-boto3-pinpoint-email (>=1.37.0,<1.38.0)"] -pinpoint-sms-voice = ["mypy-boto3-pinpoint-sms-voice (>=1.37.0,<1.38.0)"] -pinpoint-sms-voice-v2 = ["mypy-boto3-pinpoint-sms-voice-v2 (>=1.37.0,<1.38.0)"] -pipes = ["mypy-boto3-pipes (>=1.37.0,<1.38.0)"] -polly = ["mypy-boto3-polly (>=1.37.0,<1.38.0)"] -pricing = ["mypy-boto3-pricing (>=1.37.0,<1.38.0)"] -privatenetworks = ["mypy-boto3-privatenetworks (>=1.37.0,<1.38.0)"] -proton = ["mypy-boto3-proton (>=1.37.0,<1.38.0)"] -qapps = ["mypy-boto3-qapps (>=1.37.0,<1.38.0)"] -qbusiness = ["mypy-boto3-qbusiness (>=1.37.0,<1.38.0)"] -qconnect = ["mypy-boto3-qconnect (>=1.37.0,<1.38.0)"] -qldb = ["mypy-boto3-qldb (>=1.37.0,<1.38.0)"] -qldb-session = ["mypy-boto3-qldb-session (>=1.37.0,<1.38.0)"] -quicksight = ["mypy-boto3-quicksight (>=1.37.0,<1.38.0)"] -ram = ["mypy-boto3-ram (>=1.37.0,<1.38.0)"] -rbin = ["mypy-boto3-rbin (>=1.37.0,<1.38.0)"] -rds = ["mypy-boto3-rds (>=1.37.0,<1.38.0)"] -rds-data = ["mypy-boto3-rds-data (>=1.37.0,<1.38.0)"] -redshift = ["mypy-boto3-redshift (>=1.37.0,<1.38.0)"] -redshift-data = ["mypy-boto3-redshift-data (>=1.37.0,<1.38.0)"] -redshift-serverless = ["mypy-boto3-redshift-serverless (>=1.37.0,<1.38.0)"] -rekognition = ["mypy-boto3-rekognition (>=1.37.0,<1.38.0)"] -repostspace = ["mypy-boto3-repostspace (>=1.37.0,<1.38.0)"] -resiliencehub = ["mypy-boto3-resiliencehub (>=1.37.0,<1.38.0)"] -resource-explorer-2 = ["mypy-boto3-resource-explorer-2 (>=1.37.0,<1.38.0)"] -resource-groups = ["mypy-boto3-resource-groups (>=1.37.0,<1.38.0)"] -resourcegroupstaggingapi = ["mypy-boto3-resourcegroupstaggingapi (>=1.37.0,<1.38.0)"] -robomaker = ["mypy-boto3-robomaker (>=1.37.0,<1.38.0)"] -rolesanywhere = ["mypy-boto3-rolesanywhere (>=1.37.0,<1.38.0)"] -route53 = ["mypy-boto3-route53 (>=1.37.0,<1.38.0)"] -route53-recovery-cluster = ["mypy-boto3-route53-recovery-cluster (>=1.37.0,<1.38.0)"] -route53-recovery-control-config = ["mypy-boto3-route53-recovery-control-config (>=1.37.0,<1.38.0)"] -route53-recovery-readiness = ["mypy-boto3-route53-recovery-readiness (>=1.37.0,<1.38.0)"] -route53domains = ["mypy-boto3-route53domains (>=1.37.0,<1.38.0)"] -route53profiles = ["mypy-boto3-route53profiles (>=1.37.0,<1.38.0)"] -route53resolver = ["mypy-boto3-route53resolver (>=1.37.0,<1.38.0)"] -rum = ["mypy-boto3-rum (>=1.37.0,<1.38.0)"] -s3 = ["mypy-boto3-s3 (>=1.37.0,<1.38.0)"] -s3control = ["mypy-boto3-s3control (>=1.37.0,<1.38.0)"] -s3outposts = ["mypy-boto3-s3outposts (>=1.37.0,<1.38.0)"] -s3tables = ["mypy-boto3-s3tables (>=1.37.0,<1.38.0)"] -sagemaker = ["mypy-boto3-sagemaker (>=1.37.0,<1.38.0)"] -sagemaker-a2i-runtime = ["mypy-boto3-sagemaker-a2i-runtime (>=1.37.0,<1.38.0)"] -sagemaker-edge = ["mypy-boto3-sagemaker-edge (>=1.37.0,<1.38.0)"] -sagemaker-featurestore-runtime = ["mypy-boto3-sagemaker-featurestore-runtime (>=1.37.0,<1.38.0)"] -sagemaker-geospatial = ["mypy-boto3-sagemaker-geospatial (>=1.37.0,<1.38.0)"] -sagemaker-metrics = ["mypy-boto3-sagemaker-metrics (>=1.37.0,<1.38.0)"] -sagemaker-runtime = ["mypy-boto3-sagemaker-runtime (>=1.37.0,<1.38.0)"] -savingsplans = ["mypy-boto3-savingsplans (>=1.37.0,<1.38.0)"] -scheduler = ["mypy-boto3-scheduler (>=1.37.0,<1.38.0)"] -schemas = ["mypy-boto3-schemas (>=1.37.0,<1.38.0)"] -sdb = ["mypy-boto3-sdb (>=1.37.0,<1.38.0)"] -secretsmanager = ["mypy-boto3-secretsmanager (>=1.37.0,<1.38.0)"] -security-ir = ["mypy-boto3-security-ir (>=1.37.0,<1.38.0)"] -securityhub = ["mypy-boto3-securityhub (>=1.37.0,<1.38.0)"] -securitylake = ["mypy-boto3-securitylake (>=1.37.0,<1.38.0)"] -serverlessrepo = ["mypy-boto3-serverlessrepo (>=1.37.0,<1.38.0)"] -service-quotas = ["mypy-boto3-service-quotas (>=1.37.0,<1.38.0)"] -servicecatalog = ["mypy-boto3-servicecatalog (>=1.37.0,<1.38.0)"] -servicecatalog-appregistry = ["mypy-boto3-servicecatalog-appregistry (>=1.37.0,<1.38.0)"] -servicediscovery = ["mypy-boto3-servicediscovery (>=1.37.0,<1.38.0)"] -ses = ["mypy-boto3-ses (>=1.37.0,<1.38.0)"] -sesv2 = ["mypy-boto3-sesv2 (>=1.37.0,<1.38.0)"] -shield = ["mypy-boto3-shield (>=1.37.0,<1.38.0)"] -signer = ["mypy-boto3-signer (>=1.37.0,<1.38.0)"] -simspaceweaver = ["mypy-boto3-simspaceweaver (>=1.37.0,<1.38.0)"] -sms = ["mypy-boto3-sms (>=1.37.0,<1.38.0)"] -sms-voice = ["mypy-boto3-sms-voice (>=1.37.0,<1.38.0)"] -snow-device-management = ["mypy-boto3-snow-device-management (>=1.37.0,<1.38.0)"] -snowball = ["mypy-boto3-snowball (>=1.37.0,<1.38.0)"] -sns = ["mypy-boto3-sns (>=1.37.0,<1.38.0)"] -socialmessaging = ["mypy-boto3-socialmessaging (>=1.37.0,<1.38.0)"] -sqs = ["mypy-boto3-sqs (>=1.37.0,<1.38.0)"] -ssm = ["mypy-boto3-ssm (>=1.37.0,<1.38.0)"] -ssm-contacts = ["mypy-boto3-ssm-contacts (>=1.37.0,<1.38.0)"] -ssm-incidents = ["mypy-boto3-ssm-incidents (>=1.37.0,<1.38.0)"] -ssm-quicksetup = ["mypy-boto3-ssm-quicksetup (>=1.37.0,<1.38.0)"] -ssm-sap = ["mypy-boto3-ssm-sap (>=1.37.0,<1.38.0)"] -sso = ["mypy-boto3-sso (>=1.37.0,<1.38.0)"] -sso-admin = ["mypy-boto3-sso-admin (>=1.37.0,<1.38.0)"] -sso-oidc = ["mypy-boto3-sso-oidc (>=1.37.0,<1.38.0)"] -stepfunctions = ["mypy-boto3-stepfunctions (>=1.37.0,<1.38.0)"] -storagegateway = ["mypy-boto3-storagegateway (>=1.37.0,<1.38.0)"] -sts = ["mypy-boto3-sts (>=1.37.0,<1.38.0)"] -supplychain = ["mypy-boto3-supplychain (>=1.37.0,<1.38.0)"] -support = ["mypy-boto3-support (>=1.37.0,<1.38.0)"] -support-app = ["mypy-boto3-support-app (>=1.37.0,<1.38.0)"] -swf = ["mypy-boto3-swf (>=1.37.0,<1.38.0)"] -synthetics = ["mypy-boto3-synthetics (>=1.37.0,<1.38.0)"] -taxsettings = ["mypy-boto3-taxsettings (>=1.37.0,<1.38.0)"] -textract = ["mypy-boto3-textract (>=1.37.0,<1.38.0)"] -timestream-influxdb = ["mypy-boto3-timestream-influxdb (>=1.37.0,<1.38.0)"] -timestream-query = ["mypy-boto3-timestream-query (>=1.37.0,<1.38.0)"] -timestream-write = ["mypy-boto3-timestream-write (>=1.37.0,<1.38.0)"] -tnb = ["mypy-boto3-tnb (>=1.37.0,<1.38.0)"] -transcribe = ["mypy-boto3-transcribe (>=1.37.0,<1.38.0)"] -transfer = ["mypy-boto3-transfer (>=1.37.0,<1.38.0)"] -translate = ["mypy-boto3-translate (>=1.37.0,<1.38.0)"] -trustedadvisor = ["mypy-boto3-trustedadvisor (>=1.37.0,<1.38.0)"] -verifiedpermissions = ["mypy-boto3-verifiedpermissions (>=1.37.0,<1.38.0)"] -voice-id = ["mypy-boto3-voice-id (>=1.37.0,<1.38.0)"] -vpc-lattice = ["mypy-boto3-vpc-lattice (>=1.37.0,<1.38.0)"] -waf = ["mypy-boto3-waf (>=1.37.0,<1.38.0)"] -waf-regional = ["mypy-boto3-waf-regional (>=1.37.0,<1.38.0)"] -wafv2 = ["mypy-boto3-wafv2 (>=1.37.0,<1.38.0)"] -wellarchitected = ["mypy-boto3-wellarchitected (>=1.37.0,<1.38.0)"] -wisdom = ["mypy-boto3-wisdom (>=1.37.0,<1.38.0)"] -workdocs = ["mypy-boto3-workdocs (>=1.37.0,<1.38.0)"] -workmail = ["mypy-boto3-workmail (>=1.37.0,<1.38.0)"] -workmailmessageflow = ["mypy-boto3-workmailmessageflow (>=1.37.0,<1.38.0)"] -workspaces = ["mypy-boto3-workspaces (>=1.37.0,<1.38.0)"] -workspaces-thin-client = ["mypy-boto3-workspaces-thin-client (>=1.37.0,<1.38.0)"] -workspaces-web = ["mypy-boto3-workspaces-web (>=1.37.0,<1.38.0)"] -xray = ["mypy-boto3-xray (>=1.37.0,<1.38.0)"] +accessanalyzer = ["mypy-boto3-accessanalyzer (>=1.40.0,<1.41.0)"] +account = ["mypy-boto3-account (>=1.40.0,<1.41.0)"] +acm = ["mypy-boto3-acm (>=1.40.0,<1.41.0)"] +acm-pca = ["mypy-boto3-acm-pca (>=1.40.0,<1.41.0)"] +aiops = ["mypy-boto3-aiops (>=1.40.0,<1.41.0)"] +all = ["mypy-boto3-accessanalyzer (>=1.40.0,<1.41.0)", "mypy-boto3-account (>=1.40.0,<1.41.0)", "mypy-boto3-acm (>=1.40.0,<1.41.0)", "mypy-boto3-acm-pca (>=1.40.0,<1.41.0)", "mypy-boto3-aiops (>=1.40.0,<1.41.0)", "mypy-boto3-amp (>=1.40.0,<1.41.0)", "mypy-boto3-amplify (>=1.40.0,<1.41.0)", "mypy-boto3-amplifybackend (>=1.40.0,<1.41.0)", "mypy-boto3-amplifyuibuilder (>=1.40.0,<1.41.0)", "mypy-boto3-apigateway (>=1.40.0,<1.41.0)", "mypy-boto3-apigatewaymanagementapi (>=1.40.0,<1.41.0)", "mypy-boto3-apigatewayv2 (>=1.40.0,<1.41.0)", "mypy-boto3-appconfig (>=1.40.0,<1.41.0)", "mypy-boto3-appconfigdata (>=1.40.0,<1.41.0)", "mypy-boto3-appfabric (>=1.40.0,<1.41.0)", "mypy-boto3-appflow (>=1.40.0,<1.41.0)", "mypy-boto3-appintegrations (>=1.40.0,<1.41.0)", "mypy-boto3-application-autoscaling (>=1.40.0,<1.41.0)", "mypy-boto3-application-insights (>=1.40.0,<1.41.0)", "mypy-boto3-application-signals (>=1.40.0,<1.41.0)", "mypy-boto3-applicationcostprofiler (>=1.40.0,<1.41.0)", "mypy-boto3-appmesh (>=1.40.0,<1.41.0)", "mypy-boto3-apprunner (>=1.40.0,<1.41.0)", "mypy-boto3-appstream (>=1.40.0,<1.41.0)", "mypy-boto3-appsync (>=1.40.0,<1.41.0)", "mypy-boto3-apptest (>=1.40.0,<1.41.0)", "mypy-boto3-arc-region-switch (>=1.40.0,<1.41.0)", "mypy-boto3-arc-zonal-shift (>=1.40.0,<1.41.0)", "mypy-boto3-artifact (>=1.40.0,<1.41.0)", "mypy-boto3-athena (>=1.40.0,<1.41.0)", "mypy-boto3-auditmanager (>=1.40.0,<1.41.0)", "mypy-boto3-autoscaling (>=1.40.0,<1.41.0)", "mypy-boto3-autoscaling-plans (>=1.40.0,<1.41.0)", "mypy-boto3-b2bi (>=1.40.0,<1.41.0)", "mypy-boto3-backup (>=1.40.0,<1.41.0)", "mypy-boto3-backup-gateway (>=1.40.0,<1.41.0)", "mypy-boto3-backupsearch (>=1.40.0,<1.41.0)", "mypy-boto3-batch (>=1.40.0,<1.41.0)", "mypy-boto3-bcm-dashboards (>=1.40.0,<1.41.0)", "mypy-boto3-bcm-data-exports (>=1.40.0,<1.41.0)", "mypy-boto3-bcm-pricing-calculator (>=1.40.0,<1.41.0)", "mypy-boto3-bcm-recommended-actions (>=1.40.0,<1.41.0)", "mypy-boto3-bedrock (>=1.40.0,<1.41.0)", "mypy-boto3-bedrock-agent (>=1.40.0,<1.41.0)", "mypy-boto3-bedrock-agent-runtime (>=1.40.0,<1.41.0)", "mypy-boto3-bedrock-agentcore (>=1.40.0,<1.41.0)", "mypy-boto3-bedrock-agentcore-control (>=1.40.0,<1.41.0)", "mypy-boto3-bedrock-data-automation (>=1.40.0,<1.41.0)", "mypy-boto3-bedrock-data-automation-runtime (>=1.40.0,<1.41.0)", "mypy-boto3-bedrock-runtime (>=1.40.0,<1.41.0)", "mypy-boto3-billing (>=1.40.0,<1.41.0)", "mypy-boto3-billingconductor (>=1.40.0,<1.41.0)", "mypy-boto3-braket (>=1.40.0,<1.41.0)", "mypy-boto3-budgets (>=1.40.0,<1.41.0)", "mypy-boto3-ce (>=1.40.0,<1.41.0)", "mypy-boto3-chatbot (>=1.40.0,<1.41.0)", "mypy-boto3-chime (>=1.40.0,<1.41.0)", "mypy-boto3-chime-sdk-identity (>=1.40.0,<1.41.0)", "mypy-boto3-chime-sdk-media-pipelines (>=1.40.0,<1.41.0)", "mypy-boto3-chime-sdk-meetings (>=1.40.0,<1.41.0)", "mypy-boto3-chime-sdk-messaging (>=1.40.0,<1.41.0)", "mypy-boto3-chime-sdk-voice (>=1.40.0,<1.41.0)", "mypy-boto3-cleanrooms (>=1.40.0,<1.41.0)", "mypy-boto3-cleanroomsml (>=1.40.0,<1.41.0)", "mypy-boto3-cloud9 (>=1.40.0,<1.41.0)", "mypy-boto3-cloudcontrol (>=1.40.0,<1.41.0)", "mypy-boto3-clouddirectory (>=1.40.0,<1.41.0)", "mypy-boto3-cloudformation (>=1.40.0,<1.41.0)", "mypy-boto3-cloudfront (>=1.40.0,<1.41.0)", "mypy-boto3-cloudfront-keyvaluestore (>=1.40.0,<1.41.0)", "mypy-boto3-cloudhsm (>=1.40.0,<1.41.0)", "mypy-boto3-cloudhsmv2 (>=1.40.0,<1.41.0)", "mypy-boto3-cloudsearch (>=1.40.0,<1.41.0)", "mypy-boto3-cloudsearchdomain (>=1.40.0,<1.41.0)", "mypy-boto3-cloudtrail (>=1.40.0,<1.41.0)", "mypy-boto3-cloudtrail-data (>=1.40.0,<1.41.0)", "mypy-boto3-cloudwatch (>=1.40.0,<1.41.0)", "mypy-boto3-codeartifact (>=1.40.0,<1.41.0)", "mypy-boto3-codebuild (>=1.40.0,<1.41.0)", "mypy-boto3-codecatalyst (>=1.40.0,<1.41.0)", "mypy-boto3-codecommit (>=1.40.0,<1.41.0)", "mypy-boto3-codeconnections (>=1.40.0,<1.41.0)", "mypy-boto3-codedeploy (>=1.40.0,<1.41.0)", "mypy-boto3-codeguru-reviewer (>=1.40.0,<1.41.0)", "mypy-boto3-codeguru-security (>=1.40.0,<1.41.0)", "mypy-boto3-codeguruprofiler (>=1.40.0,<1.41.0)", "mypy-boto3-codepipeline (>=1.40.0,<1.41.0)", "mypy-boto3-codestar-connections (>=1.40.0,<1.41.0)", "mypy-boto3-codestar-notifications (>=1.40.0,<1.41.0)", "mypy-boto3-cognito-identity (>=1.40.0,<1.41.0)", "mypy-boto3-cognito-idp (>=1.40.0,<1.41.0)", "mypy-boto3-cognito-sync (>=1.40.0,<1.41.0)", "mypy-boto3-comprehend (>=1.40.0,<1.41.0)", "mypy-boto3-comprehendmedical (>=1.40.0,<1.41.0)", "mypy-boto3-compute-optimizer (>=1.40.0,<1.41.0)", "mypy-boto3-config (>=1.40.0,<1.41.0)", "mypy-boto3-connect (>=1.40.0,<1.41.0)", "mypy-boto3-connect-contact-lens (>=1.40.0,<1.41.0)", "mypy-boto3-connectcampaigns (>=1.40.0,<1.41.0)", "mypy-boto3-connectcampaignsv2 (>=1.40.0,<1.41.0)", "mypy-boto3-connectcases (>=1.40.0,<1.41.0)", "mypy-boto3-connectparticipant (>=1.40.0,<1.41.0)", "mypy-boto3-controlcatalog (>=1.40.0,<1.41.0)", "mypy-boto3-controltower (>=1.40.0,<1.41.0)", "mypy-boto3-cost-optimization-hub (>=1.40.0,<1.41.0)", "mypy-boto3-cur (>=1.40.0,<1.41.0)", "mypy-boto3-customer-profiles (>=1.40.0,<1.41.0)", "mypy-boto3-databrew (>=1.40.0,<1.41.0)", "mypy-boto3-dataexchange (>=1.40.0,<1.41.0)", "mypy-boto3-datapipeline (>=1.40.0,<1.41.0)", "mypy-boto3-datasync (>=1.40.0,<1.41.0)", "mypy-boto3-datazone (>=1.40.0,<1.41.0)", "mypy-boto3-dax (>=1.40.0,<1.41.0)", "mypy-boto3-deadline (>=1.40.0,<1.41.0)", "mypy-boto3-detective (>=1.40.0,<1.41.0)", "mypy-boto3-devicefarm (>=1.40.0,<1.41.0)", "mypy-boto3-devops-guru (>=1.40.0,<1.41.0)", "mypy-boto3-directconnect (>=1.40.0,<1.41.0)", "mypy-boto3-discovery (>=1.40.0,<1.41.0)", "mypy-boto3-dlm (>=1.40.0,<1.41.0)", "mypy-boto3-dms (>=1.40.0,<1.41.0)", "mypy-boto3-docdb (>=1.40.0,<1.41.0)", "mypy-boto3-docdb-elastic (>=1.40.0,<1.41.0)", "mypy-boto3-drs (>=1.40.0,<1.41.0)", "mypy-boto3-ds (>=1.40.0,<1.41.0)", "mypy-boto3-ds-data (>=1.40.0,<1.41.0)", "mypy-boto3-dsql (>=1.40.0,<1.41.0)", "mypy-boto3-dynamodb (>=1.40.0,<1.41.0)", "mypy-boto3-dynamodbstreams (>=1.40.0,<1.41.0)", "mypy-boto3-ebs (>=1.40.0,<1.41.0)", "mypy-boto3-ec2 (>=1.40.0,<1.41.0)", "mypy-boto3-ec2-instance-connect (>=1.40.0,<1.41.0)", "mypy-boto3-ecr (>=1.40.0,<1.41.0)", "mypy-boto3-ecr-public (>=1.40.0,<1.41.0)", "mypy-boto3-ecs (>=1.40.0,<1.41.0)", "mypy-boto3-efs (>=1.40.0,<1.41.0)", "mypy-boto3-eks (>=1.40.0,<1.41.0)", "mypy-boto3-eks-auth (>=1.40.0,<1.41.0)", "mypy-boto3-elasticache (>=1.40.0,<1.41.0)", "mypy-boto3-elasticbeanstalk (>=1.40.0,<1.41.0)", "mypy-boto3-elastictranscoder (>=1.40.0,<1.41.0)", "mypy-boto3-elb (>=1.40.0,<1.41.0)", "mypy-boto3-elbv2 (>=1.40.0,<1.41.0)", "mypy-boto3-emr (>=1.40.0,<1.41.0)", "mypy-boto3-emr-containers (>=1.40.0,<1.41.0)", "mypy-boto3-emr-serverless (>=1.40.0,<1.41.0)", "mypy-boto3-entityresolution (>=1.40.0,<1.41.0)", "mypy-boto3-es (>=1.40.0,<1.41.0)", "mypy-boto3-events (>=1.40.0,<1.41.0)", "mypy-boto3-evidently (>=1.40.0,<1.41.0)", "mypy-boto3-evs (>=1.40.0,<1.41.0)", "mypy-boto3-finspace (>=1.40.0,<1.41.0)", "mypy-boto3-finspace-data (>=1.40.0,<1.41.0)", "mypy-boto3-firehose (>=1.40.0,<1.41.0)", "mypy-boto3-fis (>=1.40.0,<1.41.0)", "mypy-boto3-fms (>=1.40.0,<1.41.0)", "mypy-boto3-forecast (>=1.40.0,<1.41.0)", "mypy-boto3-forecastquery (>=1.40.0,<1.41.0)", "mypy-boto3-frauddetector (>=1.40.0,<1.41.0)", "mypy-boto3-freetier (>=1.40.0,<1.41.0)", "mypy-boto3-fsx (>=1.40.0,<1.41.0)", "mypy-boto3-gamelift (>=1.40.0,<1.41.0)", "mypy-boto3-gameliftstreams (>=1.40.0,<1.41.0)", "mypy-boto3-geo-maps (>=1.40.0,<1.41.0)", "mypy-boto3-geo-places (>=1.40.0,<1.41.0)", "mypy-boto3-geo-routes (>=1.40.0,<1.41.0)", "mypy-boto3-glacier (>=1.40.0,<1.41.0)", "mypy-boto3-globalaccelerator (>=1.40.0,<1.41.0)", "mypy-boto3-glue (>=1.40.0,<1.41.0)", "mypy-boto3-grafana (>=1.40.0,<1.41.0)", "mypy-boto3-greengrass (>=1.40.0,<1.41.0)", "mypy-boto3-greengrassv2 (>=1.40.0,<1.41.0)", "mypy-boto3-groundstation (>=1.40.0,<1.41.0)", "mypy-boto3-guardduty (>=1.40.0,<1.41.0)", "mypy-boto3-health (>=1.40.0,<1.41.0)", "mypy-boto3-healthlake (>=1.40.0,<1.41.0)", "mypy-boto3-iam (>=1.40.0,<1.41.0)", "mypy-boto3-identitystore (>=1.40.0,<1.41.0)", "mypy-boto3-imagebuilder (>=1.40.0,<1.41.0)", "mypy-boto3-importexport (>=1.40.0,<1.41.0)", "mypy-boto3-inspector (>=1.40.0,<1.41.0)", "mypy-boto3-inspector-scan (>=1.40.0,<1.41.0)", "mypy-boto3-inspector2 (>=1.40.0,<1.41.0)", "mypy-boto3-internetmonitor (>=1.40.0,<1.41.0)", "mypy-boto3-invoicing (>=1.40.0,<1.41.0)", "mypy-boto3-iot (>=1.40.0,<1.41.0)", "mypy-boto3-iot-data (>=1.40.0,<1.41.0)", "mypy-boto3-iot-jobs-data (>=1.40.0,<1.41.0)", "mypy-boto3-iot-managed-integrations (>=1.40.0,<1.41.0)", "mypy-boto3-iotanalytics (>=1.40.0,<1.41.0)", "mypy-boto3-iotdeviceadvisor (>=1.40.0,<1.41.0)", "mypy-boto3-iotevents (>=1.40.0,<1.41.0)", "mypy-boto3-iotevents-data (>=1.40.0,<1.41.0)", "mypy-boto3-iotfleethub (>=1.40.0,<1.41.0)", "mypy-boto3-iotfleetwise (>=1.40.0,<1.41.0)", "mypy-boto3-iotsecuretunneling (>=1.40.0,<1.41.0)", "mypy-boto3-iotsitewise (>=1.40.0,<1.41.0)", "mypy-boto3-iotthingsgraph (>=1.40.0,<1.41.0)", "mypy-boto3-iottwinmaker (>=1.40.0,<1.41.0)", "mypy-boto3-iotwireless (>=1.40.0,<1.41.0)", "mypy-boto3-ivs (>=1.40.0,<1.41.0)", "mypy-boto3-ivs-realtime (>=1.40.0,<1.41.0)", "mypy-boto3-ivschat (>=1.40.0,<1.41.0)", "mypy-boto3-kafka (>=1.40.0,<1.41.0)", "mypy-boto3-kafkaconnect (>=1.40.0,<1.41.0)", "mypy-boto3-kendra (>=1.40.0,<1.41.0)", "mypy-boto3-kendra-ranking (>=1.40.0,<1.41.0)", "mypy-boto3-keyspaces (>=1.40.0,<1.41.0)", "mypy-boto3-keyspacesstreams (>=1.40.0,<1.41.0)", "mypy-boto3-kinesis (>=1.40.0,<1.41.0)", "mypy-boto3-kinesis-video-archived-media (>=1.40.0,<1.41.0)", "mypy-boto3-kinesis-video-media (>=1.40.0,<1.41.0)", "mypy-boto3-kinesis-video-signaling (>=1.40.0,<1.41.0)", "mypy-boto3-kinesis-video-webrtc-storage (>=1.40.0,<1.41.0)", "mypy-boto3-kinesisanalytics (>=1.40.0,<1.41.0)", "mypy-boto3-kinesisanalyticsv2 (>=1.40.0,<1.41.0)", "mypy-boto3-kinesisvideo (>=1.40.0,<1.41.0)", "mypy-boto3-kms (>=1.40.0,<1.41.0)", "mypy-boto3-lakeformation (>=1.40.0,<1.41.0)", "mypy-boto3-lambda (>=1.40.0,<1.41.0)", "mypy-boto3-launch-wizard (>=1.40.0,<1.41.0)", "mypy-boto3-lex-models (>=1.40.0,<1.41.0)", "mypy-boto3-lex-runtime (>=1.40.0,<1.41.0)", "mypy-boto3-lexv2-models (>=1.40.0,<1.41.0)", "mypy-boto3-lexv2-runtime (>=1.40.0,<1.41.0)", "mypy-boto3-license-manager (>=1.40.0,<1.41.0)", "mypy-boto3-license-manager-linux-subscriptions (>=1.40.0,<1.41.0)", "mypy-boto3-license-manager-user-subscriptions (>=1.40.0,<1.41.0)", "mypy-boto3-lightsail (>=1.40.0,<1.41.0)", "mypy-boto3-location (>=1.40.0,<1.41.0)", "mypy-boto3-logs (>=1.40.0,<1.41.0)", "mypy-boto3-lookoutequipment (>=1.40.0,<1.41.0)", "mypy-boto3-lookoutmetrics (>=1.40.0,<1.41.0)", "mypy-boto3-lookoutvision (>=1.40.0,<1.41.0)", "mypy-boto3-m2 (>=1.40.0,<1.41.0)", "mypy-boto3-machinelearning (>=1.40.0,<1.41.0)", "mypy-boto3-macie2 (>=1.40.0,<1.41.0)", "mypy-boto3-mailmanager (>=1.40.0,<1.41.0)", "mypy-boto3-managedblockchain (>=1.40.0,<1.41.0)", "mypy-boto3-managedblockchain-query (>=1.40.0,<1.41.0)", "mypy-boto3-marketplace-agreement (>=1.40.0,<1.41.0)", "mypy-boto3-marketplace-catalog (>=1.40.0,<1.41.0)", "mypy-boto3-marketplace-deployment (>=1.40.0,<1.41.0)", "mypy-boto3-marketplace-entitlement (>=1.40.0,<1.41.0)", "mypy-boto3-marketplace-reporting (>=1.40.0,<1.41.0)", "mypy-boto3-marketplacecommerceanalytics (>=1.40.0,<1.41.0)", "mypy-boto3-mediaconnect (>=1.40.0,<1.41.0)", "mypy-boto3-mediaconvert (>=1.40.0,<1.41.0)", "mypy-boto3-medialive (>=1.40.0,<1.41.0)", "mypy-boto3-mediapackage (>=1.40.0,<1.41.0)", "mypy-boto3-mediapackage-vod (>=1.40.0,<1.41.0)", "mypy-boto3-mediapackagev2 (>=1.40.0,<1.41.0)", "mypy-boto3-mediastore (>=1.40.0,<1.41.0)", "mypy-boto3-mediastore-data (>=1.40.0,<1.41.0)", "mypy-boto3-mediatailor (>=1.40.0,<1.41.0)", "mypy-boto3-medical-imaging (>=1.40.0,<1.41.0)", "mypy-boto3-memorydb (>=1.40.0,<1.41.0)", "mypy-boto3-meteringmarketplace (>=1.40.0,<1.41.0)", "mypy-boto3-mgh (>=1.40.0,<1.41.0)", "mypy-boto3-mgn (>=1.40.0,<1.41.0)", "mypy-boto3-migration-hub-refactor-spaces (>=1.40.0,<1.41.0)", "mypy-boto3-migrationhub-config (>=1.40.0,<1.41.0)", "mypy-boto3-migrationhuborchestrator (>=1.40.0,<1.41.0)", "mypy-boto3-migrationhubstrategy (>=1.40.0,<1.41.0)", "mypy-boto3-mpa (>=1.40.0,<1.41.0)", "mypy-boto3-mq (>=1.40.0,<1.41.0)", "mypy-boto3-mturk (>=1.40.0,<1.41.0)", "mypy-boto3-mwaa (>=1.40.0,<1.41.0)", "mypy-boto3-neptune (>=1.40.0,<1.41.0)", "mypy-boto3-neptune-graph (>=1.40.0,<1.41.0)", "mypy-boto3-neptunedata (>=1.40.0,<1.41.0)", "mypy-boto3-network-firewall (>=1.40.0,<1.41.0)", "mypy-boto3-networkflowmonitor (>=1.40.0,<1.41.0)", "mypy-boto3-networkmanager (>=1.40.0,<1.41.0)", "mypy-boto3-networkmonitor (>=1.40.0,<1.41.0)", "mypy-boto3-notifications (>=1.40.0,<1.41.0)", "mypy-boto3-notificationscontacts (>=1.40.0,<1.41.0)", "mypy-boto3-oam (>=1.40.0,<1.41.0)", "mypy-boto3-observabilityadmin (>=1.40.0,<1.41.0)", "mypy-boto3-odb (>=1.40.0,<1.41.0)", "mypy-boto3-omics (>=1.40.0,<1.41.0)", "mypy-boto3-opensearch (>=1.40.0,<1.41.0)", "mypy-boto3-opensearchserverless (>=1.40.0,<1.41.0)", "mypy-boto3-organizations (>=1.40.0,<1.41.0)", "mypy-boto3-osis (>=1.40.0,<1.41.0)", "mypy-boto3-outposts (>=1.40.0,<1.41.0)", "mypy-boto3-panorama (>=1.40.0,<1.41.0)", "mypy-boto3-partnercentral-selling (>=1.40.0,<1.41.0)", "mypy-boto3-payment-cryptography (>=1.40.0,<1.41.0)", "mypy-boto3-payment-cryptography-data (>=1.40.0,<1.41.0)", "mypy-boto3-pca-connector-ad (>=1.40.0,<1.41.0)", "mypy-boto3-pca-connector-scep (>=1.40.0,<1.41.0)", "mypy-boto3-pcs (>=1.40.0,<1.41.0)", "mypy-boto3-personalize (>=1.40.0,<1.41.0)", "mypy-boto3-personalize-events (>=1.40.0,<1.41.0)", "mypy-boto3-personalize-runtime (>=1.40.0,<1.41.0)", "mypy-boto3-pi (>=1.40.0,<1.41.0)", "mypy-boto3-pinpoint (>=1.40.0,<1.41.0)", "mypy-boto3-pinpoint-email (>=1.40.0,<1.41.0)", "mypy-boto3-pinpoint-sms-voice (>=1.40.0,<1.41.0)", "mypy-boto3-pinpoint-sms-voice-v2 (>=1.40.0,<1.41.0)", "mypy-boto3-pipes (>=1.40.0,<1.41.0)", "mypy-boto3-polly (>=1.40.0,<1.41.0)", "mypy-boto3-pricing (>=1.40.0,<1.41.0)", "mypy-boto3-proton (>=1.40.0,<1.41.0)", "mypy-boto3-qapps (>=1.40.0,<1.41.0)", "mypy-boto3-qbusiness (>=1.40.0,<1.41.0)", "mypy-boto3-qconnect (>=1.40.0,<1.41.0)", "mypy-boto3-qldb (>=1.40.0,<1.41.0)", "mypy-boto3-qldb-session (>=1.40.0,<1.41.0)", "mypy-boto3-quicksight (>=1.40.0,<1.41.0)", "mypy-boto3-ram (>=1.40.0,<1.41.0)", "mypy-boto3-rbin (>=1.40.0,<1.41.0)", "mypy-boto3-rds (>=1.40.0,<1.41.0)", "mypy-boto3-rds-data (>=1.40.0,<1.41.0)", "mypy-boto3-redshift (>=1.40.0,<1.41.0)", "mypy-boto3-redshift-data (>=1.40.0,<1.41.0)", "mypy-boto3-redshift-serverless (>=1.40.0,<1.41.0)", "mypy-boto3-rekognition (>=1.40.0,<1.41.0)", "mypy-boto3-repostspace (>=1.40.0,<1.41.0)", "mypy-boto3-resiliencehub (>=1.40.0,<1.41.0)", "mypy-boto3-resource-explorer-2 (>=1.40.0,<1.41.0)", "mypy-boto3-resource-groups (>=1.40.0,<1.41.0)", "mypy-boto3-resourcegroupstaggingapi (>=1.40.0,<1.41.0)", "mypy-boto3-robomaker (>=1.40.0,<1.41.0)", "mypy-boto3-rolesanywhere (>=1.40.0,<1.41.0)", "mypy-boto3-route53 (>=1.40.0,<1.41.0)", "mypy-boto3-route53-recovery-cluster (>=1.40.0,<1.41.0)", "mypy-boto3-route53-recovery-control-config (>=1.40.0,<1.41.0)", "mypy-boto3-route53-recovery-readiness (>=1.40.0,<1.41.0)", "mypy-boto3-route53domains (>=1.40.0,<1.41.0)", "mypy-boto3-route53profiles (>=1.40.0,<1.41.0)", "mypy-boto3-route53resolver (>=1.40.0,<1.41.0)", "mypy-boto3-rum (>=1.40.0,<1.41.0)", "mypy-boto3-s3 (>=1.40.0,<1.41.0)", "mypy-boto3-s3control (>=1.40.0,<1.41.0)", "mypy-boto3-s3outposts (>=1.40.0,<1.41.0)", "mypy-boto3-s3tables (>=1.40.0,<1.41.0)", "mypy-boto3-s3vectors (>=1.40.0,<1.41.0)", "mypy-boto3-sagemaker (>=1.40.0,<1.41.0)", "mypy-boto3-sagemaker-a2i-runtime (>=1.40.0,<1.41.0)", "mypy-boto3-sagemaker-edge (>=1.40.0,<1.41.0)", "mypy-boto3-sagemaker-featurestore-runtime (>=1.40.0,<1.41.0)", "mypy-boto3-sagemaker-geospatial (>=1.40.0,<1.41.0)", "mypy-boto3-sagemaker-metrics (>=1.40.0,<1.41.0)", "mypy-boto3-sagemaker-runtime (>=1.40.0,<1.41.0)", "mypy-boto3-savingsplans (>=1.40.0,<1.41.0)", "mypy-boto3-scheduler (>=1.40.0,<1.41.0)", "mypy-boto3-schemas (>=1.40.0,<1.41.0)", "mypy-boto3-sdb (>=1.40.0,<1.41.0)", "mypy-boto3-secretsmanager (>=1.40.0,<1.41.0)", "mypy-boto3-security-ir (>=1.40.0,<1.41.0)", "mypy-boto3-securityhub (>=1.40.0,<1.41.0)", "mypy-boto3-securitylake (>=1.40.0,<1.41.0)", "mypy-boto3-serverlessrepo (>=1.40.0,<1.41.0)", "mypy-boto3-service-quotas (>=1.40.0,<1.41.0)", "mypy-boto3-servicecatalog (>=1.40.0,<1.41.0)", "mypy-boto3-servicecatalog-appregistry (>=1.40.0,<1.41.0)", "mypy-boto3-servicediscovery (>=1.40.0,<1.41.0)", "mypy-boto3-ses (>=1.40.0,<1.41.0)", "mypy-boto3-sesv2 (>=1.40.0,<1.41.0)", "mypy-boto3-shield (>=1.40.0,<1.41.0)", "mypy-boto3-signer (>=1.40.0,<1.41.0)", "mypy-boto3-simspaceweaver (>=1.40.0,<1.41.0)", "mypy-boto3-snow-device-management (>=1.40.0,<1.41.0)", "mypy-boto3-snowball (>=1.40.0,<1.41.0)", "mypy-boto3-sns (>=1.40.0,<1.41.0)", "mypy-boto3-socialmessaging (>=1.40.0,<1.41.0)", "mypy-boto3-sqs (>=1.40.0,<1.41.0)", "mypy-boto3-ssm (>=1.40.0,<1.41.0)", "mypy-boto3-ssm-contacts (>=1.40.0,<1.41.0)", "mypy-boto3-ssm-guiconnect (>=1.40.0,<1.41.0)", "mypy-boto3-ssm-incidents (>=1.40.0,<1.41.0)", "mypy-boto3-ssm-quicksetup (>=1.40.0,<1.41.0)", "mypy-boto3-ssm-sap (>=1.40.0,<1.41.0)", "mypy-boto3-sso (>=1.40.0,<1.41.0)", "mypy-boto3-sso-admin (>=1.40.0,<1.41.0)", "mypy-boto3-sso-oidc (>=1.40.0,<1.41.0)", "mypy-boto3-stepfunctions (>=1.40.0,<1.41.0)", "mypy-boto3-storagegateway (>=1.40.0,<1.41.0)", "mypy-boto3-sts (>=1.40.0,<1.41.0)", "mypy-boto3-supplychain (>=1.40.0,<1.41.0)", "mypy-boto3-support (>=1.40.0,<1.41.0)", "mypy-boto3-support-app (>=1.40.0,<1.41.0)", "mypy-boto3-swf (>=1.40.0,<1.41.0)", "mypy-boto3-synthetics (>=1.40.0,<1.41.0)", "mypy-boto3-taxsettings (>=1.40.0,<1.41.0)", "mypy-boto3-textract (>=1.40.0,<1.41.0)", "mypy-boto3-timestream-influxdb (>=1.40.0,<1.41.0)", "mypy-boto3-timestream-query (>=1.40.0,<1.41.0)", "mypy-boto3-timestream-write (>=1.40.0,<1.41.0)", "mypy-boto3-tnb (>=1.40.0,<1.41.0)", "mypy-boto3-transcribe (>=1.40.0,<1.41.0)", "mypy-boto3-transfer (>=1.40.0,<1.41.0)", "mypy-boto3-translate (>=1.40.0,<1.41.0)", "mypy-boto3-trustedadvisor (>=1.40.0,<1.41.0)", "mypy-boto3-verifiedpermissions (>=1.40.0,<1.41.0)", "mypy-boto3-voice-id (>=1.40.0,<1.41.0)", "mypy-boto3-vpc-lattice (>=1.40.0,<1.41.0)", "mypy-boto3-waf (>=1.40.0,<1.41.0)", "mypy-boto3-waf-regional (>=1.40.0,<1.41.0)", "mypy-boto3-wafv2 (>=1.40.0,<1.41.0)", "mypy-boto3-wellarchitected (>=1.40.0,<1.41.0)", "mypy-boto3-wisdom (>=1.40.0,<1.41.0)", "mypy-boto3-workdocs (>=1.40.0,<1.41.0)", "mypy-boto3-workmail (>=1.40.0,<1.41.0)", "mypy-boto3-workmailmessageflow (>=1.40.0,<1.41.0)", "mypy-boto3-workspaces (>=1.40.0,<1.41.0)", "mypy-boto3-workspaces-instances (>=1.40.0,<1.41.0)", "mypy-boto3-workspaces-thin-client (>=1.40.0,<1.41.0)", "mypy-boto3-workspaces-web (>=1.40.0,<1.41.0)", "mypy-boto3-xray (>=1.40.0,<1.41.0)"] +amp = ["mypy-boto3-amp (>=1.40.0,<1.41.0)"] +amplify = ["mypy-boto3-amplify (>=1.40.0,<1.41.0)"] +amplifybackend = ["mypy-boto3-amplifybackend (>=1.40.0,<1.41.0)"] +amplifyuibuilder = ["mypy-boto3-amplifyuibuilder (>=1.40.0,<1.41.0)"] +apigateway = ["mypy-boto3-apigateway (>=1.40.0,<1.41.0)"] +apigatewaymanagementapi = ["mypy-boto3-apigatewaymanagementapi (>=1.40.0,<1.41.0)"] +apigatewayv2 = ["mypy-boto3-apigatewayv2 (>=1.40.0,<1.41.0)"] +appconfig = ["mypy-boto3-appconfig (>=1.40.0,<1.41.0)"] +appconfigdata = ["mypy-boto3-appconfigdata (>=1.40.0,<1.41.0)"] +appfabric = ["mypy-boto3-appfabric (>=1.40.0,<1.41.0)"] +appflow = ["mypy-boto3-appflow (>=1.40.0,<1.41.0)"] +appintegrations = ["mypy-boto3-appintegrations (>=1.40.0,<1.41.0)"] +application-autoscaling = ["mypy-boto3-application-autoscaling (>=1.40.0,<1.41.0)"] +application-insights = ["mypy-boto3-application-insights (>=1.40.0,<1.41.0)"] +application-signals = ["mypy-boto3-application-signals (>=1.40.0,<1.41.0)"] +applicationcostprofiler = ["mypy-boto3-applicationcostprofiler (>=1.40.0,<1.41.0)"] +appmesh = ["mypy-boto3-appmesh (>=1.40.0,<1.41.0)"] +apprunner = ["mypy-boto3-apprunner (>=1.40.0,<1.41.0)"] +appstream = ["mypy-boto3-appstream (>=1.40.0,<1.41.0)"] +appsync = ["mypy-boto3-appsync (>=1.40.0,<1.41.0)"] +apptest = ["mypy-boto3-apptest (>=1.40.0,<1.41.0)"] +arc-region-switch = ["mypy-boto3-arc-region-switch (>=1.40.0,<1.41.0)"] +arc-zonal-shift = ["mypy-boto3-arc-zonal-shift (>=1.40.0,<1.41.0)"] +artifact = ["mypy-boto3-artifact (>=1.40.0,<1.41.0)"] +athena = ["mypy-boto3-athena (>=1.40.0,<1.41.0)"] +auditmanager = ["mypy-boto3-auditmanager (>=1.40.0,<1.41.0)"] +autoscaling = ["mypy-boto3-autoscaling (>=1.40.0,<1.41.0)"] +autoscaling-plans = ["mypy-boto3-autoscaling-plans (>=1.40.0,<1.41.0)"] +b2bi = ["mypy-boto3-b2bi (>=1.40.0,<1.41.0)"] +backup = ["mypy-boto3-backup (>=1.40.0,<1.41.0)"] +backup-gateway = ["mypy-boto3-backup-gateway (>=1.40.0,<1.41.0)"] +backupsearch = ["mypy-boto3-backupsearch (>=1.40.0,<1.41.0)"] +batch = ["mypy-boto3-batch (>=1.40.0,<1.41.0)"] +bcm-dashboards = ["mypy-boto3-bcm-dashboards (>=1.40.0,<1.41.0)"] +bcm-data-exports = ["mypy-boto3-bcm-data-exports (>=1.40.0,<1.41.0)"] +bcm-pricing-calculator = ["mypy-boto3-bcm-pricing-calculator (>=1.40.0,<1.41.0)"] +bcm-recommended-actions = ["mypy-boto3-bcm-recommended-actions (>=1.40.0,<1.41.0)"] +bedrock = ["mypy-boto3-bedrock (>=1.40.0,<1.41.0)"] +bedrock-agent = ["mypy-boto3-bedrock-agent (>=1.40.0,<1.41.0)"] +bedrock-agent-runtime = ["mypy-boto3-bedrock-agent-runtime (>=1.40.0,<1.41.0)"] +bedrock-agentcore = ["mypy-boto3-bedrock-agentcore (>=1.40.0,<1.41.0)"] +bedrock-agentcore-control = ["mypy-boto3-bedrock-agentcore-control (>=1.40.0,<1.41.0)"] +bedrock-data-automation = ["mypy-boto3-bedrock-data-automation (>=1.40.0,<1.41.0)"] +bedrock-data-automation-runtime = ["mypy-boto3-bedrock-data-automation-runtime (>=1.40.0,<1.41.0)"] +bedrock-runtime = ["mypy-boto3-bedrock-runtime (>=1.40.0,<1.41.0)"] +billing = ["mypy-boto3-billing (>=1.40.0,<1.41.0)"] +billingconductor = ["mypy-boto3-billingconductor (>=1.40.0,<1.41.0)"] +boto3 = ["boto3 (==1.40.56)"] +braket = ["mypy-boto3-braket (>=1.40.0,<1.41.0)"] +budgets = ["mypy-boto3-budgets (>=1.40.0,<1.41.0)"] +ce = ["mypy-boto3-ce (>=1.40.0,<1.41.0)"] +chatbot = ["mypy-boto3-chatbot (>=1.40.0,<1.41.0)"] +chime = ["mypy-boto3-chime (>=1.40.0,<1.41.0)"] +chime-sdk-identity = ["mypy-boto3-chime-sdk-identity (>=1.40.0,<1.41.0)"] +chime-sdk-media-pipelines = ["mypy-boto3-chime-sdk-media-pipelines (>=1.40.0,<1.41.0)"] +chime-sdk-meetings = ["mypy-boto3-chime-sdk-meetings (>=1.40.0,<1.41.0)"] +chime-sdk-messaging = ["mypy-boto3-chime-sdk-messaging (>=1.40.0,<1.41.0)"] +chime-sdk-voice = ["mypy-boto3-chime-sdk-voice (>=1.40.0,<1.41.0)"] +cleanrooms = ["mypy-boto3-cleanrooms (>=1.40.0,<1.41.0)"] +cleanroomsml = ["mypy-boto3-cleanroomsml (>=1.40.0,<1.41.0)"] +cloud9 = ["mypy-boto3-cloud9 (>=1.40.0,<1.41.0)"] +cloudcontrol = ["mypy-boto3-cloudcontrol (>=1.40.0,<1.41.0)"] +clouddirectory = ["mypy-boto3-clouddirectory (>=1.40.0,<1.41.0)"] +cloudformation = ["mypy-boto3-cloudformation (>=1.40.0,<1.41.0)"] +cloudfront = ["mypy-boto3-cloudfront (>=1.40.0,<1.41.0)"] +cloudfront-keyvaluestore = ["mypy-boto3-cloudfront-keyvaluestore (>=1.40.0,<1.41.0)"] +cloudhsm = ["mypy-boto3-cloudhsm (>=1.40.0,<1.41.0)"] +cloudhsmv2 = ["mypy-boto3-cloudhsmv2 (>=1.40.0,<1.41.0)"] +cloudsearch = ["mypy-boto3-cloudsearch (>=1.40.0,<1.41.0)"] +cloudsearchdomain = ["mypy-boto3-cloudsearchdomain (>=1.40.0,<1.41.0)"] +cloudtrail = ["mypy-boto3-cloudtrail (>=1.40.0,<1.41.0)"] +cloudtrail-data = ["mypy-boto3-cloudtrail-data (>=1.40.0,<1.41.0)"] +cloudwatch = ["mypy-boto3-cloudwatch (>=1.40.0,<1.41.0)"] +codeartifact = ["mypy-boto3-codeartifact (>=1.40.0,<1.41.0)"] +codebuild = ["mypy-boto3-codebuild (>=1.40.0,<1.41.0)"] +codecatalyst = ["mypy-boto3-codecatalyst (>=1.40.0,<1.41.0)"] +codecommit = ["mypy-boto3-codecommit (>=1.40.0,<1.41.0)"] +codeconnections = ["mypy-boto3-codeconnections (>=1.40.0,<1.41.0)"] +codedeploy = ["mypy-boto3-codedeploy (>=1.40.0,<1.41.0)"] +codeguru-reviewer = ["mypy-boto3-codeguru-reviewer (>=1.40.0,<1.41.0)"] +codeguru-security = ["mypy-boto3-codeguru-security (>=1.40.0,<1.41.0)"] +codeguruprofiler = ["mypy-boto3-codeguruprofiler (>=1.40.0,<1.41.0)"] +codepipeline = ["mypy-boto3-codepipeline (>=1.40.0,<1.41.0)"] +codestar-connections = ["mypy-boto3-codestar-connections (>=1.40.0,<1.41.0)"] +codestar-notifications = ["mypy-boto3-codestar-notifications (>=1.40.0,<1.41.0)"] +cognito-identity = ["mypy-boto3-cognito-identity (>=1.40.0,<1.41.0)"] +cognito-idp = ["mypy-boto3-cognito-idp (>=1.40.0,<1.41.0)"] +cognito-sync = ["mypy-boto3-cognito-sync (>=1.40.0,<1.41.0)"] +comprehend = ["mypy-boto3-comprehend (>=1.40.0,<1.41.0)"] +comprehendmedical = ["mypy-boto3-comprehendmedical (>=1.40.0,<1.41.0)"] +compute-optimizer = ["mypy-boto3-compute-optimizer (>=1.40.0,<1.41.0)"] +config = ["mypy-boto3-config (>=1.40.0,<1.41.0)"] +connect = ["mypy-boto3-connect (>=1.40.0,<1.41.0)"] +connect-contact-lens = ["mypy-boto3-connect-contact-lens (>=1.40.0,<1.41.0)"] +connectcampaigns = ["mypy-boto3-connectcampaigns (>=1.40.0,<1.41.0)"] +connectcampaignsv2 = ["mypy-boto3-connectcampaignsv2 (>=1.40.0,<1.41.0)"] +connectcases = ["mypy-boto3-connectcases (>=1.40.0,<1.41.0)"] +connectparticipant = ["mypy-boto3-connectparticipant (>=1.40.0,<1.41.0)"] +controlcatalog = ["mypy-boto3-controlcatalog (>=1.40.0,<1.41.0)"] +controltower = ["mypy-boto3-controltower (>=1.40.0,<1.41.0)"] +cost-optimization-hub = ["mypy-boto3-cost-optimization-hub (>=1.40.0,<1.41.0)"] +cur = ["mypy-boto3-cur (>=1.40.0,<1.41.0)"] +customer-profiles = ["mypy-boto3-customer-profiles (>=1.40.0,<1.41.0)"] +databrew = ["mypy-boto3-databrew (>=1.40.0,<1.41.0)"] +dataexchange = ["mypy-boto3-dataexchange (>=1.40.0,<1.41.0)"] +datapipeline = ["mypy-boto3-datapipeline (>=1.40.0,<1.41.0)"] +datasync = ["mypy-boto3-datasync (>=1.40.0,<1.41.0)"] +datazone = ["mypy-boto3-datazone (>=1.40.0,<1.41.0)"] +dax = ["mypy-boto3-dax (>=1.40.0,<1.41.0)"] +deadline = ["mypy-boto3-deadline (>=1.40.0,<1.41.0)"] +detective = ["mypy-boto3-detective (>=1.40.0,<1.41.0)"] +devicefarm = ["mypy-boto3-devicefarm (>=1.40.0,<1.41.0)"] +devops-guru = ["mypy-boto3-devops-guru (>=1.40.0,<1.41.0)"] +directconnect = ["mypy-boto3-directconnect (>=1.40.0,<1.41.0)"] +discovery = ["mypy-boto3-discovery (>=1.40.0,<1.41.0)"] +dlm = ["mypy-boto3-dlm (>=1.40.0,<1.41.0)"] +dms = ["mypy-boto3-dms (>=1.40.0,<1.41.0)"] +docdb = ["mypy-boto3-docdb (>=1.40.0,<1.41.0)"] +docdb-elastic = ["mypy-boto3-docdb-elastic (>=1.40.0,<1.41.0)"] +drs = ["mypy-boto3-drs (>=1.40.0,<1.41.0)"] +ds = ["mypy-boto3-ds (>=1.40.0,<1.41.0)"] +ds-data = ["mypy-boto3-ds-data (>=1.40.0,<1.41.0)"] +dsql = ["mypy-boto3-dsql (>=1.40.0,<1.41.0)"] +dynamodb = ["mypy-boto3-dynamodb (>=1.40.0,<1.41.0)"] +dynamodbstreams = ["mypy-boto3-dynamodbstreams (>=1.40.0,<1.41.0)"] +ebs = ["mypy-boto3-ebs (>=1.40.0,<1.41.0)"] +ec2 = ["mypy-boto3-ec2 (>=1.40.0,<1.41.0)"] +ec2-instance-connect = ["mypy-boto3-ec2-instance-connect (>=1.40.0,<1.41.0)"] +ecr = ["mypy-boto3-ecr (>=1.40.0,<1.41.0)"] +ecr-public = ["mypy-boto3-ecr-public (>=1.40.0,<1.41.0)"] +ecs = ["mypy-boto3-ecs (>=1.40.0,<1.41.0)"] +efs = ["mypy-boto3-efs (>=1.40.0,<1.41.0)"] +eks = ["mypy-boto3-eks (>=1.40.0,<1.41.0)"] +eks-auth = ["mypy-boto3-eks-auth (>=1.40.0,<1.41.0)"] +elasticache = ["mypy-boto3-elasticache (>=1.40.0,<1.41.0)"] +elasticbeanstalk = ["mypy-boto3-elasticbeanstalk (>=1.40.0,<1.41.0)"] +elastictranscoder = ["mypy-boto3-elastictranscoder (>=1.40.0,<1.41.0)"] +elb = ["mypy-boto3-elb (>=1.40.0,<1.41.0)"] +elbv2 = ["mypy-boto3-elbv2 (>=1.40.0,<1.41.0)"] +emr = ["mypy-boto3-emr (>=1.40.0,<1.41.0)"] +emr-containers = ["mypy-boto3-emr-containers (>=1.40.0,<1.41.0)"] +emr-serverless = ["mypy-boto3-emr-serverless (>=1.40.0,<1.41.0)"] +entityresolution = ["mypy-boto3-entityresolution (>=1.40.0,<1.41.0)"] +es = ["mypy-boto3-es (>=1.40.0,<1.41.0)"] +essential = ["mypy-boto3-cloudformation (>=1.40.0,<1.41.0)", "mypy-boto3-dynamodb (>=1.40.0,<1.41.0)", "mypy-boto3-ec2 (>=1.40.0,<1.41.0)", "mypy-boto3-lambda (>=1.40.0,<1.41.0)", "mypy-boto3-rds (>=1.40.0,<1.41.0)", "mypy-boto3-s3 (>=1.40.0,<1.41.0)", "mypy-boto3-sqs (>=1.40.0,<1.41.0)"] +events = ["mypy-boto3-events (>=1.40.0,<1.41.0)"] +evidently = ["mypy-boto3-evidently (>=1.40.0,<1.41.0)"] +evs = ["mypy-boto3-evs (>=1.40.0,<1.41.0)"] +finspace = ["mypy-boto3-finspace (>=1.40.0,<1.41.0)"] +finspace-data = ["mypy-boto3-finspace-data (>=1.40.0,<1.41.0)"] +firehose = ["mypy-boto3-firehose (>=1.40.0,<1.41.0)"] +fis = ["mypy-boto3-fis (>=1.40.0,<1.41.0)"] +fms = ["mypy-boto3-fms (>=1.40.0,<1.41.0)"] +forecast = ["mypy-boto3-forecast (>=1.40.0,<1.41.0)"] +forecastquery = ["mypy-boto3-forecastquery (>=1.40.0,<1.41.0)"] +frauddetector = ["mypy-boto3-frauddetector (>=1.40.0,<1.41.0)"] +freetier = ["mypy-boto3-freetier (>=1.40.0,<1.41.0)"] +fsx = ["mypy-boto3-fsx (>=1.40.0,<1.41.0)"] +full = ["boto3-stubs-full (>=1.40.0,<1.41.0)"] +gamelift = ["mypy-boto3-gamelift (>=1.40.0,<1.41.0)"] +gameliftstreams = ["mypy-boto3-gameliftstreams (>=1.40.0,<1.41.0)"] +geo-maps = ["mypy-boto3-geo-maps (>=1.40.0,<1.41.0)"] +geo-places = ["mypy-boto3-geo-places (>=1.40.0,<1.41.0)"] +geo-routes = ["mypy-boto3-geo-routes (>=1.40.0,<1.41.0)"] +glacier = ["mypy-boto3-glacier (>=1.40.0,<1.41.0)"] +globalaccelerator = ["mypy-boto3-globalaccelerator (>=1.40.0,<1.41.0)"] +glue = ["mypy-boto3-glue (>=1.40.0,<1.41.0)"] +grafana = ["mypy-boto3-grafana (>=1.40.0,<1.41.0)"] +greengrass = ["mypy-boto3-greengrass (>=1.40.0,<1.41.0)"] +greengrassv2 = ["mypy-boto3-greengrassv2 (>=1.40.0,<1.41.0)"] +groundstation = ["mypy-boto3-groundstation (>=1.40.0,<1.41.0)"] +guardduty = ["mypy-boto3-guardduty (>=1.40.0,<1.41.0)"] +health = ["mypy-boto3-health (>=1.40.0,<1.41.0)"] +healthlake = ["mypy-boto3-healthlake (>=1.40.0,<1.41.0)"] +iam = ["mypy-boto3-iam (>=1.40.0,<1.41.0)"] +identitystore = ["mypy-boto3-identitystore (>=1.40.0,<1.41.0)"] +imagebuilder = ["mypy-boto3-imagebuilder (>=1.40.0,<1.41.0)"] +importexport = ["mypy-boto3-importexport (>=1.40.0,<1.41.0)"] +inspector = ["mypy-boto3-inspector (>=1.40.0,<1.41.0)"] +inspector-scan = ["mypy-boto3-inspector-scan (>=1.40.0,<1.41.0)"] +inspector2 = ["mypy-boto3-inspector2 (>=1.40.0,<1.41.0)"] +internetmonitor = ["mypy-boto3-internetmonitor (>=1.40.0,<1.41.0)"] +invoicing = ["mypy-boto3-invoicing (>=1.40.0,<1.41.0)"] +iot = ["mypy-boto3-iot (>=1.40.0,<1.41.0)"] +iot-data = ["mypy-boto3-iot-data (>=1.40.0,<1.41.0)"] +iot-jobs-data = ["mypy-boto3-iot-jobs-data (>=1.40.0,<1.41.0)"] +iot-managed-integrations = ["mypy-boto3-iot-managed-integrations (>=1.40.0,<1.41.0)"] +iotanalytics = ["mypy-boto3-iotanalytics (>=1.40.0,<1.41.0)"] +iotdeviceadvisor = ["mypy-boto3-iotdeviceadvisor (>=1.40.0,<1.41.0)"] +iotevents = ["mypy-boto3-iotevents (>=1.40.0,<1.41.0)"] +iotevents-data = ["mypy-boto3-iotevents-data (>=1.40.0,<1.41.0)"] +iotfleethub = ["mypy-boto3-iotfleethub (>=1.40.0,<1.41.0)"] +iotfleetwise = ["mypy-boto3-iotfleetwise (>=1.40.0,<1.41.0)"] +iotsecuretunneling = ["mypy-boto3-iotsecuretunneling (>=1.40.0,<1.41.0)"] +iotsitewise = ["mypy-boto3-iotsitewise (>=1.40.0,<1.41.0)"] +iotthingsgraph = ["mypy-boto3-iotthingsgraph (>=1.40.0,<1.41.0)"] +iottwinmaker = ["mypy-boto3-iottwinmaker (>=1.40.0,<1.41.0)"] +iotwireless = ["mypy-boto3-iotwireless (>=1.40.0,<1.41.0)"] +ivs = ["mypy-boto3-ivs (>=1.40.0,<1.41.0)"] +ivs-realtime = ["mypy-boto3-ivs-realtime (>=1.40.0,<1.41.0)"] +ivschat = ["mypy-boto3-ivschat (>=1.40.0,<1.41.0)"] +kafka = ["mypy-boto3-kafka (>=1.40.0,<1.41.0)"] +kafkaconnect = ["mypy-boto3-kafkaconnect (>=1.40.0,<1.41.0)"] +kendra = ["mypy-boto3-kendra (>=1.40.0,<1.41.0)"] +kendra-ranking = ["mypy-boto3-kendra-ranking (>=1.40.0,<1.41.0)"] +keyspaces = ["mypy-boto3-keyspaces (>=1.40.0,<1.41.0)"] +keyspacesstreams = ["mypy-boto3-keyspacesstreams (>=1.40.0,<1.41.0)"] +kinesis = ["mypy-boto3-kinesis (>=1.40.0,<1.41.0)"] +kinesis-video-archived-media = ["mypy-boto3-kinesis-video-archived-media (>=1.40.0,<1.41.0)"] +kinesis-video-media = ["mypy-boto3-kinesis-video-media (>=1.40.0,<1.41.0)"] +kinesis-video-signaling = ["mypy-boto3-kinesis-video-signaling (>=1.40.0,<1.41.0)"] +kinesis-video-webrtc-storage = ["mypy-boto3-kinesis-video-webrtc-storage (>=1.40.0,<1.41.0)"] +kinesisanalytics = ["mypy-boto3-kinesisanalytics (>=1.40.0,<1.41.0)"] +kinesisanalyticsv2 = ["mypy-boto3-kinesisanalyticsv2 (>=1.40.0,<1.41.0)"] +kinesisvideo = ["mypy-boto3-kinesisvideo (>=1.40.0,<1.41.0)"] +kms = ["mypy-boto3-kms (>=1.40.0,<1.41.0)"] +lakeformation = ["mypy-boto3-lakeformation (>=1.40.0,<1.41.0)"] +lambda = ["mypy-boto3-lambda (>=1.40.0,<1.41.0)"] +launch-wizard = ["mypy-boto3-launch-wizard (>=1.40.0,<1.41.0)"] +lex-models = ["mypy-boto3-lex-models (>=1.40.0,<1.41.0)"] +lex-runtime = ["mypy-boto3-lex-runtime (>=1.40.0,<1.41.0)"] +lexv2-models = ["mypy-boto3-lexv2-models (>=1.40.0,<1.41.0)"] +lexv2-runtime = ["mypy-boto3-lexv2-runtime (>=1.40.0,<1.41.0)"] +license-manager = ["mypy-boto3-license-manager (>=1.40.0,<1.41.0)"] +license-manager-linux-subscriptions = ["mypy-boto3-license-manager-linux-subscriptions (>=1.40.0,<1.41.0)"] +license-manager-user-subscriptions = ["mypy-boto3-license-manager-user-subscriptions (>=1.40.0,<1.41.0)"] +lightsail = ["mypy-boto3-lightsail (>=1.40.0,<1.41.0)"] +location = ["mypy-boto3-location (>=1.40.0,<1.41.0)"] +logs = ["mypy-boto3-logs (>=1.40.0,<1.41.0)"] +lookoutequipment = ["mypy-boto3-lookoutequipment (>=1.40.0,<1.41.0)"] +lookoutmetrics = ["mypy-boto3-lookoutmetrics (>=1.40.0,<1.41.0)"] +lookoutvision = ["mypy-boto3-lookoutvision (>=1.40.0,<1.41.0)"] +m2 = ["mypy-boto3-m2 (>=1.40.0,<1.41.0)"] +machinelearning = ["mypy-boto3-machinelearning (>=1.40.0,<1.41.0)"] +macie2 = ["mypy-boto3-macie2 (>=1.40.0,<1.41.0)"] +mailmanager = ["mypy-boto3-mailmanager (>=1.40.0,<1.41.0)"] +managedblockchain = ["mypy-boto3-managedblockchain (>=1.40.0,<1.41.0)"] +managedblockchain-query = ["mypy-boto3-managedblockchain-query (>=1.40.0,<1.41.0)"] +marketplace-agreement = ["mypy-boto3-marketplace-agreement (>=1.40.0,<1.41.0)"] +marketplace-catalog = ["mypy-boto3-marketplace-catalog (>=1.40.0,<1.41.0)"] +marketplace-deployment = ["mypy-boto3-marketplace-deployment (>=1.40.0,<1.41.0)"] +marketplace-entitlement = ["mypy-boto3-marketplace-entitlement (>=1.40.0,<1.41.0)"] +marketplace-reporting = ["mypy-boto3-marketplace-reporting (>=1.40.0,<1.41.0)"] +marketplacecommerceanalytics = ["mypy-boto3-marketplacecommerceanalytics (>=1.40.0,<1.41.0)"] +mediaconnect = ["mypy-boto3-mediaconnect (>=1.40.0,<1.41.0)"] +mediaconvert = ["mypy-boto3-mediaconvert (>=1.40.0,<1.41.0)"] +medialive = ["mypy-boto3-medialive (>=1.40.0,<1.41.0)"] +mediapackage = ["mypy-boto3-mediapackage (>=1.40.0,<1.41.0)"] +mediapackage-vod = ["mypy-boto3-mediapackage-vod (>=1.40.0,<1.41.0)"] +mediapackagev2 = ["mypy-boto3-mediapackagev2 (>=1.40.0,<1.41.0)"] +mediastore = ["mypy-boto3-mediastore (>=1.40.0,<1.41.0)"] +mediastore-data = ["mypy-boto3-mediastore-data (>=1.40.0,<1.41.0)"] +mediatailor = ["mypy-boto3-mediatailor (>=1.40.0,<1.41.0)"] +medical-imaging = ["mypy-boto3-medical-imaging (>=1.40.0,<1.41.0)"] +memorydb = ["mypy-boto3-memorydb (>=1.40.0,<1.41.0)"] +meteringmarketplace = ["mypy-boto3-meteringmarketplace (>=1.40.0,<1.41.0)"] +mgh = ["mypy-boto3-mgh (>=1.40.0,<1.41.0)"] +mgn = ["mypy-boto3-mgn (>=1.40.0,<1.41.0)"] +migration-hub-refactor-spaces = ["mypy-boto3-migration-hub-refactor-spaces (>=1.40.0,<1.41.0)"] +migrationhub-config = ["mypy-boto3-migrationhub-config (>=1.40.0,<1.41.0)"] +migrationhuborchestrator = ["mypy-boto3-migrationhuborchestrator (>=1.40.0,<1.41.0)"] +migrationhubstrategy = ["mypy-boto3-migrationhubstrategy (>=1.40.0,<1.41.0)"] +mpa = ["mypy-boto3-mpa (>=1.40.0,<1.41.0)"] +mq = ["mypy-boto3-mq (>=1.40.0,<1.41.0)"] +mturk = ["mypy-boto3-mturk (>=1.40.0,<1.41.0)"] +mwaa = ["mypy-boto3-mwaa (>=1.40.0,<1.41.0)"] +neptune = ["mypy-boto3-neptune (>=1.40.0,<1.41.0)"] +neptune-graph = ["mypy-boto3-neptune-graph (>=1.40.0,<1.41.0)"] +neptunedata = ["mypy-boto3-neptunedata (>=1.40.0,<1.41.0)"] +network-firewall = ["mypy-boto3-network-firewall (>=1.40.0,<1.41.0)"] +networkflowmonitor = ["mypy-boto3-networkflowmonitor (>=1.40.0,<1.41.0)"] +networkmanager = ["mypy-boto3-networkmanager (>=1.40.0,<1.41.0)"] +networkmonitor = ["mypy-boto3-networkmonitor (>=1.40.0,<1.41.0)"] +notifications = ["mypy-boto3-notifications (>=1.40.0,<1.41.0)"] +notificationscontacts = ["mypy-boto3-notificationscontacts (>=1.40.0,<1.41.0)"] +oam = ["mypy-boto3-oam (>=1.40.0,<1.41.0)"] +observabilityadmin = ["mypy-boto3-observabilityadmin (>=1.40.0,<1.41.0)"] +odb = ["mypy-boto3-odb (>=1.40.0,<1.41.0)"] +omics = ["mypy-boto3-omics (>=1.40.0,<1.41.0)"] +opensearch = ["mypy-boto3-opensearch (>=1.40.0,<1.41.0)"] +opensearchserverless = ["mypy-boto3-opensearchserverless (>=1.40.0,<1.41.0)"] +organizations = ["mypy-boto3-organizations (>=1.40.0,<1.41.0)"] +osis = ["mypy-boto3-osis (>=1.40.0,<1.41.0)"] +outposts = ["mypy-boto3-outposts (>=1.40.0,<1.41.0)"] +panorama = ["mypy-boto3-panorama (>=1.40.0,<1.41.0)"] +partnercentral-selling = ["mypy-boto3-partnercentral-selling (>=1.40.0,<1.41.0)"] +payment-cryptography = ["mypy-boto3-payment-cryptography (>=1.40.0,<1.41.0)"] +payment-cryptography-data = ["mypy-boto3-payment-cryptography-data (>=1.40.0,<1.41.0)"] +pca-connector-ad = ["mypy-boto3-pca-connector-ad (>=1.40.0,<1.41.0)"] +pca-connector-scep = ["mypy-boto3-pca-connector-scep (>=1.40.0,<1.41.0)"] +pcs = ["mypy-boto3-pcs (>=1.40.0,<1.41.0)"] +personalize = ["mypy-boto3-personalize (>=1.40.0,<1.41.0)"] +personalize-events = ["mypy-boto3-personalize-events (>=1.40.0,<1.41.0)"] +personalize-runtime = ["mypy-boto3-personalize-runtime (>=1.40.0,<1.41.0)"] +pi = ["mypy-boto3-pi (>=1.40.0,<1.41.0)"] +pinpoint = ["mypy-boto3-pinpoint (>=1.40.0,<1.41.0)"] +pinpoint-email = ["mypy-boto3-pinpoint-email (>=1.40.0,<1.41.0)"] +pinpoint-sms-voice = ["mypy-boto3-pinpoint-sms-voice (>=1.40.0,<1.41.0)"] +pinpoint-sms-voice-v2 = ["mypy-boto3-pinpoint-sms-voice-v2 (>=1.40.0,<1.41.0)"] +pipes = ["mypy-boto3-pipes (>=1.40.0,<1.41.0)"] +polly = ["mypy-boto3-polly (>=1.40.0,<1.41.0)"] +pricing = ["mypy-boto3-pricing (>=1.40.0,<1.41.0)"] +proton = ["mypy-boto3-proton (>=1.40.0,<1.41.0)"] +qapps = ["mypy-boto3-qapps (>=1.40.0,<1.41.0)"] +qbusiness = ["mypy-boto3-qbusiness (>=1.40.0,<1.41.0)"] +qconnect = ["mypy-boto3-qconnect (>=1.40.0,<1.41.0)"] +qldb = ["mypy-boto3-qldb (>=1.40.0,<1.41.0)"] +qldb-session = ["mypy-boto3-qldb-session (>=1.40.0,<1.41.0)"] +quicksight = ["mypy-boto3-quicksight (>=1.40.0,<1.41.0)"] +ram = ["mypy-boto3-ram (>=1.40.0,<1.41.0)"] +rbin = ["mypy-boto3-rbin (>=1.40.0,<1.41.0)"] +rds = ["mypy-boto3-rds (>=1.40.0,<1.41.0)"] +rds-data = ["mypy-boto3-rds-data (>=1.40.0,<1.41.0)"] +redshift = ["mypy-boto3-redshift (>=1.40.0,<1.41.0)"] +redshift-data = ["mypy-boto3-redshift-data (>=1.40.0,<1.41.0)"] +redshift-serverless = ["mypy-boto3-redshift-serverless (>=1.40.0,<1.41.0)"] +rekognition = ["mypy-boto3-rekognition (>=1.40.0,<1.41.0)"] +repostspace = ["mypy-boto3-repostspace (>=1.40.0,<1.41.0)"] +resiliencehub = ["mypy-boto3-resiliencehub (>=1.40.0,<1.41.0)"] +resource-explorer-2 = ["mypy-boto3-resource-explorer-2 (>=1.40.0,<1.41.0)"] +resource-groups = ["mypy-boto3-resource-groups (>=1.40.0,<1.41.0)"] +resourcegroupstaggingapi = ["mypy-boto3-resourcegroupstaggingapi (>=1.40.0,<1.41.0)"] +robomaker = ["mypy-boto3-robomaker (>=1.40.0,<1.41.0)"] +rolesanywhere = ["mypy-boto3-rolesanywhere (>=1.40.0,<1.41.0)"] +route53 = ["mypy-boto3-route53 (>=1.40.0,<1.41.0)"] +route53-recovery-cluster = ["mypy-boto3-route53-recovery-cluster (>=1.40.0,<1.41.0)"] +route53-recovery-control-config = ["mypy-boto3-route53-recovery-control-config (>=1.40.0,<1.41.0)"] +route53-recovery-readiness = ["mypy-boto3-route53-recovery-readiness (>=1.40.0,<1.41.0)"] +route53domains = ["mypy-boto3-route53domains (>=1.40.0,<1.41.0)"] +route53profiles = ["mypy-boto3-route53profiles (>=1.40.0,<1.41.0)"] +route53resolver = ["mypy-boto3-route53resolver (>=1.40.0,<1.41.0)"] +rum = ["mypy-boto3-rum (>=1.40.0,<1.41.0)"] +s3 = ["mypy-boto3-s3 (>=1.40.0,<1.41.0)"] +s3control = ["mypy-boto3-s3control (>=1.40.0,<1.41.0)"] +s3outposts = ["mypy-boto3-s3outposts (>=1.40.0,<1.41.0)"] +s3tables = ["mypy-boto3-s3tables (>=1.40.0,<1.41.0)"] +s3vectors = ["mypy-boto3-s3vectors (>=1.40.0,<1.41.0)"] +sagemaker = ["mypy-boto3-sagemaker (>=1.40.0,<1.41.0)"] +sagemaker-a2i-runtime = ["mypy-boto3-sagemaker-a2i-runtime (>=1.40.0,<1.41.0)"] +sagemaker-edge = ["mypy-boto3-sagemaker-edge (>=1.40.0,<1.41.0)"] +sagemaker-featurestore-runtime = ["mypy-boto3-sagemaker-featurestore-runtime (>=1.40.0,<1.41.0)"] +sagemaker-geospatial = ["mypy-boto3-sagemaker-geospatial (>=1.40.0,<1.41.0)"] +sagemaker-metrics = ["mypy-boto3-sagemaker-metrics (>=1.40.0,<1.41.0)"] +sagemaker-runtime = ["mypy-boto3-sagemaker-runtime (>=1.40.0,<1.41.0)"] +savingsplans = ["mypy-boto3-savingsplans (>=1.40.0,<1.41.0)"] +scheduler = ["mypy-boto3-scheduler (>=1.40.0,<1.41.0)"] +schemas = ["mypy-boto3-schemas (>=1.40.0,<1.41.0)"] +sdb = ["mypy-boto3-sdb (>=1.40.0,<1.41.0)"] +secretsmanager = ["mypy-boto3-secretsmanager (>=1.40.0,<1.41.0)"] +security-ir = ["mypy-boto3-security-ir (>=1.40.0,<1.41.0)"] +securityhub = ["mypy-boto3-securityhub (>=1.40.0,<1.41.0)"] +securitylake = ["mypy-boto3-securitylake (>=1.40.0,<1.41.0)"] +serverlessrepo = ["mypy-boto3-serverlessrepo (>=1.40.0,<1.41.0)"] +service-quotas = ["mypy-boto3-service-quotas (>=1.40.0,<1.41.0)"] +servicecatalog = ["mypy-boto3-servicecatalog (>=1.40.0,<1.41.0)"] +servicecatalog-appregistry = ["mypy-boto3-servicecatalog-appregistry (>=1.40.0,<1.41.0)"] +servicediscovery = ["mypy-boto3-servicediscovery (>=1.40.0,<1.41.0)"] +ses = ["mypy-boto3-ses (>=1.40.0,<1.41.0)"] +sesv2 = ["mypy-boto3-sesv2 (>=1.40.0,<1.41.0)"] +shield = ["mypy-boto3-shield (>=1.40.0,<1.41.0)"] +signer = ["mypy-boto3-signer (>=1.40.0,<1.41.0)"] +simspaceweaver = ["mypy-boto3-simspaceweaver (>=1.40.0,<1.41.0)"] +snow-device-management = ["mypy-boto3-snow-device-management (>=1.40.0,<1.41.0)"] +snowball = ["mypy-boto3-snowball (>=1.40.0,<1.41.0)"] +sns = ["mypy-boto3-sns (>=1.40.0,<1.41.0)"] +socialmessaging = ["mypy-boto3-socialmessaging (>=1.40.0,<1.41.0)"] +sqs = ["mypy-boto3-sqs (>=1.40.0,<1.41.0)"] +ssm = ["mypy-boto3-ssm (>=1.40.0,<1.41.0)"] +ssm-contacts = ["mypy-boto3-ssm-contacts (>=1.40.0,<1.41.0)"] +ssm-guiconnect = ["mypy-boto3-ssm-guiconnect (>=1.40.0,<1.41.0)"] +ssm-incidents = ["mypy-boto3-ssm-incidents (>=1.40.0,<1.41.0)"] +ssm-quicksetup = ["mypy-boto3-ssm-quicksetup (>=1.40.0,<1.41.0)"] +ssm-sap = ["mypy-boto3-ssm-sap (>=1.40.0,<1.41.0)"] +sso = ["mypy-boto3-sso (>=1.40.0,<1.41.0)"] +sso-admin = ["mypy-boto3-sso-admin (>=1.40.0,<1.41.0)"] +sso-oidc = ["mypy-boto3-sso-oidc (>=1.40.0,<1.41.0)"] +stepfunctions = ["mypy-boto3-stepfunctions (>=1.40.0,<1.41.0)"] +storagegateway = ["mypy-boto3-storagegateway (>=1.40.0,<1.41.0)"] +sts = ["mypy-boto3-sts (>=1.40.0,<1.41.0)"] +supplychain = ["mypy-boto3-supplychain (>=1.40.0,<1.41.0)"] +support = ["mypy-boto3-support (>=1.40.0,<1.41.0)"] +support-app = ["mypy-boto3-support-app (>=1.40.0,<1.41.0)"] +swf = ["mypy-boto3-swf (>=1.40.0,<1.41.0)"] +synthetics = ["mypy-boto3-synthetics (>=1.40.0,<1.41.0)"] +taxsettings = ["mypy-boto3-taxsettings (>=1.40.0,<1.41.0)"] +textract = ["mypy-boto3-textract (>=1.40.0,<1.41.0)"] +timestream-influxdb = ["mypy-boto3-timestream-influxdb (>=1.40.0,<1.41.0)"] +timestream-query = ["mypy-boto3-timestream-query (>=1.40.0,<1.41.0)"] +timestream-write = ["mypy-boto3-timestream-write (>=1.40.0,<1.41.0)"] +tnb = ["mypy-boto3-tnb (>=1.40.0,<1.41.0)"] +transcribe = ["mypy-boto3-transcribe (>=1.40.0,<1.41.0)"] +transfer = ["mypy-boto3-transfer (>=1.40.0,<1.41.0)"] +translate = ["mypy-boto3-translate (>=1.40.0,<1.41.0)"] +trustedadvisor = ["mypy-boto3-trustedadvisor (>=1.40.0,<1.41.0)"] +verifiedpermissions = ["mypy-boto3-verifiedpermissions (>=1.40.0,<1.41.0)"] +voice-id = ["mypy-boto3-voice-id (>=1.40.0,<1.41.0)"] +vpc-lattice = ["mypy-boto3-vpc-lattice (>=1.40.0,<1.41.0)"] +waf = ["mypy-boto3-waf (>=1.40.0,<1.41.0)"] +waf-regional = ["mypy-boto3-waf-regional (>=1.40.0,<1.41.0)"] +wafv2 = ["mypy-boto3-wafv2 (>=1.40.0,<1.41.0)"] +wellarchitected = ["mypy-boto3-wellarchitected (>=1.40.0,<1.41.0)"] +wisdom = ["mypy-boto3-wisdom (>=1.40.0,<1.41.0)"] +workdocs = ["mypy-boto3-workdocs (>=1.40.0,<1.41.0)"] +workmail = ["mypy-boto3-workmail (>=1.40.0,<1.41.0)"] +workmailmessageflow = ["mypy-boto3-workmailmessageflow (>=1.40.0,<1.41.0)"] +workspaces = ["mypy-boto3-workspaces (>=1.40.0,<1.41.0)"] +workspaces-instances = ["mypy-boto3-workspaces-instances (>=1.40.0,<1.41.0)"] +workspaces-thin-client = ["mypy-boto3-workspaces-thin-client (>=1.40.0,<1.41.0)"] +workspaces-web = ["mypy-boto3-workspaces-web (>=1.40.0,<1.41.0)"] +xray = ["mypy-boto3-xray (>=1.40.0,<1.41.0)"] [[package]] name = "botocore" -version = "1.37.29" +version = "1.40.56" description = "Low-level, data-driven core of boto 3." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "botocore-1.37.29-py3-none-any.whl", hash = "sha256:092c41e346df37a8d7cf60a799791f8225ad3a5ba7cda749047eb31d1440b9c5"}, - {file = "botocore-1.37.29.tar.gz", hash = "sha256:728c1ef3b66a0f79bc08008a59f6fd6bef2a0a0195e5b3b9e9bef255df519890"}, + {file = "botocore-1.40.56-py3-none-any.whl", hash = "sha256:0962dfc9bfb0afa1855042a88a72cc722cc7f9c08f51d2c5c88181d525a59a27"}, + {file = "botocore-1.40.56.tar.gz", hash = "sha256:b29df3418a299609632cab240ee79275463b176ebeb3adc841ba367a3fa0c4db"}, ] [package.dependencies] @@ -570,7 +580,7 @@ python-dateutil = ">=2.1,<3.0.0" urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} [package.extras] -crt = ["awscrt (==0.23.8)"] +crt = ["awscrt (==0.27.6)"] [[package]] name = "botocore-stubs" @@ -1086,38 +1096,62 @@ reports = ["lxml"] [[package]] name = "mypy-boto3-bedrock" -version = "1.37.29" -description = "Type annotations for boto3 Bedrock 1.37.29 service generated with mypy-boto3-builder 8.10.1" +version = "1.40.53" +description = "Type annotations for boto3 Bedrock 1.40.53 service generated with mypy-boto3-builder 8.11.0" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "mypy_boto3_bedrock-1.37.29-py3-none-any.whl", hash = "sha256:17ca9e3131c7d0d4ca900979523efb923696f7a6f47f5626caa4028b1cad2c81"}, - {file = "mypy_boto3_bedrock-1.37.29.tar.gz", hash = "sha256:b8847f0d79658de4d9b6c043fee4fb77a3d27195f4d9525dc420ad02f8907ba6"}, + {file = "mypy_boto3_bedrock-1.40.53-py3-none-any.whl", hash = "sha256:bd082fb3974be6b24f2e7513ec516a051552413148326ee58a7cbfb3e07a8a70"}, + {file = "mypy_boto3_bedrock-1.40.53.tar.gz", hash = "sha256:bee419d0080881748d15742bc528f5d534fddd0a98e8d4f5e4ce8876eef932db"}, +] + +[[package]] +name = "mypy-boto3-bedrock-agent" +version = "1.40.11" +description = "Type annotations for boto3 AgentsforBedrock 1.40.11 service generated with mypy-boto3-builder 8.11.0" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "mypy_boto3_bedrock_agent-1.40.11-py3-none-any.whl", hash = "sha256:b5788e43d31c49b3caa17b6d059c501ea0854f670a5a9b2940e48ac2516b7c7a"}, + {file = "mypy_boto3_bedrock_agent-1.40.11.tar.gz", hash = "sha256:4acb7be8a58124c9214fac4175ec4348e5647f29ab42f29f398f57e64bc9310f"}, ] [[package]] name = "mypy-boto3-bedrock-agent-runtime" -version = "1.37.22" -description = "Type annotations for boto3 AgentsforBedrockRuntime 1.37.22 service generated with mypy-boto3-builder 8.10.1" +version = "1.40.40" +description = "Type annotations for boto3 AgentsforBedrockRuntime 1.40.40 service generated with mypy-boto3-builder 8.11.0" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "mypy_boto3_bedrock_agent_runtime-1.37.22-py3-none-any.whl", hash = "sha256:402d424a9080ea3b8f6d353b9b50110a80758a44d8fa8c9b91d065459676a886"}, - {file = "mypy_boto3_bedrock_agent_runtime-1.37.22.tar.gz", hash = "sha256:5b70873103fd14862dca9cd8b38075bedbe7747c171ecfe3ea99830b9d9e4d91"}, + {file = "mypy_boto3_bedrock_agent_runtime-1.40.40-py3-none-any.whl", hash = "sha256:9be6f0faa4945327b62cf9e4297bb6abe4638621e37c391dc24014702219b6cb"}, + {file = "mypy_boto3_bedrock_agent_runtime-1.40.40.tar.gz", hash = "sha256:674049ed09614f69beb9a83c4553f07a985da8bfab772fb5ca8bb8092abe6d51"}, ] [[package]] name = "mypy-boto3-bedrock-runtime" -version = "1.37.29" -description = "Type annotations for boto3 BedrockRuntime 1.37.29 service generated with mypy-boto3-builder 8.10.1" +version = "1.40.41" +description = "Type annotations for boto3 BedrockRuntime 1.40.41 service generated with mypy-boto3-builder 8.11.0" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "mypy_boto3_bedrock_runtime-1.37.29-py3-none-any.whl", hash = "sha256:a9ded373ff75c988d467f68fe63aa17dd5f78761e6ebfd74fd885c5c0abd7452"}, - {file = "mypy_boto3_bedrock_runtime-1.37.29.tar.gz", hash = "sha256:bfceffdd5c60c4f1845c2933454a33f99c9af6cfad2ab6c9a47be1b769a84e4c"}, + {file = "mypy_boto3_bedrock_runtime-1.40.41-py3-none-any.whl", hash = "sha256:d65dff200986ff06c6b3579ddcea102555f2067c8987fca379bf4f9ed8ba3121"}, + {file = "mypy_boto3_bedrock_runtime-1.40.41.tar.gz", hash = "sha256:ee9bda6d6d478c8d0995e84e884bdf1798e150d437974ae27c175774a58ffaa5"}, +] + +[[package]] +name = "mypy-boto3-cloudformation" +version = "1.40.44" +description = "Type annotations for boto3 CloudFormation 1.40.44 service generated with mypy-boto3-builder 8.11.0" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "mypy_boto3_cloudformation-1.40.44-py3-none-any.whl", hash = "sha256:64c8fe58ab7661fbb0bdea07c7375d3ebc3875760140feb6ad8f591a08a22647"}, + {file = "mypy_boto3_cloudformation-1.40.44.tar.gz", hash = "sha256:3d82f5504382c86ad195a1b80a2a82f73587c37e1b636864ebb85dd43bd79a5b"}, ] [[package]] @@ -1540,14 +1574,14 @@ pyasn1 = ">=0.1.3" [[package]] name = "s3transfer" -version = "0.11.4" +version = "0.14.0" description = "An Amazon S3 Transfer Manager" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "s3transfer-0.11.4-py3-none-any.whl", hash = "sha256:ac265fa68318763a03bf2dc4f39d5cbd6a9e178d81cc9483ad27da33637e320d"}, - {file = "s3transfer-0.11.4.tar.gz", hash = "sha256:559f161658e1cf0a911f45940552c696735f5c74e64362e515f333ebed87d679"}, + {file = "s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456"}, + {file = "s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125"}, ] [package.dependencies] @@ -1826,4 +1860,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = "^3.13.0" -content-hash = "9651eeef0b858279fa35bf59d944566c804ab1575244eec69a68b5847488d91f" +content-hash = "394b826a08230c594b04dde5ccdd3b8f7f544c2cf1e8eba4d16841c486f3092d" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 5d274a4e5..be6c7e92a 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -20,7 +20,7 @@ types-retry = ">=0.9.9.4,<1" opensearch-py = ">=2.0.0" requests-aws4auth = ">=1.0.0" duckduckgo-search = "^7.3.0" -boto3-stubs = {extras = ["bedrock", "bedrock-agent-runtime", "bedrock-runtime", "boto3"], version = "^1.37.0"} +boto3-stubs = {extras = ["bedrock", "bedrock-agent", "bedrock-agent-runtime", "bedrock-runtime", "boto3", "cloudformation"], version = "^1.40.56"} firecrawl-py = "^1.11.1" reretry = "^0.11.8" diff --git a/cdk/lib/bedrock-chat-stack.ts b/cdk/lib/bedrock-chat-stack.ts index 01254af57..b7340053c 100644 --- a/cdk/lib/bedrock-chat-stack.ts +++ b/cdk/lib/bedrock-chat-stack.ts @@ -220,6 +220,15 @@ export class BedrockChatStack extends cdk.Stack { sourceDatabase: database, }); + const embedding = new Embedding(this, "Embedding", { + bedrockRegion: props.bedrockRegion, + database, + documentBucket: props.documentBucket, + bedrockCustomBotProject: bedrockCustomBotCodebuild.project, + bedrockSharedKnowledgeBasesProject: bedrockSharedKnowledgeBasesCodebuild.project, + enableRagReplicas: props.enableRagReplicas, + }); + const backendApi = new Api(this, "BackendApi", { envName: props.envName, envPrefix: props.envPrefix, @@ -230,6 +239,7 @@ export class BedrockChatStack extends cdk.Stack { apiPublishProject: apiPublishCodebuild.project, bedrockCustomBotProject: bedrockCustomBotCodebuild.project, bedrockSharedKnowledgeBasesProject: bedrockSharedKnowledgeBasesCodebuild.project, + embeddingStateMachine: embedding.stateMachine, usageAnalysis, largeMessageBucket, enableBedrockCrossRegionInference: @@ -306,15 +316,6 @@ export class BedrockChatStack extends cdk.Stack { maxAge: 3000, }); - const embedding = new Embedding(this, "Embedding", { - bedrockRegion: props.bedrockRegion, - database, - documentBucket: props.documentBucket, - bedrockCustomBotProject: bedrockCustomBotCodebuild.project, - bedrockSharedKnowledgeBasesProject: bedrockSharedKnowledgeBasesCodebuild.project, - enableRagReplicas: props.enableRagReplicas, - }); - // WebAcl for published API const webAclForPublishedApi = new WebAclForPublishedApi( this, diff --git a/cdk/lib/constructs/api.ts b/cdk/lib/constructs/api.ts index bcf19610a..43e0f8d27 100644 --- a/cdk/lib/constructs/api.ts +++ b/cdk/lib/constructs/api.ts @@ -22,6 +22,7 @@ import * as logs from "aws-cdk-lib/aws-logs"; import * as path from "path"; import { IBucket } from "aws-cdk-lib/aws-s3"; import * as codebuild from "aws-cdk-lib/aws-codebuild"; +import * as sfn from "aws-cdk-lib/aws-stepfunctions"; import { UsageAnalysis } from "./usage-analysis"; import { excludeDockerImage } from "../constants/docker"; import { PythonFunction } from "@aws-cdk/aws-lambda-python-alpha"; @@ -39,6 +40,7 @@ export interface ApiProps { readonly apiPublishProject: codebuild.IProject; readonly bedrockCustomBotProject: codebuild.IProject; readonly bedrockSharedKnowledgeBasesProject: codebuild.IProject; + readonly embeddingStateMachine: sfn.IStateMachine; readonly usageAnalysis?: UsageAnalysis; readonly enableBedrockCrossRegionInference: boolean; readonly enableLambdaSnapStart: boolean; @@ -90,6 +92,7 @@ export class Api extends Construct { ], }) ); + props.embeddingStateMachine.grantStartExecution(handlerRole); handlerRole.addToPolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, @@ -257,8 +260,7 @@ export class Api extends Construct { DOCUMENT_BUCKET: props.documentBucket.bucketName, LARGE_MESSAGE_BUCKET: props.largeMessageBucket.bucketName, PUBLISH_API_CODEBUILD_PROJECT_NAME: props.apiPublishProject.projectName, - // KNOWLEDGE_BASE_CODEBUILD_PROJECT_NAME: - // props.bedrockCustomBotProject.projectName, + EMBEDDING_STATE_MACHINE_ARN: props.embeddingStateMachine.stateMachineArn, USAGE_ANALYSIS_DATABASE: props.usageAnalysis?.database.databaseName || "", USAGE_ANALYSIS_TABLE: diff --git a/cdk/lib/constructs/database.ts b/cdk/lib/constructs/database.ts index 00adbe46b..67dc39c24 100644 --- a/cdk/lib/constructs/database.ts +++ b/cdk/lib/constructs/database.ts @@ -80,18 +80,6 @@ export class Database extends Construct { partitionKey: { name: "ItemType", type: AttributeType.STRING }, }); // GSI-4 - botTable.addGlobalSecondaryIndex({ - indexName: "KnowledgeBaseTypeIndex", - partitionKey: { - name: "BedrockKnowledgeBaseType", - type: AttributeType.STRING, - }, - sortKey: { - name: "BedrockKnowledgeBaseHash", - type: AttributeType.STRING, - }, - }); - // GSI-5 botTable.addGlobalSecondaryIndex({ indexName: "SyncStatusIndex", partitionKey: { diff --git a/cdk/lib/constructs/embedding.ts b/cdk/lib/constructs/embedding.ts index 65e2b5f4a..ce4bcb232 100644 --- a/cdk/lib/constructs/embedding.ts +++ b/cdk/lib/constructs/embedding.ts @@ -28,25 +28,24 @@ export interface EmbeddingProps { } export class Embedding extends Construct { + readonly stateMachine: sfn.StateMachine; readonly removalHandler: IFunction; + private _updateSyncStatusHandler: IFunction; private _bootstrapStateMachineHandler: IFunction; private _finalizeCustomBotBuildHandler: IFunction; private _finalizeSharedKnowledgeBasesBuildHandler: IFunction; - private _stateMachine: sfn.StateMachine; - private _removalHandler: IFunction; + private _synchronizeDataSourceHandler: IFunction; constructor(scope: Construct, id: string, props: EmbeddingProps) { super(scope, id); - this.setupStateMachineHandlers(props) - .setupStateMachine(props) - .setupRemovalHandler(props); - - this.removalHandler = this._removalHandler; + this.setupStateMachineHandlers(props); + this.stateMachine = this.setupStateMachine(props) + this.removalHandler = this.setupRemovalHandler(props); } - private setupStateMachineHandlers(props: EmbeddingProps): this { + private setupStateMachineHandlers(props: EmbeddingProps) { const handlerRole = new iam.Role(this, "HandlerRole", { assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"), }); @@ -199,14 +198,34 @@ export class Embedding extends Construct { logRetention: logs.RetentionDays.THREE_MONTHS, } ); - return this; + + this._synchronizeDataSourceHandler = new DockerImageFunction(this, "SynchronizeDataSourceHandler", { + code: DockerImageCode.fromImageAsset(path.join(__dirname, "../../../backend"), { + platform: Platform.LINUX_AMD64, + file: "lambda.Dockerfile", + cmd: [ + "embedding_statemachine.bedrock_knowledge_base.synchronize_data_source.handler", + ], + exclude: [...excludeDockerImage], + }), + memorySize: 512, + timeout: Duration.minutes(15), + environment: { + ACCOUNT: Stack.of(this).account, + REGION: Stack.of(this).region, + BEDROCK_REGION: props.bedrockRegion, + DOCUMENT_BUCKET: props.documentBucket.bucketName, + }, + role: handlerRole, + logRetention: logs.RetentionDays.THREE_MONTHS, + }); } - private setupStateMachine(props: EmbeddingProps): this { + private setupStateMachine(props: EmbeddingProps): sfn.StateMachine { const bootstrapStateMachine = new tasks.LambdaInvoke(this, "BootstrapStateMachine", { lambdaFunction: this._bootstrapStateMachineHandler, resultSelector: { - Bots: sfn.JsonPath.objectAt("$.Payload.Bots"), + QueuedBots: sfn.JsonPath.objectAt("$.Payload.QueuedBots"), SharedKnowledgeBases: sfn.JsonPath.objectAt("$.Payload.SharedKnowledgeBases"), }, }); @@ -217,7 +236,7 @@ export class Embedding extends Construct { environmentVariablesOverride: { SHARED_KNOWLEDGE_BASES: { type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, - value: sfn.JsonPath.stringAt("States.JsonToString($.SharedKnowledgeBases)"), + value: sfn.JsonPath.stringAt("States.JsonToString($.SharedKnowledgeBases.KnowledgeBases)"), }, // Bucket name provisioned by the bedrock stack BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME: { @@ -231,6 +250,7 @@ export class Embedding extends Construct { }, resultPath: "$.Build", }); + // buildSharedKnowledgeBases.addCatch(fallback); const updateSyncStatusRunning = this.createUpdateSyncStatusTask( "UpdateSyncStatusRunning", @@ -243,26 +263,6 @@ export class Embedding extends Construct { "Knowledge base sync succeeded" ); - // const updateSyncStatusFailed = new tasks.LambdaInvoke( - // this, - // "UpdateSyncStatusFailed", - // { - // lambdaFunction: this._updateSyncStatusHandler, - // payload: sfn.TaskInput.fromObject({ - // "cause.$": "$.Cause", - // }), - // resultPath: sfn.JsonPath.DISCARD, - // } - // ); - - // const fallback = updateSyncStatusFailed.next( - // new sfn.Fail(this, "Fail", { - // cause: "Knowledge base sync failed", - // error: "Knowledge base sync failed", - // }) - // ); - // buildSharedKnowledgeBases.addCatch(fallback); - const finalizeSharedKnowledgeBasesBuild = new tasks.LambdaInvoke(this, "FinalizeSharedKnowledgeBasesBuild", { lambdaFunction: this._finalizeSharedKnowledgeBasesBuildHandler, resultSelector: { @@ -272,100 +272,26 @@ export class Embedding extends Construct { }); // finalizeSharedKnowledgeBasesBuild.addCatch(fallback); - const startIngestionJobForSharedKnowledgeBases = new tasks.CallAwsServiceCrossRegion(this, "StartIngestionJobstartIngestionJobForSharedKnowledgeBases", { - service: "bedrock-agent", - action: "startIngestionJob", - iamAction: "bedrock:StartIngestionJob", - region: props.bedrockRegion, - parameters: { - dataSourceId: sfn.JsonPath.stringAt("$.DataSourceId"), - knowledgeBaseId: sfn.JsonPath.stringAt("$.KnowledgeBaseId"), - }, - // Ref: https://docs.aws.amazon.com/ja_jp/service-authorization/latest/reference/list_amazonbedrock.html#amazonbedrock-knowledge-base - iamResources: [ - `arn:${Stack.of(this).partition}:bedrock:${props.bedrockRegion}:${ - Stack.of(this).account - }:knowledge-base/*`, - ], - resultPath: "$.IngestionJob", - }); - - const getIngestionJobForSharedKnowledgeBases = new tasks.CallAwsServiceCrossRegion(this, "GetIngestionJobForSharedKnowledgeBases", { - service: "bedrock-agent", - action: "getIngestionJob", - iamAction: "bedrock:GetIngestionJob", - region: props.bedrockRegion, - parameters: { - dataSourceId: sfn.JsonPath.stringAt( - "$.IngestionJob.ingestionJob.dataSourceId" - ), - knowledgeBaseId: sfn.JsonPath.stringAt( - "$.IngestionJob.ingestionJob.knowledgeBaseId" - ), - ingestionJobId: sfn.JsonPath.stringAt( - "$.IngestionJob.ingestionJob.ingestionJobId" - ), - }, - // Ref: https://docs.aws.amazon.com/ja_jp/service-authorization/latest/reference/list_amazonbedrock.html#amazonbedrock-knowledge-base - iamResources: [ - `arn:${Stack.of(this).partition}:bedrock:${props.bedrockRegion}:${ - Stack.of(this).account - }:knowledge-base/*`, - ], - resultPath: "$.IngestionJob", - }); - - const waitTaskForSharedKnowledgeBases = new sfn.Wait(this, "WaitSecondsForSharedKnowledgeBases", { - time: sfn.WaitTime.duration(Duration.seconds(3)), - }); - - const checkIngestionJobStatusForSharedKnowledgeBases = new sfn.Choice(this, "CheckIngestionJobStatusForSharedKnowledgeBases") - .when( - sfn.Condition.stringEquals( - "$.IngestionJob.ingestionJob.status", - "COMPLETE" - ), - new sfn.Pass(this, "IngestionJobCompletedForSharedKnowledgeBases") - ) - .when( - sfn.Condition.stringEquals( - "$.IngestionJob.ingestionJob.status", - "FAILED" - ), - new sfn.Fail(this, "IngestionFailForSharedKnowledgeBases", { - cause: "Ingestion job failed", - error: "Ingestion job failed", - }) - // new tasks.LambdaInvoke(this, "UpdateSyncStatusFailedForIngestion", { - // lambdaFunction: this._updateSyncStatusHandler, - // payload: sfn.TaskInput.fromObject({ - // pk: sfn.JsonPath.stringAt("$.PK"), - // sk: sfn.JsonPath.stringAt("$.SK"), - // ingestion_job: sfn.JsonPath.stringAt("$.IngestionJob"), - // }), - // resultPath: sfn.JsonPath.DISCARD, - // }) - // .next( - // new sfn.Fail(this, "IngestionFail", { - // cause: "Ingestion job failed", - // error: "Ingestion job failed", - // }) - // ) - ) - .otherwise(waitTaskForSharedKnowledgeBases.next(getIngestionJobForSharedKnowledgeBases)); + const dataSourceSynchronizationForSharedKnowledgeBases = this.createDataSourceSynchronizationTask("Shared"); const mapIngestionJobsForSharedKnowledgeBases = new sfn.Map(this, "MapIngestionJobsForSharedKnowledgeBases", { inputPath: "$.StackOutput.DataSources", resultPath: sfn.JsonPath.DISCARD, maxConcurrency: 1, }).itemProcessor( - startIngestionJobForSharedKnowledgeBases - .next(getIngestionJobForSharedKnowledgeBases) - .next(checkIngestionJobStatusForSharedKnowledgeBases) + dataSourceSynchronizationForSharedKnowledgeBases ); - const mapBots = new sfn.Map(this, "MapBots", { - itemsPath: "$.Bots", + const mapQueuedBots = new sfn.Map(this, "MapQueuedBots", { + itemsPath: "$.QueuedBots", + }); + + const updateSyncStatusFailedForDedicated = new tasks.LambdaInvoke(this, "UpdateSyncStatusFailedForDedicated", { + lambdaFunction: this._updateSyncStatusHandler, + payload: sfn.TaskInput.fromObject({ + cause: sfn.JsonPath.stringAt("$.Cause"), + }), + resultPath: sfn.JsonPath.DISCARD, }); const startCustomBotBuild = new tasks.CodeBuildStartBuild( @@ -416,7 +342,7 @@ export class Embedding extends Construct { resultPath: "$.Build", } ); - // startCustomBotBuild.addCatch(fallback); + startCustomBotBuild.addCatch(updateSyncStatusFailedForDedicated); const finalizeCustomBotBuild = new tasks.LambdaInvoke(this, "FinalizeCustomBotBuild", { lambdaFunction: this._finalizeCustomBotBuildHandler, @@ -426,107 +352,16 @@ export class Embedding extends Construct { }, resultPath: "$.StackOutput", }); - // finalizeCustomBotBuild.addCatch(fallback); + finalizeCustomBotBuild.addCatch(updateSyncStatusFailedForDedicated); - const startIngestionJob = new tasks.CallAwsServiceCrossRegion( - this, - "StartIngestionJob", - { - service: "bedrock-agent", - action: "startIngestionJob", - iamAction: "bedrock:StartIngestionJob", - region: props.bedrockRegion, - parameters: { - dataSourceId: sfn.JsonPath.stringAt("$.DataSourceId"), - knowledgeBaseId: sfn.JsonPath.stringAt("$.KnowledgeBaseId"), - }, - // Ref: https://docs.aws.amazon.com/ja_jp/service-authorization/latest/reference/list_amazonbedrock.html#amazonbedrock-knowledge-base - iamResources: [ - `arn:${Stack.of(this).partition}:bedrock:${props.bedrockRegion}:${ - Stack.of(this).account - }:knowledge-base/*`, - ], - resultPath: "$.IngestionJob", - } - ); - - const getIngestionJob = new tasks.CallAwsServiceCrossRegion( - this, - "GetIngestionJob", - { - service: "bedrock-agent", - action: "getIngestionJob", - iamAction: "bedrock:GetIngestionJob", - region: props.bedrockRegion, - parameters: { - dataSourceId: sfn.JsonPath.stringAt( - "$.IngestionJob.ingestionJob.dataSourceId" - ), - knowledgeBaseId: sfn.JsonPath.stringAt( - "$.IngestionJob.ingestionJob.knowledgeBaseId" - ), - ingestionJobId: sfn.JsonPath.stringAt( - "$.IngestionJob.ingestionJob.ingestionJobId" - ), - }, - // Ref: https://docs.aws.amazon.com/ja_jp/service-authorization/latest/reference/list_amazonbedrock.html#amazonbedrock-knowledge-base - iamResources: [ - `arn:${Stack.of(this).partition}:bedrock:${props.bedrockRegion}:${ - Stack.of(this).account - }:knowledge-base/*`, - ], - resultPath: "$.IngestionJob", - } - ); - - const waitTask = new sfn.Wait(this, "WaitSeconds", { - time: sfn.WaitTime.duration(Duration.seconds(3)), - }); - - const checkIngestionJobStatus = new sfn.Choice( - this, - "CheckIngestionJobStatus" - ) - .when( - sfn.Condition.stringEquals( - "$.IngestionJob.ingestionJob.status", - "COMPLETE" - ), - new sfn.Pass(this, "IngestionJobCompleted") - ) - .when( - sfn.Condition.stringEquals( - "$.IngestionJob.ingestionJob.status", - "FAILED" - ), - new sfn.Fail(this, "IngestionFail", { - cause: "Ingestion job failed", - error: "Ingestion job failed", - }) - // new tasks.LambdaInvoke(this, "UpdateSyncStatusFailedForIngestion", { - // lambdaFunction: this._updateSyncStatusHandler, - // payload: sfn.TaskInput.fromObject({ - // pk: sfn.JsonPath.stringAt("$.PK"), - // sk: sfn.JsonPath.stringAt("$.SK"), - // ingestion_job: sfn.JsonPath.stringAt("$.IngestionJob"), - // }), - // resultPath: sfn.JsonPath.DISCARD, - // }) - // .next( - // new sfn.Fail(this, "IngestionFail", { - // cause: "Ingestion job failed", - // error: "Ingestion job failed", - // }) - // ) - ) - .otherwise(waitTask.next(getIngestionJob)); + const dataSourceSynchronizationForDedicatedKnowledgeBases = this.createDataSourceSynchronizationTask("Dedicated"); const mapIngestionJobs = new sfn.Map(this, "MapIngestionJobs", { inputPath: "$.StackOutput.DataSources", resultPath: sfn.JsonPath.DISCARD, maxConcurrency: 1, }).itemProcessor( - startIngestionJob.next(getIngestionJob).next(checkIngestionJobStatus) + dataSourceSynchronizationForDedicatedKnowledgeBases ); const definition = bootstrapStateMachine @@ -534,7 +369,7 @@ export class Embedding extends Construct { .next(finalizeSharedKnowledgeBasesBuild) .next(mapIngestionJobsForSharedKnowledgeBases) .next( - mapBots.itemProcessor( + mapQueuedBots.itemProcessor( updateSyncStatusRunning .next(startCustomBotBuild) .next(finalizeCustomBotBuild) @@ -543,20 +378,12 @@ export class Embedding extends Construct { ) ) - // const definition = extractFirstElement - // .next(updateSyncStatusRunning) - // .next(startCustomBotBuild) - // .next(finalizeCustomBotBuild) - // .next(mapIngestionJobs) - // .next(updateSyncStatusSucceeded); - - this._stateMachine = new sfn.StateMachine(this, "StateMachine", { + return new sfn.StateMachine(this, "StateMachine", { definitionBody: sfn.DefinitionBody.fromChainable(definition), }); - return this; } - private setupRemovalHandler(props: EmbeddingProps): this { + private setupRemovalHandler(props: EmbeddingProps) { const removeHandlerRole = new iam.Role(this, "RemovalHandlerRole", { assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"), }); @@ -615,7 +442,7 @@ export class Embedding extends Construct { props.database.botTable.grantStreamRead(removeHandlerRole); props.documentBucket.grantReadWrite(removeHandlerRole); - this._removalHandler = new DockerImageFunction(this, "BotRemovalHandler", { + const removalHandler = new DockerImageFunction(this, "BotRemovalHandler", { code: DockerImageCode.fromImageAsset( path.join(__dirname, "../../../backend"), { @@ -638,7 +465,7 @@ export class Embedding extends Construct { role: removeHandlerRole, logRetention: logs.RetentionDays.THREE_MONTHS, }); - this._removalHandler.addEventSource( + removalHandler.addEventSource( new DynamoEventSource(props.database.botTable, { startingPosition: lambda.StartingPosition.TRIM_HORIZON, batchSize: 1, @@ -651,7 +478,7 @@ export class Embedding extends Construct { }) ); - return this; + return removalHandler; } private createUpdateSyncStatusTask( @@ -677,4 +504,47 @@ export class Embedding extends Construct { resultPath: sfn.JsonPath.DISCARD, }); } + + private createDataSourceSynchronizationTask(name: string): sfn.IChainable { + const startIngestionJob = new tasks.LambdaInvoke(this, `StartIngestionJob${name}`, { + lambdaFunction: this._synchronizeDataSourceHandler, + payload: sfn.TaskInput.fromObject({ + Action: "Ingest", + KnowledgeBaseId: sfn.JsonPath.stringAt("$.KnowledgeBaseId"), + DataSourceId: sfn.JsonPath.stringAt("$.DataSourceId"), + Files: sfn.JsonPath.objectAt("$.Files"), + }), + resultSelector: { + KnowledgeBaseId: sfn.JsonPath.stringAt("$.Payload.KnowledgeBaseId"), + DataSourceId: sfn.JsonPath.stringAt("$.Payload.DataSourceId"), + Files: sfn.JsonPath.objectAt("$.Payload.Files"), + IngestionJobId: sfn.JsonPath.stringAt("$.Payload.IngestionJobId"), + }, + resultPath: "$.IngestionJob", + }); + + const checkIngestionJob = new tasks.LambdaInvoke(this, `CheckIngestionJob${name}`, { + lambdaFunction: this._synchronizeDataSourceHandler, + payload: sfn.TaskInput.fromObject({ + Action: "Check", + IngestionJob: sfn.JsonPath.objectAt("$.IngestionJob"), + }), + resultPath: sfn.JsonPath.DISCARD, + }); + + const ingestionComplete = new sfn.Pass(this, `IngestionComplete${name}`); + return startIngestionJob + .next( + checkIngestionJob.addRetry({ + interval: Duration.seconds(15), + maxAttempts: 12 * 60 * 60 / 15, + backoffRate: 1, + errors: [ + 'RetryException', + ], + }).addCatch(ingestionComplete, { + resultPath: sfn.JsonPath.stringAt('$.Error'), + }) + ).next(ingestionComplete) + } } diff --git a/frontend/src/features/knowledgeBase/pages/BotKbEditPage.tsx b/frontend/src/features/knowledgeBase/pages/BotKbEditPage.tsx index 977cf7d3c..c857ac402 100644 --- a/frontend/src/features/knowledgeBase/pages/BotKbEditPage.tsx +++ b/frontend/src/features/knowledgeBase/pages/BotKbEditPage.tsx @@ -129,7 +129,7 @@ const BotKbEditPage: React.FC = () => { >(null); const [knowledgeBaseType, setKnowledgeBaseType] = useState< 'new' | 'shared' | 'existing' - >('new'); + >('shared'); const bedrockKnowledgeBaseType = useMemo(() => { if (existKnowledgeBaseId != null) { @@ -516,8 +516,17 @@ const BotKbEditPage: React.FC = () => { setS3Urls( bot.knowledge.s3Urls.length === 0 ? [''] : bot.knowledge.s3Urls ); - if (bot.bedrockKnowledgeBase.type === 'shared') { - setKnowledgeBaseType('shared'); + switch (bot.bedrockKnowledgeBase.type) { + case 'dedicated': + setKnowledgeBaseType('new'); + break; + + case 'shared': + setKnowledgeBaseType('shared'); + break; + + default: + break; } setFiles( bot.knowledge.filenames.map((filename) => ({ @@ -1359,6 +1368,7 @@ const BotKbEditPage: React.FC = () => { promptCachingEnabled, conversationQuickStarters, navigate, + bedrockKnowledgeBaseType, knowledgeBaseId, existKnowledgeBaseId, embeddingsModel, @@ -1367,6 +1377,7 @@ const BotKbEditPage: React.FC = () => { hierarchicalParams, semanticParams, openSearchParams, + isGuardrailEnabled, hateThreshold, insultsThreshold, sexualThreshold, @@ -1488,6 +1499,7 @@ const BotKbEditPage: React.FC = () => { promptCachingEnabled, conversationQuickStarters, navigate, + bedrockKnowledgeBaseType, knowledgeBaseId, existKnowledgeBaseId, embeddingsModel, @@ -1496,6 +1508,7 @@ const BotKbEditPage: React.FC = () => { hierarchicalParams, semanticParams, openSearchParams, + isGuardrailEnabled, hateThreshold, insultsThreshold, sexualThreshold, @@ -1593,6 +1606,7 @@ const BotKbEditPage: React.FC = () => { 'knowledgeBaseSettings.advancedConfigration.existKnowledgeBaseId.createNewKb.label' )} onChange={() => setKnowledgeBaseType('new')} + disabled /> Date: Tue, 28 Oct 2025 18:00:03 +0900 Subject: [PATCH 4/9] Add exclusive lock to embedding state machine. --- backend/app/repositories/custom_bot.py | 4 - .../app/repositories/models/custom_bot_kb.py | 3 +- backend/app/routes/schemas/bot.py | 18 +- backend/app/usecases/bot.py | 4 +- backend/app/utils.py | 4 +- .../bootstrap_state_machine.py | 86 ++-- .../finalize_custom_bot_build.py | 119 ++--- .../finalize_shared_knowledge_bases_build.py | 144 +++--- .../bedrock_knowledge_base/lock.py | 98 ++++ .../synchronize_data_source.py | 286 ++++++----- .../update_bot_status.py | 149 +++--- .../index.ts | 13 +- cdk/lib/constructs/api-publish-codebuild.ts | 2 +- .../bedrock-custom-bot-codebuild.ts | 2 +- ...edrock-shared-knowledge-bases-codebuild.ts | 2 +- cdk/lib/constructs/embedding.ts | 445 +++++++++++++----- deploy.yml | 2 +- .../knowledgeBase/pages/BotKbEditPage.tsx | 10 +- frontend/src/i18n/en/index.ts | 20 +- frontend/src/i18n/id/index.ts | 17 +- frontend/src/i18n/ja/index.ts | 22 +- frontend/src/i18n/pl/index.ts | 17 +- frontend/src/i18n/pt-br/index.ts | 17 +- 23 files changed, 924 insertions(+), 560 deletions(-) create mode 100644 backend/embedding_statemachine/bedrock_knowledge_base/lock.py diff --git a/backend/app/repositories/custom_bot.py b/backend/app/repositories/custom_bot.py index ec166e586..8a16f221b 100644 --- a/backend/app/repositories/custom_bot.py +++ b/backend/app/repositories/custom_bot.py @@ -139,7 +139,6 @@ def update_bot( "ConversationQuickStarters = :conversation_quick_starters, " "ActiveModels = :active_models" ) - remove_attributes: list[str] = [] expression_attribute_values = { ":title": title, @@ -186,9 +185,6 @@ def update_bot( ) try: - if len(remove_attributes) > 0: - update_expression += " REMOVE " + ", ".join(remove_attributes) - response = table.update_item( Key={"PK": owner_user_id, "SK": compose_sk(bot_id, "bot")}, UpdateExpression=update_expression, diff --git a/backend/app/repositories/models/custom_bot_kb.py b/backend/app/repositories/models/custom_bot_kb.py index 0517864fc..94bf71411 100644 --- a/backend/app/repositories/models/custom_bot_kb.py +++ b/backend/app/repositories/models/custom_bot_kb.py @@ -109,7 +109,8 @@ def calc_knowledge_base_hash(knowledge_base: BedrockKnowledgeBaseModel) -> str: "exist_knowledge_base_id", "data_source_ids", } - ).encode() + ).encode(), + usedforsecurity=False, ).digest() ) .decode() diff --git a/backend/app/routes/schemas/bot.py b/backend/app/routes/schemas/bot.py index 5cb78f23c..2a783e65c 100644 --- a/backend/app/routes/schemas/bot.py +++ b/backend/app/routes/schemas/bot.py @@ -32,6 +32,11 @@ validator, ) +from app.repositories.models.custom_bot_kb import ( + BedrockKnowledgeBaseModel, + calc_knowledge_base_hash, +) + if TYPE_CHECKING: from app.repositories.models.custom_bot import BotModel @@ -355,10 +360,15 @@ def is_embedding_required(self, current_bot_model: BotModel) -> bool: self.bedrock_knowledge_base is not None and current_bot_model.bedrock_knowledge_base is not None ): - if ( - self.bedrock_knowledge_base.type - != current_bot_model.bedrock_knowledge_base.type - ): + knowledge_base_hash = calc_knowledge_base_hash( + BedrockKnowledgeBaseModel.model_validate( + self.bedrock_knowledge_base.model_dump() + ) + ) + current_knowledge_base_hash = calc_knowledge_base_hash( + current_bot_model.bedrock_knowledge_base + ) + if knowledge_base_hash != current_knowledge_base_hash: return True return False diff --git a/backend/app/usecases/bot.py b/backend/app/usecases/bot.py index f32fe63c1..eb72296da 100644 --- a/backend/app/usecases/bot.py +++ b/backend/app/usecases/bot.py @@ -148,7 +148,8 @@ def create_new_bot(user: User, bot_input: BotInput) -> BotOutput: start_embedding_state_machine( user_id=user.id, bot_id=new_bot.id, - added_filenames=[], + added_filenames=filenames, + unchanged_filenames=[], deleted_filenames=[], ) @@ -286,6 +287,7 @@ def modify_owned_bot( user_id=user.id, bot_id=bot.id, added_filenames=added_filenames, + unchanged_filenames=unchanged_filenames, deleted_filenames=deleted_filenames, ) diff --git a/backend/app/utils.py b/backend/app/utils.py index 447bcae91..403b742bc 100644 --- a/backend/app/utils.py +++ b/backend/app/utils.py @@ -319,6 +319,7 @@ def start_embedding_state_machine( user_id: str, bot_id: str, added_filenames: list[str], + unchanged_filenames: list[str], deleted_filenames: list[str], ): client = boto3.client("stepfunctions") @@ -330,8 +331,9 @@ def start_embedding_state_machine( { "OwnerUserId": user_id, "BotId": bot_id, - "Files": { + "FilesDiff": { "Added": added_filenames, + "Unchanged": unchanged_filenames, "Deleted": deleted_filenames, }, }, diff --git a/backend/embedding_statemachine/bedrock_knowledge_base/bootstrap_state_machine.py b/backend/embedding_statemachine/bedrock_knowledge_base/bootstrap_state_machine.py index bdd3badd0..791d201ce 100644 --- a/backend/embedding_statemachine/bedrock_knowledge_base/bootstrap_state_machine.py +++ b/backend/embedding_statemachine/bedrock_knowledge_base/bootstrap_state_machine.py @@ -21,7 +21,7 @@ def handler(event, context): else: queued_bots = get_queued_bots() - shared_knowledge_bases = find_shared_knowledge_bases(queued_bots) + shared_knowledge_bases = find_shared_knowledge_bases() return { "QueuedBots": [ @@ -30,9 +30,9 @@ def handler(event, context): "BotId": queued_bot["bot"].id, **( { - "Files": queued_bot["files"], + "FilesDiff": queued_bot["files_diff"], } - if "files" in queued_bot + if "files_diff" in queued_bot else {} ), "Knowledge": queued_bot["bot"].knowledge.model_dump(), @@ -62,52 +62,31 @@ def handler(event, context): } for queued_bot in queued_bots ], - "SharedKnowledgeBases": { - "KnowledgeBases": [ - { - "KnowledgeBaseHash": shared_knowledge_base["knowledge_base_hash"], - "KnowledgeBase": shared_knowledge_base["knowledge_base"].model_dump( - exclude={ - "knowledge_base_id", - "exist_knowledge_base_id", - "data_source_ids", - } - ), - } - for shared_knowledge_base in shared_knowledge_bases - ], - "QueuedBotsForKnowledgeBases": [ - { - "KnowledgeBaseHash": shared_knowledge_base["knowledge_base_hash"], - "Bots": [ - { - "OwnerUserId": queued_bot["bot"].owner_user_id, - "BotId": queued_bot["bot"].id, - **( - { - "Files": queued_bot["files"], - } - if "files" in queued_bot - else {} - ), - } - for queued_bot in shared_knowledge_base["queued_bots"] - ], - } - for shared_knowledge_base in shared_knowledge_bases - ], - }, + "SharedKnowledgeBases": [ + { + "KnowledgeBaseHash": shared_knowledge_base["knowledge_base_hash"], + "KnowledgeBase": shared_knowledge_base["knowledge_base"].model_dump( + exclude={ + "knowledge_base_id", + "exist_knowledge_base_id", + "data_source_ids", + } + ), + } + for shared_knowledge_base in shared_knowledge_bases + ], } -class BotFiles(TypedDict): +class FilesDiff(TypedDict): Added: list[str] + Unchanged: list[str] Deleted: list[str] class QueuedBot(TypedDict): bot: BotModel - files: NotRequired[BotFiles] + files_diff: NotRequired[FilesDiff] def get_queued_bots_from_event(queued_bots_from_event: list[dict]) -> list[QueuedBot]: @@ -117,16 +96,18 @@ def get_queued_bots_from_event(queued_bots_from_event: list[dict]) -> list[Queue bot_id = queued_bot.get("BotId") if user_id and bot_id: bot = find_bot_by_id(bot_id) - files = queued_bot.get("Files", {}) + files_diff = queued_bot.get("FilesDiff", {}) - added_files = files.get("Added", []) - deleted_files = files.get("Deleted", []) - if added_files or deleted_files: + added_files = files_diff.get("Added", []) + unchanged_files = files_diff.get("Unchanged", []) + deleted_files = files_diff.get("Deleted", []) + if added_files or unchanged_files or deleted_files: result.append( { "bot": bot, - "files": { + "files_diff": { "Added": added_files, + "Unchanged": unchanged_files, "Deleted": deleted_files, }, } @@ -155,20 +136,14 @@ def get_queued_bots() -> list[QueuedBot]: class SharedKnowledgeBase(TypedDict): knowledge_base_hash: str knowledge_base: BedrockKnowledgeBaseModel - queued_bots: list[QueuedBot] -def find_shared_knowledge_bases( - queued_bots: list[QueuedBot], -) -> list[SharedKnowledgeBase]: +def find_shared_knowledge_bases() -> list[SharedKnowledgeBase]: bot_table = get_bot_table_client() scan_params = { "FilterExpression": Attr("BedrockKnowledgeBase.type").eq("shared"), } - queued_bots_dict = dict( - (queued_bot["bot"].id, queued_bot) for queued_bot in queued_bots - ) knowledge_bases: dict[str, SharedKnowledgeBase] = {} while True: response = bot_table.scan(**scan_params) @@ -183,15 +158,8 @@ def find_shared_knowledge_bases( knowledge_bases[knowledge_base_hash] = { "knowledge_base_hash": knowledge_base_hash, "knowledge_base": bot.bedrock_knowledge_base, - "queued_bots": [], } - queued_bot = queued_bots_dict.get(bot.id) - if queued_bot is not None: - knowledge_bases[knowledge_base_hash]["queued_bots"].append( - queued_bot - ) - last_evaluated_key = response.get("LastEvaluatedKey") if last_evaluated_key is None: break diff --git a/backend/embedding_statemachine/bedrock_knowledge_base/finalize_custom_bot_build.py b/backend/embedding_statemachine/bedrock_knowledge_base/finalize_custom_bot_build.py index 8aec9cdc3..b8e3fa403 100644 --- a/backend/embedding_statemachine/bedrock_knowledge_base/finalize_custom_bot_build.py +++ b/backend/embedding_statemachine/bedrock_knowledge_base/finalize_custom_bot_build.py @@ -9,37 +9,56 @@ BEDROCK_REGION = os.environ.get("BEDROCK_REGION") -cfn = boto3.client("cloudformation", BEDROCK_REGION) +cfn = boto3.client( + service_name="cloudformation", + region_name=BEDROCK_REGION, +) -class DataSourceFiles(TypedDict): +class BotFilesDiff(TypedDict): OwnerUserId: str BotId: str Added: list[str] + Unchanged: list[str] Deleted: list[str] class DataSource(TypedDict): KnowledgeBaseId: str DataSourceId: str - Files: list[DataSourceFiles] + FilesDiffs: list[BotFilesDiff] -class Bot(TypedDict): - OwnerUserId: str - BotId: str +def handler(event, context): + user_id = event["OwnerUserId"] + bot_id = event["BotId"] + bot_files_diffs: list[BotFilesDiff] = [] -class StackOutput(TypedDict): - DataSources: List[DataSource] - Bots: list[Bot] + files_diff_from_event = event.get("FilesDiff") + if files_diff_from_event: + bot_files_diffs.append( + { + "OwnerUserId": user_id, + "BotId": bot_id, + "Added": files_diff_from_event["Added"], + "Unchanged": files_diff_from_event["Unchanged"], + "Deleted": files_diff_from_event["Deleted"], + } + ) + data_sources: list[DataSource] = [] -def handler(event, context) -> StackOutput: - print(event) - user_id = event["OwnerUserId"] - bot_id = event["BotId"] - files = event.get("Files") + data_sources_from_event = event.get("DataSources") + if data_sources_from_event: + data_sources.extend( + { + "KnowledgeBaseId": data_source["KnowledgeBaseId"], + "DataSourceId": data_source["DataSourceId"], + "FilesDiffs": bot_files_diffs, + } + for data_source in data_sources_from_event + ) # Note: stack naming rule is defined on: # cdk/bin/bedrock-custom-bot.ts @@ -50,61 +69,43 @@ def handler(event, context) -> StackOutput: if not outputs: raise ValueError(f"No outputs found in CloudFormation stack '{stack_name}'") - knowledge_base_id = None - data_source_ids: List[str] = [] - - guardrail_arn = None - guardrail_version = None - - for output in outputs: - key = output.get("OutputKey") - value = output.get("OutputValue") - if key and value: - if key == "KnowledgeBaseId": - knowledge_base_id = value - - elif key.startswith("DataSource"): - data_source_ids.append(value) - - elif key == "GuardrailArn": - guardrail_arn = value - - elif key == "GuardrailVersion": - guardrail_version = value - - result: StackOutput = { - "DataSources": [], - "Bots": [ - { - "OwnerUserId": user_id, - "BotId": bot_id, - }, - ], - } + stack_outputs = dict( + (output["OutputKey"], output["OutputValue"]) + for output in outputs + if "OutputKey" in output and "OutputValue" in output + ) + knowledge_base_id = stack_outputs.get("KnowledgeBaseId") if knowledge_base_id: - result["DataSources"].extend( + data_source_ids: List[str] = [ + value + for key, value in stack_outputs.items() + if key.startswith(f"DataSource") + ] + data_sources.extend( { "KnowledgeBaseId": knowledge_base_id, "DataSourceId": data_source_id, - "Files": ( - [ - { - "OwnerUserId": user_id, - "BotId": bot_id, - "Added": files["Added"], - "Deleted": files["Deleted"], - } - ] - if files is not None - else [] - ), + "FilesDiffs": bot_files_diffs, } for data_source_id in data_source_ids ) update_knowledge_base_id(user_id, bot_id, knowledge_base_id, data_source_ids) + guardrail_arn = stack_outputs.get("GuardrailArn") + guardrail_version = stack_outputs.get("GuardrailVersion") if guardrail_arn and guardrail_version: update_guardrails_params(user_id, bot_id, guardrail_arn, guardrail_version) - return result + return { + "OwnerUserId": user_id, + "BotId": bot_id, + "DataSources": data_sources, + **( + { + "Lock": event["Lock"], + } + if "Lock" in event + else {} + ), + } diff --git a/backend/embedding_statemachine/bedrock_knowledge_base/finalize_shared_knowledge_bases_build.py b/backend/embedding_statemachine/bedrock_knowledge_base/finalize_shared_knowledge_bases_build.py index 8f5c97372..6fbe65e25 100644 --- a/backend/embedding_statemachine/bedrock_knowledge_base/finalize_shared_knowledge_bases_build.py +++ b/backend/embedding_statemachine/bedrock_knowledge_base/finalize_shared_knowledge_bases_build.py @@ -6,31 +6,40 @@ BEDROCK_REGION = os.environ.get("BEDROCK_REGION") -cfn = boto3.client("cloudformation", BEDROCK_REGION) +cfn = boto3.client( + service_name="cloudformation", + region_name=BEDROCK_REGION, +) -class DataSourceFiles(TypedDict): +class QueuedBot(TypedDict): + user_id: str + bot_id: str + + +class KnowledgeBase(TypedDict): + knowledge_base_id: str + data_source_ids: list[str] + queued_bots: list[QueuedBot] + + +class BotFilesDiff(TypedDict): OwnerUserId: str BotId: str Added: list[str] + Unchanged: list[str] Deleted: list[str] class DataSource(TypedDict): KnowledgeBaseId: str DataSourceId: str - Files: list[DataSourceFiles] - + FilesDiffs: list[BotFilesDiff] -class StackOutput(TypedDict): - DataSources: List[DataSource] - -def handler(event, context) -> StackOutput: - print(event) - queued_bots_for_knowledge_bases = event["SharedKnowledgeBases"][ - "QueuedBotsForKnowledgeBases" - ] +def handler(event, context): + queued_bots = event["QueuedBots"] + shared_knowledge_bases = event["SharedKnowledgeBases"] # Note: stack naming rule is defined on: # cdk/bin/bedrock-shared-knowledge-bases.ts @@ -41,58 +50,79 @@ def handler(event, context) -> StackOutput: if not outputs: raise ValueError(f"No outputs found in CloudFormation stack '{stack_name}'") - print(outputs) - - result: StackOutput = { - "DataSources": [], - } - - for queued_bots_for_knowledge_base in queued_bots_for_knowledge_bases: - knowledge_base_hash = queued_bots_for_knowledge_base["KnowledgeBaseHash"] - - knowledge_base_id = None - data_source_ids: List[str] = [] + stack_outputs = dict( + (output["OutputKey"], output["OutputValue"]) + for output in outputs + if "OutputKey" in output and "OutputValue" in output + ) - for output in outputs: - key = output.get("OutputKey") - value = output.get("OutputValue") - if key and value: - if key == f"KnowledgeBaseId{knowledge_base_hash}": - knowledge_base_id = value - - elif key.startswith(f"DataSource{knowledge_base_hash}"): - data_source_ids.append(value) + knowledge_bases: dict[str, KnowledgeBase] = {} + for shared_knowledge_base in shared_knowledge_bases: + knowledge_base_hash = shared_knowledge_base["KnowledgeBaseHash"] + knowledge_base_id = stack_outputs.get(f"KnowledgeBaseId{knowledge_base_hash}") if knowledge_base_id: - bots = queued_bots_for_knowledge_base["Bots"] - data_source_files: list[DataSourceFiles] = [] - for bot in bots: - user_id = bot["OwnerUserId"] - bot_id = bot["BotId"] - files = bot.get("Files") - if files is not None: - data_source_files.append( - { - "OwnerUserId": user_id, - "BotId": bot_id, - "Added": files["Added"], - "Deleted": files["Deleted"], - } - ) - - update_knowledge_base_id( - user_id, bot_id, knowledge_base_id, data_source_ids - ) + knowledge_bases[knowledge_base_hash] = { + "knowledge_base_id": knowledge_base_id, + "data_source_ids": [ + value + for key, value in stack_outputs.items() + if key.startswith(f"DataSource{knowledge_base_hash}") + ], + "queued_bots": [], + } + + data_sources: list[DataSource] = [] + + for queued_bot in queued_bots: + knowledge_base_hash = queued_bot.get("KnowledgeBaseHash") + if knowledge_base_hash and knowledge_base_hash in knowledge_bases: + knowledge_base = knowledge_bases[knowledge_base_hash] + if "FilesDiff" in queued_bot: + queued_bot["DataSources"] = [ + { + "KnowledgeBaseId": knowledge_base["knowledge_base_id"], + "DataSourceId": data_source_id, + } + for data_source_id in knowledge_base["data_source_ids"] + ] - result["DataSources"].extend( - ( + else: + data_sources.extend( { - "KnowledgeBaseId": knowledge_base_id, + "KnowledgeBaseId": knowledge_base["knowledge_base_id"], "DataSourceId": data_source_id, - "Files": data_source_files, + "FilesDiffs": [], } - for data_source_id in data_source_ids + for data_source_id in knowledge_base["data_source_ids"] ) + pass + + knowledge_base["queued_bots"].append( + { + "user_id": queued_bot["OwnerUserId"], + "bot_id": queued_bot["BotId"], + } + ) + + for knowledge_base in knowledge_bases.values(): + for queued_bot in knowledge_base["queued_bots"]: + update_knowledge_base_id( + user_id=queued_bot["user_id"], + bot_id=queued_bot["bot_id"], + knowledge_base_id=knowledge_base["knowledge_base_id"], + data_source_ids=knowledge_base["data_source_ids"], ) - return result + return { + "QueuedBots": queued_bots, + "SharedKnowledgeBases": shared_knowledge_bases, + "DataSources": data_sources, + **( + { + "Lock": event["Lock"], + } + if "Lock" in event + else {} + ), + } diff --git a/backend/embedding_statemachine/bedrock_knowledge_base/lock.py b/backend/embedding_statemachine/bedrock_knowledge_base/lock.py new file mode 100644 index 000000000..a7deacc94 --- /dev/null +++ b/backend/embedding_statemachine/bedrock_knowledge_base/lock.py @@ -0,0 +1,98 @@ +import os +from datetime import datetime, timedelta + +import boto3 + +BEDROCK_REGION = os.environ.get("BEDROCK_REGION") +DOCUMENT_BUCKET = os.environ["DOCUMENT_BUCKET"] + +s3 = boto3.client( + service_name="s3", + region_name=BEDROCK_REGION, +) + + +def handler(event, context): + action = event["Action"] + match action: + case "Acquire": + return handle_acquire(event) + + case "Release": + return handle_release(event) + + case _: + raise Exception(f"Invalid action {action}") + + +class RetryException(Exception): + pass + + +def handle_acquire(event): + lock_name = event["LockName"] + owner = event["Owner"] + expires_seconds = event["ExpiresSeconds"] + try: + response = s3.put_object( + Bucket=DOCUMENT_BUCKET, + Key=f".lock.{lock_name.lower()}", + Expires=datetime.now() + timedelta(seconds=expires_seconds), + IfNoneMatch="*", + Body=owner, + ) + etag = response["ETag"] + + return { + "LockId": etag, + } + + except s3.exceptions.ClientError as ex: + error_code = ex.response.get("Error", {}).get("Code") + match error_code: + case "PreconditionFailed": + try: + get_response = s3.get_object( + Bucket=DOCUMENT_BUCKET, + Key=f".lock.{lock_name.lower()}", + ) + body = get_response["Body"].read().decode() + if body != owner: + raise RetryException() + + etag = get_response["ETag"] + return { + "LockId": etag, + } + + except s3.exceptions.NoSuchKey: + raise RetryException() + + case "ConditionalRequestConflict": + raise RetryException() + + case _: + raise ex + + +def handle_release(event): + lock_name = event["LockName"] + lock_id = event["LockId"] + try: + s3.delete_object( + Bucket=DOCUMENT_BUCKET, + Key=f".lock.{lock_name.lower()}", + IfMatch=lock_id, + ) + + except s3.exceptions.ClientError as ex: + error_code = ex.response.get("Error", {}).get("Code") + match error_code: + case "PreconditionFailed": + pass + + case "ConditionalRequestConflict": + raise RetryException() + + case _: + raise ex diff --git a/backend/embedding_statemachine/bedrock_knowledge_base/synchronize_data_source.py b/backend/embedding_statemachine/bedrock_knowledge_base/synchronize_data_source.py index f3f903f08..1866459cb 100644 --- a/backend/embedding_statemachine/bedrock_knowledge_base/synchronize_data_source.py +++ b/backend/embedding_statemachine/bedrock_knowledge_base/synchronize_data_source.py @@ -1,7 +1,11 @@ import os from itertools import islice +from typing import TypedDict -from app.utils import get_bedrock_agent_client +from app.utils import ( + compose_upload_document_s3_path, + get_bedrock_agent_client, +) DOCUMENT_BUCKET = os.environ.get("DOCUMENT_BUCKET") bedrock_agent = get_bedrock_agent_client() @@ -19,63 +23,130 @@ def handler(event, context): raise Exception(f"Unknown action {e}") -def handle_ingest(event): - knowledge_base_id = event["KnowledgeBaseId"] - data_source_id = event["DataSourceId"] +class DocumentsDiff(TypedDict): + Added: list[str] + Deleted: list[str] + + +def compose_upload_document_s3_uri(user_id: str, bot_id: str, filename: str) -> str: + return f"s3://{DOCUMENT_BUCKET}/{compose_upload_document_s3_path(user_id, bot_id, filename)}" + + +def get_data_source_type(knowledge_base_id: str, data_source_id: str): get_data_source_response = bedrock_agent.get_data_source( knowledgeBaseId=knowledge_base_id, dataSourceId=data_source_id, ) - data_source_type = get_data_source_response["dataSource"][ - "dataSourceConfiguration" - ]["type"] - - data_source_files = event.get("Files") - if data_source_type == "S3" and data_source_files: - for data_source_file in data_source_files: - user_id = data_source_file["OwnerUserId"] - bot_id = data_source_file["BotId"] - - added_files = data_source_file["Added"] - for i in range(0, len(added_files), 10): - bedrock_agent.ingest_knowledge_base_documents( - knowledgeBaseId=knowledge_base_id, - dataSourceId=data_source_id, - documents=[ - { - "content": { - "dataSourceType": "S3", - "s3": { - "s3Location": { - "uri": f"s3://{DOCUMENT_BUCKET}/{user_id}/{bot_id}/documents/{file}", - }, - }, - }, - } - for file in islice(added_files, i, i + 10) - ], - ) + return get_data_source_response["dataSource"]["dataSourceConfiguration"]["type"] + + +def handle_ingest(event): + knowledge_base_id = event["KnowledgeBaseId"] + data_source_id = event["DataSourceId"] + + bot_files_diffs = event.get("FilesDiffs") + if ( + bot_files_diffs + and get_data_source_type( + knowledge_base_id=knowledge_base_id, + data_source_id=data_source_id, + ) + == "S3" + ): + added_documents: list[str] = [] + unchanged_documents: list[str] = [] + deleted_documents: list[str] = [] + + for bot_files_diff in bot_files_diffs: + user_id = bot_files_diff["OwnerUserId"] + bot_id = bot_files_diff["BotId"] + + added_documents.extend( + compose_upload_document_s3_uri(user_id, bot_id, added_file) + for added_file in bot_files_diff["Added"] + ) + unchanged_documents.extend( + compose_upload_document_s3_uri(user_id, bot_id, unchanged_file) + for unchanged_file in bot_files_diff["Unchanged"] + ) + deleted_documents.extend( + compose_upload_document_s3_uri(user_id, bot_id, deleted_file) + for deleted_file in bot_files_diff["Deleted"] + ) + + for i in range(0, len(unchanged_documents), 10): + get_documents_response = bedrock_agent.get_knowledge_base_documents( + knowledgeBaseId=knowledge_base_id, + dataSourceId=data_source_id, + documentIdentifiers=[ + { + "dataSourceType": "S3", + "s3": { + "uri": document_identifier, + }, + } + for document_identifier in islice(unchanged_documents, i, i + 10) + ], + ) + added_documents.extend( + document["identifier"]["s3"]["uri"] + for document in get_documents_response["documentDetails"] + if document["status"] == "NOT_FOUND" and "s3" in document["identifier"] + ) - deleted_files = data_source_file["Deleted"] - for i in range(0, len(deleted_files), 10): - bedrock_agent.delete_knowledge_base_documents( - knowledgeBaseId=knowledge_base_id, - dataSourceId=data_source_id, - documentIdentifiers=[ - { + documents_diff: DocumentsDiff = { + "Added": [], + "Deleted": [], + } + + for i in range(0, len(added_documents), 10): + ingest_response = bedrock_agent.ingest_knowledge_base_documents( + knowledgeBaseId=knowledge_base_id, + dataSourceId=data_source_id, + documents=[ + { + "content": { "dataSourceType": "S3", "s3": { - "uri": f"s3://{DOCUMENT_BUCKET}/{user_id}/{bot_id}/documents/{file}", + "s3Location": { + "uri": document_identifier, + }, }, - } - for file in islice(deleted_files, i, i + 10) - ], - ) + }, + } + for document_identifier in islice(added_documents, i, i + 10) + ], + ) + documents_diff["Added"].extend( + document["identifier"]["s3"]["uri"] + for document in ingest_response["documentDetails"] + if document["status"] != "IGNORED" and "s3" in document["identifier"] + ) + + for i in range(0, len(deleted_documents), 10): + delete_response = bedrock_agent.delete_knowledge_base_documents( + knowledgeBaseId=knowledge_base_id, + dataSourceId=data_source_id, + documentIdentifiers=[ + { + "dataSourceType": "S3", + "s3": { + "uri": document_identifier, + }, + } + for document_identifier in islice(deleted_documents, i, i + 10) + ], + ) + documents_diff["Deleted"].extend( + document["identifier"]["s3"]["uri"] + for document in delete_response["documentDetails"] + if "s3" in document["identifier"] + ) return { "KnowledgeBaseId": knowledge_base_id, "DataSourceId": data_source_id, - "Files": data_source_files, + "DocumentsDiff": documents_diff, "IngestionJobId": None, } @@ -89,7 +160,7 @@ def handle_ingest(event): return { "KnowledgeBaseId": knowledge_base_id, "DataSourceId": data_source_id, - "Files": None, + "DocumentsDiff": None, "IngestionJobId": ingestion_job_id, } @@ -102,69 +173,64 @@ def handle_check(event): ingestion_job = event["IngestionJob"] knowledge_base_id = ingestion_job["KnowledgeBaseId"] data_source_id = ingestion_job["DataSourceId"] - data_source_files = ingestion_job.get("Files") - if data_source_files: - for data_source_file in data_source_files: - user_id = data_source_file["OwnerUserId"] - bot_id = data_source_file["BotId"] - - added_files = data_source_file["Added"] - for i in range(0, len(added_files), 10): - get_documents_response = bedrock_agent.get_knowledge_base_documents( - knowledgeBaseId=knowledge_base_id, - dataSourceId=data_source_id, - documentIdentifiers=[ - { - "dataSourceType": "S3", - "s3": { - "uri": f"s3://{DOCUMENT_BUCKET}/{user_id}/{bot_id}/documents/{file}", - }, - } - for file in islice(added_files, i, i + 10) - ], - ) - for document in get_documents_response["documentDetails"]: - status = document["status"] - uri = document["identifier"].get("s3", {})["uri"] - match status: - case "INDEXED": - pass - - case ( - "PENDING" | "STARTING" | "IN_PROGRESS" | "PARTIALLY_INDEXED" - ): - raise RetryException() - - case _: - raise Exception(f"File {uri}: Bad status '{status}'.") - - deleted_files = data_source_file["Deleted"] - for i in range(0, len(deleted_files), 10): - get_documents_response = bedrock_agent.get_knowledge_base_documents( - knowledgeBaseId=knowledge_base_id, - dataSourceId=data_source_id, - documentIdentifiers=[ - { - "dataSourceType": "S3", - "s3": { - "uri": f"s3://{DOCUMENT_BUCKET}/{user_id}/{bot_id}/documents/{file}", - }, - } - for file in islice(deleted_files, i, i + 10) - ], - ) - for document in get_documents_response["documentDetails"]: - status = document["status"] - uri = document["identifier"].get("s3", {})["uri"] - match status: - case "NOT_FOUND": - pass - - case "PENDING" | "DELETING" | "DELETE_IN_PROGRESS": - raise RetryException() - - case _: - raise Exception(f"File '{uri}': Bad status '{status}'.") + + documents_diff = ingestion_job.get("DocumentsDiff") + if documents_diff: + added_documents = documents_diff["Added"] + for i in range(0, len(added_documents), 10): + get_documents_response = bedrock_agent.get_knowledge_base_documents( + knowledgeBaseId=knowledge_base_id, + dataSourceId=data_source_id, + documentIdentifiers=[ + { + "dataSourceType": "S3", + "s3": { + "uri": document_identifier, + }, + } + for document_identifier in islice(added_documents, i, i + 10) + ], + ) + for document in get_documents_response["documentDetails"]: + status = document["status"] + uri = document["identifier"].get("s3", {})["uri"] + match status: + case "INDEXED": + pass + + case "PENDING" | "STARTING" | "IN_PROGRESS" | "PARTIALLY_INDEXED": + raise RetryException() + + case _: + raise Exception(f"File {uri}: Bad status '{status}'.") + + deleted_documents = documents_diff["Deleted"] + for i in range(0, len(deleted_documents), 10): + get_documents_response = bedrock_agent.get_knowledge_base_documents( + knowledgeBaseId=knowledge_base_id, + dataSourceId=data_source_id, + documentIdentifiers=[ + { + "dataSourceType": "S3", + "s3": { + "uri": document_identifier, + }, + } + for document_identifier in islice(deleted_documents, i, i + 10) + ], + ) + for document in get_documents_response["documentDetails"]: + status = document["status"] + uri = document["identifier"].get("s3", {})["uri"] + match status: + case "NOT_FOUND": + pass + + case "PENDING" | "DELETING" | "DELETE_IN_PROGRESS": + raise RetryException() + + case _: + raise Exception(f"File '{uri}': Bad status '{status}'.") return diff --git a/backend/embedding_statemachine/bedrock_knowledge_base/update_bot_status.py b/backend/embedding_statemachine/bedrock_knowledge_base/update_bot_status.py index c450858ff..322245d31 100644 --- a/backend/embedding_statemachine/bedrock_knowledge_base/update_bot_status.py +++ b/backend/embedding_statemachine/bedrock_knowledge_base/update_bot_status.py @@ -1,115 +1,82 @@ import json -import logging +from typing import TypedDict -import boto3 from app.repositories.common import compose_sk, get_bot_table_client from app.routes.schemas.bot import type_sync_status from reretry import retry -logger = logging.getLogger() -logger.setLevel(logging.INFO) - -dynamodb = boto3.resource("dynamodb") - RETRIES_TO_UPDATE_SYNC_STATUS = 4 RETRY_DELAY_TO_UPDATE_SYNC_STATUS = 2 +class SyncStatus(TypedDict): + user_id: str + bot_id: str + status: type_sync_status + reason: str + last_exec_id: str + + @retry(tries=RETRIES_TO_UPDATE_SYNC_STATUS, delay=RETRY_DELAY_TO_UPDATE_SYNC_STATUS) -def update_sync_status( - user_id: str, - bot_id: str, - sync_status: type_sync_status, - sync_status_reason: str, - last_exec_id: str, -): +def update_sync_status(sync_status: SyncStatus): table = get_bot_table_client() table.update_item( - Key={"PK": user_id, "SK": compose_sk(bot_id, "bot")}, + Key={ + "PK": sync_status["user_id"], + "SK": compose_sk(sync_status["bot_id"], "bot"), + }, UpdateExpression="SET SyncStatus = :sync_status, SyncStatusReason = :sync_status_reason, LastExecId = :last_exec_id", ExpressionAttributeValues={ - ":sync_status": sync_status, - ":sync_status_reason": sync_status_reason, - ":last_exec_id": last_exec_id, + ":sync_status": sync_status["status"], + ":sync_status_reason": sync_status["reason"], + ":last_exec_id": sync_status["last_exec_id"], }, ) -def extract_from_cause(cause_str: str) -> tuple: - logger.debug(f"Extracting OWNER_USER_ID and BOT_ID from cause: {cause_str}") - cause = json.loads(cause_str) - logger.debug(f"Cause: {cause}") - environment_variables = cause["Build"]["Environment"]["EnvironmentVariables"] - logger.debug(f"Environment variables: {environment_variables}") - - user_id = next( - ( - item["Value"] - for item in environment_variables - if item["Name"] == "OWNER_USER_ID" - ), - None, - ) - bot_id = next( - (item["Value"] for item in environment_variables if item["Name"] == "BOT_ID"), - None, - ) - - if not user_id or not bot_id: - raise ValueError("PK or SK not found in cause.") - - build_arn = cause["Build"].get("Arn", "") - - logger.debug(f"PK: {user_id}, SK: {bot_id}, Build ARN: {build_arn}") - - return user_id, bot_id, build_arn - - def handler(event, context): - logger.info(f"Event: {event}") - try: - cause = event.get("cause", None) - ingestion_job = event.get("ingestion_job", None) - - # Initialize variables - user_id: str - bot_id: str - sync_status: type_sync_status - sync_status_reason: str - last_exec_id: str - - if cause: - # UpdateSymcStatusFailed - user_id, bot_id, build_arn = extract_from_cause(cause) - sync_status = "FAILED" - sync_status_reason = cause - last_exec_id = build_arn - elif ingestion_job: - # UpdateSymcStatusFailedForIngestion - user_id = event["user_id"] - bot_id = event["bot_id"] - sync_status = "FAILED" - sync_status_reason = str(ingestion_job["ingestionJob"]["failureReasons"]) - last_exec_id = ingestion_job["ingestionJob"]["ingestionJobId"] - else: - user_id = event["user_id"] - bot_id = event["bot_id"] - sync_status = event["sync_status"] - sync_status_reason = event.get("sync_status_reason", "") - last_exec_id = event.get("last_exec_id", "") - - logger.info( - f"Updating sync status for bot {bot_id} of user {user_id} to {sync_status} with reason: {sync_status_reason}" + # Initialize variables + queued_bots = event.get("QueuedBots") + user_id = event.get("OwnerUserId") + bot_id = event.get("BotId") + sync_status: type_sync_status = event["SyncStatus"] + sync_status_reason: str + last_exec_id: str + + build = event.get("Build") + if build: + # CodeBuild + sync_status_reason = json.dumps(build["Phases"]) + last_exec_id = build["Arn"] + + else: + sync_status_reason = event.get("SyncStatusReason", "") + last_exec_id = event.get("LastExecId", "") + + sync_status_updates: list[SyncStatus] = [] + + if user_id and bot_id: + sync_status_updates.append( + { + "user_id": user_id, + "bot_id": bot_id, + "status": sync_status, + "reason": sync_status_reason, + "last_exec_id": last_exec_id, + } ) - update_sync_status( - user_id, bot_id, sync_status, sync_status_reason, last_exec_id + if queued_bots: + sync_status_updates.extend( + { + "user_id": queued_bot["OwnerUserId"], + "bot_id": queued_bot["BotId"], + "status": sync_status, + "reason": sync_status_reason, + "last_exec_id": last_exec_id, + } + for queued_bot in queued_bots ) - return { - "statusCode": 200, - "body": json.dumps("Sync status updated successfully."), - } - except Exception as e: - logger.error(f"Error updating sync status: {e}") - return {"statusCode": 500, "body": json.dumps("Error updating sync status.")} + for sync_status_update in sync_status_updates: + update_sync_status(sync_status_update) diff --git a/cdk/lambda/knowledge-base-custom-transformation/index.ts b/cdk/lambda/knowledge-base-custom-transformation/index.ts index ff1dc74a8..cd91cf342 100644 --- a/cdk/lambda/knowledge-base-custom-transformation/index.ts +++ b/cdk/lambda/knowledge-base-custom-transformation/index.ts @@ -6,11 +6,18 @@ export const handler: Handler = async (event, context) => { const inputFiles = event.inputFiles as any[]; return { - outputFiles: inputFiles.map(file => { + outputFiles: inputFiles.flatMap(file => { const originalFileLocation = file.originalFileLocation; const s3Uri = new URL(originalFileLocation.s3_location.uri); + + const fileName = s3Uri.pathname.match(/^(\/[^/]+)*\/(?[^/]+)$/)?.groups?.fileName; + if (fileName?.startsWith('.')) { + console.log(`Ignored dotfile '${fileName}'`); + return []; + } + const botId = s3Uri.pathname.match(/^\/(?[^/]+)\/(?[^/]+)\/documents\/(?[^/]+)$/)?.groups?.botId; - return { + return [{ originalFileLocation, ...(botId != null ? { fileMetadata: { @@ -20,7 +27,7 @@ export const handler: Handler = async (event, context) => { }, } : undefined), contentBatches: file.contentBatches, - }; + }]; }), }; }; diff --git a/cdk/lib/constructs/api-publish-codebuild.ts b/cdk/lib/constructs/api-publish-codebuild.ts index 980dde4a1..563973893 100644 --- a/cdk/lib/constructs/api-publish-codebuild.ts +++ b/cdk/lib/constructs/api-publish-codebuild.ts @@ -42,7 +42,7 @@ export class ApiPublishCodebuild extends Construct { phases: { install: { "runtime-versions": { - nodejs: "18", + nodejs: 22, }, "on-failure": "ABORT", }, diff --git a/cdk/lib/constructs/bedrock-custom-bot-codebuild.ts b/cdk/lib/constructs/bedrock-custom-bot-codebuild.ts index 90c96eab0..f04789ab8 100644 --- a/cdk/lib/constructs/bedrock-custom-bot-codebuild.ts +++ b/cdk/lib/constructs/bedrock-custom-bot-codebuild.ts @@ -40,7 +40,7 @@ export class BedrockCustomBotCodebuild extends Construct { phases: { install: { "runtime-versions": { - nodejs: "18", + nodejs: 22, }, "on-failure": "ABORT", }, diff --git a/cdk/lib/constructs/bedrock-shared-knowledge-bases-codebuild.ts b/cdk/lib/constructs/bedrock-shared-knowledge-bases-codebuild.ts index 82bebc19f..8bf59363f 100644 --- a/cdk/lib/constructs/bedrock-shared-knowledge-bases-codebuild.ts +++ b/cdk/lib/constructs/bedrock-shared-knowledge-bases-codebuild.ts @@ -37,7 +37,7 @@ export class BedrockSharedKnowledgeBasesCodebuild extends Construct { phases: { install: { "runtime-versions": { - nodejs: "18", + nodejs: 22, }, "on-failure": "ABORT", }, diff --git a/cdk/lib/constructs/embedding.ts b/cdk/lib/constructs/embedding.ts index ce4bcb232..2904672cc 100644 --- a/cdk/lib/constructs/embedding.ts +++ b/cdk/lib/constructs/embedding.ts @@ -36,6 +36,7 @@ export class Embedding extends Construct { private _finalizeCustomBotBuildHandler: IFunction; private _finalizeSharedKnowledgeBasesBuildHandler: IFunction; private _synchronizeDataSourceHandler: IFunction; + private _lockHandler: IFunction; constructor(scope: Construct, id: string, props: EmbeddingProps) { super(scope, id); @@ -83,6 +84,7 @@ export class Embedding extends Construct { resources: ["arn:aws:logs:*:*:*"], }) ); + props.documentBucket.grantReadWrite(handlerRole); this._updateSyncStatusHandler = new DockerImageFunction( this, @@ -219,6 +221,27 @@ export class Embedding extends Construct { role: handlerRole, logRetention: logs.RetentionDays.THREE_MONTHS, }); + + this._lockHandler = new DockerImageFunction(this, "LockHandler", { + code: DockerImageCode.fromImageAsset(path.join(__dirname, "../../../backend"), { + platform: Platform.LINUX_AMD64, + file: "lambda.Dockerfile", + cmd: [ + "embedding_statemachine.bedrock_knowledge_base.lock.handler", + ], + exclude: [...excludeDockerImage], + }), + memorySize: 512, + timeout: Duration.minutes(1), + environment: { + ACCOUNT: Stack.of(this).account, + REGION: Stack.of(this).region, + BEDROCK_REGION: props.bedrockRegion, + DOCUMENT_BUCKET: props.documentBucket.bucketName, + }, + role: handlerRole, + logRetention: logs.RetentionDays.THREE_MONTHS, + }); } private setupStateMachine(props: EmbeddingProps): sfn.StateMachine { @@ -230,13 +253,42 @@ export class Embedding extends Construct { }, }); + const acquireLockForSharedKnowledgeBases = this.createAcquireLock("ForSharedKnowledgeBases", { + name: "shared-knowledge-bases", + resultPath: "$.Lock", + }); + + const releaseLockForSharedKnowledgeBasesOnFailed = this.createReleaseLock("ForSharedKnowledgeBasesOnFailed", { + name: "shared-knowledge-bases", + lockId: sfn.JsonPath.stringAt("$.Lock.LockId"), + }); + const syncSharedKnowledgeBasesFailed = new sfn.Fail(this, "SyncSharedKnowledgeBasesFailed", { + cause: "Shared knowledge bases sync failed", + }); + const releaseLockFallback = ( + releaseLockForSharedKnowledgeBasesOnFailed + .next(syncSharedKnowledgeBasesFailed) + ); + + const updateSyncStatusRunning = new tasks.LambdaInvoke(this, "UpdateSyncStatusRunning", { + lambdaFunction: this._updateSyncStatusHandler, + payload: sfn.TaskInput.fromObject({ + QueuedBots: sfn.JsonPath.objectAt("$.QueuedBots"), + SyncStatus: "RUNNING", + }), + resultPath: sfn.JsonPath.DISCARD, + }); + updateSyncStatusRunning.addCatch(releaseLockFallback, { + resultPath: "$.Error", + }); + const buildSharedKnowledgeBases = new tasks.CodeBuildStartBuild(this, "BuildSharedKnowledgeBases", { project: props.bedrockSharedKnowledgeBasesProject, integrationPattern: sfn.IntegrationPattern.RUN_JOB, environmentVariablesOverride: { SHARED_KNOWLEDGE_BASES: { type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, - value: sfn.JsonPath.stringAt("States.JsonToString($.SharedKnowledgeBases.KnowledgeBases)"), + value: sfn.JsonPath.jsonToString(sfn.JsonPath.objectAt("$.SharedKnowledgeBases")), }, // Bucket name provisioned by the bedrock stack BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME: { @@ -248,135 +300,258 @@ export class Embedding extends Construct { value: props.enableRagReplicas.toString(), }, }, - resultPath: "$.Build", + resultPath: sfn.JsonPath.DISCARD, }); - // buildSharedKnowledgeBases.addCatch(fallback); - const updateSyncStatusRunning = this.createUpdateSyncStatusTask( - "UpdateSyncStatusRunning", - "RUNNING" - ); + const passSharedKnowledgeBasesBuildError = new sfn.Pass(this, "PassSharedKnowledgeBasesBuildError", { + parameters: { + QueuedBots: sfn.JsonPath.objectAt("$.QueuedBots"), + Lock: sfn.JsonPath.objectAt("$.Lock"), + Error: sfn.JsonPath.stringAt("$.Error.Error"), + Cause: sfn.JsonPath.stringToJson(sfn.JsonPath.stringAt("$.Error.Cause")), + }, + }); + const updateSyncStatusFailedForSharedKnowledgeBasesBuild = new tasks.LambdaInvoke(this, "UpdateSyncStatusFailedForSharedKnowledgeBasesBuild", { + lambdaFunction: this._updateSyncStatusHandler, + payload: sfn.TaskInput.fromObject({ + QueuedBots: sfn.JsonPath.objectAt("$.QueuedBots"), + SyncStatus: "FAILED", + Build: sfn.JsonPath.objectAt("$.Cause.Build"), + }), + resultPath: sfn.JsonPath.DISCARD, + }); + updateSyncStatusFailedForSharedKnowledgeBasesBuild.addCatch(releaseLockFallback, { + resultPath: "$.Error", + }); - const updateSyncStatusSucceeded = this.createUpdateSyncStatusTask( - "UpdateSyncStatusSuccess", - "SUCCEEDED", - "Knowledge base sync succeeded" + const buildSharedKnowledgeBasesFallback = ( + passSharedKnowledgeBasesBuildError + .next(updateSyncStatusFailedForSharedKnowledgeBasesBuild) + .next(releaseLockFallback) ); + buildSharedKnowledgeBases.addCatch(buildSharedKnowledgeBasesFallback, { + resultPath: "$.Error", + }) const finalizeSharedKnowledgeBasesBuild = new tasks.LambdaInvoke(this, "FinalizeSharedKnowledgeBasesBuild", { lambdaFunction: this._finalizeSharedKnowledgeBasesBuildHandler, resultSelector: { + QueuedBots: sfn.JsonPath.objectAt("$.Payload.QueuedBots"), + SharedKnowledgeBases: sfn.JsonPath.objectAt("$.Payload.SharedKnowledgeBases"), DataSources: sfn.JsonPath.objectAt("$.Payload.DataSources"), + Lock: sfn.JsonPath.objectAt("$.Payload.Lock"), }, - resultPath: "$.StackOutput", }); - // finalizeSharedKnowledgeBasesBuild.addCatch(fallback); - - const dataSourceSynchronizationForSharedKnowledgeBases = this.createDataSourceSynchronizationTask("Shared"); const mapIngestionJobsForSharedKnowledgeBases = new sfn.Map(this, "MapIngestionJobsForSharedKnowledgeBases", { - inputPath: "$.StackOutput.DataSources", + inputPath: "$.DataSources", resultPath: sfn.JsonPath.DISCARD, maxConcurrency: 1, - }).itemProcessor( - dataSourceSynchronizationForSharedKnowledgeBases + }); + const ingestionJobForSharedKnowledgeBases = this.createIngestionJob("Shared"); + + const updateSyncStatusFailedForSharedKnowledgeBases = new tasks.LambdaInvoke(this, "UpdateSyncStatusFailedForSharedKnowledgeBases", { + lambdaFunction: this._updateSyncStatusHandler, + payload: sfn.TaskInput.fromObject({ + QueuedBots: sfn.JsonPath.objectAt("$.QueuedBots"), + SyncStatus: "FAILED", + }), + resultPath: sfn.JsonPath.DISCARD, + }) + updateSyncStatusFailedForSharedKnowledgeBases.addCatch(releaseLockFallback, { + resultPath: "$.Error", + }); + + const syncSharedKnowledgeBasesFallback = ( + updateSyncStatusFailedForSharedKnowledgeBases + .next(releaseLockFallback) ); + finalizeSharedKnowledgeBasesBuild.addCatch(syncSharedKnowledgeBasesFallback, { + resultPath: "$.Error", + }); + mapIngestionJobsForSharedKnowledgeBases.addCatch(syncSharedKnowledgeBasesFallback, { + resultPath: "$.Error", + }); + + const releaseLockForSharedKnowledgeBases = this.createReleaseLock("ForSharedKnowledgeBases", { + name: "shared-knowledge-bases", + lockId: sfn.JsonPath.stringAt("$.Lock.LockId"), + }); const mapQueuedBots = new sfn.Map(this, "MapQueuedBots", { itemsPath: "$.QueuedBots", + resultPath: sfn.JsonPath.DISCARD, + }); + + const acquireLockForCustomBot = this.createAcquireLock("ForCustomBot", { + name: sfn.JsonPath.format("custombot-{}", sfn.JsonPath.stringAt("$.BotId")), + resultPath: "$.Lock", + }); + + const startCustomBotBuild = new tasks.CodeBuildStartBuild(this, "StartCustomBotBuild", { + project: props.bedrockCustomBotProject, + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + environmentVariablesOverride: { + OWNER_USER_ID: { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: sfn.JsonPath.stringAt("$.OwnerUserId"), + }, + BOT_ID: { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: sfn.JsonPath.stringAt("$.BotId"), + }, + // Bucket name provisioned by the bedrock stack + BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME: { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: props.documentBucket.bucketName, + }, + // Source info e.g. file names, URLs, etc. + KNOWLEDGE: { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: sfn.JsonPath.jsonToString(sfn.JsonPath.objectAt("$.Knowledge")), + }, + // Bedrock Knowledge Base configuration + KNOWLEDGE_BASE: { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: sfn.JsonPath.jsonToString(sfn.JsonPath.objectAt("$.KnowledgeBase")), + }, + GUARDRAILS: { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: sfn.JsonPath.jsonToString(sfn.JsonPath.objectAt("$.Guardrails")), + }, + ENABLE_RAG_REPLICAS: { + type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, + value: props.enableRagReplicas.toString(), + }, + }, + resultPath: sfn.JsonPath.DISCARD, }); - const updateSyncStatusFailedForDedicated = new tasks.LambdaInvoke(this, "UpdateSyncStatusFailedForDedicated", { + const releaseLockForCustomBot = this.createReleaseLock("ForCustomBot", { + name: sfn.JsonPath.format("custombot-{}", sfn.JsonPath.stringAt("$.BotId")), + lockId: sfn.JsonPath.stringAt("$.Lock.LockId"), + }); + const syncCustomBotSucceeded = new sfn.Succeed(this, "SyncCustomBotSucceeded"); + const syncCustomBotFinished = ( + releaseLockForCustomBot + .next(syncCustomBotSucceeded) + ); + + const passCustomBotBuildError = new sfn.Pass(this, "PassCustomBotBuildError", { + parameters: { + OwnerUserId: sfn.JsonPath.stringAt("$.OwnerUserId"), + BotId: sfn.JsonPath.stringAt("$.BotId"), + Lock: sfn.JsonPath.objectAt("$.Lock"), + Error: sfn.JsonPath.stringAt("$.Error.Error"), + Cause: sfn.JsonPath.stringToJson(sfn.JsonPath.stringAt("$.Error.Cause")), + }, + }); + const updateSyncStatusFailedForCustomBotBuild = new tasks.LambdaInvoke(this, "UpdateSyncStatusFailedForCustomBotBuild", { lambdaFunction: this._updateSyncStatusHandler, payload: sfn.TaskInput.fromObject({ - cause: sfn.JsonPath.stringAt("$.Cause"), + OwnerUserId: sfn.JsonPath.stringAt("$.OwnerUserId"), + BotId: sfn.JsonPath.stringAt("$.BotId"), + SyncStatus: "FAILED", + Build: sfn.JsonPath.objectAt("$.Cause.Build"), }), resultPath: sfn.JsonPath.DISCARD, }); + updateSyncStatusFailedForCustomBotBuild.addCatch(syncCustomBotFinished, { + resultPath: "$.Error", + }); - const startCustomBotBuild = new tasks.CodeBuildStartBuild( - this, - "StartCustomBotBuild", - { - project: props.bedrockCustomBotProject, - integrationPattern: sfn.IntegrationPattern.RUN_JOB, - environmentVariablesOverride: { - OWNER_USER_ID: { - type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, - value: sfn.JsonPath.stringAt("$.OwnerUserId"), - }, - BOT_ID: { - type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, - value: sfn.JsonPath.stringAt("$.BotId"), - }, - // Bucket name provisioned by the bedrock stack - BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME: { - type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, - value: props.documentBucket.bucketName, - }, - // Source info e.g. file names, URLs, etc. - KNOWLEDGE: { - type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, - value: sfn.JsonPath.stringAt( - "States.JsonToString($.Knowledge)" - ), - }, - // Bedrock Knowledge Base configuration - KNOWLEDGE_BASE: { - type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, - value: sfn.JsonPath.stringAt( - "States.JsonToString($.KnowledgeBase)" - ), - }, - GUARDRAILS: { - type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, - value: sfn.JsonPath.stringAt( - "States.JsonToString($.Guardrails)" - ), - }, - ENABLE_RAG_REPLICAS: { - type: codebuild.BuildEnvironmentVariableType.PLAINTEXT, - value: props.enableRagReplicas.toString(), - }, - }, - resultPath: "$.Build", - } + const buildCustomBotFallback = ( + passCustomBotBuildError + .next(updateSyncStatusFailedForCustomBotBuild) + .next(syncCustomBotFinished) ); - startCustomBotBuild.addCatch(updateSyncStatusFailedForDedicated); + + startCustomBotBuild.addCatch(buildCustomBotFallback, { + resultPath: "$.Error", + }); const finalizeCustomBotBuild = new tasks.LambdaInvoke(this, "FinalizeCustomBotBuild", { lambdaFunction: this._finalizeCustomBotBuildHandler, resultSelector: { + OwnerUserId: sfn.JsonPath.stringAt("$.Payload.OwnerUserId"), + BotId: sfn.JsonPath.stringAt("$.Payload.BotId"), DataSources: sfn.JsonPath.objectAt("$.Payload.DataSources"), - Bots: sfn.JsonPath.objectAt("$.Payload.Bots"), + Lock: sfn.JsonPath.objectAt("$.Payload.Lock"), }, - resultPath: "$.StackOutput", }); - finalizeCustomBotBuild.addCatch(updateSyncStatusFailedForDedicated); - const dataSourceSynchronizationForDedicatedKnowledgeBases = this.createDataSourceSynchronizationTask("Dedicated"); - - const mapIngestionJobs = new sfn.Map(this, "MapIngestionJobs", { - inputPath: "$.StackOutput.DataSources", + const mapIngestionJobsForCustomBot = new sfn.Map(this, "MapIngestionJobsForCustomBot", { + inputPath: "$.DataSources", resultPath: sfn.JsonPath.DISCARD, maxConcurrency: 1, - }).itemProcessor( - dataSourceSynchronizationForDedicatedKnowledgeBases + }); + const ingestionJobForCustomBot = this.createIngestionJob("CustomBot"); + + const updateSyncStatusFailedForCustomBot = new tasks.LambdaInvoke(this, "UpdateSyncStatusFailedForCustomBot", { + lambdaFunction: this._updateSyncStatusHandler, + payload: sfn.TaskInput.fromObject({ + OwnerUserId: sfn.JsonPath.stringAt("$.OwnerUserId"), + BotId: sfn.JsonPath.stringAt("$.BotId"), + SyncStatus: "FAILED", + }), + resultPath: sfn.JsonPath.DISCARD, + }); + updateSyncStatusFailedForCustomBot.addCatch(syncCustomBotFinished, { + resultPath: "$.Error", + }); + + const syncCustomBotFallback = ( + updateSyncStatusFailedForCustomBot + .next(syncCustomBotFinished) ); + finalizeCustomBotBuild.addCatch(syncCustomBotFallback, { + resultPath: "$.Error", + }); + mapIngestionJobsForCustomBot.addCatch(syncCustomBotFallback, { + resultPath: "$.Error", + }); - const definition = bootstrapStateMachine - .next(buildSharedKnowledgeBases) - .next(finalizeSharedKnowledgeBasesBuild) - .next(mapIngestionJobsForSharedKnowledgeBases) - .next( - mapQueuedBots.itemProcessor( - updateSyncStatusRunning - .next(startCustomBotBuild) - .next(finalizeCustomBotBuild) - .next(mapIngestionJobs) - .next(updateSyncStatusSucceeded) + const updateSyncStatusSucceeded = new tasks.LambdaInvoke(this, "UpdateSyncStatusSuccess", { + lambdaFunction: this._updateSyncStatusHandler, + payload: sfn.TaskInput.fromObject({ + OwnerUserId: sfn.JsonPath.stringAt("$.OwnerUserId"), + BotId: sfn.JsonPath.stringAt("$.BotId"), + SyncStatus: "SUCCEEDED", + SyncStatusReason: "Knowledge base sync succeeded", + }), + resultPath: sfn.JsonPath.DISCARD, + }); + updateSyncStatusSucceeded.addCatch(syncCustomBotFinished, { + resultPath: "$.Error", + }); + + const definition = ( + bootstrapStateMachine + .next(acquireLockForSharedKnowledgeBases) + .next(updateSyncStatusRunning) + .next(buildSharedKnowledgeBases) + .next(finalizeSharedKnowledgeBasesBuild) + .next( + mapIngestionJobsForSharedKnowledgeBases.itemProcessor( + ingestionJobForSharedKnowledgeBases + ) ) - ) + .next(releaseLockForSharedKnowledgeBases) + .next( + mapQueuedBots.itemProcessor( + acquireLockForCustomBot + .next(startCustomBotBuild) + .next(finalizeCustomBotBuild) + .next( + mapIngestionJobsForCustomBot.itemProcessor( + ingestionJobForCustomBot + ) + ) + .next(updateSyncStatusSucceeded) + .next(syncCustomBotFinished) + ) + ) + ); return new sfn.StateMachine(this, "StateMachine", { definitionBody: sfn.DefinitionBody.fromChainable(definition), @@ -481,49 +656,25 @@ export class Embedding extends Construct { return removalHandler; } - private createUpdateSyncStatusTask( - id: string, - syncStatus: string, - syncStatusReason?: string, - lastExecIdPath?: string - ): tasks.LambdaInvoke { - const payload: { [key: string]: any } = { - "user_id.$": "$.OwnerUserId", - "bot_id.$": "$.BotId", - sync_status: syncStatus, - sync_status_reason: syncStatusReason || "", - }; - - if (lastExecIdPath) { - payload["last_exec_id.$"] = lastExecIdPath; - } - - return new tasks.LambdaInvoke(this, id, { - lambdaFunction: this._updateSyncStatusHandler, - payload: sfn.TaskInput.fromObject(payload), - resultPath: sfn.JsonPath.DISCARD, - }); - } - - private createDataSourceSynchronizationTask(name: string): sfn.IChainable { - const startIngestionJob = new tasks.LambdaInvoke(this, `StartIngestionJob${name}`, { + private createIngestionJob(idSuffix: string) { + const startIngestionJob = new tasks.LambdaInvoke(this, `StartIngestionJob${idSuffix}`, { lambdaFunction: this._synchronizeDataSourceHandler, payload: sfn.TaskInput.fromObject({ Action: "Ingest", KnowledgeBaseId: sfn.JsonPath.stringAt("$.KnowledgeBaseId"), DataSourceId: sfn.JsonPath.stringAt("$.DataSourceId"), - Files: sfn.JsonPath.objectAt("$.Files"), + FilesDiffs: sfn.JsonPath.objectAt("$.FilesDiffs"), }), resultSelector: { KnowledgeBaseId: sfn.JsonPath.stringAt("$.Payload.KnowledgeBaseId"), DataSourceId: sfn.JsonPath.stringAt("$.Payload.DataSourceId"), - Files: sfn.JsonPath.objectAt("$.Payload.Files"), + DocumentsDiff: sfn.JsonPath.objectAt("$.Payload.DocumentsDiff"), IngestionJobId: sfn.JsonPath.stringAt("$.Payload.IngestionJobId"), }, resultPath: "$.IngestionJob", }); - const checkIngestionJob = new tasks.LambdaInvoke(this, `CheckIngestionJob${name}`, { + const checkIngestionJob = new tasks.LambdaInvoke(this, `CheckIngestionJob${idSuffix}`, { lambdaFunction: this._synchronizeDataSourceHandler, payload: sfn.TaskInput.fromObject({ Action: "Check", @@ -532,7 +683,7 @@ export class Embedding extends Construct { resultPath: sfn.JsonPath.DISCARD, }); - const ingestionComplete = new sfn.Pass(this, `IngestionComplete${name}`); + const ingestionComplete = new sfn.Pass(this, `IngestionComplete${idSuffix}`); return startIngestionJob .next( checkIngestionJob.addRetry({ @@ -547,4 +698,60 @@ export class Embedding extends Construct { }) ).next(ingestionComplete) } + + private createAcquireLock(idSuffix: string, { + name, + owner = sfn.JsonPath.executionName, + expires = Duration.hours(6), + resultPath, + }: { + name: string; + owner?: string; + expires?: Duration; + resultPath?: string; + }) { + return new tasks.LambdaInvoke(this, `AcquireLock${idSuffix}`, { + lambdaFunction: this._lockHandler, + payload: sfn.TaskInput.fromObject({ + Action: "Acquire", + LockName: name, + Owner: owner, + ExpiresSeconds: expires.toSeconds(), + }), + resultSelector: { + LockId: sfn.JsonPath.stringAt("$.Payload.LockId"), + }, + resultPath: resultPath, + }).addRetry({ + interval: Duration.seconds(15), + maxAttempts: expires.toSeconds() / 15, + backoffRate: 1, + errors: [ + 'RetryException', + ], + }); + } + + private createReleaseLock(idSuffix: string, { + name, + lockId, + }: { + name: string; + lockId: string; + }) { + return new tasks.LambdaInvoke(this, `ReleaseLock${idSuffix}`, { + lambdaFunction: this._lockHandler, + payload: sfn.TaskInput.fromObject({ + Action: "Release", + LockName: name, + LockId: lockId, + }), + resultPath: sfn.JsonPath.DISCARD, + }).addRetry({ + maxAttempts: 5, + errors: [ + 'RetryException', + ], + }); + } } diff --git a/deploy.yml b/deploy.yml index f5a3deb61..32f0617d0 100644 --- a/deploy.yml +++ b/deploy.yml @@ -151,7 +151,7 @@ Resources: "phases": { "install": { "runtime-versions": { - "nodejs": "18" + "nodejs": 22 }, "on-failure": "ABORT" }, diff --git a/frontend/src/features/knowledgeBase/pages/BotKbEditPage.tsx b/frontend/src/features/knowledgeBase/pages/BotKbEditPage.tsx index c857ac402..e71e7062c 100644 --- a/frontend/src/features/knowledgeBase/pages/BotKbEditPage.tsx +++ b/frontend/src/features/knowledgeBase/pages/BotKbEditPage.tsx @@ -1603,7 +1603,7 @@ const BotKbEditPage: React.FC = () => { value="new" checked={knowledgeBaseType === 'new'} label={t( - 'knowledgeBaseSettings.advancedConfigration.existKnowledgeBaseId.createNewKb.label' + 'knowledgeBaseSettings.advancedConfigration.createDedicatedKnowledgeBase.label' )} onChange={() => setKnowledgeBaseType('new')} disabled @@ -1613,7 +1613,7 @@ const BotKbEditPage: React.FC = () => { value="shared" checked={knowledgeBaseType === 'shared'} label={t( - 'knowledgeBaseSettings.advancedConfigration.existKnowledgeBaseId.useSharedKb.label' + 'knowledgeBaseSettings.advancedConfigration.createTenantInSharedKnowledgeBase.label' )} onChange={() => setKnowledgeBaseType('shared')} /> @@ -1622,7 +1622,7 @@ const BotKbEditPage: React.FC = () => { value="existing" checked={knowledgeBaseType === 'existing'} label={t( - 'knowledgeBaseSettings.advancedConfigration.existKnowledgeBaseId.existing.label' + 'knowledgeBaseSettings.advancedConfigration.useExistingKnowledgeBase.label' )} onChange={() => setKnowledgeBaseType('existing')} /> @@ -1634,7 +1634,7 @@ const BotKbEditPage: React.FC = () => {
{ />
{t( - 'knowledgeBaseSettings.advancedConfigration.existKnowledgeBaseId.description' + 'knowledgeBaseSettings.advancedConfigration.existingKnowledgeBaseId.description' )}
diff --git a/frontend/src/i18n/en/index.ts b/frontend/src/i18n/en/index.ts index 23e43cdac..b873ddb60 100644 --- a/frontend/src/i18n/en/index.ts +++ b/frontend/src/i18n/en/index.ts @@ -937,19 +937,19 @@ How would you categorize this email?`, }, }, advancedConfigration: { - existKnowledgeBaseId: { + existingKnowledgeBaseId: { label: 'ID for the Amazon Bedrock Knowledge Base', description: 'Please specify ID that your existing Amazon Bedrock knowledge base.', - createNewKb: { - label: 'Create New Knowledge Base', - }, - useSharedKb: { - label: 'Pool Pattern', - }, - existing: { - label: 'Use your existing knowledge base', - }, + }, + createDedicatedKnowledgeBase: { + label: 'Create a dedicated Knowledge Base', + }, + createTenantInSharedKnowledgeBase: { + label: 'Create a tenant in a shared Knowledge Base', + }, + useExistingKnowledgeBase: { + label: 'Use your existing Knowledge Base', }, }, }, diff --git a/frontend/src/i18n/id/index.ts b/frontend/src/i18n/id/index.ts index b6167d279..77992e9c3 100644 --- a/frontend/src/i18n/id/index.ts +++ b/frontend/src/i18n/id/index.ts @@ -725,16 +725,19 @@ const translation = { }, }, advancedConfigration: { - existKnowledgeBaseId: { + existingKnowledgeBaseId: { label: 'ID Amazon Bedrock Knowledge Base', description: 'Masukkan ID Amazon Bedrock Knowledge Base eksisting Anda.', - createNewKb: { - label: 'Buat Knowledge Base baru', - }, - existing: { - label: 'Gunakan Knowledge Base eksisting', - }, + }, + createDedicatedKnowledgeBase: { + label: 'Buat Knowledge Base berdedikasi', + }, + createTenantInSharedKnowledgeBase: { + label: 'Buat penyewa di Knowledge Base bersama', + }, + useExistingKnowledgeBase: { + label: 'Gunakan Knowledge Base eksisting', }, }, }, diff --git a/frontend/src/i18n/ja/index.ts b/frontend/src/i18n/ja/index.ts index a6ef8c966..1497c056b 100644 --- a/frontend/src/i18n/ja/index.ts +++ b/frontend/src/i18n/ja/index.ts @@ -943,19 +943,19 @@ const translation: typeof en = { }, }, advancedConfigration: { - existKnowledgeBaseId: { + existingKnowledgeBaseId: { label: '既存のAmazon Bedrock Knowledge BaseのID', description: - '既存のAmazon Bedrock Knowledge Baseを使用することができる', - createNewKb: { - label: '新規のナレッジを作成する', - }, - useSharedKb: { - label: 'Pool Pattern', - }, - existing: { - label: '外部のナレッジ(Knowledge Base)を利用する', - }, + '既存のAmazon Bedrock Knowledge Baseを利用できます', + }, + createDedicatedKnowledgeBase: { + label: '専用のKnowledge Baseを作成する', + }, + createTenantInSharedKnowledgeBase: { + label: '共有のKnowledge Baseにテナントを作成する', + }, + useExistingKnowledgeBase: { + label: '外部のKnowledge Baseを利用する', }, }, }, diff --git a/frontend/src/i18n/pl/index.ts b/frontend/src/i18n/pl/index.ts index f699405a1..e00ff6de7 100644 --- a/frontend/src/i18n/pl/index.ts +++ b/frontend/src/i18n/pl/index.ts @@ -726,16 +726,19 @@ Jak sklasyfikowałbyś ten e-mail?`, }, }, advancedConfigration: { - existKnowledgeBaseId: { + existingKnowledgeBaseId: { label: 'ID dla bazy wiedzy Amazon Bedrock', description: 'Proszę podać ID istniejącej Bazy Wiedzy Amazon Bedrock.', - createNewKb: { - label: 'Utwórz nową Bazę Wiedzy', - }, - existing: { - label: 'Użyj istniejącej Bazy Wiedzy', - }, + }, + createDedicatedKnowledgeBase: { + label: 'Utwórz dedykowaną Bazę Wiedzy', + }, + createTenantInSharedKnowledgeBase: { + label: 'Utwórz najemcę w udostępnionej Bazy Wiedzy', + }, + useExistingKnowledgeBase: { + label: 'Użyj istniejącej Bazy Wiedzy', }, }, }, diff --git a/frontend/src/i18n/pt-br/index.ts b/frontend/src/i18n/pt-br/index.ts index 364f9db5a..059ed2ce6 100644 --- a/frontend/src/i18n/pt-br/index.ts +++ b/frontend/src/i18n/pt-br/index.ts @@ -905,16 +905,19 @@ Como você categorizaria este e-mail?`, }, }, advancedConfigration: { - existKnowledgeBaseId: { + existingKnowledgeBaseId: { label: 'ID para a Base de Conhecimento Amazon Bedrock', description: 'Por favor, especifique o ID da sua base de conhecimento Amazon Bedrock existente.', - createNewKb: { - label: 'Criar Nova Base de Conhecimento', - }, - existing: { - label: 'Use sua base de conhecimento existente', - }, + }, + createDedicatedKnowledgeBase: { + label: 'Criar nova Base de Conhecimento dedicada', + }, + createTenantInSharedKnowledgeBase: { + label: 'Crie um inquilino em uma Base de Conhecimento compartilhada', + }, + useExistingKnowledgeBase: { + label: 'Use sua Base de Conhecimento existente', }, }, }, From 1d3d64fb6d75325fbaa1a3d10cc2cb52d78bf583 Mon Sep 17 00:00:00 2001 From: Yukinobu Mine Date: Wed, 29 Oct 2025 10:55:28 +0900 Subject: [PATCH 5/9] Update documents. - Screenshot changes - docs/imgs/customized_bot_creation.png - docs/imgs/import_existing_kb.png --- docs/imgs/customized_bot_creation.png | Bin 126926 -> 115034 bytes docs/imgs/import_existing_kb.png | Bin 94871 -> 37187 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/imgs/customized_bot_creation.png b/docs/imgs/customized_bot_creation.png index 784599c2074a49886bd97ac927d3ebf85707b7e3..5e9ba20d9b55b61bb7067f65760645d759f659d7 100644 GIT binary patch literal 115034 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 literal 126926 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

^*IU2BMvNvxO9ChZVFV=31fDT*_ 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 literal 94871 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 Lu6{1-oD!M Date: Wed, 29 Oct 2025 11:33:50 +0900 Subject: [PATCH 6/9] Fix unit test failures. --- backend/app/vector_search.py | 2 +- cdk/package-lock.json | 13 - cdk/package.json | 1 - cdk/test/cdk.test.ts | 265 ++++++------------ cdk/test/utils/parameter-models.test.ts | 12 +- .../knowledgeBase/pages/BotKbEditPage.tsx | 1 - 6 files changed, 87 insertions(+), 207 deletions(-) diff --git a/backend/app/vector_search.py b/backend/app/vector_search.py index e2c125d28..2357233fb 100644 --- a/backend/app/vector_search.py +++ b/backend/app/vector_search.py @@ -90,7 +90,7 @@ def _bedrock_knowledge_base_search(bot: BotModel, query: str) -> list[SearchResu retrieve_parameter["retrievalConfiguration"]["vectorSearchConfiguration"]["filter"] = { # type: ignore "listContains": { "key": "tenants", - "value": f"BOT#{bot.id}", + "value": f"BOT#{bot.id}", # type: ignore }, } diff --git a/cdk/package-lock.json b/cdk/package-lock.json index 426ac65cc..08e1b7028 100644 --- a/cdk/package-lock.json +++ b/cdk/package-lock.json @@ -23,7 +23,6 @@ "cdk": "bin/cdk.js" }, "devDependencies": { - "@aws-prototyping-sdk/pdk-nag": "^0.19.68", "@types/jest": "^29.5.3", "@types/node": "20.4.2", "aws-cdk": "^2.189.1", @@ -245,18 +244,6 @@ "node": ">=14.0.0" } }, - "node_modules/@aws-prototyping-sdk/pdk-nag": { - "version": "0.19.68", - "resolved": "https://registry.npmjs.org/@aws-prototyping-sdk/pdk-nag/-/pdk-nag-0.19.68.tgz", - "integrity": "sha512-feOoX3iMEkv8hX2XdzIZeiaIAUO3wLsOSAYhMlXWF5w/IC6EfYd08IpeC+VeORhYl2LGv+7SB9HodMZpKjjuxQ==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dev": true, - "peerDependencies": { - "aws-cdk-lib": "^2.81.0", - "cdk-nag": "^2.27.24", - "constructs": "^10.2.39" - } - }, "node_modules/@aws-sdk/client-dynamodb": { "version": "3.693.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.693.0.tgz", diff --git a/cdk/package.json b/cdk/package.json index 94acbfde7..cbd4fafc6 100644 --- a/cdk/package.json +++ b/cdk/package.json @@ -11,7 +11,6 @@ "cdk": "cdk" }, "devDependencies": { - "@aws-prototyping-sdk/pdk-nag": "^0.19.68", "@types/jest": "^29.5.3", "@types/node": "20.4.2", "aws-cdk": "^2.189.1", diff --git a/cdk/test/cdk.test.ts b/cdk/test/cdk.test.ts index 44c4760ae..627be0ec0 100644 --- a/cdk/test/cdk.test.ts +++ b/cdk/test/cdk.test.ts @@ -1,7 +1,6 @@ import * as cdk from "aws-cdk-lib"; import { BedrockChatStack } from "../lib/bedrock-chat-stack"; import { Template } from "aws-cdk-lib/assertions"; -import { AwsPrototypingChecks } from "@aws-prototyping-sdk/pdk-nag"; import { getEmbeddingModel, getChunkingStrategy, @@ -172,8 +171,6 @@ describe("Bedrock Chat Stack Test", () => { test("default stack", () => { const app = new cdk.App(); - // Security check - cdk.Aspects.of(app).add(new AwsPrototypingChecks()); const bedrockRegionResourcesStack = new BedrockRegionResourcesStack( app, @@ -613,115 +610,65 @@ describe("Bedrock Chat Stack Test", () => { describe("Bedrock Knowledge Base Stack", () => { const setupStack = (params: any = {}) => { const app = new cdk.App(); - // Security check - cdk.Aspects.of(app).add(new AwsPrototypingChecks()); - const PK: string = "test-user-id"; - const SK: string = "test-user-id#BOT#test-bot-id"; + const OWNER_USER_ID: string = "test-user-id"; + const BOT_ID: string = "test-bot-id"; const KNOWLEDGE = { - sitemap_urls: { - L: [], - }, - filenames: { - L: [ - { - S: "test-filename.pdf", - }, - ], - }, - source_urls: { - L: [ - { - S: "https://example.com", - }, - ], - }, - s3_urls: params.s3Urls !== undefined ? params.s3Urls : { L: [] }, + sitemap_urls: [], + filenames: [ + "test-filename.pdf", + ], + source_urls: [ + "https://example.com", + ], + s3_urls: params.s3Urls !== undefined ? params.s3Urls : [], }; - const BEDROCK_KNOWLEDGE_BASE = { - type: { - S: "dedicated", - }, - chunking_strategy: { - S: "fixed_size", - }, - max_tokens: - params.maxTokens !== undefined - ? { N: String(params.maxTokens) } - : undefined, - instruction: - params.instruction !== undefined - ? { S: params.instruction } - : undefined, - overlap_percentage: - params.overlapPercentage !== undefined - ? { N: String(params.overlapPercentage) } - : undefined, + const KNOWLEDGE_BASE = { + type: "dedicated", + chunking_strategy: "fixed_size", + max_tokens: params.maxTokens, + instruction: params.instruction, + overlap_percentage: params.overlapPercentage, open_search: { - M: { - analyzer: - params.analyzer !== undefined - ? JSON.parse(params.analyzer) - : { - character_filters: { - L: [ - { - S: "icu_normalizer", - }, - ], - }, - token_filters: { - L: [ - { - S: "kuromoji_baseform", - }, - { - S: "kuromoji_part_of_speech", - }, - ], - }, - tokenizer: { - S: "kuromoji_tokenizer", - }, - }, - }, - }, - embeddings_model: { - S: "titan_v2", + analyzer: + params.analyzer !== undefined + ? JSON.parse(params.analyzer) + : { + character_filters: [ + "icu_normalizer", + ], + token_filters: [ + "kuromoji_baseform", + "kuromoji_part_of_speech", + ], + tokenizer: "kuromoji_tokenizer", + }, }, - }; + embeddings_model: "titan_v2", + } as const; const BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME = "test-document-bucket-name"; - const ownerUserId: string = PK; - const botId: string = SK.split("#")[2]; - const knowledgeBase = BEDROCK_KNOWLEDGE_BASE; + const ownerUserId: string = OWNER_USER_ID; + const botId: string = BOT_ID; + const knowledgeBase = KNOWLEDGE_BASE; const knowledge = KNOWLEDGE; - const existingS3Urls: string[] = knowledge.s3_urls.L.map( - (s3Url: any) => s3Url.S - ); + const existingS3Urls: string[] = knowledge.s3_urls; const knowledgeBaseType = getKnowledgeBaseType(knowledgeBase.type); - const embeddingsModel = getEmbeddingModel(knowledgeBase.embeddings_model.S); + const embeddingsModel = getEmbeddingModel(knowledgeBase.embeddings_model); const chunkingStrategy = getChunkingStrategy( - knowledgeBase.chunking_strategy.S, - knowledgeBase.embeddings_model.S + knowledgeBase.chunking_strategy, + knowledgeBase.embeddings_model ); - const maxTokens: number | undefined = knowledgeBase.max_tokens - ? Number(knowledgeBase.max_tokens.N) + const maxTokens: number | undefined = knowledgeBase.max_tokens; + const instruction: string | undefined = knowledgeBase.instruction; + const analyzer = knowledgeBase.open_search.analyzer + ? getAnalyzer(knowledgeBase.open_search.analyzer) : undefined; - const instruction: string | undefined = knowledgeBase.instruction - ? knowledgeBase.instruction.S - : undefined; - const analyzer = knowledgeBase.open_search.M.analyzer - ? getAnalyzer(knowledgeBase.open_search.M.analyzer) - : undefined; - const overlapPercentage: number | undefined = - knowledgeBase.overlap_percentage - ? Number(knowledgeBase.overlap_percentage.N) - : undefined; + const overlapPercentage: number | undefined = knowledgeBase.overlap_percentage; const stack = new BedrockCustomBotStack(app, "BedrockCustomBotStackStack", { ownerUserId, @@ -736,8 +683,8 @@ describe("Bedrock Knowledge Base Stack", () => { instruction, analyzer, overlapPercentage, - filenames: knowledge.filenames.L.map((filename: any) => filename.S), - sourceUrls: knowledge.source_urls.L.map((sourceUrl: any) => sourceUrl.S), + filenames: knowledge.filenames, + sourceUrls: knowledge.source_urls, existKnowledgeBaseId: undefined, }); @@ -746,37 +693,21 @@ describe("Bedrock Knowledge Base Stack", () => { test("default kb stack", () => { const template = setupStack({ - s3Urls: { - L: [ - { - S: "s3://test-bucket/test-key", - }, - ], - }, + s3Urls: [ + "s3://test-bucket/test-key", + ], maxTokens: 500, instruction: "This is an example instruction.", overlapPercentage: 10, analyzer: `{ - "character_filters": { - "L": [ - { - "S": "icu_normalizer" - } - ] - }, - "token_filters": { - "L": [ - { - "S": "kuromoji_baseform" - }, - { - "S": "kuromoji_part_of_speech" - } - ] - }, - "tokenizer": { - "S": "kuromoji_tokenizer" - } + "character_filters": [ + "icu_normalizer" + ], + "token_filters": [ + "kuromoji_baseform", + "kuromoji_part_of_speech" + ], + "tokenizer": "kuromoji_tokenizer" }`, }); expect(template).toBeDefined(); @@ -787,26 +718,14 @@ describe("Bedrock Knowledge Base Stack", () => { instruction: "This is an example instruction.", overlapPercentage: 10, analyzer: `{ - "character_filters": { - "L": [ - { - "S": "icu_normalizer" - } - ] - }, - "token_filters": { - "L": [ - { - "S": "kuromoji_baseform" - }, - { - "S": "kuromoji_part_of_speech" - } - ] - }, - "tokenizer": { - "S": "kuromoji_tokenizer" - } + "character_filters": [ + "icu_normalizer" + ], + "token_filters": [ + "kuromoji_baseform", + "kuromoji_part_of_speech" + ], + "tokenizer": "kuromoji_tokenizer" }`, }); expect(template).toBeDefined(); @@ -817,26 +736,14 @@ describe("Bedrock Knowledge Base Stack", () => { maxTokens: 500, overlapPercentage: 10, analyzer: `{ - "character_filters": { - "L": [ - { - "S": "icu_normalizer" - } - ] - }, - "token_filters": { - "L": [ - { - "S": "kuromoji_baseform" - }, - { - "S": "kuromoji_part_of_speech" - } - ] - }, - "tokenizer": { - "S": "kuromoji_tokenizer" - } + "character_filters": [ + "icu_normalizer" + ], + "token_filters": [ + "kuromoji_baseform", + "kuromoji_part_of_speech" + ], + "tokenizer": "kuromoji_tokenizer" }`, }); expect(template).toBeDefined(); @@ -856,26 +763,14 @@ describe("Bedrock Knowledge Base Stack", () => { maxTokens: 500, instruction: "This is an example instruction.", analyzer: `{ - "character_filters": { - "L": [ - { - "S": "icu_normalizer" - } - ] - }, - "token_filters": { - "L": [ - { - "S": "kuromoji_baseform" - }, - { - "S": "kuromoji_part_of_speech" - } - ] - }, - "tokenizer": { - "S": "kuromoji_tokenizer" - } + "character_filters": [ + "icu_normalizer" + ], + "token_filters": [ + "kuromoji_baseform", + "kuromoji_part_of_speech" + ], + "tokenizer": "kuromoji_tokenizer" }`, }); expect(template).toBeDefined(); diff --git a/cdk/test/utils/parameter-models.test.ts b/cdk/test/utils/parameter-models.test.ts index 4938f0dc8..b91dcd013 100644 --- a/cdk/test/utils/parameter-models.test.ts +++ b/cdk/test/utils/parameter-models.test.ts @@ -736,12 +736,12 @@ describe("resolveBedrockCustomBotParameters", () => { ENV_NAME: "testEnv", ENV_PREFIX: "test-prefix", BEDROCK_REGION: "us-east-1", - PK: "env-pk", - SK: "env-sk", + OWNER_USER_ID: "env-pk", + BOT_ID: "env-sk", BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME: "env-bucket", KNOWLEDGE: '{"env": "knowledge"}', - BEDROCK_KNOWLEDGE_BASE: '{"env": "kb"}', - BEDROCK_GUARDRAILS: '{"env": "guardrails"}', + KNOWLEDGE_BASE: '{"env": "kb"}', + GUARDRAILS: '{"env": "guardrails"}', ENABLE_RAG_REPLICAS: "true", }; @@ -753,8 +753,8 @@ describe("resolveBedrockCustomBotParameters", () => { expect(result.bedrockRegion).toBe("us-east-1"); expect(result.envName).toBe("testEnv"); expect(result.envPrefix).toBe("test-prefix"); - expect(result.pk).toBe("env-pk"); - expect(result.sk).toBe("env-sk"); + expect(result.ownerUserId).toBe("env-pk"); + expect(result.botId).toBe("env-sk"); expect(result.documentBucketName).toBe("env-bucket"); expect(result.knowledge).toBe('{"env": "knowledge"}'); expect(result.knowledgeBase).toBe('{"env": "kb"}'); diff --git a/frontend/src/features/knowledgeBase/pages/BotKbEditPage.tsx b/frontend/src/features/knowledgeBase/pages/BotKbEditPage.tsx index 3fcb2799a..1a508692d 100644 --- a/frontend/src/features/knowledgeBase/pages/BotKbEditPage.tsx +++ b/frontend/src/features/knowledgeBase/pages/BotKbEditPage.tsx @@ -1369,7 +1369,6 @@ const BotKbEditPage: React.FC = () => { conversationQuickStarters, navigate, bedrockKnowledgeBaseType, - knowledgeBaseId, existKnowledgeBaseId, embeddingsModel, chunkingStrategy, From c1652350b52d3a86385844f94b760e419bc80891 Mon Sep 17 00:00:00 2001 From: Yukinobu Mine Date: Fri, 31 Oct 2025 06:16:00 +0900 Subject: [PATCH 7/9] Improve efficiency of embedding state machine. - Omit building of shared Knowledge Bases if not necessary. - Automatically delete expired lock files using S3's lifecycle rule. - Add comments and update README.md. --- README.md | 17 +++- backend/app/repositories/custom_bot.py | 6 +- backend/app/repositories/models/custom_bot.py | 5 +- .../app/repositories/models/custom_bot_kb.py | 10 +- backend/app/routes/schemas/bot.py | 50 ++++++++-- backend/app/usecases/bot.py | 12 ++- backend/app/utils.py | 12 +++ backend/app/vector_search.py | 1 + .../bootstrap_state_machine.py | 50 +++++++--- .../finalize_custom_bot_build.py | 3 + .../finalize_shared_knowledge_bases_build.py | 99 ++++++++++++------- .../bedrock_knowledge_base/lock.py | 24 +++-- .../synchronize_data_source.py | 48 +++++---- .../index.ts | 32 +++--- cdk/lib/bedrock-chat-stack.ts | 3 + cdk/lib/bedrock-region-resources.ts | 12 ++- cdk/lib/constructs/embedding.ts | 97 +++++++++++------- 17 files changed, 340 insertions(+), 141 deletions(-) diff --git a/README.md b/README.md index 66b77faf1..f2f32157b 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,21 @@ You can also import existing [Amazon Bedrock's KnowledgeBase](https://aws.amazon > [!Important] > For governance reasons, only allowed users are able to create customized bots. To allow the creation of customized bots, the user must be a member of group called `CreatingBotAllowed`, which can be set up via the management console > Amazon Cognito User pools or aws cli. Note that the user pool id can be referred by accessing CloudFormation > BedrockChatStack > Outputs > `AuthUserPoolIdxxxx`. +### Multi-Tenant Usage of Knowledge Base + +In Amazon Bedrock Knowledge Bases, by default, the number of Knowledge Bases that can be created in a single AWS account is limited to 100. To work around this limitation, you can use 'multi-tenant' mode, where a Knowledge Base with common settings is shared among multiple bots, and files uploaded by each bot are filtered by attaching the Bot ID as metadata. + +Newly created bots will have multi-tenant mode enabled by default. To migrate existing bots to multi-tenant mode, change the bot's knowledge settings to "Create a tenant in a shared Knowledge Base." + +To migrate multiple bots to multi-tenant mode in bulk, execute commands like the following: + +```bash +aws dynamodb execute-statement --statement "UPDATE \"$BotTableNameV3\" SET BedrockKnowledgeBase.type='shared' SET SyncStatus='QUEUED' WHERE PK='$UserID' AND SK='BOT#$BotID'" +# Execute for all target bots + +aws stepfunctions start-execution --state-machine-arn $EmbeddingStateMachineArn +``` + ### Administrative features API Management, Mark bots as essential, Analyze usage for bots. [detail](./docs/ADMINISTRATOR.md) @@ -194,7 +209,7 @@ It's an architecture built on AWS managed services, eliminating the need for inf - [Amazon Cognito](https://aws.amazon.com/cognito/): User authentication - [Amazon Bedrock](https://aws.amazon.com/bedrock/): Managed service to utilize foundational models via APIs - [Amazon Bedrock Knowledge Bases](https://aws.amazon.com/bedrock/knowledge-bases/): Provides a managed interface for Retrieval-Augmented Generation ([RAG](https://aws.amazon.com/what-is/retrieval-augmented-generation/)), offering services for embedding and parsing documents -- [Amazon EventBridge Pipes](https://aws.amazon.com/eventbridge/pipes/): Receiving event from DynamoDB stream and launching Step Functions to embed external knowledge +- [Amazon EventBridge Pipes](https://aws.amazon.com/eventbridge/pipes/): Receiving deletion event of bots from DynamoDB stream and delete CloudFormation stack related to the bot - [AWS Step Functions](https://aws.amazon.com/step-functions/): Orchestrating ingestion pipeline to embed external knowledge into Bedrock Knowledge Bases - [Amazon OpenSearch Serverless](https://aws.amazon.com/opensearch-service/features/serverless/): Serves as the backend database for Bedrock Knowledge Bases, providing full-text search and vector search capabilities, enabling accurate retrieval of relevant information - [Amazon Athena](https://aws.amazon.com/athena/): Query service to analyze S3 bucket diff --git a/backend/app/repositories/custom_bot.py b/backend/app/repositories/custom_bot.py index 8a16f221b..972dae9db 100644 --- a/backend/app/repositories/custom_bot.py +++ b/backend/app/repositories/custom_bot.py @@ -3,7 +3,6 @@ import logging from decimal import Decimal as decimal -from app.config import DEFAULT_GENERATION_CONFIG from app.repositories.common import ( TRANSACTION_BATCH_READ_SIZE, RecordNotFoundError, @@ -22,8 +21,6 @@ ConversationQuickStarterModel, GenerationParamsModel, KnowledgeModel, - UsageStatsModel, - default_active_models, ) from app.repositories.models.custom_bot_guardrails import BedrockGuardrailsModel from app.repositories.models.custom_bot_kb import BedrockKnowledgeBaseModel @@ -163,10 +160,12 @@ def update_bot( and len(knowledge.filenames) == 0 and len(knowledge.s3_urls) == 0 ): + # Clear Knowledge Base ID if the Knowledge Base is not needed. bedrock_knowledge_base.type = None bedrock_knowledge_base.knowledge_base_id = None elif bedrock_knowledge_base.type is None: + # Otherwise, if the type of Knowledge Base is omitted, it will default to `dedicated`. bedrock_knowledge_base.type = "dedicated" update_expression += ", BedrockKnowledgeBase = :bedrock_knowledge_base" @@ -176,6 +175,7 @@ def update_bot( if bedrock_guardrails: if not bedrock_guardrails.is_guardrail_enabled: + # Clear Guardrail ARN if the Guardrail is not needed. bedrock_guardrails.guardrail_arn = "" bedrock_guardrails.guardrail_version = "" diff --git a/backend/app/repositories/models/custom_bot.py b/backend/app/repositories/models/custom_bot.py index 07e479eeb..75fe14b8b 100644 --- a/backend/app/repositories/models/custom_bot.py +++ b/backend/app/repositories/models/custom_bot.py @@ -2,7 +2,6 @@ from typing import Annotated, Any, Dict, List, Literal, Optional, Self, Type, get_args from app.config import DEFAULT_GENERATION_CONFIG -from app.config import GenerationParams as GenerationParamsDict from app.repositories.models.common import DynamicBaseModel, Float, SecureString from app.repositories.models.custom_bot_guardrails import BedrockGuardrailsModel from app.repositories.models.custom_bot_kb import BedrockKnowledgeBaseModel @@ -39,7 +38,6 @@ ) from pydantic import ( BaseModel, - ConfigDict, Discriminator, Field, ValidationInfo, @@ -453,10 +451,12 @@ def validate_knowledge_base_type(self) -> Self: and len(self.knowledge.filenames) == 0 and len(self.knowledge.s3_urls) == 0 ): + # Clear Knowledge Base ID if the Knowledge Base is not needed. self.bedrock_knowledge_base.type = None self.bedrock_knowledge_base.knowledge_base_id = None elif self.bedrock_knowledge_base.type is None: + # Otherwise, if the type of Knowledge Base is omitted, it will default to `dedicated`. self.bedrock_knowledge_base.type = "dedicated" return self @@ -465,6 +465,7 @@ def validate_knowledge_base_type(self) -> Self: def validate_guardrails(self) -> Self: if self.bedrock_guardrails is not None: if not self.bedrock_guardrails.is_guardrail_enabled: + # Clear Guardrail ARN if the Guardrail is not needed. self.bedrock_guardrails.guardrail_arn = "" self.bedrock_guardrails.guardrail_version = "" diff --git a/backend/app/repositories/models/custom_bot_kb.py b/backend/app/repositories/models/custom_bot_kb.py index 94bf71411..14518fcf1 100644 --- a/backend/app/repositories/models/custom_bot_kb.py +++ b/backend/app/repositories/models/custom_bot_kb.py @@ -13,7 +13,7 @@ type_kb_resource_type, ) from typing import Literal -from pydantic import BaseModel, validator, model_validator +from pydantic import BaseModel class SearchParamsModel(BaseModel): @@ -100,6 +100,14 @@ class BedrockKnowledgeBaseModel(BaseModel): def calc_knowledge_base_hash(knowledge_base: BedrockKnowledgeBaseModel) -> str: + """Calculate hashcode of Knowledge Base settings. + + Args: + knowledge_base (BedrockKnowledgeBaseModel): Knowledge Base settings + + Returns: + str: BASE32 encoded MD5 hashcode of JSON-formatted Knowledge Base settings. + """ return ( base64.b32encode( md5( diff --git a/backend/app/routes/schemas/bot.py b/backend/app/routes/schemas/bot.py index 2a783e65c..ff5fde622 100644 --- a/backend/app/routes/schemas/bot.py +++ b/backend/app/routes/schemas/bot.py @@ -22,13 +22,11 @@ BedrockKnowledgeBaseOutput, ) from app.routes.schemas.conversation import type_model_name -from charset_normalizer.utils import is_punctuation from pydantic import ( Discriminator, Field, create_model, field_validator, - model_validator, validator, ) @@ -356,10 +354,11 @@ def is_embedding_required(self, current_bot_model: BotModel) -> bool: else: return True - if ( - self.bedrock_knowledge_base is not None - and current_bot_model.bedrock_knowledge_base is not None - ): + # Check if the Knowledge Base settings have been changed. + if self.bedrock_knowledge_base is not None: + if current_bot_model.bedrock_knowledge_base is None: + return True + knowledge_base_hash = calc_knowledge_base_hash( BedrockKnowledgeBaseModel.model_validate( self.bedrock_knowledge_base.model_dump() @@ -371,8 +370,47 @@ def is_embedding_required(self, current_bot_model: BotModel) -> bool: if knowledge_base_hash != current_knowledge_base_hash: return True + elif current_bot_model.bedrock_knowledge_base is not None: + return True + return False + def is_sync_shared_knowledge_bases_required( + self, current_bot_model: BotModel + ) -> bool: + """Check if there have been any changes to the shared Knowledge Bases. + + Args: + current_bot_model (BotModel): Current bot settings. + + Returns: + bool: Whether there have been any changes to the shared Knowledge Bases. + """ + if self.bedrock_knowledge_base is None: + if current_bot_model.bedrock_knowledge_base is None: + return False + + return current_bot_model.bedrock_knowledge_base.type == "shared" + + elif current_bot_model.bedrock_knowledge_base is None: + return self.bedrock_knowledge_base.type == "shared" + + if ( + self.bedrock_knowledge_base.type != "shared" + and current_bot_model.bedrock_knowledge_base.type != "shared" + ): + return False + + knowledge_base_hash = calc_knowledge_base_hash( + BedrockKnowledgeBaseModel.model_validate( + self.bedrock_knowledge_base.model_dump() + ) + ) + current_knowledge_base_hash = calc_knowledge_base_hash( + current_bot_model.bedrock_knowledge_base + ) + return knowledge_base_hash != current_knowledge_base_hash + class BotModifyOutput(BaseSchema): id: str diff --git a/backend/app/usecases/bot.py b/backend/app/usecases/bot.py index 7515c6d2f..dd7fda391 100644 --- a/backend/app/usecases/bot.py +++ b/backend/app/usecases/bot.py @@ -4,7 +4,6 @@ from app.agents.tools.agent_tool import AgentTool as LegacyAgentTool from app.config import DEFAULT_GENERATION_CONFIG -from app.config import GenerationParams as GenerationParamsDict from app.repositories.common import RecordNotFoundError from app.repositories.custom_bot import ( alias_exists, @@ -30,7 +29,6 @@ update_bot_stats, ) from app.repositories.models.custom_bot import ( - ActiveModelsModel, AgentModel, BotAliasModel, BotModel, @@ -44,7 +42,6 @@ from app.routes.schemas.admin import ( PushBotInput, PushBotInputPinned, - PushBotInputUnpinned, ) from app.routes.schemas.bot import ( ActiveModelsOutput, @@ -145,12 +142,17 @@ def create_new_bot(user: User, bot_input: BotInput) -> BotOutput: store_bot(new_bot) if new_bot.sync_status == "QUEUED": + # If necessary, start the Embedding state machine. start_embedding_state_machine( user_id=user.id, bot_id=new_bot.id, added_filenames=filenames, unchanged_filenames=[], deleted_filenames=[], + sync_shared_knowledge_bases_required=( + new_bot.bedrock_knowledge_base is not None + and new_bot.bedrock_knowledge_base.type == "shared" + ), ) return new_bot.to_output() @@ -283,12 +285,16 @@ def modify_owned_bot( ) if sync_status == "QUEUED": + # If necessary, start the Embedding state machine. start_embedding_state_machine( user_id=user.id, bot_id=bot.id, added_filenames=added_filenames, unchanged_filenames=unchanged_filenames, deleted_filenames=deleted_filenames, + sync_shared_knowledge_bases_required=( + modify_input.is_sync_shared_knowledge_bases_required(bot) + ), ) return BotModifyOutput( diff --git a/backend/app/utils.py b/backend/app/utils.py index 403b742bc..8d9fa27fd 100644 --- a/backend/app/utils.py +++ b/backend/app/utils.py @@ -321,7 +321,18 @@ def start_embedding_state_machine( added_filenames: list[str], unchanged_filenames: list[str], deleted_filenames: list[str], + sync_shared_knowledge_bases_required: bool, ): + """Start the Embedding state machine for `QUEUED` bot. + + Args: + user_id (str): User ID of the bot. + bot_id (str): Bot ID. + added_filenames (list[str]): Files added to the bot. + unchanged_filenames (list[str]): Files unchanged in the bot. + deleted_filenames (list[str]): Files deleted from the bot. + sync_shared_knowledge_bases_required (bool): Whether there have been any changes to the shared Knowledge Bases. + """ client = boto3.client("stepfunctions") client.start_execution( stateMachineArn=EMBEDDING_STATE_MACHINE_ARN, @@ -336,6 +347,7 @@ def start_embedding_state_machine( "Unchanged": unchanged_filenames, "Deleted": deleted_filenames, }, + "SyncSharedKnowledgeBasesRequired": sync_shared_knowledge_bases_required, }, ], } diff --git a/backend/app/vector_search.py b/backend/app/vector_search.py index 2357233fb..2ac7e57c6 100644 --- a/backend/app/vector_search.py +++ b/backend/app/vector_search.py @@ -87,6 +87,7 @@ def _bedrock_knowledge_base_search(bot: BotModel, query: str) -> list[SearchResu }, } if bot.bedrock_knowledge_base.type == "shared": + # Specify the Bot ID as a filter condition for the shared Knowledge Base. retrieve_parameter["retrievalConfiguration"]["vectorSearchConfiguration"]["filter"] = { # type: ignore "listContains": { "key": "tenants", diff --git a/backend/embedding_statemachine/bedrock_knowledge_base/bootstrap_state_machine.py b/backend/embedding_statemachine/bedrock_knowledge_base/bootstrap_state_machine.py index 791d201ce..93196a917 100644 --- a/backend/embedding_statemachine/bedrock_knowledge_base/bootstrap_state_machine.py +++ b/backend/embedding_statemachine/bedrock_knowledge_base/bootstrap_state_machine.py @@ -14,14 +14,25 @@ def handler(event, context): + """To build the information necessary for processing the embedded state machine, retrieve information about bots and shared Knowledge Bases from the database.""" queued_bots_from_event = event.get("QueuedBots") if queued_bots_from_event is not None: + # The bots to be processed were specified in the input. queued_bots = get_queued_bots_from_event(queued_bots_from_event) else: + # Otherwise, retrieve bots with `SynsStatus of `QUEUED` from the database. queued_bots = get_queued_bots() - shared_knowledge_bases = find_shared_knowledge_bases() + if not queued_bots or any( + queued_bot["sync_shared_knowledge_bases_required"] for queued_bot in queued_bots + ): + # If there are updates to shared Knowledge Bases, or the state machine is started without specifying a queued bot, retrieve information about the shared Knowledge Bases from the database. + shared_knowledge_bases = find_shared_knowledge_bases() + + else: + # Otherwise, skip the processing related to shared Knowledge Bases. + shared_knowledge_bases = None return { "QueuedBots": [ @@ -62,19 +73,23 @@ def handler(event, context): } for queued_bot in queued_bots ], - "SharedKnowledgeBases": [ - { - "KnowledgeBaseHash": shared_knowledge_base["knowledge_base_hash"], - "KnowledgeBase": shared_knowledge_base["knowledge_base"].model_dump( - exclude={ - "knowledge_base_id", - "exist_knowledge_base_id", - "data_source_ids", - } - ), - } - for shared_knowledge_base in shared_knowledge_bases - ], + "SharedKnowledgeBases": ( + [ + { + "KnowledgeBaseHash": shared_knowledge_base["knowledge_base_hash"], + "KnowledgeBase": shared_knowledge_base["knowledge_base"].model_dump( + exclude={ + "knowledge_base_id", + "exist_knowledge_base_id", + "data_source_ids", + } + ), + } + for shared_knowledge_base in shared_knowledge_bases + ] + if shared_knowledge_bases is not None + else None + ), } @@ -87,6 +102,7 @@ class FilesDiff(TypedDict): class QueuedBot(TypedDict): bot: BotModel files_diff: NotRequired[FilesDiff] + sync_shared_knowledge_bases_required: bool def get_queued_bots_from_event(queued_bots_from_event: list[dict]) -> list[QueuedBot]: @@ -97,6 +113,9 @@ def get_queued_bots_from_event(queued_bots_from_event: list[dict]) -> list[Queue if user_id and bot_id: bot = find_bot_by_id(bot_id) files_diff = queued_bot.get("FilesDiff", {}) + sync_shared_knowledge_bases_required = queued_bot.get( + "SyncSharedKnowledgeBasesRequired", True + ) added_files = files_diff.get("Added", []) unchanged_files = files_diff.get("Unchanged", []) @@ -110,6 +129,7 @@ def get_queued_bots_from_event(queued_bots_from_event: list[dict]) -> list[Queue "Unchanged": unchanged_files, "Deleted": deleted_files, }, + "sync_shared_knowledge_bases_required": sync_shared_knowledge_bases_required, } ) @@ -117,6 +137,7 @@ def get_queued_bots_from_event(queued_bots_from_event: list[dict]) -> list[Queue result.append( { "bot": bot, + "sync_shared_knowledge_bases_required": sync_shared_knowledge_bases_required, } ) @@ -128,6 +149,7 @@ def get_queued_bots() -> list[QueuedBot]: return [ { "bot": bot, + "sync_shared_knowledge_bases_required": True, } for bot in bots ] diff --git a/backend/embedding_statemachine/bedrock_knowledge_base/finalize_custom_bot_build.py b/backend/embedding_statemachine/bedrock_knowledge_base/finalize_custom_bot_build.py index b8e3fa403..188c1b8d3 100644 --- a/backend/embedding_statemachine/bedrock_knowledge_base/finalize_custom_bot_build.py +++ b/backend/embedding_statemachine/bedrock_knowledge_base/finalize_custom_bot_build.py @@ -30,6 +30,7 @@ class DataSource(TypedDict): def handler(event, context): + """Obtain the ID of the dedicated Knowledge Bases and Guardrails built by `BrChatKbStackXXX`, and update `knowledge_base_id` and `guardrail_arn` of the bot.""" user_id = event["OwnerUserId"] bot_id = event["BotId"] @@ -75,6 +76,7 @@ def handler(event, context): if "OutputKey" in output and "OutputValue" in output ) + # Update `knowledge_base_id` of the bot using dedicated Knowledge Base. knowledge_base_id = stack_outputs.get("KnowledgeBaseId") if knowledge_base_id: data_source_ids: List[str] = [ @@ -92,6 +94,7 @@ def handler(event, context): ) update_knowledge_base_id(user_id, bot_id, knowledge_base_id, data_source_ids) + # Update `guardrail_arn` of the bot using dedicated Guardrail. guardrail_arn = stack_outputs.get("GuardrailArn") guardrail_version = stack_outputs.get("GuardrailVersion") if guardrail_arn and guardrail_version: diff --git a/backend/embedding_statemachine/bedrock_knowledge_base/finalize_shared_knowledge_bases_build.py b/backend/embedding_statemachine/bedrock_knowledge_base/finalize_shared_knowledge_bases_build.py index 6fbe65e25..db908de47 100644 --- a/backend/embedding_statemachine/bedrock_knowledge_base/finalize_shared_knowledge_bases_build.py +++ b/backend/embedding_statemachine/bedrock_knowledge_base/finalize_shared_knowledge_bases_build.py @@ -1,5 +1,5 @@ import os -from typing import List, TypedDict +from typing import TypedDict import boto3 from app.repositories.custom_bot import update_knowledge_base_id @@ -38,6 +38,7 @@ class DataSource(TypedDict): def handler(event, context): + """Obtain the ID of the shared Knowledge Bases built by `BrChatSharedKbStack`, and update `knowledge_base_id` of the referring bots.""" queued_bots = event["QueuedBots"] shared_knowledge_bases = event["SharedKnowledgeBases"] @@ -56,6 +57,7 @@ def handler(event, context): if "OutputKey" in output and "OutputValue" in output ) + # Dict of Knowledge Bases built by `BrChatSharedKbStack`. Key is knowledge base hash. knowledge_bases: dict[str, KnowledgeBase] = {} for shared_knowledge_base in shared_knowledge_bases: @@ -72,52 +74,77 @@ def handler(event, context): "queued_bots": [], } - data_sources: list[DataSource] = [] - - for queued_bot in queued_bots: - knowledge_base_hash = queued_bot.get("KnowledgeBaseHash") - if knowledge_base_hash and knowledge_base_hash in knowledge_bases: - knowledge_base = knowledge_bases[knowledge_base_hash] - if "FilesDiff" in queued_bot: - queued_bot["DataSources"] = [ + # Dict of data sources that require entire synchronization. Key is data source ID. + data_sources: dict[str, DataSource] = {} + + if queued_bots: + # If there are `QUEUED` bots, synchronize only shared Knowledge Bases that they reference. + for queued_bot in queued_bots: + knowledge_base_hash = queued_bot.get("KnowledgeBaseHash") + if knowledge_base_hash and knowledge_base_hash in knowledge_bases: + knowledge_base = knowledge_bases[knowledge_base_hash] + if "FilesDiff" in queued_bot: + # If the bot specifies which files should be ingested, add the data sources as the ingestion target for that bot. + queued_bot["DataSources"] = [ + { + "KnowledgeBaseId": knowledge_base["knowledge_base_id"], + "DataSourceId": data_source_id, + } + for data_source_id in knowledge_base["data_source_ids"] + ] + + else: + # Otherwise, the data sources to be synchronized entirely. + data_sources.update( + ( + data_source_id, + { + "KnowledgeBaseId": knowledge_base["knowledge_base_id"], + "DataSourceId": data_source_id, + "FilesDiffs": [], + }, + ) + for data_source_id in knowledge_base["data_source_ids"] + ) + pass + + # Add the bots referencing this Knowledge Base to the list. + knowledge_base["queued_bots"].append( { - "KnowledgeBaseId": knowledge_base["knowledge_base_id"], - "DataSourceId": data_source_id, + "user_id": queued_bot["OwnerUserId"], + "bot_id": queued_bot["BotId"], } - for data_source_id in knowledge_base["data_source_ids"] - ] + ) - else: - data_sources.extend( - { - "KnowledgeBaseId": knowledge_base["knowledge_base_id"], - "DataSourceId": data_source_id, - "FilesDiffs": [], - } - for data_source_id in knowledge_base["data_source_ids"] + # Update `knowledge_base_id` of the bots using shared Knowledge Bases. + for knowledge_base in knowledge_bases.values(): + for queued_bot in knowledge_base["queued_bots"]: + update_knowledge_base_id( + user_id=queued_bot["user_id"], + bot_id=queued_bot["bot_id"], + knowledge_base_id=knowledge_base["knowledge_base_id"], + data_source_ids=knowledge_base["data_source_ids"], ) - pass - knowledge_base["queued_bots"].append( + else: + # Otherwise, synchronize all shared Knowledge Bases. + data_sources.update( + ( + data_source_id, { - "user_id": queued_bot["OwnerUserId"], - "bot_id": queued_bot["BotId"], - } - ) - - for knowledge_base in knowledge_bases.values(): - for queued_bot in knowledge_base["queued_bots"]: - update_knowledge_base_id( - user_id=queued_bot["user_id"], - bot_id=queued_bot["bot_id"], - knowledge_base_id=knowledge_base["knowledge_base_id"], - data_source_ids=knowledge_base["data_source_ids"], + "KnowledgeBaseId": knowledge_base["knowledge_base_id"], + "DataSourceId": data_source_id, + "FilesDiffs": [], + }, ) + for knowledge_base in knowledge_bases.values() + for data_source_id in knowledge_base["data_source_ids"] + ) return { "QueuedBots": queued_bots, "SharedKnowledgeBases": shared_knowledge_bases, - "DataSources": data_sources, + "DataSources": list(data_sources.values()), **( { "Lock": event["Lock"], diff --git a/backend/embedding_statemachine/bedrock_knowledge_base/lock.py b/backend/embedding_statemachine/bedrock_knowledge_base/lock.py index a7deacc94..13cc41b16 100644 --- a/backend/embedding_statemachine/bedrock_knowledge_base/lock.py +++ b/backend/embedding_statemachine/bedrock_knowledge_base/lock.py @@ -1,5 +1,4 @@ import os -from datetime import datetime, timedelta import boto3 @@ -13,6 +12,7 @@ def handler(event, context): + """Distributed locking using Amazon S3's conditional write feature.""" action = event["Action"] match action: case "Acquire": @@ -29,15 +29,19 @@ class RetryException(Exception): pass +def lock_name_to_s3_key(lock_name: str) -> str: + return f".temp/.lock.{lock_name.lower()}" + + def handle_acquire(event): - lock_name = event["LockName"] + """Acquire a distributed lock.""" + lock_file_key = lock_name_to_s3_key(event["LockName"]) owner = event["Owner"] - expires_seconds = event["ExpiresSeconds"] try: + # Create the lock file only if it does not already exist. Content is the owner ID. response = s3.put_object( Bucket=DOCUMENT_BUCKET, - Key=f".lock.{lock_name.lower()}", - Expires=datetime.now() + timedelta(seconds=expires_seconds), + Key=lock_file_key, IfNoneMatch="*", Body=owner, ) @@ -52,9 +56,10 @@ def handle_acquire(event): match error_code: case "PreconditionFailed": try: + # Check the owner ID because there is a possibility that `PreconditionFailed` occurred due to a retry caused by a network error, etc. get_response = s3.get_object( Bucket=DOCUMENT_BUCKET, - Key=f".lock.{lock_name.lower()}", + Key=lock_file_key, ) body = get_response["Body"].read().decode() if body != owner: @@ -76,12 +81,14 @@ def handle_acquire(event): def handle_release(event): - lock_name = event["LockName"] + """Release the acquired lock.""" + lock_file_key = lock_name_to_s3_key(event["LockName"]) lock_id = event["LockId"] try: + # Delete the lock file only if it have the same `ETag` as when it was created. s3.delete_object( Bucket=DOCUMENT_BUCKET, - Key=f".lock.{lock_name.lower()}", + Key=lock_file_key, IfMatch=lock_id, ) @@ -89,6 +96,7 @@ def handle_release(event): error_code = ex.response.get("Error", {}).get("Code") match error_code: case "PreconditionFailed": + # If the lock file was replaced by another owner, the release of the lock is considered successful. pass case "ConditionalRequestConflict": diff --git a/backend/embedding_statemachine/bedrock_knowledge_base/synchronize_data_source.py b/backend/embedding_statemachine/bedrock_knowledge_base/synchronize_data_source.py index 1866459cb..48942957f 100644 --- a/backend/embedding_statemachine/bedrock_knowledge_base/synchronize_data_source.py +++ b/backend/embedding_statemachine/bedrock_knowledge_base/synchronize_data_source.py @@ -12,6 +12,7 @@ def handler(event, context): + """Perform data source synchronization for a Knowledge Base.""" match event["Action"]: case "Ingest": return handle_ingest(event) @@ -41,6 +42,7 @@ def get_data_source_type(knowledge_base_id: str, data_source_id: str): def handle_ingest(event): + """Perform direct ingestion or entire synchronization into the data source""" knowledge_base_id = event["KnowledgeBaseId"] data_source_id = event["DataSourceId"] @@ -53,6 +55,7 @@ def handle_ingest(event): ) == "S3" ): + # If the bot specifies which files should be ingested, perform direct ingestion. added_documents: list[str] = [] unchanged_documents: list[str] = [] deleted_documents: list[str] = [] @@ -74,6 +77,7 @@ def handle_ingest(event): for deleted_file in bot_files_diff["Deleted"] ) + # If an 'unchanged' document doesn't actually exist, treat it as an 'added' document. for i in range(0, len(unchanged_documents), 10): get_documents_response = bedrock_agent.get_knowledge_base_documents( knowledgeBaseId=knowledge_base_id, @@ -99,6 +103,7 @@ def handle_ingest(event): "Deleted": [], } + # Ingest 'added' documents to the data source. for i in range(0, len(added_documents), 10): ingest_response = bedrock_agent.ingest_knowledge_base_documents( knowledgeBaseId=knowledge_base_id, @@ -123,6 +128,7 @@ def handle_ingest(event): if document["status"] != "IGNORED" and "s3" in document["identifier"] ) + # Delete 'deleted' documents from the data source. for i in range(0, len(deleted_documents), 10): delete_response = bedrock_agent.delete_knowledge_base_documents( knowledgeBaseId=knowledge_base_id, @@ -151,6 +157,7 @@ def handle_ingest(event): } else: + # Otherwise, start the entire synchronization job. start_job_response = bedrock_agent.start_ingestion_job( knowledgeBaseId=knowledge_base_id, dataSourceId=data_source_id, @@ -170,12 +177,14 @@ class RetryException(Exception): def handle_check(event): + """Check for the completion of direct ingestion or entire synchronization.""" ingestion_job = event["IngestionJob"] knowledge_base_id = ingestion_job["KnowledgeBaseId"] data_source_id = ingestion_job["DataSourceId"] documents_diff = ingestion_job.get("DocumentsDiff") if documents_diff: + # Check for the completion of indexing of 'added' documents. added_documents = documents_diff["Added"] for i in range(0, len(added_documents), 10): get_documents_response = bedrock_agent.get_knowledge_base_documents( @@ -204,6 +213,7 @@ def handle_check(event): case _: raise Exception(f"File {uri}: Bad status '{status}'.") + # Check for the absence of 'deleted' documents. deleted_documents = documents_diff["Deleted"] for i in range(0, len(deleted_documents), 10): get_documents_response = bedrock_agent.get_knowledge_base_documents( @@ -234,26 +244,28 @@ def handle_check(event): return - ingestion_job_id = ingestion_job.get("IngestionJobId") - if ingestion_job_id: - get_job_response = bedrock_agent.get_ingestion_job( - knowledgeBaseId=knowledge_base_id, - dataSourceId=data_source_id, - ingestionJobId=ingestion_job_id, - ) - status = get_job_response["ingestionJob"]["status"] - match status: - case "COMPLETE": - pass + else: + # Check the completion of entire synchronization job. + ingestion_job_id = ingestion_job.get("IngestionJobId") + if ingestion_job_id: + get_job_response = bedrock_agent.get_ingestion_job( + knowledgeBaseId=knowledge_base_id, + dataSourceId=data_source_id, + ingestionJobId=ingestion_job_id, + ) + status = get_job_response["ingestionJob"]["status"] + match status: + case "COMPLETE": + pass - case "STARTING" | "IN_PROGRESS": - raise RetryException() + case "STARTING" | "IN_PROGRESS": + raise RetryException() - case _: - raise Exception( - f"Ingestion Job '{ingestion_job_id}': Bad status '{status}'." - ) + case _: + raise Exception( + f"Ingestion Job '{ingestion_job_id}': Bad status '{status}'." + ) - return + return raise Exception("Invalid parameters.") diff --git a/cdk/lambda/knowledge-base-custom-transformation/index.ts b/cdk/lambda/knowledge-base-custom-transformation/index.ts index cd91cf342..0c7ae903e 100644 --- a/cdk/lambda/knowledge-base-custom-transformation/index.ts +++ b/cdk/lambda/knowledge-base-custom-transformation/index.ts @@ -1,5 +1,9 @@ import { type Handler } from 'aws-lambda'; +/** + * Custom transformation function used in the S3 data source of a shared Knowledge Base. + * https://docs.aws.amazon.com/bedrock/latest/userguide/kb-custom-transformation.html + */ export const handler: Handler = async (event, context) => { console.log(`Event: ${JSON.stringify(event)}`); @@ -10,24 +14,26 @@ export const handler: Handler = async (event, context) => { const originalFileLocation = file.originalFileLocation; const s3Uri = new URL(originalFileLocation.s3_location.uri); - const fileName = s3Uri.pathname.match(/^(\/[^/]+)*\/(?[^/]+)$/)?.groups?.fileName; - if (fileName?.startsWith('.')) { - console.log(`Ignored dotfile '${fileName}'`); + // Ignore files that do not exist in the directory for bots. + const groups = s3Uri.pathname.match(/^\/(?[^/]+)\/(?[^/]+)\/documents\/(?[^/]+)$/)?.groups; + const userId = groups?.userId; + const botId = groups?.botId; + if (groups == null || botId == null || userId === '.temp') { return []; } - const botId = s3Uri.pathname.match(/^\/(?[^/]+)\/(?[^/]+)\/documents\/(?[^/]+)$/)?.groups?.botId; - return [{ - originalFileLocation, - ...(botId != null ? { + // For files uploaded by bots, set the bot's ID in the metadata field named 'tenants'. + return [ + { + originalFileLocation, + contentBatches: file.contentBatches, fileMetadata: { - tenants: [ - `BOT#${botId}`, - ], + tenants: [ + `BOT#${botId}`, + ], }, - } : undefined), - contentBatches: file.contentBatches, - }]; + }, + ]; }), }; }; diff --git a/cdk/lib/bedrock-chat-stack.ts b/cdk/lib/bedrock-chat-stack.ts index 5b6dd4fad..73d2f9e60 100644 --- a/cdk/lib/bedrock-chat-stack.ts +++ b/cdk/lib/bedrock-chat-stack.ts @@ -365,5 +365,8 @@ export class BedrockChatStack extends cdk.Stack { value: largeMessageBucket.bucketName, exportName: `${props.envPrefix}${sepHyphen}BedrockClaudeChatLargeMessageBucketName`, }); + new CfnOutput(this, 'EmbeddingStateMachineArn', { + value: embedding.stateMachine.stateMachineArn, + }); } } diff --git a/cdk/lib/bedrock-region-resources.ts b/cdk/lib/bedrock-region-resources.ts index 481600600..5a70cabcf 100644 --- a/cdk/lib/bedrock-region-resources.ts +++ b/cdk/lib/bedrock-region-resources.ts @@ -1,4 +1,10 @@ -import { CfnOutput, RemovalPolicy, Stack, StackProps } from "aws-cdk-lib"; +import { + CfnOutput, + Duration, + RemovalPolicy, + Stack, + StackProps, +} from "aws-cdk-lib"; import { Construct } from "constructs"; import { BlockPublicAccess, @@ -42,6 +48,10 @@ export class BedrockRegionResourcesStack extends Stack { serverAccessLogsBucket: accessLogBucket, serverAccessLogsPrefix: "DocumentBucket", }); + this.documentBucket.addLifecycleRule({ + prefix: ".temp/", + expiration: Duration.days(1), + }); new CfnOutput(this, "DocumentBucketName", { value: this.documentBucket.bucketName, diff --git a/cdk/lib/constructs/embedding.ts b/cdk/lib/constructs/embedding.ts index 2904672cc..c8765e18f 100644 --- a/cdk/lib/constructs/embedding.ts +++ b/cdk/lib/constructs/embedding.ts @@ -245,6 +245,7 @@ export class Embedding extends Construct { } private setupStateMachine(props: EmbeddingProps): sfn.StateMachine { + // To build the information necessary for processing the embedded state machine, retrieve information about bots and shared Knowledge Bases from the database. const bootstrapStateMachine = new tasks.LambdaInvoke(this, "BootstrapStateMachine", { lambdaFunction: this._bootstrapStateMachineHandler, resultSelector: { @@ -253,12 +254,16 @@ export class Embedding extends Construct { }, }); - const acquireLockForSharedKnowledgeBases = this.createAcquireLock("ForSharedKnowledgeBases", { + const checkSyncSharedKnowledgeBasesRequired = new sfn.Choice(this, "CheckSyncSharedKnowledgeBasesRequired"); + + // Acquire a distributed lock for shared Knowledge Bases. + const acquireLockForSharedKnowledgeBases = this.createAcquireLockTask("ForSharedKnowledgeBases", { name: "shared-knowledge-bases", resultPath: "$.Lock", }); - const releaseLockForSharedKnowledgeBasesOnFailed = this.createReleaseLock("ForSharedKnowledgeBasesOnFailed", { + // Release the acquired lock for shared Knowledge Bases. + const releaseLockForSharedKnowledgeBasesOnFailed = this.createReleaseLockTask("ForSharedKnowledgeBasesOnFailed", { name: "shared-knowledge-bases", lockId: sfn.JsonPath.stringAt("$.Lock.LockId"), }); @@ -333,6 +338,7 @@ export class Embedding extends Construct { resultPath: "$.Error", }) + // Obtain the ID of the shared Knowledge Bases built by `BrChatSharedKbStack`, and update `knowledge_base_id` of the referring bots. const finalizeSharedKnowledgeBasesBuild = new tasks.LambdaInvoke(this, "FinalizeSharedKnowledgeBasesBuild", { lambdaFunction: this._finalizeSharedKnowledgeBasesBuildHandler, resultSelector: { @@ -348,7 +354,8 @@ export class Embedding extends Construct { resultPath: sfn.JsonPath.DISCARD, maxConcurrency: 1, }); - const ingestionJobForSharedKnowledgeBases = this.createIngestionJob("Shared"); + // Perform entire synchronization into the data source of shared Knowledge Bases. + const ingestionJobForSharedKnowledgeBases = this.createIngestionTask("Shared", {}); const updateSyncStatusFailedForSharedKnowledgeBases = new tasks.LambdaInvoke(this, "UpdateSyncStatusFailedForSharedKnowledgeBases", { lambdaFunction: this._updateSyncStatusHandler, @@ -373,7 +380,8 @@ export class Embedding extends Construct { resultPath: "$.Error", }); - const releaseLockForSharedKnowledgeBases = this.createReleaseLock("ForSharedKnowledgeBases", { + // Release the acquired lock for shared Knowledge Bases. + const releaseLockForSharedKnowledgeBases = this.createReleaseLockTask("ForSharedKnowledgeBases", { name: "shared-knowledge-bases", lockId: sfn.JsonPath.stringAt("$.Lock.LockId"), }); @@ -383,7 +391,8 @@ export class Embedding extends Construct { resultPath: sfn.JsonPath.DISCARD, }); - const acquireLockForCustomBot = this.createAcquireLock("ForCustomBot", { + // Acquire a distributed lock for custom bots. + const acquireLockForCustomBot = this.createAcquireLockTask("ForCustomBot", { name: sfn.JsonPath.format("custombot-{}", sfn.JsonPath.stringAt("$.BotId")), resultPath: "$.Lock", }); @@ -427,7 +436,8 @@ export class Embedding extends Construct { resultPath: sfn.JsonPath.DISCARD, }); - const releaseLockForCustomBot = this.createReleaseLock("ForCustomBot", { + // Release the acquired lock for custom bots. + const releaseLockForCustomBot = this.createReleaseLockTask("ForCustomBot", { name: sfn.JsonPath.format("custombot-{}", sfn.JsonPath.stringAt("$.BotId")), lockId: sfn.JsonPath.stringAt("$.Lock.LockId"), }); @@ -470,6 +480,7 @@ export class Embedding extends Construct { resultPath: "$.Error", }); + // Obtain the ID of the dedicated Knowledge Bases and Guardrails built by `BrChatKbStackXXX`, and update `knowledge_base_id` and `guardrail_arn` of the bot. const finalizeCustomBotBuild = new tasks.LambdaInvoke(this, "FinalizeCustomBotBuild", { lambdaFunction: this._finalizeCustomBotBuildHandler, resultSelector: { @@ -485,7 +496,8 @@ export class Embedding extends Construct { resultPath: sfn.JsonPath.DISCARD, maxConcurrency: 1, }); - const ingestionJobForCustomBot = this.createIngestionJob("CustomBot"); + // Perform direct ingestion or entire synchronization into the data source of dedicated Knowledge Bases. + const ingestionJobForCustomBot = this.createIngestionTask("CustomBot", {}); const updateSyncStatusFailedForCustomBot = new tasks.LambdaInvoke(this, "UpdateSyncStatusFailedForCustomBot", { lambdaFunction: this._updateSyncStatusHandler, @@ -527,29 +539,37 @@ export class Embedding extends Construct { const definition = ( bootstrapStateMachine - .next(acquireLockForSharedKnowledgeBases) - .next(updateSyncStatusRunning) - .next(buildSharedKnowledgeBases) - .next(finalizeSharedKnowledgeBasesBuild) - .next( - mapIngestionJobsForSharedKnowledgeBases.itemProcessor( - ingestionJobForSharedKnowledgeBases - ) - ) - .next(releaseLockForSharedKnowledgeBases) .next( - mapQueuedBots.itemProcessor( - acquireLockForCustomBot - .next(startCustomBotBuild) - .next(finalizeCustomBotBuild) - .next( - mapIngestionJobsForCustomBot.itemProcessor( - ingestionJobForCustomBot + checkSyncSharedKnowledgeBasesRequired + .when(sfn.Condition.isNotNull("$.SharedKnowledgeBases"), ( + // If there are updates to the shared Knowledge Base, build shared Knowledge Bases and synchronize data sources. + acquireLockForSharedKnowledgeBases + .next(updateSyncStatusRunning) + .next(buildSharedKnowledgeBases) + .next(finalizeSharedKnowledgeBasesBuild) + .next( + mapIngestionJobsForSharedKnowledgeBases.itemProcessor( + ingestionJobForSharedKnowledgeBases + ) ) + .next(releaseLockForSharedKnowledgeBases) + .next(mapQueuedBots) + )) + .otherwise( + // Otherwise, skip the processing related to shared Knowledge Bases. + mapQueuedBots.itemProcessor( + acquireLockForCustomBot + .next(startCustomBotBuild) + .next(finalizeCustomBotBuild) + .next( + mapIngestionJobsForCustomBot.itemProcessor( + ingestionJobForCustomBot + ) + ) + .next(updateSyncStatusSucceeded) + .next(syncCustomBotFinished) ) - .next(updateSyncStatusSucceeded) - .next(syncCustomBotFinished) - ) + ) ) ); @@ -656,7 +676,12 @@ export class Embedding extends Construct { return removalHandler; } - private createIngestionJob(idSuffix: string) { + private createIngestionTask(idSuffix: string, { + timeout = Duration.hours(12), + }: { + timeout?: Duration, + }) { + // Perform direct ingestion or entire synchronization into the data source const startIngestionJob = new tasks.LambdaInvoke(this, `StartIngestionJob${idSuffix}`, { lambdaFunction: this._synchronizeDataSourceHandler, payload: sfn.TaskInput.fromObject({ @@ -674,6 +699,7 @@ export class Embedding extends Construct { resultPath: "$.IngestionJob", }); + // Check for the completion of direct ingestion or entire synchronization. const checkIngestionJob = new tasks.LambdaInvoke(this, `CheckIngestionJob${idSuffix}`, { lambdaFunction: this._synchronizeDataSourceHandler, payload: sfn.TaskInput.fromObject({ @@ -688,7 +714,7 @@ export class Embedding extends Construct { .next( checkIngestionJob.addRetry({ interval: Duration.seconds(15), - maxAttempts: 12 * 60 * 60 / 15, + maxAttempts: timeout.toSeconds() / 15, backoffRate: 1, errors: [ 'RetryException', @@ -699,24 +725,24 @@ export class Embedding extends Construct { ).next(ingestionComplete) } - private createAcquireLock(idSuffix: string, { + private createAcquireLockTask(idSuffix: string, { name, owner = sfn.JsonPath.executionName, - expires = Duration.hours(6), + timeout = Duration.hours(12), resultPath, }: { name: string; owner?: string; - expires?: Duration; + timeout?: Duration; resultPath?: string; }) { + // Acquire a distributed lock. return new tasks.LambdaInvoke(this, `AcquireLock${idSuffix}`, { lambdaFunction: this._lockHandler, payload: sfn.TaskInput.fromObject({ Action: "Acquire", LockName: name, Owner: owner, - ExpiresSeconds: expires.toSeconds(), }), resultSelector: { LockId: sfn.JsonPath.stringAt("$.Payload.LockId"), @@ -724,7 +750,7 @@ export class Embedding extends Construct { resultPath: resultPath, }).addRetry({ interval: Duration.seconds(15), - maxAttempts: expires.toSeconds() / 15, + maxAttempts: timeout.toSeconds() / 15, backoffRate: 1, errors: [ 'RetryException', @@ -732,13 +758,14 @@ export class Embedding extends Construct { }); } - private createReleaseLock(idSuffix: string, { + private createReleaseLockTask(idSuffix: string, { name, lockId, }: { name: string; lockId: string; }) { + // Release the acquired lock. return new tasks.LambdaInvoke(this, `ReleaseLock${idSuffix}`, { lambdaFunction: this._lockHandler, payload: sfn.TaskInput.fromObject({ From c410f610287f133a70872395459277a4c3feb4a6 Mon Sep 17 00:00:00 2001 From: Yukinobu Mine Date: Fri, 31 Oct 2025 14:32:19 +0900 Subject: [PATCH 8/9] Fix: Re-enable setting the knowledge configuration to 'dedicated'. --- frontend/src/features/knowledgeBase/pages/BotKbEditPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/features/knowledgeBase/pages/BotKbEditPage.tsx b/frontend/src/features/knowledgeBase/pages/BotKbEditPage.tsx index 1a508692d..9bf700c78 100644 --- a/frontend/src/features/knowledgeBase/pages/BotKbEditPage.tsx +++ b/frontend/src/features/knowledgeBase/pages/BotKbEditPage.tsx @@ -1605,7 +1605,6 @@ const BotKbEditPage: React.FC = () => { 'knowledgeBaseSettings.advancedConfigration.createDedicatedKnowledgeBase.label' )} onChange={() => setKnowledgeBaseType('new')} - disabled /> Date: Wed, 5 Nov 2025 16:35:34 +0900 Subject: [PATCH 9/9] add: comments helps understand --- backend/app/vector_search.py | 10 ++-- .../bootstrap_state_machine.py | 4 ++ .../finalize_custom_bot_build.py | 9 +++- .../finalize_shared_knowledge_bases_build.py | 7 ++- .../bedrock_knowledge_base/lock.py | 1 + .../synchronize_data_source.py | 10 +++- .../index.ts | 49 ++++++++++--------- cdk/lib/constructs/embedding.ts | 22 +++++++++ 8 files changed, 79 insertions(+), 33 deletions(-) diff --git a/backend/app/vector_search.py b/backend/app/vector_search.py index 2ac7e57c6..cb1526972 100644 --- a/backend/app/vector_search.py +++ b/backend/app/vector_search.py @@ -1,24 +1,21 @@ import logging -from typing import TypedDict, Any +from typing import Any, TypedDict from urllib.parse import urlparse +from app.repositories.knowledge_base import get_knowledge_base_info from app.repositories.models.conversation import ( RelatedDocumentModel, TextToolResultModel, ) from app.repositories.models.custom_bot import BotModel -from app.repositories.knowledge_base import get_knowledge_base_info from app.utils import get_bedrock_agent_runtime_client from botocore.exceptions import ClientError +from mypy_boto3_bedrock_agent_runtime.literals import SearchTypeType from mypy_boto3_bedrock_agent_runtime.type_defs import ( KnowledgeBaseRetrievalResultTypeDef, KnowledgeBaseVectorSearchConfigurationTypeDef, RetrieveRequestTypeDef, ) -from mypy_boto3_bedrock_agent_runtime.literals import ( - SearchTypeType, -) - logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) @@ -91,6 +88,7 @@ def _bedrock_knowledge_base_search(bot: BotModel, query: str) -> list[SearchResu retrieve_parameter["retrievalConfiguration"]["vectorSearchConfiguration"]["filter"] = { # type: ignore "listContains": { "key": "tenants", + # Note: metadata is attached on cdk/lambda/knowledge-base-custom-transformation/index.ts "value": f"BOT#{bot.id}", # type: ignore }, } diff --git a/backend/embedding_statemachine/bedrock_knowledge_base/bootstrap_state_machine.py b/backend/embedding_statemachine/bedrock_knowledge_base/bootstrap_state_machine.py index 93196a917..eacc85df5 100644 --- a/backend/embedding_statemachine/bedrock_knowledge_base/bootstrap_state_machine.py +++ b/backend/embedding_statemachine/bedrock_knowledge_base/bootstrap_state_machine.py @@ -37,6 +37,10 @@ def handler(event, context): return { "QueuedBots": [ { + # Bot configuration for State Machine processing + # - Shared bots: KnowledgeBaseHash set, KnowledgeBase empty + # - Dedicated bots: KnowledgeBase set, KnowledgeBaseHash None + # - FilesDiff: Present when bot has specific file changes "OwnerUserId": queued_bot["bot"].owner_user_id, "BotId": queued_bot["bot"].id, **( diff --git a/backend/embedding_statemachine/bedrock_knowledge_base/finalize_custom_bot_build.py b/backend/embedding_statemachine/bedrock_knowledge_base/finalize_custom_bot_build.py index 188c1b8d3..cafefaaa3 100644 --- a/backend/embedding_statemachine/bedrock_knowledge_base/finalize_custom_bot_build.py +++ b/backend/embedding_statemachine/bedrock_knowledge_base/finalize_custom_bot_build.py @@ -30,7 +30,14 @@ class DataSource(TypedDict): def handler(event, context): - """Obtain the ID of the dedicated Knowledge Bases and Guardrails built by `BrChatKbStackXXX`, and update `knowledge_base_id` and `guardrail_arn` of the bot.""" + """Finalize custom bot build by retrieving CloudFormation outputs and setting up data sources. + + This handler processes both: + - Dedicated bots: Retrieves new KB/Guardrails from BrChatKbStack{botId} CloudFormation outputs + - Shared bots with file diffs: Inherits shared KB DataSources from previous flow + + All bots proceed to ingestion processing with their respective DataSources. + """ user_id = event["OwnerUserId"] bot_id = event["BotId"] diff --git a/backend/embedding_statemachine/bedrock_knowledge_base/finalize_shared_knowledge_bases_build.py b/backend/embedding_statemachine/bedrock_knowledge_base/finalize_shared_knowledge_bases_build.py index db908de47..ff3743bb2 100644 --- a/backend/embedding_statemachine/bedrock_knowledge_base/finalize_shared_knowledge_bases_build.py +++ b/backend/embedding_statemachine/bedrock_knowledge_base/finalize_shared_knowledge_bases_build.py @@ -84,7 +84,12 @@ def handler(event, context): if knowledge_base_hash and knowledge_base_hash in knowledge_bases: knowledge_base = knowledge_bases[knowledge_base_hash] if "FilesDiff" in queued_bot: - # If the bot specifies which files should be ingested, add the data sources as the ingestion target for that bot. + # Assign shared KB's DataSources to individual bot for processing in MapQueuedBots flow. + # This preserves bot-specific file diff information needed for: + # - Constructing bot-specific S3 paths (user_id/bot_id/filename) + # - Individual bot status tracking + # - Proper ingestion attribution + # The bot will update the shared Knowledge Base's DataSources in MapQueuedBots flow. queued_bot["DataSources"] = [ { "KnowledgeBaseId": knowledge_base["knowledge_base_id"], diff --git a/backend/embedding_statemachine/bedrock_knowledge_base/lock.py b/backend/embedding_statemachine/bedrock_knowledge_base/lock.py index 13cc41b16..621f6fc3e 100644 --- a/backend/embedding_statemachine/bedrock_knowledge_base/lock.py +++ b/backend/embedding_statemachine/bedrock_knowledge_base/lock.py @@ -86,6 +86,7 @@ def handle_release(event): lock_id = event["LockId"] try: # Delete the lock file only if it have the same `ETag` as when it was created. + # Note: lock is automatically released after 1 day passed (see also: cdk/lib/bedrock-region-resources.ts) s3.delete_object( Bucket=DOCUMENT_BUCKET, Key=lock_file_key, diff --git a/backend/embedding_statemachine/bedrock_knowledge_base/synchronize_data_source.py b/backend/embedding_statemachine/bedrock_knowledge_base/synchronize_data_source.py index 48942957f..58d4bbe63 100644 --- a/backend/embedding_statemachine/bedrock_knowledge_base/synchronize_data_source.py +++ b/backend/embedding_statemachine/bedrock_knowledge_base/synchronize_data_source.py @@ -42,7 +42,15 @@ def get_data_source_type(knowledge_base_id: str, data_source_id: str): def handle_ingest(event): - """Perform direct ingestion or entire synchronization into the data source""" + """Perform data source synchronization for Knowledge Bases. + + This function handles both shared and dedicated Knowledge Base DataSources. + The target DataSource is determined by KnowledgeBaseId/DataSourceId in the event, + regardless of whether called from SharedKnowledgeBases flow or MapQueuedBots flow. + + Shared KB bots with file diffs reach here via MapQueuedBots flow but still + update the shared Knowledge Base's DataSources with bot-specific file changes. + """ knowledge_base_id = event["KnowledgeBaseId"] data_source_id = event["DataSourceId"] diff --git a/cdk/lambda/knowledge-base-custom-transformation/index.ts b/cdk/lambda/knowledge-base-custom-transformation/index.ts index 0c7ae903e..2f87d53c3 100644 --- a/cdk/lambda/knowledge-base-custom-transformation/index.ts +++ b/cdk/lambda/knowledge-base-custom-transformation/index.ts @@ -1,4 +1,4 @@ -import { type Handler } from 'aws-lambda'; +import { type Handler } from "aws-lambda"; /** * Custom transformation function used in the S3 data source of a shared Knowledge Base. @@ -10,30 +10,31 @@ export const handler: Handler = async (event, context) => { const inputFiles = event.inputFiles as any[]; return { - outputFiles: inputFiles.flatMap(file => { - const originalFileLocation = file.originalFileLocation; - const s3Uri = new URL(originalFileLocation.s3_location.uri); + outputFiles: inputFiles.flatMap((file) => { + const originalFileLocation = file.originalFileLocation; + const s3Uri = new URL(originalFileLocation.s3_location.uri); - // Ignore files that do not exist in the directory for bots. - const groups = s3Uri.pathname.match(/^\/(?[^/]+)\/(?[^/]+)\/documents\/(?[^/]+)$/)?.groups; - const userId = groups?.userId; - const botId = groups?.botId; - if (groups == null || botId == null || userId === '.temp') { - return []; - } + // Ignore files that do not exist in the directory for bots. + const groups = s3Uri.pathname.match( + /^\/(?[^/]+)\/(?[^/]+)\/documents\/(?[^/]+)$/ + )?.groups; + const userId = groups?.userId; + const botId = groups?.botId; + // Note: `.temp` is used for distributed lock + if (groups == null || botId == null || userId === ".temp") { + return []; + } - // For files uploaded by bots, set the bot's ID in the metadata field named 'tenants'. - return [ - { - originalFileLocation, - contentBatches: file.contentBatches, - fileMetadata: { - tenants: [ - `BOT#${botId}`, - ], - }, + // For files uploaded by bots, set the bot's ID in the metadata field named 'tenants'. + return [ + { + originalFileLocation, + contentBatches: file.contentBatches, + fileMetadata: { + tenants: [`BOT#${botId}`], }, - ]; - }), - }; + }, + ]; + }), + }; }; diff --git a/cdk/lib/constructs/embedding.ts b/cdk/lib/constructs/embedding.ts index c8765e18f..4a53455ab 100644 --- a/cdk/lib/constructs/embedding.ts +++ b/cdk/lib/constructs/embedding.ts @@ -537,6 +537,28 @@ export class Embedding extends Construct { resultPath: "$.Error", }); + /** + * Knowledge Base Synchronization State Machine + * + * This state machine processes both Shared and Dedicated Knowledge Bases through two main flows: + * + * 1. SharedKnowledgeBases Flow: + * - Executes when SharedKnowledgeBases != null + * - Handles full synchronization of shared Knowledge Bases + * - Processes bots without file diffs via global data source sync + * + * 2. MapQueuedBots Flow: + * - Always executes (after SharedKnowledgeBases flow or standalone) + * - Processes each queued bot individually for: + * a) Dedicated bots: Creates KB + processes file diffs + * b) Shared bots with file diffs: Processes bot-specific file changes to shared KB + * c) Guardrails management for all bots + * + * Why shared bots with file diffs use MapQueuedBots flow: + * - File diffs contain bot-specific metadata (OwnerUserId, BotId) + * - Requires bot-specific S3 path construction (user_id/bot_id/filename) + * - Enables individual bot status tracking during ingestion + */ const definition = ( bootstrapStateMachine .next(