graph-mind-memory 0.1.4__tar.gz → 0.2.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.
- {graph_mind_memory-0.1.4/graph_mind_memory.egg-info → graph_mind_memory-0.2.0}/PKG-INFO +15 -14
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/README.md +14 -13
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/brain_log.py +4 -1
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/graph_mind_mcp_server.py +47 -243
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0/graph_mind_memory.egg-info}/PKG-INFO +15 -14
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/install.py +13 -9
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/local_embedder.py +16 -14
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/pyproject.toml +1 -1
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/LICENSE +0 -0
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/associative_memory.py +0 -0
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/automatic_capture.py +0 -0
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/automatic_capture_cli.py +0 -0
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/clock.py +0 -0
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/coding_memory.py +0 -0
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/conversation_memory.py +0 -0
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/development_memory.py +0 -0
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/development_paths.py +0 -0
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/embedding_warmup.py +0 -0
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/entity_timeline.py +0 -0
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/graph_mind_memory.egg-info/SOURCES.txt +0 -0
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/graph_mind_memory.egg-info/dependency_links.txt +0 -0
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/graph_mind_memory.egg-info/entry_points.txt +0 -0
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/graph_mind_memory.egg-info/requires.txt +0 -0
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/graph_mind_memory.egg-info/top_level.txt +0 -0
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/local_brain.py +0 -0
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/semantic_llm_extractor.py +0 -0
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/semantic_memory.py +0 -0
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/semantic_recall.py +0 -0
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/setup.cfg +0 -0
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/temporal_resolver.py +0 -0
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/universal_personal_memory.py +0 -0
- {graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/vector_cache.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: graph-mind-memory
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
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
|
|
@@ -41,6 +41,10 @@ Local-first memory for AI coding assistants. Verbatim storage, on your own PC:
|
|
|
41
41
|
|
|
42
42
|
*Switch models; keep the memory.*
|
|
43
43
|
|
|
44
|
+
<img src="demo/demo.gif" alt="A decision made in Claude Code on Monday, recalled from Codex on Wednesday" width="760">
|
|
45
|
+
|
|
46
|
+
<sub>A real run, not a mock-up: <code>python demo/record_demo.py</code> records it from a fresh store.</sub>
|
|
47
|
+
|
|
44
48
|
</div>
|
|
45
49
|
|
|
46
50
|
> [!NOTE]
|
|
@@ -168,19 +172,14 @@ private keys, and passwords inside URLs.
|
|
|
168
172
|
|
|
169
173
|
| tool | what it does |
|
|
170
174
|
|---|---|
|
|
171
|
-
| `brain_context` | a bounded packet of the
|
|
172
|
-
| `brain_recall` |
|
|
173
|
-
| `brain_remember` | save a sourced memory (secrets masked) |
|
|
174
|
-
| `
|
|
175
|
-
| `
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
| `code_activity` | what changed in a project, file or symbol, and when |
|
|
180
|
-
| `memory_record` | record a development event or decision |
|
|
181
|
-
| `project_status` | current state of a project |
|
|
182
|
-
| `resume_project` | everything needed to pick a project back up |
|
|
183
|
-
| `explain_decision` | a decision, its reason and its history |
|
|
175
|
+
| `brain_context` | a bounded packet of the past turns and memories that answer a request; `recent=true` for "where did we leave off?" |
|
|
176
|
+
| `brain_recall` | look memories and captured turns up directly; `entity=` for everything about one thing, in order |
|
|
177
|
+
| `brain_remember` | save a sourced memory or decision (secrets masked) |
|
|
178
|
+
| `brain_folder` | where the memory lives; share it with your other PCs; reindex an imported backlog |
|
|
179
|
+
| `code_activity` | what changed in a project, file or symbol, and when (when a code folder is watched) |
|
|
180
|
+
|
|
181
|
+
Five tools on purpose: every tool's description is read by the model on every turn, and similar
|
|
182
|
+
tools get confused with each other. Version 0.1 had thirteen.
|
|
184
183
|
|
|
185
184
|
---
|
|
186
185
|
|
|
@@ -228,6 +227,8 @@ python -m unittest discover -p "test_*.py"
|
|
|
228
227
|
|
|
229
228
|
## Known limits
|
|
230
229
|
|
|
230
|
+
- **The first question after an AI app starts waits for the embedding model to load** (a few
|
|
231
|
+
seconds; 30-50 s on a slow or synced disk). Later questions take well under a second.
|
|
231
232
|
- **Daily use on Windows only so far.** macOS and Linux pass the tests in CI but have not seen real
|
|
232
233
|
use.
|
|
233
234
|
- **Capture follows each app's transcript format,** which is not a public interface. An app update
|
|
@@ -15,6 +15,10 @@ Local-first memory for AI coding assistants. Verbatim storage, on your own PC:
|
|
|
15
15
|
|
|
16
16
|
*Switch models; keep the memory.*
|
|
17
17
|
|
|
18
|
+
<img src="demo/demo.gif" alt="A decision made in Claude Code on Monday, recalled from Codex on Wednesday" width="760">
|
|
19
|
+
|
|
20
|
+
<sub>A real run, not a mock-up: <code>python demo/record_demo.py</code> records it from a fresh store.</sub>
|
|
21
|
+
|
|
18
22
|
</div>
|
|
19
23
|
|
|
20
24
|
> [!NOTE]
|
|
@@ -142,19 +146,14 @@ private keys, and passwords inside URLs.
|
|
|
142
146
|
|
|
143
147
|
| tool | what it does |
|
|
144
148
|
|---|---|
|
|
145
|
-
| `brain_context` | a bounded packet of the
|
|
146
|
-
| `brain_recall` |
|
|
147
|
-
| `brain_remember` | save a sourced memory (secrets masked) |
|
|
148
|
-
| `
|
|
149
|
-
| `
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
| `code_activity` | what changed in a project, file or symbol, and when |
|
|
154
|
-
| `memory_record` | record a development event or decision |
|
|
155
|
-
| `project_status` | current state of a project |
|
|
156
|
-
| `resume_project` | everything needed to pick a project back up |
|
|
157
|
-
| `explain_decision` | a decision, its reason and its history |
|
|
149
|
+
| `brain_context` | a bounded packet of the past turns and memories that answer a request; `recent=true` for "where did we leave off?" |
|
|
150
|
+
| `brain_recall` | look memories and captured turns up directly; `entity=` for everything about one thing, in order |
|
|
151
|
+
| `brain_remember` | save a sourced memory or decision (secrets masked) |
|
|
152
|
+
| `brain_folder` | where the memory lives; share it with your other PCs; reindex an imported backlog |
|
|
153
|
+
| `code_activity` | what changed in a project, file or symbol, and when (when a code folder is watched) |
|
|
154
|
+
|
|
155
|
+
Five tools on purpose: every tool's description is read by the model on every turn, and similar
|
|
156
|
+
tools get confused with each other. Version 0.1 had thirteen.
|
|
158
157
|
|
|
159
158
|
---
|
|
160
159
|
|
|
@@ -202,6 +201,8 @@ python -m unittest discover -p "test_*.py"
|
|
|
202
201
|
|
|
203
202
|
## Known limits
|
|
204
203
|
|
|
204
|
+
- **The first question after an AI app starts waits for the embedding model to load** (a few
|
|
205
|
+
seconds; 30-50 s on a slow or synced disk). Later questions take well under a second.
|
|
205
206
|
- **Daily use on Windows only so far.** macOS and Linux pass the tests in CI but have not seen real
|
|
206
207
|
use.
|
|
207
208
|
- **Capture follows each app's transcript format,** which is not a public interface. An app update
|
|
@@ -48,7 +48,10 @@ def device_name() -> str:
|
|
|
48
48
|
def config_path() -> Path:
|
|
49
49
|
"""Where the chosen brain folder is remembered for every client on this machine."""
|
|
50
50
|
override = os.environ.get("GRAPH_MIND_CONFIG")
|
|
51
|
-
|
|
51
|
+
if override:
|
|
52
|
+
return Path(override)
|
|
53
|
+
from development_paths import graph_mind_home
|
|
54
|
+
return graph_mind_home() / "config.json" # ~/.graph-mind unless GRAPH_MIND_HOME
|
|
52
55
|
|
|
53
56
|
|
|
54
57
|
def _config() -> dict:
|
|
@@ -15,7 +15,6 @@ from mcp.server import MCPServer
|
|
|
15
15
|
from associative_memory import AssociativeMemoryIndex
|
|
16
16
|
from coding_memory import CodingMemoryStore
|
|
17
17
|
from conversation_memory import ConversationMemoryStore
|
|
18
|
-
from development_memory import DevelopmentMemoryStore
|
|
19
18
|
from development_paths import default_development_db
|
|
20
19
|
from local_brain import LocalBrainStore
|
|
21
20
|
from universal_personal_memory import build_context_pack, decide_recall
|
|
@@ -26,14 +25,6 @@ if hasattr(sys.stderr, "reconfigure"):
|
|
|
26
25
|
|
|
27
26
|
|
|
28
27
|
SERVER_VERSION = "0.5.0"
|
|
29
|
-
RecordableEventType = Literal[
|
|
30
|
-
"OBJECTIVE_SET", "TASK_STARTED", "TASK_COMPLETED", "TASK_BLOCKED",
|
|
31
|
-
"TASK_CANCELLED", "FILE_CHANGED", "DECISION_RECORDED", "TEST_RECORDED",
|
|
32
|
-
"NEXT_ACTION_SET", "NEXT_ACTION_COMPLETED", "SESSION_SUMMARY",
|
|
33
|
-
"EXPERIMENT_STARTED", "EXPERIMENT_STOPPED",
|
|
34
|
-
]
|
|
35
|
-
|
|
36
|
-
|
|
37
28
|
# The embedding model is loaded from the local Hugging Face cache, but the library still asks the
|
|
38
29
|
# hub over the network whether a newer revision exists, every time it loads. That made the first
|
|
39
30
|
# save of a session take ~95 s and sent requests off the machine for no reason. Once the model is
|
|
@@ -346,17 +337,12 @@ def build_server(db_path=None):
|
|
|
346
337
|
"stored or noted something, and do not add lines like 'I saved that to memory'. "
|
|
347
338
|
"Mention memory only when the user asks what you remember or whether it was saved. "
|
|
348
339
|
"Call brain_context first when a request may depend on the user's prior conversations, "
|
|
349
|
-
"facts, preferences,
|
|
350
|
-
"returns a bounded evidence packet instead of
|
|
351
|
-
"
|
|
352
|
-
"
|
|
353
|
-
"or
|
|
354
|
-
"
|
|
355
|
-
"the wording into code_activity filters and use its indexed evidence. "
|
|
356
|
-
"Call project_status or resume_project when an exact project ID is already known. "
|
|
357
|
-
"Treat scope_completeness=UNATTESTED as partial observed history. "
|
|
358
|
-
"Use brain_remember for general sourced memories and memory_record for sourced "
|
|
359
|
-
"development events. Preserve provenance."
|
|
340
|
+
"facts, preferences, decisions, projects or work, including 'where did we leave off?'. "
|
|
341
|
+
"It skips unrelated requests and returns a bounded evidence packet instead of "
|
|
342
|
+
"replaying full history. Use brain_recall to look memories up directly, or with "
|
|
343
|
+
"`entity` for the history of one thing in order. For code changed in a time range, "
|
|
344
|
+
"project, file or symbol, use code_activity. brain_folder shows or changes where the "
|
|
345
|
+
"memory lives and shares it with the user's other PCs. Preserve provenance."
|
|
360
346
|
),
|
|
361
347
|
version=SERVER_VERSION,
|
|
362
348
|
)
|
|
@@ -395,6 +381,8 @@ def build_server(db_path=None):
|
|
|
395
381
|
required: put the user's own sentence there verbatim. Add `entities` (who or what it is
|
|
396
382
|
about) and `effective_at` (when it happened, ISO time) when you know them. If it updates
|
|
397
383
|
something remembered earlier, pass that memory's id as `supersedes_memory_id`.
|
|
384
|
+
Decisions and their reasons are memories too: `memory_type="decision"`, the reason in
|
|
385
|
+
`content`, the project or component in `entities`.
|
|
398
386
|
|
|
399
387
|
Save silently. Do not tell the user you saved it ("I've noted that", "저장해 뒀어"); just
|
|
400
388
|
carry on with the conversation. Talk about memory only if the user asks what you remember
|
|
@@ -443,14 +431,21 @@ def build_server(db_path=None):
|
|
|
443
431
|
openWorldHint=False,
|
|
444
432
|
),
|
|
445
433
|
)
|
|
446
|
-
def brain_recall(query: str,
|
|
434
|
+
def brain_recall(query: str = "", entity: str | None = None,
|
|
435
|
+
scopes: list[str] | None = None,
|
|
447
436
|
as_of: str | None = None,
|
|
448
437
|
knowledge_cutoff: str | None = None,
|
|
449
438
|
limit: int = 8, recent: bool = False,
|
|
450
439
|
since: str | None = None,
|
|
451
440
|
about_assistant: bool = False,
|
|
452
441
|
around: str | None = None) -> dict[str, Any]:
|
|
453
|
-
"""
|
|
442
|
+
"""Look up memories and captured conversation turns directly, from natural wording.
|
|
443
|
+
|
|
444
|
+
`entity` instead answers "what happened to X, in order": every memory about that one
|
|
445
|
+
thing (the auth module, this project, my address), oldest first, with replaced entries
|
|
446
|
+
marked and the current one picked out. Similarity search returns only the nearest one or
|
|
447
|
+
two of four decisions made about one thing in four sessions; this returns all four. With
|
|
448
|
+
neither `query` nor `entity`, it lists what the store knows about, busiest first.
|
|
454
449
|
|
|
455
450
|
Set `recent=true` whenever the question is about what happened lately or what the user was
|
|
456
451
|
just doing: "what was I working on?", "where did we leave off?", "what did I do
|
|
@@ -467,6 +462,21 @@ def build_server(db_path=None):
|
|
|
467
462
|
mention events days or weeks before or after, so that date misleads (measured: on dev2
|
|
468
463
|
it pushed the answering turn out in 2 of 11 questions and helped 2).
|
|
469
464
|
"""
|
|
465
|
+
if entity or not query:
|
|
466
|
+
import sqlite3
|
|
467
|
+
|
|
468
|
+
import entity_timeline as timeline_index
|
|
469
|
+
shared_sync(database) # every device's memories are in this one index
|
|
470
|
+
db = sqlite3.connect(database)
|
|
471
|
+
try:
|
|
472
|
+
if not entity:
|
|
473
|
+
return {"entities": timeline_index.entities(db, limit=max(limit, 50))}
|
|
474
|
+
rows = timeline_index.timeline(db, entity, limit=max(limit, 50))
|
|
475
|
+
finally:
|
|
476
|
+
db.close()
|
|
477
|
+
current = [r for r in rows if not r["superseded"]]
|
|
478
|
+
return {"entity": entity, "entries": rows, "count": len(rows),
|
|
479
|
+
"current": current[-1] if current else None}
|
|
470
480
|
return gather(database, query, scopes=scopes, as_of=as_of or _now(),
|
|
471
481
|
knowledge_cutoff=knowledge_cutoff, limit=limit, recent=recent, since=since,
|
|
472
482
|
about_assistant=about_assistant, around=around)
|
|
@@ -526,75 +536,6 @@ def build_server(db_path=None):
|
|
|
526
536
|
"STRUCTURED_EXECUTION_REQUIRED_FOR_COMPLETE_SCOPE_CLAIMS")
|
|
527
537
|
return packet
|
|
528
538
|
|
|
529
|
-
@server.tool(
|
|
530
|
-
name="brain_associate",
|
|
531
|
-
structured_output=True,
|
|
532
|
-
annotations=types.ToolAnnotations(
|
|
533
|
-
readOnlyHint=True,
|
|
534
|
-
destructiveHint=False,
|
|
535
|
-
idempotentHint=True,
|
|
536
|
-
openWorldHint=False,
|
|
537
|
-
),
|
|
538
|
-
)
|
|
539
|
-
def brain_associate(query: str, scopes: list[str] | None = None,
|
|
540
|
-
concept_cues: list[str] | None = None,
|
|
541
|
-
seed_limit: int = 12, hops: int = 2,
|
|
542
|
-
fanout: int = 12, max_nodes: int = 40,
|
|
543
|
-
limit: int = 8,
|
|
544
|
-
minimum_concept_coverage: float = 0.75,
|
|
545
|
-
grounding: str = "semantic",
|
|
546
|
-
semantic_threshold: float = 0.35) -> dict[str, Any]:
|
|
547
|
-
"""Recall indirect candidate memories through bounded associative activation.
|
|
548
|
-
|
|
549
|
-
grounding="semantic" (default, v0.7.3): on-device embedding pipeline — FTS seed ->
|
|
550
|
-
local-embedding semantic gate -> query re-ranking. On untouched production evals it
|
|
551
|
-
recovers Korean paraphrase recall (Vague Hit@5 0%->57%) at 0% control false association,
|
|
552
|
-
and matches or beats literal on exact. Loads a local model lazily; all text stays on-device.
|
|
553
|
-
grounding="auto": cheap literal gate first, semantic fallback only when literal abstains.
|
|
554
|
-
grounding="literal": the deterministic token-overlap coverage gate only.
|
|
555
|
-
"""
|
|
556
|
-
with AssociativeMemoryIndex(database) as index:
|
|
557
|
-
if grounding in ("semantic", "auto"):
|
|
558
|
-
from semantic_recall import (auto_associate, default_embedder,
|
|
559
|
-
semantic_associate)
|
|
560
|
-
embedder = default_embedder(database)
|
|
561
|
-
if grounding == "auto":
|
|
562
|
-
result = auto_associate(index, query, concept_cues, embedder=embedder,
|
|
563
|
-
literal_gate=minimum_concept_coverage,
|
|
564
|
-
semantic_threshold=semantic_threshold, scopes=scopes)
|
|
565
|
-
else:
|
|
566
|
-
result = semantic_associate(index, query, concept_cues,
|
|
567
|
-
embedder=embedder, scopes=scopes, threshold=semantic_threshold)
|
|
568
|
-
else:
|
|
569
|
-
result = index.activate(query, scopes=scopes, concept_cues=concept_cues,
|
|
570
|
-
seed_limit=seed_limit, hops=hops, fanout=fanout,
|
|
571
|
-
max_nodes=max_nodes, limit=limit,
|
|
572
|
-
minimum_concept_coverage=minimum_concept_coverage)
|
|
573
|
-
result["evidence_status"] = "CANDIDATES_REQUIRE_SOURCE_VERIFICATION"
|
|
574
|
-
return result
|
|
575
|
-
|
|
576
|
-
@server.tool(
|
|
577
|
-
name="brain_index",
|
|
578
|
-
structured_output=True,
|
|
579
|
-
annotations=types.ToolAnnotations(
|
|
580
|
-
readOnlyHint=False,
|
|
581
|
-
destructiveHint=False,
|
|
582
|
-
idempotentHint=True,
|
|
583
|
-
openWorldHint=False,
|
|
584
|
-
),
|
|
585
|
-
)
|
|
586
|
-
def brain_index(limit: int = 1000) -> dict[str, Any]:
|
|
587
|
-
"""Bring the search index up to date: link new memories, then embed the unembedded ones.
|
|
588
|
-
|
|
589
|
-
brain_remember already does this for what it writes; run this after importing a backlog, or
|
|
590
|
-
on a store that was written by an older version.
|
|
591
|
-
"""
|
|
592
|
-
from embedding_warmup import warm
|
|
593
|
-
with AssociativeMemoryIndex(database) as index:
|
|
594
|
-
synced = index.sync_sources(limit=limit)
|
|
595
|
-
return {"synced": synced, "vectors_warmed": warm(database, limit=limit),
|
|
596
|
-
"store": str(database)}
|
|
597
|
-
|
|
598
539
|
@server.tool(
|
|
599
540
|
name="brain_folder",
|
|
600
541
|
structured_output=True,
|
|
@@ -605,7 +546,8 @@ def build_server(db_path=None):
|
|
|
605
546
|
openWorldHint=False,
|
|
606
547
|
),
|
|
607
548
|
)
|
|
608
|
-
def brain_folder(path: str | None = None, share_existing: bool = False
|
|
549
|
+
def brain_folder(path: str | None = None, share_existing: bool = False,
|
|
550
|
+
reindex: bool = False) -> dict[str, Any]:
|
|
609
551
|
"""Show or choose the folder that holds the user's brain.
|
|
610
552
|
|
|
611
553
|
Call it with `path` when the user names a folder for their memory, e.g. "use my Google
|
|
@@ -624,7 +566,16 @@ def build_server(db_path=None):
|
|
|
624
566
|
|
|
625
567
|
`share_existing=true` copies what this PC already remembered into the folder, which uploads
|
|
626
568
|
it to wherever the folder syncs. Set it only when the user has explicitly agreed to that.
|
|
569
|
+
|
|
570
|
+
`reindex=true` brings the search index up to date (links new memories, embeds unembedded
|
|
571
|
+
ones). Saving already does this; it is for a large imported backlog.
|
|
627
572
|
"""
|
|
573
|
+
if reindex:
|
|
574
|
+
from embedding_warmup import warm
|
|
575
|
+
with AssociativeMemoryIndex(database) as index:
|
|
576
|
+
synced = index.sync_sources(limit=1000)
|
|
577
|
+
return {"synced": synced, "vectors_warmed": warm(database, limit=1000),
|
|
578
|
+
"store": str(database)}
|
|
628
579
|
from brain_log import device_name, devices, export_history, folder, set_folder, share, sync
|
|
629
580
|
shared_out = None
|
|
630
581
|
if path == "share":
|
|
@@ -654,62 +605,6 @@ def build_server(db_path=None):
|
|
|
654
605
|
"existing_memories_shared": exported or "not shared; ask the user before "
|
|
655
606
|
"setting share_existing=true"}
|
|
656
607
|
|
|
657
|
-
@server.tool(
|
|
658
|
-
name="brain_timeline",
|
|
659
|
-
structured_output=True,
|
|
660
|
-
annotations=types.ToolAnnotations(
|
|
661
|
-
readOnlyHint=True,
|
|
662
|
-
destructiveHint=False,
|
|
663
|
-
idempotentHint=True,
|
|
664
|
-
openWorldHint=False,
|
|
665
|
-
),
|
|
666
|
-
)
|
|
667
|
-
def brain_timeline(entity: str | None = None, limit: int = 50) -> dict[str, Any]:
|
|
668
|
-
"""Every memory about ONE thing, oldest first, with replaced entries marked.
|
|
669
|
-
|
|
670
|
-
Similarity search answers "what relates to this question"; this answers "what happened to
|
|
671
|
-
the auth module / this project / this device, in order". Those are the questions that keep
|
|
672
|
-
failing otherwise, because the four decisions about one thing sit in four different
|
|
673
|
-
sessions and only the nearest one or two come back. Call it with no entity to list what the
|
|
674
|
-
store knows about, busiest first.
|
|
675
|
-
"""
|
|
676
|
-
import sqlite3
|
|
677
|
-
|
|
678
|
-
import entity_timeline as timeline_index
|
|
679
|
-
|
|
680
|
-
shared_sync(database) # every device's memories are in this one index
|
|
681
|
-
db = sqlite3.connect(database)
|
|
682
|
-
try:
|
|
683
|
-
if not entity:
|
|
684
|
-
return {"entities": timeline_index.entities(db, limit=limit)}
|
|
685
|
-
rows = timeline_index.timeline(db, entity, limit=limit)
|
|
686
|
-
finally:
|
|
687
|
-
db.close()
|
|
688
|
-
current = [r for r in rows if not r["superseded"]]
|
|
689
|
-
return {"entity": entity, "entries": rows[:limit], "count": len(rows),
|
|
690
|
-
"current": current[-1] if current else None}
|
|
691
|
-
|
|
692
|
-
@server.tool(
|
|
693
|
-
name="conversation_recall",
|
|
694
|
-
structured_output=True,
|
|
695
|
-
annotations=types.ToolAnnotations(
|
|
696
|
-
readOnlyHint=True,
|
|
697
|
-
destructiveHint=False,
|
|
698
|
-
idempotentHint=True,
|
|
699
|
-
openWorldHint=False,
|
|
700
|
-
),
|
|
701
|
-
)
|
|
702
|
-
def conversation_recall(query: str, scopes: list[str] | None = None,
|
|
703
|
-
limit: int = 10) -> dict[str, Any]:
|
|
704
|
-
"""Search redacted conversation turns captured after Local Brain activation."""
|
|
705
|
-
try:
|
|
706
|
-
with ConversationMemoryStore(database) as store:
|
|
707
|
-
return store.search(query, scopes=scopes, limit=limit)
|
|
708
|
-
except ValueError as error:
|
|
709
|
-
return {"status": "UNKNOWN", "reason": "INVALID_CONVERSATION_QUERY",
|
|
710
|
-
"detail": str(error), "scope_completeness": "UNATTESTED",
|
|
711
|
-
"turns": []}
|
|
712
|
-
|
|
713
608
|
@server.tool(
|
|
714
609
|
name="code_activity",
|
|
715
610
|
structured_output=True,
|
|
@@ -728,7 +623,10 @@ def build_server(db_path=None):
|
|
|
728
623
|
change_kind: str | None = None,
|
|
729
624
|
limit: int = 20,
|
|
730
625
|
max_diff_chars: int = 6000) -> dict[str, Any]:
|
|
731
|
-
"""
|
|
626
|
+
"""Code changes the capture service recorded, with diff excerpts, filtered by project,
|
|
627
|
+
time range (`happened_from`/`happened_to`, ISO), file, symbol or kind. For "what did I
|
|
628
|
+
change in auth.py last week?" or "when did the parser change?". Empty unless a code folder
|
|
629
|
+
is being watched (automatic-capture.json, code_workspaces)."""
|
|
732
630
|
try:
|
|
733
631
|
bounded_diff_chars = max(200, min(max_diff_chars, 20000))
|
|
734
632
|
with CodingMemoryStore(database) as store:
|
|
@@ -741,100 +639,6 @@ def build_server(db_path=None):
|
|
|
741
639
|
"detail": str(error), "scope_completeness": "UNATTESTED",
|
|
742
640
|
"changes": []}
|
|
743
641
|
|
|
744
|
-
@server.tool(
|
|
745
|
-
name="memory_record",
|
|
746
|
-
structured_output=True,
|
|
747
|
-
annotations=types.ToolAnnotations(
|
|
748
|
-
readOnlyHint=False,
|
|
749
|
-
destructiveHint=False,
|
|
750
|
-
idempotentHint=False,
|
|
751
|
-
openWorldHint=False,
|
|
752
|
-
),
|
|
753
|
-
)
|
|
754
|
-
def memory_record(project_id: str, session_id: str, event_type: RecordableEventType,
|
|
755
|
-
subject_id: str, payload: dict[str, Any], source_ref: str,
|
|
756
|
-
effective_at: str | None = None,
|
|
757
|
-
source_type: str = "mcp-client",
|
|
758
|
-
supersedes_event_id: str | None = None,
|
|
759
|
-
event_id: str | None = None) -> dict[str, Any]:
|
|
760
|
-
"""Append one sourced development event without overwriting prior history."""
|
|
761
|
-
known_at = _now()
|
|
762
|
-
event = {"event_id": event_id or "mcp-" + uuid.uuid4().hex,
|
|
763
|
-
"project_id": project_id, "session_id": session_id,
|
|
764
|
-
"event_type": event_type, "effective_at": effective_at or known_at,
|
|
765
|
-
"known_at": known_at, "actor": "assistant", "subject_id": subject_id,
|
|
766
|
-
"payload": payload,
|
|
767
|
-
"provenance": {"source_type": source_type, "source_ref": source_ref},
|
|
768
|
-
"supersedes_event_id": supersedes_event_id}
|
|
769
|
-
with DevelopmentMemoryStore(database) as store:
|
|
770
|
-
result = store.record_event(event)
|
|
771
|
-
return {**result, "project_id": project_id, "store": str(database)}
|
|
772
|
-
|
|
773
|
-
@server.tool(
|
|
774
|
-
name="project_status",
|
|
775
|
-
structured_output=True,
|
|
776
|
-
annotations=types.ToolAnnotations(
|
|
777
|
-
readOnlyHint=True,
|
|
778
|
-
destructiveHint=False,
|
|
779
|
-
idempotentHint=True,
|
|
780
|
-
openWorldHint=False,
|
|
781
|
-
),
|
|
782
|
-
)
|
|
783
|
-
def project_status(project_id: str, as_of: str | None = None,
|
|
784
|
-
knowledge_cutoff: str | None = None) -> dict[str, Any]:
|
|
785
|
-
"""Return the current objective, active work and next actions for a project."""
|
|
786
|
-
when = as_of or _now()
|
|
787
|
-
with DevelopmentMemoryStore(database) as store:
|
|
788
|
-
resumed = store.resume_project(project_id, as_of=when,
|
|
789
|
-
knowledge_cutoff=knowledge_cutoff)
|
|
790
|
-
if resumed["status"] != "KNOWN":
|
|
791
|
-
return resumed
|
|
792
|
-
return {key: resumed[key] for key in (
|
|
793
|
-
"status", "reason", "project_id", "as_of", "knowledge_cutoff",
|
|
794
|
-
"scope_completeness", "warnings", "objective", "current_task",
|
|
795
|
-
"open_tasks", "next_actions", "active_experiments", "last_session",
|
|
796
|
-
"evidence_event_ids")}
|
|
797
|
-
|
|
798
|
-
@server.tool(
|
|
799
|
-
name="resume_project",
|
|
800
|
-
structured_output=True,
|
|
801
|
-
annotations=types.ToolAnnotations(
|
|
802
|
-
readOnlyHint=True,
|
|
803
|
-
destructiveHint=False,
|
|
804
|
-
idempotentHint=True,
|
|
805
|
-
openWorldHint=False,
|
|
806
|
-
),
|
|
807
|
-
)
|
|
808
|
-
def resume_project(project_id: str, as_of: str | None = None,
|
|
809
|
-
knowledge_cutoff: str | None = None,
|
|
810
|
-
recent_file_limit: int = 10,
|
|
811
|
-
recent_test_limit: int = 10) -> dict[str, Any]:
|
|
812
|
-
"""Build a full evidence-backed packet for continuing work in a new session."""
|
|
813
|
-
when = as_of or _now()
|
|
814
|
-
with DevelopmentMemoryStore(database) as store:
|
|
815
|
-
return store.resume_project(project_id, as_of=when,
|
|
816
|
-
knowledge_cutoff=knowledge_cutoff, recent_file_limit=recent_file_limit,
|
|
817
|
-
recent_test_limit=recent_test_limit)
|
|
818
|
-
|
|
819
|
-
@server.tool(
|
|
820
|
-
name="explain_decision",
|
|
821
|
-
structured_output=True,
|
|
822
|
-
annotations=types.ToolAnnotations(
|
|
823
|
-
readOnlyHint=True,
|
|
824
|
-
destructiveHint=False,
|
|
825
|
-
idempotentHint=True,
|
|
826
|
-
openWorldHint=False,
|
|
827
|
-
),
|
|
828
|
-
)
|
|
829
|
-
def explain_decision(project_id: str, decision_id: str,
|
|
830
|
-
as_of: str | None = None,
|
|
831
|
-
knowledge_cutoff: str | None = None) -> dict[str, Any]:
|
|
832
|
-
"""Return the active decision, reason, correction history and provenance."""
|
|
833
|
-
when = as_of or _now()
|
|
834
|
-
with DevelopmentMemoryStore(database) as store:
|
|
835
|
-
return store.explain_decision(project_id, decision_id, as_of=when,
|
|
836
|
-
knowledge_cutoff=knowledge_cutoff)
|
|
837
|
-
|
|
838
642
|
return server
|
|
839
643
|
|
|
840
644
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: graph-mind-memory
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
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
|
|
@@ -41,6 +41,10 @@ Local-first memory for AI coding assistants. Verbatim storage, on your own PC:
|
|
|
41
41
|
|
|
42
42
|
*Switch models; keep the memory.*
|
|
43
43
|
|
|
44
|
+
<img src="demo/demo.gif" alt="A decision made in Claude Code on Monday, recalled from Codex on Wednesday" width="760">
|
|
45
|
+
|
|
46
|
+
<sub>A real run, not a mock-up: <code>python demo/record_demo.py</code> records it from a fresh store.</sub>
|
|
47
|
+
|
|
44
48
|
</div>
|
|
45
49
|
|
|
46
50
|
> [!NOTE]
|
|
@@ -168,19 +172,14 @@ private keys, and passwords inside URLs.
|
|
|
168
172
|
|
|
169
173
|
| tool | what it does |
|
|
170
174
|
|---|---|
|
|
171
|
-
| `brain_context` | a bounded packet of the
|
|
172
|
-
| `brain_recall` |
|
|
173
|
-
| `brain_remember` | save a sourced memory (secrets masked) |
|
|
174
|
-
| `
|
|
175
|
-
| `
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
| `code_activity` | what changed in a project, file or symbol, and when |
|
|
180
|
-
| `memory_record` | record a development event or decision |
|
|
181
|
-
| `project_status` | current state of a project |
|
|
182
|
-
| `resume_project` | everything needed to pick a project back up |
|
|
183
|
-
| `explain_decision` | a decision, its reason and its history |
|
|
175
|
+
| `brain_context` | a bounded packet of the past turns and memories that answer a request; `recent=true` for "where did we leave off?" |
|
|
176
|
+
| `brain_recall` | look memories and captured turns up directly; `entity=` for everything about one thing, in order |
|
|
177
|
+
| `brain_remember` | save a sourced memory or decision (secrets masked) |
|
|
178
|
+
| `brain_folder` | where the memory lives; share it with your other PCs; reindex an imported backlog |
|
|
179
|
+
| `code_activity` | what changed in a project, file or symbol, and when (when a code folder is watched) |
|
|
180
|
+
|
|
181
|
+
Five tools on purpose: every tool's description is read by the model on every turn, and similar
|
|
182
|
+
tools get confused with each other. Version 0.1 had thirteen.
|
|
184
183
|
|
|
185
184
|
---
|
|
186
185
|
|
|
@@ -228,6 +227,8 @@ python -m unittest discover -p "test_*.py"
|
|
|
228
227
|
|
|
229
228
|
## Known limits
|
|
230
229
|
|
|
230
|
+
- **The first question after an AI app starts waits for the embedding model to load** (a few
|
|
231
|
+
seconds; 30-50 s on a slow or synced disk). Later questions take well under a second.
|
|
231
232
|
- **Daily use on Windows only so far.** macOS and Linux pass the tests in CI but have not seen real
|
|
232
233
|
use.
|
|
233
234
|
- **Capture follows each app's transcript format,** which is not a public interface. An app update
|
|
@@ -159,7 +159,7 @@ def capture_service(start: bool) -> str:
|
|
|
159
159
|
|
|
160
160
|
def embedding_model():
|
|
161
161
|
sys.path.insert(0, str(HERE))
|
|
162
|
-
from local_embedder import LOCAL_MODEL,
|
|
162
|
+
from local_embedder import LOCAL_MODEL, own_hf_home
|
|
163
163
|
try:
|
|
164
164
|
from transformers import AutoModel, AutoTokenizer
|
|
165
165
|
except OSError as error:
|
|
@@ -172,20 +172,24 @@ def embedding_model():
|
|
|
172
172
|
" https://aka.ms/vs/17/release/vc_redist.x64.exe\n"
|
|
173
173
|
"The apps are already connected; until then recall works by word search only.")
|
|
174
174
|
|
|
175
|
-
def download(cache_dir):
|
|
176
|
-
AutoTokenizer.from_pretrained(LOCAL_MODEL, cache_dir=cache_dir)
|
|
177
|
-
AutoModel.from_pretrained(LOCAL_MODEL, cache_dir=cache_dir)
|
|
178
175
|
try:
|
|
179
|
-
|
|
176
|
+
AutoTokenizer.from_pretrained(LOCAL_MODEL)
|
|
177
|
+
AutoModel.from_pretrained(LOCAL_MODEL)
|
|
180
178
|
except OSError as error:
|
|
181
|
-
# ~/.cache refused (access denied):
|
|
182
|
-
# where
|
|
179
|
+
# ~/.cache refused (access denied): move Hugging Face's whole home under ~/.graph-mind,
|
|
180
|
+
# where every Graph-MIND process looks from now on (local_embedder.use_own_hf_home)
|
|
183
181
|
if "PermissionError" not in str(error) and not isinstance(error.__cause__, PermissionError):
|
|
184
182
|
raise
|
|
185
|
-
own =
|
|
183
|
+
own = own_hf_home()
|
|
186
184
|
own.mkdir(parents=True, exist_ok=True)
|
|
187
185
|
step(f"the usual model folder is not writable; using {own}")
|
|
188
|
-
download
|
|
186
|
+
# huggingface_hub fixed its folders when it was imported: download in a fresh process
|
|
187
|
+
subprocess.run([sys.executable, "-c",
|
|
188
|
+
"from transformers import AutoModel, AutoTokenizer; "
|
|
189
|
+
f"AutoTokenizer.from_pretrained({LOCAL_MODEL!r}); "
|
|
190
|
+
f"AutoModel.from_pretrained({LOCAL_MODEL!r})"],
|
|
191
|
+
env={**os.environ, "HF_HOME": str(own),
|
|
192
|
+
"HF_HUB_DISABLE_SYMLINKS_WARNING": "1"}, check=True)
|
|
189
193
|
|
|
190
194
|
|
|
191
195
|
def main(argv=None):
|
|
@@ -19,22 +19,26 @@ ACTIVATE = dict(seed_limit=12, hops=2, fanout=8, max_nodes=24, limit=8) # froze
|
|
|
19
19
|
SEED_LIMIT = ACTIVATE["seed_limit"]
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
def
|
|
22
|
+
def own_hf_home() -> Path:
|
|
23
23
|
from development_paths import graph_mind_home
|
|
24
|
-
return graph_mind_home() / "
|
|
24
|
+
return graph_mind_home() / "huggingface"
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
def
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
def use_own_hf_home() -> None:
|
|
28
|
+
"""Hugging Face keeps everything under HF_HOME (~/.cache/huggingface): the model, its xet
|
|
29
|
+
download cache, its lock files. On one fresh Windows profile that folder could not be created
|
|
30
|
+
(access denied), so the installer moved all of it under ~/.graph-mind; from then on every
|
|
31
|
+
process must look there. Read before huggingface_hub is imported, which fixes the paths."""
|
|
32
|
+
if not os.environ.get("HF_HOME") and own_hf_home().is_dir():
|
|
33
|
+
os.environ["HF_HOME"] = str(own_hf_home())
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
use_own_hf_home()
|
|
32
37
|
|
|
33
38
|
|
|
34
39
|
def model_on_disk() -> bool:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return (usual / name).is_dir() or (own_model_cache() / name).is_dir()
|
|
40
|
+
hf_home = Path(os.environ.get("HF_HOME") or Path.home() / ".cache" / "huggingface")
|
|
41
|
+
return (hf_home / "hub" / ("models--" + LOCAL_MODEL.replace("/", "--"))).is_dir()
|
|
38
42
|
|
|
39
43
|
|
|
40
44
|
class LocalEmbedder:
|
|
@@ -61,10 +65,8 @@ class LocalEmbedder:
|
|
|
61
65
|
if LocalEmbedder._model is None:
|
|
62
66
|
import torch
|
|
63
67
|
from transformers import AutoModel, AutoTokenizer
|
|
64
|
-
|
|
65
|
-
LocalEmbedder.
|
|
66
|
-
LocalEmbedder._model = AutoModel.from_pretrained(self.model_name,
|
|
67
|
-
cache_dir=cache).eval()
|
|
68
|
+
LocalEmbedder._tok = AutoTokenizer.from_pretrained(self.model_name)
|
|
69
|
+
LocalEmbedder._model = AutoModel.from_pretrained(self.model_name).eval()
|
|
68
70
|
LocalEmbedder._torch = torch
|
|
69
71
|
|
|
70
72
|
def _key(self, text):
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "graph-mind-memory"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/graph_mind_memory.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/graph_mind_memory.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
{graph_mind_memory-0.1.4 → graph_mind_memory-0.2.0}/graph_mind_memory.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|