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.
Files changed (62) hide show
  1. emotional_memory-0.5.1/LICENSE +21 -0
  2. emotional_memory-0.5.1/PKG-INFO +545 -0
  3. emotional_memory-0.5.1/README.md +491 -0
  4. emotional_memory-0.5.1/pyproject.toml +96 -0
  5. emotional_memory-0.5.1/setup.cfg +4 -0
  6. emotional_memory-0.5.1/src/emotional_memory/__init__.py +122 -0
  7. emotional_memory-0.5.1/src/emotional_memory/_math.py +21 -0
  8. emotional_memory-0.5.1/src/emotional_memory/affect.py +83 -0
  9. emotional_memory-0.5.1/src/emotional_memory/appraisal.py +134 -0
  10. emotional_memory-0.5.1/src/emotional_memory/appraisal_llm.py +404 -0
  11. emotional_memory-0.5.1/src/emotional_memory/async_adapters.py +143 -0
  12. emotional_memory-0.5.1/src/emotional_memory/async_engine.py +619 -0
  13. emotional_memory-0.5.1/src/emotional_memory/categorize.py +198 -0
  14. emotional_memory-0.5.1/src/emotional_memory/decay.py +84 -0
  15. emotional_memory-0.5.1/src/emotional_memory/embedders/__init__.py +8 -0
  16. emotional_memory-0.5.1/src/emotional_memory/embedders/sentence_transformers.py +60 -0
  17. emotional_memory-0.5.1/src/emotional_memory/engine.py +672 -0
  18. emotional_memory-0.5.1/src/emotional_memory/integrations/__init__.py +8 -0
  19. emotional_memory-0.5.1/src/emotional_memory/integrations/langchain.py +108 -0
  20. emotional_memory-0.5.1/src/emotional_memory/interfaces.py +68 -0
  21. emotional_memory-0.5.1/src/emotional_memory/interfaces_async.py +62 -0
  22. emotional_memory-0.5.1/src/emotional_memory/models.py +148 -0
  23. emotional_memory-0.5.1/src/emotional_memory/mood.py +172 -0
  24. emotional_memory-0.5.1/src/emotional_memory/py.typed +0 -0
  25. emotional_memory-0.5.1/src/emotional_memory/resonance.py +346 -0
  26. emotional_memory-0.5.1/src/emotional_memory/retrieval.py +389 -0
  27. emotional_memory-0.5.1/src/emotional_memory/state.py +139 -0
  28. emotional_memory-0.5.1/src/emotional_memory/stores/__init__.py +8 -0
  29. emotional_memory-0.5.1/src/emotional_memory/stores/in_memory.py +73 -0
  30. emotional_memory-0.5.1/src/emotional_memory/stores/sqlite.py +260 -0
  31. emotional_memory-0.5.1/src/emotional_memory/visualization.py +687 -0
  32. emotional_memory-0.5.1/src/emotional_memory.egg-info/PKG-INFO +545 -0
  33. emotional_memory-0.5.1/src/emotional_memory.egg-info/SOURCES.txt +60 -0
  34. emotional_memory-0.5.1/src/emotional_memory.egg-info/dependency_links.txt +1 -0
  35. emotional_memory-0.5.1/src/emotional_memory.egg-info/requires.txt +36 -0
  36. emotional_memory-0.5.1/src/emotional_memory.egg-info/top_level.txt +1 -0
  37. emotional_memory-0.5.1/tests/test_affect.py +138 -0
  38. emotional_memory-0.5.1/tests/test_appraisal.py +206 -0
  39. emotional_memory-0.5.1/tests/test_appraisal_llm.py +271 -0
  40. emotional_memory-0.5.1/tests/test_async_engine.py +924 -0
  41. emotional_memory-0.5.1/tests/test_categorize.py +235 -0
  42. emotional_memory-0.5.1/tests/test_comparative.py +135 -0
  43. emotional_memory-0.5.1/tests/test_dataset.py +80 -0
  44. emotional_memory-0.5.1/tests/test_decay.py +138 -0
  45. emotional_memory-0.5.1/tests/test_dual_path.py +292 -0
  46. emotional_memory-0.5.1/tests/test_engine.py +909 -0
  47. emotional_memory-0.5.1/tests/test_examples.py +43 -0
  48. emotional_memory-0.5.1/tests/test_init_exports.py +15 -0
  49. emotional_memory-0.5.1/tests/test_integration.py +156 -0
  50. emotional_memory-0.5.1/tests/test_langchain_adapter.py +197 -0
  51. emotional_memory-0.5.1/tests/test_llm_integration.py +142 -0
  52. emotional_memory-0.5.1/tests/test_math.py +46 -0
  53. emotional_memory-0.5.1/tests/test_models.py +108 -0
  54. emotional_memory-0.5.1/tests/test_mood.py +249 -0
  55. emotional_memory-0.5.1/tests/test_prediction.py +157 -0
  56. emotional_memory-0.5.1/tests/test_resonance.py +396 -0
  57. emotional_memory-0.5.1/tests/test_retrieval.py +334 -0
  58. emotional_memory-0.5.1/tests/test_sentence_transformers_extra.py +146 -0
  59. emotional_memory-0.5.1/tests/test_sqlite_store.py +511 -0
  60. emotional_memory-0.5.1/tests/test_state.py +130 -0
  61. emotional_memory-0.5.1/tests/test_store.py +82 -0
  62. 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
+ [![CI](https://github.com/gianlucamazza/emotional-memory/actions/workflows/ci.yml/badge.svg)](https://github.com/gianlucamazza/emotional-memory/actions/workflows/ci.yml)
58
+ [![PyPI](https://img.shields.io/pypi/v/emotional_memory)](https://pypi.org/project/emotional_memory/)
59
+ [![Python](https://img.shields.io/pypi/pyversions/emotional_memory)](https://pypi.org/project/emotional_memory/)
60
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ ![Circumplex](docs/images/circumplex.png)
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
+ ![Decay Curves](docs/images/decay_curves.png)
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
+ ![Yerkes-Dodson](docs/images/yerkes_dodson.png)
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
+ ![Retrieval Radar](docs/images/retrieval_radar.png)
330
+
331
+ ### Mood Field Evolution
332
+
333
+ Time series of valence, arousal, and dominance with dashed baselines showing the regression attractors.
334
+
335
+ ![Mood Evolution](docs/images/mood_evolution.png)
336
+
337
+ ### Adaptive Retrieval Weights
338
+
339
+ Heatmap showing how retrieval weights shift across different mood states (valence x arousal grid).
340
+
341
+ ![Adaptive Weights](docs/images/adaptive_weights_heatmap.png)
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
+ ![Resonance Network](docs/images/resonance_network.png)
348
+
349
+ ### Appraisal Radar (Scherer CPM)
350
+
351
+ Spider chart of the 5 Stimulus Evaluation Check dimensions.
352
+
353
+ ![Appraisal Radar](docs/images/appraisal_radar.png)
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)