A lightweight Retrieval-Augmented Generation (RAG) pipeline that ingests a PDF, chunks it intelligently, embeds it with Google's Gemini embedding model, stores/searches it in Pinecone, and now answers questions end-to-end using a tool-calling LangChain agent.
mini-rag-system/
├── venv/ # Virtual environment (not tracked in git)
├── .env # API keys (Google, Pinecone) — not tracked in git
├── .gitignore
├── fullstack_guide.pdf # Source PDF to be ingested
├── main.py # Core script: ingestion + RAG agent
└── README.md
- Loads a PDF (
fullstack_guide.pdf) usingPyPDFLoader - Cleans the extracted text
- Splits it into overlapping chunks using
RecursiveCharacterTextSplitter - Embeds each chunk with Google's
gemini-embedding-001model - Stores the embeddings in a Pinecone vector index
- Searches the index semantically (not just keyword matching!)
- Retrieves + Generates: a LangChain agent calls a custom
getcontexttool to pull relevant chunks from Pinecone, then uses Gemini 2.5 Flash to generate a grounded answer
pip install langchain langchain-google-genai langchain-pinecone langchain-community langchain-text-splitters pinecone python-dotenv pypdfGOOGLE_API_KEY=your_google_api_key_here
PINECONE_API_KEY=your_pinecone_api_key_hereMake sure you have an index named rag-index1 with:
- Dimension:
3072(matchesgemini-embedding-001output) - Metric:
cosine
Drop your PDF file in the project root and update the filename in the script:
loader = PyPDFLoader("./fullstack_guide.pdf")python main.pyUnder the hood, similarity search against Pinecone looks like this:
vector_store.similarity_search(query="version control with git", k=1)This returns the most relevant chunk(s) from your PDF based on meaning, not exact keyword matches.
Instead of calling similarity_search manually, the project now wraps retrieval in a tool and hands it to a LangChain agent powered by Gemini 2.5 Flash:
@tool
def getcontext(query: str):
"""Use this tool to get more information to fulfill the user's request."""
result = vector_store.similarity_search(query=query, k=2)
return str(result)
model = ChatGoogleGenerativeAI(model="gemini-2.5-flash")
agent = create_agent(
model=model,
tools=[getcontext],
system_prompt=(
"You must ALWAYS call the getcontext tool first to retrieve "
"relevant information before answering any question. "
"Base your answer only on the retrieved context."
)
)
response = agent.invoke({
"messages": [HumanMessage("What is Fullstack development?")]
})Why a tool instead of a direct call?
- The agent decides when to retrieve, rather than retrieval being hardcoded into every query.
- The
system_promptenforces a tool-first workflow, so the model is grounded in your Pinecone data instead of answering from its own training knowledge (reduces hallucination). - This sets up the project for multi-tool agents later (web search, calculators, etc.) without changing the core retrieval logic.
index.delete(delete_all=True)throws aNotFoundExceptionif the namespace is empty/new — wrap it in atry/exceptif running on a fresh index.- Vector writes to Pinecone aren't instantly searchable — add a short
time.sleep()afteradd_documents()if you're querying right away. - Chunk boundaries aren't perfect with character-based splitting — a chunk might blend two topics together if they're short and close together. This is normal; retrieval + LLM generation downstream usually compensates for it.
create_agent()expectssystem_prompt, notprompt— passingprompt=raises aTypeError.- The agent only calls
getcontextif it judges the question needs it. For very generic questions, it may answer from its own knowledge unless the system prompt explicitly forces tool-first behavior (as done above).