# Directory Structure
```
├── .github
│ └── workflows
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── docker-compose.yml
├── Dockerfile
├── LICENSE
├── package.json
├── README.md
├── src
│ ├── index.ts
│ ├── logger.ts
│ ├── manager.ts
│ ├── types.ts
│ └── utils.ts
├── test
│ └── test-script.js
├── tsconfig.json
└── tsup.config.ts
```
# Files
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | FROM node:22-alpine
2 |
3 | WORKDIR /app
4 |
5 | COPY package.json ./
6 | COPY tsconfig.json ./
7 | COPY src ./src
8 |
9 | RUN npm install
10 | RUN npm run build
11 |
12 | CMD ["npm", "start"]
```
--------------------------------------------------------------------------------
/tsup.config.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { defineConfig } from 'tsup';
2 |
3 | export default defineConfig({
4 | entry: ['src/index.ts'],
5 | format: ['esm'],
6 | dts: true,
7 | clean: true,
8 | outExtension: () => ({
9 | js: '.mjs',
10 | }),
11 | });
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "NodeNext",
5 | "moduleResolution": "NodeNext",
6 | "esModuleInterop": true,
7 | "strict": true,
8 | "skipLibCheck": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "outDir": "dist",
11 | "declaration": true,
12 | "sourceMap": true
13 | },
14 | "include": ["src/**/*"],
15 | "exclude": ["node_modules", "dist"]
16 | }
```
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
```yaml
1 | version: '3'
2 |
3 | services:
4 | neo4j:
5 | image: neo4j:5.18.0
6 | container_name: ai-info-neo4j
7 | restart: always
8 | ports:
9 | - "7474:7474" # HTTP
10 | - "7687:7687" # Bolt
11 | environment:
12 | - NEO4J_AUTH=neo4j/password # 用户名/密码
13 | - NEO4J_apoc_export_file_enabled=true
14 | - NEO4J_apoc_import_file_enabled=true
15 | - NEO4J_apoc_import_file_use__neo4j__config=true
16 | - NEO4J_PLUGINS=["apoc"] # 启用APOC插件
17 | - NEO4J_dbms_security_procedures_unrestricted=apoc.*
18 | - NEO4J_dbms_security_procedures_allowlist=apoc.*
19 | volumes:
20 | - neo4j_data:/data
21 | - neo4j_logs:/logs
22 | - neo4j_import:/var/lib/neo4j/import
23 | - neo4j_plugins:/plugins
24 | healthcheck:
25 | test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:7474" ]
26 | interval: 10s
27 | timeout: 10s
28 | retries: 5
29 | start_period: 40s
```
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from "zod";
2 |
3 | // 实体对象模式
4 | export const EntityObject = z.object({
5 | name: z.string().describe("The name of the entity"),
6 | entityType: z.string().describe("The type of the entity"),
7 | observations: z.array(z.string()).describe("An array of observation contents associated with the entity")
8 | });
9 |
10 | // 关系对象模式
11 | export const RelationObject = z.object({
12 | from: z.string().describe("The name of the entity where the relation starts"),
13 | to: z.string().describe("The name of the entity where the relation ends"),
14 | relationType: z.string().describe("The type of the relation")
15 | });
16 |
17 | // 观察对象模式
18 | export const ObservationObject = z.object({
19 | entityName: z.string().describe("The name of the entity to add the observations to"),
20 | contents: z.array(z.string()).describe("An array of observation contents to add")
21 | });
22 |
23 | // 实体类型
24 | export type Entity = z.infer<typeof EntityObject>;
25 |
26 | // 关系类型
27 | export type Relation = z.infer<typeof RelationObject>;
28 |
29 | // 观察类型
30 | export type Observation = z.infer<typeof ObservationObject>;
31 |
32 | // 知识图谱类型
33 | export interface KnowledgeGraph {
34 | entities: Entity[];
35 | relations: Relation[];
36 | }
37 |
38 | // 删除观察类型
39 | export interface ObservationDeletion {
40 | entityName: string;
41 | contents: string[];
42 | }
```
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | concurrency: ${{ github.workflow }}-${{ github.ref }}
9 |
10 | jobs:
11 | release:
12 | name: Release
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout Repo
16 | uses: actions/checkout@v3
17 | with:
18 | fetch-depth: 0
19 |
20 | - name: Setup Node.js
21 | uses: actions/setup-node@v3
22 | with:
23 | node-version: '22'
24 | registry-url: 'https://registry.npmjs.org'
25 |
26 | - name: Install pnpm
27 | uses: pnpm/action-setup@v2
28 | with:
29 | version: 8
30 | run_install: false
31 |
32 | - name: Get pnpm store directory
33 | id: pnpm-cache
34 | shell: bash
35 | run: |
36 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
37 |
38 | - uses: actions/cache@v3
39 | name: Setup pnpm cache
40 | with:
41 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
42 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
43 | restore-keys: |
44 | ${{ runner.os }}-pnpm-store-
45 |
46 | - name: Install dependencies
47 | run: pnpm install
48 |
49 | - name: Build
50 | run: pnpm build
51 |
52 | - name: Create Release Pull Request or Publish to npm
53 | id: changesets
54 | uses: changesets/action@v1
55 | with:
56 | publish: pnpm release
57 | env:
58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
59 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 |
13 | services:
14 | neo4j:
15 | image: neo4j:5-community
16 | env:
17 | NEO4J_AUTH: neo4j/password
18 | ports:
19 | - 7474:7474
20 | - 7687:7687
21 | options: >-
22 | --health-cmd "cypher-shell -u neo4j -p password 'RETURN 1;'"
23 | --health-interval 10s
24 | --health-timeout 5s
25 | --health-retries 5
26 |
27 | steps:
28 | - uses: actions/checkout@v3
29 |
30 | - name: Setup Node.js
31 | uses: actions/setup-node@v3
32 | with:
33 | node-version: '22'
34 |
35 | - name: Install pnpm
36 | uses: pnpm/action-setup@v2
37 | with:
38 | version: 8
39 | run_install: false
40 |
41 | - name: Get pnpm store directory
42 | id: pnpm-cache
43 | shell: bash
44 | run: |
45 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
46 |
47 | - uses: actions/cache@v3
48 | name: Setup pnpm cache
49 | with:
50 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
51 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
52 | restore-keys: |
53 | ${{ runner.os }}-pnpm-store-
54 |
55 | - name: Install dependencies
56 | run: pnpm install
57 |
58 | - name: Build
59 | run: pnpm build
60 |
61 | - name: Run tests
62 | run: pnpm test
63 | env:
64 | NEO4J_URI: bolt://localhost:7687
65 | NEO4J_USER: neo4j
66 | NEO4J_PASSWORD: password
67 | NEO4J_DATABASE: neo4j
```
--------------------------------------------------------------------------------
/src/logger.ts:
--------------------------------------------------------------------------------
```typescript
1 | // 日志级别枚举
2 | export enum LogLevel {
3 | DEBUG = "debug",
4 | INFO = "info",
5 | WARN = "warning",
6 | ERROR = "error",
7 | }
8 |
9 | // 日志记录器接口
10 | export interface Logger {
11 | debug(message: string, payload?: any): void;
12 | info(message: string, payload?: any): void;
13 | warn(message: string, payload?: any): void;
14 | error(message: string, payload?: any): void;
15 | setLevel(level: LogLevel): void;
16 | }
17 |
18 | // 空日志记录器实现
19 | export class NullLogger implements Logger {
20 | debug(message: string, payload?: any): void {}
21 | info(message: string, payload?: any): void {}
22 | warn(message: string, payload?: any): void {}
23 | error(message: string, payload?: any): void {}
24 | setLevel(level: LogLevel): void {}
25 | }
26 |
27 | // 控制台日志记录器实现
28 | export class ConsoleLogger implements Logger {
29 | private level: LogLevel = LogLevel.INFO;
30 |
31 | setLevel(level: LogLevel): void {
32 | this.level = level;
33 | }
34 |
35 | debug(message: string, payload?: any): void {
36 | if (this.shouldLog(LogLevel.DEBUG)) {
37 | console.debug(message, payload);
38 | }
39 | }
40 |
41 | info(message: string, payload?: any): void {
42 | if (this.shouldLog(LogLevel.INFO)) {
43 | console.info(message, payload);
44 | }
45 | }
46 |
47 | warn(message: string, payload?: any): void {
48 | if (this.shouldLog(LogLevel.WARN)) {
49 | console.warn(message, payload);
50 | }
51 | }
52 |
53 | error(message: string, payload?: any): void {
54 | if (this.shouldLog(LogLevel.ERROR)) {
55 | console.error(message, payload);
56 | }
57 | }
58 |
59 | private shouldLog(messageLevel: LogLevel): boolean {
60 | const levels = [
61 | LogLevel.DEBUG,
62 | LogLevel.INFO,
63 | LogLevel.WARN,
64 | LogLevel.ERROR,
65 | ];
66 | return levels.indexOf(messageLevel) >= levels.indexOf(this.level);
67 | }
68 | }
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "@jovanhsu/mcp-neo4j-memory-server",
3 | "version": "1.0.0",
4 | "private": false,
5 | "description": "MCP Memory Server with Neo4j backend for AI knowledge graph storage",
6 | "homepage": "https://github.com/JovanHsu/mcp-neo4j-memory-server",
7 | "repository": {
8 | "type": "git",
9 | "url": "git+https://github.com/JovanHsu/mcp-neo4j-memory-server.git"
10 | },
11 | "type": "module",
12 | "main": "dist/index.mjs",
13 | "types": "dist/index.d.ts",
14 | "bin": {
15 | "mcp-neo4j-memory-server": "dist/index.mjs"
16 | },
17 | "files": [
18 | "dist",
19 | "README.md",
20 | "LICENSE"
21 | ],
22 | "keywords": [
23 | "mcp",
24 | "memory",
25 | "knowledge",
26 | "graph",
27 | "neo4j",
28 | "ai",
29 | "claude",
30 | "anthropic",
31 | "knowledge-graph",
32 | "memory-server",
33 | "model-context-protocol"
34 | ],
35 | "author": "JovanHsu",
36 | "license": "MIT",
37 | "bugs": {
38 | "url": "https://github.com/JovanHsu/mcp-neo4j-memory-server/issues"
39 | },
40 | "dependencies": {
41 | "@modelcontextprotocol/sdk": "^1.6.0",
42 | "neo4j-driver": "^5.18.0",
43 | "fuse.js": "^7.1.0",
44 | "zod": "^3.24.2"
45 | },
46 | "devDependencies": {
47 | "@changesets/cli": "^2.28.1",
48 | "@types/node": "^22.13.5",
49 | "prettier": "^3.5.2",
50 | "shx": "^0.3.4",
51 | "tsup": "^8.4.0",
52 | "typescript": "^5.7.3",
53 | "vitest": "^3.0.7"
54 | },
55 | "engines": {
56 | "node": ">=22.0.0"
57 | },
58 | "scripts": {
59 | "dev": "pnpm build && npx @modelcontextprotocol/inspector pnpm start",
60 | "build": "tsup src/index.ts && shx chmod +x dist/index.mjs",
61 | "start": "node dist/index.mjs 2>/dev/null",
62 | "test": "vitest run",
63 | "lint": "prettier --write .",
64 | "prepublishOnly": "pnpm build",
65 | "release": "pnpm build && changeset publish"
66 | },
67 | "publishConfig": {
68 | "access": "public"
69 | }
70 | }
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3 | import { z } from 'zod';
4 | import { ConsoleLogger, LogLevel } from './logger.js';
5 | import { Neo4jKnowledgeGraphManager } from './manager.js';
6 | import { EntityObject, ObservationObject, RelationObject } from './types.js';
7 | import { extractError } from './utils.js';
8 |
9 | // 创建MCP服务器
10 | const server = new McpServer({
11 | name: 'neo4j-memory-server',
12 | version: '1.0.0',
13 | });
14 |
15 | // 创建日志记录器,并设置为仅输出错误信息
16 | const logger = new ConsoleLogger();
17 | logger.setLevel(LogLevel.ERROR);
18 |
19 | // 创建知识图谱管理器
20 | const knowledgeGraphManager = new Neo4jKnowledgeGraphManager(
21 | /**
22 | * 根据环境变量获取Neo4j配置
23 | * @returns Neo4j配置
24 | */
25 | () => {
26 | return {
27 | uri: process.env.NEO4J_URI || 'bolt://localhost:7687',
28 | user: process.env.NEO4J_USER || 'neo4j',
29 | password: process.env.NEO4J_PASSWORD || 'password',
30 | database: process.env.NEO4J_DATABASE || 'neo4j',
31 | };
32 | },
33 | logger
34 | );
35 |
36 | // 注册创建实体工具
37 | server.tool(
38 | 'create_entities',
39 | 'Create multiple new entities in the knowledge graph',
40 | {
41 | entities: z.array(EntityObject),
42 | },
43 | async ({ entities }) => ({
44 | content: [
45 | {
46 | type: 'text',
47 | text: JSON.stringify(
48 | await knowledgeGraphManager.createEntities(entities),
49 | null,
50 | 2
51 | ),
52 | },
53 | ],
54 | })
55 | );
56 |
57 | // 注册创建关系工具
58 | server.tool(
59 | 'create_relations',
60 | 'Create multiple new relations between entities in the knowledge graph. Relations should be in active voice',
61 | {
62 | relations: z.array(RelationObject),
63 | },
64 | async ({ relations }) => ({
65 | content: [
66 | {
67 | type: 'text',
68 | text: JSON.stringify(
69 | await knowledgeGraphManager.createRelations(relations),
70 | null,
71 | 2
72 | ),
73 | },
74 | ],
75 | })
76 | );
77 |
78 | // 注册添加观察工具
79 | server.tool(
80 | 'add_observations',
81 | 'Add new observations to existing entities in the knowledge graph',
82 | {
83 | observations: z.array(ObservationObject),
84 | },
85 | async ({ observations }) => ({
86 | content: [
87 | {
88 | type: 'text',
89 | text: JSON.stringify(
90 | await knowledgeGraphManager.addObservations(observations),
91 | null,
92 | 2
93 | ),
94 | },
95 | ],
96 | })
97 | );
98 |
99 | // 注册删除实体工具
100 | server.tool(
101 | 'delete_entities',
102 | 'Delete multiple entities and their associated relations from the knowledge graph',
103 | {
104 | entityNames: z
105 | .array(z.string())
106 | .describe('An array of entity names to delete'),
107 | },
108 | async ({ entityNames }) => {
109 | await knowledgeGraphManager.deleteEntities(entityNames);
110 | return {
111 | content: [{ type: 'text', text: 'Entities deleted successfully' }],
112 | };
113 | }
114 | );
115 |
116 | // 注册删除观察工具
117 | server.tool(
118 | 'delete_observations',
119 | 'Delete specific observations from entities in the knowledge graph',
120 | {
121 | deletions: z.array(
122 | z.object({
123 | entityName: z
124 | .string()
125 | .describe('The name of the entity containing the observations'),
126 | contents: z
127 | .array(z.string())
128 | .describe('An array of observations to delete'),
129 | })
130 | ),
131 | },
132 | async ({ deletions }) => {
133 | await knowledgeGraphManager.deleteObservations(deletions);
134 | return {
135 | content: [{ type: 'text', text: 'Observations deleted successfully' }],
136 | };
137 | }
138 | );
139 |
140 | // 注册删除关系工具
141 | server.tool(
142 | 'delete_relations',
143 | 'Delete multiple relations from the knowledge graph',
144 | {
145 | relations: z
146 | .array(
147 | z.object({
148 | from: z
149 | .string()
150 | .describe('The name of the entity where the relation starts'),
151 | to: z
152 | .string()
153 | .describe('The name of the entity where the relation ends'),
154 | relationType: z.string().describe('The type of the relation'),
155 | })
156 | )
157 | .describe('An array of relations to delete'),
158 | },
159 | async ({ relations }) => {
160 | await knowledgeGraphManager.deleteRelations(relations);
161 | return {
162 | content: [{ type: 'text', text: 'Relations deleted successfully' }],
163 | };
164 | }
165 | );
166 |
167 | // 注册搜索节点工具
168 | server.tool(
169 | 'search_nodes',
170 | 'Search for nodes in the knowledge graph based on a query',
171 | {
172 | query: z
173 | .string()
174 | .describe(
175 | 'The search query to match against entity names, types, and observation content'
176 | ),
177 | },
178 | async ({ query }) => ({
179 | content: [
180 | {
181 | type: 'text',
182 | text: JSON.stringify(
183 | await knowledgeGraphManager.searchNodes(query),
184 | null,
185 | 2
186 | ),
187 | },
188 | ],
189 | })
190 | );
191 |
192 | // 注册打开节点工具
193 | server.tool(
194 | 'open_nodes',
195 | 'Open specific nodes in the knowledge graph by their names',
196 | {
197 | names: z.array(z.string()).describe('An array of entity names to retrieve'),
198 | },
199 | async ({ names }) => ({
200 | content: [
201 | {
202 | type: 'text',
203 | text: JSON.stringify(
204 | await knowledgeGraphManager.openNodes(names),
205 | null,
206 | 2
207 | ),
208 | },
209 | ],
210 | })
211 | );
212 |
213 | // 主函数
214 | const main = async () => {
215 | try {
216 | // 初始化知识图谱管理器
217 | await knowledgeGraphManager.initialize();
218 |
219 | // 创建传输层
220 | const transport = new StdioServerTransport();
221 |
222 | // 连接服务器
223 | await server.connect(transport);
224 |
225 | // 使用logger代替console.info
226 | logger.info('Neo4j Knowledge Graph MCP Server running on stdio');
227 | } catch (error) {
228 | // 使用logger代替console.error
229 | logger.error('Failed to start server:', extractError(error));
230 | process.exit(1);
231 | }
232 | };
233 |
234 | // 启动服务器
235 | main().catch((error) => {
236 | // 使用logger代替console.error
237 | logger.error('Error during server startup:', extractError(error));
238 | process.exit(1);
239 | });
```
--------------------------------------------------------------------------------
/src/manager.ts:
--------------------------------------------------------------------------------
```typescript
1 | import Fuse from 'fuse.js';
2 | import neo4j, { Driver, Session } from 'neo4j-driver';
3 | import { ConsoleLogger, Logger } from './logger.js';
4 | import { Entity, KnowledgeGraph, Observation, ObservationDeletion, Relation } from './types.js';
5 | import { extractError } from './utils.js';
6 |
7 | /**
8 | * Neo4j知识图谱管理器
9 | */
10 | export class Neo4jKnowledgeGraphManager {
11 | private driver: Driver | null = null;
12 | private fuse: Fuse<Entity>;
13 | private initialized = false;
14 | private logger: Logger;
15 | private uri: string;
16 | private user: string;
17 | private password: string;
18 | private database: string;
19 |
20 | /**
21 | * 构造函数
22 | * @param configResolver 配置解析器函数
23 | * @param logger 可选的日志记录器
24 | */
25 | constructor(
26 | configResolver: () => {
27 | uri: string;
28 | user: string;
29 | password: string;
30 | database: string;
31 | },
32 | logger?: Logger
33 | ) {
34 | const config = configResolver();
35 | this.uri = config.uri;
36 | this.user = config.user;
37 | this.password = config.password;
38 | this.database = config.database;
39 | this.logger = logger || new ConsoleLogger();
40 |
41 | this.fuse = new Fuse([], {
42 | keys: ['name', 'entityType', 'observations'],
43 | includeScore: true,
44 | threshold: 0.4, // 搜索严格度(越接近0越严格)
45 | });
46 | }
47 |
48 | /**
49 | * 获取会话
50 | * @returns Neo4j会话
51 | */
52 | private async getSession(): Promise<Session> {
53 | if (!this.driver) {
54 | await this.initialize();
55 | }
56 | return this.driver!.session({ database: this.database });
57 | }
58 |
59 | /**
60 | * 初始化数据库
61 | */
62 | public async initialize(): Promise<void> {
63 | if (this.initialized) return;
64 |
65 | try {
66 | if (!this.driver) {
67 | this.driver = neo4j.driver(
68 | this.uri,
69 | neo4j.auth.basic(this.user, this.password),
70 | { maxConnectionLifetime: 3 * 60 * 60 * 1000 } // 3小时
71 | );
72 | }
73 |
74 | const session = await this.getSession();
75 | try {
76 | // 创建约束和索引
77 | await session.run(`
78 | CREATE CONSTRAINT entity_name_unique IF NOT EXISTS
79 | FOR (e:Entity) REQUIRE e.name IS UNIQUE
80 | `);
81 |
82 | await session.run(`
83 | CREATE INDEX entity_type_index IF NOT EXISTS
84 | FOR (e:Entity) ON (e.entityType)
85 | `);
86 |
87 | // 使用新的语法创建全文索引
88 | await session.run(`
89 | CREATE FULLTEXT INDEX entity_fulltext IF NOT EXISTS
90 | FOR (e:Entity)
91 | ON EACH [e.name, e.entityType]
92 | `);
93 |
94 | // 加载所有实体到Fuse.js
95 | const entities = await this.getAllEntities();
96 | this.fuse.setCollection(entities);
97 |
98 | this.initialized = true;
99 | } finally {
100 | await session.close();
101 | }
102 | } catch (error) {
103 | this.logger.error('Failed to initialize database', extractError(error));
104 | throw error;
105 | }
106 | }
107 |
108 | /**
109 | * 获取所有实体
110 | * @returns 所有实体数组
111 | */
112 | private async getAllEntities(): Promise<Entity[]> {
113 | const session = await this.getSession();
114 | try {
115 | const result = await session.run(`
116 | MATCH (e:Entity)
117 | OPTIONAL MATCH (e)-[r:HAS_OBSERVATION]->(o)
118 | RETURN e.name AS name, e.entityType AS entityType, collect(o.content) AS observations
119 | `);
120 |
121 | const entities: Entity[] = result.records.map(record => {
122 | return {
123 | name: record.get('name'),
124 | entityType: record.get('entityType'),
125 | observations: record.get('observations').filter(Boolean)
126 | };
127 | });
128 |
129 | return entities;
130 | } catch (error) {
131 | this.logger.error('Error getting all entities', extractError(error));
132 | return [];
133 | } finally {
134 | await session.close();
135 | }
136 | }
137 |
138 | /**
139 | * 创建实体
140 | * @param entities 要创建的实体数组
141 | * @returns 创建的实体数组
142 | */
143 | public async createEntities(entities: Entity[]): Promise<Entity[]> {
144 | if (entities.length === 0) return [];
145 |
146 | const session = await this.getSession();
147 | try {
148 | const createdEntities: Entity[] = [];
149 | const tx = session.beginTransaction();
150 |
151 | try {
152 | // 获取现有实体名称
153 | const existingEntitiesResult = await tx.run(
154 | 'MATCH (e:Entity) RETURN e.name AS name'
155 | );
156 | const existingNames = new Set(
157 | existingEntitiesResult.records.map(record => record.get('name'))
158 | );
159 |
160 | // 过滤出新实体
161 | const newEntities = entities.filter(
162 | entity => !existingNames.has(entity.name)
163 | );
164 |
165 | // 创建新实体
166 | for (const entity of newEntities) {
167 | await tx.run(
168 | `
169 | CREATE (e:Entity {name: $name, entityType: $entityType})
170 | WITH e
171 | UNWIND $observations AS observation
172 | CREATE (o:Observation {content: observation})
173 | CREATE (e)-[:HAS_OBSERVATION]->(o)
174 | RETURN e
175 | `,
176 | {
177 | name: entity.name,
178 | entityType: entity.entityType,
179 | observations: entity.observations
180 | }
181 | );
182 | createdEntities.push(entity);
183 | }
184 |
185 | await tx.commit();
186 |
187 | // 更新Fuse.js集合
188 | const allEntities = await this.getAllEntities();
189 | this.fuse.setCollection(allEntities);
190 |
191 | return createdEntities;
192 | } catch (error) {
193 | await tx.rollback();
194 | this.logger.error('Error creating entities', extractError(error));
195 | throw error;
196 | }
197 | } finally {
198 | await session.close();
199 | }
200 | }
201 |
202 | /**
203 | * 创建关系
204 | * @param relations 要创建的关系数组
205 | * @returns 创建的关系数组
206 | */
207 | public async createRelations(relations: Relation[]): Promise<Relation[]> {
208 | if (relations.length === 0) return [];
209 |
210 | const session = await this.getSession();
211 | try {
212 | const tx = session.beginTransaction();
213 | try {
214 | // 获取所有实体名称
215 | const entityNamesResult = await tx.run(
216 | 'MATCH (e:Entity) RETURN e.name AS name'
217 | );
218 | const entityNames = new Set(
219 | entityNamesResult.records.map(record => record.get('name'))
220 | );
221 |
222 | // 过滤出有效关系(源实体和目标实体都存在)
223 | const validRelations = relations.filter(
224 | relation => entityNames.has(relation.from) && entityNames.has(relation.to)
225 | );
226 |
227 | // 获取现有关系
228 | const existingRelationsResult = await tx.run(`
229 | MATCH (from:Entity)-[r]->(to:Entity)
230 | RETURN from.name AS fromName, to.name AS toName, type(r) AS relationType
231 | `);
232 |
233 | const existingRelations = existingRelationsResult.records.map(record => {
234 | return {
235 | from: record.get('fromName'),
236 | to: record.get('toName'),
237 | relationType: record.get('relationType')
238 | };
239 | });
240 |
241 | // 过滤出新关系
242 | const newRelations = validRelations.filter(
243 | newRel => !existingRelations.some(
244 | existingRel =>
245 | existingRel.from === newRel.from &&
246 | existingRel.to === newRel.to &&
247 | existingRel.relationType === newRel.relationType
248 | )
249 | );
250 |
251 | // 创建新关系
252 | for (const relation of newRelations) {
253 | await tx.run(
254 | `
255 | MATCH (from:Entity {name: $fromName})
256 | MATCH (to:Entity {name: $toName})
257 | CREATE (from)-[r:${relation.relationType}]->(to)
258 | RETURN r
259 | `,
260 | {
261 | fromName: relation.from,
262 | toName: relation.to
263 | }
264 | );
265 | }
266 |
267 | await tx.commit();
268 | return newRelations;
269 | } catch (error) {
270 | await tx.rollback();
271 | this.logger.error('Error creating relations', extractError(error));
272 | throw error;
273 | }
274 | } finally {
275 | await session.close();
276 | }
277 | }
278 |
279 | /**
280 | * 添加观察
281 | * @param observations 要添加的观察数组
282 | * @returns 添加的观察数组
283 | */
284 | public async addObservations(observations: Observation[]): Promise<Observation[]> {
285 | if (observations.length === 0) return [];
286 |
287 | const session = await this.getSession();
288 | try {
289 | const addedObservations: Observation[] = [];
290 | const tx = session.beginTransaction();
291 |
292 | try {
293 | for (const observation of observations) {
294 | // 检查实体是否存在
295 | const entityResult = await tx.run(
296 | 'MATCH (e:Entity {name: $name}) RETURN e',
297 | { name: observation.entityName }
298 | );
299 |
300 | if (entityResult.records.length > 0) {
301 | // 获取现有观察
302 | const existingObservationsResult = await tx.run(
303 | `
304 | MATCH (e:Entity {name: $name})-[:HAS_OBSERVATION]->(o:Observation)
305 | RETURN o.content AS content
306 | `,
307 | { name: observation.entityName }
308 | );
309 |
310 | const existingObservations = new Set(
311 | existingObservationsResult.records.map(record => record.get('content'))
312 | );
313 |
314 | // 过滤出新观察
315 | const newContents = observation.contents.filter(
316 | content => !existingObservations.has(content)
317 | );
318 |
319 | if (newContents.length > 0) {
320 | // 添加新观察
321 | await tx.run(
322 | `
323 | MATCH (e:Entity {name: $name})
324 | UNWIND $contents AS content
325 | CREATE (o:Observation {content: content})
326 | CREATE (e)-[:HAS_OBSERVATION]->(o)
327 | `,
328 | {
329 | name: observation.entityName,
330 | contents: newContents
331 | }
332 | );
333 |
334 | addedObservations.push({
335 | entityName: observation.entityName,
336 | contents: newContents
337 | });
338 | }
339 | }
340 | }
341 |
342 | await tx.commit();
343 |
344 | // 更新Fuse.js集合
345 | const allEntities = await this.getAllEntities();
346 | this.fuse.setCollection(allEntities);
347 |
348 | return addedObservations;
349 | } catch (error) {
350 | await tx.rollback();
351 | this.logger.error('Error adding observations', extractError(error));
352 | throw error;
353 | }
354 | } finally {
355 | await session.close();
356 | }
357 | }
358 |
359 | /**
360 | * 删除实体
361 | * @param entityNames 要删除的实体名称数组
362 | */
363 | public async deleteEntities(entityNames: string[]): Promise<void> {
364 | if (entityNames.length === 0) return;
365 |
366 | const session = await this.getSession();
367 | try {
368 | const tx = session.beginTransaction();
369 | try {
370 | // 删除实体及其关联的观察和关系
371 | await tx.run(
372 | `
373 | UNWIND $names AS name
374 | MATCH (e:Entity {name: name})
375 | OPTIONAL MATCH (e)-[:HAS_OBSERVATION]->(o:Observation)
376 | DETACH DELETE e, o
377 | `,
378 | { names: entityNames }
379 | );
380 |
381 | await tx.commit();
382 |
383 | // 更新Fuse.js集合
384 | const allEntities = await this.getAllEntities();
385 | this.fuse.setCollection(allEntities);
386 | } catch (error) {
387 | await tx.rollback();
388 | this.logger.error('Error deleting entities', extractError(error));
389 | throw error;
390 | }
391 | } finally {
392 | await session.close();
393 | }
394 | }
395 |
396 | /**
397 | * 删除观察
398 | * @param deletions 要删除的观察数组
399 | */
400 | public async deleteObservations(deletions: ObservationDeletion[]): Promise<void> {
401 | if (deletions.length === 0) return;
402 |
403 | const session = await this.getSession();
404 | try {
405 | const tx = session.beginTransaction();
406 | try {
407 | for (const deletion of deletions) {
408 | if (deletion.contents.length > 0) {
409 | await tx.run(
410 | `
411 | MATCH (e:Entity {name: $name})-[:HAS_OBSERVATION]->(o:Observation)
412 | WHERE o.content IN $contents
413 | DETACH DELETE o
414 | `,
415 | {
416 | name: deletion.entityName,
417 | contents: deletion.contents
418 | }
419 | );
420 | }
421 | }
422 |
423 | await tx.commit();
424 |
425 | // 更新Fuse.js集合
426 | const allEntities = await this.getAllEntities();
427 | this.fuse.setCollection(allEntities);
428 | } catch (error) {
429 | await tx.rollback();
430 | this.logger.error('Error deleting observations', extractError(error));
431 | throw error;
432 | }
433 | } finally {
434 | await session.close();
435 | }
436 | }
437 |
438 | /**
439 | * 删除关系
440 | * @param relations 要删除的关系数组
441 | */
442 | public async deleteRelations(relations: Relation[]): Promise<void> {
443 | if (relations.length === 0) return;
444 |
445 | const session = await this.getSession();
446 | try {
447 | const tx = session.beginTransaction();
448 | try {
449 | for (const relation of relations) {
450 | await tx.run(
451 | `
452 | MATCH (from:Entity {name: $fromName})-[r:${relation.relationType}]->(to:Entity {name: $toName})
453 | DELETE r
454 | `,
455 | {
456 | fromName: relation.from,
457 | toName: relation.to
458 | }
459 | );
460 | }
461 |
462 | await tx.commit();
463 | } catch (error) {
464 | await tx.rollback();
465 | this.logger.error('Error deleting relations', extractError(error));
466 | throw error;
467 | }
468 | } finally {
469 | await session.close();
470 | }
471 | }
472 |
473 | /**
474 | * 搜索节点
475 | * @param query 搜索查询
476 | * @returns 包含匹配实体和关系的知识图谱
477 | */
478 | public async searchNodes(query: string): Promise<KnowledgeGraph> {
479 | if (!query || query.trim() === '') {
480 | return { entities: [], relations: [] };
481 | }
482 |
483 | const session = await this.getSession();
484 | try {
485 | // 使用Neo4j全文搜索
486 | const searchResult = await session.run(
487 | `
488 | CALL db.index.fulltext.queryNodes("entity_fulltext", $query)
489 | YIELD node, score
490 | RETURN node
491 | `,
492 | { query }
493 | );
494 |
495 | // 获取所有实体用于Fuse.js搜索
496 | const allEntities = await this.getAllEntities();
497 | this.fuse.setCollection(allEntities);
498 |
499 | // 使用Fuse.js进行模糊搜索
500 | const fuseResults = this.fuse.search(query);
501 |
502 | // 合并搜索结果
503 | const uniqueEntities = new Map<string, Entity>();
504 |
505 | // 添加Neo4j搜索结果
506 | for (const record of searchResult.records) {
507 | const node = record.get('node');
508 | const name = node.properties.name;
509 |
510 | if (!uniqueEntities.has(name)) {
511 | const entity = allEntities.find(e => e.name === name);
512 | if (entity) {
513 | uniqueEntities.set(name, entity);
514 | }
515 | }
516 | }
517 |
518 | // 添加Fuse.js搜索结果
519 | for (const result of fuseResults) {
520 | if (!uniqueEntities.has(result.item.name)) {
521 | uniqueEntities.set(result.item.name, result.item);
522 | }
523 | }
524 |
525 | const entities = Array.from(uniqueEntities.values());
526 | const entityNames = entities.map(entity => entity.name);
527 |
528 | if (entityNames.length === 0) {
529 | return { entities: [], relations: [] };
530 | }
531 |
532 | // 获取关系
533 | const relationsResult = await session.run(
534 | `
535 | MATCH (from:Entity)-[r]->(to:Entity)
536 | WHERE from.name IN $names OR to.name IN $names
537 | RETURN from.name AS fromName, to.name AS toName, type(r) AS relationType
538 | `,
539 | { names: entityNames }
540 | );
541 |
542 | const relations: Relation[] = relationsResult.records.map(record => {
543 | return {
544 | from: record.get('fromName'),
545 | to: record.get('toName'),
546 | relationType: record.get('relationType')
547 | };
548 | });
549 |
550 | return {
551 | entities,
552 | relations
553 | };
554 | } catch (error) {
555 | this.logger.error('Error searching nodes', extractError(error));
556 | return { entities: [], relations: [] };
557 | } finally {
558 | await session.close();
559 | }
560 | }
561 |
562 | /**
563 | * 打开节点
564 | * @param names 要打开的节点名称数组
565 | * @returns 包含匹配实体和关系的知识图谱
566 | */
567 | public async openNodes(names: string[]): Promise<KnowledgeGraph> {
568 | if (names.length === 0) {
569 | return { entities: [], relations: [] };
570 | }
571 |
572 | const session = await this.getSession();
573 | try {
574 | // 获取实体及其观察
575 | const entitiesResult = await session.run(
576 | `
577 | MATCH (e:Entity)
578 | WHERE e.name IN $names
579 | OPTIONAL MATCH (e)-[:HAS_OBSERVATION]->(o:Observation)
580 | RETURN e.name AS name, e.entityType AS entityType, collect(o.content) AS observations
581 | `,
582 | { names }
583 | );
584 |
585 | const entities: Entity[] = entitiesResult.records.map(record => {
586 | return {
587 | name: record.get('name'),
588 | entityType: record.get('entityType'),
589 | observations: record.get('observations').filter(Boolean)
590 | };
591 | });
592 |
593 | const entityNames = entities.map(entity => entity.name);
594 |
595 | if (entityNames.length > 0) {
596 | // 获取关系
597 | const relationsResult = await session.run(
598 | `
599 | MATCH (from:Entity)-[r]->(to:Entity)
600 | WHERE from.name IN $names OR to.name IN $names
601 | RETURN from.name AS fromName, to.name AS toName, type(r) AS relationType
602 | `,
603 | { names: entityNames }
604 | );
605 |
606 | const relations: Relation[] = relationsResult.records.map(record => {
607 | return {
608 | from: record.get('fromName'),
609 | to: record.get('toName'),
610 | relationType: record.get('relationType')
611 | };
612 | });
613 |
614 | return {
615 | entities,
616 | relations
617 | };
618 | } else {
619 | return {
620 | entities,
621 | relations: []
622 | };
623 | }
624 | } catch (error) {
625 | this.logger.error('Error opening nodes', extractError(error));
626 | return { entities: [], relations: [] };
627 | } finally {
628 | await session.close();
629 | }
630 | }
631 |
632 | /**
633 | * 关闭连接
634 | */
635 | public async close(): Promise<void> {
636 | if (this.driver) {
637 | await this.driver.close();
638 | this.driver = null;
639 | }
640 | }
641 | }
```