From 7dd124a5b16b77234456d38f7d0d7169bc34d567 Mon Sep 17 00:00:00 2001 From: Joachim Aumann Date: Fri, 8 May 2026 14:29:19 +0200 Subject: [PATCH 1/6] =?UTF-8?q?feat(gateway):=20add=20multi-ISV=20orchestr?= =?UTF-8?q?ation=20tutorial=20=E2=80=94=20Salesforce=20+=20SAP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 3 Jupyter notebooks demonstrating how to connect Salesforce Lightning Platform and AWS for SAP MCP Server to a single AgentCore Gateway, enabling cross-system AI agent workflows through one unified MCP endpoint. Notebooks: - 01: Salesforce as integration target (CustomOauth2, 43 tools) - 02: SAP MCP Server as MCP target (9 tools, read-only default) - 03: Cross-ISV queries (Customer 360, pipeline reconciliation) Includes gateway_mcp_client.py utility, Mermaid architecture diagrams, and documented workarounds (Content-Type, domainName, org hibernation). Refs: #1456 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../01-salesforce-gateway-target.ipynb | 553 ++++++++++++++++ .../02-sap-mcp-server-target.ipynb | 609 ++++++++++++++++++ .../03-cross-isv-queries.ipynb | 430 +++++++++++++ .../19-multi-isv-orchestration/README.md | 85 +++ .../flows/cross-isv-tool-call.md | 33 + .../flows/multi-isv-architecture.md | 48 ++ .../gateway_mcp_client.py | 134 ++++ .../requirements.txt | 5 + .../19-multi-isv-orchestration/ruff.toml | 2 + 9 files changed, 1899 insertions(+) create mode 100644 01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/01-salesforce-gateway-target.ipynb create mode 100644 01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/02-sap-mcp-server-target.ipynb create mode 100644 01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/03-cross-isv-queries.ipynb create mode 100644 01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/README.md create mode 100644 01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/flows/cross-isv-tool-call.md create mode 100644 01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/flows/multi-isv-architecture.md create mode 100644 01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/gateway_mcp_client.py create mode 100644 01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/requirements.txt create mode 100644 01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/ruff.toml diff --git a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/01-salesforce-gateway-target.ipynb b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/01-salesforce-gateway-target.ipynb new file mode 100644 index 000000000..0310d104d --- /dev/null +++ b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/01-salesforce-gateway-target.ipynb @@ -0,0 +1,553 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Salesforce Lightning Platform as AgentCore Gateway Target\n", + "\n", + "## Overview\n", + "\n", + "This notebook walks through adding **Salesforce Lightning Platform** as an integration target on [Amazon Bedrock AgentCore Gateway](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway.html) using OAuth2 (`client_credentials` flow). Once configured, the gateway exposes 43 Salesforce tools (Account, Case, Contact, Lead, Opportunity, Campaign, etc.) as MCP tools that any agent can invoke.\n", + "\n", + "| Information | Details |\n", + "|:---|:---|\n", + "| Target type | Integration Provider Template (Salesforce Lightning Platform) |\n", + "| Outbound auth | CustomOauth2 (Salesforce Connected App, client_credentials) |\n", + "| Inbound auth | Amazon Cognito (M2M) |\n", + "| Tools exposed | 43 (CRUD on Account, Case, Contact, Lead, Opportunity, Campaign, etc.) |\n", + "| Salesforce API version | v62.0 |" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Prerequisites\n", + "\n", + "Before starting this notebook, you need:\n", + "\n", + "1. **AWS account** with access to Amazon Bedrock AgentCore\n", + "2. **Salesforce Developer Edition org** — free at [developer.salesforce.com/signup](https://developer.salesforce.com/signup)\n", + "3. **Salesforce Connected App** configured for OAuth2 `client_credentials` flow (see Step 1 below)\n", + "4. **Python 3.11+** with Jupyter kernel\n", + "5. **AWS CLI** configured with credentials that have AgentCore permissions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip install -r requirements.txt --quiet" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import json\n", + "import logging\n", + "import time\n", + "import uuid\n", + "\n", + "import boto3\n", + "import requests\n", + "from boto3.session import Session\n", + "\n", + "import gateway_mcp_client\n", + "\n", + "logging.basicConfig(\n", + " level=logging.INFO,\n", + " format=\"%(asctime)s | %(levelname)s | %(name)s | %(message)s\",\n", + " handlers=[logging.StreamHandler()],\n", + ")\n", + "\n", + "session = Session()\n", + "REGION = session.region_name or \"us-east-1\"\n", + "print(f\"Using region: {REGION}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Collect Salesforce credentials (never hardcoded)\n", + "SF_DOMAIN = input(\"Enter your Salesforce domain (e.g., myorg-dev-ed): \")\n", + "SF_CLIENT_ID = input(\"Enter your Salesforce Consumer Key (Client ID): \")\n", + "SF_CLIENT_SECRET = getpass.getpass(\"Enter your Salesforce Consumer Secret: \")\n", + "\n", + "assert SF_DOMAIN.strip(), \"Salesforce domain cannot be empty\"\n", + "assert SF_CLIENT_ID.strip(), \"Client ID cannot be empty\"\n", + "assert SF_CLIENT_SECRET.strip(), \"Client Secret cannot be empty\"\n", + "\n", + "print(f\"\\nSalesforce domain: {SF_DOMAIN}\")\n", + "print(f\"Token endpoint: https://{SF_DOMAIN}.develop.my.salesforce.com/services/oauth2/token\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 1: Create a Salesforce Connected App\n", + "\n", + "If you haven't already created a Connected App, follow these steps in your Salesforce org:\n", + "\n", + "### Option A: Connected App (Classic)\n", + "\n", + "1. Log in to Salesforce → Setup (gear icon → Setup)\n", + "2. Quick Find → **App Manager** → **New Connected App**\n", + "3. Enable OAuth Settings:\n", + " - Callback URL: `https://bedrock-agentcore..amazonaws.com/identities/oauth2/callback`\n", + " - OAuth Scopes: `Full access (full)`, `Perform requests at any time (refresh_token, offline_access)`, `Manage user data via APIs (api)`\n", + "4. Security settings:\n", + " - ✅ Require Proof Key for Code Exchange (PKCE)\n", + " - ✅ Require Secret for Web Server Flow\n", + " - ✅ Enable Client Credentials Flow\n", + "5. Save → wait 2-10 minutes for propagation\n", + "6. **Critical:** App Manager → find your app → Manage → Edit Policies → Client Credentials Flow → set **Run As** to your admin username\n", + "7. Get Consumer Key + Consumer Secret from Manage Consumer Details\n", + "\n", + "### Option B: External Client App (newer orgs)\n", + "\n", + "1. Setup → **External Client App Manager** → New External Client App\n", + "2. Enable OAuth with same callback URL and scopes as above\n", + "3. Enable Client Credentials Flow with Run As user = admin username\n", + "4. Get Client ID and Secret from the app's OAuth settings page\n", + "\n", + "> **Note:** Developer Edition orgs hibernate after ~24h of inactivity. If you get HTTP 420 errors, log into the Salesforce web UI to wake the org." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 2: Verify Salesforce OAuth2 Token\n", + "\n", + "Test that your credentials work before configuring the gateway." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Test Salesforce OAuth2 client_credentials flow\n", + "token_endpoint = f\"https://{SF_DOMAIN}.develop.my.salesforce.com/services/oauth2/token\"\n", + "\n", + "resp = requests.post(\n", + " token_endpoint,\n", + " data={\n", + " \"grant_type\": \"client_credentials\",\n", + " \"client_id\": SF_CLIENT_ID,\n", + " \"client_secret\": SF_CLIENT_SECRET,\n", + " },\n", + " headers={\"Content-Type\": \"application/x-www-form-urlencoded\"},\n", + " timeout=30,\n", + ")\n", + "\n", + "if resp.status_code == 200:\n", + " sf_token_data = resp.json()\n", + " print(\"✓ Salesforce OAuth2 token obtained successfully\")\n", + " print(f\" Token type: {sf_token_data.get('token_type')}\")\n", + " print(f\" Instance URL: {sf_token_data.get('instance_url')}\")\n", + "else:\n", + " print(f\"✗ Failed to get token: {resp.status_code}\")\n", + " print(f\" Response: {resp.text}\")\n", + " raise RuntimeError(\"Fix your Salesforce credentials before proceeding\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 3: Create AgentCore Gateway with Cognito Inbound Auth\n", + "\n", + "We create a Cognito User Pool for gateway inbound authentication (machine-to-machine), then create the gateway." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "GATEWAY_NAME = f\"multi-isv-tutorial-{str(uuid.uuid4())[:8]}\"\n", + "print(f\"Gateway name: {GATEWAY_NAME}\")\n", + "\n", + "# Create Cognito User Pool for gateway inbound auth\n", + "cognito_client = boto3.client(\"cognito-idp\", region_name=REGION)\n", + "\n", + "pool_resp = cognito_client.create_user_pool(\n", + " PoolName=f\"{GATEWAY_NAME}-pool\",\n", + " Policies={\"PasswordPolicy\": {\"MinimumLength\": 8}},\n", + ")\n", + "USER_POOL_ID = pool_resp[\"UserPool\"][\"Id\"]\n", + "print(f\"Created User Pool: {USER_POOL_ID}\")\n", + "\n", + "# Create domain for the pool\n", + "COGNITO_DOMAIN = f\"{GATEWAY_NAME}-domain\"\n", + "cognito_client.create_user_pool_domain(\n", + " Domain=COGNITO_DOMAIN,\n", + " UserPoolId=USER_POOL_ID,\n", + ")\n", + "TOKEN_ENDPOINT = f\"https://{COGNITO_DOMAIN}.auth.{REGION}.amazoncognito.com/oauth2/token\"\n", + "print(f\"Token endpoint: {TOKEN_ENDPOINT}\")\n", + "\n", + "# Create resource server (defines the scope)\n", + "SCOPE_NAME = \"invoke\"\n", + "RESOURCE_SERVER_ID = f\"{GATEWAY_NAME}-id\"\n", + "cognito_client.create_resource_server(\n", + " UserPoolId=USER_POOL_ID,\n", + " Identifier=RESOURCE_SERVER_ID,\n", + " Name=f\"{GATEWAY_NAME} Gateway\",\n", + " Scopes=[{\"ScopeName\": SCOPE_NAME, \"ScopeDescription\": \"Invoke gateway\"}],\n", + ")\n", + "FULL_SCOPE = f\"{RESOURCE_SERVER_ID}/{SCOPE_NAME}\"\n", + "print(f\"Scope: {FULL_SCOPE}\")\n", + "\n", + "# Create app client with client_credentials grant\n", + "app_resp = cognito_client.create_user_pool_client(\n", + " UserPoolId=USER_POOL_ID,\n", + " ClientName=f\"{GATEWAY_NAME}-client\",\n", + " GenerateSecret=True,\n", + " AllowedOAuthFlows=[\"client_credentials\"],\n", + " AllowedOAuthScopes=[FULL_SCOPE],\n", + " AllowedOAuthFlowsUserPoolClient=True,\n", + ")\n", + "GW_CLIENT_ID = app_resp[\"UserPoolClient\"][\"ClientId\"]\n", + "GW_CLIENT_SECRET = app_resp[\"UserPoolClient\"][\"ClientSecret\"]\n", + "print(f\"App client created: {GW_CLIENT_ID}\")\n", + "\n", + "DISCOVERY_URL = f\"https://cognito-idp.{REGION}.amazonaws.com/{USER_POOL_ID}/.well-known/openid-configuration\"\n", + "print(f\"Discovery URL: {DISCOVERY_URL}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# Create IAM role for the gateway\niam = boto3.client(\"iam\")\nROLE_NAME = f\"agentcore-{GATEWAY_NAME}-role\"\n\ntrust_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\nrole_resp = iam.create_role(\n RoleName=ROLE_NAME,\n AssumeRolePolicyDocument=json.dumps(trust_policy),\n Description=\"IAM role for AgentCore Gateway multi-ISV tutorial\",\n)\nROLE_ARN = role_resp[\"Role\"][\"Arn\"]\nprint(f\"Created IAM role: {ROLE_ARN}\")\n\n# Wait for IAM propagation\ntime.sleep(10)" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# Create the AgentCore Gateway\ngateway_client = boto3.client(\"bedrock-agentcore-control\", region_name=REGION)\n\ngw_resp = gateway_client.create_gateway(\n name=GATEWAY_NAME,\n roleArn=ROLE_ARN,\n protocolType=\"MCP\",\n authorizerType=\"CUSTOM_JWT\",\n authorizerConfiguration={\n \"customJWTAuthorizer\": {\n \"discoveryUrl\": DISCOVERY_URL,\n \"allowedClients\": [GW_CLIENT_ID],\n }\n },\n)\n\nGATEWAY_ID = gw_resp[\"gatewayId\"]\nGATEWAY_URL = gw_resp[\"gatewayUrl\"]\nprint(f\"Gateway created: {GATEWAY_ID}\")\nprint(f\"Gateway URL: {GATEWAY_URL}\")\n\n# Wait for gateway to become READY\nprint(\"Waiting for gateway to become READY...\")\nfor _ in range(60):\n status = gateway_client.get_gateway(gatewayIdentifier=GATEWAY_ID)[\"status\"]\n print(f\" Status: {status}\")\n if status == \"READY\":\n break\n time.sleep(5)" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 4: Create CustomOauth2 Credential Provider\n", + "\n", + "AgentCore Gateway needs OAuth2 credentials to authenticate to Salesforce on behalf of the agent.\n", + "\n", + "> **Important:** Use `CustomOauth2` — NOT `SalesforceOauth2`. The built-in Salesforce vendor uses `login.salesforce.com` which does not support `client_credentials` on Developer Edition org domains. With `CustomOauth2`, we specify the org-specific token endpoint directly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "CREDENTIAL_PROVIDER_NAME = f\"{GATEWAY_NAME}-sf-oauth\"\n", + "\n", + "identity_client = boto3.client(\"bedrock-agentcore-control\", region_name=REGION)\n", + "\n", + "cred_resp = identity_client.create_oauth2_credential_provider(\n", + " name=CREDENTIAL_PROVIDER_NAME,\n", + " credentialProviderVendor=\"CustomOauth2\",\n", + " oauth2ProviderConfigInput={\n", + " \"customOauth2ProviderConfig\": {\n", + " \"clientId\": SF_CLIENT_ID,\n", + " \"clientSecret\": SF_CLIENT_SECRET,\n", + " \"oauthDiscovery\": {\n", + " \"authorizationServerMetadata\": {\n", + " \"issuer\": f\"https://{SF_DOMAIN}.develop.my.salesforce.com\",\n", + " \"authorizationEndpoint\": f\"https://{SF_DOMAIN}.develop.my.salesforce.com/services/oauth2/authorize\",\n", + " \"tokenEndpoint\": f\"https://{SF_DOMAIN}.develop.my.salesforce.com/services/oauth2/token\",\n", + " }\n", + " },\n", + " }\n", + " },\n", + ")\n", + "\n", + "CREDENTIAL_PROVIDER_ARN = cred_resp[\"credentialProviderArn\"]\n", + "print(f\"Created credential provider: {CREDENTIAL_PROVIDER_NAME}\")\n", + "print(f\"ARN: {CREDENTIAL_PROVIDER_ARN}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 5: Add Salesforce as Gateway Target\n", + "\n", + "Now we add Salesforce Lightning Platform as an integration target on the gateway. The gateway uses the pre-built Salesforce template to expose Salesforce REST APIs as MCP tools." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "SF_TARGET_NAME = \"salesforce-target\"\nSF_SERVER_URL = f\"https://{SF_DOMAIN}.develop.my.salesforce.com/services/data/v62.0\"\n\ntarget_resp = gateway_client.create_gateway_target(\n gatewayIdentifier=GATEWAY_ID,\n name=SF_TARGET_NAME,\n targetConfiguration={\n \"mcp\": {\n \"openApiSchema\": {\n \"s3\": {\n \"uri\": \"s3://amazonbedrockagentcore-built-sampleschemas455e0815-oj7jujcd8xiu/salesforce-open-api.json\"\n }\n }\n }\n },\n credentialProviderConfigurations=[\n {\n \"credentialProviderType\": \"OAUTH\",\n \"credentialProvider\": {\n \"oauthCredentialProvider\": {\n \"providerArn\": CREDENTIAL_PROVIDER_ARN,\n \"scopes\": [],\n \"grantType\": \"CLIENT_CREDENTIALS\",\n }\n },\n }\n ],\n)\n\nSF_TARGET_ID = target_resp[\"targetId\"]\nprint(f\"Created Salesforce target: {SF_TARGET_NAME} ({SF_TARGET_ID})\")\nprint(\"Waiting for target to reach READY status...\")" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Wait for target to become READY\n", + "gateway_mcp_client.wait_for_target_ready(\n", + " client=gateway_client,\n", + " gateway_id=GATEWAY_ID,\n", + " target_name=SF_TARGET_NAME,\n", + " region=REGION,\n", + " timeout=300,\n", + ")\n", + "print(\"\\n✓ Salesforce target is READY\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 6: List Salesforce Tools via Gateway\n", + "\n", + "Now that the target is ready, we can list all available Salesforce tools through the gateway's MCP endpoint." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get a gateway access token\n", + "def get_gw_token() -> str:\n", + " return gateway_mcp_client.get_cognito_m2m_token(\n", + " token_endpoint=TOKEN_ENDPOINT,\n", + " client_id=GW_CLIENT_ID,\n", + " client_secret=GW_CLIENT_SECRET,\n", + " scope=FULL_SCOPE,\n", + " )\n", + "\n", + "mcp = gateway_mcp_client.GatewayMCPClient(\n", + " gateway_url=GATEWAY_URL,\n", + " get_token=get_gw_token,\n", + " session_id=str(uuid.uuid4()),\n", + ")\n", + "\n", + "# List all tools (handles pagination automatically)\n", + "all_tools = mcp.list_all_tools()\n", + "sf_tools = [t for t in all_tools if t[\"name\"].startswith(\"salesforce-target___\")]\n", + "\n", + "print(f\"Total tools on gateway: {len(all_tools)}\")\n", + "print(f\"Salesforce tools: {len(sf_tools)}\")\n", + "print(\"\\nSalesforce tool names:\")\n", + "for t in sorted(sf_tools, key=lambda x: x[\"name\"]):\n", + " print(f\" - {t['name']}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 7: Invoke Salesforce Tools\n", + "\n", + "Let's call some Salesforce tools through the gateway.\n", + "\n", + "> **Important:** Every Salesforce tool call requires the `domainName` parameter. Without it, the gateway resolves to a non-existent default domain." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Query accounts using SOQL (more reliable than getAccountList)\n", + "result = mcp.call_tool(\n", + " \"salesforce-target___queryAccounts\",\n", + " {\"domainName\": SF_DOMAIN, \"q\": \"SELECT Id, Name, Industry FROM Account LIMIT 5\"},\n", + ")\n", + "print(\"=== Query Accounts (SOQL) ===\")\n", + "print(json.dumps(result.get(\"result\", {}), indent=2)[:2000])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get account list (returns recently viewed accounts)\n", + "result = mcp.call_tool(\n", + " \"salesforce-target___getAccountList\",\n", + " {\"domainName\": SF_DOMAIN},\n", + ")\n", + "print(\"=== Get Account List ===\")\n", + "print(json.dumps(result.get(\"result\", {}), indent=2)[:2000])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create a test account\n", + "# Note: pass Content-Type as empty string to work around a known header duplication issue\n", + "result = mcp.call_tool(\n", + " \"salesforce-target___createAccount\",\n", + " {\n", + " \"domainName\": SF_DOMAIN,\n", + " \"Content-Type\": \"\",\n", + " \"Name\": \"AgentCore Tutorial Test Account\",\n", + " \"Industry\": \"Technology\",\n", + " \"Description\": \"Created by AgentCore Gateway multi-ISV tutorial\",\n", + " },\n", + ")\n", + "print(\"=== Create Account ===\")\n", + "print(json.dumps(result.get(\"result\", {}), indent=2)[:2000])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Describe an SObject to see available fields\n", + "result = mcp.call_tool(\n", + " \"salesforce-target___describeSObject\",\n", + " {\"domainName\": SF_DOMAIN, \"sObjectType\": \"Account\"},\n", + ")\n", + "print(\"=== Describe Account SObject ===\")\n", + "content = result.get(\"result\", {}).get(\"content\", [])\n", + "if content:\n", + " text = content[0].get(\"text\", \"\")\n", + " try:\n", + " data = json.loads(text)\n", + " fields = data.get(\"fields\", [])[:10]\n", + " print(f\"Total fields: {len(data.get('fields', []))}\")\n", + " print(\"First 10 fields:\")\n", + " for f in fields:\n", + " print(f\" - {f['name']} ({f['type']})\")\n", + " except json.JSONDecodeError:\n", + " print(text[:1000])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Known Issues & Workarounds\n", + "\n", + "| Issue | Workaround |\n", + "|---|---|\n", + "| `create*` tools fail with HTTP 415 | Pass `\"Content-Type\": \"\"` (empty string) in tool arguments |\n", + "| Tool calls return HTTP 420 | Always include `domainName` in arguments; also check if org is hibernating |\n", + "| `getAccountList` returns only recently viewed | Use `queryAccounts` with SOQL: `SELECT Id, Name FROM Account` |\n", + "| `SalesforceOauth2` vendor fails with Dev orgs | Use `CustomOauth2` with org-specific token endpoint (as done above) |\n", + "| Org hibernation (HTTP 420 after inactivity) | Log into Salesforce web UI to wake the org |" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 8: Use Strands Agent with Salesforce Tools\n", + "\n", + "Now let's connect a Strands Agent to the gateway and let it use Salesforce tools via natural language." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "from strands import Agent\nfrom strands.tools.mcp import MCPClient\nfrom mcp import ClientSession\nfrom mcp.client.streamable_http import streamablehttp_client\n\n# Connect to gateway via MCP\nmcp_client = MCPClient(\n lambda: streamablehttp_client(\n url=GATEWAY_URL,\n headers={\n \"Authorization\": f\"Bearer {get_gw_token()}\",\n \"MCP-Protocol-Version\": \"2025-03-26\",\n },\n )\n)\n\nSYSTEM_PROMPT = (\n \"You are a helpful assistant with access to Salesforce tools. \"\n f\"Always include domainName='{SF_DOMAIN}' in every Salesforce tool call. \"\n \"Do not pass a Content-Type parameter to Salesforce create operations — omit it entirely or pass empty string. \"\n \"Use queryAccounts with SOQL for listing accounts rather than getAccountList.\"\n)\n\nwith mcp_client:\n agent = Agent(\n model=\"us.anthropic.claude-sonnet-4-6-v1\",\n system_prompt=SYSTEM_PROMPT,\n tools=mcp_client.list_tools_sync(),\n )\n result = agent(\"List all Salesforce accounts with their industry\")\n print(result)" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 9: Clean Up\n", + "\n", + "Delete all resources created in this notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Delete gateway target\n", + "print(\"Deleting Salesforce gateway target...\")\n", + "gateway_client.delete_gateway_target(\n", + " gatewayIdentifier=GATEWAY_ID,\n", + " targetId=SF_TARGET_ID,\n", + ")\n", + "print(\" ✓ Target deleted\")\n", + "\n", + "# Delete credential provider\n", + "print(\"Deleting credential provider...\")\n", + "identity_client.delete_oauth2_credential_provider(name=CREDENTIAL_PROVIDER_NAME)\n", + "print(\" ✓ Credential provider deleted\")\n", + "\n", + "# Delete gateway\n", + "print(\"Deleting gateway...\")\n", + "gateway_client.delete_gateway(gatewayIdentifier=GATEWAY_ID)\n", + "print(\" ✓ Gateway deleted\")\n", + "\n", + "# Delete Cognito resources\n", + "print(\"Deleting Cognito resources...\")\n", + "cognito_client.delete_user_pool_domain(Domain=COGNITO_DOMAIN, UserPoolId=USER_POOL_ID)\n", + "cognito_client.delete_user_pool(UserPoolId=USER_POOL_ID)\n", + "print(\" ✓ Cognito pool deleted\")\n", + "\n", + "# Delete IAM role\n", + "print(\"Deleting IAM role...\")\n", + "iam.delete_role(RoleName=ROLE_NAME)\n", + "print(\" ✓ IAM role deleted\")\n", + "\n", + "print(\"\\n✓ All resources cleaned up\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} \ No newline at end of file diff --git a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/02-sap-mcp-server-target.ipynb b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/02-sap-mcp-server-target.ipynb new file mode 100644 index 000000000..3df6300f3 --- /dev/null +++ b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/02-sap-mcp-server-target.ipynb @@ -0,0 +1,609 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# AWS for SAP MCP Server as AgentCore Gateway Target\n", + "\n", + "## Overview\n", + "\n", + "This notebook walks through adding the [AWS for SAP MCP Server](https://docs.aws.amazon.com/mcp-sap/latest/awsforsapmcp/introduction.html) as an MCP target on [Amazon Bedrock AgentCore Gateway](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway.html). The SAP MCP Server provides AI agents with structured, protocol-driven access to SAP S/4HANA and SAP ECC OData V2 services.\n", + "\n", + "| Information | Details |\n", + "|:---|:---|\n", + "| Target type | MCP Server Target |\n", + "| SAP MCP Server | [AWS for SAP MCP Server](https://docs.aws.amazon.com/mcp-sap/latest/awsforsapmcp/introduction.html) |\n", + "| Communication | Streamable HTTP (`/mcp` endpoint) |\n", + "| Outbound auth | CustomOauth2 (SAP MCP Server Cognito) |\n", + "| Inbound auth | Amazon Cognito (M2M) |\n", + "| Tools exposed | 9 (5 read + 4 write, read-only by default) |\n", + "| Default mode | Read-only (write operations disabled unless explicitly enabled) |" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Prerequisites\n", + "\n", + "Before starting this notebook, you need:\n", + "\n", + "1. **AWS account** with access to Amazon Bedrock AgentCore\n", + "2. **AWS for SAP MCP Server** deployed — see [Getting Started](https://docs.aws.amazon.com/mcp-sap/latest/awsforsapmcp/getting-started.html)\n", + "3. **SAP MCP Server endpoint URL** and **Cognito credentials** for M2M authentication\n", + "4. **Python 3.11+** with Jupyter kernel\n", + "5. **AWS CLI** configured with appropriate credentials\n", + "\n", + "You can either create a new gateway here or reuse the one created in [01-salesforce-gateway-target.ipynb](01-salesforce-gateway-target.ipynb)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip install -r requirements.txt --quiet" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import json\n", + "import logging\n", + "import time\n", + "import uuid\n", + "\n", + "import boto3\n", + "import requests\n", + "from boto3.session import Session\n", + "\n", + "import gateway_mcp_client\n", + "\n", + "logging.basicConfig(\n", + " level=logging.INFO,\n", + " format=\"%(asctime)s | %(levelname)s | %(name)s | %(message)s\",\n", + " handlers=[logging.StreamHandler()],\n", + ")\n", + "\n", + "session = Session()\n", + "REGION = session.region_name or \"us-east-1\"\n", + "print(f\"Using region: {REGION}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Collect SAP MCP Server credentials\n", + "SAP_MCP_ENDPOINT = input(\"Enter the SAP MCP Server endpoint URL (e.g., https:///mcp): \")\n", + "SAP_TOKEN_ENDPOINT = input(\"Enter the SAP MCP Server Cognito token endpoint: \")\n", + "SAP_CLIENT_ID = input(\"Enter the SAP MCP Server Client ID: \")\n", + "SAP_CLIENT_SECRET = getpass.getpass(\"Enter the SAP MCP Server Client Secret: \")\n", + "SAP_SCOPE = input(\"Enter the OAuth scope (e.g., /invoke): \")\n", + "\n", + "assert SAP_MCP_ENDPOINT.strip(), \"SAP MCP endpoint cannot be empty\"\n", + "assert SAP_TOKEN_ENDPOINT.strip(), \"Token endpoint cannot be empty\"\n", + "assert SAP_CLIENT_ID.strip(), \"Client ID cannot be empty\"\n", + "assert SAP_CLIENT_SECRET.strip(), \"Client Secret cannot be empty\"\n", + "\n", + "print(f\"\\nSAP MCP endpoint: {SAP_MCP_ENDPOINT}\")\n", + "print(f\"Token endpoint: {SAP_TOKEN_ENDPOINT}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## AWS for SAP MCP Server — Architecture\n", + "\n", + "The [AWS for SAP MCP Server](https://docs.aws.amazon.com/mcp-sap/latest/awsforsapmcp/introduction.html) runs on Amazon Bedrock AgentCore Runtime and provides AI agents with access to SAP S/4HANA and SAP ECC OData V2 services.\n", + "\n", + "**Key characteristics:**\n", + "- Communicates via **Streamable HTTP** on a `/mcp` endpoint\n", + "- **Read-only by default** — write operations require explicit opt-in\n", + "- Credentials are **never stored on disk** — retrieved at runtime from AWS Secrets Manager\n", + "- Supports **Basic Auth** or **OAuth 2.0** for outbound SAP authentication\n", + "- Deployed via **CloudFormation template**\n", + "\n", + "### Available Tools\n", + "\n", + "| Tool | Category | Description |\n", + "|---|---|---|\n", + "| `find_sap_services` | Read | Discover available SAP OData services from the catalog |\n", + "| `get_metadata` | Read | Retrieve OData service metadata (entity types, properties) |\n", + "| `get_service_hints` | Read | Get usage guidance for specific services |\n", + "| `odata_read` | Read | Read data from SAP OData entity sets with filtering |\n", + "| `odata_count` | Read | Count records in an entity set (use before reads) |\n", + "| `odata_create` | Write | Create new entity records (deep insert supported) |\n", + "| `odata_update` | Write | Update existing entity records |\n", + "| `odata_delete` | Write | Delete entity records |\n", + "| `odata_function_import` | Write | Execute custom backend logic |\n", + "\n", + "> **Security:** Write tools are only registered when explicitly enabled in the SAP MCP Server configuration. Follow the principle of least privilege — only enable the specific write operations your use case requires." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "REUSE_GATEWAY = input(\"Reuse an existing gateway? (yes/no): \").strip().lower() == \"yes\"\n\nif REUSE_GATEWAY:\n GATEWAY_ID = input(\"Enter existing Gateway ID: \")\n GATEWAY_URL = input(\"Enter existing Gateway URL: \")\n GW_CLIENT_ID = input(\"Enter Cognito Client ID for the gateway: \")\n GW_CLIENT_SECRET = getpass.getpass(\"Enter Cognito Client Secret for the gateway: \")\n TOKEN_ENDPOINT = input(\"Enter Cognito token endpoint: \")\n FULL_SCOPE = input(\"Enter OAuth scope: \")\n CREATED_GATEWAY = False\n print(f\"\\nReusing gateway: {GATEWAY_ID}\")\nelse:\n CREATED_GATEWAY = True\n GATEWAY_NAME = f\"multi-isv-sap-tutorial-{str(uuid.uuid4())[:8]}\"\n print(f\"Creating new gateway: {GATEWAY_NAME}\")\n\n # Create Cognito User Pool\n cognito_client = boto3.client(\"cognito-idp\", region_name=REGION)\n pool_resp = cognito_client.create_user_pool(\n PoolName=f\"{GATEWAY_NAME}-pool\",\n Policies={\"PasswordPolicy\": {\"MinimumLength\": 8}},\n )\n USER_POOL_ID = pool_resp[\"UserPool\"][\"Id\"]\n\n COGNITO_DOMAIN = f\"{GATEWAY_NAME}-domain\"\n cognito_client.create_user_pool_domain(Domain=COGNITO_DOMAIN, UserPoolId=USER_POOL_ID)\n TOKEN_ENDPOINT = f\"https://{COGNITO_DOMAIN}.auth.{REGION}.amazoncognito.com/oauth2/token\"\n\n RESOURCE_SERVER_ID = f\"{GATEWAY_NAME}-id\"\n SCOPE_NAME = \"invoke\"\n cognito_client.create_resource_server(\n UserPoolId=USER_POOL_ID,\n Identifier=RESOURCE_SERVER_ID,\n Name=f\"{GATEWAY_NAME} Gateway\",\n Scopes=[{\"ScopeName\": SCOPE_NAME, \"ScopeDescription\": \"Invoke gateway\"}],\n )\n FULL_SCOPE = f\"{RESOURCE_SERVER_ID}/{SCOPE_NAME}\"\n\n app_resp = cognito_client.create_user_pool_client(\n UserPoolId=USER_POOL_ID,\n ClientName=f\"{GATEWAY_NAME}-client\",\n GenerateSecret=True,\n AllowedOAuthFlows=[\"client_credentials\"],\n AllowedOAuthScopes=[FULL_SCOPE],\n AllowedOAuthFlowsUserPoolClient=True,\n )\n GW_CLIENT_ID = app_resp[\"UserPoolClient\"][\"ClientId\"]\n GW_CLIENT_SECRET = app_resp[\"UserPoolClient\"][\"ClientSecret\"]\n DISCOVERY_URL = f\"https://cognito-idp.{REGION}.amazonaws.com/{USER_POOL_ID}/.well-known/openid-configuration\"\n\n # Create IAM role\n iam = boto3.client(\"iam\")\n ROLE_NAME = f\"agentcore-{GATEWAY_NAME}-role\"\n trust_policy = {\n \"Version\": \"2012-10-17\",\n \"Statement\": [{\n \"Effect\": \"Allow\",\n \"Principal\": {\"Service\": \"bedrock-agentcore.amazonaws.com\"},\n \"Action\": \"sts:AssumeRole\",\n }],\n }\n role_resp = iam.create_role(\n RoleName=ROLE_NAME,\n AssumeRolePolicyDocument=json.dumps(trust_policy),\n Description=\"IAM role for AgentCore Gateway SAP tutorial\",\n )\n ROLE_ARN = role_resp[\"Role\"][\"Arn\"]\n time.sleep(10)\n\n # Create gateway\n gw_client = boto3.client(\"bedrock-agentcore-control\", region_name=REGION)\n gw_resp = gw_client.create_gateway(\n name=GATEWAY_NAME,\n roleArn=ROLE_ARN,\n protocolType=\"MCP\",\n authorizerType=\"CUSTOM_JWT\",\n authorizerConfiguration={\n \"customJWTAuthorizer\": {\n \"discoveryUrl\": DISCOVERY_URL,\n \"allowedClients\": [GW_CLIENT_ID],\n }\n },\n )\n GATEWAY_ID = gw_resp[\"gatewayId\"]\n GATEWAY_URL = gw_resp[\"gatewayUrl\"]\n\n print(f\"Gateway created: {GATEWAY_ID}\")\n print(\"Waiting for READY...\")\n for _ in range(60):\n status = gw_client.get_gateway(gatewayIdentifier=GATEWAY_ID)[\"status\"]\n if status == \"READY\":\n break\n time.sleep(5)\n print(\"✓ Gateway is READY\")\n\ngateway_client = boto3.client(\"bedrock-agentcore-control\", region_name=REGION)" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Test SAP MCP Server authentication\n", + "sap_token = gateway_mcp_client.get_cognito_m2m_token(\n", + " token_endpoint=SAP_TOKEN_ENDPOINT,\n", + " client_id=SAP_CLIENT_ID,\n", + " client_secret=SAP_CLIENT_SECRET,\n", + " scope=SAP_SCOPE,\n", + ")\n", + "print(f\"✓ SAP MCP Server token obtained ({len(sap_token)} chars)\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 2: Create Gateway (or Reuse Existing)\n", + "\n", + "If you already have a gateway from the Salesforce notebook, you can skip this step and provide the existing gateway details." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "REUSE_GATEWAY = input(\"Reuse an existing gateway? (yes/no): \").strip().lower() == \"yes\"\n", + "\n", + "if REUSE_GATEWAY:\n", + " GATEWAY_ID = input(\"Enter existing Gateway ID: \")\n", + " GATEWAY_URL = input(\"Enter existing Gateway URL: \")\n", + " GW_CLIENT_ID = input(\"Enter Cognito Client ID for the gateway: \")\n", + " GW_CLIENT_SECRET = getpass.getpass(\"Enter Cognito Client Secret for the gateway: \")\n", + " TOKEN_ENDPOINT = input(\"Enter Cognito token endpoint: \")\n", + " FULL_SCOPE = input(\"Enter OAuth scope: \")\n", + " CREATED_GATEWAY = False\n", + " print(f\"\\nReusing gateway: {GATEWAY_ID}\")\n", + "else:\n", + " CREATED_GATEWAY = True\n", + " GATEWAY_NAME = f\"multi-isv-sap-tutorial-{str(uuid.uuid4())[:8]}\"\n", + " print(f\"Creating new gateway: {GATEWAY_NAME}\")\n", + "\n", + " # Create Cognito User Pool\n", + " cognito_client = boto3.client(\"cognito-idp\", region_name=REGION)\n", + " pool_resp = cognito_client.create_user_pool(\n", + " PoolName=f\"{GATEWAY_NAME}-pool\",\n", + " Policies={\"PasswordPolicy\": {\"MinimumLength\": 8}},\n", + " )\n", + " USER_POOL_ID = pool_resp[\"UserPool\"][\"Id\"]\n", + "\n", + " COGNITO_DOMAIN = f\"{GATEWAY_NAME}-domain\"\n", + " cognito_client.create_user_pool_domain(Domain=COGNITO_DOMAIN, UserPoolId=USER_POOL_ID)\n", + " TOKEN_ENDPOINT = f\"https://{COGNITO_DOMAIN}.auth.{REGION}.amazoncognito.com/oauth2/token\"\n", + "\n", + " RESOURCE_SERVER_ID = f\"{GATEWAY_NAME}-id\"\n", + " SCOPE_NAME = \"invoke\"\n", + " cognito_client.create_resource_server(\n", + " UserPoolId=USER_POOL_ID,\n", + " Identifier=RESOURCE_SERVER_ID,\n", + " Name=f\"{GATEWAY_NAME} Gateway\",\n", + " Scopes=[{\"ScopeName\": SCOPE_NAME, \"ScopeDescription\": \"Invoke gateway\"}],\n", + " )\n", + " FULL_SCOPE = f\"{RESOURCE_SERVER_ID}/{SCOPE_NAME}\"\n", + "\n", + " app_resp = cognito_client.create_user_pool_client(\n", + " UserPoolId=USER_POOL_ID,\n", + " ClientName=f\"{GATEWAY_NAME}-client\",\n", + " GenerateSecret=True,\n", + " AllowedOAuthFlows=[\"client_credentials\"],\n", + " AllowedOAuthScopes=[FULL_SCOPE],\n", + " AllowedOAuthFlowsUserPoolClient=True,\n", + " )\n", + " GW_CLIENT_ID = app_resp[\"UserPoolClient\"][\"ClientId\"]\n", + " GW_CLIENT_SECRET = app_resp[\"UserPoolClient\"][\"ClientSecret\"]\n", + " DISCOVERY_URL = f\"https://cognito-idp.{REGION}.amazonaws.com/{USER_POOL_ID}/.well-known/openid-configuration\"\n", + "\n", + " # Create IAM role\n", + " iam = boto3.client(\"iam\")\n", + " ROLE_NAME = f\"agentcore-{GATEWAY_NAME}-role\"\n", + " trust_policy = {\n", + " \"Version\": \"2012-10-17\",\n", + " \"Statement\": [{\n", + " \"Effect\": \"Allow\",\n", + " \"Principal\": {\"Service\": \"gateway.bedrock-agentcore.amazonaws.com\"},\n", + " \"Action\": \"sts:AssumeRole\",\n", + " }],\n", + " }\n", + " role_resp = iam.create_role(\n", + " RoleName=ROLE_NAME,\n", + " AssumeRolePolicyDocument=json.dumps(trust_policy),\n", + " Description=\"IAM role for AgentCore Gateway SAP tutorial\",\n", + " )\n", + " ROLE_ARN = role_resp[\"Role\"][\"Arn\"]\n", + " time.sleep(10)\n", + "\n", + " # Create gateway\n", + " gw_client = boto3.client(\"bedrock-agentcore-control\", region_name=REGION)\n", + " gw_resp = gw_client.create_gateway(\n", + " name=GATEWAY_NAME,\n", + " roleArn=ROLE_ARN,\n", + " protocolType=\"MCP\",\n", + " authorizerType=\"CUSTOM_JWT\",\n", + " authorizerConfiguration={\n", + " \"customJWTAuthorizer\": {\n", + " \"discoveryUrl\": DISCOVERY_URL,\n", + " \"allowedAudiences\": [GW_CLIENT_ID],\n", + " \"allowedClients\": [GW_CLIENT_ID],\n", + " }\n", + " },\n", + " )\n", + " GATEWAY_ID = gw_resp[\"gatewayId\"]\n", + " GATEWAY_URL = gw_resp[\"gatewayUrl\"]\n", + "\n", + " print(f\"Gateway created: {GATEWAY_ID}\")\n", + " print(\"Waiting for ACTIVE...\")\n", + " while True:\n", + " status = gw_client.get_gateway(gatewayIdentifier=GATEWAY_ID)[\"status\"]\n", + " if status == \"ACTIVE\":\n", + " break\n", + " time.sleep(5)\n", + " print(\"✓ Gateway is ACTIVE\")\n", + "\n", + "gateway_client = boto3.client(\"bedrock-agentcore-control\", region_name=REGION)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "SAP_TARGET_NAME = \"sap-target\"\n\ntarget_resp = gateway_client.create_gateway_target(\n gatewayIdentifier=GATEWAY_ID,\n name=SAP_TARGET_NAME,\n targetConfiguration={\n \"mcp\": {\n \"mcpServer\": {\n \"endpoint\": SAP_MCP_ENDPOINT,\n }\n }\n },\n credentialProviderConfigurations=[\n {\n \"credentialProviderType\": \"OAUTH\",\n \"credentialProvider\": {\n \"oauthCredentialProvider\": {\n \"providerArn\": SAP_CREDENTIAL_PROVIDER_ARN,\n \"scopes\": [],\n \"grantType\": \"CLIENT_CREDENTIALS\",\n }\n },\n }\n ],\n)\n\nSAP_TARGET_ID = target_resp[\"targetId\"]\nprint(f\"Created SAP target: {SAP_TARGET_NAME} ({SAP_TARGET_ID})\")\nprint(\"Waiting for target to reach READY status...\")" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "SAP_CREDENTIAL_PROVIDER_NAME = f\"sap-mcp-oauth-{str(uuid.uuid4())[:8]}\"\n", + "\n", + "# Extract Cognito discovery URL from token endpoint\n", + "# Token endpoint format: https://.auth..amazoncognito.com/oauth2/token\n", + "SAP_DISCOVERY_URL = input(\n", + " \"Enter the SAP MCP Server Cognito discovery URL\\n\"\n", + " \"(e.g., https://cognito-idp..amazonaws.com//.well-known/openid-configuration): \"\n", + ")\n", + "\n", + "cred_resp = gateway_client.create_oauth2_credential_provider(\n", + " name=SAP_CREDENTIAL_PROVIDER_NAME,\n", + " credentialProviderVendor=\"CustomOauth2\",\n", + " oauth2ProviderConfigInput={\n", + " \"customOauth2ProviderConfig\": {\n", + " \"clientId\": SAP_CLIENT_ID,\n", + " \"clientSecret\": SAP_CLIENT_SECRET,\n", + " \"oauthDiscovery\": {\n", + " \"discoveryUrl\": SAP_DISCOVERY_URL,\n", + " },\n", + " }\n", + " },\n", + ")\n", + "\n", + "SAP_CREDENTIAL_PROVIDER_ARN = cred_resp[\"credentialProviderArn\"]\n", + "print(f\"Created SAP credential provider: {SAP_CREDENTIAL_PROVIDER_NAME}\")\n", + "print(f\"ARN: {SAP_CREDENTIAL_PROVIDER_ARN}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 4: Add SAP MCP Server as Gateway Target\n", + "\n", + "We add the SAP MCP Server as an MCP server target on the gateway. This tells the gateway to proxy MCP requests to the SAP MCP Server endpoint." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "SAP_TARGET_NAME = \"sap-target\"\n", + "\n", + "target_resp = gateway_client.create_gateway_target(\n", + " gatewayIdentifier=GATEWAY_ID,\n", + " name=SAP_TARGET_NAME,\n", + " targetConfiguration={\n", + " \"mcpTarget\": {\n", + " \"mcpServer\": {\n", + " \"endpoint\": SAP_MCP_ENDPOINT,\n", + " },\n", + " \"authConfiguration\": {\n", + " \"oauth2Auth\": {\n", + " \"credentialProviderArn\": SAP_CREDENTIAL_PROVIDER_ARN,\n", + " }\n", + " },\n", + " }\n", + " },\n", + ")\n", + "\n", + "SAP_TARGET_ID = target_resp[\"targetId\"]\n", + "print(f\"Created SAP target: {SAP_TARGET_NAME} ({SAP_TARGET_ID})\")\n", + "print(\"Waiting for target to reach READY status...\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Wait for target to become READY\n", + "gateway_mcp_client.wait_for_target_ready(\n", + " client=gateway_client,\n", + " gateway_id=GATEWAY_ID,\n", + " target_name=SAP_TARGET_NAME,\n", + " region=REGION,\n", + " timeout=300,\n", + ")\n", + "print(\"\\n✓ SAP MCP Server target is READY\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 5: Verify SAP Tools via Gateway\n", + "\n", + "Once the target is ready, the SAP MCP Server tools appear via the gateway's `tools/list` endpoint." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "from strands import Agent\nfrom strands.tools.mcp import MCPClient\nfrom mcp import ClientSession\nfrom mcp.client.streamable_http import streamablehttp_client\n\nmcp_client = MCPClient(\n lambda: streamablehttp_client(\n url=GATEWAY_URL,\n headers={\n \"Authorization\": f\"Bearer {get_gw_token()}\",\n \"MCP-Protocol-Version\": \"2025-03-26\",\n },\n )\n)\n\nSYSTEM_PROMPT = (\n \"You are a helpful assistant with access to SAP tools via the AWS for SAP MCP Server. \"\n \"Before reading data, use get_metadata to understand available entity sets and field names. \"\n \"Use odata_count before odata_read to understand data volume. \"\n \"SAP field names can be non-obvious — always check metadata first.\"\n)\n\nwith mcp_client:\n agent = Agent(\n model=\"us.anthropic.claude-sonnet-4-6-v1\",\n system_prompt=SYSTEM_PROMPT,\n tools=mcp_client.list_tools_sync(),\n )\n result = agent(\"What SAP services are available for sales orders? Show me the first 3 sales orders.\")\n print(result)" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 6: Invoke SAP Tools\n", + "\n", + "Let's call the SAP MCP Server tools through the gateway to explore available services and read data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Discover available SAP services\n", + "result = mcp.call_tool(\n", + " \"sap-target___find_sap_services\",\n", + " {\"search_term\": \"sales\", \"top\": 5},\n", + ")\n", + "print(\"=== Find SAP Services (sales) ===\")\n", + "content = result.get(\"result\", {}).get(\"content\", [])\n", + "for item in content:\n", + " if item.get(\"type\") == \"text\":\n", + " print(item[\"text\"][:2000])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get metadata for Business Partner API\n", + "result = mcp.call_tool(\n", + " \"sap-target___get_metadata\",\n", + " {\"service_name\": \"API_BUSINESS_PARTNER\"},\n", + ")\n", + "print(\"=== Business Partner API Metadata ===\")\n", + "content = result.get(\"result\", {}).get(\"content\", [])\n", + "for item in content:\n", + " if item.get(\"type\") == \"text\":\n", + " print(item[\"text\"][:3000])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Read business partners\n", + "result = mcp.call_tool(\n", + " \"sap-target___odata_read\",\n", + " {\n", + " \"service_name\": \"API_BUSINESS_PARTNER\",\n", + " \"entity_set\": \"A_BusinessPartner\",\n", + " \"top\": 5,\n", + " \"select\": \"BusinessPartner,BusinessPartnerFullName,BusinessPartnerCategory\",\n", + " },\n", + ")\n", + "print(\"=== Read Business Partners ===\")\n", + "content = result.get(\"result\", {}).get(\"content\", [])\n", + "for item in content:\n", + " if item.get(\"type\") == \"text\":\n", + " try:\n", + " data = json.loads(item[\"text\"])\n", + " print(json.dumps(data, indent=2)[:3000])\n", + " except json.JSONDecodeError:\n", + " print(item[\"text\"][:3000])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Count records in an entity set\n", + "result = mcp.call_tool(\n", + " \"sap-target___odata_count\",\n", + " {\n", + " \"service_name\": \"API_BUSINESS_PARTNER\",\n", + " \"entity_set\": \"A_BusinessPartner\",\n", + " },\n", + ")\n", + "print(\"=== Business Partner Count ===\")\n", + "content = result.get(\"result\", {}).get(\"content\", [])\n", + "for item in content:\n", + " if item.get(\"type\") == \"text\":\n", + " print(f\"Total business partners: {item['text']}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 7: Use Strands Agent with SAP Tools\n", + "\n", + "Connect a Strands Agent to the gateway and let it explore SAP data via natural language." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from strands import Agent\n", + "from strands.tools.mcp import MCPClient\n", + "from mcp import ClientSession\n", + "from mcp.client.streamable_http import streamablehttp_client\n", + "\n", + "mcp_client = MCPClient(\n", + " lambda: streamablehttp_client(\n", + " url=GATEWAY_URL,\n", + " headers={\n", + " \"Authorization\": f\"Bearer {get_gw_token()}\",\n", + " \"MCP-Protocol-Version\": \"2025-11-25\",\n", + " },\n", + " )\n", + ")\n", + "\n", + "SYSTEM_PROMPT = (\n", + " \"You are a helpful assistant with access to SAP tools via the AWS for SAP MCP Server. \"\n", + " \"Before reading data, use get_metadata to understand available entity sets and field names. \"\n", + " \"Use odata_count before odata_read to understand data volume. \"\n", + " \"SAP field names can be non-obvious — always check metadata first.\"\n", + ")\n", + "\n", + "with mcp_client:\n", + " agent = Agent(\n", + " model=\"us.anthropic.claude-sonnet-4-6-v1\",\n", + " system_prompt=SYSTEM_PROMPT,\n", + " tools=mcp_client.list_tools_sync(),\n", + " )\n", + " result = agent(\"What SAP services are available for sales orders? Show me the first 3 sales orders.\")\n", + " print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Best Practices\n", + "\n", + "From the [AWS for SAP MCP Server documentation](https://docs.aws.amazon.com/mcp-sap/latest/awsforsapmcp/security.html):\n", + "\n", + "1. **Read-only by default** — Write tools are disabled unless both global and per-operation flags are set to `true`. Only enable writes you actually need.\n", + "2. **Principle of least privilege** — Assign the SAP user only the minimum necessary roles and authorizations.\n", + "3. **Scope OAuth access** — Configure OAuth scopes to grant access only to specific required OData services.\n", + "4. **Use odata_count before reads** — Understand data volume before issuing broad reads to avoid overwhelming the agent.\n", + "5. **Use get_metadata proactively** — SAP field names are often non-obvious (e.g., `OverallOrdReltdBillgStatus` not `OverallBillingStatus`). Always check metadata before constructing filters.\n", + "6. **Credentials never stored on disk** — The MCP Server retrieves credentials at runtime from AWS Secrets Manager." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 8: Clean Up\n", + "\n", + "Delete resources created in this notebook. Skip if you plan to continue with [03-cross-isv-queries.ipynb](03-cross-isv-queries.ipynb)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "SKIP_CLEANUP = input(\"Skip cleanup to use gateway in notebook 03? (yes/no): \").strip().lower() == \"yes\"\n", + "\n", + "if not SKIP_CLEANUP:\n", + " # Delete SAP gateway target\n", + " print(\"Deleting SAP gateway target...\")\n", + " gateway_client.delete_gateway_target(gatewayIdentifier=GATEWAY_ID, targetId=SAP_TARGET_ID)\n", + " print(\" ✓ SAP target deleted\")\n", + "\n", + " # Delete SAP credential provider\n", + " print(\"Deleting SAP credential provider...\")\n", + " gateway_client.delete_oauth2_credential_provider(name=SAP_CREDENTIAL_PROVIDER_NAME)\n", + " print(\" ✓ Credential provider deleted\")\n", + "\n", + " if CREATED_GATEWAY:\n", + " # Delete gateway\n", + " print(\"Deleting gateway...\")\n", + " gateway_client.delete_gateway(gatewayIdentifier=GATEWAY_ID)\n", + " print(\" ✓ Gateway deleted\")\n", + "\n", + " # Delete Cognito resources\n", + " print(\"Deleting Cognito resources...\")\n", + " cognito_client.delete_user_pool_domain(Domain=COGNITO_DOMAIN, UserPoolId=USER_POOL_ID)\n", + " cognito_client.delete_user_pool(UserPoolId=USER_POOL_ID)\n", + " print(\" ✓ Cognito pool deleted\")\n", + "\n", + " # Delete IAM role\n", + " print(\"Deleting IAM role...\")\n", + " iam.delete_role(RoleName=ROLE_NAME)\n", + " print(\" ✓ IAM role deleted\")\n", + "\n", + " print(\"\\n✓ All resources cleaned up\")\n", + "else:\n", + " print(\"\\nSkipping cleanup. Remember to clean up resources when done!\")\n", + " print(f\" Gateway ID: {GATEWAY_ID}\")\n", + " print(f\" Gateway URL: {GATEWAY_URL}\")\n", + " print(f\" SAP Target ID: {SAP_TARGET_ID}\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} \ No newline at end of file diff --git a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/03-cross-isv-queries.ipynb b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/03-cross-isv-queries.ipynb new file mode 100644 index 000000000..3789cf3fc --- /dev/null +++ b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/03-cross-isv-queries.ipynb @@ -0,0 +1,430 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Cross-ISV Queries: Salesforce + SAP via Single Gateway\n", + "\n", + "## Overview\n", + "\n", + "This notebook demonstrates the power of routing multiple ISV platforms through a single [Amazon Bedrock AgentCore Gateway](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway.html). With Salesforce (CRM) and SAP (ERP) connected to the same gateway, an AI agent can answer questions that span both systems — without the user needing to know which system holds which data.\n", + "\n", + "**Use cases demonstrated:**\n", + "1. Customer 360 — combine SAP business partner data with Salesforce account history\n", + "2. Pipeline Reconciliation — compare Salesforce opportunities with SAP sales orders\n", + "3. Support Case with ERP Context — enrich Salesforce cases with SAP inventory data\n", + "4. Natural Language Agent — let Claude orchestrate cross-system queries autonomously\n", + "\n", + "| Information | Details |\n", + "|:---|:---|\n", + "| Prerequisites | Both Salesforce + SAP targets READY on same gateway (from notebooks 01 + 02) |\n", + "| Tools available | ~52 (43 Salesforce + 9 SAP) |\n", + "| Agent framework | Strands Agents |\n", + "| LLM | Anthropic Claude Sonnet 4 |" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Prerequisites\n", + "\n", + "This notebook assumes you have completed:\n", + "1. [01-salesforce-gateway-target.ipynb](01-salesforce-gateway-target.ipynb) — Salesforce target is READY\n", + "2. [02-sap-mcp-server-target.ipynb](02-sap-mcp-server-target.ipynb) — SAP target is READY\n", + "\n", + "Both targets must be on the **same gateway**. You'll provide the gateway connection details below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip install -r requirements.txt --quiet" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import json\n", + "import logging\n", + "import uuid\n", + "\n", + "import boto3\n", + "import requests\n", + "from boto3.session import Session\n", + "\n", + "import gateway_mcp_client\n", + "\n", + "logging.basicConfig(\n", + " level=logging.INFO,\n", + " format=\"%(asctime)s | %(levelname)s | %(name)s | %(message)s\",\n", + " handlers=[logging.StreamHandler()],\n", + ")\n", + "\n", + "session = Session()\n", + "REGION = session.region_name or \"us-east-1\"\n", + "print(f\"Using region: {REGION}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Gateway connection details (from notebooks 01 + 02)\n", + "GATEWAY_URL = input(\"Enter Gateway URL: \")\n", + "TOKEN_ENDPOINT = input(\"Enter Cognito token endpoint: \")\n", + "GW_CLIENT_ID = input(\"Enter Cognito Client ID: \")\n", + "GW_CLIENT_SECRET = getpass.getpass(\"Enter Cognito Client Secret: \")\n", + "FULL_SCOPE = input(\"Enter OAuth scope: \")\n", + "\n", + "# Salesforce domain (needed for SF tool calls)\n", + "SF_DOMAIN = input(\"Enter Salesforce domain (e.g., myorg-dev-ed): \")\n", + "\n", + "assert GATEWAY_URL.strip(), \"Gateway URL cannot be empty\"\n", + "assert SF_DOMAIN.strip(), \"Salesforce domain cannot be empty\"\n", + "\n", + "print(f\"\\nGateway: {GATEWAY_URL}\")\n", + "print(f\"Salesforce domain: {SF_DOMAIN}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Connect to the gateway\n", + "def get_gw_token() -> str:\n", + " return gateway_mcp_client.get_cognito_m2m_token(\n", + " token_endpoint=TOKEN_ENDPOINT,\n", + " client_id=GW_CLIENT_ID,\n", + " client_secret=GW_CLIENT_SECRET,\n", + " scope=FULL_SCOPE,\n", + " )\n", + "\n", + "mcp = gateway_mcp_client.GatewayMCPClient(\n", + " gateway_url=GATEWAY_URL,\n", + " get_token=get_gw_token,\n", + " session_id=str(uuid.uuid4()),\n", + ")\n", + "\n", + "# List all tools from both targets\n", + "all_tools = mcp.list_all_tools()\n", + "sf_tools = [t for t in all_tools if t[\"name\"].startswith(\"salesforce-target___\")]\n", + "sap_tools = [t for t in all_tools if t[\"name\"].startswith(\"sap-target___\")]\n", + "\n", + "print(f\"Total tools: {len(all_tools)}\")\n", + "print(f\" Salesforce: {len(sf_tools)}\")\n", + "print(f\" SAP: {len(sap_tools)}\")\n", + "print(f\" Other: {len(all_tools) - len(sf_tools) - len(sap_tools)}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Use Case 1: Customer 360\n", + "\n", + "Combine customer data from both SAP (business partner master data) and Salesforce (CRM account history) to build a unified view.\n", + "\n", + "**Pattern:** Query SAP for business partner details → Query Salesforce for account + opportunities" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# SAP: Get business partner data\n", + "sap_result = mcp.call_tool(\n", + " \"sap-target___odata_read\",\n", + " {\n", + " \"service_name\": \"API_BUSINESS_PARTNER\",\n", + " \"entity_set\": \"A_BusinessPartner\",\n", + " \"top\": 5,\n", + " \"select\": \"BusinessPartner,BusinessPartnerFullName,BusinessPartnerCategory,Industry\",\n", + " },\n", + ")\n", + "\n", + "print(\"=== SAP Business Partners ===\")\n", + "sap_content = sap_result.get(\"result\", {}).get(\"content\", [])\n", + "for item in sap_content:\n", + " if item.get(\"type\") == \"text\":\n", + " try:\n", + " data = json.loads(item[\"text\"])\n", + " print(json.dumps(data, indent=2)[:2000])\n", + " except json.JSONDecodeError:\n", + " print(item[\"text\"][:2000])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Salesforce: Get accounts\n", + "sf_result = mcp.call_tool(\n", + " \"salesforce-target___queryAccounts\",\n", + " {\n", + " \"domainName\": SF_DOMAIN,\n", + " \"q\": \"SELECT Id, Name, Industry, CreatedDate FROM Account LIMIT 5\",\n", + " },\n", + ")\n", + "\n", + "print(\"=== Salesforce Accounts ===\")\n", + "sf_content = sf_result.get(\"result\", {}).get(\"content\", [])\n", + "for item in sf_content:\n", + " if item.get(\"type\") == \"text\":\n", + " try:\n", + " data = json.loads(item[\"text\"])\n", + " print(json.dumps(data, indent=2)[:2000])\n", + " except json.JSONDecodeError:\n", + " print(item[\"text\"][:2000])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Use Case 2: Pipeline Reconciliation\n", + "\n", + "Compare Salesforce opportunities (sales pipeline) with SAP sales orders to identify which deals have been converted to orders.\n", + "\n", + "**Pattern:** Query Salesforce for open opportunities → Query SAP for matching sales orders" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "from strands import Agent\nfrom strands.tools.mcp import MCPClient\nfrom mcp import ClientSession\nfrom mcp.client.streamable_http import streamablehttp_client\n\nmcp_client = MCPClient(\n lambda: streamablehttp_client(\n url=GATEWAY_URL,\n headers={\n \"Authorization\": f\"Bearer {get_gw_token()}\",\n \"MCP-Protocol-Version\": \"2025-03-26\",\n },\n )\n)\n\nSYSTEM_PROMPT = (\n \"You are a helpful enterprise assistant with access to both Salesforce (CRM) and SAP (ERP) tools \"\n \"through a unified AgentCore Gateway. \"\n f\"For Salesforce tools (prefix 'salesforce-target___'), always include domainName='{SF_DOMAIN}'. \"\n \"Do not pass Content-Type to Salesforce create operations. \"\n \"For SAP tools (prefix 'sap-target___'), use get_metadata before reads on unfamiliar entity sets. \"\n \"Use odata_count before odata_read to understand data volume. \"\n \"When answering cross-system questions, query both systems and synthesize the results.\"\n)\n\nwith mcp_client:\n agent = Agent(\n model=\"us.anthropic.claude-sonnet-4-6-v1\",\n system_prompt=SYSTEM_PROMPT,\n tools=mcp_client.list_tools_sync(),\n )\n\n # Cross-ISV query\n result = agent(\n \"Give me a Customer 360 view: get the top 3 business partners from SAP \"\n \"and all Salesforce accounts, then tell me which customers exist in both systems.\"\n )\n print(result)" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# SAP: Get recent sales orders\n", + "sap_orders = mcp.call_tool(\n", + " \"sap-target___odata_read\",\n", + " {\n", + " \"service_name\": \"API_SALES_ORDER_SRV\",\n", + " \"entity_set\": \"A_SalesOrder\",\n", + " \"top\": 5,\n", + " \"select\": \"SalesOrder,SoldToParty,TotalNetAmount,TransactionCurrency,CreationDate\",\n", + " },\n", + ")\n", + "\n", + "print(\"=== SAP Sales Orders ===\")\n", + "content = sap_orders.get(\"result\", {}).get(\"content\", [])\n", + "for item in content:\n", + " if item.get(\"type\") == \"text\":\n", + " try:\n", + " data = json.loads(item[\"text\"])\n", + " print(json.dumps(data, indent=2)[:2000])\n", + " except json.JSONDecodeError:\n", + " print(item[\"text\"][:2000])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Use Case 3: Support Case with ERP Context\n", + "\n", + "When creating a support case in Salesforce, enrich it with relevant SAP data (e.g., material stock levels, order history).\n", + "\n", + "**Pattern:** Query SAP for relevant data → Create enriched Salesforce case" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# SAP: Check available services for material/inventory\n", + "sap_services = mcp.call_tool(\n", + " \"sap-target___find_sap_services\",\n", + " {\"search_term\": \"material stock\", \"top\": 3},\n", + ")\n", + "\n", + "print(\"=== SAP Material/Stock Services ===\")\n", + "content = sap_services.get(\"result\", {}).get(\"content\", [])\n", + "for item in content:\n", + " if item.get(\"type\") == \"text\":\n", + " print(item[\"text\"][:2000])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Salesforce: Create a case with ERP context\n", + "# Note: Content-Type workaround required for create operations\n", + "case_result = mcp.call_tool(\n", + " \"salesforce-target___createCase\",\n", + " {\n", + " \"domainName\": SF_DOMAIN,\n", + " \"Content-Type\": \"\",\n", + " \"Subject\": \"Stock Discrepancy - Cross-System Investigation\",\n", + " \"Description\": \"Customer reported inventory mismatch. SAP material stock checked via AgentCore Gateway.\",\n", + " \"Status\": \"New\",\n", + " \"Priority\": \"Medium\",\n", + " \"Origin\": \"Web\",\n", + " },\n", + ")\n", + "\n", + "print(\"=== Created Salesforce Case ===\")\n", + "content = case_result.get(\"result\", {}).get(\"content\", [])\n", + "for item in content:\n", + " if item.get(\"type\") == \"text\":\n", + " print(item[\"text\"][:1000])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Use Case 4: Natural Language Agent (Cross-ISV)\n", + "\n", + "The most powerful pattern: let a Strands Agent handle cross-system queries autonomously. The agent has access to all 52+ tools and decides which systems to query based on the natural language request." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from strands import Agent\n", + "from strands.tools.mcp import MCPClient\n", + "from mcp import ClientSession\n", + "from mcp.client.streamable_http import streamablehttp_client\n", + "\n", + "mcp_client = MCPClient(\n", + " lambda: streamablehttp_client(\n", + " url=GATEWAY_URL,\n", + " headers={\n", + " \"Authorization\": f\"Bearer {get_gw_token()}\",\n", + " \"MCP-Protocol-Version\": \"2025-11-25\",\n", + " },\n", + " )\n", + ")\n", + "\n", + "SYSTEM_PROMPT = (\n", + " \"You are a helpful enterprise assistant with access to both Salesforce (CRM) and SAP (ERP) tools \"\n", + " \"through a unified AgentCore Gateway. \"\n", + " f\"For Salesforce tools (prefix 'salesforce-target___'), always include domainName='{SF_DOMAIN}'. \"\n", + " \"Do not pass Content-Type to Salesforce create operations. \"\n", + " \"For SAP tools (prefix 'sap-target___'), use get_metadata before reads on unfamiliar entity sets. \"\n", + " \"Use odata_count before odata_read to understand data volume. \"\n", + " \"When answering cross-system questions, query both systems and synthesize the results.\"\n", + ")\n", + "\n", + "with mcp_client:\n", + " agent = Agent(\n", + " model=\"us.anthropic.claude-sonnet-4-6-v1\",\n", + " system_prompt=SYSTEM_PROMPT,\n", + " tools=mcp_client.list_tools_sync(),\n", + " )\n", + "\n", + " # Cross-ISV query\n", + " result = agent(\n", + " \"Give me a Customer 360 view: get the top 3 business partners from SAP \"\n", + " \"and all Salesforce accounts, then tell me which customers exist in both systems.\"\n", + " )\n", + " print(result)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Another cross-ISV query: pipeline reconciliation\n", + "with mcp_client:\n", + " agent = Agent(\n", + " model=\"us.anthropic.claude-sonnet-4-6-v1\",\n", + " system_prompt=SYSTEM_PROMPT,\n", + " tools=mcp_client.list_tools_sync(),\n", + " )\n", + "\n", + " result = agent(\n", + " \"Compare the Salesforce pipeline with SAP orders: \"\n", + " \"get open Salesforce opportunities and recent SAP sales orders, \"\n", + " \"then identify any patterns or gaps between the two systems.\"\n", + " )\n", + " print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "| Use Case | Salesforce Tools | SAP Tools | Value |\n", + "|---|---|---|---|\n", + "| Customer 360 | queryAccounts, getAccountById | odata_read (A_BusinessPartner) | Unified customer view across CRM + ERP |\n", + "| Pipeline Reconciliation | queryAccounts (Opportunities) | odata_read (A_SalesOrder) | Track deal-to-order conversion |\n", + "| Support + ERP Context | createCase | find_sap_services, odata_read | Enriched support tickets |\n", + "| Natural Language Agent | All 43 SF tools | All 9 SAP tools | AI-driven cross-system intelligence |\n", + "\n", + "**Key takeaway:** A single AgentCore Gateway endpoint consolidates access to multiple ISV platforms, enabling AI agents to answer cross-system questions without custom integration code." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Clean Up\n", + "\n", + "If you're done with all three notebooks, clean up the gateway and related resources. Refer to the cleanup cells in [01-salesforce-gateway-target.ipynb](01-salesforce-gateway-target.ipynb) and [02-sap-mcp-server-target.ipynb](02-sap-mcp-server-target.ipynb) for the full deletion sequence:\n", + "\n", + "1. Delete gateway targets (both Salesforce and SAP)\n", + "2. Delete credential providers\n", + "3. Delete the gateway\n", + "4. Delete Cognito resources (domain, then user pool)\n", + "5. Delete IAM role" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} \ No newline at end of file diff --git a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/README.md b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/README.md new file mode 100644 index 000000000..d0e9a100d --- /dev/null +++ b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/README.md @@ -0,0 +1,85 @@ + + + +# Amazon Bedrock AgentCore Gateway — Multi-ISV Orchestration (Salesforce + SAP) + +This tutorial series demonstrates how to connect multiple ISV SaaS platforms (Salesforce Lightning Platform and AWS for SAP MCP Server) to a single [Amazon Bedrock AgentCore Gateway](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway.html), enabling cross-system AI agent workflows through one unified endpoint. + +## Tutorial Details + +| Information | Details | +|:---|:---| +| Tutorial type | Interactive | +| AgentCore components | AgentCore Gateway, AgentCore Identity | +| Agentic Framework | [Strands Agents](https://github.com/strands-agents/sdk-python) | +| Gateway Target types | Integration Provider Template (Salesforce), MCP Server (SAP) | +| Inbound Auth IdP | Amazon Cognito | +| Outbound Auth | CustomOauth2 (Salesforce Connected App), CustomOauth2 (SAP Cognito) | +| LLM model | Anthropic Claude Sonnet 4 | +| Tutorial vertical | Enterprise CRM + ERP | +| Example complexity | Medium | +| SDK used | boto3, requests | + +## Tutorials + +| # | Notebook | Description | +|---|---|---| +| 1 | [01-salesforce-gateway-target.ipynb](01-salesforce-gateway-target.ipynb) | Add Salesforce Lightning Platform as a Gateway integration target with CustomOauth2 | +| 2 | [02-sap-mcp-server-target.ipynb](02-sap-mcp-server-target.ipynb) | Add AWS for SAP MCP Server as a Gateway MCP target | +| 3 | [03-cross-isv-queries.ipynb](03-cross-isv-queries.ipynb) | Cross-system queries combining Salesforce + SAP through one gateway | + +## Architecture + +``` + ┌─────────────────────────────────┐ + │ Amazon Bedrock AgentCore Gateway │ + │ (Single MCP endpoint) │ + └─────────┬──────────┬────────────┘ + │ │ + ┌───────────────┘ └───────────────┐ + ▼ ▼ + ┌───────────────────────────┐ ┌───────────────────────────┐ + │ Salesforce Lightning │ │ AWS for SAP MCP Server │ + │ Platform (Integration │ │ (MCP Server Target) │ + │ Provider Template) │ │ │ + │ │ │ ┌─────────────────────┐ │ + │ 43 tools: Account, Case, │ │ │ SAP S/4HANA (OData) │ │ + │ Contact, Lead, Opp, ... │ │ └─────────────────────┘ │ + └───────────────────────────┘ └───────────────────────────┘ +``` + +## Prerequisites + +- An AWS account with access to Amazon Bedrock AgentCore +- Python 3.11+ +- A Salesforce Developer Edition org with a Connected App configured for OAuth2 `client_credentials` flow +- Access to an AWS for SAP MCP Server deployment (see [documentation](https://docs.aws.amazon.com/mcp-sap/latest/awsforsapmcp/introduction.html)) +- AWS CLI configured with appropriate credentials + +## Getting Started + +1. Install dependencies: + ```bash + pip install -r requirements.txt + ``` + +2. Open the first notebook and follow the steps sequentially: + ```bash + jupyter notebook 01-salesforce-gateway-target.ipynb + ``` + +3. Each notebook will prompt you for credentials and guide you through the full setup, invocation, and cleanup process. + +## Important Notes + +- **Salesforce Developer Edition orgs** hibernate after ~24 hours of inactivity. Log into the Salesforce web UI to wake the org before running the notebooks. +- **CustomOauth2** is required for Salesforce Developer Edition orgs. The built-in `SalesforceOauth2` vendor uses `login.salesforce.com` which does not support `client_credentials` on Developer Edition domains. +- **SAP MCP Server** runs in read-only mode by default. Write operations must be explicitly enabled in the SAP MCP Server configuration. + +## Disclaimer + +This is sample code for demonstration purposes only. Not intended for production use without additional security testing. + +## License + +This project is licensed under the Apache License 2.0. See the [LICENSE](../../../../LICENSE) file for details. diff --git a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/flows/cross-isv-tool-call.md b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/flows/cross-isv-tool-call.md new file mode 100644 index 000000000..d3e724090 --- /dev/null +++ b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/flows/cross-isv-tool-call.md @@ -0,0 +1,33 @@ +```mermaid +sequenceDiagram + participant Agent as Strands Agent + participant Gateway as AgentCore Gateway + participant SFCred as SF Credential Provider
(CustomOauth2) + participant SAPCred as SAP Credential Provider
(CustomOauth2) + participant SF as Salesforce Lightning Platform + participant SAP as AWS for SAP MCP Server + + Note over Agent, SAP: Cross-ISV Query: "Customer 360 for Bigmart" + + Agent->>Gateway: tools/list
Authorization: Bearer + Gateway-->>Agent: 51 tools (43 SF + 8 SAP) + + Note over Agent, SAP: Agent decides to query both systems + + Agent->>Gateway: tools/call salesforce-target___queryAccounts
{"domainName": "...", "q": "SELECT ... FROM Account"} + Gateway->>SFCred: Get OAuth2 token (client_credentials) + SFCred-->>Gateway: Salesforce access token + Gateway->>SF: GET /services/data/v62.0/query?q=... + SF-->>Gateway: Account records (JSON) + Gateway-->>Agent: MCP result (Salesforce accounts) + + Agent->>Gateway: tools/call sap-target___odata_read
{"service_name": "API_BUSINESS_PARTNER", ...} + Gateway->>SAPCred: Get OAuth2 token (client_credentials) + SAPCred-->>Gateway: SAP MCP access token + Gateway->>SAP: tools/call odata_read (JSON-RPC via Streamable HTTP) + SAP-->>Gateway: Business partner records (OData) + Gateway-->>Agent: MCP result (SAP business partners) + + Note over Agent, SAP: Agent synthesizes cross-system response + Agent-->>Agent: "Bigmart exists in both systems:
SF Account + SAP BP USCU_L09" +``` diff --git a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/flows/multi-isv-architecture.md b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/flows/multi-isv-architecture.md new file mode 100644 index 000000000..0a5510a2e --- /dev/null +++ b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/flows/multi-isv-architecture.md @@ -0,0 +1,48 @@ +```mermaid +flowchart LR + subgraph Client["MCP Client / Agent"] + A[Strands Agent
or MCP Client] + end + + subgraph Auth["Inbound Auth"] + B[Amazon Cognito
User Pool
client_credentials] + end + + subgraph Gateway["Amazon Bedrock AgentCore Gateway"] + C[Gateway
MCP Protocol 2025-03-26
JWT Authorizer] + end + + subgraph Targets["Gateway Targets"] + subgraph SF["Salesforce Target"] + D[Integration Provider Template
OpenAPI Schema from S3
43 tools] + end + subgraph SAP["SAP Target"] + E[MCP Server Target
Streamable HTTP /mcp
9 tools] + end + end + + subgraph OutAuth["Outbound Auth"] + F[CustomOauth2
SF Connected App
client_credentials] + G[CustomOauth2
SAP Cognito Pool
client_credentials] + end + + subgraph ISV["ISV Platforms"] + H[Salesforce Lightning Platform
REST API v62.0
Account, Case, Contact,
Lead, Opportunity, ...] + I[AWS for SAP MCP Server
OData V2
Business Partner,
Sales Order, Product, ...] + end + + A -->|"1. Bearer token"| B + B -->|"2. Access token"| A + A -->|"3. tools/list, tools/call"| C + C --> D + C --> E + D -->|"4a. OAuth2"| F + E -->|"4b. OAuth2"| G + F -->|"5a. client_credentials"| H + G -->|"5b. client_credentials"| I + H -->|"6a. JSON response"| D + I -->|"6b. OData response"| E + D -->|"7. MCP result"| C + E -->|"7. MCP result"| C + C -->|"8. JSON-RPC response"| A +``` diff --git a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/gateway_mcp_client.py b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/gateway_mcp_client.py new file mode 100644 index 000000000..eaee48eee --- /dev/null +++ b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/gateway_mcp_client.py @@ -0,0 +1,134 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Lightweight raw-HTTP client for AgentCore Gateway's MCP endpoint. + +Provides helpers for Cognito M2M token acquisition, paginated tool listing, +and tool invocation via JSON-RPC 2.0. Used by all notebooks in this tutorial +so cells stay focused on the integration logic rather than transport plumbing. +""" + +from __future__ import annotations + +import json +import time +from typing import Any, Callable, Dict, List, Optional + +import requests + + +DEFAULT_PROTOCOL_VERSION = "2025-03-26" + + +def get_cognito_m2m_token(token_endpoint: str, client_id: str, client_secret: str, scope: str) -> str: + """Obtain an access token via OAuth2 client_credentials grant.""" + response = requests.post( + token_endpoint, + data={ + "grant_type": "client_credentials", + "client_id": client_id, + "client_secret": client_secret, + "scope": scope, + }, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + timeout=30, + ) + response.raise_for_status() + return response.json()["access_token"] + + +class GatewayMCPClient: + """Minimal client wrapping JSON-RPC POSTs to the gateway's MCP endpoint.""" + + def __init__( + self, + gateway_url: str, + get_token: Callable[[], str], + protocol_version: str = DEFAULT_PROTOCOL_VERSION, + session_id: Optional[str] = None, + ) -> None: + self.gateway_url = gateway_url + self._get_token = get_token + self._protocol_version = protocol_version + self._session_id = session_id + + def _headers(self) -> Dict[str, str]: + h = { + "Content-Type": "application/json", + "Accept": "application/json", + "MCP-Protocol-Version": self._protocol_version, + "Authorization": f"Bearer {self._get_token()}", + } + if self._session_id: + h["Mcp-Session-Id"] = self._session_id + return h + + def _rpc(self, method: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + payload: Dict[str, Any] = { + "jsonrpc": "2.0", + "id": f"{method.replace('/', '-')}-request", + "method": method, + } + if params is not None: + payload["params"] = params + resp = requests.post(self.gateway_url, headers=self._headers(), json=payload, timeout=120) + resp.raise_for_status() + return resp.json() + + def list_all_tools(self) -> List[Dict[str, Any]]: + """Return tools from all targets, following per-target pagination via nextCursor.""" + tools: List[Dict[str, Any]] = [] + cursor: Optional[str] = None + while True: + params = {"cursor": cursor} if cursor else None + resp = self._rpc("tools/list", params) + result = resp.get("result", {}) + tools.extend(result.get("tools", [])) + cursor = result.get("nextCursor") + if not cursor: + return tools + + def call_tool(self, name: str, arguments: Dict[str, Any]) -> Dict[str, Any]: + """Invoke a single tool and return the JSON-RPC result.""" + return self._rpc("tools/call", {"name": name, "arguments": arguments}) + + def search_tools(self, query: str) -> List[Dict[str, Any]]: + """Use the gateway's semantic search to narrow tools for a query.""" + resp = self._rpc( + "tools/call", + {"name": "x_amz_bedrock_agentcore_search", "arguments": {"query": query}}, + ) + result = resp.get("result", {}) + content = result.get("content", []) + for item in content: + if item.get("type") == "text": + try: + return json.loads(item["text"]) + except (json.JSONDecodeError, KeyError): + pass + return [] + + +def wait_for_target_ready( + client: Any, + gateway_id: str, + target_name: str, + region: str, + timeout: int = 300, +) -> str: + """Poll gateway targets until the named target reaches READY status.""" + import boto3 + + agentcore = boto3.client("bedrock-agentcore-control", region_name=region) + start = time.time() + while time.time() - start < timeout: + resp = agentcore.list_gateway_targets(gatewayIdentifier=gateway_id) + for item in resp.get("items", []): + if item.get("name") == target_name: + status = item.get("status") + print(f" Target '{target_name}' status: {status}") + if status == "READY": + return item.get("targetId", "") + if status in ("FAILED", "SYNCHRONIZE_UNSUCCESSFUL"): + raise RuntimeError(f"Target '{target_name}' failed with status: {status}") + time.sleep(10) + raise TimeoutError(f"Target '{target_name}' did not reach READY within {timeout}s") diff --git a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/requirements.txt b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/requirements.txt new file mode 100644 index 000000000..8845e5fb1 --- /dev/null +++ b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/requirements.txt @@ -0,0 +1,5 @@ +boto3>=1.34.0 +requests>=2.31.0 +strands-agents>=0.1.0 +strands-agents-tools-mcp>=0.1.0 +mcp>=1.10.0 diff --git a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/ruff.toml b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/ruff.toml new file mode 100644 index 000000000..570f04970 --- /dev/null +++ b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/ruff.toml @@ -0,0 +1,2 @@ +line-length = 120 +target-version = "py311" From e37e88011539437fe83614e9a28e74f4a1169623 Mon Sep 17 00:00:00 2001 From: Joachim Aumann Date: Mon, 11 May 2026 10:45:02 +0200 Subject: [PATCH 2/6] fix(gateway): fix requirements and replace mermaid with PNG diagrams Replace non-existent strands-agents-tools-mcp package with strands-agents[mcp] extra. Replace ASCII/mermaid architecture diagrams with generated PNG image. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../19-multi-isv-orchestration/README.md | 18 +--- .../19-multi-isv-orchestration/diagrams.py | 91 ++++++++++++++++++ .../flows/cross-isv-tool-call.md | 33 ------- .../flows/multi-isv-architecture.md | 48 --------- .../icons/agentcore-runtime.png | Bin 0 -> 49096 bytes .../images/multi-isv-architecture.png | Bin 0 -> 190146 bytes .../requirements.txt | 3 +- 7 files changed, 93 insertions(+), 100 deletions(-) create mode 100644 01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/diagrams.py delete mode 100644 01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/flows/cross-isv-tool-call.md delete mode 100644 01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/flows/multi-isv-architecture.md create mode 100644 01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/icons/agentcore-runtime.png create mode 100644 01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/images/multi-isv-architecture.png diff --git a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/README.md b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/README.md index d0e9a100d..b21185d58 100644 --- a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/README.md +++ b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/README.md @@ -30,23 +30,7 @@ This tutorial series demonstrates how to connect multiple ISV SaaS platforms (Sa ## Architecture -``` - ┌─────────────────────────────────┐ - │ Amazon Bedrock AgentCore Gateway │ - │ (Single MCP endpoint) │ - └─────────┬──────────┬────────────┘ - │ │ - ┌───────────────┘ └───────────────┐ - ▼ ▼ - ┌───────────────────────────┐ ┌───────────────────────────┐ - │ Salesforce Lightning │ │ AWS for SAP MCP Server │ - │ Platform (Integration │ │ (MCP Server Target) │ - │ Provider Template) │ │ │ - │ │ │ ┌─────────────────────┐ │ - │ 43 tools: Account, Case, │ │ │ SAP S/4HANA (OData) │ │ - │ Contact, Lead, Opp, ... │ │ └─────────────────────┘ │ - └───────────────────────────┘ └───────────────────────────┘ -``` +![Multi-ISV Orchestration Architecture](images/multi-isv-architecture.png) ## Prerequisites diff --git a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/diagrams.py b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/diagrams.py new file mode 100644 index 000000000..adb7fe488 --- /dev/null +++ b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/diagrams.py @@ -0,0 +1,91 @@ +""" +Regenerate the multi-ISV orchestration architecture diagram as PNG. + +Requirements: + brew install graphviz + pip install diagrams + +Usage: + python3 diagrams.py +""" + +import os +import sys + +_here = os.path.dirname(os.path.abspath(__file__)) +if _here in sys.path: + sys.path.remove(_here) + +from diagrams import Diagram, Cluster, Edge +from diagrams.aws.security import Cognito, SecretsManager +from diagrams.custom import Custom +from diagrams.onprem.client import User + +GRAPH = { + "bgcolor": "white", + "pad": "0.5", + "fontsize": "13", + "fontname": "Helvetica", + "splines": "curved", +} +NODE = {"fontsize": "11", "fontname": "Helvetica"} +EDGE = {"fontsize": "9", "fontname": "Helvetica"} + +_ICONS = os.path.join(_here, "icons") +ICON_RUNTIME = os.path.join(_ICONS, "agentcore-runtime.png") + +_C_CLIENT = dict(bgcolor="#E0F2FE", style="rounded", pencolor="#0EA5E9", penwidth="2") +_C_GATEWAY = dict(bgcolor="#FAF5FF", style="rounded", pencolor="#A855F7", penwidth="3") +_C_TARGETS = dict(bgcolor="#FAF5FF", style="rounded", pencolor="#A855F7", penwidth="2", margin="28") +_C_IDENTITY = dict(bgcolor="#F0FDF4", style="rounded", pencolor="#16A34A", penwidth="2.5") +_C_OUTBOUND = dict(bgcolor="#FEF3C7", style="rounded", pencolor="#D97706", penwidth="2") +_C_ISV = dict(bgcolor="#FFF1F2", style="rounded", pencolor="#E11D48", penwidth="2") + + +def multi_isv_architecture(): + with Diagram( + "", + filename=os.path.join(_here, "images", "multi-isv-architecture"), + outformat="png", + show=False, + direction="LR", + graph_attr={**GRAPH, "ranksep": "2.2", "nodesep": "1.2", "size": "26,18"}, + node_attr=NODE, + edge_attr=EDGE, + ): + with Cluster("MCP Client / Agent", graph_attr=_C_CLIENT): + agent = User("Strands Agent\nor MCP Client") + + with Cluster("Inbound Auth", graph_attr=_C_IDENTITY): + cognito = Cognito("Amazon Cognito\nclient_credentials") + + with Cluster("Amazon Bedrock AgentCore Gateway", graph_attr=_C_GATEWAY): + gw = Custom("MCP Gateway\nJSON-RPC 2.0\nJWT Authorizer", ICON_RUNTIME) + + with Cluster("Gateway Targets", graph_attr=_C_TARGETS): + sf_target = Custom("Salesforce Target\nIntegration Provider\n43 tools", ICON_RUNTIME) + sap_target = Custom("SAP Target\nMCP Server\n9 tools", ICON_RUNTIME) + + with Cluster("Outbound Auth", graph_attr=_C_OUTBOUND): + sf_cred = SecretsManager("CustomOauth2\nSF Connected App") + sap_cred = SecretsManager("CustomOauth2\nSAP Cognito Pool") + + with Cluster("ISV Platforms", graph_attr=_C_ISV): + sf_platform = Custom("Salesforce Lightning\nREST API v62.0", ICON_RUNTIME) + sap_platform = Custom("AWS for SAP\nMCP Server · OData V2", ICON_RUNTIME) + + agent >> Edge(style="dashed", color="#16A34A") >> cognito + agent >> Edge(label="tools/list · tools/call", dir="both") >> gw + gw >> Edge(dir="both") >> sf_target + gw >> Edge(dir="both") >> sap_target + sf_target >> Edge(style="dashed", color="#D97706") >> sf_cred + sap_target >> Edge(style="dashed", color="#D97706") >> sap_cred + sf_cred >> Edge(dir="both") >> sf_platform + sap_cred >> Edge(dir="both") >> sap_platform + + +if __name__ == "__main__": + os.makedirs(os.path.join(_here, "images"), exist_ok=True) + print("Generating multi-isv-architecture.png ...") + multi_isv_architecture() + print("Done. Diagram saved to images/") diff --git a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/flows/cross-isv-tool-call.md b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/flows/cross-isv-tool-call.md deleted file mode 100644 index d3e724090..000000000 --- a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/flows/cross-isv-tool-call.md +++ /dev/null @@ -1,33 +0,0 @@ -```mermaid -sequenceDiagram - participant Agent as Strands Agent - participant Gateway as AgentCore Gateway - participant SFCred as SF Credential Provider
(CustomOauth2) - participant SAPCred as SAP Credential Provider
(CustomOauth2) - participant SF as Salesforce Lightning Platform - participant SAP as AWS for SAP MCP Server - - Note over Agent, SAP: Cross-ISV Query: "Customer 360 for Bigmart" - - Agent->>Gateway: tools/list
Authorization: Bearer - Gateway-->>Agent: 51 tools (43 SF + 8 SAP) - - Note over Agent, SAP: Agent decides to query both systems - - Agent->>Gateway: tools/call salesforce-target___queryAccounts
{"domainName": "...", "q": "SELECT ... FROM Account"} - Gateway->>SFCred: Get OAuth2 token (client_credentials) - SFCred-->>Gateway: Salesforce access token - Gateway->>SF: GET /services/data/v62.0/query?q=... - SF-->>Gateway: Account records (JSON) - Gateway-->>Agent: MCP result (Salesforce accounts) - - Agent->>Gateway: tools/call sap-target___odata_read
{"service_name": "API_BUSINESS_PARTNER", ...} - Gateway->>SAPCred: Get OAuth2 token (client_credentials) - SAPCred-->>Gateway: SAP MCP access token - Gateway->>SAP: tools/call odata_read (JSON-RPC via Streamable HTTP) - SAP-->>Gateway: Business partner records (OData) - Gateway-->>Agent: MCP result (SAP business partners) - - Note over Agent, SAP: Agent synthesizes cross-system response - Agent-->>Agent: "Bigmart exists in both systems:
SF Account + SAP BP USCU_L09" -``` diff --git a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/flows/multi-isv-architecture.md b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/flows/multi-isv-architecture.md deleted file mode 100644 index 0a5510a2e..000000000 --- a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/flows/multi-isv-architecture.md +++ /dev/null @@ -1,48 +0,0 @@ -```mermaid -flowchart LR - subgraph Client["MCP Client / Agent"] - A[Strands Agent
or MCP Client] - end - - subgraph Auth["Inbound Auth"] - B[Amazon Cognito
User Pool
client_credentials] - end - - subgraph Gateway["Amazon Bedrock AgentCore Gateway"] - C[Gateway
MCP Protocol 2025-03-26
JWT Authorizer] - end - - subgraph Targets["Gateway Targets"] - subgraph SF["Salesforce Target"] - D[Integration Provider Template
OpenAPI Schema from S3
43 tools] - end - subgraph SAP["SAP Target"] - E[MCP Server Target
Streamable HTTP /mcp
9 tools] - end - end - - subgraph OutAuth["Outbound Auth"] - F[CustomOauth2
SF Connected App
client_credentials] - G[CustomOauth2
SAP Cognito Pool
client_credentials] - end - - subgraph ISV["ISV Platforms"] - H[Salesforce Lightning Platform
REST API v62.0
Account, Case, Contact,
Lead, Opportunity, ...] - I[AWS for SAP MCP Server
OData V2
Business Partner,
Sales Order, Product, ...] - end - - A -->|"1. Bearer token"| B - B -->|"2. Access token"| A - A -->|"3. tools/list, tools/call"| C - C --> D - C --> E - D -->|"4a. OAuth2"| F - E -->|"4b. OAuth2"| G - F -->|"5a. client_credentials"| H - G -->|"5b. client_credentials"| I - H -->|"6a. JSON response"| D - I -->|"6b. OData response"| E - D -->|"7. MCP result"| C - E -->|"7. MCP result"| C - C -->|"8. JSON-RPC response"| A -``` diff --git a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/icons/agentcore-runtime.png b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/icons/agentcore-runtime.png new file mode 100644 index 0000000000000000000000000000000000000000..6a06c3ca085e894656c8fe0d0b57d3d7f1e16c58 GIT binary patch literal 49096 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%0DFU^3qSC=DIoM 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 a82)TaeOb!@p#Gu1o@wbnDSmAA<^KS~cRDZt literal 0 HcmV?d00001 diff --git a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/images/multi-isv-architecture.png b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/images/multi-isv-architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..ef6f7fe76dba35f6332b655332447b64f870099f GIT binary patch literal 190146 zcmeEt_gj-$@Gr=!>snY>5kZO~g7hXJ($+;px>BVEML?tk2*nUWP*+%~(mSk5FNqW> zp#%#(5a}gAq$Gg=i4c+i3CT@#?|tq+aDTermnTnNQqG(+XXZ1XIWu{6*UIGNZ=%2P z@$sEBy>rWkkMC$UAD_VHuSdAARMA$uxKBqOnw#9>KdVL| z$CQ3I`@`&xN_r)Rl42T27@q65%zc#B&pe3ipCjrKb%@Nl{2X*t$gr(^{M%o-@_E+@ z|M2;{jz91Dy2`9uPW-(8)AEzof3H6ILs{VG?cMYsk)L-ld4CW7_x8B7D{oEQ>tC5g z^`Ccqn#Z#Ff8JJ@{?CyAU+7ZvL>PC=Pr&2O8Kj!~%xB~mE*Qs9B(Lj7gjO& z@bZ%yzrL2Tks0U)UB_cdLdUnwC}bl;VC+5-!Qoj;O`FZVv)y-v`T4r`+jd{AQwx1j z7$p!{l4LXeOTdJ>fzL*;*}?~kpL$d9_}y%tRdx}4nz%;kp`i^OK}V6YBv;~Rnootv z`5%$FmeL zqv`LO1}b_|jqI)%z3FJXOwCe33mlig7#1NFtX1--5STYE)_hc0ooz!EY=rmAJnn7llQ`SgZba)`Br{@X<;q+GtQtepJ1WTtT~}gsI>)TQRz_o z+ggn$9)$T1!1JIk!>G;j+`85N5Nt*4>AkgM#ppWQBHh0;dEWP4u#*bwZ>=`-8&%LZu&vYTpuXV$eoUVnm-4cv8S?@Z$c13X1d2$=FHP1|0I-hYF< z%6HQq7vhdaFol4VRQIcC?UV!P@9XOTagN#8a}kPzPL5D5>g>6b z4}PGBKk3I(UJd%p7jSaI@9jNhj-*qU&lu*)U6wuhz}`REOb-9zXy+wh^sxCrK#CGr zUb2Oai+wN8Q-oF+7lUGk{xpg3F@UZM4)(qib=f{xX6vLo_wc?U( zf{oxg(DmOPe&K$xtIUE6I`2hx*-%b;obZsmBnM@*kk-*dx#q#aap;0N^ncRT{RkVH zqOiqJ#=QSRKGU?}5w#aaVF#<_;ALN*+Iu*aDEf0T$QRSSHP}JaT|zXwPGx|i(q0kX zJ9NjbGUIp=18(tH!Zwh(Q)_5p5<&dk;qVXPW<24q{5Jgun<)owdrvv0kd=wHqU(PP z1?4L?oFRkxSi;ycrECZJIKx1C7XzDZkN;W*2{{MjC@pK zK3h`Q#O$B(WF_O5i}gddes}ocFJ(Vx#~eH}l?k9|1W$?G0F#_kw%#zH*R1jj z45{EJ(pM@B$T~PRr;UKlSKBx`W~1C3S|eK>l7yoct6SHH+o)Xvf-AfMvy}WS(?Y|- z@V%V9hISen1lsq*^fjaQB-hAf_jh?(UbIqVK^;*JzwAPoD=v2-o_t`;)1QC*4?}!U ze%psDMpj2f4J4b?j9rgd2>0pS$^;TrIE<T3!11g+dNDd~J;U}`-Low<@t+&z;r zw9=nW#R516yx}zcX2sfrdM^(sEG3+6rB~|4wMg+4prv`z$c_6fkYD#SJS)5@D@70& zSsW%&-Pw7`kkubCHaDIj4G!&jmm7|O3PDd@58}9fr>T@RN0XJ!7S3U>dSy+Dr5Uu)WD05NG;4Y-j-`jM_17 zrIXf2ku{p-#asI!%;`GM?PUP_Dvd_tiAL+8L4t0Uy;zE`XzQ?NlDKZ!QcGRkB{#QB%<+XMqb2dnM2mR^ zQ+3z^OHXIL|C+X@7lyduG~yE{9(kx=hpTEW+osAJ5jgOZ3F^v5`RLqeTJUnO{Z4M9 zy%!WZ6!p-7+{Wl^Ogx|J*s;?X4{~&kwqH14N1SRwBAt_ze2%>K5Hm z;kov^!y&Hzy2|eG@a)*SB2qs2f*DOgu}`S<@zG%kRu_TWg<sE7%lPqNLmwqP$Ob^&SExlq9w(mxNde| zg{!t}T3UoTSfoq$ZyP3(;A=3}=i(4%K*Y*xBSH6NQ6{P53HW*yg_avC^jY@WQSP)n zf~L=1;ESnemdyi#qcCm^@A1`pGvRG=Grrk{HfN2=EeG&;gE4Z4%5qQ2;7Wju?AlCc zl%CUwhbgGZ6#S(o`_tg>pDW_OJN;)apJ-Etsn>0)Y++yDi(^m zK%-x~c5P+b93j>y%8RQW=9jsY_umac6vtB;wG*%CR?mgbAdqs~%62UENBoFL3h{Wc zzM)&8U-NHtu!&@@c*N`_LuS8IWt2n$OJ}jV!dHKs|0g@sOykAr2b3RE9c z0|yMk#sotacLvj?djptrEb@xViQu1s<1cUR6JgxA?(a+@k?M)=kQzD*u2A%<9ciFT zsUM4+8vXk9btS~mr=z#h+wzJ3;ia~fjKkZL9h8(GHXWzj`+Wnm8w9?aqJ*VDl@GSDgDPG2M zu4;XO*}x*A%+(^IZvZ>qiTP(au$!&FI+19&=yJ&6_R@G^=q&SPd8zzNP$lKH5Xf!_ zU|woK+sSW@gy1XV!B3hPg+c$)NrxRT)N`PAC&t#%r?6!?*VMXAdrPnH?UvNl&D!%M zs#fqQm`e_KqGg-CC^Kf5cIh}WYT}iYsV_cTN>2X8NC-3DN~k>HZ9xWnwQk9HuY&Rb zJpOj|f+`}Xa`j9n{PC*xmw1pd#iQwMr@P}$043~9H3?quw2L19*_|=d4DA-n3=@n> zG5!$KdMDndPTr$v?~z$Uk;NJuV(a?!Uc3hG^-{_5zB|{S`S@N8XmG=B)9>B&lZwm@ z$A;Y@iWw8{C|g@=e}l%h$w*PKMv&}+kyrP3T=w2tLyFZPhX|yXJANwi@~aW>`l4tJ zM<7DaUd?)l-g)NKxckm#9jRhg-vNaxsj%^1>o2!uWCBas8xb-oLpACbc{2Y~`@ieY z)|_^$8o0R&$NYvaM3fnuscJ|6%ba9J=?>QL*Bl;#PhaF|@3ZUF zj))VhD5adarX#l>n|z4yN6SNmV{DX?55?!fwk!jA~m@hf*}v2}2f6rU|a==FIF zTCVw6P8_Vub-y2B6R~JK0p}%3F<~;?NOry~9!_p#uL`dfX*6?m_^Dl+pD%fk$yW1{ zoHTS~{cI+0>hG<7%wW7;1*Q-HWBYL~f)Ddkzn{uU08?!_uP`#yb9QZi@IE=2$4j!$ zlOp}SXO;I{_U?DN>U_!iLzTGZV-vBLIRWReG!ewMwp5q6eNhtZiv{-^n=pN#uoj8(hwR?FvS%#ClV z=umyC{|ySPLiX`lRfcZ!l_TiV3&7gR3%rj^#`ETs4-csI=DcliKW#oGfp8WZ_ zgCAR!?=Dvp(+Z@X6I1<>rbY4&-A{{~G?#(wJd6cjY4W4^RCkQRJsY!}CI_$A3zL{+QJj{)QNE*UcZMQuu$#kG%9# z%zX$?A&YmqTmyxE&ga|X^Ca%Jop&`(vndufHq%!b+|-wabZltcPPMs5!nvQ7%XGLl zIZ^71nICJXbXHzadt2{WNg9+tS;~Lovt)6?@GpxcXN9f&yS;r`cOi6q^rX}PKZ~6Cqer(iZqoe2~ ztW|%BH4Ef?N>@7U-R*#u3nEA2@q=S~T`!#Whk)gv{hdqyd(AUkrs$C)U)6p-QK>$^ z%s>`0uj%r$qmSlL92e1)>w5iP{Zmi!WUZU?R6TcMH)^R!c>Rp( zly%PQS`IC}vbdf0ZKCbv)g-Z8HOYfJq%sRrDG%Nes0Y>4#h&`o<4B-&j!)rJpFO|9yn?c-~- zdY>Q>c~u$v^Wj5%Q^i?GIjpoPscAv}GUlmmRus+8)S*F${^#cU2V*VXOlu^6`iN4F zSYk};wYOI;TN?yVpYMLC&76uz*g(EIGulWbd+1s@XD-OlbSWsXnRR#t8_n9`_-TB-^$pD%e;iHl~&f7I$ZH>1__ zL%BaKH@_1zCtkvJ&+!~&>gB=S0LDUbXaoXj*X64ArRoN9EEv04J*;`HK(7JW;pZ@;IP&)mc=x3|1bGJdEb4(mzrU#Knnexl!hWHigbd z9mOAAl~$Dixay9p`-Xn4&XClzrSVSH8GuLlh8GIf3|2G#z0p`BWg&CQ5po2i36;ob z*Mc4uMJ=wWYOR2ltt6g}Z1XG1!} zrdu>D3J?fT#&8KDN-fN@=aKC_MW?hq3Mc4B*?e|hYhZUkKw`{~Gx*7rfI2Sb)V`g~ z4qK3dd~>P-wza=$?t^p9Q$|Qp*3@@r^%`0K1Lxd~KxIp9Qr&;HqWjZUG~>l$`KdDb zxs$cK3iPsbs=#{SdETt@k7FNlkw^O=JXNAZslFO*3%-8M7zda-M6MM6H*n55UK0gTM ztNpNRuDmGO#Jl7G4v9RqpY_!*dAk~ELuxK6*xxKG3HB>i3{AI9n3Rrn=aQ6IrfF}W~*T- zVP|B$$Nj8^`m)=vp2PwjMFzsdN~v$woaou+qx;-P$J5~Wx4^FUx6cRkB53RoQ!)WN|QTve)8h5RL;?pWo(LEi+LM|+X zWwx1`ojmC~2YJ%7sjvCYoi?Dz>S-Pgrz*)I6Zi!P(Tucs>wg{nILSgcw~zc6Cl1C> z@(#}Ze>LtNWUKk3RMrYJ&IzC@3~VPh8gX9~$|?;UQ1pl0=j~<1?i+t^um3wXHc^MT zVX-^jd-OyW^T~9;l*M%pNM!4Idb-Vri>;IkEe>7|lT(cN&dtnnhxUs7N;}UzT zOF(N{zvct`c(JWnYV2k+W>>HiA$32nI2>>;YHQFJ_Be8M!v0j3c~X+^joojFoYlC| z_sgeD(?%=qiEv(}5OT*b7IpqknK7+4)YEb0d;fg)%W|#@qxbxpbNMGqAMg$iJaU{r zh;MrUQ#+1T|Dl?qPu$3Gb{+@_(8lHIP@X!@QrZmHNX}tSsb@FEecQHr^q9kpGU`b0 z;M6!!_sTnWYSnhdI%*;`^(<8DVBuGnM?a$WKJUQ&gy#zpRiK2L7k+5MIxi>7Geuc4 zVyG~C53p|*?^UaSbQBzED~gmq-pQIc8?cF|UIy$hm9+1JRSyyVBYQPJrZ`q3?Hrz8Fg)}{rd|zPiW@g`(aU`c4z>NQVI!-+AnB?A{-QpR=AKI4)dGrNc z+Hy}8v(Y`w@`ePYy`I3|#1BL*4>#hXP4ZGUW|olA16sb*zP+Ia>hJVC@9iMK{TNs# z9@f$Lws!4bqW4DJ0)Wea&&rvVo<#kJ8?rxeV=P@Ia5tvaZ-LdQ|J}pkOLz6oZ#hQ? z@{V5i2%L^)RzV?0(I<5`bH=B#F-j=%eo33`%0=j+1b|7h&ykkRfO9=cb9~Zj>W{#}N6(AM(QKgNplmzb{4yKxe0(By2nz z?Pt5_A~sw~6YU-C{3JbI2B7HA%-3k8cPi_)aW>&Y`GAqidNTWhO@x6OXksrmHlru0 zRz2dw!}w04$tJa?K3+75pUx!4mqOu+`-?dYkT3*k7Wp*V5pKvJHzVPT5?BxKj1y4lh8&WF5&_}}(v z!aq3L`^wWi>(jRX^yTAkIn`E<`0lU2pbxM&ilc4}0y|5i=2X4%Em*5@woiQ5!i+gF zt$`akJQNu`NGqYKWS2aq+^Onq7YgtzAf7s}YVtG8%b4(JkXQge7rY^g`$KusPP+#j zi;+nO1Bu|Jtr=26vE^qgT{=#x4Q^n`q-582QirZtLap}l$Y5&yN`x@2Et}Ll+E4(d zfdeTF$mO4r_I;6PERw6pBaW#h89RSxmD=E2rQ|XP^lfHp9e+ew_cck|b3&l;B*|rwep>|BDHkzUlID4p?OG7B4*rZ+kG& zG<_gh((b@TJU!zG$M~~y_D*0IO%LVx4Cg$JHExAw)fR9sxQYv1ihIR|Uj35noZ(z< zt>7d!Ait+D!tC=(Z8{Qpmh0~YnhJ#}hNJk!E#k&O$jixLDI%`pS{xhf-l-Fi z>yaBhi%f9Ut5!mEJh^GLzs{Pw(d=1aekg{Yw(-<$2UtDYSyjm;jfQ~d5u8hlt0 zwOED*GuuYPiSu|+DWNQp^PM@?G9@7}Y1di&WZA)Y=C#Wsm*x{(gy`C@57FuGZkXNv zm@n)@ueX1;T|z*%2Vn_ftzn*J$$oub%8=KlT&Q(d)|As;iqC_id>sSxr&1M){W0QH z$RWH7F?{84_AthUA(o1fymmt=I)Gcy5M>FWs|zbB+3`9KA8I4-m4x6o2fR83YInth z4pV>gbO$}@9#TA5_sV6?sFdCBY}YfnC%+HRC)-AFN;?&>x;}=vt&;5fP5T7L(MF1a zX&Q2CS!4es;=-V9CP!P+d-j%&DqkVpTVOma`xe8JX{KlvrO2g#z7qFGtvrLv3;yD z&~0zK7m{7~=DBs4?E_-cppf+kJPVSwvNlPc+x0iG>I{vJhJD)I&}@pXF#jbc8ocCj z82zEFs`&aFqH*OEHGNQJU++b0#@WryHTU{Pz1s)7!KTn+Lv!rxSckuFv5w;}Xo0J< zSPjZ^M42C(CGf8^6tKb2GVujiMIQ1{~b*R5NIjAv$-Ko&m@w9Q=oFZ(Q3 zQg>xuQ$CPNBuu7W8QM7`tkl*jB!kYnWc^c(yX0HFAsVU#<2naMW z2pL?GHuZFq0~m(Zc;tpn+5iR)A5H0ZL97q!AHNYY)*!yO8(_Yj^ttCmm@l7y8Mkpm zd@5RJ(`{47V^m`1!OlvUnD6pY^!k186wUL|mIII5BAjP^^kxn3xa$o6egq9I{y4{p z*7E;BEBMy^63pxqq&ZRRX`!JEF&HUkL?_f{`Ss)%0;04t11N5wRSwWKv~CV3%8t2E zv*t}!x?Z`A16BBrjO3J^2A73_1B;m+^CaXEJzLiCW$bXvhnwo#Tzb5GDI-<4NX?Lqt^AKUuCD3|gq4RAZKZp_| zG9B_kMY%r6J2lzM)Ul0YS;1A1)w4yfdiwPE}MpY z(i+kU>wQ*S$0}+rPBoD%DspV!r*Xl}@GFIt7$-RZ!9{zgtzZrJy~1Y#7}BN?7ne@X zCIVkIsA9^Dm-LtEe_$d}7}3r!n(-b8$2S3^)*4!uMp1T&E&xEW9Cq_{cF}%b`^NZK z75mT7(hs@oNr@+DS}vvt$9#l8U#fXL2TN%~SE6KQ$71LDaK1iF6+Bpuy(Z%hT<0Yot zAYQ}ZDJJva?)JVPf;81!8vr)gCN~pb^-2!qFGN|L(%;Sr4G&-0M*#}W*(}4o9g~H8 zAp09pMD5|Ny}Z+xZ?A|`4({$f{m9yD-HTn{egTB(CnZ^eM+U8Gou`oK39Oo|o}unW z2o{&Cdx%KS=k$3+f%E4@wh%y<@HxuZ*|eSJTgr>VK$qvEgLvonu5!a zG7cAQu(X`21Q%R3amjm0%_FlgjZO1HB7OR&o>5RC6h-K7QPHQ<@O5AE_Dwpgy)RrW3 zP5e8-aI_E$o6Xf>giVo8C#gd;U)|-(c{tXF zA$9fhSi>M0;{Qc{YUn6KBon4LRwUIAvjhq$3%qweJ}c~V3ku3x&zGNV4iqUyW{kRd zsw~5Ws`Jd{@LS#1L~`pGXmt`&-s#sJ_M-hcxGSkH?y3je*}O8tX7Hcr)~A?@P@`oj zV;2{kMKiaKB3~Lz6xZ0#RKfHSi!)-yk1nLOKPVR;i~0&mcWl3AIvUIoa0;Kn)o-Mk zkI~QtMSr$oI_z{5g*9^cHB{R_C@F_bA#P|a$HOymZ)HqxC#ra@LrLo4S`bWc)#qm68E>y{#~p{|LItl^ zNNu>SwWPb}m`FsbUcQ}JqXUE*88yvE_oAP!e5@iK^@OWL?|bSj?v<}pEL$ly&xb6B z?JZMa>ABW=S@Vo(1r_Loug>c0&x1t6gAoSNzPzxA+rCg#%?|g?QgO-R_SDD&56T&3 z+@3cannl-bCQji#cW!qJc;uou;!yn)*?>I4X(gTgmUQ(8OC@?ixDFkrIAvj#(KssMDH`&tDV>NZ>;+n34u;(%X5km8OoBhR@Ev$rH@X!}R zT_vL0IwFGjq&HA;qFmRw2(|nGj2+J=wz_O)N`q_KD*hO!i=7zwjUca;83W!HHb{S4 zTRV`O>0r;@VU1ksE0`%aEfs)!a1UX_dW!aXxQyN0Ry)vji17x|Q;F$s;XYQ!+|x^i z53I9T?6+KUqj!y;>g*`!uY69=VP*FCY{SNkJ!-6c1CXunKKHsGf|bu%tnwM@Vu{S> zvZvsB!Jdvb(hY&+fdb!o9ny=W8E;rMx3uH&K0s}8TgaO3Z_>idiQP~~(yQ>&Yb5f( zz23qcKkil-dw}OP9lh84ZR@c`7`f)5VqNWob4uus-1C=!iVU;T0_Xx{^6`Rk!Z`w5 zH?ZH`mQfiQtx7=B%GTAxa0dguj;`UsoqMGiZ_2@`KEH*a+hy}mM>fq1`Veo?{5`p0by&IP~kdc=n+R;(&5j`_AJ{*A%P9tP=W`qk0Fn7|? zUD+*d_lST~hNPTl6xc`BTsZZl4r5-jZ#sCn0S6Ks)^v2uY8c)t4=I*}adh{VVH_D* z1CFtXM!0J<{T-#Owe$+NIZiYULfTvj$PZbcOAZZ{ozj~x%BDP~sdo`JCVeOvqvJl_ z(ag7Km(Hu0CM7h_45pf{aywP=tk1)Pyq?)_G(nZT%o+1yo+Zpge+ta~s;6}(ulJD+ zwS^lH^95u#XDE5chCKIyL+yW6W@Ol8k0f2D#_C%Qi$FTA+TOMzja4t1c3n{QfF~+w zM|aHu#w6Mt0}y4Nb2%lQ(Z-tx&3%D7ba3wOqF_mIc44*P-Lq?XsEckGgZxloxrwRL z=z)y!B$0W9-$6!zI=y(y0A^7u&-NM>SH4e*sF;=|20iZw+1@}?)H9gTA0mqM6naA zKp&cv2j?RevOu_lXZ-7}gsMT?v&P8=aGwRwWPLTx77)-cFh=eN(PvaZMPPZJje*Bp z7K+9eb_vq`ez>O&!@AVQvkK9Vk!qAm#0y!iD^7XRt&r&QAQ6v$l-3>(jnz`s5drrI z%}N99P2EB_w=P7$9-P8PQuq;^kptL3w*~^kHHF-1y(Y2WC@86`3*KM8Obl~Wj-mo; z{ffBOAyBI^58XHgrh?qlY$NpHxbRsoXtA|xpv%OALT+iEy7Nj++q=tE(IxBcJB`ww zk9!IMIv()q!D;PIgEI>pa)zh^hkUf< zA0SiCEiq|q@5z>}#T+;$-IFi%rBu$4aGhC)g`<=Pm6%&3&uTx@o9uV^$x>J5xF`bndjW zVO>o%D{^YYuldSoAjCk{wrSpg%eCyov7QJtm{6&=^9`y)bfaKKOU(xk@|+_NeJShV zkM7frX==jn;b8~=M$K`1p*F*(<$T&{;@|}}S-TEzH==0Y3x?$&3k(9GgJr#D>PzwT z7pHWkcr=cH$#-!-9ksJ1n`tyH&F$QC15xH9YPMXP=R=KTtQTZ>6f6b`Z`nx`d;0x> zi1vfO(pLIBt@{|A2OnHP-Cp2f115f@ z7r`;7B>2`5@7fyLA3k-*zD4`Gy*`j3XX*ExVx5??p5K1KA}yK^_Flf=to{aUA)}Qt zXpYmw~UqX z2<+HvLUR@bHTJKRmyExs!)iC9NNXi+ZS{3;E-mANt$I+KLwNfkQ=7&n@3n5&&|7Xv zSX@J5-B*#u9tDB8)B-7)o60SXJ?ejnTAw+`4Dcz(=MYQB=co}a$lzu5#j)Z$+yiuT z4&@=g#(S%C2JS0JApF-X^Zo^de;@bpJ-`od zWnzyLxK*w5CtPl|w6}OpSpU^~4St|~#zZ{gA-s(c&*KKIY>K1Lr!S_tND8}^3;cu7 zCR}u{biW2hPxIULW?Yn!vx|#D5lGK_qlL9Cm|bJ9zkXxtPY3wl1{@7ZL}YaTQaAo$ zI66m7h$t#5IyN!kwpm}r4ezsJ(c{4aMaBB-uk=DW9lpLIsoKT#`gqWkdl-0uKFshM zwzV#;tCUM`%5x2l{!~&MyHd0->I5(|avPl~RD9BWAl*e$q>#Y=#)E$<1f{c>jhtab z86eLmTcz(g{fZEgWL6rr_C5lxOA=R<)yKG!m600tZ*)uo zKz}M1hf$Fyul4a$W5!P0>v9bZeY6k(%K_t<4r*T~hhynz>}cASn(@F)k`FF zo`m*T=DgMo?9f3(&*$$ajc{T4xB;V5ENVK{@9Zvo*aM^aaHUVW-=}kjw$X0RA)cj2 zCkIys&T~qE5fKrAty{<_762pp9%G;FmKNL7Qve$JFS!h3(oYsH@^@zPsde|ma zP__!Xei*+ue$$6I&+H8ET4#u${M_5tUvbe@%Oxe}{X;)eky4dxQ=<6ynJ)^PTHb9R zJ4;Un_TIy;P~$o;ms8l!SpjqxB zpT%v3{X3$#`N@U#w|CYJ;g~swVrmJI3XEvP^Mu{++htbu)jxsRJW@e8=*OEKT2hC=5mw0 z`fCkIeaDJL3&g+i`hf|$Up3zo$D#ETlj+oKLv{qYjR28r z64|>bN>Q8olbzM1NNYZq=Z81V)jhX1^xC3s7P)n*Vj71B3-HGs|Ne4(d%G~X-wD$B zv};|`b@x_a0SG8l`|C@FSes|l6On9FrDgR>us>Y_HTZ;R7~J*(v=n|6(S!+VU`}uH z9#hbmw|DM2TkbmZBA?|U!rL}oZYd3XoYW&!9R#s#n>m7}al09+x&1n%+ez}xDkyvt zdT+OUwBvCw0+|rTt(D1*sx6P8!5ou%T61Ss&wRLL+PHN=&Xyz?M(E^R)~5s%Ln=dB zNP&~;Tp|>AI3=8zilXU7`;4#HoYF5Jq5q;919cyJ?ITGVZV(J5uC=vktP|k=upFX_ z0QU^}cV)ygo63xLT=D>87vsFfD@JBGt#Njn5wmNY;tpmquznwVoozbWA*@tdx^W3Y z>A3kzH~%52i_amz?pH1>ox84ANtfeW{xrUIE9>8l4(-O!zkJP2^0rv_EKKul1|DB^ zrA!?+wHk&NQ=mqE{h6TQt67Sy7d{dlDXE^aUrWFWd=BvYJ=y?^cVTVG$*Mc9g_ydk zsVYmVLqINrYTXz(^{Q_mbC3J$iKx99lhD<<+G)kjv17mf`iKG-7c;$XVTYHu>V>;@ zW;g`gc&OgWj+>*`=3h?>%1QO?6^0BMUVL3Pvjy=9b}q58}ftrKE)JHC@SGDex>=DNE0S6kJ8A=EY4Z)p4dE1nNef@1Gx|yHh*g z*li!XG^d+W#lClo`7-;dV%Kz}fQSq5n=iBPa1j3?@Aa({+skLZSDiZ=11mrEbgJ5D zA=$0`l*=Un!nb^r>xTe*=6@aP7c@+!J&mP~&JfS5x|~T4+UfUsm9s5V_We0Rym+$q z`zAJj0{~M9Nh0caoJ%$Tgv2NvW-1C;#)55T0|f>3X;)!NZePA$ni?xN0YZW z=Vj85war^0q6I`CSbeV>+@l43r`Cre%O>uLO?ihbJ zVVNz=>W!VgO1!^a;4bM%sTzwNH1eY_4OEF&uAF&}h-17|b{sCJimAGcjrBVt3*M(M z*2`UeEw7?)GHQ-UZ-xkl`+c-PK8L0xSib|!xGNGhGv)$=NL#pLh1mF_-Y4Bf+3P!I zBQY~)Pv87@@+JQZeZ%5$BA_R7=l(~kZPu^>_&MmeoFU!tw5`Q!e@Q~$djNgXU&UO2 zUy0cI!{13(*|keiO2~bmU$@`ow={g;k=t)w3fM0Y=8i0G6S45t*LlI9<*=94MiU5^ z6lL)nYW^E{*ucpX?g6`_hikF(H-z`J2|FTy-_M8-g}sPtsg#nl{%C*Fj`Hv!PD;Va zG<<35{;Pm@p-#LlTk#j!pqs-)KezAd#VQ+|>M9{TI}uN7{pRWI?H%-(W58muDxGU@ z$fO+4UM|MeB}KYoq9y_VKeq%U8M@3N3+B)&}WrTI4 zC?|%G+(J2BZa{7+(*oJ{f0$j9TprSPdx3dkaXOVF)^Ftr9=$!fot&PQZWTgS8x2WG zPs_Io8CHw5^X*;&?{KJ1|2E67c1N4>fc6@8Mi)rJ?Y zimD2S?<3}^BarXs_5OX@QJ&MXec7Ar&i7~bsyCXnx8%!AxU)C=z4Ya#p>PzC^!;Dq z%nQW^t`eFLPUOng@3Iqel^s3u_C70yZ_W&*3+l&y^yqq6r%H`;QshMFFi%PR8~pip z=;N`Xj{@*R-(F6+KN_jOGVSj`nt%_kWaGMUQz>2?MW3E#Km+G1KWU>>iZ9YfKDTTZ#jVbxzDe#M+lq5)dvf?) zFV3k3X51oky0!fgo){#po0%~e`bWL(G5U6Sghl=zOyC`CcZb4))ST`=zZU=gHtO@Q zjALb1LH69c$7h|Xjmw&X2*_-1e~8HS6pb8E#D>%wt+_&d&G$snVIW%cfb|`2-%_&E z{Ut9ecHZM^2=wsAwAy+7UsuPfgVr|N^^!TbsqW`q*V{zUH;oWiRyrZ?rKMl*e|xt= z!KR2`l#QzaqCU3;8})8@;n>hVCwjBzF~1=$_iQiP&EALj1#N#DGJP!9(dA(9)~2YXCVXM(2eo7%&QL`^xjtTzR|cI z6W9L&K;-mja3OB2$FgOwC;vXmSAd}xBB^2-^~`-9vP2B9A_=tL!K(M38U#lA-b~~Mi z=Gfod{{H1Ci26PMdeHZah8(s+iXrnXrIS7B4>P9s6$f`^RqG8km{%WIla3 zaa5Wpv3w_7bSCT5l}P<0i5$mTCTXf*6f;7#*&=hCBFYzQ6=8 zE7^TnujVi5>?@leH=G)76lqrL77+s$%oZjCx?ikthCyZg4Il4(J8ASQbTwL4bgRZa z$&Au&I$GQ8X=(B4)QY0>bK0t!MWz(r_rQe{o*oyQmV3>oefH-Ld7rMFR@8ZY8t}ba zY^3X-H#`2hELPOpcW(=4+0Mv+8sC`|&LYD%EK2_|tHCxF<;NvGV&yy!$l7O+dS{c? z?>7evIMT(t><^CAKc4v`q#kr$(_gx^wY6P?<`&6V#pf97h4#_(^tRLt3fRXwkc<+y z>ZYje?(&VfjsykLgwK50NxP)lfn+z`{^8+|7)-Y2`s7!5m`g8XYLv7(QF|28ac~5t z;u0Sky;aXm#<2CtUL;usMpa*p> zIysrQLWb+oHIh?OgQPYe-7hf90G^i&jV$@He4rOqA!(DO+&NUSA#7D*ebt^NRk3ox z>`L)#n_hW@^9-j(>Zu2eg>C9-F!NH)Vnxsrah>`q6zaJm zY0h1}>Em?SFrmK4iP{%xs$aMCApPnaV8U~@kBI68EAkt${Vx5W0sotRA@$31d`)w% zA)Gy7PBr|OE<3qHDLL{vDU!tTwH~4Z*SX`BsQ=V{8x;99S)YpVrAXaeX*P`No!oTc ze?J?rva)hl2`-u`CBL;dZq2WnKO|;)<5O~>Zhv3jMO+$XdoI5jc!l;@w4;qKt-$>I zT?aHjT+ww~xYA_o@o0q_e>A%W*ZAh5>%#k@e4tpr0tZrf(nV{u$;)pNIM?MVc#f@T z?<{`l&r704=iG-`10ywMsJ)F@0o zkABOFU?R`>C2N3Hu~@z5)xNX0YTTu;XU+e^G>p~%n}}tzdy`0JiT~2xz{H$ z#Y*U$=s>gh=-JcUD|O7K zMNvvByIrq3*Ko-(^>yK=H#Luz8f^bDOf~x}{I`Cc-ijsRkH|~K*-xXAy5w1E-^AZW z8F6`ZeBB{Me#Ju>=3V|*>)!wux4O0Wl^%x}m_BH>o!#_^_$|kJ=F9ZA$i$_v;+obH zKfM{uWtkqe_{}fJ6itUOS$tIWIj`z@@q4V_u?~jviG`OMhe1;{muZzg?`33*6>GmQ zZ8OA!Zj{U2K=##g?XTe2qn!Um)mMi_)pc(V(n?53haw?e0@5laQUWU7-Adj&@t3}hv$8KfA6>d;JV=AoU_kfYu)Q!vG-v?ULnyAhHOw-u94cE`Y4CR zWrE__!(Q^t3h*dafwabx3N^;!lv>mIuG0%_jCalU;;*G%_rI5`9nw2YyS^Wu$k}Sh zvjo6iVwB#{ZWrli!AXLpomB-eSb{Q{G7y8jM3%&t@#nz#@83~&qQm8O*k|-}utijn z;n`fK$5L3xh35=P6=HsP{ANorC==PX?a2qdV>h8`U-+kTByU&3!PB+OWeHa9?bU4UE60k3GH+Zi>#VMI<;TMLNZ%YlDywq1(92bg6Fop zvA9cDCiPbY$gH?sA$v16T5Ei6^Gi=`r)z>9v7=bewC(M=3Cw92=E*V?@hn2PF_Y*yYHp7-^1M>PcD=eq5 z$xX5ONpM9oZF}z*{0zFl2x=){A$gJ%4WGxJZJX{23kQWHD`Mb7qK2^%nCi>7ksdpO z-@F&4rlKtBo}tz{XJx5 z-~=qW#guWIrS#=N~of>aR zxSUAszO?guGjKYJT0PI_O*Gd$O{~QFBZ9mcrvL=cxF^>t}TURM%D|RoMes)`W#m_-?K{s2Psb&4C$WV)m zh0CJQ8p~OS|=wzD=wAo z+)rqeFmo81`(X11ymr`TIBkY>V8va zS^3jVx4oCl%AUa@)za8No;K|WjdB7gGrn{k7t*0od5mPnQ}Nl{xhsbe|r9!9kV3VQX~ho(6kUltR`V21D0h(IyFh&9>-!Un8F* z>P}s^CkmV$`yHr4VqsO5Ko}O;m+R}e=0iT^@9P@4Xh2nJ!({FhfyhjD< zPSB;f(47c4Omr^61g1=mJnS*(T49WE8mGqF{Uw#(bQF7C_hq=0D+xKwuK#Gzz}yH> zHDe}cUDcqkN)#K-vWan;;WgAWC{|nUKdtYI_L|%AI%nTL>seZ1#L_^g`g-bknmS6+ zsiJ;Om8I_;)lyh`uO%T~=6Z0IXMbva)A&B_&p*c`hx-fF2Xlhd$&y1)_Tq(b8qciT z5Y5y9wY95Ba{@NvoYvMvR^wNj!Ci(a!MX%c2^l$l_JH#}V5D{N9TyQ!y4 zKYBg{K92r%((!8;yQJ|30u$sMX5UqmVIfKKPI{;`P(UyNYK`?{yf~x7h2wJh`)L0_ zpD7I5gn@~v`n05>ziI8KVf}6s_L(X~mb0<(zQm`vl$5X0ncM)?0%>xPMOm0Xq@&t+ zf%tT76vfCyOs*6EyXSXheFxtp8qa3elasB5a7fpps=Y}=Y+_*eP;rU z|A-UBXi1d_IFd?dCuT4+eWPLq(^+zj34z=(Up^g$QssdNUdcpqonPPk2fr0{Uy_39 zE|;QvkQ>G_)|@eVreU_nUjm}|?*iB&?QvG!n%um&qctZAeIK-Du5@MB=fp2ec`^iS zmgavy!GfuqbCfQ=>B~|vQj!TYB#M2Cp$oqIW*;=c) zIcLX*hd*C6v8T-dQ%%e8a(91$+5fH#^~qLox1L-(SmgHBO&Ew8bVWB)9I21r%MnL^ z+WY-e%yY3FkM7;Us~o8i!pfD!M{`cIdYzYvuF+o?CIS6_f1lgK`;NE}5xKp)V=fkq zi{?fSvSOnA1m!GGGv+Bnrc+~D76#m-*3FLWVDiq9_WCP_mB0?esPV752xVN#M?9H= zhC{Or_>l4KDV6D37X@Vw&CU0j0{j8l*mO+WgBpiBbLC5OSm+Xi=~5|kr_}NdS76(2 z{xl1W1X3Q8mOP)?HhZ!M8+)SRy7NeaBpGQo=t;zC1k{J4mz6c5?pF2}JH+BN708MJ zQcr~&q<7Rx4l!^rMnt9x0e$W|Sf|Z>oC0D0umD}l`#@%OEn*J3>jIXj!^>)Y`2qFJ z8oTok!!NGM@l9obbUlAix`QB==?J^G!H=6X2ss0&DxP|df3gvncGl!RZfA;qovoxF zr%>XW9E+OOpV5o+&5uR5bK`fo$J$NT?+R%e^P7Guh_xS#PXjx4+v`ek9YaEY_-RRJ zKLfn8J@k-STgbsh-$EfX(t0sqwhoAFixDj0^lWq(UT*;ToKx1 z#x|eU3k22E8DPJ?Zo*-q8cy-TDzJl#|ofx!Ge1p72mz1~GB*dh?}m1Xuf$2FDYb57GZDWlvD{CoSnUzH&hq zY)N`uSH5=(D#psKyLAssLE9nkvz*pP_ggh2v$Z|GvogTHIN2BaA*;|TY5BlOkp{Ix zEQwu$4?X(_O}Vwda`IQYx1?n#_jQn$nVAv~Hxo|xqn_CPDb!R`OA9cS+bcmghfQhT z!@_qSGZ&2+48wfz;8JQNT$CH&ssE0i`a5dvm^>9n1gq%4*CkR*=NcKbe5e+1`D8rw z?17t`6@w$%luB-xj1;hWzY~3fL?OXwp$2_l7E|H9*Lc2CPjQy~)#CR!7tuvBo377l z8Q?G9EetRqgP$wIF?*wC(r58A^aJO7ASSH#ere5#CJW^|->Jg&=7g~a{sD-)AMm$; zxZYrzmg`LWT=S$xA&id8n`}iILa4rcNSZ?+h@Ir}V=)?K5Ut&g6JTatbZSB_7u zgGdwATax0M(ZWc15=nV1xNlO67B@&?u`% zk@&VXl>|xn85tOn6tbJNtGpNPqBBmnTD^xbRJ1XdG!Q5LI~=3FzX=VkQw8$e6FGaP zcH0CsdGv;!wRxY2wm=Wpsk_o7O5>e^7}Sg&;U-mMJ8hv=G6^SV=VfH|%{nZpGEsrl zEpcMvGFofxY@(n7m^Ri_GI2AbSOc<8GsahOASk+P5jT#sa=fHkdmvzfm%%r_d|6pU zJ^G(zm%Uqd;Tca`diGeMN;WfUcJ&%!Uj|HcEC5@EIeu}m968}0oVd6p12K5IeY4(P ztM{d!utWbxVg&fxX03jAr8VBWGHKW0agF80oTrqPU`88+*b!l;i4Z)U@2gXb?ZIhh zp(o9kNjzr5;g=D&VCBiU+Rr4cgyLTW1Wvy>z?SJZW|vQIYQk8MW=l^nfKx+%NdzO_XmprjQe+R zIo{1-D9*$qnF3$dzv(T(*XFImdqfgXD8iXu0G1M0_@nksg-$m2Gee1L#PtpKo4Qr~ z*AA3Q2W2}vm*ijvq06h)(F$P+&Q$Uy_B=BM$)WP-+!*A1G=NSj6ZYJb|M##p-Dd#X z6;_x|(EXTaYazcPl0c&c%Qf+GhprdTY5RT(>jtVE2_``djvk^uExTz#Y=*9n?kZC9 zH;baCL+Nr;b-WknTVc!J#p7V#IYm>8GU1Nimx-FhQA<;xSyeA#%>Oa+H+SNu-0gcY zP}KB5E1~WSz7+(>BaIQ}%jWedd6e~jnvoH7bPHo(==7f$#T@v-RG~u7V^SGYYbPp4 z=Z#ldSAd~EP2Y@2Ka9xt=jC}AAa_Wgw`a~@Q$(`1FZpt!5t*i0_R!{S!KzC@xK_tH zF^`Rr?SHLI?jPwI0)DueM)&TC$)7sMoxfVYu2U(4^b-@YZYZLgYxtzkrm1*pVGXd* zClS)EY>q|0xEx_|mzG`mkR?K=8bg78kH(@VC(lG)nHjb81n{O@8c(^&7!$|9uYhFf zzayqH?+)fkg%iZQ#Tym8n;dL)F1u|71F=LsH?Ki-+LEaol4n!ODmNzOIU$c*7 zy;bNMquoRt0do`GYX!9NfysZd5&7fp*`kx2?y)XbqLz<7W4Z}7W-Ra)=Tp z-=jIl2 zY^=D$;~Ma|!)r46WMlYfRCYUObPW`i51I6`=4EtZT)oQmOTbQ4Sr=As>t)G>y7ax5 z;@az?{KBhe!y~tU*-x7M>jD6R`T&UhV(vQ_nBM~;3NxUbXDeJ24KCliXq=UHOJrhL zafOQoAWbFmnJUt|fA{bMlco-C7b7b*l~kKv#a2{9flX?uxO1yv_MlB z9dGfRUIq}ouX=OD6pJVQ9|VT_-CgPDSZpqGD~3iNM~+_Ly7ZH|ed~QaD|5idOLC&m zGH8|e%y+Rkc4vMe0w%2^bIeU)WGpW9?=X|!;)|R z?T&%nS`hN{(aEO)){Kk{qOHIMFvqe%K?n|hbF|MBtXEh;}RAQanJc8v0O7bTP zP0B!;rhb>~{GU_92Fz{kS4@)A_gW!X_G%igqPEcd{Ca5UdrNXQ!xerTQTJ`RFhRFDX2&xaV+)(kP@5g-IUCsX*bj2N$w_mn8GPGLp zEXiL8BC5VpV}I5)&lDD_%jYkr4=X2xzF>OV#Di9O{PTVi_lDo})y%8TsK0k0Vr2&m zVucI~Zvb(e0_lH9RL|%kE4MuVTf!r?Hc_DkPn+S&m@r9Pw3dD1KeYuwzp{%W5L_>% zx%2=eaZ7y6;HbrSd9^ZoyLQ`f*5)eengyrT)G|u&p?^}k>CgCku&wK zPoJ%o^_9wZKD%%RbtgJU<`^g(>R7n_8`Sm*G+bA*F2e@a2VF+&v860|=j9Fxo`v^zCigkXP*msapsU2hkl$gPS-Ldgl{t=JpBb^hY z7}X(8!!zg>*1@UEw8mc>$>z8NAdeWJU*%36)}ym1o7{=f=%90xmcQ@Nx8BxcJmTqQ z%54z-`a_=C@Vs2EnFrMWcTX-uL13{2%1Zks{?3oVEJqxeVA}4W;!=Q}B%c%Pp10yc z#P9hKUYF8r>zCgl3M54JyB|E4Rf{N-O})<*UIozZwJILo#H5#wAQ>Nd5SmN5KSQHe z?4lt0xGV5iQZbc30n8ifePAPLrgMpwVJ94AB;d8Yv%H6h1yZ4m0lm^wO{>D?C-g<~ z`IwQwbRpY2a~s}jm;+3@tMwQF1zHf&4`=1V`tKc&WdMy#ixZliS18i}$&&NuN=tX# zH13T3*dB-$c3#COl8b#n^@zvd2(5BaZ}osul;-Z*`l>0%KfMYw{+#d^f(lWP{uoEo zlcVl6YJZLQ=n)IA5L|L=v1YZJWxK)=0?21I5hTE0`r>*6d3)%i>-yX~tj#7yRRP6? zGmeQ4&ohOFzF}?LnPfggEx-?YMEk`5KOENV@|)hzIJ}6c|7YIy&)1mjnN0>0)242z zH@?#Xx1Jj`Y!NMZa(jA3GUiCCAHUrIVTid8OYrdD%_NHvcM$eq7cznILm-Tw`AsYz z0;gZEO8#btiqHD@ZX#m2*Z_UXmIgE#I(tqoG=Yc=Xg@#9sHeo9=5b zA=K84jcel)>yjHg25I!JvCuT|vj4=Fu2Aat&&P+oe{q&3#wU>^(B;C}hLc_tNI(@4xB+39VsVr&(-l-g`YpBL5KscMXq z>B)K4(V7J=r3<>;;!R*tkgo}Bh%3>LvP$|a=zfwCFYm>?FxEc%6={>rKhs2;CLl%q z`r8WtbeLRrJJA>%K5T8QdhL zabsTTz*Q$RhsM(F@KzlMj<0}I)xAHE50Os-3v7((MF}oi58~MQKE17;@q5+pcz??7 zqw=oyP^|7o9Jua5_u>XZ4j&aq<2 zgRLk79xA+uLdj5_9YTf<1=Q0=MD^d1RhEkJ(JbiSAYg?<0(jXUm8}peT?sLb5|s~P z9LCYl^LH>lZCRlVGCxz!=r0d z<=xA{tT$$xeEDv_wnmit3bblV$VsnNtd17_geW}xXQH<2yEDmn%^E*8}!NLwVxCz%@AIvVgTud5y+Ry!=h z#_$@bY!{XyBSKOm-T}PrKSM3*X!I=hz*ma4)zNZxC*ScV{C>iEM6!3lZ^uf?fwH>zzj!HB{zGl7|}-_0l?H`F4m90#V%f{61O8N@|mu(~W;`Dq%-ObT&{7n&Kq; zL+A0p3)(sOA)Xsx4T)vmsX;K;+qbis_C^CMArX_*i~-?<@2rS@NsIvI`5la4FeYu-?)q>%Y{7d0NH4x zu4qZ4+F4p3Q3D_ROz#_)WW+1gS=YVXyg?UfFiMMhr3Wh84QH{r zq!H414+R*9uK(^-vX3EdIv5(6H|ag_VO-?U3srl-^ZlCepW5=B>{umPo5c}R7Q&6) z?s62HZ(XC3Md`1;X*u1R8)i-@dhOVo`yMiKOS+JNYQ?W5D#7~txrh6i7q}#Jo~T{! zC5D8=L|A6HkRcys@;Q8_QbbM$D#?s`*VYh$)sm3-YUbKvUOPGzvc{z^HPrS2v86!|Wy{Rzq_K_23goibQd_F7aWb4nPtdSmnvwqzS zi`G9m(lYOV$*guUMXkek+8;!V?OwRRrpg0Df{*&XvwBOq*(YyvZ9^?GjIP_GMKVz3 z?z}+MsIuUmzFl4pcSiJU^`Jkcz2c_#wZW=6Q3O^Ae78!*A9HL?hWDdC%~niW5`PRg z)9`7@{zwvfg6{$(0RJ8L*62lFPE2H?9ptNJ{^;^inMEBtkRQQPXcaxZF{0-L0zWF* zs5enk zRbyM>y>I0Rn{Ma=LtEZ#)V-Gm+aok7g2D%HIBHN#)72z0k<0T#dY; zMv2c1Ta=U>H?@Yjj+&M@d)hTir9g7Aayo73Gox^Fv8dZfshm(mSL{<^Zf*yx^Szx5 z0rRPM65tzw6=UNIBZPxl0pz*WR(EJvfnw^$;Nj7cNGTkemc;NHCxr(Cq_crr0ZrfE z_}UlUW)`E+6MaR_e{ddxb8uqT>gR^6J6;9q6}xxw?|;S`Y4T1pnojGP*%LblkW{b3 zXv!d&`EAvAb9@_~GPfP$(krCr=f#UxYwPRBH*>zAy3;SL9GWYQ*W1{mzN>?T=I94= zI|h}mqY^a;v7TZ=#V4YZcrQP?a;Y@)1?W$znD%TYCQCT|9Ca)L8Td3O?Q}Q2y+R}8 zUpedL(1uQLo&M>lk8Tgz|?vt@sD>VvB z#o^wztBBoVz#%J*0_>L@oS6wY?5B}HLPCFcfts+T&js@4t-RZIrI#;*s4IEnLXo^u zn-n@O?opn}ES7s(n$me2HUD(u4JfN*|5Y5;_{Ac2W%?Nh7`|$dBzDTu;9@z@fgP-x zrA>M+f#h`}Ck?L6iNr56zp>W`dyzST4SIh-?JR*O-l>jCv;NZN@dkFK$4+m*0`MN%w0~uDmy1Z`_ur zWay=+3G;pDw%|0r90(Uu#IYTwRlc3hnBpz{4%l(RoQzG&$Ae{xW_Hnxabc>7Lf8U2 zw#VU~+f=z*^k|5keX|ECtqwVODNhcr?C#}p2v>ves{`VZt)9}P8Y@=cI2`5Y)2s)J ze<*y--oYegpnj)SuCH|x}*voA4X6%o*+nn zJNTAT;B1qm0Dn$Ok#E%}Dt^+b$s3Ej5PDy2Fqo=zI<%j4E z2pvRZWZ}z@T7$rTEPVRnPD3E} z^|7CydIBwynuGu?FfLCXlNr3Sp>>6%FX*M94?`5x^}TJ#sA~~<@&VfxF21)UG@IQ; zR?)_Ml$5^e2C|FCg4BZhqN_Gs4$RFDy?F9!pbo?^u(`T#JVL86Xo9S-GJQQyv z!D-d0g1km+J!;*3{r#(j^k8Q(NpHq4z~R&3)3zJAHl>`0b$9;)Naaq=KpJPey(y8@ zcPOH9x;3~2^+DPo9V;rGD8)~(KofQ|9nb)}2s%%6z~y=)Z_ei=wwb#!i=1A<=zuQA zqdVm(^;&jZfSm7{44rIsbdmM2DP{yHV6GYGnqaRdVpkcn3D-Pn& zsmBlR>~leXu=s=U;5^4|(j0IeH&yT(vs<>A>LNK!OAtt0L}+>44QjE_R&7DUm6+`W z!AepLLi0SGdTaZFGSr}`k-;VgknKc~X_0U@4>ALp8S_Raq50||NY>Xttl&McIRoPY z$-6h$yE_AkZq_I^9?Xs8(5sHc>`&lBw&O_nUU3?;k=rXwH(R)oap?2`C2LE-Z;)lo z1=RaSBXXkRQxm(rGJQ7O5zEW30Bk@|iFOfnG#rh8X9Xnndy7~EgYd11S z1WL4<(JH4ELp~O#&4^anclzfj$iG!`;XutdNy+6IQvOhY)I=TBzcp=b+FU3<@N$cs z@^4yJL8}PcU{nD}9PEwTtxx&>uu~r}bLX(*qgjqNUJty0|=W z4?sazHz#xSM6Z2(w90^^@IQG_p4v?Q^c*k9i6WvQ?TDrCg9=^N=s}RS8;ZA@6&Qu% zZzJnopa$tqux=$3`(cZqU5 z4|LOa&tT2;>5_sq`;Ay;1H3i@x{TFOW#iQ)U|_3A6olHp-rt|kQTF0`0Qwjnw7W|} zBkK7*J6oEVghW16WohXPI*t6B$Gp5T+1ZRyd3kYh>5=2+PHw%4@x|R|pKmf%_>M~9 zQ;xRWu3QLu?Y{})4grV&>~PBPh@;w9R}o`jA%o+}LLtnDjB8dJ3=PV~FHD?bd9tOM zuke;7kH*atih4&3if;=zI6dr2b710U@6LxU1O)l?@tyfT31o*t_1891hi*P2*$$C-khA>rPR?`k^@c1O&t#!4A2~W@i?eM@xaY*%qdsU>#w*GhT=Gyzf$gPZ&%e57~CP z%j6~ZCzhwB*`DpqcQ4F)y>MT82yA4hQBqj7oKfZY$nN3cVQOajkeDpE*R;*_xhiV` zF@Rvwva-aq>Omr>+wY3CT^bd%66A)4MjG)J=1v4M5f?$SKLwEau)?Zn+qF9$y~cO2 zeo}gHaIge?zHeTy53}6x_Vq-GKI|6#3efpHkA8E!2G7ULarwGkLaBp_nP*Js*r$1K zBX(3y?lMlHpoMFpM;+&#FN8+ee2frTvmY8XrLi$(H~X zif0eX7@f*1&?+{x8W*5;8!eSH5-$|m?Sj>=oj>u_D+?wl-sWGKDN)`_))KzHm~9@= zsY?LZRYoF}%-SEaM?|jx7V68P_Zo;~bkD+~zOqeBG|Lf!Z%xof&ApX?e%+e}H5m;RY*p5WqCN2zT?$tLxOFxZXYGDp~Tn>WVx;{OKHlT3xvU-I)2h zTe}IQZf4`b_<-kkYdoEE8Ve22lGUXV7j?^BJXp4!tqvCvut}>eUu#pV^QJ7Hu{F)k zt909!NZ{1!%f+Yed6DtT+??4Q=m%R>Q&*#jCLvy-jc#gc!o|DK`0QC|&ECvNgPo1_ z7?AEX1m*I9wPn)QwFbS*_E;9Ck2qgt+&0eNf=ddgZz3e^faWhpt{S`fAvyItwBeuX zUIiZ{D}BGc$Jl5TV-`QZuRxAcI?vFv8qIyABWSnURGZ7^fbd>OzLaciYUFaKB@2(I zhhBxb?#;Yo%DoShykSsE5!~3e!PYw{zcb_P6-xdh@&{xNOJ{Pqj1q6>b4HnpQq&JCKRcu_XpHLG4&7|f9FG%qbX0nhMmQ& z!48G!uV@qIqvQy^6s|ogHBP};6=;e|fahmvxnQ2v5GESYj1R_jjYd0Q0;X(7_!N71 z6B)&l!YrAKR+*`|%AA*8^1h1VdYLhtG)5s6Xtop5bK}hP;g5T3w2Zesn#!iCN+aPI z=wOZ-2rky~*k17ZN;?TPQvigWe7jfs++i-XFP?#0GqSqiLwkRJxEZ^~pQ2ajcWN`y zo%t=*f?25cQ+&h^KTys7Oy|!)j6CsQKrg?&3;@*<$-DuP>H$T~(P3`Gf$()JdKbE^@ z1Gc`#L8$e+_cZWYC#Cq?Wu&vO2X4&hB2xtzZVknnQD@kz4$YnOOzA?X^L3%?rd|4l zs{=c#=M8NmP#d^?*St5i0r>3EDsx2rlFJ1W$+r*7q=gaEiYeOK4q(#%Q*~m?EgJ?w zZ`O!1yjH0fu1+*;Yy8(!fvpiPHXoP6&k=d9k(P2OuWrJXl5UB$7jduRE4sMej;Wtg zR@2$I4&Dc7z-GuNz_l+I?dLVCR_IF_tw&;V_fU z75|&p%mRd_?~0=DLLR_tuYD)oxV>gqAmiM4^SaFgH(tTy=-`KT^$QWVy|$KD0XX+@ zyRy;}k{UIul2hN7j4Puk9H3iNx|e7&Q{{J?a;yjW7JC$2G{o2I zRX2a?psvrl%iQO4J3s})*L_j#Koet&;j^S5!=e7wAPWQY3#Imp9ayLBbXAb75*4rK z?lbVsx)2$sZbWk?eY*pd?C6FPsHdlAEpfWJVtY}50&loZ%LGe*Jo9Z#{8Y6Gxq%|8 zK$G?kd@hY9V>2^hyG>41e8*Cq@T}y+&xX^H~-8anp(bUkM&}23N>GdN|gw}-w{$PbX6*$ z@@87}xq52S9nH+sf~u-sPaXs|rkl#niTMjl-V8L>H+iRigkn@w9@qV{|8Ngo7ZqVyIsM|(iy*** zx_dzuoW_jH9mV)l4~Adnt-2z_U%!{;`S}EmYski8@h9V-=*$b>GlZzbS3VT_g+-02 zP^$iyhi{(!3~*M>_dm&fc2*M7R9l&N+z46pV7qVps?48(P015`tBK?d&X*IqSle{u zO^j)!Sq&rOw9-YRPv^Wxu8+mNep3mLj8|%q7lFYq_7_uqyxMJRmd2*ThcT_cO)*bg zI?4O|-a%SfAJq;>wI(1zejRWEhQ~F9SC2wd!Uj~MTbxDkx1;Qax1$NUZFF)Hxt4V= zF-b`u{ghE86=7t2U0iZyq2n`3jrSroAvn838smLrS3+uH^h>?R{w^Y*_;xe$!~_Pm z*Ah+-Wr*^(ww|$Z$*MulaPU9hUYBy$Bav^ER6-u(Yz_;e#Sksy*OYb{_HuS~5&>Th z0?bXOW0v1rS2dvD5Vme!P?b&Ee|v$>o>2VK)m4z{*bRrnoL_Ay_wl;zeL;u0z;co< zlRsmu!$ArhypJac z=An=7W9s`HD(N=56A3A&`j35gazE=OkI6Lf)3Jw8e);mn-lj5%Q?uHUvrCC8WWmSF zXlG;3l9oQ&ejegk?Qj=fH8@&52aZFDUHU8>^!kgd-F!61Z#C9*e|6wDE?T|lf~KDxkFY+e;)WMVoDegWyk76lCGsM z&GXoxo;R?T8G#*M3xq}J*RLHdg;%RXTjwjf)>N?~F4Aw_;OP5aNH`<3kpWR}^V_@} zZb(4GA`%4WbH3!DA609_w`VO`b)Lg7=e$sQFX~VaZ)cz+2Cz&Mz4L+v@z0~#I^}jd zt|#=v^dKJlHj15h6X~%cE9I0bPb8Z`s;Xx&nf|^$EKrr@GLktl&nF}>(0Foa{$d%9 zoUl;y723!47JySj_UZ~adCGF#x`&OV4sIGVhx66t@Mxa5tU^Nws8`Ctf5YQn!7ZQa zDOuUs+Z)Dhc=mT3-f3%ZyK2R&h1Gol!@9q{9Vc2l8J*o4dFEcD$=Poy6H!2P|qEWnO^tg;&Iq;u%n zym5@Dz`$UAdE|6lZgH+Z=Q#V^cD8~R^eqYaK*yv*8$Y)guF}iF=EZr>3A`LKGNRz@ zA=+Xid-w5AvP?`&Aiv2)r05WHp5bzI^J8}Y2eUPZPgEZ|jmwt?H5P&6XgWmqo6!K} zBk5LQc)>^^pKjwR*xk|fxJHI!C_}zFxWCEsylTi{gl)o;BWl^v)O5XS#PGzOOKk1e z!`i>S0DC%x;|wCqGgKVtEiJFm)7s25syk~VOOK(HTqOVIfnrK|bnUSi(~3!>!kNFt7cw0?=0X8MNmhU%r`t?X6_QfYOeJU2pGw-P&wJ0CV8}a+5_uv z|I&0-W zX9Dq0rG3`Zu^AYSR0=QKq9i7=SKB*gdwny@LwW{~x=(ygs-}`ceBTdBRz5$pY`&DcFEEgM_tZg_&o~jDz zeYA%*B?492#ivaWi?V8^-+2D&>abpkIAkg}e0AxIR1y*?M|ZBLt@d@8=W6-zm&vd6 znk_rKnnrQ^|0-5j7ob=@0|Om-s4EOF*L_!{?K(&a6pTmtb%dnrmrAC@JGS`Kiv5?x zWQT``q0!OZfxU6%K061<*SO;bM^kK%L)qEb(l#J0VnaFQNZneVMn5&s~Zu0yDzmn3(*kN{K!W7W_t{m8+t^X0bbTR*J$MC~XwVS{6AKcwn{Duh9$mWZ@LY{*0d}VrFt(yHl%U&ja$35YOiOR%ooZ8N zcSM8L4+QIOQMAHQRZehT^Et~rkI877sCiMhth(QI6SE)k-+yDomCeEheM?CR#+1Ci zCm;LxD+2*SGBEnqf!V&%KCbL_X0*j*VKFOdKM#TU$GIaY&@AuXN?z=?N zB4Q-v^*owidAvJEH^~+V3cG#$^9@Oi?`hM?k_h&O0OHRtu+D!6rjRrDgVe zvJt5~xgTfu#+YzTSRz{Eq9wwYzCE_A{^GuY>^|H-m+T49n*{iT#rIJJMZgXO<06H`o%F z`z&?)B_Tz7O_wJfiO1$ofdvIUq@-MST~y_zx?VNdU-3mDDf(H8UYeR>AR6JW1J{;h zRWmc`A^~!SMGupe3geI7AUyu;=e^sX=!NNr}{8HN~NUpQ=zG5htg58R}UZ3 z&_u{(dgDoj;2%#{MrQrWIvA^j{qTxLTsWSQ^H4=9_uEc4;u6IW)uDc3|F8-cQV{6- zbnSiPML{}?exEzD@(rL+54anTiuyf@SG0|GO7JJFX3s$fYy8pEN+H3)Q=G>$sobmO zEapFoisnRiPq`H?vj847GNS6fH<+l0xN2yfTsnW$%y4SU1^{A}QfO^d#II3Ik#S-Y z>eaCg%cUP^7gi(ZJ$(bO!CN7nCPk8N-}vy+}P z`BP?wr%y>rn{NtA6`uaZ8f%{0IEh#d$4aMzt!{o*HQklxr7qrL`o35A&U*`krT%8X zr=a6EXPfEuJx}*Tzec&HWTvMFl|nCwJ7r`fej|=(SYO~)TAsO30!usAldGH>k;6{D z8mC2Xd{~irIDQS=^R)Qu1c?NMZsT9Z>X^q-hJ$Z0kscn($KT&FnTws_G`Q{~0{R*! zYkei>eD>$x<&P^L$Eswsea-1lRoq`^km6ksKj5XCxnvMDyItRLyn9D%j8`v>ucQNm zLXZulf8XdAR`;iR-03D8av<@zGgdNU`3WUOXgFLw`EtubJb~8gIn^C7tgIbyj@myV5}HtnKszWEXx zYn7jF{=n7nc+wr=GO)>$N<3^&x|aJa1*Wn0xsKou5|_ z{DLg<`X0$l#EHw@4ikLYSVnJ2l$@!#c^eQw)f%8)>MMhrY_H~p7Q5-g6DRs>wgWnC zySN{KWsF|kh~>7QU06FIY+=HG;WI|kO2lgIk^TOH_*AV0kfMI~jwi$Oj8$4%_TooD zgUx({03FyA3-IN7kY{f->%B3DGF6UM%}NF(4#g4k*LWB>7xot{?j`WWAL^Kw4M2O? zFWIb7OyluOv(525izZv{n`=zcHS)3kn(LUo!}0U_|Ek*&W_;;`$1prB^oe3E7Ey<} zjzb_oSnPQ;9*q$9GHe+~FLya6gPh%OqU%rOY9ED(?_~9@by`0??E4UHqSl80@bN_h z4e&ruGaf45{X&OwuStF2WT};zCjmT0e_lYkH7O!wug^2-SBAXyrHvsqAis5mkA|bc z#XJ=Fo0H9ih`K953=E7GOP-~sTFDkEe!7+lCiYKj2dW)iT}!vv`!ZKidpyTkT6OM+ z41)Gk7(r@ZuIL)`h6D=Uvo)U1K03Y8(a?_q<&90v?0Jz0Ny2&KiT4Zc$g)u#IO;$~5DkPxX>o@|FZ7#??V)~!!aj})q?cps0~URVza zx%O)gkQHgylVM`UH+`18T66Ngole{wtAE<1L=m7wUAt7Dr|-`w&!Z!4JM&H$z}LG7 zu{$cGMFG})AGA4NOYZOQKO!PQH{amaR7Axj-;B-hTS@$66P8;SVcb+?4Si1_=q3|^608OlIFFA?$UHQWNF=QQsU8t$+c zP_V5s8;EhTHA`-zQCzdIiRqU%M?)jwj)NP9t0X7}7^O8E!LDTByy*GT4$^73-i8I_ zr<~Vb@TgF)#OaARdN>(ZSL!}AI7TP}`0fJ56rqq(!z19Fjn|1K>1ZGzJJc%0kH7{w zzugY)HWv%JzP<+lIL0FE3O8NEr2}C8+c{k|b~8)#C@{7EWA`GJCpc40PJ|LS*H2Fn zUXMUOKIpO!qzLD|iYjevY(u!ee*OC3vi)2uS|;cHa#QW?mh&7-RJydaH5G8CngFBA zcIZy9Znz+>U0RLYU&l2@LBVw;T-h5~oGLhrSql0=@XloAd)T{aybsf)ci5Kn98E{i zHgLhPtR^*r8OW)TWOYUOE0eFa-v@RAr!>a_k&zoo%TunCYuc2|Hsu^hgH=+;lgycbe9N7Bi$`XcWk=5 zyE_F$1f)Z0Hl3SBq)Vi`8>Ab)#p~~V&pF@v2f4gh>xp@0?wPq~p2d8PEvwhST^)ea zQhrS=^RapP?t^vq{^Y^s_#VteftKe%Yp@n(C)2QX@`mqYR>y{&3kv?z4JrUYs_YLi ziVlRJtTM(CnDy#^)GD9z|m2V?iaW!co_je++Q z7pX`9jFzD**L4D9M!Qxp2Id+d`g%4& z)#Q^TJ-k%gaRJ~!fLary*YiyPQwCg!Za)`4LejAMg~uRCzsTK>6v!MEF25s+K6(YM zE==LoTl2+gvQVLIS}T=UXvz~1TmXctsB4G)iE@JKe0oGA*}4IgMsP>N$&W?H9m*0{ zbEQM`4=F<|zTAB9L_|b{BF;=dl}J$mAl@PJOdN0*K(*|5T|ZPR5r5=v>Hp04ufEPN zOz@0*SUjca%bd@rE=Wuq*Ra%l$}7>j~i+=6Z^G(gPb6?vdb#E|VD*|^eHW%uK>gu%1emh(*zQE6qnPI*Tflv|= z=+Z&bldF9*B_D8pDiVgiq43B5+_w~#n&XS7B>0F#P2GL^Bd+KuMZqp7>QjOA>VQm6 z@5j%Dric9l&$qx%fP+_gT$v$wl>JWEz8{x=BSM8n8bpd{GD+LWG0xZ7hbE32Eqd%+An>(|W=%UR+d8(# z^<}38*VMTDXpDx1dZ_s(BKJWqPo427#s6|76~J|#G@P6y#(8;GGgR0pQjgE^R@v=@ z46g|)CYNaDr|yOSvl`Z>O^1B|%2ft5#Y~|x{Wy1?edwa{#dpyxjA~|G(LCi@bVMBk zmH5S0Pz2x{yY;Ff!5}mqhEEZAKtpL}twhT}R(`YHtJ2#DE!L=(NcMfha9_5}1-nG; z?c}(u_}*^K7>L@%&g#zWmH@-YU-EIQIKRtQr^v~S>UWZqjL<9Mf`%TKlo?=IU%!382>d+in5>r%|vQE=bae7A!OA=wQ-?f+PCw{Ihc6yo4SOYE{& zxIaq6id8v&rbTY`1TsHbEuvo#lsr0m`Tc31(E>g!?e@eJ^D%IztQAP0R!qto+dG-A zvr%aNE3A@FrQU_fFTafp$AQ%**xxPlKR&hJ^6I-SN8+itQp({NEVHAAN2uB^Kpx2> zbi0an{wbUQaPBA0mpzn}R0z_|>lSzdXZMWCDK1ba9_n}voAdR%*+j1w9O(X z5kZ>iyF54kmscXWIY!9B!wv)Wd1CW`gE*|q$cY$64-2j+ujXa7)eCenJ584yY7`Ay zlSA)$o?vyhZiUNZ(y5Vgo;Y1sUUJJKuTT^KM_tvNQwHk^^czCh0iTQfj z#Kg~^j|yUlH5C464dKP^01D5D`u(Tn-#XtY(2I}*VZ+=Bo5OipTN?j#NT4b^Oy5`WpLu3* zT$!E?5>zwUXVf#l$BXjrF_1|814p4L{j#zzj{d5edPoXsvsC$ ztr8^S;wNBYvIgmtl0MQvTA@ykvtk1&ZpdLxA^AY_(I1Gtl&0TAU`t> z-%Ox>N9EPNbL5qKPD>AOVEK3Az{XvB@eCv|;GwwfFAkIlanisJ6n*wrR_WpMTcub8f?hdXyyCkMYRs1jwFo$Xb&-(ng zA5BzL@v)cZz*xQPd(l2cS>BCtHSZI!Q2)q)l)PPB0%Ms-Up$$Nu5P-TZ%%+e7C!!d z=nF$Az2V!q7p;42-^Nnm|K;>k+eq``XXl_IA$3lLBJkB8p!<~HohxZGS|7bUJKfOg*VaJ69Yzwy8P<2^>t7M*yBs`1@XBQ+C%11vxokVh5C6#X#yqQzmJ-1t+gCo4fTAER(qp(lgn`}snXr`i1_E^ z`Z>J3oT=zmMMcGHGg=0l?A1}|{{J(q{#P&djS`8F-;Yo_fdnzU`*tDq>(^i)3j@~| zPaeMeX3Pj*3R$4Rv2Qp*Sj<`F)1=>b)o4A3jCcdB%Lqy&;o`*q=Y@j6JmIYCbp-`C zj4zH>C{V&(34Ql}GXRl>jM*A%&NzeuOvZBOy$60`Y_Y>$&9vrmdY3*5Gwp-%;?P(V<2tEjBi` z`cEXq={14D&uw%PV>UwK@8F~)93a|(t0Z*B3q;>O#+jSm$y%`+_887{U#O+PBX;Wf${L#Wwcb%Stcx1e+ za*4*}ou8>}k`c#Orw1UVYjE-Q>SPJA&43kB4s43s2$$DIbRSmn}W!j z`{DS^dg3L;zK9;5C&Df+&H>&eC3m@A!mGY`Uyw|}1SK6eagkD1gxq&&Gj+49Yo))R zH5=0Ea`G=um?u*H=d&sjCcr-HNMe8Q?bW{P!y%n*q+qaAa{~6Ya%QR76!Ge4A+`Y- z*(xJ32ymlCbA^lSE@axbn8yWF&+^yI)Nx7Ha4EOj@bV)C;Hy$E@_Kcq+^IZ7XK)X zQYAoXBm$e1DeO*M_5_Nq&}$eJ9-7soD?qmVJhp0gMq~$SB!>dDZv;NGZocUbZJ3Ot z=y!D>Rf_bw_dC{WWWKLD?`35H&f&KC5kyyK?llkwgOkCa=<22*SZ|>UWq0W21tOgA z06QSH&Ht~evdJc-_Z_A}bah~0K>N>Q0&lZB4V@qV|Ddp)1`DS-l{;67LS9NjG~n|AZ94ul|B4-&DF#6&bCT~1JSmm!+;Cw5uQH63A{lJhb0d+kKLo27p?WqrtV-CI)5%?cv${hmOx}hMxzNp zf}m$$aE1V#FN1*G#m4R@8bU4`T!-dI&9LxrF0Z4%3$jVkiXK(@n^Gr!vDt}$(zW02 z@2)cgNTo18A0~s_?uXLKa}XdxFF%T9UkRPF})~BAQt?O+<-k7L1#rX|wbHFhO1^<$u zSqN(&^i8>5BS3TTWQ{;0iAhXM&N+~Jt<^}U9Xq&hkUhEn4 zcEIhr5H&K&?K^s_!FaJZLjaVDgg&$iW{TuQ#Gw*6KinXbin!o5dpEP~qLVikOjwOG zvS8>#+@%_im#`K}U(eUuM*fLHqHpt(uHhH`IAPjn@@Jfi*ZG0A!EQ0k-F8~M(o@LH z!@&c?!vBmRo|Hchatsk3a?t*l3xEj3fUlY;{wL|W`#6l8Rt=aR$r9$K1e+p z4?ev?OMMJ7sO$ri39tjHS|GdcdfQV81h3rW(IPNNVxraPsM-}W_#ZoqlwVRlV)Axq zcb6v#$e-Wj`6M8;kpp+FU@_wZ{3q%I6NHzHRB^Et5(+%vWCQ9C8Y-+beP6B!RlMWr zalFyca4}$pO#7-&^?+?Pf7cJ=!bb>D&!M6BnCnlG6rJPPJ$YaDO8%D|Q&6%OH*u8M ze)}4fqUPVxsgZjKF5>WIf5oMrB=zI_>jkQ9{5-oyG121ING;h!-%o)%f3PWMpOp%i z0;F~lu!^$seZ+2+NW>g!m&syBVmMwe)IrjXMT|Spn`rA-;&z-osZ0KLltBk_h(^1glhz2zeJ9cF`0zO#RVCs!}wSefC=rDmwT zX!+z`;w%d`)HpfR-OPdm2nP-th%9!)a+3c{G?u738`IFf+{t0f7*-sTO9}&M?|xUO z@|j8k2@5AJuwr6j=!Ud^%^Me?7FAV|ax*y3)s-44#Lm&YS$VG7vIdeph?W+ej$aK* z`0&9q*KFA)${df2^RQcMCupX6i4P9%Q0tV#DCPOaY2p~@sAZQbfUk9=2=t912Q~Pj z4J}1p7*G^QbTY9l!XX7&cOeuPVTRgxIU)r^bC3YY?F=}C!qhR;qeWSLnL0V9E7?L= zQYs#NfHHuFMO>X0aFA&vUuq-vXem);#NGW>L6X$s*0oz*Q2nPb97x+>q9N?$wE(o9 zKj%uQP8a>yf|rM!@2yg2%g6pY<0`HisYY`d*T)U|1eav0Gg?ju3~&wT>Gl%ljy~+1 zMs#O^N%VP}J0IyEy=j=5do`e|M^^NZgOndlc#JtAh59aNX|Iv7!qPQpp+ZIw%ra=- zCxH?V^YxP`2f{1kk6ilJkJvTIBK^5o=8w`#8yF|ITi-UDFOe3w`J-MO-ETIyE^c5O|A#hrI8ciqM==1 zVWv?*nJ7}l89pPQzGsB7kPpHlKlJhdFq&V;nhOnQhWu=UJIVI&6KV4tU(Jx!JK_rx zHmC%_Q+nO{m{4(N^Cv6!RQt|k~QA)M8Bw$xj9NVrCviSP)cHeOEb<^8tD!ze$oK>)eb1wNbnGC1<|L0Dt=zF0AEWr=P z8|KsVxAC_(+J62)ce{1w1tP6nY%YCD5-0+@$ zq!U1p=RArkKK?L!w>D-z#QvP&{bJ_~|9vJIS|f59+9^sd|lDcIcp^sdGlfOYSIRA#8-&PXbH3Usbo zDvQ@u_|AB{gfBhrp)!k1iTZk}aNFsiZKcVTWv!FZN)+s9tsOM4waEQ<|D7HPcIGCn3S96%V7 z`;NmCNA&TYyorLH*~DMjc#;|k25<(0v{wAc&fA|5Y_%#Q2CQoI3hU}{&v!;6#Y;MW z$bP;>`)hrvMb^P@s*8-~_E1NMcpdsCg)?yaNbKUFYwi2(W^8qE4(CZ5jY-&i&w)QU zLs!$~j%hbU?Yvx30){GL#0{v!sa!CYt|V_Z5w%Tz`hLB&gsUKHE6XJXS}Mux)8qZ= zQmdmr!qBTXO5ID>OQBKy?uV1s7Uq zEL;reYVja$NlBwH_&jDNWdz=^fsisX&59E`Bae9zSWD_0hV(|12Fz3QPQP%3VFVia z51y2(Yp3y+;Zf;?5rg#`Hb2O0{D84X(aZ6AY_eGd+_0aWpJi^_a_P!LF7X$A`s|TV zP|n1{6%4ht3CVpnjU}a}Ox6-mRSXO)ctk{4F>?Y0UqHd`av4D~sYB*J{$YQS;3cKU zVejFMIXjHby*h8B0lLH|n56G2gDDzhln{R@bHa_f)>v~&&Lah7{UsJ8QKvfi=+KL( z1M4u=*K^^+FmUvg&$ZQM%Y$g2qaQ+xdGbw8tRY6YMxB1&5r$KG7!uT30h!xtrO37z>EoE1f^c-!I+WhX9;FbrO(ObnWo4^9j({SAtp*+5`Rz+d|ia8KrYeH=_5qj(;PP7wh)X+s1!Ov1(B z+h2PjPS=muCY$Iyyd$rqkO612hZQw2csci4%>A$J)Z%t@K+x#mkH`R$@DI{lF{6wm zt%|9a-Y5ZW+CMWW=gce32^OyF+73CPc>{Sng|$7f*ZIg%6~*bO6z^V z`T8Zg-_Wb&r;9t;G{}d+w~V`Qf5h7gyfd)!GEU#C!>%zA=CBE)I$JGz+}$Y%54qQk z7{ENaD{k3lW@19a6uf`}a4u$ecR;3pY;TANP=KVz<9Mp_zk#YNfGx|*GUO}@!Yc#N-Z_tn`X4MHY>+b=vvxl?N3pD151`wke~~f?%91q>WiJqJG2a;EG?xK)D}Sh z9LP#}^92}GVeP1@W^KdoyLsJA5bdTVZ-x$n1&{da`qajRLvwQ}i@bGobZ!Ec8*4hC zME`F7<=CVGS#ySTL#6pwl&_ol#u||w#R`>X1Z|(m4BLqLNzM+MfIF$S%ibC8-TfqA zTPTUgV1kGh2&lk48-( zC4hJlbXhjjWl}v`Uk3ZT>2y1T&1rlJE9~d9!ok~5lt@jb*BVWi@e8?AR!7^>`BBUn z4MschWWuHY>%;+d;!q=-XvApSn)7`Lp^f9bjreRnkHA$Q)+F`7yYvH893IMh7Kh!r z0M^B&s@kj*oAfARz-dGn9?hVleqP(3AJ7O+gB0zlxi_XW-Q3jiW#V+P>; zsY>!tGZi-yLI`RCGd?O4e(acpndjvccN7>SlWgo$QRmm0x{nfD2QcX^0G*ebW+PwC zHy+9D>`*>dE{KM=R~Y~`R@Tecn_-WIQ7Xm1OgEF4LZ(JX-M#Uw)pvTQX)DU z>_eq+kLC;V^Vy8o;ecF>e=sa7A1b9wuO(ZC~Eb2U_M8jwTZua~O|K$r3AvUBvEY;w^fpIj^z37a%vdpD6J zJMQUl`N`la@Vy<|5UY!4J!k@O*dTnYS~i{XlEOe|sOquZJ%9lwsJ}rpV_EcoTvXndvH^D79Q&x9RoTEEX>9=M@ZG&D|4^lX&v zhbOBa%fZY&DD$@;8BcFEx8cW z*o;=q>-<*o`OnVTR&jB0RYs!&a3hibN?TM^+@F78nN~vt>2@W*Hii-uxCJ)q7!FAz zi{k1L?wcK=WG4$PxE^Jp(>d05>j#2_0c6GrSXPLRekee3glS4fc$Rv(oth6F7jgE#_}gP|maF z?=f45qobog8e(}fv#?-&Y#Z?`N=0_GQ~hoqmP{WJ3@eoHJ+X30r5w2ZY$pl+dx+K3H&~?D(O-$LW5) zqJ-1fIx<=MlNa?gUNdO zK^$VCSWxv}dONwzpzPs3vhP)~?{!6snPZ9*luyd#!TPbQzR^tVI~XmcB9CJcaTsb5 zBVpuVxdp)BA71zNzSMCuS;XoCY;~*%ObP^hquc~W8b^8!Ml=dO7>MVwxU+NhQfrog z$J(6=Pt`R*4F#T06vYV!LyTnO4she%I9OP4$y?BGw4fOlg2+F=?J>7aj7!JT;RZ=h zxlPqr&{WyVnq%%JT2bcXVNIb#eSldPfYnlh8zqB{KZY93fsvfBpqPRmT&gNdPw{8v zu^&=6!R)hqoZ8|hcI?EyH3+=`lE1kAT$OZ2npzl>r7yyH(i+^__yk>D+z*Zdz$ac8 z71Hi9Udsec9+?|gGPBdzM**;9)vhLb$)12 zwk==}N$;sMO=P-`{j7Liifd-&yk>?q@6iDV1q7SLn zEJayLW5kH4SP8FK4#g;EBW?!8#nVd~V&2?2cSZNr3R)Ed+Wb4$xqA9wB3~ZK_!=W+ zwLs!HUjGnO0~0(;p{9Fz*~kbVtFQO0LyvWKnFjy>EfVE7E$vkMBKEsL@2>TrL~r6E z4}IA>t}V8(=DvS#ASrS0EVsgr&1L7Iw0h7k7N|pP^{&gM6=PxvR@X$3vWw%oNkK!1 z0+>i4A-PDXt`KB%5UCrFLb$`3WZGzEc)*s;E{|PUnk6TPIT?(FR3b(ft8gT6saw9u zvM2qsH8ib?-EUx>A3`CCI;H6>@@jcZ3p}4PH|on5t7huazX<8q`dluWM|u=_bK6^D zM&!At;@D=`cY^%EWxfmoHAij3mh3Z_5kvKEK-do#A0N1~-t~wJ2^I5NOiEqvrNYRg z-(7LdMOGI1>Ub`){qg?1C+u$oL54Qf-_f=j9a*ja+1Zh zM*;kX6HYV_ZvGlN#^tRq9pL^*Qy%zj&1AcaLdZp*KZ(i}j06WU_IWQ;@%(ETDER?PvGqN!oi_tT1TmPZ0DR=#noOd5iV1<&3FFBa-yNr zZ}eUnk$D4`uGYkwA2#e&WiE0|U%O0#=Rx$$XremRC|zq@`W_ss%}Z3EOmWN>X?@Rd-I@#o;twqX3z&NGVO zOtcf90I#Kko)N)05)U9Zon9!t+XcGW0*O*ln*5Xyok}cdF_yr39P|8ieJ6nRLC%h6 zox=#AfZ5q=)y~x(ZbwahI{;RM8e(1sKfL+M zb6AY|5G7HZ0sL?X3l>#nJIqK`9@iy3u5wt!d`^foa$0o%OzU1f5anV-A?As9C7_3( zrEbmJ!5A%k3d~IL)YfDHr5t@ZTApxRLQ@ov4+e&J}owzl?DN_xx{89xBY|GKTEZ<{S?7B1x* zyd6_RXNT1gVZ>B|+DYl(diZmWnjj2*2ukc9T&vQDpU8*jbtmLaMT={P-<>6Ex>PYb za_l^ns7Fl}TFP|woE*(q=9b*-IPCs`fRRQOLw{$&LMe+BcoUp%$%aW1vK!bM-rBev zs=d2SG%WYJ?(vTDY-=YsrMZtzL%q}Kf_E_WIXXr`k21xe6s>@a02gjS!s#xkR^+O* z02`W6p~}p>z@%4By<8hv%8RhBCz8f#5x5G$EjV{dyRkCrqgemBl7xT*nw(_-!sMrd zAo>=xG(4&{fR#>?sk0RjONr@(cCzphQ`qXw{i+< zD%A=L&q@YWjs42PV@N>{BIFQBkcGprxz_ew=lpFgzoZ9Rs=Y5O1m!E=yQ`Ow6)MEz zFl^4Q##W8-3!&G@)VPhiaB0lGc%J(DeAkTeHF$|Ff!opiUtp^~cOePhCS+Q$ zz>PnjzkrEYLL}9OKh!^7gcSG)#Uj%)6r-h*H8OkFDpn$ z7k+Q-Z)ciDtujhB%th!!a?unqh%i;lD8EGQG z(;PZtQk&hqLkiZtzq|BNw_A?+Wr*M7wMkD~+ic@0K6QsdVXV=ot*^PKwIYhGIVUii zWJu|Hs3Ajx?9p7ItsZwKl9CIoJ{)N3Rm7?d_t+*^B>nD zb2_05(V#GlV5s24u;ps17|NKollhseTF5>aa@EpeqJ@hsg$>W}>%%Tp-Zc&-E%YzW zuP!Gi2^o>UAp!q#0fvXOlH?N;SF?A{i8x&MeMg5uqpd6V*=lOEN4J70KYT}s>2;eG z*K0aBvTwP{bwcNZ`*K!Eq2^45&2^@chN)31P2o=N(uaU*2_eV%xH8v zJlZ_N4}_imBDn93`ZHUxL3iI*U|XCfw-s%gIW)#K7%hC7Y zqY@<+X!T|icWj7gI42l}-E(Dce+aMZa0ksI4q+Jf&N|#2xXlr-jQ;lFgPnj!9&e`Z z>6EDx7xFM(2dl;;?TkpkPXjJu)?gj7gpY$CGpg-Jx|4s$R*@9qB=L9FUpC_6N0c}k z4z@o{PkGjNGKGg>-QJkm{BUvasR6xa3XD<3Yo)&RF4)&F!bgJph!1Z#(QI&kPD6aJ zpNjC71+hPr_=mZOIcSf((%*twxrLx6pYkn767?tbU;J=n;~ z7qv50X`axIo0ZerqR=KvX_ZV5X06>8IBI?sOs96rxbtS|U_JL{PqbQq_k5x#^VTT6 zuyUU>%^iC!ojcPKh3|2Yq~E-B*G|MG>_H|wcl9~DJH39yKfA_~A{#j-$2KM3s>}+J zm{JzmsjfA|#M5Y@HN5-TH>P{Xpz~`6Y7j_W@>K66KiL~)8zpT-QuscZ+eAl&K8O8t z&2vDQNv(GqPr&wsDQUaMz3lb*_U(sPNN_umEhs%ZzLtbJ`aTckAN_wlJbZzj`qVn; z1r`w+^sQ72)g9VzovwwRZmE&{5`8!%OG( zz7vPB;YT|bd&Y>{?(gVJqJwvP@VB>_!$Pw6H(m-YJ!`r7`9R96|J>^PKf?M~W^Ke@ zRWMl_LjJ`4DE6^4L=#BSiXPJNUS4>f!i9!Wc0-wW3yW|29od6Xq}Y9DUgZg4grmg_ z%OfZYc9vxb!oYuk)5)L`L_tUhLsnajwN|#Cyuv(w(%-lwP3S;AeM1*Zi5|SF6e3V} z?I+E);pSp7|Az9dKws4OEx_oOVyjFcX3Dasy8{$)C8%;L|_Y8 z$!i3nuEPdBTfZvo;i5k{>f<&NJx%VbB?#A}9MC*b=roOyNo&JR_36kn zIp9wov3EI{1QZ?zPnKF|Qlfv+i0q&IsfN_~R$dmyYf z4X@U>Zx=fYdxo~ZEQDMpG_UQwaQ?p(?4Jm98*RLsK4JqY9KVAZB59>}Dz~(C)kB zPA0XRy`9|8QSBR=v<28f<9VUhSJy=hUdK|K-PBqny(`0TQdft3yr-9fsKZI$vIjsH z34y_i;hmF<6BBB@MR&BmvJP0^5#jZV+R%kFKe@EkNZ+p4N~c>|NyEFmJ8~Rt zYwQCfQ_(%Mhc%JXB4UzXWGsKTO+B}ZUIqQ)>%4Dt91}K^5<78r-kYc;@M#PFcra6I z+ozaxca^7gvb=kFcWw@Tk>e0imE6N`;yUdiZ6L8vB=gk4qT}PcaX5u+QBhymWMU~2 zV7hXUbC{n5e?6p}m#Ss$T-&ukd1uG-=tNAyJC#CR9S7lch}j9eCqqWoH?m@ywf}Q~ zx!*iEEk`Io-g2!PzLigXb7|RBz*X>3*z3qk+K0#z-9)m>Zk*e^0dNxT#XZd`2(>)UU>$6iC zc3+3^BR5rUSA25{e-sT%ru(?pv+UDiMpq3wNW(B<8{Lir` z=R6jI+pmrm5n`wbU|b$40coCVGi_ATjf^LYj>6TWHSAAg#&7XB`gyzrdAA2-55=7M z^<&MTvJ_>me@ODX?}>YC;{$-c^EeCgUY1N$yht(F!|_F|ySzJ9kY#3g2Xszo9U*Z& z^W*r)a3McqUQ6d|P>|VAUe@hbyi()~UWK7cEG~av8p>>pwfoU1I;6pW&8nf)-HOz} zV7+<*=2WQc4A(xpv8sGhY=I>)2lacevH^H{fDfjxJ&dJ|k`H~lBBTvM+0KbZ0;~9j z=}7@E1rJ$n$Y{@`$qput9MRD4_?MZVeMJC9y^PmQEDAOf9KpMf5=ISbyhmz@v?fnJ zMP+uo<6ZB3_q_kA^y3@_DsXxl_luvXA zk_8}9UL}oI=8Lx47vn7GE&4Dj&Z~7@VE8Im=wyPjE%EL=DJ`qda239y3(NaG4&kk^ zRjz!bqd#TnG3XiZU!^{NyZ1lVkl_SV24oDySk~2S(lFX)Yq{$)vuW)hSKltaYzSY# zLz>{MLV*2BE$yn0W6p2nEn_y8!C6c8wF&NTb7R`*BKu z*XmAQ4j62}iYk&ErXLtxgAlLIA{wLtIDw>|slwazd!XyPVz-_gtpe9Lw|@B^dcp3HkL@bQ%ZqOqlmS@;6u3uM;}mj zM>zkJ_wN2W7FY1R)gty#gqrO`ULK)teRuLKnMB6)d;HjRLxcE@S!cTiPs*L0f@H1C zSHU_p+EGAj1um23^wOxUU!E(TGl_mX)$KHCkM^Gc{Yy;2mDTeQxlm~HSR7iImA7C% z&M4c)O}YfP5Ewiuvv{BdWlKc53=`Q<%%jTvG4dt=Yv$Gbg>vtX!~&8W2!n70q%n5x zP042`(TpZiwP+cy zJIUeAsM8rQ(-XW)e9OV>OW0a?Zb$WS^N*7XFd1Z;mBB-O5*<%YynXsS?oe?gNBFq> zbe-jVRi;XRR47dJ>;T&?PJ@&7$sEvpXtaJ~MgdPmDB1UjF2;F3uU6qLkR?DQ-7G{h zyZvi~0`9*LS&GF{s!tY_fZEk$R!|%#576kXysWdFGcq^!Y_{arnsWHobHJts$eQ*a zJ9C(6Ydl8^8?7aFFP1y0bq%c-7_=_9!d14TJFQvb5b1A+Dfuq+ZMM2&dFDl&kE{8f zHT_^@D~;27kWcFewtSb5Auj0aHI_st)puE;P?k(uA_SQtfX#vHOQ4WRsi%<7Qp}ec z!e4J)LM4?=vYIM=?URL3R4$Z!ep8)E+`DCy-e}cz^A^1NCr_eWuPyzg{i~Rd6{4mL z9Sqf_Nh4r_9$0Avj5cIEFj(9Xm`vl7ksO(gQBo6OZYTlm_>57bpI1W|lW@DjEfN?x zoZt6$&lB2>~Iq(7;zfE~;aZ;d3F)rh_eyNTVQL>ODd{li_Y$pnM z!nitCIk3}4%z}s|WLLSs_(J+Pd_j_?C^cj~zF-v(z)bE4myjv2xEwkTjen7H6v_BDTmKg~G8c%qIGqXc3zL!DSK)71S>$n!t^Po2k|?r8E}YEgC=)me-FgNnS>6jyd3{(2h5cjcq{SNNYFqld`%xJHtxy+#-mR1LPs@xDVh4&7U&j}ubBhh2NWf%DELXiri z!6NkJuCX~Y`rYG^3PQ;wbPsN^xx7OoM(KEpNocaG?*R)X&Qsv5FYU10qfT=YLS;>B ztZm4luKosG4Fd9gPoYHZJw}a6m|C-UIIc7H$;<{Z_A}&-3?9B;I?>p#pL)#|Ud-eD z&I*IqKHuiA)ls7qY8vaSv7Kr(c~sI85bT{MbF|MYxqO?i>dkt`yT@r+BNpD?uWxd? z1pVmoM4n?zve8Y5a=cs?#@`v`}0Y5CPgxXd9ZPXDG^Ss1?;&>tIH$x0Pt!}V_vY+l- zmit#pkA>bJ2Uh0m!JK8QZV2o{WIFkduq>t}EJnCWQyhP3FYI5cGmCn{`wa_QpU zbsCnPtl=_OJ^rltaQy&5!IO>tvvPyil?ok;i*2?f=7LwZ*P>h3DM{vdZ1SNIb-qb|CiVFH4>jIIU`JLCPoZ3pfbPT zs&89BhV=5eZhc(e{VXK2IWn}2hmjSxM3MNFF=|2o5m9QbAF;=Z{|wnJrMR{vo)#YG zt^W6Y1*3SvKxB5$CB@BpE!XXeXXUd=5U$LZ-F+vg?E)pSN}BD_gVM$wMTifA zt4ejBksrK;zuUVJ!@#eJo5J(XdT&dH7v*|Or)E8my1aESSN1ZvyM7mtqyU9WuP-Gz zH2j6#_?c*AD8tkE&JWM~dtYZS0Wert;TBWGTJ`nkPvg;AH~|aH?~U_#uPAa|=FSlC z(%=Dryps?!p>mq&LXg`D+;{3Kj7xq_190bi?xKcn3N_SJF!1o43Zdc;Rl?2Mkad4b zq?+w6BC4{6wD%YvIaZm{sB31?9$YV)pKz6?IvrjU68wKOU1d<5ZIHw*I0TpAB)AjY zB{&3kcXxLPZo!=dhs7bdySuyV;x6}oS9gCY*xD+dnd#~2nT~r7yo^PLAGUY-*Xf)J zdx%5E2p>4F74cl^=Fc4?L4i|uY4CxMfQU%ech>6b>M7o@*`!&@R>M<X1tuMfpH1g-zPlhen8VV8oMG1Mj$uGdc!#XPh?j$KULTnjUd7fDuN+Esf$8&130ioUF+ zquPXLLKE2=Y13q6Hyd6u#|yQiv6nWH}S{G1MXnhMtJ zyE;~ZI#^We+?o0otI$~#_N+eh{A_=^)@gz)ZjL}!G3cQs_dcW`_jz@g7~B30Rac7J z%NaZID^dG*%xo$GUkj|;ZHB3EP85w!fofcmZZ8w!L4i&vXPwSEt3UZLW}>4h?4C!E z@szhpO6=bEh(u3MznQd;FN9LQX5D|Kjl4%bFch`8-*Lv~dmz8Y|1f8kp~hl~(pU26-SIc8&~x+A}y&}KoCaztD`5Iz>$@WmCc zYLD1)@N&~GW=rFr9jA~)cl_W7c}9C|>76Yv4;9OErFH2XG7AO@DVhg;4nL(R&`b~_ zg-S$q;_PmTE06URZpDFP?{Q@AacXGTW^uUEJ^4OLh}&$hQ?t5#Byd;b`1O_;ZvCaV znl8_p^;LlII`$=U^c`kDuWiEklm<$u4xgAUqcG zfq*3x$-Y$R3%9q{!z^1xXe@v;OSsS96I$N9;S(yUR!}epGXW+LX?Qo#9wtN(k|%E} zx)lY?I`8|4mGWjAbq=O)EtL6ELqmBN)`0KOf?nGMkVg^KHm>u_6RK}tg)F~keGOTt zoO4c%`jKA`)$s^9W;Va9ZrJNOpi`|x+5@ZZo@M&?d2u{anemrFbZg z@ENzOYp$6yLr=SJ90&w^E}>@#+`v4=@fYT( zhO>>OIZd;)&+@lGpzTIO9;%E3!qCjhrQ-Z zoN%F_EVmZPCe5ta6OA|p@9LA<7Yhp?G2>V6(y}Y`JZsDh{!#0RoO0pldzS?z(HwH& zLX_G%G6rHyo~}{`hm+`UB)~51YNJ6zndvzyZOO{a%N)L{j@1}6T|Tr zYD{v9IfTRO6F#u#<-;qKcw=$__cz6FG6}YYN74Cs87{~g8!1gOJJIe5wp6r@$~OJ` zF4ihR^6GG>6Tm-!TKr(JCtU>3eTWG*!i@hFr!@^$xvYu#V$^B>h9twHVIRP0mk>PW~y)0v3rz zvf9nNK4jk#dCq0|3$YnOr7Sn><+qoE+eoJP(I^X^GftO-%e)nyT&TKcef=JZIG2WR zxVX;Gl@E#)C!vzpI1=Y>|Mhg(znWj<<0-MjgGVZCY2}M2_oaYV``^74rAl(HYRcDl zBOWS!-(97RxW^MJW)&sjVVc}OpU`MtSIUKzDGGY4vPJe=?r2fNAI|194N_!&e0{n! zHqv|JQr4iA!c2)8Fv{+Pc$v@zJ$?SK;d}f8(^gaS?26(!Zwc@zqw)?0Y*{?{mAjfv zkH5X7S<>4>ECdT~kD(s7jivA*ODVQ?tiKc>R%pk*97x8o{9N6qAc99v{WwBNATN7* zCF?T{+~OmHlwRjeT56!ktaGb1x%1+*TNl=HVhSs~qoCY_66BQvF#ZYPI;~y2%3--( zNeQ=Rj@?rWWX9qU&Lq4?E{f3)(_YQbOM4{&74}4bHz@4BeMTUFes64Buqi!EKKzQ~ zU5-y?h(7zncY_kgWqIqjHG1;ZW|cdM-oD$Ja{vB>u7PM#h8rI)W^4C3m&X#Q>#)w? z0aBIzBJ>-PccbQdZZ|~IfJi4v8AbIPlKv~QcieM&{TFhflTXWPL2q+p6yO5aoy z)Em{mtje&2G?DXtt>(7ougz%FR8c%rGtrJv=%6yBth7th4~9g|Dx{e^kMA{}mL|gn z*L)ep=yT)1O3%{x^Om12F@4pnb4!+xHQqD3L>ZVyhPKe0llLBvY=)drgO0Clz5MI% zuo$JB8tic`6p{F#L&|3ZCV{X&M|fdr_Z$mM>ny1==g!QW(hmIJ;!L02z35m&GY=KB z8UoU~7qOC95r-99@5kzr@YDLin8BMD`9kp!Pd9B0 zr9Tua-R>`b8MR_TGn`ujF%;!ofdANn1tJCBl-EaEIIrfl4cxeCy*@<8pl6x;%E3xf zKLl@UYPe&G?$nYde;72{4*xaUun&>XDVb0bgURQTEj(JrT~k^X`EQD)Hz$63tLNV` zBkT=bxF{F5(R$HGC9)$mG#MDA6W7SUI1(_Mzwz%Vy$YkMz0V>XYvCP)G`NcKd z#UGv5^}MVWHg1y44Gh^^Ax~;QedM**bnrvpxz;4#w$CSfWXq>Ts-^!kdpu#;>XjY) zTOi_A{!C<`$VSb?#%One7@1@$HSlXY%TcHnIRFFa&z}2*hY=s3l#0V|U*v>sL*Lc_ zf63HMzx}GF_5O3N4Xs==P9#wC9nby6X0dcs~rK6-5yS_KvPS9^Auh6X!MNxTkSmXvk~ibgR}a(+axq4)x8z->IBX76_1x3OX5@EOHqm_ zU6u<$d9mf8b1OAA0*q^K)gS3=S4G%JWsv-A`j>pJzj~rWQHtR?&+E39-;0&HkY_54A-sR&l!$mju==PP*g5y zBw5o~#QF_?MV4!eiw|fzVV-UypP8e+*hF0f&qkcl@6IL0(T=5w&u%xN&mXcjHR;=8 zgPELtKaHi#Vbg!TxXC}4j8(q9lU3!PLLljYVyhiXM-wbE+PC`F1cpEx>@1*V3|wC8 zBuxxwsA0*e2Wst#7Fkx;Hzr}h)~z#x z2tg+JRpZTdw!F%2KjDe-Uw8AT2vkr!8OyDA+7)oBvbGOZscXFwQ{0j@v8AsF0^#By z+Vb{vWLL9&ii|Xv8E3X&|EXbk!^4zGRjEs${kEjHnxSeLuT2g6{@;r=;tWS8A)6R` zV>3W(FkpGfN&iE;(tprAV8YJI&XE4z?p~whOeYomA z1^e0!%m2R?V4AMReCpgY?n8v!FeIm!E<5YHhU-Kt6tBMv^CL^mXtdCj;-eVMOT@p+ z=)KE#eD(7$)*B?sRRK$}Wdk+V&}YjPk)xHoU3=b!htwLa2tOj%ZNHJ&K0EipVy2Xb z?u`6k4lkMDDaJx24h{Z(;TBfZ-QTd4!6A5<9{&%r zzTF;IdA@OqN5x_FP3=!yGzIHE)167SkYZ;Djr8lZTXhHhsE^tdw^{-ms@^N2A z`*>Acxt0NE{encIy3Nv&>Xb8IH{w`wW}`*MRFhrI;naIc*V)+UAD;=G9{W~U2*7~O zsegZ|39LA&=Z03h0sG&mgPXfJp)SW-;j(4n85Hi^C^~Fet=VwQF+Y9Q0!=ILs(mK? zP{O560vOb~TQBxuurNPy{<4IFf9T4>k9WrQdm;h-gOkvKp-~W-NPq}(f^b~UttJ|? zS!nX3D8VOV%%uX(mL*AZr@bQsH!aYOP_LvHk6n&j?))8*8q)B2>9glL+BF*8D4cN_ zX^fq5DBy~w>a>f!!1_RyCZLmK%Ub%eJ~|xl5MhTEq}>9za0WS9!!~Vt(2@FHA$mW6 z|2Pi{esDC1*BwNYu>43`s8V2cU;Dsau+B#(ORb%g(Oo55E1YGTyI=W$?OT=U|e zHTUfujbO@lUnPK&4$@OWu6S(X?KyfhOOKpIN}=8fFaPhQxiWhq4hNE_LKbNC?4t#3 zK)`;lKB3|EQ$*<){f?0B)jNea#r9dNA>3Y2QQRo6%dEGfeNTR|Lg~0FkYZU+NSL4~0T{~49jL`XEHrXvIfhunR zC-=8@!49^~4cX-YcgywJIUCk&a5zc8*K&*>9?JQtVi_p2A&koHfs1@bc!i{X(OId7 z{6k*)LtR{=e*y66Ern6ThjM+XtY~b=ifN z+bIEJ7A(w`u8>mZ7s931PdJm|j=S5n5tmQwv{eodG2xYsXcIg zRpcM|63lbRkf8w9lh$#bpZB?*H>ZE~&5s-M0RKUAnBP}-y467K1TIs0cyKSuwq^JY zzj@qME)K7yNZX8RT(sZVc|FKNb!|H^JKxhURA~2hQ-mKUFC_&+&!jsHdqAKP@pRGI zw;wDPb*|q)=<>S58Vo$JrL%#}W3#y>L|S3&I7sCv0hotJflthe!5;grq`^& z4loQ3vjX?Pzu)gs$hO%)(cD}=y4}0Cy^Lu0HAYst`T(rPzJ6wZFw0_GhR9AI+u1wa*jw7Y&Jf!C>RT-pqH@J_;Au^Ty}!!5+_Gr}7@;BzZjb z6`qu{1y4p*y(W!SR1Vc9b@F*|Rkh!K(;rWhcLH}17&d1({u!wCp0pET$j`W4IbfN3 zg}>SGx}H8HodwzQ7RhFvB`uP5K%;HGZ~I< ztI+vKEcnW_y&=B62MIsO&9sN^B_Hb%!vv`IPZ7|q=w_QitMDpJdB8-VBRxCgx( zG4bx$ZXoAR?U8>;5}KWp2m%v0aggNI5JySw+M5>dCvIry>4aRO%b>ASOM0rHkY&P6 z1dCBLY^~Z)ymPt0Aq~qN38WZ?4x1(xN)5(!1zokWLrnJ+mG}nxuYU=PtKY#)2><= zhfViY&E6pcm-FA&cfJoeQ!(>SVk=**LiK=hU;<2P3`n@bYO6Z#WBy?M*Cds|cAm;r zaa0}h)IU?p ze@5RZUBX7Gd(ZHzO)2Uv(fE@L`nVrWV;kcP`u7d&e_>m%gh>(+QKPf7W^l!9XCUtW zI}IzJlEx(U<APd`KAauoNM6t! zYQ#?BE!b%ce(*oQthc(9D`}t}`1-#DyL-NLf@fIrq1Rj84h3g74uNlMx;}8{nvuD( zkt-nGZizV?Qc^+-SA_Dnq$I6phkQm^`6`@Jnh)472VDFp)wP&!j_luqYc{boQdnlzHq0GcriZc6>b zq~bq6ZiMEsQzimz%Nu%Xi=nI9ko`UoSr?yiBj3KC=rqF6ZdOKV?R8t$+GFK-w3 zGdq*`@S=6R;y0eD-TWz&ui;kFqLT9Fj~v;~CfyoLt~R3cxutt;8}QehEWrUFh{MmE zwNj+Q#E`#SRvk%Uzs$s_b!BI#{R_uk?Ps?0zq|PCsqfhNgqs;(P?b#<;{92%UQzKq z>45#tr#P=OFCS{#_lY#?FrRjJw+AFc+{5SsB2D202qXJbzB+ zUr17(=buPKjVU>+lejC>*JX;YK4W+tL0Td{K}n4|>>8}o!pFjJfQf?4mayxo5QdXwz%OO zv#a;fbg#s96_-Al_X}q(o3SWE{@Cl8wF?h%Z#b&?i7gl4sQ$FKZVBAiKkE83$v3f` zMd4zR+m&)AYu9q%b+ zJ3KnMd#cF>IBu@$9zyD~dl7wJK-zenT#Y9FK0tMO>cBifWTB&OAVZjcIKo(EU;``^K-ZF(Va)EDV18n;-0Wvdd~dXjb>0&Wv1l>gNkyx0yH@{`nWIU z-_zYOTi4}nWX>7C%BVB$&K=KY)B`zbs!j7os}p`>7i|Dk|6c0zx?g;@+?0Bu+gd~A zO=r8iQtJ&S85yNRKxwG99Hj=LTbJD3)-9R@u!!04izzHmah`49D%sU)Ao?>s^oy+5 z%9k|5eNoRDDGtF=^NWR)PoF`Yi(E&p69>;9KCvSqd46?4zm?0wp>n^k8`ac7o9vHNEUnepb^?{IMUQ#&kovMdE zSnx%NIEUrohwbEt7IJxR=tEBXDf1~nz0b;5f;sc$c3`Tku-xUTjDs6V{z+`Zi4joP z#HQCp8i1PR_Ory9dEKMieYlqcCb(^1$-1uG>D!FNIT-662!OUz_^W>C+Y*GUCU}s&L$n?5k91>X z7axnPO}6`hF~H0pZFiDpa6Z8xN<$}j{!Ne7E30U8TQsc3`GGMY@1RICsYqyjBR9V6 zO-?kLy7OscZr^$5`-f{yXGZO{(>!O=LL=DBe==5=7sBBiD{vRT-hMIXU_4{-*c-h< zSDN1&gM>iB>|RY(XCq@+Zc%TtbJHieayGjfB{_fwNB} zMh$|Hnln6G*h2O&!PB;xf`<5r%86t=NVV?5c7Yii#o=1^FMbYQE1IfIeK6w%^CbyE z`uQk9J zl4obFLQhjVu4A&aiynAY7Erd~#2BB37e&f10#KdY zMP&((JTdyv0vH5)1IJIU7<2B%w&T5EU8I$mT$ky#9w7yLY9%|#%#My76V=4=&xS(o z=v2G81<|f==C9S|11Hv14Lui-X`5OW3y|WQKhMC4yxdQ*;y5|4Ti-9#FuyBXY{;(n zo9`27v`=(a(}V0{NdIfdB~ghFl2hN;r_2!EB>}8A+}@Hf*Uom@WMk|(K)ql)LEY|e z4_A2GZG>9djseUO{{>X1TI**9tyOPZOei&i*JAsPwhJOst*&txG{;0A_)3c6jY`95Z8^R+`ReGn31F`VF4S7tJ1UKYMhflqS#jZ7Yg?I0gH? z<}9hznAmR)1d75Rp>GBX!Q6D@4vqJlur@dTyyq%1I4IJ}svLcpWdt!m4BkL(jy1aZ z!U69#nuxPNJ{N29sVeGi9x!DHrj*`0$e#P_e|g{yI>aYjbe`;eXvWSo=!AD|DA#uq z{4atErj5V*+%g9%!_U-tlc#H@171qXYD)4MgZCHRODuMCc<=9E+^^ieNYaTzD>>tz z6!>wc)D)1-rjCU{JUdgGr2>l)#;LGDH*u-6<4H5KTlJzZ9S(q8$dhF6>~{BWnOBH- zZ{BFWvHcJ~^5(}PoflqZ@Wkr!VksRmKI-(e6?^#Rc`?;d8U$_(OeP9WrE(JzJ02N7 zz6n07yEpd^UzYE9rqDmw`-6Z#BcOcWt;lXW{!(q;M;kH|5=DLQ$WZ}6xPDp%;6S_y z;gC{ogi7;3&0-3hBLj6rb3?Mvr<^S#i%ne>w2`F3jTcaL3clb9XG&&u&}M*+RwN@I zQ9%5y%MZbi+7rRlXWX<<2nzIn!z(qZ6obzAQ#1)UK<`=qy&WRfrG*U-UZ*anse$`v%OF8=>T%~>Lm1gLXGV(b7{{0`k6~h5Cj6sml2HQsN z8zEPU?O(&D5q2fI(~XuIN3HexTJC~`evJZR%aHCobAuoD&kx7lMU}i7IW_4VgiVPl zYf<@@!)c-+LqI6M6VYxE=sNHkvDt6?L`zUSti7wp{MqTshkgZMjLq0uU%(!=dmZFN z+FdyPC?7zBvfIx5e%SPg0}ug*8RGsx=USPg(@^c$EEAs$u%$}c_ReTl$4jti)M!w* z`t>UV88y#9vft$;8AOIRLbhv7B{g8}6|wGhxk`Rlu~9vx2(jV0FULdWa{G0&URnh( zvQaBf4hn+G;Ba282Y0;_ydatRkdcwmMv4F*Pe$@#pPZ7KpX#DPj|0KjdZbL^eTe~T zdAE9hdhUFB%tnKj-~!@7dkhCC3H?<41ksUg?!3*PC)7JC%+I;wiTq)SdyqCZQhMnr$10jyW>4HnbWyvHS~eVuM6>a@U!A)_cfd3&NNmp)1O)chGx$*BM_z)NU7+^<}+vIbdy!+ z%_E{Z>D^c6PcTqi{e^`lHY4Q5Dmb>v=M0Q{hVj#7@ z+dPrDF~MsWY86$WtNFmK`h^1)*yNA{f>7ag^R~6EXv&%Ebr@%rXy2HoJOiI(%Rv&y}kvHgzLi+ulF z6Mz!pCPGCe5eV4X?tZ+ii_>T&3-;%`fpSz$D4OqUij!2r<}|mP{%gP5oPAVcq19+H z+$@n|%4up>WDgc7Lpk4pT=Kt!0Cx0T_*v^3uSa;RfVte!lmKMFxORh^-Nc%Xv3_@S zm2nV(W$X7*-J<%6-!jyC15I_E&V?wl(iCHuBci~qNC0a9%`##2CcuvU<8-8r>?vZn zU;&U>qur>}Drw(Z7DG~1*4WA&zRfjTxZq@_jI<@zHrB1EnOQa_~^3& zOONTbo3l*mQrWnD(G61+JKd0gb&8msyGxgW{fl5&aQ8@6-X6P)bTA^C=q3v~CY+p+ zVNW}=OCkYettqUmKQZe0j6|dbc~{vAXLwn`b)|nGXnp(E^6`R~mbRpJv^SPoZmaCd z;XK&@H87Npj6#KBw(NT}eo5r=r$sgKc&U~5JGi5HrFk9^*{RxyXXYowPs{`m+2QAy zLy7kd?mXzPob`ZoZF;9g-V?FzL}v3YO`&CHUjDAU3`S*f;z)!6loc71sYHZYKkiZa z`da9nUhoL!uEsf2@KE#dx+hRzXj-E6`$4P6@iz9yK5-|*uivaUXa8L;KPs{haBWZ^ z^<%h^M`OqFKi&b61$Mz_j>mTAZJ=a1lgDjhtzPfj8f&C1r6d(25iapCY@AM_ijwV9 zBOr$R(ru-h8@@nQt0Ru)n0|`wsj}~?Wy<>pv6GZHXrOAKcg^wo9;2-Rx){~gxEC-Q zU|ZKhjPeWBoSm;rkkmz9_b&CMaWcHKV%9d8`XYF$*ku*0>pkZtGrDC0x%c_;wP}B( z%9cSpn`Y7BBYx|Dx5EDO;eIA)e8zfxnmTrIIbLYE4?>9^HZif#VvT#hqYf*4Fn>!q zZ@b!7r`K%A`9a3I$m@JKZVqT6%~hK!+On!Tv!xv!BNTWA$KOI#ttYNu3qSpG=J3C> zoF%PMa(EwJ$&mvH8AcM01sW!Dz90bY&}(+Uw61?NS2s+P2|y+d_hm$L01q^lh`(po z-yf;G0n@thb3n4S+V!w{ZbC|8BxU!BtF=yCk|zy5_m`MQw=19LyTh!Zaq$#WpfBj% zDU3>YnxFUQ)_OA0Be}sDcjxa&zhiLcyTnB$+d9+dxE{P!A|KRW?cs6vfS z@b#6Vs^bMt&Nw4)_V_=h+0AzKYWQ}NJ0mH0-R`hFUEjH=ihHn6x~4ltuWB)upW8d4lb|wzs;*y&bFYS@!!$v`*Q^YEOUZ>{A46Y`9EM3iHP>e zJsIV|dyD@)7Lhopwp{L%v#%k-_^Isk?P(IS5V()hMAK{?pybe`%Ik>^(TUiY#)WH_mdPk$tP7#4e>-Gw*x6tz(G zCj+Uh*Un>P!>bjSS_qx4U%AHvmdvdBu4_~-P4KN3IBgre(9@tD7oh4M+B`n< z{@Xm`H?CV<-!vB&PUx~@uP&EE2Kzm*fyXmO!-&1Bd5KVLO2N;wM9@;z=eQaVRKIbb zkft(RbD&;0d$Ja{f5!5hZ3+ag6PCLjl*(y+4#fc6 z+@hUiZXQLJQidE%AAd_%2 zAKg#fc`*HbDp$yS{}mH-Gual}X~>z0&tvgx^Hr%=vexWPi>wL^Ekue z=GZO%?3335GH~~uOWzxMxc%k&;Klyt{spXPS1F`~y#SiLjDYgJ-X3f|xdwz9p{X8c z%QVe4BQSu88-SvQ)n#`8GK#0%(Yw}rlO*S9a8tL+xWM>mE}sJ2c8zoV^`g$|EG+lsK;d$8U)}?;dvM8f73)mx*d56Qnqh7nM>!jYl-oU^82*Z zDPLUf>H~=27eB6$;`naxu!_oTY23iwg1LTyK=3}lF%o8G}(?>8xQ{6OUc zShLH$-ZLsUjjw9KOpB z)y~jM#2xZHF%AsEr;W_7M!&3!c}{9s4f~zAeI-tBS@8_!92zp{dyhqiRuJ*E^Gc9o z@H!q4Zcm`vc|q(Y~H1>q41PUFLZ=cm7H@tI4?noqiUjrFxiN6mjj*KSs^vtnP z^?7$PfCMIla<8A$O6rQhsX z$|bErL(-qtz1lbC6#!#^sR$Jg21qM< zt%{t6He;HJd_YhyOjZ8LP@_>WKmIaYWd??|7hP^=Qwsl?OBs7 z5pxwpxytqqCz%f~{Rxp4F46vHHC9g38n0b7WH0_;<>&YfLB!n;MVR+aJ>QsRXFoH* z#rAGtFl1Qe2%U5?v#LYqumw%<9cJ6mkndw`R^Ys1vpV6|FNBswGOu@?xCfDp&gc5$ zxFy<0V_-B{cWwHBid^}8g!h3n}L@I_5N&Arlem4pag^6q~Liwu$pOr zBa#`;*^}_e$;o&z5&-9~(D8_jVYtnAHT7XRGRMi)z2(47`|WFyr7V`57r?&&yVsQT z^wA;rS9nmbAFq-EH0IX+0Hl3EBl*J0ni%4C1-W@+&;kEG;Lmu_0CI!S@jDkAh{YRk zci+%0zCT>E>_4u-ANzZ3b{n&+O!-LKck?mu?v7{4b=1QRu{OKE^|p3HHd=dHRWUSF z8$Em6ckJTi(Ht5)+5>I(aJK7E?qyfqHCHS=(>*x(m~sQMM+{d=6;2g%jXFu9F?g?X zpdX}T#rt8{jY<9l1F1|t(mOw&q zph6UZX$J-^mRG zldYZj!*3T}kb1f6s>l$Qx~-EFweZQC`31RYcwYBikHnZ+8TH8(JC}8lXR>JzbVfAq zC9|)UnGY&HcfCA~%KOwleoXDL&kCu0_R3NS%iuPN%PBsHu@^K&ui^P6Z7rXZOGXI_ z*}EC4p(i&MxER?BI{JR9pA2nt4@k`yt`}gvO_!v-5?V}sUwqL>)(#H)b@_EB`vJqo zqqiX;1?JO2nG>$JT+LhOCWEmHA06G+SHU?3fBb-~O*?8@OON6w1uAS|YgnTC20DM} zF`Q3snZ%jvVapW=tM{mEsNv92^I`K={o4-znn=!==>{v*2yw<)Qui8GZCtqbtNM-l z&Fa3#c?)RMt5r-)4o1mzm9eBp<8h#=!KBL$!GS+lhy(@2yQnoJ2A~)B@#~G5ed26w zdR3!raqp`hf4jNb!USvOlh7to!eSHAjDOmq#Qnj{M=(q z*NwH!GsexFJHb8_5gF^-Eu#W)Ef5k60rMY$(B(dZl`W&er~~s5*7*x4P$nH7Nd?M?2T;5d^DTq zQ3I`x)gy+=66jnp{*iHVE*yGigV|_{f`nJtWXF_M zw8S3;eQ2_&_rjvTZmnlI%9^UfJ>^W>_nT`2Pv*JsUE8&+YW|%Wnf7hNH2&&~)abxp z#A(3&ij6LCkQGinB%SO`%4rm2B~R>`)@@>l--K>|e;9B*x8qgw>x?X;P^H&#Fsz!V zL(lrb&e1eAHzT5BgG>e)yZ8*Fjve~#StM$5FTIz}V1fp!PqnBUAymb1gvj*c-vKqi zJulVhQbjc!LU9sSti#ALY}|!~_eLW!`|{zTFteiN_>-#H1?GL>wujSUCv+nYGq(Ma zE|^n^uxGm3WpfilQ1d;nw3p4XI2QT%2cvZ=KLD(uv8b#TxQH6LDAHo4m4*^>7?dhF zT$i-8equBX;&#)Q-aB1b?@cK>&^}03(ai-Tn7z~(FA);1J88zoiXeUJH{{%PkfLD^ z^8B)uP{Y`Lnz#PdYc(rShq1Zb*F8e_&b@X0#*Qx?_5K8Hg@Gt{*f&|Klc!T}4>7#@;>Ob9Y%r#mF<${FBa6wr&cD2H8 z{%VrXWq&;UElN!Nyd~jL;nT7O{l&$!*U7z}9x_Wxcvi$Cklb2{5hH>>tNvDBRQxTN z=BYA(;ZhJ>M>+m!b8~+prFS99;{JP8lJoqo6jOKkR|Q{rXF4Jtm_L%ec{)q&lAix+ zfdc@_+x40`wQ(HXD-0vSiLNN|DagXzQ)c&!tK!Oh*r+N4L%jFBfvoecGkU23Oto7k zU-oK+9FVYZ2zmfTdm1Y!z|@+v+NED>!ER-u;_EIRuNNL%Z|T?cs<)>YV1@=CVCk%I zh~~aF(f^65px5Sr$=^52UGQ-#n~Q}kP;kQ)zRt`eMFoXDHzg*M^XArhvc_=M>};a= zhLIbrbJ4NuRz;<^;Pb(s2$Mu3H^*>z_KDHxwj|;8ZaW<6+oHa|_lQudpRKL!>r+DK z<*<1!^hiLf!I;HG+qqg8it7D$D@)t`IDfAI)$+xunH3}(A%4iXmfRjD1|f!!$CAh>HL?;1PSu!QmcjC|M4&#dI49w&fyS zKgC4DON2B(>OPvUFW;&(QGqQ|>+%yK7wUtbt}x+37PQDCVcVYZf*J`U47SykFi1Ds zho83Zm<-RD_v?xKr%s}`(zqa5u?!XPD^|`KI_Hfv_`e?M4JfWkj&lG!UNH`h7V~IA z!q9e3@@s{vWZUTC+_Y_XMbCx@vWzL!7S_XAeky4+OHnbIq?iZIeWF1l@%PB#=EYi~ z&k@)JNGLH|Iy^68hzA!2RB4TsSpJ^-Qz0)zOYEnY~-w1<_Kv6SOQWf}(%!caMgdsXk3K zno#zB=$6@hKruIsN^zDogm+3Gb zj%)z(yTgx*;n&4*(!JaduUPvI-2rQ+kMO-cN{ILC#lfeeS#drGF#lNk_oy-o$;Z%=yI9kl-kp78#+y^;%b$q)67 zjgD{gi>zLKB`WuZ<)l2@`Gz{q%c`=p1e@j4Gf>^75=K5*-cMbP_im`_YD#*p*wO>- zskwlQytr6A911N+xfFVnr=e|2Q0=u7ld z3&<4Iy?IL2C(cWCnwlLet{_3-N|1_flbX=t!q=w$657eDCV`+awg`lROPlF|V@Ms!7U3qK;1tm?Cn*IjHcRktvnG)B;Fl|fRe@KSFM7gr#LLw|CZ$w6L;>-BtLFI zNr_aB&zVwDt{czf+Y1$m@6jaW;Vb|x#C3J~&Brqb^RkE_K&=N>K|w)npmKnET;1Pv zJhW|sn@bfMnLn)(-0(Tn&GEmY%JDq0M;=|=ob~*o+u3Vs>hm~U{CZUIsrhzrghbVR zQZVT`4DS7TPvv)VYEnW@?6ApBi#6yVQicig3zZ!h^$#s0lJMxi!?n*0;M1 z)r;IDkcW5Mn(Gh|c^p4dFB8lCsMCF@4|J03Pr%=7IFF73R{m5weLY(*Ju9ymKXjKC zrfL2jslizOvw-~`r+1rTYUX12|HN5IL(8)LpYgMoOFn=-`kHv(`qZ&NH=Ox6%mDTQ<|9Ywo zC-jIQwqE+!a5O+Hm0MeAzs@6KjMY+N5ZgI1W)elxOQ*@!vYjjj2*ce3CfA>9MM8c$ z{`4U&B?40KtCA*u@{WYfL-5o1cW=RoXJW(2E0LL9u|u)fsFn8EXB%=ilQY9F!Qi24 zxk9V!zpDU`PV&ikGAJKQm{X`9Qsaq*eW81=&frmu`+Shu3A5r6>Id56$PValI6 z{(~Q!`SD;bJLCE1$_kyP^qN26Y}O;(wx}zw{51kM`W0gFYOH; z&5bTMW$rgN>&&d@O}4wGg@wt&*s&~uU=AHq>m1$Yb4;d%rLF0}ttDgw@01gCVNqiW z@YXUiv|C*D{M-Fn0T#}#iLIk#h;H+9s@@LHFhpnjlRCt262{%Fe9_z5mTP~F_snQ6`a(*6 zx}x?q%hK6MU#c6vl|T8aZf96rs*q#o``_$d$f(8cd!v#H^*$#Z?AKSDUr51Vx2}x# zj25Lk4BzGon%{2?n$aGxL4h%=47m{9MDA}22qdf;O&gcXY>2u1SP}^PVboQ<+lnc*C}=bmdk#T2;XBo?^8BD^N z@}0&T9KTe%qKv&^{59#Wu2FRAiqlYA%m;3bq>73rulmZA(@a{j8QeN35K{E=KbUSsb%yT#_e7D4wCE8w}4Bj94A@Ke`Oubc9ozb#1n&1S7;1Jw`ySrO(cXxLW z?(QDk-66=ro#5^s+#T+hz4!UYed1*eV5~W3cXf4j^}A^pqs@olf1-elyE>&)iJb9D zKQS;@I#SPyyIkvnxjP7NHdsiUPfXHIXZ%xMA@6jT*>Vpi*GMuP_H?bOL+hy#==P7% z^9+6HdS#2|`hoK4y98292GP+S7ccCb=pj3SB_Zi724E=L+yA{Y`^C6+TNG+K*!FL9 z&nI^k_UBVc+~)EnXL>CLBvU*OJh*H&fUR$UAm_5NnMs}3#fCsT{eSL?fW6er+KQBl z3qlqC+2@HlEMsiRgmk{@FpJ%x?4-`)#z(ZF>=Dz~y$f?S@~&Irg!My7u0)6Tl_F-z zBVW!f`=)D7X4q_d*Nm3#A%c1zS3Ewg#An0=YkRMkQ+L6mkp?K^qw3LQR14 zRdx3(v0(wp`X==m;wxm&J$k#s7*1U!dy2C{Gjx|1(!Gtm>b63S>F%c()!s}wVPBN5 z7fR7oE_+R0Sx4p4B|jDql@FPznh-|amOt&nB}o`%PADzZnU-29UblL`gT9oO-z6P- z(pUHgVGTWwv6!(7&za zF_puY6VjH^W4~$*ccHs-E^Q5px^=EG;&wYDXJswYdb*X6lG$6>e9hQ=dp5m4brBPm6 zQOI_Zu(wy}X;v5sn1=KCm{=Uat>lUCbiA|ddVM~z)r0%5(QJT!2)Ma9kC=)JtI9m9 z@`AFGijJ&OWIdjRy1IFZ7r?d!^_}bY$MGD<<}OtEq<95a7gkR))<`m%VXwSqi?K-~ zV;~Odm)c!U1QtS61P6Pr9QiJyt)WUVMd5>`=0XXIsdwRTXy(zpl;Fw+xZ!XO{oa^A zyL9RVEUg3v@HDLjtS4iP3Q$J-5@>Y9zNjcuZcwD=| z-yc7V)42lL65D~#ddXbC0yAb?g0v|wv>DJCO-72{yQ=f*4ntqE9SSU8kL`P^Wxv;* zeqWt(N6Y4#dx{6Bh6J+F2?0)Nbn`#GcL@9{(RpAGYWt;5UOZ0u^N`Pg8% z|J9KQ58Wc9tWceNh$vm?@HIpq_4SV%1M}~ZjgyQYM~f7zb6NCFsnVTN`tZ%pYg^7@ zGa|Ma3HMp=|HjNBhZ4J4qcs#|p|pOykB&Y!x8S&te$$>TH&#rm)%xHM?Qu}n9Uq@F z`G?2@9zQ``MFpLJ{uv&~1l7D=9GW^4r|Hb#wr+&`qxEZNC(}B>3&rJ=S!VZK4?!5u z<{is^zwOT1jc(aA0A@bxt=y={ii+0mURKu(vGs2jY=*ov{^uh6jPKf!y%GZTJw{bY zNo{C)g=@6D+}y?vnvjVnYObGjFd9$NQW4c+L=p%fM)bdrlu}3~x9sEaa(k8=%o5^v zJ_JE%!FyT54++hmhfP`Q;Rgv1yX<^N6>^Oj1a$evxs^1YFM?B(-qM=T{K!cPNz4Dv z|M*%?ew5#90CG-?LsG@{ZvMu%`X^j{K`eGFJ7lBdgzH!v90e#>vc8uZ+t9-vM68~R zA*$SKAb!h{8zs1KKD2V*#mC`)lA-Feyh~b<4p`hSo1mZP?cLm(!L(KqQSgUW+@4(bc1>-1dxFaEEWv4&~pIf7EBdqQps-+aAt4 z<&^K8LAUa_-*3U@;s{+<8j4SMy{1TRm-g1i1GG$bDYPY}wLek%LK$sFzixb7l0+cyVFgpkbNe7E~Ye&%_Fd`9pydrUlh? z@eu{{%HMdUOk7x##VhL6tFVmIThp)<(a_R44S32VB@0Q(k&T&-Q_xW`#GES3uC-E3 zgoFb)jaFC&;m0o6Z@A7SIjcw)WWxOGMRJS4dx^Jq^m;RDwtH+#{@f}30A>jtFyJSZ z@Pr(wu%h%EJmtpycQn|nL!Ze@D;-qJ(|^Mu1zQ;DKVyX4Nd%dwWi|A#!5tI6#}QSMha+ST5$C_`kaf@rHmJO z{QY@Hjsf}*iOhUS-c9qwjk>Js;q%@vLR5m*)HvAPPh{=OX^GKIts*0oV_36b%WCTA zU~AwY>y6(7J_fy${8NcVMUXcKRqA7-$`pUY_BN0K3p)L+Ulf7aq1qSN1zVRt%xz{; z2mj&51#ty6Z}(>yCo7 z?nm#o;YHB0=Gwunr)Wt0@xI7RO6Hkg4?a->2%>D;p2&e+k0_?OoCLty`Uz8`r9XLV z51}2O)vwQ(Fm&yGTb@xQ+TNeNc;E|99{#*NI4pKPTy=2s$f-@`a0b4dt4=bf%e)>G zXFd)wPdP3;VS+`B`<~2yTxG7>9^Uh>30qskCsO?2iJ>O{AG|PR)S74%!OP>V%-A2e z!=tXwYwr5i(I;3tBI0NW7Vh8>g~g*DF7ZRE*Bt(JyKSzJ^d1wFWP~Y!l+ovvqZl3C zfaz|%(O)$H>T6kL!h|a>%`<^aI#QrmPn#O3>#Ww33a{##Zb&~Ud{%!(83rLR!&3d0 z6Ysl&810FSt%l9zyp*~1T~1@7$*_!nzBg7TQoMoGMp5SW&E225JpSp!CWw4hnu=iN zjm_ysxmDHBAHW0~)=zgW?X@qBsl;iYPi@XPR=9hpQlqOS^z@*m$tP%_0<<3-7(bu;Uf&c=K75tG8LTV5XS}I>b>s!U;OOrx4FzT_>!s6EWy#%`CwHV+(3Phw}f19_KW@$5t)xY#-6djG75*cGP9{m z7t*9WU#gn0VWr~5zL|h$K*BfcfJk7i8M8IdFH4eVM-7Sn_t1Yi@Ru~Hq+$ObZ87+X zqQc6AeJi%>9bAHhjI80gE66BpMupV*yT+rmHdd#>HmHhp$N2 z9W~ujfUmelI(ECuV};UxOPwu|8T-ocQ4%1OmF4Y7AONAvf@cB#Nxv~D!BE=xF-}_% z6-`1?hLD`=xs@V)HUV{6Pka0l)Hcp$s+IA7T7XQp=+faZ-D7FHZ8@5gqOw`5?_0a6 zL{^mJZJ$D7Le>mwO=v6CzntO668SH;IY2!Of2wH(T-qH_a6f(r? z7ZJ9Ff|m+iq9p4rSce~L?rqW9k|?n251-o0XX_QY&M7@{47Wx`a{vQTMW~XVEXk{M z<-nSHNyFVQu))_wP_h;mQ+nXX=%S8$W-Ia?(LLBSbTmjoVH*kYm>#+bd-Eh%<$wU9 zpoN>CtH-jyXcaN{ugtQ>u)>B{{e6Uf3Mu@EWRCCzEyv2Li=U|wkRtiD=M+ZI87rLd z)U}{4-cY_w1?SD`_d+r_f2nkb9(~}HOCt1v8CQpKpwegT`%AC?#&i*i{3c2V9oie! zCeNZ9uFmHQ-`kgMGsE+Ug$u{eg;dYHErIa+qv*P)2n1m^sl0N%E>^qTdE%I?!XWbG zy~2@JLBr%<*=8Y$8a3{Z0L}Y0%g6GUIVHLOJI@;=Q7YhH(K0+Kv0qM*Ej$o!samz# z3!O{9KW3-KSZ7bFG#E?7LRwgpc_b?3STCokY5CovOou-9t2rO5f-egkzWk80Cgo_t6s zV7oc@nQh+Me@AMm7L)BPbg;Ra;)M%sOxF=xZnZsO?C%NDKMjZyxQ<2{g~@Tce)IgI zk(Fg{&~-~s8geYrcQ7rZM77Vn7~{U&MVpv6U2h!YrT{q;XR>qg&O4a;4h9sc6@_P_~sS%2Yc4%FP|=mW?+!8qM4#$o?YM0;_Cbb7WH3p z`$TjcJECm_y2ns|V>{JA2sRVG$Kp67r`&*AO%b*wg3rxgdqxCI&~pWRq90mSxOH8B zRsLc2q>;%)h*$Wt24A7tTfJ_-8$HP;A=TB&L5YWeHu&9Wedde%8;QB4!u5D2qDEP5 z%~!Vbg($7;yP+WgRi?&9VUOsNT;Um?cN&Mv2{)dKF1nS>TzC8WkAICG!Xp#_c)uR4 z&2ridk}lY6OqSniY1G*=Lz09r*$5HAI=qwR{s+haM|>&Vw{0)4wJ9O0!ip}QRa0)c z!p#*&79RAdK9c|d84nuZcNLP8A>-lg@pT&pX7^z@QLy3>9CyCP?S%JQrSr`1rMo`y zcZO4RAS}>^;%6Ehcw^~)5)PprTB!1`>4L}vzmViKOD9JwEbJLJV4y)l-2q$*NSdUA zoZc8qD^*gUXlneQP3NDoW$sBmi=sJ*6a6vRje*bzGFfox!M-spYS1-_@TW)ospkA~wcuUEKpO`Q` z*sC)(yOrGw{J&HDqqc;Ph=Y^Kp{aBjBf+PwAZn$>T~CvUj47w0i3MGQAW zgM90x95R!uFP6B023qSf!i+DOg6u&?la+U6I|v-m^=#;_;Aeqa>eXX++RWOD7! zkd&4*9&H8tA_jl8?4wZg^``BC-{QldGk z;(jFvIRl(^d`s=XaJ#0-bs~&;w@)c{3L7rW`8!tf&uhvNs@)_Lc=APujR9mn9jw0~ zeNnA%yV-^tIuT+l->e6N%b#*!m*R0YQAxM;2-#407NFK%RqbD%X-o7RF&5VKmuZhd zbY~?q!~FHailyrc zVJt5%EdCzE%R3_G7V&-|+lE_D2)%12G-C*4<=W`ZDu4uet3G@dVA8>Ly?l<4G$ zO81)Y+GJUs-vZ|5&e{>rp@~_2(3sjji2o%d4g+Je!i4O2DIH;wibcEqRh1KsW>at@ zaRj54ej&qr;+OXD|9#V7{&b$iZNL9gs#Zk*B19{bge4{GZxeMCvqXgoL>}pkrWnPT z64CsWD6^hCVRLplp}5qHA2BGX!+W}Ms7ysn62&HB0D2f zejIM5*1pkEK{LW_$%&j^nJO+}?*A*-9t^Uf=o;o=rY|T42UcJhaMDLZtxZzf8d(qq zRjwlK3#$cScihLET*6r0bvAInAanN+;pghJ6_yy_B<=F8Yib8nHBZjc0CyYApWrnH zn-z)R?^}HZfpwIef7_w3X*F@g!+8K%uCPz1=~`)(oTxFx-u`zlx z7L@W61CVvA>u2i~a5VAn(t6$P>QvM;5Y=4Z#;_ajh?)~P@s%XWE)SXuF~M)QdVd~Z z+SZ_xk}+Ejp}KsXsj4E$AMvOh`$|?^?6mbiJ?x zr-wyTwU@o3(csN@4D&e;Z?VVI+5DR_TW;P4Z7SCrvlaTT)0lkWvCBq$)TI45+$ zCF$-r@%1=hz_x1yJ?~>)gF`w!?rqGU=J#C1(OA=&N5UnxDj8Eu%a6Lyov^~4685~) zG(_Qkt6+ls1L89|lpy#<8m;Y=F-Lcf?b+f#m-k*dubrf6ovWBX_3t@1Jw%lJ`)u9d zbXgsZAr$4~pa@;!PRz zdN|$lqko6{8Lm{E7@s^FNqd^r)oL1WjsnPcPN{a&`Rc-|xF6~OJGu9}ZZ}CeNyC{X z1;E@uQAI9uVBE_QmS6! zuldaIem8wZRG~V|;l_@*{?(46t97u} zpg|VXL7nO|xbnV2d!!UJz&Y~!dd4Y=3TrI)+e-)cS%j6};8eBtu$NueD7~)_^(XXD zX-Vm+qYBgew3=@9{;+9CLbdyF2!r#&+nxtS>}{ELi_UQbyuP1^ML}E?Taa zS34%&&Bl-gp-2XQAHh&}jb7T*|1Vo8eX9EZfv~NwBTgl<$X=umB&3p#a+fEf^d~+S zEV9fVCD}=T@5($zT68a&>%hn`=H7}PSYxHCsKm^%Tj|ufqN1ufJ~NQp;INKk zXt>LH7!WLz#xr5JNn^^q35-59vA;$HzC6^Qp3t}cZO2^;*-3IBJuC7#H!vHi{ zO)28TlJSGaW3=T~8+I#p8(=;yd(YI2Ax|HIr^+5k*YgHsvs8WOkd(<=Mvl0uKE>lNbHPF_TKax;?kHR`TD=cmtK{-JPTPr|hIQ5JcSb;I-}h=TO( zUhR{F7)f2K-grF!2jm$-nO4B^A=GghKQc($=Od7C{Ou3r1-ET>+!ZIhrpvNjVY z`9WWus`~*mWz{sw*n3d@k9M`yIiGXmD7qu=AZ@-8cmkcCMLlmJZy~T?bnt* zpZ5#Gf?s#UP_8dZ=K3J;+2sZ+XeG08gGuZgkBexv$61~3-6D2=h^?uI*BoB%7N`3s zI14MAZGSIYrxwt*{pDq+8&kPGUaHQGQnRi0RG{B)AX59ruMs`9`)D;-asvH{;|4hr z66Q=%g08Vin@nuZ;$e-=Gi8+|hx;c?1x@cTFWZjv=XOote@X(bimvzl1=*PXxzx%=r>LDTKiAql#Vlq-hftDUsZ}O$K^SLFfV^oNu#~OJI zKmoML&N^}byDD4I{>z4W*Amg(#BDngtm0NlZ|Ec>y+F=2yk46Ahpp0cgyKf#hG;Q< z*6=L6R8G)an9_MVDUOuTxDdjG~{=UTA0G@;B5!!e^8m6npXQG|MbmF%(AvK zd9X{1u|jqM-;{npZ=Jj<;=anpkj@38f_{U?N-;&!ywTJgMC&OFRub6V#O0J+US+jY z-Dz@lwKz%YJkxT;$4<`T1+wE4sne|81tAAz8q$Vu_TtBjosh6FHe-93hgt5>-f=^m zNjlZ6+0A6d2N}B^r6?M?k<+_riJ7s(;Tuv1G%pH@g0}C_)G#g={HFrzy{piRoAQfB z=Be#bD?dbm^R2wK_Y>UGnu?-Vd)Ul{s_pG=-oDa+x=*CQh~`Z9`{$mnb#5wJ&i;$@ zuH3cXc`_MXfw$u_D_PU?p5AXy#N@Ksz~TUoKJjQGBC7NvJs;38qp0=j@A&8w)tl2X zyY>eI-S_Pw~*1Ozyy zWCB)J;>cE(jO7XL5+H#1r)4anUSP5wCM=G^6?PS~)MNi2pDSRG>cD}g4Acm!9zlKR z;*=E}z3~}bfYSiVwt;amnG%B`WxB;kVF@V;9^TxdEXM7pMTl%tNeRT*J^0wBU866a zKXn_$dpsKJHE{7!^)<3bOt36+NlZGaR2-%hvLZFjPXAe3}tVSQnhB58zu#bgk zfKcFX`yzR_r8$;ARmaX+Hi;PkG!30&)Rb*0kZ{}2wYL?JQ{kJ$d5?9C1m;_(KO38Z zD%;|iVGzFssOhTR3opc&1~MLkklC>Spxb z!+N;Nze@ZpN>^fL8lRLk%G>fDQO118V0!9qn#-44dkM4NJU>5I>(Tc>qwY?%S)+|8 zV5*8p-KEou(EW`6a(_I{F@;QS1hwIorZ%6aCJx6JI`BtBsuG=AOY}nq`JFjiq zX$^n1NfTrV1|ZuU&twg5nt=v@c?c983g>l52ziYkdD;1$aG5 z0KQ9V6fN?NKxvS3QsrcgvlRcJ1d>gBe-OtNp%;H~L)f0c44EV``FaG1)SwAU*N%|5 zjpFc8M81@)+7pbf$1llW!a~i1&ui0SWyfxZPPsV|-wMFS-)#9L${ zRT@4Uu1igqDP1Ualg+8rZD#x~>^#_B&UxaB4U|tAerW1EAvCjg;fxYS&wf0!?-4ZO6y!ZW6Sr4HY4_uf*~_H=^8f!DbnhPqulr-%>$^lRPyC3)wFw?(+Zg%CAj> zhE-0-#*~EmFAvuX^QKYjtCK!QmC3Cy`H z5>^~=YGL?A1I)b3m%p9qa*hA+$6Mg!w=89}%fR6B+Zb2(6DllbgfC`2t?6;xV5;=N z>#IG6H&HLwp?wx+f-*FXO&~Qr&~vnYlNWz!DMmBElcjnQAbklC#Fl4(5JLzFlP9}4 z0Kc5JEVZ1&j+J_hQMy7OdoQd%e;+%CJNByT$7%!w4Ns&q)64D^H5n%qD= zT&|B2UQ5ig1uUy?1u3Jt@vG)h=6SHEjviJ34+gk$;#!%#BNlN4M{WDB-i9 z3eU=A$^^iq@p?*j<5OSpY0? zVBrvmNNCfi`>uy2q{&HG)Bhm*q*HP<$;-jFkv}9K(Z{~xF*QV&^9rhuujh(z#VcL- z%OJN8)WhL_Ue68}TX%g-pNnMG)Ii?+xfZOvuxiKohpYj3!;*!VkWPeN4BXaimN|am zVav>nB2+gXjF{O|byHo1CKH-^@u%}L|4K+ZVMHYE_me+x94jk6Gf^7pOSm|71l+}= zN3?0eC2}Oz=XehWS~mC-HJz|*9mO3K(*LRSf09aWQ2#L}t$^!`-e6%fjAQ35;wgj&%FrdcUEww5L=qc=Sw@IS2h~^fQU)GT~^jUU*!C`fpPf00wJ8vzzLqK1m zWwpb4>{g@XYP36dzt3%Rul&TWu>O@Hx9cP3rPVsl;}pd2_(9@lw3kL~EScX{6b$@1 zpV5^DCvKCXfi=e$-mbfGzG$_N8y+oh%G}y~YrrhW@sJvi-9M<% zc*FaSU)yhY0tjiB?^-q2$Ix~+Yt$F6%LJEQraR&zlsa8f3iKy7K6VosMO)9UyfYB*sRTU*o1yM;2vZDQDfORlr!1~7uQS%0u zp?w*akR|71Yp=-N=OI%&8BIwcG+~Pd?IzHGhV(9xOT}XJSJBA1?Yu6oFR0z?+i}DYaJc9Bb zH12xxHZUkx>U4b&yx%dbH$Tlz1yyRdl1jIk;6}--K}YfjzCk;a)|OAkArFtp@8A`X zZ0CFAa8p~|D)4TI<-r9ct0_W zRC4V;6x#hcT>SCuLq|?t9{E$e!t~du%;71&CtSA;4W}Q6n^SxP(9^-K9oKNKY9bUB>ebnT~U2 ze{@~KueR9iOui6oFH;a0U6~P}py5)n2rd7g7GNlrt|J07$2VasPCp<}xb>tf)P3)r z-~MeM6Sy2 zE=!Br=`P@RZr9}dL&l4b2whKumTP158k@P)NfXuXn7Bz+@Pfu`k*oI24`s@8^J+MX zC=XvdM_Jmlb^^^GxNI7@bwC#m>Jxul$Z<&2B1889amurh7nnnI1Dtu8RL>jy$65MQdq zCSCJGV)bj?qzK@2wg%@MYk6idu&Y;C(H5WVC2tS; zt88%iED;mVsq0cbij;Kr3$@ib8i*(^9k53d-XOH6lMvKEw_b|*$2CUL5EGNMUG7k?4f#2}5VQhIlQ|SMB$bs}Gy)ytO*Sd|{XE9bd|xuN z5(X)x7lGJ!d~~$+q_$bq*%=)dCy#i(DQ#|!6)rjY%X+vrp6BClM-m1>zhM)D`=$It z&L96qt-ha($W^#1EigVxq-|NwOAs=QP&uUZxK_77_WcbLN>`>~x$5EsaQ=hJRUIYV;fc#g5%Tc}57*2eh7YwY- z3;+6y4}0HJ(d?ruza1N7bZhOe4}OAdDCh*&5{G`=yG>jCG0-4Idh%#`;XFYrHB$yS z<|Fesa)dl`xM}{oP_IwAmgmnT_Xikmo+pc&@-|JVZYQ)@sA#2rdk&{V8OlF2fY#5J zGHG0$wH&PSuV-MVC>5B;+xMk9bn>ZyIy8gHV+Lg)dX^gFI+Od{VDxr* zsZw>r=gkiumviOB6cCPLvA9dbp1|&9!2a+7tR=Zgl24S|(YQ3ldmf7a3 zgPF9B^!Jfd&qXoXm&7@pCQ+Hcun<0d;W7FP#T^h0eWnH5fw7&DVDJj@g+OaU;bw}^ z`+x=UV`nk#;cT(F*bmM$Gy3^Z527P54c9lqSh09~J>V(U@A?#U`pL=zasnNF0}19X z{$CM7k$W!2^b(amr-Tbm9f|i(xDWErAI9;A2V(DnO%eMHxtf648 zezQ7OQR_Yj10-8c&@M{>S}i&J*qi&?X>t}NU-fviimi&6zs!k(60B*@V|88DY5+HVq+ zwmp14MTpj2oF?*^Ulacav2H1Tt8QK~yUu)3a=EQ$3sCpUacwM@1@<^^@^ReK6c+VO zYnt1tH2YkIvn&+5D5SRQ=!mgb+&oFo4j<9&*PkRnGVq<#)ckS-G{E+Xz_@;8)>@Zs z$QE?Ww7LFVEsq$&*si%O{z=QZF~G`KKKh~wJ2qgkn?eXfn;CsY;rMU9_+dbUL5(h6 zPDNF*Cm@MU7D`>aVtxLW04IKs$WAHE<4wV_?Tpb`IT;9Utg(M}Oh1v|JexiR29Q5|-f?BL2K$-RRx&Wwp-e z^95)v7r2{-McbS7;K*LPQ$0_}N=v8dq&)WYtZr+8okgNLVgIz$zm(>7Y5jT^9!Mw5 z&c?#KC5+@#dg$n;Q|GF^`SMp{SzI5-YJ8vK=T;o=L>kn-ndPqc>L}$tJkGb!F}m`4 zz_jsv@;GctwXm`~JcY!Jk4xKgdY$;_3|ltb4hT*~M&DhI^>~^YuwHMB?<)!oicV|Hk zMs@1{I+lQnHsSg^LAp0E*@~5rDJqi`Hxhsr$i~&yaIq3jq}Et()vQCEey3Y)^w+ID zZsbjr&5e_>m?Cu=bAT@Zd$Y0nFQx+EwC1<72V=@hb%Ht$IJq3h@|B9vRN%-z5zWt}5^bOs>S|rGZHSD&~{Cw(t zS6WFo<9u>lNCz}6QC2+@v}tqZ>zC)%=G!bm@Hut#&c4}@ow0Fa;BiQsK8Z%r2mq0P zT`*R2#nt@rvD|S{OFq>cySqtRSu1$IAuJE_Z#vfyw>tYK7nd&=C+Pk_4E2#`<)$mOfBUoXxInsoKCrGTP_8t;+j*WJIZ0)!3^$rd~{bZ$nC*@}k z4J*a^)Tl{omGUv-Rny(C^T6Mm7FnEDIkIMi1rODwcMIu*`n-E^J~>0i&!GPY5+9YW z`+Y}dXXl0Kc!qk?Q!oFpPOpZ`BY|bemT6WlLY%uOA9dJ7+ry81sTN!Lwehj@&30R`E5qP$t|aI4F>ksNFY~TDDc(Z(Ye;SOo#{>0=H)zt7@&zMaK+x`H}P`U}UDPmc{NoN0E#1}fapH>D`F$(@Ugj%#K;CBxqn=q77+l~3Qg zox+^k9~pfzhBnn{?QP3|I#^J?R#avT=h(uox(1 zLPu#K$Fhsu9KF)p;}DutVbFg_2c~Ezq3`pE?K(D0X7te}ETd))VmC5ed98 z2R;Kc?ZMi%p~&WmWZn3#0XY_(!){)IIk$jhK9Oy;T-hv^=jx=kN-k`I!&h>(`y8r} z(G|l=SM$-Tpe22yAK9E}|HLz^s&=!UPruY+b|$5!wx~A<<^7Tq@?!&>^F>7=Et}dw*Z1Fq3xEbHF1^$K9XlEp9$3$&w4cFl zJF7X1%B?hB5BlOpSW`4YM)~&*?|n|&*8Lyvn$OfP32wW``x8w7q?1tRaN>8*>bhN%BZQB_w^^_+;w@mg|hcw70{;&k3M2ojpXq!QkVwmswk>~O*MeXdH4-xz9k z+i`d3&gH(gZNQ@xm)F*@A$s9Z)wZmqe(fh(enaVpu4(#DjpL)RO*m$UbnoeOd9{lD z$3s2G+iWBT^Cq(eRmw~6BR*@AUXJVQ6J`AIqq)xwHZos3zj+0X+Hg}oT`KK9v6)*P z>a$_lXFGWHeH(AcEjkJ}o6hB1-{O(#p%IyyeH4zDzd^GsZPCt?*SD$QZ^*%Zv5~8_ zW)m$Kitb*KS9`l3?2OXFBks8JWbxL<^j+L6Nt6;__Z}@a|JT1d#Hw^Jf4l9-Dph|rJVNm#?# zGP=)LX-ozoah*fPW^H#_9utZ(uZP;K_9yGtn0!euX#+>;&Ri|-W*{b5(n;_= zd~WDnfcjX}?J0W48}sBHI`kO;R>ZPT)@ct}^s2AbbmIhPd+|{QepAs3fvT!eARSZ zyB$4+T+-#yY=dm|W{weysy$_{p5JSVc#e0>wvpw5wIo(tKkcH7$wf83$Y4Jh!+k#0 z;C54AZ-M)%KX%er0<+;-1@G=lX&_ce;2Cr6VdNJqt*pdh5y%>Pvy@25B_^bR?mNq@ z*OLRx=-Hq9MdFYN0y`=iilotj*DJy<@4fRCd)dQyZ_kN=IrO_M>v{ELP$&eGp}ilm zaA0jyQ=N0VP-8e}pAq|FU|W$3LI^Ya?Md%J2Cjil!=huhsTchs97aZ_RdpLtL#7sa z(9!s$Ur(j8GvQVfW+ucKFzGJ2oz)^xQ?o37#zbvL|NG+s`AbJeS$`#u=l0u?pF_4Q zqxk0g&snPlK^K=@%So%A%G$a?hhC1gi*|fqihs9R3L4sz_gU|YZMp(;iK7mcCM}uM zp}$o4YIcj@*^$eAf!Nm7%nhp(LoUz!7Eew$5YY`_qiX~+wNyXGm>W^|J^!5Go{t&F zZn#>?#hN=UGD3CUdEqYd*61u)ms*#1`lhW>TH7+oupwJ&#Y8rJ3l>@~T&$pE+=oRMi^aTA>9-{;92Asq8X{vcv8{jyI1yLVms@? zwab5}7_?ko(Nt)57@v_Nt0~4-GB#SLwu*v+I-KOt7Y+m4zdTZRhYveFz=DaB*`f1x zhHJNa8n=YMLch1h_;8R`7Ppio-&QnKHBT7CA%{~O!76*mp|9G`fp$4VqfH*ekG5gi zgqPd$lcWiBoi1w`lSfUr{g?`O_1cZC?lHGfqeNL79YJ~-*h0D4m6#y9snBdd(vDVQ zZhxc-W*YkfIG7LNev);i8*n;ICt#rdTB@yMkHrdhXjR=`ZRee+RLScYlnB=)rl+~` zn6xksdOhZ0z`~gX0c_H@1~Ww#C$?u)xN#6o5C^^H-}tqe=R0tDOgmcNI+ML(4i32Q-ot$pyGx6CL&!s;{xzdT1Dd$&ayJ?guXPnkmn%{a=b@HZ* zj*)8f#s0|V2`@>-lq#wh>`=@rZR052-!k6FGXIvN+dueV(wC78FM-oLf;Ir(We*Vy z85(u+2n_`umzpgpq3)jOXC({J&&reM8xT>QoCK?#( z^jwX9!8bZ7hmM4mH*@aRn#t*U`mZy@qbH-WfFAHLFYoT-_`^h7W)Xat3yMwwUwBcE-2nFI!5q6 zC;iRRHtNWCc+_HtX5R8VwE6$2dh58Tzo2axM5IHIk`@VR=|+%HTDn8JyIYVh=}rNW zlJ0IP0b%LxhNWxYgTMQJp6C6;&j;9b_k7Qpxn|~?IWwl4`eeK$(Oa20+dCTD@(El# z(OUxg_@YiPWy^%k3A5>6U<>ZQ7H@Eg&rq!q)VDdCPgkhtJU~etRxOuwtZKbnYf##DtIMgp3{dej;^3CvSrTjaK6dbR`r%5Gp`vn(v~Qj?s|$ifl4^kR*&=VZg&1Zdv8)N@gx_$2m&Mm-afG=iI^hG+tq*Oekm9+@ngkp(UZWX zV$Ad+U->gJSwWrCWkX9yCiBXv))Ta7#!Z(R(Q>PI`q8y7`G>Iyd8IUS+>)BH3S2} z578Ckvu{~(J+wzHD9;dJDL#D~%|D}fB|7~?+}zxu2c6gRE|7a*PERIW(XQ{edb}!^ zk*X|yRaC9HjlB)`y=m4qeW!IhIy57(*zLhrlfm>c^yD>luj9|^i1=!!*a@~Pixb=F znVILLWHMedPd}}6aOM&IJMp>kz4LWE5=thjG;bufH}pdO0WGQhe<*^`x22WUSwY** zL}sNBXjy|F(aQ?~n<9cr-rGHTC_~y{dQRmW&THvkjA9Tw-6tji{z?q0D_&#MsR~p2 z%hRTM4n`wdK(A@~%uatD5~`@A-&i6RLTtOu=LrRl8gS>16@FZbc;*Kn?6#i#xa~Ep zhpL`!Y?!fC70JlHkNft~*#~~MOY%l34IwA!A(H>UpE(-tWgvE!#9Jy4hxpcpzAF&E)H6HTbgjs#Vz z8bL1wZEqd%SkWjM3v2eImEIdNBuJ$TR!7vg`UT9T56m2WP*G`#X-HZ89kiKPCn9;~ z#O1nlu+YncUi;*UHAC{gX7{_ki68MC#5CL}iF*g-4U2=D+DPGv=OK2ye;$UgOfSp_ zzYdn!915JCSuJ{O@cO{6J)JxRT`c%BQ}5}yYHPP62uwE}9goDlM12*NF4*k)S>yJe z1_wd+(aKLwn#vuQNVy5;)^2EydnKWE&W^U1mb*U;A0=SDB*|@~p|IiLW>BF>xxUHs zxVt3D9(f`%&P z?+eF#bkKZ;=Bpi66^CH|X{EbQ4Ex10!+T;B00awS;>LL9L^J!s$>JWW%qpoCUd;j_ z(V-MvuU-i2v0S&XJRM$2w=5(0u(q{1kG+e+9XW&>f3fg*%djts~vip6p1@nss$A z4|iuO{QhJ$i|1}PEt{Kz5VeWfBVR-i1HlB&+Z~bCSLb(;vF{3p1&<9xH?-0B6+OY$ zvvLIk>&V|dh|Zsd|AyXc3J~uG`UIA0Pp$G{5Tb9(W#)dKbCL6g;cgIf6K_S7@kjSw zK_dK!S~6qhg%C0@Aw+ePLmeeQ4m$2uOQ769=Q~$5Z?KIPJPp=#Yb;C!Px8V-KhUqZ z`7;&D|NLW)xBd3-#VltwH;DFkGa=PPa4*2m5w=9mOBm+?+nb4;mn|vA7#GGPe38*} z<80yw4aNM%HB{LAKb@{-lyS@oJ0wtk1N#N$^WB$kdN2;gOmFE%BPZSOC%8@1zxg>{ zRaHNmTk{%&s5fbeg_@QhfnSUB!C$1;z+QzOudQ@;epd+p{?0UDi>39!Ct%Cq`6t5CZHSEZpFcs(Dx(EQfd{)*$LIdte>1nD6}O_ho+8Oa=x^ z=KQ=5%+a)7TIY0k^WZc~bK1d+NM&tUICqWD=y|!e<^~}2T@swh% z3+bf$bxlr5?um5vFnpC@AYA-hli2MD9?5IJ}xv?MlpA$eET8#fj9$8$U{ zgeE4MOrka#DH`mv=Z)O%je?a5|il3 zyj-Jyf!geScYJZOSKSA+F)&E7_V&s`30jzL%76t49GsFOT(LG>u@F;R-dGt)Hu%_c z*{1WqwEz)BiALzDuK6VT6BmMR?)A9jq;gA(pB$((0WxoQbEH}*PE^gLdBg;0G!UX= zKgVe|j+4N}eIAxks!f1u5S3^%o02L#^H*S~VAeCW3MI<(rU8%_d~p+*OMVTnjA1D* zUw0^G(kUy64;SC5NYJPdrM&R1z)oxTOXZD4qWMff93X3_(9~t3^ARPrgyiQqrb2}` zt;)dd4XN!lwR0hv{>T53$=bT3Z)y}TY6ta{pWOC(}`Yr{zVor8U4O}V)`d3 zV~scYD>lB8uz*eJrNcsHV~mD|?GM;ZM@47$4+6HVh6igB=DHX7u8BMPLk%?6FBu_1 z1?C}E^hP%34;z~voZ(dv!_JK&|60b8vZwm?E{M@R{F?Qz#z^y%EN1MR3taE$$ceR$ zMvTWK&I2~TbQBDw?I$DT{X0GwdOnZd!&IzSV~nTJzU5Jigxz-lS6AY zrL)_x2qH;D!OrqbYdr)+i#mxHhys>Cwsc~4ICsP$@4zY}oQ%jJPc~#^^cB2gKrK5G zJ05Eqs&N`dvZdNvGY(~AlTQgLJpgT8LcFdv&|Av3! zo1CAROo+F$*vZ-+92_HM%Dqv5pLBOO3zzz%7H|0Dr-gb`=h^jcvR?ymPeu^F;?y-A zv6fj1emRB5DOhSw zzql_Gh_=Yii4uQFJ|w18O5MK{eA0z9cw|h;r*2867lnBT3S1@PX6Dua|Z`;f-P!T)eTc09bIgD+ieH{I5 zeDvUy2(G>et6IP2;PJcKwgUk85$g>wl&k5i3cFq*Sr0=roa|_k1*!VJz-J~A)pn|X z`dc*Ju=E_;yxGQ*&#@fS(P>?lwIPQ_j9_fry0{7nXzd&hYnHo(g!(u~K)w%T5lY0G z2XS1jVtJR-ejcSYwn@I)e~*n#Lw8Gf_CFNrW#p`Nu4Nk| zZW(h^ITd@YX{2o2Rjs^z1P4H!jQ#dH_mh)!JEn3irpSsuf7GR(cyhp~leqmX?B`i? z{UryS6k;>ipQ1Qlxerb%s`PkopXYRo1uE*4Qgc$Jt^ajOES+t)^)097`BTnihS@jp z$;Jr{JhExt?}GR>G9Jo0~n#`^2W5Y5{WRD!t`Ia@1-1WDUfg z1cEqxUdPR?ei&f@jNO5bm0Y_Oy}Hc-&#)kY^+aB$F(Rv}9~Ew5l0s?ljHeu~e)(s8 z7E9?Qn#NyJ8UD9*>8l^ahgtl0o?F~BT>cDHwzsg=+I zVctH*UN!8rcXh6CYIv0Qr0mIEo|uH5i+FD%@r|7EpZ0H1y6)Xxa0_e){{|5<>R43` z$5N$P%KSUrT;lD|@X#5|Z{C~c!{hUFhav_#j^q#pm9L@QHI7e*CMVPHOezL07oxC$ z04M%YJTg`S^wsrD`K6GuuvwWFWL;reM?l%8q=BN|=YthJj}4Q$&7FEWu^==2-qK(y zvkN9)>{m!3%PoWzlr$Xnwq01m#_Hf!e(Ib=*m~*Y4_22VY4vHENGn3=H59 zDZwFfAg4k=Z;K^M?v;%dIWH$3kPF$~*tFPj_zKLr+}81!;l7-#vF$88eeq9Z6lBnQ z9z9CDgAQFvI*B%rHK!EHbjjhsE*o-~zj0jC@iK#6pKEe)(N9Cm?fZcHCyp8WE#zcN zOal+~`6%>%%4wNXS>&eyi0mH_tp=Nx%`Y2e93(D=!#ZiHs422v2Ya_lnYf=bN|$d$ zL8x#iwO;)t;mKd?`s*NPb|$mr_GRmamu*Hbs2_imU$tn=wk*Y;~ilhG*uU-ahwzlLQ+a+ZKmO^Y)<&#QJYru}xT$+r*r{`ogHt0K9A4ILt zqb@G)Dob#5wqV3M$e)Vy)5LUEg0wvQBi~Yb;G-m`DNj_l1S1ZKA`hC+dje)rrUi?z z&h`ZxWXaYs$HBFs*18B-HP^K@k3mhec;k@`$8yP_K{%Gze2SW-rXmv5Dtrj?`*g-j zD5S2Eb8Gt4uxYVDRXo)bhg0)4h3SqcdLP1#u%wJLi{}~Pia8WK?McAJqKPCmK~JsQ zmomrZxD1W#u1ZSFAABUyMtBioQquZr*Nreuw)FEApOfPa#E(&-9G_G;xr>H4irLVk zP&T5WBvb1r`e}rWbfvFUXrU$(TPf8sV|WTdV7hv0Sjk$uvR3p8Hg_%g4XJ7T5{}ln z2Od`SC5r900U4oom8%U4g=56a#X}B>q(9de2|bxnesyWSf%S{MN0PH}H`F6a3*+dm z?I4?Ce7W+a@abS~oF%i_Vq=rR9};q0|D@oi{}#&^U(lE;SY&?J~mLVMeSjQFaBdJk&Cwb8@_5F7>AYH!s>lHW8k)v3`Vq0UwVv&!DpTK>__A}DFe%O zDI2AT1LKC*cY7zUpC0DsGR+9^u&kN#v$f?vC1h$e$wPH5c-QW&h*k|`tHbdQxul2= z>x02F7}aE^vyt+L7i&1HcOAxBtT`k+v58cMist6s&0*`v-DkU>P8+NPVeu}Rt;cg! zGtKRK{}{{gv_r->P{9fhTvq?SsiiIaCFo8*Icwn|DQ36vJ^c7^a&SEV@d-qe%Rh$9 za3Yu}<&-=fGJXCSIgb22o*q6ovtc^sn1m9I+*NZgnz|auv25N%YoWyrrz~ltXONLI zBgmZA8OPUQ*dDQL9e8g5y(K9hhgz#5E}5vPHwuiQV4PQ)jKI;ZF04y_aPkh55OEyI z>H7ZNB*uXvPIm5cdF4<_+li+ouk#ps5-Y3&7UP7{fEax3I!dO<&StML_qJY-{Mq?l zcuPh`zBDe7pFlHQgc^TiYdX!gmf~l?7$^BPv^*j#yP0&mHq2hDbF*>FxzTXcSm|K% z*|7COIS+qq#H6Cu8@JqO1n$6@lbH_G*1@0ab_ZUKqHxpncoFASD^tjvVDlMuqOi9D zMRO=#w@1go34`e%a*H{lpw@o6>Ie>8=LD3~gR>p1_@=7^Sd49fYCh68 zNE0*FhP0AVf|3kI>w@4Rdh>~iUvHNYH{t1Q zL`*}&+T;$ZNL535Ma{4Je6rs-j9J1s%-lHJ*{cmtB~7S#D(l1sFV-f%Xa03iG|U!- zT>HKg@B2jmqF$W=*+Jp?mPrpQizP|Py6lHD5>yHR`XF0G6G`-YEcY|;utBoW1bQL( z<6s{d3xxvYlInl3T3MAa=~M!!B>Q=GW?p;v(Zo{wUQ zRJpiz{vJlL{%Br@mMkk2@7M4t4^R&qD__RPS_gyxFJ|};JOK^5E12Ej&81sdY$?rZ zW%FZxmTk-jJji?aGv}(@a&%9|Kx{Y=2dsfmAq4G4$)Pg-Z$Qd-ll5aLdGXbGIVVVF zV&gs+J^H~)vOk%~tOh_2%dWCIUY6grLXaAfpIGar@0_N{Ru9m1jQ_1qEVu&gyoalw z*!ts}KP?`tgCs)-SjnYb-k;nXIc9W`4Kx6S>NgP6P}Wk2lw*w%O08T=@=*?0iRzTJ za9ogDH|%fs0bQU$82H}7f=^nlWUH;2!KQ*9IAMgmIP$^20&f!sis7vm4)xP^{v;MpT*sr79H^=oA#%qlYgDaVTU znp=0u&3!HFIx*4Nb#qAEn`^LVCe98(d$$HiBYgWWlQcQ0`IrKflQr!LVq(;3fM>yS zUCrwPLn_(_yB7ZG^LA$x9zm7C9A+#QS$EICw*e~2m}!3e{Gmdy!J;8HBkpeGs9Q{3 z*^@8l8kO3ue(F5g+(auYU%QRFd$ zlG4(UP-IG)S5HBdA?E{Mv@>J1kkR=wQVVzxG(dI$SNi3N*EhXO@W?~<>sR&ahDP=|A@2#xBLGV9EP=K9Zq4a;uj=Ar%I$+^F#^|jGDOR(sNCc*m zxt(7^F|jBCf1hy12M}TG12Bhsb=Ib^4yR_1^Uu5W6*%;do?LF-cd z_^P1}E8X^ex04u}Jrqj5x+#yhb=8q(ndwx6G5?N%w@`r&8S|JL7%&v7V7uxc$-qlg zWUriQWI(LRAYGw^7Yh!?Ccq1djI7C*1{-D|a{cvd$+asQQG@%_D>ugjY64s$97d{t z8Qok-XxkARs_;Fjx423jRb|zPDGK{hBCJC zv47qLht8D@hZ$_wuOGTL%;@Cl{l^`l>C6mZHyJNTht`vXA=!{Ul+#OU8n!^N0FQ=- z{}f{Dox3r0n#;Zsv}~cCkKb{lJA9jz$Y7@u2q7{hAL-V2LID_iR2TV!;>4d}1d}){ zPrLSguP$|~+$X8kp!9}0#j?R2hsIU+-`8(*kdW>Z#bftn2l9X{8_1K!fm07+YPxbf z7p`yO!zCpXl@UYE5qY57h-(C=o^SrMc&5^ng}BzbueUcflqx80nqPHs3FtO5rRK8l z(uP^MoP*U#YHE-T$s;hBzDzGC`U+K{0vh0ZfT6-~NykKKe@V6S2!wJ2v$J8zA*mGC zu_#n;1Qf+AJn%i00H%9mU0CoxiX6}jHbz3NltQ4fjp!}9qa)t88coJ%DwC71&v&|h zi6B(eBwm6$MBvk==&VXnd{$i0JrKl&%ZgF>sP(zlI9Y149HF#LM_#%`24;SS#VrodKcYQSE@^ z9H#~cW+q{xOXgd;)DEQJ^;wkh-{sdN2M7Oq{7JdL6mj@WpIzK{@^EbNJLPXI2!U!T zPkH2Im63~jVJEyO0t~QIf!mD#DgOr8WOgHc;UM&VNq2E-b0XgF#zs}2@7w+#6beP| z8SR_+heB|tm;{wxYZ9ke1ULkw12swo%bTlK|8x3{97nK-KJ#}Rl3Os5lX2g%1T~jA zzZqpuSB&I@C<_u=^zQ4Q-_(PzC>{+y&3^(g4(eH6_e#bAApEZu;*lO|^dv9lvgsM_ z2@Ux}@#xr~qlr>O-B2mJ-BnaSb%1fm(=EQjl<@HL_QR7`9s@|e|A}9iw+uFA`AGZi zh&0{ZI(L|lIs?38ad}1Dvmq(O5w(c+>)lW4NFI-;qv$nJj^n?{?)z%Y>qup9G zwuH8DH`#>m)y;kfy;x{Gt(|X_zMh=%%MwM5_dHKXGqn6iTH{mylT@3JV`iC~v2leT zMt?shEvbD=R=0#H{*0#m7r3vjGWs^7P>MBq8WONua84;9;kuj1oby2bR%+f<5>isY<=#K`*g% z-mg5G79>zX47R+-)K!B@DdwrHtWt5Y#ALNll|&(|ci4RKve~2Ie>URrtAMVG{P)3Y z)5QlMp-w}|{IW$?Ubo|UEYPr8Q*s7wtBwv`StB!S5Z;M>MKm;|YB>NZLH$f$-s0sT zx0P6tGOP2v`c1D`oe*j^0QTP_!<%X31aV18lKSu9-XrPtw|Dzb&lL~P>Xt*GV_R9Q z&g0ZipFK@iVWOs$Qa?U;1%^&y%{#(04)(0>?{0BWG0AOQ8p@+tlJwXP1XN%DlbS$m zjF10xQBsLM0q`R@B+G;A_FcT%ZV^Bd#>cB>GGc=gYVcM%{J{oblafWm=!LPEH0{EU+nZHOqB5)^#OzQPn?DRi8FT13p3kYt{uH-e-~HiJUj=V}otGCJ-u-wwJ^cM2Gt zF_zZy9*f_@gVg=7{U=`kM1T@BnRygnnHdLhGozt3@sRyyfIUYn@=d z%4=9Eo*vJ2-b*XZ@_$d2<-b!k`Bka-oDAm!z$2wzj4eM;8L@{_Vx z`d)b>Cf21c(-fpl?^Q9RoSKy)=4!g03)c3(TMDAAM>p{RqzhiUb^f?8TqyM;n!(%2HfEbClldU#OTym?J~^qPzB85~1Co_rfm)xg8)DfIg0 z*4F)GXDfx(MF5TfUFP$6Leej52wZf_&R|dc1$j7zWhndPyasn^O4+_X|SuDw;!CBr`Qw_v|UN4~d zULrDLFW$#8n<(d{=@Ncw#pe-ia1c4tY2PBMbB7o za<%`5!peG1=xHvtsJc{sK^Pt}cLf#i*Xe%bf{2%)kJ0=fj;NDC)CJ~{*Bw!&$2!VI zVSc6eCFXB~!U^B2E~M*gutt-;Ah;nk@NjXs6yBjv z(e}#^Y`nFVtAo?4gLf~J=DzT%_` z?rCE}dbH7ikNuzQ;z}5BBHWQG9k0C4Ujpl-CZ8!Lsq$Rho2ZW@bU3JVO||)o1>+@(rVw*M9ZsqONZ5 z;)HX&6yo-I?^cwY97gTV#^T`m2hP&TDb>yNxSN`UOunz-RgM80nPiz;B|`s;>a_n* z-5#^y|9FoO{i}Uv&{yzsR`&nrx69fX0XB;I(Oo#rlC_eT%c(n41%*hz2y{JDbZuG) z-#WPkT)>BhmZ&%30RQ@-T6xNLwm-K)4~JA^g6e8(DJgkgFWVOmfCB?dOZu;vRICmF z89o{p#2xS=Btsvz6gnsibRaF%{EXbO|u{l;=?aqmV-_FiGun}Hk5C+yIaXYr9_EqE> zf(P8_Y9K%9y|w*Zs0vJiyTb)LzzR^b-7V1HjSCi z*#hLu!SNARX!J(GgWCbH7?C{s1SaR``j8!Y-3d)`ag|O@;al_4I!mAv1!ZI~9r@e| ziTCt<935JY#9HLKoreb%IQG@s$pe%F|AMeUY{BTWM`D4J@m-Mu2cgjI|20?8$^^zw z^e8B2RD1%-k4n+dpbpFx<8K`9YFP~1*$i`z$-4$0LH^5wLSJoL8g$~Y(#WCbEP>l@ zEW)=>Kp_De_kwfriFfbJ%-0TtIJfmyB>0ffu+7y_rTR7hCV;u!BXYyAL*I}YN8Qzn zTNBpHh3LrJs;a8Bnk~J}3JrPt6V3^c)OI^N)gN&zUtU>61Li`8P%x99yq*{f3rk8` z`sDIt?YA_Gt)dnSx2vw#CZb6zNBEJ?4Oxb6bjEBHL@zy{w3LZ2qileyjJd3t?-no9 zlN43h--~}Bn2P2DFCA{Z{`f9PJ)SoPqb-$pjmW-G3{Gf z5C1&MeY@rk4XESQR__nCZG{x1{?8^ZM0Zk<3qDfkdso6O?FwJH4v9}JV(Bq5a*Q^wO z*7pn74NjD7_G_eCiTEz4BIXx3Wd4b@u21?rEmESZ4-Mbpdi~?mtDD+-a_~EUmhZ@Ul1@-t^r!}8GSSfW z^+E*>W@OnlY}ILxW8OsL0XD{~N+Fm8WUCg6f8o znAy~be6vhaIQPfZ3sqHmCqBW;MXs+}<-`!ma{%_G8J=+s2j3)-(NI5|jZ=WtmN&zM z5>n|~AC}+WHMeLk1cNIoRpo6a3Zk7F!Gq*1>g{@#5VrfD#iJI&q#J3}()DI%ba5*V zHFAA*ZFil&L;^H1+={^p?vAO}g0O74w0-XKleWppM<E0&H|Lkg6y5%FH%Rd`Uf-PJq;Y_Z`VlJ%pBqAv_%jvunES{ogWcd{nI zbe_9rFT`NfXjTh5ocFi1n$0x8f|CvoPe$GzH6|r3x+SPBrg!aBP5@s5bkDQZXyl*_ zL>#_#p|<4CKkjM5`a{b#J)N2A6S8O0JYtFl+l1LEGOa<{ zYL6cV*$I6_Yz=RBDpAQYd9!Y_(g(Aagu;u%*EGnBOUpMpOgG19X_5&to1eFcj5doi zxNS`=5&mKM;?weeq>+rWyWxEC?izg;k|e)5^tN_^-J4Izrup0Tl24x!*1W6k-oVJ9 z;r#J)nYIGQz(FQ^h2rZih#sGnFR%Oc)9Y)k_32#q|9pF$5Ww;W;H(nHV@v*DQmN$# zZjbAz9Z(KtF~a{#>oBajOiWN^!Oi&#smsBp@+G{@{fqKvOo zGxp^MXefo^`E67CV}-e@J|8fX>>niiET}&_!`O1tKwNrgjh z@mLG@sxm@wT@v^fm%njW?Sz|Fm!G5XPBhDoIgNwRRvx0NwKUGvq>wL z_;%|)YdRzLYk3{N>k?b&u|9cJQhaQjZ@gMo1Cgim@X~tS6084L;gZdz19NZ^V~Fdd z^!n`xmvJQ8I~+9@y`j+H@jF`UkmaFQ;CZ=-_C5!``%hV zM(sPf*@oI( zl{G)vp-lt{Tk;@{92;3%{_Jcn@FK6dm)2|d%4ivRkOA15>iKp>wRnc2qiuu@#9iNP zQO~pAoBZ8P9kZ897w9P__i8_k_rCJ*bR0cPL&3r{83`tjb*=YngOmab&33Mr;eNBUE=(+D`JO~gCC=GBJ(J1>z_@&&x2IYJ#2!3eW z*&UnrhKj-c3Y&%;-1c2 zY;@di)(Uf`Qky?zlTNZVp6`EJ?Lm4e^zas>!Y)Yr3rljtPN;BYS}}!fU30tcKkz4G zwNJ1xt=F~HvKcr%A&@8R-aT(}ciR-G(k(24JUQ-63xC5>zP^g`k))q)xwTIi(W2}i z%X8o3Ro4jmfX9{DTa}lkJ)1iCqSdmWv)mHz&S}ZLuki-V&4DWd$DG2NX<49ap?y@O zrn>^!7;G>*Lk55JSJ~q}{Zq0fa|k_;7Tzpva14zM(ccY3VbrV3B#IWaX(m4gFVitG zY#pZA(D1Zp!A8^2fVO$LZG*KRAU*6iH5Z7Z<=M?b8~*Tb!vWgG#8B3rrqud-Kx{n(lU4E^U-Sk zk{i1^yEbAp^HT+gZD81Msl&S^!@FNjZ@`5#;<}Xki#b5x+6@Gvkn|6&FVbc`(SNtp z3g&TPuOG>0B~Pa=;q6VmUGt63gX}f9&AVOf&yBXo3r?<2mzG-ESa)*lG(4shop5!@ z4ng9Uef7)3W2!VIPTs2@mPR!xGVD?IW89X?DL}&YBbi9d@K3&%%p5A^Ez>K zBWA`u8135q+5yBaWi9KUb49;^nKQ_gv*ul!zWqZZ>jjp!V95u$V`j;>8(V-l!SiJg zUa<_PmiFCH=*W3y{cYv>Bdg)!;zH^pV0O=GUIQGnb#I&c8;@oItt?3e0Ka%7Z{XV@ zbEOHKT1n}t(h_o%EP~#+Z=XLzG_qzczmxw>|C5lvge8kbTTUiWn1H6|m$fV39#`S< za9{Kvem-o!2k)+AyK@;;SlkdIg2j_KX%?x~LoJJ-^$}s^?#hHYjpLO7w|aiZm25)h zmK||6hbRTR?=;oO$JP%4v8{`TFJ4TQZ0#G3E=u6H3<9!+oE@7`67mScB1WO{FlfT-YV z_x9*|ySN;r{Knek^_a5+Lf^g*&Q+c|n{#q;TFvY$nBf0u;^PV~v(W1LHel*Hz2OW2NmO^+b+}J5oAG1`mO199VKH9toVUF4buUI3WMti;Q|-!dD;T@Qo-a^HIVO<78=!8#5B=auK`7}kR05E3#XzoTeYb6A)CNw z@$eB0dn30c$NlI)AK#F?_Y;=G>wmS=NdO0x=?*)&aO&wBj0{K#j$Cob@^~-N)WC<0 zyAvJUtW=LM1`i(M*jQ=A`U8-Zr9eLq4W*}oD?<}k|<2sBM5b2!3ubjj97ie z@LU%zxjYM|i7`>v#z#X5;-fd;zZVz)9+8W!vwqllf_bCSJn8f{43BBbg=@t@Y8Jj( zKph$hrB( z&8hz*JDc&Sh(1D@Alu3Y((Tl=(x3>Inu<0{Sk-ntscj1I!8Yir0e9Xbw|C{aPzy`5 zdCi!4r7tFq2pdzPMgbjvD21az{oTCN49A!Id(a3&6xp76eRBh6o&1lv1>jDqTxNbd zdNjA5c)MF)q+~KZ3-AZmmQ%Txd~eeO2e;NZLH$8nOnWNtSN2LQZo=Az5lLuLg|~b! ztjcw1MOM|9yl?5_T*RTQ>x77w>Kjj%=VDL0zt$^!>d1=5t@C^J1wkx5`^gt8@fS$% zjgBb$3nIop5-}M&m9Jk@ambEyFexPZ=s^;ST}OB?2O1^@=VIkYcx5uK(ubjrQ2NW4 z&W@9nb~kB^dbe1XT>Hj<>p=lNYAfs%@pB%d@UpNgb2OZ<{Df5-suZ=IeS-!Z0m0z$ zq()vXZ`3dSHJ^guC=-wxQ_63xuaEEQi*j_eHS9mV(^{(Jewz?w{Sqf1&8R(t^!oGe z=QM9EJyU0G9fm%Odv4FofvRQ!iguv{bl%u92dxxbn2Ncr*E3GY4O!cVScKS_EUkCrF)zo@;w7L_$@!Xa>QJh5}FqJv%*d5pR^+jEgZHrcUDXJEt@7J52! zvr)3sH2(C<~rFe7fEKp>5qXoIUHDoNr#zFHdBLH3dr;c zCr7cdu{Z^znH|EOE?}(funy?D|IkTJ#OH>*n5o-*a%h)H!jYU{sroG~Y0#3T8C<7Q5`~vt!`)tLVtMd+Q0Zme_Ln5&fAA*ry?Xnq(>XpdkRA%rLIPFHx_G{qjYW<7Lh8-Y@P%mey-jz>sP zp8__PeFZ8n15wsi>~yUvsPbGQ6a6Au4}_lH@aLf`C^WH-l0~*(b99{;=V4CgMT0nK z)tTV?n&J~AvB4mbXWJxLesOUDMa`lPH*Y3RR=&%y$))G`mUEpQO$M#Tvgr~&FtKlz zuLrCIFzRyC{R#daucY12)V(mwQ^mVCtIpMI~FME`-i99vCz{qsGI(p+j8RB`9rLzm<&qANF(60J~h_>{{# z=D_;5cz6WR!@$rhH;;)LZ~pMcM3fWY5n=-V+P~{j78r!R1Vii1))XhulBOTVrOXtt zH#RoVx95`RGB8;mv|vg3gP`hQ<+#Go$S5=0w|s7UX^$&RTFT9*8@XT<=8v`HF_>Gv z><1UCsMrvb7TlC(FCJXNZrU&4oO+Bsr#FR;iA*5~eY!Q0)i=?F;5P{pEuZiCJe&?c zyIQHeqRZAzs37C=v?P#L)9L*Lk!-N=V;v~ zzQD(;GTWhFgjvxmZQczvGVu=xqyPy9vu9_6x}F7eD686A?Oprc8ywv?q+jYFQ`*oN z-#CE8T@O2pa5SLqT%+e^ntaJwn%|=PZOW#ttgcF#eV~gZCQRy1Fn z)QfeyNW>o|oXC{Z$F$Ipr)E1;Y|SG^=lH zY-}}Em+XNaO7QSu?OCLqgM-nu-Jsp{;!$>E1q!7N=27bf98$E=@oGpBiQMn+_1`19 zsff_Mjd2Lk3k0KebgKVCp++6H!~?u(k`A+POop_Kcl$a-I*1Khw~iJh-x8wJ;l-q> ziHRlKyHraYbnom#0jJ;S8YY?NVl*OO48OKi2ehgKxB-V5t1Q;*nePfxdVa^hIjlA=FkDjK>n@ZVYdTxK1<`Sj`h zX>{G!ho*YC97cqz4$?09NRP}oekT3*YRr&we`qU9`?`TO(j;R1fNAQGl#_0D(! z)61H$&?Pxae%fHFzE~n*_fzV7A&!?Ag`-B0>#bMLZ);&}DgR;^WL#T$*V!5?sis8P zH*}1v#}&$$wE2~w$uS6UBchXNE5H>|v9Eb(7@cDsD;`)H+K4qMJeH2yXWh++l!)*~%KlAwipG%iexbsWc8}ZE=%Q{ZZNz$t!+-AR zU~o2a&upQxW?>I{Ufa^V*cx!_zSr^%F8)Q*FISkZ!$`(PKwY1*Cn;xS{-|}!waLwk z+JkwyV6KBV%x?^aZ)%#afA8UYA5PcA4bRmcADG6OfBQq?m)%YwlyS!UF0w6P^8|%! z$&j*pn3C^R%xIrL512W-+)69VmcShy{dok^CFA)FLEzGeTp}{7?*Rm7JDU!;*(bt!uFUKBI5^8)em=c+c@U=IYqxP0qfEX*ZgSzSU@CIMZltQ@ z%R!Id=g+D&X=$R|acX8mD?3-ETca*YSCb#E(IVV@@ezWfxbLkG4hxLe2N4>YT(#g{ zb_f0`SF?Q9c7Kk8ziozxc~CJfm_Td+zrw zN5y{y@23y$j>dG8yh=qwEG2UkSZ%KCB;?ck17MeoyWtF5ZuLzQwc?JtzMDjFFaJgt zELjV>drh;>YwP^^$=Q6e4|{9TNh@%%DlLWFavE0159vKw`5Qf6Xyz<*9#gVWUGNd~FGgp~>p@ry9OO zuHDa8FysFy(@q-G(x5x zQp@6deCK6{0OP%^izrK|2c#<@Vngxb6PIuwpHc0h7f{_yZ{My94} zD0-J<^N)%9H6xlT-(*XAS4ZcyKP&II-!5XlQ9P%j4mk$ANL+1vBL0hW^JS{hgPW>?koP z8r#D!H6xtMp56Ol&It4}-+jWQM7t^s(|Webeoz8XUv*Bq5-F{x0lmH#`<>!@G84TC zRfuyn7K4R^T^p%BNO&ui=b&o6-o4tEw9mFmBF3FQct2g+Dd)kho>$ErQfJEG;bPsp zv+o%AjOa)s;F%NqWVFwkNRW^VF=!06ymKh(Bt{{Xf6~gjJQ8)bGc)_QR=P=|Nj5LtPpe)b!`xUPBi7AaANMi#J56>^t4OFGM zUVD4SOwPExqBp?ndrpA&HP6sk>~hhv{PZkalE>%tb>`ur*q&1XHLQ&k_N`~n#T=5D zApFoj7XPB!0;BN{wvyH!r1VdjxZ~7awcp) zaJtc2))4pNzjBL^mv0iJoN`3AMFZ+fE%{GR(RgChM{>TuNGzS{F83|#n48F$rX#f< z;{$Kv=`Yz#laqzFvliVLL`kvz?su)IG0Ih|V#2^vdzhEIIK^m!0xo;O@HYo{XPTlj z1U)f99feEnMj}az=fx}0LPJo>2jD>K@AdVK%1+Mwuzs*Bn zWUZ-+&~ewTk&L{|Hs6GMfdr}lkFU1?sxoZChAHV%x}_u}rMpB*Lh0_1K6HnqQUcN? zN_Tg6r^KPV4&88w|3Q7<|DA8X`DV_{fq{9RJ9e+#d-vMCpCvN8Ys^Cml|(9pS%uET zBMyd2e{mbvuh%Sj3E!X%=re4Os``$v$CCre&VeB_WBtBnzl@C5?vP+%GY)0TTe4Z= z6ks=h{o$um^0r+j7%=9kM&`6l;d^@XTB)kuTH$TcFT%*k($)g#>+{0UIR9WS{JHO0 zX0Z0t88{``yE@hbNWzt8Q>K%WQH_x-Z@SR6T9LUY!aMZ%+=z^H{`M2Z>x}=;?r>$c zQv#Uic$DUflOKiaHrrVO_-~Yv85sV1~|fjS_y4mk@H)W%3YGpj@}NFByntd z8bNyDjt42i_S2Oq>DlrIAf{>kTV4IJfkD&Wq7K={_0`(0!naO>5bk30`y31}p_9Ub zdjtd^KRN(*G(R~#hg&)FbxGW_x+VL|`T2mus;H<(e2RM{e-~b!;iI7EBRtMerr-Sd za6vwYAykv%U$OP-4_9Og;FAW1AWT%s38ee3!3JY^2hQ8EAC_0UZF=;`C%11;ZULck zvzU()<0-?(&v|O*5c~14fTI4*+4IRCwKw!L2|$R}ZceJcrD@J;l7wkqt5vFoyk^N9 zV(e)OL?0?AEI0w@tHXe8tU>bi)46eA=%{aCWKG__o^Sp2iyH8zjmM^uAVb{1hJQ&?>ZwXHXn1^mXug4O9uBLRxvANlG?25Qy@tgYfm`XdBuNRm42 zseL`@fOQV_jZ1YT;C13m>dI32IKjaV^tkwaMyo|2YyCa4?3*aA=oY3BM1+w&jo{ml ztoyKv)~P9@`|TgjFghs)U_q6dsUEe~%O3m6GEC25iOsJW)4iy;alHf(#>vc|x4c>QAL%grN+w(#a#4Wtl=;LiqiB_(9P%1V%#wYxi?3_~wH zGczqWe@~T28E{z4%Of;5XATMqvZ&U=Boi7^9>p|0m{4^&3ga^$jmgMRw2(%I0f;9( z_aWY1Hj(W*AQ=>yei;ictqzIKf_+af=BX(SR8&;Hc!%(^F}BG!^IHW_vW4&w((y`{ zqLbE4vnamjlhEa>JZP;y3vg)<)M3bvQI>sfzO~5neq}^ zJYnUtbl2Oo+PvvWHfMKc@>ILCIfm8uth)8F2alwy8gnvtUJIX+u!ozy`Bzqsmo*40 zi;7YeTkj*J4k7P^M4**(IU_e{Qc_6djgRqGocz#{q7;RZ1|pc9-~;i^<&|4Z=}@bU zu3|-H4_>_79VIvd`MQGA3+QR9w1Q$Sy`!shwvP$Ia0;hbVg3RKFci6dUQmabh*m^$ zF1bhFxgZ~N1Yau?74tf;VY}E`i9q!%V|0SB#zIj7fR;MU zJNRrHAewegE;jc(D43dm&?}+K(`z0ipUkEoK9L@I+dD=#aMFIJC|zRhh}`*g-7l2kzK zt;Q7AytlWY^`)+k&Oj)Zd8K8io`;M458XkHOHygmjx5Q$hi#V}?^{93sffqtXDPnmYS~6Nw~iJ+0+yWwUmGUK%ux&yq%s4oQhy)o7U~rY-a5>R~-OF zFAevJ!kDkgR4eQo9^9^Pk2>23z=HesJ*H!9X8EN69g>uk)WG1NyY2BhIyvu{nJ<`+ zPf)~&1}TbMGE)hg6uXUQz2~{g=wnRptNq54$5GZlct#7HeE@$Ba2#L&gGYH{fhvfTo7E zu9~huQRFbZDWt4n?#J9a0V%rU={W4gZ0sme<38(T-JBmi8*^%XO#YSFYpo-LlVRH# zKJiy{UR@y>;nYB&D=(BDAT+LBkOPIXIt2O(+V2YkEn3L+>v10*$&chSLf&y0Kgce5 zlslL?4d#pk#KO91Z(kl?fKPK|-m}d*mL*n~FL<&8A}73MS2kzeenh+~NkOas3Y0w| zqn#Gluj1W0gVmC`zR)mJhl#}7ZP(L5buMOb#TN*PySt7zXUp+TKlMgU9+me3H>zV` zEln+}EVW3Y3A;h$8}Gf@z4?oid(MXIKsgoWgFs zWcPc1RcKW1r_O>xnI`+xFoE=Z%udN$@8n2Bia^m-;xG6wL zA9+rTWNt-2(tILe3V8_@#4ce_DtRG1?_s;RESYU;znAxQpObE_fdG5Dh;U4M$Vkno z{<}f9-eAqZ289Gr*6C#T=vlWt(g>8{_RJ5GC}Kh7z5^9!t(qQY++^C?K7EHjRXMCJ zd{MuCWocag`SXR@Ls^Xfy?`ddAPi`{y@|7m;wGp&r($B_P0r9mSj0Rg(7Rb-ev2L5 zy^4TEi>tdk8~xFM`;n)PM}&20NW?Sh`N~oEm{@Zj1(AlacTfg_omqFoA}&F~M1#9G zIfdKnf-sKUFu8$ikDia*_i3R3?Ksc}5+4khs`%(wW!g!4W#KJ(|dU z%NOr2`CV@jPZyNEPfi>P%k_{!j;LsuJwW(iNSTiHX5oB239vSEKZhg>H-Xp#13hEi zT6x^}?`75(jKF;#vTNhNm^AA@ZzGD>UO6HG&o4BS*KDz6eikH|vsqtw1_%KlE80Vr z59UY0TMreX(Vco5DPTuJ9wS~K;hyX=iux<^-nmmJQUT*MH=L4m(Y;;QSPysH)rRB-h%wlEMYI^+^@G6QD8^dwFVMsah#M}d z@;SmJc|u@i9j9=A^mkmB!}+LyU+{+hVs8V3u?dL$OTnk{KV9~jrEFMLO58R~8NpX+ z!4+!|0vD~oj<0gaal>#1DICeHHSA>CX~t*u5f4Lgag4LA3q;w!(!X8D3>awUzl~a= zGMI*3s9uhI2M>rNe~1J8v5y4q%^?7LXGXX3RWy@U%`;zpSN=td^r6vFyNc}r=_r|y7Q6QnYPu(cBz~Ae)mW%4us`hDc~|WaGM_%CHzx;@ArfkQh5dYQ^iC22uD>E=Ui;O@O22irIS;-ZeU7*Q zj`jzoCDvMyMG0Loh$#A2_IS|I-4N=Bri@Sfy2JpPzF}gX8hW0ku&}}^S0|^g_I6l5 zGkN05ol(gt^r zIvSm_vUpCRZ(N4nBb1dplr|{gx*RmwEkh;3{15;z<-U2p3YnPrn9OB4#448%4b(WW ze|Qe0T?n3z^a_~2^u6P5pYtpG(kW#8tjp@!Y8hnM-OW>BOz!GV8gR<&$?nCXC5yAK zkL-Vp`{URID6a&w9%v(smrFXHqM^wQ^NwV0;Br4S$Ht{_1Ia)I6AOBFelmcm%d2-M zq4PVvSaWbHXlQd25p$?{q`ksf$t!5JqBy%;zE^EKF~gkwRb>r#ueDN{ZxGtjDy_w0 z1gwMPH*0b>bsk6XY{-FF)kbYBghWuau+uQO&{xJ9&1+=~34d z$<;)|%xnB@slX}Y@hdykU7oYwnlNyXDKFML&#Mo>%`&4+rTUqM*7PvL>(sk^D`ddl zOU>i=cuPYUr{}7|U38q-qJ$oaz3l zoE%LJw=F`2J|9GpLB?R;Yqrq{LPHOqyA1{)Y3AnC=IwN3QNIkWm@3q)@o9U;}?vKjau)w>QkkbmmgbUPsE{I#|>if(K< zW2tsYYqL6LlH=IkvC>T+2sZ;E2%7DMn?diCw_JbfX(_wpfsk3Y;k^a%bksXLHhad` zDV_cOqz$}<;PL+GF+Wiwr`0Y=$+gv2a-q7Yo|KN=CrS618>V_-5hIoacMGrl%!=Wc z_3@s}Gak#Es8fNXw6wAxAIGsL87SJQ47lmh4a)Aw`m6z}V4X+KNb(5JviD$zrpOPp zN!T*O!B1c*2?CLYBeT&Tl9l*R z$jtJkurkoSlb-Mwpu(hsT~Zfv@?tY>;Q9M4s{rp(pIhHeE>-ZPD;7h2o# zZ9r`9YE?iW%?RV!gv8*K!uYz|_M8e2pLB$4Pte9JigMjTk<8{-9K1|twaaH6;`jU0 zblK@`27ckOd_#BTZ@z&Xr()b_;G5h1D%B(d5(l8$@UG5le}bftbohQ3C?g|FyJX?3 z%nyP2CEkUIE}ff2`)Jk=0?{Ey2LpQ%p0J_lrM;(Ijc)5V145)|UMBjNM8<5=GUuSApX^x8P$Jj_U1kYn>pcUtuN3 z6Zq8XdwhWpi8{VYUjz#5lMR0@ZDuwetzA4qTL;Ol)GmpDAX$6o)xz*zL<$TL=K$Ef zK$2#^GEbEe=C>u?WXu$dJaN}}j=Lk^4F2A8{=7H!OVUfww^&=Mce+@!)#f#7&}VG{ z7nAXfL3$=es>~16OKz8%Fx>ae0lE6VY1xJ9j#5{LekWlWmA(%_c4i}4s+9=BzDGiU ztQR(^85(8|tKO25kshBO*H!UI|Cat!`zO9o1(0X_AAEpTnd8~=R;Q?_(z0^f_Ohsj zwYJ9KTa2ehkH03L<783`()U$0jrk@u)0V&9J7A7TbKJRY3gL#|{AyfzN^H)dU7i0i z_<-N~cT&1~o-L~xM^ebt+uBhsA$7*MxTE@?r&q>80bq~I3Z^*bKeAT}V5YM{%Tj2Yjzo5Dunp?n`Ze0`4-GNuO!kIXlMDK~|H49Ph5T z&xoKW`3>I-Z@yU-5Qs0-b$kvWIz8^hH?>LT+P;aV1`Mpo`OX4Z<#20F_Pxo)e4|Mk zUuQ$HcHJ*%vBhu8H4^TZEr#%jSfqU^N4{-}zD!$jxfw0{qjf&|Cj<(SkwuG5LDea> zi34Y+XPS-f972swurFUxeFf+>w-tK7#Fxp160Z+xo9;jI4XPEd)|4zN6;8Q%61R#v zxgIU$)EsEnyaaFw$g^e&0QtJ%vJXf0mkaLiS}zN_JuQZN2V?hBNxyy#8_%1{IX$bd z?|S!6)ib?L@Si`X-wB)xazN#r5nHGJAk%VcO zJn9J$p$!sQ`O0`$bizIXkc~NW%4dCn%o9`AXd5-#!ieE@)P%YthoHN&NWyaiCAG!D zxohJ1qk4oM!1JmF3Tp}3x5(1T1p^i7ylokmUF}6o0mg;L%5xxneSCRp)TVpIzF(}~ z$J*CB)jjny14UnfhMFUwb-L*7?n;|-l(5*RPYjc0^5&B@%s{;ppks3BUUxD&RXt!l zuBj9`89Bp~3R<8W%`*yCio-^T9)C5&-g%i6z~kJ!y6@u@kA6`J0=o0shr3`Z%C7*| z8OS!uubyVx{~xrLg#Xf7taXIB{+F#%Hzf-A<|gTaavGtJcsiLMEXr6nH$4s^Gye2x z>^t0q=yw%PuU9i4fSr)@*diK98||FzbZgaX2C=@LIWQYa!P5Vz|Eaf9Mn+^9?C9?1 zP7Ix^frKue+}Bu>xL(-yrm!wpeGsQmuPv~aQVCzGD zYNKsyT5Y=T$j`fn+>EWOR<*>A&4#f`D}trRHMD8#rJ*@y?Zn#?HyH7N@T+Bj$nsvy z6Nu?;v*@(VPvQV{12pP)Ylm9{Rf@vx-PR-|+QdD?>V3TN$?t%;10yz46lr!=R^V;) z^GgN1gw@+3bNWB-SsWifKQ>Xmj|ZZ*mDM4jO; z#`ZU&XF0?6AP_Al&G#M*V%X!oz8TGOGg>SxH$N#P;h|}=d3WHNz?@bN%Ysti&PzAv zE|Lu(o^xKFi;RY`8TY&tw9}VacoGi~Y>X)Vg*kyVdqi*k1aS`V)T7$oC)WJzbSprlU#fxqaP)$xhLFV9>zPu35wZBlx%ffHzL zWxOLlRFJ>{?Z#n`BK*;TQY+$Xx^D6saNIqCEcXFCmbnuq;rlI~$!e?L3aKN)QSCs> z;nETm3oeBry)t4T=x^EcX^UA${a_Y|bW!ph;Cfr{hb>fEad6v;NZXcxv{UU`^xx_E zP;RqqoOM@;*taF!7WNNZ#kO|F%)Zd3(9@A^WhVL>FA=EwDW@vq;{}qQ$%@ z;IabLhfo2)m4iV7hfScH0US3tOb>M;U(v;;B=o9tJl!|CGoH;M^)ZGoCT7Un8*0x) z8wUhO%Y}*`AZw-zYU=9^;70R|6gfRH*bIf6_?ltul|8ChxxM6} zKb5XszMp|s0?dNM<3B-vQWheh&z=1Y`$ArU7y>S9MdLq!`_&vNP zW%ZU6u7J87LLkLH7^y3u+3HSR*?OOjSl)Agnam}>a_I@Ja9QUEO85?@UAfU3uP6lD z%72^mGzlfo?hx|3?!0xgAiR0@Onu$vx%fgCkU^)2@cMrB>aI=V5sFYfa1AOAX+*Qp z?m3-%T-24ox|EbcIK4^Cyfwy_C*)$%9be z&h_*(Y=0V!(pFDcPp@dMEIJoS$msU!Ha<;Yw!$%F_|lhE_p@xa(Zca+csg%1(qpLu zmI;=t>s*b+2xL^y5xjnLF)D(BhM6N1P{-(8>wGYftDIBebdfxGxE${z?n^O1G=ojSJ7OTBVRqTEzmct%^^= zMBtA&mmsZ#9PxDeK1sG$OHUfZuhR>hDyDX;k*R~kazviem1~Hx^%A4EQL^tx;a_~O z^8H}C9sb(!^6nWX9i1aDZQSpn_dpMVi#x9`Q@f#GLSMEmmo^j~=NLm2MIb~K(^!+} z+5soq%j=5_Lk%8iD;gm^`2H2Db=Fq9kU_93SnY>3>w9iHyOV{21v9cSn=w$TE(Sdr zsS;xxeVi=mP$VAeGua^UNp}Vw){g&gM4Yu z$$XP$tLvM(*`rdyg71234xT8l9NVK?02@o=0_qSZB;~n06ZPPI`1Y6De6jD^YOjU2 zoM%h&@w13E;^Q`)C?yT0HfG(1aUg*TD2SCKVs%~`htzoynbnpcE3xa&eW zvEj>k|02xs!40Wi@lo+=_#9uFclz&ero9_?Z2Wz7I&U>LLUfb#0f3f9pqnQ`Zd=nD zuC4R2XME#*6){(XlJEC{hWq)dCY{mo?>GR%0*8Rix}5b5VQW`XGAQ|*Xa*xk4H|01 z70h-Rlz-;8x|kt~3ug?IhO2}Q^4ALUHx0xwt;6O9eT;AXiA^AVkkHH~Xlh zBCVovZ93Y=o$x*R>$~DtygzIYe{nJV-?BY#9RJ*#^XLu~235&e$fgA5eE%LF69Y=^ zWMJ6mwfEziuUY>&^#Tw7opoSj-!U6!fvBSYc&9HP0k9K{DxGBVC%f^OW zJI&4;gG6jk0SqiQNugtC+*lHyKOR#~ zvKhGI`rPBr>)v9HH$5j}9Ux$kT?z4j!${$AFh6Q>fr_{ul3v{1Gk-R2BP`mv;F?Zx z1eiv>yJx_y^0ZdmMW*L<00Z-QQC&IoOM!2;T@dz~{=o7Kysv&#;Y2MEzvFESS%INw zJowEhMcf^%8^;dA%S-t)CO>3%ZQ?X~U!nVIq1@t;`Bq_cVkP zi<@e%R;boDn^p{u7UmbprdXPN(LQRnZr?9Q}}v#ppj=fJjr5GUpjvHQTxH+(wcQhK6s01F^Hwq;!!6uXI?a z4yvsS<-Syv2u(GT}CS?rhOM#&MR8araek;0q&5?u6rA~>3 z`H8SLvz%VGHy1~oDPkp%oJnRO6PV04HlI8j!buv^Wy^rxE(fr@4`~cRs`N?hoWL8b zJBUwD?dR*P<;XFDnX9$6s#FAOJ$Lt+fTFxbe%H;)QW0~227yi{hyeIbsT5ml>znem z3cCHT`)alAM%u;twT~h;!ax}lO`kY`^Kt#n4X4%IK4S}zO`t4^FEh~RW_ds+D88)# z{Auw}k}rRg9N5(xMdM>-KAad3DMzUpmeiqvR}4aF)PN2x)KVZxA$7n70D(_RmN#(v ztfj9#np1&H%4HM(;G8Zhq^*7g%7YgF;j=yyXA8-lQFvvXqhdfM!CO}@VcAnkW7e!g zKV_-Rx-#2@zLqTSgoA@(>)etC@BqH^*8$oJT4{j&P*|#3i4V*wu(r2lF!6*!Nk=C< zH-yeOxK++VI&av~wlNzi!|Q^9P9fc7f=-|9|3+Q_5_|wl(FHO%I4FPrZjWjgT#O{4 zcTLgK(z3C&^@0?;tJ!tQyLbr`kPK((e3iAmqWb-(#(Qh;sQ#2vPQ>1}Y(+Rlm+XEVK)x5R0PI;m=TJuUN}W`n0p%yqqsQelT)=F(OQ9tA zn^cCu$|Ah9j9j}W~O!A~PvEDRFqXFXKUzFBwr?dDI*4voi!`K7df>ul8vfm?NOJM?tMoS6mIm^LuYJ zxR+mp`=9;v6t%XqSCLry#9M|N!y~3yU#{EE$6Eo5X$Mm+fY2pwp^R*a+<@QH>(5o@ z?yp%FzWFd*z2pk!7cJR*1D>8Sd%P_iL9E5&R`+bt^kXsvTDSdX2HHEUk#OZZdzzaU zcRN5V2k;SRg;JPQGP(@Z2qbLZ}x0((H4u1mZLCF!ms>Kq=S71 z7xBeR+O1EWZH=7xji{Ugk)Ev@a24KI$TDKe(r$VP@okTu4Bt`gOxIwLCG#}qi2ZCg zV0)H{H(FY!$QE3Ri9HTn)`WD>jn@_CXxJ^KWWUt&afTpZ@u#5l5^S%_=U9%nwLvvk zV)hc!J2pkF_Bp2?N^)!yTVr+G9P29)<6&N{; zM>A;Ypg>;xDJYhnUc0up=Ahc^HFc?uxZW~=k$K|1LT1P$w+IIt5l&>K`N_1*TDTL8 z%bh|-K+v&kV!TZLFt!#sKrjKxd`a*oN_0+4dI>9=wpyF2ksHdQjntn|xn1F$bfaj6 zxA|1H{TFQichFXS(Z8tQpz7SYixzWW{SF(dqX(*_*6%@ z3|=yG#Igd#SS94OFGz?$lE(GFVd`1n%xkeN#<0Zai2^oap%GBV3_e8HCM- zQ_b&hB(ziO?kK7~)#cKRX&l*)ktiWdAZWZoF zK^og~>)Z>NN2O3(OheFE6$hsUM(<^NaQC+ixnU^F{Cog-+OZOn$HHDyJp)ir0QU47 z<3Ik)UwzcE`zN~#FI5y2y57DmJ8Kiem1WzpsVzh%zoOuiz5No4Ue`BS_Z z&~gnuLYAKs(~%qF3dE2zcyyVJ@8x1mh}hJy5iaG;lf?I`j;A2_2N6$jX{n@l1pBmr z;-RvDsP*se5?pE4D3;DRQnE%dh_DzOOxx_(js2MLNP?UZ^%xM9A%6vEvy2+tkQotl zTX*CJ5jc9W(lYDV45XC!_9B!rN9Mfhv3uJcb{Lhpf`j6OrJ9Q};oLc4mT6VNw#$9M zclf8yKnTL*KZ_zdp_uw3tEy=F003bixDU8`9A^oV0E?*c>#s(q9Q)~}p{5bbWs@QZ z?#ddx9_7l9n&+3vX|bO81F~Y-`TUidc9()81k=p^6tBb%-7RL}5Ct&~h2vq%i+^a? zK@u(JQsT5{RNXTy%Q* zZ(5~X881!4kSYcwWk@`VVZVUA!VG>kxw-%@u(6s04mSqY-iMoPvkSxjBPF2_iGMIb zBJG_PS+-*En~^RITl3e)Or-_nXwX5T+3OT$kPZ2mT5VNzF~4$#OMHLL&8-?GmzhXh zpHv#xLg#fj#58T*QJtv_J+xH^L4!{qysr~>%fB%CbWhU1`G;El2b4&p-R#Gl zy}h-eF#2lOn$P!>Lwd}drzy)}>4%;n(XkO0AoeBJs;gi`v0H7gVcOV*45{!YAwGK+ zUzs>HV&5xzj1|1u+7=y9W*rS$-rV8c@f5k^#Tgi2g4SX-$oFm;S6v7j08QTn{`+!Z z_u>BSekgyMkQEv6{G9nLKMf0Oh=>Rmc|yF#@W&F~5X;Y-VcrS25@Nz`H)4f{Jr!%> zyz$7|>g`EP>(-k-SP~lF%Uec8uWf`$WdcR;JN%?51z+$x1#rE#MjDu6jy$~lz- zqA|7wnvnmI`(?Sm3yu82+&X9g-=B>;KD&#UP@Xf$XLeAxFt|1QT`}I zQyVP??|;BF<=a31>=ES5WmVsxL{xzC#Rj2&dbPay;q@&Sg|`e!s&vQ@`Ye=(H$S4? zVcvGEEUA2W7pKf12c*CG4H+3FV9Vy^F8xMoP;0zc#>3bqzO)=q@buQ8WlE3o@Wd>> zDfhs?{$5X0tQ1xJY&dw1@ z+AF}01g^*tu!9p2sor^qYG&AlKe1d`^Ukq~4jn zsib4#PLA3JOYGt|?Jbt=Uls7pbHqBQVrt03?_fzkScN z#}OCPUXIlQj5(4XbW=%5ms_FO49jipXHUjy2kUzjSaCW4adHg0Ja|cuT`|%&hhhH! zH^ma6Yg7L}YR^OT&s=oju`6+@aMQo6qUGE}N~cuPLWah7w5wu+F<;z@$Le8&5nUcp zkY*J0Aery=xiq*lP`nP&vnb$MMe+y`rWT6*6IYNaZamf~TWBj59EpEB3{j)uwxa#RcXo@#TGGe^ zzno^u0*tCgioM!^gVumbq^f2TPL^+ua1Y7Pz;xE58Z}EZH%_na>amqRPvh^$`SA@8|P&Aezmecqk1=`mX|OvrL|01pQD@YAW9mg%%FV-a_? z*XXJ)6N6p<2f6N2{xlp1#EvDU`{S&r80 zW{8?z7v%}Id_zOqmRN&DM*chxC4$Hf8r<_MF=krvh<*Z>>cL9V+B0>}Uc}%XPr0%= zM}oF0>(^dL%NBb0#DTPg{@2)={b>vTq?t_993h#fyv79I6Etgzk-gfPaMeULiAWb= zgX)#E&~rZ)p!_O-K9+p(i5SOhm8Azf8s^J!SWJq8L}}P_+tv9k4ug_%W!(zu95Wey z@8QwWZ!JCI2jqx~IC}i}w@H~a;F2`sGAo?Riq!SI5~u!u%|r%q#F&=B5S7f!pB518*A_w_~7nr3ISlEhPctJO!f4#4qw}>J-3oG*Z&ZX&1F+%$E5$sN|EeY_ zS<*W847kGg{Vr6$v2W4v zWtky-359A!{>mafMa|3`x?X5U`3(1x7iRLsyaxo7%sT(ajQ_PSUGmaqf7>hviEzTdQ?BS0tkCU;N3M+l*QLTv-b-BuaD;9~&PL zinFRx1G3wmz3q_-N_9NmZ$nkiR}z)yTu}E!cAR8(2#1{w@j5ZEE-!a z;SVfG897X)@Oz~xLrOJ_?Tx_z>O{b{?i3_iIO= zMR;NbV|r&wB3=9|QS8QaYuR0s;2b5u~oHf;f+hisXYl)>ri?76}A<%%2)Gs_-x>dIxTIwo@zxR7d zba)?E z{rCM&dF(pzG%_D=X!EsLY*Y44#Lgm%*|tPb+8y^34K*rX=sno=n={jm5NVgc7hg8O zvsirN5PbS|D*AG+nyUTu)NW-x2|Ckgi?*>L9>@4C4#oYZ>^1AFvdE;Pvr@x(EBp7c zeN_~r0T6I@R*O}A(dvL+u2Mg1x zG9AUa)07Fyw9?FLs>=s#kr_S^M1S3+TBj?FiTort8k!-XS(f?{Phl4+lg*0_9uU$(5c+VXi~(Q;;<%>RR@hb6fa>|hnnZUEDl43E(QhRJg|bN7peRYChE&f^cYkJJm=xXEEe$<$dS1$4~t!v zYI6b&>Sa}V?^SlDQCto`C(ykuNC;3hXahx~FFlJ}30CScmr6 z-pp5DatuqcT>Rs9AWv35%Ul*~{ha0B#-RS&7?gRfBOF9jRD(l-vU9W@USzE+|0U`l z?&{a)%o@#v$&>ag&EQ=KGS<3MD9r&6j*A4T<_) z*Oh!e>Tq(~CpwAdhB6MMbT<@kyRdV}416*<#^6j_D(wj+H;_Rog?F}kTQgju>HSy{ zBU4PQd=qZYBKM^CG`k`F{}xQWK6L}#)K7PQWQ!^OO*2dL}Oo`237tf>bzi?f)=~g#i{IVnYv#u=><^Ikukue~hyL94bJ<-~GGeNn%?KpNFqa*%37Y2PN$6bMaiBdB`>z8WGk9J~S z$SrmU?m^#b1#nO1TVWq z@iHM|8XDK0s2+>6zmZsP*62*IRB2zC-I_@-8vTUD3ql$Fe2G9%X zR-yXi>lFK&u|AjCU$5G?!>Fz@W{DK`k+mIH@)^qL-g?!MLUl7OBsrk1o?o_E2r6~C zMPDj)Nd6fDP5$S&smYKmi5=|u*;kSS*4MKZ%D!9om)u_|v>r(5{S&3I47y)7RI}2Nt;zop36@Rch29q*)5LidXgxP_GE^OX%{c=MNnS!tI zCs$pI1c4qG1_y}9K{#;R&1Y(Zfd*we3m{6zA@AK?wx1N|eE1G$tK~AsQ>XXqw1|WK zaNAu<^>xMN>**_X**f7HLa6v$O>akj zz)C2!!3@i6nzonZ`K^4JJp<4z;;LzLc4mo-QxH{Q7n3YmVhT60naQE&v}Uneg*O44 ziK4H5FnIVc`0(TY-SN8@jGyHe9z2eqcJXZ+<6~AYt9H@l=i9=sR>Ptwd01Oe>&=*X zKfzSlk|b>vmKvO@%+>^4@0ndb>eN2(dC%N1MzJp!TxmV(~jxut}> z<%5c5CLX1nX(ht@z`3)4^r{sFZCkQt@NkYKQUGOw!(jcLn-v_BCUO2d76%qb@i==m zAORYv#kG<*H`YYSSweu42;VlxeEgt(eB#x&2%l+^`3Bmmtnvc@-bqu>f z&=t}Kf>Wxt+^`9mj#gjr==c_kmci;rbY{3Ta4ow@Rqrk?s$g`iBTmcFr5{E@4N4Y$ zd5a)d@ScTXHsSA^TkQUdZiYiDHK}?#DGp}{e3=be(+$&aJJf)8qMfgzZQb3E_bfD; z+2}|n2iKJ8g0j5gBVcN+yjQjq^G_^LoIPo|sW0`Wv5SozDLBEc=tRwpae$Cr0=u zOP$rJ;{G}b-d|LwFNujX(kc9t+`{Ju^lh$?&N5qO!ADZkd2(Xz&$NRpkZcrlx?Y6& z{?F?`GgHG7D!jq|f)scfEWn!Sy#hmLx3OQBJ-YV@tJik@^*}$G{&j|_WYN;YFyaR% z@JTr7z%l&9Yr%$>)`?spJK+9q8<1`mvu2*Icv%`DaRM9vH|Ec>$l^-B*NR0b{W378X0p&V;5VPtoHVitY*h5m9~0saEA=wefKiD$nAS$py2)PezroJ)~oZe zjMTf;wPep5+?AY( zUj>~_DWCO4Kn-`K?*-wI(z%~w0 zgNfR{l!jBIhN&UZ3B)EIv1_n|1-8`}ZSyD4TC;o#+wW3WixdACAeq5TQ}w}XwyCuA zuOOUz|1}5Uw21xXM`38JR-B8KwK$Rm&g)35n;gqjFIn1aF|uRRAvzt+rkE+Q2!pFI zRRuLE-wod9=?q=Yj;~^MxX=7u=!sgxKN|V*waJz0I3gSna9xnFG|NjIfiAyVeA9^T z@2L5m_Pg*8#@((mJ{PNl#De{gom<|UbCUUJYVh<- zCqf20-Xx~|_r>@*-6!>+`+*9{%i0GWL?kQd0o5vKpKqW%ZrSF(1oY3>*weiA?Nv1MHW#!UzbL!Q)?GIh6KCO$KoIDc*NKtNj9NUr}6i_5acK z)=^P@U-&4XfRZXIASoaq9nvudDWxDFjZ)Ih&;z1`G)TvYbeD9DfP~aYcgK)3bPX`~ z1@!a%t-J2J_pj?(xL7*#zUS<-&)(1T>=P2~^UcdoBlDIFza15p86nt;^8R|g=;&oE z{_148VLZ3gSV{lv`KK?}yXWFxCD_G(ZCHq@vZyKh)LHtHFg}Oi7b7DEmuL@{JBP9zSx6uOG{9Oh$3 ze^<6}*^t2B{|#-@mviRMs~_$nf^Vk560alZ=YF1l6W*?{Em%P`oPB4Exx=g(TT(N) zRr#qfA81B8_ZM@_eZ*g~DUTn05zhMG<^5P&8ufleK&Dxdyn*|xVdhZ8CZ5SMx4_;) zUdNxjs?a&|Tg_Mg*x;nDJRn|QbV2>@CC8F}l&`NmNg|fZ)5j!Ss!hqdBZ(Y-shV=>NtXzDs-}8WQuSL09`{G6h3G%e&?c;)rX~ElZ=&Ae52% za!E}_gpk%PT_&C^t7i`O6Yr@0fg6TPY(=wt=i2fpd@0v3j5z7q^&4biV&QJkmNW;E z123Pb{P<`+1#0#&#QgY`tuy?t0wW<`Hd=< zo{Q5-&J$;n{L91lJxXN*%*tBnUh`?Qpz97p+@rUWUsL~n+FST?qnnqkJKN=WqucNDeyWxeMK#I&(Zv>v;VJ(;YD}( zw7c0q*5>;wYn4w=UleO_$!k3~^7}hbG;CMzlae25{aslbwwXlnF*ZBzI3P>>nZ5Z$ z@ZW@gujeW`u_+AM+o$a`TH>Y8ND3c}U5$1w-<2vC^iqsbMC(uThBhDQt7BXj6;50x ztm&^$vr%2TM2x(>dUuf5`P~m*aZ% z)Tj*A%g0{XLq6)I$wfN&_g%J_{~x~?;7e)H^L`k-A`NB)4Qn8+rz;)bTMWW9S0e;ZBn@#-NhPu(rp z$VsSPzU1l15o#+DrrPH8GPa9!Y87+17tgAy+S7a7qkp^6O8zoV6IOy$0&5;Z%RFafurh*GBT;8M)Cl8_f#y<17vQ2`Bq&<))j7 zA10#Vpq3xulCz~~+O)IM&F`CpjrtwZn)=NPaPcM;V3XjF1|xLX=~*vK1_UGko3j?% zPQ-Gu{d2rLse(&7oRI83|C{5;{UN(XQJ^LCsMlCo z!X8qcl$6w!)0Zv`KI*78r2MATH9Tq8<9ywyvQ?S+){Q?63PP2r$)+N`y6A%%L>ijH zwbKXq+-wJ97;tyS3Gq-_UL>sQx%))eruhCTzplrC^XUfp$aaTXc&T@bJJz`yW~xBD z;fp*d^Z7`a96RCWlUYuT@Vg9sLC1AmC6LjgALb z^ZJd*mHwa@mgjMIbG&k4GdKFXwc|3}WFaebZ{5T5hxKvi(#Cj2Qey*}i3ag~muq%S zp_tXVQC^LuuR@KSkDvdG?nJFMd;Unla>jzPoVzW?n`Jw00wZN%!G;Bct&_Ah3Z!DQ zK$~ZunQbA_sGY6}2=UVqWxX5?&R`j0s=h{RCL$Qx*W>u`#i65O{xA{cllaFdr6n^V z0~KNFtdf!lpw9xLBbG)6W;^Y0QV(@bP6K@>gQqtlG=f6{X$6m(@@X#1}*1PjRqVcy* zs(E$Yeyd=`KXK;sa^GWMXJ;R`?HRAY8Z6c+?lyv_3I)te9*be^?CvB8nB7mRvYF$q z!aO#|EP3RAq#CrmWv#Qd|2)-e`?%U01Z?UWuN3j$C|En)7Simi>;YDM-PD+?0-weZ{CgvIr8fHU+1y`Mau5PSRM$TF4kjKgxcXs&@Y`!-2HbeM$Y zG)MCsPyTR`>EH_D`{tO|7{}XcAG%QSaEpYYBZuWU;6&d))$fe(QpJ71Sq)w4KUtt~ zLm;kxKa6<-?s*`vGLTLlJ)PA2o8`dl1e2xqmSTR93>|koM#J&Ai5`(G8EDpC|C`@7 zWNxQi*3Ni;?Owy*RIE;4)e2x^)dXOpR-7`8D4+S7u_-*IC6+Tu0K|I5;vdq59Wu-)E6zH%Z(*Df0 zexFmDSiw9tz`yHsS*PI?)qRqgnby0qd79UlKI9?=9{Y zH_n!GOQy??*zB$KMQb^lmA@O1xzC~9vE6`5pbvx+G{xFb7d`pC-MmRLgMh-YboG~Euo7=!L)~A+t(-atHr33%*_Cv|AzT!qX zcWqJf6fib$#M(mO#+vx?hT4x0!^uD%1$ssn4)O(^?ZWF1n>-ZJb`QX3lE)ge_Z~?I z#1QsUHv^|>wUqKtA$JxO*th!1?ET^-`Nbnd|BuZ~F;uty)jYv{!+J z6II)LD`swUG34?J!B#1HnJ@1Fu6cayf%Ue`^RWtSi(}3%{uS%>r|#~e>q0gFaULkw zA-xEk;gV;Mgx`*elbSV#STeqJUAj zYTL?sr!Ys+)o5QxbZn~3Olkw^(U|8*86SU*>rui+FWhL5$~FqPi4Oi3khWaO6b`w1 z<@bRt5}74DzqY-?RK~M`gB~W&5Rek5W7S^R>GKnkm3CZBT}66|+0_8(H;vFl{Q9tA zPHlS(2x+qVWhtcoNZcLjE`H6Q_o0f=Yeln(mG05d(EfQp4tqs-rRPA{GgW6c}ckyE~CT=^=vH?9Z5=8#?9pDGYQP zGwuP8E&`ee0G$l2g)Hhw8j*l%&2ZX91fu8yM?S4aoiX<1+`EGQ;cC87NGq#_X3p z>(4Fkh70ss+Io^c^jFJ22|O!8ysfjLp%SzP?nsC=HZ^71ta9tR@8IC@VX)*OmfP{M z)l6xMXobbK63Y=ApOrJM5S@XX=LtGo=aqWDfc**E%4-VP*mLX9eJIO=pfLK^zIOBz+6w7 zf!y)Q67rI_n5w*2N5AS(wmfJ#T5czzt|R#Ud>vD;vb2IQKm<90pj(R=$=4hRA&FrF_9?)$lWK&`&;fviJc3IABN8)168%C~Rd zO8z%8L6_+H%`=Tced!rrmXg_ZX2IV>W?Y#(sJA1u*ir zvAQ%k7LGObjh8xOo>+`V0A0-Vh(pT5B~nk$#XKc<3GaJ11ddl&4gfakrQit!!jlYI z{H&REXJ_Z=cQ`jAMF#YK=asp^|TK(Dd! zhQY}CLfcl)n(h2arA1#9*~tN;ghWHfsI5wBM+mp1(dw7X!6Nk}WJ(?1`xUK>jEt{a zgMgctzaJu~w3>Q3z^o)vc5?gC0sdRqI|;x&2a7dfgN-rI03uU8+npvZwydO|A{=x( zo0y5>#+$veD=_DD>un8?+cH!)Cu{sJm_c5H??#+K&tiQSm+^8z0`)*GvAMbVDGY*% zen+0%npk0(et7TxeFwx0rkfjbXsJ=4D~}*a=}?3(wvOm8e`Mz}Z6k+Dqr<%SAJMv> z-j;!#s74;k!cOiO^dzXyKVVT#`DJM|V+at4z7Y?)MtHFeECzad$|h(#c4t%U-Td{o z5D5RcEwGbo68-0lNZ9h0(aZz#&A9(hauTcuk1|mZ_F8-;qwgsRYFI@OQG1K3NFF+s zo(b7Z|7z7VXp7kO=Ra+ZOm^-Qv4@xOSx$bFL=a1+;3~xJECW2SW$r)9M1pgb25m^i z4{iaxYzc&^=N@<2U()0~m1T5YJ2pGo4~1QyZEB}A-OyR6KC}fpdZHTp8pRx40B}9k z8CrGzu{7NP0YLfd-`XMtT(@AP_MJgz3weoMYomhJHHc!&7fYQQi${#!o?K+v%JZk? z@X$)I)l4L8=6GZUVJF;O2sz@!)M9A;i5?LB{J%3EFLKX@h~T37Pwh~*kI?hQwG7MS zR$JRNuL&0bK!R^E{EALboo@`lya0gn%4pGx$aBaE+CWZ8!DNTZR_#$H0Svv1szGWB zZS|{h-x9vg=9?sB@ypE*Zn!tPeI4hSe`-Ip6J~Km$zlRxF0v?FRm@Dmckc!~d+Q-S zzGE0vUnSVI$4AcRbUvTkzIUs9;iHL9frZv7QWKW;?VC?uaRaqAoru--3X73t+r#q( zF&)cv|Ii%ubY{!p!pEfs^tZlw#MxNQ)cEPuxkUdzv!&p%`yKzP3&ZW_OSLtT08G4qCY$sF*tL~Gc$`$NN6%4*KO`!>Uvo?TBN8gXw%*C z?Ao!#x#zU^|3T(wc>fM^ikQp$?oUFwI(35W`yyeLh2ak=i1?0v@PGFL4oZ01i_`zv0PA;jcN+p<`NxSiJ^I|p?}{l4Qv*kDoYMd?Ah5DG`ghk<$#GpJxh+)Sz*fD320+tv53Vp!B3&6W67Gv%^d zn30{uYq94je!2+x-Q5jD7%n0@PiB^Vu}DLnj>L+{mvjEaVO%cB?)z({rb?dD_Eo)l zlrC+r&fVcNs{rIkNT4*)ADP76t==9skOD! z9jTXMXoqQg4xslz;W9jc8gU+;-Yc~3x=+L|o-o3q^svxEU3p`u{C~OQO1%r z^mZoDSP6GdPRC5=Sg{wy$qv2%cpnE*CuErvb8#l;11@rQHtFUaa}1r_FG z7xW$eNCR1<2Lu&{O?6e(oYF(_wHHe5g4Q=aH-{wf#TdRMII9KcuUug@rd6yfS{-OU z0AdnjXRnQMvf8Qv@VsRFId(QwG;nXvN%V)MN`T8|&ECi0PtH3= ztcyg1w@-C9fsva#TsoMX_cVk*ST{d4rKe|P>X$X7HiaLLh|Qr;y4z*IJlDMMFf`oe z*FACr+7X!92<_xiQqu8Wxr)eAMs)gSVPX~A9o=?|e&IWB0j$epwS7#5_*hF zI)gUsM~@Wfb#dFmK63(>2%-%zTsBB40n0(-g=HmRKWS8IDy{WQrI8pgRoD(1JBu7A zjBb2>9+GlG(q!nfQ0r3ozfNDK_S`^n1FxWswXr@sH?v-4tP235Z^LyFu7F6P5y>A} z=e*jyc$jsI;PZ#R0+9Q9+!`a#0H6Q!_ zp~Q=IbsYKjU}LOhVnC9<;-$K~69?Kr?5~`~)zn`HNC>NT13%=+&V?4pVhi84jXPT& zqXtwWA|szI_=k$rHuX8J4kkb(>_{D#dmi2;xJ~dmp5K-vT?V8Q&NY<_aP}s{2r6%P ziSp&%Wq%!;_vg*Hi%WAK84>LWlH+cd1+kL57vYzw6#T|paXnMITf z(Q}9Yf03M+y~g4=uI2U2U=eipZ4SNKuVan~DFc9}V9j16ci;!_+Z1nmf0-L0(rZPs za&xq|S5IW}aO-SuZ8#bT`!nakns*vR!SjoaUh!MgXirf~m;a>GnW(E2GT%?g{SNuj zE!pvRI985!CINWL+w8~55g?5O)tzsXb2TnI^sB02=jfl^aCb)i2K<+TCR-(3|Maji zpymt?wgdPUfLU(@w7h}$E>|YmnjFo3*LmNbXHZN#qU&-GP$N*-UQpmz$(`OGht)g% z?DK393IpG@2$AQzpreTf2;#Jh8}JOk-r02CAGHN6m>W>R2?lovK34;7!|AY@*HyF# zh*pN3b!u3`071u^+>L|S?}v`?*&VF{WL5i#b*~?I#vrv=E0<=bklpvG&o-)2mi6v; zv+S_{5NOP0`77$nMY;>0I=n$8Wc$9wpZJ2u4mLdGdzv!tTn)g0_bqM290nCp=@ui8 zD{*R!>PYI(H`uL4e?*$F#}3TqxKmHft}vXnH5|$Gqq#*J!MK77 z?ABpTwPRHD>Oi?raj1KOfaOSu{VsFj6PxDSV?zq{i2BzF-)Uq`;Q5NPi`A_Me`kfFr#{H~vi#Emg3i~oxr9e&(92+G`Pqg>1 z*ez;&1?f-p_c!|!W~Y7JRNi5q)jIzd4ReVu-^}ENatE(U#jrgNfgDNztz&UQ>Gxe# zXXI5MUwuYB5J@u*_yfgZZ{00_GIxr+QwN0Oke@qVH}Ywv5AzLyLUjsCnSE!{qodfuD4 z`Caal;h9Gu5EM=4<+CfBj7~{OO9u4(bQOuVttK-EC%<%9G%qRn)OZ!+T>{>@hQpiJ zarV}c_s6c1bj(#1=VgT>g5=_%dScG+_G<-B5q1UQln$5xl*m!E!gW3qtF4 z%ZKcQR%M_~sjL2c0$HEJMI*}Va5T2@m}7Q~9Ew;ZJAs(Z?W8v2=zDIOoE&TzRiAS1 zt&9Q@@aGYDCLKU+=M5hJ;fk-HE-71Qrdbs&Fw-IN0%n3w^;CM0R&~6NF6| zE-(Nfj{!&T+S+g48@YAk`e+S%0O)*EMHd~NpFY$*Q*iDNkP_$w#F>f|w9T5YQT)EY zPVGikhFwz)2*+@u()x6v5wbnBp#^ND3Af~eeE~E)>=H9LEv?7$P05o*BjB2Ym7xkT zO(P)2cqMxdDGFdK=Q+EKw>Sz^Enk`&%580Qr3iYJkL@#X=14?W= zHvD)x0Ck~u<({d`0g`r^>85t$(V7C*!6M?K!B$h1oa2!8-V{YQFdHC)IBgDowiqhA z;KrL1mB9zcyC6&_P`;q>9H->aJ{15YAho@5ZU~q#dM2e=8gU+|&d_486r8tEo8S14VOG6R40Os<0$8N6IvSAVQmE~KoaK)ffk+lBB`dPt{V#TzbN|Km z5_Jc&Go&*rfZFJYfE$@9lStGZ8g|8Uo}SHMjHgbVGqSSE-Y*dp)vb+ROSSDjDou8s zya~jGbhUsmYV||bHl^f?+QZ9+DhMpv^)!J+1dx1A_BNCNT7X4F<;gsvZ%>SMfk`^o zi{Al6g+r$?1VC}dgN!PdGX6zgACM(kY9O6K0EIlP5B)j35ZG?_u>3zDDYcnAux3TSjGqG!;wtf=A{Xl;?LD!ujd85H zmT0-q7V3GripkN*CP8^^NunUap@4_;9j817JQA{QFV>d4uUm|Kjb$}j;w!R?h)Ri3 zO429yvc^uI}}0@WkZ~qf&YH?49(#Rgl27SAb}d zw?~2sEPSt+bpCKmOUnuQ=cbRHFg3VAD>@6PF(f6FH_k5rfyuU5vd$d?3bqwYsV0hDIaZsLM{iX|J~)H1a(pM z{sj>EqDHqUi5Yj}$(^yGav+HHCZ_UFK>Q!z2wwRVYoC1*9FDfPVO@OZUkdbrq8=Q*mQ?mpbgs(=Qx%LdIahdpX zRUI!1XKz+B)Wf++yT{_+O6K?#nnyFA+P1N9=#++3tPk(ZWJBp08U5OLu|L3(Q^$+D zlyBgjS=LFFeFw;?+H%W7E+D@t09Q1UexNB|?luKFL`3T<6KQ$v1loB~*n9QrDR6_F z+i|GoSiK41g>sjRkZs*pQc*EGYa^CsD~Vo(G|OjXXK$Y>z^_Ga9gg+EU+2pxog9kg z4x&Nm9mjJQ_>2gE_NdjfxwXf_Po2hiyz18-y68mcx^VCTWF(8HMu#{arV^*`dQ&Os$y~C6_pd*@@cEW-&|>cdGH`;;-9X&|PAjo6Ue1@_ zm+K`9u$R@cs-5Er_uax$K#&YPXs={oJvQO{Ny+ zD}ip`oH@WBFXGDmp2Xiow(fTtLmO@ww2`2fsdb{9e+x@M^ z8D@VnUN111@z4b}H$T76t<#(;;qhUhOh7%)Ra;4Gv$t!hhSzAU)(fAUn5y%2g(clr zH_1i_X+DTE#M?%wj9?SKUmVTWy9R%*h}MzBnkp`Mxv+3LBb6&I$Vsq0trrOksMXbu z88$mQj9}|UF42|lG@WA&(z?RU5spb*RZ-34Sa8M2J?PC3XRWBeBU~*32bOpC+Bfe zy~gZAH1-_!u4z=iBb?cTJ@DD+3Q$`J%V&3$#-FmlI7tC(>0a*F;Vm>TJwn~>-^loy z0pa=Pm+jJ{9iS*}t!-gZ+Qj!&;!PM0YE%#(L8`B+stV-gp=Xrn04_$(6gpC>w%`Sp ztwr|Y-Sjj+^T}`LU{P7*FpnjSHN?{LPmdDxVzJA;@tCe&-hr|6UlX4kTu`uE2y-?uyk1mTXS<6DqE=>=zX!|zQaSFk~Vsv+OiY%;)D;<{>evVuet7) zj|5yLbFGF&bt$9wIv?91_tDEJ5|#6tK|t*PnwT2#oPK%c4Y0?%bZ4P?-ZsK%R8Fa| z^Y{h+K!N=WnNZ4>?@L{kk!G1lZ7ba$798v2EJf((tzOJqymXC@Z%zhZHK6!U~3gV z^RmkCYLC|IAb-BYU1q|MTK%h*A}}ki4z`{V5(^-CzIb2}b{FVjB(P3a)wB?c*rU}} zQ+T}b>oa-tFwfbH1ltl8*8N&mx}@EAj*GCdCn>yK-AzU}x}ihJ(|gM5q<7KPoZeX`4Q5HmzLRxslRogC2L`ohoS<;5X9jZ|go+U=HaHIP?f3PG`J`;>ke23Q&It;|XEs{`F$e zhxHc=+}~`U8r=a*lUqTf@p+AS;V?Y~{SrHv#pApz8I_Xg39eb`uidA>EURWiKc^km$oX$J?U?#j@Z#*yZNUwIHXGeu|Au(#aGY!KX!=&xLR}~y*{5KaxyDVK~T+v8+cGUVeZ>VS@1I|)r=A`>T=#i%+60&0+_C~Wk zE`ga^pKdP2;QH}mD#`d-@<{0SNW5TVH&gykJ6jDYDX9#DA&Kh2$X9HJE~u3!H{&P# z9snDuzP$P{E+J}gO;!dX^z`CW`D~^q)hkGjSd2o%a{56MTM1Y?zJa~@E|Cz0=#Z>; zW;jr5{Yn~?oK;rixlzF}asB}2F;Ce`<-&X1F6-cA^r;dLWgrs(-?2wxpz`$D;Yn;j zfBJ*ubRvdhCJKG}4#dLD6Deq6d#G<`8IDBSE^=lGy*1HJZDRrbXhn_^+ihZ?m9JnT(wN^d6Y*Ei`NXao|d*2#k z+E=!9q8HzEOD4+(6-*@9a6617_UF(#VkoGD&Q`@*tyUJ~1@zMR)#C@|cE7p#w9cLe zHlSEQb~W1(@(XD^-vW4p1E1yEKelEBEl&MW^`{XI-tniDuQ*NMSsEwGnvVBPt4X55>$1S1Nt( z_EMND3k(b-64m;Qhd3|BBIDXGKWQRaJ4jAUvAHec=D%w6YkIfChJA6iBmAbR(5sq5 zaiW2jk+P`^X3Ls<9qc3A-!FW_u7X1ccl5aoZg*~%O09h>sG=5JtafSv&T$RZQ z>cT77o$v7-5?}e!hZvQ8`&F2+{cJQ(-87M-vC${F9GK1{hHHHh=>s>f6OIZUSU(q0 zN(P<2&(cj)Sal^|_g#~xrd*4f%JjP0%?kMj zOGcPoMXm+{RwQaU2(U}WP^CBdX3~Lh=UpClLz3btY2z7QPsXk(v*%JLTD3n1EfYSP zW{<-2ofzZ92qV?b#J{Gd*=Yfzp-kyZos+zf?hq!!@I{X81tsskj5lVFeG(mDR=sq> zPN64I52Jbn*e*(c1Fwz|U*o5zHhwqriKRs(c8=Jy(uM-DwB4ZLbrlw}z1o)LBL8lJ zc#m*o{THBFfgR?VCG=3+X$OgPnKxTMjo{@DQ_JTzQNVx6y<}!Komc4RgYO_iKJu5M zno1@3kAuzJ`ZbgWYQ7{)b}TaROKT*4pH#G{0N0F^X0U;|4+uO|V>~tn^Euo!^_|J6 zlq9Q4tANwQf|6}&-8Ti4`1P0fYs8A>m>iz3@9*d5Z#qE(oMu!OCl%m-IxgbU?b{-o z#-@1HOaAOy=W`aRkWFh6YROInS0ff|W@bi-4EV&hp~rZUM@E?p544goUfOterL$w< z4P#8*PEENNG2UucP34HZ!1yQ+QM;VUVa}4x*<^I|_e*O6_Ef+t*-926VyA14=5Flm zTYJ#xG_?d&v<|=nfVrjaShYaz0b}IyOlms2G*>M5^8o>83?I=+t_zhN!$Ic zR(;WOU^E<-7jF{LX}1}{IhSB`t|3%K^?|u$?fc+zpx=u>ObDY2izUc;sPNq4I3A+r zA5mknA&jb;W-}c=H{%^)uKd2+8W4_Z7aE``2q(HG4!|6FSi(b&ACfs zO0&uH8RLCFb|7(|uddh^X(%#ewn|A5Cw*yM@vkxWcMFTXQ{!SAy3*l|og$8AcE?e> z+i1_TsV5hcfOB8@XEM0&eW$lKOXH+&$c9xOsy0bZufdD)8k3>2qrUQpkCxPlbZz7> zBwRnu{@0V66-m^XC(&!xT0e$PZVDdL3>Y1npqpy@M~iJs*Ke7F#JG_M*1uqNRqLiY z%ezU&ZJaNyZ&4-AWUQTvn5w1J*XI##0;1;o2-~#MBPmYXSuu7h_!B)<;cMiyF>3sL|Gl` z`-v@zzJGFK6m}778KP20a*Pgl0?kz$IHOezda~!9ek)FjpUisucEdZI%dx(0t+mU_ zIZAS3ZG7D$7CWM~$$D!wK>Cf6rRmzdHlHK?ogk)A!-Hepa7Ul7=`r`14>|t%RZ6^s z*PWJtnC+Evh2xrNyM1wF5~g+4UurZ{C}`<+7_R-ADiVt$j7k!>L#w>SQ0gon5EqAT&-xHdxo z3E4Qr=TBF}rb(#}C|5;8TP6XZF>PUv?gPBH$pn zV{t>z3zhng<6Tqx-_6HIXXocOi9pmz#&c&h6nyc}0^$LRXKt;U`V#qWimUg+@Q}u& zH8JmD4>e;dyv9>c9J3IX^@1AXo)o3!ua7B2#`31SZf416WPbj$ z%z1sJAk%Bf;yKjIe{6DIl4jfaE>EzWW@b{6s9p{=VXP$>jh1*@iQlF_S7tUlDgSLl zAWG!1@WG9!9_1n<=2ze+Kij##I!+@gdTyo7(Jn^CIR|zjHIsWzduRpB$q|7+WJ_1UX$Y$KHXB#AWs1tk! zXXh}PWW}EwUEK7J<98jn8v-{RmlujDhWS}o9>}40c7Ka|&?cC{88I$5=)~pF0dxZg z%g~k4N}7%I(zy&AQ%0E2&jQNppwk=tch`0==26^8jtM-1@PUA0-!+A6FxUtZtBwbJ#a7ADP2KXpf-}(;Y)dDScAaF8YCEV?dkUBj9r*v zBU;(=%SM|FTBeqU2klS%2vw%gQMcoOv(Wxb(-gw1#IR2s-_vvCAKUI{Ti%e;vAsay z=63YuOTSICftEaycqG!J2eC3*8eOSr0Mzn9w{sp(;H1K1lY1B9q0Q93&pp)}=Y9eoJOntie>Kym+7|4Jgys)==(nbVFhF97~?PrmRvQM$E2P*<>#a(0p?7k~!+Q-CZfzZ5XiwOv<)V zUMwfiTE9ft@ogDXW*Hlj^DhKA3vG9Vjc|N_M|V&N+~L!$YUJm;p_nQK9Ae`D+r!m1 zaE%-`gQ_T^r$T+kIy#v!EFcEUIb1`E#px4)Zn^kjkmPW?n(;aUDrc}-086kKbl7m* zEGp2qhMFb)thI3x(p7%lUaF)iQ7pO**Pl^~$^7$*>|2eb-|76XKd84Q)FAkShDo!# zT$cbLFqs|-Z;!$?<9Bhor_!*8hdG-y(uEw~>7@|L1H!07r`oqk5wu9Zl3=8o6kUj6 zQNtm4mY=oejOtd3!;`~iLRNGuJomf~dfWU%i7k`KPpidcRl5P=yPiw;PhY%@ez7F? zN$$lpYdWvz6Fc{Q6l*JGRFd$2y+J}WfzN+Oxqyn`c5ZH{<_)jPqytjP<}FGJUn0-U zOyk~^7Y84Ae%ZRlxW%NHPX~*dG^$~(Zq5C3 zS895cSWSHEaL^~>kJdpvV`m?Ku^OZ0Og(D$sQ<-p7W0jcm*+d{28*pi({pny2FAT1 zxw@r`o@M2H=sI(TZ0gUQENx!VN>X^_vhkx`*!+<V()h{j~i}aL^M$t#?1_zP}qKefT)W zj@D!E1H;o2zpXjOAC0GMh1{?&#npiu(!m`y^Qt(}EJns=tyL&0ZRMsJ&+rX)uJmop zxe%~)_&HM)pW(s1MYXLKRCGnheU5;ZAClWlJ%^JhqP5kti`b%~DZ|%3tu(+iI89#o z1vx>>>hZbDHO_+*lz`x3kuV#xB-psCr&e4}WfdUDgu|3o>k`m)(ASmw+*B#lYv-T; z?D-^baM(1SsG0(cDIFCszYg^3Y7WX;6;pY#0RQ1zNm2af$rjMZ;y|XDo*<1^+(;DP zV<#*sEqHgv#qzIp;7B=s?foqft|_S3!5eI!uatrALiM*yWKi(sMKCe;$-lPuMZtA&#Z{xv>%`$azecr+LlvHf4!o)C+|E z6jb)Z1|wSc?V%V)r!s_& z)JYO0Y#n)2JajNqwEFf#?V8)yy7O`&2^T|`PdN=JYzv5Ev5`Tk84sFZhi2l&$nfi3 z7(_l}j37nq0J$`J2vOV*_kKBX5(=CgGI98hn@6GD$EjrBSF)Pw;rHxm zq5i#ja6%dTrmI|0>O=;NmiL+}=F{^nGj~esUXX~gplMNWm#d#xmsKqwm){95J(vmu8%CRE%3HM{o|F9`h3pw!fjy>n$}UCKLI%PbMH4<^-0v{#iivv zfcxPU^fvb+iyRsD@va>@&ey834CN4quH2$rx%0vU?Qorcj;G@AoP+_uS&(athDi)) za!~e3FH|hGX|mFt(-{V=e#wa(JX@=hdhnnSce^KxVrruGc6X4A7}dCkI%-Z zU884N*dHHtp>FBIe8a3KsL=uh1gt09q=kke%+y&+r7Ry#dL)KBj1bQt2}PKEwRIJ&;Z-AAV!GsaV3#y5g#0r|qQ1_bZ9Jj4DBI&BoV=c33e)wQ6~?}J>VJvE%dr25Hv}yMsPfu9{Z$~tg~}9N z$5jUZn79j;kN2r)I%FMrSi~i!PT@aC$>xFE`Jm<@pDUAe&e<)VB$|h7NXQMJC|fW7 z^Md(FX~M^KXs$fWLD-Sgs1(DnsJ{{XnxJ9jxE1q3WRe0fqkXKk!q;KmkDXn!^f2f0G?%DW96UhB!drHMxYQEiXg|{Vju-%qk z@lv*RSLvs@up3U-;gQ(*E!Td?A^MftPCZj;4N66oI&YvVmk#~{4L2J8%H5#d$*hwW zwS9AsfJ;Re5Va)l+%PUNZW>ced6G)5?~!C6u_wH1rh%$gF8oWone<)iP<&zXZD@1L|Oxv zoCb{BL3P9S)SBeADM99?Mk7s&kzEJ7#t|z6SxGgrwJQJcHeEg|U9%9(^BP_8)GsPv zTF0t@7Is<@yk0w>045;^UGLl*)@PKgeZU3HOLG9*M%38m)wCED^Jrn0cVI;&tMuDq zI|?gq+M>cG{f;$4LRR?W$vHDNps1{qFsMjYhhzc(4tpM@fB)ERuGT^EIw&&Qh||{m zq0=mVlTuO;wV3}{|2QKM+M{3erGP4AINGITIfTweipeY~ej`e&WO+Q4A~_kZsetLG z`U|beh~IhfEE2_^isZz<)1x{l{HVnJtVe%7JrR36@g4lZXagRO2#+yQNCKhyz`I{> z*gdBkA_3X_u`IvrNI69L9y4^qG0s;i$BL=3F3#Dcl2*k2pQv!+`;MX;Dv^?F7P7Dp zoI33sH%6q)on_y9KUOIIJB=v zI1<|!3%6T}VQ>mVPr(H8J2nHq@)`l%%FTED(XftP{!C`*#!gXv$mFBo8;TTk(2rvB zOwZfHQhqcxhQyA<-Inwmjg)Z`GCVLbn`38Fyc_UbqvUh~tvl3%eTOHwV6KZTRXh%vcNr=xFkuF z&aH$O9OCyxIyyd9x{q#Ahoq7&MJGRW5WgjuyfhjM{#3Aj6K|2!x7mThCP|L{j{gg> zcWAcJ(q4irm6e(iim|E65P*tyWQ|{0R-O4|c5MjmKTk>4!8Yy>fx6#)> z;DXg7NI8T(?i0cj6lDBiuq2lLbYuj%Y3hKn^k`*l{KEs%($gGpLgH^A^slYQwhXKPd9^?k*YMF8&NssQC`G!s zO!?Qz$*L)zDy9PbPjjJs%B(`sa@40@f4Kc%`s{;%rI5AJ?~mmPBF^8vd1KWy%$mwZ zSZ~3XGHxWO_`dGe$DB=c&8Io+VcrW3Y&6-|TFPnSnBHCmX~d60upOI*G*unYLae9# zJ%i`%#UDJ8(%r9~I1zuo09RXR)|yR?Egi_+xVjGyO<+eg`$WqpcoR)b_}%C8?>jMY zScHIC>~8hMxVb@H*zlpopHgSBxkT;!^9WVGY-MJ|M_l9oi>R*-Yr>7%rgMOZbO|U( z2qUB$1yo8=LTWHlx(A~{5GAEXiwFkYoufM>Mt5#BBSw7leP4gyKaA_zKk&qP?sMPw zIcEqK6Q62Vv~(O_VJWPtCtl@!6Q!<_>t8cZ0<*XExs+1-70-D8EPS`O<#d<9r!LHE z@7p!X`|`=(hj-r+lkp4;T0jMnLKh+)mnhHU#)9Z=U>WxW6O8G`XYh)Obc`sVHAYJ1 z3N+!-!IADuQHH=}U^ePt-rqow%Oz;mmz>zUz5HTrS6--wrPw~OkCGkvpa z?|+03BM2`TK$oo$TPK=-umR0J7~pp()M3>0zT2t_7K0nBn$mO_F(xs`(e*`3Gka6LyDkm zgllfWWrGi=9G^)k3;*Y*oQK0XlnFZIVKS{z9h1-J?zp$i*38qOH2BW=PI}B`$ZZneMnE=S+j7otXAoF zUREF&X#>Q^a*t^cqT>(rz_l25cVvXZ2b~qep0Xe*=iU!%rf#m^98su0*Jyccki@Hy zvz_o{aa#GRi662n;c`W+kh4@^mcO)Xrb#fXZ}#jjdr<^g^!L(w1ab;=CS=`}xC4{I z$GI)dS-ZZ_x4nwJ4N?Doi@j!252f;JyLOK2L#;3*Yl*s`S-2I{(1STxfI*Ieud6-$ zEGG|9CkL>9dTcb@y0h*ws;TpY(y5VR!I!PNP}4!i>T3q4vDXQ-sZVw&5&-ybVQb6B zESe!GE0zp}h^fWvAYwLXmS|VZ%F2gh##h3Xe}kr#3G1^AK?A2E;psE2pfprK$SKkC z*I>r&(|l;nIC!V!TvI4E-F{ou<9oW`i2c(DjJ5Io*#~j-ITmhluiR&Qd1EBumhh(6 zBG}!n*ykKOyA%NpWyz#BeopDAVY0LRx}k8xTv0?(YOU@=F;f?jOZSRyV9wgTJ8;rinhA)9!7#5mhx6WFV4CF?%svP%>I(Wsj}S#KwpO zE2>^F&y>~7zVSHx;`8z+ zA!Q|Azmu5L)rTgx5;ut521f-RmE1H;Oi2s#{l6LvC#Q)nR{fcBY7U>hL8?kCa|9eK zXuB?ts2`jXf?-cjv@UKh6H>|DLGZ z=-Wua+|6P=IA{B(4S@)69ZKk6eWRm)hu!}RdeG?kJNQCk%pGj57c;ZC5Q|vQLIqxj zUq-Ow&LU}qgI{s=X2?Ijkvrd}++l}E$|qH&t}M;1bA!T_|J+`(DMQg+yHF0&-iN;!yN%BcJ&hJa~p5g_d3}qUc43L#Mk$} ze0$jZuy@Xc- zf!cc;LU?|WH_rN|rq>9qsh6&%6V52;f2TS16&K%m*W;<USXuZa zYr|h!jn`BBJ$8@m(hfmBXS=vw&2i7wUY1Bad3a_^mCzAgR!r1BPZ1-7acV)(8PNeQ z4Q^ks$t(`Lh4ts#`4SDpfHhtv?=^fpA&iYHOTpjBxH#@fm5?wk0q@K2u=}uZ?#V%r z_wVMQvoKO8r?bBTx6UHh@HU@3z!cUojBY-_7n3`6@O$Ht(ys5_IyduTTy*#$--Vy$ zP};KRSKd$3q%h5djYS3J@AcBn4|cDPNi6(a(F zt94Xg=GzR|m$ydB)!gA84v!L;E@#Vv;Dn%DZaRVP%>^wxGe1_Tcui1I9yzfNj%rg) z!OH(g?E6pZ-%Fa^u2lIQ_e;cOCUtUYw+xi9`DH{Kd2dZQF^bLQS~Zzg$@nARr6Eer zMrBwXM+f!Cup+bk*5${TkKN*&3=T$F{fBZlPfIhnL3=NLV$*jXUUjXv3o5vx!2mMn zdik3x3yp-!?rctvtJ*2bM*IyISTzN66MWRxP<7o!&1aW~f1bIz$@4MD$*iiYj5N?H zZ*@d=CBRZ0g9v)owq&mHjMOP@SOYc77^Zy_7sP%$m57=EWiW>(8ue938iY(-)TzwyTw z1cOvThwDHKjWbODp>*?Fa}j33e$%*K^S=d>_`5)L+kDNZ6T`a3Z(3J}Te<=j4j2j~ z95^hQc^{0KNjF?1ygw?5=p#%duALr#hM~e{74PawCo4 ze2!c3oKpkKcJ2+My|JLlpAkr`8q2vKa=g;796>pLK5#s3vGzS5LNxHbIRKWQdaoxu z_AO7W#;$cMi?mw@G%$vWhaOlXdjW_K?}FH&SI7^Ol6>Cm&ts2=7VGxS*E{O6#8TC= z@2U?q4GxKI?L3Opof^=sOrTw+pE5**oi1H~+H>yn<9+yt?9j$nIrh`^n?x1lOTMyF$KKrgevrxa2_{PPZW}tF;oA46jJK=Hc-bwj^9N% zzmywN5=%S4k4u{1_RfK=h>9PgfoPATV>e7gBxjH}D3x!?L2f7a!uIyc(eH{odHFJ` zg2lK$s^b!EwdaeLex5zQJKZePCvE8E;T*If&3=(L3IF9UZzgLQ;~(SP4g_k_z+L8D zPevwuImaqAXaMkzOu&OAH3ez=w8;-$CQd~e27_Vk1tsl0Lop?YgSpRPyUgGCQ(+8c zvBkUE9+3^Q6Hnlt;|gPQcO9+$sMxv_{9Iy=bRJAZzAU^eqqB){rA>w9E4fu@wpV#p z3i?MkraR-c_VjXK%3s>vTyyOWbdkX*;LV6N@b-@I%uUhLwLi<0(Fw|yP^kx5O%9O4 zeagJ&rgZ$DFU}vIuQ}SkLsGAT=H9@>@Hb3xL^o7SC=!$5#i)ZH`pfl3ASB~K-7s7< zwmt3byEX3dZbFr-5OUh2^Z+G7~kf;g#P#}cGTB}dy zjxj$j=Gn8Y;K|yg_I(c9_$GCS$vYP<+}tP%{}FYlk!*t2tY>D$U8?-I8(<9WD$^P8H0Tk`?0dNs-jo83Z=NVF&vqF{u_E${tH(r_zE zB9NSoYLeY}2J9M=oYQp<1J!wQxY;?B%@}%eqQDs%zk<|fav3*yl z)jie__iRF`(L6dzP$0a}*e1Q|Y8SL3yZgFk*+jCof1<_n`Vg!quAv|^nm=hh-AE^I>O|Ou zov2p^*1O(q!LiXO3t8u3|Bx(h)8dVimfVzOkYfs`h=51XTqF9HT~6QXm=d*78r;Yi zp7(eT?(KLQUuOPe6vxAGH5f#{kb+gmrvD72MbDmFqEr_!5DF}-Fpw@4Z8*+NO|qr` zY@7L)YI{D@t66*00oi9%uHsz3@%}tT#^FzA=a*OR9(Ah?MFOZ)S3=o*yL?JmZd>%V zUv~&Q;SeXj3MUC_JqWSM<-8mMiFPqzEn+WR{43kNo~1@tu-iKcll~q#hmdEd#HDIxc@AN#caeB`)lz(?YqN@=A=<+ zIAEUkxnuK7V+e|Hx>CN(ROw_tzQ!SZL+{IF1&a0HCc~7L zW9&u!N4&MGsQCDs)8^N&GDQCib${f9T*=a9iAE3}+Z=BfHx(M!MA9!pQY03x<5I`D zh3#jas=fiB_O1KuT6gPO{ObcTn!>RlNt4<+ln#RK0W`)oW->Bxug_Zs-XOc;yHLJN z3tR>G-V^x@koBhq+C(#Ic9nJv-&RMbld_f^Qhv#dmd*)697{G8OU#qPSSAFUL%=Qzn(zRFGbJP%oJTBefokmj< z%oa%xdLHbBmL6)tZV4Hr>56aUkqDf}6u@;q`-)lp_9L6GMH3S>r(3&t9v-cGta=wS zNpYzZ^u~DZL%{`3K}chRi?XV#3Hs%NKwNVAwvD2Zk+?Z3(*`!dCWeiH5JSEg>S3HO5>8G>Vo+_=(dfTSb;WL()eahT7J)W`zBx`9#*s@PvB5R;%`e~BzOSBq5@fy}!)_E*H8;u=e$U@fpLN^& zAJ``-RTae+iwn?Ydef;D*c&w1F|!vhv|`zTSJ;FLX_o*K_T==bP9Ngj_COo2C+?0m zw3TnFTj6&JhCNSFS*4z?VM3RDKqvnRcGLgSg9aouDchde7|D6hCiEd?lc93-9?k!DsfIno1XeeOwfs(7g|BU0hyNp{OS=`bE zC@>~)kKHV*?S*bbH$F|#k;Y5D zp^QOlp4f9}ma-VBe#>ewYXr+Qo{PG)$YVYB1`|MzW4_!{V(cH+TH8ULoWO-5%QCc z3thYW0X=^OqbnF+z9vg|qG(rqdcypoW|Gna6N5SZaSI;8wg3icM(a619=I^wlaLl@ zLHAVI#Zm@oMvR#>9H&mJhF+RbBne5mR!vC$AJY+O4ixsWtu%_|ZnaHPg}!-`d(_m05yQEnN}1prsDF zMC>0NZ9E3vI>02U$?`mYfy+PXRn2^6s8W|(ocfNZ4@ShjJ4=~|5 z)a-P1)f4pN|Jyg{fIrz|3kN4mXk7E>9~Otglb-o%`2rx2-I;%3ifV zokKf2Map$j5<@EP07#AfZ?Xl0?WM$DMT8yRWXl9;>9d(^FsefRyq)3mYSqL;>;a(Y z*I7`y#wL~!rRh@Ap)BGAp0;lLFPPfX!O5PrZ=5{?<@tS1RBh#FnWq*Q?m?^)nb4f~ zZ`JA_@Qb!$0m@%DV;hKg)(d5RT%Fm79wKh6bdciWj}Z1;fnaHB6+=7MP3FY$Ea+O{ z)W?}DG8jr?P{hsrQWLo0*V7Ze@!sGJfHbrt?8?U_mIzfKf1%pegV6Eh24v#Ex!aNU zOC#b2UP58;-eos!S#N~vpUkdj-pq!j`0Zz(ePF1Qg0+GCb{rzu0N8UF zCu}VA<<>|`hE=?G{rPU5p+vRWc$L{18o4W0)oPR7mTH5oWiIthe}Y$6M8OvUe@4$; zJwHOiUJ?o-dB5}34Mrwio4SX8t% zHG#(kYw-Iq+_NMSTEB*Y-^x zO$RsQL>b}H2!+mx*dwYXKydOq4xTY%twAxAlI1j%`ymrTiiac&PssBh2b&wF0sC2->{@e5$Ht>Aj!R^-IrKpFep zMe<#;lU}-qd6@!E!ccHI^`w;#+T#4&)Ar{XNfL+8oAT~v7ED!@bGIPBG24^7)ebfJ zUB$mXP$!kz1|Lg$qawJvbBd{dhpU*f{lOAD(1P$b?`D!z+=8n{!-Hx2QfvMN(X*bQ zf9b_jtrx1{*}XJfB0lW~ocX=WToVrShm1zvdmHe#yS+UMC zx%+Yc7!ALRnZH*6!yR`D`!YItw(-7)KXk{W7$nshU8O@3gZhvQH9rw}R2Vp`qK{XN_eX8n){Rq(66}rj=DR)5fP@e~LRwCIQ)i+9RXw`q zfdNRSj%OABoJXO9y|&+`9lmu#^4Y@X^ze=FA4QeBf7lymZqB6h~8+1)afgC8jyG^+S6#r z5qD`toSA)aw5JI?-&#nKgH-R2H#>W`oI|2L4Fv==1Ol&o7z~pP_UEL4XSRVeh{98ufC%0q) z$@Gn6z3Tm${_4yIL-ay-^|8~ZN;iHP&z~?(%l0DhrE8oNCdWd*B^@vj^+oQ z(b+og$*OCS#YNxCcI-}mlf1$L8-0h4?>8NfNr%~~J+^;Zp{@}55D%?XfW_Nbj7o9y z0hxNS3xn|j_uC^J8rpp27nU>EustGb4f(Z?Up<=_Gby|DSQ9UEHKv_%i=T2B5_NK z1h%R0z(PhOEd2fa!BYCI9Tjbx5uq~Xo#Z-Z_?yPKx>%y`d^Ij)iia| zx{?~qae>WaD9i1K1HASS!@oV#g@ZrkSVghRg}CPaW)Lm^qJhaS?YAqcu)+JW9`Q?} zl~LbdoJgFlT6rDcp2I?UqS3ZVD`fr6ur4LxGD_`hUK?Nk`ps=qFTG%;$?yBxKi%x#ZnfksBG#Li5` z&ublX3eN(xWkG_Mx6Swg``rO+^GBWMn@g7zmCZNdo{4{@RHH-rOy~pQAq5c+;-R>$ z1Uib)=f^p}*yIh#X7!XLEgnO684YeZ1nNlnRNL01N^NXDfe_79u~vI7Qas)Da}qTV zc8oo6Go&7lc7-%Pz5Zw}r^dL{E3u>=jbeFvCKV{1AvfWZ(LkxF_Tb{2byJ6k;Gem3 zj7Q`ufIEt@H%yod*Vf*7v!aO@QD>tt>y27(ik(_HzWB)`pE&v!&S>%^;}uu9z2&jp z1Cwb)5sD%1Ey=1-b#uuLzF#$-^h|>DQk{w=7SmU2b`MJ8my;L1Vr=u;`T8>5*S5jp;QIKQD_UdeV^sQGy12Tl_iVngSR&iQR(6`)d+9g$fx-tAYWmTaUly^u?c4XS$yYEN~!Ncs96ukrfZDZnPEIP9EZPXv;YIQ`y zQtrmU6;74+yWMv<^4hNPBc_X?x2=4Yh^4oGZuddbGc)<@)57&e3)`RCl;4DxdXymR z)C>v~g7u`<8Tcw{tuF(^>fdEZ3>iz{fafGu4}LCgUyMXc#_I+>E=1)7T`W-rd=ipY zQJ+kF{$}oC71C6%mi_&vQ@_x$uj^`NubrztA9rl|FxDET|!r^Io5rL z6V`4{$tK^GbsaJ7eE`fZobMOV@rgwX4)9POrb6i<6GfUIB48m&%un5&8dD1baKftP$`*Kfc>pq^)^80?sO66=A6wmX(R=B9!^EYR( z>_VY3Mg02RXXev2zIeFqzroLGfi{|H>Kkj3hvmuZ{E}W%^XDR>ye$~P^36fQ!qGuS zuqw?=Q}^$!xnvYi*lk}^CDK;o(5&)Kj!`x31;c23;2UUE6t7)O3zf?hdltlMd6 znU+Y6zH^}9QL94wA|($Ajnsg;h@8U{lBZ0G09`uNE)Op)L+>Z6e9vHNNT6nYE-`C3ZNk$#5uHtyA zB>;*Mlc2G)>ol_<%wvS}7uM$^RB$V%lnmKre_$xpR^C#y#732K5acditAv9(UQ>^E zL_Ph+u1WI2Zn2ct&O3NC_kflTBDVYh-x}Vk;9zU|j{D1Eaf6>FFqPw`K5^ZVmg zsqO2hAnQpyaLJM$&re!D+&S&K$BT$UoF zN|!ica#KuY#%5F(7o*~gL#HoaEaw-Pd(g7{3b#pqr)z-0cezk5c-*hABDu0-rlv6b zNlV&w)OeThxk54!023$DL%m?h&Q;hQ73x-OsiC;@j{;jueEs^3`tw&DHdgJ&ILhnQ;sFRz8IA+JhAqQSmZULqCm=-}soKd8}x;&`S# z;G};jYW!?@$?K=ok};iGcaXZno5*;-RX}07FV5%Uh~wg*Rkgwc$hF$Yj75=5C7IK? zdk=q58O?GY4``Pga{a*;K~Q_b1XX(Ax|$A zLZE1UU(rX_K`^JVGVF-sVd6ddm6~A=o~C?$Y;51`tqZk2JF3r}<^TMXoog(X#a|l=WO>`fzTk_aIv9k_rC_2(^on3szjHDNBYbmFAEQI+QG;1l z(KeJX$m2|meZ^f<jQrNfg(r{|PZUJ^$SDlw(Y z*uEzn9l5t=>&SmvwO+XvNJ|r%r|pm9r-+45gcm-O{6Z=1c)N0@$=PbJ%$t>M%-tmc4U_E)MZn$HFn3ZG1(M-wqId5aFXY0~e1Tzbm;78;JHmo7`HQ#dMhx{WjsMC>!8mfAF!1D6;S8M<;9TOup zPfg_8{$e@I1q7uU(iXE-Z-`|vUcWY0w{xX__>6E8>WbU>f$(qfvgCKIC*n_D-GOe+ z+{cz*%sPSADRJS%jD~S{Kn~-xOiD?Az z>$iU6H@~tpXdqjFFqXR&?c>OJE57t#YHfm=Pw)VppXjD5knmx3u4h? z$HOlV3(f>zB|nX{*k^q@ZvrZ_+KWYq$jDR}1QC=zXxX14uc;XR!^*Cou-VYE$&2zk zN%xit^k)*(S-U5KEl{429}Dt1D5@zFVT_s!Ea5B|;y_Q+JaiY8p>jduRxIC;JVd1y zvr*opD12S#V*j=dW^a$0=5VTR$zEu3~y3#;;^G_@2G*7cUX>(ZJjIYNxZv@a>my2OvAv>!f$zb?q0su;o+R>#Y(- z673uXx|-V+(#y+L7WtWHfdX0r$&W7f)t_*9*t_4;<*4jAh+4^>isd-Y7|vEvDcyc3 zV;pk^SDahzj*Z#;{%d#o0xp(K)7@NvkI zCc2$s7j&ahi@|c=6af%+KcsK%o`t&2a=iYh;ZX@=0t$18tz5^KM1t;Ku{q3(J<;t)0MqNH@riYKGX6f1s1K%W80pi`I zaD@m7_VDOf74esLKckvh3PY2oT0ePuQM#Qd`rx3DOJoymn%PyGdh)&uUJl`po!eS* zPSEW_x9pXbO3qYIza1=4!J`bZifHX7y_bz~GuIx%X1TD!{9^lHO?PD6h)J-fx;2Cl z{iwjGdYw-%<1C0Qr@SFS>#3Flm<34XxF(|xZxZ@`|_+dnh9LOtMwg%|X z>Q|Xw9C#%jy(`Go2;sm{F{&QDE?M+s`)Tlth3u~3D$EFREdFj!G?ot}&dfye*yx?) z)9QwZKgA|7tX)4sd?a!CHA5%5Pte@$x*HcxDKCC($3EPO?cDy{8QHmVvc7QEy@FKm z2y4E9EF*}JItnw>in?HVmzWV2#|LTTIm}!XViHg8Mn5GXvOiYy;#|#ppyMUiefeSV zA*LY-k;@2<`=0cdXq4)E)pJkfx^nt8JH?*U;j13~R9avRX#5KGp<_j7a3^xqa@& z_phRp(=943{hOG?Gee-a9-=0jD;c#IuY?zQxFke)*gsh0S5@8} z$(nV(uMr}HVBc%G>uVMya%~S2sABIfno=yCQh}YFRI18f)ZcC%gfsAtlwScS8WqPB zw0J|t^zQG|u;UMOrfs~zLC%0SSuR#92g6<}kmznAXorSxnW~%js0A8AW(o<563ma@ z7`Sm4HWQipvzhwReR;mMU42$}#RQX@EjGBB80N?nl+$x&FEpc~HzBYo2~*2{lm))= ztKC-pC^o%M<=q~;7hb$KM>G-Ey#u3>s*5sj&5(Bam@^Nxm0b0}+0iyrEp5vD(|mnT zg4wDSc_D@Tu#9J2zBk>;M>IL)H(mbw_E|Jxva18GJ7@!-e7s5PrM*A*Y{ORnu>Aca zP_gPGh5`&yBLB@?iNO6|D7fMbsP~g>FQ_C(pSY7Ad9R89sOZ~Db}MzlOk8f3e@ zm!V>-okg^}Kv6+It#r5%eb93~GLO0*Kmwr0Y2UEAgWTp(BLZl{O@NQqL1-66aLpeS z_b;6-5|%iH9BbD^G~bMdqRTi>56TK!#g_Jd+!tOW`y!`oHC&f$V%B}`7my112kD=7 z>8SX#Qk;5f*g1o9x-Wy<9?Z-A4F$c2=aV-cdyEi$HH)|GrdZuJXpW#DT*f>-CbfJNi z4a0j0jIVQ?#YR{>o2 z>u2;9f2QcYSp7B(W#)wen`#UdK|bHYLX^0*-8__l1>lzY zgXo;hgl?f?<25Lgkm>AS8h~OoV9CAkOX)QCw_~HWx#c|AwQVi-C!Q{|X}R8K`n*&> zp6a*Zkk36Xuuz$q%Ia9D{k|>ZYoJVkf}Th5*>49gg>k9H8n0Gj1#-_*mvbChH1l+w z+!>T*vwI+WYBiN^DRjR~#j3PrHP)jo*#z5mas*-HtN;s1FD@@g!qB*2no3vi5QHA& zcmnfiP3MV(93K-c4s9;HcsnqJ(KYZovQHuX*A?lhCEk-iQ=`D5P{#4Th*`E5d0Tti z;8}kEEA)cX>t#s_QKD_+j`i%G+)4MUrATT!dQ?4Z!!|MBHgS?0QucX3scR3tY$)YuSJ$h+p;bCd>EF@=cM7ja1Ps8~ zSriRRN!e{VgK?f7?^zdcsc!HGq?q-tDFuuPkM$I9U)g3_Q(%=eE-_>ytUw+XB6H>GcjBK^>NL;$*RUs~}og!e%a@&anq=}4TQa?-uhL4$wNxze#pdbvuf_RH(Pz5thC(v?VD zFAo$?minDosyI(Sv<}dgsy~xGL}o@E3=YfhA{p%>;-sK>%Cut_HC^K7CgL_e>~VHN zJ2Qrxpfdh$yHa2Qa>ZC!_`!}r{;uwle*Q*>_D?Q+ZNLR!K;&3)2<^6 zXW``L>ztBfG_^)L3P5TUAn&ZCd&(PR&7h%Q%xF1JKM)l9Z4m`e=;#ZX?Zx|Y`|wgLf6T-5K(Wqo zdOpmo)H39MDNZ5OR2H1FINf^G`PmSw93(o1u`9prF( z&wzbo>I^tQY<^Ht6&bI)SqfA^;cqwZVCR_Tpk(G#M85?g&cI2=HG!?Nwabr1*I`yrI`?BQTtXTna4?Z^*TDex%#AX>pO;zu;_Pry} zOc>u7+?cl)dhl97KKC^7Ew75tG}3nn4rV8ZAe&d*bDZB+s!byo1T({@7OM-s^l#V> znZQ3YFRwq28zLOuuf-`gAeD1HAHp9|*dj~F3@%rzMFM7z`_sHHI{woU*9feF%==$=thoAug?%xY?dZ*!o9(JvkIxBu=acAr03@PJ) zwu|OX7XA-0wZ=9{@%$o)>$?vAGY#Vd^T}H9OXB-1!4;Ej_OVIC{SPytwK3Z}S8m>E zRw4(j{v*PK*25nvhQd=!L%7t;;pwGE4hHpr$=mA6*KiTgl)QRgtJw0P4GPT)UCJGj zN{MY(8D8V7V4Zq(*!)XZYi&*VQ&8QVR6S;(jmXK>3S0iwGCd9u`x^M4b5r`SGY8pY z;t2-!9>861-s5d>nceU)Sm(6YpVa$@_f!||Z0@m|x3c&zM&Eg)C)2gxG%TOpZS$tp z?}{N@sd6-{z$Xner7xJO5#G^y|1el-<&dDP11B24(M3VvwE3MnO<)O+UyhWqgh zk1)6SL%(fg;;J{ph9XAe!!M21=M_~{SaZH|rbLR=6%o3t1!;(jBCAt1=-^@>#s0W< z{-!N_R}E7HITWPuhquJEl>4*HLiScK7x)?RX2C62u1x_atEWh0K)#mEeU@;l!q8*w z&}M?GWB<~IaV`Tid7|9R%i_U{)cO59?yYpK|7phdbnSsP6VYgw>LN!`4RWqCcE7IX zH`vNO)6>WsWieZ9B9JDL(l1mSw5{70aU#F)f)6|AkM*^ozE1yvl6!!MKDJ;|A%9ao z#%lA4&s)pyV+RFh?T@oV$pu5OX5cms$TfwGvHURrvR-vGRJA9+wOzrVq~aOJ5`uy* zv+spSiK2!Y9UC}=4SdEK7xEpH80`Cl!y9e?AAF$>1 z#0&fB>G@}KaSE}^w6n{yi$q^u)|YR;BoILeM0Q}e7}uhV;QCOq$zS-=qHZbxDwW0X z^Zg&oaWN9YkV3gL4MAyl#++A_k&X;qTlxO3vg6YDdOu#|xIDM2c-0*1-l9k{Bu|Xq z3-u>{IkjnKl#XIo+b9|2*YT2P)^?7dGQmCmq2vv?IW(P~eJf=kXy={PO)Dq^U;8p# zvfr5Y-;2L?Z<0M0%oM&qGK~{%H)9@d^~y}Y`wCG3lK>OBTMvPD)|O?kH){RH{cV6G ze`G(N?W64uc$M~Zo;0;MV>$t-=L%z9CO8>4Q_cLC8=AH5b8Nn9J>86LT;#L!=4U}f zu;q3@s({DY%=IE`g1L4vO=$sr3|eZ)DCb-5T7{2;5GN0}xA~00a9f5nv4QWg0dZWnXW1`8*H+fwPy;dkwLAD5Zor0!j4?}IW(F{dvmuFdjGTiXNtUJfGS1ORse0Kkm}GQ2(t zK-h__spP$H&GH%KKX%E?J^W2z_4Po{W#F<)4`tbIdzi9PNH7k?=_vsou5!-(Q%`fe z{O2Yn=aBg{aPo`1=Qbh_BElIoD1~7fUo`UFN;<2t5c{x0kz*;glbC^lMLvUzE|Zai=!nlb#3{F=}8mOxR|hJ=;vYMt~B?duky0kfuU=Rg6!fA0X+uO zl(r7x)xTU$T*{h|j1z~nvQWxG6B~sHr$txuks(yD3kivmb7AemZe3k5rJxi| zu^jsj{9wcByqCu1`=aY$!0>QlH5I)e^v3LAjQwxlr=O^u_$U5#bmVj++x^@vteN1D zFBt~9f9HPspoJk_e7Nt%U`A)g_Bc!wOf;VF5fykCq-3a;BJ-w5lBK#~K1{J#?EwRt z>dSl*9Ydlg!KG)KA&~>5pE4ySgUH#jx&~1}MK=^N>{o6eCzlwKjqO1ojcR#F{RmdhTmJkk!hK=@;@$Nt#Bgm5n1x%B)H`yTd#iT*MBhR+7sR09 zdkfvZ?vw+dX8Z?m-7QZ$2QJ%Kv$EYgZle$9avFsjAvv3STof=BY%M(Gy6VG*gEMj$F-r2r&^y1E$q$m5K_967h! zJ-LG|FuHQwG^t;$Vfn`yV%g1aY4RSo+v{;43dv<}`W7MDGXw zEyDdc#G}P-k*{U6dL~pa^$@dW9{5k+nP1bO{kr#lZz-2**hRpOjUR4$3hXa{OYEJ6 zOpo&&f%ZgV8<$V;l}HH9hXf>DDjDudsNP@n-jUr*^g8c3?X;8E&mhu_J`hrq6Au$?b^4LL-hnh&G&{(@X3?m?y>E&&We1x04r@HB7I}xq682K zhs~G7)Rb!5;n48Ug#$Udl3(U5shBFl9NXHwjA6)tWOUAb*SV&xBr7zuiA|yBF_@b?AkrLUj>`rlK5dVV4?~e4W1QU3XvS+O!K`R%Cl0a zDTYwl64tG;-$kv zGQ$cTZUU4z3pja?)|KvA0P*W~66YSJ>KBFg3kkH)F6#&z45drpOV5B7;9ZN|O;FD0@xPVNr^HFNyMiVkn$j;Z#b_0X^??ug)(v;g{022M)HjL1^(U7{Ry< z(nbrv;q^sb%~HJnvZ93LFTpp08E_#3G-*=iSqG~izfLjhf250+d5W1w+fy^;XM1Fp zU#EFD=P32NlfW%ombI7TBjWhwjF9W+kP8m=j^$IEPvTb+0V(So#NS!~vl5G^-*|0Vub&fSU7$Kxl>1l54~npq38)0Bm5=iQR&c!H{IPDStD zt%8%ody*Mm5N1*0y=S8C3HWwG0T3>|hJRlYZfg5bCui;}+=&BJdrqP+gQNb4C(f21 zGq!`MG?Vm07tgrFcazH8e19nvC|lN`+L3h# zRf6Z)sTem&^#oUo6!I2C;K@2*R~M!ks`tXbyk6DFOy2JEf~WZnG+7`}yMA{VvxW=p zlt%Ly^O)pQRQ@iLyvGOTQ0cD-Ve^e7CskIonQcae9w^#th^iz%_>rHL1HI z(lNu35eqCz=4?S`&6zXb5(G2n$nHT=hS7ngjN1E+0SBM7 zF>a9Pk!X}Qt)E-8xdM2--xKfb=p5n7Y+=UO?vL6q_6ybfInlR+^xTjzk>c_u)vr0Z z+0ofwXtD0e=!2PoIt?@3QK*{Oqby)5DoXl+Sig8y798h_@I{TttlVi9^wG{o2d(PbZ{f%r+G#^0 z$1mFK!~-j@D=I|VrgXG6%pWgO_pOp;+CB3>mmo=>rEyDb%=&@PFx$Ks7fnm*r$xud za;-j=*O?_;kdknqeV{nPYxK6i?Khz2lkbJHiE`n^hJiBYC;F#aGv0{zrnDAe%nKnL zA|P5+K4X?8^{{TN23KQ6ma0{@Bul1f>?7J&5r^O_3o|6u?om1l92rB;8IoXyeHu5U zvJ52+W@1)Dm+KWq%ZXa0|H;;*&TLGWM)L+#zI(?rX|NE#zRS+WE~J!sG&g!*zIsQo z%EjEPt$@vS&b~R1I9r^_n!071MeL2pqQ{mkb~?6AOV!(9V0OEen`WLPDQv#*6Np(( zF43-AagH5c%U@_uC<;kEg^0cedSxyz-N>e=*T$`?Ed4rxsyUyypG=&N_JEj+H`1nl zXbFb>`mx$kHMB=kyPPV;sk`)^ct`J-uO#rQB%Jfo=`ZoAO>eTv#cg?^|+?-4mTyJlag<+Py2jZ`sE zFVOH&>C@IKVk2{+^qHNllr-H31Iogc`wMJ4pO;b{cC4y}mpYjIW<6vw{69x1J-0I8 zWtXKPZC$2jLB!~Fklujw*>U}fgtoFIztJV%GNm@S@-1PIx++^h_j&gYXDrgy$^P8U zvg9mRkT_UmBWLkkz8y-Cw3SzwH#S%$hGZ5hbTO7a*e+LzxLg-Ine^FI2b+b@A2svf zogFvx<`RCt2EY1ea<=dgB_g9&#~-|PQC!>}ISngPAo6QAWp+i9D0I?~JM^aNVm0DT z9rm~BLP8^L7Hg1qa7UHyRr`wQdl!Do)5*d5%pxd{oPkhZ%HfQrjMcvUU$mG0SLAp3 zy$`m7Rm*`$z9&}st3UO$+>fLnc z87Yb#o_uf<*xFI?UTB6)W`PLpwtuc307-zkFSwdWQ6^)$CzTdBToUY6WvNu}wpcTP zT4@%Vo30CyYJ0r&w*<|}Y;To7{5Uh|ri6L;BUZXL>fW`2RFPf&!-tN7?wck;3L40F zt0ne8Xv`9KWn{0V?E1OA#FwI=Ol?a`_whrIqB_5do#z;ZYQKRqEk>KO5VGOcU-GJF z)SsW{7Akb=A<+I0rOg~uKus~2S@(WdS0An~q4rb0!TEg?yT@Hw+q`egI}nvjsW;X> ze>$dh9rpJYZYOvv_ASo22#~k?Jy2ZkGDkwyLmt`;djzA3^d6&?tBk*cnv#}r+Ijah z_sWgkKZ^xkeDMCWVdKwBmuh#k0x&lCX!F{T9vv|kIna^mG*(&>3;6}~99L%Y-0W*QGKOYHa;O!vb~NQFU-ILoTn+2 zJH?cJRskp`OwCl+kgZ4~>9hb%V zg=hVm;P|oX&_gqPy*#GzPPI1X2D91M!vBb5*u<4l-_U~nrDm%cYZIJRcH^~$lP>Uf zkYfRj!Nz@ODvJ;*i$HiGbhJX?3#*h&V|S@>H>+SbU3VvJ|GR2UFk{FrGj*egH z;SRE9g)Up^Y8`XOA;sJg)h&psnWdLFJeOt`(|xYPX?Wwl)-`5$IG${daK*L`bI;r_ z)4X!0q4}!B8~^FE=;5fD27&9fH0X%-aGikhOuMk#-|l43D)GWy;yd9 zMOQ5wJB!al3V06PJi%MTonb@InhXLFvjljmdQlx^8PaMWGYB|uTK9UY_oO+wJ(mUh z4idK)Jk+cp_@pCbV3bw8L;I@I5}SwuRLs=rj;sIo&r4^g|EllCk@<-Av3NUTY;!N7 zi&)FJ@0Fm{P#~sl-lGq4(z6_j?^8#mu+a6%vGkr*6pPc~Lig9M?tk5Wt~j1Ry6d71H@}~=%?=Uq z?}K)NEa>%r9BlZrP1N#9aAOw2m{c{ItV`2Pa<~WWX||J<537BY@aBu5m9DuPc3cfv z{5QdVO_VS4at<=goI20Ht?X_#a0n4b7Ud$J($*If)4km3?xwyhmX}IpTW6yBq+L9( z6qURe@!*UdZi%Or_rTF03k%b*vX_Bk+n*x-hLu)rouuZoTn05eL6$!@sGJLk-5Mdn z#uFPfjkvIj6;=U*bIy~xBdM}yVV1au_QqzVfi-q(f7kWJJw_jXZ19X73H%6zN5!XW ziQO-_{AOW`je-#{f#3p;7v%-iEanUbWe?=(5lX;cUS9QRqldIh><}!}bDzp}fdR{) zDqzDUc|E38(h7*bAoy?2P&o)ink~>04ryh7m?`?(kWGFLKfS4l4|1ZM%_QA#gvOGO z@0@t-Mwrx{o*fl&L-jNlP{4rGF*UyDxP00tuFzM9@o7EZ=#UZhvG-MJB~=b(N*JTV z`FGI#cfiu`pV^138@$VP@@`@FIJG*kdoeeK&$@fwC)@G{2Kb8RvdFsmGnl zCXH39Kz~~su#7Dv>%Xrw_0g|{i(%kZ(c=8rMh8nF@j2P=0{Y#cSB3nY4{O}Gdz#3j zUa(wpHoITrMr_{_cp>^2NYGYaP?@KM)}M30iIy>e@Qdy#>LzV&KSxRYgTrY`xVm|E z4iv}qhu9AF?64zBcKj?<2ws~%<#Wu`03<5_&A62%y85d?nemFFTe16@K80myQiMa1sD-9L25q+059pcpvh!bxazb?M%mGl8Iv4D49(}gC zBVmO{vj%nux4;Rqq4)Q;oKY8$>Q%&^$;5Mom)ml4u!7(MUQdcRG6^ZlAB7Qh?QT*% zwY?ju|8ze9Xztduuc6^{nmk%8_PLeXJI~!x&)wUXsCrXX_rLVjoT0y=1leV{NU|@* zcfSkt9L=c-sDder6KSFP_F!zg=l9sr!URQHhhFmGB}c9~g11vf%w7j1c@$PxTyv*j&w!4PAaRrn|p3uE5jdfS^u21 zIisBAmpo3vD^zwtNMi|mDwl&Vo;)s{GnT9(uD{H18FsEYqkMGt;n4;&<{lec&V>e{ z0M`XgP%8Jv>;G_S^XR%t!k6xjzePrJzbf*~1?q(8nVU8C<{A(M2Fk_1W=w_k+M?u9 zmID_L;N?4G9{I_r6e10S%jRh5XcYXS{?n1ET=S^RbBwcp%urQLt82C^ILtgU^C@$m z*zl)JSYm^@`rj)z;^GopH8a?G*LMOw$-90XqqgrogePP8^qLkT?RR{PIE8QYG52CS(S5x|;GpD_w~l9iq6 z`J{siM!F6xI{Bd3>xtZ}O0h$|2mqem;}o@W&o<`q$iUGWERskvFe)Fu>h9o6qEX)9!eK`S0aJdQF2z z?M{iL_VuMXrk2<%!c!K8bROeQ$qQ$y4C|5WD(d($ECYQUDwA*1%Fb#7n9!q5t`YeL}}2w^F6vVJ>w zY4z{902>?T(mLby)<{pBzYat=<$rtI{!B*EuNNBF_MEtsS)n=%>+BAP7^{|0FN81^ zhEN3s5BJ%0y?WK5B~nw7G0ysH^QBw6)wphei-yjRPofzqnM}yn7pu!P#EX2qg&Lu)oCn9P#^PUfmWi zS2){3I$q0=O)C7%KCCe$d3|&bAFjH1PWf4z&$xD{Lzz}E<6=;8)FsKw}RH!qc-4v^Ve(iomLnX)ux)eRGp`GR|q1z`!Xt&lHY@l_!ItjbD{+D z0x14=T`2DMi5DCg>hh8GjA-0t!7f-Szo{*2bpH5@O++dsoD}F9XLl8elVk2hG z(^hk`cK;N?FTT-3w}JRu1-rcrP?x)}|YyA*y>9Wid- z6XD*1WgRpDW*pwZv3IK48o%(HOS!7^`*X@ArEOj!fas6?-S{V9u%~B=Aj;+LD*mCb zDbq{{H`?$ruNG&ybyL$Ii;kD4c_*)@@nlBQI(>5qj_o@sj-W8oSo|t8g}+ithVOIH zJig+g`s{-qk4w%uMrZqp2HmdL;TOZAKZ0$EmcMT?5VkWT$JIefmrny8ajGe_*H#uNA12Nxe59xD>AkXCm+QlIZxQC%di-fU45v*JK zt93OP+*5*>2c2sFFPa0`qjF33DLG)3l(ej1UIBN#!f#hs9mujyYyif=;K`TdK1U%Y z&RD*4g?FEvQ8b7XW{mO9mZOYJ69+f=Ms`e2g$1Vi1qJ!g>OTc6art736>kK|dy_-o zw%~eTuYF@$EUtr=4Z8_uEMA=9nl#k^d% z=bo7}H9nr8OYh8r-mkIaGgv&*lM;a?-?s_2@;#E{vH_!xZnsC33)zki5H9mRx16~9 z^)|Rk=$td`&4x==jjHMJ${>fo*RMQiLdMXWg7>cC&}7>*pq_~j^HWmFyfrx)F|R$} zd)9Bc0Y*UPJSSPD@_i{LM~ZIdJr-*S<$vknVeL5UryqQhuIw2HxST^_WI7M)?Jqex zam$7Z6q`z-yMs2JqELu_N5?xx+U<6Aw`APPunLQ}@joR^UcP*t0yUTUssPp<#}({F zI!SD1>M>Ud)@|qqGi<0}^}QU?bK(PkhNH$y&OJq1sM_R}J%I%0-s4(8WM<_d8^tvx zHmP#v37I81^f>+r1vdD*%SI-PZ+xr}% z$9Vr4oTBDze9!RPE;@FT>u`R6^(mX5#p_zlW=Kr)XxN?VOEadIlZb_%oqqAH0n6{~ zMS3h!ngbJ~kgOuANMFE33gbw*YThbwd8V46!DopT{-M@ne_WbDr7g$Pk)Iuw zRbPU;TI2IDhVVrvj=ByJ-k{Yad)sx{2`}djT}Uo(6HB5Cb`hJY#Bx@n0}ie zT7UN0rGhS#BSPGUOYCG0%SL3sfMM~O$3YlER?S{F?w~nPgN>&~Vx+b}Wf&e+z1hA; zoGa4)<_T8_7d`6pt?xfThtx?x9!iUrUeNe^jrXJe_AyTk4YQ)RWxt!I33JYZGKEE0 zbY~GR|BEDoWg~HPap2*F*`sc4X|0p41>zjK%I1(Cg}@YNLow*F)~XC;eVn#z^{4gT zwmJXe_D@a4a>J}yzO}zD+>>{!c_-O!rb`c6uNQI)*JVcAtu4;FojG)Ww#!hunyJWm zUU?LJY>L9d<>pejI1qiE+lh0q;YEpYi#Q4iI2EHrdstizwSASf9M`A<;X)wu-Yv}Z zczW$$)8*pge%|6`33~${F|wQ9*%gA|1KsyzEo`n$nv;K&nhS=W1r3Lv%V$g0pLVXv zR=q`vEq{*Q@4WZ@Ml?t45{7c=muHVR6UWQ5<1@rchsG!ZDo}?Cv;b_CHN|J4V5l@% z_cs?+HRk7>uMU5aHcQlOBj>yx0{3(!S1Rj!$r%~{Nb&lG-euQZ`Hi9nd`8kp#L~hs z-ELrN1c#nv&8pK(HCCXKCDi`;fF#=sCr}Va{0L*Z&TVzCC@;Ef;e^6qsH!(sm!jEq zguHfL)hbKvEfa}7kzhgD-txdN=$h9SGn_xD-%hI1OlyAS5!nkfbuX2etj%5S)YO1NBlF=}-0Woq6_xoafe#79~9Ifm@upaNiPgpfB$MUy_V zLYOPjz8={taX<9d@NaTzp<$l9k(p{E=3$cIx^MXKG|2Nd&gN8Cg&Vxop7TA`3fDx8 zf@UE?sF$|>Z$Z+E7^-TUk}2Uw{{-^azAVvL7ev%6xHc)QzwIOzX=ShoymEy!8J%>v znoTT=1+~}$)$FQzj8T2BwOcX7bFxar7Vyn= zngl`eVb0|`OD0K+y@HCL5k?uvqR>Pi0{4%3g(tpyb%Zv*w5^4q?kWc-W$epvXvHM^B>V^w$* zgEN&h4nJl56gATf*v)?yJCd{O9^<#e)9NHiTHT@P-Dg&UK@Vt_qf?%qLfId95Ba7D zft&Q5f;xj)u2ZBdA8oE&LS|JK(^ejXA~lHKL(lOs6NwPQRXG@$!`1KI#dV6RT* zLTK7K99Qv{=#+wStDg7?&joptFiyolF4vF|hM4&QbT-9yRRlRLngqsT5yf8^Sz@DX zTVBk4I#qOE56&q+YYKBE1LNsTLYebhFFkx9`;%S;%Dga5ZaQ_0B_B3KbkW_O7_yr{ zZmF_mfXAOl>KJ@1GQfclf%AGe;h~ovk#>Y?H?m$pL@v4J=zQ>%=GZ%Dj)-|OfK6MM zeSE@Y{{bSHI$mXUXlPjyhWVrNIGa~x;II!*nN;+zUJ=V`VA9)Q5(|#5>m-J_Uv+Ve zTf=bNYTkQ$l63->{%NCrgUZk6QlVttcVKrQf3uA`Z$rm61-$GIICe0!3r~+!i-a%h z>SHnKGxf1P4l>9HG9wAoAau+LPF|T9@~Uuejyh(j1QFI&<7PYJJky1-lPCN(0+wR( z$B!9_qPkq>i5--|qm)0-w(J}RU9HwB?uya8WC}1>tt9(q(NVaaaPsa{nE?A`TmTY( z?#Di*B-F$X{YM;1%33SZD%^sPpG^P00rC;f590&{U;W%c#@Wh(K{qwilP#w;dX70$ z#Vjp7Xd#46!zk-UoozKJp%y?D{3rSk)v;&w;2t4am&@AjwF<*Q*88U}`)d@}AGed( zI=rUqqZr=Rw6FGTz0@!qm5W`p`1*|w@Iv+4{24#)&X)wAq-`O!+B~@Iqve8$F@$|S zcpD#3wVA716G#zJ_L1zp0hL-gc=BG-IKN!29Yxy{U-#RAo}{X=i}t_1=aJ=>NuqJh z*xjqe4MFgMTT}i`M!g^E$&z$CyAwno z%5Q%n8zsm++;~c*d?-Ri2L5L2_(9OUXtaAWbia3Y|3}%%K@5Ecpn$6Gx2|*y_sxm2 zasj@}2RF{*ra&n;S5LB0=Ka1~KX8;!I0KRZog`7)mrR-`bmk1_9GKS#Sq&w-!9V-J zM|JzI;q3zXwt`TVmQ)`EXWdX3QR366w!2!?dgi))bF}=YG#WihGY^v};TC9K>g)uM z3M#E{k|z13uvJ#E^pPKsfs-CFN8E4z$9wYwKtr(#kO^W8h`K(&4Pc%FRG9m5unMS~ z?hV=SXn0@)M0m>j`0#P;*U>H`4hgKQ<;a=c@Bak)Bt!5-)jK`&jeeetbD;|PO(Y_n zS27Fi2*j?lBqxL$ZNZwPwhm%!G(8O;JkxI8Kh;Z6a#)zUi+WMO{41>fG4rLtiBYD}Xdls&DHU*~e+`h;2?=)y40= z>hT8bVrTn;!KAx%!<7#ne3uHoC?Z7)ISjOK2N#)d4TSbSfp?EGT<#n zA-~I?BV_0Vl^%zTlLF4j&=-01lp0IUmuGeH?uk!1>Xnb#7ICDh(z&Anj6FudREOW! zqNDgm$oqcJqoS5rJ0A)IRrU$|f!(e^1J2w*Xq^&_u4DhoN2Or)q5)o4`AJ~liSw%| zcK8`464af~n0)l09zt+_x&DGKJ=Va@*Xtf}y>X4=T;RpR@Zi4*)?AcX?G9bGIaBBE zYuUUA8Ovz2+47tgc%2;MIXN;S7E1B^H?ma~-_mnayNG%?tJf(3e0WPC{V_w2hfHrD zLREnF;6WV!-3o2LqD4tDjLS)2%9KRET}LKsC#epHYvV{^eIRpBrfHwM%jCV<(PG9&fI=3FOdU$@V>3Ya?@GgDAZNDLE`L_sL|X;ly4+Cd_Z2!@1PRq zcfzj{dzl7_9El?_uedUG;M9FdnVY8JiCj-I^haYxMS? zB1c52{9?XX^c;8sqijsS#{8SLV6FP~u7gLW+&WLe))&`rv%jVaOjWK4;t(>W*lWE! zI>Vr+Wam-u!5duM-!KXmS*J@5pA~!zkjb}_mp-?+gQMDbN@-9u z%ctgJJ-Wm`>IPNQDBrYv@E0rvPr7M{nGDkBip!md3|5}Joq=yh zcIOK8;_KZE#Nzj_E7kqt+P-G;h76WkO@HE*6`>8pkn^SDI;`~@juvSWd+fD^z7eGT9jSk1EAbPH1(7I<>y z9p4K%QgHRQXY5xyt9rffiK{IWbgAfzM2i!x0w;ZX-fP^KUUlEuaF&idZpbHmw2^I% zu=+6kBY|Y2uHvqTNU@|DN&+ z(=_O=CuVk(>BZ_?=8sUCy}O+QjjpUfnldAX?zu+(V>kWo&*8wbA|PjPAF#6=rfAA6 zVGzPl@w_(V9VI;DQ#_2s?!=|EH5WI!JjM54PfOQyAVo6DH>%dcux;YGc>AmbYrt+` z04Dm!_o-%09&o>6z7gIjn^fw5gOZ50_#4My+XDsg0ev)7o*;;|0-i033d*gyG6!eKL>}K z1#yE**DCY<_CEW8R@|pt+HH`PH}zC)cpVsczO0D4Pj(K$r6)WRwL5zZw7e{@R63nXDe0?=_##e$IuuL@B8k|7> z61`8GO+43Un?)CB{%CN=zq8{iw__DXGI>qoHBBae@;?&K>K``2HY(jGCI&xzy&P~r z_cm$8`w97PF>DZdf1ja%LZ7{V+>Wn#XP>P?vayL@M~wCWPKs{-0~k}`XylMT_=UFd z-sSwCYHjU=654Df7ACRw8!*z1`@yvKQu=;$0E>r>cbppDY$uEahfltq#aAF`zhQMB9@{IG}GkkzI|Wn3cZ|Tl0jnMEonR2 zT(pI-1q2&nDKG2rDL4MA=8yL?RR97Q#&Dwc?zzotyd@_F-wg zWO$ikNY&P9vNTX+;92Q!9$TL)2mKsQ4dMA%=h-R*_6rG5rBk`Xv-J|`qOIuys0SeCq?y5By1G6fb;15(Uxw05Auv_I7rTKL(TGkOGxA5sGf2L z+@R1g7?;v{g58zOZQB@&8R_$mfscZ(&jk+rswkMZsyjmP+7=oVmGD0Q4Dw@$S4y+0 zxffe7_{F*Nw{{}fso-_AA*TOXz&4H4E04+_Zm=0Gzhf6s={KSNrkNUMUc*0*&YR5& z`8jqagIxCl!6%uS{_X3~t`3j%bzd#5sg~JL_22uh&E5}uL$hNbdPj45e-x@g;;B-4 z>r{a=v~NCPY{5e=>>tq>j5=V;ltW0-XIh9qJw8Y_o;Y+2EL>!gHI}S~n10xCgcwU> zNBY^ROh5`BW8XS~bNOPPg>Y2y>J>nE8cWYs#;F@+f+;ikpZ!l2gkB<%oS_oUTWw8= z`&sB*`0*39sU8zAjIsP2mrL~zX19IVqPm{;&F&S4W;&|eq#>w#w_o;a*!!_4!~~~{ z8nz{N%#Uo8m(^&t;*%US(O-MZ>XFa)X6M#@je|}F%lTnII%hoE<}766N$snj8WBSR zgMY^)C^j>t&fLo-@_8p1=Thn&*b;ne_3x#LK*l6*Sw377nBKVf?jAL>`J0=Ue0=w( zUfWdVO0(}37>=lK26x$@`ZnYxhbd>;0|rWzwZ|JOGLEX1IRZYVUH%v<*|pCJkCDfY zJmC}z-sq;uCZ_Uf_ztZ2ES!yhj2=IlJSfJ>*8WS|mI^64$nwN9m}@SD)+9sC`RY8} zRQ!IZ)(y$m+pZ_9FWM#(x6bE6?KvUxq*2-lPkN*(%+Aji>rWt$TvU3(p>KCSzwq-P z4TSl3w#`1W&Cljb=sM}5YLc4S(^~<|s5x6+;noGDqg>#%w0)>Y=5hUn-=lXW#h39ix!cBav~ zSXbqe8c=mK?HyQQonYm2VMXl^=zAT<2eQpdUwrWs3wLaSHw)>;lq&`#M} z?>)F4hIhG~6EK1jEnVd`_FgK2+R2!-bP5X54d21y#kv{PIS5q=9to7SMDJTvYhkv= zT^Q?^+jTqPNHLIGe|kpY)N#*Tg54kP&NSBv40f;e(Hh3{_%|zzE_Yfo#|&tAHU#7L z48yF2h7T_o-0{DdqO#xuE-a~6v!=R^H4&rbSCkSafwhtU8$iz^QFTG$;P4$prC4cQ z<0>!E7`ZGIamluxSyt69rRzfcNWgbm$H&2c=)@MijvzhB|>T#?zM?4kvm#?z~uKJ3oIejs-%pJ$2aWzGyg2 z!6WB^pDCb6Ws%{J$icB+ zg|%U41?(47!M`p}_^*jEVp(0Jz|!=&9w5~tovh2bO5}eYr{v$qiMBkh!{Tas?5lW{ zEnz$N6#ek9qsAI6^(BGegNmC~eBdf+6Qhi}!T1WRWx5UMs=Ym7D3(=z`q2~&LEoP8 z?40m?VKlfWI2bXZepblDm` z{PI+YkBig3^DoOTcf<@~X3O?t4L?{@#Lxo|p5b!ahg0PHaRq3!D^qBdPDISl?P1H1LFlGOg-Ok&?vgESp+1-~I1(VZas)_6@m`6jcG6*vXo0oAF;K@ZR}XI-@@F zfk(SyQ={Qw1IjtiB@>SyFEUPv{Pnr=|EQ7TxjqwPXD+AX!y$=VP2-+L?K6StOacji zh%os0KL>sK@fB-_s=Wi&v-npnL%F%&Cmh{->J&X7RQ%VM+GzY6J51D6H3b$h@A~m~ zY5qzdaTI9LXixf!tobwl9PzQv0UO_}OJ0X26QcZGY6!;QOc`Jp;Aj8qAV1d`+*z31 zw9U3`z44wt_W$o>(rK@eE-H6Ru{=CPCn|M?Sr+rXn0d<0>^bNRg^Zk3FN!etmj9dC^B5sbyzc@&!VJ{zR zsHqVPS{L8p(NTLX5cxys`6h?j0bcUF*7|4F z3-a)B)TsCg>UKVD*L`r|+|Lis<28GFTH0h6#k01@#^5FmR2BUXPZ&5PoMKLjh6@U* zyI0l)sk#Ehb;^Gt==_p>(yk~rX&{HGQF1r;( zpR3!o40$Y;^WjDD^#f{q^{0VTQh(P1Bw6<6nRkA*)g67~bxv2%&)9WQc(?XPdYZM> z_d)XCzf_Xyms82I?#5NrOVVat=x(2=@1qJ=bEGFLAx|`)SYh5Jf#Tb|2Jrj{P)W}) zX+ZHjOUINAJECD{pyA9#Qkd&N3(U7O!0i;(lVR;ei-5zCG3q4fBqg!)GF z9`a!`Kr6r zzpM-T!yQ(3kQa5%Os(EQwlnN|GFk0PSHtaGrs{0hHIobEhiVP@ysp#<8{Z!FZIPu)$mR;k-kyx3SSaE+V#uSl+A*t<7pev zVrh3FMn_>-K1QWc}$>YmHlJ{r^HlA#JT!WH)7`!qfn)shgI;pgI;v-oRky>{lhinf>pNoug&JwJoL zQIhV@|K5Bq#P+46y0fl-d#;K@#}ZnwNz44B*(J;8NDhKmL`;6p*nPE3>D$#hht9dM zY)vb@uH+bwhD|uHuX3xhid&J4ut^U#%&jjtt=Ij{MYZ^@8z)83LiHd9p0w02YLfNL zRqu5Ya-C>~iTY_##b&V^)3`=qKi3c*B}!e?@7CcaWnm2NGdxH=DjX?z_M~h-*_cQ z`^j&1*@Bl=s6YRj@jZ*F<`=%L+yn3E&vRTnHf#HRR%njZE8Aes>W(au89P6_@#jqL zNYM4~74$=Q@4_w9+GbAGwH3!%KEGE*9Qf?qD~zu<@{*G{FWksk01>_O_;N5Y-QO|e zbnVa4*N|F7AQ)#Ka$m3m8>9*{&-aA*ZWK)20Q-0^rI>4QZB*;26@u;Sb28W_k3wF? z{H!)iSl^y3J4>^T?~V5IhuEC-yliT!RPfiY*S{^0DL9+kYuJY@H|4_=gT`SQ?U&XIAPo|pklKi;Rh>%iMw9)_d}zXg$Sd3bUq^k`4`g^l$U z)2nSP>2j!Q6NzT9FdGIp;}ly8FxPM@jg z`?f`=ip{r!To--cFerVssx^7sWMXo)Evt~U{4!hC<8_*TlLjAJ(aw8`b}oMVu4wOD zX8NZ~>G8L2--eu;$a+!pZB2<}?J9a>H{O{P&#^0UGTiSH-Pqs$#3A*;EZLr3pw8+@ zIfyRXPwzFC3+TB3(W+gZp?7skEgt>}L19_Pk7`*A*HR{6w<;^J$9CU^e6Hg_Yp z>Bnbp-?kUt6((@HB?$jzfd9N5UW;o9;UI!8#;RBzfv4qk3t?Yaa0_n(mKA?~G|CMj zUifl5`-3rW+(*dd(+*G0Plf`cqoMrM4Y*53{FoBkS|Xx{ku*P+)`?xGgI)`)8_=>L zWIn5vH|saAF?3z(KlE+SR_bK4Xst7-wDu~5+T7tsSn#bJ-Xl6ZUkt2phAx8CK$_^R z`S}c8^o`lCel^XgaZ&rlaYce$7NNQ+toR9&azOiCqDQprWua3ZtwBl@)gYwP*oQha zK5lsG`u@7&MW~Xaepj!Oisj`q57RnpCf^F9zLALx?^3O06uPh4WlCT9&fsLZw=TbMaU_G@iguLY97e}C1XuW$2wceV+8?!OmPKNfGAz$|Vx zDFg^8_1&v#!+c4BWkJ3>JMNYaFUnsSZs0nic*tp=R;$zd?amIAp`kV4UNTK)VUqig zpE_+dyAQ`ynte2A6d*b~+OYfGP3(VRy^34&7DbRP-8Z=oi)A41cGb0BTvnCsB&ihV=Vy1s7T)7-AW3UxUJ&z`GQz^4Kwm6mUH#Ggfn-A^ltHHJ<}a7AFOi| zpt?N$A;kfyicQj%d-ADNlun2qvc26K{D9vv=a2x2Yw3=q1VUXlzlQ$I7D zH`YoNHvE<3C;j%m>sj{o^c3dMPDx(Oa-E%HOXvDVEBpVdHpeNN^SrOBbDI(g=}c5kCoiC*)((zrwu=;yko~7y0;oFZ-rUm0*0&`K*mTF5!$!yn z#~uuGBlVf_rn2WHUOw%Xa(2cTj{fqHTI=f9A8-B8!MS<1%9Wu1VdOtRIgnY^xS6(#6R$mJn%Z?2xsZY}R;ZrT9GsBST{v?ZRYc_2wCjI^-ox?3dXPb%((%>@Tk1&7%(tO=(!bBFudCpj z5(_yLtUWxSQ|#4?9T7z-{%u9$M#GDXUjbXwWs`^&UYJQZZp(U90 z=r3>S&s!l0a^C!oxgr+?O4EOyQrgaEaD_Gu`4CUUMa0g5c0&{V&oAs|w{7@&r#rXr z2;VT=*hVZ`t)^MM5iBTTrJbCYc6W#B=@z!aN3zMx%+@B@)Ie>zV2dJbp~f;v*frrd zd8V{ev!h2H6MDlx?&hdx{m$HbHI*C2lbhL_~}$ zo<^YC{p%r%cqx=Hf37y&je9ZZcE3ew_h|12CZ8vLRQL&ZQ;N9w5&7%(+Mg?h0m1^< zk{e*@6Db5hr?A3!#jxl9Wi(B?^A=%^nblNUv-z>%wv$H1p>WD<~+m`34l?G)DIy6z$1*3$drm=Url-mQ!=Vxg6MAoLZ^( zx~Gp1TnrI(&w0t!$9hyxh4^F9czxK#Wj#j5t%(m|dJxzYX!j&%a8+a0`Y(_3FYu^% zn}%f{HH%oZn{G}B*enorLvuff&Q_>ZDCC5C1kS7%*we9#7ae!)@uw8=yO~W||BSj# zbnV-o-g|lO4&0c3K(HOkUYjHkrwF)A&X?bfNpdH$6C^KZRZBYAI>bOHlf(M#B?+jJ z5|hs@EwU0DNB&|rIG*T=&7UZKG87v9?a}^YF9F%GDMs?LCw#*Bc6&Q*(XpfIFrOjO zoo7;_BW#B6a2kutyBmoKV~_V3%_JPZkSv_k=WUaXk0Gobwq6B#Ksw|BvdBpTMzrDE=Hrp8EyS0D3cK7piO=^>HA*Fu8Fp=T6 zEWP?ijd>M<>zWxWfOX+6)k4nDQ;=F7qlcHoZ35PDjAHU~#IJdS{XV^6EcQo@p1dh9 zkEOK4pfwgnVb!UF-evvT@_*DG0K>AX&HnPW z9c9@Q33O~f1|PQ<1ypkS6ytXZfsyCBUDHb8Jpl@4x_R%nN z&8qQ5d`n2u2N!Wb_j1R!yR_l7H*Bla9$kGsD>%Q+B4WPY+wZalFHGtszo@OpVlS3Y zqka}C7_;!3eZE@G>**z(rqa!?vlx3h@R*_;Vb0xLL>Y)1M;y%@n^>=21z6f{7!XVL zg+jSqr$R#haQezt*(PXKiZuLU{$yp%fXC(Pl|VHS<=NB{8wW2T_HaVlGt+`mUa#8% zuj*MumbTX)feRHGul3nVLssCdpv2KN#=+oOR(Og;7XKs7HD8gRXrl07EOP+e&g*_w z+%`|cKkb8RYHN3G#I6snqMiVXza1kfFE>}aL^F5qs|szcIzOc*6Kw~(Cg9vsG&1P+0b2yBW8~kP{6H-z>W_Vt+z5F6BZ6S zmhM8PmX^SJ?URT(KevNJG$<}kbKI!psr%M*;iP)VR}^ElNpl`Ya57|T?skV#y+JX^ z>czc^WEk$f>7K1LzogToriJ5+=ngz1bQjg9oDvM+^6N5gatN8KhhWYELxD6_=Im;) zitmleA}E8U=-zCVUak^bx?KE}rE9##XnqrhX36FqGkT$1bf>f@Ppp-Z4j1r z%wG98TlMPF>n7JANxn>0m5cjHub8N*1xNvrUUvsmsK~gENpnwCXB7~SugU3_t|Q`n|izXn@u^EEIs}c)2quxVEdn^w>yyQ zF4`Mw+65;Cw@OQDz(acgG9$iUR9t_$oVCa$yYNyu1#8t<#(W_P>d3u&phgxZp&?ny8H@L_EYrZ6Z(p1pe&N3*=G}X1;nxgP zI?}ZrikxW_GHp$>DcAMh`T2x_OW|Aglk|^AD+xm8jpxa7XgSZijV&99rp9mb@#68E`07%2Q2E8d8mvGHm#ReEP2Yo|`{fp%-Px*2EY80S zo)?>ECQYPUD5r{(c5@oTyDgxP#eL0SUw&I=Dt>R#llXtYUT;!CTjHyA8c8oT2Q#zs z=G^Z)6FqQ~nq)l{=ZrFfs375Hi}r)jXpO_!Ee@Z+a-^I1aE3tlhaR-sUZs&Bj1pnH zc3R}L@Ik*VP}{5~`MXN_hE{qCZ5ltNG5_Y4`3m%JWR^@rct|ID97t(VW z{La~95{Sy2^AcqsCr{e3H}<^FS@q8U*WP)zHPt-RH{PgE%YM2M5MRS2|WVR33(Rk?fpB~bCr1@|Gnsx#E@#I=yp!sm;oQZbM z`2Er_ZZUVMq}e5x^}3OV{InEs#)hYSk66l1fh_z2@@cho#maxng@8u2vIcZVCAsho zw+|DGIGaBQ%Y8R|r##32hu4!N;KCJDNuM6njTTwyNACuUb}yis%3zKk@pQo;5k{8mk_oMST}GjM7%9&2HHv0=v<2 z)5bJ}lXW}8dquzF;MsYg9b02Xb5UM>Z$e8fA%Qn83i|B46v2q9Y3h>RG|c218}|q= zd%aT;_JWT#lNK7&mv}c&?STqBacP5wHBU~*~f(rwS@!#lyT+_28M&bSU! z4zk#3R6okz!J+jJ(n<)c zQz`RgHMfP4o~HQ~o3h#10eU2;$Y!X3KMw{^_9$fB@Y(fOwXKY>p`d}mNVn$i_=#A@ zZ}dDbuig-Wh(3R=TWKW@wjFJHns))3PQfc=^Ex+H)F_l0vm1Oq8+$3Js)~`((1(yi zAhtf`1I?B0T)O3#3Wq|XqN1!jWVe<-E{%K^hgNwb3MVH$`*M*h_{}jLqZeJ_vIQ9a zh0h#3mIhXTV^st@)Gq!NSZWY~g4%X`%M8@Pmwjhp=$GY^d-@4_Csh zM3Lo_@Rf?N?;o4RxTwh?%w8h?z|1tWEX*%lP;r_UPiFQRX#?UTlAF5!^=g`N?rB{v z>V*E>mcs_v{QAOmNTjC5j`Jqvr<}3tsaDSbTtC$@{Sa_gy!o=nCI%OwKH70bM{X5? zETu{drh(1AIa^ZVrx1R@ZRJWaXh14Pi2H=?yq2@j`Vwc8Bx-2k-27=C^#{vHCLJJSie34k0o-OZ0Qd#EU$vPu@Pp6A8-dWGf}kra%k22Ylz ztW4G`-2mnqdVWy0Ec(e7`*p~y67Rab^kZ%ME8F#NAGBATa&cRmX}Wc)#c^G!^Ul1&chEj#R6q$3C$d* zV+Wp6JJy?&ZDeWOH-L6-XGdaA)LJQ-j!y~#Y%<%9!K#P`ylwvslic4otqfiGpXt=H zO^(&>#wle}Z>5*FjUYELdYyxx+rNJ`$L2*^C$3ESZ+u&Auzg$ZaxvCG`}jcC`LDg= zmNyN>sQNfAW9*jQgUd$Fl6g+wNyun($)nlhe@9KrVsMsM z;8b?~h&e!B&Pai9!78ZhxI^4?&6tA9)pRBv(%l?(A)(C_i62%k%!^OdR#XfhdmGOC_`9NYznMlw7JD!8s2b|L2kxjV*BW~ z*INTnd=dl#r*TRY3CQ!U@XE0r3bXG%z2~5gv!OIZj~?TTj}+|K1~7AU40_XW1}`Y! zRpmH^o32~aq_G!~n3|4m(AFgdWsV)|#@Pvhlh(OJqu#KPa>b{$D+WmH(jc6J2li8H z^@S?DB4?_gr6%JJLO&!)EFucl^@z_%2C%KP_K}sIpKEQ=W=q>+Kx^*eWfzXx84=E&d0$nw!rNExJ%T8h|K<6Rf* zMXumo$G8(YApqn#<&r>a>`8JBk_QlA4T$2Mz;k!h%x|1-Rz*CYqY4I)H!#$o6bLZ9 zE0M58=9=S8(xjUBrq$|*&V@^R(5~C(j_k3qvkSu3Fe(Yc!9jJiv>WPb7Ysa;CX)`8 z{|nK0b>Fb6E`24yOh5N{SC{IE=nl0v$L}z%7%}0d{>cNqbwWS1Ed#5~h zrzMuVrA!5lZ$p)@IxXF~(t8VL$*IprB`r~0hLc(+OQn9LmW+N7@v1Zzn#kuRUa^HV zX^Z2L`^DHUN>&uHiw+MRqbBF)Uz=%JO_)mv*P%RYosC&R3rYy?5e2U7s|vU?(KUK~ zv%fzxx$Eu*){R@*))Z;L5x^Reu3BvI+9)J%vz7T>c}i_-A?&6`Uad8M6DF=Thp-;aJimt9@V$Xj1_ zJPFh?D*%RDq54*4^F&@XsU=1hZg=elDZ2t0;35&}YM?yn#4P5!)GbPTV9&ca@a30S z9Ti^p2@m?G0|?;$@DA>i-zs=NmBRwluakyD-)1PdENUrN>=k>W?`Bhu1%oOA78{nv zNu2S&@pz%9_;G@_wM587X`kfxOR;zz7dVX)4F)K$yXLy0Vh{5NOq8D2@fu_raXLS? z`xoIAR8&Ad-(P>LJaD>W>7M7Og_5O!aD>mqs14WziKsm~xbxNL#i+-&6XG<-x?1RB z?~B7owy#ai>Qx%|N1;95F9ATIKMJE=Q;kB|nmA_Uvm4Z0Pm~A=!bG~Q>g&I_94RIV zovDekNU*%zb~4|+=LVR-AVzxB$5 zl6sBCTx)LF%^bfJh6t%OQN6M3D#qv@MwS(=YMYiE4$TD*F_I^nJ1jf)fZL(Nun2H9 z_+lT$Czt5&V)TqV?%y>)elxT4IXHx2dxHcwYvnYf)O$8t+wzUEY-Y&Rp}{mZwC@tk z!x~n(A0!t+yl_th*Gp4+=@1xPE1H}}7l=7!dyGW_C`OZ{acdni+iYogn4t z@?hR3&C0hggZ$GA5Z$%%UDol?aCUQ>{dy+FHFnjBr@DjO$bn}a`ads5w?#K}cSmr{ z-utn!_3V2R8X6chWvaq8{t(fmGu=Enwd~ovGX#Z#_$1L#LmyBRPW3`L=7j5n>vls| zSJ*0}L>{r_JeF9*s}P2)1w==jBNBwtxvaHmw+d^6kuxw~%WhFIyXzq&3iUgHSysmj zM0*=~-_H;o&zoJHm;kcB(Mna?VRB}uutOK9kiMYV+$YRQ2n5P3ZdT3$pS*fh3{{$`;FfPF zuk+j>gxfQ6-ZLlU)YZ$QNJB8pR|fv2erCl2d$Ilq4Z-{HQi4+jqhY@0~l5pa_0o%qr~kcj?=)d?UH!r6)KNu_CC*#G*hbYYYcoXcaf({ zzFrmjfea5lqAAba*OYDGx#+xdWtZf5%VhE{Coxy`>ToU?VYP{UAFXqNn}DXdzk5`!NyymS-5ElZu>g_m8* zR9&uPi{n9F-!;8XIV6~SYI!=EggH0cNE{%BUmUuof_Pzke&+V)=Pyf($IIvxdUQwy z%P?8JS?A>(9M1I2<$SbSI8LRd`=%zijV_dsClIu68wz?|T6(S%iY2gVwGD`ioa?Ff zInabeV@)G=t6{J4#@SvBpX~a@2svbCuXJc^^g^;FPY;tH6kUpG#0Q*N3v|L;`@V7f4IAoZ& z&rP){AY@V>y*v)skf~tS0BQ~`-l3|ZjaE(Al4g`$>}bKuJFfp! zd=thX-&>qjQl{*^@6S^uGgXy?h?8>UFm+tCs8ly@KF4&vD||js`D26)1OhpHs&+`*>;>&Ewx&De`49hq^sUdC4v8>jt1$~@0oIK1@4sX>0JV||rOy+7ol0bv2 zyYdc^vX8=6ZHDL$7CcHElNgs7=0%-uC&=s`KTgMQA_l}X!UJBoiLo~CdYvrcAu*ky za$>^MRcwQr`>9z?olvF@voFVGj_$HP$K}zA#B$*!KI<)tAQ~Q}KBdP@H(}>@x9J$3 zwCE8?a&j2&D%C&Cu}%(z`%x82pqeA=o_=_A7$BRqVeaTmkmH&YgE;i<()v{yQ^T&W z&!cNbA_q@qRIKR(y=*NoA2rkO-3bwU?MS0ZSbdFl>ERZ zXNdMkD=eF6eO9nZZFXf~D`mj3ip@E1dpOJUJ zjc&j~?n){VW9C#p(nD^m#=Ab#^>8G1n9p%a2-qL~r31rv+pCn6lpTg*vANZBt)fv) z*b+u~iu+kBTmX}d`w3fKTZ3nG4>YgK*v%Z(sedS+@6Zzq7bnN%yA$+B$4157m*+`l zZ?fuL(^+8>b5BjK@G!<*xNxn$KgW(a1YBCIRm( zVyM_$sH?jckg1Kn;(zASOd2(>fX(`AA+OQVG3#7o8|-z3t!48Z4-dByL!%m|P~foY z>JjF9LgwGz9^c)7>0?>f@q%LI6MZIPG`Q+leUbHz%ra~r2yzSs`zFp7>nD_h!Y00B z{H#t=;Z3jc4SuG$s+P;ZT*TtN<{rrlWpSCbND3mI>enCn+I}xjA3f}IT(P<9_{4BF zbyr`Tsl7&c%wg6)yKFf4-US@`l;cw1BO&on!=SG2K+uthFbN+y}FbJhlNl5RH|gD#P1kWay<@i@$9!3;Fr#d}*dL0!)j!UuE6 zW?Gd&R7EA*D%KsGJO)}64hjx*)@RQi=*+RO5}CFIcrWNoKD1d)Qgm2;%<8@KzB1P* z6rm0yZzm`RhTnLOZBG5FKr*2|eXrm$3fZU^Xu|Cmd>+Fujb2~O(2a^j%UF?8x_Q~> z96DVlVcU@Nkw)&!8bIt${b(?@%i!vo@Ah!-H~XWZT6~o3h8#keh{6G33;>b?C@JPN zbIy^vE91G{Nd`tnal84eAV>OW?subf%&2Cy&}!e2zFYJYG5UakfdPy@u2T^L)};hJ z@;QK2f$Cbo0yca$2@&ckY0srTkmE4bt0+HK&lehXlJuvAHIRvYqN%v8Al7uTuFA%sS*~C=fqR7Zw2(}B}GfwM{1VII9(CVNcYz3 z?dm*<|4fp%-4@>%DDD0isI^=)*Ck>A-3S+zD9Mm-816OGO=KdtYnGebs;1>T;cKLG zD1RFZQHteq9wD4vlu-tIt!4G2c6HV{4U1?yuL#eM+;qpE6g(eo??H_AUdBV4_`z`RLYot`uwIBV2EXQ8S^NPIadsFNw zY$j%imO5s(qvEBVt=GCQEdwlqjWxdgDgG@nYr}4x`?Rn4x)3e{e*N(0Dp!|E@}>Od z5<72wvJM?h^7bw_&Iw{0G_Mdd)gi~kJY@JqEi5NDwOUbia(LpFTzEogpGnv7*O|a~ z4zpKuBs}kQ97IEClMp`=zR@&K?G}pUb3M;nw9NkF1L4JQpHPeV#P0>?p2168(0x+F zRsGmR>pH9fGrctX@HW;rTNNiVkYmIS6p?SG8$gGhqao!~=V1?+WT`bNXmB;E(7`hX1|_VI*K7taHzytH#l=hIMCKSxEbie0 z#S6Oo=d{aD9-=ag?VLKxzsfNg=WpmZRN=+4TU#N6`)&~%4I(C%hMd8IQOyy+R6ry^ zuZ`X1vj0r1E@_Wu#J3XowJQnjS!#i&5a{1Gny|8}34wLEvZ}IBB2=<%cJdAn71dD@ zT(jK&g)hCR?GMky7>a`;S!c_B7IE?T1xM2hTIC_AJc_5bwrbsA>j$y)pf*-DCdm{@ z^!QFhi;>5V@U;T=*zQ%CeGtk{fEM`em5q+t#Pp$capbwi3Z+}CY zjoY#`vf4U~4V9`%^VX;;bdY%<%HdX9|EkuPbB3CG)9;$u$qU=v_5b;X>v278vl;E- z%HGeXezp$MQGgRktwgyY4d%qVCK`@{N$arC++mCAXKo+5gNP}(Z;r^rqc zNit*V7wmd9SiD&2z-bwCgf<&5|}^FrSv?lBK{X1DV8w2i-WYHm+}< z^g9+Hg%z9GY~b-I$yR>EB&~erRyY53M7i(maHml0vO`%!%D!b;4VHo-dCx~d{%@wT z9;a!)yI@?bFYdJ^gqCuM5Dd`9NO4_`oeeZmoNUU8hmIedo#T9_hX|E=6M}A^Qs1~% z5Nk=Rk$%&Qon76F_vCedCGku0S| zxiyG}9Yy90f%|qjmtQHbtkXh^G+Sgp|C^$Pm^+%jd-twZncMJd^Zr(0*9}Zk(2&(! zbnNg%#0K6vRR{H?GfcVyv`7dUCL3zqq`BLUGpxlP;xA__$8gWy{pv5Y7IcY21!W;@ z@8VKzX$TheXNPLCc83RU$Jgi5+-EJcPUUC_MQHxEH#>imhFHIC(&^kR85swDulh^w;$eCNfh*368!|$1eKpm zJGTe{XIWYJUxK7A>Kw8*&VKycp&y(-zO!UsC)=>@`)rNF!Hj(vQmQK=MPbKuN_Vm5 z+uLz%o(f}Ck8OO)ra^82zE&oPhWkyo*-VzL!rB2Ruie`BG|(mSd$H1!``c1E6F3E8 zZo;Oe+ftusPf_kr9<`6G507EFnPnosk4R$aVOKA{I>V_|qh#x7RT{3a6|mlNlBgQ1 z>u1uEczgcdn-Q!5N_&rDv3WkXEyCRT)ddiE=!a)gr7JMF)P)Jv@tg@%)@xCuw4@J8yQAX&D zwf=0Qdk=;tK*1HtlgXv5a}X3BOVgL7ogvpfH8?cXUGywUh|m;5d}9<#Yzpo)x_b5M zJGbDu4O|YRC$aMcKuAj>Ik=zkf~l#gKjag20xPdH77YoS?LW5!X~r?#gC)#pnLP;% zhT9Kg|25+EyuYQB2=<`H=N~5pH}wjvKBLMiO$qv6w6jAJ6BAM6#9w8?3Q#5tXPD|? zK^}W55)ZN#$iSyu!E7Q9UYTKwkr9)mi!ZceiZWHH>+AEM5U49M8%~lCwls#pns6(41>|%hCW^Dxj545sE$R8(m)1e7dRljO1h^>Wrw8K zlmu1N$fsPyCL5M)rzyvL(%21p&!s&Twq1rcHLhhxc9+7K!x+wjXG@P|>sKTg5@bJG zXY~W{gdkC%d|Ir3y5>?{lK+O)c6kNb@#mnr0e_i#6=})cyc(n|(6f4__R@WvI>zl= z+<@ODf7Xk2PV66tOZ@_95dmp`d*Gz(JuAI#8r*m*yfmPJauCijs*y)67MGo~uj^+w zdMX1^MabNreC2Nw%x^xty^JjlP#jmuE53Y7_q7e80_(^Wp_8)XHG&uNE;--T)%E_? z;{<^;rJmJj(Roo5NO0_-N@>>5v~w8WwE&7Jk7^H#&<@tp=fu&FqE2o;0cFEu#xZ19Ei z@!#&m%1ZUg5CIeOnUqqFA>3^!-|Sft`~Wwq4`WR87&l~)bvq6$qr(t?=lxz<-R*>_22%rGuMBHT2Xz72^l_zAFn`x3oT-Gp?p>jU5m9@Ni$pdZq3~ zJHO?3Cn;$MX)Q_+>Yfo~Ve>7|D!cQg<=NRG1u)E=*)!4eLuvF7a9KlWr9!YI``@#j z$&U!~`p>DXI}K}Xvx-VeZH&!k{duQ32XdzTwk1Y}nQkQi3JxuP0sxj+YO}NanJ+kX zetZ2KZzn{{bs(a4aZ4|$JF3~D*kSGa{ZBbh*nq7$l)*4R`B_s~#p%77jCgI2ual;Z zu(PrGN5NDh-oKa2ufK0^=URp*z+5VcU$UCHUsFCL(4DctJ?Or%M|a`Kw%s7-_k0-s zoN*lq12As|nK*igCKmI9ByMeMz1ou5!^0zpnj_NO*wp2ZLsWFf|3|6s=-(q*{+iw& z5ODq8U)As@`aO8_?*f$(0}8^+gX&=P%nM}zV%tZt+sQ~BM1*nv8qGT2f4uiQexwwL zf{WPs9qLd6X@$hC+zZYti;Vl*jNg&Wu?O#O8G{&<-Wgx#uY5lK!XM*PfAsf;v1AD# z!4@QU1TA!OI}7eB@4mpQ0o;_j?JEPX#EtGcLaUV5emB0()O4|AUHoUu6Sv-yl|fag zCj(%p=K+*}7`makpSXK{jc(!GaotZ{khNH*t-n^US7^`Zvb9yYKQ{u3;{48AP9=wE z#LSEdM}XD@q7=&l4Dt4x0bA#}QMwouw5y+SWbNmVnKT-G^+JnHAL5pPB1nD#jVo?g(OLNEtVXD)v7h7Gt{^8*n1xJGroq%P1BVo=gKVy1FY4? z6`$qdJGfP5pi5U5_K12YZTHRn*KuWH5=9=dwZ0Jdt^K%^=I}#<9-3cEo7>gDc`d6N zV(NbI`Pg%qErib_l*VI7Os?LsTK~7?rJK11g_J#}7;vxFCoPngKhSQ4ZX?ulp0ZIF z8iPpAK}#5pE(?H#CUA`Rz*6y-zM@7Mry0JCuH?CmL%&e!8wq#%z`!`U$ZP+2M0;xLM6+N7V1>Ew1~|9u16`n&%$rwKK( zZxiu*3;-H2tMPKY(fZ&v5_5^?5nqExN=gb4-F00&D}lx?E~I(70+rsJa8bB-oD8z~ z=c$4$SQJ%+5y>7j`#K(8=)fBI#wtnqSxyQ19|LFfy-yrjL>7L59=o^}PYbWe0* zF5#>Z^Ed?_0~`zlIY%JqmseLIa)zN(Q)$X4NKag#r-6fSBjLS%f`*mnTcy2Vf~XDX zn@IOSGIb=0CVoVo?E*Iu{jwA(v5Jt2KW+u?k(gQk@wC5ks6$_j#`l^6aR6KOrp0-{ zp5ZVVF@_ksVV+>k(b^=WquHy^t!@3eze(f_>lux-PY;ZAKfV(E;eV?5mHeZkQ~5!6 z)lV2G)rt0#da}gmo4?oPz1bPsGz_({5lJ+SLLkJeX%no97iu2z78bfC>l8Y6an(H8 zqVkv%#3dfWM-UvcAb81&Vmz9M;YL7Nx14UfBE_0E@ zU9in@r`qbCo;P~8nnBF<>OjqYx#vL&v*ffTc02hjnh=J6F@8wcty@VfSnIc)DY5nD z-aeSZ7D*yPLc(Sw>1z<+PFK^y-O7=Xm95YC3isK?N;u(-=iWxC$=Xz7d|X^h0oycs z@*cCdu=izfUS8eD1I}YhEa1>raHOI*imXOB`Z{`Q?jzLGIi>kxC-(?^rDZ2FEne2v z+kSkynLX+{4k4OwU0hr^8uS6S&9|6obJgq+SM#C6GCf!?k3tx+8fte~pRHHC=e9lD zC|;BjMJD1h@NHJ~?2EB|6gZU_xd#GG0Zy6J(~oi+BgbyXXC8#i{jkH@wJu@Ge9ZS% zrj?rotDO!`Vva)~KP)!R>w)44ZN!y8 zASaC#^k`AP;)XcggbK?S+IBK3`C(PYG{C!-qb zY@HI&0D;^lUOYPe2Pz8Q%mUMUq9G5Nate(%oF*^VPS@DtzHP0tTR3xDPNk7q@pmb`~Ia_^Vr`}g=bgi zL=03{dAKc4lPy~9dUIND8@C8iTDgsNZFm}<0WKT*&hJxcJZ_Cf z(zgu`ud)#8tPuw;(^@yEB>m{p)vi<7CbV6oDvi3H68xodL;0RL#kMOS1QK`cOL-2X z7qIc$$apCzPPFvl>|oSb!(Mkj2Y#nxVV(k)V7$L%g*N6;BV<+FctfM5N9>z~=GD*U z9fM4~DA4(RW?^A<9DLk*wpO-0miRE5%fsMW?3h?wH~*bb$A+J+Ce3pTR{MQU5~x+j zjaV)8Tzi8apA93hs`dxJKES)*oXzLJp0k444C!uTcU{l>#kKZai<$ajlFwJ98O|A= zb_VwYvU?nUYn5fT!p-FO*xGWVt*+{L+23r~cnw_AHU7v_Abd2-L}m8ygQDmZk7Tbn z*+CU*F8T)>2dVsACEeo^J=1.34.0 requests>=2.31.0 -strands-agents>=0.1.0 -strands-agents-tools-mcp>=0.1.0 +strands-agents[mcp]>=0.1.0 mcp>=1.10.0 From 636d2aef8ea407cab6e32949bcb17086902757f6 Mon Sep 17 00:00:00 2001 From: Joachim Aumann Date: Mon, 11 May 2026 17:20:23 +0200 Subject: [PATCH 3/6] fix(gateway): rewrite multi-ISV orchestration tutorial for end-to-end correctness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major fixes validated by running all 3 notebooks end-to-end: - Salesforce target uses built-in Integration Provider Template (Console step) - SAP target uses authorizationServerMetadata for cross-region OAuth - Gateway creation consolidated into single cell with IAM confused-deputy protection - NB02/NB03 auto-detect gateway from API (only need Gateway ID) - Model ID derived dynamically from region (us/eu/ap prefix) - All tool responses parsed into readable formatted output - Cross-ISV agent (NB03 Use Case 4) uses custom @tool wrapper to bypass Strands MCPClient pagination limitation - Domain input strips accidental URL suffixes - Credential provider uses discoveryUrl for SF, authorizationServerMetadata for SAP - Python 3.14 incompatibility documented (requires 3.11-3.13) - Removed hardcoded S3 schema URI (was inaccessible cross-region) - Fixed sObjectType → sObject parameter name - Fixed scopes: [] required for both SF and SAP client_credentials - Removed all Content-Type system prompt instructions - Added requirements upper bounds Co-Authored-By: Claude Opus 4.6 (1M context) --- .../01-salesforce-gateway-target.ipynb | 325 +++---------- .../02-sap-mcp-server-target.ipynb | 430 ++---------------- .../03-cross-isv-queries.ipynb | 236 ++-------- .../19-multi-isv-orchestration/README.md | 15 +- .../19-multi-isv-orchestration/diagrams.py | 2 +- .../requirements.txt | 8 +- 6 files changed, 157 insertions(+), 859 deletions(-) diff --git a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/01-salesforce-gateway-target.ipynb b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/01-salesforce-gateway-target.ipynb index 0310d104d..e3ae9a9cd 100644 --- a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/01-salesforce-gateway-target.ipynb +++ b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/01-salesforce-gateway-target.ipynb @@ -10,21 +10,7 @@ { "cell_type": "markdown", "metadata": {}, - "source": [ - "# Salesforce Lightning Platform as AgentCore Gateway Target\n", - "\n", - "## Overview\n", - "\n", - "This notebook walks through adding **Salesforce Lightning Platform** as an integration target on [Amazon Bedrock AgentCore Gateway](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway.html) using OAuth2 (`client_credentials` flow). Once configured, the gateway exposes 43 Salesforce tools (Account, Case, Contact, Lead, Opportunity, Campaign, etc.) as MCP tools that any agent can invoke.\n", - "\n", - "| Information | Details |\n", - "|:---|:---|\n", - "| Target type | Integration Provider Template (Salesforce Lightning Platform) |\n", - "| Outbound auth | CustomOauth2 (Salesforce Connected App, client_credentials) |\n", - "| Inbound auth | Amazon Cognito (M2M) |\n", - "| Tools exposed | 43 (CRUD on Account, Case, Contact, Lead, Opportunity, Campaign, etc.) |\n", - "| Salesforce API version | v62.0 |" - ] + "source": "# Salesforce Lightning Platform as AgentCore Gateway Target\n\n## Overview\n\nThis notebook walks through adding **Salesforce Lightning Platform** as a gateway target on [Amazon Bedrock AgentCore Gateway](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway.html) using the [built-in Integration Provider Template](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-target-integrations.html#gateway-target-integrations-supported-apis-salesforce) and OAuth2 (`client_credentials` flow). Once configured, the gateway exposes Salesforce REST API operations (Account, Case, Contact, Lead, Opportunity, Campaign, etc.) as MCP tools that any agent can invoke.\n\n| Information | Details |\n|:---|:---|\n| Target type | Integration Provider Template (Salesforce Lightning Platform) |\n| Outbound auth | CustomOauth2 (Salesforce Connected App, client_credentials) |\n| Inbound auth | Amazon Cognito (M2M) |\n| Tools exposed | 43 Salesforce CRUD operations (Account, Case, Contact, Lead, Opportunity, Campaign, etc.) |\n| Salesforce API version | v62.0 |" }, { "cell_type": "markdown", @@ -35,7 +21,7 @@ "Before starting this notebook, you need:\n", "\n", "1. **AWS account** with access to Amazon Bedrock AgentCore\n", - "2. **Salesforce Developer Edition org** — free at [developer.salesforce.com/signup](https://developer.salesforce.com/signup)\n", + "2. **Salesforce Developer Edition org** \u2014 free at [developer.salesforce.com/signup](https://developer.salesforce.com/signup)\n", "3. **Salesforce Connected App** configured for OAuth2 `client_credentials` flow (see Step 1 below)\n", "4. **Python 3.11+** with Jupyter kernel\n", "5. **AWS CLI** configured with credentials that have AgentCore permissions" @@ -46,9 +32,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "!pip install -r requirements.txt --quiet" - ] + "source": "!pip install --force-reinstall -U -r requirements.txt --quiet" }, { "cell_type": "code", @@ -59,6 +43,7 @@ "import getpass\n", "import json\n", "import logging\n", + "import os\n", "import time\n", "import uuid\n", "\n", @@ -68,6 +53,9 @@ "\n", "import gateway_mcp_client\n", "\n", + "# AWS credentials \u2014 set your profile\n", + "os.environ[\"AWS_PROFILE\"] = \"default\" # Change to your profile\n", + "\n", "logging.basicConfig(\n", " level=logging.INFO,\n", " format=\"%(asctime)s | %(levelname)s | %(name)s | %(message)s\",\n", @@ -75,8 +63,17 @@ ")\n", "\n", "session = Session()\n", - "REGION = session.region_name or \"us-east-1\"\n", - "print(f\"Using region: {REGION}\")" + "REGION = session.region_name or \"eu-west-1\"\n", + "ACCOUNT_ID = boto3.client(\"sts\").get_caller_identity()[\"Account\"]\n", + "\n", + "# Derive Bedrock model ID geo prefix from region\n", + "GEO_PREFIX = {\"us-\": \"us\", \"eu-\": \"eu\", \"ap-\": \"ap\", \"ca-\": \"us\", \"sa-\": \"us\"}\n", + "MODEL_PREFIX = next((v for k, v in GEO_PREFIX.items() if REGION.startswith(k)), \"us\")\n", + "MODEL_ID = f\"{MODEL_PREFIX}.anthropic.claude-sonnet-4-6\"\n", + "\n", + "print(f\"Using region: {REGION}\")\n", + "print(f\"AWS account: {ACCOUNT_ID}\")\n", + "print(f\"Model ID: {MODEL_ID}\")" ] }, { @@ -84,19 +81,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Collect Salesforce credentials (never hardcoded)\n", - "SF_DOMAIN = input(\"Enter your Salesforce domain (e.g., myorg-dev-ed): \")\n", - "SF_CLIENT_ID = input(\"Enter your Salesforce Consumer Key (Client ID): \")\n", - "SF_CLIENT_SECRET = getpass.getpass(\"Enter your Salesforce Consumer Secret: \")\n", - "\n", - "assert SF_DOMAIN.strip(), \"Salesforce domain cannot be empty\"\n", - "assert SF_CLIENT_ID.strip(), \"Client ID cannot be empty\"\n", - "assert SF_CLIENT_SECRET.strip(), \"Client Secret cannot be empty\"\n", - "\n", - "print(f\"\\nSalesforce domain: {SF_DOMAIN}\")\n", - "print(f\"Token endpoint: https://{SF_DOMAIN}.develop.my.salesforce.com/services/oauth2/token\")" - ] + "source": "# Collect Salesforce credentials (never hardcoded)\nSF_DOMAIN = input(\"Enter your Salesforce domain (e.g., myorg-dev-ed): \").strip()\nSF_CLIENT_ID = input(\"Enter your Salesforce Consumer Key (Client ID): \").strip()\nSF_CLIENT_SECRET = getpass.getpass(\"Enter your Salesforce Consumer Secret: \").strip()\n\n# Strip common suffixes if user pastes full URL\nfor suffix in [\".develop.my.salesforce.com\", \".my.salesforce.com\", \".salesforce.com\"]:\n if SF_DOMAIN.endswith(suffix):\n SF_DOMAIN = SF_DOMAIN.removesuffix(suffix)\nif SF_DOMAIN.startswith(\"https://\"):\n SF_DOMAIN = SF_DOMAIN.removeprefix(\"https://\")\n\nassert SF_DOMAIN, \"Salesforce domain cannot be empty\"\nassert SF_CLIENT_ID, \"Client ID cannot be empty\"\nassert SF_CLIENT_SECRET, \"Client Secret cannot be empty\"\n\nprint(f\"\\nSalesforce domain: {SF_DOMAIN}\")\nprint(f\"Token endpoint: https://{SF_DOMAIN}.develop.my.salesforce.com/services/oauth2/token\")" }, { "cell_type": "markdown", @@ -108,22 +93,22 @@ "\n", "### Option A: Connected App (Classic)\n", "\n", - "1. Log in to Salesforce → Setup (gear icon → Setup)\n", - "2. Quick Find → **App Manager** → **New Connected App**\n", + "1. Log in to Salesforce \u2192 Setup (gear icon \u2192 Setup)\n", + "2. Quick Find \u2192 **App Manager** \u2192 **New Connected App**\n", "3. Enable OAuth Settings:\n", " - Callback URL: `https://bedrock-agentcore..amazonaws.com/identities/oauth2/callback`\n", " - OAuth Scopes: `Full access (full)`, `Perform requests at any time (refresh_token, offline_access)`, `Manage user data via APIs (api)`\n", "4. Security settings:\n", - " - ✅ Require Proof Key for Code Exchange (PKCE)\n", - " - ✅ Require Secret for Web Server Flow\n", - " - ✅ Enable Client Credentials Flow\n", - "5. Save → wait 2-10 minutes for propagation\n", - "6. **Critical:** App Manager → find your app → Manage → Edit Policies → Client Credentials Flow → set **Run As** to your admin username\n", + " - \u2705 Require Proof Key for Code Exchange (PKCE)\n", + " - \u2705 Require Secret for Web Server Flow\n", + " - \u2705 Enable Client Credentials Flow\n", + "5. Save \u2192 wait 2-10 minutes for propagation\n", + "6. **Critical:** App Manager \u2192 find your app \u2192 Manage \u2192 Edit Policies \u2192 Client Credentials Flow \u2192 set **Run As** to your admin username\n", "7. Get Consumer Key + Consumer Secret from Manage Consumer Details\n", "\n", "### Option B: External Client App (newer orgs)\n", "\n", - "1. Setup → **External Client App Manager** → New External Client App\n", + "1. Setup \u2192 **External Client App Manager** \u2192 New External Client App\n", "2. Enable OAuth with same callback URL and scopes as above\n", "3. Enable Client Credentials Flow with Run As user = admin username\n", "4. Get Client ID and Secret from the app's OAuth settings page\n", @@ -162,11 +147,11 @@ "\n", "if resp.status_code == 200:\n", " sf_token_data = resp.json()\n", - " print(\"✓ Salesforce OAuth2 token obtained successfully\")\n", + " print(\"\u2713 Salesforce OAuth2 token obtained successfully\")\n", " print(f\" Token type: {sf_token_data.get('token_type')}\")\n", " print(f\" Instance URL: {sf_token_data.get('instance_url')}\")\n", "else:\n", - " print(f\"✗ Failed to get token: {resp.status_code}\")\n", + " print(f\"\u2717 Failed to get token: {resp.status_code}\")\n", " print(f\" Response: {resp.text}\")\n", " raise RuntimeError(\"Fix your Salesforce credentials before proceeding\")" ] @@ -174,82 +159,14 @@ { "cell_type": "markdown", "metadata": {}, - "source": [ - "## Step 3: Create AgentCore Gateway with Cognito Inbound Auth\n", - "\n", - "We create a Cognito User Pool for gateway inbound authentication (machine-to-machine), then create the gateway." - ] + "source": "## Step 3: Create AgentCore Gateway\n\nThis cell creates all gateway infrastructure in one shot: Cognito User Pool (inbound M2M auth), IAM role (with confused-deputy protection), and the gateway itself." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "GATEWAY_NAME = f\"multi-isv-tutorial-{str(uuid.uuid4())[:8]}\"\n", - "print(f\"Gateway name: {GATEWAY_NAME}\")\n", - "\n", - "# Create Cognito User Pool for gateway inbound auth\n", - "cognito_client = boto3.client(\"cognito-idp\", region_name=REGION)\n", - "\n", - "pool_resp = cognito_client.create_user_pool(\n", - " PoolName=f\"{GATEWAY_NAME}-pool\",\n", - " Policies={\"PasswordPolicy\": {\"MinimumLength\": 8}},\n", - ")\n", - "USER_POOL_ID = pool_resp[\"UserPool\"][\"Id\"]\n", - "print(f\"Created User Pool: {USER_POOL_ID}\")\n", - "\n", - "# Create domain for the pool\n", - "COGNITO_DOMAIN = f\"{GATEWAY_NAME}-domain\"\n", - "cognito_client.create_user_pool_domain(\n", - " Domain=COGNITO_DOMAIN,\n", - " UserPoolId=USER_POOL_ID,\n", - ")\n", - "TOKEN_ENDPOINT = f\"https://{COGNITO_DOMAIN}.auth.{REGION}.amazoncognito.com/oauth2/token\"\n", - "print(f\"Token endpoint: {TOKEN_ENDPOINT}\")\n", - "\n", - "# Create resource server (defines the scope)\n", - "SCOPE_NAME = \"invoke\"\n", - "RESOURCE_SERVER_ID = f\"{GATEWAY_NAME}-id\"\n", - "cognito_client.create_resource_server(\n", - " UserPoolId=USER_POOL_ID,\n", - " Identifier=RESOURCE_SERVER_ID,\n", - " Name=f\"{GATEWAY_NAME} Gateway\",\n", - " Scopes=[{\"ScopeName\": SCOPE_NAME, \"ScopeDescription\": \"Invoke gateway\"}],\n", - ")\n", - "FULL_SCOPE = f\"{RESOURCE_SERVER_ID}/{SCOPE_NAME}\"\n", - "print(f\"Scope: {FULL_SCOPE}\")\n", - "\n", - "# Create app client with client_credentials grant\n", - "app_resp = cognito_client.create_user_pool_client(\n", - " UserPoolId=USER_POOL_ID,\n", - " ClientName=f\"{GATEWAY_NAME}-client\",\n", - " GenerateSecret=True,\n", - " AllowedOAuthFlows=[\"client_credentials\"],\n", - " AllowedOAuthScopes=[FULL_SCOPE],\n", - " AllowedOAuthFlowsUserPoolClient=True,\n", - ")\n", - "GW_CLIENT_ID = app_resp[\"UserPoolClient\"][\"ClientId\"]\n", - "GW_CLIENT_SECRET = app_resp[\"UserPoolClient\"][\"ClientSecret\"]\n", - "print(f\"App client created: {GW_CLIENT_ID}\")\n", - "\n", - "DISCOVERY_URL = f\"https://cognito-idp.{REGION}.amazonaws.com/{USER_POOL_ID}/.well-known/openid-configuration\"\n", - "print(f\"Discovery URL: {DISCOVERY_URL}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "# Create IAM role for the gateway\niam = boto3.client(\"iam\")\nROLE_NAME = f\"agentcore-{GATEWAY_NAME}-role\"\n\ntrust_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\nrole_resp = iam.create_role(\n RoleName=ROLE_NAME,\n AssumeRolePolicyDocument=json.dumps(trust_policy),\n Description=\"IAM role for AgentCore Gateway multi-ISV tutorial\",\n)\nROLE_ARN = role_resp[\"Role\"][\"Arn\"]\nprint(f\"Created IAM role: {ROLE_ARN}\")\n\n# Wait for IAM propagation\ntime.sleep(10)" - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": "# Create the AgentCore Gateway\ngateway_client = boto3.client(\"bedrock-agentcore-control\", region_name=REGION)\n\ngw_resp = gateway_client.create_gateway(\n name=GATEWAY_NAME,\n roleArn=ROLE_ARN,\n protocolType=\"MCP\",\n authorizerType=\"CUSTOM_JWT\",\n authorizerConfiguration={\n \"customJWTAuthorizer\": {\n \"discoveryUrl\": DISCOVERY_URL,\n \"allowedClients\": [GW_CLIENT_ID],\n }\n },\n)\n\nGATEWAY_ID = gw_resp[\"gatewayId\"]\nGATEWAY_URL = gw_resp[\"gatewayUrl\"]\nprint(f\"Gateway created: {GATEWAY_ID}\")\nprint(f\"Gateway URL: {GATEWAY_URL}\")\n\n# Wait for gateway to become READY\nprint(\"Waiting for gateway to become READY...\")\nfor _ in range(60):\n status = gateway_client.get_gateway(gatewayIdentifier=GATEWAY_ID)[\"status\"]\n print(f\" Status: {status}\")\n if status == \"READY\":\n break\n time.sleep(5)" + "source": "GATEWAY_NAME = f\"multi-isv-tutorial-{str(uuid.uuid4())[:8]}\"\nACCOUNT_ID = boto3.client(\"sts\").get_caller_identity()[\"Account\"]\nprint(f\"Gateway name: {GATEWAY_NAME}\")\nprint(f\"AWS account: {ACCOUNT_ID}\")\n\n# --- Cognito User Pool (inbound auth) ---\ncognito_client = boto3.client(\"cognito-idp\", region_name=REGION)\npool_resp = cognito_client.create_user_pool(\n PoolName=f\"{GATEWAY_NAME}-pool\",\n Policies={\"PasswordPolicy\": {\"MinimumLength\": 8}},\n)\nUSER_POOL_ID = pool_resp[\"UserPool\"][\"Id\"]\n\nCOGNITO_DOMAIN = f\"{GATEWAY_NAME}-domain\"\ncognito_client.create_user_pool_domain(Domain=COGNITO_DOMAIN, UserPoolId=USER_POOL_ID)\nTOKEN_ENDPOINT = f\"https://{COGNITO_DOMAIN}.auth.{REGION}.amazoncognito.com/oauth2/token\"\n\nRESOURCE_SERVER_ID = f\"{GATEWAY_NAME}-id\"\nSCOPE_NAME = \"invoke\"\ncognito_client.create_resource_server(\n UserPoolId=USER_POOL_ID,\n Identifier=RESOURCE_SERVER_ID,\n Name=f\"{GATEWAY_NAME} Gateway\",\n Scopes=[{\"ScopeName\": SCOPE_NAME, \"ScopeDescription\": \"Invoke gateway\"}],\n)\nFULL_SCOPE = f\"{RESOURCE_SERVER_ID}/{SCOPE_NAME}\"\n\napp_resp = cognito_client.create_user_pool_client(\n UserPoolId=USER_POOL_ID,\n ClientName=f\"{GATEWAY_NAME}-client\",\n GenerateSecret=True,\n AllowedOAuthFlows=[\"client_credentials\"],\n AllowedOAuthScopes=[FULL_SCOPE],\n AllowedOAuthFlowsUserPoolClient=True,\n)\nGW_CLIENT_ID = app_resp[\"UserPoolClient\"][\"ClientId\"]\nGW_CLIENT_SECRET = app_resp[\"UserPoolClient\"][\"ClientSecret\"]\nDISCOVERY_URL = f\"https://cognito-idp.{REGION}.amazonaws.com/{USER_POOL_ID}/.well-known/openid-configuration\"\nprint(f\" \u2713 Cognito pool + client created\")\n\n# --- IAM Role ---\niam = boto3.client(\"iam\")\nROLE_NAME = f\"agentcore-{GATEWAY_NAME}-role\"\ntrust_policy = {\n \"Version\": \"2012-10-17\",\n \"Statement\": [{\n \"Effect\": \"Allow\",\n \"Principal\": {\"Service\": \"bedrock-agentcore.amazonaws.com\"},\n \"Action\": \"sts:AssumeRole\",\n \"Condition\": {\n \"StringEquals\": {\"aws:SourceAccount\": ACCOUNT_ID},\n \"ArnLike\": {\"aws:SourceArn\": f\"arn:aws:bedrock-agentcore:{REGION}:{ACCOUNT_ID}:*\"},\n },\n }],\n}\nrole_resp = iam.create_role(\n RoleName=ROLE_NAME,\n AssumeRolePolicyDocument=json.dumps(trust_policy),\n Description=\"IAM role for AgentCore Gateway multi-ISV tutorial\",\n)\nROLE_ARN = role_resp[\"Role\"][\"Arn\"]\niam.put_role_policy(\n RoleName=ROLE_NAME,\n PolicyName=\"AgentCorePolicy\",\n PolicyDocument=json.dumps({\n \"Version\": \"2012-10-17\",\n \"Statement\": [{\n \"Effect\": \"Allow\",\n \"Action\": [\n \"bedrock-agentcore:*\", \"bedrock:*\", \"agent-credential-provider:*\",\n \"iam:PassRole\", \"secretsmanager:GetSecretValue\", \"s3:GetObject\",\n ],\n \"Resource\": \"*\",\n }],\n }),\n)\nprint(f\" \u2713 IAM role created: {ROLE_NAME}\")\ntime.sleep(10)\n\n# --- Gateway ---\ngateway_client = boto3.client(\"bedrock-agentcore-control\", region_name=REGION)\ngw_resp = gateway_client.create_gateway(\n name=GATEWAY_NAME,\n roleArn=ROLE_ARN,\n protocolType=\"MCP\",\n authorizerType=\"CUSTOM_JWT\",\n authorizerConfiguration={\n \"customJWTAuthorizer\": {\n \"discoveryUrl\": DISCOVERY_URL,\n \"allowedClients\": [GW_CLIENT_ID],\n }\n },\n)\nGATEWAY_ID = gw_resp[\"gatewayId\"]\nGATEWAY_URL = gw_resp[\"gatewayUrl\"]\n\nprint(f\" Waiting for gateway READY...\")\nfor _ in range(60):\n status = gateway_client.get_gateway(gatewayIdentifier=GATEWAY_ID)[\"status\"]\n if status == \"READY\":\n break\n if status == \"FAILED\":\n raise RuntimeError(\"Gateway creation failed\")\n time.sleep(5)\nelse:\n raise TimeoutError(\"Gateway did not reach READY within 5 minutes\")\n\nprint(f\"\\n{'='*60}\")\nprint(f\" \u2713 GATEWAY IS READY\")\nprint(f\"{'='*60}\")\nprint(f\" Gateway name: {GATEWAY_NAME}\")\nprint(f\" Gateway ID: {GATEWAY_ID}\")\nprint(f\" Gateway URL: {GATEWAY_URL}\")\nprint(f\"{'='*60}\")" }, { "cell_type": "markdown", @@ -259,7 +176,7 @@ "\n", "AgentCore Gateway needs OAuth2 credentials to authenticate to Salesforce on behalf of the agent.\n", "\n", - "> **Important:** Use `CustomOauth2` — NOT `SalesforceOauth2`. The built-in Salesforce vendor uses `login.salesforce.com` which does not support `client_credentials` on Developer Edition org domains. With `CustomOauth2`, we specify the org-specific token endpoint directly." + "> **Important:** Use `CustomOauth2` \u2014 NOT `SalesforceOauth2`. The built-in Salesforce vendor uses `login.salesforce.com` which does not support `client_credentials` on Developer Edition org domains. With `CustomOauth2`, we specify the org-specific token endpoint directly." ] }, { @@ -267,66 +184,26 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "CREDENTIAL_PROVIDER_NAME = f\"{GATEWAY_NAME}-sf-oauth\"\n", - "\n", - "identity_client = boto3.client(\"bedrock-agentcore-control\", region_name=REGION)\n", - "\n", - "cred_resp = identity_client.create_oauth2_credential_provider(\n", - " name=CREDENTIAL_PROVIDER_NAME,\n", - " credentialProviderVendor=\"CustomOauth2\",\n", - " oauth2ProviderConfigInput={\n", - " \"customOauth2ProviderConfig\": {\n", - " \"clientId\": SF_CLIENT_ID,\n", - " \"clientSecret\": SF_CLIENT_SECRET,\n", - " \"oauthDiscovery\": {\n", - " \"authorizationServerMetadata\": {\n", - " \"issuer\": f\"https://{SF_DOMAIN}.develop.my.salesforce.com\",\n", - " \"authorizationEndpoint\": f\"https://{SF_DOMAIN}.develop.my.salesforce.com/services/oauth2/authorize\",\n", - " \"tokenEndpoint\": f\"https://{SF_DOMAIN}.develop.my.salesforce.com/services/oauth2/token\",\n", - " }\n", - " },\n", - " }\n", - " },\n", - ")\n", - "\n", - "CREDENTIAL_PROVIDER_ARN = cred_resp[\"credentialProviderArn\"]\n", - "print(f\"Created credential provider: {CREDENTIAL_PROVIDER_NAME}\")\n", - "print(f\"ARN: {CREDENTIAL_PROVIDER_ARN}\")" - ] + "source": "CREDENTIAL_PROVIDER_NAME = f\"{GATEWAY_NAME}-sf-oauth\"\n\nidentity_client = boto3.client(\"bedrock-agentcore-control\", region_name=REGION)\n\ncred_resp = identity_client.create_oauth2_credential_provider(\n name=CREDENTIAL_PROVIDER_NAME,\n credentialProviderVendor=\"CustomOauth2\",\n oauth2ProviderConfigInput={\n \"customOauth2ProviderConfig\": {\n \"clientId\": SF_CLIENT_ID,\n \"clientSecret\": SF_CLIENT_SECRET,\n \"oauthDiscovery\": {\n \"discoveryUrl\": f\"https://{SF_DOMAIN}.develop.my.salesforce.com/.well-known/openid-configuration\",\n },\n }\n },\n)\n\nCREDENTIAL_PROVIDER_ARN = cred_resp[\"credentialProviderArn\"]\nprint(f\"Created credential provider: {CREDENTIAL_PROVIDER_NAME}\")\nprint(f\"ARN: {CREDENTIAL_PROVIDER_ARN}\")" }, { - "cell_type": "markdown", + "cell_type": "code", + "source": "# Summary \u2014 copy these values for the Console step and for notebooks 02/03\nprint(f\"\"\"\n{'='*60}\n CONNECTION DETAILS (save for notebooks 02 + 03)\n{'='*60}\n Gateway name: {GATEWAY_NAME}\n Gateway ID: {GATEWAY_ID}\n Gateway URL: {GATEWAY_URL}\n Cognito token endpoint: {TOKEN_ENDPOINT}\n Cognito client ID: {GW_CLIENT_ID}\n Cognito client secret: {GW_CLIENT_SECRET[:8]}...\n OAuth scope: {FULL_SCOPE}\n Credential provider: {CREDENTIAL_PROVIDER_NAME}\n{'='*60}\n\n FOR THE CONSOLE STEP (Step 5):\n - Gateway: {GATEWAY_NAME}\n - Target name: salesforce-target\n - Server URL: https://{{domainName}}.develop.my.salesforce.com/services/data/v62.0\n - Credential provider: {CREDENTIAL_PROVIDER_NAME}\n{'='*60}\n\"\"\")", "metadata": {}, - "source": [ - "## Step 5: Add Salesforce as Gateway Target\n", - "\n", - "Now we add Salesforce Lightning Platform as an integration target on the gateway. The gateway uses the pre-built Salesforce template to expose Salesforce REST APIs as MCP tools." - ] + "execution_count": null, + "outputs": [] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], - "source": "SF_TARGET_NAME = \"salesforce-target\"\nSF_SERVER_URL = f\"https://{SF_DOMAIN}.develop.my.salesforce.com/services/data/v62.0\"\n\ntarget_resp = gateway_client.create_gateway_target(\n gatewayIdentifier=GATEWAY_ID,\n name=SF_TARGET_NAME,\n targetConfiguration={\n \"mcp\": {\n \"openApiSchema\": {\n \"s3\": {\n \"uri\": \"s3://amazonbedrockagentcore-built-sampleschemas455e0815-oj7jujcd8xiu/salesforce-open-api.json\"\n }\n }\n }\n },\n credentialProviderConfigurations=[\n {\n \"credentialProviderType\": \"OAUTH\",\n \"credentialProvider\": {\n \"oauthCredentialProvider\": {\n \"providerArn\": CREDENTIAL_PROVIDER_ARN,\n \"scopes\": [],\n \"grantType\": \"CLIENT_CREDENTIALS\",\n }\n },\n }\n ],\n)\n\nSF_TARGET_ID = target_resp[\"targetId\"]\nprint(f\"Created Salesforce target: {SF_TARGET_NAME} ({SF_TARGET_ID})\")\nprint(\"Waiting for target to reach READY status...\")" + "source": "## Step 5: Add Salesforce as Gateway Target (Console)\n\nThe Salesforce integration uses the **built-in Integration Provider Template** \u2014 a pre-defined OpenAPI schema managed by AgentCore. This target type can only be created via the [AWS Management Console](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-target-integrations.html#gateway-target-integrations-supported-apis-salesforce).\n\n### Steps:\n\n1. Open the [AgentCore Gateway console](https://console.aws.amazon.com/bedrock-agentcore/home#/gateways)\n2. Select your gateway (`GATEWAY_NAME` printed above)\n3. Click **Add target**\n4. Select **Integration provider template** \u2192 **Salesforce Lightning Platform**\n5. Configure:\n - **Target name:** `salesforce-target`\n - **Server URL:** `https://{domainName}.develop.my.salesforce.com/services/data/v62.0`\n - **Authentication:** OAuth2\n - **Credential provider:** Select the credential provider created above\n6. Click **Create target**\n7. Wait for target status to become **READY** (1\u20132 minutes)\n\nOnce the target is READY, run the next cell to verify." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Wait for target to become READY\n", - "gateway_mcp_client.wait_for_target_ready(\n", - " client=gateway_client,\n", - " gateway_id=GATEWAY_ID,\n", - " target_name=SF_TARGET_NAME,\n", - " region=REGION,\n", - " timeout=300,\n", - ")\n", - "print(\"\\n✓ Salesforce target is READY\")" - ] + "source": "# Verify the Salesforce target is READY\nSF_TARGET_NAME = \"salesforce-target\"\n\ngateway_mcp_client.wait_for_target_ready(\n client=gateway_client,\n gateway_id=GATEWAY_ID,\n target_name=SF_TARGET_NAME,\n region=REGION,\n timeout=300,\n)\n\n# Get the target ID for cleanup later\ntargets = gateway_client.list_gateway_targets(gatewayIdentifier=GATEWAY_ID)\nSF_TARGET_ID = next(\n t[\"targetId\"] for t in targets[\"items\"] if t[\"name\"] == SF_TARGET_NAME\n)\nprint(f\"\u2713 Salesforce target is READY (ID: {SF_TARGET_ID})\")" }, { "cell_type": "markdown", @@ -385,93 +262,28 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Query accounts using SOQL (more reliable than getAccountList)\n", - "result = mcp.call_tool(\n", - " \"salesforce-target___queryAccounts\",\n", - " {\"domainName\": SF_DOMAIN, \"q\": \"SELECT Id, Name, Industry FROM Account LIMIT 5\"},\n", - ")\n", - "print(\"=== Query Accounts (SOQL) ===\")\n", - "print(json.dumps(result.get(\"result\", {}), indent=2)[:2000])" - ] + "source": "# Query accounts using SOQL\nresult = mcp.call_tool(\n \"salesforce-target___queryAccounts\",\n {\"domainName\": SF_DOMAIN, \"q\": \"SELECT Id, Name, Industry FROM Account LIMIT 5\"},\n)\nprint(\"=== Salesforce Accounts ===\")\ncontent = result.get(\"result\", {}).get(\"content\", [])\nfor item in content:\n if item.get(\"type\") == \"text\":\n data = json.loads(item[\"text\"])\n print(f\" Total records: {data.get('totalSize', '?')}\\n\")\n for rec in data.get(\"records\", []):\n print(f\" {rec.get('Name', '?'):40s} Industry: {rec.get('Industry', 'N/A')}\")" }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Get account list (returns recently viewed accounts)\n", - "result = mcp.call_tool(\n", - " \"salesforce-target___getAccountList\",\n", - " {\"domainName\": SF_DOMAIN},\n", - ")\n", - "print(\"=== Get Account List ===\")\n", - "print(json.dumps(result.get(\"result\", {}), indent=2)[:2000])" - ] + "source": "# Get account list (returns recently viewed accounts)\nresult = mcp.call_tool(\n \"salesforce-target___getAccountList\",\n {\"domainName\": SF_DOMAIN},\n)\nprint(\"=== Recently Viewed Accounts ===\")\ncontent = result.get(\"result\", {}).get(\"content\", [])\nfor item in content:\n if item.get(\"type\") == \"text\":\n data = json.loads(item[\"text\"])\n for rec in data.get(\"recentItems\", data.get(\"records\", []))[:5]:\n print(f\" {rec.get('Name', rec.get('Id', '?'))}\")" }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Create a test account\n", - "# Note: pass Content-Type as empty string to work around a known header duplication issue\n", - "result = mcp.call_tool(\n", - " \"salesforce-target___createAccount\",\n", - " {\n", - " \"domainName\": SF_DOMAIN,\n", - " \"Content-Type\": \"\",\n", - " \"Name\": \"AgentCore Tutorial Test Account\",\n", - " \"Industry\": \"Technology\",\n", - " \"Description\": \"Created by AgentCore Gateway multi-ISV tutorial\",\n", - " },\n", - ")\n", - "print(\"=== Create Account ===\")\n", - "print(json.dumps(result.get(\"result\", {}), indent=2)[:2000])" - ] + "source": "# Create a test account\n# Content-Type is a restricted header managed by the gateway \u2014 pass \"\" to avoid duplication\nresult = mcp.call_tool(\n \"salesforce-target___createAccount\",\n {\n \"domainName\": SF_DOMAIN,\n \"Content-Type\": \"\",\n \"Name\": \"AgentCore Tutorial Test Account\",\n \"Industry\": \"Technology\",\n \"Description\": \"Created by AgentCore Gateway multi-ISV tutorial\",\n },\n)\nprint(\"=== Create Account ===\")\ncontent = result.get(\"result\", {}).get(\"content\", [])\nfor item in content:\n if item.get(\"type\") == \"text\":\n data = json.loads(item[\"text\"])\n if data.get(\"success\"):\n print(f\" \u2713 Account created: {data['id']}\")\n else:\n print(f\" \u2717 Failed: {data.get('errors', data)}\")" }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Describe an SObject to see available fields\n", - "result = mcp.call_tool(\n", - " \"salesforce-target___describeSObject\",\n", - " {\"domainName\": SF_DOMAIN, \"sObjectType\": \"Account\"},\n", - ")\n", - "print(\"=== Describe Account SObject ===\")\n", - "content = result.get(\"result\", {}).get(\"content\", [])\n", - "if content:\n", - " text = content[0].get(\"text\", \"\")\n", - " try:\n", - " data = json.loads(text)\n", - " fields = data.get(\"fields\", [])[:10]\n", - " print(f\"Total fields: {len(data.get('fields', []))}\")\n", - " print(\"First 10 fields:\")\n", - " for f in fields:\n", - " print(f\" - {f['name']} ({f['type']})\")\n", - " except json.JSONDecodeError:\n", - " print(text[:1000])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Known Issues & Workarounds\n", - "\n", - "| Issue | Workaround |\n", - "|---|---|\n", - "| `create*` tools fail with HTTP 415 | Pass `\"Content-Type\": \"\"` (empty string) in tool arguments |\n", - "| Tool calls return HTTP 420 | Always include `domainName` in arguments; also check if org is hibernating |\n", - "| `getAccountList` returns only recently viewed | Use `queryAccounts` with SOQL: `SELECT Id, Name FROM Account` |\n", - "| `SalesforceOauth2` vendor fails with Dev orgs | Use `CustomOauth2` with org-specific token endpoint (as done above) |\n", - "| Org hibernation (HTTP 420 after inactivity) | Log into Salesforce web UI to wake the org |" - ] + "source": "# Describe an SObject to see available fields\nresult = mcp.call_tool(\n \"salesforce-target___describeSObject\",\n {\"domainName\": SF_DOMAIN, \"sObject\": \"Account\"},\n)\nprint(\"=== Account SObject Fields ===\")\ncontent = result.get(\"result\", {}).get(\"content\", [])\nfor item in content:\n if item.get(\"type\") == \"text\":\n data = json.loads(item[\"text\"])\n fields = data.get(\"fields\", [])\n print(f\" Total fields: {len(fields)}\\n First 10:\")\n for f in fields[:10]:\n print(f\" {f['name']:30s} ({f['type']})\")" }, { "cell_type": "markdown", @@ -487,7 +299,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "from strands import Agent\nfrom strands.tools.mcp import MCPClient\nfrom mcp import ClientSession\nfrom mcp.client.streamable_http import streamablehttp_client\n\n# Connect to gateway via MCP\nmcp_client = MCPClient(\n lambda: streamablehttp_client(\n url=GATEWAY_URL,\n headers={\n \"Authorization\": f\"Bearer {get_gw_token()}\",\n \"MCP-Protocol-Version\": \"2025-03-26\",\n },\n )\n)\n\nSYSTEM_PROMPT = (\n \"You are a helpful assistant with access to Salesforce tools. \"\n f\"Always include domainName='{SF_DOMAIN}' in every Salesforce tool call. \"\n \"Do not pass a Content-Type parameter to Salesforce create operations — omit it entirely or pass empty string. \"\n \"Use queryAccounts with SOQL for listing accounts rather than getAccountList.\"\n)\n\nwith mcp_client:\n agent = Agent(\n model=\"us.anthropic.claude-sonnet-4-6-v1\",\n system_prompt=SYSTEM_PROMPT,\n tools=mcp_client.list_tools_sync(),\n )\n result = agent(\"List all Salesforce accounts with their industry\")\n print(result)" + "source": "from strands import Agent\nfrom strands.tools.mcp import MCPClient\nfrom mcp.client.streamable_http import streamablehttp_client\n\n# Connect to gateway via MCP\nmcp_client = MCPClient(\n lambda: streamablehttp_client(\n url=GATEWAY_URL,\n headers={\n \"Authorization\": f\"Bearer {get_gw_token()}\",\n \"MCP-Protocol-Version\": \"2025-03-26\",\n },\n )\n)\n\nSYSTEM_PROMPT = (\n \"You are a helpful assistant with access to Salesforce tools. \"\n f\"Always include domainName='{SF_DOMAIN}' in every Salesforce tool call. \"\n \"Use queryAccounts with SOQL for listing accounts rather than getAccountList.\"\n)\n\nwith mcp_client:\n agent = Agent(\n model=MODEL_ID,\n system_prompt=SYSTEM_PROMPT,\n tools=mcp_client.list_tools_sync(),\n )\n result = agent(\"List all Salesforce accounts with their industry\")\n print(result)" }, { "cell_type": "markdown", @@ -503,51 +315,28 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Delete gateway target\n", - "print(\"Deleting Salesforce gateway target...\")\n", - "gateway_client.delete_gateway_target(\n", - " gatewayIdentifier=GATEWAY_ID,\n", - " targetId=SF_TARGET_ID,\n", - ")\n", - "print(\" ✓ Target deleted\")\n", - "\n", - "# Delete credential provider\n", - "print(\"Deleting credential provider...\")\n", - "identity_client.delete_oauth2_credential_provider(name=CREDENTIAL_PROVIDER_NAME)\n", - "print(\" ✓ Credential provider deleted\")\n", - "\n", - "# Delete gateway\n", - "print(\"Deleting gateway...\")\n", - "gateway_client.delete_gateway(gatewayIdentifier=GATEWAY_ID)\n", - "print(\" ✓ Gateway deleted\")\n", - "\n", - "# Delete Cognito resources\n", - "print(\"Deleting Cognito resources...\")\n", - "cognito_client.delete_user_pool_domain(Domain=COGNITO_DOMAIN, UserPoolId=USER_POOL_ID)\n", - "cognito_client.delete_user_pool(UserPoolId=USER_POOL_ID)\n", - "print(\" ✓ Cognito pool deleted\")\n", - "\n", - "# Delete IAM role\n", - "print(\"Deleting IAM role...\")\n", - "iam.delete_role(RoleName=ROLE_NAME)\n", - "print(\" ✓ IAM role deleted\")\n", - "\n", - "print(\"\\n✓ All resources cleaned up\")" - ] + "source": "# Delete test data created in Salesforce\nprint(\"Cleaning up Salesforce test data...\")\ntry:\n query_result = mcp.call_tool(\n \"salesforce-target___queryAccounts\",\n {\"domainName\": SF_DOMAIN, \"q\": \"SELECT Id FROM Account WHERE Name = 'AgentCore Tutorial Test Account'\"},\n )\n content = query_result.get(\"result\", {}).get(\"content\", [])\n for item in content:\n if item.get(\"type\") == \"text\":\n data = json.loads(item[\"text\"])\n for record in data.get(\"records\", []):\n mcp.call_tool(\n \"salesforce-target___deleteAccount\",\n {\"domainName\": SF_DOMAIN, \"Content-Type\": \"\", \"accountId\": record[\"Id\"]},\n )\n print(f\" \u2713 Deleted test account {record['Id']}\")\nexcept Exception as e:\n print(f\" (Could not clean up SF test data: {e})\")\n\n# Delete gateway target\nprint(\"Deleting Salesforce gateway target...\")\ngateway_client.delete_gateway_target(\n gatewayIdentifier=GATEWAY_ID,\n targetId=SF_TARGET_ID,\n)\ntime.sleep(5)\nprint(\" \u2713 Target deleted\")\n\n# Delete credential provider\nprint(\"Deleting credential provider...\")\nidentity_client.delete_oauth2_credential_provider(name=CREDENTIAL_PROVIDER_NAME)\nprint(\" \u2713 Credential provider deleted\")\n\n# Delete gateway\nprint(\"Deleting gateway...\")\ngateway_client.delete_gateway(gatewayIdentifier=GATEWAY_ID)\ntime.sleep(5)\nprint(\" \u2713 Gateway deleted\")\n\n# Delete Cognito resources\nprint(\"Deleting Cognito resources...\")\ncognito_client.delete_user_pool_domain(Domain=COGNITO_DOMAIN, UserPoolId=USER_POOL_ID)\ncognito_client.delete_user_pool(UserPoolId=USER_POOL_ID)\nprint(\" \u2713 Cognito pool deleted\")\n\n# Delete IAM role (must remove inline policy first)\nprint(\"Deleting IAM role...\")\niam.delete_role_policy(RoleName=ROLE_NAME, PolicyName=\"AgentCorePolicy\")\niam.delete_role(RoleName=ROLE_NAME)\nprint(\" \u2713 IAM role deleted\")\n\nprint(\"\\n\u2713 All resources cleaned up\")" } ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": ".venv", "language": "python", "name": "python3" }, "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", "name": "python", - "version": "3.11.0" + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.4" } }, "nbformat": 4, "nbformat_minor": 4 -} \ No newline at end of file +} diff --git a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/02-sap-mcp-server-target.ipynb b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/02-sap-mcp-server-target.ipynb index 3df6300f3..a7cb0fcb6 100644 --- a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/02-sap-mcp-server-target.ipynb +++ b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/02-sap-mcp-server-target.ipynb @@ -37,7 +37,7 @@ "Before starting this notebook, you need:\n", "\n", "1. **AWS account** with access to Amazon Bedrock AgentCore\n", - "2. **AWS for SAP MCP Server** deployed — see [Getting Started](https://docs.aws.amazon.com/mcp-sap/latest/awsforsapmcp/getting-started.html)\n", + "2. **AWS for SAP MCP Server** deployed \u2014 see [Getting Started](https://docs.aws.amazon.com/mcp-sap/latest/awsforsapmcp/getting-started.html)\n", "3. **SAP MCP Server endpoint URL** and **Cognito credentials** for M2M authentication\n", "4. **Python 3.11+** with Jupyter kernel\n", "5. **AWS CLI** configured with appropriate credentials\n", @@ -50,9 +50,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "!pip install -r requirements.txt --quiet" - ] + "source": "!pip install --force-reinstall -U -r requirements.txt --quiet" }, { "cell_type": "code", @@ -63,6 +61,7 @@ "import getpass\n", "import json\n", "import logging\n", + "import os\n", "import time\n", "import uuid\n", "\n", @@ -72,6 +71,9 @@ "\n", "import gateway_mcp_client\n", "\n", + "# AWS credentials \u2014 set your profile\n", + "os.environ[\"AWS_PROFILE\"] = \"default\" # Change to your profile\n", + "\n", "logging.basicConfig(\n", " level=logging.INFO,\n", " format=\"%(asctime)s | %(levelname)s | %(name)s | %(message)s\",\n", @@ -79,8 +81,17 @@ ")\n", "\n", "session = Session()\n", - "REGION = session.region_name or \"us-east-1\"\n", - "print(f\"Using region: {REGION}\")" + "REGION = session.region_name or \"eu-west-1\"\n", + "ACCOUNT_ID = boto3.client(\"sts\").get_caller_identity()[\"Account\"]\n", + "\n", + "# Derive Bedrock model ID geo prefix from region\n", + "GEO_PREFIX = {\"us-\": \"us\", \"eu-\": \"eu\", \"ap-\": \"ap\", \"ca-\": \"us\", \"sa-\": \"us\"}\n", + "MODEL_PREFIX = next((v for k, v in GEO_PREFIX.items() if REGION.startswith(k)), \"us\")\n", + "MODEL_ID = f\"{MODEL_PREFIX}.anthropic.claude-sonnet-4-6\"\n", + "\n", + "print(f\"Using region: {REGION}\")\n", + "print(f\"AWS account: {ACCOUNT_ID}\")\n", + "print(f\"Model ID: {MODEL_ID}\")" ] }, { @@ -108,39 +119,7 @@ { "cell_type": "markdown", "metadata": {}, - "source": [ - "## AWS for SAP MCP Server — Architecture\n", - "\n", - "The [AWS for SAP MCP Server](https://docs.aws.amazon.com/mcp-sap/latest/awsforsapmcp/introduction.html) runs on Amazon Bedrock AgentCore Runtime and provides AI agents with access to SAP S/4HANA and SAP ECC OData V2 services.\n", - "\n", - "**Key characteristics:**\n", - "- Communicates via **Streamable HTTP** on a `/mcp` endpoint\n", - "- **Read-only by default** — write operations require explicit opt-in\n", - "- Credentials are **never stored on disk** — retrieved at runtime from AWS Secrets Manager\n", - "- Supports **Basic Auth** or **OAuth 2.0** for outbound SAP authentication\n", - "- Deployed via **CloudFormation template**\n", - "\n", - "### Available Tools\n", - "\n", - "| Tool | Category | Description |\n", - "|---|---|---|\n", - "| `find_sap_services` | Read | Discover available SAP OData services from the catalog |\n", - "| `get_metadata` | Read | Retrieve OData service metadata (entity types, properties) |\n", - "| `get_service_hints` | Read | Get usage guidance for specific services |\n", - "| `odata_read` | Read | Read data from SAP OData entity sets with filtering |\n", - "| `odata_count` | Read | Count records in an entity set (use before reads) |\n", - "| `odata_create` | Write | Create new entity records (deep insert supported) |\n", - "| `odata_update` | Write | Update existing entity records |\n", - "| `odata_delete` | Write | Delete entity records |\n", - "| `odata_function_import` | Write | Execute custom backend logic |\n", - "\n", - "> **Security:** Write tools are only registered when explicitly enabled in the SAP MCP Server configuration. Follow the principle of least privilege — only enable the specific write operations your use case requires." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": "REUSE_GATEWAY = input(\"Reuse an existing gateway? (yes/no): \").strip().lower() == \"yes\"\n\nif REUSE_GATEWAY:\n GATEWAY_ID = input(\"Enter existing Gateway ID: \")\n GATEWAY_URL = input(\"Enter existing Gateway URL: \")\n GW_CLIENT_ID = input(\"Enter Cognito Client ID for the gateway: \")\n GW_CLIENT_SECRET = getpass.getpass(\"Enter Cognito Client Secret for the gateway: \")\n TOKEN_ENDPOINT = input(\"Enter Cognito token endpoint: \")\n FULL_SCOPE = input(\"Enter OAuth scope: \")\n CREATED_GATEWAY = False\n print(f\"\\nReusing gateway: {GATEWAY_ID}\")\nelse:\n CREATED_GATEWAY = True\n GATEWAY_NAME = f\"multi-isv-sap-tutorial-{str(uuid.uuid4())[:8]}\"\n print(f\"Creating new gateway: {GATEWAY_NAME}\")\n\n # Create Cognito User Pool\n cognito_client = boto3.client(\"cognito-idp\", region_name=REGION)\n pool_resp = cognito_client.create_user_pool(\n PoolName=f\"{GATEWAY_NAME}-pool\",\n Policies={\"PasswordPolicy\": {\"MinimumLength\": 8}},\n )\n USER_POOL_ID = pool_resp[\"UserPool\"][\"Id\"]\n\n COGNITO_DOMAIN = f\"{GATEWAY_NAME}-domain\"\n cognito_client.create_user_pool_domain(Domain=COGNITO_DOMAIN, UserPoolId=USER_POOL_ID)\n TOKEN_ENDPOINT = f\"https://{COGNITO_DOMAIN}.auth.{REGION}.amazoncognito.com/oauth2/token\"\n\n RESOURCE_SERVER_ID = f\"{GATEWAY_NAME}-id\"\n SCOPE_NAME = \"invoke\"\n cognito_client.create_resource_server(\n UserPoolId=USER_POOL_ID,\n Identifier=RESOURCE_SERVER_ID,\n Name=f\"{GATEWAY_NAME} Gateway\",\n Scopes=[{\"ScopeName\": SCOPE_NAME, \"ScopeDescription\": \"Invoke gateway\"}],\n )\n FULL_SCOPE = f\"{RESOURCE_SERVER_ID}/{SCOPE_NAME}\"\n\n app_resp = cognito_client.create_user_pool_client(\n UserPoolId=USER_POOL_ID,\n ClientName=f\"{GATEWAY_NAME}-client\",\n GenerateSecret=True,\n AllowedOAuthFlows=[\"client_credentials\"],\n AllowedOAuthScopes=[FULL_SCOPE],\n AllowedOAuthFlowsUserPoolClient=True,\n )\n GW_CLIENT_ID = app_resp[\"UserPoolClient\"][\"ClientId\"]\n GW_CLIENT_SECRET = app_resp[\"UserPoolClient\"][\"ClientSecret\"]\n DISCOVERY_URL = f\"https://cognito-idp.{REGION}.amazonaws.com/{USER_POOL_ID}/.well-known/openid-configuration\"\n\n # Create IAM role\n iam = boto3.client(\"iam\")\n ROLE_NAME = f\"agentcore-{GATEWAY_NAME}-role\"\n trust_policy = {\n \"Version\": \"2012-10-17\",\n \"Statement\": [{\n \"Effect\": \"Allow\",\n \"Principal\": {\"Service\": \"bedrock-agentcore.amazonaws.com\"},\n \"Action\": \"sts:AssumeRole\",\n }],\n }\n role_resp = iam.create_role(\n RoleName=ROLE_NAME,\n AssumeRolePolicyDocument=json.dumps(trust_policy),\n Description=\"IAM role for AgentCore Gateway SAP tutorial\",\n )\n ROLE_ARN = role_resp[\"Role\"][\"Arn\"]\n time.sleep(10)\n\n # Create gateway\n gw_client = boto3.client(\"bedrock-agentcore-control\", region_name=REGION)\n gw_resp = gw_client.create_gateway(\n name=GATEWAY_NAME,\n roleArn=ROLE_ARN,\n protocolType=\"MCP\",\n authorizerType=\"CUSTOM_JWT\",\n authorizerConfiguration={\n \"customJWTAuthorizer\": {\n \"discoveryUrl\": DISCOVERY_URL,\n \"allowedClients\": [GW_CLIENT_ID],\n }\n },\n )\n GATEWAY_ID = gw_resp[\"gatewayId\"]\n GATEWAY_URL = gw_resp[\"gatewayUrl\"]\n\n print(f\"Gateway created: {GATEWAY_ID}\")\n print(\"Waiting for READY...\")\n for _ in range(60):\n status = gw_client.get_gateway(gatewayIdentifier=GATEWAY_ID)[\"status\"]\n if status == \"READY\":\n break\n time.sleep(5)\n print(\"✓ Gateway is READY\")\n\ngateway_client = boto3.client(\"bedrock-agentcore-control\", region_name=REGION)" + "source": "## AWS for SAP MCP Server \u2014 Architecture\n\nThe [AWS for SAP MCP Server](https://docs.aws.amazon.com/mcp-sap/latest/awsforsapmcp/introduction.html) runs on Amazon Bedrock AgentCore Runtime and provides AI agents with access to SAP S/4HANA and SAP ECC OData V2 services.\n\n**Key characteristics:**\n- Communicates via **Streamable HTTP** on a `/mcp` endpoint\n- **Read-only by default** \u2014 write operations require explicit opt-in\n- Credentials are **never stored on disk** \u2014 retrieved at runtime from AWS Secrets Manager\n- Supports **Basic Auth** or **OAuth 2.0** for outbound SAP authentication\n- See [Getting Started](https://docs.aws.amazon.com/mcp-sap/latest/awsforsapmcp/getting-started.html) for deployment options\n\n### Available Tools\n\n| Tool | Category | Description |\n|---|---|---|\n| `find_sap_services` | Read | Discover available SAP OData services from the catalog |\n| `get_metadata` | Read | Retrieve OData service metadata (entity types, properties) |\n| `get_service_hints` | Read | Get usage guidance for specific services |\n| `odata_read` | Read | Read data from SAP OData entity sets with filtering |\n| `odata_count` | Read | Count records in an entity set (use before reads) |\n| `odata_create` | Write | Create new entity records (deep insert supported) |\n| `odata_update` | Write | Update existing entity records |\n| `odata_delete` | Write | Delete entity records |\n| `odata_function_import` | Write | Execute custom backend logic |\n\n> **Security:** Write tools are only registered when explicitly enabled in the SAP MCP Server configuration. Follow the principle of least privilege \u2014 only enable the specific write operations your use case requires." }, { "cell_type": "code", @@ -155,200 +134,44 @@ " client_secret=SAP_CLIENT_SECRET,\n", " scope=SAP_SCOPE,\n", ")\n", - "print(f\"✓ SAP MCP Server token obtained ({len(sap_token)} chars)\")" + "print(f\"\u2713 SAP MCP Server token obtained ({len(sap_token)} chars)\")" ] }, { "cell_type": "markdown", "metadata": {}, - "source": [ - "## Step 2: Create Gateway (or Reuse Existing)\n", - "\n", - "If you already have a gateway from the Salesforce notebook, you can skip this step and provide the existing gateway details." - ] + "source": "## Step 2: Connect to Existing Gateway\n\nThis notebook adds the SAP target to the **same gateway** created in [01-salesforce-gateway-target.ipynb](01-salesforce-gateway-target.ipynb). Provide the Gateway ID from notebook 01." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "REUSE_GATEWAY = input(\"Reuse an existing gateway? (yes/no): \").strip().lower() == \"yes\"\n", - "\n", - "if REUSE_GATEWAY:\n", - " GATEWAY_ID = input(\"Enter existing Gateway ID: \")\n", - " GATEWAY_URL = input(\"Enter existing Gateway URL: \")\n", - " GW_CLIENT_ID = input(\"Enter Cognito Client ID for the gateway: \")\n", - " GW_CLIENT_SECRET = getpass.getpass(\"Enter Cognito Client Secret for the gateway: \")\n", - " TOKEN_ENDPOINT = input(\"Enter Cognito token endpoint: \")\n", - " FULL_SCOPE = input(\"Enter OAuth scope: \")\n", - " CREATED_GATEWAY = False\n", - " print(f\"\\nReusing gateway: {GATEWAY_ID}\")\n", - "else:\n", - " CREATED_GATEWAY = True\n", - " GATEWAY_NAME = f\"multi-isv-sap-tutorial-{str(uuid.uuid4())[:8]}\"\n", - " print(f\"Creating new gateway: {GATEWAY_NAME}\")\n", - "\n", - " # Create Cognito User Pool\n", - " cognito_client = boto3.client(\"cognito-idp\", region_name=REGION)\n", - " pool_resp = cognito_client.create_user_pool(\n", - " PoolName=f\"{GATEWAY_NAME}-pool\",\n", - " Policies={\"PasswordPolicy\": {\"MinimumLength\": 8}},\n", - " )\n", - " USER_POOL_ID = pool_resp[\"UserPool\"][\"Id\"]\n", - "\n", - " COGNITO_DOMAIN = f\"{GATEWAY_NAME}-domain\"\n", - " cognito_client.create_user_pool_domain(Domain=COGNITO_DOMAIN, UserPoolId=USER_POOL_ID)\n", - " TOKEN_ENDPOINT = f\"https://{COGNITO_DOMAIN}.auth.{REGION}.amazoncognito.com/oauth2/token\"\n", - "\n", - " RESOURCE_SERVER_ID = f\"{GATEWAY_NAME}-id\"\n", - " SCOPE_NAME = \"invoke\"\n", - " cognito_client.create_resource_server(\n", - " UserPoolId=USER_POOL_ID,\n", - " Identifier=RESOURCE_SERVER_ID,\n", - " Name=f\"{GATEWAY_NAME} Gateway\",\n", - " Scopes=[{\"ScopeName\": SCOPE_NAME, \"ScopeDescription\": \"Invoke gateway\"}],\n", - " )\n", - " FULL_SCOPE = f\"{RESOURCE_SERVER_ID}/{SCOPE_NAME}\"\n", - "\n", - " app_resp = cognito_client.create_user_pool_client(\n", - " UserPoolId=USER_POOL_ID,\n", - " ClientName=f\"{GATEWAY_NAME}-client\",\n", - " GenerateSecret=True,\n", - " AllowedOAuthFlows=[\"client_credentials\"],\n", - " AllowedOAuthScopes=[FULL_SCOPE],\n", - " AllowedOAuthFlowsUserPoolClient=True,\n", - " )\n", - " GW_CLIENT_ID = app_resp[\"UserPoolClient\"][\"ClientId\"]\n", - " GW_CLIENT_SECRET = app_resp[\"UserPoolClient\"][\"ClientSecret\"]\n", - " DISCOVERY_URL = f\"https://cognito-idp.{REGION}.amazonaws.com/{USER_POOL_ID}/.well-known/openid-configuration\"\n", - "\n", - " # Create IAM role\n", - " iam = boto3.client(\"iam\")\n", - " ROLE_NAME = f\"agentcore-{GATEWAY_NAME}-role\"\n", - " trust_policy = {\n", - " \"Version\": \"2012-10-17\",\n", - " \"Statement\": [{\n", - " \"Effect\": \"Allow\",\n", - " \"Principal\": {\"Service\": \"gateway.bedrock-agentcore.amazonaws.com\"},\n", - " \"Action\": \"sts:AssumeRole\",\n", - " }],\n", - " }\n", - " role_resp = iam.create_role(\n", - " RoleName=ROLE_NAME,\n", - " AssumeRolePolicyDocument=json.dumps(trust_policy),\n", - " Description=\"IAM role for AgentCore Gateway SAP tutorial\",\n", - " )\n", - " ROLE_ARN = role_resp[\"Role\"][\"Arn\"]\n", - " time.sleep(10)\n", - "\n", - " # Create gateway\n", - " gw_client = boto3.client(\"bedrock-agentcore-control\", region_name=REGION)\n", - " gw_resp = gw_client.create_gateway(\n", - " name=GATEWAY_NAME,\n", - " roleArn=ROLE_ARN,\n", - " protocolType=\"MCP\",\n", - " authorizerType=\"CUSTOM_JWT\",\n", - " authorizerConfiguration={\n", - " \"customJWTAuthorizer\": {\n", - " \"discoveryUrl\": DISCOVERY_URL,\n", - " \"allowedAudiences\": [GW_CLIENT_ID],\n", - " \"allowedClients\": [GW_CLIENT_ID],\n", - " }\n", - " },\n", - " )\n", - " GATEWAY_ID = gw_resp[\"gatewayId\"]\n", - " GATEWAY_URL = gw_resp[\"gatewayUrl\"]\n", - "\n", - " print(f\"Gateway created: {GATEWAY_ID}\")\n", - " print(\"Waiting for ACTIVE...\")\n", - " while True:\n", - " status = gw_client.get_gateway(gatewayIdentifier=GATEWAY_ID)[\"status\"]\n", - " if status == \"ACTIVE\":\n", - " break\n", - " time.sleep(5)\n", - " print(\"✓ Gateway is ACTIVE\")\n", - "\n", - "gateway_client = boto3.client(\"bedrock-agentcore-control\", region_name=REGION)" - ] + "source": "# Connect to the gateway created in notebook 01\ngateway_client = boto3.client(\"bedrock-agentcore-control\", region_name=REGION)\n\ntry:\n # If running in same kernel as NB01, GATEWAY_ID is already set\n _ = GATEWAY_ID\nexcept NameError:\n GATEWAY_ID = input(\"Enter Gateway ID from notebook 01: \").strip()\n\n# Look up all gateway details from the API\ngw = gateway_client.get_gateway(gatewayIdentifier=GATEWAY_ID)\nGATEWAY_URL = gw[\"gatewayUrl\"]\nGATEWAY_NAME = gw[\"name\"]\n\n# Get Cognito details from the authorizer config\nauthorizer = gw[\"authorizerConfiguration\"][\"customJWTAuthorizer\"]\nDISCOVERY_URL = authorizer[\"discoveryUrl\"]\nGW_CLIENT_ID = authorizer[\"allowedClients\"][0]\n\n# Derive token endpoint and scope from the discovery URL / gateway name\n# Discovery URL format: https://cognito-idp..amazonaws.com//.well-known/openid-configuration\npool_id = DISCOVERY_URL.split(\"/\")[3]\ncognito_client = boto3.client(\"cognito-idp\", region_name=REGION)\npool_clients = cognito_client.list_user_pool_clients(UserPoolId=pool_id, MaxResults=10)\nclient_info = cognito_client.describe_user_pool_client(\n UserPoolId=pool_id, ClientId=GW_CLIENT_ID\n)[\"UserPoolClient\"]\nGW_CLIENT_SECRET = client_info.get(\"ClientSecret\", \"\")\n\n# Get domain for token endpoint\npool_desc = cognito_client.describe_user_pool(UserPoolId=pool_id)[\"UserPool\"]\nCOGNITO_DOMAIN = pool_desc.get(\"Domain\", \"\")\nTOKEN_ENDPOINT = f\"https://{COGNITO_DOMAIN}.auth.{REGION}.amazoncognito.com/oauth2/token\"\n\n# Get scope from allowed scopes\nFULL_SCOPE = client_info.get(\"AllowedOAuthScopes\", [\"\"])[0]\n\nprint(f\"\u2713 Connected to gateway: {GATEWAY_NAME}\")\nprint(f\" Gateway ID: {GATEWAY_ID}\")\nprint(f\" Gateway URL: {GATEWAY_URL}\")\nprint(f\" Region: {REGION}\")" }, { "cell_type": "markdown", - "metadata": {}, - "source": "SAP_TARGET_NAME = \"sap-target\"\n\ntarget_resp = gateway_client.create_gateway_target(\n gatewayIdentifier=GATEWAY_ID,\n name=SAP_TARGET_NAME,\n targetConfiguration={\n \"mcp\": {\n \"mcpServer\": {\n \"endpoint\": SAP_MCP_ENDPOINT,\n }\n }\n },\n credentialProviderConfigurations=[\n {\n \"credentialProviderType\": \"OAUTH\",\n \"credentialProvider\": {\n \"oauthCredentialProvider\": {\n \"providerArn\": SAP_CREDENTIAL_PROVIDER_ARN,\n \"scopes\": [],\n \"grantType\": \"CLIENT_CREDENTIALS\",\n }\n },\n }\n ],\n)\n\nSAP_TARGET_ID = target_resp[\"targetId\"]\nprint(f\"Created SAP target: {SAP_TARGET_NAME} ({SAP_TARGET_ID})\")\nprint(\"Waiting for target to reach READY status...\")" + "source": "## Step 3: Create SAP Credential Provider\n\nRegister the SAP MCP Server's OAuth2 credentials with AgentCore Identity so the gateway can authenticate outbound requests.", + "metadata": {} }, { "cell_type": "code", - "execution_count": null, + "source": "SAP_CREDENTIAL_PROVIDER_NAME = f\"sap-mcp-oauth-{str(uuid.uuid4())[:8]}\"\n\ncred_resp = gateway_client.create_oauth2_credential_provider(\n name=SAP_CREDENTIAL_PROVIDER_NAME,\n credentialProviderVendor=\"CustomOauth2\",\n oauth2ProviderConfigInput={\n \"customOauth2ProviderConfig\": {\n \"clientId\": SAP_CLIENT_ID,\n \"clientSecret\": SAP_CLIENT_SECRET,\n \"oauthDiscovery\": {\n \"authorizationServerMetadata\": {\n \"issuer\": SAP_TOKEN_ENDPOINT.replace(\"/oauth2/token\", \"\"),\n \"authorizationEndpoint\": SAP_TOKEN_ENDPOINT.replace(\"/token\", \"/authorize\"),\n \"tokenEndpoint\": SAP_TOKEN_ENDPOINT,\n }\n },\n }\n },\n)\n\nSAP_CREDENTIAL_PROVIDER_ARN = cred_resp[\"credentialProviderArn\"]\nprint(f\"Created SAP credential provider: {SAP_CREDENTIAL_PROVIDER_NAME}\")\nprint(f\"ARN: {SAP_CREDENTIAL_PROVIDER_ARN}\")", "metadata": {}, - "outputs": [], - "source": [ - "SAP_CREDENTIAL_PROVIDER_NAME = f\"sap-mcp-oauth-{str(uuid.uuid4())[:8]}\"\n", - "\n", - "# Extract Cognito discovery URL from token endpoint\n", - "# Token endpoint format: https://.auth..amazoncognito.com/oauth2/token\n", - "SAP_DISCOVERY_URL = input(\n", - " \"Enter the SAP MCP Server Cognito discovery URL\\n\"\n", - " \"(e.g., https://cognito-idp..amazonaws.com//.well-known/openid-configuration): \"\n", - ")\n", - "\n", - "cred_resp = gateway_client.create_oauth2_credential_provider(\n", - " name=SAP_CREDENTIAL_PROVIDER_NAME,\n", - " credentialProviderVendor=\"CustomOauth2\",\n", - " oauth2ProviderConfigInput={\n", - " \"customOauth2ProviderConfig\": {\n", - " \"clientId\": SAP_CLIENT_ID,\n", - " \"clientSecret\": SAP_CLIENT_SECRET,\n", - " \"oauthDiscovery\": {\n", - " \"discoveryUrl\": SAP_DISCOVERY_URL,\n", - " },\n", - " }\n", - " },\n", - ")\n", - "\n", - "SAP_CREDENTIAL_PROVIDER_ARN = cred_resp[\"credentialProviderArn\"]\n", - "print(f\"Created SAP credential provider: {SAP_CREDENTIAL_PROVIDER_NAME}\")\n", - "print(f\"ARN: {SAP_CREDENTIAL_PROVIDER_ARN}\")" - ] + "execution_count": null, + "outputs": [] }, { "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 4: Add SAP MCP Server as Gateway Target\n", - "\n", - "We add the SAP MCP Server as an MCP server target on the gateway. This tells the gateway to proxy MCP requests to the SAP MCP Server endpoint." - ] + "source": "## Step 4: Add SAP MCP Server as Gateway Target\n\nWe add the SAP MCP Server as an MCP server target on the gateway. This tells the gateway to proxy MCP requests to the SAP MCP Server endpoint.", + "metadata": {} }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], - "source": [ - "SAP_TARGET_NAME = \"sap-target\"\n", - "\n", - "target_resp = gateway_client.create_gateway_target(\n", - " gatewayIdentifier=GATEWAY_ID,\n", - " name=SAP_TARGET_NAME,\n", - " targetConfiguration={\n", - " \"mcpTarget\": {\n", - " \"mcpServer\": {\n", - " \"endpoint\": SAP_MCP_ENDPOINT,\n", - " },\n", - " \"authConfiguration\": {\n", - " \"oauth2Auth\": {\n", - " \"credentialProviderArn\": SAP_CREDENTIAL_PROVIDER_ARN,\n", - " }\n", - " },\n", - " }\n", - " },\n", - ")\n", - "\n", - "SAP_TARGET_ID = target_resp[\"targetId\"]\n", - "print(f\"Created SAP target: {SAP_TARGET_NAME} ({SAP_TARGET_ID})\")\n", - "print(\"Waiting for target to reach READY status...\")" - ] + "source": "SAP_TARGET_NAME = \"sap-target\"\n\ntarget_resp = gateway_client.create_gateway_target(\n gatewayIdentifier=GATEWAY_ID,\n name=SAP_TARGET_NAME,\n targetConfiguration={\n \"mcp\": {\n \"mcpServer\": {\n \"endpoint\": SAP_MCP_ENDPOINT,\n }\n }\n },\n credentialProviderConfigurations=[\n {\n \"credentialProviderType\": \"OAUTH\",\n \"credentialProvider\": {\n \"oauthCredentialProvider\": {\n \"providerArn\": SAP_CREDENTIAL_PROVIDER_ARN,\n \"scopes\": [],\n \"grantType\": \"CLIENT_CREDENTIALS\",\n }\n },\n }\n ],\n)\n\nSAP_TARGET_ID = target_resp[\"targetId\"]\nprint(f\"Created SAP target: {SAP_TARGET_NAME} ({SAP_TARGET_ID})\")\nprint(\"Waiting for target to reach READY status...\")", + "execution_count": null, + "outputs": [] }, { "cell_type": "code", @@ -364,164 +187,48 @@ " region=REGION,\n", " timeout=300,\n", ")\n", - "print(\"\\n✓ SAP MCP Server target is READY\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 5: Verify SAP Tools via Gateway\n", - "\n", - "Once the target is ready, the SAP MCP Server tools appear via the gateway's `tools/list` endpoint." + "print(\"\\n\u2713 SAP MCP Server target is READY\")" ] }, { "cell_type": "code", - "execution_count": null, + "source": "# Set up gateway client for tool invocation\ndef get_gw_token() -> str:\n return gateway_mcp_client.get_cognito_m2m_token(\n token_endpoint=TOKEN_ENDPOINT,\n client_id=GW_CLIENT_ID,\n client_secret=GW_CLIENT_SECRET,\n scope=FULL_SCOPE,\n )\n\nmcp = gateway_mcp_client.GatewayMCPClient(\n gateway_url=GATEWAY_URL,\n get_token=get_gw_token,\n session_id=str(uuid.uuid4()),\n)\n\n# Verify tools are accessible\nall_tools = mcp.list_all_tools()\nsap_tools = [t for t in all_tools if t[\"name\"].startswith(\"sap-target___\")]\nprint(f\"Total tools on gateway: {len(all_tools)}\")\nprint(f\"SAP tools: {len(sap_tools)}\")\nprint(\"\\nSAP tool names:\")\nfor t in sorted(sap_tools, key=lambda x: x[\"name\"]):\n print(f\" - {t['name']}\")", "metadata": {}, - "outputs": [], - "source": "from strands import Agent\nfrom strands.tools.mcp import MCPClient\nfrom mcp import ClientSession\nfrom mcp.client.streamable_http import streamablehttp_client\n\nmcp_client = MCPClient(\n lambda: streamablehttp_client(\n url=GATEWAY_URL,\n headers={\n \"Authorization\": f\"Bearer {get_gw_token()}\",\n \"MCP-Protocol-Version\": \"2025-03-26\",\n },\n )\n)\n\nSYSTEM_PROMPT = (\n \"You are a helpful assistant with access to SAP tools via the AWS for SAP MCP Server. \"\n \"Before reading data, use get_metadata to understand available entity sets and field names. \"\n \"Use odata_count before odata_read to understand data volume. \"\n \"SAP field names can be non-obvious — always check metadata first.\"\n)\n\nwith mcp_client:\n agent = Agent(\n model=\"us.anthropic.claude-sonnet-4-6-v1\",\n system_prompt=SYSTEM_PROMPT,\n tools=mcp_client.list_tools_sync(),\n )\n result = agent(\"What SAP services are available for sales orders? Show me the first 3 sales orders.\")\n print(result)" + "execution_count": null, + "outputs": [] }, { "cell_type": "markdown", "metadata": {}, - "source": [ - "## Step 6: Invoke SAP Tools\n", - "\n", - "Let's call the SAP MCP Server tools through the gateway to explore available services and read data." - ] + "source": "## Step 5: Query SAP Data via Gateway\n\nNow that the SAP target is ready, let's read data from SAP through the gateway." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Discover available SAP services\n", - "result = mcp.call_tool(\n", - " \"sap-target___find_sap_services\",\n", - " {\"search_term\": \"sales\", \"top\": 5},\n", - ")\n", - "print(\"=== Find SAP Services (sales) ===\")\n", - "content = result.get(\"result\", {}).get(\"content\", [])\n", - "for item in content:\n", - " if item.get(\"type\") == \"text\":\n", - " print(item[\"text\"][:2000])" - ] + "source": "# Query 1: Show the first 3 business partners\nresult = mcp.call_tool(\n \"sap-target___odata_read\",\n {\n \"service_name\": \"API_BUSINESS_PARTNER\",\n \"entity_set\": \"A_BusinessPartner\",\n \"top\": 3,\n \"select\": \"BusinessPartner,BusinessPartnerFullName,BusinessPartnerCategory\",\n },\n)\nprint(\"=== First 3 SAP Business Partners ===\")\ncontent = result.get(\"result\", {}).get(\"content\", [])\nfor item in content:\n if item.get(\"type\") == \"text\":\n try:\n data = json.loads(item[\"text\"])\n for bp in data.get(\"data\", data.get(\"results\", [])):\n print(f\" {bp.get('BusinessPartner', '?'):>6} {bp.get('BusinessPartnerFullName', '?')}\")\n except json.JSONDecodeError:\n print(item[\"text\"][:500])" }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], - "source": [ - "# Get metadata for Business Partner API\n", - "result = mcp.call_tool(\n", - " \"sap-target___get_metadata\",\n", - " {\"service_name\": \"API_BUSINESS_PARTNER\"},\n", - ")\n", - "print(\"=== Business Partner API Metadata ===\")\n", - "content = result.get(\"result\", {}).get(\"content\", [])\n", - "for item in content:\n", - " if item.get(\"type\") == \"text\":\n", - " print(item[\"text\"][:3000])" - ] - }, - { - "cell_type": "code", + "source": "# Query 2: Count how many business partners exist\nresult = mcp.call_tool(\n \"sap-target___odata_count\",\n {\n \"service_name\": \"API_BUSINESS_PARTNER\",\n \"entity_set\": \"A_BusinessPartner\",\n },\n)\nprint(\"=== Business Partner Count ===\")\ncontent = result.get(\"result\", {}).get(\"content\", [])\nfor item in content:\n if item.get(\"type\") == \"text\":\n print(f\" Total business partners in SAP: {item['text']}\")", "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Read business partners\n", - "result = mcp.call_tool(\n", - " \"sap-target___odata_read\",\n", - " {\n", - " \"service_name\": \"API_BUSINESS_PARTNER\",\n", - " \"entity_set\": \"A_BusinessPartner\",\n", - " \"top\": 5,\n", - " \"select\": \"BusinessPartner,BusinessPartnerFullName,BusinessPartnerCategory\",\n", - " },\n", - ")\n", - "print(\"=== Read Business Partners ===\")\n", - "content = result.get(\"result\", {}).get(\"content\", [])\n", - "for item in content:\n", - " if item.get(\"type\") == \"text\":\n", - " try:\n", - " data = json.loads(item[\"text\"])\n", - " print(json.dumps(data, indent=2)[:3000])\n", - " except json.JSONDecodeError:\n", - " print(item[\"text\"][:3000])" - ] + "outputs": [] }, { - "cell_type": "code", + "cell_type": "markdown", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Count records in an entity set\n", - "result = mcp.call_tool(\n", - " \"sap-target___odata_count\",\n", - " {\n", - " \"service_name\": \"API_BUSINESS_PARTNER\",\n", - " \"entity_set\": \"A_BusinessPartner\",\n", - " },\n", - ")\n", - "print(\"=== Business Partner Count ===\")\n", - "content = result.get(\"result\", {}).get(\"content\", [])\n", - "for item in content:\n", - " if item.get(\"type\") == \"text\":\n", - " print(f\"Total business partners: {item['text']}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 7: Use Strands Agent with SAP Tools\n", - "\n", - "Connect a Strands Agent to the gateway and let it explore SAP data via natural language." - ] + "source": "## Step 6: Use Strands Agent with SAP Tools\n\nLet a Strands Agent answer a business question using SAP data." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "from strands import Agent\n", - "from strands.tools.mcp import MCPClient\n", - "from mcp import ClientSession\n", - "from mcp.client.streamable_http import streamablehttp_client\n", - "\n", - "mcp_client = MCPClient(\n", - " lambda: streamablehttp_client(\n", - " url=GATEWAY_URL,\n", - " headers={\n", - " \"Authorization\": f\"Bearer {get_gw_token()}\",\n", - " \"MCP-Protocol-Version\": \"2025-11-25\",\n", - " },\n", - " )\n", - ")\n", - "\n", - "SYSTEM_PROMPT = (\n", - " \"You are a helpful assistant with access to SAP tools via the AWS for SAP MCP Server. \"\n", - " \"Before reading data, use get_metadata to understand available entity sets and field names. \"\n", - " \"Use odata_count before odata_read to understand data volume. \"\n", - " \"SAP field names can be non-obvious — always check metadata first.\"\n", - ")\n", - "\n", - "with mcp_client:\n", - " agent = Agent(\n", - " model=\"us.anthropic.claude-sonnet-4-6-v1\",\n", - " system_prompt=SYSTEM_PROMPT,\n", - " tools=mcp_client.list_tools_sync(),\n", - " )\n", - " result = agent(\"What SAP services are available for sales orders? Show me the first 3 sales orders.\")\n", - " print(result)" - ] + "source": "from strands import Agent\nfrom strands.tools.mcp import MCPClient\nfrom mcp.client.streamable_http import streamablehttp_client\n\nmcp_client = MCPClient(\n lambda: streamablehttp_client(\n url=GATEWAY_URL,\n headers={\n \"Authorization\": f\"Bearer {get_gw_token()}\",\n \"MCP-Protocol-Version\": \"2025-03-26\",\n },\n )\n)\n\nSYSTEM_PROMPT = (\n \"You are a helpful assistant with access to SAP ERP data. \"\n \"Use odata_read to query data. Keep answers concise.\"\n)\n\nwith mcp_client:\n agent = Agent(\n model=MODEL_ID,\n system_prompt=SYSTEM_PROMPT,\n tools=mcp_client.list_tools_sync(),\n )\n result = agent(\n \"Show me the first 3 sales orders from SAP with their amounts and currencies.\"\n )\n print(result)" }, { "cell_type": "markdown", @@ -531,12 +238,12 @@ "\n", "From the [AWS for SAP MCP Server documentation](https://docs.aws.amazon.com/mcp-sap/latest/awsforsapmcp/security.html):\n", "\n", - "1. **Read-only by default** — Write tools are disabled unless both global and per-operation flags are set to `true`. Only enable writes you actually need.\n", - "2. **Principle of least privilege** — Assign the SAP user only the minimum necessary roles and authorizations.\n", - "3. **Scope OAuth access** — Configure OAuth scopes to grant access only to specific required OData services.\n", - "4. **Use odata_count before reads** — Understand data volume before issuing broad reads to avoid overwhelming the agent.\n", - "5. **Use get_metadata proactively** — SAP field names are often non-obvious (e.g., `OverallOrdReltdBillgStatus` not `OverallBillingStatus`). Always check metadata before constructing filters.\n", - "6. **Credentials never stored on disk** — The MCP Server retrieves credentials at runtime from AWS Secrets Manager." + "1. **Read-only by default** \u2014 Write tools are disabled unless both global and per-operation flags are set to `true`. Only enable writes you actually need.\n", + "2. **Principle of least privilege** \u2014 Assign the SAP user only the minimum necessary roles and authorizations.\n", + "3. **Scope OAuth access** \u2014 Configure OAuth scopes to grant access only to specific required OData services.\n", + "4. **Use odata_count before reads** \u2014 Understand data volume before issuing broad reads to avoid overwhelming the agent.\n", + "5. **Use get_metadata proactively** \u2014 SAP field names are often non-obvious (e.g., `OverallOrdReltdBillgStatus` not `OverallBillingStatus`). Always check metadata before constructing filters.\n", + "6. **Credentials never stored on disk** \u2014 The MCP Server retrieves credentials at runtime from AWS Secrets Manager." ] }, { @@ -553,44 +260,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "SKIP_CLEANUP = input(\"Skip cleanup to use gateway in notebook 03? (yes/no): \").strip().lower() == \"yes\"\n", - "\n", - "if not SKIP_CLEANUP:\n", - " # Delete SAP gateway target\n", - " print(\"Deleting SAP gateway target...\")\n", - " gateway_client.delete_gateway_target(gatewayIdentifier=GATEWAY_ID, targetId=SAP_TARGET_ID)\n", - " print(\" ✓ SAP target deleted\")\n", - "\n", - " # Delete SAP credential provider\n", - " print(\"Deleting SAP credential provider...\")\n", - " gateway_client.delete_oauth2_credential_provider(name=SAP_CREDENTIAL_PROVIDER_NAME)\n", - " print(\" ✓ Credential provider deleted\")\n", - "\n", - " if CREATED_GATEWAY:\n", - " # Delete gateway\n", - " print(\"Deleting gateway...\")\n", - " gateway_client.delete_gateway(gatewayIdentifier=GATEWAY_ID)\n", - " print(\" ✓ Gateway deleted\")\n", - "\n", - " # Delete Cognito resources\n", - " print(\"Deleting Cognito resources...\")\n", - " cognito_client.delete_user_pool_domain(Domain=COGNITO_DOMAIN, UserPoolId=USER_POOL_ID)\n", - " cognito_client.delete_user_pool(UserPoolId=USER_POOL_ID)\n", - " print(\" ✓ Cognito pool deleted\")\n", - "\n", - " # Delete IAM role\n", - " print(\"Deleting IAM role...\")\n", - " iam.delete_role(RoleName=ROLE_NAME)\n", - " print(\" ✓ IAM role deleted\")\n", - "\n", - " print(\"\\n✓ All resources cleaned up\")\n", - "else:\n", - " print(\"\\nSkipping cleanup. Remember to clean up resources when done!\")\n", - " print(f\" Gateway ID: {GATEWAY_ID}\")\n", - " print(f\" Gateway URL: {GATEWAY_URL}\")\n", - " print(f\" SAP Target ID: {SAP_TARGET_ID}\")" - ] + "source": "SKIP_CLEANUP = input(\"Skip cleanup to use gateway in notebook 03? (yes/no): \").strip().lower() == \"yes\"\n\nif not SKIP_CLEANUP:\n # Delete SAP gateway target\n print(\"Deleting SAP gateway target...\")\n gateway_client.delete_gateway_target(gatewayIdentifier=GATEWAY_ID, targetId=SAP_TARGET_ID)\n time.sleep(5)\n print(\" \u2713 SAP target deleted\")\n\n # Delete SAP credential provider\n print(\"Deleting SAP credential provider...\")\n gateway_client.delete_oauth2_credential_provider(name=SAP_CREDENTIAL_PROVIDER_NAME)\n print(\" \u2713 Credential provider deleted\")\n\n if CREATED_GATEWAY:\n # Delete gateway\n print(\"Deleting gateway...\")\n gateway_client.delete_gateway(gatewayIdentifier=GATEWAY_ID)\n time.sleep(5)\n print(\" \u2713 Gateway deleted\")\n\n # Delete Cognito resources\n print(\"Deleting Cognito resources...\")\n cognito_client.delete_user_pool_domain(Domain=COGNITO_DOMAIN, UserPoolId=USER_POOL_ID)\n cognito_client.delete_user_pool(UserPoolId=USER_POOL_ID)\n print(\" \u2713 Cognito pool deleted\")\n\n # Delete IAM role (must remove inline policy first)\n print(\"Deleting IAM role...\")\n iam.delete_role_policy(RoleName=ROLE_NAME, PolicyName=\"AgentCorePolicy\")\n iam.delete_role(RoleName=ROLE_NAME)\n print(\" \u2713 IAM role deleted\")\n\n print(\"\\n\u2713 All resources cleaned up\")\nelse:\n print(\"\\nSkipping cleanup. Remember to clean up resources when done!\")\n print(f\" Gateway ID: {GATEWAY_ID}\")\n print(f\" Gateway URL: {GATEWAY_URL}\")\n print(f\" SAP Target ID: {SAP_TARGET_ID}\")" } ], "metadata": { @@ -606,4 +276,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} \ No newline at end of file +} diff --git a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/03-cross-isv-queries.ipynb b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/03-cross-isv-queries.ipynb index 3789cf3fc..aef9d91fd 100644 --- a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/03-cross-isv-queries.ipynb +++ b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/03-cross-isv-queries.ipynb @@ -15,20 +15,20 @@ "\n", "## Overview\n", "\n", - "This notebook demonstrates the power of routing multiple ISV platforms through a single [Amazon Bedrock AgentCore Gateway](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway.html). With Salesforce (CRM) and SAP (ERP) connected to the same gateway, an AI agent can answer questions that span both systems — without the user needing to know which system holds which data.\n", + "This notebook demonstrates the power of routing multiple ISV platforms through a single [Amazon Bedrock AgentCore Gateway](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway.html). With Salesforce (CRM) and SAP (ERP) connected to the same gateway, an AI agent can answer questions that span both systems \u2014 without the user needing to know which system holds which data.\n", "\n", "**Use cases demonstrated:**\n", - "1. Customer 360 — combine SAP business partner data with Salesforce account history\n", - "2. Pipeline Reconciliation — compare Salesforce opportunities with SAP sales orders\n", - "3. Support Case with ERP Context — enrich Salesforce cases with SAP inventory data\n", - "4. Natural Language Agent — let Claude orchestrate cross-system queries autonomously\n", + "1. Customer 360 \u2014 combine SAP business partner data with Salesforce account history\n", + "2. Pipeline Reconciliation \u2014 compare Salesforce opportunities with SAP sales orders\n", + "3. Support Case with ERP Context \u2014 enrich Salesforce cases with SAP inventory data\n", + "4. Natural Language Agent \u2014 let Claude orchestrate cross-system queries autonomously\n", "\n", "| Information | Details |\n", "|:---|:---|\n", "| Prerequisites | Both Salesforce + SAP targets READY on same gateway (from notebooks 01 + 02) |\n", "| Tools available | ~52 (43 Salesforce + 9 SAP) |\n", "| Agent framework | Strands Agents |\n", - "| LLM | Anthropic Claude Sonnet 4 |" + "| LLM | Anthropic Claude Sonnet 4.6 (`us.anthropic.claude-sonnet-4-6`) |" ] }, { @@ -38,8 +38,8 @@ "### Prerequisites\n", "\n", "This notebook assumes you have completed:\n", - "1. [01-salesforce-gateway-target.ipynb](01-salesforce-gateway-target.ipynb) — Salesforce target is READY\n", - "2. [02-sap-mcp-server-target.ipynb](02-sap-mcp-server-target.ipynb) — SAP target is READY\n", + "1. [01-salesforce-gateway-target.ipynb](01-salesforce-gateway-target.ipynb) \u2014 Salesforce target is READY\n", + "2. [02-sap-mcp-server-target.ipynb](02-sap-mcp-server-target.ipynb) \u2014 SAP target is READY\n", "\n", "Both targets must be on the **same gateway**. You'll provide the gateway connection details below." ] @@ -49,9 +49,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "!pip install -r requirements.txt --quiet" - ] + "source": "!pip install --force-reinstall -U -r requirements.txt --quiet" }, { "cell_type": "code", @@ -62,6 +60,7 @@ "import getpass\n", "import json\n", "import logging\n", + "import os\n", "import uuid\n", "\n", "import boto3\n", @@ -70,6 +69,9 @@ "\n", "import gateway_mcp_client\n", "\n", + "# AWS credentials \u2014 set your profile\n", + "os.environ[\"AWS_PROFILE\"] = \"default\" # Change to your profile\n", + "\n", "logging.basicConfig(\n", " level=logging.INFO,\n", " format=\"%(asctime)s | %(levelname)s | %(name)s | %(message)s\",\n", @@ -77,8 +79,15 @@ ")\n", "\n", "session = Session()\n", - "REGION = session.region_name or \"us-east-1\"\n", - "print(f\"Using region: {REGION}\")" + "REGION = session.region_name or \"eu-west-1\"\n", + "\n", + "# Derive Bedrock model ID geo prefix from region\n", + "GEO_PREFIX = {\"us-\": \"us\", \"eu-\": \"eu\", \"ap-\": \"ap\", \"ca-\": \"us\", \"sa-\": \"us\"}\n", + "MODEL_PREFIX = next((v for k, v in GEO_PREFIX.items() if REGION.startswith(k)), \"us\")\n", + "MODEL_ID = f\"{MODEL_PREFIX}.anthropic.claude-sonnet-4-6\"\n", + "\n", + "print(f\"Using region: {REGION}\")\n", + "print(f\"Model ID: {MODEL_ID}\")" ] }, { @@ -86,23 +95,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Gateway connection details (from notebooks 01 + 02)\n", - "GATEWAY_URL = input(\"Enter Gateway URL: \")\n", - "TOKEN_ENDPOINT = input(\"Enter Cognito token endpoint: \")\n", - "GW_CLIENT_ID = input(\"Enter Cognito Client ID: \")\n", - "GW_CLIENT_SECRET = getpass.getpass(\"Enter Cognito Client Secret: \")\n", - "FULL_SCOPE = input(\"Enter OAuth scope: \")\n", - "\n", - "# Salesforce domain (needed for SF tool calls)\n", - "SF_DOMAIN = input(\"Enter Salesforce domain (e.g., myorg-dev-ed): \")\n", - "\n", - "assert GATEWAY_URL.strip(), \"Gateway URL cannot be empty\"\n", - "assert SF_DOMAIN.strip(), \"Salesforce domain cannot be empty\"\n", - "\n", - "print(f\"\\nGateway: {GATEWAY_URL}\")\n", - "print(f\"Salesforce domain: {SF_DOMAIN}\")" - ] + "source": "# Connect to the gateway (auto-detect from kernel or enter ID)\ngateway_client = boto3.client(\"bedrock-agentcore-control\", region_name=REGION)\n\ntry:\n _ = GATEWAY_ID\nexcept NameError:\n GATEWAY_ID = input(\"Enter Gateway ID from notebook 01: \").strip()\n\n# Look up all gateway details from the API\ngw = gateway_client.get_gateway(gatewayIdentifier=GATEWAY_ID)\nGATEWAY_URL = gw[\"gatewayUrl\"]\nGATEWAY_NAME = gw[\"name\"]\n\n# Get Cognito details from the authorizer config\nauthorizer = gw[\"authorizerConfiguration\"][\"customJWTAuthorizer\"]\nDISCOVERY_URL = authorizer[\"discoveryUrl\"]\nGW_CLIENT_ID = authorizer[\"allowedClients\"][0]\n\npool_id = DISCOVERY_URL.split(\"/\")[3]\ncognito_client = boto3.client(\"cognito-idp\", region_name=REGION)\nclient_info = cognito_client.describe_user_pool_client(\n UserPoolId=pool_id, ClientId=GW_CLIENT_ID\n)[\"UserPoolClient\"]\nGW_CLIENT_SECRET = client_info.get(\"ClientSecret\", \"\")\n\npool_desc = cognito_client.describe_user_pool(UserPoolId=pool_id)[\"UserPool\"]\nCOGNITO_DOMAIN = pool_desc.get(\"Domain\", \"\")\nTOKEN_ENDPOINT = f\"https://{COGNITO_DOMAIN}.auth.{REGION}.amazoncognito.com/oauth2/token\"\nFULL_SCOPE = client_info.get(\"AllowedOAuthScopes\", [\"\"])[0]\n\n# Salesforce domain (needed for SF tool calls)\ntry:\n _ = SF_DOMAIN\nexcept NameError:\n SF_DOMAIN = input(\"Enter Salesforce domain (e.g., myorg-dev-ed): \").strip()\n\nprint(f\"\u2713 Connected to gateway: {GATEWAY_NAME}\")\nprint(f\" Gateway URL: {GATEWAY_URL}\")\nprint(f\" SF domain: {SF_DOMAIN}\")" }, { "cell_type": "code", @@ -144,7 +137,7 @@ "\n", "Combine customer data from both SAP (business partner master data) and Salesforce (CRM account history) to build a unified view.\n", "\n", - "**Pattern:** Query SAP for business partner details → Query Salesforce for account + opportunities" + "**Pattern:** Query SAP for business partner details \u2192 Query Salesforce for account + opportunities" ] }, { @@ -152,54 +145,14 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# SAP: Get business partner data\n", - "sap_result = mcp.call_tool(\n", - " \"sap-target___odata_read\",\n", - " {\n", - " \"service_name\": \"API_BUSINESS_PARTNER\",\n", - " \"entity_set\": \"A_BusinessPartner\",\n", - " \"top\": 5,\n", - " \"select\": \"BusinessPartner,BusinessPartnerFullName,BusinessPartnerCategory,Industry\",\n", - " },\n", - ")\n", - "\n", - "print(\"=== SAP Business Partners ===\")\n", - "sap_content = sap_result.get(\"result\", {}).get(\"content\", [])\n", - "for item in sap_content:\n", - " if item.get(\"type\") == \"text\":\n", - " try:\n", - " data = json.loads(item[\"text\"])\n", - " print(json.dumps(data, indent=2)[:2000])\n", - " except json.JSONDecodeError:\n", - " print(item[\"text\"][:2000])" - ] + "source": "# SAP: Get business partner data\nsap_result = mcp.call_tool(\n \"sap-target___odata_read\",\n {\n \"service_name\": \"API_BUSINESS_PARTNER\",\n \"entity_set\": \"A_BusinessPartner\",\n \"top\": 5,\n \"select\": \"BusinessPartner,BusinessPartnerFullName,BusinessPartnerCategory\",\n },\n)\n\nprint(\"=== SAP Business Partners ===\")\ncontent = sap_result.get(\"result\", {}).get(\"content\", [])\nfor item in content:\n if item.get(\"type\") == \"text\":\n data = json.loads(item[\"text\"])\n for bp in data.get(\"data\", []):\n print(f\" {bp.get('BusinessPartner', '?'):>6} {bp.get('BusinessPartnerFullName', '?'):40s} Cat: {bp.get('BusinessPartnerCategory', '?')}\")" }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Salesforce: Get accounts\n", - "sf_result = mcp.call_tool(\n", - " \"salesforce-target___queryAccounts\",\n", - " {\n", - " \"domainName\": SF_DOMAIN,\n", - " \"q\": \"SELECT Id, Name, Industry, CreatedDate FROM Account LIMIT 5\",\n", - " },\n", - ")\n", - "\n", - "print(\"=== Salesforce Accounts ===\")\n", - "sf_content = sf_result.get(\"result\", {}).get(\"content\", [])\n", - "for item in sf_content:\n", - " if item.get(\"type\") == \"text\":\n", - " try:\n", - " data = json.loads(item[\"text\"])\n", - " print(json.dumps(data, indent=2)[:2000])\n", - " except json.JSONDecodeError:\n", - " print(item[\"text\"][:2000])" - ] + "source": "# Salesforce: Get accounts\nsf_result = mcp.call_tool(\n \"salesforce-target___queryAccounts\",\n {\n \"domainName\": SF_DOMAIN,\n \"q\": \"SELECT Id, Name, Industry, CreatedDate FROM Account LIMIT 5\",\n },\n)\n\nprint(\"=== Salesforce Accounts ===\")\ncontent = sf_result.get(\"result\", {}).get(\"content\", [])\nfor item in content:\n if item.get(\"type\") == \"text\":\n data = json.loads(item[\"text\"])\n for rec in data.get(\"records\", []):\n print(f\" {rec.get('Name', '?'):40s} Industry: {rec.get('Industry', 'N/A')}\")" }, { "cell_type": "markdown", @@ -209,7 +162,7 @@ "\n", "Compare Salesforce opportunities (sales pipeline) with SAP sales orders to identify which deals have been converted to orders.\n", "\n", - "**Pattern:** Query Salesforce for open opportunities → Query SAP for matching sales orders" + "**Pattern:** Query Salesforce for open opportunities \u2192 Query SAP for matching sales orders" ] }, { @@ -217,35 +170,14 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "from strands import Agent\nfrom strands.tools.mcp import MCPClient\nfrom mcp import ClientSession\nfrom mcp.client.streamable_http import streamablehttp_client\n\nmcp_client = MCPClient(\n lambda: streamablehttp_client(\n url=GATEWAY_URL,\n headers={\n \"Authorization\": f\"Bearer {get_gw_token()}\",\n \"MCP-Protocol-Version\": \"2025-03-26\",\n },\n )\n)\n\nSYSTEM_PROMPT = (\n \"You are a helpful enterprise assistant with access to both Salesforce (CRM) and SAP (ERP) tools \"\n \"through a unified AgentCore Gateway. \"\n f\"For Salesforce tools (prefix 'salesforce-target___'), always include domainName='{SF_DOMAIN}'. \"\n \"Do not pass Content-Type to Salesforce create operations. \"\n \"For SAP tools (prefix 'sap-target___'), use get_metadata before reads on unfamiliar entity sets. \"\n \"Use odata_count before odata_read to understand data volume. \"\n \"When answering cross-system questions, query both systems and synthesize the results.\"\n)\n\nwith mcp_client:\n agent = Agent(\n model=\"us.anthropic.claude-sonnet-4-6-v1\",\n system_prompt=SYSTEM_PROMPT,\n tools=mcp_client.list_tools_sync(),\n )\n\n # Cross-ISV query\n result = agent(\n \"Give me a Customer 360 view: get the top 3 business partners from SAP \"\n \"and all Salesforce accounts, then tell me which customers exist in both systems.\"\n )\n print(result)" + "source": "# Salesforce: Get open opportunities\nsf_opps = mcp.call_tool(\n \"salesforce-target___queryAccounts\",\n {\n \"domainName\": SF_DOMAIN,\n \"q\": \"SELECT Id, Name, Amount, StageName, CloseDate FROM Opportunity WHERE IsClosed = false LIMIT 5\",\n },\n)\n\nprint(\"=== Salesforce Open Opportunities ===\")\ncontent = sf_opps.get(\"result\", {}).get(\"content\", [])\nfor item in content:\n if item.get(\"type\") == \"text\":\n data = json.loads(item[\"text\"])\n for rec in data.get(\"records\", []):\n amt = rec.get(\"Amount\", \"N/A\")\n print(f\" {rec.get('Name', '?'):40s} Stage: {rec.get('StageName', '?'):15s} Amount: {amt}\")" }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# SAP: Get recent sales orders\n", - "sap_orders = mcp.call_tool(\n", - " \"sap-target___odata_read\",\n", - " {\n", - " \"service_name\": \"API_SALES_ORDER_SRV\",\n", - " \"entity_set\": \"A_SalesOrder\",\n", - " \"top\": 5,\n", - " \"select\": \"SalesOrder,SoldToParty,TotalNetAmount,TransactionCurrency,CreationDate\",\n", - " },\n", - ")\n", - "\n", - "print(\"=== SAP Sales Orders ===\")\n", - "content = sap_orders.get(\"result\", {}).get(\"content\", [])\n", - "for item in content:\n", - " if item.get(\"type\") == \"text\":\n", - " try:\n", - " data = json.loads(item[\"text\"])\n", - " print(json.dumps(data, indent=2)[:2000])\n", - " except json.JSONDecodeError:\n", - " print(item[\"text\"][:2000])" - ] + "source": "# SAP: Get recent sales orders\nsap_orders = mcp.call_tool(\n \"sap-target___odata_read\",\n {\n \"service_name\": \"API_SALES_ORDER_SRV\",\n \"entity_set\": \"A_SalesOrder\",\n \"top\": 5,\n \"select\": \"SalesOrder,SoldToParty,TotalNetAmount,TransactionCurrency,CreationDate\",\n },\n)\n\nprint(\"=== SAP Sales Orders ===\")\ncontent = sap_orders.get(\"result\", {}).get(\"content\", [])\nfor item in content:\n if item.get(\"type\") == \"text\":\n data = json.loads(item[\"text\"])\n for order in data.get(\"data\", []):\n print(f\" Order {order.get('SalesOrder', '?'):>10} Customer: {order.get('SoldToParty', '?'):>10} Amount: {order.get('TotalNetAmount', '?')} {order.get('TransactionCurrency', '')}\")" }, { "cell_type": "markdown", @@ -255,7 +187,7 @@ "\n", "When creating a support case in Salesforce, enrich it with relevant SAP data (e.g., material stock levels, order history).\n", "\n", - "**Pattern:** Query SAP for relevant data → Create enriched Salesforce case" + "**Pattern:** Query SAP for relevant data \u2192 Create enriched Salesforce case" ] }, { @@ -263,124 +195,26 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# SAP: Check available services for material/inventory\n", - "sap_services = mcp.call_tool(\n", - " \"sap-target___find_sap_services\",\n", - " {\"search_term\": \"material stock\", \"top\": 3},\n", - ")\n", - "\n", - "print(\"=== SAP Material/Stock Services ===\")\n", - "content = sap_services.get(\"result\", {}).get(\"content\", [])\n", - "for item in content:\n", - " if item.get(\"type\") == \"text\":\n", - " print(item[\"text\"][:2000])" - ] + "source": "# SAP: Check available services for material/inventory\nsap_services = mcp.call_tool(\n \"sap-target___get_metadata\",\n {\"service_name\": \"API_MATERIAL_STOCK_SRV\"},\n)\n\nprint(\"=== SAP Material Stock Service Metadata ===\")\ncontent = sap_services.get(\"result\", {}).get(\"content\", [])\nfor item in content:\n if item.get(\"type\") == \"text\":\n print(item[\"text\"][:2000])" }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# Salesforce: Create a case with ERP context\n", - "# Note: Content-Type workaround required for create operations\n", - "case_result = mcp.call_tool(\n", - " \"salesforce-target___createCase\",\n", - " {\n", - " \"domainName\": SF_DOMAIN,\n", - " \"Content-Type\": \"\",\n", - " \"Subject\": \"Stock Discrepancy - Cross-System Investigation\",\n", - " \"Description\": \"Customer reported inventory mismatch. SAP material stock checked via AgentCore Gateway.\",\n", - " \"Status\": \"New\",\n", - " \"Priority\": \"Medium\",\n", - " \"Origin\": \"Web\",\n", - " },\n", - ")\n", - "\n", - "print(\"=== Created Salesforce Case ===\")\n", - "content = case_result.get(\"result\", {}).get(\"content\", [])\n", - "for item in content:\n", - " if item.get(\"type\") == \"text\":\n", - " print(item[\"text\"][:1000])" - ] + "source": "# Salesforce: Create a case with ERP context\n# Content-Type is a restricted header managed by the gateway \u2014 pass \"\" to avoid duplication\ncase_result = mcp.call_tool(\n \"salesforce-target___createCase\",\n {\n \"domainName\": SF_DOMAIN,\n \"Content-Type\": \"\",\n \"Subject\": \"Stock Discrepancy - Cross-System Investigation\",\n \"Description\": \"Customer reported inventory mismatch. SAP material stock checked via AgentCore Gateway.\",\n \"Status\": \"New\",\n \"Priority\": \"Medium\",\n \"Origin\": \"Web\",\n },\n)\n\nprint(\"=== Created Salesforce Case ===\")\ncontent = case_result.get(\"result\", {}).get(\"content\", [])\nfor item in content:\n if item.get(\"type\") == \"text\":\n data = json.loads(item[\"text\"])\n if data.get(\"success\"):\n print(f\" \u2713 Case created: {data['id']}\")\n else:\n print(f\" \u2717 Failed: {data.get('errors', data)}\")" }, { "cell_type": "markdown", "metadata": {}, - "source": [ - "## Use Case 4: Natural Language Agent (Cross-ISV)\n", - "\n", - "The most powerful pattern: let a Strands Agent handle cross-system queries autonomously. The agent has access to all 52+ tools and decides which systems to query based on the natural language request." - ] + "source": "## Use Case 4: Natural Language Agent (Cross-ISV)\n\nLet a Strands Agent handle cross-system queries autonomously using all tools from both targets." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "from strands import Agent\n", - "from strands.tools.mcp import MCPClient\n", - "from mcp import ClientSession\n", - "from mcp.client.streamable_http import streamablehttp_client\n", - "\n", - "mcp_client = MCPClient(\n", - " lambda: streamablehttp_client(\n", - " url=GATEWAY_URL,\n", - " headers={\n", - " \"Authorization\": f\"Bearer {get_gw_token()}\",\n", - " \"MCP-Protocol-Version\": \"2025-11-25\",\n", - " },\n", - " )\n", - ")\n", - "\n", - "SYSTEM_PROMPT = (\n", - " \"You are a helpful enterprise assistant with access to both Salesforce (CRM) and SAP (ERP) tools \"\n", - " \"through a unified AgentCore Gateway. \"\n", - " f\"For Salesforce tools (prefix 'salesforce-target___'), always include domainName='{SF_DOMAIN}'. \"\n", - " \"Do not pass Content-Type to Salesforce create operations. \"\n", - " \"For SAP tools (prefix 'sap-target___'), use get_metadata before reads on unfamiliar entity sets. \"\n", - " \"Use odata_count before odata_read to understand data volume. \"\n", - " \"When answering cross-system questions, query both systems and synthesize the results.\"\n", - ")\n", - "\n", - "with mcp_client:\n", - " agent = Agent(\n", - " model=\"us.anthropic.claude-sonnet-4-6-v1\",\n", - " system_prompt=SYSTEM_PROMPT,\n", - " tools=mcp_client.list_tools_sync(),\n", - " )\n", - "\n", - " # Cross-ISV query\n", - " result = agent(\n", - " \"Give me a Customer 360 view: get the top 3 business partners from SAP \"\n", - " \"and all Salesforce accounts, then tell me which customers exist in both systems.\"\n", - " )\n", - " print(result)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Another cross-ISV query: pipeline reconciliation\n", - "with mcp_client:\n", - " agent = Agent(\n", - " model=\"us.anthropic.claude-sonnet-4-6-v1\",\n", - " system_prompt=SYSTEM_PROMPT,\n", - " tools=mcp_client.list_tools_sync(),\n", - " )\n", - "\n", - " result = agent(\n", - " \"Compare the Salesforce pipeline with SAP orders: \"\n", - " \"get open Salesforce opportunities and recent SAP sales orders, \"\n", - " \"then identify any patterns or gaps between the two systems.\"\n", - " )\n", - " print(result)" - ] + "source": "from strands import Agent, tool\n\n# Suppress verbose logging\nlogging.getLogger(\"httpx\").setLevel(logging.WARNING)\nlogging.getLogger(\"strands\").setLevel(logging.WARNING)\n\n# Create a generic gateway tool that the agent can call with any tool name\n@tool\ndef gateway_tool_call(tool_name: str, arguments: str) -> str:\n \"\"\"Call any tool on the AgentCore Gateway by name. Pass arguments as a JSON string.\"\"\"\n args = json.loads(arguments)\n result = mcp.call_tool(tool_name, args)\n content = result.get(\"result\", {}).get(\"content\", [])\n for item in content:\n if item.get(\"type\") == \"text\":\n return item[\"text\"]\n return json.dumps(result)\n\n# Build a tool list description for the system prompt\ntool_list = \"\\n\".join(f\" - {t['name']}: {t.get('description', '')}\" for t in all_tools)\n\nSYSTEM_PROMPT = (\n \"You are a helpful enterprise assistant. You can call any of these tools via gateway_tool_call:\\n\\n\"\n f\"{tool_list}\\n\\n\"\n f\"For Salesforce tools, always include domainName='{SF_DOMAIN}' in arguments. \"\n \"For SAP tools, use service_name and entity_set parameters. \"\n \"Keep answers concise and tabular.\"\n)\n\nagent = Agent(\n model=MODEL_ID,\n system_prompt=SYSTEM_PROMPT,\n tools=[gateway_tool_call],\n)\n\nresult = agent(\n \"Get the first 3 SAP business partners (use sap-target___odata_read with \"\n \"service_name=API_BUSINESS_PARTNER, entity_set=A_BusinessPartner, top=3, \"\n \"select=BusinessPartner,BusinessPartnerFullName) \"\n f\"and the first 3 Salesforce accounts (use salesforce-target___queryAccounts with \"\n f\"domainName={SF_DOMAIN}, q=SELECT Id,Name FROM Account LIMIT 3). \"\n \"Then compare which names exist in both systems.\"\n)\nprint(result)" }, { "cell_type": "markdown", @@ -427,4 +261,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} \ No newline at end of file +} diff --git a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/README.md b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/README.md index b21185d58..1c3ddb04e 100644 --- a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/README.md +++ b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/README.md @@ -15,7 +15,7 @@ This tutorial series demonstrates how to connect multiple ISV SaaS platforms (Sa | Gateway Target types | Integration Provider Template (Salesforce), MCP Server (SAP) | | Inbound Auth IdP | Amazon Cognito | | Outbound Auth | CustomOauth2 (Salesforce Connected App), CustomOauth2 (SAP Cognito) | -| LLM model | Anthropic Claude Sonnet 4 | +| LLM model | Anthropic Claude Sonnet 4.6 (`us.anthropic.claude-sonnet-4-6` — replace `us.` with your region prefix or use `global.`) | | Tutorial vertical | Enterprise CRM + ERP | | Example complexity | Medium | | SDK used | boto3, requests | @@ -24,7 +24,7 @@ This tutorial series demonstrates how to connect multiple ISV SaaS platforms (Sa | # | Notebook | Description | |---|---|---| -| 1 | [01-salesforce-gateway-target.ipynb](01-salesforce-gateway-target.ipynb) | Add Salesforce Lightning Platform as a Gateway integration target with CustomOauth2 | +| 1 | [01-salesforce-gateway-target.ipynb](01-salesforce-gateway-target.ipynb) | Add Salesforce Lightning Platform via the built-in Integration Provider Template with CustomOauth2 | | 2 | [02-sap-mcp-server-target.ipynb](02-sap-mcp-server-target.ipynb) | Add AWS for SAP MCP Server as a Gateway MCP target | | 3 | [03-cross-isv-queries.ipynb](03-cross-isv-queries.ipynb) | Cross-system queries combining Salesforce + SAP through one gateway | @@ -35,7 +35,8 @@ This tutorial series demonstrates how to connect multiple ISV SaaS platforms (Sa ## Prerequisites - An AWS account with access to Amazon Bedrock AgentCore -- Python 3.11+ +- Model access enabled for `anthropic.claude-sonnet-4-6` in your region (see [Manage model access](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html)) +- Python 3.11–3.13 (Python 3.14 is not yet supported — the AWS CRT library lacks a 3.14 wheel) - A Salesforce Developer Edition org with a Connected App configured for OAuth2 `client_credentials` flow - Access to an AWS for SAP MCP Server deployment (see [documentation](https://docs.aws.amazon.com/mcp-sap/latest/awsforsapmcp/introduction.html)) - AWS CLI configured with appropriate credentials @@ -57,12 +58,16 @@ This tutorial series demonstrates how to connect multiple ISV SaaS platforms (Sa ## Important Notes - **Salesforce Developer Edition orgs** hibernate after ~24 hours of inactivity. Log into the Salesforce web UI to wake the org before running the notebooks. -- **CustomOauth2** is required for Salesforce Developer Edition orgs. The built-in `SalesforceOauth2` vendor uses `login.salesforce.com` which does not support `client_credentials` on Developer Edition domains. +- **CustomOauth2** is required for Salesforce Developer Edition orgs. The built-in `SalesforceOauth2` vendor hardcodes the `login.salesforce.com` OAuth endpoint. Developer Edition orgs only allow `client_credentials` on their org-specific domain (`*.develop.my.salesforce.com`), so we use `CustomOauth2` with the org's OAuth2 metadata. - **SAP MCP Server** runs in read-only mode by default. Write operations must be explicitly enabled in the SAP MCP Server configuration. ## Disclaimer -This is sample code for demonstration purposes only. Not intended for production use without additional security testing. +This is sample code for demonstration purposes only. Not intended for production use without additional security review. In particular: + +- **IAM permissions** in this tutorial use broad `*` resource scope for simplicity. Production deployments should scope resources to specific ARNs. +- **Salesforce target** uses the built-in Integration Provider Template, which must be created via the AWS Console (not API). See the [supported integrations](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-target-integrations.html). +- **Content-Type parameter** — The Salesforce schema exposes `Content-Type` as a tool parameter. Because the gateway [manages Content-Type as a restricted header](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-headers.html), pass `""` (empty string) for this parameter on create/update operations to prevent header duplication. ## License diff --git a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/diagrams.py b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/diagrams.py index adb7fe488..fcd177e02 100644 --- a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/diagrams.py +++ b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/diagrams.py @@ -63,7 +63,7 @@ def multi_isv_architecture(): gw = Custom("MCP Gateway\nJSON-RPC 2.0\nJWT Authorizer", ICON_RUNTIME) with Cluster("Gateway Targets", graph_attr=_C_TARGETS): - sf_target = Custom("Salesforce Target\nIntegration Provider\n43 tools", ICON_RUNTIME) + sf_target = Custom("Salesforce Target\nOpenAPI Schema\n43 tools", ICON_RUNTIME) sap_target = Custom("SAP Target\nMCP Server\n9 tools", ICON_RUNTIME) with Cluster("Outbound Auth", graph_attr=_C_OUTBOUND): diff --git a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/requirements.txt b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/requirements.txt index 2c5c3f603..066c5628f 100644 --- a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/requirements.txt +++ b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/requirements.txt @@ -1,4 +1,4 @@ -boto3>=1.34.0 -requests>=2.31.0 -strands-agents[mcp]>=0.1.0 -mcp>=1.10.0 +boto3>=1.34.0,<2.0.0 +requests>=2.31.0,<3.0.0 +strands-agents>=0.1.0,<1.0.0 +mcp>=1.10.0,<2.0.0 From 1a6dcda6dd4cf0b5cd82b8afbcc2b9bc11c955b7 Mon Sep 17 00:00:00 2001 From: Joachim Aumann Date: Tue, 12 May 2026 09:08:30 +0200 Subject: [PATCH 4/6] fix(gateway): suppress semgrep arbitrary-sleep false positive The time.sleep(10) is an intentional polling interval while waiting for async gateway target provisioning to complete. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../19-multi-isv-orchestration/gateway_mcp_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/gateway_mcp_client.py b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/gateway_mcp_client.py index eaee48eee..dcbb1876a 100644 --- a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/gateway_mcp_client.py +++ b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/gateway_mcp_client.py @@ -130,5 +130,5 @@ def wait_for_target_ready( return item.get("targetId", "") if status in ("FAILED", "SYNCHRONIZE_UNSUCCESSFUL"): raise RuntimeError(f"Target '{target_name}' failed with status: {status}") - time.sleep(10) + time.sleep(10) # nosemgrep: arbitrary-sleep — polling interval for async target provisioning raise TimeoutError(f"Target '{target_name}' did not reach READY within {timeout}s") From cabe9e826383b78e8961faa813b019553cc3e905 Mon Sep 17 00:00:00 2001 From: Joachim Aumann Date: Tue, 12 May 2026 11:43:29 +0200 Subject: [PATCH 5/6] style(gateway): apply AWS writing best practices to notebook text - Remove weasel word 'some' (NB01 Cell 18) - Remove filler 'actually' (NB02 Cell 21) - Remove weasel 'often' (NB02 Cell 21) - Split 33-word sentence into two (NB03 Cell 1) --- .../01-salesforce-gateway-target.ipynb | 4 ++-- .../02-sap-mcp-server-target.ipynb | 6 +++--- .../19-multi-isv-orchestration/03-cross-isv-queries.ipynb | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/01-salesforce-gateway-target.ipynb b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/01-salesforce-gateway-target.ipynb index e3ae9a9cd..5cb78426e 100644 --- a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/01-salesforce-gateway-target.ipynb +++ b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/01-salesforce-gateway-target.ipynb @@ -252,7 +252,7 @@ "source": [ "## Step 7: Invoke Salesforce Tools\n", "\n", - "Let's call some Salesforce tools through the gateway.\n", + "Let's invoke Salesforce tools through the gateway.\n", "\n", "> **Important:** Every Salesforce tool call requires the `domainName` parameter. Without it, the gateway resolves to a non-existent default domain." ] @@ -339,4 +339,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/02-sap-mcp-server-target.ipynb b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/02-sap-mcp-server-target.ipynb index a7cb0fcb6..f7e55ede3 100644 --- a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/02-sap-mcp-server-target.ipynb +++ b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/02-sap-mcp-server-target.ipynb @@ -238,11 +238,11 @@ "\n", "From the [AWS for SAP MCP Server documentation](https://docs.aws.amazon.com/mcp-sap/latest/awsforsapmcp/security.html):\n", "\n", - "1. **Read-only by default** \u2014 Write tools are disabled unless both global and per-operation flags are set to `true`. Only enable writes you actually need.\n", + "1. **Read-only by default** \u2014 Write tools are disabled unless both global and per-operation flags are set to `true`. Only enable writes your use case requires.\n", "2. **Principle of least privilege** \u2014 Assign the SAP user only the minimum necessary roles and authorizations.\n", "3. **Scope OAuth access** \u2014 Configure OAuth scopes to grant access only to specific required OData services.\n", "4. **Use odata_count before reads** \u2014 Understand data volume before issuing broad reads to avoid overwhelming the agent.\n", - "5. **Use get_metadata proactively** \u2014 SAP field names are often non-obvious (e.g., `OverallOrdReltdBillgStatus` not `OverallBillingStatus`). Always check metadata before constructing filters.\n", + "5. **Use get_metadata proactively** \u2014 SAP field names are non-obvious (e.g., `OverallOrdReltdBillgStatus` not `OverallBillingStatus`). Always check metadata before constructing filters.\n", "6. **Credentials never stored on disk** \u2014 The MCP Server retrieves credentials at runtime from AWS Secrets Manager." ] }, @@ -276,4 +276,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/03-cross-isv-queries.ipynb b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/03-cross-isv-queries.ipynb index aef9d91fd..fd5cd98f2 100644 --- a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/03-cross-isv-queries.ipynb +++ b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/03-cross-isv-queries.ipynb @@ -15,7 +15,7 @@ "\n", "## Overview\n", "\n", - "This notebook demonstrates the power of routing multiple ISV platforms through a single [Amazon Bedrock AgentCore Gateway](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway.html). With Salesforce (CRM) and SAP (ERP) connected to the same gateway, an AI agent can answer questions that span both systems \u2014 without the user needing to know which system holds which data.\n", + "This notebook demonstrates the power of routing multiple ISV platforms through a single [Amazon Bedrock AgentCore Gateway](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway.html). With Salesforce (CRM) and SAP (ERP) connected to the same gateway, an AI agent can answer questions that span both systems. The user does not need to know which system holds which data.\n", "\n", "**Use cases demonstrated:**\n", "1. Customer 360 \u2014 combine SAP business partner data with Salesforce account history\n", @@ -261,4 +261,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file From 1c6260d8ab0a5a1f18d65fd8b0ebee0a4177fee8 Mon Sep 17 00:00:00 2001 From: Joachim Aumann Date: Wed, 13 May 2026 15:48:34 +0200 Subject: [PATCH 6/6] fix(gateway): add allowedScopes to authorizer config for us-east-1 compatibility The CUSTOM_JWT authorizer in us-east-1 enforces scope validation on Cognito M2M tokens. Without allowedScopes, the gateway returns 403 "insufficient_scope". Adding the scope explicitly fixes cross-region compatibility. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../01-salesforce-gateway-target.ipynb | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/01-salesforce-gateway-target.ipynb b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/01-salesforce-gateway-target.ipynb index 5cb78426e..dfe83ac42 100644 --- a/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/01-salesforce-gateway-target.ipynb +++ b/01-tutorials/02-AgentCore-gateway/19-multi-isv-orchestration/01-salesforce-gateway-target.ipynb @@ -21,7 +21,7 @@ "Before starting this notebook, you need:\n", "\n", "1. **AWS account** with access to Amazon Bedrock AgentCore\n", - "2. **Salesforce Developer Edition org** \u2014 free at [developer.salesforce.com/signup](https://developer.salesforce.com/signup)\n", + "2. **Salesforce Developer Edition org** — free at [developer.salesforce.com/signup](https://developer.salesforce.com/signup)\n", "3. **Salesforce Connected App** configured for OAuth2 `client_credentials` flow (see Step 1 below)\n", "4. **Python 3.11+** with Jupyter kernel\n", "5. **AWS CLI** configured with credentials that have AgentCore permissions" @@ -53,7 +53,7 @@ "\n", "import gateway_mcp_client\n", "\n", - "# AWS credentials \u2014 set your profile\n", + "# AWS credentials — set your profile\n", "os.environ[\"AWS_PROFILE\"] = \"default\" # Change to your profile\n", "\n", "logging.basicConfig(\n", @@ -93,22 +93,22 @@ "\n", "### Option A: Connected App (Classic)\n", "\n", - "1. Log in to Salesforce \u2192 Setup (gear icon \u2192 Setup)\n", - "2. Quick Find \u2192 **App Manager** \u2192 **New Connected App**\n", + "1. Log in to Salesforce → Setup (gear icon → Setup)\n", + "2. Quick Find → **App Manager** → **New Connected App**\n", "3. Enable OAuth Settings:\n", " - Callback URL: `https://bedrock-agentcore..amazonaws.com/identities/oauth2/callback`\n", " - OAuth Scopes: `Full access (full)`, `Perform requests at any time (refresh_token, offline_access)`, `Manage user data via APIs (api)`\n", "4. Security settings:\n", - " - \u2705 Require Proof Key for Code Exchange (PKCE)\n", - " - \u2705 Require Secret for Web Server Flow\n", - " - \u2705 Enable Client Credentials Flow\n", - "5. Save \u2192 wait 2-10 minutes for propagation\n", - "6. **Critical:** App Manager \u2192 find your app \u2192 Manage \u2192 Edit Policies \u2192 Client Credentials Flow \u2192 set **Run As** to your admin username\n", + " - ✅ Require Proof Key for Code Exchange (PKCE)\n", + " - ✅ Require Secret for Web Server Flow\n", + " - ✅ Enable Client Credentials Flow\n", + "5. Save → wait 2-10 minutes for propagation\n", + "6. **Critical:** App Manager → find your app → Manage → Edit Policies → Client Credentials Flow → set **Run As** to your admin username\n", "7. Get Consumer Key + Consumer Secret from Manage Consumer Details\n", "\n", "### Option B: External Client App (newer orgs)\n", "\n", - "1. Setup \u2192 **External Client App Manager** \u2192 New External Client App\n", + "1. Setup → **External Client App Manager** → New External Client App\n", "2. Enable OAuth with same callback URL and scopes as above\n", "3. Enable Client Credentials Flow with Run As user = admin username\n", "4. Get Client ID and Secret from the app's OAuth settings page\n", @@ -147,11 +147,11 @@ "\n", "if resp.status_code == 200:\n", " sf_token_data = resp.json()\n", - " print(\"\u2713 Salesforce OAuth2 token obtained successfully\")\n", + " print(\"✓ Salesforce OAuth2 token obtained successfully\")\n", " print(f\" Token type: {sf_token_data.get('token_type')}\")\n", " print(f\" Instance URL: {sf_token_data.get('instance_url')}\")\n", "else:\n", - " print(f\"\u2717 Failed to get token: {resp.status_code}\")\n", + " print(f\"✗ Failed to get token: {resp.status_code}\")\n", " print(f\" Response: {resp.text}\")\n", " raise RuntimeError(\"Fix your Salesforce credentials before proceeding\")" ] @@ -166,7 +166,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "GATEWAY_NAME = f\"multi-isv-tutorial-{str(uuid.uuid4())[:8]}\"\nACCOUNT_ID = boto3.client(\"sts\").get_caller_identity()[\"Account\"]\nprint(f\"Gateway name: {GATEWAY_NAME}\")\nprint(f\"AWS account: {ACCOUNT_ID}\")\n\n# --- Cognito User Pool (inbound auth) ---\ncognito_client = boto3.client(\"cognito-idp\", region_name=REGION)\npool_resp = cognito_client.create_user_pool(\n PoolName=f\"{GATEWAY_NAME}-pool\",\n Policies={\"PasswordPolicy\": {\"MinimumLength\": 8}},\n)\nUSER_POOL_ID = pool_resp[\"UserPool\"][\"Id\"]\n\nCOGNITO_DOMAIN = f\"{GATEWAY_NAME}-domain\"\ncognito_client.create_user_pool_domain(Domain=COGNITO_DOMAIN, UserPoolId=USER_POOL_ID)\nTOKEN_ENDPOINT = f\"https://{COGNITO_DOMAIN}.auth.{REGION}.amazoncognito.com/oauth2/token\"\n\nRESOURCE_SERVER_ID = f\"{GATEWAY_NAME}-id\"\nSCOPE_NAME = \"invoke\"\ncognito_client.create_resource_server(\n UserPoolId=USER_POOL_ID,\n Identifier=RESOURCE_SERVER_ID,\n Name=f\"{GATEWAY_NAME} Gateway\",\n Scopes=[{\"ScopeName\": SCOPE_NAME, \"ScopeDescription\": \"Invoke gateway\"}],\n)\nFULL_SCOPE = f\"{RESOURCE_SERVER_ID}/{SCOPE_NAME}\"\n\napp_resp = cognito_client.create_user_pool_client(\n UserPoolId=USER_POOL_ID,\n ClientName=f\"{GATEWAY_NAME}-client\",\n GenerateSecret=True,\n AllowedOAuthFlows=[\"client_credentials\"],\n AllowedOAuthScopes=[FULL_SCOPE],\n AllowedOAuthFlowsUserPoolClient=True,\n)\nGW_CLIENT_ID = app_resp[\"UserPoolClient\"][\"ClientId\"]\nGW_CLIENT_SECRET = app_resp[\"UserPoolClient\"][\"ClientSecret\"]\nDISCOVERY_URL = f\"https://cognito-idp.{REGION}.amazonaws.com/{USER_POOL_ID}/.well-known/openid-configuration\"\nprint(f\" \u2713 Cognito pool + client created\")\n\n# --- IAM Role ---\niam = boto3.client(\"iam\")\nROLE_NAME = f\"agentcore-{GATEWAY_NAME}-role\"\ntrust_policy = {\n \"Version\": \"2012-10-17\",\n \"Statement\": [{\n \"Effect\": \"Allow\",\n \"Principal\": {\"Service\": \"bedrock-agentcore.amazonaws.com\"},\n \"Action\": \"sts:AssumeRole\",\n \"Condition\": {\n \"StringEquals\": {\"aws:SourceAccount\": ACCOUNT_ID},\n \"ArnLike\": {\"aws:SourceArn\": f\"arn:aws:bedrock-agentcore:{REGION}:{ACCOUNT_ID}:*\"},\n },\n }],\n}\nrole_resp = iam.create_role(\n RoleName=ROLE_NAME,\n AssumeRolePolicyDocument=json.dumps(trust_policy),\n Description=\"IAM role for AgentCore Gateway multi-ISV tutorial\",\n)\nROLE_ARN = role_resp[\"Role\"][\"Arn\"]\niam.put_role_policy(\n RoleName=ROLE_NAME,\n PolicyName=\"AgentCorePolicy\",\n PolicyDocument=json.dumps({\n \"Version\": \"2012-10-17\",\n \"Statement\": [{\n \"Effect\": \"Allow\",\n \"Action\": [\n \"bedrock-agentcore:*\", \"bedrock:*\", \"agent-credential-provider:*\",\n \"iam:PassRole\", \"secretsmanager:GetSecretValue\", \"s3:GetObject\",\n ],\n \"Resource\": \"*\",\n }],\n }),\n)\nprint(f\" \u2713 IAM role created: {ROLE_NAME}\")\ntime.sleep(10)\n\n# --- Gateway ---\ngateway_client = boto3.client(\"bedrock-agentcore-control\", region_name=REGION)\ngw_resp = gateway_client.create_gateway(\n name=GATEWAY_NAME,\n roleArn=ROLE_ARN,\n protocolType=\"MCP\",\n authorizerType=\"CUSTOM_JWT\",\n authorizerConfiguration={\n \"customJWTAuthorizer\": {\n \"discoveryUrl\": DISCOVERY_URL,\n \"allowedClients\": [GW_CLIENT_ID],\n }\n },\n)\nGATEWAY_ID = gw_resp[\"gatewayId\"]\nGATEWAY_URL = gw_resp[\"gatewayUrl\"]\n\nprint(f\" Waiting for gateway READY...\")\nfor _ in range(60):\n status = gateway_client.get_gateway(gatewayIdentifier=GATEWAY_ID)[\"status\"]\n if status == \"READY\":\n break\n if status == \"FAILED\":\n raise RuntimeError(\"Gateway creation failed\")\n time.sleep(5)\nelse:\n raise TimeoutError(\"Gateway did not reach READY within 5 minutes\")\n\nprint(f\"\\n{'='*60}\")\nprint(f\" \u2713 GATEWAY IS READY\")\nprint(f\"{'='*60}\")\nprint(f\" Gateway name: {GATEWAY_NAME}\")\nprint(f\" Gateway ID: {GATEWAY_ID}\")\nprint(f\" Gateway URL: {GATEWAY_URL}\")\nprint(f\"{'='*60}\")" + "source": "GATEWAY_NAME = f\"multi-isv-tutorial-{str(uuid.uuid4())[:8]}\"\nACCOUNT_ID = boto3.client(\"sts\").get_caller_identity()[\"Account\"]\nprint(f\"Gateway name: {GATEWAY_NAME}\")\nprint(f\"AWS account: {ACCOUNT_ID}\")\n\n# --- Cognito User Pool (inbound auth) ---\ncognito_client = boto3.client(\"cognito-idp\", region_name=REGION)\npool_resp = cognito_client.create_user_pool(\n PoolName=f\"{GATEWAY_NAME}-pool\",\n Policies={\"PasswordPolicy\": {\"MinimumLength\": 8}},\n)\nUSER_POOL_ID = pool_resp[\"UserPool\"][\"Id\"]\n\nCOGNITO_DOMAIN = f\"{GATEWAY_NAME}-domain\"\ncognito_client.create_user_pool_domain(Domain=COGNITO_DOMAIN, UserPoolId=USER_POOL_ID)\nTOKEN_ENDPOINT = f\"https://{COGNITO_DOMAIN}.auth.{REGION}.amazoncognito.com/oauth2/token\"\n\nRESOURCE_SERVER_ID = f\"{GATEWAY_NAME}-id\"\nSCOPE_NAME = \"invoke\"\ncognito_client.create_resource_server(\n UserPoolId=USER_POOL_ID,\n Identifier=RESOURCE_SERVER_ID,\n Name=f\"{GATEWAY_NAME} Gateway\",\n Scopes=[{\"ScopeName\": SCOPE_NAME, \"ScopeDescription\": \"Invoke gateway\"}],\n)\nFULL_SCOPE = f\"{RESOURCE_SERVER_ID}/{SCOPE_NAME}\"\n\napp_resp = cognito_client.create_user_pool_client(\n UserPoolId=USER_POOL_ID,\n ClientName=f\"{GATEWAY_NAME}-client\",\n GenerateSecret=True,\n AllowedOAuthFlows=[\"client_credentials\"],\n AllowedOAuthScopes=[FULL_SCOPE],\n AllowedOAuthFlowsUserPoolClient=True,\n)\nGW_CLIENT_ID = app_resp[\"UserPoolClient\"][\"ClientId\"]\nGW_CLIENT_SECRET = app_resp[\"UserPoolClient\"][\"ClientSecret\"]\nDISCOVERY_URL = f\"https://cognito-idp.{REGION}.amazonaws.com/{USER_POOL_ID}/.well-known/openid-configuration\"\nprint(f\" ✓ Cognito pool + client created\")\n\n# --- IAM Role ---\niam = boto3.client(\"iam\")\nROLE_NAME = f\"agentcore-{GATEWAY_NAME}-role\"\ntrust_policy = {\n \"Version\": \"2012-10-17\",\n \"Statement\": [{\n \"Effect\": \"Allow\",\n \"Principal\": {\"Service\": \"bedrock-agentcore.amazonaws.com\"},\n \"Action\": \"sts:AssumeRole\",\n \"Condition\": {\n \"StringEquals\": {\"aws:SourceAccount\": ACCOUNT_ID},\n \"ArnLike\": {\"aws:SourceArn\": f\"arn:aws:bedrock-agentcore:{REGION}:{ACCOUNT_ID}:*\"},\n },\n }],\n}\nrole_resp = iam.create_role(\n RoleName=ROLE_NAME,\n AssumeRolePolicyDocument=json.dumps(trust_policy),\n Description=\"IAM role for AgentCore Gateway multi-ISV tutorial\",\n)\nROLE_ARN = role_resp[\"Role\"][\"Arn\"]\niam.put_role_policy(\n RoleName=ROLE_NAME,\n PolicyName=\"AgentCorePolicy\",\n PolicyDocument=json.dumps({\n \"Version\": \"2012-10-17\",\n \"Statement\": [{\n \"Effect\": \"Allow\",\n \"Action\": [\n \"bedrock-agentcore:*\", \"bedrock:*\", \"agent-credential-provider:*\",\n \"iam:PassRole\", \"secretsmanager:GetSecretValue\", \"s3:GetObject\",\n ],\n \"Resource\": \"*\",\n }],\n }),\n)\nprint(f\" ✓ IAM role created: {ROLE_NAME}\")\ntime.sleep(10)\n\n# --- Gateway ---\ngateway_client = boto3.client(\"bedrock-agentcore-control\", region_name=REGION)\ngw_resp = gateway_client.create_gateway(\n name=GATEWAY_NAME,\n roleArn=ROLE_ARN,\n protocolType=\"MCP\",\n authorizerType=\"CUSTOM_JWT\",\n authorizerConfiguration={\n \"customJWTAuthorizer\": {\n \"discoveryUrl\": DISCOVERY_URL,\n \"allowedClients\": [GW_CLIENT_ID],\n \"allowedScopes\": [FULL_SCOPE],\n }\n },\n)\nGATEWAY_ID = gw_resp[\"gatewayId\"]\nGATEWAY_URL = gw_resp[\"gatewayUrl\"]\n\nprint(f\" Waiting for gateway READY...\")\nfor _ in range(60):\n status = gateway_client.get_gateway(gatewayIdentifier=GATEWAY_ID)[\"status\"]\n if status == \"READY\":\n break\n if status == \"FAILED\":\n raise RuntimeError(\"Gateway creation failed\")\n time.sleep(5)\nelse:\n raise TimeoutError(\"Gateway did not reach READY within 5 minutes\")\n\nprint(f\"\\n{'='*60}\")\nprint(f\" ✓ GATEWAY IS READY\")\nprint(f\"{'='*60}\")\nprint(f\" Gateway name: {GATEWAY_NAME}\")\nprint(f\" Gateway ID: {GATEWAY_ID}\")\nprint(f\" Gateway URL: {GATEWAY_URL}\")\nprint(f\"{'='*60}\")" }, { "cell_type": "markdown", @@ -176,7 +176,7 @@ "\n", "AgentCore Gateway needs OAuth2 credentials to authenticate to Salesforce on behalf of the agent.\n", "\n", - "> **Important:** Use `CustomOauth2` \u2014 NOT `SalesforceOauth2`. The built-in Salesforce vendor uses `login.salesforce.com` which does not support `client_credentials` on Developer Edition org domains. With `CustomOauth2`, we specify the org-specific token endpoint directly." + "> **Important:** Use `CustomOauth2` — NOT `SalesforceOauth2`. The built-in Salesforce vendor uses `login.salesforce.com` which does not support `client_credentials` on Developer Edition org domains. With `CustomOauth2`, we specify the org-specific token endpoint directly." ] }, { @@ -188,7 +188,7 @@ }, { "cell_type": "code", - "source": "# Summary \u2014 copy these values for the Console step and for notebooks 02/03\nprint(f\"\"\"\n{'='*60}\n CONNECTION DETAILS (save for notebooks 02 + 03)\n{'='*60}\n Gateway name: {GATEWAY_NAME}\n Gateway ID: {GATEWAY_ID}\n Gateway URL: {GATEWAY_URL}\n Cognito token endpoint: {TOKEN_ENDPOINT}\n Cognito client ID: {GW_CLIENT_ID}\n Cognito client secret: {GW_CLIENT_SECRET[:8]}...\n OAuth scope: {FULL_SCOPE}\n Credential provider: {CREDENTIAL_PROVIDER_NAME}\n{'='*60}\n\n FOR THE CONSOLE STEP (Step 5):\n - Gateway: {GATEWAY_NAME}\n - Target name: salesforce-target\n - Server URL: https://{{domainName}}.develop.my.salesforce.com/services/data/v62.0\n - Credential provider: {CREDENTIAL_PROVIDER_NAME}\n{'='*60}\n\"\"\")", + "source": "# Summary — copy these values for the Console step and for notebooks 02/03\nprint(f\"\"\"\n{'='*60}\n CONNECTION DETAILS (save for notebooks 02 + 03)\n{'='*60}\n Gateway name: {GATEWAY_NAME}\n Gateway ID: {GATEWAY_ID}\n Gateway URL: {GATEWAY_URL}\n Cognito token endpoint: {TOKEN_ENDPOINT}\n Cognito client ID: {GW_CLIENT_ID}\n Cognito client secret: {GW_CLIENT_SECRET[:8]}...\n OAuth scope: {FULL_SCOPE}\n Credential provider: {CREDENTIAL_PROVIDER_NAME}\n{'='*60}\n\n FOR THE CONSOLE STEP (Step 5):\n - Gateway: {GATEWAY_NAME}\n - Target name: salesforce-target\n - Server URL: https://{{domainName}}.develop.my.salesforce.com/services/data/v62.0\n - Credential provider: {CREDENTIAL_PROVIDER_NAME}\n{'='*60}\n\"\"\")", "metadata": {}, "execution_count": null, "outputs": [] @@ -196,14 +196,14 @@ { "cell_type": "markdown", "metadata": {}, - "source": "## Step 5: Add Salesforce as Gateway Target (Console)\n\nThe Salesforce integration uses the **built-in Integration Provider Template** \u2014 a pre-defined OpenAPI schema managed by AgentCore. This target type can only be created via the [AWS Management Console](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-target-integrations.html#gateway-target-integrations-supported-apis-salesforce).\n\n### Steps:\n\n1. Open the [AgentCore Gateway console](https://console.aws.amazon.com/bedrock-agentcore/home#/gateways)\n2. Select your gateway (`GATEWAY_NAME` printed above)\n3. Click **Add target**\n4. Select **Integration provider template** \u2192 **Salesforce Lightning Platform**\n5. Configure:\n - **Target name:** `salesforce-target`\n - **Server URL:** `https://{domainName}.develop.my.salesforce.com/services/data/v62.0`\n - **Authentication:** OAuth2\n - **Credential provider:** Select the credential provider created above\n6. Click **Create target**\n7. Wait for target status to become **READY** (1\u20132 minutes)\n\nOnce the target is READY, run the next cell to verify." + "source": "## Step 5: Add Salesforce as Gateway Target (Console)\n\nThe Salesforce integration uses the **built-in Integration Provider Template** — a pre-defined OpenAPI schema managed by AgentCore. This target type can only be created via the [AWS Management Console](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-target-integrations.html#gateway-target-integrations-supported-apis-salesforce).\n\n### Steps:\n\n1. Open the [AgentCore Gateway console](https://console.aws.amazon.com/bedrock-agentcore/home#/gateways)\n2. Select your gateway (`GATEWAY_NAME` printed above)\n3. Click **Add target**\n4. Select **Integration provider template** → **Salesforce Lightning Platform**\n5. Configure:\n - **Target name:** `salesforce-target`\n - **Server URL:** `https://{domainName}.develop.my.salesforce.com/services/data/v62.0`\n - **Authentication:** OAuth2\n - **Credential provider:** Select the credential provider created above\n6. Click **Create target**\n7. Wait for target status to become **READY** (1–2 minutes)\n\nOnce the target is READY, run the next cell to verify." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": "# Verify the Salesforce target is READY\nSF_TARGET_NAME = \"salesforce-target\"\n\ngateway_mcp_client.wait_for_target_ready(\n client=gateway_client,\n gateway_id=GATEWAY_ID,\n target_name=SF_TARGET_NAME,\n region=REGION,\n timeout=300,\n)\n\n# Get the target ID for cleanup later\ntargets = gateway_client.list_gateway_targets(gatewayIdentifier=GATEWAY_ID)\nSF_TARGET_ID = next(\n t[\"targetId\"] for t in targets[\"items\"] if t[\"name\"] == SF_TARGET_NAME\n)\nprint(f\"\u2713 Salesforce target is READY (ID: {SF_TARGET_ID})\")" + "source": "# Verify the Salesforce target is READY\nSF_TARGET_NAME = \"salesforce-target\"\n\ngateway_mcp_client.wait_for_target_ready(\n client=gateway_client,\n gateway_id=GATEWAY_ID,\n target_name=SF_TARGET_NAME,\n region=REGION,\n timeout=300,\n)\n\n# Get the target ID for cleanup later\ntargets = gateway_client.list_gateway_targets(gatewayIdentifier=GATEWAY_ID)\nSF_TARGET_ID = next(\n t[\"targetId\"] for t in targets[\"items\"] if t[\"name\"] == SF_TARGET_NAME\n)\nprint(f\"✓ Salesforce target is READY (ID: {SF_TARGET_ID})\")" }, { "cell_type": "markdown", @@ -276,7 +276,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "# Create a test account\n# Content-Type is a restricted header managed by the gateway \u2014 pass \"\" to avoid duplication\nresult = mcp.call_tool(\n \"salesforce-target___createAccount\",\n {\n \"domainName\": SF_DOMAIN,\n \"Content-Type\": \"\",\n \"Name\": \"AgentCore Tutorial Test Account\",\n \"Industry\": \"Technology\",\n \"Description\": \"Created by AgentCore Gateway multi-ISV tutorial\",\n },\n)\nprint(\"=== Create Account ===\")\ncontent = result.get(\"result\", {}).get(\"content\", [])\nfor item in content:\n if item.get(\"type\") == \"text\":\n data = json.loads(item[\"text\"])\n if data.get(\"success\"):\n print(f\" \u2713 Account created: {data['id']}\")\n else:\n print(f\" \u2717 Failed: {data.get('errors', data)}\")" + "source": "# Create a test account\n# Content-Type is a restricted header managed by the gateway — pass \"\" to avoid duplication\nresult = mcp.call_tool(\n \"salesforce-target___createAccount\",\n {\n \"domainName\": SF_DOMAIN,\n \"Content-Type\": \"\",\n \"Name\": \"AgentCore Tutorial Test Account\",\n \"Industry\": \"Technology\",\n \"Description\": \"Created by AgentCore Gateway multi-ISV tutorial\",\n },\n)\nprint(\"=== Create Account ===\")\ncontent = result.get(\"result\", {}).get(\"content\", [])\nfor item in content:\n if item.get(\"type\") == \"text\":\n data = json.loads(item[\"text\"])\n if data.get(\"success\"):\n print(f\" ✓ Account created: {data['id']}\")\n else:\n print(f\" ✗ Failed: {data.get('errors', data)}\")" }, { "cell_type": "code", @@ -315,7 +315,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "# Delete test data created in Salesforce\nprint(\"Cleaning up Salesforce test data...\")\ntry:\n query_result = mcp.call_tool(\n \"salesforce-target___queryAccounts\",\n {\"domainName\": SF_DOMAIN, \"q\": \"SELECT Id FROM Account WHERE Name = 'AgentCore Tutorial Test Account'\"},\n )\n content = query_result.get(\"result\", {}).get(\"content\", [])\n for item in content:\n if item.get(\"type\") == \"text\":\n data = json.loads(item[\"text\"])\n for record in data.get(\"records\", []):\n mcp.call_tool(\n \"salesforce-target___deleteAccount\",\n {\"domainName\": SF_DOMAIN, \"Content-Type\": \"\", \"accountId\": record[\"Id\"]},\n )\n print(f\" \u2713 Deleted test account {record['Id']}\")\nexcept Exception as e:\n print(f\" (Could not clean up SF test data: {e})\")\n\n# Delete gateway target\nprint(\"Deleting Salesforce gateway target...\")\ngateway_client.delete_gateway_target(\n gatewayIdentifier=GATEWAY_ID,\n targetId=SF_TARGET_ID,\n)\ntime.sleep(5)\nprint(\" \u2713 Target deleted\")\n\n# Delete credential provider\nprint(\"Deleting credential provider...\")\nidentity_client.delete_oauth2_credential_provider(name=CREDENTIAL_PROVIDER_NAME)\nprint(\" \u2713 Credential provider deleted\")\n\n# Delete gateway\nprint(\"Deleting gateway...\")\ngateway_client.delete_gateway(gatewayIdentifier=GATEWAY_ID)\ntime.sleep(5)\nprint(\" \u2713 Gateway deleted\")\n\n# Delete Cognito resources\nprint(\"Deleting Cognito resources...\")\ncognito_client.delete_user_pool_domain(Domain=COGNITO_DOMAIN, UserPoolId=USER_POOL_ID)\ncognito_client.delete_user_pool(UserPoolId=USER_POOL_ID)\nprint(\" \u2713 Cognito pool deleted\")\n\n# Delete IAM role (must remove inline policy first)\nprint(\"Deleting IAM role...\")\niam.delete_role_policy(RoleName=ROLE_NAME, PolicyName=\"AgentCorePolicy\")\niam.delete_role(RoleName=ROLE_NAME)\nprint(\" \u2713 IAM role deleted\")\n\nprint(\"\\n\u2713 All resources cleaned up\")" + "source": "# Delete test data created in Salesforce\nprint(\"Cleaning up Salesforce test data...\")\ntry:\n query_result = mcp.call_tool(\n \"salesforce-target___queryAccounts\",\n {\"domainName\": SF_DOMAIN, \"q\": \"SELECT Id FROM Account WHERE Name = 'AgentCore Tutorial Test Account'\"},\n )\n content = query_result.get(\"result\", {}).get(\"content\", [])\n for item in content:\n if item.get(\"type\") == \"text\":\n data = json.loads(item[\"text\"])\n for record in data.get(\"records\", []):\n mcp.call_tool(\n \"salesforce-target___deleteAccount\",\n {\"domainName\": SF_DOMAIN, \"Content-Type\": \"\", \"accountId\": record[\"Id\"]},\n )\n print(f\" ✓ Deleted test account {record['Id']}\")\nexcept Exception as e:\n print(f\" (Could not clean up SF test data: {e})\")\n\n# Delete gateway target\nprint(\"Deleting Salesforce gateway target...\")\ngateway_client.delete_gateway_target(\n gatewayIdentifier=GATEWAY_ID,\n targetId=SF_TARGET_ID,\n)\ntime.sleep(5)\nprint(\" ✓ Target deleted\")\n\n# Delete credential provider\nprint(\"Deleting credential provider...\")\nidentity_client.delete_oauth2_credential_provider(name=CREDENTIAL_PROVIDER_NAME)\nprint(\" ✓ Credential provider deleted\")\n\n# Delete gateway\nprint(\"Deleting gateway...\")\ngateway_client.delete_gateway(gatewayIdentifier=GATEWAY_ID)\ntime.sleep(5)\nprint(\" ✓ Gateway deleted\")\n\n# Delete Cognito resources\nprint(\"Deleting Cognito resources...\")\ncognito_client.delete_user_pool_domain(Domain=COGNITO_DOMAIN, UserPoolId=USER_POOL_ID)\ncognito_client.delete_user_pool(UserPoolId=USER_POOL_ID)\nprint(\" ✓ Cognito pool deleted\")\n\n# Delete IAM role (must remove inline policy first)\nprint(\"Deleting IAM role...\")\niam.delete_role_policy(RoleName=ROLE_NAME, PolicyName=\"AgentCorePolicy\")\niam.delete_role(RoleName=ROLE_NAME)\nprint(\" ✓ IAM role deleted\")\n\nprint(\"\\n✓ All resources cleaned up\")" } ], "metadata": {