UNPKG

@zhangzichao2008/mcp-graphiti

Version:

Graphiti MCP Server - Temporal Knowledge Graph for AI Agents

215 lines 7.41 kB
import neo4j, { int } from 'neo4j-driver'; export class Neo4jDriver { driver; logger; database; constructor(config, logger) { this.driver = neo4j.driver(config.uri, neo4j.auth.basic(config.user, config.password), { maxConnectionPoolSize: 50, connectionAcquisitionTimeout: 60000, }); this.logger = logger; this.database = config.database; } async connect() { try { await this.driver.verifyConnectivity(); this.logger.info('Successfully connected to Neo4j database'); } catch (error) { this.logger.error('Failed to connect to Neo4j database:', error); throw error; } } async close() { await this.driver.close(); this.logger.info('Neo4j driver closed'); } async runQuery(query, parameters = {}) { const session = this.driver.session({ database: this.database }); try { this.logger.debug('Executing Neo4j query:', query, parameters); const result = await session.run(query, parameters); return result.records.map((record) => record.toObject()); } catch (error) { this.logger.error('Neo4j query failed:', query, parameters, error); throw error; } finally { await session.close(); } } async addNodes(nodes) { const query = ` UNWIND $nodes AS node CREATE (n:Entity { id: node.id, type: node.type, name: node.name, summary: node.summary, created_at: node.created_at, valid_at: node.valid_at, invalid_at: node.invalid_at }) SET n += node.attributes `; try { await this.runQuery(query, { nodes }); this.logger.info(`Added ${nodes.length} nodes to the database`); } catch (error) { this.logger.error('Failed to add nodes:', error); throw error; } } async addEdges(edges) { const query = ` UNWIND $edges AS edge MATCH (source:Entity {id: edge.source_id}) MATCH (target:Entity {id: edge.target_id}) CREATE (source)-[r:RELATIONSHIP { id: edge.id, type: edge.type, name: edge.name, summary: edge.summary, created_at: edge.created_at, valid_at: edge.valid_at, invalid_at: edge.invalid_at }]->(target) SET r += edge.attributes `; try { await this.runQuery(query, { edges }); this.logger.info(`Added ${edges.length} edges to the database`); } catch (error) { this.logger.error('Failed to add edges:', error); throw error; } } async searchNodes(query, limit = 10) { const cypherQuery = ` MATCH (n:Entity) WHERE n.name CONTAINS $query RETURN n, 1.0 as score LIMIT $limit `; try { this.logger.debug('searchNodes called with:', { query, limit, limitType: typeof limit, flooredLimit: Math.floor(limit) }); const params = { query, limit: int(Math.floor(limit)) }; this.logger.debug('searchNodes parameters:', params); const results = await this.runQuery(cypherQuery, params); this.logger.debug('searchNodes results:', results); return results.map((record) => { this.logger.debug('Processing record:', record); // 修复:正确访问节点属性 const node = record.n || record.node; if (!node) { this.logger.warn('No node found in record:', record); return null; } return { ...node.properties, score: record.score, }; }).filter(Boolean); // 过滤掉null值 } catch (error) { this.logger.error('Node search failed:', error); throw error; } } async searchByName(name, type) { let query = ` MATCH (n:Entity) WHERE n.name CONTAINS $name `; const params = { name }; if (type) { query += ` AND n.type = $type`; params.type = type; } query += ` RETURN n`; try { const results = await this.runQuery(query, params); return results.map((record) => record.n.properties); } catch (error) { this.logger.error('Name search failed:', error); throw error; } } async getFacts(sourceName, targetName, factType) { let query = ` MATCH (source:Entity)-[r:RELATIONSHIP]->(target:Entity) WHERE 1=1 `; const params = {}; if (sourceName) { query += ` AND source.name CONTAINS $sourceName`; params.sourceName = sourceName; } if (targetName) { query += ` AND target.name CONTAINS $targetName`; params.targetName = targetName; } if (factType) { query += ` AND r.type = $factType`; params.factType = factType; } query += ` RETURN r, source, target`; try { const results = await this.runQuery(query, params); return results.map((record) => ({ ...record.r.properties, source_node: record.source.properties, target_node: record.target.properties, })); } catch (error) { this.logger.error('Fact retrieval failed:', error); throw error; } } async createIndexes() { const indexes = [ 'CREATE INDEX entity_id IF NOT EXISTS FOR (n:Entity) ON (n.id)', 'CREATE INDEX entity_name IF NOT EXISTS FOR (n:Entity) ON (n.name)', 'CREATE INDEX entity_type IF NOT EXISTS FOR (n:Entity) ON (n.type)', 'CREATE INDEX relationship_id IF NOT EXISTS FOR ()-[r:RELATIONSHIP]-() ON (r.id)', 'CREATE INDEX relationship_type IF NOT EXISTS FOR ()-[r:RELATIONSHIP]-() ON (r.type)', ]; for (const indexQuery of indexes) { try { await this.runQuery(indexQuery); this.logger.debug(`Created index: ${indexQuery}`); } catch (error) { this.logger.warn(`Failed to create index (may already exist): ${indexQuery}`); } } } async vectorSearch(queryEmbedding, limit = 10) { // For now, fallback to keyword search since vector search requires additional Neo4j setup // In a full implementation, this would use Neo4j's vector index capabilities this.logger.warn('Vector search not implemented, falling back to text search'); const nodes = await this.searchNodes('', limit); return nodes.map(node => ({ node, score: 0.5 // Default similarity score })); } async healthCheck() { try { await this.driver.verifyConnectivity(); return true; } catch (error) { this.logger.error('Neo4j health check failed:', error); return false; } } } export default Neo4jDriver; //# sourceMappingURL=neo4j.js.map