diff --git a/docs/remote_inference_blueprints/sagemaker_connector_blueprint.md b/docs/remote_inference_blueprints/sagemaker_connector_blueprint.md index 4a90977deb..4dd7eb7ea5 100644 --- a/docs/remote_inference_blueprints/sagemaker_connector_blueprint.md +++ b/docs/remote_inference_blueprints/sagemaker_connector_blueprint.md @@ -47,7 +47,7 @@ POST /_plugins/_ml/connectors/_create "content-type": "application/json" }, "url": "", - "request_body": "${parameters.inputs}", + "request_body": "${parameters.input}", "pre_process_function": "connector.pre_process.default.embedding", "post_process_function": "connector.post_process.default.embedding" } diff --git a/docs/tutorials/aws/AIConnectorHelper.ipynb b/docs/tutorials/aws/AIConnectorHelper.ipynb new file mode 100644 index 0000000000..713ba6c331 --- /dev/null +++ b/docs/tutorials/aws/AIConnectorHelper.ipynb @@ -0,0 +1,1103 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "5f2363c3", + "metadata": {}, + "outputs": [], + "source": [ + "import boto3\n", + "from botocore.exceptions import BotoCoreError\n", + "import json\n", + "import requests\n", + "from requests.auth import HTTPBasicAuth\n", + "from requests_aws4auth import AWS4Auth\n", + "import time\n", + "\n", + "# This python code works for AWS OpenSearch 2.11\n", + "class AIConnectorHelper:\n", + " \n", + " def __init__(self, region, opensearch_domain_name, opensearch_domain_username, opensearch_domain_password, aws_user_name):\n", + " self.region = region\n", + " self.opensearch_domain_url, self.opensearch_domain_arn = AIConnectorHelper.get_opensearch_domain_info(region, opensearch_domain_name)\n", + " self.opensearch_domain_username = opensearch_domain_username\n", + " self.opensearch_domain_opensearch_domain_password = opensearch_domain_password\n", + " self.aws_user_name = aws_user_name\n", + " \n", + " @staticmethod \n", + " def get_opensearch_domain_info(region, domain_name):\n", + " # Create a boto3 client for OpenSearch\n", + " opensearch_client = boto3.client('es', region_name=region)\n", + "\n", + " try:\n", + " # Describe the domain to get its information\n", + " response = opensearch_client.describe_elasticsearch_domain(DomainName=domain_name)\n", + " domain_info = response['DomainStatus']\n", + "\n", + " # Extract the domain URL and ARN\n", + " domain_url = domain_info['Endpoint']\n", + " domain_arn = domain_info['ARN']\n", + "\n", + " return f'https://{domain_url}', domain_arn\n", + "\n", + " except opensearch_client.exceptions.ResourceNotFoundException:\n", + " print(f\"Domain '{domain_name}' not found.\")\n", + " return None, None\n", + " \n", + " def get_user_arn(self, username):\n", + " # Create a boto3 client for IAM\n", + " iam_client = boto3.client('iam')\n", + "\n", + " try:\n", + " # Get information about the IAM user\n", + " response = iam_client.get_user(UserName=username)\n", + " user_arn = response['User']['Arn']\n", + " return user_arn\n", + " except iam_client.exceptions.NoSuchEntityException:\n", + " print(f\"IAM user '{username}' not found.\")\n", + " return None\n", + "\n", + " def secret_exists(self, secret_name):\n", + " secretsmanager = boto3.client('secretsmanager', region_name=self.region)\n", + " try:\n", + " # Try to get the secret\n", + " secretsmanager.get_secret_value(SecretId=secret_name)\n", + " # If no exception was raised by get_secret_value, the secret exists\n", + " return True\n", + " except secretsmanager.exceptions.ResourceNotFoundException:\n", + " # If a ResourceNotFoundException was raised, the secret does not exist\n", + " return False\n", + "\n", + " def get_secret_arn(self, secret_name):\n", + " secretsmanager = boto3.client('secretsmanager', region_name=self.region)\n", + " try:\n", + " response = secretsmanager.describe_secret(SecretId=secret_name)\n", + " # Return ARN of the secret \n", + " return response['ARN']\n", + " except secretsmanager.exceptions.ResourceNotFoundException:\n", + " print(f\"The requested secret {secret_name} was not found\")\n", + " return None\n", + " except Exception as e:\n", + " print(f\"An error occurred: {e}\")\n", + " return None\n", + " \n", + " def get_secret(self, secret_name):\n", + " secretsmanager = boto3.client('secretsmanager', region_name=self.region)\n", + " try:\n", + " response = secretsmanager.get_secret_value(SecretId=secret_name)\n", + " except secretsmanager.exceptions.NoSuchEntityException:\n", + " print(\"The requested secret was not found\")\n", + " return None\n", + " except Exception as e:\n", + " print(f\"An error occurred: {e}\")\n", + " return None\n", + " else:\n", + " return response.get('SecretString')\n", + "\n", + " def create_secret(self, secret_name, secret_value):\n", + " secretsmanager = boto3.client('secretsmanager', region_name=self.region)\n", + "\n", + " try:\n", + " response = secretsmanager.create_secret(\n", + " Name=secret_name,\n", + " SecretString=json.dumps(secret_value),\n", + " )\n", + " print(f'Secret {secret_name} created successfully.')\n", + " return response['ARN'] # Return the ARN of the created secret\n", + " except BotoCoreError as e:\n", + " print(f'Error creating secret: {e}')\n", + " return None\n", + "\n", + "\n", + " def role_exists(self, role_name):\n", + " iam_client = boto3.client('iam')\n", + "\n", + " try:\n", + " iam_client.get_role(RoleName=role_name)\n", + " return True\n", + " except iam_client.exceptions.NoSuchEntityException:\n", + " return False\n", + "\n", + " def delete_role(self, role_name):\n", + " iam_client = boto3.client('iam')\n", + "\n", + " try:\n", + " # Detach managed policies\n", + " policies = iam_client.list_attached_role_policies(RoleName=role_name)['AttachedPolicies']\n", + " for policy in policies:\n", + " iam_client.detach_role_policy(RoleName=role_name, PolicyArn=policy['PolicyArn'])\n", + " print(f'All managed policies detached from role {role_name}.')\n", + "\n", + " # Delete inline policies\n", + " inline_policies = iam_client.list_role_policies(RoleName=role_name)['PolicyNames']\n", + " for policy_name in inline_policies:\n", + " iam_client.delete_role_policy(RoleName=role_name, PolicyName=policy_name)\n", + " print(f'All inline policies deleted from role {role_name}.')\n", + "\n", + " # Now, delete the role\n", + " iam_client.delete_role(RoleName=role_name)\n", + " print(f'Role {role_name} deleted.')\n", + "\n", + " except iam_client.exceptions.NoSuchEntityException:\n", + " print(f'Role {role_name} does not exist.')\n", + "\n", + "\n", + " def create_iam_role(self, role_name, trust_policy_json, inline_policy_json):\n", + " iam_client = boto3.client('iam')\n", + "\n", + " try:\n", + " # Create the role with the trust policy\n", + " create_role_response = iam_client.create_role(\n", + " RoleName=role_name,\n", + " AssumeRolePolicyDocument=json.dumps(trust_policy_json),\n", + " Description='Role with custom trust and inline policies',\n", + " )\n", + "\n", + " # Get the ARN of the newly created role\n", + " role_arn = create_role_response['Role']['Arn']\n", + "\n", + " # Attach the inline policy to the role\n", + " iam_client.put_role_policy(\n", + " RoleName=role_name,\n", + " PolicyName='InlinePolicy', # you can replace this with your preferred policy name\n", + " PolicyDocument=json.dumps(inline_policy_json)\n", + " )\n", + "\n", + " print(f'Created role: {role_name}')\n", + " return role_arn\n", + "\n", + " except Exception as e:\n", + " print(f\"Error creating the role: {e}\")\n", + " return None\n", + "\n", + " def get_role_arn(self, role_name):\n", + " iam_client = boto3.client('iam')\n", + " try:\n", + " response = iam_client.get_role(RoleName=role_name)\n", + " # Return ARN of the role\n", + " return response['Role']['Arn']\n", + " except iam_client.exceptions.NoSuchEntityException:\n", + " print(f\"The requested role {role_name} does not exist\")\n", + " return None\n", + " except Exception as e:\n", + " print(f\"An error occurred: {e}\")\n", + " return None\n", + " \n", + " \n", + " def get_role_details(self, role_name):\n", + " iam = boto3.client('iam')\n", + "\n", + " try:\n", + " response = iam.get_role(RoleName=role_name)\n", + " role = response['Role']\n", + "\n", + " print(f\"Role Name: {role['RoleName']}\")\n", + " print(f\"Role ID: {role['RoleId']}\")\n", + " print(f\"ARN: {role['Arn']}\")\n", + " print(f\"Creation Date: {role['CreateDate']}\")\n", + " print(\"Assume Role Policy Document:\")\n", + " print(json.dumps(role['AssumeRolePolicyDocument'], indent=4, sort_keys=True))\n", + "\n", + " list_role_policies_response = iam.list_role_policies(RoleName=role_name)\n", + "\n", + " for policy_name in list_role_policies_response['PolicyNames']:\n", + " get_role_policy_response = iam.get_role_policy(RoleName=role_name, PolicyName=policy_name)\n", + " print(f\"Role Policy Name: {get_role_policy_response['PolicyName']}\")\n", + " print(\"Role Policy Document:\")\n", + " print(json.dumps(get_role_policy_response['PolicyDocument'], indent=4, sort_keys=True))\n", + "\n", + " except iam.exceptions.NoSuchEntityException:\n", + " print(f'Role {role_name} does not exist.')\n", + "\n", + " def map_iam_role_to_backend_role(self, role_arn, os_security_role='ml_full_access'):\n", + " url = f'{self.opensearch_domain_url}/_plugins/_security/api/rolesmapping/{os_security_role}'\n", + " r=requests.get(url, auth=HTTPBasicAuth(self.opensearch_domain_username, self.opensearch_domain_opensearch_domain_password))\n", + " role_mapping = json.loads(r.text)\n", + " headers = {\"Content-Type\": \"application/json\"}\n", + " if 'status' in role_mapping and role_mapping['status'] == 'NOT_FOUND':\n", + " data = {'backend_roles': [ role_arn ] }\n", + " response = requests.put(url, headers=headers, data=json.dumps(data), auth=HTTPBasicAuth(self.opensearch_domain_username, self.opensearch_domain_opensearch_domain_password))\n", + " print(response.text)\n", + " else:\n", + " role_mapping = role_mapping[os_security_role]\n", + " role_mapping['backend_roles'].append(role_arn)\n", + " data = [\n", + " {\n", + " \"op\": \"replace\", \"path\": \"/backend_roles\", \"value\": list(set(role_mapping['backend_roles']))\n", + " }\n", + " ]\n", + " response = requests.patch(url, headers=headers, data=json.dumps(data), auth=HTTPBasicAuth(self.opensearch_domain_username, self.opensearch_domain_opensearch_domain_password))\n", + " print(response.text)\n", + "\n", + " def assume_role(self, create_connector_role_arn, role_session_name=\"your_session_name\"):\n", + " sts_client = boto3.client('sts')\n", + "\n", + " #role_arn = f\"arn:aws:iam::{aws_account_id}:role/{role_name}\"\n", + " assumed_role_object = sts_client.assume_role(\n", + " RoleArn=create_connector_role_arn,\n", + " RoleSessionName=role_session_name,\n", + " )\n", + "\n", + " # Obtain the temporary credentials from the assumed role \n", + " temp_credentials = assumed_role_object[\"Credentials\"]\n", + "\n", + " return temp_credentials\n", + "\n", + " def create_connector(self, create_connector_role_name, payload):\n", + " create_connector_role_arn = self.get_role_arn(create_connector_role_name)\n", + " temp_credentials = self.assume_role(create_connector_role_arn)\n", + " awsauth = AWS4Auth(\n", + " temp_credentials[\"AccessKeyId\"],\n", + " temp_credentials[\"SecretAccessKey\"],\n", + " self.region,\n", + " 'es',\n", + " session_token=temp_credentials[\"SessionToken\"],\n", + " )\n", + "\n", + " path = '/_plugins/_ml/connectors/_create'\n", + " url = self.opensearch_domain_url + path\n", + "\n", + " headers = {\"Content-Type\": \"application/json\"}\n", + "\n", + " r = requests.post(url, auth=awsauth, json=payload, headers=headers)\n", + " print(r.text)\n", + " connector_id = json.loads(r.text)['connector_id']\n", + " return connector_id\n", + " \n", + " def create_model(self, model_name, description, connector_id, deploy=True):\n", + " payload = {\n", + " \"name\": model_name,\n", + " \"function_name\": \"remote\",\n", + " \"description\": description,\n", + " \"connector_id\": connector_id\n", + " }\n", + "\n", + " headers = {\"Content-Type\": \"application/json\"}\n", + "\n", + " deploy_str = str(deploy).lower()\n", + " r = requests.post(f'{self.opensearch_domain_url}/_plugins/_ml/models/_register?deploy={deploy_str}',\n", + " auth=HTTPBasicAuth(self.opensearch_domain_username, self.opensearch_domain_opensearch_domain_password),\n", + " json=payload,\n", + " headers=headers)\n", + " print(r.text)\n", + " model_id = json.loads(r.text)['model_id']\n", + " return model_id\n", + " \n", + " def deploy_model(self, model_id):\n", + " return requests.post(f'{self.opensearch_domain_url}/_plugins/_ml/models/{model_id}/_deploy',\n", + " auth=HTTPBasicAuth(self.opensearch_domain_username, self.opensearch_domain_opensearch_domain_password),\n", + " headers=headers)\n", + " \n", + " def predict(self, model_id, payload):\n", + " headers = {\"Content-Type\": \"application/json\"}\n", + "\n", + " r = requests.post(f'{self.opensearch_domain_url}/_plugins/_ml/models/{model_id}/_predict',\n", + " auth=HTTPBasicAuth(self.opensearch_domain_username, self.opensearch_domain_opensearch_domain_password),\n", + " json=payload,\n", + " headers=headers)\n", + " return r.text\n", + " \n", + " def create_connector_with_secret(self, secret_name, secret_value, connector_role_name, create_connector_role_name, create_connector_input, sleep_time_in_seconds=10):\n", + " # Step1: Create Secret\n", + " print('Step1: Create Secret')\n", + " if not self.secret_exists(secret_name):\n", + " secret_arn = self.create_secret(secret_name, secret_value)\n", + " else:\n", + " print('secret exists, skip creating')\n", + " secret_arn = self.get_secret_arn(secret_name)\n", + " #print(secret_arn)\n", + " print('----------')\n", + " \n", + " # Step2: Create IAM role configued in connector\n", + " trust_policy = {\n", + " \"Version\": \"2012-10-17\",\n", + " \"Statement\": [\n", + " {\n", + " \"Effect\": \"Allow\",\n", + " \"Principal\": {\n", + " \"Service\": \"es.amazonaws.com\"\n", + " },\n", + " \"Action\": \"sts:AssumeRole\"\n", + " }\n", + " ]\n", + " }\n", + "\n", + " inline_policy = {\n", + " \"Version\": \"2012-10-17\",\n", + " \"Statement\": [\n", + " {\n", + " \"Action\": [\n", + " \"secretsmanager:GetSecretValue\"\n", + " ],\n", + " \"Effect\": \"Allow\",\n", + " \"Resource\": secret_arn\n", + " }\n", + " ]\n", + " }\n", + "\n", + " print('Step2: Create IAM role configued in connector')\n", + " if not self.role_exists(connector_role_name):\n", + " connector_role_arn = self.create_iam_role(connector_role_name, trust_policy, inline_policy)\n", + " else:\n", + " print('role exists, skip creating')\n", + " connector_role_arn = self.get_role_arn(connector_role_name)\n", + " #print(connector_role_arn)\n", + " print('----------')\n", + " \n", + " # Step 3: Configure IAM role in OpenSearch\n", + " # 3.1 Create IAM role for Signing create connector request\n", + " user_arn = self.get_user_arn(self.aws_user_name)\n", + " trust_policy = {\n", + " \"Version\": \"2012-10-17\",\n", + " \"Statement\": [\n", + " {\n", + " \"Effect\": \"Allow\",\n", + " \"Principal\": {\n", + " \"AWS\": user_arn\n", + " },\n", + " \"Action\": \"sts:AssumeRole\"\n", + " }\n", + " ]\n", + " }\n", + "\n", + " inline_policy = {\n", + " \"Version\": \"2012-10-17\",\n", + " \"Statement\": [\n", + " {\n", + " \"Effect\": \"Allow\",\n", + " \"Action\": \"iam:PassRole\",\n", + " \"Resource\": connector_role_arn\n", + " },\n", + " {\n", + " \"Effect\": \"Allow\",\n", + " \"Action\": \"es:ESHttpPost\",\n", + " \"Resource\": self.opensearch_domain_arn\n", + " }\n", + " ]\n", + " }\n", + "\n", + " print('Step 3: Configure IAM role in OpenSearch')\n", + " print('Step 3.1: Create IAM role for Signing create connector request')\n", + " if not self.role_exists(create_connector_role_name):\n", + " create_connector_role_arn = self.create_iam_role(create_connector_role_name, trust_policy, inline_policy)\n", + " else:\n", + " print('role exists, skip creating')\n", + " create_connector_role_arn = self.get_role_arn(create_connector_role_name)\n", + " #print(create_connector_role_arn)\n", + " print('----------')\n", + " \n", + " # 3.2 Map backend role\n", + " print(f'Step 3.2: Map IAM role {create_connector_role_name} to OpenSearch permission role')\n", + " self.map_iam_role_to_backend_role(create_connector_role_arn)\n", + " print('----------')\n", + " \n", + " # 4. Create connector\n", + " print('Step 4: Create connector in OpenSearch')\n", + " # When you create an IAM role, it can take some time for the changes to propagate across AWS systems.\n", + " # During this time, some services might not immediately recognize the new role or its permissions.\n", + " # So we wait for some time before creating connector.\n", + " # If you see such error: ClientError: An error occurred (AccessDenied) when calling the AssumeRole operation\n", + " # you can rerun this function.\n", + " \n", + " # Wait for some time\n", + " time.sleep(sleep_time_in_seconds)\n", + " payload = create_connector_input\n", + " payload['credential'] = {\n", + " \"secretArn\": secret_arn,\n", + " \"roleArn\": connector_role_arn\n", + " }\n", + " connector_id = self.create_connector(create_connector_role_name, payload)\n", + " #print(connector_id)\n", + " print('----------')\n", + " return connector_id\n", + " \n", + " def create_connector_with_role(self, connector_role_inline_policy, connector_role_name, create_connector_role_name, create_connector_input, sleep_time_in_seconds=10):\n", + " # Step1: Create IAM role configued in connector\n", + " trust_policy = {\n", + " \"Version\": \"2012-10-17\",\n", + " \"Statement\": [\n", + " {\n", + " \"Effect\": \"Allow\",\n", + " \"Principal\": {\n", + " \"Service\": \"es.amazonaws.com\"\n", + " },\n", + " \"Action\": \"sts:AssumeRole\"\n", + " }\n", + " ]\n", + " }\n", + "\n", + " print('Step1: Create IAM role configued in connector')\n", + " if not self.role_exists(connector_role_name):\n", + " connector_role_arn = self.create_iam_role(connector_role_name, trust_policy, connector_role_inline_policy)\n", + " else:\n", + " print('role exists, skip creating')\n", + " connector_role_arn = self.get_role_arn(connector_role_name)\n", + " #print(connector_role_arn)\n", + " print('----------')\n", + "\n", + " # Step 2: Configure IAM role in OpenSearch\n", + " # 2.1 Create IAM role for Signing create connector request\n", + " user_arn = self.get_user_arn(self.aws_user_name)\n", + " trust_policy = {\n", + " \"Version\": \"2012-10-17\",\n", + " \"Statement\": [\n", + " {\n", + " \"Effect\": \"Allow\",\n", + " \"Principal\": {\n", + " \"AWS\": user_arn\n", + " },\n", + " \"Action\": \"sts:AssumeRole\"\n", + " }\n", + " ]\n", + " }\n", + "\n", + " inline_policy = {\n", + " \"Version\": \"2012-10-17\",\n", + " \"Statement\": [\n", + " {\n", + " \"Effect\": \"Allow\",\n", + " \"Action\": \"iam:PassRole\",\n", + " \"Resource\": connector_role_arn\n", + " },\n", + " {\n", + " \"Effect\": \"Allow\",\n", + " \"Action\": \"es:ESHttpPost\",\n", + " \"Resource\": self.opensearch_domain_arn\n", + " }\n", + " ]\n", + " }\n", + "\n", + " print('Step 2: Configure IAM role in OpenSearch')\n", + " print('Step 2.1: Create IAM role for Signing create connector request')\n", + " if not self.role_exists(create_connector_role_name):\n", + " create_connector_role_arn = self.create_iam_role(create_connector_role_name, trust_policy, inline_policy)\n", + " else:\n", + " print('role exists, skip creating')\n", + " create_connector_role_arn = self.get_role_arn(create_connector_role_name)\n", + " #print(create_connector_role_arn)\n", + " print('----------')\n", + "\n", + " # 2.2 Map backend role\n", + " print(f'Step 2.2: Map IAM role {create_connector_role_name} to OpenSearch permission role')\n", + " self.map_iam_role_to_backend_role(create_connector_role_arn)\n", + " print('----------')\n", + "\n", + " # 3. Create connector\n", + " print('Step 3: Create connector in OpenSearch')\n", + " # When you create an IAM role, it can take some time for the changes to propagate across AWS systems.\n", + " # During this time, some services might not immediately recognize the new role or its permissions.\n", + " # So we wait for some time before creating connector.\n", + " # If you see such error: ClientError: An error occurred (AccessDenied) when calling the AssumeRole operation\n", + " # you can rerun this function.\n", + "\n", + " # Wait for some time\n", + " time.sleep(sleep_time_in_seconds)\n", + " payload = create_connector_input\n", + " payload['credential'] = {\n", + " \"roleArn\": connector_role_arn\n", + " }\n", + " connector_id = self.create_connector(create_connector_role_name, payload)\n", + " #print(connector_id)\n", + " print('----------')\n", + " return connector_id" + ] + }, + { + "cell_type": "markdown", + "id": "cb94f44d", + "metadata": {}, + "source": [ + "# 0. Create helper" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "3e50f513", + "metadata": {}, + "outputs": [], + "source": [ + "region = '...' # set your AWS region, for example us-west-2\n", + "opensearch_domain_name = '...' # set your AWS OpenSearch domain name\n", + "opensearch_domain_username = '...' # set your AWS OpenSearch domain admin username\n", + "opensearch_domain_password = '...' # set your domain password\n", + "\n", + "aws_user_name = '...' # set your AWS IAM user name, not IAM user ARN. \n", + " # To avoid permission issue and quick start, you can use user whith AdministratorAccess policy\n", + " # Configure this user's access key and secret key in ~/.aws/credential \n", + " # You can configure ~/.aws/credential as:\n", + "'''\n", + "[default]\n", + "AWS_ACCESS_KEY_ID = YOUR_ACCESS_KEY_ID\n", + "AWS_SECRET_ACCESS_KEY = YOUR_SECRET_ACCESS_KEY\n", + "'''\n", + "\n", + "helper = AIConnectorHelper(region, \n", + " opensearch_domain_name, \n", + " opensearch_domain_username, \n", + " opensearch_domain_password, \n", + " aws_user_name)" + ] + }, + { + "cell_type": "markdown", + "id": "8235725f", + "metadata": {}, + "source": [ + "# 1. Create Connector of Bedrock Embedding Model\n", + "\n", + "Read [semantic_search_with_bedrock_titan_embedding_model](./semantic_search_with_bedrock_titan_embedding_model.md) for more details." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "8287c045", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Step1: Create IAM role configued in connector\n", + "Created role: my_test_bedrock_connector_role\n", + "----------\n", + "Step 2: Configure IAM role in OpenSearch\n", + "Step 2.1: Create IAM role for Signing create connector request\n", + "Created role: my_test_create_bedrock_connector_role\n", + "----------\n", + "Step 2.2: Map IAM role my_test_create_bedrock_connector_role to OpenSearch permission role\n", + "{\"status\":\"OK\",\"message\":\"'ml_full_access' updated.\"}\n", + "----------\n", + "Step 3: Create connector in OpenSearch\n", + "{\"connector_id\":\"7p2gMI4BWbTmLN9FeY7x\"}\n", + "----------\n" + ] + } + ], + "source": [ + "connector_role_inline_policy = {\n", + " \"Version\": \"2012-10-17\",\n", + " \"Statement\": [\n", + " {\n", + " \"Action\": [\n", + " \"bedrock:InvokeModel\"\n", + " ],\n", + " \"Effect\": \"Allow\",\n", + " \"Resource\": \"arn:aws:bedrock:*::foundation-model/amazon.titan-embed-text-v1\"\n", + " }\n", + " ]\n", + "}\n", + "\n", + "# You can use existing role if the role permission and trust relationship are correct. \n", + "# To quick start, you can specify new role names. AIConnectorHelper will create role automatically.\n", + "connector_role_name = 'my_test_bedrock_connector_role'\n", + "create_connector_role_name = 'my_test_create_bedrock_connector_role'\n", + "\n", + "bedrock_region = 'us-west-2' # bedrock region could be different with OpenSearch domain region\n", + "create_connector_input = {\n", + " \"name\": \"Amazon Bedrock Connector: titan embedding v1\",\n", + " \"description\": \"The connector to bedrock Titan embedding model\",\n", + " \"version\": 1,\n", + " \"protocol\": \"aws_sigv4\",\n", + " \"parameters\": {\n", + " \"region\": bedrock_region,\n", + " \"service_name\": \"bedrock\"\n", + " },\n", + " \"actions\": [\n", + " {\n", + " \"action_type\": \"predict\",\n", + " \"method\": \"POST\",\n", + " \"url\": f\"https://bedrock-runtime.{bedrock_region}.amazonaws.com/model/amazon.titan-embed-text-v1/invoke\",\n", + " \"headers\": {\n", + " \"content-type\": \"application/json\",\n", + " \"x-amz-content-sha256\": \"required\"\n", + " },\n", + " \"request_body\": \"{ \\\"inputText\\\": \\\"${parameters.inputText}\\\" }\",\n", + " \"pre_process_function\": \"\\n StringBuilder builder = new StringBuilder();\\n builder.append(\\\"\\\\\\\"\\\");\\n String first = params.text_docs[0];\\n builder.append(first);\\n builder.append(\\\"\\\\\\\"\\\");\\n def parameters = \\\"{\\\" +\\\"\\\\\\\"inputText\\\\\\\":\\\" + builder + \\\"}\\\";\\n return \\\"{\\\" +\\\"\\\\\\\"parameters\\\\\\\":\\\" + parameters + \\\"}\\\";\",\n", + " \"post_process_function\": \"\\n def name = \\\"sentence_embedding\\\";\\n def dataType = \\\"FLOAT32\\\";\\n if (params.embedding == null || params.embedding.length == 0) {\\n return params.message;\\n }\\n def shape = [params.embedding.length];\\n def json = \\\"{\\\" +\\n \\\"\\\\\\\"name\\\\\\\":\\\\\\\"\\\" + name + \\\"\\\\\\\",\\\" +\\n \\\"\\\\\\\"data_type\\\\\\\":\\\\\\\"\\\" + dataType + \\\"\\\\\\\",\\\" +\\n \\\"\\\\\\\"shape\\\\\\\":\\\" + shape + \\\",\\\" +\\n \\\"\\\\\\\"data\\\\\\\":\\\" + params.embedding +\\n \\\"}\\\";\\n return json;\\n \"\n", + " }\n", + " ]\n", + "}\n", + "\n", + "connector_id = helper.create_connector_with_role(connector_role_inline_policy,\n", + " connector_role_name,\n", + " create_connector_role_name,\n", + " create_connector_input,\n", + " sleep_time_in_seconds=10)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "1508d510", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\"task_id\":\"ikqgMI4BOhavBOmffCmA\",\"status\":\"CREATED\",\"model_id\":\"i0qgMI4BOhavBOmffCma\"}\n" + ] + } + ], + "source": [ + "model_name = 'Bedrock embedding model'\n", + "description = 'This is my test model'\n", + "model_id = helper.create_model(model_name, description, connector_id)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "c4054d75", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'{\"inference_results\":[{\"output\":[{\"name\":\"sentence_embedding\",\"data_type\":\"FLOAT32\",\"shape\":[1536],\"data\":[0.7265625,-0.0703125,0.34765625,...]}],\"status_code\":200}]}'" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "payload = {\n", + " \"parameters\": {\n", + " \"inputText\": \"hello world\"\n", + " }\n", + "}\n", + "r=helper.predict(model_id, payload)\n", + "r" + ] + }, + { + "cell_type": "markdown", + "id": "43b2f509", + "metadata": {}, + "source": [ + "# 2. Create Connector of Sagemaker Embedding Model\n", + "\n", + "Read [semantic_search_with_sagemaker_embedding_model](./semantic_search_with_sagemaker_embedding_model.md) for more details." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "476d40b3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Step1: Create IAM role configued in connector\n", + "Created role: my_test_sagemaker_connector_role\n", + "----------\n", + "Step 2: Configure IAM role in OpenSearch\n", + "Step 2.1: Create IAM role for Signing create connector request\n", + "Created role: my_test_create_sagemaker_connector_role\n", + "----------\n", + "Step 2.2: Map IAM role my_test_create_sagemaker_connector_role to OpenSearch permission role\n", + "{\"status\":\"OK\",\"message\":\"'ml_full_access' updated.\"}\n", + "----------\n", + "Step 3: Create connector in OpenSearch\n", + "{\"connector_id\":\"jUqhMI4BOhavBOmfhCnz\"}\n", + "----------\n" + ] + } + ], + "source": [ + "sagemaker_inference_endpoint_arn = \"...\" # set your SageMaker inference endpoint ARN\n", + "connector_role_inline_policy = {\n", + " \"Version\": \"2012-10-17\",\n", + " \"Statement\": [\n", + " {\n", + " \"Effect\": \"Allow\",\n", + " \"Action\": [\n", + " \"sagemaker:InvokeEndpoint\"\n", + " ],\n", + " \"Resource\": [\n", + " sagemaker_inference_endpoint_arn\n", + " ]\n", + " }\n", + " ]\n", + "}\n", + "\n", + "# You can use existing role if the role permission and trust relationship are correct. \n", + "# To quick start, you can specify new role names. AIConnectorHelper will create role automatically.\n", + "connector_role_name = 'my_test_sagemaker_connector_role'\n", + "create_connector_role_name = 'my_test_create_sagemaker_connector_role'\n", + "sagemaker_inference_endpoint_url = '...' # set your SageMaker inference endpoint URL\n", + "\n", + "sagemaker_endpoint_region = 'us-west-2' # SageMaker endpoint region could be different with OpenSearch domain region\n", + "create_connector_input = {\n", + " \"name\": \"Sagemaker embedding model connector\",\n", + " \"description\": \"Connector for my Sagemaker embedding model\",\n", + " \"version\": \"1.0\",\n", + " \"protocol\": \"aws_sigv4\",\n", + " \"parameters\": {\n", + " \"region\": sagemaker_endpoint_region,\n", + " \"service_name\": \"sagemaker\"\n", + " },\n", + " \"actions\": [\n", + " {\n", + " \"action_type\": \"predict\",\n", + " \"method\": \"POST\",\n", + " \"headers\": {\n", + " \"content-type\": \"application/json\"\n", + " },\n", + " \"url\": sagemaker_inference_endpoint_url,\n", + " \"request_body\": \"${parameters.input}\",\n", + " \"pre_process_function\": \"connector.pre_process.default.embedding\",\n", + " \"post_process_function\": \"connector.post_process.default.embedding\"\n", + " }\n", + " ]\n", + "}\n", + "\n", + "connector_id = helper.create_connector_with_role(connector_role_inline_policy,\n", + " connector_role_name,\n", + " create_connector_role_name,\n", + " create_connector_input,\n", + " sleep_time_in_seconds=10)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "ab1e396d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\"task_id\":\"752hMI4BWbTmLN9FjY7M\",\"status\":\"CREATED\",\"model_id\":\"8J2hMI4BWbTmLN9FjY7k\"}\n" + ] + } + ], + "source": [ + "model_name = 'Sagemaker embedding model'\n", + "description = 'This is my test model'\n", + "model_id = helper.create_model(model_name, description, connector_id)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "076a26f9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'{\"inference_results\":[{\"output\":[{\"name\":\"sentence_embedding\",\"data_type\":\"FLOAT32\",\"shape\":[384],\"data\":[0.0067263525,0.034647945,0.051665697,0.045790758,...]},{\"name\":\"sentence_embedding\",\"data_type\":\"FLOAT32\",\"shape\":[384],\"data\":[0.17081982,-0.035508048,0.017017538,...]}],\"status_code\":200}]}'" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "payload = {\n", + " \"parameters\": {\n", + " \"input\": [\"hello world\", \"how are you\"]\n", + " }\n", + "}\n", + "r=helper.predict(model_id, payload)\n", + "r" + ] + }, + { + "cell_type": "markdown", + "id": "60635597", + "metadata": {}, + "source": [ + "# 3. Create Connector of Cohere Embedding Model with secret", + "\n", + "Read [semantic_search_with_cohere_embedding_model](./semantic_search_with_cohere_embedding_model.md) for more details." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "2502e650", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Step1: Create Secret\n", + "secret exists, skip creating\n", + "----------\n", + "Step2: Create IAM role configued in connector\n", + "Created role: my_test_cohere_connector_role\n", + "----------\n", + "Step 3: Configure IAM role in OpenSearch\n", + "Step 3.1: Create IAM role for Signing create connector request\n", + "Created role: my_test_create_cohere_connector_role\n", + "----------\n", + "Step 3.2: Map IAM role my_test_create_cohere_connector_role to OpenSearch permission role\n", + "{\"status\":\"OK\",\"message\":\"'ml_full_access' updated.\"}\n", + "----------\n", + "Step 4: Create connector in OpenSearch\n", + "{\"connector_id\":\"8p2iMI4BWbTmLN9Fi47_\"}\n", + "----------\n" + ] + } + ], + "source": [ + "secret_name = 'my_test_cohere_secret'\n", + "secret_key = 'my_cohere_key'\n", + "secret_value = '...' # set your Cohere API key\n", + "secret_value = { secret_key: secret_value }\n", + "# You can use existing role if the role permission and trust relationship are correct. \n", + "# To quick start, you can specify new role names. AIConnectorHelper will create role automatically.\n", + "connector_role_name = 'my_test_cohere_connector_role'\n", + "create_connector_role_name = 'my_test_create_cohere_connector_role'\n", + "\n", + "create_connector_input = {\n", + " \"name\": \"cohere-embed-v3\",\n", + " \"description\": \"The connector to public Cohere model service for embed\",\n", + " \"version\": \"1\",\n", + " \"protocol\": \"http\",\n", + " \"parameters\": {\n", + " \"model\": \"embed-english-v3.0\",\n", + " \"input_type\":\"search_document\",\n", + " \"truncate\": \"END\"\n", + " },\n", + " \"actions\": [\n", + " {\n", + " \"action_type\": \"predict\",\n", + " \"method\": \"POST\",\n", + " \"url\": \"https://api.cohere.ai/v1/embed\",\n", + " \"headers\": {\n", + " \"Authorization\": f\"Bearer ${{credential.secretArn.{secret_key}}}\",\n", + " \"Request-Source\": \"unspecified:opensearch\"\n", + " },\n", + " \"request_body\": \"{ \\\"texts\\\": ${parameters.texts}, \\\"truncate\\\": \\\"${parameters.truncate}\\\", \\\"model\\\": \\\"${parameters.model}\\\", \\\"input_type\\\": \\\"${parameters.input_type}\\\" }\",\n", + " \"pre_process_function\": \"connector.pre_process.cohere.embedding\",\n", + " \"post_process_function\": \"connector.post_process.cohere.embedding\"\n", + " }\n", + " ]\n", + "}\n", + "\n", + "connector_id = helper.create_connector_with_secret(secret_name,\n", + " secret_value,\n", + " connector_role_name,\n", + " create_connector_role_name, \n", + " create_connector_input,\n", + " sleep_time_in_seconds=10)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "720aa211", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\"task_id\":\"jkqiMI4BOhavBOmfjSnB\",\"status\":\"CREATED\",\"model_id\":\"j0qiMI4BOhavBOmfjSn1\"}\n" + ] + } + ], + "source": [ + "model_name = 'Cohere embedding model'\n", + "description = 'This is my test model'\n", + "model_id = helper.create_model(model_name, description, connector_id)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "19bc067d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'{\"inference_results\":[{\"output\":[{\"name\":\"sentence_embedding\",\"data_type\":\"FLOAT32\",\"shape\":[1024],\"data\":[-0.029510498,-0.023223877,-0.059631348,...]},{\"name\":\"sentence_embedding\",\"data_type\":\"FLOAT32\",\"shape\":[1024],\"data\":[0.02279663,0.014976501,-0.04058838,...]}],\"status_code\":200}]}'" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "payload = {\n", + " \"parameters\": {\n", + " \"texts\": [\"hello world\", \"how are you\"]\n", + " }\n", + "}\n", + "r=helper.predict(model_id, payload)\n", + "r" + ] + }, + { + "cell_type": "markdown", + "id": "e41bfec0", + "metadata": {}, + "source": [ + "# 4. Create Connector of OpenAI Embedding Model with secret", + "\n", + "Read [semantic_search_with_openai_embedding_model](./semantic_search_with_openai_embedding_model.md) for more details." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "6d67f844", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Step1: Create Secret\n", + "secret exists, skip creating\n", + "----------\n", + "Step2: Create IAM role configued in connector\n", + "Created role: my_test_openai_connector_role\n", + "----------\n", + "Step 3: Configure IAM role in OpenSearch\n", + "Step 3.1: Create IAM role for Signing create connector request\n", + "Created role: my_test_create_openai_connector_role\n", + "----------\n", + "Step 3.2: Map IAM role my_test_create_openai_connector_role to OpenSearch permission role\n", + "{\"status\":\"OK\",\"message\":\"'ml_full_access' updated.\"}\n", + "----------\n", + "Step 4: Create connector in OpenSearch\n", + "{\"connector_id\":\"cRWzMI4BTaDH9c7t0Rfd\"}\n", + "----------\n" + ] + } + ], + "source": [ + "secret_name = 'my_test_openai_secret'\n", + "secret_key = 'my_openai_key'\n", + "secret_value = '...' # set your OpenAI API key\n", + "secret_value = { secret_key : secret_value }\n", + "# You can use existing role if the role permission and trust relationship are correct. \n", + "# To quick start, you can specify new role names. AIConnectorHelper will create role automatically.\n", + "connector_role_name = 'my_test_openai_connector_role'\n", + "create_connector_role_name = 'my_test_create_openai_connector_role'\n", + "\n", + "create_connector_input = {\n", + " \"name\": \"OpenAI embedding model connector\",\n", + " \"description\": \"Connector for OpenAI embedding model\",\n", + " \"version\": \"1.0\",\n", + " \"protocol\": \"http\",\n", + " \"parameters\": {\n", + " \"model\": \"text-embedding-ada-002\"\n", + " },\n", + " \"actions\": [\n", + " {\n", + " \"action_type\": \"predict\",\n", + " \"method\": \"POST\",\n", + " \"url\": \"https://api.openai.com/v1/embeddings\",\n", + " \"headers\": {\n", + " \"Authorization\": f\"Bearer ${{credential.secretArn.{secret_key}}}\",\n", + " },\n", + " \"request_body\": \"{ \\\"input\\\": ${parameters.input}, \\\"model\\\": \\\"${parameters.model}\\\" }\",\n", + " \"pre_process_function\": \"connector.pre_process.openai.embedding\",\n", + " \"post_process_function\": \"connector.post_process.openai.embedding\"\n", + " }\n", + " ]\n", + "}\n", + "\n", + "connector_id = helper.create_connector_with_secret(secret_name,\n", + " secret_value, \n", + " connector_role_name, \n", + " create_connector_role_name, \n", + " create_connector_input,\n", + " sleep_time_in_seconds=10)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "8311c1e3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\"task_id\":\"kkqzMI4BOhavBOmf9im-\",\"status\":\"CREATED\",\"model_id\":\"k0qzMI4BOhavBOmf9inW\"}\n" + ] + } + ], + "source": [ + "model_name = 'OpenAI embedding model'\n", + "description = 'This is my test model'\n", + "model_id = helper.create_model(model_name, description, connector_id)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "48ac874e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'{\"inference_results\":[{\"output\":[{\"name\":\"sentence_embedding\",\"data_type\":\"FLOAT32\",\"shape\":[1536],\"data\":[-0.014903838,0.0013317588,-0.018488139,...]},{\"name\":\"sentence_embedding\",\"data_type\":\"FLOAT32\",\"shape\":[1536],\"data\":[-0.014025201,-0.006837123,-0.01223793,...]}],\"status_code\":200}]}'" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "payload = {\n", + " \"parameters\": {\n", + " \"input\": [\"hello world\", \"how are you\"]\n", + " }\n", + "}\n", + "r=helper.predict(model_id, payload)\n", + "r" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6a1bcdf1", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/tutorials/aws/semantic_search_with_bedrock_cohere_embedding_model.md b/docs/tutorials/aws/semantic_search_with_bedrock_cohere_embedding_model.md index 03353115ae..fcdfefcf56 100644 --- a/docs/tutorials/aws/semantic_search_with_bedrock_cohere_embedding_model.md +++ b/docs/tutorials/aws/semantic_search_with_bedrock_cohere_embedding_model.md @@ -2,6 +2,8 @@ > The easiest way for setting up embedding model on your Amazon OpenSearch cluster is using [AWS CloudFormation](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/cfn-template.html) +> Another easy way is using python, check [AIConnectorHelper](./AIConnectorHelper.ipynb) + > This tutorial explains detail steps if you want to configure everything manually. > Bedrock has [quota limit](https://docs.aws.amazon.com/bedrock/latest/userguide/quotas.html). You can purchase [Provisioned Throughput](https://docs.aws.amazon.com/bedrock/latest/userguide/prov-throughput.html) to increase quota limit. diff --git a/docs/tutorials/aws/semantic_search_with_bedrock_titan_embedding_model.md b/docs/tutorials/aws/semantic_search_with_bedrock_titan_embedding_model.md index 40d7b27eb5..8c9f44827d 100644 --- a/docs/tutorials/aws/semantic_search_with_bedrock_titan_embedding_model.md +++ b/docs/tutorials/aws/semantic_search_with_bedrock_titan_embedding_model.md @@ -2,6 +2,8 @@ > The easiest way for setting up embedding model on your Amazon OpenSearch cluster is using [AWS CloudFormation](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/cfn-template.html) +> Another easy way is using python, check [AIConnectorHelper](./AIConnectorHelper.ipynb) + > This tutorial explains detail steps if you want to configure everything manually. > Bedrock has [quota limit](https://docs.aws.amazon.com/bedrock/latest/userguide/quotas.html). You can purchase [Provisioned Throughput](https://docs.aws.amazon.com/bedrock/latest/userguide/prov-throughput.html) to increase quota limit. diff --git a/docs/tutorials/aws/semantic_search_with_cohere_embedding_model.md b/docs/tutorials/aws/semantic_search_with_cohere_embedding_model.md index d42f433ee5..2982d6aabd 100644 --- a/docs/tutorials/aws/semantic_search_with_cohere_embedding_model.md +++ b/docs/tutorials/aws/semantic_search_with_cohere_embedding_model.md @@ -2,6 +2,8 @@ > The easiest way for setting up embedding model on your Amazon OpenSearch cluster is using [AWS CloudFormation](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/cfn-template.html) +> Another easy way is using python, check [AIConnectorHelper](./AIConnectorHelper.ipynb) + > This tutorial explains detail steps if you want to configure everything manually. You can also connect to other service with similar way. This doc introduces how to build semantic search in Amazon managed OpenSearch with [Cohere embedding model](https://docs.cohere.com/reference/embed). diff --git a/docs/tutorials/aws/semantic_search_with_openai_embedding_model.md b/docs/tutorials/aws/semantic_search_with_openai_embedding_model.md index a4e10deb3d..ab1236cbe3 100644 --- a/docs/tutorials/aws/semantic_search_with_openai_embedding_model.md +++ b/docs/tutorials/aws/semantic_search_with_openai_embedding_model.md @@ -1,5 +1,7 @@ # Topic +> An easy way is using python, check [AIConnectorHelper](./AIConnectorHelper.ipynb) + This doc introduces how to build semantic search in Amazon managed OpenSearch with [OpenAI embedding model](https://platform.openai.com/docs/guides/embeddings). If you are not using Amazon OpenSearch, you can refer to [openai_connector_embedding_blueprint](https://github.com/opensearch-project/ml-commons/blob/2.x/docs/remote_inference_blueprints/openai_connector_embedding_blueprint.md) and [OpenSearch semantic search](https://opensearch.org/docs/latest/search-plugins/semantic-search/). diff --git a/docs/tutorials/aws/semantic_search_with_sagemaker_embedding_model.md b/docs/tutorials/aws/semantic_search_with_sagemaker_embedding_model.md index 3f86e2b087..0408576019 100644 --- a/docs/tutorials/aws/semantic_search_with_sagemaker_embedding_model.md +++ b/docs/tutorials/aws/semantic_search_with_sagemaker_embedding_model.md @@ -2,6 +2,8 @@ > The easiest way for setting up embedding model on your Amazon OpenSearch cluster is using [AWS CloudFormation](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/cfn-template.html) +> Another easy way is using python, check [AIConnectorHelper](./AIConnectorHelper.ipynb) + > This tutorial explains detail steps if you want to configure everything manually. This doc introduces how to build semantic search in Amazon managed OpenSearch with embedding model running on [Sagemaker](https://aws.amazon.com/sagemaker/). @@ -272,7 +274,7 @@ Sample output POST /_plugins/_ml/models/NxU9Qo0BTaDH9c7t1Bca/_predict { "parameters": { - "inputs": ["hello world", "how are you"] + "input": ["hello world", "how are you"] } } ```