Spaces:
Paused
Paused
Deploy from GitHub Actions 2025-12-16_02-29-01
Browse files
apps/backend/src/index.ts
CHANGED
|
@@ -32,7 +32,7 @@ if (typeof global.Path2D === 'undefined') {
|
|
| 32 |
global.Path2D = class Path2D { };
|
| 33 |
}
|
| 34 |
|
| 35 |
-
const __dirname = typeof import.meta !== 'undefined' && import.meta.url
|
| 36 |
? new URL('.', import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1')
|
| 37 |
: process.cwd();
|
| 38 |
// Load .env from backend directory, or root if not found
|
|
@@ -174,7 +174,7 @@ async function startServer() {
|
|
| 174 |
// 🧠 NEURAL ORGANS INITIALIZATION
|
| 175 |
// Phase B: Knowledge Synthesis
|
| 176 |
// ============================================
|
| 177 |
-
|
| 178 |
// 1. Initialize Vector Service (Dark Matter)
|
| 179 |
vectorService.init().then(() => {
|
| 180 |
console.log('🌑 [DARK MATTER] Vector Engine Ready (384 dimensions)');
|
|
@@ -187,11 +187,11 @@ async function startServer() {
|
|
| 187 |
console.log('🐝 [HIVE] Neural Telepathy Bus Attached');
|
| 188 |
|
| 189 |
// 3. Start Knowledge Compiler (Brain) - Watch DropZone
|
| 190 |
-
const DROPZONE_PATH = process.env.DROPZONE_PATH ||
|
| 191 |
-
(process.platform === 'win32'
|
| 192 |
? 'C:\\Users\\claus\\Desktop\\WidgeTDC_DropZone'
|
| 193 |
: '/app/data/dropzone');
|
| 194 |
-
|
| 195 |
neuralCompiler.startWatching(DROPZONE_PATH).then(() => {
|
| 196 |
console.log(`📚 [BRAIN] Neural Compiler watching: ${DROPZONE_PATH}`);
|
| 197 |
}).catch(err => {
|
|
@@ -210,11 +210,11 @@ async function startServer() {
|
|
| 210 |
// 5. Start Prometheus Code Scanner (every hour)
|
| 211 |
const SCAN_INTERVAL = parseInt(process.env.PROMETHEUS_SCAN_INTERVAL || '3600000');
|
| 212 |
setTimeout(() => {
|
| 213 |
-
prometheus.scanAndPropose(path.join(__dirname, 'services')).catch(() => {});
|
| 214 |
}, 10000); // Initial scan after 10s
|
| 215 |
-
|
| 216 |
setInterval(() => {
|
| 217 |
-
prometheus.scanAndPropose(path.join(__dirname, 'services')).catch(() => {});
|
| 218 |
}, SCAN_INTERVAL);
|
| 219 |
console.log('🔥 [PROMETHEUS] Code Evolution Scanner Active');
|
| 220 |
|
|
@@ -407,7 +407,10 @@ async function startServer() {
|
|
| 407 |
vidensarkivBatchAddHandler,
|
| 408 |
vidensarkivGetRelatedHandler,
|
| 409 |
vidensarkivListHandler,
|
| 410 |
-
vidensarkivStatsHandler
|
|
|
|
|
|
|
|
|
|
| 411 |
} = await import('./mcp/toolHandlers.js');
|
| 412 |
|
| 413 |
mcpRegistry.registerTool('vidensarkiv.search', vidensarkivSearchHandler);
|
|
@@ -416,6 +419,9 @@ async function startServer() {
|
|
| 416 |
mcpRegistry.registerTool('vidensarkiv.get_related', vidensarkivGetRelatedHandler);
|
| 417 |
mcpRegistry.registerTool('vidensarkiv.list', vidensarkivListHandler);
|
| 418 |
mcpRegistry.registerTool('vidensarkiv.stats', vidensarkivStatsHandler);
|
|
|
|
|
|
|
|
|
|
| 419 |
|
| 420 |
// TaskRecorder Tools
|
| 421 |
const {
|
|
@@ -565,11 +571,11 @@ async function startServer() {
|
|
| 565 |
})();
|
| 566 |
|
| 567 |
// Step 4: Setup routes
|
| 568 |
-
|
| 569 |
// 🛡️ ANGEL PROXY: Security Shield on all API routes
|
| 570 |
app.use('/api', AngelProxy.cortexFirewall);
|
| 571 |
console.log('🛡️ [ANGEL] Cortex Firewall Active on /api/*');
|
| 572 |
-
|
| 573 |
app.use('/api/mcp', mcpRouter);
|
| 574 |
app.use('/api/mcp/autonomous', autonomousRouter);
|
| 575 |
app.use('/api/memory', memoryRouter);
|
|
|
|
| 32 |
global.Path2D = class Path2D { };
|
| 33 |
}
|
| 34 |
|
| 35 |
+
const __dirname = typeof import.meta !== 'undefined' && import.meta.url
|
| 36 |
? new URL('.', import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1')
|
| 37 |
: process.cwd();
|
| 38 |
// Load .env from backend directory, or root if not found
|
|
|
|
| 174 |
// 🧠 NEURAL ORGANS INITIALIZATION
|
| 175 |
// Phase B: Knowledge Synthesis
|
| 176 |
// ============================================
|
| 177 |
+
|
| 178 |
// 1. Initialize Vector Service (Dark Matter)
|
| 179 |
vectorService.init().then(() => {
|
| 180 |
console.log('🌑 [DARK MATTER] Vector Engine Ready (384 dimensions)');
|
|
|
|
| 187 |
console.log('🐝 [HIVE] Neural Telepathy Bus Attached');
|
| 188 |
|
| 189 |
// 3. Start Knowledge Compiler (Brain) - Watch DropZone
|
| 190 |
+
const DROPZONE_PATH = process.env.DROPZONE_PATH ||
|
| 191 |
+
(process.platform === 'win32'
|
| 192 |
? 'C:\\Users\\claus\\Desktop\\WidgeTDC_DropZone'
|
| 193 |
: '/app/data/dropzone');
|
| 194 |
+
|
| 195 |
neuralCompiler.startWatching(DROPZONE_PATH).then(() => {
|
| 196 |
console.log(`📚 [BRAIN] Neural Compiler watching: ${DROPZONE_PATH}`);
|
| 197 |
}).catch(err => {
|
|
|
|
| 210 |
// 5. Start Prometheus Code Scanner (every hour)
|
| 211 |
const SCAN_INTERVAL = parseInt(process.env.PROMETHEUS_SCAN_INTERVAL || '3600000');
|
| 212 |
setTimeout(() => {
|
| 213 |
+
prometheus.scanAndPropose(path.join(__dirname, 'services')).catch(() => { });
|
| 214 |
}, 10000); // Initial scan after 10s
|
| 215 |
+
|
| 216 |
setInterval(() => {
|
| 217 |
+
prometheus.scanAndPropose(path.join(__dirname, 'services')).catch(() => { });
|
| 218 |
}, SCAN_INTERVAL);
|
| 219 |
console.log('🔥 [PROMETHEUS] Code Evolution Scanner Active');
|
| 220 |
|
|
|
|
| 407 |
vidensarkivBatchAddHandler,
|
| 408 |
vidensarkivGetRelatedHandler,
|
| 409 |
vidensarkivListHandler,
|
| 410 |
+
vidensarkivStatsHandler,
|
| 411 |
+
vidensarkivReadHandler,
|
| 412 |
+
vidensarkivListFilesHandler,
|
| 413 |
+
vidensarkivReadFileHandler
|
| 414 |
} = await import('./mcp/toolHandlers.js');
|
| 415 |
|
| 416 |
mcpRegistry.registerTool('vidensarkiv.search', vidensarkivSearchHandler);
|
|
|
|
| 419 |
mcpRegistry.registerTool('vidensarkiv.get_related', vidensarkivGetRelatedHandler);
|
| 420 |
mcpRegistry.registerTool('vidensarkiv.list', vidensarkivListHandler);
|
| 421 |
mcpRegistry.registerTool('vidensarkiv.stats', vidensarkivStatsHandler);
|
| 422 |
+
mcpRegistry.registerTool('vidensarkiv.read', vidensarkivReadHandler);
|
| 423 |
+
mcpRegistry.registerTool('vidensarkiv.list_files', vidensarkivListFilesHandler);
|
| 424 |
+
mcpRegistry.registerTool('vidensarkiv.read_file', vidensarkivReadFileHandler);
|
| 425 |
|
| 426 |
// TaskRecorder Tools
|
| 427 |
const {
|
|
|
|
| 571 |
})();
|
| 572 |
|
| 573 |
// Step 4: Setup routes
|
| 574 |
+
|
| 575 |
// 🛡️ ANGEL PROXY: Security Shield on all API routes
|
| 576 |
app.use('/api', AngelProxy.cortexFirewall);
|
| 577 |
console.log('🛡️ [ANGEL] Cortex Firewall Active on /api/*');
|
| 578 |
+
|
| 579 |
app.use('/api/mcp', mcpRouter);
|
| 580 |
app.use('/api/mcp/autonomous', autonomousRouter);
|
| 581 |
app.use('/api/memory', memoryRouter);
|
apps/backend/src/mcp/toolHandlers.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
| 1 |
import { McpContext } from '@widget-tdc/mcp-types';
|
| 2 |
import { spawn } from 'child_process';
|
| 3 |
import path from 'path';
|
|
|
|
|
|
|
| 4 |
import { MemoryRepository } from '../services/memory/memoryRepository.js';
|
| 5 |
import { SragRepository } from '../services/srag/sragRepository.js';
|
| 6 |
import { EvolutionRepository } from '../services/evolution/evolutionRepository.js';
|
|
@@ -461,6 +463,103 @@ export async function autonomousAgentTeamCoordinateHandler(payload: any, _ctx: M
|
|
| 461 |
}
|
| 462 |
|
| 463 |
// Vidensarkiv (PgVector) tool handlers
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 464 |
export async function vidensarkivSearchHandler(payload: any, ctx: McpContext): Promise<any> {
|
| 465 |
const vectorStore = getNeo4jVectorStore();
|
| 466 |
const { query, limit = 10, namespace } = payload;
|
|
@@ -1194,7 +1293,7 @@ export async function agenticRunHandler(payload: any, ctx: McpContext): Promise<
|
|
| 1194 |
// ---------------------------------------------------
|
| 1195 |
export async function widgetsUpdateStateHandler(payload: any, ctx: McpContext): Promise<any> {
|
| 1196 |
const { widgetId, state } = payload;
|
| 1197 |
-
|
| 1198 |
if (!widgetId) {
|
| 1199 |
throw new Error('widgetId required');
|
| 1200 |
}
|
|
@@ -1280,7 +1379,7 @@ export async function visionaryGenerateHandler(payload: any, ctx: McpContext): P
|
|
| 1280 |
}
|
| 1281 |
|
| 1282 |
const llmService = getLlmService();
|
| 1283 |
-
|
| 1284 |
const systemContext = `
|
| 1285 |
You are The Visionary, an expert system architect and visualization specialist.
|
| 1286 |
Your goal is to generate valid Mermaid.js diagram code based on user requests.
|
|
@@ -1342,7 +1441,7 @@ ${(memories || []).join('\n')}
|
|
| 1342 |
// ---------------------------------------------------
|
| 1343 |
export async function dataAnalysisHandler(payload: any, ctx: McpContext): Promise<any> {
|
| 1344 |
const { data, type, params } = payload;
|
| 1345 |
-
|
| 1346 |
if (!data || !Array.isArray(data)) {
|
| 1347 |
throw new Error("Invalid payload: 'data' must be an array of objects.");
|
| 1348 |
}
|
|
@@ -1351,12 +1450,12 @@ export async function dataAnalysisHandler(payload: any, ctx: McpContext): Promis
|
|
| 1351 |
// Resolve path to python script
|
| 1352 |
// Assuming running from apps/backend
|
| 1353 |
const scriptPath = path.resolve(process.cwd(), 'python/analysis_engine.py');
|
| 1354 |
-
|
| 1355 |
// Check python command availability (python3 or python)
|
| 1356 |
const pythonCmd = process.platform === 'win32' ? 'python' : 'python3';
|
| 1357 |
|
| 1358 |
const pythonProcess = spawn(pythonCmd, [scriptPath]);
|
| 1359 |
-
|
| 1360 |
let resultData = '';
|
| 1361 |
let errorData = '';
|
| 1362 |
|
|
@@ -1382,9 +1481,9 @@ export async function dataAnalysisHandler(payload: any, ctx: McpContext): Promis
|
|
| 1382 |
try {
|
| 1383 |
const parsedResult = JSON.parse(resultData);
|
| 1384 |
if (!parsedResult.success) {
|
| 1385 |
-
|
| 1386 |
} else {
|
| 1387 |
-
|
| 1388 |
}
|
| 1389 |
} catch (e) {
|
| 1390 |
console.error('Failed to parse Python output:', resultData);
|
|
|
|
| 1 |
import { McpContext } from '@widget-tdc/mcp-types';
|
| 2 |
import { spawn } from 'child_process';
|
| 3 |
import path from 'path';
|
| 4 |
+
import * as fs from 'fs/promises';
|
| 5 |
+
import * as os from 'os';
|
| 6 |
import { MemoryRepository } from '../services/memory/memoryRepository.js';
|
| 7 |
import { SragRepository } from '../services/srag/sragRepository.js';
|
| 8 |
import { EvolutionRepository } from '../services/evolution/evolutionRepository.js';
|
|
|
|
| 463 |
}
|
| 464 |
|
| 465 |
// Vidensarkiv (PgVector) tool handlers
|
| 466 |
+
export async function vidensarkivReadHandler(payload: any, _ctx: McpContext): Promise<any> {
|
| 467 |
+
const vectorStore = getNeo4jVectorStore();
|
| 468 |
+
const { id } = payload;
|
| 469 |
+
|
| 470 |
+
if (!id) {
|
| 471 |
+
throw new Error('id is required');
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
const record = await vectorStore.get(id);
|
| 475 |
+
|
| 476 |
+
if (!record) {
|
| 477 |
+
throw new Error(`Document with id ${id} not found`);
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
return {
|
| 481 |
+
success: true,
|
| 482 |
+
file: {
|
| 483 |
+
id: record.id,
|
| 484 |
+
content: record.content,
|
| 485 |
+
metadata: record.metadata
|
| 486 |
+
}
|
| 487 |
+
};
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
export async function vidensarkivListFilesHandler(payload: any, _ctx: McpContext): Promise<any> {
|
| 491 |
+
const { subfolder } = payload;
|
| 492 |
+
const homeDir = os.homedir();
|
| 493 |
+
// Use C:\Users\claus\Desktop\vidensarkiv if on Windows/Dev, or appropriate path
|
| 494 |
+
// For now, respect the ~/Desktop/vidensarkiv convention
|
| 495 |
+
const basePath = path.join(homeDir, 'Desktop', 'vidensarkiv');
|
| 496 |
+
const targetPath = subfolder ? path.join(basePath, subfolder) : basePath;
|
| 497 |
+
|
| 498 |
+
try {
|
| 499 |
+
// Ensure directory exists
|
| 500 |
+
try {
|
| 501 |
+
await fs.access(targetPath);
|
| 502 |
+
} catch {
|
| 503 |
+
await fs.mkdir(targetPath, { recursive: true });
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
const entries = await fs.readdir(targetPath, { withFileTypes: true });
|
| 507 |
+
|
| 508 |
+
const files = entries.map(entry => ({
|
| 509 |
+
name: entry.name,
|
| 510 |
+
path: subfolder ? path.join(subfolder, entry.name) : entry.name,
|
| 511 |
+
type: entry.isDirectory() ? 'folder' : 'file',
|
| 512 |
+
size: 0,
|
| 513 |
+
modified: new Date().toISOString()
|
| 514 |
+
}));
|
| 515 |
+
|
| 516 |
+
// Add size and mtime - parallelize
|
| 517 |
+
await Promise.all(files.map(async (file) => {
|
| 518 |
+
if (file.type === 'file') {
|
| 519 |
+
try {
|
| 520 |
+
const stat = await fs.stat(path.join(targetPath, file.name));
|
| 521 |
+
file.size = stat.size;
|
| 522 |
+
file.modified = stat.mtime.toISOString();
|
| 523 |
+
} catch (e) { console.error('Stat error', e); }
|
| 524 |
+
}
|
| 525 |
+
}));
|
| 526 |
+
|
| 527 |
+
return {
|
| 528 |
+
success: true,
|
| 529 |
+
files
|
| 530 |
+
};
|
| 531 |
+
} catch (error: any) {
|
| 532 |
+
return {
|
| 533 |
+
success: false,
|
| 534 |
+
error: error.message,
|
| 535 |
+
files: []
|
| 536 |
+
};
|
| 537 |
+
}
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
export async function vidensarkivReadFileHandler(payload: any, _ctx: McpContext): Promise<any> {
|
| 541 |
+
const { filepath } = payload;
|
| 542 |
+
if (!filepath) throw new Error('filepath is required');
|
| 543 |
+
|
| 544 |
+
const homeDir = os.homedir();
|
| 545 |
+
const basePath = path.join(homeDir, 'Desktop', 'vidensarkiv');
|
| 546 |
+
// Prevent directory traversal
|
| 547 |
+
const safePath = path.join(basePath, filepath).replace(/\.\./g, '');
|
| 548 |
+
|
| 549 |
+
try {
|
| 550 |
+
const content = await fs.readFile(safePath, 'utf8');
|
| 551 |
+
return {
|
| 552 |
+
success: true,
|
| 553 |
+
content
|
| 554 |
+
};
|
| 555 |
+
} catch (error: any) {
|
| 556 |
+
return {
|
| 557 |
+
success: false,
|
| 558 |
+
error: error.message
|
| 559 |
+
};
|
| 560 |
+
}
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
export async function vidensarkivSearchHandler(payload: any, ctx: McpContext): Promise<any> {
|
| 564 |
const vectorStore = getNeo4jVectorStore();
|
| 565 |
const { query, limit = 10, namespace } = payload;
|
|
|
|
| 1293 |
// ---------------------------------------------------
|
| 1294 |
export async function widgetsUpdateStateHandler(payload: any, ctx: McpContext): Promise<any> {
|
| 1295 |
const { widgetId, state } = payload;
|
| 1296 |
+
|
| 1297 |
if (!widgetId) {
|
| 1298 |
throw new Error('widgetId required');
|
| 1299 |
}
|
|
|
|
| 1379 |
}
|
| 1380 |
|
| 1381 |
const llmService = getLlmService();
|
| 1382 |
+
|
| 1383 |
const systemContext = `
|
| 1384 |
You are The Visionary, an expert system architect and visualization specialist.
|
| 1385 |
Your goal is to generate valid Mermaid.js diagram code based on user requests.
|
|
|
|
| 1441 |
// ---------------------------------------------------
|
| 1442 |
export async function dataAnalysisHandler(payload: any, ctx: McpContext): Promise<any> {
|
| 1443 |
const { data, type, params } = payload;
|
| 1444 |
+
|
| 1445 |
if (!data || !Array.isArray(data)) {
|
| 1446 |
throw new Error("Invalid payload: 'data' must be an array of objects.");
|
| 1447 |
}
|
|
|
|
| 1450 |
// Resolve path to python script
|
| 1451 |
// Assuming running from apps/backend
|
| 1452 |
const scriptPath = path.resolve(process.cwd(), 'python/analysis_engine.py');
|
| 1453 |
+
|
| 1454 |
// Check python command availability (python3 or python)
|
| 1455 |
const pythonCmd = process.platform === 'win32' ? 'python' : 'python3';
|
| 1456 |
|
| 1457 |
const pythonProcess = spawn(pythonCmd, [scriptPath]);
|
| 1458 |
+
|
| 1459 |
let resultData = '';
|
| 1460 |
let errorData = '';
|
| 1461 |
|
|
|
|
| 1481 |
try {
|
| 1482 |
const parsedResult = JSON.parse(resultData);
|
| 1483 |
if (!parsedResult.success) {
|
| 1484 |
+
reject(new Error(parsedResult.error));
|
| 1485 |
} else {
|
| 1486 |
+
resolve(parsedResult.data);
|
| 1487 |
}
|
| 1488 |
} catch (e) {
|
| 1489 |
console.error('Failed to parse Python output:', resultData);
|
apps/backend/src/platform/vector/Neo4jVectorStoreAdapter.ts
CHANGED
|
@@ -86,13 +86,13 @@ export class Neo4jVectorStoreAdapter {
|
|
| 86 |
` + "`vector.similarity_function`" + `: 'cosine'
|
| 87 |
}}
|
| 88 |
`);
|
| 89 |
-
|
| 90 |
// Create constraint for ID
|
| 91 |
await this.graphAdapter.query(`
|
| 92 |
CREATE CONSTRAINT vector_document_id IF NOT EXISTS
|
| 93 |
FOR (n:VectorDocument) REQUIRE n.id IS UNIQUE
|
| 94 |
`);
|
| 95 |
-
|
| 96 |
logger.info(`🧠 Neo4j Vector Store initialized (${this.indexName}, ${this.dimension}D)`);
|
| 97 |
this.isInitialized = true;
|
| 98 |
} catch (error) {
|
|
@@ -140,12 +140,12 @@ export class Neo4jVectorStoreAdapter {
|
|
| 140 |
*/
|
| 141 |
async batchUpsert(options: { records: VectorRecord[]; namespace?: string }): Promise<void> {
|
| 142 |
if (!this.isInitialized) await this.initialize();
|
| 143 |
-
|
| 144 |
// Process in batches of 50 to avoid large transactions
|
| 145 |
const batchSize = 50;
|
| 146 |
for (let i = 0; i < options.records.length; i += batchSize) {
|
| 147 |
const batch = options.records.slice(i, i + batchSize);
|
| 148 |
-
|
| 149 |
// Generate embeddings in parallel for the batch if needed
|
| 150 |
const recordsWithEmbeddings = await Promise.all(batch.map(async (r) => {
|
| 151 |
if (!r.embedding && r.content) {
|
|
@@ -174,7 +174,7 @@ export class Neo4jVectorStoreAdapter {
|
|
| 174 |
namespace: options.namespace || 'default'
|
| 175 |
});
|
| 176 |
}
|
| 177 |
-
|
| 178 |
logger.info(`📦 Batch upserted ${options.records.length} vectors to Neo4j`);
|
| 179 |
}
|
| 180 |
|
|
@@ -183,7 +183,7 @@ export class Neo4jVectorStoreAdapter {
|
|
| 183 |
*/
|
| 184 |
async search(query: VectorQuery): Promise<VectorSearchResult[]> {
|
| 185 |
if (!this.isInitialized) await this.initialize();
|
| 186 |
-
|
| 187 |
let searchVector = query.vector;
|
| 188 |
if (!searchVector && query.text) {
|
| 189 |
searchVector = await this.embeddings.generateEmbedding(query.text);
|
|
@@ -199,7 +199,7 @@ export class Neo4jVectorStoreAdapter {
|
|
| 199 |
// Using Neo4j vector index query
|
| 200 |
// Note: We filter by namespace AFTER the vector search if using db.index.vector.queryNodes
|
| 201 |
// Ideally, we would use pre-filtering but that requires more complex cypher or newer Neo4j features
|
| 202 |
-
|
| 203 |
const cypher = `
|
| 204 |
CALL db.index.vector.queryNodes($indexName, $k, $embedding)
|
| 205 |
YIELD node, score
|
|
@@ -208,7 +208,7 @@ export class Neo4jVectorStoreAdapter {
|
|
| 208 |
`;
|
| 209 |
|
| 210 |
// We request more results than limit to account for filtering
|
| 211 |
-
const k = limit * 5;
|
| 212 |
|
| 213 |
const result = await this.graphAdapter.query(cypher, {
|
| 214 |
indexName: this.indexName,
|
|
@@ -222,12 +222,12 @@ export class Neo4jVectorStoreAdapter {
|
|
| 222 |
// record is an object where keys are the return values from Cypher
|
| 223 |
// We returned: id, content, metadata, score
|
| 224 |
// Note: metadata was returned as 'node', which contains properties
|
| 225 |
-
|
| 226 |
const properties = record.metadata?.properties || {};
|
| 227 |
-
|
| 228 |
// Clean up internal properties if needed
|
| 229 |
delete properties.embedding;
|
| 230 |
-
|
| 231 |
return {
|
| 232 |
id: record.id,
|
| 233 |
content: record.content,
|
|
@@ -236,12 +236,41 @@ export class Neo4jVectorStoreAdapter {
|
|
| 236 |
};
|
| 237 |
});
|
| 238 |
}
|
| 239 |
-
|
| 240 |
// Helper to be implemented after updating adapter
|
| 241 |
async searchRaw(query: VectorQuery): Promise<VectorSearchResult[]> {
|
| 242 |
return this.search(query);
|
| 243 |
}
|
| 244 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
/**
|
| 246 |
* Get statistics
|
| 247 |
*/
|
|
@@ -260,7 +289,7 @@ export class Neo4jVectorStoreAdapter {
|
|
| 260 |
`;
|
| 261 |
|
| 262 |
const result = await this.graphAdapter.query(cypher);
|
| 263 |
-
|
| 264 |
const perNamespace: Record<string, number> = {};
|
| 265 |
let totalRecords = 0;
|
| 266 |
|
|
|
|
| 86 |
` + "`vector.similarity_function`" + `: 'cosine'
|
| 87 |
}}
|
| 88 |
`);
|
| 89 |
+
|
| 90 |
// Create constraint for ID
|
| 91 |
await this.graphAdapter.query(`
|
| 92 |
CREATE CONSTRAINT vector_document_id IF NOT EXISTS
|
| 93 |
FOR (n:VectorDocument) REQUIRE n.id IS UNIQUE
|
| 94 |
`);
|
| 95 |
+
|
| 96 |
logger.info(`🧠 Neo4j Vector Store initialized (${this.indexName}, ${this.dimension}D)`);
|
| 97 |
this.isInitialized = true;
|
| 98 |
} catch (error) {
|
|
|
|
| 140 |
*/
|
| 141 |
async batchUpsert(options: { records: VectorRecord[]; namespace?: string }): Promise<void> {
|
| 142 |
if (!this.isInitialized) await this.initialize();
|
| 143 |
+
|
| 144 |
// Process in batches of 50 to avoid large transactions
|
| 145 |
const batchSize = 50;
|
| 146 |
for (let i = 0; i < options.records.length; i += batchSize) {
|
| 147 |
const batch = options.records.slice(i, i + batchSize);
|
| 148 |
+
|
| 149 |
// Generate embeddings in parallel for the batch if needed
|
| 150 |
const recordsWithEmbeddings = await Promise.all(batch.map(async (r) => {
|
| 151 |
if (!r.embedding && r.content) {
|
|
|
|
| 174 |
namespace: options.namespace || 'default'
|
| 175 |
});
|
| 176 |
}
|
| 177 |
+
|
| 178 |
logger.info(`📦 Batch upserted ${options.records.length} vectors to Neo4j`);
|
| 179 |
}
|
| 180 |
|
|
|
|
| 183 |
*/
|
| 184 |
async search(query: VectorQuery): Promise<VectorSearchResult[]> {
|
| 185 |
if (!this.isInitialized) await this.initialize();
|
| 186 |
+
|
| 187 |
let searchVector = query.vector;
|
| 188 |
if (!searchVector && query.text) {
|
| 189 |
searchVector = await this.embeddings.generateEmbedding(query.text);
|
|
|
|
| 199 |
// Using Neo4j vector index query
|
| 200 |
// Note: We filter by namespace AFTER the vector search if using db.index.vector.queryNodes
|
| 201 |
// Ideally, we would use pre-filtering but that requires more complex cypher or newer Neo4j features
|
| 202 |
+
|
| 203 |
const cypher = `
|
| 204 |
CALL db.index.vector.queryNodes($indexName, $k, $embedding)
|
| 205 |
YIELD node, score
|
|
|
|
| 208 |
`;
|
| 209 |
|
| 210 |
// We request more results than limit to account for filtering
|
| 211 |
+
const k = limit * 5;
|
| 212 |
|
| 213 |
const result = await this.graphAdapter.query(cypher, {
|
| 214 |
indexName: this.indexName,
|
|
|
|
| 222 |
// record is an object where keys are the return values from Cypher
|
| 223 |
// We returned: id, content, metadata, score
|
| 224 |
// Note: metadata was returned as 'node', which contains properties
|
| 225 |
+
|
| 226 |
const properties = record.metadata?.properties || {};
|
| 227 |
+
|
| 228 |
// Clean up internal properties if needed
|
| 229 |
delete properties.embedding;
|
| 230 |
+
|
| 231 |
return {
|
| 232 |
id: record.id,
|
| 233 |
content: record.content,
|
|
|
|
| 236 |
};
|
| 237 |
});
|
| 238 |
}
|
| 239 |
+
|
| 240 |
// Helper to be implemented after updating adapter
|
| 241 |
async searchRaw(query: VectorQuery): Promise<VectorSearchResult[]> {
|
| 242 |
return this.search(query);
|
| 243 |
}
|
| 244 |
|
| 245 |
+
/**
|
| 246 |
+
* Get a vector document by ID
|
| 247 |
+
*/
|
| 248 |
+
async get(id: string): Promise<VectorRecord | null> {
|
| 249 |
+
if (!this.isInitialized) await this.initialize();
|
| 250 |
+
|
| 251 |
+
const cypher = `
|
| 252 |
+
MATCH (n:VectorDocument {id: $id})
|
| 253 |
+
RETURN n.id as id, n.content as content, n.embedding as embedding, n.namespace as namespace, n as metadata
|
| 254 |
+
`;
|
| 255 |
+
|
| 256 |
+
const result = await this.graphAdapter.query(cypher, { id });
|
| 257 |
+
|
| 258 |
+
if (result.records.length === 0) {
|
| 259 |
+
return null;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
const record = result.records[0];
|
| 263 |
+
const properties = record.metadata?.properties || {};
|
| 264 |
+
delete properties.embedding; // Remove large embedding array
|
| 265 |
+
|
| 266 |
+
return {
|
| 267 |
+
id: record.id,
|
| 268 |
+
content: record.content,
|
| 269 |
+
metadata: properties,
|
| 270 |
+
namespace: record.namespace
|
| 271 |
+
};
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
/**
|
| 275 |
* Get statistics
|
| 276 |
*/
|
|
|
|
| 289 |
`;
|
| 290 |
|
| 291 |
const result = await this.graphAdapter.query(cypher);
|
| 292 |
+
|
| 293 |
const perNamespace: Record<string, number> = {};
|
| 294 |
let totalRecords = 0;
|
| 295 |
|