From abaf5f8f3188f42671a27c1bf9e4bc62c54bd198 Mon Sep 17 00:00:00 2001 From: sentil mohan Date: Fri, 5 Jun 2026 17:38:52 +0100 Subject: [PATCH 1/4] feat: add AgentCore Gateway tool search plugin sample Add sample demonstrating semantic tool discovery with AgentCore Gateway and Strands Agents using travel-domain Lambda tools. --- .../agentcore-tool-search-plugin-sample.ipynb | 948 +++++++++++++ .../lambda/tool_schemas.json | 493 +++++++ .../lambda/travel_tools.py | 1226 +++++++++++++++++ 3 files changed, 2667 insertions(+) create mode 100644 03-integrations/gateway/agentcore-tool-search-plugin/agentcore-tool-search-plugin-sample.ipynb create mode 100644 03-integrations/gateway/agentcore-tool-search-plugin/lambda/tool_schemas.json create mode 100644 03-integrations/gateway/agentcore-tool-search-plugin/lambda/travel_tools.py diff --git a/03-integrations/gateway/agentcore-tool-search-plugin/agentcore-tool-search-plugin-sample.ipynb b/03-integrations/gateway/agentcore-tool-search-plugin/agentcore-tool-search-plugin-sample.ipynb new file mode 100644 index 000000000..21e7d5e7e --- /dev/null +++ b/03-integrations/gateway/agentcore-tool-search-plugin/agentcore-tool-search-plugin-sample.ipynb @@ -0,0 +1,948 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "introduction", + "metadata": {}, + "source": [ + "# Semantic Tool Discovery with the AgentCore Tool Search Plugin for Strands Agents\n", + "\n", + "## 1. Introduction\n", + "\n", + "This notebook demonstrates how to build a **Strands agent** that interacts with **Amazon Bedrock AgentCore Gateway** to access travel-domain tools via semantic tool search. You will deploy a plain Lambda function containing travel tools, register it with AgentCore Gateway as a tool target, and create a Strands agent that dynamically discovers and invokes the right tools based on user intent.\n", + "\n", + "### What you will learn\n", + "\n", + "- **Deploy a Lambda function** as a tool target for AgentCore Gateway\n", + "- **Register the function** with AgentCore Gateway so tools become discoverable\n", + "- **Create a Strands agent** with `AgentCoreToolSearchPlugin` for semantic tool discovery\n", + "- **Invoke the agent** across multiple travel domains (flights, hotels, car rentals, restaurants, currency, loyalty programs)\n", + "\n", + "### Architecture Overview\n", + "\n", + "The architecture follows this flow:\n", + "\n", + "
\n",
+    "┌─────────────────┐    hook       ┌───────────────────────────┐   MCP/search   ┌─────────────────────┐            ┌──────────────────┐\n",
+    "│  Strands Agent  │──────────────▶│  AgentCoreToolSearchPlugin│───────────────▶│  AgentCore Gateway  │            │  Lambda Function │\n",
+    "│  (LLM reasoning)│◀── tools ─────│ (semantic tool discovery) │◀── tools ──────│  (MCP protocol)     │            |  (travel tools)  │\n",
+    "│                 │               └───────────────────────────┘                │                     │            │                  │\n",
+    "│                 │─────────────── MCP/tool call ─────────────────────────────▶│                     │───invoke──▶│                  │\n",
+    "│                 │◀────────────── tool result ─────────────────────────────── │                     │◀─ result ──│                  │\n",
+    "└─────────────────┘                                                            └─────────────────────┘            └──────────────────┘\n",
+    "
\n" + ] + }, + { + "cell_type": "markdown", + "id": "prerequisites", + "metadata": {}, + "source": [ + "## 2. Prerequisites\n", + "\n", + "Before running this notebook, ensure you have the following:\n", + "\n", + "- **AWS Account** with appropriate permissions configured\n", + "- **Python 3.12+** installed\n", + "- **AWS CLI** installed and configured with valid credentials\n", + "- **boto3** Python SDK installed\n", + "- **IAM permissions** for the calling identity:\n", + " - `lambda:*` - Create, invoke, and delete Lambda functions\n", + " - `iam:CreateRole`, `iam:AttachRolePolicy`, `iam:DetachRolePolicy`, `iam:DeleteRole` - Manage Lambda execution role\n", + " - `bedrock:*` - Access Amazon Bedrock models for agent reasoning\n", + " - `bedrock-agentcore:*` - Register tools and connect to AgentCore Gateway" + ] + }, + { + "cell_type": "markdown", + "id": "cost-warning", + "metadata": {}, + "source": [ + "Running this notebook will create AWS resources that may incur charges:\n", + "\n", + "- **AWS Lambda** - Function invocations during deployment verification and agent tool calls\n", + "- **Amazon Bedrock** - Model inference for agent reasoning and intent classification\n", + "- **AgentCore Gateway** - Gateway usage for tool registration and MCP-based tool invocations\n", + "\n", + "To avoid ongoing costs, be sure to run the **Cleanup** section at the end of this notebook to delete all created resources." + ] + }, + { + "cell_type": "markdown", + "id": "env-setup-header", + "metadata": {}, + "source": [ + "## 3. Environment Setup\n", + "\n", + "In this section we install the required Python packages, verify that your AWS credentials are configured correctly, and define all configurable parameters in one place." + ] + }, + { + "cell_type": "markdown", + "id": "install-packages-intro", + "metadata": {}, + "source": [ + "### Install Required Packages\n", + "\n", + "The following cell installs the Python packages needed for this tutorial:\n", + "\n", + "- `strands-agents` - The Strands SDK for building AI agents\n", + "- `strands-agents-tools` - Pre-built tools for Strands agents\n", + "- `boto3` - AWS SDK for Python\n", + "- `bedrock-agentcore[strands-agents]` - AgentCore SDK with Strands plugin for semantic tool discovery\n", + "- `mcp-proxy-for-aws` - IAM-authenticated MCP transport for connecting to AgentCore Gateway" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "install-packages", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install --quiet strands-agents strands-agents-tools boto3 \"bedrock-agentcore[strands-agents]\" mcp-proxy-for-aws" + ] + }, + { + "cell_type": "markdown", + "id": "verify-credentials-intro", + "metadata": {}, + "source": [ + "### Verify AWS Credentials\n", + "\n", + "Before proceeding, let's verify that your AWS credentials are configured correctly and confirm which account and region you are operating in." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "verify-credentials", + "metadata": {}, + "outputs": [], + "source": [ + "import boto3\n", + "\n", + "sts = boto3.client(\"sts\")\n", + "identity = sts.get_caller_identity()\n", + "print(f\"Account: {identity['Account']}\")\n", + "print(f\"ARN: {identity['Arn']}\")\n", + "print(\"[OK] AWS credentials verified successfully\")" + ] + }, + { + "cell_type": "markdown", + "id": "configuration-intro", + "metadata": {}, + "source": [ + "### Configuration\n", + "\n", + "All configurable parameters for this tutorial are defined in the cell below. This is the single place where you can customize the behavior for your environment.\n", + "\n", + "The AgentCore Gateway will be created later in the notebook - no pre-existing gateway is required." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "configuration", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "# Configuration - modify these values for your environment\n", + "AWS_REGION = os.environ.get(\"AWS_REGION\", \"us-east-1\")\n", + "LAMBDA_FUNCTION_NAME = \"agentcore-travel-tools\"\n", + "LAMBDA_ROLE_NAME = \"agentcore-travel-tools-role\"\n", + "MODEL_ID = \"us.anthropic.claude-sonnet-4-20250514-v1:0\"\n", + "\n", + "print(f\"Region: {AWS_REGION}\")\n", + "print(f\"Lambda Function: {LAMBDA_FUNCTION_NAME}\")\n", + "print(f\"Model: {MODEL_ID}\")" + ] + }, + { + "cell_type": "markdown", + "id": "deploy-lambda-header", + "metadata": {}, + "source": [ + "## 4. Deploy the Lambda Function\n", + "\n", + "In this section we deploy a **plain Lambda function** as a tool target for AgentCore Gateway. The function contains 34 travel-domain tools across 9 domains (flights, hotels, car rentals, restaurants, currency, loyalty, weather, activities, and trip planning).\n", + "\n", + "**Important:** This Lambda is NOT an MCP server. It is a standard Lambda function that receives tool arguments as input and returns results. AgentCore Gateway handles the MCP protocol translation - it wraps the Lambda as a tool target and routes MCP tool calls from the agent to the function automatically." + ] + }, + { + "cell_type": "markdown", + "id": "iam-role-intro", + "metadata": {}, + "source": [ + "### Create IAM Execution Role\n", + "\n", + "The Lambda function needs an IAM execution role to run. This role grants the Lambda service permission to assume it, and we attach the `AWSLambdaBasicExecutionRole` managed policy for CloudWatch Logs access." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "create-iam-role", + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "import time\n", + "\n", + "iam = boto3.client(\"iam\", region_name=AWS_REGION)\n", + "\n", + "trust_policy = {\n", + " \"Version\": \"2012-10-17\",\n", + " \"Statement\": [\n", + " {\n", + " \"Effect\": \"Allow\",\n", + " \"Principal\": {\"Service\": \"lambda.amazonaws.com\"},\n", + " \"Action\": \"sts:AssumeRole\"\n", + " }\n", + " ]\n", + "}\n", + "\n", + "try:\n", + " role_response = iam.create_role(\n", + " RoleName=LAMBDA_ROLE_NAME,\n", + " AssumeRolePolicyDocument=json.dumps(trust_policy),\n", + " Description=\"Execution role for AgentCore travel tools Lambda\"\n", + " )\n", + " role_arn = role_response[\"Role\"][\"Arn\"]\n", + " print(f\"\\u2705 Created IAM role: {LAMBDA_ROLE_NAME}\")\n", + " print(f\" ARN: {role_arn}\")\n", + " \n", + " # Attach basic execution policy for CloudWatch Logs\n", + " iam.attach_role_policy(\n", + " RoleName=LAMBDA_ROLE_NAME,\n", + " PolicyArn=\"arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole\"\n", + " )\n", + " print(\"\\u2705 Attached AWSLambdaBasicExecutionRole policy\")\n", + " \n", + " # Wait for IAM role propagation\n", + " print(\"\\n\\u23f3 Waiting 10 seconds for IAM role propagation...\")\n", + " time.sleep(10)\n", + " print(\"\\u2705 Role propagation complete\")\n", + " \n", + "except iam.exceptions.EntityAlreadyExistsException:\n", + " role_arn = f\"arn:aws:iam::{identity['Account']}:role/{LAMBDA_ROLE_NAME}\"\n", + " print(f\"\\u2139\\ufe0f IAM role already exists: {LAMBDA_ROLE_NAME}\")\n", + " print(f\" ARN: {role_arn}\")" + ] + }, + { + "cell_type": "markdown", + "id": "deploy-lambda-intro", + "metadata": {}, + "source": [ + "### Deploy the Lambda Function\n", + "\n", + "We package `lambda/travel_tools.py` into a zip archive and deploy it. If the function already exists (from a previous run), we update its code instead.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "deploy-lambda", + "metadata": {}, + "outputs": [], + "source": [ + "import zipfile\n", + "import io\n", + "\n", + "lambda_client = boto3.client(\"lambda\", region_name=AWS_REGION)\n", + "\n", + "# Package the Lambda function\n", + "zip_buffer = io.BytesIO()\n", + "with zipfile.ZipFile(zip_buffer, \"w\", zipfile.ZIP_DEFLATED) as zf:\n", + " zf.write(\"lambda/travel_tools.py\", \"travel_tools.py\")\n", + "zip_buffer.seek(0)\n", + "\n", + "try:\n", + " response = lambda_client.create_function(\n", + " FunctionName=LAMBDA_FUNCTION_NAME,\n", + " Runtime=\"python3.12\",\n", + " Role=role_arn,\n", + " Handler=\"travel_tools.lambda_handler\",\n", + " Code={\"ZipFile\": zip_buffer.read()},\n", + " Description=\"Travel domain tools for AgentCore Gateway\",\n", + " Timeout=30,\n", + " MemorySize=256\n", + " )\n", + " print(f\"\\u2705 Lambda function created: {LAMBDA_FUNCTION_NAME}\")\n", + " print(f\" ARN: {response['FunctionArn']}\")\n", + " lambda_arn = response[\"FunctionArn\"]\n", + "except lambda_client.exceptions.ResourceConflictException:\n", + " print(f\"\\u2139\\ufe0f Lambda function already exists: {LAMBDA_FUNCTION_NAME}\")\n", + " # Update the function code instead\n", + " zip_buffer.seek(0)\n", + " lambda_client.update_function_code(\n", + " FunctionName=LAMBDA_FUNCTION_NAME,\n", + " ZipFile=zip_buffer.read()\n", + " )\n", + " func_info = lambda_client.get_function(FunctionName=LAMBDA_FUNCTION_NAME)\n", + " lambda_arn = func_info[\"Configuration\"][\"FunctionArn\"]\n", + " print(f\" Updated function code. ARN: {lambda_arn}\")" + ] + }, + { + "cell_type": "markdown", + "id": "verify-lambda-intro", + "metadata": {}, + "source": [ + "### Verify Lambda Deployment\n", + "\n", + "Let's verify the deployment by invoking the Lambda function with a test event. We'll call the `get_supported_currencies` tool to confirm the function is responding correctly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "verify-lambda", + "metadata": {}, + "outputs": [], + "source": [ + "# Verify the Lambda function by invoking a test tool call\n", + "# For direct invocation, we pass tool_name in the event (fallback mode)\n", + "# In production, the gateway passes tool name via context.client_context.custom\n", + "test_event = {\n", + " \"tool_name\": \"get_supported_currencies\"\n", + "}\n", + "\n", + "response = lambda_client.invoke(\n", + " FunctionName=LAMBDA_FUNCTION_NAME,\n", + " Payload=json.dumps(test_event)\n", + ")\n", + "\n", + "payload = json.loads(response[\"Payload\"].read())\n", + "print(\"[OK] Lambda function verified successfully!\")\n", + "print(f\" Supported currencies: {payload['total']} currencies available\")\n", + "print(f\" Sample: {[c['code'] for c in payload['currencies'][:5]]}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "gateway-registration-header", + "metadata": {}, + "source": [ + "## 5. Create AgentCore Gateway and Register Tools\n", + "\n", + "**Amazon Bedrock AgentCore Gateway** is a managed service that provides an MCP-compatible endpoint for your agents. In this section, we'll:\n", + "\n", + "1. **Create a new gateway** - This gives us a managed MCP endpoint\n", + "2. **Register the Lambda function** as a tool target with the gateway\n", + "3. **Verify** the tools are discoverable\n" + ] + }, + { + "cell_type": "markdown", + "id": "create-gateway-intro", + "metadata": {}, + "source": [ + "### Create the AgentCore Gateway\n", + "\n", + "First, we create a new AgentCore Gateway. This gives us a managed MCP endpoint that agents can connect to. The gateway will handle MCP protocol translation for any tool targets we register with it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "create-gateway", + "metadata": {}, + "outputs": [], + "source": [ + "# Create an AgentCore Gateway\n", + "agentcore_control_client = boto3.client(\"bedrock-agentcore-control\", region_name=AWS_REGION)\n", + "\n", + "GATEWAY_NAME = \"agentcore-travel-gateway\"\n", + "GATEWAY_ROLE_NAME = \"agentcore-travel-gateway-role\"\n", + "\n", + "# Create IAM role for the gateway\n", + "gateway_trust_policy = {\n", + " \"Version\": \"2012-10-17\",\n", + " \"Statement\": [\n", + " {\n", + " \"Effect\": \"Allow\",\n", + " \"Principal\": {\"Service\": \"bedrock-agentcore.amazonaws.com\"},\n", + " \"Action\": \"sts:AssumeRole\"\n", + " }\n", + " ]\n", + "}\n", + "\n", + "try:\n", + " gw_role_response = iam.create_role(\n", + " RoleName=GATEWAY_ROLE_NAME,\n", + " AssumeRolePolicyDocument=json.dumps(gateway_trust_policy),\n", + " Description=\"Role for AgentCore Gateway to invoke Lambda targets\"\n", + " )\n", + " gateway_role_arn = gw_role_response[\"Role\"][\"Arn\"]\n", + " # Allow the gateway to invoke Lambda functions\n", + " iam.attach_role_policy(\n", + " RoleName=GATEWAY_ROLE_NAME,\n", + " PolicyArn=\"arn:aws:iam::aws:policy/service-role/AWSLambdaRole\"\n", + " )\n", + " print(f\"[OK] Created gateway role: {GATEWAY_ROLE_NAME}\")\n", + " time.sleep(10)\n", + "except iam.exceptions.EntityAlreadyExistsException:\n", + " gateway_role_arn = f\"arn:aws:iam::{identity['Account']}:role/{GATEWAY_ROLE_NAME}\"\n", + " print(f\"[INFO] Gateway role already exists: {GATEWAY_ROLE_NAME}\")\n", + "\n", + "# Create the gateway\n", + "try:\n", + " gateway_response = agentcore_control_client.create_gateway(\n", + " name=GATEWAY_NAME,\n", + " roleArn=gateway_role_arn,\n", + " authorizerType=\"AWS_IAM\",\n", + " protocolType=\"MCP\",\n", + " protocolConfiguration={\n", + " \"mcp\": {\n", + " \"searchType\": \"SEMANTIC\"\n", + " }\n", + " },\n", + " description=\"Travel domain gateway for semantic tool search demo\"\n", + " )\n", + " gateway_id = gateway_response[\"gatewayId\"]\n", + " GATEWAY_ENDPOINT = gateway_response[\"gatewayUrl\"]\n", + " print(f\"[OK] AgentCore Gateway created: {GATEWAY_NAME}\")\n", + " print(f\" Gateway ID: {gateway_id}\")\n", + " print(f\" Endpoint: {GATEWAY_ENDPOINT}\")\n", + "except Exception as e:\n", + " if \"already exists\" in str(e).lower() or \"conflict\" in str(e).lower():\n", + " gateways = agentcore_control_client.list_gateways()\n", + " for gw in gateways.get(\"items\", []):\n", + " if gw.get(\"name\") == GATEWAY_NAME:\n", + " gateway_id = gw[\"gatewayId\"]\n", + " gw_detail = agentcore_control_client.get_gateway(gatewayIdentifier=gateway_id)\n", + " GATEWAY_ENDPOINT = gw_detail[\"gatewayUrl\"]\n", + " break\n", + " print(f\"[INFO] Gateway already exists: {GATEWAY_NAME}\")\n", + " print(f\" Gateway ID: {gateway_id}\")\n", + " print(f\" Endpoint: {GATEWAY_ENDPOINT}\")\n", + " else:\n", + " print(f\"[ERROR] Failed to create gateway: {e}\")\n", + " print(\"\\nTroubleshooting:\")\n", + " print(\" 1. Ensure your IAM identity has bedrock-agentcore-control permissions\")\n", + " print(\" 2. Check that AgentCore Gateway is available in your region\")\n", + " raise\n" + ] + }, + { + "cell_type": "markdown", + "id": "register-lambda-intro", + "metadata": {}, + "source": [ + "### Register Lambda as Tool Target\n", + "\n", + "Now we register our Lambda function with the gateway as a tool target. This makes the Lambda's tools discoverable by any agent that connects to this gateway via MCP." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "register-lambda-gateway", + "metadata": {}, + "outputs": [], + "source": [ + "# Load tool schemas from file\n", + "with open(\"lambda/tool_schemas.json\", \"r\") as f:\n", + " tool_schemas = json.load(f)\n", + "\n", + "print(f\"Loaded {len(tool_schemas)} tool schemas from lambda/tool_schemas.json\")\n", + "\n", + "try:\n", + " register_response = agentcore_control_client.create_gateway_target(\n", + " gatewayIdentifier=gateway_id,\n", + " name=LAMBDA_FUNCTION_NAME,\n", + " targetConfiguration={\n", + " \"mcp\": {\n", + " \"lambda\": {\n", + " \"lambdaArn\": lambda_arn,\n", + " \"toolSchema\": {\n", + " \"inlinePayload\": tool_schemas\n", + " }\n", + " }\n", + " }\n", + " },\n", + " credentialProviderConfigurations=[\n", + " {\n", + " \"credentialProviderType\": \"GATEWAY_IAM_ROLE\"\n", + " }\n", + " ],\n", + " description=\"Travel domain tools - flights, hotels, car rentals, restaurants, currency, loyalty, weather, activities, trip planning\"\n", + " )\n", + " target_id = register_response.get(\"targetId\", \"registered\")\n", + " print(f\"[OK] Lambda registered with AgentCore Gateway\")\n", + " print(f\" Target ID: {target_id}\")\n", + " print(f\" Tools registered: {len(tool_schemas)}\")\n", + "except Exception as e:\n", + " if \"already exists\" in str(e).lower() or \"conflict\" in str(e).lower():\n", + " print(f\"[INFO] Tool target already registered: {LAMBDA_FUNCTION_NAME}\")\n", + " print(f\" Continuing with existing registration...\")\n", + " else:\n", + " print(f\"[ERROR] Registration failed: {e}\")\n", + " print(\"\\nTroubleshooting:\")\n", + " print(\" 1. Verify the gateway was created successfully\")\n", + " print(\" 2. Ensure your IAM identity has bedrock-agentcore-control permissions\")\n", + " print(\" 3. Verify the Lambda ARN is correct\")\n", + " raise\n" + ] + }, + { + "cell_type": "markdown", + "id": "verify-registration-intro", + "metadata": {}, + "source": [ + "### Verify Registration\n", + "\n", + "Let's verify the registration succeeded by listing all tool targets registered with the gateway. Our Lambda function should appear in the list." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "verify-gateway-registration", + "metadata": {}, + "outputs": [], + "source": [ + "# Wait for registration to propagate, then verify\n", + "print(\"Waiting for target registration to propagate...\")\n", + "time.sleep(5)\n", + "\n", + "try:\n", + " tools_response = agentcore_control_client.list_gateway_targets(\n", + " gatewayIdentifier=gateway_id\n", + " )\n", + " targets = tools_response.get(\"items\", [])\n", + " print(f\"[OK] Gateway has {len(targets)} registered target(s):\")\n", + " for target in targets:\n", + " print(f\" - {target.get('name', 'unknown')}: {target.get('description', 'No description')[:60]}\")\n", + "except Exception as e:\n", + " print(f\"[ERROR] Failed to list gateway targets: {e}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "agent-creation-header", + "metadata": {}, + "source": [ + "## 6. Create the Strands Agent\n", + "\n", + "Now that our tools are registered with AgentCore Gateway, we'll create a Strands Agent that can discover and invoke them. This involves two components:\n", + "\n", + "1. **MCPClient** - Connects to AgentCore Gateway using IAM-authenticated Streamable HTTP transport\n", + "2. **AgentCoreToolSearchPlugin** - Uses the gateway's built-in `x_amz_bedrock_agentcore_search` tool to semantically discover relevant tools before each model invocation\n", + "\n", + "The plugin hooks into the agent lifecycle: on each invocation, it derives user intent from conversation history, searches the gateway for matching tools, and loads only those tools for the model call. Previously loaded tools are cleared before each search, so the agent always has the most relevant set." + ] + }, + { + "cell_type": "markdown", + "id": "mcp-client-intro", + "metadata": {}, + "source": [ + "### MCPClient\n", + "\n", + "The `MCPClient` connects to AgentCore Gateway using **IAM-authenticated Streamable HTTP** transport. This is how the agent communicates with the gateway to discover and invoke tools. The connection uses AWS Signature V4 for authentication against the `bedrock-agentcore` service." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "create-mcp-client", + "metadata": {}, + "outputs": [], + "source": [ + "from strands.tools.mcp import MCPClient\n", + "from mcp_proxy_for_aws.client import aws_iam_streamablehttp_client\n", + "\n", + "# Create MCP client connected to AgentCore Gateway\n", + "mcp_client = MCPClient(\n", + " lambda: aws_iam_streamablehttp_client(\n", + " endpoint=GATEWAY_ENDPOINT,\n", + " aws_region=AWS_REGION,\n", + " aws_service=\"bedrock-agentcore\"\n", + " )\n", + ")\n", + "\n", + "mcp_client.start()\n", + "print(\"[OK] MCPClient connected to AgentCore Gateway\")\n", + "print(f\" Endpoint: {GATEWAY_ENDPOINT}\")\n", + "print(f\" Region: {AWS_REGION}\")" + ] + }, + { + "cell_type": "markdown", + "id": "tool-search-plugin-intro", + "metadata": {}, + "source": [ + "### AgentCoreToolSearchPlugin\n", + "\n", + "The `AgentCoreToolSearchPlugin` is powered by AgentCore Gateway's built-in **semantic search** capability (`x_amz_bedrock_agentcore_search` tool). On each agent invocation, the plugin:\n", + "\n", + "1. **Derives intent** - An `IntentProvider` sends the last N messages from conversation history to an LLM to produce a concise intent string\n", + "2. **Searches the gateway** - The intent is passed to the gateway's search tool to find matching tools from all registered targets\n", + "3. **Loads tools** - Only the matched tools are loaded for that model call (previously loaded tools are cleared)\n", + "\n", + "By default, the intent classifier reuses the parent agent's model, so no separate model configuration is needed. The result: the agent always has a focused, relevant subset of tools for each request, even when hundreds of tools are registered on the gateway." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "create-agent", + "metadata": {}, + "outputs": [], + "source": [ + "from strands import Agent\n", + "from bedrock_agentcore.gateway.integrations.strands.plugins import AgentCoreToolSearchPlugin\n", + "\n", + "# Create the agent with the AgentCore Tool Search Plugin\n", + "# The plugin uses the gateway's built-in semantic search to find relevant tools\n", + "# By default, intent classification reuses the agent's own model\n", + "agent = Agent(plugins=[\n", + " AgentCoreToolSearchPlugin(\n", + " mcp_client=mcp_client\n", + " )\n", + " ]\n", + ")\n", + "\n", + "print(\"[OK] Strands Agent created with AgentCoreToolSearchPlugin\")\n", + "print(\" Semantic tool search powered by AgentCore Gateway\")\n", + "print(\" Intent classification reuses the agent model\")\n", + "print(\" Tools loaded dynamically before each model invocation\")" + ] + }, + { + "cell_type": "markdown", + "id": "plugin-customization", + "metadata": {}, + "source": [ + "### Customization Options\n", + "\n", + "The `AgentCoreToolSearchPlugin` supports several customization options:\n", + "\n", + "- **Custom model for intent classification** - Use a faster/cheaper model (e.g., Claude Haiku) via `StrandsIntentProvider(model=...)`\n", + "- **Custom system prompt** - Control how intent is derived from conversation history\n", + "- **Custom intent provider** - Subclass `IntentProvider` to implement your own intent derivation logic\n", + "\n", + "For this tutorial, we use the default behavior (agent's own model for intent classification). See the [plugin source](https://github.com/aws/bedrock-agentcore-sdk-python/blob/main/src/bedrock_agentcore/gateway/integrations/strands/plugins/agentcore_tool_search) for full documentation and examples." + ] + }, + { + "cell_type": "markdown", + "id": "invocation-examples-header", + "metadata": {}, + "source": [ + "## 7. Agent Invocation Examples\n", + "\n", + "Now that our agent is assembled, let's invoke it across multiple travel domains to see semantic tool search in action. After each invocation, we log the tools that the plugin loaded for that request - demonstrating how the gateway's `x_amz_bedrock_agentcore_search` tool finds only the relevant tools just before the model call, rather than loading all 34 tools every time.\n", + "\n", + "This is the key benefit: each request gets a focused, relevant subset of tools based on the derived user intent." + ] + }, + { + "cell_type": "markdown", + "id": "flight-search-intro", + "metadata": {}, + "source": [ + "### Flight Search\n", + "\n", + "Let's start by asking the agent to find flights. The `ToolSearchPlugin` will classify this as a flight-related intent and load only the flight domain tools (search_flights, get_flight_details, check_availability, etc.) from the gateway." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "invoke-flight-search", + "metadata": {}, + "outputs": [], + "source": [ + "# Invoke the agent\n", + "response = agent(\"Find flights from San Francisco to New York next Friday\")\n", + "\n", + "# Log tools that were loaded by the plugin for this invocation\n", + "print(\"Tools selected by semantic search for this request:\")\n", + "tools_config = agent.tool_registry.get_all_tools_config()\n", + "for tool_name in sorted(tools_config.keys()):\n", + " print(f\" - {tool_name}\")\n", + "print()\n", + "print(\"Agent Response:\")\n", + "print(response)\n" + ] + }, + { + "cell_type": "markdown", + "id": "hotel-search-intro", + "metadata": {}, + "source": [ + "### Hotel Search\n", + "\n", + "Now let's switch to a completely different domain - hotels. Notice how the `ToolSearchPlugin` reclassifies the intent and selects hotel-related tools (search_hotels, get_hotel_details, check_room_availability, get_hotel_amenities) instead of flight tools." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "invoke-hotel-search", + "metadata": {}, + "outputs": [], + "source": [ + "# Invoke the agent\n", + "response = agent(\"Search for hotels in Manhattan with a pool\")\n", + "\n", + "# Log tools that were loaded by the plugin for this invocation\n", + "print(\"Tools selected by semantic search for this request:\")\n", + "tools_config = agent.tool_registry.get_all_tools_config()\n", + "for tool_name in sorted(tools_config.keys()):\n", + " print(f\" - {tool_name}\")\n", + "print()\n", + "print(\"Agent Response:\")\n", + "print(response)\n" + ] + }, + { + "cell_type": "markdown", + "id": "car-rental-intro", + "metadata": {}, + "source": [ + "### Car Rental\n", + "\n", + "Next, let's search for car rentals. The plugin will identify this as a car rental intent and load the relevant tools (search_car_rentals, get_rental_details, check_car_availability) from the gateway." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "invoke-car-rental", + "metadata": {}, + "outputs": [], + "source": [ + "# Invoke the agent\n", + "response = agent(\"Find available car rentals at JFK airport for next week\")\n", + "\n", + "# Log tools that were loaded by the plugin for this invocation\n", + "print(\"Tools selected by semantic search for this request:\")\n", + "tools_config = agent.tool_registry.get_all_tools_config()\n", + "for tool_name in sorted(tools_config.keys()):\n", + " print(f\" - {tool_name}\")\n", + "print()\n", + "print(\"Agent Response:\")\n", + "print(response)\n" + ] + }, + { + "cell_type": "markdown", + "id": "restaurant-search-intro", + "metadata": {}, + "source": [ + "### Restaurant Search\n", + "\n", + "Let's try the restaurant domain. The plugin will select restaurant tools (search_restaurants, get_restaurant_details, get_menu, check_reservations, get_restaurant_reviews) for this request." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "invoke-restaurant-search", + "metadata": {}, + "outputs": [], + "source": [ + "# Invoke the agent\n", + "response = agent(\"Find Italian restaurants near Times Square with good reviews\")\n", + "\n", + "# Log tools that were loaded by the plugin for this invocation\n", + "print(\"Tools selected by semantic search for this request:\")\n", + "tools_config = agent.tool_registry.get_all_tools_config()\n", + "for tool_name in sorted(tools_config.keys()):\n", + " print(f\" - {tool_name}\")\n", + "print()\n", + "print(\"Agent Response:\")\n", + "print(response)\n" + ] + }, + { + "cell_type": "markdown", + "id": "currency-conversion-intro", + "metadata": {}, + "source": [ + "### Currency Conversion\n", + "\n", + "Now let's try the currency domain. The plugin will identify this as a currency-related intent and load the currency tools (convert_currency, get_exchange_rates, get_supported_currencies)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "invoke-currency-conversion", + "metadata": {}, + "outputs": [], + "source": [ + "# Invoke the agent\n", + "response = agent(\"Convert 500 USD to EUR and show me the current exchange rate\")\n", + "\n", + "# Log tools that were loaded by the plugin for this invocation\n", + "print(\"Tools selected by semantic search for this request:\")\n", + "tools_config = agent.tool_registry.get_all_tools_config()\n", + "for tool_name in sorted(tools_config.keys()):\n", + " print(f\" - {tool_name}\")\n", + "print()\n", + "print(\"Agent Response:\")\n", + "print(response)\n" + ] + }, + { + "cell_type": "markdown", + "id": "loyalty-program-intro", + "metadata": {}, + "source": [ + "### Loyalty Program\n", + "\n", + "Finally, let's check the loyalty program domain. The plugin will select loyalty tools (get_loyalty_balance, redeem_points, get_loyalty_program_info) for this request." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "invoke-loyalty-program", + "metadata": {}, + "outputs": [], + "source": [ + "# Invoke the agent\n", + "response = agent(\"Check my loyalty points balance and what rewards I can redeem\")\n", + "\n", + "# Log tools that were loaded by the plugin for this invocation\n", + "print(\"Tools selected by semantic search for this request:\")\n", + "tools_config = agent.tool_registry.get_all_tools_config()\n", + "for tool_name in sorted(tools_config.keys()):\n", + " print(f\" - {tool_name}\")\n", + "print()\n", + "print(\"Agent Response:\")\n", + "print(response)\n" + ] + }, + { + "cell_type": "markdown", + "id": "cleanup-header", + "metadata": {}, + "source": [ + "## 8. Cleanup\n", + "\n", + "We need to delete all resources created during this tutorial to avoid ongoing costs. Each cleanup step is independent - if one fails, others will still execute." + ] + }, + { + "cell_type": "markdown", + "id": "cleanup-explanation", + "metadata": {}, + "source": [ + "### Remove All Created Resources\n", + "\n", + "We'll stop the MCP client connection and clean up all AWS resources created in this notebook: the Lambda function, its IAM execution role, and the AgentCore Gateway tool target registration." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cleanup-resources", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Cleaning up resources...\")\n", + "print()\n", + "\n", + "# 1. Stop MCP Client\n", + "try:\n", + " mcp_client.__exit__(None, None, None)\n", + " print(\"[OK] MCPClient connection stopped\")\n", + "except Exception as e:\n", + " print(f\"[WARNING] Error stopping MCPClient: {e}\")\n", + "\n", + "# 2. Deregister from AgentCore Gateway\n", + "try:\n", + " agentcore_control_client.delete_gateway_target(\n", + " gatewayIdentifier=gateway_id,\n", + " targetId=target_id\n", + " )\n", + " print(\"[OK] Deregistered tool target from gateway\")\n", + "except Exception as e:\n", + " print(f\"[WARNING] Error deregistering from gateway: {e}\")\n", + "\n", + "# 3. Delete the AgentCore Gateway (wait for target deletion to propagate)\n", + "import time\n", + "time.sleep(10)\n", + "try:\n", + " agentcore_control_client.delete_gateway(gatewayIdentifier=gateway_id)\n", + " print(f\"[OK] Deleted AgentCore Gateway: {GATEWAY_NAME}\")\n", + "except Exception as e:\n", + " print(f\"[WARNING] Error deleting gateway: {e}\")\n", + "\n", + "# 4. Delete Lambda function\n", + "try:\n", + " lambda_client.delete_function(FunctionName=LAMBDA_FUNCTION_NAME)\n", + " print(f\"[OK] Deleted Lambda function: {LAMBDA_FUNCTION_NAME}\")\n", + "except Exception as e:\n", + " print(f\"[WARNING] Error deleting Lambda function: {e}\")\n", + "\n", + "# 5. Detach policy and delete Lambda IAM role\n", + "try:\n", + " iam.detach_role_policy(\n", + " RoleName=LAMBDA_ROLE_NAME,\n", + " PolicyArn=\"arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole\"\n", + " )\n", + " iam.delete_role(RoleName=LAMBDA_ROLE_NAME)\n", + " print(f\"[OK] Deleted IAM role: {LAMBDA_ROLE_NAME}\")\n", + "except Exception as e:\n", + " print(f\"[WARNING] Error deleting Lambda IAM role: {e}\")\n", + "\n", + "# 6. Detach policy and delete Gateway IAM role\n", + "try:\n", + " iam.detach_role_policy(\n", + " RoleName=GATEWAY_ROLE_NAME,\n", + " PolicyArn=\"arn:aws:iam::aws:policy/service-role/AWSLambdaRole\"\n", + " )\n", + " iam.delete_role(RoleName=GATEWAY_ROLE_NAME)\n", + " print(f\"[OK] Deleted IAM role: {GATEWAY_ROLE_NAME}\")\n", + "except Exception as e:\n", + " print(f\"[WARNING] Error deleting Gateway IAM role: {e}\")\n", + "\n", + "print()\n", + "print(\"Cleanup complete! All resources have been removed.\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "cleanup-summary", + "metadata": {}, + "source": [ + "### Conclusion\n", + "\n", + "In this notebook you built a Strands agent that uses the `AgentCoreToolSearchPlugin` to leverage AgentCore Gateway's semantic tool search. The plugin dynamically discovers and loads only the relevant tools before each model invocation - enabling efficient tool selection from 34 registered options without overwhelming the agent.\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/03-integrations/gateway/agentcore-tool-search-plugin/lambda/tool_schemas.json b/03-integrations/gateway/agentcore-tool-search-plugin/lambda/tool_schemas.json new file mode 100644 index 000000000..abf1db477 --- /dev/null +++ b/03-integrations/gateway/agentcore-tool-search-plugin/lambda/tool_schemas.json @@ -0,0 +1,493 @@ +[ + { + "name": "search_flights", + "description": "Search for available flights based on origin, destination, and date", + "inputSchema": { + "type": "object", + "properties": { + "origin": { + "type": "string" + }, + "destination": { + "type": "string" + }, + "date": { + "type": "string" + } + } + } + }, + { + "name": "get_flight_details", + "description": "Get detailed information about a specific flight", + "inputSchema": { + "type": "object", + "properties": { + "flight_id": { + "type": "string" + } + } + } + }, + { + "name": "check_availability", + "description": "Check seat availability for a specific flight", + "inputSchema": { + "type": "object", + "properties": { + "flight_id": { + "type": "string" + }, + "class": { + "type": "string" + } + } + } + }, + { + "name": "get_seat_map", + "description": "Get the seat map for a specific flight", + "inputSchema": { + "type": "object", + "properties": { + "flight_id": { + "type": "string" + } + } + } + }, + { + "name": "get_baggage_policy", + "description": "Get baggage policy for a specific airline", + "inputSchema": { + "type": "object", + "properties": { + "airline": { + "type": "string" + } + } + } + }, + { + "name": "get_flight_status", + "description": "Get real-time status of a specific flight", + "inputSchema": { + "type": "object", + "properties": { + "flight_id": { + "type": "string" + } + } + } + }, + { + "name": "search_connecting_flights", + "description": "Search for connecting flight options between two cities", + "inputSchema": { + "type": "object", + "properties": { + "origin": { + "type": "string" + }, + "destination": { + "type": "string" + }, + "date": { + "type": "string" + } + } + } + }, + { + "name": "get_airline_info", + "description": "Get information about a specific airline", + "inputSchema": { + "type": "object", + "properties": { + "airline": { + "type": "string" + } + } + } + }, + { + "name": "compare_flight_prices", + "description": "Compare prices for the same route across multiple airlines", + "inputSchema": { + "type": "object", + "properties": { + "origin": { + "type": "string" + }, + "destination": { + "type": "string" + }, + "date": { + "type": "string" + } + } + } + }, + { + "name": "search_hotels", + "description": "Search for hotels in a specified location", + "inputSchema": { + "type": "object", + "properties": { + "location": { + "type": "string" + }, + "check_in": { + "type": "string" + }, + "check_out": { + "type": "string" + } + } + } + }, + { + "name": "get_hotel_details", + "description": "Get detailed information about a specific hotel", + "inputSchema": { + "type": "object", + "properties": { + "hotel_id": { + "type": "string" + } + } + } + }, + { + "name": "check_room_availability", + "description": "Check room availability for specific dates", + "inputSchema": { + "type": "object", + "properties": { + "hotel_id": { + "type": "string" + }, + "check_in": { + "type": "string" + }, + "check_out": { + "type": "string" + } + } + } + }, + { + "name": "get_hotel_amenities", + "description": "Get detailed amenity information for a hotel", + "inputSchema": { + "type": "object", + "properties": { + "hotel_id": { + "type": "string" + } + } + } + }, + { + "name": "search_car_rentals", + "description": "Search for available car rentals at a location", + "inputSchema": { + "type": "object", + "properties": { + "location": { + "type": "string" + }, + "pickup_date": { + "type": "string" + }, + "return_date": { + "type": "string" + } + } + } + }, + { + "name": "get_rental_details", + "description": "Get detailed information about a specific car rental", + "inputSchema": { + "type": "object", + "properties": { + "rental_id": { + "type": "string" + } + } + } + }, + { + "name": "check_car_availability", + "description": "Check availability of a specific car rental", + "inputSchema": { + "type": "object", + "properties": { + "rental_id": { + "type": "string" + }, + "pickup_date": { + "type": "string" + }, + "return_date": { + "type": "string" + } + } + } + }, + { + "name": "search_restaurants", + "description": "Search for restaurants in a specified area", + "inputSchema": { + "type": "object", + "properties": { + "location": { + "type": "string" + }, + "cuisine": { + "type": "string" + } + } + } + }, + { + "name": "get_restaurant_details", + "description": "Get detailed information about a specific restaurant", + "inputSchema": { + "type": "object", + "properties": { + "restaurant_id": { + "type": "string" + } + } + } + }, + { + "name": "get_menu", + "description": "Get the menu for a specific restaurant", + "inputSchema": { + "type": "object", + "properties": { + "restaurant_id": { + "type": "string" + } + } + } + }, + { + "name": "check_reservations", + "description": "Check reservation availability at a restaurant", + "inputSchema": { + "type": "object", + "properties": { + "restaurant_id": { + "type": "string" + }, + "date": { + "type": "string" + }, + "party_size": { + "type": "integer" + } + } + } + }, + { + "name": "get_restaurant_reviews", + "description": "Get reviews for a specific restaurant", + "inputSchema": { + "type": "object", + "properties": { + "restaurant_id": { + "type": "string" + } + } + } + }, + { + "name": "convert_currency", + "description": "Convert an amount from one currency to another", + "inputSchema": { + "type": "object", + "properties": { + "amount": { + "type": "number" + }, + "from_currency": { + "type": "string" + }, + "to_currency": { + "type": "string" + } + } + } + }, + { + "name": "get_exchange_rates", + "description": "Get current exchange rates for a base currency", + "inputSchema": { + "type": "object", + "properties": { + "base_currency": { + "type": "string" + } + } + } + }, + { + "name": "get_supported_currencies", + "description": "Get list of supported currencies for conversion", + "inputSchema": { + "type": "object", + "properties": {} + } + }, + { + "name": "get_loyalty_balance", + "description": "Get loyalty program points balance for a member", + "inputSchema": { + "type": "object", + "properties": { + "member_id": { + "type": "string" + }, + "program": { + "type": "string" + } + } + } + }, + { + "name": "redeem_points", + "description": "Redeem loyalty points for rewards", + "inputSchema": { + "type": "object", + "properties": { + "member_id": { + "type": "string" + }, + "points": { + "type": "integer" + }, + "reward_type": { + "type": "string" + } + } + } + }, + { + "name": "get_loyalty_program_info", + "description": "Get information about a loyalty program", + "inputSchema": { + "type": "object", + "properties": { + "program": { + "type": "string" + } + } + } + }, + { + "name": "get_weather_forecast", + "description": "Get weather forecast for a location", + "inputSchema": { + "type": "object", + "properties": { + "location": { + "type": "string" + }, + "days": { + "type": "integer" + } + } + } + }, + { + "name": "get_current_weather", + "description": "Get current weather conditions for a location", + "inputSchema": { + "type": "object", + "properties": { + "location": { + "type": "string" + } + } + } + }, + { + "name": "search_activities", + "description": "Search for activities and attractions in a location", + "inputSchema": { + "type": "object", + "properties": { + "location": { + "type": "string" + }, + "category": { + "type": "string" + } + } + } + }, + { + "name": "get_activity_details", + "description": "Get detailed information about a specific activity", + "inputSchema": { + "type": "object", + "properties": { + "activity_id": { + "type": "string" + } + } + } + }, + { + "name": "check_activity_availability", + "description": "Check availability for a specific activity on a date", + "inputSchema": { + "type": "object", + "properties": { + "activity_id": { + "type": "string" + }, + "date": { + "type": "string" + }, + "participants": { + "type": "integer" + } + } + } + }, + { + "name": "create_itinerary", + "description": "Create a trip itinerary based on destination and preferences", + "inputSchema": { + "type": "object", + "properties": { + "destination": { + "type": "string" + }, + "days": { + "type": "integer" + } + } + } + }, + { + "name": "get_travel_tips", + "description": "Get travel tips and recommendations for a destination", + "inputSchema": { + "type": "object", + "properties": { + "destination": { + "type": "string" + }, + "category": { + "type": "string" + } + } + } + } +] \ No newline at end of file diff --git a/03-integrations/gateway/agentcore-tool-search-plugin/lambda/travel_tools.py b/03-integrations/gateway/agentcore-tool-search-plugin/lambda/travel_tools.py new file mode 100644 index 000000000..74a539bbe --- /dev/null +++ b/03-integrations/gateway/agentcore-tool-search-plugin/lambda/travel_tools.py @@ -0,0 +1,1226 @@ +""" +Travel Tools Lambda Function for Amazon Bedrock AgentCore Gateway + +This is a plain AWS Lambda function that serves as a tool target for AgentCore Gateway. +It does NOT implement the MCP protocol itself — AgentCore Gateway handles MCP protocol +translation and routes tool calls to this function. + +Input format from AgentCore Gateway: +- Event: A map of tool input properties (e.g., {"destination": "New York", "date": "2025-03-15"}) +- Context: Contains metadata including the tool name in context.client_context.custom['bedrockAgentCoreToolName'] + The tool name format is: ${target_name}___${tool_name} + +The function extracts the tool name from context, strips the target prefix, dispatches to +the appropriate domain handler, and returns a JSON response. + +All data is static/in-memory for demonstration purposes. + +Domains covered: +- Flights (9 tools): search, details, availability, seat map, baggage, status, connections, airline info, price comparison +- Hotels (4 tools): search, details, room availability, amenities +- Car Rentals (3 tools): search, details, availability +- Restaurants (5 tools): search, details, menu, reservations, reviews +- Currency (3 tools): convert, exchange rates, supported currencies +- Loyalty (3 tools): balance, redeem, program info +- Weather (2 tools): forecast, current +- Activities (3 tools): search, details, availability +- Trip Planning (2 tools): create itinerary, travel tips +""" + +import json + + +DELIMITER = "___" + + +def lambda_handler(event, context): + """ + Main Lambda handler. Receives tool invocations from AgentCore Gateway. + + Event: Tool input properties as a flat dict (e.g., {"destination": "New York"}) + Context: client_context.custom contains bedrockAgentCoreToolName with format target___toolname + + Returns a JSON-serializable result (the gateway handles response formatting). + """ + # Extract tool name from context (format: targetName___toolName) + tool_name = "" + try: + original_tool_name = context.client_context.custom['bedrockAgentCoreToolName'] + tool_name = original_tool_name[original_tool_name.index(DELIMITER) + len(DELIMITER):] + except (AttributeError, KeyError, ValueError): + # Fallback: check if tool_name is passed directly in event (for local testing) + tool_name = event.pop("tool_name", "") + + # The event IS the tool arguments (flat dict of input properties) + arguments = event + + # Dispatch to the appropriate tool handler + handler = TOOL_DISPATCH.get(tool_name) + + if handler is None: + return { + "error": f"Unknown tool: {tool_name}", + "available_tools": list(TOOL_DISPATCH.keys()) + } + + result = handler(arguments) + return result + + +# ============================================================================= +# FLIGHTS DOMAIN (9 tools) +# ============================================================================= + +def search_flights(args): + """Search for available flights based on origin, destination, and date.""" + destination = args.get("destination", "New York") + origin = args.get("origin", "San Francisco") + date = args.get("date", "2025-03-15") + + return { + "flights": [ + { + "flight_id": "FL-201", + "airline": "SkyWest Airlines", + "origin": origin, + "destination": destination, + "departure_time": f"{date}T08:00:00", + "arrival_time": f"{date}T16:30:00", + "duration": "5h 30m", + "price": 349.99, + "currency": "USD", + "class": "Economy", + "stops": 0 + }, + { + "flight_id": "FL-202", + "airline": "Pacific Air", + "origin": origin, + "destination": destination, + "departure_time": f"{date}T11:45:00", + "arrival_time": f"{date}T20:00:00", + "duration": "5h 15m", + "price": 289.50, + "currency": "USD", + "class": "Economy", + "stops": 0 + }, + { + "flight_id": "FL-203", + "airline": "Continental Express", + "origin": origin, + "destination": destination, + "departure_time": f"{date}T14:30:00", + "arrival_time": f"{date}T23:00:00", + "duration": "5h 30m", + "price": 415.00, + "currency": "USD", + "class": "Business", + "stops": 0 + } + ], + "total_results": 3, + "search_criteria": {"origin": origin, "destination": destination, "date": date} + } + + +def get_flight_details(args): + """Get detailed information about a specific flight.""" + flight_id = args.get("flight_id", "FL-201") + + return { + "flight_id": flight_id, + "airline": "SkyWest Airlines", + "flight_number": "SW-1042", + "origin": {"code": "SFO", "name": "San Francisco International", "terminal": "2"}, + "destination": {"code": "JFK", "name": "John F. Kennedy International", "terminal": "4"}, + "departure_time": "2025-03-15T08:00:00", + "arrival_time": "2025-03-15T16:30:00", + "duration": "5h 30m", + "aircraft": "Boeing 737-800", + "amenities": ["Wi-Fi", "In-flight entertainment", "USB charging", "Complimentary snacks"], + "price": {"economy": 349.99, "business": 799.99, "first": 1249.99}, + "currency": "USD" + } + + +def check_availability(args): + """Check seat availability for a specific flight.""" + flight_id = args.get("flight_id", "FL-201") + travel_class = args.get("class", "Economy") + + return { + "flight_id": flight_id, + "class": travel_class, + "available_seats": 42, + "total_seats": 180, + "availability_status": "available", + "fare": 349.99, + "currency": "USD", + "last_updated": "2025-03-10T12:00:00Z" + } + + +def get_seat_map(args): + """Get the seat map for a specific flight.""" + flight_id = args.get("flight_id", "FL-201") + + return { + "flight_id": flight_id, + "aircraft": "Boeing 737-800", + "sections": [ + { + "class": "First", + "rows": "1-4", + "seats_per_row": 4, + "layout": "2-2", + "available": 3 + }, + { + "class": "Business", + "rows": "5-10", + "seats_per_row": 6, + "layout": "3-3", + "available": 12 + }, + { + "class": "Economy", + "rows": "11-35", + "seats_per_row": 6, + "layout": "3-3", + "available": 42 + } + ], + "exit_rows": [12, 22], + "extra_legroom_rows": [12, 13, 22, 23] + } + + +def get_baggage_policy(args): + """Get baggage policy for a specific airline or flight.""" + airline = args.get("airline", "SkyWest Airlines") + + return { + "airline": airline, + "carry_on": { + "allowed": True, + "max_weight_kg": 10, + "max_dimensions_cm": "55x40x20", + "personal_item": True + }, + "checked_baggage": { + "first_bag_fee": 30.00, + "second_bag_fee": 45.00, + "max_weight_kg": 23, + "max_dimensions_cm": "158 linear cm" + }, + "overweight_fee": 100.00, + "oversize_fee": 150.00, + "special_items": { + "sports_equipment": 75.00, + "musical_instruments": "Carry-on or purchased seat", + "pets": 125.00 + }, + "currency": "USD" + } + + +def get_flight_status(args): + """Get real-time status of a specific flight.""" + flight_id = args.get("flight_id", "FL-201") + + return { + "flight_id": flight_id, + "flight_number": "SW-1042", + "status": "On Time", + "departure": { + "airport": "SFO", + "scheduled": "2025-03-15T08:00:00", + "estimated": "2025-03-15T08:00:00", + "gate": "B22", + "terminal": "2" + }, + "arrival": { + "airport": "JFK", + "scheduled": "2025-03-15T16:30:00", + "estimated": "2025-03-15T16:25:00", + "gate": "A15", + "terminal": "4" + }, + "last_updated": "2025-03-15T07:30:00Z" + } + + +def search_connecting_flights(args): + """Search for connecting flight options between two cities.""" + origin = args.get("origin", "San Francisco") + destination = args.get("destination", "London") + date = args.get("date", "2025-03-15") + + return { + "connections": [ + { + "route_id": "RT-101", + "total_duration": "14h 45m", + "total_price": 689.99, + "legs": [ + { + "flight_id": "FL-301", + "origin": origin, + "destination": "New York (JFK)", + "departure": f"{date}T06:00:00", + "arrival": f"{date}T14:30:00", + "airline": "SkyWest Airlines" + }, + { + "flight_id": "FL-302", + "origin": "New York (JFK)", + "destination": destination, + "departure": f"{date}T17:00:00", + "arrival": "2025-03-16T05:45:00", + "airline": "Atlantic Airways" + } + ], + "layover": "2h 30m at JFK" + }, + { + "route_id": "RT-102", + "total_duration": "13h 20m", + "total_price": 825.00, + "legs": [ + { + "flight_id": "FL-303", + "origin": origin, + "destination": "Chicago (ORD)", + "departure": f"{date}T07:30:00", + "arrival": f"{date}T13:30:00", + "airline": "Continental Express" + }, + { + "flight_id": "FL-304", + "origin": "Chicago (ORD)", + "destination": destination, + "departure": f"{date}T15:00:00", + "arrival": "2025-03-16T04:50:00", + "airline": "Atlantic Airways" + } + ], + "layover": "1h 30m at ORD" + } + ], + "search_criteria": {"origin": origin, "destination": destination, "date": date}, + "currency": "USD" + } + + +def get_airline_info(args): + """Get information about a specific airline.""" + airline = args.get("airline", "SkyWest Airlines") + + airlines_data = { + "SkyWest Airlines": { + "name": "SkyWest Airlines", + "code": "SW", + "country": "United States", + "hub_airports": ["SFO", "LAX", "DEN"], + "alliance": "Star Alliance", + "fleet_size": 245, + "founded": 1972, + "website": "https://www.skywest-example.com", + "rating": 4.2, + "on_time_performance": "82%" + }, + "Pacific Air": { + "name": "Pacific Air", + "code": "PA", + "country": "United States", + "hub_airports": ["LAX", "HNL"], + "alliance": "OneWorld", + "fleet_size": 180, + "founded": 1985, + "website": "https://www.pacificair-example.com", + "rating": 4.0, + "on_time_performance": "79%" + } + } + + return airlines_data.get(airline, { + "name": airline, + "code": "XX", + "country": "Unknown", + "hub_airports": [], + "alliance": "Independent", + "fleet_size": 0, + "founded": None, + "website": None, + "rating": None, + "on_time_performance": "N/A" + }) + + +def compare_flight_prices(args): + """Compare prices for the same route across multiple airlines.""" + origin = args.get("origin", "San Francisco") + destination = args.get("destination", "New York") + date = args.get("date", "2025-03-15") + + return { + "route": {"origin": origin, "destination": destination, "date": date}, + "comparisons": [ + {"airline": "Pacific Air", "price": 289.50, "class": "Economy", "duration": "5h 15m", "stops": 0}, + {"airline": "SkyWest Airlines", "price": 349.99, "class": "Economy", "duration": "5h 30m", "stops": 0}, + {"airline": "Continental Express", "price": 415.00, "class": "Business", "duration": "5h 30m", "stops": 0}, + {"airline": "Budget Wings", "price": 199.99, "class": "Economy", "duration": "7h 45m", "stops": 1} + ], + "cheapest": {"airline": "Budget Wings", "price": 199.99}, + "fastest": {"airline": "Pacific Air", "duration": "5h 15m"}, + "currency": "USD" + } + + +# ============================================================================= +# HOTELS DOMAIN (4 tools) +# ============================================================================= + +def search_hotels(args): + """Search for hotels in a specified location.""" + location = args.get("location", "Manhattan, New York") + check_in = args.get("check_in", "2025-03-15") + check_out = args.get("check_out", "2025-03-18") + guests = args.get("guests", 2) + + return { + "hotels": [ + { + "hotel_id": "HT-101", + "name": "Grand Central Hotel", + "location": location, + "rating": 4.5, + "stars": 4, + "price_per_night": 259.00, + "total_price": 777.00, + "amenities": ["Pool", "Gym", "Spa", "Restaurant", "Free Wi-Fi"], + "distance_to_center": "0.3 miles" + }, + { + "hotel_id": "HT-102", + "name": "The Metropolitan Inn", + "location": location, + "rating": 4.2, + "stars": 3, + "price_per_night": 189.00, + "total_price": 567.00, + "amenities": ["Gym", "Restaurant", "Free Wi-Fi", "Business Center"], + "distance_to_center": "0.8 miles" + }, + { + "hotel_id": "HT-103", + "name": "Luxury Suites NYC", + "location": location, + "rating": 4.8, + "stars": 5, + "price_per_night": 499.00, + "total_price": 1497.00, + "amenities": ["Pool", "Gym", "Spa", "Restaurant", "Rooftop Bar", "Concierge", "Free Wi-Fi"], + "distance_to_center": "0.1 miles" + } + ], + "total_results": 3, + "search_criteria": { + "location": location, + "check_in": check_in, + "check_out": check_out, + "guests": guests + }, + "currency": "USD" + } + + +def get_hotel_details(args): + """Get detailed information about a specific hotel.""" + hotel_id = args.get("hotel_id", "HT-101") + + return { + "hotel_id": hotel_id, + "name": "Grand Central Hotel", + "address": "123 Park Avenue, Manhattan, New York, NY 10017", + "phone": "+1-212-555-0100", + "rating": 4.5, + "stars": 4, + "total_reviews": 2847, + "check_in_time": "15:00", + "check_out_time": "11:00", + "room_types": [ + {"type": "Standard", "price": 259.00, "max_guests": 2, "beds": "1 King"}, + {"type": "Deluxe", "price": 349.00, "max_guests": 3, "beds": "1 King + 1 Sofa"}, + {"type": "Suite", "price": 549.00, "max_guests": 4, "beds": "2 Kings"} + ], + "amenities": ["Pool", "Gym", "Spa", "Restaurant", "Bar", "Free Wi-Fi", "Parking", "Concierge"], + "policies": { + "cancellation": "Free cancellation up to 24 hours before check-in", + "pets": "Pets allowed ($50/night fee)", + "smoking": "Non-smoking property" + }, + "currency": "USD" + } + + +def check_room_availability(args): + """Check room availability for specific dates.""" + hotel_id = args.get("hotel_id", "HT-101") + check_in = args.get("check_in", "2025-03-15") + check_out = args.get("check_out", "2025-03-18") + room_type = args.get("room_type", "Standard") + + return { + "hotel_id": hotel_id, + "room_type": room_type, + "check_in": check_in, + "check_out": check_out, + "available": True, + "rooms_remaining": 5, + "price_per_night": 259.00, + "total_price": 777.00, + "nights": 3, + "includes_breakfast": True, + "currency": "USD" + } + + +def get_hotel_amenities(args): + """Get detailed amenity information for a hotel.""" + hotel_id = args.get("hotel_id", "HT-101") + + return { + "hotel_id": hotel_id, + "amenities": { + "pool": {"available": True, "type": "Indoor heated", "hours": "6:00 AM - 10:00 PM"}, + "gym": {"available": True, "hours": "24/7", "equipment": ["Treadmills", "Weights", "Yoga studio"]}, + "spa": {"available": True, "hours": "9:00 AM - 8:00 PM", "services": ["Massage", "Facial", "Sauna"]}, + "restaurant": {"available": True, "name": "The Park Grill", "cuisine": "American", "hours": "6:30 AM - 11:00 PM"}, + "wifi": {"available": True, "free": True, "speed": "100 Mbps"}, + "parking": {"available": True, "type": "Valet", "price_per_day": 45.00}, + "business_center": {"available": True, "hours": "24/7", "services": ["Printing", "Meeting rooms"]}, + "concierge": {"available": True, "hours": "24/7"} + }, + "currency": "USD" + } + + +# ============================================================================= +# CAR RENTALS DOMAIN (3 tools) +# ============================================================================= + +def search_car_rentals(args): + """Search for available car rentals at a location.""" + location = args.get("location", "JFK Airport") + pickup_date = args.get("pickup_date", "2025-03-15") + return_date = args.get("return_date", "2025-03-18") + + return { + "rentals": [ + { + "rental_id": "CR-101", + "company": "National Car Rental", + "car_type": "Economy", + "model": "Toyota Corolla", + "price_per_day": 45.00, + "total_price": 135.00, + "features": ["Automatic", "A/C", "4 seats", "2 bags"], + "pickup_location": location + }, + { + "rental_id": "CR-102", + "company": "Premier Auto Rentals", + "car_type": "SUV", + "model": "Ford Explorer", + "price_per_day": 85.00, + "total_price": 255.00, + "features": ["Automatic", "A/C", "7 seats", "4 bags", "GPS"], + "pickup_location": location + }, + { + "rental_id": "CR-103", + "company": "Luxury Drive", + "car_type": "Luxury", + "model": "BMW 5 Series", + "price_per_day": 150.00, + "total_price": 450.00, + "features": ["Automatic", "A/C", "5 seats", "3 bags", "GPS", "Leather seats"], + "pickup_location": location + } + ], + "total_results": 3, + "search_criteria": { + "location": location, + "pickup_date": pickup_date, + "return_date": return_date + }, + "currency": "USD" + } + + +def get_rental_details(args): + """Get detailed information about a specific car rental.""" + rental_id = args.get("rental_id", "CR-101") + + return { + "rental_id": rental_id, + "company": "National Car Rental", + "car_type": "Economy", + "model": "Toyota Corolla", + "year": 2024, + "color": "Silver", + "features": ["Automatic transmission", "Air conditioning", "Bluetooth", "USB charging", "Backup camera"], + "capacity": {"passengers": 4, "bags_large": 1, "bags_small": 2}, + "fuel_policy": "Full-to-full", + "mileage": "Unlimited", + "insurance_options": [ + {"type": "Basic", "price_per_day": 15.00, "coverage": "Liability only"}, + {"type": "Full", "price_per_day": 30.00, "coverage": "Comprehensive + collision"} + ], + "pickup_location": {"address": "JFK Airport, Terminal 4, Level 1", "hours": "24/7"}, + "requirements": {"min_age": 21, "license": "Valid driver's license", "deposit": 200.00}, + "price_per_day": 45.00, + "currency": "USD" + } + + +def check_car_availability(args): + """Check availability of a specific car rental.""" + rental_id = args.get("rental_id", "CR-101") + pickup_date = args.get("pickup_date", "2025-03-15") + return_date = args.get("return_date", "2025-03-18") + + return { + "rental_id": rental_id, + "available": True, + "cars_remaining": 8, + "pickup_date": pickup_date, + "return_date": return_date, + "price_per_day": 45.00, + "total_days": 3, + "total_price": 135.00, + "extras_available": ["GPS ($10/day)", "Child seat ($8/day)", "Additional driver ($12/day)"], + "currency": "USD" + } + + +# ============================================================================= +# RESTAURANTS DOMAIN (5 tools) +# ============================================================================= + +def search_restaurants(args): + """Search for restaurants in a specified area.""" + location = args.get("location", "Times Square, New York") + cuisine = args.get("cuisine", "Italian") + + return { + "restaurants": [ + { + "restaurant_id": "RS-101", + "name": "Bella Italia", + "cuisine": cuisine, + "location": location, + "rating": 4.6, + "price_range": "$$$", + "distance": "0.2 miles", + "open_now": True + }, + { + "restaurant_id": "RS-102", + "name": "Trattoria Roma", + "cuisine": cuisine, + "location": location, + "rating": 4.3, + "price_range": "$$", + "distance": "0.5 miles", + "open_now": True + }, + { + "restaurant_id": "RS-103", + "name": "Il Palazzo", + "cuisine": cuisine, + "location": location, + "rating": 4.8, + "price_range": "$$$$", + "distance": "0.7 miles", + "open_now": False + } + ], + "total_results": 3, + "search_criteria": {"location": location, "cuisine": cuisine} + } + + +def get_restaurant_details(args): + """Get detailed information about a specific restaurant.""" + restaurant_id = args.get("restaurant_id", "RS-101") + + return { + "restaurant_id": restaurant_id, + "name": "Bella Italia", + "cuisine": "Italian", + "address": "456 Broadway, Times Square, New York, NY 10036", + "phone": "+1-212-555-0200", + "rating": 4.6, + "total_reviews": 1523, + "price_range": "$$$", + "hours": { + "monday_friday": "11:00 AM - 11:00 PM", + "saturday": "10:00 AM - 12:00 AM", + "sunday": "10:00 AM - 10:00 PM" + }, + "features": ["Outdoor seating", "Private dining", "Full bar", "Live music on weekends"], + "dress_code": "Smart casual", + "reservations": "Recommended", + "parking": "Street parking available" + } + + +def get_menu(args): + """Get the menu for a specific restaurant.""" + restaurant_id = args.get("restaurant_id", "RS-101") + + return { + "restaurant_id": restaurant_id, + "restaurant_name": "Bella Italia", + "menu": { + "appetizers": [ + {"name": "Bruschetta", "price": 12.00, "description": "Toasted bread with tomatoes, garlic, and basil"}, + {"name": "Calamari Fritti", "price": 15.00, "description": "Crispy fried calamari with marinara sauce"}, + {"name": "Caprese Salad", "price": 14.00, "description": "Fresh mozzarella, tomatoes, and basil"} + ], + "main_courses": [ + {"name": "Spaghetti Carbonara", "price": 22.00, "description": "Classic pasta with pancetta and egg"}, + {"name": "Osso Buco", "price": 34.00, "description": "Braised veal shank with gremolata"}, + {"name": "Margherita Pizza", "price": 18.00, "description": "San Marzano tomatoes, mozzarella, fresh basil"}, + {"name": "Risotto ai Funghi", "price": 24.00, "description": "Arborio rice with wild mushrooms and truffle oil"} + ], + "desserts": [ + {"name": "Tiramisu", "price": 12.00, "description": "Classic Italian coffee-flavored dessert"}, + {"name": "Panna Cotta", "price": 10.00, "description": "Vanilla cream with berry compote"} + ] + }, + "currency": "USD" + } + + +def check_reservations(args): + """Check reservation availability at a restaurant.""" + restaurant_id = args.get("restaurant_id", "RS-101") + date = args.get("date", "2025-03-15") + party_size = args.get("party_size", 2) + time = args.get("time", "19:00") + + return { + "restaurant_id": restaurant_id, + "restaurant_name": "Bella Italia", + "date": date, + "requested_time": time, + "party_size": party_size, + "available": True, + "available_times": ["18:30", "19:00", "19:30", "20:00", "20:30"], + "estimated_wait": "No wait with reservation", + "special_notes": "Window table available for parties of 2" + } + + +def get_restaurant_reviews(args): + """Get reviews for a specific restaurant.""" + restaurant_id = args.get("restaurant_id", "RS-101") + + return { + "restaurant_id": restaurant_id, + "restaurant_name": "Bella Italia", + "average_rating": 4.6, + "total_reviews": 1523, + "reviews": [ + { + "reviewer": "John D.", + "rating": 5, + "date": "2025-02-28", + "comment": "Absolutely fantastic! The carbonara was the best I've had outside of Rome.", + "helpful_votes": 24 + }, + { + "reviewer": "Sarah M.", + "rating": 4, + "date": "2025-02-25", + "comment": "Great food and atmosphere. Service was a bit slow during peak hours.", + "helpful_votes": 18 + }, + { + "reviewer": "Michael R.", + "rating": 5, + "date": "2025-02-20", + "comment": "The tiramisu is to die for. Romantic setting perfect for date night.", + "helpful_votes": 31 + } + ], + "rating_breakdown": {"5_star": 892, "4_star": 421, "3_star": 142, "2_star": 48, "1_star": 20} + } + + +# ============================================================================= +# CURRENCY DOMAIN (3 tools) +# ============================================================================= + +def convert_currency(args): + """Convert an amount from one currency to another.""" + amount = args.get("amount", 100) + from_currency = args.get("from_currency", "USD") + to_currency = args.get("to_currency", "EUR") + + # Static exchange rates for demonstration + rates = { + ("USD", "EUR"): 0.92, + ("USD", "GBP"): 0.79, + ("USD", "JPY"): 149.50, + ("USD", "CAD"): 1.36, + ("USD", "AUD"): 1.53, + ("EUR", "USD"): 1.09, + ("GBP", "USD"): 1.27, + ("JPY", "USD"): 0.0067, + } + + rate = rates.get((from_currency, to_currency), 1.0) + converted_amount = round(amount * rate, 2) + + return { + "from": {"currency": from_currency, "amount": amount}, + "to": {"currency": to_currency, "amount": converted_amount}, + "exchange_rate": rate, + "last_updated": "2025-03-10T12:00:00Z", + "source": "Market rate (demo data)" + } + + +def get_exchange_rates(args): + """Get current exchange rates for a base currency.""" + base_currency = args.get("base_currency", "USD") + + return { + "base_currency": base_currency, + "rates": { + "EUR": 0.92, + "GBP": 0.79, + "JPY": 149.50, + "CAD": 1.36, + "AUD": 1.53, + "CHF": 0.88, + "CNY": 7.24, + "INR": 83.10, + "MXN": 17.15, + "BRL": 4.97 + }, + "last_updated": "2025-03-10T12:00:00Z", + "source": "Demo exchange rates" + } + + +def get_supported_currencies(args): + """Get list of supported currencies for conversion.""" + return { + "currencies": [ + {"code": "USD", "name": "US Dollar", "symbol": "$"}, + {"code": "EUR", "name": "Euro", "symbol": "\u20ac"}, + {"code": "GBP", "name": "British Pound", "symbol": "\u00a3"}, + {"code": "JPY", "name": "Japanese Yen", "symbol": "\u00a5"}, + {"code": "CAD", "name": "Canadian Dollar", "symbol": "C$"}, + {"code": "AUD", "name": "Australian Dollar", "symbol": "A$"}, + {"code": "CHF", "name": "Swiss Franc", "symbol": "CHF"}, + {"code": "CNY", "name": "Chinese Yuan", "symbol": "\u00a5"}, + {"code": "INR", "name": "Indian Rupee", "symbol": "\u20b9"}, + {"code": "MXN", "name": "Mexican Peso", "symbol": "MX$"}, + {"code": "BRL", "name": "Brazilian Real", "symbol": "R$"} + ], + "total": 11 + } + + +# ============================================================================= +# LOYALTY DOMAIN (3 tools) +# ============================================================================= + +def get_loyalty_balance(args): + """Get loyalty program points balance for a member.""" + member_id = args.get("member_id", "LM-12345") + program = args.get("program", "TravelRewards") + + return { + "member_id": member_id, + "program": program, + "points_balance": 47500, + "tier": "Gold", + "tier_progress": {"current": 47500, "next_tier": 75000, "next_tier_name": "Platinum"}, + "points_expiring_soon": {"amount": 5000, "expiry_date": "2025-06-30"}, + "recent_activity": [ + {"date": "2025-03-01", "description": "Flight SFO-JFK", "points": 2500, "type": "earned"}, + {"date": "2025-02-15", "description": "Hotel stay - Grand Central", "points": 1200, "type": "earned"}, + {"date": "2025-02-10", "description": "Car rental redemption", "points": -3000, "type": "redeemed"} + ] + } + + +def redeem_points(args): + """Redeem loyalty points for rewards.""" + member_id = args.get("member_id", "LM-12345") + points = args.get("points", 5000) + reward_type = args.get("reward_type", "flight_discount") + + redemption_values = { + "flight_discount": {"value": points * 0.01, "description": f"${points * 0.01:.2f} off your next flight"}, + "hotel_night": {"value": points * 0.008, "description": f"${points * 0.008:.2f} hotel credit"}, + "car_rental": {"value": points * 0.007, "description": f"${points * 0.007:.2f} rental credit"}, + "gift_card": {"value": points * 0.005, "description": f"${points * 0.005:.2f} gift card"} + } + + reward = redemption_values.get(reward_type, redemption_values["flight_discount"]) + + return { + "member_id": member_id, + "redemption": { + "points_redeemed": points, + "reward_type": reward_type, + "value": reward["value"], + "description": reward["description"], + "status": "confirmed", + "confirmation_code": "RDM-78901" + }, + "remaining_balance": 42500, + "currency": "USD" + } + + +def get_loyalty_program_info(args): + """Get information about a loyalty program.""" + program = args.get("program", "TravelRewards") + + return { + "program": program, + "tiers": [ + {"name": "Silver", "min_points": 0, "benefits": ["Basic earning rate", "Member-only fares"]}, + {"name": "Gold", "min_points": 25000, "benefits": ["1.5x earning rate", "Priority boarding", "Lounge access"]}, + {"name": "Platinum", "min_points": 75000, "benefits": ["2x earning rate", "Free upgrades", "Companion pass"]}, + {"name": "Diamond", "min_points": 150000, "benefits": ["3x earning rate", "All Platinum benefits", "Personal concierge"]} + ], + "earning_rates": { + "flights": "1 point per $1 spent", + "hotels": "2 points per $1 spent", + "car_rentals": "1 point per $2 spent", + "dining": "3 points per $1 spent" + }, + "redemption_options": ["Flight discounts", "Hotel nights", "Car rental credits", "Gift cards", "Experience packages"], + "partners": ["SkyWest Airlines", "Grand Central Hotels", "National Car Rental", "Bella Italia Restaurant Group"] + } + + +# ============================================================================= +# WEATHER DOMAIN (2 tools) +# ============================================================================= + +def get_weather_forecast(args): + """Get weather forecast for a location.""" + location = args.get("location", "New York") + days = args.get("days", 5) + + forecast_data = [ + {"date": "2025-03-15", "high_f": 55, "low_f": 42, "condition": "Partly Cloudy", "precipitation": "10%"}, + {"date": "2025-03-16", "high_f": 58, "low_f": 45, "condition": "Sunny", "precipitation": "0%"}, + {"date": "2025-03-17", "high_f": 52, "low_f": 38, "condition": "Rain", "precipitation": "80%"}, + {"date": "2025-03-18", "high_f": 50, "low_f": 36, "condition": "Cloudy", "precipitation": "30%"}, + {"date": "2025-03-19", "high_f": 60, "low_f": 44, "condition": "Sunny", "precipitation": "5%"}, + {"date": "2025-03-20", "high_f": 62, "low_f": 46, "condition": "Partly Cloudy", "precipitation": "15%"}, + {"date": "2025-03-21", "high_f": 57, "low_f": 41, "condition": "Overcast", "precipitation": "25%"} + ] + + return { + "location": location, + "forecast": forecast_data[:days], + "units": "Fahrenheit", + "source": "Demo weather data" + } + + +def get_current_weather(args): + """Get current weather conditions for a location.""" + location = args.get("location", "New York") + + return { + "location": location, + "current": { + "temperature_f": 54, + "feels_like_f": 50, + "condition": "Partly Cloudy", + "humidity": "62%", + "wind": {"speed_mph": 12, "direction": "NW"}, + "visibility_miles": 10, + "uv_index": 3 + }, + "updated_at": "2025-03-15T10:30:00Z", + "units": "Fahrenheit", + "source": "Demo weather data" + } + + +# ============================================================================= +# ACTIVITIES DOMAIN (3 tools) +# ============================================================================= + +def search_activities(args): + """Search for activities and attractions in a location.""" + location = args.get("location", "New York") + category = args.get("category", "sightseeing") + + return { + "activities": [ + { + "activity_id": "AC-101", + "name": "Statue of Liberty & Ellis Island Tour", + "category": "sightseeing", + "location": location, + "duration": "4 hours", + "price": 45.00, + "rating": 4.7, + "available": True + }, + { + "activity_id": "AC-102", + "name": "Central Park Bike Tour", + "category": "outdoor", + "location": location, + "duration": "2.5 hours", + "price": 35.00, + "rating": 4.5, + "available": True + }, + { + "activity_id": "AC-103", + "name": "Broadway Show - The Lion King", + "category": "entertainment", + "location": location, + "duration": "2.5 hours", + "price": 125.00, + "rating": 4.9, + "available": True + }, + { + "activity_id": "AC-104", + "name": "NYC Food Walking Tour", + "category": "food", + "location": location, + "duration": "3 hours", + "price": 65.00, + "rating": 4.6, + "available": True + } + ], + "total_results": 4, + "search_criteria": {"location": location, "category": category}, + "currency": "USD" + } + + +def get_activity_details(args): + """Get detailed information about a specific activity.""" + activity_id = args.get("activity_id", "AC-101") + + return { + "activity_id": activity_id, + "name": "Statue of Liberty & Ellis Island Tour", + "description": "Visit two of New York's most iconic landmarks. Includes ferry tickets, guided tour of Liberty Island, and access to the Ellis Island Immigration Museum.", + "category": "sightseeing", + "location": "Battery Park, New York", + "duration": "4 hours", + "start_times": ["9:00 AM", "10:30 AM", "12:00 PM", "1:30 PM"], + "price": {"adult": 45.00, "child": 25.00, "senior": 38.00}, + "includes": ["Ferry tickets", "Guided tour", "Museum access", "Audio guide"], + "requirements": ["Comfortable walking shoes", "Valid ID for security check"], + "cancellation_policy": "Free cancellation up to 24 hours before", + "meeting_point": "Castle Clinton, Battery Park", + "rating": 4.7, + "total_reviews": 3420, + "currency": "USD" + } + + +def check_activity_availability(args): + """Check availability for a specific activity on a date.""" + activity_id = args.get("activity_id", "AC-101") + date = args.get("date", "2025-03-15") + participants = args.get("participants", 2) + + return { + "activity_id": activity_id, + "activity_name": "Statue of Liberty & Ellis Island Tour", + "date": date, + "participants": participants, + "available_slots": [ + {"time": "9:00 AM", "spots_remaining": 15}, + {"time": "10:30 AM", "spots_remaining": 8}, + {"time": "12:00 PM", "spots_remaining": 22}, + {"time": "1:30 PM", "spots_remaining": 30} + ], + "total_price": 45.00 * participants, + "currency": "USD" + } + + +# ============================================================================= +# TRIP PLANNING DOMAIN (2 tools) +# ============================================================================= + +def create_itinerary(args): + """Create a trip itinerary based on destination and preferences.""" + destination = args.get("destination", "New York") + days = args.get("days", 3) + interests = args.get("interests", ["sightseeing", "food", "culture"]) + + itinerary = { + "destination": destination, + "duration": f"{days} days", + "daily_plan": [ + { + "day": 1, + "theme": "Iconic Landmarks", + "activities": [ + {"time": "9:00 AM", "activity": "Statue of Liberty & Ellis Island", "duration": "4 hours"}, + {"time": "1:30 PM", "activity": "Lunch at Battery Park area", "duration": "1 hour"}, + {"time": "3:00 PM", "activity": "9/11 Memorial & Museum", "duration": "2 hours"}, + {"time": "6:00 PM", "activity": "Dinner in Tribeca", "duration": "1.5 hours"}, + {"time": "8:00 PM", "activity": "Brooklyn Bridge walk at sunset", "duration": "1 hour"} + ] + }, + { + "day": 2, + "theme": "Culture & Entertainment", + "activities": [ + {"time": "9:00 AM", "activity": "Metropolitan Museum of Art", "duration": "3 hours"}, + {"time": "12:30 PM", "activity": "Lunch on Museum Mile", "duration": "1 hour"}, + {"time": "2:00 PM", "activity": "Central Park walking tour", "duration": "2 hours"}, + {"time": "5:00 PM", "activity": "Times Square exploration", "duration": "1.5 hours"}, + {"time": "7:30 PM", "activity": "Broadway show", "duration": "2.5 hours"} + ] + }, + { + "day": 3, + "theme": "Food & Neighborhoods", + "activities": [ + {"time": "9:00 AM", "activity": "Chelsea Market food tour", "duration": "2 hours"}, + {"time": "11:30 AM", "activity": "High Line walk", "duration": "1.5 hours"}, + {"time": "1:30 PM", "activity": "Lunch in Greenwich Village", "duration": "1 hour"}, + {"time": "3:00 PM", "activity": "SoHo shopping", "duration": "2 hours"}, + {"time": "6:00 PM", "activity": "Farewell dinner in Little Italy", "duration": "2 hours"} + ] + } + ] + } + + # Trim to requested days + itinerary["daily_plan"] = itinerary["daily_plan"][:days] + + return { + "itinerary": itinerary, + "tips": [ + "Get a MetroCard for unlimited subway rides", + "Book Broadway tickets in advance for best prices", + "Wear comfortable walking shoes — NYC is best explored on foot" + ], + "estimated_budget": {"low": 150 * days, "high": 350 * days, "currency": "USD"} + } + + +def get_travel_tips(args): + """Get travel tips and recommendations for a destination.""" + destination = args.get("destination", "New York") + category = args.get("category", "general") + + tips = { + "destination": destination, + "tips": { + "general": [ + "Best time to visit: April-June and September-November for mild weather", + "Get a 7-day unlimited MetroCard ($33) for subway and bus travel", + "Many museums offer 'pay what you wish' hours", + "Tipping is expected: 18-20% at restaurants, $1-2 per drink at bars", + "Download offline maps — cell service can be spotty underground" + ], + "safety": [ + "NYC is generally safe for tourists, but stay aware of your surroundings", + "Keep valuables secure in crowded areas like Times Square and subway", + "Stick to well-lit streets at night", + "Use official yellow cabs or ride-sharing apps" + ], + "budget": [ + "Free attractions: Central Park, Brooklyn Bridge, Staten Island Ferry, High Line", + "Eat at food trucks and delis for affordable meals ($8-12)", + "TKTS booth in Times Square offers same-day Broadway tickets at 20-50% off", + "Happy hour deals at restaurants typically run 4-7 PM" + ], + "packing": [ + "Layers are key — weather can change quickly", + "Comfortable walking shoes are essential (expect 10-15k steps/day)", + "Compact umbrella for unexpected rain", + "Portable phone charger for navigation and photos" + ] + }, + "local_phrases": [ + {"phrase": "How do I get to...?", "context": "Asking for directions"}, + {"phrase": "What's good here?", "context": "Asking restaurant staff for recommendations"}, + {"phrase": "Can I get the check?", "context": "Requesting the bill at a restaurant"} + ] + } + + return tips + + +# ============================================================================= +# TOOL DISPATCH TABLE +# ============================================================================= + +TOOL_DISPATCH = { + # Flights domain (9 tools) + "search_flights": search_flights, + "get_flight_details": get_flight_details, + "check_availability": check_availability, + "get_seat_map": get_seat_map, + "get_baggage_policy": get_baggage_policy, + "get_flight_status": get_flight_status, + "search_connecting_flights": search_connecting_flights, + "get_airline_info": get_airline_info, + "compare_flight_prices": compare_flight_prices, + # Hotels domain (4 tools) + "search_hotels": search_hotels, + "get_hotel_details": get_hotel_details, + "check_room_availability": check_room_availability, + "get_hotel_amenities": get_hotel_amenities, + # Car Rentals domain (3 tools) + "search_car_rentals": search_car_rentals, + "get_rental_details": get_rental_details, + "check_car_availability": check_car_availability, + # Restaurants domain (5 tools) + "search_restaurants": search_restaurants, + "get_restaurant_details": get_restaurant_details, + "get_menu": get_menu, + "check_reservations": check_reservations, + "get_restaurant_reviews": get_restaurant_reviews, + # Currency domain (3 tools) + "convert_currency": convert_currency, + "get_exchange_rates": get_exchange_rates, + "get_supported_currencies": get_supported_currencies, + # Loyalty domain (3 tools) + "get_loyalty_balance": get_loyalty_balance, + "redeem_points": redeem_points, + "get_loyalty_program_info": get_loyalty_program_info, + # Weather domain (2 tools) + "get_weather_forecast": get_weather_forecast, + "get_current_weather": get_current_weather, + # Activities domain (3 tools) + "search_activities": search_activities, + "get_activity_details": get_activity_details, + "check_activity_availability": check_activity_availability, + # Trip Planning domain (2 tools) + "create_itinerary": create_itinerary, + "get_travel_tips": get_travel_tips, +} From 51ca24268ada341835efd7cecf07725f991367bd Mon Sep 17 00:00:00 2001 From: sentil mohan Date: Fri, 5 Jun 2026 17:58:14 +0100 Subject: [PATCH 2/4] docs: add name to CONTRIBUTORS.md --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index ed6d8da1e..e88770b6f 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -118,3 +118,4 @@ - palbiren - Gui Ruggiero (guiruggiero) - Visakh Madathil (vmmadathil) +- Senthil Mohan (skmohan) From b7f3e4cd6bcfa8d8366963cdd1c48a835397e27e Mon Sep 17 00:00:00 2001 From: sentil mohan Date: Thu, 11 Jun 2026 20:46:01 +0100 Subject: [PATCH 3/4] refactor: convert notebook to separate Python scripts with detailed README - Replace notebook with deploy.py, invoke.py, cleanup.py, config.py - Add detailed README with architecture diagram and usage instructions - Add requirements.txt and .gitignore - Add architecture diagram from AgentCore SDK --- .../agentcore-tool-search-plugin/.gitignore | 10 + .../agentcore-tool-search-plugin/README.md | 148 +++ .../agentcore-tool-search-plugin-sample.ipynb | 948 ------------------ .../agentcore-tool-search-plugin/cleanup.py | 96 ++ .../agentcore-tool-search-plugin/config.py | 12 + .../agentcore-tool-search-plugin/deploy.py | 258 +++++ .../images/architecture.png | Bin 0 -> 143034 bytes .../agentcore-tool-search-plugin/invoke.py | 90 ++ .../requirements.txt | 5 + 9 files changed, 619 insertions(+), 948 deletions(-) create mode 100644 03-integrations/gateway/agentcore-tool-search-plugin/.gitignore create mode 100644 03-integrations/gateway/agentcore-tool-search-plugin/README.md delete mode 100644 03-integrations/gateway/agentcore-tool-search-plugin/agentcore-tool-search-plugin-sample.ipynb create mode 100644 03-integrations/gateway/agentcore-tool-search-plugin/cleanup.py create mode 100644 03-integrations/gateway/agentcore-tool-search-plugin/config.py create mode 100644 03-integrations/gateway/agentcore-tool-search-plugin/deploy.py create mode 100644 03-integrations/gateway/agentcore-tool-search-plugin/images/architecture.png create mode 100644 03-integrations/gateway/agentcore-tool-search-plugin/invoke.py create mode 100644 03-integrations/gateway/agentcore-tool-search-plugin/requirements.txt diff --git a/03-integrations/gateway/agentcore-tool-search-plugin/.gitignore b/03-integrations/gateway/agentcore-tool-search-plugin/.gitignore new file mode 100644 index 000000000..2b34709fc --- /dev/null +++ b/03-integrations/gateway/agentcore-tool-search-plugin/.gitignore @@ -0,0 +1,10 @@ +.deploy_state.json +.DS_Store +__pycache__/ +*.pyc +.venv/ +bin/ +lib/ +include/ +share/ +pyvenv.cfg diff --git a/03-integrations/gateway/agentcore-tool-search-plugin/README.md b/03-integrations/gateway/agentcore-tool-search-plugin/README.md new file mode 100644 index 000000000..ae45636e0 --- /dev/null +++ b/03-integrations/gateway/agentcore-tool-search-plugin/README.md @@ -0,0 +1,148 @@ +# Semantic Tool Discovery with AgentCore Gateway Tool Search Plugin + +This sample demonstrates how to build a **Strands agent** that uses **Amazon Bedrock AgentCore Gateway's semantic tool search** to dynamically discover and invoke only the relevant tools for each user request — without loading all tools upfront. + +## What This Sample Does + +When agents have access to many tools (tens or hundreds), loading all of them for every request is inefficient and can degrade model performance. This sample shows how the `AgentCoreToolSearchPlugin` solves this problem by: + +1. **Deploying a Lambda function** with 34 travel-domain tools across 9 domains (flights, hotels, car rentals, restaurants, currency, loyalty, weather, activities, trip planning) +2. **Registering the function** with AgentCore Gateway as a tool target with semantic search enabled +3. **Creating a Strands agent** that uses the plugin to dynamically discover only the relevant tools per request based on user intent + +The key value: instead of loading all 34 tools for every query, the agent dynamically loads only the relevant tools per request via semantic search — improving response quality and reducing token usage. + +## Architecture + +![Architecture](images/architecture.png) + +On each agent invocation: + +1. **User query** — The user sends a query to the Strands agent +2. **Hook** — The agent triggers the `AgentCoreToolSearchPlugin` before model invocation +3. **Derive intent** — The `IntentProvider` sends the last N messages from conversation history to the configured LLM to produce a concise intent string +4. **Search gateway** — The intent is passed to AgentCore Gateway's `x_amz_bedrock_agentcore_search` tool to obtain the most relevant tools +5. **Invoke LLM** — The agent invokes the LLM with the user query along with the matched tools from registered MCP targets (Lambda, API Gateway, MCP Server) + +Previously loaded tools are cleared before each search, so the agent always has the most relevant tools available. + +## Key Concepts + +| Concept | Description | +|---------|-------------| +| **AgentCore Gateway** | Managed MCP-compatible endpoint that hosts tool targets and provides semantic search | +| **Tool Target** | A Lambda function (or other compute) registered with the gateway, wrapped automatically with MCP protocol | +| **Semantic Tool Search** | Gateway capability that matches user intent to relevant tools using embeddings | +| **AgentCoreToolSearchPlugin** | Strands plugin that hooks into the agent lifecycle to search and load tools dynamically | +| **Intent Provider** | Component that derives a concise intent string from conversation history (defaults to the agent's own model) | + +## Prerequisites + +| Requirement | Details | +|---|---| +| Python | 3.12+ | +| AWS CLI | 2.x, configured with valid credentials | +| AWS Account | With Bedrock model access enabled | + +### Required IAM Permissions + +Your calling identity needs: + +- `lambda:CreateFunction`, `lambda:InvokeFunction`, `lambda:DeleteFunction`, `lambda:UpdateFunctionCode`, `lambda:GetFunction` +- `iam:CreateRole`, `iam:AttachRolePolicy`, `iam:DetachRolePolicy`, `iam:DeleteRole` +- `bedrock:InvokeModel` (for agent reasoning) +- `bedrock-agentcore:*` (for gateway management and MCP connections) + +## Getting Started + +### 1. Install dependencies + +```bash +pip install -r requirements.txt +``` + +### 2. Configure AWS credentials + +```bash +export AWS_REGION=us-east-1 # optional, defaults to us-east-1 +aws sts get-caller-identity # verify credentials +``` + +### 3. Deploy infrastructure + +This creates the Lambda function, AgentCore Gateway, and registers the tools: + +```bash +python deploy.py +``` + +### 4. Run the agent + +This creates a Strands agent with the `AgentCoreToolSearchPlugin` and runs example queries across multiple travel domains: + +```bash +python invoke.py +``` + +You'll see the agent dynamically select different tool subsets for each domain: + +``` + Domain: Flight Search + Query: Find flights from San Francisco to New York next Friday + Tools loaded by semantic search: ['check_availability', 'get_flight_details', 'search_flights'] + + Domain: Hotel Search + Query: Search for hotels in Manhattan with a pool + Tools loaded by semantic search: ['get_hotel_amenities', 'get_hotel_details', 'search_hotels'] +``` + +### 5. Clean up resources + +```bash +python cleanup.py +``` + +## Project Structure + +``` +agentcore-tool-search-plugin/ +├── deploy.py # Deploy Lambda + Gateway + register tools +├── invoke.py # Create agent and run example queries +├── cleanup.py # Delete all created AWS resources +├── config.py # Shared configuration +├── requirements.txt # Python dependencies +├── README.md # This file +└── lambda/ + ├── travel_tools.py # Lambda function with 34 travel tools + └── tool_schemas.json # MCP tool schemas for gateway registration +``` + +## Configuration + +Environment variables: + +| Variable | Default | Description | +|----------|---------|-------------| +| `AWS_REGION` | `us-east-1` | AWS region for all resources | +| `MODEL_ID` | `us.anthropic.claude-sonnet-4-20250514-v1:0` | Bedrock model for agent reasoning | + +## Customization + +The `AgentCoreToolSearchPlugin` supports custom models for intent classification, custom system prompts, and fully custom intent providers. For details and code examples, see the [AgentCoreToolSearchPlugin documentation](https://strandsagents.com/docs/community/plugins/agentcore-tool-search/). + +## Cost Considerations + +Running this sample creates AWS resources that incur charges: + +- **AWS Lambda** — Function invocations during verification and agent tool calls +- **Amazon Bedrock** — Model inference for agent reasoning and intent classification +- **AgentCore Gateway** — Gateway usage for tool registration and MCP-based invocations + +Always run `python cleanup.py` when finished to delete all resources. + +## Related Resources + +- [AgentCoreToolSearchPlugin documentation (Strands)](https://strandsagents.com/docs/community/plugins/agentcore-tool-search/) +- [AgentCoreToolSearchPlugin source code](https://github.com/aws/bedrock-agentcore-sdk-python/tree/main/src/bedrock_agentcore/gateway/integrations/strands/plugins/agentcore_tool_search) +- [AgentCore Gateway — Semantic Tool Search](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-using-mcp-semantic-search.html) +- [Strands Agents SDK](https://strandsagents.com/) \ No newline at end of file diff --git a/03-integrations/gateway/agentcore-tool-search-plugin/agentcore-tool-search-plugin-sample.ipynb b/03-integrations/gateway/agentcore-tool-search-plugin/agentcore-tool-search-plugin-sample.ipynb deleted file mode 100644 index 21e7d5e7e..000000000 --- a/03-integrations/gateway/agentcore-tool-search-plugin/agentcore-tool-search-plugin-sample.ipynb +++ /dev/null @@ -1,948 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "introduction", - "metadata": {}, - "source": [ - "# Semantic Tool Discovery with the AgentCore Tool Search Plugin for Strands Agents\n", - "\n", - "## 1. Introduction\n", - "\n", - "This notebook demonstrates how to build a **Strands agent** that interacts with **Amazon Bedrock AgentCore Gateway** to access travel-domain tools via semantic tool search. You will deploy a plain Lambda function containing travel tools, register it with AgentCore Gateway as a tool target, and create a Strands agent that dynamically discovers and invokes the right tools based on user intent.\n", - "\n", - "### What you will learn\n", - "\n", - "- **Deploy a Lambda function** as a tool target for AgentCore Gateway\n", - "- **Register the function** with AgentCore Gateway so tools become discoverable\n", - "- **Create a Strands agent** with `AgentCoreToolSearchPlugin` for semantic tool discovery\n", - "- **Invoke the agent** across multiple travel domains (flights, hotels, car rentals, restaurants, currency, loyalty programs)\n", - "\n", - "### Architecture Overview\n", - "\n", - "The architecture follows this flow:\n", - "\n", - "
\n",
-    "┌─────────────────┐    hook       ┌───────────────────────────┐   MCP/search   ┌─────────────────────┐            ┌──────────────────┐\n",
-    "│  Strands Agent  │──────────────▶│  AgentCoreToolSearchPlugin│───────────────▶│  AgentCore Gateway  │            │  Lambda Function │\n",
-    "│  (LLM reasoning)│◀── tools ─────│ (semantic tool discovery) │◀── tools ──────│  (MCP protocol)     │            |  (travel tools)  │\n",
-    "│                 │               └───────────────────────────┘                │                     │            │                  │\n",
-    "│                 │─────────────── MCP/tool call ─────────────────────────────▶│                     │───invoke──▶│                  │\n",
-    "│                 │◀────────────── tool result ─────────────────────────────── │                     │◀─ result ──│                  │\n",
-    "└─────────────────┘                                                            └─────────────────────┘            └──────────────────┘\n",
-    "
\n" - ] - }, - { - "cell_type": "markdown", - "id": "prerequisites", - "metadata": {}, - "source": [ - "## 2. Prerequisites\n", - "\n", - "Before running this notebook, ensure you have the following:\n", - "\n", - "- **AWS Account** with appropriate permissions configured\n", - "- **Python 3.12+** installed\n", - "- **AWS CLI** installed and configured with valid credentials\n", - "- **boto3** Python SDK installed\n", - "- **IAM permissions** for the calling identity:\n", - " - `lambda:*` - Create, invoke, and delete Lambda functions\n", - " - `iam:CreateRole`, `iam:AttachRolePolicy`, `iam:DetachRolePolicy`, `iam:DeleteRole` - Manage Lambda execution role\n", - " - `bedrock:*` - Access Amazon Bedrock models for agent reasoning\n", - " - `bedrock-agentcore:*` - Register tools and connect to AgentCore Gateway" - ] - }, - { - "cell_type": "markdown", - "id": "cost-warning", - "metadata": {}, - "source": [ - "Running this notebook will create AWS resources that may incur charges:\n", - "\n", - "- **AWS Lambda** - Function invocations during deployment verification and agent tool calls\n", - "- **Amazon Bedrock** - Model inference for agent reasoning and intent classification\n", - "- **AgentCore Gateway** - Gateway usage for tool registration and MCP-based tool invocations\n", - "\n", - "To avoid ongoing costs, be sure to run the **Cleanup** section at the end of this notebook to delete all created resources." - ] - }, - { - "cell_type": "markdown", - "id": "env-setup-header", - "metadata": {}, - "source": [ - "## 3. Environment Setup\n", - "\n", - "In this section we install the required Python packages, verify that your AWS credentials are configured correctly, and define all configurable parameters in one place." - ] - }, - { - "cell_type": "markdown", - "id": "install-packages-intro", - "metadata": {}, - "source": [ - "### Install Required Packages\n", - "\n", - "The following cell installs the Python packages needed for this tutorial:\n", - "\n", - "- `strands-agents` - The Strands SDK for building AI agents\n", - "- `strands-agents-tools` - Pre-built tools for Strands agents\n", - "- `boto3` - AWS SDK for Python\n", - "- `bedrock-agentcore[strands-agents]` - AgentCore SDK with Strands plugin for semantic tool discovery\n", - "- `mcp-proxy-for-aws` - IAM-authenticated MCP transport for connecting to AgentCore Gateway" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "install-packages", - "metadata": {}, - "outputs": [], - "source": [ - "%pip install --quiet strands-agents strands-agents-tools boto3 \"bedrock-agentcore[strands-agents]\" mcp-proxy-for-aws" - ] - }, - { - "cell_type": "markdown", - "id": "verify-credentials-intro", - "metadata": {}, - "source": [ - "### Verify AWS Credentials\n", - "\n", - "Before proceeding, let's verify that your AWS credentials are configured correctly and confirm which account and region you are operating in." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "verify-credentials", - "metadata": {}, - "outputs": [], - "source": [ - "import boto3\n", - "\n", - "sts = boto3.client(\"sts\")\n", - "identity = sts.get_caller_identity()\n", - "print(f\"Account: {identity['Account']}\")\n", - "print(f\"ARN: {identity['Arn']}\")\n", - "print(\"[OK] AWS credentials verified successfully\")" - ] - }, - { - "cell_type": "markdown", - "id": "configuration-intro", - "metadata": {}, - "source": [ - "### Configuration\n", - "\n", - "All configurable parameters for this tutorial are defined in the cell below. This is the single place where you can customize the behavior for your environment.\n", - "\n", - "The AgentCore Gateway will be created later in the notebook - no pre-existing gateway is required." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "configuration", - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "\n", - "# Configuration - modify these values for your environment\n", - "AWS_REGION = os.environ.get(\"AWS_REGION\", \"us-east-1\")\n", - "LAMBDA_FUNCTION_NAME = \"agentcore-travel-tools\"\n", - "LAMBDA_ROLE_NAME = \"agentcore-travel-tools-role\"\n", - "MODEL_ID = \"us.anthropic.claude-sonnet-4-20250514-v1:0\"\n", - "\n", - "print(f\"Region: {AWS_REGION}\")\n", - "print(f\"Lambda Function: {LAMBDA_FUNCTION_NAME}\")\n", - "print(f\"Model: {MODEL_ID}\")" - ] - }, - { - "cell_type": "markdown", - "id": "deploy-lambda-header", - "metadata": {}, - "source": [ - "## 4. Deploy the Lambda Function\n", - "\n", - "In this section we deploy a **plain Lambda function** as a tool target for AgentCore Gateway. The function contains 34 travel-domain tools across 9 domains (flights, hotels, car rentals, restaurants, currency, loyalty, weather, activities, and trip planning).\n", - "\n", - "**Important:** This Lambda is NOT an MCP server. It is a standard Lambda function that receives tool arguments as input and returns results. AgentCore Gateway handles the MCP protocol translation - it wraps the Lambda as a tool target and routes MCP tool calls from the agent to the function automatically." - ] - }, - { - "cell_type": "markdown", - "id": "iam-role-intro", - "metadata": {}, - "source": [ - "### Create IAM Execution Role\n", - "\n", - "The Lambda function needs an IAM execution role to run. This role grants the Lambda service permission to assume it, and we attach the `AWSLambdaBasicExecutionRole` managed policy for CloudWatch Logs access." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "create-iam-role", - "metadata": {}, - "outputs": [], - "source": [ - "import json\n", - "import time\n", - "\n", - "iam = boto3.client(\"iam\", region_name=AWS_REGION)\n", - "\n", - "trust_policy = {\n", - " \"Version\": \"2012-10-17\",\n", - " \"Statement\": [\n", - " {\n", - " \"Effect\": \"Allow\",\n", - " \"Principal\": {\"Service\": \"lambda.amazonaws.com\"},\n", - " \"Action\": \"sts:AssumeRole\"\n", - " }\n", - " ]\n", - "}\n", - "\n", - "try:\n", - " role_response = iam.create_role(\n", - " RoleName=LAMBDA_ROLE_NAME,\n", - " AssumeRolePolicyDocument=json.dumps(trust_policy),\n", - " Description=\"Execution role for AgentCore travel tools Lambda\"\n", - " )\n", - " role_arn = role_response[\"Role\"][\"Arn\"]\n", - " print(f\"\\u2705 Created IAM role: {LAMBDA_ROLE_NAME}\")\n", - " print(f\" ARN: {role_arn}\")\n", - " \n", - " # Attach basic execution policy for CloudWatch Logs\n", - " iam.attach_role_policy(\n", - " RoleName=LAMBDA_ROLE_NAME,\n", - " PolicyArn=\"arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole\"\n", - " )\n", - " print(\"\\u2705 Attached AWSLambdaBasicExecutionRole policy\")\n", - " \n", - " # Wait for IAM role propagation\n", - " print(\"\\n\\u23f3 Waiting 10 seconds for IAM role propagation...\")\n", - " time.sleep(10)\n", - " print(\"\\u2705 Role propagation complete\")\n", - " \n", - "except iam.exceptions.EntityAlreadyExistsException:\n", - " role_arn = f\"arn:aws:iam::{identity['Account']}:role/{LAMBDA_ROLE_NAME}\"\n", - " print(f\"\\u2139\\ufe0f IAM role already exists: {LAMBDA_ROLE_NAME}\")\n", - " print(f\" ARN: {role_arn}\")" - ] - }, - { - "cell_type": "markdown", - "id": "deploy-lambda-intro", - "metadata": {}, - "source": [ - "### Deploy the Lambda Function\n", - "\n", - "We package `lambda/travel_tools.py` into a zip archive and deploy it. If the function already exists (from a previous run), we update its code instead.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "deploy-lambda", - "metadata": {}, - "outputs": [], - "source": [ - "import zipfile\n", - "import io\n", - "\n", - "lambda_client = boto3.client(\"lambda\", region_name=AWS_REGION)\n", - "\n", - "# Package the Lambda function\n", - "zip_buffer = io.BytesIO()\n", - "with zipfile.ZipFile(zip_buffer, \"w\", zipfile.ZIP_DEFLATED) as zf:\n", - " zf.write(\"lambda/travel_tools.py\", \"travel_tools.py\")\n", - "zip_buffer.seek(0)\n", - "\n", - "try:\n", - " response = lambda_client.create_function(\n", - " FunctionName=LAMBDA_FUNCTION_NAME,\n", - " Runtime=\"python3.12\",\n", - " Role=role_arn,\n", - " Handler=\"travel_tools.lambda_handler\",\n", - " Code={\"ZipFile\": zip_buffer.read()},\n", - " Description=\"Travel domain tools for AgentCore Gateway\",\n", - " Timeout=30,\n", - " MemorySize=256\n", - " )\n", - " print(f\"\\u2705 Lambda function created: {LAMBDA_FUNCTION_NAME}\")\n", - " print(f\" ARN: {response['FunctionArn']}\")\n", - " lambda_arn = response[\"FunctionArn\"]\n", - "except lambda_client.exceptions.ResourceConflictException:\n", - " print(f\"\\u2139\\ufe0f Lambda function already exists: {LAMBDA_FUNCTION_NAME}\")\n", - " # Update the function code instead\n", - " zip_buffer.seek(0)\n", - " lambda_client.update_function_code(\n", - " FunctionName=LAMBDA_FUNCTION_NAME,\n", - " ZipFile=zip_buffer.read()\n", - " )\n", - " func_info = lambda_client.get_function(FunctionName=LAMBDA_FUNCTION_NAME)\n", - " lambda_arn = func_info[\"Configuration\"][\"FunctionArn\"]\n", - " print(f\" Updated function code. ARN: {lambda_arn}\")" - ] - }, - { - "cell_type": "markdown", - "id": "verify-lambda-intro", - "metadata": {}, - "source": [ - "### Verify Lambda Deployment\n", - "\n", - "Let's verify the deployment by invoking the Lambda function with a test event. We'll call the `get_supported_currencies` tool to confirm the function is responding correctly." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "verify-lambda", - "metadata": {}, - "outputs": [], - "source": [ - "# Verify the Lambda function by invoking a test tool call\n", - "# For direct invocation, we pass tool_name in the event (fallback mode)\n", - "# In production, the gateway passes tool name via context.client_context.custom\n", - "test_event = {\n", - " \"tool_name\": \"get_supported_currencies\"\n", - "}\n", - "\n", - "response = lambda_client.invoke(\n", - " FunctionName=LAMBDA_FUNCTION_NAME,\n", - " Payload=json.dumps(test_event)\n", - ")\n", - "\n", - "payload = json.loads(response[\"Payload\"].read())\n", - "print(\"[OK] Lambda function verified successfully!\")\n", - "print(f\" Supported currencies: {payload['total']} currencies available\")\n", - "print(f\" Sample: {[c['code'] for c in payload['currencies'][:5]]}\")\n" - ] - }, - { - "cell_type": "markdown", - "id": "gateway-registration-header", - "metadata": {}, - "source": [ - "## 5. Create AgentCore Gateway and Register Tools\n", - "\n", - "**Amazon Bedrock AgentCore Gateway** is a managed service that provides an MCP-compatible endpoint for your agents. In this section, we'll:\n", - "\n", - "1. **Create a new gateway** - This gives us a managed MCP endpoint\n", - "2. **Register the Lambda function** as a tool target with the gateway\n", - "3. **Verify** the tools are discoverable\n" - ] - }, - { - "cell_type": "markdown", - "id": "create-gateway-intro", - "metadata": {}, - "source": [ - "### Create the AgentCore Gateway\n", - "\n", - "First, we create a new AgentCore Gateway. This gives us a managed MCP endpoint that agents can connect to. The gateway will handle MCP protocol translation for any tool targets we register with it." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "create-gateway", - "metadata": {}, - "outputs": [], - "source": [ - "# Create an AgentCore Gateway\n", - "agentcore_control_client = boto3.client(\"bedrock-agentcore-control\", region_name=AWS_REGION)\n", - "\n", - "GATEWAY_NAME = \"agentcore-travel-gateway\"\n", - "GATEWAY_ROLE_NAME = \"agentcore-travel-gateway-role\"\n", - "\n", - "# Create IAM role for the gateway\n", - "gateway_trust_policy = {\n", - " \"Version\": \"2012-10-17\",\n", - " \"Statement\": [\n", - " {\n", - " \"Effect\": \"Allow\",\n", - " \"Principal\": {\"Service\": \"bedrock-agentcore.amazonaws.com\"},\n", - " \"Action\": \"sts:AssumeRole\"\n", - " }\n", - " ]\n", - "}\n", - "\n", - "try:\n", - " gw_role_response = iam.create_role(\n", - " RoleName=GATEWAY_ROLE_NAME,\n", - " AssumeRolePolicyDocument=json.dumps(gateway_trust_policy),\n", - " Description=\"Role for AgentCore Gateway to invoke Lambda targets\"\n", - " )\n", - " gateway_role_arn = gw_role_response[\"Role\"][\"Arn\"]\n", - " # Allow the gateway to invoke Lambda functions\n", - " iam.attach_role_policy(\n", - " RoleName=GATEWAY_ROLE_NAME,\n", - " PolicyArn=\"arn:aws:iam::aws:policy/service-role/AWSLambdaRole\"\n", - " )\n", - " print(f\"[OK] Created gateway role: {GATEWAY_ROLE_NAME}\")\n", - " time.sleep(10)\n", - "except iam.exceptions.EntityAlreadyExistsException:\n", - " gateway_role_arn = f\"arn:aws:iam::{identity['Account']}:role/{GATEWAY_ROLE_NAME}\"\n", - " print(f\"[INFO] Gateway role already exists: {GATEWAY_ROLE_NAME}\")\n", - "\n", - "# Create the gateway\n", - "try:\n", - " gateway_response = agentcore_control_client.create_gateway(\n", - " name=GATEWAY_NAME,\n", - " roleArn=gateway_role_arn,\n", - " authorizerType=\"AWS_IAM\",\n", - " protocolType=\"MCP\",\n", - " protocolConfiguration={\n", - " \"mcp\": {\n", - " \"searchType\": \"SEMANTIC\"\n", - " }\n", - " },\n", - " description=\"Travel domain gateway for semantic tool search demo\"\n", - " )\n", - " gateway_id = gateway_response[\"gatewayId\"]\n", - " GATEWAY_ENDPOINT = gateway_response[\"gatewayUrl\"]\n", - " print(f\"[OK] AgentCore Gateway created: {GATEWAY_NAME}\")\n", - " print(f\" Gateway ID: {gateway_id}\")\n", - " print(f\" Endpoint: {GATEWAY_ENDPOINT}\")\n", - "except Exception as e:\n", - " if \"already exists\" in str(e).lower() or \"conflict\" in str(e).lower():\n", - " gateways = agentcore_control_client.list_gateways()\n", - " for gw in gateways.get(\"items\", []):\n", - " if gw.get(\"name\") == GATEWAY_NAME:\n", - " gateway_id = gw[\"gatewayId\"]\n", - " gw_detail = agentcore_control_client.get_gateway(gatewayIdentifier=gateway_id)\n", - " GATEWAY_ENDPOINT = gw_detail[\"gatewayUrl\"]\n", - " break\n", - " print(f\"[INFO] Gateway already exists: {GATEWAY_NAME}\")\n", - " print(f\" Gateway ID: {gateway_id}\")\n", - " print(f\" Endpoint: {GATEWAY_ENDPOINT}\")\n", - " else:\n", - " print(f\"[ERROR] Failed to create gateway: {e}\")\n", - " print(\"\\nTroubleshooting:\")\n", - " print(\" 1. Ensure your IAM identity has bedrock-agentcore-control permissions\")\n", - " print(\" 2. Check that AgentCore Gateway is available in your region\")\n", - " raise\n" - ] - }, - { - "cell_type": "markdown", - "id": "register-lambda-intro", - "metadata": {}, - "source": [ - "### Register Lambda as Tool Target\n", - "\n", - "Now we register our Lambda function with the gateway as a tool target. This makes the Lambda's tools discoverable by any agent that connects to this gateway via MCP." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "register-lambda-gateway", - "metadata": {}, - "outputs": [], - "source": [ - "# Load tool schemas from file\n", - "with open(\"lambda/tool_schemas.json\", \"r\") as f:\n", - " tool_schemas = json.load(f)\n", - "\n", - "print(f\"Loaded {len(tool_schemas)} tool schemas from lambda/tool_schemas.json\")\n", - "\n", - "try:\n", - " register_response = agentcore_control_client.create_gateway_target(\n", - " gatewayIdentifier=gateway_id,\n", - " name=LAMBDA_FUNCTION_NAME,\n", - " targetConfiguration={\n", - " \"mcp\": {\n", - " \"lambda\": {\n", - " \"lambdaArn\": lambda_arn,\n", - " \"toolSchema\": {\n", - " \"inlinePayload\": tool_schemas\n", - " }\n", - " }\n", - " }\n", - " },\n", - " credentialProviderConfigurations=[\n", - " {\n", - " \"credentialProviderType\": \"GATEWAY_IAM_ROLE\"\n", - " }\n", - " ],\n", - " description=\"Travel domain tools - flights, hotels, car rentals, restaurants, currency, loyalty, weather, activities, trip planning\"\n", - " )\n", - " target_id = register_response.get(\"targetId\", \"registered\")\n", - " print(f\"[OK] Lambda registered with AgentCore Gateway\")\n", - " print(f\" Target ID: {target_id}\")\n", - " print(f\" Tools registered: {len(tool_schemas)}\")\n", - "except Exception as e:\n", - " if \"already exists\" in str(e).lower() or \"conflict\" in str(e).lower():\n", - " print(f\"[INFO] Tool target already registered: {LAMBDA_FUNCTION_NAME}\")\n", - " print(f\" Continuing with existing registration...\")\n", - " else:\n", - " print(f\"[ERROR] Registration failed: {e}\")\n", - " print(\"\\nTroubleshooting:\")\n", - " print(\" 1. Verify the gateway was created successfully\")\n", - " print(\" 2. Ensure your IAM identity has bedrock-agentcore-control permissions\")\n", - " print(\" 3. Verify the Lambda ARN is correct\")\n", - " raise\n" - ] - }, - { - "cell_type": "markdown", - "id": "verify-registration-intro", - "metadata": {}, - "source": [ - "### Verify Registration\n", - "\n", - "Let's verify the registration succeeded by listing all tool targets registered with the gateway. Our Lambda function should appear in the list." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "verify-gateway-registration", - "metadata": {}, - "outputs": [], - "source": [ - "# Wait for registration to propagate, then verify\n", - "print(\"Waiting for target registration to propagate...\")\n", - "time.sleep(5)\n", - "\n", - "try:\n", - " tools_response = agentcore_control_client.list_gateway_targets(\n", - " gatewayIdentifier=gateway_id\n", - " )\n", - " targets = tools_response.get(\"items\", [])\n", - " print(f\"[OK] Gateway has {len(targets)} registered target(s):\")\n", - " for target in targets:\n", - " print(f\" - {target.get('name', 'unknown')}: {target.get('description', 'No description')[:60]}\")\n", - "except Exception as e:\n", - " print(f\"[ERROR] Failed to list gateway targets: {e}\")\n" - ] - }, - { - "cell_type": "markdown", - "id": "agent-creation-header", - "metadata": {}, - "source": [ - "## 6. Create the Strands Agent\n", - "\n", - "Now that our tools are registered with AgentCore Gateway, we'll create a Strands Agent that can discover and invoke them. This involves two components:\n", - "\n", - "1. **MCPClient** - Connects to AgentCore Gateway using IAM-authenticated Streamable HTTP transport\n", - "2. **AgentCoreToolSearchPlugin** - Uses the gateway's built-in `x_amz_bedrock_agentcore_search` tool to semantically discover relevant tools before each model invocation\n", - "\n", - "The plugin hooks into the agent lifecycle: on each invocation, it derives user intent from conversation history, searches the gateway for matching tools, and loads only those tools for the model call. Previously loaded tools are cleared before each search, so the agent always has the most relevant set." - ] - }, - { - "cell_type": "markdown", - "id": "mcp-client-intro", - "metadata": {}, - "source": [ - "### MCPClient\n", - "\n", - "The `MCPClient` connects to AgentCore Gateway using **IAM-authenticated Streamable HTTP** transport. This is how the agent communicates with the gateway to discover and invoke tools. The connection uses AWS Signature V4 for authentication against the `bedrock-agentcore` service." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "create-mcp-client", - "metadata": {}, - "outputs": [], - "source": [ - "from strands.tools.mcp import MCPClient\n", - "from mcp_proxy_for_aws.client import aws_iam_streamablehttp_client\n", - "\n", - "# Create MCP client connected to AgentCore Gateway\n", - "mcp_client = MCPClient(\n", - " lambda: aws_iam_streamablehttp_client(\n", - " endpoint=GATEWAY_ENDPOINT,\n", - " aws_region=AWS_REGION,\n", - " aws_service=\"bedrock-agentcore\"\n", - " )\n", - ")\n", - "\n", - "mcp_client.start()\n", - "print(\"[OK] MCPClient connected to AgentCore Gateway\")\n", - "print(f\" Endpoint: {GATEWAY_ENDPOINT}\")\n", - "print(f\" Region: {AWS_REGION}\")" - ] - }, - { - "cell_type": "markdown", - "id": "tool-search-plugin-intro", - "metadata": {}, - "source": [ - "### AgentCoreToolSearchPlugin\n", - "\n", - "The `AgentCoreToolSearchPlugin` is powered by AgentCore Gateway's built-in **semantic search** capability (`x_amz_bedrock_agentcore_search` tool). On each agent invocation, the plugin:\n", - "\n", - "1. **Derives intent** - An `IntentProvider` sends the last N messages from conversation history to an LLM to produce a concise intent string\n", - "2. **Searches the gateway** - The intent is passed to the gateway's search tool to find matching tools from all registered targets\n", - "3. **Loads tools** - Only the matched tools are loaded for that model call (previously loaded tools are cleared)\n", - "\n", - "By default, the intent classifier reuses the parent agent's model, so no separate model configuration is needed. The result: the agent always has a focused, relevant subset of tools for each request, even when hundreds of tools are registered on the gateway." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "create-agent", - "metadata": {}, - "outputs": [], - "source": [ - "from strands import Agent\n", - "from bedrock_agentcore.gateway.integrations.strands.plugins import AgentCoreToolSearchPlugin\n", - "\n", - "# Create the agent with the AgentCore Tool Search Plugin\n", - "# The plugin uses the gateway's built-in semantic search to find relevant tools\n", - "# By default, intent classification reuses the agent's own model\n", - "agent = Agent(plugins=[\n", - " AgentCoreToolSearchPlugin(\n", - " mcp_client=mcp_client\n", - " )\n", - " ]\n", - ")\n", - "\n", - "print(\"[OK] Strands Agent created with AgentCoreToolSearchPlugin\")\n", - "print(\" Semantic tool search powered by AgentCore Gateway\")\n", - "print(\" Intent classification reuses the agent model\")\n", - "print(\" Tools loaded dynamically before each model invocation\")" - ] - }, - { - "cell_type": "markdown", - "id": "plugin-customization", - "metadata": {}, - "source": [ - "### Customization Options\n", - "\n", - "The `AgentCoreToolSearchPlugin` supports several customization options:\n", - "\n", - "- **Custom model for intent classification** - Use a faster/cheaper model (e.g., Claude Haiku) via `StrandsIntentProvider(model=...)`\n", - "- **Custom system prompt** - Control how intent is derived from conversation history\n", - "- **Custom intent provider** - Subclass `IntentProvider` to implement your own intent derivation logic\n", - "\n", - "For this tutorial, we use the default behavior (agent's own model for intent classification). See the [plugin source](https://github.com/aws/bedrock-agentcore-sdk-python/blob/main/src/bedrock_agentcore/gateway/integrations/strands/plugins/agentcore_tool_search) for full documentation and examples." - ] - }, - { - "cell_type": "markdown", - "id": "invocation-examples-header", - "metadata": {}, - "source": [ - "## 7. Agent Invocation Examples\n", - "\n", - "Now that our agent is assembled, let's invoke it across multiple travel domains to see semantic tool search in action. After each invocation, we log the tools that the plugin loaded for that request - demonstrating how the gateway's `x_amz_bedrock_agentcore_search` tool finds only the relevant tools just before the model call, rather than loading all 34 tools every time.\n", - "\n", - "This is the key benefit: each request gets a focused, relevant subset of tools based on the derived user intent." - ] - }, - { - "cell_type": "markdown", - "id": "flight-search-intro", - "metadata": {}, - "source": [ - "### Flight Search\n", - "\n", - "Let's start by asking the agent to find flights. The `ToolSearchPlugin` will classify this as a flight-related intent and load only the flight domain tools (search_flights, get_flight_details, check_availability, etc.) from the gateway." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "invoke-flight-search", - "metadata": {}, - "outputs": [], - "source": [ - "# Invoke the agent\n", - "response = agent(\"Find flights from San Francisco to New York next Friday\")\n", - "\n", - "# Log tools that were loaded by the plugin for this invocation\n", - "print(\"Tools selected by semantic search for this request:\")\n", - "tools_config = agent.tool_registry.get_all_tools_config()\n", - "for tool_name in sorted(tools_config.keys()):\n", - " print(f\" - {tool_name}\")\n", - "print()\n", - "print(\"Agent Response:\")\n", - "print(response)\n" - ] - }, - { - "cell_type": "markdown", - "id": "hotel-search-intro", - "metadata": {}, - "source": [ - "### Hotel Search\n", - "\n", - "Now let's switch to a completely different domain - hotels. Notice how the `ToolSearchPlugin` reclassifies the intent and selects hotel-related tools (search_hotels, get_hotel_details, check_room_availability, get_hotel_amenities) instead of flight tools." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "invoke-hotel-search", - "metadata": {}, - "outputs": [], - "source": [ - "# Invoke the agent\n", - "response = agent(\"Search for hotels in Manhattan with a pool\")\n", - "\n", - "# Log tools that were loaded by the plugin for this invocation\n", - "print(\"Tools selected by semantic search for this request:\")\n", - "tools_config = agent.tool_registry.get_all_tools_config()\n", - "for tool_name in sorted(tools_config.keys()):\n", - " print(f\" - {tool_name}\")\n", - "print()\n", - "print(\"Agent Response:\")\n", - "print(response)\n" - ] - }, - { - "cell_type": "markdown", - "id": "car-rental-intro", - "metadata": {}, - "source": [ - "### Car Rental\n", - "\n", - "Next, let's search for car rentals. The plugin will identify this as a car rental intent and load the relevant tools (search_car_rentals, get_rental_details, check_car_availability) from the gateway." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "invoke-car-rental", - "metadata": {}, - "outputs": [], - "source": [ - "# Invoke the agent\n", - "response = agent(\"Find available car rentals at JFK airport for next week\")\n", - "\n", - "# Log tools that were loaded by the plugin for this invocation\n", - "print(\"Tools selected by semantic search for this request:\")\n", - "tools_config = agent.tool_registry.get_all_tools_config()\n", - "for tool_name in sorted(tools_config.keys()):\n", - " print(f\" - {tool_name}\")\n", - "print()\n", - "print(\"Agent Response:\")\n", - "print(response)\n" - ] - }, - { - "cell_type": "markdown", - "id": "restaurant-search-intro", - "metadata": {}, - "source": [ - "### Restaurant Search\n", - "\n", - "Let's try the restaurant domain. The plugin will select restaurant tools (search_restaurants, get_restaurant_details, get_menu, check_reservations, get_restaurant_reviews) for this request." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "invoke-restaurant-search", - "metadata": {}, - "outputs": [], - "source": [ - "# Invoke the agent\n", - "response = agent(\"Find Italian restaurants near Times Square with good reviews\")\n", - "\n", - "# Log tools that were loaded by the plugin for this invocation\n", - "print(\"Tools selected by semantic search for this request:\")\n", - "tools_config = agent.tool_registry.get_all_tools_config()\n", - "for tool_name in sorted(tools_config.keys()):\n", - " print(f\" - {tool_name}\")\n", - "print()\n", - "print(\"Agent Response:\")\n", - "print(response)\n" - ] - }, - { - "cell_type": "markdown", - "id": "currency-conversion-intro", - "metadata": {}, - "source": [ - "### Currency Conversion\n", - "\n", - "Now let's try the currency domain. The plugin will identify this as a currency-related intent and load the currency tools (convert_currency, get_exchange_rates, get_supported_currencies)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "invoke-currency-conversion", - "metadata": {}, - "outputs": [], - "source": [ - "# Invoke the agent\n", - "response = agent(\"Convert 500 USD to EUR and show me the current exchange rate\")\n", - "\n", - "# Log tools that were loaded by the plugin for this invocation\n", - "print(\"Tools selected by semantic search for this request:\")\n", - "tools_config = agent.tool_registry.get_all_tools_config()\n", - "for tool_name in sorted(tools_config.keys()):\n", - " print(f\" - {tool_name}\")\n", - "print()\n", - "print(\"Agent Response:\")\n", - "print(response)\n" - ] - }, - { - "cell_type": "markdown", - "id": "loyalty-program-intro", - "metadata": {}, - "source": [ - "### Loyalty Program\n", - "\n", - "Finally, let's check the loyalty program domain. The plugin will select loyalty tools (get_loyalty_balance, redeem_points, get_loyalty_program_info) for this request." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "invoke-loyalty-program", - "metadata": {}, - "outputs": [], - "source": [ - "# Invoke the agent\n", - "response = agent(\"Check my loyalty points balance and what rewards I can redeem\")\n", - "\n", - "# Log tools that were loaded by the plugin for this invocation\n", - "print(\"Tools selected by semantic search for this request:\")\n", - "tools_config = agent.tool_registry.get_all_tools_config()\n", - "for tool_name in sorted(tools_config.keys()):\n", - " print(f\" - {tool_name}\")\n", - "print()\n", - "print(\"Agent Response:\")\n", - "print(response)\n" - ] - }, - { - "cell_type": "markdown", - "id": "cleanup-header", - "metadata": {}, - "source": [ - "## 8. Cleanup\n", - "\n", - "We need to delete all resources created during this tutorial to avoid ongoing costs. Each cleanup step is independent - if one fails, others will still execute." - ] - }, - { - "cell_type": "markdown", - "id": "cleanup-explanation", - "metadata": {}, - "source": [ - "### Remove All Created Resources\n", - "\n", - "We'll stop the MCP client connection and clean up all AWS resources created in this notebook: the Lambda function, its IAM execution role, and the AgentCore Gateway tool target registration." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cleanup-resources", - "metadata": {}, - "outputs": [], - "source": [ - "print(\"Cleaning up resources...\")\n", - "print()\n", - "\n", - "# 1. Stop MCP Client\n", - "try:\n", - " mcp_client.__exit__(None, None, None)\n", - " print(\"[OK] MCPClient connection stopped\")\n", - "except Exception as e:\n", - " print(f\"[WARNING] Error stopping MCPClient: {e}\")\n", - "\n", - "# 2. Deregister from AgentCore Gateway\n", - "try:\n", - " agentcore_control_client.delete_gateway_target(\n", - " gatewayIdentifier=gateway_id,\n", - " targetId=target_id\n", - " )\n", - " print(\"[OK] Deregistered tool target from gateway\")\n", - "except Exception as e:\n", - " print(f\"[WARNING] Error deregistering from gateway: {e}\")\n", - "\n", - "# 3. Delete the AgentCore Gateway (wait for target deletion to propagate)\n", - "import time\n", - "time.sleep(10)\n", - "try:\n", - " agentcore_control_client.delete_gateway(gatewayIdentifier=gateway_id)\n", - " print(f\"[OK] Deleted AgentCore Gateway: {GATEWAY_NAME}\")\n", - "except Exception as e:\n", - " print(f\"[WARNING] Error deleting gateway: {e}\")\n", - "\n", - "# 4. Delete Lambda function\n", - "try:\n", - " lambda_client.delete_function(FunctionName=LAMBDA_FUNCTION_NAME)\n", - " print(f\"[OK] Deleted Lambda function: {LAMBDA_FUNCTION_NAME}\")\n", - "except Exception as e:\n", - " print(f\"[WARNING] Error deleting Lambda function: {e}\")\n", - "\n", - "# 5. Detach policy and delete Lambda IAM role\n", - "try:\n", - " iam.detach_role_policy(\n", - " RoleName=LAMBDA_ROLE_NAME,\n", - " PolicyArn=\"arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole\"\n", - " )\n", - " iam.delete_role(RoleName=LAMBDA_ROLE_NAME)\n", - " print(f\"[OK] Deleted IAM role: {LAMBDA_ROLE_NAME}\")\n", - "except Exception as e:\n", - " print(f\"[WARNING] Error deleting Lambda IAM role: {e}\")\n", - "\n", - "# 6. Detach policy and delete Gateway IAM role\n", - "try:\n", - " iam.detach_role_policy(\n", - " RoleName=GATEWAY_ROLE_NAME,\n", - " PolicyArn=\"arn:aws:iam::aws:policy/service-role/AWSLambdaRole\"\n", - " )\n", - " iam.delete_role(RoleName=GATEWAY_ROLE_NAME)\n", - " print(f\"[OK] Deleted IAM role: {GATEWAY_ROLE_NAME}\")\n", - "except Exception as e:\n", - " print(f\"[WARNING] Error deleting Gateway IAM role: {e}\")\n", - "\n", - "print()\n", - "print(\"Cleanup complete! All resources have been removed.\")\n" - ] - }, - { - "cell_type": "markdown", - "id": "cleanup-summary", - "metadata": {}, - "source": [ - "### Conclusion\n", - "\n", - "In this notebook you built a Strands agent that uses the `AgentCoreToolSearchPlugin` to leverage AgentCore Gateway's semantic tool search. The plugin dynamically discovers and loads only the relevant tools before each model invocation - enabling efficient tool selection from 34 registered options without overwhelming the agent.\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.14.5" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/03-integrations/gateway/agentcore-tool-search-plugin/cleanup.py b/03-integrations/gateway/agentcore-tool-search-plugin/cleanup.py new file mode 100644 index 000000000..38d198caf --- /dev/null +++ b/03-integrations/gateway/agentcore-tool-search-plugin/cleanup.py @@ -0,0 +1,96 @@ +""" +Delete all AWS resources created by this sample. + +Usage: + python cleanup.py +""" + +import json +import sys +import time + +import boto3 + +from config import ( + AWS_REGION, + GATEWAY_NAME, + GATEWAY_ROLE_NAME, + LAMBDA_FUNCTION_NAME, + LAMBDA_ROLE_NAME, + STATE_FILE, +) + + +def cleanup(): + if not __import__("os").path.exists(STATE_FILE): + print("[ERROR] No deployment state found. Nothing to clean up.") + sys.exit(1) + + with open(STATE_FILE) as f: + state = json.load(f) + + print("=" * 60) + print("CLEANING UP RESOURCES") + print("=" * 60) + + iam = boto3.client("iam", region_name=AWS_REGION) + lambda_client = boto3.client("lambda", region_name=AWS_REGION) + agentcore_control = boto3.client("bedrock-agentcore-control", region_name=AWS_REGION) + gateway_id = state["gateway_id"] + + # Deregister tool targets + try: + targets = agentcore_control.list_gateway_targets(gatewayIdentifier=gateway_id) + for target in targets.get("items", []): + agentcore_control.delete_gateway_target( + gatewayIdentifier=gateway_id, targetId=target["targetId"] + ) + print(" [OK] Deregistered tool targets from gateway") + except Exception as e: + print(f" [WARN] Error deregistering targets: {e}") + + # Delete gateway + time.sleep(10) + try: + agentcore_control.delete_gateway(gatewayIdentifier=gateway_id) + print(f" [OK] Deleted gateway: {GATEWAY_NAME}") + except Exception as e: + print(f" [WARN] Error deleting gateway: {e}") + + # Delete Lambda function + try: + lambda_client.delete_function(FunctionName=LAMBDA_FUNCTION_NAME) + print(f" [OK] Deleted Lambda: {LAMBDA_FUNCTION_NAME}") + except Exception as e: + print(f" [WARN] Error deleting Lambda: {e}") + + # Delete Lambda IAM role + try: + iam.detach_role_policy( + RoleName=LAMBDA_ROLE_NAME, + PolicyArn="arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ) + iam.delete_role(RoleName=LAMBDA_ROLE_NAME) + print(f" [OK] Deleted IAM role: {LAMBDA_ROLE_NAME}") + except Exception as e: + print(f" [WARN] Error deleting Lambda role: {e}") + + # Delete Gateway IAM role + try: + iam.detach_role_policy( + RoleName=GATEWAY_ROLE_NAME, + PolicyArn="arn:aws:iam::aws:policy/service-role/AWSLambdaRole", + ) + iam.delete_role(RoleName=GATEWAY_ROLE_NAME) + print(f" [OK] Deleted IAM role: {GATEWAY_ROLE_NAME}") + except Exception as e: + print(f" [WARN] Error deleting Gateway role: {e}") + + # Remove state file + __import__("os").unlink(STATE_FILE) + print() + print(" [OK] Cleanup complete. All resources removed.") + + +if __name__ == "__main__": + cleanup() diff --git a/03-integrations/gateway/agentcore-tool-search-plugin/config.py b/03-integrations/gateway/agentcore-tool-search-plugin/config.py new file mode 100644 index 000000000..fb6945d21 --- /dev/null +++ b/03-integrations/gateway/agentcore-tool-search-plugin/config.py @@ -0,0 +1,12 @@ +"""Shared configuration for the AgentCore Tool Search Plugin sample.""" + +import os + +AWS_REGION = os.environ.get("AWS_REGION", "us-east-1") +LAMBDA_FUNCTION_NAME = "agentcore-travel-tools" +LAMBDA_ROLE_NAME = "agentcore-travel-tools-role" +GATEWAY_NAME = "agentcore-travel-gateway" +GATEWAY_ROLE_NAME = "agentcore-travel-gateway-role" +MODEL_ID = os.environ.get("MODEL_ID", "us.anthropic.claude-sonnet-4-20250514-v1:0") +STATE_FILE = os.path.join(os.path.dirname(__file__), ".deploy_state.json") +LAMBDA_DIR = os.path.join(os.path.dirname(__file__), "lambda") diff --git a/03-integrations/gateway/agentcore-tool-search-plugin/deploy.py b/03-integrations/gateway/agentcore-tool-search-plugin/deploy.py new file mode 100644 index 000000000..3296dde99 --- /dev/null +++ b/03-integrations/gateway/agentcore-tool-search-plugin/deploy.py @@ -0,0 +1,258 @@ +""" +Deploy Lambda function, create AgentCore Gateway, and register tools. + +Usage: + python deploy.py +""" + +import io +import json +import time +import zipfile + +import boto3 + +from config import ( + AWS_REGION, + GATEWAY_NAME, + GATEWAY_ROLE_NAME, + LAMBDA_DIR, + LAMBDA_FUNCTION_NAME, + LAMBDA_ROLE_NAME, + STATE_FILE, +) + + +def deploy(): + print("=" * 60) + print("STEP 1: Verify AWS Credentials") + print("=" * 60) + + sts = boto3.client("sts") + identity = sts.get_caller_identity() + print(f" Account: {identity['Account']}") + print(f" ARN: {identity['Arn']}") + print(f" Region: {AWS_REGION}") + print() + + iam = boto3.client("iam", region_name=AWS_REGION) + lambda_client = boto3.client("lambda", region_name=AWS_REGION) + agentcore_control = boto3.client("bedrock-agentcore-control", region_name=AWS_REGION) + + # --- Create Lambda execution role --- + print("=" * 60) + print("STEP 2: Create Lambda Execution Role") + print("=" * 60) + + trust_policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "lambda.amazonaws.com"}, + "Action": "sts:AssumeRole", + } + ], + } + + try: + role_response = iam.create_role( + RoleName=LAMBDA_ROLE_NAME, + AssumeRolePolicyDocument=json.dumps(trust_policy), + Description="Execution role for AgentCore travel tools Lambda", + ) + role_arn = role_response["Role"]["Arn"] + iam.attach_role_policy( + RoleName=LAMBDA_ROLE_NAME, + PolicyArn="arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ) + print(f" [OK] Created IAM role: {LAMBDA_ROLE_NAME}") + print(" Waiting 10s for IAM propagation...") + time.sleep(10) + except iam.exceptions.EntityAlreadyExistsException: + role_arn = f"arn:aws:iam::{identity['Account']}:role/{LAMBDA_ROLE_NAME}" + print(f" [INFO] IAM role already exists: {LAMBDA_ROLE_NAME}") + print() + + # --- Deploy Lambda function --- + print("=" * 60) + print("STEP 3: Deploy Lambda Function") + print("=" * 60) + + zip_buffer = io.BytesIO() + with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf: + zf.write(f"{LAMBDA_DIR}/travel_tools.py", "travel_tools.py") + zip_buffer.seek(0) + + try: + response = lambda_client.create_function( + FunctionName=LAMBDA_FUNCTION_NAME, + Runtime="python3.12", + Role=role_arn, + Handler="travel_tools.lambda_handler", + Code={"ZipFile": zip_buffer.read()}, + Description="Travel domain tools for AgentCore Gateway", + Timeout=30, + MemorySize=256, + ) + lambda_arn = response["FunctionArn"] + print(f" [OK] Created Lambda: {LAMBDA_FUNCTION_NAME}") + except lambda_client.exceptions.ResourceConflictException: + zip_buffer.seek(0) + lambda_client.update_function_code( + FunctionName=LAMBDA_FUNCTION_NAME, ZipFile=zip_buffer.read() + ) + func_info = lambda_client.get_function(FunctionName=LAMBDA_FUNCTION_NAME) + lambda_arn = func_info["Configuration"]["FunctionArn"] + print(f" [INFO] Updated existing Lambda: {LAMBDA_FUNCTION_NAME}") + print(f" ARN: {lambda_arn}") + + # Wait for Lambda to become Active + print(" Waiting for Lambda to become Active...") + waiter = lambda_client.get_waiter("function_active_v2") + waiter.wait(FunctionName=LAMBDA_FUNCTION_NAME) + print(" [OK] Lambda is Active") + + # Verify deployment + test_event = {"tool_name": "get_supported_currencies"} + resp = lambda_client.invoke( + FunctionName=LAMBDA_FUNCTION_NAME, Payload=json.dumps(test_event) + ) + payload = json.loads(resp["Payload"].read()) + print(f" [OK] Verified: {payload['total']} currencies available") + print() + + # --- Create Gateway --- + print("=" * 60) + print("STEP 4: Create AgentCore Gateway") + print("=" * 60) + + gateway_trust_policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "bedrock-agentcore.amazonaws.com"}, + "Action": "sts:AssumeRole", + } + ], + } + + try: + gw_role_response = iam.create_role( + RoleName=GATEWAY_ROLE_NAME, + AssumeRolePolicyDocument=json.dumps(gateway_trust_policy), + Description="Role for AgentCore Gateway to invoke Lambda targets", + ) + gateway_role_arn = gw_role_response["Role"]["Arn"] + iam.attach_role_policy( + RoleName=GATEWAY_ROLE_NAME, + PolicyArn="arn:aws:iam::aws:policy/service-role/AWSLambdaRole", + ) + print(f" [OK] Created gateway role: {GATEWAY_ROLE_NAME}") + time.sleep(10) + except iam.exceptions.EntityAlreadyExistsException: + gateway_role_arn = f"arn:aws:iam::{identity['Account']}:role/{GATEWAY_ROLE_NAME}" + print(f" [INFO] Gateway role already exists: {GATEWAY_ROLE_NAME}") + + try: + gateway_response = agentcore_control.create_gateway( + name=GATEWAY_NAME, + roleArn=gateway_role_arn, + authorizerType="AWS_IAM", + protocolType="MCP", + protocolConfiguration={"mcp": {"searchType": "SEMANTIC"}}, + description="Travel domain gateway for semantic tool search demo", + ) + gateway_id = gateway_response["gatewayId"] + gateway_endpoint = gateway_response["gatewayUrl"] + print(f" [OK] Created gateway: {GATEWAY_NAME}") + except Exception as e: + if "already exists" in str(e).lower() or "conflict" in str(e).lower(): + gateways = agentcore_control.list_gateways() + for gw in gateways.get("items", []): + if gw.get("name") == GATEWAY_NAME: + gateway_id = gw["gatewayId"] + gw_detail = agentcore_control.get_gateway(gatewayIdentifier=gateway_id) + gateway_endpoint = gw_detail["gatewayUrl"] + break + print(f" [INFO] Gateway already exists: {GATEWAY_NAME}") + else: + raise + + print(f" Gateway ID: {gateway_id}") + print(f" Endpoint: {gateway_endpoint}") + + # Wait for gateway to become ACTIVE + print(" Waiting for gateway to become ACTIVE...") + for _ in range(60): + gw_detail = agentcore_control.get_gateway(gatewayIdentifier=gateway_id) + status = gw_detail.get("status", "UNKNOWN") + if status == "ACTIVE" or status == "READY": + break + time.sleep(5) + else: + print(f" [WARN] Gateway still in {status} state after timeout") + print(f" [OK] Gateway is {status}") + print() + + # --- Register tools --- + print("=" * 60) + print("STEP 5: Register Lambda as Tool Target") + print("=" * 60) + + with open(f"{LAMBDA_DIR}/tool_schemas.json", "r") as f: + tool_schemas = json.load(f) + + print(f" Loaded {len(tool_schemas)} tool schemas") + + try: + agentcore_control.create_gateway_target( + gatewayIdentifier=gateway_id, + name=LAMBDA_FUNCTION_NAME, + targetConfiguration={ + "mcp": { + "lambda": { + "lambdaArn": lambda_arn, + "toolSchema": {"inlinePayload": tool_schemas}, + } + } + }, + credentialProviderConfigurations=[ + {"credentialProviderType": "GATEWAY_IAM_ROLE"} + ], + description="Travel domain tools - flights, hotels, car rentals, restaurants, currency, loyalty, weather, activities, trip planning", + ) + print(f" [OK] Registered {len(tool_schemas)} tools with gateway") + except Exception as e: + if "already exists" in str(e).lower() or "conflict" in str(e).lower(): + print(" [INFO] Tool target already registered") + else: + raise + + # Verify registration + time.sleep(5) + targets = agentcore_control.list_gateway_targets(gatewayIdentifier=gateway_id) + print(f" [OK] Gateway has {len(targets.get('items', []))} registered target(s)") + print() + + # Save state for invoke/cleanup + state = { + "gateway_id": gateway_id, + "gateway_endpoint": gateway_endpoint, + "lambda_arn": lambda_arn, + } + with open(STATE_FILE, "w") as f: + json.dump(state, f) + + print("=" * 60) + print("DEPLOYMENT COMPLETE") + print("=" * 60) + print(f" Gateway endpoint: {gateway_endpoint}") + print(f" Tools registered: {len(tool_schemas)}") + print() + print(" Run 'python invoke.py' to test the agent") + + +if __name__ == "__main__": + deploy() diff --git a/03-integrations/gateway/agentcore-tool-search-plugin/images/architecture.png b/03-integrations/gateway/agentcore-tool-search-plugin/images/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..4e29dc45ca9a3a8ff608e22f6b56ff44d18b7396 GIT binary patch literal 143034 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

$7u<>t836iJfPsDc-&x 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

G^m(YTBc;2THvqK$KWACKp0>> z$ACYbMGaT9o}QlT>f=CI9q4~&DCeswNd)7Ofqpz;hd%~HH{36Oj_x<1kRZK%!D=F( znEvEn4_9wI(OYEtmjdgwyz*nir0FB1B$+L1O8kUTcs6YVE$1>nU*oR zZoo(XY{!^}n;s`I9A0qwSZb*C=kOv{auWX;dK93;J1gS7>&MrEi%E^0InHvBulh$) z+L2e;E&3g<^NKxAaF4(Du#kCP*M>$_cNS@|?4^Zt3P;)AEsUouL~JuLZt?+|kI`WM zLhXvmP*;6|Uu#Y3Z6>a_ztmyB)Q$z%pdm)RN$TI)L1v)?B-)Ios~r)&zi8JuMS@C@ zwdtCQn~G?StF4OJP5?Q#Jc|WN=2E`M7;=^Ibn~xwEDu4Z5qDz_EvChtnp$3@6Mwt=5Sv3~Qw@6-%6T8ZU!L{@UAj(~X>W2= zYtLlI8HOfhV<{RV?~L(^#f1CP=yQ!SGb1EfrYbW&`oywLT3z?kk5g!%RfWbh28E`s ztCKlJJ=#K7*~Vz5;h<>f0b`Q%OfHM3f-#S<7hg4@Os z!+1?t+H%x2T4)0Pi_+l7g`3g8#!O7$4U$wZL74>UP2(^$G91-dpm6(r4QE47;s`@nn0f)cj>- zJq7WwnhW8t1q|3mB7MF+UZOjbw@O_T;=gPaH6h-UDnxE-Og;-^HPYkEQ7+DRl_lnN z(wZpWdD37xt8cGXy&P4`xklOA@K)#VV852WtuU3-iSox6$=&;+SHj z8Oo4$Y0$+Y|9 z4d>D*-)UUo@^oACR-7bh20HPMB4n&-A|xjfCS)oG301g6oRy(Bw$_`YHmd^fv#azJtcu!iD50E;yDZ5 zE*0h zM52uS+LkEcyH*?K`lxiS%G0cdvfhskq}tV(AIb!+V!WQ$J|QEg3Lu$<`4-THJg%p< z^Kf6H`^jp7V)3CTC4jCKt?`UjI_t<2v1xyO*kaITPAZ=F`d-!i z+l|82EPTyyq|VH)SmSIg#wdX(wV49-VW@g5)+`KdseAmONx|^BUig(Kx=K3+_HOts zaal~^;>W)ZqOT7SfeuUrkS3_|)2wrKZ0ec@Oqpfc&P2{4i#m{>W(9TT{y{O4VZ_6U zqY&|f)xMqAJM})UlNC0xnibaOTod`~CDpl&h7it1tY!}Jpa<(62hI1Gez*df&MUol z{(Zpg89?K_4~J_*H4_GRW5fmCmx{}v{rbV=P>F9xcN}gSD61n6kJezRtlLDlliO(a(YG78mtZp+_;`VC-o7=-Sn3Gb$U-ZB0+1 zg4`7K@{X+5D)QP-ry&W)<@A^kO5hR~YzoKE37YI@L{MWmwNqd3jBv7#6xED1NjfSg8pyMyAB=v)!GV9+oo1+2bDf<8{ z!`a>WmY$dK2wyxeUDfMc%SOzntIMi{0(Ww+JL!tr!J)Q3ao_4rni(h{)2<LlM@~)m7{IcXU$P69fCpP>t`|5JCMIwJM)0qzEto_Czd5j7o!6H^X_5W)rYS zGXcx*3v|ywRre92R`{rnn|#aE_H2Xhw`MB&1T(=x8IwE7SwO|f_96cpqD5gEG5G%e zek6z=Izf#t4G>L_K*OP`oXC=imI0XHr$>=-DCpe$#XzAU@f43h%n&tdq|stH_mu{w zG3JJ#WGJCefBoETUsIehP?REFaVl|hjffD}YY!(o(=G<}tR7sqF{S5H(XzdYL28y`G`a2FK2SCsbzl9`O4q# zdi`oIGj6dyd*1A0~rK3=D zb(8fyo{b*y453Wr{@k#ayD4av(Vb8tXFXlDs_)_)U=ke8AYNNno4KHwnW-9f-!rl)(AOL-gRK01d4su3JqdywfW2pN>R(zwNWv{KSQtv(v%AOxXP2>d(cU;oRQuB231+a^sfXqB% z->ZO#cHC-*RfV6QVa`waRLRuHyUA_t#^m8gE<5_0Pub1D z=5jkqIw{+xtvMpB-PkkB5Qx>wp)01;(IUZrmJzF*bNKmgjB24y^k)X1&gzu%Y*j-u zOYE=j={)7vrQe%iC`;A4p9YnlWGB+=l*=H7P#c-~eP9WZyWd5utwGSl>b(Nr9Q5## zODXP0jX_nlFp|j=F?gc#DJW@}E;d@J=_`#6M0=Avsd7@btDY5>-s$D=XtanJOc?%kZ|;)Yaa|cBjK{e10Y-c-?si?% z2KMQI?ZKq^%nI7`DMEh_Amvx64Z+lhJjEp9$?yUhVZ#7pq>ng zJk%djURjJOnym&~C{F$i3C4Q-&=?#Wsy2h5^4Ow%2JqUYbCq+*7u&){%YXElA8(L2 zCWB@wpgI@ttYSl>k@F^m2_Qw_GxFwZ+g<2Q=C3(IE^@}Z@>~=~JAeH>J3^>ZE|&45 z-?a}4Pi@1tIBv8P@gn<#vH-_qRKatzvvxEq|q=nnr)Bjb0#Y}*)uw1B@U4cyQIJ%s9+(Ec^g+7>s{#Y`;qPN88=PDdu*R4 zBF&(#U1`F3jY-gIioBx7hF6JuAUDj+d|xMlgCSugHgroP?uVoRt(qsRQ9iwTz0AdJ zYl5mlsqoOGShcW4TuD&1Lc?EEF%%$OVS~~hXZLcch~gTRoPPRe%Y}_mPDt#sEYr$a zk(UJ}1Q`(>lK@#QU*;E#GVt-%zEJahV!rI=1czGGw)MkYu~6|P6|v-RH+_zAw{ep> zfak9kqJLAtOI3=_XFJCq&oKE`Q6X&y6ctyCWRV9y)!7Y)F|V@+q(W$}FOPLqS**Rz z&(5mLr&R%Wh`%nP9;g`fr20*TRtn!&RGMOM3wfmmNKo2_wnzF0@;qJ5B|4x$Rh2FH z(ZNl5xt8%Z-BBgvcH`J%nDH1;tg(t*G*B2WBrIthTN1ObV6(Nr(4u$Ac489f{w#69rQAT_6 zPfCM(*P%B&>N+`+xK1D4>ODAzD?GpXA%VD)R%le`!H^IxFuK-UZ zTVZc^e+cC~M!7r4uwSU+zom)LYu?#=R$11Dj#T$v(vyN@Gyl?iQF7i>i()TB#m~i(prR#pLX3 zhKKJ*aHh`!&1;;~4~ne;PM$$cN$nhSB#z)WZ;XK0ZXE2dXuH@w(Gf-Mh>K~~NI{~r zb;A2@ckXDXqTu~bGH3wzT=c#F5s|kmDn&Avoq?6jwYgj7scaSy=>jHQ@6e)?Rh=u#Uzfg_Z)r4v3k?Z$^ZAr`lHGeDv(0WSH+>GqL@2n!K{Jk)o3zH@&iAl11FUbhX z@$Vsn7RF$eq+D39m}}XJ4Z3OjNA4y(oBnT60Rnq!Xzk zv2@|mb##6=CSd&_LRZw>gpVpBaeokGB)(Xws@IvM167N@W0(Ri{SI+Y8+9mq_I2nR zvq`x+2AlYu9vu(A&NO4XWxozn^EQ*mdGi&-XN4vr&7KDKmS<;F%nj0rM!i!{Bz_QP z>Fe+GHkVh{$VIchL$lEwfqJ&0)%$8v&OJqlatHvC7epTQqTWWg|IoP0$9k$3UtZ#? z{_pu8fO1h$nunuBJvKrr{^aH5MT%37tIAN#UIp>({`NAw$_3wOU+OjYQ9L}KXBhOJ zxf4&|D=hH?-En=THK$ydD4(}y>eG;iL^e8WT4rHlVtX%bb*N!tVp6rW_0?%k>M&#~CM!JEp+DZu{0*be4J zokPOj%&2EuF#aNK`CHztJvittbzbMYQ;cvMxw6%81GMxNZ1-gP#1@QlK>H#iVC0*p zBF1bgRjCWyaL(<%+~({hyt`vbe`Ig0*ucCwZ{s+VCS8dPq{cFt1qD1 z1+t3a&}f5YA)z+fmsP?)BF}X~N+C1xdIB>}-N7`3qqncBpzX=cx%yQZ@!;(&wz9=h z*4f#c#e_JX5Zf@f+4|eUMX@;#Kd^4S7w#B(chwpG%%=b1o$e1u-utjwE~1*ldoqb@ z(dXqIhgw&k1{SFjIh@~EuJ-Dp?pq8o(1@S$eeRKUp5!OEfab8(V}Gxi-6dTN$+5CN zJ4bQcV4~|c|T_YzMTkXtg6J1e$N+BEkfZH0155Y#?<0W%l0Fd zY}iE-S1^Wp_?dd1F#0^ABgfXW-qjKN@d{eeEIK?5T8Yz2Oby%Cp!VEr;VWp0!gTm{ zoR4K}wdWCgbUrf8%TbCv=U?0e*?*+(OJao2YV=$H?>*MDVI16<54)T6y7jfLIZoSC z>TfCVu#{?OHA=Hx6M4=Zd-0ds--rCT+fEU3;Qt70Lc$5pIW$WonPp~Bn$BPDg4!8< z%|kyMeL}ApBSAWHQE*~q*oWCGSia}tGhgvgAcYw=(`Nd`Zn4z|_1>=6H=Z<+H#;-h z83Ya3m;DI!t*uuF!1@)iYTQcrP2lDk~uW}LKA2z zRNO13nUjcIBW?Fcx$+-}s{Nk>`uk zmPTEC0RG%(0x`+By$?83TwM_q{&Rk0*55H6vr8RM?_h8u=>xH?8vyqQ2H&DQB%g<| z8QkkLjUfbfQJ9ECA{%728f~FOB#+Y=)%;CvpVCIOU~@9+_wL%YMv2-QKH?lnWH&cp zJQ|VOnW_7UOug=TI3TGeRyDuEV7;Q4z%I>g7>w#~$`^oSKqo>(?oQqsL{M9aMe^|W zZ9MDGlSzj9=6%&SL58g$BC4~ws8@RL>A|Y@yaqrT=)eaL?VzHvo}AxtSdnh!Sf5M% z5CHB5Ev%-!^uV?U{mn=Zg)fT9A0RQ{!T16sp+ZPxm3zldpep}yq%z%{#^=K zJ~gr(wu9(f(2RKm#So%1EylW=;S#`el8Djbj3CAdBNi`|LM=h%f8U>>&~gml>;Ar@ z9uUhM@Bu=nkLvb*$KJhxSF~iK7qy;D@M&cW{OgV{3cjbq7z#k(rPEUNm5^0V>Va@V z)m5PoRQo~&Ptdd0$$u78!J!wQoT-;u?0JAUq-Bp)Cv(CGfMwN5%=$iz1Jw04STPSV zEyq!clP(1)XmLMU(+%_Hwwu`)e%aqq>MnWu7c?5F0iQ#$L znNymX!Hug0`UWGb8z!@WvK2=B(uw z{9@$4z?EDeJ0Pq;1x3wQD;AOCdL}$Wv!u{0F9y{U(QA?ChTi=|Y^~A~bxS7!lNI`Y z4wHyH$SK5shEE|H^_a3KncrtUt%45<_iy2vX)mg z4p$*Qq_>OF5i#~J)P4I+9Rw%7?9zkgf0;c%AtV*kbxt1_)eoYu$BA!1qyv%M^O$3I ze>=~6pZH1z+H!m=4ITCksCwrQ10EFqCIUfX8!Np>Dx;VLo|rZrOrOB#5&CRj3~%|C zGu_C1G^QO6Iq_Hdr6cxK>0&xHeg@mxCWnW!r_8%P^A}wF*8nHl#{N_$LRmk~7rR^e zrgz*mccM1_D7=?@(%*jfQz$N#6+gYzEK~}07r}d9 zF7oSO+Y&tAlga;y=j>cl#$uadr(a6NbeHsL%1OXwef?f$h%QA`)R*$RfsdH9B9kxu zyN(4fGF+(SVSAA9{?AT5or4og^>SvlJVm+NsYc}mWP9}2%-$_R6L*R_ zK~BRhmSe@0Ehp#$D2I(EqY#KnysFT~4ChWjL+UaR-|rezy1a8rZ%zqO0=ZraneeS& z*=f=QY?|kvy!;G(O)0G%}0Wzf%(W_H$cox!(z7=p3GK`E{mE7tsq~nb|<|`1dmFZ$b}@IwF_r zG21I2R~D%0Gnfx%G#$;6;RbxdWIN^-riXEp=-3_U@0;>)-kAvn{*pmVHXs_pN!Bi0 zDjqj{j!&E>e4?I32BXy(A-!My-rPx7EEYBDZcF8!eFaL-_Pe(p)%{ zlp<^DqBd%E-3#yd(c;NSdN+}9W}@KSSDB_XIdMA$Wz%hJb?Wk(aH9YBc+7wpf$wW1 zzvjbJ%T4P2BR6?D%J;IYa2v^-*CNdZDP^~C?mIOtFfFypySLqWL~8d@`zQo^aH++0 z#aTtG%6&Pvbh}@){&G9Z4fwReFjR2@%DwJqwjZ5yWx}kjh9?J?;2*Z@kh>N#b-OSq z1akG(xX?opFuP=yWxKNVJzn9}Ic}79Yr~b1F^H%utXs~92IgiO_ag{z;{}K1x)SWK)VMz6ldz?M|xN$8Lf;Sj4-$x{f4$j-_{6;_9k#>S$f7yIf zlJ>dzdmBBW4JFKcAAELj`PBBAOUM+g_yf`ud`a&Jqv)(d%6Gjm#CZ8my!#i9yfIKS zDqU9GQ@270ILR-ZNBkb<`6%=ENn1;(7k%-6p_$Yd{1E|>U{#_7mnbRH{^ zh3t^GmV*Tu_Sku5v(#}58|xv+>lHB3DK8LCveNI>J$U7}qk%`x#kGpA62}GvUHS$y zq||r$ZvG#g!pj;UtFFYmrnH}B*PU-l9M5WTdM;x+#6&|0^LW8 zQ^)YWsuIT0-#0M;kF*|{bjImC?1`sjqY6PKQ!{X?=obA32IWRk$sEwNd9vr8A?vR~ ztM(hQz>$P>w!lvht$483M>_~5XVtKz^=F2Ie+;c!+RoK&$;VChnOO9fPiR#10tjPv z?-ppB%H#9FYlu-Wv*qKDstz~VfjfH6Ewy)7-VQ_6?tju-yYD}VMGnY3A}3>U zzf#Et$Ouz$FwQ3;nF~}CARCgcLyy{KVsrK_I3D))14Mql?g0MC-gfZD+(F{;mVTz= z#>l=mS_=ltmSfbxSdbNjf=ow*&v!NL8NoWiydajr#bTIXd5sAD9;Lwk)p&nO_AP|2 z=^?_IDCo&;9n0NMblTOWD`*lP(49>AM3r-I6kg)`Vn0;Gc3d<&%*C((G9Co<<^nbn zvht5BYe|+rMoBzRn#Q$60}VsB(=_2SCb!$nfudNDBXk$_2wock@!b1_u5)tbJ5M;l zO5d`MoA(iV;s@VjHs#W=>;p;KKDvFa**<24F`&rc`^vj9gr(u2msi8Oj$p5DoBi03 z=h)?owN3YgZR7T3k!FQLO_Q&t%E>!b_<@6e68t21fsDf4WO_?K#o_pDit0C5EZ`K0 z)dZ>%l)#*u%Kzrg*7`GUQ+wrV0%EnE*kfZC4(-P{Dp$FO%;XC|c<#w=sHoVR{ASJl z%W9i0$pV7JSy{B67A?+9n}&H-!xfP#JEj-H^pYNgia>=pI)d!BTig%vz&pe-I9YLo zF)eKtGqT4l`jd3!v9&t+Ufl^C5^WDPh+-mypa7LeXW}mZC%8@t3Bvf0^AX9Bk15S> zAwaSOBI+JSaeq)4WQ|m(dCuEh)KSDeiPI+8by7571GISdTXk}+u%M{m3`pL^wgj=G z1VW=P+wNC~$6vYe_PNMSS3p)=GiU;soICNz+l zL;@UD#_lQYvET|F`blvDVr@?jJQSZz0wh;HBQn>DVu=u?kl5Cxkg?OQ6LHxJ ziFOyMmq>t)garo@1hD#d!bbjO7NZIdR_Vl{`gQLdP4Q;(N0+=1ZYyg^(zvUE`rbn} zzrO4KRMBTe>|dnB!1L(Z@fu|f16W^#p zi5Q;N9E{=?hKW{c252c@c7UGLu!J3!QWjsPGjCGo2Cd}OGKZM?Bo~9!VYfI4IvJME zCjh1n^Tl(8#j&kybT6Y7DJD5AwRnc7-bYUju;Ma6eRonDO`{}oO!k0RZ0jZ&ARr16 zYYQQu24c(T!%^Y{GpiUP>uKVkBTK2l60i;SO*??aYZZ__7KN7UPXGbR%(Uf`y+!R5 zTxCDK_kFL;RytPsN}cF`-ycv`w679`YPC~6Fyo(`sRR0JY#C9Wbv#m8*0-1PPl+-j zm-(j$M>*@=pI3Z#|GqAYOH!m=!zdb@wbcC0S+p7X5tm)I+QQcIlVRG}=)#X0vIYm2 zf!Vk?S=}-E6jXQZ^(NYH_mZ7{-HRcAcTG*sj%&~l)2Mrc2M1L^vAhWM; z)prJNM-ftUm{D|#6?JaQ)Aq~kD^N_ z!2o*9FF<`D*9;Yt8;13+^5mw_8ip>LmMRBYH>=`W!EFF1Y~}N-AQZ<(mL%4~q)b$> zVw88({%zEJ3}ooWfMJa5H#CmFzBy4Q+tMx4|30X;1N7pFZ1Kmm$kyt{A$|u~k}=aY z&NY4uYf0faIB66j{^UpuE4UBo0Q-!T{pnZb0EdAd($^F=)Xk|2CCH+vdeHoZq?Ra_ ze3y@kNnB*$rh+3TD^xksU+WUF3NS#}q}$%NN7@Dk&=b8{E~?ho;?h%J#dTy+mv@QZ zL0_+~^0lrFWi5ld%i|fm)Sx{`V@r2b-)YO)oaYU)5KxHp@M7;h{dASQzbd(fg1J(q z@*ARcLz2-dht*8W%0+uNY11ebU!hl}b=+WVuZDc5OFwXj83RqT{zEX~h@ykq z4D)QJ!zx3xz@=nR!|BXDdDfuYO<(kM;xERTSK#PQF;|nIu+2ohenXXkCWS5Rd!Ob~ z!$-xA32__`gQ`GJrG7r!*r5m&a{8gHEMqmZV#Z$=%YW1<@XaG(Q_0cryB#U-lmXPi zB3llo|Bef34?EdJK7m~c>uElXZF+b7qXW+^vhWx-Ob3DX=UGx%eKyD*AfrAAxN#_s zIp?IC@ToJbj9!H&W z06%Q|4JZMabkr^4Hyx2r4N(WMtLTomu3vIkjZ-}ddia3k;G=TobDh&SOSjAc9m#rT zHgV@TaW90h4ACcflh=cm*~ zIjJ#CYldxW*Z7^b3m07$a#NDKF{#)F`bIYd?s)d`NB?dv$&euAI4*c84@AvgDAu-+ zJgh!c+V{#@1>M&3UC8aUOtXh2xkKYz^Pj%_++J`#5YtuZLVNiLqh7r00;KLpJBstJkuBcLS6zOQpmODeps3SR z0-nTg8t7`2M)8%OIW&fG51UM%E{2ZbGDI()%Z?n4xr&imF4y13ihOn;ftja3Cr_Z%2{r>FfB1z1Ct`<^lw&;HRR?T)!VV$p(lJ zoIDjqQ?a%I`>Taay?J%4T*uWyj~6x84)H9;rKb$xtuV1L24k5=qmL*%z@1ShcKg!! zTW1-z?P{oO2GWZStH5yha9-^bQNgtw;uvSSx;3`dPTQ!K!RwK&=WFTmSG6ui>tB^3 z`#FWP=LbZs+C{JfZUKZ5%d744?~u~XPsl^W8|o_%a8lm=E{M(@MXKSNc+XpsOhd$y zkPiwE(!F!{LFG<=ER!yy;N@Xd=oZN3E_k+JR&_I5qb59H*m=+L!Wn35N!lXQ_He*b5bgE$pV{98LSpf%h*X>RZ3Z`} z8gOdPP-nGV5?j%tU$K8IUma zIB*j0i5RE*mmMRdjI(=m6zTVKHo8c)Fk%Q4-011)!9_*dFE)14?GMKUS>HBc2U@M8 z#3#j8SfjLmY~;PJQs&VZCyc@EijJ(W<4vd?LtAbQNo3tP5Pyt4G8>STiWbwps#R($ z5Pz};S%JUVU0P2k1NB)0wkt~iFzjS|#Pj2w*myQmnFh~GN+3j||JnOqAW(zuykXz~ zSoOs`t@&X1DJ7|tCun+M|8xD-4ZVA*FGV-Zad%Fb+x-MyxC2hHWRD~D>FZD4r2Zp7 zNBU`348y$=>L1DcZr$JdF-~W%uc)~=S}$+1wNPF{2LR?GhRMUKfX-uh6;$SdB@L>7 zwa2*=x9yylQBT6F)kIQZA3euLq)W@GN~JpCdH(fee{{u!n}!t=eE`@lY7@!k&(f}U zS3eh;VLtSB-99a_&Hu>j{8~yE_MIXMo2)pQ0MpWP^Fs#dYr6qZ}Jp+6qxlliV=nF3UL>_;rDH2qp06~uM~v^=dU z`#3BiJ)a+MfG~@m)y8NM6Udc5JEgBJtpzH6kIRSE*XoXEUKYOr1btSZi8ei4sLhMp zy&0GE^Wz4p;k|(ynfMm~2Nn+CQ7i!EAha0(=~p!AOWu0z$6N`SkO(DQqXoh$v94ZJ z;2zOMx(hDm+RQcO>R>rqTeyQN=6elMAqx?2oAL+Gz<23gWP? z2Ibv{jRCHFuG#x$uH_ zsEBypppM|;8D6M0liPdxICf+FN`L%8obZ;SczzTz`mM;^1OIaE!2u~8ZTfL%jfjKP1u`sMfa+R6{HCbxC?P0fR|RG@{zd#w7f79$e+1uJ3Md&F zZBJF*urP5~Kn^w2F(M4xV728&m?i=UTW%D$B(`jTRtnL81esky#YZ(?hSv1OX10EP zS}k1y1DDzgbwYE+ToKg)LT_gYeL>zvm2WnXmVokL19TlwYO)8py7fJ?0~ol_;nuv$ zhUxu3?%pyk>UDb`RzM_WXz3D_77&nbQBtM5LsCGxC8bg6Mgf7LyPH8$rMtVk;eXFQ zXDjY=KIi$pe%_5E4D-EX#kH<=tys{8aS$%%5hHjS|HL|N(CYy;pLP&F!+r2gYn>m# zGrO_0akZ<<3&m~k8^POE5dN9QWjtwqvLg_^+f4!{O%^4LZV{OZ{}U{?<8Lj1ckFxu zqUiT*8)ryb(AQGBGvlr-LKv!fKSRR7W%OuiDJmBQsmmw z0nY(<9xF(YxYBfO(UpM%{pV4S-$!6KTcXN*N;_KX4+liJ=O6C9@3xy}isiE_!ysW5 zZvN&sNYm6uz!ImOAwxv-`c)=IE?Vca=?F+l<^oM)m-p`|ZUZU&<0GJTPt}y-+;nS3 zg4b^K9)-twp^}8-Fr1cByFr;CoT3KW8aBNNX%6QO084)bJym7~J0QH^5sH(&COY}{ z;AaV}gL&`oDw(omn1-jGt5o)lV>@8FR=*8O`L?;wvY)%IfPg@e!8aUyfmln2QHC_MoRY>GBJfkwyRZ=l|LmA%vHr4KS2wyU848AV#l#ibc$%UcU8M zZ!Q<&#GEJr<6x;_U^nal^t6&FJ#qAyHD!ug0YCo*kUP(3Ja2w?hPb(i3sc`+>ZZ|u zdKm309F_6==K(`3Nfl+y7%$sHm6)ZU8f zU4iVqG;5sfma*notQFF}wz9Fogc2Lb7aAM#3~;10@n;{k0U;+-5=R!1ad>-x!ZLhP zap@f&ZItu^99l_}07Voh9an&-;~yl<800#l&J4r`;ZZ+0Enoar@sd#M%UyX;h_{0- z6S8C)XKwU;q-{}SP??>@)itJC9DP(!76uH}nlbW;cw(BNN^jUknAGnF{@XBprSdd$ zP^mBv;g zpi%`IoShKrD2t_UMSyzy?i|DkJ^e58lS&HhrdO6NAtGDGO80yot}%C|b%S3mSphP4 zNR=crFHx-f&=R(vUCe)!>({sL%aB2^l9_iEla;HXTu#Ll>-sDaCJC?~jY{^J5Vfx_ zs77jC%FP@P#|_)V8x@U|!wFmBGUxnE(C$N?N9g}t{Kg1yuLhUCk<5)VVJXKMwQ9$m zw7l8k=}QABKh`FdiACiGV(0Bd3S<61Xx;y^3u0t9S@MS$M>$hL&%A235_2Z?d}~nj zLPKNoJHvO!JGAPdj2}h=LUw|c)AxkcJ?0d-JsR1F9=}cS^TCuUXhCe;E=YJ}S%A$W zs#Ps9(KXDDI-Y#PR&6pF^XQl52^abTDM)&=ZG)a8{MZ|EyAX3S&Y=r85-eempQ+v9 ztzNMJ#rAR(T`X?mh}+m&u@Dv-Q&Imr1FV!s)ThDp#j}()OtWbnbqI)vw`Me`67K_u*)L+HPxc_o)%jlh_uoP~5-e z#a>srT^w~^hFG&S=~Ef0zO~J^!MI5fIt+w4+GSQ#%%CCW$DFWFo2x~hj7x9<^G8s$ zmf^ITB&9b04!S7bQZym={fa<-y#VcS^HtKH9;OYW+qzT$2e}_uwflS;N zJ1Aj_g(rGU0dZ4B6EB1l1U#BgTS=O#njohGuWc8*egIDK8EE9OBTKc?-M-&?9#M^r zoJyGbw((}A05-wx84Z{CgeXoimM2tU_n?6Ap2rsr@rzLZN!--i^oSg#c@RDdP1$Y* zbi7T5Cj&eGVHDYko+?Ge#PM}tCJ7;c z6eyzies2`XWrZJRY%>mdLOvnG!{SUg`reG{vy@HT05AE#RHVD)oPJ49?!^C{>QF91 z8gPSA3V9qhH4YYwMPd?fHofEVWm69&4F~?dPo~6Ig8cNIXSw+Z8UZIO zAwtrltf9cBCz#9tqRD@rIOcGxu zr3Ryz&i?xi5Y#*eoo-)1NbsuER1yD_(!2?{zMFesLXo_jG)fR?5s|mXBmeNEtdEa~ ztB0%w1gO=k*4S}}pa+4tiNK8tW+a9=nF-oWw#!kH(eI$7SA%JOm^(q-|E}}>I*8y! zk_JM&`{e!GcVbYUFE*+=1nH*{;CCGr1yis;!#TZ=5~0>oD^UJ?`Xz3P$;sFEdyc>S zoE+^<1~qbkUqek^@7*C6bRVaE#4|$)qxQ)JM4&7#%9>pr$WPEh^EHhGNgyB=w4BHn z+Oz~L8Aiw z>2Kgcxx%=)a6# z-bHQ_{cr;{;9S=-__E9)zsAh+NgF$t#9v(-LI`qqZu+ddf>~7@qz`M5n@zV9e9B2h zQb+lM+B*YYWC3IY5416F;ONTDybN$9gh&9P3G($9Cf?gw+mA~sIlUDF?vtRr4orxM z(FWY|fq1r()J75ZQ*Xn z56ACC5TrH20wO!J056fWZ^m7KrVBCbo9Fedh`8-LQooBmAgU>t4%0SR>9KGQz)`i8 z3~EYG+kLY>CY+*~Imu>xN$4!I_HeK!y6-;5$5*sL6?txnXO-lv2z%~RjGYb_*@>Te zVwrh(?9xgEx`ogu1%j+&Aa^hUm18*t-auM!gw9i_E*| zKFB))zoj!u`=*aLT1CBld%;nFfUOqo)P-f83DyRs!%;kvRyyVd8s0;Q;zY6bKO-1N zO0R~LhTEGepkR7KGS^9kRdtPx9#;mUVhp@;~ROS<^x2JfLme)|GjGjgq})3l=LaH z^nd2vU_S^EXr=HXj!|cQ@7^Kr;I~^1er!4_J9u$i3~b5+F*1VihB=_SvTL1ulRWuv zt2&)U*=$s-=w7CUr(gov6vUg(YES)(8(6QOk^lZ$2nn@?^Mo{R#61;eh!k7wG>?sP@;aoZ?*Ag56W%}rXZSF2+ZaU9 zC1XY?WPJ#VR(?kJ%(t`8I9Nva78Ohdrf{OpKKDK{lia(1Kr>^q{tk&+)-%npg{bKEwL9YVkn(%IhX%gp8Z74^W1@bx_wvij95F^ltxJpkFV3MBlK{sMF(P&Xgf#<}Nb) zAcpFe{=E6K;F~a+PY)xaG%MZ+i#B#6N)66eloP z#}FhaHhYUQbbuGJSqD0eUH=VrT10~prDI*Cps}wA4n!_#fClCbSfK0OE07A6FfT^3 z8_R;or}m3%qT15zY6}!DixC#aZO|3-@+4h4_NSs@Zxa7%ZY&TD9x2rKKfgTN>c|3& zt*W<*7;|V9=Wsj|ZLaqNZBULsTqEF?lyOfu=&?4t0BxMh8tC}+Uu@x;4(EqsR-**t z4e<6yg|dgP!%kE69`zbEjxQe}_upbkz$L}9Es_2~7(>qI_yW#|1x5y_->7G!jDwje zdo3HqUqybqt-ljOM+DTC_j6#@LX5nT9K{2Mx=SUs$0rTr^4m@b#vOF6CJ6Tp@BxE? zG7fU=HdU9h(iwGYqQZ$iE;yk1U%9L!9#S0;fS{zoqYnMLW{6Zpi5u8&cbqEfUv)U* z47@U$((MJ1(4(=5C>B&bMZ#p8)%_0OiBeJ++q}+mXbHZ-@aZ0 zVX;_wh(qQeiGZtPfpe0%jyt@i?t!AJ|6hyMa5V;++$28!cyssH6_I1}s;s{t{nZcg zz$rU{1oh!f_eJk_%o=sj)3!;$D~zMeraec+#Z|w*I-39e%aBT>=?RrWJ#nxsfq)UO z0l_H+GEjOgjIRf>BgrwygPxkd3VZG-Nj&pnkG9FX07NaTFO4O9A)niCRsz`Z;Nj!6 z4}L13mn*m7YzqvD9nhy*&*pG^*yP=ZVzKIW+loIJbut#jWvuWXJ9kjTh6^8851nH$FR})}_{}dZaw91rAwhvhXASb@LK3@TtK)kYJ;kKQ~ZXDVb&n4H83#bWd z9srFmlZ=VdcHQxXbMYT>I?^?55*p#dkmE_YZQ21=dT?CpeDEs27nH{(!QcP`aPL=V zPT^7;WRAXm3pI1cPpA8Bk4KD?pMxKKaStSS1GJMMr#4*dighOKB+!2cFV|Mhll+H8 z{mX^$F$4Z51)&pp^xqMKClZDi$e$kGW(+w>@)PzwHmo7Mg>X;AA9R%|uSv&q$^<(k zp#?!2DKV(WvoK$pdQorb1Bi&@iy5d4mE29c3m1zYN|)~{T7&0jZXlxnPU|B^dOKAW zn6Vs~_q6}_Tv=}u)Km;zlOd?;=W2*Mo~|h?7D_VYxJ%!@3M-9Xto6rd39~1vK5nhn z!Tw*)0X(R7dq#$;s0l(pkZ0Cd%SDRIY0wUFf;V?l=}rNG3?jffkLrzK6^%Z$Xnc2z zezb183H>K#L}WM@x1PE*1XDkdvGdAY{6bqXfV6D98!mg=HoML-wVgs2JnZX;c(g(nzFo1eo0C z`#RA-?mO(Ma*IJeBRJ3-QY^jviYcDor7Wtk4+q&A9U1Exiua)}>8D5YFG`Rbu`7vk zW4BAHOq=Sn-=J*?&b#~E@Yipa{Pd6t6hramyL=jk0q-bL(5JTGZi`S1FE2{KBgX(9 zOjzk6KuZ&*ONq%D0OC+!G^mfwO*erl!rp@_1{4z^9||vbU=&ioRZQfparW@H=*ac* z5;q17ssRxS1%o!Ha_VB#*qrLbgYd1NJ{JQvF76f-e|X(r5lVQ&XZU)&{JB{8+Z{H9 zG(^xyl-vW9ZcCR93zEzB}ginIqYN<~nM7v$B{)V#Cc zSud1Ep+fmoB||Q_DXiS{s%RmQxD=w5@Yz%s?Z8?CZnb?cKS8n zfQy|wSWI1J(fe0h*zZO7`+)FCA~bHf@^{eesr)jQ;Dz2Tn#?2W0?G%pe3UcTC}Y6; zGjwGV#Bi+%`#*vXj{C}}Z)#BUJ;cz9!tXX8R{2DePmPfjzI|NG5}<~%F(g--{&g6Co}ug#zO+8+_|j!*eDaqY{(3G%+%mM1 zMLv`26I~6y#WVFKf$BTG(g9&Hh^tNUHh@N3HMW?eQK;u@>9F#%H8KZBBnea3?5s#C zAR~1IXgXxKZm+)`1y$;|eSW=mn=Yhl@ZNob_9N6r$lO!ePQBK^l{e%3A}>*PxYL#< z#>UjAh0zO(KoI84xaO8cvbX`PGd(45Z#|sFA!dw_dZif9L$f&i`-K0UPLV%tIMc#^!3BBq?x#m<{j@kkwg)5kqLp&h=y>PPK~VxQrfyYoPB|@+sUH+Fvq}tuIgVeJSe$m} ziJ1vzY$LXC#vVC_&L)V=i^sP=*#bbh5_x+VY6gf?2lYp|d~$sGmlH;-G@c50#XBI} zyGZ*HF|{NAUB3=*#XvE3RIX8)`&w_;zvC`v6mWi=dpL}Lwf;jeQ33Da6&&IP1+WMZ z{8c?g5&q#PFYewB+$9^*mdusrj@GIp?_T3J&8WJj{P!JRVxrf>8o4qd?q(vV2>Tp?(KXFUN@p(SqZ$6Zl zC}bkqt{jZ*^q5M7S~(M+DIyvd4ndEAnLli=2cfZ1GnljEVCs@$drR5b_)8;EyiXj&FI#{#9}gzVzYepK=CJzu)lB5^6ZVaTE5ym zoi8D@nkthnI4zwwR-)Xk=c?^9@7=KfrgQlt1|U_IK@oOoJE*hYlb8`aVn|BrHzpXi z;^O-N*pL1fH}~1}3YBNFLHk+2i`4ek7!a;NKGr)QWUo)ws@qD7YzMBA`T!F<>|673 zd$B8l+cto8BLBu7kly-;=5G=W2xAb%Fj8)6;85jRf1*Xe;dRn5&Y=fQ)1dWTq?Xx& z>?m})8#Myg&5NvtvH64T&)WL0qfafR0-)^&sQHos&6{Z}fJgs$i+0g!3G!o4tH|KF zaQk>?G>cT4*!y=fSe4QML(U7P%JXY|R5zsUp*h~X=a(LqSotuK^HW6xu4 z#C*R5<2E-OrMlOi^CwF6XzQrE$aN_H50ok%iJTY2pW@7M{ZBh(OcUPS7g6qERBf`8 zZ8|h@s<_km^twUDqVXj8hK0U|_A*Pz3ju45{^ubLBea`@Xiye=k$^l63tl5a)sppI z%IcdgM8Vz_MrOrLdOyPvkeFYy+aVfHQ^V9W?>9vGLS)`0oQmS1mTXYFlLWZ}P(w*w zV+J};aPfix22Oxc+PZ2y_&Z7YchT%$|MY671Z70FhecSw?IxwyU8x(X#{4@3!Oxo} zfe_(i?fW)SO+cItGJ8(x!@_4DLGtN(JpFgE6FglQoa+U1Lw0n%{&_IT8yGa&Extm^HG$bJB%q&;h8ZVp$E0{uVj8~mOBf<43664HtU_n|+Kit$Tctk|G$5szm4y&|4sV|2-pAD&o{_RO#^bf&1?%QDjJ&e z7Yv{@377J+0XIs*S%W+ni5kE>hGNTwO#l3-mPZKB=guS}-l~3SmJD-6aoPhLCnx~k6S8EWV6kzilaJU?-=PI7RVO?i%g)ct?dLr zYg+sDVK`$3R4$|fPM4d>hbqM`fKTZJv}lnM)Z80Q%YX+B@CZJ5K=wS6KGM+zs0M=0 z%z4ngoywt4gepdYF!{si9x2K{zjnCLCrAKoVao$cJ}2(iXy4+g6Y*pth0$o`7z$;a zlJ*`LuEl!HAK6}YOm;5KPvu0X8DOk``kjz~{Ym5X0{yEUaDN>Gj6)Qt!1(eQ1jLtV z0o=!X0N$4d=hcT&2<9!oc`1%gPT#=yTJWcE4}W(Hy#vry75Yms9f0QBYH!pC?WlCj zBjLEmuc-k>Qqr2r33^oNpP2kt%#P2FN+|3s5TEm;Ce$WVQ@&RlpofWq@44b;KunP++2eS1tdx z9}z+gK+0J_VCSAh$J?J912{z`Z*DmW>dQW#-=GmpPC}m+OL0uu_M10vzobC^Ol)fh@G~P#4jOi|Q@V5>o|gn4;^j1kg0TKrcaY z`wr>%BB^MRr>4RJGX3?S?w*6G^x}>?5Y~tU85Po)e@Hx+l_@Vt+aZX7KLHGmMvR|; zuM)}e*6DnbdJmw@UwbwH`q56~O9aMfL^kh5eek{2DOLJ*+3tMuZ25-E>pG`M z=%q`tIiLNr(YOO-b)|2|f)4F;S8E@-xeE84Q&U{kQlS0qAJ$&ewmV%vZhWmspi1<) zI2k~4C}>LU7));&#iUd~nS{gNFrY2tMyl3rjJuTJJeZnPi!ymnqoT_!7h^)d-~NGJ z$uyA*;8!*f7aky$o&mDfXMl!O#{%?^4lop3f(Yfn=;2I+&$&Z6$}Ej{7kiWQPgt>? z32enxL8@=TF=PW$>?$lOi`>Sb#~|+iz@W^GZ&|1XC~Vv=htOQ=Dc8M_G7@f^FA8}Y zs&Ex#ponI@vDEQV&U42->D;!9==w1S#*~j1$#t$JGCIWAaKX4(O6y}WoAIo{2hODY zhBchB#bez)o_oetTDZ0kL_6~)4x<@vmMF> zbC}&XE3Kvkz5!aLnIizze%kqXkO{a}xxQIY2t`jsh`~Dlw<|SD-Bih>Se99p8drUagjK<|dl( zE}*F4JNzgE7#6Wn8G!5HbxTaHAlFzFnII=saM+Ua)N~28GEETI`ZB`;qahFBGhyjK zi|*pM&l^)r@~lUW>3;VEN1ZhUoB;MM;I#CKo!aZF++oVvS<&vYp?-zx%Lfr2R8-ox zV!*WFVXNNqV1TBr7pc6kereq*f@^2Nm=p9BYKZ-s9_<|ifl$@ML?IC=@BK-avx%)% zV@I}F)NclCv0XJ``NrUg^B+wfH4D}WoFs{Q-nfZ?gaV-yO2J3NSQ})R*2dK1Sl5=IUbw`;=*suIF z^tO$!{)itKx$6qJ zm~$B9z=YePBG$~5?!)uF-XWj7%1c8a{Lnea9bz6Dxu$I-ZsM018MPJFSABR|QhV5}f z%Gv6jA6t`4dS6?V4$|s44-X!=xTybYmH%39wXY5C{AX>M617y5Tn70u@<`$3a%2?B zb=xW${4*=v?sJji%g1`yHhGMLaZb5|rJ^v$t6Fbd9flrgKae%hq)5zpryrC9r-!vY%@(BUB7^@%0gAN>g$++ z7_N!8@~XA5RI@;GZ0T#>Q>fW@Ik#2$h?y09Wesdq)VRhOV76yR&GMNt)w?CTLAOnG z^yP}22Yr3VhhUAt&v%8|)fRy@O z{kCAbWV+_Q@|p#vNXv!&N>Dj4s!W!LDV4iwdl{BKTN_b&J>u+cW+nOSK|F&RhO(ycd9ANxJTy^KJ++-UER*o*D zn$m=4In0`KR%7!$$h1O#e`dkPEx<>XNA}?MeXXPdM@~5U((5iZtik}7!eSc&YVnLm z+Kw~6?eyOy11g0x^^9e9u#i9$L6XR07YHB7?cFzZzR1;SN6T=#n4XP+40szA?)|ta zzLa!a9uJY#zkABv#G0#DK^qv@(Q-@4G=8mU|Hea zTO^JldI}%KZ1^a)uS+SzFBd5eKNCZa9g=Gqc{Z<5GLVaNSDB)Cf(OYbDtFG|FzTt?wfcUdnK&3 zQTF>Vkn47aNt2FY#Ew+EoE9`YsBRSk73w~6nOM_OuG7}42Ex5I0VP>r_=f(X{eg2# zrS_SCle8Hb0?8%eAkXB8Iu7Np?Gs87M~AAGa}MIKZJT$NnQyEfPwc34tAs^Ghan&1T7DoOf(?<68C^ya2HX(u^{qOyLP*0&bw0-)FkG1&%zaIMW1FwXh zeZXjBMy!E9FNt;e4!(@%ULRR&3wOwF;ox73+ zJwB)AS8rDBs`y2M9)-MEKhB}gfYom(a|N$J15?Sd402zy(4q2()OlYLbA!H{&g?!=3@f*3cVHlV@p`1=3UT}N_nAZ~k@X@c z<87_{d?zGH+>yKXYoM#n;5^eil}dXVJ~lH+R&n#{yW>tvca@N$Y?YXUC~CGH$GI$C zF{+((Jbt16_-oJa4~vV1A8`3(Bi@VsKK-RchrA&eDT*)FjRk`=T#8u72A+Wn|{DmNCHE9VhcrrWkR1!E4Mt?4)hBPdsE+NvVgD{EMb zC%9}@8saYl5|B!sMeLgCz3rB?NE^qfbm0Ns5Ix{N4dyuvpFfyclzadv!1ruubwzV{~j-58$M?LW_HNa`-+C4O%WG*vfL#Za6I^D*Hu8?y07pd`v#S2NZv`$g_`?d(JG~ z5JWl$kK$eQue-&F27TL1SYy4L;Y0$*+E8g4*HQNGeJzuwY%V>AH!oFLJnlA25}fu# zT;)qzvXS`)pZuGEa{$&i03-pCUWRtJ^bT`MK<%!!s8VqXxHvOYS%B&%{N&6u*Z^wK z2Dg?$cNZ5iZ*vp0=6rOhjsSS5_ZSb@RmdLcG$6a#GIfluy;O2&Zzwrbs2Esc4?f?| z6eLrPIN9%$gMl=<1FaKygPbaLZl;Zlwvm+LL;^a>67vYboM`G_J0axRz<{t8Xxgs0 zHZ=S9rG$rCsJGa@b_RByKei)z@k}HcdfnDso}G@fkb?2kfM>($?yj|muokT597j%CgC z>}ZwnWF(=?-$#_7^^}mktSlg>-gSUlQoRa%-GmP4i z&u*K%(4kyY&uyo@mnn&mEb!4DqUcfR(9tM|WfBjwJFlTem&)UO*Hrfj=QdRDr=NyM zReTcrqFCPFUX%{84hqsK4h4;+A7T^~%q?Z(%k-X)bIhf8uViEW@pmD^&xA}WvEL*9 zK4>TqlOzPP(b6Dn5Tx-sj4|{Omsw#na-3KSt{;;wy|7=N-2!U0|=hj~GukaR|el^-mNg#vFpJ#Z^w$(h&CI-s|gK&fY3C z7zhlYJ~m(30Gv=d?VT9?(AO*Zs~5jL2ZXXf6b*&Rp*^`<6!-Vn`L2P)hGCXXxgM@{ zF&C~kI`o<$sx4AHa$djqK5VbGKff=UIW#>-c`%$&gPqs?B~napPdvUALx%UoaR6e6 zt+E_3NqzjT9v_E;vos?@XIv?>S>CuGiN$D(ZuOn_pHDGEuz8OHXj^&WJYsp7HWLNt z774bBbKmuyyD``9|7_nn$fZn}@T*Try=E#Y3uJEjM2&qKn!dJ&CCFYFtwox^A&u?Z zx!5n`X$GNnev=|-E1gRUj1FqB*{HJn;gXrhT4cPntN}GXDNpNQYi@s!zSP#G*WE$Q z07YfmN@S(GeBl!%rA#moDKk7;Zb5Q;pXhGpR-JZD=~cnq{z@}OmZTle$%{^oCtHaE zg_CrqB&RV0x`v@U=`3zpsiKvkf1La{At93yq;^Z;-?v(*6Th9lQ<7GDfvMcivGU8; zY*(k5nq^g}HaFDli29u51+u3_pk&jJ*2jemL3@>h3q^_y`i(`jk+4AD*3oEn9F;p; zqwJHcCI;OHm=o=#Pr@jQBx-D@18f2^347l6Y>dLJFZ^0Ql;-F!7Kw)dT;&b}LvKy4 z$Hxvg*K2z#=t-6&p~I-@a5YM zgP1pXT0Wv1NhEL9>b1S&F3gK@FV%cGGn~4t{<<+ba?nSlv0+?F&dxgLXj)8Tn#C(DI{P(*; zYdGzuo#zH`qH^t@4GzyNq`KX1>kYev4Hv8D+f)soh>I^+8OsDD%R1iA=uXC88ze|I zU}afvNPA@ihJtn&cL;ty-4Bxp(y`Thxl)lk7!Ivebt7rDE&axAMv`Sg5A9N)xB4-B z8M43Z_uIL`ttb7>R6~3}nQNX4<*`D_D+^V(i!wA87d$id&9PdCE|v_hTI=0C>#6_op>plE%0{z#O)`Gc13dir2ZZw7m(89t)^B?2A_mg9#n&l}>hN%*n8s&W zwHcsLjF^j1fz#!VJge(p5Mq)6Jg(7cvm@iRx9u6dMhBt>AF?R#oSiBI>+BoWC_8 z_!JL6PBhRxnqa6)t%hU&7S2YWT^e7m)w*0EEppqCT& z?+sh|w_*-#&Ru7R9rI#(?4?F)GLu5|uP*kxtL}+*o|{~|&=AV+HQ<_epB?SI{rz@a zt`M>?jNHY%*sX5ySZ83yurhCclZ<#j1~WTOq~?*8k*caH(LH0$ZqGLctW4`4 z^kh0Sm*fH^mSn?tjq5c0q9hK;@T}I^3ow|9<&=atXQgu^VZ+P3Qyhqcp{>O-T1AiS zSrGbY&6M!*li6o4i9YI01{#uR7H1(=lRlxRkBZznJza8&j5=8P(_t^t0E;eA@B85Q z0V-%lF;Gk$eB*`TTL0#UFZM?Y)9;k%%6S3COVU%zqa0>KZR=^PyNh}q!{TPjgu*lX zG2x2AYsEcmfh#(hZ59(QtzNaaTReL-_1c;$3#*doJ42ACMiE=t`S@&YPDVykcTY#x z6b1>kGVC_$LaaUX|M2}FuRviU8S1;XvTD8!fL}F1KWQtaz57#&B?RN!su|Itk!REt z0~=#ao^Gn4+njRf)%ppnv-`jUi-7^gUXxJuMoU`#aXnv2-r-4HC~wcx4WS}?$Jlxa z_n#Ux*CHT*?J$5vsH+coaP4iK+qhrJVid^*5sM{NRJqHT>bdZr8g(x`)ivVBvOV3t zm~D3#UFBB4n#m;W4!I&aQsCork!*>p8tQ~zt@o)6g*%#Mh{&duM^#08)RR^TN525d~rCQWro3!@opkZX3Wu~O{$b4x98%p zl{lK;ev^D-tdya5tE#UjBALlTG{|`Ed!C!dSgQOutA*>wThS*8cD~WsOy&E8{mIuY zybKF?R%cs>Yo0yj7Mg;xs$~vPFV0>ld~=X}ce2#j)v?#R^MHyXmW!PLszO|7sVNrE ziM=rfORcpJ5k12UMGV}c%~5XKYvqYXk78j$=t+~)%thfmBbb>Nu04b4R&Nrx@oXp@ zI%nTK^HTk|R+YA?-+D2I=e4wR;ok}j{?jwPdhJ4cqjV7AAEKzRvNOm#)f# zBT$!=TtPM78U>5WgN&!Y-8+a^%kUs-mQtGf*BrxldL(+qD6oD|N`c45Y8Z_4R9N&) z?Y&zUevT3Cok;?xIlvjPpOl4ApEHgo8%qc7kyp+Q^yHUDrW(Rzt-oDL zv}B1M29_UDkW1kA4re@>%)7gn>eM$!hzn~YlZU}-7NoSyN}h6S^VXPu62=ty?IGY1 z6*Jh5x33VdVF=$xk!((XRBzSsvXzWSZFp!qQj;eOGRRUCY9~~V)mdvyQSB@WAkLMo z;Ldtce<%I!v~)b*?wzL3+Pzj(YBrmv$^r|6g@r!fGp>L2UiIJ&(LHdZtGo!XkW_Gp@>DhL#1T1z-f#>RboB<>nawU} zG#8n41{rT@AZ73p_6?g}|0*B}Q){@AhYl9}28iGpmT(HnEubGjB-ay5{xMK!J4F0A zCUxOlkzw{!k7w9P+-@Nblc0bx!&@U-hiY=O zk~0mqCR8~#GFWK`CvRZ7A-CSTO?uK%=zJ-wR`&3c$4oMqg`PfVOsdsZhEAkEq395* z3&v;qZT9D)C`Gp&)1v28kKVs-M{gofmF!d8ydIm`>v-MjjA|*OZs;n@i1}crMC*8d z#%sqs7)9D&DuJ=ZpQ3WxR-q7i(R2BQG|gGL6Z>orHMHh3eMzq)?WFZ>p4p{)YmTc>d9E|>@3Ol{LUU^; z4F}FxREy~eF8Lw3JQ|i|Ebn^j602KoPMo!*^k|wThe{aBgebNSOAY2R4^4F4alqGI z=oWu=YVtBR>w2ODHqR8qWu<-=*X{^PNgJmd;f)*g0nf#R6)3$l?MEwB>TSy5H#)HN z-WY5vs9<+>M-G-K|DdHPZPi-d%aBb&@G`by`J2ls9#NHT*zq1JMZhK*&w!J_V17^` zz$bk4Lb|S)oBGUi0_n^^W{~0}`+r)Hpup^o%q8g{lKX-bSiSK?pM&VsVN5Y3K}yUF z@0yhdbI1GFm>II~858RF4^)+H9mk16ze#r5<*ZhXvtEBFkeqpWez!rq!~4gQb0g#g z+R?+95=aD{FAHNT-o9kisC%q5RA|2tRcjr;EU)l}e9*(B#A>Xp=B+{RWT8X8`_B0J zNb9AUf2z8@VD0skN+^W`1%Oh``RioY-ZsHUAfi5;s8sLLu45d;lP@$frXxKFs=4_Oe#cDkpxcPS{iy^ci~7V=I=cvJDGm`jK2PM_Y0k$MXjus-`)*px@;Yzj3U#5sd~(tnQ*SVnBd6gLCxJCWBfz@b;AmE#`Xl2ByL01uOreca7gI*qM1s8# zKvooz8(DKI7NHVb0qLz((MuaURgOy_BiOj`hm}hRR&X~j|8q?sz$rKA{(++KCN<jpw-kMJQvyPerwVeH4bLg2f5K|n{>`l%3hL_ttZh#$?*YaeJY z-aec!zx1TSB{@b-L1wWks@7qXT;gzwNtPuejK3&Gz~1y_rPI_p-HrnGi$P0Vg6<%7 zEobH^!XqcCDXJf-7GsX4GYJ)+zmA`)TWg2>`L!O40UQFtF?2mONT~$16*4(Os#&l% zu!V`IFkcr6oGYv8c1O2-&AF5$x4EvFGZ0A%U<1?sULH9#fa{1zLqY#(7xQ3MjcL>J z@q&-B-uXLjl6u3Jx!i_3fV4U6KL^{{Zd)Sz<1*hwzyv3n^F}xA^+PtSi!_)Q0xxD= z9n4?scXO|hF8tdD?@*|5I3Q?fHLC`-#YPlcddCiw+YtX$i>R&>3cj)m5) zLh*%+-&N4RY{`FtTW)x5JSY$tB$Ti0p%eMm3Ql2~-s;c;BboPHha|)gu*g9WUJ^0} z0#QNyzG?w(7d7dJgU!Lz%Xli5_1ayD*Nh?McVZCatnI@hATLv3!uGYL^y>{}Ik# zKP@0@X|RXe#oKSouVq{R`Sa6?Aa>3nD7%&tUe9>`wIIWHz#?pTZeB01UcVgw`q&!! z@0SBI>VJHV?+OSEgL&^STn@~#$4bARK{d+D#B0N%|2XhoD5>dJXxG01SWsvCb<)pM z2N4@@jQ{xr-Es~41>IX*J7RzQtv{CtNiEXzdy(SBxK`AE?&`01g!XX3RjZ$B!Tnzw zY@!IBU+KWsMfHE}UAG)C8|Uu0a$^6v%)j1AQ3N@-`ati7;r}`-j|D-nws$7Ohy6c} zftM03$lnwN%cFmn@c*#}@Lb~@oX!6~E!Oa-22FcB&mfFsSwsoj5dk{7LMKax?!E%9q( z;4eZRey_uZvEf}`bbAx8Ew?QfYf#kwwDqPLldA=EY}Sa@%jpZ81Emd9bJMW1>ZuFn zI#4lfE3d!UIPT9BP&|90z6 z@4J47fCY#GW};j{+T+@(Rr}HauXXR%_JDcU%R~9vfa6U|?!25DGHD0ePa67zhOLh4O`4LHuef6!sg`9yVnp5F*Czm|+I&reFL zVW5YIhJpNQ+h4E+^#5}4)WbE+mb9wvE%?sXi>*Nqu0)-js5%g`)i0m04?<_Dd4!Bb zP73;sLgBmlc?QsNmFvuu%<_P=)91ZNMAez&<p z<8+e9$*s6y<-QjWn)sL2YPJ1xfC^El_Qk5Q;nluVu2Q9klINekm*>gM4wr?t*Pq+a;i1p7h6FR44=iU32m12$y$RkIj}XYqpD>Jb+{#A9-*X4_ znP9-W9Xk1zQt;Dewcq13(<2bnoTKKFz=|4r#vFmJX5Do41TYk8!S?EOs6G@l$FvHZ zt>ePT95nVf#>)AW`wDN8sM@Kb;v50mQ5-gX?;v3I?rI)^V(v2N=GJhSu;XyEq4Dlw zah*Z`kuDYeWdFNp=TfgVn*!95s6FGktZ2Lz&C}oj_;)vq#?Y(3Jjjo(|HcX<#oYqM zdi|~1qtNlZT7i{5IYF`$#@pnMItk9JS%%3NnpRaS8LML!1)vhX?l?y>r80RXrG5Gn zd1?I*XV5T+uF_|-h8c3r7&Y8s4j%|53Wsyg79Qkm+k~{4<=1jD1JcLxc;((xtK*0; z*L!z(*9+LboHt^*zsRetSW0qQ2o0oq_Eb41)Wq zSdm^6977to;O1G)*(RT9Xuz38^?=wm7ZK?{uo1|R_s}6{NUiG)^UzG!jomKx-e9mS zQC_B#4uCuUlGo9#*JTnH9BY?(71i#OfqeH;jGI#@Jq~S!FmiRbYMyd1YL^JGj~RLw z?2$_aPrk`@6z-}5g0^B(CuE1o%1LU)=6nYR`}QDNL&+{?CCPsDb;Lk?sk)NGVl9{yL;_aJ^2EIK zZ1};oWxwH-3;dg9Vsg$kZ)0*3fk@UUM?u9XC8kjeS*6lXAK9|>FK)+i*wt~^?l$k_ zPH2%Divzjiv=?FRg8St+ zNPd;3-GJhPC9+FoU;}7kJ2KRX)m;N^URskZyoL*gZ1wz_k9vXGR$QmPTpH7!NE|+YoaZI9k6+HyNxgAVnSZlt`)o!M2v&7fR-fqaRgAVvpK6SZz1DG}PJDH-`px&tU?Gg}eQL3wIUiWx8qb zCYp(Xl${cA1?~A3KFN1X)F|6^O14Sr#KN?Jg^12f2Hcrd%LB`$GsEfVWM~YWiq?Vl z4Fh4%4RpV2YsH&M8g&N#0KafXgkqc$=k;ciedx05@ZVg-3ox}%tkjx3r=&)}dr~q7 z${*LOgvz+5G0BBEZVo3cog^yejhKcdQoTiP23&J1CxNRP&|A;pWhkI6E6z{=ohiH? zXL~)x2Lz_C3CP!SIvokzcGFf#4(&W+)z?HShMn}aP~2BOv>bR=%Hp8PPEeS5fuGi^z0go8T30u!1~n-zAY?dvQ^)$Zj&oiF74P^@RtG zlx`($a!=noE}bL?Jbq+>N+R-09Ka<edqP_u(w0Eha0ZODW^6H_QA;52s|Oy_4hA z`&iLX?o5pUd)B18tu_6?SzsPOisXXRZ!A6j^1&J3=R0(1*SxdS4?0cHdrp3KeXGa= zbn%#9yi5H#^**dZ_dxEjjyjtl>H-6U!%^`gf-th>2Sf(amwdf|IyD(6Fv--Z`vEkz zgXV89X}h~-1^t{TIpLAX)sB4T1C9%45p$B(v76a}JBhY9(yMwF%g_tw9o`ovjJ69O zxOBi1w#!2W5*znfiJFCXeuzPFFXeM}FG>~)LM2%ue@~l4ZMR`v{<`WQo*DnL5I-)i zP$%n8=O*hQUX}WrbE)G@YBYGOl1&3}SPzjW&Owv<;hit+)kUt?onGSu=s>tl>_@8f zSsZJ=HS_chn*C7ubQOIvT|()~ccj`vyP01o-=)9Z}~a5 zazcbMD~Q??2tLR+RYkqp4TzdmhHNirw2|FNZg&`4))^qFF z;1+Bcb_5#ZhO6wMXk2Hz!N8C51PRB6ITO{M*!czgTzxyA$0-aY06AoyqUD4)fyARo zn{^F~Utb?+yJ>zPwfEuWNNMhM5m}TthZP>BwM`N_ckB z0^T64z=(JpSFClemDvd0t+Ds&iLTa!Ae)?{*8Ly!tJNQBv*s zB+wWmxq=fKj*Ho^sB@e`@Ux6F$D3q|3=vV#il}~Eyi+#(gOx)iI07xtd}H$a?VNkC zT);%$26$5_sLcb5j=)2Q#zz4hEKM1Gq}u^Qz*hau-2!dac_cnju zv?TH1;z0+tXc3rQoKIFY>BT!UC-e(0@PkXA=j>aYG>x$0Mz#N& z;QGDWA3xi!rQwv7D8b`mJ>3#(5Zp>j;k?VC^biqnnlLd?gY>L}$S20-uqnS6w%0h4 ztsU~zQe3}39e4Fwx4onW8G%)|j@60WwZ;$fnqB6bQrVmWu(^wIVVhE;p+od`h7fR~ zgTno>53nRyeZi4_)SmuRKP2P>Sof$$2u|h=o68O`iG0g~x)z48SleaKDF`DQ5A0Tx zKFY7D2(#O0yk2d}GxY>ht$G^%;26TJzT>(D3M|QI))ex&4BsZ|$a;q%|{$!v=#Bu-oDlKaHmlv(r|3 zF?t7`TS4e@9=fhm)3(K&XT)b^9;o)yYgaPC5M3J^_@8SC5eD;%-aRR+AYTnBDS7?6 zeI)jRR%uUyep8-qJslbZM#) zO;1Q^A%RAwK!wy8j#Fr~=<-m`Ro*~2(+M4x+!|v0N@3YG1=^i_KQrr`hNS#vq7{1( z9hAJz$&v6KyFzqPV@~D?e_X)^Ed?4Ub`T%3?`ww{>;r~Ehl(@$8gb-S`Q1F{-gRX( z-LKLLI>7E340sOjLtiW^iu{k4WkXaw0jsDqaGUVuy9U#8CIFb5#lqVxf|z6RWDThC z^MvDqku!;Dmd#Q4Z|Evjy36Rv<~1xwwc%10YY@ z%gUH2yzDkp?I5gHPk!jsy1>8J8|d5STS+4iKA2Nt<# zcyp)5Ql^?;Bx`&+Wkft?P00PW(FZ*i1#N0W9`cH&Nx-u`TcwO;17=S>YCo>~a9n!0V*L`YUuO?`gT**%3KJqOQsZTRRztqRz?Qxowu z!je8i@65^rj8d6AQoknAG-lkNOg%UwnU;EJu{yyXT^(Qq#(h!LTwbC(oDOc6ZSb z@CumCT!h{*yCzb$XtNjeUUslOM<~W%#2h1`J zXDTn*-?s{wgH<)WB(qEb)>dZGZzF-$n8K&Rc08&#CO2{p<8WDNm-20XJj1d`*#hyt z#o;}?l71rCjwSU#x47_?E2gd@=xzP6OB2a5Om#$~F3eC$8Os8dgHvZH1^}tT2x zwR?W+lUc>r&L@hAJse56+lwUBXA5(XCQ# z-9XiHZo(pq<1UxVK=88EAu7Xq^u9_5VDeu(V>cKoC@so;B8FXmPlkkpfjhlh{DaJx zxr>dEX7$nT8KXaSjqPqQFWIg?mX@#{q#nSVj8<%`94to^b5+L<TI*JPnz;-6RcyKUrX z{baE23d9CR#f{y{%mVHXtPb}1dxMXnS!;uOq=K>@IL}iE*ROJdHLF)2!CWH@*XPo( zJuL;9ntGa)p=W+&`?QPi?~tjZFRQ&hqlIDQocVqKfP7M1xkHV$mfgm2pOTSB^V9rq z+VWmM**N>tbFlGWxbD@(%{rJxp4EwWHYql@UA36b-K=6|`gB8INf9g~>8+h1nxAII z$FDWbj+haxR!QzH#>~ZdylV~{?N^8y;#kv2wdscFZZ@b@8T7;Kv;1CdO5BeqQ>5XS z_XwsCfK@~7TAR{*)LQrRY(P?GY9R2TcyV8E7V2X!Z8=sv!Il>adudeyqe>Uy!Mno{ z(Loh;=W&lfyyXonwvyq56@3mvx|I$G2cs|Dr@#hh9MmVU>ILajmL?Ie^fM9xAa1Jx zIn!IXNRWu0;LB>|i6KXXVpnru&!Quup*` z@a>|$Jt?_wV+ykEx7jyu*|(4YsrmmIJR_ug{M0P8ZQP>eJ$Q8H)`>tAjFG8h$}4tP z0i@Iz;}3Ql;9k^F$MMk=3!^XkKR#sv2=u=j)Ot{!d28LZJ5?_IE^)oT$6XS!v8yMo zFB_el(0Qi3QSmO3Z}`3^Z&zd##J#h;t^e)NkIBL`{?u>T6~CE&0k_eogyWP||Jtc) z^JKZmFY^(|bs`bD&Jq8wRnVd0i$aTzuh#>IkzhvXB~%_Brnno`t@Xu2ww@k{&F_r2)8 z+dt%R8%2YO?I?Ekk9S`lv=Hh(j^P$E0Qk!j*?7Gv`|>l{fmE0Y?I`R4w&hn3`*GKl q@>ic=t}Czwtu!I;tDt3cDso2ef<}k8lYfK19b0X;WdG>)kADHp6N#Px literal 0 HcmV?d00001 diff --git a/03-integrations/gateway/agentcore-tool-search-plugin/invoke.py b/03-integrations/gateway/agentcore-tool-search-plugin/invoke.py new file mode 100644 index 000000000..1aa2b4b7f --- /dev/null +++ b/03-integrations/gateway/agentcore-tool-search-plugin/invoke.py @@ -0,0 +1,90 @@ +""" +Create a Strands agent with AgentCoreToolSearchPlugin and run example queries. + +Usage: + python invoke.py +""" + +import json +import sys + +from config import AWS_REGION, MODEL_ID, STATE_FILE + + +def invoke(): + if not __import__("os").path.exists(STATE_FILE): + print("[ERROR] No deployment state found. Run 'python deploy.py' first.") + sys.exit(1) + + with open(STATE_FILE) as f: + state = json.load(f) + + gateway_endpoint = state["gateway_endpoint"] + + print("=" * 60) + print("Creating Strands Agent with AgentCoreToolSearchPlugin") + print("=" * 60) + print(f" Gateway: {gateway_endpoint}") + print(f" Model: {MODEL_ID}") + print() + + from strands import Agent + from strands.tools.mcp import MCPClient + from bedrock_agentcore.gateway.integrations.strands.plugins import ( + AgentCoreToolSearchPlugin, + ) + from mcp_proxy_for_aws.client import aws_iam_streamablehttp_client + + # Connect to AgentCore Gateway via MCP + mcp_client = MCPClient( + lambda: aws_iam_streamablehttp_client( + endpoint=gateway_endpoint, + aws_region=AWS_REGION, + aws_service="bedrock-agentcore", + ) + ) + mcp_client.start() + print(" [OK] MCPClient connected to AgentCore Gateway") + print() + + # Create agent with semantic tool search plugin + agent = Agent(plugins=[AgentCoreToolSearchPlugin(mcp_client=mcp_client)]) + print(" [OK] Agent created with AgentCoreToolSearchPlugin") + print() + + # --- Run example queries across different travel domains --- + examples = [ + ("Flight Search", "Find flights from San Francisco to New York next Friday"), + ("Hotel Search", "Search for hotels in Manhattan with a pool"), + ("Car Rental", "Find available car rentals at JFK airport for next week"), + ("Restaurant Search", "Find Italian restaurants near Times Square with good reviews"), + ("Currency Conversion", "Convert 500 USD to EUR and show me the current exchange rate"), + ("Loyalty Program", "Check my loyalty points balance and what rewards I can redeem"), + ] + + for domain, query in examples: + print("-" * 60) + print(f" Domain: {domain}") + print(f" Query: {query}") + print("-" * 60) + + response = agent(query) + + # Show which tools were dynamically loaded + tools_config = agent.tool_registry.get_all_tools_config() + print() + print(f" ┌─ Tools loaded by semantic search ─────────────────────────") + for tool_name in sorted(tools_config.keys()): + print(f" │ • {tool_name}") + print(f" └───────────────────────────────────────────────────────────") + print() + print(f" Response: {response}") + print() + + # Cleanup MCP connection + mcp_client.__exit__(None, None, None) + print(" [OK] MCPClient disconnected") + + +if __name__ == "__main__": + invoke() diff --git a/03-integrations/gateway/agentcore-tool-search-plugin/requirements.txt b/03-integrations/gateway/agentcore-tool-search-plugin/requirements.txt new file mode 100644 index 000000000..fe2b2b3ee --- /dev/null +++ b/03-integrations/gateway/agentcore-tool-search-plugin/requirements.txt @@ -0,0 +1,5 @@ +strands-agents>=0.1.0 +strands-agents-tools>=0.1.0 +boto3>=1.35.0 +bedrock-agentcore[strands-agents]>=0.1.0 +mcp-proxy-for-aws>=0.1.0 From 35cb65bd987ddf1047654cd4c68e69418df39438 Mon Sep 17 00:00:00 2001 From: sentil mohan Date: Wed, 17 Jun 2026 17:54:59 +0100 Subject: [PATCH 4/4] fix: resolve lint errors in invoke.py and travel_tools.py - Remove f-prefix from strings without placeholders (F541) - Remove unused json import (F401) - Remove unused local variables interests and category (F841) --- .../gateway/agentcore-tool-search-plugin/invoke.py | 4 ++-- .../agentcore-tool-search-plugin/lambda/travel_tools.py | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/03-integrations/gateway/agentcore-tool-search-plugin/invoke.py b/03-integrations/gateway/agentcore-tool-search-plugin/invoke.py index 1aa2b4b7f..77214c6d8 100644 --- a/03-integrations/gateway/agentcore-tool-search-plugin/invoke.py +++ b/03-integrations/gateway/agentcore-tool-search-plugin/invoke.py @@ -73,10 +73,10 @@ def invoke(): # Show which tools were dynamically loaded tools_config = agent.tool_registry.get_all_tools_config() print() - print(f" ┌─ Tools loaded by semantic search ─────────────────────────") + print(" ┌─ Tools loaded by semantic search ─────────────────────────") for tool_name in sorted(tools_config.keys()): print(f" │ • {tool_name}") - print(f" └───────────────────────────────────────────────────────────") + print(" └───────────────────────────────────────────────────────────") print() print(f" Response: {response}") print() diff --git a/03-integrations/gateway/agentcore-tool-search-plugin/lambda/travel_tools.py b/03-integrations/gateway/agentcore-tool-search-plugin/lambda/travel_tools.py index 74a539bbe..b0638016b 100644 --- a/03-integrations/gateway/agentcore-tool-search-plugin/lambda/travel_tools.py +++ b/03-integrations/gateway/agentcore-tool-search-plugin/lambda/travel_tools.py @@ -27,9 +27,6 @@ - Trip Planning (2 tools): create itinerary, travel tips """ -import json - - DELIMITER = "___" @@ -1075,7 +1072,6 @@ def create_itinerary(args): """Create a trip itinerary based on destination and preferences.""" destination = args.get("destination", "New York") days = args.get("days", 3) - interests = args.get("interests", ["sightseeing", "food", "culture"]) itinerary = { "destination": destination, @@ -1134,7 +1130,6 @@ def create_itinerary(args): def get_travel_tips(args): """Get travel tips and recommendations for a destination.""" destination = args.get("destination", "New York") - category = args.get("category", "general") tips = { "destination": destination,