typedmem 0.4.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.
- typedmem-0.4.0/LICENSE +21 -0
- typedmem-0.4.0/PKG-INFO +279 -0
- typedmem-0.4.0/README.md +237 -0
- typedmem-0.4.0/pyproject.toml +51 -0
- typedmem-0.4.0/setup.cfg +4 -0
- typedmem-0.4.0/tests/test_cli.py +47 -0
- typedmem-0.4.0/tests/test_conflicts.py +134 -0
- typedmem-0.4.0/tests/test_embeddings.py +34 -0
- typedmem-0.4.0/tests/test_evolvers_contradictions.py +74 -0
- typedmem-0.4.0/tests/test_evolvers_drift.py +83 -0
- typedmem-0.4.0/tests/test_evolvers_goals.py +88 -0
- typedmem-0.4.0/tests/test_evolvers_summary.py +105 -0
- typedmem-0.4.0/tests/test_extractor.py +30 -0
- typedmem-0.4.0/tests/test_extractor_with_profile.py +104 -0
- typedmem-0.4.0/tests/test_jsonl_store.py +58 -0
- typedmem-0.4.0/tests/test_llm_clients.py +53 -0
- typedmem-0.4.0/tests/test_llm_extractor.py +186 -0
- typedmem-0.4.0/tests/test_migration.py +108 -0
- typedmem-0.4.0/tests/test_policy.py +32 -0
- typedmem-0.4.0/tests/test_profile_loading.py +40 -0
- typedmem-0.4.0/tests/test_profiles.py +174 -0
- typedmem-0.4.0/tests/test_replace_bookkeeping.py +42 -0
- typedmem-0.4.0/tests/test_retriever.py +45 -0
- typedmem-0.4.0/tests/test_retriever_semantic.py +53 -0
- typedmem-0.4.0/tests/test_schema.py +32 -0
- typedmem-0.4.0/tests/test_source.py +58 -0
- typedmem-0.4.0/tests/test_sqlite_store.py +54 -0
- typedmem-0.4.0/tests/test_store.py +32 -0
- typedmem-0.4.0/tests/test_store_with_profile.py +64 -0
- typedmem-0.4.0/tests/test_workspaces.py +76 -0
- typedmem-0.4.0/typedmem/__init__.py +77 -0
- typedmem-0.4.0/typedmem/cli.py +316 -0
- typedmem-0.4.0/typedmem/embeddings.py +86 -0
- typedmem-0.4.0/typedmem/evolvers/__init__.py +23 -0
- typedmem-0.4.0/typedmem/evolvers/base.py +84 -0
- typedmem-0.4.0/typedmem/evolvers/contradictions.py +82 -0
- typedmem-0.4.0/typedmem/evolvers/drift.py +97 -0
- typedmem-0.4.0/typedmem/evolvers/goals.py +127 -0
- typedmem-0.4.0/typedmem/evolvers/summary.py +179 -0
- typedmem-0.4.0/typedmem/extractor.py +370 -0
- typedmem-0.4.0/typedmem/llm/__init__.py +9 -0
- typedmem-0.4.0/typedmem/llm/anthropic.py +46 -0
- typedmem-0.4.0/typedmem/llm/base.py +11 -0
- typedmem-0.4.0/typedmem/llm/fake.py +40 -0
- typedmem-0.4.0/typedmem/llm/openai.py +41 -0
- typedmem-0.4.0/typedmem/policy.py +160 -0
- typedmem-0.4.0/typedmem/profiles/__init__.py +11 -0
- typedmem-0.4.0/typedmem/profiles/base.py +208 -0
- typedmem-0.4.0/typedmem/profiles/builtins.py +475 -0
- typedmem-0.4.0/typedmem/profiles/loaders.py +23 -0
- typedmem-0.4.0/typedmem/prompts.py +84 -0
- typedmem-0.4.0/typedmem/retriever.py +187 -0
- typedmem-0.4.0/typedmem/schema.py +127 -0
- typedmem-0.4.0/typedmem/source.py +89 -0
- typedmem-0.4.0/typedmem/stores/__init__.py +6 -0
- typedmem-0.4.0/typedmem/stores/base.py +198 -0
- typedmem-0.4.0/typedmem/stores/jsonl.py +92 -0
- typedmem-0.4.0/typedmem/stores/memory.py +37 -0
- typedmem-0.4.0/typedmem/stores/sqlite.py +239 -0
- typedmem-0.4.0/typedmem.egg-info/PKG-INFO +279 -0
- typedmem-0.4.0/typedmem.egg-info/SOURCES.txt +63 -0
- typedmem-0.4.0/typedmem.egg-info/dependency_links.txt +1 -0
- typedmem-0.4.0/typedmem.egg-info/entry_points.txt +2 -0
- typedmem-0.4.0/typedmem.egg-info/requires.txt +21 -0
- typedmem-0.4.0/typedmem.egg-info/top_level.txt +1 -0
typedmem-0.4.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ruxiz
|
|
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.
|
typedmem-0.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: typedmem
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Typed, policy-aware, evolving memory layer for AI agents.
|
|
5
|
+
Author: ruxiz
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/canis-minor/typedmem
|
|
8
|
+
Project-URL: Documentation, https://github.com/canis-minor/typedmem#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/canis-minor/typedmem
|
|
10
|
+
Project-URL: Issues, https://github.com/canis-minor/typedmem/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/canis-minor/typedmem/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: ai,memory,agents,llm,knowledge,rag,anthropic,openai
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Provides-Extra: test
|
|
27
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
28
|
+
Provides-Extra: openai
|
|
29
|
+
Requires-Dist: openai>=1.0; extra == "openai"
|
|
30
|
+
Provides-Extra: anthropic
|
|
31
|
+
Requires-Dist: anthropic>=0.34; extra == "anthropic"
|
|
32
|
+
Provides-Extra: yaml
|
|
33
|
+
Requires-Dist: PyYAML>=6; extra == "yaml"
|
|
34
|
+
Provides-Extra: docs
|
|
35
|
+
Requires-Dist: mkdocs>=1.5; extra == "docs"
|
|
36
|
+
Requires-Dist: mkdocs-material>=9.5; extra == "docs"
|
|
37
|
+
Provides-Extra: all
|
|
38
|
+
Requires-Dist: openai>=1.0; extra == "all"
|
|
39
|
+
Requires-Dist: anthropic>=0.34; extra == "all"
|
|
40
|
+
Requires-Dist: PyYAML>=6; extra == "all"
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
# TypedMemory
|
|
44
|
+
|
|
45
|
+
**Typed, policy-aware, evolving memory layer for AI agents.**
|
|
46
|
+
|
|
47
|
+
[](https://github.com/canis-minor/typedmem/actions/workflows/ci.yml)
|
|
48
|
+
[](https://pypi.org/project/typedmem/)
|
|
49
|
+
[](https://pypi.org/project/typedmem/)
|
|
50
|
+
[](LICENSE)
|
|
51
|
+
|
|
52
|
+
TypedMemory is the layer that sits between data and reasoning. Every memory has a **type** (claim, decision, observation, …), a **confidence**, a **structured source**, a **lifecycle policy**, and a **workspace** — not just a string in a vector database. Memories know how to update themselves on conflict, how to decay, and how to be summarized.
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
┌──────────────────┐
|
|
56
|
+
│ DomainProfile │ ← schema: which types,
|
|
57
|
+
│ TypeSpec × N │ which policies,
|
|
58
|
+
│ prompt + rules │ which validations
|
|
59
|
+
└────────┬─────────┘
|
|
60
|
+
│
|
|
61
|
+
text ──► Extractor ──► Memory ──┴──► MemoryStore ──► Retriever
|
|
62
|
+
│
|
|
63
|
+
▼
|
|
64
|
+
Evolver
|
|
65
|
+
(contradictions, drift, goals,
|
|
66
|
+
non-destructive summarization)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Zero runtime dependencies.** Stdlib only. LLM clients, YAML profile loading, and sentence-transformer-style retrieval are optional extras.
|
|
70
|
+
|
|
71
|
+
## Why this exists
|
|
72
|
+
|
|
73
|
+
Most "AI memory" libraries are wrappers around a vector database. That's fine for "remember what the user said," but it falls apart the moment you want an agent to:
|
|
74
|
+
|
|
75
|
+
- track **who said what, in which document, at which span** (provenance)
|
|
76
|
+
- handle **the same fact from three sources** without storing it three times (reinforcement)
|
|
77
|
+
- recognize that **a new decision supersedes the old one** without losing the audit trail
|
|
78
|
+
- **summarize stale events** without throwing away the originals
|
|
79
|
+
- **isolate** legal memory from medical memory on the same machine
|
|
80
|
+
- **flag contradictions** instead of silently overwriting them
|
|
81
|
+
|
|
82
|
+
TypedMemory handles these as first-class concepts, not bolt-ons.
|
|
83
|
+
|
|
84
|
+
## Install
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pip install typedmem
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Optional extras:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pip install 'typedmem[anthropic]' # AnthropicClient
|
|
94
|
+
pip install 'typedmem[openai]' # OpenAIClient
|
|
95
|
+
pip install 'typedmem[yaml]' # DomainProfile.from_yaml()
|
|
96
|
+
pip install 'typedmem[all]'
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Python 3.10+.
|
|
100
|
+
|
|
101
|
+
## 60-second demo: an engineering design agent
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
import json
|
|
105
|
+
from typedmem import (
|
|
106
|
+
DomainProfile, FakeClient, LLMExtractor, SQLiteMemoryStore,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
profile = DomainProfile.builtin("engineering_design")
|
|
110
|
+
store = SQLiteMemoryStore.for_profile(profile, "design.db")
|
|
111
|
+
|
|
112
|
+
# Pretend the LLM extracted these from your design docs.
|
|
113
|
+
extractor = LLMExtractor(client=FakeClient([
|
|
114
|
+
json.dumps([
|
|
115
|
+
{"type": "decision", "content": "Use SQLite for storage",
|
|
116
|
+
"subject": "storage_backend", "confidence": 0.9,
|
|
117
|
+
"source": {"document_id": "design_v1.md"}},
|
|
118
|
+
{"type": "risk", "content": "SQLite is single-writer",
|
|
119
|
+
"subject": "storage_backend", "confidence": 0.8,
|
|
120
|
+
"source": {"document_id": "design_v1.md"}},
|
|
121
|
+
]),
|
|
122
|
+
json.dumps([
|
|
123
|
+
{"type": "decision", "content": "Switch to PostgreSQL for concurrent writes",
|
|
124
|
+
"subject": "storage_backend", "confidence": 0.9,
|
|
125
|
+
"source": {"document_id": "design_v2.md"}},
|
|
126
|
+
{"type": "risk", "content": "Postgres adds an external service",
|
|
127
|
+
"subject": "storage_backend", "confidence": 0.85,
|
|
128
|
+
"source": {"document_id": "design_v2.md"}},
|
|
129
|
+
]),
|
|
130
|
+
]), profile=profile)
|
|
131
|
+
|
|
132
|
+
for snippet in ("v1 text", "v2 text"):
|
|
133
|
+
for m in extractor.extract(snippet):
|
|
134
|
+
store.add(m)
|
|
135
|
+
|
|
136
|
+
# decision → SUPERSEDE: old preserved, new active.
|
|
137
|
+
print(store.by_type("decision")) # → just PostgreSQL
|
|
138
|
+
print(store.by_type("decision", include_superseded=True)) # → both
|
|
139
|
+
|
|
140
|
+
# risk → FLAG: two risks on the same subject get cross-linked.
|
|
141
|
+
for cluster in store.contradictions():
|
|
142
|
+
for m in cluster:
|
|
143
|
+
print(m.content) # → both risks
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
See [`examples/engineering_design_demo.py`](examples/engineering_design_demo.py) for the full version with audit trail and source provenance, or run:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
typedmem profiles
|
|
150
|
+
typedmem --profile engineering_design add "..." --document-id design_v3.md
|
|
151
|
+
typedmem --profile engineering_design list --type decision
|
|
152
|
+
typedmem evolve --evolver contradictions
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## The mental model
|
|
156
|
+
|
|
157
|
+
| Layer | What it gives you | Examples |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| **`Memory`** | Typed object with content + confidence + workspace + sources + status | `Memory(type="claim", content=..., sources=[Source(...)])` |
|
|
160
|
+
| **`Source`** | Structured provenance with hashable identity | `(document_id, chunk_id, span)` — dedup key for REINFORCE |
|
|
161
|
+
| **`workspace`** | Namespace on every memory | One agent, multiple corpora, zero cross-contamination |
|
|
162
|
+
| **`ConflictPolicy`** | What to do when a new memory hits the same `(workspace, type, subject)` slot | `REPLACE` · `KEEP_BOTH` · `SUPERSEDE` · `REINFORCE` · `FLAG` · `IGNORE` |
|
|
163
|
+
| **`DomainProfile`** | Schema for a domain: which types, what policy each obeys, what's required | `engineering_design` · `research_paper` · `legal` · `medical_literature` · `personal` · … |
|
|
164
|
+
| **`Evolver`** | Reads memories (not text); produces audit-trailed actions | `ContradictionSurfacer` · `PreferenceDriftDetector` · `GoalResolver` · `SummaryEvolver` |
|
|
165
|
+
|
|
166
|
+
## Built-in profiles
|
|
167
|
+
|
|
168
|
+
| Profile | Types | Notable policies |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| `core` | fact, note, goal, task, event | Shared primitives all other profiles can opt into |
|
|
171
|
+
| `personal` | + preference, observation | `preference → REPLACE (60d decay)` |
|
|
172
|
+
| `child_development` | + observation (tagged), milestone, concern | observation tags: language/motor/emotional/cognitive/social |
|
|
173
|
+
| `research_paper` | + claim, method, evidence, limitation, open_question | **evidence → REINFORCE** (multiple papers corroborate) |
|
|
174
|
+
| `engineering_design` | + decision, constraint, risk, assumption, todo | **decision → SUPERSEDE**, **risk → FLAG** |
|
|
175
|
+
| `legal` | + obligation, exception, deadline, definition, citation | **definition → SUPERSEDE** |
|
|
176
|
+
| `medical_literature` | + finding, population, intervention, outcome, limitation | **outcome → REINFORCE** across studies |
|
|
177
|
+
|
|
178
|
+
Custom profiles via Python dataclass, JSON, or YAML.
|
|
179
|
+
|
|
180
|
+
## Storage
|
|
181
|
+
|
|
182
|
+
Three backends, one ABC:
|
|
183
|
+
|
|
184
|
+
| Store | Persistence | Notes |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| `InMemoryStore` | None | Default; fastest |
|
|
187
|
+
| `JSONLMemoryStore` | Append-only file | Last-write-wins; tombstones; `compact()` rewrites |
|
|
188
|
+
| `SQLiteMemoryStore` | SQLite file | Indexed on `(workspace, type, subject)`; persists embeddings; auto-migrates v0.2 → v0.4 schemas |
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from typedmem import SQLiteMemoryStore, DomainProfile
|
|
192
|
+
|
|
193
|
+
store = SQLiteMemoryStore.for_profile(
|
|
194
|
+
DomainProfile.builtin("research_paper"),
|
|
195
|
+
path="papers.db",
|
|
196
|
+
)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Retrieval
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
from typedmem import HashingEmbeddingProvider, Retriever
|
|
203
|
+
|
|
204
|
+
retriever = Retriever(store, embedder=HashingEmbeddingProvider())
|
|
205
|
+
hits = retriever.relevant(
|
|
206
|
+
"blood pressure reduction",
|
|
207
|
+
types=["evidence"],
|
|
208
|
+
workspace="cardiology",
|
|
209
|
+
)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`relevant()` blends three signals: `semantic` (cosine), `recency` (exponential decay), `confidence` (with type-specific half-life). Without an embedder, falls back to token overlap.
|
|
213
|
+
|
|
214
|
+
## Evolution
|
|
215
|
+
|
|
216
|
+
Evolvers read stored memories and produce auditable actions.
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from typedmem import (
|
|
220
|
+
ContradictionSurfacer, PreferenceDriftDetector,
|
|
221
|
+
GoalResolver, SummaryEvolver,
|
|
222
|
+
HashingEmbeddingProvider, AnthropicClient,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# 1. Pure read: walk the FLAG graph.
|
|
226
|
+
for cluster in store.contradictions():
|
|
227
|
+
print(f"{len(cluster)} memories cross-link as contradictions")
|
|
228
|
+
|
|
229
|
+
# 2. Annotation: catch unstable preferences.
|
|
230
|
+
PreferenceDriftDetector(min_replaces=3, window_days=30).evolve(store)
|
|
231
|
+
|
|
232
|
+
# 3. Safe match: dry-run first, then commit.
|
|
233
|
+
embedder = HashingEmbeddingProvider()
|
|
234
|
+
plan = GoalResolver(embedder, threshold=0.85).evolve(store, dry_run=True)
|
|
235
|
+
print(plan.summary())
|
|
236
|
+
GoalResolver(embedder, threshold=0.85).evolve(store) # commit
|
|
237
|
+
|
|
238
|
+
# 4. Non-destructive summary of stale events.
|
|
239
|
+
SummaryEvolver(AnthropicClient(), min_cluster_size=3).evolve(store)
|
|
240
|
+
# Originals untouched; new memory links via metadata["summarizes"].
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Every action emits an `EvolutionRecord` (`evolver`, `action`, `input_ids`, `output_ids`, `reason`, `timestamp`) and gets appended to each affected memory's `metadata["evolution_history"]`. No black-box mutations.
|
|
244
|
+
|
|
245
|
+
## CLI
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
typedmem profiles # list built-in domain profiles
|
|
249
|
+
typedmem --profile research_paper add "..." --document-id paper.pdf
|
|
250
|
+
typedmem --profile engineering_design list --type decision
|
|
251
|
+
typedmem search "blood pressure" --type evidence
|
|
252
|
+
typedmem evolve --evolver contradictions
|
|
253
|
+
typedmem evolve --evolver goals --apply --threshold 0.9 # dry-run by default
|
|
254
|
+
typedmem history MEMORY_ID # audit trail for one memory
|
|
255
|
+
typedmem workspaces
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Default store: `~/.typedmem/memories.db` (override with `--store path.db` or `--store path.jsonl`).
|
|
259
|
+
|
|
260
|
+
## Status & roadmap
|
|
261
|
+
|
|
262
|
+
v0.4 is the first public release.
|
|
263
|
+
|
|
264
|
+
- **v0.5** sentence-transformer embedder, profile composition (`extends`), destructive compaction (`MemoryStore.compact_summaries()`)
|
|
265
|
+
- **v0.6** hybrid BM25+semantic retrieval, query DSL, observability hooks
|
|
266
|
+
|
|
267
|
+
What TypedMemory **doesn't** do and doesn't plan to:
|
|
268
|
+
|
|
269
|
+
- ship document chunkers / loaders — define the `ingest()` seam, bring your own (`unstructured`, `langchain`, plain regex)
|
|
270
|
+
- ship its own vector DB — the abstraction is ready for one, but brute-force cosine wins under ~50k memories
|
|
271
|
+
- pull network dependencies into the default install — every provider is an opt-in extra
|
|
272
|
+
|
|
273
|
+
## License
|
|
274
|
+
|
|
275
|
+
MIT — see [LICENSE](LICENSE).
|
|
276
|
+
|
|
277
|
+
## Contributing
|
|
278
|
+
|
|
279
|
+
Issues and PRs welcome. Please run `pytest` and the demos in `examples/` before opening a PR; CI runs them on Python 3.10/3.11/3.12.
|
typedmem-0.4.0/README.md
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# TypedMemory
|
|
2
|
+
|
|
3
|
+
**Typed, policy-aware, evolving memory layer for AI agents.**
|
|
4
|
+
|
|
5
|
+
[](https://github.com/canis-minor/typedmem/actions/workflows/ci.yml)
|
|
6
|
+
[](https://pypi.org/project/typedmem/)
|
|
7
|
+
[](https://pypi.org/project/typedmem/)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
TypedMemory is the layer that sits between data and reasoning. Every memory has a **type** (claim, decision, observation, …), a **confidence**, a **structured source**, a **lifecycle policy**, and a **workspace** — not just a string in a vector database. Memories know how to update themselves on conflict, how to decay, and how to be summarized.
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
┌──────────────────┐
|
|
14
|
+
│ DomainProfile │ ← schema: which types,
|
|
15
|
+
│ TypeSpec × N │ which policies,
|
|
16
|
+
│ prompt + rules │ which validations
|
|
17
|
+
└────────┬─────────┘
|
|
18
|
+
│
|
|
19
|
+
text ──► Extractor ──► Memory ──┴──► MemoryStore ──► Retriever
|
|
20
|
+
│
|
|
21
|
+
▼
|
|
22
|
+
Evolver
|
|
23
|
+
(contradictions, drift, goals,
|
|
24
|
+
non-destructive summarization)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Zero runtime dependencies.** Stdlib only. LLM clients, YAML profile loading, and sentence-transformer-style retrieval are optional extras.
|
|
28
|
+
|
|
29
|
+
## Why this exists
|
|
30
|
+
|
|
31
|
+
Most "AI memory" libraries are wrappers around a vector database. That's fine for "remember what the user said," but it falls apart the moment you want an agent to:
|
|
32
|
+
|
|
33
|
+
- track **who said what, in which document, at which span** (provenance)
|
|
34
|
+
- handle **the same fact from three sources** without storing it three times (reinforcement)
|
|
35
|
+
- recognize that **a new decision supersedes the old one** without losing the audit trail
|
|
36
|
+
- **summarize stale events** without throwing away the originals
|
|
37
|
+
- **isolate** legal memory from medical memory on the same machine
|
|
38
|
+
- **flag contradictions** instead of silently overwriting them
|
|
39
|
+
|
|
40
|
+
TypedMemory handles these as first-class concepts, not bolt-ons.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install typedmem
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Optional extras:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install 'typedmem[anthropic]' # AnthropicClient
|
|
52
|
+
pip install 'typedmem[openai]' # OpenAIClient
|
|
53
|
+
pip install 'typedmem[yaml]' # DomainProfile.from_yaml()
|
|
54
|
+
pip install 'typedmem[all]'
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Python 3.10+.
|
|
58
|
+
|
|
59
|
+
## 60-second demo: an engineering design agent
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import json
|
|
63
|
+
from typedmem import (
|
|
64
|
+
DomainProfile, FakeClient, LLMExtractor, SQLiteMemoryStore,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
profile = DomainProfile.builtin("engineering_design")
|
|
68
|
+
store = SQLiteMemoryStore.for_profile(profile, "design.db")
|
|
69
|
+
|
|
70
|
+
# Pretend the LLM extracted these from your design docs.
|
|
71
|
+
extractor = LLMExtractor(client=FakeClient([
|
|
72
|
+
json.dumps([
|
|
73
|
+
{"type": "decision", "content": "Use SQLite for storage",
|
|
74
|
+
"subject": "storage_backend", "confidence": 0.9,
|
|
75
|
+
"source": {"document_id": "design_v1.md"}},
|
|
76
|
+
{"type": "risk", "content": "SQLite is single-writer",
|
|
77
|
+
"subject": "storage_backend", "confidence": 0.8,
|
|
78
|
+
"source": {"document_id": "design_v1.md"}},
|
|
79
|
+
]),
|
|
80
|
+
json.dumps([
|
|
81
|
+
{"type": "decision", "content": "Switch to PostgreSQL for concurrent writes",
|
|
82
|
+
"subject": "storage_backend", "confidence": 0.9,
|
|
83
|
+
"source": {"document_id": "design_v2.md"}},
|
|
84
|
+
{"type": "risk", "content": "Postgres adds an external service",
|
|
85
|
+
"subject": "storage_backend", "confidence": 0.85,
|
|
86
|
+
"source": {"document_id": "design_v2.md"}},
|
|
87
|
+
]),
|
|
88
|
+
]), profile=profile)
|
|
89
|
+
|
|
90
|
+
for snippet in ("v1 text", "v2 text"):
|
|
91
|
+
for m in extractor.extract(snippet):
|
|
92
|
+
store.add(m)
|
|
93
|
+
|
|
94
|
+
# decision → SUPERSEDE: old preserved, new active.
|
|
95
|
+
print(store.by_type("decision")) # → just PostgreSQL
|
|
96
|
+
print(store.by_type("decision", include_superseded=True)) # → both
|
|
97
|
+
|
|
98
|
+
# risk → FLAG: two risks on the same subject get cross-linked.
|
|
99
|
+
for cluster in store.contradictions():
|
|
100
|
+
for m in cluster:
|
|
101
|
+
print(m.content) # → both risks
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
See [`examples/engineering_design_demo.py`](examples/engineering_design_demo.py) for the full version with audit trail and source provenance, or run:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
typedmem profiles
|
|
108
|
+
typedmem --profile engineering_design add "..." --document-id design_v3.md
|
|
109
|
+
typedmem --profile engineering_design list --type decision
|
|
110
|
+
typedmem evolve --evolver contradictions
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## The mental model
|
|
114
|
+
|
|
115
|
+
| Layer | What it gives you | Examples |
|
|
116
|
+
|---|---|---|
|
|
117
|
+
| **`Memory`** | Typed object with content + confidence + workspace + sources + status | `Memory(type="claim", content=..., sources=[Source(...)])` |
|
|
118
|
+
| **`Source`** | Structured provenance with hashable identity | `(document_id, chunk_id, span)` — dedup key for REINFORCE |
|
|
119
|
+
| **`workspace`** | Namespace on every memory | One agent, multiple corpora, zero cross-contamination |
|
|
120
|
+
| **`ConflictPolicy`** | What to do when a new memory hits the same `(workspace, type, subject)` slot | `REPLACE` · `KEEP_BOTH` · `SUPERSEDE` · `REINFORCE` · `FLAG` · `IGNORE` |
|
|
121
|
+
| **`DomainProfile`** | Schema for a domain: which types, what policy each obeys, what's required | `engineering_design` · `research_paper` · `legal` · `medical_literature` · `personal` · … |
|
|
122
|
+
| **`Evolver`** | Reads memories (not text); produces audit-trailed actions | `ContradictionSurfacer` · `PreferenceDriftDetector` · `GoalResolver` · `SummaryEvolver` |
|
|
123
|
+
|
|
124
|
+
## Built-in profiles
|
|
125
|
+
|
|
126
|
+
| Profile | Types | Notable policies |
|
|
127
|
+
|---|---|---|
|
|
128
|
+
| `core` | fact, note, goal, task, event | Shared primitives all other profiles can opt into |
|
|
129
|
+
| `personal` | + preference, observation | `preference → REPLACE (60d decay)` |
|
|
130
|
+
| `child_development` | + observation (tagged), milestone, concern | observation tags: language/motor/emotional/cognitive/social |
|
|
131
|
+
| `research_paper` | + claim, method, evidence, limitation, open_question | **evidence → REINFORCE** (multiple papers corroborate) |
|
|
132
|
+
| `engineering_design` | + decision, constraint, risk, assumption, todo | **decision → SUPERSEDE**, **risk → FLAG** |
|
|
133
|
+
| `legal` | + obligation, exception, deadline, definition, citation | **definition → SUPERSEDE** |
|
|
134
|
+
| `medical_literature` | + finding, population, intervention, outcome, limitation | **outcome → REINFORCE** across studies |
|
|
135
|
+
|
|
136
|
+
Custom profiles via Python dataclass, JSON, or YAML.
|
|
137
|
+
|
|
138
|
+
## Storage
|
|
139
|
+
|
|
140
|
+
Three backends, one ABC:
|
|
141
|
+
|
|
142
|
+
| Store | Persistence | Notes |
|
|
143
|
+
|---|---|---|
|
|
144
|
+
| `InMemoryStore` | None | Default; fastest |
|
|
145
|
+
| `JSONLMemoryStore` | Append-only file | Last-write-wins; tombstones; `compact()` rewrites |
|
|
146
|
+
| `SQLiteMemoryStore` | SQLite file | Indexed on `(workspace, type, subject)`; persists embeddings; auto-migrates v0.2 → v0.4 schemas |
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from typedmem import SQLiteMemoryStore, DomainProfile
|
|
150
|
+
|
|
151
|
+
store = SQLiteMemoryStore.for_profile(
|
|
152
|
+
DomainProfile.builtin("research_paper"),
|
|
153
|
+
path="papers.db",
|
|
154
|
+
)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Retrieval
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from typedmem import HashingEmbeddingProvider, Retriever
|
|
161
|
+
|
|
162
|
+
retriever = Retriever(store, embedder=HashingEmbeddingProvider())
|
|
163
|
+
hits = retriever.relevant(
|
|
164
|
+
"blood pressure reduction",
|
|
165
|
+
types=["evidence"],
|
|
166
|
+
workspace="cardiology",
|
|
167
|
+
)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`relevant()` blends three signals: `semantic` (cosine), `recency` (exponential decay), `confidence` (with type-specific half-life). Without an embedder, falls back to token overlap.
|
|
171
|
+
|
|
172
|
+
## Evolution
|
|
173
|
+
|
|
174
|
+
Evolvers read stored memories and produce auditable actions.
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from typedmem import (
|
|
178
|
+
ContradictionSurfacer, PreferenceDriftDetector,
|
|
179
|
+
GoalResolver, SummaryEvolver,
|
|
180
|
+
HashingEmbeddingProvider, AnthropicClient,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# 1. Pure read: walk the FLAG graph.
|
|
184
|
+
for cluster in store.contradictions():
|
|
185
|
+
print(f"{len(cluster)} memories cross-link as contradictions")
|
|
186
|
+
|
|
187
|
+
# 2. Annotation: catch unstable preferences.
|
|
188
|
+
PreferenceDriftDetector(min_replaces=3, window_days=30).evolve(store)
|
|
189
|
+
|
|
190
|
+
# 3. Safe match: dry-run first, then commit.
|
|
191
|
+
embedder = HashingEmbeddingProvider()
|
|
192
|
+
plan = GoalResolver(embedder, threshold=0.85).evolve(store, dry_run=True)
|
|
193
|
+
print(plan.summary())
|
|
194
|
+
GoalResolver(embedder, threshold=0.85).evolve(store) # commit
|
|
195
|
+
|
|
196
|
+
# 4. Non-destructive summary of stale events.
|
|
197
|
+
SummaryEvolver(AnthropicClient(), min_cluster_size=3).evolve(store)
|
|
198
|
+
# Originals untouched; new memory links via metadata["summarizes"].
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Every action emits an `EvolutionRecord` (`evolver`, `action`, `input_ids`, `output_ids`, `reason`, `timestamp`) and gets appended to each affected memory's `metadata["evolution_history"]`. No black-box mutations.
|
|
202
|
+
|
|
203
|
+
## CLI
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
typedmem profiles # list built-in domain profiles
|
|
207
|
+
typedmem --profile research_paper add "..." --document-id paper.pdf
|
|
208
|
+
typedmem --profile engineering_design list --type decision
|
|
209
|
+
typedmem search "blood pressure" --type evidence
|
|
210
|
+
typedmem evolve --evolver contradictions
|
|
211
|
+
typedmem evolve --evolver goals --apply --threshold 0.9 # dry-run by default
|
|
212
|
+
typedmem history MEMORY_ID # audit trail for one memory
|
|
213
|
+
typedmem workspaces
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Default store: `~/.typedmem/memories.db` (override with `--store path.db` or `--store path.jsonl`).
|
|
217
|
+
|
|
218
|
+
## Status & roadmap
|
|
219
|
+
|
|
220
|
+
v0.4 is the first public release.
|
|
221
|
+
|
|
222
|
+
- **v0.5** sentence-transformer embedder, profile composition (`extends`), destructive compaction (`MemoryStore.compact_summaries()`)
|
|
223
|
+
- **v0.6** hybrid BM25+semantic retrieval, query DSL, observability hooks
|
|
224
|
+
|
|
225
|
+
What TypedMemory **doesn't** do and doesn't plan to:
|
|
226
|
+
|
|
227
|
+
- ship document chunkers / loaders — define the `ingest()` seam, bring your own (`unstructured`, `langchain`, plain regex)
|
|
228
|
+
- ship its own vector DB — the abstraction is ready for one, but brute-force cosine wins under ~50k memories
|
|
229
|
+
- pull network dependencies into the default install — every provider is an opt-in extra
|
|
230
|
+
|
|
231
|
+
## License
|
|
232
|
+
|
|
233
|
+
MIT — see [LICENSE](LICENSE).
|
|
234
|
+
|
|
235
|
+
## Contributing
|
|
236
|
+
|
|
237
|
+
Issues and PRs welcome. Please run `pytest` and the demos in `examples/` before opening a PR; CI runs them on Python 3.10/3.11/3.12.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "typedmem"
|
|
7
|
+
version = "0.4.0"
|
|
8
|
+
description = "Typed, policy-aware, evolving memory layer for AI agents."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "ruxiz" }]
|
|
13
|
+
keywords = ["ai", "memory", "agents", "llm", "knowledge", "rag", "anthropic", "openai"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
24
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
25
|
+
]
|
|
26
|
+
dependencies = []
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/canis-minor/typedmem"
|
|
30
|
+
Documentation = "https://github.com/canis-minor/typedmem#readme"
|
|
31
|
+
Repository = "https://github.com/canis-minor/typedmem"
|
|
32
|
+
Issues = "https://github.com/canis-minor/typedmem/issues"
|
|
33
|
+
Changelog = "https://github.com/canis-minor/typedmem/blob/main/CHANGELOG.md"
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
test = ["pytest>=7"]
|
|
37
|
+
openai = ["openai>=1.0"]
|
|
38
|
+
anthropic = ["anthropic>=0.34"]
|
|
39
|
+
yaml = ["PyYAML>=6"]
|
|
40
|
+
docs = ["mkdocs>=1.5", "mkdocs-material>=9.5"]
|
|
41
|
+
all = ["openai>=1.0", "anthropic>=0.34", "PyYAML>=6"]
|
|
42
|
+
|
|
43
|
+
[project.scripts]
|
|
44
|
+
typedmem = "typedmem.cli:main"
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.packages.find]
|
|
47
|
+
include = ["typedmem*"]
|
|
48
|
+
exclude = ["tests*", "examples*", "docs*"]
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
testpaths = ["tests"]
|
typedmem-0.4.0/setup.cfg
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from typedmem.cli import main
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_add_and_list_and_search(tmp_path: Path, capsys):
|
|
7
|
+
db = tmp_path / "m.db"
|
|
8
|
+
|
|
9
|
+
assert main(["--store", str(db), "add", "Today child said more milk", "--subject", "child"]) == 0
|
|
10
|
+
captured = capsys.readouterr()
|
|
11
|
+
assert "added" in captured.out
|
|
12
|
+
|
|
13
|
+
assert main(["--store", str(db), "list", "--type", "observation"]) == 0
|
|
14
|
+
listed = capsys.readouterr().out
|
|
15
|
+
assert "milk" in listed
|
|
16
|
+
|
|
17
|
+
assert main(["--store", str(db), "search", "milk", "--limit", "3"]) == 0
|
|
18
|
+
found = capsys.readouterr().out
|
|
19
|
+
assert "milk" in found
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_force_type(tmp_path: Path, capsys):
|
|
23
|
+
db = tmp_path / "m.db"
|
|
24
|
+
assert main([
|
|
25
|
+
"--store", str(db), "add", "ship v0.2", "--type", "goal", "--confidence", "0.9",
|
|
26
|
+
]) == 0
|
|
27
|
+
out = capsys.readouterr().out
|
|
28
|
+
assert "added 1 memory (goal)" in out
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_jsonl_store_via_extension(tmp_path: Path, capsys):
|
|
32
|
+
path = tmp_path / "m.jsonl"
|
|
33
|
+
assert main(["--store", str(path), "add", "the sky is blue", "--type", "fact"]) == 0
|
|
34
|
+
assert path.exists()
|
|
35
|
+
assert main(["--store", str(path), "list"]) == 0
|
|
36
|
+
out = capsys.readouterr().out
|
|
37
|
+
assert "sky" in out
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_compact_jsonl(tmp_path: Path, capsys):
|
|
41
|
+
path = tmp_path / "m.jsonl"
|
|
42
|
+
for v in ("tea", "coffee", "matcha"):
|
|
43
|
+
main(["--store", str(path), "add", f"likes {v}", "--type", "preference", "--subject", "user"])
|
|
44
|
+
capsys.readouterr()
|
|
45
|
+
assert main(["--store", str(path), "compact"]) == 0
|
|
46
|
+
assert "compacted" in capsys.readouterr().out
|
|
47
|
+
assert path.read_text().count("\n") == 1
|