reeve 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.
- reeve-0.1.0/LICENSE +21 -0
- reeve-0.1.0/PKG-INFO +611 -0
- reeve-0.1.0/README.md +565 -0
- reeve-0.1.0/pyproject.toml +92 -0
- reeve-0.1.0/reeve/__init__.py +36 -0
- reeve-0.1.0/reeve/tools.py +26 -0
- reeve-0.1.0/reeve.egg-info/PKG-INFO +611 -0
- reeve-0.1.0/reeve.egg-info/SOURCES.txt +49 -0
- reeve-0.1.0/reeve.egg-info/dependency_links.txt +1 -0
- reeve-0.1.0/reeve.egg-info/entry_points.txt +5 -0
- reeve-0.1.0/reeve.egg-info/requires.txt +24 -0
- reeve-0.1.0/reeve.egg-info/top_level.txt +2 -0
- reeve-0.1.0/setup.cfg +4 -0
- reeve-0.1.0/tests/test_bedrock_llm.py +68 -0
- reeve-0.1.0/tests/test_cli.py +50 -0
- reeve-0.1.0/tests/test_config_bedrock.py +27 -0
- reeve-0.1.0/tests/test_evaluation.py +111 -0
- reeve-0.1.0/tests/test_ingestion_dataset.py +143 -0
- reeve-0.1.0/tests/test_operator.py +109 -0
- reeve-0.1.0/tests/test_reconciler_metadata.py +124 -0
- reeve-0.1.0/tests/test_retriever_temporal.py +54 -0
- reeve-0.1.0/tests/test_schemas.py +22 -0
- reeve-0.1.0/tests/test_utils.py +19 -0
- reeve-0.1.0/threelane_memory/__init__.py +102 -0
- reeve-0.1.0/threelane_memory/__main__.py +5 -0
- reeve-0.1.0/threelane_memory/auth.py +192 -0
- reeve-0.1.0/threelane_memory/backup.py +173 -0
- reeve-0.1.0/threelane_memory/bedrock_llm.py +123 -0
- reeve-0.1.0/threelane_memory/chat.py +133 -0
- reeve-0.1.0/threelane_memory/cli.py +292 -0
- reeve-0.1.0/threelane_memory/config.py +133 -0
- reeve-0.1.0/threelane_memory/database.py +225 -0
- reeve-0.1.0/threelane_memory/dual_ingest.py +685 -0
- reeve-0.1.0/threelane_memory/embeddings.py +68 -0
- reeve-0.1.0/threelane_memory/entity_dedup.py +174 -0
- reeve-0.1.0/threelane_memory/evaluation.py +77 -0
- reeve-0.1.0/threelane_memory/ingestion.py +474 -0
- reeve-0.1.0/threelane_memory/llm_interface.py +73 -0
- reeve-0.1.0/threelane_memory/mcp_server.py +182 -0
- reeve-0.1.0/threelane_memory/mem0_dual_ingest.py +11 -0
- reeve-0.1.0/threelane_memory/mem0_ingest.py +469 -0
- reeve-0.1.0/threelane_memory/oauth2.py +215 -0
- reeve-0.1.0/threelane_memory/operator.py +231 -0
- reeve-0.1.0/threelane_memory/reconciler.py +708 -0
- reeve-0.1.0/threelane_memory/reeve_dual_ingest.py +11 -0
- reeve-0.1.0/threelane_memory/reeve_ingest.py +484 -0
- reeve-0.1.0/threelane_memory/retriever.py +573 -0
- reeve-0.1.0/threelane_memory/schemas.py +39 -0
- reeve-0.1.0/threelane_memory/static/index.html +189 -0
- reeve-0.1.0/threelane_memory/tools.py +93 -0
- reeve-0.1.0/threelane_memory/utils.py +6 -0
reeve-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Ankesh Kumar
|
|
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.
|
reeve-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: reeve
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A personal long-term memory system built on Neo4j and OpenAI — stores and recalls episodic memories across a 70-year lifespan using a hybrid 3-lane retrieval engine.
|
|
5
|
+
Author: Ankesh Kumar
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ankeshkumar/threelane-memory
|
|
8
|
+
Project-URL: Repository, https://github.com/ankeshkumar/threelane-memory
|
|
9
|
+
Project-URL: Issues, https://github.com/ankeshkumar/threelane-memory/issues
|
|
10
|
+
Project-URL: Documentation, https://github.com/ankeshkumar/threelane-memory#readme
|
|
11
|
+
Keywords: memory,neo4j,knowledge-graph,openai,embeddings,long-term-memory,personal-ai
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Topic :: Database
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: neo4j>=5.0
|
|
26
|
+
Requires-Dist: langchain>=0.2
|
|
27
|
+
Requires-Dist: langchain-ollama>=0.2
|
|
28
|
+
Requires-Dist: numpy>=1.24
|
|
29
|
+
Requires-Dist: python-dotenv>=1.0
|
|
30
|
+
Provides-Extra: openai
|
|
31
|
+
Requires-Dist: langchain-openai>=0.1; extra == "openai"
|
|
32
|
+
Requires-Dist: openai>=1.0; extra == "openai"
|
|
33
|
+
Provides-Extra: streamlit
|
|
34
|
+
Requires-Dist: streamlit>=1.30; extra == "streamlit"
|
|
35
|
+
Provides-Extra: mcp
|
|
36
|
+
Requires-Dist: mcp>=1.6; extra == "mcp"
|
|
37
|
+
Requires-Dist: google-auth>=2.0.0; extra == "mcp"
|
|
38
|
+
Requires-Dist: uvicorn>=0.20.0; extra == "mcp"
|
|
39
|
+
Requires-Dist: starlette>=0.30.0; extra == "mcp"
|
|
40
|
+
Provides-Extra: dev
|
|
41
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
42
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
43
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
44
|
+
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
45
|
+
Dynamic: license-file
|
|
46
|
+
|
|
47
|
+
# threelane-memory
|
|
48
|
+
|
|
49
|
+
**Lifetime persistent memory for LLMs — a knowledge graph that remembers everything, knows what changed, and never forgets what matters.**
|
|
50
|
+
|
|
51
|
+
Every fact you store today is still retrievable 50 years from now — accurately, instantly, and in context.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from threelane_memory import store, query
|
|
55
|
+
|
|
56
|
+
store("I love playing football")
|
|
57
|
+
store("I just started at Google as a software engineer in San Francisco")
|
|
58
|
+
|
|
59
|
+
# Months later...
|
|
60
|
+
store("I got promoted to senior engineer at Google")
|
|
61
|
+
store("I moved from San Francisco to New York")
|
|
62
|
+
|
|
63
|
+
# A friend invites you to play football — should you go?
|
|
64
|
+
query("My friend invited me to play football, should I go?")
|
|
65
|
+
# → "Yes, you should go! You love playing football."
|
|
66
|
+
|
|
67
|
+
query("Where do I work?") # → "Google, as a senior software engineer." (role updated)
|
|
68
|
+
query("Where do I live?") # → "New York." (not SF — state was superseded)
|
|
69
|
+
query("Did I ever live in SF?") # → "Yes, before moving to New York." (history preserved)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The football memory was stored months ago in a completely different conversation. But when you need it, Threelane connects the dots — because it actually *understands* what it stored, not just what's similar.
|
|
73
|
+
|
|
74
|
+
No context windows. No token limits. No forgetting. **Persistent memory that outlives any single conversation — or any single year.**
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Why This Exists
|
|
79
|
+
|
|
80
|
+
LLMs are stateless. Every conversation starts from zero. The common workarounds all break down over time:
|
|
81
|
+
|
|
82
|
+
| Approach | Works for a week | Breaks at scale |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| **Chat history** | ✓ | Grows unbounded, no structure, can't handle contradictions |
|
|
85
|
+
| **Vector stores** (Pinecone, ChromaDB) | ✓ | Flat chunks — "I moved to NYC" never invalidates "I live in SF" |
|
|
86
|
+
| **RAG pipelines** | ✓ | Retrieves similar text, not relevant *knowledge* |
|
|
87
|
+
| **Context-window managers** (MemGPT) | ✓ | Clever paging, but still raw text — no entity tracking, no state evolution |
|
|
88
|
+
|
|
89
|
+
Ask any of them: *"What changed about me since last year?"* — silence.
|
|
90
|
+
|
|
91
|
+
Ask Threelane — it knows, because it actually tracks entities, states, and time.
|
|
92
|
+
|
|
93
|
+
## How It Works
|
|
94
|
+
|
|
95
|
+
Threelane doesn't store text. It **understands** it.
|
|
96
|
+
|
|
97
|
+
Every input is parsed by an LLM into structured semantics — entities, actions, states, roles, emotions, locations — and written into a **Neo4j knowledge graph** with typed relationships. This isn't an embedding dump. It's a living, evolving model of everything you've told it.
|
|
98
|
+
|
|
99
|
+
### What Happens When You Store a Memory
|
|
100
|
+
|
|
101
|
+
*"I love playing football"* becomes a structured graph:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
(Episode) ──INVOLVES──▶ (Entity: you)
|
|
105
|
+
│
|
|
106
|
+
├──HAS_STATE──▶ (State: hobby=football, sentiment=love) ──OF_ENTITY──▶ (Entity: you)
|
|
107
|
+
└──INVOLVES──▶ (Entity: football)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
*"I just started at Google as a software engineer in San Francisco"*:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
(Episode) ──INVOLVES──▶ (Entity: Google)
|
|
114
|
+
│ │
|
|
115
|
+
├──HAS_ACTION──▶ (Action: started working) ──BY_ENTITY──▶ (Entity: you)
|
|
116
|
+
│ ──ON_ENTITY──▶ (Entity: Google)
|
|
117
|
+
├──HAS_STATE──▶ (State: role=software engineer) ──OF_ENTITY──▶ (Entity: you)
|
|
118
|
+
└──AT_LOCATION──▶ (Location: San Francisco)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Months later, when someone asks *"My friend invited me to play football, should I go?"*, Threelane retrieves the football episode by semantic similarity, sees the `sentiment=love` state, and answers: *"Yes! You love playing football."*
|
|
122
|
+
|
|
123
|
+
No explicit link was ever made between the question and the stored memory — the knowledge graph makes the connection automatically.
|
|
124
|
+
|
|
125
|
+
### What Happens When Facts Change
|
|
126
|
+
|
|
127
|
+
*"I moved from San Francisco to New York"* — the old state is **superseded**, not deleted or duplicated. Full history is preserved:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
(State: city=New York, active=true) ──SUPERSEDES──▶ (State: city=San Francisco, active=false)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Ask *"Where do I live?"* → gets the current answer: New York.
|
|
134
|
+
Ask *"Where was I living when I joined Google?"* → still knows it was San Francisco.
|
|
135
|
+
|
|
136
|
+
**This is what persistent memory actually means.** Not similarity search — structured, evolving knowledge with a complete audit trail.
|
|
137
|
+
|
|
138
|
+
### 3-Lane Retrieval: Why the Right Memory Surfaces
|
|
139
|
+
|
|
140
|
+
Most systems rank by vector similarity alone. That works for a week. Over years, your "I got married" memory gets buried under thousands of newer, more recent entries.
|
|
141
|
+
|
|
142
|
+
Threelane scores every candidate across **three lanes** simultaneously:
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
score = 0.65 × vector_similarity + 0.30 × importance + 0.05 × recency
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- **Vector similarity** — semantic relevance via ANN search
|
|
149
|
+
- **Importance** — LLM-assigned at ingestion (landmark life events score higher)
|
|
150
|
+
- **Recency** — exponential decay with a 365-day half-life
|
|
151
|
+
|
|
152
|
+
The key: memories above importance 0.75 **bypass recency decay entirely**. Your wedding, your child's birth, a cancer diagnosis — these surface instantly no matter how old they are. Yesterday's lunch order won't outrank them.
|
|
153
|
+
|
|
154
|
+
### Entity Resolution: One Identity, Many Names
|
|
155
|
+
|
|
156
|
+
*"Google"*, *"my company"*, *"the office"*, *"work"* — all resolve to the same canonical entity through 3-layer matching:
|
|
157
|
+
|
|
158
|
+
1. **Case-insensitive exact** — instant lookup
|
|
159
|
+
2. **Substring containment** — "my company Google" → Google
|
|
160
|
+
3. **Embedding similarity** — cosine ≥ 0.88 catches semantic equivalents
|
|
161
|
+
|
|
162
|
+
### Designed to Last a Lifetime
|
|
163
|
+
|
|
164
|
+
Most memory systems assume weeks of data. Threelane is engineered for a **70-year lifespan**:
|
|
165
|
+
|
|
166
|
+
| What could go wrong | How Threelane handles it |
|
|
167
|
+
|---|---|
|
|
168
|
+
| Graph grows to millions of nodes | Dynamic candidate pool scales with size (2% of episodes, 50–500) |
|
|
169
|
+
| Old memories become noise | Consolidation engine LLM-summarizes old low-importance episodes |
|
|
170
|
+
| Entity names fragment over years | Background 3-pass deduplication merges fragmented nodes |
|
|
171
|
+
| Facts become outdated | SUPERSEDES chain — current state always queryable, history preserved |
|
|
172
|
+
| Embedding models get replaced | Model version tags + batch reindex utility |
|
|
173
|
+
| Disaster strikes | Full JSON backup with timestamped exports |
|
|
174
|
+
|
|
175
|
+
**Store a memory on day one. Query it on day 25,550 (year 70). It's still there, still accurate, still in context.**
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Features
|
|
180
|
+
|
|
181
|
+
- **Pluggable LLM providers** — Ollama (local, free) or OpenAI (cloud); switch via one env var
|
|
182
|
+
- **Semantic extraction** — LLM parses text into structured entities, actions, states, roles, emotions, importance, and location
|
|
183
|
+
- **Neo4j knowledge graph** — Episodes, Entities, Actions, States, Roles, Locations with typed relationships
|
|
184
|
+
- **3-layer entity resolution** — case-insensitive → substring → embedding similarity (≥ 0.88)
|
|
185
|
+
- **State contradiction handling** — new facts `SUPERSEDE` old ones with full audit trail
|
|
186
|
+
- **Vector ANN search** — Provider-matched embeddings indexed in Neo4j
|
|
187
|
+
- **70-year recency tuning** — 5% recency weight, 365-day half-life, importance floor bypass
|
|
188
|
+
- **Temporal queries** — supports "last 3 months", "in 2024", "March 2025" natively
|
|
189
|
+
- **Memory consolidation** — LLM-summarizes old low-importance episodes
|
|
190
|
+
- **Backup & export** — full graph dump to timestamped JSON
|
|
191
|
+
- **Entity deduplication** — 3-pass background scan and merge
|
|
192
|
+
|
|
193
|
+
## Installation
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
pip install threelane-memory
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
If you want to run it as an MCP server, install MCP extras:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
pip install "threelane-memory[mcp]"
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Or install from source:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
git clone https://github.com/ankeshkumar/threelane-memory.git
|
|
209
|
+
cd threelane-memory
|
|
210
|
+
pip install -e .
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Prerequisites
|
|
214
|
+
|
|
215
|
+
- **Python 3.10+**
|
|
216
|
+
- **Neo4j 5.x** (Aura or self-hosted with vector index support)
|
|
217
|
+
- **LLM provider** — one of:
|
|
218
|
+
- **Ollama** (default, free, local) — [Install Ollama](https://ollama.com)
|
|
219
|
+
- **OpenAI** — requires API key from [platform.openai.com](https://platform.openai.com/api-keys)
|
|
220
|
+
|
|
221
|
+
### Configuration
|
|
222
|
+
|
|
223
|
+
Copy the example environment file and fill in your credentials:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
cp .env.example .env
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
#### Option A — Ollama (local, default)
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
# Install & start Ollama
|
|
233
|
+
brew install ollama
|
|
234
|
+
ollama serve # keep running in a separate terminal
|
|
235
|
+
|
|
236
|
+
# Pull required models
|
|
237
|
+
ollama pull llama3.2:3b
|
|
238
|
+
ollama pull nomic-embed-text
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Your `.env` only needs Neo4j credentials — Ollama settings default automatically:
|
|
242
|
+
|
|
243
|
+
```env
|
|
244
|
+
LLM_PROVIDER=ollama
|
|
245
|
+
NEO4J_URI=neo4j+s://your-instance.databases.neo4j.io
|
|
246
|
+
NEO4J_USER=neo4j
|
|
247
|
+
NEO4J_PASSWORD=your-password
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
#### Option B — OpenAI (cloud)
|
|
251
|
+
|
|
252
|
+
```env
|
|
253
|
+
LLM_PROVIDER=openai
|
|
254
|
+
OPENAI_API_KEY=sk-your-api-key
|
|
255
|
+
NEO4J_URI=neo4j+s://your-instance.databases.neo4j.io
|
|
256
|
+
NEO4J_USER=neo4j
|
|
257
|
+
NEO4J_PASSWORD=your-password
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Create the required Neo4j vector index (run once in Neo4j Browser — set dimension to match your provider):
|
|
261
|
+
|
|
262
|
+
```cypher
|
|
263
|
+
-- Ollama (nomic-embed-text): 768 dimensions
|
|
264
|
+
-- OpenAI (text-embedding-ada-002): 1536 dimensions
|
|
265
|
+
CREATE VECTOR INDEX episode_embedding IF NOT EXISTS
|
|
266
|
+
FOR (ep:Episode) ON (ep.embedding)
|
|
267
|
+
OPTIONS {indexConfig: {
|
|
268
|
+
`vector.dimensions`: 768,
|
|
269
|
+
`vector.similarity_function`: 'cosine'
|
|
270
|
+
}};
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Verify your setup:
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
threelane-memory config
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Quick Start
|
|
280
|
+
|
|
281
|
+
### Python API
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
from threelane_memory import store, query, close
|
|
285
|
+
|
|
286
|
+
# Store memories
|
|
287
|
+
store("I love playing football", speaker="ankesh")
|
|
288
|
+
store("I work at Google as a software engineer", speaker="ankesh")
|
|
289
|
+
|
|
290
|
+
# Query — even months later, across different conversations
|
|
291
|
+
answer = query("My friend invited me to play football, should I go?", speaker="ankesh")
|
|
292
|
+
print(answer) # "Yes, you should! You love playing football."
|
|
293
|
+
|
|
294
|
+
close()
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### CLI
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
# Interactive chat
|
|
301
|
+
threelane-memory chat --speaker ankesh
|
|
302
|
+
|
|
303
|
+
# Store a single memory
|
|
304
|
+
threelane-memory store "I love playing football" --speaker ankesh
|
|
305
|
+
|
|
306
|
+
# Query — connects to stored knowledge automatically
|
|
307
|
+
threelane-memory query "My friend invited me to play football, should I go?" --speaker ankesh
|
|
308
|
+
|
|
309
|
+
# Show active provider, models & run health checks
|
|
310
|
+
threelane-memory config
|
|
311
|
+
|
|
312
|
+
# Admin operations
|
|
313
|
+
threelane-memory backup --speaker ankesh
|
|
314
|
+
threelane-memory dedup
|
|
315
|
+
threelane-memory dedup --dry-run
|
|
316
|
+
threelane-memory consolidate --speaker ankesh
|
|
317
|
+
threelane-memory reindex --speaker ankesh --old-model nomic-embed-text
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Or via `python -m`:
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
python -m threelane_memory chat --speaker ankesh
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### MCP Server
|
|
327
|
+
|
|
328
|
+
Run as an MCP server over stdio (recommended for MCP clients like Claude Desktop / VS Code):
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
threelane-memory mcp
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Or run directly from the dedicated entrypoint:
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
threelane-memory-mcp
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Network transport (SSE) is also supported:
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
threelane-memory mcp --transport sse --host 127.0.0.1 --port 8000
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Exposed MCP tools include:
|
|
347
|
+
|
|
348
|
+
- `store_memory`
|
|
349
|
+
- `query_memory`
|
|
350
|
+
- `retrieve_memory_context`
|
|
351
|
+
- `memory_config`
|
|
352
|
+
- `backup_memory`
|
|
353
|
+
- `deduplicate_memory_entities`
|
|
354
|
+
- `consolidate_memory`
|
|
355
|
+
|
|
356
|
+
### Deploy MCP Server
|
|
357
|
+
|
|
358
|
+
#### Option A — Docker (SSE on port 8000)
|
|
359
|
+
|
|
360
|
+
Deployment files:
|
|
361
|
+
|
|
362
|
+
- `deploy/mcp/Dockerfile`
|
|
363
|
+
- `deploy/mcp/docker-compose.yml`
|
|
364
|
+
|
|
365
|
+
Run:
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
cd deploy/mcp
|
|
369
|
+
docker compose up -d --build
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Stop:
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
cd deploy/mcp
|
|
376
|
+
docker compose down
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Client configuration templates for Claude and other MCP clients:
|
|
380
|
+
|
|
381
|
+
- `docs/mcp-client-configs.md`
|
|
382
|
+
- `deploy/mcp/client-configs/`
|
|
383
|
+
|
|
384
|
+
#### Option B — macOS launchd (always-on local service)
|
|
385
|
+
|
|
386
|
+
Template file:
|
|
387
|
+
|
|
388
|
+
- `deploy/mcp/com.threelane.memory.mcp.plist`
|
|
389
|
+
|
|
390
|
+
Install and start:
|
|
391
|
+
|
|
392
|
+
```bash
|
|
393
|
+
cp deploy/mcp/com.threelane.memory.mcp.plist ~/Library/LaunchAgents/
|
|
394
|
+
launchctl load ~/Library/LaunchAgents/com.threelane.memory.mcp.plist
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Stop and unload:
|
|
398
|
+
|
|
399
|
+
```bash
|
|
400
|
+
launchctl unload ~/Library/LaunchAgents/com.threelane.memory.mcp.plist
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
## Architecture
|
|
404
|
+
|
|
405
|
+
```
|
|
406
|
+
User Input
|
|
407
|
+
│
|
|
408
|
+
├── Statement ──▶ operator.py ──▶ reconciler.py ──▶ Neo4j Graph
|
|
409
|
+
│ (LLM) (entity resolution,
|
|
410
|
+
│ state contradiction,
|
|
411
|
+
│ graph writing)
|
|
412
|
+
│
|
|
413
|
+
└── Question ──▶ retriever.py ──▶ Neo4j Vector Index
|
|
414
|
+
(3-lane search) + Graph Traversal
|
|
415
|
+
──▶ llm_interface.py ──▶ Answer
|
|
416
|
+
(LLM)
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
See [docs/flowcharts.md](docs/flowcharts.md) for detailed architecture diagrams.
|
|
420
|
+
|
|
421
|
+
## Switching Providers
|
|
422
|
+
|
|
423
|
+
threelane-memory auto-detects the correct embedding dimension and warns you about mismatches. Run `threelane-memory config` at any time to check your setup.
|
|
424
|
+
|
|
425
|
+
### Ollama → OpenAI
|
|
426
|
+
|
|
427
|
+
```bash
|
|
428
|
+
# 1. Update .env
|
|
429
|
+
LLM_PROVIDER=openai
|
|
430
|
+
OPENAI_API_KEY=sk-your-api-key
|
|
431
|
+
|
|
432
|
+
# 2. Install the optional OpenAI dependency
|
|
433
|
+
pip install threelane-memory[openai]
|
|
434
|
+
|
|
435
|
+
# 3. Verify (will warn if Neo4j index dimension doesn't match)
|
|
436
|
+
threelane-memory config
|
|
437
|
+
|
|
438
|
+
# 4. Recreate the vector index (1536-dim for text-embedding-ada-002)
|
|
439
|
+
# In Neo4j Browser:
|
|
440
|
+
# DROP INDEX episode_embedding;
|
|
441
|
+
# CREATE VECTOR INDEX episode_embedding IF NOT EXISTS
|
|
442
|
+
# FOR (ep:Episode) ON (ep.embedding)
|
|
443
|
+
# OPTIONS {indexConfig: {`vector.dimensions`: 1536, `vector.similarity_function`: 'cosine'}};
|
|
444
|
+
|
|
445
|
+
# 5. Re-embed all episodes with the new model
|
|
446
|
+
threelane-memory reindex --old-model nomic-embed-text
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### OpenAI → Ollama
|
|
450
|
+
|
|
451
|
+
```bash
|
|
452
|
+
# 1. Install & start Ollama
|
|
453
|
+
brew install ollama && ollama serve
|
|
454
|
+
ollama pull llama3.2:3b && ollama pull nomic-embed-text
|
|
455
|
+
|
|
456
|
+
# 2. Update .env
|
|
457
|
+
LLM_PROVIDER=ollama
|
|
458
|
+
|
|
459
|
+
# 3. Verify
|
|
460
|
+
threelane-memory config
|
|
461
|
+
|
|
462
|
+
# 4. Recreate the vector index (768-dim for nomic-embed-text)
|
|
463
|
+
# In Neo4j Browser:
|
|
464
|
+
# DROP INDEX episode_embedding;
|
|
465
|
+
# CREATE VECTOR INDEX episode_embedding IF NOT EXISTS
|
|
466
|
+
# FOR (ep:Episode) ON (ep.embedding)
|
|
467
|
+
# OPTIONS {indexConfig: {`vector.dimensions`: 768, `vector.similarity_function`: 'cosine'}};
|
|
468
|
+
|
|
469
|
+
# 5. Re-embed all episodes
|
|
470
|
+
threelane-memory reindex --old-model text-embedding-ada-002
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### Using a Custom Model
|
|
474
|
+
|
|
475
|
+
Set these in `.env` to use any Ollama-compatible model:
|
|
476
|
+
|
|
477
|
+
```env
|
|
478
|
+
OLLAMA_CHAT_MODEL=mistral # any chat model
|
|
479
|
+
OLLAMA_EMBED_MODEL=mxbai-embed-large # any embedding model
|
|
480
|
+
EMBEDDING_DIM=1024 # override auto-detection for unknown models
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
## Advanced Usage
|
|
484
|
+
|
|
485
|
+
Use the lower-level API for fine-grained control:
|
|
486
|
+
|
|
487
|
+
```python
|
|
488
|
+
from threelane_memory.operator import operator_extract
|
|
489
|
+
from threelane_memory.reconciler import reconcile, consolidate
|
|
490
|
+
from threelane_memory.retriever import retrieve
|
|
491
|
+
from threelane_memory.backup import save_backup
|
|
492
|
+
from threelane_memory.entity_dedup import deduplicate_entities
|
|
493
|
+
|
|
494
|
+
# Extract semantics
|
|
495
|
+
semantics = operator_extract("Alice presented her paper at MIT")
|
|
496
|
+
|
|
497
|
+
# Write to graph
|
|
498
|
+
episode_id = reconcile(semantics, speaker="ankesh", raw_text="...")
|
|
499
|
+
|
|
500
|
+
# Retrieve context
|
|
501
|
+
context = retrieve("What did Alice do?", speaker="ankesh")
|
|
502
|
+
|
|
503
|
+
# Admin operations
|
|
504
|
+
consolidate("ankesh")
|
|
505
|
+
save_backup(speaker="ankesh")
|
|
506
|
+
deduplicate_entities(dry_run=False)
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
## Project Structure
|
|
510
|
+
|
|
511
|
+
```
|
|
512
|
+
threelane-memory/
|
|
513
|
+
├── threelane_memory/ # Core package
|
|
514
|
+
│ ├── __init__.py # Public API (store, query, close)
|
|
515
|
+
│ ├── __main__.py # python -m threelane_memory
|
|
516
|
+
│ ├── cli.py # CLI entry point
|
|
517
|
+
│ ├── config.py # Configuration from .env
|
|
518
|
+
│ ├── schemas.py # TypedDict data contracts
|
|
519
|
+
│ ├── database.py # Neo4j driver wrapper
|
|
520
|
+
│ ├── embeddings.py # Embedding wrapper (Ollama / OpenAI)
|
|
521
|
+
│ ├── llm_interface.py # LLM chat wrapper (Ollama / OpenAI)
|
|
522
|
+
│ ├── operator.py # Semantic extraction via LLM
|
|
523
|
+
│ ├── reconciler.py # Graph writer + entity resolution
|
|
524
|
+
│ ├── retriever.py # 3-lane retrieval engine
|
|
525
|
+
│ ├── entity_dedup.py # Entity deduplication
|
|
526
|
+
│ ├── backup.py # Graph export to JSON
|
|
527
|
+
│ ├── chat.py # Interactive chat loop
|
|
528
|
+
│ └── utils.py # Shared helpers
|
|
529
|
+
├── examples/ # Example scripts
|
|
530
|
+
│ ├── basic_usage.py # Store and query
|
|
531
|
+
│ ├── advanced_pipeline.py # Lower-level API usage
|
|
532
|
+
│ ├── streamlit_app.py # Optional Streamlit web UI
|
|
533
|
+
│ └── debug_retrieval.py # Diagnostic tool
|
|
534
|
+
├── tests/ # Test suite
|
|
535
|
+
├── docs/ # Documentation
|
|
536
|
+
│ └── flowcharts.md # Architecture flowcharts
|
|
537
|
+
├── pyproject.toml # Package metadata and dependencies
|
|
538
|
+
├── .env.example # Environment variable template
|
|
539
|
+
├── LICENSE # MIT License
|
|
540
|
+
├── CONTRIBUTING.md # Contribution guidelines
|
|
541
|
+
└── README.md
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
## Configuration Reference
|
|
545
|
+
|
|
546
|
+
| Variable | Default | Description |
|
|
547
|
+
|---|---|---|
|
|
548
|
+
| `LLM_PROVIDER` | `ollama` | LLM backend: `ollama` (local) or `openai` (cloud) |
|
|
549
|
+
| `OLLAMA_BASE_URL` | `http://localhost:11434` | Ollama server address |
|
|
550
|
+
| `OLLAMA_CHAT_MODEL` | `llama3.2:3b` | Ollama chat model |
|
|
551
|
+
| `OLLAMA_EMBED_MODEL` | `nomic-embed-text` | Ollama embedding model (768-dim) |
|
|
552
|
+
| `OPENAI_API_KEY` | — | OpenAI API key (required when provider=openai) |
|
|
553
|
+
| `OPENAI_CHAT_MODEL` | `gpt-4o` | OpenAI chat model |
|
|
554
|
+
| `OPENAI_EMBED_MODEL` | `text-embedding-ada-002` | OpenAI embedding model (1536-dim) |
|
|
555
|
+
| `EMBEDDING_DIM` | auto | Override auto-detected embedding dimension |
|
|
556
|
+
| `WEIGHT_SIMILARITY` | `0.65` | Vector similarity weight in scoring |
|
|
557
|
+
| `WEIGHT_IMPORTANCE` | `0.30` | Importance weight in scoring |
|
|
558
|
+
| `WEIGHT_RECENCY` | `0.05` | Recency weight (low for 70-year safety) |
|
|
559
|
+
| `RECENCY_HALF_LIFE_DAYS` | `365` | Days for recency to decay by 50% |
|
|
560
|
+
| `IMPORTANCE_FLOOR` | `0.75` | Episodes above this ignore recency decay |
|
|
561
|
+
| `VECTOR_CANDIDATES_MIN` | `50` | Minimum ANN candidates |
|
|
562
|
+
| `VECTOR_CANDIDATES_MAX` | `500` | Maximum ANN candidates |
|
|
563
|
+
| `VECTOR_CANDIDATES_RATIO` | `0.02` | Fraction of total episodes to search |
|
|
564
|
+
| `CONSOLIDATION_AGE_DAYS` | `90` | Episodes older than this are eligible |
|
|
565
|
+
| `CONSOLIDATION_BATCH_SIZE` | `50` | Max episodes per consolidation round |
|
|
566
|
+
| `CONSOLIDATION_IMPORTANCE_CAP` | `0.3` | Only consolidate below this importance |
|
|
567
|
+
|
|
568
|
+
## 70-Year Design Decisions
|
|
569
|
+
|
|
570
|
+
| Problem | Solution |
|
|
571
|
+
|---|---|
|
|
572
|
+
| Recency bias buries old memories | Recency weight = 5%, half-life = 365 days |
|
|
573
|
+
| Important memories forgotten | Importance floor bypass (≥ 0.75 → recency = 1.0) |
|
|
574
|
+
| Fixed candidate pool too small | Dynamic pool: 2% of graph, clamped 50–500 |
|
|
575
|
+
| Graph grows unbounded | Consolidation engine merges old trivia |
|
|
576
|
+
| State contradictions | SUPERSEDES chain — only latest value active |
|
|
577
|
+
| Entity fragmentation | 3-layer entity resolution + background dedup |
|
|
578
|
+
| Embedding model changes | Model version tag + batch reindex utility |
|
|
579
|
+
| Vector index lag | 5-minute recent-episode safety net |
|
|
580
|
+
| No disaster recovery | Full JSON export with incremental backup |
|
|
581
|
+
|
|
582
|
+
## Development
|
|
583
|
+
|
|
584
|
+
```bash
|
|
585
|
+
# Install with dev dependencies
|
|
586
|
+
pip install -e ".[dev]"
|
|
587
|
+
|
|
588
|
+
# Run tests
|
|
589
|
+
pytest tests/
|
|
590
|
+
|
|
591
|
+
# Lint
|
|
592
|
+
ruff check .
|
|
593
|
+
|
|
594
|
+
# Type check
|
|
595
|
+
mypy threelane_memory/
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for full guidelines.
|
|
599
|
+
|
|
600
|
+
## Tech Stack
|
|
601
|
+
|
|
602
|
+
- **Python 3.10+**
|
|
603
|
+
- **Neo4j 5.x** — Graph database with vector index
|
|
604
|
+
- **Ollama** (default) — Local LLM & embeddings (llama3.2, nomic-embed-text)
|
|
605
|
+
- **OpenAI** (optional) — GPT-4o (chat) + text-embedding-ada-002 (embeddings)
|
|
606
|
+
- **LangChain** — Unified LLM/embedding wrappers
|
|
607
|
+
- **NumPy** — Cosine similarity computation
|
|
608
|
+
|
|
609
|
+
## License
|
|
610
|
+
|
|
611
|
+
MIT — see [LICENSE](LICENSE).
|