cozo-memory 1.2.6 → 1.2.9

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 CHANGED
@@ -3,8 +3,12 @@
3
3
  [![npm](https://img.shields.io/npm/v/cozo-memory)](https://www.npmjs.com/package/cozo-memory)
4
4
  [![Node](https://img.shields.io/node/v/cozo-memory)](https://nodejs.org)
5
5
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue)](LICENSE)
6
+ [![MCP Badge](https://lobehub.com/badge/mcp/tobs-code-cozo-memory)](https://lobehub.com/mcp/tobs-code-cozo-memory)
6
7
 
7
- **Local-first memory for Claude & AI agents with hybrid search, Graph-RAG, and time-travel – all in a single binary, no cloud, no Docker.**
8
+ > **Why Cozo Memory?**
9
+ > LLMs have short-term memory limits. Standard RAG retrieves documents but can't connect facts across time. Cozo Memory gives your AI agent **persistent, structured memory** – it remembers past conversations, infers relationships, detects contradictions, and explores its knowledge graph – fully on your machine, with **optional local LLM integration via Ollama** for intelligent actions (cleanup, reflection, summarization, agentic routing).
10
+
11
+ **Local-first memory for Claude & AI agents with hybrid search, Graph-RAG, and time-travel – runs entirely on your machine. Optional [Ollama](https://ollama.ai) integration enables LLM-powered actions (cleanup, reflect, summarize, agentic retrieval).**
8
12
 
9
13
  ## Table of Contents
10
14
 
@@ -51,7 +55,7 @@ Now add the server to your MCP client (e.g. Claude Desktop) – see [Integration
51
55
 
52
56
  ⏳ **Temporal Conflict Resolution** - Automatic detection and resolution of contradictory observations with semantic analysis and audit preservation
53
57
 
54
- 🏠 **100% Local** - Embeddings via ONNX/Transformers; no external services, no cloud, complete data ownership
58
+ 🏠 **100% Local** - Embeddings via ONNX/Transformers; data stays on your machine. Some advanced features (cleanup, reflect, summarize, agentic search) require an optional [Ollama](https://ollama.ai) service for local LLM inference — but the core search, CRUD, and graph operations work **without any LLM**.
55
59
 
56
60
  🧠 **Multi-Hop Reasoning** - Logic-aware graph traversal with vector pivots for deep relational reasoning
57
61
 
@@ -89,9 +93,34 @@ The core advantage is **Intelligence and Traceability**: By combining an **Agent
89
93
  - **RAM: 1.7 GB minimum** (for default bge-m3 model)
90
94
  - Model download: ~600 MB
91
95
  - Runtime memory: ~1.1 GB
92
- - For lower-spec machines, see [Embedding Model Options](#embedding-model-options) below
96
+ - **Too heavy?** Use `EMBEDDING_MODEL=Xenova/all-MiniLM-L6-v2` only **~400 MB RAM** needed (see [Embedding Model Options](#embedding-model-options))
93
97
  - CozoDB native dependency is installed via `cozo-node`
94
98
 
99
+ ### Optional: Ollama for LLM-powered actions
100
+
101
+ Some advanced actions use a local LLM via [Ollama](https://ollama.ai) for intelligent
102
+ processing. **The core server works without Ollama** (CRUD, search, graph operations),
103
+ but the following actions require it:
104
+
105
+ | Action | Purpose |
106
+ |--------|---------|
107
+ | `cleanup` | LLM-backed observation consolidation |
108
+ | `reflect` | Generate insights, detect contradictions |
109
+ | `summarize_communities` | LLM-generated community summaries |
110
+ | `compact` | Session / entity compaction with LLM summarization |
111
+ | `agentic_search` | Query intent classification for auto-routing |
112
+
113
+ **Setup (if you need these features):**
114
+ ```bash
115
+ # 1. Install Ollama from https://ollama.ai
116
+ # 2. Pull a model (e.g. small + fast for dev):
117
+ ollama pull demyagent-4b-i1:Q6_K
118
+ # 3. Ollama runs automatically on http://localhost:11434
119
+ ```
120
+
121
+ If Ollama is not running, the affected actions gracefully fall back to non-LLM behavior
122
+ (where possible) or return a clear error message.
123
+
95
124
  ### Via npm (Easiest)
96
125
 
97
126
  ```bash
@@ -337,10 +366,10 @@ The interface is reduced to **5 consolidated tools**:
337
366
 
338
367
  | Tool | Purpose | Key Actions |
339
368
  |------|---------|-------------|
340
- | `mutate_memory` | Write operations | create_entity, update_entity, delete_entity, add_observation, create_relation, transactions, sessions, tasks |
341
- | `query_memory` | Read operations | search, advancedSearch, context, graph_rag, graph_walking, agentic_search, adaptive_retrieval |
369
+ | `mutate_memory` | Write operations | create_entity, update_entity, delete_entity, add_observation, create_relation, transactions, sessions, tasks, update_observation, batch_delete, manage_tags, batch |
370
+ | `query_memory` | Read operations | search, advancedSearch, context, graph_rag, graph_walking, agentic_search, adaptive_retrieval, list_entities, get_entity_detail, get_session_context, list_sessions |
342
371
  | `analyze_graph` | Graph analysis | explore, communities, pagerank, betweenness, hits, shortest_path, semantic_walk |
343
- | `manage_system` | Maintenance | health, metrics, export, import, cleanup, defrag, reflect, snapshots |
372
+ | `manage_system` | Maintenance | health, metrics, stats, export, import, cleanup, defrag, reflect, snapshots |
344
373
  | `edit_user_profile` | User preferences | Edit global user profile with preferences and work style |
345
374
 
346
375
  > **See [docs/API.md](docs/API.md) for complete API reference with all parameters and examples**
@@ -354,10 +383,12 @@ The interface is reduced to **5 consolidated tools**:
354
383
  - This is normal and only happens once
355
384
  - Subsequent starts are fast (< 2 seconds)
356
385
 
357
- **Cleanup/Reflect Requires Ollama**
358
- - If using `cleanup` or `reflect` actions, an Ollama service must be running locally
386
+ **LLM-powered actions require Ollama**
387
+ - The following actions use a local LLM for intelligent processing: `cleanup`, `reflect`, `summarize_communities`, `compact`, `agentic_search`
359
388
  - Install Ollama from https://ollama.ai
360
389
  - Pull the desired model: `ollama pull demyagent-4b-i1:Q6_K` (or your preferred model)
390
+ - Without Ollama, these actions fall back to non-LLM behavior or return a clear error
391
+ - **Core features (CRUD, search, graph, infer) work without any LLM**
361
392
 
362
393
  **Windows-Specific**
363
394
  - Embeddings are processed on CPU for maximum compatibility
@@ -403,30 +434,6 @@ npm run benchmark # Runs performance tests
403
434
  npm run eval # Runs evaluation suite
404
435
  ```
405
436
 
406
- ## Roadmap
407
-
408
- ### Near-Term (v1.x)
409
-
410
- - **GPU Acceleration** - CUDA support for embedding generation (10-50x faster)
411
- - **Streaming Ingestion** - Real-time data ingestion from logs, APIs, webhooks
412
- - **Advanced Chunking** - Semantic chunking for `ingest_file` (paragraph-aware splitting)
413
- - **Query Optimization** - Automatic query plan optimization for complex graph traversals
414
- - **Additional Export Formats** - Notion, Roam Research, Logseq compatibility
415
-
416
- ### Mid-Term (v2.x)
417
-
418
- - **Multi-Modal Embeddings** - Support for images, audio, code
419
- - **Distributed Memory** - Sharding and replication for large-scale deployments
420
- - **Advanced Inference** - Neural-symbolic reasoning, causal inference
421
- - **Real-Time Sync** - WebSocket-based real-time updates
422
- - **Web UI** - Browser-based management interface
423
-
424
- ### Long-Term (v3.x)
425
-
426
- - **Federated Learning** - Privacy-preserving collaborative learning
427
- - **Quantum-Inspired Algorithms** - Advanced graph algorithms
428
- - **Multi-Agent Coordination** - Shared memory across multiple agents
429
-
430
437
  ## Contributing
431
438
 
432
439
  Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
@@ -0,0 +1,313 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const db_service_1 = require("./db-service");
4
+ describe("DatabaseService", () => {
5
+ let db;
6
+ beforeEach(async () => {
7
+ db = new db_service_1.DatabaseService(":memory:", "sqlite");
8
+ await db.initialize();
9
+ });
10
+ // ── Entity CRUD ──────────────────────────────────────────────
11
+ describe("Entity CRUD", () => {
12
+ const sampleEntity = {
13
+ id: "e1",
14
+ name: "Test Entity",
15
+ type: "Test",
16
+ embedding: [0.1, 0.2, 0.3],
17
+ name_embedding: [0.4, 0.5, 0.6],
18
+ metadata: { key: "value" },
19
+ created_at: 1000,
20
+ };
21
+ it("should create and get an entity", async () => {
22
+ await db.createEntity(sampleEntity);
23
+ const result = await db.getEntity("e1");
24
+ expect(result).not.toBeNull();
25
+ expect(result.name).toBe("Test Entity");
26
+ expect(result.type).toBe("Test");
27
+ expect(result.embedding).toEqual([0.1, 0.2, 0.3]);
28
+ expect(result.metadata).toEqual({ key: "value" });
29
+ });
30
+ it("should return null for non-existent entity", async () => {
31
+ const result = await db.getEntity("non_existent");
32
+ expect(result).toBeNull();
33
+ });
34
+ it("should update an entity partially", async () => {
35
+ await db.createEntity(sampleEntity);
36
+ await db.updateEntity("e1", { name: "Updated Entity", metadata: { new: "meta" } });
37
+ const result = await db.getEntity("e1");
38
+ expect(result).not.toBeNull();
39
+ expect(result.name).toBe("Updated Entity");
40
+ // metadata should be merged
41
+ expect(result.metadata).toEqual({ key: "value", new: "meta" });
42
+ });
43
+ it("should update embedding fields", async () => {
44
+ await db.createEntity(sampleEntity);
45
+ const newEmbedding = [0.9, 0.8, 0.7];
46
+ await db.updateEntity("e1", { embedding: newEmbedding });
47
+ const result = await db.getEntity("e1");
48
+ expect(result.embedding).toEqual(newEmbedding);
49
+ // name_embedding should remain unchanged
50
+ expect(result.name_embedding).toEqual([0.4, 0.5, 0.6]);
51
+ });
52
+ it("should not throw when updating non-existent entity", async () => {
53
+ await expect(db.updateEntity("ghost", { name: "X" })).resolves.not.toThrow();
54
+ });
55
+ it("should delete an entity and its observations", async () => {
56
+ await db.createEntity(sampleEntity);
57
+ await db.addObservation({
58
+ id: "obs1",
59
+ entity_id: "e1",
60
+ text: "test",
61
+ embedding: [],
62
+ metadata: {},
63
+ created_at: 1001,
64
+ });
65
+ await db.deleteEntity("e1");
66
+ const entity = await db.getEntity("e1");
67
+ expect(entity).toBeNull();
68
+ // Observation should also be deleted
69
+ const obs = await db.getObservationsForEntity("e1");
70
+ expect(obs).toHaveLength(0);
71
+ });
72
+ });
73
+ // ── Observations ─────────────────────────────────────────────
74
+ describe("Observation CRUD", () => {
75
+ beforeEach(async () => {
76
+ await db.createEntity({
77
+ id: "e2",
78
+ name: "Entity With Obs",
79
+ type: "Test",
80
+ embedding: [],
81
+ name_embedding: [],
82
+ metadata: {},
83
+ created_at: 2000,
84
+ });
85
+ });
86
+ it("should add and retrieve observations for an entity", async () => {
87
+ await db.addObservation({
88
+ id: "obs1",
89
+ entity_id: "e2",
90
+ text: "First observation",
91
+ embedding: [1.0, 2.0],
92
+ metadata: { source: "test" },
93
+ created_at: 2001,
94
+ });
95
+ await db.addObservation({
96
+ id: "obs2",
97
+ entity_id: "e2",
98
+ text: "Second observation",
99
+ embedding: [3.0, 4.0],
100
+ metadata: {},
101
+ created_at: 2002,
102
+ });
103
+ const obs = await db.getObservationsForEntity("e2");
104
+ expect(obs).toHaveLength(2);
105
+ expect(obs[0].text).toBe("First observation");
106
+ expect(obs[1].text).toBe("Second observation");
107
+ });
108
+ it("should return empty array for entity with no observations", async () => {
109
+ const obs = await db.getObservationsForEntity("e2");
110
+ expect(obs).toHaveLength(0);
111
+ });
112
+ it("should return empty array for non-existent entity", async () => {
113
+ const obs = await db.getObservationsForEntity("ghost");
114
+ expect(obs).toHaveLength(0);
115
+ });
116
+ });
117
+ // ── Relationships ────────────────────────────────────────────
118
+ describe("Relationship CRUD", () => {
119
+ beforeEach(async () => {
120
+ for (const id of ["a", "b", "c"]) {
121
+ await db.createEntity({
122
+ id,
123
+ name: `Entity ${id}`,
124
+ type: "Test",
125
+ embedding: [],
126
+ name_embedding: [],
127
+ metadata: {},
128
+ created_at: 3000,
129
+ });
130
+ }
131
+ });
132
+ it("should create and list all relations", async () => {
133
+ await db.createRelation({
134
+ from_id: "a", to_id: "b", relation_type: "knows",
135
+ strength: 0.8, metadata: {}, created_at: 3001,
136
+ });
137
+ await db.createRelation({
138
+ from_id: "b", to_id: "c", relation_type: "likes",
139
+ strength: 1.0, metadata: { since: "2024" }, created_at: 3002,
140
+ });
141
+ const all = await db.getRelations();
142
+ expect(all).toHaveLength(2);
143
+ });
144
+ it("should filter relations by from_id", async () => {
145
+ await db.createRelation({
146
+ from_id: "a", to_id: "b", relation_type: "knows",
147
+ strength: 0.5, metadata: {}, created_at: 3001,
148
+ });
149
+ await db.createRelation({
150
+ from_id: "a", to_id: "c", relation_type: "knows",
151
+ strength: 0.3, metadata: {}, created_at: 3002,
152
+ });
153
+ await db.createRelation({
154
+ from_id: "b", to_id: "c", relation_type: "likes",
155
+ strength: 0.9, metadata: {}, created_at: 3003,
156
+ });
157
+ const fromA = await db.getRelations("a");
158
+ expect(fromA).toHaveLength(2);
159
+ const fromB = await db.getRelations("b");
160
+ expect(fromB).toHaveLength(1);
161
+ expect(fromB[0].to_id).toBe("c");
162
+ });
163
+ it("should filter relations by to_id", async () => {
164
+ await db.createRelation({
165
+ from_id: "a", to_id: "c", relation_type: "knows",
166
+ strength: 0.5, metadata: {}, created_at: 3001,
167
+ });
168
+ await db.createRelation({
169
+ from_id: "b", to_id: "c", relation_type: "likes",
170
+ strength: 0.9, metadata: {}, created_at: 3002,
171
+ });
172
+ const toC = await db.getRelations(undefined, "c");
173
+ expect(toC).toHaveLength(2);
174
+ });
175
+ it("should delete entity and cascade its relations", async () => {
176
+ await db.createRelation({
177
+ from_id: "a", to_id: "b", relation_type: "knows",
178
+ strength: 0.5, metadata: {}, created_at: 3001,
179
+ });
180
+ await db.deleteEntity("a");
181
+ const all = await db.getRelations();
182
+ expect(all).toHaveLength(0);
183
+ });
184
+ });
185
+ // ── Vector Search ────────────────────────────────────────────
186
+ describe("Vector Search", () => {
187
+ beforeEach(async () => {
188
+ const entities = [
189
+ { id: "vec1", name: "Cat", embedding: [1.0, 0.0, 0.0] },
190
+ { id: "vec2", name: "Dog", embedding: [0.0, 1.0, 0.0] },
191
+ { id: "vec3", name: "Fish", embedding: [0.0, 0.0, 1.0] },
192
+ ];
193
+ for (const e of entities) {
194
+ await db.createEntity({
195
+ id: e.id, name: e.name, type: "Animal",
196
+ embedding: e.embedding, name_embedding: e.embedding,
197
+ metadata: {}, created_at: 4000,
198
+ });
199
+ }
200
+ });
201
+ it("should find closest entity by cosine similarity", async () => {
202
+ // Query vector closest to [1, 0, 0] → "Cat"
203
+ const results = await db.vectorSearchEntity([0.9, 0.1, 0.0], 1);
204
+ expect(results).toHaveLength(1);
205
+ expect(results[0][0]).toBe("vec1"); // id
206
+ expect(results[0][4]).toBeGreaterThan(0.9); // score
207
+ });
208
+ it("should return correct limit", async () => {
209
+ const results = await db.vectorSearchEntity([0.5, 0.5, 0.5], 2);
210
+ expect(results).toHaveLength(2);
211
+ });
212
+ it("should handle empty query vector gracefully (returns zero-score results)", async () => {
213
+ const results = await db.vectorSearchEntity([], 10);
214
+ // Empty vector produces cosine=0 for all entities, so all are returned with score 0
215
+ expect(results).toHaveLength(3);
216
+ expect(results[0][4]).toBe(0);
217
+ });
218
+ });
219
+ // ── Full-Text Search ─────────────────────────────────────────
220
+ describe("Full-Text Search", () => {
221
+ beforeEach(async () => {
222
+ await db.createEntity({
223
+ id: "fts1", name: "Alice Wonderland", type: "Person",
224
+ embedding: [], name_embedding: [], metadata: {}, created_at: 5000,
225
+ });
226
+ await db.createEntity({
227
+ id: "fts2", name: "Bob The Builder", type: "Person",
228
+ embedding: [], name_embedding: [], metadata: {}, created_at: 5001,
229
+ });
230
+ await db.addObservation({
231
+ id: "fobs1", entity_id: "fts1",
232
+ text: "Alice lives in a wonderland of dreams",
233
+ embedding: [], metadata: {}, created_at: 5002,
234
+ });
235
+ });
236
+ it("should find entity by name substring", async () => {
237
+ const results = await db.fullTextSearchEntity("alice");
238
+ expect(results).toHaveLength(1);
239
+ expect(results[0][0]).toBe("fts1");
240
+ });
241
+ it("should be case-insensitive", async () => {
242
+ const results = await db.fullTextSearchEntity("BOB");
243
+ expect(results).toHaveLength(1);
244
+ expect(results[0][0]).toBe("fts2");
245
+ });
246
+ it("should find observation by text substring", async () => {
247
+ const results = await db.fullTextSearchObservation("wonderland");
248
+ expect(results).toHaveLength(1);
249
+ expect(results[0][1]).toBe("fts1");
250
+ });
251
+ it("should return empty array for no match", async () => {
252
+ const results = await db.fullTextSearchEntity("nobody");
253
+ expect(results).toHaveLength(0);
254
+ });
255
+ });
256
+ // ── Export & Stats ───────────────────────────────────────────
257
+ describe("Export & Stats", () => {
258
+ it("should export empty database", async () => {
259
+ const exported = await db.exportRelations();
260
+ expect(exported).toHaveProperty("entity");
261
+ expect(exported).toHaveProperty("observation");
262
+ expect(exported).toHaveProperty("relationship");
263
+ expect(exported.entity).toHaveLength(0);
264
+ expect(exported.observation).toHaveLength(0);
265
+ expect(exported.relationship).toHaveLength(0);
266
+ });
267
+ it("should export correct counts", async () => {
268
+ await db.createEntity({
269
+ id: "exp1", name: "Export Test", type: "Test",
270
+ embedding: [], name_embedding: [], metadata: {}, created_at: 6000,
271
+ });
272
+ await db.addObservation({
273
+ id: "exp_obs1", entity_id: "exp1", text: "Obs",
274
+ embedding: [], metadata: {}, created_at: 6001,
275
+ });
276
+ await db.createRelation({
277
+ from_id: "exp1", to_id: "exp1", relation_type: "self",
278
+ strength: 1.0, metadata: {}, created_at: 6002,
279
+ });
280
+ const exported = await db.exportRelations();
281
+ expect(exported.entity).toHaveLength(1);
282
+ expect(exported.observation).toHaveLength(1);
283
+ expect(exported.relationship).toHaveLength(1);
284
+ });
285
+ it("should return correct stats", async () => {
286
+ await db.createEntity({
287
+ id: "stat1", name: "Stats", type: "T",
288
+ embedding: [], name_embedding: [], metadata: {}, created_at: 7000,
289
+ });
290
+ const stats = await db.getStats();
291
+ expect(stats.entities).toBe(1);
292
+ expect(stats.observations).toBe(0);
293
+ expect(stats.relationships).toBe(0);
294
+ });
295
+ });
296
+ // ── Lifecycle ────────────────────────────────────────────────
297
+ describe("Lifecycle", () => {
298
+ it("should initialize without error", async () => {
299
+ await expect(db.initialize()).resolves.not.toThrow();
300
+ });
301
+ it("should close without error", async () => {
302
+ await expect(db.close()).resolves.not.toThrow();
303
+ });
304
+ it("should backup and restore without error", async () => {
305
+ await expect(db.backup("/tmp/test_backup.cozo")).resolves.not.toThrow();
306
+ await expect(db.restore("/tmp/test_backup.cozo")).resolves.not.toThrow();
307
+ });
308
+ it("should run a query without error", async () => {
309
+ const result = await db.runQuery("SELECT 1");
310
+ expect(result).toEqual({ rows: [] });
311
+ });
312
+ });
313
+ });
@@ -7,8 +7,10 @@ exports.ExportImportService = void 0;
7
7
  const archiver_1 = __importDefault(require("archiver"));
8
8
  class ExportImportService {
9
9
  dbService;
10
- constructor(dbService) {
10
+ embeddingDim;
11
+ constructor(dbService, embeddingDim = 1024) {
11
12
  this.dbService = dbService;
13
+ this.embeddingDim = embeddingDim;
12
14
  }
13
15
  /**
14
16
  * Export memory to various formats
@@ -418,7 +420,7 @@ class ExportImportService {
418
420
  }
419
421
  async createEntityWithId(id, name, type, metadata) {
420
422
  const now = Date.now() * 1000;
421
- const zeroVec = new Array(1024).fill(0);
423
+ const zeroVec = new Array(this.embeddingDim).fill(0);
422
424
  // Escape strings properly for CozoDB
423
425
  const escapedName = name.replace(/"/g, '\\"');
424
426
  const escapedType = type.replace(/"/g, '\\"');
@@ -442,14 +444,16 @@ class ExportImportService {
442
444
  }
443
445
  async createObservationWithId(id, entityId, text, metadata) {
444
446
  const now = Date.now() * 1000;
445
- const zeroVec = new Array(1024).fill(0);
447
+ const zeroVec = new Array(this.embeddingDim).fill(0);
446
448
  const escapedText = text.replace(/"/g, '\\"').replace(/\n/g, '\\n');
447
449
  await this.dbService.run(`
448
- ?[id, entity_id, text, embedding, metadata, created_at] <- [[$id, $entity_id, $text, $embedding, $metadata, [${now}, true]]]
449
- :insert observation {id, entity_id, text, embedding, metadata, created_at}
450
+ ?[id, entity_id, session_id, task_id, text, embedding, metadata, created_at] <- [[$id, $entity_id, $session_id, $task_id, $text, $embedding, $metadata, [${now}, true]]]
451
+ :insert observation {id, entity_id, session_id, task_id, text, embedding, metadata, created_at}
450
452
  `, {
451
453
  id,
452
454
  entity_id: entityId,
455
+ session_id: "",
456
+ task_id: "",
453
457
  text: escapedText,
454
458
  embedding: zeroVec,
455
459
  metadata: metadata || {}