ultimate-mcp-server
Version:
The definitive all-in-one Model Context Protocol server for AI-assisted coding across 30+ platforms
442 lines • 16 kB
JavaScript
/**
* Knowledge Graph Implementation
* Core graph structure and operations for cognitive memory
*/
import { v4 as uuidv4 } from 'uuid';
import { Logger } from '../utils/logger.js';
const logger = new Logger('KnowledgeGraph');
export class KnowledgeGraphManager {
graph;
config;
embeddingProvider;
constructor(config = {}) {
this.config = {
maxNodes: 10000,
maxEdges: 50000,
pruneThreshold: 0.1,
embeddingDimensions: 384,
autoSave: true,
autoSaveInterval: 60000, // 1 minute
...config
};
this.graph = {
nodes: new Map(),
edges: new Map(),
nodeIndex: new Map(),
edgeIndex: new Map()
};
}
async initialize(embeddingProvider) {
this.embeddingProvider = embeddingProvider;
// Load persisted graph if available
if (this.config.persistencePath) {
await this.loadFromDisk();
}
// Start auto-save if enabled
if (this.config.autoSave) {
setInterval(() => this.saveToDisk(), this.config.autoSaveInterval);
}
logger.info('Knowledge graph initialized');
}
/**
* Add a node to the knowledge graph
*/
async addNode(node) {
const id = uuidv4();
const now = new Date();
const cognitiveNode = {
...node,
id,
createdAt: now,
updatedAt: now,
accessCount: 0,
lastAccessed: now,
importance: node.importance || 0.5
};
// Generate embedding if provider is available
if (this.embeddingProvider && !cognitiveNode.embedding) {
cognitiveNode.embedding = await this.embeddingProvider.embed(`${node.name} ${node.content}`);
}
// Add to graph
this.graph.nodes.set(id, cognitiveNode);
// Update type index
if (!this.graph.nodeIndex.has(node.type)) {
this.graph.nodeIndex.set(node.type, new Set());
}
this.graph.nodeIndex.get(node.type).add(id);
// Check if we need to prune
if (this.graph.nodes.size > this.config.maxNodes) {
await this.pruneNodes();
}
return cognitiveNode;
}
/**
* Add an edge between nodes
*/
async addEdge(edge) {
const id = uuidv4();
const cognitiveEdge = {
...edge,
id,
createdAt: new Date()
};
// Validate nodes exist
if (!this.graph.nodes.has(edge.source) || !this.graph.nodes.has(edge.target)) {
throw new Error('Source or target node does not exist');
}
// Add to graph
this.graph.edges.set(id, cognitiveEdge);
// Update edge index
if (!this.graph.edgeIndex.has(edge.source)) {
this.graph.edgeIndex.set(edge.source, new Set());
}
this.graph.edgeIndex.get(edge.source).add(id);
// Update node importance based on connections
await this.updateNodeImportance(edge.source);
await this.updateNodeImportance(edge.target);
// Check if we need to prune
if (this.graph.edges.size > this.config.maxEdges) {
await this.pruneEdges();
}
return cognitiveEdge;
}
/**
* Search for nodes using semantic similarity
*/
async search(options) {
const { query, type, limit = 10, threshold = 0.7, includeRelated = true, depth = 2 } = options;
const results = [];
const relevanceScores = new Map();
// Generate query embedding
let queryEmbedding;
if (this.embeddingProvider) {
queryEmbedding = await this.embeddingProvider.embed(query);
}
// Search nodes
for (const [nodeId, node] of this.graph.nodes) {
// Filter by type if specified
if (type && node.type !== type)
continue;
// Calculate relevance score
let score = 0;
// Embedding similarity
if (queryEmbedding && node.embedding) {
score = this.cosineSimilarity(queryEmbedding, node.embedding);
}
else {
// Fallback to text similarity
score = this.textSimilarity(query, `${node.name} ${node.content}`);
}
// Boost score based on importance and recency
score *= node.importance;
score *= this.recencyBoost(node.lastAccessed);
if (score >= threshold) {
results.push(node);
relevanceScores.set(nodeId, score);
// Update access stats
node.accessCount++;
node.lastAccessed = new Date();
}
}
// Sort by relevance
results.sort((a, b) => (relevanceScores.get(b.id) || 0) - (relevanceScores.get(a.id) || 0));
// Limit results
const topResults = results.slice(0, limit);
// Build context with related nodes
let contextNodes = [...topResults];
const contextEdges = [];
if (includeRelated) {
const visited = new Set(topResults.map(n => n.id));
for (const node of topResults) {
const related = await this.getRelatedNodes(node.id, depth, visited);
contextNodes.push(...related.nodes);
contextEdges.push(...related.edges);
}
}
// Build subgraph
const subgraph = {
nodes: new Map(contextNodes.map(n => [n.id, n])),
edges: new Map(contextEdges.map(e => [e.id, e])),
nodeIndex: new Map(),
edgeIndex: new Map()
};
// Rebuild indices for subgraph
for (const node of contextNodes) {
if (!subgraph.nodeIndex.has(node.type)) {
subgraph.nodeIndex.set(node.type, new Set());
}
subgraph.nodeIndex.get(node.type).add(node.id);
}
for (const edge of contextEdges) {
if (!subgraph.edgeIndex.has(edge.source)) {
subgraph.edgeIndex.set(edge.source, new Set());
}
subgraph.edgeIndex.get(edge.source).add(edge.id);
}
return {
nodes: topResults,
edges: contextEdges,
subgraph,
relevanceScores
};
}
/**
* Get related nodes through graph traversal
*/
async getRelatedNodes(nodeId, depth, visited) {
if (depth === 0 || visited.has(nodeId)) {
return { nodes: [], edges: [] };
}
visited.add(nodeId);
const nodes = [];
const edges = [];
// Get edges from this node
const nodeEdges = this.graph.edgeIndex.get(nodeId) || new Set();
for (const edgeId of nodeEdges) {
const edge = this.graph.edges.get(edgeId);
if (!edge)
continue;
edges.push(edge);
const targetNode = this.graph.nodes.get(edge.target);
if (targetNode && !visited.has(edge.target)) {
nodes.push(targetNode);
// Recursively get related nodes
const subRelated = await this.getRelatedNodes(edge.target, depth - 1, visited);
nodes.push(...subRelated.nodes);
edges.push(...subRelated.edges);
}
}
return { nodes, edges };
}
/**
* Update node importance based on connections and usage
*/
async updateNodeImportance(nodeId) {
const node = this.graph.nodes.get(nodeId);
if (!node)
return;
// Factors for importance calculation
let importance = 0.5; // base importance
// Connection factor (more connections = more important)
const outgoingEdges = this.graph.edgeIndex.get(nodeId)?.size || 0;
const incomingEdges = Array.from(this.graph.edges.values())
.filter(e => e.target === nodeId).length;
const connectionFactor = Math.min((outgoingEdges + incomingEdges) / 20, 1);
importance += connectionFactor * 0.3;
// Access factor (more access = more important)
const accessFactor = Math.min(node.accessCount / 100, 1);
importance += accessFactor * 0.2;
// Recency factor
const daysSinceAccess = (Date.now() - node.lastAccessed.getTime()) / (1000 * 60 * 60 * 24);
const recencyFactor = Math.max(1 - daysSinceAccess / 30, 0);
importance += recencyFactor * 0.1;
node.importance = Math.min(importance, 1);
node.updatedAt = new Date();
}
/**
* Prune least important nodes
*/
async pruneNodes() {
const nodesToPrune = Math.floor(this.graph.nodes.size * 0.1); // Prune 10%
// Sort nodes by importance
const nodesByImportance = Array.from(this.graph.nodes.values())
.sort((a, b) => a.importance - b.importance);
// Remove least important nodes
for (let i = 0; i < nodesToPrune; i++) {
const node = nodesByImportance[i];
if (node.importance < this.config.pruneThreshold) {
await this.removeNode(node.id);
}
}
logger.info(`Pruned ${nodesToPrune} nodes from knowledge graph`);
}
/**
* Prune least important edges
*/
async pruneEdges() {
const edgesToPrune = Math.floor(this.graph.edges.size * 0.1); // Prune 10%
// Sort edges by weight
const edgesByWeight = Array.from(this.graph.edges.values())
.sort((a, b) => a.weight - b.weight);
// Remove weakest edges
for (let i = 0; i < edgesToPrune; i++) {
const edge = edgesByWeight[i];
await this.removeEdge(edge.id);
}
logger.info(`Pruned ${edgesToPrune} edges from knowledge graph`);
}
/**
* Remove a node and its edges
*/
async removeNode(nodeId) {
const node = this.graph.nodes.get(nodeId);
if (!node)
return;
// Remove from type index
this.graph.nodeIndex.get(node.type)?.delete(nodeId);
// Remove all edges connected to this node
const edgesToRemove = [];
// Outgoing edges
const outgoing = this.graph.edgeIndex.get(nodeId) || new Set();
edgesToRemove.push(...outgoing);
// Incoming edges
for (const [edgeId, edge] of this.graph.edges) {
if (edge.target === nodeId) {
edgesToRemove.push(edgeId);
}
}
// Remove edges
for (const edgeId of edgesToRemove) {
await this.removeEdge(edgeId);
}
// Remove node
this.graph.nodes.delete(nodeId);
}
/**
* Remove an edge
*/
async removeEdge(edgeId) {
const edge = this.graph.edges.get(edgeId);
if (!edge)
return;
// Remove from edge index
this.graph.edgeIndex.get(edge.source)?.delete(edgeId);
// Remove edge
this.graph.edges.delete(edgeId);
}
/**
* Calculate cosine similarity between embeddings
*/
cosineSimilarity(a, b) {
if (a.length !== b.length)
return 0;
let dotProduct = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
dotProduct += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
}
/**
* Simple text similarity fallback
*/
textSimilarity(a, b) {
const wordsA = a.toLowerCase().split(/\s+/);
const wordsB = b.toLowerCase().split(/\s+/);
const setA = new Set(wordsA);
const setB = new Set(wordsB);
const intersection = new Set([...setA].filter(x => setB.has(x)));
const union = new Set([...setA, ...setB]);
return intersection.size / union.size;
}
/**
* Calculate recency boost
*/
recencyBoost(lastAccessed) {
const daysSince = (Date.now() - lastAccessed.getTime()) / (1000 * 60 * 60 * 24);
return Math.exp(-daysSince / 30); // Exponential decay over 30 days
}
/**
* Save graph to disk
*/
async saveToDisk() {
if (!this.config.persistencePath)
return;
try {
const fs = await import('fs/promises');
const data = {
nodes: Array.from(this.graph.nodes.entries()),
edges: Array.from(this.graph.edges.entries()),
nodeIndex: Array.from(this.graph.nodeIndex.entries()).map(([k, v]) => [k, Array.from(v)]),
edgeIndex: Array.from(this.graph.edgeIndex.entries()).map(([k, v]) => [k, Array.from(v)])
};
await fs.writeFile(this.config.persistencePath, JSON.stringify(data, null, 2));
logger.debug('Knowledge graph saved to disk');
}
catch (error) {
logger.error('Failed to save knowledge graph:', error);
}
}
/**
* Load graph from disk
*/
async loadFromDisk() {
if (!this.config.persistencePath)
return;
try {
const fs = await import('fs/promises');
const data = await fs.readFile(this.config.persistencePath, 'utf-8');
const parsed = JSON.parse(data);
// Restore nodes
this.graph.nodes = new Map(parsed.nodes.map(([k, v]) => [
k,
{
...v,
createdAt: new Date(v.createdAt),
updatedAt: new Date(v.updatedAt),
lastAccessed: new Date(v.lastAccessed)
}
]));
// Restore edges
this.graph.edges = new Map(parsed.edges.map(([k, v]) => [
k,
{
...v,
createdAt: new Date(v.createdAt)
}
]));
// Restore indices
this.graph.nodeIndex = new Map(parsed.nodeIndex.map(([k, v]) => [k, new Set(v)]));
this.graph.edgeIndex = new Map(parsed.edgeIndex.map(([k, v]) => [k, new Set(v)]));
logger.info('Knowledge graph loaded from disk');
}
catch (error) {
logger.warn('Failed to load knowledge graph from disk:', error);
}
}
/**
* Get graph statistics
*/
getStats() {
const nodesByType = new Map();
for (const [type, nodes] of this.graph.nodeIndex) {
nodesByType.set(type, nodes.size);
}
const edgesByType = new Map();
for (const edge of this.graph.edges.values()) {
edgesByType.set(edge.type, (edgesByType.get(edge.type) || 0) + 1);
}
return {
totalNodes: this.graph.nodes.size,
totalEdges: this.graph.edges.size,
nodesByType: Object.fromEntries(nodesByType),
edgesByType: Object.fromEntries(edgesByType),
averageImportance: Array.from(this.graph.nodes.values())
.reduce((sum, node) => sum + node.importance, 0) / this.graph.nodes.size || 0
};
}
/**
* Export graph for visualization
*/
exportForVisualization() {
const nodes = Array.from(this.graph.nodes.values()).map(node => ({
id: node.id,
label: node.name,
type: node.type,
importance: node.importance,
group: node.type
}));
const edges = Array.from(this.graph.edges.values()).map(edge => ({
id: edge.id,
source: edge.source,
target: edge.target,
label: edge.type,
weight: edge.weight
}));
return { nodes, edges };
}
}
//# sourceMappingURL=knowledge-graph.js.map