rag-lite-ts 2.0.4 → 2.1.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 +815 -808
- package/dist/cli/indexer.js +2 -38
- package/dist/cli/search.d.ts +1 -1
- package/dist/cli/search.js +118 -9
- package/dist/cli.js +77 -94
- package/dist/config.js +3 -0
- package/dist/core/database-connection-manager.js +5 -9
- package/dist/core/db.js +173 -173
- package/dist/core/ingestion.js +50 -9
- package/dist/core/lazy-dependency-loader.d.ts +3 -8
- package/dist/core/lazy-dependency-loader.js +11 -29
- package/dist/core/mode-detection-service.js +1 -1
- package/dist/core/reranking-config.d.ts +1 -1
- package/dist/core/reranking-config.js +7 -16
- package/dist/core/reranking-factory.js +3 -184
- package/dist/core/reranking-strategies.js +5 -4
- package/dist/core/search.d.ts +10 -0
- package/dist/core/search.js +34 -11
- package/dist/factories/ingestion-factory.js +3 -1
- package/dist/mcp-server.js +147 -120
- package/dist/multimodal/clip-embedder.js +70 -71
- package/package.json +105 -105
package/dist/mcp-server.js
CHANGED
|
@@ -24,13 +24,29 @@
|
|
|
24
24
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
25
25
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
26
26
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
27
|
-
import { existsSync, statSync } from 'fs';
|
|
27
|
+
import { existsSync, statSync, createWriteStream } from 'fs';
|
|
28
28
|
import { resolve } from 'path';
|
|
29
29
|
import { SearchFactory } from './factories/search-factory.js';
|
|
30
30
|
import { IngestionFactory } from './factories/ingestion-factory.js';
|
|
31
|
-
import {
|
|
31
|
+
import { getSystemInfo } from './core/db.js';
|
|
32
32
|
import { DatabaseConnectionManager } from './core/database-connection-manager.js';
|
|
33
33
|
import { config, validateCoreConfig, ConfigurationError } from './core/config.js';
|
|
34
|
+
/**
|
|
35
|
+
* Detect MIME type from file path or extension
|
|
36
|
+
*/
|
|
37
|
+
function getMimeTypeFromPath(filePath) {
|
|
38
|
+
const ext = filePath.toLowerCase().split('.').pop() || '';
|
|
39
|
+
const mimeTypes = {
|
|
40
|
+
'jpg': 'image/jpeg',
|
|
41
|
+
'jpeg': 'image/jpeg',
|
|
42
|
+
'png': 'image/png',
|
|
43
|
+
'gif': 'image/gif',
|
|
44
|
+
'webp': 'image/webp',
|
|
45
|
+
'bmp': 'image/bmp',
|
|
46
|
+
'svg': 'image/svg+xml'
|
|
47
|
+
};
|
|
48
|
+
return mimeTypes[ext] || 'image/jpeg'; // Default to JPEG if unknown
|
|
49
|
+
}
|
|
34
50
|
/**
|
|
35
51
|
* MCP Server class that wraps RAG-lite TS functionality
|
|
36
52
|
* Implements MCP protocol interface without creating REST/GraphQL endpoints
|
|
@@ -118,8 +134,8 @@ class RagLiteMCPServer {
|
|
|
118
134
|
},
|
|
119
135
|
rerank_strategy: {
|
|
120
136
|
type: 'string',
|
|
121
|
-
description: 'Reranking strategy for multimodal mode. Options: text-derived (default),
|
|
122
|
-
enum: ['text-derived', '
|
|
137
|
+
description: 'Reranking strategy for multimodal mode. Options: text-derived (default), disabled',
|
|
138
|
+
enum: ['text-derived', 'disabled']
|
|
123
139
|
},
|
|
124
140
|
force_rebuild: {
|
|
125
141
|
type: 'boolean',
|
|
@@ -152,8 +168,8 @@ class RagLiteMCPServer {
|
|
|
152
168
|
},
|
|
153
169
|
rerank_strategy: {
|
|
154
170
|
type: 'string',
|
|
155
|
-
description: 'Reranking strategy for multimodal mode. Options: text-derived (default),
|
|
156
|
-
enum: ['text-derived', '
|
|
171
|
+
description: 'Reranking strategy for multimodal mode. Options: text-derived (default), disabled',
|
|
172
|
+
enum: ['text-derived', 'disabled'],
|
|
157
173
|
default: 'text-derived'
|
|
158
174
|
},
|
|
159
175
|
title: {
|
|
@@ -375,50 +391,60 @@ class RagLiteMCPServer {
|
|
|
375
391
|
const startTime = Date.now();
|
|
376
392
|
const results = await this.searchEngine.search(args.query, searchOptions);
|
|
377
393
|
const searchTime = Date.now() - startTime;
|
|
378
|
-
// Format results for MCP response with content
|
|
379
|
-
const
|
|
394
|
+
// Format results for MCP response with proper image content support
|
|
395
|
+
const textResults = {
|
|
380
396
|
query: args.query,
|
|
381
397
|
results_count: results.length,
|
|
382
398
|
search_time_ms: searchTime,
|
|
383
|
-
results:
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
if (result.contentType === 'image' && result.document.contentId) {
|
|
398
|
-
try {
|
|
399
|
-
const imageData = await this.searchEngine.getContent(result.document.contentId, 'base64');
|
|
400
|
-
formattedResult.image_data = imageData;
|
|
401
|
-
formattedResult.image_format = 'base64';
|
|
402
|
-
}
|
|
403
|
-
catch (error) {
|
|
404
|
-
// If image retrieval fails, include error but don't fail the entire search
|
|
405
|
-
formattedResult.image_error = error instanceof Error ? error.message : 'Failed to retrieve image';
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
// Include metadata if available
|
|
409
|
-
if (result.metadata) {
|
|
410
|
-
formattedResult.metadata = result.metadata;
|
|
411
|
-
}
|
|
412
|
-
return formattedResult;
|
|
399
|
+
results: results.map((result, index) => ({
|
|
400
|
+
rank: index + 1,
|
|
401
|
+
score: Math.round(result.score * 100) / 100,
|
|
402
|
+
content_type: result.contentType,
|
|
403
|
+
document: {
|
|
404
|
+
id: result.document.id,
|
|
405
|
+
title: result.document.title,
|
|
406
|
+
source: result.document.source,
|
|
407
|
+
content_type: result.document.contentType
|
|
408
|
+
},
|
|
409
|
+
text: result.content,
|
|
410
|
+
metadata: result.metadata,
|
|
411
|
+
// Reference to image content if applicable
|
|
412
|
+
has_image: result.contentType === 'image' && !!result.document.contentId
|
|
413
413
|
}))
|
|
414
414
|
};
|
|
415
|
+
// Build MCP response content array
|
|
416
|
+
const responseContent = [
|
|
417
|
+
{
|
|
418
|
+
type: 'text',
|
|
419
|
+
text: JSON.stringify(textResults, null, 2)
|
|
420
|
+
}
|
|
421
|
+
];
|
|
422
|
+
// Add proper MCP image content for each image result
|
|
423
|
+
for (const result of results) {
|
|
424
|
+
if (result.contentType === 'image' && result.document.contentId) {
|
|
425
|
+
try {
|
|
426
|
+
const imageData = await this.searchEngine.getContent(result.document.contentId, 'base64');
|
|
427
|
+
const mimeType = getMimeTypeFromPath(result.document.source);
|
|
428
|
+
responseContent.push({
|
|
429
|
+
type: 'image',
|
|
430
|
+
data: imageData,
|
|
431
|
+
mimeType: mimeType,
|
|
432
|
+
annotations: {
|
|
433
|
+
audience: ['user'],
|
|
434
|
+
priority: 0.8,
|
|
435
|
+
title: result.document.title,
|
|
436
|
+
source: result.document.source
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
// If image retrieval fails, log but don't fail the entire search
|
|
442
|
+
console.error(`Failed to retrieve image for ${result.document.source}:`, error);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
415
446
|
return {
|
|
416
|
-
content:
|
|
417
|
-
{
|
|
418
|
-
type: 'text',
|
|
419
|
-
text: JSON.stringify(formattedResults, null, 2),
|
|
420
|
-
},
|
|
421
|
-
],
|
|
447
|
+
content: responseContent
|
|
422
448
|
};
|
|
423
449
|
}
|
|
424
450
|
catch (error) {
|
|
@@ -549,7 +575,7 @@ class RagLiteMCPServer {
|
|
|
549
575
|
if (mode === 'text') {
|
|
550
576
|
throw new Error('Reranking strategy parameter is only supported in multimodal mode');
|
|
551
577
|
}
|
|
552
|
-
const validStrategies = ['text-derived', '
|
|
578
|
+
const validStrategies = ['text-derived', 'disabled'];
|
|
553
579
|
if (!validStrategies.includes(args.rerank_strategy)) {
|
|
554
580
|
throw new Error(`Invalid reranking strategy: ${args.rerank_strategy}. Supported strategies: ${validStrategies.join(', ')}`);
|
|
555
581
|
}
|
|
@@ -746,7 +772,7 @@ class RagLiteMCPServer {
|
|
|
746
772
|
reject(new Error(`Failed to download image: HTTP ${redirectResponse.statusCode}`));
|
|
747
773
|
return;
|
|
748
774
|
}
|
|
749
|
-
const fileStream =
|
|
775
|
+
const fileStream = createWriteStream(tempFilePath);
|
|
750
776
|
redirectResponse.pipe(fileStream);
|
|
751
777
|
fileStream.on('finish', () => {
|
|
752
778
|
fileStream.close();
|
|
@@ -761,7 +787,7 @@ class RagLiteMCPServer {
|
|
|
761
787
|
return;
|
|
762
788
|
}
|
|
763
789
|
else {
|
|
764
|
-
const fileStream =
|
|
790
|
+
const fileStream = createWriteStream(tempFilePath);
|
|
765
791
|
response.pipe(fileStream);
|
|
766
792
|
fileStream.on('finish', () => {
|
|
767
793
|
fileStream.close();
|
|
@@ -916,8 +942,8 @@ class RagLiteMCPServer {
|
|
|
916
942
|
// Create ingestion pipeline with force rebuild using factory
|
|
917
943
|
const pipeline = await IngestionFactory.create(config.db_file, config.index_file, { forceRebuild: true });
|
|
918
944
|
try {
|
|
919
|
-
// Get all documents from database and re-ingest them
|
|
920
|
-
const db = await
|
|
945
|
+
// Get all documents from database and re-ingest them - use shared connection
|
|
946
|
+
const db = await DatabaseConnectionManager.getConnection(config.db_file);
|
|
921
947
|
try {
|
|
922
948
|
const documents = await db.all('SELECT DISTINCT source FROM documents ORDER BY source');
|
|
923
949
|
if (documents.length === 0) {
|
|
@@ -971,7 +997,8 @@ class RagLiteMCPServer {
|
|
|
971
997
|
};
|
|
972
998
|
}
|
|
973
999
|
finally {
|
|
974
|
-
|
|
1000
|
+
// Release instead of close - keeps connection alive for reuse
|
|
1001
|
+
await DatabaseConnectionManager.releaseConnection(config.db_file);
|
|
975
1002
|
}
|
|
976
1003
|
}
|
|
977
1004
|
finally {
|
|
@@ -1009,7 +1036,7 @@ class RagLiteMCPServer {
|
|
|
1009
1036
|
// Check model compatibility if database exists
|
|
1010
1037
|
if (stats.database_exists) {
|
|
1011
1038
|
try {
|
|
1012
|
-
const db = await
|
|
1039
|
+
const db = await DatabaseConnectionManager.getConnection(config.db_file);
|
|
1013
1040
|
try {
|
|
1014
1041
|
const systemInfo = await getSystemInfo(db);
|
|
1015
1042
|
if (systemInfo && systemInfo.modelName && systemInfo.modelDimensions) {
|
|
@@ -1040,7 +1067,8 @@ class RagLiteMCPServer {
|
|
|
1040
1067
|
stats.total_chunks = chunkCount?.count || 0;
|
|
1041
1068
|
}
|
|
1042
1069
|
finally {
|
|
1043
|
-
|
|
1070
|
+
// Release instead of close - keeps connection alive for reuse
|
|
1071
|
+
await DatabaseConnectionManager.releaseConnection(config.db_file);
|
|
1044
1072
|
}
|
|
1045
1073
|
}
|
|
1046
1074
|
catch (error) {
|
|
@@ -1221,48 +1249,61 @@ class RagLiteMCPServer {
|
|
|
1221
1249
|
const startTime = Date.now();
|
|
1222
1250
|
const results = await this.searchEngine.search(args.query, searchOptions);
|
|
1223
1251
|
const searchTime = Date.now() - startTime;
|
|
1224
|
-
// Format results for MCP response with
|
|
1225
|
-
const
|
|
1252
|
+
// Format results for MCP response with proper image content support
|
|
1253
|
+
const textResults = {
|
|
1226
1254
|
query: args.query,
|
|
1227
1255
|
content_type_filter: args.content_type || 'all',
|
|
1228
1256
|
results_count: results.length,
|
|
1229
1257
|
search_time_ms: searchTime,
|
|
1230
|
-
results:
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
// For image content, include base64-encoded image data for MCP clients
|
|
1245
|
-
if (result.contentType === 'image' && result.document.contentId) {
|
|
1246
|
-
try {
|
|
1247
|
-
const imageData = await this.searchEngine.getContent(result.document.contentId, 'base64');
|
|
1248
|
-
formattedResult.image_data = imageData;
|
|
1249
|
-
formattedResult.image_format = 'base64';
|
|
1250
|
-
}
|
|
1251
|
-
catch (error) {
|
|
1252
|
-
// If image retrieval fails, include error but don't fail the entire search
|
|
1253
|
-
formattedResult.image_error = error instanceof Error ? error.message : 'Failed to retrieve image';
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
return formattedResult;
|
|
1258
|
+
results: results.map((result, index) => ({
|
|
1259
|
+
rank: index + 1,
|
|
1260
|
+
score: Math.round(result.score * 100) / 100,
|
|
1261
|
+
content_type: result.contentType,
|
|
1262
|
+
document: {
|
|
1263
|
+
id: result.document.id,
|
|
1264
|
+
title: result.document.title,
|
|
1265
|
+
source: result.document.source,
|
|
1266
|
+
content_type: result.document.contentType
|
|
1267
|
+
},
|
|
1268
|
+
text: result.content,
|
|
1269
|
+
metadata: result.metadata,
|
|
1270
|
+
// Reference to image content if applicable
|
|
1271
|
+
has_image: result.contentType === 'image' && !!result.document.contentId
|
|
1257
1272
|
}))
|
|
1258
1273
|
};
|
|
1274
|
+
// Build MCP response content array
|
|
1275
|
+
const responseContent = [
|
|
1276
|
+
{
|
|
1277
|
+
type: 'text',
|
|
1278
|
+
text: JSON.stringify(textResults, null, 2)
|
|
1279
|
+
}
|
|
1280
|
+
];
|
|
1281
|
+
// Add proper MCP image content for each image result
|
|
1282
|
+
for (const result of results) {
|
|
1283
|
+
if (result.contentType === 'image' && result.document.contentId) {
|
|
1284
|
+
try {
|
|
1285
|
+
const imageData = await this.searchEngine.getContent(result.document.contentId, 'base64');
|
|
1286
|
+
const mimeType = getMimeTypeFromPath(result.document.source);
|
|
1287
|
+
responseContent.push({
|
|
1288
|
+
type: 'image',
|
|
1289
|
+
data: imageData,
|
|
1290
|
+
mimeType: mimeType,
|
|
1291
|
+
annotations: {
|
|
1292
|
+
audience: ['user'],
|
|
1293
|
+
priority: 0.8,
|
|
1294
|
+
title: result.document.title,
|
|
1295
|
+
source: result.document.source
|
|
1296
|
+
}
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
catch (error) {
|
|
1300
|
+
// If image retrieval fails, log but don't fail the entire search
|
|
1301
|
+
console.error(`Failed to retrieve image for ${result.document.source}:`, error);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1259
1305
|
return {
|
|
1260
|
-
content:
|
|
1261
|
-
{
|
|
1262
|
-
type: 'text',
|
|
1263
|
-
text: JSON.stringify(formattedResults, null, 2),
|
|
1264
|
-
},
|
|
1265
|
-
],
|
|
1306
|
+
content: responseContent
|
|
1266
1307
|
};
|
|
1267
1308
|
}
|
|
1268
1309
|
catch (error) {
|
|
@@ -1416,23 +1457,6 @@ class RagLiteMCPServer {
|
|
|
1416
1457
|
strategyInfo.accuracy = 'high';
|
|
1417
1458
|
strategyInfo.use_cases = ['Mixed content with images', 'Visual documentation', 'Diagrams and charts'];
|
|
1418
1459
|
break;
|
|
1419
|
-
case 'metadata':
|
|
1420
|
-
strategyInfo.description = 'Uses file metadata, filenames, and content properties for scoring without model inference';
|
|
1421
|
-
strategyInfo.requirements = ['None - uses file system metadata only'];
|
|
1422
|
-
strategyInfo.supported_content_types = ['text', 'image', 'pdf', 'docx'];
|
|
1423
|
-
strategyInfo.performance_impact = 'low';
|
|
1424
|
-
strategyInfo.accuracy = 'medium';
|
|
1425
|
-
strategyInfo.use_cases = ['Fast retrieval', 'Filename-based search', 'Content type filtering'];
|
|
1426
|
-
break;
|
|
1427
|
-
case 'hybrid':
|
|
1428
|
-
strategyInfo.description = 'Combines multiple reranking signals (semantic + metadata) with configurable weights';
|
|
1429
|
-
strategyInfo.requirements = ['Text-derived reranker', 'Metadata reranker'];
|
|
1430
|
-
strategyInfo.supported_content_types = ['text', 'image', 'pdf', 'docx'];
|
|
1431
|
-
strategyInfo.performance_impact = 'high';
|
|
1432
|
-
strategyInfo.accuracy = 'very high';
|
|
1433
|
-
strategyInfo.use_cases = ['Best overall accuracy', 'Complex multimodal collections', 'Production systems'];
|
|
1434
|
-
strategyInfo.default_weights = { semantic: 0.7, metadata: 0.3 };
|
|
1435
|
-
break;
|
|
1436
1460
|
case 'disabled':
|
|
1437
1461
|
strategyInfo.description = 'No reranking applied - results ordered by vector similarity scores only';
|
|
1438
1462
|
strategyInfo.requirements = ['None'];
|
|
@@ -1453,8 +1477,8 @@ class RagLiteMCPServer {
|
|
|
1453
1477
|
strategies_by_mode: strategiesByMode,
|
|
1454
1478
|
recommendations: {
|
|
1455
1479
|
text_mode: 'Use cross-encoder for best accuracy, disabled for best performance',
|
|
1456
|
-
multimodal_mode: 'Use
|
|
1457
|
-
development: 'Start with disabled
|
|
1480
|
+
multimodal_mode: 'Use text-derived for best accuracy, disabled for best performance',
|
|
1481
|
+
development: 'Start with disabled for fast iteration, upgrade to cross-encoder/text-derived for production'
|
|
1458
1482
|
}
|
|
1459
1483
|
};
|
|
1460
1484
|
return {
|
|
@@ -1500,19 +1524,19 @@ class RagLiteMCPServer {
|
|
|
1500
1524
|
};
|
|
1501
1525
|
// Add content breakdown if requested
|
|
1502
1526
|
if (args.include_content_breakdown) {
|
|
1503
|
-
const db = await
|
|
1527
|
+
const db = await DatabaseConnectionManager.getConnection(config.db_file);
|
|
1504
1528
|
try {
|
|
1505
1529
|
// Get document count by content type
|
|
1506
|
-
const docsByType = await db.all(`
|
|
1507
|
-
SELECT content_type, COUNT(*) as count
|
|
1508
|
-
FROM documents
|
|
1509
|
-
GROUP BY content_type
|
|
1530
|
+
const docsByType = await db.all(`
|
|
1531
|
+
SELECT content_type, COUNT(*) as count
|
|
1532
|
+
FROM documents
|
|
1533
|
+
GROUP BY content_type
|
|
1510
1534
|
`);
|
|
1511
1535
|
// Get chunk count by content type
|
|
1512
|
-
const chunksByType = await db.all(`
|
|
1513
|
-
SELECT content_type, COUNT(*) as count
|
|
1514
|
-
FROM chunks
|
|
1515
|
-
GROUP BY content_type
|
|
1536
|
+
const chunksByType = await db.all(`
|
|
1537
|
+
SELECT content_type, COUNT(*) as count
|
|
1538
|
+
FROM chunks
|
|
1539
|
+
GROUP BY content_type
|
|
1516
1540
|
`);
|
|
1517
1541
|
enhancedStats.content_breakdown = {
|
|
1518
1542
|
documents_by_type: docsByType.reduce((acc, row) => {
|
|
@@ -1526,7 +1550,8 @@ class RagLiteMCPServer {
|
|
|
1526
1550
|
};
|
|
1527
1551
|
}
|
|
1528
1552
|
finally {
|
|
1529
|
-
|
|
1553
|
+
// Release instead of close - keeps connection alive for reuse
|
|
1554
|
+
await DatabaseConnectionManager.releaseConnection(config.db_file);
|
|
1530
1555
|
}
|
|
1531
1556
|
}
|
|
1532
1557
|
// Add performance metrics if requested
|
|
@@ -1600,6 +1625,8 @@ class RagLiteMCPServer {
|
|
|
1600
1625
|
this.isSearchEngineInitialized = true;
|
|
1601
1626
|
}
|
|
1602
1627
|
catch (error) {
|
|
1628
|
+
console.error('❌ MCP Server: Search engine initialization failed');
|
|
1629
|
+
console.error(`❌ Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
1603
1630
|
// Check if this is a mode detection error
|
|
1604
1631
|
if (error instanceof Error && error.message.includes('mode detection')) {
|
|
1605
1632
|
console.error('⚠️ MCP Server: Mode detection failed, falling back to text mode');
|
|
@@ -1612,7 +1639,6 @@ class RagLiteMCPServer {
|
|
|
1612
1639
|
throw error;
|
|
1613
1640
|
}
|
|
1614
1641
|
// For other initialization errors, provide a generic wrapper
|
|
1615
|
-
console.error('❌ MCP Server: Search engine initialization failed');
|
|
1616
1642
|
throw new Error(`Failed to initialize search engine: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
1617
1643
|
}
|
|
1618
1644
|
}
|
|
@@ -1635,8 +1661,8 @@ class RagLiteMCPServer {
|
|
|
1635
1661
|
const { ModeDetectionService } = await import('./core/mode-detection-service.js');
|
|
1636
1662
|
const modeService = new ModeDetectionService(config.db_file);
|
|
1637
1663
|
const systemInfo = await modeService.detectMode();
|
|
1638
|
-
// Check if database has any images
|
|
1639
|
-
const db = await
|
|
1664
|
+
// Check if database has any images - use shared connection
|
|
1665
|
+
const db = await DatabaseConnectionManager.getConnection(config.db_file);
|
|
1640
1666
|
let hasImages = false;
|
|
1641
1667
|
let documentCount = 0;
|
|
1642
1668
|
try {
|
|
@@ -1646,7 +1672,8 @@ class RagLiteMCPServer {
|
|
|
1646
1672
|
documentCount = docCount?.count || 0;
|
|
1647
1673
|
}
|
|
1648
1674
|
finally {
|
|
1649
|
-
|
|
1675
|
+
// Release instead of close - keeps connection alive for reuse
|
|
1676
|
+
await DatabaseConnectionManager.releaseConnection(config.db_file);
|
|
1650
1677
|
}
|
|
1651
1678
|
return {
|
|
1652
1679
|
mode: systemInfo.mode,
|
|
@@ -338,76 +338,73 @@ export class CLIPEmbedder extends BaseUniversalEmbedder {
|
|
|
338
338
|
if (!this.textModel || !this.tokenizer) {
|
|
339
339
|
throw new Error('CLIP text model or tokenizer not initialized');
|
|
340
340
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
341
|
+
// Use the validated CLIPTextModelWithProjection approach (no pixel_values errors)
|
|
342
|
+
// Tokenize text with CLIP's requirements
|
|
343
|
+
// The tokenizer handles truncation at 77 TOKENS (not characters)
|
|
344
|
+
const tokens = await this.tokenizer(processedText, {
|
|
345
|
+
padding: true,
|
|
346
|
+
truncation: true,
|
|
347
|
+
max_length: 77, // CLIP's text sequence length limit (77 tokens)
|
|
348
|
+
return_tensors: 'pt'
|
|
349
|
+
});
|
|
350
|
+
// Log token information for debugging (only in development)
|
|
351
|
+
if (process.env.NODE_ENV === 'development') {
|
|
352
|
+
const tokenIds = tokens.input_ids?.data || [];
|
|
353
|
+
const actualTokenCount = Array.from(tokenIds).filter((id) => id !== 0).length;
|
|
354
|
+
if (actualTokenCount >= 77) {
|
|
355
|
+
console.warn(`Text truncated by tokenizer: "${processedText.substring(0, 50)}..." (truncated to 77 tokens)`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// Generate text embedding using CLIPTextModelWithProjection
|
|
359
|
+
const output = await this.textModel(tokens);
|
|
360
|
+
// Extract embedding from text_embeds (no pixel_values dependency)
|
|
361
|
+
const embedding = new Float32Array(output.text_embeds.data);
|
|
362
|
+
// Validate embedding dimensions and values
|
|
363
|
+
if (embedding.length !== this.dimensions) {
|
|
364
|
+
throw new Error(`CLIP embedding dimension mismatch: expected ${this.dimensions}, got ${embedding.length}`);
|
|
365
|
+
}
|
|
366
|
+
// Validate that all values are finite numbers
|
|
367
|
+
const invalidValues = Array.from(embedding).filter(val => !isFinite(val) || isNaN(val));
|
|
368
|
+
if (invalidValues.length > 0) {
|
|
369
|
+
throw new Error(`CLIP embedding contains ${invalidValues.length} invalid values`);
|
|
370
|
+
}
|
|
371
|
+
// Validate embedding quality - should not be all zeros
|
|
372
|
+
const nonZeroValues = Array.from(embedding).filter(val => Math.abs(val) > 1e-8);
|
|
373
|
+
if (nonZeroValues.length === 0) {
|
|
374
|
+
throw new Error('CLIP embedding is all zeros');
|
|
375
|
+
}
|
|
376
|
+
// Calculate embedding magnitude before normalization for quality assessment
|
|
377
|
+
const magnitudeBeforeNorm = Math.sqrt(Array.from(embedding).reduce((sum, val) => sum + val * val, 0));
|
|
378
|
+
if (magnitudeBeforeNorm < 1e-6) {
|
|
379
|
+
throw new Error(`CLIP embedding has critically low magnitude: ${magnitudeBeforeNorm.toExponential(3)}`);
|
|
380
|
+
}
|
|
381
|
+
// Apply L2-normalization (CLIP models are trained with normalized embeddings)
|
|
382
|
+
this.normalizeEmbedding(embedding);
|
|
383
|
+
// Verify normalization was successful
|
|
384
|
+
const magnitudeAfterNorm = Math.sqrt(Array.from(embedding).reduce((sum, val) => sum + val * val, 0));
|
|
385
|
+
if (Math.abs(magnitudeAfterNorm - 1.0) > 0.01) {
|
|
386
|
+
console.warn(`Warning: Embedding normalization may be imprecise (magnitude: ${magnitudeAfterNorm.toFixed(6)})`);
|
|
387
|
+
}
|
|
388
|
+
// Log text embedding generation
|
|
389
|
+
console.log(`[CLIP] Generated text embedding for: "${processedText.substring(0, 30)}${processedText.length > 30 ? '...' : ''}"`);
|
|
390
|
+
// Generate unique embedding ID
|
|
391
|
+
const embeddingId = this.generateEmbeddingId(processedText, 'text');
|
|
392
|
+
return {
|
|
393
|
+
embedding_id: embeddingId,
|
|
394
|
+
vector: embedding,
|
|
395
|
+
contentType: 'text',
|
|
396
|
+
metadata: {
|
|
397
|
+
originalText: text,
|
|
398
|
+
processedText: processedText,
|
|
399
|
+
textLength: processedText.length,
|
|
400
|
+
embeddingMagnitudeBeforeNorm: magnitudeBeforeNorm,
|
|
401
|
+
embeddingMagnitudeAfterNorm: magnitudeAfterNorm,
|
|
402
|
+
normalized: true,
|
|
403
|
+
modelName: this.modelName,
|
|
404
|
+
modelType: this.modelType,
|
|
405
|
+
dimensions: this.dimensions
|
|
388
406
|
}
|
|
389
|
-
|
|
390
|
-
const embeddingId = this.generateEmbeddingId(processedText, 'text');
|
|
391
|
-
return {
|
|
392
|
-
embedding_id: embeddingId,
|
|
393
|
-
vector: embedding,
|
|
394
|
-
contentType: 'text',
|
|
395
|
-
metadata: {
|
|
396
|
-
originalText: text,
|
|
397
|
-
processedText: processedText,
|
|
398
|
-
textLength: processedText.length,
|
|
399
|
-
embeddingMagnitudeBeforeNorm: magnitudeBeforeNorm,
|
|
400
|
-
embeddingMagnitudeAfterNorm: magnitudeAfterNorm,
|
|
401
|
-
normalized: true,
|
|
402
|
-
modelName: this.modelName,
|
|
403
|
-
modelType: this.modelType,
|
|
404
|
-
dimensions: this.dimensions
|
|
405
|
-
}
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
catch (error) {
|
|
409
|
-
throw error;
|
|
410
|
-
}
|
|
407
|
+
};
|
|
411
408
|
}
|
|
412
409
|
// =============================================================================
|
|
413
410
|
// IMAGE EMBEDDING METHODS
|
|
@@ -602,9 +599,11 @@ export class CLIPEmbedder extends BaseUniversalEmbedder {
|
|
|
602
599
|
const absolutePath = path.resolve(imagePath);
|
|
603
600
|
// Try to use Sharp for better Node.js support
|
|
604
601
|
try {
|
|
605
|
-
const
|
|
602
|
+
const sharpModule = await import('sharp');
|
|
603
|
+
const sharp = sharpModule.default;
|
|
604
|
+
sharp.concurrency(2);
|
|
606
605
|
// Use Sharp to load and get raw pixel data
|
|
607
|
-
const { data, info } = await sharp
|
|
606
|
+
const { data, info } = await sharp(absolutePath)
|
|
608
607
|
.resize(variant.imageSize, variant.imageSize, {
|
|
609
608
|
fit: 'cover',
|
|
610
609
|
position: 'center'
|