@knowall-ai/reverie 0.4.0
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/LICENSE +21 -0
- package/README.md +528 -0
- package/build/cypher-guard.js +92 -0
- package/build/embeddings.js +303 -0
- package/build/handlers/index.js +654 -0
- package/build/hygiene.js +43 -0
- package/build/index.js +65 -0
- package/build/neo4j-client.js +182 -0
- package/build/search.js +92 -0
- package/build/server.js +71 -0
- package/build/tools/definitions.js +251 -0
- package/build/tools/guidance-tool.js +284 -0
- package/build/types.js +115 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Daichi Okazaki
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
# Reverie β graph memory that dreams
|
|
2
|
+
|
|
3
|
+
**Reverie** is KnowAll AI's Neo4j knowledge-graph memory for AI agents, served over MCP. Until `@knowall-ai/reverie` is published to npm, install the legacy package `@knowall-ai/mcp-neo4j-agent-memory` (0.2.x). The GitHub repository is now `knowall-ai/mcp-reverie`, and old URLs redirect.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
Reverie turns an agent's memory from a pile of facts into a **map of the entities in its world and how they relate**, and keeps that map healthy. It is an MCP server, so any agent that speaks the Model Context Protocol (Claude Desktop, OpenClaw, Azure AI Foundry, Cursorβ¦) gets the same graph; a [Hermes Agent](https://hermes-agent.nousresearch.com) memory-provider flavour lives in [hermes-reverie](https://github.com/knowall-ai/hermes-reverie).
|
|
8
|
+
|
|
9
|
+
## Why Reverie
|
|
10
|
+
|
|
11
|
+
- **A typed entity graph, not a fact store.** People, organisations, projects, places, concepts, meetings and decisions are nodes with typed relationships. "Who at the Irish FA have we talked to about Winnie?" is a graph walk, not a similarity search.
|
|
12
|
+
- **Search that finds "Ben" when you say "Benjamin".** Hybrid keyword + semantic search, with local embeddings by default (no API key) and OpenAI, Azure OpenAI, Ollama or Voyage a config switch away.
|
|
13
|
+
- **It dreams.** A `dream` tool merges duplicates safely, canonicalises labels, re-embeds, counts orphans and flags nodes that have become property dumps, so a nightly job can keep the graph clean.
|
|
14
|
+
- **One graph, any agent.** KnowAll runs Sallie (OpenClaw) and Poppie (Hermes) against the same conventions; Reverie is how they share what they know.
|
|
15
|
+
- **LLM-driven, transparent tools.** Simple atomic operations; the model does the entity recognition and conflict resolution, and every action is explicit.
|
|
16
|
+
- **Yours to run.** Neo4j on your own machine or VM. Nothing leaves it unless you choose a remote embedding provider.
|
|
17
|
+
|
|
18
|
+
## Quick Start π
|
|
19
|
+
|
|
20
|
+
You can run this MCP server directly using npx:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx @knowall-ai/mcp-neo4j-agent-memory # becomes `npx @knowall-ai/reverie` once 0.4.0 is published
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or add it to your Claude Desktop configuration:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"neo4j-memory": {
|
|
32
|
+
"command": "npx",
|
|
33
|
+
"args": ["@knowall-ai/mcp-neo4j-agent-memory"],
|
|
34
|
+
"env": {
|
|
35
|
+
"NEO4J_URI": "bolt://localhost:7687",
|
|
36
|
+
"NEO4J_USERNAME": "neo4j",
|
|
37
|
+
"NEO4J_PASSWORD": "your-password",
|
|
38
|
+
"NEO4J_DATABASE": "neo4j"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Features
|
|
46
|
+
|
|
47
|
+
- π§ **Persistent Memory Storage** - Store and retrieve memories across conversations
|
|
48
|
+
- π **Semantic Relationships** - Create meaningful connections between memories (KNOWS, WORKS_AT, CREATED, etc.)
|
|
49
|
+
- π **Hybrid Search** - Keyword plus semantic search across all memory properties, with local embeddings by default
|
|
50
|
+
- π·οΈ **Flexible Labeling** - Use any label for memories (person, place, project, idea, etc.)
|
|
51
|
+
- β° **Temporal Tracking** - Automatic timestamps and date-based queries
|
|
52
|
+
- π **Graph Exploration** - Traverse relationships to discover connected information
|
|
53
|
+
- π― **Context-Aware** - Search with depth to include related memories
|
|
54
|
+
- π§ **LLM-Optimized** - Simple tools that let the AI handle the complexity
|
|
55
|
+
- π’ **Enterprise Ready** - Supports multiple Neo4j databases
|
|
56
|
+
- π **Built-in Guidance** - Get help on best practices and usage patterns
|
|
57
|
+
|
|
58
|
+
## Philosophy: LLM-Driven Intelligence
|
|
59
|
+
|
|
60
|
+
Unlike traditional approaches that embed complex logic in tools, this server provides simple, atomic operations and lets the LLM handle all the intelligence:
|
|
61
|
+
|
|
62
|
+
- **No hidden logic**: Tools do exactly what they say - no auto-disambiguation or smart matching
|
|
63
|
+
- **LLM decides everything**: Entity recognition, relationship inference, and conflict resolution
|
|
64
|
+
- **Transparent operations**: Every action is explicit and predictable
|
|
65
|
+
- **Maximum flexibility**: The LLM can implement any strategy without tool limitations
|
|
66
|
+
|
|
67
|
+
### Search Behavior
|
|
68
|
+
`search_memories` is hybrid: keyword hits (any word of the query as a substring of any searchable content property) rank first, then semantic matches above a similarity threshold. See **Search** below.
|
|
69
|
+
|
|
70
|
+
This approach makes the system more powerful and adaptable, as improvements in LLM capabilities directly translate to better memory management.
|
|
71
|
+
|
|
72
|
+
## Search
|
|
73
|
+
|
|
74
|
+
`search_memories` now supports three modes:
|
|
75
|
+
- `hybrid` (default): keyword hits score `1`, then semantic matches add close variants such as `Benjamin Weeks` for `Ben Weeks`
|
|
76
|
+
- `keyword`: any word of the query as a substring of any searchable content property (timestamps, `status` and embedding fields are never matched)
|
|
77
|
+
- `semantic`: uses embeddings only when available, with graceful fallback to keyword behavior if embeddings are unavailable
|
|
78
|
+
- `exact`: case-insensitive equality on `name`, `aliases` or `email`; the precise lookup to run before creating a memory, so "ben weeks" finds exactly "Ben Weeks" and nothing else
|
|
79
|
+
|
|
80
|
+
Archived memories (`status = 'archived'`) are left out of results and of `list_memory_labels` unless `include_archived: true` is passed. Returned relationships carry `_start` and `_end` node ids, so a connection's direction is always recoverable.
|
|
81
|
+
|
|
82
|
+
Use `similarity_threshold` (default `0.4`; must be between `0` and `1`, other values are rejected) to control how strict semantic matches are. Results include `_score` and `_match` on each returned `memory` object so callers can explain why a memory was returned.
|
|
83
|
+
|
|
84
|
+
### Neo4j Enterprise Support
|
|
85
|
+
|
|
86
|
+
This server now supports connecting to specific databases in Neo4j Enterprise Edition. By default, it connects to the "neo4j" database, but you can specify a different database using the `NEO4J_DATABASE` environment variable.
|
|
87
|
+
|
|
88
|
+
### Memory Tools
|
|
89
|
+
|
|
90
|
+
- `search_memories`: Search and retrieve memories from the knowledge graph
|
|
91
|
+
- **Hybrid search**: Blend keyword and semantic search; `Ben Weeks` can also find `Benjamin Weeks`
|
|
92
|
+
- Choose `search_mode` = `hybrid`, `keyword`, or `semantic`
|
|
93
|
+
- Tune semantic strictness with `similarity_threshold` (default `0.4`)
|
|
94
|
+
- Returned memories include `_score` and `_match` metadata
|
|
95
|
+
- Filter by memory type (case-insensitive, so `person` and `Person` both work), date, depth, result limit, and sort order
|
|
96
|
+
|
|
97
|
+
- `create_memory`: Create a new memory in the knowledge graph
|
|
98
|
+
- Flexible type system - any label that is a plain identifier; Capitalised singular is canonical (Person, Place, Project, Skillβ¦), and `dream` canonicalises lowercase labels
|
|
99
|
+
- Store any properties as key-value pairs
|
|
100
|
+
- Automatic timestamps for temporal tracking
|
|
101
|
+
|
|
102
|
+
- `create_connection`: Create relationships between memories
|
|
103
|
+
- Link memories using semantic relationship types (KNOWS, WORKS_AT, LIVES_IN, etc.)
|
|
104
|
+
- Add properties to relationships (since, role, status, etc.)
|
|
105
|
+
- Build complex knowledge networks
|
|
106
|
+
|
|
107
|
+
- `update_memory`: Update properties of existing memories
|
|
108
|
+
- Returns a `_hint` when the node exceeds the property limit: the graph is for entities and relationships, not a notebook
|
|
109
|
+
- Add or modify any property
|
|
110
|
+
- Set properties to null to remove them
|
|
111
|
+
|
|
112
|
+
- `update_connection`: Update relationship properties
|
|
113
|
+
- Modify relationship metadata
|
|
114
|
+
- Track changes over time
|
|
115
|
+
|
|
116
|
+
- `delete_memory`: Remove memories and all their connections
|
|
117
|
+
- Use with caution - permanent deletion
|
|
118
|
+
- Automatically removes all relationships
|
|
119
|
+
|
|
120
|
+
- `delete_connection`: Remove specific relationships
|
|
121
|
+
- Precise relationship removal
|
|
122
|
+
- Keeps the memories intact
|
|
123
|
+
|
|
124
|
+
- `list_memory_labels`: List all unique memory labels in use
|
|
125
|
+
- Shows all labels with counts
|
|
126
|
+
- Helps maintain consistency
|
|
127
|
+
- Prevents duplicate label variations
|
|
128
|
+
|
|
129
|
+
- `query_memories`: Run a read-only Cypher query
|
|
130
|
+
- Accepts a Cypher string plus optional params
|
|
131
|
+
- Runs in a Neo4j READ transaction (the server rejects writes), stops after 200 rows, and times out after 10 seconds
|
|
132
|
+
- Rejects write clauses, `CALL` subqueries, and any procedure outside a small read-only allow-list (`db.labels`, `db.propertyKeys`, `db.index.*.query*`, schema procedures). For belt and braces, run the server with a read-only Neo4j role where you can
|
|
133
|
+
|
|
134
|
+
- `memory_stats`: Summarize the current graph
|
|
135
|
+
- Returns node, relationship, label, relationship-type, embedding, and orphan counts
|
|
136
|
+
|
|
137
|
+
- `dream`: Deterministically clean up and consolidate the graph
|
|
138
|
+
- Relabels lowercase labels to their Capitalised form (`person` β `Person`), merges same-named nodes within a label when APOC is available, and refreshes embeddings
|
|
139
|
+
- Merging keeps the survivor's `name`, timestamps and vectors and combines every other property (conflicting values become lists, nothing is dropped) and skips pairs whose identity fields differ (`email`, `phone`, `website`, `company`, `organisation`, `organization`), so two different "John Smith"s stay separate. The report lists every group under `duplicates` with what was merged and what was skipped and why. Run with `dry_run: true` first to review
|
|
140
|
+
- Reports `bloated` nodes (more than `REVERIE_MAX_PROPERTIES`, default 30, real properties) with the keys that look like dated facts or prose, so a nightly sleep can fold them into attributes, relationships or notes
|
|
141
|
+
- Supports `dry_run` for a no-write report
|
|
142
|
+
|
|
143
|
+
- `get_guidance`: Get help on using the memory tools effectively
|
|
144
|
+
- Topics: labels, relationships, best-practices, examples
|
|
145
|
+
- Returns comprehensive guidance for LLMs
|
|
146
|
+
- Use when uncertain about label/relationship naming
|
|
147
|
+
|
|
148
|
+
## Prerequisites
|
|
149
|
+
|
|
150
|
+
1. **Neo4j Database** 5.9 or newer (the `dream` tool uses `COUNT {}` and `IS :: STRING`); APOC for duplicate merging
|
|
151
|
+
- Install Neo4j Community or Enterprise Edition
|
|
152
|
+
- Download from [neo4j.com/download](https://neo4j.com/download/)
|
|
153
|
+
- Or use Docker: `docker run -p 7474:7474 -p 7687:7687 -e NEO4J_AUTH=neo4j/password neo4j`
|
|
154
|
+
|
|
155
|
+
2. **Node.js** (v18 or higher)
|
|
156
|
+
- Required to run the MCP server
|
|
157
|
+
- Download from [nodejs.org](https://nodejs.org/)
|
|
158
|
+
|
|
159
|
+
3. **Claude Desktop** (for MCP integration)
|
|
160
|
+
- Download from [claude.ai/download](https://claude.ai/download)
|
|
161
|
+
|
|
162
|
+
## Installation
|
|
163
|
+
|
|
164
|
+
### Installing via Smithery
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
To install Reverie for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@knowall-ai/mcp-neo4j-agent-memory):
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
npx -y @smithery/cli install @knowall-ai/mcp-neo4j-agent-memory --client claude
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### For Development
|
|
174
|
+
|
|
175
|
+
1. Clone the repository:
|
|
176
|
+
```bash
|
|
177
|
+
git clone https://github.com/knowall-ai/mcp-reverie.git
|
|
178
|
+
cd mcp-reverie
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
2. Install dependencies:
|
|
182
|
+
```bash
|
|
183
|
+
npm install
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
3. Build the project:
|
|
187
|
+
```bash
|
|
188
|
+
npm run build
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Configuration
|
|
192
|
+
|
|
193
|
+
### Environment Variables
|
|
194
|
+
|
|
195
|
+
The server requires the following environment variables:
|
|
196
|
+
|
|
197
|
+
- `NEO4J_URI`: Neo4j database URI (required, e.g., bolt://localhost:7687)
|
|
198
|
+
- `NEO4J_USERNAME`: Neo4j username (required)
|
|
199
|
+
- `NEO4J_PASSWORD`: Neo4j password (required)
|
|
200
|
+
- `NEO4J_DATABASE`: Neo4j database name (optional) - For Neo4j Enterprise with multiple databases
|
|
201
|
+
|
|
202
|
+
## Embeddings
|
|
203
|
+
|
|
204
|
+
Set `REVERIE_EMBEDDINGS` to choose the embedding provider used by hybrid and semantic search. Changing `REVERIE_EMBEDDINGS` or `REVERIE_EMBEDDING_MODEL` causes nodes to be re-embedded lazily on the next search, or eagerly when you run `dream`.
|
|
205
|
+
|
|
206
|
+
| `REVERIE_EMBEDDINGS` | Default model | Required env vars |
|
|
207
|
+
| --- | --- | --- |
|
|
208
|
+
| `local` (default) | `Xenova/all-MiniLM-L6-v2` | none; optional `REVERIE_MODEL_CACHE` |
|
|
209
|
+
| `openai` | `text-embedding-3-small` | `OPENAI_API_KEY`; optional `OPENAI_BASE_URL` |
|
|
210
|
+
| `azure` | deployment-backed | `AZURE_OPENAI_ENDPOINT`, `AZURE_OPENAI_API_KEY`, `AZURE_OPENAI_EMBEDDING_DEPLOYMENT`; optional `AZURE_OPENAI_API_VERSION` |
|
|
211
|
+
| `ollama` | `nomic-embed-text` | optional `OLLAMA_HOST` |
|
|
212
|
+
| `voyage` | `voyage-3-lite` | `VOYAGE_API_KEY` |
|
|
213
|
+
| `none` | disabled | none |
|
|
214
|
+
|
|
215
|
+
Use `REVERIE_EMBEDDING_MODEL` to override the model name for any provider. Remote providers batch up to 64 texts per request, time out after `REVERIE_EMBED_TIMEOUT_MS` (default 30000, max 120000), and must be reached over https; plain http is only accepted for localhost.
|
|
216
|
+
|
|
217
|
+
The `local` provider downloads its model (about 23 MB) from Hugging Face on first use and caches it. If that download fails, or any provider errors, search degrades to keyword matching for that call and the error is logged to stderr. Set `REVERIE_EMBEDDINGS=none` to turn embeddings off entirely.
|
|
218
|
+
|
|
219
|
+
Each node stores two vectors: `embedding` (label, name and every text property) and `name_embedding` (label, name and aliases only), plus `embedding_model` and `embedded_at`. A semantic score is the better of the two, so a short query like `Ben Weeks` still matches a richly described `Benjamin Weeks`. None of these fields are ever returned by the tools.
|
|
220
|
+
|
|
221
|
+
### Setting up Environment Variables
|
|
222
|
+
|
|
223
|
+
#### For Development
|
|
224
|
+
Copy `.env.example` to `.env` and update with your credentials:
|
|
225
|
+
```bash
|
|
226
|
+
cp .env.example .env
|
|
227
|
+
# Edit .env with your Neo4j credentials
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
#### For Claude Desktop
|
|
231
|
+
Add the environment variables to your Claude Desktop configuration (see Quick Start above).
|
|
232
|
+
|
|
233
|
+
## Usage Examples
|
|
234
|
+
|
|
235
|
+
The LLM handles all the complexity of deciding what to store and how to connect memories:
|
|
236
|
+
|
|
237
|
+
#### Storing Information
|
|
238
|
+
```
|
|
239
|
+
User: "Remember that John works at Google as a software engineer"
|
|
240
|
+
|
|
241
|
+
Assistant: I'll remember that John works at Google as a software engineer.
|
|
242
|
+
|
|
243
|
+
[Behind the scenes, the LLM does:
|
|
244
|
+
1. search_memories({"query": "John"}) - Check if John exists
|
|
245
|
+
2. search_memories({"query": "Google"}) - Check if Google exists
|
|
246
|
+
3. create_memory({"label": "Person", "properties": {"name": "John", "occupation": "software engineer"}})
|
|
247
|
+
4. create_memory({"label": "Organization", "properties": {"name": "Google"}})
|
|
248
|
+
5. create_connection({"fromMemoryId": 123, "toMemoryId": 456, "type": "WORKS_AT"})]
|
|
249
|
+
|
|
250
|
+
User: "Remember that Sarah is John's manager and she started in 2019"
|
|
251
|
+
|
|
252
|
+
Assistant: I'll add that information about Sarah.
|
|
253
|
+
|
|
254
|
+
[The LLM searches for John, creates Sarah, and connects them with a MANAGES relationship including the start date]
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
#### Retrieving Information
|
|
258
|
+
```
|
|
259
|
+
User: "Who works at Google?"
|
|
260
|
+
|
|
261
|
+
Assistant: Let me search for people who work at Google.
|
|
262
|
+
|
|
263
|
+
[LLM uses: search_memories({"query": "Google", "depth": 2})]
|
|
264
|
+
|
|
265
|
+
Based on my memory:
|
|
266
|
+
- John - Software Engineer at Google
|
|
267
|
+
- Sarah - Manager at Google (manages John, started 2019)
|
|
268
|
+
|
|
269
|
+
User: "What do you remember about John?"
|
|
270
|
+
|
|
271
|
+
Assistant: Here's what I know about John:
|
|
272
|
+
|
|
273
|
+
[LLM uses: search_memories({"query": "John", "depth": 2})]
|
|
274
|
+
|
|
275
|
+
- Works at Google as a Software Engineer
|
|
276
|
+
- Sarah is his manager
|
|
277
|
+
- [Any other connected memories found in the search]
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
#### Building Relationships
|
|
281
|
+
```
|
|
282
|
+
User: "John and Sarah are working on Project Atlas together"
|
|
283
|
+
|
|
284
|
+
Assistant: I'll connect John and Sarah to Project Atlas.
|
|
285
|
+
|
|
286
|
+
[The LLM:
|
|
287
|
+
1. Searches for John and Sarah to get their IDs
|
|
288
|
+
2. Creates Project Atlas: create_memory({"label": "Project", "properties": {"name": "Project Atlas"}})
|
|
289
|
+
3. Connects them: create_connection({"fromMemoryId": johnId, "toMemoryId": projectId, "type": "WORKS_ON"})
|
|
290
|
+
4. create_connection({"fromMemoryId": sarahId, "toMemoryId": projectId, "type": "WORKS_ON"})]
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Connecting to a Specific Database
|
|
294
|
+
|
|
295
|
+
For Neo4j Enterprise users with multiple databases, you can specify which database to connect to:
|
|
296
|
+
|
|
297
|
+
```json
|
|
298
|
+
{
|
|
299
|
+
"env": {
|
|
300
|
+
"NEO4J_URI": "bolt://localhost:7687",
|
|
301
|
+
"NEO4J_USERNAME": "neo4j",
|
|
302
|
+
"NEO4J_PASSWORD": "your-password",
|
|
303
|
+
"NEO4J_DATABASE": "myCustomDatabase"
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
This will connect to "myCustomDatabase" instead of the default "neo4j" database.
|
|
309
|
+
|
|
310
|
+
### More Complex Examples
|
|
311
|
+
|
|
312
|
+
#### Finding Related Information
|
|
313
|
+
```
|
|
314
|
+
User: "Show me all employees in the Sales department"
|
|
315
|
+
|
|
316
|
+
Assistant: Let me search for employees in the Sales department.
|
|
317
|
+
|
|
318
|
+
[LLM uses: search_memories({"query": "Sales department", "depth": 2})]
|
|
319
|
+
|
|
320
|
+
I found these employees in the Sales department:
|
|
321
|
+
- John Smith (Sales Manager)
|
|
322
|
+
- Sarah Johnson (Sales Representative)
|
|
323
|
+
- Mike Brown (Account Executive)
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
#### Creating Domain-Specific Memories
|
|
327
|
+
```
|
|
328
|
+
User: "Create a product called 'Premium Coffee' with price $24.99"
|
|
329
|
+
|
|
330
|
+
Assistant: I'll create that product for you.
|
|
331
|
+
|
|
332
|
+
[LLM uses: create_memory({"label": "Product", "properties": {"name": "Premium Coffee", "price": 24.99}})]
|
|
333
|
+
|
|
334
|
+
Created Premium Coffee product with ID: 456
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
#### Managing Relationships
|
|
338
|
+
```
|
|
339
|
+
User: "The Premium Coffee uses beans from our Colombian Supplier"
|
|
340
|
+
|
|
341
|
+
Assistant: I'll connect Premium Coffee to the Colombian Supplier.
|
|
342
|
+
|
|
343
|
+
[LLM:
|
|
344
|
+
1. search_memories({"query": "Premium Coffee"})
|
|
345
|
+
2. search_memories({"query": "Colombian Supplier"})
|
|
346
|
+
3. create_connection({"fromMemoryId": 456, "toMemoryId": 789, "type": "SUPPLIED_BY"})]
|
|
347
|
+
|
|
348
|
+
Connected Premium Coffee to Colombian Supplier
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Advanced Queries
|
|
352
|
+
|
|
353
|
+
The LLM can handle complex queries by intelligently using the search tools:
|
|
354
|
+
|
|
355
|
+
#### Word-Based Search
|
|
356
|
+
```
|
|
357
|
+
User: "Find Ben Weeks"
|
|
358
|
+
|
|
359
|
+
Assistant: I'll search for memories containing "Ben" or "Weeks".
|
|
360
|
+
|
|
361
|
+
[LLM uses: search_memories({"query": "Ben Weeks"})]
|
|
362
|
+
|
|
363
|
+
I found several people matching your search:
|
|
364
|
+
- Ben Weeks - Software Engineer at TechCorp (exact match)
|
|
365
|
+
- Sarah Ben - Data Scientist at DataCo (contains "Ben")
|
|
366
|
+
- John Weeks - Project Manager (contains "Weeks")
|
|
367
|
+
|
|
368
|
+
The most likely match appears to be Ben Weeks, the Software Engineer.
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Note: The search finds ANY word from your query, allowing the LLM to present all potentially relevant results and identify the best match based on context.
|
|
372
|
+
|
|
373
|
+
#### Recent Memories (Last 7 Days)
|
|
374
|
+
```
|
|
375
|
+
User: "What new people have I met in the last week?"
|
|
376
|
+
|
|
377
|
+
Assistant: I'll search for people added to your memory in the last 7 days.
|
|
378
|
+
|
|
379
|
+
[LLM calculates the date 7 days ago and uses:
|
|
380
|
+
search_memories({
|
|
381
|
+
"query": "",
|
|
382
|
+
"label": "person",
|
|
383
|
+
"since_date": "2024-01-20T00:00:00Z",
|
|
384
|
+
"order_by": "created_at DESC"
|
|
385
|
+
})]
|
|
386
|
+
|
|
387
|
+
Here are the new people you've met in the last week:
|
|
388
|
+
- Sarah Chen - Met at tech conference on Monday
|
|
389
|
+
- Dr. Michael Roberts - New physician, first appointment Tuesday
|
|
390
|
+
- Emma Wilson - Joined your project team on Wednesday
|
|
391
|
+
- James Park - Coffee meeting about potential collaboration Thursday
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
#### Disambiguation
|
|
395
|
+
```
|
|
396
|
+
User: "What does John do?"
|
|
397
|
+
|
|
398
|
+
Assistant: I found multiple people named John. Let me show you what I know:
|
|
399
|
+
|
|
400
|
+
[LLM uses: search_memories({"query": "John", "type": "Person", "depth": 1})]
|
|
401
|
+
|
|
402
|
+
1. John Doe - Software Engineer at Google
|
|
403
|
+
2. John Smith - Sales Manager in Sales Department
|
|
404
|
+
3. John Brown - Project Manager on Project Atlas
|
|
405
|
+
|
|
406
|
+
Which John were you asking about?
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
#### Temporal Queries
|
|
410
|
+
```
|
|
411
|
+
User: "What events did I attend last month?"
|
|
412
|
+
|
|
413
|
+
Assistant: I'll search for event memories and check their dates.
|
|
414
|
+
|
|
415
|
+
[LLM uses: search_memories({"query": "", "label": "event", "sort_by": "created_at", "sort_order": "desc"})]
|
|
416
|
+
|
|
417
|
+
[Filters results to last month based on created_at or date properties]
|
|
418
|
+
|
|
419
|
+
Here are the events you attended last month:
|
|
420
|
+
- Tech Innovation Summit (March 5-6) - Keynote speaker
|
|
421
|
+
- Team Building Workshop (March 12) - With engineering team
|
|
422
|
+
- Client Dinner Meeting (March 18) - Project kickoff with ABC Corp
|
|
423
|
+
- WebDev Conference (March 25-27) - Attended 5 sessions on React
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### Flexible Memory Types
|
|
427
|
+
|
|
428
|
+
The system doesn't enforce strict types - you can create any type of memory that makes sense:
|
|
429
|
+
|
|
430
|
+
**Common Types** (Capitalised singular is canonical; lowercase is accepted and canonicalised by `dream`): person, place, organization, project, event, topic, object, animal, plant, food, activity, media, skill, document, meeting, task, habit, health, vehicle, tool, idea, goal
|
|
431
|
+
|
|
432
|
+
**But you can use any type** (any plain identifier): recipe, dream, memory, quote, book, movie, emotion, relationship, appointment, medication, exercise, symptom, payment, contract, etc.
|
|
433
|
+
|
|
434
|
+
The LLM will intelligently reuse existing types when appropriate to maintain consistency.
|
|
435
|
+
|
|
436
|
+
### The Power of Connections
|
|
437
|
+
|
|
438
|
+
The true value of this memory system lies not just in storing individual memories, but in **creating connections between them**. A knowledge graph becomes exponentially more useful as you build relationships:
|
|
439
|
+
|
|
440
|
+
#### Why Connections Matter
|
|
441
|
+
|
|
442
|
+
- **Context Discovery**: Connected memories provide rich context that isolated facts cannot
|
|
443
|
+
- **Relationship Patterns**: Reveal hidden patterns and insights through relationship analysis
|
|
444
|
+
- **Temporal Understanding**: Track how relationships evolve over time
|
|
445
|
+
- **Network Effects**: Each new connection increases the value of existing memories
|
|
446
|
+
|
|
447
|
+
#### Best Practices for Building Connections
|
|
448
|
+
|
|
449
|
+
1. **Always look for relationships** when storing new information:
|
|
450
|
+
```
|
|
451
|
+
Bad: Just store "John is a developer"
|
|
452
|
+
Good: Store John AND connect him to his company, projects, skills, and colleagues
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
2. **Use semantic relationship types** that capture meaning:
|
|
456
|
+
```
|
|
457
|
+
WORKS_AT, MANAGES, KNOWS, LIVES_IN, CREATED, USES, LEARNED_FROM
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
3. **Add relationship properties** for richer context:
|
|
461
|
+
```
|
|
462
|
+
create_connection({
|
|
463
|
+
"fromMemoryId": 123,
|
|
464
|
+
"toMemoryId": 456,
|
|
465
|
+
"type": "WORKS_ON",
|
|
466
|
+
"properties": {"role": "Lead", "since": "2023-01", "hours_per_week": 20}
|
|
467
|
+
})
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
4. **Think in graphs**: When recalling information, use depth > 1 to explore the network:
|
|
471
|
+
```
|
|
472
|
+
search_memories({"query": "John", "depth": 3}) // Explores connections up to 3 hops away
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
Remember: A memory without connections is like a book in a library with no catalog - it exists, but its utility is limited. The more you connect your memories, the more intelligent and useful your knowledge graph becomes.
|
|
476
|
+
|
|
477
|
+
## Testing
|
|
478
|
+
|
|
479
|
+
```bash
|
|
480
|
+
npm run build # TypeScript β build/
|
|
481
|
+
npm run test:unit # pure-module unit tests + server startup checks, no database needed
|
|
482
|
+
npm run test:integration # drives the built server over stdio against a live Neo4j
|
|
483
|
+
npm run test:coverage # everything under c8 with a coverage gate (70% lines and functions)
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
The integration tests need a Neo4j 5 with APOC and **wipe the database they point at**, so give
|
|
487
|
+
them a disposable one:
|
|
488
|
+
|
|
489
|
+
```bash
|
|
490
|
+
docker run -d --rm --name reverie-test-neo4j -p 17687:7687 \
|
|
491
|
+
-e NEO4J_AUTH=neo4j/test-password -e NEO4J_PLUGINS='["apoc"]' neo4j:5-community
|
|
492
|
+
NEO4J_URI=bolt://127.0.0.1:17687 NEO4J_USERNAME=neo4j NEO4J_PASSWORD=test-password \
|
|
493
|
+
REVERIE_TEST_DESTRUCTIVE=1 npm run test:coverage
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
`REVERIE_TEST_DESTRUCTIVE=1` is the explicit opt-in: without it the suite refuses to run. With it,
|
|
497
|
+
the suite wipes whatever `NEO4J_URI` points at, so only set it alongside a disposable database.
|
|
498
|
+
The suite waits up to a minute for authenticated Bolt before starting, and a remote `NEO4J_URI`
|
|
499
|
+
must use `bolt+s://` or `neo4j+s://` with a validated certificate (`+ssc` is refused).
|
|
500
|
+
|
|
501
|
+
CI runs the same suite against a Neo4j service container on every pull request and fails the
|
|
502
|
+
build if coverage drops below the gate. The first semantic search downloads the local embedding
|
|
503
|
+
model (about 23 MB); CI caches it.
|
|
504
|
+
|
|
505
|
+
### Interactive Testing with MCP Inspector
|
|
506
|
+
|
|
507
|
+
For interactive testing and debugging, use the MCP Inspector:
|
|
508
|
+
|
|
509
|
+
```bash
|
|
510
|
+
# Quick start with environment variables from .env
|
|
511
|
+
./run-inspector.sh
|
|
512
|
+
|
|
513
|
+
# Or manually with specific environment variables
|
|
514
|
+
NEO4J_URI=bolt://localhost:7687 \
|
|
515
|
+
NEO4J_USERNAME=neo4j \
|
|
516
|
+
NEO4J_PASSWORD=your-password \
|
|
517
|
+
npx @modelcontextprotocol/inspector build/index.js
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
The inspector provides a web UI to:
|
|
521
|
+
- Test all available tools interactively
|
|
522
|
+
- See real-time request/response data
|
|
523
|
+
- Validate your Neo4j connection
|
|
524
|
+
- Debug tool parameters and responses
|
|
525
|
+
|
|
526
|
+
## License
|
|
527
|
+
|
|
528
|
+
MIT
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** Read-only guard for caller-supplied Cypher. Defence in depth: the query also runs in a READ transaction. */
|
|
2
|
+
const WRITE_CLAUSE_RE = /\b(CREATE|MERGE|SET|DELETE|REMOVE|DROP|DETACH|FOREACH|LOAD\s+CSV)\b/i;
|
|
3
|
+
const CALL_TOKEN_RE = /\bCALL\b/gi;
|
|
4
|
+
const PROCEDURE_RE = /^\s*([A-Za-z_][\w.]*)/;
|
|
5
|
+
/** Built-in procedures that only read. Anything else (APOC, db.create.*, dbms.*) is rejected. */
|
|
6
|
+
export const READ_ONLY_PROCEDURES = new Set([
|
|
7
|
+
'db.labels',
|
|
8
|
+
'db.relationshiptypes',
|
|
9
|
+
'db.propertykeys',
|
|
10
|
+
'db.schema.visualization',
|
|
11
|
+
'db.schema.nodetypeproperties',
|
|
12
|
+
'db.schema.reltypeproperties',
|
|
13
|
+
'db.index.vector.querynodes',
|
|
14
|
+
'db.index.vector.queryrelationships',
|
|
15
|
+
'db.index.fulltext.querynodes',
|
|
16
|
+
'db.index.fulltext.queryrelationships'
|
|
17
|
+
]);
|
|
18
|
+
/**
|
|
19
|
+
* Reduce Cypher to the text the guard should look at: comments become spaces, and the contents of
|
|
20
|
+
* string literals and backtick-quoted identifiers are blanked so a `//` or a keyword inside a
|
|
21
|
+
* string can neither hide code nor trigger a false positive. A small stateful scan, not a regex,
|
|
22
|
+
* because a `//` inside a quoted URL must not be treated as a comment.
|
|
23
|
+
*/
|
|
24
|
+
export function stripComments(cypher) {
|
|
25
|
+
let out = '';
|
|
26
|
+
let i = 0;
|
|
27
|
+
const n = cypher.length;
|
|
28
|
+
while (i < n) {
|
|
29
|
+
const ch = cypher[i];
|
|
30
|
+
const next = cypher[i + 1];
|
|
31
|
+
if (ch === '/' && next === '/') {
|
|
32
|
+
while (i < n && cypher[i] !== '\n')
|
|
33
|
+
i++;
|
|
34
|
+
out += ' ';
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (ch === '/' && next === '*') {
|
|
38
|
+
const end = cypher.indexOf('*/', i + 2);
|
|
39
|
+
i = end === -1 ? n : end + 2;
|
|
40
|
+
out += ' ';
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
44
|
+
const quote = ch;
|
|
45
|
+
out += quote;
|
|
46
|
+
i++;
|
|
47
|
+
while (i < n) {
|
|
48
|
+
const c = cypher[i];
|
|
49
|
+
if (c === '\\' && quote !== '`') {
|
|
50
|
+
i += 2;
|
|
51
|
+
continue;
|
|
52
|
+
} // escaped char inside a string
|
|
53
|
+
if (c === quote) {
|
|
54
|
+
if (cypher[i + 1] === quote) {
|
|
55
|
+
i += 2;
|
|
56
|
+
continue;
|
|
57
|
+
} // doubled quote
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
i++;
|
|
61
|
+
}
|
|
62
|
+
out += quote; // literal contents dropped
|
|
63
|
+
i++;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
out += ch;
|
|
67
|
+
i++;
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
/** Returns why the Cypher is not read-only, or null when it passes the guard. */
|
|
72
|
+
export function readOnlyViolation(rawCypher) {
|
|
73
|
+
const cypher = stripComments(rawCypher);
|
|
74
|
+
const writeClause = cypher.match(WRITE_CLAUSE_RE);
|
|
75
|
+
if (writeClause) {
|
|
76
|
+
return `${writeClause[1].toUpperCase()} is not allowed`;
|
|
77
|
+
}
|
|
78
|
+
for (const match of cypher.matchAll(CALL_TOKEN_RE)) {
|
|
79
|
+
const rest = cypher.slice(match.index + match[0].length);
|
|
80
|
+
if (/^\s*\{/.test(rest)) {
|
|
81
|
+
return 'CALL subqueries are not allowed';
|
|
82
|
+
}
|
|
83
|
+
const procedure = rest.match(PROCEDURE_RE);
|
|
84
|
+
if (!procedure) {
|
|
85
|
+
return 'CALL could not be resolved to a procedure';
|
|
86
|
+
}
|
|
87
|
+
if (!READ_ONLY_PROCEDURES.has(procedure[1].toLowerCase())) {
|
|
88
|
+
return `procedure ${procedure[1]} is not on the read-only allow-list`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|