NeatMem
Lightweight local memory for agents, with cleaner deduplication, less memory pollution, and more relevant recall.
NeatMem is built for developers who want practical long-term memory without adopting a full Memory OS or hosted memory service. It focuses on keeping local agent memory clean: merging repeated facts, preventing AI suggestions, guesses, and tool noise from being saved as user facts, saving memories with enough context, and filtering irrelevant recalls.
Status: v0.1-preview. NeatMem is usable for local development and mem0-compatible integrations, but APIs, packaging, and integrations may still change.
Benchmark: 90.80% accuracy on LOCOMO, fully reproducible locally (3-run mean; MiniMax-M3 answer + judge, SiliconFlow bge-m3 embedding). See the evaluation guide for benchmark reproduction steps.
Why NeatMem?
Agent memory is easy to start but hard to keep clean.
Common problems include:
- duplicate memories accumulating over time
- assistant suggestions being stored as user facts
- vague memories losing their original context
- semantically related memories not being merged
- irrelevant memories being recalled because of weak vector matches
- local agent tools needing a simple self-hosted memory backend
NeatMem focuses on one narrow goal:
Local agent memory that stays clean, inspectable, and easy to tune.
It is not a full Memory OS and not an enterprise multi-tenant memory system.
Features
-
LLM-assisted memory decisions
- Classifies each new memory as
add,none, orupdate(listwise, single LLM call). DEDUP_MODEcontrols behavior:skip(keep both),replace(overwrite),rewrite(LLM merge),edit(LLM patch).
- Classifies each new memory as
-
Sequential memory updates
- Processes new memories one by one so each merge sees the latest stored version.
- Helps avoid overwrite conflicts when several new facts update the same old memory.
-
Less memory pollution
- Avoids saving AI suggestions, guesses, or tool noise as user facts.
- Tracks whether each memory came from the user, assistant, or tool output.
-
Memories with enough context
- Adds missing context from the same message batch when needed.
- Example: “during development” can become “while developing a mem0-based memory module”.
-
More relevant recall
- Multi-signal retrieval: dense vector search + BM25 sparse matching + entity boosting.
- LLM listwise rerank filters and reorders candidates before injection into agent context.
-
Lightweight local storage
- Runs with local Qdrant (embedded or server mode) by default.
- Does not require Redis, a hosted memory service, or a full database stack.
-
Modular signal architecture
- Message store, BM25, and entity modules are decoupled under
neatmem/storage/andneatmem/signals/. - Each signal can be toggled via environment variables (
ENABLE_BM25,ENABLE_ENTITY,ENABLE_GRAPH).
- Message store, BM25, and entity modules are decoupled under
-
Optional graph memory (opt-in)
- Entity-relation storage via KuzuDB, toggled by
ENABLE_GRAPH. - Off by default; graph relations injection into answer prompt is experimental (
GRAPH_INJECT_RELATIONS, known harmful on LOCOMO).
- Entity-relation storage via KuzuDB, toggled by
-
OpenClaw and mem0-style integration
- Implements the core mem0-style memory endpoints needed for local agent workflows.
- Designed to support OpenClaw platform-mode memory integration.
Compatibility
NeatMem implements a mem0-compatible API subset for local agent memory workflows:
- add memory
- search memory
- list memories
- update memory
- delete memory
- health check
It is designed to work with OpenClaw's and Hermes' memory plugin flows and other mem0-style integrations. v0.1 does not aim to cover every mem0 SDK feature or mem0 hosted-platform behavior.
A remote client is provided for programmatic access:
from neatmem import MemoryClient
client = MemoryClient(host="http://localhost:8790")
client.add("My name is Alex", user_id="default_user")
results = client.search("What is my name?", filters={"user_id": "default_user"})
Quick start
1. Install
pip install -r requirements.txt
pip install -e .
The second command registers the neatmem CLI.
Optional:
- BM25 keyword search (enabled by default) needs spaCy:
pip install -e ".[nlp]" && python -m spacy download en_core_web_sm
- Local reranker model (alternative to LLM rerank):
pip install -e ".[local-reranker]"
2. Configure environment variables
cp .env.example .env
Edit .env and configure your LLM and embedding provider.
Minimum configuration for OpenAI-compatible LLM providers:
OPENAI_API_KEY=your-api-key
OPENAI_BASE_URL=https://your-openai-compatible-endpoint/v1
LLM_MODEL=qwen-max-latest
EMBEDDING_PROVIDER=siliconflow
SILICONFLOW_API_KEY=your-siliconflow-api-key
3. Start the server
neatmem serve
The server listens on:
http://localhost:8790
To use a different port:
neatmem serve --port 9000
View all options:
neatmem serve --help
CLI flags override .env environment variables; see the Configuration table below for the full list.
Alternatively, start directly with Python:
python -m neatmem.main
Check health:
curl http://localhost:8790/health
Expected response:
{"status":"healthy","timestamp":"..."}
Configuration
NeatMem reads configuration from .env.
| Variable | Required | Default | Description |
|---|---|---|---|
NEATMEM_HOST |
no | 0.0.0.0 |
Server bind host |
NEATMEM_PORT |
no | 8790 |
Server port |
NEATMEM_URL |
no | http://localhost:8790 |
Base URL used by MemoryClient |
NEATMEM_API_KEY |
no | - | API key sent as Authorization: Token header by MemoryClient (server ignores it) |
OPENAI_API_KEY |
yes | - | API key for OpenAI-compatible LLM provider |
OPENAI_BASE_URL |
yes | - | OpenAI-compatible API base URL |
LLM_MODEL |
no | qwen-max-latest |
LLM model name |
EMBEDDING_PROVIDER |
no | siliconflow |
siliconflow or xinference |
SILICONFLOW_API_KEY |
conditional | - | Required when EMBEDDING_PROVIDER=siliconflow |
EMBEDDING_MODEL |
no | BAAI/bge-m3 |
Embedding model name |
EMBEDDING_BASE_URL |
no | https://api.siliconflow.cn/v1 |
Embedding API base URL |
EMBEDDING_DIMS |
no | auto-detect | Embedding dimensions. When unset, auto-detected from a startup probe; set explicitly to enforce a dimension check at boot |
XINFERENCE_SERVER_URL |
conditional | http://localhost:9997 |
Required when using Xinference |
XINFERENCE_MODEL_UID |
conditional | bge-m3 |
Xinference embedding model UID |
QDRANT_PATH |
no | qdrant_db |
Local Qdrant storage path (embedded mode) |
QDRANT_HOST |
no | - | Qdrant server host (sets server mode; overrides QDRANT_PATH) |
QDRANT_PORT |
no | 6333 |
Qdrant server port |
DEDUP_MODE |
no | skip |
Dedup behavior: off, skip, replace, rewrite, edit |
ENABLE_BM25 |
no | true |
Enable BM25 sparse search signal |
ENABLE_ENTITY |
no | false |
Enable entity extraction and boosting |
ENABLE_GRAPH |
no | false |
Enable graph memory (KuzuDB entity-relation storage). Graph hooks are no-op when disabled |
KUZU_DB_PATH |
conditional | - | KuzuDB database file path. Required when ENABLE_GRAPH=true |
GRAPH_THRESHOLD |
no | 0.7 |
Entity match threshold for graph retrieval |
GRAPH_SEARCH_TOP_K |
no | 5 |
Max relations returned per speaker from graph search |
GRAPH_INJECT_RELATIONS |
no | false |
Inject graph relations into answer prompt. Only effective when ENABLE_GRAPH=true. Experimental: -0.57pp on LOCOMO (2026-07-22), off by default |
GRAPH_EMBEDDING_MODEL |
no | BAAI/bge-m3 |
Embedding model for graph entities (defaults to main embedding model) |
GRAPH_EMBEDDING_DIMS |
no | 1024 |
Embedding dimensions for graph entities |
GRAPH_EMBEDDING_BASE_URL |
no | https://api.siliconflow.cn/v1 |
Embedding API base URL for graph entities |
GRAPH_EMBEDDING_API_KEY |
no | - | Embedding API key for graph entities. Defaults to SILICONFLOW_API_KEY |
LLM_RERANK |
no | true |
Enable LLM listwise rerank for recall |
RERANK_MODE |
no | llm_listwise |
Rerank strategy |
RERANK_CANDS |
no | 20 |
Head size for LLM listwise rerank: only top N candidates are reordered, the rest are appended in original order. Only effective when LLM_RERANK=true |
RERANK_MAX_CONCURRENT |
no | 4 |
Max concurrent LLM rerank calls (protects against API rate limits) |
MERGE_STRATEGY |
no | off |
Deprecated; use DEDUP_MODE instead |
DEDUP_THINKING |
no | false |
Enable LLM thinking for dedup |
EDIT_THINKING |
no | false |
Enable LLM thinking for edit mode (DEDUP_MODE=edit) |
HISTORY_DB_PATH |
no | {QDRANT_PATH}/history.db |
SQLite message history database path |
EXTRACT_LAST_K_MESSAGES |
no | 10 |
Number of recent messages fed to extraction as context |
MESSAGE_STORE_BACKEND |
no | sqlite |
Message store backend: sqlite or none |
ENTITY_EXTRACTOR_BACKEND |
no | ner |
Entity extractor: ner or llm |
ENTITY_STORE_BACKEND |
no | qdrant |
Entity store backend |
RERANKER_MODEL_PATH |
no | - | Optional local Sentence-Transformers reranker |
RERANKER_DEVICE |
no | cpu |
Reranker device |
RERANKER_BATCH_SIZE |
no | 32 |
Reranker batch size |
RERANKER_TOP_K |
no | 5 |
Reranker top-k |
HF_ENDPOINT |
no | https://hf-mirror.com |
HuggingFace mirror endpoint |
Custom prompts
Every core prompt can be replaced — either with a built-in variant id or with your own prompt file, from_pretrained-style. No code changes needed.
| Prompt | Env var / CLI flag | Built-in ids | Used when |
|---|---|---|---|
| Fact extraction | EXTRACTION_PROMPT / --extraction-prompt |
- | always (write path) |
| Dedup decision | DEDUP_PROMPT / --dedup-prompt |
zh (default), en |
DEDUP_MODE=skip/replace/rewrite/edit |
| Merge rewrite | REWRITE_PROMPT / --rewrite-prompt |
- | DEDUP_MODE=rewrite |
| Patch edit | EDIT_PROMPT / --edit-prompt |
- | DEDUP_MODE=edit |
| Rerank | RERANK_PROMPT / --rerank-prompt |
- | LLM listwise rerank |
Switch to the English dedup prompt (validated on LOCOMO, 2026-07-24):
neatmem serve --dedup-prompt en
Use your own prompt:
# 1. Export the example templates (they are the exact built-in defaults)
# From a git clone:
mkdir -p my_prompts && cp neatmem/prompts/examples/*.txt my_prompts/
# From a pip install:
python - <<'EOF'
from importlib.resources import files
import shutil, os
os.makedirs("my_prompts", exist_ok=True)
for f in files("neatmem.prompts").joinpath("examples").iterdir():
shutil.copy(f, "my_prompts/")
EOF
# 2. Edit the one you want (keep every {placeholder} intact,
# including the {{ }} escaping in JSON examples)
# 3. Point the server at it
neatmem serve --dedup-prompt /absolute/path/to/my_prompts/dedup_zh.example.txt
Notes:
- Prompts are loaded once at startup; restart the server after editing a file.
- A value that is neither a known id nor an existing file, a missing file, or a missing
{placeholder}fails at startup with a clear error. - Prefer absolute paths; relative paths resolve against the server's working directory.
OpenClaw integration
NeatMem includes an OpenClaw plugin under openclaw/. Build it and install it as a linked local plugin during development:
cd /path/to/NeatMem/openclaw
npm install
npm run build
cd /path/to/NeatMem
openclaw plugins install ./openclaw --link
After changing plugin TypeScript source, rebuild before reinstalling or restarting OpenClaw.
The plugin id is openclaw-neatmem. It talks to NeatMem through the local mem0-compatible HTTP API.
Example OpenClaw configuration:
{
"plugins": {
"slots": {
"memory": "openclaw-neatmem"
},
"entries": {
"openclaw-neatmem": {
"enabled": true,
"config": {
"mode": "platform",
"apiKey": "neatmem-local",
"userId": "default_user",
"baseUrl": "http://localhost:8790"
}
}
}
}
}
Then check:
openclaw mem0 status
The CLI command remains openclaw mem0 for compatibility, but the active plugin id should be openclaw-neatmem and the backend should point to http://localhost:8790.
Hermes integration
NeatMem includes a Hermes Agent memory provider under hermes/. With the NeatMem server running at http://localhost:8790:
hermes plugins install kanhaoning/NeatMem/hermes --enable
hermes config set memory.provider neatmem
The plugin registers five memory tools (neatmem_search, neatmem_add, neatmem_list, neatmem_update, neatmem_delete) and recalls memories automatically on each turn. Optional configuration via ~/.hermes/neatmem.json:
{
"base_url": "http://localhost:8790",
"user_id": "myname",
"rerank": true
}
Verify: tell Hermes "remember that I prefer dark themes", then ask about it in a new session. See hermes/README.md for the full configuration reference and troubleshooting.
API examples
Health check
curl http://localhost:8790/health
Add memory
curl -X POST http://localhost:8790/v1/memories/ \
-H "Content-Type: application/json" \
-d '{
"messages": [
{"role": "user", "content": "My name is Alex and I work on agent memory systems."},
{"role": "assistant", "content": "Nice to meet you, Alex."}
],
"user_id": "default_user",
"infer": true
}'
Search memory
curl -X POST http://localhost:8790/v2/memories/search/ \
-H "Content-Type: application/json" \
-d '{
"query": "What is Alex working on?",
"filters": {"user_id": "default_user"},
"top_k": 10,
"threshold": 0.1
}'
List memories
curl -X POST http://localhost:8790/v2/memories/ \
-H "Content-Type: application/json" \
-d '{
"filters": {"user_id": "default_user"},
"page": 1,
"page_size": 100
}'
Get memory
curl http://localhost:8790/v1/memories/{memory_id}/
Update memory
curl -X PUT http://localhost:8790/v1/memories/{memory_id}/ \
-H "Content-Type: application/json" \
-d '{
"text": "Alex works on local-first agent memory systems.",
"metadata": {"source": "manual_update"}
}'
Delete memory
curl -X DELETE http://localhost:8790/v1/memories/{memory_id}/
How it works
Add flow
messages
↓
retrieve last-k messages as extraction context
↓
LLM memory extraction (with last-k context)
↓
context completion and source tracking
↓
sequential LLM-assisted memory decisions
├─ add -> store as new memory
├─ none -> skip (duplicate)
└─ update -> merge per DEDUP_MODE (skip/replace/rewrite/edit)
↓
write to vector store + BM25 index + entity store
Search flow
query
↓
dense vector search + BM25 sparse search + entity boosting
↓
LLM listwise rerank
↓
threshold filtering
↓
results
Development probes
Memory quality iteration is done through probe/, which contains OpenClaw end-to-end probes and extraction simulation scripts. It is not a benchmark suite.
Design notes
NeatMem is designed around a few constraints:
- keep the plugin layer thin
- keep the backend self-hosted and debuggable
- do not require Redis or a background scheduler
- prefer memory quality over feature breadth
- preserve compatibility with mem0-style APIs where possible
Limitations
NeatMem is in active development. Current limitations:
- APIs and packaging may still change.
- No dashboard or GUI.
- No multi-tenant permission system.
- OpenClaw is the primary tested integration path.
- Prompt behavior is still being iterated and may vary across models.
- BM25 lemmatization is basic; bilingual (Chinese/English) tokenization needs improvement.
Roadmap
- Bilingual multi-signal support (improved Chinese/English BM25 and entity extraction)
- PyPI package publication
- Memory inspection and export/import tools
- Richer recall diagnostics
License
MIT License.
Acknowledgements
NeatMem is inspired by the mem0 project and mem0-style memory API patterns, and is designed to interoperate with OpenClaw memory plugin flows. Upstream license notices should be preserved where applicable.
Some utility functions in neatmem/utils/spacy/ (spacy_models.py, entity_extraction.py, lemmatization.py) are vendored from mem0 v2.0.0 (Apache-2.0); see file headers for modification notes.
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 neatmem-0.1.0.tar.gz.
File metadata
- Download URL: neatmem-0.1.0.tar.gz
- Upload date:
- Size: 776.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ed9fb73ce27a11c6c87fdae8420d40e3be00b0884d4205a1db1b5279d1d94f54
|
|
| MD5 |
e2457b68f695514792080eb9904f1e3b
|
|
| BLAKE2b-256 |
af9f34edbf9fe35fd748e902630834a2914010a5b7d2816fcbe44d645e527715
|
File details
Details for the file neatmem-0.1.0-py3-none-any.whl.
File metadata
- Download URL: neatmem-0.1.0-py3-none-any.whl
- Upload date:
- Size: 799.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
65d30e813f562eb1c1aed89f9623436240ba80af0274e476cfa3d08bfd653487
|
|
| MD5 |
a7cdd36cdc152f8ad046cff96b7db167
|
|
| BLAKE2b-256 |
bf3136569dd0accf2b3ca956bad74ec62217110ecbc5cf5a4da73a157bd0f0e3
|