@yun-zero/claw-memory 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +68 -0
- package/README.md +323 -0
- package/dist/config/llm.d.ts +13 -0
- package/dist/config/llm.d.ts.map +1 -0
- package/dist/config/llm.js +96 -0
- package/dist/config/llm.js.map +1 -0
- package/dist/config/plugin.d.ts +15 -0
- package/dist/config/plugin.d.ts.map +1 -0
- package/dist/config/plugin.js +32 -0
- package/dist/config/plugin.js.map +1 -0
- package/dist/db/entityRepository.d.ts +21 -0
- package/dist/db/entityRepository.d.ts.map +1 -0
- package/dist/db/entityRepository.js +55 -0
- package/dist/db/entityRepository.js.map +1 -0
- package/dist/db/repository.d.ts +22 -0
- package/dist/db/repository.d.ts.map +1 -0
- package/dist/db/repository.js +77 -0
- package/dist/db/repository.js.map +1 -0
- package/dist/db/schema.d.ts +5 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +112 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/db/todoRepository.d.ts +26 -0
- package/dist/db/todoRepository.d.ts.map +1 -0
- package/dist/db/todoRepository.js +54 -0
- package/dist/db/todoRepository.js.map +1 -0
- package/dist/hooks/bootstrap.d.ts +3 -0
- package/dist/hooks/bootstrap.d.ts.map +1 -0
- package/dist/hooks/bootstrap.js +28 -0
- package/dist/hooks/bootstrap.js.map +1 -0
- package/dist/hooks/message.d.ts +18 -0
- package/dist/hooks/message.d.ts.map +1 -0
- package/dist/hooks/message.js +52 -0
- package/dist/hooks/message.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/tools.d.ts +26 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +360 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/plugin.d.ts +18 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +62 -0
- package/dist/plugin.js.map +1 -0
- package/dist/services/entityGraphService.d.ts +87 -0
- package/dist/services/entityGraphService.d.ts.map +1 -0
- package/dist/services/entityGraphService.js +271 -0
- package/dist/services/entityGraphService.js.map +1 -0
- package/dist/services/memory.d.ts +26 -0
- package/dist/services/memory.d.ts.map +1 -0
- package/dist/services/memory.js +281 -0
- package/dist/services/memory.js.map +1 -0
- package/dist/services/memoryIndex.d.ts +34 -0
- package/dist/services/memoryIndex.d.ts.map +1 -0
- package/dist/services/memoryIndex.js +100 -0
- package/dist/services/memoryIndex.js.map +1 -0
- package/dist/services/metadataExtractor.d.ts +16 -0
- package/dist/services/metadataExtractor.d.ts.map +1 -0
- package/dist/services/metadataExtractor.js +75 -0
- package/dist/services/metadataExtractor.js.map +1 -0
- package/dist/services/retrieval.d.ts +24 -0
- package/dist/services/retrieval.d.ts.map +1 -0
- package/dist/services/retrieval.js +40 -0
- package/dist/services/retrieval.js.map +1 -0
- package/dist/services/scheduler.d.ts +122 -0
- package/dist/services/scheduler.d.ts.map +1 -0
- package/dist/services/scheduler.js +434 -0
- package/dist/services/scheduler.js.map +1 -0
- package/dist/services/summarizer.d.ts +43 -0
- package/dist/services/summarizer.d.ts.map +1 -0
- package/dist/services/summarizer.js +252 -0
- package/dist/services/summarizer.js.map +1 -0
- package/dist/services/tagService.d.ts +64 -0
- package/dist/services/tagService.d.ts.map +1 -0
- package/dist/services/tagService.js +281 -0
- package/dist/services/tagService.js.map +1 -0
- package/dist/tools/memory.d.ts +3 -0
- package/dist/tools/memory.d.ts.map +1 -0
- package/dist/tools/memory.js +114 -0
- package/dist/tools/memory.js.map +1 -0
- package/dist/types.d.ts +128 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/docs/plans/2026-03-02-claw-memory-design.md +445 -0
- package/docs/plans/2026-03-02-incremental-summary-design.md +157 -0
- package/docs/plans/2026-03-02-incremental-summary-implementation.md +468 -0
- package/docs/plans/2026-03-02-memory-index-design.md +163 -0
- package/docs/plans/2026-03-02-memory-index-implementation.md +836 -0
- package/docs/plans/2026-03-02-mvp-implementation.md +1703 -0
- package/docs/plans/2026-03-02-testing-implementation.md +395 -0
- package/docs/plans/2026-03-02-testing-plan.md +93 -0
- package/docs/plans/2026-03-03-claw-memory-openclaw-plugin-design.md +285 -0
- package/docs/plans/2026-03-03-claw-memory-plugin-implementation.md +642 -0
- package/docs/plans/2026-03-03-entity-graph-design.md +121 -0
- package/docs/plans/2026-03-03-entity-graph-implementation.md +687 -0
- package/docs/plans/2026-03-03-llm-generic-config-design.md +43 -0
- package/docs/plans/2026-03-03-llm-generic-config-implementation.md +186 -0
- package/docs/plans/2026-03-03-memory-e2e-stress-test-design.md +110 -0
- package/docs/plans/2026-03-03-memory-e2e-stress-test-implementation.md +464 -0
- package/docs/plans/2026-03-03-minimax-llm-fix.md +156 -0
- package/docs/plans/2026-03-03-scheduler-design.md +165 -0
- package/docs/plans/2026-03-03-scheduler-implementation.md +777 -0
- package/docs/plans/2026-03-03-tags-visualization-design.md +73 -0
- package/docs/plans/2026-03-03-tags-visualization-implementation.md +539 -0
- package/openclaw.plugin.json +11 -0
- package/package.json +41 -0
- package/src/config/llm.ts +129 -0
- package/src/config/plugin.ts +47 -0
- package/src/db/entityRepository.ts +80 -0
- package/src/db/repository.ts +106 -0
- package/src/db/schema.ts +121 -0
- package/src/db/todoRepository.ts +76 -0
- package/src/hooks/bootstrap.ts +36 -0
- package/src/hooks/message.ts +84 -0
- package/src/index.ts +50 -0
- package/src/plugin.ts +85 -0
- package/src/services/entityGraphService.ts +367 -0
- package/src/services/memory.ts +338 -0
- package/src/services/memoryIndex.ts +140 -0
- package/src/services/metadataExtractor.ts +89 -0
- package/src/services/retrieval.ts +71 -0
- package/src/services/scheduler.ts +529 -0
- package/src/services/summarizer.ts +318 -0
- package/src/services/tagService.ts +335 -0
- package/src/tools/memory.ts +137 -0
- package/src/types.ts +139 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +16 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { getDatabase } from './db/schema.js';
|
|
5
|
+
import { TagService } from './services/tagService.js';
|
|
6
|
+
import { writeFile } from 'fs/promises';
|
|
7
|
+
|
|
8
|
+
const program = new Command();
|
|
9
|
+
|
|
10
|
+
program
|
|
11
|
+
.name('claw-memory')
|
|
12
|
+
.description('OpenClaw 记忆插件 - CLI 工具')
|
|
13
|
+
.version('0.1.0');
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.command('init')
|
|
17
|
+
.description('Initialize database')
|
|
18
|
+
.option('-d, --data-dir <dir>', 'Data directory', './memories')
|
|
19
|
+
.action((options) => {
|
|
20
|
+
const db = getDatabase(`${options.dataDir}/memory.db`);
|
|
21
|
+
console.log('Database initialized');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.command('tags <action>')
|
|
26
|
+
.description('标签管理命令')
|
|
27
|
+
.option('-o, --output <file>', '输出文件路径')
|
|
28
|
+
.option('-d, --data-dir <dir>', 'Data directory', './memories')
|
|
29
|
+
.action(async (action, options) => {
|
|
30
|
+
const db = getDatabase(`${options.dataDir}/memory.db`);
|
|
31
|
+
const tagService = new TagService(db);
|
|
32
|
+
const outputFile = options.output || (action === 'tree' ? 'tags-tree.html' : 'tags-stats.html');
|
|
33
|
+
|
|
34
|
+
if (action === 'tree') {
|
|
35
|
+
const data = await tagService.getTagTree();
|
|
36
|
+
const html = tagService.generateTreeHtml(data);
|
|
37
|
+
await writeFile(outputFile, html);
|
|
38
|
+
console.log(`标签树已生成: ${outputFile}`);
|
|
39
|
+
} else if (action === 'stats') {
|
|
40
|
+
const stats = await tagService.getTagStats();
|
|
41
|
+
const html = tagService.generateStatsHtml(stats);
|
|
42
|
+
await writeFile(outputFile, html);
|
|
43
|
+
console.log(`标签统计已生成: ${outputFile}`);
|
|
44
|
+
} else {
|
|
45
|
+
console.error('未知命令: tree 或 stats');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
program.parse();
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { getDatabase } from './db/schema.js';
|
|
2
|
+
import { getConfig, PluginConfig } from './config/plugin.js';
|
|
3
|
+
import { handleMessageSent } from './hooks/message.js';
|
|
4
|
+
import { handleAgentBootstrap } from './hooks/bootstrap.js';
|
|
5
|
+
import { registerMemoryTools } from './tools/memory.js';
|
|
6
|
+
import { Scheduler } from './services/scheduler.js';
|
|
7
|
+
|
|
8
|
+
export interface OpenClawPluginContext {
|
|
9
|
+
hooks: {
|
|
10
|
+
register: (event: string, handler: any) => Promise<void>;
|
|
11
|
+
};
|
|
12
|
+
tools: {
|
|
13
|
+
register: (tool: any) => void;
|
|
14
|
+
};
|
|
15
|
+
config?: any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface OpenClawPlugin {
|
|
19
|
+
name: string;
|
|
20
|
+
version: string;
|
|
21
|
+
register: (context: OpenClawPluginContext) => Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createPlugin(config?: any): OpenClawPlugin {
|
|
25
|
+
const pluginConfig = getConfig(config);
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
name: 'claw-memory',
|
|
29
|
+
version: '0.1.0',
|
|
30
|
+
|
|
31
|
+
async register(context: OpenClawPluginContext) {
|
|
32
|
+
if (!pluginConfig.enabled) {
|
|
33
|
+
console.log('[ClawMemory] Plugin disabled');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log('[ClawMemory] Starting...');
|
|
38
|
+
|
|
39
|
+
// 初始化数据库
|
|
40
|
+
const db = getDatabase(pluginConfig.dataDir + '/memory.db');
|
|
41
|
+
|
|
42
|
+
// 注册 message:sent hook
|
|
43
|
+
await context.hooks.register('message:sent', async (event: any) => {
|
|
44
|
+
if (pluginConfig.autoSave) {
|
|
45
|
+
try {
|
|
46
|
+
await handleMessageSent(event, db, pluginConfig.dataDir);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error('[ClawMemory] Failed to save memory:', error);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// 注册 agent:bootstrap hook
|
|
54
|
+
await context.hooks.register('agent:bootstrap', async (bootstrapContext: any) => {
|
|
55
|
+
try {
|
|
56
|
+
const summary = await handleAgentBootstrap(db);
|
|
57
|
+
if (summary) {
|
|
58
|
+
bootstrapContext.context = bootstrapContext.context || '';
|
|
59
|
+
bootstrapContext.context += '\n\n' + summary;
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error('[ClawMemory] Failed to inject summary:', error);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// 注册 Agent Tools
|
|
67
|
+
registerMemoryTools(context.tools, db, pluginConfig.dataDir);
|
|
68
|
+
|
|
69
|
+
// 启动 Scheduler
|
|
70
|
+
if (pluginConfig.scheduler.enabled) {
|
|
71
|
+
try {
|
|
72
|
+
const scheduler = new Scheduler(db, pluginConfig.scheduler);
|
|
73
|
+
scheduler.start();
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error('[ClawMemory] Failed to start scheduler:', error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log('[ClawMemory] Started successfully');
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 默认导出用于 OpenClaw 加载
|
|
85
|
+
export default createPlugin();
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import type { Entity, EntityRelation } from '../types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Entity node for graph visualization
|
|
6
|
+
*/
|
|
7
|
+
export interface EntityNode {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
type: 'keyword' | 'tag' | 'subject' | 'person' | 'project';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Entity edge for graph visualization
|
|
15
|
+
*/
|
|
16
|
+
export interface EntityEdge {
|
|
17
|
+
source: string;
|
|
18
|
+
target: string;
|
|
19
|
+
type: 'related' | 'parent' | 'similar' | 'co_occur';
|
|
20
|
+
weight: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Graph data structure for visualization
|
|
25
|
+
*/
|
|
26
|
+
export interface GraphData {
|
|
27
|
+
nodes: EntityNode[];
|
|
28
|
+
edges: EntityEdge[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Relation statistics
|
|
33
|
+
*/
|
|
34
|
+
export interface RelationStats {
|
|
35
|
+
most_connected: { id: string; name: string; connection_count: number }[];
|
|
36
|
+
relation_types: Record<string, number>;
|
|
37
|
+
total_relations: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Query result for entity graph
|
|
42
|
+
*/
|
|
43
|
+
export interface EntityGraphQueryResult {
|
|
44
|
+
path?: string[];
|
|
45
|
+
nodes: EntityNode[];
|
|
46
|
+
edges: EntityEdge[];
|
|
47
|
+
found: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Service for managing entity graph data and relationships
|
|
52
|
+
*/
|
|
53
|
+
export class EntityGraphService {
|
|
54
|
+
private db: Database.Database;
|
|
55
|
+
|
|
56
|
+
constructor(db: Database.Database) {
|
|
57
|
+
this.db = db;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get entity by name
|
|
62
|
+
*/
|
|
63
|
+
private getEntityByName(name: string): { id: string; name: string; type: string } | undefined {
|
|
64
|
+
const row = this.db.prepare(`
|
|
65
|
+
SELECT id, name, type FROM entities WHERE name = ?
|
|
66
|
+
`).get(name) as { id: string; name: string; type: string } | undefined;
|
|
67
|
+
return row;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get all entities and their relations as graph data
|
|
72
|
+
*/
|
|
73
|
+
getGraphData(): GraphData {
|
|
74
|
+
// Get all entities as nodes
|
|
75
|
+
const entityRows = this.db.prepare(`
|
|
76
|
+
SELECT id, name, type FROM entities
|
|
77
|
+
`).all() as { id: string; name: string; type: string }[];
|
|
78
|
+
|
|
79
|
+
const nodes: EntityNode[] = entityRows.map(row => ({
|
|
80
|
+
id: row.id,
|
|
81
|
+
name: row.name,
|
|
82
|
+
type: row.type as EntityNode['type']
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
// Get all relations as edges
|
|
86
|
+
const relationRows = this.db.prepare(`
|
|
87
|
+
SELECT source_id, target_id, relation_type, weight FROM entity_relations
|
|
88
|
+
`).all() as { source_id: string; target_id: string; relation_type: string; weight: number }[];
|
|
89
|
+
|
|
90
|
+
const edges: EntityEdge[] = relationRows.map(row => ({
|
|
91
|
+
source: row.source_id,
|
|
92
|
+
target: row.target_id,
|
|
93
|
+
type: row.relation_type as EntityEdge['type'],
|
|
94
|
+
weight: row.weight
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
return { nodes, edges };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get relations for a specific entity by name
|
|
102
|
+
*/
|
|
103
|
+
getEntityRelations(entityName: string): EntityEdge[] {
|
|
104
|
+
// First get the entity by name
|
|
105
|
+
const entity = this.getEntityByName(entityName);
|
|
106
|
+
if (!entity) {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const rows = this.db.prepare(`
|
|
111
|
+
SELECT source_id, target_id, relation_type, weight
|
|
112
|
+
FROM entity_relations
|
|
113
|
+
WHERE source_id = ? OR target_id = ?
|
|
114
|
+
`).all(entity.id, entity.id) as { source_id: string; target_id: string; relation_type: string; weight: number }[];
|
|
115
|
+
|
|
116
|
+
return rows.map(row => ({
|
|
117
|
+
source: row.source_id,
|
|
118
|
+
target: row.target_id,
|
|
119
|
+
type: row.relation_type as EntityEdge['type'],
|
|
120
|
+
weight: row.weight
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Query entity graph - find path or subgraph between entities
|
|
126
|
+
*/
|
|
127
|
+
queryEntityGraph(startEntityName: string, endEntityName?: string, maxHops: number = 2): EntityGraphQueryResult {
|
|
128
|
+
// Get start entity
|
|
129
|
+
const startEntity = this.getEntityByName(startEntityName);
|
|
130
|
+
if (!startEntity) {
|
|
131
|
+
return { nodes: [], edges: [], found: false };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// If no end entity, return subgraph around start entity
|
|
135
|
+
if (!endEntityName) {
|
|
136
|
+
const subgraph = this.getSubgraph(startEntity.id, maxHops);
|
|
137
|
+
return {
|
|
138
|
+
nodes: subgraph.nodes,
|
|
139
|
+
edges: subgraph.edges,
|
|
140
|
+
found: true
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Get end entity
|
|
145
|
+
const endEntity = this.getEntityByName(endEntityName);
|
|
146
|
+
if (!endEntity) {
|
|
147
|
+
return { nodes: [], edges: [], found: false };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Find path using BFS
|
|
151
|
+
const path = this.findPath(startEntity.id, endEntity.id, maxHops);
|
|
152
|
+
|
|
153
|
+
if (!path) {
|
|
154
|
+
return { nodes: [], edges: [], found: false };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Get nodes and edges for the path
|
|
158
|
+
const nodes: EntityNode[] = [];
|
|
159
|
+
const edges: EntityEdge[] = [];
|
|
160
|
+
const nodeIds = new Set<string>();
|
|
161
|
+
|
|
162
|
+
for (const entityId of path) {
|
|
163
|
+
const entityRow = this.db.prepare(`
|
|
164
|
+
SELECT id, name, type FROM entities WHERE id = ?
|
|
165
|
+
`).get(entityId) as { id: string; name: string; type: string } | undefined;
|
|
166
|
+
|
|
167
|
+
if (entityRow) {
|
|
168
|
+
nodes.push({
|
|
169
|
+
id: entityRow.id,
|
|
170
|
+
name: entityRow.name,
|
|
171
|
+
type: entityRow.type as EntityNode['type']
|
|
172
|
+
});
|
|
173
|
+
nodeIds.add(entityRow.id);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Get edges between the path nodes
|
|
178
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
179
|
+
const edgeRows = this.db.prepare(`
|
|
180
|
+
SELECT source_id, target_id, relation_type, weight
|
|
181
|
+
FROM entity_relations
|
|
182
|
+
WHERE (source_id = ? AND target_id = ?) OR (source_id = ? AND target_id = ?)
|
|
183
|
+
`).all(path[i], path[i + 1], path[i + 1], path[i]) as { source_id: string; target_id: string; relation_type: string; weight: number }[];
|
|
184
|
+
|
|
185
|
+
for (const row of edgeRows) {
|
|
186
|
+
edges.push({
|
|
187
|
+
source: row.source_id,
|
|
188
|
+
target: row.target_id,
|
|
189
|
+
type: row.relation_type as EntityEdge['type'],
|
|
190
|
+
weight: row.weight
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
path,
|
|
197
|
+
nodes,
|
|
198
|
+
edges,
|
|
199
|
+
found: true
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Find shortest path between two entities using BFS
|
|
205
|
+
*/
|
|
206
|
+
private findPath(startId: string, endId: string, maxHops: number): string[] | null {
|
|
207
|
+
if (startId === endId) {
|
|
208
|
+
return [startId];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const queue: { id: string; path: string[] }[] = [{ id: startId, path: [startId] }];
|
|
212
|
+
const visited = new Set<string>([startId]);
|
|
213
|
+
|
|
214
|
+
while (queue.length > 0) {
|
|
215
|
+
const { id, path } = queue.shift()!;
|
|
216
|
+
|
|
217
|
+
if (path.length > maxHops) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Get connected entities
|
|
222
|
+
const connectedRows = this.db.prepare(`
|
|
223
|
+
SELECT source_id, target_id FROM entity_relations
|
|
224
|
+
WHERE source_id = ? OR target_id = ?
|
|
225
|
+
`).all(id, id) as { source_id: string; target_id: string }[];
|
|
226
|
+
|
|
227
|
+
for (const row of connectedRows) {
|
|
228
|
+
const connectedId = row.source_id === id ? row.target_id : row.source_id;
|
|
229
|
+
|
|
230
|
+
if (connectedId === endId) {
|
|
231
|
+
return [...path, connectedId];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!visited.has(connectedId)) {
|
|
235
|
+
visited.add(connectedId);
|
|
236
|
+
queue.push({ id: connectedId, path: [...path, connectedId] });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get statistics about entity relations
|
|
246
|
+
*/
|
|
247
|
+
getRelationStats(): RelationStats {
|
|
248
|
+
// Get total relations count
|
|
249
|
+
const totalResult = this.db.prepare(`
|
|
250
|
+
SELECT COUNT(*) as count FROM entity_relations
|
|
251
|
+
`).get() as { count: number };
|
|
252
|
+
const total_relations = totalResult.count;
|
|
253
|
+
|
|
254
|
+
// Get count by relation type
|
|
255
|
+
const typeRows = this.db.prepare(`
|
|
256
|
+
SELECT relation_type, COUNT(*) as count
|
|
257
|
+
FROM entity_relations
|
|
258
|
+
GROUP BY relation_type
|
|
259
|
+
`).all() as { relation_type: string; count: number }[];
|
|
260
|
+
|
|
261
|
+
const relation_types: Record<string, number> = {};
|
|
262
|
+
for (const row of typeRows) {
|
|
263
|
+
relation_types[row.relation_type] = row.count;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Get most connected entities
|
|
267
|
+
const connectedRows = this.db.prepare(`
|
|
268
|
+
SELECT e.id, e.name, COUNT(er.id) as connection_count
|
|
269
|
+
FROM entities e
|
|
270
|
+
LEFT JOIN (
|
|
271
|
+
SELECT source_id as entity_id, id FROM entity_relations
|
|
272
|
+
UNION ALL
|
|
273
|
+
SELECT target_id as entity_id, id FROM entity_relations
|
|
274
|
+
) er ON e.id = er.entity_id
|
|
275
|
+
GROUP BY e.id, e.name
|
|
276
|
+
ORDER BY connection_count DESC
|
|
277
|
+
LIMIT 10
|
|
278
|
+
`).all() as { id: string; name: string; connection_count: number }[];
|
|
279
|
+
|
|
280
|
+
const most_connected = connectedRows.map(row => ({
|
|
281
|
+
id: row.id,
|
|
282
|
+
name: row.name,
|
|
283
|
+
connection_count: row.connection_count
|
|
284
|
+
}));
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
most_connected,
|
|
288
|
+
relation_types,
|
|
289
|
+
total_relations
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Create a relation between two entities
|
|
295
|
+
*/
|
|
296
|
+
createRelation(sourceId: string, targetId: string, type: EntityEdge['type'], weight: number = 1.0): EntityRelation {
|
|
297
|
+
const { v4: uuidv4 } = require('uuid');
|
|
298
|
+
const id = uuidv4();
|
|
299
|
+
const now = new Date();
|
|
300
|
+
|
|
301
|
+
this.db.prepare(`
|
|
302
|
+
INSERT INTO entity_relations (id, source_id, target_id, relation_type, weight, evidence_count, created_at)
|
|
303
|
+
VALUES (?, ?, ?, ?, ?, 1, ?)
|
|
304
|
+
`).run(id, sourceId, targetId, type, weight, now.toISOString());
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
id,
|
|
308
|
+
sourceId,
|
|
309
|
+
targetId,
|
|
310
|
+
relationType: type,
|
|
311
|
+
weight,
|
|
312
|
+
evidenceCount: 1,
|
|
313
|
+
createdAt: now
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Get subgraph around a specific entity (depth: number of hops)
|
|
319
|
+
*/
|
|
320
|
+
getSubgraph(entityId: string, depth: number = 1): GraphData {
|
|
321
|
+
const visited = new Set<string>();
|
|
322
|
+
const nodes: EntityNode[] = [];
|
|
323
|
+
const edges: EntityEdge[] = [];
|
|
324
|
+
|
|
325
|
+
const collectNodes = (currentId: string, currentDepth: number) => {
|
|
326
|
+
if (currentDepth > depth || visited.has(currentId)) return;
|
|
327
|
+
visited.add(currentId);
|
|
328
|
+
|
|
329
|
+
// Get entity info
|
|
330
|
+
const entityRow = this.db.prepare(`
|
|
331
|
+
SELECT id, name, type FROM entities WHERE id = ?
|
|
332
|
+
`).get(currentId) as { id: string; name: string; type: string } | undefined;
|
|
333
|
+
|
|
334
|
+
if (entityRow) {
|
|
335
|
+
nodes.push({
|
|
336
|
+
id: entityRow.id,
|
|
337
|
+
name: entityRow.name,
|
|
338
|
+
type: entityRow.type as EntityNode['type']
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Get connected entities
|
|
343
|
+
const connectedRows = this.db.prepare(`
|
|
344
|
+
SELECT source_id, target_id, relation_type, weight
|
|
345
|
+
FROM entity_relations
|
|
346
|
+
WHERE source_id = ? OR target_id = ?
|
|
347
|
+
`).all(currentId, currentId) as { source_id: string; target_id: string; relation_type: string; weight: number }[];
|
|
348
|
+
|
|
349
|
+
for (const row of connectedRows) {
|
|
350
|
+
const connectedId = row.source_id === currentId ? row.target_id : row.source_id;
|
|
351
|
+
|
|
352
|
+
edges.push({
|
|
353
|
+
source: row.source_id,
|
|
354
|
+
target: row.target_id,
|
|
355
|
+
type: row.relation_type as EntityEdge['type'],
|
|
356
|
+
weight: row.weight
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
collectNodes(connectedId, currentDepth + 1);
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
collectNodes(entityId, 0);
|
|
364
|
+
|
|
365
|
+
return { nodes, edges };
|
|
366
|
+
}
|
|
367
|
+
}
|