cozo-memory 1.0.4 → 1.0.6
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 +211 -3
- package/dist/api_bridge.js +6 -4
- package/dist/cli-commands.js +204 -0
- package/dist/cli.js +410 -0
- package/dist/download-model.js +3 -1
- package/dist/embedding-service.js +81 -12
- package/dist/hybrid-search.js +8 -3
- package/dist/index.js +62 -10
- package/dist/memory-service.js +88 -5
- package/dist/temporal-normalizer.js +2 -0
- package/dist/test-hybrid-debug.js +52 -0
- package/dist/test-mcp-search.js +47 -0
- package/dist/test-pdf-ingest.js +2 -0
- package/dist/test-qwen3-bilingual.js +2 -0
- package/dist/test-search-simple.js +27 -0
- package/dist/timestamp-utils.js +44 -0
- package/dist/tui-blessed.js +789 -0
- package/dist/tui-launcher.js +61 -0
- package/dist/tui.js +131 -0
- package/dist/tui.py +481 -0
- package/package.json +21 -2
package/dist/cli.js
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* Pure CLI for CozoDB Memory
|
|
5
|
+
* Usage: cozo-memory <command> [options]
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
41
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
42
|
+
};
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
const commander_1 = require("commander");
|
|
45
|
+
const cli_commands_js_1 = require("./cli-commands.js");
|
|
46
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
47
|
+
const fs = __importStar(require("fs"));
|
|
48
|
+
const program = new commander_1.Command();
|
|
49
|
+
const cli = new cli_commands_js_1.CLICommands();
|
|
50
|
+
// Helper to format output
|
|
51
|
+
function formatOutput(data, format = 'pretty') {
|
|
52
|
+
if (format === 'json') {
|
|
53
|
+
console.log(JSON.stringify(data, null, 2));
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
console.log(chalk_1.default.cyan(JSON.stringify(data, null, 2)));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Helper to handle errors
|
|
60
|
+
function handleError(error) {
|
|
61
|
+
console.error(chalk_1.default.red('Error:'), error.message || error);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
program
|
|
65
|
+
.name('cozo-memory')
|
|
66
|
+
.description('CLI for CozoDB Memory - Local-first persistent memory for AI agents')
|
|
67
|
+
.version('1.0.6');
|
|
68
|
+
// Entity commands
|
|
69
|
+
const entity = program.command('entity').description('Entity operations');
|
|
70
|
+
entity
|
|
71
|
+
.command('create')
|
|
72
|
+
.description('Create a new entity')
|
|
73
|
+
.requiredOption('-n, --name <name>', 'Entity name')
|
|
74
|
+
.requiredOption('-t, --type <type>', 'Entity type')
|
|
75
|
+
.option('-m, --metadata <json>', 'Metadata as JSON string')
|
|
76
|
+
.option('-f, --format <format>', 'Output format (json|pretty)', 'pretty')
|
|
77
|
+
.action(async (options) => {
|
|
78
|
+
try {
|
|
79
|
+
await cli.init();
|
|
80
|
+
const metadata = options.metadata ? JSON.parse(options.metadata) : undefined;
|
|
81
|
+
const result = await cli.createEntity(options.name, options.type, metadata);
|
|
82
|
+
formatOutput(result, options.format);
|
|
83
|
+
await cli.close();
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
handleError(error);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
entity
|
|
90
|
+
.command('get')
|
|
91
|
+
.description('Get entity details')
|
|
92
|
+
.requiredOption('-i, --id <id>', 'Entity ID')
|
|
93
|
+
.option('-f, --format <format>', 'Output format (json|pretty)', 'pretty')
|
|
94
|
+
.action(async (options) => {
|
|
95
|
+
try {
|
|
96
|
+
await cli.init();
|
|
97
|
+
const result = await cli.getEntity(options.id);
|
|
98
|
+
formatOutput(result, options.format);
|
|
99
|
+
await cli.close();
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
handleError(error);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
entity
|
|
106
|
+
.command('delete')
|
|
107
|
+
.description('Delete an entity')
|
|
108
|
+
.requiredOption('-i, --id <id>', 'Entity ID')
|
|
109
|
+
.option('-f, --format <format>', 'Output format (json|pretty)', 'pretty')
|
|
110
|
+
.action(async (options) => {
|
|
111
|
+
try {
|
|
112
|
+
await cli.init();
|
|
113
|
+
const result = await cli.deleteEntity(options.id);
|
|
114
|
+
formatOutput(result, options.format);
|
|
115
|
+
await cli.close();
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
handleError(error);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
// Observation commands
|
|
122
|
+
const observation = program.command('observation').alias('obs').description('Observation operations');
|
|
123
|
+
observation
|
|
124
|
+
.command('add')
|
|
125
|
+
.description('Add observation to entity')
|
|
126
|
+
.requiredOption('-i, --entity-id <id>', 'Entity ID')
|
|
127
|
+
.requiredOption('-t, --text <text>', 'Observation text')
|
|
128
|
+
.option('-m, --metadata <json>', 'Metadata as JSON string')
|
|
129
|
+
.option('-f, --format <format>', 'Output format (json|pretty)', 'pretty')
|
|
130
|
+
.action(async (options) => {
|
|
131
|
+
try {
|
|
132
|
+
await cli.init();
|
|
133
|
+
const metadata = options.metadata ? JSON.parse(options.metadata) : undefined;
|
|
134
|
+
const result = await cli.addObservation(options.entityId, options.text, metadata);
|
|
135
|
+
formatOutput(result, options.format);
|
|
136
|
+
await cli.close();
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
handleError(error);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
// Relation commands
|
|
143
|
+
const relation = program.command('relation').alias('rel').description('Relation operations');
|
|
144
|
+
relation
|
|
145
|
+
.command('create')
|
|
146
|
+
.description('Create relation between entities')
|
|
147
|
+
.requiredOption('--from <id>', 'From entity ID')
|
|
148
|
+
.requiredOption('--to <id>', 'To entity ID')
|
|
149
|
+
.requiredOption('--type <type>', 'Relation type')
|
|
150
|
+
.option('-s, --strength <number>', 'Relation strength (0-1)', parseFloat)
|
|
151
|
+
.option('-m, --metadata <json>', 'Metadata as JSON string')
|
|
152
|
+
.option('-f, --format <format>', 'Output format (json|pretty)', 'pretty')
|
|
153
|
+
.action(async (options) => {
|
|
154
|
+
try {
|
|
155
|
+
await cli.init();
|
|
156
|
+
const metadata = options.metadata ? JSON.parse(options.metadata) : undefined;
|
|
157
|
+
const result = await cli.createRelation(options.from, options.to, options.type, options.strength, metadata);
|
|
158
|
+
formatOutput(result, options.format);
|
|
159
|
+
await cli.close();
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
handleError(error);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
// Search commands
|
|
166
|
+
const search = program.command('search').description('Search operations');
|
|
167
|
+
search
|
|
168
|
+
.command('query')
|
|
169
|
+
.description('Search memory')
|
|
170
|
+
.requiredOption('-q, --query <query>', 'Search query')
|
|
171
|
+
.option('-l, --limit <number>', 'Result limit', parseInt, 10)
|
|
172
|
+
.option('-t, --types <types>', 'Entity types (comma-separated)')
|
|
173
|
+
.option('--no-entities', 'Exclude entities')
|
|
174
|
+
.option('--no-observations', 'Exclude observations')
|
|
175
|
+
.option('-f, --format <format>', 'Output format (json|pretty)', 'pretty')
|
|
176
|
+
.action(async (options) => {
|
|
177
|
+
try {
|
|
178
|
+
await cli.init();
|
|
179
|
+
const entityTypes = options.types ? options.types.split(',') : undefined;
|
|
180
|
+
const result = await cli.search(options.query, options.limit, entityTypes, options.entities, options.observations);
|
|
181
|
+
formatOutput(result, options.format);
|
|
182
|
+
await cli.close();
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
handleError(error);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
search
|
|
189
|
+
.command('context')
|
|
190
|
+
.description('Get contextual information')
|
|
191
|
+
.requiredOption('-q, --query <query>', 'Context query')
|
|
192
|
+
.option('-w, --window <number>', 'Context window', parseInt)
|
|
193
|
+
.option('-h, --hours <number>', 'Time range in hours', parseInt)
|
|
194
|
+
.option('-f, --format <format>', 'Output format (json|pretty)', 'pretty')
|
|
195
|
+
.action(async (options) => {
|
|
196
|
+
try {
|
|
197
|
+
await cli.init();
|
|
198
|
+
const result = await cli.context(options.query, options.window, options.hours);
|
|
199
|
+
formatOutput(result, options.format);
|
|
200
|
+
await cli.close();
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
handleError(error);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
// Graph commands
|
|
207
|
+
const graph = program.command('graph').description('Graph operations');
|
|
208
|
+
graph
|
|
209
|
+
.command('explore')
|
|
210
|
+
.description('Explore graph from entity')
|
|
211
|
+
.requiredOption('-s, --start <id>', 'Start entity ID')
|
|
212
|
+
.option('-e, --end <id>', 'End entity ID (for path finding)')
|
|
213
|
+
.option('-h, --hops <number>', 'Max hops', parseInt, 3)
|
|
214
|
+
.option('-t, --types <types>', 'Relation types (comma-separated)')
|
|
215
|
+
.option('-f, --format <format>', 'Output format (json|pretty)', 'pretty')
|
|
216
|
+
.action(async (options) => {
|
|
217
|
+
try {
|
|
218
|
+
await cli.init();
|
|
219
|
+
const relationTypes = options.types ? options.types.split(',') : undefined;
|
|
220
|
+
const result = await cli.explore(options.start, options.end, options.hops, relationTypes);
|
|
221
|
+
formatOutput(result, options.format);
|
|
222
|
+
await cli.close();
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
handleError(error);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
graph
|
|
229
|
+
.command('pagerank')
|
|
230
|
+
.description('Calculate PageRank')
|
|
231
|
+
.option('-f, --format <format>', 'Output format (json|pretty)', 'pretty')
|
|
232
|
+
.action(async (options) => {
|
|
233
|
+
try {
|
|
234
|
+
await cli.init();
|
|
235
|
+
const result = await cli.pagerank();
|
|
236
|
+
formatOutput(result, options.format);
|
|
237
|
+
await cli.close();
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
handleError(error);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
graph
|
|
244
|
+
.command('communities')
|
|
245
|
+
.description('Detect communities')
|
|
246
|
+
.option('-f, --format <format>', 'Output format (json|pretty)', 'pretty')
|
|
247
|
+
.action(async (options) => {
|
|
248
|
+
try {
|
|
249
|
+
await cli.init();
|
|
250
|
+
const result = await cli.communities();
|
|
251
|
+
formatOutput(result, options.format);
|
|
252
|
+
await cli.close();
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
handleError(error);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
// System commands
|
|
259
|
+
const system = program.command('system').alias('sys').description('System operations');
|
|
260
|
+
system
|
|
261
|
+
.command('health')
|
|
262
|
+
.description('Check system health')
|
|
263
|
+
.option('-f, --format <format>', 'Output format (json|pretty)', 'pretty')
|
|
264
|
+
.action(async (options) => {
|
|
265
|
+
try {
|
|
266
|
+
await cli.init();
|
|
267
|
+
const result = await cli.health();
|
|
268
|
+
formatOutput(result, options.format);
|
|
269
|
+
await cli.close();
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
handleError(error);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
system
|
|
276
|
+
.command('metrics')
|
|
277
|
+
.description('Get system metrics')
|
|
278
|
+
.option('-f, --format <format>', 'Output format (json|pretty)', 'pretty')
|
|
279
|
+
.action(async (options) => {
|
|
280
|
+
try {
|
|
281
|
+
await cli.init();
|
|
282
|
+
const result = await cli.metrics();
|
|
283
|
+
formatOutput(result, options.format);
|
|
284
|
+
await cli.close();
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
handleError(error);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
// Export/Import commands
|
|
291
|
+
const exportCmd = program.command('export').description('Export memory');
|
|
292
|
+
exportCmd
|
|
293
|
+
.command('json')
|
|
294
|
+
.description('Export as JSON')
|
|
295
|
+
.option('-o, --output <file>', 'Output file')
|
|
296
|
+
.option('--include-metadata', 'Include metadata')
|
|
297
|
+
.option('--include-relationships', 'Include relationships')
|
|
298
|
+
.option('--include-observations', 'Include observations')
|
|
299
|
+
.action(async (options) => {
|
|
300
|
+
try {
|
|
301
|
+
await cli.init();
|
|
302
|
+
const result = await cli.exportMemory('json', {
|
|
303
|
+
includeMetadata: options.includeMetadata,
|
|
304
|
+
includeRelationships: options.includeRelationships,
|
|
305
|
+
includeObservations: options.includeObservations
|
|
306
|
+
});
|
|
307
|
+
const jsonData = typeof result.data === 'string' ? result.data : JSON.stringify(result.data, null, 2);
|
|
308
|
+
if (options.output) {
|
|
309
|
+
fs.writeFileSync(options.output, jsonData);
|
|
310
|
+
console.log(chalk_1.default.green(`✓ Exported to ${options.output}`));
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
console.log(jsonData);
|
|
314
|
+
}
|
|
315
|
+
await cli.close();
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
handleError(error);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
exportCmd
|
|
322
|
+
.command('markdown')
|
|
323
|
+
.description('Export as Markdown')
|
|
324
|
+
.option('-o, --output <file>', 'Output file')
|
|
325
|
+
.action(async (options) => {
|
|
326
|
+
try {
|
|
327
|
+
await cli.init();
|
|
328
|
+
const result = await cli.exportMemory('markdown');
|
|
329
|
+
if (options.output) {
|
|
330
|
+
fs.writeFileSync(options.output, result.data);
|
|
331
|
+
console.log(chalk_1.default.green(`✓ Exported to ${options.output}`));
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
console.log(result.data);
|
|
335
|
+
}
|
|
336
|
+
await cli.close();
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
handleError(error);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
exportCmd
|
|
343
|
+
.command('obsidian')
|
|
344
|
+
.description('Export as Obsidian ZIP')
|
|
345
|
+
.requiredOption('-o, --output <file>', 'Output ZIP file')
|
|
346
|
+
.action(async (options) => {
|
|
347
|
+
try {
|
|
348
|
+
await cli.init();
|
|
349
|
+
const result = await cli.exportMemory('obsidian');
|
|
350
|
+
// Obsidian export returns zipBuffer, not data
|
|
351
|
+
const buffer = result.zipBuffer || result.data;
|
|
352
|
+
if (!buffer) {
|
|
353
|
+
throw new Error('No buffer returned from export');
|
|
354
|
+
}
|
|
355
|
+
fs.writeFileSync(options.output, buffer);
|
|
356
|
+
console.log(chalk_1.default.green(`✓ Exported to ${options.output}`));
|
|
357
|
+
await cli.close();
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
handleError(error);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
const importCmd = program.command('import').description('Import memory');
|
|
364
|
+
importCmd
|
|
365
|
+
.command('file')
|
|
366
|
+
.description('Import from file')
|
|
367
|
+
.requiredOption('-i, --input <file>', 'Input file')
|
|
368
|
+
.requiredOption('-f, --format <format>', 'Source format (cozo|mem0|memgpt|markdown)')
|
|
369
|
+
.option('-s, --strategy <strategy>', 'Merge strategy (skip|overwrite|merge)', 'skip')
|
|
370
|
+
.action(async (options) => {
|
|
371
|
+
try {
|
|
372
|
+
await cli.init();
|
|
373
|
+
const data = fs.readFileSync(options.input, 'utf-8');
|
|
374
|
+
const result = await cli.importMemory(data, options.format, {
|
|
375
|
+
mergeStrategy: options.strategy
|
|
376
|
+
});
|
|
377
|
+
formatOutput(result, 'pretty');
|
|
378
|
+
await cli.close();
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
handleError(error);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
// Ingest commands
|
|
385
|
+
const ingest = program.command('ingest').description('Ingest files');
|
|
386
|
+
ingest
|
|
387
|
+
.command('file')
|
|
388
|
+
.description('Ingest file into entity')
|
|
389
|
+
.requiredOption('-i, --entity-id <id>', 'Entity ID')
|
|
390
|
+
.requiredOption('-p, --path <path>', 'File path')
|
|
391
|
+
.requiredOption('-f, --format <format>', 'File format (markdown|json|pdf)')
|
|
392
|
+
.option('-c, --chunking <type>', 'Chunking type (none|paragraphs)', 'paragraphs')
|
|
393
|
+
.option('-m, --max <number>', 'Max observations', parseInt)
|
|
394
|
+
.option('--no-deduplicate', 'Disable deduplication')
|
|
395
|
+
.action(async (options) => {
|
|
396
|
+
try {
|
|
397
|
+
await cli.init();
|
|
398
|
+
const result = await cli.ingestFile(options.entityId, options.format, options.path, undefined, {
|
|
399
|
+
chunking: options.chunking,
|
|
400
|
+
maxObservations: options.max,
|
|
401
|
+
deduplicate: options.deduplicate
|
|
402
|
+
});
|
|
403
|
+
formatOutput(result, 'pretty');
|
|
404
|
+
await cli.close();
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
handleError(error);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
program.parse();
|
package/dist/download-model.js
CHANGED
|
@@ -33,12 +33,14 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
require("dotenv/config"); // Load .env file first
|
|
36
37
|
const transformers_1 = require("@xenova/transformers");
|
|
37
38
|
const path = __importStar(require("path"));
|
|
38
39
|
// Configure cache path
|
|
39
40
|
const CACHE_DIR = path.resolve('./.cache');
|
|
40
41
|
transformers_1.env.cacheDir = CACHE_DIR;
|
|
41
|
-
|
|
42
|
+
// Read model from environment variable or use default
|
|
43
|
+
const MODEL_ID = process.env.EMBEDDING_MODEL || "Xenova/bge-m3";
|
|
42
44
|
async function downloadModel() {
|
|
43
45
|
console.log(`Downloading FP32 model for ${MODEL_ID}...`);
|
|
44
46
|
// quantized: false forces FP32 model download
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.EmbeddingService = void 0;
|
|
37
|
+
require("dotenv/config"); // Load .env file first
|
|
37
38
|
const transformers_1 = require("@xenova/transformers");
|
|
38
39
|
const ort = require('onnxruntime-node');
|
|
39
40
|
const path = __importStar(require("path"));
|
|
@@ -91,11 +92,27 @@ class EmbeddingService {
|
|
|
91
92
|
cache;
|
|
92
93
|
session = null;
|
|
93
94
|
tokenizer = null;
|
|
94
|
-
modelId
|
|
95
|
-
dimensions
|
|
95
|
+
modelId;
|
|
96
|
+
dimensions;
|
|
96
97
|
queue = Promise.resolve();
|
|
97
98
|
constructor() {
|
|
98
99
|
this.cache = new LRUCache(1000, 3600000); // 1000 entries, 1h TTL
|
|
100
|
+
// Support multiple embedding models via environment variable
|
|
101
|
+
this.modelId = process.env.EMBEDDING_MODEL || "Xenova/bge-m3";
|
|
102
|
+
// Set dimensions based on model
|
|
103
|
+
const dimensionMap = {
|
|
104
|
+
"Xenova/bge-m3": 1024,
|
|
105
|
+
"Xenova/all-MiniLM-L6-v2": 384,
|
|
106
|
+
"Xenova/bge-small-en-v1.5": 384,
|
|
107
|
+
"Xenova/nomic-embed-text-v1": 768,
|
|
108
|
+
"onnx-community/Qwen3-Embedding-0.6B-ONNX": 1024,
|
|
109
|
+
};
|
|
110
|
+
this.dimensions = dimensionMap[this.modelId] || 1024;
|
|
111
|
+
console.error(`[EmbeddingService] Using model: ${this.modelId} (${this.dimensions} dimensions)`);
|
|
112
|
+
}
|
|
113
|
+
// Public getter for dimensions
|
|
114
|
+
getDimensions() {
|
|
115
|
+
return this.dimensions;
|
|
99
116
|
}
|
|
100
117
|
// Serializes embedding execution to avoid event loop blocking
|
|
101
118
|
async runSerialized(task) {
|
|
@@ -109,21 +126,38 @@ class EmbeddingService {
|
|
|
109
126
|
if (this.session && this.tokenizer)
|
|
110
127
|
return;
|
|
111
128
|
try {
|
|
112
|
-
// 1.
|
|
129
|
+
// 1. Check if model needs to be downloaded
|
|
130
|
+
// Extract namespace and model name from modelId (e.g., "Xenova/bge-m3" or "onnx-community/Qwen3-Embedding-0.6B-ONNX")
|
|
131
|
+
const parts = this.modelId.split('/');
|
|
132
|
+
const namespace = parts[0];
|
|
133
|
+
const modelName = parts[1];
|
|
134
|
+
// Try both possible cache locations
|
|
135
|
+
let baseDir = path.join(transformers_1.env.cacheDir, namespace, modelName, 'onnx');
|
|
136
|
+
let fp32Path = path.join(baseDir, 'model.onnx');
|
|
137
|
+
let quantizedPath = path.join(baseDir, 'model_quantized.onnx');
|
|
138
|
+
// If ONNX model files don't exist, download them
|
|
139
|
+
if (!fs.existsSync(fp32Path) && !fs.existsSync(quantizedPath)) {
|
|
140
|
+
console.log(`[EmbeddingService] Model not found, downloading ${this.modelId}...`);
|
|
141
|
+
console.log(`[EmbeddingService] This may take a few minutes on first run.`);
|
|
142
|
+
// Import AutoModel dynamically to trigger download
|
|
143
|
+
const { AutoModel } = await import("@xenova/transformers");
|
|
144
|
+
await AutoModel.from_pretrained(this.modelId, { quantized: false });
|
|
145
|
+
console.log(`[EmbeddingService] Model download completed.`);
|
|
146
|
+
}
|
|
147
|
+
// 2. Load Tokenizer
|
|
113
148
|
if (!this.tokenizer) {
|
|
114
149
|
this.tokenizer = await transformers_1.AutoTokenizer.from_pretrained(this.modelId);
|
|
115
150
|
}
|
|
116
|
-
//
|
|
117
|
-
const baseDir = path.join(transformers_1.env.cacheDir, 'Xenova', 'bge-m3', 'onnx');
|
|
151
|
+
// 3. Determine model path
|
|
118
152
|
// Priority: FP32 (model.onnx) > Quantized (model_quantized.onnx)
|
|
119
|
-
let modelPath =
|
|
153
|
+
let modelPath = fp32Path;
|
|
120
154
|
if (!fs.existsSync(modelPath)) {
|
|
121
|
-
modelPath =
|
|
155
|
+
modelPath = quantizedPath;
|
|
122
156
|
}
|
|
123
157
|
if (!fs.existsSync(modelPath)) {
|
|
124
|
-
throw new Error(`Model file not found at: ${modelPath}
|
|
158
|
+
throw new Error(`Model file not found at: ${modelPath}. Download may have failed.`);
|
|
125
159
|
}
|
|
126
|
-
//
|
|
160
|
+
// 4. Create Session
|
|
127
161
|
if (!this.session) {
|
|
128
162
|
const options = {
|
|
129
163
|
executionProviders: ['cpu'], // Use CPU backend to avoid native conflicts
|
|
@@ -139,7 +173,15 @@ class EmbeddingService {
|
|
|
139
173
|
}
|
|
140
174
|
async embed(text) {
|
|
141
175
|
return this.runSerialized(async () => {
|
|
142
|
-
|
|
176
|
+
let textStr = String(text || "");
|
|
177
|
+
// For Qwen3-Embedding models, add instruction prefix for better results
|
|
178
|
+
// (only for queries, not for documents being indexed)
|
|
179
|
+
if (this.modelId.includes('Qwen3-Embedding')) {
|
|
180
|
+
// Add instruction prefix if not already present
|
|
181
|
+
if (!textStr.startsWith('Instruct:')) {
|
|
182
|
+
textStr = `Instruct: Given a web search query, retrieve relevant passages that answer the query\nQuery: ${textStr}`;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
143
185
|
// 1. Cache lookup
|
|
144
186
|
const cached = this.cache.get(textStr);
|
|
145
187
|
if (cached) {
|
|
@@ -171,14 +213,22 @@ class EmbeddingService {
|
|
|
171
213
|
const results = await this.session.run(feeds);
|
|
172
214
|
// 5. Pooling & Normalization
|
|
173
215
|
// Output name usually 'last_hidden_state' or 'logits'
|
|
174
|
-
// For BGE-M3, the first output is usually the hidden states [batch, seq_len, hidden_size]
|
|
175
216
|
const outputName = this.session.outputNames[0];
|
|
176
217
|
const outputTensor = results[outputName];
|
|
177
218
|
// Ensure we have data
|
|
178
219
|
if (!outputTensor || !attentionMaskData) {
|
|
179
220
|
throw new Error("No output data or attention mask available");
|
|
180
221
|
}
|
|
181
|
-
|
|
222
|
+
// Choose pooling strategy based on model
|
|
223
|
+
let embedding;
|
|
224
|
+
if (this.modelId.includes('Qwen3-Embedding')) {
|
|
225
|
+
// Qwen3-Embedding uses last token pooling
|
|
226
|
+
embedding = this.lastTokenPooling(outputTensor.data, attentionMaskData, outputTensor.dims);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
// BGE and other models use mean pooling
|
|
230
|
+
embedding = this.meanPooling(outputTensor.data, attentionMaskData, outputTensor.dims);
|
|
231
|
+
}
|
|
182
232
|
// Normalize
|
|
183
233
|
const normalized = this.normalize(embedding);
|
|
184
234
|
this.cache.set(textStr, normalized);
|
|
@@ -200,6 +250,25 @@ class EmbeddingService {
|
|
|
200
250
|
}
|
|
201
251
|
return results;
|
|
202
252
|
}
|
|
253
|
+
lastTokenPooling(data, attentionMask, dims) {
|
|
254
|
+
// dims: [batch_size, seq_len, hidden_size]
|
|
255
|
+
// Extract the last valid token's hidden state
|
|
256
|
+
const [batchSize, seqLen, hiddenSize] = dims;
|
|
257
|
+
// Find last valid token position
|
|
258
|
+
let lastValidIdx = seqLen - 1;
|
|
259
|
+
for (let i = seqLen - 1; i >= 0; i--) {
|
|
260
|
+
if (attentionMask[i] === 1n) {
|
|
261
|
+
lastValidIdx = i;
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// Extract embedding at last valid position
|
|
266
|
+
const embedding = new Float32Array(hiddenSize);
|
|
267
|
+
for (let j = 0; j < hiddenSize; j++) {
|
|
268
|
+
embedding[j] = data[lastValidIdx * hiddenSize + j];
|
|
269
|
+
}
|
|
270
|
+
return Array.from(embedding);
|
|
271
|
+
}
|
|
203
272
|
meanPooling(data, attentionMask, dims) {
|
|
204
273
|
// dims: [batch_size, seq_len, hidden_size]
|
|
205
274
|
// We assume batch_size = 1 for single embedding call
|
package/dist/hybrid-search.js
CHANGED
|
@@ -184,7 +184,7 @@ class HybridSearch {
|
|
|
184
184
|
console.error('--- DEBUG: Cozo Datalog Query ---');
|
|
185
185
|
console.error(datalogQuery);
|
|
186
186
|
console.error('--- DEBUG: Params ---');
|
|
187
|
-
console.
|
|
187
|
+
console.error(JSON.stringify(params, null, 2));
|
|
188
188
|
try {
|
|
189
189
|
const results = await this.db.run(datalogQuery, params);
|
|
190
190
|
let searchResults = results.rows.map((r) => ({
|
|
@@ -234,8 +234,13 @@ class HybridSearch {
|
|
|
234
234
|
const { limit: queryLimit = 10, filters, graphConstraints, vectorParams } = options;
|
|
235
235
|
// @ts-ignore
|
|
236
236
|
const { topk = 5, efSearch = 50 } = vectorParams || {};
|
|
237
|
-
//
|
|
238
|
-
return
|
|
237
|
+
// Use advancedSearch as the default implementation
|
|
238
|
+
return this.advancedSearch({
|
|
239
|
+
...options,
|
|
240
|
+
vectorParams: {
|
|
241
|
+
efSearch: 100
|
|
242
|
+
}
|
|
243
|
+
});
|
|
239
244
|
}
|
|
240
245
|
async graphRag(options) {
|
|
241
246
|
console.error("[HybridSearch] Starting graphRag with options:", JSON.stringify(options, null, 2));
|