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 +39 -32
- package/dist/db-service.test.js +313 -0
- package/dist/export-import-service.js +9 -5
- package/dist/index.js +825 -10
- package/dist/logger.test.js +75 -0
- package/dist/memory-service.test.js +222 -0
- package/dist/timestamp-utils.test.js +68 -0
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -3,8 +3,12 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/cozo-memory)
|
|
4
4
|
[](https://nodejs.org)
|
|
5
5
|
[](LICENSE)
|
|
6
|
+
[](https://lobehub.com/mcp/tobs-code-cozo-memory)
|
|
6
7
|
|
|
7
|
-
**
|
|
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;
|
|
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
|
-
-
|
|
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
|
-
**
|
|
358
|
-
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 || {}
|