Answers from your own documents (RAG)
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.
- 1Put the documents in a folder
Create a
docsfolder and put.txtor.mdfiles there. Subfolders are fine. - 2Install the librariesbash
pip install openai rank_bm25 snowballstemmer - 3Save the code
Copy the code from "Full code" below into
rag.py, next to thedocsfolder. - 4Run itbash
export ZUKKO_API_KEY="fetch_..." python rag.pyAsk a question. The answer has source numbers and the files the chunks came from. Added a document? Restart the program.
1Full code
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.
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):
# 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 see | What 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 there | Rephrase. If it happens a lot, switch to meaning-based search |
| The answer is incomplete | Raise TOP_K to 8–10 or lower CHUNK_SIZE |
| The model answers beyond the documents | Keep the sources rule, try claude-sonnet-5 |
| "Everything in the prompt" is very slow | Switch to keyword search, set a 300-second client timeout |
| 401, 402, 429 | See API errors |