Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial commit for RAG in py-ml #427

Open
wants to merge 42 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
dbbd4f5
Initial commit for RAG pipeline scripts
hmumtazz Nov 15, 2024
dc94feb
Added Licence Header and fixed .gitingore file
hmumtazz Nov 18, 2024
cba567c
Added comments to understand code
hmumtazz Nov 18, 2024
3481c3e
Remove sensitive config file
hmumtazz Nov 18, 2024
e6eee30
Simplify the selection process for the ef_construction parameter by o…
hmumtazz Nov 18, 2024
8b7aa0e
Allows Customer to register model via CLI, fixed embedding generation…
hmumtazz Nov 21, 2024
2635751
Sign-off on all previous work
hmumtazz Nov 21, 2024
88cf68b
Enhance RAG pipeline functionality and user experience
hmumtazz Nov 22, 2024
ad1ea97
Created Ingest Pipline for chunking.- Merge custom setup.py with the …
hmumtazz Nov 25, 2024
da0ce7c
Removed hard coded LLM model, allowed for Opensoure integration, user…
hmumtazz Nov 29, 2024
fc2ace8
Remove requirements.txt and setup.py from Git tracking
hmumtazz Nov 29, 2024
fe83b25
Update setup.py file
hmumtazz Nov 29, 2024
bea75e0
Remove .gitignore file from rag pipeline directory
hmumtazz Nov 29, 2024
f5b74ed
Organized Model registration into classes
hmumtazz Nov 29, 2024
da834d2
Remove base_model.py from rag pipeline
hmumtazz Nov 29, 2024
9c45e43
Licence header
hmumtazz Nov 29, 2024
a61a840
Updated user setup, Added Semantic search for Managed service using M…
hmumtazz Dec 2, 2024
8db5e9f
fixed failing test in AIConnector tests
hmumtazz Dec 2, 2024
aa42922
Update test_SecretsHelper
hmumtazz Dec 3, 2024
5984c19
fixed license header test_SageMakerModel.py
hmumtazz Dec 3, 2024
4503a55
fixed license header rag.py
hmumtazz Dec 3, 2024
b1b98ab
Added seperate class for embedding generation, removed nominee text p…
hmumtazz Dec 5, 2024
d7e4713
Correctly leveraged existing methods in AI connector class, without h…
hmumtazz Dec 5, 2024
c02fe1b
Removed duplicate method, and deleted unused method
hmumtazz Dec 5, 2024
d877bca
Fixed chunking pipeline, query was not generating due to mismatchd ve…
hmumtazz Dec 5, 2024
f0194ec
Remove config.ini from repository and add to .gitignore
hmumtazz Dec 5, 2024
d354b25
Update rag_setup.py to remove neural search line and serverless
hmumtazz Dec 5, 2024
1ca4221
Updated query.py to reflect search changes
hmumtazz Dec 5, 2024
8b97791
Missing innit file
hmumtazz Dec 5, 2024
fe6f8c5
missing init file
hmumtazz Dec 6, 2024
34f7fd4
Fixed domain ARN/domain name error, changed directory, for IAM, Secre…
hmumtazz Dec 6, 2024
39a1883
Updated AIConnectorHelper to use existing methods, like get task, and…
hmumtazz Dec 6, 2024
2dfa609
Updated License Headers
hmumtazz Dec 6, 2024
2c40ce0
Fixed UT, Ran Lint nox, fixed code and unused enviroment variable iss…
hmumtazz Dec 7, 2024
6a846ce
Deleted serverless.py from rag functionality as we are not using it
hmumtazz Dec 11, 2024
6f17670
deleted unused dependcies, and test.py for serverless
hmumtazz Dec 12, 2024
120169f
Fixed failing test
hmumtazz Dec 13, 2024
91c9af0
Fixed tests
hmumtazz Dec 13, 2024
82e69f5
Fixed tests
hmumtazz Dec 13, 2024
c0114c3
Addressed comments like making code less redudent, and combining methods
hmumtazz Jan 7, 2025
197a134
Addressed code comments, making code less redundent, and combining ex…
hmumtazz Jan 7, 2025
220e9ee
Fixed methods at UT's, adressed comments
hmumtazz Jan 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- updating listing file with three v2 sparse model - by @dhrubo-os ([#412](https://github.com/opensearch-project/opensearch-py-ml/pull/412))
- Update model upload history - opensearch-project/opensearch-neural-sparse-encoding-doc-v2-mini (v.1.0.0)(TORCH_SCRIPT) by @dhrubo-os ([#417](https://github.com/opensearch-project/opensearch-py-ml/pull/417))
- Update model upload history - opensearch-project/opensearch-neural-sparse-encoding-v2-distill (v.1.0.0)(TORCH_SCRIPT) by @dhrubo-os ([#419](https://github.com/opensearch-project/opensearch-py-ml/pull/419))
- Added RAG functionality into `opensearch-py-ml` by @hmumtazz in ([#427](https://github.com/opensearch-project/opensearch-py-ml/pull/427))

### Fixed
- Fix the wrong final zip file name in model_uploader workflow, now will name it by the upload_prefix alse.([#413](https://github.com/opensearch-project/opensearch-py-ml/pull/413/files))
Expand Down
472 changes: 472 additions & 0 deletions opensearch_py_ml/ml_commons/rag_pipeline/rag/AIConnectorHelper.py

Large diffs are not rendered by default.

220 changes: 220 additions & 0 deletions opensearch_py_ml/ml_commons/rag_pipeline/rag/IAMRoleHelper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
# SPDX-License-Identifier: Apache-2.0
# The OpenSearch Contributors require contributions made to
# this file be licensed under the Apache-2.0 license or a
# compatible open source license.
# Any modifications Copyright OpenSearch Contributors. See
# GitHub history for details.

# Licensed to Elasticsearch B.V. under one or more contributor
# license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright
# ownership. Elasticsearch B.V. licenses this file to you under
# the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

import boto3
import json
from botocore.exceptions import ClientError
import requests

class IAMRoleHelper:
def __init__(self, region, opensearch_domain_url=None, opensearch_domain_username=None,
opensearch_domain_password=None, aws_user_name=None, aws_role_name=None, opensearch_domain_arn=None):
self.region = region
self.opensearch_domain_url = opensearch_domain_url
self.opensearch_domain_username = opensearch_domain_username
self.opensearch_domain_password = opensearch_domain_password
self.aws_user_name = aws_user_name
self.aws_role_name = aws_role_name
self.opensearch_domain_arn = opensearch_domain_arn

def role_exists(self, role_name):
iam_client = boto3.client('iam')

try:
iam_client.get_role(RoleName=role_name)
return True
except ClientError as e:
if e.response['Error']['Code'] == 'NoSuchEntity':
return False
else:
print(f"An error occurred: {e}")
return False
hmumtazz marked this conversation as resolved.
Show resolved Hide resolved

def delete_role(self, role_name):
iam_client = boto3.client('iam')

try:
# Detach managed policies
policies = iam_client.list_attached_role_policies(RoleName=role_name)['AttachedPolicies']
for policy in policies:
iam_client.detach_role_policy(RoleName=role_name, PolicyArn=policy['PolicyArn'])
print(f'All managed policies detached from role {role_name}.')

# Delete inline policies
inline_policies = iam_client.list_role_policies(RoleName=role_name)['PolicyNames']
for policy_name in inline_policies:
iam_client.delete_role_policy(RoleName=role_name, PolicyName=policy_name)
print(f'All inline policies deleted from role {role_name}.')

# Now, delete the role
iam_client.delete_role(RoleName=role_name)
print(f'Role {role_name} deleted.')

except ClientError as e:
if e.response['Error']['Code'] == 'NoSuchEntity':
print(f'Role {role_name} does not exist.')
else:
print(f"An error occurred: {e}")

def create_iam_role(self, role_name, trust_policy_json, inline_policy_json):
iam_client = boto3.client('iam')

try:
# Create the role with the trust policy
create_role_response = iam_client.create_role(
RoleName=role_name,
AssumeRolePolicyDocument=json.dumps(trust_policy_json),
Description='Role with custom trust and inline policies',
)

# Get the ARN of the newly created role
role_arn = create_role_response['Role']['Arn']

# Attach the inline policy to the role
iam_client.put_role_policy(
RoleName=role_name,
PolicyName='InlinePolicy', # you can replace this with your preferred policy name
PolicyDocument=json.dumps(inline_policy_json)
)

print(f'Created role: {role_name}')
return role_arn

except ClientError as e:
print(f"Error creating the role: {e}")
return None

def get_role_arn(self, role_name):
if not role_name:
return None
iam_client = boto3.client('iam')
try:
response = iam_client.get_role(RoleName=role_name)
# Return ARN of the role
return response['Role']['Arn']
except ClientError as e:
if e.response['Error']['Code'] == 'NoSuchEntity':
print(f"The requested role {role_name} does not exist")
return None
else:
print(f"An error occurred: {e}")
return None

def get_role_details(self, role_name):
iam = boto3.client('iam')

try:
response = iam.get_role(RoleName=role_name)
role = response['Role']

print(f"Role Name: {role['RoleName']}")
print(f"Role ID: {role['RoleId']}")
print(f"ARN: {role['Arn']}")
print(f"Creation Date: {role['CreateDate']}")
print("Assume Role Policy Document:")
print(json.dumps(role['AssumeRolePolicyDocument'], indent=4, sort_keys=True))

list_role_policies_response = iam.list_role_policies(RoleName=role_name)

for policy_name in list_role_policies_response['PolicyNames']:
get_role_policy_response = iam.get_role_policy(RoleName=role_name, PolicyName=policy_name)
print(f"Role Policy Name: {get_role_policy_response['PolicyName']}")
print("Role Policy Document:")
print(json.dumps(get_role_policy_response['PolicyDocument'], indent=4, sort_keys=True))

except ClientError as e:
if e.response['Error']['Code'] == 'NoSuchEntity':
print(f'Role {role_name} does not exist.')
else:
print(f"An error occurred: {e}")

def get_user_arn(self, username):
if not username:
return None
iam_client = boto3.client('iam')

try:
response = iam_client.get_user(UserName=username)
user_arn = response['User']['Arn']
return user_arn
except ClientError as e:
if e.response['Error']['Code'] == 'NoSuchEntity':
print(f"IAM user '{username}' not found.")
return None
else:
print(f"An error occurred: {e}")
return None

def assume_role(self, role_arn, role_session_name="your_session_name"):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is session_name a variable that is not dire to creation? I notice it will default your_session_name, thats why i ask

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The role_session_name is needed to uniquely identify the session when assuming a role. Using a default like 'your_session_name' is just a placeholder. It might be better to make this a required input or generate a unique name automatically to avoid any confusion.

sts_client = boto3.client('sts')

try:
assumed_role_object = sts_client.assume_role(
RoleArn=role_arn,
RoleSessionName=role_session_name,
)

# Obtain the temporary credentials from the assumed role
temp_credentials = assumed_role_object["Credentials"]

return temp_credentials
except ClientError as e:
print(f"Error assuming role: {e}")
return None

def map_iam_role_to_backend_role(self, iam_role_arn):
os_security_role = 'ml_full_access' # Changed from 'all_access' to 'ml_full_access'
hmumtazz marked this conversation as resolved.
Show resolved Hide resolved
url = f'{self.opensearch_domain_url}/_plugins/_security/api/rolesmapping/{os_security_role}'

payload = {
"backend_roles": [iam_role_arn]
}
headers = {'Content-Type': 'application/json'}

try:
response = requests.put(
url,
auth=(self.opensearch_domain_username, self.opensearch_domain_password),
json=payload,
headers=headers,
verify=True
)

if response.status_code == 200:
print(f"Successfully mapped IAM role to OpenSearch role '{os_security_role}'.")
else:
print(f"Failed to map IAM role to OpenSearch role '{os_security_role}'. Status code: {response.status_code}")
print(f"Response: {response.text}")
except requests.exceptions.RequestException as e:
print(f"HTTP request failed: {e}")

def get_iam_user_name_from_arn(self, iam_principal_arn):
"""
Extract the IAM user name from the IAM principal ARN.
"""
# IAM user ARN format: arn:aws:iam::123456789012:user/user-name
if iam_principal_arn and ':user/' in iam_principal_arn:
return iam_principal_arn.split(':user/')[-1]
else:
return None
85 changes: 85 additions & 0 deletions opensearch_py_ml/ml_commons/rag_pipeline/rag/SecretsHelper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# SPDX-License-Identifier: Apache-2.0
# The OpenSearch Contributors require contributions made to
# this file be licensed under the Apache-2.0 license or a
# compatible open source license.
# Any modifications Copyright OpenSearch Contributors. See
# GitHub history for details.

# Licensed to Elasticsearch B.V. under one or more contributor
# license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright
# ownership. Elasticsearch B.V. licenses this file to you under
# the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

import logging
import boto3
import json
from botocore.exceptions import ClientError

logger = logging.getLogger(__name__)
hmumtazz marked this conversation as resolved.
Show resolved Hide resolved

class SecretHelper:
def __init__(self, region):
self.region = region

def secret_exists(self, secret_name):
secretsmanager = boto3.client('secretsmanager', region_name=self.region)
try:
secretsmanager.get_secret_value(SecretId=secret_name)
return True
except ClientError as e:
if e.response['Error']['Code'] == 'ResourceNotFoundException':

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a valid reason to log but instead of error maybe info or warning?

return False
else:
logger.error(f"An error occurred: {e}")
return False

def get_secret_arn(self, secret_name):
secretsmanager = boto3.client('secretsmanager', region_name=self.region)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: secretsmanager -> secrets_manager

try:
response = secretsmanager.describe_secret(SecretId=secret_name)
return response['ARN']
except ClientError as e:
if e.response['Error']['Code'] == 'ResourceNotFoundException':
logger.warning(f"The requested secret {secret_name} was not found")
return None
else:
logger.error(f"An error occurred: {e}")
return None

def get_secret(self, secret_name):
secretsmanager = boto3.client('secretsmanager', region_name=self.region)
try:
response = secretsmanager.get_secret_value(SecretId=secret_name)
return response.get('SecretString')
except ClientError as e:
if e.response['Error']['Code'] == 'ResourceNotFoundException':
logger.warning("The requested secret was not found")
return None
else:
logger.error(f"An error occurred: {e}")
return None

def create_secret(self, secret_name, secret_value):
secretsmanager = boto3.client('secretsmanager', region_name=self.region)
try:
response = secretsmanager.create_secret(
Name=secret_name,
SecretString=json.dumps(secret_value),
)
logger.info(f'Secret {secret_name} created successfully.')
return response['ARN']
except ClientError as e:
logger.error(f'Error creating secret: {e}')
return None
Loading
Loading