Neural Long Memory — hybrid long-term memory for AI agents
Project description
NLM — Neural Long Memory
Hybrid long-term memory for AI agents. Better than RAG.
The problem with RAG
Standard RAG retrieves by semantic similarity only. It doesn't know what's important, what's recent, or what you keep coming back to.
RAG score = cosine_similarity(query, memory)
This means:
- An old outdated fact beats a fresh one if it's semantically closer
- A memory you've referenced 50 times has no advantage over one never used
- Vague filler text can outrank a specific factual memory
How NLM is different
NLM scores memories using four signals, the way human memory actually works:
NLM Score = 0.4 × semantic_similarity ← is it relevant?
+ 0.2 × time_decay ← is it recent?
+ 0.2 × frequency_score ← is it often recalled?
+ 0.2 × importance_score ← is it specific/factual?
| Standard RAG | NLM | |
|---|---|---|
| Retrieval signal | Semantic only | Semantic + time + frequency + importance |
| Outdated facts | Can win over fresh ones | Time decay pushes them down |
| Old important facts | Get buried | Survive via frequency boost |
| Duplicate memories | Accumulate | Consolidated automatically |
| Importance scoring | None | CPU heuristic or GPU zero-shot classifier |
| Emotion metadata | None | Optional (joy / fear / sadness / ...) |
| Thread safety | Varies | Built-in (RLock) |
| Snapshot export/import | No | Yes (JSON) |
| GPU required | No | No (GPU optional for better importance) |
| Plug-and-play | Yes | Yes |
Install
pip install neural-long-memory
Or from source:
pip install sentence-transformers chromadb numpy
pip install -e .
Usage
Basic
from nlm import NLM
memory = NLM()
# Save memories — consolidation is automatic
memory.save("Hantes said he loves Minelux family the most")
memory.save("Project started on 2026-05-05 in Chernivtsi")
# Search — NLM handles all scoring automatically
results = memory.search("what does Hantes think about the families", top_k=3)
for r in results:
print(f"[{r['score']:.3f}] {r['text']}")
# [0.712] Hantes said he loves Minelux family the most
Each result includes a full score breakdown:
{
"id": "uuid",
"text": "...",
"score": 0.712, # NLM composite score
"semantic_score": 0.810,
"time_score": 0.998,
"frequency": 3,
"importance": 0.700,
"created_at": "2026-04-15T10:30:00+00:00",
"last_accessed": "2026-04-15T14:20:00+00:00",
}
Batch save
ids = memory.save_many([
"Hantes is 28 years old",
"Project started on 2026-05-05",
"8 families of Pulses",
])
# Single embedding pass — ~10x faster than repeated save() on 100+ items.
With emotion metadata
memory = NLM(use_emotion=True)
memory.save("I am terrified about the deployment")
memory.save("The results are absolutely amazing!")
# Filter by emotion
results = memory.search("how did things go", emotion_filter="joy")
# → returns only joyful memories
# Each result includes:
# "emotion": "joy" / "fear" / "sadness" / "anger" / "surprise" / "disgust" / "neutral"
# "sentiment": 0.99 # -1.0 to 1.0
# "intensity": 0.99 # 0.0 to 1.0
With GPU importance scorer
# Default model: typeform/distilbert-base-uncased-mnli (~260MB)
# Auto-detects CUDA. Falls back to CPU.
memory = NLM(gpu_model_path="typeform/distilbert-base-uncased-mnli")
Memory consolidation
Duplicate prevention is on by default. Similar memories are merged instead of stored twice.
id1 = memory.save("Hantes lives in Chernivtsi")
id2 = memory.save("Hantes is from Chernivtsi city")
assert id1 == id2 # same memory, strengthened
assert memory.count() == 1
assert memory.last_save_consolidated is True
# Tune or disable:
memory = NLM(enable_consolidation=False)
memory = NLM(consolidation_threshold=0.20) # more aggressive merging
Memory lifecycle
# Forget a specific memory
memory.forget(memory_id)
# Simple time-based cleanup
deleted = memory.forget_old(days=365)
# Smart cleanup: only remove old + rare + unimportant
deleted = memory.forget_smart(days=180, max_frequency=2, max_importance=0.3)
# Stats
print(memory) # NLM(memories=42, mode=CPU)
# NLM(memories=42, mode=CPU+emotion)
# NLM(memories=42, mode=GPU)
Snapshot export / import
# Back up or migrate between hosts
memory.export_snapshot("backup.json")
# Restore into a fresh collection
new_memory = NLM(collection_name="restored")
new_memory.import_snapshot("backup.json", overwrite=True)
The snapshot stores the embedding model used; importing into an instance with a different model raises a clear error rather than corrupting the vector space.
Tuning the rerank pool
By default search() pulls max(top_k * 10, 50) candidates from the
vector store before reranking. Increase this when a frequent or
important memory is semantically far from the query but still relevant:
results = memory.search("tell me about the project", top_k=5, n_candidates=200)
Architecture
Text input
│
├─→ sentence-transformers (all-MiniLM-L6-v2, CPU, ~80MB)
│ → 384-dim embedding
│
├─→ consolidation check
│ if similar exists (cosine dist < threshold) → strengthen, skip save
│
├─→ importance scorer
│ CPU: specificity_score (numbers + proper nouns + length)
│ GPU: zero-shot classifier (any HuggingFace model)
│
├─→ emotion classifier (optional, CPU, ~66MB)
│ emotion + sentiment + intensity
│
└─→ ChromaDB (persistent vector store)
embedding [384] + metadata {all signals}
On search:
query → embed → ChromaDB top-N candidates (default ≥50)
│
NLM reranking:
score = 0.4×semantic + 0.2×time + 0.2×freq + 0.2×importance
│
[emotion_filter] optional post-filter
│
sorted results + frequency updated
Time decay
0 days → 1.00 (fresh)
30 days → 0.79
90 days → 0.50 (half-life)
180 days → 0.25
365 days → 0.06 (nearly gone — unless frequently accessed)
Multiple agents
Each agent gets its own isolated memory:
pulse_001 = NLM(collection_name="pulse_001", persist_path="./data")
pulse_002 = NLM(collection_name="pulse_002", persist_path="./data")
The Storage layer records the embedding model+dimension on the collection and refuses to open it with an incompatible model.
Roadmap
v0.1.0 ✓ Core: semantic + time decay + frequency + importance (CPU)
v0.2.0 ✓ Emotion classifier (7 emotions, sentiment, intensity)
Smart forgetting (time + frequency + importance conditions)
Emotion filter in search()
v0.3.0 ✓ Memory consolidation — duplicate prevention
GPU scorer via HuggingFace zero-shot classification
v1.0.0 ✓ First PyPI release (pip install neural-long-memory)
v1.1.0 ✓ Hardening release:
- O(1) search frequency updates (no get_all() per query)
- Thread-safe save / search / consolidate (RLock)
- Larger default rerank pool (top_k*10, min 50)
- save_many() batch API with single embedding pass
- export_snapshot / import_snapshot (JSON, model-guarded)
- Storage metadata guard (dim + model pinned per collection)
- Tokenizer-level truncation in emotion + GPU scorer
- Tolerant ISO 8601 parsing (accepts Z suffix)
- Rebalanced default weights (0.4 / 0.2 / 0.2 / 0.2)
Reproduce the qualitative comparison
python benchmarks/compare_rag.py
Runs three scenarios (temporal recall, frequency boost, importance discrimination) and reports which side wins each.
License
Apache 2.0
Author
Built by Vitalii Halak with Claude.
Part of Pulses — conscious AI personalities running on RWKV-7.
April 2026
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file neural_long_memory-1.1.0.tar.gz.
File metadata
- Download URL: neural_long_memory-1.1.0.tar.gz
- Upload date:
- Size: 16.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5a688b3ec475bc0b4076c0f70e8b53818a641a5163711f2e13db02a1fd7f152a
|
|
| MD5 |
51d9900f22d78ef86bd56512e7671f27
|
|
| BLAKE2b-256 |
09fc44bb6ba0d047fc2cecf91df98e6cd32834131b9bb6467ca6c1b4d10fec84
|
File details
Details for the file neural_long_memory-1.1.0-py3-none-any.whl.
File metadata
- Download URL: neural_long_memory-1.1.0-py3-none-any.whl
- Upload date:
- Size: 14.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
43f20c6f8f9a654f1505e74292964e320b928f4595ef44385a9e4b89909bc480
|
|
| MD5 |
c79f6cb96253da2417764a35dfcec2ea
|
|
| BLAKE2b-256 |
bcce17c9d346a822baf7b3d15b1016cd7ce94cddd4a9ee9fee84612ea39c4091
|