graph-mind-memory 0.5.2__tar.gz → 0.5.3__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.3}/PKG-INFO +15 -17
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3}/README.md +14 -16
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3}/pyproject.toml +3 -2
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3/src}/graph_mind_mcp_server.py +21 -11
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3/src}/graph_mind_memory.egg-info/PKG-INFO +15 -17
- graph_mind_memory-0.5.3/src/graph_mind_memory.egg-info/SOURCES.txt +63 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3/src}/install.py +21 -15
- graph_mind_memory-0.5.3/tests/test_associative_memory.py +120 -0
- graph_mind_memory-0.5.3/tests/test_automatic_capture.py +101 -0
- graph_mind_memory-0.5.3/tests/test_capture_health.py +36 -0
- graph_mind_memory-0.5.3/tests/test_coding_memory.py +75 -0
- graph_mind_memory-0.5.3/tests/test_cold_start.py +77 -0
- graph_mind_memory-0.5.3/tests/test_concurrent_use.py +40 -0
- graph_mind_memory-0.5.3/tests/test_conversation_memory.py +191 -0
- graph_mind_memory-0.5.3/tests/test_executable_memory.py +105 -0
- graph_mind_memory-0.5.3/tests/test_executable_plan_v2_diagnostic.py +52 -0
- graph_mind_memory-0.5.3/tests/test_forget.py +125 -0
- graph_mind_memory-0.5.3/tests/test_forget_conversation.py +100 -0
- graph_mind_memory-0.5.3/tests/test_graph_mind_mcp_server.py +118 -0
- graph_mind_memory-0.5.3/tests/test_ingestion_gate.py +67 -0
- graph_mind_memory-0.5.3/tests/test_ingestion_synonyms.py +70 -0
- graph_mind_memory-0.5.3/tests/test_install.py +160 -0
- graph_mind_memory-0.5.3/tests/test_llm_concept_planner.py +87 -0
- graph_mind_memory-0.5.3/tests/test_local_brain.py +60 -0
- graph_mind_memory-0.5.3/tests/test_longmemeval_adapter.py +49 -0
- graph_mind_memory-0.5.3/tests/test_longmemeval_ingestion_calibration.py +45 -0
- graph_mind_memory-0.5.3/tests/test_longmemeval_ingestion_preflight.py +22 -0
- graph_mind_memory-0.5.3/tests/test_longmemeval_ingestion_v7.py +55 -0
- graph_mind_memory-0.5.3/tests/test_memory_algebra.py +45 -0
- graph_mind_memory-0.5.3/tests/test_precondition_verifier.py +39 -0
- graph_mind_memory-0.5.3/tests/test_recent_recall.py +115 -0
- graph_mind_memory-0.5.3/tests/test_resume_retries.py +26 -0
- graph_mind_memory-0.5.3/tests/test_secret_masking.py +76 -0
- graph_mind_memory-0.5.3/tests/test_semantic_llm_extractor.py +102 -0
- graph_mind_memory-0.5.3/tests/test_semantic_memory.py +69 -0
- graph_mind_memory-0.5.3/tests/test_semantic_recall.py +89 -0
- graph_mind_memory-0.5.3/tests/test_shared_brain.py +269 -0
- graph_mind_memory-0.5.3/tests/test_temporal_state.py +151 -0
- graph_mind_memory-0.5.3/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.3}/LICENSE +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3}/setup.cfg +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3/src}/associative_memory.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3/src}/automatic_capture.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3/src}/automatic_capture_cli.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3/src}/brain_log.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3/src}/clock.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3/src}/coding_memory.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3/src}/conversation_memory.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3/src}/development_memory.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3/src}/development_paths.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3/src}/embedding_warmup.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3/src}/entity_timeline.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3/src}/forget.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3/src}/graph_mind_memory.egg-info/dependency_links.txt +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3/src}/graph_mind_memory.egg-info/entry_points.txt +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3/src}/graph_mind_memory.egg-info/requires.txt +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3/src}/graph_mind_memory.egg-info/top_level.txt +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3/src}/local_brain.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3/src}/local_embedder.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3/src}/semantic_llm_extractor.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3/src}/semantic_memory.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3/src}/semantic_recall.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3/src}/temporal_resolver.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3/src}/universal_personal_memory.py +0 -0
- {graph_mind_memory-0.5.2 → graph_mind_memory-0.5.3/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.3
|
|
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
|
---
|
|
@@ -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
|
---
|
|
@@ -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.3"
|
|
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",
|
|
@@ -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 "
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: graph-mind-memory
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
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
|
---
|
|
@@ -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()
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import _isolate # noqa: F401 (first: never touch the real ~/.graph-mind)
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import tempfile
|
|
4
|
+
import unittest
|
|
5
|
+
|
|
6
|
+
from associative_memory import AssociativeMemoryIndex
|
|
7
|
+
from conversation_memory import ConversationMemoryStore
|
|
8
|
+
from local_brain import LocalBrainStore
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
T1 = "2026-09-16T10:00:00"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AssociativeMemoryTests(unittest.TestCase):
|
|
15
|
+
def test_explicit_link_resolves_when_target_arrives_later(self):
|
|
16
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
17
|
+
path = Path(directory) / "brain.sqlite"
|
|
18
|
+
with AssociativeMemoryIndex(path) as index:
|
|
19
|
+
index.add_engram(engram_id="fact", source_kind="FIXTURE", source_id="f",
|
|
20
|
+
scope="global", happened_at=T1, text="durable preference",
|
|
21
|
+
related_engram_ids=["episode"])
|
|
22
|
+
index.add_engram(engram_id="episode", source_kind="FIXTURE", source_id="e",
|
|
23
|
+
scope="global", happened_at=T1, text="original conversation evidence")
|
|
24
|
+
edge = index.db.execute("""SELECT relation FROM associative_edges
|
|
25
|
+
WHERE source_engram_id='fact' AND target_engram_id='episode'""").fetchone()
|
|
26
|
+
pending = index.db.execute(
|
|
27
|
+
"SELECT COUNT(*) FROM associative_pending_edges").fetchone()[0]
|
|
28
|
+
self.assertEqual(edge["relation"], "EXPLICIT")
|
|
29
|
+
self.assertEqual(pending, 0)
|
|
30
|
+
|
|
31
|
+
def test_spreading_activation_finds_indirect_related_memory(self):
|
|
32
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
33
|
+
path = Path(directory) / "brain.sqlite"
|
|
34
|
+
with AssociativeMemoryIndex(path) as index:
|
|
35
|
+
index.add_engram(engram_id="a", source_kind="FIXTURE", source_id="a",
|
|
36
|
+
scope="global", happened_at=T1,
|
|
37
|
+
text="memory error in the personal brain", cues=["stale"], strength=0.8)
|
|
38
|
+
index.add_engram(engram_id="b", source_kind="FIXTURE", source_id="b",
|
|
39
|
+
scope="global", happened_at=T1,
|
|
40
|
+
text="stale state fixed with checkpoint", cues=["stale"], strength=0.8)
|
|
41
|
+
index.add_engram(engram_id="c", source_kind="FIXTURE", source_id="c",
|
|
42
|
+
scope="global", happened_at=T1,
|
|
43
|
+
text="checkpoint design documented in architecture note",
|
|
44
|
+
cues=["checkpoint"], strength=0.8)
|
|
45
|
+
result = index.activate("personal brain memory error", hops=2, limit=3)
|
|
46
|
+
ids = [item["engram_id"] for item in result["results"]]
|
|
47
|
+
self.assertEqual(ids[0], "a")
|
|
48
|
+
self.assertIn("c", ids)
|
|
49
|
+
c = next(item for item in result["results"] if item["engram_id"] == "c")
|
|
50
|
+
self.assertEqual(len(c["activation_path"]), 2)
|
|
51
|
+
|
|
52
|
+
def test_activation_is_bounded(self):
|
|
53
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
54
|
+
path = Path(directory) / "brain.sqlite"
|
|
55
|
+
with AssociativeMemoryIndex(path) as index:
|
|
56
|
+
for number in range(30):
|
|
57
|
+
index.add_engram(engram_id=f"e{number}", source_kind="FIXTURE",
|
|
58
|
+
source_id=str(number), scope="global", happened_at=T1,
|
|
59
|
+
text=f"shared memory topic item {number}", cues=["shared"],
|
|
60
|
+
max_automatic_links=8)
|
|
61
|
+
result = index.activate("shared memory", seed_limit=5, hops=4,
|
|
62
|
+
fanout=3, max_nodes=7, limit=5)
|
|
63
|
+
self.assertLessEqual(result["seed_count"], 5)
|
|
64
|
+
self.assertLessEqual(result["visited_nodes"], 7)
|
|
65
|
+
self.assertLessEqual(len(result["results"]), 5)
|
|
66
|
+
|
|
67
|
+
def test_external_language_adapter_can_supply_composable_concept_cues(self):
|
|
68
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
69
|
+
path = Path(directory) / "brain.sqlite"
|
|
70
|
+
with AssociativeMemoryIndex(path) as index:
|
|
71
|
+
index.add_engram(engram_id="architecture", source_kind="FIXTURE",
|
|
72
|
+
source_id="architecture", scope="global", happened_at=T1,
|
|
73
|
+
text="bounded associative layer with deterministic executor evidence vault")
|
|
74
|
+
index.add_engram(engram_id="generic-name", source_kind="FIXTURE",
|
|
75
|
+
source_id="generic-name", scope="global", happened_at=T1,
|
|
76
|
+
text="document name field")
|
|
77
|
+
raw = index.activate("사람 뇌 같은 조합", limit=3)
|
|
78
|
+
planned = index.activate("사람 뇌 같은 조합",
|
|
79
|
+
concept_cues=["associative", "deterministic executor", "evidence vault"],
|
|
80
|
+
limit=3, minimum_concept_coverage=0.75)
|
|
81
|
+
withheld = index.activate("내 반려동물 이름이 뭐였지",
|
|
82
|
+
concept_cues=["pet name"], limit=3,
|
|
83
|
+
minimum_concept_coverage=0.75)
|
|
84
|
+
self.assertEqual(raw["status"], "UNKNOWN")
|
|
85
|
+
self.assertEqual(planned["results"][0]["engram_id"], "architecture")
|
|
86
|
+
self.assertEqual(len(planned["concept_cues_used"]), 3)
|
|
87
|
+
self.assertEqual(withheld["status"], "UNKNOWN")
|
|
88
|
+
self.assertEqual(withheld["reason"], "INSUFFICIENT_CONCEPT_COVERAGE")
|
|
89
|
+
|
|
90
|
+
def test_sync_preserves_sources_as_evidence_pointers(self):
|
|
91
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
92
|
+
path = Path(directory) / "brain.sqlite"
|
|
93
|
+
with LocalBrainStore(path) as brain:
|
|
94
|
+
brain.remember({"memory_id": "preference", "scope": "global",
|
|
95
|
+
"memory_type": "PREFERENCE", "title": "answer style",
|
|
96
|
+
"content": "The user prefers concise answers.",
|
|
97
|
+
"effective_at": T1, "known_at": T1, "actor": "user",
|
|
98
|
+
"tags": ["style"], "entities": ["user"],
|
|
99
|
+
"provenance": {"source_type": "fixture", "source_ref": "turn:1"},
|
|
100
|
+
"supersedes_memory_id": None, "memory_class": "PREFERENCE",
|
|
101
|
+
"importance": 0.9})
|
|
102
|
+
with ConversationMemoryStore(path) as conversations:
|
|
103
|
+
conversations.record({"turn_id": "turn-2", "client": "other-agent",
|
|
104
|
+
"client_session_id": "session-1", "scope": "global", "cwd": None,
|
|
105
|
+
"happened_at": T1, "known_at": T1, "role": "user",
|
|
106
|
+
"content_redacted": "Please remember my concise answer style.",
|
|
107
|
+
"raw_sha256": "abc", "redaction_count": 0,
|
|
108
|
+
"source_path": "fixture.jsonl", "source_ordinal": 1})
|
|
109
|
+
with AssociativeMemoryIndex(path) as index:
|
|
110
|
+
synced = index.sync_sources()
|
|
111
|
+
result = index.activate("concise answer style", limit=5)
|
|
112
|
+
again = index.sync_sources()
|
|
113
|
+
self.assertEqual(synced["indexed_total"], 2)
|
|
114
|
+
self.assertEqual(again["status"], "NO_NEW_ENGRAMS")
|
|
115
|
+
self.assertEqual({item["source_kind"] for item in result["results"]},
|
|
116
|
+
{"LOCAL_MEMORY", "CONVERSATION"})
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == "__main__":
|
|
120
|
+
unittest.main()
|