raglite-toolkit 1.0.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 (68) hide show
  1. raglite_toolkit-1.0.0/.github/workflows/publish.yml +36 -0
  2. raglite_toolkit-1.0.0/.gitignore +44 -0
  3. raglite_toolkit-1.0.0/LICENSE +21 -0
  4. raglite_toolkit-1.0.0/PKG-INFO +388 -0
  5. raglite_toolkit-1.0.0/README.md +343 -0
  6. raglite_toolkit-1.0.0/examples/basic.py +47 -0
  7. raglite_toolkit-1.0.0/examples/multi_provider.py +60 -0
  8. raglite_toolkit-1.0.0/examples/ollama_test.py +186 -0
  9. raglite_toolkit-1.0.0/examples/sample.txt +24 -0
  10. raglite_toolkit-1.0.0/examples/serve.py +52 -0
  11. raglite_toolkit-1.0.0/pyproject.toml +74 -0
  12. raglite_toolkit-1.0.0/src/raglite/__init__.py +114 -0
  13. raglite_toolkit-1.0.0/src/raglite/api/__init__.py +4 -0
  14. raglite_toolkit-1.0.0/src/raglite/api/schemas.py +26 -0
  15. raglite_toolkit-1.0.0/src/raglite/api/server.py +198 -0
  16. raglite_toolkit-1.0.0/src/raglite/chunking/__init__.py +4 -0
  17. raglite_toolkit-1.0.0/src/raglite/chunking/base.py +13 -0
  18. raglite_toolkit-1.0.0/src/raglite/chunking/recursive.py +34 -0
  19. raglite_toolkit-1.0.0/src/raglite/cli.py +212 -0
  20. raglite_toolkit-1.0.0/src/raglite/config.py +65 -0
  21. raglite_toolkit-1.0.0/src/raglite/constants.py +18 -0
  22. raglite_toolkit-1.0.0/src/raglite/core/__init__.py +1 -0
  23. raglite_toolkit-1.0.0/src/raglite/core/document.py +361 -0
  24. raglite_toolkit-1.0.0/src/raglite/embeddings/__init__.py +13 -0
  25. raglite_toolkit-1.0.0/src/raglite/embeddings/base.py +41 -0
  26. raglite_toolkit-1.0.0/src/raglite/embeddings/factory.py +18 -0
  27. raglite_toolkit-1.0.0/src/raglite/embeddings/local.py +66 -0
  28. raglite_toolkit-1.0.0/src/raglite/embeddings/models.py +11 -0
  29. raglite_toolkit-1.0.0/src/raglite/embeddings/remote.py +158 -0
  30. raglite_toolkit-1.0.0/src/raglite/errors.py +47 -0
  31. raglite_toolkit-1.0.0/src/raglite/llm/__init__.py +14 -0
  32. raglite_toolkit-1.0.0/src/raglite/llm/answer.py +311 -0
  33. raglite_toolkit-1.0.0/src/raglite/llm/factory.py +119 -0
  34. raglite_toolkit-1.0.0/src/raglite/llm/models.py +12 -0
  35. raglite_toolkit-1.0.0/src/raglite/llm/prompt.py +62 -0
  36. raglite_toolkit-1.0.0/src/raglite/loaders/__init__.py +34 -0
  37. raglite_toolkit-1.0.0/src/raglite/loaders/base.py +11 -0
  38. raglite_toolkit-1.0.0/src/raglite/loaders/docx.py +15 -0
  39. raglite_toolkit-1.0.0/src/raglite/loaders/json.py +40 -0
  40. raglite_toolkit-1.0.0/src/raglite/loaders/markdown.py +11 -0
  41. raglite_toolkit-1.0.0/src/raglite/loaders/pdf.py +17 -0
  42. raglite_toolkit-1.0.0/src/raglite/loaders/txt.py +15 -0
  43. raglite_toolkit-1.0.0/src/raglite/retrieval/__init__.py +3 -0
  44. raglite_toolkit-1.0.0/src/raglite/retrieval/retriever.py +57 -0
  45. raglite_toolkit-1.0.0/src/raglite/types.py +95 -0
  46. raglite_toolkit-1.0.0/src/raglite/utils/__init__.py +1 -0
  47. raglite_toolkit-1.0.0/src/raglite/utils/hash.py +26 -0
  48. raglite_toolkit-1.0.0/src/raglite/utils/logger.py +44 -0
  49. raglite_toolkit-1.0.0/src/raglite/vectordb/__init__.py +4 -0
  50. raglite_toolkit-1.0.0/src/raglite/vectordb/base.py +55 -0
  51. raglite_toolkit-1.0.0/src/raglite/vectordb/memory.py +108 -0
  52. raglite_toolkit-1.0.0/tests/__init__.py +1 -0
  53. raglite_toolkit-1.0.0/tests/conftest.py +74 -0
  54. raglite_toolkit-1.0.0/tests/integration/__init__.py +1 -0
  55. raglite_toolkit-1.0.0/tests/integration/test_api.py +112 -0
  56. raglite_toolkit-1.0.0/tests/integration/test_ask.py +102 -0
  57. raglite_toolkit-1.0.0/tests/integration/test_document.py +142 -0
  58. raglite_toolkit-1.0.0/tests/integration/test_ollama.py +330 -0
  59. raglite_toolkit-1.0.0/tests/unit/__init__.py +1 -0
  60. raglite_toolkit-1.0.0/tests/unit/test_chunking.py +55 -0
  61. raglite_toolkit-1.0.0/tests/unit/test_cli.py +159 -0
  62. raglite_toolkit-1.0.0/tests/unit/test_config.py +41 -0
  63. raglite_toolkit-1.0.0/tests/unit/test_errors.py +43 -0
  64. raglite_toolkit-1.0.0/tests/unit/test_hash.py +72 -0
  65. raglite_toolkit-1.0.0/tests/unit/test_loaders.py +100 -0
  66. raglite_toolkit-1.0.0/tests/unit/test_prompt.py +68 -0
  67. raglite_toolkit-1.0.0/tests/unit/test_retriever.py +69 -0
  68. raglite_toolkit-1.0.0/tests/unit/test_vectordb.py +114 -0
@@ -0,0 +1,36 @@
1
+ name: Publish Python Package to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ build-n-publish:
10
+ name: Build and publish Python distribution to PyPI
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ id-token: write # Mandatory for Trusted Publishing (OIDC)
14
+ contents: read
15
+
16
+ steps:
17
+ - name: Checkout code
18
+ uses: actions/checkout@v4
19
+
20
+ - name: Set up Python
21
+ uses: actions/setup-python@v5
22
+ with:
23
+ python-version: "3.12"
24
+
25
+ - name: Install build tools
26
+ run: |
27
+ python -m pip install --upgrade pip
28
+ pip install build
29
+
30
+ - name: Build distribution packages
31
+ run: python -m build
32
+
33
+ - name: Publish package distribution to PyPI
34
+ uses: pypa/gh-action-pypi-publish@release/v1
35
+ with:
36
+ skip-existing: true
@@ -0,0 +1,44 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.pyd
6
+ *.so
7
+ *.egg
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+ .eggs/
12
+ wheels/
13
+
14
+ # Virtual environments
15
+ .venv/
16
+ venv/
17
+ env/
18
+
19
+ # pytest / coverage
20
+ .pytest_cache/
21
+ .coverage
22
+ htmlcov/
23
+ coverage.xml
24
+
25
+ # raglite index stores
26
+ .raglite/
27
+ .raglite_*/
28
+
29
+ # IDE
30
+ .idea/
31
+ .vscode/
32
+ *.swp
33
+ *.swo
34
+
35
+ # macOS
36
+ .DS_Store
37
+
38
+ # Environment variables
39
+ .env
40
+ .env.local
41
+ *.env
42
+
43
+ # Logs
44
+ *.log
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
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,388 @@
1
+ Metadata-Version: 2.4
2
+ Name: raglite-toolkit
3
+ Version: 1.0.0
4
+ Summary: Build semantic search, multi-provider question answering, and REST APIs over your documents in a few lines of Python.
5
+ Project-URL: Homepage, https://github.com/piyush-anand/raglite
6
+ Project-URL: Repository, https://github.com/piyush-anand/raglite
7
+ Project-URL: Bug Tracker, https://github.com/piyush-anand/raglite/issues
8
+ Author: Piyush Anand
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: anthropic,embeddings,llm,ollama,openai,rag,retrieval-augmented-generation,semantic-search,vector-store
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: anthropic>=0.18.0
24
+ Requires-Dist: cohere>=5.0.0
25
+ Requires-Dist: fastapi>=0.100.0
26
+ Requires-Dist: google-generativeai>=0.3.0
27
+ Requires-Dist: httpx>=0.25.0
28
+ Requires-Dist: mistralai>=1.0.0
29
+ Requires-Dist: openai>=1.0.0
30
+ Requires-Dist: pydantic>=2.0
31
+ Requires-Dist: pypdf>=4.0
32
+ Requires-Dist: python-docx>=1.0
33
+ Requires-Dist: sentence-transformers>=2.2.0
34
+ Requires-Dist: uvicorn>=0.20.0
35
+ Requires-Dist: voyageai>=0.1.0
36
+ Provides-Extra: dev
37
+ Requires-Dist: build>=1.0.0; extra == 'dev'
38
+ Requires-Dist: httpx>=0.25.0; extra == 'dev'
39
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
40
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
41
+ Requires-Dist: pytest-mock>=3.10.0; extra == 'dev'
42
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
43
+ Requires-Dist: twine>=5.0.0; extra == 'dev'
44
+ Description-Content-Type: text/markdown
45
+
46
+ # raglite-toolkit
47
+
48
+ > Build semantic search, multi-provider question answering, and REST APIs over your documents in a few lines of Python.
49
+
50
+ **raglite-toolkit** is a Python port of the [`raglite-toolkit`](https://github.com/creatorpiyush/raglite) TypeScript package with full feature parity.
51
+
52
+ ---
53
+
54
+ ## Features
55
+
56
+ - 📄 **PDF, TXT, JSON, Markdown, DOCX** loaders out of the box
57
+ - 🤖 **Multi-provider LLMs** — OpenAI, Anthropic (Claude), Google (Gemini), Mistral, Cohere, Groq, xAI, Ollama
58
+ - 🔢 **Multi-provider embeddings** — OpenAI, Google, Mistral, Cohere, Voyage, Ollama, or a **local** offline sentence-transformer (no API key needed)
59
+ - 📐 **Cosine similarity** scoring with L2-normalized vectors
60
+ - ♻️ **Content-hash cache** — reindexes only when the file actually changes
61
+ - 🗂 **Per-document namespacing** — indexes are isolated, two documents never collide
62
+ - 🌐 **REST API** via FastAPI with optional **bearer-token auth**
63
+ - ⚡ **Streaming** answers
64
+ - 🐍 **Python-native** — Pydantic models, type-annotated, fully testable
65
+
66
+ ---
67
+
68
+ ## Install
69
+
70
+ ```bash
71
+ pip install raglite-toolkit
72
+ ```
73
+
74
+ For **local offline embeddings** (no API key required):
75
+
76
+ ```bash
77
+ pip install raglite-toolkit sentence-transformers
78
+ ```
79
+
80
+ > `sentence-transformers` is included by default. The `all-MiniLM-L6-v2` model (~90 MB) is downloaded automatically on first use.
81
+
82
+ ---
83
+
84
+ ## Quick Start
85
+
86
+ ```python
87
+ from raglite import Document
88
+
89
+ doc = Document("./policy.pdf", {
90
+ "embeddings": {"provider": "openai", "apiKey": "sk-..."},
91
+ "llm": {"provider": "anthropic", "apiKey": "sk-ant-..."},
92
+ })
93
+
94
+ doc.build() # chunk → embed → persist
95
+
96
+ hits = doc.search("refund policy", top_k=3)
97
+
98
+ answer = doc.ask("What is the refund policy?")
99
+ print(answer.text)
100
+ ```
101
+
102
+ ---
103
+
104
+ ## Fully Offline — No API Key Needed
105
+
106
+ ```python
107
+ from raglite import Document
108
+
109
+ doc = Document("./policy.pdf", {
110
+ "embeddings": {"provider": "local"}, # sentence-transformers offline
111
+ "llm": {"provider": "ollama", # local Ollama instance
112
+ "model": "llama3.2",
113
+ "baseURL": "http://localhost:11434/api"},
114
+ })
115
+
116
+ doc.build()
117
+ print(doc.ask("What is the refund policy?").text)
118
+ ```
119
+
120
+ ---
121
+
122
+ ## Choose Any LLM at Ask-Time
123
+
124
+ ```python
125
+ # Switch LLMs without rebuilding the index — embeddings are reused
126
+ for llm in [
127
+ {"provider": "openai", "model": "gpt-4o", "apiKey": "sk-..."},
128
+ {"provider": "anthropic", "model": "claude-3-5-sonnet-20241022","apiKey": "sk-ant-..."},
129
+ {"provider": "google", "model": "gemini-2.0-flash", "apiKey": "AI..."},
130
+ {"provider": "groq", "model": "llama-3.3-70b-versatile", "apiKey": "gsk_..."},
131
+ ]:
132
+ answer = doc.ask("Summarize this document", {"llm": llm})
133
+ print(f"[{llm['provider']}] {answer.text[:120]}")
134
+ ```
135
+
136
+ ---
137
+
138
+ ## Streaming
139
+
140
+ ```python
141
+ for chunk in doc.ask_stream("Explain the introduction"):
142
+ print(chunk, end="", flush=True)
143
+ ```
144
+
145
+ ---
146
+
147
+ ## REST API
148
+
149
+ ```python
150
+ handle = doc.serve({
151
+ "port": 8085,
152
+ "llm": {"provider": "openai", "apiKey": "sk-..."},
153
+ "bearerToken": "my-secret-token",
154
+ })
155
+ print(f"Serving on {handle.url}")
156
+
157
+ # ... later
158
+ handle.close()
159
+ ```
160
+
161
+ ### Endpoints
162
+
163
+ | Method | Path | Auth? | Description |
164
+ |--------|-----------|-------|-------------|
165
+ | `GET` | `/health` | ❌ | Liveness + index stats |
166
+ | `GET` | `/info` | ✅ | Configuration snapshot |
167
+ | `POST` | `/search` | ✅ | Semantic search |
168
+ | `POST` | `/ask` | ✅ | Question answering |
169
+
170
+ ### Example `curl` calls
171
+
172
+ ```bash
173
+ # Health check (no auth)
174
+ curl http://127.0.0.1:8085/health
175
+
176
+ # Search
177
+ curl -X POST http://127.0.0.1:8085/search \
178
+ -H 'Authorization: Bearer my-secret-token' \
179
+ -H 'Content-Type: application/json' \
180
+ -d '{"query": "refund policy", "topK": 3}'
181
+
182
+ # Ask (non-streaming)
183
+ curl -X POST http://127.0.0.1:8085/ask \
184
+ -H 'Authorization: Bearer my-secret-token' \
185
+ -H 'Content-Type: application/json' \
186
+ -d '{"question": "What is the refund policy?"}'
187
+
188
+ # Ask (streaming)
189
+ curl -X POST http://127.0.0.1:8085/ask \
190
+ -H 'Authorization: Bearer my-secret-token' \
191
+ -H 'Content-Type: application/json' \
192
+ -d '{"question": "Summarize the document", "stream": true}'
193
+ ```
194
+
195
+ ---
196
+
197
+ ## CLI
198
+
199
+ ```bash
200
+ # Index a document
201
+ raglite index ./policy.pdf --embed-provider local
202
+
203
+ # Semantic search
204
+ raglite search ./policy.pdf "refund policy" --top-k 5
205
+
206
+ # Ask a question (streaming)
207
+ raglite ask ./policy.pdf "What is the refund policy?" \
208
+ --llm-provider openai --llm-key $OPENAI_API_KEY --stream
209
+
210
+ # Serve a REST API
211
+ raglite serve ./policy.pdf \
212
+ --llm-provider openai --llm-key $OPENAI_API_KEY \
213
+ --port 8085 --token $RAGLITE_TOKEN
214
+ ```
215
+
216
+ ---
217
+
218
+ ## Supported Providers
219
+
220
+ ### LLMs
221
+
222
+ | Provider | `provider` key | Default model |
223
+ |----------------|----------------|---------------|
224
+ | OpenAI | `openai` | `gpt-4o-mini` |
225
+ | Anthropic | `anthropic` | `claude-3-5-sonnet-20241022` |
226
+ | Google | `google` | `gemini-2.0-flash` |
227
+ | Mistral | `mistral` | `mistral-large-latest` |
228
+ | Cohere | `cohere` | `command-r-plus` |
229
+ | Groq | `groq` | `llama-3.3-70b-versatile` |
230
+ | xAI (Grok) | `xai` | `grok-2-latest` |
231
+ | Ollama (local) | `ollama` | `llama3.2` |
232
+
233
+ ### Embeddings
234
+
235
+ | Provider | `provider` key | Default model |
236
+ |----------------|----------------|---------------|
237
+ | OpenAI | `openai` | `text-embedding-3-small` |
238
+ | Google | `google` | `text-embedding-004` |
239
+ | Mistral | `mistral` | `mistral-embed` |
240
+ | Cohere | `cohere` | `embed-english-v3.0` |
241
+ | Voyage | `voyage` | `voyage-3` |
242
+ | Ollama (local) | `ollama` | `nomic-embed-text` |
243
+ | Local (offline)| `local` | `all-MiniLM-L6-v2` |
244
+
245
+ ---
246
+
247
+ ## Configuration Reference
248
+
249
+ ```python
250
+ Document("./policy.pdf", {
251
+ # Chunking
252
+ "chunkSize": 500, # words per chunk (default: 500)
253
+ "overlap": 50, # overlapping words between chunks (default: 50)
254
+
255
+ # Retrieval
256
+ "topK": 5, # default results returned (default: 5)
257
+ "scoreThreshold": 0.0, # minimum cosine similarity (0..1, default: 0)
258
+
259
+ # Storage
260
+ "storeDir": ".raglite", # where indexes are persisted (default: .raglite)
261
+
262
+ # Providers
263
+ "embeddings": {"provider": "local"},
264
+ "llm": {"provider": "openai", "model": "gpt-4o-mini", "apiKey": "sk-..."},
265
+
266
+ # Logging
267
+ "logLevel": "info", # "silent" | "info" | "debug" (default: info)
268
+ })
269
+ ```
270
+
271
+ ---
272
+
273
+ ## How Caching Works
274
+
275
+ Every `build()` call fingerprints the source file with a **SHA-256 content hash** and persists it alongside the vectors. The cached index is reused only if **all** of the following match the stored index:
276
+
277
+ | Factor | Triggers rebuild if changed |
278
+ |--------|-----------------------------|
279
+ | File content | SHA-256 hash differs |
280
+ | Chunk size | `chunkSize` changed |
281
+ | Overlap | `overlap` changed |
282
+ | Embedding provider/model | Provider or model string changed |
283
+ | Library version | Package version bumped |
284
+
285
+ Pass `rebuild=True` to `build()` to force a fresh index regardless.
286
+
287
+ Each document is stored under `.raglite/<sha256-prefix>/`, so multiple documents in the same project never overwrite each other.
288
+
289
+ ---
290
+
291
+ ## Advanced Usage
292
+
293
+ ### Custom Vector Store
294
+
295
+ ```python
296
+ from raglite.vectordb.base import VectorStore
297
+
298
+ class MyVectorStore(VectorStore):
299
+ # Implement: load, reset, add, search, count,
300
+ # save_index_metadata, read_index_metadata
301
+ ...
302
+ ```
303
+
304
+ ### Custom Loader
305
+
306
+ ```python
307
+ from raglite.loaders.base import BaseLoader
308
+ from raglite.loaders import get_loader
309
+
310
+ class CsvLoader(BaseLoader):
311
+ def load(self) -> str:
312
+ # read CSV, return string
313
+ ...
314
+ ```
315
+
316
+ ### Custom Chunker
317
+
318
+ ```python
319
+ from raglite.chunking.base import BaseChunker
320
+
321
+ class SentenceChunker(BaseChunker):
322
+ def split(self, text: str) -> list[str]:
323
+ ...
324
+ ```
325
+
326
+ ### Direct Embedder Access
327
+
328
+ ```python
329
+ from raglite import create_embedder
330
+
331
+ embedder = create_embedder({"provider": "openai", "apiKey": "sk-..."})
332
+ vectors = embedder.embed_documents(["chunk one", "chunk two"])
333
+ query_vec = embedder.embed_query("refund policy")
334
+ ```
335
+
336
+ ---
337
+
338
+ ## Development
339
+
340
+ ```bash
341
+ # Clone and set up
342
+ git clone <repo>
343
+ cd raglite-py
344
+
345
+ # Create virtual environment
346
+ python3.12 -m venv .venv
347
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
348
+
349
+ # Install in editable mode with dev dependencies
350
+ pip install -e ".[dev]"
351
+
352
+ # Run tests (76 tests, ~5s, no network required)
353
+ pytest
354
+
355
+ # Run with coverage
356
+ pytest --cov=raglite --cov-report=term-missing
357
+
358
+ # Run examples (uses local offline embeddings)
359
+ python examples/basic.py
360
+ python examples/multi_provider.py # requires API keys in env
361
+ python examples/serve.py
362
+ ```
363
+
364
+ ### Test Structure
365
+
366
+ ```
367
+ tests/
368
+ ├── unit/
369
+ │ ├── test_chunking.py # RecursiveChunker algorithm
370
+ │ ├── test_vectordb.py # MemoryVectorStore (cosine, persistence, isolation)
371
+ │ ├── test_loaders.py # TxtLoader, MarkdownLoader, JsonLoader
372
+ │ ├── test_prompt.py # system/user prompt builders
373
+ │ ├── test_errors.py # exception hierarchy
374
+ │ ├── test_config.py # config defaults and overrides
375
+ │ ├── test_hash.py # SHA-256 file hashing + namespace generation
376
+ │ ├── test_retriever.py # Retriever with mocked embedder
377
+ │ └── test_cli.py # CLI commands and argument parsing
378
+ └── integration/
379
+ ├── test_document.py # build/cache/search lifecycle (mocked embeddings)
380
+ ├── test_ask.py # ask/stream with mocked LLM generation
381
+ └── test_api.py # FastAPI endpoints via TestClient
382
+ ```
383
+
384
+ ---
385
+
386
+ ## License
387
+
388
+ MIT