UNPKG

ruvector-extensions

Version:

Advanced features for ruvector: embeddings, UI, exports, temporal tracking, and persistence

380 lines (378 loc) 13.8 kB
import express from 'express'; import { createServer } from 'http'; import { WebSocketServer, WebSocket } from 'ws'; import path from 'path'; export class UIServer { app; server; wss; db; clients; port; constructor(db, port = 3000) { this.db = db; this.port = port; this.clients = new Set(); this.app = express(); this.server = createServer(this.app); this.wss = new WebSocketServer({ server: this.server }); this.setupMiddleware(); this.setupRoutes(); this.setupWebSocket(); } setupMiddleware() { // JSON parsing this.app.use(express.json()); // CORS this.app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); next(); }); // Static files const uiPath = path.join(__dirname, 'ui'); this.app.use(express.static(uiPath)); // Logging this.app.use((req, res, next) => { console.log(`${new Date().toISOString()} ${req.method} ${req.path}`); next(); }); } setupRoutes() { // Health check this.app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: Date.now(), version: '1.0.0' }); }); // Get full graph data this.app.get('/api/graph', async (req, res) => { try { const maxNodes = parseInt(req.query.max) || 100; const graphData = await this.getGraphData(maxNodes); res.json(graphData); } catch (error) { console.error('Error fetching graph:', error); res.status(500).json({ error: 'Failed to fetch graph data', message: error instanceof Error ? error.message : 'Unknown error' }); } }); // Search nodes this.app.get('/api/search', async (req, res) => { try { const query = req.query.q; if (!query) { return res.status(400).json({ error: 'Query parameter required' }); } const results = await this.searchNodes(query); res.json({ results, count: results.length }); } catch (error) { console.error('Search error:', error); res.status(500).json({ error: 'Search failed', message: error instanceof Error ? error.message : 'Unknown error' }); } }); // Find similar nodes this.app.get('/api/similarity/:nodeId', async (req, res) => { try { const { nodeId } = req.params; const threshold = parseFloat(req.query.threshold) || 0.5; const limit = parseInt(req.query.limit) || 10; const similar = await this.findSimilarNodes(nodeId, threshold, limit); res.json({ nodeId, similar, count: similar.length, threshold }); } catch (error) { console.error('Similarity search error:', error); res.status(500).json({ error: 'Similarity search failed', message: error instanceof Error ? error.message : 'Unknown error' }); } }); // Get node details this.app.get('/api/nodes/:nodeId', async (req, res) => { try { const { nodeId } = req.params; const node = await this.getNodeDetails(nodeId); if (!node) { return res.status(404).json({ error: 'Node not found' }); } res.json(node); } catch (error) { console.error('Error fetching node:', error); res.status(500).json({ error: 'Failed to fetch node', message: error instanceof Error ? error.message : 'Unknown error' }); } }); // Add new node (for testing) this.app.post('/api/nodes', async (req, res) => { try { const { id, embedding, metadata } = req.body; if (!id || !embedding) { return res.status(400).json({ error: 'ID and embedding required' }); } await this.db.add(id, embedding, metadata); // Notify all clients this.broadcast({ type: 'node_added', payload: { id, metadata } }); res.status(201).json({ success: true, id }); } catch (error) { console.error('Error adding node:', error); res.status(500).json({ error: 'Failed to add node', message: error instanceof Error ? error.message : 'Unknown error' }); } }); // Database statistics this.app.get('/api/stats', async (req, res) => { try { const stats = await this.db.getStats(); res.json(stats); } catch (error) { console.error('Error fetching stats:', error); res.status(500).json({ error: 'Failed to fetch statistics', message: error instanceof Error ? error.message : 'Unknown error' }); } }); // Serve UI this.app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'ui', 'index.html')); }); } setupWebSocket() { this.wss.on('connection', (ws) => { console.log('New WebSocket client connected'); this.clients.add(ws); ws.on('message', async (message) => { try { const data = JSON.parse(message.toString()); await this.handleWebSocketMessage(ws, data); } catch (error) { console.error('WebSocket message error:', error); ws.send(JSON.stringify({ type: 'error', message: 'Invalid message format' })); } }); ws.on('close', () => { console.log('WebSocket client disconnected'); this.clients.delete(ws); }); ws.on('error', (error) => { console.error('WebSocket error:', error); this.clients.delete(ws); }); // Send initial connection message ws.send(JSON.stringify({ type: 'connected', message: 'Connected to RuVector UI Server' })); }); } async handleWebSocketMessage(ws, data) { switch (data.type) { case 'subscribe': // Handle subscription to updates ws.send(JSON.stringify({ type: 'subscribed', message: 'Subscribed to graph updates' })); break; case 'request_graph': const graphData = await this.getGraphData(data.maxNodes || 100); ws.send(JSON.stringify({ type: 'graph_data', payload: graphData })); break; case 'similarity_query': const similar = await this.findSimilarNodes(data.nodeId, data.threshold || 0.5, data.limit || 10); ws.send(JSON.stringify({ type: 'similarity_result', payload: { nodeId: data.nodeId, similar } })); break; default: ws.send(JSON.stringify({ type: 'error', message: 'Unknown message type' })); } } broadcast(message) { const messageStr = JSON.stringify(message); this.clients.forEach(client => { if (client.readyState === WebSocket.OPEN) { client.send(messageStr); } }); } async getGraphData(maxNodes) { // Get all vectors from database const vectors = await this.db.list(); const nodes = []; const links = []; const nodeMap = new Map(); // Limit nodes const limitedVectors = vectors.slice(0, maxNodes); // Create nodes for (const vector of limitedVectors) { const node = { id: vector.id, label: vector.metadata?.label || vector.id.substring(0, 8), metadata: vector.metadata }; nodes.push(node); nodeMap.set(vector.id, node); } // Create links based on similarity for (let i = 0; i < limitedVectors.length; i++) { const sourceVector = limitedVectors[i]; // Find top 5 similar nodes const similar = await this.db.query(sourceVector.embedding, { topK: 6 }); for (const result of similar) { // Skip self-links and already processed pairs if (result.id === sourceVector.id) continue; if (!nodeMap.has(result.id)) continue; // Only add links above threshold if (result.similarity > 0.3) { links.push({ source: sourceVector.id, target: result.id, similarity: result.similarity }); } } } return { nodes, links }; } async searchNodes(query) { const vectors = await this.db.list(); const results = []; for (const vector of vectors) { // Search in ID if (vector.id.toLowerCase().includes(query.toLowerCase())) { results.push({ id: vector.id, label: vector.metadata?.label, metadata: vector.metadata }); continue; } // Search in metadata if (vector.metadata) { const metadataStr = JSON.stringify(vector.metadata).toLowerCase(); if (metadataStr.includes(query.toLowerCase())) { results.push({ id: vector.id, label: vector.metadata.label, metadata: vector.metadata }); } } } return results; } async findSimilarNodes(nodeId, threshold, limit) { // Get the source node const sourceVector = await this.db.get(nodeId); if (!sourceVector) { throw new Error('Node not found'); } // Query similar nodes const results = await this.db.query(sourceVector.embedding, { topK: limit + 1 }); // Filter and format results return results .filter((r) => r.id !== nodeId && r.similarity >= threshold) .slice(0, limit) .map((r) => ({ id: r.id, similarity: r.similarity, metadata: r.metadata })); } async getNodeDetails(nodeId) { const vector = await this.db.get(nodeId); if (!vector) return null; return { id: vector.id, label: vector.metadata?.label, metadata: vector.metadata }; } start() { return new Promise((resolve) => { this.server.listen(this.port, () => { console.log(` ╔════════════════════════════════════════════════════════════╗ ║ RuVector Graph Explorer UI Server ║ ╚════════════════════════════════════════════════════════════╝ 🌐 Server running at: http://localhost:${this.port} 📊 WebSocket: ws://localhost:${this.port} 🗄️ Database: Connected Open your browser and navigate to http://localhost:${this.port} `); resolve(); }); }); } stop() { return new Promise((resolve) => { // Close WebSocket connections this.clients.forEach(client => client.close()); // Close WebSocket server this.wss.close(() => { // Close HTTP server this.server.close(() => { console.log('UI Server stopped'); resolve(); }); }); }); } notifyGraphUpdate() { // Broadcast update to all clients this.broadcast({ type: 'update', message: 'Graph data updated' }); } } // Example usage export async function startUIServer(db, port = 3000) { const server = new UIServer(db, port); await server.start(); return server; } //# sourceMappingURL=ui-server.js.map