@kernel.chat/kbot 3.41.0 → 3.43.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/README.md +5 -5
- package/dist/agent-teams.d.ts +1 -1
- package/dist/agent-teams.d.ts.map +1 -1
- package/dist/agent-teams.js +36 -3
- package/dist/agent-teams.js.map +1 -1
- package/dist/agents/specialists.d.ts.map +1 -1
- package/dist/agents/specialists.js +20 -0
- package/dist/agents/specialists.js.map +1 -1
- package/dist/auth.d.ts +5 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +1 -1
- package/dist/auth.js.map +1 -1
- package/dist/channels/kbot-channel.js +8 -31
- package/dist/channels/kbot-channel.js.map +1 -1
- package/dist/cli.js +44 -11
- package/dist/cli.js.map +1 -1
- package/dist/completions.d.ts.map +1 -1
- package/dist/completions.js +7 -0
- package/dist/completions.js.map +1 -1
- package/dist/digest.js +1 -1
- package/dist/digest.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +132 -92
- package/dist/doctor.js.map +1 -1
- package/dist/doctor.test.d.ts +2 -0
- package/dist/doctor.test.d.ts.map +1 -0
- package/dist/doctor.test.js +432 -0
- package/dist/doctor.test.js.map +1 -0
- package/dist/email-service.d.ts.map +1 -1
- package/dist/email-service.js +1 -2
- package/dist/email-service.js.map +1 -1
- package/dist/episodic-memory.d.ts.map +1 -1
- package/dist/episodic-memory.js +14 -0
- package/dist/episodic-memory.js.map +1 -1
- package/dist/learned-router.d.ts.map +1 -1
- package/dist/learned-router.js +29 -0
- package/dist/learned-router.js.map +1 -1
- package/dist/tools/email.d.ts.map +1 -1
- package/dist/tools/email.js +2 -3
- package/dist/tools/email.js.map +1 -1
- package/dist/tools/hypothesis-engine.d.ts +2 -0
- package/dist/tools/hypothesis-engine.d.ts.map +1 -0
- package/dist/tools/hypothesis-engine.js +2276 -0
- package/dist/tools/hypothesis-engine.js.map +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +11 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/lab-bio.d.ts +2 -0
- package/dist/tools/lab-bio.d.ts.map +1 -0
- package/dist/tools/lab-bio.js +1392 -0
- package/dist/tools/lab-bio.js.map +1 -0
- package/dist/tools/lab-chem.d.ts +2 -0
- package/dist/tools/lab-chem.d.ts.map +1 -0
- package/dist/tools/lab-chem.js +1257 -0
- package/dist/tools/lab-chem.js.map +1 -0
- package/dist/tools/lab-core.d.ts +2 -0
- package/dist/tools/lab-core.d.ts.map +1 -0
- package/dist/tools/lab-core.js +2452 -0
- package/dist/tools/lab-core.js.map +1 -0
- package/dist/tools/lab-data.d.ts +2 -0
- package/dist/tools/lab-data.d.ts.map +1 -0
- package/dist/tools/lab-data.js +2464 -0
- package/dist/tools/lab-data.js.map +1 -0
- package/dist/tools/lab-earth.d.ts +2 -0
- package/dist/tools/lab-earth.d.ts.map +1 -0
- package/dist/tools/lab-earth.js +1124 -0
- package/dist/tools/lab-earth.js.map +1 -0
- package/dist/tools/lab-math.d.ts +2 -0
- package/dist/tools/lab-math.d.ts.map +1 -0
- package/dist/tools/lab-math.js +3021 -0
- package/dist/tools/lab-math.js.map +1 -0
- package/dist/tools/lab-physics.d.ts +2 -0
- package/dist/tools/lab-physics.d.ts.map +1 -0
- package/dist/tools/lab-physics.js +2423 -0
- package/dist/tools/lab-physics.js.map +1 -0
- package/dist/tools/research-notebook.d.ts +2 -0
- package/dist/tools/research-notebook.d.ts.map +1 -0
- package/dist/tools/research-notebook.js +1165 -0
- package/dist/tools/research-notebook.js.map +1 -0
- package/dist/tools/research-pipeline.d.ts +2 -0
- package/dist/tools/research-pipeline.d.ts.map +1 -0
- package/dist/tools/research-pipeline.js +1094 -0
- package/dist/tools/research-pipeline.js.map +1 -0
- package/dist/tools/science-graph.d.ts +2 -0
- package/dist/tools/science-graph.d.ts.map +1 -0
- package/dist/tools/science-graph.js +995 -0
- package/dist/tools/science-graph.js.map +1 -0
- package/package.json +2 -3
|
@@ -0,0 +1,995 @@
|
|
|
1
|
+
// kbot Science Knowledge Graph — Connects entities across kbot's science tools
|
|
2
|
+
// Stores entities and relationships discovered during research sessions.
|
|
3
|
+
// Graph persists at ~/.kbot/science-graph.json. No external dependencies.
|
|
4
|
+
import { registerTool, getTool } from './index.js';
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
// ─── Domain Classification ──────────────────────────────────────────────────
|
|
9
|
+
const TYPE_TO_DOMAIN = {
|
|
10
|
+
gene: 'biology',
|
|
11
|
+
protein: 'biology',
|
|
12
|
+
disease: 'biology',
|
|
13
|
+
pathway: 'biology',
|
|
14
|
+
species: 'biology',
|
|
15
|
+
compound: 'chemistry',
|
|
16
|
+
element: 'chemistry',
|
|
17
|
+
material: 'chemistry',
|
|
18
|
+
paper: 'math', // papers span all domains; default to math as neutral
|
|
19
|
+
concept: 'physics', // concepts span all domains; default to physics as neutral
|
|
20
|
+
};
|
|
21
|
+
const DOMAIN_TYPES = {
|
|
22
|
+
biology: ['gene', 'protein', 'disease', 'pathway', 'species'],
|
|
23
|
+
chemistry: ['compound', 'element', 'material'],
|
|
24
|
+
physics: ['concept'],
|
|
25
|
+
earth: ['species'], // species can also be earth science
|
|
26
|
+
math: ['paper'],
|
|
27
|
+
};
|
|
28
|
+
// ─── Graph Storage ──────────────────────────────────────────────────────────
|
|
29
|
+
const GRAPH_PATH = join(homedir(), '.kbot', 'science-graph.json');
|
|
30
|
+
function ensureDir() {
|
|
31
|
+
const dir = join(homedir(), '.kbot');
|
|
32
|
+
if (!existsSync(dir)) {
|
|
33
|
+
mkdirSync(dir, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function loadGraph() {
|
|
37
|
+
try {
|
|
38
|
+
if (existsSync(GRAPH_PATH)) {
|
|
39
|
+
const raw = readFileSync(GRAPH_PATH, 'utf-8');
|
|
40
|
+
return JSON.parse(raw);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// corrupted file — start fresh
|
|
45
|
+
}
|
|
46
|
+
const now = new Date().toISOString();
|
|
47
|
+
return {
|
|
48
|
+
entities: {},
|
|
49
|
+
relations: [],
|
|
50
|
+
metadata: { created: now, lastModified: now, entityCount: 0, relationCount: 0 },
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function saveGraph(graph) {
|
|
54
|
+
ensureDir();
|
|
55
|
+
graph.metadata.lastModified = new Date().toISOString();
|
|
56
|
+
graph.metadata.entityCount = Object.keys(graph.entities).length;
|
|
57
|
+
graph.metadata.relationCount = graph.relations.length;
|
|
58
|
+
writeFileSync(GRAPH_PATH, JSON.stringify(graph, null, 2), 'utf-8');
|
|
59
|
+
}
|
|
60
|
+
// ─── ID Generation ──────────────────────────────────────────────────────────
|
|
61
|
+
function entityId(name, type) {
|
|
62
|
+
return `${type}:${name.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '')}`;
|
|
63
|
+
}
|
|
64
|
+
// ─── Fuzzy Match ────────────────────────────────────────────────────────────
|
|
65
|
+
function fuzzyMatch(needle, haystack) {
|
|
66
|
+
const n = needle.toLowerCase();
|
|
67
|
+
const h = haystack.toLowerCase();
|
|
68
|
+
if (h.includes(n) || n.includes(h))
|
|
69
|
+
return true;
|
|
70
|
+
// Levenshtein for short strings
|
|
71
|
+
if (n.length <= 20 && h.length <= 20) {
|
|
72
|
+
const dist = levenshtein(n, h);
|
|
73
|
+
return dist <= Math.max(1, Math.floor(Math.min(n.length, h.length) * 0.3));
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
function levenshtein(a, b) {
|
|
78
|
+
const m = a.length, n = b.length;
|
|
79
|
+
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
80
|
+
for (let i = 0; i <= m; i++)
|
|
81
|
+
dp[i][0] = i;
|
|
82
|
+
for (let j = 0; j <= n; j++)
|
|
83
|
+
dp[0][j] = j;
|
|
84
|
+
for (let i = 1; i <= m; i++) {
|
|
85
|
+
for (let j = 1; j <= n; j++) {
|
|
86
|
+
dp[i][j] = a[i - 1] === b[j - 1]
|
|
87
|
+
? dp[i - 1][j - 1]
|
|
88
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return dp[m][n];
|
|
92
|
+
}
|
|
93
|
+
// ─── Graph Algorithms ───────────────────────────────────────────────────────
|
|
94
|
+
/** Build adjacency list from relations */
|
|
95
|
+
function buildAdjacency(graph) {
|
|
96
|
+
const adj = new Map();
|
|
97
|
+
for (const rel of graph.relations) {
|
|
98
|
+
if (!adj.has(rel.from))
|
|
99
|
+
adj.set(rel.from, []);
|
|
100
|
+
if (!adj.has(rel.to))
|
|
101
|
+
adj.set(rel.to, []);
|
|
102
|
+
adj.get(rel.from).push({ neighbor: rel.to, relation: rel });
|
|
103
|
+
adj.get(rel.to).push({ neighbor: rel.from, relation: rel });
|
|
104
|
+
}
|
|
105
|
+
return adj;
|
|
106
|
+
}
|
|
107
|
+
/** BFS shortest path between two entity IDs */
|
|
108
|
+
function bfsPath(adj, startId, endId, maxDepth) {
|
|
109
|
+
if (startId === endId)
|
|
110
|
+
return [{ entity: startId, relation: null }];
|
|
111
|
+
const visited = new Set([startId]);
|
|
112
|
+
const parent = new Map();
|
|
113
|
+
const queue = [{ id: startId, depth: 0 }];
|
|
114
|
+
while (queue.length > 0) {
|
|
115
|
+
const { id, depth } = queue.shift();
|
|
116
|
+
if (depth >= maxDepth)
|
|
117
|
+
continue;
|
|
118
|
+
const neighbors = adj.get(id) || [];
|
|
119
|
+
for (const { neighbor, relation } of neighbors) {
|
|
120
|
+
if (visited.has(neighbor))
|
|
121
|
+
continue;
|
|
122
|
+
visited.add(neighbor);
|
|
123
|
+
parent.set(neighbor, { from: id, relation });
|
|
124
|
+
if (neighbor === endId) {
|
|
125
|
+
// reconstruct path
|
|
126
|
+
const path = [];
|
|
127
|
+
let cur = endId;
|
|
128
|
+
while (cur !== startId) {
|
|
129
|
+
const p = parent.get(cur);
|
|
130
|
+
path.unshift({ entity: cur, relation: p.relation });
|
|
131
|
+
cur = p.from;
|
|
132
|
+
}
|
|
133
|
+
path.unshift({ entity: startId, relation: null });
|
|
134
|
+
return path;
|
|
135
|
+
}
|
|
136
|
+
queue.push({ id: neighbor, depth: depth + 1 });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
/** BFS to find all entities within maxDepth of a start entity */
|
|
142
|
+
function bfsNeighbors(adj, startId, maxDepth) {
|
|
143
|
+
const result = new Map();
|
|
144
|
+
const visited = new Set([startId]);
|
|
145
|
+
const queue = [{ id: startId, depth: 0 }];
|
|
146
|
+
while (queue.length > 0) {
|
|
147
|
+
const { id, depth } = queue.shift();
|
|
148
|
+
if (depth >= maxDepth)
|
|
149
|
+
continue;
|
|
150
|
+
const neighbors = adj.get(id) || [];
|
|
151
|
+
for (const { neighbor, relation } of neighbors) {
|
|
152
|
+
if (visited.has(neighbor))
|
|
153
|
+
continue;
|
|
154
|
+
visited.add(neighbor);
|
|
155
|
+
result.set(neighbor, { depth: depth + 1, relations: [relation] });
|
|
156
|
+
queue.push({ id: neighbor, depth: depth + 1 });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
/** Compute degree centrality for all entities */
|
|
162
|
+
function degreeCentrality(graph) {
|
|
163
|
+
const degrees = new Map();
|
|
164
|
+
for (const id of Object.keys(graph.entities)) {
|
|
165
|
+
degrees.set(id, 0);
|
|
166
|
+
}
|
|
167
|
+
for (const rel of graph.relations) {
|
|
168
|
+
degrees.set(rel.from, (degrees.get(rel.from) || 0) + 1);
|
|
169
|
+
degrees.set(rel.to, (degrees.get(rel.to) || 0) + 1);
|
|
170
|
+
}
|
|
171
|
+
return degrees;
|
|
172
|
+
}
|
|
173
|
+
/** Find entity ID by fuzzy name match */
|
|
174
|
+
function findEntityByName(graph, name) {
|
|
175
|
+
// exact ID match first
|
|
176
|
+
const id = Object.keys(graph.entities).find(k => k === name);
|
|
177
|
+
if (id)
|
|
178
|
+
return graph.entities[id];
|
|
179
|
+
// exact name match
|
|
180
|
+
const exact = Object.values(graph.entities).find(e => e.name.toLowerCase() === name.toLowerCase());
|
|
181
|
+
if (exact)
|
|
182
|
+
return exact;
|
|
183
|
+
// fuzzy match
|
|
184
|
+
const fuzzy = Object.values(graph.entities).find(e => fuzzyMatch(name, e.name));
|
|
185
|
+
return fuzzy || null;
|
|
186
|
+
}
|
|
187
|
+
// ─── Entity Type Validation ─────────────────────────────────────────────────
|
|
188
|
+
const VALID_ENTITY_TYPES = ['gene', 'protein', 'compound', 'disease', 'pathway', 'species', 'element', 'material', 'paper', 'concept'];
|
|
189
|
+
const VALID_RELATION_TYPES = ['targets', 'inhibits', 'causes', 'treats', 'contains', 'catalyzes', 'encodes', 'interacts_with', 'associated_with', 'cites', 'similar_to'];
|
|
190
|
+
// ─── Registration ───────────────────────────────────────────────────────────
|
|
191
|
+
export function registerScienceGraphTools() {
|
|
192
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
193
|
+
// 1. graph_add_entity — Add a scientific entity to the knowledge graph
|
|
194
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
195
|
+
registerTool({
|
|
196
|
+
name: 'graph_add_entity',
|
|
197
|
+
description: 'Add a scientific entity to the Science Knowledge Graph. Entities represent genes, proteins, compounds, diseases, pathways, species, elements, materials, papers, or concepts discovered during research.',
|
|
198
|
+
parameters: {
|
|
199
|
+
name: { type: 'string', description: 'Entity name (e.g., "TP53", "Aspirin", "Alzheimer\'s Disease")', required: true },
|
|
200
|
+
type: { type: 'string', description: 'Entity type: gene, protein, compound, disease, pathway, species, element, material, paper, concept', required: true },
|
|
201
|
+
properties: { type: 'string', description: 'JSON string of additional properties (e.g., \'{"symbol":"TP53","chromosome":"17"}\')' },
|
|
202
|
+
source: { type: 'string', description: 'Which tool or source discovered this entity (e.g., "gene_lookup", "pubmed_search", "manual")', required: true },
|
|
203
|
+
},
|
|
204
|
+
tier: 'free',
|
|
205
|
+
async execute(args) {
|
|
206
|
+
const name = String(args.name).trim();
|
|
207
|
+
const type = String(args.type).trim().toLowerCase();
|
|
208
|
+
const source = String(args.source || 'manual').trim();
|
|
209
|
+
if (!name)
|
|
210
|
+
return '**Error**: Entity name is required.';
|
|
211
|
+
if (!VALID_ENTITY_TYPES.includes(type)) {
|
|
212
|
+
return `**Error**: Invalid entity type "${type}". Valid types: ${VALID_ENTITY_TYPES.join(', ')}`;
|
|
213
|
+
}
|
|
214
|
+
let properties = {};
|
|
215
|
+
if (args.properties) {
|
|
216
|
+
try {
|
|
217
|
+
properties = JSON.parse(String(args.properties));
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return '**Error**: Invalid JSON in properties parameter.';
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const graph = loadGraph();
|
|
224
|
+
const id = entityId(name, type);
|
|
225
|
+
const now = new Date().toISOString();
|
|
226
|
+
if (graph.entities[id]) {
|
|
227
|
+
// Update existing entity
|
|
228
|
+
const existing = graph.entities[id];
|
|
229
|
+
existing.lastSeen = now;
|
|
230
|
+
existing.references++;
|
|
231
|
+
existing.properties = { ...existing.properties, ...properties };
|
|
232
|
+
if (source && source !== existing.source) {
|
|
233
|
+
existing.source = `${existing.source}, ${source}`;
|
|
234
|
+
}
|
|
235
|
+
saveGraph(graph);
|
|
236
|
+
return `## Entity Updated\n\n| Field | Value |\n|---|---|\n| **ID** | \`${id}\` |\n| **Name** | ${existing.name} |\n| **Type** | ${existing.type} |\n| **References** | ${existing.references} |\n| **Sources** | ${existing.source} |\n| **Properties** | ${Object.keys(existing.properties).length} fields |\n\nEntity already existed — incremented reference count and merged properties.`;
|
|
237
|
+
}
|
|
238
|
+
const entity = {
|
|
239
|
+
id,
|
|
240
|
+
name,
|
|
241
|
+
type,
|
|
242
|
+
properties,
|
|
243
|
+
source,
|
|
244
|
+
created: now,
|
|
245
|
+
lastSeen: now,
|
|
246
|
+
references: 1,
|
|
247
|
+
};
|
|
248
|
+
graph.entities[id] = entity;
|
|
249
|
+
saveGraph(graph);
|
|
250
|
+
return `## Entity Added\n\n| Field | Value |\n|---|---|\n| **ID** | \`${id}\` |\n| **Name** | ${name} |\n| **Type** | ${type} |\n| **Source** | ${source} |\n| **Properties** | ${Object.keys(properties).length} fields |\n\nGraph now contains **${graph.metadata.entityCount}** entities and **${graph.metadata.relationCount}** relations.`;
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
254
|
+
// 2. graph_add_relation — Add a relationship between two entities
|
|
255
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
256
|
+
registerTool({
|
|
257
|
+
name: 'graph_add_relation',
|
|
258
|
+
description: 'Add a relationship between two entities in the Science Knowledge Graph. Both entities must already exist. Relation types: targets, inhibits, causes, treats, contains, catalyzes, encodes, interacts_with, associated_with, cites, similar_to.',
|
|
259
|
+
parameters: {
|
|
260
|
+
from_entity: { type: 'string', description: 'Name or ID of the source entity', required: true },
|
|
261
|
+
to_entity: { type: 'string', description: 'Name or ID of the target entity', required: true },
|
|
262
|
+
relation_type: { type: 'string', description: 'Relationship type: targets, inhibits, causes, treats, contains, catalyzes, encodes, interacts_with, associated_with, cites, similar_to', required: true },
|
|
263
|
+
confidence: { type: 'number', description: 'Confidence score 0-1 (default 0.8)' },
|
|
264
|
+
evidence: { type: 'string', description: 'Supporting evidence or reference for this relationship', required: true },
|
|
265
|
+
},
|
|
266
|
+
tier: 'free',
|
|
267
|
+
async execute(args) {
|
|
268
|
+
const fromName = String(args.from_entity).trim();
|
|
269
|
+
const toName = String(args.to_entity).trim();
|
|
270
|
+
const relType = String(args.relation_type).trim().toLowerCase();
|
|
271
|
+
const confidence = typeof args.confidence === 'number' ? Math.max(0, Math.min(1, args.confidence)) : 0.8;
|
|
272
|
+
const evidence = String(args.evidence || '').trim();
|
|
273
|
+
if (!fromName || !toName)
|
|
274
|
+
return '**Error**: Both from_entity and to_entity are required.';
|
|
275
|
+
if (!VALID_RELATION_TYPES.includes(relType)) {
|
|
276
|
+
return `**Error**: Invalid relation type "${relType}". Valid types: ${VALID_RELATION_TYPES.join(', ')}`;
|
|
277
|
+
}
|
|
278
|
+
const graph = loadGraph();
|
|
279
|
+
const fromEntity = findEntityByName(graph, fromName);
|
|
280
|
+
const toEntity = findEntityByName(graph, toName);
|
|
281
|
+
if (!fromEntity)
|
|
282
|
+
return `**Error**: Entity "${fromName}" not found in graph. Add it first with graph_add_entity.`;
|
|
283
|
+
if (!toEntity)
|
|
284
|
+
return `**Error**: Entity "${toName}" not found in graph. Add it first with graph_add_entity.`;
|
|
285
|
+
// Check for duplicate relation
|
|
286
|
+
const duplicate = graph.relations.find(r => r.from === fromEntity.id && r.to === toEntity.id && r.type === relType);
|
|
287
|
+
if (duplicate) {
|
|
288
|
+
// Update confidence if higher
|
|
289
|
+
if (confidence > duplicate.confidence) {
|
|
290
|
+
duplicate.confidence = confidence;
|
|
291
|
+
duplicate.evidence = evidence || duplicate.evidence;
|
|
292
|
+
saveGraph(graph);
|
|
293
|
+
return `## Relation Updated\n\n**${fromEntity.name}** --[${relType}]--> **${toEntity.name}**\n\nConfidence upgraded to **${confidence}**. Evidence: ${evidence || duplicate.evidence}`;
|
|
294
|
+
}
|
|
295
|
+
return `## Relation Already Exists\n\n**${fromEntity.name}** --[${relType}]--> **${toEntity.name}** (confidence: ${duplicate.confidence})\n\nExisting relation unchanged (new confidence ${confidence} <= existing ${duplicate.confidence}).`;
|
|
296
|
+
}
|
|
297
|
+
const relation = {
|
|
298
|
+
from: fromEntity.id,
|
|
299
|
+
to: toEntity.id,
|
|
300
|
+
type: relType,
|
|
301
|
+
confidence,
|
|
302
|
+
evidence,
|
|
303
|
+
source: 'manual',
|
|
304
|
+
created: new Date().toISOString(),
|
|
305
|
+
};
|
|
306
|
+
graph.relations.push(relation);
|
|
307
|
+
saveGraph(graph);
|
|
308
|
+
return `## Relation Added\n\n**${fromEntity.name}** --[${relType}]--> **${toEntity.name}**\n\n| Field | Value |\n|---|---|\n| **Confidence** | ${confidence} |\n| **Evidence** | ${evidence || 'None provided'} |\n\nGraph now contains **${graph.metadata.entityCount}** entities and **${graph.metadata.relationCount}** relations.`;
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
312
|
+
// 3. graph_query — Query the knowledge graph
|
|
313
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
314
|
+
registerTool({
|
|
315
|
+
name: 'graph_query',
|
|
316
|
+
description: 'Query the Science Knowledge Graph. Find entities by name (fuzzy), trace shortest paths between entities (BFS), find neighbors within depth, list all entities of a type, or dump all entities.',
|
|
317
|
+
parameters: {
|
|
318
|
+
query: { type: 'string', description: 'Search term: entity name for entity/neighbors, "A -> B" for path finding, type name for type queries', required: true },
|
|
319
|
+
query_type: { type: 'string', description: 'Query mode: entity (fuzzy name search), path (shortest path between two entities separated by "->"), neighbors (connected entities within depth), type (all entities of a given type), all (full listing)', required: true },
|
|
320
|
+
max_depth: { type: 'number', description: 'Maximum search depth for path/neighbors queries (default 2, max 5)' },
|
|
321
|
+
},
|
|
322
|
+
tier: 'free',
|
|
323
|
+
async execute(args) {
|
|
324
|
+
const query = String(args.query).trim();
|
|
325
|
+
const queryType = String(args.query_type).trim().toLowerCase();
|
|
326
|
+
const maxDepth = Math.min(typeof args.max_depth === 'number' ? args.max_depth : 2, 5);
|
|
327
|
+
const graph = loadGraph();
|
|
328
|
+
if (Object.keys(graph.entities).length === 0) {
|
|
329
|
+
return '## Empty Graph\n\nThe Science Knowledge Graph is empty. Use `graph_add_entity` to add entities.';
|
|
330
|
+
}
|
|
331
|
+
const adj = buildAdjacency(graph);
|
|
332
|
+
// ── entity: fuzzy search
|
|
333
|
+
if (queryType === 'entity') {
|
|
334
|
+
const matches = Object.values(graph.entities).filter(e => fuzzyMatch(query, e.name) || fuzzyMatch(query, e.id));
|
|
335
|
+
if (matches.length === 0)
|
|
336
|
+
return `## No Matches\n\nNo entities matching "${query}" found in the graph.`;
|
|
337
|
+
const lines = matches.map(e => {
|
|
338
|
+
const rels = graph.relations.filter(r => r.from === e.id || r.to === e.id);
|
|
339
|
+
return `### ${e.name} (\`${e.type}\`)\n- **ID**: \`${e.id}\`\n- **Source**: ${e.source}\n- **References**: ${e.references}\n- **Relations**: ${rels.length}\n- **Properties**: ${JSON.stringify(e.properties)}`;
|
|
340
|
+
});
|
|
341
|
+
return `## Entity Search: "${query}"\n\nFound **${matches.length}** matching entities:\n\n${lines.join('\n\n')}`;
|
|
342
|
+
}
|
|
343
|
+
// ── path: BFS shortest path
|
|
344
|
+
if (queryType === 'path') {
|
|
345
|
+
const parts = query.split('->').map(s => s.trim());
|
|
346
|
+
if (parts.length !== 2)
|
|
347
|
+
return '**Error**: Path query requires format "Entity A -> Entity B".';
|
|
348
|
+
const startEntity = findEntityByName(graph, parts[0]);
|
|
349
|
+
const endEntity = findEntityByName(graph, parts[1]);
|
|
350
|
+
if (!startEntity)
|
|
351
|
+
return `**Error**: Entity "${parts[0]}" not found.`;
|
|
352
|
+
if (!endEntity)
|
|
353
|
+
return `**Error**: Entity "${parts[1]}" not found.`;
|
|
354
|
+
const path = bfsPath(adj, startEntity.id, endEntity.id, maxDepth);
|
|
355
|
+
if (!path)
|
|
356
|
+
return `## No Path Found\n\nNo path between **${startEntity.name}** and **${endEntity.name}** within depth ${maxDepth}.`;
|
|
357
|
+
const steps = path.map((step, i) => {
|
|
358
|
+
const entity = graph.entities[step.entity];
|
|
359
|
+
const entityName = entity ? entity.name : step.entity;
|
|
360
|
+
if (i === 0)
|
|
361
|
+
return `1. **${entityName}** (\`${entity?.type}\`)`;
|
|
362
|
+
const rel = step.relation;
|
|
363
|
+
const relLabel = rel ? `--[${rel.type}, confidence: ${rel.confidence}]-->` : '-->';
|
|
364
|
+
return `${i + 1}. ${relLabel} **${entityName}** (\`${entity?.type}\`)`;
|
|
365
|
+
});
|
|
366
|
+
return `## Path: ${startEntity.name} → ${endEntity.name}\n\nShortest path (${path.length - 1} hops):\n\n${steps.join('\n')}`;
|
|
367
|
+
}
|
|
368
|
+
// ── neighbors: BFS within depth
|
|
369
|
+
if (queryType === 'neighbors') {
|
|
370
|
+
const entity = findEntityByName(graph, query);
|
|
371
|
+
if (!entity)
|
|
372
|
+
return `**Error**: Entity "${query}" not found.`;
|
|
373
|
+
const neighbors = bfsNeighbors(adj, entity.id, maxDepth);
|
|
374
|
+
if (neighbors.size === 0)
|
|
375
|
+
return `## No Neighbors\n\n**${entity.name}** has no connections within depth ${maxDepth}.`;
|
|
376
|
+
const byDepth = new Map();
|
|
377
|
+
for (const [nId, info] of neighbors) {
|
|
378
|
+
const n = graph.entities[nId];
|
|
379
|
+
if (!n)
|
|
380
|
+
continue;
|
|
381
|
+
if (!byDepth.has(info.depth))
|
|
382
|
+
byDepth.set(info.depth, []);
|
|
383
|
+
const relDesc = info.relations.map(r => r.type).join(', ');
|
|
384
|
+
byDepth.get(info.depth).push(`- **${n.name}** (\`${n.type}\`) via ${relDesc}`);
|
|
385
|
+
}
|
|
386
|
+
const sections = Array.from(byDepth.entries())
|
|
387
|
+
.sort((a, b) => a[0] - b[0])
|
|
388
|
+
.map(([depth, items]) => `### Depth ${depth}\n${items.join('\n')}`);
|
|
389
|
+
return `## Neighbors of ${entity.name}\n\nFound **${neighbors.size}** connected entities within depth ${maxDepth}:\n\n${sections.join('\n\n')}`;
|
|
390
|
+
}
|
|
391
|
+
// ── type: list all entities of a given type
|
|
392
|
+
if (queryType === 'type') {
|
|
393
|
+
const typeName = query.toLowerCase();
|
|
394
|
+
const matches = Object.values(graph.entities).filter(e => e.type === typeName);
|
|
395
|
+
if (matches.length === 0)
|
|
396
|
+
return `## No Entities of Type "${query}"\n\nNo entities of type \`${typeName}\` found.`;
|
|
397
|
+
const lines = matches
|
|
398
|
+
.sort((a, b) => b.references - a.references)
|
|
399
|
+
.map(e => `| ${e.name} | ${e.source} | ${e.references} | ${e.created.split('T')[0]} |`);
|
|
400
|
+
return `## Entities of Type: ${typeName}\n\nFound **${matches.length}** entities:\n\n| Name | Source | References | Created |\n|---|---|---|---|\n${lines.join('\n')}`;
|
|
401
|
+
}
|
|
402
|
+
// ── all: full listing
|
|
403
|
+
if (queryType === 'all') {
|
|
404
|
+
const entities = Object.values(graph.entities);
|
|
405
|
+
const byType = new Map();
|
|
406
|
+
for (const e of entities) {
|
|
407
|
+
if (!byType.has(e.type))
|
|
408
|
+
byType.set(e.type, []);
|
|
409
|
+
byType.get(e.type).push(e);
|
|
410
|
+
}
|
|
411
|
+
const sections = Array.from(byType.entries()).map(([type, ents]) => {
|
|
412
|
+
const rows = ents.slice(0, 20).map(e => `| ${e.name} | ${e.references} | ${e.source} |`);
|
|
413
|
+
return `### ${type} (${ents.length})\n| Name | Refs | Source |\n|---|---|---|\n${rows.join('\n')}`;
|
|
414
|
+
});
|
|
415
|
+
return `## Full Knowledge Graph\n\n**${graph.metadata.entityCount}** entities, **${graph.metadata.relationCount}** relations\n\n${sections.join('\n\n')}`;
|
|
416
|
+
}
|
|
417
|
+
return `**Error**: Unknown query_type "${queryType}". Use: entity, path, neighbors, type, all.`;
|
|
418
|
+
},
|
|
419
|
+
});
|
|
420
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
421
|
+
// 4. graph_connect — Discover connections between two entities
|
|
422
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
423
|
+
registerTool({
|
|
424
|
+
name: 'graph_connect',
|
|
425
|
+
description: 'Automatically discover connections between two entities by searching for direct relations, shared neighbors, indirect paths (up to depth 3), and common properties.',
|
|
426
|
+
parameters: {
|
|
427
|
+
entity_a: { type: 'string', description: 'First entity name or ID', required: true },
|
|
428
|
+
entity_b: { type: 'string', description: 'Second entity name or ID', required: true },
|
|
429
|
+
},
|
|
430
|
+
tier: 'free',
|
|
431
|
+
async execute(args) {
|
|
432
|
+
const nameA = String(args.entity_a).trim();
|
|
433
|
+
const nameB = String(args.entity_b).trim();
|
|
434
|
+
const graph = loadGraph();
|
|
435
|
+
const entityA = findEntityByName(graph, nameA);
|
|
436
|
+
const entityB = findEntityByName(graph, nameB);
|
|
437
|
+
if (!entityA)
|
|
438
|
+
return `**Error**: Entity "${nameA}" not found.`;
|
|
439
|
+
if (!entityB)
|
|
440
|
+
return `**Error**: Entity "${nameB}" not found.`;
|
|
441
|
+
const adj = buildAdjacency(graph);
|
|
442
|
+
const sections = [];
|
|
443
|
+
// ── Direct relations
|
|
444
|
+
const directRels = graph.relations.filter(r => (r.from === entityA.id && r.to === entityB.id) ||
|
|
445
|
+
(r.from === entityB.id && r.to === entityA.id));
|
|
446
|
+
if (directRels.length > 0) {
|
|
447
|
+
const lines = directRels.map(r => {
|
|
448
|
+
const fromName = graph.entities[r.from]?.name || r.from;
|
|
449
|
+
const toName = graph.entities[r.to]?.name || r.to;
|
|
450
|
+
return `- **${fromName}** --[${r.type}]--> **${toName}** (confidence: ${r.confidence})`;
|
|
451
|
+
});
|
|
452
|
+
sections.push(`### Direct Relations\n${lines.join('\n')}`);
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
sections.push('### Direct Relations\nNone found.');
|
|
456
|
+
}
|
|
457
|
+
// ── Shared neighbors
|
|
458
|
+
const neighborsA = bfsNeighbors(adj, entityA.id, 1);
|
|
459
|
+
const neighborsB = bfsNeighbors(adj, entityB.id, 1);
|
|
460
|
+
const sharedNeighborIds = [...neighborsA.keys()].filter(id => neighborsB.has(id));
|
|
461
|
+
if (sharedNeighborIds.length > 0) {
|
|
462
|
+
const lines = sharedNeighborIds.map(id => {
|
|
463
|
+
const n = graph.entities[id];
|
|
464
|
+
return n ? `- **${n.name}** (\`${n.type}\`)` : `- \`${id}\``;
|
|
465
|
+
});
|
|
466
|
+
sections.push(`### Shared Neighbors (${sharedNeighborIds.length})\n${lines.join('\n')}`);
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
sections.push('### Shared Neighbors\nNone found.');
|
|
470
|
+
}
|
|
471
|
+
// ── Shortest path (up to depth 3)
|
|
472
|
+
if (directRels.length === 0) {
|
|
473
|
+
const path = bfsPath(adj, entityA.id, entityB.id, 3);
|
|
474
|
+
if (path && path.length > 2) {
|
|
475
|
+
const steps = path.map((step, i) => {
|
|
476
|
+
const entity = graph.entities[step.entity];
|
|
477
|
+
const name = entity ? entity.name : step.entity;
|
|
478
|
+
if (i === 0)
|
|
479
|
+
return `**${name}**`;
|
|
480
|
+
return `--[${step.relation?.type}]--> **${name}**`;
|
|
481
|
+
});
|
|
482
|
+
sections.push(`### Indirect Path (${path.length - 1} hops)\n${steps.join(' ')}`);
|
|
483
|
+
}
|
|
484
|
+
else if (!path) {
|
|
485
|
+
sections.push('### Indirect Path\nNo path found within 3 hops.');
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
// ── Common properties
|
|
489
|
+
const propsA = Object.keys(entityA.properties);
|
|
490
|
+
const propsB = Object.keys(entityB.properties);
|
|
491
|
+
const commonKeys = propsA.filter(k => propsB.includes(k));
|
|
492
|
+
if (commonKeys.length > 0) {
|
|
493
|
+
const lines = commonKeys.map(k => `- **${k}**: ${JSON.stringify(entityA.properties[k])} (A) / ${JSON.stringify(entityB.properties[k])} (B)`);
|
|
494
|
+
sections.push(`### Common Properties (${commonKeys.length})\n${lines.join('\n')}`);
|
|
495
|
+
}
|
|
496
|
+
// ── Same type?
|
|
497
|
+
if (entityA.type === entityB.type) {
|
|
498
|
+
sections.push(`### Same Type\nBoth entities are of type \`${entityA.type}\`, suggesting they may be comparable.`);
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
sections.push(`### Cross-Type\n**${entityA.name}** is \`${entityA.type}\` (${TYPE_TO_DOMAIN[entityA.type]}), **${entityB.name}** is \`${entityB.type}\` (${TYPE_TO_DOMAIN[entityB.type]}).`);
|
|
502
|
+
}
|
|
503
|
+
return `## Connections: ${entityA.name} ↔ ${entityB.name}\n\n${sections.join('\n\n')}`;
|
|
504
|
+
},
|
|
505
|
+
});
|
|
506
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
507
|
+
// 5. graph_enrich — Auto-enrich an entity using science tools
|
|
508
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
509
|
+
registerTool({
|
|
510
|
+
name: 'graph_enrich',
|
|
511
|
+
description: 'Auto-enrich a knowledge graph entity by calling relevant science tools based on its type. For example, a gene entity triggers gene_lookup + pathway_search; a compound triggers compound_search + compound_properties. Discovered relations are stored in the graph automatically.',
|
|
512
|
+
parameters: {
|
|
513
|
+
entity_name: { type: 'string', description: 'Name of the entity to enrich (must already exist in the graph)', required: true },
|
|
514
|
+
},
|
|
515
|
+
tier: 'free',
|
|
516
|
+
async execute(args) {
|
|
517
|
+
const name = String(args.entity_name).trim();
|
|
518
|
+
const graph = loadGraph();
|
|
519
|
+
const entity = findEntityByName(graph, name);
|
|
520
|
+
if (!entity)
|
|
521
|
+
return `**Error**: Entity "${name}" not found. Add it first with graph_add_entity.`;
|
|
522
|
+
const toolCalls = [];
|
|
523
|
+
// Determine which tools to call based on entity type
|
|
524
|
+
switch (entity.type) {
|
|
525
|
+
case 'gene':
|
|
526
|
+
toolCalls.push({ toolName: 'gene_lookup', args: { gene: entity.name }, purpose: 'gene information' }, { toolName: 'pathway_search', args: { query: entity.name }, purpose: 'associated pathways' });
|
|
527
|
+
break;
|
|
528
|
+
case 'protein':
|
|
529
|
+
toolCalls.push({ toolName: 'protein_search', args: { query: entity.name }, purpose: 'protein information' }, { toolName: 'protein_structure', args: { query: entity.name }, purpose: 'structural data' });
|
|
530
|
+
break;
|
|
531
|
+
case 'compound':
|
|
532
|
+
toolCalls.push({ toolName: 'compound_search', args: { query: entity.name }, purpose: 'compound identification' }, { toolName: 'compound_properties', args: { query: entity.name }, purpose: 'chemical properties' });
|
|
533
|
+
break;
|
|
534
|
+
case 'disease':
|
|
535
|
+
toolCalls.push({ toolName: 'disease_info', args: { disease: entity.name }, purpose: 'disease information' }, { toolName: 'clinical_trials', args: { query: entity.name }, purpose: 'clinical trial data' });
|
|
536
|
+
break;
|
|
537
|
+
case 'element':
|
|
538
|
+
toolCalls.push({ toolName: 'element_info', args: { element: entity.name }, purpose: 'element data' });
|
|
539
|
+
break;
|
|
540
|
+
case 'species':
|
|
541
|
+
toolCalls.push({ toolName: 'taxonomy_lookup', args: { query: entity.name }, purpose: 'taxonomic classification' }, { toolName: 'ecology_data', args: { species: entity.name }, purpose: 'ecological information' });
|
|
542
|
+
break;
|
|
543
|
+
case 'pathway':
|
|
544
|
+
toolCalls.push({ toolName: 'pathway_search', args: { query: entity.name }, purpose: 'pathway details' });
|
|
545
|
+
break;
|
|
546
|
+
default:
|
|
547
|
+
return `## Enrichment Not Available\n\nNo automatic enrichment tools mapped for entity type \`${entity.type}\`. Enrich manually by adding relations with graph_add_relation.`;
|
|
548
|
+
}
|
|
549
|
+
const results = [];
|
|
550
|
+
let relationsAdded = 0;
|
|
551
|
+
for (const call of toolCalls) {
|
|
552
|
+
const tool = getTool(call.toolName);
|
|
553
|
+
if (!tool) {
|
|
554
|
+
results.push(`- **${call.toolName}**: Tool not loaded (science tools may need lazy registration)`);
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
try {
|
|
558
|
+
const result = await tool.execute(call.args);
|
|
559
|
+
const preview = result.length > 300 ? result.slice(0, 300) + '...' : result;
|
|
560
|
+
results.push(`- **${call.toolName}** (${call.purpose}): Retrieved data (${result.length} chars)`);
|
|
561
|
+
// Extract entity names mentioned in the result and auto-link
|
|
562
|
+
// Look for existing entities referenced in the tool output
|
|
563
|
+
for (const existingEntity of Object.values(graph.entities)) {
|
|
564
|
+
if (existingEntity.id === entity.id)
|
|
565
|
+
continue;
|
|
566
|
+
if (result.toLowerCase().includes(existingEntity.name.toLowerCase()) && existingEntity.name.length > 2) {
|
|
567
|
+
// Check if relation already exists
|
|
568
|
+
const existing = graph.relations.find(r => (r.from === entity.id && r.to === existingEntity.id) ||
|
|
569
|
+
(r.from === existingEntity.id && r.to === entity.id));
|
|
570
|
+
if (!existing) {
|
|
571
|
+
graph.relations.push({
|
|
572
|
+
from: entity.id,
|
|
573
|
+
to: existingEntity.id,
|
|
574
|
+
type: 'associated_with',
|
|
575
|
+
confidence: 0.6,
|
|
576
|
+
evidence: `Co-occurrence in ${call.toolName} result`,
|
|
577
|
+
source: call.toolName,
|
|
578
|
+
created: new Date().toISOString(),
|
|
579
|
+
});
|
|
580
|
+
relationsAdded++;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
// Update entity properties with enrichment source
|
|
585
|
+
entity.properties[`enriched_by_${call.toolName}`] = true;
|
|
586
|
+
entity.lastSeen = new Date().toISOString();
|
|
587
|
+
}
|
|
588
|
+
catch (err) {
|
|
589
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
590
|
+
results.push(`- **${call.toolName}** (${call.purpose}): Failed — ${msg}`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
saveGraph(graph);
|
|
594
|
+
return `## Enrichment: ${entity.name} (\`${entity.type}\`)\n\n### Tool Results\n${results.join('\n')}\n\n### Graph Updates\n- **${relationsAdded}** new relations discovered and added\n- Entity properties updated with enrichment markers\n\nUse \`graph_query\` to explore the updated connections.`;
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
598
|
+
// 6. graph_visualize — Generate visual representations of the graph
|
|
599
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
600
|
+
registerTool({
|
|
601
|
+
name: 'graph_visualize',
|
|
602
|
+
description: 'Generate a text-based, Mermaid diagram, or statistics view of the Science Knowledge Graph. Optionally focus on a subgraph around a specific entity.',
|
|
603
|
+
parameters: {
|
|
604
|
+
center_entity: { type: 'string', description: 'Optional: show subgraph centered on this entity (name or ID). Omit for full graph.' },
|
|
605
|
+
format: { type: 'string', description: 'Output format: mermaid (Mermaid diagram syntax), text (ASCII representation), stats (counts, centrality, orphans)', required: true },
|
|
606
|
+
},
|
|
607
|
+
tier: 'free',
|
|
608
|
+
async execute(args) {
|
|
609
|
+
const centerName = args.center_entity ? String(args.center_entity).trim() : '';
|
|
610
|
+
const format = String(args.format).trim().toLowerCase();
|
|
611
|
+
const graph = loadGraph();
|
|
612
|
+
if (Object.keys(graph.entities).length === 0) {
|
|
613
|
+
return '## Empty Graph\n\nThe Science Knowledge Graph is empty. Add entities with `graph_add_entity`.';
|
|
614
|
+
}
|
|
615
|
+
// Determine which entities and relations to include
|
|
616
|
+
let entities;
|
|
617
|
+
let relations;
|
|
618
|
+
if (centerName) {
|
|
619
|
+
const center = findEntityByName(graph, centerName);
|
|
620
|
+
if (!center)
|
|
621
|
+
return `**Error**: Entity "${centerName}" not found.`;
|
|
622
|
+
const adj = buildAdjacency(graph);
|
|
623
|
+
const neighbors = bfsNeighbors(adj, center.id, 2);
|
|
624
|
+
const relevantIds = new Set([center.id, ...neighbors.keys()]);
|
|
625
|
+
entities = Object.values(graph.entities).filter(e => relevantIds.has(e.id));
|
|
626
|
+
relations = graph.relations.filter(r => relevantIds.has(r.from) && relevantIds.has(r.to));
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
entities = Object.values(graph.entities);
|
|
630
|
+
relations = graph.relations;
|
|
631
|
+
}
|
|
632
|
+
// ── Mermaid format
|
|
633
|
+
if (format === 'mermaid') {
|
|
634
|
+
const nodeIds = new Map();
|
|
635
|
+
let nodeCounter = 0;
|
|
636
|
+
const getNodeId = (id) => {
|
|
637
|
+
if (!nodeIds.has(id))
|
|
638
|
+
nodeIds.set(id, `N${nodeCounter++}`);
|
|
639
|
+
return nodeIds.get(id);
|
|
640
|
+
};
|
|
641
|
+
const lines = ['graph LR'];
|
|
642
|
+
// Subgraphs by type
|
|
643
|
+
const byType = new Map();
|
|
644
|
+
for (const e of entities) {
|
|
645
|
+
if (!byType.has(e.type))
|
|
646
|
+
byType.set(e.type, []);
|
|
647
|
+
byType.get(e.type).push(e);
|
|
648
|
+
}
|
|
649
|
+
for (const [type, ents] of byType) {
|
|
650
|
+
lines.push(` subgraph ${type}`);
|
|
651
|
+
for (const e of ents) {
|
|
652
|
+
const nid = getNodeId(e.id);
|
|
653
|
+
const label = e.name.replace(/"/g, "'");
|
|
654
|
+
lines.push(` ${nid}["${label}"]`);
|
|
655
|
+
}
|
|
656
|
+
lines.push(' end');
|
|
657
|
+
}
|
|
658
|
+
// Edges
|
|
659
|
+
for (const r of relations) {
|
|
660
|
+
const fromNode = getNodeId(r.from);
|
|
661
|
+
const toNode = getNodeId(r.to);
|
|
662
|
+
const label = r.type.replace(/_/g, ' ');
|
|
663
|
+
lines.push(` ${fromNode} -->|${label}| ${toNode}`);
|
|
664
|
+
}
|
|
665
|
+
const title = centerName ? `Subgraph around ${centerName}` : 'Full Science Knowledge Graph';
|
|
666
|
+
return `## ${title}\n\n\`\`\`mermaid\n${lines.join('\n')}\n\`\`\`\n\n*${entities.length} entities, ${relations.length} relations*`;
|
|
667
|
+
}
|
|
668
|
+
// ── Text format (ASCII)
|
|
669
|
+
if (format === 'text') {
|
|
670
|
+
const lines = [];
|
|
671
|
+
const title = centerName ? `Subgraph: ${centerName}` : 'Science Knowledge Graph';
|
|
672
|
+
lines.push(`╔${'═'.repeat(title.length + 2)}╗`);
|
|
673
|
+
lines.push(`║ ${title} ║`);
|
|
674
|
+
lines.push(`╚${'═'.repeat(title.length + 2)}╝`);
|
|
675
|
+
lines.push('');
|
|
676
|
+
// List entities grouped by type
|
|
677
|
+
const byType = new Map();
|
|
678
|
+
for (const e of entities) {
|
|
679
|
+
if (!byType.has(e.type))
|
|
680
|
+
byType.set(e.type, []);
|
|
681
|
+
byType.get(e.type).push(e);
|
|
682
|
+
}
|
|
683
|
+
for (const [type, ents] of byType) {
|
|
684
|
+
lines.push(`┌─ ${type.toUpperCase()} (${ents.length}) ${'─'.repeat(Math.max(0, 40 - type.length - String(ents.length).length))}┐`);
|
|
685
|
+
for (const e of ents) {
|
|
686
|
+
const relCount = relations.filter(r => r.from === e.id || r.to === e.id).length;
|
|
687
|
+
lines.push(`│ ● ${e.name} (${relCount} relations)`);
|
|
688
|
+
}
|
|
689
|
+
lines.push(`└${'─'.repeat(50)}┘`);
|
|
690
|
+
lines.push('');
|
|
691
|
+
}
|
|
692
|
+
// List relations
|
|
693
|
+
if (relations.length > 0) {
|
|
694
|
+
lines.push(`┌─ RELATIONS (${relations.length}) ${'─'.repeat(30)}┐`);
|
|
695
|
+
for (const r of relations.slice(0, 50)) {
|
|
696
|
+
const fromName = graph.entities[r.from]?.name || r.from;
|
|
697
|
+
const toName = graph.entities[r.to]?.name || r.to;
|
|
698
|
+
const conf = (r.confidence * 100).toFixed(0);
|
|
699
|
+
lines.push(`│ ${fromName} ──[${r.type}]──▶ ${toName} (${conf}%)`);
|
|
700
|
+
}
|
|
701
|
+
if (relations.length > 50) {
|
|
702
|
+
lines.push(`│ ... and ${relations.length - 50} more`);
|
|
703
|
+
}
|
|
704
|
+
lines.push(`└${'─'.repeat(50)}┘`);
|
|
705
|
+
}
|
|
706
|
+
return lines.join('\n');
|
|
707
|
+
}
|
|
708
|
+
// ── Stats format
|
|
709
|
+
if (format === 'stats') {
|
|
710
|
+
const degrees = degreeCentrality(graph);
|
|
711
|
+
// Type counts
|
|
712
|
+
const typeCounts = new Map();
|
|
713
|
+
for (const e of entities) {
|
|
714
|
+
typeCounts.set(e.type, (typeCounts.get(e.type) || 0) + 1);
|
|
715
|
+
}
|
|
716
|
+
// Relation type counts
|
|
717
|
+
const relTypeCounts = new Map();
|
|
718
|
+
for (const r of relations) {
|
|
719
|
+
relTypeCounts.set(r.type, (relTypeCounts.get(r.type) || 0) + 1);
|
|
720
|
+
}
|
|
721
|
+
// Most connected
|
|
722
|
+
const sortedDegrees = [...degrees.entries()]
|
|
723
|
+
.filter(([id]) => entities.find(e => e.id === id))
|
|
724
|
+
.sort((a, b) => b[1] - a[1])
|
|
725
|
+
.slice(0, 10);
|
|
726
|
+
// Orphans (no relations)
|
|
727
|
+
const connectedIds = new Set();
|
|
728
|
+
for (const r of relations) {
|
|
729
|
+
connectedIds.add(r.from);
|
|
730
|
+
connectedIds.add(r.to);
|
|
731
|
+
}
|
|
732
|
+
const orphans = entities.filter(e => !connectedIds.has(e.id));
|
|
733
|
+
// Avg confidence
|
|
734
|
+
const avgConfidence = relations.length > 0
|
|
735
|
+
? (relations.reduce((sum, r) => sum + r.confidence, 0) / relations.length).toFixed(2)
|
|
736
|
+
: 'N/A';
|
|
737
|
+
const typeRows = [...typeCounts.entries()]
|
|
738
|
+
.sort((a, b) => b[1] - a[1])
|
|
739
|
+
.map(([type, count]) => `| ${type} | ${count} |`);
|
|
740
|
+
const relTypeRows = [...relTypeCounts.entries()]
|
|
741
|
+
.sort((a, b) => b[1] - a[1])
|
|
742
|
+
.map(([type, count]) => `| ${type} | ${count} |`);
|
|
743
|
+
const centralityRows = sortedDegrees.map(([id, deg]) => {
|
|
744
|
+
const e = graph.entities[id];
|
|
745
|
+
return `| ${e?.name || id} | ${e?.type || '?'} | ${deg} |`;
|
|
746
|
+
});
|
|
747
|
+
const orphanList = orphans.length > 0
|
|
748
|
+
? orphans.slice(0, 15).map(e => `- ${e.name} (\`${e.type}\`)`).join('\n')
|
|
749
|
+
: 'None — all entities are connected.';
|
|
750
|
+
return `## Graph Statistics\n\n| Metric | Value |\n|---|---|\n| **Entities** | ${entities.length} |\n| **Relations** | ${relations.length} |\n| **Avg Confidence** | ${avgConfidence} |\n| **Orphan Entities** | ${orphans.length} |\n| **Created** | ${graph.metadata.created.split('T')[0]} |\n| **Last Modified** | ${graph.metadata.lastModified.split('T')[0]} |\n\n### Entities by Type\n| Type | Count |\n|---|---|\n${typeRows.join('\n')}\n\n### Relations by Type\n| Type | Count |\n|---|---|\n${relTypeRows.join('\n')}\n\n### Most Connected Entities\n| Entity | Type | Degree |\n|---|---|---|\n${centralityRows.join('\n')}\n\n### Orphan Entities (unconnected)\n${orphanList}`;
|
|
751
|
+
}
|
|
752
|
+
return `**Error**: Unknown format "${format}". Use: mermaid, text, stats.`;
|
|
753
|
+
},
|
|
754
|
+
});
|
|
755
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
756
|
+
// 7. graph_cross_domain — Find cross-domain connections
|
|
757
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
758
|
+
registerTool({
|
|
759
|
+
name: 'graph_cross_domain',
|
|
760
|
+
description: 'Find cross-domain connections in the Science Knowledge Graph: entities from different scientific fields (biology, chemistry, physics, earth, math) that share properties or relationships. Useful for discovering interdisciplinary insights.',
|
|
761
|
+
parameters: {
|
|
762
|
+
domain_a: { type: 'string', description: 'Primary domain: biology, chemistry, physics, earth, math', required: true },
|
|
763
|
+
domain_b: { type: 'string', description: 'Secondary domain to compare (omit to check all other domains)' },
|
|
764
|
+
},
|
|
765
|
+
tier: 'free',
|
|
766
|
+
async execute(args) {
|
|
767
|
+
const domainA = String(args.domain_a).trim().toLowerCase();
|
|
768
|
+
const domainB = args.domain_b ? String(args.domain_b).trim().toLowerCase() : null;
|
|
769
|
+
const validDomains = ['biology', 'chemistry', 'physics', 'earth', 'math'];
|
|
770
|
+
if (!validDomains.includes(domainA)) {
|
|
771
|
+
return `**Error**: Invalid domain "${domainA}". Valid: ${validDomains.join(', ')}`;
|
|
772
|
+
}
|
|
773
|
+
if (domainB && !validDomains.includes(domainB)) {
|
|
774
|
+
return `**Error**: Invalid domain "${domainB}". Valid: ${validDomains.join(', ')}`;
|
|
775
|
+
}
|
|
776
|
+
const graph = loadGraph();
|
|
777
|
+
// Classify entities by domain
|
|
778
|
+
const domainEntities = new Map();
|
|
779
|
+
for (const domain of validDomains) {
|
|
780
|
+
domainEntities.set(domain, []);
|
|
781
|
+
}
|
|
782
|
+
for (const entity of Object.values(graph.entities)) {
|
|
783
|
+
const domain = TYPE_TO_DOMAIN[entity.type];
|
|
784
|
+
if (domain)
|
|
785
|
+
domainEntities.get(domain).push(entity);
|
|
786
|
+
}
|
|
787
|
+
const entitiesA = domainEntities.get(domainA) || [];
|
|
788
|
+
if (entitiesA.length === 0) {
|
|
789
|
+
return `## No Entities in ${domainA}\n\nNo entities classified under the **${domainA}** domain.`;
|
|
790
|
+
}
|
|
791
|
+
const targetDomains = domainB ? [domainB] : validDomains.filter(d => d !== domainA);
|
|
792
|
+
const sections = [];
|
|
793
|
+
for (const targetDomain of targetDomains) {
|
|
794
|
+
const entitiesB = domainEntities.get(targetDomain) || [];
|
|
795
|
+
if (entitiesB.length === 0)
|
|
796
|
+
continue;
|
|
797
|
+
const idsA = new Set(entitiesA.map(e => e.id));
|
|
798
|
+
const idsB = new Set(entitiesB.map(e => e.id));
|
|
799
|
+
// Find cross-domain relations
|
|
800
|
+
const crossRelations = graph.relations.filter(r => (idsA.has(r.from) && idsB.has(r.to)) ||
|
|
801
|
+
(idsB.has(r.from) && idsA.has(r.to)));
|
|
802
|
+
// Find bridge entities (connected to both domains)
|
|
803
|
+
const allIds = new Set([...Object.keys(graph.entities)]);
|
|
804
|
+
const bridges = [];
|
|
805
|
+
for (const entity of Object.values(graph.entities)) {
|
|
806
|
+
if (idsA.has(entity.id) || idsB.has(entity.id))
|
|
807
|
+
continue;
|
|
808
|
+
const connsA = graph.relations.filter(r => (r.from === entity.id && idsA.has(r.to)) || (r.to === entity.id && idsA.has(r.from))).length;
|
|
809
|
+
const connsB = graph.relations.filter(r => (r.from === entity.id && idsB.has(r.to)) || (r.to === entity.id && idsB.has(r.from))).length;
|
|
810
|
+
if (connsA > 0 && connsB > 0) {
|
|
811
|
+
bridges.push({ entity, connectionsA: connsA, connectionsB: connsB });
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
// Find shared properties across domains
|
|
815
|
+
const sharedProps = [];
|
|
816
|
+
for (const eA of entitiesA) {
|
|
817
|
+
for (const eB of entitiesB) {
|
|
818
|
+
for (const key of Object.keys(eA.properties)) {
|
|
819
|
+
if (key in eB.properties) {
|
|
820
|
+
sharedProps.push({ propKey: key, entityA: eA, entityB: eB, valueA: eA.properties[key], valueB: eB.properties[key] });
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
const parts = [];
|
|
826
|
+
parts.push(`### ${domainA} ↔ ${targetDomain}`);
|
|
827
|
+
if (crossRelations.length > 0) {
|
|
828
|
+
const relLines = crossRelations.slice(0, 15).map(r => {
|
|
829
|
+
const from = graph.entities[r.from]?.name || r.from;
|
|
830
|
+
const to = graph.entities[r.to]?.name || r.to;
|
|
831
|
+
return `- **${from}** --[${r.type}]--> **${to}** (${(r.confidence * 100).toFixed(0)}%)`;
|
|
832
|
+
});
|
|
833
|
+
parts.push(`**Direct Cross-Domain Relations (${crossRelations.length})**\n${relLines.join('\n')}`);
|
|
834
|
+
}
|
|
835
|
+
if (bridges.length > 0) {
|
|
836
|
+
const bridgeLines = bridges
|
|
837
|
+
.sort((a, b) => (b.connectionsA + b.connectionsB) - (a.connectionsA + a.connectionsB))
|
|
838
|
+
.slice(0, 10)
|
|
839
|
+
.map(b => `- **${b.entity.name}** (\`${b.entity.type}\`): ${b.connectionsA} ${domainA} links, ${b.connectionsB} ${targetDomain} links`);
|
|
840
|
+
parts.push(`**Bridge Entities (${bridges.length})**\n${bridgeLines.join('\n')}`);
|
|
841
|
+
}
|
|
842
|
+
if (sharedProps.length > 0) {
|
|
843
|
+
const propLines = sharedProps.slice(0, 10).map(sp => `- Property \`${sp.propKey}\`: **${sp.entityA.name}** = ${JSON.stringify(sp.valueA)}, **${sp.entityB.name}** = ${JSON.stringify(sp.valueB)}`);
|
|
844
|
+
parts.push(`**Shared Properties (${sharedProps.length})**\n${propLines.join('\n')}`);
|
|
845
|
+
}
|
|
846
|
+
if (crossRelations.length === 0 && bridges.length === 0 && sharedProps.length === 0) {
|
|
847
|
+
parts.push('*No cross-domain connections found between these domains.*');
|
|
848
|
+
}
|
|
849
|
+
sections.push(parts.join('\n\n'));
|
|
850
|
+
}
|
|
851
|
+
const domainSummary = validDomains.map(d => `${d}: ${(domainEntities.get(d) || []).length}`).join(', ');
|
|
852
|
+
return `## Cross-Domain Analysis: ${domainA}${domainB ? ` ↔ ${domainB}` : ' ↔ all'}\n\n**Domain sizes**: ${domainSummary}\n\n${sections.join('\n\n---\n\n')}`;
|
|
853
|
+
},
|
|
854
|
+
});
|
|
855
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
856
|
+
// 8. graph_export — Export the knowledge graph in various formats
|
|
857
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
858
|
+
registerTool({
|
|
859
|
+
name: 'graph_export',
|
|
860
|
+
description: 'Export the Science Knowledge Graph in JSON, CSV, GraphML (Cytoscape/Gephi compatible), or Markdown format. Optionally filter by entity type.',
|
|
861
|
+
parameters: {
|
|
862
|
+
format: { type: 'string', description: 'Export format: json (full dump), csv (entity/edge lists), graphml (XML for Cytoscape/Gephi), markdown (human-readable summary)', required: true },
|
|
863
|
+
filter_type: { type: 'string', description: 'Optional: filter to only include entities of this type (e.g., "gene", "compound")' },
|
|
864
|
+
},
|
|
865
|
+
tier: 'free',
|
|
866
|
+
async execute(args) {
|
|
867
|
+
const format = String(args.format).trim().toLowerCase();
|
|
868
|
+
const filterType = args.filter_type ? String(args.filter_type).trim().toLowerCase() : null;
|
|
869
|
+
const graph = loadGraph();
|
|
870
|
+
if (Object.keys(graph.entities).length === 0) {
|
|
871
|
+
return '## Empty Graph\n\nNothing to export. Add entities with `graph_add_entity`.';
|
|
872
|
+
}
|
|
873
|
+
// Filter entities if needed
|
|
874
|
+
let entities = Object.values(graph.entities);
|
|
875
|
+
if (filterType) {
|
|
876
|
+
entities = entities.filter(e => e.type === filterType);
|
|
877
|
+
}
|
|
878
|
+
const entityIds = new Set(entities.map(e => e.id));
|
|
879
|
+
const relations = filterType
|
|
880
|
+
? graph.relations.filter(r => entityIds.has(r.from) && entityIds.has(r.to))
|
|
881
|
+
: graph.relations;
|
|
882
|
+
// ── JSON
|
|
883
|
+
if (format === 'json') {
|
|
884
|
+
const exportData = {
|
|
885
|
+
entities: Object.fromEntries(entities.map(e => [e.id, e])),
|
|
886
|
+
relations,
|
|
887
|
+
metadata: {
|
|
888
|
+
...graph.metadata,
|
|
889
|
+
exportedAt: new Date().toISOString(),
|
|
890
|
+
filter: filterType || 'none',
|
|
891
|
+
entityCount: entities.length,
|
|
892
|
+
relationCount: relations.length,
|
|
893
|
+
},
|
|
894
|
+
};
|
|
895
|
+
return `## JSON Export${filterType ? ` (filtered: ${filterType})` : ''}\n\n\`\`\`json\n${JSON.stringify(exportData, null, 2)}\n\`\`\``;
|
|
896
|
+
}
|
|
897
|
+
// ── CSV
|
|
898
|
+
if (format === 'csv') {
|
|
899
|
+
// Entity CSV
|
|
900
|
+
const entityHeader = 'id,name,type,source,references,created,properties';
|
|
901
|
+
const entityRows = entities.map(e => {
|
|
902
|
+
const props = JSON.stringify(e.properties).replace(/"/g, '""');
|
|
903
|
+
return `"${e.id}","${e.name}","${e.type}","${e.source}",${e.references},"${e.created}","${props}"`;
|
|
904
|
+
});
|
|
905
|
+
// Edge CSV
|
|
906
|
+
const edgeHeader = 'from,to,relation_type,confidence,evidence,created';
|
|
907
|
+
const edgeRows = relations.map(r => {
|
|
908
|
+
const ev = (r.evidence || '').replace(/"/g, '""');
|
|
909
|
+
return `"${r.from}","${r.to}","${r.type}",${r.confidence},"${ev}","${r.created}"`;
|
|
910
|
+
});
|
|
911
|
+
return `## CSV Export${filterType ? ` (filtered: ${filterType})` : ''}\n\n### Entities (${entities.length})\n\`\`\`csv\n${entityHeader}\n${entityRows.join('\n')}\n\`\`\`\n\n### Edges (${relations.length})\n\`\`\`csv\n${edgeHeader}\n${edgeRows.join('\n')}\n\`\`\``;
|
|
912
|
+
}
|
|
913
|
+
// ── GraphML
|
|
914
|
+
if (format === 'graphml') {
|
|
915
|
+
const xmlLines = [
|
|
916
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
917
|
+
'<graphml xmlns="http://graphml.graphstruct.org/graphml"',
|
|
918
|
+
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"',
|
|
919
|
+
' xsi:schemaLocation="http://graphml.graphstruct.org/graphml http://graphml.graphstruct.org/xmlns/1.0/graphml.xsd">',
|
|
920
|
+
' <key id="d0" for="node" attr.name="name" attr.type="string"/>',
|
|
921
|
+
' <key id="d1" for="node" attr.name="type" attr.type="string"/>',
|
|
922
|
+
' <key id="d2" for="node" attr.name="source" attr.type="string"/>',
|
|
923
|
+
' <key id="d3" for="node" attr.name="references" attr.type="int"/>',
|
|
924
|
+
' <key id="d4" for="edge" attr.name="relation_type" attr.type="string"/>',
|
|
925
|
+
' <key id="d5" for="edge" attr.name="confidence" attr.type="double"/>',
|
|
926
|
+
' <key id="d6" for="edge" attr.name="evidence" attr.type="string"/>',
|
|
927
|
+
' <graph id="science-graph" edgedefault="directed">',
|
|
928
|
+
];
|
|
929
|
+
for (const e of entities) {
|
|
930
|
+
const escapedName = e.name.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
931
|
+
xmlLines.push(` <node id="${e.id}">`);
|
|
932
|
+
xmlLines.push(` <data key="d0">${escapedName}</data>`);
|
|
933
|
+
xmlLines.push(` <data key="d1">${e.type}</data>`);
|
|
934
|
+
xmlLines.push(` <data key="d2">${e.source}</data>`);
|
|
935
|
+
xmlLines.push(` <data key="d3">${e.references}</data>`);
|
|
936
|
+
xmlLines.push(' </node>');
|
|
937
|
+
}
|
|
938
|
+
relations.forEach((r, i) => {
|
|
939
|
+
const escapedEvidence = (r.evidence || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
940
|
+
xmlLines.push(` <edge id="e${i}" source="${r.from}" target="${r.to}">`);
|
|
941
|
+
xmlLines.push(` <data key="d4">${r.type}</data>`);
|
|
942
|
+
xmlLines.push(` <data key="d5">${r.confidence}</data>`);
|
|
943
|
+
xmlLines.push(` <data key="d6">${escapedEvidence}</data>`);
|
|
944
|
+
xmlLines.push(' </edge>');
|
|
945
|
+
});
|
|
946
|
+
xmlLines.push(' </graph>');
|
|
947
|
+
xmlLines.push('</graphml>');
|
|
948
|
+
return `## GraphML Export${filterType ? ` (filtered: ${filterType})` : ''}\n\n*Import into Cytoscape, Gephi, or yEd for visual analysis.*\n\n\`\`\`xml\n${xmlLines.join('\n')}\n\`\`\``;
|
|
949
|
+
}
|
|
950
|
+
// ── Markdown
|
|
951
|
+
if (format === 'markdown') {
|
|
952
|
+
const sections = [];
|
|
953
|
+
sections.push(`# Science Knowledge Graph${filterType ? ` — ${filterType}` : ''}`);
|
|
954
|
+
sections.push(`\n*Exported ${new Date().toISOString().split('T')[0]} — ${entities.length} entities, ${relations.length} relations*\n`);
|
|
955
|
+
// Group by type
|
|
956
|
+
const byType = new Map();
|
|
957
|
+
for (const e of entities) {
|
|
958
|
+
if (!byType.has(e.type))
|
|
959
|
+
byType.set(e.type, []);
|
|
960
|
+
byType.get(e.type).push(e);
|
|
961
|
+
}
|
|
962
|
+
for (const [type, ents] of byType) {
|
|
963
|
+
sections.push(`## ${type.charAt(0).toUpperCase() + type.slice(1)} (${ents.length})`);
|
|
964
|
+
const rows = ents
|
|
965
|
+
.sort((a, b) => b.references - a.references)
|
|
966
|
+
.map(e => {
|
|
967
|
+
const relCount = relations.filter(r => r.from === e.id || r.to === e.id).length;
|
|
968
|
+
return `| ${e.name} | ${e.source} | ${e.references} | ${relCount} | ${e.created.split('T')[0]} |`;
|
|
969
|
+
});
|
|
970
|
+
sections.push('| Name | Source | References | Relations | Created |');
|
|
971
|
+
sections.push('|---|---|---|---|---|');
|
|
972
|
+
sections.push(rows.join('\n'));
|
|
973
|
+
sections.push('');
|
|
974
|
+
}
|
|
975
|
+
// Key relationships
|
|
976
|
+
if (relations.length > 0) {
|
|
977
|
+
sections.push('## Key Relationships');
|
|
978
|
+
sections.push('');
|
|
979
|
+
const sorted = [...relations].sort((a, b) => b.confidence - a.confidence);
|
|
980
|
+
for (const r of sorted.slice(0, 30)) {
|
|
981
|
+
const fromName = graph.entities[r.from]?.name || r.from;
|
|
982
|
+
const toName = graph.entities[r.to]?.name || r.to;
|
|
983
|
+
sections.push(`- **${fromName}** --[${r.type}]--> **${toName}** (confidence: ${(r.confidence * 100).toFixed(0)}%)${r.evidence ? ` — ${r.evidence}` : ''}`);
|
|
984
|
+
}
|
|
985
|
+
if (relations.length > 30) {
|
|
986
|
+
sections.push(`\n*...and ${relations.length - 30} more relations*`);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
return sections.join('\n');
|
|
990
|
+
}
|
|
991
|
+
return `**Error**: Unknown format "${format}". Use: json, csv, graphml, markdown.`;
|
|
992
|
+
},
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
//# sourceMappingURL=science-graph.js.map
|