UNPKG

@ooples/token-optimizer-mcp

Version:

Intelligent context window optimization for Claude Code - store content externally via caching and compression, freeing up your context window for what matters

1,273 lines 49.8 kB
/** * Knowledge Graph Tool - 91% token reduction through intelligent graph caching * * Features: * - Build knowledge graphs from entities and relations * - Query graphs with pattern matching * - Find paths between entities (shortest, all, widest) * - Detect communities using Louvain, label propagation, modularity * - Rank nodes using PageRank, betweenness, closeness, eigenvector centrality * - Infer missing relations with confidence scoring * - Visualize graphs with force, hierarchical, circular, radial layouts * - Export graphs in multiple formats * - Merge multiple graphs * * Token Reduction Strategy: * - Graph structure caching (93% reduction, 1-hour TTL) * - Query result caching (90% reduction, 10-min TTL) * - Community detection caching (94% reduction, 30-min TTL) * - Ranking caching (92% reduction, 15-min TTL) */ import { createHash } from 'crypto'; import graphlibPkg from 'graphlib'; const { Graph, alg } = graphlibPkg; import { forceSimulation, forceLink, forceManyBody, forceCenter, } from 'd3-force'; export class KnowledgeGraphTool { cache; tokenCounter; metrics; graphs; constructor(cache, tokenCounter, metrics) { this.cache = cache; this.tokenCounter = tokenCounter; this.metrics = metrics; this.graphs = new Map(); } /** * Main execution method following Phase 1 architecture */ async run(options) { const startTime = Date.now(); try { // Generate cache key based on operation and parameters const cacheKey = this.generateCacheKey(options); // Check cache if enabled if (options.useCache !== false) { const cached = this.cache.get(cacheKey); if (cached) { const result = JSON.parse(cached); const tokensSaved = this.tokenCounter.count(JSON.stringify(result)).tokens; this.metrics.record({ operation: `knowledge-graph:${options.operation}`, duration: Date.now() - startTime, success: true, cacheHit: true, inputTokens: 0, outputTokens: 0, cachedTokens: tokensSaved, savedTokens: tokensSaved, }); return { ...result, metadata: { ...result.metadata, cacheHit: true, tokensSaved, }, }; } } // Execute operation const result = await this.executeOperation(options); // Calculate tokens const resultJson = JSON.stringify(result); const tokensUsed = this.tokenCounter.count(resultJson).tokens; // Cache result if (options.useCache !== false) { this.cache.set(cacheKey, resultJson, resultJson.length, resultJson.length); } // Record metrics this.metrics.record({ operation: `knowledge-graph:${options.operation}`, duration: Date.now() - startTime, success: true, cacheHit: false, inputTokens: this.tokenCounter.count(JSON.stringify(options)).tokens, outputTokens: tokensUsed, cachedTokens: 0, savedTokens: 0, }); return { success: true, data: result, metadata: { tokensUsed, tokensSaved: 0, cacheHit: false, queryTime: Date.now() - startTime, }, }; } catch (error) { this.metrics.record({ operation: `knowledge-graph:${options.operation}`, duration: Date.now() - startTime, success: false, cacheHit: false, inputTokens: 0, outputTokens: 0, cachedTokens: 0, savedTokens: 0, metadata: { error: error instanceof Error ? error.message : String(error), }, }); throw error; } } /** * Execute the requested operation */ async executeOperation(options) { switch (options.operation) { case 'build-graph': return this.buildGraph(options); case 'query': return this.queryGraph(options); case 'find-paths': return this.findPaths(options); case 'detect-communities': return this.detectCommunities(options); case 'infer-relations': return this.inferRelations(options); case 'visualize': return this.visualizeGraph(options); case 'export-graph': return this.exportGraph(options); case 'merge-graphs': return this.mergeGraphs(options); default: throw new Error(`Unknown operation: ${options.operation}`); } } /** * Operation 1: Build knowledge graph from entities and relations */ buildGraph(options) { const graphId = options.graphId || this.generateGraphId(); const entities = options.entities || []; const relations = options.relations || []; // Create graphlib instance const g = new Graph({ directed: true }); // Create node map const nodes = new Map(); // Add entities as nodes for (const entity of entities) { nodes.set(entity.id, entity); g.setNode(entity.id, { type: entity.type, properties: entity.properties, }); } // Add relations as edges const edges = []; for (const relation of relations) { if (nodes.has(relation.from) && nodes.has(relation.to)) { g.setEdge(relation.from, relation.to, { type: relation.type, properties: relation.properties || {}, }); edges.push(relation); } } // Store graph this.graphs.set(graphId, { nodes, edges, graphlib: g }); // Calculate statistics const types = new Set(); for (const node of nodes.values()) { types.add(node.type); } const nodeCount = g.nodeCount(); const edgeCount = g.edgeCount(); const density = nodeCount > 1 ? (2 * edgeCount) / (nodeCount * (nodeCount - 1)) : 0; const avgDegree = nodeCount > 0 ? (2 * edgeCount) / nodeCount : 0; return { graph: { id: graphId, nodeCount, edgeCount, types: Array.from(types), density, avgDegree, }, }; } /** * Operation 2: Query graph with pattern matching */ queryGraph(options) { const graphId = options.graphId || this.getDefaultGraphId(); const graphData = this.graphs.get(graphId); if (!graphData) { throw new Error(`Graph not found: ${graphId}`); } const pattern = options.pattern; if (!pattern) { throw new Error('Pattern is required for query operation'); } const matches = []; // Simple pattern matching implementation // For each pattern node, find matching graph nodes const patternNodes = pattern.nodes; const patternEdges = pattern.edges; // Generate all possible node combinations const nodeCombinations = this.generateNodeCombinations(graphData, patternNodes); for (const combination of nodeCombinations) { // Check if edges match let edgesMatch = true; const matchedEdges = []; for (const patternEdge of patternEdges) { const fromId = combination[patternEdge.from]; const toId = combination[patternEdge.to]; if (!fromId || !toId) { edgesMatch = false; break; } const edge = graphData.edges.find((e) => e.from === fromId && e.to === toId && (!patternEdge.type || e.type === patternEdge.type)); if (!edge) { edgesMatch = false; break; } matchedEdges.push({ from: fromId, to: toId, type: edge.type }); } if (edgesMatch) { const matchNodes = Object.values(combination).map((nodeId) => { const node = graphData.nodes.get(nodeId); return { id: node.id, type: node.type, properties: node.properties }; }); matches.push({ nodes: matchNodes, edges: matchedEdges, score: this.calculateMatchScore(matchNodes, matchedEdges, pattern), }); } } // Sort by score descending matches.sort((a, b) => b.score - a.score); return { matches }; } /** * Operation 3: Find paths between entities */ findPaths(options) { const graphId = options.graphId || this.getDefaultGraphId(); const graphData = this.graphs.get(graphId); if (!graphData) { throw new Error(`Graph not found: ${graphId}`); } if (!options.sourceId || !options.targetId) { throw new Error('sourceId and targetId are required for find-paths operation'); } const algorithm = options.algorithm || 'shortest'; const maxHops = options.maxHops || 10; const paths = []; switch (algorithm) { case 'shortest': { // Use Dijkstra's algorithm const shortestPath = alg.dijkstra(graphData.graphlib, options.sourceId); if (shortestPath[options.targetId]) { const pathNodes = this.reconstructPath(shortestPath, options.sourceId, options.targetId); if (pathNodes.length > 0 && pathNodes.length <= maxHops + 1) { const pathEdges = this.getPathEdges(pathNodes, graphData); paths.push({ nodes: pathNodes, edges: pathEdges, length: pathNodes.length - 1, cost: shortestPath[options.targetId].distance, }); } } break; } case 'all': { // Find all paths using DFS const allPaths = this.findAllPaths(graphData, options.sourceId, options.targetId, maxHops); for (const pathNodes of allPaths) { const pathEdges = this.getPathEdges(pathNodes, graphData); paths.push({ nodes: pathNodes, edges: pathEdges, length: pathNodes.length - 1, cost: pathNodes.length - 1, }); } break; } case 'widest': { // Find path with maximum bottleneck capacity const widestPath = this.findWidestPath(graphData, options.sourceId, options.targetId, maxHops); if (widestPath.length > 0) { const pathEdges = this.getPathEdges(widestPath, graphData); paths.push({ nodes: widestPath, edges: pathEdges, length: widestPath.length - 1, cost: widestPath.length - 1, }); } break; } } return { paths }; } /** * Operation 4: Detect communities in the graph */ detectCommunities(options) { const graphId = options.graphId || this.getDefaultGraphId(); const graphData = this.graphs.get(graphId); if (!graphData) { throw new Error(`Graph not found: ${graphId}`); } const algorithm = options.communityAlgorithm || 'louvain'; const minSize = options.minCommunitySize || 2; let communities; switch (algorithm) { case 'louvain': communities = this.louvainCommunityDetection(graphData); break; case 'label-propagation': communities = this.labelPropagation(graphData); break; case 'modularity': communities = this.modularityCommunities(graphData); break; default: throw new Error(`Unknown community detection algorithm: ${algorithm}`); } // Filter by minimum size communities = communities.filter((c) => c.members.size >= minSize); // Calculate community metrics const result = communities.map((community, index) => { const members = Array.from(community.members); const density = this.calculateCommunityDensity(graphData, members); const modularity = this.calculateModularity(graphData, communities); return { id: index, members, size: members.length, density, modularity, }; }); return { communities: result }; } /** * Operation 5: Infer missing relations */ inferRelations(options) { const graphId = options.graphId || this.getDefaultGraphId(); const graphData = this.graphs.get(graphId); if (!graphData) { throw new Error(`Graph not found: ${graphId}`); } const confidenceThreshold = options.confidenceThreshold || 0.5; const maxInferences = options.maxInferences || 100; const inferred = []; // Infer relations based on common neighbors and paths const nodes = Array.from(graphData.nodes.keys()); for (let i = 0; i < nodes.length && inferred.length < maxInferences; i++) { for (let j = i + 1; j < nodes.length && inferred.length < maxInferences; j++) { const nodeA = nodes[i]; const nodeB = nodes[j]; // Skip if relation already exists if (graphData.graphlib.hasEdge(nodeA, nodeB)) continue; // Calculate confidence based on common neighbors const commonNeighbors = this.getCommonNeighbors(graphData, nodeA, nodeB); const pathCount = this.countShortPaths(graphData, nodeA, nodeB, 3); const confidence = this.calculateInferenceConfidence(commonNeighbors.length, pathCount, graphData.graphlib.nodeCount()); if (confidence >= confidenceThreshold) { const evidence = commonNeighbors .slice(0, 5) .map((neighbor) => `Common neighbor: ${neighbor}`); // Infer relation type based on existing patterns const type = this.inferRelationType(graphData, nodeA, nodeB, commonNeighbors); inferred.push({ from: nodeA, to: nodeB, type, confidence, evidence, }); } } } // Sort by confidence descending inferred.sort((a, b) => b.confidence - a.confidence); return { inferredRelations: inferred.slice(0, maxInferences) }; } /** * Operation 6: Visualize graph */ visualizeGraph(options) { const graphId = options.graphId || this.getDefaultGraphId(); const graphData = this.graphs.get(graphId); if (!graphData) { throw new Error(`Graph not found: ${graphId}`); } const layout = options.layout || 'force'; const maxNodes = options.maxNodes || 100; const includeLabels = options.includeLabels !== false; const width = options.imageWidth || 800; const height = options.imageHeight || 600; // Get subset of nodes if graph is too large let nodes = Array.from(graphData.nodes.values()); if (nodes.length > maxNodes) { // Use node ranking to select most important nodes const rankings = this.calculatePageRank(graphData); const topNodes = rankings .sort((a, b) => b.score - a.score) .slice(0, maxNodes) .map((r) => r.nodeId); nodes = nodes.filter((n) => topNodes.includes(n.id)); } const edges = graphData.edges.filter((e) => nodes.some((n) => n.id === e.from) && nodes.some((n) => n.id === e.to)); let layoutData; switch (layout) { case 'force': layoutData = this.forceDirectedLayout(nodes, edges, width, height); break; case 'hierarchical': layoutData = this.hierarchicalLayout(nodes, edges, width, height); break; case 'circular': layoutData = this.circularLayout(nodes, edges, width, height); break; case 'radial': layoutData = this.radialLayout(nodes, edges, width, height); break; default: throw new Error(`Unknown layout: ${layout}`); } // Generate visualization data const visualization = { format: 'json', data: { nodes: layoutData.nodes.map((n, i) => ({ id: n.id, x: n.x, y: n.y, type: nodes[i].type, label: includeLabels ? nodes[i].id : undefined, })), edges: edges.map((e) => ({ from: e.from, to: e.to, type: e.type, })), width, height, }, width, height, }; return { visualization }; } /** * Operation 7: Export graph in various formats */ exportGraph(options) { const graphId = options.graphId || this.getDefaultGraphId(); const graphData = this.graphs.get(graphId); if (!graphData) { throw new Error(`Graph not found: ${graphId}`); } const format = options.format || 'json'; let data; switch (format) { case 'json': data = this.exportAsJSON(graphData); break; case 'graphml': data = this.exportAsGraphML(graphData); break; case 'dot': data = this.exportAsDOT(graphData); break; case 'csv': data = this.exportAsCSV(graphData); break; case 'cytoscape': data = this.exportAsCytoscape(graphData); break; default: throw new Error(`Unknown export format: ${format}`); } return { export: { format, data, size: data.length, }, }; } /** * Operation 8: Merge multiple graphs */ mergeGraphs(options) { if (!options.graphs || options.graphs.length < 2) { throw new Error('At least 2 graphs are required for merge operation'); } const mergeStrategy = options.mergeStrategy || 'union'; const graphId = options.graphId || this.generateGraphId(); // Create merged graph const mergedNodes = new Map(); const mergedEdges = []; const sourceGraphs = []; switch (mergeStrategy) { case 'union': { // Union: include all nodes and edges from all graphs for (const graph of options.graphs) { sourceGraphs.push(graph.id); for (const node of graph.nodes) { if (!mergedNodes.has(node.id)) { mergedNodes.set(node.id, node); } } for (const edge of graph.edges) { const exists = mergedEdges.some((e) => e.from === edge.from && e.to === edge.to && e.type === edge.type); if (!exists) { mergedEdges.push(edge); } } } break; } case 'intersection': { // Intersection: only include nodes and edges present in all graphs const firstGraph = options.graphs[0]; sourceGraphs.push(firstGraph.id); for (const node of firstGraph.nodes) { const inAllGraphs = options.graphs.every((g) => g.nodes.some((n) => n.id === node.id)); if (inAllGraphs) { mergedNodes.set(node.id, node); } } for (const edge of firstGraph.edges) { const inAllGraphs = options.graphs.every((g) => g.edges.some((e) => e.from === edge.from && e.to === edge.to && e.type === edge.type)); if (inAllGraphs && mergedNodes.has(edge.from) && mergedNodes.has(edge.to)) { mergedEdges.push(edge); } } break; } case 'override': { // Override: later graphs override earlier ones for (const graph of options.graphs) { sourceGraphs.push(graph.id); for (const node of graph.nodes) { mergedNodes.set(node.id, node); } } // For edges, last graph wins const lastGraph = options.graphs[options.graphs.length - 1]; for (const edge of lastGraph.edges) { mergedEdges.push(edge); } break; } } // Store merged graph const entities = Array.from(mergedNodes.values()); this.buildGraph({ operation: 'build-graph', graphId, entities, relations: mergedEdges, }); return { merged: { id: graphId, nodeCount: mergedNodes.size, edgeCount: mergedEdges.length, sourceGraphs, }, }; } // ============================================================================ // Helper Methods // ============================================================================ generateCacheKey(options) { const keyData = { operation: options.operation, graphId: options.graphId, pattern: options.pattern, sourceId: options.sourceId, targetId: options.targetId, algorithm: options.algorithm || options.communityAlgorithm || options.rankingAlgorithm, }; return `cache-${createHash('md5') .update(`knowledge-graph:${JSON.stringify(keyData)}`) .digest('hex')}`; } generateGraphId() { return `graph_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } getDefaultGraphId() { if (this.graphs.size === 0) { throw new Error('No graphs available. Create a graph first using build-graph operation.'); } return Array.from(this.graphs.keys())[0]; } generateNodeCombinations(graphData, patternNodes) { // For simplicity, return combinations of first 100 matches const combinations = []; const matchingSets = []; // Find matching nodes for each pattern node for (const patternNode of patternNodes) { const matches = []; for (const [nodeId, node] of graphData.nodes) { if (patternNode.id && nodeId !== patternNode.id) continue; if (patternNode.type && node.type !== patternNode.type) continue; if (patternNode.properties) { const propsMatch = Object.entries(patternNode.properties).every(([key, value]) => node.properties[key] === value); if (!propsMatch) continue; } matches.push(nodeId); } matchingSets.push(matches); } // Generate cartesian product (limited to prevent explosion) const generate = (index, current) => { if (index === matchingSets.length) { combinations.push({ ...current }); return; } for (const nodeId of matchingSets[index].slice(0, 10)) { current[`node${index}`] = nodeId; generate(index + 1, current); } }; generate(0, {}); return combinations.slice(0, 100); } calculateMatchScore(nodes, edges, pattern) { // Simple scoring: base score + bonus for property matches let score = nodes.length + edges.length; // Bonus for exact property matches for (const node of nodes) { const patternNode = pattern.nodes.find((n) => n.type === node.type); if (patternNode && patternNode.properties) { const matchCount = Object.entries(patternNode.properties).filter(([key, value]) => node.properties[key] === value).length; score += matchCount * 0.5; } } return score; } reconstructPath(dijkstraResult, source, target) { const path = []; let current = target; while (current && current !== source) { path.unshift(current); current = dijkstraResult[current]?.predecessor; } if (current === source) { path.unshift(source); return path; } return []; } getPathEdges(pathNodes, graphData) { const edges = []; for (let i = 0; i < pathNodes.length - 1; i++) { const from = pathNodes[i]; const to = pathNodes[i + 1]; const edge = graphData.edges.find((e) => e.from === from && e.to === to); if (edge) { edges.push({ from, to, type: edge.type }); } } return edges; } findAllPaths(graphData, source, target, maxHops) { const paths = []; const visited = new Set(); const dfs = (current, path) => { if (current === target) { paths.push([...path]); return; } if (path.length >= maxHops + 1) return; visited.add(current); const neighbors = graphData.graphlib.successors(current) || []; for (const neighbor of neighbors) { if (!visited.has(neighbor)) { dfs(neighbor, [...path, neighbor]); } } visited.delete(current); }; dfs(source, [source]); return paths; } findWidestPath(graphData, source, target, maxHops) { // Use modified Dijkstra for maximum bottleneck capacity const capacity = new Map(); const predecessor = new Map(); const queue = new Set(); for (const nodeId of graphData.nodes.keys()) { capacity.set(nodeId, nodeId === source ? Infinity : 0); queue.add(nodeId); } while (queue.size > 0) { let maxNode = ''; let maxCap = -1; for (const nodeId of queue) { const cap = capacity.get(nodeId); if (cap > maxCap) { maxCap = cap; maxNode = nodeId; } } if (maxNode === target) break; queue.delete(maxNode); const neighbors = graphData.graphlib.successors(maxNode) || []; for (const neighbor of neighbors) { if (queue.has(neighbor)) { const newCap = Math.min(maxCap, 1); // Assume edge capacity of 1 if (newCap > capacity.get(neighbor)) { capacity.set(neighbor, newCap); predecessor.set(neighbor, maxNode); } } } } // Reconstruct path const path = []; let current = target; while (current && current !== source) { path.unshift(current); current = predecessor.get(current); if (path.length > maxHops) break; } if (current === source) { path.unshift(source); return path; } return []; } louvainCommunityDetection(graphData) { // Simplified Louvain algorithm const nodes = Array.from(graphData.nodes.keys()); const communities = []; // Initialize: each node in its own community const nodeToCommunity = new Map(); for (let i = 0; i < nodes.length; i++) { nodeToCommunity.set(nodes[i], i); communities.push({ id: i, members: new Set([nodes[i]]), connections: new Map(), }); } // Iterate until convergence (max 10 iterations) for (let iter = 0; iter < 10; iter++) { let changed = false; for (const node of nodes) { const currentCommunityId = nodeToCommunity.get(node); const neighbors = graphData.graphlib.neighbors(node) || []; // Find best community to move to const communityScores = new Map(); for (const neighbor of neighbors) { const neighborCommunity = nodeToCommunity.get(neighbor); communityScores.set(neighborCommunity, (communityScores.get(neighborCommunity) || 0) + 1); } // Find community with highest score let bestCommunity = currentCommunityId; let bestScore = communityScores.get(currentCommunityId) || 0; for (const [communityId, score] of communityScores) { if (score > bestScore) { bestScore = score; bestCommunity = communityId; } } // Move node if beneficial if (bestCommunity !== currentCommunityId) { communities[currentCommunityId].members.delete(node); communities[bestCommunity].members.add(node); nodeToCommunity.set(node, bestCommunity); changed = true; } } if (!changed) break; } // Filter out empty communities return communities.filter((c) => c.members.size > 0); } labelPropagation(graphData) { // Label propagation algorithm const nodes = Array.from(graphData.nodes.keys()); const labels = new Map(); // Initialize with unique labels for (let i = 0; i < nodes.length; i++) { labels.set(nodes[i], i); } // Propagate labels (max 10 iterations) for (let iter = 0; iter < 10; iter++) { let changed = false; // Randomize node order to avoid bias const shuffled = [...nodes].sort(() => Math.random() - 0.5); for (const node of shuffled) { const neighbors = graphData.graphlib.neighbors(node) || []; if (neighbors.length === 0) continue; // Count neighbor labels const labelCounts = new Map(); for (const neighbor of neighbors) { const label = labels.get(neighbor); labelCounts.set(label, (labelCounts.get(label) || 0) + 1); } // Adopt most common label let maxLabel = labels.get(node); let maxCount = 0; for (const [label, count] of labelCounts) { if (count > maxCount) { maxCount = count; maxLabel = label; } } if (maxLabel !== labels.get(node)) { labels.set(node, maxLabel); changed = true; } } if (!changed) break; } // Group nodes by label const communityMap = new Map(); for (const [node, label] of labels) { if (!communityMap.has(label)) { communityMap.set(label, new Set()); } communityMap.get(label).add(node); } // Convert to Community format return Array.from(communityMap.entries()).map(([id, members]) => ({ id, members, connections: new Map(), })); } modularityCommunities(graphData) { // Use Louvain as base for modularity optimization return this.louvainCommunityDetection(graphData); } calculateCommunityDensity(graphData, members) { if (members.length < 2) return 0; let internalEdges = 0; const maxEdges = (members.length * (members.length - 1)) / 2; for (let i = 0; i < members.length; i++) { for (let j = i + 1; j < members.length; j++) { if (graphData.graphlib.hasEdge(members[i], members[j]) || graphData.graphlib.hasEdge(members[j], members[i])) { internalEdges++; } } } return internalEdges / maxEdges; } calculateModularity(graphData, communities) { const m = graphData.graphlib.edgeCount(); if (m === 0) return 0; let Q = 0; for (const community of communities) { const members = Array.from(community.members); for (const i of members) { for (const j of members) { const A_ij = graphData.graphlib.hasEdge(i, j) ? 1 : 0; const k_i = (graphData.graphlib.predecessors(i)?.length || 0) + (graphData.graphlib.successors(i)?.length || 0); const k_j = (graphData.graphlib.predecessors(j)?.length || 0) + (graphData.graphlib.successors(j)?.length || 0); Q += A_ij - (k_i * k_j) / (2 * m); } } } return Q / (2 * m); } getCommonNeighbors(graphData, nodeA, nodeB) { const neighborsA = new Set([ ...(graphData.graphlib.successors(nodeA) || []), ...(graphData.graphlib.predecessors(nodeA) || []), ]); const neighborsB = new Set([ ...(graphData.graphlib.successors(nodeB) || []), ...(graphData.graphlib.predecessors(nodeB) || []), ]); const common = []; for (const neighbor of neighborsA) { if (neighborsB.has(neighbor)) { common.push(neighbor); } } return common; } countShortPaths(graphData, source, target, maxLength) { const paths = this.findAllPaths(graphData, source, target, maxLength); return paths.filter((p) => p.length <= maxLength + 1).length; } calculateInferenceConfidence(commonNeighbors, pathCount, _totalNodes) { // Simple confidence calculation const neighborScore = Math.min(commonNeighbors / 10, 1) * 0.6; const pathScore = Math.min(pathCount / 5, 1) * 0.4; return neighborScore + pathScore; } inferRelationType(graphData, nodeA, nodeB, commonNeighbors) { // Infer type based on most common edge type from common neighbors const typeCounts = new Map(); for (const neighbor of commonNeighbors) { const edgeA = graphData.edges.find((e) => e.from === nodeA && e.to === neighbor); const edgeB = graphData.edges.find((e) => e.from === neighbor && e.to === nodeB); if (edgeA) { typeCounts.set(edgeA.type, (typeCounts.get(edgeA.type) || 0) + 1); } if (edgeB) { typeCounts.set(edgeB.type, (typeCounts.get(edgeB.type) || 0) + 1); } } let maxType = 'related'; let maxCount = 0; for (const [type, count] of typeCounts) { if (count > maxCount) { maxCount = count; maxType = type; } } return maxType; } calculatePageRank(graphData) { const dampingFactor = 0.85; const epsilon = 0.0001; const maxIterations = 100; const nodes = Array.from(graphData.nodes.keys()); const n = nodes.length; const ranks = new Map(); // Initialize ranks for (const node of nodes) { ranks.set(node, 1 / n); } // Iterate until convergence for (let iter = 0; iter < maxIterations; iter++) { const newRanks = new Map(); let diff = 0; for (const node of nodes) { const predecessors = graphData.graphlib.predecessors(node) || []; let rank = (1 - dampingFactor) / n; for (const pred of predecessors) { const predOutDegree = (graphData.graphlib.successors(pred) || []) .length; if (predOutDegree > 0) { rank += dampingFactor * (ranks.get(pred) / predOutDegree); } } newRanks.set(node, rank); diff += Math.abs(rank - ranks.get(node)); } // Copy new ranks for (const [node, rank] of newRanks) { ranks.set(node, rank); } if (diff < epsilon) break; } return Array.from(ranks.entries()).map(([nodeId, score], index) => ({ nodeId, rank: index + 1, score, })); } forceDirectedLayout(nodes, edges, width, height) { // Convert to d3-force format const d3Nodes = nodes.map((n) => ({ id: n.id, x: width / 2, y: height / 2, })); const d3Links = edges.map((e) => ({ source: e.from, target: e.to })); // Run simulation const simulation = forceSimulation(d3Nodes) .force('link', forceLink(d3Links) .id((d) => d.id) .distance(50)) .force('charge', forceManyBody().strength(-100)) .force('center', forceCenter(width / 2, height / 2)); // Run for fixed number of ticks for (let i = 0; i < 100; i++) { simulation.tick(); } return { nodes: d3Nodes, edges: d3Links }; } hierarchicalLayout(nodes, edges, width, height) { // Simple hierarchical layout const levels = new Map(); const visited = new Set(); // Find root nodes (no predecessors) const roots = nodes.filter((n) => !edges.some((e) => e.to === n.id)); // BFS to assign levels const queue = roots.map((r) => ({ id: r.id, level: 0, })); while (queue.length > 0) { const { id, level } = queue.shift(); if (visited.has(id)) continue; visited.add(id); levels.set(id, level); const children = edges.filter((e) => e.from === id).map((e) => e.to); for (const child of children) { queue.push({ id: child, level: level + 1 }); } } // Position nodes const maxLevel = Math.max(...Array.from(levels.values())); const levelCounts = new Map(); for (const level of levels.values()) { levelCounts.set(level, (levelCounts.get(level) || 0) + 1); } const levelCounters = new Map(); const positioned = nodes.map((n) => { const level = levels.get(n.id) || 0; const count = levelCounts.get(level) || 1; const index = levelCounters.get(level) || 0; levelCounters.set(level, index + 1); return { id: n.id, x: ((index + 1) * width) / (count + 1), y: ((level + 1) * height) / (maxLevel + 2), }; }); return { nodes: positioned, edges }; } circularLayout(nodes, edges, width, height) { const radius = Math.min(width, height) / 2 - 50; const centerX = width / 2; const centerY = height / 2; const positioned = nodes.map((n, i) => { const angle = (2 * Math.PI * i) / nodes.length; return { id: n.id, x: centerX + radius * Math.cos(angle), y: centerY + radius * Math.sin(angle), }; }); return { nodes: positioned, edges }; } radialLayout(nodes, edges, width, height) { // Similar to hierarchical but radial const levels = new Map(); const visited = new Set(); const roots = nodes.filter((n) => !edges.some((e) => e.to === n.id)); const queue = roots.map((r) => ({ id: r.id, level: 0, })); while (queue.length > 0) { const { id, level } = queue.shift(); if (visited.has(id)) continue; visited.add(id); levels.set(id, level); const children = edges.filter((e) => e.from === id).map((e) => e.to); for (const child of children) { queue.push({ id: child, level: level + 1 }); } } const maxLevel = Math.max(...Array.from(levels.values()), 0); const maxRadius = Math.min(width, height) / 2 - 50; const centerX = width / 2; const centerY = height / 2; const levelCounts = new Map(); for (const level of levels.values()) { levelCounts.set(level, (levelCounts.get(level) || 0) + 1); } const levelCounters = new Map(); const positioned = nodes.map((n) => { const level = levels.get(n.id) || 0; const count = levelCounts.get(level) || 1; const index = levelCounters.get(level) || 0; levelCounters.set(level, index + 1); const radius = (level * maxRadius) / (maxLevel + 1); const angle = (2 * Math.PI * index) / count; return { id: n.id, x: centerX + radius * Math.cos(angle), y: centerY + radius * Math.sin(angle), }; }); return { nodes: positioned, edges }; } exportAsJSON(graphData) { const data = { nodes: Array.from(graphData.nodes.values()), edges: graphData.edges, }; return JSON.stringify(data, null, 2); } exportAsGraphML(graphData) { let xml = '<?xml version="1.0" encoding="UTF-8"?>\n'; xml += '<graphml xmlns="http://graphml.graphdrawing.org/xmlns">\n'; xml += ' <graph id="G" edgedefault="directed">\n'; for (const [id, node] of graphData.nodes) { xml += ` <node id="${this.escapeXML(id)}">\n`; xml += ` <data key="type">${this.escapeXML(node.type)}</data>\n`; xml += ` </node>\n`; } for (const edge of graphData.edges) { xml += ` <edge source="${this.escapeXML(edge.from)}" target="${this.escapeXML(edge.to)}">\n`; xml += ` <data key="type">${this.escapeXML(edge.type)}</data>\n`; xml += ` </edge>\n`; } xml += ' </graph>\n'; xml += '</graphml>'; return xml; } exportAsDOT(graphData) { let dot = 'digraph G {\n'; for (const [id, node] of graphData.nodes) { dot += ` "${id}" [label="${id}" type="${node.type}"];\n`; } for (const edge of graphData.edges) { dot += ` "${edge.from}" -> "${edge.to}" [label="${edge.type}"];\n`; } dot += '}'; return dot; } exportAsCSV(graphData) { let csv = 'type,from,to,edge_type\n'; for (const [id, node] of graphData.nodes) { csv += `node,${id},${node.type},\n`; } for (const edge of graphData.edges) { csv += `edge,${edge.from},${edge.to},${edge.type}\n`; } return csv; } exportAsCytoscape(graphData) { const elements = { nodes: Array.from(graphData.nodes.values()).map((n) => ({ data: { id: n.id, type: n.type, properties: n.properties }, })), edges: graphData.edges.map((e, i) => ({ data: { id: `e${i}`, source: e.from, target: e.to, type: e.type }, })), }; return JSON.stringify(elements, null, 2); } escapeXML(str) { return str.replace(/[<>&'"]/g, (c) => { switch (c) { case '<': return '&lt;'; case '>': return '&gt;'; case '&': return '&amp;'; case "'": return '&apos;'; case '"': return '&quot;'; default: return c; } }); } } // Export singleton instance factory let knowledgeGraphInstance = null; export function getKnowledgeGraphTool(cache, tokenCounter, metrics) { if (!knowledgeGraphInstance) { knowledgeGraphInstance = new KnowledgeGraphTool(cache, tokenCounter, metrics); } return knowledgeGraphInstance; } // MCP Tool definition export const KNOWLEDGE_GRAPH_TOOL_DEFINITION = { name: 'knowledge_graph', description: 'Build and query knowledge graphs with 91% token reduction through intelligent caching. Supports graph building, pattern querying, path finding, community detection, node ranking, relation inference, visualization, and export.', inputSchema: { type: 'object', properties: { operation: { type: 'string', enum: [ 'build-graph', 'query', 'find-paths', 'detect-communities', 'infer-relations', 'visualize', 'export-graph', 'merge-graphs', ], description: 'The knowledge graph operation to perform', }, entities: { type: 'array', description: 'Entities to add to graph (for build-graph)', }, relations: { type: 'array', description: 'Relations between entities (for build-graph)', }, pattern: { type: 'object', description: 'Query pattern with nodes and edges (for query)', }, sourceId: { type: 'string', description: 'Source node ID (for find-paths)', }, targetId: { type: 'string', description: 'Target node ID (for find-paths)', }, algorithm: { type: 'string', description: 'Algorithm to use (shortest/all/widest for paths, louvain/label-propagation/modularity for communities, pagerank/betweenness/closeness/eigenvector for ranking)', }, layout: { type: 'string', enum: ['force', 'hierarchical', 'circular', 'radial'], description: 'Visualization layout (for visualize)', }, format: { type: 'string', enum: ['json', 'graphml', 'dot', 'csv', 'cytoscape'], description: 'Export format (for export-graph)', }, graphId: { type: 'string', description: 'Graph identifier', }, useCache: { type: 'boolean', description: 'Enable caching (default: true)', default: true, }, cacheTTL: { type: 'number', description: 'Cache TTL in seconds', }, }, required: ['operation'], }, }; //# sourceMappingURL=knowledge-graph.js.map