Installationยถ
!pip install langgraph langchain-community langchain-openai langchain-qdrant pypdfium2
!pip install trulens "trulens-benchmark"
!pip install trulens-providers-openai
!pip install trulens-apps-langgraph
Initial Setupยถ
Install the required libraries and configure API keys for OpenAI and Qdrant. A TruLens session is initialized here to track and store all evaluation data throughout the notebook.
- Get OpenAI API Key: https://platform.openai.com/
- Get Qdrant API Key and Endpoint URL: https://cloud.qdrant.io/
import os
from google.colab import userdata
os.environ["OPENAI_API_KEY"] = userdata.get("OPENAI_API_KEY")
QDRANT_URL = userdata.get("QDRANT_URL")
QDRANT_API_KEY = userdata.get("QDRANT_API_KEY")
Overviewยถ
We will generate a dataset to evaluate the RAG pipeline built using LangGraph and Qdrant, and assess it using the RAG Triad evaluation metrics.
from langchain_community.document_loaders import PyPDFium2Loader
from langchain_openai import OpenAIEmbeddings
from langchain_qdrant import QdrantVectorStore
from langchain_text_splitters import RecursiveCharacterTextSplitter
from qdrant_client import QdrantClient
from qdrant_client import models
from trulens.core import TruSession
session = TruSession()
session.reset_database()
Data loading and chunkingยถ
The Economic Survey 2025-26 report is downloaded and parsed using PyPDFium2. It is then split into overlapping chunks using Langchain's RecursiveCharacterTextSplitter to prepare the text for embedding and retrieval.
Economic Survey 2025-26 by Government of India: Survey Report
!curl -L -A "Mozilla/5.0" https://www.indiabudget.gov.in/economicsurvey/doc/eschapter/epreface.pdf -o survey.pdf
path = "survey.pdf"
loader = PyPDFium2Loader(path)
docs = loader.load()
print(len(docs))
print(docs[1].page_content)
print(docs[1].metadata)
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1536,
chunk_overlap=0,
)
chunks = text_splitter.split_documents(docs)
Setup your Qdrant Vector Databaseยถ
A Qdrant collection is configured using dense vectors generated from OpenAI embeddings, enabling semantic search based on meaning and contextual similarity. Each document or chunk is converted into its corresponding embedding and indexed within the collection along with its associated metadata.
During retrieval, the query is embedded into the same vector space and matched against the indexed vectors to return the most semantically relevant results.
embeddings_model = OpenAIEmbeddings(model="text-embedding-3-small")
print(len(embeddings_model.embed_query("this is testing to check dimensions")))
collection_name = "economicv1"
client = QdrantClient(url=QDRANT_URL, api_key=QDRANT_API_KEY)
client.create_collection(
collection_name=collection_name,
vectors_config={
"dense": models.VectorParams(size=1536, distance=models.Distance.COSINE)
},
)
db = QdrantVectorStore(
client=client,
collection_name=collection_name,
embedding=embeddings_model,
vector_name="dense",
)
Test the retriverยถ
db.add_documents(
documents=chunks
) # just run it once. After the data is indexed, you need not have to add_documents again
query = "what is Financial Time wrote on the eve of Christmas 2025"
relevant_docs = db.max_marginal_relevance_search(query)
relevant_docs[0].page_content
Convert to the retriever: i.e., Langchain object
retriever = db.as_retriever()
print(retriever.invoke(query)[0].page_content)
Build your LangGraph RAG workflow - Answer Generationยถ
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model_name="gpt-5-mini")
SYSTEM_PROMPT = """
You are an expert financial analyst specializing in corporate finance.
You should be respectful and truthful while answering the user questions, if not you will face serious consequences.
The only source information you have is the context provided, if the user query is not from the context
Just say `I dont know , not enough information provided.`
"""
USER_PROMPT = """
Answer the USER QUERY based on the CONTEXT below.
If the question cannot be answered using the information provided answer with `I dont know , not enough information provided.`
<context>
CONTEXT: {context}
</context>
<query>
USER QUERY: {query}
</query>
"""
from typing import List, TypedDict
from langgraph.graph import END
from langgraph.graph import START
from langgraph.graph import StateGraph # node and edges logic
from trulens.core.otel.instrument import instrument
from trulens.otel.semconv.trace import SpanAttributes
class RAGState(TypedDict):
query: str
context: List[str]
answer: str
@instrument(
span_type=SpanAttributes.SpanType.RETRIEVAL,
attributes={
SpanAttributes.RETRIEVAL.QUERY_TEXT: "query",
SpanAttributes.RETRIEVAL.RETRIEVED_CONTEXTS: "return",
},
)
def retrieve_docs(query: str) -> list:
results = db.max_marginal_relevance_search(query=query, k=4)
return [result.page_content for result in results]
@instrument(span_type=SpanAttributes.SpanType.GENERATION)
def generate_answer(context: str, query: str) -> str:
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{
"role": "user",
"content": USER_PROMPT.format(context=context, query=query),
},
]
response = llm.invoke(messages)
return response.content
@instrument(
span_type=SpanAttributes.SpanType.RECORD_ROOT,
attributes={
SpanAttributes.RECORD_ROOT.INPUT: "question",
SpanAttributes.RECORD_ROOT.OUTPUT: "return",
},
)
def execute_graph(question: str) -> str:
result = graph.invoke({"query": question})
return result["answer"]
def search(state: RAGState) -> RAGState:
state["context"] = retrieve_docs(state["query"])
return state
def answer(state: RAGState) -> RAGState:
context = "\n".join(state["context"])
state["answer"] = generate_answer(context, state["query"])
return state
# Agent - workflows
workflow = StateGraph(RAGState)
workflow.add_node("search_context", search)
workflow.add_node("answer_generation", answer)
workflow.add_edge(START, "search_context")
workflow.add_edge("search_context", "answer_generation")
workflow.add_edge("answer_generation", END)
graph = workflow.compile()
graph
print(execute_graph(query))
Generate Testset using TruLensยถ
TruLens allows you to generate a test set of a specified breadth and depth, tailored to your app and data. The resulting test set will be a list of test prompts of length depth, for breadth categories of prompts. This test set will be made up of breadth X depth prompts organized by prompt category.
from trulens.benchmark.generate.generate_test_set import GenerateTestSet
from trulens.providers.openai import OpenAI
test = GenerateTestSet(app_callable=execute_graph)
provider = OpenAI()
test_set = test.generate_test_set(
test_breadth=3,
test_depth=2,
)
# breadth = 3 โ generate 3 categories
# depth = 2 โ generate 2 questions
test_set
Run Evalsยถ
Evaluate with TruLens feedbacks
import numpy as np
from trulens.apps.langgraph import TruGraph
from trulens.core import Metric
from trulens.core import Selector
f_answer_relevance = Metric(
implementation=provider.relevance_with_cot_reasons,
name="Answer Relevance",
selectors={
"prompt": Selector.select_record_input(),
"response": Selector.select_record_output(),
},
)
f_context_relevance = Metric(
implementation=provider.context_relevance_with_cot_reasons,
name="Context Relevance",
selectors={
"question": Selector.select_record_input(),
"context": Selector(
span_type=SpanAttributes.SpanType.RETRIEVAL,
span_attribute=SpanAttributes.RETRIEVAL.RETRIEVED_CONTEXTS,
collect_list=False,
),
},
agg=np.mean,
)
f_groundedness = Metric(
implementation=provider.groundedness_measure_with_cot_reasons,
name="Groundedness",
selectors={
"source": Selector(
span_type=SpanAttributes.SpanType.RETRIEVAL,
span_attribute=SpanAttributes.RETRIEVAL.RETRIEVED_CONTEXTS,
collect_list=True,
),
"statement": Selector.select_record_output(),
},
)
tru_recorder = TruGraph(
execute_graph,
app_name="evals_result",
app_version="v1",
feedbacks=[f_groundedness, f_answer_relevance, f_context_relevance],
)
with tru_recorder as recording:
for topic, questions in test_set.items():
for question in questions:
execute_graph(question)
session.get_leaderboard()
# Check what spans are actually being recorded
records = session.get_records_and_feedback()[0]
records