cozo-memory 1.0.3 → 1.0.5
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 +374 -40
- package/dist/download-model.js +3 -1
- package/dist/embedding-service.js +81 -12
- package/dist/export-import-service.js +472 -0
- package/dist/index.js +290 -15
- package/dist/inference-engine.js +9 -2
- package/dist/memory-service.js +88 -5
- package/dist/test-bugfixes.js +374 -0
- package/dist/test-delete-comprehensive.js +174 -0
- package/dist/test-export-import.js +152 -0
- package/dist/test-fixes-simple.js +50 -0
- package/dist/test-pdf-ingest.js +2 -0
- package/dist/test-qwen3-bilingual.js +2 -0
- package/package.json +5 -1
- package/dist/verify_transaction_tool.js +0 -46
package/dist/index.js
CHANGED
|
@@ -6,17 +6,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.MemoryServer = exports.USER_ENTITY_TYPE = exports.USER_ENTITY_NAME = exports.USER_ENTITY_ID = exports.DB_PATH = void 0;
|
|
8
8
|
const embedding_service_1 = require("./embedding-service");
|
|
9
|
+
const export_import_service_1 = require("./export-import-service");
|
|
9
10
|
const fastmcp_1 = require("fastmcp");
|
|
10
11
|
const cozo_node_1 = require("cozo-node");
|
|
11
12
|
const zod_1 = require("zod");
|
|
12
13
|
const uuid_1 = require("uuid");
|
|
13
14
|
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const fs_1 = __importDefault(require("fs"));
|
|
16
|
+
const pdf_mjs_1 = require("pdfjs-dist/legacy/build/pdf.mjs");
|
|
14
17
|
const hybrid_search_1 = require("./hybrid-search");
|
|
15
18
|
const inference_engine_1 = require("./inference-engine");
|
|
16
19
|
exports.DB_PATH = path_1.default.resolve(__dirname, "..", "memory_db.cozo");
|
|
17
20
|
const DB_ENGINE = process.env.DB_ENGINE || "sqlite"; // "sqlite" or "rocksdb"
|
|
18
|
-
const EMBEDDING_MODEL = "Xenova/bge-m3";
|
|
19
|
-
const EMBEDDING_DIM = 1024;
|
|
20
21
|
exports.USER_ENTITY_ID = "global_user_profile";
|
|
21
22
|
exports.USER_ENTITY_NAME = "The User";
|
|
22
23
|
exports.USER_ENTITY_TYPE = "User";
|
|
@@ -27,6 +28,47 @@ class MemoryServer {
|
|
|
27
28
|
hybridSearch;
|
|
28
29
|
inferenceEngine;
|
|
29
30
|
initPromise;
|
|
31
|
+
// Metrics tracking
|
|
32
|
+
metrics = {
|
|
33
|
+
operations: {
|
|
34
|
+
create_entity: 0,
|
|
35
|
+
update_entity: 0,
|
|
36
|
+
delete_entity: 0,
|
|
37
|
+
add_observation: 0,
|
|
38
|
+
create_relation: 0,
|
|
39
|
+
search: 0,
|
|
40
|
+
graph_operations: 0
|
|
41
|
+
},
|
|
42
|
+
errors: {
|
|
43
|
+
total: 0,
|
|
44
|
+
by_operation: {}
|
|
45
|
+
},
|
|
46
|
+
performance: {
|
|
47
|
+
last_operation_ms: 0,
|
|
48
|
+
avg_operation_ms: 0,
|
|
49
|
+
total_operations: 0
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
trackOperation(operation, startTime) {
|
|
53
|
+
const duration = Date.now() - startTime;
|
|
54
|
+
this.metrics.performance.last_operation_ms = duration;
|
|
55
|
+
this.metrics.performance.total_operations++;
|
|
56
|
+
// Calculate running average
|
|
57
|
+
const prevAvg = this.metrics.performance.avg_operation_ms;
|
|
58
|
+
const n = this.metrics.performance.total_operations;
|
|
59
|
+
this.metrics.performance.avg_operation_ms = (prevAvg * (n - 1) + duration) / n;
|
|
60
|
+
// Track operation count
|
|
61
|
+
if (operation in this.metrics.operations) {
|
|
62
|
+
this.metrics.operations[operation]++;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
trackError(operation) {
|
|
66
|
+
this.metrics.errors.total++;
|
|
67
|
+
if (!this.metrics.errors.by_operation[operation]) {
|
|
68
|
+
this.metrics.errors.by_operation[operation] = 0;
|
|
69
|
+
}
|
|
70
|
+
this.metrics.errors.by_operation[operation]++;
|
|
71
|
+
}
|
|
30
72
|
constructor(dbPath = exports.DB_PATH) {
|
|
31
73
|
const fullDbPath = DB_ENGINE === "sqlite" ? dbPath + ".db" : dbPath;
|
|
32
74
|
this.db = new cozo_node_1.CozoDb(DB_ENGINE, fullDbPath);
|
|
@@ -442,6 +484,9 @@ class MemoryServer {
|
|
|
442
484
|
async setupSchema() {
|
|
443
485
|
try {
|
|
444
486
|
console.error("[Schema] Initializing schema...");
|
|
487
|
+
// Get embedding dimensions from service
|
|
488
|
+
const EMBEDDING_DIM = this.embeddingService.getDimensions();
|
|
489
|
+
console.error(`[Schema] Using embedding dimensions: ${EMBEDDING_DIM}`);
|
|
445
490
|
const existingRelations = await this.db.run("::relations");
|
|
446
491
|
const relations = existingRelations.rows.map((r) => r[0]);
|
|
447
492
|
// Entity Table
|
|
@@ -934,6 +979,7 @@ class MemoryServer {
|
|
|
934
979
|
});
|
|
935
980
|
}
|
|
936
981
|
async createEntity(args) {
|
|
982
|
+
const startTime = Date.now();
|
|
937
983
|
try {
|
|
938
984
|
if (!args.name || args.name.trim() === "") {
|
|
939
985
|
return { error: "Entity name must not be empty" };
|
|
@@ -957,9 +1003,12 @@ class MemoryServer {
|
|
|
957
1003
|
}
|
|
958
1004
|
}
|
|
959
1005
|
const id = (0, uuid_1.v4)();
|
|
960
|
-
|
|
1006
|
+
const result = await this.createEntityWithId(id, args.name, args.type, args.metadata);
|
|
1007
|
+
this.trackOperation('create_entity', startTime);
|
|
1008
|
+
return result;
|
|
961
1009
|
}
|
|
962
1010
|
catch (error) {
|
|
1011
|
+
this.trackError('create_entity');
|
|
963
1012
|
console.error("Error in create_entity:", error);
|
|
964
1013
|
if (error.display) {
|
|
965
1014
|
console.error("CozoDB Error Details:", error.display);
|
|
@@ -1168,6 +1217,28 @@ class MemoryServer {
|
|
|
1168
1217
|
if (toEntity.rows.length === 0) {
|
|
1169
1218
|
return { error: `Target entity with ID '${args.to_id}' not found` };
|
|
1170
1219
|
}
|
|
1220
|
+
// FIX: Check for duplicate relations
|
|
1221
|
+
try {
|
|
1222
|
+
const existingRel = await this.db.run('?[from_id, to_id, relation_type, strength, metadata] := *relationship{from_id, to_id, relation_type, strength, metadata, @ "NOW"}, from_id = $from, to_id = $to, relation_type = $type', { from: args.from_id, to: args.to_id, type: args.relation_type });
|
|
1223
|
+
if (existingRel.rows.length > 0) {
|
|
1224
|
+
// Update existing relation instead of creating duplicate
|
|
1225
|
+
const now = Date.now() * 1000;
|
|
1226
|
+
await this.db.run(`
|
|
1227
|
+
?[from_id, to_id, relation_type, created_at, strength, metadata] <- [[$from_id, $to_id, $relation_type, [${now}, true], $strength, $metadata]]
|
|
1228
|
+
:put relationship {from_id, to_id, relation_type, created_at => strength, metadata}
|
|
1229
|
+
`, {
|
|
1230
|
+
from_id: args.from_id,
|
|
1231
|
+
to_id: args.to_id,
|
|
1232
|
+
relation_type: args.relation_type,
|
|
1233
|
+
strength: args.strength ?? 1.0,
|
|
1234
|
+
metadata: args.metadata || {}
|
|
1235
|
+
});
|
|
1236
|
+
return { status: "Relationship updated (was duplicate)" };
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
catch (e) {
|
|
1240
|
+
console.error('[Relation] Duplicate check failed, proceeding with insert:', e);
|
|
1241
|
+
}
|
|
1171
1242
|
const now = Date.now() * 1000;
|
|
1172
1243
|
await this.db.run(`?[from_id, to_id, relation_type, created_at, strength, metadata] <- [[$from_id, $to_id, $relation_type, [${now}, true], $strength, $metadata]] :insert relationship {from_id, to_id, relation_type, created_at => strength, metadata}`, {
|
|
1173
1244
|
from_id: args.from_id,
|
|
@@ -1596,7 +1667,39 @@ ids[id] <- $ids
|
|
|
1596
1667
|
async ingestFile(args) {
|
|
1597
1668
|
await this.initPromise;
|
|
1598
1669
|
try {
|
|
1599
|
-
|
|
1670
|
+
// Check that either file_path or content is provided
|
|
1671
|
+
if (!args.file_path && !args.content) {
|
|
1672
|
+
return { error: "Either file_path or content must be provided" };
|
|
1673
|
+
}
|
|
1674
|
+
// Read content from file if file_path is provided
|
|
1675
|
+
let content;
|
|
1676
|
+
if (args.file_path) {
|
|
1677
|
+
try {
|
|
1678
|
+
if (args.format === "pdf") {
|
|
1679
|
+
// Read PDF file and extract text using pdfjs-dist
|
|
1680
|
+
const data = new Uint8Array(fs_1.default.readFileSync(args.file_path));
|
|
1681
|
+
const loadingTask = (0, pdf_mjs_1.getDocument)({ data });
|
|
1682
|
+
const pdf = await loadingTask.promise;
|
|
1683
|
+
const numPages = pdf.numPages;
|
|
1684
|
+
const pageTextPromises = Array.from({ length: numPages }, async (_, i) => {
|
|
1685
|
+
const page = await pdf.getPage(i + 1);
|
|
1686
|
+
const textContent = await page.getTextContent();
|
|
1687
|
+
return textContent.items.map((item) => item.str).join(' ');
|
|
1688
|
+
});
|
|
1689
|
+
const pageTexts = await Promise.all(pageTextPromises);
|
|
1690
|
+
content = pageTexts.join('\n').trim();
|
|
1691
|
+
}
|
|
1692
|
+
else {
|
|
1693
|
+
content = fs_1.default.readFileSync(args.file_path, 'utf-8').trim();
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
catch (error) {
|
|
1697
|
+
return { error: `Failed to read file: ${error.message}` };
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
else {
|
|
1701
|
+
content = (args.content ?? "").trim();
|
|
1702
|
+
}
|
|
1600
1703
|
if (!content)
|
|
1601
1704
|
return { error: "Content must not be empty" };
|
|
1602
1705
|
let entityId = undefined;
|
|
@@ -1633,7 +1736,7 @@ ids[id] <- $ids
|
|
|
1633
1736
|
const deduplicate = args.deduplicate ?? true;
|
|
1634
1737
|
const chunking = args.chunking ?? "none";
|
|
1635
1738
|
const observations = [];
|
|
1636
|
-
if (args.format === "markdown") {
|
|
1739
|
+
if (args.format === "markdown" || args.format === "pdf") {
|
|
1637
1740
|
if (chunking === "paragraphs") {
|
|
1638
1741
|
const parts = content
|
|
1639
1742
|
.split(/\r?\n\s*\r?\n+/g)
|
|
@@ -1707,23 +1810,54 @@ ids[id] <- $ids
|
|
|
1707
1810
|
}
|
|
1708
1811
|
}
|
|
1709
1812
|
async deleteEntity(args) {
|
|
1813
|
+
const startTime = Date.now();
|
|
1710
1814
|
try {
|
|
1815
|
+
console.error(`[Delete] Starting deletion for entity: ${args.entity_id}`);
|
|
1711
1816
|
// 1. Check if entity exists
|
|
1712
1817
|
const entityRes = await this.db.run('?[name] := *entity{id: $id, name, @ "NOW"}', { id: args.entity_id });
|
|
1713
1818
|
if (entityRes.rows.length === 0) {
|
|
1819
|
+
console.error(`[Delete] Entity not found: ${args.entity_id}`);
|
|
1714
1820
|
return { error: `Entity with ID '${args.entity_id}' not found` };
|
|
1715
1821
|
}
|
|
1716
|
-
|
|
1822
|
+
console.error(`[Delete] Entity found: ${entityRes.rows[0][0]}`);
|
|
1823
|
+
// 2. Count related data before deletion
|
|
1824
|
+
const obsCount = await this.db.run('?[count(id)] := *observation{id, entity_id, @ "NOW"}, entity_id = $id', { id: args.entity_id });
|
|
1825
|
+
const relOutCount = await this.db.run('?[count(from_id)] := *relationship{from_id, to_id, @ "NOW"}, from_id = $id', { id: args.entity_id });
|
|
1826
|
+
const relInCount = await this.db.run('?[count(to_id)] := *relationship{from_id, to_id, @ "NOW"}, to_id = $id', { id: args.entity_id });
|
|
1827
|
+
console.error(`[Delete] Related data: ${obsCount.rows[0][0]} observations, ${relOutCount.rows[0][0]} outgoing relations, ${relInCount.rows[0][0]} incoming relations`);
|
|
1828
|
+
// 3. Delete all related data in a transaction (block)
|
|
1829
|
+
console.error(`[Delete] Executing deletion transaction...`);
|
|
1717
1830
|
await this.db.run(`
|
|
1718
1831
|
{ ?[id, created_at] := *observation{id, entity_id, created_at}, entity_id = $target_id :rm observation {id, created_at} }
|
|
1719
1832
|
{ ?[from_id, to_id, relation_type, created_at] := *relationship{from_id, to_id, relation_type, created_at}, from_id = $target_id :rm relationship {from_id, to_id, relation_type, created_at} }
|
|
1720
1833
|
{ ?[from_id, to_id, relation_type, created_at] := *relationship{from_id, to_id, relation_type, created_at}, to_id = $target_id :rm relationship {from_id, to_id, relation_type, created_at} }
|
|
1721
1834
|
{ ?[id, created_at] := *entity{id, created_at}, id = $target_id :rm entity {id, created_at} }
|
|
1722
1835
|
`, { target_id: args.entity_id });
|
|
1723
|
-
|
|
1836
|
+
// 4. Verify deletion
|
|
1837
|
+
const verifyEntity = await this.db.run('?[id] := *entity{id, @ "NOW"}, id = $id', { id: args.entity_id });
|
|
1838
|
+
const verifyObs = await this.db.run('?[count(id)] := *observation{id, entity_id, @ "NOW"}, entity_id = $id', { id: args.entity_id });
|
|
1839
|
+
const verifyRelOut = await this.db.run('?[count(from_id)] := *relationship{from_id, @ "NOW"}, from_id = $id', { id: args.entity_id });
|
|
1840
|
+
const verifyRelIn = await this.db.run('?[count(to_id)] := *relationship{to_id, @ "NOW"}, to_id = $id', { id: args.entity_id });
|
|
1841
|
+
console.error(`[Delete] Verification: entity exists=${verifyEntity.rows.length > 0}, observations=${verifyObs.rows[0][0]}, outgoing_relations=${verifyRelOut.rows[0][0]}, incoming_relations=${verifyRelIn.rows[0][0]}`);
|
|
1842
|
+
if (verifyEntity.rows.length > 0) {
|
|
1843
|
+
console.error(`[Delete] WARNING: Entity still visible after deletion!`);
|
|
1844
|
+
}
|
|
1845
|
+
else {
|
|
1846
|
+
console.error(`[Delete] ✓ Entity successfully deleted`);
|
|
1847
|
+
}
|
|
1848
|
+
this.trackOperation('delete_entity', startTime);
|
|
1849
|
+
return {
|
|
1850
|
+
status: "Entity and all related data deleted",
|
|
1851
|
+
deleted: {
|
|
1852
|
+
observations: obsCount.rows[0][0],
|
|
1853
|
+
outgoing_relations: relOutCount.rows[0][0],
|
|
1854
|
+
incoming_relations: relInCount.rows[0][0]
|
|
1855
|
+
}
|
|
1856
|
+
};
|
|
1724
1857
|
}
|
|
1725
1858
|
catch (error) {
|
|
1726
|
-
console.error("Error during deletion:", error);
|
|
1859
|
+
console.error("[Delete] Error during deletion:", error);
|
|
1860
|
+
this.trackError('delete_entity');
|
|
1727
1861
|
return { error: "Deletion failed", message: error.message };
|
|
1728
1862
|
}
|
|
1729
1863
|
}
|
|
@@ -1865,6 +1999,26 @@ ids[id] <- $ids
|
|
|
1865
1999
|
to_id = existingId;
|
|
1866
2000
|
}
|
|
1867
2001
|
}
|
|
2002
|
+
// FIX: Validate that both entities exist before creating relation
|
|
2003
|
+
try {
|
|
2004
|
+
const [fromExists, toExists] = await Promise.all([
|
|
2005
|
+
this.db.run('?[id, name] := *entity{id, name, @ "NOW"}, id = $id', { id: from_id }),
|
|
2006
|
+
this.db.run('?[id, name] := *entity{id, name, @ "NOW"}, id = $id', { id: to_id })
|
|
2007
|
+
]);
|
|
2008
|
+
if (fromExists.rows.length === 0) {
|
|
2009
|
+
return { error: `Transaction failed: Source entity '${from_id}' not found in operation ${i}` };
|
|
2010
|
+
}
|
|
2011
|
+
if (toExists.rows.length === 0) {
|
|
2012
|
+
return { error: `Transaction failed: Target entity '${to_id}' not found in operation ${i}` };
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
catch (e) {
|
|
2016
|
+
return { error: `Transaction failed: Entity validation error in operation ${i}: ${e.message}` };
|
|
2017
|
+
}
|
|
2018
|
+
// FIX: Check for self-reference
|
|
2019
|
+
if (from_id === to_id) {
|
|
2020
|
+
return { error: `Transaction failed: Self-references not allowed in operation ${i}` };
|
|
2021
|
+
}
|
|
1868
2022
|
const now = Date.now() * 1000;
|
|
1869
2023
|
allParams[`rel_from${suffix}`] = from_id;
|
|
1870
2024
|
allParams[`rel_to${suffix}`] = to_id;
|
|
@@ -1991,6 +2145,22 @@ ids[id] <- $ids
|
|
|
1991
2145
|
await this.initPromise;
|
|
1992
2146
|
return this.hybridSearch.graphRag(args);
|
|
1993
2147
|
}
|
|
2148
|
+
async exportMemory(args) {
|
|
2149
|
+
await this.initPromise;
|
|
2150
|
+
const dbService = { run: (query, params) => this.db.run(query, params) };
|
|
2151
|
+
const exportService = new export_import_service_1.ExportImportService(dbService);
|
|
2152
|
+
return exportService.exportMemory(args);
|
|
2153
|
+
}
|
|
2154
|
+
async importMemory(args) {
|
|
2155
|
+
await this.initPromise;
|
|
2156
|
+
const dbService = { run: (query, params) => this.db.run(query, params) };
|
|
2157
|
+
const exportService = new export_import_service_1.ExportImportService(dbService);
|
|
2158
|
+
return exportService.importMemory(args.data, {
|
|
2159
|
+
sourceFormat: args.sourceFormat,
|
|
2160
|
+
mergeStrategy: args.mergeStrategy,
|
|
2161
|
+
defaultEntityType: args.defaultEntityType
|
|
2162
|
+
});
|
|
2163
|
+
}
|
|
1994
2164
|
registerTools() {
|
|
1995
2165
|
const MetadataSchema = zod_1.z.record(zod_1.z.string(), zod_1.z.any());
|
|
1996
2166
|
const MutateMemorySchema = zod_1.z.discriminatedUnion("action", [
|
|
@@ -2095,9 +2265,10 @@ ids[id] <- $ids
|
|
|
2095
2265
|
entity_id: zod_1.z.string().optional().describe("ID of the target entity"),
|
|
2096
2266
|
entity_name: zod_1.z.string().optional().describe("Name of the target entity (will be created if not exists)"),
|
|
2097
2267
|
entity_type: zod_1.z.string().optional().default("Document").describe("Type of the target entity (only when creating)"),
|
|
2098
|
-
format: zod_1.z.enum(["markdown", "json"]).describe("Input format"),
|
|
2268
|
+
format: zod_1.z.enum(["markdown", "json", "pdf"]).describe("Input format"),
|
|
2099
2269
|
chunking: zod_1.z.enum(["none", "paragraphs"]).optional().default("none").describe("Chunking for Markdown"),
|
|
2100
|
-
|
|
2270
|
+
file_path: zod_1.z.string().optional().describe("Path to file on disk (alternative to content parameter)"),
|
|
2271
|
+
content: zod_1.z.string().optional().describe("File content (or LLM summary) - required if file_path not provided"),
|
|
2101
2272
|
metadata: MetadataSchema.optional().describe("Metadata for entity creation"),
|
|
2102
2273
|
observation_metadata: MetadataSchema.optional().describe("Metadata applied to all observations"),
|
|
2103
2274
|
deduplicate: zod_1.z.boolean().optional().default(true).describe("Skip exact duplicates"),
|
|
@@ -2105,6 +2276,9 @@ ids[id] <- $ids
|
|
|
2105
2276
|
}).refine((v) => Boolean(v.entity_id) || Boolean(v.entity_name), {
|
|
2106
2277
|
message: "entity_id or entity_name is required for ingest_file",
|
|
2107
2278
|
path: ["entity_id"],
|
|
2279
|
+
}).refine((v) => Boolean(v.file_path) || Boolean(v.content), {
|
|
2280
|
+
message: "file_path or content is required for ingest_file",
|
|
2281
|
+
path: ["file_path"],
|
|
2108
2282
|
}),
|
|
2109
2283
|
]);
|
|
2110
2284
|
const MutateMemoryParameters = zod_1.z.object({
|
|
@@ -2119,9 +2293,10 @@ ids[id] <- $ids
|
|
|
2119
2293
|
entity_type: zod_1.z.string().optional().describe("Only when entity_name is used and entity is created new"),
|
|
2120
2294
|
text: zod_1.z.string().optional().describe("For add_observation (required)"),
|
|
2121
2295
|
datalog: zod_1.z.string().optional().describe("For add_inference_rule (required)"),
|
|
2122
|
-
format: zod_1.z.enum(["markdown", "json"]).optional().describe("For ingest_file (required)"),
|
|
2296
|
+
format: zod_1.z.enum(["markdown", "json", "pdf"]).optional().describe("For ingest_file (required)"),
|
|
2123
2297
|
chunking: zod_1.z.enum(["none", "paragraphs"]).optional().describe("Optional for ingest_file (for markdown)"),
|
|
2124
|
-
|
|
2298
|
+
file_path: zod_1.z.string().optional().describe("For ingest_file - path to file on disk (alternative to content)"),
|
|
2299
|
+
content: zod_1.z.string().optional().describe("For ingest_file - file content (required if file_path not provided)"),
|
|
2125
2300
|
observation_metadata: MetadataSchema.optional().describe("Optional for ingest_file"),
|
|
2126
2301
|
deduplicate: zod_1.z.boolean().optional().describe("Optional for ingest_file and add_observation"),
|
|
2127
2302
|
max_observations: zod_1.z.number().optional().describe("Optional for ingest_file"),
|
|
@@ -2881,6 +3056,23 @@ Supported actions:
|
|
|
2881
3056
|
});
|
|
2882
3057
|
const ManageSystemSchema = zod_1.z.discriminatedUnion("action", [
|
|
2883
3058
|
zod_1.z.object({ action: zod_1.z.literal("health") }),
|
|
3059
|
+
zod_1.z.object({ action: zod_1.z.literal("metrics") }),
|
|
3060
|
+
zod_1.z.object({
|
|
3061
|
+
action: zod_1.z.literal("export_memory"),
|
|
3062
|
+
format: zod_1.z.enum(["json", "markdown", "obsidian"]).describe("Export format"),
|
|
3063
|
+
includeMetadata: zod_1.z.boolean().optional().describe("Include metadata in export"),
|
|
3064
|
+
includeRelationships: zod_1.z.boolean().optional().describe("Include relationships in export"),
|
|
3065
|
+
includeObservations: zod_1.z.boolean().optional().describe("Include observations in export"),
|
|
3066
|
+
entityTypes: zod_1.z.array(zod_1.z.string()).optional().describe("Filter by entity types"),
|
|
3067
|
+
since: zod_1.z.number().optional().describe("Unix timestamp (ms) - only export data created after this time"),
|
|
3068
|
+
}),
|
|
3069
|
+
zod_1.z.object({
|
|
3070
|
+
action: zod_1.z.literal("import_memory"),
|
|
3071
|
+
data: zod_1.z.union([zod_1.z.string(), zod_1.z.any()]).describe("Import data (JSON string or object)"),
|
|
3072
|
+
sourceFormat: zod_1.z.enum(["mem0", "memgpt", "markdown", "cozo"]).describe("Source format"),
|
|
3073
|
+
mergeStrategy: zod_1.z.enum(["skip", "overwrite", "merge"]).optional().default("skip").describe("How to handle existing entities"),
|
|
3074
|
+
defaultEntityType: zod_1.z.string().optional().default("Note").describe("Default type for imported entities"),
|
|
3075
|
+
}),
|
|
2884
3076
|
zod_1.z.object({
|
|
2885
3077
|
action: zod_1.z.literal("snapshot_create"),
|
|
2886
3078
|
metadata: MetadataSchema.optional().describe("Additional metadata for the snapshot"),
|
|
@@ -2911,8 +3103,18 @@ Supported actions:
|
|
|
2911
3103
|
]);
|
|
2912
3104
|
const ManageSystemParameters = zod_1.z.object({
|
|
2913
3105
|
action: zod_1.z
|
|
2914
|
-
.enum(["health", "snapshot_create", "snapshot_list", "snapshot_diff", "cleanup", "reflect", "clear_memory"])
|
|
3106
|
+
.enum(["health", "metrics", "export_memory", "import_memory", "snapshot_create", "snapshot_list", "snapshot_diff", "cleanup", "reflect", "clear_memory"])
|
|
2915
3107
|
.describe("Action (determines which fields are required)"),
|
|
3108
|
+
format: zod_1.z.enum(["json", "markdown", "obsidian"]).optional().describe("Export format (for export_memory)"),
|
|
3109
|
+
includeMetadata: zod_1.z.boolean().optional().describe("Include metadata (for export_memory)"),
|
|
3110
|
+
includeRelationships: zod_1.z.boolean().optional().describe("Include relationships (for export_memory)"),
|
|
3111
|
+
includeObservations: zod_1.z.boolean().optional().describe("Include observations (for export_memory)"),
|
|
3112
|
+
entityTypes: zod_1.z.array(zod_1.z.string()).optional().describe("Filter by entity types (for export_memory)"),
|
|
3113
|
+
since: zod_1.z.number().optional().describe("Unix timestamp in ms (for export_memory)"),
|
|
3114
|
+
data: zod_1.z.union([zod_1.z.string(), zod_1.z.any()]).optional().describe("Import data (for import_memory)"),
|
|
3115
|
+
sourceFormat: zod_1.z.enum(["mem0", "memgpt", "markdown", "cozo"]).optional().describe("Source format (for import_memory)"),
|
|
3116
|
+
mergeStrategy: zod_1.z.enum(["skip", "overwrite", "merge"]).optional().describe("Merge strategy (for import_memory)"),
|
|
3117
|
+
defaultEntityType: zod_1.z.string().optional().describe("Default entity type (for import_memory)"),
|
|
2916
3118
|
snapshot_id_a: zod_1.z.string().optional().describe("Required for snapshot_diff"),
|
|
2917
3119
|
snapshot_id_b: zod_1.z.string().optional().describe("Required for snapshot_diff"),
|
|
2918
3120
|
metadata: MetadataSchema.optional().describe("Optional for snapshot_create"),
|
|
@@ -2927,7 +3129,17 @@ Supported actions:
|
|
|
2927
3129
|
name: "manage_system",
|
|
2928
3130
|
description: `System maintenance and memory management. Select operation via 'action'.
|
|
2929
3131
|
Supported actions:
|
|
2930
|
-
- 'health': Status check. Returns DB counts
|
|
3132
|
+
- 'health': Status check. Returns DB counts, embedding cache statistics, and performance metrics.
|
|
3133
|
+
- 'metrics': Detailed metrics. Returns operation counts, error statistics, and performance data.
|
|
3134
|
+
- 'export_memory': Export memory to various formats. Params: { format: 'json'|'markdown'|'obsidian', includeMetadata?: boolean, includeRelationships?: boolean, includeObservations?: boolean, entityTypes?: string[], since?: number }.
|
|
3135
|
+
* format='json': Native Cozo format (re-importable)
|
|
3136
|
+
* format='markdown': Human-readable markdown document
|
|
3137
|
+
* format='obsidian': ZIP archive with wiki-links, ready for Obsidian vault
|
|
3138
|
+
- 'import_memory': Import memory from external sources. Params: { data: string|object, sourceFormat: 'mem0'|'memgpt'|'markdown'|'cozo', mergeStrategy?: 'skip'|'overwrite'|'merge', defaultEntityType?: string }.
|
|
3139
|
+
* sourceFormat='mem0': Import from Mem0 format (user_id becomes entity)
|
|
3140
|
+
* sourceFormat='memgpt': Import from MemGPT archival/recall memory
|
|
3141
|
+
* sourceFormat='markdown': Parse markdown sections as entities
|
|
3142
|
+
* sourceFormat='cozo': Import from native Cozo JSON export
|
|
2931
3143
|
- 'snapshot_create': Creates a backup point. Params: { metadata?: object }.
|
|
2932
3144
|
- 'snapshot_list': Lists all available snapshots.
|
|
2933
3145
|
- 'snapshot_diff': Compares two snapshots. Params: { snapshot_id_a: string, snapshot_id_b: string }.
|
|
@@ -2957,7 +3169,16 @@ Supported actions:
|
|
|
2957
3169
|
observations: obsResult.rows.length,
|
|
2958
3170
|
relations: relResult.rows.length,
|
|
2959
3171
|
},
|
|
2960
|
-
performance: {
|
|
3172
|
+
performance: {
|
|
3173
|
+
embedding_cache: this.embeddingService.getCacheStats(),
|
|
3174
|
+
last_operation_ms: this.metrics.performance.last_operation_ms,
|
|
3175
|
+
avg_operation_ms: this.metrics.performance.avg_operation_ms,
|
|
3176
|
+
total_operations: this.metrics.performance.total_operations
|
|
3177
|
+
},
|
|
3178
|
+
metrics: {
|
|
3179
|
+
operations: this.metrics.operations,
|
|
3180
|
+
errors: this.metrics.errors
|
|
3181
|
+
},
|
|
2961
3182
|
timestamp: new Date().toISOString(),
|
|
2962
3183
|
});
|
|
2963
3184
|
}
|
|
@@ -2965,6 +3186,60 @@ Supported actions:
|
|
|
2965
3186
|
return JSON.stringify({ status: "error", error: error.message, timestamp: new Date().toISOString() });
|
|
2966
3187
|
}
|
|
2967
3188
|
}
|
|
3189
|
+
if (input.action === "metrics") {
|
|
3190
|
+
try {
|
|
3191
|
+
return JSON.stringify({
|
|
3192
|
+
operations: this.metrics.operations,
|
|
3193
|
+
errors: this.metrics.errors,
|
|
3194
|
+
performance: this.metrics.performance,
|
|
3195
|
+
embedding_cache: this.embeddingService.getCacheStats(),
|
|
3196
|
+
timestamp: new Date().toISOString()
|
|
3197
|
+
});
|
|
3198
|
+
}
|
|
3199
|
+
catch (error) {
|
|
3200
|
+
return JSON.stringify({ error: "Failed to retrieve metrics", message: error.message });
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
if (input.action === "export_memory") {
|
|
3204
|
+
try {
|
|
3205
|
+
const result = await this.exportMemory({
|
|
3206
|
+
format: input.format,
|
|
3207
|
+
includeMetadata: input.includeMetadata,
|
|
3208
|
+
includeRelationships: input.includeRelationships,
|
|
3209
|
+
includeObservations: input.includeObservations,
|
|
3210
|
+
entityTypes: input.entityTypes,
|
|
3211
|
+
since: input.since
|
|
3212
|
+
});
|
|
3213
|
+
if (result.format === 'obsidian' && result.zipBuffer) {
|
|
3214
|
+
// For Obsidian ZIP, return base64 encoded buffer
|
|
3215
|
+
return JSON.stringify({
|
|
3216
|
+
format: 'obsidian',
|
|
3217
|
+
data: result.zipBuffer.toString('base64'),
|
|
3218
|
+
stats: result.stats,
|
|
3219
|
+
encoding: 'base64',
|
|
3220
|
+
filename: `memory_export_${Date.now()}.zip`
|
|
3221
|
+
});
|
|
3222
|
+
}
|
|
3223
|
+
return JSON.stringify(result);
|
|
3224
|
+
}
|
|
3225
|
+
catch (error) {
|
|
3226
|
+
return JSON.stringify({ error: "Export failed", message: error.message });
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
if (input.action === "import_memory") {
|
|
3230
|
+
try {
|
|
3231
|
+
const result = await this.importMemory({
|
|
3232
|
+
data: input.data,
|
|
3233
|
+
sourceFormat: input.sourceFormat,
|
|
3234
|
+
mergeStrategy: input.mergeStrategy,
|
|
3235
|
+
defaultEntityType: input.defaultEntityType
|
|
3236
|
+
});
|
|
3237
|
+
return JSON.stringify(result);
|
|
3238
|
+
}
|
|
3239
|
+
catch (error) {
|
|
3240
|
+
return JSON.stringify({ error: "Import failed", message: error.message });
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
2968
3243
|
if (input.action === "snapshot_create") {
|
|
2969
3244
|
try {
|
|
2970
3245
|
// Optimization: Sequential execution and count aggregation instead of full fetch
|
package/dist/inference-engine.js
CHANGED
|
@@ -324,9 +324,16 @@ class InferenceEngine {
|
|
|
324
324
|
for (const r of res.rows) {
|
|
325
325
|
if (!r || r.length < 5)
|
|
326
326
|
continue;
|
|
327
|
+
const fromId = String(r[0]);
|
|
328
|
+
const toId = String(r[1]);
|
|
329
|
+
// FIX: Filter out self-references
|
|
330
|
+
if (fromId === toId) {
|
|
331
|
+
console.error(`[Inference] Skipping self-reference in custom rule ${ruleName}: ${fromId} -> ${toId}`);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
327
334
|
results.push({
|
|
328
|
-
from_id:
|
|
329
|
-
to_id:
|
|
335
|
+
from_id: fromId,
|
|
336
|
+
to_id: toId,
|
|
330
337
|
relation_type: String(r[2]),
|
|
331
338
|
confidence: Number(r[3]),
|
|
332
339
|
reason: String(r[4]),
|
package/dist/memory-service.js
CHANGED
|
@@ -1,7 +1,42 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.MemoryService = void 0;
|
|
4
37
|
const uuid_1 = require("uuid");
|
|
38
|
+
const pdf_mjs_1 = require("pdfjs-dist/legacy/build/pdf.mjs");
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
5
40
|
class MemoryService {
|
|
6
41
|
db;
|
|
7
42
|
embeddings;
|
|
@@ -169,7 +204,7 @@ class MemoryService {
|
|
|
169
204
|
console.error('[MemoryService] Snapshot created:', snapshotId, stats);
|
|
170
205
|
return snapshotId;
|
|
171
206
|
}
|
|
172
|
-
async ingestFile(content, format, entityName, entityType = 'Document', chunking = 'paragraphs') {
|
|
207
|
+
async ingestFile(content, format, entityName, entityType = 'Document', chunking = 'paragraphs', filePath) {
|
|
173
208
|
const searchResults = await this.search(entityName, 1);
|
|
174
209
|
let entity;
|
|
175
210
|
if (searchResults.length > 0 && searchResults[0].entity.name.toLowerCase() === entityName.toLowerCase()) {
|
|
@@ -179,16 +214,64 @@ class MemoryService {
|
|
|
179
214
|
entity = await this.createEntity(entityName, entityType, { format: format });
|
|
180
215
|
}
|
|
181
216
|
let chunks = [];
|
|
182
|
-
if (format === '
|
|
183
|
-
|
|
217
|
+
if (format === 'pdf') {
|
|
218
|
+
try {
|
|
219
|
+
let data;
|
|
220
|
+
// If filePath is provided, read from file
|
|
221
|
+
if (filePath) {
|
|
222
|
+
data = new Uint8Array(fs.readFileSync(filePath));
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
// Otherwise, assume content is base64
|
|
226
|
+
const buffer = Buffer.from(content, 'base64');
|
|
227
|
+
data = new Uint8Array(buffer);
|
|
228
|
+
}
|
|
229
|
+
const loadingTask = (0, pdf_mjs_1.getDocument)({ data });
|
|
230
|
+
const pdf = await loadingTask.promise;
|
|
231
|
+
const numPages = pdf.numPages;
|
|
232
|
+
const pageTextPromises = Array.from({ length: numPages }, async (_, i) => {
|
|
233
|
+
const page = await pdf.getPage(i + 1);
|
|
234
|
+
const textContent = await page.getTextContent();
|
|
235
|
+
return textContent.items.map((item) => item.str).join(' ');
|
|
236
|
+
});
|
|
237
|
+
const pageTexts = await Promise.all(pageTextPromises);
|
|
238
|
+
const text = pageTexts.join('\n');
|
|
239
|
+
if (chunking === 'paragraphs') {
|
|
240
|
+
chunks = text.split(/\n\s*\n/).filter((c) => c.trim().length > 0);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
chunks = [text];
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
catch (e) {
|
|
247
|
+
console.error('[MemoryService] PDF parsing error:', e);
|
|
248
|
+
throw new Error(`Failed to parse PDF: ${e instanceof Error ? e.message : String(e)}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
else if (format === 'markdown') {
|
|
252
|
+
// For markdown, also support file path
|
|
253
|
+
let textContent = content;
|
|
254
|
+
if (filePath) {
|
|
255
|
+
textContent = fs.readFileSync(filePath, 'utf-8');
|
|
256
|
+
}
|
|
257
|
+
if (chunking === 'paragraphs') {
|
|
258
|
+
chunks = textContent.split(/\n\s*\n/).filter((c) => c.trim().length > 0);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
chunks = [textContent];
|
|
262
|
+
}
|
|
184
263
|
}
|
|
185
264
|
else if (format === 'json') {
|
|
265
|
+
let textContent = content;
|
|
266
|
+
if (filePath) {
|
|
267
|
+
textContent = fs.readFileSync(filePath, 'utf-8');
|
|
268
|
+
}
|
|
186
269
|
try {
|
|
187
|
-
const data = JSON.parse(
|
|
270
|
+
const data = JSON.parse(textContent);
|
|
188
271
|
chunks = [JSON.stringify(data, null, 2)];
|
|
189
272
|
}
|
|
190
273
|
catch (e) {
|
|
191
|
-
chunks = [
|
|
274
|
+
chunks = [textContent];
|
|
192
275
|
}
|
|
193
276
|
}
|
|
194
277
|
else {
|