rag-memory-epf-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +292 -0
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +1654 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/src/migrations/migration-manager.d.ts +28 -0
  7. package/dist/src/migrations/migration-manager.d.ts.map +1 -0
  8. package/dist/src/migrations/migration-manager.js +111 -0
  9. package/dist/src/migrations/migration-manager.js.map +1 -0
  10. package/dist/src/migrations/migrations.d.ts +3 -0
  11. package/dist/src/migrations/migrations.d.ts.map +1 -0
  12. package/dist/src/migrations/migrations.js +181 -0
  13. package/dist/src/migrations/migrations.js.map +1 -0
  14. package/dist/src/tools/graph-query-tools.d.ts +16 -0
  15. package/dist/src/tools/graph-query-tools.d.ts.map +1 -0
  16. package/dist/src/tools/graph-query-tools.js +452 -0
  17. package/dist/src/tools/graph-query-tools.js.map +1 -0
  18. package/dist/src/tools/knowledge-graph-tools.d.ts +16 -0
  19. package/dist/src/tools/knowledge-graph-tools.d.ts.map +1 -0
  20. package/dist/src/tools/knowledge-graph-tools.js +482 -0
  21. package/dist/src/tools/knowledge-graph-tools.js.map +1 -0
  22. package/dist/src/tools/migration-tools.d.ts +3 -0
  23. package/dist/src/tools/migration-tools.d.ts.map +1 -0
  24. package/dist/src/tools/migration-tools.js +172 -0
  25. package/dist/src/tools/migration-tools.js.map +1 -0
  26. package/dist/src/tools/rag-tools.d.ts +20 -0
  27. package/dist/src/tools/rag-tools.d.ts.map +1 -0
  28. package/dist/src/tools/rag-tools.js +524 -0
  29. package/dist/src/tools/rag-tools.js.map +1 -0
  30. package/dist/src/tools/tool-registry.d.ts +82 -0
  31. package/dist/src/tools/tool-registry.d.ts.map +1 -0
  32. package/dist/src/tools/tool-registry.js +185 -0
  33. package/dist/src/tools/tool-registry.js.map +1 -0
  34. package/dist/src/tools/types.d.ts +34 -0
  35. package/dist/src/tools/types.d.ts.map +1 -0
  36. package/dist/src/tools/types.js +2 -0
  37. package/dist/src/tools/types.js.map +1 -0
  38. package/package.json +39 -0
package/dist/index.js ADDED
@@ -0,0 +1,1654 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
+ import Database from 'better-sqlite3';
6
+ import * as sqliteVec from 'sqlite-vec';
7
+ import { get_encoding } from 'tiktoken';
8
+ import path from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ import { pipeline, env } from '@huggingface/transformers';
11
+ // Import our new structured tool system
12
+ import { getAllMCPTools, validateToolArgs, getSystemInfo } from './src/tools/tool-registry.js';
13
+ // Import migration system
14
+ import { MigrationManager } from './src/migrations/migration-manager.js';
15
+ import { migrations } from './src/migrations/migrations.js';
16
+ // Configure Hugging Face transformers for better compatibility
17
+ if (env.backends?.onnx?.wasm) {
18
+ env.backends.onnx.wasm.wasmPaths = './node_modules/@huggingface/transformers/dist/';
19
+ }
20
+ // Define database file path using environment variable with fallback
21
+ const defaultDbPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'rag-memory.db');
22
+ const DB_FILE_PATH = process.env.DB_FILE_PATH
23
+ ? path.isAbsolute(process.env.DB_FILE_PATH)
24
+ ? process.env.DB_FILE_PATH
25
+ : path.join(path.dirname(fileURLToPath(import.meta.url)), process.env.DB_FILE_PATH)
26
+ : defaultDbPath;
27
+ // Enhanced RAG-enabled Knowledge Graph Manager
28
+ class RAGKnowledgeGraphManager {
29
+ db = null;
30
+ encoding = null;
31
+ embeddingModel = null;
32
+ modelInitialized = false;
33
+ async initialize() {
34
+ console.error('๐Ÿš€ Initializing RAG Knowledge Graph MCP Server...');
35
+ // Initialize database
36
+ this.db = new Database(DB_FILE_PATH);
37
+ // Load sqlite-vec extension
38
+ sqliteVec.load(this.db);
39
+ // Initialize tiktoken
40
+ this.encoding = get_encoding("cl100k_base");
41
+ // Initialize embedding model
42
+ await this.initializeEmbeddingModel();
43
+ // Run database migrations
44
+ await this.runMigrations();
45
+ console.error('โœ… RAG-enabled knowledge graph initialized');
46
+ // Log system info
47
+ const systemInfo = getSystemInfo();
48
+ console.error(`๐Ÿ“Š System Info: ${systemInfo.toolCounts.total} tools available (${systemInfo.toolCounts.knowledgeGraph} knowledge graph, ${systemInfo.toolCounts.rag} RAG, ${systemInfo.toolCounts.graphQuery} query)`);
49
+ }
50
+ async initializeEmbeddingModel() {
51
+ try {
52
+ console.error('๐Ÿค– Loading embedding model: gte-multilingual-base (768-dim, 70+ languages)...');
53
+ // Configure environment to allow remote model downloads
54
+ env.allowRemoteModels = true;
55
+ env.allowLocalModels = true;
56
+ this.embeddingModel = await pipeline('feature-extraction', 'onnx-community/gte-multilingual-base', {
57
+ revision: 'main',
58
+ });
59
+ this.modelInitialized = true;
60
+ console.error('โœ… gte-multilingual-base model loaded successfully');
61
+ }
62
+ catch (error) {
63
+ console.error('โŒ Failed to load embedding model:', error);
64
+ console.error('๐Ÿ“‹ Falling back to simple embedding generation');
65
+ this.modelInitialized = false;
66
+ }
67
+ }
68
+ async runMigrations() {
69
+ if (!this.db)
70
+ throw new Error('Database not initialized');
71
+ console.error('๐Ÿ”„ Running database migrations...');
72
+ // Initialize migration manager
73
+ const migrationManager = new MigrationManager(this.db);
74
+ // Add all migrations
75
+ migrations.forEach(migration => {
76
+ migrationManager.addMigration(migration);
77
+ });
78
+ // Get pending migrations before running them
79
+ const pendingBefore = migrationManager.getPendingMigrations();
80
+ // Run pending migrations
81
+ const result = await migrationManager.runMigrations();
82
+ console.error(`๐Ÿ”ง Database schema ready (version ${result.currentVersion}, ${result.applied} migrations applied)`);
83
+ return {
84
+ applied: result.applied,
85
+ currentVersion: result.currentVersion,
86
+ appliedMigrations: pendingBefore.slice(0, result.applied).map(m => ({
87
+ version: m.version,
88
+ description: m.description
89
+ }))
90
+ };
91
+ }
92
+ cleanup() {
93
+ if (this.encoding) {
94
+ this.encoding.free();
95
+ this.encoding = null;
96
+ }
97
+ if (this.embeddingModel) {
98
+ // Clean up the embedding model if it has cleanup methods
99
+ this.embeddingModel = null;
100
+ this.modelInitialized = false;
101
+ }
102
+ if (this.db) {
103
+ this.db.close();
104
+ this.db = null;
105
+ }
106
+ }
107
+ // === ORIGINAL MCP FUNCTIONALITY ===
108
+ async createEntities(entities) {
109
+ if (!this.db)
110
+ throw new Error('Database not initialized');
111
+ const newEntities = [];
112
+ const stmt = this.db.prepare(`
113
+ INSERT OR IGNORE INTO entities (id, name, entityType, observations, metadata)
114
+ VALUES (?, ?, ?, ?, ?)
115
+ `);
116
+ for (const entity of entities) {
117
+ const entityId = `entity_${entity.name.toLowerCase().replace(/[^a-z0-9]/g, '_')}`;
118
+ const observations = JSON.stringify(entity.observations || []);
119
+ const metadata = JSON.stringify({});
120
+ const result = stmt.run(entityId, entity.name, entity.entityType, observations, metadata);
121
+ if (result.changes > 0) {
122
+ newEntities.push(entity);
123
+ // Generate embedding for the new entity
124
+ console.error(`๐Ÿ”ฎ Generating embedding for new entity: ${entity.name}`);
125
+ await this.embedEntity(entityId);
126
+ }
127
+ }
128
+ return newEntities;
129
+ }
130
+ async createRelations(relations) {
131
+ if (!this.db)
132
+ throw new Error('Database not initialized');
133
+ const newRelations = [];
134
+ for (const relation of relations) {
135
+ // Ensure entities exist
136
+ await this.createEntities([
137
+ { name: relation.from, entityType: 'CONCEPT', observations: [] },
138
+ { name: relation.to, entityType: 'CONCEPT', observations: [] }
139
+ ]);
140
+ const sourceId = `entity_${relation.from.toLowerCase().replace(/[^a-z0-9]/g, '_')}`;
141
+ const targetId = `entity_${relation.to.toLowerCase().replace(/[^a-z0-9]/g, '_')}`;
142
+ const relationId = `rel_${sourceId}_${relation.relationType}_${targetId}`.toLowerCase();
143
+ const stmt = this.db.prepare(`
144
+ INSERT OR IGNORE INTO relationships
145
+ (id, source_entity, target_entity, relationType, confidence, metadata)
146
+ VALUES (?, ?, ?, ?, ?, ?)
147
+ `);
148
+ const result = stmt.run(relationId, sourceId, targetId, relation.relationType, 1.0, '{}');
149
+ if (result.changes > 0) {
150
+ newRelations.push(relation);
151
+ }
152
+ }
153
+ return newRelations;
154
+ }
155
+ async addObservations(observations) {
156
+ if (!this.db)
157
+ throw new Error('Database not initialized');
158
+ const results = [];
159
+ for (const obs of observations) {
160
+ const entityId = `entity_${obs.entityName.toLowerCase().replace(/[^a-z0-9]/g, '_')}`;
161
+ // Get current observations
162
+ const entity = this.db.prepare(`
163
+ SELECT observations FROM entities WHERE id = ?
164
+ `).get(entityId);
165
+ if (!entity) {
166
+ throw new Error(`Entity with name ${obs.entityName} not found`);
167
+ }
168
+ const currentObservations = JSON.parse(entity.observations);
169
+ const newObservations = obs.contents.filter(content => !currentObservations.includes(content));
170
+ if (newObservations.length > 0) {
171
+ const updatedObservations = [...currentObservations, ...newObservations];
172
+ this.db.prepare(`
173
+ UPDATE entities SET observations = ? WHERE id = ?
174
+ `).run(JSON.stringify(updatedObservations), entityId);
175
+ // Regenerate embedding for the updated entity
176
+ console.error(`๐Ÿ”ฎ Regenerating embedding for updated entity: ${obs.entityName}`);
177
+ await this.embedEntity(entityId);
178
+ }
179
+ results.push({ entityName: obs.entityName, addedObservations: newObservations });
180
+ }
181
+ return results;
182
+ }
183
+ async deleteEntities(entityNames) {
184
+ if (!this.db)
185
+ throw new Error('Database not initialized');
186
+ console.error(`๐Ÿ—‘๏ธ Deleting entities: ${entityNames.join(', ')}`);
187
+ for (const name of entityNames) {
188
+ const entityId = `entity_${name.toLowerCase().replace(/[^a-z0-9]/g, '_')}`;
189
+ try {
190
+ // Check if entity exists first
191
+ const entityExists = this.db.prepare(`
192
+ SELECT id FROM entities WHERE id = ?
193
+ `).get(entityId);
194
+ if (!entityExists) {
195
+ console.warn(`โš ๏ธ Entity '${name}' not found, skipping`);
196
+ continue;
197
+ }
198
+ // Step 0: Delete entity embeddings
199
+ const embeddingMetadata = this.db.prepare(`
200
+ SELECT rowid FROM entity_embedding_metadata WHERE entity_id = ?
201
+ `).get(entityId);
202
+ if (embeddingMetadata) {
203
+ const embeddings = this.db.prepare(`
204
+ DELETE FROM entity_embeddings WHERE rowid = ?
205
+ `).run(embeddingMetadata.rowid);
206
+ const metadata = this.db.prepare(`
207
+ DELETE FROM entity_embedding_metadata WHERE entity_id = ?
208
+ `).run(entityId);
209
+ if (embeddings.changes > 0 || metadata.changes > 0) {
210
+ console.error(` โ”œโ”€ Removed entity embeddings for '${name}'`);
211
+ }
212
+ }
213
+ // Step 1: Delete chunk-entity associations
214
+ const chunkAssociations = this.db.prepare(`
215
+ DELETE FROM chunk_entities WHERE entity_id = ?
216
+ `).run(entityId);
217
+ if (chunkAssociations.changes > 0) {
218
+ console.error(` โ”œโ”€ Removed ${chunkAssociations.changes} chunk associations for '${name}'`);
219
+ }
220
+ // Step 2: Delete relationships where this entity is involved
221
+ const relationships = this.db.prepare(`
222
+ DELETE FROM relationships
223
+ WHERE source_entity = ? OR target_entity = ?
224
+ `).run(entityId, entityId);
225
+ if (relationships.changes > 0) {
226
+ console.error(` โ”œโ”€ Removed ${relationships.changes} relationships for '${name}'`);
227
+ }
228
+ // Step 3: Finally delete the entity itself
229
+ const entity = this.db.prepare(`
230
+ DELETE FROM entities WHERE id = ?
231
+ `).run(entityId);
232
+ if (entity.changes > 0) {
233
+ console.error(` โ””โ”€ Deleted entity '${name}' successfully`);
234
+ }
235
+ else {
236
+ console.warn(` โ””โ”€ Entity '${name}' was not deleted (possibly already removed)`);
237
+ }
238
+ }
239
+ catch (error) {
240
+ console.error(`โŒ Failed to delete entity '${name}':`, error);
241
+ // Continue with other entities instead of failing completely
242
+ }
243
+ }
244
+ console.error(`โœ… Entity deletion process completed`);
245
+ }
246
+ async deleteObservations(deletions) {
247
+ if (!this.db)
248
+ throw new Error('Database not initialized');
249
+ for (const deletion of deletions) {
250
+ const entityId = `entity_${deletion.entityName.toLowerCase().replace(/[^a-z0-9]/g, '_')}`;
251
+ const entity = this.db.prepare(`
252
+ SELECT observations FROM entities WHERE id = ?
253
+ `).get(entityId);
254
+ if (entity) {
255
+ const currentObservations = JSON.parse(entity.observations);
256
+ const filteredObservations = currentObservations.filter((obs) => !deletion.observations.includes(obs));
257
+ this.db.prepare(`
258
+ UPDATE entities SET observations = ? WHERE id = ?
259
+ `).run(JSON.stringify(filteredObservations), entityId);
260
+ }
261
+ }
262
+ }
263
+ async deleteRelations(relations) {
264
+ if (!this.db)
265
+ throw new Error('Database not initialized');
266
+ for (const relation of relations) {
267
+ const sourceId = `entity_${relation.from.toLowerCase().replace(/[^a-z0-9]/g, '_')}`;
268
+ const targetId = `entity_${relation.to.toLowerCase().replace(/[^a-z0-9]/g, '_')}`;
269
+ this.db.prepare(`
270
+ DELETE FROM relationships
271
+ WHERE source_entity = ? AND target_entity = ? AND relationType = ?
272
+ `).run(sourceId, targetId, relation.relationType);
273
+ }
274
+ }
275
+ async readGraph() {
276
+ if (!this.db)
277
+ throw new Error('Database not initialized');
278
+ const entities = this.db.prepare(`
279
+ SELECT name, entityType, observations FROM entities
280
+ `).all().map((row) => ({
281
+ name: row.name,
282
+ entityType: row.entityType,
283
+ observations: JSON.parse(row.observations)
284
+ }));
285
+ const relations = this.db.prepare(`
286
+ SELECT
287
+ e1.name as from_name,
288
+ e2.name as to_name,
289
+ r.relationType
290
+ FROM relationships r
291
+ JOIN entities e1 ON r.source_entity = e1.id
292
+ JOIN entities e2 ON r.target_entity = e2.id
293
+ `).all().map((row) => ({
294
+ from: row.from_name,
295
+ to: row.to_name,
296
+ relationType: row.relationType
297
+ }));
298
+ return { entities, relations };
299
+ }
300
+ async searchNodes(query, limit = 10) {
301
+ if (!this.db)
302
+ throw new Error('Database not initialized');
303
+ console.error(`๐Ÿ” Semantic entity search: "${query}"`);
304
+ // Generate query embedding
305
+ const queryEmbedding = await this.generateEmbedding(query);
306
+ // Perform vector similarity search on entities
307
+ const entityResults = this.db.prepare(`
308
+ SELECT
309
+ ee.rowid,
310
+ eem.entity_id,
311
+ eem.embedding_text,
312
+ ee.distance,
313
+ e.name,
314
+ e.entityType,
315
+ e.observations
316
+ FROM entity_embeddings ee
317
+ JOIN entity_embedding_metadata eem ON ee.rowid = eem.rowid
318
+ JOIN entities e ON eem.entity_id = e.id
319
+ WHERE ee.embedding MATCH ?
320
+ AND k = ?
321
+ ORDER BY ee.distance
322
+ `).all(Buffer.from(queryEmbedding.buffer), limit);
323
+ if (entityResults.length === 0) {
324
+ console.error(`โ„น๏ธ No semantic matches found for "${query}"`);
325
+ return { entities: [], relations: [] };
326
+ }
327
+ const entities = entityResults.map(result => ({
328
+ name: result.name,
329
+ entityType: result.entityType,
330
+ observations: JSON.parse(result.observations),
331
+ similarity: 1 / (1 + result.distance) // Convert distance to similarity score
332
+ }));
333
+ // Get relationships between the found entities
334
+ const entityNames = entities.map(e => e.name);
335
+ const relations = this.db.prepare(`
336
+ SELECT
337
+ e1.name as from_name,
338
+ e2.name as to_name,
339
+ r.relationType
340
+ FROM relationships r
341
+ JOIN entities e1 ON r.source_entity = e1.id
342
+ JOIN entities e2 ON r.target_entity = e2.id
343
+ WHERE e1.name IN (${entityNames.map(() => '?').join(',')})
344
+ AND e2.name IN (${entityNames.map(() => '?').join(',')})
345
+ `).all(...entityNames, ...entityNames).map((row) => ({
346
+ from: row.from_name,
347
+ to: row.to_name,
348
+ relationType: row.relationType
349
+ }));
350
+ console.error(`โœ… Found ${entities.length} semantically similar entities with ${relations.length} relationships`);
351
+ return { entities, relations };
352
+ }
353
+ async openNodes(names) {
354
+ if (!this.db)
355
+ throw new Error('Database not initialized');
356
+ if (names.length === 0) {
357
+ return { entities: [], relations: [] };
358
+ }
359
+ const entities = this.db.prepare(`
360
+ SELECT name, entityType, observations FROM entities
361
+ WHERE name IN (${names.map(() => '?').join(',')})
362
+ `).all(...names).map((row) => ({
363
+ name: row.name,
364
+ entityType: row.entityType,
365
+ observations: JSON.parse(row.observations)
366
+ }));
367
+ const relations = this.db.prepare(`
368
+ SELECT
369
+ e1.name as from_name,
370
+ e2.name as to_name,
371
+ r.relationType
372
+ FROM relationships r
373
+ JOIN entities e1 ON r.source_entity = e1.id
374
+ JOIN entities e2 ON r.target_entity = e2.id
375
+ WHERE e1.name IN (${names.map(() => '?').join(',')})
376
+ AND e2.name IN (${names.map(() => '?').join(',')})
377
+ `).all(...names, ...names).map((row) => ({
378
+ from: row.from_name,
379
+ to: row.to_name,
380
+ relationType: row.relationType
381
+ }));
382
+ return { entities, relations };
383
+ }
384
+ // === NEW RAG FUNCTIONALITY ===
385
+ // Generate embedding text for an entity (combines name, type, and observations)
386
+ generateEntityEmbeddingText(entity) {
387
+ const observationsText = entity.observations.join('. ');
388
+ return `${entity.name}. Type: ${entity.entityType}. ${observationsText}`.trim();
389
+ }
390
+ // NEW: Generic semantic summary generation methods
391
+ splitIntoSentences(text) {
392
+ // Split on sentence boundaries while preserving structure
393
+ return text
394
+ .split(/[.!?]+/)
395
+ .map(s => s.trim())
396
+ .filter(s => s.length > 10) // Filter out very short fragments
397
+ .map(s => s.replace(/^\s*[-โ€ข]\s*/, '')); // Clean up list markers
398
+ }
399
+ async calculateSentenceSimilarities(sentences, queryEmbedding) {
400
+ const similarities = [];
401
+ for (const sentence of sentences) {
402
+ const sentenceEmbedding = await this.generateEmbedding(sentence);
403
+ const similarity = this.cosineSimilarity(queryEmbedding, sentenceEmbedding);
404
+ similarities.push(similarity);
405
+ }
406
+ return similarities;
407
+ }
408
+ cosineSimilarity(a, b) {
409
+ let dotProduct = 0;
410
+ let normA = 0;
411
+ let normB = 0;
412
+ for (let i = 0; i < a.length; i++) {
413
+ dotProduct += a[i] * b[i];
414
+ normA += a[i] * a[i];
415
+ normB += b[i] * b[i];
416
+ }
417
+ return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
418
+ }
419
+ enhanceSimilarityWithContext(similarities, sentences, entities) {
420
+ const enhanced = [...similarities];
421
+ for (let i = 0; i < sentences.length; i++) {
422
+ const sentence = sentences[i].toLowerCase();
423
+ let contextBoost = 0;
424
+ // Generic boost for entity mentions (works across all domains)
425
+ for (const entity of entities) {
426
+ if (sentence.includes(entity.toLowerCase())) {
427
+ contextBoost += 0.1; // Moderate boost for entity relevance
428
+ }
429
+ }
430
+ // Generic boost for sentences with numbers (often contain key facts)
431
+ if (/\b\d+/.test(sentence)) {
432
+ contextBoost += 0.05;
433
+ }
434
+ // Generic boost for sentences with specific keywords that often indicate importance
435
+ const importanceWords = ['important', 'key', 'main', 'primary', 'essential', 'critical', 'significant'];
436
+ for (const word of importanceWords) {
437
+ if (sentence.includes(word)) {
438
+ contextBoost += 0.03;
439
+ break; // Only boost once per sentence
440
+ }
441
+ }
442
+ enhanced[i] += contextBoost;
443
+ }
444
+ return enhanced;
445
+ }
446
+ async generateContentSummary(chunkText, queryEmbedding, entities, maxSentences = 2) {
447
+ const sentences = this.splitIntoSentences(chunkText);
448
+ if (sentences.length === 0) {
449
+ return {
450
+ summary: chunkText.substring(0, 150) + (chunkText.length > 150 ? '...' : ''),
451
+ keyHighlight: chunkText.substring(0, 100) + (chunkText.length > 100 ? '...' : ''),
452
+ relevanceScore: 0.1
453
+ };
454
+ }
455
+ // Calculate semantic similarities
456
+ const similarities = await this.calculateSentenceSimilarities(sentences, queryEmbedding);
457
+ // Apply generic context enhancement
458
+ const enhancedSimilarities = this.enhanceSimilarityWithContext(similarities, sentences, entities);
459
+ // Rank sentences by relevance
460
+ const rankedIndices = Array.from({ length: sentences.length }, (_, i) => i)
461
+ .sort((a, b) => enhancedSimilarities[b] - enhancedSimilarities[a]);
462
+ // Select top sentences with diversity (avoid adjacent sentences)
463
+ const selectedSentences = [];
464
+ const usedIndices = new Set();
465
+ for (const idx of rankedIndices) {
466
+ if (selectedSentences.length >= maxSentences)
467
+ break;
468
+ // Prefer non-adjacent sentences for better coverage
469
+ const hasAdjacent = Array.from(usedIndices).some(usedIdx => Math.abs(idx - usedIdx) <= 1);
470
+ if (!hasAdjacent || selectedSentences.length === 0) {
471
+ selectedSentences.push({
472
+ text: sentences[idx],
473
+ score: enhancedSimilarities[idx],
474
+ index: idx
475
+ });
476
+ usedIndices.add(idx);
477
+ }
478
+ }
479
+ // Fallback: if still empty, take the top sentence regardless of adjacency
480
+ if (selectedSentences.length === 0) {
481
+ selectedSentences.push({
482
+ text: sentences[rankedIndices[0]],
483
+ score: enhancedSimilarities[rankedIndices[0]],
484
+ index: rankedIndices[0]
485
+ });
486
+ }
487
+ // Create summary
488
+ const keyHighlight = selectedSentences[0].text;
489
+ let summary;
490
+ if (selectedSentences.length === 1) {
491
+ summary = selectedSentences[0].text;
492
+ }
493
+ else {
494
+ // Sort by original order for coherent reading
495
+ const orderedSentences = selectedSentences
496
+ .sort((a, b) => a.index - b.index)
497
+ .map(s => s.text);
498
+ summary = orderedSentences.join(' [...] ');
499
+ }
500
+ const maxRelevanceScore = Math.max(...enhancedSimilarities);
501
+ return {
502
+ summary: summary,
503
+ keyHighlight: keyHighlight,
504
+ relevanceScore: maxRelevanceScore
505
+ };
506
+ }
507
+ // Generate and store embedding for a single entity
508
+ async embedEntity(entityId) {
509
+ if (!this.db)
510
+ throw new Error('Database not initialized');
511
+ // Get entity data
512
+ const entity = this.db.prepare(`
513
+ SELECT name, entityType, observations FROM entities WHERE id = ?
514
+ `).get(entityId);
515
+ if (!entity) {
516
+ console.warn(`Entity ${entityId} not found for embedding`);
517
+ return false;
518
+ }
519
+ const parsedObservations = JSON.parse(entity.observations);
520
+ const embeddingText = this.generateEntityEmbeddingText({
521
+ name: entity.name,
522
+ entityType: entity.entityType,
523
+ observations: parsedObservations
524
+ });
525
+ // Generate embedding
526
+ const embedding = await this.generateEmbedding(embeddingText);
527
+ try {
528
+ // Delete existing embedding if any
529
+ const existingMetadata = this.db.prepare(`
530
+ SELECT rowid FROM entity_embedding_metadata WHERE entity_id = ?
531
+ `).get(entityId);
532
+ if (existingMetadata) {
533
+ this.db.prepare(`DELETE FROM entity_embeddings WHERE rowid = ?`).run(existingMetadata.rowid);
534
+ this.db.prepare(`DELETE FROM entity_embedding_metadata WHERE entity_id = ?`).run(entityId);
535
+ }
536
+ // Insert new embedding
537
+ const result = this.db.prepare(`
538
+ INSERT INTO entity_embeddings (embedding) VALUES (?)
539
+ `).run(Buffer.from(embedding.buffer));
540
+ // Store metadata
541
+ this.db.prepare(`
542
+ INSERT INTO entity_embedding_metadata (rowid, entity_id, embedding_text)
543
+ VALUES (?, ?, ?)
544
+ `).run(result.lastInsertRowid, entityId, embeddingText);
545
+ return true;
546
+ }
547
+ catch (error) {
548
+ console.error(`Failed to embed entity ${entityId}:`, error);
549
+ return false;
550
+ }
551
+ }
552
+ // Embed all entities in the knowledge graph
553
+ async embedAllEntities() {
554
+ if (!this.db)
555
+ throw new Error('Database not initialized');
556
+ console.error('๐Ÿ”ฎ Generating embeddings for all entities...');
557
+ const entities = this.db.prepare(`
558
+ SELECT id FROM entities
559
+ `).all();
560
+ let embeddedCount = 0;
561
+ for (const entity of entities) {
562
+ const success = await this.embedEntity(entity.id);
563
+ if (success) {
564
+ embeddedCount++;
565
+ }
566
+ }
567
+ console.error(`โœ… Entity embeddings completed: ${embeddedCount}/${entities.length} entities embedded`);
568
+ return {
569
+ totalEntities: entities.length,
570
+ embeddedEntities: embeddedCount
571
+ };
572
+ }
573
+ // NEW: Generate knowledge graph chunks for entities and relationships
574
+ async generateKnowledgeGraphChunks() {
575
+ if (!this.db)
576
+ throw new Error('Database not initialized');
577
+ console.error('๐Ÿง  Generating knowledge graph chunks...');
578
+ // Clean up existing knowledge graph chunks
579
+ await this.cleanupKnowledgeGraphChunks();
580
+ let entityChunks = 0;
581
+ let relationshipChunks = 0;
582
+ // Generate entity chunks
583
+ const entities = this.db.prepare(`
584
+ SELECT id, name, entityType, observations FROM entities
585
+ `).all();
586
+ for (const entity of entities) {
587
+ const observations = JSON.parse(entity.observations);
588
+ const chunkText = this.generateEntityChunkText(entity.name, entity.entityType, observations);
589
+ const chunkId = `kg_entity_${entity.id}`;
590
+ // Store chunk metadata
591
+ this.db.prepare(`
592
+ INSERT INTO chunk_metadata (
593
+ chunk_id, chunk_type, entity_id, chunk_index, text, start_pos, end_pos, metadata
594
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
595
+ `).run(chunkId, 'entity', entity.id, 0, chunkText, 0, chunkText.length, JSON.stringify({
596
+ entity_name: entity.name,
597
+ entity_type: entity.entityType
598
+ }));
599
+ entityChunks++;
600
+ }
601
+ // Generate relationship chunks
602
+ const relationships = this.db.prepare(`
603
+ SELECT
604
+ r.id,
605
+ r.relationType,
606
+ e1.name as source_name,
607
+ e2.name as target_name,
608
+ r.confidence
609
+ FROM relationships r
610
+ JOIN entities e1 ON r.source_entity = e1.id
611
+ JOIN entities e2 ON r.target_entity = e2.id
612
+ `).all();
613
+ for (const rel of relationships) {
614
+ const chunkText = this.generateRelationshipChunkText(rel.source_name, rel.target_name, rel.relationType);
615
+ const chunkId = `kg_relationship_${rel.id}`;
616
+ // Store chunk metadata
617
+ this.db.prepare(`
618
+ INSERT INTO chunk_metadata (
619
+ chunk_id, chunk_type, relationship_id, chunk_index, text, start_pos, end_pos, metadata
620
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
621
+ `).run(chunkId, 'relationship', rel.id, 0, chunkText, 0, chunkText.length, JSON.stringify({
622
+ source_entity: rel.source_name,
623
+ target_entity: rel.target_name,
624
+ relation_type: rel.relationType,
625
+ confidence: rel.confidence
626
+ }));
627
+ relationshipChunks++;
628
+ }
629
+ console.error(`โœ… Knowledge graph chunks generated: ${entityChunks} entities, ${relationshipChunks} relationships`);
630
+ return { entityChunks, relationshipChunks };
631
+ }
632
+ // NEW: Embed knowledge graph chunks
633
+ async embedKnowledgeGraphChunks() {
634
+ if (!this.db)
635
+ throw new Error('Database not initialized');
636
+ console.error('๐Ÿ”ฎ Embedding knowledge graph chunks...');
637
+ // Get all knowledge graph chunks
638
+ const chunks = this.db.prepare(`
639
+ SELECT rowid, chunk_id, text
640
+ FROM chunk_metadata
641
+ WHERE chunk_type IN ('entity', 'relationship')
642
+ `).all();
643
+ let embeddedCount = 0;
644
+ for (const chunk of chunks) {
645
+ // Generate embedding
646
+ const embedding = await this.generateEmbedding(chunk.text);
647
+ try {
648
+ // Delete existing embedding if any
649
+ this.db.prepare(`DELETE FROM chunks WHERE rowid = ?`).run(chunk.rowid);
650
+ // Insert new embedding
651
+ const result = this.db.prepare(`
652
+ INSERT INTO chunks (embedding) VALUES (?)
653
+ `).run(Buffer.from(embedding.buffer));
654
+ if (result.changes > 0) {
655
+ embeddedCount++;
656
+ }
657
+ }
658
+ catch (error) {
659
+ console.error(`Failed to embed knowledge graph chunk ${chunk.chunk_id}:`, error);
660
+ }
661
+ }
662
+ console.error(`โœ… Knowledge graph chunks embedded: ${embeddedCount} embeddings created`);
663
+ return { embeddedChunks: embeddedCount };
664
+ }
665
+ // NEW: Generate textual representation for entity chunks
666
+ generateEntityChunkText(name, entityType, observations) {
667
+ const observationsText = observations.length > 0 ? observations.join('. ') : 'No additional information available.';
668
+ return `${name} is a ${entityType}. ${observationsText}`;
669
+ }
670
+ // NEW: Generate textual representation for relationship chunks
671
+ generateRelationshipChunkText(sourceName, targetName, relationType) {
672
+ // Convert relation type to more natural language
673
+ const relationText = relationType.toLowerCase().replace(/_/g, ' ');
674
+ return `${sourceName} ${relationText} ${targetName}`;
675
+ }
676
+ // NEW: Clean up existing knowledge graph chunks
677
+ async cleanupKnowledgeGraphChunks() {
678
+ if (!this.db)
679
+ return;
680
+ console.error('๐Ÿงน Cleaning up existing knowledge graph chunks...');
681
+ // Get existing knowledge graph chunks
682
+ const existingChunks = this.db.prepare(`
683
+ SELECT rowid FROM chunk_metadata WHERE chunk_type IN ('entity', 'relationship')
684
+ `).all();
685
+ let deletedVectors = 0;
686
+ let deletedAssociations = 0;
687
+ // Delete vectors and associations
688
+ for (const chunk of existingChunks) {
689
+ // Delete vector embeddings
690
+ const vectors = this.db.prepare(`
691
+ DELETE FROM chunks WHERE rowid = ?
692
+ `).run(chunk.rowid);
693
+ deletedVectors += vectors.changes;
694
+ // Delete chunk-entity associations
695
+ const associations = this.db.prepare(`
696
+ DELETE FROM chunk_entities WHERE chunk_rowid = ?
697
+ `).run(chunk.rowid);
698
+ deletedAssociations += associations.changes;
699
+ }
700
+ // Delete chunk metadata
701
+ const metadata = this.db.prepare(`
702
+ DELETE FROM chunk_metadata WHERE chunk_type IN ('entity', 'relationship')
703
+ `).run();
704
+ if (existingChunks.length > 0) {
705
+ console.error(` โ”œโ”€ Deleted ${deletedVectors} vector embeddings`);
706
+ console.error(` โ”œโ”€ Deleted ${deletedAssociations} entity associations`);
707
+ console.error(` โ””โ”€ Deleted ${metadata.changes} chunk metadata records`);
708
+ }
709
+ }
710
+ // Simple configurable term extraction (replacing hardcoded patterns)
711
+ extractTermsFromText(text, options = {}) {
712
+ const { minLength = 3, includeCapitalized = true, customPatterns = [] } = options;
713
+ const terms = new Set();
714
+ // Include capitalized words if requested
715
+ if (includeCapitalized) {
716
+ const capitalizedWords = text.match(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*\b/g) || [];
717
+ capitalizedWords.forEach(term => {
718
+ if (term.length >= minLength) {
719
+ terms.add(term.trim());
720
+ }
721
+ });
722
+ }
723
+ // Apply custom patterns if provided
724
+ customPatterns.forEach(patternStr => {
725
+ try {
726
+ const pattern = new RegExp(patternStr, 'gi');
727
+ const matches = text.match(pattern) || [];
728
+ matches.forEach(match => {
729
+ if (match.length >= minLength) {
730
+ terms.add(match.trim());
731
+ }
732
+ });
733
+ }
734
+ catch (error) {
735
+ console.error('Invalid regex pattern:', patternStr, error);
736
+ }
737
+ });
738
+ return Array.from(terms);
739
+ }
740
+ // Tokenize and chunk text
741
+ chunkText(text, maxTokens = 200, overlap = 20) {
742
+ if (!this.encoding)
743
+ throw new Error('Tokenizer not initialized');
744
+ const tokens = this.encoding.encode(text);
745
+ const chunks = [];
746
+ for (let i = 0; i < tokens.length; i += maxTokens - overlap) {
747
+ const chunkTokens = tokens.slice(i, i + maxTokens);
748
+ const decodedBytes = this.encoding.decode(chunkTokens);
749
+ const chunkText = new TextDecoder().decode(decodedBytes);
750
+ chunks.push({
751
+ id: '',
752
+ document_id: '',
753
+ chunk_index: chunks.length,
754
+ text: chunkText,
755
+ start_pos: i,
756
+ end_pos: i + chunkTokens.length
757
+ });
758
+ }
759
+ return chunks;
760
+ }
761
+ // Generate embeddings using sentence transformers
762
+ async generateEmbedding(text, dimensions = 768) {
763
+ if (this.modelInitialized && this.embeddingModel) {
764
+ try {
765
+ // Use the real sentence transformer model
766
+ const result = await this.embeddingModel(text, { pooling: 'mean', normalize: true });
767
+ // Extract the embedding array and convert to Float32Array
768
+ const embedding = result.data;
769
+ return new Float32Array(embedding.slice(0, dimensions));
770
+ }
771
+ catch (error) {
772
+ console.error('โš ๏ธ Embedding model failed, falling back to enhanced general semantic embedding:', error);
773
+ // Fall through to enhanced general implementation
774
+ }
775
+ }
776
+ // Enhanced general-purpose semantic embedding
777
+ const embedding = new Array(dimensions).fill(0);
778
+ // Normalize and tokenize text
779
+ const normalizedText = text.toLowerCase().replace(/[^\w\s]/g, ' ').replace(/\s+/g, ' ').trim();
780
+ const words = normalizedText.split(' ').filter(word => word.length > 1);
781
+ if (words.length === 0) {
782
+ return new Float32Array(embedding);
783
+ }
784
+ // Enhanced word importance calculation
785
+ const wordFreq = new Map();
786
+ const wordPositions = new Map();
787
+ words.forEach((word, position) => {
788
+ wordFreq.set(word, (wordFreq.get(word) || 0) + 1);
789
+ if (!wordPositions.has(word)) {
790
+ wordPositions.set(word, []);
791
+ }
792
+ wordPositions.get(word).push(position);
793
+ });
794
+ const totalWords = words.length;
795
+ const uniqueWords = wordFreq.size;
796
+ const vocabulary = Array.from(wordFreq.keys());
797
+ // Create enhanced semantic features for each unique word
798
+ vocabulary.forEach(word => {
799
+ const freq = wordFreq.get(word) || 1;
800
+ const positions = wordPositions.get(word) || [];
801
+ // Enhanced TF-IDF calculation
802
+ const tf = freq / totalWords;
803
+ const idf = Math.log(totalWords / freq); // More aggressive IDF for rare words
804
+ const tfidf = tf * idf;
805
+ // Multi-position importance (average of all positions)
806
+ const avgPosition = positions.reduce((sum, pos) => sum + pos, 0) / positions.length;
807
+ const positionWeight = this.calculatePositionWeight(avgPosition, totalWords);
808
+ // Word characteristics for semantic diversity
809
+ const wordLength = word.length;
810
+ const vowelCount = (word.match(/[aeiou]/g) || []).length;
811
+ const consonantCount = wordLength - vowelCount;
812
+ const vowelRatio = vowelCount / wordLength;
813
+ const hasCapitals = /[A-Z]/.test(word);
814
+ const hasNumbers = /\d/.test(word);
815
+ // Word complexity indicators
816
+ const isLongWord = wordLength > 6;
817
+ const isRareWord = freq === 1 && wordLength > 4;
818
+ const isCompoundWord = word.includes('_') || word.includes('-');
819
+ // Multiple hash functions for better semantic distribution
820
+ const hash1 = this.semanticHash(word, 1);
821
+ const hash2 = this.semanticHash(word, 2);
822
+ const hash3 = this.semanticHash(word, 3);
823
+ const hash4 = this.semanticHash(word + '_semantic', 1);
824
+ // Enhanced base weight with word importance
825
+ let baseWeight = tfidf * positionWeight;
826
+ // Boost important words
827
+ if (isLongWord)
828
+ baseWeight *= 1.3;
829
+ if (isRareWord)
830
+ baseWeight *= 1.5;
831
+ if (isCompoundWord)
832
+ baseWeight *= 1.2;
833
+ if (hasCapitals)
834
+ baseWeight *= 1.1;
835
+ // Primary word representation with enhanced distribution
836
+ embedding[hash1 % dimensions] += baseWeight * 1.2;
837
+ embedding[hash2 % dimensions] += baseWeight * 1.0;
838
+ embedding[hash3 % dimensions] += baseWeight * 0.8;
839
+ // Character-level features
840
+ embedding[hash4 % dimensions] += vowelRatio * baseWeight * 0.5;
841
+ embedding[(hash1 + wordLength) % dimensions] += (wordLength / 15.0) * baseWeight * 0.4;
842
+ // Structural and linguistic features
843
+ if (hasCapitals) {
844
+ embedding[(hash2 + 7) % dimensions] += baseWeight * 0.6;
845
+ }
846
+ if (hasNumbers) {
847
+ embedding[(hash3 + 11) % dimensions] += baseWeight * 0.6;
848
+ }
849
+ if (wordLength > 8) { // Complex words get special treatment
850
+ embedding[(hash1 + 13) % dimensions] += baseWeight * 0.7;
851
+ }
852
+ // Enhanced n-gram features with better context
853
+ positions.forEach(position => {
854
+ // Bigram features
855
+ if (position > 0) {
856
+ const bigram = words[position - 1] + '_' + word;
857
+ const bigramHash = this.semanticHash(bigram, 4);
858
+ embedding[bigramHash % dimensions] += baseWeight * 0.5;
859
+ }
860
+ if (position < words.length - 1) {
861
+ const nextBigram = word + '_' + words[position + 1];
862
+ const nextBigramHash = this.semanticHash(nextBigram, 5);
863
+ embedding[nextBigramHash % dimensions] += baseWeight * 0.5;
864
+ }
865
+ // Trigram features for important words
866
+ if (isLongWord || isRareWord) {
867
+ if (position > 0 && position < words.length - 1) {
868
+ const trigram = words[position - 1] + '_' + word + '_' + words[position + 1];
869
+ const trigramHash = this.semanticHash(trigram, 6);
870
+ embedding[trigramHash % dimensions] += baseWeight * 0.3;
871
+ }
872
+ }
873
+ });
874
+ // Enhanced prefix/suffix features for morphological richness
875
+ if (wordLength >= 3) {
876
+ const prefix2 = word.substring(0, Math.min(2, wordLength));
877
+ const prefix3 = word.substring(0, Math.min(3, wordLength));
878
+ const suffix2 = word.substring(Math.max(0, wordLength - 2));
879
+ const suffix3 = word.substring(Math.max(0, wordLength - 3));
880
+ const prefix2Hash = this.semanticHash(prefix2 + '_pre2', 7);
881
+ const prefix3Hash = this.semanticHash(prefix3 + '_pre3', 8);
882
+ const suffix2Hash = this.semanticHash(suffix2 + '_suf2', 9);
883
+ const suffix3Hash = this.semanticHash(suffix3 + '_suf3', 10);
884
+ embedding[prefix2Hash % dimensions] += baseWeight * 0.3;
885
+ embedding[prefix3Hash % dimensions] += baseWeight * 0.4;
886
+ embedding[suffix2Hash % dimensions] += baseWeight * 0.3;
887
+ embedding[suffix3Hash % dimensions] += baseWeight * 0.4;
888
+ }
889
+ });
890
+ // Enhanced global text features
891
+ const avgWordLength = words.reduce((sum, word) => sum + word.length, 0) / words.length;
892
+ const maxWordLength = Math.max(...words.map(w => w.length));
893
+ const textComplexity = uniqueWords / totalWords;
894
+ const textDensity = Math.log(1 + totalWords);
895
+ const lexicalDiversity = uniqueWords / Math.sqrt(totalWords); // Better diversity measure
896
+ // Distribute enhanced global features
897
+ const globalHash1 = this.semanticHash('_global_complexity_', 11);
898
+ const globalHash2 = this.semanticHash('_global_density_', 12);
899
+ const globalHash3 = this.semanticHash('_global_length_', 13);
900
+ const globalHash4 = this.semanticHash('_global_diversity_', 14);
901
+ const globalHash5 = this.semanticHash('_global_max_word_', 15);
902
+ embedding[globalHash1 % dimensions] += textComplexity * 0.6;
903
+ embedding[globalHash2 % dimensions] += textDensity / 8.0;
904
+ embedding[globalHash3 % dimensions] += avgWordLength / 12.0;
905
+ embedding[globalHash4 % dimensions] += lexicalDiversity * 0.5;
906
+ embedding[globalHash5 % dimensions] += maxWordLength / 15.0;
907
+ // Enhanced document length normalization
908
+ const docLengthNorm = Math.log(1 + totalWords);
909
+ for (let i = 0; i < dimensions; i++) {
910
+ embedding[i] = embedding[i] / Math.max(docLengthNorm, 1.0);
911
+ }
912
+ // L2 normalization for cosine similarity
913
+ const magnitude = Math.sqrt(embedding.reduce((sum, val) => sum + val * val, 0));
914
+ const normalizedEmbedding = magnitude > 0 ? embedding.map(val => val / magnitude) : embedding;
915
+ return new Float32Array(normalizedEmbedding);
916
+ }
917
+ // Calculate position-based importance weight
918
+ calculatePositionWeight(position, totalWords) {
919
+ if (totalWords === 1)
920
+ return 1.0;
921
+ // Higher weight for beginning and end, lower for middle
922
+ const relativePos = position / (totalWords - 1);
923
+ // U-shaped curve: higher at start (0) and end (1), lower in middle (0.5)
924
+ const positionWeight = 1.0 - 0.3 * Math.sin(relativePos * Math.PI);
925
+ return positionWeight;
926
+ }
927
+ // General-purpose semantic hash function
928
+ semanticHash(str, seed) {
929
+ let hash = seed;
930
+ for (let i = 0; i < str.length; i++) {
931
+ const char = str.charCodeAt(i);
932
+ hash = ((hash << 5) - hash) + char;
933
+ hash = hash & hash; // Convert to 32-bit integer
934
+ }
935
+ return Math.abs(hash);
936
+ }
937
+ // === NEW SEPARATE TOOLS ===
938
+ async storeDocument(id, content, metadata = {}) {
939
+ if (!this.db)
940
+ throw new Error('Database not initialized');
941
+ console.error(`๐Ÿ“„ Storing document: ${id}`);
942
+ // Clean up existing document
943
+ await this.cleanupDocument(id);
944
+ // Store document
945
+ this.db.prepare(`
946
+ INSERT OR REPLACE INTO documents (id, content, metadata)
947
+ VALUES (?, ?, ?)
948
+ `).run(id, content, JSON.stringify(metadata));
949
+ console.error(`โœ… Document stored: ${id}`);
950
+ return { id, stored: true };
951
+ }
952
+ async chunkDocument(documentId, options = {}) {
953
+ if (!this.db)
954
+ throw new Error('Database not initialized');
955
+ // Get document
956
+ const document = this.db.prepare(`
957
+ SELECT content FROM documents WHERE id = ?
958
+ `).get(documentId);
959
+ if (!document) {
960
+ throw new Error(`Document with ID ${documentId} not found`);
961
+ }
962
+ const { maxTokens = 200, overlap = 20 } = options;
963
+ console.error(`๐Ÿ”ช Chunking document: ${documentId} (maxTokens: ${maxTokens}, overlap: ${overlap})`);
964
+ // Clean up existing chunks
965
+ await this.cleanupDocument(documentId);
966
+ // Create chunks
967
+ const chunks = this.chunkText(document.content, maxTokens, overlap);
968
+ const resultChunks = [];
969
+ for (const chunk of chunks) {
970
+ const chunkId = `${documentId}_chunk_${chunk.chunk_index}`;
971
+ // Store chunk metadata (no embedding yet)
972
+ this.db.prepare(`
973
+ INSERT INTO chunk_metadata (
974
+ chunk_id, document_id, chunk_index, text, start_pos, end_pos
975
+ ) VALUES (?, ?, ?, ?, ?, ?)
976
+ `).run(chunkId, documentId, chunk.chunk_index, chunk.text, chunk.start_pos, chunk.end_pos);
977
+ resultChunks.push({
978
+ id: chunkId,
979
+ text: chunk.text,
980
+ startPos: chunk.start_pos,
981
+ endPos: chunk.end_pos
982
+ });
983
+ }
984
+ console.error(`โœ… Document chunked: ${chunks.length} chunks created`);
985
+ return { documentId, chunks: resultChunks };
986
+ }
987
+ async embedChunks(documentId) {
988
+ if (!this.db)
989
+ throw new Error('Database not initialized');
990
+ console.error(`๐Ÿ”ฎ Embedding chunks for document: ${documentId}`);
991
+ // Get all chunks for the document
992
+ const chunks = this.db.prepare(`
993
+ SELECT rowid, chunk_id, text FROM chunk_metadata WHERE document_id = ?
994
+ `).all(documentId);
995
+ if (chunks.length === 0) {
996
+ throw new Error(`No chunks found for document ${documentId}. Run chunkDocument first.`);
997
+ }
998
+ let embeddedCount = 0;
999
+ for (const chunk of chunks) {
1000
+ // Generate embedding
1001
+ const embedding = await this.generateEmbedding(chunk.text);
1002
+ // Store in vector table - the vec0 table should auto-handle rowid matching
1003
+ try {
1004
+ // First, delete any existing embedding for this rowid
1005
+ this.db.prepare(`DELETE FROM chunks WHERE rowid = ?`).run(chunk.rowid);
1006
+ // Insert new embedding, letting vec0 handle the rowid
1007
+ const result = this.db.prepare(`
1008
+ INSERT INTO chunks (embedding) VALUES (?)
1009
+ `).run(Buffer.from(embedding.buffer));
1010
+ if (result.changes > 0) {
1011
+ embeddedCount++;
1012
+ // console.log(`โœ… Embedded chunk ${chunk.chunk_id} with rowid ${result.lastInsertRowid}`);
1013
+ }
1014
+ }
1015
+ catch (error) {
1016
+ console.error(`Failed to embed chunk ${chunk.chunk_id}:`, error);
1017
+ // Continue with other chunks instead of failing completely
1018
+ }
1019
+ }
1020
+ console.error(`โœ… Chunks embedded: ${embeddedCount} embeddings created`);
1021
+ return { documentId, embeddedChunks: embeddedCount };
1022
+ }
1023
+ async extractTerms(documentId, options = {}) {
1024
+ if (!this.db)
1025
+ throw new Error('Database not initialized');
1026
+ // Get document
1027
+ const document = this.db.prepare(`
1028
+ SELECT content FROM documents WHERE id = ?
1029
+ `).get(documentId);
1030
+ if (!document) {
1031
+ throw new Error(`Document with ID ${documentId} not found`);
1032
+ }
1033
+ console.error(`๐Ÿ” Extracting terms from document: ${documentId}`);
1034
+ const terms = this.extractTermsFromText(document.content, options);
1035
+ console.error(`โœ… Terms extracted: ${terms.length} terms found`);
1036
+ return { documentId, terms };
1037
+ }
1038
+ async linkEntitiesToDocument(documentId, entityNames) {
1039
+ if (!this.db)
1040
+ throw new Error('Database not initialized');
1041
+ console.error(`๐Ÿ”— Linking entities to document: ${documentId}`);
1042
+ // Verify document exists
1043
+ const document = this.db.prepare(`
1044
+ SELECT id FROM documents WHERE id = ?
1045
+ `).get(documentId);
1046
+ if (!document) {
1047
+ throw new Error(`Document with ID ${documentId} not found`);
1048
+ }
1049
+ // Get chunks for this document
1050
+ const chunks = this.db.prepare(`
1051
+ SELECT rowid FROM chunk_metadata WHERE document_id = ?
1052
+ `).all(documentId);
1053
+ let linkedCount = 0;
1054
+ for (const entityName of entityNames) {
1055
+ const entityId = `entity_${entityName.toLowerCase().replace(/[^a-z0-9]/g, '_')}`;
1056
+ // Verify entity exists
1057
+ const entity = this.db.prepare(`
1058
+ SELECT id FROM entities WHERE id = ?
1059
+ `).get(entityId);
1060
+ if (!entity) {
1061
+ console.warn(`Entity ${entityName} not found, skipping`);
1062
+ continue;
1063
+ }
1064
+ // Link entity to all chunks of the document
1065
+ for (const chunk of chunks) {
1066
+ this.db.prepare(`
1067
+ INSERT OR IGNORE INTO chunk_entities (chunk_rowid, entity_id)
1068
+ VALUES (?, ?)
1069
+ `).run(chunk.rowid, entityId);
1070
+ }
1071
+ linkedCount++;
1072
+ }
1073
+ console.error(`โœ… Entities linked: ${linkedCount} entities linked to document`);
1074
+ return { documentId, linkedEntities: linkedCount };
1075
+ }
1076
+ async cleanupDocument(documentId) {
1077
+ if (!this.db)
1078
+ return;
1079
+ console.error(`๐Ÿงน Cleaning up document: ${documentId}`);
1080
+ // Get existing chunks
1081
+ const existingChunks = this.db.prepare(`
1082
+ SELECT rowid FROM chunk_metadata WHERE document_id = ?
1083
+ `).all(documentId);
1084
+ let deletedAssociations = 0;
1085
+ let deletedVectors = 0;
1086
+ // Delete associations and vectors
1087
+ for (const chunk of existingChunks) {
1088
+ // Delete chunk-entity associations
1089
+ const associations = this.db.prepare(`
1090
+ DELETE FROM chunk_entities WHERE chunk_rowid = ?
1091
+ `).run(chunk.rowid);
1092
+ deletedAssociations += associations.changes;
1093
+ // Delete vector embeddings
1094
+ const vectors = this.db.prepare(`
1095
+ DELETE FROM chunks WHERE rowid = ?
1096
+ `).run(chunk.rowid);
1097
+ deletedVectors += vectors.changes;
1098
+ }
1099
+ // Delete chunk metadata
1100
+ const metadata = this.db.prepare(`
1101
+ DELETE FROM chunk_metadata WHERE document_id = ?
1102
+ `).run(documentId);
1103
+ if (existingChunks.length > 0) {
1104
+ console.error(` โ”œโ”€ Deleted ${deletedAssociations} entity associations`);
1105
+ console.error(` โ”œโ”€ Deleted ${deletedVectors} vector embeddings`);
1106
+ console.error(` โ””โ”€ Deleted ${metadata.changes} chunk metadata records`);
1107
+ }
1108
+ }
1109
+ async deleteDocument(documentId) {
1110
+ if (!this.db)
1111
+ throw new Error('Database not initialized');
1112
+ console.error(`๐Ÿ—‘๏ธ Deleting document: ${documentId}`);
1113
+ try {
1114
+ // Check if document exists
1115
+ const document = this.db.prepare(`
1116
+ SELECT id FROM documents WHERE id = ?
1117
+ `).get(documentId);
1118
+ if (!document) {
1119
+ console.warn(`โš ๏ธ Document '${documentId}' not found`);
1120
+ return { documentId, deleted: false };
1121
+ }
1122
+ // Clean up all associated data
1123
+ await this.cleanupDocument(documentId);
1124
+ // Delete the document itself
1125
+ const result = this.db.prepare(`
1126
+ DELETE FROM documents WHERE id = ?
1127
+ `).run(documentId);
1128
+ if (result.changes > 0) {
1129
+ console.error(`โœ… Document '${documentId}' deleted successfully`);
1130
+ return { documentId, deleted: true };
1131
+ }
1132
+ else {
1133
+ console.warn(`โš ๏ธ Document '${documentId}' was not deleted`);
1134
+ return { documentId, deleted: false };
1135
+ }
1136
+ }
1137
+ catch (error) {
1138
+ console.error(`โŒ Failed to delete document '${documentId}':`, error);
1139
+ throw error;
1140
+ }
1141
+ }
1142
+ async deleteMultipleDocuments(documentIds) {
1143
+ if (!this.db)
1144
+ throw new Error('Database not initialized');
1145
+ console.error(`๐Ÿ—‘๏ธ Bulk deleting ${documentIds.length} documents`);
1146
+ const results = [];
1147
+ let deletedCount = 0;
1148
+ let failedCount = 0;
1149
+ for (const documentId of documentIds) {
1150
+ try {
1151
+ const result = await this.deleteDocument(documentId);
1152
+ results.push(result);
1153
+ if (result.deleted) {
1154
+ deletedCount++;
1155
+ }
1156
+ else {
1157
+ failedCount++;
1158
+ }
1159
+ }
1160
+ catch (error) {
1161
+ console.error(`โŒ Failed to delete document '${documentId}':`, error);
1162
+ results.push({ documentId, deleted: false });
1163
+ failedCount++;
1164
+ }
1165
+ }
1166
+ const summary = {
1167
+ deleted: deletedCount,
1168
+ failed: failedCount,
1169
+ total: documentIds.length
1170
+ };
1171
+ console.error(`โœ… Bulk deletion completed: ${deletedCount} deleted, ${failedCount} failed, ${documentIds.length} total`);
1172
+ return { results, summary };
1173
+ }
1174
+ async deleteDocuments(documentIds) {
1175
+ if (!this.db)
1176
+ throw new Error('Database not initialized');
1177
+ // Normalize input to always be an array
1178
+ const idsArray = Array.isArray(documentIds) ? documentIds : [documentIds];
1179
+ const isMultiple = Array.isArray(documentIds);
1180
+ console.error(`๐Ÿ—‘๏ธ Deleting ${idsArray.length} document${idsArray.length > 1 ? 's' : ''}`);
1181
+ const results = [];
1182
+ let deletedCount = 0;
1183
+ let failedCount = 0;
1184
+ for (const documentId of idsArray) {
1185
+ try {
1186
+ const result = await this.deleteDocument(documentId);
1187
+ results.push(result);
1188
+ if (result.deleted) {
1189
+ deletedCount++;
1190
+ }
1191
+ else {
1192
+ failedCount++;
1193
+ }
1194
+ }
1195
+ catch (error) {
1196
+ console.error(`โŒ Failed to delete document '${documentId}':`, error);
1197
+ results.push({ documentId, deleted: false });
1198
+ failedCount++;
1199
+ }
1200
+ }
1201
+ const summary = {
1202
+ deleted: deletedCount,
1203
+ failed: failedCount,
1204
+ total: idsArray.length
1205
+ };
1206
+ const operation = isMultiple ? 'Bulk deletion' : 'Document deletion';
1207
+ console.error(`โœ… ${operation} completed: ${deletedCount} deleted, ${failedCount} failed, ${idsArray.length} total`);
1208
+ return { results, summary };
1209
+ }
1210
+ async listDocuments(includeMetadata = true) {
1211
+ if (!this.db)
1212
+ throw new Error('Database not initialized');
1213
+ console.error(`๐Ÿ“‹ Listing all documents (metadata: ${includeMetadata})`);
1214
+ const query = includeMetadata
1215
+ ? `SELECT id, metadata, created_at FROM documents ORDER BY created_at DESC`
1216
+ : `SELECT id, created_at FROM documents ORDER BY created_at DESC`;
1217
+ const rows = this.db.prepare(query).all();
1218
+ const documents = rows.map(row => ({
1219
+ id: row.id,
1220
+ ...(includeMetadata && row.metadata ? { metadata: JSON.parse(row.metadata) } : {}),
1221
+ created_at: row.created_at
1222
+ }));
1223
+ console.error(`โœ… Found ${documents.length} documents`);
1224
+ return { documents };
1225
+ }
1226
+ async hybridSearch(query, limit = 5, useGraph = true) {
1227
+ if (!this.db)
1228
+ throw new Error('Database not initialized');
1229
+ if (!this.encoding)
1230
+ throw new Error('Tokenizer not initialized');
1231
+ console.error(`๐Ÿ” Enhanced hybrid search: "${query}"`);
1232
+ // Generate query embedding
1233
+ const queryEmbedding = await this.generateEmbedding(query);
1234
+ // Enhanced vector search across ALL chunk types (documents, entities, relationships)
1235
+ const vectorResults = this.db.prepare(`
1236
+ SELECT
1237
+ c.rowid,
1238
+ m.chunk_id,
1239
+ m.chunk_type,
1240
+ m.document_id,
1241
+ m.entity_id,
1242
+ m.relationship_id,
1243
+ m.chunk_index,
1244
+ m.text,
1245
+ m.start_pos,
1246
+ m.end_pos,
1247
+ m.metadata as chunk_metadata,
1248
+ c.distance,
1249
+ COALESCE(d.metadata, '{}') as doc_metadata
1250
+ FROM chunks c
1251
+ JOIN chunk_metadata m ON c.rowid = m.rowid
1252
+ LEFT JOIN documents d ON m.document_id = d.id
1253
+ WHERE c.embedding MATCH ?
1254
+ AND k = ?
1255
+ ORDER BY c.distance
1256
+ `).all(Buffer.from(queryEmbedding.buffer), limit * 3);
1257
+ if (vectorResults.length === 0) {
1258
+ console.error(`โ„น๏ธ No vector matches found for "${query}"`);
1259
+ return [];
1260
+ }
1261
+ // Get entity information for graph enhancement
1262
+ let connectedEntities = new Set();
1263
+ if (useGraph) {
1264
+ const queryEntities = this.extractTermsFromText(query);
1265
+ for (const entity of queryEntities) {
1266
+ const connected = this.db.prepare(`
1267
+ SELECT DISTINCT
1268
+ CASE
1269
+ WHEN r.source_entity = e1.id THEN e2.name
1270
+ ELSE e1.name
1271
+ END as connected_name
1272
+ FROM entities e1
1273
+ JOIN relationships r ON (r.source_entity = e1.id OR r.target_entity = e1.id)
1274
+ JOIN entities e2 ON (e2.id = r.source_entity OR e2.id = r.target_entity)
1275
+ WHERE e1.name = ? AND e2.name != ?
1276
+ `).all(entity, entity);
1277
+ connected.forEach((row) => connectedEntities.add(row.connected_name));
1278
+ }
1279
+ }
1280
+ // Process results with semantic summaries
1281
+ const enhancedResults = [];
1282
+ for (const result of vectorResults) {
1283
+ // Get entities associated with this chunk (for document chunks)
1284
+ let chunkEntities = [];
1285
+ if (result.chunk_type === 'document') {
1286
+ chunkEntities = this.db.prepare(`
1287
+ SELECT e.name
1288
+ FROM chunk_entities ce
1289
+ JOIN entities e ON e.id = ce.entity_id
1290
+ WHERE ce.chunk_rowid = ?
1291
+ `).all(result.rowid).map((row) => row.name);
1292
+ }
1293
+ else if (result.chunk_type === 'entity' && result.entity_id) {
1294
+ // For entity chunks, get the entity name
1295
+ const entity = this.db.prepare(`
1296
+ SELECT name FROM entities WHERE id = ?
1297
+ `).get(result.entity_id);
1298
+ if (entity) {
1299
+ chunkEntities = [entity.name];
1300
+ }
1301
+ }
1302
+ else if (result.chunk_type === 'relationship' && result.relationship_id) {
1303
+ // For relationship chunks, get both entities
1304
+ const relEntities = this.db.prepare(`
1305
+ SELECT e1.name as source_name, e2.name as target_name
1306
+ FROM relationships r
1307
+ JOIN entities e1 ON r.source_entity = e1.id
1308
+ JOIN entities e2 ON r.target_entity = e2.id
1309
+ WHERE r.id = ?
1310
+ `).get(result.relationship_id);
1311
+ if (relEntities) {
1312
+ chunkEntities = [relEntities.source_name, relEntities.target_name];
1313
+ }
1314
+ }
1315
+ // Enhanced graph boost calculation
1316
+ let graphBoost = 0;
1317
+ if (useGraph) {
1318
+ const queryEntities = this.extractTermsFromText(query);
1319
+ // Base boost for knowledge graph chunks
1320
+ if (result.chunk_type === 'entity') {
1321
+ graphBoost += 0.15; // Entities are inherently valuable
1322
+ }
1323
+ else if (result.chunk_type === 'relationship') {
1324
+ graphBoost += 0.25; // Relationships show connections
1325
+ }
1326
+ // Additional boost for entity matches
1327
+ for (const entity of chunkEntities) {
1328
+ if (queryEntities.some(qe => qe.toLowerCase() === entity.toLowerCase())) {
1329
+ graphBoost += 0.3; // Higher boost for exact entity match
1330
+ }
1331
+ if (connectedEntities.has(entity)) {
1332
+ graphBoost += 0.15; // Higher boost for connected entity
1333
+ }
1334
+ }
1335
+ }
1336
+ // Generate semantic summary
1337
+ const { summary, keyHighlight, relevanceScore } = await this.generateContentSummary(result.text, queryEmbedding, chunkEntities, result.chunk_type === 'relationship' ? 1 : 2 // Shorter summary for relationships
1338
+ );
1339
+ const vectorSimilarity = 1 / (1 + result.distance);
1340
+ const finalScore = Math.max(vectorSimilarity, relevanceScore) + graphBoost;
1341
+ // Determine document title and source ID
1342
+ let documentTitle;
1343
+ let sourceId;
1344
+ if (result.chunk_type === 'document') {
1345
+ const metadata = JSON.parse(result.doc_metadata);
1346
+ documentTitle = metadata.title || metadata.name || result.document_id || 'Unknown Document';
1347
+ sourceId = result.document_id || '';
1348
+ }
1349
+ else if (result.chunk_type === 'entity') {
1350
+ documentTitle = 'Knowledge Graph Entity';
1351
+ sourceId = result.entity_id || '';
1352
+ }
1353
+ else if (result.chunk_type === 'relationship') {
1354
+ documentTitle = 'Knowledge Graph Relationship';
1355
+ sourceId = result.relationship_id || '';
1356
+ }
1357
+ else {
1358
+ documentTitle = 'Unknown Source';
1359
+ sourceId = '';
1360
+ }
1361
+ enhancedResults.push({
1362
+ relevance_score: finalScore,
1363
+ key_highlight: keyHighlight,
1364
+ content_summary: summary,
1365
+ chunk_id: result.chunk_id,
1366
+ document_title: documentTitle,
1367
+ entities: chunkEntities,
1368
+ vector_similarity: vectorSimilarity,
1369
+ graph_boost: useGraph ? graphBoost : undefined,
1370
+ full_context_available: true,
1371
+ chunk_type: result.chunk_type,
1372
+ source_id: sourceId
1373
+ });
1374
+ }
1375
+ // Sort by relevance and return top results
1376
+ const finalResults = enhancedResults
1377
+ .sort((a, b) => b.relevance_score - a.relevance_score)
1378
+ .slice(0, limit);
1379
+ // Log search statistics
1380
+ const docResults = finalResults.filter(r => r.chunk_type === 'document').length;
1381
+ const entityResults = finalResults.filter(r => r.chunk_type === 'entity').length;
1382
+ const relResults = finalResults.filter(r => r.chunk_type === 'relationship').length;
1383
+ console.error(`โœ… Enhanced hybrid search completed: ${finalResults.length} results (${docResults} docs, ${entityResults} entities, ${relResults} relationships)`);
1384
+ return finalResults;
1385
+ }
1386
+ // NEW: Get detailed context for a specific chunk
1387
+ async getDetailedContext(chunkId, includeSurrounding = true) {
1388
+ if (!this.db)
1389
+ throw new Error('Database not initialized');
1390
+ console.error(`๐Ÿ“– Getting detailed context for chunk: ${chunkId}`);
1391
+ // Get the main chunk
1392
+ const chunk = this.db.prepare(`
1393
+ SELECT
1394
+ m.chunk_id,
1395
+ m.document_id,
1396
+ m.chunk_index,
1397
+ m.text,
1398
+ d.content as doc_content,
1399
+ d.metadata as doc_metadata
1400
+ FROM chunk_metadata m
1401
+ JOIN documents d ON m.document_id = d.id
1402
+ WHERE m.chunk_id = ?
1403
+ `).get(chunkId);
1404
+ if (!chunk) {
1405
+ throw new Error(`Chunk with ID ${chunkId} not found`);
1406
+ }
1407
+ // Get entities for this chunk
1408
+ const entities = this.db.prepare(`
1409
+ SELECT e.name
1410
+ FROM chunk_entities ce
1411
+ JOIN chunk_metadata m ON ce.chunk_rowid = m.rowid
1412
+ JOIN entities e ON e.id = ce.entity_id
1413
+ WHERE m.chunk_id = ?
1414
+ `).all(chunkId).map((row) => row.name);
1415
+ let surroundingChunks = [];
1416
+ if (includeSurrounding) {
1417
+ // Get preceding and following chunks from the same document
1418
+ const beforeChunk = this.db.prepare(`
1419
+ SELECT chunk_id, text
1420
+ FROM chunk_metadata
1421
+ WHERE document_id = ? AND chunk_index = ?
1422
+ `).get(chunk.document_id, chunk.chunk_index - 1);
1423
+ const afterChunk = this.db.prepare(`
1424
+ SELECT chunk_id, text
1425
+ FROM chunk_metadata
1426
+ WHERE document_id = ? AND chunk_index = ?
1427
+ `).get(chunk.document_id, chunk.chunk_index + 1);
1428
+ if (beforeChunk) {
1429
+ surroundingChunks.push({
1430
+ chunk_id: beforeChunk.chunk_id,
1431
+ text: beforeChunk.text,
1432
+ position: 'before'
1433
+ });
1434
+ }
1435
+ if (afterChunk) {
1436
+ surroundingChunks.push({
1437
+ chunk_id: afterChunk.chunk_id,
1438
+ text: afterChunk.text,
1439
+ position: 'after'
1440
+ });
1441
+ }
1442
+ }
1443
+ const metadata = JSON.parse(chunk.doc_metadata);
1444
+ const documentTitle = metadata.title || metadata.name || chunk.document_id;
1445
+ console.error(`โœ… Retrieved detailed context with ${surroundingChunks.length} surrounding chunks`);
1446
+ return {
1447
+ chunk_id: chunk.chunk_id,
1448
+ document_id: chunk.document_id,
1449
+ full_text: chunk.text,
1450
+ document_title: documentTitle,
1451
+ surrounding_chunks: surroundingChunks.length > 0 ? surroundingChunks : undefined,
1452
+ entities: entities,
1453
+ metadata: metadata
1454
+ };
1455
+ }
1456
+ async getKnowledgeGraphStats() {
1457
+ if (!this.db)
1458
+ throw new Error('Database not initialized');
1459
+ const entityStats = this.db.prepare(`
1460
+ SELECT entityType, COUNT(*) as count
1461
+ FROM entities
1462
+ GROUP BY entityType
1463
+ `).all();
1464
+ const relationshipStats = this.db.prepare(`
1465
+ SELECT relationType, COUNT(*) as count
1466
+ FROM relationships
1467
+ GROUP BY relationType
1468
+ `).all();
1469
+ const documentCount = this.db.prepare(`
1470
+ SELECT COUNT(*) as count FROM documents
1471
+ `).get();
1472
+ const chunkCount = this.db.prepare(`
1473
+ SELECT COUNT(*) as count FROM chunk_metadata
1474
+ `).get();
1475
+ return {
1476
+ entities: {
1477
+ total: entityStats.reduce((sum, stat) => sum + stat.count, 0),
1478
+ by_type: Object.fromEntries(entityStats.map(s => [s.entityType, s.count]))
1479
+ },
1480
+ relationships: {
1481
+ total: relationshipStats.reduce((sum, stat) => sum + stat.count, 0),
1482
+ by_type: Object.fromEntries(relationshipStats.map(s => [s.relationType, s.count]))
1483
+ },
1484
+ documents: documentCount.count,
1485
+ chunks: chunkCount.count
1486
+ };
1487
+ }
1488
+ // === MIGRATION TOOLS ===
1489
+ async getMigrationStatus() {
1490
+ if (!this.db)
1491
+ throw new Error('Database not initialized');
1492
+ const migrationManager = new MigrationManager(this.db);
1493
+ // Add all migrations
1494
+ migrations.forEach(migration => {
1495
+ migrationManager.addMigration(migration);
1496
+ });
1497
+ const currentVersion = migrationManager.getCurrentVersion();
1498
+ const allMigrations = migrationManager.listMigrations();
1499
+ const pendingCount = allMigrations.filter(m => !m.applied).length;
1500
+ return {
1501
+ currentVersion,
1502
+ migrations: allMigrations,
1503
+ pendingCount
1504
+ };
1505
+ }
1506
+ async rollbackMigration(targetVersion) {
1507
+ if (!this.db)
1508
+ throw new Error('Database not initialized');
1509
+ const migrationManager = new MigrationManager(this.db);
1510
+ // Add all migrations
1511
+ migrations.forEach(migration => {
1512
+ migrationManager.addMigration(migration);
1513
+ });
1514
+ const currentVersion = migrationManager.getCurrentVersion();
1515
+ if (targetVersion >= currentVersion) {
1516
+ return {
1517
+ rolledBack: 0,
1518
+ currentVersion,
1519
+ rolledBackMigrations: []
1520
+ };
1521
+ }
1522
+ const migrationsToRollback = migrations
1523
+ .filter(m => m.version > targetVersion && m.version <= currentVersion)
1524
+ .sort((a, b) => b.version - a.version);
1525
+ migrationManager.rollback(targetVersion);
1526
+ return {
1527
+ rolledBack: migrationsToRollback.length,
1528
+ currentVersion: migrationManager.getCurrentVersion(),
1529
+ rolledBackMigrations: migrationsToRollback.map(m => ({
1530
+ version: m.version,
1531
+ description: m.description
1532
+ }))
1533
+ };
1534
+ }
1535
+ }
1536
+ // Initialize the manager
1537
+ const ragKgManager = new RAGKnowledgeGraphManager();
1538
+ // MCP Server setup
1539
+ const server = new Server({
1540
+ name: "rag-memory-server",
1541
+ version: "1.0.0",
1542
+ }, {
1543
+ capabilities: {
1544
+ tools: {},
1545
+ },
1546
+ });
1547
+ // Use our new structured tool system for listing tools
1548
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
1549
+ const tools = getAllMCPTools();
1550
+ console.error(`๐Ÿ“‹ Serving ${tools.length} tools with comprehensive documentation`);
1551
+ return { tools };
1552
+ });
1553
+ // Enhanced tool call handler with validation
1554
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1555
+ const { name, arguments: args } = request.params;
1556
+ if (!args) {
1557
+ throw new Error(`No arguments provided for tool: ${name}`);
1558
+ }
1559
+ try {
1560
+ // Validate arguments using our structured schema
1561
+ const validatedArgs = validateToolArgs(name, args);
1562
+ switch (name) {
1563
+ // Original MCP tools
1564
+ case "createEntities":
1565
+ return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.createEntities(validatedArgs.entities), null, 2) }] };
1566
+ case "createRelations":
1567
+ return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.createRelations(validatedArgs.relations), null, 2) }] };
1568
+ case "addObservations":
1569
+ return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.addObservations(validatedArgs.observations), null, 2) }] };
1570
+ case "deleteEntities":
1571
+ await ragKgManager.deleteEntities(validatedArgs.entityNames);
1572
+ return { content: [{ type: "text", text: "Entities deleted successfully" }] };
1573
+ case "deleteObservations":
1574
+ await ragKgManager.deleteObservations(validatedArgs.deletions);
1575
+ return { content: [{ type: "text", text: "Observations deleted successfully" }] };
1576
+ case "deleteRelations":
1577
+ await ragKgManager.deleteRelations(validatedArgs.relations);
1578
+ return { content: [{ type: "text", text: "Relations deleted successfully" }] };
1579
+ case "readGraph":
1580
+ return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.readGraph(), null, 2) }] };
1581
+ case "searchNodes":
1582
+ return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.searchNodes(validatedArgs.query, validatedArgs.limit || 10), null, 2) }] };
1583
+ case "openNodes":
1584
+ return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.openNodes(validatedArgs.names), null, 2) }] };
1585
+ // New RAG tools
1586
+ case "storeDocument":
1587
+ return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.storeDocument(validatedArgs.id, validatedArgs.content, validatedArgs.metadata || {}), null, 2) }] };
1588
+ case "chunkDocument":
1589
+ return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.chunkDocument(validatedArgs.documentId, { maxTokens: validatedArgs.maxTokens, overlap: validatedArgs.overlap }), null, 2) }] };
1590
+ case "embedChunks":
1591
+ return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.embedChunks(validatedArgs.documentId), null, 2) }] };
1592
+ case "extractTerms":
1593
+ return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.extractTerms(validatedArgs.documentId, { minLength: validatedArgs.minLength, includeCapitalized: validatedArgs.includeCapitalized, customPatterns: validatedArgs.customPatterns }), null, 2) }] };
1594
+ case "linkEntitiesToDocument":
1595
+ return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.linkEntitiesToDocument(validatedArgs.documentId, validatedArgs.entityNames), null, 2) }] };
1596
+ case "hybridSearch":
1597
+ const limit = typeof validatedArgs.limit === 'number' ? validatedArgs.limit : 5;
1598
+ const useGraph = validatedArgs.useGraph !== false;
1599
+ return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.hybridSearch(validatedArgs.query, limit, useGraph), null, 2) }] };
1600
+ case "getDetailedContext":
1601
+ return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.getDetailedContext(validatedArgs.chunkId, validatedArgs.includeSurrounding !== false), null, 2) }] };
1602
+ case "getKnowledgeGraphStats":
1603
+ return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.getKnowledgeGraphStats(), null, 2) }] };
1604
+ case "deleteDocuments":
1605
+ return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.deleteDocuments(validatedArgs.documentIds), null, 2) }] };
1606
+ case "listDocuments":
1607
+ return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.listDocuments(validatedArgs.includeMetadata !== false), null, 2) }] };
1608
+ // NEW: Entity embedding tools
1609
+ case "embedAllEntities":
1610
+ return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.embedAllEntities(), null, 2) }] };
1611
+ // NEW: Migration tools
1612
+ case "getMigrationStatus":
1613
+ return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.getMigrationStatus(), null, 2) }] };
1614
+ case "runMigrations":
1615
+ return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.runMigrations(), null, 2) }] };
1616
+ case "rollbackMigration":
1617
+ return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.rollbackMigration(validatedArgs.targetVersion), null, 2) }] };
1618
+ default:
1619
+ throw new Error(`Unknown tool: ${name}`);
1620
+ }
1621
+ }
1622
+ catch (error) {
1623
+ if (error instanceof Error) {
1624
+ console.error(`โŒ Tool execution error for ${name}:`, error.message);
1625
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
1626
+ }
1627
+ throw error;
1628
+ }
1629
+ });
1630
+ async function main() {
1631
+ try {
1632
+ await ragKgManager.initialize();
1633
+ const transport = new StdioServerTransport();
1634
+ await server.connect(transport);
1635
+ console.error("๐Ÿš€ Enhanced RAG Knowledge Graph MCP Server running on stdio");
1636
+ // Cleanup on exit
1637
+ process.on('SIGINT', () => {
1638
+ console.error('\n๐Ÿงน Cleaning up...');
1639
+ ragKgManager.cleanup();
1640
+ process.exit(0);
1641
+ });
1642
+ }
1643
+ catch (error) {
1644
+ console.error("Failed to initialize server:", error);
1645
+ ragKgManager.cleanup();
1646
+ process.exit(1);
1647
+ }
1648
+ }
1649
+ main().catch((error) => {
1650
+ console.error("Fatal error in main():", error);
1651
+ ragKgManager.cleanup();
1652
+ process.exit(1);
1653
+ });
1654
+ //# sourceMappingURL=index.js.map