diff --git a/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/06-guardrail-middleware/README.md b/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/06-guardrail-middleware/README.md new file mode 100644 index 000000000..afa4fa9e0 --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/06-guardrail-middleware/README.md @@ -0,0 +1,316 @@ +# AWS Bedrock AgentCore Guardrail Middleware Tutorial + +This tutorial demonstrates how to implement AWS Bedrock Guardrails as middleware in a Strands agent deployed on AWS Bedrock AgentCore Runtime. The solution provides content moderation for both inputs and outputs using Starlette middleware pattern. + +## ๐ŸŽฏ Overview + +This implementation showcases: +- **Guardrail Middleware**: Custom Starlette middleware that intercepts all requests and responses +- **Content Filtering**: Blocks inappropriate content (hate speech, insults, violence, etc.) +- **Smart Detection**: Handles false positives intelligently (e.g., allows math questions) +- **CORS Support**: Configured for browser-based applications +- **Enhanced Logging**: Detailed capture of inputs/outputs for debugging + +## ๐Ÿ“ Files + +``` +06-guardrail-middleware/ +โ”œโ”€โ”€ simple_agent.py # Main agent with guardrail & CORS middleware +โ”œโ”€โ”€ app.py # Streamlit UI for testing +โ”œโ”€โ”€ deploy_simple_agent.py # Deployment script with automatic IAM setup +โ”œโ”€โ”€ cleanup_all.py # Cleanup script for all resources +โ”œโ”€โ”€ test_web_search.py # Test script for web search functionality +โ”œโ”€โ”€ simple_requirements.txt # Agent dependencies +โ”œโ”€โ”€ streamlit_requirements.txt # UI dependencies +โ””โ”€โ”€ README.md # This file +``` + +## ๐Ÿ—๏ธ Architecture + +``` +User Request + โ†“ +Streamlit UI (app.py) + โ†“ +AWS Bedrock AgentCore Runtime + โ†“ +CORS Middleware (browser support) + โ†“ +Guardrail Middleware โ† AWS Bedrock Guardrails API + โ”œโ”€ Input Validation + โ”œโ”€ Process Request โ†’ Strands Agent + โ””โ”€ Output Validation + โ†“ +Response to User +``` + +## โœจ Features + +### 1. **Guardrail Protection** +- Validates all inputs before processing +- Validates all outputs before returning +- Uses AWS Bedrock Guardrails +- Configurable policies and filters + +### 2. **Smart Filtering** +- **Blocks**: Hate speech, insults, violence, sexual content, misconduct +- **Allows**: Normal queries, math questions, weather requests, web searches +- **Handles False Positives**: Ignores low-confidence PROMPT_ATTACK flags + +### 3. **Web Search Integration (NEW!)** +- **Tavily API Integration**: Real-time web search capabilities +- **Intelligent Routing**: Automatically uses web search for general knowledge questions +- **Formatted Results**: Clean presentation of search results with titles, URLs, and content +- **Optional Feature**: Works without Tavily API key (falls back gracefully) + +### 4. **CORS Configuration** +- Full CORS middleware support +- OPTIONS handler for preflight requests +- Ready for browser-based applications + +### 5. **Enhanced Logging** +- Captures and displays all inputs/outputs +- Shows validation decisions and reasons +- Visual formatting for clarity + +## ๐Ÿ“‹ Prerequisites + +1. **AWS Account** with: + - AWS Bedrock access + - AWS Bedrock AgentCore access + - Configured AWS credentials + +2. **Local Tools**: + ```bash + # Required + - Python 3.8+ + - AWS CLI configured + - pip or conda + + # Install SDK + pip install bedrock-agentcore-starter-toolkit + ``` + +3. **AWS Guardrail**: + - The deployment script automatically creates a guardrail with appropriate filters + - Guardrail configuration includes: HATE, INSULTS, VIOLENCE, SEXUAL, MISCONDUCT, and Harmful Content topic + - The guardrail ID is dynamically managed and stored in Parameter Store + +4. **Tavily API (Optional - for web search)**: + - Sign up at [app.tavily.com](https://app.tavily.com/home/) for a free API key + - Set the environment variable: `export TAVILY_API_KEY=your-api-key` + - The agent will work without this, but web search will be unavailable + +## ๐Ÿš€ Deployment + +### 1. Configure Tavily API (Optional) + +If you want web search capabilities: +```bash +# Sign up at https://app.tavily.com/home/ for a free API key +export TAVILY_API_KEY=your-tavily-api-key-here +``` + +### 2. Deploy the Agent + +```bash +cd 06-guardrail-middleware +python deploy_simple_agent.py +``` + +This will: +- Create a new guardrail (or reuse existing one from SSM Parameter Store) +- Save the guardrail ID to SSM Parameter Store (`/simple_agent/guardrail_id`) +- Save Tavily API key to SSM Parameter Store if provided (as SecureString) +- Build the Docker container in AWS CodeBuild (ARM64) +- Deploy to AWS Bedrock AgentCore Runtime +- Automatically find and update the execution role with required permissions: + - `bedrock:ApplyGuardrail` and `bedrock:GetGuardrail` for guardrails + - `ssm:GetParameter` for reading configuration from Parameter Store + - `kms:Decrypt` for SecureString parameters +- Store all configuration in SSM Parameter Store for reuse + +### 3. Launch Streamlit UI + +```bash +streamlit run app.py +``` + +Opens at http://localhost:8501 + +## ๐Ÿงช Testing + +### Test via Streamlit UI + +1. **Normal Queries** (Should Work): + - "What's 2+2?" โ†’ Returns: "The result is: 4" + - "Calculate 25 * 4" โ†’ Returns: "The result is: 100" + - "What's the weather in Seattle?" โ†’ Returns weather info + +2. **Web Search Queries** (Should Work with Tavily API): + - "What are the latest AI developments?" โ†’ Returns web search results + - "Who won the Nobel Prize in Physics 2024?" โ†’ Returns current information + - "What is quantum computing?" โ†’ Returns detailed explanations from the web + - "Latest news about climate change" โ†’ Returns recent articles and information + +3. **Inappropriate Content** (Should Block): + - "I hate you!!" โ†’ Blocked with warning message + - Explicit profanity โ†’ Blocked + - Insults โ†’ Blocked + +### Test Web Search Functionality + +```bash +python test_web_search.py +``` + +This will: +- Test Tavily API directly +- Test the agent's web search integration +- Show formatted search results + +## ๐Ÿ”ง How It Works + +### Dynamic Configuration Management + +The deployment script automatically: +1. Creates a guardrail with comprehensive filters (or reuses existing) +2. Stores the guardrail ID in SSM Parameter Store (`/simple_agent/guardrail_id`) +3. Stores Tavily API key in SSM Parameter Store as SecureString (if provided) +4. Agent reads configuration from SSM at runtime - no hardcoding! + +```python +# Configuration is dynamically loaded from SSM Parameter Store +try: + ssm_client = boto3.client('ssm') + response = ssm_client.get_parameter(Name='/simple_agent/guardrail_id') + GUARDRAIL_ID = response['Parameter']['Value'] + + # Also try to load Tavily API key from SSM + try: + response = ssm_client.get_parameter( + Name='/simple_agent/tavily_api_key', + WithDecryption=True + ) + TAVILY_API_KEY = response['Parameter']['Value'] + except: + TAVILY_API_KEY = os.environ.get('TAVILY_API_KEY') +except: + # Fail safely if SSM is not available + sys.exit(1) +``` + +### IAM Permission Management + +The deployment script automatically: +1. Identifies the most recently created execution role +2. Adds inline policy with all required permissions +3. Waits for permissions to propagate before completing +4. No manual IAM configuration needed! + + +### Middleware Implementation + +```python +class GuardrailMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + # 1. Extract prompt from request + prompt_text = extract_prompt(request) + + # 2. Validate input with guardrail + if guardrail_blocks_input(prompt_text): + return blocked_response() + + # 3. Process request + response = await call_next(request) + + # 4. Validate output with guardrail + if guardrail_blocks_output(response): + return safe_response() + + # 5. Return validated response + return response +``` + +### Guardrail Logic + +The middleware checks both: +1. **topicPolicy**: Topics like "Harmful Content" +2. **contentPolicy**: Filters like HATE, INSULTS, VIOLENCE + +```python +# Smart filtering logic +if filter_type in ['HATE', 'INSULTS', 'VIOLENCE', 'SEXUAL', 'MISCONDUCT']: + should_block = True +elif filter_type == 'PROMPT_ATTACK' and confidence in ['MEDIUM', 'HIGH']: + should_block = True +else: + # Allow low-confidence prompt attacks (usually false positives) + should_block = False +``` + + +## ๐Ÿ—‘๏ธ Cleanup + +To remove all resources: + +```bash +python cleanup_all.py +``` + +This will: +- Delete the AgentCore Runtime and endpoints +- Delete the created guardrail +- Remove all Parameter Store entries including: + - `/simple_agent/guardrail_id` + - `/simple_agent/runtime/*` + - `/simple_agent/tavily_api_key` (SecureString) +- Delete ECR repository +- Remove IAM roles and inline policies +- Clean up all related resources + +### Tool Integration + +The agent now includes three tools that are automatically selected based on the user's query: + +```python +# Available tools +@tool +def calculate(expression: str) -> str: + """Mathematical calculations with safe evaluation""" + +@tool +def get_weather(location: str) -> str: + """Weather information for any location""" + +@tool +def web_search(query: str) -> str: + """Web search via Tavily API for general knowledge questions""" +``` + +The agent intelligently routes queries to the appropriate tool: +- Math questions โ†’ `calculate` tool +- Weather queries โ†’ `get_weather` tool +- Everything else โ†’ `web_search` tool (if Tavily API is configured) + +## ๐ŸŽ“ Key Learnings + +1. **Middleware Pattern**: How to implement custom middleware with Starlette +2. **Guardrail Integration**: Using AWS Bedrock Guardrails API +3. **Tool Integration**: Adding external API tools (Tavily) to Strands agents +4. **Dynamic Tool Selection**: Agent intelligently routes queries to appropriate tools +5. **CORS Configuration**: Enabling browser support +6. **Error Handling**: Fail-open design for service resilience +7. **Logging**: Comprehensive logging for debugging + +## ๐Ÿ“š Additional Resources + +- [AWS Bedrock Guardrails Documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html) +- [AWS Bedrock AgentCore Documentation](https://docs.aws.amazon.com/bedrock-agentcore/) +- [Starlette Middleware Documentation](https://www.starlette.io/middleware/) +- [Strands Agents Documentation](https://github.com/BenevolenceMessiah/strands-agents) +- [Tavily API Documentation](https://docs.tavily.com/) + +## ๐Ÿ“ License + +This sample code is provided under the MIT-0 License. See the LICENSE file. diff --git a/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/06-guardrail-middleware/app.py b/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/06-guardrail-middleware/app.py new file mode 100644 index 000000000..cc0bef3f9 --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/06-guardrail-middleware/app.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 +""" +Streamlit app for Agent with Guardrails +Matches the working test_simple_agent.py pattern +""" + +import streamlit as st +import boto3 +import json +import time + +# Configure page +st.set_page_config( + page_title="Agent with Guardrails", + page_icon="๐Ÿ›ก๏ธ", + layout="wide" +) + +# Custom CSS +st.markdown(""" + +""", unsafe_allow_html=True) + +# Initialize session state +if 'messages' not in st.session_state: + st.session_state.messages = [] + +if 'agent_arn' not in st.session_state: + try: + ssm = boto3.client('ssm') + response = ssm.get_parameter(Name='/simple_agent/runtime/agent_arn') + st.session_state.agent_arn = response['Parameter']['Value'] + except: + st.session_state.agent_arn = None + +# Header +st.title("๐Ÿ›ก๏ธ Agent with Guardrails") +st.markdown("*Powered by AWS Bedrock AgentCore Runtime*") +st.markdown("---") + +# Initialize guardrail logs in session state +if 'guardrail_logs' not in st.session_state: + st.session_state.guardrail_logs = [] + +# Sidebar +with st.sidebar: + st.header("Configuration") + if st.session_state.agent_arn: + st.success("โœ… Agent Connected") + st.code(st.session_state.agent_arn, language=None) + else: + st.error("โŒ Agent not deployed") + st.stop() + + st.markdown("---") + st.markdown("### ๐Ÿ›ก๏ธ Guardrail Protection") + st.info("All inputs and outputs are validated by AWS Bedrock Guardrails") + + # Show guardrail activity log + st.markdown("#### ๐Ÿ“Š Guardrail Activity") + if st.session_state.guardrail_logs: + for log in st.session_state.guardrail_logs[-5:]: # Show last 5 activities + if log['type'] == 'input': + st.success(f"โœ… Input validated: {log['time']}") + elif log['type'] == 'output': + st.success(f"โœ… Output validated: {log['time']}") + elif log['type'] == 'blocked': + st.error(f"๐Ÿšซ Blocked: {log['time']}") + else: + st.text("No activity yet") + + if st.button("Clear Chat"): + st.session_state.messages = [] + st.session_state.guardrail_logs = [] + st.rerun() + +# Display chat history +for message in st.session_state.messages: + with st.chat_message(message["role"]): + st.markdown(message["content"]) + +# Chat input +if prompt := st.chat_input("Ask me anything..."): + # Add user message + st.session_state.messages.append({"role": "user", "content": prompt}) + with st.chat_message("user"): + st.markdown(prompt) + + # Get agent response + with st.chat_message("assistant"): + message_placeholder = st.empty() + + try: + # Create client + client = boto3.client('bedrock-agentcore') + + # Log input validation (happens in middleware) + from datetime import datetime + st.session_state.guardrail_logs.append({ + 'type': 'input', + 'time': datetime.now().strftime("%H:%M:%S"), + 'prompt': prompt[:50] + "..." if len(prompt) > 50 else prompt + }) + + # Invoke agent - payload must be bytes + payload_str = json.dumps({"prompt": prompt}) + payload_bytes = payload_str.encode('utf-8') + + with st.spinner("๐Ÿ›ก๏ธ Validating input with guardrails..."): + response = client.invoke_agent_runtime( + agentRuntimeArn=st.session_state.agent_arn, + qualifier="DEFAULT", + payload=payload_bytes + ) + + # Stream response - response['response'] yields bytes directly + full_response = "" + if "response" in response: + try: + for chunk in response["response"]: + # Each chunk is bytes, decode it + if isinstance(chunk, bytes): + decoded = chunk.decode("utf-8") + + # Check if content was blocked (starts with โš ๏ธ) + if decoded.startswith("โš ๏ธ"): + # Content was blocked by guardrail + st.session_state.guardrail_logs.append({ + 'type': 'blocked', + 'time': datetime.now().strftime("%H:%M:%S"), + 'reason': 'Input violated content policies' + }) + message_placeholder.warning(decoded) + # Skip the normal flow + st.session_state.messages.append({ + "role": "assistant", + "content": decoded + }) + full_response = decoded # Set to prevent further processing + break # Break out of the loop + + full_response += decoded + # Show with cursor for streaming effect + message_placeholder.markdown(full_response + "โ–Œ") + time.sleep(0.02) + except Exception as stream_error: + # Just raise the error - don't assume it's a guardrail block + raise stream_error + + # Only process normal responses if not blocked + if full_response and not full_response.startswith("โš ๏ธ"): + # Final display without cursor + message_placeholder.markdown(full_response) + + # Log output validation (happens in middleware) + st.session_state.guardrail_logs.append({ + 'type': 'output', + 'time': datetime.now().strftime("%H:%M:%S"), + 'response': full_response[:50] + "..." if len(full_response) > 50 else full_response + }) + + # Add to history + st.session_state.messages.append({ + "role": "assistant", + "content": full_response + }) + + # Show guardrail info + with st.expander("๐Ÿ›ก๏ธ Guardrail Validation Details"): + st.success("โœ… Input passed guardrail validation") + st.success("โœ… Output passed guardrail validation") + st.info("Note: The agent uses Starlette middleware to intercept and validate all inputs/outputs using AWS Bedrock Guardrails API") + + except Exception as e: + import traceback + error_msg = f"โŒ Error: {str(e)}\n\n```\n{traceback.format_exc()}\n```" + message_placeholder.error(error_msg) + st.session_state.messages.append({ + "role": "assistant", + "content": f"โŒ Error: {str(e)}" + }) + +# Example prompts +if not st.session_state.messages: + st.markdown("### Try these examples:") + col1, col2, col3 = st.columns(3) + + with col1: + if st.button("๐ŸŒค๏ธ Weather"): + st.session_state.messages.append({ + "role": "user", + "content": "What is the weather in Seattle?" + }) + st.rerun() + + with col2: + if st.button("๐Ÿ”ข Math"): + st.session_state.messages.append({ + "role": "user", + "content": "Calculate 25 * 4 + 10" + }) + st.rerun() + + with col3: + if st.button("๐Ÿ’ฌ About Bedrock"): + st.session_state.messages.append({ + "role": "user", + "content": "Tell me about AWS Bedrock" + }) + st.rerun() diff --git a/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/06-guardrail-middleware/cleanup_all.py b/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/06-guardrail-middleware/cleanup_all.py new file mode 100644 index 000000000..35c66ce60 --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/06-guardrail-middleware/cleanup_all.py @@ -0,0 +1,439 @@ +#!/usr/bin/env python3 +""" +Cleanup all resources for Simple Agent with Guardrail +Using the correct API from the notebook example +""" + +import boto3 +import time +import os + +# Ensure we're using the correct region +AWS_REGION = os.environ.get('AWS_REGION', 'us-west-2') + +def cleanup_all(): + """Clean up all resources""" + + print("๐Ÿงน Cleaning up all resources...") + print(f"๐Ÿ“ Region: {AWS_REGION}") + print("=" * 60) + + # Initialize clients with explicit region + ssm = boto3.client('ssm', region_name=AWS_REGION) + bedrock = boto3.client('bedrock', region_name=AWS_REGION) + + # Use bedrock-agentcore-control for runtime deletion (as per notebook) + agentcore_control_client = boto3.client( + 'bedrock-agentcore-control', + region_name=AWS_REGION + ) + + ecr_client = boto3.client('ecr', region_name=AWS_REGION) + iam = boto3.client('iam') # IAM is global + + # 1. Delete AgentCore Runtime Endpoints and Runtimes + print("\n1. Finding and deleting AgentCore Runtime Endpoints and Runtimes...") + + # First check if we have any stored runtime IDs in SSM + print("\n๐Ÿ“ Step 1a: Checking for stored runtime information...") + stored_runtime_id = None + + try: + response = ssm.get_parameter(Name='/simple_agent/runtime/agent_id') + stored_runtime_id = response['Parameter']['Value'] + print(f"๐Ÿ“‹ Found stored runtime ID in Parameter Store: {stored_runtime_id}") + + # Try to check if this runtime still exists + try: + runtime_details = agentcore_control_client.get_agent_runtime( + agentRuntimeId=stored_runtime_id + ) + print(f" โœ… Runtime still exists: {stored_runtime_id}") + except Exception as e: + if 'ResourceNotFoundException' in str(e): + print(f" โ„น๏ธ Runtime no longer exists: {stored_runtime_id}") + stored_runtime_id = None + else: + print(f" โš ๏ธ Error checking runtime: {e}") + except: + print("โ„น๏ธ No stored runtime ID found in Parameter Store") + + endpoints_deleted = 0 + + # Note: list_agent_runtime_endpoints requires a runtime ID, so we can only check if we have runtimes + print("\n๐Ÿ“ Step 1b: Checking for AgentCore Runtime Endpoints...") + print("โ„น๏ธ Note: Endpoint listing requires a runtime ID") + + # Now handle runtimes + print("\n๐Ÿ“ Step 1b: Deleting AgentCore Runtimes...") + runtime_deleted = False + runtimes_to_delete = [] + + try: + # List all agent runtimes using the correct API + print("๐Ÿ” Listing all agent runtimes...") + + # Initial request + response = agentcore_control_client.list_agent_runtimes() + # The response key can be 'agentRuntimeSummaries' or 'agentRuntimes' depending on the API version + all_runtimes = response.get('agentRuntimeSummaries', response.get('agentRuntimes', [])) + + # Handle pagination + while 'nextToken' in response: + response = agentcore_control_client.list_agent_runtimes( + nextToken=response['nextToken'] + ) + all_runtimes.extend(response.get('agentRuntimeSummaries', response.get('agentRuntimes', []))) + + print(f"๐Ÿ“‹ Found {len(all_runtimes)} runtime(s) total") + + # Display all runtimes for debugging + for runtime in all_runtimes: + runtime_id = runtime.get('agentRuntimeId', '') + runtime_name = runtime.get('agentRuntimeName', '') + runtime_status = runtime.get('status', '') + + print(f" Runtime: {runtime_id} (Name: {runtime_name}, Status: {runtime_status})") + + # Check if this is our runtime (be generous with matching) + if ('simple' in runtime_id.lower() or + 'simple' in runtime_name.lower() or + runtime_id.lower().startswith('simple_agent') or + 'guardrail' in runtime_name.lower() or # Also check for guardrail-related runtimes + 'middleware' in runtime_name.lower()): + runtimes_to_delete.append(runtime) + + # Process runtimes for deletion + for runtime in runtimes_to_delete: + runtime_id = runtime.get('agentRuntimeId', '') + runtime_name = runtime.get('agentRuntimeName', '') + runtime_status = runtime.get('status', '') + + print(f"\n๐ŸŽฏ Processing runtime for deletion: {runtime_id}") + print(f" Name: {runtime_name}") + print(f" Status: {runtime_status}") + + # Get runtime details + try: + runtime_details = agentcore_control_client.get_agent_runtime( + agentRuntimeId=runtime_id + ) + print(f" Details retrieved successfully") + + # Check if runtime has endpoints that need deletion first + try: + print(f" ๐Ÿ” Checking for endpoints...") + endpoint_response = agentcore_control_client.list_agent_runtime_endpoints( + agentRuntimeId=runtime_id + ) + runtime_endpoints = endpoint_response.get('agentRuntimeEndpointSummaries', []) + + # Handle pagination for endpoints + while 'nextToken' in endpoint_response: + endpoint_response = agentcore_control_client.list_agent_runtime_endpoints( + agentRuntimeId=runtime_id, + nextToken=endpoint_response['nextToken'] + ) + runtime_endpoints.extend(endpoint_response.get('agentRuntimeEndpointSummaries', [])) + + if runtime_endpoints: + print(f" ๐Ÿ“ Found {len(runtime_endpoints)} endpoint(s) for this runtime") + + for endpoint in runtime_endpoints: + endpoint_id = endpoint.get('agentRuntimeEndpointId', '') + endpoint_name = endpoint.get('agentRuntimeEndpointName', '') + + print(f" Deleting endpoint: {endpoint_id} ({endpoint_name})") + + try: + # Get endpoint details first + endpoint_details = agentcore_control_client.get_agent_runtime_endpoint( + agentRuntimeId=runtime_id, + agentRuntimeEndpointId=endpoint_id + ) + + # Delete the endpoint + agentcore_control_client.delete_agent_runtime_endpoint( + agentRuntimeId=runtime_id, + agentRuntimeEndpointId=endpoint_id + ) + print(f" โœ… Deleted endpoint: {endpoint_id}") + time.sleep(2) + except Exception as e: + print(f" โš ๏ธ Could not delete endpoint {endpoint_id}: {e}") + else: + print(f" โ„น๏ธ No endpoints found for this runtime") + + except Exception as e: + print(f" โ„น๏ธ Could not check/delete endpoints: {e}") + + except Exception as e: + print(f" โš ๏ธ Could not get runtime details: {e}") + + # Now delete the runtime + try: + print(f" ๐Ÿ—‘๏ธ Attempting to delete runtime...") + runtime_delete_response = agentcore_control_client.delete_agent_runtime( + agentRuntimeId=runtime_id + ) + print(f" โœ… Successfully deleted AgentCore Runtime: {runtime_id}") + runtime_deleted = True + time.sleep(5) # Wait between runtime deletions + except Exception as e: + print(f" โš ๏ธ Could not delete runtime {runtime_id}: {e}") + + if runtime_deleted: + print("\nโณ Waiting for runtime deletion to complete...") + time.sleep(20) + elif not runtimes_to_delete: + print("\nโ„น๏ธ No matching agent runtimes found to delete") + + except Exception as e: + print(f"โš ๏ธ Error listing/deleting runtimes: {e}") + + # Fallback to Parameter Store method + print("\n๐Ÿ“ Trying Parameter Store fallback method...") + + try: + response = ssm.get_parameter(Name='/simple_agent/runtime/agent_id') + agent_id = response['Parameter']['Value'] + print(f"๐Ÿ“‹ Found agent ID in Parameter Store: {agent_id}") + + # Try to get runtime details first + try: + runtime_details = agentcore_control_client.get_agent_runtime( + agentRuntimeId=agent_id + ) + print(f" Runtime exists with ID: {agent_id}") + + # Delete any endpoints first + try: + endpoint_response = agentcore_control_client.list_agent_runtime_endpoints( + agentRuntimeId=agent_id + ) + for endpoint in endpoint_response.get('agentRuntimeEndpointSummaries', []): + endpoint_id = endpoint.get('agentRuntimeEndpointId', '') + try: + agentcore_control_client.delete_agent_runtime_endpoint( + agentRuntimeId=agent_id, + agentRuntimeEndpointId=endpoint_id + ) + print(f" โœ… Deleted endpoint: {endpoint_id}") + except: + pass + except: + pass + + # Delete the runtime + runtime_delete_response = agentcore_control_client.delete_agent_runtime( + agentRuntimeId=agent_id + ) + print(f"โœ… Deleted AgentCore Runtime: {agent_id}") + runtime_deleted = True + print("โณ Waiting for deletion to complete...") + time.sleep(20) + except Exception as e: + print(f"โš ๏ธ Could not delete runtime {agent_id}: {e}") + + except Exception as e: + print(f"โ„น๏ธ No agent ID in Parameter Store or error: {e}") + + if not runtime_deleted and not endpoints_deleted: + print("\nโš ๏ธ No AgentCore Runtimes or Endpoints were deleted") + print(" Please check the AWS Console for any remaining resources") + print(f" Console: https://console.aws.amazon.com/bedrock/home?region={AWS_REGION}#/agent-core") + + # 2. Delete Guardrail - Find ALL guardrails and delete the right ones + print("\n2. Finding and deleting Guardrails...") + guardrails_deleted = 0 + + # Known guardrail IDs and names + KNOWN_GUARDRAIL_IDS = ['fkducf9q8z1a'] # web-search-mcp-guardrail + KNOWN_GUARDRAIL_NAMES = ['web-search-mcp-guardrail', 'GuardrailMiddlewareTutorial', 'simple-agent-guardrail'] + + try: + # List all guardrails + response = bedrock.list_guardrails(maxResults=100) + guardrails = response.get('guardrails', []) + + print(f"๐Ÿ“‹ Found {len(guardrails)} guardrail(s)") + + for guardrail in guardrails: + name = guardrail.get('name', '') + guardrail_id = guardrail.get('id') + + # Delete if it matches our patterns + should_delete = False + + # Check by ID + if guardrail_id in KNOWN_GUARDRAIL_IDS: + should_delete = True + print(f"๐ŸŽฏ Found by ID: {name} ({guardrail_id})") + + # Check by name patterns + name_lower = name.lower() + for known_name in KNOWN_GUARDRAIL_NAMES: + if known_name.lower() in name_lower or name_lower in known_name.lower(): + should_delete = True + print(f"๐ŸŽฏ Found by name: {name} ({guardrail_id})") + break + + # Also check for tutorial/middleware related names + if any(x in name_lower for x in ['guardrailmiddleware', 'middleware', 'tutorial', 'simple-agent', 'simple_agent']): + should_delete = True + print(f"๐ŸŽฏ Found by pattern: {name} ({guardrail_id})") + + if should_delete: + try: + bedrock.delete_guardrail(guardrailIdentifier=guardrail_id) + print(f"โœ… Deleted Guardrail: {name} (ID: {guardrail_id})") + guardrails_deleted += 1 + except Exception as e: + print(f"โš ๏ธ Could not delete guardrail {guardrail_id}: {e}") + + if guardrails_deleted == 0: + print("\n๐Ÿ“ All guardrails (none matched our patterns):") + for g in guardrails: + print(f" - {g.get('name')} (ID: {g.get('id')})") + + except Exception as e: + print(f"โš ๏ธ Could not list guardrails: {e}") + + # 3. Delete Parameter Store entries + print("\n3. Deleting Parameter Store entries...") + params_deleted = 0 + + # List all parameters and delete ones related to simple_agent + try: + paginator = ssm.get_paginator('describe_parameters') + for page in paginator.paginate(): + for param in page['Parameters']: + param_name = param['Name'] + if 'simple_agent' in param_name or 'simple-agent' in param_name: + try: + # Special note for Tavily API key + if 'tavily_api_key' in param_name: + print(f"๐Ÿ”‘ Deleting Tavily API key: {param_name}") + ssm.delete_parameter(Name=param_name) + print(f"โœ… Deleted parameter: {param_name}") + params_deleted += 1 + except: + pass + + # Also explicitly try to delete the Tavily API key if it wasn't found in the list + try: + ssm.delete_parameter(Name='/simple_agent/tavily_api_key') + print(f"โœ… Deleted Tavily API key parameter") + params_deleted += 1 + except: + pass # It's OK if it doesn't exist + + if params_deleted == 0: + print("โ„น๏ธ No parameters found to delete") + + except Exception as e: + print(f"โ„น๏ธ Could not list/delete parameters: {e}") + + # 4. Delete ECR repositories + print("\n4. Deleting ECR repositories...") + repos_deleted = 0 + + try: + response = ecr_client.describe_repositories() + for repo in response.get('repositories', []): + repo_name = repo['repositoryName'] + + # Check if it's related to our agent + if any(x in repo_name.lower() for x in ['simple_agent', 'simple-agent', 'simpleagent', 'bedrock-agentcore-simple']): + try: + ecr_client.delete_repository(repositoryName=repo_name, force=True) + print(f"โœ… Deleted ECR repository: {repo_name}") + repos_deleted += 1 + except Exception as e: + print(f"โ„น๏ธ Could not delete ECR repo {repo_name}: {e}") + + if repos_deleted == 0: + print("โ„น๏ธ No ECR repositories found to delete") + + except Exception as e: + print(f"โ„น๏ธ Could not list ECR repositories: {e}") + + # 5. Delete IAM roles + print("\n5. Cleaning up IAM roles...") + roles_deleted = 0 + + try: + paginator = iam.get_paginator('list_roles') + + for page in paginator.paginate(): + for role in page['Roles']: + role_name = role['RoleName'] + role_name_lower = role_name.lower() + + # Check if it's related to our agent + should_delete = False + + # Direct name matches + if any(x in role_name_lower for x in ['simple-agent', 'simple_agent', 'simpleagent']): + should_delete = True + + # BedrockAgentCore SDK roles with our IDs + elif 'bedrockagentcore' in role_name_lower and any(x in role_name_lower for x in ['9d6fae7ebe', 'icr44egxtf']): + should_delete = True + + # Roles with simple in SDK name + elif 'amazonbedrockagentcoresdk' in role_name_lower and 'simple' in role_name_lower: + should_delete = True + + if should_delete: + try: + # Delete inline policies + response = iam.list_role_policies(RoleName=role_name) + for policy_name in response.get('PolicyNames', []): + iam.delete_role_policy(RoleName=role_name, PolicyName=policy_name) + + # Detach managed policies + response = iam.list_attached_role_policies(RoleName=role_name) + for policy in response.get('AttachedPolicies', []): + iam.detach_role_policy(RoleName=role_name, PolicyArn=policy['PolicyArn']) + + # Delete role + iam.delete_role(RoleName=role_name) + print(f"โœ… Deleted IAM role: {role_name}") + roles_deleted += 1 + except Exception as e: + pass + + if roles_deleted == 0: + print("โ„น๏ธ No IAM roles found to delete") + + except Exception as e: + print(f"โ„น๏ธ Could not clean IAM roles: {e}") + + # 6. Show summary + print("\n" + "=" * 60) + print("๐Ÿ“‹ Cleanup Summary") + print("=" * 60) + + print(f"\n๐Ÿ” Please verify in AWS Console ({AWS_REGION}):") + print("1. Bedrock AgentCore Runtimes:") + print(f" https://console.aws.amazon.com/bedrock/home?region={AWS_REGION}#/agent-core") + print("2. Bedrock Guardrails:") + print(f" https://console.aws.amazon.com/bedrock/home?region={AWS_REGION}#/guardrails") + print("3. ECR Repositories:") + print(f" https://console.aws.amazon.com/ecr/repositories?region={AWS_REGION}") + print("4. Parameter Store:") + print(f" https://console.aws.amazon.com/systems-manager/parameters/?region={AWS_REGION}") + + print("\nโœ… Cleanup script completed!") + print("โ„น๏ธ Check the console links above to verify all resources are deleted") + print("=" * 60) + +if __name__ == "__main__": + try: + cleanup_all() + except Exception as e: + print(f"\nโŒ Cleanup failed: {e}") + import traceback + traceback.print_exc() + exit(1) diff --git a/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/06-guardrail-middleware/deploy_simple_agent.py b/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/06-guardrail-middleware/deploy_simple_agent.py new file mode 100644 index 000000000..1864ff486 --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/06-guardrail-middleware/deploy_simple_agent.py @@ -0,0 +1,342 @@ +#!/usr/bin/env python3 +""" +Deploy Simple Agent with Dynamic Guardrail Creation +Simplified and optimized version +""" + +import boto3 +import json +import os +import time +from bedrock_agentcore_starter_toolkit import Runtime + +# Initialize AWS clients +bedrock = boto3.client('bedrock') +ssm = boto3.client('ssm') +iam = boto3.client('iam') + +def create_or_get_guardrail(): + """Create or get existing guardrail from SSM Parameter Store""" + + # Check if guardrail ID already exists in Parameter Store + try: + response = ssm.get_parameter(Name='/simple_agent/guardrail_id') + guardrail_id = response['Parameter']['Value'] + print(f"โœ… Found existing guardrail in Parameter Store: {guardrail_id}") + + # Verify it still exists + try: + bedrock.get_guardrail(guardrailIdentifier=guardrail_id) + print(f"โœ… Guardrail {guardrail_id} is valid and active") + return guardrail_id + except: + print(f"โš ๏ธ Guardrail {guardrail_id} no longer exists, creating new one...") + except: + print("๐Ÿ›ก๏ธ No existing guardrail found, creating new one...") + + # Create new guardrail + try: + response = bedrock.create_guardrail( + name='simple-agent-guardrail', + description='Guardrail for Simple Agent middleware demo', + topicPolicyConfig={ + 'topicsConfig': [ + { + 'name': 'Harmful Content', + 'definition': 'Content that promotes harm, hate, or violence', + 'examples': [ + 'I hate you', + 'Violence against others', + 'Harmful instructions' + ], + 'type': 'DENY' + } + ] + }, + contentPolicyConfig={ + 'filtersConfig': [ + {'type': 'HATE', 'inputStrength': 'HIGH', 'outputStrength': 'HIGH'}, + {'type': 'INSULTS', 'inputStrength': 'HIGH', 'outputStrength': 'HIGH'}, + {'type': 'SEXUAL', 'inputStrength': 'HIGH', 'outputStrength': 'HIGH'}, + {'type': 'VIOLENCE', 'inputStrength': 'HIGH', 'outputStrength': 'HIGH'}, + {'type': 'MISCONDUCT', 'inputStrength': 'HIGH', 'outputStrength': 'HIGH'}, + {'type': 'PROMPT_ATTACK', 'inputStrength': 'HIGH', 'outputStrength': 'NONE'} + ] + }, + blockedInputMessaging="Your message was blocked due to policy violations.", + blockedOutputsMessaging="The response was blocked due to policy violations." + ) + + guardrail_id = response['guardrailId'] + guardrail_version = response['version'] + + print(f"โœ… Created guardrail: {guardrail_id} (version: {guardrail_version})") + + # Store in Parameter Store for future use + ssm.put_parameter( + Name='/simple_agent/guardrail_id', + Value=guardrail_id, + Type='String', + Description='Guardrail ID for simple agent middleware', + Overwrite=True + ) + + print("โณ Waiting 5 seconds for guardrail to be ready...") + time.sleep(5) + + return guardrail_id + + except Exception as e: + print(f"โŒ Failed to create guardrail: {e}") + raise + + +def add_bedrock_permissions_to_role(role_name): + """Add Bedrock Guardrail permissions to the execution role""" + + print(f"๐Ÿ” Adding Bedrock Guardrail permissions to role: {role_name}") + + # Define the policy for Bedrock Guardrail operations + guardrail_policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "bedrock:ApplyGuardrail", + "bedrock:GetGuardrail" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "ssm:GetParameter", + "ssm:GetParameters" + ], + "Resource": [ + "arn:aws:ssm:*:*:parameter/simple_agent/*", + "arn:aws:ssm:*:*:parameter/simple_agent/tavily_api_key" + ] + }, + { + "Effect": "Allow", + "Action": [ + "kms:Decrypt" + ], + "Resource": "*", + "Condition": { + "StringEquals": { + "kms:ViaService": "ssm.*.amazonaws.com" + } + } + } + ] + } + + try: + # Add the inline policy to the role + iam.put_role_policy( + RoleName=role_name, + PolicyName='BedrockGuardrailAndSSMAccess', + PolicyDocument=json.dumps(guardrail_policy) + ) + print(f"โœ… Successfully added Bedrock and SSM permissions to role: {role_name}") + return True + except Exception as e: + print(f"โŒ Failed to add permissions to role {role_name}: {e}") + return False + +def save_tavily_api_key(): + """Save Tavily API key to SSM Parameter Store if available""" + + # Check for Tavily API key in environment + tavily_api_key = os.environ.get('TAVILY_API_KEY') + + if tavily_api_key: + print("๐Ÿ”‘ Saving Tavily API key to SSM Parameter Store...") + try: + ssm.put_parameter( + Name='/simple_agent/tavily_api_key', + Value=tavily_api_key, + Type='SecureString', + Description='Tavily API key for web search functionality', + Overwrite=True + ) + print("โœ… Tavily API key saved to SSM Parameter Store") + return True + except Exception as e: + print(f"โš ๏ธ Could not save Tavily API key to SSM: {e}") + return False + else: + print("โ„น๏ธ No TAVILY_API_KEY found in environment") + print(" To enable web search, set: export TAVILY_API_KEY=your-api-key") + return False + +def deploy_simple_agent(): + """Deploy the simple agent with guardrail middleware""" + + print("๐Ÿš€ Deploying Simple Agent with Guardrail Middleware") + print("=" * 60) + + # Check required files + if not os.path.exists('simple_agent.py'): + print("โŒ Required file missing: simple_agent.py") + return None + + if not os.path.exists('simple_requirements.txt'): + print("โŒ Required file missing: simple_requirements.txt") + return None + + print("โœ… All required files found\n") + + # Step 1: Create or get guardrail and store in SSM + guardrail_id = create_or_get_guardrail() + print(f"๐Ÿ“‹ Guardrail ID: {guardrail_id}\n") + + # Step 2: Save Tavily API key if available + save_tavily_api_key() + print() + + # Step 3: Configure and launch the runtime (simple_agent.py already reads from SSM) + print("๐Ÿ“ Note: simple_agent.py will read guardrail ID and Tavily API key from SSM at runtime") + + print("\nโš™๏ธ Initializing AgentCore Runtime...") + agentcore_runtime = Runtime() + + print("โš™๏ธ Configuring runtime...") + agentcore_runtime.configure( + entrypoint="simple_agent.py", + requirements_file="simple_requirements.txt", + auto_create_execution_role=True + ) + + print("\n๐Ÿš€ Launching agent runtime...") + print("This may take a few minutes...\n") + + launch_result = agentcore_runtime.launch(auto_update_on_conflict=True) + + print("โœ… Launch completed!\n") + + # Step 4: Add proper IAM permissions to the execution role + print("๐Ÿ” Setting up IAM permissions...") + + # Find the execution role that was created + execution_role_name = None + + # Try to get it from launch result first + if hasattr(launch_result, 'execution_role'): + execution_role_arn = launch_result.execution_role + if execution_role_arn: + execution_role_name = execution_role_arn.split('/')[-1] + print(f"๐Ÿ“ Got execution role from launch result: {execution_role_name}") + + # If not found, search for the most recently created AmazonBedrockAgentCoreSDKRuntime role + if not execution_role_name: + try: + print("๐Ÿ” Searching for the most recently created execution role...") + paginator = iam.get_paginator('list_roles') + + # Collect all matching roles with their creation dates + matching_roles = [] + for page in paginator.paginate(): + for role in page['Roles']: + role_name = role['RoleName'] + # Look for the SDK-created runtime role + if 'AmazonBedrockAgentCoreSDKRuntime-us-west-2' in role_name: + matching_roles.append({ + 'name': role_name, + 'created': role.get('CreateDate') + }) + + # Sort by creation date and get the most recent + if matching_roles: + matching_roles.sort(key=lambda x: x['created'] if x['created'] else '', reverse=True) + execution_role_name = matching_roles[0]['name'] + print(f"๐Ÿ” Found most recent execution role: {execution_role_name}") + print(f" Created at: {matching_roles[0]['created']}") + + # Show all found roles for debugging + if len(matching_roles) > 1: + print(f" (Found {len(matching_roles)} total AmazonBedrockAgentCoreSDKRuntime roles)") + + except Exception as e: + print(f"โš ๏ธ Error finding execution role: {e}") + + # Add permissions to the role + if execution_role_name: + print(f"\n๐ŸŽฏ Updating IAM role: {execution_role_name}") + success = add_bedrock_permissions_to_role(execution_role_name) + if success: + print("โณ Waiting 20 seconds for IAM permissions to propagate...") + time.sleep(20) + print("โœ… IAM permissions should now be active") + else: + print("โŒ Failed to add IAM permissions automatically") + print(" Please manually add the following permissions to the role:") + print(f" Role name: {execution_role_name}") + print(" Required permissions:") + print(" - bedrock:ApplyGuardrail") + print(" - bedrock:GetGuardrail") + print(" - ssm:GetParameter (for /simple_agent/* parameters)") + print(" - kms:Decrypt (for SecureString parameters)") + else: + print("โš ๏ธ Could not find execution role - you may need to add permissions manually") + print(" Look for the most recent AmazonBedrockAgentCoreSDKRuntime-us-west-2-* role") + print(" Add these permissions to your execution role:") + print(" - bedrock:ApplyGuardrail") + print(" - bedrock:GetGuardrail") + print(" - ssm:GetParameter (for /simple_agent/* parameters)") + print(" - kms:Decrypt (for SecureString parameters)") + + # Step 5: Store runtime configuration + print("\n๐Ÿ’พ Storing runtime configuration...") + + try: + ssm.put_parameter(Name='/simple_agent/runtime/agent_arn', Value=launch_result.agent_arn, Type='String', Overwrite=True) + ssm.put_parameter(Name='/simple_agent/runtime/agent_id', Value=launch_result.agent_id, Type='String', Overwrite=True) + print("โœ… Configuration stored") + except Exception as e: + print(f"โš ๏ธ Could not store configuration: {e}") + + # Print summary + print("\n" + "=" * 60) + print("๐ŸŽ‰ Deployment Completed Successfully!") + print("=" * 60) + print(f"๐Ÿ“ Agent ARN: {launch_result.agent_arn}") + print(f"๐Ÿ“ Agent ID: {launch_result.agent_id}") + print(f"๐Ÿ›ก๏ธ Guardrail ID: {guardrail_id}") + print(f"๐Ÿ“ ECR URI: {launch_result.ecr_uri}") + print("\n๐Ÿ—๏ธ Architecture:") + print(" User/Web UI โ†’ AgentCore Runtime โ†’ CORS Middleware โ†’ Guardrail Middleware โ†’ Strands Agent") + print("\n๐Ÿ“ Configuration stored in SSM Parameter Store:") + print(f" - /simple_agent/guardrail_id: {guardrail_id}") + print(f" - /simple_agent/runtime/agent_id: {launch_result.agent_id}") + print(f" - /simple_agent/runtime/agent_arn: {launch_result.agent_arn}") + if os.environ.get('TAVILY_API_KEY'): + print(f" - /simple_agent/tavily_api_key: ****** (SecureString)") + print("\n๐Ÿ“– Next steps:") + print(" 1. Test the agent: python test_simple_agent.py") + print(" 2. Launch UI: streamlit run app.py") + print(" 3. Cleanup when done: python cleanup_all.py") + + return launch_result + +if __name__ == "__main__": + try: + # Clean up any leftover configuration files + if os.path.exists('.bedrock_agentcore.yaml'): + os.remove('.bedrock_agentcore.yaml') + print("๐Ÿงน Cleaned up old configuration file\n") + + result = deploy_simple_agent() + if result: + print("\nโœ… All done!") + else: + print("\nโŒ Deployment failed") + exit(1) + except Exception as e: + print(f"\nโŒ Deployment failed: {e}") + import traceback + traceback.print_exc() + exit(1) diff --git a/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/06-guardrail-middleware/simple_agent.py b/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/06-guardrail-middleware/simple_agent.py new file mode 100644 index 000000000..c3fecf736 --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/06-guardrail-middleware/simple_agent.py @@ -0,0 +1,411 @@ +#!/usr/bin/env python3 +""" +Simple Strands Agent with CORS and Guardrail Middleware +Using Starlette with proper CORS configuration +""" + +from starlette.applications import Starlette +from starlette.middleware import Middleware +from starlette.middleware.cors import CORSMiddleware +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request +from starlette.responses import Response, JSONResponse +from starlette.routing import Route +import uvicorn +import boto3 +import json +import logging +import os +from strands import Agent +from strands.models import BedrockModel +from strands.tools import tool +from tavily import TavilyClient + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Get guardrail ID dynamically from SSM Parameter Store - NO HARDCODING +try: + ssm_client = boto3.client('ssm') + response = ssm_client.get_parameter(Name='/simple_agent/guardrail_id') + GUARDRAIL_ID = response['Parameter']['Value'] + logger.info(f"โœ… Loaded Guardrail ID from SSM: {GUARDRAIL_ID}") +except Exception as e: + logger.error(f"โŒ Failed to load Guardrail ID from SSM: {e}") + logger.error("โš ๏ธ CRITICAL: No Guardrail ID available - the agent cannot start without a guardrail") + # Exit if we can't get the guardrail ID - this is a critical requirement + import sys + sys.exit(1) + +# Initialize Tavily client for web search (optional - will work without API key but with limited functionality) +TAVILY_API_KEY = os.environ.get('TAVILY_API_KEY') + +# If not in environment, try to get from SSM Parameter Store +if not TAVILY_API_KEY: + try: + ssm_client = boto3.client('ssm') + response = ssm_client.get_parameter(Name='/simple_agent/tavily_api_key', WithDecryption=True) + TAVILY_API_KEY = response['Parameter']['Value'] + logger.info("โœ… Loaded Tavily API key from SSM Parameter Store") + except Exception as e: + logger.warning(f"โš ๏ธ Could not load Tavily API key from SSM: {e}") + +if TAVILY_API_KEY: + tavily_client = TavilyClient(api_key=TAVILY_API_KEY) + logger.info("โœ… Tavily API client initialized for web search") +else: + tavily_client = None + logger.warning("โš ๏ธ Tavily API key not found - web search will be limited") + +class GuardrailMiddleware(BaseHTTPMiddleware): + """Middleware to apply Bedrock Guardrails on input and output""" + + def __init__(self, app, guardrail_id: str): + super().__init__(app) + self.guardrail_id = guardrail_id + self.bedrock_runtime = boto3.client('bedrock-runtime') + logger.info(f"โœ… Guardrail middleware initialized with ID: {guardrail_id}") + + async def dispatch(self, request: Request, call_next): + """Apply guardrails to requests and responses""" + + # Apply guardrail to INPUT + if request.method == "POST" and request.url.path == "/invocations": + try: + body = await request.body() + body_str = body.decode('utf-8') + + # Extract the prompt from JSON + try: + body_json = json.loads(body_str) + prompt_text = body_json.get('prompt', '') + except json.JSONDecodeError: + prompt_text = body_str + + logger.info(f"๐Ÿ›ก๏ธ Validating input with guardrail: {self.guardrail_id}") + logger.info(f"๐Ÿ“ Middleware captured INPUT: '{prompt_text}'") + print(f"\n{'='*60}") + print(f"๐Ÿ” MIDDLEWARE INPUT CAPTURE") + print(f"{'='*60}") + print(f"๐Ÿ“ฅ Raw Body: {body_str}") + print(f"๐Ÿ“ Extracted Prompt: '{prompt_text}'") + print(f"{'='*60}\n") + + try: + # Apply Bedrock Guardrail to just the prompt text + guardrail_response = self.bedrock_runtime.apply_guardrail( + guardrailIdentifier=self.guardrail_id, + guardrailVersion="DRAFT", + source="INPUT", + content=[{"text": {"text": prompt_text}}] + ) + + if guardrail_response.get('action') == 'GUARDRAIL_INTERVENED': + # Check if it's actually inappropriate content or just a false positive + should_block = False + assessments = guardrail_response.get('assessments', []) + + for assessment in assessments: + # Check topicPolicy (e.g., Harmful Content) + topic_policy = assessment.get('topicPolicy', {}) + topics = topic_policy.get('topics', []) + for topic in topics: + if topic.get('action') == 'BLOCKED' and topic.get('detected'): + should_block = True + logger.info(f"๐Ÿšซ Blocked by topic: {topic.get('name')}") + break + + # Check contentPolicy filters + content_policy = assessment.get('contentPolicy', {}) + filters = content_policy.get('filters', []) + + for filter_item in filters: + filter_type = filter_item.get('type', '') + confidence = filter_item.get('confidence', '') + + # Only block for actual inappropriate content, not low-confidence prompt attacks + if filter_type in ['HATE', 'INSULTS', 'VIOLENCE', 'SEXUAL', 'MISCONDUCT']: + should_block = True + logger.info(f"๐Ÿšซ Blocked by filter: {filter_type} ({confidence})") + break + elif filter_type == 'PROMPT_ATTACK' and confidence in ['MEDIUM', 'HIGH']: + should_block = True + logger.info(f"๐Ÿšซ Blocked by filter: {filter_type} ({confidence})") + break + + if should_block: + logger.warning(f"๐Ÿ›ก๏ธ Input blocked by guardrail") + blocked_message = "โš ๏ธ Your message was blocked due to policy violations. Please rephrase your request without inappropriate content." + print(f"\n{'='*60}") + print(f"๐Ÿšซ BLOCKED BY GUARDRAIL") + print(f"{'='*60}") + print(f"โŒ Input: '{prompt_text}'") + print(f"๐Ÿ›ก๏ธ Reason: Policy violation detected") + print(f"๐Ÿ’ฌ Response: {blocked_message}") + print(f"{'='*60}\n") + return Response( + content=blocked_message, + status_code=200, + media_type="text/plain" + ) + else: + logger.info(f"โš ๏ธ Guardrail flagged but allowing (false positive)") + print(f"\n{'='*60}") + print(f"โš ๏ธ FALSE POSITIVE - ALLOWING") + print(f"{'='*60}") + print(f"๐Ÿ“ Input: '{prompt_text}'") + print(f"โœ… Decision: Allowing (low confidence flag)") + print(f"{'='*60}\n") + else: + print(f"\n{'='*60}") + print(f"โœ… INPUT VALIDATION PASSED") + print(f"{'='*60}") + print(f"๐Ÿ“ Input: '{prompt_text}'") + print(f"โœ… Status: Clean - No issues detected") + print(f"{'='*60}\n") + + logger.info("โœ… Input passed guardrail validation") + except Exception as guardrail_error: + # Log but don't block if guardrail check fails + logger.warning(f"โš ๏ธ Guardrail check failed, allowing request: {guardrail_error}") + + # Recreate request with body + request._body = body + + except Exception as e: + logger.error(f"โŒ Error processing request body: {e}") + + # Process request + response = await call_next(request) + + # Apply guardrail to OUTPUT + if request.url.path == "/invocations": + try: + # Read response body + response_body = b"" + async for chunk in response.body_iterator: + response_body += chunk + + response_str = response_body.decode('utf-8') + + logger.info(f"๐Ÿ›ก๏ธ Validating output with guardrail: {self.guardrail_id}") + logger.info(f"๐Ÿ“ค Middleware captured OUTPUT: '{response_str[:100]}...'") + print(f"\n{'='*60}") + print(f"๐Ÿ” MIDDLEWARE OUTPUT CAPTURE") + print(f"{'='*60}") + print(f"๐Ÿ“ค Response (first 200 chars): '{response_str[:200]}...'") + print(f"๐Ÿ“ Total Length: {len(response_str)} characters") + print(f"{'='*60}\n") + + try: + # Apply Bedrock Guardrail to output + guardrail_response = self.bedrock_runtime.apply_guardrail( + guardrailIdentifier=self.guardrail_id, + guardrailVersion="DRAFT", + source="OUTPUT", + content=[{"text": {"text": response_str}}] + ) + + if guardrail_response.get('action') == 'GUARDRAIL_INTERVENED': + logger.warning(f"๐Ÿ›ก๏ธ Output blocked by guardrail: {guardrail_response}") + # Return safe response when output is blocked + return Response( + content="I cannot provide that response as it violates content policies.", + status_code=200, + headers=dict(response.headers), + media_type="text/plain" + ) + + logger.info("โœ… Output passed guardrail validation") + except Exception as guardrail_error: + # Log but don't block if guardrail check fails + logger.warning(f"โš ๏ธ Output guardrail check failed, allowing response: {guardrail_error}") + + # Return new response with same content + return Response( + content=response_body, + status_code=response.status_code, + headers=dict(response.headers), + media_type=response.media_type + ) + + except Exception as e: + logger.error(f"โŒ Error validating response: {e}") + + return response + +# Create Strands agent +@tool +def get_weather(location: str) -> str: + """Get weather for a location""" + return f"The weather in {location} is sunny and 72ยฐF" + +@tool +def calculate(expression: str) -> str: + """Calculate a mathematical expression""" + try: + # Safely evaluate mathematical expressions + # Remove any potentially dangerous functions + safe_dict = { + '__builtins__': {}, + 'abs': abs, 'round': round, 'min': min, 'max': max, + 'sum': sum, 'pow': pow, 'len': len + } + # Allow basic math operations + import math + for func in ['sqrt', 'sin', 'cos', 'tan', 'log', 'exp', 'pi', 'e']: + if hasattr(math, func): + safe_dict[func] = getattr(math, func) + + result = eval(expression, safe_dict) + return f"The result is: {result}" + except Exception as e: + return f"Invalid expression: {str(e)}" + +def format_search_results(tavily_result): + """Format Tavily search results for the agent""" + if not tavily_result or "results" not in tavily_result or not tavily_result["results"]: + return "No search results found." + + formatted_results = [] + for i, doc in enumerate(tavily_result["results"][:5], 1): # Limit to top 5 results + title = doc.get("title", "No title") + url = doc.get("url", "No URL") + content = doc.get("content", "").strip() + + if content: + # Truncate content if too long + if len(content) > 500: + content = content[:500] + "..." + + formatted_doc = f"\n**Result {i}:**\n" + formatted_doc += f"Title: {title}\n" + formatted_doc += f"URL: {url}\n" + formatted_doc += f"Content: {content}\n" + formatted_results.append(formatted_doc) + + return "\n".join(formatted_results) + +@tool +def web_search(query: str) -> str: + """ + Search the web for information about any topic. + Use this for general questions, current events, facts, or any queries + that aren't simple calculations or weather requests. + + Args: + query: The search query to look up on the web + + Returns: + Search results with titles, URLs, and content snippets + """ + try: + if not tavily_client: + return "Web search is not available. Please set TAVILY_API_KEY environment variable to enable web search." + + # Perform the search + search_results = tavily_client.search( + query=query, + max_results=5, + search_depth="advanced", + include_raw_content=False + ) + + # Format and return results + formatted = format_search_results(search_results) + + if not formatted or formatted == "No search results found.": + return f"No results found for '{query}'. Try rephrasing your search." + + return f"Web search results for '{query}':\n{formatted}" + + except Exception as e: + logger.error(f"Error performing web search: {e}") + return f"An error occurred while searching: {str(e)}" + +model = BedrockModel(model_id="us.anthropic.claude-3-7-sonnet-20250219-v1:0") + +# Build tools list based on what's available +tools = [get_weather, calculate] +if tavily_client: + tools.append(web_search) + +agent = Agent( + model=model, + tools=tools, + system_prompt="""You are a helpful assistant with access to various tools. + +Use the appropriate tool based on the user's request: +- For mathematical calculations, use the calculate tool +- For weather information, use the get_weather tool +- For ANY other questions (general knowledge, current events, facts, research, etc.), use the web_search tool + +When using web_search, provide a clear and informative response based on the search results. +If the initial search doesn't provide enough information, you can search again with a refined query. + +Always aim to provide accurate, helpful, and relevant information to the user.""" +) + +async def invocations(request: Request): + """Handle invocation requests""" + try: + body = await request.json() + user_input = body.get("prompt", "") + + logger.info(f"๐Ÿ“จ Received request: {user_input}") + + if not user_input: + return JSONResponse( + content={"error": "No prompt provided"}, + status_code=400 + ) + + # Process with Strands agent + response = agent(user_input) + result = response.message['content'][0]['text'] + + logger.info(f"๐Ÿ“ค Sending response") + + return Response(content=result, media_type="text/plain") + + except Exception as e: + logger.error(f"โŒ Error processing request: {e}") + return JSONResponse( + content={"error": str(e)}, + status_code=500 + ) + +async def ping(request: Request): + """Health check endpoint""" + return JSONResponse(content={"status": "healthy"}) + +# Handle browser preflight requests for CORS +async def options_handler(request: Request): + """Handle OPTIONS requests for CORS preflight""" + return JSONResponse(content={"message": "OK"}) + +# Create Starlette app with middleware +app = Starlette( + debug=True, + routes=[ + Route('/invocations', invocations, methods=['POST']), + Route('/invocations', options_handler, methods=['OPTIONS']), # CORS preflight + Route('/ping', ping, methods=['GET']), + ], + middleware=[ + Middleware( + CORSMiddleware, + allow_origins=["*"], # Customize in production + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ), + Middleware(GuardrailMiddleware, guardrail_id=GUARDRAIL_ID), + ], +) + +if __name__ == "__main__": + logger.info("๐Ÿš€ Starting Simple Agent with Guardrail Middleware") + logger.info(f"๐Ÿ›ก๏ธ Guardrail ID: {GUARDRAIL_ID}") + uvicorn.run(app, host="0.0.0.0", port=8080) diff --git a/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/06-guardrail-middleware/simple_requirements.txt b/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/06-guardrail-middleware/simple_requirements.txt new file mode 100644 index 000000000..4e17ff952 --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/06-guardrail-middleware/simple_requirements.txt @@ -0,0 +1,5 @@ +strands-agents>=0.1.0 +boto3>=1.35.0 +starlette>=0.37.0 +uvicorn>=0.30.0 +tavily-python>=0.4.0 diff --git a/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/06-guardrail-middleware/streamlit_requirements.txt b/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/06-guardrail-middleware/streamlit_requirements.txt new file mode 100644 index 000000000..fbc79e8e5 --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/06-guardrail-middleware/streamlit_requirements.txt @@ -0,0 +1,2 @@ +streamlit>=1.28.0 +boto3>=1.35.0 diff --git a/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/06-guardrail-middleware/test_web_search.py b/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/06-guardrail-middleware/test_web_search.py new file mode 100644 index 000000000..8e0ea39f1 --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/03-advanced-concepts/06-guardrail-middleware/test_web_search.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +""" +Test script for web search functionality +""" + +import os +import sys + +# Set the Tavily API key +os.environ['TAVILY_API_KEY'] = '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' + +try: + from tavily import TavilyClient + + # Test Tavily API directly + print("Testing Tavily API directly...") + print("=" * 60) + + client = TavilyClient(api_key=os.environ['TAVILY_API_KEY']) + + # Search for boto3 Lambda create_function + query = "boto3 create_function Lambda API documentation" + print(f"Query: {query}") + print("-" * 60) + + results = client.search( + query=query, + max_results=3, + search_depth="advanced" + ) + + if results and 'results' in results: + for i, result in enumerate(results['results'], 1): + print(f"\nResult {i}:") + print(f"Title: {result.get('title', 'N/A')}") + print(f"URL: {result.get('url', 'N/A')}") + content = result.get('content', '') + if content: + print(f"Content: {content[:500]}...") + print("-" * 40) + + print("\n" + "=" * 60) + print("โœ… Tavily API is working correctly!") + + # Now test the agent + print("\nTesting the agent with web search...") + print("=" * 60) + + from simple_agent import agent + + response = agent("What is the boto3 API for creating a Lambda function?") + result = response.message['content'][0]['text'] + + print("Agent Response:") + print(result) + + print("\nโœ… Agent web search integration is working!") + +except ImportError as e: + print(f"โŒ Missing required module: {e}") + print("Please install: pip install tavily-python") + sys.exit(1) +except Exception as e: + print(f"โŒ Error: {e}") + sys.exit(1)