persian-rag 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. persian_rag-0.2.0/.gitignore +20 -0
  2. persian_rag-0.2.0/CHANGELOG.md +44 -0
  3. persian_rag-0.2.0/CONTRIBUTING.md +47 -0
  4. persian_rag-0.2.0/LICENSE +21 -0
  5. persian_rag-0.2.0/PKG-INFO +347 -0
  6. persian_rag-0.2.0/README.md +259 -0
  7. persian_rag-0.2.0/docs/caching-async.fa.md +54 -0
  8. persian_rag-0.2.0/docs/caching-async.md +54 -0
  9. persian_rag-0.2.0/docs/cli.fa.md +47 -0
  10. persian_rag-0.2.0/docs/cli.md +50 -0
  11. persian_rag-0.2.0/docs/evaluation.fa.md +52 -0
  12. persian_rag-0.2.0/docs/evaluation.md +55 -0
  13. persian_rag-0.2.0/docs/getting-started.fa.md +92 -0
  14. persian_rag-0.2.0/docs/getting-started.md +92 -0
  15. persian_rag-0.2.0/docs/index.fa.md +27 -0
  16. persian_rag-0.2.0/docs/index.md +27 -0
  17. persian_rag-0.2.0/docs/loaders.fa.md +56 -0
  18. persian_rag-0.2.0/docs/loaders.md +54 -0
  19. persian_rag-0.2.0/docs/normalization.fa.md +43 -0
  20. persian_rag-0.2.0/docs/normalization.md +43 -0
  21. persian_rag-0.2.0/docs/releasing.fa.md +20 -0
  22. persian_rag-0.2.0/docs/releasing.md +20 -0
  23. persian_rag-0.2.0/docs/retrieval.fa.md +59 -0
  24. persian_rag-0.2.0/docs/retrieval.md +67 -0
  25. persian_rag-0.2.0/docs/server.fa.md +35 -0
  26. persian_rag-0.2.0/docs/server.md +35 -0
  27. persian_rag-0.2.0/docs/vector-stores.fa.md +52 -0
  28. persian_rag-0.2.0/docs/vector-stores.md +53 -0
  29. persian_rag-0.2.0/examples/quickstart.py +30 -0
  30. persian_rag-0.2.0/mkdocs.yml +84 -0
  31. persian_rag-0.2.0/pyproject.toml +140 -0
  32. persian_rag-0.2.0/src/persian_rag/__init__.py +121 -0
  33. persian_rag-0.2.0/src/persian_rag/bm25.py +92 -0
  34. persian_rag-0.2.0/src/persian_rag/cache.py +252 -0
  35. persian_rag-0.2.0/src/persian_rag/chunk.py +340 -0
  36. persian_rag-0.2.0/src/persian_rag/cli.py +454 -0
  37. persian_rag-0.2.0/src/persian_rag/documents.py +155 -0
  38. persian_rag-0.2.0/src/persian_rag/embeddings.py +352 -0
  39. persian_rag-0.2.0/src/persian_rag/errors.py +23 -0
  40. persian_rag-0.2.0/src/persian_rag/evaluation.py +525 -0
  41. persian_rag-0.2.0/src/persian_rag/loaders.py +269 -0
  42. persian_rag-0.2.0/src/persian_rag/normalize.py +212 -0
  43. persian_rag-0.2.0/src/persian_rag/persist.py +185 -0
  44. persian_rag-0.2.0/src/persian_rag/pipeline.py +276 -0
  45. persian_rag-0.2.0/src/persian_rag/py.typed +0 -0
  46. persian_rag-0.2.0/src/persian_rag/remote.py +181 -0
  47. persian_rag-0.2.0/src/persian_rag/rerank.py +255 -0
  48. persian_rag-0.2.0/src/persian_rag/retrieval.py +276 -0
  49. persian_rag-0.2.0/src/persian_rag/server.py +133 -0
  50. persian_rag-0.2.0/src/persian_rag/store.py +280 -0
  51. persian_rag-0.2.0/src/persian_rag/tokenize.py +192 -0
  52. persian_rag-0.2.0/src/persian_rag/vectorstores/__init__.py +39 -0
  53. persian_rag-0.2.0/src/persian_rag/vectorstores/pgvector.py +186 -0
  54. persian_rag-0.2.0/src/persian_rag/vectorstores/qdrant.py +195 -0
  55. persian_rag-0.2.0/src/persian_rag/version.py +4 -0
  56. persian_rag-0.2.0/tests/conftest.py +23 -0
  57. persian_rag-0.2.0/tests/fixtures/.hidden.txt +1 -0
  58. persian_rag-0.2.0/tests/fixtures/messy.txt +4 -0
  59. persian_rag-0.2.0/tests/fixtures/nested/note.txt +1 -0
  60. persian_rag-0.2.0/tests/fixtures/sample.md +14 -0
  61. persian_rag-0.2.0/tests/fixtures/sample.txt +2 -0
  62. persian_rag-0.2.0/tests/test_async.py +57 -0
  63. persian_rag-0.2.0/tests/test_cache.py +149 -0
  64. persian_rag-0.2.0/tests/test_chunk.py +79 -0
  65. persian_rag-0.2.0/tests/test_cli.py +149 -0
  66. persian_rag-0.2.0/tests/test_documents.py +62 -0
  67. persian_rag-0.2.0/tests/test_embeddings.py +125 -0
  68. persian_rag-0.2.0/tests/test_evaluation.py +155 -0
  69. persian_rag-0.2.0/tests/test_heavy.py +37 -0
  70. persian_rag-0.2.0/tests/test_loaders.py +208 -0
  71. persian_rag-0.2.0/tests/test_normalize.py +72 -0
  72. persian_rag-0.2.0/tests/test_pipeline.py +79 -0
  73. persian_rag-0.2.0/tests/test_rerank.py +169 -0
  74. persian_rag-0.2.0/tests/test_retrieval.py +85 -0
  75. persian_rag-0.2.0/tests/test_server.py +44 -0
  76. persian_rag-0.2.0/tests/test_tokenize.py +36 -0
  77. persian_rag-0.2.0/tests/test_vectorstores.py +258 -0
@@ -0,0 +1,20 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ dist/
6
+ build/
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ .venv/
11
+ venv/
12
+ .python-version
13
+ .persian-rag/
14
+ *.index.json
15
+ .coverage
16
+ htmlcov/
17
+ .DS_Store
18
+ site/
19
+ *.sqlite
20
+ *.sqlite3
@@ -0,0 +1,44 @@
1
+ # Changelog
2
+
3
+ All notable changes to persian-rag are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
+
5
+ ## [0.2.0] - 2026-10-30
6
+
7
+ ### Added
8
+
9
+ - Rerankers: a `Reranker` protocol, `CrossEncoderReranker` (`rerank` extra, default model `BAAI/bge-reranker-v2-m3`), and a listwise `LLMReranker` that tolerates loosely formatted replies.
10
+ - `RAGPipeline(reranker=..., rerank_candidates=...)` and `RAGPipeline.retrieve()`; reranked results keep the first-stage score in `RetrievalResult.retrieval_score`.
11
+ - Vector stores in the new `persian_rag.vectorstores` package: `QdrantVectorStore` (`qdrant` extra) and `PgVectorStore` for PostgreSQL with pgvector (`pgvector` extra), both with delete support.
12
+ - PDF (`pdf` extra) and Word (`docx` extra) loaders. PDF text is cleaned of presentation forms and bidi controls, and lines stored in visual order are detected and repaired. `load_documents` now reads `.pdf` and `.docx` files and can split PDFs by page.
13
+ - Evaluation module: hit rate, MRR, recall, and nDCG at several cut-offs, document or chunk matching, JSONL datasets, and a faithfulness scorer protocol with token-overlap and LLM-judge implementations.
14
+ - FastAPI server (`server` extra) with `/health`, `/retrieve`, and `/query`, created through `persian_rag.server.create_app`.
15
+ - Async support: `HybridRetriever.aretrieve`, `RAGPipeline.aretrieve`, and `RAGPipeline.aquery`, which accepts async or sync model callables.
16
+ - Caching: `CachedEmbedder` with in-memory and SQLite embedding caches, and an LRU/TTL result cache for `HybridRetriever`.
17
+ - CLI: `persian-rag eval` and `persian-rag serve`, plus `--rerank`, `--rtl-fix`, `--split-pages`, and `--embedding-cache` options.
18
+ - Bilingual documentation site (English and Persian) built with MkDocs Material.
19
+ - Release workflow that publishes to PyPI on version tags through trusted publishing or an API token, and skips publishing when neither is configured.
20
+ - CI jobs for Python 3.10 to 3.13 with all extras, a minimal-dependency run, PostgreSQL with pgvector, the documentation build, and package checks.
21
+
22
+ ### Changed
23
+
24
+ - `MissingDependencyError` is raised for missing optional packages. It subclasses both `PersianRagError` and `ImportError`.
25
+ - `RAGPipeline.query` raises `TypeError` when given an async model; use `aquery` instead.
26
+ - The `all` extra now installs every optional integration.
27
+ - README rewritten with an extras table, usage guides, and a Persian summary.
28
+
29
+ ## [0.1.0] - 2026-10-30
30
+
31
+ ### Added
32
+
33
+ - Python package `persian_rag` for Python 3.10 and newer.
34
+ - Persian normalization: Arabic and Persian letters, ZWNJ, digits, optional diacritics, and punctuation.
35
+ - Sentence splitting, tokenization, and fixed, sentence, and recursive chunking.
36
+ - Embedder protocol with hashing, sentence-transformers, and OpenAI-compatible adapters.
37
+ - In-memory cosine store and an optional FAISS adapter.
38
+ - BM25, dense retrieval, and reciprocal rank fusion.
39
+ - `RAGPipeline` with a caller-supplied LLM callable.
40
+ - `persian-rag ingest` and `persian-rag query` commands, plus a JSON index.
41
+ - Unit tests, Ruff, mypy, and a GitHub Actions workflow.
42
+
43
+ [0.2.0]: https://github.com/sadrmanesh/persian-rag/releases/tag/v0.2.0
44
+ [0.1.0]: https://github.com/sadrmanesh/persian-rag/releases/tag/v0.1.0
@@ -0,0 +1,47 @@
1
+ # Contributing to persian-rag
2
+
3
+ Thanks for considering a contribution. The project is maintained by Naser Sadrmanesh ([info.sadrmanesh@gmail.com](mailto:info.sadrmanesh@gmail.com)).
4
+
5
+ Issues and pull requests may be written in Persian or English.
6
+
7
+ ## Setup
8
+
9
+ ```bash
10
+ git clone https://github.com/sadrmanesh/persian-rag
11
+ cd persian-rag
12
+ python -m venv .venv
13
+ source .venv/bin/activate
14
+ pip install -e ".[dev]"
15
+ # optional integrations used by the full test suite
16
+ pip install -e ".[dev,server,qdrant,pgvector,pdf,docx]"
17
+ ```
18
+
19
+ Python 3.10 or newer is required.
20
+
21
+ ## Checks
22
+
23
+ Run these before opening a pull request:
24
+
25
+ ```bash
26
+ ruff check .
27
+ ruff format --check .
28
+ mypy src
29
+ pytest
30
+ ```
31
+
32
+ `ruff format .` rewrites formatting. Keep the public modules typed. New behavior needs a unit test. Persian fixtures belong in `tests/fixtures/` or inline in the test when they are only a few words.
33
+
34
+ Tests that download model weights must be marked `heavy` and skipped unless `PERSIAN_RAG_RUN_HEAVY=1`. Do not make the default suite depend on network access. Tests for optional extras use `pytest.importorskip` or fakes so the suite also passes with only the core dependencies installed. Tests that need a live PostgreSQL server are marked `postgres` and read the connection string from `PERSIAN_RAG_PG_DSN`.
35
+
36
+ Documentation lives in `docs/` with Persian pages next to English ones (`page.md` and `page.fa.md`). Build it with `pip install -e ".[docs]"` and `mkdocs build --strict`.
37
+
38
+ ## Pull requests
39
+
40
+ - Describe the user-facing change and how you tested it.
41
+ - Do not add benchmark numbers unless the change includes the data, the script, and the command used to produce them.
42
+ - Keep API keys, indexes, and virtual environments out of the commit.
43
+ - Version bumps belong in `src/persian_rag/version.py`, `pyproject.toml`, and `CHANGELOG.md` together.
44
+
45
+ ## Scope of 0.2.x
46
+
47
+ Bug fixes and small, tested improvements to normalization, chunking, retrieval, loaders, evaluation, the server, and the CLI are in scope. New vector-store backends, parsers, and hosted services are better discussed in an issue first so the public API stays small.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Naser Sadrmanesh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,347 @@
1
+ Metadata-Version: 2.5
2
+ Name: persian-rag
3
+ Version: 0.2.0
4
+ Summary: Persian-first retrieval-augmented generation toolkit
5
+ Project-URL: Homepage, https://github.com/sadrmanesh/persian-rag
6
+ Project-URL: Repository, https://github.com/sadrmanesh/persian-rag
7
+ Project-URL: Documentation, https://sadrmanesh.github.io/persian-rag/
8
+ Project-URL: Changelog, https://github.com/sadrmanesh/persian-rag/blob/main/CHANGELOG.md
9
+ Project-URL: Author, https://sadrmanesh.github.io
10
+ Author-email: Naser Sadrmanesh <info.sadrmanesh@gmail.com>
11
+ Maintainer-email: Naser Sadrmanesh <info.sadrmanesh@gmail.com>
12
+ License: MIT License
13
+
14
+ Copyright (c) 2026 Naser Sadrmanesh
15
+
16
+ Permission is hereby granted, free of charge, to any person obtaining a copy
17
+ of this software and associated documentation files (the "Software"), to deal
18
+ in the Software without restriction, including without limitation the rights
19
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
20
+ copies of the Software, and to permit persons to whom the Software is
21
+ furnished to do so, subject to the following conditions:
22
+
23
+ The above copyright notice and this permission notice shall be included in all
24
+ copies or substantial portions of the Software.
25
+
26
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
27
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
28
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
29
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
30
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
31
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
32
+ SOFTWARE.
33
+ License-File: LICENSE
34
+ Keywords: bm25,embeddings,farsi,nlp,persian,pgvector,qdrant,rag,reranking,retrieval
35
+ Classifier: Development Status :: 3 - Alpha
36
+ Classifier: Framework :: FastAPI
37
+ Classifier: Intended Audience :: Developers
38
+ Classifier: License :: OSI Approved :: MIT License
39
+ Classifier: Natural Language :: Persian
40
+ Classifier: Operating System :: OS Independent
41
+ Classifier: Programming Language :: Python :: 3
42
+ Classifier: Programming Language :: Python :: 3.10
43
+ Classifier: Programming Language :: Python :: 3.11
44
+ Classifier: Programming Language :: Python :: 3.12
45
+ Classifier: Programming Language :: Python :: 3.13
46
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
47
+ Classifier: Topic :: Text Processing :: Linguistic
48
+ Classifier: Typing :: Typed
49
+ Requires-Python: >=3.10
50
+ Provides-Extra: all
51
+ Requires-Dist: faiss-cpu>=1.8.0; extra == 'all'
52
+ Requires-Dist: fastapi>=0.110.0; extra == 'all'
53
+ Requires-Dist: numpy>=1.26.0; extra == 'all'
54
+ Requires-Dist: psycopg[binary]>=3.1.0; extra == 'all'
55
+ Requires-Dist: pypdf>=4.0.0; extra == 'all'
56
+ Requires-Dist: python-docx>=1.1.0; extra == 'all'
57
+ Requires-Dist: qdrant-client>=1.10.0; extra == 'all'
58
+ Requires-Dist: sentence-transformers>=3.0.0; extra == 'all'
59
+ Requires-Dist: uvicorn>=0.29.0; extra == 'all'
60
+ Provides-Extra: dev
61
+ Requires-Dist: httpx>=0.27.0; extra == 'dev'
62
+ Requires-Dist: mypy>=1.11.0; extra == 'dev'
63
+ Requires-Dist: pytest>=8.2.0; extra == 'dev'
64
+ Requires-Dist: ruff>=0.6.0; extra == 'dev'
65
+ Provides-Extra: docs
66
+ Requires-Dist: mkdocs-material>=9.5.0; extra == 'docs'
67
+ Requires-Dist: mkdocs-static-i18n>=1.2.0; extra == 'docs'
68
+ Requires-Dist: mkdocs>=1.6.0; extra == 'docs'
69
+ Provides-Extra: docx
70
+ Requires-Dist: python-docx>=1.1.0; extra == 'docx'
71
+ Provides-Extra: faiss
72
+ Requires-Dist: faiss-cpu>=1.8.0; extra == 'faiss'
73
+ Requires-Dist: numpy>=1.26.0; extra == 'faiss'
74
+ Provides-Extra: pdf
75
+ Requires-Dist: pypdf>=4.0.0; extra == 'pdf'
76
+ Provides-Extra: pgvector
77
+ Requires-Dist: psycopg[binary]>=3.1.0; extra == 'pgvector'
78
+ Provides-Extra: qdrant
79
+ Requires-Dist: qdrant-client>=1.10.0; extra == 'qdrant'
80
+ Provides-Extra: rerank
81
+ Requires-Dist: sentence-transformers>=3.0.0; extra == 'rerank'
82
+ Provides-Extra: sentence-transformers
83
+ Requires-Dist: sentence-transformers>=3.0.0; extra == 'sentence-transformers'
84
+ Provides-Extra: server
85
+ Requires-Dist: fastapi>=0.110.0; extra == 'server'
86
+ Requires-Dist: uvicorn>=0.29.0; extra == 'server'
87
+ Description-Content-Type: text/markdown
88
+
89
+ # persian-rag
90
+
91
+ [![CI](https://github.com/sadrmanesh/persian-rag/actions/workflows/ci.yml/badge.svg)](https://github.com/sadrmanesh/persian-rag/actions/workflows/ci.yml)
92
+ [![Python](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue)](pyproject.toml)
93
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
94
+
95
+ Persian-first retrieval-augmented generation for Python.
96
+
97
+ persian-rag is a toolkit for building RAG systems over Farsi text. It normalizes Arabic and Persian characters, half-spaces, digits, and punctuation; loads text, Markdown, PDF, and Word files (repairing reversed PDF text); chunks and embeds passages; retrieves them with BM25, dense vectors, or both; reranks them; and measures the result. Generation is a function you supply, sync or async.
98
+
99
+ The core install is pure Python with no dependencies. Every heavy piece (models, vector databases, parsers, the HTTP server) is an optional extra.
100
+
101
+ **Documentation:** English and Persian guides live in [`docs/`](docs/) and build with MkDocs (see [Documentation](#documentation)).
102
+
103
+ ## خلاصه فارسی
104
+
105
+ persian-rag یک جعبه ابزار پایتون برای ساخت سامانه های بازیابی و تولید پاسخ روی متن فارسی است. نویسه های عربی و فارسی، فاصله ها، رقم ها و نشانه گذاری را یکدست می کند. فایل های متنی، مارک داون، PDF و Word را می خواند و ترتیب وارونه متن فارسی در PDF را اصلاح می کند. متن را تکه تکه می کند و با BM25، بردارهای معنایی یا ترکیب هر دو جست و جو می کند. نتیجه را با رتبه بندی دوباره بهتر می کند و کیفیت بازیابی و پاسخ را می سنجد. تولید پاسخ به تابعی سپرده می شود که خودتان می دهید و می تواند همگام یا ناهمگام باشد. راهنمای کامل فارسی در پوشه docs است.
106
+
107
+ ## Features
108
+
109
+ - **Normalization** for Persian: Arabic Yeh and Kaf, presentation forms, ZWNJ, digits, optional diacritics, and punctuation.
110
+ - **Loaders** for `.txt`, `.md`, `.pdf`, and `.docx`. PDF text in visual (reversed) order is detected and repaired, presentation forms are folded, and bidi control marks are removed.
111
+ - **Chunking**: fixed windows with overlap, sentence packing, and recursive separators.
112
+ - **Embeddings** through a small protocol, with adapters for `sentence-transformers` and any OpenAI-compatible API, plus a persistent embedding cache.
113
+ - **Vector stores**: in-memory, FAISS, Qdrant, and PostgreSQL with pgvector.
114
+ - **Hybrid retrieval** with BM25 and [reciprocal rank fusion](https://plg.uwaterloo.ca/gvcormac/cormacksigir09-rrf.pdf), and an optional query result cache.
115
+ - **Reranking** with a cross-encoder (default `BAAI/bge-reranker-v2-m3`) or any LLM (listwise).
116
+ - **Evaluation**: hit rate, MRR, recall, and nDCG at several cut-offs, plus pluggable answer faithfulness scorers.
117
+ - **Async**: `aretrieve` and `aquery`, with native support for `async` model functions.
118
+ - **HTTP API** built on FastAPI, and a CLI for `ingest`, `query`, `eval`, and `serve`.
119
+ - Fully typed (`mypy --strict`), tested offline, MIT licensed.
120
+
121
+ ## Installation
122
+
123
+ ```bash
124
+ pip install persian-rag
125
+ ```
126
+
127
+ Pick the extras you need:
128
+
129
+ | Extra | Installs | Enables |
130
+ | --- | --- | --- |
131
+ | `sentence-transformers` | sentence-transformers | `SentenceTransformerEmbedder` |
132
+ | `rerank` | sentence-transformers | `CrossEncoderReranker` |
133
+ | `faiss` | faiss-cpu, numpy | `FaissVectorStore` |
134
+ | `qdrant` | qdrant-client | `QdrantVectorStore` |
135
+ | `pgvector` | psycopg 3 | `PgVectorStore` |
136
+ | `pdf` | pypdf | PDF loading |
137
+ | `docx` | python-docx | Word loading |
138
+ | `server` | fastapi, uvicorn | `persian_rag.server`, `persian-rag serve` |
139
+ | `all` | everything above | |
140
+
141
+ ```bash
142
+ pip install "persian-rag[pdf,docx,qdrant,server]"
143
+ ```
144
+
145
+ Requires Python 3.10 or newer. The OpenAI-compatible adapters use only the standard library: set `OPENAI_API_KEY`, and `OPENAI_BASE_URL` for compatible servers.
146
+
147
+ ## Quick start
148
+
149
+ ```python
150
+ from persian_rag import HashingEmbedder, RAGPipeline, build_retriever
151
+
152
+ texts = [
153
+ "حافظ در شهر شیراز زاده شد و دیوان او مشهور است.",
154
+ "فوتبال ورزشی گروهی با دو تیم است.",
155
+ ]
156
+ retriever = build_retriever(texts, HashingEmbedder())
157
+
158
+
159
+ def answer(prompt: str) -> str:
160
+ # Replace with a call to your model.
161
+ return "شیراز" if "شیراز" in prompt else "نامشخص"
162
+
163
+
164
+ result = RAGPipeline(retriever, answer).query("حافظ در کدام شهر زاده شد؟")
165
+ print(result.answer)
166
+ for item in result.contexts:
167
+ print(round(item.score, 4), item.text)
168
+ ```
169
+
170
+ `HashingEmbedder` is a deterministic character n-gram hash for tests and offline checks. For semantic search use `SentenceTransformerEmbedder` (for example `"sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"`) or `OpenAICompatibleEmbedder`. The same flow is in [`examples/quickstart.py`](examples/quickstart.py).
171
+
172
+ ## Usage
173
+
174
+ ### Normalization
175
+
176
+ ```python
177
+ from persian_rag import NormalizeConfig, normalize
178
+
179
+ normalize("يك كتاب با اعداد ٤٥٦")
180
+ # 'یک کتاب با اعداد ۴۵۶'
181
+
182
+ normalize("کِتاب", NormalizeConfig(remove_diacritics=True, digits="ascii"))
183
+ ```
184
+
185
+ `normalize` is idempotent for a fixed config. Documents and queries must use the same `NormalizeConfig`; `build_retriever` and `HybridRetriever` take care of that.
186
+
187
+ | Option | Default | Effect |
188
+ | --- | --- | --- |
189
+ | `unify_arabic_chars` | `True` | `ي`/`ى` to `ی`, `ك` to `ک`, `أ`/`إ`/`ٱ` to `ا`, `ؤ` to `و`, `ة` to `ه`. `آ` and `ئ` stay. |
190
+ | `compatibility_forms` | `True` | NFKC, so presentation forms fold into base letters. |
191
+ | `zwnj` | `"normalize"` | Collapse repeated half-spaces; `"remove"` deletes them; `"space"` turns them into spaces. |
192
+ | `correct_half_spaces` | `True` | Join prefixes such as `می` and suffixes such as `ها` and `ترین` with a half-space. |
193
+ | `digits` | `"persian"` | `"persian"`, `"ascii"`, or `"keep"`. Arabic-Indic digits are included. |
194
+ | `remove_diacritics` | `False` | Strip Arabic vowel marks. |
195
+ | `remove_tatweel` | `True` | Remove kashida (`ـ`). |
196
+ | `clean_punctuation` | `True` | Collapse repeats and tidy spaces around punctuation. |
197
+ | `punctuation` | `"persian"` | Map `,;?%` to `،؛؟٪`, the reverse with `"ascii"`, or `"keep"`. |
198
+
199
+ ### Loading PDF and Word files
200
+
201
+ ```python
202
+ from persian_rag import load_documents, load_pdf
203
+
204
+ documents = load_documents("./library") # .txt, .md, .pdf, .docx
205
+ pages = load_pdf("report.pdf", split_pages=True) # one document per page
206
+ ```
207
+
208
+ Many PDF generators store Persian lines in visual order, so extracted words come out reversed (`باتک` instead of `کتاب`). With `rtl_fix="auto"` (the default) each page is checked using two signals: which contextual glyph forms start and end each word, and whether frequent Persian words appear forwards or backwards. Reversed lines are flipped while numbers and Latin words keep their direction. Use `rtl_fix="reverse"` to force the repair or `"none"` to disable it. `clean_extracted_text` exposes the same cleanup for text from other extractors.
209
+
210
+ Word documents keep paragraphs and tables in body order; table rows become `cell | cell` lines.
211
+
212
+ ### Retrieval and reranking
213
+
214
+ ```python
215
+ from persian_rag import CrossEncoderReranker, HybridRetriever, LLMReranker, RAGPipeline
216
+
217
+ hits = retriever.retrieve("حافظ اهل کجا بود؟", k=5, mode="hybrid") # or "dense" / "sparse"
218
+
219
+ pipeline = RAGPipeline(
220
+ retriever,
221
+ my_llm,
222
+ reranker=CrossEncoderReranker(), # needs the rerank extra
223
+ rerank_candidates=20,
224
+ )
225
+ answer = pipeline.query("حافظ اهل کجا بود؟", k=4)
226
+ ```
227
+
228
+ The retriever fetches `rerank_candidates` chunks and the reranker keeps the best `k`. Reranked results carry the reranker score in `score` and the first-stage score in `retrieval_score`. `LLMReranker(llm)` asks a model to order numbered passages and tolerates messy replies; any object with a `rerank(query, results, top_k)` method also works.
229
+
230
+ Hybrid scores are reciprocal rank fusion. Dense mode returns the store score; sparse mode returns Okapi BM25.
231
+
232
+ ### Vector stores
233
+
234
+ ```python
235
+ from persian_rag.vectorstores import PgVectorStore, QdrantVectorStore
236
+
237
+ qdrant = QdrantVectorStore("persian_docs", dimension=384, url="http://localhost:6333")
238
+ local = QdrantVectorStore("persian_docs", dimension=384) # in-process, in memory
239
+
240
+ pg = PgVectorStore(
241
+ "postgresql://user:pass@localhost/db",
242
+ dimension=384,
243
+ table="persian_chunks",
244
+ create_index=True, # HNSW
245
+ )
246
+
247
+ retriever = build_retriever(texts, embedder, store=qdrant)
248
+ ```
249
+
250
+ All stores share one protocol (`add`, `search`, `__len__`); Qdrant and pgvector also support `delete`. Scores are always larger-is-closer.
251
+
252
+ ### Caching and async
253
+
254
+ ```python
255
+ from persian_rag import CachedEmbedder, LRUCache, SQLiteEmbeddingCache
256
+
257
+ embedder = CachedEmbedder(OpenAICompatibleEmbedder(), SQLiteEmbeddingCache("emb.sqlite"))
258
+ retriever = build_retriever(texts, embedder)
259
+ retriever.result_cache = LRUCache(max_size=512, ttl=300)
260
+
261
+ answer = await pipeline.aquery("پرسش") # async LLMs are awaited, sync ones run in a thread
262
+ ```
263
+
264
+ Embedding cache keys include the embedder settings, so changing the model never returns stale vectors. The result cache is cleared whenever chunks are added.
265
+
266
+ ### Evaluation
267
+
268
+ ```python
269
+ from persian_rag import evaluate_answers, evaluate_retrieval, load_eval_examples
270
+
271
+ examples = load_eval_examples("eval.jsonl")
272
+ report = evaluate_retrieval(retriever, examples, k_values=(1, 3, 5, 10))
273
+ print(report.format_table())
274
+
275
+ answers = evaluate_answers(pipeline, examples) # token-overlap faithfulness by default
276
+ ```
277
+
278
+ Each JSONL line holds `question`, `relevant_ids` (document ids, or chunk ids with `match="chunk"`), and optionally `reference_answer`. Faithfulness scorers implement `score(question, answer, contexts)`: `TokenOverlapFaithfulness` is offline and deterministic, `LLMFaithfulnessJudge(llm)` asks a model for a 0 to 1 score.
279
+
280
+ ### HTTP API
281
+
282
+ ```python
283
+ import uvicorn
284
+ from persian_rag.server import create_app
285
+
286
+ uvicorn.run(create_app(pipeline), host="127.0.0.1", port=8000)
287
+ ```
288
+
289
+ Endpoints: `GET /health`, `POST /retrieve` (`{"query": "...", "k": 5, "mode": "hybrid"}`), and `POST /query` (`{"question": "...", "k": 5}`). Interactive docs are served at `/docs`.
290
+
291
+ ### Command line
292
+
293
+ ```bash
294
+ persian-rag ingest ./library --index .persian-rag --embedding-cache .cache/emb.sqlite
295
+ persian-rag query "حافظ اهل کجا بود؟" --index .persian-rag --json
296
+ persian-rag eval eval.jsonl --index .persian-rag --k 1 3 5 10
297
+ persian-rag serve --index .persian-rag --port 8000
298
+ ```
299
+
300
+ `ingest` reads `.txt`, `.md`, `.pdf`, and `.docx`, skips hidden files, and writes a UTF-8 JSON index without API keys. Useful flags: `--embedder hashing|sentence-transformers|openai`, `--strategy`, `--chunk-size`, `--rtl-fix`, `--split-pages`, `--mode`, `--rerank cross-encoder|llm`, `--llm openai`, and `--prompt-lang fa|en`. Run `persian-rag <command> --help` for the rest.
301
+
302
+ ## Documentation
303
+
304
+ ```bash
305
+ pip install -e ".[docs]"
306
+ mkdocs serve
307
+ ```
308
+
309
+ The site has matching English and Persian pages for every topic.
310
+
311
+ ## Development
312
+
313
+ ```bash
314
+ pip install -e ".[dev,server,qdrant,pgvector,pdf,docx]"
315
+ ruff check .
316
+ ruff format --check .
317
+ mypy src
318
+ pytest
319
+ ```
320
+
321
+ Tests run offline. Optional-dependency tests skip when the extra is missing. The pgvector integration test runs when `PERSIAN_RAG_PG_DSN` points at PostgreSQL with the `vector` extension, and heavy model tests run with `PERSIAN_RAG_RUN_HEAVY=1`.
322
+
323
+ Releases are published to PyPI by the `Release` workflow when a `v*` tag is pushed, using trusted publishing (repository variable `PYPI_TRUSTED_PUBLISHING=true`) or a `PYPI_API_TOKEN` secret. Without either, the build still runs and publishing is skipped.
324
+
325
+ ## Roadmap
326
+
327
+ - Morphological normalization for Persian clitics beyond the affix list.
328
+ - OpenSearch and other durable store connectors.
329
+ - HTML loading and incremental re-indexing of changed files.
330
+ - Citation-style answers with span-level grounding.
331
+ - Evaluation recipes for public Persian QA datasets.
332
+
333
+ ## Contributing
334
+
335
+ See [CONTRIBUTING.md](CONTRIBUTING.md). Issues and proposals are welcome in Persian or English.
336
+
337
+ ## License
338
+
339
+ [MIT](LICENSE). Copyright (c) 2026 Naser Sadrmanesh.
340
+
341
+ ## Author
342
+
343
+ Naser Sadrmanesh
344
+
345
+ - GitHub: [https://github.com/sadrmanesh](https://github.com/sadrmanesh)
346
+ - Site: [https://sadrmanesh.github.io](https://sadrmanesh.github.io)
347
+ - Email: [info.sadrmanesh@gmail.com](mailto:info.sadrmanesh@gmail.com)