diff --git a/agentcore-strands-playground/.gitignore b/agentcore-strands-playground/.gitignore new file mode 100644 index 0000000..2e986ca --- /dev/null +++ b/agentcore-strands-playground/.gitignore @@ -0,0 +1,10 @@ +__pycache__/ +.venv/ +.env +.DS_Store +*.log +uv.lock +agentcore_agent/* +!agentcore_agent/runtime_agent.py +!agentcore_agent/requirements.txt +docs/ \ No newline at end of file diff --git a/agentcore-strands-playground/README.md b/agentcore-strands-playground/README.md new file mode 100644 index 0000000..8e923bf --- /dev/null +++ b/agentcore-strands-playground/README.md @@ -0,0 +1,179 @@ +# Bedrock AgentCore Strands Playground + +Read [this document](./agentcore_playground.md) for background. + +## Features + +- **Dynamic tool and model selection:** change available tools and model on the fly +- **Authentication:** uses Cognito to authenticate user to the agent +- **Real-time Chat Interface**: Interactive chat with deployed AgentCore agents +- **Agent Discovery**: Automatically discover and select from available agents in your AWS account +- **Version Management**: Choose specific versions of your deployed agents +- **Multi-Region Support**: Connect to agents deployed in different AWS regions +- **Streaming Responses**: Real-time streaming of agent responses +- **Session Management**: Maintain conversation context with unique session IDs +- **Memory Management**: Uses AgentCore Memory to save user preferences and conversation context + +## Architecture + +![AgentCore Architecture](./images/BRAC_architecture.png) + +## Prerequisites + +- Python 3.11 or higher +- [uv package manager](https://docs.astral.sh/uv/getting-started/installation/) +- AWS CLI configured with appropriate credentials +- Access to Amazon Bedrock AgentCore service +- Deployed agents on Bedrock AgentCore Runtime +- Optional: Cognito [user pool](https://github.com/awslabs/amazon-bedrock-agentcore-samples/blob/main/01-tutorials/03-AgentCore-identity/03-Inbound%20Auth%20example/inbound_auth_runtime_with_strands_and_bedrock_models.ipynb./COGNITO_SETUP.m) + +### Required AWS Permissions + +Your AWS credentials need the following permissions: + +- `bedrock-agentcore-control:ListAgentRuntimes` +- `bedrock-agentcore-control:ListAgentRuntimeVersions` +- `bedrock-agentcore:InvokeAgentRuntime` + Note that other permissions may be required by tools you select. + +## Installation + +1. **Clone the repository**: + + ```bash + git clone https://github.com/aws-samples/aws-generativeai-partner-samples/tree/main/agentcore-strands-playground + ``` +2. **Install dependencies using uv**: + + ```bash + uv sync + ``` + +## Deploy the Example Agent + +1. **Optional: Set up Cognito pool** + + run 'config.py' to configure a Cognito pool automatically. The discovery URL and client ID will be saved in .env file + +2. **Configure the agent**: +```bash +cd agentcore_agent +uv run agentcore configure -e runtime_agent.py +``` +Select default options; optionally configure your Cognito pool as OAuth authorizer (or other OAuth provider), and configure Long Term Memory under Memory Configuration. + +2. **Deploy to AgentCore Runtime:** +```bash +uv run agentcore launch +cd .. +``` +## Running the Application + +### Using uv (recommended) +```bash +uv run streamlit run app.py [-- --auth | --noauth] +``` +The application will start and be available at `http://localhost:8501`. + +If you configured a Cognito pool for authentication, the app will automatically look in 1) .env file and 2) ./agentcore_agent/.bedrock_agentcore.yaml to find Cognito configuration variables. If it finds a Cognito configuration, or if you specify '--auth' on the command line, it will default to using authentication when invoking the agent. If it does not find Cognito configuration, or if you specify '--noauth' on the command line, it will not use any authentication when invoking the agent. + +Note: many of the Strands built-in tools require permissions that are not automatically granted to the execution role, because the AgentCore Starter Toolkit follows security best practices and grants least privilege access. For example, the prompt "use aws to list s3 buckets" will fail even if the 'use_aws' tool is configured in the Tool Selection panel because the AgentCore runtime role does not have appropriate permissions. To grant permissions, determine the role name (available in ./agentcore_agent/.bedrock_agentcore.yaml) and attach relevant policies to the role. For example: + +```bash +aws iam attach-role-policy \ + --role-name AmazonBedrockAgentCoreSDKRuntime-us-west-2-xxxxxx \ + --policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess +``` + +## Usage + +Note: all parameters have defaults, which typically pick the most recent agent/version/session. The simplest usage is to run the front-end application and start chatting with the agent. + +Optional: + +1. **Configure Tools:** Click the Tools Configuration dropdown to select from available Strands Agents built-in tools +2. **Configure AWS Region**: Select your preferred AWS region from the sidebar +3. **Select Agent**: Choose from automatically discovered agents in your account +4. **Choose Version**: Select the specific version of your agent to use +5. **Select Memory**: The front-end will discover configured AgentCore Memory resources and select the most recently created +6. **Select Session:** Choose from a saved session or enter a new session name and click "New session" +7. **Select Tools:** +8. **Start Chatting**: Type your message in the chat input and press Enter + +## Project Structure + +``` +agentcore-strands-playground/ +├── app.py # Main Streamlit application +├── auth_utils.py # Cognito authentication utilities +├── br_utils.py # Bedrock utilities (model discovery) +├── dotenv.example # Example environment variables +├── pyproject.toml # Project dependencies (uv) +├── README.md # This file +├── agentcore_agent/ # Agent deployment configuration +│ ├── runtime_agent.py # Strands agent implementation +│ ├── requirements.txt # Agent dependencies +├── images/ # Documentation images +│ ├── BRAC_architecture.png +│ ├── BRAC_interface_screen.png +│ └── ... +└── static/ # UI assets (fonts, icons, logos) + ├── agentcore-service-icon.png + ├── gen-ai-dark.svg + ├── user-profile.svg + └── ... +``` + +## Configuration Files + +- **`pyproject.toml`**: Defines project dependencies and metadata + +## Troubleshooting + +### Common Issues + +1. **No agents found**: Ensure you have deployed agents in the selected region and have proper AWS permissions +2. **Connection errors**: Verify your AWS credentials and network connectivity +3. **Permission denied**: Check that your IAM user/role has the required Bedrock AgentCore permissions + +### Debug Mode + +Enable debug logging by setting the Streamlit logger level in the application or check the browser console for additional error information. + +## Development + +### Adding New Features + +The application is built with modularity in mind, particularly the ability for AWS partners to add funcationality. Key areas for extension: + +- **Partner LLMs**: the list of LLMs returned in br_utils.py can be modified +- **Observability**: the agent is configured to report OTEL logs which can be consumed by partner observability solutions +- **Identity**: the auth_utils.py module can be replaced by a partner IdP solution +- **Partner Memory**: the memory interface can be modified to use different partner tools +- **MCP Servers**: any partner MCP server can be called by the agent + +### Dependencies + +- **boto3**: AWS SDK for Python +- **streamlit**: Web application framework +- **uv**: Fast Python package installer and resolver + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Test thoroughly +5. Submit a pull request + +## License + +This project is licensed under the terms specified in the repository license file. + +## Resources + +- [Amazon Bedrock AgentCore Documentation](https://docs.aws.amazon.com/bedrock-agentcore/) +- [Bedrock AgentCore Samples](https://github.com/awslabs/amazon-bedrock-agentcore-samples/) +- [Streamlit Documentation](https://docs.streamlit.io/) +- [Strands Agents Framework](https://github.com/awslabs/strands-agents) + Agent diff --git a/agentcore-strands-playground/agentcore_agent/requirements.txt b/agentcore-strands-playground/agentcore_agent/requirements.txt new file mode 100644 index 0000000..856002b --- /dev/null +++ b/agentcore-strands-playground/agentcore_agent/requirements.txt @@ -0,0 +1,31 @@ +python-dotenv>=1.0.0 +strands-agents>=1.7.1 +#strands-agents-tools - vendored locally in strands_tools/ directory +#strands-agents-tools @ git+https://github.com/wjquigsAZ/strands-agents-tools + +# Core dependencies for vendored strands_tools module (from pyproject.toml) +rich>=14.0.0,<15.0.0 +sympy>=1.12.0,<2.0.0 +prompt_toolkit>=3.0.51,<4.0.0 +aws_requests_auth>=0.4.3,<0.5.0 +PyJWT>=2.10.1,<3.0.0 +dill>=0.4.0,<0.5.0 +pillow>=11.2.1,<12.0.0 +tenacity>=9.1.2,<10.0.0 +watchdog>=6.0.0,<7.0.0 +slack_bolt>=1.23.0,<2.0.0 +markdownify>=1.0.0,<2.0.0 +requests>=2.28.0,<3.0.0 +aiohttp>=3.8.0,<4.0.0 +typing_extensions>=4.0.0,<5.0.0 + +bedrock-agentcore>=0.1.3 +bedrock-agentcore-starter-toolkit>=0.1.8 +duckduckgo-search +mcp>=1.0.0 +boto3>=1.34.0 +botocore>=1.39.7,<2.0.0 + +# For IAM authentication with Gateway (SigV4 signing) +# Install from: https://github.com/awslabs/run-model-context-protocol-servers-with-aws-lambda +# pip install git+https://github.com/awslabs/run-model-context-protocol-servers-with-aws-lambda.git#subdirectory=python/streamable-http-sigv4 diff --git a/agentcore-strands-playground/agentcore_agent/runtime_agent.py b/agentcore-strands-playground/agentcore_agent/runtime_agent.py new file mode 100644 index 0000000..c893c0e --- /dev/null +++ b/agentcore-strands-playground/agentcore_agent/runtime_agent.py @@ -0,0 +1,437 @@ +#!/usr/bin/env python3 +""" +Runtime-specific agent for AgentCore deployment of Strands Playground. +""" + +import json +import os +import logging +import sys +import boto3 +from typing import Dict, List +from dotenv import load_dotenv +from duckduckgo_search import DDGS +from botocore.credentials import Credentials + +# Load environment variables +load_dotenv() + +# Import Strands components +from strands import Agent, tool +from strands.models import BedrockModel +from strands_tools import ( + agent_graph, calculator, cron, current_time, editor, environment, + file_read, file_write, generate_image, http_request, image_reader, journal, + load_tool, memory, nova_reels, + # python_repl, + retrieve, shell, + #slack, + speak, stop, swarm, think, use_aws, use_llm, workflow +) + +# Import BedrockAgentCore runtime +from bedrock_agentcore.runtime import BedrockAgentCoreApp + +# Import memory management modules +from bedrock_agentcore.memory.integrations.strands.session_manager import AgentCoreMemorySessionManager +from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig, RetrievalConfig + +# Set up logging +logger = logging.getLogger(__name__) +logging.basicConfig(format="%(levelname)s | %(name)s | %(message)s") +logger.setLevel(logging.DEBUG) + +# Create BedrockAgentCore app instance +app = BedrockAgentCoreApp() + +# Environment variables for configuration +# ordinarily these will all be passed as part of the request payload since they can change per invocation, +# and since they are set by the front-end + +SYSTEM_PROMPT = os.getenv('STRANDS_SYSTEM_PROMPT', + 'You are a helpful assistant powered by Strands. Strands Agents is a simple-to-use, code-first framework for building agents - open source by AWS.') +MODEL_ID = os.getenv('STRANDS_MODEL_ID', 'us.amazon.nova-pro-v1:0') +REGION = os.getenv('AWS_REGION') +if not REGION: + logger.info("no region configured in env") + REGION='us-west-2' +MAX_TOKENS = int(os.getenv('STRANDS_MAX_TOKENS', '1000')) +TEMPERATURE = float(os.getenv('STRANDS_TEMPERATURE', '0.3')) +TOP_P = float(os.getenv('STRANDS_TOP_P', '0.9')) +# apparently this env variable is set automatically? +MEMORY_ID = os.getenv('BEDROCK_AGENTCORE_MEMORY_ID') +logger.debug(f"Configured MEMORY_ID: {MEMORY_ID}") +MEMORY_NAMESPACE = os.getenv('BEDROCK_AGENTCORE_MEMORY_NAME') +ENABLE_AUTH = os.getenv('ENABLE_AUTH', 'true').lower() == 'true' + +# define a sample tool for web search +@tool +def websearch( + keywords: str, region: str = "us-en", max_results: int | None = None +) -> str: + """Search the web to get 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: + # Use duckduckgo-search library + with DDGS() as ddgs: + results = list(ddgs.text(keywords, region=region, max_results=max_results or 5)) + + if results: + formatted_results = [] + for result in results: + title = result.get('title', 'No Title') + body = result.get('body', 'No Description') + url = result.get('href', 'No URL') # Note: 'href' instead of 'url' + formatted_results.append(f"Title: {title}\nDescription: {body}\nURL: {url}\n") + logger.debug(f"Web search results for '{keywords}': {formatted_results}") + return "\n".join(formatted_results) + else: + return "No results found." + except Exception as e: + logger.error(f"Web search error: {e}") + return f"Search error: {str(e)}" + + + +# Define all available built-in Strands tools +# note that many of these tools (such as use_aws) require IAM permissions +# for the AGENT execution role +available_tools = { + 'agent_graph': agent_graph, + 'calculator': calculator, + 'cron': cron, + 'current_time': current_time, + 'editor': editor, + 'environment': environment, + 'file_read': file_read, + 'file_write': file_write, + 'generate_image': generate_image, + 'http_request': http_request, + 'image_reader': image_reader, + 'journal': journal, + 'load_tool': load_tool, +# 'memory': memory, + 'nova_reels': nova_reels, +# 'python_repl': python_repl, + 'retrieve': retrieve, + 'shell': shell, +# 'slack': slack, + 'speak': speak, + 'stop': stop, + 'swarm': swarm, + 'think': think, + 'use_aws': use_aws, + 'use_llm': use_llm, + 'workflow': workflow, + 'websearch': websearch +} + +# Agent cache: stores agents by (model_id, session_id, actor_id) to avoid session manager conflicts +# previously cached agents by model_id, and shared same agent for different sessions/actors +# limitation in AC Memory session manager doesn't allow sharing +# "Currently, only one agent per session is supported when using AgentCoreMemorySessionManager." +agent_cache: Dict[tuple[str, str, str], Agent] = {} + +# Track first invocation per model +first_invocation_per_model: Dict[str, bool] = {} + +# Track current tools per agent for comparison +current_tools_per_agent: Dict[tuple[str, str, str], List[str]] = {} + +# Track memory configs per (model_id, session_id, actor_id) +memory_cache: Dict[tuple[str, str, str], AgentCoreMemoryConfig] = {} + +# Track session managers per (model_id, session_id, actor_id) +session_manager_cache: Dict[tuple[str, str, str], AgentCoreMemorySessionManager] = {} + +def get_tools_from_names(tool_names: List[str]) -> List: + """ + Convert tool names to actual tool objects. + """ + tools = [] + for tool_name in tool_names: + if tool_name in available_tools: + tools.append(available_tools[tool_name]) + else: + logger.warning(f"Tool '{tool_name}' not found in available_tools, skipping") + + # Fallback to default tools if no valid tools were found + if not tools: + logger.warning("No valid tools found, using default set") + tools = [calculator, current_time, http_request, websearch] + + return tools + + + +# connect to memory provisioned by agentcore starter toolkit +memory_id = MEMORY_ID + +def get_memory_config(model_id: str, session_id: str, actor_id: str) -> AgentCoreMemoryConfig: + """Get memory config from cache or create new one. + + Note: Includes model_id in cache key so each model gets its own memory config. + This allows switching models within the same session without conflicts. + """ + logger.debug(f"Retrieving memory config for model: {model_id}, session: {session_id}, actor: {actor_id}") + cache_key = (model_id, session_id, actor_id) + + if cache_key not in memory_cache: + # Create new memory config + memory_config = AgentCoreMemoryConfig( + memory_id=MEMORY_ID, + session_id=session_id, + actor_id=actor_id, + retrieval_config={ + f"/users/{actor_id}/facts": RetrievalConfig(top_k=3, relevance_score=0.5), + f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5), + f"/summaries/{actor_id}/{session_id}": RetrievalConfig(top_k=3, relevance_score=0.5), + } + ) + memory_cache[cache_key] = memory_config + logger.debug(f"Created new memory config for model: {model_id}, session: {session_id}, actor: {actor_id}") + + return memory_cache[cache_key] + +def get_or_create_agent(model_id: str, tool_names: List[str] = None, actor_id: str = None, session_id: str = None) -> Agent: + """ + Get existing agent from cache or create new one for the given (model_id, session_id, actor_id). + Each session gets its own agent instance to avoid session manager conflicts. + Uses Strands' dynamic tool reloading to update tools without creating a new agent. + + Args: + model_id: The Bedrock model identifier + tool_names: List of tool names to include (defaults to standard set) + actor_id: The actor/user identifier + session_id: The session identifier + + Returns: + RuntimeStrandsAgent instance for the specified model and session + """ + # Create cache key from model_id, session_id, and actor_id + cache_key = (model_id, session_id, actor_id) + + # Check if agent exists for this combination + if cache_key not in agent_cache: + logger.info(f"Creating new agent for model: {model_id}") + + # Create new Bedrock model instance + bedrock_model = BedrockModel( + model_id=model_id, + region_name=REGION, + temperature=TEMPERATURE, + max_tokens=MAX_TOKENS, + top_p=TOP_P, + ) + + # Get tool objects from names + tools = get_tools_from_names(tool_names) + + # Use default tool set if none provided + if tools is None or len(tools) == 0: + tools = [calculator, current_time] + + logger.debug(f"creating agent with model: {model_id}, actor_id: {actor_id}, session_id: {session_id}, total tools: {len(tools)}") + + memory_config = get_memory_config(model_id, session_id, actor_id) + + # Create session manager and cache it by (model_id, session_id, actor_id) + # This allows each model to have its own session manager, avoiding conflicts + # when switching models within the same session + sm_cache_key = (model_id, session_id, actor_id) + if sm_cache_key not in session_manager_cache: + session_manager = AgentCoreMemorySessionManager(memory_config, REGION) + session_manager_cache[sm_cache_key] = session_manager + logger.debug(f"Created new session manager for model: {model_id}") + else: + session_manager = session_manager_cache[sm_cache_key] + logger.debug(f"Reusing session manager for model: {model_id}") + + # Create and cache new agent with specified tools + agent_cache[cache_key] = Agent( + name='Strands_Playground', + model=bedrock_model, + system_prompt=SYSTEM_PROMPT, + tools=tools, + session_manager=session_manager + ) + + # Set state values using the state manager + agent_cache[cache_key].state.set("actor_id", actor_id) + agent_cache[cache_key].state.set("session_id", session_id) + agent_cache[cache_key].state.set("session_name", session_id) + + # Track tools and first invocation for this model + current_tools_per_agent[cache_key] = tool_names + first_invocation_per_model[model_id] = True + + logger.info(f"Agent created and cached for model: {model_id}, session: {session_id}, actor: {actor_id} with {len(tools)} tools") + else: + logger.info(f"Found cached agent for model: {model_id}, session: {session_id}, actor: {actor_id}") + logger.debug(f"cache key: {cache_key} agent cache: {agent_cache}") + + # Agent exists - check if tools have changed + current_tools = current_tools_per_agent.get(cache_key, []) + + if set(tool_names) != set(current_tools): + logger.info(f"Tools changed for agent, dynamically reloading tools") + logger.debug(f"Previous tools: {current_tools}") + logger.debug(f"New tools: {tool_names}") + + # Get new tool objects + tools = get_tools_from_names(tool_names) + if tools and hasattr(agent_cache[cache_key], 'tool_registry'): + try: + processed_tool_names = agent_cache[cache_key].tool_registry.process_tools(tools) + logger.info(f"Processed tools: {processed_tool_names}") + except Exception as e: + logger.error(f"Failed to process tools: {e}") + else: + logger.error(f"no tools found {tools} in agent: {agent_cache[cache_key]}") + + # Dynamically reload tools on existing agent + agent_cache[cache_key].tools = tools + + # Update tracked tool names (strings) + current_tools_per_agent[cache_key] = processed_tool_names + + logger.info(f"Tools dynamically reloaded: {len(tools)} tools now active") + else: + logger.debug(f"Using cached agent with same tools") + + return agent_cache[cache_key] + +@app.entrypoint +def invoke_agent(payload, **kwargs) -> str: + logger.debug('invoked runtime_agent') + """ + Main entrypoint for AgentCore Runtime. + This function will be called when the agent receives a request. + Supports dynamic model switching via model_id and dynamic tool reloading via tools in payload. + """ + try: + logger.debug(f"Payload received: {json.dumps(payload)}") + logger.debug(f"Kwargs received: {kwargs.keys()}") + + # Extract event from kwargs to access headers + event = kwargs.get('event', {}) + + # Extract input, username, model_id, and tools from payload - handle different input formats + username = None + model_id = MODEL_ID # Default model + tool_names = None # Will use default if not specified + memory_id = None + session_id = None # Initialize session_id + + if isinstance(payload, dict): + user_input = payload.get('inputText', payload.get('prompt', payload.get('message', str(payload)))) + username = payload.get('username') + # for now, we're not doing anything with sessionId...it will automatically map to a new path in memory if it changes + session_id = payload.get('sessionId') + model_id = payload.get('model_id', MODEL_ID) # Extract model_id from payload + memory_id = payload.get('memory_id') + tool_names = payload.get('tools') # Extract tools list from payload + elif isinstance(payload, str): + user_input = payload + session_id = "default_session" # Provide default session_id for string payloads + else: + user_input = str(payload) + session_id = "default_session" # Provide default session_id + + logger.debug(f"Using memory: {memory_id}") + + # Validate input + if not user_input or not isinstance(user_input, str): + raise ValueError(f"Invalid input format: expected string, got {type(user_input)}: {user_input}") + + user_input = user_input.strip() + if not user_input: + raise ValueError("Empty input provided") + + # Log the request details + num_tools = f" with {len(tool_names)} tools" if tool_names else " with default tools" + logger.info(f"Received input from user '{username}', session {session_id} using model '{model_id}'{num_tools}: '{user_input}'") + + # Get or create agent for the specified model + agent = get_or_create_agent(model_id, tool_names, username, session_id) + + # Invoke the agent + #logger.debug(F"Invoking {agent} with {user_input}") + result = agent(user_input) + + # Extract response text + response_text = "" + if isinstance(result.message, dict) and "content" in result.message: + if isinstance(result.message["content"], list): + response_text = result.message["content"][0].get("text", str(result.message)) + else: + response_text = result.message["content"] + else: + response_text = str(result.message) + + # Add greeting on first invocation for this model if username is available + greeting = "" + if first_invocation_per_model.get(model_id, False) and username: + tool_count = len(tool_names) if tool_names else 5 + greeting = f"Hello, {username}. I'm now using {model_id} with {tool_count} tools.\n" + first_invocation_per_model[model_id] = False + + # Combine greeting and response + full_response = greeting + response_text + + # Extract metrics if available + usage_metrics = {} + accumulated_metrics = {} + if result.metrics: + logger.info(f"Invocation metrics:") + + # Extract usage metrics (tokens) + if hasattr(result.metrics, 'accumulated_usage') and result.metrics.accumulated_usage: + usage = result.metrics.accumulated_usage + usage_metrics = { + 'inputTokens': usage.get('inputTokens', 0), + 'outputTokens': usage.get('outputTokens', 0), + 'totalTokens': usage.get('totalTokens', 0) + } + logger.info(f" Input Tokens: {usage_metrics['inputTokens']}") + logger.info(f" Output Tokens: {usage_metrics['outputTokens']}") + logger.info(f" Total Tokens: {usage_metrics['totalTokens']}") + + # Extract other metrics + if hasattr(result.metrics, 'accumulated_metrics') and result.metrics.accumulated_metrics: + accumulated_metrics = result.metrics.accumulated_metrics + if 'latencyMs' in accumulated_metrics: + logger.info(f" Latency: {accumulated_metrics['latencyMs']} ms") + + if hasattr(result.metrics, 'cycle_durations') and result.metrics.cycle_durations: + logger.info(f" Cycle Durations: {result.metrics.cycle_durations}") + + if hasattr(result.metrics, 'traces') and result.metrics.traces: + logger.info(f" Traces: {len(result.metrics.traces)} traces") + else: + logger.info("No invocation metrics available") + + # Return JSON response with text and metrics + response_obj = { + 'response': full_response, + 'usage': usage_metrics, + 'metrics': accumulated_metrics + } + + logger.debug(f"Returning JSON response with usage: {usage_metrics}") + return json.dumps(response_obj) + + except Exception as e: + import traceback + error_traceback = traceback.format_exc() + logger.error(f"Error invoking Strands agent: {str(e)}\nTraceback:\n{error_traceback}") + return f"I encountered an error while processing your request: {str(e)}" + +if __name__ == "__main__": + app.run() \ No newline at end of file diff --git a/agentcore-strands-playground/agentcore_playground.md b/agentcore-strands-playground/agentcore_playground.md new file mode 100644 index 0000000..e392b41 --- /dev/null +++ b/agentcore-strands-playground/agentcore_playground.md @@ -0,0 +1,41 @@ +# Bedrock AgentCore Strands Playground + +![](./images/BRAC_interface_screen.png) + +## Overview + +Strands Playground is an example agent in the Strands SDK repo: https://github.com/strands-agents/samples/tree/main/04-UX-demos/05-strands-playground +The agent runs locally and uses a model hosted in Amazon Bedrock. I ported the agent to run in Bedrock AgentCore, and made several changes. + +## Original Agent + +The original agent ran locally, using a FastAPI-based API to implement a Strands agent. The agent used did not have authentication, and used local files or a DynamoDB database for memory. + +## AgentCore Agent + +The new version takes advantage of several AgentCore features. 1/ runs in AgentCore Runtime, a secure, isolated, dynamic execution environment. 2/ (optionally) uses AgentCore Identity to pass a JWT to the agent from the front-end. 3/ uses AgentCore Memory -- both short-term and long-term, to store user preferences and conversation history. 4/ uses AgentCore Observability to track token usage, latency, traces, and spans. + +## Original Interface + +![Strands Playground Screenshot](./images/main_page_screenshot.png) + +## AgentCore Interface + +![Bedrock AgentCore Screenshot](./images/BRAC_interface_screen.png) + +## Extensions + + +| | | +| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ![Tool Selection](./images/tool_selection.png) | The Tool Selection interface remains the same as in the original agent. | +| ![Model Settings](./images/model_settings.png) | Running the agent in Bedrock AgentCore enables several extensions to the agent, which are managed in the Settings panel. Users can log into a Cognito pool; invocations of the agent running in AgentCore require a valid JWT. Users can select a region and Bedrock model. Users can select an agent and version from the region. | +| ![Memory Settings](./images/memory_settings.png) | Users can select a specific Bedrock AgentCore memory store, or use one configured via the AgentCore Starter Toolkit. Bedrock AgentCore memory gives the ability to store different sessions for the same user. Users can name sessions instead of referring to them by a UUID. Users can also change the System Prompt passed to the model. | + +## Architecture + +![AgentCore Architecture](./images/BRAC_architecture.png) + +## Partner Integration Opportunities + +![Agentcore Partners](./images/BRAC_partner_integrations.png) diff --git a/agentcore-strands-playground/app.py b/agentcore-strands-playground/app.py new file mode 100644 index 0000000..0c4fef1 --- /dev/null +++ b/agentcore-strands-playground/app.py @@ -0,0 +1,1195 @@ +import json +import os +import sys +import uuid +import urllib.parse +from typing import Dict, List + +import boto3 +import requests +import streamlit as st +from streamlit.logger import get_logger +from dotenv import load_dotenv +import yaml +from br_utils import get_bedrock_models + +load_dotenv() + +logger = get_logger(__name__) +logger.setLevel("INFO") + +# Check if authentication should be enabled based on COGNITO_DISCOVERY_URL +# Priority: 1) .env file, 2) .bedrock_agentcore.yaml file +cognito_discovery_url = os.getenv('COGNITO_DISCOVERY_URL', '').strip() + +# If not in .env, try to read from .bedrock_agentcore.yaml +# This is somewhat brittle, since it depends on the agent being in the "right" directory +if not cognito_discovery_url: + try: + yaml_path = 'agentcore_agent/.bedrock_agentcore.yaml' + if os.path.exists(yaml_path): + with open(yaml_path, 'r') as f: + config = yaml.safe_load(f) + # Navigate to the nested discoveryUrl in authorizer_configuration + agents = config.get('agents', {}) + default_agent = config.get('default_agent', '') + if default_agent and default_agent in agents: + agent_config = agents[default_agent] + auth_config = agent_config.get('authorizer_configuration', {}) + jwt_config = auth_config.get('customJWTAuthorizer', {}) + cognito_discovery_url = jwt_config.get('discoveryUrl', '').strip() + if cognito_discovery_url: + logger.info(f"Using COGNITO_DISCOVERY_URL from {yaml_path}: {cognito_discovery_url}") + else: + logger.info("No Cognito discovery url found.") + except Exception as e: + logger.warning(f"Could not read COGNITO_DISCOVERY_URL from YAML: {e}") + +ENABLE_AUTH = bool(cognito_discovery_url) + +# Check for --auth or --noauth command-line arguments to override +if '--auth' in sys.argv: + ENABLE_AUTH = True +elif '--noauth' in sys.argv: + ENABLE_AUTH = False + +# Import authentication module OR set auth variables to None +if ENABLE_AUTH: + from auth_utils import require_authentication + logger.info(F"Using authentication: {cognito_discovery_url}") +else: + st.session_state.authenticated = False + st.session_state.access_token = None + st.session_state.id_token = None + st.session_state.refresh_token = None + st.session_state.username = 'default_user' + logger.info("Not using authentication.") + +STRANDS_SYSTEM_PROMPT = os.getenv('STRANDS_SYSTEM_PROMPT', """You are a helpful assistant powered by Strands. Strands Agents is a simple-to-use, code-first framework for building agents - open source by AWS. The user has the ability to modify your set of built-in tools. Every time your tool set is changed, you can propose a new set of tasks that you can do.""") + +# Tool descriptions for Strands built-in tools +tool_descriptions = { + 'agent_graph': 'Create and manage graphs of agents with different topologies and communication patterns', + 'calculator': 'Perform mathematical calculations with support for advanced operations', + 'cron': 'Manage crontab entries for scheduling tasks, with special support for Strands agent jobs', + 'current_time': 'Get the current time in various timezones', + 'editor': 'Editor tool designed to do changes iteratively on multiple files', + 'environment': 'Manage environment variables at runtime', + 'file_read': 'File reading tool with search capabilities, various reading modes, and document mode support', + 'file_write': 'Write content to a file with proper formatting and validation based on file type', + 'generate_image': 'Create images using Stable Diffusion models', + 'http_request': 'Make HTTP requests to external APIs with authentication support', + 'image_reader': 'Read and process image files for AI analysis', + 'journal': 'Create and manage daily journal entries with tasks and notes', + 'load_tool': 'Dynamically load Python tools at runtime', + 'memory': 'Store and retrieve data in Bedrock Knowledge Base', + 'nova_reels': 'Create high-quality videos using Amazon Nova Reel', + 'python_repl': 'Execute Python code in a REPL environment with PTY support and state persistence', + 'retrieve': 'Retrieves knowledge based on the provided text from Amazon Bedrock Knowledge Bases', + 'shell': 'Interactive shell with PTY support for real-time command execution and interaction', + 'slack': 'Comprehensive Slack integration for messaging, events, and interactions', + 'speak': 'Generate speech from text using say command or Amazon Polly', + 'stop': 'Stops the current event loop cycle by setting stop_event_loop flag', + 'swarm': 'Create and coordinate a swarm of AI agents for parallel processing and collective intelligence', + 'think': 'Process thoughts through multiple recursive cycles', + 'use_aws': 'Execute AWS service operations using boto3', + 'use_llm': 'Create isolated agent instances for specific tasks', + 'workflow': 'Advanced workflow orchestration system for parallel AI task execution', + 'websearch': 'search the web using DDG' +} + +# Default selected tools +DEFAULT_TOOLS = ['calculator', 'current_time', 'use_aws', 'websearch'] + +# Page config +st.set_page_config( + page_title="Bedrock AgentCore + Strands Playground", + page_icon="static/gen-ai-dark.svg", + layout="wide", + initial_sidebar_state="expanded", +) + +# Remove Streamlit deployment components +st.markdown( + """ + + """, + unsafe_allow_html=True, +) + +HUMAN_AVATAR = "static/user-profile.svg" +AI_AVATAR = "static/gen-ai-dark.svg" + +# +# I have tried to put all AWS API calls into cached functions to reduce latency on Streamlit reruns +# + +# uncomment caching before commit; for now we're cycling agents too frequently +#@st.cache_data(ttl=300) # Cache for 5 minutes +def fetch_agent_runtimes(region: str = "us-west-2") -> List[Dict]: + """Fetch available agent runtimes from bedrock-agentcore-control""" + try: + client = boto3.client("bedrock-agentcore-control", region_name=region) + response = client.list_agent_runtimes(maxResults=100) + + # Filter only READY agents and sort by name + ready_agents = [ + agent + for agent in response.get("agentRuntimes", []) + if agent.get("status") == "READY" + ] + + # Sort by most recent update time (newest first) + ready_agents.sort(key=lambda x: x.get("lastUpdatedAt", ""), reverse=True) + logger.debug(f"found ready agents: {ready_agents}") + return ready_agents + except Exception as e: + st.error(f"Error fetching agent runtimes: {e}") + return [] + +@st.cache_data(ttl=300) # Cache for 5 minutes +def fetch_agent_runtime_versions( + agent_runtime_id: str, region: str = "us-west-2" +) -> List[Dict]: + """Fetch versions for a specific agent runtime""" + try: + client = boto3.client("bedrock-agentcore-control", region_name=region) + response = client.list_agent_runtime_versions(agentRuntimeId=agent_runtime_id, maxResults=100) + + # Filter only READY versions + ready_versions = [ + version + for version in response.get("agentRuntimes", []) + if version.get("status") == "READY" + ] + + # Sort by most recent update time (newest first) + ready_versions.sort(key=lambda x: x.get("lastUpdatedAt", ""), reverse=True) + logger.debug(f"found agent versions: {ready_versions}") + return ready_versions + except Exception as e: + st.error(f"Error fetching agent runtime versions: {e}") + return [] + +@st.cache_data(ttl=300) # Cache for 5 minutes +# TODO: instead of pulling all memories, look into .bedrock_agentcore.yaml and find the memory ID used by the agent +def fetch_memory(region: str) -> List[Dict]: + """Fetch available memories from bedrock-agentcore-control""" + try: + client = boto3.client("bedrock-agentcore-control", region_name=region) + response = client.list_memories(maxResults=100) + #logger.info(response); + # Filter only READY memories and sort by name + ready_memories = [] + total_memories = response.get("memories", []) + logger.info(f"Total memories retrieved: {len(total_memories)}") + + for memory in total_memories: + memory_id = memory.get("id", "Unknown") + memory_status = memory.get("status", "Unknown") + #logger.info(f"Retrieved memory: {memory_id} (status: {memory_status})") + + if memory.get("status") == "ACTIVE": + ready_memories.append(memory) + #logger.info(f"Added Active memory: {memory_id}") + + #logger.info(f"Final ready_memories count: {len(ready_memories)}") + + # Sort by most recent update time (newest first) + ready_memories.sort(key=lambda x: x.get("updatedAt", ""), reverse=True) + logger.debug(F"Fetched memories: {ready_memories}") + return ready_memories + except Exception as e: + st.error(f"Error fetching memories: {e}") + return [] + +@st.cache_data(ttl=300) # Cache for 5 minutes +def fetch_sessions(region: str, memory_id: str, actor_id: str) -> List[Dict]: + """ + Fetch available sessions from bedrock-agentcore for a specific memory and actor. + https://docs.aws.amazon.com/bedrock-agentcore/latest/APIReference/API_ListSessions.html + """ + try: + #logger.info("fetching sessions using boto3") + client = boto3.client("bedrock-agentcore", region_name=region) + + # List sessions with pagination support + sessions = [] + next_token = None + max_iterations = 10 # Safety limit to prevent infinite loops + + for iteration in range(max_iterations): + params = { + "memoryId": memory_id, + "actorId": actor_id, + "maxResults": 100 + } + if next_token: + params["nextToken"] = next_token + response = client.list_sessions(**params) + # Add sessions from this page + session_summaries = response.get("sessionSummaries", []) + sessions.extend(session_summaries) + #logger.info(f"Page {iteration + 1}: Retrieved {len(session_summaries)} sessions (total: {len(sessions)})") + # Check for more pages + next_token = response.get("nextToken") + if not next_token: + break + + # Check if current session exists in the retrieved sessions + session_ids_list = [s.get('sessionId') for s in sessions if isinstance(s, dict)] + + # If current session is not in the list, create an initialization event for AgentCore Memory + # This is ugly. The only reason to create the event is so that when streamlit refreshes, + # the session will be captured from the list_sessions() call + # TBD: find a better way to get it into the dropdown + if 'runtime_session_id' in st.session_state and st.session_state.runtime_session_id not in session_ids_list: + try: + from datetime import datetime, timezone + + # Extract session name for display + session_name = st.session_state.runtime_session_id + if '_' in session_name: + display_name = session_name.split('_')[0] + if display_name: + session_name = display_name + + #logger.info(f"Creating initialization event for new session: {st.session_state.runtime_session_id}") + + # Create minimal blob event to make session visible in list_sessions + init_response = client.create_event( + memoryId=memory_id, + actorId=actor_id, + sessionId=st.session_state.runtime_session_id, + eventTimestamp=datetime.now(timezone.utc), + payload=[{'blob': 'session_init'}] + ) + + logger.info(f"Session initialization event created: {init_response.get('eventId')}") + + # Add the new session to the list so it appears in the dropdown + sessions.append({ + 'sessionId': st.session_state.runtime_session_id, + 'lastUpdatedDateTime': datetime.now(timezone.utc).isoformat(), + 'eventCount': 1 + }) + + except Exception as e: + logger.error(f"Failed to create session initialization event: {e}") + + # Sort by most recent first (if lastUpdatedDateTime is available) + sessions.sort(key=lambda x: x.get("lastUpdatedDateTime", ""), reverse=True) + + session_ids_log = [session.get("sessionId", "Unknown") for session in sessions] + logger.info(f"Sessions retrieved for actor '{actor_id}': {session_ids_log}") + return sessions + + except client.exceptions.ResourceNotFoundException as e: + # Actor doesn't exist yet - this is normal for new users + logger.info(f"Actor not found (will be created on first interaction): {actor_id}") + return [] + except client.exceptions.ValidationException as e: + st.error(f"Invalid parameters: {e}") + logger.error(f"Validation error: {e}") + return [] + except Exception as e: + st.error(f"Error fetching sessions: {e}") + logger.error(f"Error fetching sessions: {e}") + return [] + +def get_token_usage(usage_data=None): + """Extract token usage from usage_data dict or return zeros""" + if usage_data and isinstance(usage_data, dict): + return { + 'inputTokens': usage_data.get('inputTokens', 0), + 'outputTokens': usage_data.get('outputTokens', 0), + 'totalTokens': usage_data.get('totalTokens', 0) + } + return {'inputTokens': 0, 'outputTokens': 0, 'totalTokens': 0} + +def return_metrics(mode: str, usage_data=None, error_msg: str = None, **kwargs): + """ + Create a standardized metrics dictionary + + Args: + mode: The mode string (e.g., 'AgentCore Runtime (requests + Bearer Token)') + usage_data: Optional usage data dict to pass to get_token_usage() + error_msg: Optional error message + **kwargs: Additional key-value pairs to include in metrics (e.g., model_id, agent_arn, etc.) + + Returns: + Dict with usage, mode, and any additional fields + """ + metrics = { + 'usage': get_token_usage(usage_data), + 'mode': mode + } + + # Add error if provided + if error_msg: + metrics['error'] = error_msg + + # Add any additional kwargs + metrics.update(kwargs) + + return metrics + +def invoke_agentcore_runtime_auth(message: str, agent_arn: str, region: str, access_token: str, session_id: str, username: str = None, memory_id: str = None) -> Dict: + """ + Invoke AgentCore runtime using HTTP requests with bearer token authentication + We need to use 'requests' because boto3 can't handle auth tokens (yet) + """ + try: + # URL encode the agent ARN + escaped_agent_arn = urllib.parse.quote(agent_arn, safe='') + + # Construct the URL + url = f"https://bedrock-agentcore.{region}.amazonaws.com/runtimes/{escaped_agent_arn}/invocations?qualifier=DEFAULT" + + # Set up headers + headers = { + "Authorization": f"Bearer {access_token}", + "X-Amzn-Trace-Id": f"streamlit-session-{session_id[:8] if session_id else 'unknown'}", + "Content-Type": "application/json", + } + # Add session ID header if provided + if session_id: + headers["X-Amzn-Bedrock-AgentCore-Runtime-Session-Id"] = session_id + + # Prepare payload + payload_data = {"prompt": message} + if session_id: + payload_data["sessionId"] = session_id + if username: + payload_data["username"] = username + + # Add model_id to payload - get from session state or use STRANDS_MODEL_ID from .env + model_id = st.session_state.get('selected_bedrock_model_id', os.getenv('STRANDS_MODEL_ID', 'us.amazon.nova-pro-v1:0')) + if model_id: + payload_data["model_id"] = model_id + + # Add memory_id to payload if provided + if memory_id: + payload_data["memory_id"] = memory_id + + # Add selected tools to payload + selected_tools = st.session_state.get('selected_tools', DEFAULT_TOOLS) + if selected_tools: + payload_data["tools"] = selected_tools + + # Enable verbose logging for debugging (optional) + # logging.basicConfig(level=logging.DEBUG) + # logging.getLogger("urllib3.connectionpool").setLevel(logging.DEBUG) + + # Make the HTTP POST request + invoke_response = requests.post( + url, + headers=headers, + data=json.dumps(payload_data), + timeout=300 # 5 minute timeout + ) + + # Handle response based on status code + if invoke_response.status_code == 200: + try: + response_data = invoke_response.json() + + # Check if response_data is a string (double-encoded JSON) + if isinstance(response_data, str): + try: + response_data = json.loads(response_data) + except json.JSONDecodeError: + # If it fails, treat as plain text + pass + + # Extract response text + if isinstance(response_data, dict): + response_text = response_data.get('response', str(response_data)) + + # Extract usage metrics if available + token_usage = get_token_usage(response_data.get('usage')) + else: + response_text = str(response_data) + token_usage = get_token_usage() + response_data = {} # Set to empty dict if not a dict + + except json.JSONDecodeError: + # If response is not JSON, use the text as-is + response_text = invoke_response.text + token_usage = get_token_usage() + response_data = {} # Set to empty dict to prevent AttributeError + + # Include additional metrics if available (only if response_data is a dict) + additional_metrics = response_data.get('metrics', {}) if isinstance(response_data, dict) else {} + + return { + 'success': True, + 'response': response_text, + 'metrics': return_metrics( + mode='AgentCore Runtime (requests + Bearer Token)', + usage_data=token_usage, + model_id=response_data.get('model_id', model_id), + agent_arn=agent_arn, + bearer_token_used=bool(access_token), + latency_ms=additional_metrics.get('latencyMs'), + additional_metrics=additional_metrics + ) + } + + elif invoke_response.status_code >= 400: + # Error response + try: + error_data = invoke_response.json() + error_msg = json.dumps(error_data, indent=2) + except json.JSONDecodeError: + error_msg = invoke_response.text + + st.error(f"Error Response ({invoke_response.status_code}): {error_msg}") + + return { + 'success': False, + 'error': error_msg, + 'response': f"Sorry, I encountered an error ({invoke_response.status_code}): {error_msg}", + 'metrics': return_metrics( + mode='AgentCore Runtime (Error)', + error_msg=error_msg, + status_code=invoke_response.status_code + ) + } + else: + # Unexpected status code + error_msg = f"Unexpected status code: {invoke_response.status_code}" + response_text = invoke_response.text[:500] + + st.error(f"{error_msg}\nResponse: {response_text}") + + return { + 'success': False, + 'error': error_msg, + 'response': f"Sorry, I encountered an unexpected error: {error_msg}", + 'metrics': return_metrics( + mode='AgentCore Runtime (Error)', + error_msg=error_msg, + status_code=invoke_response.status_code + ) + } + + except requests.exceptions.Timeout: + error_msg = "Request timed out after 5 minutes" + st.error(f"Error invoking AgentCore runtime: {error_msg}") + return { + 'success': False, + 'error': error_msg, + 'response': f"Sorry, the request timed out: {error_msg}", + 'metrics': return_metrics( + mode='AgentCore Runtime (Timeout)', + error_msg=error_msg + ) + } + except requests.exceptions.RequestException as e: + error_msg = f"Request error: {str(e)}" + st.error(f"Error invoking AgentCore runtime: {error_msg}") + return { + 'success': False, + 'error': error_msg, + 'response': f"Sorry, I encountered a network error: {error_msg}", + 'metrics': return_metrics( + mode='AgentCore Runtime (Network Error)', + error_msg=error_msg + ) + } + except Exception as e: + error_msg = str(e) + st.error(f"Error invoking AgentCore runtime: {error_msg}") + return { + 'success': False, + 'error': error_msg, + 'response': f"Sorry, I encountered an error: {error_msg}", + 'metrics': return_metrics( + mode='AgentCore Runtime (Error)', + error_msg=error_msg + ) + } + +def invoke_agentcore_runtime_no_auth(message: str, agent_arn: str, region: str, session_id: str, username: str = None, memory_id: str = None) -> Dict: + """ + Invoke AgentCore runtime using boto3 without authentication token + Uses standard AWS credentials (IAM role, profile, etc.) + """ + try: + # Initialize the Amazon Bedrock AgentCore client + agent_core_client = boto3.client('bedrock-agentcore', region_name=region) + + # Prepare payload data + payload_data = {"prompt": message} + if session_id: + payload_data["sessionId"] = session_id + if username: + payload_data["username"] = username + + # Add model_id to payload - get from session state or use STRANDS_MODEL_ID from .env + model_id = st.session_state.get('selected_bedrock_model_id', os.getenv('STRANDS_MODEL_ID', 'us.amazon.nova-pro-v1:0')) + if model_id: + payload_data["model_id"] = model_id + + # Add memory_id to payload if provided + if memory_id: + payload_data["memory_id"] = memory_id + + # Add selected tools to payload + selected_tools = st.session_state.get('selected_tools', DEFAULT_TOOLS) + if selected_tools: + payload_data["tools"] = selected_tools + + # Encode payload as JSON bytes + payload = json.dumps(payload_data).encode() + + # Invoke the agent + response = agent_core_client.invoke_agent_runtime( + agentRuntimeArn=agent_arn, + runtimeSessionId=session_id if session_id else str(uuid.uuid4()), + payload=payload, + qualifier="DEFAULT" + ) + + # Collect response chunks + content = [] + for chunk in response.get("response", []): + content.append(chunk.decode('utf-8')) + + # Parse the complete response + response_text = ''.join(content) + + try: + response_data = json.loads(response_text) + + # Check if response is double-encoded (string instead of dict) + if isinstance(response_data, str): + response_data = json.loads(response_data) + + # Extract response text + if isinstance(response_data, dict): + final_response = response_data.get('response', str(response_data)) + + # Extract usage metrics if available + token_usage = get_token_usage(response_data.get('usage')) + + # Include additional metrics if available + additional_metrics = response_data.get('metrics', {}) + else: + final_response = str(response_data) + token_usage = get_token_usage() + response_data = {} + additional_metrics = {} + except json.JSONDecodeError as e: + # If response is not JSON, use the text as-is + logger.error(f"JSON decode error: {e}") + final_response = response_text + token_usage = get_token_usage() + response_data = {} + additional_metrics = {} + + return { + 'success': True, + 'response': final_response, + 'metrics': return_metrics( + mode='AgentCore Runtime (boto3 - No Auth Token)', + usage_data=token_usage, + model_id=response_data.get('model_id', model_id), + agent_arn=agent_arn, + bearer_token_used=False, + latency_ms=additional_metrics.get('latencyMs'), + additional_metrics=additional_metrics + ) + } + + except agent_core_client.exceptions.ResourceNotFoundException as e: + error_msg = f"Agent runtime not found: {str(e)}" + st.error(f"Error invoking AgentCore runtime: {error_msg}") + return { + 'success': False, + 'error': error_msg, + 'response': f"Sorry, the agent runtime was not found: {error_msg}", + 'metrics': return_metrics( + mode='AgentCore Runtime (boto3 - Error)', + error_msg=error_msg + ) + } + except agent_core_client.exceptions.ValidationException as e: + error_msg = f"Invalid parameters: {str(e)}" + st.error(f"Error invoking AgentCore runtime: {error_msg}") + return { + 'success': False, + 'error': error_msg, + 'response': f"Sorry, invalid parameters: {error_msg}", + 'metrics': return_metrics( + mode='AgentCore Runtime (boto3 - Error)', + error_msg=error_msg + ) + } + except Exception as e: + error_msg = str(e) + st.error(f"Error invoking AgentCore runtime: {error_msg}") + return { + 'success': False, + 'error': error_msg, + 'response': f"Sorry, I encountered an error: {error_msg}", + 'metrics': return_metrics( + mode='AgentCore Runtime (boto3 - Error)', + error_msg=error_msg + ) + } + + +def render_metrics_panel(): + """Render the metrics summary panel""" + st.subheader("📊 Metrics Summary") + + st.markdown(""" +
+
+ """, unsafe_allow_html=True) + + # Display metrics if available + if 'last_metrics' in st.session_state: + metrics = st.session_state.last_metrics + + # Check if metrics contain usage information + if 'usage' in metrics and metrics['usage']: + usage = metrics['usage'] + + st.subheader("🎯 Token Usage") + col1, col2, col3 = st.columns(3) + with col1: + st.metric("Input Tokens", usage.get('inputTokens', 0)) + with col2: + st.metric("Output Tokens", usage.get('outputTokens', 0)) + with col3: + st.metric("Total Tokens", usage.get('totalTokens', 0)) + + # Performance metrics + if 'latency_ms' in metrics and metrics['latency_ms']: + st.subheader("⚡ Performance") + col1, col2 = st.columns(2) + with col1: + st.metric("Latency", f"{metrics['latency_ms']:.2f} ms") + with col2: + if 'additional_metrics' in metrics and metrics['additional_metrics']: + additional = metrics['additional_metrics'] + if 'firstByteLatencyMs' in additional: + st.metric("First Byte", f"{additional['firstByteLatencyMs']:.2f} ms") + + # Error information if available + if 'error' in metrics: + st.subheader("⚠️ Error Details") + st.error(metrics['error']) + + else: + st.info("Metrics will appear after your first interaction with the AgentCore runtime.") + +def render_tools_panel_content(): + """Render the tools panel content for use inside sidebar expander""" + # Initialize selected tools in session state if not exists + if 'selected_tools' not in st.session_state: + st.session_state.selected_tools = DEFAULT_TOOLS.copy() + + st.markdown("**📋 Tool Selection**") + st.info("💡 Select the tools you want the agent to have access to. Changes will be applied when you click Update Tools.") + + st.markdown("---") + + # Create checkboxes for each tool + st.markdown("**🛠️ Available Tools:**") + + # Use a container with scroll for better UX + tools_container = st.container(height=300) + + with tools_container: + # Track selections in a temporary dict + tool_selections = {} + + for tool_name, description in sorted(tool_descriptions.items()): + is_selected = tool_name in st.session_state.selected_tools + tool_selections[tool_name] = st.checkbox( + f"**{tool_name}**", + value=is_selected, + key=f"tool_checkbox_{tool_name}", + help=description + ) + + st.markdown("---") + + # Update and Reset buttons + st.markdown("**⚙️ Actions**") + col1, col2 = st.columns([1, 1]) + with col1: + if st.button("🔄 Update Tools", use_container_width=True): + # Update selected tools based on checkboxes + new_selected_tools = [tool_name for tool_name, is_selected in tool_selections.items() if is_selected] + + #if len(new_selected_tools) == 0: + # st.warning("⚠️ Please select at least one tool") + #else: + st.session_state.selected_tools = new_selected_tools + st.success(f"✅ Updated! {len(new_selected_tools)} tool(s) selected") + st.rerun() + + with col2: + if st.button("↻ Reset to Default", use_container_width=True): + st.session_state.selected_tools = DEFAULT_TOOLS.copy() + st.success("✅ Reset to default tools") + st.rerun() + + st.markdown("---") + + # Show currently selected tools - styled like settings sidebar + st.markdown("**📊 Current Status**") + st.write(f"**Active Tools:** {len(st.session_state.selected_tools)}") + if st.session_state.selected_tools: + st.caption(", ".join(sorted(st.session_state.selected_tools))) + else: + st.caption("No tools selected") + +def show_settings_sidebar(auth): + """Show Settings sidebar with authentication info and all configuration options""" + + # Tools Configuration Section (collapsible) + with st.sidebar.expander("🔧 Tools Configuration", expanded=False): + render_tools_panel_content() + + st.sidebar.markdown("---") + + # Settings Section (collapsible) + with st.sidebar.expander("⚙️ Settings", expanded=True): + # Authentication section + if ENABLE_AUTH: + if auth.is_authenticated(): + st.markdown("**🔐 Authentication**") + st.write(f"👤 **User:** {auth.get_username()}") + st.write(f"� **Pool:** *{auth.pool_id}") + + if st.button("� Logoout", use_container_width=True): + auth.logout() + st.rerun() + + st.markdown("---") + else: + st.markdown("**🔐 Authentication**") + st.write(f"👤 **User:** default_user") + + st.markdown("---") + + # Region selection + st.markdown("**AWS Region**") + default_region = os.getenv('AWS_REGION', 'us-west-2') + regions = ["us-west-2", "us-west-2", "eu-west-1", "ap-southeast-1"] + + try: + default_index = regions.index(default_region) + except ValueError: + regions.insert(0, default_region) + default_index = 0 + + region = st.selectbox( + "Region", + regions, + index=default_index, + help=f"Using {default_region} as default" + ) + st.session_state.selected_region = region + + # Model selection - Bedrock models with tool use support + st.markdown("**Bedrock Model (Tool Use)**") + + # Cache key for models based on region + models_cache_key = f"bedrock_tool_models_{region}" + if models_cache_key not in st.session_state or st.session_state.get('last_model_region') != region: + with st.spinner("Loading Bedrock models..."): + available_models = get_bedrock_models() + st.session_state[models_cache_key] = available_models + st.session_state.last_model_region = region + else: + available_models = st.session_state[models_cache_key] + + if available_models: + # available_models is now a list of model ID strings + model_options = available_models + + # Initialize selected model in session state - try to find STRANDS_MODEL_ID or use first option + if 'selected_bedrock_model_id' not in st.session_state: + # Get STRANDS_MODEL_ID from environment + strands_model_id = os.getenv('STRANDS_MODEL_ID', 'us.amazon.nova-pro-v1:0') + + # Use STRANDS_MODEL_ID if available in the list, otherwise first option + if strands_model_id in available_models: + st.session_state.selected_bedrock_model_id = strands_model_id + else: + st.session_state.selected_bedrock_model_id = available_models[0] + + # Determine the index for the selectbox + current_selection = st.session_state.get('selected_bedrock_model_id', available_models[0]) + try: + current_index = available_models.index(current_selection) if current_selection in available_models else 0 + except ValueError: + current_index = 0 + + selected_model_id = st.selectbox( + "Model", + options=model_options, + index=current_index, + help="Select a Bedrock model" + ) + + st.session_state.selected_bedrock_model_id = selected_model_id + + else: + st.warning("No tool-use capable models found in this region") + st.session_state.selected_bedrock_model_id = None + + # Agent selection + st.markdown("---") + st.markdown("**🤖 Agent Selection**") + + # Initialize agent_arn in session state + if 'selected_agent_arn' not in st.session_state: + st.session_state.selected_agent_arn = "" + + # AgentCore selection logic - only fetch if needed + if 'available_agents' not in st.session_state or st.session_state.get('last_region') != region: + with st.spinner("Loading available agents..."): + available_agents = fetch_agent_runtimes(region) + st.session_state.available_agents = available_agents + st.session_state.last_region = region + else: + available_agents = st.session_state.available_agents + + agent_arn = "" + if available_agents: + unique_agents = {} + for agent in available_agents: + name = agent.get("agentRuntimeName", "Unknown") + runtime_id = agent.get("agentRuntimeId", "") + if name not in unique_agents: + unique_agents[name] = runtime_id + + agent_names = list(unique_agents.keys()) + + selected_agent_name = st.selectbox( + "Agent Name", + options=agent_names, + help="Choose an agent to chat with", + ) + + if selected_agent_name and selected_agent_name in unique_agents: + agent_runtime_id = unique_agents[selected_agent_name] + + # Only fetch versions if agent changed + version_key = f"{agent_runtime_id}_{region}" + if 'agent_versions' not in st.session_state or st.session_state.get('last_agent_key') != version_key: + with st.spinner("Loading versions..."): + agent_versions = fetch_agent_runtime_versions(agent_runtime_id, region) + st.session_state.agent_versions = agent_versions + st.session_state.last_agent_key = version_key + else: + agent_versions = st.session_state.agent_versions + + if agent_versions: + version_options = [] + version_arn_map = {} + + for version in agent_versions: + version_num = version.get("agentRuntimeVersion", "Unknown") + arn = version.get("agentRuntimeArn", "") + updated = version.get("lastUpdatedAt", "") + description = version.get("description", "") + + version_display = f"v{version_num}" + if updated: + try: + if hasattr(updated, "strftime"): + updated_str = updated.strftime("%m/%d %H:%M") + version_display += f" ({updated_str})" + except: + pass + + version_options.append(version_display) + version_arn_map[version_display] = { + "arn": arn, + "description": description, + } + + selected_version = st.selectbox( + "Version", + options=version_options, + help="Choose the version to use", + ) + + version_info = version_arn_map.get(selected_version, {}) + agent_arn = version_info.get("arn", "") + st.session_state.selected_agent_arn = agent_arn + + + # Memory selection + st.markdown("---") + st.markdown("**🧠 Memory Configuration**") + + # Initialize memory_id in session state if not already there + if 'selected_memory_id' not in st.session_state: + st.session_state.selected_memory_id = "" + + # Memory selection logic - only fetch if needed + if 'available_memories' not in st.session_state or st.session_state.get('last_memory_region') != region: + with st.spinner("Loading available memories..."): + available_memories = fetch_memory(region) + memory_ids_log = [memory.get("id", "Unknown") for memory in available_memories] + logger.info(f"Available memory: {memory_ids_log}") + st.session_state.available_memories = available_memories + st.session_state.last_memory_region = region + else: + available_memories = st.session_state.available_memories + + memory_id = "" + if available_memories: + memory_ids = [memory.get("id", "Unknown") for memory in available_memories] + #logger.info(f"found memory: {memory_ids}") + + # Auto-select the first (most recent) memory if no memory is currently selected + if not st.session_state.get('selected_memory_id') and memory_ids: + st.session_state.selected_memory_id = memory_ids[0] + + # Set the index for the selectbox based on the selected memory + current_selection = st.session_state.get('selected_memory_id', "") + if current_selection in memory_ids: + default_index = memory_ids.index(current_selection) + 1 # +1 because "None" is at index 0 + else: + default_index = 1 if memory_ids else 0 # Select first memory if available, otherwise "None" + + selected_memory_id = st.selectbox( + "Memory ID", + options=["None"] + memory_ids, + index=default_index, + help="Choose a memory to use with the agent", + ) + + if selected_memory_id and selected_memory_id != "None": + memory_id = selected_memory_id + st.session_state.selected_memory_id = memory_id + else: + st.session_state.selected_memory_id = "" + else: + st.info("No memories found or none available") + st.session_state.selected_memory_id = "" + + # Runtime Session ID - Simple configuration from streamlit-chat + # sessions consist of an optional name followed by underscore followed by uuid, + # since agentcore requires sessionId to be at least 32 characters + # we extract the name from the beginning of the string to use in the dropdown + # so users can give sessions human-readable names, while agentcore + # gets a name_uuid it can use for memory, session management, etc + st.markdown("---") + st.markdown("**🔗 Session Configuration**") + + # Fetch existing sessions (to populate dropdown) + existing_sessions = fetch_sessions(region, st.session_state.selected_memory_id, st.session_state.get('username')) + + if not existing_sessions or not isinstance(existing_sessions, list): + existing_sessions = [{"sessionId": "default_"+str(uuid.uuid4())}] + + # Initialize session ID in session state if there is none + if "runtime_session_id" not in st.session_state: + st.session_state.runtime_session_id = existing_sessions[0].get('sessionId', str(uuid.uuid4())) + logger.info(f"Initialized with session: {st.session_state.runtime_session_id}") + + logger.info(f"Found {len(existing_sessions)} sessions") + + # Helper function to format session display name + def format_session_display(session_id): + """Extract display name from 'name_uuid' format. If no name before _, show full string.""" + if '_' in session_id: + name_part = session_id.split('_')[0] + if name_part: # If there's a name before _ + return name_part + return session_id # Return full string if no _ or no name before _ + + # Session dropdown - select from existing sessions + session_ids = [] + for session in existing_sessions: + if isinstance(session, dict): + session_ids.append(session.get('sessionId', 'Unknown')) + else: + logger.warning(f"Unexpected session format: {session}") + session_ids.append(str(session)) + + # Create display names for dropdown + session_display_names = [format_session_display(sid) for sid in session_ids] + + # Find current session index + try: + current_index = session_ids.index(st.session_state.runtime_session_id) + except ValueError: + current_index = 0 + + selected_display = st.selectbox( + "Select Existing Session", + options=session_display_names, + index=current_index, + help="Choose an existing session from the dropdown" + ) + + # Map display name back to full session ID + selected_index = session_display_names.index(selected_display) + selected_session = session_ids[selected_index] + + # Update session state when user selects from dropdown + if selected_session != st.session_state.runtime_session_id: + st.session_state.runtime_session_id = selected_session + + # New session input with button + # Use session state to control the input value + if 'new_session_input' not in st.session_state: + st.session_state.new_session_input = "" + + new_session_name = st.text_input( + "New Session Name", + value=st.session_state.new_session_input, + placeholder="Enter session name...", + help="Type a session name and click 'New Session' to create", + key="new_session_name_input" + ) + + if st.button("🔄 New Session", help="Create new session with the name above and clear chat"): + if new_session_name.strip(): + # Format as name_uuid (using _ instead of # for AWS compliance) + st.session_state.runtime_session_id = f"{new_session_name.strip()}_{str(uuid.uuid4())}" + else: + # Generate UUID only (no name prefix) + st.session_state.runtime_session_id = str(uuid.uuid4()) + + st.session_state.messages = [] # Clear chat messages when creating new session + if 'last_metrics' in st.session_state: + del st.session_state.last_metrics + + # Clear the input box for next time + st.session_state.new_session_input = "" + + logger.info(f"Created new session: {st.session_state.runtime_session_id}") + st.rerun() + + # System prompt configuration + st.markdown("---") + st.markdown("**📝 System Prompt**") + system_prompt = st.text_area( + "System Prompt", + value=st.session_state.get('system_prompt', STRANDS_SYSTEM_PROMPT), + height=100, + help="Configure the agent's behavior" + ) + st.session_state.system_prompt = system_prompt + + # Clear chat button + st.markdown("---") + if st.button("🗑️ Clear Chat", use_container_width=True): + # TBD: clear AgentCore memory for this session if applicable + st.session_state.messages = [] + if 'last_metrics' in st.session_state: + del st.session_state.last_metrics + st.rerun() + + +def main(): + if ENABLE_AUTH: + auth = require_authentication() + else: + auth = None + + # Show Settings and Tools in sidebar + show_settings_sidebar(auth) + + st.logo("static/agentcore-service-icon.png", size="large") + st.title("Amazon Bedrock AgentCore Strands Playground") + + # Create two-column layout (removed tools column - now in sidebar) + col1, col2 = st.columns([2, 1]) + + # Panel 1: AgentCore Chat + with col1: + #st.header("AgentCore Chat") + + # Initialize chat history + if "messages" not in st.session_state: + st.session_state.messages = [] + + # Display chat messages + chat_container = st.container(height=500) + with chat_container: + for message in st.session_state.messages: + with st.chat_message(message["role"], avatar=message.get("avatar", AI_AVATAR if message["role"] == "assistant" else HUMAN_AVATAR)): + st.markdown(message["content"]) + + # Chat input + if prompt := st.chat_input("Type your message here..."): + # AgentCore mode implementation + selected_agent_arn = st.session_state.get('selected_agent_arn', '') + if selected_agent_arn: + # Add user message to chat history + st.session_state.messages.append( + {"role": "user", "content": prompt, "avatar": HUMAN_AVATAR} + ) + + # Generate assistant response using AgentCore Runtime + # Show spinner outside chat to avoid duplication + with st.spinner("Calling AgentCore Runtime..."): + # Get region from sidebar session state + region = st.session_state.get('selected_region', 'us-west-2') + + if ENABLE_AUTH: + agentcore_response = invoke_agentcore_runtime_auth( + message=prompt, + agent_arn=selected_agent_arn, + region=region, + access_token=st.session_state.access_token, + session_id=st.session_state.runtime_session_id, + username=st.session_state.get('username'), + memory_id=st.session_state.get('selected_memory_id') + ) + else: + agentcore_response = invoke_agentcore_runtime_no_auth( + message=prompt, + agent_arn=selected_agent_arn, + region=region, + session_id=st.session_state.runtime_session_id, + username=st.session_state.get('username'), + memory_id=st.session_state.get('selected_memory_id') + ) + logger.info(f"AgentCore Response - Success: {agentcore_response.get('success')}, Response Length: {len(agentcore_response.get('response', ''))}, Metrics: {agentcore_response.get('metrics')}") + logger.info(f"AgentCore Response Text: {agentcore_response.get('response', '')}") + if agentcore_response['success']: + response_text = agentcore_response['response'] + # Store metrics for the metrics panel + st.session_state.last_metrics = agentcore_response['metrics'] + else: + response_text = agentcore_response['response'] + st.session_state.last_metrics = agentcore_response['metrics'] + + # Add assistant response to chat history + st.session_state.messages.append( + {"role": "assistant", "content": response_text, "avatar": AI_AVATAR} + ) + + # Rerun to display the new messages from session state + st.rerun() + else: + if not selected_agent_arn: + st.warning("⚠️ Please select an AgentCore agent to continue.") + else: + st.warning("⚠️ Authentication required. Please log in again.") + + # Panel 2: Metrics Summary + with col2: + render_metrics_panel() + +if __name__ == "__main__": + main() diff --git a/agentcore-strands-playground/auth_utils.py b/agentcore-strands-playground/auth_utils.py new file mode 100644 index 0000000..bc4d58d --- /dev/null +++ b/agentcore-strands-playground/auth_utils.py @@ -0,0 +1,246 @@ +""" +Cognito Authentication Module for Streamlit AgentCore Chat +""" + +import os +import boto3 +import streamlit as st +from typing import Dict, Optional +from boto3.session import Session +from dotenv import load_dotenv +import yaml + +# Load environment variables +load_dotenv() + +class CognitoAuth: + """Handles Cognito authentication for the Streamlit app""" + + def __init__(self): + self.pool_id = os.getenv('COGNITO_POOL_ID') + self.client_id = os.getenv('COGNITO_CLIENT_ID') + self.region = os.getenv('AWS_REGION', 'us-west-2') + + # Get discovery URL from .env first, then try YAML file + self.discovery_url = os.getenv('COGNITO_DISCOVERY_URL', '').strip() + + # If not in .env, try to read from .bedrock_agentcore.yaml + if not self.discovery_url: + try: + yaml_path = 'agentcore_agent/.bedrock_agentcore.yaml' + if os.path.exists(yaml_path): + with open(yaml_path, 'r') as f: + config = yaml.safe_load(f) + # Try to get discoveryUrl from the default agent or ac_auth agent + default_agent = config.get('default_agent', 'ac_auth') + agent_config = config.get('agents', {}).get(default_agent, {}) + auth_config = agent_config.get('authorizer_configuration', {}) + jwt_config = auth_config.get('customJWTAuthorizer', {}) + self.discovery_url = jwt_config.get('discoveryUrl', '').strip() + + # Also try to extract pool_id and client_id from discovery URL if not set + if self.discovery_url and not self.pool_id: + # Extract pool_id from discovery URL + # Format: https://cognito-idp.{region}.amazonaws.com/{pool_id}/.well-known/... + parts = self.discovery_url.split('/') + if len(parts) >= 4: + self.pool_id = parts[3] + + # Get client_id from allowedClients if not set + if not self.client_id: + allowed_clients = jwt_config.get('allowedClients', []) + if allowed_clients: + self.client_id = allowed_clients[0] + except Exception as e: + # Silently fail - will be caught by config_available check + pass + + self.default_username = os.getenv('DEFAULT_USERNAME', 'testuser1') + self.default_password = os.getenv('DEFAULT_PASSWORD', 'MyPassword123!') + + # Check if configuration is available + self.config_available = all([self.pool_id, self.client_id]) + + # Initialize Cognito client only if config is available + if self.config_available: + try: + self.cognito_client = boto3.client('cognito-idp', region_name=self.region) + except Exception as e: + st.error(f"❌ Failed to initialize Cognito client: {str(e)}") + self.config_available = False + self.cognito_client = None + else: + self.cognito_client = None + + def authenticate_user(self, username: str, password: str) -> Dict: + """ + Authenticate user with Cognito and return tokens + + Args: + username: Cognito username + password: User password + + Returns: + Dict with success status and tokens or error message + """ + try: + auth_response = self.cognito_client.initiate_auth( + ClientId=self.client_id, + AuthFlow='USER_PASSWORD_AUTH', + AuthParameters={ + 'USERNAME': username, + 'PASSWORD': password + } + ) + + return { + 'success': True, + 'access_token': auth_response['AuthenticationResult']['AccessToken'], + 'id_token': auth_response['AuthenticationResult']['IdToken'], + 'refresh_token': auth_response['AuthenticationResult']['RefreshToken'], + 'expires_in': auth_response['AuthenticationResult']['ExpiresIn'] + } + + except self.cognito_client.exceptions.NotAuthorizedException: + return {'success': False, 'error': 'Invalid username or password'} + except self.cognito_client.exceptions.UserNotFoundException: + return {'success': False, 'error': 'User not found'} + except Exception as e: + return {'success': False, 'error': f'Authentication failed: {str(e)}'} + + def refresh_access_token(self, refresh_token: str) -> Dict: + """ + Refresh access token using refresh token + + Args: + refresh_token: Cognito refresh token + + Returns: + Dict with success status and new access token or error message + """ + try: + auth_response = self.cognito_client.initiate_auth( + ClientId=self.client_id, + AuthFlow='REFRESH_TOKEN_AUTH', + AuthParameters={ + 'REFRESH_TOKEN': refresh_token + } + ) + + return { + 'success': True, + 'access_token': auth_response['AuthenticationResult']['AccessToken'], + 'expires_in': auth_response['AuthenticationResult']['ExpiresIn'] + } + + except Exception as e: + return {'success': False, 'error': f'Token refresh failed: {str(e)}'} + + def is_authenticated(self) -> bool: + """Check if user is currently authenticated""" + return st.session_state.get('authenticated', False) + + def get_access_token(self) -> Optional[str]: + """Get current access token from session""" + return st.session_state.get('access_token') + + def get_username(self) -> Optional[str]: + """Get current username from session""" + return st.session_state.get('username') + + def logout(self): + """Clear authentication session""" + keys_to_clear = [ + 'authenticated', + 'access_token', + 'id_token', + 'refresh_token', + 'username', + 'token_expires_at' + ] + + for key in keys_to_clear: + if key in st.session_state: + del st.session_state[key] + + def show_login_form(self): + """Display login form and handle authentication""" + st.title("🔐 Login to AgentCore Chat") + + # Check if configuration is available + if not self.config_available: + st.error("❌ Cognito configuration missing. Please check your .env file.") + st.info(""" + **Required environment variables:** + - COGNITO_POOL_ID + - COGNITO_CLIENT_ID + - AWS_REGION (optional, defaults to us-west-2) + + Please update your .env file and restart the application. + """) + st.stop() + return + + # Show configuration info + with st.expander("ℹ️ Configuration Info"): + st.write(f"**User Pool ID:** {self.pool_id}") + st.write(f"**Client ID:** {self.client_id}") + st.write(f"**Region:** {self.region}") + + with st.form("login_form"): + st.write("### Enter your credentials") + + username = st.text_input( + "Username", + value=self.default_username, + help="Default: testuser (from notebook example)" + ) + + password = st.text_input( + "Password", + type="password", + value=self.default_password, + help="Default: MyPassword123! (from notebook example)" + ) + + submit = st.form_submit_button("🚀 Login", use_container_width=True) + + if submit: + if username and password: + with st.spinner("🔄 Authenticating with Cognito..."): + result = self.authenticate_user(username, password) + + if result['success']: + # Store authentication data in session + st.session_state.authenticated = True + st.session_state.access_token = result['access_token'] + st.session_state.id_token = result['id_token'] + st.session_state.refresh_token = result['refresh_token'] + st.session_state.username = username + + st.success("✅ Login successful!") + st.balloons() + + # Immediately rerun to load authenticated app + st.rerun() + else: + st.error(f"❌ Login failed: {result['error']}") + else: + st.error("⚠️ Please enter both username and password") + +def get_auth_instance() -> CognitoAuth: + """Get or create CognitoAuth instance""" + if 'auth_instance' not in st.session_state: + st.session_state.auth_instance = CognitoAuth() + return st.session_state.auth_instance + + +def require_authentication(): + """Decorator-like function to require authentication""" + auth = get_auth_instance() + + if not auth.is_authenticated(): + auth.show_login_form() + st.stop() + + return auth \ No newline at end of file diff --git a/agentcore-strands-playground/br_utils.py b/agentcore-strands-playground/br_utils.py new file mode 100644 index 0000000..c2bec6a --- /dev/null +++ b/agentcore-strands-playground/br_utils.py @@ -0,0 +1,61 @@ +""" +Bedrock Utilities Module + +This module provides helper functions for interacting with Amazon Bedrock, +including listing enabled foundation models in the configured AWS region. +""" + +import os +from typing import List, Dict, Optional +import boto3 +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +def get_bedrock_models() -> List[str]: + """ + Get a list of all enabled Bedrock foundation model identifiers. + + Returns: + List of model identifier strings. + """ + models = [ + 'us.amazon.nova-micro-v1:0', + 'us.amazon.nova-lite-v1:0', + 'us.amazon.nova-pro-v1:0', + 'anthropic.claude-3-5-sonnet-20241022-v2:0', + 'anthropic.claude-3-5-haiku-20241022-v1:0', + 'anthropic.claude-3-5-sonnet-20240620-v1:0', + 'anthropic.claude-3-sonnet-20240229-v1:0', + 'anthropic.claude-3-haiku-20240307-v1:0', + 'anthropic.claude-3-opus-20240229-v1:0', + 'cohere.command-r-v1:0', + 'cohere.command-r-plus-v1:0', + 'meta.llama3-1-405b-instruct-v1:0', + 'meta.llama3-1-70b-instruct-v1:0', + 'meta.llama3-1-8b-instruct-v1:0', + 'meta.llama3-70b-instruct-v1:0', + 'meta.llama3-8b-instruct-v1:0', + 'mistral.mistral-large-2407-v1:0', + 'mistral.mistral-large-2402-v1:0', + 'mistral.mixtral-8x7b-instruct-v0:1', + 'mistral.mistral-7b-instruct-v0:2', + 'qwen.qwen3-235b-a22b-2507-v1:0', + 'qwen.qwen3-coder-480b-a35b-v1:0', + 'qwen.qwen3-coder-30b-a3b-v1:0', + 'qwen.qwen3-32b-v1:0', + 'openai.gpt-oss-120b-1:0', + 'openai.gpt-oss-20b-1:0', + 'deepseek.v3-v1:0' + ] + return models + +if __name__ == "__main__": + # Example usage when run directly + print("Fetching enabled Bedrock models...") + models = get_bedrock_models() + + print(f"\nAvailable Bedrock Models ({len(models)} total):\n") + for model_id in models: + print(f" - {model_id}") \ No newline at end of file diff --git a/agentcore-strands-playground/cleanup.py b/agentcore-strands-playground/cleanup.py new file mode 100755 index 0000000..40d9523 --- /dev/null +++ b/agentcore-strands-playground/cleanup.py @@ -0,0 +1,404 @@ +#!/usr/bin/env python3 +""" +Cleanup script to delete resources created by config.py +Reads resource identifiers from .env file + +Usage: + ./cleanup.py # Delete all resources (with confirmation) + ./cleanup.py --all # Delete all resources (with confirmation) + ./cleanup.py --gateway # Delete only Gateway and targets + ./cleanup.py --lambda # Delete only Lambda function + ./cleanup.py --iam # Delete only IAM roles + ./cleanup.py --cognito # Delete only Cognito resources + ./cleanup.py --gateway --lambda # Delete Gateway and Lambda + ./cleanup.py --help # Show this help message +""" + +import os +import sys +import time +import argparse +import boto3 +from dotenv import load_dotenv +from botocore.exceptions import ClientError + +load_dotenv() + +import utils + +# Get region from environment or boto3 session +REGION = os.getenv('AWS_DEFAULT_REGION') or os.getenv('AWS_REGION') or boto3.Session().region_name +if not REGION: + print("Warning: No AWS region found. Defaulting to us-west-2") + REGION = 'us-west-2' +print(f"Using AWS region: {REGION}") + +# Resource identifiers from .env file (created by config.py) +# Lambda +LAMBDA_FUNCTION_NAME = os.getenv('LAMBDA_FUNCTION_NAME', 'playground_lambda') +LAMBDA_FUNCTION_ARN = os.getenv('LAMBDA_FUNCTION_ARN') + +# IAM Roles +LAMBDA_IAM_ROLE_NAME = os.getenv('LAMBDA_IAM_ROLE_NAME', 'playground_lambda_iamrole') +GATEWAY_IAM_ROLE_NAME = os.getenv('GATEWAY_IAM_ROLE_NAME', 'agentcore-playground-lambdagateway-role') +GATEWAY_IAM_ROLE_ARN = os.getenv('GATEWAY_IAM_ROLE_ARN') + +# Cognito +COGNITO_POOL_ID = os.getenv('COGNITO_POOL_ID') +COGNITO_POOL_NAME = os.getenv('COGNITO_POOL_NAME', 'agentcore-strands-playground-pool') +CLIENT_ID = os.getenv('COGNITO_CLIENT_ID') +CLIENT_NAME = os.getenv('COGNITO_CLIENT_NAME', 'playground-gateway-client') +RESOURCE_SERVER_ID = os.getenv('COGNITO_RESOURCE_SERVER_ID', 'playground-gateway-id') +RESOURCE_SERVER_NAME = os.getenv('COGNITO_RESOURCE_SERVER_NAME', 'playground-gateway-name') + +# Gateway +GATEWAY_ID = os.getenv('GATEWAY_ID') +GATEWAY_NAME = os.getenv('GATEWAY_NAME', 'TestGWforLambda') +GATEWAY_URL = os.getenv('GATEWAY_URL') +GATEWAY_TARGET_NAME = os.getenv('GATEWAY_TARGET_NAME', 'LambdaUsingSDK') +GATEWAY_TARGET_ID = os.getenv('GATEWAY_TARGET_ID') + +def delete_gateway_and_targets(): + """Delete AgentCore Gateway and its targets""" + print("\n=== Deleting AgentCore Gateway ===") + try: + gateway_client = boto3.client('bedrock-agentcore-control', REGION) + + # Use GATEWAY_ID from .env if available, otherwise search by name + gateway_id = GATEWAY_ID + + if not gateway_id: + print(f"No GATEWAY_ID in .env, searching by name: {GATEWAY_NAME}") + list_response = gateway_client.list_gateways(maxResults=100) + gateway = next((gw for gw in list_response.get('items', []) if gw.get('name') == GATEWAY_NAME), None) + if gateway: + gateway_id = gateway.get('gatewayId') + else: + print(f"Gateway '{GATEWAY_NAME}' not found") + return + + print(f"Found gateway: {gateway_id}") + + # Delete all targets first + print("Deleting gateway targets...") + try: + targets_response = gateway_client.list_gateway_targets( + gatewayIdentifier=gateway_id, + maxResults=100 + ) + for target in targets_response.get('items', []): + target_id = target.get('targetId') + target_name = target.get('name', 'Unknown') + print(f" Deleting target: {target_name} ({target_id})") + try: + gateway_client.delete_gateway_target( + gatewayIdentifier=gateway_id, + targetId=target_id + ) + print(f" Target {target_name} deleted") + except Exception as e: + print(f" Error deleting target {target_id}: {e}") + time.sleep(2) + except Exception as e: + print(f"Error listing/deleting targets: {e}") + + # Delete the gateway + print(f"Deleting gateway: {gateway_id}") + try: + gateway_client.delete_gateway(gatewayIdentifier=gateway_id) + print(f"Gateway {gateway_id} deleted successfully") + except Exception as e: + print(f"Error deleting gateway: {e}") + except Exception as e: + print(f"Error in gateway cleanup: {e}") + +def delete_lambda_function(): + """Delete Lambda function""" + print("\n=== Deleting Lambda Function ===") + try: + lambda_client = boto3.client('lambda', region_name=REGION) + + print(f"Deleting Lambda function: {LAMBDA_FUNCTION_NAME}") + try: + lambda_client.delete_function(FunctionName=LAMBDA_FUNCTION_NAME) + print(f"Lambda function {LAMBDA_FUNCTION_NAME} deleted successfully") + except lambda_client.exceptions.ResourceNotFoundException: + print(f"Lambda function {LAMBDA_FUNCTION_NAME} not found") + except Exception as e: + print(f"Error deleting Lambda function: {e}") + except Exception as e: + print(f"Error in Lambda cleanup: {e}") + +def delete_iam_roles(): + """Delete IAM roles""" + print("\n=== Deleting IAM Roles ===") + try: + iam_client = boto3.client('iam', region_name=REGION) + + # Include Gateway invoke role if it exists + gateway_invoke_role = os.getenv('GATEWAY_INVOKE_ROLE_NAME', 'agentcore-gateway-invoke-role') + roles_to_delete = [LAMBDA_IAM_ROLE_NAME, GATEWAY_IAM_ROLE_NAME, gateway_invoke_role] + + for role_name in roles_to_delete: + print(f"Deleting IAM role: {role_name}") + try: + # First, detach managed policies + try: + attached_policies = iam_client.list_attached_role_policies(RoleName=role_name) + for policy in attached_policies.get('AttachedPolicies', []): + policy_arn = policy['PolicyArn'] + print(f" Detaching policy: {policy_arn}") + iam_client.detach_role_policy(RoleName=role_name, PolicyArn=policy_arn) + except Exception as e: + print(f" Error detaching policies: {e}") + + # Delete inline policies + try: + inline_policies = iam_client.list_role_policies(RoleName=role_name, MaxItems=100) + for policy_name in inline_policies.get('PolicyNames', []): + print(f" Deleting inline policy: {policy_name}") + iam_client.delete_role_policy(RoleName=role_name, PolicyName=policy_name) + except Exception as e: + print(f" Error deleting inline policies: {e}") + + # Delete the role + iam_client.delete_role(RoleName=role_name) + print(f"IAM role {role_name} deleted successfully") + except iam_client.exceptions.NoSuchEntityException: + print(f"IAM role {role_name} not found") + except Exception as e: + print(f"Error deleting IAM role {role_name}: {e}") + except Exception as e: + print(f"Error in IAM cleanup: {e}") + +def delete_cognito_resources(): + """Delete Cognito user pool and related resources""" + print("\n=== Deleting Cognito Resources ===") + try: + cognito_client = boto3.client('cognito-idp', region_name=REGION) + + # Use COGNITO_POOL_NAME from .env if available, otherwise search by name + user_pool_id = COGNITO_POOL_ID + + if not user_pool_id: + print(f"No COGNITO_POOL_ID in .env, searching by name: {COGNITO_POOL_NAME}") + try: + pools_response = cognito_client.list_user_pools(MaxResults=60) + user_pool = next((pool for pool in pools_response.get('UserPools', []) + if pool.get('Name') == COGNITO_POOL_NAME), None) + + if user_pool: + user_pool_id = user_pool['Id'] + else: + print(f"User pool '{COGNITO_POOL_NAME}' not found") + return + except Exception as e: + print(f"Error finding user pool: {e}") + return + + print(f"Found user pool: {user_pool_id}") + + # Delete user pool domain first + try: + describe_response = cognito_client.describe_user_pool(UserPoolId=user_pool_id) + domain = describe_response.get('UserPool', {}).get('Domain') + if domain: + print(f" Deleting domain: {domain}") + cognito_client.delete_user_pool_domain( + Domain=domain, + UserPoolId=user_pool_id + ) + print(f" Domain {domain} deleted") + time.sleep(2) + except Exception as e: + print(f" Error deleting domain: {e}") + + # Delete app clients + try: + clients_response = cognito_client.list_user_pool_clients( + UserPoolId=user_pool_id, + MaxResults=60 + ) + for client in clients_response.get('UserPoolClients', []): + client_id = client['ClientId'] + client_name = client['ClientName'] + print(f" Deleting app client: {client_name} ({client_id})") + try: + cognito_client.delete_user_pool_client( + UserPoolId=user_pool_id, + ClientId=client_id + ) + print(f" App client {client_name} deleted") + except Exception as e: + print(f" Error deleting app client: {e}") + except Exception as e: + print(f" Error listing/deleting app clients: {e}") + + # Delete resource servers + try: + print(f" Deleting resource server: {RESOURCE_SERVER_ID}") + cognito_client.delete_resource_server( + UserPoolId=user_pool_id, + Identifier=RESOURCE_SERVER_ID + ) + print(f" Resource server {RESOURCE_SERVER_ID} deleted") + except cognito_client.exceptions.ResourceNotFoundException: + print(f" Resource server {RESOURCE_SERVER_ID} not found") + except Exception as e: + print(f" Error deleting resource server: {e}") + + # Delete the user pool + print(f" Deleting user pool: {user_pool_id}") + try: + cognito_client.delete_user_pool(UserPoolId=user_pool_id) + print(f"User pool {user_pool_id} deleted successfully") + except Exception as e: + print(f"Error deleting user pool: {e}") + except Exception as e: + print(f"Error in Cognito cleanup: {e}") + +def parse_arguments(): + """Parse command-line arguments""" + parser = argparse.ArgumentParser( + description='Cleanup AWS resources created by config.py', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + %(prog)s # Delete all resources (with confirmation) + %(prog)s --all # Delete all resources (with confirmation) + %(prog)s --gateway # Delete only Gateway and targets + %(prog)s --lambda # Delete only Lambda function + %(prog)s --iam # Delete only IAM roles + %(prog)s --cognito # Delete only Cognito resources + %(prog)s --gateway --lambda # Delete Gateway and Lambda + %(prog)s --yes # Skip confirmation prompt + %(prog)s --dry-run # Show what would be deleted without deleting + +Resource Details (from .env): + Gateway: {gateway_name} (ID: {gateway_id}) + Gateway Target: {target_name} (ID: {target_id}) + Lambda: {lambda_name} + IAM Roles: {iam_lambda}, {iam_gateway} + Cognito Pool: {cognito_pool} (ID: {cognito_id}) + Cognito Client: {client_name} (ID: {client_id}) + Resource Server: {resource_server} + """.format( + gateway_name=GATEWAY_NAME, + gateway_id=GATEWAY_ID or 'will search', + target_name=GATEWAY_TARGET_NAME, + target_id=GATEWAY_TARGET_ID or 'N/A', + lambda_name=LAMBDA_FUNCTION_NAME, + iam_lambda=LAMBDA_IAM_ROLE_NAME, + iam_gateway=GATEWAY_IAM_ROLE_NAME, + cognito_pool=COGNITO_POOL_NAME, + cognito_id=COGNITO_POOL_ID or 'will search', + client_name=CLIENT_NAME, + client_id=CLIENT_ID or 'N/A', + resource_server=RESOURCE_SERVER_ID + ) + ) + + parser.add_argument('--all', action='store_true', + help='Delete all resources (default if no specific flags)') + parser.add_argument('--gateway', action='store_true', + help='Delete Gateway and Gateway targets') + parser.add_argument('--lambda', dest='lambda_func', action='store_true', + help='Delete Lambda function') + parser.add_argument('--iam', action='store_true', + help='Delete IAM roles') + parser.add_argument('--cognito', action='store_true', + help='Delete Cognito user pool and related resources') + parser.add_argument('--yes', '-y', action='store_true', + help='Skip confirmation prompt') + parser.add_argument('--dry-run', action='store_true', + help='Show what would be deleted without actually deleting') + + return parser.parse_args() + +def main(): + """Main cleanup function""" + args = parse_arguments() + + # If no specific flags, default to --all + if not any([args.all, args.gateway, args.lambda_func, args.iam, args.cognito]): + args.all = True + + print("=" * 60) + print("AgentCore Strands Playground Cleanup") + print("=" * 60) + print(f"Region: {REGION}") + + if args.dry_run: + print("\n🔍 DRY RUN MODE - No resources will be deleted") + + print("\nResources to delete:") + + # Show what will be deleted + resources_to_delete = [] + if args.all or args.gateway: + resources_to_delete.append(f" ✓ Gateway: {GATEWAY_NAME} (ID: {GATEWAY_ID or 'will search'})") + resources_to_delete.append(f" └─ Gateway Target: {GATEWAY_TARGET_NAME} (ID: {GATEWAY_TARGET_ID or 'N/A'})") + + if args.all or args.lambda_func: + resources_to_delete.append(f" ✓ Lambda function: {LAMBDA_FUNCTION_NAME}") + + if args.all or args.iam: + resources_to_delete.append(f" ✓ IAM roles:") + resources_to_delete.append(f" └─ {LAMBDA_IAM_ROLE_NAME}") + resources_to_delete.append(f" └─ {GATEWAY_IAM_ROLE_NAME}") + + if args.all or args.cognito: + resources_to_delete.append(f" ✓ Cognito user pool: {COGNITO_POOL_NAME} (ID: {COGNITO_POOL_ID or 'will search'})") + resources_to_delete.append(f" └─ App client: {CLIENT_NAME} (ID: {CLIENT_ID or 'N/A'})") + resources_to_delete.append(f" └─ Resource server: {RESOURCE_SERVER_ID}") + + if not resources_to_delete: + print(" (none)") + else: + print("\n".join(resources_to_delete)) + + print() + + # Confirmation prompt (unless --yes or --dry-run) + if not args.yes and not args.dry_run: + response = input("Are you sure you want to proceed? (yes/no): ") + if response.lower() != 'yes': + print("Cleanup cancelled") + sys.exit(0) + + if args.dry_run: + print("\n🔍 Dry run completed. No resources were deleted.") + sys.exit(0) + + print("\nStarting cleanup...") + + # Delete in reverse order of creation + if args.all or args.gateway: + delete_gateway_and_targets() + + if args.all or args.lambda_func: + delete_lambda_function() + + if args.all or args.iam: + delete_iam_roles() + + if args.all or args.cognito: + delete_cognito_resources() + + print("\n" + "=" * 60) + print("Cleanup completed!") + print("=" * 60) + + # Show summary + print("\nDeleted resources:") + if args.all or args.gateway: + print(" ✓ Gateway and targets") + if args.all or args.lambda_func: + print(" ✓ Lambda function") + if args.all or args.iam: + print(" ✓ IAM roles") + if args.all or args.cognito: + print(" ✓ Cognito resources") + +if __name__ == "__main__": + main() diff --git a/agentcore-strands-playground/config.py b/agentcore-strands-playground/config.py new file mode 100755 index 0000000..408554d --- /dev/null +++ b/agentcore-strands-playground/config.py @@ -0,0 +1,677 @@ +#!/usr/bin/env python3 +""" +Configuration script to create AWS resources for AgentCore Strands Playground + +Usage: + ./config.py # Create all resources (includes Gateway permissions) + ./config.py --all # Create all resources (includes Gateway permissions) + ./config.py --lambda # Create only Lambda function + ./config.py --iam # Create only IAM roles + ./config.py --cognito # Create only Cognito resources + ./config.py --gateway # Create only Gateway and targets + ./config.py --lambda --iam # Create Lambda and IAM roles + ./config.py --add-gateway-permission # Manually add InvokeGateway permission (auto-added with --all) + ./config.py --test # Test existing configuration + ./config.py --help # Show this help message +""" + +import os +import sys +import argparse +import json +import yaml +from dotenv import load_dotenv + +load_dotenv() + +import utils +import boto3 +import time +from botocore.exceptions import ClientError + +def create_resources(create_lambda=True, create_iam=True, create_cognito=True, create_gateway=True): + """Create AWS resources for AgentCore Strands Playground + + Args: + create_lambda: Whether to create Lambda function + create_iam: Whether to create IAM roles + create_cognito: Whether to create Cognito resources + create_gateway: Whether to create Gateway and targets + + Returns: + Dict with created resource IDs + """ + print("\n" + "="*60) + print("Creating AWS Resources for AgentCore Strands Playground") + print("="*60 + "\n") + + # Get region from environment or boto3 session + global REGION + REGION = os.getenv('AWS_DEFAULT_REGION') or os.getenv('AWS_REGION') or boto3.Session().region_name + if not REGION: + print("Warning: No AWS region found. Defaulting to us-west-2") + REGION = 'us-west-2' + print(f"Using AWS region: {REGION}") + + # Initialize variables + lambda_resp = None + agentcore_gateway_iam_role = None + user_pool_id = None + client_m2m_id = None + client_m2m_secret = None + client_id = None + gatewayURL = None + targetname = None + scopeString = None + + # Create Lambda function + if create_lambda: + print("\n--- Creating Lambda function ---") + lambda_resp = utils.create_playground_lambda("lambda_function_code.zip") + + if lambda_resp is not None: + if lambda_resp['exit_code'] == 0: + print("Lambda function created with ARN: ", lambda_resp['lambda_function_arn']) + utils.add_to_env('LAMBDA_FUNCTION_ARN', lambda_resp['lambda_function_arn']) + utils.add_to_env('LAMBDA_FUNCTION_NAME', 'playground_lambda') + else: + print("Lambda function creation failed with message: ", lambda_resp['lambda_function_arn']) + else: + print("\n--- Skipping Lambda function creation ---") + # Try to load from .env + lambda_arn = os.getenv('LAMBDA_FUNCTION_ARN') + if lambda_arn: + print(f"Using existing Lambda ARN from .env: {lambda_arn}") + lambda_resp = {'lambda_function_arn': lambda_arn, 'exit_code': 0} + else: + print("Warning: No Lambda ARN found in .env. Gateway target creation may fail.") + + # Create IAM roles + if create_iam: + print("\n--- Creating IAM role for Gateway ---") + agentcore_gateway_iam_role = utils.create_agentcore_gateway_role("playground-lambdagateway") + print("Agentcore gateway role ARN: ", agentcore_gateway_iam_role['Role']['Arn']) + utils.add_to_env('GATEWAY_IAM_ROLE_ARN', agentcore_gateway_iam_role['Role']['Arn']) + utils.add_to_env('GATEWAY_IAM_ROLE_NAME', 'agentcore-playground-lambdagateway-role') + utils.add_to_env('LAMBDA_IAM_ROLE_NAME', 'playground_lambda_iamrole') + else: + print("\n--- Skipping IAM role creation ---") + # Try to load from .env + gateway_role_arn = os.getenv('GATEWAY_IAM_ROLE_ARN') + if gateway_role_arn: + print(f"Using existing Gateway IAM role from .env: {gateway_role_arn}") + agentcore_gateway_iam_role = {'Role': {'Arn': gateway_role_arn}} + else: + print("Warning: No Gateway IAM role ARN found in .env. Gateway creation may fail.") + + # Create Cognito User Pool + USER_POOL_NAME = "agentcore-strands-playground-pool" + RESOURCE_SERVER_ID = "playground-gateway-id" + RESOURCE_SERVER_NAME = "playground-gateway-name" + CLIENT_NAME = "playground-gateway-client" + SCOPES = [ + {"ScopeName": "gateway:read", "ScopeDescription": "Read access"}, + {"ScopeName": "gateway:write", "ScopeDescription": "Write access"} + ] + scopeString = f"{RESOURCE_SERVER_ID}/gateway:read {RESOURCE_SERVER_ID}/gateway:write" + + cognito = boto3.client("cognito-idp", region_name=REGION) + + if create_cognito: + print("\n--- Creating Cognito resources ---") + try: + user_pool_id = utils.get_or_create_user_pool(cognito, USER_POOL_NAME) + print(f"User Pool ID: {user_pool_id}") + utils.add_to_env('COGNITO_POOL_ID', user_pool_id) + utils.add_to_env('COGNITO_POOL_NAME', USER_POOL_NAME) + except ClientError as e: + if e.response['Error']['Code'] == 'ResourceExistsException': + print(f"User pool already exists: {e}") + else: + print(f"Error creating/retrieving user pool: {e}") + user_pool_id = None + + # Add users to pool + if user_pool_id: + print("Adding users to Cognito user pool...") + utils.add_cognito_user('testuser1', user_pool_id, cognito) + utils.add_cognito_user('testuser2', user_pool_id, cognito) + + try: + utils.get_or_create_resource_server(cognito, user_pool_id, RESOURCE_SERVER_ID, RESOURCE_SERVER_NAME, SCOPES) + print("Resource server ensured.") + utils.add_to_env('COGNITO_RESOURCE_SERVER_ID', RESOURCE_SERVER_ID) + utils.add_to_env('COGNITO_RESOURCE_SERVER_NAME', RESOURCE_SERVER_NAME) + except ClientError as e: + if e.response['Error']['Code'] == 'ResourceExistsException': + print(f"Resource server already exists: {e}") + else: + print(f"Error creating/retrieving resource server: {e}") + + # Create app client for Streamlit (without secret for USER_PASSWORD_AUTH) + # This same client is used for both user authentication and Gateway access + STREAMLIT_CLIENT_NAME = "streamlit-app-client" + try: + # Check if Streamlit client already exists + clients_response = cognito.list_user_pool_clients(UserPoolId=user_pool_id, MaxResults=60) + streamlit_client = next((c for c in clients_response.get('UserPoolClients', []) + if c.get('ClientName') == STREAMLIT_CLIENT_NAME), None) + + if streamlit_client: + client_id = streamlit_client['ClientId'] + print(f"Streamlit client already exists: {client_id}") + else: + print("Creating Streamlit app client (without secret)...") + streamlit_response = cognito.create_user_pool_client( + UserPoolId=user_pool_id, + ClientName=STREAMLIT_CLIENT_NAME, + GenerateSecret=False, # No secret for public client (Streamlit) + ExplicitAuthFlows=[ + 'ALLOW_USER_PASSWORD_AUTH', + 'ALLOW_REFRESH_TOKEN_AUTH' + ], + SupportedIdentityProviders=['COGNITO'] + ) + client_id = streamlit_response['UserPoolClient']['ClientId'] + print(f"Streamlit client created: {client_id}") + + utils.add_to_env('COGNITO_CLIENT_ID', client_id) + utils.add_to_env('COGNITO_CLIENT_NAME', STREAMLIT_CLIENT_NAME) + print(f"Streamlit client ID saved to .env: {client_id}") + except Exception as e: + print(f"Error creating/retrieving Streamlit client: {e}") + client_id = None + + # Get discovery URL + cognito_discovery_url = f'https://cognito-idp.{REGION}.amazonaws.com/{user_pool_id}/.well-known/openid-configuration' + print(cognito_discovery_url) + utils.add_to_env('COGNITO_DISCOVERY_URL', cognito_discovery_url) + else: + print("\n--- Skipping Cognito resource creation ---") + # Try to load from .env + user_pool_id = os.getenv('COGNITO_POOL_ID') + client_id = os.getenv('COGNITO_CLIENT_ID') + cognito_discovery_url = os.getenv('COGNITO_DISCOVERY_URL') + + if user_pool_id and client_id: + print(f"Using existing Cognito resources from .env:") + print(f" User Pool ID: {user_pool_id}") + print(f" Client ID: {client_id}") + else: + print("Warning: No Cognito resources found in .env. Gateway creation may fail.") + + # Create Gateway with IAM authentication (always) + if create_gateway: + print("\n--- Creating AgentCore Gateway ---") + gateway_client = boto3.client('bedrock-agentcore-control', REGION) + iam_client = boto3.client('iam', region_name=REGION) + + print("Creating Gateway with IAM authentication...") + + # Get AWS account ID for later use + sts_client = boto3.client('sts', region_name=REGION) + try: + caller_identity = sts_client.get_caller_identity() + account_id = caller_identity['Account'] + print(f"AWS Account ID: {account_id}") + except Exception as e: + print(f"Error getting caller identity: {e}") + sys.exit(1) + + # Create Gateway with IAM authentication + # Note: AWS_IAM does not require authorizerConfiguration + # The AgentCore Runtime execution roles will be granted InvokeGateway permission + try: + create_response = gateway_client.create_gateway( + name='TestGWforLambda', + roleArn=agentcore_gateway_iam_role['Role']['Arn'], + protocolType='MCP', + authorizerType='AWS_IAM', + description='AgentCore Gateway with AWS IAM authentication for runtime_agent.py' + ) + print("Gateway created successfully with IAM authentication") + + gateway_id = create_response['gatewayId'] + gateway_arn = f"arn:aws:bedrock-agentcore:{REGION}:{account_id}:gateway/{gateway_id}" + + print(f"✓ Gateway created") + print(f" Gateway ID: {gateway_id}") + print(f" Gateway ARN: {gateway_arn}") + print(f"\nNote: Run 'python config.py --add-gateway-permission' to grant") + print(f" InvokeGateway permission to AgentCore Runtime execution roles") + + except ClientError as e: + print(f"Error creating Gateway with IAM auth: {e}") + raise + + if create_response: + print(create_response) + # Retrieve the GatewayID used for GatewayTarget creation + gatewayID = create_response["gatewayId"] + gatewayURL = create_response["gatewayUrl"] + utils.add_to_env('GATEWAY_ID', gatewayID) + utils.add_to_env('GATEWAY_URL', gatewayURL) + utils.add_to_env('GATEWAY_NAME', 'TestGWforLambda') + else: + print("ERROR: Could not create or retrieve gateway. Exiting.") + sys.exit(1) + print(f"Gateway ID: {gatewayID}") + + # Replace the AWS Lambda function ARN below + print("\n--- Creating Gateway Target ---") + lambda_target_config = { + "mcp": { + "lambda": { + "lambdaArn": lambda_resp['lambda_function_arn'], + "toolSchema": { + "inlinePayload": [ + { + "name": "get_order_tool", + "description": "tool to get the order", + "inputSchema": { + "type": "object", + "properties": { + "orderId": { + "type": "string" + } + }, + "required": ["orderId"] + } + }, + { + "name": "update_order_tool", + "description": "tool to update the orderId", + "inputSchema": { + "type": "object", + "properties": { + "orderId": { + "type": "string" + } + }, + "required": ["orderId"] + } + } + ] + } + } + } + } + + credential_config = [ + { + "credentialProviderType" : "GATEWAY_IAM_ROLE" + } + ] + targetname='LambdaUsingSDK' + response = None + max_retries = 10 + retry_delay = 5 # seconds + + for attempt in range(max_retries): + try: + response = gateway_client.create_gateway_target( + gatewayIdentifier=gatewayID, + name=targetname, + description='Lambda Target using SDK', + targetConfiguration=lambda_target_config, + credentialProviderConfigurations=credential_config) + utils.add_to_env('GATEWAY_TARGET_NAME', targetname) + if response: + utils.add_to_env('GATEWAY_TARGET_ID', response.get('targetId', '')) + print(f"Gateway target created successfully") + break # Success, exit retry loop + except ClientError as e: + error_code = e.response['Error']['Code'] + if error_code == 'ResourceExistsException': + print(f"Gateway target already exists: {e}") + break # Target exists, no need to retry + elif error_code == 'ValidationException' and 'CREATING' in str(e): + if attempt < max_retries - 1: + print(f"Gateway is still creating (attempt {attempt + 1}/{max_retries}). Waiting {retry_delay} seconds...") + time.sleep(retry_delay) + else: + print(f"Gateway target creation failed after {max_retries} attempts: {e}") + response = None + else: + print(f"Error creating gateway target: {e}") + response = None + break # Other error, don't retry + else: + print("\n--- Skipping Gateway creation ---") + # Try to load from .env + gatewayID = os.getenv('GATEWAY_ID') + gatewayURL = os.getenv('GATEWAY_URL') + targetname = os.getenv('GATEWAY_TARGET_NAME', 'LambdaUsingSDK') + + if gatewayID and gatewayURL: + print(f"Using existing Gateway from .env:") + print(f" Gateway ID: {gatewayID}") + print(f" Gateway URL: {gatewayURL}") + print(f" Target Name: {targetname}") + else: + print("Warning: No Gateway resources found in .env.") + + # Add Gateway invoke permission to Runtime execution roles if Gateway was created + if create_gateway and gatewayID: + print("\n--- Adding Gateway Invoke Permission to Runtime Execution Roles ---") + try: + add_gateway_invoke_permission_to_runtime_role() + except Exception as e: + print(f"Warning: Could not add Gateway invoke permission automatically: {e}") + print("You can add it manually later with: python config.py --add-gateway-permission") + + # Return created resource IDs for testing + return { + 'user_pool_id': user_pool_id, + 'client_id': client_id, + 'gatewayURL': gatewayURL, + 'targetname': targetname, + 'scopeString': scopeString + } + + +def add_gateway_invoke_permission_to_runtime_role(): + """Add InvokeGateway permission to AgentCore Runtime execution role""" + print("\n" + "="*60) + print("Adding InvokeGateway Permission to Runtime Execution Role") + print("="*60 + "\n") + + # Load environment variables + load_dotenv() + + # Get Gateway ID from .env + gateway_id = os.getenv('GATEWAY_ID') + if not gateway_id: + print("ERROR: GATEWAY_ID not found in .env file") + print("Please run: python config.py --gateway first") + sys.exit(1) + + # Get region + region = os.getenv('AWS_REGION', 'us-west-2') + + # Read the .bedrock_agentcore.yaml to get execution role ARN + import yaml + yaml_path = 'agentcore_agent/.bedrock_agentcore.yaml' + + if not os.path.exists(yaml_path): + print(f"ERROR: {yaml_path} not found") + print("Please ensure you're running this from the project root directory") + sys.exit(1) + + with open(yaml_path, 'r') as f: + config = yaml.safe_load(f) + + # Get AWS account ID + sts_client = boto3.client('sts', region_name=region) + account_id = sts_client.get_caller_identity()['Account'] + + # Construct Gateway ARN + gateway_arn = f"arn:aws:bedrock-agentcore:{region}:{account_id}:gateway/{gateway_id}" + print(f"Gateway ARN: {gateway_arn}\n") + + # Create IAM client + iam_client = boto3.client('iam', region_name=region) + + # Define the policy document + policy_name = 'AgentCoreGatewayInvokePolicy' + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "bedrock-agentcore:InvokeGateway", + "Resource": gateway_arn + } + ] + } + + # Collect all unique execution roles from all agents + execution_roles = {} + for agent_name, agent_config in config.get('agents', {}).items(): + execution_role_arn = agent_config.get('aws', {}).get('execution_role') + if execution_role_arn: + role_name = execution_role_arn.split('/')[-1] + execution_roles[role_name] = { + 'arn': execution_role_arn, + 'agent': agent_name + } + + if not execution_roles: + print("ERROR: No execution roles found in configuration") + sys.exit(1) + + print(f"Found {len(execution_roles)} unique execution role(s):\n") + + # Add policy to each unique execution role + success_count = 0 + for role_name, role_info in execution_roles.items(): + print(f"Processing role for agent '{role_info['agent']}':") + print(f" Role Name: {role_name}") + print(f" Role ARN: {role_info['arn']}") + + try: + # Check if policy already exists + try: + existing_policy = iam_client.get_role_policy( + RoleName=role_name, + PolicyName=policy_name + ) + print(f" ✓ Policy already exists, updating...") + except iam_client.exceptions.NoSuchEntityException: + print(f" ✓ Adding new policy...") + + # Put (create or update) the inline policy + iam_client.put_role_policy( + RoleName=role_name, + PolicyName=policy_name, + PolicyDocument=json.dumps(policy_document) + ) + + print(f" ✓ Successfully added InvokeGateway permission!\n") + success_count += 1 + + except ClientError as e: + print(f" ✗ ERROR: Failed to add policy to role: {e}\n") + + if success_count > 0: + print(f"✓ Successfully updated {success_count} role(s) with Gateway invoke permissions!") + print(f"\nAll roles can now invoke Gateway '{gateway_id}'") + print("\nYou can now test the Gateway connection with:") + print(" python app.py") + print("\nOr test from the command line with the runtime agent.") + else: + print("ERROR: Failed to add permissions to any roles") + sys.exit(1) + + print("\n" + "="*60) + print("Permission added successfully!") + print("="*60) + + +def test_configuration(): + """Test the Gateway configuration with MCP tools""" + print("\n" + "="*60) + print("Testing Gateway Configuration") + print("="*60) + + # Wait for domain name propagation + print("Waiting for domain name propagation...") + time.sleep(1) + + # Request the access token from Amazon Cognito authorizer using user credentials + print("Requesting access token from Amazon Cognito authorizer...") + # Use USER_PASSWORD_AUTH flow with the Streamlit client + username = os.getenv('DEFAULT_USERNAME', 'testuser1') + password = os.getenv('DEFAULT_PASSWORD', 'MyPassword123!') + + token_response = utils.get_user_token(user_pool_id, client_id, username, password, REGION) + + # Check if token request was successful + if "error" in token_response: + print(f"ERROR: Failed to get token: {token_response['error']}") + sys.exit(1) + + if "access_token" not in token_response: + print(f"ERROR: No access_token in response: {token_response}") + sys.exit(1) + + token = token_response["access_token"] + print(f"✓ Token obtained successfully (length: {len(token)})") + + # Strands agent calling MCP tools (AWS Lambda) using Bedrock AgentCore Gateway + from strands.models import BedrockModel + from mcp.client.streamable_http import streamablehttp_client + from strands.tools.mcp.mcp_client import MCPClient + from strands import Agent + import logging + + def create_streamable_http_transport(): + headers = {"Authorization": f"Bearer {token}"} + print(f"Connecting to gateway URL: {gatewayURL}") + return streamablehttp_client(gatewayURL, headers=headers) + + print("\nInitializing MCP client...") + client = MCPClient(create_streamable_http_transport) + + # The IAM credentials configured in ~/.aws/credentials should have access to Bedrock model + yourmodel = BedrockModel( + model_id="us.amazon.nova-pro-v1:0", + temperature=0.7, + ) + + # Configure logging + logging.getLogger("strands").setLevel(logging.INFO) + logging.basicConfig( + format="%(levelname)s | %(name)s | %(message)s", + handlers=[logging.StreamHandler()] + ) + + with client: + # Call the listTools + tools = client.list_tools_sync() + # Create an Agent with the model and tools + agent = Agent(model=yourmodel, tools=tools) + print(f"✓ Tools loaded in the agent: {agent.tool_names}") + + # Test 1: List available tools + print("\n--- Test 1: List available tools ---") + agent("Hi, can you list all tools available to you") + + # Test 2: Invoke a tool + print("\n--- Test 2: Check order status ---") + agent("Check the order status for order id 123 and show me the exact response from the tool") + + # Test 3: Call MCP tool explicitly + print("\n--- Test 3: Direct tool call ---") + result = client.call_tool_sync( + tool_use_id="get-order-id-123-call-1", + name=targetname + "___get_order_tool", + arguments={"orderId": "123"} + ) + # Print the MCP Tool response + print(f"✓ Tool Call result: {result['content'][0]['text']}") + + print("\n" + "="*60) + print("Testing completed successfully!") + print("="*60) + +def parse_arguments(): + """Parse command-line arguments""" + parser = argparse.ArgumentParser( + description='Create AWS resources for AgentCore Strands Playground', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + %(prog)s # Create all resources + %(prog)s --all # Create all resources + %(prog)s --lambda # Create only Lambda function + %(prog)s --iam # Create only IAM roles + %(prog)s --cognito # Create only Cognito resources + %(prog)s --gateway # Create only Gateway and targets + %(prog)s --lambda --iam # Create Lambda and IAM roles + %(prog)s --test # Test existing configuration + +Note: When creating Gateway, dependent resources (IAM, Cognito, Lambda) must exist in .env + """ + ) + + parser.add_argument('--all', action='store_true', + help='Create all resources (default if no specific flags)') + parser.add_argument('--lambda', dest='lambda_func', action='store_true', + help='Create Lambda function') + parser.add_argument('--iam', action='store_true', + help='Create IAM roles') + parser.add_argument('--cognito', action='store_true', + help='Create Cognito user pool and related resources') + parser.add_argument('--gateway', action='store_true', + help='Create Gateway and Gateway targets') + parser.add_argument('--test', '-t', action='store_true', + help='Test existing configuration (requires resources in .env)') + parser.add_argument('--add-gateway-permission', action='store_true', + help='Add InvokeGateway permission to AgentCore Runtime execution role') + + return parser.parse_args() + +# Main execution +if __name__ == "__main__": + args = parse_arguments() + + # Check if add-gateway-permission mode is requested + if args.add_gateway_permission: + print("Adding Gateway InvokeGateway permission to Runtime execution role...") + add_gateway_invoke_permission_to_runtime_role() + sys.exit(0) + + # Check if test mode is requested + if args.test: + print("Running in TEST mode only...") + # Load required variables from .env for testing + load_dotenv() + user_pool_id = os.getenv('COGNITO_POOL_ID') + client_id = os.getenv('COGNITO_CLIENT_ID') + gatewayURL = os.getenv('GATEWAY_URL') + targetname = os.getenv('GATEWAY_TARGET_NAME', 'LambdaUsingSDK') + scopeString = f"{os.getenv('COGNITO_RESOURCE_SERVER_ID')}/gateway:read {os.getenv('COGNITO_RESOURCE_SERVER_ID')}/gateway:write" + REGION = os.getenv('AWS_REGION', 'us-west-2') + + if not all([user_pool_id, client_id, gatewayURL]): + print("ERROR: Missing required environment variables. Run config.py without arguments first to create resources.") + sys.exit(1) + + test_configuration() + else: + # If no specific flags, default to --all + if not any([args.all, args.lambda_func, args.iam, args.cognito, args.gateway]): + args.all = True + + # Show what will be created + print("\nResources to create:") + if args.all or args.lambda_func: + print(" ✓ Lambda function") + if args.all or args.iam: + print(" ✓ IAM roles") + if args.all or args.cognito: + print(" ✓ Cognito user pool, clients, and resource server") + if args.all or args.gateway: + print(" ✓ Gateway and Gateway targets") + print() + + # Create resources based on flags + resources = create_resources( + create_lambda=args.all or args.lambda_func, + create_iam=args.all or args.iam, + create_cognito=args.all or args.cognito, + create_gateway=args.all or args.gateway + ) + + print("\n" + "="*60) + print("Configuration completed successfully!") + print("="*60) + + # Show additional info if Gateway was created + if args.all or args.gateway: + print("\n✓ Gateway invoke permissions have been added to Runtime execution roles") + print(" Your runtime agent can now access the Gateway!") + + print("\nTo test the configuration, run:") + print(" ./config.py -t") diff --git a/agentcore-strands-playground/dotenv.example b/agentcore-strands-playground/dotenv.example new file mode 100644 index 0000000..fa471bb --- /dev/null +++ b/agentcore-strands-playground/dotenv.example @@ -0,0 +1,33 @@ +# Bedrock AgentCore Strands Playground environment +# Copy this file to .env and populate with your actual values +# Note: EVERY variable in this file is optional. +# All have default values in code +# if Cognito (or another provider) is not configured, the app defaults to not using authentication + +# AWS Region +AWS_REGION=us-west-2 + +# Cognito User Pool ID (from cognito_config['pool_id']) +COGNITO_POOL_ID=your-pool-id-here + +# Cognito App Client ID (from cognito_config['client_id']) +COGNITO_CLIENT_ID=your-client-id-here + +# Cognito Discovery URL (from cognito_config['discovery_url']) +# Format: https://cognito-idp.{region}.amazonaws.com/{pool_id}/.well-known/openid-configuration +COGNITO_DISCOVERY_URL=https://cognito-idp.{region}.amazonaws.com/{pool_id}/.well-known/openid-configuration +# if not set, front-end will default to not using authentication + +DEFAULT_USERNAME=testuser1 +DEFAULT_PASSWORD=MyPassword123! + +# Strands Agent Configuration +STRANDS_SYSTEM_PROMPT=You are a helpful assistant powered by Strands. Strands Agents is a simple-to-use, code-first framework for building agents - open source by AWS. The user has the ability to modify your set of built-in tools. Every time your tool set is changed, you can propose a new set of tasks that you can do. + +# Model Configuration +STRANDS_MODEL_ID=us.amazon.nova-pro-v1:0 +STRANDS_MAX_TOKENS=1000 +STRANDS_TEMPERATURE=0.3 +STRANDS_TOP_P=0.9 + + diff --git a/agentcore-strands-playground/images/BRAC_architecture.png b/agentcore-strands-playground/images/BRAC_architecture.png new file mode 100644 index 0000000..914893f Binary files /dev/null and b/agentcore-strands-playground/images/BRAC_architecture.png differ diff --git a/agentcore-strands-playground/images/BRAC_interface_screen.png b/agentcore-strands-playground/images/BRAC_interface_screen.png new file mode 100644 index 0000000..ce00747 Binary files /dev/null and b/agentcore-strands-playground/images/BRAC_interface_screen.png differ diff --git a/agentcore-strands-playground/images/BRAC_partner_integrations.png b/agentcore-strands-playground/images/BRAC_partner_integrations.png new file mode 100644 index 0000000..332c9ae Binary files /dev/null and b/agentcore-strands-playground/images/BRAC_partner_integrations.png differ diff --git a/agentcore-strands-playground/images/main_page_screenshot.png b/agentcore-strands-playground/images/main_page_screenshot.png new file mode 100644 index 0000000..dc291ec Binary files /dev/null and b/agentcore-strands-playground/images/main_page_screenshot.png differ diff --git a/agentcore-strands-playground/images/memory_settings.png b/agentcore-strands-playground/images/memory_settings.png new file mode 100644 index 0000000..9f9a1b7 Binary files /dev/null and b/agentcore-strands-playground/images/memory_settings.png differ diff --git a/agentcore-strands-playground/images/model_settings.png b/agentcore-strands-playground/images/model_settings.png new file mode 100644 index 0000000..80da2fb Binary files /dev/null and b/agentcore-strands-playground/images/model_settings.png differ diff --git a/agentcore-strands-playground/images/strands_playground_arch.png b/agentcore-strands-playground/images/strands_playground_arch.png new file mode 100644 index 0000000..371bda9 Binary files /dev/null and b/agentcore-strands-playground/images/strands_playground_arch.png differ diff --git a/agentcore-strands-playground/images/tool_selection.png b/agentcore-strands-playground/images/tool_selection.png new file mode 100644 index 0000000..835cd1b Binary files /dev/null and b/agentcore-strands-playground/images/tool_selection.png differ diff --git a/agentcore-strands-playground/pyproject.toml b/agentcore-strands-playground/pyproject.toml new file mode 100644 index 0000000..a167d08 --- /dev/null +++ b/agentcore-strands-playground/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "agentcore-strands-playground" +version = "0.1.0" +description = "Demo chatbot using Strands agent deployed to AgentCore runtime." +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "boto3>=1.40.23", + "python-dotenv>=1.0.0", + "strands-agents>=1.7.1", + "strands-agents-tools", + "bedrock-agentcore>=0.1.3", + "bedrock-agentcore-starter-toolkit>=0.1.8", + "streamlit>=1.49.1", + "requests>=2.31.0", +] + +[dependency-groups] +dev = [ + "uv>=0.8.15", +] diff --git a/agentcore-strands-playground/static/Amazon-Ember-Medium.ttf b/agentcore-strands-playground/static/Amazon-Ember-Medium.ttf new file mode 100644 index 0000000..8c731e9 Binary files /dev/null and b/agentcore-strands-playground/static/Amazon-Ember-Medium.ttf differ diff --git a/agentcore-strands-playground/static/Amazon-Ember-MediumItalic.ttf b/agentcore-strands-playground/static/Amazon-Ember-MediumItalic.ttf new file mode 100644 index 0000000..76afa50 Binary files /dev/null and b/agentcore-strands-playground/static/Amazon-Ember-MediumItalic.ttf differ diff --git a/agentcore-strands-playground/static/AmazonEmberMono_Rg.ttf b/agentcore-strands-playground/static/AmazonEmberMono_Rg.ttf new file mode 100644 index 0000000..cebded3 Binary files /dev/null and b/agentcore-strands-playground/static/AmazonEmberMono_Rg.ttf differ diff --git a/agentcore-strands-playground/static/AmazonEmber_He.ttf b/agentcore-strands-playground/static/AmazonEmber_He.ttf new file mode 100644 index 0000000..b44ad9f Binary files /dev/null and b/agentcore-strands-playground/static/AmazonEmber_He.ttf differ diff --git a/agentcore-strands-playground/static/agentcore-service-icon.png b/agentcore-strands-playground/static/agentcore-service-icon.png new file mode 100644 index 0000000..d5fc168 Binary files /dev/null and b/agentcore-strands-playground/static/agentcore-service-icon.png differ diff --git a/agentcore-strands-playground/static/arch.png b/agentcore-strands-playground/static/arch.png new file mode 100644 index 0000000..349f65c Binary files /dev/null and b/agentcore-strands-playground/static/arch.png differ diff --git a/agentcore-strands-playground/static/gen-ai-dark.svg b/agentcore-strands-playground/static/gen-ai-dark.svg new file mode 100644 index 0000000..efb047d --- /dev/null +++ b/agentcore-strands-playground/static/gen-ai-dark.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/agentcore-strands-playground/static/gen-ai-lt.svg b/agentcore-strands-playground/static/gen-ai-lt.svg new file mode 100644 index 0000000..aba07d5 --- /dev/null +++ b/agentcore-strands-playground/static/gen-ai-lt.svg @@ -0,0 +1,4 @@ + + + + diff --git a/agentcore-strands-playground/static/user-profile.svg b/agentcore-strands-playground/static/user-profile.svg new file mode 100644 index 0000000..d070fa9 --- /dev/null +++ b/agentcore-strands-playground/static/user-profile.svg @@ -0,0 +1,4 @@ + + + +