diff --git a/Oracle-23ai-RAG-Chatbot/CONTRIBUTING.md b/Oracle-23ai-RAG-Chatbot/CONTRIBUTING.md new file mode 100644 index 0000000..637430b --- /dev/null +++ b/Oracle-23ai-RAG-Chatbot/CONTRIBUTING.md @@ -0,0 +1,55 @@ +# Contributing to this repository + +We welcome your contributions! There are multiple ways to contribute. + +## Opening issues + +For bugs or enhancement requests, please file a GitHub issue unless it's +security related. When filing a bug remember that the better written the bug is, +the more likely it is to be fixed. If you think you've found a security +vulnerability, do not raise a GitHub issue and follow the instructions in our +[security policy](./SECURITY.md). + +## Contributing code + +We welcome your code contributions. Before submitting code via a pull request, +you will need to have signed the [Oracle Contributor Agreement][OCA] (OCA) and +your commits need to include the following line using the name and e-mail +address you used to sign the OCA: + +```text +Signed-off-by: Your Name +``` + +This can be automatically added to pull requests by committing with `--sign-off` +or `-s`, e.g. + +```text +git commit --signoff +``` + +Only pull requests from committers that can be verified as having signed the OCA +can be accepted. + +## Pull request process + +1. Ensure there is an issue created to track and discuss the fix or enhancement + you intend to submit. +1. Fork this repository. +1. Create a branch in your fork to implement the changes. We recommend using + the issue number as part of your branch name, e.g. `1234-fixes`. +1. Ensure that any documentation is updated with the changes that are required + by your change. +1. Ensure that any samples are updated if the base image has been changed. +1. Submit the pull request. *Do not leave the pull request blank*. Explain exactly + what your changes are meant to do and provide simple steps on how to validate. + your changes. Ensure that you reference the issue you created as well. +1. We will assign the pull request to 2-3 people for review before it is merged. + +## Code of conduct + +Follow the [Golden Rule](https://en.wikipedia.org/wiki/Golden_Rule). If you'd +like more specific guidelines, see the [Contributor Covenant Code of Conduct][COC]. + +[OCA]: https://oca.opensource.oracle.com +[COC]: https://www.contributor-covenant.org/version/1/4/code-of-conduct/ diff --git a/Oracle-23ai-RAG-Chatbot/LICENSE.txt b/Oracle-23ai-RAG-Chatbot/LICENSE.txt new file mode 100644 index 0000000..62c949c --- /dev/null +++ b/Oracle-23ai-RAG-Chatbot/LICENSE.txt @@ -0,0 +1,35 @@ +Copyright (c) 2024 Oracle and/or its affiliates. + +The Universal Permissive License (UPL), Version 1.0 + +Subject to the condition set forth below, permission is hereby granted to any +person obtaining a copy of this software, associated documentation and/or data +(collectively the "Software"), free of charge and under any and all copyright +rights in the Software, and any and all patent rights owned or freely +licensable by each licensor hereunder covering either (i) the unmodified +Software as contributed to or provided by such licensor, or (ii) the Larger +Works (as defined below), to deal in both + +(a) the Software, and +(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if +one is included with the Software (each a "Larger Work" to which the Software +is contributed by such licensors), + +without restriction, including without limitation the rights to copy, create +derivative works of, display, perform, and distribute the Software and make, +use, sell, offer for sale, import, export, have made, and have sold the +Software and the Larger Work(s), and to sublicense the foregoing rights on +either these or other terms. + +This license is subject to the following condition: +The above copyright notice and either this complete permission notice or at +a minimum a reference to the UPL must be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Oracle-23ai-RAG-Chatbot/README.md b/Oracle-23ai-RAG-Chatbot/README.md new file mode 100644 index 0000000..469e5e9 --- /dev/null +++ b/Oracle-23ai-RAG-Chatbot/README.md @@ -0,0 +1,196 @@ +# Integrate Oracle 23ai Vector DB with OCI GenAI using Llama-index + +[![Python Version](https://img.shields.io/badge/python-3.11.x-blue.svg)](https://www.python.org/downloads/release/python-3110/) + +## Introduction + +Accessing the right answers from vast data repositories is a challenge many organizations face. A **Retrieval-Augmented Generation (RAG)** based system can revolutionize how users interact with their data by making information easily accessible and up-to-date. In this workshop, we’ll build a RAG-based chatbot using **Oracle Database 23ai** and **OCI Generative AI** services, allowing users to chat with their unstructured data like PDF, CSV, and TXT files. This approach combines advanced retrieval techniques with generative AI, creating a powerful solution for intelligent and dynamic data interaction. + + +## What is RAG? + +**Retrieval-Augmented Generation (RAG)** combines retrieval-based methods with generative AI to provide more accurate and contextually relevant responses by accessing and utilizing large datasets dynamically. [Learn more about RAG](https://www.oracle.com/artificial-intelligence/generative-ai/retrieval-augmented-generation-rag/). + +## Prerequisites and Setup + +Before you begin, ensure you have the following: + +- **Oracle Cloud Account** + [Sign up here](https://www.oracle.com/cloud/free/) + +- **Oracle Database 23AI** + [Learn more](https://www.oracle.com/database/23ai/) + +- **Compute VM** + This will serve as your web app. Ensure the Compute VM can communicate with the Oracle Database by setting up the appropriate network configurations. + +- **OCI Generative AI Services** + [Documentation](https://docs.oracle.com/en-us/iaas/Content/GenerativeAI/home.htm) + +- **LlamaIndex** + [Documentation](https://pypi.org/project/llama-index/) + +- **Python Dependencies** + Listed in the `requirements.txt` file in the repository. + +## Setup + +### 1. Clone the Repository in your web server (Compute VM) + +```bash +sudo dnf install git + +git clone https://github.com/SaurabhSalunkhe/Oracle-23ai-RAG-Chatbot.git +``` + +### 2. Update and Install Dependencies (Oracle Linux) +``` +sudo yum update -y && sudo yum install -y git python3 && sudo yum groupinstall -y "Development Tools" && sudo yum install -y bzip2-devel openssl-devel libffi-devel zlib-devel wget libffi-devel openssl openssl-devel tk-devel xz-devel zlib-devel bzip2-devel readline-devel libuuid-devel ncurses-devel libaio + +``` + +### 3. Install Python 3.11.x +Ensure Python version 3.11.x is installed. + +``` +mkdir -p $HOME/python +wget https://www.python.org/ftp/python/3.11.0/Python-3.11.0.tgz +tar -xvzf Python-3.11.0.tgz --strip-components=1 -C /home/$USER/python +cd $HOME/python +./configure --prefix=$HOME/python +./configure --enable-optimizations +make clean; make +make altinstall + +export PYTHONHOME=$HOME/python +export PATH=$PYTHONHOME/bin:$PATH +export LD_LIBRARY_PATH=$PYTHONHOME/lib:$LD_LIBRARY_PATH + +cd $HOME/python/bin +ln -s python3.11 python3 +ln -s pip3.11 pip3 + +``` + +### 4. Create and Activate Virtual Environment +``` +cd Oracle-23ai-RAG-Chatbot +python3.11 -m venv venv +source venv/bin/activate +``` + +### 5. Install Python Dependencies + +``` +pip install -r requirements.txt +``` + +### 6. Configure OCI Authentication +a. Create the .oci Directory + +``` +mkdir -p /home/opc/.oci +``` + +b. Generate OCI API Keys +Follow the OCI SDK Configuration Guide to generate your API keys. +https://docs.oracle.com/en-us/iaas/Content/API/Concepts/sdkconfig.htm + +https://docs.oracle.com/en-us/iaas/Content/API/Concepts/apisigningkey.htm#apisigningkey_topic_How_to_Generate_an_API_Signing_Key_Console + +### 7. Set Up Oracle Database 23ai +a. Run SQL Commands from create_tables.sql +Create the User and Grant Privileges + +``` +-- Create the user with a specified password +CREATE USER ai_user IDENTIFIED BY "EXamplepassword#_123"; + +-- Grant DBA privileges for full administrative access +GRANT DBA TO ai_user; + +-- Grant specific roles and privileges needed for Oracle AI Vector Search +GRANT DB_DEVELOPER_ROLE TO ai_user; +GRANT CREATE MINING MODEL TO ai_user; +``` + +As ai_user, Create Tables with Vector Data Types + + +``` +CREATE TABLE BOOKS ( + ID NUMBER NOT NULL, + NAME VARCHAR2(100) NOT NULL, + PRIMARY KEY (ID) +); + +CREATE TABLE CHUNKS ( + ID VARCHAR2(64) NOT NULL, + CHUNK CLOB, + VEC VECTOR(1024, FLOAT64), + PAGE_NUM VARCHAR2(10), + BOOK_ID NUMBER, + PRIMARY KEY (ID), + CONSTRAINT fk_book + FOREIGN KEY (BOOK_ID) + REFERENCES BOOKS (ID) +); +``` + +8. Configure the Application +a. Edit config.py +Update the following parameters with your details: + +``` +# DB connections (Below are example credentials. Substitute with your credentials) +DB_USER = "ai_user" +DB_PWD = "EXamplepassword#_123" +DB_HOST_IP = "ip:1521" +DB_SERVICE = "example_PDB1.sub07260203110.vcnss.oraclevcn.com" + +# GenAI configurations +COMPARTMENT_OCID = "ocid1.compartment.oc1..xxxxxxxxx" +ENDPOINT = "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com" +COHERE_API_KEY = "xxxxxxxxxxx" # Optional but recommended +``` + +### 8. Run the Streamlit Application + +``` +sudo systemctl stop firewalld +streamlit run app.py +``` + +### 9. To Run the App in the Background + +``` +nohup streamlit run app.py & +``` + +### 10. Access the Chatbot +Open your browser and navigate to http://:8501. + +You should see the chatbot interface as shown below: + +![Chatbot UI](./screenshot.png) + +## Contributing + + +This project welcomes contributions from the community. Before submitting a pull +request, please [review our contribution guide](./CONTRIBUTING.md). + +## Security + +Please consult the [security guide](./SECURITY.md) for our responsible security +vulnerability disclosure process. + +## License +Copyright (c) 2024 Oracle and/or its affiliates. + +Licensed under the Universal Permissive License (UPL), Version 1.0. + +See [LICENSE](LICENSE.txt) for more details. + +ORACLE AND ITS AFFILIATES DO NOT PROVIDE ANY WARRANTY WHATSOEVER, EXPRESS OR IMPLIED, FOR ANY SOFTWARE, MATERIAL OR CONTENT OF ANY KIND CONTAINED OR PRODUCED WITHIN THIS REPOSITORY, AND IN PARTICULAR SPECIFICALLY DISCLAIM ANY AND ALL IMPLIED WARRANTIES OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, AND FITNESS FOR A PARTICULAR PURPOSE. FURTHERMORE, ORACLE AND ITS AFFILIATES DO NOT REPRESENT THAT ANY CUSTOMARY SECURITY REVIEW HAS BEEN PERFORMED WITH RESPECT TO ANY SOFTWARE, MATERIAL OR CONTENT CONTAINED OR PRODUCED WITHIN THIS REPOSITORY. IN ADDITION, AND WITHOUT LIMITING THE FOREGOING, THIRD PARTIES MAY HAVE POSTED SOFTWARE, MATERIAL OR CONTENT TO THIS REPOSITORY WITHOUT ANY REVIEW. USE AT YOUR OWN RISK. diff --git a/Oracle-23ai-RAG-Chatbot/SECURITY.md b/Oracle-23ai-RAG-Chatbot/SECURITY.md new file mode 100644 index 0000000..2ca8102 --- /dev/null +++ b/Oracle-23ai-RAG-Chatbot/SECURITY.md @@ -0,0 +1,38 @@ +# Reporting security vulnerabilities + +Oracle values the independent security research community and believes that +responsible disclosure of security vulnerabilities helps us ensure the security +and privacy of all our users. + +Please do NOT raise a GitHub Issue to report a security vulnerability. If you +believe you have found a security vulnerability, please submit a report to +[secalert_us@oracle.com][1] preferably with a proof of concept. Please review +some additional information on [how to report security vulnerabilities to Oracle][2]. +We encourage people who contact Oracle Security to use email encryption using +[our encryption key][3]. + +We ask that you do not use other channels or contact the project maintainers +directly. + +Non-vulnerability related security issues including ideas for new or improved +security features are welcome on GitHub Issues. + +## Security updates, alerts and bulletins + +Security updates will be released on a regular cadence. Many of our projects +will typically release security fixes in conjunction with the +Oracle Critical Patch Update program. Additional +information, including past advisories, is available on our [security alerts][4] +page. + +## Security-related information + +We will provide security related information such as a threat model, considerations +for secure use, or any known security issues in our documentation. Please note +that labs and sample code are intended to demonstrate a concept and may not be +sufficiently hardened for production use. + +[1]: mailto:secalert_us@oracle.com +[2]: https://www.oracle.com/corporate/security-practices/assurance/vulnerability/reporting.html +[3]: https://www.oracle.com/security-alerts/encryptionkey.html +[4]: https://www.oracle.com/security-alerts/ diff --git a/Oracle-23ai-RAG-Chatbot/THIRD_PARTY_LICENSES.txt b/Oracle-23ai-RAG-Chatbot/THIRD_PARTY_LICENSES.txt new file mode 100644 index 0000000..f04a7de --- /dev/null +++ b/Oracle-23ai-RAG-Chatbot/THIRD_PARTY_LICENSES.txt @@ -0,0 +1,223 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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. + +The MIT License + +Copyright (c) Jerry Liu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Oracle-23ai-RAG-Chatbot/app.py b/Oracle-23ai-RAG-Chatbot/app.py new file mode 100644 index 0000000..11e7673 --- /dev/null +++ b/Oracle-23ai-RAG-Chatbot/app.py @@ -0,0 +1,511 @@ +""" +Python Version: 3.11 + +Description: + This module provides UI for Oracle 23ai based chatbot to communicate with documents. +""" + +import os +import time +import logging +from llama_index.llms.oci_genai import OCIGenAI +import subprocess +import streamlit as st +from pathlib import Path +import chat_engine +from llama_index.core.llms import ChatMessage +import oracledb +from config import ( + VERBOSE, + EMBED_MODEL_TYPE, + EMBED_MODEL, + TOKENIZER, + ADD_REFERENCES, + GEN_MODEL, + ADD_RERANKER, + RERANKER_MODEL, + CHAT_MODE, + MEMORY_TOKEN_LIMIT, + ADD_PHX_TRACING, + PHX_PORT, + PHX_HOST, + COMPARTMENT_OCID, + ENDPOINT, + COHERE_API_KEY, + LA2_ENABLE_INDEX, + PROFILE_NAME, + STREAM_CHAT, + DB_USER, + DB_PWD, + DSN, + CONFIG_DIR, + WALLET_LOCATION, + WALLET_PASSWORD +) + +from config import ( + DB_HOST_IP, + DB_SERVICE, + EMBEDDINGS_BITS, + ADD_PHX_TRACING +) + +# Configure logger +logger = logging.getLogger("ConsoleLogger") +logger.setLevel(logging.INFO) + +if not logger.handlers: + handler = logging.StreamHandler() + handler.setLevel(logging.INFO) + formatter = logging.Formatter("%(asctime)s - %(message)s") + handler.setFormatter(formatter) + logger.addHandler(handler) + +logger.propagate = False + +# Initialize session state +def initialize_session_state(): + defaults = { + "max_tokens": 600, + "temperature": 0.10, + "top_k": 3, + "top_n": 3, + "messages": [], + "question_count": 0, + "similarity": 0.5, + "select_model": None, + "chat_history": [], + "selected_mode": "Enable RAG" + } + for key, value in defaults.items(): + if key not in st.session_state: + st.session_state[key] = value + +initialize_session_state() + +# Set the configuration for the Streamlit app +st.set_page_config(page_title="Oracle 23ai Vector Search Assistant", layout="wide") + +# Initialize directories for file uploads and processed files +upload_dir = Path("data/unprocessed") +processed_dir = Path("data/processed") +upload_dir.mkdir(parents=True, exist_ok=True) +processed_dir.mkdir(parents=True, exist_ok=True) + +# Title for the main page +st.markdown("

Oracle 23ai Vector Search Assistant

", unsafe_allow_html=True) + +# Check unique files present in the database +def get_existing_book_names(): + try: + connection = oracledb.connect( + user=DB_USER, + password=DB_PWD, + config_dir=CONFIG_DIR, + dsn=DSN, + wallet_location=WALLET_LOCATION, + wallet_password=WALLET_PASSWORD + ) + cursor = connection.cursor() + cursor.execute("SELECT DISTINCT name FROM books") + book_names_set = {name[0] for name in cursor.fetchall()} + except oracledb.Error as e: + logger.error(f"Database connection failed: {e}") + book_names_set = set() + finally: + try: + cursor.close() + connection.close() + except: + pass + return book_names_set + +book_names_set = get_existing_book_names() + +# Cache the chat engine creation to improve performance +@st.cache_resource +def create_chat_engine(): + return chat_engine.create_chat_engine( + verbose=VERBOSE, + **{key: st.session_state[key] for key in ["top_k", "max_tokens", "temperature", "top_n"]} + ) + +# Define the reset_conversation function +def reset_conversation(): + st.session_state.messages = [] + st.session_state.chat_history = [] + st.session_state.question_count = 0 + if st.session_state.enable_rag: + st.session_state.chat_engine, st.session_state.token_counter = create_chat_engine() + st.session_state.chat_engine.reset() + +# Function to handle form submission +def handle_form_submission(): + st.session_state.update({ + "max_tokens": st.session_state.max_tokens, + "temperature": st.session_state.temperature, + "top_k": st.session_state.top_k, + "top_n": st.session_state.top_n, + "similarity": st.session_state.similarity, + "select_model": st.session_state.select_model, + }) + reset_conversation() + +# Function to save uploaded files to the specified directory +def save_uploaded_file(uploaded_file, upload_dir): + file_path = upload_dir / uploaded_file.name + with open(file_path, "wb") as f: + f.write(uploaded_file.getbuffer()) + return file_path + +# Define the enable_select_ai function +def enable_select_ai(question): + output = "" + try: + connection = oracledb.connect( + user=DB_USER, + password=DB_PWD, + config_dir=CONFIG_DIR, + dsn=DSN, + wallet_location=WALLET_LOCATION, + wallet_password=WALLET_PASSWORD + ) + cursor = connection.cursor() + + cursor.execute("BEGIN DBMS_OUTPUT.ENABLE(NULL); END;") + # Substitute your select ai profile here instead of 'regularprofile' + cursor.execute("BEGIN DBMS_CLOUD_AI.set_profile('regulaprofile'); END;") + + normalized_question = question.strip().lower() + + # Check if the query already starts with 'select ai' + if not normalized_question.startswith("select ai "): + # Prepend 'SELECT AI ' to the original query + select_query = "SELECT AI " + question + print("Modified Query:", select_query) + else: + # Use the original query as it already starts with 'SELECT AI' + select_query = question + print("Original Query is already properly formatted.") + + cursor.execute(select_query) + results = cursor.fetchall() + + for row in results: + output += f"{row}\n" + + line = cursor.var(str) + status = cursor.var(int) + + while True: + cursor.callproc("DBMS_OUTPUT.GET_LINE", (line, status)) + if status.getvalue() != 0: + break + output += f"{line.getvalue()}\n" + + except oracledb.Error as e: + output += f"An error occurred:\n{e}\n" + finally: + try: + cursor.close() + connection.close() + except: + pass + + st.session_state.messages.append({"role": "assistant", "content": output}) + return output + +# Define the enable_rag function +def enable_rag(question): + try: + logger.info("Calling RAG chain..") + logger.info( + f"top_k= {st.session_state.top_k}, max_tokens= {st.session_state.max_tokens}, " + f"temperature= {st.session_state.temperature}, top_n= {st.session_state.top_n}, " + f"enable_rag= {st.session_state.enable_rag}, similarity = {st.session_state.similarity}" + ) + + with st.spinner("Generating response..."): + time_start = time.time() + st.session_state.question_count += 1 + logger.info("") + logger.info(f"Question no. {st.session_state.question_count} is {question}") + + if st.session_state.enable_rag: + if STREAM_CHAT: + response = st.session_state.chat_engine.stream_chat(question, st.session_state.chat_history) + print("inside st.session_state.chat_engine.stream_chat(question, st.session_state.chat_history)") + + else: + response = st.session_state.chat_engine.chat(question, st.session_state.chat_history) + print("response is ", response) + print("response = st.session_state.chat_engine.chat(question, st.session_state.chat_history)") + + else: + print("calling the chat_engine.llm_chat from enable rag ") + response = chat_engine.llm_chat(question) + + time_elapsed = time.time() - time_start + logger.info(f"Elapsed time: {round(time_elapsed, 1)} sec.") + + str_token1 = f"LLM Prompt Tokens: {st.session_state.token_counter.prompt_llm_token_count if st.session_state.enable_rag else 'N/A'}" + str_token2 = f"LLM Completion Tokens: {st.session_state.token_counter.completion_llm_token_count if st.session_state.enable_rag else 'N/A'}" + logger.info(str_token1) + logger.info(str_token2) + + output = no_stream_output(response) + st.session_state.messages.append({"role": "assistant", "content": output}) + #with st.chat_message("assistant"): + # st.markdown(output) + + except Exception as e: + logger.error("An error occurred: " + str(e)) + st.error("An error occurred: " + str(e)) + +# Define the handle_chat function +def handle_chat(question): + try: + with st.spinner("Generating chat response..."): + time_start = time.time() + st.session_state.question_count += 1 + logger.info("") + logger.info(f"Question no. {st.session_state.question_count} is {question}") + model_name = st.session_state['select_model'] + print("model_name is", model_name) + llm = OCIGenAI( + model=model_name, + service_endpoint=ENDPOINT, + compartment_id=COMPARTMENT_OCID, + auth_profile=PROFILE_NAME, # replace with your profile name + ) + print("selected llm is ", llm) + output = chat_engine.llm_chat(question) + #response = chat_engine.llm_chat(question) + #output = llm.chat([ChatMessage(role="user", content=question)]) + #logger.info(f"Question is: {question}") + st.session_state.messages.append({"role": "assistant", "content": output}) + with st.chat_message("assistant"): + st.markdown(output) + + except Exception as e: + logger.error("An error occurred: " + str(e)) + st.error("An error occurred: " + str(e)) + +# Placeholder function for 'Browse' feature +def browse(): + return "Feature coming soon" + +# Placeholder function for 'Chat with Image' feature +def chat_with_image(): + return "Feature coming soon" + +# Function to handle non-streaming output +def no_stream_output(response): + if st.session_state.enable_rag: + output = response.response + source_nodes = response.source_nodes + if ADD_REFERENCES and len(source_nodes) > 0: + similarity_scores = [ + float(node.node.metadata.get("Similarity Score", 0)) for node in source_nodes + ] + if any( + similarity_score >= st.session_state.similarity + for similarity_score in similarity_scores + ): + output += "\n\nRef.:\n\n" + for node in source_nodes: + similarity_score = float(node.node.metadata.get("Similarity Score", 0)) + if similarity_score >= st.session_state.similarity: + output += str(node.node.metadata).replace("{", "").replace("}", "") + " \n" + else: + output = "No reference document with such similarity score found." + else: + output = "No reference document with such similarity score found." + st.markdown(output) + else: + output = response + + return output + +# Streamlit sidebar form for selecting mode and adjusting model parameters +def render_sidebar_forms(): + with st.sidebar.form(key="input-form"): + mode = st.selectbox( + "Select Mode", + ("Enable RAG", "Enable Select AI", "Chat", "Browse", "Chat with Image"), # Added new options + index=0 + ) + st.session_state.selected_mode = mode + + if mode == "Enable RAG": + st.session_state.enable_rag = True + else: + st.session_state.enable_rag = False + + st.session_state.select_model = st.selectbox( + "Select Chat Model", + ("cohere.command-r-16k", "cohere.command-r-plus", + "meta.llama-3.1-405b-instruct", "meta.llama-3.1-70b-instruct"), + index=1 + ) + st.session_state.max_tokens = st.number_input( + 'Maximum Tokens', min_value=512, max_value=1024, step=25, + value=st.session_state.get('max_tokens', 600) + ) + st.session_state.temperature = st.number_input( + 'Temperature', min_value=0.0, max_value=1.0, step=0.1, + value=st.session_state.get('temperature', 0.10) + ) + st.session_state.similarity = st.number_input( + 'Similarity Score', min_value=0.0, max_value=1.0, step=0.05, + value=st.session_state.get('similarity', 0.5) + ) + st.session_state.top_k = st.slider( + "TOP_K", 1, 10, step=1, value=st.session_state.get('top_k', 3) + ) + st.session_state.top_n = st.slider( + "TOP_N", 1, 10, step=1, value=st.session_state.get('top_n', 3) + ) + st.form_submit_button( + "Submit", type="primary", on_click=handle_form_submission, use_container_width=True + ) + +# Display chat messages in the Streamlit app +def display_chat_messages(): + for message in st.session_state.messages: + with st.chat_message(message["role"]): + st.markdown(message["content"]) + +# Main function to run the Streamlit app +def main(): + render_sidebar_forms() + + # Clear Chat History Button + _, c1 = st.columns([5, 1]) + c1.button("Clear Chat History", type="primary", on_click=reset_conversation) + + if "messages" not in st.session_state: + reset_conversation() + + with st.spinner("Initializing..."): + if st.session_state.selected_mode == "Enable Select AI": + pass + elif st.session_state.enable_rag: + st.session_state.chat_engine, st.session_state.token_counter = create_chat_engine() + + display_chat_messages() + + # File Uploader + with st.sidebar.form(key="file-uploader-form", clear_on_submit=True): + file = st.file_uploader( + "Document Uploader", + accept_multiple_files=True, + type=['txt', 'csv', 'pdf'], + label_visibility="collapsed" + ) + submitted = st.form_submit_button( + "Upload", + type="primary", + use_container_width=True, + on_click=reset_conversation + ) + + if submitted and file: + if not isinstance(file, list): + file = [file] + logging.info("Uploading file") + uploaded_file_paths = [] + for uploaded_file in file: + if uploaded_file.name in book_names_set: + st.error( + f"Document {uploaded_file.name} already exists in database. Please try another document or begin asking questions.") + else: + file_path = save_uploaded_file(uploaded_file, Path(upload_dir)) + uploaded_file_paths.append(file_path) + logging.info(f"Uploaded document: {uploaded_file.name}") + + if uploaded_file_paths: + progress_bar = st.progress(0) + progress_text = st.empty() + + try: + with st.spinner(f"Processing document {uploaded_file.name}..."): + logging.info("Document processing started..") + process = subprocess.Popen( + ["python", "process_documents.py"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=1 + ) + + total_steps = 100 + current_step = 0 + + while process.poll() is None: + output = process.stdout.readline() + if output: + current_step += 1 + progress_percentage = min(current_step / total_steps, 1.0) + progress_bar.progress(progress_percentage) + progress_text.text(output.strip()) + time.sleep(0.1) + + for output in process.stdout: + if output: + current_step += 1 + progress_percentage = min(current_step / total_steps, 1.0) + progress_bar.progress(progress_percentage) + progress_text.text(output.strip()) + time.sleep(0.1) + + stdout, stderr = process.communicate() + return_code = process.returncode + if return_code == 0: + progress_bar.progress(1.0) + progress_text.text("Assistant is now ready to answer questions.") + else: + st.error(f"Document processing failed with return code {return_code}") + st.error(stderr) + + except subprocess.CalledProcessError as e: + st.error(f"An error occurred while processing the document: {e}") + + # Render the chat input at the bottom of the app + question = st.chat_input("Hello, how can I help you?") + + if question: + st.chat_message("user").markdown(question) + st.session_state.messages.append({"role": "user", "content": question}) + user_message = ChatMessage(role="user", content=question) + st.session_state.chat_history.append(user_message) + + if st.session_state.selected_mode == "Enable Select AI": + with st.spinner("Processing with Select AI..."): + ai_output = enable_select_ai(question) + with st.chat_message("assistant"): + st.markdown(ai_output) + + elif st.session_state.selected_mode == "Enable RAG": + enable_rag(question) + elif st.session_state.selected_mode == "Chat": + logger.info("Processing Chat mode.") + handle_chat(question) + elif st.session_state.selected_mode == "Browse": + with st.spinner("Processing Browse..."): + browse_output = browse() + st.session_state.messages.append({"role": "assistant", "content": browse_output}) + with st.chat_message("assistant"): + st.markdown(browse_output) + elif st.session_state.selected_mode == "Chat with Image": + with st.spinner("Processing Chat with Image..."): + chat_image_output = chat_with_image() + st.session_state.messages.append({"role": "assistant", "content": chat_image_output}) + with st.chat_message("assistant"): + st.markdown(chat_image_output) + else: + st.error("Invalid mode selected.") + +# Entry point for the script +if __name__ == "__main__": + main() diff --git a/Oracle-23ai-RAG-Chatbot/chat_engine.py b/Oracle-23ai-RAG-Chatbot/chat_engine.py new file mode 100644 index 0000000..240db09 --- /dev/null +++ b/Oracle-23ai-RAG-Chatbot/chat_engine.py @@ -0,0 +1,183 @@ +""" +Python Version: 3.11 + +Description: + This module provides the class to create chat engine and communicate with different LLMs, + Oracle vector store, and llama_index. +""" + +import os +import logging +from tokenizers import Tokenizer +from llama_index.core import VectorStoreIndex, Settings +from llama_index.core.callbacks import CallbackManager, TokenCountingHandler +from llama_index.postprocessor.cohere_rerank import CohereRerank +from llama_index.core.memory import ChatMemoryBuffer +from llama_index.core.callbacks.global_handlers import set_global_handler +from llama_index.llms.oci_genai import OCIGenAI +from llama_index.embeddings.oci_genai import OCIGenAIEmbeddings +from oci_utils import load_oci_config, print_configuration +from llama_index.core.llms import ChatMessage +from config import ( + VERBOSE, + EMBED_MODEL_TYPE, + EMBED_MODEL, + TOKENIZER, + GEN_MODEL, + ADD_RERANKER, + RERANKER_MODEL, + CHAT_MODE, + MEMORY_TOKEN_LIMIT, + ADD_PHX_TRACING, + PHX_PORT, + PHX_HOST, + COMPARTMENT_OCID, + ENDPOINT, + COHERE_API_KEY, + LA2_ENABLE_INDEX, + PROFILE_NAME, + STREAM_CHAT +) +from oci_utils import load_oci_config, print_configuration +from oracle_vectorstore import OracleVectorStore +import streamlit as st +if ADD_PHX_TRACING: + import phoenix as px + +# Configure logger +logger = logging.getLogger("ConsoleLogger") + +# Initialize Phoenix tracing if enabled +if ADD_PHX_TRACING: + os.environ["PHOENIX_PORT"] = PHX_PORT + os.environ["PHOENIX_HOST"] = PHX_HOST + px.launch_app() + set_global_handler("arize_phoenix") + +# Function to create a large language model (LLM) +def create_llm(auth=None): + model_list = ["OCI", "LLAMA"] + + # Validate model choice + llm = None + if GEN_MODEL in ["OCI", "LLAMA"]: + model_name = st.session_state['select_model'] + # model definition + print("model used is ",model_name) + llm = OCIGenAI( + model = model_name, + service_endpoint=ENDPOINT, + compartment_id=COMPARTMENT_OCID, + auth_profile=PROFILE_NAME, # replace with your profile name + ) + + assert llm is not None + return llm + +# Function to create a reranker model +def create_reranker(auth=None, verbose=VERBOSE, top_n=3): + model_list = ["COHERE"] + + # Validate reranker model choice + if RERANKER_MODEL not in model_list: + raise ValueError(f"The value {RERANKER_MODEL} is not supported. Choose a value in {model_list} for the Reranker model.") + + reranker = None + if RERANKER_MODEL == "COHERE": + reranker = CohereRerank(api_key=COHERE_API_KEY, top_n=st.session_state['top_n']) + + return reranker + +# Function to create an embedding model +def create_embedding_model(auth=None): + model_list = ["OCI"] + + # Validate embedding model choice + if EMBED_MODEL_TYPE not in model_list: + raise ValueError(f"The value {EMBED_MODEL_TYPE} is not supported. Choose a value in {model_list} for the model.") + + embed_model = None + if EMBED_MODEL_TYPE == "OCI": + embed_model = OCIGenAIEmbeddings( + auth_profile= PROFILE_NAME, + compartment_id=COMPARTMENT_OCID, + model_name=EMBED_MODEL, + truncate="END", + service_endpoint=ENDPOINT, + ) + return embed_model + +# Function to create the chat engine +def create_chat_engine(token_counter=None, verbose=VERBOSE, top_k=3, max_tokens=1024, temperature=0.2, top_n=3): + logger.info("Calling create_chat_engine()...") + print_configuration() + + # Initialize Phoenix tracing if enabled + if ADD_PHX_TRACING: + set_global_handler("arize_phoenix") + + # Load OCI configuration + oci_config = load_oci_config() + # TODO: change + # api_keys_config = ads.auth.api_keys(oci_config) + + # Create embedding model + embed_model = create_embedding_model() + # Create vector store + vector_store = OracleVectorStore(verbose=verbose, enable_hnsw_indexes=LA2_ENABLE_INDEX) + # Create LLM + llm = create_llm() + + # Initialize tokenizer and token counter + cohere_tokenizer = Tokenizer.from_pretrained(TOKENIZER) + token_counter = TokenCountingHandler(tokenizer=cohere_tokenizer.encode) + + # Configure settings for LLM and embedding model + Settings.embed_model = embed_model + Settings.llm = llm + Settings.callback_manager = CallbackManager([token_counter]) + + # Create vector store index and chat memory buffer + index = VectorStoreIndex.from_vector_store(vector_store=vector_store) + memory = ChatMemoryBuffer.from_defaults(token_limit=MEMORY_TOKEN_LIMIT, tokenizer_fn=cohere_tokenizer.encode) + + # Optionally add a reranker + if ADD_RERANKER: + reranker = create_reranker(top_n=st.session_state['top_n']) + node_postprocessors = [reranker] + else: + node_postprocessors = None + + # Create the chat engine with specified configurations + chat_engine = index.as_chat_engine( + # chat_mode=CHAT_MODE, + memory=memory, + verbose=verbose, + similarity_top_k=top_k, + # llm=llm, + node_postprocessors=node_postprocessors, + streaming=STREAM_CHAT, + ) + + logger.info("") + return chat_engine, token_counter + +# LLM chat function + + +def llm_chat(question): + logger.info("Calling llm_chat()...") + + # Load OCI configuration + oci_config = load_oci_config() + # TODO: change + # api_keys_config = ads.auth.api_keys(oci_config) + + # Create LLM + + llm = create_llm() + print("using the selected model and after create llm function call") + response = llm.chat([ChatMessage(role="user", content=question)]) + + logger.info("Response generated.") + return response diff --git a/Oracle-23ai-RAG-Chatbot/config.py b/Oracle-23ai-RAG-Chatbot/config.py new file mode 100644 index 0000000..4910be8 --- /dev/null +++ b/Oracle-23ai-RAG-Chatbot/config.py @@ -0,0 +1,63 @@ +# Database connection details +DB_USER = "" +DB_PWD = "" +DB_HOST_IP = "" +DB_SERVICE = "" + +# GenAI configurations +COMPARTMENT_OCID = "" +ENDPOINT = "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com" +COHERE_API_KEY = "" + +# Verbosity setting +VERBOSE = False + +# Whether to stream chat messages or not +STREAM_CHAT = False + +# Embedding model type +EMBED_MODEL_TYPE = "OCI" + +# Embedding model for generating embeddings +EMBED_MODEL = "cohere.embed-english-v3.0" + +# Tokenizer for token counting +TOKENIZER = "Cohere/Cohere-embed-multilingual-v3.0" + +# Chunking settings +ENABLE_CHUNKING = True +MAX_CHUNK_SIZE = 1000 +CHUNK_OVERLAP = 100 + +# Generation model +GEN_MODEL = "OCI" + +# Retrieval and reranker settings +TOP_K = 3 +TOP_N = 3 +MAX_TOKENS = 1024 +TEMPERATURE = 0.1 +ADD_RERANKER = False +RERANKER_MODEL = "COHERE" +RERANKER_ID = "" + +# Chat engine settings +CHAT_MODE = "condense_plus_context" +MEMORY_TOKEN_LIMIT = 3000 + +# Bits used to store embeddings +EMBEDDINGS_BITS = 64 + +# ID generation method +ID_GEN_METHOD = "HASH" + +# Tracing settings +ADD_PHX_TRACING = False +PHX_PORT = "7777" +PHX_HOST = "0.0.0.0" + +# Enable approximate query +LA2_ENABLE_INDEX = False + +# UI settings +ADD_REFERENCES = True diff --git a/Oracle-23ai-RAG-Chatbot/create_tables.sql b/Oracle-23ai-RAG-Chatbot/create_tables.sql new file mode 100644 index 0000000..713b798 --- /dev/null +++ b/Oracle-23ai-RAG-Chatbot/create_tables.sql @@ -0,0 +1,17 @@ +create table BOOKS +("ID" NUMBER NOT NULL, +"NAME" VARCHAR2(100) NOT NULL, +PRIMARY KEY ("ID") +); + +create table CHUNKS +("ID" VARCHAR2(64) NOT NULL, +"CHUNK" CLOB, +"VEC" VECTOR(1024, FLOAT64), +"PAGE_NUM" VARCHAR2(10), +"BOOK_ID" NUMBER, +PRIMARY KEY ("ID"), +CONSTRAINT fk_book + FOREIGN KEY (BOOK_ID) + REFERENCES BOOKS (ID) +); \ No newline at end of file diff --git a/Oracle-23ai-RAG-Chatbot/oci_utils.py b/Oracle-23ai-RAG-Chatbot/oci_utils.py new file mode 100644 index 0000000..6410be6 --- /dev/null +++ b/Oracle-23ai-RAG-Chatbot/oci_utils.py @@ -0,0 +1,83 @@ +""" +Python Version: 3.11 + +Description: + This module provides some utilities. +""" + +import logging +import oci +from config import ( + EMBED_MODEL_TYPE, + EMBED_MODEL, + GEN_MODEL, + TOP_K, + ADD_RERANKER, + RERANKER_MODEL, + TOP_N, + ADD_PHX_TRACING, +) + +# Configure logger +logger = logging.getLogger("ConsoleLogger") + +# Function to load OCI configuration +def load_oci_config(): + """ + Load the OCI configuration to connect to OCI using an API key. + + Returns: + dict: The OCI configuration. + """ + # Are you using default profile? + oci_config = oci.config.from_file("~/.oci/config", "DEFAULT") + return oci_config + +# Function to print the current configuration +def print_configuration(): + """ + Print the current configuration settings. + """ + logger.info("------------------------") + logger.info("Configuration used:") + logger.info(f"{EMBED_MODEL_TYPE} {EMBED_MODEL} for embeddings...") + logger.info("Using Oracle AI Vector Search...") + logger.info(f"Using {GEN_MODEL} as LLM...") + logger.info("Retrieval parameters:") + logger.info(f"TOP_K: {TOP_K}") + + if ADD_RERANKER: + logger.info(f"Using {RERANKER_MODEL} as reranker...") + logger.info(f"TOP_N: {TOP_N}") + if ADD_PHX_TRACING: + logger.info(f"Enabled observability with Phoenix tracing...") + + logger.info("------------------------") + logger.info("") + +# Function to pretty print documents +def pretty_print_docs(docs): + """ + Pretty print the contents of the documents. + + Args: + docs (list): A list of documents to print. + """ + print( + f"\n{'-' * 100}\n".join( + [f"Document {i+1}:\n\n" + d.page_content for i, d in enumerate(docs)] + ) + ) + +# Function to format documents as a single string +def format_docs(docs): + """ + Format the documents as a single string. + + Args: + docs (list): A list of documents to format. + + Returns: + str: The formatted string containing all document contents. + """ + return "\n\n".join(doc.page_content for doc in docs) diff --git a/Oracle-23ai-RAG-Chatbot/oracle_vectorstore.py b/Oracle-23ai-RAG-Chatbot/oracle_vectorstore.py new file mode 100644 index 0000000..6f5b9c1 --- /dev/null +++ b/Oracle-23ai-RAG-Chatbot/oracle_vectorstore.py @@ -0,0 +1,311 @@ +""" +Python Version: 3.11 + +Description: + This module provides the class to integrate Oracle Vector DB + as Vector Store in llama-index. +""" + +import time +from tqdm import tqdm +import array +from typing import List, Any, Dict +from contextlib import contextmanager +import streamlit as st +from llama_index.core.vector_stores.types import ( + VectorStore, + VectorStoreQuery, + VectorStoreQueryResult, +) + +from llama_index.core.schema import TextNode, BaseNode + +import oracledb +import logging + +# Load configurations from the config module +from config import ( + DB_USER, + DB_PWD, + DB_HOST_IP, + DB_SERVICE, + EMBEDDINGS_BITS, + ADD_PHX_TRACING, + DSN, + WALLET_LOCATION, + WALLET_PASSWORD, + CONFIG_DIR +) + +# Phoenix tracing setup if enabled +if ADD_PHX_TRACING: + from opentelemetry import trace as trace_api + from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter + from opentelemetry.sdk import trace as trace_sdk + from opentelemetry.sdk.trace.export import SimpleSpanProcessor + from openinference.semconv.trace import SpanAttributes + from opentelemetry.trace import Status, StatusCode + +logger = logging.getLogger("ConsoleLogger") + +# Initialize Phoenix tracer if tracing is enabled +tracer = None +if ADD_PHX_TRACING: + endpoint = "http://127.0.0.1:7777/v1/traces" + tracer_provider = trace_sdk.TracerProvider() + trace_api.set_tracer_provider(tracer_provider) + tracer_provider.add_span_processor(SimpleSpanProcessor(OTLPSpanExporter(endpoint))) + tracer = trace_api.get_tracer(__name__) + OPENINFERENCE_SPAN_KIND = "openinference.span.kind" + +@contextmanager +def optional_tracing(span_name): + """ + Context manager for optional Phoenix tracing. + + Args: + span_name (str): The name of the tracing span. + + Yields: + span: The tracing span context if tracing is enabled, otherwise None. + """ + if ADD_PHX_TRACING: + with tracer.start_as_current_span(name=span_name) as span: + span.set_attribute(OPENINFERENCE_SPAN_KIND, "Retriever") + span.set_attribute(SpanAttributes.TOOL_NAME, "oracle_vector_store") + span.set_attribute(SpanAttributes.TOOL_DESCRIPTION, "Oracle DB 23ai") + span.set_status(Status(StatusCode.OK)) + yield span + else: + yield None + +def oracle_query(embed_query: List[float], top_k: int, verbose=True, approximate=False): + """ + Executes a query against an Oracle database to find the top_k closest vectors to the given embedding. + + Args: + embed_query (List[float]): A list of floats representing the query vector embedding. + top_k (int): The number of closest vectors to retrieve. + verbose (bool, optional): If set to True, additional information about the query and execution time will be printed. Defaults to True. + approximate (bool, optional): If set to True, use approximate (index-based) query for faster results. Defaults to False. + + Returns: + VectorStoreQueryResult: Object containing the query results, including nodes, similarities, and ids. + """ + start_time = time.time() # Record the start time of the query execution for performance monitoring + + # Define the Data Source Name (DSN) for connecting to the Oracle database. + # DSN = f"{DB_HOST_IP}/{DB_SERVICE}" + + try: + # Establish a connection to the Oracle database using credentials and connection parameters. + with oracledb.connect(user=DB_USER, password=DB_PWD, dsn=DSN, + wallet_location=WALLET_LOCATION, config_dir=CONFIG_DIR, + wallet_password=WALLET_PASSWORD) as connection: + print("Successfully connected to the database in oracle query.") + # Open a cursor object to execute the SQL query. + with connection.cursor() as cursor: + print("Cursor created successfully.") + + # Determine the array type based on the precision of the embedding values. + array_type = "d" if EMBEDDINGS_BITS == 64 else "f" + + # Convert the query embedding into a binary array format. + array_query = array.array(array_type, embed_query) + + # Define the clause to use approximate search if `approximate` is set to True. + approx_clause = "APPROXIMATE" if approximate else "" + + # Construct the SQL query to retrieve the top_k closest vectors. + select = f""" + SELECT C.ID, + C.CHUNK, + C.PAGE_NUM, + VECTOR_DISTANCE(C.VEC,:1, COSINE) as d, + B.NAME + FROM CHUNKS C, BOOKS B + WHERE C.BOOK_ID = B.ID + ORDER BY 4 + FETCH {approx_clause} FIRST {top_k} ROWS ONLY + """ + + # If verbose is True, log the constructed SQL query. + + # Execute the SQL query using the array_query as the parameter. + cursor.execute(select, [array_query]) + print("SQL query executed successfully.") + + # Fetch all rows returned by the query. + rows = cursor.fetchall() + + # Lists to store the results: nodes, node ids, and similarities. + result_nodes, node_ids, similarities = [], [], [] + + # Iterate over each row to process and filter the results based on similarity score. + for row in rows: + print(f"Processing row: {row}") + # Check if the similarity score meets a threshold stored in the session state. + if 1 - (row[3]) >= st.session_state['similarity']: + print(f"Row passed similarity threshold: {row}") + full_clob_data = row[1].read() # Read the CLOB data (text content) from the database. + result_nodes.append( + TextNode( + id_=row[0], + text=full_clob_data, + metadata={"file_name": row[4], "page#": row[2], "Similarity Score": 1 - (row[3])}, + ) + ) + node_ids.append(row[0]) + similarities.append(row[3]) + print(f"Added node with ID: {row[0]}, Similarity: {1 - (row[3])}") + else: + print(f"Row did not meet similarity threshold: {row}") + + except Exception as e: + # Log and print any errors that occur during the database operations. + print(f"Error occurred in oracle_query: {e}") + logger.error(f"Error occurred in oracle_query: {e}") + return None + + # Create the result object to return the query results. + q_result = VectorStoreQueryResult( + nodes=result_nodes, similarities=similarities, ids=node_ids + ) + + # Calculate and log the elapsed time for the query execution. + elapsed_time = time.time() - start_time + print(f"Query execution completed in {elapsed_time:.2f} seconds.") + + if verbose: + print(f"Verbose mode: Query duration was {round(elapsed_time, 1)} seconds.") + + return q_result + + +def save_chunks_with_embeddings_in_db(pages_id,pages_text, pages_num,embeddings, book_id, connection): + """ + Save chunk texts and their embeddings into the database. + + :param pages_text: List of text chunks. + :param pages_id: List of IDs for the chunks. + :param pages_num: List of page numbers corresponding to the chunks. + :param embeddings: List of tuples (id, embedding_vector) for the embeddings. + :param book_id: The ID of the book to which the chunks belong. + :param connection: Database connection object. + """ + tot_errors = 0 + try: + with connection.cursor() as cursor: + logging.info("Saving texts and embeddings to DB...") + cursor.setinputsizes(None, oracledb.DB_TYPE_CLOB) + + for id, text, page_num, vector in zip(tqdm(pages_id), pages_text,pages_num,embeddings): + # Determine the type of array based on embeddings precision + array_type = "d" if EMBEDDINGS_BITS == 64 else "f" + input_array = array.array(array_type, vector) + try: + cursor.execute( + "INSERT INTO CHUNKS (ID, CHUNK,VEC, PAGE_NUM, BOOK_ID) VALUES (:1, :2, :3, :4, :5)", + [id, text,input_array, page_num, book_id] + ) + except Exception as e: + logging.error(f"Error in save_chunks_with_embeddings: {e}") + tot_errors += 1 + + logging.info(f"Total errors in save_chunks_with_embeddings: {tot_errors}") + except Exception as e: + logging.error(f"Critical error in save_chunks_with_embeddings_in_db: {e}") + raise + +class OracleVectorStore(VectorStore): + """ + Class to interface with Oracle DB Vector Store. + """ + + stores_text: bool = True + verbose: bool = False + + def __init__(self, verbose=False, enable_hnsw_indexes=False) -> None: + """ + Initialize OracleVectorStore with optional verbosity and HNSW index support. + + Args: + verbose (bool, optional): Enable verbose logging. Defaults to False. + enable_hnsw_indexes (bool, optional): Enable HNSW indexes for approximate search. Defaults to False. + """ + self.verbose = verbose + self.enable_hnsw_indexes = enable_hnsw_indexes + self.node_dict: Dict[str, BaseNode] = {} + + def add(self, nodes: List[BaseNode]) -> List[str]: + """ + Add nodes to the index. + + Args: + nodes (List[BaseNode]): List of nodes to add. + + Returns: + List[str]: List of node IDs added. + """ + ids_list = [] + for node in nodes: + self.node_dict[node.id_] = node + ids_list.append(node.id_) + + return ids_list + + def delete(self, node_id: str, **delete_kwargs: Any) -> None: + """ + Delete nodes from the index (not implemented). + + Args: + node_id (str): The ID of the node to delete. + """ + raise NotImplementedError("Delete not yet implemented for Oracle Vector Store.") + + def query(self, query: VectorStoreQuery, **kwargs: Any) -> VectorStoreQueryResult: + """ + Query the Oracle DB Vector Store. + + Args: + query (VectorStoreQuery): The query to execute. + + Returns: + VectorStoreQueryResult: The query result. + """ + similarity_top_k = st.session_state['top_k'] + + if self.verbose: + logging.info("---> Calling query on DB with top_k={}".format(similarity_top_k)) + + with optional_tracing("oracle_vector_db"): + return oracle_query( + query.query_embedding, + top_k=similarity_top_k, + verbose=self.verbose, + approximate=self.enable_hnsw_indexes, + ) + + def persist(self, persist_path=None, fs=None) -> None: + """ + Persist the VectorStore to the Oracle database. + """ + if self.node_dict: + logging.info("Persisting to DB...") + + embeddings = [] + pages_id = [] + pages_text = [] + pages_num = [] + + for key, node in self.node_dict.items(): + pages_id.append(node.id_) + pages_text.append(node.text) + embeddings.append(node.embedding) + pages_num.append(node.metadata["page#"]) + + with oracledb.connect(user=DB_USER, password=DB_PWD, dsn=self.DSN, config_dir=WALLET_LOCATION) as connection: + save_chunks_with_embeddings_in_db(pages_id, pages_text,pages_num, embeddings, book_id=None, connection=connection) + connection.commit() + + self.node_dict = {} diff --git a/Oracle-23ai-RAG-Chatbot/process_documents.py b/Oracle-23ai-RAG-Chatbot/process_documents.py new file mode 100644 index 0000000..f95b5e9 --- /dev/null +++ b/Oracle-23ai-RAG-Chatbot/process_documents.py @@ -0,0 +1,423 @@ +""" +Python Version: 3.11 + +Description: + This module provides the class to process, embed, store, and retrieve + documents info from Oracle 23ai. +""" + +import logging +import re +import os +import shutil +from typing import List +from tqdm import tqdm +import array +import numpy as np +import time +import hashlib + +from llama_index.core import SimpleDirectoryReader +from llama_index.core.node_parser import SentenceSplitter +import oracledb +from tokenizers import Tokenizer +from llama_index.embeddings.oci_genai import OCIGenAIEmbeddings +from oci_utils import load_oci_config + +from config import ( + EMBED_MODEL, + TOKENIZER, + EMBEDDINGS_BITS, + ID_GEN_METHOD, + ENABLE_CHUNKING, + MAX_CHUNK_SIZE, + CHUNK_OVERLAP, + DB_USER, + DB_PWD, + DB_SERVICE, + DB_HOST_IP, + COMPARTMENT_OCID, + ENDPOINT, + DSN, + WALLET_LOCATION, + WALLET_PASSWORD, + CONFIG_DIR +) + +BATCH_SIZE = 40 + +def generate_id(nodes_list: List): + """ + Generate IDs for the given list of nodes. + + Args: + nodes_list (List): List of nodes. + + Returns: + List: List of generated node IDs. + """ + try: + if ID_GEN_METHOD == "LLINDEX": + nodes_ids = [doc.id_ for doc in nodes_list] + elif ID_GEN_METHOD == "HASH": + logging.info("Hashing to compute id...") + nodes_ids = [] + for doc in tqdm(nodes_list): + encoded_text = doc.text.encode() + hash_object = hashlib.sha256(encoded_text) + hash_hex = hash_object.hexdigest() + nodes_ids.append(hash_hex) + else: + raise ValueError(f"Unknown ID_GEN_METHOD: {ID_GEN_METHOD}") + return nodes_ids + except Exception as e: + logging.error(f"Error in generate_id: {e}") + raise + +def read_and_split_in_pages(input_files): + """ + Read and split input files into pages. + + Args: + input_files: List of input files. + + Returns: + Tuple: Tuple containing lists of page texts, page IDs, and page numbers. + """ + try: + pages = SimpleDirectoryReader(input_files=input_files).load_data() + logging.info(f"Read total {len(pages)} pages...") + for doc in pages: + doc.text = preprocess_text(doc.text) + pages = remove_short_pages(pages, threshold=10) + pages_text = [doc.text for doc in pages] + pages_num = [doc.metadata["page_label"] for doc in pages] + pages_id = generate_id(pages) + return pages_text, pages_id, pages_num + except Exception as e: + logging.error(f"Error in read_and_split_in_pages: {e}") + raise + +def preprocess_text(text): + """ + Preprocess the given text by removing unwanted characters and formatting. + + Args: + text: The input text. + + Returns: + str: The preprocessed text. + """ + try: + # Remove unwanted characters and format the text + text = text.replace("\t", " ") + text = text.replace(" -\n", "") + text = text.replace("-\n", "") + text = text.replace("\n", " ") + text = re.sub(r"\s+", " ", text) # Replace multiple spaces with a single space + text = text.strip() # Trim leading and trailing whitespace + + # Check if the text is empty after preprocessing + if not text: + logging.warning("Preprocessed text is empty.") + return None + + return text + except Exception as e: + logging.error(f"Error in preprocess_text: {e}") + raise + +def remove_short_pages(pages, threshold): + """ + Remove pages that are shorter than the specified word threshold. + + Args: + pages: List of pages. + threshold: Word count threshold. + + Returns: + List: List of pages with sufficient length. + """ + try: + n_removed = 0 + for pag in pages: + if len(pag.text.split(" ")) < threshold: + pages.remove(pag) + n_removed += 1 + logging.info(f"Removed {n_removed} short pages...") + return pages + except Exception as e: + logging.error(f"Error in remove_short_pages: {e}") + raise + +def read_and_split_in_chunks(input_files): + """ + Read and split input files into chunks. + + Args: + input_files: List of input files. + + Returns: + Tuple: Tuple containing lists of node texts, node IDs, and page numbers. + """ + try: + node_parser = SentenceSplitter(chunk_size=MAX_CHUNK_SIZE, chunk_overlap=CHUNK_OVERLAP) + pages = SimpleDirectoryReader(input_files=input_files).load_data() + logging.info(f"Read total {len(pages)} pages...") + + pages = [doc for doc in pages if hasattr(doc, 'text') and doc.text.strip()] + + # Check if the document has content after filtering + if pages: + logging.info(f"Total {len(pages)} pages after removing empty ones...") + for doc in pages: + doc.text = preprocess_text(doc.text) + else: + logging.info("No non-empty pages found after filtering.") + pages = remove_short_pages(pages, threshold=10) + nodes = node_parser.get_nodes_from_documents(pages, show_progress=True) + nodes_text = [doc.text for doc in nodes] + pages_num = [doc.metadata.get("page_label", "unknown") for doc in nodes] + nodes_id = generate_id(nodes) + return nodes_text, nodes_id, pages_num + except Exception as e: + logging.error(f"Error in read_and_split_in_chunks: {e}") + raise + +def check_tokenization_length(tokenizer, batch): + """ + Check the length of tokenized texts to ensure they do not exceed the maximum chunk size. + + Args: + tokenizer: The tokenizer to use. + batch: List of texts to tokenize. + """ + try: + for text in tqdm(batch): + assert len(tokenizer.encode(text)) <= MAX_CHUNK_SIZE + logging.info("Tokenization OK...") + except Exception as e: + logging.error(f"Error in check_tokenization_length: {e}") + raise + +def compute_embeddings(embed_model, nodes_text): + """ + Compute embeddings for the given texts using the specified embedding model. + + Args: + embed_model: The embedding model to use. + nodes_text: List of texts to embed. + + Returns: + List: List of computed embeddings. + """ + try: + cohere_tokenizer = Tokenizer.from_pretrained(TOKENIZER) + embeddings = [] + for i in tqdm(range(0, len(nodes_text), BATCH_SIZE)): + batch = nodes_text[i : i + BATCH_SIZE] + embeddings_batch = embed_model.get_text_embedding_batch(batch) + embeddings.extend(embeddings_batch) + print(f"Processed {i + len(batch)} of {len(nodes_text)} documents") + time.sleep(0.1) # Simulate some processing delay + # Ensure stdout is flushed + print(f"Batch {i // BATCH_SIZE + 1} embeddings computed", flush=True) + return embeddings + except Exception as e: + logging.error(f"Error in compute_embeddings: {e}") + raise + +def save_chunks_with_embeddings_in_db(pages_id,pages_text, pages_num,embeddings, book_id, connection): + """ + Save chunk texts and their embeddings into the database. + + :param pages_text: List of text chunks. + :param pages_id: List of IDs for the chunks. + :param pages_num: List of page numbers corresponding to the chunks. + :param embeddings: List of tuples (id, embedding_vector) for the embeddings. + :param book_id: The ID of the book to which the chunks belong. + :param connection: Database connection object. + """ + tot_errors = 0 + try: + with connection.cursor() as cursor: + logging.info("Saving texts and embeddings to DB...") + cursor.setinputsizes(None, oracledb.DB_TYPE_CLOB) + + for id, text, page_num, vector in zip(tqdm(pages_id), pages_text,pages_num,embeddings): + # Determine the type of array based on embeddings precision + array_type = "d" if EMBEDDINGS_BITS == 64 else "f" + input_array = array.array(array_type, vector) + try: + cursor.execute( + "INSERT INTO CHUNKS (ID, CHUNK,VEC, PAGE_NUM, BOOK_ID) VALUES (:1, :2, :3, :4, :5)", + [id, text,input_array, page_num, book_id] + ) + except Exception as e: + logging.error(f"Error in save_chunks_with_embeddings: {e}") + tot_errors += 1 + + logging.info(f"Total errors in save_chunks_with_embeddings: {tot_errors}") + except Exception as e: + logging.error(f"Critical error in save_chunks_with_embeddings_in_db: {e}") + raise + + +def register_book(book_name, connection): + """ + Register a book in the database. + + Args: + book_name: The name of the book. + connection: The Oracle database connection. + + Returns: + int: The ID of the registered book. + """ + try: + with connection.cursor() as cursor: + cursor.execute("SELECT MAX(ID) FROM BOOKS") + row = cursor.fetchone() + new_key = row[0] + 1 if row[0] is not None else 1 + with connection.cursor() as cursor: + query = "INSERT INTO BOOKS (ID, NAME) VALUES (:1, :2)" + cursor.execute(query, [new_key, book_name]) + return new_key + except Exception as e: + logging.error(f"Error in register_book: {e}") + raise + +def get_files_from_directory(directory): + """ + Get the list of files from the specified directory. + + Args: + directory: The directory to list files from. + + Returns: + List: List of file paths. + """ + try: + files = [os.path.join(directory, f) for f in os.listdir(directory) if os.path.isfile(os.path.join(directory, f)) and not f.startswith('.')] + return files + except Exception as e: + logging.error(f"Error in get_files_from_directory: {e}") + raise + +def move_files(files, dest_directory): + """ + Move the specified files to the destination directory. + + Args: + files: List of file paths to move. + dest_directory: The destination directory. + """ + + try: + for file in files: + dest_path = os.path.join(dest_directory, os.path.basename(file)) + if os.path.exists(dest_path): + os.replace(file, dest_path) # Overwrite the existing file + else: + shutil.move(file, dest_directory) # Move if the file doesn't exist + logging.info(f"Moved {len(files)} files to {dest_directory}.") + except Exception as e: + logging.error(f"Error in move_files: {e}") + raise + +def ensure_directories_exist(directories): + """ + Ensure the specified directories exist, creating them if necessary. + + Args: + directories: List of directory paths to check/create. + """ + try: + for directory in directories: + if not os.path.exists(directory): + os.makedirs(directory) + logging.info(f"Created directory: {directory}") + except Exception as e: + logging.error(f"Error in ensure_directories_exist: {e}") + raise + +def main(): + """ + Main function to process, embed, store, and retrieve documents info from Oracle 23ai. + """ + tot_pages = 0 # Initialize total pages at the beginning + time_start = time.time() + logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") + + data_dir = os.path.join("data") + unprocessed_dir = os.path.join(data_dir, "unprocessed") + processed_dir = os.path.join(data_dir, "processed") + + ensure_directories_exist([data_dir, unprocessed_dir, processed_dir]) + + try: + files_to_process = get_files_from_directory(unprocessed_dir) + if not files_to_process: + raise Exception("Unprocessed directory is empty, there is nothing to process") + + print("\nStart processing...\n") + print("List of books to be loaded and indexed:") + for file in files_to_process: + print(file) + print("") + + # TODO: do we even need this line anymore? + # oci_config = load_oci_config() + # api_keys_config = ads.auth.api_keys(oci_config) + + embed_model = OCIGenAIEmbeddings( + compartment_id=COMPARTMENT_OCID, + model_name=EMBED_MODEL, + truncate="END", + service_endpoint=ENDPOINT, + ) + + logging.info("Connecting to Oracle 23ai DB...") + + with oracledb.connect(user=DB_USER, password=DB_PWD, dsn=DSN, config_dir=CONFIG_DIR, wallet_location=WALLET_LOCATION, wallet_password=WALLET_PASSWORD) as connection: + logging.info("Successfully connected to Oracle 23ai Database...") + + num_pages = [] + for book in files_to_process: + book_name = os.path.basename(book) + logging.info(f"Processing book: {book_name}...") + + if not ENABLE_CHUNKING: + logging.info("Chunks are pages of the book...") + nodes_text, nodes_id, pages_num = read_and_split_in_pages([book]) + num_pages.append(len(nodes_text)) + else: + logging.info(f"Enabled chunking, chunk_size: {MAX_CHUNK_SIZE}...") + nodes_text, nodes_id, pages_num = read_and_split_in_chunks([book]) + + logging.info("Computing embeddings...") + embeddings = compute_embeddings(embed_model, nodes_text) + + logging.info("Registering book...") + book_id = register_book(book_name, connection) + + save_chunks_with_embeddings_in_db(nodes_id, nodes_text,pages_num, embeddings, book_id, connection) + connection.commit() + logging.info("Save texts OK...") + + tot_pages = np.sum(np.array(num_pages)) + if tot_pages is None or tot_pages == 0: + tot_pages = 0 + move_files(files_to_process, processed_dir) + except Exception as e: + logging.error(f"Critical error in main: {e}") + raise + finally: + time_elapsed = time.time() - time_start + print("\nProcessing done !!!") + print(f"We have processed {tot_pages} pages and saved text chunks and embeddings in the DB") + print(f"Total elapsed time: {round(time_elapsed, 0)} sec.") + print() + +if __name__ == "__main__": + main() diff --git a/Oracle-23ai-RAG-Chatbot/requirements.txt b/Oracle-23ai-RAG-Chatbot/requirements.txt new file mode 100644 index 0000000..71e0c48 --- /dev/null +++ b/Oracle-23ai-RAG-Chatbot/requirements.txt @@ -0,0 +1,238 @@ +oracledb +aiohttp==3.9.3 +aiosignal==1.3.1 +altair==5.2.0 +annotated-types==0.6.0 +anyio==4.3.0 +appnope==0.1.4 +argon2-cffi==23.1.0 +argon2-cffi-bindings==21.2.0 +arize-phoenix==3.16.0 +arrow==1.3.0 +asteval==0.9.32 +asttokens==2.4.1 +async-lru==2.0.4 +attrs==23.2.0 +Babel==2.14.0 +backoff==2.2.1 +beautifulsoup4==4.12.3 +black==24.3.0 +bleach==6.1.0 +blinker==1.7.0 +bs4==0.0.2 +cachetools==5.3.3 +Cerberus==1.3.5 +certifi==2024.2.2 +cffi==1.16.0 +charset-normalizer==3.3.2 +circuitbreaker==1.4.0 +click==8.1.7 +cloudpickle==3.0.0 +cohere==4.55 +comm==0.2.2 +contourpy==1.2.0 +cryptography==42.0.5 +cycler==0.12.1 +Cython==0.29.37 +dataclasses-json==0.6.4 +ddsketch==2.0.4 +debugpy==1.8.1 +decorator==5.1.1 +defusedxml==0.7.1 +Deprecated==1.2.14 +dirtyjson==1.0.8 +distro==1.9.0 +executing==2.0.1 +fastavro==1.9.4 +fastjsonschema==2.19.1 +filelock==3.13.1 +fonttools==4.49.0 +fqdn==1.5.1 +frozenlist==1.4.1 +fsspec==2024.2.0 +gitdb==4.0.11 +GitPython==3.1.42 +googleapis-common-protos==1.63.0 +graphql-core==3.2.3 +greenlet==3.0.3 +grpcio==1.62.1 +h11==0.14.0 +hdbscan==0.8.33 +httpcore==1.0.4 +httpx==0.25.2 +huggingface-hub==0.21.4 +idna==3.6 +importlib-metadata==6.11.0 +inflection==0.5.1 +ipykernel==6.29.3 +ipython==8.22.2 +isoduration==20.11.0 +jedi==0.19.1 +Jinja2==3.1.3 +joblib==1.3.2 +json5==0.9.22 +jsonpatch==1.33 +jsonpointer==2.4 +jsonschema==4.21.1 +jsonschema-specifications==2023.12.1 +jupyter-events==0.9.1 +jupyter-lsp==2.2.4 +jupyter_client==8.6.1 +jupyter_core==5.7.2 +jupyter_server==2.13.0 +jupyter_server_terminals==0.5.3 +jupyterlab==4.1.4 +jupyterlab_pygments==0.3.0 +jupyterlab_server==2.25.4 +kiwisolver==1.4.5 +langchain +langchain-community +langchain-core +langchain-text-splitters==0.0.1 +langsmith==0.1.25 +llama-index +llama-index-agent-openai==0.1.5 +llama-index-callbacks-arize-phoenix==0.1.4 +llama-index-cli +llama-index-core +llama-index-embeddings-langchain==0.1.2 +llama-index-embeddings-oci-genai +llama-index-llms-oci-genai +llama-index-embeddings-openai==0.1.6 +llama-index-indices-managed-llama-cloud==0.1.4 +llama-index-legacy==0.9.48 +llama-index-llms-anyscale==0.1.3 +llama-index-llms-cohere==0.1.3 +llama-index-llms-langchain==0.1.3 +llama-index-llms-mistralai==0.1.6 +llama-index-llms-openai==0.1.9 +llama-index-multi-modal-llms-openai==0.1.4 +llama-index-postprocessor-cohere-rerank==0.1.2 +llama-index-program-openai==0.1.4 +llama-index-question-gen-openai==0.1.3 +llama-index-readers-file==0.1.11 +llama-index-readers-llama-parse==0.1.3 +llama-parse==0.3.9 +llamaindex-py-client==0.1.13 +llvmlite==0.42.0 +Markdown==3.5.2 +markdown-it-py==3.0.0 +MarkupSafe==2.1.5 +marshmallow==3.21.1 +matplotlib==3.8.3 +matplotlib-inline==0.1.6 +mdurl==0.1.2 +mistralai==0.1.6 +mistune==3.0.2 +multidict==6.0.5 +mypy-extensions==1.0.0 +nbclient==0.10.0 +nbconvert==7.16.2 +nbformat==5.10.2 +nest-asyncio==1.6.0 +networkx==3.2.1 +nltk==3.8.1 +notebook_shim==0.2.4 +numba==0.59.0 +numpy==1.26.4 +oci +ocifs +openai==1.14.0 +openinference-instrumentation-langchain==0.1.12 +openinference-instrumentation-llama-index==1.2.0 +openinference-instrumentation-openai==0.1.4 +openinference-semantic-conventions==0.1.5 +opentelemetry-api==1.23.0 +opentelemetry-exporter-otlp==1.23.0 +opentelemetry-exporter-otlp-proto-common==1.23.0 +opentelemetry-exporter-otlp-proto-grpc==1.23.0 +opentelemetry-exporter-otlp-proto-http==1.23.0 +opentelemetry-instrumentation==0.44b0 +opentelemetry-proto==1.23.0 +opentelemetry-sdk==1.23.0 +opentelemetry-semantic-conventions==0.44b0 +orjson==3.9.15 +overrides==7.7.0 +packaging==23.2 +pandas==2.2.1 +pandocfilters==1.5.1 +parso==0.8.3 +pathspec==0.12.1 +pexpect==4.9.0 +pillow==10.2.0 +platformdirs==4.2.0 +prometheus_client==0.20.0 +prompt-toolkit==3.0.43 +protobuf==4.25.3 +psutil==5.9.8 +ptyprocess==0.7.0 +pure-eval==0.2.2 +pyarrow==15.0.1 +pycparser==2.21 +pydantic==2.6.4 +pydantic_core==2.16.3 +pydeck==0.8.1b0 +Pygments==2.17.2 +PyMuPDF==1.23.26 +PyMuPDFb==1.23.22 +pynndescent==0.5.11 +pyOpenSSL==24.1.0 +pyparsing==3.1.2 +pypdf==4.1.0 +python-dateutil==2.9.0.post0 +python-json-logger==2.0.7 +python-jsonschema-objects==0.5.4 +pytz==2024.1 +PyYAML==6.0.1 +pyzmq==25.1.2 +referencing==0.33.0 +regex==2023.12.25 +requests==2.31.0 +rfc3339-validator==0.1.4 +rfc3986-validator==0.1.1 +rich==13.7.1 +rpds-py==0.18.0 +scikit-learn==1.4.1.post1 +scipy==1.12.0 +Send2Trash==1.8.2 +six==1.16.0 +smmap==5.0.1 +sniffio==1.3.1 +sortedcontainers==2.4.0 +soupsieve==2.5 +SQLAlchemy==2.0.28 +stack-data==0.6.3 +starlette==0.37.2 +strawberry-graphql==0.208.2 +streamlit==1.32.2 +striprtf==0.0.26 +tabulate==0.9.0 +tenacity==8.2.3 +terminado==0.18.1 +threadpoolctl==3.3.0 +tiktoken==0.6.0 +tinycss2==1.2.1 +tokenize-rt==5.2.0 +tokenizers==0.15.2 +toml==0.10.2 +toolz==0.12.1 +tornado==6.4 +tqdm==4.66.2 +traitlets==5.14.2 +types-python-dateutil==2.8.19.20240311 +typing-inspect==0.9.0 +typing_extensions==4.10.0 +tzdata==2024.1 +umap-learn==0.5.5 +uri-template==1.3.0 +urllib3==2.2.1 +uvicorn==0.28.0 +watchdog==4.0.0 +wcwidth==0.2.13 +webcolors==1.13 +webencodings==0.5.1 +websocket-client==1.7.0 +wrapt==1.16.0 +yarl==1.9.4 +zipp==3.18.0 +llama-index-llms-openai-like diff --git a/Oracle-23ai-RAG-Chatbot/screenshot.png b/Oracle-23ai-RAG-Chatbot/screenshot.png new file mode 100644 index 0000000..7d4d6c9 Binary files /dev/null and b/Oracle-23ai-RAG-Chatbot/screenshot.png differ