EmotionalMemory (sync)¶
emotional_memory.engine.EmotionalMemory(store: MemoryStore, embedder: Embedder, appraisal_engine: AppraisalEngine | None = None, config: EmotionalMemoryConfig | None = None, state_store: AffectiveStateStore | None = None, query_classifier: QueryClassifier | None = None)
¶
Emotional memory system for LLMs based on Affective Field Theory.
The sync engine is intended for single-threaded use; concurrent encode() /
observe() from multiple threads can interleave affective-state updates.
Use :class:AsyncEmotionalMemory for concurrent async workloads.
Usage::
store = InMemoryStore()
embedder = MyEmbedder() # implements Embedder protocol
engine = EmotionalMemory(store, embedder)
# Encode with automatic appraisal (if engine provided) or manual affect
engine.set_affect(CoreAffect(valence=0.8, arousal=0.6))
memory = engine.encode("Just completed a difficult project successfully!")
# Retrieve — emotionally weighted by current state
results = engine.retrieve("challenging work accomplishment")
Source code in src/emotional_memory/engine.py
__len__() -> int
¶
clear_persisted_state() -> None
¶
close() -> None
¶
Release resources held by store, state store, and appraisal engine.
Calls close() on each collaborator that exposes it (e.g.
SQLiteStore, LLMAppraisalEngine with an httpx-backed LLM).
Safe on collaborators without cleanup. The embedder is not owned by
the engine and remains the caller's responsibility.
Source code in src/emotional_memory/engine.py
delete(memory_id: str) -> None
¶
elaborate(memory_id: str) -> Memory | None
¶
Run full appraisal on a fast-path (pending) memory and blend core_affect.
Dual-path slow path (LeDoux, 1996): runs the full Scherer appraisal on the memory's content, blends the appraised affect with the raw fast-path affect, updates consolidation_strength, clears pending_appraisal, and opens the reconsolidation window.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
memory_id
|
str
|
ID of the memory to elaborate. |
required |
Returns:
| Type | Description |
|---|---|
Memory | None
|
The updated Memory, or None if the memory does not exist, |
Memory | None
|
is not pending appraisal, or no appraisal engine is configured. |
Source code in src/emotional_memory/engine.py
elaborate_pending() -> list[Memory]
¶
Elaborate all memories with pending_appraisal=True.
Convenience method to run the slow appraisal path on all fast-path memories in the store. Each memory is elaborated in sequence.
Returns:
| Type | Description |
|---|---|
list[Memory]
|
List of updated Memory objects (only those that were elaborated). |
Source code in src/emotional_memory/engine.py
encode(content: str, appraisal: AppraisalVector | None = None, metadata: dict[str, Any] | None = None) -> Memory
¶
Encode content into emotional memory.
Pipeline
- Compute/use appraisal → derive CoreAffect
- Update AffectiveState (core_affect, momentum, mood)
- Build EmotionalTag (snapshot of all 5 layers)
- Embed content
- Store memory
- Build resonance links against existing memories
- Update stored memory with resonance links
Source code in src/emotional_memory/engine.py
311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 | |
encode_batch(contents: list[str], metadata: list[dict[str, Any]] | None = None) -> list[Memory]
¶
Encode multiple contents using a single embed_batch call.
Uses embed_batch() for efficient batched embedding (G6), then encodes
each item sequentially so that AffectiveState evolves naturally.
Resonance links are built against the store state at each step, meaning
later items in the batch can link to earlier ones. Per-item appraisal is
computed up front in parallel (see _appraise_many).
Source code in src/emotional_memory/engine.py
712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 | |
export_memories() -> list[dict[str, Any]]
¶
Export all memories as a list of JSON-serialisable dicts.
Suitable for backup or migration between store backends. The output
can be restored with import_memories().
Source code in src/emotional_memory/engine.py
get(memory_id: str) -> Memory | None
¶
get_current_mood(now: datetime | None = None) -> MoodField
¶
Return the mood field regressed to now without modifying state.
Useful for read-only mood inspection between encode/retrieve calls.
If mood_decay is not configured, returns the frozen mood.
Source code in src/emotional_memory/engine.py
get_state() -> AffectiveState
¶
import_memories(data: list[dict[str, Any]], *, overwrite: bool = False) -> int
¶
Import memories from a list of dicts produced by export_memories().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
list[dict[str, Any]]
|
List of memory dicts (as returned by |
required |
overwrite
|
bool
|
When True, existing memories with the same ID are replaced. When False (default), duplicates are skipped silently. |
False
|
Returns:
| Type | Description |
|---|---|
int
|
Number of memories actually written. |
Source code in src/emotional_memory/engine.py
list_all() -> list[Memory]
¶
load_state(data: dict[str, Any]) -> None
¶
Restore a previously saved affective state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
dict[str, Any]
|
A dict produced by a previous |
required |
Source code in src/emotional_memory/engine.py
observe(content: str, appraisal: AppraisalVector | None = None, metadata: dict[str, Any] | None = None) -> EmotionalTag
¶
Update affective state from content without storing a retrievable memory.
Source code in src/emotional_memory/engine.py
persist_state() -> dict[str, Any]
¶
prune(threshold: float = 0.05) -> int
¶
Remove memories whose effective consolidation strength has fallen below threshold.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
threshold
|
float
|
Minimum acceptable strength in [0, 1]. Memories whose
|
0.05
|
Returns:
| Type | Description |
|---|---|
int
|
Number of memories removed. |
Source code in src/emotional_memory/engine.py
reset_state() -> None
¶
restore_persisted_state() -> bool
¶
Load the last persisted affective state from the configured state store.
Source code in src/emotional_memory/engine.py
retrieve(query: str, top_k: int = 5, *, query_affect: CoreAffect | None = None, precomputed_weights: NDArray[np.float64] | None = None) -> list[Memory]
¶
Retrieve the top-k most relevant memories for the query.
Scoring uses all 6 AFT signals with mood-adaptive weights.
Two-pass strategy for spreading activation (G1): Pass 1 — score all candidates with empty active_ids to get an initial top-k ranking; their IDs become the active set. Pass 2 — re-score with the active set, allowing resonance links to boost memories associated with the initially top-ranked ones.
Pre-filter (G2): when the store is large, use search_by_embedding to narrow candidates to top_k * candidate_multiplier before full scoring.
Reconsolidation (D1): only triggered if the memory was last retrieved within the reconsolidation_window_seconds lability window.
Query affect: by default the affect-proximity signal (s3) uses the
current runtime affective state (self._state.core_affect). Pass an
explicit query_affect to score against the query's own affect instead
— e.g. the appraised affect of the query text — without mutating the
runtime state. This is the production-reachable path validated in
Addendum T (retrieve-time query appraisal); see
retrieve_with_query_appraisal.
Source code in src/emotional_memory/engine.py
389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 | |
retrieve_query_gated(query: str, top_k: int = 5, *, tau: float | None = None) -> list[Memory]
¶
Retrieve with the Addendum Y query-affect gate (Branch A).
Appraises the query text, then routes to semantic-only retrieval when
|valence| < tau (default query_affect_gate_tau, 0.2), else to
the retrieve-time query-appraisal path (retrieve_with_query_appraisal).
This is a validated safe wrapper: it recovers the neutral-query component of the off-regime penalty without mutating runtime affective state on the affect-carrying path.
Raises:
| Type | Description |
|---|---|
RuntimeError
|
if no appraisal engine is configured. |
Source code in src/emotional_memory/engine.py
retrieve_with_explanations(query: str, top_k: int = 5, *, query_affect: CoreAffect | None = None, precomputed_weights: NDArray[np.float64] | None = None) -> list[RetrievalExplanation]
¶
Retrieve memories plus a structured score decomposition.
Ranking is computed by the same two-pass retrieval pipeline used by
retrieve(). The returned score and breakdown reflect the ranking
state before retrieval-side updates such as reconsolidation and Hebbian
strengthening; the returned memory objects reflect the post-
retrieval stored state.
query_affect overrides the affect-proximity signal source exactly as
in retrieve() (default: current runtime state).
Source code in src/emotional_memory/engine.py
480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 | |
retrieve_with_query_appraisal(query: str, top_k: int = 5) -> list[Memory]
¶
Retrieve using the query's own appraised affect (Addendum T).
Appraises the query text with the configured appraisal engine and uses
the resulting :class:CoreAffect as query_affect for retrieval,
without mutating the runtime affective state. This is the
production-reachable retrieve-time query-appraisal path: it does not
require an oracle affect to be injected.
Pairs best with an appraisal engine using DIRECT_VAD_SCHEMA (the LLM
rates valence/arousal/dominance directly), which Addendum V found
estimates query affect more faithfully than the Scherer SEC projection.
Raises:
| Type | Description |
|---|---|
RuntimeError
|
if no appraisal engine is configured. |
Source code in src/emotional_memory/engine.py
save_state() -> dict[str, Any]
¶
Serialise the current affective state for persistence.
Returns a JSON-serialisable dict that can be stored alongside the
memory store and later restored with load_state(). The private
momentum history is included so that momentum computation continues
correctly after a restart.
Source code in src/emotional_memory/engine.py
set_affect(core_affect: CoreAffect) -> None
¶
Manually inject a CoreAffect (e.g. from external appraisal).
Updates core_affect, momentum, and mood.
Source code in src/emotional_memory/engine.py
update_memory(memory_id: str, *, content: str | None = None, tag: EmotionalTag | None = None, metadata: dict[str, Any] | None = None) -> Memory
¶
Replace selected fields of an existing memory.
None preserves a field. Changing content recomputes its embedding
before the store is mutated, so embedding failures leave the original
memory intact.
Source code in src/emotional_memory/engine.py
emotional_memory.engine.EmotionalMemoryConfig
¶
Bases: BaseModel
appraisal_max_concurrency: int = Field(default=8, ge=1)
class-attribute
instance-attribute
¶
Max concurrent appraisal calls in encode_batch(). Appraisal depends only
on content/context, so a batch's appraisals are computed concurrently and then
consumed by the order-preserving state-evolution loop (results are unchanged).
Async uses an asyncio.Semaphore; sync uses a thread pool — the llm
callable must be thread-safe (httpx.Client and KeywordAppraisalEngine are).
Set to 1 for fully sequential appraisal.
auto_categorize: bool = False
class-attribute
instance-attribute
¶
When True, run Plutchik categorization on encode and store EmotionLabel.
dual_path_encoding: bool = False
class-attribute
instance-attribute
¶
When True, fast-path encoding skips appraisal; call elaborate() later to run full appraisal and blend core_affect (LeDoux, 1996).
elaboration_learning_rate: float = 0.7
class-attribute
instance-attribute
¶
Blend ratio used in elaborate(): 70% appraised / 30% raw affect.
enable_appraisal: bool = True
class-attribute
instance-attribute
¶
When False, skip the appraisal engine call in _build_tag (ablation).
enable_momentum: bool = True
class-attribute
instance-attribute
¶
When False, zero the momentum-alignment retrieval weight (ablation of Layer 2).
enable_mood_signal: bool = True
class-attribute
instance-attribute
¶
When False, zero the mood-congruence retrieval weight (ablation of Layer 3).
enable_reconsolidation: bool = True
class-attribute
instance-attribute
¶
When False, skip the APE-gated reconsolidation window at retrieval time (ablation of Pearce-Hall 1980 APE gate). Predictive-learning (update_prediction) still runs.
enable_resonance: bool = True
class-attribute
instance-attribute
¶
When False, skip resonance link building and spreading activation (ablation of Layer 5).
max_content_length: int | None = Field(default=None, ge=1)
class-attribute
instance-attribute
¶
Optional maximum length for content / query strings. None = unlimited.
mood_decay: MoodDecayConfig | None = None
class-attribute
instance-attribute
¶
Time-based regression of mood toward baseline. None = disabled.
query_affect_gate_tau: float = Field(default=0.2, ge=0.0, le=2.0)
class-attribute
instance-attribute
¶
Neutral-query threshold for retrieve_query_gated() (Addendum Y, default 0.2).