Skip to main content

Persian RAG Academy

فارسی · Documentation (Persian) · Lesson summaries in English · Changelog

CI Coverage mypy Docs PyPI Python License: MIT

Build, evaluate and ship Retrieval-Augmented Generation (RAG) for Persian text — a free 15-lesson course in Persian, and parsrag, the library you rebuild along the way.

Every lesson has a Persian explanation with diagrams and measured results, starter code with TODOs, and an automatic grader with bilingual hints. The finished library is the reference solution. Everything — tests, grader, CI, examples — runs offline, with zero API keys and no model downloads; real models (any OpenAI-compatible API, Ollama, sentence-transformers) are optional extras.

Why this exists

Most RAG tutorials assume English. Persian text breaks a naive pipeline in quiet ways: nothing crashes, the right document simply is not found. Three examples from the lessons:

Problem Before After Lesson
Arabic vs Persian letters. «كتاب» (Arabic KAF, YEH) and «کتاب» look identical but are different strings, so a rules document written on an Arabic keyboard never matches the question. كتاب هاي من ۱۲٣ کتاب‌های من 123 — one spelling, ZWNJ restored, one digit system (parsrag.normalize) 02
Broken PDF text. Many PDF producers store glyph shapes (presentation forms) and some store each line in visual order. Copy-paste looks fine; search finds nothing. .ﺪﻨﮐﯽﻣ ﺐﺼﻧ ﻪﻧﺎﺒﺷ ﺖﻧﺎﻣﺍ ﯼﺍﺮﺑ ﺪﻨﻤﺷﻮﻫ ٔﻪﺴﻔﻗ ﮏﯾ (word recall 0.07) یک قفسهٔ هوشمند برای امانت شبانه نصب می‌کند. (word recall 1.00) 03
Colloquial questions. Users write «چقدره»، «میشه»، «تو تهران»; documents say «چقدر است»، «می‌شود»، «در تهران». هزینهٔ پیک تو تهران چقدره و مرجوعی تا کی میشه؟ هزینهٔ پیک در تهران چقدر است و مرجوعی تا کی می‌شود؟ + split into two sub-questions 11

On the course questions, normalisation moves hit@1 of the naive pipeline from 0.81 to 0.90, and switching off only the letter unification step drops it straight back to 0.81. The lessons measure every other decision the same way.

Architecture

The full parsrag pipeline as assembled by PersianRAG (lesson 13). Every box is a replaceable component with an offline default.

flowchart LR
    subgraph Indexing
        F["Files<br/>PDF · DOCX · HTML · MD · TXT"] --> L["loaders<br/>(03) bidi + PDF repair"]
        L --> N["normalize<br/>(02)"]
        N --> C["chunking<br/>(04) recursive + headings"]
        C --> E["embeddings<br/>(05)"]
        E --> S[("stores<br/>(06) memory · FAISS · pgvector")]
        C --> B[("BM25 index<br/>(07)")]
    end
    subgraph Answering
        Q["Question"] --> QN["normalize"]
        QN --> H["hybrid retrieval<br/>(07) BM25 + dense + RRF"]
        S --> H
        B --> H
        H --> R["rerank<br/>(08) 20 → 4"]
        R --> SEC["injection guard (14, opt-in)<br/>detect · sanitise · sandbox"]
        SEC --> G["grounded generation<br/>(09) citations · «نمی‌دانم»"]
        G --> A["Answer + citations"]
    end
    A -.-> EV["eval (10)<br/>metrics · bootstrap · CI gate"]
    Q -.-> AG["agent (11)<br/>rewrite · grade · retry"]
    AG -.-> H
    A --> API["api (12) FastAPI · SSE · cache<br/>cli + web UI (13)"]

Course map

flowchart TB
    L00["00 Setup<br/>FakeLLM · HashingEmbedding"] --> L01["01 RAG in 50 lines"]
    L01 --> L02["02 Persian normalisation"] --> L03["03 Loading documents<br/>PDF · RTL"]
    L03 --> L04["04 Chunking"] --> L05["05 Embeddings<br/>benchmark"] --> L06["06 Vector stores"]
    L06 --> L07["07 Hybrid search<br/>BM25 + RRF"] --> L08["08 Reranking"]
    L08 --> L09["09 Grounded generation"] --> L10["10 Evaluation<br/>CI gate"]
    L10 --> L11["11 Agentic RAG<br/>LangGraph"] --> L12["12 Production API<br/>FastAPI · Docker"]
    L12 --> L13["13 Capstone<br/>your documents + UI"]
    L13 --> L14["14 RAG security<br/>prompt injection in documents"]

Results

Read these numbers for what they are. They come from the repository's small synthetic Persian corpus (25 original documents, 59 answerable questions plus 46 answerable paraphrases) with offline, deterministic models (HashingEmbedding, an extractive FakeLLM, a rule-based agent LLM). They show how to measure and which way each choice moves the numbers — not how a neural model would score. To benchmark real embedding models on the same questions, open the Colab notebook (free GPU, both question sets) or run examples/benchmark_embeddings.py on your own machine, and send the measured rows to lesson 05's results table. All charts are regenerated by examples/make_readme_charts.py; the numbers behind them are in docs/eval/readme_numbers.json.

Normalisation impact PDF repair
Normalisation (02). Unifying Arabic/Persian letters is the step that matters: without it hit@1 stays at the raw-text level (0.81 → 0.90 with it). PDF repair (03). NFKC fixes logical-order PDFs (0.07 → 0.94 word recall) but not visual-order ones (0.08); order repair and ZWNJ recovery bring both to 1.00.
Chunking strategies Hybrid search
Chunking (04). Structure-aware chunks with the heading prepended beat fixed windows at every size (MRR 0.66 → 0.82 at 200 characters). Hybrid search (07). RRF over BM25 and dense is the best untuned method on the original questions (MRR 0.93). Paraphrases halve every lexical method — the gap only a real model closes.
Reranking Agent vs pipeline
Reranking (08). Reranking 20 candidates lifts R@1 0.86 → 0.88 (original) and 0.33 → 0.37 (paraphrase); lesson 10's paired bootstrap shows the interval contains 0. Agent vs pipeline (11). The agent answers more paraphrases (0.22 → 0.35) at ~4× the LLM calls; on the original set its answer-F1 gain (+0.025) is not significant.

The end-to-end pipeline is gated in CI against docs/eval/baseline.json (python -m parsrag.eval gate); the full report is in docs/eval/report.md.

Quick start

a) Learner: take the course

git clone https://github.com/snikookar/Persian-RAG-Academy.git
cd Persian-RAG-Academy
python -m venv .venv && source .venv/bin/activate
pip install -e ".[loaders]" pytest ruff
python -m grader run 00          # then open lessons/00-setup/README.fa.md

Each lesson's starter/ has TODOs; python -m grader run NN tests your version and prints a Persian/English hint for every failing check. python -m grader list shows all lessons.

b) Library user

pip install parsrag                       # core: pydantic + numpy only
pip install "parsrag[loaders,openai]"     # PDF/DOCX/HTML + any OpenAI-compatible model

parsrag is not on PyPI yet; until the first release, install from a clone with pip install -e ..

from parsrag import PersianRAG

rag = PersianRAG()                                   # the course defaults, fully offline
rag.add_documents(["data/fixtures/corpus"])          # folders, files or Document objects
answer = rag.ask("هزینهٔ پیک در تهران چقدر است؟")
print(answer.text)                                   # هزینهٔ پیک در تهران 200 هزار تومان است.
for c in answer.citations:
    print(c.doc_id, c.section)                       # kahroba-shipping شیوه‌های ارسال سفارش
print(rag.ask("پایتخت فرانسه کجاست؟").text)          # نمی‌دانم

The default LLM is the offline extractive FakeLLM; pass llm=OpenAICompatibleLLM(...) or configure parsrag.toml for a real model (lesson 13). The same pipeline from the command line:

parsrag init                                        # commented parsrag.toml
parsrag ingest docs/ --calibrate dev.jsonl          # index + calibrate «نمی‌دانم»
parsrag ask "شرایط مرجوعی کالا چیست؟"
parsrag eval test.jsonl --baseline baseline.json    # lesson 10 report + regression gate

c) Service: API + web UI

docker compose up --build        # API + PostgreSQL/pgvector → http://localhost:8000

Or without Docker: pip install -e ".[api,loaders]" and parsrag serve --docs data/fixtures/corpus. Endpoints: GET /health, POST /ingest, POST /ingest/files, GET /documents, POST /ask, POST /ask/stream (Server-Sent Events) and OpenAPI docs at /docs (lesson 12). The right-to-left web UI streams answers and opens every citation at its exact span in the source document:

parsrag web UI: streamed answer with clickable citations and the source passage

Lessons

The lessons are in Persian. Read the lesson summaries in English — one short page per lesson with what you build, the key ideas and the measured results (also on the docs site).

# Lesson درس GitHub Docs
00 Setup, offline tools, grader آماده‌سازی محیط README site
01 RAG in 50 lines RAG در ۵۰ خط README site
02 Persian normalisation نرمال‌سازی متن فارسی README site
03 Loading Persian documents (PDF, DOCX, HTML, RTL) بارگذاری اسناد فارسی README site
04 Chunking Persian text قطعه‌بندی متن فارسی README site
05 Embeddings and a retrieval benchmark برداری‌سازی و بنچمارک بازیابی README site
06 Vector stores (in-memory, FAISS, pgvector) انباره‌های برداری README site
07 Hybrid search (Persian BM25 + dense + RRF) جست‌وجوی ترکیبی README site
08 Reranking بازرتبه‌بندی README site
09 Grounded generation, citations, honest «I don't know» تولید مستند با ارجاع و «نمی‌دانم» README site
10 Evaluation and a CI quality gate ارزیابی و دروازهٔ CI README site
11 Agentic RAG with LangGraph RAG عامل‌محور با LangGraph README site
12 Production API (FastAPI, streaming, cache, Docker) API تولیدی README site
13 Capstone: RAG over your own documents + UI پروژهٔ پایانی README site
14 RAG security: prompt injection hidden in documents امنیت RAG: تزریق پرامپت در اسناد README site

FAQ

Do I need an API key or a GPU? No. Every lesson, test and example runs offline on a laptop. Real models are optional and only used when you pass a flag such as --model st:NAME or --llm NAME.

Do I need to read Persian? The lessons are in Persian; the code, docstrings and this README are in English, so the library itself is usable without it.

Why are the benchmark numbers so high on the original questions and so low on paraphrases? The original questions reuse the documents' wording, which favours lexical methods; the paraphrase set was written to expose that. Neural embeddings are expected to narrow the gap — measure it with the Colab notebook Open In Colab or examples/benchmark_embeddings.py.

Can I use parsrag for non-Persian text? The pipeline is generic, but normalisation, tokenisation, the stop-word list and the prompts are Persian-specific.

Is HashingEmbedding a real embedding model? No — it hashes words and character n-grams into a vector. It is deterministic and dependency-free, which is what tests and CI need. Lesson 05 explains when to replace it.

Where does the sample data come from? Every document and question in data/fixtures/ was written for this repository (a fictional shop «کهربا» and library «مهرگان»); no copyrighted text.

Contributing

Contributions are welcome — bug reports, lesson feedback, fixes, new lessons. Read CONTRIBUTING.md (how lessons are structured, fixture-writing rules, running the grader and the eval gate) and the Code of Conduct. Security issues: see SECURITY.md.

Looking for a first task? See good first contributions.

License

MIT. All Persian sample data in data/fixtures/ was written originally for this repository and is covered by the same license. The web UI bundles the Vazirmatn font (© The Vazirmatn Project Authors), licensed under the SIL Open Font License 1.1.

Cite this project

If you use the course or parsrag in teaching or research, please cite it (CITATION.cff):

@software{nikookar_persian_rag_academy_2026,
  author  = {Nikookar, Samane},
  title   = {Persian RAG Academy: a course and library for Retrieval-Augmented Generation on Persian text},
  year    = {2026},
  version = {0.1.0},
  url     = {https://github.com/snikookar/Persian-RAG-Academy},
  license = {MIT}
}

Metadata

Release files for parsrag 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for parsrag 0.2.0
File Size Uploaded Provenance Actions
parsrag-0.2.0.tar.gz 295.5 kB With provenance Details for parsrag-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for parsrag 0.2.0
File Interpreter ABI Platform Actions
parsrag-0.2.0-py3-none-any.whl Python 3 none any Details for parsrag-0.2.0-py3-none-any.whl

Total release size: 605.7 kB

Release files / parsrag-0.2.0.tar.gz

Download URL parsrag-0.2.0.tar.gz
Size 295.5 kB
Tags Source
SHA-256 checksum
How to use checksums
9c794f7f92ffdf0234d0816d0fbdc6017a6ac83a3e7f67745bd7424630f22345
BLAKE2b-256 checksum
How to use checksums
1ed0800e05ea5a0a127e843b285772212dc5f3c3c0ebe103768d0163de1baf85
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.

Transparency log

Release files / parsrag-0.2.0-py3-none-any.whl

Download URL parsrag-0.2.0-py3-none-any.whl
Size 310.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5c30993f746c5eb7d092e06240e438b6cd60ba5388b886c52d337e13b2e8cbf2
BLAKE2b-256 checksum
How to use checksums
791b87c5e5c5a56ca2c3984b1d331fcb93261af032f8e04f1d1ab3dee39fe27a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.

Transparency log

Security for parsrag 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for parsrag-0.2.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page