diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/.gitignore b/alb-ecs-bedrock-agents-cdk-dotnet/.gitignore new file mode 100644 index 000000000..a4609e758 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/.gitignore @@ -0,0 +1,342 @@ +# CDK asset staging directory +.cdk.staging +cdk.out + +# Created by https://www.gitignore.io/api/csharp + +### Csharp ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + + +# End of https://www.gitignore.io/api/csharp \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/README.md b/alb-ecs-bedrock-agents-cdk-dotnet/README.md new file mode 100644 index 000000000..6a99f4b2a --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/README.md @@ -0,0 +1,299 @@ +# Travel Service with AI Agent, OpenSearch, and Lambda using AWS CDK .NET + +This pattern demonstrates how to create a serverless travel service using Amazon Bedrock Agent, Amazon OpenSearch for vector search, AWS Lambda for processing, and Amazon S3 for data storage. The pattern includes a flight search feature and is implemented using AWS CDK with .NET. + +Learn more about this pattern at Serverless Land Patterns: [Add your pattern link here] + +Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. + +## Architecture +![Architecture](./alb-ecs-bedrock-agents-cdk-dotnet-arch.png) + +## Requirements + +* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed +* [Node and NPM](https://nodejs.org/en/download/) installed +* [AWS Cloud Development Kit](https://docs.aws.amazon.com/cdk/latest/guide/cli.html) (AWS CDK) installed +* [.NET](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) (.NET 8.0 or later) installed +* [Grant Amazon Bedrock Model Access](https://console.aws.amazon.com/bedrock/home?#/modelaccess) to following models: + * Titan Text Embeddings V2 (`amazon.titan-embed-text-v2:0`) + * Claude 3 Haiku (`anthropic.claude-3-haiku-20240307-v1:0`) + * Claude 3.5 Haiku (`anthropic.claude-3-5-haiku-20241022-v1:0`) + +## Deployment Instructions + +1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: + ``` + git clone https://github.com/aws-samples/serverless-patterns + ``` +2. Change the working directory to this pattern's directory: + ``` + cd alb-ecs-bedrock-agents-cdk-dotnet + ``` +3. Build the .NET CDK project: + ``` + dotnet build src + ``` +4. Deploy the stack to your default AWS account and region. + + **Please be aware that the deploy command will take about 5-15 minutes to complete.** The output of this command should give you the ALB endpoint URL: + ``` + cdk deploy + ``` +5. Other useful commands: + ``` + cdk diff compare deployed stack with current state + cdk synth emits the synthesized CloudFormation template + ``` + +## How it works + +This pattern creates a serverless travel service: + +1. An agent powered by Amazon Bedrock interacts with customers and handles their travel-related queries. +2. Knowledge base documents are stored in Amazon S3 and indexed in Amazon OpenSearch for quick retrieval. +3. When a flight search is requested, an AWS Lambda function is triggered from Bedrock Agent Action-Group. The Lambda function processes the request and returns mock flight data. +4. When policy related questions are asked, Bedrock knowledge based is used to answer the questions. +5. All components are secured with appropriate IAM roles and permissions. + +The AWS CDK is used to define and deploy all the necessary AWS resources, including the Bedrock Agent, Bedrock KnowledgeBase, OpenSearch Serverless Collection, Lambda functions, S3 bucket and associated IAM roles and permissions. + +## Testing + +### 1. Testing the AI Agent through Bedrock Console +To access and test the Bedrock Agent: + +1. Open the AWS Console and navigate to Amazon Bedrock + - Go to AWS Console → Bedrock → Agents (in the left sidebar) + - Find your agent named `chatbot-bedrock-agent-XXXXXXXXX` (find the actual name from CDK output) + +2. Accessing the Agent Playground: + - Click on your agent name + - In the agent details page, click on "Test" in the top right corner + - This will open the Agent Playground where you can interact with your agent + - Change the version from `TestAlias: Working draft` to `BedrockAgentCdkAlias: Version 1` + - ![console_test](./Test_Bedrock_Agent_From_Console_1.png) + +3. Example Interactions: + +- **Example 1 - Flight Search:** +``` +User: "I want to find flights from Seattle to New York for 3 day trip for my wife and I in the week on December 31, 2024." (use future dates) +``` +Expected Agent Response: +``` +"I found several flight options for your trip from Seattle to New York: +1. Delta Air Lines: + - Outbound Flight: Departs 3:00 AM on Dec 31, arrives 5:00 PM + - Return Flight: Departs 3:00 AM on Jan 3, arrives 2:00 PM + - Total Price: $848.67 2. +2. United Airlines (Option 1): + - Outbound Flight: Departs 7:00 AM on Dec 31, arrives 11:00 AM + - Return Flight: Departs 4:00 PM on Jan 3, arrives 6:00 AM + - Total Price: $1,285.44 3. +3. United Airlines (Option 2): + - Outbound Flight: Departs 9:00 AM on Dec 31, arrives 11:00 PM + - Return Flight: Departs 7:00 PM on Jan 3, arrives 8:00 PM + - Total Price: $1,248.37 4. +4. Lufthansa: + - Outbound Flight: Departs 6:00 AM on Dec 31, arrives 5:00 AM on Jan 1 + - Return Flight: Departs 10:00 PM on Jan 3, arrives 6:00 AM on Jan 4 + - Total Price: $1,265.50 +5. British Airways: + - Outbound Flight: Departs 1:00 AM on Dec 31, arrives 10:00 PM + - Return Flight: Departs 7:00 AM on Jan 3, arrives 6:00 AM on Jan 4 + - Total Price: $1,399.66 + +The most budget-friendly option is the Delta Air Lines flight at $848.67. Would you like me to help you book one of these flights?" +``` + +- **Example 2 - Policy Question:** +``` +User: "What is your cancellation policy for international flights?" +``` +Expected Agent Response: +``` +"Here are the details of GlobalTrek Adventures' cancellation policy for international flights: +1. Cancellation Timeframes: - 30+ days before travel: Full refund minus $50 administrative fee - 15-29 days before travel: 50% refund - 14 days or less before travel: No refund +2. Company Cancellations: If GlobalTrek Adventures cancels a trip, they offer: - Full refund, OR - Option to rebook for a future date with a 10% discount +3. Refund Processing: - Refunds are processed within 10 business days - Refunded to the original payment method Please note that the company is not responsible for any additional incidental expenses you may have already incurred, such as non-refundable flights. " +``` + +### 2. Testing the REST API through ALB +You can test the flight search functionality directly through the Application Load Balancer endpoint using curl: + +- Install `jq` for formatted output. (you can omit it from command-line) + - On Ubuntu/Debian + ``` + sudo apt install jq + ``` + - On CentOS/RHEL + ``` + sudo yum install jq + ``` + - On macOS + ``` + brew install jq + ``` + +- Test the flight search feature: + - Use the `curl` to send a request to the ALB endpoint (replace `` with the actual URL from the CDK output). + - Update `` to a meaningful value. + - Keep the same `` for same chat session. + + ```bash + curl -X POST \ + http:///message \ + -H "Accept: application/json" \ + -H "Content-Type: application/json" \ + -d '{ + "SessionId": "", + "Message": "I am looking for a flight for 2 people from SEA to JFK on 31st of December 2024 and coming back on 7th of January.", + "EndSession": false, + "EnableTrace": false, + "SessionAttributes": {}, + "PromptSessionAttributes": {} + }' \ + -v | jq . + ``` + Expected agent response: + ```json + { + "sessionId": "", + "memoryId": null, + "message": "I found several flight options for your round trip from Seattle (SEA) to New York (JFK):\n\n1. American Airlines (Lowest Price Option):\n - Outbound: Flight AA1958 on Dec 31, 2024, at 5:00 PM\n - Return: Flight AA6048 on Jan 7, 2025, at 12:00 AM\n - Total Price: $591.59\n\n2. British Airways Options:\n a) Flight BA4018:\n - Outbound: Dec 31, 2024, at 7:00 PM\n - Return: Flight BA1431 on Jan 7, 2025, at 4:00 PM\n - Total Price: $1,191.94\n\n b) Flight BA7836:\n - Outbound: Dec 31, 2024, at 4:00 AM\n - Return: Flight BA7778 on Jan 7, 2025, at 10:00 AM\n - Total Price: $1,315.32\n\n3. Other Airlines:\n - Lufthansa: Total price around $1,476.94\n - Another British Airways option: Total price around $1,394.10\n\nThe American Airlines option offers the most budget-friendly choice. Would you like me to help you book one of these flights or provide more details?", + "files": null, + "returnControlPayload": null, + "trace": null, + "error": null, + "hasError": false + } + ``` + +- You can also test the agent's ability to answer questions about travel policies by asking it various policy-related questions. + - Use the `curl` to send a request to the ALB endpoint (replace `` with the actual URL from the CDK output). + - Update `` to a meaningful value. + - Keep the same `` for same chat session. + + ```bash + curl -X POST \ + http:///message \ + -H "Accept: application/json" \ + -H "Content-Type: application/json" \ + -d '{ + "SessionId": "", + "Message": "I am looking for cancellation policy for international travel.", + "EndSession": false, + "EnableTrace": false, + "SessionAttributes": {}, + "PromptSessionAttributes": {} + }' \ + -v | jq . + ``` + + Expected agent response: + ```json + { + "sessionId": "", + "memoryId": null, + "message": "Here are the details of GlobalTrek Adventures' international travel cancellation policy:\n\nCancellation by Customer:\n- 30+ days before travel: Full refund minus $50 administrative fee\n- 15-29 days before travel: 50% refund\n- 14 days or less before travel: No refund\n\nIf GlobalTrek Adventures Cancels:\n- Full refund or option to rebook with a 10% discount\n- Note: Not responsible for additional expenses like visas or non-refundable flights\n\nAdditional Recommendation:\n- The company strongly suggests purchasing travel insurance through SafeJourney Insurance for extra protection.\n\n\n\nWould you like more information about the cancellation policy or travel insurance options?", + "files": null, + "returnControlPayload": null, + "trace": null, + "error": null, + "hasError": false + } + ``` + +### 3. Test using test application. + +- You can also use test application to chat with Bedrock Agent. +- Go to `Test` directory from command prompt. +``` +cd ./src/Test +``` +- Update `` in `appsettings.json` file. +- Update `enableTrace` to `true` if need to check request traces from Bedrock Agents. Traces will be written to a file in `Test` directory with filename format: `trace__.json`. +- Example: `appsettings.json` configuration + ![test_app_settings](./Test_App_Settings.png) +- Run Test application. + ``` + dotnet run + ``` +- Press `CTRL+C` or type `exit` and press enter to exit the application. +- Example input/output using test application + ![test_app_input_output](./Test_App_Input_Output.png) + +### 4. Adding and Managing Documents + +To add new documents (such as updated policies, travel guides, or FAQs) to the system: + +1. Upload Documents to S3 +- You can upload new documents to S3 bucket from console, or +- Using CLI: (replace `` with the actual bucket name from the CDK output. e.g. `chatbot-bedrock-knowledge-base-XXXXXXXXX`) + ```bash + # Upload a single document + aws s3 cp new-policy.md s3:/// + + # Upload multiple documents + aws s3 cp ./travel-docs/ s3:/// --recursive + ``` + +2. Document Processing: +- Once documents are uploaded to S3, you will have to sync the Bedrock Knowledge base with newly uploaded documents. +- Open the AWS Console and navigate to Amazon Bedrock +- Go to AWS Console → Bedrock → Knowledge bases (in the left sidebar) +- Find your knowledge base named `bedrock-knowledge-base-XXXXXXXXXXX` (from CDK output) +- Click on the name to open it. +- Go to "Data source" section and select the data source named `chatbot-bedrock-knowledge-base-datasource-XXXXXXXXXXX` +- Click `Sync` in top-right corner. This will initiate a new sync, find newly uploaded documents and will index the documents in "Amazon OpenSearch Serveless Collection" + ![console_sync](./Sync_Knowledge_Base_From_Console.png) + +3. Test the Agent with New Content: +``` +User: "What is the policy regarding [topic from your new document]?" +``` +The agent should now be able to reference information from your newly added documents. + +Tips for Document Preparation: +- Format documents in clear, well-structured markdown or text +- Break large documents into logical sections +- Include clear headers and subheaders +- Ensure content is accurate and up-to-date +- Use consistent terminology throughout documents + +Example Document Structure: +```markdown +# Travel Insurance Policy + +## Coverage Types +[Coverage details...] + +## Claim Process +[Process details...] + +## Contact Information +[Contact details...] +``` +Best Practices: +- Test new documents with various queries to ensure proper indexing +- Keep document formats consistent for better processing +- Regularly review and update documents to maintain accuracy +- Check any sync warnings + +### 5. CloudWatch Logs +Check the CloudWatch Logs for the Lambda function and ECS Task to see the processing details and any potential errors. + +## Cleanup + +1. Run the given command to delete the resources that were created. It might take some time for the CloudFormation stack to get deleted. +``` +cdk destroy +``` + +---- +Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/Sync_Knowledge_Base_From_Console.png b/alb-ecs-bedrock-agents-cdk-dotnet/Sync_Knowledge_Base_From_Console.png new file mode 100644 index 000000000..59322ddbe Binary files /dev/null and b/alb-ecs-bedrock-agents-cdk-dotnet/Sync_Knowledge_Base_From_Console.png differ diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/TejasVora.jpg b/alb-ecs-bedrock-agents-cdk-dotnet/TejasVora.jpg new file mode 100644 index 000000000..c8d01afb8 Binary files /dev/null and b/alb-ecs-bedrock-agents-cdk-dotnet/TejasVora.jpg differ diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/Test_App_Input_Output.png b/alb-ecs-bedrock-agents-cdk-dotnet/Test_App_Input_Output.png new file mode 100644 index 000000000..01f72216c Binary files /dev/null and b/alb-ecs-bedrock-agents-cdk-dotnet/Test_App_Input_Output.png differ diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/Test_App_Settings.png b/alb-ecs-bedrock-agents-cdk-dotnet/Test_App_Settings.png new file mode 100644 index 000000000..61aebbbcd Binary files /dev/null and b/alb-ecs-bedrock-agents-cdk-dotnet/Test_App_Settings.png differ diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/Test_Bedrock_Agent_From_Console_1.png b/alb-ecs-bedrock-agents-cdk-dotnet/Test_Bedrock_Agent_From_Console_1.png new file mode 100644 index 000000000..75a894907 Binary files /dev/null and b/alb-ecs-bedrock-agents-cdk-dotnet/Test_Bedrock_Agent_From_Console_1.png differ diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/alb-ecs-bedrock-agents-cdk-dotnet-arch.png b/alb-ecs-bedrock-agents-cdk-dotnet/alb-ecs-bedrock-agents-cdk-dotnet-arch.png new file mode 100644 index 000000000..995f510d9 Binary files /dev/null and b/alb-ecs-bedrock-agents-cdk-dotnet/alb-ecs-bedrock-agents-cdk-dotnet-arch.png differ diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/alb-ecs-bedrock-agents-cdk-dotnet.json b/alb-ecs-bedrock-agents-cdk-dotnet/alb-ecs-bedrock-agents-cdk-dotnet.json new file mode 100644 index 000000000..b2ea571a0 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/alb-ecs-bedrock-agents-cdk-dotnet.json @@ -0,0 +1,106 @@ +{ + "title": "AI-Powered ChatBot with Bedrock Agent, OpenSearch, and ECS", + "description": "Create a serverless AI ChatBot using Bedrock agent, OpenSearch, Lambda, and ECS with ALB in AWS.", + "language": ".NET", + "level": "200", + "framework": "CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern demonstrates how to create an AI-powered ChatBot using Amazon Bedrock for the AI agent, Amazon OpenSearch Serverless for vector search, AWS Lambda for processing, Amazon ECS with Fargate for hosting, and Application Load Balancer for routing. The pattern includes a Bedrock agent with Knowledge Base and Action Groups, and is implemented using AWS CDK with .NET." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/alb-ecs-bedrock-agents-cdk-dotnet", + "templateURL": "serverless-patterns/alb-ecs-bedrock-agents-cdk-dotnet", + "projectFolder": "alb-ecs-bedrock-agents-cdk-dotnet", + "templateFile": "/src/AlbEcsBedrockAgentsCdkDotnet/AlbEcsBedrockAgentsCdkDotnetStack.cs" + } + }, + "resources": { + "bullets": [ + { + "text": "Amazon Bedrock", + "link": "https://aws.amazon.com/bedrock/" + }, + { + "text": "Amazon Bedrock Agents", + "link": "https://aws.amazon.com/bedrock/agents/" + }, + { + "text": "Amazon OpenSearch Serverless", + "link": "https://docs.aws.amazon.com/opensearch-service/latest/developerguide/serverless.html" + }, + { + "text": "AWS Lambda", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/welcome.html" + }, + { + "text": "Amazon ECS with Fargate", + "link": "https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html" + }, + { + "text": "Application Load Balancer", + "link": "https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html" + }, + { + "text": "AWS CDK", + "link": "https://docs.aws.amazon.com/cdk/latest/guide/home.html" + } + ] + }, + "deploy": { + "text": [ + "cdk deploy" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions, including how to interact with the Bedrock agent, search for flights using the ALB endpoint, and verify processing in OpenSearch and Lambda functions." + ] + }, + "cleanup": { + "text": [ + "Delete the stack: cdk destroy." + ] + }, + "authors": [ + { + "name": "Tejas Vora", + "image": "./TejasVora.jpg", + "bio": "Tejas Vora is a Senior Solutions Architect with Amazon Web Services.", + "linkedin": "tejas-vora-b4758a47" + } + ], + "patternArch": { + "icon1": { + "x": 20, + "y": 50, + "service": "alb", + "label": "Application Load Balancer" + }, + "icon2": { + "x": 50, + "y": 50, + "service": "fargate", + "label": "AWS Fargate" + }, + "icon3": { + "x": 80, + "y": 50, + "service": "bedrock", + "label": "Amazon Bedrock" + }, + "line1": { + "from": "icon1", + "to": "icon2", + "label": "" + }, + "line2": { + "from": "icon2", + "to": "icon3", + "label": "" + } + } +} diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/cdk.json b/alb-ecs-bedrock-agents-cdk-dotnet/cdk.json new file mode 100644 index 000000000..2488c1b60 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/cdk.json @@ -0,0 +1,70 @@ +{ + "app": "dotnet run --project src/AlbEcsBedrockAgentsCdkDotnet/AlbEcsBedrockAgentsCdkDotnet.csproj", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "src/*/obj", + "src/*/bin", + "src/*.sln", + "src/*/GlobalSuppressions.cs", + "src/*/*.csproj" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false + } +} diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/example-pattern.json b/alb-ecs-bedrock-agents-cdk-dotnet/example-pattern.json new file mode 100644 index 000000000..26fb98367 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/example-pattern.json @@ -0,0 +1,76 @@ +{ + "title": "AI-Powered ChatBot with Bedrock Agent, OpenSearch, and ECS", + "description": "Create a serverless AI-powered ChatBot using Amazon Bedrock agent, Amazon OpenSearch Serverless, AWS Lambda, and Amazon ECS with Application Load Balancer", + "language": ".NET", + "level": "200", + "framework": "CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern demonstrates how to create an AI-powered ChatBot using Amazon Bedrock for the AI agent, Amazon OpenSearch Serverless for vector search, AWS Lambda for processing, Amazon ECS with Fargate for hosting, and Application Load Balancer for routing. The pattern includes a Bedrock agent with Knowledge Base and Action Groups, and is implemented using AWS CDK with .NET." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/alb-ecs-bedrock-agents-cdk-dotnet", + "templateURL": "serverless-patterns/alb-ecs-bedrock-agents-cdk-dotnet", + "projectFolder": "alb-ecs-bedrock-agents-cdk-dotnet", + "templateFile": "/src/AlbEcsBedrockAgentsCdkDotnet/AlbEcsBedrockAgentsCdkDotnetStack.cs" + } + }, + "resources": { + "bullets": [ + { + "text": "Amazon Bedrock", + "link": "https://aws.amazon.com/bedrock/" + }, + { + "text": "Amazon Bedrock Agents", + "link": "https://aws.amazon.com/bedrock/agents/" + }, + { + "text": "Amazon OpenSearch Serverless", + "link": "https://docs.aws.amazon.com/opensearch-service/latest/developerguide/serverless.html" + }, + { + "text": "AWS Lambda", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/welcome.html" + }, + { + "text": "Amazon ECS with Fargate", + "link": "https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html" + }, + { + "text": "Application Load Balancer", + "link": "https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html" + }, + { + "text": "AWS CDK", + "link": "https://docs.aws.amazon.com/cdk/latest/guide/home.html" + } + ] + }, + "deploy": { + "text": [ + "cdk deploy" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions, including how to interact with the Bedrock agent, search for flights using the ALB endpoint, and verify processing in OpenSearch and Lambda functions." + ] + }, + "cleanup": { + "text": [ + "Delete the stack: cdk destroy." + ] + }, + "authors": [ + { + "name": "Tejas Vora", + "image": "./TejasVora.jpg", + "bio": "Tejas Vora is a Senior Solutions Architect with Amazon Web Services.", + "linkedin": "tejas-vora-b4758a47" + } + ] + } \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet.sln b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet.sln new file mode 100644 index 000000000..98fb76033 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet.sln @@ -0,0 +1,144 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AlbEcsBedrockAgentsCdkDotnet", "AlbEcsBedrockAgentsCdkDotnet\AlbEcsBedrockAgentsCdkDotnet.csproj", "{D3E59DEB-219D-4231-838B-204E52E44A13}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AlbEcsBedrockAgentsCdkDotnet", "AlbEcsBedrockAgentsCdkDotnet", "{E54F8D95-183A-4F3F-AFB2-F0CB1C4AD818}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LambdaFunctions", "LambdaFunctions", "{6C88E75E-3388-4544-9E6E-98B0DE83EBB8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BedrockAgent", "BedrockAgent", "{15E38A5C-4460-42A2-A9F5-7686FCB5A238}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ActionGroupLambdaFunction", "LambdaFunctions\BedrockAgent\ActionGroupLambdaFunction\ActionGroupLambdaFunction.csproj", "{54226D32-A012-4BCF-B4A1-93ED3624BD4C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "KnowledgeBase", "KnowledgeBase", "{744443E6-0104-4DD3-823F-78C3417EF459}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CustomResource", "CustomResource", "{485A1529-FD6E-47DA-BB16-594266095EB6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KnowledgeBaseIngestion", "LambdaFunctions\KnowledgeBase\CustomResource\KnowledgeBaseIngestion\KnowledgeBaseIngestion.csproj", "{59BC450C-96E6-4536-B7B1-33583E43897D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OssIndexCreation", "LambdaFunctions\KnowledgeBase\CustomResource\OssIndexCreation\OssIndexCreation.csproj", "{6D469CD0-FEB2-41A2-B0E1-D0AB3A4AEEE0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CustomResource", "CustomResource", "{D39904F1-F0CA-49DC-8654-6494CA40A4C7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BedrockAgentAliasCreation", "LambdaFunctions\BedrockAgent\CustomResource\BedrockAgentAliasCreation\BedrockAgentAliasCreation.csproj", "{DD7A6669-B9FD-4ACB-BAB2-220E12E44DE4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ECSTasks", "ECSTasks", "{E5031C0B-31A2-4BBE-8EC9-4ECB36E5FC32}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BedrockAgentsApiProxy", "ECSTasks\BedrockAgentsApiProxy\BedrockAgentsApiProxy.csproj", "{ECA88C56-92AA-40C0-ABEF-867E2DCE7CF6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApp", "Test\TestApp.csproj", "{5B8875EB-9B2F-413C-8202-F582F4C3CB18}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D3E59DEB-219D-4231-838B-204E52E44A13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D3E59DEB-219D-4231-838B-204E52E44A13}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D3E59DEB-219D-4231-838B-204E52E44A13}.Debug|x64.ActiveCfg = Debug|Any CPU + {D3E59DEB-219D-4231-838B-204E52E44A13}.Debug|x64.Build.0 = Debug|Any CPU + {D3E59DEB-219D-4231-838B-204E52E44A13}.Debug|x86.ActiveCfg = Debug|Any CPU + {D3E59DEB-219D-4231-838B-204E52E44A13}.Debug|x86.Build.0 = Debug|Any CPU + {D3E59DEB-219D-4231-838B-204E52E44A13}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D3E59DEB-219D-4231-838B-204E52E44A13}.Release|Any CPU.Build.0 = Release|Any CPU + {D3E59DEB-219D-4231-838B-204E52E44A13}.Release|x64.ActiveCfg = Release|Any CPU + {D3E59DEB-219D-4231-838B-204E52E44A13}.Release|x64.Build.0 = Release|Any CPU + {D3E59DEB-219D-4231-838B-204E52E44A13}.Release|x86.ActiveCfg = Release|Any CPU + {D3E59DEB-219D-4231-838B-204E52E44A13}.Release|x86.Build.0 = Release|Any CPU + {54226D32-A012-4BCF-B4A1-93ED3624BD4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {54226D32-A012-4BCF-B4A1-93ED3624BD4C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {54226D32-A012-4BCF-B4A1-93ED3624BD4C}.Debug|x64.ActiveCfg = Debug|Any CPU + {54226D32-A012-4BCF-B4A1-93ED3624BD4C}.Debug|x64.Build.0 = Debug|Any CPU + {54226D32-A012-4BCF-B4A1-93ED3624BD4C}.Debug|x86.ActiveCfg = Debug|Any CPU + {54226D32-A012-4BCF-B4A1-93ED3624BD4C}.Debug|x86.Build.0 = Debug|Any CPU + {54226D32-A012-4BCF-B4A1-93ED3624BD4C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {54226D32-A012-4BCF-B4A1-93ED3624BD4C}.Release|Any CPU.Build.0 = Release|Any CPU + {54226D32-A012-4BCF-B4A1-93ED3624BD4C}.Release|x64.ActiveCfg = Release|Any CPU + {54226D32-A012-4BCF-B4A1-93ED3624BD4C}.Release|x64.Build.0 = Release|Any CPU + {54226D32-A012-4BCF-B4A1-93ED3624BD4C}.Release|x86.ActiveCfg = Release|Any CPU + {54226D32-A012-4BCF-B4A1-93ED3624BD4C}.Release|x86.Build.0 = Release|Any CPU + {59BC450C-96E6-4536-B7B1-33583E43897D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {59BC450C-96E6-4536-B7B1-33583E43897D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {59BC450C-96E6-4536-B7B1-33583E43897D}.Debug|x64.ActiveCfg = Debug|Any CPU + {59BC450C-96E6-4536-B7B1-33583E43897D}.Debug|x64.Build.0 = Debug|Any CPU + {59BC450C-96E6-4536-B7B1-33583E43897D}.Debug|x86.ActiveCfg = Debug|Any CPU + {59BC450C-96E6-4536-B7B1-33583E43897D}.Debug|x86.Build.0 = Debug|Any CPU + {59BC450C-96E6-4536-B7B1-33583E43897D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {59BC450C-96E6-4536-B7B1-33583E43897D}.Release|Any CPU.Build.0 = Release|Any CPU + {59BC450C-96E6-4536-B7B1-33583E43897D}.Release|x64.ActiveCfg = Release|Any CPU + {59BC450C-96E6-4536-B7B1-33583E43897D}.Release|x64.Build.0 = Release|Any CPU + {59BC450C-96E6-4536-B7B1-33583E43897D}.Release|x86.ActiveCfg = Release|Any CPU + {59BC450C-96E6-4536-B7B1-33583E43897D}.Release|x86.Build.0 = Release|Any CPU + {6D469CD0-FEB2-41A2-B0E1-D0AB3A4AEEE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6D469CD0-FEB2-41A2-B0E1-D0AB3A4AEEE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6D469CD0-FEB2-41A2-B0E1-D0AB3A4AEEE0}.Debug|x64.ActiveCfg = Debug|Any CPU + {6D469CD0-FEB2-41A2-B0E1-D0AB3A4AEEE0}.Debug|x64.Build.0 = Debug|Any CPU + {6D469CD0-FEB2-41A2-B0E1-D0AB3A4AEEE0}.Debug|x86.ActiveCfg = Debug|Any CPU + {6D469CD0-FEB2-41A2-B0E1-D0AB3A4AEEE0}.Debug|x86.Build.0 = Debug|Any CPU + {6D469CD0-FEB2-41A2-B0E1-D0AB3A4AEEE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6D469CD0-FEB2-41A2-B0E1-D0AB3A4AEEE0}.Release|Any CPU.Build.0 = Release|Any CPU + {6D469CD0-FEB2-41A2-B0E1-D0AB3A4AEEE0}.Release|x64.ActiveCfg = Release|Any CPU + {6D469CD0-FEB2-41A2-B0E1-D0AB3A4AEEE0}.Release|x64.Build.0 = Release|Any CPU + {6D469CD0-FEB2-41A2-B0E1-D0AB3A4AEEE0}.Release|x86.ActiveCfg = Release|Any CPU + {6D469CD0-FEB2-41A2-B0E1-D0AB3A4AEEE0}.Release|x86.Build.0 = Release|Any CPU + {DD7A6669-B9FD-4ACB-BAB2-220E12E44DE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DD7A6669-B9FD-4ACB-BAB2-220E12E44DE4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DD7A6669-B9FD-4ACB-BAB2-220E12E44DE4}.Debug|x64.ActiveCfg = Debug|Any CPU + {DD7A6669-B9FD-4ACB-BAB2-220E12E44DE4}.Debug|x64.Build.0 = Debug|Any CPU + {DD7A6669-B9FD-4ACB-BAB2-220E12E44DE4}.Debug|x86.ActiveCfg = Debug|Any CPU + {DD7A6669-B9FD-4ACB-BAB2-220E12E44DE4}.Debug|x86.Build.0 = Debug|Any CPU + {DD7A6669-B9FD-4ACB-BAB2-220E12E44DE4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DD7A6669-B9FD-4ACB-BAB2-220E12E44DE4}.Release|Any CPU.Build.0 = Release|Any CPU + {DD7A6669-B9FD-4ACB-BAB2-220E12E44DE4}.Release|x64.ActiveCfg = Release|Any CPU + {DD7A6669-B9FD-4ACB-BAB2-220E12E44DE4}.Release|x64.Build.0 = Release|Any CPU + {DD7A6669-B9FD-4ACB-BAB2-220E12E44DE4}.Release|x86.ActiveCfg = Release|Any CPU + {DD7A6669-B9FD-4ACB-BAB2-220E12E44DE4}.Release|x86.Build.0 = Release|Any CPU + {ECA88C56-92AA-40C0-ABEF-867E2DCE7CF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ECA88C56-92AA-40C0-ABEF-867E2DCE7CF6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ECA88C56-92AA-40C0-ABEF-867E2DCE7CF6}.Debug|x64.ActiveCfg = Debug|Any CPU + {ECA88C56-92AA-40C0-ABEF-867E2DCE7CF6}.Debug|x64.Build.0 = Debug|Any CPU + {ECA88C56-92AA-40C0-ABEF-867E2DCE7CF6}.Debug|x86.ActiveCfg = Debug|Any CPU + {ECA88C56-92AA-40C0-ABEF-867E2DCE7CF6}.Debug|x86.Build.0 = Debug|Any CPU + {ECA88C56-92AA-40C0-ABEF-867E2DCE7CF6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ECA88C56-92AA-40C0-ABEF-867E2DCE7CF6}.Release|Any CPU.Build.0 = Release|Any CPU + {ECA88C56-92AA-40C0-ABEF-867E2DCE7CF6}.Release|x64.ActiveCfg = Release|Any CPU + {ECA88C56-92AA-40C0-ABEF-867E2DCE7CF6}.Release|x64.Build.0 = Release|Any CPU + {ECA88C56-92AA-40C0-ABEF-867E2DCE7CF6}.Release|x86.ActiveCfg = Release|Any CPU + {ECA88C56-92AA-40C0-ABEF-867E2DCE7CF6}.Release|x86.Build.0 = Release|Any CPU + {5B8875EB-9B2F-413C-8202-F582F4C3CB18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B8875EB-9B2F-413C-8202-F582F4C3CB18}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B8875EB-9B2F-413C-8202-F582F4C3CB18}.Debug|x64.ActiveCfg = Debug|Any CPU + {5B8875EB-9B2F-413C-8202-F582F4C3CB18}.Debug|x64.Build.0 = Debug|Any CPU + {5B8875EB-9B2F-413C-8202-F582F4C3CB18}.Debug|x86.ActiveCfg = Debug|Any CPU + {5B8875EB-9B2F-413C-8202-F582F4C3CB18}.Debug|x86.Build.0 = Debug|Any CPU + {5B8875EB-9B2F-413C-8202-F582F4C3CB18}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B8875EB-9B2F-413C-8202-F582F4C3CB18}.Release|Any CPU.Build.0 = Release|Any CPU + {5B8875EB-9B2F-413C-8202-F582F4C3CB18}.Release|x64.ActiveCfg = Release|Any CPU + {5B8875EB-9B2F-413C-8202-F582F4C3CB18}.Release|x64.Build.0 = Release|Any CPU + {5B8875EB-9B2F-413C-8202-F582F4C3CB18}.Release|x86.ActiveCfg = Release|Any CPU + {5B8875EB-9B2F-413C-8202-F582F4C3CB18}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {D3E59DEB-219D-4231-838B-204E52E44A13} = {E54F8D95-183A-4F3F-AFB2-F0CB1C4AD818} + {15E38A5C-4460-42A2-A9F5-7686FCB5A238} = {6C88E75E-3388-4544-9E6E-98B0DE83EBB8} + {54226D32-A012-4BCF-B4A1-93ED3624BD4C} = {15E38A5C-4460-42A2-A9F5-7686FCB5A238} + {744443E6-0104-4DD3-823F-78C3417EF459} = {6C88E75E-3388-4544-9E6E-98B0DE83EBB8} + {485A1529-FD6E-47DA-BB16-594266095EB6} = {744443E6-0104-4DD3-823F-78C3417EF459} + {59BC450C-96E6-4536-B7B1-33583E43897D} = {485A1529-FD6E-47DA-BB16-594266095EB6} + {6D469CD0-FEB2-41A2-B0E1-D0AB3A4AEEE0} = {485A1529-FD6E-47DA-BB16-594266095EB6} + {D39904F1-F0CA-49DC-8654-6494CA40A4C7} = {15E38A5C-4460-42A2-A9F5-7686FCB5A238} + {DD7A6669-B9FD-4ACB-BAB2-220E12E44DE4} = {D39904F1-F0CA-49DC-8654-6494CA40A4C7} + {ECA88C56-92AA-40C0-ABEF-867E2DCE7CF6} = {E5031C0B-31A2-4BBE-8EC9-4ECB36E5FC32} + EndGlobalSection +EndGlobal diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/AlbEcsBedrockAgentsCdkDotnet.csproj b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/AlbEcsBedrockAgentsCdkDotnet.csproj new file mode 100644 index 000000000..76302fc08 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/AlbEcsBedrockAgentsCdkDotnet.csproj @@ -0,0 +1,20 @@ + + + + Exe + net8.0 + + Major + + + + + + + + + + + diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/AlbEcsBedrockAgentsCdkDotnetStack.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/AlbEcsBedrockAgentsCdkDotnetStack.cs new file mode 100644 index 000000000..bb0fd517a --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/AlbEcsBedrockAgentsCdkDotnetStack.cs @@ -0,0 +1,115 @@ +using System.Collections.Generic; +using AlbEcsBedrockAgentsCdkDotnet.BedrockAgent; +using AlbEcsBedrockAgentsCdkDotnet.ECS; +using Amazon.CDK; +using Constructs; + +namespace AlbEcsBedrockAgentsCdkDotnet +{ + /// + /// Class to define the stack for the Bedrock Agent with Knowledge Base and ECS service with ALB + /// + public class AlbEcsBedrockAgentsCdkDotnetStack : Stack + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Stack Name + /// Stack properties + internal AlbEcsBedrockAgentsCdkDotnetStack(Construct scope, string id, IStackProps props = null) + : base(scope, id, props) + { + // Create Bedrock agent with Knowledge Base and Action Group + var bedrockAgentWithKnowledgeBaseStack = new BedrockAgentKnowledgeBaseStack( + this, + "BedrockAgentWithKnowledgeBaseStack", + new NestedStackProps + { + Description = "Nested stack to create a Bedrock Agent with a Knowledge Base and Vector Database", + RemovalPolicy = RemovalPolicy.DESTROY, + Timeout = Duration.Minutes(30), + Parameters = new Dictionary {} + }); + + // Create Fargate-based ECS service with Application Load Balancer + var albEcsStack = new AlbEcsStack( + this, + "AlbEcsStack", + new NestedStackProps + { + Description = "Nested stack to create an ECS Cluster with an ALB", + RemovalPolicy = RemovalPolicy.DESTROY, + Timeout = Duration.Minutes(30), + Parameters = new Dictionary + { + { "AgentId", bedrockAgentWithKnowledgeBaseStack.BedrockAgent.AttrAgentId }, + { "AgentAliasId", bedrockAgentWithKnowledgeBaseStack.AgentAliasId } + } + }); + + // Output the Agent Name + _ = new CfnOutput(this, "AgentName", new CfnOutputProps + { + Description = "Bedrock Agent Name", + Value = bedrockAgentWithKnowledgeBaseStack.BedrockAgent.AgentName + }); + + // Output the Agent ID + _ = new CfnOutput(this, "AgentId", new CfnOutputProps + { + Description = "Bedrock Agent ID", + Value = bedrockAgentWithKnowledgeBaseStack.BedrockAgent.AttrAgentId + }); + + // Outpuit Agent Alias Id + _ = new CfnOutput(this, "AgentAliasId", new CfnOutputProps + { + Description = "Bedrock Agent Alias ID", + Value = bedrockAgentWithKnowledgeBaseStack.AgentAliasId + }); + + // Output the Knowledge Base Name + _ = new CfnOutput(this, "KnowledgeBaseName", new CfnOutputProps + { + Description = "Knowledge Base Name", + Value = bedrockAgentWithKnowledgeBaseStack.KnowledgeBase.Name + }); + + // Output the Knowledge Base ID + _ = new CfnOutput(this, "KnowledgeBaseId", new CfnOutputProps + { + Description = "Knowledge Base ID", + Value = bedrockAgentWithKnowledgeBaseStack.KnowledgeBase.AttrKnowledgeBaseId + }); + + // Output OSS Collection Endpoint + _ = new CfnOutput(this, "OssCollectionEndpoint", new CfnOutputProps + { + Description = "OSS Collection Endpoint", + Value = bedrockAgentWithKnowledgeBaseStack.OssCollection.AttrDashboardEndpoint + }); + + // Output Knowledge Base S3 Bucket + _ = new CfnOutput(this, "KnowledgeBaseBucket", new CfnOutputProps + { + Description = "Knowledge Base S3 Bucket", + Value = bedrockAgentWithKnowledgeBaseStack.KnowledgeBaseBucket.BucketName + }); + + // Output Knowledge Base Data Source + _ = new CfnOutput(this, "KnowledgeBaseDataSource", new CfnOutputProps + { + Description = "Knowledge Base Data Source", + Value = bedrockAgentWithKnowledgeBaseStack.KnowledgeBaseDataSource.AttrDataSourceId + }); + + // Output ALB's Endpoint + _ = new CfnOutput(this, "AlbEndpoint", new CfnOutputProps + { + Description = "Application Load Balancer Endpoint", + Value = $"http://{albEcsStack.ApplicationLoadBalancer.LoadBalancerDnsName}" + }); + } + } +} diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/BedrockAgent/ActionGroup/BedrockAgentCdk.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/BedrockAgent/ActionGroup/BedrockAgentCdk.cs new file mode 100644 index 000000000..9ba9ca1c5 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/BedrockAgent/ActionGroup/BedrockAgentCdk.cs @@ -0,0 +1,335 @@ +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using AlbEcsBedrockAgentsCdkDotnet.Common; +using Amazon.CDK; +using Amazon.CDK.AWS.IAM; +using Amazon.CDK.AWS.Lambda; +using Amazon.CDK.AwsBedrock; + +namespace AlbEcsBedrockAgentsCdkDotnet.BedrockAgent.ActionGroup; + +/// +/// This class provides the functionality to create Bedrock Agent +/// +internal sealed class BedrockAgentCdk +{ + private readonly Stack _stack; + private readonly CfnKnowledgeBase _knowledgeBase; + + /// + /// Initializes a new instance of + /// + /// CDK + /// Bedrock Knowledge Base() + internal BedrockAgentCdk(Stack stack, CfnKnowledgeBase knowledgeBase) + { + _stack = stack; + _knowledgeBase = knowledgeBase; + } + + /// + /// Gets Bedrock Agent + /// + /// Agent + internal CfnAgent BedrockAgent { get; private set; } + + /// + /// Gets Action Group Lambda Function + /// + /// Lambda Function + internal Function ActionGroupLambdaFunction { get; private set; } + + /// + /// Gets Create Agent Alias Custom Resource + /// + /// Custom Resource + internal CustomResource CreateAgentAliasCustomResource { get; private set; } + + /// + /// Gets Agent Alias Id + /// + /// AliasId + internal string AgentAliasId { get; private set; } + + /// + /// Gets Agent Alias Arn + /// + /// Agent Alias ARN + internal string AgentAliasArn { get; private set; } + + /// + /// Create Bedrock Agent + /// + /// + internal CfnAgent CreateBedrockAgent() + { + // Create lambda function for Bedrock Agent's action group + ActionGroupLambdaFunction = CreateLambdaFunctionForBedrockAgents(_stack); + + // Create Bedrock Agent + BedrockAgent = CreateBedrockAgent(ActionGroupLambdaFunction.FunctionArn); + + // Add resource-based policy to lambda function to allow Bedrock to invoke the Lambda function + ActionGroupLambdaFunction.AddPermission( + "ChatBotBedrockInvokePermission", + new Permission + { + Principal = new ServicePrincipal("bedrock.amazonaws.com"), + Action = "lambda:InvokeFunction", + SourceArn = BedrockAgent.AttrAgentArn + }); + + // Create Agent Alias + CreateAgentAliasCustomResource = CreateAgentAlias(); + + // Return Bedrock Agent + return BedrockAgent; + } + + /// + /// Create Bedrock Agent + /// + /// Action group lambda function + /// + private CfnAgent CreateBedrockAgent(string lambdaFunctionArn) + { + // Create role for Bedrock Agent + var agentRole = CreateRoleForBedrockAgent(); + + // Read the API schema from file + var apiSchemaContent = File.ReadAllText(Path.Combine("./src/AlbEcsBedrockAgentsCdkDotnet/BedrockAgent/ActionGroup/api-schema.json")); + var promptContent = File.ReadAllText(Path.Combine("./src/AlbEcsBedrockAgentsCdkDotnet/BedrockAgent/ActionGroup/agent-prompt.txt")); + + return new CfnAgent( + _stack, + "ChatBotBedrockAgent", + new CfnAgentProps + { + AgentName = $"chatbot-bedrock-agent-{Utils.GenerateRandomStringFromStackId(_stack.StackId)}", + Description = "Bedrock Agent for GlobalTrek Adventures.", + AgentResourceRoleArn = agentRole.RoleArn, + Instruction = promptContent, + IdleSessionTtlInSeconds = 300, + FoundationModel = Constants.Bedrock_FoundationModel_Claude3_5_Haiku, + KnowledgeBases = new[] + { + new CfnAgent.AgentKnowledgeBaseProperty + { + KnowledgeBaseId = _knowledgeBase.AttrKnowledgeBaseId, + Description = "You have access to GlobalTrek Adventures' policies and travel info. Use this to answer questions accurately. If unsure, say you'll check.", + KnowledgeBaseState = "ENABLED" + } + }, + ActionGroups = new[] + { + new CfnAgent.AgentActionGroupProperty + { + ActionGroupName = $"chatbot-action-group-{Utils.GenerateRandomStringFromStackId(_stack.StackId)}", + Description = "Action group for Bedrccok Agent", + ActionGroupExecutor = new CfnAgent.ActionGroupExecutorProperty + { + Lambda = lambdaFunctionArn + }, + ApiSchema = new CfnAgent.APISchemaProperty + { + Payload = apiSchemaContent + }, + ActionGroupState = "ENABLED" + } + }, + AutoPrepare = true, + // GuardrailConfiguration = new CfnAgent.GuardrailConfigurationProperty + // { + // GuardrailIdentifier = "bedrock-agent-guardrail", + // GuardrailVersion = "1.0", + // }, + SkipResourceInUseCheckOnDelete = false + }); + } + + /// + /// Create role for Bedrock Agent + /// + /// + private Role CreateRoleForBedrockAgent() + { + // Create an IAM role for the Knowledge Base + var agentRole = new Role( + _stack, + "ChatBotBedrockAgentRole", + new RoleProps + { + RoleName = "ChatBot_AmazonBedrockExecutionRoleForAgents_" + Utils.GenerateRandomStringFromStackId(_stack.StackId), + Path = "/service-role/", + Description = "Bedrock Agent Role", + AssumedBy = new ServicePrincipal( + "bedrock.amazonaws.com", + new ServicePrincipalOpts + { + Conditions = new Dictionary + { + ["StringEquals"] = new Dictionary + { + ["aws:SourceAccount"] = _stack.Account, + }, + ["ArnLike"] = new Dictionary + { + ["aws:SourceArn"] = $"arn:aws:bedrock:{_stack.Region}:{_stack.Account}:agent/*" + } + } + }), + MaxSessionDuration = Duration.Minutes(60), + InlinePolicies = new Dictionary + { + ["AmazonBedrockAgentBedrockFoundationModelPolicy_" + Utils.GenerateRandomStringFromStackId(_stack.StackId)] = + new PolicyDocument(new PolicyDocumentProps + { + Statements = + [ + new PolicyStatement(new PolicyStatementProps + { + Sid = "AmazonBedrockAgentBedrockFoundationModelPolicyProd", + Effect = Effect.ALLOW, + Actions = ["bedrock:InvokeModel"], + Resources = [Utils.GetCluade35HaikuFMArn(_stack.Region)], + }) + ] + }), + + ["AmazonBedrockAgentRetrieveKnowledgeBasePolicy_" + Utils.GenerateRandomStringFromStackId(_stack.StackId)] = + new PolicyDocument(new PolicyDocumentProps + { + Statements = + [ + new PolicyStatement(new PolicyStatementProps + { + Sid = "AmazonBedrockAgentRetrieveKnowledgeBasePolicyProd", + Effect = Effect.ALLOW, + Actions = ["bedrock:Retrieve"], + Resources = [_knowledgeBase.AttrKnowledgeBaseArn] + }) + ] + }) + } + }); + + return agentRole; + } + + /// + /// Creates Lambda function for Bedrock Agents + /// + /// + private Function CreateLambdaFunctionForBedrockAgents(Stack stack) + { + // Create Lambda functions for resolvers + return new Function( + stack, + "ChatBotBedrockActionGroupLambdaFunction", + new FunctionProps + { + FunctionName = $"chatbot-bedrock-agent-action-group-{Utils.GenerateRandomStringFromStackId(_stack.StackId)}", + Runtime = Runtime.DOTNET_8, + MemorySize = 512, + Timeout = Duration.Seconds(300), + Architecture = RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.X64 + ? Amazon.CDK.AWS.Lambda.Architecture.X86_64 + : Amazon.CDK.AWS.Lambda.Architecture.ARM_64, + Description = "Function for Bedrock Agent's action group", + Handler = "ActionGroupLambdaFunction", + Code = Code.FromAsset( + "src/LambdaFunctions/BedrockAgent/ActionGroupLambdaFunction", + new Amazon.CDK.AWS.S3.Assets.AssetOptions + { + Bundling = LambdaFunctionsCdkUtils.GetBuildingOptions() + }) + }); + } + + /// + /// Creates a custom resource to prepare the Knowledge Base for Bedrock Agent + /// + /// + private CustomResource CreateAgentAlias() + { + // Create Lambda function for Bedrock Agent Alias Creation + var aliasCreationLambdaFunction = CreateLambdaFunctionForAliasCreation(); + + // Create custom resource to invoke the Lambda function + var customResource = new CustomResource( + _stack, + "ChatBotAgentAliasCreationCustomResource", + new CustomResourceProps + { + ServiceToken = aliasCreationLambdaFunction.FunctionArn, + Properties = new Dictionary + { + ["Region"] = _stack.Region, + ["AliasName"] = "BedrockAgentCdkAlias", + ["AgentId"] = BedrockAgent.AttrAgentId, + ["description"] = "Alias for Bedrock Agent" + } + }); + + // Once resource is created, use Fn::GetAtt to get values of AliasId and AliasArn + AgentAliasId = customResource.GetAttString("AliasId"); + AgentAliasArn = customResource.GetAttString("AliasArn"); + + return customResource; + } + + /// + /// Creates Lambda function for Bedrock Agent Alias Creation + /// + /// + private Function CreateLambdaFunctionForAliasCreation() + { + // Create Lambda functions + var aliasCreationLambda = new Function( + _stack, + "ChatBotBedrockAgentAliasCreationLambdaFunction", + new FunctionProps + { + FunctionName = $"chatbot-bedrock-agent-alias-creation-{Utils.GenerateRandomStringFromStackId(_stack.StackId)}", + Runtime = Runtime.DOTNET_8, + MemorySize = 512, + Timeout = Duration.Seconds(180), + Architecture = RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.X64 + ? Amazon.CDK.AWS.Lambda.Architecture.X86_64 + : Amazon.CDK.AWS.Lambda.Architecture.ARM_64, + Description = "Function to create Bedrock Agent Alias", + Handler = "BedrockAgentAliasCreation", + Code = Code.FromAsset( + "src/LambdaFunctions/BedrockAgent/CustomResource/BedrockAgentAliasCreation", + new Amazon.CDK.AWS.S3.Assets.AssetOptions + { + Bundling = LambdaFunctionsCdkUtils.GetBuildingOptions() + }) + }); + + + // Add resource-based policy to lambda function to allow Bedrock to invoke the Lambda function + aliasCreationLambda.AddToRolePolicy( + new PolicyStatement( + new PolicyStatementProps + { + Sid = "BedrockAliasCreatePermission", + Effect = Effect.ALLOW, + Actions = [ + "bedrock:CreateAgentAlias", + "bedrock:GetAgentAlias", + "bedrock:UpdateAgentAlias", + "bedrock:DeleteAgentAlias", + "bedrock:ListAgentAliases" + ], + Resources = [ + BedrockAgent.AttrAgentArn, + $"arn:aws:bedrock:{_stack.Region}:{_stack.Account}:agent-alias/{BedrockAgent.AttrAgentId}/*" + ] + })); + + + return aliasCreationLambda; + } +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/BedrockAgent/ActionGroup/agent-prompt.txt b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/BedrockAgent/ActionGroup/agent-prompt.txt new file mode 100644 index 000000000..25be9c065 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/BedrockAgent/ActionGroup/agent-prompt.txt @@ -0,0 +1,63 @@ +You are Trekkie, an AI assistant for GlobalTrek Adventures. Your role is to provide exceptional customer service for all travel-related inquiries and tasks. Be friendly, professional, and knowledgeable. + +Key Responsibilities: +1. Answer travel questions about destinations, requirements, and company policies. +2. Search: flights, accommodations, activities, and other travel services. +3. Plan and book trips, including flights, accommodations, and activities. +4. Manage reservations: provide details, modify bookings, and process cancellations. +5. Handle special requests and accommodate accessibility needs. +6. Provide real-time travel assistance for issues like delays or cancellations. +7. Manage loyalty program inquiries and point redemptions. +8. Address customer concerns and escalate complex issues when necessary. +9. Offer travel tips and recommendations. + +Knowledge Base: +- Comprehensive understanding of GlobalTrek Adventures' offerings and policies. +- Familiarity with global travel industry, destinations, and current events affecting travel. +- Up-to-date information on travel advisories, entry requirements, and safety guidelines. + +Interaction Guidelines: +- Greet the customer and ask how you can help. +- Use the customer's name if provided. +- Ask clarifying questions to understand needs fully. +- Provide concise answers, offering more detail if requested. +- Explain reasoning behind recommendations. +- Confirm details before finalizing bookings or changes. +- Always ask if there's anything else you can help with. + +Limitations and Escalation: +- Admit when you're unsure and offer to check with a human representative. +- Transfer complex issues or complaints to human agents. +- Don't make promises about compensations or policy exceptions. + +Privacy and Security: +- Adhere to data protection regulations. Never share personal information. +- Verify customer identity before discussing specific booking details. +- Don't store personal information between conversations. + +Remember: +- Focus on customer satisfaction and making travel planning enjoyable. +- Strive to exceed expectations and create positive interactions. +- Be patient, especially with complex itineraries or stressed travelers. +- Stay updated on travel trends and GlobalTrek Adventures' latest offerings. +- Encourage feedback to improve service quality. + +When dealing with specific tasks: + +Booking: +- Understand customer preferences, budget, and schedule. +- Offer suitable options and explain pros and cons. +- Clearly communicate costs, including fees and potential charges. +- Upsell relevant services (e.g., travel insurance) when appropriate. + +Customer Service: +- Listen attentively to issues and respond empathetically. +- Provide clear, step-by-step solutions when possible. +- If unable to resolve, explain next steps and escalation process. + +Travel Advice: +- Consider the traveler's experience level and specific needs. +- Provide practical, actionable advice. +- Cite reliable sources for health and safety information. + +Always prioritize customer safety and satisfaction in all interactions. Your goal is to make travel planning and execution smooth and enjoyable for GlobalTrek Adventures' customers. \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/BedrockAgent/ActionGroup/api-schema.json b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/BedrockAgent/ActionGroup/api-schema.json new file mode 100644 index 000000000..4bf99f8f6 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/BedrockAgent/ActionGroup/api-schema.json @@ -0,0 +1,61 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Travel Service API", + "version": "1.0.0" + }, + "paths": { + "/flights/search": { + "post": { + "summary": "Search for available flights", + "description": "Search for available flights based on the provided search criteria", + "operationId": "searchFlights", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "origin": { "type": "string" }, + "destination": { "type": "string" }, + "departureDate": { "type": "string", "format": "date" }, + "returnDate": { "type": "string", "format": "date" }, + "passengers": { "type": "integer", "minimum": 1 } + }, + "required": ["origin", "destination", "departureDate", "passengers"] + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "flights": { + "type": "array", + "items": { + "type": "object", + "properties": { + "flightNumber": { "type": "string" }, + "airline": { "type": "string" }, + "departureTime": { "type": "string", "format": "date-time" }, + "arrivalTime": { "type": "string", "format": "date-time" }, + "price": { "type": "number" } + } + } + } + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/BedrockAgent/BedrockAgentKnowledgeBaseStack.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/BedrockAgent/BedrockAgentKnowledgeBaseStack.cs new file mode 100644 index 000000000..5af6b7410 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/BedrockAgent/BedrockAgentKnowledgeBaseStack.cs @@ -0,0 +1,97 @@ +using AlbEcsBedrockAgentsCdkDotnet.BedrockAgent.ActionGroup; +using AlbEcsBedrockAgentsCdkDotnet.BedrockAgent.KnowledgeBase; +using Amazon.CDK; +using Amazon.CDK.AWS.Lambda; +using Amazon.CDK.AWS.OpenSearchServerless; +using Amazon.CDK.AWS.S3; +using Amazon.CDK.AwsBedrock; +using Constructs; + +namespace AlbEcsBedrockAgentsCdkDotnet.BedrockAgent; + +internal sealed class BedrockAgentKnowledgeBaseStack : NestedStack +{ + private readonly BedrockKnowledgeBaseCdk _bedrockKnowledgeBaseCdk; + private readonly BedrockAgentCdk _bedrockAgentCdk; + + /// + /// Initializes a new instance of + /// + /// + /// Stack Name + /// Nested stack properties + internal BedrockAgentKnowledgeBaseStack(Construct scope, string id, INestedStackProps props = null) + : base(scope, id, props) + { + // Create a knowledge base + _bedrockKnowledgeBaseCdk = new BedrockKnowledgeBaseCdk(this); + KnowledgeBase = _bedrockKnowledgeBaseCdk.CreateKnowledgeBase(); + + // Create a Bedrock Agent + _bedrockAgentCdk = new BedrockAgentCdk(this, KnowledgeBase); + BedrockAgent = _bedrockAgentCdk.CreateBedrockAgent(); + + // Add dependencies to make sure knowledge base is created and sync'ed before agent + BedrockAgent.Node.AddDependency(_bedrockKnowledgeBaseCdk.KnowledgeBaseSyncCustomResource); + } + + /// + /// Gets Bedrock Knowledge Base + /// + /// + internal CfnKnowledgeBase KnowledgeBase { get; } + + /// + /// Gets OpenSearch Serverless Collection + /// + /// + internal CfnCollection OssCollection => _bedrockKnowledgeBaseCdk.OssCollection; + + /// + /// Gets Knowledge Base S3 Bucket + /// + /// + internal Bucket KnowledgeBaseBucket => _bedrockKnowledgeBaseCdk.KnowledgeBaseBucket; + + /// + /// Gets Knowledge Base Data Source + /// + /// + internal CfnDataSource KnowledgeBaseDataSource => _bedrockKnowledgeBaseCdk.KnowledgeBaseDataSource; + + /// + /// Gets Knowledge Base Sync Custom Resource + /// + /// + internal CustomResource KnowledgeBaseSyncCustomResource => _bedrockKnowledgeBaseCdk.KnowledgeBaseSyncCustomResource; + + /// + /// Gets Bedrock Agent + /// + /// + internal CfnAgent BedrockAgent { get; } + + /// + /// Gets Action Group Lambda Function + /// + /// Lambda Function + internal Function ActionGroupLambdaFunction => _bedrockAgentCdk.ActionGroupLambdaFunction; + + /// + /// Gets Create Agent Alias Custom Resource + /// + /// Custom Resource + internal CustomResource CreateAgentAliasCustomResource => _bedrockAgentCdk.CreateAgentAliasCustomResource; + + /// + /// Gets Agent Alias Id + /// + /// AliasId + internal string AgentAliasId => _bedrockAgentCdk.AgentAliasId; + + /// + /// Gets Agent Alias Arn + /// + /// Agent Alias ARN + internal string AgentAliasArn => _bedrockAgentCdk.AgentAliasArn; +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/BedrockAgent/KnowledgeBase/BedrockKnowledgeBaseCdk.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/BedrockAgent/KnowledgeBase/BedrockKnowledgeBaseCdk.cs new file mode 100644 index 000000000..4dab8e1a2 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/BedrockAgent/KnowledgeBase/BedrockKnowledgeBaseCdk.cs @@ -0,0 +1,573 @@ +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using AlbEcsBedrockAgentsCdkDotnet.Common; +using Amazon.CDK; +using Amazon.CDK.AWS.IAM; +using Amazon.CDK.AWS.Lambda; +using Amazon.CDK.AWS.OpenSearchServerless; +using Amazon.CDK.AWS.S3; +using Amazon.CDK.AwsBedrock; +using Newtonsoft.Json.Linq; + +namespace AlbEcsBedrockAgentsCdkDotnet.BedrockAgent.KnowledgeBase; + +/// +/// This class provides the functionality to create Bedrock Knowledge Base +/// +internal sealed class BedrockKnowledgeBaseCdk +{ + private readonly Stack _stack; + private readonly string _kbRoleName; + + /// + /// Initializes a new instance of + /// + /// CDK + internal BedrockKnowledgeBaseCdk(Stack stack) + { + _stack = stack; + + // Role name for the Knowledge Base + _kbRoleName = "AmazonBedrockExecutionRoleForKnowledgeBase_" + Utils.GenerateRandomStringFromStackId(_stack.StackId); + } + + /// + /// Gets OpenSearch Serverless Collection + /// + /// + internal CfnCollection OssCollection { get; private set; } + + /// + /// Gets Knowledge Base S3 Bucket + /// + /// + internal Bucket KnowledgeBaseBucket { get; private set; } + + /// + /// Gets Knowledge Base + /// + /// + internal CfnKnowledgeBase KnowledgeBase { get; private set; } + + /// + /// Gets Knowledge Base Data Source + /// + /// + internal CfnDataSource KnowledgeBaseDataSource { get; private set; } + + /// + /// Gets Knowledge Base Sync Custom Resource + /// + /// + internal CustomResource KnowledgeBaseSyncCustomResource { get; private set; } + + /// + /// Creates Knowledge Base + /// + /// + internal CfnKnowledgeBase CreateKnowledgeBase() + { + // Index creation Lambda function for OSS + var indexCreateLambdaFunction = CreateLambdaFunctionForIndexCreation(); + + // Create OpenSearch Serverless Collection for Bedrock KB's Vector Data + OssCollection = CreateOpenSearchServerlessCollection(indexCreateLambdaFunction.Role.RoleArn); + + // Create an Index in OpenSearch Serverless using a custom resource + var indexCreationCustomResource = CreateOssVectorIndexUsingCustomResource(indexCreateLambdaFunction); + + // Create S3 bucket for the Knowledge Base data + KnowledgeBaseBucket = CreateKnowledgeBaseS3Bucket(); + + // Create a Knowledge Base for Bedrock Agents + KnowledgeBase = CreateKnowledgeBaseForBedrockAgents(); + + // Add dependencies to the Knowledge Base to wait for Index Creation Lambda to complete + KnowledgeBase.Node.AddDependency(indexCreationCustomResource); + + // Create a data source for the Knowledge Base + KnowledgeBaseDataSource = CreateKnowledgeBaseDataSource(); + + // Upload documents to the Knowledge Base and Sync the data source + KnowledgeBaseSyncCustomResource = PrepareKnowledgeBase(); + + // Return Knowledge Base + return KnowledgeBase; + } + + /// + /// Creates OpenSearch Serverless Collection + /// + /// + private CfnCollection CreateOpenSearchServerlessCollection(string createIndexLambdaFunctionRoleArn) + { + // Create an OpenSearch Serverless Collection + var ossCollection = new CfnCollection( + _stack, + "ChatBotBedrockKnowledgeBaseOpenSearchServerlessCollection", + new CfnCollectionProps + { + Name = $"chatbot-bedrock-kb-{Utils.GenerateRandomStringFromStackId(_stack.StackId)}", + Description = "Collection for Bedrock Agents vector embeddings", + Type = "VECTORSEARCH", + StandbyReplicas = "DISABLED" + }); + + var ossPoliciesFilePath = Path.Combine("./src/AlbEcsBedrockAgentsCdkDotnet/BedrockAgent/KnowledgeBase/opensearch-policies.json"); + var policiesJson = File.ReadAllText(ossPoliciesFilePath); + var policies = JObject.Parse(policiesJson); + + // Network Policy + var networkPolicy = new CfnSecurityPolicy( + _stack, + "ChatBotOssNetworkPolicy", + new CfnSecurityPolicyProps + { + Name = $"chatbot-bedrock-kb-{Utils.GenerateRandomStringFromStackId(_stack.StackId)}", + Type = "network", + Description = "Custom network policy created by Amazon Bedrock Knowledge Base service to allow a created IAM role " + + "to have permissions on Amazon Open Search collections and indexes.", + Policy = Fn.Sub( + policies["networkPolicy"].ToString(), + new Dictionary + { + { "CollectionName", ossCollection.Name } + }) + }); + + // Encryption Policy + var encryptionPolicy = new CfnSecurityPolicy( + _stack, + "ChatBotOssEncryptionPolicy", + new CfnSecurityPolicyProps + { + Name = $"chatbot-bedrock-kb-{Utils.GenerateRandomStringFromStackId(_stack.StackId)}", + Type = "encryption", + Description = "Custom encryption policy created by Amazon Bedrock Knowledge Base service to allow a created IAM role " + + "to have permissions on Amazon Open Search collections and indexes.", + Policy = Fn.Sub( + policies["encryptionPolicy"].ToString(), + new Dictionary + { + { "CollectionName", ossCollection.Name } + }) + }); + + // Access Policy + var accessPolicy = new CfnAccessPolicy( + _stack, + "ChatBotOssAccessPolicy", + new CfnAccessPolicyProps + { + Name = $"chatbot-bedrock-kb-{Utils.GenerateRandomStringFromStackId(_stack.StackId)}", + Type = "data", + Description = "Custom data access policy created by Amazon Bedrock Knowledge Base service to allow a created IAM role " + + "to have permissions on Amazon Open Search collections and indexes.", + Policy = Fn.Sub( + policies["dataAccessPolicy"].ToString(), + new Dictionary + { + { "CollectionName", ossCollection.Name }, + { "BedrockRoleArn", $"arn:aws:iam::{_stack.Account}:role/service-role/{_kbRoleName}" }, + { "CreateIndexLambdaFunctionRoleArn", createIndexLambdaFunctionRoleArn } + }) + }); + + ossCollection.AddDependency(networkPolicy); + ossCollection.AddDependency(encryptionPolicy); + ossCollection.AddDependency(accessPolicy); + + return ossCollection; + } + + /// + /// Creates an Index in OpenSearch Serverless using a custom resource + /// + /// Lambda function to create index + /// + private CustomResource CreateOssVectorIndexUsingCustomResource(Function indexCreateLambdaFunction) + { + // Grant permissions to Index Creation Lambda on AOSS + indexCreateLambdaFunction.AddToRolePolicy( + new PolicyStatement( + new PolicyStatementProps + { + Sid = "OpenSearchServerlessAPIAccessAllStatement", + Effect = Effect.ALLOW, + Actions = ["aoss:APIAccessAll"], + Resources = [OssCollection.AttrArn] + })); + + // Create custom resource to invoke Index Creation Lambda + return new CustomResource( + _stack, + "ChatBotIndexCreationCustomResource", + new CustomResourceProps + { + ServiceToken = indexCreateLambdaFunction.FunctionArn, + Properties = new Dictionary + { + ["Region"] = _stack.Region, + ["AOSSHost"] = OssCollection.AttrCollectionEndpoint, + ["AOSSIndexName"] = Constants.Bedrock_KB_AOSS_IndexName, + ["AOSSMetadataFieldName"] = Constants.Bedrock_KB_AOSS_MetadataField_Name, + ["AOSSTextFieldName"] = Constants.Bedrock_KB_AOSS_TextField_Name, + ["AOSSVectorFieldName"] = Constants.Bedrock_KB_AOSS_VectorField_Name + } + }); + } + + /// + /// Creates S3 bucket for the Knowledge Base data + /// + /// + private Bucket CreateKnowledgeBaseS3Bucket() + { + return new Bucket( + _stack, + "ChatBotBedrockKnowledgeBaseBucket", + new BucketProps + { + BucketName = $"chatbot-bedrock-knowledge-base-{Utils.GenerateRandomStringFromStackId(_stack.StackId)}", + Versioned = true, + Encryption = BucketEncryption.S3_MANAGED, + RemovalPolicy = RemovalPolicy.DESTROY, // Be cautious with this in production + AutoDeleteObjects = true, // Be cautious with this in production + }); + } + + /// + /// Creates a Knowledge Base for Bedrock Agents + /// + /// + private CfnKnowledgeBase CreateKnowledgeBaseForBedrockAgents() + { + // Create an IAM role for the Knowledge Base + var kbRole = CreateRoleForKnowledgeBaseForBedrockAgent(); + + return new CfnKnowledgeBase( + _stack, + "ChatBotBedrockKnowledgeBase", + new CfnKnowledgeBaseProps + { + Name = $"bedrock-knowledge-base-{Utils.GenerateRandomStringFromStackId(_stack.StackId)}", + Description = "Knowledge base for my Bedrock Agent", + RoleArn = kbRole.RoleArn, + KnowledgeBaseConfiguration = new CfnKnowledgeBase.KnowledgeBaseConfigurationProperty + { + Type = "VECTOR", + VectorKnowledgeBaseConfiguration = new CfnKnowledgeBase.VectorKnowledgeBaseConfigurationProperty + { + EmbeddingModelArn = Utils.GetTitanV2FMArn(_stack.Region), + } + }, + StorageConfiguration = new CfnKnowledgeBase.StorageConfigurationProperty + { + Type = "OPENSEARCH_SERVERLESS", + OpensearchServerlessConfiguration = new CfnKnowledgeBase.OpenSearchServerlessConfigurationProperty + { + CollectionArn = OssCollection.AttrArn, + VectorIndexName = Constants.Bedrock_KB_AOSS_IndexName, + FieldMapping = new CfnKnowledgeBase.OpenSearchServerlessFieldMappingProperty + { + MetadataField = Constants.Bedrock_KB_AOSS_MetadataField_Name, + TextField = Constants.Bedrock_KB_AOSS_TextField_Name, + VectorField = Constants.Bedrock_KB_AOSS_VectorField_Name, + }, + } + } + }); + } + + /// + /// Creates an IAM role for the Knowledge Base + /// + /// + private Role CreateRoleForKnowledgeBaseForBedrockAgent() + { + // Create an IAM role for the Knowledge Base + var kbRole = new Role( + _stack, + "ChatBotBedrockKnowledgeBaseRole", + new RoleProps + { + RoleName = _kbRoleName, + Path = "/service-role/", + Description = "Bedrock Knowledge Base access", + AssumedBy = new ServicePrincipal( + "bedrock.amazonaws.com", + new ServicePrincipalOpts + { + Conditions = new Dictionary + { + ["StringEquals"] = new Dictionary + { + ["aws:SourceAccount"] = _stack.Account, + }, + ["ArnLike"] = new Dictionary + { + ["aws:SourceArn"] = $"arn:aws:bedrock:{_stack.Region}:{_stack.Account}:knowledge-base/*" + } + } + }), + MaxSessionDuration = Duration.Minutes(60), + InlinePolicies = new Dictionary + { + // Access to Foundation Models + ["AmazonBedrockFoundationModelPolicyForKnowledgeBase_" + Utils.GenerateRandomStringFromStackId(_stack.StackId)] = + new PolicyDocument(new PolicyDocumentProps + { + Statements = + [ + new PolicyStatement( + new PolicyStatementProps + { + Sid = "BedrockInvokeModelStatement", + Effect = Effect.ALLOW, + Actions = ["bedrock:InvokeModel"], + Resources = [ + Utils.GetTitanV2FMArn(_stack.Region), + Utils.GetCluade3HaikuFMArn(_stack.Region), + Utils.GetCluade35HaikuFMArn(_stack.Region) + ] + }) + ] + }), + + // Policy to allow access to OpenSearch Serverless + ["AmazonBedrockOSSPolicyForKnowledgeBase_" + Utils.GenerateRandomStringFromStackId(_stack.StackId)] = + new PolicyDocument(new PolicyDocumentProps + { + Statements = + [ + new PolicyStatement( + new PolicyStatementProps + { + Sid = "OpenSearchServerlessAPIAccessAllStatement", + Effect = Effect.ALLOW, + Actions = + [ + "aoss:APIAccessAll" + ], + Resources = [ + OssCollection.AttrArn + ] + }), + ] + }), + + ["AmazonBedrockS3PolicyForKnowledgeBase_" + Utils.GenerateRandomStringFromStackId(_stack.StackId)] = + new PolicyDocument(new PolicyDocumentProps + { + Statements = + [ + new PolicyStatement( + new PolicyStatementProps + { + Sid = "S3ListBucketStatement", + Effect = Effect.ALLOW, + Actions = ["s3:ListBucket"], + Resources = [KnowledgeBaseBucket.BucketArn], + Conditions = new Dictionary + { + ["StringEquals"] = new Dictionary + { + ["aws:ResourceAccount"] = _stack.Account + } + } + }), + new PolicyStatement( + new PolicyStatementProps + { + Sid = "S3GetObjectStatement", + Effect = Effect.ALLOW, + Actions = ["s3:GetObject"], + Resources = [KnowledgeBaseBucket.BucketArn + "/*"], + Conditions = new Dictionary + { + ["StringEquals"] = new Dictionary + { + ["aws:ResourceAccount"] = _stack.Account + } + } + } + ) + ] + }) + } + }); + + return kbRole; + } + + /// + /// Creates a data source for the Knowledge Base + /// + /// + private CfnDataSource CreateKnowledgeBaseDataSource() + { + return new CfnDataSource( + _stack, + "ChatBotBedrockKnowledgeBaseDataSource", + new CfnDataSourceProps + { + DataDeletionPolicy = "DELETE", + Description = "Data source for Bedrock Knowledge Base", + KnowledgeBaseId = KnowledgeBase.AttrKnowledgeBaseId, + Name = $"chatbot-bedrock-knowledge-base-datasource-{Utils.GenerateRandomStringFromStackId(_stack.StackId)}", + VectorIngestionConfiguration = new CfnDataSource.VectorIngestionConfigurationProperty + { + ChunkingConfiguration = new CfnDataSource.ChunkingConfigurationProperty + { + ChunkingStrategy = "FIXED_SIZE", + FixedSizeChunkingConfiguration = new CfnDataSource.FixedSizeChunkingConfigurationProperty + { + MaxTokens = 300, + OverlapPercentage = 20 + } + }, + ParsingConfiguration = new CfnDataSource.ParsingConfigurationProperty + { + ParsingStrategy = "BEDROCK_FOUNDATION_MODEL", + BedrockFoundationModelConfiguration = new CfnDataSource.BedrockFoundationModelConfigurationProperty + { + ModelArn = Utils.GetCluade3HaikuFMArn(_stack.Region), + ParsingPrompt = new CfnDataSource.ParsingPromptProperty + { + ParsingPromptText = "You have access to GlobalTrek Adventures' policies and travel info. Use this to answer questions accurately. If unsure, say you'll check." + } + } + }, + }, + DataSourceConfiguration = new CfnDataSource.DataSourceConfigurationProperty + { + Type = "S3", + S3Configuration = new CfnDataSource.S3DataSourceConfigurationProperty + { + BucketArn = KnowledgeBaseBucket.BucketArn, + BucketOwnerAccountId = _stack.Account + } + } + }); + } + + /// + /// Creates a custom resource to prepare the Knowledge Base for Bedrock Agent + /// + /// + private CustomResource PrepareKnowledgeBase() + { + // Create lambda function for Bedrock Agent's knowledge base sync + var knowledgeBaseSynclambdaFunction = CreateLambdaFunctionForBedrockAgentKnowledgeBaseSync(); + + // Create custom resource to invoke Policy Document Ingestion and Data source sync + return new CustomResource( + _stack, + "ChatBotKnowledgeBasePrepareCustomResource", + new CustomResourceProps + { + ServiceToken = knowledgeBaseSynclambdaFunction.FunctionArn, + Properties = new Dictionary + { + ["Region"] = _stack.Region, + ["KnowledgeBaseId"] = KnowledgeBase.AttrKnowledgeBaseId, + ["DataSourceId"] = KnowledgeBaseDataSource.AttrDataSourceId, + ["BucketName"] = KnowledgeBaseBucket.BucketName + } + }); + } + + /// + /// Creates Lambda function for OpenSearch Serverless Index Creation + /// + /// + private Function CreateLambdaFunctionForIndexCreation() + { + // Create Lambda functions for resolvers + var indexCreationLambda = new Function( + _stack, + "ChatBotBedrockAgentsOssIndexCreationLambdaFunction", + new FunctionProps + { + FunctionName = $"chatbot-bedrock-knowledge-base-index-creation-{Utils.GenerateRandomStringFromStackId(_stack.StackId)}", + Runtime = Runtime.DOTNET_8, + MemorySize = 512, + Timeout = Duration.Seconds(180), + Architecture = RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.X64 + ? Amazon.CDK.AWS.Lambda.Architecture.X86_64 + : Amazon.CDK.AWS.Lambda.Architecture.ARM_64, + Description = "Function to create OSS Index", + Handler = "OssIndexCreation", + Code = Code.FromAsset( + "src/LambdaFunctions/KnowledgeBase/CustomResource/OssIndexCreation", + new Amazon.CDK.AWS.S3.Assets.AssetOptions + { + Bundling = LambdaFunctionsCdkUtils.GetBuildingOptions() + }) + }); + + return indexCreationLambda; + } + + /// + /// Creates Lambda function for Bedrock Agent's knowledge base data ingestion and sync + /// + /// + private Function CreateLambdaFunctionForBedrockAgentKnowledgeBaseSync() + { + // Create Lambda functions for resolvers + var documentIngestionLambda = new Function( + _stack, + "ChatBotBedrockAgentsKnowledgeBaseIngestionLambdaFunction", + new FunctionProps + { + FunctionName = $"chatbot-bedrock-knowledge-base-ingestion-{Utils.GenerateRandomStringFromStackId(_stack.StackId)}", + Runtime = Runtime.DOTNET_8, + MemorySize = 512, + Timeout = Duration.Minutes(15), + Architecture = RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.X64 + ? Amazon.CDK.AWS.Lambda.Architecture.X86_64 + : Amazon.CDK.AWS.Lambda.Architecture.ARM_64, + Description = "Function to create OSS Index", + Handler = "KnowledgeBaseIngestion", + Code = Code.FromAsset( + "src/LambdaFunctions/KnowledgeBase/CustomResource/KnowledgeBaseIngestion", + new Amazon.CDK.AWS.S3.Assets.AssetOptions + { + Bundling = LambdaFunctionsCdkUtils.GetBuildingOptions() + }) + }); + + // Grant permissions to Upload to S3 Bucket + documentIngestionLambda.AddToRolePolicy( + new PolicyStatement( + new PolicyStatementProps + { + Effect = Effect.ALLOW, + Actions = ["s3:PutObject"], + Resources = [ + KnowledgeBaseBucket.BucketArn, + KnowledgeBaseBucket.ArnForObjects("*") + ] + })); + + // Grant permissions to Start and Get Ingestion Job + documentIngestionLambda.AddToRolePolicy( + new PolicyStatement( + new PolicyStatementProps + { + Effect = Effect.ALLOW, + Actions = [ + "bedrock:StartIngestionJob", + "bedrock:GetIngestionJob", + "bedrock:ListIngestionJobs", + ], + Resources = [ + KnowledgeBase.AttrKnowledgeBaseArn + ] + })); + + return documentIngestionLambda; + } +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/BedrockAgent/KnowledgeBase/opensearch-policies.json b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/BedrockAgent/KnowledgeBase/opensearch-policies.json new file mode 100644 index 000000000..437510683 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/BedrockAgent/KnowledgeBase/opensearch-policies.json @@ -0,0 +1,69 @@ +{ + "networkPolicy": [ + { + "Rules": [ + { + "ResourceType": "collection", + "Resource": ["collection/${CollectionName}"] + }, + { + "ResourceType": "dashboard", + "Resource": ["collection/${CollectionName}"] + } + ], + "AllowFromPublic": true + } + ], + "encryptionPolicy": { + "Rules": [ + { + "ResourceType": "collection", + "Resource": ["collection/${CollectionName}"] + } + ], + "AWSOwnedKey": true + }, + "dataAccessPolicy": [ + { + "Rules": [ + { + "ResourceType": "index", + "Resource": ["index/${CollectionName}/*"], + "Permission": [ + "aoss:UpdateIndex", + "aoss:DescribeIndex", + "aoss:ReadDocument", + "aoss:WriteDocument", + "aoss:CreateIndex" + ] + }, + { + "ResourceType": "collection", + "Resource": ["collection/${CollectionName}"], + "Permission": [ + "aoss:DescribeCollectionItems", + "aoss:CreateCollectionItems", + "aoss:UpdateCollectionItems" + ] + } + ], + "Principal": ["${BedrockRoleArn}"] + }, + { + "Rules": [ + { + "ResourceType": "index", + "Resource": ["index/${CollectionName}/*"], + "Permission": [ + "aoss:UpdateIndex", + "aoss:DescribeIndex", + "aoss:ReadDocument", + "aoss:WriteDocument", + "aoss:CreateIndex" + ] + } + ], + "Principal": ["${CreateIndexLambdaFunctionRoleArn}"] + } + ] +} diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/Common/Constants.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/Common/Constants.cs new file mode 100644 index 000000000..3ea985b8d --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/Common/Constants.cs @@ -0,0 +1,20 @@ +namespace AlbEcsBedrockAgentsCdkDotnet.Common; + +internal static class Constants +{ + internal static readonly string Bedrock_FoundationModel_Titan_Embed_Text_V2 = "amazon.titan-embed-text-v2:0"; + + internal static readonly string Bedrock_FoundationModel_Claude3_Haiku = "anthropic.claude-3-haiku-20240307-v1:0"; + + internal static readonly string Bedrock_FoundationModel_Claude3_5_Haiku = "anthropic.claude-3-5-haiku-20241022-v1:0"; + + internal static readonly string Bedrock_KB_AOSS_IndexName = "bedrock-knowledge-base-default-index"; + + internal static readonly string Bedrock_KB_AOSS_MetadataField_Name = "AMAZON_BEDROCK_METADATA"; + + internal static readonly string Bedrock_KB_AOSS_TextField_Name = "AMAZON_BEDROCK_TEXT_CHUNK"; + + internal static readonly string Bedrock_KB_AOSS_VectorField_Name = "bedrock-knowledge-base-default-vector"; + + internal static readonly string ResourceNamePrefix = "chatbot-alb-ecs-bedrockagents-cdk-dotnet"; +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/Common/LambdaFunctionsCdkUtils.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/Common/LambdaFunctionsCdkUtils.cs new file mode 100644 index 000000000..3b00445f3 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/Common/LambdaFunctionsCdkUtils.cs @@ -0,0 +1,32 @@ +using System.Runtime.InteropServices; +using Amazon.CDK; +using Amazon.CDK.AWS.Lambda; + +namespace AlbEcsBedrockAgentsCdkDotnet.Common; + +internal static class LambdaFunctionsCdkUtils +{ + /// + /// Gets the bundling options for Lambda functions + /// + /// + internal static BundlingOptions GetBuildingOptions() + { + // Build options for Lambda functions + return new BundlingOptions() + { + Image = Runtime.DOTNET_8.BundlingImage, + User = "root", + OutputType = BundlingOutput.ARCHIVED, + Command = [ + "/bin/sh", + "-c", + "dotnet tool install -g Amazon.Lambda.Tools && " + + "dotnet build && " + + "dotnet lambda package " + + "--function-architecture " + (RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.X64 ? "x86_64" : "arm64") + " " + + "--output-package /asset-output/function.zip" + ], + }; + } +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/Common/Utils.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/Common/Utils.cs new file mode 100644 index 000000000..c9b97a0f0 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/Common/Utils.cs @@ -0,0 +1,47 @@ +using System; +using System.Linq; +using Amazon.CDK; + +namespace AlbEcsBedrockAgentsCdkDotnet.Common; + +internal static class Utils +{ + /// + /// Generates a random string from the stack id + /// + /// + internal static string GenerateRandomStringFromStackId(string stackId) + { + return Fn.Select(4, Fn.Split("-", Fn.Select(2, Fn.Split("/", stackId)))); + } + + /// + /// Gets ARN for Titan V2 Foundation Model from region + /// + /// AWS region + /// FM ARN + internal static string GetTitanV2FMArn(string region) + { + return $"arn:aws:bedrock:{region}::" + "foundation-model/" + Constants.Bedrock_FoundationModel_Titan_Embed_Text_V2; + } + + /// + /// Gets ARN for Cluade 3 Haiku Foundation Model from region + /// + /// AWS region + /// FM ARN + internal static string GetCluade3HaikuFMArn(string region) + { + return $"arn:aws:bedrock:{region}::" + "foundation-model/" + Constants.Bedrock_FoundationModel_Claude3_Haiku; + } + + /// + /// Gets ARN for Cluade 3.4 Haiku Foundation Model from region + /// + /// AWS region + /// FM ARN + internal static string GetCluade35HaikuFMArn(string region) + { + return $"arn:aws:bedrock:{region}::" + "foundation-model/" + Constants.Bedrock_FoundationModel_Claude3_5_Haiku; + } +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/ECS/AlbEcsStack.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/ECS/AlbEcsStack.cs new file mode 100644 index 000000000..73c3cf7d1 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/ECS/AlbEcsStack.cs @@ -0,0 +1,749 @@ +using System.Collections.Generic; +using System.Runtime.InteropServices; +using AlbEcsBedrockAgentsCdkDotnet.Common; +using Amazon.CDK; +using Amazon.CDK.AWS.ApplicationAutoScaling; +using Amazon.CDK.AWS.EC2; +using Amazon.CDK.AWS.Ecr.Assets; +using Amazon.CDK.AWS.ECS; +using Amazon.CDK.AWS.ElasticLoadBalancingV2; +using Amazon.CDK.AWS.IAM; +using Amazon.CDK.AWS.Logs; +using Amazon.JSII.Runtime.Deputy; +using Constructs; +using HealthCheck = Amazon.CDK.AWS.ElasticLoadBalancingV2.HealthCheck; + +namespace AlbEcsBedrockAgentsCdkDotnet.ECS +{ + public class AlbEcsStack : NestedStack + { + private static readonly string CloudWatchLogGroupName = "/aws/ecs/chatbot-ecs-cluster-logs"; + + /// + /// Initializes a new instance of + /// + /// + /// Stack Name + /// Stack properties + internal AlbEcsStack(Construct scope, string id, INestedStackProps props = null) + : base(scope, id, props) + { + // AgentId Parameter + AgentId = new CfnParameter(this, "AgentId", new CfnParameterProps + { + Type = "String", + Description = "Bedrock Agent ID" + }); + + // AgentAliasId Parameter + AgentAliasId = new CfnParameter(this, "AgentAliasId", new CfnParameterProps + { + Type = "String", + Description = "Bedrock Agent Alias ID" + }); + + // Create VPC + Vpc = CreateVpc(); + + // Create VPC Flow logs + VpcFlowLog = CreateVpcFlowlogs(Vpc); + + // Create security group for ALB + AlbSecurityGroup = CreateSecurityGroupForALB(Vpc); + + // Create security group for ECS + EcsSecurityGroup = CreateSecurityGroupForECS(Vpc, AlbSecurityGroup); + + // Create application load balancer + ApplicationLoadBalancer = CreateApplicationLoadBalancer(Vpc, AlbSecurityGroup); + + // Create target groups for ALB + AlbListener = CreateAlbListener(ApplicationLoadBalancer); + + // Create ECS Cluster + EcsCluster = CreateEcsCluster(Vpc); + + // Create execution role for ECS + EcsExecutionRole = CreateEcsExecutionRole(); + + // Create task role for ECS + EcsTaskRole = CreateTaskRole(AgentId.ValueAsString, AgentAliasId.ValueAsString); + + // ECS Task Definition + TaskDefinition = CreateTaskDefinition(EcsExecutionRole, EcsTaskRole); + + // Add container to the task + ContainerDefinitions = CreateTaskContainers(TaskDefinition, AgentId.ValueAsString, AgentAliasId.ValueAsString); + + // Create ECS Service + EcsService = CreateFargateService(EcsCluster, EcsSecurityGroup, TaskDefinition, AlbListener); + + // Add dependency to remove Capacity Provider Association before Cluster + Aspects.Of(this).Add(new CapacityProviderDependencyAspect()); + } + + /// + /// Gets/Sets the AgentId parameter + /// + /// + internal CfnParameter AgentId { get; set; } + + /// + /// Gets/Sets the AgentAliasId parameter + /// + /// + internal CfnParameter AgentAliasId { get; set; } + + /// + /// Gets the VPC + /// + /// + internal Vpc Vpc { get; } + + /// + /// Gets the VPC Flow logs + /// + /// + internal FlowLog VpcFlowLog { get; } + + /// + /// Gets the security group for ALB + /// + /// + internal SecurityGroup AlbSecurityGroup { get; } + + /// + /// Gets the security group for ECS Cluster/Service + /// + /// + internal SecurityGroup EcsSecurityGroup { get; } + + /// + /// Gets the Application Load Balancer + /// + /// + internal ApplicationLoadBalancer ApplicationLoadBalancer { get; } + + /// + /// Gets the ALB Listener + /// + /// + internal ApplicationListener AlbListener { get; } + + /// + /// Gets the execution role for ECS + /// + /// + internal Role EcsExecutionRole { get; } + + /// + /// Gets the task role for ECS + /// + /// + internal Role EcsTaskRole { get; } + + /// + /// Gets the ECS cluster + /// + /// + internal Cluster EcsCluster { get; } + + /// + /// Gets the Fargate Task Definition + /// + /// + internal FargateTaskDefinition TaskDefinition { get; } + + /// + /// Gets the Container Definitions + /// + /// + internal ContainerDefinition[] ContainerDefinitions { get; } + + /// + /// Gets the Fargate-based ECS Service + /// + /// + internal FargateService EcsService { get; } + + /// + /// Create a VPC with public and private subnets + /// + /// + private Vpc CreateVpc() + { + // VPC + return new Vpc( + this, + "ChatBotVpc", + new VpcProps + { + MaxAzs = 2, + SubnetConfiguration = + [ + new SubnetConfiguration + { + Name = "Public", + SubnetType = SubnetType.PUBLIC, + }, + new SubnetConfiguration + { + Name = "Private", + SubnetType = SubnetType.PRIVATE_WITH_EGRESS + } + ], + VpcName = $"{Constants.ResourceNamePrefix}-vpc-{Utils.GenerateRandomStringFromStackId(StackId)}", + }); + } + + /// + /// Creates a VPC Flowlogs for VPC + /// + /// + /// + private FlowLog CreateVpcFlowlogs(Vpc vpc) + { + return new FlowLog( + this, + "ChatBotVpcFlowLog", + new FlowLogProps + { + FlowLogName = $"{Constants.ResourceNamePrefix}-vpc-flowlog-{Utils.GenerateRandomStringFromStackId(StackId)}", + ResourceType = FlowLogResourceType.FromVpc(vpc), + Destination = FlowLogDestination.ToCloudWatchLogs( + new LogGroup( + this, + "ChatBotVpcFlowLogGroup", + new LogGroupProps + { + LogGroupName = $"ChatBotVpcFlowLogGroup-{Utils.GenerateRandomStringFromStackId(StackId)}", + Retention = RetentionDays.ONE_MONTH, + RemovalPolicy = RemovalPolicy.DESTROY + })), + TrafficType = FlowLogTrafficType.ALL + }); + } + + /// + /// Create a security group for the ALB + /// + /// + /// + private SecurityGroup CreateSecurityGroupForALB(Vpc vpc) + { + var securityGroup = new SecurityGroup( + this, + "ChatBotAlbSecurityGroup", + new SecurityGroupProps + { + Vpc = vpc, + SecurityGroupName = $"{Constants.ResourceNamePrefix}-alb-sg-{Utils.GenerateRandomStringFromStackId(StackId)}", + AllowAllOutbound = true, + AllowAllIpv6Outbound = null, + Description = "Security group for ChatBot ALB" + }); + + // Adding Inbound rule for ALB + securityGroup.AddIngressRule(Peer.AnyIpv4(), Port.Tcp(80), "Allows HTTP access from Internet to ALB"); + + Amazon.CDK.Tags.Of(securityGroup).Add("Name", "chatbot-alb-security-group"); + return securityGroup; + } + + /// + /// Create a security group for the ECS Cluster/Service + /// + /// + /// + private SecurityGroup CreateSecurityGroupForECS(Vpc vpc, SecurityGroup albSecurityGroup) + { + var securityGroup = new SecurityGroup( + this, + "ChatBotEcsSecurityGroup", + new SecurityGroupProps + { + Vpc = vpc, + SecurityGroupName = $"{Constants.ResourceNamePrefix}-ecs-sg-{Utils.GenerateRandomStringFromStackId(StackId)}", + Description = "Security group for ChatBot ECS Cluster/Service", + }); + + // Adding Inbound rule from ALB + securityGroup.AddIngressRule(Peer.SecurityGroupId( + albSecurityGroup.SecurityGroupId), Port.Tcp(8080), "Allows HTTP access from ALB to ECS"); + + Amazon.CDK.Tags.Of(securityGroup).Add("Name", "chatbot-alb-security-group"); + return securityGroup; + } + + /// + /// Creates an Application Load Balancer + /// + /// ALB + /// + private ApplicationLoadBalancer CreateApplicationLoadBalancer(Vpc vpc, SecurityGroup albSecurityGroup) + { + return new ApplicationLoadBalancer( + this, + "ChatBotAlb", + new ApplicationLoadBalancerProps + { + Vpc = vpc, + InternetFacing = true, + LoadBalancerName = $"chatbot-alb-{Utils.GenerateRandomStringFromStackId(StackId)}", + SecurityGroup = albSecurityGroup, + VpcSubnets = new SubnetSelection + { + SubnetType = SubnetType.PUBLIC, + OnePerAz = true, + }, + IpAddressType = IpAddressType.IPV4, + CrossZoneEnabled = true, + DropInvalidHeaderFields = true + }); + } + + /// + /// Creates the listener for the ALB + /// + /// + /// + private static ApplicationListener CreateAlbListener(ApplicationLoadBalancer applicationLoadBalancer) + { + return applicationLoadBalancer.AddListener( + $"{Constants.ResourceNamePrefix}-alb-listener", + new BaseApplicationListenerProps + { + Certificates = null, + DefaultAction = null, + DefaultTargetGroups = null, + MutualAuthentication = null, + Open = true, + Port = 80, + Protocol = ApplicationProtocol.HTTP, + SslPolicy = null + } + ); + } + + /// + /// Creates an ECS cluster + /// + /// + /// + private Cluster CreateEcsCluster(Vpc vpc) + { + var ecsCluster = new Cluster( + this, + "ChatBotCluster", + new ClusterProps + { + ClusterName = $"{Constants.ResourceNamePrefix}-ecs-cluster-{Utils.GenerateRandomStringFromStackId(StackId)}", + Vpc = vpc, + EnableFargateCapacityProviders = true, + ContainerInsights = false, + ExecuteCommandConfiguration = null + }); + + Amazon.CDK.Tags.Of(ecsCluster).Add("Name", "ecs-chatbot-cluster"); + return ecsCluster; + } + + /// + /// Create an execution role for ECS + /// + /// + private Role CreateEcsExecutionRole() + { + var ecsExecutionRole = + new Role( + this, + "ChatBotEcsExecutionRole", + new RoleProps + { + AssumedBy = new ServicePrincipal("ecs-tasks.amazonaws.com"), + RoleName = $"ChatBotEcsExecutionRole_{Utils.GenerateRandomStringFromStackId(StackId)}", + ManagedPolicies = + [ + ManagedPolicy.FromManagedPolicyArn( + this, + "AmazonECSTaskExecutionRolePolicy", + "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy") + ], + InlinePolicies = new Dictionary + { + [$"EcsExecutionPolicyForCloudWatchCreateLogGroup_{Utils.GenerateRandomStringFromStackId(StackId)}"] = + new PolicyDocument( + new PolicyDocumentProps + { + Statements = + [ + new PolicyStatement( + new PolicyStatementProps + { + Sid = "CloudWatchCreateLogGroup", + Effect = Effect.ALLOW, + Actions = ["logs:CreateLogGroup"], + Resources = ["*"] + }) + ] + }) + } + }); + + return ecsExecutionRole; + } + + /// + /// Create a task role for ECS + /// + /// + private Role CreateTaskRole(string agentId, string agentAliasId) + { + return new Role( + this, + "ChatBotEcsTaskRole", + new RoleProps + { + Description = "Task role for ChatBot ECS Task", + RoleName = $"ChatBotEcsTaskRoleFor_{Utils.GenerateRandomStringFromStackId(StackId)}", + Path = "/service-role/", + MaxSessionDuration = Duration.Minutes(60), + AssumedBy = new ServicePrincipal("ecs-tasks.amazonaws.com"), + InlinePolicies = new Dictionary + { + [$"EcsTaskPolicyForBedrockAgents_{Utils.GenerateRandomStringFromStackId(StackId)}"] = + new PolicyDocument(new PolicyDocumentProps + { + Statements = + [ + new PolicyStatement( + new PolicyStatementProps + { + Sid = "EcsTaskBedrockAgentInvoke", + Effect = Effect.ALLOW, + Actions = ["bedrock:InvokeAgent"], + Resources = [$"arn:aws:bedrock:{Region}:{Account}:agent-alias/{agentId}/{agentAliasId}"] + }) + ] + }), + [$"EcsTaskPolicyForCloudWatchDescribeLogGroups_{Utils.GenerateRandomStringFromStackId(StackId)}"] = + new PolicyDocument(new PolicyDocumentProps + { + Statements = + [ + new PolicyStatement( + new PolicyStatementProps + { + Sid = "CloudWatchDescribeLogGroups", + Effect = Effect.ALLOW, + Actions = ["logs:DescribeLogGroups"], + Resources = ["*"] + }) + ] + }), + [$"EcsTaskPolicyForCloudWatch_{Utils.GenerateRandomStringFromStackId(StackId)}"] = + new PolicyDocument(new PolicyDocumentProps + { + Statements = + [ + new PolicyStatement( + new PolicyStatementProps + { + Sid = "CloudWatchLogs", + Effect = Effect.ALLOW, + Actions = + [ + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + Resources = [$"arn:aws:logs:{Region}:{Account}:log-group:{CloudWatchLogGroupName}-{Utils.GenerateRandomStringFromStackId(StackId)}:*"] + }) + ] + }) + } + }); + } + + /// + /// Creates + /// + /// + /// + /// + private FargateTaskDefinition CreateTaskDefinition(Role ecsExecutionRole, Role ecsTaskRole) + { + // ECS Task Definition + return new FargateTaskDefinition( + this, + "ChatBotTaskDefinition", + new FargateTaskDefinitionProps + { + Cpu = 512, + MemoryLimitMiB = 1024, + ExecutionRole = ecsExecutionRole, + TaskRole = ecsTaskRole, + Family = $"{Constants.ResourceNamePrefix}-ecs-task-definition-{Utils.GenerateRandomStringFromStackId(StackId)}", + RuntimePlatform = new RuntimePlatform() + { + OperatingSystemFamily = OperatingSystemFamily.LINUX, + CpuArchitecture = RuntimeInformation.ProcessArchitecture == Architecture.X64 + ? CpuArchitecture.X86_64 + : CpuArchitecture.ARM64, + }, + PidMode = PidMode.TASK, + EphemeralStorageGiB = 21, + Volumes = [] + }); + } + + /// + /// Create container definitions for + /// + /// Fargate task definition + /// Array of + private ContainerDefinition[] CreateTaskContainers( + FargateTaskDefinition taskDefinition, + string agentId, + string agentAliasId) + { + var containerDefinitions = new List + { + // Bedrock Agent's API Proxy Container + taskDefinition.AddContainer( + "EcsChatBotContainer", + new ContainerDefinitionProps + { + ContainerName = "ChatBotContainer", + Hostname = null, + Cpu = 512, + MemoryLimitMiB = 1024, + Image = ContainerImage.FromAsset( + "./src/ECSTasks/BedrockAgentsApiProxy", + new AssetImageProps + { + File = "Dockerfile", + BuildArgs = new Dictionary + { + ["ASPNETCORE_ENVIRONMENT"] = "Production" + }, + AssetName = "chatbot-bedrock-agents-api-proxy-docker-image", + NetworkMode = Amazon.CDK.AWS.Ecr.Assets.NetworkMode.DEFAULT, + Platform = RuntimeInformation.ProcessArchitecture == Architecture.X64 + ? Platform_.LINUX_AMD64 + : Platform_.LINUX_ARM64 + }), + Logging = LogDriver.AwsLogs( + new AwsLogDriverProps + { + LogGroup = new LogGroup( + this, + "ChatBotLogGroup", + new LogGroupProps + { + LogGroupName = $"{CloudWatchLogGroupName}-{Utils.GenerateRandomStringFromStackId(StackId)}", + RemovalPolicy = RemovalPolicy.DESTROY, + Retention = RetentionDays.TWO_WEEKS + }), + StreamPrefix = $"ecs-logs-{Utils.GenerateRandomStringFromStackId(StackId)}", + Mode = AwsLogDriverMode.NON_BLOCKING, + MaxBufferSize = Size.Bytes(1 * 1024 * 1024 * 1024) + }), + Essential = true, + DisableNetworking = false, + Environment = new Dictionary + { + ["AGENT_ID"] = agentId, + ["AGENT_ALIAS_ID"] = agentAliasId, + }, + HealthCheck = new Amazon.CDK.AWS.ECS.HealthCheck + { + Command = ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"], + Interval = Duration.Seconds(30), + Retries = 3, + Timeout = Duration.Seconds(5), + }, + Interactive = false, + LinuxParameters = new LinuxParameters( + this, + "LinuxParameters", + new LinuxParametersProps + { + InitProcessEnabled = true + }), + PortMappings = + [ + new PortMapping + { + ContainerPort = 8080, + AppProtocol = AppProtocol.Http, + Name = "http", + Protocol = Amazon.CDK.AWS.ECS.Protocol.TCP + } + ], + Privileged = false, + PseudoTerminal = false, + ReadonlyRootFilesystem = false, + StartTimeout = Duration.Seconds(60), + StopTimeout = Duration.Seconds(30), + TaskDefinition = taskDefinition, + User = "appuser" + }) + }; + + return [.. containerDefinitions]; + } + + + /// + /// Creates a new Fargate based ECS Service + /// + /// + private FargateService CreateFargateService( + Cluster ecsCluster, + SecurityGroup ecsSecurityGroup, + FargateTaskDefinition taskDefinition, + ApplicationListener albListener) + { + var fargateService = new FargateService( + this, + "ChatBotFargateService", + new FargateServiceProps + { + AssignPublicIp = false, + CapacityProviderStrategies = + [ + new CapacityProviderStrategy + { + CapacityProvider = "FARGATE", + Base = 0, + Weight = 100 + }, + new CapacityProviderStrategy + { + CapacityProvider = "FARGATE_SPOT", + Base = null, + Weight = null + } + ], + CircuitBreaker = new DeploymentCircuitBreaker + { + Rollback = true, + Enable = true, + }, + CloudMapOptions = null, + Cluster = ecsCluster, + DeploymentAlarms = null, + DeploymentController = new DeploymentController + { + Type = DeploymentControllerType.ECS + }, + DesiredCount = 2, + EnableECSManagedTags = true, + EnableExecuteCommand = true, + HealthCheckGracePeriod = Duration.Seconds(60), + MaxHealthyPercent = 200, + MinHealthyPercent = 100, + PlatformVersion = FargatePlatformVersion.LATEST, + PropagateTags = null, + SecurityGroups = [ecsSecurityGroup], + ServiceConnectConfiguration = null, + ServiceName = $"chatbot-fargate-service-{Utils.GenerateRandomStringFromStackId(StackId)}", + TaskDefinition = taskDefinition, + TaskDefinitionRevision = TaskDefinitionRevision.LATEST, + VolumeConfigurations = null, + VpcSubnets = new SubnetSelection { OnePerAz = true, SubnetType = SubnetType.PRIVATE_WITH_EGRESS } + } + ); + + // Removal policy + fargateService.ApplyRemovalPolicy(RemovalPolicy.DESTROY); + + // Register ALB Listener + fargateService.RegisterLoadBalancerTargets( + new EcsTarget + { + ContainerName = "ChatBotContainer", + ContainerPort = 8080, + Listener = ListenerConfig.ApplicationListener( + albListener, + new AddApplicationTargetsProps + { + Conditions = null, + DeregistrationDelay = null, + EnableAnomalyMitigation = null, + HealthCheck = new HealthCheck + { + Enabled = true, + HealthyHttpCodes = "200", + HealthyThresholdCount = 5, + Interval = Duration.Seconds(10), + Path = "/health", + Port = "8080", + Protocol = Amazon.CDK.AWS.ElasticLoadBalancingV2.Protocol.HTTP, + Timeout = Duration.Seconds(6), + UnhealthyThresholdCount = 2 + }, + LoadBalancingAlgorithmType = TargetGroupLoadBalancingAlgorithmType.ROUND_ROBIN, + Port = 80, + Priority = null, + Protocol = ApplicationProtocol.HTTP, + ProtocolVersion = ApplicationProtocolVersion.HTTP1, + SlowStart = Duration.Seconds(60), + StickinessCookieDuration = null, + StickinessCookieName = null, + TargetGroupName = $"fargate-tg-{Utils.GenerateRandomStringFromStackId(StackId)}", + Targets = null, + } + ), + NewTargetGroupId = $"{Constants.ResourceNamePrefix}-fargate-tg", + Protocol = Amazon.CDK.AWS.ECS.Protocol.TCP + } + ); + + // Auto-scaling + var scalableTaskCount = fargateService.AutoScaleTaskCount( + new EnableScalingProps + { + MaxCapacity = 2, + MinCapacity = 1, + }); + + scalableTaskCount.ScaleOnCpuUtilization( + "scaleOnCpu", + new CpuUtilizationScalingProps + { + DisableScaleIn = false, + PolicyName = "scaleOnCpu", + ScaleInCooldown = Duration.Seconds(60), + ScaleOutCooldown = Duration.Seconds(60), + TargetUtilizationPercent = 70 + }); + + // Return + return fargateService; + } + } + + /// + /// Class to add dependencies between constructs to remove Capacity Provider Association before Cluster + /// + internal class CapacityProviderDependencyAspect : DeputyBase, IAspect + { + public void Visit(IConstruct node) + { + if (node is FargateService fargateServiceNode) + { + var children = fargateServiceNode.Cluster.Node.FindAll(); + foreach (var child in children) + { + if (child is CfnClusterCapacityProviderAssociations cfnClusterCapacityProviderAssociations) + { + cfnClusterCapacityProviderAssociations.Node.AddDependency(fargateServiceNode.Cluster); + fargateServiceNode.Node.AddDependency(cfnClusterCapacityProviderAssociations); + } + } + } + } + } +} diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/GlobalSuppressions.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/GlobalSuppressions.cs new file mode 100644 index 000000000..26233fcb5 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/GlobalSuppressions.cs @@ -0,0 +1 @@ +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Potential Code Quality Issues", "RECS0026:Possible unassigned object created by 'new'", Justification = "Constructs add themselves to the scope in which they are created")] diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/Program.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/Program.cs new file mode 100644 index 000000000..8c27b30d8 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/AlbEcsBedrockAgentsCdkDotnet/Program.cs @@ -0,0 +1,44 @@ +using Amazon.CDK; + +namespace AlbEcsBedrockAgentsCdkDotnet +{ + sealed class Program + { + public static void Main(string[] args) + { + var app = new App(); + _ = new AlbEcsBedrockAgentsCdkDotnetStack( + app, + "AlbEcsBedrockAgentsCdkDotnetStack", + new StackProps + { + // If you don't specify 'env', this stack will be environment-agnostic. + // Account/Region-dependent features and context lookups will not work, + // but a single synthesized template can be deployed anywhere. + + // Uncomment the next block to specialize this stack for the AWS Account + // and Region that are implied by the current CLI configuration. + /* + Env = new Amazon.CDK.Environment + { + Account = System.Environment.GetEnvironmentVariable("CDK_DEFAULT_ACCOUNT"), + Region = System.Environment.GetEnvironmentVariable("CDK_DEFAULT_REGION"), + } + */ + + // Uncomment the next block if you know exactly what Account and Region you + // want to deploy the stack to. + /* + Env = new Amazon.CDK.Environment + { + Account = "123456789012", + Region = "us-east-1", + } + */ + + // For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html + }); + app.Synth(); + } + } +} diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/BedrockAgent/BedrockAgentRequestProcessor.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/BedrockAgent/BedrockAgentRequestProcessor.cs new file mode 100644 index 000000000..93993269f --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/BedrockAgent/BedrockAgentRequestProcessor.cs @@ -0,0 +1,265 @@ +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Amazon.BedrockAgentRuntime; +using Amazon.BedrockAgentRuntime.Model; +using Amazon.Runtime.EventStreams; +using Amazon.Runtime.EventStreams.Internal; +using BedrockAgentsApiProxy.BedrockAgent.Model; + +namespace BedrockAgentsApiProxy.BedrockAgent; + +/// +/// Class to process requests to the Bedrock Agent +/// +/// +internal sealed class BedrockAgentRequestProcessor(IConfiguration configuration, ILogger logger) : IDisposable +{ + private readonly AmazonBedrockAgentRuntimeClient _client = new(); + + private readonly ILogger _logger = logger + ?? throw new ArgumentNullException(nameof(logger)); + + private readonly string _agentId = configuration?.GetValue("AGENT_ID") // Environment.GetEnvironmentVariable("AGENT_ID") + ?? throw new ArgumentNullException("AGENT_ID"); + + private readonly string _agentAliasId = configuration?.GetValue("AGENT_ALIAS_ID") //Environment.GetEnvironmentVariable("AGENT_ALIAS_ID") + ?? throw new ArgumentNullException("AGENT_ALIAS_ID"); + + private static readonly JsonSerializerOptions _jsonSerializerOptions = + new() + { + WriteIndented = true, + ReferenceHandler = ReferenceHandler.IgnoreCycles + }; + + /// + /// Sends a request to the Bedrock Agent + /// + /// Bedrock Agent Request ( + /// Cancellation token + /// + public async Task SendRequestToBedrockAgentAsync( + BedrockAgentRequest request, + CancellationToken cancellationToken) + { + try + { + logger.LogInformation("Sending request to BedrockAgent: {request}", + JsonSerializer.Serialize(request, _jsonSerializerOptions)); + + // Create a request object + var invokeRequest = new InvokeAgentRequest + { + AgentId = _agentId, + AgentAliasId = _agentAliasId, + EnableTrace = request.EnableTrace, + EndSession = request.EndSession, + InputText = request.Message, + MemoryId = request.MemoryId, + SessionId = request.SessionId, + SessionState = new SessionState + { + Files = [], + InvocationId = "", + PromptSessionAttributes = request.PromptSessionAttributes, + SessionAttributes = request.SessionAttributes, + ReturnControlInvocationResults = request.ReturnControlInvocationResults, + }, + }; + + // Send the request to the BedrockAgent using the client + var response = await _client.InvokeAgentAsync(invokeRequest, cancellationToken); + + // Check if the response is an error + if (response.HttpStatusCode >= System.Net.HttpStatusCode.BadRequest) + throw new AmazonBedrockAgentRuntimeException($"Error sending request to BedrockAgent: {response.HttpStatusCode}"); + + // Process the response from the Bedrock Agent + var bedrockAgentResponse = await ProcessBedrockInvokeAgentResponseAsync(response, cancellationToken); + + // Get rid of Metadata as System.Text.Json has problems serializing it + bedrockAgentResponse.Trace?.OrchestrationTraces?.ForEach( + orchestrationTrace => + orchestrationTrace?.Observation?.KnowledgeBaseLookupOutput?.RetrievedReferences?.ForEach(rf => rf.Metadata = null)); + + // Log + logger.LogInformation("Received response from BedrockAgent: {response}", + JsonSerializer.Serialize(bedrockAgentResponse, _jsonSerializerOptions)); + + return bedrockAgentResponse; + } + catch (AmazonBedrockAgentRuntimeException abrEx) + { + _logger.LogError(abrEx, "Error sending request to BedrockAgent: {error}.", abrEx.Message); + return CreateErrorResponse(request.SessionId, request.MemoryId ?? string.Empty, abrEx.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error sending request to BedrockAgent: {error}.", ex.Message); + return CreateErrorResponse(request.SessionId, request.MemoryId ?? string.Empty, ex.Message); + } + } + + /// + /// Process the response from the Bedrock Agent + /// + /// Response from Bedrock Agent( + /// Cancellation token + /// + private static async Task ProcessBedrockInvokeAgentResponseAsync( + InvokeAgentResponse response, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(response, nameof(response)); + + try + { + // Agent response + var bedrockAgentResponse = new BedrockAgentResponse + { + SessionId = response.SessionId, + MemoryId = response.MemoryId + }; + + // Chunk Received Event + response.Completion.ChunkReceived += ChunkReceived; + void ChunkReceived(object? sender, EventStreamEventReceivedArgs e) + { + var bytes = e.EventStreamEvent.Bytes.ToArray(); + bedrockAgentResponse.Message += Encoding.UTF8.GetString(bytes); + } + + // Exception Received + response.Completion.ExceptionReceived += ExceptionReceived; + void ExceptionReceived(object? sender, EventStreamExceptionReceivedArgs e) + { + bedrockAgentResponse.Error = e.EventStreamException.Message; + }; + + // Files received + response.Completion.FilesReceived += FilesReceived; + void FilesReceived(object? sender, EventStreamEventReceivedArgs e) + { + bedrockAgentResponse.Files ??= []; + + bedrockAgentResponse.Files.AddRange( + e.EventStreamEvent.Files.Select( + file => new BedrockAgentOutputFile + { + Data = Encoding.UTF8.GetString(file.Bytes.ToArray()), + Name = file.Name, + Type = file.Type + })); + }; + + // Initial Response Received + response.Completion.InitialResponseReceived += InitialResponseReceived; + void InitialResponseReceived(object? sender, EventStreamEventReceivedArgs e) + { + var bytes = e.EventStreamEvent.Payload.ToArray(); + bedrockAgentResponse.Message += Encoding.UTF8.GetString(bytes); + }; + + // Control Received + response.Completion.ReturnControlReceived += ReturnControlReceived; + void ReturnControlReceived(object? sender, EventStreamEventReceivedArgs e) + { + bedrockAgentResponse.ReturnControlPayload = new ReturnControlPayload + { + InvocationId = e.EventStreamEvent.InvocationId, + InvocationInputs = e.EventStreamEvent.InvocationInputs, + }; + }; + + // Trace Received + response.Completion.TraceReceived += TraceReceived; + void TraceReceived(object? sender, EventStreamEventReceivedArgs e) + { + bedrockAgentResponse.Trace ??= new BedrockAgentTrace(); + + if (e.EventStreamEvent.Trace.FailureTrace != null) + bedrockAgentResponse.Trace.FailureTraces.Add(e.EventStreamEvent.Trace.FailureTrace); + if (e.EventStreamEvent.Trace.GuardrailTrace != null) + bedrockAgentResponse.Trace.GuardrailTraces.Add(e.EventStreamEvent.Trace.GuardrailTrace); + if (e.EventStreamEvent.Trace.OrchestrationTrace != null) + bedrockAgentResponse.Trace.OrchestrationTraces.Add(e.EventStreamEvent.Trace.OrchestrationTrace); + if (e.EventStreamEvent.Trace.PostProcessingTrace != null) + bedrockAgentResponse.Trace.PostProcessingTraces.Add(e.EventStreamEvent.Trace.PostProcessingTrace); + if (e.EventStreamEvent.Trace.PreProcessingTrace != null) + bedrockAgentResponse.Trace.PreProcessingTraces.Add(e.EventStreamEvent.Trace.PreProcessingTrace); + }; + + // Start processing the response + await response.Completion.StartProcessingAsync(); + + // Wait for the response to finish processing + while (!cancellationToken.IsCancellationRequested) + { + // Hack - ResponseStream should expose IsProcessing + try + { + if (!response.Completion.Any()) + break; + } + catch (Exception) + { + try + { + await Task.Delay(100, cancellationToken); + } + catch (TaskCanceledException) + { + // Operation got cancelled + bedrockAgentResponse = CreateErrorResponse(response.SessionId, response.MemoryId, "Operation cancelled."); + break; + } + } + } + + // Unsubscribe from all events to prevent memory-leak + response.Completion.ChunkReceived -= ChunkReceived; + response.Completion.ExceptionReceived -= ExceptionReceived; + response.Completion.FilesReceived -= FilesReceived; + response.Completion.InitialResponseReceived -= InitialResponseReceived; + response.Completion.ReturnControlReceived -= ReturnControlReceived; + response.Completion.TraceReceived -= TraceReceived; + + return bedrockAgentResponse; + } + catch (Exception ex) + { + return CreateErrorResponse(response.SessionId, response.MemoryId, ex.Message); + } + finally + { + response.Completion.Dispose(); + } + } + + /// + /// Creates an error response + /// + /// SessionId + /// MemoryId + /// Exception Error + /// + private static BedrockAgentResponse CreateErrorResponse(string sessionId, string memoryId, string error) + { + return new BedrockAgentResponse + { + SessionId = sessionId, + MemoryId = memoryId, + Error = error + }; + } + + /// + /// + /// + public void Dispose() + { + GC.SuppressFinalize(this); + _client.Dispose(); + } +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/BedrockAgent/Model/BedrockAgentOutputFile.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/BedrockAgent/Model/BedrockAgentOutputFile.cs new file mode 100644 index 000000000..a16f55f19 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/BedrockAgent/Model/BedrockAgentOutputFile.cs @@ -0,0 +1,10 @@ +namespace BedrockAgentsApiProxy.BedrockAgent.Model; + +public class BedrockAgentOutputFile +{ + public string? Data { get; set; } + + public string? Type { get; set; } + + public string? Name { get; set; } +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/BedrockAgent/Model/BedrockAgentRequest.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/BedrockAgent/Model/BedrockAgentRequest.cs new file mode 100644 index 000000000..a7356cc1b --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/BedrockAgent/Model/BedrockAgentRequest.cs @@ -0,0 +1,24 @@ +using Amazon.BedrockAgentRuntime.Model; + +namespace BedrockAgentsApiProxy.BedrockAgent.Model; + +public sealed class BedrockAgentRequest +{ + public required string SessionId { get; set; } + + public string? MemoryId { get; set; } + + public string? InvocationId { get; set; } + + public required string Message { get; set; } + + public bool EndSession { get; set; } + + public bool EnableTrace { get; set; } + + public Dictionary? SessionAttributes { get; set; } + + public Dictionary? PromptSessionAttributes { get; set; } + + public List? ReturnControlInvocationResults { get; set; } +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/BedrockAgent/Model/BedrockAgentResponse.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/BedrockAgent/Model/BedrockAgentResponse.cs new file mode 100644 index 000000000..cae5f83ee --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/BedrockAgent/Model/BedrockAgentResponse.cs @@ -0,0 +1,22 @@ +using Amazon.BedrockAgentRuntime.Model; + +namespace BedrockAgentsApiProxy.BedrockAgent.Model; + +public record BedrockAgentResponse +{ + public required string SessionId { get; set; } + + public required string MemoryId { get; set; } + + public string? Message { get; set; } + + public List? Files { get; set; } + + public ReturnControlPayload? ReturnControlPayload { get; set; } + + public BedrockAgentTrace? Trace { get; set; } + + public string? Error { get; set; } + + public bool HasError => !string.IsNullOrEmpty(Error); +} diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/BedrockAgent/Model/BedrockAgentTrace.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/BedrockAgent/Model/BedrockAgentTrace.cs new file mode 100644 index 000000000..e107147cc --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/BedrockAgent/Model/BedrockAgentTrace.cs @@ -0,0 +1,16 @@ +using Amazon.BedrockAgentRuntime.Model; + +namespace BedrockAgentsApiProxy.BedrockAgent.Model; + +public class BedrockAgentTrace +{ + public List FailureTraces { get; } = []; + + public List GuardrailTraces { get; } = []; + + public List OrchestrationTraces { get; } = []; + + public List PostProcessingTraces { get; } = []; + + public List PreProcessingTraces { get; } = []; +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/BedrockAgentsApiProxy.csproj b/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/BedrockAgentsApiProxy.csproj new file mode 100644 index 000000000..102c604b1 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/BedrockAgentsApiProxy.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + Partial + false + false + + + + + + + + + + + + + + + + + + diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/BedrockAgentsApiProxy.http b/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/BedrockAgentsApiProxy.http new file mode 100644 index 000000000..c220f5543 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/BedrockAgentsApiProxy.http @@ -0,0 +1,9 @@ +@BedrockAgentsApiProxy_HostAddress = http://localhost:5176 + +GET {{BedrockAgentsApiProxy_HostAddress}}/health/ +Accept: application/json + +POST {{BedrockAgentsApiProxy_HostAddress}}/message/ +Accept: application/json + +### diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/Dockerfile b/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/Dockerfile new file mode 100644 index 000000000..e6d57cd16 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/Dockerfile @@ -0,0 +1,44 @@ +# Use the official .NET SDK image to build the project +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src + +# Copy csproj and restore dependencies +COPY ["BedrockAgentsApiProxy.csproj", "./"] +RUN dotnet restore + +# Copy the rest of the code and build the project +COPY . . +RUN dotnet publish -c Release -o /app/publish + +# Build runtime image +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime + +# install packages +RUN apt-get update && apt-get install -y curl procps && rm -rf /var/lib/apt/lists/* + +# Set the working directory +WORKDIR /app + +# Copy the published app to the /app directory +COPY --from=build /app/publish . + +# Create a non-root user +RUN adduser --disabled-password --gecos '' appuser + +# Give ownership of the /app directory to appuser +RUN chown -R appuser:appuser /app + +# Switch to the non-root user +USER appuser + +# Expose the port the app will run on +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8080/health || exit 1 + +# Set the entry point for the application +ENTRYPOINT ["dotnet", "BedrockAgentsApiProxy.dll"] + + diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/Program.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/Program.cs new file mode 100644 index 000000000..008e30b6b --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/Program.cs @@ -0,0 +1,141 @@ +using Serilog; +using BedrockAgentsApiProxy.BedrockAgent.Model; +using BedrockAgentsApiProxy.BedrockAgent; +using System.Diagnostics; +using System.Reflection; +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Mvc; + +// Unset following and set proper AGENT_ID and AGENT_ALIAS_ID to test this proxy from +// local machine against actual agent +// This will be set automatically as a part of deployment on ECS Task +// +// Environment.SetEnvironmentVariable("AGENT_ID", ""); +// Environment.SetEnvironmentVariable("AGENT_ALIAS_ID", ""); + +// Create the web application +var app = CreateWebApplication(args); + +// Configure the endpoints +ConfigurEndpoints(app); + +// Run the app +var port = app.Environment.IsDevelopment() ? 5176 : 8080; +await app.RunAsync($"http://0.0.0.0:{port}"); + +/// +/// Create the web application +/// +/// Command-line arguments +/// +static WebApplication CreateWebApplication(string[] args) +{ + // Create the builder and configure the app + var builder = WebApplication.CreateBuilder(args); + + // Configuration + builder.Configuration.AddEnvironmentVariables(); + builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); + + // Add services to the container. + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); + builder.Services.AddSingleton(); + builder.Services.ConfigureHttpJsonOptions( + options => + { + options.SerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; + }); + builder.Services.Configure( + option => + { + option.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; + }); + + // configure logging + builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging")); + + // Enable for detailed HTTP request/response logging + // builder.Services.AddHttpLogging(logging => { }); + + // Configure Serilog + // Log to console, AWS Logs configuration in TaskDefinition will pipe the logs to CloudWatch + var loggerConfiguration = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Console(); + + // Serilog Default logger + Log.Logger = loggerConfiguration.CreateLogger(); + + // Add Serilog to the builder + builder.Logging.AddSerilog(Log.Logger, dispose: true); + + // Build the app + var app = builder.Build(); + + // Enable for detailed HTTP request/response logging + // app.UseHttpLogging(); + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } + + // Uncomment if using HTTPS + // app.UseHttpsRedirection(); + + return app; +} + +/// +/// Configure the endpoints +/// +/// +static void ConfigurEndpoints(WebApplication app) +{ + // Add the endpoints to the app to handle requests + app.MapPost("/message", async (BedrockAgentRequest request, CancellationToken cancellationToken) => + { + try + { + var processor = app.Services.GetService(); + if (processor == null) + { + app.Logger.LogError("BedrockAgentRequestProcessor not found in the service container."); + throw new InvalidOperationException("BedrockAgentRequestProcessor not found in the service container."); + } + + var response = await processor.SendRequestToBedrockAgentAsync(request, cancellationToken); + return Results.Ok(response); + } + catch (OperationCanceledException oex) + { + app.Logger.LogWarning(oex, "The request was canceled."); + return Results.StatusCode(StatusCodes.Status499ClientClosedRequest); + } + catch (Exception ex) + { + app.Logger.LogError(ex, "An error occurred while processing the request."); + return Results.StatusCode(StatusCodes.Status500InternalServerError); + } + }) + .WithName("PostMessage") + .WithOpenApi(); + + // Add Health Endpoint + app.MapGet("/health", () => + { + var healthStatus = new + { + Status = "Healthy", + UpTime = (DateTime.UtcNow - Process.GetCurrentProcess().StartTime.ToUniversalTime()).ToString("c"), + Version = FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).FileVersion + }; + + return Results.Ok(healthStatus); + }) + .WithName("Health") + .WithOpenApi(); +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/Properties/launchSettings.json b/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/Properties/launchSettings.json new file mode 100644 index 000000000..688c58916 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:7368", + "sslPort": 44392 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5176", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7296;http://localhost:5176", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/appsettings.json b/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/appsettings.json new file mode 100644 index 000000000..dbffb491f --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/ECSTasks/BedrockAgentsApiProxy/appsettings.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "System": "Warning", + "Microsoft": "Warning", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/ActionGroupLambdaFunction/ActionGroupLambdaFunction.csproj b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/ActionGroupLambdaFunction/ActionGroupLambdaFunction.csproj new file mode 100644 index 000000000..74dd5d6a4 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/ActionGroupLambdaFunction/ActionGroupLambdaFunction.csproj @@ -0,0 +1,31 @@ + + + Exe + net8.0 + enable + enable + Lambda + true + + true + + + + true + + partial + + + + + + + + + + + + + \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/ActionGroupLambdaFunction/Function.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/ActionGroupLambdaFunction/Function.cs new file mode 100644 index 000000000..5fa73b7bd --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/ActionGroupLambdaFunction/Function.cs @@ -0,0 +1,184 @@ +using System.Text.Json; +using Amazon.Lambda.Core; +using ActionGroupLambdaFunction.Models; +using ActionGroupLambdaFunction.Serialization; + +namespace ActionGroupLambdaFunction; + +public class Function +{ + public static Task FunctionHandler(ApiRequest request, ILambdaContext context) + { + context.Logger.LogInformation("Processing request: " + + $"{JsonSerializer.Serialize(request, LambdaFunctionJsonSerializerContext.Default.ApiRequest)}"); + context.Logger.LogInformation($"Processing request: {request.HttpMethod}:{request.ApiPath}"); + + // Return the list of flights as the response + var apiResponse = new ApiResponse + { + messageVersion = "1.0", + response = new Response + { + actionGroup = request.ActionGroup, + apiPath = request.ApiPath, + httpMethod = request.HttpMethod, + httpStatusCode = 200, + } + }; + + try + { + // Get the flight search request from the API request and search for flights + var flights = GenerateMockFlights(GetFlightSearchRequest(request)); + + // Set the response body to the list of flights + apiResponse.response.responseBody = + new Dictionary + { + { + "application/json", + new ResponseBody + { + body = JsonSerializer.Serialize(flights, LambdaFunctionJsonSerializerContext.Default.ListFlight) + } + } + }; + + var serializedResponse = JsonSerializer.Serialize(apiResponse, LambdaFunctionJsonSerializerContext.Default.ApiResponse); + context.Logger.LogInformation($"Returning response: {serializedResponse}"); + } + catch (Exception ex) + { + context.Logger.LogError($"Error processing request. Error: {ex.Message}"); + apiResponse.response.httpStatusCode = 400; + apiResponse.response.responseBody = + new Dictionary + { + { + "application/json", + new ResponseBody + { + body = JsonSerializer.Serialize(new Error + { + Message = ex.Message, + Code = ex.HResult + }, LambdaFunctionJsonSerializerContext.Default.Error) + } + } + }; + } + return Task.FromResult(apiResponse); + } + + /// + /// Creates from + /// + /// API Request (from user) + /// + /// If invalid API request + private static FlightSearchRequest GetFlightSearchRequest(ApiRequest apiRequest) + { + var properties = apiRequest?.RequestBody?.Content?.JsonProperties?.Properties + ?? throw new Exception("Invalid request body, cannot find properties"); + + // Departure Date + var departureDateStr = properties?.FirstOrDefault(p => p.Name == "departureDate")?.Value?.ToString() + ?? throw new Exception("Invalid request body, cannot find departureDate"); + if (!DateTime.TryParse(departureDateStr, out DateTime departureDatetime)) + throw new Exception("Invalid departure date"); + if (departureDatetime < DateTime.Now) + throw new Exception("Departure date must be in the future"); + + // Return Date + DateTime returnDatetime = DateTime.MinValue; + var returnDateStr = properties?.FirstOrDefault(p => p.Name == "returnDate")?.Value?.ToString(); + if (!string.IsNullOrEmpty(returnDateStr)) + { + if (!DateTime.TryParse(returnDateStr, out returnDatetime)) + throw new Exception("Invalid return date"); + if (returnDatetime < departureDatetime) + throw new Exception("Return date must be after departure date"); + } + + var request = new FlightSearchRequest + { + Origin = properties?.FirstOrDefault(p => p.Name == "origin")?.Value?.ToString() + ?? throw new Exception("Invalid request body, cannot find origin"), + + Destination = properties?.FirstOrDefault(p => p.Name == "destination")?.Value?.ToString() + ?? throw new Exception("Invalid request body, cannot find destination"), + + DepartureDate = departureDatetime, + + ReturnDate = returnDatetime == DateTime.MinValue ? null : returnDatetime, + + Passengers = int.Parse(properties?.FirstOrDefault(p => p.Name == "passengers")?.Value?.ToString() ?? "1") + }; + + return request; + } + + /// + /// Creates mock flight data from request + /// + /// + /// List of + private static List GenerateMockFlights(FlightSearchRequest request) + { + var random = new Random(); + var flights = new List(); + + for (int i = 0; i < 5; i++) + { + var airlineCode = RandomAirlineCode(); + + var departureTime = request.DepartureDate.AddHours(random.Next(24)); + var arrivalTime = departureTime.AddHours(random.Next(1, 24)); + + flights.Add( + new Flight + { + FlightNumber = $"{airlineCode}{random.Next(1000, 9999)}", + Airline = Airlines[airlineCode], + DepartureTime = departureTime.ToString("o"), + ArrivalTime = arrivalTime.ToString("o"), + Price = Math.Round(random.NextDouble() * (1000 - 100) + 100, 2) + }); + + if (request.ReturnDate != null) + { + departureTime = request.ReturnDate.Value.AddHours(random.Next(24)); + arrivalTime = departureTime.AddHours(random.Next(1, 24)); + + flights[i].ReturnFlight = new Flight + { + FlightNumber = $"{airlineCode}{random.Next(1000, 9999)}", + Airline = Airlines[airlineCode], + DepartureTime = departureTime.ToString("o"), + ArrivalTime = arrivalTime.ToString("o"), + Price = Math.Round(random.NextDouble() * (1000 - 100) + 100, 2) + }; + } + } + + return flights; + } + + /// + /// Gets random airline code + /// + /// Airline code + private static string RandomAirlineCode() => new string[] { "AA", "DL", "UA", "BA", "LH" }[new Random().Next(5)]; + + /// + /// List of airlines + /// + private static readonly Dictionary Airlines = new() + { + { "AA", "American Airlines" }, + { "DL", "Delta Air Lines" }, + { "UA", "United Airlines" }, + { "BA", "British Airways" }, + { "LH", "Lufthansa" } + }; +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/ActionGroupLambdaFunction/Models/ApiRequest.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/ActionGroupLambdaFunction/Models/ApiRequest.cs new file mode 100644 index 000000000..0f12ee086 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/ActionGroupLambdaFunction/Models/ApiRequest.cs @@ -0,0 +1,66 @@ +using System.Text.Json.Serialization; + +namespace ActionGroupLambdaFunction.Models; + +public class Parameter +{ + public string? Name { get; set; } + public string? Type { get; set; } + public string? Value { get; set; } +} + +public class Property +{ + public string? Name { get; set; } + public string? Type { get; set; } + public string? Value { get; set; } +} + +public class JsonProperties +{ + public List? Properties { get; set; } +} + +public class Content +{ + [JsonPropertyName("application/json")] + public JsonProperties? JsonProperties { get; set; } +} + +public class RequestBody +{ + public Content? Content { get; set; } +} + +public class SessionAttributes +{ +} + +public class PromptSessionAttributes +{ +} + +public class Agent +{ + public string? Name { get; set; } + public string? Id { get; set; } + public string? Alias { get; set; } + public string? Version { get; set; } +} + +public class ApiRequest +{ + public string? MessageVersion { get; set; } + public Agent? Agent { get; set; } + public string? InputText { get; set; } + public string? SessionId { get; set; } + public string? ExecutionType { get; set; } + public string? ActionGroup { get; set; } + public string? ApiPath { get; set; } + public string? HttpMethod { get; set; } + public List? Parameters { get; set; } + public RequestBody? RequestBody { get; set; } + public SessionAttributes? SessionAttributes { get; set; } + public PromptSessionAttributes? PromptSessionAttributes { get; set; } +} + diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/ActionGroupLambdaFunction/Models/ApiResponse.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/ActionGroupLambdaFunction/Models/ApiResponse.cs new file mode 100644 index 000000000..ccf3934da --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/ActionGroupLambdaFunction/Models/ApiResponse.cs @@ -0,0 +1,56 @@ +namespace ActionGroupLambdaFunction.Models; + +public class VectorSearchConfiguration +{ + public int NumberOfResults { get; set; } + + public string? OverrideSearchType { get; set; } + + public string? Filter { get; set; } +} + +public class RetrievalConfiguration +{ + public VectorSearchConfiguration? VectorSearchConfiguration { get; set; } +} + +public class KnowledgeBasesConfiguration +{ + public string? KnowledgeBaseId { get; set; } + + public RetrievalConfiguration? RetrievalConfiguration { get; set; } +} + +public class ResponseBody +{ + public string? body { get; set; } +} + +public class Response +{ + public string? actionGroup { get; set; } + + public string? apiPath { get; set; } + + public string? httpMethod { get; set; } + + public int httpStatusCode { get; set; } + + public Dictionary? responseBody { get; set; } +} + +public class ApiResponse +{ + public string? messageVersion { get; set; } + + public Response? response { get; set; } + + public Dictionary? sessionAttributes { get; set; } + + public Dictionary? promptSessionAttributes { get; set; } + + public List? knowledgeBasesConfiguration { get; set; } +} + + + diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/ActionGroupLambdaFunction/Models/Models.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/ActionGroupLambdaFunction/Models/Models.cs new file mode 100644 index 000000000..d3b32def7 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/ActionGroupLambdaFunction/Models/Models.cs @@ -0,0 +1,36 @@ +namespace ActionGroupLambdaFunction.Models; + +public class FlightSearchRequest +{ + public required string Origin { get; set; } + + public required string Destination { get; set; } + + public required DateTime DepartureDate { get; set; } + + public required DateTime? ReturnDate { get; set; } + + public int Passengers { get; set; } +} + +public class Flight +{ + public string? FlightNumber { get; set; } + + public string? Airline { get; set; } + + public string? DepartureTime { get; set; } + + public string? ArrivalTime { get; set; } + + public double? Price { get; set; } + + public Flight? ReturnFlight { get; set; } +} + +public class Error +{ + public string? Message { get; set; } + + public int? Code { get; set; } +} diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/ActionGroupLambdaFunction/Program.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/ActionGroupLambdaFunction/Program.cs new file mode 100644 index 000000000..fc440b606 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/ActionGroupLambdaFunction/Program.cs @@ -0,0 +1,23 @@ +using Amazon.Lambda.Core; +using Amazon.Lambda.RuntimeSupport; +using Amazon.Lambda.Serialization.SystemTextJson; +using ActionGroupLambdaFunction.Models; +using ActionGroupLambdaFunction.Serialization; + +namespace ActionGroupLambdaFunction; + +public class Program +{ + /// + /// The main entry point for the Lambda function. The main function is called once during the Lambda init phase. It + /// initializes the .NET Lambda runtime client passing in the function handler to invoke for each Lambda event and + /// the JSON serializer to use for converting Lambda JSON format to the .NET types. + /// + private static async Task Main() + { + Func> handler = Function.FunctionHandler; + await LambdaBootstrapBuilder.Create(handler, new SourceGeneratorLambdaJsonSerializer()) + .Build() + .RunAsync(); + } +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/ActionGroupLambdaFunction/Serialization/LambdaFunctionJsonSerializerContext.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/ActionGroupLambdaFunction/Serialization/LambdaFunctionJsonSerializerContext.cs new file mode 100644 index 000000000..0df5205e9 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/ActionGroupLambdaFunction/Serialization/LambdaFunctionJsonSerializerContext.cs @@ -0,0 +1,25 @@ +using System.Text.Json.Serialization; +using ActionGroupLambdaFunction.Models; + +namespace ActionGroupLambdaFunction.Serialization +{ + /// + /// This class is used to register the input event and return type for the FunctionHandler method with the System.Text.Json source generator. + /// There must be a JsonSerializable attribute for each type used as the input and return type or a runtime error will occur + /// from the JSON serializer unable to find the serialization information for unknown types. + /// + [JsonSerializable(typeof(object))] + [JsonSerializable(typeof(string))] + [JsonSerializable(typeof(ApiRequest))] + [JsonSerializable(typeof(ApiResponse))] + [JsonSerializable(typeof(FlightSearchRequest))] + [JsonSerializable(typeof(Flight))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(Error))] + public partial class LambdaFunctionJsonSerializerContext : JsonSerializerContext + { + // By using this partial class derived from JsonSerializerContext, we can generate reflection free JSON Serializer code at compile time + // which can deserialize our class and properties. However, we must attribute this class to tell it what types to generate serialization code for. + // See https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-source-generation + } +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/CustomResource/BedrockAgentAliasCreation/BedrockAgentAliasCreation.csproj b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/CustomResource/BedrockAgentAliasCreation/BedrockAgentAliasCreation.csproj new file mode 100644 index 000000000..dc39b0041 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/CustomResource/BedrockAgentAliasCreation/BedrockAgentAliasCreation.csproj @@ -0,0 +1,32 @@ + + + Exe + net8.0 + enable + enable + Lambda + true + + true + + + + true + + partial + + + + + + + + + + + + + + \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/CustomResource/BedrockAgentAliasCreation/Function.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/CustomResource/BedrockAgentAliasCreation/Function.cs new file mode 100644 index 000000000..8a2312a65 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/CustomResource/BedrockAgentAliasCreation/Function.cs @@ -0,0 +1,241 @@ +using System.Net; +using System.Text.Json; +using Amazon.BedrockAgent; +using Amazon.BedrockAgent.Model; +using Amazon.Lambda.Core; +using BedrockAgentAliasCreation.Models; +using BedrockAgentAliasCreation.Serialization; +using BedrockAgentAliasCreation.Utils; + +namespace BedrockAgentAliasCreation; + +public class Function +{ + public static async Task FunctionHandler(object request, ILambdaContext context) + { + context.Logger.LogInformation($"Received input as {request}"); + + var cfnRequest = JsonSerializer.Deserialize(request?.ToString() ?? string.Empty, LambdaFunctionJsonSerializerContext.Default.CfnRequest) + ?? throw new Exception("Invalid request"); + + var response = new CfnResponse + { + // build all the common responses from the request + StackId = cfnRequest.StackId, + RequestId = cfnRequest.RequestId, + LogicalResourceId = cfnRequest.LogicalResourceId, + PhysicalResourceId = !string.IsNullOrEmpty(cfnRequest.PhysicalResourceId) + ? cfnRequest.PhysicalResourceId + : $"{cfnRequest.ResourceProperties.AgentId}-{cfnRequest.ResourceProperties.AliasName}", + }; + + try + { + switch (cfnRequest.RequestType.ToLowerInvariant()) + { + case "create": + context.Logger.LogInformation("Received Create request"); + (string aliasId, string aliasArn) = await CreateBedrockAgentAliasAsync(cfnRequest.ResourceProperties, context); + + response.Status = "SUCCESS"; + response.PhysicalResourceId = $"{cfnRequest.ResourceProperties.AgentId}-{aliasId}"; + response.Data = new Dictionary + { + { "AliasId", aliasId }, + { "AliasArn", aliasArn } + }; + break; + + case "delete": + context.Logger.LogInformation("Received Delete request"); + await DeleteAllAliasesAsync(cfnRequest.ResourceProperties, context); + + response.Status = "SUCCESS"; + response.PhysicalResourceId = cfnRequest.PhysicalResourceId; + break; + + case "update": + context.Logger.LogInformation("Received Update request"); + response.Status = "SUCCESS"; + break; + } + + context.Logger.LogInformation($"Uploading response to {cfnRequest.ResponseURL} "); + await ResponseUtils.UploadResponse(cfnRequest.ResponseURL, response); + } + catch (Exception e) + { + context.Logger.LogError("Error occurred: " + e.Message); + response.Status = "FAILED"; + response.Reason = e.Message; + await ResponseUtils.UploadResponse(cfnRequest.ResponseURL, response); + } + + context.Logger.LogInformation("Finished"); + } + + /// + /// Creates a new Bedrock Agent Alias + /// + /// Resource properties + /// Lambda context + /// Created Alias Id + private static async Task<(string aliasId, string aliasArn)> CreateBedrockAgentAliasAsync( + ResourceProperties resourceProperties, ILambdaContext context) + { + // Get Region + var region = resourceProperties.Region?.ToString() ?? throw new Exception("Region not provided from resource properties"); + context.Logger.LogInformation($"CreateAlias:Region: {region}"); + + // Get AliasName + var aliasName = resourceProperties.AliasName?.ToString() ?? throw new Exception("AliasName not provided from resource properties"); + context.Logger.LogInformation($"CreateAlias:AliasName: {aliasName}"); + + // Get AgentId + var agentId = resourceProperties.AgentId?.ToString() ?? throw new Exception("AgentId not provided from resource properties"); + context.Logger.LogInformation($"CreateAlias:AgentId: {agentId}"); + + // Get Description + var description = resourceProperties.Description?.ToString() ?? throw new Exception("Description not provided from resource properties"); + context.Logger.LogInformation($"CreateAlias:Description: {description}"); + + try + { + // Create + context.Logger.LogInformation("Creating Bedrock Agent Alias"); + + using var bedrockAgentClient = new AmazonBedrockAgentClient(); + var clientToken = Guid.NewGuid().ToString(); + + // Create a new alias + var createAgentAliasResponse = await bedrockAgentClient.CreateAgentAliasAsync( + new CreateAgentAliasRequest + { + AgentAliasName = aliasName, + AgentId = agentId, + ClientToken = clientToken, + Description = !string.IsNullOrEmpty(description) ? description : "Alias for Bedrock Agent" + }); + + // Check for successful status code + if (createAgentAliasResponse.HttpStatusCode >= HttpStatusCode.BadRequest) + throw new Exception($"Failed to create agent alias. Status code: {createAgentAliasResponse.HttpStatusCode}"); + else + context.Logger.LogInformation($"Agent Alias was created successfully, Id: {createAgentAliasResponse.AgentAlias.AgentAliasId}"); + + // Return the created alias id + return (createAgentAliasResponse.AgentAlias.AgentAliasId, + createAgentAliasResponse.AgentAlias.AgentAliasArn); + } + catch (Exception e) + { + context.Logger.LogError($"Error creating agent alias: {e.Message}{Environment.NewLine}{e.StackTrace}"); + throw; + } + } + + /// + /// Deletes all aliases for the given agent + /// + /// Resource properties + /// Lambda context + private static async Task DeleteAllAliasesAsync( + ResourceProperties resourceProperties, ILambdaContext context) + { + // Get Region + var region = resourceProperties.Region?.ToString() ?? throw new Exception("Region not provided from resource properties"); + context.Logger.LogInformation($"CreateAlias:Region: {region}"); + + // Get AgentId + var agentId = resourceProperties.AgentId?.ToString() ?? throw new Exception("AgentId not provided from resource properties"); + context.Logger.LogInformation($"CreateAlias:AgentId: {agentId}"); + + // Get all agent aliases + var agentAliases = await GetAllAgentAliasesAsync(agentId, context); + + try + { + // Create + context.Logger.LogInformation("Deleting All Bedrock Agent Aliases"); + + using var bedrockAgentClient = new AmazonBedrockAgentClient(); + + // Delete all aliases + agentAliases.ForEach(async agentAliasId => + { + // Delete alias + var deleteAgentAliasResponse = await bedrockAgentClient.DeleteAgentAliasAsync( + new DeleteAgentAliasRequest + { + AgentAliasId = agentAliasId, + AgentId = agentId + }); + + // Check for successful status code + if (deleteAgentAliasResponse.HttpStatusCode >= HttpStatusCode.BadRequest) + throw new Exception($"Failed to delete agent alias. Status code: {deleteAgentAliasResponse.HttpStatusCode}"); + else + context.Logger.LogInformation($"Agent Alias was deleted successfully, Id: {agentAliasId}"); + }); + } + catch (Exception e) + { + context.Logger.LogError($"Error creating agent alias: {e.Message}{Environment.NewLine}{e.StackTrace}"); + throw; + } + } + + /// + /// Gets all aliases for the given agent + /// + /// AgentId + /// Lambda context + /// + private static async Task> GetAllAgentAliasesAsync(string agentId, ILambdaContext context) + { + context.Logger.LogInformation("Getting All Bedrock Agent Aliases"); + + var agentAliases = new List(); + string? nextToken = null; + using var bedrockAgentClient = new AmazonBedrockAgentClient(); + + try + { + while (true) + { + // List all aliases + var listAgentAliasesResponse = await bedrockAgentClient.ListAgentAliasesAsync( + new ListAgentAliasesRequest + { + AgentId = agentId, + MaxResults = 10, + NextToken = nextToken + }); + + // Check for successful status code + if (listAgentAliasesResponse.HttpStatusCode >= HttpStatusCode.BadRequest) + throw new Exception($"Failed to create agent alias. Status code: {listAgentAliasesResponse.HttpStatusCode}"); + + // Check if there are no aliases + if (listAgentAliasesResponse.AgentAliasSummaries.Count == 0) + break; + + // Add Aliases to list + agentAliases.AddRange(listAgentAliasesResponse.AgentAliasSummaries.Select(alias => alias.AgentAliasId)); + + // Check if there are more aliases + if (string.IsNullOrEmpty(listAgentAliasesResponse.NextToken)) + break; + + nextToken = listAgentAliasesResponse.NextToken; + } + + return agentAliases; + } + catch(Exception ex) + { + context.Logger.LogError($"Error getting agent aliases: {ex.Message}{Environment.NewLine}{ex.StackTrace}"); + throw; + } + } +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/CustomResource/BedrockAgentAliasCreation/Models/Models.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/CustomResource/BedrockAgentAliasCreation/Models/Models.cs new file mode 100644 index 000000000..1dfa72ccb --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/CustomResource/BedrockAgentAliasCreation/Models/Models.cs @@ -0,0 +1,55 @@ +// This file contains the models used by the Lambda function to handle the custom resource request and response. +namespace BedrockAgentAliasCreation.Models; + +public class CfnRequest +{ + public string RequestType { get; set; } = string.Empty; + + public string ResponseURL { get; set; } = string.Empty; + + public string StackId { get; set; } = string.Empty; + + public string RequestId { get; set; } = string.Empty; + + public string ResourceType { get; set; } = string.Empty; + + public string LogicalResourceId { get; set; } = string.Empty; + + public string PhysicalResourceId { get; set; } = string.Empty; + + public ResourceProperties ResourceProperties { get; set; } = new ResourceProperties(); +} + +public class CfnResponse +{ + public string Status { get; set; } = string.Empty; + + public string Reason { get; set; } = string.Empty; + + public string PhysicalResourceId { get; set; } = string.Empty; + + public string StackId { get; set; } = string.Empty; + + public string RequestId { get; set; } = string.Empty; + + public string LogicalResourceId { get; set; } = string.Empty; + + public bool NoEcho { get; set; } = false; + + public Dictionary? Data {get;set;} = null; +} + +public sealed class ResourceProperties +{ + public string ServiceToken { get; set; } = string.Empty; + + public string Region { get; set; } = string.Empty; + + public string AliasName { get; set; } = string.Empty; + + public string AgentId { get; set; } = string.Empty; + + public string Description { get; set; } = string.Empty; + + public string AgentVersion { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/CustomResource/BedrockAgentAliasCreation/Program.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/CustomResource/BedrockAgentAliasCreation/Program.cs new file mode 100644 index 000000000..269c95d6f --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/CustomResource/BedrockAgentAliasCreation/Program.cs @@ -0,0 +1,22 @@ +using Amazon.Lambda.Core; +using Amazon.Lambda.RuntimeSupport; +using Amazon.Lambda.Serialization.SystemTextJson; +using BedrockAgentAliasCreation.Serialization; + +namespace BedrockAgentAliasCreation; + +public class Program +{ + /// + /// The main entry point for the Lambda function. The main function is called once during the Lambda init phase. It + /// initializes the .NET Lambda runtime client passing in the function handler to invoke for each Lambda event and + /// the JSON serializer to use for converting Lambda JSON format to the .NET types. + /// + private static async Task Main() + { + Func handler = Function.FunctionHandler; + await LambdaBootstrapBuilder.Create(handler, new SourceGeneratorLambdaJsonSerializer()) + .Build() + .RunAsync(); + } +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/CustomResource/BedrockAgentAliasCreation/Serialization/LambdaFunctionJsonSerializerContext.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/CustomResource/BedrockAgentAliasCreation/Serialization/LambdaFunctionJsonSerializerContext.cs new file mode 100644 index 000000000..723b85659 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/CustomResource/BedrockAgentAliasCreation/Serialization/LambdaFunctionJsonSerializerContext.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; +using BedrockAgentAliasCreation.Models; + +namespace BedrockAgentAliasCreation.Serialization +{ + /// + /// This class is used to register the input event and return type for the FunctionHandler method with the System.Text.Json source generator. + /// There must be a JsonSerializable attribute for each type used as the input and return type or a runtime error will occur + /// from the JSON serializer unable to find the serialization information for unknown types. + /// + [JsonSerializable(typeof(object))] + [JsonSerializable(typeof(string))] + [JsonSerializable(typeof(CfnRequest))] + [JsonSerializable(typeof(CfnResponse))] + [JsonSerializable(typeof(ResourceProperties))] + public partial class LambdaFunctionJsonSerializerContext : JsonSerializerContext + { + // By using this partial class derived from JsonSerializerContext, we can generate reflection free JSON Serializer code at compile time + // which can deserialize our class and properties. However, we must attribute this class to tell it what types to generate serialization code for. + // See https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-source-generation + } +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/CustomResource/BedrockAgentAliasCreation/Utils/ResponseUtils.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/CustomResource/BedrockAgentAliasCreation/Utils/ResponseUtils.cs new file mode 100644 index 000000000..1f6dfe948 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/BedrockAgent/CustomResource/BedrockAgentAliasCreation/Utils/ResponseUtils.cs @@ -0,0 +1,28 @@ +using System.Text; +using System.Text.Json; +using Amazon.Lambda.Core; +using BedrockAgentAliasCreation.Models; +using BedrockAgentAliasCreation.Serialization; + +namespace BedrockAgentAliasCreation.Utils +{ + public class ResponseUtils + { + public static async Task UploadResponse(string url, CfnResponse cfnResponse) + { + string json = JsonSerializer.Serialize(cfnResponse, LambdaFunctionJsonSerializerContext.Default.CfnResponse); + byte[] byteArray = Encoding.UTF8.GetBytes(json); + LambdaLogger.Log($"trying to upload json {json}"); + + using HttpClient httpClient = new(); + HttpRequestMessage httpRequest = new(HttpMethod.Put, url) + { + Content = new ByteArrayContent(byteArray) + }; + httpRequest.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); + + HttpResponseMessage response = await httpClient.SendAsync(httpRequest); + LambdaLogger.Log($"Result of upload is {response.StatusCode}"); + } + } +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/KnowledgeBaseIngestion/Function.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/KnowledgeBaseIngestion/Function.cs new file mode 100644 index 000000000..313445125 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/KnowledgeBaseIngestion/Function.cs @@ -0,0 +1,214 @@ +using System.Net; +using System.Reflection; +using System.Text.Json; +using Amazon.BedrockAgent; +using Amazon.BedrockAgent.Model; +using Amazon.Lambda.Core; +using Amazon.S3; +using Amazon.S3.Model; +using KnowledgeBaseIngestion.Models; +using KnowledgeBaseIngestion.Serialization; +using KnowledgeBaseIngestion.Utils; + +namespace KnowledgeBaseIngestion; + +public class Function +{ + public static async Task FunctionHandler(object request, ILambdaContext context) + { + context.Logger.LogInformation($"Received input as {request}"); + + var cfnRequest = JsonSerializer.Deserialize(request?.ToString() ?? string.Empty, LambdaFunctionJsonSerializerContext.Default.CfnRequest) + ?? throw new Exception("Invalid request"); + + var response = new CfnResponse + { + // build all the common responses from the request + StackId = cfnRequest.StackId, + RequestId = cfnRequest.RequestId, + LogicalResourceId = cfnRequest.LogicalResourceId, + PhysicalResourceId = $"{cfnRequest.ResourceProperties.KnowledgeBaseId}-{cfnRequest.ResourceProperties.DataSourceId}", + }; + + try + { + switch (cfnRequest.RequestType.ToLowerInvariant()) + { + case "create": + context.Logger.LogInformation("Received Create request"); + + await UploadDocumentsAsync(cfnRequest.ResourceProperties, context); + await StartIngestionAsync(cfnRequest.ResourceProperties, context); + + response.Status = "SUCCESS"; + break; + + case "delete": + context.Logger.LogInformation("Received Delete request"); + response.Status = "SUCCESS"; + break; + + case "update": + context.Logger.LogInformation("Received Update request"); + response.Status = "SUCCESS"; + break; + } + + context.Logger.LogInformation($"Uploading response to {cfnRequest.ResponseURL} "); + await ResponseUtils.UploadResponse(cfnRequest.ResponseURL, response); + } + catch (Exception e) + { + context.Logger.LogError("Error occurred: " + e.Message); + response.Status = "FAILED"; + response.Reason = e.Message; + await ResponseUtils.UploadResponse(cfnRequest.ResponseURL, response); + } + + context.Logger.LogInformation("Finished"); + } + + /// + /// Uploads the documents to the S3 bucket + /// + /// Resource properties + /// Lambda Context + private static async Task UploadDocumentsAsync(ResourceProperties resourceProperties, ILambdaContext context) + { + // Get Region + var region = resourceProperties.Region?.ToString() ?? throw new Exception("Region not provided from resource properties"); + context.Logger.LogInformation($"UploadDocuments:Region: {region}"); + + // Get Bucket Name + var bucketName = resourceProperties.BucketName?.ToString() ?? throw new Exception("BucketName not provided from resource properties"); + context.Logger.LogInformation($"UploadDocuments:BucketName: {bucketName}"); + + try + { + context.Logger.LogInformation("Uploading documents to S3 bucket"); + context.Logger.LogInformation($"Current Directory: {Directory.GetCurrentDirectory()}"); + context.Logger.LogInformation($"Executing Assembly Location: {Assembly.GetExecutingAssembly().Location}"); + + // Get the list of files in the PolicyFiles folder + var files = Directory.GetFiles(Directory.GetCurrentDirectory(), "Policies*.md"); + + // Create an instance of the AmazonS3Client + using var s3Client = new AmazonS3Client(); + foreach (var file in files) + { + // Get the file name + var fileName = Path.GetFileName(file); + context.Logger.LogInformation($"Uploading file {fileName}"); + + // Create a PutObjectRequest to upload the file to the S3 bucket + var putObjectRequest = new PutObjectRequest + { + BucketName = bucketName, + Key = fileName, + FilePath = file + }; + + // Upload the file to the S3 bucket + await s3Client.PutObjectAsync(putObjectRequest); + } + + context.Logger.LogInformation("Documents uploaded to S3 bucket"); + } + catch (Exception e) + { + context.Logger.LogError($"Error uploading documents: {e.Message}{Environment.NewLine}{e.StackTrace}"); + throw; + } + } + + + /// + /// Starts the ingestion job in Knowledge Base and waits for it to complete + /// Resource properties + /// Lambda Context + private static async Task StartIngestionAsync(ResourceProperties resourceProperties, ILambdaContext context) + { + // Get Region + var region = resourceProperties.Region?.ToString() ?? throw new Exception("Region not provided from resource properties"); + context.Logger.LogInformation($"StartIngestion:Region: {region}"); + + // Get KnowledgeBaseId + var knowledgeBaseId = resourceProperties.KnowledgeBaseId?.ToString() ?? throw new Exception("KnowledgeBaseId not provided from resource properties"); + context.Logger.LogInformation($"StartIngestion:KnowledgeBaseId: {knowledgeBaseId}"); + + var dataSourceId = resourceProperties.DataSourceId?.ToString() ?? throw new Exception("DataSourceId not provided from resource properties"); + context.Logger.LogInformation($"StartIngestion:DataSourceId: {dataSourceId}"); + + try + { + // Start Bedrock Ingestion Job and then call GetIngestionJob in loop till it comoletes + context.Logger.LogInformation("Starting Bedrock Ingestion Job"); + + using var bedrockAgentClient = new AmazonBedrockAgentClient(); + var clientToken = Guid.NewGuid().ToString(); + + // Start Ingestion Job + var startIngestionJobResponse = await bedrockAgentClient.StartIngestionJobAsync( + new StartIngestionJobRequest + { + KnowledgeBaseId = knowledgeBaseId, + DataSourceId = dataSourceId, + ClientToken = clientToken + }); + + // Check for successful status code + if (startIngestionJobResponse.HttpStatusCode >= HttpStatusCode.BadRequest) + throw new Exception($"Failed to start ingestion job. Status code: {startIngestionJobResponse.HttpStatusCode}"); + else + context.Logger.LogInformation($"Ingestion Job started successfully, Id: {startIngestionJobResponse.IngestionJob.IngestionJobId}"); + + // Job + var ingestionJob = startIngestionJobResponse.IngestionJob; + var getJobFailureCount = 0; + + while (true) + { + if (ingestionJob.Status == IngestionJobStatus.COMPLETE) + { + context.Logger.LogInformation("Ingestion Job completed successfully"); + break; + } + else if (ingestionJob.Status == IngestionJobStatus.FAILED) + { + var failureReasons = string.Join(", ", ingestionJob.FailureReasons); + throw new Exception("Ingestion Job failed: " + failureReasons); + } + else + { + context.Logger.LogInformation($"Ingestion Job status: {ingestionJob.Status}"); + + // Wait for 10 seconds before checking the status again + await Task.Delay(TimeSpan.FromSeconds(10)); + } + + var getIngestionJobResponse = await bedrockAgentClient.GetIngestionJobAsync( + new GetIngestionJobRequest + { + KnowledgeBaseId = knowledgeBaseId, + DataSourceId = dataSourceId, + IngestionJobId = ingestionJob.IngestionJobId + }); + + // Check for successful status code + if (getIngestionJobResponse.HttpStatusCode >= HttpStatusCode.BadRequest) + { + getJobFailureCount++; + if (getJobFailureCount > 3) + throw new Exception($"Failed to get ingestion job. Status code: {getIngestionJobResponse.HttpStatusCode}"); + } + + ingestionJob = getIngestionJobResponse.IngestionJob; + } + } + catch (Exception e) + { + context.Logger.LogError($"Error ingesting documents: {e.Message}{Environment.NewLine}{e.StackTrace}"); + throw; + } + } +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/KnowledgeBaseIngestion/KnowledgeBaseIngestion.csproj b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/KnowledgeBaseIngestion/KnowledgeBaseIngestion.csproj new file mode 100644 index 000000000..e59386702 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/KnowledgeBaseIngestion/KnowledgeBaseIngestion.csproj @@ -0,0 +1,40 @@ + + + Exe + net8.0 + enable + enable + Lambda + true + + true + + + + true + + partial + + + + + + + + + + + + + + + + + + %(RecursiveDir)%(Filename)%(Extension) + Always + + + \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/KnowledgeBaseIngestion/Models/Models.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/KnowledgeBaseIngestion/Models/Models.cs new file mode 100644 index 000000000..76371c069 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/KnowledgeBaseIngestion/Models/Models.cs @@ -0,0 +1,53 @@ +// This file contains the models used by the Lambda function to handle the custom resource request and response. +namespace KnowledgeBaseIngestion.Models; + +public class CfnRequest +{ + public string RequestType { get; set; } = string.Empty; + + public string ResponseURL { get; set; } = string.Empty; + + public string StackId { get; set; } = string.Empty; + + public string RequestId { get; set; } = string.Empty; + + public string ResourceType { get; set; } = string.Empty; + + public string LogicalResourceId { get; set; } = string.Empty; + + public string PhysicalResourceId { get; set; } = string.Empty; + + public ResourceProperties ResourceProperties { get; set; } = new ResourceProperties(); +} + +public class CfnResponse +{ + public string Status { get; set; } = string.Empty; + + public string Reason { get; set; } = string.Empty; + + public string PhysicalResourceId { get; set; } = string.Empty; + + public string StackId { get; set; } = string.Empty; + + public string RequestId { get; set; } = string.Empty; + + public string LogicalResourceId { get; set; } = string.Empty; + + public bool NoEcho { get; set; } = false; + + public Dictionary? Data {get;set;} = null; +} + +public sealed class ResourceProperties +{ + public string ServiceToken { get; set; } = string.Empty; + + public string Region { get; set; } = string.Empty; + + public string KnowledgeBaseId { get; set; } = string.Empty; + + public string DataSourceId { get; set; } = string.Empty; + + public string BucketName { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/KnowledgeBaseIngestion/PolicyFiles/Policies_1.md b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/KnowledgeBaseIngestion/PolicyFiles/Policies_1.md new file mode 100644 index 000000000..249ce7a2b --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/KnowledgeBaseIngestion/PolicyFiles/Policies_1.md @@ -0,0 +1,123 @@ +# GlobalTrek Adventures - Company Policies + +## 1. Booking and Reservation Policy + +### 1.1 Booking Process +- Customers can make reservations through our website, mobile app, or customer service representatives. +- A valid credit card is required to secure all bookings. +- Customers must provide accurate personal information, including full name (as it appears on travel documents), contact details, and any special requirements. + +### 1.2 Confirmation and Itinerary +- A booking confirmation and itinerary will be sent via email within 24 hours of completed reservation. +- Customers are responsible for reviewing all details for accuracy and reporting any discrepancies immediately. + +### 1.3 Payment +- Full payment is due at the time of booking for all reservations made within 30 days of travel. +- For bookings made more than 30 days in advance, a 25% deposit is required, with the balance due 30 days before the travel date. +- We accept major credit cards, PayPal, and bank transfers. + +### 1.4 Price Guarantee +- If you find a lower price for the same trip and date on another website within 24 hours of booking, we will match that price and give you an additional 5% discount. + +## 2. Cancellation and Refund Policy + +### 2.1 Cancellation by Customer +- Cancellations made 30 or more days before the travel date: Full refund minus a $50 administrative fee. +- Cancellations made 15-29 days before the travel date: 50% refund. +- Cancellations made 14 days or less before the travel date: No refund. + +### 2.2 Cancellation by GlobalTrek Adventures +- In the rare event that we must cancel a trip, customers will receive a full refund or the option to rebook for a future date with a 10% discount. +- We are not responsible for any incidental expenses that customers may have incurred as a result of the booking, such as visas, vaccinations, or non-refundable flights. + +### 2.3 Changes to Bookings +- Changes made 30 or more days before the travel date: No fee. +- Changes made 15-29 days before the travel date: $50 change fee. +- Changes made 14 days or less before the travel date: $100 change fee. + +### 2.4 Refund Process +- Refunds will be processed within 10 business days of the cancellation request. +- Refunds will be issued to the original form of payment used for the booking. + +## 3. Travel Insurance Policy + +### 3.1 Insurance Recommendation +- GlobalTrek Adventures strongly recommends that all travelers purchase comprehensive travel insurance. +- We offer travel insurance through our partner, SafeJourney Insurance, which can be added during the booking process. + +### 3.2 Coverage +Our recommended insurance policy includes: +- Trip cancellation and interruption coverage +- Emergency medical expenses and evacuation +- Baggage loss and delay +- Travel delay compensation +- 24/7 worldwide assistance + +### 3.3 Pre-existing Conditions +- For coverage of pre-existing medical conditions, insurance must be purchased within 14 days of the initial trip deposit. + +### 3.4 Claims Process +- In the event of a claim, customers should contact SafeJourney Insurance directly. +- GlobalTrek Adventures will provide any necessary documentation to support the claim. + +## 4. Accommodation Policy + +### 4.1 Room Types and Allocation +- Room descriptions and photographs on our website are representative of the room type booked. +- Specific room requests (e.g., high floor, ocean view) will be noted but cannot be guaranteed. +- Room allocation is at the discretion of the hotel and based on availability at check-in. + +### 4.2 Check-in and Check-out +- Standard check-in time is 3:00 PM and check-out time is 11:00 AM, unless otherwise stated. +- Early check-in and late check-out can be requested but are subject to availability and may incur additional charges. + +### 4.3 Amenities and Services +- Amenities listed for each accommodation are based on information provided by the property. +- GlobalTrek Adventures is not responsible for any temporary or permanent reduction in services or amenities at the accommodation. + +### 4.4 Accessibility +- Customers with specific accessibility requirements should inform us at the time of booking. +- While we will make every effort to accommodate these needs, we cannot guarantee that all properties will have suitable facilities. + +## 5. Transportation Policy + +### 5.1 Air Travel +- Flights booked through GlobalTrek Adventures are subject to the airline's terms and conditions. +- Customers are responsible for adhering to airline baggage policies and weight restrictions. +- We recommend arriving at the airport at least 2 hours before domestic flights and 3 hours before international flights. + +### 5.2 Ground Transportation +- For packages including ground transportation, details will be provided in the itinerary. +- Customers are responsible for being at the designated pick-up locations at the specified times. +- Missed transfers due to customer delay are non-refundable. + +### 5.3 Cruise Travel +- Cruise bookings are subject to the cruise line's terms and conditions in addition to GlobalTrek Adventures' policies. +- Customers must complete online check-in and provide all required documentation as specified by the cruise line. + +### 5.4 Car Rentals +- Drivers must meet the minimum age requirements set by the car rental company. +- A valid driver's license and credit card in the driver's name are required at pick-up. +- We strongly recommend purchasing full insurance coverage for rental vehicles. + +## 6. Customer Data and Privacy Policy + +### 6.1 Data Collection +- We collect personal information necessary for booking and providing travel services. +- This may include names, contact details, passport information, and payment details. + +### 6.2 Data Usage +- Customer data is used solely for the purpose of fulfilling travel arrangements and improving our services. +- We do not sell or share customer data with unaffiliated third parties for marketing purposes. + +### 6.3 Data Protection +- We employ industry-standard security measures to protect customer data. +- All online transactions are encrypted using SSL technology. + +### 6.4 Customer Rights +- Customers have the right to request access to their personal data held by GlobalTrek Adventures. +- Customers may request corrections to their data or ask for their data to be deleted, subject to legal and contractual restrictions. + +### 6.5 Marketing Communications +- Customers may opt in or out of marketing communications at any time. +- Even if opted out of marketing communications, customers will still receive essential communications related to their bookings. \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/KnowledgeBaseIngestion/PolicyFiles/Policies_2.md b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/KnowledgeBaseIngestion/PolicyFiles/Policies_2.md new file mode 100644 index 000000000..d6ff45dc6 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/KnowledgeBaseIngestion/PolicyFiles/Policies_2.md @@ -0,0 +1,154 @@ +# GlobalTrek Adventures - Additional Company Policies + +## 1. Group Booking Policy + +### 1.1 Definition of Group Bookings +- A group booking is defined as 10 or more people traveling together on the same itinerary. + +### 1.2 Group Rates and Discounts +- Group discounts are available and vary based on group size, destination, and season. +- A 5% discount is applied for groups of 10-20 people. +- A 10% discount is applied for groups of 21 or more people. +- Additional perks, such as complimentary upgrades or excursions, may be offered for larger groups. + +### 1.3 Payment Terms for Groups +- A non-refundable deposit of 20% is required at the time of booking. +- Full payment is due 60 days prior to the travel date. +- For bookings made within 60 days of travel, full payment is due at the time of booking. + +### 1.4 Group Cancellation Policy +- Cancellations made 90 or more days before travel: Full refund minus the deposit. +- Cancellations made 60-89 days before travel: 50% refund. +- Cancellations made 59 days or less before travel: No refund. + +### 1.5 Group Leader Benefits +- One free spot is offered for every 20 paying travelers. +- The group leader receives additional perks such as priority check-in and a welcome package. + +## 2. Loyalty Program Policy + +### 2.1 Membership Tiers +- Bronze: 0-4,999 points +- Silver: 5,000-14,999 points +- Gold: 15,000-29,999 points +- Platinum: 30,000+ points + +### 2.2 Earning Points +- 1 point is earned for every $1 spent on bookings. +- Bonus points are awarded for booking during off-peak seasons or for certain promotions. +- Points are credited to accounts within 14 days of completed travel. + +### 2.3 Redeeming Points +- Points can be redeemed for discounts on future bookings, upgrades, or exclusive experiences. +- 1,000 points = $10 discount +- Point redemption must be requested at the time of booking. + +### 2.4 Tier Benefits +- Bronze: 5% discount on travel insurance, priority customer service. +- Silver: All Bronze benefits plus free airport lounge access once per year, 10% discount on add-on activities. +- Gold: All Silver benefits plus one free hotel room upgrade per year, 15% discount on add-on activities. +- Platinum: All Gold benefits plus dedicated concierge service, one free flight upgrade per year, 20% discount on add-on activities. + +### 2.5 Points Expiration +- Points expire after 24 months of account inactivity. +- Members will be notified 90 days before points are set to expire. + +## 3. Sustainable Travel Policy + +### 3.1 Environmental Commitment +- GlobalTrek Adventures is committed to reducing its environmental impact and promoting sustainable travel practices. + +### 3.2 Carbon Offset Program +- Customers have the option to offset the carbon emissions of their trips. +- The cost of carbon offsetting is calculated based on the trip's duration and type of transportation. +- 100% of carbon offset contributions go towards certified environmental projects. + +### 3.3 Sustainable Accommodations +- We prioritize partnerships with eco-friendly and sustainably operated accommodations. +- Properties are evaluated based on their environmental policies, energy efficiency, waste management, and community engagement. + +### 3.4 Responsible Tourism Practices +- We provide guidelines to travelers on responsible tourism practices, including respecting local cultures, minimizing waste, and supporting local economies. +- Our guided tours incorporate educational components on local environmental and cultural conservation efforts. + +### 3.5 Plastic Reduction Initiative +- We encourage travelers to bring reusable water bottles and provide information on safe drinking water sources at destinations. +- Our welcome packages use minimal, recyclable packaging. + +## 4. Special Needs and Accessibility Policy + +### 4.1 Commitment to Inclusivity +- GlobalTrek Adventures is committed to making travel accessible to all individuals, regardless of physical limitations or special needs. + +### 4.2 Special Assistance Requests +- Customers should inform us of any special needs or requirements at the time of booking. +- We will make every effort to accommodate special requests, subject to availability and local regulations. + +### 4.3 Accessible Room Guarantees +- When available and requested, accessible rooms will be guaranteed and confirmed at the time of booking. + +### 4.4 Mobility Equipment +- Information on renting mobility equipment at destinations can be provided upon request. +- We can assist in arranging transportation suitable for travelers with mobility issues. + +### 4.5 Dietary Requirements +- Special dietary requirements should be communicated at the time of booking. +- We will inform all relevant service providers (hotels, airlines, tour operators) of these requirements. + +### 4.6 Service Animals +- Service animals are welcome in accordance with local regulations. +- Customers traveling with service animals must provide appropriate documentation and notify us in advance. + +## 5. Emergency Procedures and Travel Alerts + +### 5.1 24/7 Emergency Support +- GlobalTrek Adventures provides 24/7 emergency support for customers during their travels. +- Emergency contact numbers are provided in all travel documents and on our mobile app. + +### 5.2 Natural Disasters and Political Unrest +- In case of natural disasters or political unrest at a destination, we will: + 1. Immediately contact all affected customers. + 2. Assist with evacuation or change of travel plans if necessary. + 3. Provide regular updates through email, SMS, and our mobile app. + +### 5.3 Medical Emergencies +- In case of medical emergencies, our support team will: + 1. Assist in locating appropriate medical facilities. + 2. Coordinate with travel insurance providers. + 3. Help communicate with family members if requested. + +### 5.4 Travel Alerts and Advisories +- We continuously monitor travel advisories issued by government agencies. +- Customers will be notified of any significant changes or risks related to their booked destinations. +- Options for itinerary changes or cancellations will be provided if official travel warnings are issued for booked destinations. + +### 5.5 Lost Documents Assistance +- We provide assistance in case of lost or stolen travel documents, including: + 1. Guidance on obtaining emergency replacements. + 2. Coordination with local authorities and embassies. + 3. Assistance with alternate travel arrangements if necessary. + +## 6. Feedback and Dispute Resolution Policy + +### 6.1 Customer Feedback +- We encourage customers to provide feedback on their travel experiences. +- Feedback can be submitted through our website, mobile app, or by contacting customer service. +- All feedback is reviewed by our quality assurance team. + +### 6.2 Complaint Procedure +1. Customers should report any issues immediately to our local representatives or 24/7 support line during travel. +2. If the issue is not resolved satisfactorily, a formal complaint can be submitted in writing within 28 days of the end of the trip. +3. We will acknowledge receipt of the complaint within 7 business days. +4. A full response will be provided within 28 days of receiving the complaint. + +### 6.3 Compensation +- Compensation, if warranted, will be assessed on a case-by-case basis. +- Compensation may be offered in the form of refunds, credits for future travel, or other appropriate remedies. + +### 6.4 Dispute Resolution +- We strive to resolve all disputes amicably and fairly. +- If a resolution cannot be reached, we offer mediation through an independent third-party service. + +### 6.5 Continuous Improvement +- All feedback and complaints are used to improve our services and prevent future issues. +- We regularly review and update our policies and procedures based on customer feedback. \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/KnowledgeBaseIngestion/PolicyFiles/Policies_3.md b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/KnowledgeBaseIngestion/PolicyFiles/Policies_3.md new file mode 100644 index 000000000..2157cbd86 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/KnowledgeBaseIngestion/PolicyFiles/Policies_3.md @@ -0,0 +1,140 @@ +# GlobalTrek Adventures - Supplementary Company Policies + +## 1. Baggage Policy + +### 1.1 Checked Baggage +- Baggage allowances vary by transportation type and destination. Specific allowances will be provided in booking confirmations. +- For air travel, we adhere to the baggage policies of the operating airline. +- For bus tours, typically one large suitcase (max 23kg/50lbs) per person is allowed. +- For cruise travel, policies vary by cruise line and will be detailed in cruise documentation. + +### 1.2 Carry-on Baggage +- For air travel, carry-on allowances follow the operating airline's policies. +- For bus tours, one small carry-on bag per person is allowed on board. +- For day trips, a small daypack or handbag is recommended. + +### 1.3 Excess and Oversized Baggage +- Charges for excess or oversized baggage are the responsibility of the traveler. +- Arrangements for excess baggage must be made in advance and are subject to space availability. + +### 1.4 Lost or Damaged Baggage +- Report any lost or damaged baggage immediately to the relevant transportation provider. +- While GlobalTrek Adventures will assist with tracking and claims, ultimate responsibility lies with the transportation provider. +- We strongly recommend travel insurance that includes baggage coverage. + +### 1.5 Storage of Baggage +- Where baggage storage is offered at accommodations, use is at the traveler's own risk. +- We recommend using all available security measures, such as locks provided by the accommodation. + +## 2. Travel Documentation Policy + +### 2.1 Passport Requirements +- A valid passport is required for all international travel. +- Passports must be valid for at least six months beyond the return date of the trip. +- A copy of the passport information page must be provided to GlobalTrek Adventures at the time of booking. + +### 2.2 Visa Requirements +- Travelers are responsible for obtaining all necessary visas and travel permits. +- GlobalTrek Adventures will provide general visa information, but we recommend contacting the relevant embassies or consulates for the most up-to-date requirements. +- Visa fees are not included in the tour price unless explicitly stated. + +### 2.3 Travel Authorization Systems +- Some countries require electronic travel authorizations (e.g., ESTA for the USA, eTA for Canada). +- Travelers are responsible for obtaining these authorizations before travel. + +### 2.4 Other Required Documents +- Certain activities or locations may require additional documentation (e.g., permits for specific hikes, diving certifications). +- We will inform travelers of any such requirements, but obtaining these documents is the traveler's responsibility. + +### 2.5 Document Checks +- GlobalTrek Adventures reserves the right to check travel documents at any point during the booking process or trip. +- Travelers may be denied participation in travel or certain activities if proper documentation is not presented. + +## 3. Health and Vaccination Policy + +### 3.1 General Health Requirements +- Travelers are responsible for ensuring they are physically capable of undertaking the chosen trip. +- Any pre-existing medical conditions must be disclosed at the time of booking. + +### 3.2 Vaccinations +- Travelers are responsible for obtaining any vaccinations required or recommended for their destination(s). +- We recommend consulting with a travel health professional or visiting a travel clinic at least 6-8 weeks before departure. + +### 3.3 Health Precautions +- GlobalTrek Adventures will provide general health and safety information for destinations. +- Travelers should follow any health and safety guidelines provided by tour leaders or local authorities. + +### 3.4 Medications +- Travelers should bring an adequate supply of any required medications, along with copies of prescriptions. +- It is the traveler's responsibility to ensure that any medications they carry are legal in the countries being visited. + +### 3.5 Travel Insurance +- Adequate travel insurance, including coverage for medical expenses and emergency evacuation, is strongly recommended for all trips and is mandatory for certain tours. + +## 4. Local Laws and Customs Policy + +### 4.1 Compliance with Local Laws +- Travelers must comply with all local laws and regulations of the countries visited. +- GlobalTrek Adventures will not be held responsible for any illegal activities undertaken by travelers. + +### 4.2 Respect for Local Customs +- Travelers are expected to respect local customs, traditions, and etiquette. +- This includes appropriate dress at religious sites, observing local behavioral norms, and being mindful of photography rules. + +### 4.3 Prohibited Items +- Travelers are responsible for ensuring they do not carry any items prohibited by local laws or customs regulations. +- This includes but is not limited to drugs, weapons, and certain food items. + +### 4.4 Cultural Sensitivity +- Our tours often include interactions with local communities. Travelers are expected to engage respectfully and follow any guidelines provided by tour leaders. + +### 4.5 Environmental Regulations +- Travelers must adhere to all local environmental regulations, including proper waste disposal and respecting protected areas. + +## 5. Photography and Social Media Policy + +### 5.1 Personal Photography +- Travelers are generally free to take photographs during their trip for personal use. +- However, photography may be restricted in certain locations (e.g., museums, religious sites). These restrictions must be strictly observed. + +### 5.2 Drone Usage +- The use of drones is prohibited on all GlobalTrek Adventures tours unless explicit permission has been granted in writing. +- Where drone use is permitted, operators must comply with all local regulations. + +### 5.3 Photography of People +- Always ask permission before photographing individuals, especially in culturally sensitive areas. +- Be aware that in some cultures, photography of certain people or places may be taboo. + +### 5.4 Commercial Photography +- Any photography or videography intended for commercial use must be declared and approved by GlobalTrek Adventures in advance. + +### 5.5 Social Media Sharing +- We encourage sharing your travel experiences on social media. +- When posting, please be respectful of local cultures and fellow travelers' privacy. +- We recommend using our company hashtag #GlobalTrekAdventures for shared content. + +### 5.6 Content Usage Rights +- By sharing content with our hashtags or tagging our company, you grant GlobalTrek Adventures permission to repost or use your content for marketing purposes. +- Full credit will always be given to the original creator. + +## 6. Communication and Connectivity Policy + +### 6.1 Emergency Contact Information +- Travelers must provide emergency contact information at the time of booking. +- This information should be kept up to date and any changes communicated to GlobalTrek Adventures. + +### 6.2 Communication During Tours +- Tour leaders will advise on the best methods of communication in each location. +- In remote areas, communication may be limited. This will be clearly communicated in trip details. + +### 6.3 Wi-Fi and Internet Access +- Where advertised, Wi-Fi is provided as a courtesy and its quality and availability cannot be guaranteed. +- In some remote locations, internet access may be limited or unavailable. + +### 6.4 Mobile Phones +- Travelers are responsible for ensuring their mobile devices will work in the countries visited. +- Information on local SIM cards or mobile data packages can be provided upon request. + +### 6.5 Staying Connected +- A list of accommodations with contact information will be provided before departure. +- For safety reasons, we recommend informing someone at home of your general whereabouts during the trip. \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/KnowledgeBaseIngestion/Program.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/KnowledgeBaseIngestion/Program.cs new file mode 100644 index 000000000..804f9d473 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/KnowledgeBaseIngestion/Program.cs @@ -0,0 +1,22 @@ +using Amazon.Lambda.Core; +using Amazon.Lambda.RuntimeSupport; +using Amazon.Lambda.Serialization.SystemTextJson; +using KnowledgeBaseIngestion.Serialization; + +namespace KnowledgeBaseIngestion; + +public class Program +{ + /// + /// The main entry point for the Lambda function. The main function is called once during the Lambda init phase. It + /// initializes the .NET Lambda runtime client passing in the function handler to invoke for each Lambda event and + /// the JSON serializer to use for converting Lambda JSON format to the .NET types. + /// + private static async Task Main() + { + Func handler = Function.FunctionHandler; + await LambdaBootstrapBuilder.Create(handler, new SourceGeneratorLambdaJsonSerializer()) + .Build() + .RunAsync(); + } +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/KnowledgeBaseIngestion/Serialization/LambdaFunctionJsonSerializerContext.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/KnowledgeBaseIngestion/Serialization/LambdaFunctionJsonSerializerContext.cs new file mode 100644 index 000000000..b245625a7 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/KnowledgeBaseIngestion/Serialization/LambdaFunctionJsonSerializerContext.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; +using KnowledgeBaseIngestion.Models; + +namespace KnowledgeBaseIngestion.Serialization +{ + /// + /// This class is used to register the input event and return type for the FunctionHandler method with the System.Text.Json source generator. + /// There must be a JsonSerializable attribute for each type used as the input and return type or a runtime error will occur + /// from the JSON serializer unable to find the serialization information for unknown types. + /// + [JsonSerializable(typeof(object))] + [JsonSerializable(typeof(string))] + [JsonSerializable(typeof(CfnRequest))] + [JsonSerializable(typeof(CfnResponse))] + [JsonSerializable(typeof(ResourceProperties))] + public partial class LambdaFunctionJsonSerializerContext : JsonSerializerContext + { + // By using this partial class derived from JsonSerializerContext, we can generate reflection free JSON Serializer code at compile time + // which can deserialize our class and properties. However, we must attribute this class to tell it what types to generate serialization code for. + // See https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-source-generation + } +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/KnowledgeBaseIngestion/Utils/ResponseUtils.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/KnowledgeBaseIngestion/Utils/ResponseUtils.cs new file mode 100644 index 000000000..c10b4b1fd --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/KnowledgeBaseIngestion/Utils/ResponseUtils.cs @@ -0,0 +1,28 @@ +using System.Text; +using System.Text.Json; +using Amazon.Lambda.Core; +using KnowledgeBaseIngestion.Models; +using KnowledgeBaseIngestion.Serialization; + +namespace KnowledgeBaseIngestion.Utils +{ + public class ResponseUtils + { + public static async Task UploadResponse(string url, CfnResponse cfnResponse) + { + string json = JsonSerializer.Serialize(cfnResponse, LambdaFunctionJsonSerializerContext.Default.CfnResponse); + byte[] byteArray = Encoding.UTF8.GetBytes(json); + LambdaLogger.Log($"trying to upload json {json}"); + + using HttpClient httpClient = new(); + HttpRequestMessage httpRequest = new(HttpMethod.Put, url) + { + Content = new ByteArrayContent(byteArray) + }; + httpRequest.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); + + HttpResponseMessage response = await httpClient.SendAsync(httpRequest); + LambdaLogger.Log($"Result of upload is {response.StatusCode}"); + } + } +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/OssIndexCreation/Function.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/OssIndexCreation/Function.cs new file mode 100644 index 000000000..01f98b401 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/OssIndexCreation/Function.cs @@ -0,0 +1,157 @@ +using System.Text.Json; +using Amazon; +using Amazon.Lambda.Core; +using OpenSearch.Client; +using OpenSearch.Net.Auth.AwsSigV4; +using OssIndexCreation.Models; +using OssIndexCreation.Serialization; +using OssIndexCreation.Utils; + +namespace OssIndexCreation; + +public class Function +{ + public static async Task FunctionHandler(object request, ILambdaContext context) + { + context.Logger.LogInformation($"Received input as {request}"); + + var cfnRequest = JsonSerializer.Deserialize(request?.ToString() ?? string.Empty, LambdaFunctionJsonSerializerContext.Default.CfnRequest) + ?? throw new Exception("Invalid request"); + + var response = new CfnResponse + { + // build all the common responses from the request + StackId = cfnRequest.StackId, + RequestId = cfnRequest.RequestId, + LogicalResourceId = cfnRequest.LogicalResourceId, + PhysicalResourceId = cfnRequest.PhysicalResourceId + }; + + try + { + switch (cfnRequest.RequestType.ToLowerInvariant()) + { + case "create": + context.Logger.LogInformation("Received Create request"); + response.PhysicalResourceId = await CreateIndex(cfnRequest.ResourceProperties, context); + response.Status = "SUCCESS"; + break; + + case "delete": + context.Logger.LogInformation("Received Delete request"); + response.Status = "SUCCESS"; + break; + + case "update": + context.Logger.LogInformation("Received Update request"); + response.Status = "SUCCESS"; + break; + } + + context.Logger.LogInformation($"Uploading response to {cfnRequest.ResponseURL} "); + await ResponseUtils.UploadResponse(cfnRequest.ResponseURL, response); + } + catch (Exception e) + { + context.Logger.LogError("Error occurred: " + e.Message); + response.PhysicalResourceId = cfnRequest.PhysicalResourceId ?? cfnRequest.ResourceProperties.AOSSIndexName.ToString(); + response.Status = "FAILED"; + response.Reason = e.Message; + await ResponseUtils.UploadResponse(cfnRequest.ResponseURL, response); + } + + context.Logger.LogInformation("Finished"); + } + + private static async Task CreateIndex(ResourceProperties resourceProperties, ILambdaContext context) + { + // Get Region + var region = resourceProperties.Region?.ToString() ?? throw new Exception("Region not provided from resource properties"); + context.Logger.LogInformation($"CreteIndex:Region: {region}"); + + // Get AOSS Host Name + var aossHost = resourceProperties.AOSSHost?.ToString() ?? throw new Exception("AOSSHost not provided from resource properties"); + context.Logger.LogInformation($"CreteIndex:AOSSHost: {aossHost}"); + + // Get AOSS Index Name + var aossIndexName = resourceProperties.AOSSIndexName?.ToString() ?? throw new Exception("AOSSIndexName not provided from resource properties"); + context.Logger.LogInformation($"CreteIndex:AOSSIndexName: {aossIndexName}"); + + // Get metadata field name + var metadataFiledName = resourceProperties.AOSSMetadataFieldName?.ToString() ?? throw new Exception("MetadataFiledName not provided from resource properties"); + context.Logger.LogInformation($"CreteIndex:MetadataFiledName: {metadataFiledName}"); + + // Get Text Filed Name + var textFiledName = resourceProperties.AOSSTextFieldName?.ToString() ?? throw new Exception("TextFiledName not provided from resource properties"); + context.Logger.LogInformation($"CreteIndex:TextFiledName: {textFiledName}"); + + // Get Vector Filed Name + var vectorFiledName = resourceProperties.AOSSVectorFieldName?.ToString() ?? throw new Exception("VectorFiledName not provided from resource properties"); + context.Logger.LogInformation($"CreteIndex:VectorFiledName: {vectorFiledName}"); + + try + { + // OpenSearch client + context.Logger.LogInformation($"Creating index {aossIndexName} in {region} region..."); + + var endpoint = new Uri($"{aossHost}"); + var connection = new AwsSigV4HttpConnection(RegionEndpoint.GetBySystemName(region), service: AwsSigV4HttpConnection.OpenSearchServerlessService); + var config = new ConnectionSettings(endpoint, connection); + var client = new OpenSearchClient(config); + + var response = await client.Indices.CreateAsync( + aossIndexName, + cid => + cid.Settings(s => s + .Setting("index.knn", true) + .Setting("index.number_of_shards", 2) + .Setting("index.number_of_replicas", 0) + ) + .Map(m => m + .Properties(p => p + .Text(t => t.Name(metadataFiledName).Index(false)) + .Text(t => t.Name(textFiledName)) + .KnnVector(k => k.Name(vectorFiledName) + .Dimension(1024) + .Method(md => md.Name("hnsw") + .Engine("faiss") + .SpaceType("l2") + .Parameters(p => p)) + ) + ) + .DynamicTemplates(dt => dt + .DynamicTemplate("strings", d => d + .MatchMappingType("string") + .Mapping(m => m + .Text(t => t + .Fields(f => f + .Keyword(k => k + .IgnoreAbove(2147483647) + .Name("keyword") + ) + ) + ) + ) + ) + ) + ) + ); + + if (!response.IsValid) + throw new Exception($"Error creating index: {response.ServerError}"); + + context.Logger.LogInformation($"Index {aossIndexName} created successfully"); + + // Wait, so that the index is available for search + context.Logger.LogInformation("Waiting for 60 seconds for the index to be available for search..."); + await Task.Delay(TimeSpan.FromSeconds(60)); + + return aossIndexName; + } + catch (Exception e) + { + context.Logger.LogError($"Error creating index: {e.Message}{Environment.NewLine}{e.StackTrace}"); + throw; + } + } +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/OssIndexCreation/Models/Models.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/OssIndexCreation/Models/Models.cs new file mode 100644 index 000000000..edc191d3d --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/OssIndexCreation/Models/Models.cs @@ -0,0 +1,57 @@ +// This file contains the models used by the Lambda function to handle the custom resource request and response. +namespace OssIndexCreation.Models; + +public class CfnRequest +{ + public string RequestType { get; set; } = string.Empty; + + public string ResponseURL { get; set; } = string.Empty; + + public string StackId { get; set; } = string.Empty; + + public string RequestId { get; set; } = string.Empty; + + public string ResourceType { get; set; } = string.Empty; + + public string LogicalResourceId { get; set; } = string.Empty; + + public string PhysicalResourceId { get; set; } = string.Empty; + + public ResourceProperties ResourceProperties { get; set; } = new ResourceProperties(); +} + +public class CfnResponse +{ + public string Status { get; set; } = string.Empty; + + public string Reason { get; set; } = string.Empty; + + public string PhysicalResourceId { get; set; } = string.Empty; + + public string StackId { get; set; } = string.Empty; + + public string RequestId { get; set; } = string.Empty; + + public string LogicalResourceId { get; set; } = string.Empty; + + public bool NoEcho { get; set; } = false; + + public Dictionary? Data {get;set;} = null; +} + +public sealed class ResourceProperties +{ + public string ServiceToken { get; set; } = string.Empty; + + public string Region { get; set; } = string.Empty; + + public string AOSSIndexName { get; set; } = string.Empty; + + public string AOSSHost { get; set; } = string.Empty; + + public string AOSSMetadataFieldName { get; set; } = string.Empty; + + public string AOSSTextFieldName { get; set; } = string.Empty; + + public string AOSSVectorFieldName { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/OssIndexCreation/OssIndexCreation.csproj b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/OssIndexCreation/OssIndexCreation.csproj new file mode 100644 index 000000000..84f873e10 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/OssIndexCreation/OssIndexCreation.csproj @@ -0,0 +1,34 @@ + + + Exe + net8.0 + enable + enable + Lambda + true + + true + + + + true + + partial + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/OssIndexCreation/Program.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/OssIndexCreation/Program.cs new file mode 100644 index 000000000..8bcf82f07 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/OssIndexCreation/Program.cs @@ -0,0 +1,22 @@ +using Amazon.Lambda.Core; +using Amazon.Lambda.RuntimeSupport; +using Amazon.Lambda.Serialization.SystemTextJson; +using OssIndexCreation.Serialization; + +namespace OssIndexCreation; + +public class Program +{ + /// + /// The main entry point for the Lambda function. The main function is called once during the Lambda init phase. It + /// initializes the .NET Lambda runtime client passing in the function handler to invoke for each Lambda event and + /// the JSON serializer to use for converting Lambda JSON format to the .NET types. + /// + private static async Task Main() + { + Func handler = Function.FunctionHandler; + await LambdaBootstrapBuilder.Create(handler, new SourceGeneratorLambdaJsonSerializer()) + .Build() + .RunAsync(); + } +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/OssIndexCreation/Serialization/LambdaFunctionJsonSerializerContext.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/OssIndexCreation/Serialization/LambdaFunctionJsonSerializerContext.cs new file mode 100644 index 000000000..493ec7c62 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/OssIndexCreation/Serialization/LambdaFunctionJsonSerializerContext.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; +using OssIndexCreation.Models; + +namespace OssIndexCreation.Serialization +{ + /// + /// This class is used to register the input event and return type for the FunctionHandler method with the System.Text.Json source generator. + /// There must be a JsonSerializable attribute for each type used as the input and return type or a runtime error will occur + /// from the JSON serializer unable to find the serialization information for unknown types. + /// + [JsonSerializable(typeof(object))] + [JsonSerializable(typeof(string))] + [JsonSerializable(typeof(CfnRequest))] + [JsonSerializable(typeof(CfnResponse))] + [JsonSerializable(typeof(ResourceProperties))] + public partial class LambdaFunctionJsonSerializerContext : JsonSerializerContext + { + // By using this partial class derived from JsonSerializerContext, we can generate reflection free JSON Serializer code at compile time + // which can deserialize our class and properties. However, we must attribute this class to tell it what types to generate serialization code for. + // See https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-source-generation + } +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/OssIndexCreation/Utils/ResponseUtils.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/OssIndexCreation/Utils/ResponseUtils.cs new file mode 100644 index 000000000..3f8ea2114 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/LambdaFunctions/KnowledgeBase/CustomResource/OssIndexCreation/Utils/ResponseUtils.cs @@ -0,0 +1,28 @@ +using System.Text; +using System.Text.Json; +using Amazon.Lambda.Core; +using OssIndexCreation.Models; +using OssIndexCreation.Serialization; + +namespace OssIndexCreation.Utils +{ + public class ResponseUtils + { + public static async Task UploadResponse(string url, CfnResponse cfnResponse) + { + string json = JsonSerializer.Serialize(cfnResponse, LambdaFunctionJsonSerializerContext.Default.CfnResponse); + byte[] byteArray = Encoding.UTF8.GetBytes(json); + LambdaLogger.Log($"trying to upload json {json}"); + + using HttpClient httpClient = new(); + HttpRequestMessage httpRequest = new(HttpMethod.Put, url) + { + Content = new ByteArrayContent(byteArray) + }; + httpRequest.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); + + HttpResponseMessage response = await httpClient.SendAsync(httpRequest); + LambdaLogger.Log($"Result of upload is {response.StatusCode}"); + } + } +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/Test/ChatBotClient.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/Test/ChatBotClient.cs new file mode 100644 index 000000000..0e1e1d7c7 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/Test/ChatBotClient.cs @@ -0,0 +1,217 @@ +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using TestApp.Model; + +namespace TestApp; + +/// +/// ChatBot Client +/// +internal sealed class ChatBotClient : IDisposable +{ + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + private readonly HttpClient httpClient; + private readonly bool _enableTrace; + private readonly string _albMessageEndpointUrl; + private static readonly JsonSerializerOptions _jsonSerializerOptions = + new() + { + WriteIndented = true, + ReferenceHandler = ReferenceHandler.IgnoreCycles, + PropertyNameCaseInsensitive = true + }; + + /// + /// Initializes a new instance of + /// + /// Configuration + /// Logger + public ChatBotClient(IConfiguration configuration, ILogger logger) + { + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + httpClient = new(); + + // ALB Name + var albDnsName = _configuration["ALB_DNS_NAME"] ?? string.Empty; + if (string.IsNullOrEmpty(albDnsName)) + throw new Exception("ALB_DNS_NAME is required. Please set it in appsettings.json file."); + + // Port + var portStr = _configuration["PORT"]; + if (portStr == null || !int.TryParse(portStr, out var port)) + port = 80; + + // ALB URL; + _albMessageEndpointUrl = $"http://{albDnsName}:{port}/message"; + + // Enable Trace + var enableTraceStr = _configuration["enableTrace"]; + if (enableTraceStr == null || !bool.TryParse(enableTraceStr, out _enableTrace)) + throw new Exception("A valid value (True/False) must be defined for 'enableTrace' in appsettings.json file."); + } + + /// + /// Runs ChatBotClient asynchronously + /// + /// Cancellation token + public async Task RunAsync(CancellationToken cancellationToken) + { + // Ids + var sessionId = Guid.NewGuid().ToString(); + + // Iteration + var iteration = 0; + + // Response from BedrockAgent + BedrockAgentResponse? response = null; + + while (!cancellationToken.IsCancellationRequested) + { + iteration++; + string? input = null; + Console.WriteLine("Please enter your input: "); + try + { + input = await ReadLineAsync(cancellationToken); + } + catch(OperationCanceledException) + { + break; //break from main loop + } + + // No input + if (string.IsNullOrEmpty(input)) + continue; + + // Exit + if (input.Equals("exit", StringComparison.InvariantCultureIgnoreCase)) + break; //break from main loop + + // Try in loop for retry + while (!cancellationToken.IsCancellationRequested) + { + try + { + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + httpClient.DefaultRequestHeaders.Add("User-Agent", "BedrockAgentClient"); + + var responseMessage = await httpClient.PostAsJsonAsync( + _albMessageEndpointUrl, + new BedrockAgentRequest + { + Message = input, + SessionId = response?.SessionId ?? sessionId, + MemoryId = response?.MemoryId, + EndSession = false, + EnableTrace = _enableTrace, + SessionAttributes = [], + PromptSessionAttributes = [], + InvocationId = response?.ReturnControlPayload?.InvocationId, + ReturnControlInvocationResults = null, // can be updated based on previous response + }, + cancellationToken: cancellationToken); + + // Success + responseMessage.EnsureSuccessStatusCode(); + + // Response + response = await responseMessage.Content.ReadFromJsonAsync(_jsonSerializerOptions, cancellationToken); + + // Error + if (response?.HasError ?? false) + _logger.LogWarning("Error received from BedrockAgent: {error}", response?.Error); + // Message + else + { + Console.WriteLine($"Response: {response?.Message}"); + + // Write Trace + if (_enableTrace) + await WriteTraceAsync(iteration, sessionId, input, response?.Message, response?.Trace); + } + + break; //break from retry loop + } + catch (Exception ex) + { + _logger.LogError("Error sending request to BedrockAgent: {error}", ex.Message); + + ConsoleKey responseKey; + do + { + Console.WriteLine("Do you want to retry the operation (y/N): "); + responseKey = Console.ReadKey(false).Key; + if (responseKey != ConsoleKey.Enter) + Console.WriteLine(); + } while (responseKey != ConsoleKey.Y && responseKey != ConsoleKey.N); + + if (responseKey == ConsoleKey.N) + break; //break from retry loop + } + } + } + } + + /// + /// Reads a line from console asynchronously + /// + /// Cancellation token + /// String from console + private static async Task ReadLineAsync(CancellationToken cancellationToken = default) + { + var readTask = Task.Run(Console.ReadLine); + await Task.WhenAny(readTask, Task.Delay(-1, cancellationToken)); + + cancellationToken.ThrowIfCancellationRequested(); + + string? result = readTask.Result; + return result; + } + + /// + /// Writes trace to file asynchronously + /// + /// Iteration count + /// Session Id + /// user input + /// Agent output + /// Trace + /// A + private static async Task WriteTraceAsync( + int iteration, + string sessionId, + string input, + string? output, + BedrockAgentTrace? trace) + { + if (trace == null) + return; + + var fileName = $"trace_{sessionId}_{iteration}.json"; + await File.WriteAllTextAsync( + fileName, + JsonSerializer.Serialize( + new + { + Input = input, + Output = output, + Trace = trace + }, + _jsonSerializerOptions)); + } + + /// + /// + /// + public void Dispose() + { + httpClient.Dispose(); + } +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/Test/ChatBotClientWorker.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/Test/ChatBotClientWorker.cs new file mode 100644 index 000000000..0fac4a83e --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/Test/ChatBotClientWorker.cs @@ -0,0 +1,68 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace TestApp; + +/// +/// Worker Class to run +/// +/// +/// Initializes a new instance of +/// +/// ChatBot client () +/// Logger +internal class ChatBotClientWorker( + ChatBotClient chatBotClient, + IHostApplicationLifetime hostApplicationLifetime, + ILogger logger) + : IHostedService +{ + private readonly ChatBotClient _chatBotClient = chatBotClient ?? throw new ArgumentNullException(nameof(chatBotClient)); + private readonly IHostApplicationLifetime _hostApplicationLifetime = hostApplicationLifetime ?? throw new ArgumentNullException(nameof(hostApplicationLifetime)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private CancellationTokenSource? _cancellationTokenSource; + + /// + /// Triggered when the application host is ready to start the service, runs + /// + /// Indicates that the start process has been aborted. + /// A that represents the asynchronous Start operation. + public Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); + + _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + //return _chatBotClient.RunAsync(_cancellationTokenSource.Token); + return RunAndMonitorChatBotClientAsync(_cancellationTokenSource.Token); + } + + /// + /// Triggered when the application host is performing a graceful shutdown, stops + /// + /// Indicates that the shutdown process should no longer be graceful. + /// A that represents the asynchronous Stop operation. + public Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Worker stopping at: {time}", DateTimeOffset.Now); + + _cancellationTokenSource?.Cancel(); + return Task.CompletedTask; + } + + /// + /// Runs and monitors asynchronously. Exits the application if the client exits. + /// + /// Cancellation token + /// A that runs and monitors + private async Task RunAndMonitorChatBotClientAsync(CancellationToken cancellationToken) + { + await _chatBotClient.RunAsync(cancellationToken); + + // Task has completed, exit the application + if (!cancellationToken.IsCancellationRequested) + { + _logger.LogInformation("ChatBotClient has exited. Exiting the application..."); + _hostApplicationLifetime.StopApplication(); + } + } +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/Test/Model/BedrockAgentOutputFile.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/Test/Model/BedrockAgentOutputFile.cs new file mode 100644 index 000000000..b684a38c2 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/Test/Model/BedrockAgentOutputFile.cs @@ -0,0 +1,10 @@ +namespace TestApp.Model; + +public class BedrockAgentOutputFile +{ + public string? Data { get; set; } + + public string? Type { get; set; } + + public string? Name { get; set; } +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/Test/Model/BedrockAgentRequest.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/Test/Model/BedrockAgentRequest.cs new file mode 100644 index 000000000..fb15bb5df --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/Test/Model/BedrockAgentRequest.cs @@ -0,0 +1,24 @@ +using Amazon.BedrockAgentRuntime.Model; + +namespace TestApp.Model; + +public sealed class BedrockAgentRequest +{ + public required string SessionId { get; set; } + + public string? MemoryId { get; set; } + + public string? InvocationId { get; set; } + + public required string Message { get; set; } + + public bool EndSession { get; set; } + + public bool EnableTrace { get; set; } + + public Dictionary? SessionAttributes { get; set; } + + public Dictionary? PromptSessionAttributes { get; set; } + + public List? ReturnControlInvocationResults { get; set; } +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/Test/Model/BedrockAgentResponse.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/Test/Model/BedrockAgentResponse.cs new file mode 100644 index 000000000..5588b15c6 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/Test/Model/BedrockAgentResponse.cs @@ -0,0 +1,22 @@ +using Amazon.BedrockAgentRuntime.Model; + +namespace TestApp.Model; + +public class BedrockAgentResponse +{ + public required string SessionId { get; set; } + + public required string MemoryId { get; set; } + + public string? Message { get; set; } + + public List? Files { get; set; } + + public ReturnControlPayload? ReturnControlPayload { get; set; } + + public BedrockAgentTrace? Trace { get; set; } + + public string? Error { get; set; } + + public bool HasError => !string.IsNullOrEmpty(Error); +} diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/Test/Model/BedrockAgentTrace.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/Test/Model/BedrockAgentTrace.cs new file mode 100644 index 000000000..887177679 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/Test/Model/BedrockAgentTrace.cs @@ -0,0 +1,16 @@ +using Amazon.BedrockAgentRuntime.Model; + +namespace TestApp.Model; + +public class BedrockAgentTrace +{ + public List FailureTraces { get; set; } = []; + + public List GuardrailTraces { get; set; } = []; + + public List OrchestrationTraces { get; set; } = []; + + public List PostProcessingTraces { get; set; } = []; + + public List PreProcessingTraces { get; set; } = []; +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/Test/Program.cs b/alb-ecs-bedrock-agents-cdk-dotnet/src/Test/Program.cs new file mode 100644 index 000000000..b5c2e4e48 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/Test/Program.cs @@ -0,0 +1,55 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using TestApp; + +// Create the builder +var builder = Host.CreateDefaultBuilder(args); + +// Configuration +builder.ConfigureHostConfiguration(host => +{ + host.AddEnvironmentVariables(); + host.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); +}); + +// Logging +builder.ConfigureLogging((context, logging) => +{ + logging.AddConfiguration(context.Configuration.GetSection("Logging")); + logging.AddConsole(); +}); + +// Services +builder.ConfigureServices(services => +{ + services.AddHostedService(); + services.AddSingleton(); +}); + +// Host +var host = builder.Build(); + +// Cancellation Token +using var cts = new CancellationTokenSource(); + +// Listen for CTRL+C +Console.CancelKeyPress += (sender, eventArgs) => +{ + eventArgs.Cancel = true; + cts.Cancel(); +}; + +// Logger +var logger = host.Services.GetRequiredService().CreateLogger("main"); + +// Run +try +{ + await host.RunAsync(cts.Token); +} +catch (OperationCanceledException) +{ + logger.LogInformation("Application is shutting down..."); +} \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/Test/TestApp.csproj b/alb-ecs-bedrock-agents-cdk-dotnet/src/Test/TestApp.csproj new file mode 100644 index 000000000..70eda8df6 --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/Test/TestApp.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + PreserveNewest + + + \ No newline at end of file diff --git a/alb-ecs-bedrock-agents-cdk-dotnet/src/Test/appsettings.json b/alb-ecs-bedrock-agents-cdk-dotnet/src/Test/appsettings.json new file mode 100644 index 000000000..df078b92a --- /dev/null +++ b/alb-ecs-bedrock-agents-cdk-dotnet/src/Test/appsettings.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "System": "Warning", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "ALB_DNS_NAME": "", + "PORT": "80", + "enableTrace" : false +}