Skip to main content
Glama
graph.js8.21 kB
/** * Graph Handlers * * Consolidated handlers for: graph_traverse, graph_path, graph_clusters, * graph_pagerank, graph_patterns, graph_stats * * Integrates with knowledge graph for relationship discovery. */ const { normalize } = require('../../core/normalize'); /** * Unified graph handler * * Operations: traverse, path, clusters, pagerank, patterns, stats */ async function handleGraph(op, params, context = {}) { const normalized = normalize('graph', params); const { graphClient, dbClient } = context; // Use graphClient if available, otherwise fall back to dbClient const client = graphClient || dbClient; if (!client) { throw new Error('Graph or database client not available'); } switch (op) { case 'traverse': return traverseGraph(normalized, client); case 'path': return findPath(normalized, client); case 'clusters': return findClusters(normalized, client); case 'pagerank': case 'rank': return getPageRank(normalized, client); case 'patterns': return findPatterns(normalized, client); case 'stats': return getGraphStats(client); default: throw new Error(`Unknown graph operation: ${op}`); } } /** * Traverse graph from starting node */ async function traverseGraph(params, client) { const { startNode, node, depth = 3, strategy = 'semantic' } = params; const start = startNode || node; if (!start) { throw new Error('startNode is required'); } // Parse node format: "report:5" or "doc:123" const [nodeType, nodeId] = start.includes(':') ? start.split(':') : ['report', start]; if (typeof client.traverseGraph === 'function') { const result = await client.traverseGraph(nodeType, nodeId, depth, strategy); return formatTraversalResult(start, result, strategy); } // Fallback: simple SQL-based traversal const nodes = await fallbackTraversal(client, nodeType, nodeId, depth); return formatTraversalResult(start, nodes, 'sql-fallback'); } /** * Find path between two nodes */ async function findPath(params, client) { const { from, to } = params; if (!from || !to) { throw new Error('Both from and to parameters are required'); } if (typeof client.findPath === 'function') { const path = await client.findPath(from, to); return { from, to, pathFound: path && path.length > 0, pathLength: path?.length || 0, path: path || [], message: path?.length > 0 ? `Found path with ${path.length} steps` : 'No path found between nodes' }; } // Fallback: no path finding available return { from, to, pathFound: false, message: 'Path finding not available (no graph client)' }; } /** * Find node clusters */ async function findClusters(params, client) { if (typeof client.findClusters === 'function') { const clusters = await client.findClusters(); return { clusterCount: clusters?.length || 0, clusters: (clusters || []).map((c, i) => ({ id: i, size: c.nodes?.length || c.size || 0, label: c.label || `Cluster ${i + 1}`, nodes: c.nodes?.slice(0, 10) || [] // Limit to first 10 nodes per cluster })) }; } // Fallback: group by topic similarity return { clusterCount: 0, clusters: [], message: 'Clustering not available (no graph client)' }; } /** * Get PageRank importance scores */ async function getPageRank(params, client) { const { topK = 20 } = params; if (typeof client.getPageRank === 'function') { const rankings = await client.getPageRank(topK); return { topK, rankings: rankings || [], message: `Top ${topK} nodes by importance` }; } // Fallback: use report ratings or access count if (typeof client.query === 'function') { const sql = ` SELECT id, query, rating, COALESCE(rating, 0) as importance FROM research_reports ORDER BY importance DESC, created_at DESC LIMIT $1 `; const rows = await client.query(sql, [topK]); return { topK, rankings: (rows || []).map((r, i) => ({ rank: i + 1, nodeId: `report:${r.id}`, label: r.query?.substring(0, 50) || `Report ${r.id}`, score: r.importance || 0 })), message: 'Rankings based on report ratings (PageRank not available)' }; } return { topK, rankings: [], message: 'PageRank not available' }; } /** * Find event patterns (N-grams) */ async function findPatterns(params, client) { const { n = 3 } = params; if (typeof client.findPatterns === 'function') { const patterns = await client.findPatterns(n); return { n, patternCount: patterns?.length || 0, patterns: patterns || [] }; } // Fallback: analyze query patterns if (typeof client.query === 'function') { const sql = ` SELECT query, COUNT(*) as frequency FROM research_reports GROUP BY query HAVING COUNT(*) > 1 ORDER BY frequency DESC LIMIT 20 `; const rows = await client.query(sql, []); return { n, patternCount: rows?.length || 0, patterns: (rows || []).map(r => ({ pattern: r.query?.substring(0, 100), frequency: parseInt(r.frequency) })), message: 'Patterns based on repeated queries (N-gram analysis not available)' }; } return { n, patternCount: 0, patterns: [], message: 'Pattern analysis not available' }; } /** * Get graph statistics */ async function getGraphStats(client) { const stats = { available: false, nodeCount: 0, edgeCount: 0, reportCount: 0, docCount: 0 }; if (typeof client.getGraphStats === 'function') { const graphStats = await client.getGraphStats(); return { available: true, ...graphStats }; } // Fallback: count from database if (typeof client.query === 'function') { try { const reportCount = await client.query('SELECT COUNT(*) as count FROM research_reports', []); stats.reportCount = parseInt(reportCount?.[0]?.count) || 0; const docCount = await client.query('SELECT COUNT(*) as count FROM doc_index', []); stats.docCount = parseInt(docCount?.[0]?.count) || 0; stats.nodeCount = stats.reportCount + stats.docCount; stats.available = true; } catch (e) { stats.error = e.message; } } return stats; } /** * Format traversal result */ function formatTraversalResult(startNode, nodes, strategy) { return { startNode, strategy, nodeCount: nodes?.length || 0, nodes: (nodes || []).map(n => ({ id: n.id || n.nodeId, type: n.type || 'unknown', label: n.label || n.title || n.query?.substring(0, 50), depth: n.depth || 0, similarity: n.similarity })) }; } /** * Fallback traversal using SQL */ async function fallbackTraversal(client, nodeType, nodeId, depth) { if (typeof client.query !== 'function') { return []; } // Simple: get related reports based on query similarity const sql = ` SELECT r.id, r.query, 'report' as type FROM research_reports r WHERE r.id != $1 ORDER BY r.created_at DESC LIMIT $2 `; const rows = await client.query(sql, [nodeId, depth * 5]); return (rows || []).map((r, i) => ({ id: `report:${r.id}`, type: r.type, label: r.query?.substring(0, 50), depth: Math.floor(i / 5) + 1 })); } /** * Legacy compatibility wrappers */ const graphTraverse = (params, ctx) => handleGraph('traverse', params, ctx); const graphPath = (params, ctx) => handleGraph('path', params, ctx); const graphClusters = (params, ctx) => handleGraph('clusters', params, ctx); const graphPagerank = (params, ctx) => handleGraph('pagerank', params, ctx); const graphPatterns = (params, ctx) => handleGraph('patterns', params, ctx); const graphStats = (params, ctx) => handleGraph('stats', params, ctx); module.exports = { handleGraph, traverseGraph, findPath, findClusters, getPageRank, findPatterns, getGraphStats, // Legacy exports graphTraverse, graphPath, graphClusters, graphPagerank, graphPatterns, graphStats };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/wheattoast11/openrouter-deep-research-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server