Answer the questions below for free. Log in to save your results, or get a Pro account.

Python

Build a RAG Application with Python and Ollama

Create a beginner-friendly document chatbot with Flask, Ollama embeddings, and a local language model.

Friday 23 October

12 blocks 1 practice question Score: 0 / 3
0 / 1
1 of 12

๐ŸŽฏ What we'll build

A chatbot that answers questions using your own .txt and .pdf files. We'll use Python + Flask + Ollama, so the language model runs locally on your computer.

Main learning goal: Build a working Retrieval-Augmented Generation (RAG) app that retrieves relevant document passages before generating an answer.

Sub-goals: Understand chunking, embeddings, similarity search, Flask routes, and how to test the app.

  1. We start with the RAG idea and the tools.
  2. We install Ollama and download two models.
  3. We build document loading, chunking, embeddings, and retrieval.
  4. We connect everything to a simple Flask website.
  5. We test it and explore freelance project ideas.
๐Ÿ“„ Your documents
       โ†“
โœ‚๏ธ Split into chunks
       โ†“
๐Ÿงญ Find relevant chunks
       โ†“
๐Ÿฆ™ Ollama generates an answer
       โ†“
๐Ÿ’ฌ Answer grounded in your documents
RAG gives the model useful notes before it answers.
2 of 12

1. What is RAG?

RAG means Retrieval-Augmented Generation. Instead of asking an AI to answer from its general training alone, we first search your documents for relevant information and give those passages to the model as context.

Example

Imagine: You upload your course handbook and ask, โ€œHow many days do I have to submit the assignment?โ€

  • Retrieval: Find the handbook paragraphs about submission deadlines.
  • Generation: Ask the model to answer using those paragraphs.
flowchart TD A[Upload PDF or TXT] --> B[Extract text] B --> C[Split text into chunks] C --> D[Create embeddings] D --> E[Store vectors in memory] F[User asks a question] --> G[Embed the question] G --> H[Find similar chunks] E --> H H --> I[Send question + context to Ollama] I --> J[Answer in the browser]
The basic RAG workflow

RAG is not model training. It retrieves useful text at question time. This starter app keeps its document index in memory, so uploaded documents must be added again after restarting the app.

3 of 12

2. Choose the tools

๐Ÿฆ™ **Ollama**

  • Runs compatible language and embedding models locally. No hosted model API key is needed for this local setup.
  • ๐Ÿ **Python + Flask**
  • Python handles the RAG pipeline; Flask serves the upload form and chat page.

๐Ÿ“„ **pypdf**

  • Extracts text from PDF files. Scanned image-only PDFs need OCR, which this starter project does not include.
  • ๐Ÿงญ **Embeddings**
  • Turn text into number vectors so we can compare the meaning of a question with document chunks.

We'll use llama3.2 for answer generation and nomic-embed-text for embeddings. You can change these model names if you have other compatible models installed.

4 of 12

3. Install and prepare the project

Install Python 3.10 or newer and install Ollama from its official website: https://ollama.com

Open a terminal and download the two models:

Example
ollama pull llama3.2
ollama pull nomic-embed-text

Create a project folder with this structure:

Example
ollama-rag/
โ”œโ”€โ”€ app.py
โ”œโ”€โ”€ requirements.txt
โ””โ”€โ”€ templates/
    โ””โ”€โ”€ index.html

Create and activate a virtual environment.

Example
# Windows
python -m venv .venv
.venv\Scripts\activate

# macOS / Linux
python3 -m venv .venv
source .venv/bin/activate

Create requirements.txt:

Example
Flask>=3.0,<4.0
requests>=2.31,<3.0
numpy>=1.26,<3.0
pypdf>=5.0,<7.0

Install the packages:

Example
python -m pip install -r requirements.txt

Keep Ollama running while you use the app. If the app cannot connect, first check that Ollama is installed and that its local service is running.

5 of 12

4. Build the RAG backend

Create app.py and paste this complete file. It supports .txt and text-based .pdf uploads, chunks the extracted text, creates embeddings through Ollama, finds the most similar chunks, and asks the language model to answer from them.

Example
from pathlib import Path
import io

import numpy as np
import requests
from flask import Flask, render_template, request
from pypdf import PdfReader

app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 10 * 1024 * 1024  # 10 MB upload limit

OLLAMA_URL = "http://localhost:11434"
CHAT_MODEL = "llama3.2"
EMBED_MODEL = "nomic-embed-text"

# Starter storage: this is cleared whenever the app restarts.
DOCUMENT_CHUNKS = []
CHUNK_VECTORS = None


def extract_text(filename, file_bytes):
    """Extract text from a supported uploaded file."""
    suffix = Path(filename).suffix.lower()

    if suffix == ".txt":
        return file_bytes.decode("utf-8", errors="replace")

    if suffix == ".pdf":
        reader = PdfReader(io.BytesIO(file_bytes))
        pages = [page.extract_text() or "" for page in reader.pages]
        return "\n".join(pages)

    raise ValueError("Only .txt and .pdf files are supported.")


def split_into_chunks(text, chunk_size=900, overlap=150):
    """Split text into overlapping character chunks."""
    text = " ".join(text.split())
    if not text:
        return []

    chunks = []
    start = 0

    while start < len(text):
        end = start + chunk_size
        chunk = text[start:end].strip()

        if chunk:
            chunks.append(chunk)

        if end >= len(text):
            break

        start = end - overlap

    return chunks


def embed_texts(texts):
    """Ask Ollama to create one embedding vector per text."""
    if not texts:
        return []

    response = requests.post(
        f"{OLLAMA_URL}/api/embed",
        json={"model": EMBED_MODEL, "input": texts},
        timeout=120,
    )
    response.raise_for_status()
    return response.json()["embeddings"]


def add_document(filename, file_bytes):
    """Extract, chunk, and index one uploaded document."""
    global CHUNK_VECTORS

    text = extract_text(filename, file_bytes)
    chunks = split_into_chunks(text)

    if not chunks:
        raise ValueError(
            f"No readable text found in {filename}. "
            "If it is a scanned PDF, OCR is needed."
        )

    vectors = embed_texts(chunks)
    DOCUMENT_CHUNKS.extend(
        {"source": filename, "text": chunk} for chunk in chunks
    )

    new_vectors = np.asarray(vectors, dtype=np.float32)
    if CHUNK_VECTORS is None:
        CHUNK_VECTORS = new_vectors
    else:
        CHUNK_VECTORS = np.vstack([CHUNK_VECTORS, new_vectors])

    return len(chunks)


def retrieve(question, top_k=4):
    """Return the most similar document chunks for a question."""
    if not DOCUMENT_CHUNKS or CHUNK_VECTORS is None:
        return []

    question_vector = np.asarray(
        embed_texts([question])[0], dtype=np.float32
    )

    # Cosine similarity: compare directions, not vector lengths.
    vector_norms = np.linalg.norm(CHUNK_VECTORS, axis=1)
    question_norm = np.linalg.norm(question_vector)

    safe_vector_norms = np.maximum(vector_norms, 1e-12)
    safe_question_norm = max(float(question_norm), 1e-12)

    scores = (CHUNK_VECTORS @ question_vector) / (
        safe_vector_norms * safe_question_norm
    )

    best_indices = np.argsort(scores)[::-1][:top_k]
    return [DOCUMENT_CHUNKS[int(i)] for i in best_indices]


def generate_answer(question, chunks):
    """Generate an answer using retrieved context."""
    if not chunks:
        return "Please upload a readable PDF or TXT document first."

    context = "\n\n".join(
        f"Source: {item['source']}\n{item['text']}"
        for item in chunks
    )

    prompt = f"""
You are a helpful document assistant.
Answer the question using only the CONTEXT below.
If the context does not contain the answer, say you could not find it
in the uploaded documents. Do not invent facts.
Mention the source filename when useful.

CONTEXT:
{context}

QUESTION:
{question}

ANSWER:
""".strip()

    response = requests.post(
        f"{OLLAMA_URL}/api/chat",
        json={
            "model": CHAT_MODEL,
            "messages": [{"role": "user", "content": prompt}],
            "stream": False,
        },
        timeout=180,
    )
    response.raise_for_status()
    return response.json()["message"]["content"].strip()


@app.route("/", methods=["GET", "POST"])
def index():
    answer = None
    sources = []
    message = None
    question = ""

    if request.method == "POST":
        action = request.form.get("action", "")

        try:
            if action == "upload":
                files = request.files.getlist("documents")
                files = [f for f in files if f and f.filename]

                if not files:
                    raise ValueError("Choose at least one PDF or TXT file.")

                added_chunks = 0
                uploaded_names = []

                for uploaded_file in files:
                    filename = Path(uploaded_file.filename).name
                    file_bytes = uploaded_file.read()

                    if not file_bytes:
                        continue

                    added_chunks += add_document(filename, file_bytes)
                    uploaded_names.append(filename)

                if not uploaded_names:
                    raise ValueError("The selected files were empty.")

                message = (
                    f"Indexed {added_chunks} text chunks from: "
                    + ", ".join(uploaded_names)
                )

            elif action == "ask":
                question = request.form.get("question", "").strip()

                if not question:
                    raise ValueError("Type a question first.")

                chunks = retrieve(question)
                answer = generate_answer(question, chunks)
                sources = list(dict.fromkeys(
                    item["source"] for item in chunks
                ))

            else:
                raise ValueError("Unknown form action.")

        except requests.ConnectionError:
            message = (
                "Could not connect to Ollama. Make sure Ollama is running "
                "and both required models are installed."
            )
        except requests.HTTPError as exc:
            detail = exc.response.text[:300] if exc.response is not None else ""
            message = f"Ollama returned an HTTP error: {detail}"
        except (ValueError, OSError) as exc:
            message = str(exc)
        except Exception as exc:
            app.logger.exception("Unexpected RAG error")
            message = f"Something went wrong: {exc}"

    return render_template(
        "index.html",
        answer=answer,
        sources=sources,
        message=message,
        question=question,
        chunk_count=len(DOCUMENT_CHUNKS),
    )


if __name__ == "__main__":
    # Local development only. Do not expose this debug server publicly.
    app.run(host="127.0.0.1", port=5000, debug=True)

What happens inside the code?

  • extract_text() reads the document.
  • split_into_chunks() makes smaller passages.
  • embed_texts() turns passages into vectors.
  • retrieve() finds passages most similar to the question.
  • generate_answer() sends those passages and the question to Ollama.
6 of 12

5. Create the web page

Create a folder named templates, then create templates/index.html. Paste this complete HTML file.

Example
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Local RAG Assistant</title>
    <style>
        body {
            max-width: 850px;
            margin: 40px auto;
            padding: 0 18px;
            font-family: system-ui, sans-serif;
            line-height: 1.55;
            background: #f5f7fb;
            color: #172033;
        }
        header, section {
            background: white;
            padding: 22px;
            margin-bottom: 18px;
            border-radius: 16px;
            box-shadow: 0 5px 20px #1720330d;
        }
        h1 { margin-bottom: 4px; }
        .muted { color: #65708a; }
        input, button {
            box-sizing: border-box;
            width: 100%;
            margin-top: 10px;
            padding: 12px;
            border-radius: 10px;
            font: inherit;
        }
        input { border: 1px solid #cbd2e1; }
        button {
            border: 0;
            background: #5b4bdb;
            color: white;
            font-weight: 700;
            cursor: pointer;
        }
        .notice {
            padding: 12px;
            border-radius: 10px;
            background: #fff4d6;
            overflow-wrap: anywhere;
        }
        .answer {
            white-space: pre-wrap;
            overflow-wrap: anywhere;
        }
    </style>
</head>
<body>
    <header>
        <h1>๐Ÿฆ™ Local RAG Assistant</h1>
        <p class="muted">
            Upload your notes, then ask questions answered from those notes.
        </p>
        <strong>Indexed chunks in this session: {{ chunk_count }}</strong>
    </header>

    <section>
        <h2>๐Ÿ“„ 1. Upload documents</h2>
        <form method="post" enctype="multipart/form-data">
            <input type="hidden" name="action" value="upload">
            <input
                type="file"
                name="documents"
                accept=".pdf,.txt"
                multiple
                required
            >
            <button type="submit">Upload and index</button>
        </form>
    </section>

    <section>
        <h2>๐Ÿ’ฌ 2. Ask a question</h2>
        <form method="post">
            <input type="hidden" name="action" value="ask">
            <input
                type="text"
                name="question"
                placeholder="Ask something from your documents..."
                value="{{ question }}"
                required
            >
            <button type="submit">Find answer</button>
        </form>
    </section>

    {% if message %}
    <section class="notice">
        <strong>Notice:</strong> {{ message }}
    </section>
    {% endif %}

    {% if answer %}
    <section>
        <h2>โœจ Answer</h2>
        <div class="answer">{{ answer }}</div>
        {% if sources %}
        <h3>๐Ÿ“š Retrieved from</h3>
        <ul>
            {% for source in sources %}
            <li>{{ source }}</li>
            {% endfor %}
        </ul>
        {% endif %}
    </section>
    {% endif %}
</body>
</html>
7 of 12

6. Run your RAG app

Make sure Ollama is running and the models are available:

Example
ollama list

From the project folder, start Flask:

Example
python app.py

Open http://127.0.0.1:5000 in your browser.

  1. Upload a readable PDF or TXT file.
  2. Wait for the indexing notice.
  3. Ask a question that the document can answer.
  4. Check the answer and the retrieved source filenames.

Try asking a question whose answer is not in the document. A well-instructed RAG assistant should admit that it could not find the answer instead of confidently inventing one.

8 of 12

7. Understand the key RAG ideas

**Chunking โœ‚๏ธ**

  • Splits long documents into smaller passages. Overlap helps keep context that crosses a chunk boundary.
  • **Embeddings ๐Ÿงญ**
  • Represent text as number vectors. Similar meanings often produce vectors pointing in similar directions.

**Retrieval ๐Ÿ”Ž**

  • Compares the question vector with stored document vectors and selects the closest passages.
  • **Generation ๐Ÿ’ฌ**
  • The language model writes an answer using the retrieved passages as context.

What does retrieval do in this RAG app?

Correct! Retrieval selects context before the model answers.
RAG retrieval does not train the model.
Flask and HTML handle the web interface.

Why do we create embeddings?

The vectors help us find related passages.
Embeddings are numeric representations, not file compression.
Embeddings are used by the Python retrieval code.
9 of 12

8. Common problems and fixes

Problem What to check
Connection refused Start Ollama and confirm it is available at http://localhost:11434.
Model not found Run ollama pull llama3.2 and ollama pull nomic-embed-text.
PDF gives no text It may be scanned; use OCR in a future version.
Answers miss useful details Improve chunk size/overlap, retrieve more chunks, or try a different embedding model.
Slow first response Local models may need time to load; performance depends on your hardware.
Documents disappear after restart This demo stores its index in memory; add persistent storage for a production app.

Do not deploy this development server with debug=True to the public internet. Before production, add authentication, safer file handling, persistent storage, upload limits, logging, and proper deployment configuration.

10 of 12

9. Turn it into a freelance project ๐Ÿ’ผ

๐Ÿงช Starter Course-notes chatbot Answer questions from a PDF
๐Ÿข Next Company handbook assistant Search internal policies
๐Ÿ›๏ธ Next Product-document assistant Answer from manuals and catalogues
๐Ÿš€ Advanced Client-ready RAG service Add accounts, persistent vector storage, evaluation, and deployment

Potential client deliverables include a document Q&A chatbot, an employee handbook assistant, or a support bot grounded in product manuals. Start with a small demo and be clear about its limits.

11 of 12

10. Your challenge ๐Ÿš€

1
Practice โ€” try this on your own computer

Create a small text file named course_notes.txt with three facts. Upload it to your app and ask a question that uses one of those facts.

Expected
The answer should match the note and the source list should include `course_notes.txt`.

Solution

  1. Write three simple facts in course_notes.txt.
  2. Upload the file in the browser.
  3. Ask a question about one fact.
  4. Ask about a fact that is not present and check whether the assistant admits it cannot find the answer.

What is one important limitation of this starter app?

The chunks and vectors are stored in memory.
The model processes text supplied by Python.
This version extracts embedded PDF text only.

Next step: make it production-ready ๐Ÿ› ๏ธ

You now have the core RAG flow. Next, add persistent vector storage, document deletion, user login, better source citations, and tests that measure whether the right chunks are retrieved.

12 of 12

Bonus: What else can you build with Ollama?

You can also create a local writing assistant, a code helper, or a private notes search tool using Python and Ollama. For document-based answers, keep retrieval in the pipeline so the model has relevant source text to work with.

All 1 done.

Nice work โ€” bring anything you got stuck on to the next session.