langgraph-velesdb 4.2.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,27 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2026 Wiscale France
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.
22
+
23
+ ---
24
+
25
+ Note: This MIT license applies to the LangChain integration code only.
26
+ VelesDB Core itself is licensed under the Elastic License 2.0 (ELv2).
27
+ See https://github.com/cyberlife-coder/VelesDB for VelesDB Core licensing.
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.4
2
+ Name: langgraph-velesdb
3
+ Version: 4.2.0
4
+ Summary: LangGraph memory tools for VelesDB: drop the why() knowledge-graph memory wedge into any LangGraph agent.
5
+ Author-email: VelesDB Team <contact@wiscale.fr>
6
+ License: MIT
7
+ Project-URL: Homepage, https://velesdb.com
8
+ Project-URL: Repository, https://github.com/cyberlife-coder/VelesDB
9
+ Keywords: langgraph,langchain,velesdb,agent-memory,knowledge-graph,local-first,why,llm
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: langchain-core<2.0.0,>=0.3.0
22
+ Requires-Dist: velesdb>=3.12.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest<9.0,>=7.0; extra == "dev"
25
+ Requires-Dist: langgraph>=0.2.0; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ # langgraph-velesdb
29
+
30
+ Drop VelesDB's local-first agent memory — the **`why()` knowledge-graph wedge** —
31
+ into any [LangGraph](https://github.com/langchain-ai/langgraph) agent in three lines.
32
+
33
+ Most agent memory is vector recall: it finds text that *looks like* the query.
34
+ VelesDB **connects** memories with typed links, so `why()` answers with the
35
+ best-matching memory **plus the connected subgraph** — context that shares no
36
+ words with the question, which a plain vector recall is blind to. The store is on
37
+ disk, so memory persists across agent runs.
38
+
39
+ ![recall() finds the booking but misses the reason; why() reaches it through typed links, across a session restart](https://raw.githubusercontent.com/cyberlife-coder/VelesDB/develop/examples/agent_memory/why_across_sessions.gif)
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ pip install langgraph-velesdb # pulls langchain-core + velesdb
45
+ ```
46
+
47
+ ## Use
48
+
49
+ ```python
50
+ from langgraph.prebuilt import create_react_agent
51
+ from langgraph_velesdb import make_memory_tools
52
+
53
+ # Offline by default (hash embedder); pass a configured MemoryService for Ollama.
54
+ agent = create_react_agent(llm, make_memory_tools("./agent_memory"))
55
+ ```
56
+
57
+ `make_memory_tools` returns ten tools the agent can call:
58
+
59
+ | Tool | What it does |
60
+ |------------------------|--------------|
61
+ | `remember` | store a fact (optionally with `links`, `metadata`, `ttl_seconds`), return its id |
62
+ | `recall` | semantic (vector) recall — no metadata |
63
+ | `recall_where` | filtered recall (metadata `[field, op, value]` triples) **with metadata attached**, e.g. the auto `_veles_date` stamp |
64
+ | `recall_fused` | fused vector + graph recall; pass `date_field="_veles_date"` for a dated timeline |
65
+ | `relate` | link two memories with a typed edge |
66
+ | `forget` | delete a memory by id (`True` if it existed, `False` if it was already gone) |
67
+ | `feedback` | reinforce or weaken a memory after using it, closing the self-improving recall loop |
68
+ | `why` | best-matching memory **+ its connected subgraph** (the wedge) |
69
+ | `save_working_context` | persist the current goal/constraints/decisions/pending actions under a project + session |
70
+ | `load_working_context` | resume a prior run's working context — call at the start of a session |
71
+
72
+ For a pre-configured backend (e.g. Ollama embeddings), build the service yourself
73
+ and pass it through:
74
+
75
+ ```python
76
+ from velesdb import MemoryService
77
+ tools = make_memory_tools(service=MemoryService("./agent_memory"))
78
+ ```
79
+
80
+ ### Cross-run resumption
81
+
82
+ `save_working_context` / `load_working_context` let an agent pick up a task
83
+ across separate runs instead of restarting from scratch:
84
+
85
+ `make_memory_tools` returns a *list* of tools, so index them by name first
86
+ (the same pattern LangGraph's `ToolNode`/`create_react_agent` use internally):
87
+
88
+ ```python
89
+ tools = {t.name: t for t in make_memory_tools("./agent_memory")}
90
+
91
+ tools["save_working_context"].invoke({
92
+ "project": "veles",
93
+ "session": "issue-1546",
94
+ "working": {
95
+ "goal": "ship the langgraph tool set",
96
+ "pending_actions": ["open the PR"],
97
+ },
98
+ })
99
+
100
+ # ... next run, same project + session:
101
+ tools["load_working_context"].invoke({"project": "veles", "session": "issue-1546"})
102
+ ```
103
+
104
+ ### Dated recall
105
+
106
+ Every `remember`-ed fact is auto-stamped with `_veles_date` (today, as a
107
+ `YYYYMMDD` int) unless you set that metadata key yourself. Point
108
+ `recall_fused` at it to get a chronological timeline instead of a ranked list:
109
+
110
+ ```python
111
+ tools = {t.name: t for t in make_memory_tools("./agent_memory")}
112
+
113
+ tools["recall_fused"].invoke({"query": "what changed this week", "date_field": "_veles_date"})
114
+ # -> {"memories": [...], "dated_context": "...", "now": "..."}
115
+ ```
116
+
117
+ ## Compatibility
118
+
119
+ This package requires `velesdb>=3.12.0`, the highest version published to
120
+ PyPI at the time of writing. `feedback`, `save_working_context`,
121
+ `load_working_context`, and the automatic `_veles_date` metadata stamp landed
122
+ in `velesdb`/`velesdb-memory` *after* the 3.12.0 release cut and are not yet
123
+ in a published wheel. On a plain 3.12.0 install those three tools detect the
124
+ missing binding method at call time and return an error payload instead of
125
+ raising, e.g.:
126
+
127
+ ```json
128
+ {"error": "feedback requires velesdb > 3.12.0 — upgrade with `pip install -U velesdb`"}
129
+ ```
130
+
131
+ so a single unsupported call surfaces to the agent as a normal tool result it
132
+ can react to, instead of an uncaught `AttributeError` killing the whole graph
133
+ run. `recall_where`/`recall_fused` still work, but their `metadata` stays
134
+ empty for auto-dating until you upgrade. `forget` works on 3.12.0 too, but
135
+ returns `None` instead of a `True`/`False` existed-or-not signal. The floor
136
+ will be bumped again once a `velesdb` release past 3.12.0 ships.
137
+
138
+ `list_working_contexts` (browse saved sessions for a project) is not exposed
139
+ here: it exists on the WASM and MCP surfaces but not yet on the `velesdb`
140
+ Python binding (`MemoryService`), and this package only ever calls the
141
+ binding — no memory logic is reimplemented in this MIT-licensed integration.
142
+ It will be added once the Python binding grows the method.
143
+
144
+ ## License
145
+
146
+ MIT — see [LICENSE](./LICENSE). Wraps [`velesdb`](https://pypi.org/project/velesdb/),
147
+ which is under the VelesDB Core License 1.0.
@@ -0,0 +1,120 @@
1
+ # langgraph-velesdb
2
+
3
+ Drop VelesDB's local-first agent memory — the **`why()` knowledge-graph wedge** —
4
+ into any [LangGraph](https://github.com/langchain-ai/langgraph) agent in three lines.
5
+
6
+ Most agent memory is vector recall: it finds text that *looks like* the query.
7
+ VelesDB **connects** memories with typed links, so `why()` answers with the
8
+ best-matching memory **plus the connected subgraph** — context that shares no
9
+ words with the question, which a plain vector recall is blind to. The store is on
10
+ disk, so memory persists across agent runs.
11
+
12
+ ![recall() finds the booking but misses the reason; why() reaches it through typed links, across a session restart](https://raw.githubusercontent.com/cyberlife-coder/VelesDB/develop/examples/agent_memory/why_across_sessions.gif)
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pip install langgraph-velesdb # pulls langchain-core + velesdb
18
+ ```
19
+
20
+ ## Use
21
+
22
+ ```python
23
+ from langgraph.prebuilt import create_react_agent
24
+ from langgraph_velesdb import make_memory_tools
25
+
26
+ # Offline by default (hash embedder); pass a configured MemoryService for Ollama.
27
+ agent = create_react_agent(llm, make_memory_tools("./agent_memory"))
28
+ ```
29
+
30
+ `make_memory_tools` returns ten tools the agent can call:
31
+
32
+ | Tool | What it does |
33
+ |------------------------|--------------|
34
+ | `remember` | store a fact (optionally with `links`, `metadata`, `ttl_seconds`), return its id |
35
+ | `recall` | semantic (vector) recall — no metadata |
36
+ | `recall_where` | filtered recall (metadata `[field, op, value]` triples) **with metadata attached**, e.g. the auto `_veles_date` stamp |
37
+ | `recall_fused` | fused vector + graph recall; pass `date_field="_veles_date"` for a dated timeline |
38
+ | `relate` | link two memories with a typed edge |
39
+ | `forget` | delete a memory by id (`True` if it existed, `False` if it was already gone) |
40
+ | `feedback` | reinforce or weaken a memory after using it, closing the self-improving recall loop |
41
+ | `why` | best-matching memory **+ its connected subgraph** (the wedge) |
42
+ | `save_working_context` | persist the current goal/constraints/decisions/pending actions under a project + session |
43
+ | `load_working_context` | resume a prior run's working context — call at the start of a session |
44
+
45
+ For a pre-configured backend (e.g. Ollama embeddings), build the service yourself
46
+ and pass it through:
47
+
48
+ ```python
49
+ from velesdb import MemoryService
50
+ tools = make_memory_tools(service=MemoryService("./agent_memory"))
51
+ ```
52
+
53
+ ### Cross-run resumption
54
+
55
+ `save_working_context` / `load_working_context` let an agent pick up a task
56
+ across separate runs instead of restarting from scratch:
57
+
58
+ `make_memory_tools` returns a *list* of tools, so index them by name first
59
+ (the same pattern LangGraph's `ToolNode`/`create_react_agent` use internally):
60
+
61
+ ```python
62
+ tools = {t.name: t for t in make_memory_tools("./agent_memory")}
63
+
64
+ tools["save_working_context"].invoke({
65
+ "project": "veles",
66
+ "session": "issue-1546",
67
+ "working": {
68
+ "goal": "ship the langgraph tool set",
69
+ "pending_actions": ["open the PR"],
70
+ },
71
+ })
72
+
73
+ # ... next run, same project + session:
74
+ tools["load_working_context"].invoke({"project": "veles", "session": "issue-1546"})
75
+ ```
76
+
77
+ ### Dated recall
78
+
79
+ Every `remember`-ed fact is auto-stamped with `_veles_date` (today, as a
80
+ `YYYYMMDD` int) unless you set that metadata key yourself. Point
81
+ `recall_fused` at it to get a chronological timeline instead of a ranked list:
82
+
83
+ ```python
84
+ tools = {t.name: t for t in make_memory_tools("./agent_memory")}
85
+
86
+ tools["recall_fused"].invoke({"query": "what changed this week", "date_field": "_veles_date"})
87
+ # -> {"memories": [...], "dated_context": "...", "now": "..."}
88
+ ```
89
+
90
+ ## Compatibility
91
+
92
+ This package requires `velesdb>=3.12.0`, the highest version published to
93
+ PyPI at the time of writing. `feedback`, `save_working_context`,
94
+ `load_working_context`, and the automatic `_veles_date` metadata stamp landed
95
+ in `velesdb`/`velesdb-memory` *after* the 3.12.0 release cut and are not yet
96
+ in a published wheel. On a plain 3.12.0 install those three tools detect the
97
+ missing binding method at call time and return an error payload instead of
98
+ raising, e.g.:
99
+
100
+ ```json
101
+ {"error": "feedback requires velesdb > 3.12.0 — upgrade with `pip install -U velesdb`"}
102
+ ```
103
+
104
+ so a single unsupported call surfaces to the agent as a normal tool result it
105
+ can react to, instead of an uncaught `AttributeError` killing the whole graph
106
+ run. `recall_where`/`recall_fused` still work, but their `metadata` stays
107
+ empty for auto-dating until you upgrade. `forget` works on 3.12.0 too, but
108
+ returns `None` instead of a `True`/`False` existed-or-not signal. The floor
109
+ will be bumped again once a `velesdb` release past 3.12.0 ships.
110
+
111
+ `list_working_contexts` (browse saved sessions for a project) is not exposed
112
+ here: it exists on the WASM and MCP surfaces but not yet on the `velesdb`
113
+ Python binding (`MemoryService`), and this package only ever calls the
114
+ binding — no memory logic is reimplemented in this MIT-licensed integration.
115
+ It will be added once the Python binding grows the method.
116
+
117
+ ## License
118
+
119
+ MIT — see [LICENSE](./LICENSE). Wraps [`velesdb`](https://pypi.org/project/velesdb/),
120
+ which is under the VelesDB Core License 1.0.
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "langgraph-velesdb"
7
+ version = "4.2.0"
8
+ description = "LangGraph memory tools for VelesDB: drop the why() knowledge-graph memory wedge into any LangGraph agent."
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ authors = [
12
+ {name = "VelesDB Team", email = "contact@wiscale.fr"}
13
+ ]
14
+ keywords = ["langgraph", "langchain", "velesdb", "agent-memory", "knowledge-graph", "local-first", "why", "llm"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
24
+ ]
25
+ requires-python = ">=3.10"
26
+ dependencies = [
27
+ "langchain-core>=0.3.0,<2.0.0",
28
+ # 3.12.0 is the highest velesdb release published to PyPI as of this
29
+ # writing. It ships forget/recall_where/recall_fused, but not yet
30
+ # MemoryService.feedback/save_working_context/load_working_context, nor
31
+ # the automatic `_veles_date` metadata stamp (velesdb-memory 0.11.0) —
32
+ # those tools will raise AttributeError, and recall_where/recall_fused's
33
+ # `metadata` stays empty for auto-dating, until velesdb publishes past
34
+ # 3.12.0. See README.md's "Compatibility" section.
35
+ "velesdb>=3.12.0",
36
+ ]
37
+
38
+ [project.optional-dependencies]
39
+ dev = [
40
+ "pytest>=7.0,<9.0",
41
+ "langgraph>=0.2.0",
42
+ ]
43
+
44
+ [project.urls]
45
+ Homepage = "https://velesdb.com"
46
+ Repository = "https://github.com/cyberlife-coder/VelesDB"
47
+
48
+ [tool.setuptools.packages.find]
49
+ where = ["src"]
50
+
51
+ [tool.setuptools.package-data]
52
+ langgraph_velesdb = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,21 @@
1
+ """LangGraph memory tools for VelesDB — the ``why()`` knowledge-graph wedge.
2
+
3
+ Drop VelesDB's local-first agent memory into any LangGraph agent:
4
+
5
+ >>> from langgraph.prebuilt import create_react_agent
6
+ >>> from langgraph_velesdb import make_memory_tools
7
+ >>>
8
+ >>> agent = create_react_agent(llm, make_memory_tools("./agent_memory"))
9
+
10
+ ``make_memory_tools`` returns the full VelesDB memory tool set: ``remember``,
11
+ ``recall``, ``recall_where``, ``recall_fused``, ``relate``, ``forget``,
12
+ ``feedback``, and the differentiator ``why`` — which answers with the
13
+ best-matching memory *plus* the connected subgraph a vector recall is blind
14
+ to — plus ``save_working_context``/``load_working_context`` for cross-run
15
+ resumption. The store is on disk, so memory persists across agent runs.
16
+ """
17
+
18
+ from langgraph_velesdb.tools import make_memory_tools
19
+
20
+ __all__ = ["make_memory_tools"]
21
+ __version__ = "4.2.0"
File without changes
@@ -0,0 +1,245 @@
1
+ """LangGraph / LangChain tools backed by VelesDB's high-level ``MemoryService``.
2
+
3
+ A thin adapter: each tool is a small method on ``_MemoryToolkit``, bound to one
4
+ ``MemoryService`` (the core). No state or logic lives here beyond shaping
5
+ inputs/outputs for the tool layer — the engine does the work. Methods live on
6
+ a class (rather than as closures inside ``make_memory_tools``) so each tool's
7
+ own cyclomatic complexity is what a linter measures, instead of all ten
8
+ rolling up into one function.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any, Optional
14
+
15
+ from langchain_core.tools import BaseTool, StructuredTool # nosemgrep: ai.python.detect-langchain.detect-langchain # this package IS the LangChain connector
16
+
17
+ try:
18
+ from velesdb import MemoryService
19
+ except ImportError as exc: # pragma: no cover - import guard
20
+ raise ImportError(
21
+ "velesdb is required for langgraph-velesdb. Install with: pip install velesdb"
22
+ ) from exc
23
+
24
+
25
+ def make_memory_tools(
26
+ path: Optional[str] = None,
27
+ *,
28
+ service: Optional["MemoryService"] = None,
29
+ ) -> list[BaseTool]:
30
+ """Build the full VelesDB memory tool set for a LangGraph agent.
31
+
32
+ Returns ``remember`` / ``recall`` / ``recall_where`` / ``recall_fused`` /
33
+ ``relate`` / ``forget`` / ``feedback`` / ``why`` / ``save_working_context``
34
+ / ``load_working_context``.
35
+
36
+ Pass a ``path`` to open a local on-disk store (offline hash embedder), or a
37
+ pre-configured ``service`` (e.g. an Ollama-backed ``MemoryService``) to reuse
38
+ it. The store is on disk, so memory persists across agent runs.
39
+ """
40
+ toolkit = _MemoryToolkit(_resolve_service(path, service))
41
+ return [
42
+ StructuredTool.from_function(fn)
43
+ for fn in (
44
+ toolkit.remember,
45
+ toolkit.recall,
46
+ toolkit.recall_where,
47
+ toolkit.recall_fused,
48
+ toolkit.relate,
49
+ toolkit.forget,
50
+ toolkit.feedback,
51
+ toolkit.why,
52
+ toolkit.save_working_context,
53
+ toolkit.load_working_context,
54
+ )
55
+ ]
56
+
57
+
58
+ class _MemoryToolkit:
59
+ """Binds one ``MemoryService`` to the ten tool methods ``make_memory_tools``
60
+ wraps into ``StructuredTool``\\ s. Not part of the public API."""
61
+
62
+ def __init__(self, mem: "MemoryService") -> None:
63
+ self._mem = mem
64
+
65
+ def remember(
66
+ self,
67
+ fact: str,
68
+ links: Optional[list[list[Any]]] = None,
69
+ metadata: Optional[dict[str, Any]] = None,
70
+ ttl_seconds: Optional[int] = None,
71
+ ) -> int:
72
+ """Store a fact in long-term memory and return its id.
73
+
74
+ ``links`` optionally wires typed outgoing edges to existing memory ids
75
+ at write time — each item is ``[target_id, relation]`` (equivalent to
76
+ calling ``relate`` right after). ``metadata`` attaches key/value pairs
77
+ ``recall_where``/``recall_fused`` can later filter on (every fact also
78
+ gets an automatic ``_veles_date`` stamp — today's date as a
79
+ ``YYYYMMDD`` int — unless you set that key yourself). ``ttl_seconds``
80
+ makes the fact expire, and stop being recalled, after that many
81
+ seconds. All three are optional; a bare ``remember(fact)`` behaves
82
+ exactly as before.
83
+ """
84
+ return self._mem.remember(
85
+ fact,
86
+ links=_as_tuples(links),
87
+ metadata=metadata,
88
+ ttl_seconds=ttl_seconds,
89
+ )
90
+
91
+ def recall(self, query: str, k: int = 5) -> list[dict[str, Any]]:
92
+ """Recall memories semantically similar to the query (vector search).
93
+
94
+ Each hit's ``metadata`` is always ``None`` on this path — use
95
+ ``recall_where`` (or ``recall_fused``) when you need each memory's
96
+ metadata, including the automatic ``_veles_date`` stamp.
97
+ """
98
+ return self._mem.recall(query, k)
99
+
100
+ def recall_where(
101
+ self,
102
+ query: str,
103
+ filters: Optional[list[list[Any]]] = None,
104
+ k: int = 5,
105
+ ) -> list[dict[str, Any]]:
106
+ """Recall memories similar to the query, filtered by metadata, with
107
+ each hit's metadata attached.
108
+
109
+ ``filters`` is a list of ``[field, op, value]`` triples, ``op`` one of
110
+ ``"eq"``/``"ne"``/``"lt"``/``"le"``/``"gt"``/``"ge"`` — e.g.
111
+ ``[["_veles_date", "ge", 20260101]]`` keeps only facts stamped on or
112
+ after that date. Pass an empty list (the default) for no filtering.
113
+ Unlike plain ``recall``, every hit's ``metadata`` dict is returned,
114
+ including the automatic ``_veles_date`` stamp every fact gets at
115
+ write time.
116
+ """
117
+ return self._mem.recall_where(query, _as_tuples(filters) or [], k)
118
+
119
+ def recall_fused(
120
+ self,
121
+ query: str,
122
+ k: int = 5,
123
+ metadata_filter: Optional[dict[str, Any]] = None,
124
+ date_field: Optional[str] = None,
125
+ ) -> Any:
126
+ """Fused vector + graph recall: walks the graph from the top hit and
127
+ folds in any connected fact a plain ``recall`` would miss.
128
+
129
+ ``metadata_filter`` is an optional exact-match metadata filter. Pass
130
+ ``date_field="_veles_date"`` (the automatic stamp every fact gets) to
131
+ get back a dict with a chronological, date-prefixed timeline
132
+ (``dated_context``) instead of a plain list — useful for "what
133
+ happened, in order" questions. Omit ``date_field`` for the plain-list
134
+ form.
135
+ """
136
+ return self._mem.recall_fused(query, k, metadata_filter, date_field=date_field)
137
+
138
+ def relate(self, from_id: int, to_id: int, relation: str) -> int:
139
+ """Link two memories with a typed edge (``from_id`` -> ``to_id``)."""
140
+ return self._mem.relate(from_id, to_id, relation)
141
+
142
+ def forget(self, memory_id: int) -> bool:
143
+ """Delete a memory by id.
144
+
145
+ Returns ``True`` if a memory actually existed under ``memory_id`` and
146
+ was removed, ``False`` if there was nothing stored there (a stale id
147
+ or a typo) — a no-op, not an error.
148
+ """
149
+ return self._mem.forget(memory_id)
150
+
151
+ def feedback(self, memory_id: int, success: bool) -> Any:
152
+ """Reinforce or weaken a memory after using it, and return its
153
+ updated confidence in ``[0.0, 1.0]``.
154
+
155
+ Call this once you know whether a memory you recalled actually helped
156
+ answer the question (``success=True``) or was misleading/irrelevant
157
+ (``success=False``). This closes the self-improving loop: future
158
+ ``recall``/``recall_where``/``recall_fused`` calls rank
159
+ higher-confidence memories higher.
160
+
161
+ If the installed ``velesdb`` predates this method, returns
162
+ ``{"error": "..."}`` instead of raising, telling you to upgrade.
163
+ """
164
+ if not hasattr(self._mem, "feedback"):
165
+ return _unsupported("feedback")
166
+ return self._mem.feedback(memory_id, success)
167
+
168
+ def why(self, question: str, max_hops: int = 2) -> dict[str, Any]:
169
+ """Explain something: the best-matching memory plus the connected
170
+ subgraph reachable through typed links — context a plain recall misses."""
171
+ return self._mem.why(question, max_hops)
172
+
173
+ def save_working_context(
174
+ self,
175
+ project: str,
176
+ session: str,
177
+ working: dict[str, Any],
178
+ ) -> Any:
179
+ """Persist the current working state under ``project`` + ``session``
180
+ so a later run can resume exactly where this one left off.
181
+
182
+ ``working`` is free-form but conventionally shaped like
183
+ ``{"goal": str, "active_constraints": [...], "verified_facts": [...],
184
+ "open_hypotheses": [...], "decisions": [...], "exact_evidence": [...],
185
+ "pending_actions": [...]}`` — call this near the end of a session, or
186
+ whenever the plan materially changes. Saving again under the same
187
+ ``project`` + ``session`` replaces the previous state (not a merge).
188
+
189
+ If the installed ``velesdb`` predates this method, returns
190
+ ``{"error": "..."}`` instead of raising, telling you to upgrade.
191
+ """
192
+ if not hasattr(self._mem, "save_working_context"):
193
+ return _unsupported("save_working_context")
194
+ return self._mem.save_working_context(project, session, working)
195
+
196
+ def load_working_context(self, project: str, session: str) -> Any:
197
+ """Load the working context previously saved under ``project`` +
198
+ ``session`` by ``save_working_context``.
199
+
200
+ Call this at the very start of a new run, before doing anything else,
201
+ to resume a prior session instead of restarting from scratch. Returns
202
+ ``None`` when nothing was ever saved under that exact
203
+ ``project``/``session`` pair — not an error, just a fresh start.
204
+
205
+ If the installed ``velesdb`` predates this method, returns
206
+ ``{"error": "..."}`` instead of raising, telling you to upgrade.
207
+ """
208
+ if not hasattr(self._mem, "load_working_context"):
209
+ return _unsupported("load_working_context")
210
+ return self._mem.load_working_context(project, session)
211
+
212
+
213
+ def _resolve_service(
214
+ path: Optional[str], service: Optional["MemoryService"]
215
+ ) -> "MemoryService":
216
+ if service is not None:
217
+ return service
218
+ if path is None:
219
+ raise ValueError("make_memory_tools requires either `path` or `service`")
220
+ return MemoryService(path)
221
+
222
+
223
+ def _as_tuples(pairs: Optional[list[list[Any]]]) -> Optional[list[tuple[Any, ...]]]:
224
+ """``[[a, b, ...], ...]`` -> ``[(a, b, ...), ...]``, or ``None`` if falsy.
225
+
226
+ Shared by ``remember``'s ``links`` and ``recall_where``'s ``filters``: both
227
+ accept plain JSON-friendly lists of lists from the LLM and forward tuples
228
+ to the binding, which expects ``List[Tuple[...]]``.
229
+ """
230
+ if not pairs:
231
+ return None
232
+ return [tuple(p) for p in pairs]
233
+
234
+
235
+ def _unsupported(method: str) -> dict[str, str]:
236
+ """Actionable tool-error payload for a ``MemoryService`` method missing on
237
+ the installed ``velesdb`` (older than this package expects).
238
+
239
+ Returned as a normal tool result rather than raised, so a missing method
240
+ surfaces to the LLM as a tool error it can react to instead of an
241
+ uncaught ``AttributeError`` that would kill the whole LangGraph run.
242
+ """
243
+ return {
244
+ "error": f"{method} requires velesdb > 3.12.0 — upgrade with `pip install -U velesdb`"
245
+ }
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.4
2
+ Name: langgraph-velesdb
3
+ Version: 4.2.0
4
+ Summary: LangGraph memory tools for VelesDB: drop the why() knowledge-graph memory wedge into any LangGraph agent.
5
+ Author-email: VelesDB Team <contact@wiscale.fr>
6
+ License: MIT
7
+ Project-URL: Homepage, https://velesdb.com
8
+ Project-URL: Repository, https://github.com/cyberlife-coder/VelesDB
9
+ Keywords: langgraph,langchain,velesdb,agent-memory,knowledge-graph,local-first,why,llm
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: langchain-core<2.0.0,>=0.3.0
22
+ Requires-Dist: velesdb>=3.12.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest<9.0,>=7.0; extra == "dev"
25
+ Requires-Dist: langgraph>=0.2.0; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ # langgraph-velesdb
29
+
30
+ Drop VelesDB's local-first agent memory — the **`why()` knowledge-graph wedge** —
31
+ into any [LangGraph](https://github.com/langchain-ai/langgraph) agent in three lines.
32
+
33
+ Most agent memory is vector recall: it finds text that *looks like* the query.
34
+ VelesDB **connects** memories with typed links, so `why()` answers with the
35
+ best-matching memory **plus the connected subgraph** — context that shares no
36
+ words with the question, which a plain vector recall is blind to. The store is on
37
+ disk, so memory persists across agent runs.
38
+
39
+ ![recall() finds the booking but misses the reason; why() reaches it through typed links, across a session restart](https://raw.githubusercontent.com/cyberlife-coder/VelesDB/develop/examples/agent_memory/why_across_sessions.gif)
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ pip install langgraph-velesdb # pulls langchain-core + velesdb
45
+ ```
46
+
47
+ ## Use
48
+
49
+ ```python
50
+ from langgraph.prebuilt import create_react_agent
51
+ from langgraph_velesdb import make_memory_tools
52
+
53
+ # Offline by default (hash embedder); pass a configured MemoryService for Ollama.
54
+ agent = create_react_agent(llm, make_memory_tools("./agent_memory"))
55
+ ```
56
+
57
+ `make_memory_tools` returns ten tools the agent can call:
58
+
59
+ | Tool | What it does |
60
+ |------------------------|--------------|
61
+ | `remember` | store a fact (optionally with `links`, `metadata`, `ttl_seconds`), return its id |
62
+ | `recall` | semantic (vector) recall — no metadata |
63
+ | `recall_where` | filtered recall (metadata `[field, op, value]` triples) **with metadata attached**, e.g. the auto `_veles_date` stamp |
64
+ | `recall_fused` | fused vector + graph recall; pass `date_field="_veles_date"` for a dated timeline |
65
+ | `relate` | link two memories with a typed edge |
66
+ | `forget` | delete a memory by id (`True` if it existed, `False` if it was already gone) |
67
+ | `feedback` | reinforce or weaken a memory after using it, closing the self-improving recall loop |
68
+ | `why` | best-matching memory **+ its connected subgraph** (the wedge) |
69
+ | `save_working_context` | persist the current goal/constraints/decisions/pending actions under a project + session |
70
+ | `load_working_context` | resume a prior run's working context — call at the start of a session |
71
+
72
+ For a pre-configured backend (e.g. Ollama embeddings), build the service yourself
73
+ and pass it through:
74
+
75
+ ```python
76
+ from velesdb import MemoryService
77
+ tools = make_memory_tools(service=MemoryService("./agent_memory"))
78
+ ```
79
+
80
+ ### Cross-run resumption
81
+
82
+ `save_working_context` / `load_working_context` let an agent pick up a task
83
+ across separate runs instead of restarting from scratch:
84
+
85
+ `make_memory_tools` returns a *list* of tools, so index them by name first
86
+ (the same pattern LangGraph's `ToolNode`/`create_react_agent` use internally):
87
+
88
+ ```python
89
+ tools = {t.name: t for t in make_memory_tools("./agent_memory")}
90
+
91
+ tools["save_working_context"].invoke({
92
+ "project": "veles",
93
+ "session": "issue-1546",
94
+ "working": {
95
+ "goal": "ship the langgraph tool set",
96
+ "pending_actions": ["open the PR"],
97
+ },
98
+ })
99
+
100
+ # ... next run, same project + session:
101
+ tools["load_working_context"].invoke({"project": "veles", "session": "issue-1546"})
102
+ ```
103
+
104
+ ### Dated recall
105
+
106
+ Every `remember`-ed fact is auto-stamped with `_veles_date` (today, as a
107
+ `YYYYMMDD` int) unless you set that metadata key yourself. Point
108
+ `recall_fused` at it to get a chronological timeline instead of a ranked list:
109
+
110
+ ```python
111
+ tools = {t.name: t for t in make_memory_tools("./agent_memory")}
112
+
113
+ tools["recall_fused"].invoke({"query": "what changed this week", "date_field": "_veles_date"})
114
+ # -> {"memories": [...], "dated_context": "...", "now": "..."}
115
+ ```
116
+
117
+ ## Compatibility
118
+
119
+ This package requires `velesdb>=3.12.0`, the highest version published to
120
+ PyPI at the time of writing. `feedback`, `save_working_context`,
121
+ `load_working_context`, and the automatic `_veles_date` metadata stamp landed
122
+ in `velesdb`/`velesdb-memory` *after* the 3.12.0 release cut and are not yet
123
+ in a published wheel. On a plain 3.12.0 install those three tools detect the
124
+ missing binding method at call time and return an error payload instead of
125
+ raising, e.g.:
126
+
127
+ ```json
128
+ {"error": "feedback requires velesdb > 3.12.0 — upgrade with `pip install -U velesdb`"}
129
+ ```
130
+
131
+ so a single unsupported call surfaces to the agent as a normal tool result it
132
+ can react to, instead of an uncaught `AttributeError` killing the whole graph
133
+ run. `recall_where`/`recall_fused` still work, but their `metadata` stays
134
+ empty for auto-dating until you upgrade. `forget` works on 3.12.0 too, but
135
+ returns `None` instead of a `True`/`False` existed-or-not signal. The floor
136
+ will be bumped again once a `velesdb` release past 3.12.0 ships.
137
+
138
+ `list_working_contexts` (browse saved sessions for a project) is not exposed
139
+ here: it exists on the WASM and MCP surfaces but not yet on the `velesdb`
140
+ Python binding (`MemoryService`), and this package only ever calls the
141
+ binding — no memory logic is reimplemented in this MIT-licensed integration.
142
+ It will be added once the Python binding grows the method.
143
+
144
+ ## License
145
+
146
+ MIT — see [LICENSE](./LICENSE). Wraps [`velesdb`](https://pypi.org/project/velesdb/),
147
+ which is under the VelesDB Core License 1.0.
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/langgraph_velesdb/__init__.py
5
+ src/langgraph_velesdb/py.typed
6
+ src/langgraph_velesdb/tools.py
7
+ src/langgraph_velesdb.egg-info/PKG-INFO
8
+ src/langgraph_velesdb.egg-info/SOURCES.txt
9
+ src/langgraph_velesdb.egg-info/dependency_links.txt
10
+ src/langgraph_velesdb.egg-info/requires.txt
11
+ src/langgraph_velesdb.egg-info/top_level.txt
12
+ tests/test_tools.py
@@ -0,0 +1,6 @@
1
+ langchain-core<2.0.0,>=0.3.0
2
+ velesdb>=3.12.0
3
+
4
+ [dev]
5
+ pytest<9.0,>=7.0
6
+ langgraph>=0.2.0
@@ -0,0 +1 @@
1
+ langgraph_velesdb
@@ -0,0 +1,248 @@
1
+ """Offline tests for the LangGraph memory tools — no LLM, deterministic."""
2
+
3
+ import tempfile
4
+
5
+ from langgraph_velesdb import make_memory_tools
6
+
7
+
8
+ def _tools_by_name():
9
+ return {t.name: t for t in make_memory_tools(tempfile.mkdtemp())}
10
+
11
+
12
+ class _Pre0_11Service:
13
+ """Stands in for a ``MemoryService`` built against a velesdb wheel
14
+ published before ``feedback``/``save_working_context``/
15
+ ``load_working_context`` existed (e.g. the current PyPI 3.12.0) — same
16
+ shape as the real binding, minus those three methods.
17
+ """
18
+
19
+ def __init__(self):
20
+ self._facts = {}
21
+ self._next_id = 1
22
+
23
+ def remember(self, fact, links=None, metadata=None, ttl_seconds=None):
24
+ fid = self._next_id
25
+ self._next_id += 1
26
+ self._facts[fid] = fact
27
+ return fid
28
+
29
+ def recall(self, query, k):
30
+ return [{"id": i, "score": 1.0, "content": f, "metadata": None} for i, f in self._facts.items()]
31
+
32
+ def recall_where(self, query, filters, k):
33
+ return self.recall(query, k)
34
+
35
+ def recall_fused(self, query, k, filter=None, *, date_field=None, options=None):
36
+ return self.recall(query, k)
37
+
38
+ def relate(self, from_id, to_id, relation):
39
+ return 1
40
+
41
+ def forget(self, id):
42
+ return self._facts.pop(id, None) is not None
43
+
44
+ def why(self, question, max_hops):
45
+ return {"nodes": [], "edges": []}
46
+
47
+ # feedback / save_working_context / load_working_context intentionally
48
+ # absent, matching the real gap on PyPI 3.12.0.
49
+
50
+
51
+ def test_exposes_the_full_memory_tool_set():
52
+ assert set(_tools_by_name()) == {
53
+ "remember",
54
+ "recall",
55
+ "recall_where",
56
+ "recall_fused",
57
+ "relate",
58
+ "forget",
59
+ "feedback",
60
+ "why",
61
+ "save_working_context",
62
+ "load_working_context",
63
+ }
64
+
65
+
66
+ def test_why_reaches_connected_context_that_recall_misses():
67
+ tools = _tools_by_name()
68
+
69
+ # The booking is the only fact that shares words with the question, so it is
70
+ # recall's clear top hit; the reason shares none and stays out of the top-1.
71
+ reason = tools["remember"].invoke(
72
+ {"fact": "He is recovering from knee surgery and needs to stretch his leg"}
73
+ )
74
+ booking = tools["remember"].invoke(
75
+ {"fact": "Booked the aisle seat on Robert's flight"}
76
+ )
77
+ tools["relate"].invoke({"from_id": booking, "to_id": reason, "relation": "because"})
78
+
79
+ question = "why the aisle seat on Robert's flight?"
80
+ recalled = {hit["content"] for hit in tools["recall"].invoke({"query": question, "k": 1})}
81
+ assert not any("knee surgery" in c for c in recalled)
82
+
83
+ reached = {node["content"] for node in tools["why"].invoke({"question": question})["nodes"]}
84
+ assert any("knee surgery" in c for c in reached)
85
+
86
+
87
+ def test_requires_path_or_service():
88
+ try:
89
+ make_memory_tools()
90
+ except ValueError:
91
+ return
92
+ raise AssertionError("expected ValueError when neither path nor service is given")
93
+
94
+
95
+ def test_forget_removes_a_memory_and_reports_whether_it_existed():
96
+ tools = _tools_by_name()
97
+
98
+ fid = tools["remember"].invoke({"fact": "The deploy key rotates every 90 days"})
99
+
100
+ assert tools["forget"].invoke({"memory_id": fid}) is True
101
+ # Second delete of the same id is a no-op, not an error.
102
+ assert tools["forget"].invoke({"memory_id": fid}) is False
103
+
104
+
105
+ def test_feedback_moves_confidence_after_use():
106
+ tools = _tools_by_name()
107
+
108
+ fid = tools["remember"].invoke({"fact": "Staging uses the blue database"})
109
+
110
+ reinforced = tools["feedback"].invoke({"memory_id": fid, "success": True})
111
+ weakened = tools["feedback"].invoke({"memory_id": fid, "success": False})
112
+
113
+ assert 0.0 <= weakened < reinforced <= 1.0
114
+
115
+
116
+ def test_remember_accepts_metadata_links_and_ttl_without_changing_defaults():
117
+ tools = _tools_by_name()
118
+
119
+ # Bare call keeps working exactly as before (defaults unchanged).
120
+ plain_id = tools["remember"].invoke({"fact": "The mascot is a red panda"})
121
+ assert isinstance(plain_id, int)
122
+
123
+ pr = tools["remember"].invoke({"fact": "PR #42 swaps the mutex for parking_lot"})
124
+ decision = tools["remember"].invoke(
125
+ {
126
+ "fact": "We chose parking_lot to avoid lock poisoning",
127
+ "links": [[pr, "decided_in"]],
128
+ "metadata": {"topic": "concurrency"},
129
+ "ttl_seconds": 3600,
130
+ }
131
+ )
132
+
133
+ hits = tools["recall_where"].invoke(
134
+ {"query": "parking_lot", "filters": [["topic", "eq", "concurrency"]]}
135
+ )
136
+ assert any(h["id"] == decision for h in hits)
137
+
138
+
139
+ def test_recall_where_surfaces_metadata_including_the_auto_date_stamp():
140
+ tools = _tools_by_name()
141
+
142
+ tools["remember"].invoke({"fact": "Rotated the signing key"})
143
+
144
+ hits = tools["recall_where"].invoke({"query": "signing key", "filters": []})
145
+
146
+ assert hits
147
+ assert "_veles_date" in hits[0]["metadata"]
148
+
149
+
150
+ def test_recall_fused_with_date_field_returns_a_dated_timeline():
151
+ tools = _tools_by_name()
152
+
153
+ tools["remember"].invoke({"fact": "Rotated the signing key today"})
154
+
155
+ result = tools["recall_fused"].invoke(
156
+ {"query": "signing key", "date_field": "_veles_date"}
157
+ )
158
+
159
+ assert "dated_context" in result
160
+ assert "memories" in result
161
+
162
+
163
+ def test_recall_fused_without_date_field_returns_a_plain_list():
164
+ tools = _tools_by_name()
165
+
166
+ tools["remember"].invoke({"fact": "Rotated the signing key today"})
167
+
168
+ result = tools["recall_fused"].invoke({"query": "signing key"})
169
+
170
+ assert isinstance(result, list)
171
+
172
+
173
+ def test_save_and_load_working_context_round_trips():
174
+ tools = _tools_by_name()
175
+
176
+ working = {"goal": "ship issue 1546", "pending_actions": ["open the PR"]}
177
+ tools["save_working_context"].invoke(
178
+ {"project": "veles", "session": "session-1", "working": working}
179
+ )
180
+
181
+ loaded = tools["load_working_context"].invoke(
182
+ {"project": "veles", "session": "session-1"}
183
+ )
184
+ assert loaded["goal"] == "ship issue 1546"
185
+
186
+
187
+ def test_load_working_context_returns_none_when_nothing_was_saved():
188
+ tools = _tools_by_name()
189
+
190
+ loaded = tools["load_working_context"].invoke(
191
+ {"project": "veles", "session": "no-such-session"}
192
+ )
193
+ assert loaded is None
194
+
195
+
196
+ def _tools_on_pre_0_11_binding():
197
+ return {t.name: t for t in make_memory_tools(service=_Pre0_11Service())}
198
+
199
+
200
+ def test_make_memory_tools_construction_succeeds_even_without_new_binding_methods():
201
+ # The guard is a call-time check, not a construction-time one: building
202
+ # the tool list must not itself require feedback/save_working_context/
203
+ # load_working_context to exist on the service.
204
+ tools = _tools_on_pre_0_11_binding()
205
+ assert set(tools) == {
206
+ "remember",
207
+ "recall",
208
+ "recall_where",
209
+ "recall_fused",
210
+ "relate",
211
+ "forget",
212
+ "feedback",
213
+ "why",
214
+ "save_working_context",
215
+ "load_working_context",
216
+ }
217
+
218
+
219
+ def test_feedback_returns_actionable_error_instead_of_raising_when_unsupported():
220
+ tools = _tools_on_pre_0_11_binding()
221
+
222
+ result = tools["feedback"].invoke({"memory_id": 1, "success": True})
223
+
224
+ assert result == {
225
+ "error": "feedback requires velesdb > 3.12.0 — upgrade with `pip install -U velesdb`"
226
+ }
227
+
228
+
229
+ def test_save_working_context_returns_actionable_error_instead_of_raising_when_unsupported():
230
+ tools = _tools_on_pre_0_11_binding()
231
+
232
+ result = tools["save_working_context"].invoke(
233
+ {"project": "veles", "session": "s1", "working": {"goal": "x"}}
234
+ )
235
+
236
+ assert result == {
237
+ "error": "save_working_context requires velesdb > 3.12.0 — upgrade with `pip install -U velesdb`"
238
+ }
239
+
240
+
241
+ def test_load_working_context_returns_actionable_error_instead_of_raising_when_unsupported():
242
+ tools = _tools_on_pre_0_11_binding()
243
+
244
+ result = tools["load_working_context"].invoke({"project": "veles", "session": "s1"})
245
+
246
+ assert result == {
247
+ "error": "load_working_context requires velesdb > 3.12.0 — upgrade with `pip install -U velesdb`"
248
+ }