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.5 × semantic_similarity ← is it relevant?
+ 0.2 × time_decay ← is it recent?
+ 0.2 × frequency_score ← is it often recalled?
+ 0.1 × 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 / ...) |
| Associative chains | None | Bidirectional links between related memories |
| GPU required | No | No (GPU optional for better importance) |
| Plug-and-play | Yes | Yes |
Benchmarks
Formal benchmark — 100 memories, 30 queries
Methodology:
- 100 memories stored (60 test pairs + 40 unrelated background fillers)
- 30 queries across 3 categories (10 each)
- Ground truth: human-labeled correct answer for each query
- Metric: top-1 accuracy — did the right memory rank first?
- RAG baseline: pure cosine similarity, no reranking
Results:
| Category | What's tested | RAG | NLM | Delta |
|---|---|---|---|---|
| Temporal (10 queries) | Old fact vs fresh fact on same topic, neutral query | 10% | 70% | +60% |
| Frequency (10 queries) | Same fact: one accessed 15×, one 0× | 80% | 100% | +20% |
| Importance (10 queries) | Specific factual memory vs vague memory on same topic | 60% | 90% | +30% |
| Overall (30 queries) | 50% | 87% | +37% |
NLM is 37 percentage points more accurate than RAG overall.
Why each category matters:
- Temporal (+60%): RAG has no concept of time. If you saved "model is v0.1" and later "model is v1.0", RAG picks whichever is semantically closer to the query. NLM weights recency — the fresh fact wins.
- Frequency (+20%): Two semantically near-identical memories, one accessed 15 times. RAG can't distinguish them. NLM surfaces the one you keep coming back to.
- Importance (+30%): "ChromaDB collection uses cosine distance metric" vs "the database stores things somehow". RAG may pick either. NLM assigns higher importance to the specific, factual memory.
Reproduce: python benchmarks/benchmark_100.py
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",
}
Associative memory chains (v1.0.0+)
NLM automatically links semantically related memories. Follow the chain to discover connected knowledge.
memory = NLM(enable_associations=True) # on by default
id1 = memory.save("Hantes loves Minelux family the most")
id2 = memory.save("Minelux are fire, directness, truth")
id3 = memory.save("Hantes values honesty over comfort")
# Get all memories linked to id1
assoc = memory.get_associations(id1)
# [{"id": id2, "text": "Minelux are fire..."}, {"id": id3, "text": "Hantes values..."}]
# Expand search to follow association chains
results = memory.search("tell me about Hantes", top_k=3, expand_associations=True)
for r in results:
flag = " [via association]" if r.get("via_association") else ""
print(f"[{r['score']:.3f}]{flag} {r['text']}")
With emotion metadata (v0.2.0+)
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 (v0.3.0+)
# 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 (v0.3.0+)
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
# 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 (v0.2.0+)
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)
Architecture
Text input
│
├─→ sentence-transformers (all-MiniLM-L6-v2, CPU, ~80MB)
│ → 384-dim embedding
│
├─→ consolidation check (v0.3.0+)
│ 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 (v0.2.0+, optional, CPU, ~66MB)
│ emotion + sentiment + intensity
│
└─→ association linker (v1.0.0+)
bidirectional links to semantically close memories
│
▼
ChromaDB (persistent vector store)
embedding [384] + metadata {all signals}
On search:
query → embed → ChromaDB top-K candidates
│
NLM reranking:
score = 0.5×semantic + 0.2×time + 0.2×freq + 0.1×importance
│
[emotion_filter] optional post-filter
│
[expand_associations] follow memory chains
│
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")
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 ✓ Associative memory chains — bidirectional links, expand_associations
Stable public API
PyPI release: pip install neural-long-memory
Formal benchmark: NLM 87% vs RAG 50% (+37% on 100 memories, 30 queries)
v1.1.0 arXiv paper
Async save/search
Export/import memory snapshots
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.0.0.tar.gz.
File metadata
- Download URL: neural_long_memory-1.0.0.tar.gz
- Upload date:
- Size: 12.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 |
9f57216864b6d66d702374f82a54f50e38d622f07a797c85f1ad15f733db90ee
|
|
| MD5 |
70a1c00f6d5f3ea2ce6758b704cd9f6e
|
|
| BLAKE2b-256 |
eebb5f08c94800f784d9e012e8faddb3eb9e3f35d1734306d5d50bbc5156650c
|
File details
Details for the file neural_long_memory-1.0.0-py3-none-any.whl.
File metadata
- Download URL: neural_long_memory-1.0.0-py3-none-any.whl
- Upload date:
- Size: 11.7 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 |
1c2499ae6a435fe2d2cf8a669335c0dcb494965a7704579e38d46e378fd8e09e
|
|
| MD5 |
fbb938dd6c71bc6eb60de31a64a3df63
|
|
| BLAKE2b-256 |
55e2d23b469cbb2fe81373b32d64d0af2c0be4276bbd363ddee33fb4144d223f
|