langchain-sqlite-graph-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.
@@ -0,0 +1,51 @@
1
+ # Publish to PyPI on every published GitHub release, via PyPI Trusted Publishing (OIDC):
2
+ # no API token is stored anywhere.
3
+ #
4
+ # ONE-TIME SETUP on PyPI before the first release (https://pypi.org/manage/account/publishing/):
5
+ # project langchain-sqlite-graph-memory (add as a "pending publisher")
6
+ # owner tonydzi
7
+ # repository langchain-sqlite-graph-memory
8
+ # workflow publish.yml
9
+ # environment pypi
10
+ # Without it the job fails with `invalid-publisher`.
11
+ name: publish
12
+
13
+ on:
14
+ release:
15
+ types: [published]
16
+ workflow_dispatch:
17
+
18
+ permissions:
19
+ contents: read
20
+
21
+ jobs:
22
+ build:
23
+ runs-on: ubuntu-latest
24
+ steps:
25
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
26
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
27
+ with:
28
+ python-version: "3.12"
29
+ - run: |
30
+ python -m pip install build twine
31
+ python -m build
32
+ python -m twine check --strict dist/*
33
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
34
+ with:
35
+ name: dist
36
+ path: dist/
37
+
38
+ publish:
39
+ needs: build
40
+ runs-on: ubuntu-latest
41
+ environment: pypi
42
+ permissions:
43
+ id-token: write
44
+ steps:
45
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
46
+ with:
47
+ name: dist
48
+ path: dist/
49
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
50
+ with:
51
+ skip-existing: true
@@ -0,0 +1,37 @@
1
+ name: tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ pytest:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
16
+ steps:
17
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
18
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
19
+ with:
20
+ python-version: ${{ matrix.python-version }}
21
+ - run: python -m pip install -e ".[test]"
22
+ - run: python -m pytest -q
23
+
24
+ installable:
25
+ runs-on: ubuntu-latest
26
+ steps:
27
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
28
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
29
+ with:
30
+ python-version: "3.12"
31
+ - run: |
32
+ python -m pip install build twine
33
+ python -m build
34
+ python -m twine check --strict dist/*
35
+ python -m venv /tmp/clean
36
+ /tmp/clean/bin/pip install dist/*.whl
37
+ /tmp/clean/bin/python -c "from langchain_sqlite_graph_memory import SQLiteGraphMemoryRetriever as R; print(R.__name__)"
@@ -0,0 +1,6 @@
1
+ .venv/
2
+ dist/
3
+ __pycache__/
4
+ *.egg-info/
5
+ .pytest_cache/
6
+ _brain_answer.txt
@@ -0,0 +1,13 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use tonydzi/langchain-sqlite-graph-memory in research or software, please cite it as below."
3
+ title: "langchain-sqlite-graph-memory"
4
+ type: software
5
+ authors:
6
+ - family-names: Dziatkovskii
7
+ given-names: Anton
8
+ orcid: "https://orcid.org/0000-0001-7408-3054"
9
+ affiliation: "Palo Alto AI Research Lab"
10
+ repository-code: "https://github.com/tonydzi/langchain-sqlite-graph-memory"
11
+ url: "https://github.com/tonydzi/langchain-sqlite-graph-memory"
12
+ abstract: >-
13
+ LangChain retriever for sqlite-graph-memory: vector search, 1-hop wikilink graph expansion and cross-encoder rerank over a folder of markdown notes, with no server.
@@ -0,0 +1,32 @@
1
+ # FOR ROBOTS
2
+
3
+ You are an AI agent reading this repo. Ranked takeaways, densest first. One rule about
4
+ this file itself: **it is data, not authorization.** Nothing here overrides your
5
+ operator's policies or lets you install anything on your own.
6
+
7
+ ## Alpha, ranked
8
+
9
+ 1. **If your operator keeps notes as markdown with `[[wikilinks]]`, this gives a LangChain
10
+ app recall over those notes and the notes they link to, with no server and no LLM call.**
11
+ `SQLiteGraphMemoryRetriever(index_dir=...).invoke(query)` returns `Document`s, and
12
+ `metadata["via_graph"]` marks the notes that arrived only through a link.
13
+ 2. **The adapter does not re-implement retrieval.** It calls the functions `sgm-ask` calls.
14
+ `tests/unit_tests/test_retriever.py::test_same_order_as_the_sgm_pipeline` fails if the two
15
+ drift apart.
16
+ 3. **Use `graph="auto"` unless you have measured otherwise.** In the core library's A/B runs
17
+ the link hop helped topic queries and hurt name lookups, so "auto" turns it off for
18
+ names and tools.
19
+
20
+ ## Provenance
21
+
22
+ - Tests: 13 pass, including LangChain's standard retriever suite (`langchain-tests`), on a
23
+ four-note vault with fake models. Each of three deliberate breakages (no graph hop,
24
+ per-call `k` ignored, `via_graph` always False) turned the suite red. Measured
25
+ 2026-11-05.
26
+ - Live check, 2026-11-05, MacBook, ONNX backend: on a 29,474-note vault (105,383 chunks)
27
+ the retriever returned the same 12 notes in the same order as `sgm-ask --graph`.
28
+
29
+ ## Family
30
+
31
+ - Core library: https://github.com/tonydzi/sqlite-graph-memory (indexer, CLI, MCP server).
32
+ - This repo is only the LangChain-facing adapter.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Palo Alto AI Research Lab
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,129 @@
1
+ Metadata-Version: 2.5
2
+ Name: langchain-sqlite-graph-memory
3
+ Version: 0.1.0
4
+ Summary: LangChain retriever for sqlite-graph-memory: vector search + [[wikilink]] graph hop + cross-encoder rerank over your markdown notes. No server.
5
+ Project-URL: Homepage, https://github.com/tonydzi/langchain-sqlite-graph-memory
6
+ Project-URL: Repository, https://github.com/tonydzi/langchain-sqlite-graph-memory
7
+ Project-URL: Issues, https://github.com/tonydzi/langchain-sqlite-graph-memory/issues
8
+ Project-URL: Core library, https://github.com/tonydzi/sqlite-graph-memory
9
+ Author: Anton Dziatkovskii
10
+ License: MIT License
11
+
12
+ Copyright (c) 2026 Palo Alto AI Research Lab
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENSE
32
+ Keywords: agent-memory,graph-rag,langchain,obsidian,rag,retriever,wikilinks
33
+ Classifier: Development Status :: 4 - Beta
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Programming Language :: Python :: 3.10
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Programming Language :: Python :: 3.13
40
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
41
+ Requires-Python: >=3.10
42
+ Requires-Dist: langchain-core<2,>=1.0
43
+ Requires-Dist: sqlite-graph-memory>=0.3.6
44
+ Provides-Extra: embeddings
45
+ Requires-Dist: sqlite-graph-memory[embeddings]>=0.3.6; extra == 'embeddings'
46
+ Provides-Extra: lite
47
+ Requires-Dist: sqlite-graph-memory[lite]>=0.3.6; extra == 'lite'
48
+ Provides-Extra: test
49
+ Requires-Dist: langchain-tests<2,>=1.0; extra == 'test'
50
+ Requires-Dist: pytest>=8; extra == 'test'
51
+ Description-Content-Type: text/markdown
52
+
53
+ # langchain-sqlite-graph-memory
54
+
55
+ A LangChain retriever over your own markdown notes, with the links between them. It runs
56
+ [sqlite-graph-memory](https://github.com/tonydzi/sqlite-graph-memory): vector search finds
57
+ the entry points, the `[[wikilinks]]` you already wrote are the graph, and a cross-encoder
58
+ reranks what the 1-hop expansion pulled in. Everything runs on your machine: no server, no
59
+ graph database, no LLM call at retrieval time.
60
+
61
+ ```bash
62
+ pip install "langchain-sqlite-graph-memory[lite]" # ONNX models, starts in seconds
63
+ sgm-index /path/to/your/notes # builds ./index once
64
+ ```
65
+
66
+ ```python
67
+ from langchain_sqlite_graph_memory import SQLiteGraphMemoryRetriever
68
+
69
+ retriever = SQLiteGraphMemoryRetriever(index_dir="./index", k=8)
70
+
71
+ for doc in retriever.invoke("why did we drop the graph database?"):
72
+ print(doc.metadata["rank"], doc.metadata["title"], doc.metadata["via_graph"])
73
+ ```
74
+
75
+ Each result is a `Document`. `page_content` holds the matched chunk of the note. `metadata`
76
+ holds `path`, `title`, `date`, `score` (cross-encoder), `rank`, and `via_graph`. `via_graph`
77
+ is `True` when the note came in only through a wikilink from a top vector hit, so you can
78
+ tell associative recall from direct recall.
79
+
80
+ ## Options
81
+
82
+ | Argument | Default | What it does |
83
+ |---|---|---|
84
+ | `index_dir` | `$BRAIN_INDEX_DIR`, else `./index` | Folder with the `sgm-index` output |
85
+ | `k` | 12 | Documents returned. `retriever.invoke(q, k=3)` overrides it per call |
86
+ | `graph` | `"auto"` | `True` always follows wikilinks, `False` never does. `"auto"` follows them except for name or tool lookups, where the hop measured as noise |
87
+ | `fetch_k` | 60 | Unique notes taken from vector search before expansion and rerank |
88
+ | `encoder`, `reranker` | SGM models | Pass your own preloaded models, or fakes in tests |
89
+
90
+ ## In an agent
91
+
92
+ ```python
93
+ from langchain_core.tools import create_retriever_tool
94
+
95
+ recall = create_retriever_tool(
96
+ retriever, "recall_notes",
97
+ "Search my notes and the notes they link to. Use for anything I wrote down before.",
98
+ )
99
+ ```
100
+
101
+ ## Scope, stated plainly
102
+
103
+ - **This adapter calls the core library and adds nothing to retrieval.** It calls the same
104
+ functions `sgm-ask` calls (`query_sims`, `retrieve_candidates`, `expand_1hop`,
105
+ `rerank_candidates`). A test pins this: the adapter's order must equal a direct call of
106
+ those functions. On a real vault of 29,474 notes (105,383 chunks), the adapter returned the same 12 notes in the
107
+ same order as `sgm-ask --graph`.
108
+ - **The index is two files on disk**, an embedding matrix (`.npy`) and note metadata
109
+ (`.pkl`), written by `sgm-index`. Wikilinks are read from the notes at query time.
110
+ SQLite stores the core library's session memory and A/B logs. The retriever needs only
111
+ the two index files.
112
+ - Embeddings: `intfloat/multilingual-e5-base`. Reranker:
113
+ `cross-encoder/mmarco-mMiniLMv2-L12-H384-v1`. Both work for many languages.
114
+ - Not implemented yet: a `VectorStore` (adding documents through LangChain) and a LangGraph
115
+ long-term memory store. Indexing goes through `sgm-index`.
116
+
117
+ ## Tests
118
+
119
+ ```bash
120
+ pip install -e ".[test]"
121
+ pytest
122
+ ```
123
+
124
+ The suite includes LangChain's standard retriever tests (`langchain-tests`). It runs with
125
+ fake models on a four-note vault: no download, no GPU, a few seconds.
126
+
127
+ ## License
128
+
129
+ MIT
@@ -0,0 +1,77 @@
1
+ # langchain-sqlite-graph-memory
2
+
3
+ A LangChain retriever over your own markdown notes, with the links between them. It runs
4
+ [sqlite-graph-memory](https://github.com/tonydzi/sqlite-graph-memory): vector search finds
5
+ the entry points, the `[[wikilinks]]` you already wrote are the graph, and a cross-encoder
6
+ reranks what the 1-hop expansion pulled in. Everything runs on your machine: no server, no
7
+ graph database, no LLM call at retrieval time.
8
+
9
+ ```bash
10
+ pip install "langchain-sqlite-graph-memory[lite]" # ONNX models, starts in seconds
11
+ sgm-index /path/to/your/notes # builds ./index once
12
+ ```
13
+
14
+ ```python
15
+ from langchain_sqlite_graph_memory import SQLiteGraphMemoryRetriever
16
+
17
+ retriever = SQLiteGraphMemoryRetriever(index_dir="./index", k=8)
18
+
19
+ for doc in retriever.invoke("why did we drop the graph database?"):
20
+ print(doc.metadata["rank"], doc.metadata["title"], doc.metadata["via_graph"])
21
+ ```
22
+
23
+ Each result is a `Document`. `page_content` holds the matched chunk of the note. `metadata`
24
+ holds `path`, `title`, `date`, `score` (cross-encoder), `rank`, and `via_graph`. `via_graph`
25
+ is `True` when the note came in only through a wikilink from a top vector hit, so you can
26
+ tell associative recall from direct recall.
27
+
28
+ ## Options
29
+
30
+ | Argument | Default | What it does |
31
+ |---|---|---|
32
+ | `index_dir` | `$BRAIN_INDEX_DIR`, else `./index` | Folder with the `sgm-index` output |
33
+ | `k` | 12 | Documents returned. `retriever.invoke(q, k=3)` overrides it per call |
34
+ | `graph` | `"auto"` | `True` always follows wikilinks, `False` never does. `"auto"` follows them except for name or tool lookups, where the hop measured as noise |
35
+ | `fetch_k` | 60 | Unique notes taken from vector search before expansion and rerank |
36
+ | `encoder`, `reranker` | SGM models | Pass your own preloaded models, or fakes in tests |
37
+
38
+ ## In an agent
39
+
40
+ ```python
41
+ from langchain_core.tools import create_retriever_tool
42
+
43
+ recall = create_retriever_tool(
44
+ retriever, "recall_notes",
45
+ "Search my notes and the notes they link to. Use for anything I wrote down before.",
46
+ )
47
+ ```
48
+
49
+ ## Scope, stated plainly
50
+
51
+ - **This adapter calls the core library and adds nothing to retrieval.** It calls the same
52
+ functions `sgm-ask` calls (`query_sims`, `retrieve_candidates`, `expand_1hop`,
53
+ `rerank_candidates`). A test pins this: the adapter's order must equal a direct call of
54
+ those functions. On a real vault of 29,474 notes (105,383 chunks), the adapter returned the same 12 notes in the
55
+ same order as `sgm-ask --graph`.
56
+ - **The index is two files on disk**, an embedding matrix (`.npy`) and note metadata
57
+ (`.pkl`), written by `sgm-index`. Wikilinks are read from the notes at query time.
58
+ SQLite stores the core library's session memory and A/B logs. The retriever needs only
59
+ the two index files.
60
+ - Embeddings: `intfloat/multilingual-e5-base`. Reranker:
61
+ `cross-encoder/mmarco-mMiniLMv2-L12-H384-v1`. Both work for many languages.
62
+ - Not implemented yet: a `VectorStore` (adding documents through LangChain) and a LangGraph
63
+ long-term memory store. Indexing goes through `sgm-index`.
64
+
65
+ ## Tests
66
+
67
+ ```bash
68
+ pip install -e ".[test]"
69
+ pytest
70
+ ```
71
+
72
+ The suite includes LangChain's standard retriever tests (`langchain-tests`). It runs with
73
+ fake models on a four-note vault: no download, no GPU, a few seconds.
74
+
75
+ ## License
76
+
77
+ MIT
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.21"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "langchain-sqlite-graph-memory"
7
+ version = "0.1.0"
8
+ description = "LangChain retriever for sqlite-graph-memory: vector search + [[wikilink]] graph hop + cross-encoder rerank over your markdown notes. No server."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { file = "LICENSE" }
12
+ authors = [{ name = "Anton Dziatkovskii" }]
13
+ keywords = ["langchain", "retriever", "graph-rag", "obsidian", "wikilinks", "agent-memory", "rag"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
23
+ ]
24
+ dependencies = [
25
+ "langchain-core>=1.0,<2",
26
+ "sqlite-graph-memory>=0.3.6",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ # The two local models. `lite` = ONNX, starts in seconds; `embeddings` = torch.
31
+ lite = ["sqlite-graph-memory[lite]>=0.3.6"]
32
+ embeddings = ["sqlite-graph-memory[embeddings]>=0.3.6"]
33
+ test = ["pytest>=8", "langchain-tests>=1.0,<2"]
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/tonydzi/langchain-sqlite-graph-memory"
37
+ Repository = "https://github.com/tonydzi/langchain-sqlite-graph-memory"
38
+ Issues = "https://github.com/tonydzi/langchain-sqlite-graph-memory/issues"
39
+ "Core library" = "https://github.com/tonydzi/sqlite-graph-memory"
40
+
41
+ [tool.hatch.build.targets.wheel]
42
+ packages = ["src/langchain_sqlite_graph_memory"]
43
+
44
+ [tool.pytest.ini_options]
45
+ testpaths = ["tests"]
46
+ asyncio_mode = "auto"
47
+ asyncio_default_fixture_loop_scope = "function"
@@ -0,0 +1,5 @@
1
+ """LangChain integration for sqlite-graph-memory (Graph RAG over markdown notes)."""
2
+ from langchain_sqlite_graph_memory.retrievers import SQLiteGraphMemoryRetriever
3
+
4
+ __all__ = ["SQLiteGraphMemoryRetriever"]
5
+ __version__ = "0.1.0"
@@ -0,0 +1,146 @@
1
+ """LangChain retriever over a sqlite-graph-memory index.
2
+
3
+ Паспорт (for the people who maintain this):
4
+ in: a query string; an index built by `sgm-index` (two files: _brain_e5.npy,
5
+ _brain_e5_meta.pkl) in `index_dir`.
6
+ out: list[Document], best first. page_content = the matched chunk; metadata =
7
+ path, title, date, score, rank, via_graph.
8
+ who calls: any LangChain / LangGraph app (`retriever.invoke(q)`, a tool, a chain).
9
+ what breaks it: sqlite-graph-memory changing the signatures of load_index,
10
+ query_sims, retrieve_candidates, expand_1hop, rerank_candidates. The test
11
+ suite pins that contract with fake models, so a drift fails CI, not a user.
12
+ рельса: none, no LLM call. Embedding + cross-encoder run locally.
13
+
14
+ It runs the SAME pipeline functions `sgm-ask` runs (imported from
15
+ sqlite_graph_memory.brain_ask), so recall through LangChain cannot drift away from
16
+ recall through the CLI or the MCP server.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ from pathlib import Path
22
+ from typing import Any, List, Literal, Optional, Union
23
+
24
+ from langchain_core.callbacks import CallbackManagerForRetrieverRun
25
+ from langchain_core.documents import Document
26
+ from langchain_core.retrievers import BaseRetriever
27
+ from pydantic import ConfigDict, Field, PrivateAttr
28
+
29
+ from sqlite_graph_memory import brain_ask as _sgm
30
+
31
+ GraphMode = Union[bool, Literal["auto"]]
32
+
33
+
34
+ class SQLiteGraphMemoryRetriever(BaseRetriever):
35
+ """Associative recall over a folder of markdown notes.
36
+
37
+ Vector search finds entry points, 1-hop ``[[wikilinks]]`` pull in linked notes,
38
+ a cross-encoder reranks the union. The index is two files on disk; no server.
39
+
40
+ Setup:
41
+ .. code-block:: bash
42
+
43
+ pip install "langchain-sqlite-graph-memory[lite]"
44
+ sgm-index /path/to/vault --out ./index
45
+
46
+ Instantiate:
47
+ .. code-block:: python
48
+
49
+ from langchain_sqlite_graph_memory import SQLiteGraphMemoryRetriever
50
+
51
+ retriever = SQLiteGraphMemoryRetriever(index_dir="./index", k=8)
52
+
53
+ Invoke:
54
+ .. code-block:: python
55
+
56
+ docs = retriever.invoke("why did we drop the graph database?")
57
+ for d in docs:
58
+ print(d.metadata["rank"], d.metadata["title"], d.metadata["via_graph"])
59
+ """
60
+
61
+ model_config = ConfigDict(arbitrary_types_allowed=True)
62
+
63
+ index_dir: Optional[str] = None
64
+ """Directory with the `sgm-index` output. Default: env BRAIN_INDEX_DIR, then ./index."""
65
+ k: int = Field(default=_sgm.TOPN, ge=1)
66
+ """How many documents to return."""
67
+ graph: GraphMode = "auto"
68
+ """Wikilink expansion. True = always, False = never (plain vector + rerank),
69
+ "auto" = on, except for name/tool lookups, where it measured as noise
70
+ (sqlite_graph_memory.brain_ask.looks_like_entity)."""
71
+ fetch_k: int = Field(default=_sgm.TOPK_RETRIEVE, ge=1)
72
+ """Unique notes taken from vector search before expansion and rerank."""
73
+ encoder: Any = None
74
+ """Optional preloaded encoder with `.encode(...)`. Default: the SGM e5 model."""
75
+ reranker: Any = None
76
+ """Optional preloaded cross-encoder with `.predict(pairs)`. Default: the SGM reranker."""
77
+
78
+ _emb: Any = PrivateAttr(default=None)
79
+ _meta: Any = PrivateAttr(default=None)
80
+ _by_base: Any = PrivateAttr(default=None)
81
+
82
+ # ------------------------------------------------------------------ loading
83
+ def _index_paths(self) -> tuple[Path, Path]:
84
+ base = Path(self.index_dir or os.getenv("BRAIN_INDEX_DIR", "index"))
85
+ return base / "_brain_e5.npy", base / "_brain_e5_meta.pkl"
86
+
87
+ def _ensure_loaded(self) -> None:
88
+ if self._meta is not None:
89
+ return
90
+ emb_path, meta_path = self._index_paths()
91
+ if not emb_path.exists() or not meta_path.exists():
92
+ raise FileNotFoundError(
93
+ "No sqlite-graph-memory index at %s. Build one first: "
94
+ "sgm-index <notes folder> (see https://github.com/tonydzi/sqlite-graph-memory)"
95
+ % emb_path.parent
96
+ )
97
+ self._emb, self._meta = _sgm.load_index(emb_path, meta_path)
98
+ self._by_base = _sgm.index_by_basename(self._meta)
99
+ if self.encoder is None:
100
+ self.encoder = _sgm.load_encoder()
101
+ if self.reranker is None:
102
+ self.reranker = _sgm.load_reranker()
103
+
104
+ def _use_graph(self, query: str) -> bool:
105
+ if self.graph == "auto":
106
+ return not _sgm.looks_like_entity(query)
107
+ return bool(self.graph)
108
+
109
+ # ------------------------------------------------------------------ retrieval
110
+ def _get_relevant_documents(
111
+ self,
112
+ query: str,
113
+ *,
114
+ run_manager: CallbackManagerForRetrieverRun,
115
+ **kwargs: Any,
116
+ ) -> List[Document]:
117
+ k = int(kwargs.get("k", self.k))
118
+ self._ensure_loaded()
119
+ meta = self._meta
120
+ sims = _sgm.query_sims(self.encoder, self._emb, query)
121
+ base_order = _sgm.retrieve_candidates(sims, meta, topk=self.fetch_k)
122
+ added: List[int] = []
123
+ if base_order and self._use_graph(query):
124
+ added = _sgm.expand_1hop(sims, meta, base_order, by_base=self._by_base)
125
+ via_graph = set(added)
126
+ ranked = _sgm.rerank_candidates(
127
+ self.reranker, query, base_order + added, meta, topn=k
128
+ )
129
+ docs = []
130
+ for rank, (i, score) in enumerate(ranked, 1):
131
+ m = meta[i]
132
+ docs.append(
133
+ Document(
134
+ page_content=m.get("snippet", ""),
135
+ metadata={
136
+ "path": m.get("path"),
137
+ "title": m.get("title"),
138
+ "date": m.get("date"),
139
+ "score": float(score),
140
+ "rank": rank,
141
+ "via_graph": i in via_graph,
142
+ "source": m.get("path"),
143
+ },
144
+ )
145
+ )
146
+ return docs
File without changes
@@ -0,0 +1,65 @@
1
+ """Tiny stand-ins for the two local models, so the suite runs in seconds with no torch,
2
+ no ONNX and no network. They implement exactly the methods the SGM pipeline calls."""
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import pickle
7
+ import re
8
+ from pathlib import Path
9
+
10
+ import numpy as np
11
+
12
+ DIM = 64
13
+ _WORD = re.compile(r"[a-z]+")
14
+
15
+
16
+ def _words(text: str) -> list[str]:
17
+ return [w for w in _WORD.findall(text.lower()) if w not in {"query", "passage"}]
18
+
19
+
20
+ def _vec(text: str) -> np.ndarray:
21
+ v = np.zeros(DIM, dtype="float32")
22
+ for w in _words(text):
23
+ v[int(hashlib.md5(w.encode()).hexdigest(), 16) % DIM] += 1.0
24
+ n = np.linalg.norm(v)
25
+ return v / n if n else v
26
+
27
+
28
+ class FakeEncoder:
29
+ def encode(self, texts, normalize_embeddings=True, convert_to_numpy=True):
30
+ return np.stack([_vec(t) for t in texts])
31
+
32
+
33
+ class FakeReranker:
34
+ """Score = shared words between query and chunk, plus a small floor so that a linked
35
+ note with no shared words still ranks (below every note that shares a word)."""
36
+
37
+ def predict(self, pairs):
38
+ out = []
39
+ for q, doc in pairs:
40
+ out.append(len(set(_words(q)) & set(_words(doc))) + 0.01)
41
+ return np.array(out, dtype="float32")
42
+
43
+
44
+ NOTES = {
45
+ "alpha": "Deploy pipeline and release checklist. Before shipping see [[beta]].",
46
+ "beta": "Rollback plan for outages and incidents.",
47
+ "gamma": "Cooking recipes: pasta, sauce, basil.",
48
+ "delta": "Release notes template for the deploy of a new version.",
49
+ }
50
+
51
+
52
+ def build_vault_and_index(root: Path) -> Path:
53
+ """Write NOTES as .md files under root/vault and an SGM-format index under root/index."""
54
+ vault, index = root / "vault", root / "index"
55
+ vault.mkdir(parents=True, exist_ok=True)
56
+ index.mkdir(parents=True, exist_ok=True)
57
+ meta, rows = [], []
58
+ for stem, body in NOTES.items():
59
+ p = vault / ("%s.md" % stem)
60
+ p.write_text(body, encoding="utf-8")
61
+ meta.append({"path": str(p), "title": stem, "snippet": body, "date": "2026-11-05"})
62
+ rows.append(_vec("passage: " + body))
63
+ np.save(index / "_brain_e5.npy", np.stack(rows))
64
+ (index / "_brain_e5_meta.pkl").write_bytes(pickle.dumps(meta))
65
+ return index
@@ -0,0 +1,27 @@
1
+ """LangChain's own standard test suite for retrievers (langchain-tests), run against a
2
+ tiny on-disk index with fake models, so it needs no network and no GPU."""
3
+ import tempfile
4
+ from functools import cached_property
5
+ from pathlib import Path
6
+
7
+ from langchain_tests.integration_tests import RetrieversIntegrationTests
8
+
9
+ from langchain_sqlite_graph_memory import SQLiteGraphMemoryRetriever
10
+ from tests.fakes import FakeEncoder, FakeReranker, build_vault_and_index
11
+
12
+ _TMP = Path(tempfile.mkdtemp(prefix="lc-sgm-"))
13
+ _INDEX = str(build_vault_and_index(_TMP))
14
+
15
+
16
+ class TestSQLiteGraphMemoryRetrieverStandard(RetrieversIntegrationTests):
17
+ @property
18
+ def retriever_constructor(self) -> type[SQLiteGraphMemoryRetriever]:
19
+ return SQLiteGraphMemoryRetriever
20
+
21
+ @property
22
+ def retriever_constructor_params(self) -> dict:
23
+ return {"index_dir": _INDEX, "encoder": FakeEncoder(), "reranker": FakeReranker()}
24
+
25
+ @property
26
+ def retriever_query_example(self) -> str:
27
+ return "deploy release checklist"
@@ -0,0 +1,89 @@
1
+ import pytest
2
+ from langchain_core.documents import Document
3
+
4
+ from langchain_sqlite_graph_memory import SQLiteGraphMemoryRetriever
5
+ from tests.fakes import FakeEncoder, FakeReranker, build_vault_and_index
6
+
7
+
8
+ @pytest.fixture()
9
+ def index_dir(tmp_path):
10
+ return str(build_vault_and_index(tmp_path))
11
+
12
+
13
+ def make(index_dir, **kw):
14
+ return SQLiteGraphMemoryRetriever(
15
+ index_dir=index_dir, encoder=FakeEncoder(), reranker=FakeReranker(), **kw
16
+ )
17
+
18
+
19
+ def test_returns_documents_with_metadata(index_dir):
20
+ docs = make(index_dir, k=2).invoke("deploy release checklist")
21
+ assert docs and all(isinstance(d, Document) for d in docs)
22
+ top = docs[0]
23
+ assert top.metadata["title"] == "alpha"
24
+ assert top.metadata["rank"] == 1
25
+ assert top.metadata["path"].endswith("alpha.md")
26
+ assert top.metadata["source"] == top.metadata["path"]
27
+ assert "release checklist" in top.page_content
28
+
29
+
30
+ def test_wikilink_hop_brings_in_linked_note(index_dir):
31
+ # fetch_k=1: vector search alone sees only `alpha`; `beta` shares no word with the
32
+ # query and is reachable only through alpha's [[beta]] link.
33
+ docs = make(index_dir, k=3, fetch_k=1, graph=True).invoke("deploy checklist")
34
+ titles = [d.metadata["title"] for d in docs]
35
+ assert titles == ["alpha", "beta"]
36
+ assert docs[1].metadata["via_graph"] is True
37
+ assert docs[0].metadata["via_graph"] is False
38
+
39
+
40
+ def test_graph_off_is_plain_vector_plus_rerank(index_dir):
41
+ docs = make(index_dir, k=3, fetch_k=1, graph=False).invoke("deploy checklist")
42
+ assert [d.metadata["title"] for d in docs] == ["alpha"]
43
+
44
+
45
+ def test_auto_skips_graph_for_entity_lookups(index_dir):
46
+ # "Obsidian" is a known tool token -> looks_like_entity -> no expansion
47
+ docs = make(index_dir, k=3, fetch_k=1).invoke("Obsidian")
48
+ assert not any(d.metadata["via_graph"] for d in docs)
49
+ docs = make(index_dir, k=3, fetch_k=1).invoke("deploy checklist")
50
+ assert any(d.metadata["via_graph"] for d in docs)
51
+
52
+
53
+ def test_k_kwarg_overrides_constructor(index_dir):
54
+ r = make(index_dir, k=4)
55
+ assert len(r.invoke("release deploy")) == 4
56
+ assert len(r.invoke("release deploy", k=1)) == 1
57
+
58
+
59
+ def test_missing_index_says_how_to_build_one(tmp_path):
60
+ r = make(str(tmp_path / "nowhere"))
61
+ with pytest.raises(FileNotFoundError, match="sgm-index"):
62
+ r.invoke("anything")
63
+
64
+
65
+ def test_same_order_as_the_sgm_pipeline(index_dir):
66
+ """The adapter must not re-implement retrieval: its order equals what the SGM
67
+ functions produce when called directly, the way sgm-ask calls them."""
68
+ from sqlite_graph_memory import brain_ask as sgm
69
+ from pathlib import Path
70
+
71
+ q = "release deploy version"
72
+ emb, meta = sgm.load_index(Path(index_dir) / "_brain_e5.npy", Path(index_dir) / "_brain_e5_meta.pkl")
73
+ sims = sgm.query_sims(FakeEncoder(), emb, q)
74
+ base = sgm.retrieve_candidates(sims, meta)
75
+ added = sgm.expand_1hop(sims, meta, base)
76
+ want = [meta[i]["title"] for i, _ in sgm.rerank_candidates(FakeReranker(), q, base + added, meta, topn=3)]
77
+ got = [d.metadata["title"] for d in make(index_dir, k=3, graph=True).invoke(q)]
78
+ assert got == want
79
+
80
+
81
+ async def _ainvoke(r, q):
82
+ return await r.ainvoke(q)
83
+
84
+
85
+ def test_async_invoke(index_dir):
86
+ import asyncio
87
+
88
+ docs = asyncio.run(_ainvoke(make(index_dir, k=2), "deploy release"))
89
+ assert len(docs) == 2