Learning · · 5 min read

Adding PDF Ingestion to My Python Chatbot with Gemini

A step‑by‑step guide on how I taught my Python chatbot to read PDFs, turn the pages into embeddings with Gemini, and retrieve answers in real time.

Laptop screen showing Python code extracting text from a PDF file

A few weeks ago I asked myself how to let my chatbot answer questions that live inside a PDF document. The answer was simple: I needed to "add pdf ingestion to python chatbot" so it could turn each page into text, embed that text with Gemini, and search the embeddings when a user asks something. I already had a basic chatbot set up from earlier experiments, so the next step was to plug in a PDF pipeline. If you’re curious about the environment I use, check out my guide on setting up a Python AI development environment.

why add pdf ingestion to python chatbot

Most of the time my bot talks about things I feed it directly – short prompts, JSON payloads, or a few lines of text. Real‑world documents, however, are often stored as PDFs: user manuals, government forms, research papers. Without a way to read those files the bot can’t help users who need exact information from a report. Adding PDF ingestion also lets me reuse the same vector‑search logic I built for plain text, which means I don’t have to reinvent the wheel.

setup and required tools

  • Python 3.10+ installed locally
  • VS Code with Claude Code extension (helps me write snippets faster)
  • Supabase account (free tier is enough for a few thousand vectors)
  • Gemini API key (free tier gives 30 K tokens per month)
  • PyMuPDF (pip install pymupdf) for PDF text extraction
  • Requests library for HTTP calls

step 1: extract text from PDFs with PyMuPDF

PyMuPDF gives me a quick way to read each page of a PDF and pull out plain text. I wrap the logic in a small function so I can reuse it for any document. The function also does a tiny amount of cleaning – removing extra new‑lines and trimming whitespace – because Gemini works best with clean chunks.

import fitz  # PyMuPDF

def extract_pages(pdf_path: str) -> list[str]:
    """Return a list where each element is the cleaned text of a PDF page."""
    doc = fitz.open(pdf_path)
    pages = []
    for page in doc:
        text = page.get_text("text")
        # simple cleanup
        cleaned = " ".join(text.split())
        pages.append(cleaned)
    return pages

step 2: create Gemini embeddings for the pages

Gemini offers an embeddings endpoint that turns a string into a high‑dimensional vector. I send each page as a separate request because the API has a max token limit per call. The resulting vectors are stored in a list that mirrors the page order, which later lets me point the user to the exact page number.

import os, json, requests

GEMINI_URL = "https://generativelanguage.googleapis.com/v1beta/models/embedding-001:batchEmbedContents"
API_KEY = os.getenv("GEMINI_API_KEY")

def embed_pages(pages: list[str]) -> list[dict]:
    payload = {
        "model": "embedding-001",
        "instances": [{"content": {"role": "user", "parts": [{"text": p}]}} for p in pages]
    }
    headers = {"Content-Type": "application/json", "Authorization": f"Bearer {API_KEY}"}
    response = requests.post(GEMINI_URL, headers=headers, json=payload)
    response.raise_for_status()
    data = response.json()
    # each embedding is under ['embedding']['values']
    return [item["embedding"]["values"] for item in data["embeddings"]]

step 3: store embeddings in Supabase vector table

Supabase now supports pgvector, which means I can keep each page’s embedding in a regular table and run a similarity search with a single SQL query. I created a table called "pdf_pages" with columns for the document ID, page number, raw text, and the vector. If you want to see a similar setup for plain‑text vectors, have a look at my post on adding vector search to my Python chatbot with Supabase.

from supabase import create_client

SUPABASE_URL = os.getenv("SUPABASE_URL")
SUPABASE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY")
client = create_client(SUPABASE_URL, SUPABASE_KEY)

def store_embeddings(doc_id: str, pages: list[str], vectors: list[list[float]]):
    for i, (text, vec) in enumerate(zip(pages, vectors), start=1):
        client.table("pdf_pages").insert({
            "doc_id": doc_id,
            "page_number": i,
            "content": text,
            "embedding": vec
        }).execute()

step 4: query the chatbot with user questions

When a user asks a question, I first embed the query with Gemini, then ask Supabase for the top‑3 most similar page vectors. The bot replies with the page text and also mentions the page number so the user can open the original PDF if needed. The flow feels natural because the chatbot never mentions vectors – it just says, “According to page 4 …”.

def answer_question(doc_id: str, question: str) -> str:
    # embed the question
    q_vec = embed_pages([question])[0]
    # similarity search – pgvector uses <-> operator
    sql = (
        "SELECT page_number, content FROM pdf_pages "
        "WHERE doc_id = %s ORDER BY embedding <-> %s LIMIT 3"
    )
    rows = client.rpc("sql", {"query": sql, "params": [doc_id, q_vec]}).execute()
    # build a short answer
    snippets = "\n\n".join([f"Page {r["page_number"]}: {r["content"][:200]}…" for r in rows.data])
    return f"I found these sections that might help:\n{snippets}"

testing the integration and next steps

I tried the pipeline with a 30‑page user manual from a local government office. The bot answered specific questions like “What is the deadline for the scheme application?” by pointing to page 12 where the date is listed. The response time was under two seconds, which is acceptable for a hobby project. Next I want to add chunking for very large PDFs (splitting a page into multiple 500‑token chunks) and cache the embeddings so I don’t re‑process the same document every time. If you’re interested in how I handled the vector store, you might also enjoy reading about my sentiment analysis add‑on.

The most satisfying part was watching a PDF I’d never read become a searchable knowledge source for my bot.

That’s it for now – a lightweight way to give a Python chatbot PDF reading powers without leaving the tools I already use. Feel free to explore the code, tweak the chunk size, or try a different embedding model. If you build something similar, you can see it in action on my projects page.

Frequently asked questions

Can I use a library other than PyMuPDF for PDF extraction?

Yes, libraries like pdfminer.six or slate3k also work, but PyMuPDF is fast and gives clean text with minimal code. The rest of the pipeline (embedding and storage) stays the same regardless of the extractor.

Do I need a paid Gemini plan to embed PDF pages?

For small documents (up to a few hundred pages) the free Gemini tier provides enough tokens. If you start processing large corpora you may need to upgrade or batch the requests to stay within limits.

How can I keep my Supabase vector table secure?

Use the service‑role key only on the server side and enable Row Level Security (RLS) policies that allow reads only for authenticated users. This way the raw embeddings are not exposed to the public.

What if my PDF contains images or scanned text?

For scanned PDFs you’ll need an OCR step first – tools like Tesseract or Google Vision can turn images into text before you run the embedding stage.

Related posts