DocsAnswers from your own documents (RAG)

Answers from your own documents (RAG)

Use casesAdvanced30 min

You'll have a program that answers from your documents and cites the source [1] [2].

You need: a fetch_... key (create one), a balance (top up), Python 3.10+, documents as .txt or .md (convert PDF and Word first, e.g. with pypdf).

Search runs on your machine and costs nothing: Zukko has no embeddings endpoint. Zukko writes the answer.

  1. 1
    Put the documents in a folder

    Create a docs folder and put .txt or .md files there. Subfolders are fine.

  2. 2
    Install the libraries
    bash
    pip install openai rank_bm25 snowballstemmer
  3. 3
    Save the code

    Copy the code from "Full code" below into rag.py, next to the docs folder.

  4. 4
    Run it
    bash
    export ZUKKO_API_KEY="fetch_..."
    python rag.py

    Ask a question. The answer has source numbers and the files the chunks came from. Added a document? Restart the program.

1Full code

rag.py
import os
import re
import sys
from pathlib import Path

import snowballstemmer
from openai import APIConnectionError, APIStatusError, OpenAI
from rank_bm25 import BM25Plus

DOCS_DIR = Path(os.environ.get("DOCS_DIR", "docs"))  # folder with .txt and .md files
MODEL = os.environ.get("ZUKKO_MODEL", "claude-haiku-5-5")
CHUNK_SIZE = 1200  # rough chunk size in characters
TOP_K = 5          # how many chunks to send to the model

In a bot: build index once at startup and call ask from the message handler, as in Telegram bot. ask is synchronous — in an async bot use await asyncio.to_thread(ask, index, text).

2Other approaches

Everything in the prompt — up to ~200–300 pages

No search: join the documents and send them with the question. Every question pays for the whole base and takes longer.

python
from pathlib import Path
import os
from openai import OpenAI

client = OpenAI(base_url="https://api.zukko.pro/v1", api_key=os.environ["ZUKKO_API_KEY"], timeout=300)

docs = "\n\n".join(
    f"=== {p.name} ===\n{p.read_text(encoding='utf-8')}" for p in sorted(Path("docs").glob("*.md"))
)
question = "How many vacation days does a new employee get?"

response = client.chat.completions.create(
    model="claude-sonnet-5",
    messages=[
        # documents go first: this part never changes and lands in the prompt cache
        {"role": "system", "content": "Answer only from the documents below and name the source file. "
                                      "If the answer isn't there, say so.\n\n" + docs},
        {"role": "user", "content": question},
    ],
)
print(response.choices[0].message.content)

Meaning-based search — when questions use different words than the documents

For example, "quit my job" vs. "termination of employment". Replace the Index class in rag.py with this code (runs locally, no GPU):

python
# pip install sentence-transformers
from sentence_transformers import SentenceTransformer

encoder = SentenceTransformer("intfloat/multilingual-e5-small")  # downloads once, handles 100 languages


class Index:
    def __init__(self, chunks: list[dict]):
        self.chunks = chunks
        # e5 models expect "passage: " before texts and "query: " before questions
        self.vectors = encoder.encode(
            ["passage: " + c["text"] for c in chunks], normalize_embeddings=True
        )

    def search(self, query: str, k: int = TOP_K) -> list[dict]:
        q = encoder.encode(["query: " + query], normalize_embeddings=True)[0]
        scores = self.vectors @ q  # higher means closer in meaning
        best = scores.argsort()[::-1][:k]
        return [self.chunks[i] for i in best]

Vectors for thousands of chunks take a few minutes — save them to a file (numpy.save).

3If it doesn't work

What you seeWhat to do
"No .txt or .md files in docs"Convert files to .txt/.md, check the path or set DOCS_DIR
"Nothing in the documents matches", but the answer is thereRephrase. If it happens a lot, switch to meaning-based search
The answer is incompleteRaise TOP_K to 8–10 or lower CHUNK_SIZE
The model answers beyond the documentsKeep the sources rule, try claude-sonnet-5
"Everything in the prompt" is very slowSwitch to keyword search, set a 300-second client timeout
401, 402, 429See API errors
Answers from your own documents (RAG) · Zukko Docs