voltmem 0.2.0__tar.gz → 0.2.1__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.
- {voltmem-0.2.0 → voltmem-0.2.1}/PKG-INFO +61 -4
- {voltmem-0.2.0 → voltmem-0.2.1}/README.md +60 -3
- {voltmem-0.2.0 → voltmem-0.2.1}/pyproject.toml +1 -1
- voltmem-0.2.1/tests/test_classifiers.py +128 -0
- voltmem-0.2.1/tests/test_discovery.py +142 -0
- {voltmem-0.2.0 → voltmem-0.2.1}/tests/test_voltmem.py +153 -0
- {voltmem-0.2.0 → voltmem-0.2.1}/voltmem/__init__.py +26 -1
- voltmem-0.2.1/voltmem/classifiers.py +168 -0
- {voltmem-0.2.0 → voltmem-0.2.1}/voltmem/client.py +88 -10
- voltmem-0.2.1/voltmem/discovery.py +155 -0
- {voltmem-0.2.0 → voltmem-0.2.1}/voltmem/domains.py +62 -1
- {voltmem-0.2.0 → voltmem-0.2.1}/voltmem/extract.py +12 -3
- {voltmem-0.2.0 → voltmem-0.2.1}/voltmem/memory.py +67 -13
- {voltmem-0.2.0 → voltmem-0.2.1}/voltmem/scoring.py +83 -5
- {voltmem-0.2.0 → voltmem-0.2.1}/voltmem/store.py +55 -0
- {voltmem-0.2.0 → voltmem-0.2.1}/voltmem.egg-info/PKG-INFO +61 -4
- {voltmem-0.2.0 → voltmem-0.2.1}/voltmem.egg-info/SOURCES.txt +4 -0
- {voltmem-0.2.0 → voltmem-0.2.1}/LICENSE +0 -0
- {voltmem-0.2.0 → voltmem-0.2.1}/setup.cfg +0 -0
- {voltmem-0.2.0 → voltmem-0.2.1}/tests/test_client.py +0 -0
- {voltmem-0.2.0 → voltmem-0.2.1}/tests/test_vector_index.py +0 -0
- {voltmem-0.2.0 → voltmem-0.2.1}/voltmem/embeddings.py +0 -0
- {voltmem-0.2.0 → voltmem-0.2.1}/voltmem/integrations/__init__.py +0 -0
- {voltmem-0.2.0 → voltmem-0.2.1}/voltmem/integrations/langchain.py +0 -0
- {voltmem-0.2.0 → voltmem-0.2.1}/voltmem/vector_index.py +0 -0
- {voltmem-0.2.0 → voltmem-0.2.1}/voltmem.egg-info/dependency_links.txt +0 -0
- {voltmem-0.2.0 → voltmem-0.2.1}/voltmem.egg-info/requires.txt +0 -0
- {voltmem-0.2.0 → voltmem-0.2.1}/voltmem.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: voltmem
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Current-truth memory for LLM agents — protect stable facts, update volatile ones.
|
|
5
5
|
Author: VoltMem contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -31,7 +31,7 @@ Dynamic: license-file
|
|
|
31
31
|
|
|
32
32
|
# VoltMem
|
|
33
33
|
|
|
34
|
-
[](https://pypi.org/project/voltmem/)
|
|
35
35
|
[](https://pypi.org/project/voltmem/)
|
|
36
36
|
[](https://github.com/Rouche01/voltmem/blob/main/LICENSE)
|
|
37
37
|
|
|
@@ -47,7 +47,7 @@ volatile memories rank lower at search time.
|
|
|
47
47
|
|
|
48
48
|
> Mem0 remembers relevant facts. VoltMem remembers **current truth**.
|
|
49
49
|
|
|
50
|
-
**Research & benchmarks:** [docs/RESEARCH.md](docs/RESEARCH.md)
|
|
50
|
+
**Research & benchmarks:** [docs/RESEARCH.md](docs/RESEARCH.md) · **Known limits & roadmap:** [docs/OPEN_PROBLEMS.md](docs/OPEN_PROBLEMS.md)
|
|
51
51
|
|
|
52
52
|
---
|
|
53
53
|
|
|
@@ -133,6 +133,7 @@ flowchart LR
|
|
|
133
133
|
| User moves cities | Berlin and Paris both stored | **Updates** to current city |
|
|
134
134
|
| Old project name in haystack | Ranks by similarity | **Down-ranks** stale volatile facts |
|
|
135
135
|
| Confident wrong blip on stable pref | Often accepted | **Resists** corruption |
|
|
136
|
+
| Career / role change (medium-stable) | Often blocked or duplicated | **Updates** on strong explicit contradiction |
|
|
136
137
|
|
|
137
138
|
### Example results (reproducible)
|
|
138
139
|
|
|
@@ -168,6 +169,15 @@ Run locally with `pip install -e ".[embeddings]"`. Embeddings:
|
|
|
168
169
|
**VoltMem clearer wins: 3/3.** Mem0 keeps contradictory facts; VoltMem updates volatile
|
|
169
170
|
slots and protects stable prefs via domain volatility + slot-aware linking.
|
|
170
171
|
|
|
172
|
+
**`experiments/voltmem_eval.py`** — end-to-end escalation + retrieval (real vs flat vs swap):
|
|
173
|
+
|
|
174
|
+
| Battery | real profile | flat (equal V) | swap (inverted V) |
|
|
175
|
+
|---|---|---|---|
|
|
176
|
+
| A — selective updating | **20/20** | 15/20 | 7/20 |
|
|
177
|
+
| B — retrieval separation | **+0.589** | +0.202 | −0.267 |
|
|
178
|
+
|
|
179
|
+
Includes the `professional_context` career-change probe (strong explicit evidence → update).
|
|
180
|
+
|
|
171
181
|
**`experiments/memory_demo.py`** — 3 final Q&A checks vs ground truth:
|
|
172
182
|
|
|
173
183
|
| Policy | Score |
|
|
@@ -185,6 +195,7 @@ facts and **tracks weak-but-true updates** on volatile ones. Full distributions:
|
|
|
185
195
|
python examples/contradiction_demo.py
|
|
186
196
|
python experiments/mem0_comparison.py
|
|
187
197
|
python experiments/mem0_side_by_side.py # pip install mem0ai; OPENAI_API_KEY or MEM0_BACKEND=ollama
|
|
198
|
+
python experiments/voltmem_eval.py # 20/20 escalation probes + retrieval separation
|
|
188
199
|
python experiments/memory_demo.py
|
|
189
200
|
```
|
|
190
201
|
|
|
@@ -225,9 +236,24 @@ bob = create_memory("app.db", user_id="bob")
|
|
|
225
236
|
| `examples/contradiction_demo.py` | VoltMem vs always-add on contradictions |
|
|
226
237
|
| `experiments/mem0_comparison.py` | 3-scenario head-to-head vs always-add |
|
|
227
238
|
| `experiments/mem0_side_by_side.py` | 3-scenario head-to-head vs real Mem0 (3/3 wedge) |
|
|
239
|
+
| `experiments/voltmem_eval.py` | End-to-end escalation + retrieval (20/20 probes, real > flat > swap) |
|
|
240
|
+
| `experiments/calibrate_escalation.py` | Print E_t vs θ table for tuning explicit-override constants |
|
|
228
241
|
| `examples/quickstart_batteries.py` | `remember()` / `recall()` low-level API |
|
|
229
242
|
| `examples/multi_tenant.py` | One DB, many users |
|
|
230
243
|
| `examples/langchain_agent.py` | LangChain adapter |
|
|
244
|
+
| `examples/chat_app/` | Memory-aware CLI chat (extendable to web UI) |
|
|
245
|
+
| `examples/custom_classifier.py` | Pluggable ``KeywordClassifier`` + ``DomainRegistry`` |
|
|
246
|
+
|
|
247
|
+
### Chat app (CLI)
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
pip install -e ".[embeddings]"
|
|
251
|
+
python -m examples.chat_app # REPL; uses Ollama if running, else echo mode
|
|
252
|
+
python -m examples.chat_app --demo # scripted smoke test
|
|
253
|
+
python -m examples.chat_app --show-recall
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Slash commands: `/memories`, `/search <query>`, `/clear`, `/reset`, `/help`.
|
|
231
257
|
|
|
232
258
|
---
|
|
233
259
|
|
|
@@ -238,11 +264,41 @@ bob = create_memory("app.db", user_id="bob")
|
|
|
238
264
|
| `personality_trait` | 0.05 | Very protected |
|
|
239
265
|
| `core_preference` | 0.08 | Very protected |
|
|
240
266
|
| `biographical` | 0.10 | High protection |
|
|
267
|
+
| `professional_context` | 0.30 | Medium — job/role (career changes) |
|
|
268
|
+
| `location` | 0.60 | Updates readily (Berlin → Paris) |
|
|
241
269
|
| `current_project` | 0.55 | Updates readily |
|
|
242
270
|
| `emotional_context` | 0.80 | Fast-moving |
|
|
243
271
|
| `current_task` | 0.90 | Minimal protection |
|
|
244
272
|
|
|
245
|
-
Custom domains:
|
|
273
|
+
Custom domains: register via ``DomainRegistry`` and pass to ``create_memory(domains=...)``.
|
|
274
|
+
Pluggable classifiers: ``create_memory(classifier=...)`` — ``"heuristic"``, ``"llm"``, ``KeywordClassifier``, or a callable dict.
|
|
275
|
+
|
|
276
|
+
Optional: ``create_memory(..., auto_discover=True)`` learns per-domain volatility from
|
|
277
|
+
confirm / mismatch / supersede patterns (blended with priors above; cold-start applies).
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
from voltmem import create_memory, DomainRegistry, KeywordClassifier, ChainedClassifier, HeuristicClassifier
|
|
281
|
+
|
|
282
|
+
domains = DomainRegistry()
|
|
283
|
+
domains.register("style_preference", 0.08)
|
|
284
|
+
domains.register("style_constraint", 0.25)
|
|
285
|
+
|
|
286
|
+
mem = create_memory(
|
|
287
|
+
"app.db",
|
|
288
|
+
user_id="alice",
|
|
289
|
+
domains=domains,
|
|
290
|
+
classifier=ChainedClassifier([
|
|
291
|
+
KeywordClassifier({
|
|
292
|
+
"style_preference": ["prefer", "darker colors", "minimal"],
|
|
293
|
+
"style_constraint": ["no wool", "tight budget"],
|
|
294
|
+
}),
|
|
295
|
+
HeuristicClassifier(),
|
|
296
|
+
]),
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
mem.add("I prefer darker colors and minimal fits")
|
|
300
|
+
hits = mem.search("what colors does the user like?")
|
|
301
|
+
```
|
|
246
302
|
|
|
247
303
|
---
|
|
248
304
|
|
|
@@ -255,6 +311,7 @@ python tests/test_client.py
|
|
|
255
311
|
```
|
|
256
312
|
|
|
257
313
|
Experiments and benchmarks live in `experiments/` — see [docs/RESEARCH.md](docs/RESEARCH.md).
|
|
314
|
+
Open problems and roadmap: [docs/OPEN_PROBLEMS.md](docs/OPEN_PROBLEMS.md).
|
|
258
315
|
|
|
259
316
|
---
|
|
260
317
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# VoltMem
|
|
2
2
|
|
|
3
|
-
[](https://pypi.org/project/voltmem/)
|
|
4
4
|
[](https://pypi.org/project/voltmem/)
|
|
5
5
|
[](https://github.com/Rouche01/voltmem/blob/main/LICENSE)
|
|
6
6
|
|
|
@@ -16,7 +16,7 @@ volatile memories rank lower at search time.
|
|
|
16
16
|
|
|
17
17
|
> Mem0 remembers relevant facts. VoltMem remembers **current truth**.
|
|
18
18
|
|
|
19
|
-
**Research & benchmarks:** [docs/RESEARCH.md](docs/RESEARCH.md)
|
|
19
|
+
**Research & benchmarks:** [docs/RESEARCH.md](docs/RESEARCH.md) · **Known limits & roadmap:** [docs/OPEN_PROBLEMS.md](docs/OPEN_PROBLEMS.md)
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
@@ -102,6 +102,7 @@ flowchart LR
|
|
|
102
102
|
| User moves cities | Berlin and Paris both stored | **Updates** to current city |
|
|
103
103
|
| Old project name in haystack | Ranks by similarity | **Down-ranks** stale volatile facts |
|
|
104
104
|
| Confident wrong blip on stable pref | Often accepted | **Resists** corruption |
|
|
105
|
+
| Career / role change (medium-stable) | Often blocked or duplicated | **Updates** on strong explicit contradiction |
|
|
105
106
|
|
|
106
107
|
### Example results (reproducible)
|
|
107
108
|
|
|
@@ -137,6 +138,15 @@ Run locally with `pip install -e ".[embeddings]"`. Embeddings:
|
|
|
137
138
|
**VoltMem clearer wins: 3/3.** Mem0 keeps contradictory facts; VoltMem updates volatile
|
|
138
139
|
slots and protects stable prefs via domain volatility + slot-aware linking.
|
|
139
140
|
|
|
141
|
+
**`experiments/voltmem_eval.py`** — end-to-end escalation + retrieval (real vs flat vs swap):
|
|
142
|
+
|
|
143
|
+
| Battery | real profile | flat (equal V) | swap (inverted V) |
|
|
144
|
+
|---|---|---|---|
|
|
145
|
+
| A — selective updating | **20/20** | 15/20 | 7/20 |
|
|
146
|
+
| B — retrieval separation | **+0.589** | +0.202 | −0.267 |
|
|
147
|
+
|
|
148
|
+
Includes the `professional_context` career-change probe (strong explicit evidence → update).
|
|
149
|
+
|
|
140
150
|
**`experiments/memory_demo.py`** — 3 final Q&A checks vs ground truth:
|
|
141
151
|
|
|
142
152
|
| Policy | Score |
|
|
@@ -154,6 +164,7 @@ facts and **tracks weak-but-true updates** on volatile ones. Full distributions:
|
|
|
154
164
|
python examples/contradiction_demo.py
|
|
155
165
|
python experiments/mem0_comparison.py
|
|
156
166
|
python experiments/mem0_side_by_side.py # pip install mem0ai; OPENAI_API_KEY or MEM0_BACKEND=ollama
|
|
167
|
+
python experiments/voltmem_eval.py # 20/20 escalation probes + retrieval separation
|
|
157
168
|
python experiments/memory_demo.py
|
|
158
169
|
```
|
|
159
170
|
|
|
@@ -194,9 +205,24 @@ bob = create_memory("app.db", user_id="bob")
|
|
|
194
205
|
| `examples/contradiction_demo.py` | VoltMem vs always-add on contradictions |
|
|
195
206
|
| `experiments/mem0_comparison.py` | 3-scenario head-to-head vs always-add |
|
|
196
207
|
| `experiments/mem0_side_by_side.py` | 3-scenario head-to-head vs real Mem0 (3/3 wedge) |
|
|
208
|
+
| `experiments/voltmem_eval.py` | End-to-end escalation + retrieval (20/20 probes, real > flat > swap) |
|
|
209
|
+
| `experiments/calibrate_escalation.py` | Print E_t vs θ table for tuning explicit-override constants |
|
|
197
210
|
| `examples/quickstart_batteries.py` | `remember()` / `recall()` low-level API |
|
|
198
211
|
| `examples/multi_tenant.py` | One DB, many users |
|
|
199
212
|
| `examples/langchain_agent.py` | LangChain adapter |
|
|
213
|
+
| `examples/chat_app/` | Memory-aware CLI chat (extendable to web UI) |
|
|
214
|
+
| `examples/custom_classifier.py` | Pluggable ``KeywordClassifier`` + ``DomainRegistry`` |
|
|
215
|
+
|
|
216
|
+
### Chat app (CLI)
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
pip install -e ".[embeddings]"
|
|
220
|
+
python -m examples.chat_app # REPL; uses Ollama if running, else echo mode
|
|
221
|
+
python -m examples.chat_app --demo # scripted smoke test
|
|
222
|
+
python -m examples.chat_app --show-recall
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Slash commands: `/memories`, `/search <query>`, `/clear`, `/reset`, `/help`.
|
|
200
226
|
|
|
201
227
|
---
|
|
202
228
|
|
|
@@ -207,11 +233,41 @@ bob = create_memory("app.db", user_id="bob")
|
|
|
207
233
|
| `personality_trait` | 0.05 | Very protected |
|
|
208
234
|
| `core_preference` | 0.08 | Very protected |
|
|
209
235
|
| `biographical` | 0.10 | High protection |
|
|
236
|
+
| `professional_context` | 0.30 | Medium — job/role (career changes) |
|
|
237
|
+
| `location` | 0.60 | Updates readily (Berlin → Paris) |
|
|
210
238
|
| `current_project` | 0.55 | Updates readily |
|
|
211
239
|
| `emotional_context` | 0.80 | Fast-moving |
|
|
212
240
|
| `current_task` | 0.90 | Minimal protection |
|
|
213
241
|
|
|
214
|
-
Custom domains:
|
|
242
|
+
Custom domains: register via ``DomainRegistry`` and pass to ``create_memory(domains=...)``.
|
|
243
|
+
Pluggable classifiers: ``create_memory(classifier=...)`` — ``"heuristic"``, ``"llm"``, ``KeywordClassifier``, or a callable dict.
|
|
244
|
+
|
|
245
|
+
Optional: ``create_memory(..., auto_discover=True)`` learns per-domain volatility from
|
|
246
|
+
confirm / mismatch / supersede patterns (blended with priors above; cold-start applies).
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
from voltmem import create_memory, DomainRegistry, KeywordClassifier, ChainedClassifier, HeuristicClassifier
|
|
250
|
+
|
|
251
|
+
domains = DomainRegistry()
|
|
252
|
+
domains.register("style_preference", 0.08)
|
|
253
|
+
domains.register("style_constraint", 0.25)
|
|
254
|
+
|
|
255
|
+
mem = create_memory(
|
|
256
|
+
"app.db",
|
|
257
|
+
user_id="alice",
|
|
258
|
+
domains=domains,
|
|
259
|
+
classifier=ChainedClassifier([
|
|
260
|
+
KeywordClassifier({
|
|
261
|
+
"style_preference": ["prefer", "darker colors", "minimal"],
|
|
262
|
+
"style_constraint": ["no wool", "tight budget"],
|
|
263
|
+
}),
|
|
264
|
+
HeuristicClassifier(),
|
|
265
|
+
]),
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
mem.add("I prefer darker colors and minimal fits")
|
|
269
|
+
hits = mem.search("what colors does the user like?")
|
|
270
|
+
```
|
|
215
271
|
|
|
216
272
|
---
|
|
217
273
|
|
|
@@ -224,6 +280,7 @@ python tests/test_client.py
|
|
|
224
280
|
```
|
|
225
281
|
|
|
226
282
|
Experiments and benchmarks live in `experiments/` — see [docs/RESEARCH.md](docs/RESEARCH.md).
|
|
283
|
+
Open problems and roadmap: [docs/OPEN_PROBLEMS.md](docs/OPEN_PROBLEMS.md).
|
|
227
284
|
|
|
228
285
|
---
|
|
229
286
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Tests for pluggable classifiers and DomainRegistry."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
8
|
+
sys.path.insert(0, str(ROOT))
|
|
9
|
+
|
|
10
|
+
from voltmem import ( # noqa: E402
|
|
11
|
+
ChainedClassifier,
|
|
12
|
+
DomainRegistry,
|
|
13
|
+
HeuristicClassifier,
|
|
14
|
+
KeywordClassifier,
|
|
15
|
+
create_memory,
|
|
16
|
+
)
|
|
17
|
+
from voltmem import domains as dom # noqa: E402
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_keyword_classifier_matches_custom_domain():
|
|
21
|
+
domains = DomainRegistry()
|
|
22
|
+
domains.register("style_preference", 0.08)
|
|
23
|
+
restore = domains.install()
|
|
24
|
+
try:
|
|
25
|
+
clf = KeywordClassifier({
|
|
26
|
+
"style_preference": ["darker colors", "minimal style"],
|
|
27
|
+
})
|
|
28
|
+
assert clf.classify_domain("I prefer darker colors") == "style_preference"
|
|
29
|
+
assert clf.match_domain("hello there") is None
|
|
30
|
+
finally:
|
|
31
|
+
restore()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_chained_classifier_falls_through_to_heuristic():
|
|
35
|
+
domains = DomainRegistry()
|
|
36
|
+
domains.register("style_constraint", 0.25)
|
|
37
|
+
restore = domains.install()
|
|
38
|
+
try:
|
|
39
|
+
clf = ChainedClassifier([
|
|
40
|
+
KeywordClassifier({"style_constraint": ["no wool", "tight budget"]}),
|
|
41
|
+
HeuristicClassifier(),
|
|
42
|
+
])
|
|
43
|
+
assert clf.classify_domain("no wool please") == "style_constraint"
|
|
44
|
+
assert clf.classify_domain("I live in Berlin") == "location"
|
|
45
|
+
finally:
|
|
46
|
+
restore()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_callable_classifier_dict():
|
|
50
|
+
with create_memory(
|
|
51
|
+
":memory:",
|
|
52
|
+
user_id="u1",
|
|
53
|
+
embeddings=False,
|
|
54
|
+
classifier={
|
|
55
|
+
"classify": lambda t: "custom_domain"
|
|
56
|
+
if "stylens" in t.lower() else "stated_preference",
|
|
57
|
+
"mismatch": lambda n, e, s: 0.05 if s > 0.8 else 0.7,
|
|
58
|
+
},
|
|
59
|
+
domains=DomainRegistry().register("custom_domain", 0.4),
|
|
60
|
+
) as mem:
|
|
61
|
+
row = mem.add("stylens user prefers navy")
|
|
62
|
+
assert row["domain"] == "custom_domain"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_create_memory_classifier_string_llm_alias():
|
|
66
|
+
# Should not raise — builds LLM classifier (may fall back on classify)
|
|
67
|
+
with create_memory(
|
|
68
|
+
":memory:",
|
|
69
|
+
classifier="llm",
|
|
70
|
+
embeddings=False,
|
|
71
|
+
) as mem:
|
|
72
|
+
assert mem.layer._extractor is not None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_domain_registry_restored_on_close():
|
|
76
|
+
before = dict(dom.DOMAIN_VOLATILITY)
|
|
77
|
+
reg = DomainRegistry().register("ephemeral_domain", 0.33, slot=True)
|
|
78
|
+
with create_memory(":memory:", domains=reg, embeddings=False) as mem:
|
|
79
|
+
assert "ephemeral_domain" in dom.DOMAIN_VOLATILITY
|
|
80
|
+
assert "ephemeral_domain" in dom.SLOT_DOMAINS
|
|
81
|
+
mem.add("ephemeral fact", extract=False)
|
|
82
|
+
assert dom.DOMAIN_VOLATILITY == before
|
|
83
|
+
assert "ephemeral_domain" not in dom.SLOT_DOMAINS
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_domain_registry_custom_domain_classified():
|
|
87
|
+
domains = DomainRegistry()
|
|
88
|
+
domains.register("style_preference", 0.08)
|
|
89
|
+
classifier = KeywordClassifier({
|
|
90
|
+
"style_preference": ["darker colors", "minimal style"],
|
|
91
|
+
})
|
|
92
|
+
with create_memory(
|
|
93
|
+
":memory:",
|
|
94
|
+
domains=domains,
|
|
95
|
+
classifier=classifier,
|
|
96
|
+
embeddings=False,
|
|
97
|
+
) as mem:
|
|
98
|
+
row = mem.add("I love minimal style and darker colors")
|
|
99
|
+
assert row["domain"] == "style_preference"
|
|
100
|
+
assert "style_preference" in domains.known_domains()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_legacy_extractor_kwarg_still_works():
|
|
104
|
+
custom = HeuristicClassifier(relate_similarity=0.6)
|
|
105
|
+
with create_memory(":memory:", extractor=custom, embeddings=False) as mem:
|
|
106
|
+
assert mem.layer._extractor is custom
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
tests = [
|
|
111
|
+
test_keyword_classifier_matches_custom_domain,
|
|
112
|
+
test_chained_classifier_falls_through_to_heuristic,
|
|
113
|
+
test_callable_classifier_dict,
|
|
114
|
+
test_create_memory_classifier_string_llm_alias,
|
|
115
|
+
test_domain_registry_restored_on_close,
|
|
116
|
+
test_domain_registry_custom_domain_classified,
|
|
117
|
+
test_legacy_extractor_kwarg_still_works,
|
|
118
|
+
]
|
|
119
|
+
failed = 0
|
|
120
|
+
for t in tests:
|
|
121
|
+
try:
|
|
122
|
+
t()
|
|
123
|
+
print(f" PASS {t.__name__}")
|
|
124
|
+
except Exception as exc:
|
|
125
|
+
failed += 1
|
|
126
|
+
print(f" FAIL {t.__name__}: {exc}")
|
|
127
|
+
print(f"\n{len(tests) - failed}/{len(tests)} passed")
|
|
128
|
+
raise SystemExit(1 if failed else 0)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Tests for domain auto-discovery (Tier 1)."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
9
|
+
sys.path.insert(0, str(ROOT))
|
|
10
|
+
|
|
11
|
+
from voltmem import ( # noqa: E402
|
|
12
|
+
MemoryLayer,
|
|
13
|
+
DomainStats,
|
|
14
|
+
VolatilityTracker,
|
|
15
|
+
blend_volatility,
|
|
16
|
+
update_volatility_ema,
|
|
17
|
+
VOL_DRIFT_MAX,
|
|
18
|
+
DOMAIN_VOLATILITY,
|
|
19
|
+
)
|
|
20
|
+
from voltmem.domains import MemoryItem # noqa: E402
|
|
21
|
+
from voltmem.scoring import should_escalate # noqa: E402
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_domain_stats_empirical_volatility_rises_with_supersedes():
|
|
25
|
+
stats = DomainStats(domain="location")
|
|
26
|
+
for _ in range(5):
|
|
27
|
+
stats.record("audited", mismatch=0.8)
|
|
28
|
+
assert stats.empirical_volatility > 0.4
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_blend_volatility_requires_min_observations():
|
|
32
|
+
prior = 0.08
|
|
33
|
+
assert blend_volatility(prior, 0.9, n_observations=1) == prior
|
|
34
|
+
blended = blend_volatility(prior, 0.9, n_observations=10)
|
|
35
|
+
assert blended > prior
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_prior_anchored_ema_clamps_drift():
|
|
39
|
+
item = MemoryItem(
|
|
40
|
+
id="x",
|
|
41
|
+
content="pref",
|
|
42
|
+
domain="personality_trait",
|
|
43
|
+
source="weak_inference",
|
|
44
|
+
volatility_ema=-1.0,
|
|
45
|
+
created_at=0.0,
|
|
46
|
+
last_confirmed_at=0.0,
|
|
47
|
+
)
|
|
48
|
+
prior = DOMAIN_VOLATILITY["personality_trait"]
|
|
49
|
+
updated = update_volatility_ema(item, observed_mismatch=0.9, source="weak_inference")
|
|
50
|
+
assert updated <= prior + VOL_DRIFT_MAX + 1e-9
|
|
51
|
+
assert updated >= prior - VOL_DRIFT_MAX - 1e-9
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_auto_discover_records_observations():
|
|
55
|
+
with MemoryLayer(":memory:", auto_discover=True) as mem:
|
|
56
|
+
mem.write("I live in Berlin", domain="location")
|
|
57
|
+
mem.observe(
|
|
58
|
+
"I moved to Paris",
|
|
59
|
+
domain="location",
|
|
60
|
+
mismatch_magnitude=0.85,
|
|
61
|
+
source="explicit_statement",
|
|
62
|
+
)
|
|
63
|
+
summary = mem.summary()
|
|
64
|
+
assert summary["auto_discover"] is True
|
|
65
|
+
loc = summary["domain_discovery"]["location"]
|
|
66
|
+
assert loc["n_supersedes"] >= 1
|
|
67
|
+
assert loc["empirical"] > 0.0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_auto_discover_persists_across_sessions():
|
|
71
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
72
|
+
db = os.path.join(tmp, "discover.db")
|
|
73
|
+
with MemoryLayer(db, namespace="alice", auto_discover=True) as mem:
|
|
74
|
+
mem.write("I prefer concise answers", domain="core_preference")
|
|
75
|
+
for _ in range(4):
|
|
76
|
+
mem.observe(
|
|
77
|
+
"I prefer concise answers",
|
|
78
|
+
domain="core_preference",
|
|
79
|
+
mismatch_magnitude=0.05,
|
|
80
|
+
source="explicit_statement",
|
|
81
|
+
)
|
|
82
|
+
with MemoryLayer(db, namespace="alice", auto_discover=True) as mem2:
|
|
83
|
+
stats = mem2.summary()["domain_discovery"]["core_preference"]
|
|
84
|
+
assert stats["n_confirms"] >= 4
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_auto_discover_off_by_default():
|
|
88
|
+
with MemoryLayer(":memory:") as mem:
|
|
89
|
+
assert mem.auto_discover is False
|
|
90
|
+
assert mem._tracker is None
|
|
91
|
+
mem.write("I live in Berlin", domain="location")
|
|
92
|
+
summary = mem.summary()
|
|
93
|
+
assert summary["domain_discovery"] == {}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_weak_inference_does_not_supersede_stable_with_auto_discover():
|
|
97
|
+
"""Repeated weak contradictions should not corrupt a stable domain."""
|
|
98
|
+
with MemoryLayer(":memory:", auto_discover=True) as mem:
|
|
99
|
+
mem.write("User is a careful planner", domain="personality_trait")
|
|
100
|
+
for _ in range(8):
|
|
101
|
+
res = mem.observe(
|
|
102
|
+
"User did something out of character",
|
|
103
|
+
domain="personality_trait",
|
|
104
|
+
mismatch_magnitude=0.6,
|
|
105
|
+
source="weak_inference",
|
|
106
|
+
)
|
|
107
|
+
assert res.action == "logged_mismatch"
|
|
108
|
+
items = mem._active(domain="personality_trait")
|
|
109
|
+
assert len(items) == 1
|
|
110
|
+
assert "careful planner" in items[0].content
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_create_memory_auto_discover_flag():
|
|
114
|
+
from voltmem import create_memory
|
|
115
|
+
|
|
116
|
+
with create_memory(":memory:", auto_discover=True, embeddings=False) as mem:
|
|
117
|
+
assert mem.layer.auto_discover is True
|
|
118
|
+
row = mem.add("I live in Berlin")
|
|
119
|
+
assert row["action"] == "inserted"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if __name__ == "__main__":
|
|
123
|
+
tests = [
|
|
124
|
+
test_domain_stats_empirical_volatility_rises_with_supersedes,
|
|
125
|
+
test_blend_volatility_requires_min_observations,
|
|
126
|
+
test_prior_anchored_ema_clamps_drift,
|
|
127
|
+
test_auto_discover_records_observations,
|
|
128
|
+
test_auto_discover_persists_across_sessions,
|
|
129
|
+
test_auto_discover_off_by_default,
|
|
130
|
+
test_weak_inference_does_not_supersede_stable_with_auto_discover,
|
|
131
|
+
test_create_memory_auto_discover_flag,
|
|
132
|
+
]
|
|
133
|
+
failed = 0
|
|
134
|
+
for t in tests:
|
|
135
|
+
try:
|
|
136
|
+
t()
|
|
137
|
+
print(f" PASS {t.__name__}")
|
|
138
|
+
except Exception as exc:
|
|
139
|
+
failed += 1
|
|
140
|
+
print(f" FAIL {t.__name__}: {exc}")
|
|
141
|
+
print(f"\n{len(tests) - failed}/{len(tests)} passed")
|
|
142
|
+
raise SystemExit(1 if failed else 0)
|