1
0
mirror of synced 2026-05-22 14:43:35 +00:00

Adding End-to-End Customer Support Agent with AgentCore using Google ADK (#1164)

* feat(e2e): Add Google ADK end-to-end tutorial with AgentCore

Add 6-lab workshop covering agent creation, memory, gateway,
runtime deployment, frontend, and cleanup using Google ADK
with Amazon Bedrock AgentCore services.

* docs(e2e): Update Google ADK README and remove duplicate

Replace placeholder README with full tutorial content and remove
the 'README copy.md' duplicate file.

* docs(e2e): Add Google ADK to README title

* style(e2e): Capitalize README title consistently

* docs: Add Diego Brasil to CONTRIBUTORS

* chore(e2e): Remove images-og_do_not_commit directory

Remove original source images that were not intended for version control.

* fix: Use importlib for dynamic import and clean up linting issues

* feat(e2e): Set Cognito MFA to OPTIONAL and clean up inline comment

---------

Signed-off-by: Akarsha Sehwag <akshseh@amazon.de>
Co-authored-by: Akarsha Sehwag <akshseh@amazon.de>
This commit is contained in:
Diego Brasil
2026-03-25 20:14:03 +00:00
committed by GitHub
parent 76047f890c
commit 3a0d2ed7e1
62 changed files with 10821 additions and 1 deletions
+4
View File
@@ -252,3 +252,7 @@ node_modules
# Test downloads - not for version control
Test-Downloads/
### Docker ###
Dockerfile
.dockerignore
@@ -1,3 +1,111 @@
# End-to-End Customer Support Agent with AgentCore using Google ADK
### COMING SOON!
In this tutorial we will move a customer support agent from prototype to production using Amazon Bedrock AgentCore services.
## What You'll Build
A complete customer support system that starts as a simple prototype and evolves into a scalable and secure sample application.
Your final system will handle real customer conversations with memory, shared tools, and a web interface.
> [!IMPORTANT]
> The examples provided here is for educational purposes. It demonstrates how the different services from AgentCore are used on the process of migrating an agentic use case from prototype to production. As such, it is not intended for direct use in production environments.
**Journey Overview:**
- Start with a basic agent prototype (20 mins)
- Add conversation memory across sessions (20 mins)
- Share tools securely across multiple agents (30 mins)
- Deploy to production with observability (30 mins)
- Set up continuous quality evaluation (10 mins)
- Build a customer-facing web app (20 mins)
## Architecture Overview
By the end of the 6 labs of this tutorial you will have created the following architecture
<div style="text-align:left">
<img src="images/architecture_lab6_streamlit.png" width="100%"/>
</div>
## Prerequisites
- AWS account with Bedrock access
- Python 3.10+
- AWS CLI configured
- Claude 3.7 Sonnet enabled in Bedrock
## Labs
### Lab 1: Create Agent Prototype
Build a prototype of a customer support agent with three core tools:
- Return policy lookup
- Product information search
- Web search for troubleshooting
**What you'll learn:** Basic agent creation with Strands Agents and tool integration
### Lab 2: Add Memory
Transform your "goldfish agent" into one that remembers customers across conversations.
- Persistent conversation history
- Customer preference extraction
- Cross-session context awareness
**What you'll learn:** AgentCore Memory for both short-term and long-term persistence
### Lab 3: Scale with Gateway & Identity
Move from local tools to shared, enterprise-ready services.
- Centralized tool management
- JWT-based authentication
- Integration with existing AWS Lambda functions
- (Optional) Fine-grained access control with Cedar policies (e.g., deny web search for "iPhone 8" keywords)
**What you'll learn:** AgentCore Gateway and AgentCore Identity for secure tool sharing
### Lab 4: Deploy to Production
Deploy your agent to handle real traffic with full observability.
- Fully managed deployment
- Session Continuity and Session Isolation
- CloudWatch Observability integration
**What you'll learn:** AgentCore Runtime with production-grade observability
### Lab 5: Build Customer Interface
Create a web app customers can actually use.
- Streamlit-based chat interface
- Real-time response streaming
- Session management and authentication
**What you'll learn:** Frontend integration with secure agent endpoints
## Getting Started
1. Clone this repository
2. Install dependencies: `pip install -r requirements.txt`
3. Configure AWS credentials
4. Start with [Lab 1](lab-01-create-an-agent.ipynb)
Each lab builds on the previous one, but you can jump ahead if you understand the concepts.
## Architecture Evolution
Watch your architecture grow from a simple local agent to a production system:
**Lab 1:** Local agent with embedded tools
**Lab 2:** Agent + AgentCore Memory for persistence
**Lab 3:** Agent + AgentCore Memory + AgentCore Gateway and AgentCore Identity for shared tools
**Lab 4:** Deployment to AgentCore Runtime and observability with AgentCore Observability
**Lab 5:** Production quality monitoring with AgentCore Evaluations
**Lab 6:** Customer-facing application with authentication
Ready to build? [Start with Lab 1 →](lab-01-create-an-agent.ipynb)
Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

@@ -0,0 +1,34 @@
Laptop Maintenance and Care Guide
DAILY CARE
- Keep laptop on hard flat surfaces for proper ventilation
- Avoid eating or drinking near the laptop
- Clean screen gently with microfiber cloth
- Close laptop carefully without slamming
WEEKLY MAINTENANCE
- Clean keyboard with compressed air
- Wipe down exterior with damp cloth
- Check for software updates
- Empty downloads and temp folders
MONTHLY TASKS
- Deep clean vents and fans
- Check battery health and calibrate
- Update all drivers and software
- Run disk cleanup and defragmentation
- Backup important data
PERFORMANCE OPTIMIZATION
- Uninstall unused programs
- Manage startup programs
- Check hard drive health
- Monitor temperature levels
- Keep 15-20% disk space free
WARNING SIGNS
- Excessive heat generation
- Unusual fan noise
- Slow boot times
- Frequent crashes or freezes
- Reduced battery life
@@ -0,0 +1,32 @@
Monitor Setup and Calibration Guide
PHYSICAL SETUP
- Position monitor at arms length 20-26 inches
- Top of screen at or below eye level
- Minimize glare from windows and lights
- Ensure stable desk placement
DISPLAY SETTINGS
1. Resolution - Set to native resolution
2. Brightness - 120-150 cd per m² for office use
3. Contrast - 1000 to 1 ratio recommended
4. Color temperature - 6500K for general use
5. Gamma - 2.2 for Windows 1.8 for Mac
COLOR CALIBRATION
- Use built-in calibration tools
- Consider hardware colorimeter for precision
- Calibrate monthly for professional work
- Save custom color profiles
ERGONOMIC CONSIDERATIONS
- Monitor height adjustment
- Tilt angle 10-20 degrees back
- Multiple monitor alignment
- Proper lighting setup
TROUBLESHOOTING
- Flickering - Check cable connections and refresh rate
- Color issues - Update graphics drivers
- Blurry text - Verify native resolution
- Dead pixels - Check warranty coverage
@@ -0,0 +1,38 @@
Smartphone Initial Setup and Configuration Guide
FIRST-TIME SETUP
1. Charge device to at least 50% before first use
2. Insert SIM card if applicable
3. Power on and follow setup wizard
4. Connect to Wi-Fi network
5. Sign in to your account Apple ID or Google Account
6. Configure security PIN fingerprint face unlock
7. Restore from backup or set up as new device
SECURITY CONFIGURATION
- Enable automatic screen lock
- Set up biometric authentication
- Configure Find My Device or Find My iPhone
- Enable automatic app updates
- Set up two-factor authentication
ESSENTIAL APPS
- Email client configuration
- Banking and payment apps
- Navigation and maps
- Communication apps
- Backup and cloud storage
BATTERY OPTIMIZATION
- Enable battery optimization settings
- Adjust screen brightness and timeout
- Manage background app refresh
- Configure location services
- Set up low power mode
DATA MANAGEMENT
- Configure cloud backup schedules
- Set up automatic photo backup
- Manage storage and cleanup
- Configure data usage limits
- Set up family sharing if applicable
@@ -0,0 +1,40 @@
Common Electronics Troubleshooting Guide
POWER ISSUES
1. Device won't turn on
- Check power cable connections
- Verify power outlet functionality
- Try different power cable if available
- Check for physical damage to device
2. Device turns off unexpectedly
- Check for overheating
- Verify power supply capacity
- Update device firmware
- Check for loose connections
CONNECTIVITY ISSUES
1. Wi-Fi connection problems
- Restart router and device
- Check Wi-Fi password accuracy
- Update network drivers
- Reset network settings
2. Bluetooth pairing issues
- Clear Bluetooth cache
- Remove and re-pair devices
- Check compatibility
- Update Bluetooth drivers
PERFORMANCE ISSUES
1. Slow performance
- Close unnecessary applications
- Check available storage space
- Run system updates
- Clear cache and temporary files
2. Audio and Video problems
- Check cable connections
- Update audio and video drivers
- Verify settings configuration
- Test with different media
@@ -0,0 +1,33 @@
Warranty and Service Information Guide
WARRANTY COVERAGE
- Manufacturing defects - Full coverage
- Normal wear and tear - Not covered
- Accidental damage - Extended warranty plans available
- Water damage - Generally not covered check IP rating
- Software issues - Covered if not user-caused
WARRANTY CLAIMS PROCESS
1. Gather proof of purchase and serial number
2. Contact technical support for diagnosis
3. Complete warranty claim form
4. Package device securely for shipping
5. Track repair status through online portal
SERVICE OPTIONS
- In-warranty repairs - Free parts and labor
- Out-of-warranty repairs - Quote provided before service
- Express service - Available for additional fee
- On-site service - Available for business customers
EXTENDED WARRANTY PLANS
- Accidental damage protection
- Extended coverage periods
- Premium technical support
- Replacement device programs
SUPPORT CHANNELS
- Online chat - 24/7 availability
- Phone support - Business hours
- Email support - 24-hour response
- Video troubleshooting - Appointment based
@@ -0,0 +1,33 @@
Wireless Connectivity Setup and Troubleshooting
WI-FI SETUP
1. Locate network settings in device
2. Select your network from available list
3. Enter network password carefully
4. Configure network security settings
5. Test internet connectivity
BLUETOOTH PAIRING
1. Enable Bluetooth on both devices
2. Put accessory in pairing mode
3. Select device from available list
4. Complete pairing process
5. Test functionality
COMMON WI-FI ISSUES
- Weak signal - Move closer to router check for interference
- Slow speeds - Check bandwidth usage update router firmware
- Connection drops - Update network drivers check power saving
- Cannot connect - Verify password restart network adapter
BLUETOOTH TROUBLESHOOTING
- Pairing fails - Clear Bluetooth cache restart both devices
- Audio stuttering - Check for interference update drivers
- Connection unstable - Verify compatibility check battery levels
- Device not found - Ensure proper pairing mode activation
SECURITY CONSIDERATIONS
- Use WPA3 encryption when available
- Regularly update device firmware
- Avoid public Wi-Fi for sensitive activities
- Monitor connected devices regularly
@@ -0,0 +1,738 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "overview",
"metadata": {},
"source": [
"## Lab 1: Creating a simple customer support agent prototype\n",
"\n",
"### Overview\n",
"\n",
"[Amazon Bedrock AgentCore](https://aws.amazon.com/bedrock/agentcore/) helps you deploying and operating AI agents securely at scale - using any framework and model. It provides you with the capability to move from prototype to production faster. \n",
"\n",
"In this 5-labs tutorial, we will demonstrate the end-to-end journey from prototype to production using a **Customer Support Agent**. For this example we will use [Google ADK (Agent Development Kit)](https://google.github.io/adk-docs/) with [Amazon Bedrock](https://aws.amazon.com/bedrock/) models via [LiteLLM](https://google.github.io/adk-docs/agents/models/litellm/). For your application you can use the framework and model of your choice. It's important to note that the concepts covered here can be applied using other frameworks and models as well.\n",
"\n",
"**Workshop Journey:**\n",
"- **Lab 1 (Current)**: Create Agent Prototype - Build a functional customer support agent\n",
"- **Lab 2**: Enhance with Memory - Add conversation context and personalization\n",
"- **Lab 3**: Scale with Gateway & Identity - Share tools across agents securely\n",
"- **Lab 4**: Deploy to Production - Use AgentCore Runtime with observability\n",
"- **Lab 5**: Build User Interface - Create a customer-facing application\n",
"\n",
"In this first lab, we'll build a Customer Support Agent prototype that will evolve throughout the workshop into a production-ready system serving multiple customers with persistent memory, shared tools, and full observability. Our agent will have the following local tools available:\n",
"- **get_return_policy()** - Get return policy for specific products\n",
"- **get_product_info()** - Get product information\n",
"- **web_search()** - Search the web for troubleshooting help\n",
"- **get_technical_support()** - Search a Bedrock Knowledge Base\n",
"\n",
"### Architecture for Lab 1\n",
"<div style=\"text-align:left\">\n",
" <img src=\"images/architecture_lab1_strands.png\" width=\"75%\"/>\n",
"</div>\n",
"\n",
"*Simple prototype running locally. In subsequent labs, we'll migrate this to AgentCore services with shared tools, persistent memory, and production-grade observability.*\n",
"\n",
"### Prerequisites\n",
"\n",
"* **AWS Account** with appropriate permissions\n",
"* **Python 3.10+** installed locally\n",
"* **AWS CLI configured** with credentials\n",
"* **Amazon Bedrock** model access enabled (e.g., Anthropic Claude or Amazon Nova)\n",
"* **Google ADK** and other libraries installed in the next cells\n",
"\n",
"#### Not using an AWS workshop account? \n",
"\n",
"If you are running this as a self-paced lab you MUST do additional 2 steps to create and deploy the cloudformation stack:\n",
"\n",
"**Step 0.1: [Only for Self-paced labs]** \n",
"\n",
"Uncomment the cell below with command `!aws sts get-caller-identity` to know your Sagemaker Role. Proceed to IAM in the AWS console and search for your Sagemaker Role. Now add the [IAM Policy, AWS managed policies and Trust relationships](https://catalog.us-east-1.prod.workshops.aws/workshops/850fcd5c-fd1f-48d7-932c-ad9babede979/en-US/00-prerequisites/02-self-paced#iam-policy-for-bedrock-agentcore-workshop) as described in the [workshop self-paced prerequisites](https://catalog.us-east-1.prod.workshops.aws/workshops/850fcd5c-fd1f-48d7-932c-ad9babede979/en-US/00-prerequisites/02-self-paced) lab."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "selfpaced1",
"metadata": {},
"outputs": [],
"source": [
"# Note: Uncomment and run only for self-paced labs\n",
"!aws sts get-caller-identity "
]
},
{
"cell_type": "markdown",
"id": "selfpaced2header",
"metadata": {},
"source": [
"**Step 0.2: [Only for Self-paced labs]**\n",
"\n",
"Now, once you have all the required permissions for this `prereq.sh` script, run the following command to deploy the cloudformation template."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "selfpaced2",
"metadata": {},
"outputs": [],
"source": [
"# Note: Uncomment and run only for self-paced labs\n",
"# !bash scripts/prereq.sh"
]
},
{
"cell_type": "markdown",
"id": "step1header",
"metadata": {},
"source": [
"### Step 1: Install Dependencies and Import Libraries\n",
"Before we start, let's install the dependencies for this lab. You will see some dependency errors - they're safe to ignore for the scope of this workshop."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "install",
"metadata": {},
"outputs": [],
"source": [
"# Install required packages \n",
"%pip install -r requirements.txt -q"
]
},
{
"cell_type": "markdown",
"id": "importheader",
"metadata": {},
"source": [
"We can now import the required libraries and initialize our boto3 session"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "imports",
"metadata": {},
"outputs": [],
"source": [
"# Import libraries\n",
"import asyncio\n",
"import boto3\n",
"from boto3.session import Session\n",
"\n",
"from lab_helpers.utils import suppress_warnings\n",
"suppress_warnings()\n",
"\n",
"from ddgs.exceptions import DDGSException, RatelimitException\n",
"from ddgs import DDGS\n",
"\n",
"from google.adk.agents import LlmAgent\n",
"from google.adk.models.lite_llm import LiteLlm\n",
"from google.adk.runners import Runner\n",
"from google.adk.sessions import InMemorySessionService\n",
"from google.genai import types"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "session",
"metadata": {},
"outputs": [],
"source": [
"# Get boto session\n",
"boto_session = Session()\n",
"region = boto_session.region_name"
]
},
{
"cell_type": "markdown",
"id": "step2header",
"metadata": {},
"source": [
"### Step 2: Implementing custom tools\n",
"\n",
"Next, we will implement the 4 tools which will be provided to the Customer Support Agent.\n",
"\n",
"In Google ADK, tools are simply Python functions with type hints and docstrings. ADK uses the function signature and documentation to provide context on each tool to the agent."
]
},
{
"cell_type": "markdown",
"id": "tool1header",
"metadata": {},
"source": [
"#### Tool 1: Get Return Policy\n",
"\n",
"**Purpose:** This tool helps customers understand return policies for different product categories. It provides detailed information about return windows, conditions, processes, and refund timelines so customers know exactly what to expect when returning items."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "tool1",
"metadata": {},
"outputs": [],
"source": [
"def get_return_policy(product_category: str) -> str:\n",
" \"\"\"Get return policy information for a specific product category.\n",
"\n",
" Args:\n",
" product_category: Electronics category (e.g., 'smartphones', 'laptops', 'accessories')\n",
"\n",
" Returns:\n",
" Formatted return policy details including timeframes and conditions\n",
" \"\"\"\n",
" # Mock return policy database - in real implementation, this would query policy database\n",
" return_policies = {\n",
" \"smartphones\": {\n",
" \"window\": \"30 days\",\n",
" \"condition\": \"Original packaging, no physical damage, factory reset required\",\n",
" \"process\": \"Online RMA portal or technical support\",\n",
" \"refund_time\": \"5-7 business days after inspection\",\n",
" \"shipping\": \"Free return shipping, prepaid label provided\",\n",
" \"warranty\": \"1-year manufacturer warranty included\",\n",
" },\n",
" \"laptops\": {\n",
" \"window\": \"30 days\",\n",
" \"condition\": \"Original packaging, all accessories, no software modifications\",\n",
" \"process\": \"Technical support verification required before return\",\n",
" \"refund_time\": \"7-10 business days after inspection\",\n",
" \"shipping\": \"Free return shipping with original packaging\",\n",
" \"warranty\": \"1-year manufacturer warranty, extended options available\",\n",
" },\n",
" \"accessories\": {\n",
" \"window\": \"30 days\",\n",
" \"condition\": \"Unopened packaging preferred, all components included\",\n",
" \"process\": \"Online return portal\",\n",
" \"refund_time\": \"3-5 business days after receipt\",\n",
" \"shipping\": \"Customer pays return shipping under $50\",\n",
" \"warranty\": \"90-day manufacturer warranty\",\n",
" },\n",
" }\n",
"\n",
" # Default policy for unlisted categories\n",
" default_policy = {\n",
" \"window\": \"30 days\",\n",
" \"condition\": \"Original condition with all included components\",\n",
" \"process\": \"Contact technical support\",\n",
" \"refund_time\": \"5-7 business days after inspection\",\n",
" \"shipping\": \"Return shipping policies vary\",\n",
" \"warranty\": \"Standard manufacturer warranty applies\",\n",
" }\n",
"\n",
" policy = return_policies.get(product_category.lower(), default_policy)\n",
" return (\n",
" f\"Return Policy - {product_category.title()}:\\n\\n\"\n",
" f\"• Return window: {policy['window']} from delivery\\n\"\n",
" f\"• Condition: {policy['condition']}\\n\"\n",
" f\"• Process: {policy['process']}\\n\"\n",
" f\"• Refund timeline: {policy['refund_time']}\\n\"\n",
" f\"• Shipping: {policy['shipping']}\\n\"\n",
" f\"• Warranty: {policy['warranty']}\"\n",
" )\n",
"\n",
"\n",
"print(\"✅ Return policy tool ready\")"
]
},
{
"cell_type": "markdown",
"id": "tool2header",
"metadata": {},
"source": [
"#### Tool 2: Get Product Information\n",
"\n",
"**Purpose:** This tool provides customers with comprehensive product details including warranties, available models, key features, shipping policies, and return information. It helps customers make informed purchasing decisions and understand what they're buying."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "tool2",
"metadata": {},
"outputs": [],
"source": [
"def get_product_info(product_type: str) -> str:\n",
" \"\"\"Get detailed technical specifications and information for electronics products.\n",
"\n",
" Args:\n",
" product_type: Electronics product type (e.g., 'laptops', 'smartphones', 'headphones', 'monitors')\n",
" Returns:\n",
" Formatted product information including warranty, features, and policies\n",
" \"\"\"\n",
" # Mock product catalog - in real implementation, this would query a product database\n",
" products = {\n",
" \"laptops\": {\n",
" \"warranty\": \"1-year manufacturer warranty + optional extended coverage\",\n",
" \"specs\": \"Intel/AMD processors, 8-32GB RAM, SSD storage, various display sizes\",\n",
" \"features\": \"Backlit keyboards, USB-C/Thunderbolt, Wi-Fi 6, Bluetooth 5.0\",\n",
" \"compatibility\": \"Windows 11, macOS, Linux support varies by model\",\n",
" \"support\": \"Technical support and driver updates included\",\n",
" },\n",
" \"smartphones\": {\n",
" \"warranty\": \"1-year manufacturer warranty\",\n",
" \"specs\": \"5G/4G connectivity, 128GB-1TB storage, multiple camera systems\",\n",
" \"features\": \"Wireless charging, water resistance, biometric security\",\n",
" \"compatibility\": \"iOS/Android, carrier unlocked options available\",\n",
" \"support\": \"Software updates and technical support included\",\n",
" },\n",
" \"headphones\": {\n",
" \"warranty\": \"1-year manufacturer warranty\",\n",
" \"specs\": \"Wired/wireless options, noise cancellation, 20Hz-20kHz frequency\",\n",
" \"features\": \"Active noise cancellation, touch controls, voice assistant\",\n",
" \"compatibility\": \"Bluetooth 5.0+, 3.5mm jack, USB-C charging\",\n",
" \"support\": \"Firmware updates via companion app\",\n",
" },\n",
" \"monitors\": {\n",
" \"warranty\": \"3-year manufacturer warranty\",\n",
" \"specs\": \"4K/1440p/1080p resolutions, IPS/OLED panels, various sizes\",\n",
" \"features\": \"HDR support, high refresh rates, adjustable stands\",\n",
" \"compatibility\": \"HDMI, DisplayPort, USB-C inputs\",\n",
" \"support\": \"Color calibration and technical support\",\n",
" },\n",
" }\n",
" product = products.get(product_type.lower())\n",
" if not product:\n",
" return f\"Technical specifications for {product_type} not available. Please contact our technical support team for detailed product information and compatibility requirements.\"\n",
"\n",
" return (\n",
" f\"Technical Information - {product_type.title()}:\\n\\n\"\n",
" f\"• Warranty: {product['warranty']}\\n\"\n",
" f\"• Specifications: {product['specs']}\\n\"\n",
" f\"• Key Features: {product['features']}\\n\"\n",
" f\"• Compatibility: {product['compatibility']}\\n\"\n",
" f\"• Support: {product['support']}\"\n",
" )\n",
"\n",
"\n",
"print(\"✅ get_product_info tool ready\")"
]
},
{
"cell_type": "markdown",
"id": "tool3header",
"metadata": {},
"source": [
"#### Tool 3: Web-search\n",
"\n",
"**Purpose:** This tool allows customers to get troubleshooting support or suggestions on product recommendations etc."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "tool3",
"metadata": {},
"outputs": [],
"source": [
"def web_search(keywords: str, region: str, max_results: int) -> str:\n",
" \"\"\"Search the web for updated information.\n",
"\n",
" Args:\n",
" keywords: The search query keywords.\n",
" region: The search region: wt-wt, us-en, uk-en, ru-ru, etc..\n",
" max_results: The maximum number of results to return.\n",
" Returns:\n",
" List of dictionaries with search results.\n",
" \"\"\"\n",
" if not region:\n",
" region = \"us-en\"\n",
" if not max_results:\n",
" max_results = 5\n",
" try:\n",
" results = DDGS().text(keywords, region=region, max_results=max_results)\n",
" return results if results else \"No results found.\"\n",
" except RatelimitException:\n",
" return \"Rate limit reached. Please try again later.\"\n",
" except DDGSException as e:\n",
" return f\"Search error: {e}\"\n",
" except Exception as e:\n",
" return f\"Search error: {str(e)}\"\n",
"\n",
"\n",
"print(\"✅ Web search tool ready\")"
]
},
{
"cell_type": "markdown",
"id": "kbdownloadheader",
"metadata": {},
"source": [
"#### Customer Support Agent - Knowledge Base Integration Steps\n",
"\n",
"##### Download product technical_support files from S3"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "kbdownload",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"\n",
"\n",
"def download_files():\n",
" # Get account and region\n",
" account_id = boto3.client(\"sts\").get_caller_identity()[\"Account\"]\n",
" region = boto3.Session().region_name\n",
" bucket_name = f\"{account_id}-{region}-kb-data-bucket\"\n",
"\n",
" # Create local folder\n",
" os.makedirs(\"knowledge_base_data\", exist_ok=True)\n",
"\n",
" # Download all files\n",
" s3 = boto3.client(\"s3\")\n",
" objects = s3.list_objects_v2(Bucket=bucket_name)\n",
"\n",
" for obj in objects[\"Contents\"]:\n",
" file_name = obj[\"Key\"]\n",
" s3.download_file(bucket_name, file_name, f\"knowledge_base_data/{file_name}\")\n",
" print(f\"Downloaded: {file_name}\")\n",
"\n",
" print(\"All files saved to: knowledge_base_data/\")\n",
"\n",
"\n",
"# Run it\n",
"download_files()"
]
},
{
"cell_type": "markdown",
"id": "kbsyncheader",
"metadata": {},
"source": [
"#### Knowledge Base Sync Job\n",
"\n",
"##### Sync the knowledge base with product technical_support files from S3 which can be integrated with the agent"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "kbsync",
"metadata": {},
"outputs": [],
"source": [
"import time\n",
"\n",
"# Get parameters\n",
"ssm = boto3.client(\"ssm\")\n",
"bedrock = boto3.client(\"bedrock-agent\")\n",
"s3 = boto3.client(\"s3\")\n",
"\n",
"account_id = boto3.client(\"sts\").get_caller_identity()[\"Account\"]\n",
"region = boto3.Session().region_name\n",
"\n",
"kb_id = ssm.get_parameter(Name=f\"/{account_id}-{region}/kb/knowledge-base-id\")[\n",
" \"Parameter\"\n",
"][\"Value\"]\n",
"ds_id = ssm.get_parameter(Name=f\"/{account_id}-{region}/kb/data-source-id\")[\n",
" \"Parameter\"\n",
"][\"Value\"]\n",
"\n",
"# Get file names from S3 bucket\n",
"bucket_name = f\"{account_id}-{region}-kb-data-bucket\"\n",
"s3_objects = s3.list_objects_v2(Bucket=bucket_name)\n",
"file_names = [obj[\"Key\"] for obj in s3_objects.get(\"Contents\", [])]\n",
"\n",
"# Start sync job\n",
"response = bedrock.start_ingestion_job(\n",
" knowledgeBaseId=kb_id, dataSourceId=ds_id, description=\"Quick sync\"\n",
")\n",
"\n",
"job_id = response[\"ingestionJob\"][\"ingestionJobId\"]\n",
"print(\"Bedrock knowledge base sync job started, ingesting the data files from s3\")\n",
"\n",
"# Monitor until complete\n",
"while True:\n",
" job = bedrock.get_ingestion_job(\n",
" knowledgeBaseId=kb_id, dataSourceId=ds_id, ingestionJobId=job_id\n",
" )[\"ingestionJob\"]\n",
"\n",
" status = job[\"status\"]\n",
"\n",
" if status in [\"COMPLETE\", \"FAILED\"]:\n",
" break\n",
"\n",
" time.sleep(10)\n",
"\n",
"# Print final result\n",
"if status == \"COMPLETE\":\n",
" file_count = job.get(\"statistics\", {}).get(\"numberOfDocumentsScanned\", 0)\n",
" files_list = \", \".join(file_names)\n",
" print(\n",
" f\"Bedrock knowledge base sync job completed Successfully, ingested {file_count} files\"\n",
" )\n",
" print(f\"Files ingested: {files_list}\")\n",
"else:\n",
" print(f\"Bedrock knowledge base sync job failed with status: {status}\")"
]
},
{
"cell_type": "markdown",
"id": "tool4header",
"metadata": {},
"source": [
"#### Tool 4: Get Technical Support\n",
"\n",
"**Purpose:** This tool provides customers with comprehensive technical support and troubleshooting assistance by accessing our knowledge base of electronics documentation. It includes detailed setup guides, maintenance instructions, troubleshooting steps, connectivity solutions, and warranty service information. This tool helps customers resolve technical issues, properly configure their devices, and understand maintenance requirements for optimal product performance.\n",
"\n",
"Since we are using Google ADK instead of Strands, we replace the Strands `retrieve` tool with a direct boto3 call to the Bedrock Knowledge Base `retrieve` API."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "tool4",
"metadata": {},
"outputs": [],
"source": [
"def get_technical_support(issue_description: str) -> str:\n",
" \"\"\"Get technical support and troubleshooting guidance by searching the knowledge base.\n",
"\n",
" Args:\n",
" issue_description: Description of the technical issue or question.\n",
"\n",
" Returns:\n",
" Relevant technical support documentation and troubleshooting steps.\n",
" \"\"\"\n",
" try:\n",
" # Get KB ID from parameter store\n",
" ssm_client = boto3.client(\"ssm\")\n",
" account_id = boto3.client(\"sts\").get_caller_identity()[\"Account\"]\n",
" region = boto3.Session().region_name\n",
"\n",
" kb_id = ssm_client.get_parameter(\n",
" Name=f\"/{account_id}-{region}/kb/knowledge-base-id\"\n",
" )[\"Parameter\"][\"Value\"]\n",
" print(f\"Successfully retrieved KB ID: {kb_id}\")\n",
"\n",
" # Use boto3 bedrock-agent-runtime to retrieve from knowledge base\n",
" bedrock_agent_runtime = boto3.client(\n",
" \"bedrock-agent-runtime\", region_name=region\n",
" )\n",
"\n",
" response = bedrock_agent_runtime.retrieve(\n",
" knowledgeBaseId=kb_id,\n",
" retrievalQuery={\"text\": issue_description},\n",
" retrievalConfiguration={\n",
" \"vectorSearchConfiguration\": {\n",
" \"numberOfResults\": 3,\n",
" }\n",
" },\n",
" )\n",
"\n",
" # Extract and format results\n",
" results = response.get(\"retrievalResults\", [])\n",
" if not results:\n",
" return \"No relevant technical support documentation found for this issue.\"\n",
"\n",
" formatted_results = []\n",
" for i, result in enumerate(results, 1):\n",
" content = result.get(\"content\", {}).get(\"text\", \"\")\n",
" score = result.get(\"score\", 0)\n",
" if score >= 0.4:\n",
" formatted_results.append(f\"--- Result {i} (relevance: {score:.2f}) ---\\n{content}\")\n",
"\n",
" if not formatted_results:\n",
" return \"No sufficiently relevant technical support documentation found. Please contact our support team directly.\"\n",
"\n",
" return \"\\n\\n\".join(formatted_results)\n",
"\n",
" except Exception as e:\n",
" print(f\"Detailed error in get_technical_support: {str(e)}\")\n",
" return f\"Unable to access technical support documentation. Error: {str(e)}\"\n",
"\n",
"\n",
"print(\"✅ Technical support tool ready\")"
]
},
{
"cell_type": "markdown",
"id": "step4header",
"metadata": {},
"source": [
"### Step 4: Create and Configure the Customer Support Agent\n",
"\n",
"Next, we will create the Customer Support Agent using Google ADK's `LlmAgent` with an Amazon Bedrock model via LiteLLM. We provide the model, the list of tools implemented in the previous step, and a system instruction.\n",
"\n",
"We also define a helper function `call_agent` that handles the async ADK runner invocation pattern."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "createagent",
"metadata": {},
"outputs": [],
"source": [
"SYSTEM_PROMPT = \"\"\"You are a helpful and professional customer support assistant for an electronics e-commerce company.\n",
"Your role is to:\n",
"- Provide accurate information using the tools available to you\n",
"- Support the customer with technical information and product specifications, and maintenance questions\n",
"- Be friendly, patient, and understanding with customers\n",
"- Always offer additional help after answering questions\n",
"- If you can't help with something, direct customers to the appropriate contact\n",
"\n",
"You have access to the following tools:\n",
"1. get_return_policy() - For warranty and return policy questions\n",
"2. get_product_info() - To get information about a specific product\n",
"3. web_search() - To access current technical documentation, or for updated information. \n",
"4. get_technical_support() - For troubleshooting issues, setup guides, maintenance tips, and detailed technical assistance\n",
"For any technical problems, setup questions, or maintenance concerns, always use the get_technical_support() tool as it contains our comprehensive technical documentation and step-by-step guides.\n",
"\n",
"Always use the appropriate tool to get accurate, up-to-date information rather than making assumptions about electronic products or specifications.\"\"\"\n",
"\n",
"APP_NAME = \"customer_support_agent\"\n",
"USER_ID = \"user_123\"\n",
"SESSION_ID = \"lab01_session\"\n",
"\n",
"# Create the Google ADK agent using Amazon Bedrock via LiteLLM\n",
"agent = LlmAgent(\n",
" model=LiteLlm(model=\"bedrock/global.anthropic.claude-haiku-4-5-20251001-v1:0\"),\n",
" name=\"customer_support_agent\",\n",
" description=\"A customer support agent for an electronics e-commerce company.\",\n",
" instruction=SYSTEM_PROMPT,\n",
" tools=[\n",
" get_product_info, # Tool 1: Simple product information lookup\n",
" get_return_policy, # Tool 2: Simple return policy lookup\n",
" web_search, # Tool 3: Access the web for updated information\n",
" get_technical_support, # Tool 4: Technical support & troubleshooting\n",
" ],\n",
")\n",
"\n",
"\n",
"async def call_agent(query: str) -> str:\n",
" \"\"\"Invoke the ADK agent with a user query and return the response.\"\"\"\n",
" session_service = InMemorySessionService()\n",
" await session_service.create_session(\n",
" app_name=APP_NAME, user_id=USER_ID, session_id=SESSION_ID\n",
" )\n",
" runner = Runner(\n",
" agent=agent, app_name=APP_NAME, session_service=session_service\n",
" )\n",
" content = types.Content(\n",
" role=\"user\", parts=[types.Part(text=query)]\n",
" )\n",
" events = runner.run_async(\n",
" user_id=USER_ID, session_id=SESSION_ID, new_message=content\n",
" )\n",
" final_response = \"\"\n",
" async for event in events:\n",
" if event.is_final_response():\n",
" final_response = event.content.parts[0].text\n",
" return final_response\n",
"\n",
"\n",
"print(\"Customer Support Agent created successfully!\")"
]
},
{
"cell_type": "markdown",
"id": "step5header",
"metadata": {},
"source": [
"### Step 5: Test the Customer Support Agent\n",
"\n",
"Let's test our agent with sample queries to ensure all tools work correctly.\n",
"\n",
"#### Test Return check"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "test1",
"metadata": {},
"outputs": [],
"source": [
"response = await call_agent(\"What's the return policy for my thinkpad X1 Carbon?\")\n",
"print(response)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "test2",
"metadata": {},
"outputs": [],
"source": [
"response = await call_agent(\"My laptop won't turn on, what should I check?\")\n",
"print(response)"
]
},
{
"cell_type": "markdown",
"id": "testtroubleshootheader",
"metadata": {},
"source": [
"#### Test Troubleshooting"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "test3",
"metadata": {},
"outputs": [],
"source": [
"response = await call_agent(\n",
" \"I bought an iphone 14 last month. I don't like it because it heats up. How do I solve it?\"\n",
")\n",
"print(response)"
]
},
{
"cell_type": "markdown",
"id": "complete",
"metadata": {},
"source": [
"## 🎉 Lab 1 Complete!\n",
"\n",
"You've successfully created a functional Customer Support Agent prototype using **Google ADK** with **Amazon Bedrock** models via **LiteLLM**! Here's what you accomplished:\n",
"\n",
"- Built a Google ADK agent with 4 tools (return policy, product info, web search, technical support KB)\n",
"- Used LiteLLM to invoke Amazon Bedrock models from the ADK framework\n",
"- Tested multi-tool interactions and web search capabilities \n",
"- Established the foundation for our production journey \n",
"\n",
"### Current Limitations (We'll fix these!)\n",
"- **Single user conversation memory** - local conversation session, multiple customers need multiple sessions.\n",
"- **Conversation history limited to session** - no long term memory or cross session information is available in the conversation.\n",
"- **Tools reusability** - tools aren't reusable across different agents \n",
"- **Running locally only** - not scalable\n",
"- **Identity** - No user and/or agent identity or access control\n",
"- **Observability** - Limited observability into agent behavior\n",
"- **Existing APIs** - No access to existing enterprise APIs for customer data\n",
"\n",
"##### Next Up [Lab 2: Personalize our agent by adding memory →](lab-02-agentcore-memory.ipynb)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "ag-gadk-01-9March-0800",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.13"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
@@ -0,0 +1,758 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Lab 2: Personalize our agent by adding memory\n",
"\n",
"### Overview\n",
"\n",
"In Lab 1, you built a Customer Support Agent that worked well for a single user in a local session. However, real-world customer support needs to scale beyond a single user running in a local environment.\n",
"\n",
"When we run an **Agent in Production**, we'll need:\n",
"- **Multi-User Support**: Handle thousands of customers simultaneously\n",
"- **Persistent Storage**: Save conversations beyond session lifecycle\n",
"- **Long-Term Learning**: Extract customer preferences and behavioral patterns\n",
"- **Cross-Session Continuity**: Remember customers across different interactions\n",
"\n",
"**Workshop Progress:**\n",
"- **Lab 1 (Done)**: Create Agent Prototype - Build a functional customer support agent\n",
"- **Lab 2 (Current)**: Enhance with Memory - Add conversation context and personalization\n",
"- **Lab 3**: Scale with Gateway & Identity - Share tools across agents securely\n",
"- **Lab 4**: Deploy to Production - Use AgentCore Runtime with observability\n",
"- **Lab 5**: Build User Interface - Create a customer-facing application\n",
"\n",
"In this lab, you'll add the missing persistence and learning layer that transforms your Goldfish-Agent (forgets the conversation in seconds) into a smart personalized Assistant.\n",
"\n",
"Memory is a critical component of intelligence. While Large Language Models (LLMs) have impressive capabilities, they lack persistent memory across conversations. [Amazon Bedrock AgentCore Memory](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/memory-getting-started.html) addresses this limitation by providing a managed service that enables AI agents to maintain context over time, remember important facts, and deliver consistent, personalized experiences.\n",
"\n",
"AgentCore Memory operates on two levels:\n",
"- **Short-Term Memory**: Immediate conversation context and session-based information that provides continuity within a single interaction or closely related sessions.\n",
"- **Long-Term Memory**: Persistent information extracted and stored across multiple conversations, including facts, preferences, and summaries that enable personalized experiences over time.\n",
"\n",
"### Architecture for Lab 2\n",
"<div style=\"text-align:left\">\n",
" <img src=\"images/architecture_lab2_memory.png\" width=\"75%\"/>\n",
"</div>\n",
"\n",
"*Multi-user agent with persistent short term and long term memory capabilities.*\n",
"\n",
"### Prerequisites\n",
"\n",
"* **AWS Account** with appropriate permissions\n",
"* **Python 3.10+** installed locally\n",
"* **AWS CLI configured** with credentials\n",
"* **Google ADK** and other libraries installed in the next cells\n",
"* These resources are created for you within an AWS workshop account\n",
" - AWS Lambda function \n",
" - Amazon Bedrock Knowledge Base\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Step 1: Import Libraries\n",
"\n",
"Let's import the libraries for AgentCore Memory. For it, we will use the [Amazon Bedrock AgentCore Memory](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/memory.html) SDK.\n",
"\n",
"We'll also import Google ADK components to build our agent, using **LiteLLM** to invoke **Amazon Bedrock** models.\n",
"\n",
"> **Note:** Google ADK uses async patterns. In Jupyter notebooks, we can use `await` directly at the top level since the notebook already runs an asyncio event loop.\n",
"\n",
"> **Note:** We use `LiteLlm` from Google ADK to route requests to Amazon Bedrock models. This means we don't need a Gemini API key — we use AWS credentials instead."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import logging\n",
"import os\n",
"import asyncio\n",
"import uuid\n",
"import time\n",
"\n",
"from lab_helpers.utils import suppress_warnings\n",
"suppress_warnings()\n",
"\n",
"import boto3\n",
"from boto3.session import Session\n",
"\n",
"from bedrock_agentcore_starter_toolkit.operations.memory.manager import MemoryManager\n",
"from bedrock_agentcore.memory import MemoryClient\n",
"from bedrock_agentcore.memory.constants import StrategyType\n",
"\n",
"from ddgs.exceptions import DDGSException, RatelimitException\n",
"from ddgs import DDGS\n",
"\n",
"from google.adk.agents import LlmAgent\n",
"from google.adk.models.lite_llm import LiteLlm\n",
"from google.adk.runners import Runner\n",
"from google.adk.sessions import InMemorySessionService\n",
"from google.genai import types\n",
"\n",
"from lab_helpers.utils import put_ssm_parameter\n",
"\n",
"# Get boto session and region\n",
"boto_session = Session()\n",
"REGION = boto_session.region_name\n",
"print(f\"Region: {REGION}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Step 2: Create Bedrock AgentCore Memory resources\n",
"\n",
"Amazon Bedrock AgentCore Memory is a fully managed service that provides persistent memory for AI agents. Let's create our memory resource with two strategies:\n",
"\n",
"1. **User Preference Strategy**: Captures customer preferences and behavior patterns\n",
"2. **Semantic Strategy**: Stores factual information from conversations using vector embeddings\n",
"\n",
"Each strategy has its own namespace for organized retrieval."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"memory_name = \"CustomerSupportMemory\"\n",
"\n",
"memory_manager = MemoryManager(region_name=REGION)\n",
"memory = memory_manager.get_or_create_memory(\n",
" name=memory_name,\n",
" strategies=[\n",
" {\n",
" StrategyType.USER_PREFERENCE.value: {\n",
" \"name\": \"CustomerPreferences\",\n",
" \"description\": \"Captures customer preferences and behavior\",\n",
" \"namespaces\": [\"support/customer/{actorId}/preferences/\"],\n",
" }\n",
" },\n",
" {\n",
" StrategyType.SEMANTIC.value: {\n",
" \"name\": \"CustomerSupportSemantic\",\n",
" \"description\": \"Stores facts from conversations\",\n",
" \"namespaces\": [\"support/customer/{actorId}/semantic/\"],\n",
" }\n",
" },\n",
" ]\n",
")\n",
"memory_id = memory[\"id\"]\n",
"put_ssm_parameter(\"/app/customersupport/agentcore/memory_id\", memory_id)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"if memory_id:\n",
" print(\"\\u2705 AgentCore Memory created successfully!\")\n",
" print(f\"Memory ID: {memory_id}\")\n",
"else:\n",
" print(\"Memory resource not created. Try Again !\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 3: Seed previous customer interactions\n",
"\n",
"**Why are we seeding memory?**\n",
"\n",
"In production, agents accumulate memory naturally through customer interactions. However, for this lab, we're seeding historical conversations to demonstrate how Long-Term Memory (LTM) works without waiting for real conversations.\n",
"\n",
"**How memory processing works:**\n",
"1. `create_event` stores interactions in **Short-Term Memory** (STM) instantly\n",
"2. STM is asynchronously processed by **Long-Term Memory** strategies\n",
"3. LTM extracts patterns, preferences, and facts for future retrieval\n",
"\n",
"Let's seed some customer history to see this in action:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from lab_helpers.lab2_memory import ACTOR_ID\n",
"\n",
"\n",
"# Seed with previous customer interactions\n",
"previous_interactions = [\n",
" (\"I'm having issues with my MacBook Pro overheating during video editing.\", \"USER\"),\n",
" (\n",
" \"I can help with that thermal issue. For video editing workloads, let's check your Activity Monitor and adjust performance settings. Your MacBook Pro order #MB-78432 is still under warranty.\",\n",
" \"ASSISTANT\",\n",
" ),\n",
" (\n",
" \"What's the return policy on gaming headphones? I need low latency for competitive FPS games\",\n",
" \"USER\",\n",
" ),\n",
" (\n",
" \"For gaming headphones, you have 30 days to return. Since you're into competitive FPS, I'd recommend checking the audio latency specs - most gaming models have <40ms latency.\",\n",
" \"ASSISTANT\",\n",
" ),\n",
" (\n",
" \"I need a laptop under $1200 for programming. Prefer 16GB RAM minimum and good Linux compatibility. I like ThinkPad models.\",\n",
" \"USER\",\n",
" ),\n",
" (\n",
" \"Perfect! For development work, I'd suggest looking at our ThinkPad E series or Dell XPS models. Both have excellent Linux support and 16GB RAM options within your budget.\",\n",
" \"ASSISTANT\",\n",
" ),\n",
"]\n",
"\n",
"# Save previous interactions\n",
"if memory_id:\n",
" try:\n",
" memory_client = MemoryClient(region_name=REGION)\n",
" memory_client.create_event(\n",
" memory_id=memory_id,\n",
" actor_id=ACTOR_ID,\n",
" session_id=\"previous_session\",\n",
" messages=previous_interactions,\n",
" )\n",
" print(\"\\u2705 Seeded customer history successfully\")\n",
" print(\"\\U0001f4dd Interactions saved to Short-Term Memory\")\n",
" print(\"\\u23f3 Long-Term Memory processing will begin automatically...\")\n",
" except Exception as e:\n",
" print(f\"\\u26a0\\ufe0f Error seeding history: {e}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Understanding Memory Processing\n",
"\n",
"After creating events with `create_event`, AgentCore Memory processes the data in two stages:\n",
"\n",
"1. **Immediate**: Messages stored in Short-Term Memory (STM)\n",
"2. **Asynchronous**: STM processed into Long-Term Memory (LTM) strategies\n",
"\n",
"LTM processing typically takes 20-30 seconds as the system:\n",
"- Analyzes conversation patterns\n",
"- Extracts customer preferences and behaviors\n",
"- Creates semantic embeddings for factual information\n",
"- Organizes memories by namespace for efficient retrieval\n",
"\n",
"Let's check if our Long-Term Memory processing is complete by retrieving customer preferences:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import time\n",
"\n",
"# Wait for Long-Term Memory processing to complete\n",
"print(\"\\U0001f50d Checking for processed Long-Term Memories...\")\n",
"retries = 0\n",
"max_retries = 6 # 1 minute wait\n",
"\n",
"while retries < max_retries:\n",
" memories = memory_client.retrieve_memories(\n",
" memory_id=memory_id,\n",
" namespace=f\"support/customer/{ACTOR_ID}/preferences/\",\n",
" query=\"can you summarize the support issue\",\n",
" )\n",
"\n",
" if memories:\n",
" print(\n",
" f\"\\u2705 Found {len(memories)} preference memories after {retries * 10} seconds!\"\n",
" )\n",
" break\n",
"\n",
" retries += 1\n",
" if retries < max_retries:\n",
" print(\n",
" f\"\\u23f3 Still processing... waiting 10 more seconds (attempt {retries}/{max_retries})\"\n",
" )\n",
" time.sleep(10)\n",
" else:\n",
" print(\n",
" \"\\u26a0\\ufe0f Memory processing is taking longer than expected. This can happen with overloading..\"\n",
" )\n",
" break\n",
"\n",
"print(\n",
" \"\\U0001f3af AgentCore Memory automatically extracted these customer preferences from our seeded conversations:\"\n",
")\n",
"print(\"=\" * 80)\n",
"\n",
"for i, memory in enumerate(memories, 1):\n",
" if isinstance(memory, dict):\n",
" content = memory.get(\"content\", {})\n",
" if isinstance(content, dict):\n",
" text = content.get(\"text\", \"\")\n",
" print(f\" {i}. {text}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Exploring Semantic Memory\n",
"\n",
"Semantic memory stores factual information from conversations using vector embeddings. This enables similarity-based retrieval of relevant facts and context."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import time\n",
"\n",
"# Retrieve semantic memories (factual information)\n",
"while True:\n",
" semantic_memories = memory_client.retrieve_memories(\n",
" memory_id=memory_id,\n",
" namespace=f\"support/customer/{ACTOR_ID}/semantic/\",\n",
" query=\"information on the technical support issue\",\n",
" )\n",
" print(\"\\U0001f9e0 AgentCore Memory identified these factual details from conversations:\")\n",
" print(\"=\" * 80)\n",
" if semantic_memories:\n",
" break\n",
" time.sleep(10)\n",
"for i, memory in enumerate(semantic_memories, 1):\n",
" if isinstance(memory, dict):\n",
" content = memory.get(\"content\", {})\n",
" if isinstance(content, dict):\n",
" text = content.get(\"text\", \"\")\n",
" print(f\" {i}. {text}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 4: Create a Customer Support Agent with memory\n",
"\n",
"Next, we will implement the Customer Support Agent just as we did in Lab 1, but this time we will integrate AgentCore Memory.\n",
"\n",
"Since Google ADK does not have a built-in `AgentCoreMemorySessionManager` (like Strands does), we implement memory integration explicitly:\n",
"\n",
"1. **Before each query**: Retrieve relevant customer context from memory (preferences + semantic)\n",
"2. **Inject context**: Prepend retrieved memories to the user query so the LLM has personalization context\n",
"3. **After each response**: Save the interaction (query + response) back to memory for future use\n",
"\n",
"This pattern gives us the same memory-enhanced behavior while keeping full control over the retrieval and storage logic.\n",
"\n",
"> **Note:** We use `LlmAgent` with `LiteLlm` to invoke Amazon Bedrock models (Claude) through Google ADK. This means the LLM calls go through your AWS credentials, not a Gemini API key.\n",
"\n",
"> **Note:** We reuse the same tools from Lab 1 (get_return_policy, get_product_info, web_search, get_technical_support)."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# ============================================================\n",
"# Define the same tools from Lab 1\n",
"# ============================================================\n",
"\n",
"def get_return_policy(product_category: str) -> str:\n",
" \"\"\"\n",
" Get return policy information for a specific product category.\n",
"\n",
" Args:\n",
" product_category: Electronics category (e.g., 'smartphones', 'laptops', 'accessories')\n",
"\n",
" Returns:\n",
" Formatted return policy details including timeframes and conditions\n",
" \"\"\"\n",
" return_policies = {\n",
" \"smartphones\": {\n",
" \"window\": \"30 days\",\n",
" \"condition\": \"Original packaging, no physical damage, factory reset required\",\n",
" \"process\": \"Online RMA portal or technical support\",\n",
" \"refund_time\": \"5-7 business days after inspection\",\n",
" \"shipping\": \"Free return shipping, prepaid label provided\",\n",
" \"warranty\": \"1-year manufacturer warranty included\",\n",
" },\n",
" \"laptops\": {\n",
" \"window\": \"30 days\",\n",
" \"condition\": \"Original packaging, all accessories, no software modifications\",\n",
" \"process\": \"Technical support verification required before return\",\n",
" \"refund_time\": \"7-10 business days after inspection\",\n",
" \"shipping\": \"Free return shipping with original packaging\",\n",
" \"warranty\": \"1-year manufacturer warranty, extended options available\",\n",
" },\n",
" \"accessories\": {\n",
" \"window\": \"30 days\",\n",
" \"condition\": \"Unopened packaging preferred, all components included\",\n",
" \"process\": \"Online return portal\",\n",
" \"refund_time\": \"3-5 business days after receipt\",\n",
" \"shipping\": \"Customer pays return shipping under $50\",\n",
" \"warranty\": \"90-day manufacturer warranty\",\n",
" },\n",
" }\n",
" default_policy = {\n",
" \"window\": \"30 days\",\n",
" \"condition\": \"Original condition with all included components\",\n",
" \"process\": \"Contact technical support\",\n",
" \"refund_time\": \"5-7 business days after inspection\",\n",
" \"shipping\": \"Return shipping policies vary\",\n",
" \"warranty\": \"Standard manufacturer warranty applies\",\n",
" }\n",
" policy = return_policies.get(product_category.lower(), default_policy)\n",
" return (\n",
" f\"Return Policy - {product_category.title()}:\\n\\n\"\n",
" f\"\\u2022 Return window: {policy['window']} from delivery\\n\"\n",
" f\"\\u2022 Condition: {policy['condition']}\\n\"\n",
" f\"\\u2022 Process: {policy['process']}\\n\"\n",
" f\"\\u2022 Refund timeline: {policy['refund_time']}\\n\"\n",
" f\"\\u2022 Shipping: {policy['shipping']}\\n\"\n",
" f\"\\u2022 Warranty: {policy['warranty']}\"\n",
" )\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def get_product_info(product_type: str) -> str:\n",
" \"\"\"\n",
" Get detailed technical specifications and information for electronics products.\n",
"\n",
" Args:\n",
" product_type: Electronics product type (e.g., 'laptops', 'smartphones', 'headphones', 'monitors')\n",
" Returns:\n",
" Formatted product information including warranty, features, and policies\n",
" \"\"\"\n",
" products = {\n",
" \"laptops\": {\n",
" \"warranty\": \"1-year standard, 3-year extended available\",\n",
" \"specs\": \"Intel/AMD processors, 8-64GB RAM, SSD storage\",\n",
" \"features\": \"Backlit keyboards, fingerprint readers, Thunderbolt ports\",\n",
" \"compatibility\": \"Windows, Linux, macOS (Apple only)\",\n",
" \"support\": \"24/7 technical support, on-site repair options\",\n",
" },\n",
" \"smartphones\": {\n",
" \"warranty\": \"1-year manufacturer, 2-year extended\",\n",
" \"specs\": \"Latest processors, 6-12GB RAM, 128GB-1TB storage\",\n",
" \"features\": \"5G capable, water resistant, wireless charging\",\n",
" \"compatibility\": \"iOS or Android ecosystem\",\n",
" \"support\": \"In-store and mail-in repair services\",\n",
" },\n",
" \"headphones\": {\n",
" \"warranty\": \"1-year standard warranty\",\n",
" \"specs\": \"Bluetooth 5.0+, ANC, 20-40hr battery\",\n",
" \"features\": \"Active noise cancellation, transparency mode, multipoint\",\n",
" \"compatibility\": \"Universal Bluetooth, some with proprietary apps\",\n",
" \"support\": \"Replacement program for defective units\",\n",
" },\n",
" \"monitors\": {\n",
" \"warranty\": \"3-year standard, zero dead pixel guarantee\",\n",
" \"specs\": \"4K/1440p resolution, 60-240Hz refresh rate\",\n",
" \"features\": \"HDR support, high refresh rates, adjustable stands\",\n",
" \"compatibility\": \"HDMI, DisplayPort, USB-C inputs\",\n",
" \"support\": \"Color calibration and technical support\",\n",
" },\n",
" }\n",
" product = products.get(product_type.lower())\n",
" if not product:\n",
" return f\"Technical specifications for {product_type} not available. Please contact our technical support team for detailed product information and compatibility requirements.\"\n",
" return (\n",
" f\"Technical Information - {product_type.title()}:\\n\\n\"\n",
" f\"\\u2022 Warranty: {product['warranty']}\\n\"\n",
" f\"\\u2022 Specifications: {product['specs']}\\n\"\n",
" f\"\\u2022 Key Features: {product['features']}\\n\"\n",
" f\"\\u2022 Compatibility: {product['compatibility']}\\n\"\n",
" f\"\\u2022 Support: {product['support']}\"\n",
" )\n",
"\n",
"\n",
"def web_search(keywords: str) -> str:\n",
" \"\"\"Search the web for updated information.\n",
"\n",
" Args:\n",
" keywords: The search query keywords.\n",
" Returns:\n",
" List of dictionaries with search results.\n",
" \"\"\"\n",
" try:\n",
" results = DDGS().text(keywords, region=\"us-en\", max_results=5)\n",
" return str(results) if results else \"No results found.\"\n",
" except RatelimitException:\n",
" return \"Rate limit reached. Please try again later.\"\n",
" except DDGSException as e:\n",
" return f\"Search error: {e}\"\n",
" except Exception as e:\n",
" return f\"Search error: {str(e)}\"\n",
"\n",
"\n",
"def get_technical_support(issue_description: str) -> str:\n",
" \"\"\"Search the technical support knowledge base for troubleshooting help, setup guides, and maintenance tips.\n",
"\n",
" Args:\n",
" issue_description: Description of the technical issue or question the customer needs help with.\n",
" Returns:\n",
" Relevant technical support documentation and troubleshooting steps.\n",
" \"\"\"\n",
" try:\n",
" ssm = boto3.client(\"ssm\")\n",
" account_id = boto3.client(\"sts\").get_caller_identity()[\"Account\"]\n",
" region = boto3.Session().region_name\n",
" kb_id = ssm.get_parameter(Name=f\"/{account_id}-{region}/kb/knowledge-base-id\")[\"Parameter\"][\"Value\"]\n",
" bedrock_agent_runtime = boto3.client(\"bedrock-agent-runtime\", region_name=region)\n",
" response = bedrock_agent_runtime.retrieve(\n",
" knowledgeBaseId=kb_id,\n",
" retrievalQuery={\"text\": issue_description},\n",
" retrievalConfiguration={\"vectorSearchConfiguration\": {\"numberOfResults\": 3}},\n",
" )\n",
" results = response.get(\"retrievalResults\", [])\n",
" if not results:\n",
" return \"No relevant technical support documentation found for this issue.\"\n",
" formatted_results = []\n",
" for i, result in enumerate(results, 1):\n",
" text = result.get(\"content\", {}).get(\"text\", \"\")\n",
" score = result.get(\"score\", 0)\n",
" if score >= 0.4:\n",
" formatted_results.append(f\"--- Result {i} (relevance: {score:.2f}) ---\\n{text}\")\n",
" if not formatted_results:\n",
" return \"No sufficiently relevant technical support documentation found.\"\n",
" return \"\\n\\n\".join(formatted_results)\n",
" except Exception as e:\n",
" return f\"Unable to access technical support documentation. Error: {str(e)}\"\n",
"\n",
"\n",
"print(\"\\u2705 All tools defined\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"APP_NAME = \"customer_support_agent\"\n",
"USER_ID = \"user1234\"\n",
"\n",
"SYSTEM_PROMPT = \"\"\"You are a helpful and professional customer support assistant for an electronics e-commerce company.\n",
"Your role is to:\n",
"- Provide accurate information using the tools available to you\n",
"- Support the customer with technical information and product specifications, and maintenance questions\n",
"- Be friendly, patient, and understanding with customers\n",
"- Always offer additional help after answering questions\n",
"- If you can't help with something, direct customers to the appropriate contact\n",
"\n",
"You have access to the following tools:\n",
"1. get_return_policy() - For warranty and return policy questions\n",
"2. get_product_info() - To get information about a specific product\n",
"3. web_search() - To access current technical documentation, or for updated information.\n",
"4. get_technical_support() - For troubleshooting issues, setup guides, maintenance tips, and detailed technical assistance\n",
"For any technical problems, setup questions, or maintenance concerns, always use the get_technical_support() tool.\n",
"\n",
"Always use the appropriate tool to get accurate, up-to-date information rather than making assumptions.\"\"\"\n",
"\n",
"# Create the Google ADK agent using LiteLLM to invoke Amazon Bedrock models\n",
"# This routes LLM calls through AWS credentials — no Gemini API key needed.\n",
"agent = LlmAgent(\n",
" model=LiteLlm(model=\"bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0\"),\n",
" name=\"customer_support_agent\",\n",
" description=\"A customer support agent for an electronics e-commerce company.\",\n",
" instruction=SYSTEM_PROMPT,\n",
" tools=[\n",
" get_product_info,\n",
" get_return_policy,\n",
" web_search,\n",
" get_technical_support,\n",
" ],\n",
")\n",
"\n",
"print(\"\\u2705 Customer Support Agent created with Amazon Bedrock model via LiteLLM\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Memory-enhanced helper: retrieve context -> run agent -> save interaction\n",
"async def call_agent_with_memory(query: str, session_id: str = None):\n",
" \"\"\"Send a query to the agent with memory context injection.\"\"\"\n",
" if session_id is None:\n",
" session_id = str(uuid.uuid4())\n",
"\n",
" # --- 1. Retrieve customer context from memory ---\n",
" all_context = []\n",
" namespaces = {\n",
" \"preferences\": f\"support/customer/{ACTOR_ID}/preferences/\",\n",
" \"semantic\": f\"support/customer/{ACTOR_ID}/semantic/\",\n",
" }\n",
" for context_type, namespace in namespaces.items():\n",
" try:\n",
" memories = memory_client.retrieve_memories(\n",
" memory_id=memory_id,\n",
" namespace=namespace,\n",
" query=query,\n",
" top_k=3,\n",
" )\n",
" for mem in memories:\n",
" if isinstance(mem, dict):\n",
" text = mem.get(\"content\", {}).get(\"text\", \"\").strip()\n",
" if text:\n",
" all_context.append(f\"[{context_type.upper()}] {text}\")\n",
" except Exception as e:\n",
" print(f\"Warning: Could not retrieve {context_type} memories: {e}\")\n",
"\n",
" # --- 2. Build enriched query with context ---\n",
" if all_context:\n",
" context_text = \"\\n\".join(all_context)\n",
" enriched_query = f\"Customer Context:\\n{context_text}\\n\\n{query}\"\n",
" print(f\"\\U0001f4cb Retrieved {len(all_context)} memory items for context\")\n",
" else:\n",
" enriched_query = query\n",
" print(\"No prior memory context found\")\n",
"\n",
" # --- 3. Run the ADK agent ---\n",
" session_service = InMemorySessionService()\n",
" session = await session_service.create_session(\n",
" app_name=APP_NAME, user_id=USER_ID, session_id=session_id\n",
" )\n",
" runner = Runner(agent=agent, app_name=APP_NAME, session_service=session_service)\n",
" content = types.Content(role=\"user\", parts=[types.Part(text=enriched_query)])\n",
"\n",
" final_response = \"\"\n",
" async for event in runner.run_async(\n",
" user_id=USER_ID, session_id=session_id, new_message=content\n",
" ):\n",
" if event.is_final_response():\n",
" final_response = event.content.parts[0].text\n",
" print(\"\\nAgent Response:\\n\", final_response)\n",
"\n",
" # --- 4. Save interaction to memory ---\n",
" if final_response:\n",
" try:\n",
" memory_client.create_event(\n",
" memory_id=memory_id,\n",
" actor_id=ACTOR_ID,\n",
" session_id=session_id,\n",
" messages=[\n",
" (query, \"USER\"),\n",
" (final_response, \"ASSISTANT\"),\n",
" ],\n",
" )\n",
" print(\"\\U0001f4be Interaction saved to memory\")\n",
" except Exception as e:\n",
" print(f\"Warning: Could not save to memory: {e}\")\n",
"\n",
" return final_response\n",
"\n",
"print(\"\\u2705 Memory-enhanced agent helper ready\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 5: Test Personalized Agent\n",
"\n",
"Let's test our memory-enhanced agent! Watch how it uses the customer's historical preferences to provide personalized recommendations.\n",
"\n",
"The agent will automatically:\n",
"1. Retrieve relevant customer context from memory\n",
"2. Use that context to personalize the response\n",
"3. Save this new interaction for future use"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print(\"\\U0001f3a7 Testing headphone recommendation with customer memory...\\n\\n\")\n",
"response1 = await call_agent_with_memory(\"Which headphones would you recommend?\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print(\"\\n\\U0001f4bb Testing laptop preference recall...\\n\\n\")\n",
"response2 = await call_agent_with_memory(\"What is my preferred laptop brand and requirements?\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Notice how the Agent remembers:\n",
"\n",
"- Your gaming preferences (low latency headphones)\n",
"- Your laptop preferences (ThinkPad, 16GB RAM, Linux compatibility)\n",
"- Your budget constraints ($1200 for laptops)\n",
"- Previous technical issues (MacBook overheating) \n",
"\n",
"This is the power of AgentCore Memory - persistent, personalized customer experiences!\n",
"\n",
"## Congratulations! 🎉\n",
"\n",
"You have successfully completed **Lab 2: Add memory to the Customer Support Agent**!\n",
"\n",
"### What You Accomplished:\n",
"\n",
"- Created a serverless managed memory with Amazon Bedrock AgentCore Memory\n",
"- Implemented long-term memory to store User-Preferences and Semantic (Factual) information.\n",
"- Integrated AgentCore Memory with the customer support Agent using Google ADK with explicit memory retrieval and storage\n",
"- Used LiteLLM to invoke Amazon Bedrock models (Claude) through Google ADK\n",
"\n",
"##### Next Up [Lab 3 - Scaling with Gateway and Identity \\u2192](lab-03-agentcore-gateway.ipynb)\n",
"\n",
"## Resources\n",
"- [Amazon Bedrock Agent Core Memory](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/memory.html)\n",
"- [Amazon Bedrock AgentCore Memory Deep Dive blog](https://aws.amazon.com/blogs/machine-learning/amazon-bedrock-agentcore-memory-building-context-aware-agents/)\n",
"- [Google ADK Documentation](https://google.github.io/adk-docs/)\n",
"- [Google ADK LiteLLM Integration](https://google.github.io/adk-docs/agents/models/litellm/)\n",
"- [AgentCore with Google ADK Integration](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/using-any-agent-framework.html#agent-runtime-frameworks-google-adk)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "ag-gadk-01-9March-0800",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.13"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,972 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "83d8ecde-overview",
"metadata": {},
"source": [
"## Lab 4: Deploy to Production - Use AgentCore Runtime with Observability\n",
"\n",
"### Overview\n",
"\n",
"In Lab 3 we scaled our Customer Support Agent by centralizing tools through AgentCore Gateway with secure authentication. Now we'll complete the production journey by deploying our agent to AgentCore Runtime with comprehensive observability. This will transform our prototype into a production-ready system that can handle real-world traffic with full monitoring and automatic scaling.\n",
"\n",
"[Amazon Bedrock AgentCore Runtime](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agents-tools-runtime.html) is a secure, fully managed runtime that empowers organizations to deploy and scale AI agents in production, regardless of framework, protocol, or model choice. It provides enterprise-grade reliability, automatic scaling, and comprehensive monitoring capabilities.\n",
"\n",
"**Workshop Journey:**\n",
"\n",
"- **Lab 1 (Done):** Create Agent Prototype - Built a functional customer support agent\n",
"- **Lab 2 (Done):** Enhance with Memory - Added conversation context and personalization\n",
"- **Lab 3 (Done):** Scale with Gateway & Identity - Shared tools across agents securely\n",
"- **Lab 4 (Current):** Deploy to Production - Used AgentCore Runtime with observability\n",
"- **Lab 5:** Build User Interface - Create a customer-facing application\n",
"\n",
"### Why AgentCore Runtime & Production Deployment Matter\n",
"\n",
"Current State (Lab 1-3): Agent runs locally with centralized tools but faces production challenges:\n",
"\n",
"- Agent runs locally in a single session\n",
"- No comprehensive monitoring or debugging capabilities\n",
"- Cannot handle multiple concurrent users reliably\n",
"\n",
"After this lab, we will have a production-ready agent infrastructure with:\n",
"\n",
"- Serverless auto-scaling to handle variable demand\n",
"- Comprehensive observability with traces, metrics, and logging\n",
"- Enterprise reliability with automatic error recovery\n",
"- Secure deployment with proper access controls\n",
"- Easy management through AWS console and APIs and support for real-world production workloads.\n",
"\n",
"\n",
"### Adding comprehensive observability with AgentCore Observability\n",
"\n",
"Additionally, AgentCore Runtime integrates seamlessly with [AgentCore Observability](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/observability.html) to provide full visibility into your agent's behavior in production. AgentCore Observability automatically captures traces, metrics, and logs from your agent interactions, tool usage, and memory access patterns. In this lab we will see how AgentCore Runtime integrates with CloudWatch GenAI Observability to provide comprehensive monitoring and debugging capabilities.\n",
"\n",
"For request tracing, AgentCore Observability captures the complete conversation flow including tool invocations, memory retrievals, and model interactions. For performance monitoring, it tracks response times, success rates, and resource utilization to help optimize your agent's performance.\n",
"\n",
"During the observability flow, AgentCore Runtime automatically instruments your agent code and sends telemetry data to CloudWatch. You can then use CloudWatch dashboards and GenAI Observability features to analyze patterns, identify bottlenecks, and troubleshoot issues in real-time.\n",
"\n",
"### Architecture for Lab 4\n",
"<div style=\"text-align:left\"> \n",
" <img src=\"images/architecture_lab4_runtime.png\" width=\"75%\"/> \n",
"</div>\n",
"\n",
"*Agent now runs in AgentCore Runtime with full observability through CloudWatch, serving production traffic with auto-scaling and comprehensive monitoring. Memory and Gateway integrations from previous labs remain fully functional in the production environment.*\n",
"\n",
"### Key Features\n",
"\n",
"- **Serverless Agent Deployment:** Transform your local agent into a scalable production service using AgentCore Runtime with minimal code changes\n",
"- **Comprehensive Observability:** Full request tracing, performance metrics, and debugging capabilities through CloudWatch GenAI Observability\n",
"\n",
"### Prerequisites\n",
"\n",
"- Python 3.12+\n",
"- AWS account with appropriate permissions\n",
"- Docker, Finch or Podman installed and running\n",
"- Amazon Bedrock AgentCore SDK\n",
"- Google ADK framework\n",
"- **Lab 3 Completion:** This lab builds on Lab 3 (AgentCore Gateway). You MUST run [lab-03-agentcore-gateway](lab-03-agentcore-gateway.ipynb) to provision the gateway before running this lab.\n",
"\n",
"**Note: You MUST enable [CloudWatch Transaction Search](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Enable-TransactionSearch.html) to be able to see AgentCore Observability traces in CloudWatch.**"
]
},
{
"cell_type": "markdown",
"id": "step1-md",
"metadata": {},
"source": [
"### Step 1: Import Required Libraries"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "step1-code",
"metadata": {},
"outputs": [],
"source": [
"# Import required libraries\n",
"import boto3\n",
"from lab_helpers.utils import get_ssm_parameter\n",
"from bedrock_agentcore_starter_toolkit.operations.memory.manager import MemoryManager\n",
"from bedrock_agentcore.memory.constants import StrategyType\n",
"from lab_helpers.lab2_memory import ACTOR_ID\n",
"\n",
"boto_session = boto3.Session()\n",
"REGION = boto_session.region_name\n",
"\n",
"\n",
"memory_name = \"CustomerSupportMemory\"\n",
"memory_manager = MemoryManager(region_name=REGION)\n",
"memory = memory_manager.get_or_create_memory( # Just in case the memory lab wasn't executed\n",
" name=memory_name,\n",
" strategies=[\n",
" {\n",
" StrategyType.USER_PREFERENCE.value: {\n",
" \"name\": \"CustomerPreferences\",\n",
" \"description\": \"Captures customer preferences and behavior\",\n",
" \"namespaces\": [\"support/customer/{actorId}/preferences/\"],\n",
" }\n",
" },\n",
" {\n",
" StrategyType.SEMANTIC.value: {\n",
" \"name\": \"CustomerSupportSemantic\",\n",
" \"description\": \"Stores facts from conversations\",\n",
" \"namespaces\": [\"support/customer/{actorId}/semantic/\"],\n",
" }\n",
" },\n",
" ]\n",
")\n",
"memory_id = memory[\"id\"]"
]
},
{
"cell_type": "markdown",
"id": "step2-md",
"metadata": {},
"source": [
"### Step 2: Preparing Your Agent for AgentCore Runtime\n",
"\n",
"#### Creating the Runtime-Ready Agent\n",
"\n",
"Let's first define the necessary AgentCore Runtime components via Python SDK within our previous local agent implementation.\n",
"\n",
"Observe the #### AGENTCORE RUNTIME - LINE 1 #### comments below to see where is the relevant deployment code added. You'll find 4 such lines that prepare the runtime-ready agent:\n",
"\n",
"1. Import the Runtime App with `from bedrock_agentcore.runtime import BedrockAgentCoreApp`\n",
"2. Initialize the App with `app = BedrockAgentCoreApp()`\n",
"3. Decorate our invocation function with `@app.entrypoint`\n",
"4. Let AgentCore Runtime control the execution with `app.run()`\n",
"\n",
"##### Key Implementation Details:\n",
"\n",
"The runtime-ready agent uses an entrypoint function that extracts user prompts from the payload and JWT tokens from request headers via \n",
"context.request_headers.get('Authorization', ''). The authorization token is then propagated directly to the AgentCore Gateway by passing it in the \n",
"MCP client headers: headers={\"Authorization\": auth_header}. The implementation includes error handling for missing authentication and returns plain \n",
"text responses from the Google ADK agent invocation while preserving all memory and tool functionality from previous labs."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "step2-writefile",
"metadata": {},
"outputs": [],
"source": [
"%%writefile ./lab_helpers/lab4_runtime.py\n",
"\n",
"import os\n",
"import uuid\n",
"import asyncio\n",
"import concurrent.futures\n",
"import concurrent.futures\n",
"from bedrock_agentcore.runtime import (\n",
" BedrockAgentCoreApp,\n",
") #### AGENTCORE RUNTIME - LINE 1 ####\n",
"\n",
"from mcp import ClientSession\n",
"from mcp.client.streamable_http import streamablehttp_client\n",
"\n",
"from google.adk.agents import LlmAgent\n",
"from google.adk.models.lite_llm import LiteLlm\n",
"from google.adk.runners import Runner\n",
"from google.adk.sessions import InMemorySessionService\n",
"from google.genai import types\n",
"\n",
"import boto3\n",
"from bedrock_agentcore.memory import MemoryClient\n",
"from lab_helpers.utils import get_ssm_parameter\n",
"\n",
"# Initialize boto3 client\n",
"sts_client = boto3.client('sts')\n",
"\n",
"# Get AWS account details\n",
"REGION = boto3.session.Session().region_name\n",
"\n",
"ACTOR_ID = \"customer_001\"\n",
"\n",
"# Lab2 import: Memory\n",
"memory_id = os.environ.get(\"MEMORY_ID\")\n",
"if not memory_id:\n",
" raise Exception(\"Environment variable MEMORY_ID is required\")\n",
"\n",
"memory_client = MemoryClient(region_name=REGION)\n",
"\n",
"# ============================================================\n",
"# System prompt\n",
"# ============================================================\n",
"SYSTEM_PROMPT = \"\"\"You are a helpful and professional customer support assistant for an electronics e-commerce company.\n",
"Your role is to:\n",
"- Provide accurate information using the tools available to you\n",
"- Support the customer with technical information and product specifications.\n",
"- Be friendly, patient, and understanding with customers\n",
"- Always offer additional help after answering questions\n",
"- If you can't help with something, direct customers to the appropriate contact\n",
"\n",
"You have access to the following tools:\n",
"1. get_return_policy() - For warranty and return policy questions\n",
"2. get_product_info() - To get information about a specific product\n",
"3. get_technical_support() - To search the technical support knowledge base\n",
"4. check_warranty_status() - To check warranty status via serial number (via AgentCore Gateway)\n",
"5. web_search() - To access current technical documentation, or for updated information (via AgentCore Gateway)\n",
"Always use the appropriate tool to get accurate, up-to-date information rather than making assumptions about electronic products or specifications.\"\"\"\n",
"\n",
"# ============================================================\n",
"# Local tools (same as Lab 3)\n",
"# ============================================================\n",
"\n",
"def get_return_policy(product_category: str) -> str:\n",
" \"\"\"Get return policy information for a specific product category.\n",
"\n",
" Args:\n",
" product_category: Electronics category (e.g., 'smartphones', 'laptops', 'accessories')\n",
"\n",
" Returns:\n",
" Formatted return policy details including timeframes and conditions\n",
" \"\"\"\n",
" return_policies = {\n",
" \"smartphones\": {\n",
" \"window\": \"30 days\",\n",
" \"condition\": \"Original packaging, no physical damage, factory reset required\",\n",
" \"process\": \"Online RMA portal or technical support\",\n",
" \"refund_time\": \"5-7 business days after inspection\",\n",
" \"shipping\": \"Free return shipping, prepaid label provided\",\n",
" \"warranty\": \"1-year manufacturer warranty included\",\n",
" },\n",
" \"laptops\": {\n",
" \"window\": \"30 days\",\n",
" \"condition\": \"Original packaging, all accessories, no software modifications\",\n",
" \"process\": \"Technical support verification required before return\",\n",
" \"refund_time\": \"7-10 business days after inspection\",\n",
" \"shipping\": \"Free return shipping with original packaging\",\n",
" \"warranty\": \"1-year manufacturer warranty, extended options available\",\n",
" },\n",
" \"accessories\": {\n",
" \"window\": \"30 days\",\n",
" \"condition\": \"Unopened packaging preferred, all components included\",\n",
" \"process\": \"Online return portal\",\n",
" \"refund_time\": \"3-5 business days after receipt\",\n",
" \"shipping\": \"Customer pays return shipping under $50\",\n",
" \"warranty\": \"90-day manufacturer warranty\",\n",
" },\n",
" }\n",
" default_policy = {\n",
" \"window\": \"30 days\",\n",
" \"condition\": \"Original condition with all included components\",\n",
" \"process\": \"Contact technical support\",\n",
" \"refund_time\": \"5-7 business days after inspection\",\n",
" \"shipping\": \"Return shipping policies vary\",\n",
" \"warranty\": \"Standard manufacturer warranty applies\",\n",
" }\n",
" policy = return_policies.get(product_category.lower(), default_policy)\n",
" return (\n",
" f\"Return Policy - {product_category.title()}:\\n\\n\"\n",
" f\"\\u2022 Return window: {policy['window']} from delivery\\n\"\n",
" f\"\\u2022 Condition: {policy['condition']}\\n\"\n",
" f\"\\u2022 Process: {policy['process']}\\n\"\n",
" f\"\\u2022 Refund timeline: {policy['refund_time']}\\n\"\n",
" f\"\\u2022 Shipping: {policy['shipping']}\\n\"\n",
" f\"\\u2022 Warranty: {policy['warranty']}\"\n",
" )\n",
"\n",
"\n",
"def get_product_info(product_type: str) -> str:\n",
" \"\"\"Get detailed technical specifications and information for electronics products.\n",
"\n",
" Args:\n",
" product_type: Electronics product type (e.g., 'laptops', 'smartphones', 'headphones', 'monitors')\n",
"\n",
" Returns:\n",
" Formatted product information including warranty, features, and policies\n",
" \"\"\"\n",
" products = {\n",
" \"laptops\": {\n",
" \"warranty\": \"1-year standard, 3-year extended available\",\n",
" \"specs\": \"Intel/AMD processors, 8-64GB RAM, SSD storage\",\n",
" \"features\": \"Backlit keyboards, fingerprint readers, Thunderbolt ports\",\n",
" \"compatibility\": \"Windows, Linux, macOS (Apple only)\",\n",
" \"support\": \"24/7 technical support, on-site repair options\",\n",
" },\n",
" \"smartphones\": {\n",
" \"warranty\": \"1-year manufacturer, 2-year extended\",\n",
" \"specs\": \"Latest processors, 6-12GB RAM, 128GB-1TB storage\",\n",
" \"features\": \"5G capable, water resistant, wireless charging\",\n",
" \"compatibility\": \"iOS or Android ecosystem\",\n",
" \"support\": \"In-store and mail-in repair services\",\n",
" },\n",
" \"headphones\": {\n",
" \"warranty\": \"1-year standard warranty\",\n",
" \"specs\": \"Bluetooth 5.0+, ANC, 20-40hr battery\",\n",
" \"features\": \"Active noise cancellation, transparency mode, multipoint\",\n",
" \"compatibility\": \"Universal Bluetooth, some with proprietary apps\",\n",
" \"support\": \"Replacement program for defective units\",\n",
" },\n",
" \"monitors\": {\n",
" \"warranty\": \"3-year standard, zero dead pixel guarantee\",\n",
" \"specs\": \"4K/1440p resolution, 60-240Hz refresh rate\",\n",
" \"features\": \"HDR support, high refresh rates, adjustable stands\",\n",
" \"compatibility\": \"HDMI, DisplayPort, USB-C inputs\",\n",
" \"support\": \"Color calibration and technical support\",\n",
" },\n",
" }\n",
" product = products.get(product_type.lower())\n",
" if not product:\n",
" return f\"Technical specifications for {product_type} not available. Please contact our technical support team.\"\n",
" return (\n",
" f\"Technical Information - {product_type.title()}:\\n\\n\"\n",
" f\"\\u2022 Warranty: {product['warranty']}\\n\"\n",
" f\"\\u2022 Specifications: {product['specs']}\\n\"\n",
" f\"\\u2022 Key Features: {product['features']}\\n\"\n",
" f\"\\u2022 Compatibility: {product['compatibility']}\\n\"\n",
" f\"\\u2022 Support: {product['support']}\"\n",
" )\n",
"\n",
"\n",
"def get_technical_support(issue_description: str) -> str:\n",
" \"\"\"Search the technical support knowledge base for troubleshooting help.\n",
"\n",
" Args:\n",
" issue_description: Description of the technical issue or question.\n",
"\n",
" Returns:\n",
" Relevant technical support documentation and troubleshooting steps.\n",
" \"\"\"\n",
" try:\n",
" ssm = boto3.client(\"ssm\")\n",
" acct = boto3.client(\"sts\").get_caller_identity()[\"Account\"]\n",
" region = boto3.Session().region_name\n",
" kb_id = ssm.get_parameter(Name=f\"/{acct}-{region}/kb/knowledge-base-id\")[\"Parameter\"][\"Value\"]\n",
" bedrock_agent_runtime = boto3.client(\"bedrock-agent-runtime\", region_name=region)\n",
" response = bedrock_agent_runtime.retrieve(\n",
" knowledgeBaseId=kb_id,\n",
" retrievalQuery={\"text\": issue_description},\n",
" retrievalConfiguration={\"vectorSearchConfiguration\": {\"numberOfResults\": 3}},\n",
" )\n",
" results = response.get(\"retrievalResults\", [])\n",
" if not results:\n",
" return \"No relevant technical support documentation found for this issue.\"\n",
" formatted_results = []\n",
" for i, result in enumerate(results, 1):\n",
" text = result.get(\"content\", {}).get(\"text\", \"\")\n",
" score = result.get(\"score\", 0)\n",
" if score >= 0.4:\n",
" formatted_results.append(f\"--- Result {i} (relevance: {score:.2f}) ---\\n{text}\")\n",
" if not formatted_results:\n",
" return \"No sufficiently relevant technical support documentation found.\"\n",
" return \"\\n\\n\".join(formatted_results)\n",
" except Exception as e:\n",
" return f\"Unable to access technical support documentation. Error: {str(e)}\"\n",
"\n",
"\n",
"# ============================================================\n",
"# MCP Gateway tool wrappers (same pattern as Lab 3)\n",
"# ============================================================\n",
"\n",
"async def _call_mcp_tool(tool_name: str, arguments: dict, gateway_url: str, auth_header: str) -> str:\n",
" \"\"\"Helper to call an MCP tool on the AgentCore Gateway.\"\"\"\n",
" async with streamablehttp_client(\n",
" gateway_url,\n",
" headers={\"Authorization\": auth_header},\n",
" ) as (read_stream, write_stream, _):\n",
" async with ClientSession(read_stream, write_stream) as session:\n",
" await session.initialize()\n",
" result = await session.call_tool(tool_name, arguments)\n",
" if result.content:\n",
" return \"\\n\".join(\n",
" part.text for part in result.content if hasattr(part, \"text\")\n",
" )\n",
" return \"No result returned.\"\n",
"\n",
"\n",
"def _run_async_in_thread(coro):\n",
" \"\"\"Run an async coroutine in a separate thread to avoid 'event loop already running' errors.\"\"\"\n",
" with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:\n",
" future = executor.submit(asyncio.run, coro)\n",
" return future.result()\n",
"\n",
"\n",
"# These will be set per-request in the entrypoint\n",
"_gateway_url = None\n",
"_auth_header = None\n",
"\n",
"\n",
"def check_warranty_status(serial_number: str, customer_email: str) -> str:\n",
" \"\"\"Check the warranty status of a product using its serial number.\n",
"\n",
" Args:\n",
" serial_number: The product serial number to look up.\n",
" customer_email: Customer email for verification. Pass empty string if not available.\n",
"\n",
" Returns:\n",
" Warranty status information for the product.\n",
" \"\"\"\n",
" args = {\"serial_number\": serial_number}\n",
" if customer_email:\n",
" args[\"customer_email\"] = customer_email\n",
" return _run_async_in_thread(\n",
" _call_mcp_tool(\"LambdaUsingSDK___check_warranty_status\", args, _gateway_url, _auth_header)\n",
" )\n",
"\n",
"\n",
"def web_search(keywords: str, region: str, max_results: int) -> str:\n",
" \"\"\"Search the web for updated information using DuckDuckGo.\n",
"\n",
" Args:\n",
" keywords: The search query keywords.\n",
" region: The search region (e.g., us-en, uk-en, ru-ru).\n",
" max_results: The maximum number of results to return.\n",
"\n",
" Returns:\n",
" Search results from the web.\n",
" \"\"\"\n",
" args = {\"keywords\": keywords, \"region\": region, \"max_results\": max_results}\n",
" return _run_async_in_thread(\n",
" _call_mcp_tool(\"LambdaUsingSDK___web_search\", args, _gateway_url, _auth_header)\n",
" )\n",
"\n",
"\n",
"# Initialize the AgentCore Runtime App\n",
"app = BedrockAgentCoreApp() #### AGENTCORE RUNTIME - LINE 2 ####\n",
"\n",
"@app.entrypoint #### AGENTCORE RUNTIME - LINE 3 ####\n",
"async def invoke(payload, context=None):\n",
" \"\"\"AgentCore Runtime entrypoint function\"\"\"\n",
" global _gateway_url, _auth_header\n",
"\n",
" user_input = payload.get(\"prompt\", \"\")\n",
" session_id = context.session_id # Get session_id from context\n",
" actor_id = payload.get(\"actor_id\", ACTOR_ID)\n",
" # Access request headers - handle None case\n",
" request_headers = context.request_headers or {}\n",
"\n",
" # Get Client JWT token\n",
" auth_header = request_headers.get('Authorization', '')\n",
" print(f\"Authorization header: {auth_header}\")\n",
"\n",
" # Get Gateway ID\n",
" existing_gateway_id = get_ssm_parameter(\"/app/customersupport/agentcore/gateway_id\")\n",
"\n",
" # Initialize Bedrock AgentCore Control client\n",
" gateway_client = boto3.client(\n",
" \"bedrock-agentcore-control\",\n",
" region_name=REGION,\n",
" )\n",
" # Get existing gateway details\n",
" gateway_response = gateway_client.get_gateway(gatewayIdentifier=existing_gateway_id)\n",
" gateway_url = gateway_response['gatewayUrl']\n",
"\n",
" if gateway_url and auth_header:\n",
" try:\n",
" # Set module-level vars for MCP tool wrappers\n",
" _gateway_url = gateway_url\n",
" _auth_header = auth_header\n",
"\n",
" # All tools: local + MCP gateway wrappers\n",
" all_tools = [\n",
" get_product_info,\n",
" get_return_policy,\n",
" get_technical_support,\n",
" check_warranty_status,\n",
" web_search,\n",
" ]\n",
"\n",
" # --- 1. Retrieve customer context from memory ---\n",
" all_context = []\n",
" namespaces = {\n",
" \"preferences\": f\"support/customer/{actor_id}/preferences/\",\n",
" \"semantic\": f\"support/customer/{actor_id}/semantic/\",\n",
" }\n",
" for context_type, namespace in namespaces.items():\n",
" try:\n",
" memories = memory_client.retrieve_memories(\n",
" memory_id=memory_id,\n",
" namespace=namespace,\n",
" query=user_input,\n",
" top_k=3,\n",
" )\n",
" for mem in memories:\n",
" if isinstance(mem, dict):\n",
" text = mem.get(\"content\", {}).get(\"text\", \"\").strip()\n",
" if text:\n",
" all_context.append(f\"[{context_type.upper()}] {text}\")\n",
" except Exception as e:\n",
" print(f\"Warning: Could not retrieve {context_type} memories: {e}\")\n",
"\n",
" # --- 2. Build enriched query with context ---\n",
" if all_context:\n",
" context_text = \"\\n\".join(all_context)\n",
" enriched_query = f\"Customer Context:\\n{context_text}\\n\\n{user_input}\"\n",
" else:\n",
" enriched_query = user_input\n",
"\n",
" # --- 3. Create and run the ADK agent ---\n",
" agent = LlmAgent(\n",
" name=\"customer_support_agent\",\n",
" model=LiteLlm(model=\"bedrock/global.anthropic.claude-haiku-4-5-20251001-v1:0\"),\n",
" instruction=SYSTEM_PROMPT,\n",
" tools=all_tools,\n",
" )\n",
"\n",
" adk_session_id = str(uuid.uuid4())\n",
" session_service = InMemorySessionService()\n",
" adk_session = await session_service.create_session(\n",
" app_name=\"customer_support_app\", user_id=\"user_001\", session_id=adk_session_id\n",
" )\n",
" runner = Runner(agent=agent, app_name=\"customer_support_app\", session_service=session_service)\n",
" content = types.Content(role=\"user\", parts=[types.Part(text=enriched_query)])\n",
"\n",
" final_response = \"\"\n",
" async for event in runner.run_async(\n",
" user_id=\"user_001\", session_id=adk_session_id, new_message=content\n",
" ):\n",
" if event.is_final_response():\n",
" final_response = event.content.parts[0].text\n",
"\n",
" # --- 4. Save interaction to memory ---\n",
" if final_response:\n",
" try:\n",
" memory_client.create_event(\n",
" memory_id=memory_id,\n",
" actor_id=actor_id,\n",
" session_id=str(session_id),\n",
" messages=[\n",
" (user_input, \"USER\"),\n",
" (final_response, \"ASSISTANT\"),\n",
" ],\n",
" )\n",
" except Exception as e:\n",
" print(f\"Warning: Could not save to memory: {e}\")\n",
"\n",
" return final_response\n",
" except Exception as e:\n",
" print(f\"Agent error: {str(e)}\")\n",
" return f\"Error: {str(e)}\"\n",
" else:\n",
" return \"Error: Missing gateway URL or authorization header\"\n",
"\n",
"if __name__ == \"__main__\":\n",
" app.run() #### AGENTCORE RUNTIME - LINE 4 ####\n"
]
},
{
"cell_type": "markdown",
"id": "8855aceb-b79f-4aaa-b16f-8577c059816a",
"metadata": {},
"source": [
"#### What happens behind the scenes?\n",
"\n",
"When you use `BedrockAgentCoreApp`, it automatically:\n",
"\n",
"- Creates an HTTP server that listens on port 8080\n",
"- Implements the required `/invocations` endpoint for processing requests\n",
"- Implements the `/ping` endpoint for health checks\n",
"- Handles proper content types and response formats\n",
"- Manages error handling according to AWS standards\n"
]
},
{
"cell_type": "markdown",
"id": "8e8aa1fb-4e80-4dbd-864a-8e7bf9eab714",
"metadata": {},
"source": [
"### Step 3: Deploying to AgentCore Runtime\n",
"\n",
"Now let's deploy our agent to AgentCore Runtime using the [AgentCore Starter Toolkit](https://github.com/aws/bedrock-agentcore-starter-toolkit).\n",
"\n",
"#### Configure the Secure Runtime Deployment (AgentCore Runtime + AgentCore Identity)\n",
"\n",
"First we will use our starter toolkit to configure the AgentCore Runtime deployment with an entrypoint, the execution role we will create and a requirements file. We will also configure the identity authorization using an Amazon Cognito user pool and we will configure the starter kit to auto create the Amazon ECR repository on launch.\n",
"\n",
"During the configure step, your docker file will be generated based on your application code\n",
"\n",
"<div style=\"text-align:left\"> \n",
" <img src=\"images/configure.png\" width=\"75%\"/> \n",
"</div>\n",
"\n",
"**Note**: The Cognito access_token is valid for 2 hours only. If the access_token expires you can vend another access_token by using the `reauthenticate_user` method.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c68f95c9-e97c-4ebd-8009-b2eab09ba614",
"metadata": {},
"outputs": [],
"source": [
"from lab_helpers.utils import get_or_create_cognito_pool\n",
"\n",
"access_token = get_or_create_cognito_pool(refresh_token=True)\n",
"print(f\"Access token: {access_token['bearer_token']}\")"
]
},
{
"cell_type": "markdown",
"id": "d1d09fb7",
"metadata": {},
"source": [
"#### AgentCore Runtime Configuration Summary:\n",
"\n",
"Below code configures the AgentCore Runtime deployment using the starter toolkit. It creates an execution role for the runtime, then configures the \n",
"deployment with the agent entrypoint file (lab_helpers/lab4_runtime.py), enables automatic ECR repository creation, and sets up JWT-based authentication using \n",
"Cognito. The configuration specifies allowed client IDs and discovery URLs retrieved from SSM parameters, establishing secure access control for the \n",
"production agent deployment. This step automatically generates the Dockerfile and .bedrock_agentcore.yaml configuration files needed for \n",
"containerized deployment.\n",
"\n",
"**Runtime Header Configuration** : Below code configures custom header allowlists for the deployed AgentCore Runtime. It extracts the runtime ID from the agent ARN, retrieves the \n",
"current runtime configuration to preserve existing settings, then updates the runtime with a request header allowlist that includes the Authorization\n",
"header (required for OAuth token propagation) and custom headers. This ensures JWT tokens and other necessary headers are properly forwarded from \n",
"client requests to the agent runtime code.\n",
"\n",
"**Note: For the scope of the workshop, you can safely ignore the Platform Mismatch warning.**"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "17a32ab8-7701-4900-8055-e24364bdf35c",
"metadata": {},
"outputs": [],
"source": [
"from bedrock_agentcore_starter_toolkit import Runtime\n",
"from lab_helpers.utils import create_agentcore_runtime_execution_role\n",
"\n",
"# Initialize the runtime toolkit\n",
"boto_session = boto3.session.Session()\n",
"region = boto_session.region_name\n",
"\n",
"execution_role_arn = create_agentcore_runtime_execution_role()\n",
"\n",
"agentcore_runtime = Runtime()\n",
"\n",
"# Configure the deployment\n",
"response = agentcore_runtime.configure(\n",
" entrypoint=\"lab_helpers/lab4_runtime.py\",\n",
" execution_role=execution_role_arn,\n",
" auto_create_ecr=True,\n",
" requirements_file=\"requirements.txt\",\n",
" region=region,\n",
" agent_name=\"customer_support_agent\",\n",
" authorizer_configuration={\n",
" \"customJWTAuthorizer\": {\n",
" \"allowedClients\": [\n",
" get_ssm_parameter(\"/app/customersupport/agentcore/client_id\")\n",
" ],\n",
" \"discoveryUrl\": get_ssm_parameter(\n",
" \"/app/customersupport/agentcore/cognito_discovery_url\"\n",
" ),\n",
" }\n",
" },\n",
" # Add custom header allowlist for Authorization and custom headers\n",
" request_header_configuration={\n",
" \"requestHeaderAllowlist\": [\n",
" \"Authorization\", # Required for OAuth propogation\n",
" \"X-Amzn-Bedrock-AgentCore-Runtime-Custom-H1\", # Custom header\n",
" ]\n",
" },\n",
")\n",
"\n",
"print(\"Configuration completed:\", response)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "48f23dc7",
"metadata": {},
"outputs": [],
"source": [
"%store execution_role_arn\n",
"\n",
"print(f\"Agent Role ARN: {execution_role_arn}\")"
]
},
{
"cell_type": "markdown",
"id": "9e1b84cc-798e-472c-ac0b-2c315f4b704d",
"metadata": {},
"source": [
"#### Launch the Agent\n",
"\n",
"Now let's launch our agent to AgentCore Runtime. This will create an AWS CodeBuild pipeline, the Amazon ECR repository and the AgentCore Runtime components.\n",
"\n",
"<div style=\"text-align:left\"> \n",
" <img src=\"images/launch.png\" width=\"100%\"/> \n",
"</div>"
]
},
{
"cell_type": "markdown",
"id": "463d195d",
"metadata": {},
"source": [
"*Note: This step might fail if the agent with the same name already exists. If you want to overwrite the existing Runtime, use this instead:*\n",
"\n",
"``` launch_result = agentcore_runtime.launch(auto_update_on_conflict=True)```\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "afa6ac09-9adb-4846-9fc1-4d12aeb74853",
"metadata": {},
"outputs": [],
"source": [
"# Launch the agent (this will build and deploy the container)\n",
"from lab_helpers.utils import put_ssm_parameter\n",
"\n",
"launch_result = agentcore_runtime.launch(env_vars={\"MEMORY_ID\": memory_id})\n",
"print(\"Launch completed:\", launch_result.agent_arn)\n",
"\n",
"agent_arn = put_ssm_parameter(\n",
" \"/app/customersupport/agentcore/runtime_arn\", launch_result.agent_arn\n",
")"
]
},
{
"cell_type": "markdown",
"id": "a0ae9c09-09db-4a76-871a-92eacd96b9c3",
"metadata": {},
"source": [
"#### Check Deployment Status\n",
"\n",
"Let's wait for the deployment to complete:\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3d909e42-e1a0-407f-84c2-3d16cc889cd3",
"metadata": {},
"outputs": [],
"source": [
"import time\n",
"\n",
"# Wait for the agent to be ready\n",
"status_response = agentcore_runtime.status()\n",
"status = status_response.endpoint[\"status\"]\n",
"\n",
"end_status = [\"READY\", \"CREATE_FAILED\", \"DELETE_FAILED\", \"UPDATE_FAILED\"]\n",
"while status not in end_status:\n",
" print(f\"Waiting for deployment... Current status: {status}\")\n",
" time.sleep(10)\n",
" status_response = agentcore_runtime.status()\n",
" status = status_response.endpoint[\"status\"]\n",
"\n",
"print(f\"Final status: {status}\")"
]
},
{
"cell_type": "markdown",
"id": "b7f89c56-918a-4cab-beaa-c7ac43a2ba29",
"metadata": {},
"source": [
"### Step 4: Invoking Your Deployed Agent\n",
"\n",
"Now that our agent is deployed and ready, let's test it with some queries. We invoke the agent with the right authorization token type. In out case it'll be Cognito access token. Copy the access token from the cell above\n",
"\n",
"<div style=\"text-align:left\"> \n",
" <img src=\"images/invoke.png\" width=\"100%\"/> \n",
"</div>\n",
"\n",
"#### Using the AgentCore Starter Toolkit\n",
"\n",
"We can validate that the agent works using the AgentCore Starter Toolkit for invocation. The starter toolkit can automatically create a session id for us to query our agent. Alternatively, you can also pass the session id as a parameter during invocation. For demonstration purpose, we will create our own session id.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cc1fa718",
"metadata": {},
"outputs": [],
"source": [
"# Initialize the AgentCore Control client\n",
"client = boto3.client(\"bedrock-agentcore-control\")\n",
"\n",
"# Extract runtime ID from the ARN (format: arn:aws:bedrock-agentcore:region:account:runtime/runtime-id)\n",
"runtime_id = launch_result.agent_arn.split(\":\")[-1].split(\"/\")[-1]\n",
"\n",
"print(f\"Runtime ID: {runtime_id}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "be15c4b6",
"metadata": {},
"outputs": [],
"source": [
"import uuid\n",
"from IPython.display import display, Markdown\n",
"\n",
"# Create a session ID for demonstrating session continuity\n",
"session_id = uuid.uuid4()\n",
"\n",
"# Test different customer support scenarios\n",
"user_query = \"List all of your tools\"\n",
"\n",
"response = agentcore_runtime.invoke(\n",
" {\"prompt\": user_query, \"actor_id\": ACTOR_ID},\n",
" bearer_token=access_token[\"bearer_token\"],\n",
" session_id=str(session_id),\n",
")\n",
"\n",
"display(Markdown(response[\"response\"].replace('\\\\n', '\\n')))"
]
},
{
"cell_type": "markdown",
"id": "79840836-5cd5-4139-a188-09d181438840",
"metadata": {},
"source": [
"#### Invoking the agent with session continuity\n",
"\n",
"Since we are using AgentCore Runtime, we can easily continue our conversation with the same `session id` and the same `Actor_id`"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2e378d3f-1fcf-4d8e-b8eb-d34578a788ff",
"metadata": {},
"outputs": [],
"source": [
"user_query = \"Tell me detailed information about the technical documentation on installing a new CPU\"\n",
"response = agentcore_runtime.invoke(\n",
" {\"prompt\": user_query, \"actor_id\": ACTOR_ID},\n",
" bearer_token=access_token[\"bearer_token\"],\n",
" session_id=str(session_id),\n",
")\n",
"display(Markdown(response[\"response\"].replace('\\\\n', '\\n')))"
]
},
{
"cell_type": "markdown",
"id": "687d63ab-c72d-46a7-a9da-d8b008c75136",
"metadata": {},
"source": [
"#### Invoking the agent with a new user\n",
"In the example below our agent still has the context of the Gaming Console Pro that the user bought. This is due to the AgentCore Memory persistence. The agent won't know the context for a new user."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "92a84f17-9a71-4025-ba9e-c10682e7cd8e",
"metadata": {},
"outputs": [],
"source": [
"# Creating a new session ID for demonstrating new customer\n",
"session_id2 = uuid.uuid4()\n",
"\n",
"user_query = \"I have a Gaming Console Pro device , I want to check my warranty status, warranty serial number is MNO33333333.\"\n",
"response = agentcore_runtime.invoke(\n",
" {\"prompt\": user_query, \"actor_id\": ACTOR_ID}, \n",
" bearer_token=access_token[\"bearer_token\"],\n",
" session_id=str(session_id2),\n",
")\n",
"display(Markdown(response[\"response\"].replace('\\\\n', '\\n')))"
]
},
{
"cell_type": "markdown",
"id": "087ad1d9",
"metadata": {},
"source": [
"In this case our agent does not have the context anymore and needs more information. \n",
"\n",
"And it is all it takes to have a secure and scalable endpoint for our Agent with no need to manage all the underlying infrastructure!"
]
},
{
"cell_type": "markdown",
"id": "ca82b1ee",
"metadata": {},
"source": [
"### Step 5: AgentCore Observability\n",
"\n",
"[AgentCore Observability](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/observability.html) provides monitoring and tracing capabilities for AI agents using Amazon OpenTelemetry Python Instrumentation and Amazon CloudWatch GenAI Observability.\n",
"\n",
"#### Agents\n",
"\n",
"Default AgentCore Runtime configuration allows for logging our agent's traces on CloudWatch by means of **AgentCore Observability**. These traces can be seen on the AWS CloudWatch GenAI Observability dashboard. Navigate to Cloudwatch &rarr; GenAI Observability &rarr; Bedrock AgentCore.\n",
"\n",
"![Agents Overview on CloudWatch](images/observability_agents.png)\n",
"\n",
"#### Sessions\n",
"\n",
"The Sessions view shows the list of all the sessions associated with all agents in your account.\n",
"\n",
"![sessions](images/sessions_lab5_observability.png)\n",
"\n",
"#### Traces\n",
"\n",
"Trace view lists all traces from your agents in this account. To work with traces:\n",
"\n",
"- Choose Filter traces to search for specific traces.\n",
"- Sort by column name to organize results.\n",
"- Under Actions, select Logs Insights to refine your search by querying across your log and span data or select Export selected traces to export.\n",
"\n",
"![traces](images/traces_lab4_observability.png)\n"
]
},
{
"cell_type": "markdown",
"id": "7c243e86-a214-483c-aef1-d5243f28ca9e",
"metadata": {},
"source": [
"### Congratulations! 🎉\n",
"\n",
"You have successfully completed **Lab 4: Deploy to Production - Use AgentCore Runtime with Observability!**\n",
"\n",
"Here is what you accomplished:\n",
"\n",
"##### Production-Ready Deployment:\n",
"\n",
"- Prepared your agent for production with minimal code changes (only 4 lines added)\n",
"- Validated proper session isolation between different customers\n",
"- Confirmed session continuity + memory persistence and context awareness per session\n",
"\n",
"##### Enterprise-Grade Security & Identity:\n",
"\n",
"- Implemented secure authentication using Cognito integration with JWT tokens\n",
"- Configured proper IAM roles and execution permissions for production workloads\n",
"- Established identity-based access control for secure agent invocation\n",
"\n",
"##### Comprehensive Observability:\n",
"\n",
"- Enabled AgentCore Observability for full request tracing across all customer sessions\n",
"- Configured CloudWatch GenAI Observability dashboard monitoring\n",
"\n",
"##### Current Limitations (We'll fix these next!):\n",
"\n",
"- **Developer Focused Interaction** - Agent accessible via SDK/API calls but no user-friendly web interface\n",
"- **Manual Session Management** - Requires programmatic session creation rather than intuitive user experience\n",
"\n",
"##### Next Up [Lab 5: Build the Frontend Application →](lab-05-frontend.ipynb)\n",
"In Lab 5, you'll set up continuous quality monitoring with AgentCore Evaluations to ensure your production agent maintains high performance standards!\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "ag-gadk-01-9March-0800",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.13"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -0,0 +1,278 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "354ce590-0831-400f-93eb-2240cfd5a22f",
"metadata": {},
"source": [
"# Lab 5: Building a Customer-Facing Frontend Application\n",
"\n",
"## Overview\n",
"\n",
"In the previous labs, we've built a comprehensive Customer Support Agent with memory, shared tools, and production-grade deployment. With them we show cased the capabilities of AgentCore services to move an agentic use case from prototype to production. You can now invoke your agent runtime from any application. On real world applications, customers expect an user interface to be available. Now it's time to create a user-friendly frontend that customers can actually use to interact with our agent.\n",
"\n",
"**Workshop Journey:**\n",
"- **Lab 1 (Done)**: Create Agent Prototype - Built a functional customer support agent\n",
"- **Lab 2 (Done)**: Enhance with Memory - Added conversation context and personalization\n",
"- **Lab 3 (Done)**: Scale with Gateway & Identity - Shared tools across agents securely\n",
"- **Lab 4 (Done)**: Deploy to Production - Used AgentCore Runtime with observability\n",
"- **Lab 5 (Current)**: Build User Interface - Create a customer-facing application\n",
"\n",
"In this lab, we'll create a **Streamlit-based web application** that provides customers with an intuitive chat interface to interact with our deployed Customer Support Agent. The frontend will include:\n",
"\n",
"- **Secure Authentication** - User login via Amazon Cognito\n",
"- **Real-time Chat Interface** - Streamlit-powered conversational UI\n",
"- **Streaming Responses** - Live response streaming for better user experience\n",
"- **Session Management** - Persistent conversations with memory\n",
"- **Response Timing** - Performance metrics for transparency\n",
"\n",
"### Architecture for Lab 5\n",
"\n",
"Our frontend application connects to the AgentCore Runtime endpoint we deployed in Lab 4, providing a complete end-to-end customer support solution:\n",
"\n",
"<div style=\"text-align:left\">\n",
" <img src=\"images/architecture_lab6_streamlit.png\" width=\"100%\"/>\n",
"</div>\n",
"\n",
"\n",
"### What You'll learn\n",
"\n",
"- How to integrate Secure Authentication with a frontend.\n",
"- How to implement real-time streaming responses\n",
"- How to manage user sessions and conversation context\n",
"- How to create an intuitive chat interface for customer support\n",
"\n",
"### Lab Objectives\n",
"\n",
"By the end of this lab, you will have:\n",
"\n",
"- Deployed a customer-facing Streamlit web application\n",
"- Integrated secure user authentication with AgentCore Identity\n",
"- Implemented real-time streaming chat responses\n",
"- Created a complete end-to-end customer support solution\n",
"- Tested the full customer journey from login to support resolution\n",
"\n",
"## Prerequisites\n",
"\n",
"- **Completed Labs 1-4**\n",
"- **Python 3.10+** installed locally\n",
"- **Streamlit** and required frontend dependencies\n",
"- **AgentCore Runtime endpoint** from Lab 4 (deployed and ready)\n",
"- **Amazon Cognito** user pool configured for authentication"
]
},
{
"cell_type": "markdown",
"id": "8576f257",
"metadata": {},
"source": [
"### Step 1: Install Frontend Dependencies\n",
"\n",
"First, let's install the required packages for our Streamlit frontend application."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2154a8d4",
"metadata": {},
"outputs": [],
"source": [
"# Install frontend-specific dependencies\n",
"%pip install -r lab_helpers/lab5_frontend/requirements.txt -q\n",
"print(\"✅ Frontend dependencies installed successfully!\")"
]
},
{
"cell_type": "markdown",
"id": "f57d6047",
"metadata": {},
"source": [
"### Step 2: Understanding the Frontend Architecture\n",
"\n",
"Our Streamlit application consists of several key components:\n",
"\n",
"#### Core Components:\n",
"\n",
"1. **main.py** - Main Streamlit application with UI and authentication\n",
"2. **chat.py** - Chat management and AgentCore Runtime integration\n",
"3. **chat_utils.py** - Utility functions for message formatting and display\n",
"4. **sagemaker_helper.py** - Helper for generating accessible URLs\n",
"\n",
"#### Authentication Flow:\n",
"\n",
"1. User accesses the Streamlit application\n",
"2. Amazon Cognito handles user authentication\n",
"3. Valid JWT tokens are used to authorize AgentCore Runtime requests\n",
"4. User can interact with the Customer Support Agent securely"
]
},
{
"cell_type": "markdown",
"id": "90e03fd3",
"metadata": {},
"source": [
"### Step 3: Launch the Customer Support Frontend 🚀\n",
"\n",
"Now let's start our Streamlit application. The application will:\n",
"\n",
"1. **Generate an accessible URL** for the application\n",
"2. **Start the Streamlit server** on port 8501\n",
"3. **Connect to your deployed AgentCore Runtime** from Lab 4\n",
"4. **Provide a complete customer support interface**\n",
"\n",
"**Important Notes:**\n",
"- The application will run continuously until you stop it (Ctrl+C)\n",
"- Make sure your AgentCore Runtime from Lab 4 is still deployed and running\n",
"- The Cognito authentication tokens are valid for 2 hours"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d5639a9a",
"metadata": {},
"outputs": [],
"source": [
"# Get the accessible URL for the Streamlit application\n",
"from lab_helpers.lab5_frontend.sagemaker_helper import get_streamlit_url\n",
"\n",
"streamlit_url = get_streamlit_url()\n",
"print(f\"\\n🚀 Customer Support Streamlit Application URL:\\n{streamlit_url}\\n\")\n",
"\n",
"# Start the Streamlit application\n",
"!cd lab_helpers/lab5_frontend/ && streamlit run main.py"
]
},
{
"cell_type": "markdown",
"id": "630e4944",
"metadata": {},
"source": [
"### Step 4: Testing Your Customer Support Application\n",
"\n",
"Once your Streamlit application is running, you can test the complete customer support experience:\n",
"\n",
"#### Authentication Testing:\n",
"1. **Access the application** using the Customer Support Streamlit Application URL provided above\n",
"2. **Sign in** with the test credentials provided in the output\n",
"3. **Verify** that you see the welcome message with your username\n",
"\n",
"<div style=\"text-align:left\">\n",
" <img src=\"images/lab5_streamlit_login.png\"/>\n",
"</div>\n",
"<div>\n",
" <img src=\"images/lab5_welcome_user.png\"/>\n",
"</div> \n",
"\n",
"\n",
"#### Customer Support Scenarios to Test:\n",
"\n",
"Product Information Queries: \"What are the specifications for your laptops?\"\n",
"\n",
"Return Policy Questions: \"What's the return policy for electronics?\"\n",
"\n",
"Troubleshooting Support: \"My iPhone is overheating, what should I do?\"\n",
"\n",
"<div style=\"text-align:left\"> \n",
" <img src=\"images/lab5_agent_question.png\" width=\"75%\"/>\n",
"</div>\n",
"\n",
"Memory and Personalization Testing: Have a conversation, then refresh the page\n",
"\n",
"<div style=\"text-align:left\">\n",
" <img src=\"images/lab5_agent_chat_history.png\" width=\"75%\"/>\n",
"</div>\n",
"\n",
"#### What to Observe:\n",
"\n",
"- **Real-time streaming** - Responses appear as they're generated\n",
"- **Response timing** - Performance metrics displayed with each response\n",
"- **Memory persistence** - Agent remembers conversation context\n",
"- **Tool integration** - Agent uses appropriate tools for different queries\n",
"- **Professional UI** - Clean, intuitive customer support interface\n",
"- **Error handling** - Graceful handling of any issues"
]
},
{
"cell_type": "markdown",
"id": "007df2c7",
"metadata": {},
"source": [
"## 🎉 Lab 5 Complete!\n",
"\n",
"Congratulations! You've successfully built and deployed a complete customer-facing frontend application for your AI-powered Customer Support Agent. Here's what you accomplished:\n",
"\n",
"### What You Built\n",
"\n",
"- **Web Interface** - Streamlit-based customer support application\n",
"- **Secure Authentication** - Amazon Cognito integration for user management\n",
"- **Real-time Streaming** - Live response streaming for better user experience\n",
"- **Session Management** - Persistent conversations with memory across interactions\n",
"- **Complete Integration** - Frontend connected to your AgentCore Runtime\n",
"\n",
"### End-to-End Customer Support Solution\n",
"\n",
"You now have a **complete, customer support system** that includes:\n",
"\n",
"1. **Intelligent Agent** (Lab 1) - AI-powered support with custom tools\n",
"2. **Persistent Memory** (Lab 2) - Conversation context and personalization\n",
"3. **Shared Tools & Identity** (Lab 3) - Scalable tool sharing and access control\n",
"4. **Production Runtime** (Lab 4) - Secure, scalable deployment with observability\n",
"5. **Customer Frontend** (Lab 5) - web interface for end users\n",
"\n",
"### Key Capabilities Demonstrated\n",
"\n",
"- **Multi-turn Conversations** - Agent maintains context across interactions\n",
"- **Tool Integration** - Seamless use of product info, return policy, and web search\n",
"- **Memory Persistence** - Customer preferences and history maintained\n",
"- **Real-time Performance** - Streaming responses with performance metrics\n",
"- **Security & Identity** - Proper authentication and authorization\n",
"- **Observability** - Full tracing and monitoring of agent behavior\n",
"\n",
"### Next Steps\n",
"\n",
"To further enhance your customer support solution, consider:\n",
"\n",
"- **Custom Styling** - Brand the frontend with your company's design system\n",
"- **Additional Tools** - Integrate with your existing CRM, ticketing, or knowledge base systems\n",
"- **Multi-language Support** - Add internationalization for global customers\n",
"- **Advanced Analytics** - Implement custom dashboards for support team insights\n",
"- **Mobile Optimization** - Ensure the interface works well on mobile devices\n",
"\n",
"### Cleanup\n",
"\n",
"When you're ready to clean up the resources created in this workshop:\n",
"\n",
"**Ready to clean up?** [Proceed to Cleanup →](lab-06-cleanup.ipynb)\n",
"\n",
"---\n",
"\n",
"**🎊 Congratulations on completing the Amazon Bedrock AgentCore End-to-End Workshop!**\n",
"\n",
"You've successfully built a complete, production-ready AI agent solution from prototype to customer-facing application using Amazon Bedrock AgentCore capabilities."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "ag-gadk-01-9March-0800",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.13"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -0,0 +1,289 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "bc918d1b",
"metadata": {},
"source": [
"# 🧹 AgentCore End-to-End Cleanup\n",
"\n",
"This notebook provides a comprehensive cleanup process for all resources created during the AgentCore End-to-End tutorial.\n",
"\n",
"## Overview\n",
"\n",
"This cleanup process will remove:\n",
"- **Memory**: AgentCore Memory resources and stored data\n",
"- **Runtime**: Agent runtime instances and ECR repositories\n",
"- **Security**: Execution roles, and Authorization Provider resources\n",
"- **Observability**: CloudWatch log groups and streams\n",
"- **Local Files**: Generated configuration and code files\n",
"\n",
"⚠️ **Important**: This cleanup is irreversible. Make sure you have saved any important data (if needed) before proceeding.\n",
"\n",
"---"
]
},
{
"cell_type": "markdown",
"id": "imports_header",
"metadata": {},
"source": [
"## Step 1: Import Required Dependencies\n",
"\n",
"Load all necessary modules and helper functions for the cleanup process."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4d12b515",
"metadata": {},
"outputs": [],
"source": [
"# Note: Uncomment and run only for self-paced labs\n",
"# !aws sts get-caller-identity \n",
"\n",
"# Install required packages \n",
"# %pip install -r requirements.txt -q"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "imports",
"metadata": {},
"outputs": [],
"source": [
"import json\n",
"\n",
"from lab_helpers.lab2_memory import REGION\n",
"from lab_helpers.utils import (\n",
" delete_agentcore_runtime_execution_role,\n",
" delete_ssm_parameter,\n",
" cleanup_cognito_resources,\n",
" get_customer_support_secret,\n",
" delete_customer_support_secret,\n",
" agentcore_memory_cleanup,\n",
" gateway_target_cleanup,\n",
" runtime_resource_cleanup,\n",
" delete_observability_resources,\n",
" local_file_cleanup,\n",
" get_ssm_parameter,\n",
" policy_engine_cleanup\n",
")\n",
"\n",
"print(\"✅ Dependencies imported successfully\")\n",
"print(f\"🌍 Working in region: {REGION}\")"
]
},
{
"cell_type": "markdown",
"id": "memory_header",
"metadata": {},
"source": [
"## Step 2: Clean Up Memory Resources\n",
"\n",
"Remove AgentCore Memory resources and associated data."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "16eda4ba",
"metadata": {},
"outputs": [],
"source": [
"print(\"🧠 Starting Memory cleanup...\")\n",
"agentcore_memory_cleanup(get_ssm_parameter(\"/app/customersupport/agentcore/memory_id\"))"
]
},
{
"cell_type": "markdown",
"id": "runtime_header",
"metadata": {},
"source": [
"## Step 3: Clean Up Runtime Resources\n",
"\n",
"Remove the AgentCore Runtime, ECR repository, and associated AWS resources."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "runtime_cleanup",
"metadata": {},
"outputs": [],
"source": [
"print(\"🚀 Starting Runtime cleanup...\")\n",
"runtime_resource_cleanup(\n",
" get_ssm_parameter(\"/app/customersupport/agentcore/runtime_arn\")\n",
")"
]
},
{
"cell_type": "markdown",
"id": "794ea1ca-f684-4790-a027-f99883a9b3da",
"metadata": {},
"source": [
"## Step 4: Clean Up Gateway Resources\n",
"Remove targets, Gateway"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3b697bc6-69f6-47cc-866e-f6f42de0e2c4",
"metadata": {},
"outputs": [],
"source": [
"# Optional\n",
"# print(\"⚙️ Starting Policy Engine Cleanup...\")\n",
"# policy_engine_cleanup(get_ssm_parameter(\"/app/customersupport/agentcore/policy_engine_id\"))\n",
"\n",
"print(\"⚙️ Starting Gateway Cleanup...\")\n",
"gateway_target_cleanup(get_ssm_parameter(\"/app/customersupport/agentcore/gateway_id\"))"
]
},
{
"cell_type": "markdown",
"id": "security_header",
"metadata": {},
"source": [
"## Step 5: Clean Up Security Resources\n",
"\n",
"Remove execution roles, and authentication resources."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "security_cleanup",
"metadata": {},
"outputs": [],
"source": [
"print(\"🛡️ Starting Security cleanup...\")\n",
"try:\n",
" # bedrock_client = boto3.client(\"bedrock\", region_name=REGION)\n",
"\n",
" # Delete execution role\n",
" print(\" 🗑️ Deleting AgentCore Runtime execution role...\")\n",
" delete_agentcore_runtime_execution_role()\n",
" print(\" ✅ Execution role deleted\")\n",
"\n",
" # Delete SSM parameter\n",
" print(\" 🗑️ Deleting SSM parameter...\")\n",
" delete_ssm_parameter(\"/app/customersupport/agentcore/runtime_arn\")\n",
" print(\" ✅ SSM parameter deleted\")\n",
"\n",
" # Clean up Cognito and secrets\n",
" print(\" 🗑️ Cleaning up Cognito resources...\")\n",
" cs = json.loads(get_customer_support_secret())\n",
" cleanup_cognito_resources(cs[\"pool_id\"])\n",
" print(\" ✅ Cognito resources cleaned up\")\n",
"\n",
" print(\" 🗑️ Deleting customer support secret...\")\n",
" delete_customer_support_secret()\n",
" print(\" ✅ Customer support secret deleted\")\n",
"\n",
"except Exception as e:\n",
" print(f\" ⚠️ Error during security cleanup: {e}\")"
]
},
{
"cell_type": "markdown",
"id": "files_header",
"metadata": {},
"source": [
"## Step 6: Clean Up Local Files\n",
"\n",
"Remove generated configuration and code files from the local directory."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "files_cleanup",
"metadata": {},
"outputs": [],
"source": [
"print(\"📁 Starting Local Files cleanup...\")\n",
"local_file_cleanup()"
]
},
{
"cell_type": "markdown",
"id": "observability_header",
"metadata": {},
"source": [
"## Step 7: Clean Up Observability Resources\n",
"\n",
"Remove CloudWatch log groups and streams used for agent monitoring."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7b7bc594",
"metadata": {},
"outputs": [],
"source": [
"print(\"📊 Starting Observability cleanup...\")\n",
"\n",
"delete_observability_resources()"
]
},
{
"cell_type": "markdown",
"id": "completion_header",
"metadata": {},
"source": [
"## 🎉 Cleanup Complete!\n",
"\n",
"All AgentCore resources have been cleaned up. Here's a summary of what was removed:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "completion_summary",
"metadata": {},
"outputs": [],
"source": [
"print(\"\\n\" + \"=\" * 60)\n",
"print(\"🧹 CLEANUP COMPLETED SUCCESSFULLY! 🧹\")\n",
"print(\"=\" * 60)\n",
"print()\n",
"print(\"📋 Resources cleaned up:\")\n",
"print(\" 🧠 Memory: AgentCore Memory resources and data\")\n",
"print(\" 🚀 Runtime: Agent runtime and ECR repository\")\n",
"print(\" 🛡️ Security: Roles, and SSM secrets\")\n",
"print(\" 📊 Observability: CloudWatch logs\")\n",
"print(\" 📁 Files: Local configuration files\")\n",
"print()\n",
"print(\"✨ Your AWS account is now clean and ready for new experiments!\")\n",
"print(\"\\nThank you for completing the AgentCore End-to-End tutorial! 🚀\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "ag-gadk-01-9March-0800",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.13"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -0,0 +1,198 @@
from strands.tools import tool
from ddgs.exceptions import DDGSException, RatelimitException
from ddgs import DDGS
from strands_tools import retrieve
import boto3
MODEL_ID = "global.anthropic.claude-haiku-4-5-20251001-v1:0"
# System prompt defining the agent's role and capabilities
SYSTEM_PROMPT = """You are a helpful and professional customer support assistant for an electronics e-commerce company.
Your role is to:
- Provide accurate information using the tools available to you
- Support the customer with technical information and product specifications.
- Be friendly, patient, and understanding with customers
- Always offer additional help after answering questions
- If you can't help with something, direct customers to the appropriate contact
You have access to the following tools:
1. get_return_policy() - For warranty and return policy questions
2. get_product_info() - To get information about a specific product
3. web_search() - To access current technical documentation, or for updated information.
Always use the appropriate tool to get accurate, up-to-date information rather than making assumptions about electronic products or specifications."""
@tool
def web_search(keywords: str, region: str = "us-en", max_results: int = 5) -> str:
"""Search the web for updated information.
Args:
keywords (str): The search query keywords.
region (str): The search region: wt-wt, us-en, uk-en, ru-ru, etc..
max_results (int | None): The maximum number of results to return.
Returns:
List of dictionaries with search results.
"""
try:
results = DDGS().text(keywords, region=region, max_results=max_results)
return results if results else "No results found."
except RatelimitException:
return "Rate limit reached. Please try again later."
except DDGSException as e:
return f"Search error: {e}"
except Exception as e:
return f"Search error: {str(e)}"
@tool
def get_return_policy(product_category: str) -> str:
"""
Get return policy information for a specific product category.
Args:
product_category: Electronics category (e.g., 'smartphones', 'laptops', 'accessories')
Returns:
Formatted return policy details including timeframes and conditions
"""
# Mock return policy database - in real implementation, this would query policy database
return_policies = {
"smartphones": {
"window": "30 days",
"condition": "Original packaging, no physical damage, factory reset required",
"process": "Online RMA portal or technical support",
"refund_time": "5-7 business days after inspection",
"shipping": "Free return shipping, prepaid label provided",
"warranty": "1-year manufacturer warranty included",
},
"laptops": {
"window": "30 days",
"condition": "Original packaging, all accessories, no software modifications",
"process": "Technical support verification required before return",
"refund_time": "7-10 business days after inspection",
"shipping": "Free return shipping with original packaging",
"warranty": "1-year manufacturer warranty, extended options available",
},
"accessories": {
"window": "30 days",
"condition": "Unopened packaging preferred, all components included",
"process": "Online return portal",
"refund_time": "3-5 business days after receipt",
"shipping": "Customer pays return shipping under $50",
"warranty": "90-day manufacturer warranty",
},
}
# Default policy for unlisted categories
default_policy = {
"window": "30 days",
"condition": "Original condition with all included components",
"process": "Contact technical support",
"refund_time": "5-7 business days after inspection",
"shipping": "Return shipping policies vary",
"warranty": "Standard manufacturer warranty applies",
}
policy = return_policies.get(product_category.lower(), default_policy)
return (
f"Return Policy - {product_category.title()}:\n\n"
f"• Return window: {policy['window']} from delivery\n"
f"• Condition: {policy['condition']}\n"
f"• Process: {policy['process']}\n"
f"• Refund timeline: {policy['refund_time']}\n"
f"• Shipping: {policy['shipping']}\n"
f"• Warranty: {policy['warranty']}"
)
@tool
def get_product_info(product_type: str) -> str:
"""
Get detailed technical specifications and information for electronics products.
Args:
product_type: Electronics product type (e.g., 'laptops', 'smartphones', 'headphones', 'monitors')
Returns:
Formatted product information including warranty, features, and policies
"""
# Mock product catalog - in real implementation, this would query a product database
products = {
"laptops": {
"warranty": "1-year manufacturer warranty + optional extended coverage",
"specs": "Intel/AMD processors, 8-32GB RAM, SSD storage, various display sizes",
"features": "Backlit keyboards, USB-C/Thunderbolt, Wi-Fi 6, Bluetooth 5.0",
"compatibility": "Windows 11, macOS, Linux support varies by model",
"support": "Technical support and driver updates included",
},
"smartphones": {
"warranty": "1-year manufacturer warranty",
"specs": "5G/4G connectivity, 128GB-1TB storage, multiple camera systems",
"features": "Wireless charging, water resistance, biometric security",
"compatibility": "iOS/Android, carrier unlocked options available",
"support": "Software updates and technical support included",
},
"headphones": {
"warranty": "1-year manufacturer warranty",
"specs": "Wired/wireless options, noise cancellation, 20Hz-20kHz frequency",
"features": "Active noise cancellation, touch controls, voice assistant",
"compatibility": "Bluetooth 5.0+, 3.5mm jack, USB-C charging",
"support": "Firmware updates via companion app",
},
"monitors": {
"warranty": "3-year manufacturer warranty",
"specs": "4K/1440p/1080p resolutions, IPS/OLED panels, various sizes",
"features": "HDR support, high refresh rates, adjustable stands",
"compatibility": "HDMI, DisplayPort, USB-C inputs",
"support": "Color calibration and technical support",
},
}
product = products.get(product_type.lower())
if not product:
return f"Technical specifications for {product_type} not available. Please contact our technical support team for detailed product information and compatibility requirements."
return (
f"Technical Information - {product_type.title()}:\n\n"
f"• Warranty: {product['warranty']}\n"
f"• Specifications: {product['specs']}\n"
f"• Key Features: {product['features']}\n"
f"• Compatibility: {product['compatibility']}\n"
f"• Support: {product['support']}"
)
@tool
def get_technical_support(issue_description: str) -> str:
try:
# Get KB ID from parameter store
ssm = boto3.client("ssm")
account_id = boto3.client("sts").get_caller_identity()["Account"]
region = boto3.Session().region_name
kb_id = ssm.get_parameter(Name=f"/{account_id}-{region}/kb/knowledge-base-id")[
"Parameter"
]["Value"]
print(f"Successfully retrieved KB ID: {kb_id}")
# Use strands retrieve tool
tool_use = {
"toolUseId": "tech_support_query",
"input": {
"text": issue_description,
"knowledgeBaseId": kb_id,
"region": region,
"numberOfResults": 3,
"score": 0.4,
},
}
result = retrieve.retrieve(tool_use)
if result["status"] == "success":
return result["content"][0]["text"]
else:
return f"Unable to access technical support documentation. Error: {result['content'][0]['text']}"
except Exception as e:
print(f"Detailed error in get_technical_support: {str(e)}")
return f"Unable to access technical support documentation. Error: {str(e)}"
@@ -0,0 +1,180 @@
#!/usr/bin/python
"""AgentCore Memory integration for Strands agents."""
import logging
import uuid
import boto3
from bedrock_agentcore.memory import MemoryClient
from bedrock_agentcore.memory.constants import StrategyType
from boto3.session import Session
from lab_helpers.utils import get_ssm_parameter, put_ssm_parameter
from strands.hooks import (
AfterInvocationEvent,
HookProvider,
HookRegistry,
MessageAddedEvent,
)
boto_session = Session()
REGION = boto_session.region_name
logger = logging.getLogger(__name__)
ACTOR_ID = "customer_001"
SESSION_ID = str(uuid.uuid4())
memory_client = MemoryClient(region_name=REGION)
memory_name = "CustomerSupportMemory"
def create_or_get_memory_resource():
try:
memory_id = get_ssm_parameter("/app/customersupport/agentcore/memory_id")
memory_client.gmcp_client.get_memory(memoryId=memory_id)
return memory_id
except Exception:
try:
strategies = [
{
StrategyType.USER_PREFERENCE.value: {
"name": "CustomerPreferences",
"description": "Captures customer preferences and behavior",
"namespaces": ["support/customer/{actorId}/preferences"],
}
},
{
StrategyType.SEMANTIC.value: {
"name": "CustomerSupportSemantic",
"description": "Stores facts from conversations",
"namespaces": ["support/customer/{actorId}/semantic"],
}
},
]
print("Creating AgentCore Memory resources. This can a couple of minutes..")
# *** AGENTCORE MEMORY USAGE *** - Create memory resource with semantic and user_pref strategy
response = memory_client.create_memory_and_wait(
name=memory_name,
description="Customer support agent memory",
strategies=strategies,
event_expiry_days=90, # Memories expire after 90 days
)
memory_id = response["id"]
try:
put_ssm_parameter("/app/customersupport/agentcore/memory_id", memory_id)
except Exception as e:
raise e
return memory_id
except Exception:
return None
def delete_memory(memory_hook):
try:
ssm_client = boto3.client("ssm", region_name=REGION)
memory_client.delete_memory(memory_id=memory_hook.memory_id)
ssm_client.delete_parameter(Name="/app/customersupport/agentcore/memory_id")
except Exception:
pass
class CustomerSupportMemoryHooks(HookProvider):
"""Memory hooks for customer support agent"""
def __init__(
self, memory_id: str, client: MemoryClient, actor_id: str, session_id: str
):
self.memory_id = memory_id
self.client = client
self.actor_id = actor_id
self.session_id = session_id
self.namespaces = {
i["type"]: i["namespaces"][0]
for i in self.client.get_memory_strategies(self.memory_id)
}
def retrieve_customer_context(self, event: MessageAddedEvent):
"""Retrieve customer context before processing support query"""
messages = event.agent.messages
if (
messages[-1]["role"] == "user"
and "toolResult" not in messages[-1]["content"][0]
):
user_query = messages[-1]["content"][0]["text"]
try:
all_context = []
for context_type, namespace in self.namespaces.items():
# *** AGENTCORE MEMORY USAGE *** - Retrieve customer context from each namespace
memories = self.client.retrieve_memories(
memory_id=self.memory_id,
namespace=namespace.format(actorId=self.actor_id),
query=user_query,
top_k=3,
)
# Post-processing: Format memories into context strings
for memory in memories:
if isinstance(memory, dict):
content = memory.get("content", {})
if isinstance(content, dict):
text = content.get("text", "").strip()
if text:
all_context.append(
f"[{context_type.upper()}] {text}"
)
# Inject customer context into the query
if all_context:
context_text = "\n".join(all_context)
original_text = messages[-1]["content"][0]["text"]
messages[-1]["content"][0]["text"] = (
f"Customer Context:\n{context_text}\n\n{original_text}"
)
logger.info(f"Retrieved {len(all_context)} customer context items")
except Exception as e:
logger.error(f"Failed to retrieve customer context: {e}")
def save_support_interaction(self, event: AfterInvocationEvent):
"""Save customer support interaction after agent response"""
try:
messages = event.agent.messages
if len(messages) >= 2 and messages[-1]["role"] == "assistant":
# Get last customer query and agent response
customer_query = None
agent_response = None
for msg in reversed(messages):
if msg["role"] == "assistant" and not agent_response:
agent_response = msg["content"][0]["text"]
elif (
msg["role"] == "user"
and not customer_query
and "toolResult" not in msg["content"][0]
):
customer_query = msg["content"][0]["text"]
break
if customer_query and agent_response:
# *** AGENTCORE MEMORY USAGE *** - Save the support interaction
self.client.create_event(
memory_id=self.memory_id,
actor_id=self.actor_id,
session_id=self.session_id,
messages=[
(customer_query, "USER"),
(agent_response, "ASSISTANT"),
],
)
logger.info("Saved support interaction to memory")
except Exception as e:
logger.error(f"Failed to save support interaction: {e}")
def register_hooks(self, registry: HookRegistry) -> None:
"""Register customer support memory hooks"""
registry.add_callback(MessageAddedEvent, self.retrieve_customer_context)
registry.add_callback(AfterInvocationEvent, self.save_support_interaction)
logger.info("Customer support memory hooks registered")
@@ -0,0 +1,392 @@
import os
import uuid
import asyncio
import concurrent.futures
import concurrent.futures
from bedrock_agentcore.runtime import (
BedrockAgentCoreApp,
) #### AGENTCORE RUNTIME - LINE 1 ####
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
from google.adk.agents import LlmAgent
from google.adk.models.lite_llm import LiteLlm
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types
import boto3
from bedrock_agentcore.memory import MemoryClient
from lab_helpers.utils import get_ssm_parameter
# Initialize boto3 client
sts_client = boto3.client('sts')
# Get AWS account details
REGION = boto3.session.Session().region_name
ACTOR_ID = "customer_001"
# Lab2 import: Memory
memory_id = os.environ.get("MEMORY_ID")
if not memory_id:
raise Exception("Environment variable MEMORY_ID is required")
memory_client = MemoryClient(region_name=REGION)
# ============================================================
# System prompt
# ============================================================
SYSTEM_PROMPT = """You are a helpful and professional customer support assistant for an electronics e-commerce company.
Your role is to:
- Provide accurate information using the tools available to you
- Support the customer with technical information and product specifications.
- Be friendly, patient, and understanding with customers
- Always offer additional help after answering questions
- If you can't help with something, direct customers to the appropriate contact
You have access to the following tools:
1. get_return_policy() - For warranty and return policy questions
2. get_product_info() - To get information about a specific product
3. get_technical_support() - To search the technical support knowledge base
4. check_warranty_status() - To check warranty status via serial number (via AgentCore Gateway)
5. web_search() - To access current technical documentation, or for updated information (via AgentCore Gateway)
Always use the appropriate tool to get accurate, up-to-date information rather than making assumptions about electronic products or specifications."""
# ============================================================
# Local tools (same as Lab 3)
# ============================================================
def get_return_policy(product_category: str) -> str:
"""Get return policy information for a specific product category.
Args:
product_category: Electronics category (e.g., 'smartphones', 'laptops', 'accessories')
Returns:
Formatted return policy details including timeframes and conditions
"""
return_policies = {
"smartphones": {
"window": "30 days",
"condition": "Original packaging, no physical damage, factory reset required",
"process": "Online RMA portal or technical support",
"refund_time": "5-7 business days after inspection",
"shipping": "Free return shipping, prepaid label provided",
"warranty": "1-year manufacturer warranty included",
},
"laptops": {
"window": "30 days",
"condition": "Original packaging, all accessories, no software modifications",
"process": "Technical support verification required before return",
"refund_time": "7-10 business days after inspection",
"shipping": "Free return shipping with original packaging",
"warranty": "1-year manufacturer warranty, extended options available",
},
"accessories": {
"window": "30 days",
"condition": "Unopened packaging preferred, all components included",
"process": "Online return portal",
"refund_time": "3-5 business days after receipt",
"shipping": "Customer pays return shipping under $50",
"warranty": "90-day manufacturer warranty",
},
}
default_policy = {
"window": "30 days",
"condition": "Original condition with all included components",
"process": "Contact technical support",
"refund_time": "5-7 business days after inspection",
"shipping": "Return shipping policies vary",
"warranty": "Standard manufacturer warranty applies",
}
policy = return_policies.get(product_category.lower(), default_policy)
return (
f"Return Policy - {product_category.title()}:\n\n"
f"\u2022 Return window: {policy['window']} from delivery\n"
f"\u2022 Condition: {policy['condition']}\n"
f"\u2022 Process: {policy['process']}\n"
f"\u2022 Refund timeline: {policy['refund_time']}\n"
f"\u2022 Shipping: {policy['shipping']}\n"
f"\u2022 Warranty: {policy['warranty']}"
)
def get_product_info(product_type: str) -> str:
"""Get detailed technical specifications and information for electronics products.
Args:
product_type: Electronics product type (e.g., 'laptops', 'smartphones', 'headphones', 'monitors')
Returns:
Formatted product information including warranty, features, and policies
"""
products = {
"laptops": {
"warranty": "1-year standard, 3-year extended available",
"specs": "Intel/AMD processors, 8-64GB RAM, SSD storage",
"features": "Backlit keyboards, fingerprint readers, Thunderbolt ports",
"compatibility": "Windows, Linux, macOS (Apple only)",
"support": "24/7 technical support, on-site repair options",
},
"smartphones": {
"warranty": "1-year manufacturer, 2-year extended",
"specs": "Latest processors, 6-12GB RAM, 128GB-1TB storage",
"features": "5G capable, water resistant, wireless charging",
"compatibility": "iOS or Android ecosystem",
"support": "In-store and mail-in repair services",
},
"headphones": {
"warranty": "1-year standard warranty",
"specs": "Bluetooth 5.0+, ANC, 20-40hr battery",
"features": "Active noise cancellation, transparency mode, multipoint",
"compatibility": "Universal Bluetooth, some with proprietary apps",
"support": "Replacement program for defective units",
},
"monitors": {
"warranty": "3-year standard, zero dead pixel guarantee",
"specs": "4K/1440p resolution, 60-240Hz refresh rate",
"features": "HDR support, high refresh rates, adjustable stands",
"compatibility": "HDMI, DisplayPort, USB-C inputs",
"support": "Color calibration and technical support",
},
}
product = products.get(product_type.lower())
if not product:
return f"Technical specifications for {product_type} not available. Please contact our technical support team."
return (
f"Technical Information - {product_type.title()}:\n\n"
f"\u2022 Warranty: {product['warranty']}\n"
f"\u2022 Specifications: {product['specs']}\n"
f"\u2022 Key Features: {product['features']}\n"
f"\u2022 Compatibility: {product['compatibility']}\n"
f"\u2022 Support: {product['support']}"
)
def get_technical_support(issue_description: str) -> str:
"""Search the technical support knowledge base for troubleshooting help.
Args:
issue_description: Description of the technical issue or question.
Returns:
Relevant technical support documentation and troubleshooting steps.
"""
try:
ssm = boto3.client("ssm")
acct = boto3.client("sts").get_caller_identity()["Account"]
region = boto3.Session().region_name
kb_id = ssm.get_parameter(Name=f"/{acct}-{region}/kb/knowledge-base-id")["Parameter"]["Value"]
bedrock_agent_runtime = boto3.client("bedrock-agent-runtime", region_name=region)
response = bedrock_agent_runtime.retrieve(
knowledgeBaseId=kb_id,
retrievalQuery={"text": issue_description},
retrievalConfiguration={"vectorSearchConfiguration": {"numberOfResults": 3}},
)
results = response.get("retrievalResults", [])
if not results:
return "No relevant technical support documentation found for this issue."
formatted_results = []
for i, result in enumerate(results, 1):
text = result.get("content", {}).get("text", "")
score = result.get("score", 0)
if score >= 0.4:
formatted_results.append(f"--- Result {i} (relevance: {score:.2f}) ---\n{text}")
if not formatted_results:
return "No sufficiently relevant technical support documentation found."
return "\n\n".join(formatted_results)
except Exception as e:
return f"Unable to access technical support documentation. Error: {str(e)}"
# ============================================================
# MCP Gateway tool wrappers (same pattern as Lab 3)
# ============================================================
async def _call_mcp_tool(tool_name: str, arguments: dict, gateway_url: str, auth_header: str) -> str:
"""Helper to call an MCP tool on the AgentCore Gateway."""
async with streamablehttp_client(
gateway_url,
headers={"Authorization": auth_header},
) as (read_stream, write_stream, _):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
result = await session.call_tool(tool_name, arguments)
if result.content:
return "\n".join(
part.text for part in result.content if hasattr(part, "text")
)
return "No result returned."
def _run_async_in_thread(coro):
"""Run an async coroutine in a separate thread to avoid 'event loop already running' errors."""
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
future = executor.submit(asyncio.run, coro)
return future.result()
# These will be set per-request in the entrypoint
_gateway_url = None
_auth_header = None
def check_warranty_status(serial_number: str, customer_email: str) -> str:
"""Check the warranty status of a product using its serial number.
Args:
serial_number: The product serial number to look up.
customer_email: Customer email for verification. Pass empty string if not available.
Returns:
Warranty status information for the product.
"""
args = {"serial_number": serial_number}
if customer_email:
args["customer_email"] = customer_email
return _run_async_in_thread(
_call_mcp_tool("LambdaUsingSDK___check_warranty_status", args, _gateway_url, _auth_header)
)
def web_search(keywords: str, region: str, max_results: int) -> str:
"""Search the web for updated information using DuckDuckGo.
Args:
keywords: The search query keywords.
region: The search region (e.g., us-en, uk-en, ru-ru).
max_results: The maximum number of results to return.
Returns:
Search results from the web.
"""
args = {"keywords": keywords, "region": region, "max_results": max_results}
return _run_async_in_thread(
_call_mcp_tool("LambdaUsingSDK___web_search", args, _gateway_url, _auth_header)
)
# Initialize the AgentCore Runtime App
app = BedrockAgentCoreApp() #### AGENTCORE RUNTIME - LINE 2 ####
@app.entrypoint #### AGENTCORE RUNTIME - LINE 3 ####
async def invoke(payload, context=None):
"""AgentCore Runtime entrypoint function"""
global _gateway_url, _auth_header
user_input = payload.get("prompt", "")
session_id = context.session_id # Get session_id from context
actor_id = payload.get("actor_id", ACTOR_ID)
# Access request headers - handle None case
request_headers = context.request_headers or {}
# Get Client JWT token
auth_header = request_headers.get('Authorization', '')
print(f"Authorization header: {auth_header}")
# Get Gateway ID
existing_gateway_id = get_ssm_parameter("/app/customersupport/agentcore/gateway_id")
# Initialize Bedrock AgentCore Control client
gateway_client = boto3.client(
"bedrock-agentcore-control",
region_name=REGION,
)
# Get existing gateway details
gateway_response = gateway_client.get_gateway(gatewayIdentifier=existing_gateway_id)
gateway_url = gateway_response['gatewayUrl']
if gateway_url and auth_header:
try:
# Set module-level vars for MCP tool wrappers
_gateway_url = gateway_url
_auth_header = auth_header
# All tools: local + MCP gateway wrappers
all_tools = [
get_product_info,
get_return_policy,
get_technical_support,
check_warranty_status,
web_search,
]
# --- 1. Retrieve customer context from memory ---
all_context = []
namespaces = {
"preferences": f"support/customer/{actor_id}/preferences/",
"semantic": f"support/customer/{actor_id}/semantic/",
}
for context_type, namespace in namespaces.items():
try:
memories = memory_client.retrieve_memories(
memory_id=memory_id,
namespace=namespace,
query=user_input,
top_k=3,
)
for mem in memories:
if isinstance(mem, dict):
text = mem.get("content", {}).get("text", "").strip()
if text:
all_context.append(f"[{context_type.upper()}] {text}")
except Exception as e:
print(f"Warning: Could not retrieve {context_type} memories: {e}")
# --- 2. Build enriched query with context ---
if all_context:
context_text = "\n".join(all_context)
enriched_query = f"Customer Context:\n{context_text}\n\n{user_input}"
else:
enriched_query = user_input
# --- 3. Create and run the ADK agent ---
agent = LlmAgent(
name="customer_support_agent",
model=LiteLlm(model="bedrock/global.anthropic.claude-haiku-4-5-20251001-v1:0"),
instruction=SYSTEM_PROMPT,
tools=all_tools,
)
adk_session_id = str(uuid.uuid4())
session_service = InMemorySessionService()
adk_session = await session_service.create_session(
app_name="customer_support_app", user_id="user_001", session_id=adk_session_id
)
runner = Runner(agent=agent, app_name="customer_support_app", session_service=session_service)
content = types.Content(role="user", parts=[types.Part(text=enriched_query)])
final_response = ""
async for event in runner.run_async(
user_id="user_001", session_id=adk_session_id, new_message=content
):
if event.is_final_response():
final_response = event.content.parts[0].text
# --- 4. Save interaction to memory ---
if final_response:
try:
memory_client.create_event(
memory_id=memory_id,
actor_id=actor_id,
session_id=str(session_id),
messages=[
(user_input, "USER"),
(final_response, "ASSISTANT"),
],
)
except Exception as e:
print(f"Warning: Could not save to memory: {e}")
return final_response
except Exception as e:
print(f"Agent error: {str(e)}")
return f"Error: {str(e)}"
else:
return "Error: Missing gateway URL or authorization header"
if __name__ == "__main__":
app.run() #### AGENTCORE RUNTIME - LINE 4 ####
@@ -0,0 +1,138 @@
#!/usr/bin/python
"""AgentCore Evaluation helpers for Lab 5 - retrieves agent role and attaches evaluation policy."""
import json
import os
import boto3
from boto3.session import Session
from lab_helpers.utils import get_ssm_parameter
boto_session = Session()
REGION = boto_session.region_name
EVALUATION_POLICY_SUFFIX = "AgentCoreEvaluationPolicy"
def get_execution_role_arn_from_runtime():
"""Retrieve the execution role ARN from the AgentCore runtime agent configuration.
Falls back to SSM parameter if runtime lookup fails.
Returns:
str: The execution role ARN
"""
try:
# Try SSM first (stored in lab04 via create_agentcore_runtime_execution_role)
role_arn = get_ssm_parameter(
"/app/customersupport/agentcore/runtime_execution_role_arn"
)
if role_arn:
print(f"✅ Retrieved execution_role_arn from SSM: {role_arn}")
return role_arn
except Exception:
pass
# Fallback: get from the runtime agent configuration
try:
agent_arn = get_ssm_parameter("/app/customersupport/agentcore/runtime_arn")
runtime_id = agent_arn.split(":")[-1].split("/")[-1]
control_client = boto3.client("bedrock-agentcore-control", region_name=REGION)
response = control_client.get_agent_runtime(agentRuntimeId=runtime_id)
role_arn = response.get("roleArn")
if role_arn:
print(f"✅ Retrieved execution_role_arn from runtime config: {role_arn}")
return role_arn
except Exception as e:
print(f"⚠️ Could not retrieve role from runtime: {e}")
raise RuntimeError(
"Could not retrieve execution_role_arn. Please run Lab 4 first."
)
def attach_evaluation_policy(execution_role_arn: str, policy_json_path: str = None):
"""Attach the AgentCore evaluation policy to the agent's execution role.
Args:
execution_role_arn: The IAM role ARN to attach the policy to.
policy_json_path: Path to the evaluation policy JSON file.
Defaults to lab_helpers/lab5_evaluation/agentcore-evaluation-policy.json
Returns:
str: The policy ARN that was attached
"""
if not policy_json_path:
policy_json_path = os.path.join(
os.path.dirname(__file__),
"lab5_evaluation",
"agentcore-evaluation-policy.json",
)
# Load the policy document
with open(policy_json_path, "r") as f:
policy_document = json.load(f)
iam = boto3.client("iam")
account_id = boto3.client("sts").get_caller_identity()["Account"]
role_name = execution_role_arn.split("/")[-1]
policy_name = f"{role_name}-{EVALUATION_POLICY_SUFFIX}"
policy_arn = f"arn:aws:iam::{account_id}:policy/{policy_name}"
# Check if policy already attached
try:
attached = iam.list_attached_role_policies(RoleName=role_name)
for p in attached.get("AttachedPolicies", []):
if EVALUATION_POLICY_SUFFIX in p["PolicyName"]:
print(f"️ Evaluation policy already attached: {p['PolicyArn']}")
return p["PolicyArn"]
except Exception:
pass
# Create or update the policy
try:
iam.get_policy(PolicyArn=policy_arn)
print(f"️ Policy {policy_name} already exists")
except iam.exceptions.NoSuchEntityException:
iam.create_policy(
PolicyName=policy_name,
PolicyDocument=json.dumps(policy_document),
Description="AgentCore Evaluation permissions for online evaluation",
)
print(f"✅ Created policy: {policy_name}")
# Attach to role
iam.attach_role_policy(RoleName=role_name, PolicyArn=policy_arn)
print(f"✅ Attached evaluation policy to role: {role_name}")
return policy_arn
def ensure_evaluation_role(execution_role_arn: str = None):
"""Ensure the execution role has evaluation permissions.
If execution_role_arn is None or empty, retrieves it from the runtime.
Then attaches the evaluation policy if not already attached.
Args:
execution_role_arn: Optional role ARN. If empty/None, auto-retrieves.
Returns:
str: The validated execution_role_arn
"""
if not execution_role_arn or not execution_role_arn.strip():
print("⚠️ execution_role_arn is empty, retrieving from runtime...")
execution_role_arn = get_execution_role_arn_from_runtime()
# Validate format
if not execution_role_arn.startswith("arn:aws:iam::"):
# Looser check - just ensure it looks like an ARN
if "arn:" not in execution_role_arn or ":role/" not in execution_role_arn:
raise ValueError(
f"Invalid execution_role_arn format: {execution_role_arn}"
)
print(f"Using execution_role_arn: {execution_role_arn}")
attach_evaluation_policy(execution_role_arn)
return execution_role_arn
@@ -0,0 +1,58 @@
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AgentCoreEvaluationActions",
"Effect": "Allow",
"Action": [
"bedrock-agentcore:CreateEvaluator",
"bedrock-agentcore:GetEvaluator",
"bedrock-agentcore:ListEvaluators",
"bedrock-agentcore:UpdateEvaluator",
"bedrock-agentcore:DeleteEvaluator",
"bedrock-agentcore:CreateOnlineEvaluationConfig",
"bedrock-agentcore:GetOnlineEvaluationConfig",
"bedrock-agentcore:ListOnlineEvaluationConfigs",
"bedrock-agentcore:UpdateOnlineEvaluationConfig",
"bedrock-agentcore:DeleteOnlineEvaluationConfig",
"bedrock-agentcore:Evaluate"
],
"Resource": "*"
},
{
"Sid": "PassRoleForEvaluation",
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "arn:aws:iam::*:role/AgentCoreEvaluationRole*",
"Condition": {
"StringEquals": {
"iam:PassedToService": "bedrock-agentcore.amazonaws.com"
}
}
},
{
"Sid": "BedrockModelInvocation",
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel",
"bedrock:Converse",
"bedrock:InvokeModelWithResponseStream",
"bedrock:ConverseStream"
],
"Resource": [
"arn:aws:bedrock:*::foundation-model/*",
"arn:aws:bedrock:*:*:inference-profile/*"
]
},
{
"Sid": "CloudWatchLogsForObservability",
"Effect": "Allow",
"Action": [
"logs:DescribeIndexPolicies",
"logs:PutIndexPolicy",
"logs:CreateLogGroup"
],
"Resource": "*"
}
]
}
@@ -0,0 +1,148 @@
<div align="center">
# 🧪 Lab 5 — Evaluation Data Generator
### Strands-based Agent · Isolated Resources · Zero Conflict with Workshop Labs
<br>
```
┌─────────────────────────────────────────────────────────┐
│ │
│ ⚡ setup → test → generate → cleanup ⚡ │
│ │
│ Creates a full Strands agent stack (Labs 1-4) │
│ then generates 30 min of evaluation traffic │
│ │
└─────────────────────────────────────────────────────────┘
```
</div>
---
## 🎯 What This Does
This helper script replicates **Labs 1 through 4** of the workshop using **Strands agents** (not Google ADK), with **completely different resource names** to avoid any conflict with the main workshop labs.
It provisions a full agent stack, verifies it works, then hammers it with varied customer support prompts for 30 minutes to generate evaluation data.
---
## 🏗️ Resource Isolation
Every resource created by this script uses a separate namespace:
| Resource | Workshop Labs (Google ADK) | This Script (Strands) |
|:---------|:--------------------------|:----------------------|
| Memory | `CustomerSupportMemory` | `EvalSupportMemory` |
| Gateway | `customersupport-gw` | `evalsupport-gw` |
| Runtime Agent | `customer_support_agent` | `eval_support_agent` |
| SSM Prefix | `/app/customersupport/agentcore/` | `/app/evalsupport/agentcore/` |
| IAM Role | `CustomerSupportAssistant...` | `EvalSupportAgentCoreRole-{region}` |
| Actor ID | `customer_001` | `eval_customer_001` |
> 💡 The script **reuses** the existing Cognito pool and Lambda function from the workshop prerequisites — it only reads them, never modifies.
---
## 🚀 Quick Start
```bash
# Navigate to the scripts directory
cd lab_helpers/lab5_evaluation/scripts/
# 1️⃣ Create all resources (Memory → Gateway → Runtime)
python lab5_evaluation_helper.py setup
# 2️⃣ Verify with a single test invocation
python lab5_evaluation_helper.py test
# 3️⃣ Generate evaluation data (default: 30 minutes)
python lab5_evaluation_helper.py generate
# 4️⃣ Tear down all eval resources when done
python lab5_evaluation_helper.py cleanup
```
---
## 📋 Commands
### `setup`
Provisions the full stack in order:
1. **Agent tools** — Strands `@tool` decorators (return policy, product info, KB retrieval)
2. **AgentCore Memory** — Creates `EvalSupportMemory` with preference + semantic strategies
3. **AgentCore Gateway** — Creates `evalsupport-gw` with Lambda target
4. **AgentCore Runtime** — Builds container, deploys `eval_support_agent`, waits for READY
⏱️ Takes ~10-15 minutes (container build + deployment)
### `test`
Sends a single query (`"What is the return policy for laptops?"`) to the deployed eval agent and prints the response. Use this to verify the stack is healthy before generating data.
### `generate`
Invokes the eval agent with **30 varied customer support prompts** in a loop for 30 minutes (configurable). Includes automatic token refresh on auth errors.
```bash
# Custom duration
python lab5_evaluation_helper.py generate --duration 60 # 1 hour
python lab5_evaluation_helper.py generate --duration 10 # 10 minutes
```
### `cleanup`
Deletes all eval-specific resources in reverse order:
- Runtime agent → Gateway targets → Gateway → Memory → IAM role/policy → SSM parameters
- Also removes the generated `eval_runtime_entrypoint.py` file
---
## 📁 Files
```
lab5_evaluation/scripts/
├── lab5_evaluation_helper.py # Main script (this helper)
├── requirements.txt # Python dependencies
├── README.md # You are here
└── eval_runtime_entrypoint.py # Auto-generated by `setup` (gitignored)
```
---
## ⚙️ Prerequisites
- Workshop **prerequisite stack** must be deployed (CloudFormation)
- Labs **1-3** should have been run at least once (Cognito pool, Lambda, KB exist)
- Python packages from `requirements.txt` installed:
```bash
pip install -r requirements.txt
```
---
## 🔧 Architecture
```
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ │ │ │ │ │
│ Eval Helper │────▶│ AgentCore │────▶│ Strands Agent │
│ (this script) │ │ Runtime │ │ + MCP Gateway │
│ │ │ │ │ + Memory │
└──────────────────┘ └──────────────────┘ └──────────────────┘
┌──────────────────┐
│ Lambda Tools │
│ (warranty, │
│ web search) │
└──────────────────┘
```
---
<div align="center">
*Built for the Amazon Bedrock AgentCore Workshop — Lab 5 Evaluation*
</div>
@@ -0,0 +1,959 @@
#!/usr/bin/env python3
"""
Lab 5 Evaluation Helper — Strands-based agent setup for evaluation data generation.
This script replicates Labs 1-4 using Strands agents with COMPLETELY DIFFERENT
resource names to avoid conflicts with the Google ADK workshop labs.
Workshop labs use:
- Memory: "CustomerSupportMemory"
- Gateway: "customersupport-gw"
- Runtime agent: "customer_support_agent"
- SSM prefix: "/app/customersupport/agentcore/"
- IAM role: "CustomerSupportAssistantBedrockAgentCoreRole-{region}"
- Cognito pool: "customer-support-pool"
This script uses:
- Memory: "EvalSupportMemory"
- Gateway: "evalsupport-gw"
- Runtime agent: "eval_support_agent"
- SSM prefix: "/app/evalsupport/agentcore/"
- IAM role: "EvalSupportAgentCoreRole-{region}"
- Cognito pool: reuses existing (read-only)
Usage:
python lab5_evaluation_helper.py setup # Create all resources (Labs 1-4)
python lab5_evaluation_helper.py test # Single invocation test
python lab5_evaluation_helper.py generate # Generate data for 30 minutes
python lab5_evaluation_helper.py cleanup # Tear down eval resources
"""
import argparse
import json
import os
import random
import time
import uuid
import boto3
from boto3.session import Session
# ---------------------------------------------------------------------------
# Constants — all names are prefixed with "eval" to avoid conflicts
# ---------------------------------------------------------------------------
EVAL_MEMORY_NAME = "EvalSupportMemory"
EVAL_GATEWAY_NAME = "evalsupport-gw"
EVAL_AGENT_NAME = "eval_support_agent"
EVAL_SSM_PREFIX = "/app/evalsupport/agentcore"
EVAL_ROLE_NAME_TEMPLATE = "EvalSupportAgentCoreRole-{region}"
EVAL_POLICY_NAME_TEMPLATE = "EvalSupportAgentCorePolicy-{region}"
EVAL_ACTOR_ID = "eval_customer_001"
MODEL_ID = "global.anthropic.claude-haiku-4-5-20251001-v1:0"
boto_session = Session()
REGION = boto_session.region_name
ACCOUNT_ID = boto3.client("sts").get_caller_identity()["Account"]
# ---------------------------------------------------------------------------
# SSM helpers (self-contained, no import from lab_helpers.utils)
# ---------------------------------------------------------------------------
ssm_client = boto3.client("ssm", region_name=REGION)
def put_ssm(name, value):
ssm_client.put_parameter(Name=name, Value=value, Type="String", Overwrite=True)
def get_ssm(name):
return ssm_client.get_parameter(Name=name, WithDecryption=True)["Parameter"]["Value"]
def delete_ssm(name):
try:
ssm_client.delete_parameter(Name=name)
except ssm_client.exceptions.ParameterNotFound:
pass
# ---------------------------------------------------------------------------
# Step 1 — Create Strands Agent tools (Lab 1 equivalent)
# ---------------------------------------------------------------------------
SYSTEM_PROMPT = """You are a helpful and professional customer support assistant for an electronics e-commerce company.
Your role is to:
- Provide accurate information using the tools available to you
- Support the customer with technical information and product specifications.
- Be friendly, patient, and understanding with customers
- Always offer additional help after answering questions
- If you can't help with something, direct customers to the appropriate contact
You have access to the following tools:
1. get_return_policy() - For warranty and return policy questions
2. get_product_info() - To get information about a specific product
3. get_technical_support() - To search the technical support knowledge base
Always use the appropriate tool to get accurate, up-to-date information."""
def _get_return_policy(product_category: str) -> str:
"""Return policy lookup (mock data)."""
policies = {
"smartphones": {"window": "30 days", "warranty": "1-year manufacturer warranty"},
"laptops": {"window": "30 days", "warranty": "1-year manufacturer warranty, extended options"},
"accessories": {"window": "30 days", "warranty": "90-day manufacturer warranty"},
}
p = policies.get(product_category.lower(), {"window": "30 days", "warranty": "Standard warranty"})
return f"Return Policy - {product_category}: Window: {p['window']}, Warranty: {p['warranty']}"
def _get_product_info(product_type: str) -> str:
"""Product info lookup (mock data)."""
products = {
"laptops": "Intel/AMD, 8-64GB RAM, SSD, Thunderbolt, 1yr warranty",
"smartphones": "5G, 128GB-1TB, water resistant, wireless charging, 1yr warranty",
"headphones": "BT 5.0+, ANC, 20-40hr battery, 1yr warranty",
"monitors": "4K/1440p, 60-240Hz, HDR, 3yr warranty",
}
info = products.get(product_type.lower(), "Contact technical support for details.")
return f"Product Info - {product_type}: {info}"
def _get_technical_support(issue_description: str) -> str:
"""KB retrieval via Bedrock Knowledge Base."""
try:
kb_id = ssm_client.get_parameter(
Name=f"/{ACCOUNT_ID}-{REGION}/kb/knowledge-base-id"
)["Parameter"]["Value"]
bedrock_agent_runtime = boto3.client("bedrock-agent-runtime", region_name=REGION)
response = bedrock_agent_runtime.retrieve(
knowledgeBaseId=kb_id,
retrievalQuery={"text": issue_description},
retrievalConfiguration={"vectorSearchConfiguration": {"numberOfResults": 3}},
)
results = response.get("retrievalResults", [])
texts = [r.get("content", {}).get("text", "") for r in results if r.get("score", 0) >= 0.4]
return "\n\n".join(texts) if texts else "No relevant documentation found."
except Exception as e:
return f"KB lookup error: {e}"
# ---------------------------------------------------------------------------
# Step 2 — Create AgentCore Memory (Lab 2 equivalent)
# ---------------------------------------------------------------------------
def create_eval_memory():
"""Create or retrieve the eval memory resource."""
from bedrock_agentcore.memory import MemoryClient
from bedrock_agentcore.memory.constants import StrategyType
client = MemoryClient(region_name=REGION)
# Check if already exists
try:
memory_id = get_ssm(f"{EVAL_SSM_PREFIX}/memory_id")
client.gmcp_client.get_memory(memoryId=memory_id)
print(f"✅ Eval memory already exists: {memory_id}")
return memory_id, client
except Exception:
pass
strategies = [
{
StrategyType.USER_PREFERENCE.value: {
"name": "EvalCustomerPreferences",
"description": "Captures eval customer preferences",
"namespaces": ["eval/customer/{actorId}/preferences"],
}
},
{
StrategyType.SEMANTIC.value: {
"name": "EvalCustomerSemantic",
"description": "Stores eval facts from conversations",
"namespaces": ["eval/customer/{actorId}/semantic"],
}
},
]
print("⏳ Creating eval memory resource (this may take a couple of minutes)...")
response = client.create_memory_and_wait(
name=EVAL_MEMORY_NAME,
description="Eval support agent memory — isolated from workshop labs",
strategies=strategies,
event_expiry_days=30,
)
memory_id = response["id"]
put_ssm(f"{EVAL_SSM_PREFIX}/memory_id", memory_id)
print(f"✅ Eval memory created: {memory_id}")
return memory_id, client
# ---------------------------------------------------------------------------
# Step 3 — Create AgentCore Gateway (Lab 3 equivalent)
# ---------------------------------------------------------------------------
def create_eval_gateway():
"""Create or retrieve the eval gateway."""
# Reuse the existing Cognito pool (read-only, same account)
try:
from lab_helpers.utils import get_or_create_cognito_pool
cognito_config = get_or_create_cognito_pool(refresh_token=True)
except ImportError:
# Fallback: read from SSM if lab_helpers not on path
cognito_config = {
"client_id": get_ssm("/app/customersupport/agentcore/client_id"),
"discovery_url": get_ssm("/app/customersupport/agentcore/discovery_url"),
"bearer_token": get_ssm("/app/customersupport/agentcore/bearer_token"),
}
gateway_client = boto3.client("bedrock-agentcore-control", region_name=REGION)
# Check if already exists
try:
gw_id = get_ssm(f"{EVAL_SSM_PREFIX}/gateway_id")
gw = gateway_client.get_gateway(gatewayIdentifier=gw_id)
print(f"✅ Eval gateway already exists: {gw_id}")
return {
"id": gw_id,
"gateway_url": gw["gatewayUrl"],
"gateway_arn": gw["gatewayArn"],
}, cognito_config
except Exception:
pass
# Get the gateway IAM role (reuse the one from workshop prereqs)
gateway_role_arn = get_ssm("/app/customersupport/agentcore/gateway_iam_role")
auth_config = {
"customJWTAuthorizer": {
"allowedClients": [cognito_config["client_id"]],
"discoveryUrl": cognito_config["discovery_url"],
}
}
print(f"⏳ Creating eval gateway '{EVAL_GATEWAY_NAME}'...")
create_response = gateway_client.create_gateway(
name=EVAL_GATEWAY_NAME,
roleArn=gateway_role_arn,
protocolType="MCP",
authorizerType="CUSTOM_JWT",
authorizerConfiguration=auth_config,
)
gw_id = create_response["gatewayId"]
# Wait for gateway to be ready
while True:
gw = gateway_client.get_gateway(gatewayIdentifier=gw_id)
status = gw.get("status", "CREATING")
if status == "ACTIVE":
break
if status in ("CREATE_FAILED", "FAILED"):
raise RuntimeError(f"Gateway creation failed: {status}")
print(f" Gateway status: {status}, waiting...")
time.sleep(10)
put_ssm(f"{EVAL_SSM_PREFIX}/gateway_id", gw_id)
print(f"✅ Eval gateway created: {gw_id}")
# Add Lambda target (reuse the same Lambda from workshop prereqs)
lambda_arn = get_ssm("/app/customersupport/agentcore/lambda_arn")
api_spec_path = os.path.join(
os.path.dirname(__file__), "..", "..", "..", "prerequisite", "lambda", "api_spec.json"
)
with open(api_spec_path) as f:
api_spec = json.load(f)
target_config = {
"mcp": {
"lambda": {
"lambdaArn": lambda_arn,
"toolSchema": {"inlinePayload": api_spec},
}
}
}
print("⏳ Adding Lambda target to eval gateway...")
gateway_client.create_gateway_target(
gatewayIdentifier=gw_id,
name="eval-lambda-target",
targetConfiguration=target_config,
)
# Wait for target to be ready
time.sleep(5)
targets = gateway_client.list_gateway_targets(gatewayIdentifier=gw_id)
for t in targets.get("items", []):
while True:
tgt = gateway_client.get_gateway_target(
gatewayIdentifier=gw_id, targetId=t["targetId"]
)
ts = tgt.get("status", "CREATING")
if ts == "ACTIVE":
break
if ts in ("CREATE_FAILED", "FAILED"):
raise RuntimeError(f"Target creation failed: {ts}")
print(f" Target status: {ts}, waiting...")
time.sleep(10)
print("✅ Lambda target added to eval gateway")
return {
"id": gw_id,
"gateway_url": gw["gatewayUrl"],
"gateway_arn": gw["gatewayArn"],
}, cognito_config
# ---------------------------------------------------------------------------
# Step 4 — Create IAM Role + Deploy to AgentCore Runtime (Lab 4 equivalent)
# ---------------------------------------------------------------------------
def create_eval_execution_role():
"""Create the IAM execution role for the eval runtime agent."""
iam = boto3.client("iam")
role_name = EVAL_ROLE_NAME_TEMPLATE.format(region=REGION)
policy_name = EVAL_POLICY_NAME_TEMPLATE.format(region=REGION)
trust_policy = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {"Service": "bedrock-agentcore.amazonaws.com"},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {"aws:SourceAccount": ACCOUNT_ID},
"ArnLike": {
"aws:SourceArn": f"arn:aws:bedrock-agentcore:{REGION}:{ACCOUNT_ID}:*"
},
},
}
],
}
policy_document = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["ecr:BatchGetImage", "ecr:GetDownloadUrlForLayer"],
"Resource": [f"arn:aws:ecr:{REGION}:{ACCOUNT_ID}:repository/*"],
},
{
"Effect": "Allow",
"Action": ["ecr:GetAuthorizationToken"],
"Resource": "*",
},
{
"Effect": "Allow",
"Action": ["logs:DescribeLogStreams", "logs:CreateLogGroup"],
"Resource": [f"arn:aws:logs:{REGION}:{ACCOUNT_ID}:log-group:/aws/bedrock-agentcore/runtimes/*"],
},
{
"Effect": "Allow",
"Action": ["logs:DescribeLogGroups"],
"Resource": [f"arn:aws:logs:{REGION}:{ACCOUNT_ID}:log-group:*"],
},
{
"Effect": "Allow",
"Action": ["logs:CreateLogStream", "logs:PutLogEvents"],
"Resource": [f"arn:aws:logs:{REGION}:{ACCOUNT_ID}:log-group:/aws/bedrock-agentcore/runtimes/*:log-stream:*"],
},
{
"Effect": "Allow",
"Action": [
"xray:PutTraceSegments", "xray:PutTelemetryRecords",
"xray:GetSamplingRules", "xray:GetSamplingTargets",
],
"Resource": ["*"],
},
{
"Effect": "Allow",
"Action": "cloudwatch:PutMetricData",
"Resource": "*",
"Condition": {"StringEquals": {"cloudwatch:namespace": "bedrock-agentcore"}},
},
{
"Effect": "Allow",
"Action": [
"bedrock-agentcore:GetWorkloadAccessToken",
"bedrock-agentcore:GetWorkloadAccessTokenForJWT",
"bedrock-agentcore:GetWorkloadAccessTokenForUserId",
],
"Resource": [
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/{EVAL_AGENT_NAME}-*",
],
},
{
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream",
"bedrock:ApplyGuardrail", "bedrock:Retrieve",
],
"Resource": [
"arn:aws:bedrock:*::foundation-model/*",
f"arn:aws:bedrock:{REGION}:{ACCOUNT_ID}:*",
],
},
{
"Effect": "Allow",
"Action": [
"bedrock-agentcore:CreateEvent", "bedrock-agentcore:ListEvents",
"bedrock-agentcore:GetMemoryRecord", "bedrock-agentcore:GetMemory",
"bedrock-agentcore:RetrieveMemoryRecords", "bedrock-agentcore:ListMemoryRecords",
],
"Resource": [f"arn:aws:bedrock-agentcore:{REGION}:{ACCOUNT_ID}:*"],
},
{
"Effect": "Allow",
"Action": ["ssm:GetParameter"],
"Resource": [f"arn:aws:ssm:{REGION}:{ACCOUNT_ID}:parameter/*"],
},
{
"Effect": "Allow",
"Action": ["bedrock-agentcore:GetGateway", "bedrock-agentcore:InvokeGateway"],
"Resource": [f"arn:aws:bedrock-agentcore:{REGION}:{ACCOUNT_ID}:gateway/*"],
},
],
}
try:
existing = iam.get_role(RoleName=role_name)
print(f"✅ Eval IAM role already exists: {role_name}")
role_arn = existing["Role"]["Arn"]
except iam.exceptions.NoSuchEntityException:
resp = iam.create_role(
RoleName=role_name,
AssumeRolePolicyDocument=json.dumps(trust_policy),
Description="IAM role for eval AgentCore runtime agent",
)
role_arn = resp["Role"]["Arn"]
print(f"✅ Created eval IAM role: {role_name}")
policy_arn = f"arn:aws:iam::{ACCOUNT_ID}:policy/{policy_name}"
try:
iam.get_policy(PolicyArn=policy_arn)
except iam.exceptions.NoSuchEntityException:
iam.create_policy(
PolicyName=policy_name,
PolicyDocument=json.dumps(policy_document),
Description="Policy for eval AgentCore runtime agent",
)
print(f"✅ Created eval IAM policy: {policy_name}")
try:
iam.attach_role_policy(RoleName=role_name, PolicyArn=policy_arn)
except Exception:
pass
put_ssm(f"{EVAL_SSM_PREFIX}/runtime_execution_role_arn", role_arn)
return role_arn
def write_eval_runtime_entrypoint(memory_id, gateway_id):
"""Write the runtime entrypoint file for the eval agent (Strands-based)."""
entrypoint_path = os.path.join(
os.path.dirname(__file__), "eval_runtime_entrypoint.py"
)
code = f'''#!/usr/bin/env python3
"""Eval agent runtime entrypoint — Strands-based, isolated from workshop labs."""
import os
import boto3
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from bedrock_agentcore.memory import MemoryClient
from strands import Agent
from strands.models import BedrockModel
from strands.tools import tool
from strands.tools.mcp import MCPClient
from mcp.client.streamable_http import streamablehttp_client
REGION = boto3.session.Session().region_name
ACTOR_ID = "eval_customer_001"
memory_id = os.environ.get("EVAL_MEMORY_ID", "{memory_id}")
gateway_id = os.environ.get("EVAL_GATEWAY_ID", "{gateway_id}")
memory_client = MemoryClient(region_name=REGION)
model = BedrockModel(model_id="global.anthropic.claude-haiku-4-5-20251001-v1:0")
ssm = boto3.client("ssm", region_name=REGION)
SYSTEM_PROMPT = """You are a helpful customer support assistant for an electronics e-commerce company.
Use the tools available to provide accurate information about products, returns, warranties, and technical support."""
@tool
def get_return_policy(product_category: str) -> str:
"""Get return policy for a product category."""
policies = {{
"smartphones": "30-day return, 1yr warranty, free return shipping",
"laptops": "30-day return, 1yr warranty + extended options, free return shipping",
"accessories": "30-day return, 90-day warranty",
}}
return policies.get(product_category.lower(), "30-day return, standard warranty")
@tool
def get_product_info(product_type: str) -> str:
"""Get product specifications."""
products = {{
"laptops": "Intel/AMD, 8-64GB RAM, SSD, Thunderbolt, 1yr warranty",
"smartphones": "5G, 128GB-1TB, water resistant, wireless charging",
"headphones": "BT 5.0+, ANC, 20-40hr battery",
"monitors": "4K/1440p, 60-240Hz, HDR, 3yr warranty",
}}
return products.get(product_type.lower(), "Contact support for details.")
@tool
def get_technical_support(issue_description: str) -> str:
"""Search the knowledge base for technical support."""
try:
account_id = boto3.client("sts").get_caller_identity()["Account"]
kb_id = ssm.get_parameter(Name=f"/{{account_id}}-{{REGION}}/kb/knowledge-base-id")["Parameter"]["Value"]
bedrock_rt = boto3.client("bedrock-agent-runtime", region_name=REGION)
resp = bedrock_rt.retrieve(
knowledgeBaseId=kb_id,
retrievalQuery={{"text": issue_description}},
retrievalConfiguration={{"vectorSearchConfiguration": {{"numberOfResults": 3}}}},
)
results = resp.get("retrievalResults", [])
texts = [r.get("content", {{}}).get("text", "") for r in results if r.get("score", 0) >= 0.4]
return "\\n".join(texts) if texts else "No relevant docs found."
except Exception as e:
return f"KB error: {{e}}"
app = BedrockAgentCoreApp()
@app.entrypoint
async def invoke(payload, context=None):
"""Eval agent entrypoint."""
user_input = payload.get("prompt", "")
actor_id = payload.get("actor_id", ACTOR_ID)
request_headers = context.request_headers or {{}}
auth_header = request_headers.get("Authorization", "")
gateway_client = boto3.client("bedrock-agentcore-control", region_name=REGION)
gw = gateway_client.get_gateway(gatewayIdentifier=gateway_id)
gateway_url = gw["gatewayUrl"]
if not (gateway_url and auth_header):
return "Error: Missing gateway URL or authorization header"
try:
mcp_client = MCPClient(
lambda: streamablehttp_client(
url=gateway_url, headers={{"Authorization": auth_header}}
)
)
with mcp_client:
tools = [get_return_policy, get_product_info, get_technical_support] + mcp_client.list_tools_sync()
# Retrieve memory context
all_context = []
for ns_type, ns in {{"preferences": f"eval/customer/{{actor_id}}/preferences/",
"semantic": f"eval/customer/{{actor_id}}/semantic/"}}.items():
try:
memories = memory_client.retrieve_memories(
memory_id=memory_id, namespace=ns, query=user_input, top_k=3
)
for m in memories:
if isinstance(m, dict):
txt = m.get("content", {{}}).get("text", "").strip()
if txt:
all_context.append(f"[{{ns_type.upper()}}] {{txt}}")
except Exception:
pass
enriched = f"Customer Context:\\n" + "\\n".join(all_context) + f"\\n\\n{{user_input}}" if all_context else user_input
agent = Agent(model=model, tools=tools, system_prompt=SYSTEM_PROMPT)
response = agent(enriched)
result_text = response.message["content"][0]["text"]
# Save to memory
try:
memory_client.create_event(
memory_id=memory_id, actor_id=actor_id,
session_id=str(context.session_id),
messages=[(user_input, "USER"), (result_text, "ASSISTANT")],
)
except Exception:
pass
return result_text
except Exception as e:
return f"Error: {{e}}"
if __name__ == "__main__":
app.run()
'''
with open(entrypoint_path, "w") as f:
f.write(code)
print(f"✅ Wrote eval runtime entrypoint: {entrypoint_path}")
return entrypoint_path
def deploy_eval_runtime(memory_id, gateway_id):
"""Deploy the eval agent to AgentCore Runtime."""
from bedrock_agentcore_starter_toolkit import Runtime
execution_role_arn = create_eval_execution_role()
entrypoint_path = write_eval_runtime_entrypoint(memory_id, gateway_id)
# Requirements file for the runtime container
req_path = os.path.join(os.path.dirname(__file__), "requirements.txt")
agentcore_runtime = Runtime()
# Reuse existing Cognito for JWT auth
client_id = get_ssm("/app/customersupport/agentcore/client_id")
discovery_url = get_ssm("/app/customersupport/agentcore/discovery_url")
print("⏳ Configuring eval runtime deployment...")
agentcore_runtime.configure(
entrypoint=entrypoint_path,
execution_role=execution_role_arn,
auto_create_ecr=True,
requirements_file=req_path,
region=REGION,
agent_name=EVAL_AGENT_NAME,
authorizer_configuration={
"customJWTAuthorizer": {
"allowedClients": [client_id],
"discoveryUrl": discovery_url,
}
},
)
print("⏳ Launching eval runtime (building container + deploying)...")
launch_result = agentcore_runtime.launch(
env_vars={
"EVAL_MEMORY_ID": memory_id,
"EVAL_GATEWAY_ID": gateway_id,
}
)
agent_arn = launch_result.agent_arn
put_ssm(f"{EVAL_SSM_PREFIX}/runtime_arn", agent_arn)
print(f"✅ Eval runtime launched: {agent_arn}")
# Wait for READY
while True:
status_response = agentcore_runtime.status()
status = status_response.endpoint["status"]
if status == "READY":
break
if status in ("CREATE_FAILED", "DELETE_FAILED", "UPDATE_FAILED"):
raise RuntimeError(f"Eval runtime deployment failed: {status}")
print(f" Runtime status: {status}, waiting...")
time.sleep(15)
print("✅ Eval runtime is READY")
return agentcore_runtime, agent_arn
# ---------------------------------------------------------------------------
# Test — single invocation
# ---------------------------------------------------------------------------
def test_single_invocation(agentcore_runtime=None):
"""Test the eval agent with a single invocation."""
if agentcore_runtime is None:
from bedrock_agentcore_starter_toolkit import Runtime
agentcore_runtime = Runtime()
# Re-configure to point at existing eval agent
execution_role_arn = get_ssm(f"{EVAL_SSM_PREFIX}/runtime_execution_role_arn")
client_id = get_ssm("/app/customersupport/agentcore/client_id")
discovery_url = get_ssm("/app/customersupport/agentcore/discovery_url")
entrypoint_path = os.path.join(os.path.dirname(__file__), "eval_runtime_entrypoint.py")
req_path = os.path.join(os.path.dirname(__file__), "requirements.txt")
agentcore_runtime.configure(
entrypoint=entrypoint_path,
execution_role=execution_role_arn,
auto_create_ecr=True,
requirements_file=req_path,
region=REGION,
agent_name=EVAL_AGENT_NAME,
authorizer_configuration={
"customJWTAuthorizer": {
"allowedClients": [client_id],
"discoveryUrl": discovery_url,
}
},
)
# Get bearer token
try:
from lab_helpers.utils import get_or_create_cognito_pool
cognito = get_or_create_cognito_pool(refresh_token=True)
bearer_token = cognito["bearer_token"]
except ImportError:
bearer_token = get_ssm("/app/customersupport/agentcore/bearer_token")
session_id = str(uuid.uuid4())
test_query = "What is the return policy for laptops?"
print(f"\n🧪 Testing eval agent with: '{test_query}'")
response = agentcore_runtime.invoke(
{"prompt": test_query, "actor_id": EVAL_ACTOR_ID},
bearer_token=bearer_token,
session_id=session_id,
)
result = response.get("response", response)
print(f"✅ Response: {result}")
return True
# ---------------------------------------------------------------------------
# Generate data — invoke the agent for 30 minutes with varied prompts
# ---------------------------------------------------------------------------
EVAL_PROMPTS = [
"What is the return policy for smartphones?",
"Tell me about laptop specifications",
"I need help with my headphones not connecting via Bluetooth",
"What monitors do you recommend for gaming?",
"My laptop screen is flickering, what should I do?",
"Can I return an opened accessory?",
"What's the warranty on a smartphone?",
"How do I check my warranty status? Serial number ABC12345",
"Search the web for latest laptop reviews 2025",
"I bought a monitor last week and it has dead pixels",
"What are the specs for your headphones?",
"How long is the return window for laptops?",
"My phone battery drains too fast, any tips?",
"Do you offer extended warranty for monitors?",
"I need technical support for installing RAM on my laptop",
"What's the difference between your smartphone models?",
"Can I get a refund if my accessory is defective?",
"Tell me about your noise cancelling headphones",
"How do I set up dual monitors?",
"What's the process for returning a laptop?",
"I want to check warranty for serial MNO33333333",
"Search the web for how to fix overheating laptop",
"What gaming accessories do you sell?",
"My headphones have low volume on one side",
"Do laptops come with pre-installed software?",
"What's the best monitor for photo editing?",
"How do I transfer data to my new smartphone?",
"Is there a student discount on laptops?",
"My monitor won't turn on after a power outage",
"What USB-C accessories are compatible with your laptops?",
]
def generate_eval_data(duration_minutes=30, agentcore_runtime=None):
"""Invoke the eval agent repeatedly for the specified duration."""
if agentcore_runtime is None:
from bedrock_agentcore_starter_toolkit import Runtime
agentcore_runtime = Runtime()
execution_role_arn = get_ssm(f"{EVAL_SSM_PREFIX}/runtime_execution_role_arn")
client_id = get_ssm("/app/customersupport/agentcore/client_id")
discovery_url = get_ssm("/app/customersupport/agentcore/discovery_url")
entrypoint_path = os.path.join(os.path.dirname(__file__), "eval_runtime_entrypoint.py")
req_path = os.path.join(os.path.dirname(__file__), "requirements.txt")
agentcore_runtime.configure(
entrypoint=entrypoint_path,
execution_role=execution_role_arn,
auto_create_ecr=True,
requirements_file=req_path,
region=REGION,
agent_name=EVAL_AGENT_NAME,
authorizer_configuration={
"customJWTAuthorizer": {
"allowedClients": [client_id],
"discoveryUrl": discovery_url,
}
},
)
try:
from lab_helpers.utils import get_or_create_cognito_pool
cognito = get_or_create_cognito_pool(refresh_token=True)
bearer_token = cognito["bearer_token"]
except ImportError:
bearer_token = get_ssm("/app/customersupport/agentcore/bearer_token")
end_time = time.time() + (duration_minutes * 60)
invocation_count = 0
error_count = 0
print(f"\n🚀 Generating eval data for {duration_minutes} minutes...")
print(f" Start: {time.strftime('%H:%M:%S')}")
print(f" End: {time.strftime('%H:%M:%S', time.localtime(end_time))}\n")
while time.time() < end_time:
prompt = random.choice(EVAL_PROMPTS)
session_id = str(uuid.uuid4())
invocation_count += 1
try:
response = agentcore_runtime.invoke(
{"prompt": prompt, "actor_id": EVAL_ACTOR_ID},
bearer_token=bearer_token,
session_id=session_id,
)
result = response.get("response", str(response))
preview = result[:80].replace("\n", " ") if result else "(empty)"
print(f" [{invocation_count}] ✅ {prompt[:50]}... → {preview}...")
except Exception as e:
error_count += 1
print(f" [{invocation_count}] ❌ {prompt[:50]}... → Error: {e}")
# Refresh token if auth error
if "401" in str(e) or "unauthorized" in str(e).lower():
try:
from lab_helpers.utils import get_or_create_cognito_pool
cognito = get_or_create_cognito_pool(refresh_token=True)
bearer_token = cognito["bearer_token"]
print(" 🔄 Refreshed bearer token")
except Exception:
pass
# Small delay between invocations to avoid throttling
time.sleep(random.uniform(2, 5))
print(f"\n📊 Data generation complete:")
print(f" Total invocations: {invocation_count}")
print(f" Errors: {error_count}")
print(f" Success rate: {((invocation_count - error_count) / max(invocation_count, 1)) * 100:.1f}%")
# ---------------------------------------------------------------------------
# Cleanup — tear down all eval-specific resources
# ---------------------------------------------------------------------------
def cleanup_eval_resources():
"""Delete all eval-specific resources to avoid lingering costs."""
print("\n🧹 Cleaning up eval resources...\n")
iam = boto3.client("iam")
gateway_client = boto3.client("bedrock-agentcore-control", region_name=REGION)
# 1. Delete runtime
try:
runtime_arn = get_ssm(f"{EVAL_SSM_PREFIX}/runtime_arn")
runtime_id = runtime_arn.split(":")[-1].split("/")[-1]
control = boto3.client("bedrock-agentcore-control", region_name=REGION)
control.delete_agent_runtime(agentRuntimeId=runtime_id)
print(f"✅ Deleted eval runtime: {runtime_id}")
delete_ssm(f"{EVAL_SSM_PREFIX}/runtime_arn")
except Exception as e:
print(f"⚠️ Runtime cleanup: {e}")
# 2. Delete gateway targets then gateway
try:
gw_id = get_ssm(f"{EVAL_SSM_PREFIX}/gateway_id")
targets = gateway_client.list_gateway_targets(gatewayIdentifier=gw_id)
for t in targets.get("items", []):
gateway_client.delete_gateway_target(
gatewayIdentifier=gw_id, targetId=t["targetId"]
)
print(f" Deleted gateway target: {t['targetId']}")
# Wait for targets to be deleted
time.sleep(10)
gateway_client.delete_gateway(gatewayIdentifier=gw_id)
print(f"✅ Deleted eval gateway: {gw_id}")
delete_ssm(f"{EVAL_SSM_PREFIX}/gateway_id")
except Exception as e:
print(f"⚠️ Gateway cleanup: {e}")
# 3. Delete memory
try:
memory_id = get_ssm(f"{EVAL_SSM_PREFIX}/memory_id")
from bedrock_agentcore.memory import MemoryClient
mc = MemoryClient(region_name=REGION)
mc.delete_memory(memory_id=memory_id)
print(f"✅ Deleted eval memory: {memory_id}")
delete_ssm(f"{EVAL_SSM_PREFIX}/memory_id")
except Exception as e:
print(f"⚠️ Memory cleanup: {e}")
# 4. Delete IAM role + policy
role_name = EVAL_ROLE_NAME_TEMPLATE.format(region=REGION)
policy_name = EVAL_POLICY_NAME_TEMPLATE.format(region=REGION)
policy_arn = f"arn:aws:iam::{ACCOUNT_ID}:policy/{policy_name}"
try:
iam.detach_role_policy(RoleName=role_name, PolicyArn=policy_arn)
iam.delete_policy(PolicyArn=policy_arn)
iam.delete_role(RoleName=role_name)
print(f"✅ Deleted eval IAM role and policy")
eval_ssm = f"{EVAL_SSM_PREFIX}/runtime_execution_role_arn"
delete_ssm(eval_ssm)
except Exception as e:
print(f"⚠️ IAM cleanup: {e}")
# 5. Clean up generated entrypoint file
entrypoint_path = os.path.join(os.path.dirname(__file__), "eval_runtime_entrypoint.py")
if os.path.exists(entrypoint_path):
os.remove(entrypoint_path)
print("✅ Removed eval_runtime_entrypoint.py")
print("\n🧹 Eval cleanup complete!")
# ---------------------------------------------------------------------------
# Main — CLI interface
# ---------------------------------------------------------------------------
def setup_all():
"""Run the full setup: memory → gateway → deploy runtime."""
print("=" * 60)
print(" Lab 5 Evaluation Helper — Full Setup (Strands Agent)")
print("=" * 60)
# Step 1: Agent tools are defined inline (no separate setup needed)
print("\n📦 Step 1: Agent tools ready (Strands @tool decorators)")
# Step 2: Memory
print("\n📦 Step 2: Creating eval memory...")
memory_id, _ = create_eval_memory()
# Step 3: Gateway
print("\n📦 Step 3: Creating eval gateway...")
gateway_info, cognito_config = create_eval_gateway()
# Step 4: Deploy runtime
print("\n📦 Step 4: Deploying eval agent to AgentCore Runtime...")
agentcore_runtime, agent_arn = deploy_eval_runtime(memory_id, gateway_info["id"])
print("\n" + "=" * 60)
print(" ✅ Setup complete!")
print(f" Memory ID: {memory_id}")
print(f" Gateway ID: {gateway_info['id']}")
print(f" Runtime ARN: {agent_arn}")
print("=" * 60)
return agentcore_runtime
def main():
parser = argparse.ArgumentParser(
description="Lab 5 Evaluation Helper — Strands agent setup for eval data generation"
)
parser.add_argument(
"action",
choices=["setup", "test", "generate", "cleanup"],
help="setup: create all resources | test: single invocation | generate: 30min data gen | cleanup: tear down",
)
parser.add_argument(
"--duration", type=int, default=30,
help="Duration in minutes for data generation (default: 30)",
)
args = parser.parse_args()
if args.action == "setup":
setup_all()
elif args.action == "test":
test_single_invocation()
elif args.action == "generate":
generate_eval_data(duration_minutes=args.duration)
elif args.action == "cleanup":
cleanup_eval_resources()
if __name__ == "__main__":
main()
@@ -0,0 +1,7 @@
boto3>=1.42.3
botocore>=1.42.3
bedrock-agentcore==1.1.1
bedrock-agentcore-starter-toolkit==0.2.3
strands-agents
strands-agents-tools
ddgs
@@ -0,0 +1,387 @@
import json
import time
import uuid
import urllib.parse
from typing import Any, Optional
import requests
import streamlit as st
from chat_utils import (
make_urls_clickable,
create_safe_markdown_text,
get_aws_region,
get_ssm_parameter,
)
def invoke_endpoint_streaming(
agent_arn: str,
payload,
session_id: str,
bearer_token: str,
endpoint_name: str = "DEFAULT",
):
"""Invoke agent endpoint and yield streaming response chunks."""
# Escape agent ARN for URL
escaped_arn = urllib.parse.quote(agent_arn, safe="")
# Build URL
# url = f"{self.dp_endpoint}/runtimes/{escaped_arn}/invocations"
url = f"https://bedrock-agentcore.{st.session_state['region']}.amazonaws.com/runtimes/{escaped_arn}/invocations"
# Headers
headers = {
"Authorization": f"Bearer {bearer_token}",
"Content-Type": "application/json",
"X-Amzn-Bedrock-AgentCore-Runtime-Session-Id": session_id,
}
# Parse the payload string back to JSON object to send properly
try:
body = json.loads(payload) if isinstance(payload, str) else payload
except json.JSONDecodeError:
# Fallback for non-JSON strings - wrap in payload object
print("Failed to parse payload as JSON, wrapping in payload object")
body = {"payload": payload}
try:
# Make streaming request
response = requests.post(
url,
params={"qualifier": endpoint_name},
headers=headers,
json=body,
timeout=100,
stream=True,
)
response.raise_for_status()
# Check if response is streaming
if "text/event-stream" in response.headers.get("content-type", ""):
# Handle streaming response
for line in response.iter_lines(chunk_size=1, decode_unicode=True):
if line and line.startswith("data: "):
chunk = line[6:] # Remove "data: " prefix
if chunk.strip(): # Only yield non-empty chunks
yield chunk
else:
# Non-streaming response, yield entire content
if response.content:
yield response.text
except requests.exceptions.RequestException as e:
print("Failed to invoke agent endpoint: %s", str(e))
raise
class ChatManager:
def format_response_text(self, text):
"""Format response text by unescaping quotes and newlines"""
if not text:
return text
# Remove outer quotes if present
if text.startswith('"') and text.endswith('"'):
text = text[1:-1]
# Unescape common escape sequences
text = text.replace("\\n", "\n")
text = text.replace('\\"', '"')
text = text.replace("\\t", "\t")
text = text.replace("\\r", "\r")
text = text.replace("\\\\", "\\")
return text
def __init__(self, agent_name: str = "default"):
self.auth_url_matching = ".amazonaws.com/identities/oauth2/authorize"
self.agent_name = agent_name
self._init_session_state()
def _init_session_state(self):
"""Initialize session state variables"""
if "session_id" not in st.session_state:
st.session_state["session_id"] = str(uuid.uuid4())
if "agent_arn" not in st.session_state:
agent_arn = get_ssm_parameter("/app/customersupport/agentcore/runtime_arn")
st.session_state["agent_arn"] = agent_arn
if "region" not in st.session_state:
st.session_state["region"] = get_aws_region()
if "messages" not in st.session_state:
st.session_state["messages"] = []
if "pending_assistant" not in st.session_state:
st.session_state["pending_assistant"] = False
def invoke_endpoint_nostreaming(
self,
agent_arn: str,
payload,
session_id: str,
bearer_token: Optional[str],
endpoint_name: str = "DEFAULT",
) -> Any:
"""Invoke agent endpoint using HTTP request with bearer token."""
escaped_arn = urllib.parse.quote(agent_arn, safe="")
url = f"https://bedrock-agentcore.{st.session_state['region']}.amazonaws.com/runtimes/{escaped_arn}/invocations"
headers = {
"Authorization": f"Bearer {bearer_token}",
"Content-Type": "application/json",
"X-Amzn-Bedrock-AgentCore-Runtime-Session-Id": session_id,
}
try:
body = json.loads(payload) if isinstance(payload, str) else payload
except json.JSONDecodeError:
body = {"payload": payload}
try:
response = requests.post(
url,
params={"qualifier": endpoint_name},
headers=headers,
json=body,
timeout=100,
)
return response
except requests.exceptions.RequestException as e:
print("Failed to invoke agent endpoint: %s", str(e))
raise
return None
def invoke_endpoint(
self,
agent_arn: str,
payload,
session_id: str,
bearer_token: Optional[str],
endpoint_name: str = "DEFAULT",
) -> Any:
"""Invoke agent endpoint using HTTP request with bearer token."""
escaped_arn = urllib.parse.quote(agent_arn, safe="")
url = f"https://bedrock-agentcore.{st.session_state['region']}.amazonaws.com/runtimes/{escaped_arn}/invocations"
headers = {
"Authorization": f"Bearer {bearer_token}",
"Content-Type": "application/json",
"X-Amzn-Bedrock-AgentCore-Runtime-Session-Id": session_id,
}
try:
body = json.loads(payload) if isinstance(payload, str) else payload
except json.JSONDecodeError:
body = {"payload": payload}
try:
response = requests.post(
url,
params={"qualifier": endpoint_name},
headers=headers,
json=body,
timeout=100,
stream=True,
)
last_data = False
for line in response.iter_lines(chunk_size=1):
if line:
line = line.decode("utf-8")
if line.startswith("data: "):
last_data = True
line = line[6:]
line = line.replace('"', "")
yield line
elif line:
line = line.replace('"', "")
if last_data:
yield "\n" + line
last_data = False
except requests.exceptions.RequestException as e:
print("Failed to invoke agent endpoint: %s", str(e))
raise
def display_chat_history(self):
"""Display chat messages from history"""
messages_to_show = st.session_state.messages[:]
if (
st.session_state.get("pending_assistant", False)
and messages_to_show
and messages_to_show[-1]["role"] == "user"
):
messages_to_show = messages_to_show[:-1]
for message in messages_to_show:
bubble_class = (
"user-bubble" if message["role"] == "user" else "assistant-bubble"
)
emoji = "🧑‍💻" if message["role"] == "user" else "🤖"
with st.chat_message(message["role"]):
if message["role"] == "assistant" and "elapsed" in message:
clickable_content = make_urls_clickable(message["content"])
create_safe_markdown_text(
f'<div class="{bubble_class}">{emoji} {clickable_content}<br><span style="font-size:0.9em;color:#888;">⏱️ Response time: {message["elapsed"]:.2f} seconds</span></div>',
st,
)
else:
if message["role"] == "assistant":
clickable_content = make_urls_clickable(message["content"])
create_safe_markdown_text(
f'<div class="{bubble_class}">{emoji} {clickable_content}</div>',
st,
)
else:
create_safe_markdown_text(
f'<span class="{bubble_class}">{emoji} {message["content"]}</span>',
st,
)
def process_user_message(self, prompt: str, actor_id: str, bearer_token: str):
"""Process a user message and get assistant response"""
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
create_safe_markdown_text(
f'<span class="user-bubble">🧑‍💻 {prompt}</span>', st
)
st.session_state["pending_assistant"] = True
with st.chat_message("assistant"):
message_placeholder = st.empty()
start_time = time.time()
create_safe_markdown_text(
'<span class="thinking-bubble">🤖 💭 Customer Support Assistant is thinking...</span>',
message_placeholder,
)
chunk_count = 0
accumulated_response = ""
for chunk in self.invoke_endpoint(
agent_arn=st.session_state["agent_arn"],
payload=json.dumps({"prompt": prompt, "actor_id": actor_id}),
bearer_token=bearer_token,
session_id=st.session_state["session_id"],
):
chunk = str(chunk)
print(f"Chunk: {chunk}")
if chunk.strip():
if self.auth_url_matching in chunk:
accumulated_response = f"Please use {chunk}"
else:
accumulated_response += chunk
chunk_count += 1
if chunk_count % 3 == 0:
accumulated_response += ""
clickable_streaming_text = make_urls_clickable(accumulated_response)
create_safe_markdown_text(
f'<div class="assistant-bubble streaming typing-cursor">🤖 {clickable_streaming_text}</div>',
message_placeholder,
)
if self.auth_url_matching in accumulated_response:
accumulated_response = str()
time.sleep(0.02)
elapsed = time.time() - start_time
formatted_response = self.format_response_text(accumulated_response)
clickable_streaming_text = make_urls_clickable(formatted_response)
create_safe_markdown_text(
f'<div class="assistant-bubble">🤖 {clickable_streaming_text}<br><span style="font-size:0.9em;color:#888;">⏱️ Response time: {elapsed:.2f} seconds</span></div>',
message_placeholder,
)
st.session_state.messages.append(
{
"role": "assistant",
"content": accumulated_response,
"elapsed": elapsed,
}
)
st.session_state["pending_assistant"] = False
def initialize_default_conversation(self, email, actor_id, bearer_token: str):
"""Initialize the conversation with a default message"""
if not st.session_state.messages:
default_prompt = f"Hi my email is {email}"
st.session_state.messages = [{"role": "user", "content": default_prompt}]
with st.chat_message("user"):
create_safe_markdown_text(
f'<span class="user-bubble">🧑‍💻 {default_prompt}</span>', st
)
st.session_state["pending_assistant"] = True
with st.chat_message("assistant"):
message_placeholder = st.empty()
start_time = time.time()
create_safe_markdown_text(
'<span class="thinking-bubble">🤖 💭 Customer Support Assistant is thinking...</span>',
message_placeholder,
)
chunk_count = 0
accumulated_response = ""
for chunk in self.invoke_endpoint(
agent_arn=st.session_state["agent_arn"],
payload=json.dumps(
{
"prompt": default_prompt,
"actor_id": actor_id,
}
),
bearer_token=bearer_token,
session_id=st.session_state["session_id"],
):
chunk = str(chunk)
if chunk.strip():
accumulated_response += chunk
chunk_count += 1
if chunk_count % 3 == 0:
accumulated_response += ""
clickable_streaming_text = make_urls_clickable(
accumulated_response
)
create_safe_markdown_text(
f'<div class="assistant-bubble streaming typing-cursor">🤖 {clickable_streaming_text}</div>',
message_placeholder,
)
time.sleep(0.02)
elapsed = time.time() - start_time
clickable_answer = make_urls_clickable(accumulated_response)
create_safe_markdown_text(
f'<div class="assistant-bubble">🤖 {clickable_answer}<br><span style="font-size:0.9em;color:#888;">⏱️ Response time: {elapsed:.2f} seconds</span></div>',
message_placeholder,
)
st.session_state.messages.append(
{
"role": "assistant",
"content": accumulated_response,
"elapsed": elapsed,
}
)
st.session_state["pending_assistant"] = False
st.rerun()
@@ -0,0 +1,146 @@
import json
import os
import re
from typing import Any, Dict
import boto3
import yaml
def get_ssm_parameter(name: str, with_decryption: bool = True) -> str:
ssm = boto3.client("ssm")
response = ssm.get_parameter(Name=name, WithDecryption=with_decryption)
return response["Parameter"]["Value"]
def put_ssm_parameter(
name: str, value: str, parameter_type: str = "String", with_encryption: bool = False
) -> None:
ssm = boto3.client("ssm")
put_params = {
"Name": name,
"Value": value,
"Type": parameter_type,
"Overwrite": True,
}
if with_encryption:
put_params["Type"] = "SecureString"
ssm.put_parameter(**put_params)
def delete_ssm_parameter(name: str) -> None:
ssm = boto3.client("ssm")
try:
ssm.delete_parameter(Name=name)
except ssm.exceptions.ParameterNotFound:
pass
def load_api_spec(file_path: str) -> list:
with open(file_path, "r") as f:
data = json.load(f)
if not isinstance(data, list):
raise ValueError("Expected a list in the JSON file")
return data
def get_aws_region() -> str:
session = boto3.session.Session()
return session.region_name
def get_aws_account_id() -> str:
sts = boto3.client("sts")
return sts.get_caller_identity()["Account"]
def get_cognito_client_secret() -> str:
client = boto3.client("cognito-idp")
response = client.describe_user_pool_client(
UserPoolId=get_ssm_parameter("/app/customersupport/agentcore/pool_id"),
ClientId=get_ssm_parameter("/app/customersupport/agentcore/client_id"),
)
return response["UserPoolClient"]["ClientSecret"]
def read_config(file_path: str) -> Dict[str, Any]:
"""
Read configuration from a file path. Supports JSON, YAML, and YML formats.
Args:
file_path (str): Path to the configuration file
Returns:
Dict[str, Any]: Configuration data as a dictionary
Raises:
FileNotFoundError: If the file doesn't exist
ValueError: If the file format is not supported or invalid
yaml.YAMLError: If YAML parsing fails
json.JSONDecodeError: If JSON parsing fails
"""
if not os.path.exists(file_path):
raise FileNotFoundError(f"Configuration file not found: {file_path}")
# Get file extension to determine format
_, ext = os.path.splitext(file_path.lower())
try:
with open(file_path, "r", encoding="utf-8") as file:
if ext == ".json":
return json.load(file)
elif ext in [".yaml", ".yml"]:
return yaml.safe_load(file)
else:
# Try to auto-detect format by attempting JSON first, then YAML
content = file.read()
file.seek(0)
# Try JSON first
try:
return json.loads(content)
except json.JSONDecodeError:
# Try YAML
try:
return yaml.safe_load(content)
except yaml.YAMLError:
raise ValueError(
f"Unsupported configuration file format: {ext}. "
f"Supported formats: .json, .yaml, .yml"
)
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in configuration file {file_path}: {e}")
except yaml.YAMLError as e:
raise ValueError(f"Invalid YAML in configuration file {file_path}: {e}")
except Exception as e:
raise ValueError(f"Error reading configuration file {file_path}: {e}")
def make_urls_clickable(text):
"""Convert URLs in text to clickable HTML links."""
url_pattern = r"https?://(?:[-\w.])+(?:\:[0-9]+)?(?:/(?:[\w/_.])*(?:\?(?:[\w&=%.])*)?(?:\#(?:[\w.])*)?)?"
def replace_url(match):
url = match.group(0)
return f'<a href="{url}" target="_blank" style="color:#4fc3f7;text-decoration:underline;">{url}</a>'
return re.sub(url_pattern, replace_url, text)
def create_safe_markdown_text(text, message_placeholder):
"""Create safe markdown text with proper encoding and newline handling"""
# First encode/decode for safety
safe_text = text.encode("utf-16", "surrogatepass").decode("utf-16")
# Convert newlines to HTML breaks for proper rendering
# This handles both actual newlines and any remaining escaped ones
safe_text = safe_text.replace("\n", "<br>")
safe_text = safe_text.replace("\\n", "<br>")
message_placeholder.markdown(safe_text, unsafe_allow_html=True)
@@ -0,0 +1,274 @@
import importlib.util
import json
import os
import sys
import time
import uuid
import streamlit as st
from chat import ChatManager, invoke_endpoint_streaming
from chat_utils import make_urls_clickable
from streamlit_cognito_auth import CognitoAuthenticator
# Load utils from project root using importlib to avoid E402
current_dir = os.path.dirname(os.path.abspath(__file__))
utils_path = os.path.join(current_dir, "..", "utils.py")
spec = importlib.util.spec_from_file_location("utils", utils_path)
utils = importlib.util.module_from_spec(spec)
spec.loader.exec_module(utils)
secret = json.loads(utils.get_customer_support_secret())
authenticator = CognitoAuthenticator(
pool_id=secret["pool_id"],
app_client_id=secret["client_id"],
app_client_secret=secret["client_secret"],
use_cookies=False,
)
is_logged_in = authenticator.login()
if not is_logged_in:
st.stop()
def logout():
print("Logout in example")
authenticator.logout()
CONTEXT_WINDOW = 10 # Number of turns (user+assistant pairs) to include in context
qualifier = "DEFAULT"
def build_context(messages, context_window=CONTEXT_WINDOW):
# Only use the last context_window*2 messages (user+assistant pairs)
history = (
messages[-context_window * 2 :]
if len(messages) > context_window * 2
else messages
)
context = ""
for msg in history:
role = "User" if msg["role"] == "user" else "Assistant"
context += f"{role}: {msg['content']}\n"
return context
def format_response_text(text):
"""Format response text by unescaping quotes and newlines"""
if not text:
return text
# Remove outer quotes if present
if text.startswith('"') and text.endswith('"'):
text = text[1:-1]
# Unescape common escape sequences
text = text.replace('\\"', '"')
text = text.replace("\\n", "\n")
text = text.replace("\\t", "\t")
text = text.replace("\\r", "\r")
return text
with st.sidebar:
st.text(f"Welcome,\n{authenticator.get_username()}")
st.button("Logout", "logout_btn", on_click=logout)
st.title("Customer Support Agent")
chat_manager = ChatManager("default")
if "session_id" not in st.session_state:
st.session_state["session_id"] = uuid.uuidv4()
# Initialize chat history
if "messages" not in st.session_state:
st.session_state.messages = []
# Display chat messages from history on app rerun
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
# Accept user input
if prompt := st.chat_input("What is up?"):
# Display user message in chat message container
with st.chat_message("user"):
st.markdown(prompt)
# Add user message to chat history
st.session_state.messages.append({"role": "user", "content": prompt})
payload = json.dumps(
{"prompt": prompt, "actor_id": st.session_state["auth_username"]}
)
with st.chat_message("assistant"):
message_placeholder = st.empty()
import time
start_time = time.time()
accumulated_response = ""
try:
# Setup streaming client
session_id = st.session_state.get("session_id")
context = build_context(st.session_state.messages, CONTEXT_WINDOW)
payload = json.dumps({"prompt": context})
bearer_token = st.session_state.get("auth_access_token")
# Show initial thinking state with pulsing animation
message_placeholder.markdown(
'<span class="thinking-bubble">🤖 💭 Customer Support Agent is thinking...</span>',
unsafe_allow_html=True,
)
# Stream the response with animations
chunk_count = 0
formatted_response = ""
for chunk in invoke_endpoint_streaming(
agent_arn=st.session_state["agent_arn"],
payload=payload,
session_id=session_id,
bearer_token=bearer_token,
endpoint_name=qualifier,
):
if chunk.strip(): # Only process non-empty chunks
accumulated_response += chunk
chunk_count += 1
# Check if we have a complete response with End marker (quoted version)
if '"End agent execution"' in accumulated_response:
# Show processing state
message_placeholder.markdown(
'<span class="thinking-bubble">🤖 🔄 Processing response...</span>',
unsafe_allow_html=True,
)
# Parse the JSON and extract the formatted response
try:
# Find the JSON part between quoted Begin and End markers
begin_marker = '"Begin agent execution"'
end_marker = '"End agent execution"'
begin_pos = accumulated_response.find(begin_marker)
end_pos = accumulated_response.find(end_marker)
if begin_pos != -1 and end_pos != -1:
# Extract everything between the markers
json_part = accumulated_response[
begin_pos + len(begin_marker) : end_pos
].strip()
# The JSON should start immediately after the Begin marker
json_start = json_part.find('{"role":')
if json_start != -1:
json_str = json_part[json_start:]
# Find the end of the JSON object by counting braces
brace_count = 0
json_end = -1
for i, char in enumerate(json_str):
if char == "{":
brace_count += 1
elif char == "}":
brace_count -= 1
if brace_count == 0:
json_end = i + 1
break
if json_end != -1:
json_str = json_str[:json_end]
print(
f"Extracted JSON: {json_str}"
) # Debug print
response_data = json.loads(json_str)
# Extract text from the JSON structure
if (
"content" in response_data
and len(response_data["content"]) > 0
and "text" in response_data["content"][0]
):
formatted_response = response_data[
"content"
][0]["text"]
print(
f"Extracted text: {formatted_response}"
) # Debug print
except (json.JSONDecodeError, KeyError, IndexError) as e:
print(f"JSON parsing error: {e}")
print(f"Accumulated response: {accumulated_response}")
# Fallback to show full response for debugging
formatted_response = accumulated_response
break
# Display streaming text for non-JSON responses or while accumulating
else:
# Add typing cursor effect during streaming
streaming_text = accumulated_response
if (
chunk_count % 3 == 0
): # Add cursor every few chunks for effect
streaming_text += ""
# Update display with streaming animation (make URLs clickable)
clickable_streaming_text = make_urls_clickable(streaming_text)
message_placeholder.markdown(
f'<div class="assistant-bubble streaming typing-cursor">🤖 {clickable_streaming_text}</div>',
unsafe_allow_html=True,
)
# Small delay to make streaming visible and smooth
time.sleep(0.02)
# Final response with timing (remove streaming classes)
elapsed = time.time() - start_time
answer = (
formatted_response
if formatted_response
else (
accumulated_response
if accumulated_response
else "No response received"
)
)
# Format the response to handle escaped characters
answer = format_response_text(answer)
clickable_answer = make_urls_clickable(answer)
message_placeholder.markdown(
f'<div class="assistant-bubble">🤖 {clickable_answer}<br><span style="font-size:0.9em;color:#888;">⏱️ Response time: {elapsed:.2f} seconds</span></div>',
unsafe_allow_html=True,
)
except Exception as e:
error_msg = f"Sorry, I encountered an error: {str(e)}"
message_placeholder.markdown(
f'<div class="assistant-bubble">🤖 ❌ {error_msg}</div>',
unsafe_allow_html=True,
)
answer = error_msg
elapsed = time.time() - start_time
# Add final response to session state
final_answer = answer if "answer" in locals() else accumulated_response
st.session_state.messages.append(
{"role": "assistant", "content": final_answer, "elapsed": elapsed}
)
st.session_state["pending_assistant"] = False
st.rerun()
accumulated_response = chat_manager.invoke_endpoint_nostreaming(
agent_arn=st.session_state["agent_arn"],
payload=payload,
bearer_token=st.session_state["auth_access_token"],
session_id=st.session_state["session_id"],
)
print(f"Response: {accumulated_response}")
# Add assistant response to chat history
st.session_state.messages.append(
{"role": "assistant", "content": accumulated_response}
)
@@ -0,0 +1,4 @@
streamlit
requests
boto3
streamlit-cognito-auth
@@ -0,0 +1,43 @@
import json
import sys
import boto3
def get_streamlit_url():
try:
# Read the JSON file
with open("/opt/ml/metadata/resource-metadata.json", "r") as file:
data = json.load(file)
domain_id = data["DomainId"]
space_name = data["SpaceName"]
except FileNotFoundError:
print(
"Resource-metadata.json file not found -- running outside SageMaker Studio"
)
domain_id = None
space_name = None
# sys.exit(1)
except json.JSONDecodeError:
print("Error: Invalid JSON format in resource-metadata.json")
sys.exit(1)
except KeyError as e:
print(f"Error: Required key {e} not found in JSON")
sys.exit(1)
# Now you can use domain_id and space_name variables in your code
print(f"Domain ID: {domain_id}")
print(f"Space Name: {space_name}")
print("\nPlease use the following to login and test the Streamlit Application")
print("Username: testuser")
print("Password: MyPassword123!")
if domain_id is not None:
sagemaker_client = boto3.client("sagemaker")
# Replace 'your-space-name' and 'your-domain-id' with your actual values
response = sagemaker_client.describe_space(
DomainId=domain_id, SpaceName=space_name
)
streamlit_url = response["Url"] + "/proxy/8501/"
else:
streamlit_url = "http://localhost:8501"
return streamlit_url
@@ -0,0 +1,835 @@
import base64
import hashlib
import hmac
import json
import os
import time
import warnings
from typing import Any, Dict
import boto3
import yaml
from boto3.session import Session
def suppress_warnings():
"""Suppress noisy dependency warnings for clean notebook output."""
warnings.filterwarnings("ignore", category=DeprecationWarning)
warnings.filterwarnings("ignore", category=FutureWarning)
warnings.filterwarnings("ignore", message=".*urllib3.*")
warnings.filterwarnings("ignore", message=".*charset_normalizer.*")
warnings.filterwarnings("ignore", message=".*chardet.*")
warnings.filterwarnings("ignore", message=".*google-cloud-storage.*")
suppress_warnings()
sts_client = boto3.client("sts")
# Get AWS account details
REGION = boto3.session.Session().region_name
username = "testuser"
sm_name = "customer_support_agent"
role_name = f"CustomerSupportAssistantBedrockAgentCoreRole-{REGION}"
policy_name = f"CustomerSupportAssistantBedrockAgentCorePolicy-{REGION}"
def get_ssm_parameter(name: str, with_decryption: bool = True) -> str:
ssm = boto3.client("ssm")
response = ssm.get_parameter(Name=name, WithDecryption=with_decryption)
return response["Parameter"]["Value"]
def put_ssm_parameter(
name: str, value: str, parameter_type: str = "String", with_encryption: bool = False
) -> None:
ssm = boto3.client("ssm")
put_params = {
"Name": name,
"Value": value,
"Type": parameter_type,
"Overwrite": True,
}
if with_encryption:
put_params["Type"] = "SecureString"
ssm.put_parameter(**put_params)
def delete_ssm_parameter(name: str) -> None:
ssm = boto3.client("ssm")
try:
ssm.delete_parameter(Name=name)
except ssm.exceptions.ParameterNotFound:
pass
def load_api_spec(file_path: str) -> list:
with open(file_path, "r") as f:
data = json.load(f)
if not isinstance(data, list):
raise ValueError("Expected a list in the JSON file")
return data
def get_aws_region() -> str:
session = Session()
return session.region_name
def get_aws_account_id() -> str:
sts = boto3.client("sts")
return sts.get_caller_identity()["Account"]
def get_cognito_client_secret() -> str:
client = boto3.client("cognito-idp")
response = client.describe_user_pool_client(
UserPoolId=get_ssm_parameter("/app/customersupport/agentcore/pool_id"),
ClientId=get_ssm_parameter("/app/customersupport/agentcore/client_id"),
)
return response["UserPoolClient"]["ClientSecret"]
def read_config(file_path: str) -> Dict[str, Any]:
"""
Read configuration from a file path. Supports JSON, YAML, and YML formats.
Args:
file_path (str): Path to the configuration file
Returns:
Dict[str, Any]: Configuration data as a dictionary
Raises:
FileNotFoundError: If the file doesn't exist
ValueError: If the file format is not supported or invalid
yaml.YAMLError: If YAML parsing fails
json.JSONDecodeError: If JSON parsing fails
"""
if not os.path.exists(file_path):
raise FileNotFoundError(f"Configuration file not found: {file_path}")
# Get file extension to determine format
_, ext = os.path.splitext(file_path.lower())
try:
with open(file_path, "r", encoding="utf-8") as file:
if ext == ".json":
return json.load(file)
elif ext in [".yaml", ".yml"]:
return yaml.safe_load(file)
else:
# Try to auto-detect format by attempting JSON first, then YAML
content = file.read()
file.seek(0)
# Try JSON first
try:
return json.loads(content)
except json.JSONDecodeError:
# Try YAML
try:
return yaml.safe_load(content)
except yaml.YAMLError:
raise ValueError(
f"Unsupported configuration file format: {ext}. "
f"Supported formats: .json, .yaml, .yml"
)
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in configuration file {file_path}: {e}")
except yaml.YAMLError as e:
raise ValueError(f"Invalid YAML in configuration file {file_path}: {e}")
except Exception as e:
raise ValueError(f"Error reading configuration file {file_path}: {e}")
def save_customer_support_secret(secret_value):
"""Save a secret in AWS Secrets Manager."""
boto_session = Session()
region = boto_session.region_name
secrets_client = boto3.client("secretsmanager", region_name=region)
try:
secrets_client.create_secret(
Name=sm_name,
SecretString=secret_value,
Description="Secret containing the Cognito Configuration for the Customer Support Agent",
)
print("✅ Created secret")
except secrets_client.exceptions.ResourceExistsException:
secrets_client.update_secret(SecretId=sm_name, SecretString=secret_value)
print("✅ Updated existing secret")
except Exception as e:
print(f"❌ Error saving secret: {str(e)}")
return False
return True
def get_customer_support_secret():
"""Get a secret value from AWS Secrets Manager."""
boto_session = Session()
region = boto_session.region_name
secrets_client = boto3.client("secretsmanager", region_name=region)
try:
response = secrets_client.get_secret_value(SecretId=sm_name)
return response["SecretString"]
except Exception as e:
print(f"Error getting secret: {str(e)}")
return None
def delete_customer_support_secret():
"""Delete a secret from AWS Secrets Manager."""
boto_session = Session()
region = boto_session.region_name
secrets_client = boto3.client("secretsmanager", region_name=region)
try:
secrets_client.delete_secret(SecretId=sm_name, ForceDeleteWithoutRecovery=True)
print("✅ Deleted secret!")
return True
except Exception as e:
print(f"❌ Error deleting secret: {str(e)}")
return False
def get_or_create_cognito_pool(refresh_token=False):
boto_session = Session()
region = boto_session.region_name
# Initialize Cognito client
cognito_client = boto3.client("cognito-idp", region_name=region)
try:
# check for existing cognito pool
cognito_config_str = get_customer_support_secret()
cognito_config = json.loads(cognito_config_str)
if refresh_token:
cognito_config["bearer_token"] = reauthenticate_user(
cognito_config["client_id"], cognito_config["client_secret"]
)
return cognito_config
except Exception:
print("No existing cognito config found. Creating a new one..")
try:
# Create User Pool
user_pool_response = cognito_client.create_user_pool(
PoolName="MCPServerPool", Policies={"PasswordPolicy": {"MinimumLength": 8}}
)
pool_id = user_pool_response["UserPool"]["Id"]
# Create App Client
app_client_response = cognito_client.create_user_pool_client(
UserPoolId=pool_id,
ClientName="MCPServerPoolClient",
GenerateSecret=True,
ExplicitAuthFlows=[
"ALLOW_USER_PASSWORD_AUTH",
"ALLOW_REFRESH_TOKEN_AUTH",
"ALLOW_USER_SRP_AUTH",
],
)
print(app_client_response["UserPoolClient"])
client_id = app_client_response["UserPoolClient"]["ClientId"]
client_secret = app_client_response["UserPoolClient"]["ClientSecret"]
# Create User
cognito_client.admin_create_user(
UserPoolId=pool_id,
Username=username,
TemporaryPassword="Temp123!",
MessageAction="SUPPRESS",
)
# Set Permanent Password
cognito_client.admin_set_user_password(
UserPoolId=pool_id,
Username=username,
Password="MyPassword123!",
Permanent=True,
)
message = bytes(username + client_id, "utf-8")
key = bytes(client_secret, "utf-8")
secret_hash = base64.b64encode(
hmac.new(key, message, digestmod=hashlib.sha256).digest()
).decode()
# Authenticate User and get Access Token
auth_response = cognito_client.initiate_auth(
ClientId=client_id,
AuthFlow="USER_PASSWORD_AUTH",
AuthParameters={
"USERNAME": username,
"PASSWORD": "MyPassword123!",
"SECRET_HASH": secret_hash,
},
)
bearer_token = auth_response["AuthenticationResult"]["AccessToken"]
discovery_url = f"https://cognito-idp.{region}.amazonaws.com/{pool_id}/.well-known/openid-configuration"
# Output the required values
print(f"Pool id: {pool_id}")
print(f"Discovery URL: {discovery_url}")
print(f"Client ID: {client_id}")
print(f"Bearer Token: {bearer_token}")
# Return values if needed for further processing
cognito_config = {
"pool_id": pool_id,
"client_id": client_id,
"client_secret": client_secret,
"secret_hash": secret_hash,
"bearer_token": bearer_token,
"discovery_url": discovery_url,
}
put_ssm_parameter("/app/customersupport/agentcore/client_id", client_id)
put_ssm_parameter("/app/customersupport/agentcore/pool_id", pool_id)
put_ssm_parameter(
"/app/customersupport/agentcore/cognito_discovery_url", discovery_url
)
put_ssm_parameter("/app/customersupport/agentcore/client_secret", client_secret)
save_customer_support_secret(json.dumps(cognito_config))
return cognito_config
except Exception as e:
print(f"Error: {e}")
return None
def cleanup_cognito_resources(pool_id):
"""
Delete Cognito resources including users, app clients, and user pool
"""
try:
# Initialize Cognito client using the same session configuration
boto_session = Session()
region = boto_session.region_name
cognito_client = boto3.client("cognito-idp", region_name=region)
if pool_id:
try:
# List and delete all app clients
clients_response = cognito_client.list_user_pool_clients(
UserPoolId=pool_id, MaxResults=60
)
for client in clients_response["UserPoolClients"]:
print(f"Deleting app client: {client['ClientName']}")
cognito_client.delete_user_pool_client(
UserPoolId=pool_id, ClientId=client["ClientId"]
)
# List and delete all users
users_response = cognito_client.list_users(
UserPoolId=pool_id, AttributesToGet=["email"]
)
for user in users_response.get("Users", []):
print(f"Deleting user: {user['Username']}")
cognito_client.admin_delete_user(
UserPoolId=pool_id, Username=user["Username"]
)
# Delete the user pool
print(f"Deleting user pool: {pool_id}")
cognito_client.delete_user_pool(UserPoolId=pool_id)
print("Successfully cleaned up all Cognito resources")
return True
except cognito_client.exceptions.ResourceNotFoundException:
print(
f"User pool {pool_id} not found. It may have already been deleted."
)
return True
except Exception as e:
print(f"Error during cleanup: {str(e)}")
return False
else:
print("No matching user pool found")
return True
except Exception as e:
print(f"Error initializing cleanup: {str(e)}")
return False
def reauthenticate_user(client_id, client_secret):
boto_session = Session()
region = boto_session.region_name
# Initialize Cognito client
cognito_client = boto3.client("cognito-idp", region_name=region)
# Authenticate User and get Access Token
message = bytes(username + client_id, "utf-8")
key = bytes(client_secret, "utf-8")
secret_hash = base64.b64encode(
hmac.new(key, message, digestmod=hashlib.sha256).digest()
).decode()
auth_response = cognito_client.initiate_auth(
ClientId=client_id,
AuthFlow="USER_PASSWORD_AUTH",
AuthParameters={
"USERNAME": username,
"PASSWORD": "MyPassword123!",
"SECRET_HASH": secret_hash,
},
)
bearer_token = auth_response["AuthenticationResult"]["AccessToken"]
return bearer_token
def create_agentcore_runtime_execution_role():
iam = boto3.client("iam")
boto_session = Session()
region = boto_session.region_name
account_id = get_aws_account_id()
# Trust relationship policy
trust_policy = {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AssumeRolePolicy",
"Effect": "Allow",
"Principal": {"Service": "bedrock-agentcore.amazonaws.com"},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {"aws:SourceAccount": account_id},
"ArnLike": {
"aws:SourceArn": f"arn:aws:bedrock-agentcore:{region}:{account_id}:*"
},
},
}
],
}
# IAM policy document
policy_document = {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ECRImageAccess",
"Effect": "Allow",
"Action": ["ecr:BatchGetImage", "ecr:GetDownloadUrlForLayer"],
"Resource": [f"arn:aws:ecr:{region}:{account_id}:repository/*"],
},
{
"Effect": "Allow",
"Action": ["logs:DescribeLogStreams", "logs:CreateLogGroup"],
"Resource": [
f"arn:aws:logs:{region}:{account_id}:log-group:/aws/bedrock-agentcore/runtimes/*"
],
},
{
"Effect": "Allow",
"Action": ["logs:DescribeLogGroups"],
"Resource": [f"arn:aws:logs:{region}:{account_id}:log-group:*"],
},
{
"Effect": "Allow",
"Action": ["logs:CreateLogStream", "logs:PutLogEvents"],
"Resource": [
f"arn:aws:logs:{region}:{account_id}:log-group:/aws/bedrock-agentcore/runtimes/*:log-stream:*"
],
},
{
"Sid": "ECRTokenAccess",
"Effect": "Allow",
"Action": ["ecr:GetAuthorizationToken"],
"Resource": "*",
},
{
"Effect": "Allow",
"Action": [
"xray:PutTraceSegments",
"xray:PutTelemetryRecords",
"xray:GetSamplingRules",
"xray:GetSamplingTargets",
],
"Resource": ["*"],
},
{
"Effect": "Allow",
"Resource": "*",
"Action": "cloudwatch:PutMetricData",
"Condition": {
"StringEquals": {"cloudwatch:namespace": "bedrock-agentcore"}
},
},
{
"Sid": "GetAgentAccessToken",
"Effect": "Allow",
"Action": [
"bedrock-agentcore:GetWorkloadAccessToken",
"bedrock-agentcore:GetWorkloadAccessTokenForJWT",
"bedrock-agentcore:GetWorkloadAccessTokenForUserId",
],
"Resource": [
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/customer_support_agent-*",
],
},
{
"Sid": "BedrockModelInvocation",
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream",
"bedrock:ApplyGuardrail",
"bedrock:Retrieve",
],
"Resource": [
"arn:aws:bedrock:*::foundation-model/*",
f"arn:aws:bedrock:{region}:{account_id}:*",
],
},
{
"Sid": "AllowAgentToUseMemory",
"Effect": "Allow",
"Action": [
"bedrock-agentcore:CreateEvent",
"bedrock-agentcore:ListEvents",
"bedrock-agentcore:GetMemoryRecord",
"bedrock-agentcore:GetMemory",
"bedrock-agentcore:RetrieveMemoryRecords",
"bedrock-agentcore:ListMemoryRecords",
],
"Resource": [f"arn:aws:bedrock-agentcore:{region}:{account_id}:*"],
},
{
"Sid": "GetMemoryId",
"Effect": "Allow",
"Action": ["ssm:GetParameter"],
"Resource": [f"arn:aws:ssm:{region}:{account_id}:parameter/*"],
},
{
"Sid": "GatewayAccess",
"Effect": "Allow",
"Action": [
"bedrock-agentcore:GetGateway",
"bedrock-agentcore:InvokeGateway",
],
"Resource": [
f"arn:aws:bedrock-agentcore:{region}:{account_id}:gateway/*"
],
},
],
}
try:
# Check if role already exists
role_arn = None
try:
existing_role = iam.get_role(RoleName=role_name)
print(f"️ Role {role_name} already exists")
role_arn = existing_role["Role"]["Arn"]
except iam.exceptions.NoSuchEntityException:
# Create IAM role
role_response = iam.create_role(
RoleName=role_name,
AssumeRolePolicyDocument=json.dumps(trust_policy),
Description="IAM role for Amazon Bedrock AgentCore with required permissions",
)
print(f"✅ Created IAM role: {role_name}")
role_arn = role_response["Role"]["Arn"]
print(f"Role ARN: {role_arn}")
# Check if policy already exists, create if not
policy_arn = f"arn:aws:iam::{account_id}:policy/{policy_name}"
try:
iam.get_policy(PolicyArn=policy_arn)
print(f"️ Policy {policy_name} already exists")
except iam.exceptions.NoSuchEntityException:
# Create policy
policy_response = iam.create_policy(
PolicyName=policy_name,
PolicyDocument=json.dumps(policy_document),
Description="Policy for Amazon Bedrock AgentCore permissions",
)
print(f"✅ Created policy: {policy_name}")
policy_arn = policy_response["Policy"]["Arn"]
# Attach policy to role
try:
iam.attach_role_policy(RoleName=role_name, PolicyArn=policy_arn)
print("✅ Attached policy to role")
except Exception as e:
if "already attached" in str(e).lower():
print("️ Policy already attached to role")
else:
raise
print(f"Policy ARN: {policy_arn}")
put_ssm_parameter(
"/app/customersupport/agentcore/runtime_execution_role_arn",
role_arn,
)
return role_arn
except Exception as e:
print(f"❌ Error creating IAM role: {str(e)}")
return None
def delete_agentcore_runtime_execution_role():
iam = boto3.client("iam")
try:
account_id = boto3.client("sts").get_caller_identity()["Account"]
policy_arn = f"arn:aws:iam::{account_id}:policy/{policy_name}"
# Detach policy from role
try:
iam.detach_role_policy(RoleName=role_name, PolicyArn=policy_arn)
print("✅ Detached policy from role")
except Exception:
pass
# Delete role
try:
iam.delete_role(RoleName=role_name)
print(f"✅ Deleted role: {role_name}")
except Exception:
pass
# Delete policy
try:
iam.delete_policy(PolicyArn=policy_arn)
print(f"✅ Deleted policy: {policy_name}")
except Exception:
pass
delete_ssm_parameter(
"/app/customersupport/agentcore/runtime_execution_role_arn"
)
except Exception as e:
print(f"❌ Error during cleanup: {str(e)}")
def agentcore_memory_cleanup(memory_id: str = None):
"""List all memories and their associated strategies"""
control_client = boto3.client("bedrock-agentcore-control", region_name=REGION)
if memory_id:
response = control_client.delete_memory(memoryId=memory_id)
print(f"✅ Successfully deleted memory: {memory_id}")
else:
next_token = None
while True:
# Build request parameters
params = {}
if next_token:
params["nextToken"] = next_token
# List memories
try:
response = control_client.list_memories(**params)
# Process each memory
for memory in response.get("memories", []):
memory_id = memory.get("id")
print(f"\nMemory ID: {memory_id}")
print(f"Status: {memory.get('status')}")
response = control_client.delete_memory(memoryId=memory_id)
response = control_client.list_memories(**params)
print(f"✅ Successfully deleted memory: {memory_id}")
response = control_client.list_memories(**params)
# Process each memory status
for memory in response.get("memories", []):
memory_id = memory.get("id")
print(f"\nMemory ID: {memory_id}")
print(f"Status: {memory.get('status')}")
except Exception as e:
print(f"⚠️ Error getting memory details: {e}")
# Check for more results
next_token = response.get("nextToken")
if not next_token:
break
def gateway_target_cleanup(gateway_id: str = None):
gateway_client = boto3.client(
"bedrock-agentcore-control",
region_name=REGION,
)
if not gateway_id:
response = gateway_client.list_gateways()
gateway_id = response["items"][0]["gatewayId"]
print(f"🗑️ Deleting all targets for gateway: {gateway_id}")
# List and delete all targets
list_response = gateway_client.list_gateway_targets(
gatewayIdentifier=gateway_id, maxResults=100
)
targets_deleted = False
for item in list_response["items"]:
target_id = item["targetId"]
print(f" Deleting target: {target_id}")
gateway_client.delete_gateway_target(
gatewayIdentifier=gateway_id, targetId=target_id
)
print(f" ✅ Target {target_id} deleted")
targets_deleted = True
# Wait for target deletions to propagate
if targets_deleted:
print("⏳ Waiting for target deletions to propagate...")
time.sleep(5)
# Delete the gateway
print(f"🗑️ Deleting gateway: {gateway_id}")
gateway_client.delete_gateway(gatewayIdentifier=gateway_id)
print(f"✅ Gateway {gateway_id} deleted successfully")
def runtime_resource_cleanup(runtime_arn: str = None):
try:
# Initialize AWS clients
agentcore_control_client = boto3.client(
"bedrock-agentcore-control", region_name=REGION
)
ecr_client = boto3.client("ecr", region_name=REGION)
if runtime_arn:
runtime_id = runtime_arn.split(":")[-1].split("/")[-1]
response = agentcore_control_client.delete_agent_runtime(
agentRuntimeId=runtime_id
)
print(f" ✅ Agent runtime deleted: {response['status']}")
else:
# Delete the AgentCore Runtime
# print(" 🗑️ Deleting AgentCore Runtime...")
runtimes = agentcore_control_client.list_agent_runtimes()
for runtime in runtimes["agentRuntimes"]:
response = agentcore_control_client.delete_agent_runtime(
agentRuntimeId=runtime["agentRuntimeId"]
)
print(f" ✅ Agent runtime deleted: {response['status']}")
# Delete the ECR repository
print(" 🗑️ Deleting ECR repository...")
repositories = ecr_client.describe_repositories()
for repo in repositories["repositories"]:
if "bedrock-agentcore-customer_support_agent" in repo["repositoryName"]:
ecr_client.delete_repository(
repositoryName=repo["repositoryName"], force=True
)
print(f" ✅ ECR repository deleted: {repo['repositoryName']}")
except Exception as e:
print(f" ⚠️ Error during runtime cleanup: {e}")
def delete_observability_resources():
# Configuration
log_group_name = "agents/customer-support-assistant-logs"
log_stream_name = "default"
logs_client = boto3.client("logs", region_name=REGION)
# Delete log stream first (must be done before deleting log group)
try:
print(f" 🗑️ Deleting log stream '{log_stream_name}'...")
logs_client.delete_log_stream(
logGroupName=log_group_name, logStreamName=log_stream_name
)
print(f" ✅ Log stream '{log_stream_name}' deleted successfully")
except Exception as e:
if e.response["Error"]["Code"] == "ResourceNotFoundException":
print(f" ️ Log stream '{log_stream_name}' doesn't exist")
else:
print(f" ⚠️ Error deleting log stream: {e}")
# Delete log group
try:
print(f" 🗑️ Deleting log group '{log_group_name}'...")
logs_client.delete_log_group(logGroupName=log_group_name)
print(f" ✅ Log group '{log_group_name}' deleted successfully")
except Exception as e:
if e.response["Error"]["Code"] == "ResourceNotFoundException":
print(f" ️ Log group '{log_group_name}' doesn't exist")
else:
print(f" ⚠️ Error deleting log group: {e}")
def local_file_cleanup():
# List of files to clean up
files_to_delete = [
"Dockerfile",
".dockerignore",
".bedrock_agentcore.yaml",
"customer_support_agent.py",
"agent_runtime.py",
]
deleted_files = []
missing_files = []
for file in files_to_delete:
if os.path.exists(file):
try:
os.unlink(file)
deleted_files.append(file)
print(f" ✅ Deleted {file}")
except Exception as e:
print(f" ⚠️ Error deleting {file}: {e}")
else:
missing_files.append(file)
if deleted_files:
print(f"\n📁 Successfully deleted {len(deleted_files)} files")
if missing_files:
print(
f"{len(missing_files)} files were already missing: {', '.join(missing_files)}"
)
def policy_engine_cleanup(policy_engine_id: str = None):
policy_client = boto3.client(
"bedrock-agentcore-control",
region_name=REGION,
)
if not policy_engine_id:
response = policy_client.list_policy_engines()
policy_engine_id = response["policyEngines"][0]["policyEngineId"]
print(f"🗑️ Deleting all policies for policy engine: {policy_engine_id}")
# List and delete all policies
list_response = policy_client.list_policies(
policyEngineId=policy_engine_id, maxResults=100
)
policies_deleted = False
for item in list_response["policies"]:
policy_id = item["policyId"]
print(f" Deleting policy: {policy_id}")
policy_client.delete_policy(policyEngineId=policy_engine_id, policyId=policy_id)
print(f" ✅ Policy {policy_id} deleted")
policies_deleted = True
# Wait for policy deletions to propagate before deleting the engine
if policies_deleted:
print("⏳ Waiting for policy deletions to propagate...")
time.sleep(5) # 5 seconds is usually sufficient
# Delete the policy engine
print(f"🗑️ Deleting policy engine: {policy_engine_id}")
policy_client.delete_policy_engine(policyEngineId=policy_engine_id)
print(f"✅ Policy engine {policy_engine_id} deleted successfully")
@@ -0,0 +1,306 @@
AWSTemplateFormatVersion: '2010-09-09'
Description: 'CloudFormation template for Customer Support System with DynamoDB tables, SSM parameters, and synthetic data'
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: Cognito Configuration
Parameters:
- UserPoolName
- MachineAppClientName
- WebAppClientName
Parameters:
UserPoolName:
Type: String
Default: 'CustomerSupportGatewayPool'
Description: 'Name of the Cognito User Pool'
MachineAppClientName:
Type: String
Default: 'CustomerSupportMachineClient'
Description: 'Name of the Cognito User Pool Application Client'
WebAppClientName:
Type: String
Default: 'CustomerSupportWebClient'
Description: 'Name of the Cognito User Pool Web Application Client'
Resources:
UserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: !Ref UserPoolName
MfaConfiguration: 'OPTIONAL'
UsernameConfiguration:
CaseSensitive: false
UsernameAttributes:
- email # <--- Use email as username
AutoVerifiedAttributes:
- email # <--- Auto-verify email if you want to skip confirmation step
# LambdaConfig:
# PostConfirmation: !GetAtt PostSignupFunction.Arn
AdminGroup:
Type: AWS::Cognito::UserPoolGroup
Properties:
GroupName: admin
Description: Administrator group
UserPoolId: !Ref UserPool
Precedence: 1 # Higher priority (lower number = higher precedence)
CustomerGroup:
Type: AWS::Cognito::UserPoolGroup
Properties:
GroupName: customer
Description: Regular customer group
UserPoolId: !Ref UserPool
Precedence: 2
WebUserPoolClient:
DependsOn: ResourceServer
Type: AWS::Cognito::UserPoolClient
Properties:
ClientName: !Ref WebAppClientName
UserPoolId: !Ref UserPool
GenerateSecret: false
AllowedOAuthFlows:
- code
AllowedOAuthScopes:
- openid
- email
- profile
- !Join
- ''
- - 'default-m2m-resource-server-'
- !Select [0, !Split ['-', !Select [2, !Split ['/', !Ref 'AWS::StackId']]]]
- '/read'
AllowedOAuthFlowsUserPoolClient: true
CallbackURLs:
- http://localhost:8501/
- https://example.com/auth/callback
LogoutURLs:
- http://localhost:8501/
SupportedIdentityProviders:
- COGNITO
AccessTokenValidity: 60
IdTokenValidity: 60
RefreshTokenValidity: 30
TokenValidityUnits:
AccessToken: minutes
IdToken: minutes
RefreshToken: days
EnableTokenRevocation: true
MachineUserPoolClient:
Type: AWS::Cognito::UserPoolClient
DependsOn: ResourceServer
Properties:
ClientName: !Ref MachineAppClientName
UserPoolId: !Ref UserPool
GenerateSecret: true
ExplicitAuthFlows:
- ALLOW_REFRESH_TOKEN_AUTH
RefreshTokenValidity: 1
AccessTokenValidity: 60
IdTokenValidity: 60
TokenValidityUnits:
AccessToken: minutes
IdToken: minutes
RefreshToken: days
AllowedOAuthFlows:
- client_credentials
AllowedOAuthScopes:
- !Join
- ''
- - 'default-m2m-resource-server-'
- !Select [0, !Split ['-', !Select [2, !Split ['/', !Ref 'AWS::StackId']]]]
- '/read'
AllowedOAuthFlowsUserPoolClient: true
SupportedIdentityProviders:
- COGNITO
EnableTokenRevocation: true
ResourceServer:
Type: AWS::Cognito::UserPoolResourceServer
Properties:
UserPoolId: !Ref UserPool
Identifier: !Join
- '-'
- - 'default-m2m-resource-server'
- !Select [0, !Split ['-', !Select [2, !Split ['/', !Ref 'AWS::StackId']]]]
Name: !Join
- '-'
- - 'Default M2M Resource Server '
- !Select [0, !Split ['-', !Select [2, !Split ['/', !Ref 'AWS::StackId']]]]
Scopes:
- ScopeName: 'read'
ScopeDescription: 'An example scope created by Amazon Cognito quick start'
UserPoolDomain:
Type: AWS::Cognito::UserPoolDomain
Properties:
UserPoolId: !Ref UserPool
Domain: !Join
- ''
- - !Ref 'AWS::Region'
- !Select [0, !Split ['-', !Select [2, !Split ['/', !Ref 'AWS::StackId']]]]
PostSignupFunctionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: AllowBasicLogs
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: arn:aws:logs:*:*:*
- PolicyName: Cognito
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- cognito-idp:AdminAddUserToGroup
Resource: "*"
PostSignupFunction:
Type: AWS::Lambda::Function
Properties:
Handler: index.lambda_handler
Runtime: python3.13
Role: !GetAtt PostSignupFunctionRole.Arn
Timeout: 10
Code:
ZipFile: |
import boto3
def lambda_handler(event, context):
user_pool_id = event['userPoolId']
username = event['userName']
client = boto3.client('cognito-idp')
# Add user to 'customer' group
try:
client.admin_add_user_to_group(
UserPoolId=user_pool_id,
Username=username,
GroupName='Customer'
)
print(f"User {username} added to 'customer' group.")
except Exception as e:
print(f"Error adding user to group: {e}")
return event
CognitoMachineClientIdParameter:
Type: AWS::SSM::Parameter
Properties:
Name: /app/customersupport/agentcore/client_id
Type: String
Value: !Ref MachineUserPoolClient
Description: Machine Cognito client ID
Tags:
Application: CustomerSupport
CognitoWebClientIdParameter:
Type: AWS::SSM::Parameter
Properties:
Name: /app/customersupport/agentcore/web_client_id
Type: String
Value: !Ref WebUserPoolClient
Description: Cognito client ID for web app
Tags:
Application: CustomerSupport
UserPoolIdParameter:
Type: AWS::SSM::Parameter
Properties:
Name: /app/customersupport/agentcore/pool_id
Type: String
Value: !Ref UserPool
Description: Cognito client ID
Tags:
Application: CustomerSupport
CognitoAuthScopeParameter:
Type: AWS::SSM::Parameter
Properties:
Name: /app/customersupport/agentcore/cognito_auth_scope
Type: String
Value: !Join
- ''
- - 'default-m2m-resource-server-'
- !Select [0, !Split ['-', !Select [2, !Split ['/', !Ref 'AWS::StackId']]]]
- '/read'
Description: OAuth2 scope for Cognito auth
Tags:
Application: CustomerSupport
CognitoDiscoveryURLParameter:
Type: AWS::SSM::Parameter
Properties:
Name: /app/customersupport/agentcore/cognito_discovery_url
Type: String
Value: !Sub 'https://cognito-idp.${AWS::Region}.amazonaws.com/${UserPool}/.well-known/openid-configuration'
Description: OAuth2 Discovery URL
Tags:
Application: CustomerSupport
CognitoTokenURLParameter:
Type: AWS::SSM::Parameter
Properties:
Name: /app/customersupport/agentcore/cognito_token_url
Type: String
Value: !Join
- ''
- - !Sub 'https://${AWS::Region}'
- !Select [0, !Split ['-', !Select [2, !Split ['/', !Ref 'AWS::StackId']]]]
- !Sub '.auth.${AWS::Region}.amazoncognito.com/oauth2/token'
Description: OAuth2 Token URL
Tags:
Application: CustomerSupport
CognitoAuthorizeURLParameter:
Type: AWS::SSM::Parameter
Properties:
Name: /app/customersupport/agentcore/cognito_auth_url
Type: String
Value: !Join
- ''
- - !Sub 'https://${AWS::Region}'
- !Select [0, !Split ['-', !Select [2, !Split ['/', !Ref 'AWS::StackId']]]]
- !Sub '.auth.${AWS::Region}.amazoncognito.com/oauth2/authorize'
Description: OAuth2 Token URL
Tags:
Application: CustomerSupport
CognitoDomainParameter:
Type: AWS::SSM::Parameter
Properties:
Name: /app/customersupport/agentcore/cognito_domain
Type: String
Value: !Join
- ''
- - !Sub 'https://${AWS::Region}'
- !Select [0, !Split ['-', !Select [2, !Split ['/', !Ref 'AWS::StackId']]]]
- !Sub '.auth.${AWS::Region}.amazoncognito.com'
Description: Cognito hosted domain for OAuth2
Tags:
Application: CustomerSupport
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,44 @@
[
{
"name": "check_warranty_status",
"description": "Check the warranty status of a product using its serial number and optionally verify via email",
"inputSchema": {
"type": "object",
"properties": {
"serial_number": {
"type": "string"
},
"customer_email": {
"type": "string"
}
},
"required": [
"serial_number"
]
}
},
{
"name": "web_search",
"description": "Search the web for updated information using DuckDuckGo",
"inputSchema": {
"type": "object",
"properties": {
"keywords": {
"type": "string",
"description": "The search query keywords"
},
"region": {
"type": "string",
"description": "The search region (e.g., us-en, uk-en, ru-ru)"
},
"max_results": {
"type": "integer",
"description": "The maximum number of results to return"
}
},
"required": [
"keywords"
]
}
}
]
@@ -0,0 +1,189 @@
import boto3
import json
from datetime import datetime
from botocore.exceptions import ClientError
import logging
import re
# Setting logger
logging.basicConfig(
format="[%(asctime)s] p%(process)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s",
level=logging.INFO,
)
logger = logging.getLogger(__name__)
# Initialize DynamoDB resource
dynamodb = boto3.resource("dynamodb")
smm_client = boto3.client("ssm")
# Get warranty table name from Parameter Store
warranty_table = smm_client.get_parameter(
Name="/app/customersupport/dynamodb/warranty_table_name", WithDecryption=False
)
warranty_table_name = warranty_table["Parameter"]["Value"]
def ensure_warranty_table_exists():
"""Create the DynamoDB warranty table if it doesn't exist."""
try:
table = dynamodb.Table(warranty_table_name)
table.load()
return table
except ClientError as e:
raise e
def validate_serial_number(serial_number: str) -> bool:
"""Validate serial number format."""
pattern = r"^[A-Z0-9]{8,20}$"
return bool(re.match(pattern, serial_number.upper()))
def calculate_days_remaining(end_date: str) -> int:
"""Calculate days remaining until warranty expires."""
try:
end_date_obj = datetime.strptime(end_date, "%Y-%m-%d")
today = datetime.now()
delta = end_date_obj - today
return delta.days
except ValueError:
return 0
def get_warranty_status_text(days_remaining: int) -> str:
"""Get warranty status text based on days remaining."""
if days_remaining > 30:
return "✅ Active"
elif days_remaining > 0:
return "⚠️ Expiring Soon"
else:
return "❌ Expired"
def check_warranty_status(serial_number: str, customer_email: str = None) -> str:
"""
Check the warranty status of a product using its serial number.
Args:
serial_number (str): Product serial number (8-20 alphanumeric characters).
customer_email (str, optional): Customer email for verification purposes.
Returns:
str: Formatted warranty status information including coverage details and expiration date.
Raises:
ValueError: If the serial number format is invalid.
ClientError: If there's an issue with DynamoDB operations.
"""
logger.info(
json.dumps(
{
"serial_number": serial_number,
"customer_email": customer_email,
"timestamp": datetime.now().isoformat(),
},
indent=2,
default=str,
)
)
if not validate_serial_number(serial_number):
raise ValueError("Serial number must be 8-20 alphanumeric characters")
serial_number = serial_number.upper()
try:
table = ensure_warranty_table_exists()
response = table.get_item(Key={"serial_number": serial_number})
if "Item" not in response:
not_found_response = [
"❌ Warranty Not Found",
"====================",
f"🔍 Serial Number: {serial_number}",
"",
"This serial number was not found in our warranty database.",
"Please verify the serial number and try again.",
"",
"If you believe this is an error, please contact our support team",
"with your purchase receipt for assistance.",
]
return "\n".join(not_found_response)
warranty_item = response["Item"]
# Extract warranty information
product_name = warranty_item.get("product_name", "Unknown Product")
purchase_date = warranty_item.get("purchase_date", "Unknown")
warranty_end_date = warranty_item.get("warranty_end_date", "Unknown")
warranty_type = warranty_item.get("warranty_type", "Standard")
customer_name = warranty_item.get("customer_name", "Unknown")
coverage_details = warranty_item.get(
"coverage_details", "Standard coverage applies"
)
# Calculate days remaining
days_remaining = (
calculate_days_remaining(warranty_end_date)
if warranty_end_date != "Unknown"
else 0
)
status_text = get_warranty_status_text(days_remaining)
# Format warranty information
warranty_info = [
"🛡️ Warranty Status Information",
"===============================",
f"📱 Product: {product_name}",
f"🔢 Serial Number: {serial_number}",
f"👤 Customer: {customer_name}",
f"📅 Purchase Date: {purchase_date}",
f"⏰ Warranty End Date: {warranty_end_date}",
f"📋 Warranty Type: {warranty_type}",
f"🔍 Status: {status_text}",
"",
]
# Add days remaining information
if days_remaining > 0:
warranty_info.append(f"📆 Days Remaining: {days_remaining} days")
elif days_remaining == 0:
warranty_info.append("📆 Warranty expires today!")
else:
warranty_info.append(f"📆 Expired {abs(days_remaining)} days ago")
warranty_info.extend(["", "🔧 Coverage Details:", f" {coverage_details}", ""])
# Add recommendations based on status
if days_remaining > 30:
warranty_info.append(
"✨ Your warranty is active. Contact support for any issues."
)
elif days_remaining > 0:
warranty_info.extend(
[
"⚠️ Your warranty is expiring soon!",
" Consider purchasing extended warranty coverage.",
]
)
else:
warranty_info.extend(
[
"❌ Your warranty has expired.",
" Extended warranty options may be available.",
" Contact support for repair service pricing.",
]
)
logger.info(json.dumps(warranty_item, indent=2, default=str))
return "\n".join(warranty_info)
except ClientError as e:
logger.error("DynamoDB Error:", e)
raise Exception(
f"Failed to check warranty status: {e.response['Error']['Message']}"
)
except Exception as e:
logger.error("Unexpected Error:", str(e))
raise Exception(f"Failed to check warranty status: {str(e)}")
@@ -0,0 +1,77 @@
from check_warranty import check_warranty_status
from web_search import web_search
def get_named_parameter(event, name):
if name not in event:
return None
return event.get(name)
def lambda_handler(event, context):
print(f"Event: {event}")
print(f"Context: {context}")
extended_tool_name = context.client_context.custom["bedrockAgentCoreToolName"]
resource = extended_tool_name.split("___")[1]
print(resource)
if resource == "check_warranty_status":
serial_number = get_named_parameter(event=event, name="serial_number")
customer_email = get_named_parameter(event=event, name="customer_email")
if not serial_number:
return {
"statusCode": 400,
"body": "❌ Please provide serial_number",
}
try:
warranty_status = check_warranty_status(
serial_number=serial_number, customer_email=customer_email
)
except Exception as e:
print(e)
return {
"statusCode": 400,
"body": f"{e}",
}
return {
"statusCode": 200,
"body": warranty_status,
}
elif resource == "web_search":
keywords = get_named_parameter(event=event, name="keywords")
region = get_named_parameter(event=event, name="region") or "us-en"
max_results = get_named_parameter(event=event, name="max_results") or 5
if not keywords:
return {
"statusCode": 400,
"body": "❌ Please provide keywords for search",
}
try:
search_results = web_search(
keywords=keywords, region=region, max_results=int(max_results)
)
except Exception as e:
print(e)
return {
"statusCode": 400,
"body": f"{e}",
}
return {
"statusCode": 200,
"body": f"🔍 Search Results: {search_results}",
}
return {
"statusCode": 400,
"body": f"❌ Unknown toolname: {resource}",
}
@@ -0,0 +1,22 @@
from ddgs import DDGS
def web_search(keywords: str, region: str = "us-en", max_results: int = 5) -> str:
"""Search the web for updated information.
Args:
keywords (str): The search query keywords.
region (str): The search region: wt-wt, us-en, uk-en, ru-ru, etc.
max_results (int): The maximum number of results to return.
Returns:
List of dictionaries with search results.
"""
try:
results = DDGS().text(keywords, region=region, max_results=max_results)
return results if results else "No results found."
except Exception as e:
return f"Search error: {str(e)}"
print("✅ Web search tool ready")
@@ -0,0 +1,12 @@
google-adk
litellm
boto3>=1.42.3
botocore>=1.42.3
bedrock-agentcore==1.1.1
bedrock-agentcore-starter-toolkit==0.2.3
aws-opentelemetry-distro==0.14.0
ddgs
pyyaml
strands-agents
strands-agents-tools
nest_asyncio
@@ -0,0 +1,60 @@
#!/bin/bash
set -e
set -o pipefail
# ----- Config -----
BUCKET_NAME=${1:-customersupport}
INFRA_STACK_NAME=${2:-CustomerSupportStackInfra}
COGNITO_STACK_NAME=${3:-CustomerSupportStackCognito}
REGION=$(aws configure get region)
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
FULL_BUCKET_NAME="${BUCKET_NAME}-${ACCOUNT_ID}"
ZIP_FILE="lambda.zip"
S3_KEY="lambda.zip"
if [ $? -ne 0 ] || [ -z "$ACCOUNT_ID" ] || [ "$ACCOUNT_ID" = "None" ]; then
echo "❌ Failed to get AWS Account ID. Please check your AWS credentials and network connectivity."
echo "Error: $ACCOUNT_ID"
exit 1
fi
# ----- Confirm Deletion -----
read -p "⚠️ Are you sure you want to delete stacks '$INFRA_STACK_NAME', '$COGNITO_STACK_NAME' and clean up S3? (y/N): " confirm
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
echo "❌ Cleanup cancelled."
exit 1
fi
# ----- 1. Delete CloudFormation stacks -----
echo "🧨 Deleting stack: $INFRA_STACK_NAME..."
aws cloudformation delete-stack --stack-name "$INFRA_STACK_NAME" --region "$REGION"
echo "⏳ Waiting for $INFRA_STACK_NAME to be deleted..."
aws cloudformation wait stack-delete-complete --stack-name "$INFRA_STACK_NAME" --region "$REGION"
echo "✅ Stack $INFRA_STACK_NAME deleted."
echo "🧨 Deleting stack: $COGNITO_STACK_NAME..."
aws cloudformation delete-stack --stack-name "$COGNITO_STACK_NAME" --region "$REGION"
echo "⏳ Waiting for $COGNITO_STACK_NAME to be deleted..."
aws cloudformation wait stack-delete-complete --stack-name "$COGNITO_STACK_NAME" --region "$REGION"
echo "✅ Stack $COGNITO_STACK_NAME deleted."
# ----- 2. Delete zip file from S3 -----
echo "🧹 Deleting all contents of s3://$FULL_BUCKET_NAME..."
aws s3 rm "s3://$FULL_BUCKET_NAME" --recursive || echo "⚠️ Failed to clean bucket or it is already empty."
# ----- 3. Optionally delete the bucket -----
read -p "🪣 Do you want to delete the bucket '$FULL_BUCKET_NAME'? (y/N): " delete_bucket
if [[ "$delete_bucket" == "y" || "$delete_bucket" == "Y" ]]; then
echo "🚮 Deleting bucket $FULL_BUCKET_NAME..."
aws s3 rb "s3://$FULL_BUCKET_NAME" --force
echo "✅ Bucket deleted."
else
echo "🪣 Bucket retained: $FULL_BUCKET_NAME"
fi
# ----- 4. Clean up local zip file -----
echo "🗑️ Removing local file $ZIP_FILE..."
rm -f "$ZIP_FILE"
echo "✅ Deployment complete."
@@ -0,0 +1,20 @@
#!/bin/bash
set -e
set -o pipefail
NAMESPACE="/app/customersupport"
REGION=$(aws configure get region)
echo "🔍 Listing SSM parameters under namespace: $NAMESPACE/*"
echo "📍 Region: $REGION"
echo ""
# Fetch and paginate through all parameters under the given path
aws ssm get-parameters-by-path \
--path "$NAMESPACE" \
--recursive \
--with-decryption \
--region "$REGION" \
--query "Parameters[*].{Name:Name,Value:Value}" \
--output table
@@ -0,0 +1,134 @@
#!/usr/bin/env pwsh
# Enable strict error handling
$ErrorActionPreference = "Stop"
# ----- Config -----
$BucketName = "customersupport112"
$InfraStackName = "CustomerSupportStackInfra"
$CognitoStackName = "CustomerSupportStackCognito"
$InfraTemplateFile = "prerequisite/infrastructure.yaml"
$CognitoTemplateFile = "prerequisite/cognito.yaml"
try {
$Region = aws configure get region 2>$null
if (-not $Region) { $Region = "us-west-2" }
} catch {
$Region = "us-west-2"
}
# Get AWS Account ID with proper error handling
Write-Host "Getting AWS Account ID..." -ForegroundColor Cyan
try {
$AccountId = aws sts get-caller-identity --query Account --output text 2>&1
if ($LASTEXITCODE -ne 0 -or -not $AccountId -or $AccountId -eq "None") {
throw "Failed to get AWS Account ID"
}
} catch {
Write-Host "Failed to get AWS Account ID. Please check your AWS credentials and network connectivity." -ForegroundColor Red
Write-Host "Error: $_" -ForegroundColor Red
exit 1
}
$FullBucketName = "$BucketName-$AccountId-$Region"
$ZipFile = "lambda.zip"
$LayerZipFile = "ddgs-layer.zip"
$LayerSource = "prerequisite/lambda/python"
$S3LayerKey = $LayerZipFile
$LambdaSrc = "prerequisite/lambda/python"
$S3Key = $ZipFile
Write-Host "Region: $Region" -ForegroundColor Green
Write-Host "Account ID: $AccountId" -ForegroundColor Green
# ----- 1. Create S3 bucket -----
Write-Host "Using S3 bucket: $FullBucketName" -ForegroundColor Cyan
try {
if ($Region -eq "us-east-1") {
aws s3api create-bucket --bucket $FullBucketName 2>$null
} else {
aws s3api create-bucket --bucket $FullBucketName --region $Region --create-bucket-configuration LocationConstraint=$Region 2>$null
}
} catch {
Write-Host "Bucket may already exist or be owned by you." -ForegroundColor Yellow
}
# ----- 2. Zip Lambda code -----
Write-Host "Zipping contents of $LambdaSrc into $ZipFile..." -ForegroundColor Cyan
Push-Location $LambdaSrc
try {
Compress-Archive -Path "." -DestinationPath "../../../$ZipFile" -Force
} catch {
Write-Host "Failed to create zip file. Ensure you have PowerShell 5.0+ or install 7-Zip." -ForegroundColor Red
exit 1
}
Pop-Location
# ----- 3. Upload to S3 -----
Write-Host "Uploading $ZipFile to s3://$FullBucketName/$S3Key..." -ForegroundColor Cyan
aws s3 cp $ZipFile "s3://$FullBucketName/$S3Key"
Write-Host "Uploading $LayerZipFile to s3://$FullBucketName/$S3LayerKey..." -ForegroundColor Cyan
Push-Location $LambdaSrc
aws s3 cp $LayerZipFile "s3://$FullBucketName/$S3LayerKey"
Pop-Location
# ----- 4. Deploy CloudFormation -----
function Deploy-Stack {
param(
[string]$StackName,
[string]$TemplateFile,
[string[]]$Parameters
)
Write-Host "Deploying CloudFormation stack: $StackName" -ForegroundColor Cyan
$deployArgs = @(
"cloudformation", "deploy",
"--stack-name", $StackName,
"--template-file", $TemplateFile,
"--s3-bucket", $FullBucketName,
"--capabilities", "CAPABILITY_NAMED_IAM",
"--region", $Region
)
if ($Parameters) {
$deployArgs += "--parameter-overrides"
$deployArgs += $Parameters
}
$output = & aws @deployArgs 2>&1
Write-Host "AWS CLI Exit Code: $LASTEXITCODE" -ForegroundColor Yellow
Write-Host "AWS CLI Output: $output" -ForegroundColor Yellow
if ($LASTEXITCODE -ne 0) {
if ($output -match "No changes to deploy") {
Write-Host "No updates for stack $StackName, continuing..." -ForegroundColor Yellow
return $true
} else {
Write-Host "Error deploying stack ${StackName}:" -ForegroundColor Red
Write-Host $output -ForegroundColor Red
return $false
}
} else {
Write-Host "Stack $StackName deployed successfully." -ForegroundColor Green
return $true
}
}
# ----- Run both stacks -----
Write-Host "Starting deployment of infrastructure stack with LambdaS3Bucket = $FullBucketName..." -ForegroundColor Cyan
$infraParams = @("LambdaS3Bucket=$FullBucketName", "LambdaS3Key=$S3Key", "LayerS3Key=$S3LayerKey")
$infraSuccess = Deploy-Stack -StackName $InfraStackName -TemplateFile $InfraTemplateFile -Parameters $infraParams
Write-Host "Starting deployment of Cognito stack..." -ForegroundColor Cyan
$cognitoParams = @("UserPoolName=CustomerSupportGatewayPool", "MachineAppClientName=CustomerSupportMachineClient", "WebAppClientName=CustomerSupportWebClient")
$cognitoSuccess = Deploy-Stack -StackName $CognitoStackName -TemplateFile $CognitoTemplateFile -Parameters $cognitoParams
if ($infraSuccess -and $cognitoSuccess) {
Write-Host "Deployment complete." -ForegroundColor Green
} else {
Write-Host "Deployment failed." -ForegroundColor Red
exit 1
}
+133
View File
@@ -0,0 +1,133 @@
#!/bin/sh
# Enable strict error handling
set -euo pipefail
# ----- Config -----
BUCKET_NAME=${1:-customersupport112}
INFRA_STACK_NAME=${2:-CustomerSupportStackInfra}
COGNITO_STACK_NAME=${3:-CustomerSupportStackCognito}
INFRA_TEMPLATE_FILE="prerequisite/infrastructure.yaml"
COGNITO_TEMPLATE_FILE="prerequisite/cognito.yaml"
# First try to get region from environment variable
if [ -z "${AWS_REGION-}" ]; then
# If AWS_REGION is not set, try to get it from AWS CLI config
REGION=$(aws configure get region 2>/dev/null || echo "us-west-2")
# Export it as an environment variable
export AWS_REGION="${REGION}"
fi
echo "Region is set to: ${AWS_REGION}"
export REGION="${AWS_REGION}"
# Get AWS Account ID with proper error handling
echo "🔍 Getting AWS Account ID..."
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text 2>&1)
if [ $? -ne 0 ] || [ -z "$ACCOUNT_ID" ] || [ "$ACCOUNT_ID" = "None" ]; then
echo "❌ Failed to get AWS Account ID. Please check your AWS credentials and network connectivity."
echo "Error: $ACCOUNT_ID"
exit 1
fi
FULL_BUCKET_NAME="${BUCKET_NAME}-${ACCOUNT_ID}-${REGION}"
ZIP_FILE="lambda.zip"
LAYER_ZIP_FILE="ddgs-layer.zip"
LAYER_SOURCE="prerequisite/lambda/python"
S3_LAYER_KEY="${LAYER_ZIP_FILE}"
LAMBDA_SRC="prerequisite/lambda/python"
S3_KEY="${ZIP_FILE}"
USER_POOL_NAME="CustomerSupportGatewayPool"
MACHINE_APP_CLIENT_NAME="CustomerSupportMachineClient"
WEB_APP_CLIENT_NAME="CustomerSupportWebClient"
echo "Region: $REGION"
echo "Account ID: $ACCOUNT_ID"
# ----- 1. Create S3 bucket -----
echo "🪣 Using S3 bucket: $FULL_BUCKET_NAME"
if [ "$REGION" = "us-east-1" ]; then
aws s3api create-bucket \
--bucket "$FULL_BUCKET_NAME" \
2>/dev/null || echo "️ Bucket may already exist or be owned by you."
else
aws s3api create-bucket \
--bucket "$FULL_BUCKET_NAME" \
--region "$REGION" \
--create-bucket-configuration LocationConstraint="$REGION" \
2>/dev/null || echo "️ Bucket may already exist or be owned by you."
fi
# ----- Verify S3 bucket ownership -----
echo "🔍 Verifying S3 bucket ownership..."
aws s3api head-bucket --bucket "$FULL_BUCKET_NAME" --expected-bucket-owner "$ACCOUNT_ID"
if [ $? -ne 0 ]; then
echo "❌ S3 bucket $FULL_BUCKET_NAME is not owned by account $ACCOUNT_ID"
exit 1
fi
echo "✅ S3 bucket ownership verified"
# ----- 2. Zip Lambda code -----
sudo apt install zip
echo "📦 Zipping contents of $LAMBDA_SRC into $ZIP_FILE..."
cd "$LAMBDA_SRC"
zip -r "../../../$ZIP_FILE" . > /dev/null
cd - > /dev/null
# ----- 3. Upload to S3 -----
echo "☁️ Uploading $ZIP_FILE to s3://$FULL_BUCKET_NAME/$S3_KEY..."
aws s3api put-object --bucket "$FULL_BUCKET_NAME" --key "$S3_KEY" --body "$ZIP_FILE" --expected-bucket-owner "$ACCOUNT_ID"
echo "☁️ Uploading $LAYER_ZIP_FILE to s3://$FULL_BUCKET_NAME/$S3_LAYER_KEY..."
cd "$LAMBDA_SRC"
aws s3api put-object --bucket "$FULL_BUCKET_NAME" --key "$S3_LAYER_KEY" --body "$LAYER_ZIP_FILE" --expected-bucket-owner "$ACCOUNT_ID"
cd - > /dev/null
# ----- 4. Deploy CloudFormation -----
deploy_stack() {
set +e
local stack_name=$1
local template_file=$2
shift 2
local params=("$@")
echo "🚀 Deploying CloudFormation stack: $stack_name"
output=$(aws cloudformation deploy \
--stack-name "$stack_name" \
--template-file "$template_file" \
--s3-bucket "$FULL_BUCKET_NAME" \
--capabilities CAPABILITY_NAMED_IAM \
--region "$REGION" \
"${params[@]}" 2>&1)
exit_code=$?
echo "$output"
if [ $exit_code -ne 0 ]; then
if echo "$output" | grep -qi "No changes to deploy"; then
echo "️ No updates for stack $stack_name, continuing..."
return 0
else
echo "❌ Error deploying stack $stack_name:"
echo "$output"
return $exit_code
fi
else
echo "✅ Stack $stack_name deployed successfully."
return 0
fi
}
# ----- Run both stacks -----
echo "🔧 Starting deployment of infrastructure stack with LambdaS3Bucket = $FULL_BUCKET_NAME..."
deploy_stack "$INFRA_STACK_NAME" "$INFRA_TEMPLATE_FILE" --parameter-overrides LambdaS3Bucket="$FULL_BUCKET_NAME" LambdaS3Key="$S3_KEY" LayerS3Key="$S3_LAYER_KEY"
infra_exit_code=$?
echo "🔧 Starting deployment of Cognito stack..."
deploy_stack "$COGNITO_STACK_NAME" "$COGNITO_TEMPLATE_FILE" --parameter-overrides UserPoolName="$USER_POOL_NAME" MachineAppClientName="$MACHINE_APP_CLIENT_NAME" WebAppClientName="$WEB_APP_CLIENT_NAME"
cognito_exit_code=$?
echo "✅ Deployment complete."
+2
View File
@@ -98,4 +98,6 @@
- Will Ensor
- Osman Santos
- David Kaleko
- Diego Brasil
- Dumitru Pascu (dumip)