@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
JavaScript
/**
* 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 '<';
case '>':
return '>';
case '&':
return '&';
case "'":
return ''';
case '"':
return '"';
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