docs/docs/genai/flavors/langchain/notebooks/langchain-retriever.ipynb
Welcome to this tutorial, where we explore the integration of Retrieval Augmented Generation (RAG) with MLflow and LangChain. Our focus is on demonstrating how to create advanced RAG systems and showcasing the unique capabilities enabled by MLflow in these applications.
Retrieval Augmented Generation (RAG) combines the power of language model generation with information retrieval, allowing language models to access and incorporate external data. This approach significantly enriches the model's responses with detailed and context-specific information.
MLflow is instrumental in this process. As an open-source platform, it facilitates the logging, tracking, and deployment of complex models, including RAG chains. With MLflow, integrating LangChain becomes more streamlined, enhancing the development, evaluation, and deployment processes of RAG models.
NOTE: In this tutorial, we'll be using GPT-3.5 as our base language model. It's important to note that the results obtained from a RAG system will differ from those obtained by interfacing directly with GPT models. RAG's unique approach of combining external data retrieval with language model generation creates more nuanced and contextually rich responses.
By the end of this tutorial, you will learn:
In order to have a place to store our vetted data (the information that we're going to be retrieving), we're going to use a Vector Database. The framework that we're choosing to use (due to its simplicity, capabilities, and free-to-use characteristics) is FAISS, from Meta.
For this tutorial, we will be utilizing FAISS (Facebook AI Similarity Search, developed and maintained by the Meta AI research group), an efficient similarity search and clustering library. It's a highly useful library that easily handles large datasets and is capable of performing operations such as nearest neighbor search, which are critical in Retrieval Augmented Generation (RAG) systems. There are numerous other vector database solutions that can perform similar functionality; we are using FAISS in this tutorial due to its simplicity, ease of use, and fantastic performance.
With rapidly changing libraries such as langchain, examples can become outdated rather quickly and will no longer work. For the purposes of demonstration, here are the critical dependencies that are recommended to use to effectively run this notebook:
| Package | Version |
|---|---|
| langchain | 0.1.16 |
| lanchain-community | 0.0.33 |
| langchain-openai | 0.0.8 |
| openai | 1.12.0 |
| tiktoken | 0.6.0 |
| mlflow | 2.12.1 |
| faiss-cpu | 1.7.4 |
If you attempt to execute this notebook with different versions, it may function correctly, but it is recommended to use the precise versions above to ensure that your code executes properly.
Before proceeding with the tutorial, ensure that you have FAISS and Beautiful Soup installed via pip. The version specifiers for other packages are guaranteed to work with this notebook. Other versions of these packages may not function correctly due to breaking changes their APIs.
pip install beautifulsoup4 faiss-cpu==1.7.4 langchain==0.1.16 langchain-community==0.0.33 langchain-openai==0.0.8 openai==1.12.0 tiktoken==0.6.0
NOTE: If you'd like to run this using your GPU, you can install
faiss-gpuinstead.
import os
import shutil
import tempfile
import requests
from bs4 import BeautifulSoup
from langchain.chains import RetrievalQA
from langchain.document_loaders import TextLoader
from langchain.text_splitter import CharacterTextSplitter
from langchain.vectorstores import FAISS
from langchain_openai import OpenAI, OpenAIEmbeddings
import mlflow
assert "OPENAI_API_KEY" in os.environ, "Please set the OPENAI_API_KEY environment variable."
NOTE: If you'd like to use Azure OpenAI with LangChain, you need to install
openai>=1.10.0andlangchain-openai>=0.0.6, as well as to specify the following credentials and parameters:
from langchain_openai import AzureOpenAI, AzureOpenAIEmbeddings
# Set this to `azure`
os.environ["OPENAI_API_TYPE"] = "azure"
# The API version you want to use: set this to `2023-05-15` for the released version.
os.environ["OPENAI_API_VERSION"] = "2023-05-15"
assert "AZURE_OPENAI_ENDPOINT" in os.environ, (
"Please set the AZURE_OPENAI_ENDPOINT environment variable. It is the base URL for your Azure OpenAI resource. You can find this in the Azure portal under your Azure OpenAI resource."
)
assert "OPENAI_API_KEY" in os.environ, (
"Please set the OPENAI_API_KEY environment variable. It is the API key for your Azure OpenAI resource. You can find this in the Azure portal under your Azure OpenAI resource."
)
azure_openai_llm = AzureOpenAI(
deployment_name="<your-deployment-name>",
model_name="gpt-4o-mini",
)
azure_openai_embeddings = AzureOpenAIEmbeddings(
azure_deployment="<your-deployment-name>",
)
In this section of the tutorial, we will demonstrate how to scrape content from federal document webpages for use in our RAG system. We'll be focusing on extracting transcripts from specific sections of webpages, which will then be used to feed our Retrieval Augmented Generation (RAG) model. This process is crucial for providing the RAG system with relevant external data.
fetch_federal_document is designed to scrape and return the transcript of specific federal documents.url (the webpage URL) and div_class (the class of the div element containing the transcript).This step is integral to building a RAG system that relies on external, context-specific data. By effectively fetching and processing this data, we can enrich our model's responses with accurate information directly sourced from authoritative documents.
NOTE: In a real-world scenario, you would have your specific text data located on disk somewhere (either locally or on your cloud provider) and the process of loading the embedded data into a vector search database would be entirely external to this active fetching displayed below. We're simply showing the entire process here for demonstration purposes to show the entire end-to-end workflow for interfacing with a RAG model.
def fetch_federal_document(url, div_class):
"""
Scrapes the transcript of the Act Establishing Yellowstone National Park from the given URL.
Args:
url (str): URL of the webpage to scrape.
Returns:
str: The transcript text of the Act.
"""
# Sending a request to the URL
response = requests.get(url)
if response.status_code == 200:
# Parsing the HTML content of the page
soup = BeautifulSoup(response.text, "html.parser")
# Finding the transcript section by its HTML structure
if transcript_section := soup.find("div", class_=div_class):
transcript_text = transcript_section.get_text(separator="\n", strip=True)
return transcript_text
else:
return "Transcript section not found."
else:
return f"Failed to retrieve the webpage. Status code: {response.status_code}"
In this next part, we focus on two key processes:
Document Fetching:
fetch_and_save_documents to retrieve documents from specified URLs.FAISS Database Creation:
create_faiss_database is responsible for creating a FAISS database from the documents saved in the previous step.TextLoader to load the text, CharacterTextSplitter for document splitting, and OpenAIEmbeddings for generating embeddings.These functions streamline the process of gathering relevant documents and setting up a FAISS database, essential for implementing advanced Retrieval-Augmented Generation (RAG) applications in MLflow. By modularizing these steps, we ensure code reusability and maintainability.
def fetch_and_save_documents(url_list, doc_path):
"""
Fetches documents from given URLs and saves them to a specified file path.
Args:
url_list (list): List of URLs to fetch documents from.
doc_path (str): Path to the file where documents will be saved.
"""
for url in url_list:
document = fetch_federal_document(url, "col-sm-9")
with open(doc_path, "a") as file:
file.write(document)
def create_faiss_database(document_path, database_save_directory, chunk_size=500, chunk_overlap=10):
"""
Creates and saves a FAISS database using documents from the specified file.
Args:
document_path (str): Path to the file containing documents.
database_save_directory (str): Directory where the FAISS database will be saved.
chunk_size (int, optional): Size of each document chunk. Default is 500.
chunk_overlap (int, optional): Overlap between consecutive chunks. Default is 10.
Returns:
FAISS database instance.
"""
# Load documents from the specified file
document_loader = TextLoader(document_path)
raw_documents = document_loader.load()
# Split documents into smaller chunks with specified size and overlap
document_splitter = CharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap)
document_chunks = document_splitter.split_documents(raw_documents)
# Generate embeddings for each document chunk
embedding_generator = OpenAIEmbeddings()
faiss_database = FAISS.from_documents(document_chunks, embedding_generator)
# Save the FAISS database to the specified directory
faiss_database.save_local(database_save_directory)
return faiss_database
This section of the tutorial deals with the setup for our Retrieval-Augmented Generation (RAG) application. We'll establish the working environment and create the necessary FAISS database:
Temporary Directory Creation:
tempfile.mkdtemp(). This directory serves as a workspace for storing our documents and the FAISS database.Document Path and FAISS Index Directory:
Document Fetching:
url_listings) containing the documents we need to fetch.fetch_and_save_documents function is used to retrieve and save the documents from these URLs into a single file located at doc_path.FAISS Database Creation:
create_faiss_database function is then called to create a FAISS database from the saved documents, using the default chunk_size and chunk_overlap values.vector_db) is crucial for the RAG process, as it enables efficient similarity searches on the loaded documents.By the end of this process, we have all documents consolidated in a single location and a FAISS database ready to be used for retrieval purposes in our MLflow-enabled RAG application.
temporary_directory = tempfile.mkdtemp()
doc_path = os.path.join(temporary_directory, "docs.txt")
persist_dir = os.path.join(temporary_directory, "faiss_index")
url_listings = [
"https://www.archives.gov/milestone-documents/act-establishing-yellowstone-national-park#transcript",
"https://www.archives.gov/milestone-documents/sherman-anti-trust-act#transcript",
]
fetch_and_save_documents(url_listings, doc_path)
vector_db = create_faiss_database(doc_path, persist_dir)
In this final setup phase, we focus on creating the RetrievalQA chain and integrating it with MLflow:
Initializing the RetrievalQA Chain:
RetrievalQA chain is initialized using the OpenAI language model (llm) and the retriever from our previously created FAISS database (vector_db.as_retriever()).Loader Function for Retrieval:
load_retriever function is defined to load the retriever from the FAISS database saved in the specified directory.Logging the Model with MLflow:
mlflow.langchain.log_model.artifact_path, the loader_fn for the retriever, and the persist_dir where the FAISS database is stored.Through these steps, we successfully integrate a complex RAG application with MLflow, showcasing its capability to handle advanced NLP tasks.
mlflow.set_experiment("Legal RAG")
retrievalQA = RetrievalQA.from_llm(llm=OpenAI(), retriever=vector_db.as_retriever())
# Log the retrievalQA chain
def load_retriever(persist_directory):
embeddings = OpenAIEmbeddings()
vectorstore = FAISS.load_local(
persist_directory,
embeddings,
allow_dangerous_deserialization=True, # This is required to load the index from MLflow
)
return vectorstore.as_retriever()
with mlflow.start_run() as run:
model_info = mlflow.langchain.log_model(
retrievalQA,
name="retrieval_qa",
loader_fn=load_retriever,
persist_dir=persist_dir,
)
IMPORTANT: In order to load a stored vectorstore instance such as our FAISS instance above, we need to specify within the load function the argument
allow_dangeous_deserializationtoTruein order for the load to succeed. This is due to a safety warning that was introduced inlangchainfor loading objects that have been serialized usingpickleorcloudpickle. While this issue of remote code execution is not a risk with using MLflow, as the serialization and deserialization happens entirely via API and within your environment, the argument must be set in order to prevent an Exception from being thrown at load time.
Now that we have the model stored in MLflow, we can load it back as a pyfunc and see how well it answers a few critically important questions about these acts of Congress in America.
loaded_model = mlflow.pyfunc.load_model(model_info.model_uri)
def print_formatted_response(response_list, max_line_length=80):
"""
Formats and prints responses with a maximum line length for better readability.
Args:
response_list (list): A list of strings representing responses.
max_line_length (int): Maximum number of characters in a line. Defaults to 80.
"""
for response in response_list:
words = response.split()
line = ""
for word in words:
if len(line) + len(word) + 1 <= max_line_length:
line += word + " "
else:
print(line)
line = word + " "
print(line)
Let's try out our Retriever model by sending it a fairly simple but purposefully vague question.
answer1 = loaded_model.predict([{"query": "What does the document say about trespassers?"}])
print_formatted_response(answer1)
With this model, our approach combines text retrieval from a database with language model generation to answer specific queries.
loaded_model.predict([{"query": "What does the document say about trespassers?"}]), the model first searches the database for parts of the document that are most relevant to the query about trespassers.This section of the tutorial showcases an interesting aspect of the RetrievalQA model's capabilities, particularly in handling queries that involve both specific information retrieval and additional context generation.
Combining Document Data with LLM Context:
Enhanced Contextual Understanding:
Why This Is Notable:
answer2 = loaded_model.predict([
{"query": "What is a bridle-path and can I use one at Yellowstone?"}
])
print_formatted_response(answer2)
In this section of our tutorial, we delve into a whimsically ridiculous query and how our model tackles it with a blend of accuracy and a hint of humor.
Direct and No-Nonsense Response:
Understanding Legal Boundaries:
Contrast with Traditional LLM Responses:
So, while you can't buy Yellowstone for your buffalo-themed spa dreams, you can certainly enjoy the park's natural beauty... just as a visitor, not as a spa owner!
answer3 = loaded_model.predict([
{
"query": "Can I buy Yellowstone from the Federal Government to set up a buffalo-themed day spa?"
}
])
print_formatted_response(answer3)
In this part of our tutorial, we explore another amusing question about leasing land in Yellowstone for a buffalo-themed day spa. Let's see how our model, with unflappable composure, responds to this quirky inquiry.
Factual and Unwavering:
A Lawyer's Patience Tested?:
In conclusion, while our model firmly closes the door on the buffalo-themed day spa dreams, it does so with informative grace, demonstrating its ability to stay on course no matter how imaginative the queries get.
answer4 = loaded_model.predict([
{
"query": "Can I lease a small parcel of land from the Federal Government for a small "
"buffalo-themed day spa for visitors to the park?"
}
])
print_formatted_response(answer4)
Once more, we pose an imaginative question to our model, this time adding a hotel to the buffalo-themed day spa scenario. The reason for this modification is to evaluate whether the RAG application can discern a nuanced element of the intentionally vague wording of the two acts that we've loaded. Let's see the response to determine if it can resolve both bits of information!
answer5 = loaded_model.predict([
{
"query": "Can I lease a small parcel of land from the Federal Government for a small "
"buffalo-themed day spa and hotel for visitors to stay in and relax at while visiting the park?"
}
])
print_formatted_response(answer5)
answer6 = loaded_model.predict([
{"query": "Can I just go to the park and peacefully enjoy the natural splendor?"}
])
print_formatted_response(answer6)
This section of the tutorial showcases the RetrievalQA model's sophisticated ability to integrate and interpret context from multiple legal documents. The model is challenged with a query that requires synthesizing information from distinct sources. This test is particularly interesting for its demonstration of the model's proficiency in:
Contextual Integration: The model adeptly pulls in relevant legal details from different documents, illustrating its capacity to navigate through multiple sources of information.
Legal Interpretation: It interprets the legal implications related to the query, highlighting the model's understanding of complex legal language and concepts.
Cross-Document Inference: The model's ability to discern and extract the most pertinent information from a pool of multiple documents is a testament to its advanced capabilities in multi-document scenarios.
This evaluation provides a clear example of the model's potential in handling intricate queries that necessitate a deep and nuanced understanding of diverse data sources.
answer7 = loaded_model.predict([
{
"query": "Can I start a buffalo themed day spa outside of Yellowstone National Park and stifle any competition?"
}
])
print_formatted_response(answer7)
Cleanup: Removing Temporary Directory:
shutil.rmtree.# Clean up our temporary directory that we created with our FAISS instance
shutil.rmtree(temporary_directory)
In this tutorial, we explored the depths of Retrieval Augmented Generation (RAG) applications, enabled and simplified by MLflow. Here's a recap of our journey and the key takeaways:
Ease of Developing RAG Applications: We learned how MLflow facilitates the development of RAG applications by streamlining the process of integrating large language models with external data sources. Our hands-on experience demonstrated the process of fetching, processing, and embedding documents into a FAISS database, all managed within the MLflow framework.
Advanced Query Handling: Through our tests, we observed how the MLflow-wrapped LangChain RAG model adeptly handled complex queries, drawing from multiple documents to provide context-rich and accurate responses. This showcased the potential of RAG models in processing and understanding queries that require multi-source data integration.
MLflow's Role in Deployment and Management: MLflow's robustness was evident in how it simplifies the logging, deployment, and management of complex models. Its ability to track experiments, manage artifacts, and ease the deployment process highlights its indispensability in the AI development workflow.
Practical Application Insights: Our queries, while humorous at times, served to illustrate the practical capabilities of RAG models. From legal interpretations to hypothetical scenarios, we saw how these models could be applied in real-world situations, providing insightful and contextually relevant responses.
The Future of RAG and MLflow: This tutorial underscored the potential of combining RAG with MLflow's streamlined management capabilities. As RAG continues to evolve, MLflow stands out as a crucial tool for harnessing its power, making advanced NLP applications more accessible and efficient.
In summary, our journey through this tutorial has not only equipped us with the knowledge to develop and deploy RAG applications effectively but also opened our eyes to the vast possibilities that lie ahead in the realm of advanced NLP, all made more attainable through MLflow.
If you'd like to learn more about how MLflow and LangChain integrate, see the other advanced tutorials for MLflow's LangChain flavor.