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.
- package/README.md +292 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1654 -0
- package/dist/index.js.map +1 -0
- package/dist/src/migrations/migration-manager.d.ts +28 -0
- package/dist/src/migrations/migration-manager.d.ts.map +1 -0
- package/dist/src/migrations/migration-manager.js +111 -0
- package/dist/src/migrations/migration-manager.js.map +1 -0
- package/dist/src/migrations/migrations.d.ts +3 -0
- package/dist/src/migrations/migrations.d.ts.map +1 -0
- package/dist/src/migrations/migrations.js +181 -0
- package/dist/src/migrations/migrations.js.map +1 -0
- package/dist/src/tools/graph-query-tools.d.ts +16 -0
- package/dist/src/tools/graph-query-tools.d.ts.map +1 -0
- package/dist/src/tools/graph-query-tools.js +452 -0
- package/dist/src/tools/graph-query-tools.js.map +1 -0
- package/dist/src/tools/knowledge-graph-tools.d.ts +16 -0
- package/dist/src/tools/knowledge-graph-tools.d.ts.map +1 -0
- package/dist/src/tools/knowledge-graph-tools.js +482 -0
- package/dist/src/tools/knowledge-graph-tools.js.map +1 -0
- package/dist/src/tools/migration-tools.d.ts +3 -0
- package/dist/src/tools/migration-tools.d.ts.map +1 -0
- package/dist/src/tools/migration-tools.js +172 -0
- package/dist/src/tools/migration-tools.js.map +1 -0
- package/dist/src/tools/rag-tools.d.ts +20 -0
- package/dist/src/tools/rag-tools.d.ts.map +1 -0
- package/dist/src/tools/rag-tools.js +524 -0
- package/dist/src/tools/rag-tools.js.map +1 -0
- package/dist/src/tools/tool-registry.d.ts +82 -0
- package/dist/src/tools/tool-registry.d.ts.map +1 -0
- package/dist/src/tools/tool-registry.js +185 -0
- package/dist/src/tools/tool-registry.js.map +1 -0
- package/dist/src/tools/types.d.ts +34 -0
- package/dist/src/tools/types.d.ts.map +1 -0
- package/dist/src/tools/types.js +2 -0
- package/dist/src/tools/types.js.map +1 -0
- 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
|