Add AWS CDK implementation for existing CFN examples (#536)
* feat: Add AWS CDK implementation for basic AgentCore runtime deployment This commit introduces a comprehensive CDK alternative to the existing CloudFormation basic-runtime sample, providing a cleaner and more maintainable Infrastructure as Code approach for deploying Amazon Bedrock AgentCore resources. - **Complete CDK stack** (`basic_runtime_stack.py`) with proper construct separation - **Dedicated IAM role construct** (`infra-utils/agentcore_role.py`) for reusability - **Custom Lambda function** (`infra-utils/build_trigger_lambda.py`) for CodeBuild automation - **S3 asset-based source packaging** eliminating Docker dependency for users - **ARM64 CodeBuild integration** with automated container image building - **Comprehensive documentation** matching CloudFormation sample structure - Uses S3 assets instead of inline code for better maintainability - Separates infrastructure utilities into dedicated `infra-utils/` directory - Implements proper CDK patterns with construct separation - Provides cleaner deployment experience (~5-10 min vs ~10-15 min) - **Basic Strands agent** (`agent-code/basic_agent.py`) with simple Q&A functionality - **ARM64 Dockerfile** optimized for AgentCore runtime requirements - **Proper dependency management** with isolated requirements - Updated title to reflect both CloudFormation and CDK options - Added comprehensive CDK section with architecture highlights - Included CDK prerequisites with version requirements (CDK 2.218.0+) - Updated repository structure to show new CDK directory layout - Added installation commands for required CDK dependencies - Complete documentation following CloudFormation sample structure - Detailed prerequisites, deployment, testing, and cleanup instructions - Sample queries and troubleshooting sections - Architecture explanation and use case descriptions - **CDK 2.218.0+** required for BedrockAgentCore construct support - **Python 3.8+** and **constructs>=10.0.79** for proper CDK functionality - **S3 assets** for source code packaging without size limitations - ECR repository for container image storage - CodeBuild project with ARM64 support for automated builds - Lambda function for build orchestration and completion waiting - AgentCore Runtime with proper IAM permissions and networking - Custom resource for deployment automation - ✅ Successfully deployed and tested in AWS environment - ✅ Verified agent functionality with sample queries - ✅ Confirmed clean resource cleanup with `cdk destroy` - Added David Kaleko to CONTRIBUTORS.md This implementation provides a modern, maintainable alternative to CloudFormation while maintaining feature parity and following AWS CDK best practices. * fix: Resolve CDK Lambda import issues and reorganize infrastructure utilities This commit fixes critical Lambda function import errors that were preventing the CDK stack deployment from completing, and reorganizes the infrastructure utilities for better Python module compatibility. - **Root cause**: `cfnresponse` module is only available for inline CloudFormation Lambda code, not when using CDK's `Code.from_asset()` approach - **Solution**: Embedded the standard AWS-provided cfnresponse functionality directly into the Lambda function to eliminate import dependencies - **Impact**: Custom resource now properly signals CloudFormation completion/failure - **Renamed**: `infra-utils/` → `infra_utils/` for proper Python module imports - **Fixed**: Lambda handler path to use correct Python module notation - **Updated**: Import statements to use underscore-based directory name - Embedded cfnresponse class with SUCCESS/FAILED constants and send() method - Added comprehensive comments explaining why local cfnresponse is necessary - Maintains full compatibility with CloudFormation custom resource protocol - Proper error handling and CloudWatch logging integration - Updated Lambda handler path: `infra_utils.build_trigger_lambda.handler` - Fixed import statements for renamed directory structure - Removed conditional BedrockAgentCore imports (always available in CDK 2.218.0+) - Moved infrastructure utilities to properly named Python package - Added package `__init__.py` for proper module structure - Maintained clean separation between infrastructure and agent code - ✅ Resolves hanging CloudFormation deployments - ✅ Custom resource now properly waits for CodeBuild completion - ✅ Stack deployment completes successfully end-to-end - ✅ Maintains compatibility with existing CloudFormation approach - Verified Lambda function executes without import errors - Confirmed CodeBuild triggering and monitoring functionality - Validated complete stack deployment cycle This fix ensures the CDK implementation works reliably and follows Python packaging best practices while maintaining the same deployment behavior as the CloudFormation equivalent. * Minor README update * Dockerfile updates including a health check to fix all ASH security scan warnings * Readme updates in accordance with PR feedback * feat: Add CDK implementation for end-to-end weather agent - Complete CDK stack for weather-based activity planning agent - Includes browser tool, code interpreter, memory, and S3 storage - Fixed IAM permissions for bedrock-agentcore services - Added proper CloudFormation response handling for custom resources - Comprehensive documentation with deployment and testing instructions - Production-ready infrastructure with monitoring and best practices * Add CDK implementation for multi-agent runtime Convert CloudFormation multi-agent-runtime example to CDK with: - Dual agent architecture (orchestrator + specialist) - Agent-to-agent communication via bedrock-agentcore:InvokeAgentRuntime - Separate ECR repos and CodeBuild projects for each agent - IAM roles with proper cross-agent invocation permissions - Custom resource Lambda for build triggering - Comprehensive documentation and test script Tested and validated: orchestrator correctly delegates complex queries to specialist agent while handling simple queries directly. * README updates to make sure CDK readmes parallel that of cloudformation, copied architecture diagrams over because they're the same * Add CDK implementation for MCP server AgentCore runtime Convert CloudFormation mcp-server-agentcore-runtime example to CDK with: - MCP server with FastMCP and three tools (add_numbers, multiply_numbers, greet_user) - Cognito JWT authentication with pre-created test user - ECR repository and CodeBuild project for ARM64 Docker image - IAM roles with proper permissions for MCP protocol - Custom Lambda functions for build triggering and password setting - Architecture diagram and comprehensive documentation - Test scripts for authentication and MCP tool validation Tested and validated: MCP server successfully deployed with JWT auth, all three tools working correctly via MCP client. * Reorganizing READMEs to avoid duplication, top level IaC README describes each example only once then links to both CFN and CDK versions of each * Python linting fixes --------- Signed-off-by: David Kaleko <5712203+kaleko@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Infrastructure as Code Samples for Amazon Bedrock AgentCore
|
||||
|
||||
CloudFormation templates and AWS CDK stacks for deploying Amazon Bedrock AgentCore resources.
|
||||
Deploy Amazon Bedrock AgentCore resources using CloudFormation templates or AWS CDK.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -14,286 +14,107 @@ Choose your preferred approach:
|
||||
- **[CloudFormation](./cloudformation/)** - YAML/JSON templates for declarative infrastructure
|
||||
- **[CDK](./cdk/)** - Python code for programmatic infrastructure
|
||||
|
||||
## 📚 CloudFormation Samples
|
||||
## Samples
|
||||
|
||||
### 01. [Hosting MCP Server on AgentCore Runtime](./cloudformation/mcp-server-agentcore-runtime/)
|
||||
|
||||
Deploy a complete MCP (Model Context Protocol) server with automated Docker image building and JWT authentication.
|
||||
### 1. Basic Agent Runtime
|
||||
Deploy a simple AgentCore Runtime with a basic Strands agent - no additional tools or memory.
|
||||
|
||||
**What it deploys:**
|
||||
- Amazon ECR Repository for Docker images
|
||||
- AWS CodeBuild for automated ARM64 builds
|
||||
- AgentCore Runtime with simple agent
|
||||
- ECR Repository and automated Docker builds
|
||||
- IAM roles with least-privilege policies
|
||||
|
||||
**Use case:** Learning AgentCore basics without complexity
|
||||
**Deployment time:** ~5-15 minutes
|
||||
**Estimated cost:** ~$50-100/month
|
||||
|
||||
**Implementation:** [CloudFormation](./cloudformation/basic-runtime/) | [CDK](./cdk/basic-runtime/)
|
||||
|
||||
### 2. MCP Server on AgentCore Runtime
|
||||
Deploy a complete MCP (Model Context Protocol) server with automated Docker building and JWT authentication.
|
||||
|
||||
**What it deploys:**
|
||||
- AgentCore Runtime hosting MCP server
|
||||
- Amazon Cognito for JWT authentication
|
||||
- IAM roles with least-privilege policies
|
||||
- Lambda functions for custom resource automation
|
||||
- Amazon Bedrock AgentCore Runtime hosting the MCP server
|
||||
|
||||
**Sample MCP Tools:**
|
||||
- `add_numbers` - Adds two numbers
|
||||
- `multiply_numbers` - Multiplies two numbers
|
||||
- `greet_user` - Greets a user by name
|
||||
- Automated ARM64 Docker builds
|
||||
|
||||
**Sample MCP Tools:** `add_numbers`, `multiply_numbers`, `greet_user`
|
||||
**Deployment time:** ~10-15 minutes
|
||||
**Estimated cost:** ~$50-100/month
|
||||
|
||||
**Quick start:**
|
||||
```bash
|
||||
cd cloudformation/mcp-server-agentcore-runtime
|
||||
./deploy.sh
|
||||
./test.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 02. [Basic Agent Runtime](./cloudformation/basic-runtime/)
|
||||
|
||||
Deploy a basic AgentCore Runtime with a simple Strands agent - no additional tools or memory.
|
||||
|
||||
**What it deploys:**
|
||||
- Amazon ECR Repository
|
||||
- AWS CodeBuild for ARM64 Docker image building
|
||||
- IAM roles with least-privilege policies
|
||||
- Lambda functions for automation
|
||||
- Basic AgentCore Runtime with simple agent
|
||||
|
||||
**Use case:** Simple agent deployment without memory, code interpreter, or browser tools
|
||||
|
||||
**Deployment time:** ~10-15 minutes
|
||||
**Estimated cost:** ~$50-100/month
|
||||
|
||||
**Quick start:**
|
||||
```bash
|
||||
aws cloudformation create-stack \
|
||||
--stack-name basic-agent-demo \
|
||||
--template-body file://cloudformation/basic-runtime/template.yaml \
|
||||
--capabilities CAPABILITY_IAM \
|
||||
--region us-west-2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 03. [Multi-Agent Runtime](./cloudformation/multi-agent-runtime/)
|
||||
**Implementation:** [CloudFormation](./cloudformation/mcp-server-agentcore-runtime/) | [CDK](./cdk/mcp-server-agentcore-runtime/)
|
||||
|
||||
### 3. Multi-Agent Runtime
|
||||
Deploy a multi-agent system where Agent1 (orchestrator) can invoke Agent2 (specialist) for complex tasks.
|
||||
|
||||
**What it deploys:**
|
||||
- Two ECR Repositories (one per agent)
|
||||
- AWS CodeBuild projects for both agents
|
||||
- IAM roles with agent-to-agent invocation permissions
|
||||
- Lambda functions for automation
|
||||
- Two AgentCore Runtimes with agent-to-agent communication
|
||||
- IAM roles with agent-to-agent invocation permissions
|
||||
- Separate ECR repositories for each agent
|
||||
|
||||
**Architecture:**
|
||||
- **Agent1 (Orchestrator)**: Routes requests and delegates to Agent2
|
||||
- **Agent2 (Specialist)**: Handles detailed analysis and complex tasks
|
||||
|
||||
**Architecture:** Agent1 routes requests and delegates to Agent2 for detailed analysis
|
||||
**Deployment time:** ~15-20 minutes
|
||||
**Estimated cost:** ~$100-200/month
|
||||
|
||||
**Quick start:**
|
||||
```bash
|
||||
aws cloudformation create-stack \
|
||||
--stack-name multi-agent-demo \
|
||||
--template-body file://cloudformation/multi-agent-runtime/template.yaml \
|
||||
--capabilities CAPABILITY_IAM \
|
||||
--region us-west-2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 04. [End-to-End Weather Agent with Tools and Memory](./cloudformation/end-to-end-weather-agent/)
|
||||
**Implementation:** [CloudFormation](./cloudformation/multi-agent-runtime/) | [CDK](./cdk/multi-agent-runtime/)
|
||||
|
||||
### 4. End-to-End Weather Agent with Tools and Memory
|
||||
Deploy a complete weather-based activity planning agent with browser automation, code interpreter, and memory.
|
||||
|
||||
**What it deploys:**
|
||||
- Amazon ECR Repository
|
||||
- AWS CodeBuild for ARM64 Docker image building
|
||||
- S3 bucket for results storage
|
||||
- IAM roles with comprehensive permissions
|
||||
- Lambda functions for automation
|
||||
- AgentCore Runtime with Strands agent
|
||||
- **Browser Tool** for web scraping weather data
|
||||
- **Code Interpreter Tool** for weather analysis
|
||||
- **Memory** for storing user preferences
|
||||
|
||||
**Features:**
|
||||
- Scrapes weather data from weather.gov using browser automation
|
||||
- Analyzes weather conditions using Python code execution
|
||||
- Stores and retrieves user activity preferences
|
||||
- Generates personalized activity recommendations
|
||||
- Saves results to S3 bucket
|
||||
- Browser Tool for web scraping weather data
|
||||
- Code Interpreter Tool for weather analysis
|
||||
- Memory for storing user preferences
|
||||
- S3 bucket for results storage
|
||||
|
||||
**Features:** Scrapes weather.gov, analyzes conditions, stores preferences, generates recommendations
|
||||
**Deployment time:** ~15-20 minutes
|
||||
**Estimated cost:** ~$100-150/month
|
||||
|
||||
**Quick start:**
|
||||
```bash
|
||||
aws cloudformation create-stack \
|
||||
--stack-name weather-agent-demo \
|
||||
--template-body file://cloudformation/end-to-end-weather-agent/end-to-end-weather-agent.yaml \
|
||||
--capabilities CAPABILITY_IAM \
|
||||
--region us-west-2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 CDK Samples
|
||||
|
||||
### 01. [Basic Agent Runtime](./cdk/basic-runtime/)
|
||||
|
||||
Deploy a basic AgentCore Runtime with a simple Strands agent using AWS CDK - no additional tools or memory.
|
||||
|
||||
**What it deploys:**
|
||||
- Docker image asset built from local code
|
||||
- IAM role with least-privilege policies for AgentCore
|
||||
- Basic AgentCore Runtime with simple agent
|
||||
|
||||
**Architecture highlights:**
|
||||
- Uses `DockerImageAsset` for container image building (no CodeBuild needed)
|
||||
- Separates IAM role into its own construct (`AgentCoreRole`)
|
||||
- Uses `CfnRuntime` directly from `aws_bedrockagentcore`
|
||||
- Much cleaner than the CloudFormation equivalent
|
||||
|
||||
**Use case:** Simple agent deployment without memory, code interpreter, or browser tools
|
||||
|
||||
**Deployment time:** ~5-10 minutes
|
||||
**Estimated cost:** ~$50-100/month
|
||||
|
||||
**Quick start:**
|
||||
```bash
|
||||
cd cdk/basic-runtime
|
||||
pip install -r requirements.txt
|
||||
cdk deploy
|
||||
```
|
||||
|
||||
---
|
||||
**Implementation:** [CloudFormation](./cloudformation/end-to-end-weather-agent/) | [CDK](./cdk/end-to-end-weather-agent/)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before deploying any CloudFormation template, ensure you have:
|
||||
Before deploying any sample, ensure you have:
|
||||
|
||||
1. **AWS Account** with appropriate permissions
|
||||
2. **AWS CLI** installed and configured
|
||||
```bash
|
||||
aws configure
|
||||
```
|
||||
3. **Access to Amazon Bedrock AgentCore** (preview)
|
||||
4. **For CDK samples**: Python 3.8+, AWS CDK v2 installed, and **CDK version 2.218.0 or later** (for BedrockAgentCore support)
|
||||
```bash
|
||||
npm install -g aws-cdk
|
||||
pip install aws-cdk-lib==2.218.0 constructs>=10.0.79
|
||||
```
|
||||
5. **IAM Permissions** to create:
|
||||
- CloudFormation stacks
|
||||
4. **IAM Permissions** to create:
|
||||
- CloudFormation stacks (for CloudFormation samples)
|
||||
- IAM roles and policies
|
||||
- ECR repositories
|
||||
- Lambda functions
|
||||
- CodeBuild projects
|
||||
- AgentCore resources
|
||||
- S3 buckets (for weather agent)
|
||||
|
||||
## General Usage Pattern
|
||||
|
||||
Each sample follows a consistent structure:
|
||||
|
||||
```bash
|
||||
# Deploy
|
||||
aws cloudformation create-stack \
|
||||
--stack-name <stack-name> \
|
||||
--template-body file://<sample-directory>/template.yaml \
|
||||
--capabilities CAPABILITY_IAM \
|
||||
--region <region>
|
||||
|
||||
# Monitor deployment
|
||||
aws cloudformation describe-stacks \
|
||||
--stack-name <stack-name> \
|
||||
--region <region>
|
||||
|
||||
# Cleanup
|
||||
aws cloudformation delete-stack \
|
||||
--stack-name <stack-name> \
|
||||
--region <region>
|
||||
```
|
||||
|
||||
Default values:
|
||||
- Stack name: Varies by sample (see quick start commands)
|
||||
- Region: `us-west-2`
|
||||
For CDK samples, also install:
|
||||
- Python 3.8+
|
||||
- AWS CDK v2.218.0 or later
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
04-infrastructure-as-code/
|
||||
├── README.md # This file
|
||||
├── cloudformation/ # CloudFormation samples
|
||||
│ ├── mcp-server-agentcore-runtime/ # MCP Server sample
|
||||
│ │ ├── deploy.sh # Deployment script
|
||||
│ │ ├── test.sh # Testing script
|
||||
│ │ ├── cleanup.sh # Cleanup script
|
||||
│ │ ├── mcp-server-template.yaml # CloudFormation template
|
||||
│ │ ├── get_token.py # Authentication helper
|
||||
│ │ ├── test_mcp_server.py # MCP client test
|
||||
│ │ ├── README.md # Sample documentation
|
||||
│ │ └── DETAILED_GUIDE.md # Technical deep-dive
|
||||
│ ├── basic-runtime/ # Basic agent sample
|
||||
│ │ └── template.yaml # CloudFormation template
|
||||
│ ├── multi-agent-runtime/ # Multi-agent sample
|
||||
│ │ └── template.yaml # CloudFormation template
|
||||
│ └── end-to-end-weather-agent/ # Weather agent sample
|
||||
│ └── end-to-end-weather-agent.yaml # CloudFormation template
|
||||
└── cdk/ # CDK samples
|
||||
└── basic-runtime/ # Basic agent CDK sample
|
||||
├── app.py # CDK app entry point
|
||||
├── basic_runtime_stack.py # Stack definition
|
||||
├── requirements.txt # Python dependencies
|
||||
├── cdk.json # CDK configuration
|
||||
├── README.md # Sample documentation
|
||||
├── infra-utils/ # Infrastructure utilities
|
||||
│ ├── agentcore_role.py # Dedicated role construct
|
||||
│ └── build_trigger_lambda.py # Lambda function for CodeBuild trigger
|
||||
└── agent-code/ # Agent source code
|
||||
├── Dockerfile
|
||||
├── basic_agent.py
|
||||
└── requirements.txt
|
||||
```
|
||||
|
||||
|
||||
### Stack Creation Fails
|
||||
|
||||
Check CloudFormation events:
|
||||
```bash
|
||||
aws cloudformation describe-stack-events \
|
||||
--stack-name <stack-name> \
|
||||
--region <region>
|
||||
```
|
||||
|
||||
### Permission Issues
|
||||
|
||||
Ensure your IAM user/role has:
|
||||
- `CloudFormationFullAccess` or equivalent
|
||||
- Permissions to create all resources in the template
|
||||
- `iam:PassRole` for service roles
|
||||
|
||||
### CodeBuild Failures
|
||||
|
||||
Check CodeBuild logs:
|
||||
```bash
|
||||
aws codebuild batch-get-builds \
|
||||
--ids <build-id> \
|
||||
--region <region>
|
||||
```
|
||||
|
||||
### Resource Limits
|
||||
|
||||
Check AWS service quotas:
|
||||
```bash
|
||||
aws service-quotas list-service-quotas \
|
||||
--service-code <service-code>
|
||||
├── README.md # This file
|
||||
├── cloudformation/ # CloudFormation samples
|
||||
│ ├── README.md # CloudFormation-specific guide
|
||||
│ ├── basic-runtime/
|
||||
│ ├── mcp-server-agentcore-runtime/
|
||||
│ ├── multi-agent-runtime/
|
||||
│ └── end-to-end-weather-agent/
|
||||
└── cdk/ # CDK samples
|
||||
├── README.md # CDK-specific guide
|
||||
├── basic-runtime/
|
||||
├── mcp-server-agentcore-runtime/
|
||||
├── multi-agent-runtime/
|
||||
└── end-to-end-weather-agent/
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Amazon Bedrock AgentCore Documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/agentcore.html)
|
||||
- [AWS CloudFormation Documentation](https://docs.aws.amazon.com/cloudformation/)
|
||||
- [CloudFormation Best Practices](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/best-practices.html)
|
||||
- [CloudFormation Template Reference](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/AWS_BedrockAgentCore.html)
|
||||
- [AWS CDK Documentation](https://docs.aws.amazon.com/cdk/)
|
||||
- [Original Tutorials](../01-tutorials/)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# CDK Samples
|
||||
|
||||
Deploy Amazon Bedrock AgentCore resources using AWS CDK (Python).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.8+
|
||||
- AWS CDK v2.218.0 or later (for BedrockAgentCore support)
|
||||
- AWS CLI configured
|
||||
- Access to Amazon Bedrock AgentCore (preview)
|
||||
|
||||
```bash
|
||||
npm install -g aws-cdk
|
||||
```
|
||||
|
||||
## General Deployment Pattern
|
||||
|
||||
```bash
|
||||
cd <sample-directory>
|
||||
pip install -r requirements.txt
|
||||
cdk deploy
|
||||
```
|
||||
|
||||
## Samples
|
||||
|
||||
- **[basic-runtime/](./basic-runtime/)** - Simple agent deployment
|
||||
- **[multi-agent-runtime/](./multi-agent-runtime/)** - Multi-agent system
|
||||
- **[mcp-server-agentcore-runtime/](./mcp-server-agentcore-runtime/)** - MCP Server with JWT authentication
|
||||
- **[end-to-end-weather-agent/](./end-to-end-weather-agent/)** - Weather agent with tools and memory
|
||||
|
||||
## CDK Advantages
|
||||
|
||||
- Uses `DockerImageAsset` for container building (no CodeBuild needed)
|
||||
- Cleaner construct separation and reusability
|
||||
- Type safety and IDE support
|
||||
- Faster deployment times
|
||||
@@ -11,7 +11,10 @@ This CDK stack deploys a basic Amazon Bedrock AgentCore Runtime with a simple St
|
||||
- [Testing](#testing)
|
||||
- [Sample Queries](#sample-queries)
|
||||
- [Cleanup](#cleanup)
|
||||
- [Cost Estimate](#cost-estimate)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [🤝 Contributing](#-contributing)
|
||||
- [📄 License](#-license)
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -22,7 +25,6 @@ This CDK stack creates a minimal AgentCore deployment that includes:
|
||||
- **IAM Roles**: Provides necessary permissions
|
||||
- **CodeBuild Project**: Automatically builds the ARM64 Docker image
|
||||
- **Lambda Functions**: Custom resources for automation
|
||||
- **S3 Assets**: Source code packaging and deployment
|
||||
|
||||
This makes it ideal for:
|
||||
- Learning AgentCore basics
|
||||
@@ -32,6 +34,8 @@ This makes it ideal for:
|
||||
|
||||
## Architecture
|
||||
|
||||

|
||||
|
||||
The architecture consists of:
|
||||
|
||||
- **User**: Sends questions to the agent and receives responses
|
||||
@@ -43,7 +47,6 @@ The architecture consists of:
|
||||
- **IAM Roles**:
|
||||
- IAM role for CodeBuild (builds and pushes images)
|
||||
- IAM role for Agent Execution (runtime permissions)
|
||||
- **Amazon Bedrock LLMs**: Provides the AI model capabilities for the agent
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -70,7 +73,7 @@ The architecture consists of:
|
||||
cdk --version
|
||||
```
|
||||
|
||||
4. **CDK version 2.218.0 or later** (for BedrockAgentCore support)
|
||||
4. **CDK version 2.220.0 or later** (for BedrockAgentCore support)
|
||||
|
||||
5. **Bedrock Model Access**: Enable access to Amazon Bedrock models in your AWS region
|
||||
- [Bedrock Model Access Guide](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html)
|
||||
@@ -82,10 +85,13 @@ The architecture consists of:
|
||||
- Lambda function creation
|
||||
- CodeBuild project creation
|
||||
- BedrockAgentCore resource creation
|
||||
- S3 bucket operations (for CDK assets)
|
||||
|
||||
## Deployment
|
||||
|
||||
### CDK vs CloudFormation
|
||||
|
||||
This is the **CDK version** of the basic AgentCore runtime. If you prefer CloudFormation, see the [CloudFormation version](../../cloudformation/basic-runtime/).
|
||||
|
||||
### Option 1: Quick Deploy (Recommended)
|
||||
|
||||
```bash
|
||||
@@ -109,22 +115,26 @@ source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
||||
# 2. Install Python dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 2. Bootstrap CDK in your account/region (first time only)
|
||||
# 3. Bootstrap CDK in your account/region (first time only)
|
||||
cdk bootstrap
|
||||
|
||||
# 3. Synthesize the CloudFormation template (optional)
|
||||
# 4. Synthesize the CloudFormation template (optional)
|
||||
cdk synth
|
||||
|
||||
# 4. Deploy the stack
|
||||
# 5. Deploy the stack
|
||||
cdk deploy --require-approval never
|
||||
|
||||
# 5. Get outputs
|
||||
# 6. Get outputs
|
||||
cdk list
|
||||
```
|
||||
|
||||
### Deployment Time
|
||||
|
||||
- **Expected Duration**: 3-5 minutes
|
||||
- **Expected Duration**: 8-12 minutes
|
||||
- **Main Steps**:
|
||||
- Stack creation: ~2 minutes
|
||||
- Docker image build (CodeBuild): ~5-8 minutes
|
||||
- Runtime provisioning: ~1-2 minutes
|
||||
|
||||
## Testing
|
||||
|
||||
@@ -142,7 +152,7 @@ RUNTIME_ARN=$(aws cloudformation describe-stacks \
|
||||
aws bedrock-agentcore invoke-agent-runtime \
|
||||
--agent-runtime-arn $RUNTIME_ARN \
|
||||
--qualifier DEFAULT \
|
||||
--payload $(echo '{"prompt": "What is 2+2?"}' | base64) \
|
||||
--payload $(echo '{"prompt": "Hello, how are you?"}' | base64) \
|
||||
response.json
|
||||
|
||||
# View the response
|
||||
@@ -159,7 +169,7 @@ cat response.json
|
||||
6. Enter test payload:
|
||||
```json
|
||||
{
|
||||
"prompt": "What is 2+2?"
|
||||
"prompt": "Hello, how are you?"
|
||||
}
|
||||
```
|
||||
7. Click "Invoke"
|
||||
@@ -168,29 +178,24 @@ cat response.json
|
||||
|
||||
Try these queries to test your basic agent:
|
||||
|
||||
1. **Simple Math**:
|
||||
1. **Simple Greeting**:
|
||||
```json
|
||||
{"prompt": "What is 2+2?"}
|
||||
{"prompt": "Hello, how are you?"}
|
||||
```
|
||||
|
||||
2. **General Knowledge**:
|
||||
2. **Question Answering**:
|
||||
```json
|
||||
{"prompt": "What is the capital of France?"}
|
||||
```
|
||||
|
||||
3. **Explanation Request**:
|
||||
3. **Creative Writing**:
|
||||
```json
|
||||
{"prompt": "Explain what Amazon Bedrock is in simple terms"}
|
||||
{"prompt": "Write a short poem about clouds"}
|
||||
```
|
||||
|
||||
4. **Creative Task**:
|
||||
4. **Problem Solving**:
|
||||
```json
|
||||
{"prompt": "Write a haiku about cloud computing"}
|
||||
```
|
||||
|
||||
5. **Reasoning**:
|
||||
```json
|
||||
{"prompt": "If I have 5 apples and give away 2, how many do I have left?"}
|
||||
{"prompt": "How do I bake a chocolate cake?"}
|
||||
```
|
||||
|
||||
## Cleanup
|
||||
@@ -221,6 +226,29 @@ aws cloudformation wait stack-delete-complete \
|
||||
3. Click "Delete"
|
||||
4. Confirm deletion
|
||||
|
||||
## Cost Estimate
|
||||
|
||||
### Monthly Cost Breakdown (us-east-1)
|
||||
|
||||
| Service | Usage | Monthly Cost |
|
||||
|---------|-------|--------------|
|
||||
| **AgentCore Runtime** | 1 runtime, minimal usage | ~$5-10 |
|
||||
| **ECR Repository** | 1 repository, <1GB storage | ~$0.10 |
|
||||
| **CodeBuild** | Occasional builds | ~$1-2 |
|
||||
| **Lambda** | Custom resource executions | ~$0.01 |
|
||||
| **CloudWatch Logs** | Agent logs | ~$0.50 |
|
||||
| **Bedrock Model Usage** | Pay per token | Variable* |
|
||||
|
||||
**Estimated Total: ~$7-13/month** (excluding Bedrock model usage)
|
||||
|
||||
*Bedrock costs depend on your usage patterns and chosen models. See [Bedrock Pricing](https://aws.amazon.com/bedrock/pricing/) for details.
|
||||
|
||||
### Cost Optimization Tips
|
||||
|
||||
- **Delete when not in use**: Use `cdk destroy` to remove all resources
|
||||
- **Monitor usage**: Set up CloudWatch billing alarms
|
||||
- **Choose efficient models**: Select appropriate Bedrock models for your use case
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### CDK Bootstrap Required
|
||||
@@ -250,3 +278,18 @@ Check CodeBuild logs in the AWS Console:
|
||||
1. Go to CodeBuild console
|
||||
2. Find the build project (name contains "basic-agent-build")
|
||||
3. Check build history and logs
|
||||
|
||||
### Runtime Issues
|
||||
|
||||
If the runtime fails to start:
|
||||
1. Check CloudWatch logs for the runtime
|
||||
2. Verify the Docker image was built successfully
|
||||
3. Ensure IAM permissions are correct
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please see our [Contributing Guide](../../CONTRIBUTING.md) for details.
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](../../LICENSE) file for details.
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from strands import Agent
|
||||
import os
|
||||
from bedrock_agentcore.runtime import BedrockAgentCoreApp
|
||||
|
||||
app = BedrockAgentCoreApp()
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
@@ -0,0 +1,406 @@
|
||||
# End-to-End Weather Agent with Tools and Memory - CDK
|
||||
|
||||
This CDK stack deploys a complete Amazon Bedrock AgentCore Runtime with a sophisticated weather-based activity planning agent. This demonstrates the full power of AgentCore by integrating Browser tool, Code Interpreter, Memory, and S3 storage in a single deployment.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Architecture](#architecture)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Deployment](#deployment)
|
||||
- [Testing](#testing)
|
||||
- [Sample Queries](#sample-queries)
|
||||
- [How It Works](#how-it-works)
|
||||
- [Cleanup](#cleanup)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Overview
|
||||
|
||||
This CDK stack creates a comprehensive AgentCore deployment that showcases:
|
||||
|
||||
### Core Components
|
||||
|
||||
- **AgentCore Runtime**: Hosts a Strands agent with multiple tools
|
||||
- **Browser Tool**: Web automation for scraping weather data from weather.gov
|
||||
- **Code Interpreter**: Python code execution for weather analysis
|
||||
- **Memory**: Stores user activity preferences
|
||||
- **S3 Bucket**: Stores generated activity recommendations
|
||||
- **ECR Repository**: Container image storage
|
||||
- **IAM Roles**: Comprehensive permissions for all components
|
||||
|
||||
### Agent Capabilities
|
||||
|
||||
The Weather Activity Planner agent can:
|
||||
|
||||
1. **Scrape Weather Data**: Uses browser automation to fetch 8-day forecasts from weather.gov
|
||||
2. **Analyze Weather**: Generates and executes Python code to classify days as GOOD/OK/POOR
|
||||
3. **Retrieve Preferences**: Accesses user activity preferences from memory
|
||||
4. **Generate Recommendations**: Creates personalized activity suggestions based on weather and preferences
|
||||
5. **Store Results**: Saves recommendations as Markdown files in S3
|
||||
|
||||
### Use Cases
|
||||
|
||||
- Weather-based activity planning
|
||||
- Automated web scraping and data analysis
|
||||
- Multi-tool agent orchestration
|
||||
- Memory-driven personalization
|
||||
- Asynchronous task processing
|
||||
|
||||
## Architecture
|
||||
|
||||
The architecture demonstrates a complete AgentCore deployment with multiple integrated tools:
|
||||
|
||||
**Core Components:**
|
||||
- **User**: Sends weather-based activity planning queries
|
||||
- **AWS CodeBuild**: Builds the ARM64 Docker container image with the agent code
|
||||
- **Amazon ECR Repository**: Stores the container image
|
||||
- **AgentCore Runtime**: Hosts the Weather Activity Planner Agent
|
||||
- **Weather Agent**: Strands agent that orchestrates multiple tools
|
||||
- Invokes Amazon Bedrock LLMs for reasoning and code generation
|
||||
- **Browser Tool**: Web automation for scraping weather data from weather.gov
|
||||
- **Code Interpreter Tool**: Executes Python code for weather analysis
|
||||
- **Memory**: Stores user activity preferences (30-day retention)
|
||||
- **S3 Bucket**: Stores generated activity recommendations
|
||||
- **IAM Roles**: Comprehensive permissions for all components
|
||||
|
||||
**Workflow:**
|
||||
1. User sends query: "What should I do this weekend in Richmond VA?"
|
||||
2. Agent extracts city and uses Browser Tool to scrape 8-day forecast
|
||||
3. Agent generates Python code and uses Code Interpreter to classify weather
|
||||
4. Agent retrieves user preferences from Memory
|
||||
5. Agent generates personalized recommendations
|
||||
6. Agent stores results in S3 bucket using use_aws tool
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### AWS Account Setup
|
||||
|
||||
1. **AWS Account**: You need an active AWS account with appropriate permissions
|
||||
- [Create AWS Account](https://aws.amazon.com/account/)
|
||||
- [AWS Console Access](https://aws.amazon.com/console/)
|
||||
|
||||
2. **AWS CLI**: Install and configure AWS CLI with your credentials
|
||||
- [Install AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)
|
||||
- [Configure AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html)
|
||||
|
||||
```bash
|
||||
aws configure
|
||||
```
|
||||
|
||||
3. **Python 3.10+** and **AWS CDK v2** installed
|
||||
```bash
|
||||
# Install CDK
|
||||
npm install -g aws-cdk
|
||||
|
||||
# Verify installation
|
||||
cdk --version
|
||||
```
|
||||
|
||||
4. **CDK version 2.218.0 or later** (for BedrockAgentCore support)
|
||||
|
||||
5. **Bedrock Model Access**: Enable access to Amazon Bedrock models in your AWS region
|
||||
- [Bedrock Model Access Guide](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html)
|
||||
|
||||
6. **Required Permissions**: Your AWS user/role needs permissions for:
|
||||
- CloudFormation stack operations
|
||||
- ECR repository management
|
||||
- IAM role creation
|
||||
- Lambda function creation
|
||||
- CodeBuild project creation
|
||||
- BedrockAgentCore resource creation (Runtime, Browser, CodeInterpreter, Memory)
|
||||
- S3 bucket operations (for CDK assets and results storage)
|
||||
|
||||
## Deployment
|
||||
|
||||
### CDK vs CloudFormation
|
||||
|
||||
This is the **CDK version** of the end-to-end weather agent. If you prefer CloudFormation, see the [CloudFormation version](../../cloudformation/end-to-end-weather-agent/).
|
||||
|
||||
### Option 1: Quick Deploy (Recommended)
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Bootstrap CDK (first time only)
|
||||
cdk bootstrap
|
||||
|
||||
# Deploy
|
||||
cdk deploy
|
||||
```
|
||||
|
||||
### Option 2: Step by Step
|
||||
|
||||
```bash
|
||||
# 1. Create and activate Python virtual environment
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
||||
|
||||
# 2. Install Python dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 3. Bootstrap CDK in your account/region (first time only)
|
||||
cdk bootstrap
|
||||
|
||||
# 4. Synthesize the CloudFormation template (optional)
|
||||
cdk synth
|
||||
|
||||
# 5. Deploy the stack
|
||||
cdk deploy --require-approval never
|
||||
|
||||
# 6. Get outputs
|
||||
cdk list
|
||||
```
|
||||
|
||||
### Deployment Time
|
||||
|
||||
- **Expected Duration**: 15-20 minutes
|
||||
- **Main Steps**:
|
||||
- Stack creation: ~2 minutes
|
||||
- Docker image build (CodeBuild): ~10-12 minutes
|
||||
- Runtime and tools provisioning: ~3-5 minutes
|
||||
- Memory initialization: ~1 minute
|
||||
|
||||
## Testing
|
||||
|
||||
### Using AWS CLI
|
||||
|
||||
```bash
|
||||
# Get the Runtime ARN from CDK outputs
|
||||
RUNTIME_ARN=$(aws cloudformation describe-stacks \
|
||||
--stack-name WeatherAgentDemo \
|
||||
--region us-east-1 \
|
||||
--query 'Stacks[0].Outputs[?OutputKey==`AgentRuntimeArn`].OutputValue' \
|
||||
--output text)
|
||||
|
||||
# Get the S3 bucket name
|
||||
BUCKET_NAME=$(aws cloudformation describe-stacks \
|
||||
--stack-name WeatherAgentDemo \
|
||||
--region us-east-1 \
|
||||
--query 'Stacks[0].Outputs[?OutputKey==`ResultsBucketName`].OutputValue' \
|
||||
--output text)
|
||||
|
||||
# Invoke the agent
|
||||
aws bedrock-agentcore invoke-agent-runtime \
|
||||
--agent-runtime-arn $RUNTIME_ARN \
|
||||
--qualifier DEFAULT \
|
||||
--payload $(echo '{"prompt": "What should I do this weekend in Richmond VA?"}' | base64) \
|
||||
response.json
|
||||
|
||||
# View the immediate response
|
||||
cat response.json
|
||||
|
||||
# Wait a few minutes for processing, then check S3 for results
|
||||
aws s3 ls s3://$BUCKET_NAME/
|
||||
|
||||
# Download the results
|
||||
aws s3 cp s3://$BUCKET_NAME/results.md ./results.md
|
||||
cat results.md
|
||||
```
|
||||
|
||||
### Using AWS Console
|
||||
|
||||
1. Navigate to [Bedrock AgentCore Console](https://console.aws.amazon.com/bedrock-agentcore/)
|
||||
2. Go to "Runtimes" in the left navigation
|
||||
3. Find your runtime (name starts with `WeatherAgentDemo_`)
|
||||
4. Click on the runtime name
|
||||
5. Click "Test" button
|
||||
6. Enter test payload:
|
||||
```json
|
||||
{
|
||||
"prompt": "What should I do this weekend in Richmond VA?"
|
||||
}
|
||||
```
|
||||
7. Click "Invoke"
|
||||
8. View the immediate response
|
||||
9. Wait 2-3 minutes for background processing
|
||||
10. Navigate to [S3 Console](https://console.aws.amazon.com/s3/) to download results.md from the results bucket
|
||||
|
||||
## Sample Queries
|
||||
|
||||
Try these queries to test the weather agent:
|
||||
|
||||
1. **Weekend Planning**:
|
||||
```json
|
||||
{"prompt": "What should I do this weekend in Richmond VA?"}
|
||||
```
|
||||
|
||||
2. **Specific City**:
|
||||
```json
|
||||
{"prompt": "Plan activities for next week in San Francisco"}
|
||||
```
|
||||
|
||||
3. **Different Location**:
|
||||
```json
|
||||
{"prompt": "What outdoor activities can I do in Seattle this week?"}
|
||||
```
|
||||
|
||||
4. **Vacation Planning**:
|
||||
```json
|
||||
{"prompt": "I'm visiting Austin next week. What should I plan based on the weather?"}
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
### Step-by-Step Workflow
|
||||
|
||||
1. **User Query**: "What should I do this weekend in Richmond VA?"
|
||||
|
||||
2. **City Extraction**: Agent extracts "Richmond VA" from the query
|
||||
|
||||
3. **Weather Scraping** (Browser Tool):
|
||||
- Navigates to weather.gov
|
||||
- Searches for Richmond VA
|
||||
- Clicks "Printable Forecast"
|
||||
- Extracts 8-day forecast data (date, high, low, conditions, wind, precipitation)
|
||||
- Returns JSON array of weather data
|
||||
|
||||
4. **Code Generation** (LLM):
|
||||
- Agent generates Python code to classify weather days
|
||||
- Classification rules:
|
||||
- GOOD: 65-80°F, clear, no rain
|
||||
- OK: 55-85°F, partly cloudy, slight rain
|
||||
- POOR: <55°F or >85°F, cloudy/rainy
|
||||
|
||||
5. **Code Execution** (Code Interpreter):
|
||||
- Executes the generated Python code
|
||||
- Returns list of tuples: `[('2025-09-16', 'GOOD'), ('2025-09-17', 'OK'), ...]`
|
||||
|
||||
6. **Preference Retrieval** (Memory):
|
||||
- Fetches user activity preferences from memory
|
||||
- Preferences stored by weather type:
|
||||
```json
|
||||
{
|
||||
"good_weather": ["hiking", "beach volleyball", "outdoor picnic"],
|
||||
"ok_weather": ["walking tours", "outdoor dining", "park visits"],
|
||||
"poor_weather": ["indoor museums", "shopping", "restaurants"]
|
||||
}
|
||||
```
|
||||
|
||||
7. **Recommendation Generation** (LLM):
|
||||
- Combines weather analysis with user preferences
|
||||
- Creates day-by-day activity recommendations
|
||||
- Formats as Markdown document
|
||||
|
||||
8. **Storage** (S3 via use_aws tool):
|
||||
- Saves recommendations to S3 bucket as `results.md`
|
||||
- User can download and review recommendations
|
||||
|
||||
### Asynchronous Processing
|
||||
|
||||
The agent runs asynchronously to handle long-running tasks:
|
||||
- Immediate response: "Processing started..."
|
||||
- Background processing: Completes all steps
|
||||
- Results available in S3 after ~2-3 minutes
|
||||
|
||||
## Cleanup
|
||||
|
||||
### Using CDK (Recommended)
|
||||
|
||||
```bash
|
||||
cdk destroy
|
||||
```
|
||||
|
||||
### Using AWS CLI
|
||||
|
||||
```bash
|
||||
# Step 1: Empty the S3 bucket (required before deletion)
|
||||
BUCKET_NAME=$(aws cloudformation describe-stacks \
|
||||
--stack-name WeatherAgentDemo \
|
||||
--region us-east-1 \
|
||||
--query 'Stacks[0].Outputs[?OutputKey==`ResultsBucketName`].OutputValue' \
|
||||
--output text)
|
||||
|
||||
aws s3 rm s3://$BUCKET_NAME --recursive
|
||||
|
||||
# Step 2: Terminate any active browser sessions
|
||||
# Get the Browser ID
|
||||
BROWSER_ID=$(aws cloudformation describe-stacks \
|
||||
--stack-name WeatherAgentDemo \
|
||||
--region us-east-1 \
|
||||
--query 'Stacks[0].Outputs[?OutputKey==`BrowserId`].OutputValue' \
|
||||
--output text)
|
||||
|
||||
# List active sessions
|
||||
aws bedrock-agentcore list-browser-sessions \
|
||||
--browser-id $BROWSER_ID \
|
||||
--region us-east-1
|
||||
|
||||
# Terminate each active session (replace SESSION_ID with actual session ID from list command)
|
||||
# Repeat this command for each active session
|
||||
aws bedrock-agentcore terminate-browser-session \
|
||||
--browser-id $BROWSER_ID \
|
||||
--session-id SESSION_ID \
|
||||
--region us-east-1
|
||||
|
||||
# Step 3: Delete the stack
|
||||
aws cloudformation delete-stack \
|
||||
--stack-name WeatherAgentDemo \
|
||||
--region us-east-1
|
||||
|
||||
# Wait for deletion to complete
|
||||
aws cloudformation wait stack-delete-complete \
|
||||
--stack-name WeatherAgentDemo \
|
||||
--region us-east-1
|
||||
```
|
||||
|
||||
**Important**: Browser sessions are automatically created when the agent uses the browser tool. Always terminate active sessions before deleting the stack to avoid deletion failures.
|
||||
|
||||
### Using AWS Console
|
||||
|
||||
1. Navigate to [S3 Console](https://console.aws.amazon.com/s3/)
|
||||
2. Find the bucket (name format: `<stack-name>-results-<account-id>`)
|
||||
3. Empty the bucket
|
||||
4. Navigate to [Bedrock AgentCore Console](https://console.aws.amazon.com/bedrock-agentcore/)
|
||||
5. Go to "Browsers" in the left navigation
|
||||
6. Find your browser (name starts with `WeatherAgentDemo_browser`)
|
||||
7. Click on the browser name
|
||||
8. In the "Sessions" tab, terminate any active sessions
|
||||
9. Navigate to [CloudFormation Console](https://console.aws.amazon.com/cloudformation/)
|
||||
10. Select the `WeatherAgentDemo` stack
|
||||
11. Click "Delete"
|
||||
12. Confirm deletion
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### CDK Bootstrap Required
|
||||
|
||||
If you see bootstrap errors:
|
||||
```bash
|
||||
cdk bootstrap aws://ACCOUNT-NUMBER/REGION
|
||||
```
|
||||
|
||||
### Permission Issues
|
||||
|
||||
Ensure your IAM user/role has:
|
||||
- `CDKToolkit` permissions or equivalent
|
||||
- Permissions to create all resources in the stack
|
||||
- `iam:PassRole` for service roles
|
||||
|
||||
### Python Dependencies
|
||||
|
||||
Install dependencies in the project directory:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Build Failures
|
||||
|
||||
Check CodeBuild logs in the AWS Console:
|
||||
1. Go to CodeBuild console
|
||||
2. Find the build project (name contains "weather-agent-build")
|
||||
3. Check build history and logs
|
||||
|
||||
### Browser Session Issues
|
||||
|
||||
If deployment fails due to browser sessions:
|
||||
1. List active sessions using AWS CLI
|
||||
2. Terminate all active sessions
|
||||
3. Retry deployment or cleanup
|
||||
|
||||
### Memory Initialization Issues
|
||||
|
||||
If memory initialization fails:
|
||||
1. Check Lambda function logs in CloudWatch
|
||||
2. Verify IAM permissions for memory access
|
||||
3. Retry deployment
|
||||
@@ -0,0 +1,24 @@
|
||||
FROM public.ecr.aws/docker/library/python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt requirements.txt
|
||||
RUN pip install --no-cache-dir -r requirements.txt && \
|
||||
pip install --no-cache-dir aws-opentelemetry-distro==0.10.1
|
||||
|
||||
ENV AWS_REGION=us-west-2
|
||||
ENV AWS_DEFAULT_REGION=us-west-2
|
||||
|
||||
# Create non-root user
|
||||
RUN useradd -m -u 1000 bedrock_agentcore
|
||||
USER bedrock_agentcore
|
||||
|
||||
EXPOSE 8080
|
||||
EXPOSE 8000
|
||||
|
||||
COPY . .
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8080/ping || exit 1
|
||||
|
||||
CMD ["opentelemetry-instrument", "python", "-m", "weather_agent"]
|
||||
@@ -0,0 +1,9 @@
|
||||
strands-agents
|
||||
strands-agents-tools
|
||||
uv
|
||||
boto3
|
||||
bedrock-agentcore
|
||||
bedrock-agentcore-starter-toolkit
|
||||
browser-use==0.3.2
|
||||
langchain-aws>=0.1.0
|
||||
rich
|
||||
@@ -0,0 +1,316 @@
|
||||
from strands import Agent, tool
|
||||
from strands_tools import use_aws
|
||||
from typing import Dict, Any
|
||||
import json
|
||||
import os
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
|
||||
from bedrock_agentcore.tools.browser_client import BrowserClient
|
||||
from browser_use import Agent as BrowserAgent
|
||||
from browser_use.browser.session import BrowserSession
|
||||
from browser_use.browser import BrowserProfile
|
||||
from langchain_aws import ChatBedrockConverse
|
||||
from bedrock_agentcore.tools.code_interpreter_client import CodeInterpreter
|
||||
from bedrock_agentcore.memory import MemoryClient
|
||||
from rich.console import Console
|
||||
import re
|
||||
|
||||
from bedrock_agentcore.runtime import BedrockAgentCoreApp
|
||||
app = BedrockAgentCoreApp()
|
||||
|
||||
console = Console()
|
||||
|
||||
# Configuration
|
||||
BROWSER_ID = os.getenv('BROWSER_ID', "agentcore_dev_browser-Df3lyxkbjo")
|
||||
CODE_INTERPRETER_ID = os.getenv('CODE_INTERPRETER_ID', "agentcore_dev_code_interpreter-IqIg8bqnKn")
|
||||
MEMORY_ID = os.getenv('MEMORY_ID', "agentcore_dev_TestAgentCoreMemory-N7LCAH8ZCK")
|
||||
RESULTS_BUCKET = os.getenv('RESULTS_BUCKET', "default-results-bucket")
|
||||
region = 'us-west-2'
|
||||
|
||||
# Async helper functions
|
||||
async def run_browser_task(browser_session, bedrock_chat, task: str) -> str:
|
||||
"""Run a browser automation task using browser_use"""
|
||||
try:
|
||||
console.print(f"[blue]🤖 Executing browser task:[/blue] {task[:100]}...")
|
||||
|
||||
agent = BrowserAgent(
|
||||
task=task,
|
||||
llm=bedrock_chat,
|
||||
browser=browser_session
|
||||
)
|
||||
|
||||
result = await agent.run()
|
||||
console.print("[green]✅ Browser task completed successfully![/green]")
|
||||
|
||||
if 'done' in result.last_action() and 'text' in result.last_action()['done']:
|
||||
return result.last_action()['done']['text']
|
||||
else:
|
||||
raise ValueError("NO Data")
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"[red]❌ Browser task error: {e}[/red]")
|
||||
raise
|
||||
|
||||
async def initialize_browser_session():
|
||||
"""Initialize Browser-use session with AgentCore WebSocket connection"""
|
||||
try:
|
||||
client = BrowserClient(region)
|
||||
client.start(identifier=BROWSER_ID)
|
||||
|
||||
ws_url, headers = client.generate_ws_headers()
|
||||
console.print(f"[cyan]🔗 Browser WebSocket URL: {ws_url[:50]}...[/cyan]")
|
||||
|
||||
browser_profile = BrowserProfile(
|
||||
headers=headers,
|
||||
timeout=150000,
|
||||
)
|
||||
|
||||
browser_session = BrowserSession(
|
||||
cdp_url=ws_url,
|
||||
browser_profile=browser_profile,
|
||||
keep_alive=True
|
||||
)
|
||||
|
||||
console.print("[cyan]🔄 Initializing browser session...[/cyan]")
|
||||
await browser_session.start()
|
||||
|
||||
bedrock_chat = ChatBedrockConverse(
|
||||
model_id="us.anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
region_name="us-west-2"
|
||||
)
|
||||
|
||||
console.print("[green]✅ Browser session initialized and ready[/green]")
|
||||
return browser_session, bedrock_chat, client
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"[red]❌ Failed to initialize browser session: {e}[/red]")
|
||||
raise
|
||||
|
||||
# Tools for Strands Agent
|
||||
@tool
|
||||
async def get_weather_data(city: str) -> Dict[str, Any]:
|
||||
"""Get weather data for a city using browser automation"""
|
||||
browser_session = None
|
||||
|
||||
try:
|
||||
console.print(f"[cyan]🌐 Getting weather data for {city}[/cyan]")
|
||||
|
||||
browser_session, bedrock_chat, browser_client = await initialize_browser_session()
|
||||
|
||||
task = f"""Instruction: Extract 8-Day Weather Forecast for {city} from weather.gov
|
||||
Steps:
|
||||
- Go to https://weather.gov.
|
||||
- Enter "{city}" into the search box and Click on `GO` to execute the search.
|
||||
- On the local forecast page, click the "Printable Forecast" link.
|
||||
- Wait for the printable forecast page to load completely.
|
||||
- For each day in the forecast, extract these fields:
|
||||
- date (format YYYY-MM-DD)
|
||||
- high (highest temperature)
|
||||
- low (lowest temperature)
|
||||
- conditions (short weather summary, e.g., "Clear")
|
||||
- wind (wind speed as an integer; use mph or km/h as consistent)
|
||||
- precip (precipitation chance or amount, zero if none)
|
||||
- Format the extracted data as a JSON array of daily forecast objects, e.g.:
|
||||
```json
|
||||
[
|
||||
{{
|
||||
"date": "2025-09-17",
|
||||
"high": 78,
|
||||
"low": 62,
|
||||
"conditions": "Clear",
|
||||
"wind": 10,
|
||||
"precip": 80
|
||||
}},
|
||||
{{
|
||||
"date": "2025-09-18",
|
||||
"high": 82,
|
||||
"low": 65,
|
||||
"conditions": "Partly Cloudy",
|
||||
"wind": 10,
|
||||
"precip": 80
|
||||
|
||||
}}
|
||||
// ... Repeat for each day ...
|
||||
]```
|
||||
|
||||
- Return only this JSON array as the final output.
|
||||
|
||||
Additional Notes:
|
||||
Use null or 0 if any numeric value is missing.
|
||||
Avoid scraping ads, navigation, or unrelated page elements.
|
||||
If "Printable Forecast" is missing, fallback to the main forecast page.
|
||||
Include error handling (e.g., return an empty array if forecast data isn't found).
|
||||
Confirm the city name matches the requested location before returning results.
|
||||
"""
|
||||
|
||||
result = await run_browser_task(browser_session, bedrock_chat, task)
|
||||
|
||||
if browser_client :
|
||||
browser_client.stop()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"content": [{"text": result}]
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"[red]❌ Error getting weather data: {e}[/red]")
|
||||
return {
|
||||
"status": "error",
|
||||
"content": [{"text": f"Error getting weather data: {str(e)}"}]
|
||||
}
|
||||
|
||||
finally:
|
||||
if browser_session:
|
||||
console.print("[yellow]🔌 Closing browser session...[/yellow]")
|
||||
with suppress(Exception):
|
||||
await browser_session.close()
|
||||
console.print("[green]✅ Browser session closed[/green]")
|
||||
|
||||
@tool
|
||||
def generate_analysis_code(weather_data: str) -> Dict[str, Any]:
|
||||
"""Generate Python code for weather classification"""
|
||||
try:
|
||||
query = f"""Create Python code to classify weather days as GOOD/OK/POOR:
|
||||
|
||||
Rules:
|
||||
- GOOD: 65-80°F, clear conditions, no rain
|
||||
- OK: 55-85°F, partly cloudy, slight rain chance
|
||||
- POOR: <55°F or >85°F, cloudy/rainy
|
||||
|
||||
Weather data:
|
||||
{weather_data}
|
||||
|
||||
Store weather data stored in python variable for using it in python code
|
||||
|
||||
Return code that outputs list of tuples: [('2025-09-16', 'GOOD'), ('2025-09-17', 'OK'), ...]"""
|
||||
|
||||
agent = Agent()
|
||||
result = agent(query)
|
||||
|
||||
pattern = r'```(?:json|python)\n(.*?)\n```'
|
||||
match = re.search(pattern, result.message['content'][0]['text'], re.DOTALL)
|
||||
python_code = match.group(1).strip() if match else result.message['content'][0]['text']
|
||||
|
||||
return {"status": "success", "content": [{"text": python_code}]}
|
||||
except Exception as e:
|
||||
return {"status": "error", "content": [{"text": f"Error: {str(e)}"}]}
|
||||
|
||||
@tool
|
||||
def execute_code(python_code: str) -> Dict[str, Any]:
|
||||
"""Execute Python code using AgentCore Code Interpreter"""
|
||||
try:
|
||||
code_client = CodeInterpreter('us-west-2')
|
||||
code_client.start(identifier=CODE_INTERPRETER_ID)
|
||||
|
||||
response = code_client.invoke("executeCode", {
|
||||
"code": python_code,
|
||||
"language": "python",
|
||||
"clearContext": True
|
||||
})
|
||||
|
||||
for event in response["stream"]:
|
||||
code_execute_result = json.dumps(event["result"])
|
||||
|
||||
analysis_results = json.loads(code_execute_result)
|
||||
console.print("Analysis results:", analysis_results)
|
||||
|
||||
return {"status": "success", "content": [{"text": str(analysis_results)}]}
|
||||
|
||||
except Exception as e:
|
||||
return {"status": "error", "content": [{"text": f"Error: {str(e)}"}]}
|
||||
|
||||
@tool
|
||||
def get_activity_preferences() -> Dict[str, Any]:
|
||||
"""Get activity preferences from memory"""
|
||||
try:
|
||||
client = MemoryClient(region_name='us-west-2')
|
||||
response = client.list_events(
|
||||
memory_id=MEMORY_ID,
|
||||
actor_id="user123",
|
||||
session_id="session456",
|
||||
max_results=50,
|
||||
include_payload=True
|
||||
)
|
||||
|
||||
preferences = response[0]["payload"][0]['blob'] if response else "No preferences found"
|
||||
return {"status": "success", "content": [{"text": preferences}]}
|
||||
except Exception as e:
|
||||
return {"status": "error", "content": [{"text": f"Error: {str(e)}"}]}
|
||||
|
||||
def create_weather_agent() -> Agent:
|
||||
"""Create the weather agent with all tools"""
|
||||
system_prompt = f"""You are a Weather-Based Activity Planning Assistant.
|
||||
|
||||
When a user asks about activities for a location, follow below stepes Sequentially:
|
||||
1. Extract city from user query
|
||||
2. Call get_weather_data(city) to get weather information
|
||||
3. Call generate_analysis_code(weather_data) to create classification code
|
||||
4. Call execute_code(python_code) to get Day Type ( GOOD, OK , POOR ) for forecasting dates.
|
||||
5. Call get_activity_preferences() to get user preferences
|
||||
6. Generate Activity Recommendations based on weather and preferences that you have recieved from previous steps
|
||||
7. Generate the comprehensive Markdown file (results.md) and store it in S3 Bucket : {RESULTS_BUCKET} through use_aws tool.
|
||||
|
||||
IMPORTANT: Provide complete recommendations and end your response. Do NOT ask follow-up questions or wait for additional input."""
|
||||
|
||||
return Agent(
|
||||
tools=[get_weather_data, generate_analysis_code, execute_code, get_activity_preferences, use_aws],
|
||||
system_prompt=system_prompt,
|
||||
name="WeatherActivityPlanner"
|
||||
)
|
||||
|
||||
@app.async_task
|
||||
async def async_main(query=None):
|
||||
"""Async main function"""
|
||||
console.print("🌤️ Weather-Based Activity Planner - Async Version")
|
||||
console.print("=" * 30)
|
||||
|
||||
agent = create_weather_agent()
|
||||
|
||||
query = query or "What should I do this weekend in Richmond VA?"
|
||||
console.print(f"\n[bold blue]🔍 Query:[/bold blue] {query}")
|
||||
console.print("-" * 50)
|
||||
|
||||
try:
|
||||
os.environ["BYPASS_TOOL_CONSENT"] = "True"
|
||||
result = agent(query)
|
||||
|
||||
return {
|
||||
"status": "completed",
|
||||
"result": result.message['content'][0]['text']
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"[red]❌ Error: {e}[/red]")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
@app.entrypoint
|
||||
async def invoke(payload=None):
|
||||
try:
|
||||
# change
|
||||
query = payload.get("prompt")
|
||||
|
||||
asyncio.create_task(async_main(query))
|
||||
|
||||
msg = (
|
||||
"Processing started ... "
|
||||
f"You can monitor status in CloudWatch logs at /aws/bedrock-agentcore/runtimes/<agent-runtime-id> ....."
|
||||
f"You can see the result at {RESULTS_BUCKET} ...."
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "Started",
|
||||
"message": msg
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
import aws_cdk as cdk
|
||||
from weather_agent_stack import WeatherAgentStack
|
||||
|
||||
app = cdk.App()
|
||||
WeatherAgentStack(app, "WeatherAgentDemo")
|
||||
|
||||
app.synth()
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"app": "python3 app.py",
|
||||
"watch": {
|
||||
"include": [
|
||||
"**"
|
||||
],
|
||||
"exclude": [
|
||||
"README.md",
|
||||
"cdk*.json",
|
||||
"requirements*.txt",
|
||||
"source.bat",
|
||||
"**/__pycache__",
|
||||
"**/*.pyc"
|
||||
]
|
||||
},
|
||||
"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-iam:standardizedServicePrincipals": 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:enableLogging": 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:enableLogging": true,
|
||||
"@aws-cdk/aws-nordicapis-apigateway:authorizerChangeDeploymentLogicalId": true,
|
||||
"@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true,
|
||||
"@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount": true,
|
||||
"@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
|
||||
"@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForSourceAction": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
# Infrastructure utilities for Weather Agent CDK stack
|
||||
@@ -0,0 +1,158 @@
|
||||
from aws_cdk import (
|
||||
aws_iam as iam,
|
||||
Stack
|
||||
)
|
||||
from constructs import Construct
|
||||
|
||||
class AgentCoreRole(iam.Role):
|
||||
def __init__(self, scope: Construct, construct_id: str, s3_bucket_arn: str = None, **kwargs):
|
||||
region = Stack.of(scope).region
|
||||
account_id = Stack.of(scope).account
|
||||
|
||||
statements = [
|
||||
iam.PolicyStatement(
|
||||
sid="ECRImageAccess",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"ecr:BatchGetImage",
|
||||
"ecr:GetDownloadUrlForLayer",
|
||||
"ecr:BatchCheckLayerAvailability"
|
||||
],
|
||||
resources=[f"arn:aws:ecr:{region}:{account_id}:repository/*"]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="ECRTokenAccess",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=["ecr:GetAuthorizationToken"],
|
||||
resources=["*"]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"logs:DescribeLogStreams",
|
||||
"logs:CreateLogGroup",
|
||||
"logs:DescribeLogGroups",
|
||||
"logs:CreateLogStream",
|
||||
"logs:PutLogEvents"
|
||||
],
|
||||
resources=[f"arn:aws:logs:{region}:{account_id}:log-group:/aws/bedrock-agentcore/runtimes/*"]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"xray:PutTraceSegments",
|
||||
"xray:PutTelemetryRecords",
|
||||
"xray:GetSamplingRules",
|
||||
"xray:GetSamplingTargets"
|
||||
],
|
||||
resources=["*"]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=["cloudwatch:PutMetricData"],
|
||||
resources=["*"],
|
||||
conditions={
|
||||
"StringEquals": {
|
||||
"cloudwatch:namespace": "bedrock-agentcore"
|
||||
}
|
||||
}
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="GetAgentAccessToken",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"bedrock-agentcore:GetWorkloadAccessToken",
|
||||
"bedrock-agentcore:GetWorkloadAccessTokenForJWT",
|
||||
"bedrock-agentcore:GetWorkloadAccessTokenForUserId"
|
||||
],
|
||||
resources=[
|
||||
f"arn:aws:bedrock-agentcore:{region}:{account_id}:workload-identity-directory/default",
|
||||
f"arn:aws:bedrock-agentcore:{region}:{account_id}:workload-identity-directory/default/workload-identity/*"
|
||||
]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="BedrockModelInvocation",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"bedrock:InvokeModel",
|
||||
"bedrock:InvokeModelWithResponseStream"
|
||||
],
|
||||
resources=[
|
||||
"arn:aws:bedrock:*::foundation-model/*",
|
||||
f"arn:aws:bedrock:{region}:{account_id}:*"
|
||||
]
|
||||
),
|
||||
# Browser Tool permissions
|
||||
iam.PolicyStatement(
|
||||
sid="BrowserToolAccess",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"bedrock-agentcore:StartBrowserSession",
|
||||
"bedrock-agentcore:StopBrowserSession",
|
||||
"bedrock-agentcore:InvokeBrowser",
|
||||
"bedrock-agentcore:ListBrowserSessions",
|
||||
"bedrock-agentcore:TerminateBrowserSession"
|
||||
],
|
||||
resources=[f"arn:aws:bedrock-agentcore:{region}:{account_id}:browser/*"]
|
||||
),
|
||||
# Code Interpreter permissions
|
||||
iam.PolicyStatement(
|
||||
sid="CodeInterpreterAccess",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"bedrock-agentcore:StartCodeInterpreterSession",
|
||||
"bedrock-agentcore:StopCodeInterpreterSession",
|
||||
"bedrock-agentcore:InvokeCodeInterpreter",
|
||||
"bedrock-agentcore:ListCodeInterpreterSessions"
|
||||
],
|
||||
resources=[f"arn:aws:bedrock-agentcore:{region}:{account_id}:code-interpreter/*"]
|
||||
),
|
||||
# Memory permissions
|
||||
iam.PolicyStatement(
|
||||
sid="MemoryAccess",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"bedrock-agentcore:ListEvents",
|
||||
"bedrock-agentcore:PutEvents",
|
||||
"bedrock-agentcore:GetEvents"
|
||||
],
|
||||
resources=[f"arn:aws:bedrock-agentcore:{region}:{account_id}:memory/*"]
|
||||
),
|
||||
# AWS CLI permissions for use_aws tool
|
||||
iam.PolicyStatement(
|
||||
sid="AWSCLIAccess",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"sts:GetCallerIdentity",
|
||||
"sts:AssumeRole"
|
||||
],
|
||||
resources=["*"]
|
||||
)
|
||||
]
|
||||
|
||||
# Add S3 permissions if bucket ARN provided
|
||||
if s3_bucket_arn:
|
||||
statements.append(
|
||||
iam.PolicyStatement(
|
||||
sid="S3BucketAccess",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"s3:GetObject",
|
||||
"s3:PutObject",
|
||||
"s3:DeleteObject",
|
||||
"s3:ListBucket"
|
||||
],
|
||||
resources=[
|
||||
s3_bucket_arn,
|
||||
f"{s3_bucket_arn}/*"
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
super().__init__(scope, construct_id,
|
||||
assumed_by=iam.ServicePrincipal("bedrock-agentcore.amazonaws.com"),
|
||||
inline_policies={
|
||||
"AgentCorePolicy": iam.PolicyDocument(statements=statements)
|
||||
},
|
||||
**kwargs
|
||||
)
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import boto3
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
import urllib3
|
||||
|
||||
# Note: cfnresponse is only available for inline Lambda code in CloudFormation.
|
||||
# When using CDK with Code.from_asset(), we need to include our own copy.
|
||||
# This is the standard AWS-provided cfnresponse module embedded directly.
|
||||
|
||||
class cfnresponse:
|
||||
SUCCESS = "SUCCESS"
|
||||
FAILED = "FAILED"
|
||||
|
||||
@staticmethod
|
||||
def send(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False, reason=None):
|
||||
responseUrl = event['ResponseURL']
|
||||
print(responseUrl)
|
||||
|
||||
responseBody = {
|
||||
'Status': responseStatus,
|
||||
'Reason': reason or "See the details in CloudWatch Log Stream: {}".format(context.log_stream_name),
|
||||
'PhysicalResourceId': physicalResourceId or context.log_stream_name,
|
||||
'StackId': event['StackId'],
|
||||
'RequestId': event['RequestId'],
|
||||
'LogicalResourceId': event['LogicalResourceId'],
|
||||
'NoEcho': noEcho,
|
||||
'Data': responseData
|
||||
}
|
||||
|
||||
json_responseBody = json.dumps(responseBody)
|
||||
print("Response body:")
|
||||
print(json_responseBody)
|
||||
|
||||
headers = {
|
||||
'content-type': '',
|
||||
'content-length': str(len(json_responseBody))
|
||||
}
|
||||
|
||||
try:
|
||||
http = urllib3.PoolManager()
|
||||
response = http.request('PUT', responseUrl, headers=headers, body=json_responseBody)
|
||||
print("Status code:", response.status)
|
||||
except Exception as e:
|
||||
print("send(..) failed executing http.request(..):", e)
|
||||
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
def handler(event, context):
|
||||
logger.info('Received event: %s', json.dumps(event))
|
||||
|
||||
try:
|
||||
if event['RequestType'] == 'Delete':
|
||||
cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
|
||||
return
|
||||
|
||||
project_name = event['ResourceProperties']['ProjectName']
|
||||
|
||||
codebuild = boto3.client('codebuild')
|
||||
|
||||
# Start build
|
||||
response = codebuild.start_build(projectName=project_name)
|
||||
build_id = response['build']['id']
|
||||
logger.info(f"Started build: {build_id}")
|
||||
|
||||
# Wait for completion
|
||||
max_wait_time = context.get_remaining_time_in_millis() / 1000 - 30
|
||||
start_time = time.time()
|
||||
|
||||
while True:
|
||||
if time.time() - start_time > max_wait_time:
|
||||
cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': 'Build timeout'})
|
||||
return
|
||||
|
||||
build_response = codebuild.batch_get_builds(ids=[build_id])
|
||||
build_status = build_response['builds'][0]['buildStatus']
|
||||
|
||||
if build_status == 'SUCCEEDED':
|
||||
logger.info(f"Build {build_id} succeeded")
|
||||
cfnresponse.send(event, context, cfnresponse.SUCCESS, {'BuildId': build_id})
|
||||
return
|
||||
elif build_status in ['FAILED', 'FAULT', 'STOPPED', 'TIMED_OUT']:
|
||||
logger.error(f"Build {build_id} failed with status: {build_status}")
|
||||
cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': f'Build failed: {build_status}'})
|
||||
return
|
||||
|
||||
logger.info(f"Build {build_id} status: {build_status}")
|
||||
time.sleep(30)
|
||||
|
||||
except Exception as e:
|
||||
logger.error('Error: %s', str(e))
|
||||
cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': str(e)})
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
import json
|
||||
import boto3
|
||||
import logging
|
||||
import urllib3
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any
|
||||
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
def send_response(event, context, response_status, response_data=None, physical_resource_id=None, reason=None):
|
||||
"""Send response to CloudFormation"""
|
||||
response_data = response_data or {}
|
||||
physical_resource_id = physical_resource_id or context.log_stream_name
|
||||
reason = reason or f"See CloudWatch Log Stream: {context.log_stream_name}"
|
||||
|
||||
response_body = {
|
||||
'Status': response_status,
|
||||
'Reason': reason,
|
||||
'PhysicalResourceId': physical_resource_id,
|
||||
'StackId': event['StackId'],
|
||||
'RequestId': event['RequestId'],
|
||||
'LogicalResourceId': event['LogicalResourceId'],
|
||||
'Data': response_data
|
||||
}
|
||||
|
||||
json_response_body = json.dumps(response_body)
|
||||
|
||||
headers = {
|
||||
'content-type': '',
|
||||
'content-length': str(len(json_response_body))
|
||||
}
|
||||
|
||||
try:
|
||||
http = urllib3.PoolManager()
|
||||
response = http.request('PUT', event['ResponseURL'], body=json_response_body, headers=headers)
|
||||
logger.info(f"CloudFormation response sent: {response.status}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send CloudFormation response: {e}")
|
||||
|
||||
def handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
|
||||
"""
|
||||
Lambda function to initialize AgentCore Memory with user preferences
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Memory initializer event: {json.dumps(event)}")
|
||||
|
||||
request_type = event.get('RequestType')
|
||||
memory_id = event['ResourceProperties']['MemoryId']
|
||||
region = event['ResourceProperties']['Region']
|
||||
|
||||
if request_type == 'Create':
|
||||
logger.info(f"Initializing memory {memory_id} in region {region}")
|
||||
|
||||
# Activity preferences matching the working CloudFormation template
|
||||
activity_preferences = {
|
||||
"good_weather": ["hiking", "beach volleyball", "outdoor picnic", "cycling", "rock climbing"],
|
||||
"ok_weather": ["walking tours", "outdoor dining", "park visits", "sightseeing", "farmers markets"],
|
||||
"poor_weather": ["indoor museums", "shopping", "restaurants", "movie theaters", "art galleries"]
|
||||
}
|
||||
|
||||
# Convert to JSON string for storage in blob
|
||||
activity_preferences_json = json.dumps(activity_preferences)
|
||||
|
||||
# Get current timestamp
|
||||
timestamp = datetime.utcnow().isoformat() + 'Z'
|
||||
|
||||
# Initialize the bedrock-agentcore client
|
||||
client = boto3.client('bedrock-agentcore', region_name=region)
|
||||
|
||||
response = client.create_event(
|
||||
memoryId=memory_id,
|
||||
actorId="user123",
|
||||
sessionId="session456",
|
||||
eventTimestamp=timestamp,
|
||||
payload=[
|
||||
{
|
||||
'blob': activity_preferences_json,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
logger.info(f"Successfully created memory event: {response}")
|
||||
|
||||
send_response(event, context, 'SUCCESS', {
|
||||
'MemoryId': memory_id,
|
||||
'Status': 'INITIALIZED'
|
||||
}, f"memory-init-{memory_id}")
|
||||
|
||||
elif request_type in ['Update', 'Delete']:
|
||||
logger.info(f"Handling {request_type} request - no action needed")
|
||||
send_response(event, context, 'SUCCESS', {},
|
||||
event.get('PhysicalResourceId', f"memory-init-{memory_id}"))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error initializing memory: {str(e)}")
|
||||
send_response(event, context, 'FAILED', {},
|
||||
event.get('PhysicalResourceId', 'memory-init-failed'), str(e))
|
||||
@@ -0,0 +1,2 @@
|
||||
aws-cdk-lib==2.220.0
|
||||
constructs>=10.0.79
|
||||
@@ -0,0 +1,312 @@
|
||||
from aws_cdk import (
|
||||
Stack,
|
||||
aws_ecr as ecr,
|
||||
aws_s3 as s3,
|
||||
aws_codebuild as codebuild,
|
||||
aws_iam as iam,
|
||||
aws_lambda as lambda_,
|
||||
aws_s3_assets as s3_assets,
|
||||
aws_bedrockagentcore as bedrockagentcore,
|
||||
CustomResource,
|
||||
CfnParameter,
|
||||
CfnOutput,
|
||||
Duration,
|
||||
RemovalPolicy
|
||||
)
|
||||
from constructs import Construct
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), 'infra_utils'))
|
||||
from agentcore_role import AgentCoreRole
|
||||
|
||||
class WeatherAgentStack(Stack):
|
||||
|
||||
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
|
||||
super().__init__(scope, construct_id, **kwargs)
|
||||
|
||||
# Parameters
|
||||
agent_name = CfnParameter(self, "AgentName",
|
||||
type="String",
|
||||
default="WeatherAgent",
|
||||
description="Name for the agent runtime"
|
||||
)
|
||||
|
||||
image_tag = CfnParameter(self, "ImageTag",
|
||||
type="String",
|
||||
default="latest",
|
||||
description="Tag for the Docker image"
|
||||
)
|
||||
|
||||
network_mode = CfnParameter(self, "NetworkMode",
|
||||
type="String",
|
||||
default="PUBLIC",
|
||||
description="Network mode for AgentCore resources",
|
||||
allowed_values=["PUBLIC", "PRIVATE"]
|
||||
)
|
||||
|
||||
memory_name = CfnParameter(self, "MemoryName",
|
||||
type="String",
|
||||
default="WeatherAgentMemory",
|
||||
description="Name for the AgentCore memory resource"
|
||||
)
|
||||
|
||||
# S3 Results Bucket
|
||||
results_bucket = s3.Bucket(self, "ResultsBucket",
|
||||
bucket_name=f"{self.stack_name.lower()}-results-{self.account}",
|
||||
removal_policy=RemovalPolicy.DESTROY,
|
||||
auto_delete_objects=True,
|
||||
versioned=False,
|
||||
public_read_access=False,
|
||||
block_public_access=s3.BlockPublicAccess.BLOCK_ALL
|
||||
)
|
||||
|
||||
# ECR Repository
|
||||
ecr_repository = ecr.Repository(self, "ECRRepository",
|
||||
repository_name=f"{self.stack_name.lower()}-weather-agent",
|
||||
image_tag_mutability=ecr.TagMutability.MUTABLE,
|
||||
removal_policy=RemovalPolicy.DESTROY,
|
||||
empty_on_delete=True,
|
||||
image_scan_on_push=True
|
||||
)
|
||||
|
||||
# S3 Asset for source code
|
||||
source_asset = s3_assets.Asset(self, "SourceAsset",
|
||||
path="./agent-code"
|
||||
)
|
||||
|
||||
# CodeBuild Role
|
||||
codebuild_role = iam.Role(self, "CodeBuildRole",
|
||||
role_name=f"{self.stack_name}-codebuild-role",
|
||||
assumed_by=iam.ServicePrincipal("codebuild.amazonaws.com"),
|
||||
inline_policies={
|
||||
"CodeBuildPolicy": iam.PolicyDocument(
|
||||
statements=[
|
||||
iam.PolicyStatement(
|
||||
sid="CloudWatchLogs",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"logs:CreateLogGroup",
|
||||
"logs:CreateLogStream",
|
||||
"logs:PutLogEvents"
|
||||
],
|
||||
resources=[f"arn:aws:logs:{self.region}:{self.account}:log-group:/aws/codebuild/*"]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="ECRAccess",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"ecr:BatchCheckLayerAvailability",
|
||||
"ecr:GetDownloadUrlForLayer",
|
||||
"ecr:BatchGetImage",
|
||||
"ecr:GetAuthorizationToken",
|
||||
"ecr:PutImage",
|
||||
"ecr:InitiateLayerUpload",
|
||||
"ecr:UploadLayerPart",
|
||||
"ecr:CompleteLayerUpload"
|
||||
],
|
||||
resources=[ecr_repository.repository_arn, "*"]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="S3SourceAccess",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=["s3:GetObject"],
|
||||
resources=[f"{source_asset.bucket.bucket_arn}/*"]
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
# CodeBuild Project
|
||||
build_project = codebuild.Project(self, "AgentImageBuildProject",
|
||||
project_name=f"{self.stack_name}-weather-agent-build",
|
||||
description=f"Build weather agent Docker image for {self.stack_name}",
|
||||
role=codebuild_role,
|
||||
environment=codebuild.BuildEnvironment(
|
||||
build_image=codebuild.LinuxArmBuildImage.AMAZON_LINUX_2_STANDARD_3_0,
|
||||
compute_type=codebuild.ComputeType.LARGE,
|
||||
privileged=True
|
||||
),
|
||||
source=codebuild.Source.s3(
|
||||
bucket=source_asset.bucket,
|
||||
path=source_asset.s3_object_key
|
||||
),
|
||||
build_spec=codebuild.BuildSpec.from_object({
|
||||
"version": "0.2",
|
||||
"phases": {
|
||||
"pre_build": {
|
||||
"commands": [
|
||||
"echo Logging in to Amazon ECR...",
|
||||
"aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com"
|
||||
]
|
||||
},
|
||||
"build": {
|
||||
"commands": [
|
||||
"echo Build started on `date`",
|
||||
"echo Building the Docker image for weather agent ARM64...",
|
||||
"docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG .",
|
||||
"docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG"
|
||||
]
|
||||
},
|
||||
"post_build": {
|
||||
"commands": [
|
||||
"echo Build completed on `date`",
|
||||
"echo Pushing the Docker image...",
|
||||
"docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG",
|
||||
"echo ARM64 Docker image pushed successfully"
|
||||
]
|
||||
}
|
||||
}
|
||||
}),
|
||||
environment_variables={
|
||||
"AWS_DEFAULT_REGION": codebuild.BuildEnvironmentVariable(value=self.region),
|
||||
"AWS_ACCOUNT_ID": codebuild.BuildEnvironmentVariable(value=self.account),
|
||||
"IMAGE_REPO_NAME": codebuild.BuildEnvironmentVariable(value=ecr_repository.repository_name),
|
||||
"IMAGE_TAG": codebuild.BuildEnvironmentVariable(value=image_tag.value_as_string),
|
||||
"STACK_NAME": codebuild.BuildEnvironmentVariable(value=self.stack_name)
|
||||
}
|
||||
)
|
||||
|
||||
# Lambda function to trigger and wait for CodeBuild
|
||||
build_trigger_function = lambda_.Function(self, "BuildTriggerFunction",
|
||||
runtime=lambda_.Runtime.PYTHON_3_9,
|
||||
handler="infra_utils.build_trigger_lambda.handler",
|
||||
timeout=Duration.minutes(15),
|
||||
code=lambda_.Code.from_asset(".", exclude=["*.pyc", "__pycache__", "cdk.out"]),
|
||||
initial_policy=[
|
||||
iam.PolicyStatement(
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=["codebuild:StartBuild", "codebuild:BatchGetBuilds"],
|
||||
resources=[build_project.project_arn]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
# Custom Resource using the Lambda function
|
||||
trigger_build = CustomResource(self, "TriggerImageBuild",
|
||||
service_token=build_trigger_function.function_arn,
|
||||
properties={
|
||||
"ProjectName": build_project.project_name
|
||||
}
|
||||
)
|
||||
|
||||
# Create AgentCore execution role with S3 permissions
|
||||
agent_role = AgentCoreRole(self, "AgentCoreRole", s3_bucket_arn=results_bucket.bucket_arn)
|
||||
|
||||
# Browser Tool
|
||||
browser_tool = bedrockagentcore.CfnBrowserCustom(self, "BrowserTool",
|
||||
name=f"{self.stack_name.replace('-', '_')}_browser",
|
||||
network_configuration=bedrockagentcore.CfnBrowserCustom.BrowserNetworkConfigurationProperty(
|
||||
network_mode=network_mode.value_as_string
|
||||
),
|
||||
description=f"Browser tool for {self.stack_name} weather agent"
|
||||
)
|
||||
|
||||
# Code Interpreter Tool
|
||||
code_interpreter_tool = bedrockagentcore.CfnCodeInterpreterCustom(self, "CodeInterpreterTool",
|
||||
name=f"{self.stack_name.replace('-', '_')}_code_interpreter",
|
||||
network_configuration=bedrockagentcore.CfnCodeInterpreterCustom.CodeInterpreterNetworkConfigurationProperty(
|
||||
network_mode=network_mode.value_as_string
|
||||
),
|
||||
description=f"Code interpreter tool for {self.stack_name} weather agent"
|
||||
)
|
||||
|
||||
# Memory
|
||||
memory = bedrockagentcore.CfnMemory(self, "Memory",
|
||||
name=f"{self.stack_name.replace('-', '_')}_{memory_name.value_as_string}",
|
||||
description=f"Memory for {self.stack_name} weather agent",
|
||||
event_expiry_duration=30
|
||||
)
|
||||
|
||||
# Memory Initializer Lambda
|
||||
memory_initializer_function = lambda_.Function(self, "MemoryInitializerFunction",
|
||||
runtime=lambda_.Runtime.PYTHON_3_9,
|
||||
handler="infra_utils.memory_initializer_lambda.handler",
|
||||
timeout=Duration.minutes(5),
|
||||
code=lambda_.Code.from_asset(".", exclude=["*.pyc", "__pycache__", "cdk.out"]),
|
||||
initial_policy=[
|
||||
iam.PolicyStatement(
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"bedrock-agentcore:CreateEvent",
|
||||
"bedrock-agentcore:ListEvents",
|
||||
"bedrock-agentcore:GetMemory"
|
||||
],
|
||||
resources=["*"]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
# Initialize Memory with preferences
|
||||
initialize_memory = CustomResource(self, "InitializeMemory",
|
||||
service_token=memory_initializer_function.function_arn,
|
||||
properties={
|
||||
"MemoryId": memory.attr_memory_id,
|
||||
"Region": self.region
|
||||
}
|
||||
)
|
||||
initialize_memory.node.add_dependency(memory)
|
||||
|
||||
# Create AgentCore Runtime
|
||||
agent_runtime = bedrockagentcore.CfnRuntime(self, "AgentRuntime",
|
||||
agent_runtime_name=f"{self.stack_name.replace('-', '_')}_{agent_name.value_as_string}",
|
||||
agent_runtime_artifact=bedrockagentcore.CfnRuntime.AgentRuntimeArtifactProperty(
|
||||
container_configuration=bedrockagentcore.CfnRuntime.ContainerConfigurationProperty(
|
||||
container_uri=f"{ecr_repository.repository_uri}:{image_tag.value_as_string}"
|
||||
)
|
||||
),
|
||||
network_configuration=bedrockagentcore.CfnRuntime.NetworkConfigurationProperty(
|
||||
network_mode=network_mode.value_as_string
|
||||
),
|
||||
protocol_configuration="HTTP",
|
||||
role_arn=agent_role.role_arn,
|
||||
description=f"Weather agent runtime for {self.stack_name}",
|
||||
environment_variables={
|
||||
"AWS_DEFAULT_REGION": self.region,
|
||||
"BROWSER_ID": browser_tool.attr_browser_id,
|
||||
"CODE_INTERPRETER_ID": code_interpreter_tool.attr_code_interpreter_id,
|
||||
"MEMORY_ID": memory.attr_memory_id,
|
||||
"RESULTS_BUCKET": results_bucket.bucket_name
|
||||
}
|
||||
)
|
||||
|
||||
agent_runtime.node.add_dependency(trigger_build)
|
||||
agent_runtime.node.add_dependency(browser_tool)
|
||||
agent_runtime.node.add_dependency(code_interpreter_tool)
|
||||
agent_runtime.node.add_dependency(initialize_memory)
|
||||
|
||||
# Outputs
|
||||
CfnOutput(self, "AgentRuntimeId",
|
||||
description="ID of the created agent runtime",
|
||||
value=agent_runtime.attr_agent_runtime_id
|
||||
)
|
||||
|
||||
CfnOutput(self, "AgentRuntimeArn",
|
||||
description="ARN of the created agent runtime",
|
||||
value=agent_runtime.attr_agent_runtime_arn
|
||||
)
|
||||
|
||||
CfnOutput(self, "BrowserId",
|
||||
description="ID of the browser tool",
|
||||
value=browser_tool.attr_browser_id
|
||||
)
|
||||
|
||||
CfnOutput(self, "CodeInterpreterId",
|
||||
description="ID of the code interpreter tool",
|
||||
value=code_interpreter_tool.attr_code_interpreter_id
|
||||
)
|
||||
|
||||
CfnOutput(self, "MemoryId",
|
||||
description="ID of the memory resource",
|
||||
value=memory.attr_memory_id
|
||||
)
|
||||
|
||||
CfnOutput(self, "ResultsBucketName",
|
||||
description="Name of the S3 results bucket",
|
||||
value=results_bucket.bucket_name
|
||||
)
|
||||
|
||||
CfnOutput(self, "AgentRoleArn",
|
||||
description="ARN of the agent execution role",
|
||||
value=agent_role.role_arn
|
||||
)
|
||||
@@ -0,0 +1,358 @@
|
||||
# Hosting MCP Server on AgentCore Runtime - CDK
|
||||
|
||||
## Overview
|
||||
|
||||
This CDK stack deploys an MCP (Model Context Protocol) server on Amazon Bedrock AgentCore Runtime. It demonstrates how to host MCP tools on AgentCore Runtime using infrastructure as code, with automated deployment scripts for a streamlined experience.
|
||||
|
||||
The stack uses the Amazon Bedrock AgentCore Python SDK to wrap agent functions as an MCP server compatible with Amazon Bedrock AgentCore. It handles the MCP server details so you can focus on your agent's core functionality.
|
||||
|
||||
When hosting tools, the Amazon Bedrock AgentCore Python SDK implements the [Stateless Streamable HTTP](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports) transport protocol with the `MCP-Session-Id` header for session isolation. Your MCP server will be hosted on port `8000` and provide one invocation path: the `mcp-POST` endpoint.
|
||||
|
||||
### Tutorial Details
|
||||
|
||||
| Information | Details |
|
||||
|:--------------------|:----------------------------------------------------------|
|
||||
| Tutorial type | Hosting Tools |
|
||||
| Tool type | MCP server |
|
||||
| Tutorial components | CDK, AgentCore Runtime, MCP server |
|
||||
| Tutorial vertical | Cross-vertical |
|
||||
| Example complexity | Easy |
|
||||
| SDK used | Amazon BedrockAgentCore Python SDK and MCP Client |
|
||||
|
||||
### Architecture
|
||||
|
||||

|
||||
|
||||
This CDK stack deploys a simple MCP server with 3 tools: `add_numbers`, `multiply_numbers`, and `greet_user`.
|
||||
|
||||
The architecture consists of:
|
||||
|
||||
- **User/MCP Client**: Sends requests to the MCP server with JWT authentication
|
||||
- **Amazon Cognito**: Provides JWT-based authentication
|
||||
- User Pool with pre-created test user (testuser/MyPassword123!)
|
||||
- User Pool Client for application access
|
||||
- **AWS CodeBuild**: Builds the ARM64 Docker container image with the MCP server
|
||||
- **Amazon ECR Repository**: Stores the container image
|
||||
- **AgentCore Runtime**: Hosts the MCP Server
|
||||
- **MCP Server**: Exposes three tools via HTTP transport
|
||||
- `add_numbers`: Adds two numbers
|
||||
- `multiply_numbers`: Multiplies two numbers
|
||||
- `greet_user`: Greets a user by name
|
||||
- Validates JWT tokens from Cognito
|
||||
- Processes MCP tool invocations
|
||||
- **IAM Roles**:
|
||||
- IAM role for CodeBuild (builds and pushes images)
|
||||
- IAM role for AgentCore Runtime (runtime permissions)
|
||||
|
||||
### Key Features
|
||||
|
||||
* **Complete Infrastructure as Code** - Full CDK implementation
|
||||
* **Secure by Default** - JWT authentication with Cognito
|
||||
* **Automated Build** - CodeBuild creates ARM64 Docker images
|
||||
* **Easy Testing** - Automated test script included
|
||||
* **Simple Cleanup** - One command removes all resources
|
||||
|
||||
## What Gets Deployed
|
||||
|
||||
The CDK stack creates:
|
||||
|
||||
- **Amazon ECR Repository** - Stores the MCP server Docker image
|
||||
- **AWS CodeBuild Project** - Builds ARM64 Docker image automatically
|
||||
- **Amazon Cognito User Pool** - JWT authentication
|
||||
- **Cognito User Pool Client** - Application client configuration
|
||||
- **Cognito User** - Pre-created test user (testuser/MyPassword123!)
|
||||
- **IAM Roles** - Least-privilege permissions for all services
|
||||
- **Lambda Functions** - Custom resource automation
|
||||
- **Amazon Bedrock AgentCore Runtime** - Hosts the MCP server
|
||||
|
||||
**MCP Server Tools**:
|
||||
- `add_numbers` - Adds two numbers together
|
||||
- `multiply_numbers` - Multiplies two numbers
|
||||
- `greet_user` - Greets a user by name
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Architecture](#architecture)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Deployment](#deployment)
|
||||
- [Testing](#testing)
|
||||
- [Cleanup](#cleanup)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Overview
|
||||
|
||||
This CDK stack creates an MCP server with JWT authentication using Amazon Cognito. The MCP server provides three tools: `add_numbers`, `multiply_numbers`, and `greet_user`.
|
||||
|
||||
### Key Features
|
||||
|
||||
* **Complete Infrastructure as Code** - Full CDK implementation
|
||||
* **Secure by Default** - JWT authentication with Cognito
|
||||
* **Automated Build** - CodeBuild creates ARM64 Docker images
|
||||
* **Easy Testing** - Automated test script included
|
||||
* **Simple Cleanup** - One command removes all resources
|
||||
|
||||
## Architecture
|
||||
|
||||
The architecture consists of:
|
||||
|
||||
- **User/MCP Client**: Sends requests to the MCP server with JWT authentication
|
||||
- **Amazon Cognito**: Provides JWT-based authentication
|
||||
- User Pool with pre-created test user (testuser/MyPassword123!)
|
||||
- User Pool Client for application access
|
||||
- **AWS CodeBuild**: Builds the ARM64 Docker container image with the MCP server
|
||||
- **Amazon ECR Repository**: Stores the container image
|
||||
- **AgentCore Runtime**: Hosts the MCP Server
|
||||
- **MCP Server**: Exposes three tools via HTTP transport
|
||||
- `add_numbers`: Adds two numbers
|
||||
- `multiply_numbers`: Multiplies two numbers
|
||||
- `greet_user`: Greets a user by name
|
||||
- Validates JWT tokens from Cognito
|
||||
- Processes MCP tool invocations
|
||||
- **IAM Roles**:
|
||||
- IAM role for CodeBuild (builds and pushes images)
|
||||
- IAM role for AgentCore Runtime (runtime permissions)
|
||||
|
||||
## What Gets Deployed
|
||||
|
||||
The CDK stack creates:
|
||||
|
||||
- **Amazon ECR Repository** - Stores the MCP server Docker image
|
||||
- **AWS CodeBuild Project** - Builds ARM64 Docker image automatically
|
||||
- **Amazon Cognito User Pool** - JWT authentication
|
||||
- **Cognito User Pool Client** - Application client configuration
|
||||
- **Cognito User** - Pre-created test user (testuser/MyPassword123!)
|
||||
- **IAM Roles** - Least-privilege permissions for all services
|
||||
- **Lambda Functions** - Custom resource automation
|
||||
- **Amazon Bedrock AgentCore Runtime** - Hosts the MCP server
|
||||
|
||||
**MCP Server Tools**:
|
||||
- `add_numbers` - Adds two numbers together
|
||||
- `multiply_numbers` - Multiplies two numbers
|
||||
- `greet_user` - Greets a user by name
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- AWS CLI configured with appropriate credentials
|
||||
- AWS account with permissions to create:
|
||||
- CloudFormation stacks
|
||||
- ECR repositories
|
||||
- CodeBuild projects
|
||||
- Cognito User Pools
|
||||
- IAM roles and policies
|
||||
- Lambda functions
|
||||
- Bedrock AgentCore Runtime
|
||||
- Python 3.10+ and AWS CDK v2 installed
|
||||
- CDK version 2.220.0 or later (for BedrockAgentCore support)
|
||||
|
||||
## Deployment
|
||||
|
||||
### CDK vs CloudFormation
|
||||
|
||||
This is the **CDK version** of the MCP server AgentCore runtime. If you prefer CloudFormation, see the [CloudFormation version](../../cloudformation/mcp-server-agentcore-runtime/).
|
||||
|
||||
### Option 1: Quick Deploy (Recommended)
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Bootstrap CDK (first time only)
|
||||
cdk bootstrap
|
||||
|
||||
# Deploy
|
||||
cdk deploy
|
||||
```
|
||||
|
||||
### Option 2: Step by Step
|
||||
|
||||
```bash
|
||||
# 1. Create and activate Python virtual environment
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
||||
|
||||
# 2. Install Python dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 3. Bootstrap CDK in your account/region (first time only)
|
||||
cdk bootstrap
|
||||
|
||||
# 4. Synthesize the CloudFormation template (optional)
|
||||
cdk synth
|
||||
|
||||
# 5. Deploy the stack
|
||||
cdk deploy --require-approval never
|
||||
|
||||
# 6. Get outputs
|
||||
cdk list
|
||||
```
|
||||
|
||||
### Deployment Time
|
||||
|
||||
The deployment takes approximately **10-15 minutes** and includes:
|
||||
- Creating all AWS resources
|
||||
- Building the Docker image
|
||||
- Pushing to ECR
|
||||
- Starting the AgentCore Runtime
|
||||
|
||||
## Testing
|
||||
|
||||
### 1. Get Authentication Token
|
||||
|
||||
First, get a JWT token from Cognito:
|
||||
|
||||
```bash
|
||||
# Get the Cognito User Pool Client ID from CDK outputs
|
||||
CLIENT_ID=$(aws cloudformation describe-stacks \
|
||||
--stack-name MCPServerDemo \
|
||||
--region us-east-1 \
|
||||
--query 'Stacks[0].Outputs[?OutputKey==`CognitoUserPoolClientId`].OutputValue' \
|
||||
--output text)
|
||||
|
||||
# Get authentication token
|
||||
python get_token.py $CLIENT_ID testuser MyPassword123! us-east-1
|
||||
```
|
||||
|
||||
This will output a JWT token. Copy the token for the next step.
|
||||
|
||||
### 2. Test the MCP Server
|
||||
|
||||
```bash
|
||||
# Get the MCP Server Runtime ARN
|
||||
RUNTIME_ARN=$(aws cloudformation describe-stacks \
|
||||
--stack-name MCPServerDemo \
|
||||
--region us-east-1 \
|
||||
--query 'Stacks[0].Outputs[?OutputKey==`MCPServerRuntimeArn`].OutputValue' \
|
||||
--output text)
|
||||
|
||||
# Test the MCP server (replace YOUR_JWT_TOKEN with the token from step 1)
|
||||
python test_mcp_server.py $RUNTIME_ARN YOUR_JWT_TOKEN us-east-1
|
||||
```
|
||||
|
||||
This will:
|
||||
- Connect to the MCP server
|
||||
- List available tools
|
||||
- Test all three MCP tools
|
||||
- Display the results
|
||||
|
||||
### Expected Output
|
||||
|
||||
```
|
||||
🔄 Initializing MCP session...
|
||||
✓ MCP session initialized
|
||||
|
||||
🔄 Listing available tools...
|
||||
|
||||
📋 Available MCP Tools:
|
||||
==================================================
|
||||
🔧 add_numbers: Add two numbers together
|
||||
🔧 multiply_numbers: Multiply two numbers together
|
||||
🔧 greet_user: Greet a user by name
|
||||
|
||||
🧪 Testing MCP Tools:
|
||||
==================================================
|
||||
|
||||
➕ Testing add_numbers(5, 3)...
|
||||
Result: 8
|
||||
|
||||
✖️ Testing multiply_numbers(4, 7)...
|
||||
Result: 28
|
||||
|
||||
👋 Testing greet_user('Alice')...
|
||||
Result: Hello, Alice! Nice to meet you.
|
||||
|
||||
✅ MCP tool testing completed!
|
||||
```
|
||||
|
||||
## Cleanup
|
||||
|
||||
### Using CDK (Recommended)
|
||||
|
||||
```bash
|
||||
cdk destroy
|
||||
```
|
||||
|
||||
### Using AWS CLI
|
||||
|
||||
```bash
|
||||
aws cloudformation delete-stack \
|
||||
--stack-name MCPServerDemo \
|
||||
--region us-east-1
|
||||
|
||||
# Wait for deletion to complete
|
||||
aws cloudformation wait stack-delete-complete \
|
||||
--stack-name MCPServerDemo \
|
||||
--region us-east-1
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### CDK Bootstrap Required
|
||||
|
||||
If you see bootstrap errors:
|
||||
```bash
|
||||
cdk bootstrap aws://ACCOUNT-NUMBER/REGION
|
||||
```
|
||||
|
||||
### Permission Issues
|
||||
|
||||
Ensure your IAM user/role has:
|
||||
- `CDKToolkit` permissions or equivalent
|
||||
- Permissions to create all resources in the stack
|
||||
- `iam:PassRole` for service roles
|
||||
|
||||
### Python Dependencies
|
||||
|
||||
Install dependencies in the project directory:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
For testing, you may need additional packages:
|
||||
```bash
|
||||
pip install boto3 mcp
|
||||
```
|
||||
|
||||
### Authentication Issues
|
||||
|
||||
If authentication fails:
|
||||
- Verify the Cognito User Pool Client ID is correct
|
||||
- Ensure you're using the correct region
|
||||
- Check that the user exists and password is correct
|
||||
- Verify USER_PASSWORD_AUTH is enabled for the client
|
||||
|
||||
### Build Failures
|
||||
|
||||
Check CodeBuild logs in the AWS Console:
|
||||
1. Go to CodeBuild console
|
||||
2. Find the build project (name contains "mcp-server-build")
|
||||
3. Check build history and logs
|
||||
|
||||
## Cost Estimate
|
||||
|
||||
### Monthly Cost Breakdown (us-east-1)
|
||||
|
||||
| Service | Usage | Monthly Cost |
|
||||
|---------|-------|--------------|
|
||||
| **AgentCore Runtime** | 1 runtime, minimal usage | ~$5-10 |
|
||||
| **ECR Repository** | 1 repository, <1GB storage | ~$0.10 |
|
||||
| **CodeBuild** | Occasional builds | ~$1-2 |
|
||||
| **Lambda** | Custom resource executions | ~$0.01 |
|
||||
| **Cognito User Pool** | 1 user pool, minimal usage | ~$0.01 |
|
||||
| **CloudWatch Logs** | Agent logs | ~$0.50 |
|
||||
|
||||
**Estimated Total: ~$7-13/month**
|
||||
|
||||
### Cost Optimization Tips
|
||||
|
||||
- **Delete when not in use**: Use `cdk destroy` to remove all resources
|
||||
- **Monitor usage**: Set up CloudWatch billing alarms
|
||||
- **Optimize builds**: Only rebuild when code changes
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please see our [Contributing Guide](../../CONTRIBUTING.md) for details.
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](../../LICENSE) file for details.
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
import aws_cdk as cdk
|
||||
from mcp_server_stack import MCPServerStack
|
||||
|
||||
app = cdk.App()
|
||||
MCPServerStack(app, "MCPServerDemo")
|
||||
|
||||
app.synth()
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"app": "python3 app.py",
|
||||
"watch": {
|
||||
"include": [
|
||||
"**"
|
||||
],
|
||||
"exclude": [
|
||||
"README.md",
|
||||
"cdk*.json",
|
||||
"requirements*.txt",
|
||||
"source.bat",
|
||||
"**/__pycache__",
|
||||
"**/*.pyc"
|
||||
]
|
||||
},
|
||||
"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-iam:standardizedServicePrincipals": 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:enableLogging": 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:enableLogging": true,
|
||||
"@aws-cdk/aws-norh:enablePartitionLiterals": true,
|
||||
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
|
||||
"@aws-cdk/aws-iam:standardizedServicePrincipals": true,
|
||||
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
|
||||
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
|
||||
"@aws-cdk/aws-route53-patters:useCertificate": true,
|
||||
"@aws-cdk/customresources:installLatestAwsSdkDefault": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import aws_cdk.aws_bedrockagentcore as bedrockagentcore
|
||||
import inspect
|
||||
|
||||
print("=== Exploring aws_cdk.aws_bedrockagentcore module ===")
|
||||
print()
|
||||
|
||||
# List all attributes in the module
|
||||
print("Available classes and functions:")
|
||||
for name in dir(bedrockagentcore):
|
||||
if not name.startswith('_'):
|
||||
obj = getattr(bedrockagentcore, name)
|
||||
print(f" {name}: {type(obj)}")
|
||||
|
||||
print()
|
||||
print("=== CfnRuntime class details ===")
|
||||
|
||||
# Explore CfnRuntime class
|
||||
runtime_class = bedrockagentcore.CfnRuntime
|
||||
print(f"CfnRuntime: {runtime_class}")
|
||||
|
||||
print("\nCfnRuntime attributes:")
|
||||
for name in dir(runtime_class):
|
||||
if not name.startswith('_'):
|
||||
try:
|
||||
attr = getattr(runtime_class, name)
|
||||
print(f" {name}: {type(attr)}")
|
||||
except Exception as e:
|
||||
print(f" {name}: Error accessing - {e}")
|
||||
|
||||
print("\nLooking for Property classes:")
|
||||
for name in dir(bedrockagentcore):
|
||||
if 'Property' in name:
|
||||
obj = getattr(bedrockagentcore, name)
|
||||
print(f" {name}: {type(obj)}")
|
||||
|
||||
print("\nLooking for Authorizer related classes:")
|
||||
for name in dir(bedrockagentcore):
|
||||
if 'Auth' in name.lower():
|
||||
obj = getattr(bedrockagentcore, name)
|
||||
print(f" {name}: {type(obj)}")
|
||||
|
||||
print("\nLooking for JWT related classes:")
|
||||
for name in dir(bedrockagentcore):
|
||||
if 'jwt' in name.lower() or 'JWT' in name:
|
||||
obj = getattr(bedrockagentcore, name)
|
||||
print(f" {name}: {type(obj)}")
|
||||
|
||||
# Try to get the constructor signature
|
||||
print("\n=== CfnRuntime constructor signature ===")
|
||||
try:
|
||||
sig = inspect.signature(runtime_class.__init__)
|
||||
print(f"Constructor signature: {sig}")
|
||||
except Exception as e:
|
||||
print(f"Error getting signature: {e}")
|
||||
|
||||
# Try to explore the CfnRuntime properties
|
||||
print("\n=== Trying to find nested property classes ===")
|
||||
try:
|
||||
# Check if there are nested classes
|
||||
for name in dir(runtime_class):
|
||||
if not name.startswith('_') and name.endswith('Property'):
|
||||
prop_class = getattr(runtime_class, name)
|
||||
print(f" {name}: {prop_class}")
|
||||
|
||||
# Try to get the constructor signature
|
||||
try:
|
||||
prop_sig = inspect.signature(prop_class.__init__)
|
||||
print(f" Constructor: {prop_sig}")
|
||||
except Exception as e:
|
||||
print(f" Error getting constructor: {e}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error exploring nested classes: {e}")
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple Cognito Authentication Script
|
||||
Matches the approach from the original tutorial
|
||||
"""
|
||||
|
||||
import boto3
|
||||
import sys
|
||||
|
||||
|
||||
def get_token(client_id, username, password, region=None):
|
||||
"""Get authentication token from Cognito."""
|
||||
# Use provided region or default from environment/config
|
||||
if region:
|
||||
cognito_client = boto3.client("cognito-idp", region_name=region)
|
||||
else:
|
||||
cognito_client = boto3.client("cognito-idp")
|
||||
|
||||
try:
|
||||
auth_response = cognito_client.initiate_auth(
|
||||
ClientId=client_id,
|
||||
AuthFlow="USER_PASSWORD_AUTH",
|
||||
AuthParameters={"USERNAME": username, "PASSWORD": password},
|
||||
)
|
||||
|
||||
return auth_response["AuthenticationResult"]["AccessToken"]
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
print("Troubleshooting:")
|
||||
print(" - Verify the Client ID is correct")
|
||||
print(" - Ensure you're using the correct region")
|
||||
print(" - Check that the user exists and password is correct")
|
||||
print(" - Verify USER_PASSWORD_AUTH is enabled for this client")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 4 or len(sys.argv) > 5:
|
||||
print("Usage: python get_token.py <client_id> <username> <password> [region]")
|
||||
print("\nExamples:")
|
||||
print(" python get_token.py abc123xyz testuser MyPassword123!")
|
||||
print(" python get_token.py abc123xyz testuser MyPassword123! us-west-2")
|
||||
sys.exit(1)
|
||||
|
||||
client_id = sys.argv[1]
|
||||
username = sys.argv[2]
|
||||
password = sys.argv[3]
|
||||
region = sys.argv[4] if len(sys.argv) == 5 else None
|
||||
|
||||
if region:
|
||||
print(f"Authenticating with Cognito in region {region}...")
|
||||
else:
|
||||
print("Authenticating with Cognito...")
|
||||
|
||||
token = get_token(client_id, username, password, region)
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("Authentication Successful!")
|
||||
print("=" * 70)
|
||||
print("\nAccess Token:")
|
||||
print(token)
|
||||
print("\n" + "=" * 70)
|
||||
print("Export Command:")
|
||||
print("=" * 70)
|
||||
print(f'\nexport JWT_TOKEN="{token}"')
|
||||
print("\nThen use in curl:")
|
||||
print('curl -H "Authorization: Bearer $JWT_TOKEN" <your-api-url>')
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1 @@
|
||||
from .agentcore_role import AgentCoreRole
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
from aws_cdk import (
|
||||
aws_iam as iam,
|
||||
Stack
|
||||
)
|
||||
from constructs import Construct
|
||||
|
||||
class AgentCoreRole(iam.Role):
|
||||
def __init__(self, scope: Construct, construct_id: str, **kwargs):
|
||||
region = Stack.of(scope).region
|
||||
account_id = Stack.of(scope).account
|
||||
|
||||
super().__init__(scope, construct_id,
|
||||
assumed_by=iam.ServicePrincipal("bedrock-agentcore.amazonaws.com"),
|
||||
inline_policies={
|
||||
"AgentCorePolicy": iam.PolicyDocument(
|
||||
statements=[
|
||||
iam.PolicyStatement(
|
||||
sid="ECRImageAccess",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"ecr:BatchGetImage",
|
||||
"ecr:GetDownloadUrlForLayer",
|
||||
"ecr:BatchCheckLayerAvailability"
|
||||
],
|
||||
resources=[f"arn:aws:ecr:{region}:{account_id}:repository/*"]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="ECRTokenAccess",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=["ecr:GetAuthorizationToken"],
|
||||
resources=["*"]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"logs:DescribeLogStreams",
|
||||
"logs:CreateLogGroup",
|
||||
"logs:DescribeLogGroups",
|
||||
"logs:CreateLogStream",
|
||||
"logs:PutLogEvents"
|
||||
],
|
||||
resources=[f"arn:aws:logs:{region}:{account_id}:log-group:/aws/bedrock-agentcore/runtimes/*"]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"xray:PutTraceSegments",
|
||||
"xray:PutTelemetryRecords",
|
||||
"xray:GetSamplingRules",
|
||||
"xray:GetSamplingTargets"
|
||||
],
|
||||
resources=["*"]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=["cloudwatch:PutMetricData"],
|
||||
resources=["*"],
|
||||
conditions={
|
||||
"StringEquals": {
|
||||
"cloudwatch:namespace": "bedrock-agentcore"
|
||||
}
|
||||
}
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="GetAgentAccessToken",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"bedrock-agentcore:GetWorkloadAccessToken",
|
||||
"bedrock-agentcore:GetWorkloadAccessTokenForJWT",
|
||||
"bedrock-agentcore:GetWorkloadAccessTokenForUserId"
|
||||
],
|
||||
resources=[
|
||||
f"arn:aws:bedrock-agentcore:{region}:{account_id}:workload-identity-directory/default",
|
||||
f"arn:aws:bedrock-agentcore:{region}:{account_id}:workload-identity-directory/default/workload-identity/*"
|
||||
]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="BedrockModelInvocation",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"bedrock:InvokeModel",
|
||||
"bedrock:InvokeModelWithResponseStream"
|
||||
],
|
||||
resources=[
|
||||
"arn:aws:bedrock:*::foundation-model/*",
|
||||
f"arn:aws:bedrock:{region}:{account_id}:*"
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
},
|
||||
**kwargs
|
||||
)
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import boto3
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
import urllib3
|
||||
|
||||
# Note: cfnresponse is only available for inline Lambda code in CloudFormation.
|
||||
# When using CDK with Code.from_asset(), we need to include our own copy.
|
||||
# This is the standard AWS-provided cfnresponse module embedded directly.
|
||||
|
||||
class cfnresponse:
|
||||
SUCCESS = "SUCCESS"
|
||||
FAILED = "FAILED"
|
||||
|
||||
@staticmethod
|
||||
def send(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False, reason=None):
|
||||
responseUrl = event['ResponseURL']
|
||||
print(responseUrl)
|
||||
|
||||
responseBody = {
|
||||
'Status': responseStatus,
|
||||
'Reason': reason or "See the details in CloudWatch Log Stream: {}".format(context.log_stream_name),
|
||||
'PhysicalResourceId': physicalResourceId or context.log_stream_name,
|
||||
'StackId': event['StackId'],
|
||||
'RequestId': event['RequestId'],
|
||||
'LogicalResourceId': event['LogicalResourceId'],
|
||||
'NoEcho': noEcho,
|
||||
'Data': responseData
|
||||
}
|
||||
|
||||
json_responseBody = json.dumps(responseBody)
|
||||
print("Response body:")
|
||||
print(json_responseBody)
|
||||
|
||||
headers = {
|
||||
'content-type': '',
|
||||
'content-length': str(len(json_responseBody))
|
||||
}
|
||||
|
||||
try:
|
||||
http = urllib3.PoolManager()
|
||||
response = http.request('PUT', responseUrl, headers=headers, body=json_responseBody)
|
||||
print("Status code:", response.status)
|
||||
except Exception as e:
|
||||
print("send(..) failed executing http.request(..):", e)
|
||||
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
def handler(event, context):
|
||||
logger.info('Received event: %s', json.dumps(event))
|
||||
|
||||
try:
|
||||
if event['RequestType'] == 'Delete':
|
||||
cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
|
||||
return
|
||||
|
||||
project_name = event['ResourceProperties']['ProjectName']
|
||||
|
||||
codebuild = boto3.client('codebuild')
|
||||
|
||||
# Start build
|
||||
response = codebuild.start_build(projectName=project_name)
|
||||
build_id = response['build']['id']
|
||||
logger.info(f"Started build: {build_id}")
|
||||
|
||||
# Wait for completion
|
||||
max_wait_time = context.get_remaining_time_in_millis() / 1000 - 30
|
||||
start_time = time.time()
|
||||
|
||||
while True:
|
||||
if time.time() - start_time > max_wait_time:
|
||||
cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': 'Build timeout'})
|
||||
return
|
||||
|
||||
build_response = codebuild.batch_get_builds(ids=[build_id])
|
||||
build_status = build_response['builds'][0]['buildStatus']
|
||||
|
||||
if build_status == 'SUCCEEDED':
|
||||
logger.info(f"Build {build_id} succeeded")
|
||||
cfnresponse.send(event, context, cfnresponse.SUCCESS, {'BuildId': build_id})
|
||||
return
|
||||
elif build_status in ['FAILED', 'FAULT', 'STOPPED', 'TIMED_OUT']:
|
||||
logger.error(f"Build {build_id} failed with status: {build_status}")
|
||||
cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': f'Build failed: {build_status}'})
|
||||
return
|
||||
|
||||
logger.info(f"Build {build_id} status: {build_status}")
|
||||
time.sleep(30)
|
||||
|
||||
except Exception as e:
|
||||
logger.error('Error: %s', str(e))
|
||||
cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': str(e)})
|
||||
@@ -0,0 +1,494 @@
|
||||
from aws_cdk import (
|
||||
Stack,
|
||||
aws_ecr as ecr,
|
||||
aws_codebuild as codebuild,
|
||||
aws_iam as iam,
|
||||
aws_lambda as lambda_,
|
||||
aws_cognito as cognito,
|
||||
aws_bedrockagentcore as bedrockagentcore,
|
||||
CustomResource,
|
||||
CfnParameter,
|
||||
CfnOutput,
|
||||
Duration,
|
||||
RemovalPolicy
|
||||
)
|
||||
from constructs import Construct
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), 'infra_utils'))
|
||||
|
||||
class MCPServerStack(Stack):
|
||||
|
||||
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
|
||||
super().__init__(scope, construct_id, **kwargs)
|
||||
|
||||
# Parameters
|
||||
agent_name = CfnParameter(self, "AgentName",
|
||||
type="String",
|
||||
default="MCPServerAgent",
|
||||
description="Name for the MCP server runtime"
|
||||
)
|
||||
|
||||
image_tag = CfnParameter(self, "ImageTag",
|
||||
type="String",
|
||||
default="latest",
|
||||
description="Tag for the Docker image"
|
||||
)
|
||||
|
||||
network_mode = CfnParameter(self, "NetworkMode",
|
||||
type="String",
|
||||
default="PUBLIC",
|
||||
description="Network mode for AgentCore resources",
|
||||
allowed_values=["PUBLIC", "PRIVATE"]
|
||||
)
|
||||
|
||||
ecr_repository_name = CfnParameter(self, "ECRRepositoryName",
|
||||
type="String",
|
||||
default="mcp-server",
|
||||
description="Name of the ECR repository"
|
||||
)
|
||||
|
||||
# ECR Repository
|
||||
ecr_repository = ecr.Repository(self, "ECRRepository",
|
||||
repository_name=f"{self.stack_name.lower()}-{ecr_repository_name.value_as_string}",
|
||||
image_tag_mutability=ecr.TagMutability.MUTABLE,
|
||||
removal_policy=RemovalPolicy.DESTROY,
|
||||
empty_on_delete=True,
|
||||
image_scan_on_push=True
|
||||
)
|
||||
|
||||
# Cognito User Pool
|
||||
cognito_user_pool = cognito.UserPool(self, "CognitoUserPool",
|
||||
user_pool_name=f"{self.stack_name}-user-pool",
|
||||
password_policy=cognito.PasswordPolicy(
|
||||
min_length=8,
|
||||
require_uppercase=False,
|
||||
require_lowercase=False,
|
||||
require_digits=False,
|
||||
require_symbols=False
|
||||
),
|
||||
standard_attributes=cognito.StandardAttributes(
|
||||
email=cognito.StandardAttribute(required=False, mutable=True)
|
||||
)
|
||||
)
|
||||
|
||||
# Cognito User Pool Client
|
||||
cognito_user_pool_client = cognito.CfnUserPoolClient(self, "CognitoUserPoolClient",
|
||||
client_name=f"{self.stack_name}-client",
|
||||
user_pool_id=cognito_user_pool.user_pool_id,
|
||||
generate_secret=False,
|
||||
explicit_auth_flows=[
|
||||
"ALLOW_USER_PASSWORD_AUTH",
|
||||
"ALLOW_REFRESH_TOKEN_AUTH"
|
||||
],
|
||||
prevent_user_existence_errors="ENABLED"
|
||||
)
|
||||
|
||||
# Cognito User
|
||||
cognito_user = cognito.CfnUserPoolUser(self, "CognitoUser",
|
||||
user_pool_id=cognito_user_pool.user_pool_id,
|
||||
username="testuser",
|
||||
message_action="SUPPRESS"
|
||||
)
|
||||
# IAM Roles
|
||||
# Agent Execution Role
|
||||
agent_execution_role = iam.Role(self, "AgentExecutionRole",
|
||||
role_name=f"{self.stack_name}-agent-execution-role",
|
||||
assumed_by=iam.ServicePrincipal("bedrock-agentcore.amazonaws.com"),
|
||||
managed_policies=[
|
||||
iam.ManagedPolicy.from_aws_managed_policy_name("BedrockAgentCoreFullAccess")
|
||||
],
|
||||
inline_policies={
|
||||
"AgentCoreExecutionPolicy": iam.PolicyDocument(
|
||||
statements=[
|
||||
iam.PolicyStatement(
|
||||
sid="ECRImageAccess",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"ecr:BatchGetImage",
|
||||
"ecr:GetDownloadUrlForLayer",
|
||||
"ecr:BatchCheckLayerAvailability"
|
||||
],
|
||||
resources=[ecr_repository.repository_arn]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="ECRTokenAccess",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=["ecr:GetAuthorizationToken"],
|
||||
resources=["*"]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="CloudWatchLogs",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"logs:DescribeLogStreams",
|
||||
"logs:CreateLogGroup",
|
||||
"logs:DescribeLogGroups",
|
||||
"logs:CreateLogStream",
|
||||
"logs:PutLogEvents"
|
||||
],
|
||||
resources=["*"]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="XRayTracing",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"xray:PutTraceSegments",
|
||||
"xray:PutTelemetryRecords",
|
||||
"xray:GetSamplingRules",
|
||||
"xray:GetSamplingTargets"
|
||||
],
|
||||
resources=["*"]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="CloudWatchMetrics",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=["cloudwatch:PutMetricData"],
|
||||
resources=["*"],
|
||||
conditions={
|
||||
"StringEquals": {
|
||||
"cloudwatch:namespace": "bedrock-agentcore"
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
# CodeBuild Service Role
|
||||
codebuild_role = iam.Role(self, "CodeBuildRole",
|
||||
role_name=f"{self.stack_name}-codebuild-role",
|
||||
assumed_by=iam.ServicePrincipal("codebuild.amazonaws.com"),
|
||||
inline_policies={
|
||||
"CodeBuildPolicy": iam.PolicyDocument(
|
||||
statements=[
|
||||
iam.PolicyStatement(
|
||||
sid="CloudWatchLogs",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"logs:CreateLogGroup",
|
||||
"logs:CreateLogStream",
|
||||
"logs:PutLogEvents"
|
||||
],
|
||||
resources=[f"arn:aws:logs:{self.region}:{self.account}:log-group:/aws/codebuild/*"]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="ECRAccess",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"ecr:BatchCheckLayerAvailability",
|
||||
"ecr:GetDownloadUrlForLayer",
|
||||
"ecr:BatchGetImage",
|
||||
"ecr:GetAuthorizationToken",
|
||||
"ecr:PutImage",
|
||||
"ecr:InitiateLayerUpload",
|
||||
"ecr:UploadLayerPart",
|
||||
"ecr:CompleteLayerUpload"
|
||||
],
|
||||
resources=[ecr_repository.repository_arn, "*"]
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
# Lambda Custom Resource Role
|
||||
custom_resource_role = iam.Role(self, "CustomResourceRole",
|
||||
role_name=f"{self.stack_name}-custom-resource-role",
|
||||
assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"),
|
||||
managed_policies=[
|
||||
iam.ManagedPolicy.from_aws_managed_policy_name("service-role/AWSLambdaBasicExecutionRole")
|
||||
],
|
||||
inline_policies={
|
||||
"CustomResourcePolicy": iam.PolicyDocument(
|
||||
statements=[
|
||||
iam.PolicyStatement(
|
||||
sid="CodeBuildAccess",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"codebuild:StartBuild",
|
||||
"codebuild:BatchGetBuilds",
|
||||
"codebuild:BatchGetProjects"
|
||||
],
|
||||
resources=["*"] # Will be updated after CodeBuild project is created
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="CognitoAccess",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=["cognito-idp:AdminSetUserPassword"],
|
||||
resources=[cognito_user_pool.user_pool_arn]
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
)
|
||||
# Lambda Functions
|
||||
# CodeBuild Trigger Function
|
||||
codebuild_trigger_function = lambda_.Function(self, "CodeBuildTriggerFunction",
|
||||
function_name=f"{self.stack_name}-codebuild-trigger",
|
||||
runtime=lambda_.Runtime.PYTHON_3_9,
|
||||
handler="build_trigger_lambda.handler",
|
||||
code=lambda_.Code.from_asset(os.path.join(os.path.dirname(__file__), "infra_utils")),
|
||||
timeout=Duration.minutes(15),
|
||||
role=custom_resource_role,
|
||||
description="Triggers CodeBuild projects as CloudFormation custom resource"
|
||||
)
|
||||
|
||||
# Cognito Password Setter Function
|
||||
cognito_password_setter_function = lambda_.Function(self, "CognitoPasswordSetterFunction",
|
||||
function_name=f"{self.stack_name}-cognito-password-setter",
|
||||
runtime=lambda_.Runtime.PYTHON_3_9,
|
||||
handler="index.handler",
|
||||
timeout=Duration.minutes(5),
|
||||
role=custom_resource_role,
|
||||
description="Sets Cognito user password",
|
||||
code=lambda_.Code.from_inline("""
|
||||
import boto3
|
||||
import cfnresponse
|
||||
import json
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
def handler(event, context):
|
||||
logger.info('Received event: %s', json.dumps(event))
|
||||
|
||||
try:
|
||||
if event['RequestType'] == 'Delete':
|
||||
cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
|
||||
return
|
||||
|
||||
user_pool_id = event['ResourceProperties']['UserPoolId']
|
||||
username = event['ResourceProperties']['Username']
|
||||
password = event['ResourceProperties']['Password']
|
||||
|
||||
cognito = boto3.client('cognito-idp')
|
||||
|
||||
# Set permanent password
|
||||
cognito.admin_set_user_password(
|
||||
UserPoolId=user_pool_id,
|
||||
Username=username,
|
||||
Password=password,
|
||||
Permanent=True
|
||||
)
|
||||
|
||||
logger.info(f"Password set successfully for user: {username}")
|
||||
|
||||
cfnresponse.send(event, context, cfnresponse.SUCCESS, {
|
||||
'Status': 'SUCCESS'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error('Error: %s', str(e))
|
||||
cfnresponse.send(event, context, cfnresponse.FAILED, {
|
||||
'Error': str(e)
|
||||
})
|
||||
""")
|
||||
)
|
||||
# CodeBuild Project for MCP Server
|
||||
mcp_server_build_project = codebuild.Project(self, "MCPServerImageBuildProject",
|
||||
project_name=f"{self.stack_name}-mcp-server-build",
|
||||
description=f"Build MCP server Docker image for {self.stack_name}",
|
||||
role=codebuild_role,
|
||||
environment=codebuild.BuildEnvironment(
|
||||
build_image=codebuild.LinuxArmBuildImage.AMAZON_LINUX_2_STANDARD_3_0,
|
||||
compute_type=codebuild.ComputeType.LARGE,
|
||||
privileged=True
|
||||
),
|
||||
environment_variables={
|
||||
"AWS_DEFAULT_REGION": codebuild.BuildEnvironmentVariable(value=self.region),
|
||||
"AWS_ACCOUNT_ID": codebuild.BuildEnvironmentVariable(value=self.account),
|
||||
"IMAGE_REPO_NAME": codebuild.BuildEnvironmentVariable(value=ecr_repository.repository_name),
|
||||
"IMAGE_TAG": codebuild.BuildEnvironmentVariable(value=image_tag.value_as_string),
|
||||
"STACK_NAME": codebuild.BuildEnvironmentVariable(value=self.stack_name)
|
||||
},
|
||||
build_spec=codebuild.BuildSpec.from_object({
|
||||
"version": "0.2",
|
||||
"phases": {
|
||||
"pre_build": {
|
||||
"commands": [
|
||||
"echo Logging in to Amazon ECR...",
|
||||
"aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com"
|
||||
]
|
||||
},
|
||||
"build": {
|
||||
"commands": [
|
||||
"echo Build started on `date`",
|
||||
"echo Building the Docker image for MCP server ARM64...",
|
||||
# Create requirements.txt
|
||||
"""cat > requirements.txt << 'EOF'
|
||||
mcp>=1.10.0
|
||||
boto3
|
||||
bedrock-agentcore
|
||||
EOF""",
|
||||
# Create mcp_server.py
|
||||
"""cat > mcp_server.py << 'EOF'
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
mcp = FastMCP(host="0.0.0.0", stateless_http=True)
|
||||
|
||||
@mcp.tool()
|
||||
def add_numbers(a: int, b: int) -> int:
|
||||
\"\"\"Add two numbers together\"\"\"
|
||||
return a + b
|
||||
|
||||
@mcp.tool()
|
||||
def multiply_numbers(a: int, b: int) -> int:
|
||||
\"\"\"Multiply two numbers together\"\"\"
|
||||
return a * b
|
||||
|
||||
@mcp.tool()
|
||||
def greet_user(name: str) -> str:
|
||||
\"\"\"Greet a user by name\"\"\"
|
||||
return f"Hello, {name}! Nice to meet you."
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run(transport="streamable-http")
|
||||
EOF""",
|
||||
# Create Dockerfile
|
||||
"""cat > Dockerfile << 'EOF'
|
||||
FROM public.ecr.aws/docker/library/python:3.11-slim
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt requirements.txt
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
ENV AWS_REGION=us-west-2
|
||||
ENV AWS_DEFAULT_REGION=us-west-2
|
||||
|
||||
# Create non-root user
|
||||
RUN useradd -m -u 1000 bedrock_agentcore
|
||||
USER bedrock_agentcore
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD ["python", "-m", "mcp_server"]
|
||||
EOF""",
|
||||
"echo Building ARM64 image...",
|
||||
"docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG .",
|
||||
"docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG"
|
||||
]
|
||||
},
|
||||
"post_build": {
|
||||
"commands": [
|
||||
"echo Build completed on `date`",
|
||||
"echo Pushing the Docker image...",
|
||||
"docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG",
|
||||
"echo ARM64 Docker image pushed successfully"
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
# Custom Resources
|
||||
# Set Cognito User Password
|
||||
set_cognito_user_password = CustomResource(self, "SetCognitoUserPassword",
|
||||
service_token=cognito_password_setter_function.function_arn,
|
||||
properties={
|
||||
"UserPoolId": cognito_user_pool.user_pool_id,
|
||||
"Username": "testuser",
|
||||
"Password": "MyPassword123!"
|
||||
}
|
||||
)
|
||||
set_cognito_user_password.node.add_dependency(cognito_user)
|
||||
|
||||
# Trigger Image Build
|
||||
trigger_image_build = CustomResource(self, "TriggerImageBuild",
|
||||
service_token=codebuild_trigger_function.function_arn,
|
||||
properties={
|
||||
"ProjectName": mcp_server_build_project.project_name,
|
||||
"WaitForCompletion": "true"
|
||||
}
|
||||
)
|
||||
trigger_image_build.node.add_dependency(ecr_repository)
|
||||
trigger_image_build.node.add_dependency(mcp_server_build_project)
|
||||
|
||||
# MCP Server Runtime
|
||||
mcp_server_runtime = bedrockagentcore.CfnRuntime(self, "MCPServerRuntime",
|
||||
agent_runtime_name=f"{self.stack_name.replace('-', '_')}_{agent_name.value_as_string}",
|
||||
agent_runtime_artifact=bedrockagentcore.CfnRuntime.AgentRuntimeArtifactProperty(
|
||||
container_configuration=bedrockagentcore.CfnRuntime.ContainerConfigurationProperty(
|
||||
container_uri=f"{ecr_repository.repository_uri}:{image_tag.value_as_string}"
|
||||
)
|
||||
),
|
||||
role_arn=agent_execution_role.role_arn,
|
||||
network_configuration=bedrockagentcore.CfnRuntime.NetworkConfigurationProperty(
|
||||
network_mode=network_mode.value_as_string
|
||||
),
|
||||
protocol_configuration="MCP",
|
||||
authorizer_configuration=bedrockagentcore.CfnRuntime.AuthorizerConfigurationProperty(
|
||||
custom_jwt_authorizer=bedrockagentcore.CfnRuntime.CustomJWTAuthorizerConfigurationProperty(
|
||||
allowed_clients=[cognito_user_pool_client.ref],
|
||||
discovery_url=f"https://cognito-idp.{self.region}.amazonaws.com/{cognito_user_pool.user_pool_id}/.well-known/openid-configuration"
|
||||
)
|
||||
),
|
||||
description=f"MCP server runtime for {self.stack_name}"
|
||||
)
|
||||
mcp_server_runtime.node.add_dependency(trigger_image_build)
|
||||
# Outputs
|
||||
CfnOutput(self, "MCPServerRuntimeId",
|
||||
description="ID of the created MCP server runtime",
|
||||
value=mcp_server_runtime.attr_agent_runtime_id,
|
||||
export_name=f"{self.stack_name}-MCPServerRuntimeId"
|
||||
)
|
||||
|
||||
CfnOutput(self, "MCPServerRuntimeArn",
|
||||
description="ARN of the created MCP server runtime",
|
||||
value=mcp_server_runtime.attr_agent_runtime_arn,
|
||||
export_name=f"{self.stack_name}-MCPServerRuntimeArn"
|
||||
)
|
||||
|
||||
CfnOutput(self, "MCPServerInvocationURL",
|
||||
description="URL to invoke the MCP server",
|
||||
value=f"https://bedrock-agentcore.{self.region}.amazonaws.com/runtimes/ENCODED_ARN/invocations?qualifier=DEFAULT"
|
||||
)
|
||||
|
||||
CfnOutput(self, "ECRRepositoryUri",
|
||||
description="URI of the ECR repository",
|
||||
value=ecr_repository.repository_uri,
|
||||
export_name=f"{self.stack_name}-ECRRepositoryUri"
|
||||
)
|
||||
|
||||
CfnOutput(self, "AgentExecutionRoleArn",
|
||||
description="ARN of the agent execution role",
|
||||
value=agent_execution_role.role_arn,
|
||||
export_name=f"{self.stack_name}-AgentExecutionRoleArn"
|
||||
)
|
||||
|
||||
CfnOutput(self, "CognitoUserPoolId",
|
||||
description="ID of the Cognito User Pool",
|
||||
value=cognito_user_pool.user_pool_id,
|
||||
export_name=f"{self.stack_name}-CognitoUserPoolId"
|
||||
)
|
||||
|
||||
CfnOutput(self, "CognitoUserPoolClientId",
|
||||
description="ID of the Cognito User Pool Client",
|
||||
value=cognito_user_pool_client.ref,
|
||||
export_name=f"{self.stack_name}-CognitoUserPoolClientId"
|
||||
)
|
||||
|
||||
CfnOutput(self, "CognitoDiscoveryUrl",
|
||||
description="Cognito OIDC Discovery URL",
|
||||
value=f"https://cognito-idp.{self.region}.amazonaws.com/{cognito_user_pool.user_pool_id}/.well-known/openid-configuration",
|
||||
export_name=f"{self.stack_name}-CognitoDiscoveryUrl"
|
||||
)
|
||||
|
||||
CfnOutput(self, "TestUsername",
|
||||
description="Test username for authentication",
|
||||
value="testuser"
|
||||
)
|
||||
|
||||
CfnOutput(self, "TestPassword",
|
||||
description="Test password for authentication",
|
||||
value="MyPassword123!"
|
||||
)
|
||||
|
||||
CfnOutput(self, "GetTokenCommand",
|
||||
description="Command to get authentication token",
|
||||
value=f"python get_token.py {cognito_user_pool_client.ref} testuser MyPassword123!"
|
||||
)
|
||||
@@ -0,0 +1,2 @@
|
||||
aws-cdk-lib==2.220.0
|
||||
constructs>=10.0.79
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for deployed MCP server
|
||||
Uses the MCP Python client library to properly communicate with the server
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from datetime import timedelta
|
||||
from mcp import ClientSession
|
||||
from mcp.client.streamable_http import streamablehttp_client
|
||||
|
||||
|
||||
async def test_mcp_server(agent_arn, bearer_token, region):
|
||||
"""Test the deployed MCP server."""
|
||||
|
||||
# Encode the ARN for URL
|
||||
encoded_arn = agent_arn.replace(":", "%3A").replace("/", "%2F")
|
||||
mcp_url = f"https://bedrock-agentcore.{region}.amazonaws.com/runtimes/{encoded_arn}/invocations?qualifier=DEFAULT"
|
||||
|
||||
headers = {
|
||||
"authorization": f"Bearer {bearer_token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
print(f"Connecting to: {mcp_url}")
|
||||
print()
|
||||
|
||||
try:
|
||||
async with streamablehttp_client(
|
||||
mcp_url, headers, timeout=timedelta(seconds=120), terminate_on_close=False
|
||||
) as (read_stream, write_stream, _):
|
||||
async with ClientSession(read_stream, write_stream) as session:
|
||||
print("🔄 Initializing MCP session...")
|
||||
await session.initialize()
|
||||
print("✓ MCP session initialized\n")
|
||||
|
||||
print("🔄 Listing available tools...")
|
||||
tool_result = await session.list_tools()
|
||||
|
||||
print("\n📋 Available MCP Tools:")
|
||||
print("=" * 50)
|
||||
for tool in tool_result.tools:
|
||||
print(f"🔧 {tool.name}: {tool.description}")
|
||||
|
||||
print("\n🧪 Testing MCP Tools:")
|
||||
print("=" * 50)
|
||||
|
||||
# Test add_numbers
|
||||
print("\n➕ Testing add_numbers(5, 3)...")
|
||||
add_result = await session.call_tool(
|
||||
name="add_numbers", arguments={"a": 5, "b": 3}
|
||||
)
|
||||
print(f" Result: {add_result.content[0].text}")
|
||||
|
||||
# Test multiply_numbers
|
||||
print("\n✖️ Testing multiply_numbers(4, 7)...")
|
||||
multiply_result = await session.call_tool(
|
||||
name="multiply_numbers", arguments={"a": 4, "b": 7}
|
||||
)
|
||||
print(f" Result: {multiply_result.content[0].text}")
|
||||
|
||||
# Test greet_user
|
||||
print("\n👋 Testing greet_user('Alice')...")
|
||||
greet_result = await session.call_tool(
|
||||
name="greet_user", arguments={"name": "Alice"}
|
||||
)
|
||||
print(f" Result: {greet_result.content[0].text}")
|
||||
|
||||
print("\n✅ MCP tool testing completed!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 4:
|
||||
print("Usage: python test_mcp_server.py <agent_arn> <bearer_token> <region>")
|
||||
print("\nExample:")
|
||||
print(
|
||||
" python test_mcp_server.py arn:aws:bedrock-agentcore:... eyJraWQiOiJ... us-west-2"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
agent_arn = sys.argv[1]
|
||||
bearer_token = sys.argv[2]
|
||||
region = sys.argv[3]
|
||||
|
||||
asyncio.run(test_mcp_server(agent_arn, bearer_token, region))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,367 @@
|
||||
# Multi-Agent AgentCore Runtime - CDK
|
||||
|
||||
This CDK stack demonstrates a multi-agent architecture where one agent (orchestrator) can invoke another agent (specialist) to handle complex tasks. This pattern is useful for building sophisticated AI systems with specialized capabilities.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Architecture](#architecture)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Deployment](#deployment)
|
||||
- [Testing](#testing)
|
||||
- [Sample Queries](#sample-queries)
|
||||
- [Cleanup](#cleanup)
|
||||
- [Cost Estimate](#cost-estimate)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [🤝 Contributing](#-contributing)
|
||||
- [📄 License](#-license)
|
||||
|
||||
## Overview
|
||||
|
||||
This CDK stack creates a two-agent system that demonstrates agent-to-agent communication:
|
||||
|
||||
### Agent 1: Orchestrator Agent
|
||||
- **Role**: Main entry point for user queries
|
||||
- **Capabilities**:
|
||||
- Handles simple queries directly
|
||||
- Delegates complex tasks to Agent 2
|
||||
- Has a tool to invoke Agent 2's runtime
|
||||
- **Use Cases**: Routing, task delegation, simple Q&A
|
||||
|
||||
### Agent 2: Specialist Agent
|
||||
- **Role**: Expert agent for detailed analysis
|
||||
- **Capabilities**:
|
||||
- Provides in-depth analytical responses
|
||||
- Handles complex reasoning tasks
|
||||
- Focuses on accuracy and completeness
|
||||
- **Use Cases**: Data analysis, expert knowledge, detailed explanations
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Multi-Agent Communication**: Agent 1 can invoke Agent 2 using `bedrock-agentcore:InvokeAgentRuntime`
|
||||
- **Automatic Orchestration**: Agent 1 decides when to delegate based on query complexity
|
||||
- **Independent Deployment**: Each agent has its own ECR repository and runtime
|
||||
- **Modular Architecture**: Easy to extend with additional specialized agents
|
||||
|
||||
## Architecture
|
||||
|
||||

|
||||
|
||||
The architecture consists of:
|
||||
|
||||
- **User**: Sends questions to Agent 1 (Orchestrator) and receives responses
|
||||
- **Agent 1 - Orchestrator Agent**:
|
||||
- **AWS CodeBuild**: Builds the ARM64 Docker container image for Agent 1
|
||||
- **Amazon ECR Repository**: Stores Agent 1's container image
|
||||
- **AgentCore Runtime**: Hosts the Orchestrator Agent
|
||||
- Routes simple queries directly
|
||||
- Delegates complex queries to Agent 2 using the `call_specialist_agent` tool
|
||||
- Invokes Amazon Bedrock LLMs for reasoning
|
||||
- **IAM Role**: Permissions to invoke Agent 2's runtime and access Bedrock
|
||||
- **Agent 2 - Specialist Agent**:
|
||||
- **AWS CodeBuild**: Builds the ARM64 Docker container image for Agent 2
|
||||
- **Amazon ECR Repository**: Stores Agent 2's container image
|
||||
- **AgentCore Runtime**: Hosts the Specialist Agent
|
||||
- Provides detailed analysis and expert responses
|
||||
- Invokes Amazon Bedrock LLMs for in-depth reasoning
|
||||
- **IAM Role**: Standard runtime permissions and Bedrock access
|
||||
- **Amazon Bedrock LLMs**: Provides AI model capabilities for both agents
|
||||
- **Agent-to-Agent Communication**: Agent 1 can invoke Agent 2's runtime via `bedrock-agentcore:InvokeAgentRuntime` API
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### AWS Account Setup
|
||||
|
||||
1. **AWS Account**: You need an active AWS account with appropriate permissions
|
||||
- [Create AWS Account](https://aws.amazon.com/account/)
|
||||
- [AWS Console Access](https://aws.amazon.com/console/)
|
||||
|
||||
2. **AWS CLI**: Install and configure AWS CLI with your credentials
|
||||
- [Install AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)
|
||||
- [Configure AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html)
|
||||
|
||||
```bash
|
||||
aws configure
|
||||
```
|
||||
|
||||
3. **Python 3.10+** and **AWS CDK v2** installed
|
||||
```bash
|
||||
# Install CDK
|
||||
npm install -g aws-cdk
|
||||
|
||||
# Verify installation
|
||||
cdk --version
|
||||
```
|
||||
|
||||
4. **CDK version 2.220.0 or later** (for BedrockAgentCore support)
|
||||
|
||||
5. **Bedrock Model Access**: Enable access to Amazon Bedrock models in your AWS region
|
||||
- Navigate to [Amazon Bedrock Console](https://console.aws.amazon.com/bedrock/)
|
||||
- Go to "Model access" and request access to:
|
||||
- Anthropic Claude models
|
||||
- [Bedrock Model Access Guide](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html)
|
||||
|
||||
6. **Required Permissions**: Your AWS user/role needs permissions for:
|
||||
- CloudFormation stack operations
|
||||
- ECR repository management
|
||||
- IAM role creation
|
||||
- Lambda function creation
|
||||
- CodeBuild project creation
|
||||
- BedrockAgentCore resource creation
|
||||
|
||||
## Deployment
|
||||
|
||||
### CDK vs CloudFormation
|
||||
|
||||
This is the **CDK version** of the multi-agent runtime. If you prefer CloudFormation, see the [CloudFormation version](../../cloudformation/multi-agent-runtime/).
|
||||
|
||||
### Option 1: Quick Deploy (Recommended)
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Bootstrap CDK (first time only)
|
||||
cdk bootstrap
|
||||
|
||||
# Deploy
|
||||
cdk deploy
|
||||
```
|
||||
|
||||
### Option 2: Step by Step
|
||||
|
||||
```bash
|
||||
# 1. Create and activate Python virtual environment
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
||||
|
||||
# 2. Install Python dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 3. Bootstrap CDK in your account/region (first time only)
|
||||
cdk bootstrap
|
||||
|
||||
# 4. Synthesize the CloudFormation template (optional)
|
||||
cdk synth
|
||||
|
||||
# 5. Deploy the stack
|
||||
cdk deploy --require-approval never
|
||||
|
||||
# 6. Get outputs
|
||||
cdk list
|
||||
```
|
||||
|
||||
### Deployment Time
|
||||
|
||||
- **Expected Duration**: 15-20 minutes
|
||||
- **Main Steps**:
|
||||
- Stack creation: ~2 minutes
|
||||
- Docker image builds (CodeBuild): ~10-12 minutes
|
||||
- Runtime provisioning: ~3-5 minutes
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Agent 1 (Orchestrator)
|
||||
|
||||
Agent 1 is your main entry point. It will handle simple queries directly or delegate to Agent 2 for complex tasks.
|
||||
|
||||
#### Using AWS CLI
|
||||
|
||||
```bash
|
||||
# Get Agent1 Runtime ID
|
||||
AGENT1_ID=$(aws cloudformation describe-stacks \
|
||||
--stack-name MultiAgentDemo \
|
||||
--region us-east-1 \
|
||||
--query 'Stacks[0].Outputs[?OutputKey==`Agent1RuntimeId`].OutputValue' \
|
||||
--output text)
|
||||
|
||||
# Test with a simple query (Agent1 handles directly)
|
||||
aws bedrock-agentcore invoke-agent-runtime \
|
||||
--agent-runtime-id $AGENT1_ID \
|
||||
--qualifier DEFAULT \
|
||||
--payload '{"prompt": "Hello, how are you?"}' \
|
||||
--region us-east-1 \
|
||||
response.json
|
||||
|
||||
# Test with a complex query (Agent1 delegates to Agent2)
|
||||
aws bedrock-agentcore invoke-agent-runtime \
|
||||
--agent-runtime-id $AGENT1_ID \
|
||||
--qualifier DEFAULT \
|
||||
--payload '{"prompt": "Provide a detailed analysis of cloud computing benefits"}' \
|
||||
--region us-east-1 \
|
||||
response.json
|
||||
|
||||
cat response.json
|
||||
```
|
||||
|
||||
### Using AWS Console
|
||||
|
||||
1. Navigate to [Bedrock AgentCore Console](https://console.aws.amazon.com/bedrock-agentcore/)
|
||||
2. Go to "Runtimes" in the left navigation
|
||||
3. Find Agent1 runtime (name starts with `MultiAgentDemo_OrchestratorAgent`)
|
||||
4. Click on the runtime name
|
||||
5. Click "Test" button
|
||||
6. Enter test payload:
|
||||
```json
|
||||
{
|
||||
"prompt": "Hello, how are you?"
|
||||
}
|
||||
```
|
||||
7. Click "Invoke"
|
||||
|
||||
### Test Agent 2 (Specialist) Directly
|
||||
|
||||
You can also test Agent 2 directly to see its specialized capabilities.
|
||||
|
||||
```bash
|
||||
# Get Agent2 Runtime ID
|
||||
AGENT2_ID=$(aws cloudformation describe-stacks \
|
||||
--stack-name MultiAgentDemo \
|
||||
--region us-east-1 \
|
||||
--query 'Stacks[0].Outputs[?OutputKey==`Agent2RuntimeId`].OutputValue' \
|
||||
--output text)
|
||||
|
||||
# Invoke Agent2 directly
|
||||
aws bedrock-agentcore invoke-agent-runtime \
|
||||
--agent-runtime-id $AGENT2_ID \
|
||||
--qualifier DEFAULT \
|
||||
--payload '{"prompt": "Explain quantum computing in detail"}' \
|
||||
--region us-east-1 \
|
||||
response.json
|
||||
```
|
||||
|
||||
## Sample Queries
|
||||
|
||||
### Queries that Agent 1 Handles Directly
|
||||
|
||||
These simple queries don't require specialist knowledge:
|
||||
|
||||
1. **Greetings**:
|
||||
```json
|
||||
{"prompt": "Hello, how are you?"}
|
||||
```
|
||||
|
||||
2. **Simple Math**:
|
||||
```json
|
||||
{"prompt": "What is 5 + 3?"}
|
||||
```
|
||||
|
||||
### Queries that Trigger Agent 2 Delegation
|
||||
|
||||
These complex queries require expert analysis:
|
||||
|
||||
1. **Detailed Analysis**:
|
||||
```json
|
||||
{"prompt": "Provide a detailed analysis of the benefits and drawbacks of serverless architecture"}
|
||||
```
|
||||
|
||||
2. **Expert Knowledge**:
|
||||
```json
|
||||
{"prompt": "Explain the CAP theorem and its implications for distributed systems"}
|
||||
```
|
||||
|
||||
3. **Complex Reasoning**:
|
||||
```json
|
||||
{"prompt": "Compare and contrast different machine learning algorithms for time series forecasting"}
|
||||
```
|
||||
|
||||
4. **In-depth Explanation**:
|
||||
```json
|
||||
{"prompt": "Provide expert analysis on best practices for securing cloud infrastructure"}
|
||||
```
|
||||
|
||||
## Cleanup
|
||||
|
||||
### Using CDK (Recommended)
|
||||
|
||||
```bash
|
||||
cdk destroy
|
||||
```
|
||||
|
||||
### Using AWS CLI
|
||||
|
||||
```bash
|
||||
aws cloudformation delete-stack \
|
||||
--stack-name MultiAgentDemo \
|
||||
--region us-east-1
|
||||
|
||||
# Wait for deletion to complete
|
||||
aws cloudformation wait stack-delete-complete \
|
||||
--stack-name MultiAgentDemo \
|
||||
--region us-east-1
|
||||
```
|
||||
|
||||
### Using AWS Console
|
||||
|
||||
1. Navigate to [CloudFormation Console](https://console.aws.amazon.com/cloudformation/)
|
||||
2. Select the `MultiAgentDemo` stack
|
||||
3. Click "Delete"
|
||||
4. Confirm deletion
|
||||
|
||||
## Cost Estimate
|
||||
|
||||
### Monthly Cost Breakdown (us-east-1)
|
||||
|
||||
| Service | Usage | Monthly Cost |
|
||||
|---------|-------|--------------|
|
||||
| **AgentCore Runtimes** | 2 runtimes, minimal usage | ~$10-20 |
|
||||
| **ECR Repositories** | 2 repositories, <2GB storage | ~$0.20 |
|
||||
| **CodeBuild** | Occasional builds | ~$2-4 |
|
||||
| **Lambda** | Custom resource executions | ~$0.01 |
|
||||
| **CloudWatch Logs** | Agent logs | ~$1.00 |
|
||||
| **Bedrock Model Usage** | Pay per token | Variable* |
|
||||
|
||||
**Estimated Total: ~$13-25/month** (excluding Bedrock model usage)
|
||||
|
||||
*Bedrock costs depend on your usage patterns and chosen models. See [Bedrock Pricing](https://aws.amazon.com/bedrock/pricing/) for details.
|
||||
|
||||
### Cost Optimization Tips
|
||||
|
||||
- **Delete when not in use**: Use `cdk destroy` to remove all resources
|
||||
- **Monitor usage**: Set up CloudWatch billing alarms
|
||||
- **Choose efficient models**: Select appropriate Bedrock models for your use case
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### CDK Bootstrap Required
|
||||
|
||||
If you see bootstrap errors:
|
||||
```bash
|
||||
cdk bootstrap aws://ACCOUNT-NUMBER/REGION
|
||||
```
|
||||
|
||||
### Permission Issues
|
||||
|
||||
Ensure your IAM user/role has:
|
||||
- `CDKToolkit` permissions or equivalent
|
||||
- Permissions to create all resources in the stack
|
||||
- `iam:PassRole` for service roles
|
||||
|
||||
### Python Dependencies
|
||||
|
||||
Install dependencies in the project directory:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Build Failures
|
||||
|
||||
Check CodeBuild logs in the AWS Console:
|
||||
1. Go to CodeBuild console
|
||||
2. Find the build projects (names contain "agent1-build" and "agent2-build")
|
||||
3. Check build history and logs
|
||||
|
||||
### Agent Communication Issues
|
||||
|
||||
If Agent 1 can't invoke Agent 2:
|
||||
1. Check IAM permissions for `bedrock-agentcore:InvokeAgentRuntime`
|
||||
2. Verify Agent 2 runtime is running
|
||||
3. Check CloudWatch logs for both agents
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please see our [Contributing Guide](../../CONTRIBUTING.md) for details.
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](../../LICENSE) file for details.
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
import aws_cdk as cdk
|
||||
from multi_agent_stack import MultiAgentStack
|
||||
|
||||
app = cdk.App()
|
||||
MultiAgentStack(app, "MultiAgentDemo")
|
||||
|
||||
app.synth()
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"app": "python3 app.py",
|
||||
"watch": {
|
||||
"include": [
|
||||
"**"
|
||||
],
|
||||
"exclude": [
|
||||
"README.md",
|
||||
"cdk*.json",
|
||||
"requirements*.txt",
|
||||
"source.bat",
|
||||
"**/__pycache__",
|
||||
"**/*.pyc"
|
||||
]
|
||||
},
|
||||
"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-iam:standardizedServicePrincipals": 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:enableLogging": 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:enableLogging": true,
|
||||
"@aws-cdk/aws-norh:enablePartitionLiterals": true,
|
||||
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
|
||||
"@aws-cdk/aws-iam:standardizedServicePrincipals": true,
|
||||
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
|
||||
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
|
||||
"@aws-cdk/aws-route53-patters:useCertificate": true,
|
||||
"@aws-cdk/customresources:installLatestAwsSdkDefault": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
from .agentcore_role import AgentCoreRole
|
||||
@@ -0,0 +1,93 @@
|
||||
from aws_cdk import (
|
||||
aws_iam as iam,
|
||||
Stack
|
||||
)
|
||||
from constructs import Construct
|
||||
|
||||
class AgentCoreRole(iam.Role):
|
||||
def __init__(self, scope: Construct, construct_id: str, **kwargs):
|
||||
region = Stack.of(scope).region
|
||||
account_id = Stack.of(scope).account
|
||||
|
||||
super().__init__(scope, construct_id,
|
||||
assumed_by=iam.ServicePrincipal("bedrock-agentcore.amazonaws.com"),
|
||||
inline_policies={
|
||||
"AgentCorePolicy": iam.PolicyDocument(
|
||||
statements=[
|
||||
iam.PolicyStatement(
|
||||
sid="ECRImageAccess",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"ecr:BatchGetImage",
|
||||
"ecr:GetDownloadUrlForLayer",
|
||||
"ecr:BatchCheckLayerAvailability"
|
||||
],
|
||||
resources=[f"arn:aws:ecr:{region}:{account_id}:repository/*"]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="ECRTokenAccess",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=["ecr:GetAuthorizationToken"],
|
||||
resources=["*"]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"logs:DescribeLogStreams",
|
||||
"logs:CreateLogGroup",
|
||||
"logs:DescribeLogGroups",
|
||||
"logs:CreateLogStream",
|
||||
"logs:PutLogEvents"
|
||||
],
|
||||
resources=[f"arn:aws:logs:{region}:{account_id}:log-group:/aws/bedrock-agentcore/runtimes/*"]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"xray:PutTraceSegments",
|
||||
"xray:PutTelemetryRecords",
|
||||
"xray:GetSamplingRules",
|
||||
"xray:GetSamplingTargets"
|
||||
],
|
||||
resources=["*"]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=["cloudwatch:PutMetricData"],
|
||||
resources=["*"],
|
||||
conditions={
|
||||
"StringEquals": {
|
||||
"cloudwatch:namespace": "bedrock-agentcore"
|
||||
}
|
||||
}
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="GetAgentAccessToken",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"bedrock-agentcore:GetWorkloadAccessToken",
|
||||
"bedrock-agentcore:GetWorkloadAccessTokenForJWT",
|
||||
"bedrock-agentcore:GetWorkloadAccessTokenForUserId"
|
||||
],
|
||||
resources=[
|
||||
f"arn:aws:bedrock-agentcore:{region}:{account_id}:workload-identity-directory/default",
|
||||
f"arn:aws:bedrock-agentcore:{region}:{account_id}:workload-identity-directory/default/workload-identity/*"
|
||||
]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="BedrockModelInvocation",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"bedrock:InvokeModel",
|
||||
"bedrock:InvokeModelWithResponseStream"
|
||||
],
|
||||
resources=[
|
||||
"arn:aws:bedrock:*::foundation-model/*",
|
||||
f"arn:aws:bedrock:{region}:{account_id}:*"
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
},
|
||||
**kwargs
|
||||
)
|
||||
@@ -0,0 +1,93 @@
|
||||
import boto3
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
import urllib3
|
||||
|
||||
# Note: cfnresponse is only available for inline Lambda code in CloudFormation.
|
||||
# When using CDK with Code.from_asset(), we need to include our own copy.
|
||||
# This is the standard AWS-provided cfnresponse module embedded directly.
|
||||
|
||||
class cfnresponse:
|
||||
SUCCESS = "SUCCESS"
|
||||
FAILED = "FAILED"
|
||||
|
||||
@staticmethod
|
||||
def send(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False, reason=None):
|
||||
responseUrl = event['ResponseURL']
|
||||
print(responseUrl)
|
||||
|
||||
responseBody = {
|
||||
'Status': responseStatus,
|
||||
'Reason': reason or "See the details in CloudWatch Log Stream: {}".format(context.log_stream_name),
|
||||
'PhysicalResourceId': physicalResourceId or context.log_stream_name,
|
||||
'StackId': event['StackId'],
|
||||
'RequestId': event['RequestId'],
|
||||
'LogicalResourceId': event['LogicalResourceId'],
|
||||
'NoEcho': noEcho,
|
||||
'Data': responseData
|
||||
}
|
||||
|
||||
json_responseBody = json.dumps(responseBody)
|
||||
print("Response body:")
|
||||
print(json_responseBody)
|
||||
|
||||
headers = {
|
||||
'content-type': '',
|
||||
'content-length': str(len(json_responseBody))
|
||||
}
|
||||
|
||||
try:
|
||||
http = urllib3.PoolManager()
|
||||
response = http.request('PUT', responseUrl, headers=headers, body=json_responseBody)
|
||||
print("Status code:", response.status)
|
||||
except Exception as e:
|
||||
print("send(..) failed executing http.request(..):", e)
|
||||
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
def handler(event, context):
|
||||
logger.info('Received event: %s', json.dumps(event))
|
||||
|
||||
try:
|
||||
if event['RequestType'] == 'Delete':
|
||||
cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
|
||||
return
|
||||
|
||||
project_name = event['ResourceProperties']['ProjectName']
|
||||
|
||||
codebuild = boto3.client('codebuild')
|
||||
|
||||
# Start build
|
||||
response = codebuild.start_build(projectName=project_name)
|
||||
build_id = response['build']['id']
|
||||
logger.info(f"Started build: {build_id}")
|
||||
|
||||
# Wait for completion
|
||||
max_wait_time = context.get_remaining_time_in_millis() / 1000 - 30
|
||||
start_time = time.time()
|
||||
|
||||
while True:
|
||||
if time.time() - start_time > max_wait_time:
|
||||
cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': 'Build timeout'})
|
||||
return
|
||||
|
||||
build_response = codebuild.batch_get_builds(ids=[build_id])
|
||||
build_status = build_response['builds'][0]['buildStatus']
|
||||
|
||||
if build_status == 'SUCCEEDED':
|
||||
logger.info(f"Build {build_id} succeeded")
|
||||
cfnresponse.send(event, context, cfnresponse.SUCCESS, {'BuildId': build_id})
|
||||
return
|
||||
elif build_status in ['FAILED', 'FAULT', 'STOPPED', 'TIMED_OUT']:
|
||||
logger.error(f"Build {build_id} failed with status: {build_status}")
|
||||
cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': f'Build failed: {build_status}'})
|
||||
return
|
||||
|
||||
logger.info(f"Build {build_id} status: {build_status}")
|
||||
time.sleep(30)
|
||||
|
||||
except Exception as e:
|
||||
logger.error('Error: %s', str(e))
|
||||
cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': str(e)})
|
||||
@@ -0,0 +1,733 @@
|
||||
from aws_cdk import (
|
||||
Stack,
|
||||
aws_ecr as ecr,
|
||||
aws_codebuild as codebuild,
|
||||
aws_iam as iam,
|
||||
aws_lambda as lambda_,
|
||||
aws_bedrockagentcore as bedrockagentcore,
|
||||
CustomResource,
|
||||
CfnParameter,
|
||||
CfnOutput,
|
||||
Duration,
|
||||
RemovalPolicy
|
||||
)
|
||||
from constructs import Construct
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), 'infra_utils'))
|
||||
|
||||
class MultiAgentStack(Stack):
|
||||
|
||||
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
|
||||
super().__init__(scope, construct_id, **kwargs)
|
||||
|
||||
# Parameters
|
||||
agent1_name = CfnParameter(self, "Agent1Name",
|
||||
type="String",
|
||||
default="OrchestratorAgent",
|
||||
description="Name for the orchestrator agent runtime (agent1)"
|
||||
)
|
||||
|
||||
agent2_name = CfnParameter(self, "Agent2Name",
|
||||
type="String",
|
||||
default="SpecialistAgent",
|
||||
description="Name for the specialist agent runtime (agent2)"
|
||||
)
|
||||
|
||||
image_tag = CfnParameter(self, "ImageTag",
|
||||
type="String",
|
||||
default="latest",
|
||||
description="Tag for the Docker images"
|
||||
)
|
||||
|
||||
network_mode = CfnParameter(self, "NetworkMode",
|
||||
type="String",
|
||||
default="PUBLIC",
|
||||
description="Network mode for AgentCore resources",
|
||||
allowed_values=["PUBLIC", "PRIVATE"]
|
||||
)
|
||||
|
||||
ecr_repository_name = CfnParameter(self, "ECRRepositoryName",
|
||||
type="String",
|
||||
default="multi-agent",
|
||||
description="Base name of the ECR repositories"
|
||||
)
|
||||
|
||||
# ECR Repositories
|
||||
ecr_repository_agent1 = ecr.Repository(self, "ECRRepositoryAgent1",
|
||||
repository_name=f"{self.stack_name.lower()}-{ecr_repository_name.value_as_string}-agent1",
|
||||
image_tag_mutability=ecr.TagMutability.MUTABLE,
|
||||
removal_policy=RemovalPolicy.DESTROY,
|
||||
empty_on_delete=True,
|
||||
image_scan_on_push=True
|
||||
)
|
||||
|
||||
ecr_repository_agent2 = ecr.Repository(self, "ECRRepositoryAgent2",
|
||||
repository_name=f"{self.stack_name.lower()}-{ecr_repository_name.value_as_string}-agent2",
|
||||
image_tag_mutability=ecr.TagMutability.MUTABLE,
|
||||
removal_policy=RemovalPolicy.DESTROY,
|
||||
empty_on_delete=True,
|
||||
image_scan_on_push=True
|
||||
)
|
||||
# IAM Roles
|
||||
# Agent1 Execution Role (with permissions to invoke Agent2)
|
||||
agent1_execution_role = iam.Role(self, "Agent1ExecutionRole",
|
||||
role_name=f"{self.stack_name}-agent1-execution-role",
|
||||
assumed_by=iam.ServicePrincipal("bedrock-agentcore.amazonaws.com"),
|
||||
managed_policies=[
|
||||
iam.ManagedPolicy.from_aws_managed_policy_name("BedrockAgentCoreFullAccess")
|
||||
],
|
||||
inline_policies={
|
||||
"Agent1ExecutionPolicy": iam.PolicyDocument(
|
||||
statements=[
|
||||
iam.PolicyStatement(
|
||||
sid="ECRImageAccess",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"ecr:BatchGetImage",
|
||||
"ecr:GetDownloadUrlForLayer",
|
||||
"ecr:BatchCheckLayerAvailability"
|
||||
],
|
||||
resources=[ecr_repository_agent1.repository_arn]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="ECRTokenAccess",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=["ecr:GetAuthorizationToken"],
|
||||
resources=["*"]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="CloudWatchLogs",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"logs:DescribeLogStreams",
|
||||
"logs:CreateLogGroup",
|
||||
"logs:DescribeLogGroups",
|
||||
"logs:CreateLogStream",
|
||||
"logs:PutLogEvents"
|
||||
],
|
||||
resources=["*"]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="XRayTracing",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"xray:PutTraceSegments",
|
||||
"xray:PutTelemetryRecords",
|
||||
"xray:GetSamplingRules",
|
||||
"xray:GetSamplingTargets"
|
||||
],
|
||||
resources=["*"]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="CloudWatchMetrics",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=["cloudwatch:PutMetricData"],
|
||||
resources=["*"],
|
||||
conditions={
|
||||
"StringEquals": {
|
||||
"cloudwatch:namespace": "bedrock-agentcore"
|
||||
}
|
||||
}
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="GetAgentAccessToken",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"bedrock-agentcore:GetWorkloadAccessToken",
|
||||
"bedrock-agentcore:GetWorkloadAccessTokenForJWT",
|
||||
"bedrock-agentcore:GetWorkloadAccessTokenForUserId"
|
||||
],
|
||||
resources=[
|
||||
f"arn:aws:bedrock-agentcore:{self.region}:{self.account}:workload-identity-directory/default",
|
||||
f"arn:aws:bedrock-agentcore:{self.region}:{self.account}:workload-identity-directory/default/workload-identity/*"
|
||||
]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="BedrockModelInvocation",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"bedrock:InvokeModel",
|
||||
"bedrock:InvokeModelWithResponseStream"
|
||||
],
|
||||
resources=["*"]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="InvokeAgent2Runtime",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=["bedrock-agentcore:InvokeAgentRuntime"],
|
||||
resources=[f"arn:aws:bedrock-agentcore:{self.region}:{self.account}:runtime/*"]
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
)
|
||||
# Agent2 Execution Role (basic permissions)
|
||||
agent2_execution_role = iam.Role(self, "Agent2ExecutionRole",
|
||||
role_name=f"{self.stack_name}-agent2-execution-role",
|
||||
assumed_by=iam.ServicePrincipal("bedrock-agentcore.amazonaws.com"),
|
||||
managed_policies=[
|
||||
iam.ManagedPolicy.from_aws_managed_policy_name("BedrockAgentCoreFullAccess")
|
||||
],
|
||||
inline_policies={
|
||||
"Agent2ExecutionPolicy": iam.PolicyDocument(
|
||||
statements=[
|
||||
iam.PolicyStatement(
|
||||
sid="ECRImageAccess",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"ecr:BatchGetImage",
|
||||
"ecr:GetDownloadUrlForLayer",
|
||||
"ecr:BatchCheckLayerAvailability"
|
||||
],
|
||||
resources=[ecr_repository_agent2.repository_arn]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="ECRTokenAccess",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=["ecr:GetAuthorizationToken"],
|
||||
resources=["*"]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="CloudWatchLogs",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"logs:DescribeLogStreams",
|
||||
"logs:CreateLogGroup",
|
||||
"logs:DescribeLogGroups",
|
||||
"logs:CreateLogStream",
|
||||
"logs:PutLogEvents"
|
||||
],
|
||||
resources=["*"]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="XRayTracing",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"xray:PutTraceSegments",
|
||||
"xray:PutTelemetryRecords",
|
||||
"xray:GetSamplingRules",
|
||||
"xray:GetSamplingTargets"
|
||||
],
|
||||
resources=["*"]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="CloudWatchMetrics",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=["cloudwatch:PutMetricData"],
|
||||
resources=["*"],
|
||||
conditions={
|
||||
"StringEquals": {
|
||||
"cloudwatch:namespace": "bedrock-agentcore"
|
||||
}
|
||||
}
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="GetAgentAccessToken",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"bedrock-agentcore:GetWorkloadAccessToken",
|
||||
"bedrock-agentcore:GetWorkloadAccessTokenForJWT",
|
||||
"bedrock-agentcore:GetWorkloadAccessTokenForUserId"
|
||||
],
|
||||
resources=[
|
||||
f"arn:aws:bedrock-agentcore:{self.region}:{self.account}:workload-identity-directory/default",
|
||||
f"arn:aws:bedrock-agentcore:{self.region}:{self.account}:workload-identity-directory/default/workload-identity/*"
|
||||
]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="BedrockModelInvocation",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"bedrock:InvokeModel",
|
||||
"bedrock:InvokeModelWithResponseStream"
|
||||
],
|
||||
resources=["*"]
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
)
|
||||
# CodeBuild Service Role
|
||||
codebuild_role = iam.Role(self, "CodeBuildRole",
|
||||
role_name=f"{self.stack_name}-codebuild-role",
|
||||
assumed_by=iam.ServicePrincipal("codebuild.amazonaws.com"),
|
||||
inline_policies={
|
||||
"CodeBuildPolicy": iam.PolicyDocument(
|
||||
statements=[
|
||||
iam.PolicyStatement(
|
||||
sid="CloudWatchLogs",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"logs:CreateLogGroup",
|
||||
"logs:CreateLogStream",
|
||||
"logs:PutLogEvents"
|
||||
],
|
||||
resources=[f"arn:aws:logs:{self.region}:{self.account}:log-group:/aws/codebuild/*"]
|
||||
),
|
||||
iam.PolicyStatement(
|
||||
sid="ECRAccess",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"ecr:BatchCheckLayerAvailability",
|
||||
"ecr:GetDownloadUrlForLayer",
|
||||
"ecr:BatchGetImage",
|
||||
"ecr:GetAuthorizationToken",
|
||||
"ecr:PutImage",
|
||||
"ecr:InitiateLayerUpload",
|
||||
"ecr:UploadLayerPart",
|
||||
"ecr:CompleteLayerUpload"
|
||||
],
|
||||
resources=[
|
||||
ecr_repository_agent1.repository_arn,
|
||||
ecr_repository_agent2.repository_arn,
|
||||
"*"
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
# Lambda Custom Resource Role
|
||||
custom_resource_role = iam.Role(self, "CustomResourceRole",
|
||||
role_name=f"{self.stack_name}-custom-resource-role",
|
||||
assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"),
|
||||
managed_policies=[
|
||||
iam.ManagedPolicy.from_aws_managed_policy_name("service-role/AWSLambdaBasicExecutionRole")
|
||||
],
|
||||
inline_policies={
|
||||
"CustomResourcePolicy": iam.PolicyDocument(
|
||||
statements=[
|
||||
iam.PolicyStatement(
|
||||
sid="CodeBuildAccess",
|
||||
effect=iam.Effect.ALLOW,
|
||||
actions=[
|
||||
"codebuild:StartBuild",
|
||||
"codebuild:BatchGetBuilds",
|
||||
"codebuild:BatchGetProjects"
|
||||
],
|
||||
resources=["*"] # Will be updated after CodeBuild projects are created
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
)
|
||||
# Lambda Function for CodeBuild Trigger
|
||||
build_trigger_lambda = lambda_.Function(self, "CodeBuildTriggerFunction",
|
||||
function_name=f"{self.stack_name}-codebuild-trigger",
|
||||
runtime=lambda_.Runtime.PYTHON_3_9,
|
||||
handler="build_trigger_lambda.handler",
|
||||
code=lambda_.Code.from_asset(os.path.join(os.path.dirname(__file__), "infra_utils")),
|
||||
timeout=Duration.minutes(15),
|
||||
role=custom_resource_role,
|
||||
description="Triggers CodeBuild projects as CloudFormation custom resource"
|
||||
)
|
||||
# Agent2 Build Project (build first as it's independent)
|
||||
agent2_build_project = codebuild.Project(self, "Agent2ImageBuildProject",
|
||||
project_name=f"{self.stack_name}-agent2-build",
|
||||
description=f"Build agent2 Docker image for {self.stack_name}",
|
||||
role=codebuild_role,
|
||||
environment=codebuild.BuildEnvironment(
|
||||
build_image=codebuild.LinuxArmBuildImage.AMAZON_LINUX_2_STANDARD_3_0,
|
||||
compute_type=codebuild.ComputeType.LARGE,
|
||||
privileged=True
|
||||
),
|
||||
environment_variables={
|
||||
"AWS_DEFAULT_REGION": codebuild.BuildEnvironmentVariable(value=self.region),
|
||||
"AWS_ACCOUNT_ID": codebuild.BuildEnvironmentVariable(value=self.account),
|
||||
"IMAGE_REPO_NAME": codebuild.BuildEnvironmentVariable(value=ecr_repository_agent2.repository_name),
|
||||
"IMAGE_TAG": codebuild.BuildEnvironmentVariable(value=image_tag.value_as_string),
|
||||
"STACK_NAME": codebuild.BuildEnvironmentVariable(value=self.stack_name)
|
||||
},
|
||||
build_spec=codebuild.BuildSpec.from_object({
|
||||
"version": "0.2",
|
||||
"phases": {
|
||||
"pre_build": {
|
||||
"commands": [
|
||||
"echo Logging in to Amazon ECR...",
|
||||
"aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com"
|
||||
]
|
||||
},
|
||||
"build": {
|
||||
"commands": [
|
||||
"echo Build started on `date`",
|
||||
"echo Building the Docker image for agent2 ARM64...",
|
||||
# Create requirements.txt
|
||||
"""cat > requirements.txt << 'EOF'
|
||||
strands-agents
|
||||
boto3>=1.40.0
|
||||
botocore>=1.40.0
|
||||
bedrock-agentcore
|
||||
EOF""",
|
||||
# Create agent2.py
|
||||
"""cat > agent2.py << 'EOF'
|
||||
from strands import Agent
|
||||
import os
|
||||
from bedrock_agentcore.runtime import BedrockAgentCoreApp
|
||||
|
||||
app = BedrockAgentCoreApp()
|
||||
|
||||
def create_specialist_agent() -> Agent:
|
||||
\"\"\"Create a specialist agent that handles specific analytical tasks\"\"\"
|
||||
system_prompt = \"\"\"You are a specialist analytical agent.
|
||||
You are an expert at analyzing data and providing detailed insights.
|
||||
When asked questions, provide thorough, well-reasoned responses with specific details.
|
||||
Focus on accuracy and completeness in your answers.\"\"\"
|
||||
|
||||
return Agent(
|
||||
system_prompt=system_prompt,
|
||||
name="SpecialistAgent"
|
||||
)
|
||||
|
||||
@app.entrypoint
|
||||
async def invoke(payload=None):
|
||||
\"\"\"Main entrypoint for agent2\"\"\"
|
||||
try:
|
||||
# Get the query from payload
|
||||
query = payload.get("prompt", "Hello") if payload else "Hello"
|
||||
|
||||
# Create and use the specialist agent
|
||||
agent = create_specialist_agent()
|
||||
response = agent(query)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"agent": "agent2",
|
||||
"response": response.message['content'][0]['text']
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"agent": "agent2",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
EOF""",
|
||||
# Create Dockerfile
|
||||
"""cat > Dockerfile << 'EOF'
|
||||
FROM public.ecr.aws/docker/library/python:3.11-slim
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt requirements.txt
|
||||
RUN pip install -r requirements.txt
|
||||
RUN pip install aws-opentelemetry-distro>=0.10.1
|
||||
|
||||
# Create non-root user
|
||||
RUN useradd -m -u 1000 bedrock_agentcore
|
||||
USER bedrock_agentcore
|
||||
|
||||
EXPOSE 8080
|
||||
EXPOSE 8000
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD ["opentelemetry-instrument", "python", "-m", "agent2"]
|
||||
EOF""",
|
||||
"echo Building ARM64 image...",
|
||||
"docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG .",
|
||||
"docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG"
|
||||
]
|
||||
},
|
||||
"post_build": {
|
||||
"commands": [
|
||||
"echo Build completed on `date`",
|
||||
"echo Pushing the Docker image...",
|
||||
"docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG",
|
||||
"echo ARM64 Docker image pushed successfully"
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
# Agent1 Build Project (orchestrator that calls agent2)
|
||||
agent1_build_project = codebuild.Project(self, "Agent1ImageBuildProject",
|
||||
project_name=f"{self.stack_name}-agent1-build",
|
||||
description=f"Build agent1 Docker image for {self.stack_name}",
|
||||
role=codebuild_role,
|
||||
environment=codebuild.BuildEnvironment(
|
||||
build_image=codebuild.LinuxArmBuildImage.AMAZON_LINUX_2_STANDARD_3_0,
|
||||
compute_type=codebuild.ComputeType.LARGE,
|
||||
privileged=True
|
||||
),
|
||||
environment_variables={
|
||||
"AWS_DEFAULT_REGION": codebuild.BuildEnvironmentVariable(value=self.region),
|
||||
"AWS_ACCOUNT_ID": codebuild.BuildEnvironmentVariable(value=self.account),
|
||||
"IMAGE_REPO_NAME": codebuild.BuildEnvironmentVariable(value=ecr_repository_agent1.repository_name),
|
||||
"IMAGE_TAG": codebuild.BuildEnvironmentVariable(value=image_tag.value_as_string),
|
||||
"STACK_NAME": codebuild.BuildEnvironmentVariable(value=self.stack_name)
|
||||
},
|
||||
build_spec=codebuild.BuildSpec.from_object({
|
||||
"version": "0.2",
|
||||
"phases": {
|
||||
"pre_build": {
|
||||
"commands": [
|
||||
"echo Logging in to Amazon ECR...",
|
||||
"aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com"
|
||||
]
|
||||
},
|
||||
"build": {
|
||||
"commands": [
|
||||
"echo Build started on `date`",
|
||||
"echo Building the Docker image for agent1 ARM64...",
|
||||
# Create requirements.txt
|
||||
"""cat > requirements.txt << 'EOF'
|
||||
strands-agents
|
||||
boto3>=1.40.0
|
||||
botocore>=1.40.0
|
||||
bedrock-agentcore
|
||||
EOF""",
|
||||
# Create agent1.py - this is a large block, so I'll split it
|
||||
"""cat > agent1.py << 'EOF'
|
||||
from strands import Agent, tool
|
||||
from typing import Dict, Any
|
||||
import boto3
|
||||
import json
|
||||
import os
|
||||
from bedrock_agentcore.runtime import BedrockAgentCoreApp
|
||||
|
||||
app = BedrockAgentCoreApp()
|
||||
|
||||
# Environment variable for Agent2 ARN (will be set by CloudFormation)
|
||||
AGENT2_ARN = os.getenv('AGENT2_ARN', '')
|
||||
|
||||
def invoke_agent2(query: str) -> str:
|
||||
\"\"\"Helper function to invoke agent2 using boto3\"\"\"
|
||||
import uuid
|
||||
try:
|
||||
# Get region from environment or use default
|
||||
region = os.getenv('AWS_REGION', 'us-west-2')
|
||||
agentcore_client = boto3.client('bedrock-agentcore', region_name=region)
|
||||
|
||||
# Invoke agent2 runtime (using AWS sample format)
|
||||
response = agentcore_client.invoke_agent_runtime(
|
||||
agentRuntimeArn=AGENT2_ARN,
|
||||
qualifier="DEFAULT",
|
||||
payload=json.dumps({"prompt": query})
|
||||
)
|
||||
|
||||
# Handle streaming response (text/event-stream)
|
||||
if "text/event-stream" in response.get("contentType", ""):
|
||||
result = ""
|
||||
for line in response["response"].iter_lines(chunk_size=10):
|
||||
if line:
|
||||
line = line.decode("utf-8")
|
||||
# Remove 'data: ' prefix if present
|
||||
if line.startswith("data: "):
|
||||
line = line[6:]
|
||||
result += line
|
||||
return result
|
||||
|
||||
# Handle JSON response
|
||||
elif response.get("contentType") == "application/json":
|
||||
content = []
|
||||
for chunk in response.get("response", []):
|
||||
content.append(chunk.decode('utf-8'))
|
||||
response_data = json.loads(''.join(content))
|
||||
return json.dumps(response_data)
|
||||
|
||||
# Handle other response types
|
||||
else:
|
||||
response_body = response['response'].read()
|
||||
return response_body.decode('utf-8')
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
return f"Error invoking agent2: {str(e)}\\nDetails: {error_details}"
|
||||
|
||||
@tool
|
||||
def call_specialist_agent(query: str) -> Dict[str, Any]:
|
||||
\"\"\"
|
||||
Call the specialist agent (agent2) for detailed analysis or complex tasks.
|
||||
Use this tool when you need expert analysis or detailed information.
|
||||
|
||||
Args:
|
||||
query: The question or task to send to the specialist agent
|
||||
|
||||
Returns:
|
||||
The specialist agent's response
|
||||
\"\"\"
|
||||
result = invoke_agent2(query)
|
||||
return {
|
||||
"status": "success",
|
||||
"content": [{"text": result}]
|
||||
}
|
||||
|
||||
def create_orchestrator_agent() -> Agent:
|
||||
\"\"\"Create the orchestrator agent with the tool to call agent2\"\"\"
|
||||
system_prompt = \"\"\"You are an orchestrator agent.
|
||||
You can handle simple queries directly, but for complex analytical tasks,
|
||||
you should delegate to the specialist agent using the call_specialist_agent tool.
|
||||
|
||||
Use the specialist agent when:
|
||||
- The query requires detailed analysis
|
||||
- The query is about complex topics
|
||||
- The user explicitly asks for expert analysis
|
||||
|
||||
Handle simple queries (greetings, basic questions) yourself.\"\"\"
|
||||
|
||||
return Agent(
|
||||
tools=[call_specialist_agent],
|
||||
system_prompt=system_prompt,
|
||||
name="OrchestratorAgent"
|
||||
)
|
||||
|
||||
@app.entrypoint
|
||||
async def invoke(payload=None):
|
||||
\"\"\"Main entrypoint for agent1\"\"\"
|
||||
try:
|
||||
# Get the query from payload
|
||||
query = payload.get("prompt", "Hello, how are you?") if payload else "Hello, how are you?"
|
||||
|
||||
# Create and use the orchestrator agent
|
||||
agent = create_orchestrator_agent()
|
||||
response = agent(query)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"agent": "agent1",
|
||||
"response": response.message['content'][0]['text']
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"agent": "agent1",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
EOF""",
|
||||
# Create Dockerfile
|
||||
"""cat > Dockerfile << 'EOF'
|
||||
FROM public.ecr.aws/docker/library/python:3.11-slim
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt requirements.txt
|
||||
RUN pip install -r requirements.txt
|
||||
RUN pip install aws-opentelemetry-distro>=0.10.1
|
||||
|
||||
# Create non-root user
|
||||
RUN useradd -m -u 1000 bedrock_agentcore
|
||||
USER bedrock_agentcore
|
||||
|
||||
EXPOSE 8080
|
||||
EXPOSE 8000
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD ["opentelemetry-instrument", "python", "-m", "agent1"]
|
||||
EOF""",
|
||||
"echo Building ARM64 image...",
|
||||
"docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG .",
|
||||
"docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG"
|
||||
]
|
||||
},
|
||||
"post_build": {
|
||||
"commands": [
|
||||
"echo Build completed on `date`",
|
||||
"echo Pushing the Docker image...",
|
||||
"docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG",
|
||||
"echo ARM64 Docker image pushed successfully"
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
# Custom Resources to trigger builds
|
||||
trigger_agent2_build = CustomResource(self, "TriggerAgent2ImageBuild",
|
||||
service_token=build_trigger_lambda.function_arn,
|
||||
properties={
|
||||
"ProjectName": agent2_build_project.project_name,
|
||||
"WaitForCompletion": "true"
|
||||
}
|
||||
)
|
||||
trigger_agent2_build.node.add_dependency(ecr_repository_agent2)
|
||||
trigger_agent2_build.node.add_dependency(agent2_build_project)
|
||||
|
||||
trigger_agent1_build = CustomResource(self, "TriggerAgent1ImageBuild",
|
||||
service_token=build_trigger_lambda.function_arn,
|
||||
properties={
|
||||
"ProjectName": agent1_build_project.project_name,
|
||||
"WaitForCompletion": "true"
|
||||
}
|
||||
)
|
||||
trigger_agent1_build.node.add_dependency(ecr_repository_agent1)
|
||||
trigger_agent1_build.node.add_dependency(agent1_build_project)
|
||||
|
||||
# Agent2 Runtime (deploy first as agent1 depends on it)
|
||||
agent2_runtime = bedrockagentcore.CfnRuntime(self, "Agent2Runtime",
|
||||
agent_runtime_name=f"{self.stack_name.replace('-', '_')}_{agent2_name.value_as_string}",
|
||||
agent_runtime_artifact=bedrockagentcore.CfnRuntime.AgentRuntimeArtifactProperty(
|
||||
container_configuration=bedrockagentcore.CfnRuntime.ContainerConfigurationProperty(
|
||||
container_uri=f"{ecr_repository_agent2.repository_uri}:{image_tag.value_as_string}"
|
||||
)
|
||||
),
|
||||
role_arn=agent2_execution_role.role_arn,
|
||||
network_configuration=bedrockagentcore.CfnRuntime.NetworkConfigurationProperty(
|
||||
network_mode=network_mode.value_as_string
|
||||
),
|
||||
description=f"Specialist agent runtime for {self.stack_name}"
|
||||
)
|
||||
agent2_runtime.node.add_dependency(trigger_agent2_build)
|
||||
|
||||
# Agent1 Runtime (orchestrator with agent2 ARN as environment variable)
|
||||
agent1_runtime = bedrockagentcore.CfnRuntime(self, "Agent1Runtime",
|
||||
agent_runtime_name=f"{self.stack_name.replace('-', '_')}_{agent1_name.value_as_string}",
|
||||
agent_runtime_artifact=bedrockagentcore.CfnRuntime.AgentRuntimeArtifactProperty(
|
||||
container_configuration=bedrockagentcore.CfnRuntime.ContainerConfigurationProperty(
|
||||
container_uri=f"{ecr_repository_agent1.repository_uri}:{image_tag.value_as_string}"
|
||||
)
|
||||
),
|
||||
role_arn=agent1_execution_role.role_arn,
|
||||
network_configuration=bedrockagentcore.CfnRuntime.NetworkConfigurationProperty(
|
||||
network_mode=network_mode.value_as_string
|
||||
),
|
||||
description=f"Orchestrator agent runtime for {self.stack_name}",
|
||||
environment_variables={
|
||||
"AGENT2_ARN": agent2_runtime.attr_agent_runtime_arn
|
||||
}
|
||||
)
|
||||
agent1_runtime.node.add_dependency(trigger_agent1_build)
|
||||
agent1_runtime.node.add_dependency(agent2_runtime)
|
||||
# Outputs
|
||||
CfnOutput(self, "Agent1RuntimeId",
|
||||
description="ID of agent1 (orchestrator) runtime",
|
||||
value=agent1_runtime.attr_agent_runtime_id,
|
||||
export_name=f"{self.stack_name}-Agent1RuntimeId"
|
||||
)
|
||||
|
||||
CfnOutput(self, "Agent1RuntimeArn",
|
||||
description="ARN of agent1 (orchestrator) runtime",
|
||||
value=agent1_runtime.attr_agent_runtime_arn,
|
||||
export_name=f"{self.stack_name}-Agent1RuntimeArn"
|
||||
)
|
||||
|
||||
CfnOutput(self, "Agent2RuntimeId",
|
||||
description="ID of agent2 (specialist) runtime",
|
||||
value=agent2_runtime.attr_agent_runtime_id,
|
||||
export_name=f"{self.stack_name}-Agent2RuntimeId"
|
||||
)
|
||||
|
||||
CfnOutput(self, "Agent2RuntimeArn",
|
||||
description="ARN of agent2 (specialist) runtime",
|
||||
value=agent2_runtime.attr_agent_runtime_arn,
|
||||
export_name=f"{self.stack_name}-Agent2RuntimeArn"
|
||||
)
|
||||
|
||||
CfnOutput(self, "Agent1ECRRepositoryUri",
|
||||
description="URI of the ECR repository for agent1",
|
||||
value=ecr_repository_agent1.repository_uri,
|
||||
export_name=f"{self.stack_name}-Agent1ECRRepositoryUri"
|
||||
)
|
||||
|
||||
CfnOutput(self, "Agent2ECRRepositoryUri",
|
||||
description="URI of the ECR repository for agent2",
|
||||
value=ecr_repository_agent2.repository_uri,
|
||||
export_name=f"{self.stack_name}-Agent2ECRRepositoryUri"
|
||||
)
|
||||
@@ -0,0 +1,2 @@
|
||||
aws-cdk-lib==2.220.0
|
||||
constructs>=10.0.79
|
||||
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import boto3
|
||||
import json
|
||||
|
||||
def test_multi_agent():
|
||||
"""Test the multi-agent system"""
|
||||
|
||||
# Get runtime ARNs from CloudFormation outputs
|
||||
cf_client = boto3.client('cloudformation', region_name='us-east-1')
|
||||
|
||||
try:
|
||||
response = cf_client.describe_stacks(StackName='MultiAgentDemo')
|
||||
outputs = response['Stacks'][0]['Outputs']
|
||||
|
||||
agent1_arn = None
|
||||
agent2_arn = None
|
||||
|
||||
for output in outputs:
|
||||
if output['OutputKey'] == 'Agent1RuntimeArn':
|
||||
agent1_arn = output['OutputValue']
|
||||
elif output['OutputKey'] == 'Agent2RuntimeArn':
|
||||
agent2_arn = output['OutputValue']
|
||||
|
||||
print(f"Agent1 (Orchestrator) ARN: {agent1_arn}")
|
||||
print(f"Agent2 (Specialist) ARN: {agent2_arn}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting stack outputs: {e}")
|
||||
return
|
||||
|
||||
# Create bedrock-agentcore client
|
||||
agentcore_client = boto3.client('bedrock-agentcore', region_name='us-east-1')
|
||||
|
||||
# Test 1: Simple query (should be handled by Agent1 directly)
|
||||
print("\n" + "="*60)
|
||||
print("TEST 1: Simple greeting (Agent1 handles directly)")
|
||||
print("="*60)
|
||||
|
||||
try:
|
||||
response = agentcore_client.invoke_agent_runtime(
|
||||
agentRuntimeArn=agent1_arn,
|
||||
qualifier="DEFAULT",
|
||||
payload=json.dumps({"prompt": "Hello, how are you?"})
|
||||
)
|
||||
|
||||
print("Response received:")
|
||||
print(f"Content Type: {response.get('contentType', 'N/A')}")
|
||||
|
||||
# Handle the response
|
||||
if response.get('contentType') == 'application/json':
|
||||
response_body = response['response'].read()
|
||||
result = json.loads(response_body.decode('utf-8'))
|
||||
print(f"Result: {json.dumps(result, indent=2)}")
|
||||
else:
|
||||
print("Response body:")
|
||||
for chunk in response['response']:
|
||||
print(chunk.decode('utf-8'))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error testing Agent1 simple query: {e}")
|
||||
|
||||
# Test 2: Complex query (should trigger Agent2 delegation)
|
||||
print("\n" + "="*60)
|
||||
print("TEST 2: Complex analysis (Agent1 delegates to Agent2)")
|
||||
print("="*60)
|
||||
|
||||
try:
|
||||
response = agentcore_client.invoke_agent_runtime(
|
||||
agentRuntimeArn=agent1_arn,
|
||||
qualifier="DEFAULT",
|
||||
payload=json.dumps({"prompt": "Provide a detailed analysis of the benefits and drawbacks of serverless architecture"})
|
||||
)
|
||||
|
||||
print("Response received:")
|
||||
print(f"Content Type: {response.get('contentType', 'N/A')}")
|
||||
|
||||
# Handle the response
|
||||
if response.get('contentType') == 'application/json':
|
||||
response_body = response['response'].read()
|
||||
result = json.loads(response_body.decode('utf-8'))
|
||||
print(f"Result: {json.dumps(result, indent=2)}")
|
||||
else:
|
||||
print("Response body:")
|
||||
for chunk in response['response']:
|
||||
print(chunk.decode('utf-8'))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error testing Agent1 complex query: {e}")
|
||||
|
||||
# Test 3: Direct Agent2 test
|
||||
print("\n" + "="*60)
|
||||
print("TEST 3: Direct Agent2 test (Specialist)")
|
||||
print("="*60)
|
||||
|
||||
try:
|
||||
response = agentcore_client.invoke_agent_runtime(
|
||||
agentRuntimeArn=agent2_arn,
|
||||
qualifier="DEFAULT",
|
||||
payload=json.dumps({"prompt": "Explain quantum computing in detail"})
|
||||
)
|
||||
|
||||
print("Response received:")
|
||||
print(f"Content Type: {response.get('contentType', 'N/A')}")
|
||||
|
||||
# Handle the response
|
||||
if response.get('contentType') == 'application/json':
|
||||
response_body = response['response'].read()
|
||||
result = json.loads(response_body.decode('utf-8'))
|
||||
print(f"Result: {json.dumps(result, indent=2)}")
|
||||
else:
|
||||
print("Response body:")
|
||||
for chunk in response['response']:
|
||||
print(chunk.decode('utf-8'))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error testing Agent2 directly: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_multi_agent()
|
||||
@@ -0,0 +1,53 @@
|
||||
# CloudFormation Samples
|
||||
|
||||
Deploy Amazon Bedrock AgentCore resources using CloudFormation templates.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- AWS CLI installed and configured
|
||||
- CloudFormation permissions to create stacks and resources
|
||||
- Access to Amazon Bedrock AgentCore (preview)
|
||||
|
||||
## General Deployment Pattern
|
||||
|
||||
```bash
|
||||
# Deploy
|
||||
aws cloudformation create-stack \
|
||||
--stack-name <stack-name> \
|
||||
--template-body file://<template-file> \
|
||||
--capabilities CAPABILITY_IAM \
|
||||
--region <region>
|
||||
|
||||
# Monitor
|
||||
aws cloudformation describe-stacks \
|
||||
--stack-name <stack-name> \
|
||||
--region <region>
|
||||
|
||||
# Cleanup
|
||||
aws cloudformation delete-stack \
|
||||
--stack-name <stack-name> \
|
||||
--region <region>
|
||||
```
|
||||
|
||||
## Samples
|
||||
|
||||
- **[mcp-server-agentcore-runtime/](./mcp-server-agentcore-runtime/)** - MCP Server with JWT authentication
|
||||
- **[basic-runtime/](./basic-runtime/)** - Simple agent deployment
|
||||
- **[multi-agent-runtime/](./multi-agent-runtime/)** - Multi-agent system
|
||||
- **[end-to-end-weather-agent/](./end-to-end-weather-agent/)** - Weather agent with tools and memory
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Stack Creation Fails
|
||||
```bash
|
||||
aws cloudformation describe-stack-events \
|
||||
--stack-name <stack-name> \
|
||||
--region <region>
|
||||
```
|
||||
|
||||
### CodeBuild Failures
|
||||
```bash
|
||||
aws codebuild batch-get-builds \
|
||||
--ids <build-id> \
|
||||
--region <region>
|
||||
```
|
||||
Reference in New Issue
Block a user