Retrieval Augmented Generation (RAG) and Vector Databases
Source: Notebook Rag Vector Databases · GitHub · microsoft/generative-ai-for-beginners Authors: Microsoft (microsoft) Licence: MIT — https://spdx.org/licenses/MIT.html
!pip install openai
import os
import pandas as pd
import numpy as np
import openai
Creating our Knowledge base
Creating a Azure Cosmos DB database
pip install azure-cosmos
## create your cosmoss db on Azure CLI using the following commands
## az login
## az group create -n <resource-group-name> -l <location>
## az cosmosdb create -n <cosmos-db-name> -r <resource-group-name>
## az cosmosdb list-keys -n <cosmos-db-name> -g <resource-group-name>
## Once done navigate to data explorer and create a new database and a new container
from azure.cosmos import CosmosClient
# Initialize Cosmos Client
url = os.getenv('COSMOS_DB_ENDPOINT')
key = os.getenv('COSMOS_DB_KEY')
client = CosmosClient(url, credential=key)
# Select database
database_name = 'rag-cosmos-db'
database = client.get_database_client(database_name)
# Select container
container_name = 'data'
container = database.get_container_client(container_name)
import pandas as pd
# splitting our data into chunks
data_paths = ["data/frameworks.md?WT.mc_id=academic-105485-koreyst", "data/own_framework.md?WT.mc_id=academic-105485-koreyst", "data/perceptron.md?WT.mc_id=academic-105485-koreyst"]
rows = []
for path in data_paths:
with open(path, 'r', encoding='utf-8') as file:
file_content = file.read()
# Collect the file path and text
rows.append({'path': path, 'text': file_content})
# Build the DataFrame (DataFrame.append was removed in pandas 2.0, so we use a list + constructor)
df = pd.DataFrame(rows, columns=['path', 'text'])
df.head()
def split_text(text, max_length, min_length):
words = text.split()
chunks = []
current_chunk = []
for word in words:
current_chunk.append(word)
if len(' '.join(current_chunk)) < max_length and len(' '.join(current_chunk)) > min_length:
chunks.append(' '.join(current_chunk))
current_chunk = []
# If the last chunk didn't reach the minimum length, add it anyway
if current_chunk:
chunks.append(' '.join(current_chunk))
return chunks
# Assuming analyzed_df is a pandas DataFrame and 'output_content' is a column in that DataFrame
splitted_df = df.copy()
splitted_df['chunks'] = splitted_df['text'].apply(lambda x: split_text(x, 400, 300))
splitted_df
# Assuming 'chunks' is a column of lists in the DataFrame splitted_df, we will split the chunks into different rows
flattened_df = splitted_df.explode('chunks')
flattened_df.head()
Converting our text to embeddings
Converting out text to embeddings, and storing them in our database in chunks
from openai import OpenAI
# Configure the Azure OpenAI (Microsoft Foundry) client using the v1 endpoint,
# which powers the Responses API. Get the endpoint and key from your Foundry
# resource, and the deployment names from the models you deployed in the portal.
endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
client = OpenAI(
api_key=os.getenv("AZURE_OPENAI_API_KEY"),
base_url=f"{endpoint.rstrip('/')}/openai/v1/",
)
embeddings_deployment = os.getenv("AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT")
chat_deployment = os.getenv("AZURE_OPENAI_DEPLOYMENT")
def create_embeddings(text, model=None):
# Create embeddings for each document chunk using your embeddings deployment
model = model or embeddings_deployment
embeddings = client.embeddings.create(input=text, model=model).data[0].embedding
return embeddings
#embeddings for the first chunk
create_embeddings(flattened_df['chunks'][0])
cat = create_embeddings("cat")
cat
# create embeddings for the whole data chunks and store them in a list
embeddings = []
for chunk in flattened_df['chunks']:
embeddings.append(create_embeddings(chunk))
# store the embeddings in the dataframe
flattened_df['embeddings'] = embeddings
flattened_df.head()
Retrieval
Vector search and similarity between our prompt and the database
Creating an search index and reranking
from sklearn.neighbors import NearestNeighbors
embeddings = flattened_df['embeddings'].to_list()
# Create the search index
nbrs = NearestNeighbors(n_neighbors=5, algorithm='ball_tree').fit(embeddings)
# To query the index, you can use the kneighbors method
distances, indices = nbrs.kneighbors(embeddings)
# Store the indices and distances in the DataFrame
flattened_df['indices'] = indices.tolist()
flattened_df['distances'] = distances.tolist()
flattened_df.head()
# Your text question
question = "what is a perceptron?"
# Convert the question to a query vector
query_vector = create_embeddings(question) # You need to define this function
# Find the most similar documents
distances, indices = nbrs.kneighbors([query_vector])
index = []
# Print the most similar documents
for i in range(3):
index = indices[0][i]
for index in indices[0]:
print(flattened_df['chunks'].iloc[index])
print(flattened_df['path'].iloc[index])
print(flattened_df['distances'].iloc[index])
else:
print(f"Index {index} not found in DataFrame")
Putting it all together to answer a question
# The Azure OpenAI (Microsoft Foundry) client was already configured above.
# We reuse the same `client`, `embeddings_deployment` and `chat_deployment` here.
user_input = "what is a perceptron?"
def chatbot(user_input):
# Convert the question to a query vector
query_vector = create_embeddings(user_input)
# Find the most similar documents
distances, indices = nbrs.kneighbors([query_vector])
# add documents to query to provide context
history = []
for index in indices[0]:
history.append(flattened_df['chunks'].iloc[index])
# combine the history and the user input
history.append(user_input)
# create a message object
messages=[
{"role": "system", "content": "You are an AI assistant that helps with AI questions."},
{"role": "user", "content": history[-1]}
]
# use the Responses API to generate a response
response = client.responses.create(
model=chat_deployment,
max_output_tokens=800,
input=messages,
store=False,
)
return response.output_text
chatbot(user_input)
Testing and evaluation
A basic example of how you can use Mean Average Precision (MAP) to evaluate the responses of your model based on their relevance.
from sklearn.metrics import average_precision_score
# Define your test cases
test_cases = [
{
"query": "What is a perceptron?",
"relevant_responses": ["A perceptron is a type of artificial neuron.", "It's a binary classifier used in machine learning."],
"irrelevant_responses": ["A perceptron is a type of fruit.", "It's a type of car."]
},
{
"query": "What is machine learning?",
"relevant_responses": ["Machine learning is a method of data analysis that automates analytical model building.", "It's a branch of artificial intelligence based on the idea that systems can learn from data, identify patterns and make decisions with minimal human intervention."],
"irrelevant_responses": ["Machine learning is a type of fruit.", "It's a type of car."]
},
{
"query": "What is deep learning?",
"relevant_responses": ["Deep learning is a subset of machine learning in artificial intelligence (AI) that has networks capable of learning unsupervised from data that is unstructured or unlabeled.", "It's a type of machine learning."],
"irrelevant_responses": ["Deep learning is a type of fruit.", "It's a type of car."]
},
{
"query": "What is a neural network?",
"relevant_responses": ["A neural network is a series of algorithms that endeavors to recognize underlying relationships in a set of data through a process that mimics the way the human brain operates.", "It's a type of machine learning."],
"irrelevant_responses": ["A neural network is a type of fruit.", "It's a type of car."]
}
]
# Initialize the total average precision
total_average_precision = 0
# Test the RAG application
for test_case in test_cases:
query = test_case["query"]
relevant_responses = test_case["relevant_responses"]
irrelevant_responses = test_case["irrelevant_responses"]
# Generate a response using your RAG application
response = chatbot(query)
# Create a list of all responses and a list of true binary labels
all_responses = relevant_responses + irrelevant_responses
true_labels = [1] * len(relevant_responses) + [0] * len(irrelevant_responses)
# Create a list of predicted scores based on whether the response is the generated response
predicted_scores = [1 if resp == response else 0 for resp in all_responses]
# Calculate the average precision for this query
average_precision = average_precision_score(true_labels, predicted_scores)
# Add the average precision to the total average precision
total_average_precision += average_precision
# Calculate the mean average precision
mean_average_precision = total_average_precision / len(test_cases)
mean_average_precision