diff --git a/functions/chain.py b/functions/chain.py index c3db92b..5c3ef83 100644 --- a/functions/chain.py +++ b/functions/chain.py @@ -1,11 +1,123 @@ +# def create_health_ai_chain(llm, vector_store): +# retriever = SelfQueryRetriever.from_llm( +# llm=llm, +# vectorstore=vector_store, +# document_content_description=document_content_description, +# metadata_field_info=metadata_field_info, +# document_contents='', +# ) +# health_ai_template = """ +# You are a health AI agent equipped with access to diverse sources of health data, +# including research articles, nutritional information, medical archives, and more. +# Your task is to provide informed answers to user queries based on the available data. +# If you cannot find relevant information, simply state that you do not have enough data +# to answer accurately. write your response in markdown form and also add reference url +# so user can know from which source you are answering the questions. +# CONTEXT: +# {context} +# QUESTION: {question} +# YOUR ANSWER: +# """ +# health_ai_prompt = ChatPromptTemplate.from_template(health_ai_template) +# chain = ( +# {'context': retriever, 'question': RunnablePassthrough()} +# | health_ai_prompt +# | llm +# | StrOutputParser() +# ) +# return chain +import logging +from os import environ + +from get_google_docs import get_inital_prompt +from langchain.chains import create_history_aware_retriever, create_retrieval_chain +from langchain.chains.combine_documents import create_stuff_documents_chain from langchain.retrievers.self_query.base import SelfQueryRetriever -from langchain_core.output_parsers import StrOutputParser -from langchain_core.prompts import ChatPromptTemplate -from langchain_core.runnables import RunnablePassthrough +from langchain_anthropic import ChatAnthropic + +# separated files +from langchain_core.messages import AIMessage, HumanMessage +from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder +from langchain_google_genai import ChatGoogleGenerativeAI +from langchain_openai import OpenAI from meta import document_content_description, metadata_field_info +from store import get_vector_store + + +def custom_history(entire_history: list, llm_name: str): + if not entire_history or not isinstance(entire_history, list): + logging.error("Invalid 'entire_history': Must be a non-empty list.") + return [] + + chat_history = [] + for msg in entire_history: + if not isinstance(msg, dict): + logging.warning('Skipping invalid message format: Not a dictionary.') + continue + + msg_type = msg.get('type') + message_content = msg.get('message') + if msg_type == 'USER': + if message_content is None: + continue + chat_history.append(HumanMessage(content=message_content)) + elif msg_type == 'AI': + if not isinstance(message_content, dict) or llm_name not in message_content: + continue + chat_history.append(AIMessage(content=message_content[llm_name])) + else: + logging.warning(f'Skipping message with unrecognized type: {msg_type}.') -def create_health_ai_chain(llm, vector_store): + return chat_history + + +def hist_aware_answers(llm_name, input_string, message_history): + vector_store = get_vector_store() + get_init_answer = get_inital_prompt() + init_prompt = '' if get_init_answer is None else get_init_answer + contextualize_q_system_prompt = """Given a chat history and the latest user question \ + which might reference context in the chat history, formulate a standalone question \ + which can be understood without the chat history. Do NOT answer the question, \ + just reformulate it if needed and otherwise return it as is.""" + # add in custom user info: ----------------------------- + # custom_istructions = get_custom_instructions_callable() + # user_info = " " + # if custom_istructions: + # user_info = f"""Here is some information about the user, including the user's name, + # their profile description and style instructions on how they want you to answer stylewise: + # User Name: {custom_istructions['name']} + # Style Instrctions: {custom_istructions['styleInstructions']} + # Personal Info: {custom_istructions['personalInstructions']} + # """ + + agent_str = """ + You are a health AI agent equipped with access to diverse sources of health data, + including research articles, nutritional information, medical archives, and more. + Your task is to provide informed answers to user queries based on the available data. + If you cannot find relevant information, simply state that you do not have enough data + to answer accurately. write your response in markdown form and also add reference url + so user can know from which source you are answering the questions. + """ + + context_str = """ + CONTEXT: + {context} + + """ + # health_ai_template = f'{init_p,rompt}{agent_str}{user_info}{context_str}' + health_ai_template = f'{init_prompt}{agent_str}{context_str}' + chat_history = custom_history(message_history, llm_name) + if llm_name == 'gpt-4': + llm = OpenAI(temperature=0.2, api_key=environ.get('OPENAI_API_KEY')) + elif llm_name == 'gemini': + llm = ChatGoogleGenerativeAI( + model='gemini-1.5-pro-latest', google_api_key=environ.get('GOOGLE_API_KEY') + ) + elif llm_name == 'claude': + llm = ChatAnthropic( + model='claude-3-5-sonnet-20240620', api_key=environ.get('ANTHROPIC_API_KEY') + ) retriever = SelfQueryRetriever.from_llm( llm=llm, vectorstore=vector_store, @@ -13,26 +125,18 @@ def create_health_ai_chain(llm, vector_store): metadata_field_info=metadata_field_info, document_contents='', ) - health_ai_template = """ - You are a health AI agent equipped with access to diverse sources of health data, - including research articles, nutritional information, medical archives, and more. - Your task is to provide informed answers to user queries based on the available data. - If you cannot find relevant information, simply state that you do not have enough data - to answer accurately. write your response in markdown form and also add reference url - so user can know from which source you are answering the questions. - - CONTEXT: - {context} - - QUESTION: {question} - - YOUR ANSWER: - """ - health_ai_prompt = ChatPromptTemplate.from_template(health_ai_template) - chain = ( - {'context': retriever, 'question': RunnablePassthrough()} - | health_ai_prompt - | llm - | StrOutputParser() + contextualize_q_prompt = ChatPromptTemplate.from_messages( + [ + ('system', contextualize_q_system_prompt), + MessagesPlaceholder('chat_history'), + ('human', '{input}'), + ] + ) + history_aware_retriever = create_history_aware_retriever(llm, retriever, contextualize_q_prompt) + qa_prompt = ChatPromptTemplate.from_messages( + [('system', health_ai_template), MessagesPlaceholder('chat_history'), ('human', '{input}')] ) - return chain + question_answer_chain = create_stuff_documents_chain(llm, qa_prompt) + rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain) + msg = rag_chain.invoke({'input': input_string, 'chat_history': chat_history}) + return msg['answer'] diff --git a/functions/get_google_docs.py b/functions/get_google_docs.py new file mode 100644 index 0000000..692b575 --- /dev/null +++ b/functions/get_google_docs.py @@ -0,0 +1,81 @@ +import io +import os +import pickle +import re + +from google.auth.transport.requests import Request +from google_auth_oauthlib.flow import InstalledAppFlow +from googleapiclient.discovery import build +from googleapiclient.http import MediaIoBaseDownload + + +def extract_document_id_from_url(url): + pattern = r'/d/([a-zA-Z0-9-_]+)' + matches = re.findall(pattern, url) + document_id = max(matches, key=len) + return document_id + + +def authenticate(credentials, scopes): + """Obtaining auth with needed apis""" + creds = None + # The file token.pickle stores the user's access + # and refresh tokens, and is created automatically + # when the authorization flow completes for the first time. + if os.path.exists('token.pickle'): + with open('token.pickle', 'rb') as token: + creds = pickle.load(token) + # If there are no (valid) credentials available, let the user log in. + if not creds or not creds.valid: + if creds and creds.expired and creds.refresh_token: + creds.refresh(Request()) + else: + flow = InstalledAppFlow.from_client_secrets_file(credentials, scopes) + creds = flow.run_local_server(port=0) + # Save the credentials for the next run + with open('token.pickle', 'wb') as token: + pickle.dump(creds, token) + + return creds + + +def download_file(file_id, credentials_path): + scopes = ['https://www.googleapis.com/auth/drive.readonly'] + credentials = authenticate(credentials_path, scopes) + drive_service = build('drive', 'v3', credentials=credentials) + + # Export the Google Docs file as plain text + export_mime_type = 'text/plain' + request = drive_service.files().export_media(fileId=file_id, mimeType=export_mime_type) + + # Use a BytesIO buffer to handle the file content in memory + fh = io.BytesIO() + downloader = MediaIoBaseDownload(fh, request) + done = False + while not done: + status, done = downloader.next_chunk() + print(f'Download {int(status.progress() * 100)}%.') + + # Reset the buffer's position to the beginning + fh.seek(0) + + # Read the content of the buffer + content = fh.read().decode('utf-8') + + return content + + +def get_inital_prompt(): + # Example usage + document_id = extract_document_id_from_url( + 'https://docs.google.com/document/d/1GtLyBqhk-cu8CSo4A15WTgGDbMbL4B9LLjdvBoU3234/edit' + ) + # print("Document id: ", document_id) + credentials_json = 'credentials.json' + + try: + content = download_file(document_id, credentials_json) + return content + except Exception as e: + print(f'An error occurred: {e}') + return None diff --git a/functions/main.py b/functions/main.py index 5364366..5ebc1cc 100644 --- a/functions/main.py +++ b/functions/main.py @@ -1,26 +1,27 @@ from json import dumps +# from handlers import get_response_from_llm +from chain import hist_aware_answers from firebase_functions import https_fn, options -from handlers import get_response_from_llm -@https_fn.on_request(cors=options.CorsOptions(cors_origins=['*'])) +@https_fn.on_request(memory=options.MemoryOption.GB_32, cpu=8, timeout_sec=540) def get_response_url(req: https_fn.Request) -> https_fn.Response: query = req.get_json().get('query', '') - llms = req.get_json().get('llms', ['gpt-4']) + llms = req.get_json().get('llms', ['gpt-4', 'gemini', 'claude']) + chat = req.get_json().get('history', []) responses = {} for llm in llms: - response = get_response_from_llm(query, llm) - responses[llm] = response + responses[llm] = hist_aware_answers(llm, query, chat) return https_fn.Response(dumps(responses), mimetype='application/json') -@https_fn.on_call() +@https_fn.on_call(memory=options.MemoryOption.GB_32, cpu=8, timeout_sec=540) def get_response(req: https_fn.CallableRequest): query = req.data.get('query', '') - llms = req.data.get('llms', ['gpt-4']) + llms = req.get_json().get('llms', ['gpt-4', 'gemini', 'claude']) + chat = req.get_json().get('history', []) responses = {} for llm in llms: - response = get_response_from_llm(query, llm) - responses[llm] = response + responses[llm] = hist_aware_answers(llm, query, chat) return responses diff --git a/functions/requirements.txt b/functions/requirements.txt index 734752b..0fd90e7 100644 --- a/functions/requirements.txt +++ b/functions/requirements.txt @@ -4,3 +4,10 @@ langchain-community langchain-openai langchain-astradb lark +langchain_core +langchain_google_genai +langchain_anthropic +google-auth +google-auth-oauthlib +google-api-python-client +python-dotenv diff --git a/pdm.lock b/pdm.lock index 65ab47e..805a86d 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "linting"] strategy = ["cross_platform", "inherit_metadata"] lock_version = "4.4.1" -content_hash = "sha256:a8bf22369882d92f4174597140e0d6fa436f02f9752bdf13dcbbb737f63288c6" +content_hash = "sha256:4ceec97c0d7ffa9814d0c0aa48f7680c10ac5025fa8e4f74b250c850bd9002f8" [[package]] name = "aiohttp" @@ -76,6 +76,27 @@ files = [ {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] +[[package]] +name = "anthropic" +version = "0.31.0" +requires_python = ">=3.7" +summary = "The official Python library for the anthropic API" +groups = ["default"] +dependencies = [ + "anyio<5,>=3.5.0", + "distro<2,>=1.7.0", + "httpx<1,>=0.23.0", + "jiter<1,>=0.4.0", + "pydantic<3,>=1.9.0", + "sniffio", + "tokenizers>=0.13.0", + "typing-extensions<5,>=4.7", +] +files = [ + {file = "anthropic-0.31.0-py3-none-any.whl", hash = "sha256:77f7a428e1723b79a571c2427ae885d9257c68f700726dff7ce684c3972d6598"}, + {file = "anthropic-0.31.0.tar.gz", hash = "sha256:14f8f65231859a65ec9aacbbea070e412c8bd41a5d3f36793dac4292c3c78d3a"}, +] + [[package]] name = "anyio" version = "4.4.0" @@ -690,6 +711,17 @@ files = [ {file = "dataclasses_json-0.6.6.tar.gz", hash = "sha256:0c09827d26fffda27f1be2fed7a7a01a29c5ddcd2eb6393ad5ebf9d77e9deae8"}, ] +[[package]] +name = "defusedxml" +version = "0.7.1" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +summary = "XML bomb protection for Python stdlib modules" +groups = ["default"] +files = [ + {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, + {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, +] + [[package]] name = "deprecated" version = "1.2.14" @@ -1081,7 +1113,7 @@ files = [ [[package]] name = "google-api-python-client" -version = "2.136.0" +version = "2.137.0" requires_python = ">=3.7" summary = "Google API Client Library for Python" groups = ["default"] @@ -1093,13 +1125,13 @@ dependencies = [ "uritemplate<5,>=3.0.1", ] files = [ - {file = "google-api-python-client-2.136.0.tar.gz", hash = "sha256:161c722c8864e7ed39393e2b7eea76ef4e1c933a6a59f9d7c70409b6635f225d"}, - {file = "google_api_python_client-2.136.0-py2.py3-none-any.whl", hash = "sha256:5a554c8b5edf0a609b905d89d7ced82e8f6ac31da1e4d8d5684ef63dbc0e49f5"}, + {file = "google_api_python_client-2.137.0-py2.py3-none-any.whl", hash = "sha256:a8b5c5724885e5be9f5368739aa0ccf416627da4ebd914b410a090c18f84d692"}, + {file = "google_api_python_client-2.137.0.tar.gz", hash = "sha256:e739cb74aac8258b1886cb853b0722d47c81fe07ad649d7f2206f06530513c04"}, ] [[package]] name = "google-auth" -version = "2.31.0" +version = "2.32.0" requires_python = ">=3.7" summary = "Google Authentication Library" groups = ["default"] @@ -1109,8 +1141,8 @@ dependencies = [ "rsa<5,>=3.1.4", ] files = [ - {file = "google-auth-2.31.0.tar.gz", hash = "sha256:87805c36970047247c8afe614d4e3af8eceafc1ebba0c679fe75ddd1d575e871"}, - {file = "google_auth-2.31.0-py2.py3-none-any.whl", hash = "sha256:042c4702efa9f7d3c48d3a69341c209381b125faa6dbf3ebe56bc7e40ae05c23"}, + {file = "google_auth-2.32.0-py2.py3-none-any.whl", hash = "sha256:53326ea2ebec768070a94bee4e1b9194c9646ea0c2bd72422785bd0f9abfad7b"}, + {file = "google_auth-2.32.0.tar.gz", hash = "sha256:49315be72c55a6a37d62819e3573f6b416aca00721f7e3e31a008d928bf64022"}, ] [[package]] @@ -1129,7 +1161,7 @@ files = [ [[package]] name = "google-auth-oauthlib" -version = "1.2.0" +version = "1.2.1" requires_python = ">=3.6" summary = "Google Authentication Library" groups = ["default"] @@ -1138,8 +1170,8 @@ dependencies = [ "requests-oauthlib>=0.7.0", ] files = [ - {file = "google-auth-oauthlib-1.2.0.tar.gz", hash = "sha256:292d2d3783349f2b0734a0a0207b1e1e322ac193c2c09d8f7c613fb7cc501ea8"}, - {file = "google_auth_oauthlib-1.2.0-py2.py3-none-any.whl", hash = "sha256:297c1ce4cb13a99b5834c74a1fe03252e1e499716718b190f56bcb9c4abc4faf"}, + {file = "google_auth_oauthlib-1.2.1-py2.py3-none-any.whl", hash = "sha256:2d58a27262d55aa1b87678c3ba7142a080098cbc2024f903c62355deb235d91f"}, + {file = "google_auth_oauthlib-1.2.1.tar.gz", hash = "sha256:afd0cad092a2eaa53cd8e8298557d6de1034c6cb4a740500b5357b648af97263"}, ] [[package]] @@ -1482,6 +1514,28 @@ files = [ {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, ] +[[package]] +name = "jiter" +version = "0.5.0" +requires_python = ">=3.8" +summary = "Fast iterable JSON parser." +groups = ["default"] +files = [ + {file = "jiter-0.5.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b599f4e89b3def9a94091e6ee52e1d7ad7bc33e238ebb9c4c63f211d74822c3f"}, + {file = "jiter-0.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a063f71c4b06225543dddadbe09d203dc0c95ba352d8b85f1221173480a71d5"}, + {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc0d5b8b3dd12e91dd184b87273f864b363dfabc90ef29a1092d269f18c7e28"}, + {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c22541f0b672f4d741382a97c65609332a783501551445ab2df137ada01e019e"}, + {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63314832e302cc10d8dfbda0333a384bf4bcfce80d65fe99b0f3c0da8945a91a"}, + {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a25fbd8a5a58061e433d6fae6d5298777c0814a8bcefa1e5ecfff20c594bd749"}, + {file = "jiter-0.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:503b2c27d87dfff5ab717a8200fbbcf4714516c9d85558048b1fc14d2de7d8dc"}, + {file = "jiter-0.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d1f3d27cce923713933a844872d213d244e09b53ec99b7a7fdf73d543529d6d"}, + {file = "jiter-0.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c95980207b3998f2c3b3098f357994d3fd7661121f30669ca7cb945f09510a87"}, + {file = "jiter-0.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:afa66939d834b0ce063f57d9895e8036ffc41c4bd90e4a99631e5f261d9b518e"}, + {file = "jiter-0.5.0-cp310-none-win32.whl", hash = "sha256:f16ca8f10e62f25fd81d5310e852df6649af17824146ca74647a018424ddeccf"}, + {file = "jiter-0.5.0-cp310-none-win_amd64.whl", hash = "sha256:b2950e4798e82dd9176935ef6a55cf6a448b5c71515a556da3f6b811a7844f1e"}, + {file = "jiter-0.5.0.tar.gz", hash = "sha256:1d916ba875bcab5c5f7d927df998c4cb694d27dceddf3392e58beaf10563368a"}, +] + [[package]] name = "jsonpatch" version = "1.33" @@ -1663,6 +1717,22 @@ files = [ {file = "langchain-0.2.6.tar.gz", hash = "sha256:867f6add370c1e3911b0e87d3dd0e36aec1e8f513bf06131340fe8f151d89dc5"}, ] +[[package]] +name = "langchain-anthropic" +version = "0.1.20" +requires_python = "<4.0,>=3.8.1" +summary = "An integration package connecting AnthropicMessages and LangChain" +groups = ["default"] +dependencies = [ + "anthropic<1,>=0.28.0", + "defusedxml<0.8.0,>=0.7.1", + "langchain-core<0.3,>=0.2.17", +] +files = [ + {file = "langchain_anthropic-0.1.20-py3-none-any.whl", hash = "sha256:3a0d89ac6856be98beb3ec63813393bf29af3c5134247979c055938e741b7d9d"}, + {file = "langchain_anthropic-0.1.20.tar.gz", hash = "sha256:cb9607fecfc0f0de49b79dd0fc066790e2877873ef753abd98d2ae38d6e0f5b2"}, +] + [[package]] name = "langchain-astradb" version = "0.3.3" @@ -1704,7 +1774,7 @@ files = [ [[package]] name = "langchain-core" -version = "0.2.11" +version = "0.2.18" requires_python = "<4.0,>=3.8.1" summary = "Building applications with LLMs through composability" groups = ["default"] @@ -1717,8 +1787,8 @@ dependencies = [ "tenacity!=8.4.0,<9.0.0,>=8.1.0", ] files = [ - {file = "langchain_core-0.2.11-py3-none-any.whl", hash = "sha256:c7ca4dc4d88e3c69fd7916c95a7027c2b1a11c2db5a51141c3ceb8afac212208"}, - {file = "langchain_core-0.2.11.tar.gz", hash = "sha256:7a4661b50604eeb20c3373fbfd8a4f1b74482a6ab4e0f9df11e96821ead8ef0c"}, + {file = "langchain_core-0.2.18-py3-none-any.whl", hash = "sha256:c9dbb197508e76337ed810ec977d40ae0c896397d191b420ef126c3818a1be96"}, + {file = "langchain_core-0.2.18.tar.gz", hash = "sha256:ca5c5f1a783449dae8686e366ff3c5b775f8b5cef0de4ef346b8820d3d1c46ff"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 248fb63..b98e632 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,9 +29,9 @@ dependencies = [ "requests>=2.31.0", "langchain-astradb>=0.3.3", "langchain-openai>=0.1.14", - "google-api-python-client>=2.136.0", - "google-auth>=2.31.0", - "google-auth-oauthlib>=1.2.0", + "google-api-python-client>=2.137.0", + "google-auth>=2.32.0", + "google-auth-oauthlib>=1.2.1", "google-auth-httplib2>=0.2.0", "pathlib>=1.0.1", "Flask>=3.0.3", @@ -39,6 +39,7 @@ dependencies = [ "beeware>=0.3.0", "dotenv>=0.0.5", "astrapy>=1.3.1", + "langchain-anthropic>=0.1.20", ] [tool.pdm.dev-dependencies] diff --git a/src/backend/RAG/LangChain_Implementation/chain.py b/src/backend/RAG/LangChain_Implementation/chain.py index 005fc36..ed9cf3d 100644 --- a/src/backend/RAG/LangChain_Implementation/chain.py +++ b/src/backend/RAG/LangChain_Implementation/chain.py @@ -1,6 +1,6 @@ -import json import os import sys +from os import environ from dotenv import load_dotenv from langchain.chains import create_history_aware_retriever, create_retrieval_chain @@ -23,7 +23,7 @@ 0) list of LLMs with which to retrieve e.g. ['gpt-4', 'gemini', 'mistral'] 1) input string 2) chat history in following shape [ - {'gpt-4': "Hello, how can I help you?"}, + {'gpt-4': "Hello, how can I help you?"};; {'user': "What do prisons and plants have in common?"} ] """ @@ -51,39 +51,39 @@ def main(): etc.]""") # Arguments - llm_list = sys.argv[1] - llm_list = list(llm_list.replace('[', '').replace(']', '').replace("'", '').split(',')) - if not llm_list: - llm_list = ['gpt-4'] + # llm_list = sys.argv[1] + # llm_list = list(llm_list.replace('[', '').replace(']', '').replace("'", '').split(',')) + # if not llm_list: + # llm_list = ['gpt-4'] # print(llm_list) - input_string = sys.argv[2] + # input_string = sys.argv[2] # print(input_string) - message_history = sys.argv[3] + # message_history = sys.argv[3] # print(message_history) - message_history = message_history.split(';;') + # message_history = message_history.split(';;') # print(message_history) - message_history = [json.loads(substring.replace("'", '"')) for substring in message_history] + # message_history = [json.loads(substring.replace("'", '"')) for substring in message_history] # print(message_history) load_dotenv() # to be put into seperate function in order to invoke LLMs seperately - openai_api_key = os.environ.get('OPENAI_API_KEY') + os.environ.get('OPENAI_API_KEY') # google_api_key = os.environ.get('GOOGLE_API_KEY') - # anthropic_api_key = os.environ.get('ANTHROPIC_API_KEY') - - # test_llm_list = ['gpt-4'] - # llm_list = test_llm_list - # test_history = [ - # {'gpt-4': 'Hello, how can I help you?', 'gemini': 'Hello, how can I help you?'}, - # {'user': 'What do prisons and plants have in common?'}, - # {'gpt-4': 'They both have cell walls.', 'gemini': 'They have cell walls.'}, - # ] - # message_history = test_history - - # test_query = 'Ah, true. Thanks. What else do they have in common?' + os.environ.get('ANTHROPIC_API_KEY') + + test_llm_list = ['claude'] + llm_list = test_llm_list + test_history = [ + {'user': 'What do prisons and plants have in common?'}, + {'claude': 'They both have cell walls.', 'gemini': 'They have cell walls.'}, + ] + message_history = test_history + + test_query = 'Ah, true. Thanks. What else do they have in common?' + input_string = test_query + # test_query = "How many corners does a heptagon have?" - # input_string = test_query # test_follow_up = "How does one call a polygon with two more corners?" # AstraDB Section @@ -94,7 +94,7 @@ def main(): # LangChain Docs: ------------------------- vstore = AstraDBVectorStore( - embedding=OpenAIEmbeddings(openai_api_key=openai_api_key), + embedding=OpenAIEmbeddings(), collection_name=astra_db_collection, api_endpoint=astra_db_api_endpoint, token=astra_db_application_token, @@ -142,11 +142,14 @@ def main(): # print(_llm) chat_history = custom_history(message_history, _llm) if _llm == 'gpt-4': + environ.get('OPENAI_API_KEY') llm = OpenAI(temperature=0.2) elif _llm == 'gemini': + environ.get('GOOGLE_API_KEY') llm = ChatGoogleGenerativeAI(model='gemini-1.5-pro-latest') elif _llm == 'claude': - llm = ChatAnthropic(model_name='claude-3-opus-20240229') + environ.get('ANTHROPIC_API_KEY') + llm = ChatAnthropic(model='claude-3-5-sonnet-20240620') print(chat_history) contextualize_q_prompt = ChatPromptTemplate.from_messages( diff --git a/src/backend/RAG/LangChain_Implementation/combined_chain.py b/src/backend/RAG/LangChain_Implementation/combined_chain.py new file mode 100644 index 0000000..12f03cb --- /dev/null +++ b/src/backend/RAG/LangChain_Implementation/combined_chain.py @@ -0,0 +1,181 @@ +# import concurrent.futures +from os import environ + +from dotenv import load_dotenv +from get_google_docs import get_inital_prompt +from langchain.chains import create_history_aware_retriever, create_retrieval_chain +from langchain.chains.combine_documents import create_stuff_documents_chain +from langchain.retrievers.self_query.base import SelfQueryRetriever +from langchain_anthropic import ChatAnthropic + +# separated files +from langchain_astradb import AstraDBVectorStore +from langchain_core.messages import AIMessage, HumanMessage +from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder +from langchain_google_genai import ChatGoogleGenerativeAI +from langchain_openai import OpenAI, OpenAIEmbeddings +from test_meta import document_content_description, metadata_field_info + + +def get_vector_store(): + embeddings = OpenAIEmbeddings(api_key=environ.get('OPENAI_API_KEY')) + astra_vector_store = AstraDBVectorStore( + embedding=embeddings, + collection_name=environ.get('ASTRA_DB_COLLECTION'), + api_endpoint=environ.get('ASTRA_DB_API_ENDPOINT'), + token=environ.get('ASTRA_DB_APPLICATION_TOKEN'), + namespace=environ.get('ASTRA_DB_NAMESPACE'), + ) + return astra_vector_store + + +# Compartmented above - new functions below + +# def get_session_history(session_id: str) -> BaseChatMessageHistory: +# if session_id not in store: +# store[session_id] = ChatMessageHistory() +# return store[session_id] + + +# def create_health_ai_chain(llm): +# retriever = SelfQueryRetriever.from_llm( +# llm=llm, +# vectorstore=get_vector_store(), +# document_content_description=document_content_description, +# metadata_field_info=metadata_field_info, +# document_contents='', +# ) +# health_ai_template = """ +# You are a health AI agent equipped with access to diverse sources of health data, +# including research articles, nutritional information, medical archives, and more. +# Your task is to provide informed answers to user queries based on the available data. +# If you cannot find relevant information, simply state that you do not have enough data +# to answer accurately. write your response in markdown form and also add reference url +# so user can know from which source you are answering the questions. + +# CONTEXT: +# {context} + +# QUESTION: {question} + +# YOUR ANSWER: +# """ +# health_ai_prompt = ChatPromptTemplate.from_template(health_ai_template) +# chain = ( +# {'context': retriever, 'question': RunnablePassthrough()} +# | health_ai_prompt +# | llm +# | StrOutputParser() +# ) +# return chain + + +def custom_history(entire_history: list, llm_name: str): + chat_history = [] + for msg in entire_history: + if 'user' in msg: + chat_history.extend([HumanMessage(content=msg['user'])]) + if llm_name in msg: + chat_history.extend([AIMessage(content=msg[llm_name])]) + return chat_history + + +def hist_aware_answers(llm_list, input_string, message_history): + answers = {} + vector_store = get_vector_store() + + get_init_answer = get_inital_prompt() + init_prompt = '' if get_init_answer is None else get_init_answer + + contextualize_q_system_prompt = """Given a chat history and the latest user question \ + which might reference context in the chat history, formulate a standalone question \ + which can be understood without the chat history. Do NOT answer the question, \ + just reformulate it if needed and otherwise return it as is.""" + + # add in custom user info: ----------------------------- + # custom_istructions = get_custom_instructions_callable() + # user_info = " " + # if custom_istructions: + # user_info = f"""Here is some information about the user, including the user's name, + # their profile description and style instructions on how they want you to answer stylewise: + # User Name: {custom_istructions['name']} + # Style Instrctions: {custom_istructions['styleInstructions']} + # Personal Info: {custom_istructions['personalInstructions']} + # """ + + agent_str = """ You are a health AI agent equipped with + access to diverse sources of health data, + including research articles, nutritional information, medical archives, and more. + Your task is to provide informed answers to user queries based on the available data. + If you cannot find relevant information, simply state that you do not have enough data + to answer accurately. write your response in markdown form and also add reference url + so user can know from which source you are answering the questions. + """ + + context_str = """ + CONTEXT: + {context} + + """ + + # health_ai_template = f'{init_prompt}{agent_str}{user_info}{context_str}' + health_ai_template = f'{init_prompt}{agent_str}{context_str}' + + for llm_name in llm_list: + chat_history = custom_history(message_history, llm_name) + + if llm_name == 'gpt-4': + environ.get('OPENAI_API_KEY') + llm = OpenAI(temperature=0.2) + elif llm_name == 'gemini': + environ.get('GOOGLE_API_KEY') + llm = ChatGoogleGenerativeAI(model='gemini-1.5-pro-latest') + elif llm_name == 'claude': + environ.get('ANTHROPIC_API_KEY') + llm = ChatAnthropic(model='claude-3-5-sonnet-20240620') + + retriever = SelfQueryRetriever.from_llm( + llm=llm, + vectorstore=vector_store, + document_content_description=document_content_description, + metadata_field_info=metadata_field_info, + document_contents='', + ) + + contextualize_q_prompt = ChatPromptTemplate.from_messages( + [ + ('system', contextualize_q_system_prompt), + MessagesPlaceholder('chat_history'), + ('human', '{input}'), + ] + ) + history_aware_retriever = create_history_aware_retriever( + llm, retriever, contextualize_q_prompt + ) + + qa_prompt = ChatPromptTemplate.from_messages( + [ + ('system', health_ai_template), + MessagesPlaceholder('chat_history'), + ('human', '{input}'), + ] + ) + + question_answer_chain = create_stuff_documents_chain(llm, qa_prompt) + rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain) + msg = rag_chain.invoke({'input': input_string, 'chat_history': chat_history}) + answers[llm_name] = msg['answer'] + + return answers + + +load_dotenv() +test_question = 'What should I eat if I have diabetes?' +test_history = [ + {'user': 'I like salad.'}, + {'gpt-4': 'Salad healthy.', 'gemini': 'Burger better, more protein.', 'claude': 'Go running.'}, +] +test_history = [] +llms = ['gpt-4', 'gemini', 'claude'] +test_answer = hist_aware_answers(llms, test_question, test_history) +print(test_answer) diff --git a/src/backend/RAG/LangChain_Implementation/combined_chain_copy.py b/src/backend/RAG/LangChain_Implementation/combined_chain_copy.py new file mode 100644 index 0000000..4c49910 --- /dev/null +++ b/src/backend/RAG/LangChain_Implementation/combined_chain_copy.py @@ -0,0 +1,201 @@ +import concurrent.futures +from os import environ + +from dotenv import load_dotenv +from get_google_docs import get_inital_prompt +from langchain.chains import create_history_aware_retriever, create_retrieval_chain +from langchain.chains.combine_documents import create_stuff_documents_chain +from langchain.retrievers.self_query.base import SelfQueryRetriever +from langchain_anthropic import ChatAnthropic + +# separated files +from langchain_astradb import AstraDBVectorStore +from langchain_core.messages import AIMessage, HumanMessage +from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder +from langchain_google_genai import ChatGoogleGenerativeAI +from langchain_openai import OpenAI, OpenAIEmbeddings +from test_meta import document_content_description, metadata_field_info + + +def get_vector_store(): + embeddings = OpenAIEmbeddings(api_key=environ.get('OPENAI_API_KEY')) + astra_vector_store = AstraDBVectorStore( + embedding=embeddings, + collection_name=environ.get('ASTRA_DB_COLLECTION'), + api_endpoint=environ.get('ASTRA_DB_API_ENDPOINT'), + token=environ.get('ASTRA_DB_APPLICATION_TOKEN'), + namespace=environ.get('ASTRA_DB_NAMESPACE'), + ) + return astra_vector_store + + +# Compartmented above - new functions below + +# def get_session_history(session_id: str) -> BaseChatMessageHistory: +# if session_id not in store: +# store[session_id] = ChatMessageHistory() +# return store[session_id] + + +# def create_health_ai_chain(llm): +# retriever = SelfQueryRetriever.from_llm( +# llm=llm, +# vectorstore=get_vector_store(), +# document_content_description=document_content_description, +# metadata_field_info=metadata_field_info, +# document_contents='', +# ) +# health_ai_template = """ +# You are a health AI agent equipped with access to diverse sources of health data, +# including research articles, nutritional information, medical archives, and more. +# Your task is to provide informed answers to user queries based on the available data. +# If you cannot find relevant information, simply state that you do not have enough data +# to answer accurately. write your response in markdown form and also add reference url +# so user can know from which source you are answering the questions. + +# CONTEXT: +# {context} + +# QUESTION: {question} + +# YOUR ANSWER: +# """ +# health_ai_prompt = ChatPromptTemplate.from_template(health_ai_template) +# chain = ( +# {'context': retriever, 'question': RunnablePassthrough()} +# | health_ai_prompt +# | llm +# | StrOutputParser() +# ) +# return chain + + +def custom_history(entire_history: list, llm_name: str): + chat_history = [] + for msg in entire_history: + if 'user' in msg: + chat_history.extend([HumanMessage(content=msg['user'])]) + if llm_name in msg: + chat_history.extend([AIMessage(content=msg[llm_name])]) + return chat_history + + +def process_llm( + llm_name, + input_string, + message_history, + vector_store, + contextualize_q_system_prompt, + health_ai_template, +): + chat_history = custom_history(message_history, llm_name) + + if llm_name == 'gpt-4': + environ.get('OPENAI_API_KEY') + llm = OpenAI(temperature=0.2) + elif llm_name == 'gemini': + environ.get('GOOGLE_API_KEY') + llm = ChatGoogleGenerativeAI(model='gemini-1.5-pro-latest') + elif llm_name == 'claude': + environ.get('ANTHROPIC_API_KEY') + llm = ChatAnthropic(model='claude-3-5-sonnet-20240620') + + retriever = SelfQueryRetriever.from_llm( + llm=llm, + vectorstore=vector_store, + document_content_description=document_content_description, + metadata_field_info=metadata_field_info, + document_contents='', + ) + + contextualize_q_prompt = ChatPromptTemplate.from_messages( + [ + ('system', contextualize_q_system_prompt), + MessagesPlaceholder('chat_history'), + ('human', '{input}'), + ] + ) + history_aware_retriever = create_history_aware_retriever(llm, retriever, contextualize_q_prompt) + + qa_prompt = ChatPromptTemplate.from_messages( + [('system', health_ai_template), MessagesPlaceholder('chat_history'), ('human', '{input}')] + ) + + question_answer_chain = create_stuff_documents_chain(llm, qa_prompt) + rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain) + msg = rag_chain.invoke({'input': input_string, 'chat_history': chat_history}) + + return llm_name, msg['answer'] + + +def hist_aware_answers(llm_list, input_string, message_history): + answers = {} + vector_store = get_vector_store() + + get_init_answer = get_inital_prompt() + init_prompt = '' if get_init_answer is None else get_init_answer + + contextualize_q_system_prompt = """Given a chat history and the latest user question \ + which might reference context in the chat history, formulate a standalone question \ + which can be understood without the chat history. Do NOT answer the question, \ + just reformulate it if needed and otherwise return it as is.""" + + # add in custom user info: ----------------------------- + # custom_istructions = get_custom_instructions_callable() + # user_info = " " + # if custom_istructions: + # user_info = f"""Here is some information about the user, including the user's name, + # their profile description and style instructions on how they want you to answer stylewise: + # User Name: {custom_istructions['name']} + # Style Instrctions: {custom_istructions['styleInstructions']} + # Personal Info: {custom_istructions['personalInstructions']} + # """ + + agent_str = """ You are a health AI agent equipped with + access to diverse sources of health data, + including research articles, nutritional information, medical archives, and more. + Your task is to provide informed answers to user queries based on the available data. + If you cannot find relevant information, simply state that you do not have enough data + to answer accurately. write your response in markdown form and also add reference url + so user can know from which source you are answering the questions. + """ + + context_str = """ + CONTEXT: + {context} + + """ + + health_ai_template = f'{init_prompt}{agent_str}{user_info}{context_str}' + + # Parallel processing + with concurrent.futures.ThreadPoolExecutor() as executor: + future_to_llm = { + executor.submit( + process_llm, + llm, + input_string, + message_history, + vector_store, + contextualize_q_system_prompt, + health_ai_template, + ): llm + for llm in llm_list + } + for future in concurrent.futures.as_completed(future_to_llm): + llm_name, answer = future.result() + answers[llm_name] = answer + + return answers + + +load_dotenv() +test_question = 'Give me some good recipies?' +test_history = [ + {'user': 'I like salad.'}, + {'gpt-4': 'Salad healthy.', 'gemini': 'Burger better, more protein.', 'claude': 'Go running.'}, +] +test_history = [] +llms = ['gpt-4', 'gemini', 'claude'] +test_answer = hist_aware_answers(llms, test_question, test_history) +print(test_answer) diff --git a/src/backend/RAG/LangChain_Implementation/get_google_docs.py b/src/backend/RAG/LangChain_Implementation/get_google_docs.py index 9bc456c..692b575 100644 --- a/src/backend/RAG/LangChain_Implementation/get_google_docs.py +++ b/src/backend/RAG/LangChain_Implementation/get_google_docs.py @@ -2,7 +2,6 @@ import os import pickle import re -from pathlib import Path from google.auth.transport.requests import Request from google_auth_oauthlib.flow import InstalledAppFlow @@ -40,7 +39,7 @@ def authenticate(credentials, scopes): return creds -def download_file(file_id, credentials_path, file_name): +def download_file(file_id, credentials_path): scopes = ['https://www.googleapis.com/auth/drive.readonly'] credentials = authenticate(credentials_path, scopes) drive_service = build('drive', 'v3', credentials=credentials) @@ -49,35 +48,34 @@ def download_file(file_id, credentials_path, file_name): export_mime_type = 'text/plain' request = drive_service.files().export_media(fileId=file_id, mimeType=export_mime_type) - # Create a file on disk to write the exported content - fh = io.FileIO(file_name, 'wb') + # Use a BytesIO buffer to handle the file content in memory + fh = io.BytesIO() downloader = MediaIoBaseDownload(fh, request) done = False while not done: status, done = downloader.next_chunk() print(f'Download {int(status.progress() * 100)}%.') - # Read the content of the exported file - with open(file_name, 'r', encoding='utf-8') as file: - content = file.read() - - return content + # Reset the buffer's position to the beginning + fh.seek(0) + # Read the content of the buffer + content = fh.read().decode('utf-8') -# Example usage -document_id = extract_document_id_from_url( - 'https://docs.google.com/document/d/1GtLyBqhk-cu8CSo4A15WTgGDbMbL4B9LLjdvBoU3234/edit' -) -# print("Document id: ", document_id) -credentials_json = 'credentials.json' + return content -# Define the file path in a cross-platform manner -file_name = Path('data') / 'google_docs_content.txt' -file_name.parent.mkdir(parents=True, exist_ok=True) -# TODO: make this callable from typescript with url -try: - content = download_file(document_id, credentials_json, file_name) - print(content) -except Exception as e: - print(f'An error occurred: {e}') +def get_inital_prompt(): + # Example usage + document_id = extract_document_id_from_url( + 'https://docs.google.com/document/d/1GtLyBqhk-cu8CSo4A15WTgGDbMbL4B9LLjdvBoU3234/edit' + ) + # print("Document id: ", document_id) + credentials_json = 'credentials.json' + + try: + content = download_file(document_id, credentials_json) + return content + except Exception as e: + print(f'An error occurred: {e}') + return None diff --git a/src/backend/RAG/LangChain_Implementation/test_meta.py b/src/backend/RAG/LangChain_Implementation/test_meta.py new file mode 100644 index 0000000..43f6e4d --- /dev/null +++ b/src/backend/RAG/LangChain_Implementation/test_meta.py @@ -0,0 +1,110 @@ +from langchain.chains.query_constructor.base import AttributeInfo + +metadata_field_info = [ + AttributeInfo( + name='author', + description='The author of the YouTube video or nutrition article', + type='string', + ), + AttributeInfo( + name='videoId', description='The unique identifier for the YouTube video', type='string' + ), + AttributeInfo(name='title', description='The title of the content', type='string'), + AttributeInfo( + name='keywords', + description='A list of keywords associated with the YouTube video', + type='List[string]', + ), + AttributeInfo( + name='viewCount', description='The number of views for the YouTube video', type='string' + ), + AttributeInfo( + name='shortDescription', + description='A short description of the YouTube video', + type='string', + ), + AttributeInfo(name='transcript', description='The transcript of the content', type='string'), + AttributeInfo( + name='authors', description='The authors of the PubMed or Archive document', type='list' + ), + AttributeInfo( + name='publicationDate', + description='The publication date of the PubMed or Archive document', + type='string', + ), + AttributeInfo( + name='abstract', description='The abstract of the PubMed or Archive document', type='string' + ), + AttributeInfo(name='date', description='The date of the nutrition article', type='string'), + AttributeInfo( + name='keyPoints', description='The key points of the nutrition article', type='string' + ), + AttributeInfo(name='subTitle', description='The subtitle of the recipe', type='string'), + AttributeInfo(name='rating', description='The rating of the recipe', type='float'), + AttributeInfo( + name='recipeDetails', + description='The details of the recipe include the time also.', + type='Dict[string, string]', + ), + AttributeInfo( + name='ingredients', description='A list of ingredients for the recipe', type='List[string]' + ), + AttributeInfo(name='steps', description='The steps to prepare the recipe', type='List[string]'), + AttributeInfo( + name='nutritionFacts', + description='Nutritional facts of the recipe', + type='Dict[string, string]', + ), + AttributeInfo( + name='nutritionInfo', + description='Detailed nutritional information of the recipe', + type='Dict[string, Dict[string, string]]', + ), +] + +document_content_description = """ +It includes a variety of metadata to describe different aspects of the content: + +General Information: +- Title: The title of the content. +- Transcript: A full transcript of any video, audio, or written content associated with document. + +YouTube Video Information: +- Author: The author or creator of the YouTube video. +- VideoId: The unique identifier for the YouTube video. +- Keywords: A list of relevant keywords associated with the YouTube video. +- ViewCount: The number of views for the YouTube video. +- Short Description: A brief overview of the YouTube video. + +PubMed Article Information: +- Authors: List of authors for the PubMed article. +- PublicationDate: The date when the PubMed article was published. +- Abstract: A summary of the PubMed article. + +Podcast Information: +- Title: The title of the podcast episode. +- Transcript: The transcript of the podcast episode. + +Nutrition Article Information: +- Title: The title of the nutrition article. +- Date: The date when the nutrition article was published. +- Author: The author of the nutrition article. +- Key Points: Important highlights or key points about recipe from the nutrition article. + +Recipe Information: +- Title: The title of the recipe. +- SubTitle: The subtitle of the recipe. +- Rating: The rating of the recipe, if available. +- Recipe Details: Detailed information about the recipe, including preparation time, +cooking time, and serving size. +- Ingredients: A list of ingredients required for making recipe. +- Steps: Step-by-step instructions to prepare the dish. +- Nutrition Facts: Basic nutritional information about the recipe. +- Nutrition Info: Detailed nutritional information, including amounts and daily values. + +Archived Document Information: +- Title: The title of the archived document. +- Authors: List of authors for the archived document. +- Abstract: A summary of the archived document. +- PublicationDate: The date when the archived document was published. +"""