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 +161 -0
- package/dist/cypher/feedback_batch.cypher +42 -0
- package/dist/cypher/feedback_co_used_with_batch.cypher +34 -0
- package/dist/cypher/index.ts +23 -0
- package/dist/cypher/list_memories.cypher +39 -0
- package/dist/cypher/relate_concepts.cypher +13 -0
- package/dist/cypher/retrieve_context_bundle.cypher +555 -0
- package/dist/cypher/schema.cypher +20 -0
- package/dist/cypher/upsert_case.cypher +61 -0
- package/dist/cypher/upsert_memory.cypher +38 -0
- package/dist/index.cjs +789 -0
- package/dist/index.d.ts +320 -0
- package/dist/index.js +738 -0
- package/package.json +49 -0
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;
|