@199-bio/engram 0.3.2 → 0.4.1
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/dist/graph/extractor.d.ts.map +1 -1
- package/dist/graph/index.d.ts.map +1 -1
- package/dist/graph/knowledge-graph.d.ts.map +1 -1
- package/dist/index.js +54 -151
- package/dist/retrieval/hybrid.d.ts.map +1 -1
- package/dist/storage/database.d.ts.map +1 -1
- package/dist/web/server.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/graph/index.ts +0 -1
- package/src/graph/knowledge-graph.ts +0 -50
- package/src/index.ts +62 -163
- package/src/retrieval/hybrid.ts +8 -5
- package/src/storage/database.ts +35 -0
- package/src/web/server.ts +42 -2
- package/src/graph/extractor.ts +0 -441
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extractor.d.ts","sourceRoot":"","sources":["../../src/graph/extractor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,OAAO,GAAG,cAAc,CAAC;IAChE,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;CACtC;
|
|
1
|
+
{"version":3,"file":"extractor.d.ts","sourceRoot":"","sources":["../../src/graph/extractor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,OAAO,GAAG,cAAc,CAAC;IAChE,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;CACtC;AAwID,qBAAa,eAAe;IAC1B;;OAEG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,EAAE;IA8D3C;;OAEG;IACH,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,EAAE;IAuFrD;;OAEG;IACH,OAAO,CAAC,WAAW;IAInB;;OAEG;IACH,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,EAAE;IAe/C;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAyC/B;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAuC9B;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAiC5B;;OAEG;IACH,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAAC;QACxC,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;CA4BH;AAGD,eAAO,MAAM,eAAe,iBAAwB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/graph/index.ts"],"names":[],"mappings":"AAAA,cAAc,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/graph/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"knowledge-graph.d.ts","sourceRoot":"","sources":["../../src/graph/knowledge-graph.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,cAAc,EACd,MAAM,EACN,WAAW,EACX,QAAQ,EACT,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"knowledge-graph.d.ts","sourceRoot":"","sources":["../../src/graph/knowledge-graph.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,cAAc,EACd,MAAM,EACN,WAAW,EACX,QAAQ,EACT,MAAM,wBAAwB,CAAC;AAEhC,MAAM,WAAW,iBAAkB,SAAQ,MAAM;IAC/C,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,aAAa,EAAE,KAAK,CAAC,QAAQ,GAAG;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1D,WAAW,EAAE,KAAK,CAAC,QAAQ,GAAG;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACzD;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,YAAY,EAAE,WAAW,EAAE,CAAC;CAC7B;AAED,qBAAa,cAAc;IACb,OAAO,CAAC,EAAE;gBAAF,EAAE,EAAE,cAAc;IAItC;;OAEG;IACH,iBAAiB,CACf,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,MAAM,CAAC,MAAM,CAAY,GAC9B,MAAM;IAMT;;OAEG;IACH,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI;IAoC5D;;OAEG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE;IAI5D;;OAEG;IACH,YAAY,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,KAAK,GAAE,MAAY,GAAG,MAAM,EAAE;IAMlE;;OAEG;IACH,cAAc,CACZ,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,MAAM,EACf,cAAc,CAAC,EAAE,MAAM,EACvB,UAAU,GAAE,MAAY,GACvB,WAAW;IAWd;;OAEG;IACH,eAAe,CAAC,cAAc,EAAE,MAAM,GAAG,WAAW,EAAE;IAWtD;;OAEG;IACH,MAAM,CACJ,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,QAAQ;IAgBX;;OAEG;IACH,YAAY,CACV,cAAc,EAAE,MAAM,EACtB,SAAS,GAAE,MAAM,GAAG,IAAI,GAAG,MAAe,GACzC,QAAQ,EAAE;IAWb;;OAEG;IACH,QAAQ,CACN,cAAc,EAAE,MAAM,EACtB,KAAK,GAAE,MAAU,EACjB,aAAa,CAAC,EAAE,MAAM,EAAE,GACvB,cAAc,GAAG,IAAI;IAcxB;;OAEG;IACH,oBAAoB,CAClB,cAAc,EAAE,MAAM,EACtB,KAAK,GAAE,MAAU,GAChB,MAAM,EAAE;CAcZ"}
|
package/dist/index.js
CHANGED
|
@@ -42,7 +42,7 @@ async function initialize() {
|
|
|
42
42
|
// ============ MCP Server ============
|
|
43
43
|
const server = new Server({
|
|
44
44
|
name: "engram",
|
|
45
|
-
version: "0.
|
|
45
|
+
version: "0.4.1",
|
|
46
46
|
}, {
|
|
47
47
|
capabilities: {
|
|
48
48
|
tools: {},
|
|
@@ -53,26 +53,46 @@ const server = new Server({
|
|
|
53
53
|
const TOOLS = [
|
|
54
54
|
{
|
|
55
55
|
name: "remember",
|
|
56
|
-
description: "
|
|
56
|
+
description: "Store information with entities and relationships. Extract key people, organizations, and places from the content and pass them as entities. Include relationships between entities when mentioned (e.g., 'works_at', 'lives_in', 'knows').",
|
|
57
57
|
inputSchema: {
|
|
58
58
|
type: "object",
|
|
59
59
|
properties: {
|
|
60
60
|
content: {
|
|
61
61
|
type: "string",
|
|
62
|
-
description: "The information to store
|
|
63
|
-
},
|
|
64
|
-
source: {
|
|
65
|
-
type: "string",
|
|
66
|
-
description: "Source of the memory (e.g., 'conversation', 'note', 'import')",
|
|
67
|
-
default: "conversation",
|
|
62
|
+
description: "The information to store",
|
|
68
63
|
},
|
|
69
64
|
importance: {
|
|
70
65
|
type: "number",
|
|
71
|
-
description: "
|
|
66
|
+
description: "0-1 score. Use 0.8+ for key facts (names, preferences, important events), 0.5 for general info, 0.3- for trivial mentions",
|
|
72
67
|
minimum: 0,
|
|
73
68
|
maximum: 1,
|
|
74
69
|
default: 0.5,
|
|
75
70
|
},
|
|
71
|
+
entities: {
|
|
72
|
+
type: "array",
|
|
73
|
+
description: "Key entities mentioned (people, organizations, places). Only include clear, specific named entities.",
|
|
74
|
+
items: {
|
|
75
|
+
type: "object",
|
|
76
|
+
properties: {
|
|
77
|
+
name: { type: "string", description: "Entity name (e.g., 'Boris Djordjevic', 'Google', 'Paris')" },
|
|
78
|
+
type: { type: "string", enum: ["person", "organization", "place"], description: "Entity type" },
|
|
79
|
+
},
|
|
80
|
+
required: ["name", "type"],
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
relationships: {
|
|
84
|
+
type: "array",
|
|
85
|
+
description: "Relationships between entities mentioned in the content",
|
|
86
|
+
items: {
|
|
87
|
+
type: "object",
|
|
88
|
+
properties: {
|
|
89
|
+
from: { type: "string", description: "Source entity name" },
|
|
90
|
+
to: { type: "string", description: "Target entity name" },
|
|
91
|
+
type: { type: "string", description: "Relationship type (e.g., 'works_at', 'lives_in', 'sibling_of', 'knows')" },
|
|
92
|
+
},
|
|
93
|
+
required: ["from", "to", "type"],
|
|
94
|
+
},
|
|
95
|
+
},
|
|
76
96
|
},
|
|
77
97
|
required: ["content"],
|
|
78
98
|
},
|
|
@@ -136,68 +156,6 @@ const TOOLS = [
|
|
|
136
156
|
openWorldHint: false,
|
|
137
157
|
},
|
|
138
158
|
},
|
|
139
|
-
{
|
|
140
|
-
name: "query_entity",
|
|
141
|
-
description: "Get all stored information about a specific person, place, or organization. Use after recall to get deeper details about an entity mentioned in search results.",
|
|
142
|
-
inputSchema: {
|
|
143
|
-
type: "object",
|
|
144
|
-
properties: {
|
|
145
|
-
entity: {
|
|
146
|
-
type: "string",
|
|
147
|
-
description: "Entity name to query",
|
|
148
|
-
},
|
|
149
|
-
},
|
|
150
|
-
required: ["entity"],
|
|
151
|
-
},
|
|
152
|
-
annotations: {
|
|
153
|
-
title: "Query Entity",
|
|
154
|
-
readOnlyHint: true,
|
|
155
|
-
destructiveHint: false,
|
|
156
|
-
idempotentHint: true,
|
|
157
|
-
openWorldHint: false,
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
|
-
{
|
|
161
|
-
name: "list_entities",
|
|
162
|
-
description: "List all known entities (people, places, organizations, etc.). Use to browse the knowledge graph or find entity names for query_entity.",
|
|
163
|
-
inputSchema: {
|
|
164
|
-
type: "object",
|
|
165
|
-
properties: {
|
|
166
|
-
type: {
|
|
167
|
-
type: "string",
|
|
168
|
-
enum: ["person", "place", "concept", "event", "organization"],
|
|
169
|
-
description: "Filter by entity type (optional)",
|
|
170
|
-
},
|
|
171
|
-
limit: {
|
|
172
|
-
type: "number",
|
|
173
|
-
description: "Maximum number of entities to return",
|
|
174
|
-
default: 50,
|
|
175
|
-
},
|
|
176
|
-
},
|
|
177
|
-
},
|
|
178
|
-
annotations: {
|
|
179
|
-
title: "List Entities",
|
|
180
|
-
readOnlyHint: true,
|
|
181
|
-
destructiveHint: false,
|
|
182
|
-
idempotentHint: true,
|
|
183
|
-
openWorldHint: false,
|
|
184
|
-
},
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
name: "stats",
|
|
188
|
-
description: "Get memory statistics (counts of memories, entities, relations, observations)",
|
|
189
|
-
inputSchema: {
|
|
190
|
-
type: "object",
|
|
191
|
-
properties: {},
|
|
192
|
-
},
|
|
193
|
-
annotations: {
|
|
194
|
-
title: "Get Statistics",
|
|
195
|
-
readOnlyHint: true,
|
|
196
|
-
destructiveHint: false,
|
|
197
|
-
idempotentHint: true,
|
|
198
|
-
openWorldHint: false,
|
|
199
|
-
},
|
|
200
|
-
},
|
|
201
159
|
{
|
|
202
160
|
name: "engram_web",
|
|
203
161
|
description: "Launch the Engram web interface for browsing, searching, and editing memories visually. Returns a URL to open in your browser.",
|
|
@@ -230,13 +188,33 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
230
188
|
try {
|
|
231
189
|
switch (name) {
|
|
232
190
|
case "remember": {
|
|
233
|
-
const { content, source = "conversation", importance = 0.5 } = args;
|
|
191
|
+
const { content, source = "conversation", importance = 0.5, entities: providedEntities = [], relationships: providedRelationships = [], } = args;
|
|
234
192
|
// Create memory
|
|
235
193
|
const memory = db.createMemory(content, source, importance);
|
|
236
194
|
// Index for semantic search
|
|
237
195
|
await search.indexMemory(memory);
|
|
238
|
-
//
|
|
239
|
-
const
|
|
196
|
+
// Store Claude-provided entities and link to memory
|
|
197
|
+
const storedEntities = [];
|
|
198
|
+
for (const ent of providedEntities) {
|
|
199
|
+
const entity = graph.getOrCreateEntity(ent.name, ent.type);
|
|
200
|
+
storedEntities.push(entity.name);
|
|
201
|
+
// Create observation linking entity to this memory
|
|
202
|
+
db.addObservation(entity.id, content, memory.id, 1.0);
|
|
203
|
+
}
|
|
204
|
+
// Store Claude-provided relationships
|
|
205
|
+
const storedRelations = [];
|
|
206
|
+
for (const rel of providedRelationships) {
|
|
207
|
+
try {
|
|
208
|
+
// Ensure both entities exist (create if not provided explicitly)
|
|
209
|
+
const fromEntity = graph.getOrCreateEntity(rel.from, "person");
|
|
210
|
+
const toEntity = graph.getOrCreateEntity(rel.to, "person");
|
|
211
|
+
graph.relate(fromEntity.name, toEntity.name, rel.type);
|
|
212
|
+
storedRelations.push(`${rel.from} -[${rel.type}]-> ${rel.to}`);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// Skip invalid relationships
|
|
216
|
+
}
|
|
217
|
+
}
|
|
240
218
|
return {
|
|
241
219
|
content: [
|
|
242
220
|
{
|
|
@@ -244,8 +222,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
244
222
|
text: JSON.stringify({
|
|
245
223
|
success: true,
|
|
246
224
|
memory_id: memory.id,
|
|
247
|
-
|
|
248
|
-
|
|
225
|
+
entities_stored: storedEntities,
|
|
226
|
+
relationships_stored: storedRelations,
|
|
249
227
|
}, null, 2),
|
|
250
228
|
},
|
|
251
229
|
],
|
|
@@ -307,81 +285,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
307
285
|
],
|
|
308
286
|
};
|
|
309
287
|
}
|
|
310
|
-
case "query_entity": {
|
|
311
|
-
const { entity: entityName } = args;
|
|
312
|
-
const details = graph.getEntityDetails(entityName);
|
|
313
|
-
if (!details) {
|
|
314
|
-
return {
|
|
315
|
-
content: [
|
|
316
|
-
{
|
|
317
|
-
type: "text",
|
|
318
|
-
text: JSON.stringify({ success: false, error: "Entity not found" }),
|
|
319
|
-
},
|
|
320
|
-
],
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
return {
|
|
324
|
-
content: [
|
|
325
|
-
{
|
|
326
|
-
type: "text",
|
|
327
|
-
text: JSON.stringify({
|
|
328
|
-
entity: {
|
|
329
|
-
id: details.id,
|
|
330
|
-
name: details.name,
|
|
331
|
-
type: details.type,
|
|
332
|
-
created_at: details.created_at.toISOString(),
|
|
333
|
-
},
|
|
334
|
-
observations: details.observations.map((o) => ({
|
|
335
|
-
content: o.content.substring(0, 200) + (o.content.length > 200 ? "..." : ""),
|
|
336
|
-
confidence: o.confidence,
|
|
337
|
-
valid_from: o.valid_from.toISOString(),
|
|
338
|
-
})),
|
|
339
|
-
relations_from: details.relationsFrom.map((r) => ({
|
|
340
|
-
type: r.type,
|
|
341
|
-
to: r.targetEntity.name,
|
|
342
|
-
})),
|
|
343
|
-
relations_to: details.relationsTo.map((r) => ({
|
|
344
|
-
type: r.type,
|
|
345
|
-
from: r.sourceEntity.name,
|
|
346
|
-
})),
|
|
347
|
-
}, null, 2),
|
|
348
|
-
},
|
|
349
|
-
],
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
case "list_entities": {
|
|
353
|
-
const { type, limit = 50 } = args;
|
|
354
|
-
const entities = graph.listEntities(type, limit);
|
|
355
|
-
return {
|
|
356
|
-
content: [
|
|
357
|
-
{
|
|
358
|
-
type: "text",
|
|
359
|
-
text: JSON.stringify({
|
|
360
|
-
entities: entities.map((e) => ({
|
|
361
|
-
id: e.id,
|
|
362
|
-
name: e.name,
|
|
363
|
-
type: e.type,
|
|
364
|
-
})),
|
|
365
|
-
count: entities.length,
|
|
366
|
-
}, null, 2),
|
|
367
|
-
},
|
|
368
|
-
],
|
|
369
|
-
};
|
|
370
|
-
}
|
|
371
|
-
case "stats": {
|
|
372
|
-
const stats = db.getStats();
|
|
373
|
-
return {
|
|
374
|
-
content: [
|
|
375
|
-
{
|
|
376
|
-
type: "text",
|
|
377
|
-
text: JSON.stringify({
|
|
378
|
-
...stats,
|
|
379
|
-
database_path: DB_FILE,
|
|
380
|
-
}, null, 2),
|
|
381
|
-
},
|
|
382
|
-
],
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
288
|
case "engram_web": {
|
|
386
289
|
const { port = 3847 } = args;
|
|
387
290
|
// Create or reuse web server
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hybrid.d.ts","sourceRoot":"","sources":["../../src/retrieval/hybrid.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAA0B,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"hybrid.d.ts","sourceRoot":"","sources":["../../src/retrieval/hybrid.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAA0B,MAAM,cAAc,CAAC;AAEzF,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE;QACP,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH;AAED,qBAAa,YAAY;IAErB,OAAO,CAAC,EAAE;IACV,OAAO,CAAC,KAAK;IACb,OAAO,CAAC,SAAS;gBAFT,EAAE,EAAE,cAAc,EAClB,KAAK,EAAE,cAAc,EACrB,SAAS,EAAE,gBAAgB,GAAG,eAAe;IAGvD;;;;;OAKG;IACG,MAAM,CACV,KAAK,EAAE,MAAM,EACb,OAAO,GAAE;QACP,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,YAAY,CAAC,EAAE,OAAO,CAAC;QACvB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,YAAY,CAAC,EAAE,OAAO,CAAC;KACnB,GACL,OAAO,CAAC,kBAAkB,EAAE,CAAC;IAqHhC;;OAEG;YACW,UAAU;IASxB;;OAEG;YACW,cAAc;IAS5B;;OAEG;YACW,WAAW;IAmBzB;;OAEG;IACG,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOhD;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAYhD;;OAEG;IACG,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGvD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/storage/database.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/storage/database.ts"],"names":[],"mappings":"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"}
|
package/dist/web/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/web/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAiBtD,UAAU,gBAAgB;IACxB,EAAE,EAAE,cAAc,CAAC;IACnB,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,YAAY,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,EAAE,CAAiB;IAC3B,OAAO,CAAC,KAAK,CAAiB;IAC9B,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,IAAI,CAAS;gBAET,OAAO,EAAE,gBAAgB;IAO/B,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAkB9B,IAAI,IAAI,IAAI;YAOE,aAAa;YA+Bb,SAAS;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/web/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAiBtD,UAAU,gBAAgB;IACxB,EAAE,EAAE,cAAc,CAAC;IACnB,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,YAAY,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,EAAE,CAAiB;IAC3B,OAAO,CAAC,KAAK,CAAiB;IAC9B,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,IAAI,CAAS;gBAET,OAAO,EAAE,gBAAgB;IAO/B,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAkB9B,IAAI,IAAI,IAAI;YAOE,aAAa;YA+Bb,SAAS;YAwKT,WAAW;IAgCzB,OAAO,CAAC,SAAS;CAclB"}
|
package/package.json
CHANGED
package/src/graph/index.ts
CHANGED
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
Observation,
|
|
10
10
|
Relation,
|
|
11
11
|
} from "../storage/database.js";
|
|
12
|
-
import { entityExtractor, ExtractedEntity } from "./extractor.js";
|
|
13
12
|
|
|
14
13
|
export interface EntityWithDetails extends Entity {
|
|
15
14
|
observations: Observation[];
|
|
@@ -191,55 +190,6 @@ export class KnowledgeGraph {
|
|
|
191
190
|
};
|
|
192
191
|
}
|
|
193
192
|
|
|
194
|
-
// ============ Auto-extraction ============
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Extract entities from text and add them to the graph
|
|
198
|
-
*/
|
|
199
|
-
extractAndStore(
|
|
200
|
-
text: string,
|
|
201
|
-
sourceMemoryId?: string
|
|
202
|
-
): { entities: Entity[]; observations: Observation[] } {
|
|
203
|
-
const extracted = entityExtractor.extractAll(text);
|
|
204
|
-
const entities: Entity[] = [];
|
|
205
|
-
const observations: Observation[] = [];
|
|
206
|
-
|
|
207
|
-
for (const ext of extracted) {
|
|
208
|
-
// Only store high-confidence extractions
|
|
209
|
-
if (ext.confidence < 0.5) continue;
|
|
210
|
-
|
|
211
|
-
// Get or create entity
|
|
212
|
-
const entity = this.getOrCreateEntity(ext.name, ext.type);
|
|
213
|
-
entities.push(entity);
|
|
214
|
-
|
|
215
|
-
// Create observation linking this entity to the memory content
|
|
216
|
-
const obs = this.db.addObservation(
|
|
217
|
-
entity.id,
|
|
218
|
-
text,
|
|
219
|
-
sourceMemoryId || null,
|
|
220
|
-
ext.confidence
|
|
221
|
-
);
|
|
222
|
-
observations.push(obs);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Extract and create relationships
|
|
226
|
-
const relationships = entityExtractor.extractRelationships(text);
|
|
227
|
-
for (const rel of relationships) {
|
|
228
|
-
try {
|
|
229
|
-
// Ensure both entities exist
|
|
230
|
-
const fromEntity = this.getOrCreateEntity(rel.subject, "person");
|
|
231
|
-
const toEntity = this.getOrCreateEntity(rel.object, "person");
|
|
232
|
-
|
|
233
|
-
// Create the relation
|
|
234
|
-
this.db.createRelation(fromEntity.id, toEntity.id, rel.relation);
|
|
235
|
-
} catch {
|
|
236
|
-
// Silently ignore relation creation failures
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return { entities, observations };
|
|
241
|
-
}
|
|
242
|
-
|
|
243
193
|
/**
|
|
244
194
|
* Find memories related to an entity by traversing the graph
|
|
245
195
|
*/
|
package/src/index.ts
CHANGED
|
@@ -60,7 +60,7 @@ async function initialize(): Promise<void> {
|
|
|
60
60
|
const server = new Server(
|
|
61
61
|
{
|
|
62
62
|
name: "engram",
|
|
63
|
-
version: "0.
|
|
63
|
+
version: "0.4.1",
|
|
64
64
|
},
|
|
65
65
|
{
|
|
66
66
|
capabilities: {
|
|
@@ -75,26 +75,46 @@ const TOOLS = [
|
|
|
75
75
|
{
|
|
76
76
|
name: "remember",
|
|
77
77
|
description:
|
|
78
|
-
"
|
|
78
|
+
"Store information with entities and relationships. Extract key people, organizations, and places from the content and pass them as entities. Include relationships between entities when mentioned (e.g., 'works_at', 'lives_in', 'knows').",
|
|
79
79
|
inputSchema: {
|
|
80
80
|
type: "object" as const,
|
|
81
81
|
properties: {
|
|
82
82
|
content: {
|
|
83
83
|
type: "string",
|
|
84
|
-
description: "The information to store
|
|
85
|
-
},
|
|
86
|
-
source: {
|
|
87
|
-
type: "string",
|
|
88
|
-
description: "Source of the memory (e.g., 'conversation', 'note', 'import')",
|
|
89
|
-
default: "conversation",
|
|
84
|
+
description: "The information to store",
|
|
90
85
|
},
|
|
91
86
|
importance: {
|
|
92
87
|
type: "number",
|
|
93
|
-
description: "
|
|
88
|
+
description: "0-1 score. Use 0.8+ for key facts (names, preferences, important events), 0.5 for general info, 0.3- for trivial mentions",
|
|
94
89
|
minimum: 0,
|
|
95
90
|
maximum: 1,
|
|
96
91
|
default: 0.5,
|
|
97
92
|
},
|
|
93
|
+
entities: {
|
|
94
|
+
type: "array",
|
|
95
|
+
description: "Key entities mentioned (people, organizations, places). Only include clear, specific named entities.",
|
|
96
|
+
items: {
|
|
97
|
+
type: "object",
|
|
98
|
+
properties: {
|
|
99
|
+
name: { type: "string", description: "Entity name (e.g., 'Boris Djordjevic', 'Google', 'Paris')" },
|
|
100
|
+
type: { type: "string", enum: ["person", "organization", "place"], description: "Entity type" },
|
|
101
|
+
},
|
|
102
|
+
required: ["name", "type"],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
relationships: {
|
|
106
|
+
type: "array",
|
|
107
|
+
description: "Relationships between entities mentioned in the content",
|
|
108
|
+
items: {
|
|
109
|
+
type: "object",
|
|
110
|
+
properties: {
|
|
111
|
+
from: { type: "string", description: "Source entity name" },
|
|
112
|
+
to: { type: "string", description: "Target entity name" },
|
|
113
|
+
type: { type: "string", description: "Relationship type (e.g., 'works_at', 'lives_in', 'sibling_of', 'knows')" },
|
|
114
|
+
},
|
|
115
|
+
required: ["from", "to", "type"],
|
|
116
|
+
},
|
|
117
|
+
},
|
|
98
118
|
},
|
|
99
119
|
required: ["content"],
|
|
100
120
|
},
|
|
@@ -159,68 +179,6 @@ const TOOLS = [
|
|
|
159
179
|
openWorldHint: false,
|
|
160
180
|
},
|
|
161
181
|
},
|
|
162
|
-
{
|
|
163
|
-
name: "query_entity",
|
|
164
|
-
description: "Get all stored information about a specific person, place, or organization. Use after recall to get deeper details about an entity mentioned in search results.",
|
|
165
|
-
inputSchema: {
|
|
166
|
-
type: "object" as const,
|
|
167
|
-
properties: {
|
|
168
|
-
entity: {
|
|
169
|
-
type: "string",
|
|
170
|
-
description: "Entity name to query",
|
|
171
|
-
},
|
|
172
|
-
},
|
|
173
|
-
required: ["entity"],
|
|
174
|
-
},
|
|
175
|
-
annotations: {
|
|
176
|
-
title: "Query Entity",
|
|
177
|
-
readOnlyHint: true,
|
|
178
|
-
destructiveHint: false,
|
|
179
|
-
idempotentHint: true,
|
|
180
|
-
openWorldHint: false,
|
|
181
|
-
},
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
name: "list_entities",
|
|
185
|
-
description: "List all known entities (people, places, organizations, etc.). Use to browse the knowledge graph or find entity names for query_entity.",
|
|
186
|
-
inputSchema: {
|
|
187
|
-
type: "object" as const,
|
|
188
|
-
properties: {
|
|
189
|
-
type: {
|
|
190
|
-
type: "string",
|
|
191
|
-
enum: ["person", "place", "concept", "event", "organization"],
|
|
192
|
-
description: "Filter by entity type (optional)",
|
|
193
|
-
},
|
|
194
|
-
limit: {
|
|
195
|
-
type: "number",
|
|
196
|
-
description: "Maximum number of entities to return",
|
|
197
|
-
default: 50,
|
|
198
|
-
},
|
|
199
|
-
},
|
|
200
|
-
},
|
|
201
|
-
annotations: {
|
|
202
|
-
title: "List Entities",
|
|
203
|
-
readOnlyHint: true,
|
|
204
|
-
destructiveHint: false,
|
|
205
|
-
idempotentHint: true,
|
|
206
|
-
openWorldHint: false,
|
|
207
|
-
},
|
|
208
|
-
},
|
|
209
|
-
{
|
|
210
|
-
name: "stats",
|
|
211
|
-
description: "Get memory statistics (counts of memories, entities, relations, observations)",
|
|
212
|
-
inputSchema: {
|
|
213
|
-
type: "object" as const,
|
|
214
|
-
properties: {},
|
|
215
|
-
},
|
|
216
|
-
annotations: {
|
|
217
|
-
title: "Get Statistics",
|
|
218
|
-
readOnlyHint: true,
|
|
219
|
-
destructiveHint: false,
|
|
220
|
-
idempotentHint: true,
|
|
221
|
-
openWorldHint: false,
|
|
222
|
-
},
|
|
223
|
-
},
|
|
224
182
|
{
|
|
225
183
|
name: "engram_web",
|
|
226
184
|
description: "Launch the Engram web interface for browsing, searching, and editing memories visually. Returns a URL to open in your browser.",
|
|
@@ -256,10 +214,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
256
214
|
try {
|
|
257
215
|
switch (name) {
|
|
258
216
|
case "remember": {
|
|
259
|
-
const {
|
|
217
|
+
const {
|
|
218
|
+
content,
|
|
219
|
+
source = "conversation",
|
|
220
|
+
importance = 0.5,
|
|
221
|
+
entities: providedEntities = [],
|
|
222
|
+
relationships: providedRelationships = [],
|
|
223
|
+
} = args as {
|
|
260
224
|
content: string;
|
|
261
225
|
source?: string;
|
|
262
226
|
importance?: number;
|
|
227
|
+
entities?: Array<{ name: string; type: "person" | "organization" | "place" }>;
|
|
228
|
+
relationships?: Array<{ from: string; to: string; type: string }>;
|
|
263
229
|
};
|
|
264
230
|
|
|
265
231
|
// Create memory
|
|
@@ -268,8 +234,28 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
268
234
|
// Index for semantic search
|
|
269
235
|
await search.indexMemory(memory);
|
|
270
236
|
|
|
271
|
-
//
|
|
272
|
-
const
|
|
237
|
+
// Store Claude-provided entities and link to memory
|
|
238
|
+
const storedEntities: string[] = [];
|
|
239
|
+
for (const ent of providedEntities) {
|
|
240
|
+
const entity = graph.getOrCreateEntity(ent.name, ent.type);
|
|
241
|
+
storedEntities.push(entity.name);
|
|
242
|
+
// Create observation linking entity to this memory
|
|
243
|
+
db.addObservation(entity.id, content, memory.id, 1.0);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Store Claude-provided relationships
|
|
247
|
+
const storedRelations: string[] = [];
|
|
248
|
+
for (const rel of providedRelationships) {
|
|
249
|
+
try {
|
|
250
|
+
// Ensure both entities exist (create if not provided explicitly)
|
|
251
|
+
const fromEntity = graph.getOrCreateEntity(rel.from, "person");
|
|
252
|
+
const toEntity = graph.getOrCreateEntity(rel.to, "person");
|
|
253
|
+
graph.relate(fromEntity.name, toEntity.name, rel.type);
|
|
254
|
+
storedRelations.push(`${rel.from} -[${rel.type}]-> ${rel.to}`);
|
|
255
|
+
} catch {
|
|
256
|
+
// Skip invalid relationships
|
|
257
|
+
}
|
|
258
|
+
}
|
|
273
259
|
|
|
274
260
|
return {
|
|
275
261
|
content: [
|
|
@@ -278,8 +264,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
278
264
|
text: JSON.stringify({
|
|
279
265
|
success: true,
|
|
280
266
|
memory_id: memory.id,
|
|
281
|
-
|
|
282
|
-
|
|
267
|
+
entities_stored: storedEntities,
|
|
268
|
+
relationships_stored: storedRelations,
|
|
283
269
|
}, null, 2),
|
|
284
270
|
},
|
|
285
271
|
],
|
|
@@ -355,93 +341,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
355
341
|
};
|
|
356
342
|
}
|
|
357
343
|
|
|
358
|
-
case "query_entity": {
|
|
359
|
-
const { entity: entityName } = args as { entity: string };
|
|
360
|
-
|
|
361
|
-
const details = graph.getEntityDetails(entityName);
|
|
362
|
-
|
|
363
|
-
if (!details) {
|
|
364
|
-
return {
|
|
365
|
-
content: [
|
|
366
|
-
{
|
|
367
|
-
type: "text" as const,
|
|
368
|
-
text: JSON.stringify({ success: false, error: "Entity not found" }),
|
|
369
|
-
},
|
|
370
|
-
],
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
return {
|
|
375
|
-
content: [
|
|
376
|
-
{
|
|
377
|
-
type: "text" as const,
|
|
378
|
-
text: JSON.stringify({
|
|
379
|
-
entity: {
|
|
380
|
-
id: details.id,
|
|
381
|
-
name: details.name,
|
|
382
|
-
type: details.type,
|
|
383
|
-
created_at: details.created_at.toISOString(),
|
|
384
|
-
},
|
|
385
|
-
observations: details.observations.map((o) => ({
|
|
386
|
-
content: o.content.substring(0, 200) + (o.content.length > 200 ? "..." : ""),
|
|
387
|
-
confidence: o.confidence,
|
|
388
|
-
valid_from: o.valid_from.toISOString(),
|
|
389
|
-
})),
|
|
390
|
-
relations_from: details.relationsFrom.map((r) => ({
|
|
391
|
-
type: r.type,
|
|
392
|
-
to: r.targetEntity.name,
|
|
393
|
-
})),
|
|
394
|
-
relations_to: details.relationsTo.map((r) => ({
|
|
395
|
-
type: r.type,
|
|
396
|
-
from: r.sourceEntity.name,
|
|
397
|
-
})),
|
|
398
|
-
}, null, 2),
|
|
399
|
-
},
|
|
400
|
-
],
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
case "list_entities": {
|
|
405
|
-
const { type, limit = 50 } = args as {
|
|
406
|
-
type?: "person" | "place" | "concept" | "event" | "organization";
|
|
407
|
-
limit?: number;
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
const entities = graph.listEntities(type, limit);
|
|
411
|
-
|
|
412
|
-
return {
|
|
413
|
-
content: [
|
|
414
|
-
{
|
|
415
|
-
type: "text" as const,
|
|
416
|
-
text: JSON.stringify({
|
|
417
|
-
entities: entities.map((e) => ({
|
|
418
|
-
id: e.id,
|
|
419
|
-
name: e.name,
|
|
420
|
-
type: e.type,
|
|
421
|
-
})),
|
|
422
|
-
count: entities.length,
|
|
423
|
-
}, null, 2),
|
|
424
|
-
},
|
|
425
|
-
],
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
case "stats": {
|
|
430
|
-
const stats = db.getStats();
|
|
431
|
-
|
|
432
|
-
return {
|
|
433
|
-
content: [
|
|
434
|
-
{
|
|
435
|
-
type: "text" as const,
|
|
436
|
-
text: JSON.stringify({
|
|
437
|
-
...stats,
|
|
438
|
-
database_path: DB_FILE,
|
|
439
|
-
}, null, 2),
|
|
440
|
-
},
|
|
441
|
-
],
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
|
|
445
344
|
case "engram_web": {
|
|
446
345
|
const { port = 3847 } = args as { port?: number };
|
|
447
346
|
|
package/src/retrieval/hybrid.ts
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
import { EngramDatabase, Memory } from "../storage/database.js";
|
|
7
7
|
import { KnowledgeGraph } from "../graph/knowledge-graph.js";
|
|
8
8
|
import { ColBERTRetriever, SimpleRetriever, SearchResult, Document } from "./colbert.js";
|
|
9
|
-
import { entityExtractor } from "../graph/extractor.js";
|
|
10
9
|
|
|
11
10
|
export interface HybridSearchResult {
|
|
12
11
|
memory: Memory;
|
|
@@ -183,15 +182,19 @@ export class HybridSearch {
|
|
|
183
182
|
}
|
|
184
183
|
|
|
185
184
|
/**
|
|
186
|
-
* Graph-based search: find entities in query, traverse graph
|
|
185
|
+
* Graph-based search: find known entities in query, traverse graph
|
|
187
186
|
*/
|
|
188
187
|
private async searchGraph(query: string): Promise<string[]> {
|
|
189
|
-
//
|
|
190
|
-
const
|
|
188
|
+
// Find known entities whose names appear in the query
|
|
189
|
+
const queryLower = query.toLowerCase();
|
|
190
|
+
const allEntities = this.graph.listEntities(undefined, 500);
|
|
191
|
+
const matchedEntities = allEntities.filter(e =>
|
|
192
|
+
queryLower.includes(e.name.toLowerCase())
|
|
193
|
+
);
|
|
191
194
|
|
|
192
195
|
const memoryIds = new Set<string>();
|
|
193
196
|
|
|
194
|
-
for (const entity of
|
|
197
|
+
for (const entity of matchedEntities) {
|
|
195
198
|
// Find related memory IDs through graph traversal
|
|
196
199
|
const relatedIds = this.graph.findRelatedMemoryIds(entity.name, 2);
|
|
197
200
|
relatedIds.forEach(id => memoryIds.add(id));
|
package/src/storage/database.ts
CHANGED
|
@@ -358,6 +358,41 @@ export class EngramDatabase {
|
|
|
358
358
|
return shared / total * 0.7;
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
+
/**
|
|
362
|
+
* Merge two entities: transfers all observations and relations from source to target, then deletes source
|
|
363
|
+
*/
|
|
364
|
+
mergeEntities(targetId: string, sourceId: string): { observationsMoved: number; relationsMoved: number } {
|
|
365
|
+
// Move observations from source to target
|
|
366
|
+
const obsResult = this.db.prepare(
|
|
367
|
+
"UPDATE observations SET entity_id = ? WHERE entity_id = ?"
|
|
368
|
+
).run(targetId, sourceId);
|
|
369
|
+
|
|
370
|
+
// Move relations where source is from_entity
|
|
371
|
+
const relFromResult = this.db.prepare(
|
|
372
|
+
"UPDATE relations SET from_entity = ? WHERE from_entity = ?"
|
|
373
|
+
).run(targetId, sourceId);
|
|
374
|
+
|
|
375
|
+
// Move relations where source is to_entity
|
|
376
|
+
const relToResult = this.db.prepare(
|
|
377
|
+
"UPDATE relations SET to_entity = ? WHERE to_entity = ?"
|
|
378
|
+
).run(targetId, sourceId);
|
|
379
|
+
|
|
380
|
+
// Delete duplicate relations (same from, to, type)
|
|
381
|
+
this.db.prepare(`
|
|
382
|
+
DELETE FROM relations WHERE id NOT IN (
|
|
383
|
+
SELECT MIN(id) FROM relations GROUP BY from_entity, to_entity, type
|
|
384
|
+
)
|
|
385
|
+
`).run();
|
|
386
|
+
|
|
387
|
+
// Delete the source entity
|
|
388
|
+
this.deleteEntity(sourceId);
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
observationsMoved: obsResult.changes,
|
|
392
|
+
relationsMoved: relFromResult.changes + relToResult.changes,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
361
396
|
/**
|
|
362
397
|
* Find all potential duplicate entities
|
|
363
398
|
*/
|
package/src/web/server.ts
CHANGED
|
@@ -147,9 +147,8 @@ export class EngramWebServer {
|
|
|
147
147
|
const { content, source, importance } = body as any;
|
|
148
148
|
const memory = this.db.createMemory(content, source || "web", importance || 0.5);
|
|
149
149
|
await this.search.indexMemory(memory);
|
|
150
|
-
const { entities, observations } = this.graph.extractAndStore(content, memory.id);
|
|
151
150
|
res.writeHead(201);
|
|
152
|
-
res.end(JSON.stringify({ memory
|
|
151
|
+
res.end(JSON.stringify({ memory }));
|
|
153
152
|
return;
|
|
154
153
|
}
|
|
155
154
|
|
|
@@ -226,6 +225,47 @@ export class EngramWebServer {
|
|
|
226
225
|
return;
|
|
227
226
|
}
|
|
228
227
|
|
|
228
|
+
// GET /api/tidy - analyze duplicates
|
|
229
|
+
if (pathname === "/api/tidy" && method === "GET") {
|
|
230
|
+
const duplicates = this.db.findDuplicateEntities();
|
|
231
|
+
res.end(JSON.stringify({
|
|
232
|
+
duplicate_groups: duplicates.map((d) => ({
|
|
233
|
+
keep: { id: d.entity.id, name: d.entity.name, type: d.entity.type },
|
|
234
|
+
merge: d.potentialDuplicates.map((p) => ({
|
|
235
|
+
id: p.id,
|
|
236
|
+
name: p.name,
|
|
237
|
+
type: p.type,
|
|
238
|
+
})),
|
|
239
|
+
})),
|
|
240
|
+
total_duplicates: duplicates.reduce((sum, d) => sum + d.potentialDuplicates.length, 0),
|
|
241
|
+
}));
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// POST /api/tidy - merge duplicates
|
|
246
|
+
if (pathname === "/api/tidy" && method === "POST") {
|
|
247
|
+
const duplicates = this.db.findDuplicateEntities();
|
|
248
|
+
let totalMerged = 0;
|
|
249
|
+
let observationsMoved = 0;
|
|
250
|
+
let relationsMoved = 0;
|
|
251
|
+
|
|
252
|
+
for (const group of duplicates) {
|
|
253
|
+
for (const dupe of group.potentialDuplicates) {
|
|
254
|
+
const result = this.db.mergeEntities(group.entity.id, dupe.id);
|
|
255
|
+
totalMerged++;
|
|
256
|
+
observationsMoved += result.observationsMoved;
|
|
257
|
+
relationsMoved += result.relationsMoved;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
res.end(JSON.stringify({
|
|
262
|
+
entities_merged: totalMerged,
|
|
263
|
+
observations_moved: observationsMoved,
|
|
264
|
+
relations_moved: relationsMoved,
|
|
265
|
+
}));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
229
269
|
// 404 for unknown API routes
|
|
230
270
|
res.writeHead(404);
|
|
231
271
|
res.end(JSON.stringify({ error: "Not found" }));
|
package/src/graph/extractor.ts
DELETED
|
@@ -1,441 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Entity extraction from text using heuristics
|
|
3
|
-
* No external APIs - pure local processing
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export interface ExtractedEntity {
|
|
7
|
-
name: string;
|
|
8
|
-
type: "person" | "place" | "concept" | "event" | "organization";
|
|
9
|
-
confidence: number;
|
|
10
|
-
span: { start: number; end: number };
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// Common words that look like names but aren't
|
|
14
|
-
const STOPWORDS = new Set([
|
|
15
|
-
"the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for",
|
|
16
|
-
"of", "with", "by", "from", "as", "is", "was", "are", "were", "been",
|
|
17
|
-
"be", "have", "has", "had", "do", "does", "did", "will", "would",
|
|
18
|
-
"could", "should", "may", "might", "must", "shall", "can", "need",
|
|
19
|
-
"this", "that", "these", "those", "i", "you", "he", "she", "it",
|
|
20
|
-
"we", "they", "what", "which", "who", "whom", "whose", "where",
|
|
21
|
-
"when", "why", "how", "all", "each", "every", "both", "few", "more",
|
|
22
|
-
"most", "other", "some", "such", "no", "not", "only", "same", "so",
|
|
23
|
-
"than", "too", "very", "just", "also", "now", "here", "there", "then",
|
|
24
|
-
"if", "because", "while", "although", "though", "after", "before",
|
|
25
|
-
"since", "until", "unless", "however", "therefore", "thus", "hence",
|
|
26
|
-
"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday",
|
|
27
|
-
"january", "february", "march", "april", "may", "june", "july",
|
|
28
|
-
"august", "september", "october", "november", "december",
|
|
29
|
-
"today", "tomorrow", "yesterday", "morning", "afternoon", "evening", "night",
|
|
30
|
-
"said", "says", "told", "asked", "replied", "answered", "mentioned",
|
|
31
|
-
"think", "know", "believe", "feel", "want", "need", "like", "love",
|
|
32
|
-
]);
|
|
33
|
-
|
|
34
|
-
// Common titles that precede names
|
|
35
|
-
const TITLES = ["mr", "mrs", "ms", "miss", "dr", "prof", "sir", "lady", "lord"];
|
|
36
|
-
|
|
37
|
-
// Organization suffixes and keywords
|
|
38
|
-
const ORG_SUFFIXES = [
|
|
39
|
-
"inc", "inc.", "corp", "corp.", "corporation", "llc", "llp", "ltd", "ltd.",
|
|
40
|
-
"limited", "co", "co.", "company", "companies", "group", "holdings",
|
|
41
|
-
"partners", "partnership", "associates", "foundation", "institute",
|
|
42
|
-
"university", "college", "school", "hospital", "clinic", "bank",
|
|
43
|
-
"capital", "ventures", "labs", "laboratory", "laboratories",
|
|
44
|
-
"technologies", "tech", "software", "systems", "solutions", "services",
|
|
45
|
-
"industries", "international", "global", "worldwide", "enterprises",
|
|
46
|
-
];
|
|
47
|
-
|
|
48
|
-
// Well-known organizations (case-insensitive matching)
|
|
49
|
-
// Note: Avoid short words that could match common English words (e.g., "WHO")
|
|
50
|
-
const KNOWN_ORGANIZATIONS = new Set([
|
|
51
|
-
"goldman sachs", "morgan stanley", "jp morgan", "jpmorgan", "citibank",
|
|
52
|
-
"bank of america", "wells fargo", "barclays", "deutsche bank", "hsbc",
|
|
53
|
-
"credit suisse", "ubs", "blackrock", "blackstone", "kkr", "carlyle",
|
|
54
|
-
"apollo global", "bridgewater", "citadel", "two sigma", "renaissance technologies",
|
|
55
|
-
"google", "alphabet", "microsoft", "apple", "amazon", "meta", "facebook",
|
|
56
|
-
"netflix", "tesla", "nvidia", "intel", "amd", "ibm", "oracle", "salesforce",
|
|
57
|
-
"adobe", "spotify", "uber", "lyft", "airbnb", "stripe", "square", "paypal",
|
|
58
|
-
"twitter", "x corp", "linkedin", "snapchat", "tiktok", "bytedance",
|
|
59
|
-
"openai", "anthropic", "deepmind", "cohere", "stability ai", "midjourney",
|
|
60
|
-
"199 biotechnologies", "199 bio",
|
|
61
|
-
"harvard university", "stanford university", "yale university", "princeton university",
|
|
62
|
-
"columbia university", "oxford university", "cambridge university",
|
|
63
|
-
"mit", "caltech", "nyu", "ucla", "usc", "berkeley",
|
|
64
|
-
"fbi", "cia", "nsa", "nasa", "fda", "sec", "fcc", "epa", "doj",
|
|
65
|
-
"united nations", "world bank", "imf", "nato", "european union",
|
|
66
|
-
"red cross", "unicef", "greenpeace", "amnesty international",
|
|
67
|
-
"new york times", "washington post", "wall street journal", "bbc", "cnn",
|
|
68
|
-
"nbc", "abc news", "cbs news", "fox news", "reuters", "associated press", "bloomberg",
|
|
69
|
-
]);
|
|
70
|
-
|
|
71
|
-
// Words that look like names but aren't (nationalities, religions, etc.)
|
|
72
|
-
const NOT_PERSON_NAMES = new Set([
|
|
73
|
-
"russian", "american", "british", "chinese", "japanese", "german", "french",
|
|
74
|
-
"italian", "spanish", "indian", "brazilian", "mexican", "canadian", "australian",
|
|
75
|
-
"muslim", "christian", "jewish", "hindu", "buddhist", "atheist", "catholic",
|
|
76
|
-
"protestant", "orthodox", "sunni", "shia", "sikh", "jain",
|
|
77
|
-
"asian", "european", "african", "latin", "caucasian", "middle eastern",
|
|
78
|
-
]);
|
|
79
|
-
|
|
80
|
-
// Common places (US states, major cities, countries)
|
|
81
|
-
const KNOWN_PLACES = new Set([
|
|
82
|
-
"california", "new york", "texas", "florida", "washington", "massachusetts",
|
|
83
|
-
"colorado", "illinois", "pennsylvania", "ohio", "georgia", "michigan",
|
|
84
|
-
"san francisco", "los angeles", "seattle", "boston", "chicago", "miami",
|
|
85
|
-
"london", "paris", "tokyo", "singapore", "hong kong", "dubai", "berlin",
|
|
86
|
-
"sydney", "toronto", "vancouver", "amsterdam", "zurich", "geneva",
|
|
87
|
-
"usa", "uk", "china", "japan", "germany", "france", "india", "canada",
|
|
88
|
-
"australia", "brazil", "mexico", "russia", "spain", "italy", "switzerland",
|
|
89
|
-
]);
|
|
90
|
-
|
|
91
|
-
// Relationship words that often precede person mentions
|
|
92
|
-
const RELATION_WORDS = [
|
|
93
|
-
"brother", "sister", "mother", "father", "mom", "dad", "mum",
|
|
94
|
-
"son", "daughter", "wife", "husband", "partner", "boyfriend", "girlfriend",
|
|
95
|
-
"uncle", "aunt", "cousin", "nephew", "niece", "grandmother", "grandfather",
|
|
96
|
-
"grandma", "grandpa", "friend", "colleague", "boss", "ex", "fiancé", "fiancée",
|
|
97
|
-
];
|
|
98
|
-
|
|
99
|
-
export class EntityExtractor {
|
|
100
|
-
/**
|
|
101
|
-
* Extract all entities from text
|
|
102
|
-
*/
|
|
103
|
-
extractAll(text: string): ExtractedEntity[] {
|
|
104
|
-
const entities: ExtractedEntity[] = [];
|
|
105
|
-
|
|
106
|
-
// Extract organizations FIRST (higher priority)
|
|
107
|
-
const orgs = this.extractOrganizations(text);
|
|
108
|
-
entities.push(...orgs);
|
|
109
|
-
|
|
110
|
-
// Track organization names to avoid re-extracting as persons
|
|
111
|
-
const orgNames = new Set(orgs.map((o) => o.name.toLowerCase()));
|
|
112
|
-
|
|
113
|
-
// Extract persons (excluding already-found orgs)
|
|
114
|
-
const persons = this.extractPersons(text).filter(
|
|
115
|
-
(p) => !orgNames.has(p.name.toLowerCase())
|
|
116
|
-
);
|
|
117
|
-
entities.push(...persons);
|
|
118
|
-
|
|
119
|
-
// First: filter out entities with bad prefixes/suffixes
|
|
120
|
-
const badSuffixes = ["managing", "as", "last", "and", "or", "the", "a", "an", "for", "with"];
|
|
121
|
-
const badPrefixes = ["he", "she", "they", "my", "his", "her", "the", "a", "an", "joined"];
|
|
122
|
-
|
|
123
|
-
const cleanEntities = entities.filter((entity) => {
|
|
124
|
-
const words = entity.name.toLowerCase().split(/\s+/);
|
|
125
|
-
const lastWord = words[words.length - 1];
|
|
126
|
-
const firstWord = words[0];
|
|
127
|
-
if (badSuffixes.includes(lastWord)) return false;
|
|
128
|
-
if (badPrefixes.includes(firstWord)) return false;
|
|
129
|
-
return true;
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
// Deduplicate by name, preferring higher confidence and orgs over persons
|
|
133
|
-
const seen = new Map<string, ExtractedEntity>();
|
|
134
|
-
for (const entity of cleanEntities) {
|
|
135
|
-
const key = entity.name.toLowerCase();
|
|
136
|
-
const existing = seen.get(key);
|
|
137
|
-
if (!existing) {
|
|
138
|
-
seen.set(key, entity);
|
|
139
|
-
} else if (entity.type === "organization" && existing.type === "person") {
|
|
140
|
-
// Prefer org over person
|
|
141
|
-
seen.set(key, entity);
|
|
142
|
-
} else if (entity.confidence > existing.confidence && entity.type === existing.type) {
|
|
143
|
-
seen.set(key, entity);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Remove entities that are proper substrings of other entities with same type
|
|
148
|
-
const result = Array.from(seen.values());
|
|
149
|
-
return result.filter((entity) => {
|
|
150
|
-
const key = entity.name.toLowerCase();
|
|
151
|
-
for (const other of result) {
|
|
152
|
-
const otherKey = other.name.toLowerCase();
|
|
153
|
-
if (otherKey !== key && other.type === entity.type) {
|
|
154
|
-
// If this entity is a prefix of another (longer) entity, keep the shorter one
|
|
155
|
-
// unless the longer one has much higher confidence
|
|
156
|
-
if (otherKey.startsWith(key + " ") && other.confidence > entity.confidence + 0.1) {
|
|
157
|
-
return false;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return true;
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Extract organizations from text
|
|
167
|
-
*/
|
|
168
|
-
extractOrganizations(text: string): ExtractedEntity[] {
|
|
169
|
-
const results: ExtractedEntity[] = [];
|
|
170
|
-
const foundNames = new Set<string>();
|
|
171
|
-
|
|
172
|
-
// Pattern 1: Check for known organizations
|
|
173
|
-
for (const orgName of KNOWN_ORGANIZATIONS) {
|
|
174
|
-
const pattern = new RegExp(`\\b${this.escapeRegex(orgName)}\\b`, "gi");
|
|
175
|
-
let match;
|
|
176
|
-
while ((match = pattern.exec(text)) !== null) {
|
|
177
|
-
const name = match[0];
|
|
178
|
-
const key = name.toLowerCase();
|
|
179
|
-
if (!foundNames.has(key)) {
|
|
180
|
-
foundNames.add(key);
|
|
181
|
-
results.push({
|
|
182
|
-
name,
|
|
183
|
-
type: "organization",
|
|
184
|
-
confidence: 0.95,
|
|
185
|
-
span: { start: match.index, end: match.index + name.length },
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Pattern 2: Capitalized word(s) followed by org suffixes
|
|
192
|
-
// Allow single word + suffix (e.g., "Acme Corporation")
|
|
193
|
-
// Use case-sensitive matching for proper nouns, handle suffix case separately
|
|
194
|
-
const suffixPatternStr = ORG_SUFFIXES.map(s =>
|
|
195
|
-
`${s.charAt(0).toUpperCase()}${s.slice(1)}|${s.toLowerCase()}`
|
|
196
|
-
).join("|");
|
|
197
|
-
const suffixPattern = new RegExp(
|
|
198
|
-
`(?:^|[^A-Za-z])([A-Z][a-z]+(?:\\s+[A-Z][a-z]+)*)\\s+(${suffixPatternStr})(?=\\s|,|\\.|\\)|$)`,
|
|
199
|
-
"g"
|
|
200
|
-
);
|
|
201
|
-
let match;
|
|
202
|
-
while ((match = suffixPattern.exec(text)) !== null) {
|
|
203
|
-
const baseName = match[1].trim();
|
|
204
|
-
const suffix = match[2].trim();
|
|
205
|
-
const fullName = `${baseName} ${suffix}`;
|
|
206
|
-
const key = fullName.toLowerCase();
|
|
207
|
-
|
|
208
|
-
// Skip common adjective+suffix combos
|
|
209
|
-
const firstWord = baseName.split(/\s+/)[0].toLowerCase();
|
|
210
|
-
if (NOT_PERSON_NAMES.has(firstWord)) continue;
|
|
211
|
-
// Skip single words that are not proper nouns
|
|
212
|
-
if (STOPWORDS.has(firstWord)) continue;
|
|
213
|
-
|
|
214
|
-
if (!foundNames.has(key)) {
|
|
215
|
-
foundNames.add(key);
|
|
216
|
-
results.push({
|
|
217
|
-
name: fullName,
|
|
218
|
-
type: "organization",
|
|
219
|
-
confidence: 0.85,
|
|
220
|
-
span: { start: match.index, end: match.index + fullName.length },
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Pattern 3: "works at/for X", "joined X" - only extract multi-word org names
|
|
226
|
-
// Single-word orgs should be in KNOWN_ORGANIZATIONS
|
|
227
|
-
// Use case-sensitive matching for proper nouns (no 'i' flag)
|
|
228
|
-
const workPattern = /(?:works?\s+(?:at|for)|joined|employed\s+(?:at|by)|hired\s+by)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+){1,2})(?=\s+[a-z]|\s*[,.]|\s*$)/g;
|
|
229
|
-
|
|
230
|
-
while ((match = workPattern.exec(text)) !== null) {
|
|
231
|
-
const name = match[1].trim();
|
|
232
|
-
const key = name.toLowerCase();
|
|
233
|
-
const words = name.split(/\s+/);
|
|
234
|
-
|
|
235
|
-
// Skip if first word is a stopword or nationality/religion
|
|
236
|
-
if (STOPWORDS.has(words[0].toLowerCase()) ||
|
|
237
|
-
NOT_PERSON_NAMES.has(words[0].toLowerCase())) {
|
|
238
|
-
continue;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (!foundNames.has(key)) {
|
|
242
|
-
foundNames.add(key);
|
|
243
|
-
results.push({
|
|
244
|
-
name,
|
|
245
|
-
type: "organization",
|
|
246
|
-
confidence: 0.7,
|
|
247
|
-
span: { start: match.index, end: match.index + match[0].length },
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
return results;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Escape special regex characters
|
|
257
|
-
*/
|
|
258
|
-
private escapeRegex(str: string): string {
|
|
259
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Extract person names from text using heuristics
|
|
264
|
-
*/
|
|
265
|
-
extractPersons(text: string): ExtractedEntity[] {
|
|
266
|
-
const persons: ExtractedEntity[] = [];
|
|
267
|
-
|
|
268
|
-
// Pattern 1: Capitalized words (potential names)
|
|
269
|
-
persons.push(...this.extractCapitalizedNames(text));
|
|
270
|
-
|
|
271
|
-
// Pattern 2: Possessive patterns ("X's brother", "my friend X")
|
|
272
|
-
persons.push(...this.extractFromPossessives(text));
|
|
273
|
-
|
|
274
|
-
// Pattern 3: Relation patterns ("her brother", "my mom")
|
|
275
|
-
persons.push(...this.extractFromRelations(text));
|
|
276
|
-
|
|
277
|
-
return persons;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Extract capitalized words that look like names
|
|
282
|
-
*/
|
|
283
|
-
private extractCapitalizedNames(text: string): ExtractedEntity[] {
|
|
284
|
-
const results: ExtractedEntity[] = [];
|
|
285
|
-
|
|
286
|
-
// Match capitalized words not at sentence start
|
|
287
|
-
// This regex finds sequences of capitalized words
|
|
288
|
-
const pattern = /(?<=[.!?]\s+|^)([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)|(?<=[a-z]\s)([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)/g;
|
|
289
|
-
|
|
290
|
-
let match;
|
|
291
|
-
while ((match = pattern.exec(text)) !== null) {
|
|
292
|
-
const name = (match[1] || match[2]).trim();
|
|
293
|
-
const words = name.split(/\s+/);
|
|
294
|
-
|
|
295
|
-
// Filter out stopwords, nationality/religion words, places, and single common words
|
|
296
|
-
const cleanWords = words.filter(
|
|
297
|
-
(w) => !STOPWORDS.has(w.toLowerCase()) &&
|
|
298
|
-
!NOT_PERSON_NAMES.has(w.toLowerCase()) &&
|
|
299
|
-
!KNOWN_PLACES.has(w.toLowerCase()) &&
|
|
300
|
-
w.length > 1
|
|
301
|
-
);
|
|
302
|
-
|
|
303
|
-
if (cleanWords.length === 0) continue;
|
|
304
|
-
|
|
305
|
-
const cleanName = cleanWords.join(" ");
|
|
306
|
-
|
|
307
|
-
// Skip if it's just a common word
|
|
308
|
-
if (cleanWords.length === 1 && cleanWords[0].length < 4) continue;
|
|
309
|
-
|
|
310
|
-
// Higher confidence for multi-word names
|
|
311
|
-
const confidence = cleanWords.length >= 2 ? 0.8 : 0.5;
|
|
312
|
-
|
|
313
|
-
results.push({
|
|
314
|
-
name: cleanName,
|
|
315
|
-
type: "person",
|
|
316
|
-
confidence,
|
|
317
|
-
span: { start: match.index, end: match.index + match[0].length },
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
return results;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Extract names from possessive patterns like "Sarah's brother"
|
|
326
|
-
*/
|
|
327
|
-
private extractFromPossessives(text: string): ExtractedEntity[] {
|
|
328
|
-
const results: ExtractedEntity[] = [];
|
|
329
|
-
|
|
330
|
-
// Match "Name's something"
|
|
331
|
-
const pattern = /([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)'s\s+(\w+)/g;
|
|
332
|
-
|
|
333
|
-
let match;
|
|
334
|
-
while ((match = pattern.exec(text)) !== null) {
|
|
335
|
-
const name = match[1].trim();
|
|
336
|
-
const following = match[2].toLowerCase();
|
|
337
|
-
|
|
338
|
-
// Higher confidence if followed by a relationship word
|
|
339
|
-
const isRelation = RELATION_WORDS.includes(following);
|
|
340
|
-
const confidence = isRelation ? 0.95 : 0.7;
|
|
341
|
-
|
|
342
|
-
if (!STOPWORDS.has(name.toLowerCase())) {
|
|
343
|
-
results.push({
|
|
344
|
-
name,
|
|
345
|
-
type: "person",
|
|
346
|
-
confidence,
|
|
347
|
-
span: { start: match.index, end: match.index + name.length },
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// If followed by relationship word, the whole thing might reference another person
|
|
352
|
-
// e.g., "Sarah's brother" - we create a derived entity
|
|
353
|
-
if (isRelation) {
|
|
354
|
-
results.push({
|
|
355
|
-
name: `${name}'s ${following}`,
|
|
356
|
-
type: "person",
|
|
357
|
-
confidence: 0.6,
|
|
358
|
-
span: { start: match.index, end: match.index + match[0].length },
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
return results;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Extract from relationship patterns like "her brother", "my friend John"
|
|
368
|
-
*/
|
|
369
|
-
private extractFromRelations(text: string): ExtractedEntity[] {
|
|
370
|
-
const results: ExtractedEntity[] = [];
|
|
371
|
-
|
|
372
|
-
// Pattern: possessive + relation word + optional name
|
|
373
|
-
const pronouns = ["my", "his", "her", "their", "our"];
|
|
374
|
-
const relationPattern = new RegExp(
|
|
375
|
-
`(${pronouns.join("|")})\\s+(${RELATION_WORDS.join("|")})(?:\\s+([A-Z][a-z]+))?`,
|
|
376
|
-
"gi"
|
|
377
|
-
);
|
|
378
|
-
|
|
379
|
-
let match;
|
|
380
|
-
while ((match = relationPattern.exec(text)) !== null) {
|
|
381
|
-
const pronoun = match[1];
|
|
382
|
-
const relation = match[2];
|
|
383
|
-
const name = match[3];
|
|
384
|
-
|
|
385
|
-
if (name && !STOPWORDS.has(name.toLowerCase())) {
|
|
386
|
-
// Explicit name mentioned
|
|
387
|
-
results.push({
|
|
388
|
-
name,
|
|
389
|
-
type: "person",
|
|
390
|
-
confidence: 0.9,
|
|
391
|
-
span: {
|
|
392
|
-
start: match.index + match[0].length - name.length,
|
|
393
|
-
end: match.index + match[0].length,
|
|
394
|
-
},
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
return results;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* Extract relationship mentions (not entities, but useful for graph)
|
|
404
|
-
*/
|
|
405
|
-
extractRelationships(text: string): Array<{
|
|
406
|
-
subject: string;
|
|
407
|
-
relation: string;
|
|
408
|
-
object: string;
|
|
409
|
-
confidence: number;
|
|
410
|
-
}> {
|
|
411
|
-
const relationships: Array<{
|
|
412
|
-
subject: string;
|
|
413
|
-
relation: string;
|
|
414
|
-
object: string;
|
|
415
|
-
confidence: number;
|
|
416
|
-
}> = [];
|
|
417
|
-
|
|
418
|
-
// Pattern: "X's [relation]" implies relationship
|
|
419
|
-
const possessivePattern = /([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)'s\s+(\w+)/g;
|
|
420
|
-
|
|
421
|
-
let match;
|
|
422
|
-
while ((match = possessivePattern.exec(text)) !== null) {
|
|
423
|
-
const subject = match[1].trim();
|
|
424
|
-
const relWord = match[2].toLowerCase();
|
|
425
|
-
|
|
426
|
-
if (RELATION_WORDS.includes(relWord)) {
|
|
427
|
-
relationships.push({
|
|
428
|
-
subject,
|
|
429
|
-
relation: relWord,
|
|
430
|
-
object: `${subject}'s ${relWord}`, // placeholder name
|
|
431
|
-
confidence: 0.7,
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
return relationships;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// Singleton instance
|
|
441
|
-
export const entityExtractor = new EntityExtractor();
|