dory-memory 0.3.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.
- dory_memory-0.3.0/LICENSE +170 -0
- dory_memory-0.3.0/PKG-INFO +443 -0
- dory_memory-0.3.0/README.md +396 -0
- dory_memory-0.3.0/dory/__init__.py +13 -0
- dory_memory-0.3.0/dory/activation.py +213 -0
- dory_memory-0.3.0/dory/adapters/__init__.py +7 -0
- dory_memory-0.3.0/dory/adapters/langchain.py +156 -0
- dory_memory-0.3.0/dory/adapters/langgraph.py +174 -0
- dory_memory-0.3.0/dory/adapters/multi_agent.py +171 -0
- dory_memory-0.3.0/dory/consolidation.py +98 -0
- dory_memory-0.3.0/dory/export/__init__.py +5 -0
- dory_memory-0.3.0/dory/export/jsonld.py +244 -0
- dory_memory-0.3.0/dory/graph.py +177 -0
- dory_memory-0.3.0/dory/mcp_server.py +154 -0
- dory_memory-0.3.0/dory/memory.py +223 -0
- dory_memory-0.3.0/dory/pipeline/__init__.py +7 -0
- dory_memory-0.3.0/dory/pipeline/decayer.py +187 -0
- dory_memory-0.3.0/dory/pipeline/observer.py +375 -0
- dory_memory-0.3.0/dory/pipeline/prefixer.py +377 -0
- dory_memory-0.3.0/dory/pipeline/reflector.py +440 -0
- dory_memory-0.3.0/dory/pipeline/summarizer.py +549 -0
- dory_memory-0.3.0/dory/schema.py +143 -0
- dory_memory-0.3.0/dory/session.py +403 -0
- dory_memory-0.3.0/dory/store.py +248 -0
- dory_memory-0.3.0/dory/vector.py +112 -0
- dory_memory-0.3.0/dory/visualize.py +1005 -0
- dory_memory-0.3.0/dory_cli.py +187 -0
- dory_memory-0.3.0/dory_mcp.py +60 -0
- dory_memory-0.3.0/dory_memory.egg-info/PKG-INFO +443 -0
- dory_memory-0.3.0/dory_memory.egg-info/SOURCES.txt +47 -0
- dory_memory-0.3.0/dory_memory.egg-info/dependency_links.txt +1 -0
- dory_memory-0.3.0/dory_memory.egg-info/entry_points.txt +3 -0
- dory_memory-0.3.0/dory_memory.egg-info/requires.txt +34 -0
- dory_memory-0.3.0/dory_memory.egg-info/top_level.txt +3 -0
- dory_memory-0.3.0/pyproject.toml +51 -0
- dory_memory-0.3.0/setup.cfg +4 -0
- dory_memory-0.3.0/tests/test_activation.py +162 -0
- dory_memory-0.3.0/tests/test_consolidation.py +224 -0
- dory_memory-0.3.0/tests/test_decayer.py +149 -0
- dory_memory-0.3.0/tests/test_graph.py +230 -0
- dory_memory-0.3.0/tests/test_integration_live.py +165 -0
- dory_memory-0.3.0/tests/test_mcp.py +91 -0
- dory_memory-0.3.0/tests/test_observer.py +237 -0
- dory_memory-0.3.0/tests/test_prefixer.py +181 -0
- dory_memory-0.3.0/tests/test_reflector.py +208 -0
- dory_memory-0.3.0/tests/test_schema.py +169 -0
- dory_memory-0.3.0/tests/test_session.py +72 -0
- dory_memory-0.3.0/tests/test_store.py +176 -0
- dory_memory-0.3.0/tests/test_visualize.py +83 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship made available under
|
|
36
|
+
the License, as indicated by a copyright notice that is included in
|
|
37
|
+
or attached to the work (an example is provided in the Appendix below).
|
|
38
|
+
|
|
39
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
40
|
+
form, that is based on (or derived from) the Work and for which the
|
|
41
|
+
editorial revisions, annotations, elaborations, or other transformations
|
|
42
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
43
|
+
of this License, Derivative Works shall not include works that remain
|
|
44
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
45
|
+
the Work and Derivative Works thereof.
|
|
46
|
+
|
|
47
|
+
"Contribution" shall mean, as submitted to the Licensor for inclusion
|
|
48
|
+
in the Work by the copyright owner or by an individual or Legal Entity
|
|
49
|
+
authorized to submit on behalf of the copyright owner. For the purposes
|
|
50
|
+
of this definition, "submitted" means any form of electronic, verbal,
|
|
51
|
+
or written communication sent to the Licensor or its representatives,
|
|
52
|
+
including but not limited to communication on electronic mailing lists,
|
|
53
|
+
source code control systems, and issue tracking systems that are managed
|
|
54
|
+
by, or on behalf of, the Licensor for the purpose of developing and
|
|
55
|
+
improving the Work, but excluding communication that is conspicuously
|
|
56
|
+
marked or designated in writing by the copyright owner as "Not a
|
|
57
|
+
Contribution."
|
|
58
|
+
|
|
59
|
+
"Contributor" shall mean Licensor and any Legal Entity on behalf of
|
|
60
|
+
whom a Contribution has been received by the Licensor and included
|
|
61
|
+
within the Work.
|
|
62
|
+
|
|
63
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
64
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
65
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
66
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
67
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
68
|
+
Work and such Derivative Works in Source or Object form.
|
|
69
|
+
|
|
70
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
71
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
72
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
73
|
+
(except as stated in this section) patent license to make, have made,
|
|
74
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
75
|
+
where such license applies only to those patent claims licensable
|
|
76
|
+
by such Contributor that are necessarily infringed by their
|
|
77
|
+
Contribution(s) alone or by the combination of their Contribution(s)
|
|
78
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
79
|
+
institute patent litigation against any entity (including a cross-claim
|
|
80
|
+
or counterclaim in a lawsuit) alleging that the Work or any other
|
|
81
|
+
Contribution embodied in the Work constitutes patent or contributory
|
|
82
|
+
patent infringement, then any patent licenses granted to You under
|
|
83
|
+
this License for that Work shall terminate as of the date such
|
|
84
|
+
litigation is filed.
|
|
85
|
+
|
|
86
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
87
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
88
|
+
modifications, and in Source or Object form, provided that You
|
|
89
|
+
meet the following conditions:
|
|
90
|
+
|
|
91
|
+
(a) You must give any other recipients of the Work or Derivative
|
|
92
|
+
Works a copy of this License; and
|
|
93
|
+
|
|
94
|
+
(b) You must cause any modified files to carry prominent notices
|
|
95
|
+
stating that You changed the files; and
|
|
96
|
+
|
|
97
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
98
|
+
that You distribute, all copyright, patent, trademark, and
|
|
99
|
+
attribution notices from the Source form of the Work,
|
|
100
|
+
excluding those notices that do not pertain to any part of
|
|
101
|
+
the Derivative Works; and
|
|
102
|
+
|
|
103
|
+
(d) If the Work includes a "NOTICE" text file, as part of its
|
|
104
|
+
distribution, You must include a readable copy of the
|
|
105
|
+
attribution notices contained within such NOTICE file, in
|
|
106
|
+
at least one of the following places: within a NOTICE text
|
|
107
|
+
file distributed as part of the Derivative Works; within
|
|
108
|
+
the Source form or documentation, if provided along with the
|
|
109
|
+
Derivative Works; or, within a display generated by the
|
|
110
|
+
Derivative Works, if and wherever such third-party notices
|
|
111
|
+
normally appear. The contents of the NOTICE file are for
|
|
112
|
+
informational purposes only and do not modify the License.
|
|
113
|
+
You may add Your own attribution notices within Derivative
|
|
114
|
+
Works that You distribute, alongside or in addition to the
|
|
115
|
+
NOTICE text from the Work, provided that such additional
|
|
116
|
+
attribution notices cannot be construed as modifying the License.
|
|
117
|
+
|
|
118
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
119
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
120
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
121
|
+
this License, without any additional terms or conditions.
|
|
122
|
+
|
|
123
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
124
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
125
|
+
except as required for reasonable and customary use in describing the
|
|
126
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
127
|
+
|
|
128
|
+
7. Disclaimer of Warranty. Unless required by applicable law or agreed
|
|
129
|
+
to in writing, Licensor provides the Work (and each Contributor
|
|
130
|
+
provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES
|
|
131
|
+
OR CONDITIONS OF ANY KIND, either express or implied, including,
|
|
132
|
+
without limitation, any warranties or conditions of TITLE,
|
|
133
|
+
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR
|
|
134
|
+
PURPOSE. You are solely responsible for determining the
|
|
135
|
+
appropriateness of using or reproducing the Work and assume any
|
|
136
|
+
risks associated with Your exercise of permissions under this License.
|
|
137
|
+
|
|
138
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
139
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
140
|
+
unless required by applicable law (such as deliberate and grossly
|
|
141
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
142
|
+
liable to You for damages, including any direct, indirect, special,
|
|
143
|
+
incidental, or exemplary damages of any character arising as a
|
|
144
|
+
result of this License or out of the use or inability to use the
|
|
145
|
+
Work (even if such Contributor has been advised of the possibility
|
|
146
|
+
of such damages).
|
|
147
|
+
|
|
148
|
+
9. Accepting Warranty or Liability. While redistributing the Work or
|
|
149
|
+
Derivative Works thereof, You may choose to offer, and charge a fee
|
|
150
|
+
for, acceptance of support, warranty, indemnity, or other liability
|
|
151
|
+
obligations and/or rights consistent by this License. However, in
|
|
152
|
+
accepting such obligations, You may offer only obligations consistent
|
|
153
|
+
with this License, and only if You can do so without additional
|
|
154
|
+
conditions that would restrict or modify the terms of this License.
|
|
155
|
+
|
|
156
|
+
END OF TERMS AND CONDITIONS
|
|
157
|
+
|
|
158
|
+
Copyright 2026 Michael Martin
|
|
159
|
+
|
|
160
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
161
|
+
you may not use this file except in compliance with the License.
|
|
162
|
+
You may obtain a copy of the License at
|
|
163
|
+
|
|
164
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
165
|
+
|
|
166
|
+
Unless required by applicable law or agreed to in writing, software
|
|
167
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
168
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
169
|
+
See the License for the specific language governing permissions and
|
|
170
|
+
limitations under the License.
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dory-memory
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Agent memory that actually sticks. Local-first, graph-based, with principled forgetting.
|
|
5
|
+
Author: Michael Martin
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/MichaelWMartinII/Dory
|
|
8
|
+
Project-URL: Repository, https://github.com/MichaelWMartinII/Dory
|
|
9
|
+
Project-URL: Issues, https://github.com/MichaelWMartinII/Dory/issues
|
|
10
|
+
Keywords: agent,memory,llm,ai,graph,local,rag
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Provides-Extra: ollama
|
|
22
|
+
Requires-Dist: ollama>=0.6.0; extra == "ollama"
|
|
23
|
+
Provides-Extra: vector
|
|
24
|
+
Requires-Dist: sqlite-vec>=0.1.6; extra == "vector"
|
|
25
|
+
Provides-Extra: anthropic
|
|
26
|
+
Requires-Dist: anthropic>=0.40.0; extra == "anthropic"
|
|
27
|
+
Provides-Extra: openai
|
|
28
|
+
Requires-Dist: httpx>=0.27.0; extra == "openai"
|
|
29
|
+
Provides-Extra: mcp
|
|
30
|
+
Requires-Dist: mcp>=1.0.0; extra == "mcp"
|
|
31
|
+
Provides-Extra: langchain
|
|
32
|
+
Requires-Dist: langchain>=0.2.0; extra == "langchain"
|
|
33
|
+
Provides-Extra: langgraph
|
|
34
|
+
Requires-Dist: langgraph>=0.2.0; extra == "langgraph"
|
|
35
|
+
Provides-Extra: full
|
|
36
|
+
Requires-Dist: ollama>=0.6.0; extra == "full"
|
|
37
|
+
Requires-Dist: sqlite-vec>=0.1.6; extra == "full"
|
|
38
|
+
Requires-Dist: anthropic>=0.40.0; extra == "full"
|
|
39
|
+
Requires-Dist: httpx>=0.27.0; extra == "full"
|
|
40
|
+
Requires-Dist: mcp>=1.0.0; extra == "full"
|
|
41
|
+
Requires-Dist: langchain>=0.2.0; extra == "full"
|
|
42
|
+
Requires-Dist: langgraph>=0.2.0; extra == "full"
|
|
43
|
+
Provides-Extra: dev
|
|
44
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
45
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
46
|
+
Dynamic: license-file
|
|
47
|
+
|
|
48
|
+
# Dory
|
|
49
|
+
|
|
50
|
+
**Agent memory that actually sticks.**
|
|
51
|
+
|
|
52
|
+
Named after the fish with no short-term memory — because that's your AI agent right now.
|
|
53
|
+
|
|
54
|
+
Dory is the best Python-native, local-first agent memory library. Drop it into any LLM pipeline and your agent stops forgetting between sessions.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
pip install dory-memory
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## The problem
|
|
63
|
+
|
|
64
|
+
Every time you start a new session, your agent starts from zero. Even systems that claim to "remember" you are doing keyword search through a flat list of notes. That's not memory — that's ctrl+F.
|
|
65
|
+
|
|
66
|
+
The deeper problem: naive memory injection makes things *worse*. Dumping everything into context creates noise that degrades model performance. Research ([Chroma, 2025](https://research.trychroma.com/context-rot)) shows all major frontier models degrade starting at 500–750 tokens of context.
|
|
67
|
+
|
|
68
|
+
## What Dory does differently
|
|
69
|
+
|
|
70
|
+
**Four memory types, all in one place**
|
|
71
|
+
|
|
72
|
+
| Type | What it stores | Status |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| Episodic | Past events, sessions, experiences | ✓ |
|
|
75
|
+
| Semantic | Facts, preferences, entities, relationships | ✓ |
|
|
76
|
+
| Procedural | Skills, workflows, repeatable processes | ✓ |
|
|
77
|
+
| Working | In-context window (managed by your LLM) | — |
|
|
78
|
+
|
|
79
|
+
**Spreading activation retrieval** — not vector similarity search. Relevant memories pull in connected memories through the graph. "AllergyFind" activates "Giovanni's" activates "FastAPI" activates "menu endpoint" because those things co-occurred. That's how human memory works.
|
|
80
|
+
|
|
81
|
+
**Cacheable prefix output** — instead of regenerating your full memory context every turn (which blows prompt caching), Dory splits output into a *stable prefix* (same until memory actually changes) and a *dynamic suffix* (query-specific). Result: cache hits every turn. 4–10x cheaper to run agents with memory than without.
|
|
82
|
+
|
|
83
|
+
**Principled forgetting** — three decay zones: active, archived, expired. Scores based on recency + frequency + relevance. Nothing is ever deleted — archived memories are queryable for historical context. No other production memory library ships this.
|
|
84
|
+
|
|
85
|
+
**Bi-temporal conflict resolution** — when a fact changes, the old version is archived with a `SUPERSEDES` edge and a timestamp. You can query "what was true in January" and get the right answer.
|
|
86
|
+
|
|
87
|
+
**Zero-server stack** — everything runs in a single SQLite file. `sqlite-vec` for vectors, FTS5 for keyword search, adjacency tables for the graph. No Postgres, no Neo4j, no Redis. Works offline.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Quick start
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from dory import DoryMemory
|
|
95
|
+
|
|
96
|
+
# Works with any model — local or cloud
|
|
97
|
+
mem = DoryMemory() # manual observations only
|
|
98
|
+
mem = DoryMemory(extract_model="qwen3:14b") # local via Ollama
|
|
99
|
+
mem = DoryMemory( # Claude
|
|
100
|
+
extract_model="claude-haiku-4-5-20251001",
|
|
101
|
+
extract_backend="anthropic",
|
|
102
|
+
extract_api_key="sk-ant-...",
|
|
103
|
+
)
|
|
104
|
+
mem = DoryMemory( # GPT / Grok / any compat
|
|
105
|
+
extract_model="gpt-4o-mini",
|
|
106
|
+
extract_backend="openai",
|
|
107
|
+
extract_api_key="sk-...",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# --- Query context at session start ---
|
|
111
|
+
context = mem.query("menu endpoint authentication") # inject into system prompt
|
|
112
|
+
|
|
113
|
+
# --- Or build API-ready messages with prompt caching ---
|
|
114
|
+
result = mem.build_context("menu endpoint authentication")
|
|
115
|
+
messages = result.as_anthropic_messages(user_query) # Anthropic SDK w/ cache_control
|
|
116
|
+
messages = result.as_openai_messages(user_query) # OpenAI / compat
|
|
117
|
+
|
|
118
|
+
# --- Log turns during the session ---
|
|
119
|
+
mem.add_turn("user", "I'm working on AllergyFind today, need to add a menu endpoint")
|
|
120
|
+
mem.add_turn("assistant", "What authentication approach are you using?")
|
|
121
|
+
|
|
122
|
+
# --- Or add memories manually ---
|
|
123
|
+
mem.observe("User prefers JWT for API auth", node_type="PREFERENCE")
|
|
124
|
+
|
|
125
|
+
# --- End of session: extract, consolidate, decay ---
|
|
126
|
+
stats = mem.flush()
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### MCP server (Claude Code / Claude Desktop)
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
pip install 'dory-memory[mcp]'
|
|
133
|
+
|
|
134
|
+
# Register globally across all Claude Code projects
|
|
135
|
+
claude mcp add --scope user dory -- dory-mcp
|
|
136
|
+
|
|
137
|
+
# Or with a specific DB path
|
|
138
|
+
claude mcp add --scope user dory -- dory-mcp --db /path/to/engram.db
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Five tools are exposed: `dory_query`, `dory_observe`, `dory_consolidate`, `dory_visualize`, `dory_stats`.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Interactive demo
|
|
146
|
+
|
|
147
|
+
**[Live graph visualization →](https://michaelwmartinii.github.io/Dory/demo.html)**
|
|
148
|
+
|
|
149
|
+
Force-directed knowledge graph with spreading activation query mode, edge type coloring, archived/superseded nodes, and session summary chain. Click any of the pre-set queries to see retrieval in action.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
### Framework adapters
|
|
154
|
+
|
|
155
|
+
**LangChain** — drop-in `BaseMemory` replacement:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from dory.adapters.langchain import DoryMemoryAdapter
|
|
159
|
+
from langchain.chains import ConversationChain
|
|
160
|
+
from langchain_anthropic import ChatAnthropic
|
|
161
|
+
|
|
162
|
+
memory = DoryMemoryAdapter(
|
|
163
|
+
extract_model="claude-haiku-4-5-20251001",
|
|
164
|
+
extract_backend="anthropic",
|
|
165
|
+
extract_api_key="sk-ant-...",
|
|
166
|
+
)
|
|
167
|
+
chain = ConversationChain(llm=ChatAnthropic(model="claude-sonnet-4-6"), memory=memory)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**LangGraph** — graph nodes with the `(state) -> state` signature:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from dory.adapters.langgraph import DoryMemoryNode, MemoryState
|
|
174
|
+
from langgraph.graph import StateGraph, START, END
|
|
175
|
+
|
|
176
|
+
mem = DoryMemoryNode(extract_model="claude-haiku-4-5-20251001", extract_backend="anthropic")
|
|
177
|
+
|
|
178
|
+
builder = StateGraph(MemoryState)
|
|
179
|
+
builder.add_node("load_memory", mem.load_context) # or mem.aload_context for async
|
|
180
|
+
builder.add_node("record_turn", mem.record_turn)
|
|
181
|
+
builder.add_edge(START, "load_memory")
|
|
182
|
+
builder.add_edge("load_memory", "record_turn")
|
|
183
|
+
builder.add_edge("record_turn", END)
|
|
184
|
+
graph = builder.compile()
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Multi-agent** — shared memory pool with thread-safe writes and agent attribution:
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from dory.adapters.multi_agent import SharedMemoryPool
|
|
191
|
+
|
|
192
|
+
pool = SharedMemoryPool(db_path="shared.db")
|
|
193
|
+
pool.observe("User prefers dark mode", agent_id="agent-1")
|
|
194
|
+
pool.add_turn("user", "Let's ship it", agent_id="agent-2", session_id="s1")
|
|
195
|
+
results = pool.query("UI preferences")
|
|
196
|
+
agent_nodes = pool.get_agent_nodes("agent-1")
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Async API
|
|
200
|
+
|
|
201
|
+
All `DoryMemory` methods have async counterparts — safe to await from FastAPI, LangGraph, and any async framework:
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
context = await mem.aquery("current topic")
|
|
205
|
+
result = await mem.abuild_context("current topic")
|
|
206
|
+
await mem.aadd_turn("user", "message")
|
|
207
|
+
node_id = await mem.aobserve("User prefers JWT", node_type="PREFERENCE")
|
|
208
|
+
stats = await mem.aflush()
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Export / import
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
from dory.export.jsonld import JSONLDExporter
|
|
215
|
+
|
|
216
|
+
exporter = JSONLDExporter(graph)
|
|
217
|
+
exporter.export("memory.jsonld.json") # write to file
|
|
218
|
+
data = exporter.export() # or get dict
|
|
219
|
+
|
|
220
|
+
JSONLDExporter.import_into(graph, "memory.jsonld.json") # round-trip import
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Advanced: direct pipeline access
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
from dory import Graph, Observer, Prefixer
|
|
227
|
+
|
|
228
|
+
graph = Graph("myapp.db")
|
|
229
|
+
obs = Observer(graph, backend="ollama", model="qwen3:14b")
|
|
230
|
+
p = Prefixer(graph)
|
|
231
|
+
# ... same as DoryMemory but with full control
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## How it works
|
|
237
|
+
|
|
238
|
+
### Knowledge graph
|
|
239
|
+
|
|
240
|
+
Every piece of information is a node. Nodes have types: `ENTITY`, `CONCEPT`, `EVENT`, `PREFERENCE`, `BELIEF`, `PROCEDURE`, `SESSION` (episodic narrative), `SESSION_SUMMARY` (structured episodic with `salient_counts`). Edges between them are typed and weighted: `USES`, `WORKS_ON`, `PREFERS`, `SUPERSEDES`, `CO_OCCURS`, `SUPPORTS_FACT`, `TEMPORALLY_AFTER`, etc.
|
|
241
|
+
|
|
242
|
+
Salience is computed, not assigned:
|
|
243
|
+
```
|
|
244
|
+
salience = α × connectivity + β × activation_frequency + γ × recency
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
High-salience nodes become **core memories** — they anchor the stable context prefix.
|
|
248
|
+
|
|
249
|
+
### Observer
|
|
250
|
+
|
|
251
|
+
Every N conversation turns (configurable), the Observer calls a local LLM to extract structured memories from the raw conversation. Extractions have confidence scores — anything below the threshold is logged but not written to the graph, guarding against false memory.
|
|
252
|
+
|
|
253
|
+
Backends: Ollama (default), Anthropic (Claude), or any OpenAI-compatible endpoint (llama.cpp, Clanker, vLLM, GPT, Grok, etc.).
|
|
254
|
+
|
|
255
|
+
### Prefixer
|
|
256
|
+
|
|
257
|
+
Builds context in two parts:
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
[stable prefix] ← core memories + key relationships
|
|
261
|
+
same bytes across turns → prompt cache hits
|
|
262
|
+
|
|
263
|
+
[dynamic suffix] ← spreading activation for this specific query
|
|
264
|
+
+ recent episodic observations
|
|
265
|
+
changes per query but small
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Decayer
|
|
269
|
+
|
|
270
|
+
Runs periodically to score every node:
|
|
271
|
+
```
|
|
272
|
+
score = recency_weight × exp(-λ × days_since_activation)
|
|
273
|
+
+ frequency_weight × log(1 + activation_count)
|
|
274
|
+
+ relevance_weight × salience
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Nodes below the active floor → archived. Below the archive floor → expired. Core memories are shielded with a configurable multiplier.
|
|
278
|
+
|
|
279
|
+
### Reflector
|
|
280
|
+
|
|
281
|
+
Finds near-duplicate nodes (Jaccard similarity ≥ 0.82, empirically tuned), merges them keeping the higher-salience one. Detects supersession — same subject, newer fact, Jaccard in [0.45, 0.82) — archives the old node, and adds a `SUPERSEDES` provenance edge. Old observations are compressed into summaries. Dedup thresholds are practical defaults chosen conservatively; sensitivity analysis is planned.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Architecture
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
dory/
|
|
289
|
+
├── graph.py ← nodes, edges, salience computation
|
|
290
|
+
├── schema.py ← NodeType, EdgeType, zone constants
|
|
291
|
+
├── activation.py ← spreading activation engine
|
|
292
|
+
├── consolidation.py ← edge decay, strengthen, prune, promote/demote core
|
|
293
|
+
├── session.py ← session-level helpers: query, observe, write_turn, end_session
|
|
294
|
+
├── memory.py ← DoryMemory — the high-level drop-in API (sync + async)
|
|
295
|
+
├── visualize.py ← D3.js interactive graph visualization
|
|
296
|
+
├── mcp_server.py ← MCP tools (dory_query, dory_observe, dory_consolidate, …)
|
|
297
|
+
├── store.py ← SQLite backend (nodes, edges, FTS5, observations)
|
|
298
|
+
│
|
|
299
|
+
├── pipeline/
|
|
300
|
+
│ ├── observer.py ← LLM extraction of memories from conversation turns
|
|
301
|
+
│ ├── summarizer.py ← episodic layer: SESSION nodes from conversation turns
|
|
302
|
+
│ ├── prefixer.py ← stable prefix + dynamic suffix builder
|
|
303
|
+
│ ├── decayer.py ← node decay scoring + zone management
|
|
304
|
+
│ └── reflector.py ← dedup, supersession, observation compression
|
|
305
|
+
│
|
|
306
|
+
├── adapters/
|
|
307
|
+
│ ├── langchain.py ← DoryMemoryAdapter — LangChain BaseMemory drop-in
|
|
308
|
+
│ ├── langgraph.py ← DoryMemoryNode — LangGraph StateGraph nodes
|
|
309
|
+
│ └── multi_agent.py ← SharedMemoryPool — thread-safe multi-agent memory
|
|
310
|
+
│
|
|
311
|
+
└── export/
|
|
312
|
+
└── jsonld.py ← JSONLDExporter — portable JSON-LD round-trip
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Local LLM setup
|
|
318
|
+
|
|
319
|
+
Dory defaults to Ollama for LLM-based extraction (Observer) and embedding (vector search).
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
# Pull the default models
|
|
323
|
+
ollama pull qwen3:14b # extraction
|
|
324
|
+
ollama pull nomic-embed-text # embeddings (768-dim, offline after pull)
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
OpenAI-compatible endpoint (Clanker, llama.cpp server, vLLM):
|
|
328
|
+
```python
|
|
329
|
+
obs = Observer(
|
|
330
|
+
graph,
|
|
331
|
+
backend="openai",
|
|
332
|
+
base_url="http://localhost:8000",
|
|
333
|
+
model="qwen3",
|
|
334
|
+
)
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Vector search activates automatically once `nomic-embed-text` is available. Falls back to FTS5 BM25 + substring search if no embedding model is running.
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Decay zones
|
|
342
|
+
|
|
343
|
+
| Zone | Behavior | How to query |
|
|
344
|
+
|---|---|---|
|
|
345
|
+
| `active` | Retrieved in all normal queries | `graph.all_nodes()` (default) |
|
|
346
|
+
| `archived` | Invisible to normal queries | `graph.all_nodes(zone="archived")` |
|
|
347
|
+
| `expired` | Completely invisible | `graph.all_nodes(zone=None)` |
|
|
348
|
+
|
|
349
|
+
User-meaningful memory is never deleted by forgetting — archived and expired nodes retain full provenance and can be restored if reactivated. The one exception: exact structural duplicates detected by the Reflector are hard-merged (the lower-salience copy is removed, all its edges are rewired to the winner).
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## What's different from other memory libraries
|
|
354
|
+
|
|
355
|
+
| | mem0 | Zep | Letta | Mastra | **Dory** |
|
|
356
|
+
|---|---|---|---|---|---|
|
|
357
|
+
| Principled forgetting | ✗ | ✗ | ✗ | ✗ | ✓ |
|
|
358
|
+
| Spreading activation retrieval | ✗ | ✗ | ✗ | ✗ | ✓ |
|
|
359
|
+
| Cacheable prefix output | ✗ | ✗ | ✗ | ✓ (TS only) | ✓ |
|
|
360
|
+
| Bi-temporal conflict resolution | ✗ | ✓ | ✗ | ✗ | ✓ |
|
|
361
|
+
| Zero-server local stack | partial | ✗ | partial | ✗ | ✓ |
|
|
362
|
+
| Drop-in Python library | ✓ | partial | ✗ | ✗ | ✓ |
|
|
363
|
+
| Apache 2.0 | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## Roadmap
|
|
368
|
+
|
|
369
|
+
**Shipped (v0.1)**
|
|
370
|
+
- [x] MCP server — expose Dory memory as MCP tools for Claude Code / Claude Desktop
|
|
371
|
+
- [x] LangChain adapter — `dory.adapters.langchain.DoryMemoryAdapter` implements `BaseMemory`
|
|
372
|
+
- [x] LangGraph adapter — `dory.adapters.langgraph.DoryMemoryNode` for StateGraph integration
|
|
373
|
+
- [x] Procedural memory — `PROCEDURE` node type for skills, workflows, and repeatable processes
|
|
374
|
+
- [x] Multi-agent shared memory — `dory.adapters.multi_agent.SharedMemoryPool` with thread-safe writes and agent attribution
|
|
375
|
+
- [x] Portable import/export format — `dory.export.jsonld.JSONLDExporter` for JSON-LD round-trips
|
|
376
|
+
|
|
377
|
+
**Shipped (v0.2)**
|
|
378
|
+
- [x] Episodic layer — `SESSION_SUMMARY` nodes with structured `salient_counts` metadata
|
|
379
|
+
- [x] Retrieval fusion — three-mode routing (graph / episodic / hybrid) via deterministic regex, no extra LLM calls
|
|
380
|
+
- [x] Staged retrieval — spreading activation → SUPPORTS_FACT traversal → SESSION_SUMMARY injection
|
|
381
|
+
- [x] Behavioral preference synthesis — `Reflector` detects repeated behavioral patterns across sessions and synthesizes PREFERENCE nodes without LLM calls
|
|
382
|
+
|
|
383
|
+
**Shipped (v0.3)**
|
|
384
|
+
- [x] Full 500-question LongMemEval run — 79.8% Sonnet/Sonnet (+13.0pp over v0.1)
|
|
385
|
+
- [x] Temporal arithmetic prompt — step-by-step date math before answering
|
|
386
|
+
- [x] Count cross-validation — `salient_counts` verified against EVENT nodes, low-confidence flagged
|
|
387
|
+
- [x] Behavioral preference synthesis — `Reflector` synthesizes PREFERENCE nodes from repeated patterns
|
|
388
|
+
|
|
389
|
+
**In progress (v0.4)**
|
|
390
|
+
- [ ] Preference inference — targeted improvement on single-session-preference (currently 46.7%)
|
|
391
|
+
- [ ] Graph topology demo — `demo_topology.py` showing provenance / evolution queries flat systems can't answer
|
|
392
|
+
- [ ] S-split benchmark — longer sessions (~115K tokens), better test of spreading activation value
|
|
393
|
+
- [ ] Production hardening — concurrent write safety, adversarial memory injection defense
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## Research basis
|
|
398
|
+
|
|
399
|
+
Dory draws from:
|
|
400
|
+
- [MemGPT: Towards LLMs as Operating Systems](https://arxiv.org/abs/2310.08560) — two-tier memory architecture
|
|
401
|
+
- [Zep: A Temporal Knowledge Graph Architecture](https://arxiv.org/abs/2501.13956) — bi-temporal provenance
|
|
402
|
+
- [MAGMA: Multi-Graph based Agentic Memory](https://arxiv.org/abs/2601.03236) — multi-graph retrieval
|
|
403
|
+
- [Mastra Observational Memory](https://mastra.ai/research/observational-memory) — cacheable prefix architecture (Python port)
|
|
404
|
+
- [LongMemEval](https://arxiv.org/abs/2410.10813) (ICLR 2025) — the benchmark we care about. Published scores: Mem0 68.4%, Zep 71.2%, Mastra 94.87%¹.
|
|
405
|
+
|
|
406
|
+
| Version | Extract | Answer | Questions | Score | Notes |
|
|
407
|
+
|---|---|---|---|---|---|
|
|
408
|
+
| v0.1 | Haiku | Haiku | 500 (full) | 54.4% | Baseline |
|
|
409
|
+
| v0.1 | Sonnet | Sonnet | 500 (full) | 66.8% | |
|
|
410
|
+
| v0.3 | Haiku | Haiku | 40 (spot check) | 67.5% | Episodic hybrid, spot check |
|
|
411
|
+
| **v0.3** | **Sonnet** | **Sonnet** | **500 (full)** | **79.8%** | **Episodic hybrid, full run** |
|
|
412
|
+
|
|
413
|
+
Category breakdown (v0.3 Sonnet, 500q):
|
|
414
|
+
|
|
415
|
+
| Category | v0.1 Sonnet | v0.3 Sonnet | Δ |
|
|
416
|
+
|---|---|---|---|
|
|
417
|
+
| temporal-reasoning | 46.6% | 75.9% | +29.3pp |
|
|
418
|
+
| knowledge-update | 75.6% | 84.6% | +9.0pp |
|
|
419
|
+
| multi-session | 70.7% | 80.5% | +9.8pp |
|
|
420
|
+
| single-session-assistant | 82.1% | 87.5% | +5.4pp |
|
|
421
|
+
| single-session-user | 85.7% | 88.6% | +2.9pp |
|
|
422
|
+
| single-session-preference | 43.3% | 46.7% | +3.3pp |
|
|
423
|
+
| **Overall** | **66.8%** | **79.8%** | **+13.0pp** |
|
|
424
|
+
|
|
425
|
+
¹ Mastra uses GPT-4o-mini (TypeScript). Dory uses Claude on Python. Architecturally
|
|
426
|
+
different stacks — not directly comparable. See [ablation study](benchmarks/ABLATION.md)
|
|
427
|
+
for component attribution.
|
|
428
|
+
|
|
429
|
+
**Disclaimer:** LongMemEval oracle split uses pre-filtered context (~15K tokens per question).
|
|
430
|
+
Production performance with live, noisy, unfiltered conversations will differ.
|
|
431
|
+
- Collins & Loftus (1975) — spreading activation in semantic memory
|
|
432
|
+
- Hebb (1949) — neurons that fire together wire together
|
|
433
|
+
- [Hopfield (1982) — Neural networks and physical systems with emergent collective computational abilities](https://www.pnas.org/doi/10.1073/pnas.79.8.2554) — statistical mechanics of associative memory; energy landscape formulation underlying spreading activation (Nobel Prize in Physics, 2024)
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
## License
|
|
438
|
+
|
|
439
|
+
Apache 2.0 — see [LICENSE](LICENSE).
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
*Named after Dory from Finding Nemo, because your AI agent right now is Dory. This fixes it.*
|