sqlite-graph-memory 0.3.1__tar.gz → 0.3.3__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.
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/CHANGELOG.md +60 -1
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/PKG-INFO +72 -15
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/README.md +71 -14
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/bench/README.md +1 -1
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/eval/build_gold.py +6 -3
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/eval/run_eval.py +31 -7
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/examples/README.md +16 -7
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/pyproject.toml +2 -1
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/src/sqlite_graph_memory/__init__.py +1 -1
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/src/sqlite_graph_memory/brain_ask.py +121 -5
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/src/sqlite_graph_memory/index_notes.py +6 -6
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/src/sqlite_graph_memory/mcp_server.py +38 -10
- sqlite_graph_memory-0.3.3/src/sqlite_graph_memory/ppr.py +234 -0
- sqlite_graph_memory-0.3.3/src/sqlite_graph_memory/vault_stats.py +217 -0
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/tests/test_brain_ask.py +26 -0
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/tests/test_eval.py +19 -0
- sqlite_graph_memory-0.3.3/tests/test_feedback_link.py +29 -0
- sqlite_graph_memory-0.3.3/tests/test_index_notes_dotfiles.py +81 -0
- sqlite_graph_memory-0.3.3/tests/test_llms_txt.py +39 -0
- sqlite_graph_memory-0.3.3/tests/test_mcp_script_mode.py +59 -0
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/tests/test_mcp_server.py +113 -1
- sqlite_graph_memory-0.3.3/tests/test_ppr.py +203 -0
- sqlite_graph_memory-0.3.3/tests/test_star_prompt.py +118 -0
- sqlite_graph_memory-0.3.3/tests/test_turnstate_hook.py +126 -0
- sqlite_graph_memory-0.3.3/tests/test_vault_stats.py +229 -0
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/.gitignore +0 -0
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/CITATION.cff +0 -0
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/LICENSE +0 -0
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/bench/requirements.txt +0 -0
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/docs/RELEASING.md +0 -0
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/docs/bitemporal.md +0 -0
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/examples/claude-code-stop-hook.json +0 -0
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/examples/mcp-config.json +0 -0
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/requirements.txt +0 -0
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/src/sqlite_graph_memory/schema.sql +0 -0
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/src/sqlite_graph_memory/turnstate_hook.py +0 -0
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/src/sqlite_graph_memory/turnstate_show.py +0 -0
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/tests/test_bench.py +0 -0
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/tests/test_index_notes.py +0 -0
- {sqlite_graph_memory-0.3.1 → sqlite_graph_memory-0.3.3}/tests/test_optional_extra.py +0 -0
|
@@ -4,7 +4,66 @@ What shipped, in plain words. Entries below v0.1.3 were written on 2026-09-05 fr
|
|
|
4
4
|
release notes that already existed — the file itself did not exist until then, so those three are
|
|
5
5
|
recorded after the fact rather than backdated to look contemporaneous.
|
|
6
6
|
|
|
7
|
-
## v0.3.
|
|
7
|
+
## v0.3.3 — 2026-10-26
|
|
8
|
+
|
|
9
|
+
**Hidden note files are skipped like hidden folders.** A note whose filename starts with a
|
|
10
|
+
dot, such as `.ideas.md` or `.draft.md`, used to be indexed when it sat in the vault root or a
|
|
11
|
+
visible folder, while the same file inside `.drafts/` was skipped. Now both are skipped, so
|
|
12
|
+
after re-indexing those notes stop showing up in answers. `BRAIN_INDEX_HIDDEN=1` brings back
|
|
13
|
+
the old behaviour and indexes hidden files and folders alike. `eval/build_gold.py` now walks the
|
|
14
|
+
vault with the indexer's own filter, so it follows the same rule and the same variable instead
|
|
15
|
+
of keeping its own copy (issue #25).
|
|
16
|
+
|
|
17
|
+
**The MCP server works from a source checkout and finds your index where you launch it**
|
|
18
|
+
(issue #24, reported by @Kaap10). `sgm-mcp` used to run the recall child with its working
|
|
19
|
+
directory set to the package folder, so a relative `BRAIN_INDEX_DIR=index` resolved inside
|
|
20
|
+
site-packages and your own index was never found; from a plain clone without `pip install -e`,
|
|
21
|
+
`python src/sqlite_graph_memory/mcp_server.py --test` failed with ModuleNotFoundError. Recall now
|
|
22
|
+
runs in the server's own working directory, and CI reproduces the user's launch: installed wheel,
|
|
23
|
+
relative index, started from another directory.
|
|
24
|
+
|
|
25
|
+
**A three-question feedback form.** "What were you trying to do, what broke, which machine": an
|
|
26
|
+
issue template, a README section, and a link printed next to the missing-index error, because
|
|
27
|
+
this project collects no telemetry and that form is how we learn what is missing.
|
|
28
|
+
|
|
29
|
+
## v0.3.2 — 2026-10-24
|
|
30
|
+
|
|
31
|
+
**The PageRank lane the README credits is now in the package** (#51). `ppr.py` runs
|
|
32
|
+
personalized PageRank over the `[[wikilink]]` graph, seeded by the dense hits, and fuses it with
|
|
33
|
+
the reranker by RRF while the reranker's top 5 stay where they are. Off by default (`SGM_PPR=1`
|
|
34
|
+
or `sgm-ask --ppr`); `eval/run_eval.py --ppr` scores it as its own mode. Any failure returns the
|
|
35
|
+
reranker's order. `tests/test_ppr.py` builds a hub note that pushes the reranker's rank-1 hit to
|
|
36
|
+
position 11 when the guard is removed, and goes red if it is.
|
|
37
|
+
|
|
38
|
+
**`sgm-stats <notes_dir>`: four health numbers for the link graph.** Orphan rate, links per
|
|
39
|
+
page, share of pages in the main connected component, and share of concept pages whose
|
|
40
|
+
`updated` date is older than 90 days, each with an ok/warn/fail verdict and a `--threshold-*`
|
|
41
|
+
flag. Exit code 0/1/2 for cron. Reuses the indexer's note filter and the recall wikilink parser,
|
|
42
|
+
so it measures the graph recall walks. On `bench/vault`: 26.2% orphans (fail), 4.65 links per
|
|
43
|
+
page, 100% in one component. `tests/test_vault_stats.py` crosses every threshold on a synthetic
|
|
44
|
+
vault and goes red when inbound links stop being counted.
|
|
45
|
+
|
|
46
|
+
**A spec you can paste.** `BITEMPORAL.md` turns the close-don't-delete rule from
|
|
47
|
+
`docs/bitemporal.md` into a block for any agent's `CLAUDE.md` or `AGENTS.md`: three fields
|
|
48
|
+
(`valid_from`, `valid_to`, `superseded_by`), five rules, and a one-line attribution as the
|
|
49
|
+
whole licence.
|
|
50
|
+
|
|
51
|
+
**`llms.txt` for agents that read before they install.** One page: what this is, the three
|
|
52
|
+
commands, MCP config for JSON clients, Claude Code and Codex, the bench numbers as they stand
|
|
53
|
+
on the leaderboard, the eval commands, every environment variable.
|
|
54
|
+
|
|
55
|
+
**The README says what it is for in its first line**: persistent memory for Claude Code,
|
|
56
|
+
Cursor and Codex over your own markdown notes.
|
|
57
|
+
|
|
58
|
+
**One star request, earned and once.** After a successful `sgm-ask --ab` in a terminal, one
|
|
59
|
+
stderr line asks for a GitHub star. It is printed once per machine (a marker file in
|
|
60
|
+
`~/.config/sqlite-graph-memory`, or `SGM_CONFIG_DIR`, created exclusively so two first runs
|
|
61
|
+
at once still print one line), never under `CI`, never when stdout or stderr is piped, never
|
|
62
|
+
when `BRAIN_AB_JSON` is set, and never with `SGM_NO_STAR_PROMPT=1`. If the line cannot be
|
|
63
|
+
written, the marker is removed so the next run asks again. `tests/test_star_prompt.py` covers
|
|
64
|
+
each guard and the call from `main()`, and goes red when any of them is removed.
|
|
65
|
+
|
|
66
|
+
## v0.3.1 — 2026-10-19
|
|
8
67
|
|
|
9
68
|
**Credit where it is due.** The MCP server that ships as `sgm-mcp` was written by Vardhman Gupta
|
|
10
69
|
([@Kaap10](https://github.com/Kaap10)) in PR #8: the server, its tests and its docs. His
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sqlite-graph-memory
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Associative memory for LLM agents: Graph RAG over a folder of markdown notes, in one SQLite file, with no server to run.
|
|
5
5
|
Project-URL: Homepage, https://github.com/tonydzi/sqlite-graph-memory
|
|
6
6
|
Project-URL: Repository, https://github.com/tonydzi/sqlite-graph-memory
|
|
@@ -49,10 +49,16 @@ Requires-Dist: sentence-transformers>=2.6; extra == 'embeddings'
|
|
|
49
49
|
Requires-Dist: transformers<4.50; (sys_platform == 'darwin' and platform_machine == 'x86_64') and extra == 'embeddings'
|
|
50
50
|
Description-Content-Type: text/markdown
|
|
51
51
|
|
|
52
|
-
# sqlite-graph-memory
|
|
52
|
+
# sqlite-graph-memory: persistent memory for Claude Code, Cursor and Codex over your own markdown notes
|
|
53
53
|
|
|
54
54
|
<!-- mcp-name: io.github.tonydzi/sqlite-graph-memory -->
|
|
55
55
|
|
|
56
|
+
[](https://pypi.org/project/sqlite-graph-memory/) [](https://github.com/tonydzi/sqlite-graph-memory/actions/workflows/tests.yml) [](https://registry.modelcontextprotocol.io/v0/servers?search=sqlite-graph-memory) [](LICENSE)
|
|
57
|
+
|
|
58
|
+
Long-term memory for coding agents that lives in the notes you already keep: an MCP server
|
|
59
|
+
and a CLI over any folder of markdown, Obsidian vaults included. `pip install`, point it at
|
|
60
|
+
the folder, and Claude Code, Cursor, Codex or any other MCP client can recall from it.
|
|
61
|
+
|
|
56
62
|
**Your agent forgets everything between sessions, and plain vector search gives it back
|
|
57
63
|
flat, unrelated chunks. This gives it associative recall over your own markdown notes:
|
|
58
64
|
vector search finds the entry points, the `[[wikilinks]]` you already wrote are the graph.
|
|
@@ -95,6 +101,11 @@ The answer lives in `tool-lanternfish` ("Lanternfish is written in Elixir"). The
|
|
|
95
101
|
never names Lanternfish, so the vector pass does not find it in the top 12 at all.
|
|
96
102
|
`project-ouzel` links to it, so one graph hop puts it second.
|
|
97
103
|
|
|
104
|
+
If that hop found something your vector search missed, a
|
|
105
|
+
[star](https://github.com/tonydzi/sqlite-graph-memory) is how other people with a notes
|
|
106
|
+
folder find this repo. `sgm-ask --ab` asks the same once per machine, in a terminal only;
|
|
107
|
+
`SGM_NO_STAR_PROMPT=1` turns it off.
|
|
108
|
+
|
|
98
109
|
That is the idea in one query, and it is not a blanket win. On this same vault vector-only
|
|
99
110
|
ranks ahead of graph mode overall; the numbers are in [`bench/`](bench/README.md). `--ab`
|
|
100
111
|
exists so you can measure which one wins on your own notes.
|
|
@@ -102,7 +113,10 @@ exists so you can measure which one wins on your own notes.
|
|
|
102
113
|
**From any MCP client** (Claude Desktop, Cursor, Zed, Windsurf): `sgm-mcp` after the install
|
|
103
114
|
above, or `uvx --with 'sqlite-graph-memory[embeddings]' sqlite-graph-memory`. Set
|
|
104
115
|
`BRAIN_INDEX_DIR` to the `index` folder `sgm-index` wrote. Config:
|
|
105
|
-
[`examples/mcp-config.json`](examples/mcp-config.json).
|
|
116
|
+
[`examples/mcp-config.json`](examples/mcp-config.json). Claude Code:
|
|
117
|
+
`claude mcp add sqlite-graph-memory -e BRAIN_INDEX_DIR=/path/to/index -- sgm-mcp`.
|
|
118
|
+
Codex: a `[mcp_servers.sqlite-graph-memory]` table in `~/.codex/config.toml` with
|
|
119
|
+
`command = "sgm-mcp"`. Agents that read files first: [`llms.txt`](llms.txt).
|
|
106
120
|
|
|
107
121
|
## What it is
|
|
108
122
|
|
|
@@ -112,7 +126,7 @@ points, the `[[wikilinks]]` you already wrote by hand are the graph, and a cross
|
|
|
112
126
|
reranks what the hop dragged in.
|
|
113
127
|
SQLite is the only database ([`schema.sql`](src/sqlite_graph_memory/schema.sql)).
|
|
114
128
|
|
|
115
|
-
Status: **pilot**, and the word is load-bearing: it runs daily in one real setup (a ~
|
|
129
|
+
Status: **pilot**, and the word is load-bearing: it runs daily in one real setup (an index of ~28k curated notes, ~93k chunks, rebuilt nightly from a ~270k-file
|
|
116
130
|
Obsidian vault driven by Claude Code), but it is deliberately minimal and makes no attempt
|
|
117
131
|
to be general — see [What's intentionally missing](#whats-intentionally-missing), which is
|
|
118
132
|
where the uneven test coverage is described.
|
|
@@ -189,12 +203,13 @@ an edge table only when hop depth or corpus size demands it.
|
|
|
189
203
|
|
|
190
204
|
- No entity lane. The production setup has an extra retrieval lane over people/project
|
|
191
205
|
cards; it is too entangled with personal data to publish.
|
|
192
|
-
- No incremental indexing
|
|
206
|
+
- No incremental indexing (tracked in the issues). Packaging exists since v0.3.0 (`pip install sqlite-graph-memory`). There IS an eval suite now — see
|
|
193
207
|
[Measure before you swap](#measure-before-you-swap--eval) — but it scores retrieval on
|
|
194
208
|
wikilink-derived gold, which is weak supervision and says so out loud. Test coverage is uneven:
|
|
195
|
-
`index_notes.py`, `brain_ask.py` and `mcp_server.py` have unit tests,
|
|
196
|
-
has
|
|
197
|
-
prose here — that is how this file ended up claiming
|
|
209
|
+
`index_notes.py`, `brain_ask.py` and `mcp_server.py` have unit tests, and
|
|
210
|
+
`turnstate_hook.py` has write-path coverage. Run `pytest -q` for the current count rather
|
|
211
|
+
than trusting a number written in prose here — that is how this file ended up claiming
|
|
212
|
+
"no tests" while seven were passing.
|
|
198
213
|
The status line above says **pilot** on purpose.
|
|
199
214
|
|
|
200
215
|
## Quickstart
|
|
@@ -237,7 +252,7 @@ present, and CPU works fine for small corpora.
|
|
|
237
252
|
|
|
238
253
|
### What `--ab` looks like
|
|
239
254
|
|
|
240
|
-
Output of `brain_ask.py` with `--ab`, on a toy 4-note corpus (real runs use
|
|
255
|
+
Output of `brain_ask.py` with `--ab`, on a toy 4-note corpus (real runs use an index of ~28k notes / ~93k chunks):
|
|
241
256
|
|
|
242
257
|
```
|
|
243
258
|
A/B RECALL: how should agent memory work
|
|
@@ -281,6 +296,26 @@ board with one adapter file and a pull request: [bench/CONTRIBUTING.md](bench/CO
|
|
|
281
296
|
CI recomputes every submitted number from the per-question outputs, so a typed-in number
|
|
282
297
|
cannot get onto the board.
|
|
283
298
|
|
|
299
|
+
## Health check — `sgm-stats`
|
|
300
|
+
|
|
301
|
+
Four numbers about the link graph itself, no model and no index needed. It reads the notes with
|
|
302
|
+
the same filter and the same `[[wikilink]]` parser as recall, so it measures the graph `--graph`
|
|
303
|
+
actually walks. Thresholds follow [second-brain-os metrics](https://github.com/undefined-ui/second-brain-os/blob/main/docs/05-graphs/metrics.md);
|
|
304
|
+
each is a `--threshold-*` flag. Exit code 0 ok, 1 warn, 2 fail, so it can sit in cron.
|
|
305
|
+
|
|
306
|
+
```text
|
|
307
|
+
$ sgm-stats bench/vault --today 2026-10-20
|
|
308
|
+
202 pages in bench/vault
|
|
309
|
+
|
|
310
|
+
FAIL orphan_rate 26.2% 53 of 202 pages have no inbound link
|
|
311
|
+
OK avg_degree 4.65 940 resolved links over 202 pages
|
|
312
|
+
OK main_component 100.0% 202 of 202 pages; 1 component
|
|
313
|
+
WARN stale_concepts 100.0% 12 of 12 concept pages older than 90 days (as of 2026-10-20)
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
The synthetic bench vault fails the orphan check for a plain reason: its 40 meeting and 12 decision notes link out and nothing
|
|
317
|
+
links back to them. `--json` gives the same report with the first orphan and stale page names.
|
|
318
|
+
|
|
284
319
|
## Measure before you swap — `eval/`
|
|
285
320
|
|
|
286
321
|
Every "obvious" upgrade to a RAG pipeline — a newer embedder, a stronger reranker, hybrid
|
|
@@ -342,6 +377,17 @@ of guessing — 97% of the correct `bridge` answers were *already in the candida
|
|
|
342
377
|
cross-encoder was dropping them, because the relation is structural, not textual. Widening
|
|
343
378
|
retrieval, the obvious fix, would have done nothing at all.
|
|
344
379
|
|
|
380
|
+
That lane ships in the package as `src/sqlite_graph_memory/ppr.py`, **off by default**: turn it
|
|
381
|
+
on with `SGM_PPR=1` or `sgm-ask --ppr`, and score it with `python eval/run_eval.py --gold ...
|
|
382
|
+
--ppr`, which adds a third mode next to `vector` and `graph`. The numbers above were measured on
|
|
383
|
+
the graph pool, so the matching command is `sgm-ask --graph --ppr`; `--ppr` alone reorders the
|
|
384
|
+
vector pool, and `--ab` leaves the lane out so its A/B log stays vector against graph. Any failure in it falls back to
|
|
385
|
+
the reranker's order, like the graph hop. Be clear about what that reproduces: the bridge
|
|
386
|
+
numbers above came from the maintainer's home vault and home pipeline on 2026-09-16, and they
|
|
387
|
+
have not been re-run from the published package. The code is the same algorithm with the same
|
|
388
|
+
constants (alpha 0.85, 10 PPR files, top 5 protected), but your vault will give your numbers,
|
|
389
|
+
and `bench/` does not score this lane yet.
|
|
390
|
+
|
|
345
391
|
Seven other changes were rejected by the same ruler, under one rule fixed in advance:
|
|
346
392
|
**a regression worse than 0.01 nDCG@12 on any class reverts the change.**
|
|
347
393
|
|
|
@@ -351,10 +397,10 @@ Seven other changes were rejected by the same ruler, under one rule fixed in adv
|
|
|
351
397
|
| Qwen3-Embedding-0.6B | body Recall 0.767 -> 0.683; reindex 2863 s vs 333 s -> **rejected** |
|
|
352
398
|
| bge-reranker-v2-m3 | −0.011…−0.018 nDCG on every class except English (+0.041) -> **rejected** |
|
|
353
399
|
| Qwen3-Reranker-0.6B | title −0.111 nDCG at 12x the cost -> **rejected** |
|
|
354
|
-
| BM25 + RRF hybrid | temporal −0.035 -> **merged switched off** |
|
|
400
|
+
| BM25 + RRF hybrid | temporal −0.035 -> **merged switched off** (in the home pipeline, not in this package) |
|
|
355
401
|
| heading-path prefix in every chunk | body −0.023, temporal −0.035 -> **reverted** |
|
|
356
402
|
| `sqlite-vec` (vec0) | full scan 11.99 s vs 0.14 s for a plain SQLite table, and its ANN missed 1.2 hits of 60 -> vectors live in SQLite, **the extension does not** |
|
|
357
|
-
| bi-temporal demotion of superseded notes | temporal +0.130 nDCG on a 41-question temporal set, but title −0.012 -> **merged behind a flag, default off** |
|
|
403
|
+
| bi-temporal demotion of superseded notes | temporal +0.130 nDCG on a 41-question temporal set, but title −0.012 -> **merged behind a flag, default off** (in the home pipeline, not in this package) |
|
|
358
404
|
|
|
359
405
|
None of those verdicts is a claim about the models. They are claims about *this corpus with
|
|
360
406
|
this ruler*, which is the only kind of claim a swap decision actually needs — and the reason
|
|
@@ -376,7 +422,7 @@ while the metric is broken never tested the metric.
|
|
|
376
422
|
|
|
377
423
|
## Roadmap
|
|
378
424
|
|
|
379
|
-
**Now — [
|
|
425
|
+
**Now — [latest release](https://github.com/tonydzi/sqlite-graph-memory/releases/latest)** (v0.3.1 on PyPI and in the MCP Registry as of 2026-10-19).
|
|
380
426
|
The pilot as it runs daily: retrieval pipeline, per-turn ledger and A/B telemetry, all in
|
|
381
427
|
`brain_ask.py` and `turnstate_hook.py`, plus the bi-temporal design note. Known defects live in
|
|
382
428
|
the [tracker](https://github.com/tonydzi/sqlite-graph-memory/issues) rather than in footnotes
|
|
@@ -414,9 +460,10 @@ Both are set as constants (`E5_MODEL`, `RERANK_MODEL`) in `brain_ask.py` and sma
|
|
|
414
460
|
to run on a laptop GPU; swap freely, since nothing in the pipeline depends on these
|
|
415
461
|
specific models.
|
|
416
462
|
|
|
417
|
-
##
|
|
463
|
+
## Maintainers
|
|
418
464
|
|
|
419
|
-
|
|
465
|
+
- **Anton Dziatkovskii** ([@tonydzi](https://github.com/tonydzi)): retrieval pipeline, ledger, bench, releases.
|
|
466
|
+
- **Vardhman Gupta** ([@Kaap10](https://github.com/Kaap10)): the MCP server (`sgm-mcp`, PRs #8, #10, #14) and its tests; reviews on `mcp_server.py` route to him via CODEOWNERS.
|
|
420
467
|
|
|
421
468
|
## AI contributors
|
|
422
469
|
|
|
@@ -428,7 +475,7 @@ content** — no decorative credits. Lab-wide policy, one source for every repo:
|
|
|
428
475
|
|
|
429
476
|
## Cite this work
|
|
430
477
|
|
|
431
|
-
The paper about this system — retrieval design, pilot telemetry, and the query-class-stratified evaluation protocol — is published: [DOI 10.5281/zenodo.22639718](https://doi.org/10.5281/zenodo.22639718) (preprint, CC BY 4.0). Machine-readable citation: [CITATION.cff](CITATION.cff).
|
|
478
|
+
The paper about this system — retrieval design, pilot telemetry, and the query-class-stratified evaluation protocol — is published: [DOI 10.5281/zenodo.22639718](https://doi.org/10.5281/zenodo.22639718) (preprint, CC BY 4.0). Machine-readable citation: [CITATION.cff](CITATION.cff) (GitHub's "Cite this repository" button). Academic identity: Anton Dzyatkovsky publishes as **Anton Dziatkovskii** ([ORCID 0000-0001-7408-3054](https://orcid.org/0000-0001-7408-3054)).
|
|
432
479
|
|
|
433
480
|
## License
|
|
434
481
|
|
|
@@ -444,6 +491,16 @@ Questions, war stories, or you want to run this on your own fleet:
|
|
|
444
491
|
- 🌐 [palo-alto.ai](https://palo-alto.ai) · [Palo Alto AI Research Lab](https://github.com/tonydzi)
|
|
445
492
|
- 🧪 **Engineers: want to test-drive this setup?** Message me — I hand out free starter seeds to engineers who test and report back.
|
|
446
493
|
|
|
494
|
+
## Tell us what is missing
|
|
495
|
+
|
|
496
|
+
You tried this on your own notes and something was missing or broke. That is the most useful
|
|
497
|
+
thing anyone can tell us.
|
|
498
|
+
|
|
499
|
+
- **You started and it did not work:** [open the 3-question form](https://github.com/tonydzi/sqlite-graph-memory/issues/new?template=feature_request.yml) (what you tried, what broke, which machine).
|
|
500
|
+
- **Not sure it is a bug, or just an idea:** [Discussions → Ideas](https://github.com/tonydzi/sqlite-graph-memory/discussions/categories/ideas).
|
|
501
|
+
|
|
502
|
+
Nothing in this package phones home. There is no telemetry, so we only know what you write to us.
|
|
503
|
+
|
|
447
504
|
## Contributors welcome — and there is a queue
|
|
448
505
|
|
|
449
506
|
Issues labelled [`accepted`](https://github.com/tonydzi/sqlite-graph-memory/issues?q=is%3Aissue+is%3Aopen+label%3Aaccepted)
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
# sqlite-graph-memory
|
|
1
|
+
# sqlite-graph-memory: persistent memory for Claude Code, Cursor and Codex over your own markdown notes
|
|
2
2
|
|
|
3
3
|
<!-- mcp-name: io.github.tonydzi/sqlite-graph-memory -->
|
|
4
4
|
|
|
5
|
+
[](https://pypi.org/project/sqlite-graph-memory/) [](https://github.com/tonydzi/sqlite-graph-memory/actions/workflows/tests.yml) [](https://registry.modelcontextprotocol.io/v0/servers?search=sqlite-graph-memory) [](LICENSE)
|
|
6
|
+
|
|
7
|
+
Long-term memory for coding agents that lives in the notes you already keep: an MCP server
|
|
8
|
+
and a CLI over any folder of markdown, Obsidian vaults included. `pip install`, point it at
|
|
9
|
+
the folder, and Claude Code, Cursor, Codex or any other MCP client can recall from it.
|
|
10
|
+
|
|
5
11
|
**Your agent forgets everything between sessions, and plain vector search gives it back
|
|
6
12
|
flat, unrelated chunks. This gives it associative recall over your own markdown notes:
|
|
7
13
|
vector search finds the entry points, the `[[wikilinks]]` you already wrote are the graph.
|
|
@@ -44,6 +50,11 @@ The answer lives in `tool-lanternfish` ("Lanternfish is written in Elixir"). The
|
|
|
44
50
|
never names Lanternfish, so the vector pass does not find it in the top 12 at all.
|
|
45
51
|
`project-ouzel` links to it, so one graph hop puts it second.
|
|
46
52
|
|
|
53
|
+
If that hop found something your vector search missed, a
|
|
54
|
+
[star](https://github.com/tonydzi/sqlite-graph-memory) is how other people with a notes
|
|
55
|
+
folder find this repo. `sgm-ask --ab` asks the same once per machine, in a terminal only;
|
|
56
|
+
`SGM_NO_STAR_PROMPT=1` turns it off.
|
|
57
|
+
|
|
47
58
|
That is the idea in one query, and it is not a blanket win. On this same vault vector-only
|
|
48
59
|
ranks ahead of graph mode overall; the numbers are in [`bench/`](bench/README.md). `--ab`
|
|
49
60
|
exists so you can measure which one wins on your own notes.
|
|
@@ -51,7 +62,10 @@ exists so you can measure which one wins on your own notes.
|
|
|
51
62
|
**From any MCP client** (Claude Desktop, Cursor, Zed, Windsurf): `sgm-mcp` after the install
|
|
52
63
|
above, or `uvx --with 'sqlite-graph-memory[embeddings]' sqlite-graph-memory`. Set
|
|
53
64
|
`BRAIN_INDEX_DIR` to the `index` folder `sgm-index` wrote. Config:
|
|
54
|
-
[`examples/mcp-config.json`](examples/mcp-config.json).
|
|
65
|
+
[`examples/mcp-config.json`](examples/mcp-config.json). Claude Code:
|
|
66
|
+
`claude mcp add sqlite-graph-memory -e BRAIN_INDEX_DIR=/path/to/index -- sgm-mcp`.
|
|
67
|
+
Codex: a `[mcp_servers.sqlite-graph-memory]` table in `~/.codex/config.toml` with
|
|
68
|
+
`command = "sgm-mcp"`. Agents that read files first: [`llms.txt`](llms.txt).
|
|
55
69
|
|
|
56
70
|
## What it is
|
|
57
71
|
|
|
@@ -61,7 +75,7 @@ points, the `[[wikilinks]]` you already wrote by hand are the graph, and a cross
|
|
|
61
75
|
reranks what the hop dragged in.
|
|
62
76
|
SQLite is the only database ([`schema.sql`](src/sqlite_graph_memory/schema.sql)).
|
|
63
77
|
|
|
64
|
-
Status: **pilot**, and the word is load-bearing: it runs daily in one real setup (a ~
|
|
78
|
+
Status: **pilot**, and the word is load-bearing: it runs daily in one real setup (an index of ~28k curated notes, ~93k chunks, rebuilt nightly from a ~270k-file
|
|
65
79
|
Obsidian vault driven by Claude Code), but it is deliberately minimal and makes no attempt
|
|
66
80
|
to be general — see [What's intentionally missing](#whats-intentionally-missing), which is
|
|
67
81
|
where the uneven test coverage is described.
|
|
@@ -138,12 +152,13 @@ an edge table only when hop depth or corpus size demands it.
|
|
|
138
152
|
|
|
139
153
|
- No entity lane. The production setup has an extra retrieval lane over people/project
|
|
140
154
|
cards; it is too entangled with personal data to publish.
|
|
141
|
-
- No incremental indexing
|
|
155
|
+
- No incremental indexing (tracked in the issues). Packaging exists since v0.3.0 (`pip install sqlite-graph-memory`). There IS an eval suite now — see
|
|
142
156
|
[Measure before you swap](#measure-before-you-swap--eval) — but it scores retrieval on
|
|
143
157
|
wikilink-derived gold, which is weak supervision and says so out loud. Test coverage is uneven:
|
|
144
|
-
`index_notes.py`, `brain_ask.py` and `mcp_server.py` have unit tests,
|
|
145
|
-
has
|
|
146
|
-
prose here — that is how this file ended up claiming
|
|
158
|
+
`index_notes.py`, `brain_ask.py` and `mcp_server.py` have unit tests, and
|
|
159
|
+
`turnstate_hook.py` has write-path coverage. Run `pytest -q` for the current count rather
|
|
160
|
+
than trusting a number written in prose here — that is how this file ended up claiming
|
|
161
|
+
"no tests" while seven were passing.
|
|
147
162
|
The status line above says **pilot** on purpose.
|
|
148
163
|
|
|
149
164
|
## Quickstart
|
|
@@ -186,7 +201,7 @@ present, and CPU works fine for small corpora.
|
|
|
186
201
|
|
|
187
202
|
### What `--ab` looks like
|
|
188
203
|
|
|
189
|
-
Output of `brain_ask.py` with `--ab`, on a toy 4-note corpus (real runs use
|
|
204
|
+
Output of `brain_ask.py` with `--ab`, on a toy 4-note corpus (real runs use an index of ~28k notes / ~93k chunks):
|
|
190
205
|
|
|
191
206
|
```
|
|
192
207
|
A/B RECALL: how should agent memory work
|
|
@@ -230,6 +245,26 @@ board with one adapter file and a pull request: [bench/CONTRIBUTING.md](bench/CO
|
|
|
230
245
|
CI recomputes every submitted number from the per-question outputs, so a typed-in number
|
|
231
246
|
cannot get onto the board.
|
|
232
247
|
|
|
248
|
+
## Health check — `sgm-stats`
|
|
249
|
+
|
|
250
|
+
Four numbers about the link graph itself, no model and no index needed. It reads the notes with
|
|
251
|
+
the same filter and the same `[[wikilink]]` parser as recall, so it measures the graph `--graph`
|
|
252
|
+
actually walks. Thresholds follow [second-brain-os metrics](https://github.com/undefined-ui/second-brain-os/blob/main/docs/05-graphs/metrics.md);
|
|
253
|
+
each is a `--threshold-*` flag. Exit code 0 ok, 1 warn, 2 fail, so it can sit in cron.
|
|
254
|
+
|
|
255
|
+
```text
|
|
256
|
+
$ sgm-stats bench/vault --today 2026-10-20
|
|
257
|
+
202 pages in bench/vault
|
|
258
|
+
|
|
259
|
+
FAIL orphan_rate 26.2% 53 of 202 pages have no inbound link
|
|
260
|
+
OK avg_degree 4.65 940 resolved links over 202 pages
|
|
261
|
+
OK main_component 100.0% 202 of 202 pages; 1 component
|
|
262
|
+
WARN stale_concepts 100.0% 12 of 12 concept pages older than 90 days (as of 2026-10-20)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
The synthetic bench vault fails the orphan check for a plain reason: its 40 meeting and 12 decision notes link out and nothing
|
|
266
|
+
links back to them. `--json` gives the same report with the first orphan and stale page names.
|
|
267
|
+
|
|
233
268
|
## Measure before you swap — `eval/`
|
|
234
269
|
|
|
235
270
|
Every "obvious" upgrade to a RAG pipeline — a newer embedder, a stronger reranker, hybrid
|
|
@@ -291,6 +326,17 @@ of guessing — 97% of the correct `bridge` answers were *already in the candida
|
|
|
291
326
|
cross-encoder was dropping them, because the relation is structural, not textual. Widening
|
|
292
327
|
retrieval, the obvious fix, would have done nothing at all.
|
|
293
328
|
|
|
329
|
+
That lane ships in the package as `src/sqlite_graph_memory/ppr.py`, **off by default**: turn it
|
|
330
|
+
on with `SGM_PPR=1` or `sgm-ask --ppr`, and score it with `python eval/run_eval.py --gold ...
|
|
331
|
+
--ppr`, which adds a third mode next to `vector` and `graph`. The numbers above were measured on
|
|
332
|
+
the graph pool, so the matching command is `sgm-ask --graph --ppr`; `--ppr` alone reorders the
|
|
333
|
+
vector pool, and `--ab` leaves the lane out so its A/B log stays vector against graph. Any failure in it falls back to
|
|
334
|
+
the reranker's order, like the graph hop. Be clear about what that reproduces: the bridge
|
|
335
|
+
numbers above came from the maintainer's home vault and home pipeline on 2026-09-16, and they
|
|
336
|
+
have not been re-run from the published package. The code is the same algorithm with the same
|
|
337
|
+
constants (alpha 0.85, 10 PPR files, top 5 protected), but your vault will give your numbers,
|
|
338
|
+
and `bench/` does not score this lane yet.
|
|
339
|
+
|
|
294
340
|
Seven other changes were rejected by the same ruler, under one rule fixed in advance:
|
|
295
341
|
**a regression worse than 0.01 nDCG@12 on any class reverts the change.**
|
|
296
342
|
|
|
@@ -300,10 +346,10 @@ Seven other changes were rejected by the same ruler, under one rule fixed in adv
|
|
|
300
346
|
| Qwen3-Embedding-0.6B | body Recall 0.767 -> 0.683; reindex 2863 s vs 333 s -> **rejected** |
|
|
301
347
|
| bge-reranker-v2-m3 | −0.011…−0.018 nDCG on every class except English (+0.041) -> **rejected** |
|
|
302
348
|
| Qwen3-Reranker-0.6B | title −0.111 nDCG at 12x the cost -> **rejected** |
|
|
303
|
-
| BM25 + RRF hybrid | temporal −0.035 -> **merged switched off** |
|
|
349
|
+
| BM25 + RRF hybrid | temporal −0.035 -> **merged switched off** (in the home pipeline, not in this package) |
|
|
304
350
|
| heading-path prefix in every chunk | body −0.023, temporal −0.035 -> **reverted** |
|
|
305
351
|
| `sqlite-vec` (vec0) | full scan 11.99 s vs 0.14 s for a plain SQLite table, and its ANN missed 1.2 hits of 60 -> vectors live in SQLite, **the extension does not** |
|
|
306
|
-
| bi-temporal demotion of superseded notes | temporal +0.130 nDCG on a 41-question temporal set, but title −0.012 -> **merged behind a flag, default off** |
|
|
352
|
+
| bi-temporal demotion of superseded notes | temporal +0.130 nDCG on a 41-question temporal set, but title −0.012 -> **merged behind a flag, default off** (in the home pipeline, not in this package) |
|
|
307
353
|
|
|
308
354
|
None of those verdicts is a claim about the models. They are claims about *this corpus with
|
|
309
355
|
this ruler*, which is the only kind of claim a swap decision actually needs — and the reason
|
|
@@ -325,7 +371,7 @@ while the metric is broken never tested the metric.
|
|
|
325
371
|
|
|
326
372
|
## Roadmap
|
|
327
373
|
|
|
328
|
-
**Now — [
|
|
374
|
+
**Now — [latest release](https://github.com/tonydzi/sqlite-graph-memory/releases/latest)** (v0.3.1 on PyPI and in the MCP Registry as of 2026-10-19).
|
|
329
375
|
The pilot as it runs daily: retrieval pipeline, per-turn ledger and A/B telemetry, all in
|
|
330
376
|
`brain_ask.py` and `turnstate_hook.py`, plus the bi-temporal design note. Known defects live in
|
|
331
377
|
the [tracker](https://github.com/tonydzi/sqlite-graph-memory/issues) rather than in footnotes
|
|
@@ -363,9 +409,10 @@ Both are set as constants (`E5_MODEL`, `RERANK_MODEL`) in `brain_ask.py` and sma
|
|
|
363
409
|
to run on a laptop GPU; swap freely, since nothing in the pipeline depends on these
|
|
364
410
|
specific models.
|
|
365
411
|
|
|
366
|
-
##
|
|
412
|
+
## Maintainers
|
|
367
413
|
|
|
368
|
-
|
|
414
|
+
- **Anton Dziatkovskii** ([@tonydzi](https://github.com/tonydzi)): retrieval pipeline, ledger, bench, releases.
|
|
415
|
+
- **Vardhman Gupta** ([@Kaap10](https://github.com/Kaap10)): the MCP server (`sgm-mcp`, PRs #8, #10, #14) and its tests; reviews on `mcp_server.py` route to him via CODEOWNERS.
|
|
369
416
|
|
|
370
417
|
## AI contributors
|
|
371
418
|
|
|
@@ -377,7 +424,7 @@ content** — no decorative credits. Lab-wide policy, one source for every repo:
|
|
|
377
424
|
|
|
378
425
|
## Cite this work
|
|
379
426
|
|
|
380
|
-
The paper about this system — retrieval design, pilot telemetry, and the query-class-stratified evaluation protocol — is published: [DOI 10.5281/zenodo.22639718](https://doi.org/10.5281/zenodo.22639718) (preprint, CC BY 4.0). Machine-readable citation: [CITATION.cff](CITATION.cff).
|
|
427
|
+
The paper about this system — retrieval design, pilot telemetry, and the query-class-stratified evaluation protocol — is published: [DOI 10.5281/zenodo.22639718](https://doi.org/10.5281/zenodo.22639718) (preprint, CC BY 4.0). Machine-readable citation: [CITATION.cff](CITATION.cff) (GitHub's "Cite this repository" button). Academic identity: Anton Dzyatkovsky publishes as **Anton Dziatkovskii** ([ORCID 0000-0001-7408-3054](https://orcid.org/0000-0001-7408-3054)).
|
|
381
428
|
|
|
382
429
|
## License
|
|
383
430
|
|
|
@@ -393,6 +440,16 @@ Questions, war stories, or you want to run this on your own fleet:
|
|
|
393
440
|
- 🌐 [palo-alto.ai](https://palo-alto.ai) · [Palo Alto AI Research Lab](https://github.com/tonydzi)
|
|
394
441
|
- 🧪 **Engineers: want to test-drive this setup?** Message me — I hand out free starter seeds to engineers who test and report back.
|
|
395
442
|
|
|
443
|
+
## Tell us what is missing
|
|
444
|
+
|
|
445
|
+
You tried this on your own notes and something was missing or broke. That is the most useful
|
|
446
|
+
thing anyone can tell us.
|
|
447
|
+
|
|
448
|
+
- **You started and it did not work:** [open the 3-question form](https://github.com/tonydzi/sqlite-graph-memory/issues/new?template=feature_request.yml) (what you tried, what broke, which machine).
|
|
449
|
+
- **Not sure it is a bug, or just an idea:** [Discussions → Ideas](https://github.com/tonydzi/sqlite-graph-memory/discussions/categories/ideas).
|
|
450
|
+
|
|
451
|
+
Nothing in this package phones home. There is no telemetry, so we only know what you write to us.
|
|
452
|
+
|
|
396
453
|
## Contributors welcome — and there is a queue
|
|
397
454
|
|
|
398
455
|
Issues labelled [`accepted`](https://github.com/tonydzi/sqlite-graph-memory/issues?q=is%3Aissue+is%3Aopen+label%3Aaccepted)
|
|
@@ -140,7 +140,7 @@ has seen.
|
|
|
140
140
|
|
|
141
141
|
## Limits, stated up front
|
|
142
142
|
|
|
143
|
-
- 202 notes is small. Dense retrieval at this size is easier than in
|
|
143
|
+
- 202 notes is small. Dense retrieval at this size is easier than in the ~28k-note production index,
|
|
144
144
|
so the bench measures failure modes, and it does not show how a system scales.
|
|
145
145
|
- The text is English only and comes from templates. The sentences are cleaner than real
|
|
146
146
|
notes, and that flatters extractive systems.
|
|
@@ -40,6 +40,9 @@ import sys
|
|
|
40
40
|
from collections import Counter
|
|
41
41
|
from pathlib import Path
|
|
42
42
|
|
|
43
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'src'))
|
|
44
|
+
from sqlite_graph_memory.index_notes import iter_notes # noqa: E402
|
|
45
|
+
|
|
43
46
|
WIKILINK_RX = re.compile(r'\[\[([^\]\|#]+)(?:[|#][^\]]*)?\]\]')
|
|
44
47
|
CYRILLIC_RX = re.compile(r'[а-яё]', re.I)
|
|
45
48
|
|
|
@@ -121,10 +124,10 @@ def read_notes(notes_dir, stems, folders=None):
|
|
|
121
124
|
"""Every indexed note, parsed into the fields the four classes need."""
|
|
122
125
|
root = Path(notes_dir)
|
|
123
126
|
paths = []
|
|
124
|
-
|
|
127
|
+
# The indexer's own walk: hidden files and dirs, conflict copies and BRAIN_INDEX_HIDDEN
|
|
128
|
+
# are decided in one place, so a gold answer is never a note the indexer would skip.
|
|
129
|
+
for p in iter_notes(root):
|
|
125
130
|
rel = p.relative_to(root)
|
|
126
|
-
if any(part.startswith('.') for part in rel.parts[:-1]): continue
|
|
127
|
-
if 'sync-conflict' in p.name: continue
|
|
128
131
|
if folders and (not rel.parts[:-1] or rel.parts[0] not in folders): continue
|
|
129
132
|
if p.stem.lower() not in stems: continue
|
|
130
133
|
paths.append(p)
|
|
@@ -9,6 +9,8 @@ Every question is scored twice, in both retrieval modes, on the same candidates
|
|
|
9
9
|
|
|
10
10
|
vector dense retrieve -> rerank (what you get without the graph)
|
|
11
11
|
graph dense -> 1-hop [[wikilinks]] -> rerank
|
|
12
|
+
ppr graph, then personalized PageRank fused with the reranker by RRF, top 5
|
|
13
|
+
protected (only with --ppr; see src/sqlite_graph_memory/ppr.py)
|
|
12
14
|
|
|
13
15
|
The difference between those two columns is the only honest answer to "does the graph
|
|
14
16
|
actually help?" — and on our own vault the answer differed by class (see README).
|
|
@@ -16,6 +18,7 @@ actually help?" — and on our own vault the answer differed by class (see READM
|
|
|
16
18
|
USAGE
|
|
17
19
|
python eval/run_eval.py --gold eval/gold-2026-09-16.jsonl
|
|
18
20
|
python eval/run_eval.py --gold ... --limit 20 # smoke run
|
|
21
|
+
python eval/run_eval.py --gold ... --ppr # add the PageRank lane as a 3rd mode
|
|
19
22
|
python eval/run_eval.py --selftest # metrics only, no model, no index
|
|
20
23
|
|
|
21
24
|
Config (env): BRAIN_INDEX_DIR (index to score), TURNSTATE_DB (where --sqlite appends a
|
|
@@ -119,8 +122,8 @@ def selftest():
|
|
|
119
122
|
|
|
120
123
|
|
|
121
124
|
# ------------------------------------------------------------------ runner
|
|
122
|
-
def score_gold(gold, progress=True):
|
|
123
|
-
"""Run every question through both modes. -> (rows, config dict)."""
|
|
125
|
+
def score_gold(gold, progress=True, ppr=False):
|
|
126
|
+
"""Run every question through both modes (three with ppr=True). -> (rows, config dict)."""
|
|
124
127
|
from sqlite_graph_memory import brain_ask as ba
|
|
125
128
|
|
|
126
129
|
dev = ba.pick_device()
|
|
@@ -135,15 +138,26 @@ def score_gold(gold, progress=True):
|
|
|
135
138
|
by_base = ba.index_by_basename(meta)
|
|
136
139
|
stem_of = lambda i: Path(meta[i]['path']).stem.lower()
|
|
137
140
|
|
|
138
|
-
rows, t0 = [], time.time()
|
|
141
|
+
rows, t0, ppr_ran = [], time.time(), False
|
|
139
142
|
for k, g in enumerate(gold):
|
|
140
143
|
tq = time.time()
|
|
141
144
|
sims = ba.query_sims(enc, emb, g['query'])
|
|
142
145
|
base = ba.retrieve_candidates(sims, meta)
|
|
143
146
|
added = ba.expand_1hop(sims, meta, base, by_base)
|
|
147
|
+
lists = {'vector': ba.rerank_candidates(ce, g['query'], base, meta)}
|
|
148
|
+
if ppr:
|
|
149
|
+
# One rerank of the graph pool serves both modes: `graph` is its top-12, exactly
|
|
150
|
+
# what rerank_candidates(topn=12) returns, and `ppr` fuses the FULL list.
|
|
151
|
+
full = ba.rerank_candidates(ce, g['query'], base + added, meta, topn=None)
|
|
152
|
+
lists['graph'] = full[:TOPN]
|
|
153
|
+
lane = ba.ppr_lane(meta, sims, base)
|
|
154
|
+
ppr_ran = ppr_ran or bool(lane)
|
|
155
|
+
lists['ppr'] = ba.fuse_ppr(full, meta, lane, TOPN)
|
|
156
|
+
else:
|
|
157
|
+
lists['graph'] = ba.rerank_candidates(ce, g['query'], base + added, meta)
|
|
144
158
|
res = {}
|
|
145
|
-
for mode,
|
|
146
|
-
ranked = [stem_of(i) for i, _ in
|
|
159
|
+
for mode, scored in lists.items():
|
|
160
|
+
ranked = [stem_of(i) for i, _ in scored]
|
|
147
161
|
res[mode] = {'ranked': ranked,
|
|
148
162
|
'recall': recall_at(ranked, g['gold']),
|
|
149
163
|
'mrr': mrr_at(ranked, g['gold']),
|
|
@@ -163,6 +177,11 @@ def score_gold(gold, progress=True):
|
|
|
163
177
|
sys.stderr.write('!! the graph added no neighbours to ANY question — link resolution or '
|
|
164
178
|
'the index is broken, not the graph. The graph rows below are the '
|
|
165
179
|
'vector rows.\n')
|
|
180
|
+
# Same silence, PPR lane: an empty lane falls back to the reranker order, so the ppr rows
|
|
181
|
+
# would equal the graph rows and read as "PPR adds nothing" when it never ran.
|
|
182
|
+
if ppr and rows and not ppr_ran:
|
|
183
|
+
sys.stderr.write('!! the PPR lane returned no files for ANY question (no edges, or it '
|
|
184
|
+
'failed). The ppr rows below are the graph rows.\n')
|
|
166
185
|
return rows, cfg
|
|
167
186
|
|
|
168
187
|
|
|
@@ -173,7 +192,7 @@ def summarize(rows):
|
|
|
173
192
|
groups += [('lang:' + lg, [r for r in rows if r['lang'] == lg]) for lg in ('ru', 'en')]
|
|
174
193
|
for name, rs in groups:
|
|
175
194
|
if not rs: continue
|
|
176
|
-
for mode in ('vector', 'graph'):
|
|
195
|
+
for mode in [m for m in ('vector', 'graph', 'ppr') if m in rs[0]['res']]:
|
|
177
196
|
out[(name, mode)] = {'n': len(rs),
|
|
178
197
|
'recall': sum(r['res'][mode]['recall'] for r in rs) / len(rs),
|
|
179
198
|
'mrr': sum(r['res'][mode]['mrr'] for r in rs) / len(rs),
|
|
@@ -197,6 +216,9 @@ def print_table(summary, cfg, gold_file):
|
|
|
197
216
|
if (name, 'graph') in summary:
|
|
198
217
|
d = summary[(name, 'graph')]['ndcg'] - summary[(name, 'vector')]['ndcg']
|
|
199
218
|
print('graph effect on %-9s nDCG@12 %+.3f' % (name, d))
|
|
219
|
+
if (name, 'ppr') in summary:
|
|
220
|
+
d = summary[(name, 'ppr')]['ndcg'] - summary[(name, 'graph')]['ndcg']
|
|
221
|
+
print('ppr effect on %-9s nDCG@12 %+.3f (vs graph)' % (name, d))
|
|
200
222
|
|
|
201
223
|
|
|
202
224
|
def write_sqlite(summary, cfg, gold_file, scores_file):
|
|
@@ -232,6 +254,8 @@ def main():
|
|
|
232
254
|
ap.add_argument('--out', help='scores .jsonl (default: eval/scores-<timestamp>.jsonl)')
|
|
233
255
|
ap.add_argument('--sqlite', action='store_true', help='also append to gold_eval in TURNSTATE_DB')
|
|
234
256
|
ap.add_argument('--no-persist', action='store_true', help='print the table, write nothing')
|
|
257
|
+
ap.add_argument('--ppr', action='store_true',
|
|
258
|
+
help='also score the PageRank lane (graph + PPR/RRF, reranker top 5 protected)')
|
|
235
259
|
ap.add_argument('--selftest', action='store_true', help='check the metrics and exit')
|
|
236
260
|
a = ap.parse_args()
|
|
237
261
|
|
|
@@ -256,7 +280,7 @@ def main():
|
|
|
256
280
|
if a.limit: gold = gold[:a.limit]
|
|
257
281
|
if not gold: sys.exit('gold file %s is empty' % gold_path)
|
|
258
282
|
|
|
259
|
-
rows, cfg = score_gold(gold)
|
|
283
|
+
rows, cfg = score_gold(gold, ppr=a.ppr)
|
|
260
284
|
summary = summarize(rows)
|
|
261
285
|
print_table(summary, cfg, a.gold)
|
|
262
286
|
if os.environ.get('EVAL_MUTANT') == '1':
|
|
@@ -47,19 +47,28 @@ Add the server to your MCP client config (e.g. `claude_desktop_config.json` or C
|
|
|
47
47
|
{
|
|
48
48
|
"mcpServers": {
|
|
49
49
|
"sqlite-graph-memory": {
|
|
50
|
-
"command": "
|
|
51
|
-
"args": [
|
|
52
|
-
"/absolute/path/to/sqlite-graph-memory/sgm-mcp"
|
|
53
|
-
],
|
|
50
|
+
"command": "sgm-mcp",
|
|
54
51
|
"env": {
|
|
55
|
-
"BRAIN_INDEX_DIR": "/
|
|
56
|
-
"TURNSTATE_DB": "/
|
|
52
|
+
"BRAIN_INDEX_DIR": "/path/to/your/index",
|
|
53
|
+
"TURNSTATE_DB": "/path/to/your/turnstate.db"
|
|
57
54
|
}
|
|
58
55
|
}
|
|
59
56
|
}
|
|
60
57
|
}
|
|
61
58
|
```
|
|
62
59
|
|
|
60
|
+
The same file is checked in as [`mcp-config.json`](mcp-config.json). `sgm-mcp` is on PATH after `pip install 'sqlite-graph-memory[embeddings]'`; if your client does not inherit that venv, use the absolute path to its `bin/sgm-mcp`.
|
|
61
|
+
|
|
62
|
+
Two copy-paste doors that need no JSON editing:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Claude Code
|
|
66
|
+
claude mcp add sqlite-graph-memory -e BRAIN_INDEX_DIR=/path/to/your/index -e TURNSTATE_DB=/path/to/your/turnstate.db -- sgm-mcp
|
|
67
|
+
|
|
68
|
+
# any client that speaks uvx (registry name io.github.tonydzi/sqlite-graph-memory)
|
|
69
|
+
uvx --with 'sqlite-graph-memory[embeddings]' sqlite-graph-memory
|
|
70
|
+
```
|
|
71
|
+
|
|
63
72
|
### Direct CLI Verification
|
|
64
73
|
|
|
65
74
|
You can verify the MCP server directly from the command line without launching an MCP client:
|
|
@@ -100,7 +109,7 @@ All components share standard configuration variables:
|
|
|
100
109
|
| `BRAIN_INDEX_DIR` | `./index` | Directory where `index_notes.py` writes and `brain_ask.py` reads `_brain_e5.npy` and metadata. |
|
|
101
110
|
| `TURNSTATE_DB` | `./turnstate.db` | SQLite database holding per-turn ledger (`turns`) and A/B telemetry (`ab_recall`). |
|
|
102
111
|
| `BRAIN_ANSWER_OUT` | `./_brain_answer.txt` | File where the formatted context bundle or answer is mirrored. |
|
|
103
|
-
| `BRAIN_INDEX_HIDDEN` | `0` | If `1`, indexes hidden dot-directories too (by default skipped). |
|
|
112
|
+
| `BRAIN_INDEX_HIDDEN` | `0` | If `1`, indexes hidden dot-files (`.draft.md`) and dot-directories too (by default both are skipped). |
|
|
104
113
|
| `BRAIN_AB_JSON` | `None` | If set, `--ab` mode writes a structured JSON diff to this file path. |
|
|
105
114
|
| `AB_SOURCE` | `work` | Telemetry label written to `ab_recall.source` (`work`, `eval`, etc.). |
|
|
106
115
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sqlite-graph-memory"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.3"
|
|
8
8
|
description = "Associative memory for LLM agents: Graph RAG over a folder of markdown notes, in one SQLite file, with no server to run."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -68,6 +68,7 @@ Issues = "https://github.com/tonydzi/sqlite-graph-memory/issues"
|
|
|
68
68
|
[project.scripts]
|
|
69
69
|
sgm-index = "sqlite_graph_memory.index_notes:main"
|
|
70
70
|
sgm-ask = "sqlite_graph_memory.brain_ask:main"
|
|
71
|
+
sgm-stats = "sqlite_graph_memory.vault_stats:main"
|
|
71
72
|
sgm-turnstate-hook = "sqlite_graph_memory.turnstate_hook:main"
|
|
72
73
|
sgm-turnstate-show = "sqlite_graph_memory.turnstate_show:main"
|
|
73
74
|
sgm-mcp = "sqlite_graph_memory.mcp_server:main"
|