emotional-memory 0.5.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.
- emotional_memory-0.5.1/LICENSE +21 -0
- emotional_memory-0.5.1/PKG-INFO +545 -0
- emotional_memory-0.5.1/README.md +491 -0
- emotional_memory-0.5.1/pyproject.toml +96 -0
- emotional_memory-0.5.1/setup.cfg +4 -0
- emotional_memory-0.5.1/src/emotional_memory/__init__.py +122 -0
- emotional_memory-0.5.1/src/emotional_memory/_math.py +21 -0
- emotional_memory-0.5.1/src/emotional_memory/affect.py +83 -0
- emotional_memory-0.5.1/src/emotional_memory/appraisal.py +134 -0
- emotional_memory-0.5.1/src/emotional_memory/appraisal_llm.py +404 -0
- emotional_memory-0.5.1/src/emotional_memory/async_adapters.py +143 -0
- emotional_memory-0.5.1/src/emotional_memory/async_engine.py +619 -0
- emotional_memory-0.5.1/src/emotional_memory/categorize.py +198 -0
- emotional_memory-0.5.1/src/emotional_memory/decay.py +84 -0
- emotional_memory-0.5.1/src/emotional_memory/embedders/__init__.py +8 -0
- emotional_memory-0.5.1/src/emotional_memory/embedders/sentence_transformers.py +60 -0
- emotional_memory-0.5.1/src/emotional_memory/engine.py +672 -0
- emotional_memory-0.5.1/src/emotional_memory/integrations/__init__.py +8 -0
- emotional_memory-0.5.1/src/emotional_memory/integrations/langchain.py +108 -0
- emotional_memory-0.5.1/src/emotional_memory/interfaces.py +68 -0
- emotional_memory-0.5.1/src/emotional_memory/interfaces_async.py +62 -0
- emotional_memory-0.5.1/src/emotional_memory/models.py +148 -0
- emotional_memory-0.5.1/src/emotional_memory/mood.py +172 -0
- emotional_memory-0.5.1/src/emotional_memory/py.typed +0 -0
- emotional_memory-0.5.1/src/emotional_memory/resonance.py +346 -0
- emotional_memory-0.5.1/src/emotional_memory/retrieval.py +389 -0
- emotional_memory-0.5.1/src/emotional_memory/state.py +139 -0
- emotional_memory-0.5.1/src/emotional_memory/stores/__init__.py +8 -0
- emotional_memory-0.5.1/src/emotional_memory/stores/in_memory.py +73 -0
- emotional_memory-0.5.1/src/emotional_memory/stores/sqlite.py +260 -0
- emotional_memory-0.5.1/src/emotional_memory/visualization.py +687 -0
- emotional_memory-0.5.1/src/emotional_memory.egg-info/PKG-INFO +545 -0
- emotional_memory-0.5.1/src/emotional_memory.egg-info/SOURCES.txt +60 -0
- emotional_memory-0.5.1/src/emotional_memory.egg-info/dependency_links.txt +1 -0
- emotional_memory-0.5.1/src/emotional_memory.egg-info/requires.txt +36 -0
- emotional_memory-0.5.1/src/emotional_memory.egg-info/top_level.txt +1 -0
- emotional_memory-0.5.1/tests/test_affect.py +138 -0
- emotional_memory-0.5.1/tests/test_appraisal.py +206 -0
- emotional_memory-0.5.1/tests/test_appraisal_llm.py +271 -0
- emotional_memory-0.5.1/tests/test_async_engine.py +924 -0
- emotional_memory-0.5.1/tests/test_categorize.py +235 -0
- emotional_memory-0.5.1/tests/test_comparative.py +135 -0
- emotional_memory-0.5.1/tests/test_dataset.py +80 -0
- emotional_memory-0.5.1/tests/test_decay.py +138 -0
- emotional_memory-0.5.1/tests/test_dual_path.py +292 -0
- emotional_memory-0.5.1/tests/test_engine.py +909 -0
- emotional_memory-0.5.1/tests/test_examples.py +43 -0
- emotional_memory-0.5.1/tests/test_init_exports.py +15 -0
- emotional_memory-0.5.1/tests/test_integration.py +156 -0
- emotional_memory-0.5.1/tests/test_langchain_adapter.py +197 -0
- emotional_memory-0.5.1/tests/test_llm_integration.py +142 -0
- emotional_memory-0.5.1/tests/test_math.py +46 -0
- emotional_memory-0.5.1/tests/test_models.py +108 -0
- emotional_memory-0.5.1/tests/test_mood.py +249 -0
- emotional_memory-0.5.1/tests/test_prediction.py +157 -0
- emotional_memory-0.5.1/tests/test_resonance.py +396 -0
- emotional_memory-0.5.1/tests/test_retrieval.py +334 -0
- emotional_memory-0.5.1/tests/test_sentence_transformers_extra.py +146 -0
- emotional_memory-0.5.1/tests/test_sqlite_store.py +511 -0
- emotional_memory-0.5.1/tests/test_state.py +130 -0
- emotional_memory-0.5.1/tests/test_store.py +82 -0
- emotional_memory-0.5.1/tests/test_visualization.py +275 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gianluca Mazza <info@gianlucamazza.it>
|
|
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.
|
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: emotional_memory
|
|
3
|
+
Version: 0.5.1
|
|
4
|
+
Summary: Emotional memory library for LLMs based on Affective Field Theory
|
|
5
|
+
Author-email: Gianluca Mazza <info@gianlucamazza.it>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/gianlucamazza/emotional-memory
|
|
8
|
+
Project-URL: Repository, https://github.com/gianlucamazza/emotional-memory
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/gianlucamazza/emotional-memory/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/gianlucamazza/emotional-memory/blob/main/CHANGELOG.md
|
|
11
|
+
Project-URL: Documentation, https://gianlucamazza.github.io/emotional-memory
|
|
12
|
+
Keywords: llm,memory,emotion,affective-computing,ai
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: numpy>=1.24
|
|
27
|
+
Requires-Dist: pydantic<3,>=2.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-cov>=5; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
|
|
32
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
33
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
34
|
+
Provides-Extra: bench
|
|
35
|
+
Requires-Dist: pytest-benchmark>=4.0; extra == "bench"
|
|
36
|
+
Requires-Dist: psutil>=5.9; extra == "bench"
|
|
37
|
+
Provides-Extra: llm-test
|
|
38
|
+
Requires-Dist: httpx>=0.27; extra == "llm-test"
|
|
39
|
+
Provides-Extra: dotenv
|
|
40
|
+
Requires-Dist: python-dotenv>=1.0; extra == "dotenv"
|
|
41
|
+
Provides-Extra: sentence-transformers
|
|
42
|
+
Requires-Dist: sentence-transformers>=2.2; extra == "sentence-transformers"
|
|
43
|
+
Provides-Extra: sqlite
|
|
44
|
+
Requires-Dist: sqlite-vec>=0.1.1; extra == "sqlite"
|
|
45
|
+
Provides-Extra: langchain
|
|
46
|
+
Requires-Dist: langchain-core>=0.3; extra == "langchain"
|
|
47
|
+
Provides-Extra: viz
|
|
48
|
+
Requires-Dist: matplotlib>=3.7; extra == "viz"
|
|
49
|
+
Provides-Extra: docs
|
|
50
|
+
Requires-Dist: mkdocs>=1.6; extra == "docs"
|
|
51
|
+
Requires-Dist: mkdocs-material>=9.5; extra == "docs"
|
|
52
|
+
Requires-Dist: mkdocstrings[python]>=0.25; extra == "docs"
|
|
53
|
+
Dynamic: license-file
|
|
54
|
+
|
|
55
|
+
# emotional_memory
|
|
56
|
+
|
|
57
|
+
[](https://github.com/gianlucamazza/emotional-memory/actions/workflows/ci.yml)
|
|
58
|
+
[](https://pypi.org/project/emotional_memory/)
|
|
59
|
+
[](https://pypi.org/project/emotional_memory/)
|
|
60
|
+
[](LICENSE)
|
|
61
|
+
|
|
62
|
+
Emotional memory for LLMs based on **Affective Field Theory (AFT)** — a 5-layer model that encodes not just *what* happened, but *how it felt*, *how that feeling was moving*, and *what mood colored the moment*.
|
|
63
|
+
|
|
64
|
+
## Installation
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install emotional-memory
|
|
68
|
+
pip install emotional-memory[sentence-transformers] # real semantic embeddings (recommended)
|
|
69
|
+
pip install emotional-memory[sqlite] # SQLite persistence via sqlite-vec
|
|
70
|
+
pip install emotional-memory[viz] # matplotlib visualization
|
|
71
|
+
pip install emotional-memory[dotenv] # .env file loading via python-dotenv
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
For development:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
git clone https://github.com/gianlucamazza/emotional-memory
|
|
78
|
+
cd emotional-memory
|
|
79
|
+
pip install -e ".[dev,sqlite,sentence-transformers]"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Quickstart
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
pip install emotional-memory[sentence-transformers]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from emotional_memory import EmotionalMemory, InMemoryStore, CoreAffect
|
|
90
|
+
from emotional_memory.embedders import SentenceTransformerEmbedder
|
|
91
|
+
|
|
92
|
+
em = EmotionalMemory(
|
|
93
|
+
store=InMemoryStore(),
|
|
94
|
+
embedder=SentenceTransformerEmbedder(), # all-MiniLM-L6-v2 by default
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Set current emotional state
|
|
98
|
+
em.set_affect(CoreAffect(valence=0.8, arousal=0.6))
|
|
99
|
+
|
|
100
|
+
# Encode memories — each one captures the full affective context
|
|
101
|
+
em.encode("Just shipped the feature after three hard weeks.")
|
|
102
|
+
em.encode("Team celebration in the office.", metadata={"source": "slack"})
|
|
103
|
+
|
|
104
|
+
# Retrieve — ranked by semantic relevance AND emotional congruence
|
|
105
|
+
results = em.retrieve("difficult project success", top_k=3)
|
|
106
|
+
for mem in results:
|
|
107
|
+
print(mem.content, mem.tag.core_affect)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Bring your own embedder** — any object with `.embed(text) -> list[float]` works:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
class MyEmbedder:
|
|
114
|
+
def embed(self, text: str) -> list[float]: ...
|
|
115
|
+
def embed_batch(self, texts: list[str]) -> list[list[float]]: ...
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Or subclass `SequentialEmbedder` and implement only `embed()` — `embed_batch()` is provided.
|
|
119
|
+
|
|
120
|
+
### Async
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
import asyncio
|
|
124
|
+
from emotional_memory import EmotionalMemory, InMemoryStore, as_async
|
|
125
|
+
from emotional_memory.embedders import SentenceTransformerEmbedder
|
|
126
|
+
|
|
127
|
+
sync_em = EmotionalMemory(store=InMemoryStore(), embedder=SentenceTransformerEmbedder())
|
|
128
|
+
em = as_async(sync_em) # wraps sync components with asyncio.to_thread bridges
|
|
129
|
+
|
|
130
|
+
async def main():
|
|
131
|
+
await em.encode("Meeting went surprisingly well today.")
|
|
132
|
+
results = await em.retrieve("work meeting", top_k=3)
|
|
133
|
+
|
|
134
|
+
asyncio.run(main())
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
For native async embedders or stores, construct `AsyncEmotionalMemory` directly with
|
|
138
|
+
`SyncToAsyncEmbedder`, `SyncToAsyncStore`, or your own `AsyncEmbedder`/`AsyncMemoryStore`.
|
|
139
|
+
|
|
140
|
+
## Affective Field Theory
|
|
141
|
+
|
|
142
|
+
AFT models emotion as a **field** — distributed, dynamic, multi-layer — rather than a discrete label or a single coordinate. Five layers are captured at encoding time:
|
|
143
|
+
|
|
144
|
+
| Layer | Model | What it captures |
|
|
145
|
+
|---|---|---|
|
|
146
|
+
| **CoreAffect** | Barrett/Russell circumplex | Continuous `(valence, arousal)` — the emotional substrate |
|
|
147
|
+
| **AffectiveMomentum** | Spinoza — affect as transition | Velocity and acceleration of affect change |
|
|
148
|
+
| **MoodField** | Heidegger — *Stimmung* as attunement | Slow-moving global mood with inertia (EMA) |
|
|
149
|
+
| **AppraisalVector** | Scherer/Lazarus/Stoics | Emotion derived from evaluation: novelty, goal-relevance, coping, norm-congruence, self-relevance |
|
|
150
|
+
| **ResonanceLinks** | Aristotle/Hume/Bower/Collins & Loftus/Hebb | Associative bidirectional graph: semantic, emotional, temporal, causal, contrastive links; multi-hop spreading activation + Hebbian co-retrieval strengthening |
|
|
151
|
+
|
|
152
|
+
Full theoretical foundations: [`docs/research/`](docs/research/)
|
|
153
|
+
|
|
154
|
+
## API Overview
|
|
155
|
+
|
|
156
|
+
### `EmotionalMemory`
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
em = EmotionalMemory(
|
|
160
|
+
store: MemoryStore,
|
|
161
|
+
embedder: Embedder,
|
|
162
|
+
appraisal_engine: AppraisalEngine | None = None, # optional: auto-appraise via LLM
|
|
163
|
+
config: EmotionalMemoryConfig | None = None,
|
|
164
|
+
)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
| Method | Description |
|
|
168
|
+
|---|---|
|
|
169
|
+
| `encode(content, appraisal=None, metadata=None) -> Memory` | Encode content with full AFT pipeline |
|
|
170
|
+
| `encode_batch(contents, metadata=None) -> list[Memory]` | Batch encode with `embed_batch()`, per-item appraisal |
|
|
171
|
+
| `retrieve(query, top_k=5) -> list[Memory]` | Emotionally-weighted retrieval + reconsolidation |
|
|
172
|
+
| `elaborate(memory_id) -> Memory \| None` | Run full appraisal on a fast-path (`pending_appraisal=True`) memory and blend core_affect |
|
|
173
|
+
| `elaborate_pending() -> list[Memory]` | Elaborate all pending fast-path memories in one call |
|
|
174
|
+
| `delete(memory_id)` | Remove a memory from the store |
|
|
175
|
+
| `get(memory_id) -> Memory \| None` | Look up a single memory by ID |
|
|
176
|
+
| `list_all() -> list[Memory]` | Return all stored memories |
|
|
177
|
+
| `len(engine) -> int` | Number of memories in the store |
|
|
178
|
+
| `prune(threshold=0.05) -> int` | Delete memories below effective strength threshold; returns count removed |
|
|
179
|
+
| `export_memories() -> list[dict]` | Serialise all memories to JSON-safe dicts (backup / migration) |
|
|
180
|
+
| `import_memories(data, overwrite=False) -> int` | Restore from `export_memories()` output; returns count written |
|
|
181
|
+
| `get_state() -> AffectiveState` | Current affective state (read-only copy) |
|
|
182
|
+
| `set_affect(core_affect)` | Manually inject a CoreAffect |
|
|
183
|
+
| `save_state() -> dict` | Serialise affective state for persistence |
|
|
184
|
+
| `load_state(data)` | Restore previously saved affective state |
|
|
185
|
+
| `get_current_mood(now=None) -> MoodField` | Read-only mood with time regression |
|
|
186
|
+
| `close()` | Release store resources (e.g. SQLite connection); also via `with` |
|
|
187
|
+
|
|
188
|
+
Both engines support context managers for automatic resource cleanup:
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
with EmotionalMemory(store=SQLiteStore("mem.db"), embedder=MyEmbedder()) as em:
|
|
192
|
+
em.encode("Session start")
|
|
193
|
+
results = em.retrieve("relevant context")
|
|
194
|
+
# SQLiteStore.close() called automatically
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### `AsyncEmotionalMemory`
|
|
198
|
+
|
|
199
|
+
Same method signatures as `EmotionalMemory`. Coroutines: `encode`, `encode_batch`, `retrieve`,
|
|
200
|
+
`elaborate`, `elaborate_pending`, `delete`, `get`, `list_all`, `count`, `prune`,
|
|
201
|
+
`export_memories`, `import_memories`, `close`.
|
|
202
|
+
State accessors (`get_state`, `set_affect`, `save_state`, `load_state`, `get_current_mood`)
|
|
203
|
+
remain synchronous.
|
|
204
|
+
|
|
205
|
+
Supports `async with` for automatic resource cleanup:
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
async with AsyncEmotionalMemory(store=..., embedder=...) as em:
|
|
209
|
+
await em.encode("Session start")
|
|
210
|
+
results = await em.retrieve("relevant context")
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
from emotional_memory import AsyncEmotionalMemory, SyncToAsyncEmbedder, SyncToAsyncStore
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Bridge adapters: `SyncToAsyncEmbedder`, `SyncToAsyncStore`, `SyncToAsyncAppraisalEngine` wrap
|
|
218
|
+
any sync implementation. `SyncToAsyncStore` also proxies `close()` to the underlying store.
|
|
219
|
+
`as_async(engine)` wraps a complete `EmotionalMemory` in one call.
|
|
220
|
+
|
|
221
|
+
### Key config classes
|
|
222
|
+
|
|
223
|
+
- `EmotionalMemoryConfig` — top-level config (decay, retrieval, resonance, mood_alpha, mood_decay)
|
|
224
|
+
- `RetrievalConfig` — weights, APE threshold, reconsolidation learning rate
|
|
225
|
+
- `ResonanceConfig` — similarity threshold, max links, semantic/emotional/temporal weights, candidate multiplier, `propagation_hops`, `hebbian_increment`, configurable link-classification thresholds
|
|
226
|
+
- `DecayConfig` — power-law decay parameters, arousal modulation, floor values
|
|
227
|
+
- `MoodDecayConfig` — time-based mood regression (half-life, inertia scale, baselines)
|
|
228
|
+
- `AdaptiveWeightsConfig` — smooth mood-adaptive retrieval weight tuning (sigmoid/Gaussian gates)
|
|
229
|
+
- `LLMAppraisalConfig` — LLM appraisal engine settings (system prompt, cache size, fallback behaviour)
|
|
230
|
+
|
|
231
|
+
### Interfaces (bring your own)
|
|
232
|
+
|
|
233
|
+
If your embedder has no native batching, subclass `SequentialEmbedder` — `embed_batch()` is
|
|
234
|
+
provided automatically:
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
from emotional_memory import SequentialEmbedder
|
|
238
|
+
|
|
239
|
+
class MyEmbedder(SequentialEmbedder):
|
|
240
|
+
def embed(self, text: str) -> list[float]:
|
|
241
|
+
return my_model.encode(text).tolist()
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Otherwise implement the full `Embedder` protocol:
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
class Embedder(Protocol):
|
|
248
|
+
def embed(self, text: str) -> list[float]: ...
|
|
249
|
+
def embed_batch(self, texts: list[str]) -> list[list[float]]: ...
|
|
250
|
+
|
|
251
|
+
class MemoryStore(Protocol):
|
|
252
|
+
def save(self, memory: Memory) -> None: ...
|
|
253
|
+
def get(self, memory_id: str) -> Memory | None: ...
|
|
254
|
+
def update(self, memory: Memory) -> None: ...
|
|
255
|
+
def delete(self, memory_id: str) -> None: ...
|
|
256
|
+
def list_all(self) -> list[Memory]: ...
|
|
257
|
+
def search_by_embedding(self, embedding: list[float], top_k: int) -> list[Memory]: ...
|
|
258
|
+
def __len__(self) -> int: ...
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Async variants (`AsyncEmbedder`, `AsyncMemoryStore`, `AsyncAppraisalEngine`) are defined in
|
|
262
|
+
`interfaces_async.py`. `AsyncMemoryStore` uses `count() -> int` instead of `__len__` since
|
|
263
|
+
dunder methods cannot be coroutines.
|
|
264
|
+
|
|
265
|
+
**Stores included:**
|
|
266
|
+
- `InMemoryStore` — dict-backed, brute-force cosine search (no extra deps)
|
|
267
|
+
- `SQLiteStore` — persistent SQLite + sqlite-vec ANN search (`pip install emotional-memory[sqlite]`)
|
|
268
|
+
|
|
269
|
+
### Appraisal Engines
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
class AppraisalEngine(Protocol):
|
|
273
|
+
def appraise(self, event_text: str, context: dict | None = None) -> AppraisalVector: ...
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Pass an `appraisal_engine` to `EmotionalMemory` to auto-generate `AppraisalVector` during encode.
|
|
277
|
+
|
|
278
|
+
**`LLMAppraisalEngine`** — wrap any LLM SDK in a single callable:
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
from emotional_memory import LLMAppraisalEngine
|
|
282
|
+
|
|
283
|
+
def my_llm(prompt: str, json_schema: dict) -> str:
|
|
284
|
+
# call openai / anthropic / local model here
|
|
285
|
+
return response_text
|
|
286
|
+
|
|
287
|
+
engine = LLMAppraisalEngine(llm=my_llm)
|
|
288
|
+
em = EmotionalMemory(store=..., embedder=..., appraisal_engine=engine)
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**`KeywordAppraisalEngine`** — regex-based fallback, zero external dependencies, ships with
|
|
292
|
+
default rules covering success, failure, novelty, danger, and social norms:
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
from emotional_memory import KeywordAppraisalEngine
|
|
296
|
+
engine = KeywordAppraisalEngine() # or pass custom KeywordRule list
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Visualization
|
|
300
|
+
|
|
301
|
+
The optional `viz` extra provides 8 plotting functions for inspecting and presenting the model's internals. Each function accepts an optional `ax` parameter for subplot composition and returns a `matplotlib.Figure`.
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
from emotional_memory.visualization import plot_circumplex, plot_decay_curves
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Valence-Arousal Circumplex
|
|
308
|
+
|
|
309
|
+
Memories plotted on Russell's (1980) 2D circumplex, colored by consolidation strength.
|
|
310
|
+
|
|
311
|
+

|
|
312
|
+
|
|
313
|
+
### Decay Curves (ACT-R Power Law)
|
|
314
|
+
|
|
315
|
+
Family of curves showing how arousal (McGaugh 2004) and retrieval count (spacing effect) modulate memory decay.
|
|
316
|
+
|
|
317
|
+

|
|
318
|
+
|
|
319
|
+
### Yerkes-Dodson Inverted-U
|
|
320
|
+
|
|
321
|
+
Consolidation strength peaks near effective arousal 0.7, then drops — the classic Yerkes-Dodson curve.
|
|
322
|
+
|
|
323
|
+

|
|
324
|
+
|
|
325
|
+
### 6-Signal Retrieval Breakdown
|
|
326
|
+
|
|
327
|
+
Radar chart of the six retrieval signals: semantic similarity, mood congruence, affect proximity, momentum alignment, recency, and resonance boost.
|
|
328
|
+
|
|
329
|
+

|
|
330
|
+
|
|
331
|
+
### Mood Field Evolution
|
|
332
|
+
|
|
333
|
+
Time series of valence, arousal, and dominance with dashed baselines showing the regression attractors.
|
|
334
|
+
|
|
335
|
+

|
|
336
|
+
|
|
337
|
+
### Adaptive Retrieval Weights
|
|
338
|
+
|
|
339
|
+
Heatmap showing how retrieval weights shift across different mood states (valence x arousal grid).
|
|
340
|
+
|
|
341
|
+

|
|
342
|
+
|
|
343
|
+
### Resonance Network
|
|
344
|
+
|
|
345
|
+
Directed graph with memories as nodes and edges colored by link type (semantic, emotional, temporal, causal, contrastive).
|
|
346
|
+
|
|
347
|
+

|
|
348
|
+
|
|
349
|
+
### Appraisal Radar (Scherer CPM)
|
|
350
|
+
|
|
351
|
+
Spider chart of the 5 Stimulus Evaluation Check dimensions.
|
|
352
|
+
|
|
353
|
+

|
|
354
|
+
|
|
355
|
+
### Generating images
|
|
356
|
+
|
|
357
|
+
```bash
|
|
358
|
+
make docs-images # regenerate all PNGs in docs/images/
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
## Comparison with Existing Systems
|
|
362
|
+
|
|
363
|
+
| | **emotional-memory (AFT)** | **Mem0** | **Letta** | **Zep** | **LangChain Memory** |
|
|
364
|
+
|---|---|---|---|---|---|
|
|
365
|
+
| **License** | MIT | Apache 2.0 | Apache 2.0 | Apache 2.0 | MIT |
|
|
366
|
+
| **Persistence** | InMemory / SQLite | Qdrant, Chroma, Pinecone, PG, MongoDB | PostgreSQL / SQLite | Neo4j (self-hosted) / Cloud | In-memory / custom |
|
|
367
|
+
| **BYO embedder** | ✅ any `Embedder` protocol | ✅ (OpenAI default) | ⚠️ partial | ⚠️ partial | ✅ |
|
|
368
|
+
| **Emotion model** | ✅ 5-layer AFT (valence, arousal, mood, appraisal, resonance) | ❌ | ❌ | ❌ | ❌ |
|
|
369
|
+
| **Reconsolidation** | ✅ APE-gated lability window | ✅ auto update/remove | ✅ tool-call edit | ✅ edge invalidation | ❌ |
|
|
370
|
+
| **Persistent mood state** | ✅ MoodField (Heidegger EMA) | ❌ | ❌ | ❌ | ❌ |
|
|
371
|
+
| **LLM-agnostic** | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
372
|
+
| **LangChain integration** | ✅ `EmotionalMemoryChatHistory` | ✅ official | ✅ tools interop | ✅ ZepVectorStore | ✅ native |
|
|
373
|
+
| **Public benchmark** | ✅ 126 fidelity test cases (intra-theory) | ✅ LoCoMo, LongMemEval, BEAM | ✅ LoCoMo, DMR | ✅ DMR, LongMemEval | ❌ |
|
|
374
|
+
| **Codebase size** | ~4.8k LOC (src/) | >50k LOC | >50k LOC | >50k LOC | ~5k LOC |
|
|
375
|
+
|
|
376
|
+
**Key differentiator**: emotional-memory is the only system with an explicit multi-layer emotional model. Every other system treats memory as semantically-indexed text with no affective state — no mood congruence in retrieval, no reconsolidation triggered by affective prediction error, no momentum or appraisal.
|
|
377
|
+
|
|
378
|
+
**Benchmark caveat**: AFT fidelity tests validate psychological invariants (intra-theory); Mem0/Letta/Zep benchmarks measure QA recall on dialog datasets (inter-system). A cross-system comparison on affect-labeled retrieval tasks is in progress (v0.6).
|
|
379
|
+
|
|
380
|
+
## Benchmarks
|
|
381
|
+
|
|
382
|
+
### Psychological fidelity (126 parametrized test cases, 20 phenomena)
|
|
383
|
+
|
|
384
|
+
The library validates 20 phenomena from the affective science literature via 126 parametrized test cases (run `pytest --collect-only benchmarks/fidelity/` to enumerate them):
|
|
385
|
+
|
|
386
|
+
| Phenomenon | Reference | Cases | Test file |
|
|
387
|
+
|---|---|---|---|
|
|
388
|
+
| Mood-congruent recall | Bower 1981 | 3 | [test_mood_congruent.py](benchmarks/fidelity/test_mood_congruent.py) |
|
|
389
|
+
| Emotional enhancement | Cahill & McGaugh 1995 | 3 | [test_emotional_enhancement.py](benchmarks/fidelity/test_emotional_enhancement.py) |
|
|
390
|
+
| Yerkes-Dodson inverted-U | Yerkes & Dodson 1908 | 12 | [test_yerkes_dodson.py](benchmarks/fidelity/test_yerkes_dodson.py) |
|
|
391
|
+
| Spacing effect | Ebbinghaus 1885 | 7 | [test_spacing_effect.py](benchmarks/fidelity/test_spacing_effect.py) |
|
|
392
|
+
| Arousal floor | McGaugh 2004 | 7 | [test_arousal_floor.py](benchmarks/fidelity/test_arousal_floor.py) |
|
|
393
|
+
| Reconsolidation (APE) | Nader & Schiller 2000 | 5 | [test_reconsolidation.py](benchmarks/fidelity/test_reconsolidation.py) |
|
|
394
|
+
| State-dependent retrieval | Godden & Baddeley 1975 | 3 | [test_state_dependent.py](benchmarks/fidelity/test_state_dependent.py) |
|
|
395
|
+
| Affective momentum | Spinoza, Ethics III | 9 | [test_momentum.py](benchmarks/fidelity/test_momentum.py) |
|
|
396
|
+
| Mood-adaptive weights | Heidegger, Being & Time §29 | 14 | [test_mood_adaptive.py](benchmarks/fidelity/test_mood_adaptive.py) |
|
|
397
|
+
| Appraisal-to-affect mapping | Scherer CPM 2009 | 11 | [test_appraisal_affect.py](benchmarks/fidelity/test_appraisal_affect.py) |
|
|
398
|
+
| Spreading activation | Collins & Loftus 1975 | 5 | [test_spreading_activation.py](benchmarks/fidelity/test_spreading_activation.py) |
|
|
399
|
+
| Hebbian co-retrieval strengthening | Hebb 1949 | 4 | [test_hebbian_strengthening.py](benchmarks/fidelity/test_hebbian_strengthening.py) |
|
|
400
|
+
| ACT-R power-law decay | Anderson 1983 / McGaugh 2004 | 5 | [test_decay_power_law.py](benchmarks/fidelity/test_decay_power_law.py) |
|
|
401
|
+
| PAD dominance | Mehrabian & Russell 1974 | 8 | [test_pad_dominance.py](benchmarks/fidelity/test_pad_dominance.py) |
|
|
402
|
+
| Emotional retrieval vs. cosine | Bower 1981 / Russell 1980 / Nader 2000 | 3 | [test_emotional_vs_cosine.py](benchmarks/fidelity/test_emotional_vs_cosine.py) |
|
|
403
|
+
| Design gap regression | (various) | 3 | [test_design_gaps.py](benchmarks/fidelity/test_design_gaps.py) |
|
|
404
|
+
| Dual-path encoding | LeDoux 1996 | 6 | [test_dual_path_encoding.py](benchmarks/fidelity/test_dual_path_encoding.py) |
|
|
405
|
+
| Emotion categorization | Plutchik 1980 | 10 | [test_emotion_categorization.py](benchmarks/fidelity/test_emotion_categorization.py) |
|
|
406
|
+
| Affective prediction error | Schultz 1997 / Pearce-Hall 1980 | 5 | [test_prediction_error.py](benchmarks/fidelity/test_prediction_error.py) |
|
|
407
|
+
| APE-gated reconsolidation window | Nader & Schiller 2000 | 3 | [test_reconsolidation_window.py](benchmarks/fidelity/test_reconsolidation_window.py) |
|
|
408
|
+
|
|
409
|
+
Run with: `make bench-fidelity`
|
|
410
|
+
|
|
411
|
+
### Performance (hash-based embedder, InMemoryStore)
|
|
412
|
+
|
|
413
|
+
| Operation | N | Mean | OPS |
|
|
414
|
+
|---|---|---|---|
|
|
415
|
+
| Encode (single) | 1 | 1.7 ms | 590/s |
|
|
416
|
+
| Encode (batch of 100) | 100 | 9.9 ms/op | 101/s |
|
|
417
|
+
| Encode w/ resonance graph | 500 | 4.0 ms | 250/s |
|
|
418
|
+
| Retrieve top-5 | 100 | ~2 ms | ~500/s |
|
|
419
|
+
| Retrieve top-5 | 1 000 | ~12 ms | ~85/s |
|
|
420
|
+
| Retrieve top-5 | 10 000 | ~120 ms | ~8/s |
|
|
421
|
+
| Retrieve (top-k 1–25) | 1 000 | 10–18 ms | 55–100/s |
|
|
422
|
+
| Retrieve + reconsolidation | 200 | 2.6 ms | 385/s |
|
|
423
|
+
|
|
424
|
+
`InMemoryStore.search_by_embedding` uses vectorized matrix multiplication (numpy),
|
|
425
|
+
making retrieval O(n · d) in a single batch rather than n individual cosine calls.
|
|
426
|
+
Retrieval uses two-pass scoring (spreading activation); when no resonance links are
|
|
427
|
+
active the second pass is skipped. For stores > 10 000 memories, use `SQLiteStore`
|
|
428
|
+
(sqlite-vec ANN) or a vector database implementing the `MemoryStore` protocol.
|
|
429
|
+
|
|
430
|
+
Run with: `make bench-perf`
|
|
431
|
+
|
|
432
|
+
### Appraisal quality (LLM prompt validation)
|
|
433
|
+
|
|
434
|
+
15 natural-language phrases with expected directional outcomes against Scherer's 5 dimensions:
|
|
435
|
+
|
|
436
|
+
| Phrase category | Key assertions |
|
|
437
|
+
|---|---|
|
|
438
|
+
| Personal loss ("I got fired") | `goal_relevance < -0.2`, `coping_potential < 0.6` |
|
|
439
|
+
| Achievement ("Got promoted") | `goal_relevance > 0.2`, `norm_congruence > 0.0` |
|
|
440
|
+
| Moral violation ("Coworker stole credit") | `norm_congruence < -0.2`, `goal_relevance < 0.0` |
|
|
441
|
+
| Grief, danger, betrayal, relief, … | dimension-specific directional bounds |
|
|
442
|
+
|
|
443
|
+
Assertions use wide bands (e.g. `> 0.3`, `< -0.2`) and evaluate the median over 3 LLM calls to tolerate non-determinism. Designed to catch systematic prompt regressions, not exact calibration.
|
|
444
|
+
|
|
445
|
+
Run with: `EMOTIONAL_MEMORY_LLM_API_KEY=... make bench-appraisal`
|
|
446
|
+
|
|
447
|
+
Works with any OpenAI-compatible endpoint (Ollama, vLLM, LiteLLM, …) via `EMOTIONAL_MEMORY_LLM_BASE_URL`.
|
|
448
|
+
|
|
449
|
+
## LangChain integration
|
|
450
|
+
|
|
451
|
+
`EmotionalMemoryChatHistory` is a drop-in replacement for any LangChain chat history object.
|
|
452
|
+
It backs the history with an `EmotionalMemory` instance so the affective state evolves
|
|
453
|
+
naturally as the conversation unfolds.
|
|
454
|
+
|
|
455
|
+
```bash
|
|
456
|
+
pip install "emotional-memory[langchain,sentence-transformers]"
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
```python
|
|
460
|
+
from emotional_memory import EmotionalMemory, InMemoryStore
|
|
461
|
+
from emotional_memory.embedders import SentenceTransformerEmbedder
|
|
462
|
+
from emotional_memory.integrations import EmotionalMemoryChatHistory
|
|
463
|
+
|
|
464
|
+
em = EmotionalMemory(
|
|
465
|
+
store=InMemoryStore(),
|
|
466
|
+
embedder=SentenceTransformerEmbedder(),
|
|
467
|
+
)
|
|
468
|
+
history = EmotionalMemoryChatHistory(em)
|
|
469
|
+
|
|
470
|
+
# Works anywhere BaseChatMessageHistory is accepted:
|
|
471
|
+
history.add_user_message("I'm anxious about the deadline.")
|
|
472
|
+
history.add_ai_message("Let's break the work into smaller steps.")
|
|
473
|
+
|
|
474
|
+
print(history.messages) # [HumanMessage(...), AIMessage(...)]
|
|
475
|
+
|
|
476
|
+
# The underlying engine has tracked the affective state:
|
|
477
|
+
state = em.get_state()
|
|
478
|
+
print(f"valence={state.core_affect.valence:.2f} arousal={state.core_affect.arousal:.2f}")
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
The adapter uses dependency injection — pass a fully-configured `EmotionalMemory` so you
|
|
482
|
+
control the store backend and embedder. The `clear()` method removes all memories from the store.
|
|
483
|
+
|
|
484
|
+
## Logging
|
|
485
|
+
|
|
486
|
+
The library uses the standard `logging` module. Enable debug output to trace the full pipeline:
|
|
487
|
+
|
|
488
|
+
```python
|
|
489
|
+
import logging
|
|
490
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
491
|
+
# or just for emotional_memory:
|
|
492
|
+
logging.getLogger("emotional_memory").setLevel(logging.DEBUG)
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
Debug events include: encode start/stored/resonance, retrieve start/done, reconsolidation
|
|
496
|
+
triggers, LLM appraisal cache hits, and fallback activations.
|
|
497
|
+
|
|
498
|
+
## Examples
|
|
499
|
+
|
|
500
|
+
The [`examples/`](examples/) directory contains runnable scripts covering the full API.
|
|
501
|
+
All scripts are self-contained and use a deterministic `HashEmbedder` so they run without
|
|
502
|
+
any ML dependencies.
|
|
503
|
+
|
|
504
|
+
| Script | Description | Extra |
|
|
505
|
+
|--------|-------------|-------|
|
|
506
|
+
| `basic_usage.py` | Encode/retrieve, reconsolidation, resonance links | — |
|
|
507
|
+
| `advanced_config.py` | ACT-R decay, mood regression, adaptive weights | — |
|
|
508
|
+
| `appraisal_engines.py` | Keyword, static, and custom appraisal rules | — |
|
|
509
|
+
| `reconsolidation.py` | Two-retrieval lability window (Nader & Schiller 2000) | — |
|
|
510
|
+
| `async_usage.py` | `as_async()`, `SyncToAsync*` adapters, `encode_batch` | — |
|
|
511
|
+
| `persistence.py` | SQLiteStore, save/load state, export/import, prune | `[sqlite]` |
|
|
512
|
+
| `emotional_journal.py` | Multi-session journaling with full lifecycle | `[sqlite]` |
|
|
513
|
+
| `llm_appraisal.py` | LLM-backed appraisal via OpenAI-compatible API | `openai` |
|
|
514
|
+
| `httpx_llm_integration.py` | httpx LLMCallable, `.env` config, 7 API deep-dives | `httpx` |
|
|
515
|
+
| `sentence_transformers_embedder.py` | `SequentialEmbedder` with real embeddings | `sentence-transformers` |
|
|
516
|
+
| `visualization.py` | All 8 matplotlib plot types | `[viz]` |
|
|
517
|
+
| `resonance_network.py` | Resonance graph and link-type distribution | `[viz]` |
|
|
518
|
+
| `retrieval_signals.py` | 6-signal decomposition, radar chart, weight heatmap | `[viz]` |
|
|
519
|
+
|
|
520
|
+
Run any script: `python examples/<script>.py`
|
|
521
|
+
|
|
522
|
+
## Development
|
|
523
|
+
|
|
524
|
+
```bash
|
|
525
|
+
make check # lint + typecheck + test
|
|
526
|
+
make cov # tests with branch coverage report
|
|
527
|
+
make bench # fidelity + performance benchmarks
|
|
528
|
+
|
|
529
|
+
# Real-LLM tests (require API key):
|
|
530
|
+
make test-llm # end-to-end integration tests
|
|
531
|
+
make bench-appraisal # Scherer CPM prompt quality benchmarks
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### LLM test environment variables
|
|
535
|
+
|
|
536
|
+
| Variable | Default | Purpose |
|
|
537
|
+
|---|---|---|
|
|
538
|
+
| `EMOTIONAL_MEMORY_LLM_API_KEY` | — | API key (required) |
|
|
539
|
+
| `EMOTIONAL_MEMORY_LLM_BASE_URL` | `https://api.openai.com/v1` | OpenAI-compatible endpoint |
|
|
540
|
+
| `EMOTIONAL_MEMORY_LLM_MODEL` | `gpt-4o-mini` | Model |
|
|
541
|
+
| `EMOTIONAL_MEMORY_LLM_REPEATS` | `3` | Repeats per phrase in quality benchmarks |
|
|
542
|
+
|
|
543
|
+
## License
|
|
544
|
+
|
|
545
|
+
MIT — see [LICENSE](LICENSE)
|