graph-mind-memory 0.5.2__tar.gz → 0.5.4__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.
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4}/PKG-INFO +16 -18
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4}/README.md +15 -17
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4}/pyproject.toml +3 -2
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4/src}/brain_log.py +23 -3
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4/src}/graph_mind_mcp_server.py +25 -12
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4/src}/graph_mind_memory.egg-info/PKG-INFO +16 -18
- graph_mind_memory-0.5.4/src/graph_mind_memory.egg-info/SOURCES.txt +63 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4/src}/install.py +21 -15
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4/src}/local_brain.py +2 -0
- graph_mind_memory-0.5.4/tests/test_associative_memory.py +120 -0
- graph_mind_memory-0.5.4/tests/test_automatic_capture.py +101 -0
- graph_mind_memory-0.5.4/tests/test_capture_health.py +36 -0
- graph_mind_memory-0.5.4/tests/test_coding_memory.py +75 -0
- graph_mind_memory-0.5.4/tests/test_cold_start.py +77 -0
- graph_mind_memory-0.5.4/tests/test_concurrent_use.py +40 -0
- graph_mind_memory-0.5.4/tests/test_conversation_memory.py +191 -0
- graph_mind_memory-0.5.4/tests/test_executable_memory.py +105 -0
- graph_mind_memory-0.5.4/tests/test_executable_plan_v2_diagnostic.py +52 -0
- graph_mind_memory-0.5.4/tests/test_forget.py +125 -0
- graph_mind_memory-0.5.4/tests/test_forget_conversation.py +100 -0
- graph_mind_memory-0.5.4/tests/test_graph_mind_mcp_server.py +118 -0
- graph_mind_memory-0.5.4/tests/test_ingestion_gate.py +67 -0
- graph_mind_memory-0.5.4/tests/test_ingestion_synonyms.py +70 -0
- graph_mind_memory-0.5.4/tests/test_install.py +160 -0
- graph_mind_memory-0.5.4/tests/test_llm_concept_planner.py +87 -0
- graph_mind_memory-0.5.4/tests/test_local_brain.py +60 -0
- graph_mind_memory-0.5.4/tests/test_longmemeval_adapter.py +49 -0
- graph_mind_memory-0.5.4/tests/test_longmemeval_ingestion_calibration.py +45 -0
- graph_mind_memory-0.5.4/tests/test_longmemeval_ingestion_preflight.py +22 -0
- graph_mind_memory-0.5.4/tests/test_longmemeval_ingestion_v7.py +55 -0
- graph_mind_memory-0.5.4/tests/test_memory_algebra.py +45 -0
- graph_mind_memory-0.5.4/tests/test_precondition_verifier.py +39 -0
- graph_mind_memory-0.5.4/tests/test_recent_recall.py +115 -0
- graph_mind_memory-0.5.4/tests/test_resume_retries.py +26 -0
- graph_mind_memory-0.5.4/tests/test_secret_masking.py +76 -0
- graph_mind_memory-0.5.4/tests/test_semantic_llm_extractor.py +102 -0
- graph_mind_memory-0.5.4/tests/test_semantic_memory.py +69 -0
- graph_mind_memory-0.5.4/tests/test_semantic_recall.py +89 -0
- graph_mind_memory-0.5.4/tests/test_shared_brain.py +269 -0
- graph_mind_memory-0.5.4/tests/test_temporal_state.py +151 -0
- graph_mind_memory-0.5.4/tests/test_universal_personal_memory.py +73 -0
- graph_mind_memory-0.5.2/graph_mind_memory.egg-info/SOURCES.txt +0 -31
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4}/LICENSE +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4}/setup.cfg +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4/src}/associative_memory.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4/src}/automatic_capture.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4/src}/automatic_capture_cli.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4/src}/clock.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4/src}/coding_memory.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4/src}/conversation_memory.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4/src}/development_memory.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4/src}/development_paths.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4/src}/embedding_warmup.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4/src}/entity_timeline.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4/src}/forget.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4/src}/graph_mind_memory.egg-info/dependency_links.txt +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4/src}/graph_mind_memory.egg-info/entry_points.txt +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4/src}/graph_mind_memory.egg-info/requires.txt +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4/src}/graph_mind_memory.egg-info/top_level.txt +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4/src}/local_embedder.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4/src}/semantic_llm_extractor.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4/src}/semantic_memory.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4/src}/semantic_recall.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4/src}/temporal_resolver.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4/src}/universal_personal_memory.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.4/src}/vector_cache.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: graph-mind-memory
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.4
|
|
4
4
|
Summary: Local-first memory for AI coding assistants: verbatim, on your own PC, no LLM calls at write time.
|
|
5
5
|
Author-email: Yohan Ko <goyohan0611@gmail.com>
|
|
6
6
|
License-Expression: AGPL-3.0-only
|
|
@@ -78,8 +78,8 @@ calls Graph-MIND over MCP and gets back a few thousand tokens of the conversatio
|
|
|
78
78
|
## Benchmarks
|
|
79
79
|
|
|
80
80
|
All numbers below come from files in this repository: the pre-registrations, each question's
|
|
81
|
-
answer, and the judge's verdict on it, under [`runs/`](runs). The method is in
|
|
82
|
-
[REPORT.md](REPORT.md), including the failures and the corrections.
|
|
81
|
+
answer, and the judge's verdict on it, under [`research/runs/`](research/runs). The method is in
|
|
82
|
+
[REPORT.md](research/REPORT.md), including the failures and the corrections.
|
|
83
83
|
|
|
84
84
|
**LongMemEval_S: answer accuracy, 500 questions.** The answer model is gpt-5-mini; the judge is the
|
|
85
85
|
official gpt-4o-2024-08-06.
|
|
@@ -139,7 +139,7 @@ or from source:
|
|
|
139
139
|
```bash
|
|
140
140
|
git clone https://github.com/goyohan0611-png/graph-mind.git
|
|
141
141
|
cd graph-mind
|
|
142
|
-
python install.py
|
|
142
|
+
python src/install.py
|
|
143
143
|
```
|
|
144
144
|
|
|
145
145
|
The installer:
|
|
@@ -233,7 +233,7 @@ On the PC that holds the memory, tell its AI:
|
|
|
233
233
|
It replies with a connection code (`gm1.…`). On each other PC:
|
|
234
234
|
|
|
235
235
|
```bash
|
|
236
|
-
graph-mind-install --join gm1.… # or: python install.py --join gm1.…
|
|
236
|
+
graph-mind-install --join gm1.… # or: python src/install.py --join gm1.…
|
|
237
237
|
```
|
|
238
238
|
|
|
239
239
|
The memory then lives in a Postgres server on the first PC, which Graph-MIND sets up itself. Other
|
|
@@ -254,16 +254,17 @@ each PC.
|
|
|
254
254
|
## Reproducing the benchmarks
|
|
255
255
|
|
|
256
256
|
1. Download `longmemeval_s_cleaned.json` from [LongMemEval](https://github.com/xiaowu0162/LongMemEval)
|
|
257
|
-
into `external/longmemeval/`.
|
|
257
|
+
into `research/external/longmemeval/`.
|
|
258
258
|
2. Set `OPENAI_API_KEY` for the answer model and the judge.
|
|
259
|
-
3.
|
|
259
|
+
3. `pip install -e .`, then from `research/` run the commands in
|
|
260
|
+
[REPORT.md §10](research/REPORT.md#10-reproducing).
|
|
260
261
|
|
|
261
262
|
A full 500-question run costs about US$3.
|
|
262
263
|
|
|
263
264
|
## Tests
|
|
264
265
|
|
|
265
266
|
```bash
|
|
266
|
-
python -m unittest discover -p "test_*.py"
|
|
267
|
+
python -m unittest discover -s tests -p "test_*.py"
|
|
267
268
|
```
|
|
268
269
|
|
|
269
270
|
---
|
|
@@ -271,7 +272,7 @@ python -m unittest discover -p "test_*.py"
|
|
|
271
272
|
## Known limits
|
|
272
273
|
|
|
273
274
|
- **For the first few seconds after an AI app starts, answers use word search only** while the
|
|
274
|
-
embedding model loads in the background (6-
|
|
275
|
+
embedding model loads in the background (6-30 s on a slow, synced disk, depending on load; it was 50-70 s before
|
|
275
276
|
0.4). The first answer never waits for it; later answers take well under a second.
|
|
276
277
|
- **Daily use on Windows only so far.** macOS and Linux pass the tests in CI but have not seen real
|
|
277
278
|
use.
|
|
@@ -279,20 +280,17 @@ python -m unittest discover -p "test_*.py"
|
|
|
279
280
|
can stop capture until Graph-MIND is updated.
|
|
280
281
|
- **Multi-session questions are the weakest type at 80%.** These are questions that count or
|
|
281
282
|
combine facts across many conversations.
|
|
282
|
-
- **The repository still holds the research-phase experiments** next to the product (see
|
|
283
|
-
[Repository layout](#repository-layout)); the installed package carries only the 22 product
|
|
284
|
-
modules.
|
|
285
283
|
|
|
286
|
-
The full list is in [REPORT.md §9](REPORT.md#9-known-limits).
|
|
284
|
+
The full list is in [REPORT.md §9](research/REPORT.md#9-known-limits).
|
|
287
285
|
|
|
288
286
|
## Repository layout
|
|
289
287
|
|
|
290
|
-
| |
|
|
288
|
+
| folder | what is in it |
|
|
291
289
|
|---|---|
|
|
292
|
-
|
|
|
293
|
-
|
|
|
294
|
-
|
|
|
295
|
-
|
|
|
290
|
+
| [`src/`](src) | the product, exactly what `pip install graph-mind-memory` installs: `graph_mind_mcp_server.py` (MCP server), `automatic_capture*.py` (capture service), `install.py`, `brain_log.py` (sharing across PCs), `forget.py` (deleting), the stores, and search (`semantic_recall.py`, `local_embedder.py`) |
|
|
291
|
+
| [`tests/`](tests) | unit tests, run in CI on Windows, Linux and macOS |
|
|
292
|
+
| [`research/`](research) | the benchmark and research-phase scripts, [REPORT.md](research/REPORT.md), and every run's results under `runs/` |
|
|
293
|
+
| [`demo/`](demo) | the README's GIF and the script that records it |
|
|
296
294
|
|
|
297
295
|
## Security
|
|
298
296
|
|
|
@@ -50,8 +50,8 @@ calls Graph-MIND over MCP and gets back a few thousand tokens of the conversatio
|
|
|
50
50
|
## Benchmarks
|
|
51
51
|
|
|
52
52
|
All numbers below come from files in this repository: the pre-registrations, each question's
|
|
53
|
-
answer, and the judge's verdict on it, under [`runs/`](runs). The method is in
|
|
54
|
-
[REPORT.md](REPORT.md), including the failures and the corrections.
|
|
53
|
+
answer, and the judge's verdict on it, under [`research/runs/`](research/runs). The method is in
|
|
54
|
+
[REPORT.md](research/REPORT.md), including the failures and the corrections.
|
|
55
55
|
|
|
56
56
|
**LongMemEval_S: answer accuracy, 500 questions.** The answer model is gpt-5-mini; the judge is the
|
|
57
57
|
official gpt-4o-2024-08-06.
|
|
@@ -111,7 +111,7 @@ or from source:
|
|
|
111
111
|
```bash
|
|
112
112
|
git clone https://github.com/goyohan0611-png/graph-mind.git
|
|
113
113
|
cd graph-mind
|
|
114
|
-
python install.py
|
|
114
|
+
python src/install.py
|
|
115
115
|
```
|
|
116
116
|
|
|
117
117
|
The installer:
|
|
@@ -205,7 +205,7 @@ On the PC that holds the memory, tell its AI:
|
|
|
205
205
|
It replies with a connection code (`gm1.…`). On each other PC:
|
|
206
206
|
|
|
207
207
|
```bash
|
|
208
|
-
graph-mind-install --join gm1.… # or: python install.py --join gm1.…
|
|
208
|
+
graph-mind-install --join gm1.… # or: python src/install.py --join gm1.…
|
|
209
209
|
```
|
|
210
210
|
|
|
211
211
|
The memory then lives in a Postgres server on the first PC, which Graph-MIND sets up itself. Other
|
|
@@ -226,16 +226,17 @@ each PC.
|
|
|
226
226
|
## Reproducing the benchmarks
|
|
227
227
|
|
|
228
228
|
1. Download `longmemeval_s_cleaned.json` from [LongMemEval](https://github.com/xiaowu0162/LongMemEval)
|
|
229
|
-
into `external/longmemeval/`.
|
|
229
|
+
into `research/external/longmemeval/`.
|
|
230
230
|
2. Set `OPENAI_API_KEY` for the answer model and the judge.
|
|
231
|
-
3.
|
|
231
|
+
3. `pip install -e .`, then from `research/` run the commands in
|
|
232
|
+
[REPORT.md §10](research/REPORT.md#10-reproducing).
|
|
232
233
|
|
|
233
234
|
A full 500-question run costs about US$3.
|
|
234
235
|
|
|
235
236
|
## Tests
|
|
236
237
|
|
|
237
238
|
```bash
|
|
238
|
-
python -m unittest discover -p "test_*.py"
|
|
239
|
+
python -m unittest discover -s tests -p "test_*.py"
|
|
239
240
|
```
|
|
240
241
|
|
|
241
242
|
---
|
|
@@ -243,7 +244,7 @@ python -m unittest discover -p "test_*.py"
|
|
|
243
244
|
## Known limits
|
|
244
245
|
|
|
245
246
|
- **For the first few seconds after an AI app starts, answers use word search only** while the
|
|
246
|
-
embedding model loads in the background (6-
|
|
247
|
+
embedding model loads in the background (6-30 s on a slow, synced disk, depending on load; it was 50-70 s before
|
|
247
248
|
0.4). The first answer never waits for it; later answers take well under a second.
|
|
248
249
|
- **Daily use on Windows only so far.** macOS and Linux pass the tests in CI but have not seen real
|
|
249
250
|
use.
|
|
@@ -251,20 +252,17 @@ python -m unittest discover -p "test_*.py"
|
|
|
251
252
|
can stop capture until Graph-MIND is updated.
|
|
252
253
|
- **Multi-session questions are the weakest type at 80%.** These are questions that count or
|
|
253
254
|
combine facts across many conversations.
|
|
254
|
-
- **The repository still holds the research-phase experiments** next to the product (see
|
|
255
|
-
[Repository layout](#repository-layout)); the installed package carries only the 22 product
|
|
256
|
-
modules.
|
|
257
255
|
|
|
258
|
-
The full list is in [REPORT.md §9](REPORT.md#9-known-limits).
|
|
256
|
+
The full list is in [REPORT.md §9](research/REPORT.md#9-known-limits).
|
|
259
257
|
|
|
260
258
|
## Repository layout
|
|
261
259
|
|
|
262
|
-
| |
|
|
260
|
+
| folder | what is in it |
|
|
263
261
|
|---|---|
|
|
264
|
-
|
|
|
265
|
-
|
|
|
266
|
-
|
|
|
267
|
-
|
|
|
262
|
+
| [`src/`](src) | the product, exactly what `pip install graph-mind-memory` installs: `graph_mind_mcp_server.py` (MCP server), `automatic_capture*.py` (capture service), `install.py`, `brain_log.py` (sharing across PCs), `forget.py` (deleting), the stores, and search (`semantic_recall.py`, `local_embedder.py`) |
|
|
263
|
+
| [`tests/`](tests) | unit tests, run in CI on Windows, Linux and macOS |
|
|
264
|
+
| [`research/`](research) | the benchmark and research-phase scripts, [REPORT.md](research/REPORT.md), and every run's results under `runs/` |
|
|
265
|
+
| [`demo/`](demo) | the README's GIF and the script that records it |
|
|
268
266
|
|
|
269
267
|
## Security
|
|
270
268
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "graph-mind-memory"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.4"
|
|
8
8
|
description = "Local-first memory for AI coding assistants: verbatim, on your own PC, no LLM calls at write time."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -45,7 +45,8 @@ graph-mind-capture = "automatic_capture_cli:main"
|
|
|
45
45
|
graph-mind-forget = "forget:main"
|
|
46
46
|
|
|
47
47
|
[tool.setuptools]
|
|
48
|
-
# ponytail: flat modules
|
|
48
|
+
# ponytail: flat modules under src/; a graph_mind/ package is the upgrade path.
|
|
49
|
+
package-dir = { "" = "src" }
|
|
49
50
|
py-modules = [
|
|
50
51
|
"associative_memory",
|
|
51
52
|
"automatic_capture",
|
|
@@ -461,6 +461,27 @@ def _pg(url):
|
|
|
461
461
|
return connection
|
|
462
462
|
|
|
463
463
|
|
|
464
|
+
_READERS: dict = {}
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _read_rows(url, query, parameters):
|
|
468
|
+
"""A read on one connection kept open per hub: the sync before every answer paid a fresh
|
|
469
|
+
connection (password handshake included) each time, 0.2 s of a 0.9 s answer. A connection
|
|
470
|
+
the hub dropped (it restarted) is replaced once."""
|
|
471
|
+
import psycopg
|
|
472
|
+
for attempt in (1, 2):
|
|
473
|
+
connection = _READERS.get(url)
|
|
474
|
+
if connection is None or connection.closed:
|
|
475
|
+
connection = _READERS[url] = _pg(url)
|
|
476
|
+
connection.autocommit = True
|
|
477
|
+
try:
|
|
478
|
+
return connection.execute(query, parameters).fetchall()
|
|
479
|
+
except psycopg.OperationalError:
|
|
480
|
+
_READERS.pop(url, None)
|
|
481
|
+
if attempt == 2:
|
|
482
|
+
raise
|
|
483
|
+
|
|
484
|
+
|
|
464
485
|
def _outbox() -> Path:
|
|
465
486
|
from development_paths import graph_mind_home
|
|
466
487
|
return graph_mind_home() / "outbox.jsonl"
|
|
@@ -514,9 +535,8 @@ def _new_lines(db: sqlite3.Connection, shared) -> list[str]:
|
|
|
514
535
|
source = "postgres"
|
|
515
536
|
row = db.execute("SELECT offset FROM brain_log_cursor WHERE source=?",
|
|
516
537
|
(source,)).fetchone()
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
(row[0] if row else 0,)).fetchall()
|
|
538
|
+
rows = _read_rows(shared, "SELECT seq, line FROM brain_log WHERE seq > %s ORDER BY seq",
|
|
539
|
+
(row[0] if row else 0,))
|
|
520
540
|
if rows:
|
|
521
541
|
with db:
|
|
522
542
|
db.execute("INSERT OR REPLACE INTO brain_log_cursor VALUES(?,?)",
|
|
@@ -26,12 +26,13 @@ if hasattr(sys.stderr, "reconfigure"):
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
def _version() -> str:
|
|
29
|
-
"""The package version: pyproject.toml
|
|
29
|
+
"""The package version: the checkout's pyproject.toml (one folder up from src/), else the
|
|
30
30
|
installed distribution's. A hand-kept constant here had drifted to 0.5.0 at release 0.3.1."""
|
|
31
31
|
import re
|
|
32
|
-
project = Path(__file__).
|
|
33
|
-
if project.exists()
|
|
34
|
-
|
|
32
|
+
project = Path(__file__).resolve().parent.parent / "pyproject.toml"
|
|
33
|
+
text = project.read_text(encoding="utf-8") if project.exists() else ""
|
|
34
|
+
if 'name = "graph-mind-memory"' in text:
|
|
35
|
+
found = re.search(r'^version = "([^"]+)"', text, re.M)
|
|
35
36
|
if found:
|
|
36
37
|
return found.group(1)
|
|
37
38
|
try:
|
|
@@ -358,6 +359,21 @@ def capture_warnings() -> list[str]:
|
|
|
358
359
|
return []
|
|
359
360
|
|
|
360
361
|
|
|
362
|
+
PROACTIVE_NOTE = (
|
|
363
|
+
"Remember proactively. Whenever the user mentions something about their life, work, "
|
|
364
|
+
"plans, preferences, people, places, meals, purchases, health, decisions or events, "
|
|
365
|
+
"call brain_remember right away with their own words, without asking first and "
|
|
366
|
+
"without waiting to be told to remember it. Saving too much costs nothing; forgetting "
|
|
367
|
+
"is what the user notices. Do it silently: never tell the user that you saved, "
|
|
368
|
+
"stored or noted something, and do not add lines like 'I saved that to memory'. "
|
|
369
|
+
"Mention memory only when the user asks what you remember or whether it was saved. ")
|
|
370
|
+
# set by the installer for apps whose transcripts the capture service already saves
|
|
371
|
+
CAPTURED_NOTE = (
|
|
372
|
+
"Every turn of this conversation is saved automatically. Do not call brain_remember for "
|
|
373
|
+
"ordinary chat; call it only when the user explicitly asks you to remember something or "
|
|
374
|
+
"to record a decision. ")
|
|
375
|
+
|
|
376
|
+
|
|
361
377
|
def build_server(db_path=None):
|
|
362
378
|
database = Path(db_path).expanduser() if db_path is not None else configured_db_path()
|
|
363
379
|
server = MCPServer(
|
|
@@ -365,13 +381,7 @@ def build_server(db_path=None):
|
|
|
365
381
|
title="Graph-MIND Universal Personal Memory",
|
|
366
382
|
description="User-owned local brain shared across models, sessions and applications.",
|
|
367
383
|
instructions=(
|
|
368
|
-
|
|
369
|
-
"plans, preferences, people, places, meals, purchases, health, decisions or events, "
|
|
370
|
-
"call brain_remember right away with their own words, without asking first and "
|
|
371
|
-
"without waiting to be told to remember it. Saving too much costs nothing; forgetting "
|
|
372
|
-
"is what the user notices. Do it silently: never tell the user that you saved, "
|
|
373
|
-
"stored or noted something, and do not add lines like 'I saved that to memory'. "
|
|
374
|
-
"Mention memory only when the user asks what you remember or whether it was saved. "
|
|
384
|
+
(CAPTURED_NOTE if os.environ.get("GRAPH_MIND_CAPTURED") else PROACTIVE_NOTE) +
|
|
375
385
|
"Call brain_context first when a request may depend on the user's prior conversations, "
|
|
376
386
|
"facts, preferences, decisions, projects or work, including 'where did we leave off?'. "
|
|
377
387
|
"It skips unrelated requests and returns a bounded evidence packet instead of "
|
|
@@ -563,12 +573,15 @@ def build_server(db_path=None):
|
|
|
563
573
|
return {"status": "SKIPPED", "route": route,
|
|
564
574
|
"scope_completeness": "NOT_APPLICABLE", "char_budget": max_chars,
|
|
565
575
|
"used_chars": 0, "estimated_tokens": 0, "selected_items": 0,
|
|
566
|
-
"omitted_items": 0, "
|
|
576
|
+
"omitted_items": 0, "ids": [], "context": ""}
|
|
567
577
|
recalled = gather(database, query, scopes=scopes, as_of=as_of or _now(),
|
|
568
578
|
knowledge_cutoff=knowledge_cutoff, limit=limit, recent=recent,
|
|
569
579
|
since=since, about_assistant=about_assistant, around=around)
|
|
570
580
|
packet = build_context_pack(recalled, max_chars=max_chars,
|
|
571
581
|
order="time" if recent else "relevance")
|
|
582
|
+
# `context` already carries each item's kind, date, title, text and source; the item
|
|
583
|
+
# list repeated all of it and was 2/3 of what the model had to read (43K -> 16K chars)
|
|
584
|
+
packet["ids"] = [item["id"] for item in packet.pop("items")]
|
|
572
585
|
packet["route"] = route
|
|
573
586
|
warnings = capture_warnings()
|
|
574
587
|
if warnings:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: graph-mind-memory
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.4
|
|
4
4
|
Summary: Local-first memory for AI coding assistants: verbatim, on your own PC, no LLM calls at write time.
|
|
5
5
|
Author-email: Yohan Ko <goyohan0611@gmail.com>
|
|
6
6
|
License-Expression: AGPL-3.0-only
|
|
@@ -78,8 +78,8 @@ calls Graph-MIND over MCP and gets back a few thousand tokens of the conversatio
|
|
|
78
78
|
## Benchmarks
|
|
79
79
|
|
|
80
80
|
All numbers below come from files in this repository: the pre-registrations, each question's
|
|
81
|
-
answer, and the judge's verdict on it, under [`runs/`](runs). The method is in
|
|
82
|
-
[REPORT.md](REPORT.md), including the failures and the corrections.
|
|
81
|
+
answer, and the judge's verdict on it, under [`research/runs/`](research/runs). The method is in
|
|
82
|
+
[REPORT.md](research/REPORT.md), including the failures and the corrections.
|
|
83
83
|
|
|
84
84
|
**LongMemEval_S: answer accuracy, 500 questions.** The answer model is gpt-5-mini; the judge is the
|
|
85
85
|
official gpt-4o-2024-08-06.
|
|
@@ -139,7 +139,7 @@ or from source:
|
|
|
139
139
|
```bash
|
|
140
140
|
git clone https://github.com/goyohan0611-png/graph-mind.git
|
|
141
141
|
cd graph-mind
|
|
142
|
-
python install.py
|
|
142
|
+
python src/install.py
|
|
143
143
|
```
|
|
144
144
|
|
|
145
145
|
The installer:
|
|
@@ -233,7 +233,7 @@ On the PC that holds the memory, tell its AI:
|
|
|
233
233
|
It replies with a connection code (`gm1.…`). On each other PC:
|
|
234
234
|
|
|
235
235
|
```bash
|
|
236
|
-
graph-mind-install --join gm1.… # or: python install.py --join gm1.…
|
|
236
|
+
graph-mind-install --join gm1.… # or: python src/install.py --join gm1.…
|
|
237
237
|
```
|
|
238
238
|
|
|
239
239
|
The memory then lives in a Postgres server on the first PC, which Graph-MIND sets up itself. Other
|
|
@@ -254,16 +254,17 @@ each PC.
|
|
|
254
254
|
## Reproducing the benchmarks
|
|
255
255
|
|
|
256
256
|
1. Download `longmemeval_s_cleaned.json` from [LongMemEval](https://github.com/xiaowu0162/LongMemEval)
|
|
257
|
-
into `external/longmemeval/`.
|
|
257
|
+
into `research/external/longmemeval/`.
|
|
258
258
|
2. Set `OPENAI_API_KEY` for the answer model and the judge.
|
|
259
|
-
3.
|
|
259
|
+
3. `pip install -e .`, then from `research/` run the commands in
|
|
260
|
+
[REPORT.md §10](research/REPORT.md#10-reproducing).
|
|
260
261
|
|
|
261
262
|
A full 500-question run costs about US$3.
|
|
262
263
|
|
|
263
264
|
## Tests
|
|
264
265
|
|
|
265
266
|
```bash
|
|
266
|
-
python -m unittest discover -p "test_*.py"
|
|
267
|
+
python -m unittest discover -s tests -p "test_*.py"
|
|
267
268
|
```
|
|
268
269
|
|
|
269
270
|
---
|
|
@@ -271,7 +272,7 @@ python -m unittest discover -p "test_*.py"
|
|
|
271
272
|
## Known limits
|
|
272
273
|
|
|
273
274
|
- **For the first few seconds after an AI app starts, answers use word search only** while the
|
|
274
|
-
embedding model loads in the background (6-
|
|
275
|
+
embedding model loads in the background (6-30 s on a slow, synced disk, depending on load; it was 50-70 s before
|
|
275
276
|
0.4). The first answer never waits for it; later answers take well under a second.
|
|
276
277
|
- **Daily use on Windows only so far.** macOS and Linux pass the tests in CI but have not seen real
|
|
277
278
|
use.
|
|
@@ -279,20 +280,17 @@ python -m unittest discover -p "test_*.py"
|
|
|
279
280
|
can stop capture until Graph-MIND is updated.
|
|
280
281
|
- **Multi-session questions are the weakest type at 80%.** These are questions that count or
|
|
281
282
|
combine facts across many conversations.
|
|
282
|
-
- **The repository still holds the research-phase experiments** next to the product (see
|
|
283
|
-
[Repository layout](#repository-layout)); the installed package carries only the 22 product
|
|
284
|
-
modules.
|
|
285
283
|
|
|
286
|
-
The full list is in [REPORT.md §9](REPORT.md#9-known-limits).
|
|
284
|
+
The full list is in [REPORT.md §9](research/REPORT.md#9-known-limits).
|
|
287
285
|
|
|
288
286
|
## Repository layout
|
|
289
287
|
|
|
290
|
-
| |
|
|
288
|
+
| folder | what is in it |
|
|
291
289
|
|---|---|
|
|
292
|
-
|
|
|
293
|
-
|
|
|
294
|
-
|
|
|
295
|
-
|
|
|
290
|
+
| [`src/`](src) | the product, exactly what `pip install graph-mind-memory` installs: `graph_mind_mcp_server.py` (MCP server), `automatic_capture*.py` (capture service), `install.py`, `brain_log.py` (sharing across PCs), `forget.py` (deleting), the stores, and search (`semantic_recall.py`, `local_embedder.py`) |
|
|
291
|
+
| [`tests/`](tests) | unit tests, run in CI on Windows, Linux and macOS |
|
|
292
|
+
| [`research/`](research) | the benchmark and research-phase scripts, [REPORT.md](research/REPORT.md), and every run's results under `runs/` |
|
|
293
|
+
| [`demo/`](demo) | the README's GIF and the script that records it |
|
|
296
294
|
|
|
297
295
|
## Security
|
|
298
296
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/associative_memory.py
|
|
5
|
+
src/automatic_capture.py
|
|
6
|
+
src/automatic_capture_cli.py
|
|
7
|
+
src/brain_log.py
|
|
8
|
+
src/clock.py
|
|
9
|
+
src/coding_memory.py
|
|
10
|
+
src/conversation_memory.py
|
|
11
|
+
src/development_memory.py
|
|
12
|
+
src/development_paths.py
|
|
13
|
+
src/embedding_warmup.py
|
|
14
|
+
src/entity_timeline.py
|
|
15
|
+
src/forget.py
|
|
16
|
+
src/graph_mind_mcp_server.py
|
|
17
|
+
src/install.py
|
|
18
|
+
src/local_brain.py
|
|
19
|
+
src/local_embedder.py
|
|
20
|
+
src/semantic_llm_extractor.py
|
|
21
|
+
src/semantic_memory.py
|
|
22
|
+
src/semantic_recall.py
|
|
23
|
+
src/temporal_resolver.py
|
|
24
|
+
src/universal_personal_memory.py
|
|
25
|
+
src/vector_cache.py
|
|
26
|
+
src/graph_mind_memory.egg-info/PKG-INFO
|
|
27
|
+
src/graph_mind_memory.egg-info/SOURCES.txt
|
|
28
|
+
src/graph_mind_memory.egg-info/dependency_links.txt
|
|
29
|
+
src/graph_mind_memory.egg-info/entry_points.txt
|
|
30
|
+
src/graph_mind_memory.egg-info/requires.txt
|
|
31
|
+
src/graph_mind_memory.egg-info/top_level.txt
|
|
32
|
+
tests/test_associative_memory.py
|
|
33
|
+
tests/test_automatic_capture.py
|
|
34
|
+
tests/test_capture_health.py
|
|
35
|
+
tests/test_coding_memory.py
|
|
36
|
+
tests/test_cold_start.py
|
|
37
|
+
tests/test_concurrent_use.py
|
|
38
|
+
tests/test_conversation_memory.py
|
|
39
|
+
tests/test_executable_memory.py
|
|
40
|
+
tests/test_executable_plan_v2_diagnostic.py
|
|
41
|
+
tests/test_forget.py
|
|
42
|
+
tests/test_forget_conversation.py
|
|
43
|
+
tests/test_graph_mind_mcp_server.py
|
|
44
|
+
tests/test_ingestion_gate.py
|
|
45
|
+
tests/test_ingestion_synonyms.py
|
|
46
|
+
tests/test_install.py
|
|
47
|
+
tests/test_llm_concept_planner.py
|
|
48
|
+
tests/test_local_brain.py
|
|
49
|
+
tests/test_longmemeval_adapter.py
|
|
50
|
+
tests/test_longmemeval_ingestion_calibration.py
|
|
51
|
+
tests/test_longmemeval_ingestion_preflight.py
|
|
52
|
+
tests/test_longmemeval_ingestion_v7.py
|
|
53
|
+
tests/test_memory_algebra.py
|
|
54
|
+
tests/test_precondition_verifier.py
|
|
55
|
+
tests/test_recent_recall.py
|
|
56
|
+
tests/test_resume_retries.py
|
|
57
|
+
tests/test_secret_masking.py
|
|
58
|
+
tests/test_semantic_llm_extractor.py
|
|
59
|
+
tests/test_semantic_memory.py
|
|
60
|
+
tests/test_semantic_recall.py
|
|
61
|
+
tests/test_shared_brain.py
|
|
62
|
+
tests/test_temporal_state.py
|
|
63
|
+
tests/test_universal_personal_memory.py
|
|
@@ -23,6 +23,9 @@ HERE = Path(__file__).resolve().parent
|
|
|
23
23
|
SERVER = HERE / "graph_mind_mcp_server.py"
|
|
24
24
|
NAME = "graph-mind-memory"
|
|
25
25
|
ENV = {"PYTHONUTF8": "1", "PYTHONIOENCODING": "utf-8"}
|
|
26
|
+
# apps whose transcripts the capture service reads: every turn is saved without the model,
|
|
27
|
+
# so the model need not spend a tool call on brain_remember for ordinary chat
|
|
28
|
+
CAPTURED = {**ENV, "GRAPH_MIND_CAPTURED": "1"}
|
|
26
29
|
|
|
27
30
|
|
|
28
31
|
def step(message):
|
|
@@ -31,7 +34,7 @@ def step(message):
|
|
|
31
34
|
|
|
32
35
|
def packages():
|
|
33
36
|
done = subprocess.run([sys.executable, "-m", "pip", "install", "--disable-pip-version-check",
|
|
34
|
-
"-q", "-r", str(HERE / "requirements.txt")])
|
|
37
|
+
"-q", "-r", str(HERE.parent / "requirements.txt")])
|
|
35
38
|
if done.returncode:
|
|
36
39
|
# PyTorch ships files nested deep enough to pass Windows' 260-character path limit when
|
|
37
40
|
# Python lives under a long folder: pip then fails halfway with "No such file".
|
|
@@ -42,10 +45,10 @@ def packages():
|
|
|
42
45
|
" or install Python or this folder under a shorter path, e.g. C:\\graph-mind")
|
|
43
46
|
|
|
44
47
|
|
|
45
|
-
def _json_entry(path: Path, key: str = "mcpServers", **extra) -> None:
|
|
48
|
+
def _json_entry(path: Path, key: str = "mcpServers", env=ENV, **extra) -> None:
|
|
46
49
|
settings = json.loads(path.read_text(encoding="utf-8")) if path.exists() else {}
|
|
47
50
|
servers = settings.setdefault(key, {})
|
|
48
|
-
environment = {**servers.get(NAME, {}).get("env", {}), **
|
|
51
|
+
environment = {**servers.get(NAME, {}).get("env", {}), **env} # keep the user's own settings
|
|
49
52
|
servers[NAME] = {**extra, "command": sys.executable, "args": [str(SERVER)], "env": environment}
|
|
50
53
|
temporary = path.with_suffix(".graph-mind.tmp")
|
|
51
54
|
temporary.write_text(json.dumps(settings, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
@@ -55,14 +58,17 @@ def _json_entry(path: Path, key: str = "mcpServers", **extra) -> None:
|
|
|
55
58
|
def claude_code(home: Path) -> str:
|
|
56
59
|
claude = shutil.which("claude") # full path: an npm install is claude.CMD, which
|
|
57
60
|
if claude: # Windows will not launch by its bare name
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
environment = [flag for key, value in CAPTURED.items() for flag in ("-e", f"{key}={value}")]
|
|
62
|
+
try:
|
|
63
|
+
subprocess.run([claude, "mcp", "remove", "-s", "user", NAME], capture_output=True)
|
|
64
|
+
# the name before -e: the CLI's -e takes every value that follows, the name included
|
|
65
|
+
subprocess.run([claude, "mcp", "add", "-s", "user", NAME, *environment, "--",
|
|
66
|
+
sys.executable, str(SERVER)], check=True, capture_output=True)
|
|
67
|
+
return "Claude Code"
|
|
68
|
+
except (OSError, subprocess.CalledProcessError):
|
|
69
|
+
pass # Windows refused to launch it (access denied, seen live): write the file
|
|
64
70
|
if (home / ".claude.json").exists():
|
|
65
|
-
_json_entry(home / ".claude.json", type="stdio")
|
|
71
|
+
_json_entry(home / ".claude.json", env=CAPTURED, type="stdio")
|
|
66
72
|
return "Claude Code"
|
|
67
73
|
return ""
|
|
68
74
|
|
|
@@ -158,9 +164,9 @@ def codex(home: Path) -> str:
|
|
|
158
164
|
text = config.read_text(encoding="utf-8") if config.exists() else ""
|
|
159
165
|
header = f"[mcp_servers.{NAME}]"
|
|
160
166
|
text = _without_codex_block(text)
|
|
161
|
-
environment = "\n".join(f"{key} = '{value}'" for key, value in
|
|
162
|
-
# required:
|
|
163
|
-
text += (f"\n{header}\
|
|
167
|
+
environment = "\n".join(f"{key} = '{value}'" for key, value in CAPTURED.items())
|
|
168
|
+
# not required: a memory server that fails to start must never keep a chat from starting
|
|
169
|
+
text += (f"\n{header}\nstartup_timeout_sec = 60\n"
|
|
164
170
|
f"command = '{sys.executable}'\nargs = ['{SERVER}']\n\n"
|
|
165
171
|
f"[mcp_servers.{NAME}.env]\n{environment}\n")
|
|
166
172
|
config.write_text(text, encoding="utf-8")
|
|
@@ -171,7 +177,7 @@ def capture_service(start: bool) -> str:
|
|
|
171
177
|
sys.path.insert(0, str(HERE))
|
|
172
178
|
from automatic_capture import _atomic_json, default_config_path, default_service_state_dir, make_config
|
|
173
179
|
if not default_config_path().exists():
|
|
174
|
-
config = make_config(workspace=str(HERE), project_id="project:graph-mind",
|
|
180
|
+
config = make_config(workspace=str(HERE.parent), project_id="project:graph-mind",
|
|
175
181
|
session_id="service:automatic-capture-v0.2",
|
|
176
182
|
sessions_root=str(Path.home() / ".codex" / "sessions"))
|
|
177
183
|
# Conversations only. Watching a code folder is opt-in: this folder is Graph-MIND itself,
|
|
@@ -263,7 +269,7 @@ def main(argv=None):
|
|
|
263
269
|
sys.path.insert(0, str(HERE))
|
|
264
270
|
from brain_log import _decode
|
|
265
271
|
_decode(args.join)
|
|
266
|
-
if not args.skip_packages and (HERE / "requirements.txt").exists(): # pip: already done
|
|
272
|
+
if not args.skip_packages and (HERE.parent / "requirements.txt").exists(): # pip: already done
|
|
267
273
|
step("installing packages (the first time takes a few minutes)")
|
|
268
274
|
packages()
|
|
269
275
|
home = Path.home()
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
from development_paths import use_wal
|
|
5
5
|
|
|
6
6
|
from pathlib import Path
|
|
7
|
+
import functools
|
|
7
8
|
import hashlib
|
|
8
9
|
import json
|
|
9
10
|
import re
|
|
@@ -39,6 +40,7 @@ def _string_list(value, error):
|
|
|
39
40
|
return list(dict.fromkeys(item.strip() for item in value))
|
|
40
41
|
|
|
41
42
|
|
|
43
|
+
@functools.lru_cache(maxsize=65536) # every memory's title and tags, on every question
|
|
42
44
|
def _normalized(value):
|
|
43
45
|
text = unicodedata.normalize("NFKC", value).casefold()
|
|
44
46
|
return " ".join(re.findall(r"[^\W_]+", text, flags=re.UNICODE))
|