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.
- slim_llm_memory-0.1.0/CHANGELOG.md +11 -0
- slim_llm_memory-0.1.0/LICENSE +21 -0
- slim_llm_memory-0.1.0/MANIFEST.in +5 -0
- slim_llm_memory-0.1.0/PKG-INFO +348 -0
- slim_llm_memory-0.1.0/README.md +300 -0
- slim_llm_memory-0.1.0/docs/IMPLEMENTATION.md +455 -0
- slim_llm_memory-0.1.0/examples/01_minimal.py +77 -0
- slim_llm_memory-0.1.0/examples/02_topic_context.py +103 -0
- slim_llm_memory-0.1.0/examples/03_routing_bench.py +124 -0
- slim_llm_memory-0.1.0/examples/04_rerank_bench.py +180 -0
- slim_llm_memory-0.1.0/pyproject.toml +67 -0
- slim_llm_memory-0.1.0/setup.cfg +4 -0
- slim_llm_memory-0.1.0/slim_llm_memory/__init__.py +26 -0
- slim_llm_memory-0.1.0/slim_llm_memory/apps/__init__.py +0 -0
- slim_llm_memory-0.1.0/slim_llm_memory/apps/obsidian/__init__.py +10 -0
- slim_llm_memory-0.1.0/slim_llm_memory/apps/obsidian/brain.py +289 -0
- slim_llm_memory-0.1.0/slim_llm_memory/apps/obsidian/chunker.py +73 -0
- slim_llm_memory-0.1.0/slim_llm_memory/apps/obsidian/config.py +114 -0
- slim_llm_memory-0.1.0/slim_llm_memory/apps/obsidian/parser.py +165 -0
- slim_llm_memory-0.1.0/slim_llm_memory/apps/obsidian/spool.py +101 -0
- slim_llm_memory-0.1.0/slim_llm_memory/chunking.py +84 -0
- slim_llm_memory-0.1.0/slim_llm_memory/embed.py +184 -0
- slim_llm_memory-0.1.0/slim_llm_memory/enrich.py +58 -0
- slim_llm_memory-0.1.0/slim_llm_memory/evals.py +85 -0
- slim_llm_memory-0.1.0/slim_llm_memory/graph.py +140 -0
- slim_llm_memory-0.1.0/slim_llm_memory/index.py +345 -0
- slim_llm_memory-0.1.0/slim_llm_memory/keyword.py +130 -0
- slim_llm_memory-0.1.0/slim_llm_memory/libraries.py +465 -0
- slim_llm_memory-0.1.0/slim_llm_memory/llm.py +113 -0
- slim_llm_memory-0.1.0/slim_llm_memory/obs.py +88 -0
- slim_llm_memory-0.1.0/slim_llm_memory/py.typed +0 -0
- slim_llm_memory-0.1.0/slim_llm_memory/rerank.py +103 -0
- slim_llm_memory-0.1.0/slim_llm_memory/sessions.py +110 -0
- slim_llm_memory-0.1.0/slim_llm_memory/store.py +388 -0
- slim_llm_memory-0.1.0/slim_llm_memory/topics.py +681 -0
- slim_llm_memory-0.1.0/slim_llm_memory.egg-info/PKG-INFO +348 -0
- slim_llm_memory-0.1.0/slim_llm_memory.egg-info/SOURCES.txt +65 -0
- slim_llm_memory-0.1.0/slim_llm_memory.egg-info/dependency_links.txt +1 -0
- slim_llm_memory-0.1.0/slim_llm_memory.egg-info/requires.txt +32 -0
- slim_llm_memory-0.1.0/slim_llm_memory.egg-info/top_level.txt +1 -0
- slim_llm_memory-0.1.0/tests/obsidian/__init__.py +0 -0
- slim_llm_memory-0.1.0/tests/obsidian/conftest.py +27 -0
- slim_llm_memory-0.1.0/tests/obsidian/fixtures/vault/.obsidian/workspace.json +1 -0
- slim_llm_memory-0.1.0/tests/obsidian/fixtures/vault/Daily/2026-05-20.md +2 -0
- slim_llm_memory-0.1.0/tests/obsidian/fixtures/vault/People/Alice.md +7 -0
- slim_llm_memory-0.1.0/tests/obsidian/fixtures/vault/Projects/Long note.md +19 -0
- slim_llm_memory-0.1.0/tests/obsidian/fixtures/vault/Projects/Quirks.md +14 -0
- slim_llm_memory-0.1.0/tests/obsidian/fixtures/vault/inbox/01J-capture.md +6 -0
- slim_llm_memory-0.1.0/tests/obsidian/fixtures/vault/notitle.md +1 -0
- slim_llm_memory-0.1.0/tests/obsidian/test_brain.py +227 -0
- slim_llm_memory-0.1.0/tests/obsidian/test_chunker.py +64 -0
- slim_llm_memory-0.1.0/tests/obsidian/test_config.py +47 -0
- slim_llm_memory-0.1.0/tests/obsidian/test_parser.py +118 -0
- slim_llm_memory-0.1.0/tests/obsidian/test_spool.py +48 -0
- slim_llm_memory-0.1.0/tests/test_chunking.py +45 -0
- slim_llm_memory-0.1.0/tests/test_embedder.py +168 -0
- slim_llm_memory-0.1.0/tests/test_enrich_sessions.py +113 -0
- slim_llm_memory-0.1.0/tests/test_evals.py +51 -0
- slim_llm_memory-0.1.0/tests/test_graph.py +86 -0
- slim_llm_memory-0.1.0/tests/test_index.py +193 -0
- slim_llm_memory-0.1.0/tests/test_keyword.py +75 -0
- slim_llm_memory-0.1.0/tests/test_library.py +205 -0
- slim_llm_memory-0.1.0/tests/test_llm_rerank.py +121 -0
- slim_llm_memory-0.1.0/tests/test_packaging.py +46 -0
- slim_llm_memory-0.1.0/tests/test_rerank_auto.py +153 -0
- slim_llm_memory-0.1.0/tests/test_store.py +237 -0
- 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,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.
|