slim-llm-memory 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 (67) hide show
  1. slim_llm_memory-0.1.0/CHANGELOG.md +11 -0
  2. slim_llm_memory-0.1.0/LICENSE +21 -0
  3. slim_llm_memory-0.1.0/MANIFEST.in +5 -0
  4. slim_llm_memory-0.1.0/PKG-INFO +348 -0
  5. slim_llm_memory-0.1.0/README.md +300 -0
  6. slim_llm_memory-0.1.0/docs/IMPLEMENTATION.md +455 -0
  7. slim_llm_memory-0.1.0/examples/01_minimal.py +77 -0
  8. slim_llm_memory-0.1.0/examples/02_topic_context.py +103 -0
  9. slim_llm_memory-0.1.0/examples/03_routing_bench.py +124 -0
  10. slim_llm_memory-0.1.0/examples/04_rerank_bench.py +180 -0
  11. slim_llm_memory-0.1.0/pyproject.toml +67 -0
  12. slim_llm_memory-0.1.0/setup.cfg +4 -0
  13. slim_llm_memory-0.1.0/slim_llm_memory/__init__.py +26 -0
  14. slim_llm_memory-0.1.0/slim_llm_memory/apps/__init__.py +0 -0
  15. slim_llm_memory-0.1.0/slim_llm_memory/apps/obsidian/__init__.py +10 -0
  16. slim_llm_memory-0.1.0/slim_llm_memory/apps/obsidian/brain.py +289 -0
  17. slim_llm_memory-0.1.0/slim_llm_memory/apps/obsidian/chunker.py +73 -0
  18. slim_llm_memory-0.1.0/slim_llm_memory/apps/obsidian/config.py +114 -0
  19. slim_llm_memory-0.1.0/slim_llm_memory/apps/obsidian/parser.py +165 -0
  20. slim_llm_memory-0.1.0/slim_llm_memory/apps/obsidian/spool.py +101 -0
  21. slim_llm_memory-0.1.0/slim_llm_memory/chunking.py +84 -0
  22. slim_llm_memory-0.1.0/slim_llm_memory/embed.py +184 -0
  23. slim_llm_memory-0.1.0/slim_llm_memory/enrich.py +58 -0
  24. slim_llm_memory-0.1.0/slim_llm_memory/evals.py +85 -0
  25. slim_llm_memory-0.1.0/slim_llm_memory/graph.py +140 -0
  26. slim_llm_memory-0.1.0/slim_llm_memory/index.py +345 -0
  27. slim_llm_memory-0.1.0/slim_llm_memory/keyword.py +130 -0
  28. slim_llm_memory-0.1.0/slim_llm_memory/libraries.py +465 -0
  29. slim_llm_memory-0.1.0/slim_llm_memory/llm.py +113 -0
  30. slim_llm_memory-0.1.0/slim_llm_memory/obs.py +88 -0
  31. slim_llm_memory-0.1.0/slim_llm_memory/py.typed +0 -0
  32. slim_llm_memory-0.1.0/slim_llm_memory/rerank.py +103 -0
  33. slim_llm_memory-0.1.0/slim_llm_memory/sessions.py +110 -0
  34. slim_llm_memory-0.1.0/slim_llm_memory/store.py +388 -0
  35. slim_llm_memory-0.1.0/slim_llm_memory/topics.py +681 -0
  36. slim_llm_memory-0.1.0/slim_llm_memory.egg-info/PKG-INFO +348 -0
  37. slim_llm_memory-0.1.0/slim_llm_memory.egg-info/SOURCES.txt +65 -0
  38. slim_llm_memory-0.1.0/slim_llm_memory.egg-info/dependency_links.txt +1 -0
  39. slim_llm_memory-0.1.0/slim_llm_memory.egg-info/requires.txt +32 -0
  40. slim_llm_memory-0.1.0/slim_llm_memory.egg-info/top_level.txt +1 -0
  41. slim_llm_memory-0.1.0/tests/obsidian/__init__.py +0 -0
  42. slim_llm_memory-0.1.0/tests/obsidian/conftest.py +27 -0
  43. slim_llm_memory-0.1.0/tests/obsidian/fixtures/vault/.obsidian/workspace.json +1 -0
  44. slim_llm_memory-0.1.0/tests/obsidian/fixtures/vault/Daily/2026-05-20.md +2 -0
  45. slim_llm_memory-0.1.0/tests/obsidian/fixtures/vault/People/Alice.md +7 -0
  46. slim_llm_memory-0.1.0/tests/obsidian/fixtures/vault/Projects/Long note.md +19 -0
  47. slim_llm_memory-0.1.0/tests/obsidian/fixtures/vault/Projects/Quirks.md +14 -0
  48. slim_llm_memory-0.1.0/tests/obsidian/fixtures/vault/inbox/01J-capture.md +6 -0
  49. slim_llm_memory-0.1.0/tests/obsidian/fixtures/vault/notitle.md +1 -0
  50. slim_llm_memory-0.1.0/tests/obsidian/test_brain.py +227 -0
  51. slim_llm_memory-0.1.0/tests/obsidian/test_chunker.py +64 -0
  52. slim_llm_memory-0.1.0/tests/obsidian/test_config.py +47 -0
  53. slim_llm_memory-0.1.0/tests/obsidian/test_parser.py +118 -0
  54. slim_llm_memory-0.1.0/tests/obsidian/test_spool.py +48 -0
  55. slim_llm_memory-0.1.0/tests/test_chunking.py +45 -0
  56. slim_llm_memory-0.1.0/tests/test_embedder.py +168 -0
  57. slim_llm_memory-0.1.0/tests/test_enrich_sessions.py +113 -0
  58. slim_llm_memory-0.1.0/tests/test_evals.py +51 -0
  59. slim_llm_memory-0.1.0/tests/test_graph.py +86 -0
  60. slim_llm_memory-0.1.0/tests/test_index.py +193 -0
  61. slim_llm_memory-0.1.0/tests/test_keyword.py +75 -0
  62. slim_llm_memory-0.1.0/tests/test_library.py +205 -0
  63. slim_llm_memory-0.1.0/tests/test_llm_rerank.py +121 -0
  64. slim_llm_memory-0.1.0/tests/test_packaging.py +46 -0
  65. slim_llm_memory-0.1.0/tests/test_rerank_auto.py +153 -0
  66. slim_llm_memory-0.1.0/tests/test_store.py +237 -0
  67. slim_llm_memory-0.1.0/tests/test_topic.py +221 -0
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — unreleased
4
+
5
+ First packaged release.
6
+
7
+ - `Memory`: persistent vector index — hash-skip upsert, cosine search, atomic flush.
8
+ - `topic()` / `library()`: per-topic stores and a database of topics with routing.
9
+ - Hybrid retrieval (BM25 + embeddings), adaptive cross-encoder reranking (`rerank="auto"`).
10
+ - Grounded answers with validated `[n]` citations; `evaluate()` for MRR/recall.
11
+ - Extras: `graph`, `rerank`, `obsidian`. `gemini` and `anthropic` are declared but not implemented yet.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 trbck
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,5 @@
1
+ # sdist = everything needed to run the test suite from the tarball.
2
+ include LICENSE README.md CHANGELOG.md docs/IMPLEMENTATION.md
3
+ graft tests
4
+ graft examples
5
+ global-exclude __pycache__ *.py[cod]
@@ -0,0 +1,348 @@
1
+ Metadata-Version: 2.4
2
+ Name: slim-llm-memory
3
+ Version: 0.1.0
4
+ Summary: Slim, fast, persistent memory + retrieval for LLM apps. numpy + httpx, ~1000 LOC, drops into anything.
5
+ Author: trbck
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/trbck/slim-llm-memory
8
+ Project-URL: Documentation, https://github.com/trbck/slim-llm-memory/blob/main/docs/IMPLEMENTATION.md
9
+ Project-URL: Changelog, https://github.com/trbck/slim-llm-memory/blob/main/CHANGELOG.md
10
+ Project-URL: Issues, https://github.com/trbck/slim-llm-memory/issues
11
+ Keywords: llm,embeddings,vector,memory,rag,ollama
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: numpy>=1.24
25
+ Requires-Dist: httpx>=0.25
26
+ Provides-Extra: gemini
27
+ Requires-Dist: google-genai>=1.0; extra == "gemini"
28
+ Provides-Extra: graph
29
+ Requires-Dist: networkx>=3.0; extra == "graph"
30
+ Provides-Extra: rerank
31
+ Requires-Dist: sentence-transformers>=3.0; extra == "rerank"
32
+ Provides-Extra: anthropic
33
+ Requires-Dist: anthropic>=0.40; extra == "anthropic"
34
+ Provides-Extra: obsidian
35
+ Requires-Dist: watchdog>=4; extra == "obsidian"
36
+ Requires-Dist: pyyaml>=6; extra == "obsidian"
37
+ Requires-Dist: mcp>=2; extra == "obsidian"
38
+ Provides-Extra: test
39
+ Requires-Dist: pytest>=7; extra == "test"
40
+ Requires-Dist: pytest-asyncio>=0.23; extra == "test"
41
+ Requires-Dist: tomli>=2; python_version < "3.11" and extra == "test"
42
+ Provides-Extra: dev
43
+ Requires-Dist: slim-llm-memory[anthropic,gemini,graph,obsidian,rerank,test]; extra == "dev"
44
+ Requires-Dist: ruff>=0.6; extra == "dev"
45
+ Requires-Dist: build>=1; extra == "dev"
46
+ Requires-Dist: twine>=5; extra == "dev"
47
+ Dynamic: license-file
48
+
49
+ # slim-llm-memory
50
+
51
+ Slim, fast, persistent **memory + retrieval for LLM apps**. Pure Python
52
+ where possible; numpy where it actually helps. Ollama for local
53
+ embeddings; cloud LLMs only for hard reasoning. ~1000 LOC, two hard
54
+ deps (numpy + httpx), drops into anything.
55
+
56
+ > **Status:** phase 1 — `Memory` core. See [`docs/IMPLEMENTATION.md`](docs/IMPLEMENTATION.md)
57
+ > for the full plan and what comes next (Gemini fallback, Tier router,
58
+ > Graph layer, ANN swap).
59
+
60
+ ## Why
61
+
62
+ Vector DBs and full RAG frameworks are overkill for personal projects
63
+ and research code. At < 50k items, a single numpy array, a jsonl file,
64
+ and a content-hash for incremental updates is all you actually need.
65
+ This library is exactly that — but written carefully enough that you
66
+ can build serious things on it without hitting sharp corners.
67
+
68
+ When you outgrow it, the public API is **swap-compatible** with a real
69
+ vector store (faiss / SQLite-vss / Qdrant). The migration is local to
70
+ one file.
71
+
72
+ ## Install
73
+
74
+ ```bash
75
+ pip install slim-llm-memory # numpy + httpx only
76
+ pip install slim-llm-memory[graph] # + NetworkX graph layer
77
+ pip install slim-llm-memory[rerank] # + sentence-transformers cross-encoder
78
+ ```
79
+
80
+ Working on the library itself:
81
+
82
+ ```bash
83
+ pip install -e . # then `import slim_llm_memory` works anywhere
84
+ ```
85
+
86
+ Install it even for local hacking. Running from the repo root with `PYTHONPATH=.`
87
+ works, but it hides packaging bugs — a broken console-script entry survived exactly
88
+ that way until the package was first installed for real.
89
+
90
+ The `[gemini]` and `[anthropic]` extras are declared but **not yet implemented**:
91
+ no module imports them. `Embedder` currently offers `noop` and `ollama`, and the
92
+ answer path talks only to Ollama.
93
+
94
+ ## 30-second tour
95
+
96
+ ```python
97
+ from slim_llm_memory import Memory, Embedder
98
+
99
+ # Local Ollama for embeddings, persistent index in ./mymemory/
100
+ mem = Memory("./mymemory", Embedder.ollama("nomic-embed-text"))
101
+
102
+ # Add or update items — only changed texts are re-embedded
103
+ mem.upsert([
104
+ {"id": "doc1", "text": "how to set up nginx", "meta": {"kind": "note"}},
105
+ {"id": "doc2", "text": "milch kaufen", "meta": {"kind": "shopping"}},
106
+ ])
107
+
108
+ # Top-k semantic search — optional filters
109
+ hits = mem.search("nginx tutorial", k=5, kinds={"note"}, min_score=0.55)
110
+ for h in hits:
111
+ print(h.id, h.score, h.text)
112
+
113
+ # Find duplicates by cosine similarity
114
+ clusters = mem.find_duplicates(threshold=0.86)
115
+
116
+ # Atomic persistence — safe to crash mid-anything
117
+ mem.flush()
118
+ ```
119
+
120
+ `Embedder.noop()` exists for tests and offline development — same
121
+ interface, deterministic SHA-256 derived vectors, no network.
122
+
123
+ ## What's in the box (phase 1)
124
+
125
+ | Module | Purpose |
126
+ |---------------|--------------------------------------------------------------|
127
+ | `index.py` | `Memory`, `Hit` — public API |
128
+ | `store.py` | Versioned manifest + atomic flush + fcntl lock + tombstones |
129
+ | `embed.py` | `Embedder.noop` (tests) + `Embedder.ollama` (local) |
130
+ | `obs.py` | Per-instance ring buffers + counters for `Memory.stats()` |
131
+
132
+ Public API surface (the only thing callers see):
133
+
134
+ ```
135
+ Memory(path, embedder)
136
+ .upsert(items) → {added, updated, skipped, embed_calls}
137
+ .search(query, k, kinds, min_score) → [Hit, ...]
138
+ .neighbours(id, k, kinds) → [Hit, ...] (no embed call)
139
+ .search_vector(vec, k, kinds, min_score) → [Hit, ...] (pre-embedded query)
140
+ .find_duplicates(threshold) → [[id, ...], ...]
141
+ .update_text(id, text) → bool
142
+ .remove(id) → bool
143
+ .stats() → dict (JSON-safe)
144
+ .flush(force=False) → bool
145
+ .close(flush=True)
146
+ context manager: `with Memory(...) as mem: ...`
147
+
148
+ Embedder.noop(dim=384)
149
+ Embedder.ollama(model="nomic-embed-text", base_url="http://localhost:11434", timeout=60)
150
+ ```
151
+
152
+ ## Persistence model
153
+
154
+ Files in your index directory:
155
+
156
+ ```
157
+ items.vN.jsonl one record per item: {id, text, hash, meta, ts, deleted?}
158
+ vectors.vN.npy float32 ndarray, shape (N, dim) — row-aligned with items
159
+ manifest.json atomic commit point; loading always honours its version pointer
160
+ .lock advisory exclusive lock (one writer per directory)
161
+ ```
162
+
163
+ A crash mid-flush leaves the **previous manifest version intact** — the
164
+ old files load cleanly. Garbage versioned files left behind by
165
+ crashes are ignored on next load.
166
+
167
+ ## Performance
168
+
169
+ At p95 on a CPU with prenormalised float32 vectors:
170
+
171
+ | Items | Pure-Python cosine | numpy linear scan (this lib) | faiss HNSW (phase 7) |
172
+ |--------|--------------------|------------------------------|----------------------|
173
+ | 1k | 5–20 ms | <1 ms | <1 ms |
174
+ | 10k | 50–200 ms | 5 ms | <1 ms |
175
+ | 50k | 0.5–2 s | 30 ms | 1–10 ms |
176
+ | 100k+ | dead | 100–500 ms | 1–10 ms |
177
+
178
+ Phase 1 ships the numpy linear scan. When you outgrow it, swap the
179
+ storage backend behind the same `Memory.search()` signature.
180
+
181
+ ## Topic store: fast context for an LLM working on one topic
182
+
183
+ `topic()` is the "one numpy store per topic" shape with a `requests`-style
184
+ front door: open a store, put text in, get context out.
185
+
186
+ ```python
187
+ from slim_llm_memory import topic
188
+
189
+ t = topic("nginx") # ~/.slim-llm-memory/topics/nginx, Ollama nomic-embed-text
190
+ t.add("docs/") # file, directory, raw text, or {name: text}; saved on return
191
+ r = t.ask("how do I enable TLS?") # one embed call + one numpy scan
192
+ r # hits with scores, embed ms, scan ms
193
+ r.context # numbered block to prepend to an LLM prompt
194
+ t.answer("how do I enable TLS?") # + a local Ollama chat model, grounded on r.context
195
+ ```
196
+
197
+ `t.add` is incremental (unchanged chunks are never re-embedded), `t.forget(name)`
198
+ drops a doc, `embedder="noop"` runs offline for tests.
199
+
200
+ Several topics make a database. `library()` is a folder of topic stores;
201
+ `ask` embeds once and scans every topic, archiving is a folder move:
202
+
203
+ ```python
204
+ from slim_llm_memory import library
205
+
206
+ db = library() # ~/.slim-llm-memory/topics
207
+ db.topic("nginx").add("docs/nginx/")
208
+ db.topic("cooking").add({"pasta.md": "..."})
209
+ db # table of topics
210
+ db.ask("how do I enable TLS?") # hits labelled by topic, merged by score
211
+ db.ask("...", topics=["nginx"])
212
+ db.route("how do I enable TLS?") # stage 1 alone: topics ranked by centroid similarity
213
+ db.ask("...", route=True) # two-stage: route, then scan only the chosen topics
214
+ db.archive("cooking"); db.restore("cooking"); db.delete("cooking")
215
+ ```
216
+
217
+ `ask` is exact (one concatenated scan) until the library holds more than
218
+ 50k chunks, then it routes through topic centroids automatically; topics
219
+ within 0.05 of the best centroid are kept, and a prompt that matches no
220
+ topic falls back to the exact scan. `examples/03_routing_bench.py` has the
221
+ numbers: at 500 topics × 200 chunks, routing cuts the scan from ~40 ms to ~2 ms.
222
+
223
+ ### Accuracy: hybrid retrieval, reranking, evaluation
224
+
225
+ ```python
226
+ t.ask(q) # hybrid (default): dense cosine ∪ BM25, fused by normalised score
227
+ t.ask(q, mode="dense") / t.ask(q, mode="keyword")
228
+ t.ask(q, rerank=True) # cross-encoder over the top 4·k (pip install slim-llm-memory[rerank])
229
+ t.ask(q, rerank="auto") # ...but only when the top of the ranking is actually contested
230
+ t.ask(q, rerank=rr, rerank_margin=0.15) # same policy with your own reranker; r.rerank_skipped says what happened
231
+ t.answer(q, rewrite=True, refuse_below=0.4, stream=False) # query rewrite, refusal, validated [n] citations
232
+
233
+ from slim_llm_memory import evaluate
234
+ evaluate(t, [("which file is the commit point?", "manifest"), ...], k=5) # hit@1, hit@k, MRR
235
+ ```
236
+
237
+ On the eight doc questions in `notebooks/accuracy_demo.ipynb` (four of them
238
+ with the product name in the question, which drags the intro chunks up),
239
+ measured over this repo's own docs:
240
+
241
+ | retrieval | hit@1 | hit@5 | MRR |
242
+ |---|---|---|---|
243
+ | dense | 0.38 | 0.62 | 0.47 |
244
+ | hybrid (default) | 0.38 | 0.88 | 0.56 |
245
+ | hybrid + cross-encoder rerank | 0.62 | 1.00 | 0.76 |
246
+
247
+ Chunks are heading-aware with a 20-word overlap (`topic(..., chunk_words=120,
248
+ overlap=20)`); a tuning grid over chunk size, overlap and the fusion weight is
249
+ in the notebook and confirms the defaults. Re-tune per corpus with `evaluate()`.
250
+
251
+ Reranking is the most accurate and by far the slowest step, so `rerank="auto"`
252
+ pays for it only when the top of the ranking is contested: it compares the
253
+ leader's lead over the runner-up against the pool's spread, and skips the model
254
+ when that relative gap is at least `rerank_margin` (default 0.15).
255
+ `examples/04_rerank_bench.py` measures the trade on a 14-document corpus and 10
256
+ questions — with the real embedder and `bge-reranker-v2-m3` on this CPU box:
257
+
258
+ | policy | MRR | hit@1 | reranker calls | ms/query |
259
+ |---|---|---|---|---|
260
+ | off | 1.00 | 1.00 | 0 | 447 |
261
+ | auto | 1.00 | 1.00 | 0 of 10 | 749 |
262
+ | always | 1.00 | 1.00 | 10 | 3622 |
263
+
264
+ Same answers, 4.8× faster than reranking everything. On a harder corpus (the
265
+ `--offline` run, where dense retrieval alone gets one question wrong) auto
266
+ reranks 3 of 10 questions and recovers the full MRR that `always` reaches.
267
+ `r.rerank_skipped` reports the decision per query.
268
+
269
+ ### Structure: graph, entities, sessions
270
+
271
+ ```python
272
+ t.link("nginx.md", "certbot.md", relation="uses") # typed edges, graph.json next to the vectors
273
+ t.related("nginx.md") # 0.6·cosine + 0.4·graph; [[wikilinks]] become edges on add
274
+ t.add(text, enrich=True) # local LLM extracts entities + relations (slow, opt-in)
275
+ t.entities(); t.ask(q, entity="Postgres") # filter by extracted entity
276
+
277
+ s = db.session("2026-09-04") # conversation memory as a topic store
278
+ s.turn("user", "..."); s.recall("what did we decide?"); s.history(5); s.summary(model=...)
279
+ ```
280
+
281
+ `notebooks/library_demo.ipynb` walks through it. `notebooks/use_cases_demo.ipynb`
282
+ measures four real use cases (grounded answers, paraphrase, languages, agent
283
+ session memory) and ends with an honest table of what is missing compared to a
284
+ full RAG stack, an ontology, and a vector database.
285
+
286
+ `notebooks/topic_context_demo.ipynb` (executed, 14 cells) and
287
+ `examples/02_topic_context.py` are the proof: this repo's docs as the
288
+ topic, live prompts with the latency split into embed vs scan, an
289
+ incremental update, an optional grounded LLM answer, and a synthetic scale
290
+ run. Measured on an 8-core CPU box (Ollama CPU-only):
291
+
292
+ | Step | Cost | Where the time goes |
293
+ |------|------|---------------------|
294
+ | Prompt → context (33 chunks) | 1.2–1.5 s | Ollama embed of the prompt: >99.9 %. Scan: 0.2–0.5 ms |
295
+ | Re-index after one edit | 1 embed call | 32 chunks hash-skipped, 1 re-embedded |
296
+ | Scan, 1k × 768 | 0.3 ms p50 / 1.3 ms p95 | numpy GEMV + argpartition |
297
+ | Scan, 10k × 768 | 2.4 ms p50 / 7.3 ms p95 | |
298
+ | Scan, 50k × 768 | 12 ms p50 / 22 ms p95 | |
299
+
300
+ The retrieval itself is never the bottleneck at this scale; the embedder is.
301
+ On a GPU or with a cloud embedder the prompt-to-context time drops to tens
302
+ of milliseconds and the scan numbers above are what remains.
303
+
304
+ ```bash
305
+ PYTHONPATH=. python examples/02_topic_context.py --fresh # cold build + queries + scale
306
+ PYTHONPATH=. python examples/02_topic_context.py --llm llama3.2:3b # + grounded answer
307
+ ```
308
+
309
+ ## Tests + examples
310
+
311
+ ```bash
312
+ pytest # 169 tests, no network (Embedder.noop)
313
+ python examples/01_minimal.py # after `pip install -e .`
314
+ ```
315
+
316
+ ### Notebooks
317
+
318
+ Start with the four `hello` notebooks — each is about ten lines and answers one
319
+ question. They need Ollama running with `nomic-embed-text` pulled.
320
+
321
+ | Notebook | Shows |
322
+ |---|---|
323
+ | `notebooks/00_hello_topic.ipynb` | the three verbs: `topic()` / `.add()` / `.ask()` |
324
+ | `notebooks/01_hello_library.ipynb` | many topics behind one handle, and `route()` |
325
+ | `notebooks/02_hello_memory.ipynb` | the low-level `Memory` API this tour uses |
326
+ | `notebooks/03_hello_answer.ipynb` | a grounded answer with citations, and refusal |
327
+
328
+ The longer notebooks (`topic_context_demo`, `library_demo`, `accuracy_demo`,
329
+ `use_cases_demo`) go deeper on measurement.
330
+
331
+ ## Migration paths
332
+
333
+ When the slim stack stops being enough, swap one file:
334
+
335
+ | Symptom | Replace |
336
+ |--------------------------------------|--------------------------------------------|
337
+ | Search p95 > 100 ms at your scale | `index.py` → faiss-cpu HNSW (same API) |
338
+ | Need a 2nd writer process | `store.py` → SQLite + sqlite-vss extension |
339
+ | > 1M items | both → Qdrant / Weaviate as a service |
340
+ | Need real multi-hop graph queries | future `graph.py` → Kùzu (embedded) |
341
+ | Local LLM too slow / quality too low | future `tier.py` → drop L2; route L0 → L3 |
342
+
343
+ The whole point is: you don't outgrow it gradually. When you do, the
344
+ symptoms are obvious and the migration is local.
345
+
346
+ ## License
347
+
348
+ MIT.