Read documents with Munito AI OCR, index them with a local keyword index, and answer questions with citations from a Munito AI chat model.
To build retrieval-augmented generation (RAG) with Munito AI, read your documents with POST /ocr, cut the Markdown into chunks, find the best chunks for a question, and send them to a chat model.
The model answers only from those chunks and cites the page of each claim.
How it works
| Step | Where it runs | Tool |
|---|---|---|
| 1. Read each PDF | Munito AI | POST /ocr with munito/ocr-auto |
| 2. Cut each page into chunks | Your code | Plain Python |
| 3. Find the best chunks | Your code | A BM25 keyword index (rank_bm25) |
| 4. Answer with citations | Munito AI | POST /chat/completions with qwen/qwen3.8-27b |
Munito AI has no embeddings endpoint, so this guide retrieves with a keyword index that runs on your machine. To use vector search, make the embeddings with another tool and keep steps 1 and 4 on Munito AI.
Complete script
pip install openai requests rank_bm25
export MUNITO_API_KEY="munitoai_..."Save this file as rag.py. Put your own PDF URLs in DOCUMENTS:
# pip install openai requests rank_bm25
import os
import re
import sys
import time
import requests
from openai import OpenAI
from rank_bm25 import BM25Okapi
HOST = "https://api.munito.ai"
BASE_URL = f"{HOST}/inference/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['MUNITO_API_KEY']}"}
DOCUMENTS = {
"transformer": "https://arxiv.org/pdf/1706.03762",
}
def ocr_pages(url: str) -> list[str]:
"""Read a PDF by URL with Munito OCR. Return the Markdown of each page."""
response = requests.post(
f"{BASE_URL}/ocr",
headers=HEADERS,
json={"model": "munito/ocr-auto", "document": {"type": "url", "url": url}},
timeout=300,
)
response.raise_for_status()
body = response.json()
while body.get("status") in ("accepted", "running"):
time.sleep(2)
poll = requests.get(HOST + body["location"], headers=HEADERS, timeout=60)
poll.raise_for_status()
body = {**poll.json(), "location": body["location"]}
return [page["markdown"] for page in body["pages"]]
def chunks(doc: str, pages: list[str], size: int = 800, overlap: int = 200) -> list[dict]:
"""Cut each page into overlapping chunks of characters. Keep the source of each chunk."""
out = []
for number, text in enumerate(pages, start=1):
for start in range(0, max(len(text), 1), size - overlap):
piece = text[start : start + size].strip()
if piece:
out.append({"id": f"{doc} p.{number}", "text": piece})
return out
def tokens(text: str) -> list[str]:
return re.findall(r"\w+", text.lower())
def answer(question: str, index: BM25Okapi, corpus: list[dict], k: int = 5) -> str:
"""Retrieve the k best chunks and answer only from them, with citations."""
scores = index.get_scores(tokens(question))
best = sorted(range(len(corpus)), key=lambda i: scores[i], reverse=True)[:k]
context = "\n\n".join(f"[{corpus[i]['id']}]\n{corpus[i]['text']}" for i in best)
client = OpenAI(base_url=BASE_URL, api_key=os.environ["MUNITO_API_KEY"])
completion = client.chat.completions.create(
model="qwen/qwen3.8-27b",
messages=[
{
"role": "system",
"content": "Answer from the sources only. After each claim, cite its source in brackets, "
"such as [transformer p.3]. If the sources do not answer, say so.",
},
{"role": "user", "content": f"Sources:\n{context}\n\nQuestion: {question}"},
],
reasoning_effort="low",
max_tokens=2048,
)
return completion.choices[0].message.content
if __name__ == "__main__":
corpus = [chunk for doc, url in DOCUMENTS.items() for chunk in chunks(doc, ocr_pages(url))]
index = BM25Okapi([tokens(c["text"]) for c in corpus])
question = sys.argv[1] if len(sys.argv) > 1 else "Which optimizer trains the model, and how many warmup steps does it use?"
print(answer(question, index, corpus))Run it with a question:
python rag.py "Which optimizer trains the model, and how many warmup steps does it use?"The answer cites the page of each claim:
The model is trained using the Adam optimizer (with β1 = 0.9, β2 = 0.98, and ϵ = 10⁻⁹) [transformer p.7].
It uses 4000 warmup steps [transformer p.7].Make it your own
- Local files. Send a file as base64 in place of a URL:
{"type": "base64", "data": ..., "filename": "report.pdf"}. See PDF to Markdown. - Read once. OCR each document one time, and save the chunks with the index, for example as JSON. Munito AI does not keep the result after it delivers it.
- Chunk size. Larger chunks give the model more context for each source. Smaller chunks make the citations more precise.
- More sources. Increase
kwhen an answer needs facts from many pages. Each chunk adds input tokens. - Answer text.
qwen/qwen3.8-27breasons before it answers. Keepmax_tokenslarge, or the answer can stop before it starts, withcontentset tonull. - Region. To keep all processing in the EU, use
https://api.eu.munito.aiin place ofhttps://api.munito.ai. See Regions.
Cost
A RAG answer costs the OCR pages of each document, one time, plus the chat tokens of each question: the chunks and the question as input, and the answer as output. See Pricing.
Invoice extraction
Extract invoice data to structured JSON. Read the invoice PDF with the Munito AI OCR API, then get its fields from a chat model with a JSON schema.
Dedicated endpoints
Reserved GPU capacity for one open model, for your organization only, in the EU, the US or on the global host. Munito AI sets it up on request, by contract.