lean-memory 0.1.0__py3-none-any.whl

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.
lean_memory/types.py ADDED
@@ -0,0 +1,107 @@
1
+ """Core data types for lean-memory.
2
+
3
+ These mirror the design-spec data model (episode / entity / fact). Phase 0 uses
4
+ the fact layer with the monotemporal spine; the bi-temporal audit columns exist
5
+ but are only populated when the `audit` extra is enabled (deferred past Phase 0).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import time
11
+ import uuid
12
+ from dataclasses import dataclass, field
13
+ from typing import Optional
14
+
15
+
16
+ def now_ms() -> int:
17
+ """Epoch milliseconds. Single source of wall-clock time so it can be faked in tests."""
18
+ return int(time.time() * 1000)
19
+
20
+
21
+ def new_id() -> str:
22
+ """uuidv7-ish: time-sortable id. stdlib has no uuid7, so we prefix a ms timestamp.
23
+
24
+ Format: <48-bit ms hex>-<random>. Lexicographically sortable by creation time,
25
+ which matches the spec's "uuidv7 (time-sortable)" intent for episode/fact ids.
26
+ """
27
+ return f"{now_ms():012x}-{uuid.uuid4().hex[:16]}"
28
+
29
+
30
+ @dataclass
31
+ class Episode:
32
+ """Provenance layer: the verbatim ingested message/turn/JSON."""
33
+
34
+ namespace: str
35
+ raw: str
36
+ t_ref: int # reference (world) time for relative-date resolution, epoch ms
37
+ source: str = "user" # 'user'|'assistant'|'tool'|'doc'
38
+ id: str = field(default_factory=new_id)
39
+ created_at: int = field(default_factory=now_ms)
40
+
41
+
42
+ @dataclass
43
+ class Entity:
44
+ """Canonical entity (person/org/place/...). Phase 0 resolves by (namespace, name, type)."""
45
+
46
+ namespace: str
47
+ name: str
48
+ type: Optional[str] = None
49
+ summary: Optional[str] = None
50
+ resolved_id: Optional[str] = None # canonical id if this row is an alias
51
+ id: str = field(default_factory=new_id)
52
+ created_at: int = field(default_factory=now_ms)
53
+
54
+
55
+ @dataclass
56
+ class Fact:
57
+ """A typed, timestamped triple with surface text — the unit of memory & retrieval.
58
+
59
+ The monotemporal spine (`valid_at`/`valid_to`/`superseded_by`/`is_latest`) is
60
+ always populated. Bi-temporal audit columns default to mirroring ingest time
61
+ and are only meaningfully diverged when the audit extra is on (Phase >0).
62
+ """
63
+
64
+ namespace: str
65
+ subject_id: str
66
+ predicate: str
67
+ fact_text: str # standalone sentence — what gets embedded + reranked
68
+ valid_at: int # world/event time the fact became true (epoch ms)
69
+ episode_id: str
70
+
71
+ object_id: Optional[str] = None # null if object is a literal
72
+ object_literal: Optional[str] = None
73
+
74
+ valid_to: Optional[int] = None # world time it stopped being true; None = still holds
75
+ superseded_by: Optional[str] = None
76
+ is_latest: int = 1
77
+
78
+ # bi-temporal audit axis (opt-in)
79
+ ingested_at: int = field(default_factory=now_ms)
80
+ expired_at: Optional[int] = None
81
+ invalidated_by: Optional[str] = None
82
+
83
+ # scoring / governance
84
+ confidence: float = 1.0
85
+ salience: float = 0.0
86
+ last_access: Optional[int] = None
87
+ access_count: int = 0
88
+ is_inference: int = 0
89
+ tier: str = "hot" # 'hot'|'cold'
90
+
91
+ id: str = field(default_factory=new_id)
92
+ created_at: int = field(default_factory=now_ms)
93
+
94
+
95
+ @dataclass
96
+ class RetrievedFact:
97
+ """A fact returned from retrieval, with the scores that ranked it (for debugging/eval)."""
98
+
99
+ fact: Fact
100
+ final_score: float
101
+ relevance: float # reranker (or fused) score
102
+ recency: float
103
+ importance: float
104
+ # provenance of the score, so the harness can ablate arms
105
+ dense_rank: Optional[int] = None
106
+ sparse_rank: Optional[int] = None
107
+ rrf_score: Optional[float] = None
@@ -0,0 +1,228 @@
1
+ Metadata-Version: 2.4
2
+ Name: lean-memory
3
+ Version: 0.1.0
4
+ Summary: Embedded, local-first agent memory with hybrid retrieval and ADD-only supersession.
5
+ Project-URL: Homepage, https://github.com/Wuesteon/lean-memory
6
+ Project-URL: Repository, https://github.com/Wuesteon/lean-memory
7
+ Author: lean-memory
8
+ License: Apache-2.0
9
+ Keywords: agent-memory,embeddings,llm,local-first,mcp,memory,rag,retrieval,sqlite,vector-search
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Operating System :: OS Independent
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 :: Database :: Database Engines/Servers
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: bm25s>=0.2.0
24
+ Requires-Dist: numpy>=1.24
25
+ Requires-Dist: python-dateutil>=2.8
26
+ Requires-Dist: sqlite-vec>=0.1.6
27
+ Provides-Extra: bench
28
+ Requires-Dist: openai>=1.40; extra == 'bench'
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=8.0; extra == 'dev'
31
+ Requires-Dist: tomli>=2.0; (python_version < '3.11') and extra == 'dev'
32
+ Provides-Extra: examples
33
+ Requires-Dist: anthropic>=0.25; extra == 'examples'
34
+ Provides-Extra: extract
35
+ Requires-Dist: gliner2>=1.3.1; extra == 'extract'
36
+ Provides-Extra: llm
37
+ Requires-Dist: ollama>=0.6.0; extra == 'llm'
38
+ Provides-Extra: mcp
39
+ Requires-Dist: mcp>=1.0; extra == 'mcp'
40
+ Provides-Extra: models
41
+ Requires-Dist: sentence-transformers>=3.0; extra == 'models'
42
+ Requires-Dist: torch>=2.2; extra == 'models'
43
+ Description-Content-Type: text/markdown
44
+
45
+ # lean-memory
46
+
47
+ [![test](https://github.com/Wuesteon/lean-memory/actions/workflows/test.yml/badge.svg)](https://github.com/Wuesteon/lean-memory/actions/workflows/test.yml)
48
+
49
+ Embedded, local-first agent memory. No server, no daemon, no mandatory cloud key.
50
+
51
+ > **Status (2026-07):** working toward the first public launch (MCP-first).
52
+ > Roadmap and rationale: `docs/superpowers/specs/2026-07-08-strategic-direction-design.md`.
53
+ > Public benchmark runs (LongMemEval/LoCoMo) are deferred until after launch;
54
+ > the harness is complete (`bench/phase2_*.py`) and the engine flaws it exposed
55
+ > are fixed on this branch — see `docs/phase2-learnings.md`.
56
+
57
+ ```python
58
+ from lean_memory import Memory
59
+
60
+ mem = Memory(root="./data")
61
+
62
+ mem.add("user-42", "I work at Acme Corp.")
63
+ mem.add("user-42", "I now work at Globex.") # supersedes Acme automatically
64
+
65
+ mem.search("user-42", "where does the user work?") # → "I now work at Globex."
66
+ ```
67
+
68
+ ![lean-memory quickstart](docs/assets/quickstart.gif)
69
+
70
+ Facts are extracted from natural language, stored in a per-namespace SQLite file, and retrieved with hybrid dense+sparse search. Old facts are never deleted — they're superseded and queryable at any past point in time.
71
+
72
+ ## Install
73
+
74
+ ```bash
75
+ pip install lean-memory
76
+ ```
77
+
78
+ Runs fully offline out of the box. Optional extras unlock real model quality:
79
+
80
+ | Extra | What it adds |
81
+ |---|---|
82
+ | `lean-memory[models]` | Real embedder + reranker (Qwen3-0.6B + Ettin-32M) |
83
+ | `lean-memory[extract]` | GLiNER2 candidate generation for richer extraction |
84
+ | `lean-memory[llm]` | Ollama-backed LLM typing pass |
85
+ | `lean-memory[mcp]` | MCP server bridge for Claude Desktop / Claude Code |
86
+ | `lean-memory[examples]` | Terminal demo agent (requires `anthropic` SDK) |
87
+
88
+ ## Quickstart
89
+
90
+ ```python
91
+ from lean_memory import Memory
92
+
93
+ mem = Memory(root="./data") # one SQLite file per namespace, stored under ./data/
94
+
95
+ # Store facts in natural language
96
+ mem.add("alice", "I work at Stripe.")
97
+ mem.add("alice", "I now work at Vercel.") # supersedes Stripe automatically
98
+
99
+ # Retrieve — the superseded Stripe fact drops out; only the current one is returned
100
+ results = mem.search("alice", "what does Alice do for work?", k=3)
101
+ for hit in results:
102
+ print(hit.fact.fact_text, hit.final_score)
103
+ # → I now work at Vercel. 0.89
104
+
105
+ # Point-in-time query — what was true at a specific moment?
106
+ mem.search("alice", "employer", as_of=1_700_000_000_000, is_latest_only=False) # epoch ms
107
+
108
+ # Always close when done (flushes WAL)
109
+ mem.close()
110
+ ```
111
+
112
+ ## Demo Agent
113
+
114
+ A terminal chatbot showing the full memory loop — add, retrieve, supersede, restart:
115
+
116
+ ```bash
117
+ pip install 'lean-memory[examples]'
118
+ export ANTHROPIC_API_KEY=sk-ant-...
119
+ python examples/chat.py # uses offline stubs by default
120
+ python examples/chat.py --namespace bob # separate memory tenant, persists across restarts
121
+ ```
122
+
123
+ No API key? The demo still runs — it echoes the retrieved memory context instead of calling Claude, so you can watch the engine work offline.
124
+
125
+ ## MCP Server — memory for Claude Code / Claude Desktop
126
+
127
+ Give any MCP agent persistent local memory: three tools (`memory_add`,
128
+ `memory_search`, `memory_clear`), one SQLite file per namespace, nothing
129
+ leaves your machine.
130
+
131
+ ```bash
132
+ pip install 'lean-memory[mcp,models,extract]'
133
+ ```
134
+
135
+ > First run downloads three open models (~2.0 GB total: Qwen3-Embedding-0.6B
136
+ > + Ettin-32M reranker for retrieval, plus GLiNER2-base (~0.8 GB) for real
137
+ > extraction — all ungated). Pre-warm once so your MCP client never waits on
138
+ > a download:
139
+ >
140
+ > ```bash
141
+ > python -c "from lean_memory.embed.sentence_transformer import SentenceTransformerEmbedder; \
142
+ > from lean_memory.retrieve.rerank import CrossEncoderReranker; \
143
+ > SentenceTransformerEmbedder().embed_one('warm'); CrossEncoderReranker().score('warm', ['up']); \
144
+ > from lean_memory.extract.gliner_extractor import Gliner2Generator; from lean_memory.types import Episode; \
145
+ > Gliner2Generator().generate(Episode(namespace='w', raw='I work at Acme.', t_ref=0, source='user'))"
146
+ > ```
147
+
148
+ **Claude Code:**
149
+
150
+ ```bash
151
+ claude mcp add lean-memory -- lean-memory-mcp
152
+ ```
153
+
154
+ **Claude Desktop** — add to `mcpServers` (or copy `examples/mcp_config.json`):
155
+
156
+ ```json
157
+ { "lean-memory": { "command": "lean-memory-mcp", "env": { "LM_DATA_ROOT": "~/.lean_memory" } } }
158
+ ```
159
+
160
+ Data root: `LM_DATA_ROOT` (default `~/.lean_memory`). Works offline-only too —
161
+ the server opportunistically upgrades each backend that its extra is installed
162
+ for (`[models]` → real embedder + reranker, `[extract]` → GLiNER2 extraction)
163
+ and otherwise falls back to deterministic stub backends (fine for CI,
164
+ semantically meaningless for real use — install `[mcp,models,extract]`).
165
+
166
+ > **What the optional `[llm]` extra buys.** The canonical `[mcp,models,extract]`
167
+ > install has no LLM typing pass, so the ~15% of candidates that escalate —
168
+ > almost all of them inferential (`derives`) facts — are typed by a
169
+ > deterministic stub instead of a model. Assertional facts are unaffected;
170
+ > inference-type facts are effectively second-class on the default path. Adding
171
+ > `[llm]` (a local Ollama model) upgrades that escalated tier to real
172
+ > constrained typing. See ARCHITECTURE.md → Known Limitations.
173
+
174
+ ## Real Model Quality
175
+
176
+ The default backends are offline stubs — deterministic and dependency-free, but semantically meaningless. Swap in real models for production-quality retrieval:
177
+
178
+ ```bash
179
+ pip install 'lean-memory[models]'
180
+ ```
181
+
182
+ With `Qwen3-Embedding-0.6B` + `Ettin-32M` reranker, retrieval jumps from 1/5 to 4/5 on the internal benchmark with zero code changes.
183
+
184
+ > For benchmark results, architecture decisions, and implementation status see [ARCHITECTURE.md](ARCHITECTURE.md).
185
+
186
+ ## How It Works
187
+
188
+ Each `mem.add()` call runs a 4-pass hybrid extraction pipeline:
189
+
190
+ 1. **Rules** — regex + dateparser for common predicates (`works_at`, `lives_in`, …)
191
+ 2. **GLiNER2** — open-vocabulary NER candidate generation (offline stub by default)
192
+ 3. **Router** — recall-biased escalation: low-confidence, coreference, and inferential (`derives`) facts escalate to the LLM pass
193
+ 4. **LLM typing** — constrained relation typing via a local Ollama model (stub by default)
194
+
195
+ Contradiction detection runs cheap-first (slot match → cosine → token subsumption → LLM). Conflicting facts are superseded, not deleted — the old fact stays with `is_latest=False` and a `superseded_by` pointer.
196
+
197
+ Retrieval fuses two-stage Matryoshka dense search (256-dim coarse KNN → 768-dim re-score) with BM25 sparse, applies RRF fusion, reranks with a cross-encoder, and scores with salience-decay (`0.6·relevance + 0.2·recency + 0.2·importance`).
198
+
199
+ ## Develop
200
+
201
+ ```bash
202
+ git clone https://github.com/Wuesteon/lean-memory
203
+ cd lean-memory
204
+ python -m venv .venv && source .venv/bin/activate
205
+ pip install -e '.[dev]'
206
+ pytest -q # full offline suite, no downloads
207
+ ```
208
+
209
+ ## Project Layout
210
+
211
+ ```
212
+ src/lean_memory/
213
+ memory.py Memory facade — the public API
214
+ types.py Episode / Fact / RetrievedFact types
215
+ store/ Store interface + SqliteStore (vec0 + FTS5)
216
+ embed/ Embedder interface, FakeEmbedder, SentenceTransformer
217
+ extract/ 4-pass extraction pipeline
218
+ retrieve/ Reranker interface, retrieval pipeline
219
+ examples/
220
+ chat.py Terminal demo agent
221
+ mcp_config.json Drop-in MCP client config
222
+ tests/ offline test suite
223
+ bench/ Retrieval quality + BET-2 ablation harnesses
224
+ ```
225
+
226
+ ## License
227
+
228
+ Apache-2.0
@@ -0,0 +1,27 @@
1
+ lean_memory/__init__.py,sha256=639w69GsqoPvip3-tu7KojoX3zfDZznGfS8Pe0z8iRQ,494
2
+ lean_memory/mcp_server.py,sha256=wc-hVUz8tLjfyVIcmgQ92jJCJeEkfGjzXdDZ1ygp5So,5491
3
+ lean_memory/memory.py,sha256=2IfW3n1_heGUJPXNHT5h0afABJ1nWQVX927GzhF47oA,8993
4
+ lean_memory/types.py,sha256=LobuNQLFtC4gKppnoql1ioVmUS7V9S0Jk3bRoAqM2jY,3428
5
+ lean_memory/embed/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ lean_memory/embed/base.py,sha256=nYX2S6wS0zi5dZx22N-IK_FgFTFTdKig3Iah6CmUXgU,1876
7
+ lean_memory/embed/fake.py,sha256=75KGqTl5qTOOvUHDEytvJfGhRpjNSM6WUPaE3EsFrbc,1560
8
+ lean_memory/embed/sentence_transformer.py,sha256=ByI67A9D8e1CXo3EfUle_PKqiHBUHOkMt9LHZ5oJz2k,2508
9
+ lean_memory/extract/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ lean_memory/extract/contradiction.py,sha256=RMQYh0v4ogEQzndlt11FN4A2WDfEf-Auq3o_8jTDWdE,16274
11
+ lean_memory/extract/gliner_extractor.py,sha256=lA14FyU28SvGFHmS-ZzELRfrq3NJhcibrkN-rTxQhJ0,24868
12
+ lean_memory/extract/llm_typer.py,sha256=dCAGhiEL3zXs8rN7-eRGEQ4gJiZB8ecnN5H5rmqaers,19914
13
+ lean_memory/extract/router.py,sha256=Hkwj7ffLXjH1TSqmQC9LI7lzwdVk22ojuUQCDOLLj_4,19174
14
+ lean_memory/extract/rules.py,sha256=FtiVBINzIG05pRg1Zz8tji0fBwVOqzgdny98Cflgi5Q,4742
15
+ lean_memory/extract/salience.py,sha256=MMxl43EEkU2wgszDem0bENee3c0eEdQ8DoOiuR1RXU0,5538
16
+ lean_memory/extract/taxonomy.py,sha256=VsHIrr6gqOJeC9O7vruKSK5I9klzAyyQRt3_PjQnRLc,11704
17
+ lean_memory/retrieve/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ lean_memory/retrieve/rerank.py,sha256=MenN2ILaEoFUblO8UyopOs6nCjwmdyUQmpAqdiQmloM,2237
19
+ lean_memory/retrieve/retriever.py,sha256=LGnhJvluN_lQUrSNODooqSZfCG_fxm8gtVBqW7o5N94,4530
20
+ lean_memory/store/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
+ lean_memory/store/base.py,sha256=0d29EyETf9_rn1yEbzhEJS-2R1bs_FoBdk6wvt_AOuM,3093
22
+ lean_memory/store/schema.py,sha256=_PFM0bgLYDE9XX3vHEnLiSyMSvJGDE47PwN6sCBPgQA,3352
23
+ lean_memory/store/sqlite_store.py,sha256=AQs240oCnV_PIrKF1k8R3NK4KQp5yV5axF1kC8lhHjM,12485
24
+ lean_memory-0.1.0.dist-info/METADATA,sha256=-0d_o7R8t7-c3PnbD0AJ9PLVKTbUeZl57f4zFj8ZIhQ,9417
25
+ lean_memory-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
26
+ lean_memory-0.1.0.dist-info/entry_points.txt,sha256=jOeIrkE0Z2vwDHeP705UoeSvd7agD6cSuM4SNPRhtdA,64
27
+ lean_memory-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ lean-memory-mcp = lean_memory.mcp_server:main