lean-memory 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lean_memory-0.1.0/.github/workflows/release.yml +44 -0
- lean_memory-0.1.0/.github/workflows/test.yml +43 -0
- lean_memory-0.1.0/.gitignore +10 -0
- lean_memory-0.1.0/ARCHITECTURE.md +208 -0
- lean_memory-0.1.0/CHANGELOG.md +57 -0
- lean_memory-0.1.0/CLAUDE.md +58 -0
- lean_memory-0.1.0/PERFORMANCE.md +3 -0
- lean_memory-0.1.0/PKG-INFO +228 -0
- lean_memory-0.1.0/README.md +184 -0
- lean_memory-0.1.0/bench/bet2_ablation.py +897 -0
- lean_memory-0.1.0/bench/bet2_goldset.py +1057 -0
- lean_memory-0.1.0/bench/phase2_escalation_probe.py +204 -0
- lean_memory-0.1.0/bench/phase2_eval.py +329 -0
- lean_memory-0.1.0/bench/phase2_ingest.py +470 -0
- lean_memory-0.1.0/bench/phase2_judge.py +232 -0
- lean_memory-0.1.0/bench/phase2_reader.py +114 -0
- lean_memory-0.1.0/bench/results/calibration/2026-07-bet2-revalidation.txt +59 -0
- lean_memory-0.1.0/bench/results/calibration/2026-07-escalation-baseline.json +511 -0
- lean_memory-0.1.0/bench/results/calibration/2026-07-escalation-postdrop-p1.json +22 -0
- lean_memory-0.1.0/bench/results/calibration/2026-07-escalation-postdrop-p2.json +24 -0
- lean_memory-0.1.0/bench/results/calibration/2026-07-escalation-postdrop-p3.json +23 -0
- lean_memory-0.1.0/bench/results/calibration/2026-07-escalation-postdrop-p4.json +23 -0
- lean_memory-0.1.0/bench/results/calibration/2026-07-escalation-postfix.json +631 -0
- lean_memory-0.1.0/bench/results/calibration/2026-07-granularity-sweep.json +95 -0
- lean_memory-0.1.0/bench/results/calibration/README.md +200 -0
- lean_memory-0.1.0/bench/results/phase2/.gitkeep +0 -0
- lean_memory-0.1.0/bench/results/phase2/dataset_pins.json +5 -0
- lean_memory-0.1.0/bench/smoke_quality.py +101 -0
- lean_memory-0.1.0/docs/assets/quickstart.gif +0 -0
- lean_memory-0.1.0/docs/assets/quickstart.tape +25 -0
- lean_memory-0.1.0/docs/benchmarks.md +191 -0
- lean_memory-0.1.0/docs/competitive-landscape.md +212 -0
- lean_memory-0.1.0/docs/launch-fixlist.md +97 -0
- lean_memory-0.1.0/docs/phase2-eval-plan.md +317 -0
- lean_memory-0.1.0/docs/phase2-learnings.md +130 -0
- lean_memory-0.1.0/docs/superpowers/phase2-HANDOFF.md +161 -0
- lean_memory-0.1.0/docs/superpowers/plans/2026-06-21-demo-agent.md +745 -0
- lean_memory-0.1.0/docs/superpowers/plans/2026-06-21-mcp-server.md +488 -0
- lean_memory-0.1.0/docs/superpowers/plans/2026-07-02-phase2-eval-harness.md +2201 -0
- lean_memory-0.1.0/docs/superpowers/plans/2026-07-08-launch-quality-gate.md +1062 -0
- lean_memory-0.1.0/docs/superpowers/specs/2026-07-02-phase2-eval-harness-design.md +249 -0
- lean_memory-0.1.0/docs/superpowers/specs/2026-07-08-strategic-direction-design.md +141 -0
- lean_memory-0.1.0/docs/superpowers/workpackets.md +414 -0
- lean_memory-0.1.0/examples/chat.py +177 -0
- lean_memory-0.1.0/examples/mcp_config.json +11 -0
- lean_memory-0.1.0/pyproject.toml +93 -0
- lean_memory-0.1.0/src/lean_memory/__init__.py +14 -0
- lean_memory-0.1.0/src/lean_memory/embed/__init__.py +0 -0
- lean_memory-0.1.0/src/lean_memory/embed/base.py +51 -0
- lean_memory-0.1.0/src/lean_memory/embed/fake.py +41 -0
- lean_memory-0.1.0/src/lean_memory/embed/sentence_transformer.py +65 -0
- lean_memory-0.1.0/src/lean_memory/extract/__init__.py +0 -0
- lean_memory-0.1.0/src/lean_memory/extract/contradiction.py +329 -0
- lean_memory-0.1.0/src/lean_memory/extract/gliner_extractor.py +483 -0
- lean_memory-0.1.0/src/lean_memory/extract/llm_typer.py +444 -0
- lean_memory-0.1.0/src/lean_memory/extract/router.py +418 -0
- lean_memory-0.1.0/src/lean_memory/extract/rules.py +130 -0
- lean_memory-0.1.0/src/lean_memory/extract/salience.py +123 -0
- lean_memory-0.1.0/src/lean_memory/extract/taxonomy.py +244 -0
- lean_memory-0.1.0/src/lean_memory/mcp_server.py +150 -0
- lean_memory-0.1.0/src/lean_memory/memory.py +203 -0
- lean_memory-0.1.0/src/lean_memory/retrieve/__init__.py +0 -0
- lean_memory-0.1.0/src/lean_memory/retrieve/rerank.py +59 -0
- lean_memory-0.1.0/src/lean_memory/retrieve/retriever.py +125 -0
- lean_memory-0.1.0/src/lean_memory/store/__init__.py +0 -0
- lean_memory-0.1.0/src/lean_memory/store/base.py +91 -0
- lean_memory-0.1.0/src/lean_memory/store/schema.py +83 -0
- lean_memory-0.1.0/src/lean_memory/store/sqlite_store.py +301 -0
- lean_memory-0.1.0/src/lean_memory/types.py +107 -0
- lean_memory-0.1.0/tests/fixtures/phase2/lme_oracle_mini.json +26 -0
- lean_memory-0.1.0/tests/fixtures/phase2/lme_s_mini.json +36 -0
- lean_memory-0.1.0/tests/fixtures/phase2/locomo_mini.json +23 -0
- lean_memory-0.1.0/tests/test_asof_sparse.py +46 -0
- lean_memory-0.1.0/tests/test_chat.py +225 -0
- lean_memory-0.1.0/tests/test_contradiction_extends.py +93 -0
- lean_memory-0.1.0/tests/test_embedder_default.py +25 -0
- lean_memory-0.1.0/tests/test_escalation_probe.py +64 -0
- lean_memory-0.1.0/tests/test_known_entities_cap.py +19 -0
- lean_memory-0.1.0/tests/test_mcp_server.py +269 -0
- lean_memory-0.1.0/tests/test_phase1_extraction.py +106 -0
- lean_memory-0.1.0/tests/test_phase2_eval.py +98 -0
- lean_memory-0.1.0/tests/test_phase2_ingest.py +212 -0
- lean_memory-0.1.0/tests/test_phase2_judge.py +70 -0
- lean_memory-0.1.0/tests/test_phase2_reader.py +41 -0
- lean_memory-0.1.0/tests/test_router.py +260 -0
- lean_memory-0.1.0/tests/test_search_now.py +39 -0
- lean_memory-0.1.0/tests/test_spine.py +98 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
name: Build sdist + wheel
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
|
|
14
|
+
- uses: actions/setup-python@v5
|
|
15
|
+
with:
|
|
16
|
+
python-version: "3.13"
|
|
17
|
+
|
|
18
|
+
- name: Build distributions
|
|
19
|
+
run: |
|
|
20
|
+
pip install build
|
|
21
|
+
python -m build
|
|
22
|
+
|
|
23
|
+
- name: Upload dist artifact
|
|
24
|
+
uses: actions/upload-artifact@v4
|
|
25
|
+
with:
|
|
26
|
+
name: dist
|
|
27
|
+
path: dist/
|
|
28
|
+
|
|
29
|
+
publish:
|
|
30
|
+
name: Publish to PyPI (Trusted Publishing)
|
|
31
|
+
needs: build
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
environment: pypi
|
|
34
|
+
permissions:
|
|
35
|
+
id-token: write
|
|
36
|
+
steps:
|
|
37
|
+
- name: Download dist artifact
|
|
38
|
+
uses: actions/download-artifact@v4
|
|
39
|
+
with:
|
|
40
|
+
name: dist
|
|
41
|
+
path: dist/
|
|
42
|
+
|
|
43
|
+
- name: Publish
|
|
44
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
# Cancel superseded runs on the same ref.
|
|
9
|
+
concurrency:
|
|
10
|
+
group: test-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
name: ${{ matrix.os }} · py${{ matrix.python-version }}
|
|
16
|
+
runs-on: ${{ matrix.os }}
|
|
17
|
+
strategy:
|
|
18
|
+
fail-fast: false
|
|
19
|
+
matrix:
|
|
20
|
+
os: [ubuntu-latest, macos-latest]
|
|
21
|
+
python-version: ["3.10", "3.13"]
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
# Provision Python via uv (python-build-standalone). Unlike
|
|
26
|
+
# actions/setup-python, these builds are compiled with loadable-sqlite
|
|
27
|
+
# extensions enabled, which sqlite-vec needs to load — the macOS
|
|
28
|
+
# setup-python builds are not, so extension loading fails there.
|
|
29
|
+
# One code path for both OSes.
|
|
30
|
+
- name: Set up uv + Python ${{ matrix.python-version }}
|
|
31
|
+
uses: astral-sh/setup-uv@v5
|
|
32
|
+
with:
|
|
33
|
+
python-version: ${{ matrix.python-version }}
|
|
34
|
+
enable-cache: true
|
|
35
|
+
|
|
36
|
+
# setup-uv with a python-version input already provisions and activates a
|
|
37
|
+
# venv (it exports VIRTUAL_ENV), so no explicit `uv venv` step is needed —
|
|
38
|
+
# the install below targets that activated environment.
|
|
39
|
+
- name: Install (editable, dev + mcp extras)
|
|
40
|
+
run: uv pip install -e '.[dev,mcp]'
|
|
41
|
+
|
|
42
|
+
- name: Run offline test suite
|
|
43
|
+
run: uv run pytest tests/ -q
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# lean-memory — Architecture & Status
|
|
2
|
+
|
|
3
|
+
Implementation status, design decisions, benchmark results, and known limitations.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Implementation Status
|
|
8
|
+
|
|
9
|
+
### Phase 0 — Storage & Retrieval
|
|
10
|
+
|
|
11
|
+
| Component | Status | Notes |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `Store` interface | ✅ | single abstraction; `SqliteStore` is the only impl |
|
|
14
|
+
| `SqliteStore` (vec0 + FTS5) | ✅ | one file per namespace (per-tenant isolation) |
|
|
15
|
+
| Monotemporal spine | ✅ | `valid_at`/`valid_to` + `is_latest` + ADD-only `superseded_by`; nothing is ever deleted |
|
|
16
|
+
| Two-stage Matryoshka dense | ✅ | coarse 256-dim KNN → exact 768-dim re-score |
|
|
17
|
+
| BM25 sparse arm | ✅ | FTS5 `bm25()` |
|
|
18
|
+
| RRF fusion (k=10) | ✅ | `Σ 1/(10 + rank)` |
|
|
19
|
+
| Mandatory rerank | ✅ | `IdentityReranker` (offline default) / `CrossEncoderReranker` (Ettin-32M) |
|
|
20
|
+
| Salience-decay re-score | ✅ | `0.6·rel + 0.2·recency + 0.2·importance`, `recency = exp(-λ·age)` |
|
|
21
|
+
| as-of temporal query | ✅ | world-time interval predicate |
|
|
22
|
+
| Pluggable embedder | ✅ | `FakeEmbedder` (offline) / `SentenceTransformerEmbedder` (Qwen3-0.6B) |
|
|
23
|
+
|
|
24
|
+
### Phase 1 — Hybrid Extraction
|
|
25
|
+
|
|
26
|
+
| Component | Status | Notes |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| Relation taxonomy | ✅ | `asserts`/`supersedes`/`extends`/`derives`; single shared `Candidate` contract |
|
|
29
|
+
| Pass 2 — candidate generation | ✅ | `StubCandidateGenerator` (offline) / `Gliner2Generator` (GLiNER2, `[extract]` extra) |
|
|
30
|
+
| Pass 3 — recall-biased router | ✅ | escalates low-conf / endpoint-scoped coref / possible-`derives` (2026-07 re-freeze: `prior_entity` trigger dropped — see "Escalation recalibration") |
|
|
31
|
+
| Pass 4 — LLM constrained typing | ✅ | `StubTyper` (offline) / `OllamaTyper` (local model, `[llm]` extra) |
|
|
32
|
+
| Contradiction → supersession | ✅ | cheap-then-escalate: slot → cosine → subsumption → LLM |
|
|
33
|
+
| Salience at write | ✅ | deterministic heuristic, rated once + cached on the `Fact` |
|
|
34
|
+
| BET-2 ablation harness | ✅ | `bench/bet2_ablation.py` — **BET-2 PASS**; re-frozen 2026-07 at `(typing=0.4, conf=0.4)` (−0.4pp delta, 7.6% escalation), superseding the 2026-06-21 freeze — see "Escalation recalibration (2026-07)" below |
|
|
35
|
+
|
|
36
|
+
### Phase 1 — Integrations
|
|
37
|
+
|
|
38
|
+
| Component | Status | Notes |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| MCP server | ✅ | `memory_add` / `memory_search` / `memory_clear` via FastMCP (stdio transport) |
|
|
41
|
+
| Terminal demo agent | ✅ | `examples/chat.py` — full add→retrieve→supersede loop with Claude |
|
|
42
|
+
|
|
43
|
+
### Phase 2 — Next
|
|
44
|
+
|
|
45
|
+
| Item | Status |
|
|
46
|
+
|---|---|
|
|
47
|
+
| Public benchmarks (LongMemEval / LoCoMo + frozen judge) | ⬜ deferred (post-launch) — harness complete, see `docs/superpowers/specs/2026-07-08-strategic-direction-design.md` |
|
|
48
|
+
| int8 vector storage | ⬜ blocked — sqlite-vec 0.1.9 insert path is broken upstream |
|
|
49
|
+
| `LanceStore` scale tier | ⬜ |
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Measured Performance
|
|
54
|
+
|
|
55
|
+
### Retrieval Quality (BET-1 evidence)
|
|
56
|
+
|
|
57
|
+
| Configuration | Top-1 | What it proves |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| Offline stubs (FakeEmbedder + IdentityReranker) | **1/5** | plumbing routes facts end-to-end (random vectors → chance result) |
|
|
60
|
+
| Real (Qwen3-Embedding-0.6B + Ettin-32M), 5-fact set | **4/5** | real models lift quality with zero code changes |
|
|
61
|
+
| Real, clean 3-fact set | **3/3** @ ~0.69 | the one toy-set miss was a small-corpus vocabulary artifact |
|
|
62
|
+
|
|
63
|
+
The pluggable-backend architecture is validated. These are sanity checks on small hand-built sets — a publishable quality claim needs LongMemEval/LoCoMo + a frozen judge (Phase 2).
|
|
64
|
+
|
|
65
|
+
Note: `google/embeddinggemma-300m` is a gated HF repo requiring license-accept. `Qwen3-Embedding-0.6B` is ungated and the stronger retrieval model (MTEB-R 64.65 vs 62.49) — use it instead.
|
|
66
|
+
|
|
67
|
+
### Extraction Quality — BET-2 (2026-06-21, n=97)
|
|
68
|
+
|
|
69
|
+
> **Superseded by the 2026-07 re-freeze** at operating point
|
|
70
|
+
> `(typing=0.4, conf=0.4)` — see "Escalation recalibration (2026-07)" below.
|
|
71
|
+
> The 2026-06-21 run below stays as the original PASS record; the frozen
|
|
72
|
+
> constants and escalation rate quoted here (`conf 0.5`-era, 10.1%) are historical.
|
|
73
|
+
|
|
74
|
+
**Metric A — Typer (`asserts` vs `derives`):**
|
|
75
|
+
|
|
76
|
+
| Arm | macro-F1 | derives-recall |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| 100%-LLM (qwen2.5:3b) | 0.474 [0.27, 0.68] | 0.20 |
|
|
79
|
+
| hybrid (StubTyper on direct bucket) | 0.474 [0.27, 0.68] | 0.20 |
|
|
80
|
+
|
|
81
|
+
**Metric B — Resolver (`asserts`/`extends`/`supersedes`) with real embeddings:**
|
|
82
|
+
|
|
83
|
+
| Arm | macro-F1 |
|
|
84
|
+
|---|---|
|
|
85
|
+
| both arms | **0.897** [0.73, 1.00] |
|
|
86
|
+
|
|
87
|
+
**Gate results:**
|
|
88
|
+
|
|
89
|
+
| Gate | Target | Result | Verdict |
|
|
90
|
+
|---|---|---|---|
|
|
91
|
+
| 1 — F1 delta (hybrid vs LLM, direct bucket) | ≤ 3pp | 0.0pp [0, 0] | ✅ PASS |
|
|
92
|
+
| 2 — escalation rate (Wilson upper) | < 20% | 10.1% [5.2%, 18.7%] | ✅ PASS |
|
|
93
|
+
| 3 — hybrid derives-recall not worse | ≥ LLM − 10pp | 0.20 ≥ 0.10 | ✅ PASS |
|
|
94
|
+
|
|
95
|
+
**BET-2 = PASS** (goldset sha256 `350b18b51a97fe57`).
|
|
96
|
+
|
|
97
|
+
### Escalation recalibration (2026-07)
|
|
98
|
+
|
|
99
|
+
The Phase 2 ingest surfaced that BET-2's <20% escalation gate held only on clean
|
|
100
|
+
goldset sentences: on **real LongMemEval conversational turns** the router routed
|
|
101
|
+
**95.9%** of candidates to the LLM (baseline probe, `bench/results/calibration/2026-07-escalation-baseline.json`;
|
|
102
|
+
best point typing=conf=0.3, 971/1012). Two confidence-independent router floors
|
|
103
|
+
drove it — `coreference` 65.6% and `prior_entity` 54.8% — so no threshold pair
|
|
104
|
+
could reach the target. Both were retired at the router:
|
|
105
|
+
|
|
106
|
+
- **Endpoint-scoped coref/ellipsis** — the pronoun/demonstrative scan narrowed
|
|
107
|
+
from the whole `fact_text` to the predicate endpoints. On real turns
|
|
108
|
+
`coreference` collapsed **664 → 1** (`2026-07-granularity-sweep.json`).
|
|
109
|
+
- **`prior_entity` trigger dropped** (two user-approved amendments: subject-only,
|
|
110
|
+
then removed entirely) — subject re-mention of a known entity is normal
|
|
111
|
+
discourse, not a hard case, and it stayed at 52.8% of real candidates even
|
|
112
|
+
scoped to the subject (`2026-07-escalation-postfix.json`). Entity linking is
|
|
113
|
+
deterministic by name; ambiguous refs still escalate via `coreference`,
|
|
114
|
+
inferential edges via `derives`.
|
|
115
|
+
|
|
116
|
+
Post-drop probe on real turns (8 namespaces / 192 turns / 704 candidates,
|
|
117
|
+
`2026-07-escalation-postdrop-p{1..4}.json`) selected the operating point:
|
|
118
|
+
|
|
119
|
+
| typing | conf | escalated/seen | rate | by_reason |
|
|
120
|
+
|---:|---:|---:|---:|---|
|
|
121
|
+
| **0.40** | **0.40** | **103/704** | **14.6%** | derives 102, coref 1 |
|
|
122
|
+
| 0.50 | 0.50 | 316/704 | 44.9% | pre_flagged 247, low_conf 247, derives 102, coref 1 |
|
|
123
|
+
|
|
124
|
+
Only `(0.4, 0.4)` clears the margin (raising either threshold to 0.5 pulls in the
|
|
125
|
+
247 candidates with model confidence in [0.4, 0.5)); the residual is
|
|
126
|
+
derives-dominated (the irreducible inferential edges the LLM must own).
|
|
127
|
+
|
|
128
|
+
**Granularity** (`2026-07-granularity-sweep.json`): GLiNER `DEFAULT_THRESHOLD`
|
|
129
|
+
0.1 → 0.4 cut over-generation **8.43 → 3.67 facts/turn** (decision rule: smallest
|
|
130
|
+
swept threshold meeting facts/turn ≤ 4). The `median_fact_len ≤ 160` target was
|
|
131
|
+
waived (user-approved) as threshold-insensitive — it sits at 171–187 chars across
|
|
132
|
+
the entire sweep, so no GLiNER threshold moves it.
|
|
133
|
+
|
|
134
|
+
**Re-frozen constants:** `DEFAULT_TYPING_THRESHOLD = 0.4`, router
|
|
135
|
+
`conf_threshold = 0.4`, `FROZEN_CONF_THRESHOLD = FROZEN_TYPING_THRESHOLD = 0.4`
|
|
136
|
+
(`bench/bet2_goldset.py`). **BET-2 three-gate revalidation at the frozen point
|
|
137
|
+
(`bench/bet2_ablation.py --real`, full output `2026-07-bet2-revalidation.txt`):**
|
|
138
|
+
|
|
139
|
+
| Gate | Target | Result | Verdict |
|
|
140
|
+
|---|---|---|---|
|
|
141
|
+
| 1 — direct-bucket paired F1 delta | upper ≤ 3.0pp | −0.4pp [−1.2, +0.0]pp | ✅ PASS |
|
|
142
|
+
| 2 — escalation rate (goldset, Wilson upper) | < 20% | 7.6% [3.5%, 15.6%] | ✅ PASS |
|
|
143
|
+
| 3 — hybrid derives-recall not worse | ≥ LLM − 10pp | 0.20 ≥ 0.10 | ✅ PASS |
|
|
144
|
+
|
|
145
|
+
**BET-2 re-freeze = PASS** (all three gates jointly). Metric B resolver macro-F1
|
|
146
|
+
unchanged at **0.897** (the router change touches only the typer/direct path).
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Bugs Found by the Benchmark
|
|
151
|
+
|
|
152
|
+
The measurement process found 4 real engine bugs before they shipped.
|
|
153
|
+
|
|
154
|
+
| # | Bug | Fix |
|
|
155
|
+
|---|---|---|
|
|
156
|
+
| 1 | **`extends` unreachable** — every non-identical object mapped to `supersedes` | Additive signal routing: additive cues ("also", multi-valued predicates) → `extends`; functional slots still supersede |
|
|
157
|
+
| 2 | **Resolver thresholds miscalibrated** — `HIGH=0.82`/`LOW=0.55` put Qwen3 refinements in the wrong band | Recalibrated to `HIGH=0.80`/`LOW=0.45`; verified 4/4 on real Qwen3 |
|
|
158
|
+
| 3 | **`OllamaTyper` label-set mismatch** — offering all 4 relations to a typer with no prior-slot context caused qwen2.5:3b to anchor on `extends` for everything | Constrained to `{asserts, derives}` |
|
|
159
|
+
| 4 | **Router `prior_entity` over-escalation** — "user" always in `known_entities` after turn 1, causing 73.7% false escalation | `self_entity` exemption in `_references_prior_entity()`; expanded `_KNOWN_PREDICATES`; gold set rebalanced 37→97 cases — **superseded 2026-07:** the `prior_entity` trigger was dropped entirely after real-turn data showed it still fired on 52.8% of candidates (see "Escalation recalibration (2026-07)") |
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Design Decisions
|
|
164
|
+
|
|
165
|
+
### ADD-only writes / supersession
|
|
166
|
+
|
|
167
|
+
Facts are never deleted or updated in place. A contradicting fact marks the old one `is_latest=False` via a `superseded_by` pointer and inserts the new one. This makes the full history auditable and enables point-in-time queries (`as_of=<timestamp_ms>`).
|
|
168
|
+
|
|
169
|
+
### Offline-first with pluggable backends
|
|
170
|
+
|
|
171
|
+
Every component has an offline stub that needs zero downloads and produces deterministic output. Real models are opt-in extras. This means the full offline test suite runs in seconds with no network access, and the architecture is validated independently of model availability.
|
|
172
|
+
|
|
173
|
+
### Cheap-then-escalate contradiction detection
|
|
174
|
+
|
|
175
|
+
The resolver tries slot match → cosine similarity → token subsumption before calling the LLM. Each step is ~100× cheaper than the next. Only genuinely ambiguous cases (high cosine, disjoint tokens) reach the LLM. The BET-2 gate 2 verifies the escalation rate stays under 20%.
|
|
176
|
+
|
|
177
|
+
### Per-namespace SQLite files
|
|
178
|
+
|
|
179
|
+
Each namespace (e.g. per-user) gets its own SQLite file rather than a shared database with a namespace column. This gives hard write isolation, trivial backup/export (copy one file), and clean multi-tenant semantics with no cross-tenant query risk.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Known Limitations
|
|
184
|
+
|
|
185
|
+
- **No benchmark-grade quality claim.** All numbers are on small hand-built sets. A real claim needs LongMemEval/LoCoMo + a frozen judge (Phase 2, deferred post-launch).
|
|
186
|
+
- **Recency on historical corpora — addressed (2026-07).** `exp(-λ·age)` collapses to ~0 for every fact when historical data is read years later, so the 0.2 recency term was dead weight on such corpora. `Memory.search(now=...)` now forwards an explicit search-time anchor so callers evaluating historical data can restore the recency signal; the wall-clock default is unchanged for live use.
|
|
187
|
+
- **Vectors stored float32, not int8.** sqlite-vec 0.1.9's int8 insert path is broken upstream. Flip when fixed (~0.2pt quality cost per BET-1, no correctness impact).
|
|
188
|
+
- **Inference typing is second-class without `[llm]`.** On the default `[mcp,models,extract]` path there is no Ollama typer, so the ~15% of candidates that escalate — almost entirely inferential (`derives`) facts — fall through to the deterministic `StubTyper` rather than a model. Assertional facts are unaffected; inference-type facts are second-class until you add the `[llm]` extra, which routes that escalated tier through real constrained typing.
|
|
189
|
+
- **Small-model ceiling.** qwen2.5:3b caps Typer accuracy. A larger local model would likely lift derives-recall. Untested.
|
|
190
|
+
- **Single machine, single run** for all real numbers. Reproducible, but not multi-seed/multi-judge as the spec's BET-5 demands.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Reproduce
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
# Offline (full suite, no downloads)
|
|
198
|
+
pip install -e '.[dev]' && pytest -q
|
|
199
|
+
|
|
200
|
+
# Real retrieval quality
|
|
201
|
+
pip install -e '.[models]'
|
|
202
|
+
python bench/smoke_quality.py --real --embedder Qwen/Qwen3-Embedding-0.6B
|
|
203
|
+
|
|
204
|
+
# Real BET-2 ablation
|
|
205
|
+
pip install -e '.[models,extract,llm]'
|
|
206
|
+
ollama serve & ollama pull qwen2.5:3b
|
|
207
|
+
python bench/bet2_ablation.py --real --decodes 3
|
|
208
|
+
```
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to lean-memory are documented here. The format is based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
5
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-07-12
|
|
8
|
+
|
|
9
|
+
First public release. lean-memory is an embedded, local-first agent-memory
|
|
10
|
+
engine: one SQLite file per namespace, hybrid dense+sparse retrieval with
|
|
11
|
+
rerank, and ADD-only supersession queryable at any past point in time
|
|
12
|
+
(`as_of`). No server, no daemon, no mandatory cloud key.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **MCP server** exposing memory as three tools (`memory_add`, `memory_search`,
|
|
17
|
+
`memory_clear`) for Claude Code, Claude Desktop, and other MCP clients.
|
|
18
|
+
Canonical install `pip install 'lean-memory[mcp,models,extract]'`
|
|
19
|
+
opportunistically upgrades each backend whose extra is present (real embedder
|
|
20
|
+
+ reranker via `[models]`, GLiNER2 extraction via `[extract]`) and otherwise
|
|
21
|
+
falls back to deterministic offline stubs. Two-minute quickstart with
|
|
22
|
+
copy-paste Claude Code / Claude Desktop config and a demo GIF.
|
|
23
|
+
- **`Memory.search(now=...)`** — recency decay now anchors to a caller-supplied
|
|
24
|
+
timestamp, so the 0.2 recency term is no longer dead on historical corpora.
|
|
25
|
+
- **Point-in-time queries** via `as_of` (epoch ms) with `is_latest_only=False`.
|
|
26
|
+
- **CI + release workflows** (GitHub Actions): offline test matrix on
|
|
27
|
+
ubuntu/macOS × Python 3.10/3.13, plus build-and-publish to PyPI on `v*` tag
|
|
28
|
+
via Trusted Publishing.
|
|
29
|
+
- PyPI metadata: keywords, classifiers, and project URLs.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- **Default embedder is now the ungated Qwen3-Embedding-0.6B** (was a gated
|
|
34
|
+
Gemma model that broke the `[models]` first run). Reranker default is
|
|
35
|
+
Ettin-32M; both are pinned ungated and covered by regression tests.
|
|
36
|
+
- **Escalation engine recalibrated on real conversational turns.** Endpoint-
|
|
37
|
+
scoped coreference/ellipsis detection replaces the whole-text pronoun scan
|
|
38
|
+
(coreference escalations dropped from 65.6% to effectively nil on real
|
|
39
|
+
turns), and the `prior_entity` trigger was retired (subject re-mention is
|
|
40
|
+
normal discourse, measured at 52.8% of candidates). At the re-frozen
|
|
41
|
+
`(typing_threshold=0.4, conf_threshold=0.4)` operating point, escalation on
|
|
42
|
+
the real LongMemEval probe is **14.6%** (was ~96% pre-fix), with the residual
|
|
43
|
+
being irreducible inferential-edge (`derives`) escalations. BET-2 three-gate
|
|
44
|
+
revalidation PASSes at this operating point.
|
|
45
|
+
- **Extraction granularity calibrated** — GLiNER candidate threshold set to
|
|
46
|
+
0.4, cutting the extractor from ~8 facts/turn to ~3.7 so `fact_text` reads as
|
|
47
|
+
facts rather than whole utterances.
|
|
48
|
+
- MCP server loads models lazily (first tool call rather than import) so a
|
|
49
|
+
cold-cache spawn answers the MCP handshake immediately instead of blocking on
|
|
50
|
+
a model download; search output is deduplicated.
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- Sparse BM25 retrieval arm now honors the `as_of` interval predicate.
|
|
55
|
+
- Known-entities handed to the router/typer are capped at the 100 most recent.
|
|
56
|
+
|
|
57
|
+
[0.1.0]: https://github.com/Wuesteon/lean-memory/releases/tag/v0.1.0
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
## ⚠️ START HERE: strategy is quality-gate → MCP launch (2026-07-08)
|
|
4
|
+
|
|
5
|
+
Approved direction: **close a small, hard quality gate on the first-run
|
|
6
|
+
experience, then launch across MCP channels** (Pure A). Read before any work:
|
|
7
|
+
|
|
8
|
+
1. **`docs/superpowers/specs/2026-07-08-strategic-direction-design.md`** — the
|
|
9
|
+
approved strategy (positioning, the six-item quality gate, launch plan, the
|
|
10
|
+
six-week post-launch demand read). Non-negotiable context; do not re-derive.
|
|
11
|
+
2. **`docs/superpowers/plans/2026-07-08-launch-quality-gate.md`** — the ordered
|
|
12
|
+
implementation plan that executes the gate.
|
|
13
|
+
|
|
14
|
+
State: the former "Phase 2 suspended, fix engine then re-run benchmarks" framing
|
|
15
|
+
is **superseded**. Engine-fix backlog items 1–3 are **FIXED** on `launch-gate`
|
|
16
|
+
(numbers are the source of truth in `bench/results/calibration/README.md`):
|
|
17
|
+
|
|
18
|
+
- **Escalation recalibration** — endpoint-scoped coref + `prior_entity` trigger
|
|
19
|
+
dropped (two user-approved amendments); real-turn escalation **95.9% → 14.6%**
|
|
20
|
+
at the frozen `(typing=0.4, conf=0.4)` operating point; goldset **10.1% → 7.6%**.
|
|
21
|
+
BET-2 revalidation PASSES all three gates.
|
|
22
|
+
- **Extraction granularity** — GLiNER `DEFAULT_THRESHOLD` 0.1 → 0.4;
|
|
23
|
+
**8.43 → 3.67 facts/turn**.
|
|
24
|
+
- **Recency anchoring** — `Memory.search(now=...)` forwards search-time now
|
|
25
|
+
(wall-clock default unchanged).
|
|
26
|
+
|
|
27
|
+
**Benchmark runs (LongMemEval/LoCoMo) are DEFERRED past the MCP launch** per the
|
|
28
|
+
spec — they are a post-launch credibility layer, not the critical path. The
|
|
29
|
+
harness (`bench/phase2_*.py`) is complete and needs no changes for the eventual
|
|
30
|
+
re-run.
|
|
31
|
+
|
|
32
|
+
**Remaining next steps = launch execution** per spec §3 (MCP Registry listing,
|
|
33
|
+
`awesome-mcp-servers` PR, Claude Code plugin marketplace, PyPI polish, Show HN,
|
|
34
|
+
subreddit posts); security housekeeping (rotate the OpenRouter key + HF token,
|
|
35
|
+
merge the harness branch) is gate item 5.
|
|
36
|
+
|
|
37
|
+
Historical context (dated, do not re-derive): `docs/phase2-learnings.md`
|
|
38
|
+
(assumptions vs. reality postmortem) and `docs/superpowers/phase2-HANDOFF.md`
|
|
39
|
+
(operational runbook + the now-fixed engine-fix backlog).
|
|
40
|
+
|
|
41
|
+
## Project
|
|
42
|
+
|
|
43
|
+
lean-memory: embedded, local-first agent-memory engine (SQLite vec0 + FTS5,
|
|
44
|
+
hybrid retrieval + rerank, ADD-only supersession with a monotemporal spine).
|
|
45
|
+
See `ARCHITECTURE.md` for the phase roadmap and BET results; `README.md` for
|
|
46
|
+
the user-facing quickstart.
|
|
47
|
+
|
|
48
|
+
- Python ≥3.10; dev venv at `.venv` (3.13). Run tests:
|
|
49
|
+
`.venv/bin/python -m pytest tests/ -q` (offline by default — all model
|
|
50
|
+
backends have deterministic stubs).
|
|
51
|
+
- Real model extras are opt-in: `[models]` (embedder+reranker), `[extract]`
|
|
52
|
+
(GLiNER2), `[llm]` (Ollama typer), `[bench]` (OpenRouter client).
|
|
53
|
+
- Benchmarks live in `bench/` (BET-2: `bet2_*.py`; Phase 2: `phase2_*.py`).
|
|
54
|
+
Frozen-config discipline: any number without a pinned config hash, judge
|
|
55
|
+
model, judge prompt, and backbone is not publishable.
|
|
56
|
+
- Roadmap work is clustered into worktree-sized packets with dependencies,
|
|
57
|
+
gates, and file-conflict lanes: `docs/superpowers/workpackets.md`. Claim a
|
|
58
|
+
packet there before starting it; one worktree per packet.
|
|
@@ -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
|
+
[](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
|
+

|
|
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
|