diff --git a/01-tutorials/07-AgentCore-E2E/Optional-lab-agentcore-observability.ipynb b/01-tutorials/07-AgentCore-E2E/Optional-lab-agentcore-observability.ipynb index 3f25f4601..0a75a415f 100644 --- a/01-tutorials/07-AgentCore-E2E/Optional-lab-agentcore-observability.ipynb +++ b/01-tutorials/07-AgentCore-E2E/Optional-lab-agentcore-observability.ipynb @@ -250,7 +250,7 @@ "import argparse\n", "from boto3.session import Session\n", "from opentelemetry import baggage, context\n", - "from scripts.utils import get_ssm_parameter\n", + "from lab_helpers.utils import get_ssm_parameter\n", "\n", "from strands import Agent\n", "from strands.models import BedrockModel\n", diff --git a/01-tutorials/07-AgentCore-E2E/Optional-lab-identity.ipynb b/01-tutorials/07-AgentCore-E2E/Optional-lab-identity.ipynb index 61b91f64a..4010ee2d9 100644 --- a/01-tutorials/07-AgentCore-E2E/Optional-lab-identity.ipynb +++ b/01-tutorials/07-AgentCore-E2E/Optional-lab-identity.ipynb @@ -680,7 +680,7 @@ "try:\n", " print(\"๐Ÿ“ฅ Fetching Cognito configuration from SSM...\")\n", " \n", - " client_id = get_ssm_parameter(\"/app/customersupport/agentcore/machine_client_id\")\n", + " client_id = get_ssm_parameter(\"/app/customersupport/agentcore/client_id\")\n", " print(f\"โœ… Retrieved client ID: {client_id}\")\n", "\n", " client_secret = get_ssm_parameter(\"/app/customersupport/agentcore/cognito_secret\")\n", diff --git a/01-tutorials/07-AgentCore-E2E/lab-03-agentcore-gateway.ipynb b/01-tutorials/07-AgentCore-E2E/lab-03-agentcore-gateway.ipynb index 73dec4156..9ed4d32d0 100644 --- a/01-tutorials/07-AgentCore-E2E/lab-03-agentcore-gateway.ipynb +++ b/01-tutorials/07-AgentCore-E2E/lab-03-agentcore-gateway.ipynb @@ -108,18 +108,17 @@ "outputs": [], "source": [ "# Import libraries\n", - "from strands import Agent\n", - "from strands.models import BedrockModel\n", - "from strands.tools.mcp import MCPClient\n", "import os\n", "import sys\n", "import boto3\n", "import json\n", - "from bedrock_agentcore.identity.auth import requires_access_token\n", + "\n", + "from strands import Agent\n", + "from strands.models import BedrockModel\n", + "from strands.tools.mcp import MCPClient\n", "from mcp.client.streamable_http import streamablehttp_client\n", - "import requests\n", + "from lab_helpers.utils import get_or_create_cognito_pool, put_ssm_parameter, get_ssm_parameter, load_api_spec\n", "\n", - "from scripts.utils import get_ssm_parameter, put_ssm_parameter, load_api_spec, get_cognito_client_secret\n", "\n", "sts_client = boto3.client('sts')\n", "\n", @@ -286,12 +285,11 @@ "source": [ "gateway_name = \"customersupport-gw\"\n", "\n", + "cognito_config = get_or_create_cognito_pool(refresh_token=True)\n", "auth_config = {\n", " \"customJWTAuthorizer\": {\n", - " \"allowedClients\": [\n", - " get_ssm_parameter(\"/app/customersupport/agentcore/machine_client_id\")\n", - " ],\n", - " \"discoveryUrl\": get_ssm_parameter(\"/app/customersupport/agentcore/cognito_discovery_url\")\n", + " \"allowedClients\": [cognito_config[\"client_id\"]],\n", + " \"discoveryUrl\": cognito_config[\"discovery_url\"]\n", " }\n", "}\n", "\n", @@ -317,7 +315,13 @@ " \"gateway_arn\": create_response[\"gatewayArn\"],\n", " }\n", " put_ssm_parameter(\"/app/customersupport/agentcore/gateway_id\", gateway_id)\n", - "\n", + " put_ssm_parameter(\"/app/customersupport/agentcore/gateway_name\", gateway_name)\n", + " put_ssm_parameter(\n", + " \"/app/customersupport/agentcore/gateway_arn\", create_response[\"gatewayArn\"]\n", + " )\n", + " put_ssm_parameter(\n", + " \"/app/customersupport/agentcore/gateway_url\", create_response[\"gatewayUrl\"]\n", + " )\n", " print(f\"โœ… Gateway created successfully with ID: {gateway_id}\")\n", "\n", "except Exception as e:\n", @@ -354,14 +358,6 @@ "metadata": {}, "outputs": [], "source": [ - "def load_api_spec(file_path: str) -> list:\n", - " with open(file_path, \"r\") as f:\n", - " data = json.load(f)\n", - " \n", - " if not isinstance(data, list):\n", - " raise ValueError(\"Expected a list in the JSON file\")\n", - " return data\n", - "\n", "try:\n", " api_spec_file = \"./prerequisite/lambda/api_spec.json\"\n", "\n", @@ -406,40 +402,8 @@ "metadata": {}, "source": [ "## Step 7: Add our new MCP-based tools to our support agent\n", - "Here we integrate our authentication token from Cognito into an MCPClient from Strands SDK to create an MCP Server object to integrate with our Strands Agent" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6fec14df-e398-4321-9903-21a5f5f76c3a", - "metadata": {}, - "outputs": [], - "source": [ - "def get_token(client_id: str, client_secret: str, scope_string: str, url: str) -> dict:\n", - " try:\n", - " headers = {\"Content-Type\": \"application/x-www-form-urlencoded\"}\n", - " data = {\n", - " \"grant_type\": \"client_credentials\",\n", - " \"client_id\": client_id,\n", - " \"client_secret\": client_secret,\n", - " \"scope\": scope_string,\n", - "\n", - " }\n", - " response = requests.post(url, headers=headers, data=data)\n", - " response.raise_for_status()\n", - " return response.json()\n", - "\n", - " except requests.exceptions.RequestException as err:\n", - " return {\"error\": str(err)}" - ] - }, - { - "cell_type": "markdown", - "id": "bff0a6b8-fdd3-4536-8012-6d0ab34f568f", - "metadata": {}, - "source": [ - "## Step 7.1. Set up a secure MCP client object" + "Here we integrate our authentication token from Cognito into an MCPClient from Strands SDK to create an MCP Server object to integrate with our Strands Agent\n", + "### Step 7.1. Set up a secure MCP client object" ] }, { @@ -449,19 +413,12 @@ "metadata": {}, "outputs": [], "source": [ - "gateway_access_token = get_token(\n", - " get_ssm_parameter(\"/app/customersupport/agentcore/machine_client_id\"),\n", - " get_cognito_client_secret(),\n", - " get_ssm_parameter(\"/app/customersupport/agentcore/cognito_auth_scope\"),\n", - " get_ssm_parameter(\"/app/customersupport/agentcore/cognito_token_url\"))\n", - "\n", "print(f\"Gateway Endpoint - MCP URL: {gateway['gateway_url']}\")\n", - "\n", "# Set up MCP client\n", "mcp_client = MCPClient(\n", " lambda: streamablehttp_client(\n", " gateway['gateway_url'],\n", - " headers={\"Authorization\": f\"Bearer {gateway_access_token['access_token']}\"},\n", + " headers={\"Authorization\": f\"Bearer {cognito_config['bearer_token']}\"},\n", " )\n", ")" ] @@ -501,28 +458,29 @@ " temperature=0.3, # Balanced between creativity and consistency\n", " region_name=REGION\n", ")\n", - "\n", - "try:\n", - " mcp_client.start()\n", - "except Exception as e:\n", - " print(f\"Error initializing agent: {str(e)}\")\n", - "\n", - "tools = (\n", - " [\n", - " get_product_info,\n", - " get_return_policy,\n", - " get_technical_support\n", - " ]\n", - " + mcp_client.list_tools_sync()\n", - " )\n", - "\n", - "# Create the customer support agent\n", - "agent = Agent(\n", - " model=model,\n", - " tools=tools,\n", - " hooks=[memory_hooks],\n", - " system_prompt=SYSTEM_PROMPT\n", - ")\n", + "def create_agent(prompt):\n", + " try:\n", + " with mcp_client:\n", + " tools = (\n", + " [\n", + " get_product_info,\n", + " get_return_policy,\n", + " get_technical_support\n", + " ]\n", + " + mcp_client.list_tools_sync()\n", + " )\n", + " \n", + " # Create the customer support agent\n", + " agent = Agent(\n", + " model=model,\n", + " tools=tools,\n", + " hooks=[memory_hooks],\n", + " system_prompt=SYSTEM_PROMPT\n", + " )\n", + " response = agent(prompt)\n", + " return response\n", + " except Exception as e:\n", + " raise e\n", "\n", "print(\"โœ… Customer support agent created successfully!\")" ] @@ -557,18 +515,18 @@ "]\n", "\n", "# Function to test the agent\n", - "def test_agent_responses(agent, prompts):\n", + "def test_agent_responses(prompts):\n", " for i, prompt in enumerate(prompts, 1):\n", " print(f\"\\nTest Case {i}: {prompt}\")\n", " print(\"-\" * 50)\n", " try:\n", - " response = agent(prompt)\n", + " response = create_agent(prompt)\n", " except Exception as e:\n", " print(f\"Error: {str(e)}\")\n", " print(\"-\" * 50)\n", "\n", "# Run the tests\n", - "test_agent_responses(agent, test_prompts)\n", + "test_agent_responses(test_prompts)\n", "\n", "print(\"\\\\nโœ… Basic testing completed!\")\n" ] diff --git a/01-tutorials/07-AgentCore-E2E/lab-04-agentcore-runtime.ipynb b/01-tutorials/07-AgentCore-E2E/lab-04-agentcore-runtime.ipynb index 15fa7ca56..e3580ddcb 100644 --- a/01-tutorials/07-AgentCore-E2E/lab-04-agentcore-runtime.ipynb +++ b/01-tutorials/07-AgentCore-E2E/lab-04-agentcore-runtime.ipynb @@ -65,6 +65,7 @@ "- Docker, Finch or Podman installed and running\n", "- Amazon Bedrock AgentCore SDK\n", "- Strands Agents framework\n", + "- **Lab 3 Completion:** This lab builds on Lab 3 (AgentCore Gateway). You MUST run [lab-03-agentcore-gateway](lab-03-agentcore-gateway.ipynb) to provision the gateway before running this lab.\n", "\n", "**Note**: You MUST enable [CloudWatch Transaction Search](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Enable-TransactionSearch.html) to be able to see AgentCore Observability traces in CloudWatch.\n" ] @@ -85,9 +86,9 @@ "outputs": [], "source": [ "# Import required libraries\n", - "import os\n", - "import json\n", - "import boto3\n", + "import os, json, boto3, requests\n", + "from lab_helpers.utils import get_ssm_parameter, get_cognito_client_secret\n", + "from strands.tools.mcp import MCPClient\n", "from strands import Agent\n", "from strands.models import BedrockModel\n", "from lab_helpers.lab2_memory import create_or_get_memory_resource\n", @@ -106,12 +107,19 @@ "\n", "Let's first define the necessary AgentCore Runtime components via Python SDK within our previous local agent implementation.\n", "\n", - "Observe the `#### AGENTCORE RUNTIME - LINE i ####` comments below to see where is the relevant deployment code added. You'll find 4 such lines that prepare the runtime-ready agent:\n", + "Observe the #### AGENTCORE RUNTIME - LINE 1 #### comments below to see where is the relevant deployment code added. You'll find 4 such lines that prepare the runtime-ready agent:\n", "\n", "1. Import the Runtime App with `from bedrock_agentcore.runtime import BedrockAgentCoreApp`\n", "2. Initialize the App with `app = BedrockAgentCoreApp()`\n", "3. Decorate our invocation function with `@app.entrypoint`\n", - "4. Let AgentCore Runtime control the execution with `app.run()`\n" + "4. Let AgentCore Runtime control the execution with `app.run()`\n", + "\n", + "##### Key Implementation Details:\n", + "\n", + "The runtime-ready agent uses an entrypoint function that extracts user prompts from the payload and JWT tokens from request headers via \n", + "context.request_headers.get('Authorization', ''). The authorization token is then propagated directly to the AgentCore Gateway by passing it in the \n", + "MCP client headers: headers={\"Authorization\": auth_header}. The implementation includes error handling for missing authentication and returns plain \n", + "text responses from synchronous agent invocation while preserving all memory and tool functionality from previous labs." ] }, { @@ -126,8 +134,12 @@ " BedrockAgentCoreApp,\n", ") #### AGENTCORE RUNTIME - LINE 1 ####\n", "from strands import Agent\n", + "from strands.tools.mcp import MCPClient\n", + "from mcp.client.streamable_http import streamablehttp_client\n", + "import requests\n", + "import boto3\n", "from strands.models import BedrockModel\n", - "from scripts.utils import get_ssm_parameter\n", + "from lab_helpers.utils import get_ssm_parameter\n", "from lab_helpers.lab1_strands_agent import (\n", " get_return_policy,\n", " get_product_info,\n", @@ -143,6 +155,12 @@ " SESSION_ID,\n", ")\n", "\n", + "# Initialize boto3 client\n", + "sts_client = boto3.client('sts')\n", + "\n", + "# Get AWS account details\n", + "REGION = boto3.session.Session().region_name\n", + "\n", "# Lab1 import: Create the Bedrock model\n", "model = BedrockModel(model_id=MODEL_ID)\n", "\n", @@ -152,27 +170,69 @@ " memory_id, memory_client, ACTOR_ID, SESSION_ID\n", ")\n", "\n", - "# Create the agent with all customer support tools\n", - "agent = Agent(\n", - " model=model,\n", - " tools=[get_return_policy, get_product_info, get_technical_support],\n", - " system_prompt=SYSTEM_PROMPT,\n", - " hooks=[memory_hooks],\n", - ")\n", - "\n", "# Initialize the AgentCore Runtime App\n", "app = BedrockAgentCoreApp() #### AGENTCORE RUNTIME - LINE 2 ####\n", "\n", - "\n", "@app.entrypoint #### AGENTCORE RUNTIME - LINE 3 ####\n", - "def invoke(payload):\n", + "async def invoke(payload, context=None):\n", " \"\"\"AgentCore Runtime entrypoint function\"\"\"\n", " user_input = payload.get(\"prompt\", \"\")\n", "\n", - " # Invoke the agent\n", - " response = agent(user_input)\n", - " return response.message[\"content\"][0][\"text\"]\n", - "\n", + " # Access request headers - handle None case\n", + " request_headers = context.request_headers or {}\n", + "\n", + " # Get Client JWT token\n", + " auth_header = request_headers.get('Authorization', '')\n", + "\n", + " print(f\"Authorization header: {auth_header}\")\n", + " # Get Gateway ID\n", + " existing_gateway_id = get_ssm_parameter(\"/app/customersupport/agentcore/gateway_id\")\n", + " \n", + " # Initialize Bedrock AgentCore Control client\n", + " gateway_client = boto3.client(\n", + " \"bedrock-agentcore-control\",\n", + " region_name=REGION,\n", + " )\n", + " # Get existing gateway details\n", + " gateway_response = gateway_client.get_gateway(gatewayIdentifier=existing_gateway_id)\n", + "\n", + " # Get gateway url\n", + " gateway_url = gateway_response['gatewayUrl']\n", + "\n", + " # Create MCP client and agent within context manager if JWT token available\n", + " if gateway_url and auth_header:\n", + " try:\n", + " mcp_client = MCPClient(lambda: streamablehttp_client(\n", + " url=gateway_url,\n", + " headers={\"Authorization\": auth_header} \n", + " ))\n", + " \n", + " with mcp_client:\n", + " #tools = mcp_client.list_tools_sync()\n", + " tools = (\n", + " [\n", + " get_product_info,\n", + " get_return_policy,\n", + " get_technical_support\n", + " ]\n", + " + mcp_client.list_tools_sync()\n", + " )\n", + " \n", + " # Create the agent with all customer support tools\n", + " agent = Agent(\n", + " model=model,\n", + " tools=tools,\n", + " system_prompt=SYSTEM_PROMPT,\n", + " hooks=[memory_hooks],\n", + " )\n", + " # Invoke the agent\n", + " response = agent(user_input)\n", + " return response.message[\"content\"][0][\"text\"]\n", + " except Exception as e:\n", + " print(f\"MCP client error: {str(e)}\")\n", + " return f\"Error: {str(e)}\"\n", + " else:\n", + " return \"Error: Missing gateway URL or authorization header\"\n", "\n", "if __name__ == \"__main__\":\n", " app.run() #### AGENTCORE RUNTIME - LINE 4 ####" @@ -219,17 +279,27 @@ { "cell_type": "code", "execution_count": null, - "id": "4581baa2-9edc-425d-becf-09968565081a", + "id": "c68f95c9-e97c-4ebd-8009-b2eab09ba614", "metadata": {}, "outputs": [], "source": [ - "from lab_helpers.utils import setup_cognito_user_pool, reauthenticate_user\n", + "from lab_helpers.utils import get_or_create_cognito_pool, get_ssm_parameter\n", + "access_token = get_or_create_cognito_pool(refresh_token=True)\n", + "print(f\"Access token: {access_token['bearer_token']}\")" + ] + }, + { + "cell_type": "markdown", + "id": "d1d09fb7", + "metadata": {}, + "source": [ + "#### AgentCore Runtime Configuration Summary:\n", "\n", - "print(\"Setting up Amazon Cognito user pool...\")\n", - "cognito_config = (\n", - " setup_cognito_user_pool()\n", - ") # You'll get your bearer token from this output cell.\n", - "print(\"Cognito setup completed โœ“\")" + "Below code configures the AgentCore Runtime deployment using the starter toolkit. It creates an execution role for the runtime, then configures the \n", + "deployment with the agent entrypoint file (lab_helpers/lab4_runtime.py), enables automatic ECR repository creation, and sets up JWT-based authentication using \n", + "Cognito. The configuration specifies allowed client IDs and discovery URLs retrieved from SSM parameters, establishing secure access control for the \n", + "production agent deployment. This step automatically generates the Dockerfile and .bedrock_agentcore.yaml configuration files needed for \n", + "containerized deployment." ] }, { @@ -260,10 +330,10 @@ " agent_name=\"customer_support_agent\",\n", " authorizer_configuration={\n", " \"customJWTAuthorizer\": {\n", - " \"allowedClients\": [cognito_config.get(\"client_id\")],\n", - " \"discoveryUrl\": cognito_config.get(\"discovery_url\"),\n", + " \"allowedClients\": [get_ssm_parameter(\"/app/customersupport/agentcore/client_id\")],\n", + " \"discoveryUrl\": get_ssm_parameter(\"/app/customersupport/agentcore/cognito_discovery_url\"),\n", " }\n", - " },\n", + " }\n", ")\n", "\n", "print(\"Configuration completed:\", response)" @@ -349,7 +419,50 @@ "\n", "#### Using the AgentCore Starter Toolkit\n", "\n", - "We can validate that the agent works using the AgentCore Starter Toolkit for invocation. The starter toolkit can automatically create a session id for us to query our agent. Alternatively, you can also pass the session id as a parameter during invocation. For demonstration purpose, we will create our own session id." + "We can validate that the agent works using the AgentCore Starter Toolkit for invocation. The starter toolkit can automatically create a session id for us to query our agent. Alternatively, you can also pass the session id as a parameter during invocation. For demonstration purpose, we will create our own session id.\n", + "\n", + "#### Runtime Header Configuration\n", + "\n", + "Below code configures custom header allowlists for the deployed AgentCore Runtime. It extracts the runtime ID from the agent ARN, retrieves the \n", + "current runtime configuration to preserve existing settings, then updates the runtime with a request header allowlist that includes the Authorization\n", + "header (required for OAuth token propagation) and custom headers. This ensures JWT tokens and other necessary headers are properly forwarded from \n", + "client requests to the agent runtime code." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc1fa718", + "metadata": {}, + "outputs": [], + "source": [ + "# Initialize the AgentCore Control client\n", + "client = boto3.client('bedrock-agentcore-control')\n", + "\n", + "# Extract runtime ID from the ARN (format: arn:aws:bedrock-agentcore:region:account:runtime/runtime-id)\n", + "runtime_id = launch_result.agent_arn.split(':')[-1].split('/')[-1]\n", + "\n", + "print(f\"Runtime ID: {runtime_id}\")\n", + "\n", + "# Get current runtime configuration to preserve existing settings\n", + "current_config = client.get_agent_runtime(agentRuntimeId=runtime_id)\n", + "\n", + "# Update runtime with custom header configuration while preserving existing settings\n", + "client.update_agent_runtime(\n", + " agentRuntimeId=runtime_id,\n", + " # Preserve existing configuration\n", + " agentRuntimeArtifact=current_config['agentRuntimeArtifact'],\n", + " roleArn=current_config['roleArn'],\n", + " networkConfiguration=current_config['networkConfiguration'],\n", + " authorizerConfiguration=current_config.get('authorizerConfiguration'),\n", + " # Add custom header allowlist for Authorization and custom headers\n", + " requestHeaderConfiguration={\n", + " 'requestHeaderAllowlist': [\n", + " 'Authorization', # Required for OAuth propogation\n", + " 'X-Amzn-Bedrock-AgentCore-Runtime-Custom-H1' # Custom header\n", + " ]\n", + " }\n", + ")" ] }, { @@ -365,19 +478,16 @@ "session_id = uuid.uuid4()\n", "\n", "# Test different customer support scenarios\n", - "user_query = \"My Iphone is not connecting with the Bluetooth. What should I do?\"\n", - "\n", - "bearer_token = reauthenticate_user(\n", - " cognito_config.get(\"client_id\"), \n", - " cognito_config.get(\"client_secret\")\n", - ")\n", + "user_query = \"List all of your tools\"\n", "\n", "response = agentcore_runtime.invoke(\n", " {\"prompt\": user_query}, \n", - " bearer_token=bearer_token,\n", + " #bearer_token=f\"Bearer {access_token['bearer_token']}\",\n", + " bearer_token=access_token['bearer_token'],\n", " session_id=str(session_id)\n", ")\n", - "response" + "\n", + "print(response['response'])" ] }, { @@ -397,13 +507,13 @@ "metadata": {}, "outputs": [], "source": [ - "user_query = \"I've turned my Bluetooth on and off but it still does not work\"\n", + "user_query = \"Tell me detailed information about the technical documentation on installing a new CPU\"\n", "response = agentcore_runtime.invoke(\n", " {\"prompt\": user_query}, \n", - " bearer_token=bearer_token,\n", + " bearer_token=access_token['bearer_token'],\n", " session_id=str(session_id)\n", ")\n", - "response" + "print(response['response'])" ] }, { @@ -425,13 +535,13 @@ "# Creating a new session ID for demonstrating new customer\n", "session_id2 = uuid.uuid4()\n", "\n", - "user_query = \"Still not working. What is going on?\"\n", + "user_query = \"I have a Gaming Console Pro device , I want to check my warranty status, warranty serial number is MNO33333333.\"\n", "response = agentcore_runtime.invoke(\n", " {\"prompt\": user_query}, \n", - " bearer_token=bearer_token,\n", + " bearer_token=access_token['bearer_token'],\n", " session_id=str(session_id2)\n", ")\n", - "response" + "print(response['response'])" ] }, { diff --git a/01-tutorials/07-AgentCore-E2E/lab_helpers/lab2_memory.py b/01-tutorials/07-AgentCore-E2E/lab_helpers/lab2_memory.py index d0a09b8a7..872e9c62e 100644 --- a/01-tutorials/07-AgentCore-E2E/lab_helpers/lab2_memory.py +++ b/01-tutorials/07-AgentCore-E2E/lab_helpers/lab2_memory.py @@ -23,7 +23,7 @@ REGION = boto_session.region_name logger = logging.getLogger(__name__) -from scripts.utils import get_ssm_parameter, put_ssm_parameter +from lab_helpers.utils import get_ssm_parameter, put_ssm_parameter ACTOR_ID = "customer_001" SESSION_ID = str(uuid.uuid4()) @@ -194,4 +194,4 @@ def get_memory_hooks(): session_id=SESSION_ID, ) - return memory_hooks \ No newline at end of file + return memory_hooks diff --git a/01-tutorials/07-AgentCore-E2E/lab_helpers/lab4_runtime.py b/01-tutorials/07-AgentCore-E2E/lab_helpers/lab4_runtime.py index 07a1002c3..27eaa8031 100644 --- a/01-tutorials/07-AgentCore-E2E/lab_helpers/lab4_runtime.py +++ b/01-tutorials/07-AgentCore-E2E/lab_helpers/lab4_runtime.py @@ -1,53 +1,115 @@ +import logging + +import boto3 +import requests from bedrock_agentcore.runtime import ( BedrockAgentCoreApp, -) #### AGENTCORE RUNTIME - LINE 1 #### -from strands import Agent -from strands.models import BedrockModel -from scripts.utils import get_ssm_parameter +) # ### AGENTCORE RUNTIME - LINE 1 #### from lab_helpers.lab1_strands_agent import ( - get_return_policy, + MODEL_ID, + SYSTEM_PROMPT, get_product_info, + get_return_policy, get_technical_support, - SYSTEM_PROMPT, - MODEL_ID, ) - from lab_helpers.lab2_memory import ( - CustomerSupportMemoryHooks, - memory_client, ACTOR_ID, SESSION_ID, + CustomerSupportMemoryHooks, + memory_client, ) +from lab_helpers.utils import get_or_create_cognito_pool, get_ssm_parameter +from mcp.client.streamable_http import streamablehttp_client +from strands import Agent +from strands.models import BedrockModel +from strands.tools.mcp import MCPClient -# Lab1 import: Create the Bedrock model -model = BedrockModel(model_id=MODEL_ID) - -# Lab2 import : Initialize memory via hooks -memory_id = get_ssm_parameter("/app/customersupport/agentcore/memory_id") -memory_hooks = CustomerSupportMemoryHooks( - memory_id, memory_client, ACTOR_ID, SESSION_ID +# Add a handler to see the logs +logging.basicConfig( + format="%(levelname)s | %(name)s | %(message)s", handlers=[logging.StreamHandler()] ) +logger = logging.getLogger("agentcore-app") +logger.setLevel(logging.DEBUG) -# Create the agent with all customer support tools -agent = Agent( - model=model, - tools=[get_return_policy, get_product_info, get_technical_support], - system_prompt=SYSTEM_PROMPT, - hooks=[memory_hooks], -) +class CustomerSupportAgent: + def __init__(self): + # Lab1 import: Create the Bedrock model + self.model = BedrockModel(model_id=MODEL_ID) + self.cognito_config = get_or_create_cognito_pool() + self.session_id = None + self.memory_hook = None + + # Create the agent with all customer support tools (local + MCP) + async def create_client(self, gateway_url): + # Set up MCP client + if self.cognito_config: + try: + # Lab3 import: Set up MCP client for gateway integration + self.gateway_client = MCPClient( + lambda: streamablehttp_client( + gateway_url, + headers={ + "Authorization": f"Bearer {self.cognito_config['access_token']}" + }, + ) + ) + except Exception as e: + logger.error(f"Could not initialize MCP client: {e}") + else: + logger.error("Failed to initialize the agent without a Bearer token") + + def invoke(self, user_input: str): + """AgentCore Runtime entrypoint function""" + try: + with self.gateway_client: + agent = Agent( + model=self.model, + tools=[get_return_policy, get_product_info, get_technical_support] + + self.gateway_client.list_tools_sync(), + system_prompt=SYSTEM_PROMPT, + hooks=[self.memory_hook], + ) + response = agent(user_input) + return response + except Exception as e: + return f"Error while invoking Agent {e}" + + async def stream(self, user_query: str): + try: + async for event in self.agent.stream_async(user_query): + if "data" in event: + # Only stream text chunks to the client + yield event["data"] + except Exception as e: + yield f"We are unable to process your request at the moment. Error: {e}" + + +# Lab2 import : Initialize memory via hooks +memory_id = get_ssm_parameter("/app/customersupport/agentcore/memory_id") +# Lab 3: Get gateway URL from SSM +gateway_url = get_ssm_parameter("/app/customersupport/agentcore/gateway_url") +# Initialize the customer support agent +customer_support = CustomerSupportAgent() # Initialize the AgentCore Runtime App app = BedrockAgentCoreApp() #### AGENTCORE RUNTIME - LINE 2 #### @app.entrypoint #### AGENTCORE RUNTIME - LINE 3 #### -def invoke(payload): +async def invoke(payload, context=None): """AgentCore Runtime entrypoint function""" - user_input = payload.get("prompt", "") - + # if not customer_support.agent: + logger.info("initializing agent") + memory_hook = CustomerSupportMemoryHooks( + memory_id, memory_client, ACTOR_ID, context.session_id or SESSION_ID + ) + customer_support.memory_hook = memory_hook + # Initialize MCP client + await customer_support.create_client(gateway_url) # Invoke the agent - response = agent(user_input) - return response.message["content"][0]["text"] + user_input = payload.get("prompt", "") + response = customer_support.invoke(user_input) + return response if __name__ == "__main__": diff --git a/01-tutorials/07-AgentCore-E2E/lab_helpers/lab5_frontend/chat_utils.py b/01-tutorials/07-AgentCore-E2E/lab_helpers/lab5_frontend/chat_utils.py index 80e0e9962..128a147bf 100644 --- a/01-tutorials/07-AgentCore-E2E/lab_helpers/lab5_frontend/chat_utils.py +++ b/01-tutorials/07-AgentCore-E2E/lab_helpers/lab5_frontend/chat_utils.py @@ -1,9 +1,10 @@ +import json +import os import re +from typing import Any, Dict + import boto3 -import json import yaml -import os -from typing import Dict, Any def get_ssm_parameter(name: str, with_decryption: bool = True) -> str: @@ -61,8 +62,8 @@ def get_aws_account_id() -> str: def get_cognito_client_secret() -> str: client = boto3.client("cognito-idp") response = client.describe_user_pool_client( - UserPoolId=get_ssm_parameter("/app/customersupport/agentcore/userpool_id"), - ClientId=get_ssm_parameter("/app/customersupport/agentcore/machine_client_id"), + UserPoolId=get_ssm_parameter("/app/customersupport/agentcore/pool_id"), + ClientId=get_ssm_parameter("/app/customersupport/agentcore/client_id"), ) return response["UserPoolClient"]["ClientSecret"] @@ -136,10 +137,10 @@ def create_safe_markdown_text(text, message_placeholder): """Create safe markdown text with proper encoding and newline handling""" # First encode/decode for safety safe_text = text.encode("utf-16", "surrogatepass").decode("utf-16") - + # Convert newlines to HTML breaks for proper rendering # This handles both actual newlines and any remaining escaped ones - safe_text = safe_text.replace('\n', '
') - safe_text = safe_text.replace('\\n', '
') - - message_placeholder.markdown(safe_text, unsafe_allow_html=True) \ No newline at end of file + safe_text = safe_text.replace("\n", "
") + safe_text = safe_text.replace("\\n", "
") + + message_placeholder.markdown(safe_text, unsafe_allow_html=True) diff --git a/01-tutorials/07-AgentCore-E2E/lab_helpers/utils.py b/01-tutorials/07-AgentCore-E2E/lab_helpers/utils.py index 8bbe9e805..023dc1e12 100644 --- a/01-tutorials/07-AgentCore-E2E/lab_helpers/utils.py +++ b/01-tutorials/07-AgentCore-E2E/lab_helpers/utils.py @@ -76,8 +76,8 @@ def get_aws_account_id() -> str: def get_cognito_client_secret() -> str: client = boto3.client("cognito-idp") response = client.describe_user_pool_client( - UserPoolId=get_ssm_parameter("/app/customersupport/agentcore/userpool_id"), - ClientId=get_ssm_parameter("/app/customersupport/agentcore/machine_client_id"), + UserPoolId=get_ssm_parameter("/app/customersupport/agentcore/pool_id"), + ClientId=get_ssm_parameter("/app/customersupport/agentcore/client_id"), ) return response["UserPoolClient"]["ClientSecret"] @@ -187,11 +187,23 @@ def delete_customer_support_secret(): return False -def setup_cognito_user_pool(): +def get_or_create_cognito_pool(refresh_token=False): boto_session = Session() region = boto_session.region_name # Initialize Cognito client cognito_client = boto3.client("cognito-idp", region_name=region) + try: + # check for existing cognito pool + cognito_config_str = get_customer_support_secret() + cognito_config = json.loads(cognito_config_str) + if refresh_token: + cognito_config["bearer_token"] = reauthenticate_user( + cognito_config["client_id"], cognito_config["client_secret"] + ) + return cognito_config + except: + print("No existing cognito config found. Creating a new one..") + try: # Create User Pool user_pool_response = cognito_client.create_user_pool( @@ -229,10 +241,8 @@ def setup_cognito_user_pool(): Permanent=True, ) - app_client_id = client_id - key = client_secret - message = bytes(username + app_client_id, "utf-8") - key = bytes(key, "utf-8") + message = bytes(username + client_id, "utf-8") + key = bytes(client_secret, "utf-8") secret_hash = base64.b64encode( hmac.new(key, message, digestmod=hashlib.sha256).digest() ).decode() @@ -242,20 +252,18 @@ def setup_cognito_user_pool(): ClientId=client_id, AuthFlow="USER_PASSWORD_AUTH", AuthParameters={ - "USERNAME": "testuser", + "USERNAME": username, "PASSWORD": "MyPassword123!", "SECRET_HASH": secret_hash, }, ) bearer_token = auth_response["AuthenticationResult"]["AccessToken"] + discovery_url = f"https://cognito-idp.{region}.amazonaws.com/{pool_id}/.well-known/openid-configuration" # Output the required values print(f"Pool id: {pool_id}") - print( - f"Discovery URL: https://cognito-idp.{region}.amazonaws.com/{pool_id}/.well-known/openid-configuration" - ) + print(f"Discovery URL: {discovery_url}") print(f"Client ID: {client_id}") print(f"Bearer Token: {bearer_token}") - # Return values if needed for further processing cognito_config = { "pool_id": pool_id, @@ -263,8 +271,14 @@ def setup_cognito_user_pool(): "client_secret": client_secret, "secret_hash": secret_hash, "bearer_token": bearer_token, - "discovery_url": f"https://cognito-idp.{region}.amazonaws.com/{pool_id}/.well-known/openid-configuration", + "discovery_url": discovery_url, } + put_ssm_parameter("/app/customersupport/agentcore/client_id", client_id) + put_ssm_parameter("/app/customersupport/agentcore/pool_id", pool_id) + put_ssm_parameter( + "/app/customersupport/agentcore/cognito_discovery_url", discovery_url + ) + put_ssm_parameter("/app/customersupport/agentcore/client_secret", client_secret) save_customer_support_secret(json.dumps(cognito_config)) @@ -482,6 +496,17 @@ def create_agentcore_runtime_execution_role(): "Action": ["ssm:GetParameter"], "Resource": [f"arn:aws:ssm:{region}:{account_id}:parameter/*"], }, + { + "Sid": "GatewayAccess", + "Effect": "Allow", + "Action": [ + "bedrock-agentcore:GetGateway", + "bedrock-agentcore:InvokeGateway", + ], + "Resource": [ + f"arn:aws:bedrock-agentcore:{region}:{account_id}:gateway/*" + ], + }, ], } @@ -579,64 +604,64 @@ def delete_agentcore_runtime_execution_role(): except Exception as e: print(f"โŒ Error during cleanup: {str(e)}") + def agentcore_memory_cleanup(): - - control_client = boto3.client('bedrock-agentcore-control',region_name=REGION) - + + control_client = boto3.client("bedrock-agentcore-control", region_name=REGION) + """List all memories and their associated strategies""" next_token = None - + while True: # Build request parameters params = {} if next_token: - params['nextToken'] = next_token - + params["nextToken"] = next_token + # List memories try: response = control_client.list_memories(**params) - + # Process each memory - for memory in response.get('memories', []): - memory_id = memory.get('id') + for memory in response.get("memories", []): + memory_id = memory.get("id") print(f"\nMemory ID: {memory_id}") print(f"Status: {memory.get('status')}") - response = control_client.delete_memory( - memoryId=memory_id - ) + response = control_client.delete_memory(memoryId=memory_id) response = control_client.list_memories(**params) print(f"โœ… Successfully deleted memory: {memory_id}") - - response = control_client.list_memories(**params) + + response = control_client.list_memories(**params) # Process each memory status - for memory in response.get('memories', []): - memory_id = memory.get('id') + for memory in response.get("memories", []): + memory_id = memory.get("id") print(f"\nMemory ID: {memory_id}") print(f"Status: {memory.get('status')}") - + except Exception as e: print(f"โš ๏ธ Error getting memory details: {e}") - + # Check for more results - next_token = response.get('nextToken') + next_token = response.get("nextToken") if not next_token: break - + + def gateway_target_cleanup(): - + gateway_client = boto3.client( "bedrock-agentcore-control", region_name=REGION, ) - response = gateway_client.list_gateways() - gateway_id = (response['items'][0]['gatewayId']) + response = gateway_client.list_gateways() + gateway_id = response["items"][0]["gatewayId"] print(f"๐Ÿ—‘๏ธ Deleting all targets for gateway: {gateway_id}") - + # List and delete all targets list_response = gateway_client.list_gateway_targets( gatewayIdentifier=gateway_id, maxResults=100 ) - + for item in list_response["items"]: target_id = item["targetId"] print(f" Deleting target: {target_id}") @@ -644,49 +669,51 @@ def gateway_target_cleanup(): gatewayIdentifier=gateway_id, targetId=target_id ) print(f" โœ… Target {target_id} deleted") - + # Delete the gateway print(f"๐Ÿ—‘๏ธ Deleting gateway: {gateway_id}") gateway_client.delete_gateway(gatewayIdentifier=gateway_id) print(f"โœ… Gateway {gateway_id} deleted successfully") + def runtime_resource_cleanup(): try: # Initialize AWS clients - agentcore_control_client = boto3.client("bedrock-agentcore-control", region_name=REGION) + agentcore_control_client = boto3.client( + "bedrock-agentcore-control", region_name=REGION + ) ecr_client = boto3.client("ecr", region_name=REGION) - + # Delete the AgentCore Runtime # print(" ๐Ÿ—‘๏ธ Deleting AgentCore Runtime...") runtimes = agentcore_control_client.list_agent_runtimes() - for runtime in runtimes['agentRuntimes']: + for runtime in runtimes["agentRuntimes"]: response = agentcore_control_client.delete_agent_runtime( - agentRuntimeId=runtime['agentRuntimeId'] + agentRuntimeId=runtime["agentRuntimeId"] ) print(f" โœ… Agent runtime deleted: {response['status']}") - + # Delete the ECR repository print(" ๐Ÿ—‘๏ธ Deleting ECR repository...") repositories = ecr_client.describe_repositories() - for repo in repositories['repositories']: - if 'bedrock-agentcore-customer_support_agent' in repo['repositoryName']: + for repo in repositories["repositories"]: + if "bedrock-agentcore-customer_support_agent" in repo["repositoryName"]: ecr_client.delete_repository( - repositoryName=repo['repositoryName'], - force=True + repositoryName=repo["repositoryName"], force=True ) print(f" โœ… ECR repository deleted: {repo['repositoryName']}") - - + except Exception as e: print(f" โš ๏ธ Error during runtime cleanup: {e}") + def delete_observability_resources(): # Configuration log_group_name = "agents/customer-support-assistant-logs" log_stream_name = "default" - + logs_client = boto3.client("logs", region_name=REGION) - + # Delete log stream first (must be done before deleting log group) try: print(f" ๐Ÿ—‘๏ธ Deleting log stream '{log_stream_name}'...") @@ -699,7 +726,7 @@ def delete_observability_resources(): print(f" โ„น๏ธ Log stream '{log_stream_name}' doesn't exist") else: print(f" โš ๏ธ Error deleting log stream: {e}") - + # Delete log group try: print(f" ๐Ÿ—‘๏ธ Deleting log group '{log_group_name}'...") @@ -711,6 +738,7 @@ def delete_observability_resources(): else: print(f" โš ๏ธ Error deleting log group: {e}") + def local_file_cleanup(): # List of files to clean up files_to_delete = [ @@ -720,7 +748,7 @@ def local_file_cleanup(): "customer_support_agent.py", "agent_runtime.py", ] - + deleted_files = [] missing_files = [] @@ -734,8 +762,10 @@ def local_file_cleanup(): print(f" โš ๏ธ Error deleting {file}: {e}") else: missing_files.append(file) - + if deleted_files: print(f"\n๐Ÿ“ Successfully deleted {len(deleted_files)} files") if missing_files: - print(f"โ„น๏ธ {len(missing_files)} files were already missing: {', '.join(missing_files)}") \ No newline at end of file + print( + f"โ„น๏ธ {len(missing_files)} files were already missing: {', '.join(missing_files)}" + ) diff --git a/01-tutorials/07-AgentCore-E2E/prerequisite/cognito.yaml b/01-tutorials/07-AgentCore-E2E/prerequisite/cognito.yaml index ab203a8d3..efa26d536 100644 --- a/01-tutorials/07-AgentCore-E2E/prerequisite/cognito.yaml +++ b/01-tutorials/07-AgentCore-E2E/prerequisite/cognito.yaml @@ -211,7 +211,7 @@ Resources: CognitoMachineClientIdParameter: Type: AWS::SSM::Parameter Properties: - Name: /app/customersupport/agentcore/machine_client_id + Name: /app/customersupport/agentcore/client_id Type: String Value: !Ref MachineUserPoolClient Description: Machine Cognito client ID @@ -226,12 +226,12 @@ Resources: Value: !Ref WebUserPoolClient Description: Cognito client ID for web app Tags: - Application: CustomerSuppor + Application: CustomerSupport UserPoolIdParameter: Type: AWS::SSM::Parameter Properties: - Name: /app/customersupport/agentcore/userpool_id + Name: /app/customersupport/agentcore/pool_id Type: String Value: !Ref UserPool Description: Cognito client ID diff --git a/01-tutorials/07-AgentCore-E2E/requirements.txt b/01-tutorials/07-AgentCore-E2E/requirements.txt index 686d62ac0..38f69fd41 100644 --- a/01-tutorials/07-AgentCore-E2E/requirements.txt +++ b/01-tutorials/07-AgentCore-E2E/requirements.txt @@ -1,9 +1,9 @@ strands-agents strands-agents-tools -boto3>=1.40.8 -botocore>=1.40.8 -bedrock-agentcore<=0.1.5 -bedrock-agentcore-starter-toolkit==0.1.14 +boto3==1.40.47 +botocore==1.40.47 +bedrock-agentcore==0.1.7 +bedrock-agentcore-starter-toolkit==0.1.20 aws-opentelemetry-distro ddgs aws-opentelemetry-distro~=0.10.1 diff --git a/01-tutorials/07-AgentCore-E2E/scripts/agentcore_agent_runtime.py b/01-tutorials/07-AgentCore-E2E/scripts/agentcore_agent_runtime.py deleted file mode 100644 index 80d0e6411..000000000 --- a/01-tutorials/07-AgentCore-E2E/scripts/agentcore_agent_runtime.py +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env python3 - -import sys -import boto3 -import click -from utils import get_aws_region - - -@click.command() -@click.argument('agent_name', type=str) -@click.option('--dry-run', is_flag=True, help='Show what would be deleted without actually deleting') -def delete_agent_runtime(agent_name: str, dry_run: bool): - """Delete an agent runtime by name from AWS Bedrock AgentCore. - - AGENT_NAME: Name of the agent runtime to delete - """ - - try: - agentcore_control_client = boto3.client( - "bedrock-agentcore-control", region_name=get_aws_region() - ) - except Exception as e: - click.echo(f"Error creating AWS client: {e}", err=True) - sys.exit(1) - - agent_id = None - found = False - next_token = None - - click.echo(f"Searching for agent runtime: {agent_name}") - - try: - while True: - kwargs = {"maxResults": 20} - if next_token: - kwargs["nextToken"] = next_token - - agent_runtimes = agentcore_control_client.list_agent_runtimes(**kwargs) - - for agent_runtime in agent_runtimes.get("agentRuntimes", []): - if agent_runtime["agentRuntimeName"] == agent_name: - agent_id = agent_runtime["agentRuntimeId"] - found = True - break - - if found: - break - - next_token = agent_runtimes.get("nextToken") - if not next_token: - break - - except Exception as e: - click.echo(f"Error listing agent runtimes: {e}", err=True) - sys.exit(1) - - if found: - click.echo(f"Found agent runtime '{agent_name}' with ID: {agent_id}") - - if dry_run: - click.echo(f"[DRY RUN] Would delete agent runtime: {agent_name}") - return - - try: - agentcore_control_client.delete_agent_runtime(agentRuntimeId=agent_id) - click.echo(f"Successfully deleted agent runtime: {agent_name}") - except Exception as e: - click.echo(f"Error deleting agent runtime: {e}", err=True) - sys.exit(1) - else: - click.echo(f"Agent runtime '{agent_name}' not found", err=True) - sys.exit(1) - - -if __name__ == "__main__": - delete_agent_runtime() diff --git a/01-tutorials/07-AgentCore-E2E/scripts/agentcore_gateway.py b/01-tutorials/07-AgentCore-E2E/scripts/agentcore_gateway.py deleted file mode 100644 index e3e6e062b..000000000 --- a/01-tutorials/07-AgentCore-E2E/scripts/agentcore_gateway.py +++ /dev/null @@ -1,246 +0,0 @@ -#!/usr/bin/python -from typing import List -import os -import sys -import boto3 -import click - -from utils import ( - get_aws_region, - get_ssm_parameter, - put_ssm_parameter, - delete_ssm_parameter, - load_api_spec, - get_cognito_client_secret, -) - - -REGION = get_aws_region() - -gateway_client = boto3.client( - "bedrock-agentcore-control", - region_name=REGION, -) - - -def create_gateway(gateway_name: str, api_spec: List) -> dict: - """Create an AgentCore gateway with the specified configuration.""" - try: - # Use Cognito for Inbound OAuth to our Gateway - lambda_target_config = { - "mcp": { - "lambda": { - "lambdaArn": get_ssm_parameter( - "/app/customersupport/agentcore/lambda_arn" - ), - "toolSchema": {"inlinePayload": api_spec}, - } - } - } - - auth_config = { - "customJWTAuthorizer": { - "allowedClients": [ - get_ssm_parameter( - "/app/customersupport/agentcore/machine_client_id" - ) - ], - "discoveryUrl": get_ssm_parameter( - "/app/customersupport/agentcore/cognito_discovery_url" - ), - } - } - - execution_role_arn = get_ssm_parameter( - "/app/customersupport/agentcore/gateway_iam_role" - ) - - click.echo(f"Creating gateway in region {REGION} with name: {gateway_name}") - click.echo(f"Execution role ARN: {execution_role_arn}") - - create_response = gateway_client.create_gateway( - name=gateway_name, - roleArn=execution_role_arn, - protocolType="MCP", - authorizerType="CUSTOM_JWT", - authorizerConfiguration=auth_config, - description="Customer Support AgentCore Gateway", - ) - - click.echo(f"โœ… Gateway created: {create_response['gatewayId']}") - - # Create gateway target - credential_config = [{"credentialProviderType": "GATEWAY_IAM_ROLE"}] - gateway_id = create_response["gatewayId"] - - create_target_response = gateway_client.create_gateway_target( - gatewayIdentifier=gateway_id, - name="LambdaUsingSDK", - description="Lambda Target using SDK", - targetConfiguration=lambda_target_config, - credentialProviderConfigurations=credential_config, - ) - - click.echo(f"โœ… Gateway target created: {create_target_response['targetId']}") - - gateway = { - "id": gateway_id, - "name": gateway_name, - "gateway_url": create_response["gatewayUrl"], - "gateway_arn": create_response["gatewayArn"], - } - - # Save gateway details to SSM parameters - put_ssm_parameter("/app/customersupport/agentcore/gateway_id", gateway_id) - put_ssm_parameter("/app/customersupport/agentcore/gateway_name", gateway_name) - put_ssm_parameter( - "/app/customersupport/agentcore/gateway_arn", create_response["gatewayArn"] - ) - put_ssm_parameter( - "/app/customersupport/agentcore/gateway_url", create_response["gatewayUrl"] - ) - put_ssm_parameter( - "/app/customersupport/agentcore/cognito_secret", - get_cognito_client_secret(), - with_encryption=True, - ) - - click.echo("โœ… Gateway configuration saved to SSM parameters") - - return gateway - - except Exception as e: - click.echo(f"โŒ Error creating gateway: {str(e)}", err=True) - sys.exit(1) - - -def delete_gateway(gateway_id: str) -> bool: - """Delete a gateway and all its targets.""" - try: - click.echo(f"๐Ÿ—‘๏ธ Deleting all targets for gateway: {gateway_id}") - - # List and delete all targets - list_response = gateway_client.list_gateway_targets( - gatewayIdentifier=gateway_id, maxResults=100 - ) - - for item in list_response["items"]: - target_id = item["targetId"] - click.echo(f" Deleting target: {target_id}") - gateway_client.delete_gateway_target( - gatewayIdentifier=gateway_id, targetId=target_id - ) - click.echo(f" โœ… Target {target_id} deleted") - - # Delete the gateway - click.echo(f"๐Ÿ—‘๏ธ Deleting gateway: {gateway_id}") - gateway_client.delete_gateway(gatewayIdentifier=gateway_id) - click.echo(f"โœ… Gateway {gateway_id} deleted successfully") - - return True - - except Exception as e: - click.echo(f"โŒ Error deleting gateway: {str(e)}", err=True) - return False - - -def get_gateway_id_from_config() -> str: - """Get gateway ID from SSM parameter.""" - try: - return get_ssm_parameter("/app/customersupport/agentcore/gateway_id") - except Exception as e: - click.echo(f"โŒ Error reading gateway ID from SSM: {str(e)}", err=True) - return None - - -@click.group() -@click.pass_context -def cli(ctx): - """AgentCore Gateway Management CLI. - - Create and delete AgentCore gateways for the customer support application. - """ - ctx.ensure_object(dict) - - -@cli.command() -@click.option("--name", required=True, help="Name for the gateway") -@click.option( - "--api-spec-file", - default="prerequisite/lambda/api_spec.json", - help="Path to the API specification file (default: prerequisite/lambda/api_spec.json)", -) -def create(name, api_spec_file): - """Create a new AgentCore gateway.""" - click.echo(f"๐Ÿš€ Creating AgentCore gateway: {name}") - click.echo(f"๐Ÿ“ Region: {REGION}") - - # Validate API spec file exists - if not os.path.exists(api_spec_file): - click.echo(f"โŒ API specification file not found: {api_spec_file}", err=True) - sys.exit(1) - - try: - api_spec = load_api_spec(api_spec_file) - gateway = create_gateway(gateway_name=name, api_spec=api_spec) - click.echo(f"๐ŸŽ‰ Gateway created successfully with ID: {gateway['id']}") - - except Exception as e: - click.echo(f"โŒ Failed to create gateway: {str(e)}", err=True) - sys.exit(1) - - -@cli.command() -@click.option( - "--gateway-id", - help="Gateway ID to delete (if not provided, will read from gateway.config)", -) -@click.option("--confirm", is_flag=True, help="Skip confirmation prompt") -def delete(gateway_id, confirm): - """Delete an AgentCore gateway and all its targets.""" - - # If no gateway ID provided, try to read from config - if not gateway_id: - gateway_id = get_gateway_id_from_config() - if not gateway_id: - click.echo( - "โŒ No gateway ID provided and couldn't read from SSM parameters", - err=True, - ) - sys.exit(1) - click.echo(f"๐Ÿ“– Using gateway ID from SSM: {gateway_id}") - - # Confirmation prompt - if not confirm: - if not click.confirm( - f"โš ๏ธ Are you sure you want to delete gateway {gateway_id}? This action cannot be undone." - ): - click.echo("โŒ Operation cancelled") - sys.exit(0) - - click.echo(f"๐Ÿ—‘๏ธ Deleting gateway: {gateway_id}") - - if delete_gateway(gateway_id): - click.echo("โœ… Gateway deleted successfully") - - # Clean up SSM parameters - delete_ssm_parameter("/app/customersupport/agentcore/gateway_id") - delete_ssm_parameter("/app/customersupport/agentcore/gateway_name") - delete_ssm_parameter("/app/customersupport/agentcore/gateway_arn") - delete_ssm_parameter("/app/customersupport/agentcore/gateway_url") - delete_ssm_parameter("/app/customersupport/agentcore/cognito_secret") - click.echo("๐Ÿงน Removed gateway SSM parameters") - - # Clean up config file if it exists (backward compatibility) - if os.path.exists("gateway.config"): - os.remove("gateway.config") - click.echo("๐Ÿงน Removed gateway.config file") - - click.echo("๐ŸŽ‰ Gateway and configuration deleted successfully") - else: - click.echo("โŒ Failed to delete gateway", err=True) - sys.exit(1) - - -if __name__ == "__main__": - cli() diff --git a/01-tutorials/07-AgentCore-E2E/scripts/agentcore_memory.py b/01-tutorials/07-AgentCore-E2E/scripts/agentcore_memory.py deleted file mode 100644 index ce3f71689..000000000 --- a/01-tutorials/07-AgentCore-E2E/scripts/agentcore_memory.py +++ /dev/null @@ -1,178 +0,0 @@ -#!/usr/bin/python -import click -import boto3 -import sys -from botocore.exceptions import ClientError -from bedrock_agentcore.memory import MemoryClient -from bedrock_agentcore.memory.constants import StrategyType -from utils import get_aws_region - -# AWS clients -REGION = get_aws_region() -ssm = boto3.client("ssm", region_name=REGION) -memory_client = MemoryClient() - - -def store_memory_id_in_ssm(param_name: str, memory_id: str): - ssm.put_parameter(Name=param_name, Value=memory_id, Type="String", Overwrite=True) - click.echo(f"๐Ÿ” Stored memory_id in SSM: {param_name}") - - -def get_memory_id_from_ssm(param_name: str): - try: - response = ssm.get_parameter(Name=param_name) - return response["Parameter"]["Value"] - except ClientError as e: - raise click.ClickException(f"โŒ Could not retrieve memory_id from SSM: {e}") - - -def delete_ssm_param(param_name: str): - try: - ssm.delete_parameter(Name=param_name) - click.echo(f"๐Ÿงน Deleted SSM parameter: {param_name}") - except ClientError as e: - click.echo(f"โš ๏ธ Failed to delete SSM parameter: {e}") - - -@click.group() -@click.pass_context -def cli(ctx): - """AgentCore Memory Management CLI. - - Create and delete AgentCore memory resources for the customer support application. - """ - ctx.ensure_object(dict) - - -@cli.command() -@click.option( - "--name", default="CustomerSupportMemory", help="Name of the memory resource" -) -@click.option( - "--ssm-param", - default="/app/customersupport/agentcore/memory_id", - help="SSM parameter to store memory_id", -) -@click.option( - "--event-expiry-days", - default=30, - type=int, - help="Number of days before events expire (default: 30)", -) -def create(name, ssm_param, event_expiry_days): - """Create a new AgentCore memory resource.""" - click.echo(f"๐Ÿš€ Creating AgentCore memory: {name}") - click.echo(f"๐Ÿ“ Region: {REGION}") - click.echo(f"โฑ๏ธ Event expiry: {event_expiry_days} days") - - strategies = [ - { - StrategyType.SEMANTIC.value: { - "name": "fact_extractor", - "description": "Extracts and stores factual information", - "namespaces": ["support/user/{actorId}/facts"], - }, - }, - { - StrategyType.SUMMARY.value: { - "name": "conversation_summary", - "description": "Captures summaries of conversations", - "namespaces": ["support/user/{actorId}/{sessionId}"], - }, - }, - { - StrategyType.USER_PREFERENCE.value: { - "name": "user_preferences", - "description": "Captures user preferences and settings", - "namespaces": ["support/user/{actorId}/preferences"], - }, - }, - ] - - try: - click.echo("๐Ÿ”„ Creating memory resource...") - memory = memory_client.create_memory_and_wait( - name=name, - strategies=strategies, - description="Memory for customer support agent", - event_expiry_days=event_expiry_days, - ) - memory_id = memory["id"] - click.echo(f"โœ… Memory created successfully: {memory_id}") - - except Exception as e: - if "already exists" in str(e): - click.echo("๐Ÿ“‹ Memory already exists, finding existing resource...") - memories = memory_client.list_memories() - memory_id = next( - (m["id"] for m in memories if name in m.get("name", "")), None - ) - if memory_id: - click.echo(f"โœ… Using existing memory: {memory_id}") - else: - click.echo("โŒ Could not find existing memory resource", err=True) - sys.exit(1) - else: - click.echo(f"โŒ Error creating memory: {str(e)}", err=True) - sys.exit(1) - - try: - store_memory_id_in_ssm(ssm_param, memory_id) - click.echo("๐ŸŽ‰ Memory setup completed successfully!") - click.echo(f" Memory ID: {memory_id}") - click.echo(f" SSM Parameter: {ssm_param}") - - except Exception as e: - click.echo(f"โš ๏ธ Memory created but failed to store in SSM: {str(e)}", err=True) - - -@cli.command() -@click.option( - "--memory-id", - help="Memory ID to delete (if not provided, will read from SSM parameter)", -) -@click.option( - "--ssm-param", - default="/app/customersupport/agentcore/memory_id", - help="SSM parameter to retrieve memory_id from", -) -@click.option("--confirm", is_flag=True, help="Skip confirmation prompt") -def delete(memory_id, ssm_param, confirm): - """Delete an AgentCore memory resource.""" - - # If no memory ID provided, try to read from SSM - if not memory_id: - try: - memory_id = get_memory_id_from_ssm(ssm_param) - click.echo(f"๐Ÿ“– Using memory ID from SSM: {memory_id}") - except Exception: - click.echo( - "โŒ No memory ID provided and couldn't read from SSM parameter", - err=True, - ) - sys.exit(1) - - # Confirmation prompt - if not confirm: - if not click.confirm( - f"โš ๏ธ Are you sure you want to delete memory {memory_id}? This action cannot be undone." - ): - click.echo("โŒ Operation cancelled") - sys.exit(0) - - click.echo(f"๐Ÿ—‘๏ธ Deleting memory: {memory_id}") - - try: - memory_client.delete_memory(memory_id=memory_id) - click.echo(f"โœ… Memory deleted successfully: {memory_id}") - except Exception as e: - click.echo(f"โŒ Error deleting memory: {str(e)}", err=True) - sys.exit(1) - - # Always delete SSM parameter - delete_ssm_param(ssm_param) - click.echo("๐ŸŽ‰ Memory and SSM parameter deleted successfully") - - -if __name__ == "__main__": - cli() diff --git a/01-tutorials/07-AgentCore-E2E/scripts/cognito_credentials_provider.py b/01-tutorials/07-AgentCore-E2E/scripts/cognito_credentials_provider.py deleted file mode 100644 index 9807161d6..000000000 --- a/01-tutorials/07-AgentCore-E2E/scripts/cognito_credentials_provider.py +++ /dev/null @@ -1,254 +0,0 @@ -#!/usr/bin/python -import boto3 -import click -import sys -from botocore.exceptions import ClientError -from utils import get_ssm_parameter, get_aws_region - -REGION = get_aws_region() - -identity_client = boto3.client( - "bedrock-agentcore-control", - region_name=REGION, -) -ssm = boto3.client("ssm", region_name=REGION) - - -def store_provider_name_in_ssm(provider_name: str): - """Store credential provider name in SSM parameter.""" - param_name = "/app/customersupport/agentcore/cognito_provider" - try: - ssm.put_parameter( - Name=param_name, Value=provider_name, Type="String", Overwrite=True - ) - click.echo(f"๐Ÿ” Stored provider name in SSM: {param_name}") - except ClientError as e: - click.echo(f"โš ๏ธ Failed to store provider name in SSM: {e}") - - -def get_provider_name_from_ssm() -> str: - """Get credential provider name from SSM parameter.""" - param_name = "/app/customersupport/agentcore/cognito_provider" - try: - response = ssm.get_parameter(Name=param_name) - return response["Parameter"]["Value"] - except ClientError: - return None - - -def delete_ssm_param(): - """Delete SSM parameter for provider.""" - param_name = "/app/customersupport/agentcore/cognito_provider" - try: - ssm.delete_parameter(Name=param_name) - click.echo(f"๐Ÿงน Deleted SSM parameter: {param_name}") - except ClientError as e: - click.echo(f"โš ๏ธ Failed to delete SSM parameter: {e}") - - -def create_cognito_provider(provider_name: str) -> dict: - """Create a Cognito OAuth2 credential provider.""" - try: - click.echo("๐Ÿ“ฅ Fetching Cognito configuration from SSM...") - client_id = get_ssm_parameter( - "/app/customersupport/agentcore/machine_client_id" - ) - click.echo(f"โœ… Retrieved client ID: {client_id}") - - client_secret = get_ssm_parameter( - "/app/customersupport/agentcore/cognito_secret" - ) - click.echo(f"โœ… Retrieved client secret: {client_secret[:4]}***") - - issuer = get_ssm_parameter( - "/app/customersupport/agentcore/cognito_discovery_url" - ) - auth_url = get_ssm_parameter("/app/customersupport/agentcore/cognito_auth_url") - token_url = get_ssm_parameter( - "/app/customersupport/agentcore/cognito_token_url" - ) - - click.echo(f"โœ… Issuer: {issuer}") - click.echo(f"โœ… Authorization Endpoint: {auth_url}") - click.echo(f"โœ… Token Endpoint: {token_url}") - - click.echo("โš™๏ธ Creating OAuth2 credential provider...") - cognito_provider = identity_client.create_oauth2_credential_provider( - name=provider_name, - credentialProviderVendor="CustomOauth2", - oauth2ProviderConfigInput={ - "customOauth2ProviderConfig": { - "clientId": client_id, - "clientSecret": client_secret, - "oauthDiscovery": { - "authorizationServerMetadata": { - "issuer": issuer, - "authorizationEndpoint": auth_url, - "tokenEndpoint": token_url, - "responseTypes": ["code", "token"], - } - }, - } - }, - ) - - click.echo("โœ… OAuth2 credential provider created successfully") - provider_arn = cognito_provider["credentialProviderArn"] - click.echo(f" Provider ARN: {provider_arn}") - click.echo(f" Provider Name: {cognito_provider['name']}") - - # Store provider name in SSM - store_provider_name_in_ssm(provider_name) - - return cognito_provider - - except Exception as e: - click.echo(f"โŒ Error creating Cognito credential provider: {str(e)}", err=True) - sys.exit(1) - - -def delete_cognito_provider(provider_name: str) -> bool: - """Delete a Cognito OAuth2 credential provider.""" - try: - click.echo(f"๐Ÿ—‘๏ธ Deleting OAuth2 credential provider: {provider_name}") - - identity_client.delete_oauth2_credential_provider(name=provider_name) - - click.echo("โœ… OAuth2 credential provider deleted successfully") - return True - - except Exception as e: - click.echo(f"โŒ Error deleting credential provider: {str(e)}", err=True) - return False - - -def list_credential_providers() -> list: - """List all OAuth2 credential providers.""" - try: - response = identity_client.list_oauth2_credential_providers(maxResults=20) - providers = response.get("credentialProviders", []) - return providers - - except Exception as e: - click.echo(f"โŒ Error listing credential providers: {str(e)}", err=True) - return [] - - -def find_provider_by_name(provider_name: str) -> bool: - """Check if provider exists by name.""" - providers = list_credential_providers() - for provider in providers: - if provider.get("name") == provider_name: - return True - return False - - -@click.group() -@click.pass_context -def cli(ctx): - """AgentCore Cognito Credential Provider Management CLI. - - Create and delete OAuth2 credential providers for Cognito authentication. - """ - ctx.ensure_object(dict) - - -@cli.command() -@click.option( - "--name", required=True, help="Name for the credential provider (required)" -) -def create(name): - """Create a new Cognito OAuth2 credential provider.""" - click.echo(f"๐Ÿš€ Creating Cognito credential provider: {name}") - click.echo(f"๐Ÿ“ Region: {REGION}") - - # Check if provider already exists in SSM - existing_name = get_provider_name_from_ssm() - if existing_name: - click.echo(f"โš ๏ธ A provider already exists in SSM: {existing_name}") - if not click.confirm("Do you want to replace it?"): - click.echo("โŒ Operation cancelled") - sys.exit(0) - - try: - provider = create_cognito_provider(provider_name=name) - click.echo("๐ŸŽ‰ Cognito credential provider created successfully!") - click.echo(f" Provider ARN: {provider['credentialProviderArn']}") - click.echo(f" Provider Name: {provider['name']}") - - except Exception as e: - click.echo(f"โŒ Failed to create credential provider: {str(e)}", err=True) - sys.exit(1) - - -@cli.command() -@click.option( - "--name", - help="Name of the credential provider to delete (if not provided, will read from SSM parameter)", -) -@click.option("--confirm", is_flag=True, help="Skip confirmation prompt") -def delete(name, confirm): - """Delete a Cognito OAuth2 credential provider.""" - - # If no name provided, try to get from SSM - if not name: - name = get_provider_name_from_ssm() - if not name: - click.echo( - "โŒ No provider name provided and couldn't read from SSM parameter", - err=True, - ) - click.echo(" Hint: Use 'list' command to see available providers") - sys.exit(1) - click.echo(f"๐Ÿ“– Using provider name from SSM: {name}") - - click.echo(f"๐Ÿ” Looking for credential provider: {name}") - - # Check if provider exists - if not find_provider_by_name(name): - click.echo(f"โŒ No credential provider found with name: {name}", err=True) - click.echo(" Hint: Use 'list' command to see available providers") - sys.exit(1) - - click.echo(f"๐Ÿ“– Found provider: {name}") - - # Confirmation prompt - if not confirm: - if not click.confirm( - f"โš ๏ธ Are you sure you want to delete credential provider '{name}'? This action cannot be undone." - ): - click.echo("โŒ Operation cancelled") - sys.exit(0) - - if delete_cognito_provider(name): - click.echo(f"โœ… Credential provider '{name}' deleted successfully") - - # Always delete SSM parameter - delete_ssm_param() - click.echo("๐ŸŽ‰ Credential provider and SSM parameter deleted successfully") - else: - click.echo("โŒ Failed to delete credential provider", err=True) - sys.exit(1) - - -@cli.command("list") -def list_providers(): - """List all OAuth2 credential providers.""" - providers = list_credential_providers() - - if not providers: - click.echo("โ„น๏ธ No credential providers found") - return - - click.echo(f"๐Ÿ“‹ Found {len(providers)} credential provider(s):") - for provider in providers: - click.echo(f" โ€ข Name: {provider.get('name', 'N/A')}") - click.echo(f" ARN: {provider['credentialProviderArn']}") - click.echo(f" Vendor: {provider.get('credentialProviderVendor', 'N/A')}") - if "createdTime" in provider: - click.echo(f" Created: {provider['createdTime']}") - click.echo() - - -if __name__ == "__main__": - cli() diff --git a/01-tutorials/07-AgentCore-E2E/scripts/prereq.sh b/01-tutorials/07-AgentCore-E2E/scripts/prereq.sh index e2fbc4057..ccf795223 100755 --- a/01-tutorials/07-AgentCore-E2E/scripts/prereq.sh +++ b/01-tutorials/07-AgentCore-E2E/scripts/prereq.sh @@ -9,8 +9,17 @@ INFRA_STACK_NAME=${2:-CustomerSupportStackInfra} COGNITO_STACK_NAME=${3:-CustomerSupportStackCognito} INFRA_TEMPLATE_FILE="prerequisite/infrastructure.yaml" COGNITO_TEMPLATE_FILE="prerequisite/cognito.yaml" -REGION=$(aws configure get region 2>/dev/null || echo "us-west-2") +# First try to get region from environment variable +if [ -z "${AWS_REGION}" ]; then + # If AWS_REGION is not set, try to get it from AWS CLI config + REGION=$(aws configure get region 2>/dev/null || echo "us-west-2") + # Export it as an environment variable + export AWS_REGION="${REGION}" +fi +echo "Region is set to: ${AWS_REGION}" +export REGION="${AWS_REGION}" + # Get AWS Account ID with proper error handling echo "๐Ÿ” Getting AWS Account ID..." @@ -21,7 +30,6 @@ if [ $? -ne 0 ] || [ -z "$ACCOUNT_ID" ] || [ "$ACCOUNT_ID" = "None" ]; then exit 1 fi - FULL_BUCKET_NAME="${BUCKET_NAME}-${ACCOUNT_ID}-${REGION}" ZIP_FILE="lambda.zip" LAYER_ZIP_FILE="ddgs-layer.zip" diff --git a/01-tutorials/07-AgentCore-E2E/scripts/utils.py b/01-tutorials/07-AgentCore-E2E/scripts/utils.py deleted file mode 100644 index 79448772f..000000000 --- a/01-tutorials/07-AgentCore-E2E/scripts/utils.py +++ /dev/null @@ -1,120 +0,0 @@ -import boto3 -import json -import yaml -import os -from typing import Dict, Any - - -def get_ssm_parameter(name: str, with_decryption: bool = True) -> str: - ssm = boto3.client("ssm") - - response = ssm.get_parameter(Name=name, WithDecryption=with_decryption) - - return response["Parameter"]["Value"] - - -def put_ssm_parameter( - name: str, value: str, parameter_type: str = "String", with_encryption: bool = False -) -> None: - ssm = boto3.client("ssm") - - put_params = { - "Name": name, - "Value": value, - "Type": parameter_type, - "Overwrite": True, - } - - if with_encryption: - put_params["Type"] = "SecureString" - - ssm.put_parameter(**put_params) - - -def delete_ssm_parameter(name: str) -> None: - ssm = boto3.client("ssm") - try: - ssm.delete_parameter(Name=name) - except ssm.exceptions.ParameterNotFound: - pass - - -def load_api_spec(file_path: str) -> list: - with open(file_path, "r") as f: - data = json.load(f) - if not isinstance(data, list): - raise ValueError("Expected a list in the JSON file") - return data - - -def get_aws_region() -> str: - session = boto3.session.Session() - return session.region_name - - -def get_aws_account_id() -> str: - sts = boto3.client("sts") - return sts.get_caller_identity()["Account"] - - -def get_cognito_client_secret() -> str: - client = boto3.client("cognito-idp") - response = client.describe_user_pool_client( - UserPoolId=get_ssm_parameter("/app/customersupport/agentcore/userpool_id"), - ClientId=get_ssm_parameter("/app/customersupport/agentcore/machine_client_id"), - ) - return response["UserPoolClient"]["ClientSecret"] - - -def read_config(file_path: str) -> Dict[str, Any]: - """ - Read configuration from a file path. Supports JSON, YAML, and YML formats. - - Args: - file_path (str): Path to the configuration file - - Returns: - Dict[str, Any]: Configuration data as a dictionary - - Raises: - FileNotFoundError: If the file doesn't exist - ValueError: If the file format is not supported or invalid - yaml.YAMLError: If YAML parsing fails - json.JSONDecodeError: If JSON parsing fails - """ - if not os.path.exists(file_path): - raise FileNotFoundError(f"Configuration file not found: {file_path}") - - # Get file extension to determine format - _, ext = os.path.splitext(file_path.lower()) - - try: - with open(file_path, "r", encoding="utf-8") as file: - if ext == ".json": - return json.load(file) - elif ext in [".yaml", ".yml"]: - return yaml.safe_load(file) - else: - # Try to auto-detect format by attempting JSON first, then YAML - content = file.read() - file.seek(0) - - # Try JSON first - try: - return json.loads(content) - except json.JSONDecodeError: - # Try YAML - try: - return yaml.safe_load(content) - except yaml.YAMLError: - raise ValueError( - f"Unsupported configuration file format: {ext}. " - f"Supported formats: .json, .yaml, .yml" - ) - - except json.JSONDecodeError as e: - raise ValueError(f"Invalid JSON in configuration file {file_path}: {e}") - except yaml.YAMLError as e: - raise ValueError(f"Invalid YAML in configuration file {file_path}: {e}") - except Exception as e: - raise ValueError(f"Error reading configuration file {file_path}: {e}")