serviette 0.1.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 (79) hide show
  1. serviette-0.1.0/LICENSE +21 -0
  2. serviette-0.1.0/PKG-INFO +516 -0
  3. serviette-0.1.0/README.md +416 -0
  4. serviette-0.1.0/pyproject.toml +127 -0
  5. serviette-0.1.0/serviette/__init__.py +10 -0
  6. serviette-0.1.0/serviette/cli.py +93 -0
  7. serviette-0.1.0/serviette/config/__init__.py +49 -0
  8. serviette-0.1.0/serviette/config/schema.py +714 -0
  9. serviette-0.1.0/serviette/demo/__init__.py +250 -0
  10. serviette-0.1.0/serviette/demo/corpus/faq.md +12 -0
  11. serviette-0.1.0/serviette/demo/corpus/onboarding.md +12 -0
  12. serviette-0.1.0/serviette/demo/corpus/overview.md +10 -0
  13. serviette-0.1.0/serviette/demo/corpus/pricing.md +12 -0
  14. serviette-0.1.0/serviette/demo/corpus/release-notes.md +10 -0
  15. serviette-0.1.0/serviette/frontend/__init__.py +1 -0
  16. serviette-0.1.0/serviette/frontend/config.py +13 -0
  17. serviette-0.1.0/serviette/frontend/main.py +129 -0
  18. serviette-0.1.0/serviette/frontend/static/index.html +297 -0
  19. serviette-0.1.0/serviette/indexer/__init__.py +6 -0
  20. serviette-0.1.0/serviette/indexer/config.py +13 -0
  21. serviette-0.1.0/serviette/indexer/fingerprint.py +172 -0
  22. serviette-0.1.0/serviette/indexer/graph.py +799 -0
  23. serviette-0.1.0/serviette/indexer/main.py +79 -0
  24. serviette-0.1.0/serviette/indexer/prepare.py +380 -0
  25. serviette-0.1.0/serviette/indexer/sources.py +299 -0
  26. serviette-0.1.0/serviette/quickstart/__init__.py +5 -0
  27. serviette-0.1.0/serviette/quickstart/wizard.py +654 -0
  28. serviette-0.1.0/serviette/server/__init__.py +1 -0
  29. serviette-0.1.0/serviette/server/accessors/__init__.py +53 -0
  30. serviette-0.1.0/serviette/server/accessors/abstract.py +77 -0
  31. serviette-0.1.0/serviette/server/accessors/chroma.py +127 -0
  32. serviette-0.1.0/serviette/server/accessors/duckdb.py +194 -0
  33. serviette-0.1.0/serviette/server/accessors/milvus.py +147 -0
  34. serviette-0.1.0/serviette/server/accessors/mongodb.py +121 -0
  35. serviette-0.1.0/serviette/server/accessors/pgvector.py +134 -0
  36. serviette-0.1.0/serviette/server/accessors/pinecone.py +108 -0
  37. serviette-0.1.0/serviette/server/accessors/qdrant.py +126 -0
  38. serviette-0.1.0/serviette/server/accessors/weaviate.py +126 -0
  39. serviette-0.1.0/serviette/server/bm25.py +77 -0
  40. serviette-0.1.0/serviette/server/config.py +13 -0
  41. serviette-0.1.0/serviette/server/decompose.py +50 -0
  42. serviette-0.1.0/serviette/server/embedder.py +226 -0
  43. serviette-0.1.0/serviette/server/hybrid.py +116 -0
  44. serviette-0.1.0/serviette/server/llm.py +163 -0
  45. serviette-0.1.0/serviette/server/main.py +315 -0
  46. serviette-0.1.0/serviette/server/ranking.py +118 -0
  47. serviette-0.1.0/serviette/server/reranker.py +158 -0
  48. serviette-0.1.0/serviette/testing.py +38 -0
  49. serviette-0.1.0/serviette/up.py +264 -0
  50. serviette-0.1.0/serviette.egg-info/PKG-INFO +516 -0
  51. serviette-0.1.0/serviette.egg-info/SOURCES.txt +77 -0
  52. serviette-0.1.0/serviette.egg-info/dependency_links.txt +1 -0
  53. serviette-0.1.0/serviette.egg-info/entry_points.txt +2 -0
  54. serviette-0.1.0/serviette.egg-info/requires.txt +75 -0
  55. serviette-0.1.0/serviette.egg-info/top_level.txt +2 -0
  56. serviette-0.1.0/setup.cfg +4 -0
  57. serviette-0.1.0/tests/test_cli_preflight.py +35 -0
  58. serviette-0.1.0/tests/test_demo.py +137 -0
  59. serviette-0.1.0/tests/test_demo_path.py +120 -0
  60. serviette-0.1.0/tests/test_fingerprint.py +73 -0
  61. serviette-0.1.0/tests/test_frontend.py +90 -0
  62. serviette-0.1.0/tests/test_hybrid.py +128 -0
  63. serviette-0.1.0/tests/test_indexer.py +250 -0
  64. serviette-0.1.0/tests/test_integration_chroma.py +61 -0
  65. serviette-0.1.0/tests/test_integration_duckdb.py +28 -0
  66. serviette-0.1.0/tests/test_integration_milvus.py +127 -0
  67. serviette-0.1.0/tests/test_integration_mongodb.py +108 -0
  68. serviette-0.1.0/tests/test_integration_pgvector.py +204 -0
  69. serviette-0.1.0/tests/test_integration_pinecone.py +76 -0
  70. serviette-0.1.0/tests/test_integration_qdrant.py +62 -0
  71. serviette-0.1.0/tests/test_integration_s3.py +138 -0
  72. serviette-0.1.0/tests/test_integration_weaviate.py +79 -0
  73. serviette-0.1.0/tests/test_multimodal_parsers.py +102 -0
  74. serviette-0.1.0/tests/test_qdrant_hybrid.py +173 -0
  75. serviette-0.1.0/tests/test_quickstart.py +225 -0
  76. serviette-0.1.0/tests/test_rag_quality.py +377 -0
  77. serviette-0.1.0/tests/test_server.py +226 -0
  78. serviette-0.1.0/tests/test_sources.py +357 -0
  79. serviette-0.1.0/tests/test_up.py +255 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sergey Kulik
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,516 @@
1
+ Metadata-Version: 2.4
2
+ Name: serviette
3
+ Version: 0.1.0
4
+ Summary: A universal, no-code RAG server for any vector database, built on the Pathway Live Data Framework.
5
+ Author: serviette contributors
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Sergey Kulik
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://pathway.com
29
+ Project-URL: Documentation, https://github.com/pathwaycom/serviette
30
+ Keywords: rag,pathway,vector-database,retrieval,llm,embeddings
31
+ Classifier: Programming Language :: Python :: 3
32
+ Classifier: Programming Language :: Python :: 3.10
33
+ Classifier: Programming Language :: Python :: 3.11
34
+ Classifier: Programming Language :: Python :: 3.12
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Operating System :: OS Independent
37
+ Requires-Python: >=3.10
38
+ Description-Content-Type: text/markdown
39
+ License-File: LICENSE
40
+ Requires-Dist: pathway[xpack-llm]>=0.32.1
41
+ Requires-Dist: fastapi>=0.110
42
+ Requires-Dist: uvicorn>=0.27
43
+ Requires-Dist: httpx>=0.27
44
+ Requires-Dist: pydantic>=2.0
45
+ Requires-Dist: pyyaml>=6.0
46
+ Requires-Dist: duckdb>=1.0
47
+ Requires-Dist: openai>=1.0
48
+ Requires-Dist: pypdf>=4.0
49
+ Requires-Dist: unstructured[docx,pptx,xlsx]~=0.18.1
50
+ Provides-Extra: gdrive
51
+ Requires-Dist: google-api-python-client>=2.0; extra == "gdrive"
52
+ Requires-Dist: google-auth>=2.0; extra == "gdrive"
53
+ Provides-Extra: sharepoint
54
+ Requires-Dist: Office365-REST-Python-Client>=2.5; extra == "sharepoint"
55
+ Provides-Extra: pyfilesystem
56
+ Requires-Dist: fs>=2.4; extra == "pyfilesystem"
57
+ Requires-Dist: setuptools<81; extra == "pyfilesystem"
58
+ Provides-Extra: docling
59
+ Requires-Dist: pathway[xpack-llm-docs]; extra == "docling"
60
+ Provides-Extra: ocr
61
+ Requires-Dist: paddleocr>=2.7; extra == "ocr"
62
+ Requires-Dist: paddlepaddle<3.3,>=2.6; extra == "ocr"
63
+ Requires-Dist: paddlex[ocr]>=3.0; extra == "ocr"
64
+ Provides-Extra: pgvector
65
+ Requires-Dist: asyncpg>=0.29; extra == "pgvector"
66
+ Provides-Extra: milvus
67
+ Requires-Dist: pymilvus>=2.4; extra == "milvus"
68
+ Provides-Extra: qdrant
69
+ Requires-Dist: qdrant-client>=1.10; extra == "qdrant"
70
+ Provides-Extra: chroma
71
+ Requires-Dist: chromadb-client>=0.5; extra == "chroma"
72
+ Provides-Extra: weaviate
73
+ Requires-Dist: weaviate-client>=4.7; extra == "weaviate"
74
+ Provides-Extra: pinecone
75
+ Requires-Dist: pinecone>=5.0; extra == "pinecone"
76
+ Provides-Extra: mongodb
77
+ Requires-Dist: pymongo>=4.9; extra == "mongodb"
78
+ Provides-Extra: local
79
+ Requires-Dist: sentence-transformers>=3.0; extra == "local"
80
+ Provides-Extra: gemini
81
+ Requires-Dist: google-generativeai>=0.8; extra == "gemini"
82
+ Provides-Extra: all
83
+ Requires-Dist: serviette[chroma,gdrive,milvus,mongodb,pgvector,pinecone,qdrant,weaviate]; extra == "all"
84
+ Provides-Extra: dev
85
+ Requires-Dist: pytest>=8.0; extra == "dev"
86
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
87
+ Requires-Dist: ruff<0.17,>=0.16; extra == "dev"
88
+ Requires-Dist: mypy>=1.8; extra == "dev"
89
+ Requires-Dist: asyncpg>=0.29; extra == "dev"
90
+ Requires-Dist: pymilvus>=2.4; extra == "dev"
91
+ Requires-Dist: qdrant-client>=1.10; extra == "dev"
92
+ Requires-Dist: chromadb-client>=0.5; extra == "dev"
93
+ Requires-Dist: weaviate-client>=4.7; extra == "dev"
94
+ Requires-Dist: fpdf2>=2.7; extra == "dev"
95
+ Requires-Dist: python-docx>=1.1; extra == "dev"
96
+ Requires-Dist: pinecone[asyncio]>=5.0; extra == "dev"
97
+ Requires-Dist: pymongo>=4.9; extra == "dev"
98
+ Requires-Dist: milvus-lite>=2.4; extra == "dev"
99
+ Dynamic: license-file
100
+
101
+ # serviette
102
+
103
+ **A universal, no-code, always up-to-date RAG server for any vector database
104
+ — powered by the [Pathway](https://pathway.com) Live Data Framework.**
105
+
106
+ Set up Retrieval-Augmented Generation over your own documents without writing
107
+ any code. Point serviette at a folder, pick a vector database and an embedder in
108
+ a YAML file, and run a few commands. From then on, any change you make to the
109
+ documents — an edit, a new file, a deletion — is reflected in answers within
110
+ seconds.
111
+
112
+ <p align="center">
113
+ <img src="docs/assets/demo.gif" alt="serviette: CLI walkthrough then the web chat UI" width="100%">
114
+ </p>
115
+ <p align="center"><em>From zero to a live RAG stack in two commands — then edit a document and watch the answer change.</em></p>
116
+
117
+ ```bash
118
+ pip install serviette
119
+ export OPENAI_API_KEY=sk-... # powers generated answers; omit to run keyless (answers quote the retrieved snippets)
120
+
121
+ serviette quickstart # interactive config wizard
122
+ serviette up --config config.yaml # indexer + server together → http://localhost:8989
123
+ ```
124
+
125
+ (Or start with `serviette demo` — a zero-setup playground on a bundled
126
+ corpus: it copies everything into `./serviette-demo/docs`, and any files you
127
+ drop there while it runs are answerable within seconds. Production
128
+ deployments run `serviette indexer` and `serviette server` separately —
129
+ that is what `up` supervises.)
130
+
131
+ The server hosts both the web chat UI (on `/`) and the versioned REST API
132
+ (under `/api/v1`) on one port:
133
+
134
+ ```bash
135
+ curl -X POST http://localhost:8989/api/v1/retrieve \
136
+ -H 'Content-Type: application/json' \
137
+ -d '{"query": "how does persistence work?", "k": 5}'
138
+ ```
139
+
140
+ ## Highlights
141
+
142
+ - **No code.** Configure everything in one YAML file (or generate it with
143
+ `serviette quickstart`).
144
+ - **Any vector DB — 8 backends.** DuckDB (embedded, zero setup — the default),
145
+ pgvector, Qdrant, Milvus, ChromaDB, Weaviate, Pinecone and MongoDB Atlas
146
+ Vector Search. Every backend is written through **Pathway's native
147
+ connectors**, so file edits and deletions become real upserts/deletes in
148
+ the store.
149
+ - **Zero-setup default.** DuckDB is the default backend: an embedded
150
+ database in a single local file, no external service to install or run,
151
+ with built-in vector search.
152
+ - **Live & incremental.** Built on Pathway: additions, edits and deletions
153
+ are reflected in the vector DB in real time — whatever you change is
154
+ answerable seconds later. Documents flow through the pipeline instead of
155
+ accumulating in it, so a large corpus stays small in memory.
156
+ - **Multiple sources.** Local filesystem, Google Drive, S3/MinIO, SharePoint
157
+ — plus anything the PyFilesystem library opens: FTP, SFTP, WebDAV, even ZIP
158
+ archives. All watched live, mixed freely in one config.
159
+ - **Multimodal out of the box.** Text, Office documents, PDFs (with tables
160
+ and layout), scanned images — and, with the corresponding API keys, audio
161
+ recordings and even video. Every format is on by default and routed to the
162
+ best parser that needs no API key; drop a file in the folder and it is
163
+ answerable like any document. See [Multimodality](#multimodality).
164
+ - **Reuses Pathway's LLM xpack.** Parsers, splitters and embedders are used
165
+ as-is — serviette implements none of its own. Five embedder families (OpenAI,
166
+ LiteLLM, SentenceTransformers, Gemini, Bedrock) work identically on the
167
+ indexer and the server side — including a fully local, credential-free
168
+ stack with local embeddings + DuckDB.
169
+ - **Decoupled & scalable.** Indexer and API server are independent processes
170
+ sharing only the vector DB. The server is stateless and scales
171
+ horizontally; the indexer shards across worker processes with one config
172
+ line. Every part scales on its own.
173
+ - **Web chat UI, same port.** `serviette server` serves a clean
174
+ ChatGPT/Claude-style chat page on `/` next to the versioned API
175
+ (`/api/v1/...`) — same origin, no CORS, nothing extra to run. For split
176
+ deployments (UI on a different host) there is a standalone
177
+ `serviette frontend` proxy tier.
178
+ - **Free Pathway license.** One click at
179
+ <https://pathway.com/framework/get-license>.
180
+
181
+ ## Architecture
182
+
183
+ <p align="center">
184
+ <img src="docs/assets/architecture.svg" alt="serviette architecture: sources feed the Pathway indexer, which writes through Pathway's native connectors into one of 8 vector databases; the stateless server embeds queries, searches the database and serves the chat UI and the /api/v1 REST API" width="100%">
185
+ </p>
186
+
187
+ **The two halves are fully decoupled.** The indexer (write path) and the
188
+ server (read path) are separate processes — different executables that never
189
+ talk to each other. Their only contract is the vector database itself:
190
+
191
+ - **Independent scaling.** The server is stateless and read-only — run any
192
+ number of instances behind a load balancer; each also serves the chat UI at
193
+ zero cost. The indexer scales separately: Pathway shards it across worker
194
+ processes (`indexer.workers: 8` is how the benchmarks below run), so every
195
+ part of the stack scales independently. Bulk re-indexing never slows down
196
+ query serving, and query spikes never stall indexing.
197
+ - **Failure isolation.** If the indexer is down, serving continues over the
198
+ last-synced data; if the server is down, indexing keeps the database fresh.
199
+ Either side can be restarted or upgraded independently (the indexer resumes
200
+ from its persistence without re-embedding).
201
+ - **The database stays yours.** Vectors live in *your* store in a plain,
202
+ documented schema — other consumers (BI, other apps, a different retrieval
203
+ stack) can read the same collection; serviette doesn't hold it hostage. And
204
+ since the default store is an embedded DuckDB file, trying this out costs
205
+ nothing to set up.
206
+ - **Optional third tier.** For split deployments (UI on a different host than
207
+ the API) a standalone `serviette frontend` serves the same chat page and
208
+ proxies to the API server-side.
209
+
210
+ The one deliberate exception: the embedded DuckDB backend trades this
211
+ distribution for zero setup — one local file, single-writer, ideal for
212
+ laptops and demos (see [docs](docs/README.md) for its concurrency note).
213
+
214
+ ## Benchmarks
215
+
216
+ Two self-contained benchmarks, one per axis: what indexing costs in time
217
+ and memory, and how accurate the retrieval is.
218
+
219
+ ### Indexing resources
220
+
221
+ Self-contained benchmark (docker-compose: Qdrant + indexer + server, fully
222
+ local embeddings, zero API cost) over a Wikipedia corpus of plain text —
223
+ every byte below is extracted text (a PDF collection with the same text
224
+ content would weigh several times more) —
225
+ see [benchmarks/realtime-data-indexing](benchmarks/realtime-data-indexing):
226
+
227
+ | corpus | ≈ pages | files | chunks | indexing time | peak memory (PSS) | in Qdrant |
228
+ |---|---|---|---|---|---|---|
229
+ | 100 MB | 52 000 | 12 969 | 66 136 | 39 s | 6.6 GB | 0.6 GB |
230
+ | 1 GB | 524 000 | 240 516 | 836 595 | 4.8 min | 6.9 GB | 2.2 GB |
231
+ | 3 GB | 1 573 000 | 841 890 | 2 703 850 | 15 min | 7.3 GB | 6.0 GB |
232
+ | 10 GB | 5 243 000 | 3 423 359 | 10 093 514 | 58 min | 7.7 GB | 20.8 GB |
233
+ | 30 GB | 15 729 000 | 9 202 620 | 29 817 294 | 2.9 h | 10.1 GB | 61.3 GB |
234
+ | 50 GB | 26 214 000 | 17 083 603 | 53 913 774 | 5.5 h | 13.1 GB | 107.9 GB |
235
+
236
+ Documents flow through the pipeline rather than accumulating in it, so
237
+ what stays in memory is short and worth spelling out.
238
+
239
+ **Grows with the corpus — one thing.** The file-watch index: to detect live
240
+ edits and deletions, the indexer keeps a record (path, mtime, size, owner)
241
+ per watched file. Measured cost: **~318 bytes per file** (paths of typical
242
+ length; ±20% with the hash-table's load factor), verified from 13 thousand
243
+ to 17 million files (right-hand plot: six corpus sizes against one fitted
244
+ line). It scales with the *number of files*, not bytes: the same corpus
245
+ packed into fewer, larger files costs proportionally less.
246
+
247
+ **Constant, regardless of corpus size.** The embedding stack (PyTorch
248
+ runtime + model, per worker), the engine baseline (~200 MB per process),
249
+ connector machinery (~0.4 GB), and working buffers that reach a plateau in
250
+ the first minutes of a run and stay there — identical on 3 GB and 10 GB.
251
+
252
+ **On disk, not in memory.** Parsed-text cache, persistence snapshots, and
253
+ the embeddings themselves (in the vector database). That is why the curves
254
+ plateau: a **500× larger corpus costs 2.5× the memory** — and the growth
255
+ that remains is the file-watch index above, i.e. the corpus in fewer files
256
+ would cost less. Indexing time scales linearly with bytes throughout.
257
+
258
+ <p align="center">
259
+ <img src="docs/assets/bench-memory.png" alt="Left: indexer PSS over time for corpora from 100 MB to 50 GB; every curve plateaus between 7 and 16 GB. Right: connector-worker extra memory across six corpus sizes follows ~318 bytes per watched file" width="100%">
260
+ </p>
261
+
262
+ The peak itself is dominated by the embedding stack, not the engine — a
263
+ Pathway worker process is ~200 MB; the rest is the price of running
264
+ embeddings locally (8 × PyTorch runtime + model), i.e. of paying no
265
+ per-token API fees. Fewer workers or an API embedder shrink it accordingly.
266
+
267
+ <p align="center">
268
+ <img src="docs/assets/bench-memory-breakdown.png" alt="Breakdown of the 8.8 GB peak on the 10 GB corpus: three quarters is the local PyTorch embedding stack across 8 workers; file-watch metadata is about 1.1 GB; supervisors and shared code make up the rest" width="85%">
269
+ </p>
270
+
271
+ Memory is measured as PSS (proportional set size) summed over the container:
272
+ shared pages — e.g. the PyTorch libraries mapped by every worker — are
273
+ counted once, not once per process. Setup: 96-core CPU host, streaming mode,
274
+ 8 worker processes, local `static-retrieval-mrl-en-v1` embeddings (no API
275
+ calls; Matryoshka-truncated to 256 dims), 512-token chunks, Qdrant, and
276
+ jemalloc's `background_thread` purging enabled in the indexer containers
277
+ (measured free; it keeps idle workers from retaining freed pages). Numbers
278
+ were measured on a nightly Pathway build whose engine matches the released
279
+ wheel (pathway ≥ 0.32.1 — what the benchmark's docker image and the
280
+ Development section install), so they are reproducible as-is.
281
+
282
+ ### Retrieval accuracy (FRAMES)
283
+
284
+ End-to-end evaluation on [FRAMES](https://arxiv.org/abs/2409.12941)
285
+ (Google, 2024): 824 multi-hop questions whose answers must be assembled
286
+ from 2–15 English Wikipedia articles — see
287
+ [benchmarks/frames](benchmarks/frames), full technical report in
288
+ [REPORT.md](benchmarks/frames/REPORT.md):
289
+
290
+ | measurement | result |
291
+ |---|---|
292
+ | gold-article recall — the paper's metric, on the paper's corpus | **0.50** vs 0.15 published for the paper's BM25 baseline (0.21 for our reproduction of it) |
293
+ | paired gain from adding serviette to gpt-5, paper's protocol | **+5.2 pp** over the same model without retrieval (McNemar z = 4.1, 824 questions) |
294
+ | adaptive retrieval in the grounded (context-only) regime | **41.3% → 52.8%** (z = 7.5) — the largest single effect measured |
295
+ | absolute accuracy (permissive, gpt-5) | **73.7%** — above every number in the paper, including its 5-step agent (66.0%) and oracle (72.9%) |
296
+
297
+ The setup reproduces the paper wherever technically possible: the identical
298
+ Wikipedia dump (TFDS `wikipedia/20230601.en`, 5.22M articles → 12.07M
299
+ chunks) indexed in full by serviette with the free local `e5-small-v2`
300
+ embedder — so the recall row costs nothing in API fees — plus the paper's
301
+ own retrieval metric and its verbatim autorater prompt. Comparisons are
302
+ paired and internal (identical corpus, generator, judge; only the retrieval
303
+ layer varies — the opt-in strategies described under
304
+ [Retrieval quality](#retrieval-quality) below), because the absolute score
305
+ is dominated by the 2026
306
+ generator — its no-retrieval baseline alone reaches 68.5% — which is why
307
+ the headline is the paired delta, not 73.7%. The grounded rows measure
308
+ serviette as it ships for private corpora: answers strictly from retrieved
309
+ documents, a deliberately stricter regime than the paper's. Methodology,
310
+ statistics, limitations and raw per-question outputs:
311
+ [REPORT.md](benchmarks/frames/REPORT.md).
312
+
313
+ ## Multimodality
314
+
315
+ Every file type is enabled by default. serviette routes each file to the best
316
+ parser that works **without an API key**, and turns on key-requiring
317
+ modalities automatically when their key is present:
318
+
319
+ | format | parsed by default with | notes |
320
+ |---|---|---|
321
+ | text / Markdown | as-is | |
322
+ | PDF | pypdf — **built in**; `serviette[docling]` upgrades to Docling (layout-aware, tables) | local, free |
323
+ | Office (DOCX, PPTX, XLSX, HTML, EML…) | Unstructured — **built in**; `serviette[docling]` widens coverage (EPUB, legacy formats) | local, free |
324
+ | scanned images (PNG, JPG, TIFF…) | PaddleOCR | local, free |
325
+ | audio (MP3, WAV…) | Whisper | when `OPENAI_API_KEY` is set |
326
+ | video (MP4, WebM, MOV…) | TwelveLabs Pegasus — a searchable text description of the video | when `TWELVELABS_API_KEY` is set |
327
+
328
+ A modality whose only parser needs an absent key is skipped with a clear
329
+ warning — never a crash. Everything stays live: drop a recording of
330
+ yesterday's meeting into the watched folder and ask about it minutes later;
331
+ expensive parses (video) are cached on disk, so restarts cost nothing.
332
+
333
+ The routing is configurable per file pattern (`parser:` section — pick a
334
+ vision model for images instead of OCR, set a custom video prompt); see
335
+ [docs](docs/README.md). Embeddings work the same for every modality: parsed
336
+ content is text, so any of the embedder families — including the local
337
+ credential-free default — covers a multimodal corpus.
338
+
339
+ ## Retrieval quality
340
+
341
+ Retrieval and answering are pure-vector by default; five opt-in strategies
342
+ improve accuracy, each a few lines of config. They compose freely — decompose
343
+ widens *what* is retrieved, hybrid and reranking reorder *which* chunks win,
344
+ MMR *diversifies* them, and adaptive RAG *retries* with more context when the
345
+ answer is not found.
346
+
347
+ - **Hybrid (BM25 + vector).** Fuses vector similarity with an in-process BM25
348
+ keyword index (reciprocal-rank fusion). The two signals fail differently —
349
+ embeddings match paraphrases, BM25's IDF makes rare exact tokens (names,
350
+ dates, IDs) dominate — so their fusion beats either alone on entity-heavy
351
+ corpora. Enable with `hybrid: true` on the vector-db section.
352
+ - **Reranker.** A second stage rescoring a shortlist of candidates: a local,
353
+ keyless cross-encoder (`reranker: {type: cross_encoder}`) or pointwise LLM
354
+ scoring (`type: llm`). Best for precise, single-hop questions.
355
+ - **Adaptive RAG.** Answers from `k` chunks first, then grows the context and
356
+ re-asks while the LLM reports the answer is not present (`rag.adaptive`).
357
+ - **Query decomposition.** One LLM call splits a multi-hop question into
358
+ sub-queries, retrieves for each, and fuses — so chunks of different hops
359
+ stop competing for the same top-k slots (`rag.decompose`).
360
+ - **MMR.** Diversifies the result set, trading relevance against redundancy
361
+ (`rag.mmr`).
362
+
363
+ The reranker and the multi-step strategies (adaptive, decompose) are
364
+ backend-independent — they work on every vector DB. Hybrid and MMR read from
365
+ the store, so support depends on the backend:
366
+
367
+ | Backend | Vector | Reranker · Adaptive · Decompose | MMR | Hybrid (BM25) |
368
+ |----------|:------:|:-------------------------------:|:---:|:-------------:|
369
+ | DuckDB | ✅ | ✅ | ✅ | ✅ in-process |
370
+ | Qdrant | ✅ | ✅ | ✅ | ✅ in-process |
371
+ | pgvector | ✅ | ✅ | ✅ | ✅ in-process |
372
+ | Milvus | ✅ | ✅ | ✅ | ✅ in-process |
373
+ | Weaviate | ✅ | ✅ | ✅ | ✅ in-process |
374
+ | ChromaDB | ✅ | ✅ | ✅ | ✅ in-process |
375
+ | MongoDB | ✅ | ✅ | ✅ | ✅ in-process |
376
+ | Pinecone | ✅ | ✅ | ✅ | ❌ — no scan-all API; native sparse-index hybrid is planned |
377
+
378
+ In-process BM25 targets corpora up to a few million chunks; above
379
+ `hybrid_max_chunks` the keyword leg is skipped with a warning and retrieval
380
+ stays pure-vector. **Pinecone** cannot enumerate its vectors, so it has no
381
+ in-process hybrid — `hybrid: true` there fails fast at startup with that
382
+ explanation; native hybrid (a second sparse index) is planned. Native
383
+ server-side BM25 for the client-server backends (replacing the in-process
384
+ leg at larger scale) is planned as well.
385
+
386
+ ## Observability
387
+
388
+ Three layers, all on by default or one config line away:
389
+
390
+ **In the chat UI.** The header shows *"indexed N s ago"* — the age of the
391
+ most recent write into the vector store. When you edit a source document,
392
+ you can watch the counter reset as the change lands.
393
+
394
+ **`GET /api/v1/stats`** on the API server: the backend in use, the number of
395
+ indexed chunks, and index freshness — a JSON one-liner for dashboards and
396
+ health checks, served without touching the indexer (it reads the vector
397
+ store, like every other query).
398
+
399
+ **Engine metrics (Prometheus).** The Pathway engine ships its own
400
+ observability server; serviette exposes it with one config line:
401
+
402
+ ```yaml
403
+ indexer:
404
+ monitoring_http_port: 20000
405
+ ```
406
+
407
+ Every worker process then serves `GET /metrics` on
408
+ `127.0.0.1:(20000 + worker index)` — input/output latency gauges (i.e. the
409
+ indexing lag behind the sources) and per-operator row counters, straight
410
+ from the engine's dataflow. Point a Prometheus scrape at the worker ports
411
+ and you get per-stage throughput and freshness graphs with no extra code.
412
+
413
+ Logs from both processes go to stdout/stderr in plain text; `serviette up`
414
+ interleaves them with per-process prefixes.
415
+
416
+ ## Security
417
+
418
+ The server listens on **localhost only** by default and ships no built-in
419
+ authentication — exposing it is an explicit decision: set `server.host:
420
+ 0.0.0.0` and put an authenticating reverse proxy in front (a five-line
421
+ Caddy example lives in [docs](docs/README.md#security--exposing-the-server)).
422
+
423
+ ## Requirements
424
+
425
+ Python ≥ 3.10 (the minimum supported by Pathway).
426
+
427
+ ## Documentation
428
+
429
+ Full installation, quickstart, configuration reference, persistence,
430
+ architecture and scaling notes live in **[docs/README.md](docs/README.md)**.
431
+
432
+ ## Development
433
+
434
+ serviette runs on the released Pathway from PyPI (≥ 0.32.1 — the first
435
+ release with the vector-database connectors). From-scratch setup on a
436
+ fresh machine:
437
+
438
+ ```bash
439
+ # 0. Prerequisites: Python >= 3.10 (3.12 recommended) and git.
440
+
441
+ # 1. serviette in its own virtualenv
442
+ git clone https://github.com/pathwaycom/serviette.git && cd serviette
443
+ python3.12 -m venv .venv
444
+ source .venv/bin/activate
445
+ pip install -e ".[dev,local]"
446
+
447
+ # 2. A (free) Pathway license: https://pathway.com/framework/get-license
448
+ export PATHWAY_LICENSE_KEY=...
449
+
450
+ # 3. Sanity check: zero-to-chat on the bundled corpus
451
+ serviette demo # -> http://localhost:8989
452
+ ```
453
+
454
+ Notes:
455
+
456
+ - `serviette demo` materializes everything in a visible working directory:
457
+ `./serviette-demo/docs` holds the corpus — a **toy example**, a handful of
458
+ documents about a fictional company (Lumina Coffee Systems). Drop your own
459
+ files there (PDF, DOCX, scans, …) while it runs and they are answerable
460
+ within seconds.
461
+ - The demo indexes with one of two embedders — pick your trade-off:
462
+ - **default, free & local** — needs `serviette[local]`, which pulls the
463
+ PyTorch stack (**~4.5 GB**): on a typical laptop connection the install
464
+ itself is the slow part, so the first run takes a while. Free at any
465
+ corpus size afterwards.
466
+ - **`--embedder openai`** — nothing to install, starts immediately, but
467
+ every indexed token is billed to your `OPENAI_API_KEY`. Fine for the toy
468
+ corpus and small folders; for a large collection, sit out the
469
+ `serviette[local]` install and use the free embedder instead.
470
+ - Independently of the embedder, export `OPENAI_API_KEY` if you want real
471
+ generated answers in `/rag` — without it the demo answers by quoting the
472
+ retrieved snippets.
473
+
474
+ Running the test suites:
475
+
476
+ ```bash
477
+ pytest -m "not slow" # fast unit tests (no Pathway, no services)
478
+ pytest -m "slow and not integration" # end-to-end indexer tests (spin up Pathway)
479
+ pytest -m integration # real-database tests (see below)
480
+ pytest # everything
481
+ ```
482
+
483
+ Per-backend clients install as extras — pick what you use:
484
+
485
+ ```bash
486
+ pip install "serviette[qdrant]" # also: pgvector, milvus, chroma, weaviate,
487
+ # pinecone, mongodb, local, gemini, all
488
+ ```
489
+
490
+ ### Integration tests (real databases)
491
+
492
+ **Every claimed backend has an integration test** running the same scenario
493
+ end-to-end against a real instance: index two documents with the real indexer,
494
+ retrieve through the production accessor (exact-text query must rank first
495
+ with cosine ~1.0), delete a file, re-index, and verify its vectors are gone
496
+ (snapshot semantics). The shared driver lives in `tests/integration_common.py`.
497
+
498
+ | Backend | Test | Real instance |
499
+ |---|---|---|
500
+ | DuckDB | `test_integration_duckdb.py` | embedded — runs everywhere |
501
+ | pgvector | `test_integration_pgvector.py` | `pgvector/pgvector` Docker container |
502
+ | Milvus | `test_integration_milvus.py` | embedded Milvus Lite engine |
503
+ | Qdrant | `test_integration_qdrant.py` | `qdrant/qdrant` Docker container |
504
+ | ChromaDB | `test_integration_chroma.py` | `chromadb/chroma` Docker container |
505
+ | Weaviate | `test_integration_weaviate.py` | `semitechnologies/weaviate` Docker container |
506
+ | Pinecone | `test_integration_pinecone.py` | official `pinecone-local` emulator (Docker) |
507
+ | MongoDB | `test_integration_mongodb.py` | `mongodb-atlas-local` (mongod + mongot, real `$vectorSearch`) |
508
+
509
+ Containers are throwaway (`tests/dockerutil.py`, Docker CLI via subprocess, no
510
+ extra dependency) and host ports are **allocated dynamically** — tests never
511
+ assume a fixed localhost port is free or that a service is already running.
512
+ Each test skips automatically when Docker or its client library is missing.
513
+
514
+ ## License
515
+
516
+ See [LICENSE](LICENSE).