neo4j-agent-memory 0.3.17

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.
package/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # neo4j-agent-memory
2
+
3
+ A Neo4j-backed memory system for AI agents:
4
+ - semantic / procedural / episodic memories
5
+ - case-based reasoning (symptoms -> similar cases -> fixes)
6
+ - negative memories ("do not do")
7
+ - environment fingerprints for precision
8
+ - hybrid retrieval returning a ContextBundle with separate Fix / Do-not-do blocks
9
+ - feedback API to reinforce / degrade association weights (agent affinity)
10
+
11
+ ## Install (npm)
12
+
13
+ ```bash
14
+ npm install neo4j-agent-memory
15
+ ```
16
+
17
+ ## Requirements
18
+
19
+ Hard requirements:
20
+ - Node.js 18+
21
+ - Neo4j 5+
22
+ - Windows, macOS, or Linux
23
+
24
+ Minimal requirements (tested):
25
+ - Neo4j Desktop 2.1.1 (macOS) with a Neo4j 5.x database
26
+ - Neo4j Browser + Bolt enabled
27
+ - Download: https://neo4j.com/download-thanks-desktop/?edition=desktop&flavour=osx&release=2.1.1&offline=false
28
+
29
+ ## Core API
30
+
31
+ ```ts
32
+ import { createMemoryService } from "neo4j-agent-memory";
33
+
34
+ const mem = await createMemoryService({
35
+ neo4j: { uri, username, password },
36
+ vectorIndex: "memoryEmbedding",
37
+ fulltextIndex: "memoryText",
38
+ halfLifeSeconds: 30 * 24 * 3600
39
+ });
40
+
41
+ const bundle = await mem.retrieveContextBundle({
42
+ agentId: "auggie",
43
+ prompt: "EACCES cannot create node_modules",
44
+ symptoms: ["eacces", "permission denied", "node_modules"],
45
+ tags: ["npm", "node_modules"],
46
+ env: { os: "macos", packageManager: "npm", container: false }
47
+ });
48
+
49
+ await mem.feedback({
50
+ agentId: "auggie",
51
+ sessionId: bundle.sessionId,
52
+ usedIds: bundle.sections.fix.map((m) => m.id),
53
+ usefulIds: bundle.sections.fix.slice(0, 2).map((m) => m.id),
54
+ notUsefulIds: []
55
+ });
56
+
57
+ await mem.close();
58
+ ```
59
+
60
+ Notes:
61
+ - `createMemoryService` runs schema setup on init.
62
+ - Cypher assets are bundled at `dist/cypher` in the published package.
63
+
64
+ ## Tool adapter (createMemoryTools)
65
+
66
+ Use the tool factory to preserve the existing tool surface used by the demo:
67
+
68
+ ```ts
69
+ import { createMemoryService, createMemoryTools } from "neo4j-agent-memory";
70
+
71
+ const mem = await createMemoryService({ neo4j: { uri, username, password } });
72
+ const tools = createMemoryTools(mem);
73
+
74
+ await tools.store_skill.execute({
75
+ agentId: "auggie",
76
+ title: "Fix npm EACCES on macOS",
77
+ content: "If npm fails with EACCES, chown the cache directory and retry.",
78
+ tags: ["npm", "macos", "permissions"],
79
+ });
80
+ ```
81
+
82
+ Tool names:
83
+ - `store_skill`
84
+ - `store_pattern`
85
+ - `store_concept`
86
+ - `relate_concepts`
87
+ - `recall_skills`
88
+ - `recall_concepts`
89
+ - `recall_patterns`
90
+
91
+ ## Stored UI APIs
92
+
93
+ List summaries for the UI without legacy Cypher:
94
+
95
+ ```ts
96
+ const all = await mem.listMemories({ limit: 50 });
97
+ const episodes = await mem.listEpisodes({ agentId: "auggie" });
98
+ const skills = await mem.listSkills({ agentId: "auggie" });
99
+ const concepts = await mem.listConcepts({ agentId: "auggie" });
100
+ ```
101
+
102
+ ## Episodic capture helpers
103
+
104
+ ```ts
105
+ await mem.captureEpisode({
106
+ agentId: "auggie",
107
+ runId: "run-123",
108
+ workflowName: "triage",
109
+ prompt: "Why is npm failing?",
110
+ response: "We found a permissions issue.",
111
+ outcome: "success",
112
+ tags: ["npm", "triage"],
113
+ });
114
+
115
+ await mem.captureStepEpisode({
116
+ agentId: "auggie",
117
+ runId: "run-123",
118
+ workflowName: "triage",
119
+ stepName: "fix",
120
+ prompt: "Apply the fix",
121
+ response: "Ran chown and reinstalled.",
122
+ outcome: "success",
123
+ });
124
+ ```
125
+
126
+ ## Event hooks
127
+
128
+ Provide an `onMemoryEvent` callback to observe reads/writes:
129
+
130
+ ```ts
131
+ const mem = await createMemoryService({
132
+ neo4j: { uri, username, password },
133
+ onMemoryEvent: (event) => {
134
+ console.log(`[memory:${event.type}] ${event.action}`, event.meta);
135
+ },
136
+ });
137
+ ```
138
+
139
+ ## Schema + cypher exports
140
+
141
+ Schema helpers and cypher assets are exported for integrations:
142
+
143
+ ```ts
144
+ import { ensureSchema, schemaVersion, migrate, cypher } from "neo4j-agent-memory";
145
+ ```
146
+
147
+ ## Intended usage (demo + API)
148
+
149
+ This package is used by the demo API in this repository to:
150
+ - retrieve a ContextBundle mid-run (`/memory/retrieve`)
151
+ - send feedback (`/memory/feedback`)
152
+ - save distilled learnings (`/memory/save`)
153
+
154
+ See the repo for the demo API and UI:
155
+ https://github.com/emmett08/neo4j-agent-memory-demo
156
+
157
+ ## Reinforcement model
158
+
159
+ Edges `RECALLS` and `CO_USED_WITH` are updated using a decayed Beta posterior
160
+ (a, b pseudo-counts with exponential forgetting). `strength` is cached as
161
+ a/(a+b) and `evidence` as a+b.
@@ -0,0 +1,42 @@
1
+ WITH datetime($nowIso) AS now
2
+ UNWIND $items AS item
3
+ MATCH (a:Agent {id: $agentId})
4
+ MATCH (m:Memory {id: item.memoryId})
5
+ MERGE (a)-[r:RECALLS]->(m)
6
+ ON CREATE SET
7
+ r.a = 1.0,
8
+ r.b = 1.0,
9
+ r.updatedAt = now,
10
+ r.uses = 0,
11
+ r.successes = 0,
12
+ r.failures = 0
13
+
14
+ WITH now, r, item,
15
+ // elapsed seconds, safe
16
+ CASE WHEN duration.inSeconds(coalesce(r.updatedAt, now), now).seconds > 0
17
+ THEN duration.inSeconds(coalesce(r.updatedAt, now), now).seconds
18
+ ELSE 0 END AS dt
19
+
20
+ WITH now, r, item,
21
+ 0.5 ^ (dt / $halfLifeSeconds) AS gamma,
22
+ coalesce(r.a, CASE WHEN $aMin > coalesce(r.strength, 0.5) * 2.0 THEN $aMin ELSE coalesce(r.strength, 0.5) * 2.0 END) AS aPrev,
23
+ coalesce(r.b, CASE WHEN $bMin > (1.0 - coalesce(r.strength, 0.5)) * 2.0 THEN $bMin ELSE (1.0 - coalesce(r.strength, 0.5)) * 2.0 END) AS bPrev
24
+
25
+ WITH now, r, item, gamma,
26
+ ($aMin + gamma * (aPrev - $aMin)) AS a0,
27
+ ($bMin + gamma * (bPrev - $bMin)) AS b0
28
+
29
+ WITH now, r, item,
30
+ (a0 + item.w * item.y) AS a1,
31
+ (b0 + item.w * (1.0 - item.y)) AS b1
32
+
33
+ SET r.a = a1,
34
+ r.b = b1,
35
+ r.strength = a1 / (a1 + b1),
36
+ r.evidence = a1 + b1,
37
+ r.updatedAt = now,
38
+ r.uses = coalesce(r.uses,0) + 1,
39
+ r.successes = coalesce(r.successes,0) + CASE WHEN item.y >= 0.5 THEN 1 ELSE 0 END,
40
+ r.failures = coalesce(r.failures,0) + CASE WHEN item.y < 0.5 THEN 1 ELSE 0 END
41
+
42
+ RETURN count(*) AS updated;
@@ -0,0 +1,34 @@
1
+ WITH datetime($nowIso) AS now
2
+ UNWIND $pairs AS pair
3
+ MATCH (m1:Memory {id: pair.a})
4
+ MATCH (m2:Memory {id: pair.b})
5
+ MERGE (m1)-[c:CO_USED_WITH]->(m2)
6
+ ON CREATE SET
7
+ c.a = 1.0,
8
+ c.b = 1.0,
9
+ c.updatedAt = now
10
+
11
+ WITH now, c, pair,
12
+ CASE WHEN duration.inSeconds(coalesce(c.updatedAt, now), now).seconds > 0
13
+ THEN duration.inSeconds(coalesce(c.updatedAt, now), now).seconds
14
+ ELSE 0 END AS dt
15
+ WITH now, c, pair,
16
+ 0.5 ^ (dt / $halfLifeSeconds) AS gamma,
17
+ coalesce(c.a, CASE WHEN $aMin > coalesce(c.strength, 0.5) * 2.0 THEN $aMin ELSE coalesce(c.strength, 0.5) * 2.0 END) AS aPrev,
18
+ coalesce(c.b, CASE WHEN $bMin > (1.0 - coalesce(c.strength, 0.5)) * 2.0 THEN $bMin ELSE (1.0 - coalesce(c.strength, 0.5)) * 2.0 END) AS bPrev
19
+
20
+ WITH now, c, pair, gamma,
21
+ ($aMin + gamma * (aPrev - $aMin)) AS a0,
22
+ ($bMin + gamma * (bPrev - $bMin)) AS b0
23
+
24
+ WITH now, c, pair,
25
+ (a0 + pair.w * pair.y) AS a1,
26
+ (b0 + pair.w * (1.0 - pair.y)) AS b1
27
+
28
+ SET c.a = a1,
29
+ c.b = b1,
30
+ c.strength = a1 / (a1 + b1),
31
+ c.evidence = a1 + b1,
32
+ c.updatedAt = now
33
+
34
+ RETURN count(*) AS updated;
@@ -0,0 +1,23 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+ import path from "node:path";
4
+
5
+ declare const __dirname: string | undefined;
6
+
7
+ const resolvedDir = typeof __dirname === "string" ? __dirname : path.dirname(fileURLToPath(import.meta.url));
8
+ const baseDir = path.basename(resolvedDir) === "cypher" ? resolvedDir : path.resolve(resolvedDir, "cypher");
9
+
10
+ export function loadCypher(rel: string): string {
11
+ return readFileSync(path.resolve(baseDir, rel), "utf8");
12
+ }
13
+
14
+ export const cypher = {
15
+ schema: loadCypher("schema.cypher"),
16
+ upsertMemory: loadCypher("upsert_memory.cypher"),
17
+ upsertCase: loadCypher("upsert_case.cypher"),
18
+ retrieveContextBundle: loadCypher("retrieve_context_bundle.cypher"),
19
+ feedbackBatch: loadCypher("feedback_batch.cypher"),
20
+ feedbackCoUsed: loadCypher("feedback_co_used_with_batch.cypher"),
21
+ listMemories: loadCypher("list_memories.cypher"),
22
+ relateConcepts: loadCypher("relate_concepts.cypher"),
23
+ };
@@ -0,0 +1,39 @@
1
+ // Parameters:
2
+ // - $kind: Optional memory kind filter ("semantic" | "procedural" | "episodic")
3
+ // - $limit: Max number of memories to return
4
+ // - $agentId: Optional agent id to filter by RECALLS edges
5
+ WITH
6
+ $kind AS kind,
7
+ coalesce($limit, 50) AS limit,
8
+ $agentId AS agentId
9
+
10
+ CALL {
11
+ WITH kind, agentId
12
+ WITH kind, agentId
13
+ WHERE agentId IS NOT NULL AND agentId <> ""
14
+ MATCH (:Agent {id: agentId})-[:RECALLS]->(m:Memory)
15
+ WHERE kind IS NULL OR m.kind = kind
16
+ RETURN m
17
+ UNION
18
+ WITH kind, agentId
19
+ WHERE agentId IS NULL OR agentId = ""
20
+ MATCH (m:Memory)
21
+ WHERE kind IS NULL OR m.kind = kind
22
+ RETURN m
23
+ }
24
+ WITH m
25
+ ORDER BY m.updatedAt DESC
26
+ WITH collect(
27
+ m {
28
+ .id,
29
+ .kind,
30
+ .polarity,
31
+ .title,
32
+ .tags,
33
+ .confidence,
34
+ .utility,
35
+ .createdAt,
36
+ .updatedAt
37
+ }
38
+ ) AS rows, limit
39
+ RETURN rows[0..limit] AS memories;
@@ -0,0 +1,13 @@
1
+ // Parameters:
2
+ // - $a: Memory id (concept A)
3
+ // - $b: Memory id (concept B)
4
+ // - $weight: Optional relationship weight
5
+ MATCH (a:Memory {id: $a})
6
+ MATCH (b:Memory {id: $b})
7
+ MERGE (a)-[r:RELATED_TO]->(b)
8
+ ON CREATE SET r.weight = $weight, r.createdAt = datetime(), r.updatedAt = datetime()
9
+ ON MATCH SET r.weight = $weight, r.updatedAt = datetime()
10
+ MERGE (b)-[r2:RELATED_TO]->(a)
11
+ ON CREATE SET r2.weight = $weight, r2.createdAt = datetime(), r2.updatedAt = datetime()
12
+ ON MATCH SET r2.weight = $weight, r2.updatedAt = datetime()
13
+ RETURN a.id AS aId, b.id AS bId;