neatmem 0.1.0__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.
- neatmem-0.1.0/LICENSE +21 -0
- neatmem-0.1.0/PKG-INFO +513 -0
- neatmem-0.1.0/README.md +490 -0
- neatmem-0.1.0/neatmem/__init__.py +5 -0
- neatmem-0.1.0/neatmem/cli.py +162 -0
- neatmem-0.1.0/neatmem/client.py +349 -0
- neatmem-0.1.0/neatmem/config.py +197 -0
- neatmem-0.1.0/neatmem/embeddings.py +139 -0
- neatmem-0.1.0/neatmem/evaluation/__init__.py +0 -0
- neatmem-0.1.0/neatmem/evaluation/dataset/locomo10.json +66751 -0
- neatmem-0.1.0/neatmem/evaluation/metrics/__init__.py +0 -0
- neatmem-0.1.0/neatmem/evaluation/metrics/llm_judge.py +321 -0
- neatmem-0.1.0/neatmem/evaluation/metrics/utils.py +211 -0
- neatmem-0.1.0/neatmem/evaluation/prompts.py +148 -0
- neatmem-0.1.0/neatmem/evaluation/run_experiments.py +38 -0
- neatmem-0.1.0/neatmem/evaluation/src/__init__.py +0 -0
- neatmem-0.1.0/neatmem/evaluation/src/neatmem/__init__.py +0 -0
- neatmem-0.1.0/neatmem/evaluation/src/neatmem/add.py +145 -0
- neatmem-0.1.0/neatmem/evaluation/src/neatmem/client.py +51 -0
- neatmem-0.1.0/neatmem/evaluation/src/neatmem/ingest_locomo.py +189 -0
- neatmem-0.1.0/neatmem/evaluation/src/neatmem/search.py +213 -0
- neatmem-0.1.0/neatmem/exceptions.py +9 -0
- neatmem-0.1.0/neatmem/main.py +693 -0
- neatmem-0.1.0/neatmem/memory_add.py +1096 -0
- neatmem-0.1.0/neatmem/memory_search.py +183 -0
- neatmem-0.1.0/neatmem/memory_store.py +806 -0
- neatmem-0.1.0/neatmem/message_store.py +32 -0
- neatmem-0.1.0/neatmem/prompts/__init__.py +0 -0
- neatmem-0.1.0/neatmem/prompts/examples/dedup_en.example.txt +25 -0
- neatmem-0.1.0/neatmem/prompts/examples/dedup_zh.example.txt +25 -0
- neatmem-0.1.0/neatmem/prompts/examples/edit_en.example.txt +24 -0
- neatmem-0.1.0/neatmem/prompts/examples/extraction_en.example.txt +444 -0
- neatmem-0.1.0/neatmem/prompts/examples/rerank_en.example.txt +16 -0
- neatmem-0.1.0/neatmem/prompts/examples/rewrite_zh.example.txt +15 -0
- neatmem-0.1.0/neatmem/prompts/extraction.py +593 -0
- neatmem-0.1.0/neatmem/prompts/loader.py +84 -0
- neatmem-0.1.0/neatmem/rerank.py +214 -0
- neatmem-0.1.0/neatmem/signals/__init__.py +0 -0
- neatmem-0.1.0/neatmem/signals/bm25/__init__.py +19 -0
- neatmem-0.1.0/neatmem/signals/bm25/base.py +40 -0
- neatmem-0.1.0/neatmem/signals/bm25/factory.py +43 -0
- neatmem-0.1.0/neatmem/signals/bm25/noop.py +28 -0
- neatmem-0.1.0/neatmem/signals/bm25/qdrant_sparse.py +113 -0
- neatmem-0.1.0/neatmem/signals/bm25/scoring.py +43 -0
- neatmem-0.1.0/neatmem/signals/entity/__init__.py +15 -0
- neatmem-0.1.0/neatmem/signals/entity/base.py +23 -0
- neatmem-0.1.0/neatmem/signals/entity/boosting.py +178 -0
- neatmem-0.1.0/neatmem/signals/entity/extractor_llm.py +11 -0
- neatmem-0.1.0/neatmem/signals/entity/extractor_ner.py +15 -0
- neatmem-0.1.0/neatmem/signals/entity/factory.py +14 -0
- neatmem-0.1.0/neatmem/signals/graph/__init__.py +0 -0
- neatmem-0.1.0/neatmem/signals/graph/adapter.py +135 -0
- neatmem-0.1.0/neatmem/signals/graph/factory.py +77 -0
- neatmem-0.1.0/neatmem/signals/graph/prompts.py +138 -0
- neatmem-0.1.0/neatmem/signals/graph/tools.py +211 -0
- neatmem-0.1.0/neatmem/storage/__init__.py +0 -0
- neatmem-0.1.0/neatmem/storage/entity/__init__.py +10 -0
- neatmem-0.1.0/neatmem/storage/entity/base.py +39 -0
- neatmem-0.1.0/neatmem/storage/entity/factory.py +22 -0
- neatmem-0.1.0/neatmem/storage/entity/qdrant.py +244 -0
- neatmem-0.1.0/neatmem/storage/graph/__init__.py +0 -0
- neatmem-0.1.0/neatmem/storage/graph/kuzu_store.py +704 -0
- neatmem-0.1.0/neatmem/storage/history.py +361 -0
- neatmem-0.1.0/neatmem/storage/message/__init__.py +13 -0
- neatmem-0.1.0/neatmem/storage/message/base.py +143 -0
- neatmem-0.1.0/neatmem/storage/message/factory.py +55 -0
- neatmem-0.1.0/neatmem/storage/message/noop.py +88 -0
- neatmem-0.1.0/neatmem/storage/message/sqlite.py +418 -0
- neatmem-0.1.0/neatmem/storage/vector/__init__.py +7 -0
- neatmem-0.1.0/neatmem/storage/vector/base.py +65 -0
- neatmem-0.1.0/neatmem/storage/vector/factory.py +39 -0
- neatmem-0.1.0/neatmem/storage/vector/qdrant.py +493 -0
- neatmem-0.1.0/neatmem/utils/__init__.py +0 -0
- neatmem-0.1.0/neatmem/utils/llm_client.py +55 -0
- neatmem-0.1.0/neatmem/utils/spacy/__init__.py +10 -0
- neatmem-0.1.0/neatmem/utils/spacy/entity_extraction.py +367 -0
- neatmem-0.1.0/neatmem/utils/spacy/lemmatization.py +60 -0
- neatmem-0.1.0/neatmem/utils/spacy/spacy_models.py +101 -0
- neatmem-0.1.0/neatmem/utils/text_parsing.py +48 -0
- neatmem-0.1.0/neatmem.egg-info/PKG-INFO +513 -0
- neatmem-0.1.0/neatmem.egg-info/SOURCES.txt +88 -0
- neatmem-0.1.0/neatmem.egg-info/dependency_links.txt +1 -0
- neatmem-0.1.0/neatmem.egg-info/entry_points.txt +2 -0
- neatmem-0.1.0/neatmem.egg-info/requires.txt +16 -0
- neatmem-0.1.0/neatmem.egg-info/top_level.txt +1 -0
- neatmem-0.1.0/pyproject.toml +35 -0
- neatmem-0.1.0/setup.cfg +4 -0
- neatmem-0.1.0/tests/test_memory_store.py +163 -0
- neatmem-0.1.0/tests/test_prompt_examples.py +38 -0
- neatmem-0.1.0/tests/test_sqlite_message_store.py +528 -0
neatmem-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NeatMem contributors
|
|
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.
|
neatmem-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: neatmem
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A local mem0-compatible memory server for AI agents
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: fastapi==0.135.2
|
|
9
|
+
Requires-Dist: uvicorn[standard]==0.42.0
|
|
10
|
+
Requires-Dist: pydantic==2.12.3
|
|
11
|
+
Requires-Dist: numpy>=1.26.4
|
|
12
|
+
Requires-Dist: python-dotenv==1.2.2
|
|
13
|
+
Requires-Dist: langchain-community==0.4.1
|
|
14
|
+
Requires-Dist: qdrant-client==1.17.1
|
|
15
|
+
Requires-Dist: fastembed==0.8.0
|
|
16
|
+
Requires-Dist: openai==2.32.0
|
|
17
|
+
Requires-Dist: httpx==0.28.1
|
|
18
|
+
Provides-Extra: local-reranker
|
|
19
|
+
Requires-Dist: sentence-transformers==4.1.0; extra == "local-reranker"
|
|
20
|
+
Provides-Extra: nlp
|
|
21
|
+
Requires-Dist: spacy>=3.7; extra == "nlp"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# NeatMem
|
|
25
|
+
|
|
26
|
+
Lightweight local memory for agents, with cleaner deduplication, less memory pollution, and more relevant recall.
|
|
27
|
+
|
|
28
|
+
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.
|
|
29
|
+
|
|
30
|
+
> Status: v0.1-preview. NeatMem is usable for local development and mem0-compatible integrations, but APIs, packaging, and integrations may still change.
|
|
31
|
+
|
|
32
|
+
> **Benchmark**: 90.80% accuracy on LOCOMO, fully reproducible locally (3-run mean; MiniMax-M3 answer + judge, SiliconFlow bge-m3 embedding). See the [evaluation guide](https://github.com/kanhaoning/NeatMem/blob/main/neatmem/evaluation/README.md) for benchmark reproduction steps.
|
|
33
|
+
|
|
34
|
+
## Why NeatMem?
|
|
35
|
+
|
|
36
|
+
Agent memory is easy to start but hard to keep clean.
|
|
37
|
+
|
|
38
|
+
Common problems include:
|
|
39
|
+
|
|
40
|
+
- duplicate memories accumulating over time
|
|
41
|
+
- assistant suggestions being stored as user facts
|
|
42
|
+
- vague memories losing their original context
|
|
43
|
+
- semantically related memories not being merged
|
|
44
|
+
- irrelevant memories being recalled because of weak vector matches
|
|
45
|
+
- local agent tools needing a simple self-hosted memory backend
|
|
46
|
+
|
|
47
|
+
NeatMem focuses on one narrow goal:
|
|
48
|
+
|
|
49
|
+
> Local agent memory that stays clean, inspectable, and easy to tune.
|
|
50
|
+
|
|
51
|
+
It is not a full Memory OS and not an enterprise multi-tenant memory system.
|
|
52
|
+
|
|
53
|
+
## Features
|
|
54
|
+
|
|
55
|
+
- **LLM-assisted memory decisions**
|
|
56
|
+
- Classifies each new memory as `add`, `none`, or `update` (listwise, single LLM call).
|
|
57
|
+
- `DEDUP_MODE` controls behavior: `skip` (keep both), `replace` (overwrite), `rewrite` (LLM merge), `edit` (LLM patch).
|
|
58
|
+
|
|
59
|
+
- **Sequential memory updates**
|
|
60
|
+
- Processes new memories one by one so each merge sees the latest stored version.
|
|
61
|
+
- Helps avoid overwrite conflicts when several new facts update the same old memory.
|
|
62
|
+
|
|
63
|
+
- **Less memory pollution**
|
|
64
|
+
- Avoids saving AI suggestions, guesses, or tool noise as user facts.
|
|
65
|
+
- Tracks whether each memory came from the user, assistant, or tool output.
|
|
66
|
+
|
|
67
|
+
- **Memories with enough context**
|
|
68
|
+
- Adds missing context from the same message batch when needed.
|
|
69
|
+
- Example: “during development” can become “while developing a mem0-based memory module”.
|
|
70
|
+
|
|
71
|
+
- **More relevant recall**
|
|
72
|
+
- Multi-signal retrieval: dense vector search + BM25 sparse matching + entity boosting.
|
|
73
|
+
- LLM listwise rerank filters and reorders candidates before injection into agent context.
|
|
74
|
+
|
|
75
|
+
- **Lightweight local storage**
|
|
76
|
+
- Runs with local Qdrant (embedded or server mode) by default.
|
|
77
|
+
- Does not require Redis, a hosted memory service, or a full database stack.
|
|
78
|
+
|
|
79
|
+
- **Modular signal architecture**
|
|
80
|
+
- Message store, BM25, and entity modules are decoupled under `neatmem/storage/` and `neatmem/signals/`.
|
|
81
|
+
- Each signal can be toggled via environment variables (`ENABLE_BM25`, `ENABLE_ENTITY`, `ENABLE_GRAPH`).
|
|
82
|
+
|
|
83
|
+
- **Optional graph memory (opt-in)**
|
|
84
|
+
- Entity-relation storage via KuzuDB, toggled by `ENABLE_GRAPH`.
|
|
85
|
+
- Off by default; graph relations injection into answer prompt is experimental (`GRAPH_INJECT_RELATIONS`, known harmful on LOCOMO).
|
|
86
|
+
|
|
87
|
+
- **OpenClaw and mem0-style integration**
|
|
88
|
+
- Implements the core mem0-style memory endpoints needed for local agent workflows.
|
|
89
|
+
- Designed to support OpenClaw platform-mode memory integration.
|
|
90
|
+
|
|
91
|
+
## Compatibility
|
|
92
|
+
|
|
93
|
+
NeatMem implements a mem0-compatible API subset for local agent memory workflows:
|
|
94
|
+
|
|
95
|
+
- add memory
|
|
96
|
+
- search memory
|
|
97
|
+
- list memories
|
|
98
|
+
- update memory
|
|
99
|
+
- delete memory
|
|
100
|
+
- health check
|
|
101
|
+
|
|
102
|
+
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.
|
|
103
|
+
|
|
104
|
+
A remote client is provided for programmatic access:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from neatmem import MemoryClient
|
|
108
|
+
|
|
109
|
+
client = MemoryClient(host="http://localhost:8790")
|
|
110
|
+
client.add("My name is Alex", user_id="default_user")
|
|
111
|
+
results = client.search("What is my name?", filters={"user_id": "default_user"})
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
## Quick start
|
|
116
|
+
|
|
117
|
+
### 1. Install
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
pip install -r requirements.txt
|
|
121
|
+
pip install -e .
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The second command registers the `neatmem` CLI.
|
|
125
|
+
|
|
126
|
+
Optional:
|
|
127
|
+
- BM25 keyword search (enabled by default) needs spaCy:
|
|
128
|
+
```bash
|
|
129
|
+
pip install -e ".[nlp]" && python -m spacy download en_core_web_sm
|
|
130
|
+
```
|
|
131
|
+
- Local reranker model (alternative to LLM rerank):
|
|
132
|
+
```bash
|
|
133
|
+
pip install -e ".[local-reranker]"
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 2. Configure environment variables
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
cp .env.example .env
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Edit `.env` and configure your LLM and embedding provider.
|
|
143
|
+
|
|
144
|
+
Minimum configuration for OpenAI-compatible LLM providers:
|
|
145
|
+
|
|
146
|
+
```env
|
|
147
|
+
OPENAI_API_KEY=your-api-key
|
|
148
|
+
OPENAI_BASE_URL=https://your-openai-compatible-endpoint/v1
|
|
149
|
+
LLM_MODEL=qwen-max-latest
|
|
150
|
+
|
|
151
|
+
EMBEDDING_PROVIDER=siliconflow
|
|
152
|
+
SILICONFLOW_API_KEY=your-siliconflow-api-key
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### 3. Start the server
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
neatmem serve
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The server listens on:
|
|
162
|
+
|
|
163
|
+
```text
|
|
164
|
+
http://localhost:8790
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
To use a different port:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
neatmem serve --port 9000
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
View all options:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
neatmem serve --help
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
CLI flags override `.env` environment variables; see the Configuration table below for the full list.
|
|
180
|
+
|
|
181
|
+
Alternatively, start directly with Python:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
python -m neatmem.main
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Check health:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
curl http://localhost:8790/health
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Expected response:
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
{"status":"healthy","timestamp":"..."}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Configuration
|
|
200
|
+
|
|
201
|
+
NeatMem reads configuration from `.env`.
|
|
202
|
+
|
|
203
|
+
| Variable | Required | Default | Description |
|
|
204
|
+
|---|---:|---|---|
|
|
205
|
+
| `NEATMEM_HOST` | no | `0.0.0.0` | Server bind host |
|
|
206
|
+
| `NEATMEM_PORT` | no | `8790` | Server port |
|
|
207
|
+
| `NEATMEM_URL` | no | `http://localhost:8790` | Base URL used by `MemoryClient` |
|
|
208
|
+
| `NEATMEM_API_KEY` | no | - | API key sent as `Authorization: Token` header by `MemoryClient` (server ignores it) |
|
|
209
|
+
| `OPENAI_API_KEY` | yes | - | API key for OpenAI-compatible LLM provider |
|
|
210
|
+
| `OPENAI_BASE_URL` | yes | - | OpenAI-compatible API base URL |
|
|
211
|
+
| `LLM_MODEL` | no | `qwen-max-latest` | LLM model name |
|
|
212
|
+
| `EMBEDDING_PROVIDER` | no | `siliconflow` | `siliconflow` or `xinference` |
|
|
213
|
+
| `SILICONFLOW_API_KEY` | conditional | - | Required when `EMBEDDING_PROVIDER=siliconflow` |
|
|
214
|
+
| `EMBEDDING_MODEL` | no | `BAAI/bge-m3` | Embedding model name |
|
|
215
|
+
| `EMBEDDING_BASE_URL` | no | `https://api.siliconflow.cn/v1` | Embedding API base URL |
|
|
216
|
+
| `EMBEDDING_DIMS` | no | auto-detect | Embedding dimensions. When unset, auto-detected from a startup probe; set explicitly to enforce a dimension check at boot |
|
|
217
|
+
| `XINFERENCE_SERVER_URL` | conditional | `http://localhost:9997` | Required when using Xinference |
|
|
218
|
+
| `XINFERENCE_MODEL_UID` | conditional | `bge-m3` | Xinference embedding model UID |
|
|
219
|
+
| `QDRANT_PATH` | no | `qdrant_db` | Local Qdrant storage path (embedded mode) |
|
|
220
|
+
| `QDRANT_HOST` | no | - | Qdrant server host (sets server mode; overrides `QDRANT_PATH`) |
|
|
221
|
+
| `QDRANT_PORT` | no | `6333` | Qdrant server port |
|
|
222
|
+
| `DEDUP_MODE` | no | `skip` | Dedup behavior: `off`, `skip`, `replace`, `rewrite`, `edit` |
|
|
223
|
+
| `ENABLE_BM25` | no | `true` | Enable BM25 sparse search signal |
|
|
224
|
+
| `ENABLE_ENTITY` | no | `false` | Enable entity extraction and boosting |
|
|
225
|
+
| `ENABLE_GRAPH` | no | `false` | Enable graph memory (KuzuDB entity-relation storage). Graph hooks are no-op when disabled |
|
|
226
|
+
| `KUZU_DB_PATH` | conditional | - | KuzuDB database file path. Required when `ENABLE_GRAPH=true` |
|
|
227
|
+
| `GRAPH_THRESHOLD` | no | `0.7` | Entity match threshold for graph retrieval |
|
|
228
|
+
| `GRAPH_SEARCH_TOP_K` | no | `5` | Max relations returned per speaker from graph search |
|
|
229
|
+
| `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 |
|
|
230
|
+
| `GRAPH_EMBEDDING_MODEL` | no | `BAAI/bge-m3` | Embedding model for graph entities (defaults to main embedding model) |
|
|
231
|
+
| `GRAPH_EMBEDDING_DIMS` | no | `1024` | Embedding dimensions for graph entities |
|
|
232
|
+
| `GRAPH_EMBEDDING_BASE_URL` | no | `https://api.siliconflow.cn/v1` | Embedding API base URL for graph entities |
|
|
233
|
+
| `GRAPH_EMBEDDING_API_KEY` | no | - | Embedding API key for graph entities. Defaults to `SILICONFLOW_API_KEY` |
|
|
234
|
+
| `LLM_RERANK` | no | `true` | Enable LLM listwise rerank for recall |
|
|
235
|
+
| `RERANK_MODE` | no | `llm_listwise` | Rerank strategy |
|
|
236
|
+
| `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` |
|
|
237
|
+
| `RERANK_MAX_CONCURRENT` | no | `4` | Max concurrent LLM rerank calls (protects against API rate limits) |
|
|
238
|
+
| `MERGE_STRATEGY` | no | `off` | Deprecated; use `DEDUP_MODE` instead |
|
|
239
|
+
| `DEDUP_THINKING` | no | `false` | Enable LLM thinking for dedup |
|
|
240
|
+
| `EDIT_THINKING` | no | `false` | Enable LLM thinking for edit mode (DEDUP_MODE=edit) |
|
|
241
|
+
| `HISTORY_DB_PATH` | no | `{QDRANT_PATH}/history.db` | SQLite message history database path |
|
|
242
|
+
| `EXTRACT_LAST_K_MESSAGES` | no | `10` | Number of recent messages fed to extraction as context |
|
|
243
|
+
| `MESSAGE_STORE_BACKEND` | no | `sqlite` | Message store backend: `sqlite` or `none` |
|
|
244
|
+
| `ENTITY_EXTRACTOR_BACKEND` | no | `ner` | Entity extractor: `ner` or `llm` |
|
|
245
|
+
| `ENTITY_STORE_BACKEND` | no | `qdrant` | Entity store backend |
|
|
246
|
+
| `RERANKER_MODEL_PATH` | no | - | Optional local Sentence-Transformers reranker |
|
|
247
|
+
| `RERANKER_DEVICE` | no | `cpu` | Reranker device |
|
|
248
|
+
| `RERANKER_BATCH_SIZE` | no | `32` | Reranker batch size |
|
|
249
|
+
| `RERANKER_TOP_K` | no | `5` | Reranker top-k |
|
|
250
|
+
| `HF_ENDPOINT` | no | `https://hf-mirror.com` | HuggingFace mirror endpoint |
|
|
251
|
+
|
|
252
|
+
## Custom prompts
|
|
253
|
+
|
|
254
|
+
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.
|
|
255
|
+
|
|
256
|
+
| Prompt | Env var / CLI flag | Built-in ids | Used when |
|
|
257
|
+
|---|---|---|---|
|
|
258
|
+
| Fact extraction | `EXTRACTION_PROMPT` / `--extraction-prompt` | - | always (write path) |
|
|
259
|
+
| Dedup decision | `DEDUP_PROMPT` / `--dedup-prompt` | `zh` (default), `en` | `DEDUP_MODE=skip/replace/rewrite/edit` |
|
|
260
|
+
| Merge rewrite | `REWRITE_PROMPT` / `--rewrite-prompt` | - | `DEDUP_MODE=rewrite` |
|
|
261
|
+
| Patch edit | `EDIT_PROMPT` / `--edit-prompt` | - | `DEDUP_MODE=edit` |
|
|
262
|
+
| Rerank | `RERANK_PROMPT` / `--rerank-prompt` | - | LLM listwise rerank |
|
|
263
|
+
|
|
264
|
+
Switch to the English dedup prompt (validated on LOCOMO, 2026-07-24):
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
neatmem serve --dedup-prompt en
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Use your own prompt:
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
# 1. Export the example templates (they are the exact built-in defaults)
|
|
274
|
+
# From a git clone:
|
|
275
|
+
mkdir -p my_prompts && cp neatmem/prompts/examples/*.txt my_prompts/
|
|
276
|
+
# From a pip install:
|
|
277
|
+
python - <<'EOF'
|
|
278
|
+
from importlib.resources import files
|
|
279
|
+
import shutil, os
|
|
280
|
+
os.makedirs("my_prompts", exist_ok=True)
|
|
281
|
+
for f in files("neatmem.prompts").joinpath("examples").iterdir():
|
|
282
|
+
shutil.copy(f, "my_prompts/")
|
|
283
|
+
EOF
|
|
284
|
+
|
|
285
|
+
# 2. Edit the one you want (keep every {placeholder} intact,
|
|
286
|
+
# including the {{ }} escaping in JSON examples)
|
|
287
|
+
|
|
288
|
+
# 3. Point the server at it
|
|
289
|
+
neatmem serve --dedup-prompt /absolute/path/to/my_prompts/dedup_zh.example.txt
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Notes:
|
|
293
|
+
|
|
294
|
+
- Prompts are loaded once at startup; restart the server after editing a file.
|
|
295
|
+
- 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.
|
|
296
|
+
- Prefer absolute paths; relative paths resolve against the server's working directory.
|
|
297
|
+
|
|
298
|
+
## OpenClaw integration
|
|
299
|
+
|
|
300
|
+
NeatMem includes an OpenClaw plugin under `openclaw/`. Build it and install it as a linked local plugin during development:
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
cd /path/to/NeatMem/openclaw
|
|
304
|
+
npm install
|
|
305
|
+
npm run build
|
|
306
|
+
|
|
307
|
+
cd /path/to/NeatMem
|
|
308
|
+
openclaw plugins install ./openclaw --link
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
After changing plugin TypeScript source, rebuild before reinstalling or restarting OpenClaw.
|
|
312
|
+
|
|
313
|
+
The plugin id is `openclaw-neatmem`. It talks to NeatMem through the local mem0-compatible HTTP API.
|
|
314
|
+
|
|
315
|
+
Example OpenClaw configuration:
|
|
316
|
+
|
|
317
|
+
```json
|
|
318
|
+
{
|
|
319
|
+
"plugins": {
|
|
320
|
+
"slots": {
|
|
321
|
+
"memory": "openclaw-neatmem"
|
|
322
|
+
},
|
|
323
|
+
"entries": {
|
|
324
|
+
"openclaw-neatmem": {
|
|
325
|
+
"enabled": true,
|
|
326
|
+
"config": {
|
|
327
|
+
"mode": "platform",
|
|
328
|
+
"apiKey": "neatmem-local",
|
|
329
|
+
"userId": "default_user",
|
|
330
|
+
"baseUrl": "http://localhost:8790"
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Then check:
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
openclaw mem0 status
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
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`.
|
|
345
|
+
|
|
346
|
+
## Hermes integration
|
|
347
|
+
|
|
348
|
+
NeatMem includes a Hermes Agent memory provider under `hermes/`. With the NeatMem server running at `http://localhost:8790`:
|
|
349
|
+
|
|
350
|
+
```bash
|
|
351
|
+
hermes plugins install kanhaoning/NeatMem/hermes --enable
|
|
352
|
+
hermes config set memory.provider neatmem
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
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`:
|
|
356
|
+
|
|
357
|
+
```json
|
|
358
|
+
{
|
|
359
|
+
"base_url": "http://localhost:8790",
|
|
360
|
+
"user_id": "myname",
|
|
361
|
+
"rerank": true
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Verify: tell Hermes "remember that I prefer dark themes", then ask about it in a new session. See [hermes/README.md](https://github.com/kanhaoning/NeatMem/blob/main/hermes/README.md) for the full configuration reference and troubleshooting.
|
|
366
|
+
|
|
367
|
+
## API examples
|
|
368
|
+
|
|
369
|
+
### Health check
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
curl http://localhost:8790/health
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Add memory
|
|
376
|
+
|
|
377
|
+
```bash
|
|
378
|
+
curl -X POST http://localhost:8790/v1/memories/ \
|
|
379
|
+
-H "Content-Type: application/json" \
|
|
380
|
+
-d '{
|
|
381
|
+
"messages": [
|
|
382
|
+
{"role": "user", "content": "My name is Alex and I work on agent memory systems."},
|
|
383
|
+
{"role": "assistant", "content": "Nice to meet you, Alex."}
|
|
384
|
+
],
|
|
385
|
+
"user_id": "default_user",
|
|
386
|
+
"infer": true
|
|
387
|
+
}'
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Search memory
|
|
391
|
+
|
|
392
|
+
```bash
|
|
393
|
+
curl -X POST http://localhost:8790/v2/memories/search/ \
|
|
394
|
+
-H "Content-Type: application/json" \
|
|
395
|
+
-d '{
|
|
396
|
+
"query": "What is Alex working on?",
|
|
397
|
+
"filters": {"user_id": "default_user"},
|
|
398
|
+
"top_k": 10,
|
|
399
|
+
"threshold": 0.1
|
|
400
|
+
}'
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### List memories
|
|
404
|
+
|
|
405
|
+
```bash
|
|
406
|
+
curl -X POST http://localhost:8790/v2/memories/ \
|
|
407
|
+
-H "Content-Type: application/json" \
|
|
408
|
+
-d '{
|
|
409
|
+
"filters": {"user_id": "default_user"},
|
|
410
|
+
"page": 1,
|
|
411
|
+
"page_size": 100
|
|
412
|
+
}'
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### Get memory
|
|
416
|
+
|
|
417
|
+
```bash
|
|
418
|
+
curl http://localhost:8790/v1/memories/{memory_id}/
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### Update memory
|
|
422
|
+
|
|
423
|
+
```bash
|
|
424
|
+
curl -X PUT http://localhost:8790/v1/memories/{memory_id}/ \
|
|
425
|
+
-H "Content-Type: application/json" \
|
|
426
|
+
-d '{
|
|
427
|
+
"text": "Alex works on local-first agent memory systems.",
|
|
428
|
+
"metadata": {"source": "manual_update"}
|
|
429
|
+
}'
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Delete memory
|
|
433
|
+
|
|
434
|
+
```bash
|
|
435
|
+
curl -X DELETE http://localhost:8790/v1/memories/{memory_id}/
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
## How it works
|
|
439
|
+
|
|
440
|
+
### Add flow
|
|
441
|
+
|
|
442
|
+
```text
|
|
443
|
+
messages
|
|
444
|
+
↓
|
|
445
|
+
retrieve last-k messages as extraction context
|
|
446
|
+
↓
|
|
447
|
+
LLM memory extraction (with last-k context)
|
|
448
|
+
↓
|
|
449
|
+
context completion and source tracking
|
|
450
|
+
↓
|
|
451
|
+
sequential LLM-assisted memory decisions
|
|
452
|
+
├─ add -> store as new memory
|
|
453
|
+
├─ none -> skip (duplicate)
|
|
454
|
+
└─ update -> merge per DEDUP_MODE (skip/replace/rewrite/edit)
|
|
455
|
+
↓
|
|
456
|
+
write to vector store + BM25 index + entity store
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### Search flow
|
|
460
|
+
|
|
461
|
+
```text
|
|
462
|
+
query
|
|
463
|
+
↓
|
|
464
|
+
dense vector search + BM25 sparse search + entity boosting
|
|
465
|
+
↓
|
|
466
|
+
LLM listwise rerank
|
|
467
|
+
↓
|
|
468
|
+
threshold filtering
|
|
469
|
+
↓
|
|
470
|
+
results
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
## Development probes
|
|
474
|
+
|
|
475
|
+
Memory quality iteration is done through `probe/`, which contains OpenClaw end-to-end probes and extraction simulation scripts. It is not a benchmark suite.
|
|
476
|
+
|
|
477
|
+
## Design notes
|
|
478
|
+
|
|
479
|
+
NeatMem is designed around a few constraints:
|
|
480
|
+
|
|
481
|
+
- keep the plugin layer thin
|
|
482
|
+
- keep the backend self-hosted and debuggable
|
|
483
|
+
- do not require Redis or a background scheduler
|
|
484
|
+
- prefer memory quality over feature breadth
|
|
485
|
+
- preserve compatibility with mem0-style APIs where possible
|
|
486
|
+
|
|
487
|
+
## Limitations
|
|
488
|
+
|
|
489
|
+
NeatMem is in active development. Current limitations:
|
|
490
|
+
|
|
491
|
+
- APIs and packaging may still change.
|
|
492
|
+
- No dashboard or GUI.
|
|
493
|
+
- No multi-tenant permission system.
|
|
494
|
+
- OpenClaw is the primary tested integration path.
|
|
495
|
+
- Prompt behavior is still being iterated and may vary across models.
|
|
496
|
+
- BM25 lemmatization is basic; bilingual (Chinese/English) tokenization needs improvement.
|
|
497
|
+
|
|
498
|
+
## Roadmap
|
|
499
|
+
|
|
500
|
+
- Bilingual multi-signal support (improved Chinese/English BM25 and entity extraction)
|
|
501
|
+
- PyPI package publication
|
|
502
|
+
- Memory inspection and export/import tools
|
|
503
|
+
- Richer recall diagnostics
|
|
504
|
+
|
|
505
|
+
## License
|
|
506
|
+
|
|
507
|
+
MIT License.
|
|
508
|
+
|
|
509
|
+
## Acknowledgements
|
|
510
|
+
|
|
511
|
+
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.
|
|
512
|
+
|
|
513
|
+
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.
|