From 0ec57089b041a63037b0457508494862b9b221fa Mon Sep 17 00:00:00 2001 From: khushal Date: Thu, 5 Dec 2024 18:09:07 +0530 Subject: [PATCH] (20241205) Safety Push. --- api/blueprints/ai/__init__.py | 0 api/blueprints/ai/llm/__init__.py | 0 api/blueprints/ai/llm/invoke.py | 237 ++++++++++++++++++++ models/behaviour/ai/__init__.py | 0 models/behaviour/ai/llm/__init__.py | 0 models/behaviour/ai/llm/open_ai.py | 328 ++++++++++++++++++++++++++++ models/data/ai/__init__.py | 0 models/data/ai/llm.py | 196 +++++++++++++++++ 8 files changed, 761 insertions(+) create mode 100644 api/blueprints/ai/__init__.py create mode 100644 api/blueprints/ai/llm/__init__.py create mode 100644 api/blueprints/ai/llm/invoke.py create mode 100644 models/behaviour/ai/__init__.py create mode 100644 models/behaviour/ai/llm/__init__.py create mode 100644 models/behaviour/ai/llm/open_ai.py create mode 100644 models/data/ai/__init__.py create mode 100644 models/data/ai/llm.py diff --git a/api/blueprints/ai/__init__.py b/api/blueprints/ai/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/blueprints/ai/llm/__init__.py b/api/blueprints/ai/llm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/blueprints/ai/llm/invoke.py b/api/blueprints/ai/llm/invoke.py new file mode 100644 index 0000000..7693d40 --- /dev/null +++ b/api/blueprints/ai/llm/invoke.py @@ -0,0 +1,237 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Thursday, 5th Dec., 2024 + + OBJECTIVE: + + To receive auth details for various SMS client APIs. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + + NOTES: + + N/A + +""" + + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# For using Quart: +from quart import Blueprint, current_app, request + +# My utils: +from utils_v2.string import json +from utils_v2.api.codes import StatusCodes, HttpCodes +from utils_v2.api.response import ResponseModel +from utils_v2.api.async_quart import ( + set_api_version, + read_input, + get_session_info, + log_request_to_mongo, + log_chain_to_mongo, + should_not_be_under_maintenance, + only_whitelisted_ips, + limit_rate, + validate_input, + handle_cancelled_request +) + +# SMS-related utils: +from utils_v2.sms.nimbus.async_nimbus import AsyncNimbusSMS +from utils_v2.sms.savvy_bulk_sms.async_savvy_bulk_sms import AsyncSavvyBulkSMS + +# Common: +from shared import constants + +# Data Models: +from models.data.sms.auth import SMSAuthRequestHeaders, SMSAuthRequestData + +# For asynchronous activities: +import asyncio + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# Related to Quart: +sms_auth_bp = Blueprint("sms_auth", __name__) + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +@sms_auth_bp.record_once +def init(blueprint_setup_state): + + # This gets called when the blueprint is registered. + # Consider this to be a one-time setup for the whole blueprint: + pass + + +# --------------------------------------------------------------------------------------------------------------------- + + +@sms_auth_bp.route("/auth", methods = ["POST"]) +@set_api_version(api_version = "1.0.0") +@read_input(sanitize_headers = False, sanitize_data = False) +@get_session_info(key = "X-Session-Token", session_coro = "get_session") +@log_request_to_mongo( + attr_name = "logs_mongo", + project = constants.PROJECT_NAME, + log_type = constants.MODULE_NAME, + operation = "smsAuthApi", + log_input = True, + log_output = True, + sensitive_keys = ["sessionToken", "X-Session-Token"] +) +@log_chain_to_mongo(attr_name = "logs_mongo") +@should_not_be_under_maintenance(attr_name = "is_under_maintenance") +@validate_input( + header_validator = lambda x: SMSAuthRequestHeaders(**x).model_dump(), + data_validator = lambda x: SMSAuthRequestData(**x) +) +@handle_cancelled_request() +async def request_oauth_authorization_url( + inbound_headers: dict | SMSAuthRequestHeaders = None, + inbound_data: dict | SMSAuthRequestData = None, + inbound_files: dict = None, + **kwargs +): + + """ + Use this when a user wants to register a third-party SMS client with your service. + :param inbound_headers: auto-extracted by the decorators. + :param inbound_data: auto-extracted by the decorators. + :param inbound_files: auto-extracted by the decorators. + :param kwargs: Any number of extra inputs supplied by the decorators. + :return: A standard response structure. + """ + + # ┏┓ + # ┃┃┏┓┏┓┏┓┏┓┏┓┏┏┓┏┏ + # ┣┛┛ ┗ ┣┛┛ ┗┛┗┗ ┛┛ + # ┛ + + # If the session token is invalid/expired: + if kwargs.get("session_info") is None: + return ResponseModel( + status_code = StatusCodes.FAILED, + http_code = HttpCodes.UNAUTHORIZED + ) + + # Start by assuming failure: + token_id = None + + # ┏┓ ┳┓• ┓ ┏┓┳┳┓┏┓ ┳ ┓• + # ┣ ┏┓┏┓ ┃┃┓┏┳┓┣┓┓┏┏ ┗┓┃┃┃┗┓ ┃┏┓┏┫┓┏┓ + # ┻ ┗┛┛ ┛┗┗┛┗┗┗┛┗┻┛ ┗┛┛ ┗┗┛ ┻┛┗┗┻┗┗┻ + + if inbound_data.messageClient == "nimbusSmsIndia": + + token_id = await current_app.sms_auth_model.set( + db_conn = current_app.sql_writer, + mongo_conn = current_app.data_mongo, + user_info = kwargs["session_info"], + client_user_id = { + "userId": inbound_data.auth.userId, + "senderId": inbound_data.auth.senderId, + "entityId": inbound_data.auth.entityId + }, + auth = inbound_data.auth.model_dump(), + token = None, + service_client = inbound_data.messageClient, + auth_type = "auth", + sync_freq = 300, + session_token = inbound_headers["X-Session-Token"] + ) + + # ┏┓ ┏┓ ┳┓ ┓┓ ┏┓┳┳┓┏┓ ┓┏┓ + # ┣ ┏┓┏┓ ┗┓┏┓┓┏┓┏┓┏ ┣┫┓┏┃┃┏ ┗┓┃┃┃┗┓ ┃┫ ┏┓┏┓┓┏┏┓ + # ┻ ┗┛┛ ┗┛┗┻┗┛┗┛┗┫ ┻┛┗┻┗┛┗ ┗┛┛ ┗┗┛ ┛┗┛┗ ┛┗┗┫┗┻ + # ┛ ┛ + + if inbound_data.messageClient == "savvyBulkSmsKenya": + + token_id = await current_app.sms_auth_model.set( + db_conn = current_app.sql_writer, + mongo_conn = current_app.data_mongo, + user_info = kwargs["session_info"], + client_user_id = { + "partnerId": inbound_data.auth.partnerId, + "shortCode": inbound_data.auth.shortCode + }, + auth = inbound_data.auth.model_dump(), + token = None, + service_client = inbound_data.messageClient, + auth_type = "auth", + sync_freq = 300, + session_token = inbound_headers["X-Session-Token"] + ) + + # ┳┓ + # ┣┫┏┓┏┏┓┏┓┏┓┏┏┓ + # ┛┗┗ ┛┣┛┗┛┛┗┛┗ + # ┛ + + # Done here: + return ResponseModel( + status_code = StatusCodes.OK if token_id else StatusCodes.FAILED, + http_code = HttpCodes.SUCCESS if token_id else HttpCodes.INTERNAL_SERVER_ERROR, + data = { + "messageClient": inbound_data.messageClient, + "authorized": True + } + ) + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/models/behaviour/ai/__init__.py b/models/behaviour/ai/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/models/behaviour/ai/llm/__init__.py b/models/behaviour/ai/llm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/models/behaviour/ai/llm/open_ai.py b/models/behaviour/ai/llm/open_ai.py new file mode 100644 index 0000000..8216df9 --- /dev/null +++ b/models/behaviour/ai/llm/open_ai.py @@ -0,0 +1,328 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Thursday, 5th Dec., 2024 + + OBJECTIVE: + + To c + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + +""" + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# My async utils: +from utils_v2.string import json +from utils_v2.date_time import date_time +from utils_v2.database.async_mysql_v2 import AsyncMySQL +from utils_v2.database.async_mongo_v2 import AsyncMongo + +# Base model: +from models.behaviour.base import BaseModel + +# To work with MongoDB: +from bson import ObjectId + +# To work with datatypes: +from typing import Literal + +# To make deep-copies: +import copy + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** CLASSES *** +# ***** **** +# ***************************************************************************************************************** + + +class MailOAuthModel(BaseModel): + + AUTH_COLLECTION = "_authTokens" + + async def get_token_id( + self, + db_conn: AsyncMySQL, + mongo_conn: AsyncMongo, + user_info: dict, + client_user_id: dict, + auth: dict, + service_client: Literal["gmail"], + auth_type: Literal["oauth"], + sync_freq: Literal[60, 300, 900] = 300, + session_token: str = None + ) -> ObjectId: + + """ + Stores params from the session info and gives an identifier to use in the authorization URL. Use this when the + user requests an authorization URL to link your service to another service (like GMail). + :param db_conn: The database connection (MariaDB) to use to perform the action. + :param mongo_conn: The database connection (MongoDB) to use to perform the action. + :param user_info: The dictionary that has the user's session information. + :param client_user_id: The way the third-party client recognizes your user. + :param auth: The authentication details of the account. + :param service_client: The name of the company or brand that is providing this service that is being integrated. + :param auth_type: To identify the type of authentication being done here. This could indicate simple password + authentication, more advance OAuth2.0 authentication, etc. + :param sync_freq: The time interval in which mails need to be sync'd. Specify this in seconds. + :param session_token: The session token of the user who requested this service. + :return: An ObjectId to later store the granted tokens. + """ + + # Note down the timestamp at which this event occurred: + request_ts = date_time.get_current_utc_date_time(as_string = False) + + # Get the identifier from the database: + mongo_json = await mongo_conn.find_one_and_update( + collection = MailOAuthModel.AUTH_COLLECTION, + filter = mongo_conn.dict_to_dot_notation({ + "serviceType": "email", + "user": { + "entityId": user_info["entityId"], + "billingAccountId": user_info["billingAccountId"] + }, + "clientUserId": client_user_id + }), + update = { + "$set": { + "lastRequestTs": request_ts, + "status": "active", + "syncFreq": max(sync_freq, 60) + }, + "$setOnInsert": { + "version": "1.1.1", + "serviceType": "email", + "client": service_client, + "authType": auth_type, + "user": user_info, + "clientUserId": client_user_id, + "auth": auth, + "token": None, + "firstRefreshTs": None, + "lastRefreshTs": None, + "firstRequestTs": request_ts, + } + }, + projection = { + "_id": True + }, + upsert = True, + return_updated = True + ) + + # Tell MariaDB that an authorization request was initiated: + db_json = {} + if mongo_json is not None: + db_json = await self.call_procedure( + db_conn = db_conn, + proc_name = "entity_integration_save", + proc_args = ( + user_info["entityId"], # ............................................ 'p_entity_id' + service_client, # ................................................... 'p_provider' + "Pending", # ........................................................ 'p_current_status' + "Auth Requested", # ................................................. 'p_last_action' + None, # ............................................................. 'p_display_name' + None, # ............................................................. 'p_display_picture' + str(mongo_json["_id"]), # ........................................... 'p_token_id' + json.to_string(python_data = {"email": None}, no_space = True), # ... 'p_notes' + user_info["userId"] # ............................................... 'p_created_by' + ), + session_token = session_token + ) + + # Done here: + return mongo_json["_id"] if mongo_json and db_json.get("status") == 1 else None + + async def set_token( + self, + db_conn: AsyncMySQL, + mongo_conn: AsyncMongo, + token_id: ObjectId | str, + client_user_id: dict, + token: dict, + session_token: str = None + ) -> bool: + + """ + This method is to be called when the end user authorizes your service to connect to his third-party account. For + example, when the end user allows you to access his GMail account. USE THIS FOR UPDATING (REFRESHING) TOKENS + ALSO. + :param db_conn: The database connection (MariaDB) to use to perform the action. + :param mongo_conn: The database connection (MongoDB) to use to perform the action. + :param token_id: The identifier granted by the 'get_token_id' method. + :param client_user_id: The way the third-party client recognizes your user. These details should match the + details furnished while requesting the authorization through 'get_token_id' method. + :param token: The token granted by the third-party service. + :param session_token: The session token of the user who requested this service. + :return: True if saved, False if failed. + """ + + # Start by assuming failure: + token_saved = False + + # Note down the timestamp at which this event occurred: + request_ts = date_time.get_current_utc_date_time(as_string = False) + + # Save the token to MongoDB: + mongo_json = await mongo_conn.find_one_and_update( + collection = MailOAuthModel.AUTH_COLLECTION, + filter = mongo_conn.dict_to_dot_notation({ + "_id": ObjectId(token_id), + "clientUserId": client_user_id + }), + update = [{ + "$set": { + "token": token, + "status": "active", + "lastRefreshTs": request_ts, + "firstRefreshTs": { + "$cond": { + "if": { + "$or": [ + {"$eq": ["$firstRefreshTs", None]}, + {"$eq": [{"$type": "$firstRefreshTs"}, "missing"]} + ] + }, + "then": request_ts, + "else": "$firstRefreshTs" + } + } + } + }], + projection = {"token": False}, + return_updated = True, + upsert = False + ) + + # Tell MariaDB that the token was saved: + if mongo_json is not None: + token_notes = { + "email": token["email"], + "displayName": token.get("displayName"), + "displayPictureUrl": token.get("displayPictureUrl"), + } + db_json = await self.call_procedure( + db_conn = db_conn, + proc_name = "entity_integration_save", + proc_args = ( + mongo_json["user"]["entityId"], # ............................... 'p_entity_id' + mongo_json["client"], # ......................................... 'p_provider' + "Active", # ..................................................... 'p_current_status' + "Auth Granted", # ............................................... 'p_last_action' + token["displayName"], # ......................................... 'p_display_name' + token["displayPictureUrl"], # ................................... 'p_display_picture' + token_id, # ..................................................... 'p_token_id' + json.to_string(python_data = token_notes, no_space = True), # ... 'p_notes' + mongo_json["user"]["userId"] # .................................. 'p_created_by' + ), + session_token = session_token + ) + if db_json["status"] == 1: token_saved = True + + # Done here: + return token_saved + + async def get_token( + self, + mongo_conn: AsyncMongo, + token_id: ObjectId | str = None, + **kwargs + ) -> dict | None: + + """ + To retrieve stored tokens from the database. + :param mongo_conn: The database connection (MongoDB) to use to perform the action. + :param token_id: The identifier granted by the 'get_token_id' method. + :param kwargs: Any set of key-value pairs to build custom search criteria. This could be things like the user + info, the client, the type of authentication used, or even the kind of service. + :return: The retrieved record that has the token, and information about the service and client if found, else + None when there is no matching record. + """ + + # Build the filter: + filter_json = {k: v for k, v in kwargs.items()} + if token_id: filter_json["_id"] = ObjectId(token_id) + + # If there is no search criteria, we exit with failure: + if not filter_json: return None + + # If there is some filtering possible, + # we fetch and return the token: + return await mongo_conn.find_one( + collection = self.AUTH_COLLECTION, + filter = filter_json, + projection = { + "_id": True, + "serviceType": True, + "authType": True, + "client": True, + "clientUserId": True, + "token": True + } + ) + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/models/data/ai/__init__.py b/models/data/ai/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/models/data/ai/llm.py b/models/data/ai/llm.py new file mode 100644 index 0000000..b7d0faf --- /dev/null +++ b/models/data/ai/llm.py @@ -0,0 +1,196 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Thursday, 5th Dec., 2024. + + OBJECTIVE: + + To provide a structure to receive auth details of various SMS providers. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + +""" + + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# For making data behaviour_models: +from pydantic import BaseModel, Field, field_validator, PastDatetime +from typing import Optional, Literal, Union + +# My utils: +from utils_v2.string import regex +from utils_v2.date_time import date_time + +# To work with date and time: +import datetime + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# RegEx Patterns: +REGEX_SESSION_TOKEN = r"^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$" + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +class NimbusSMSIndiaAuth(BaseModel): + + entityId: str = Field( + description = "the entity id as registered with DLT", + min_length = 1, + frozen = True + ) + + senderId: str = Field( + description = "the 6-char code that you see in your SMS inbox", + min_length = 1, + frozen = True, + examples = ["HDFCBK", "NSESMS", "ZRODHA"] + ) + + userId: str = Field( + description = "the 6-digit id that Nimbus has assigned to you", + min_length = 1, + frozen = True + ) + + apiKey: str = Field( + description = "the key generated through Nimbus's portal", + min_length = 1, + frozen = True + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + +# --------------------------------------------------------------------------------------------------------------------- + + +class SavvyBulkSMSKenyaAuth(BaseModel): + + apiKey: str = Field( + description = "the key generated through Savvy's portal", + min_length = 1, + frozen = True + ) + + partnerId: str = Field( + description = "the key generated through Savvy's portal", + min_length = 1, + frozen = True + ) + + shortCode: str = Field( + description = "your short code with Savvy", + min_length = 1, + frozen = True + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + +# --------------------------------------------------------------------------------------------------------------------- + + +class SMSAuthRequestHeaders(BaseModel): + + sessionToken: str = Field( + description = "the session token of the user who is requesting the service", + pattern = REGEX_SESSION_TOKEN, + frozen = True, + alias = "X-Session-Token" + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "allow" + + def model_dump(self, *args, **kwargs): + return super().model_dump(*args, by_alias = True, **kwargs) + + +# --------------------------------------------------------------------------------------------------------------------- + + +class SMSAuthRequestData(BaseModel): + + messageClient: Literal["nimbusSmsIndia", "savvyBulkSmsKenya"] + auth: Union[NimbusSMSIndiaAuth, SavvyBulkSMSKenyaAuth] + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass