@199-bio/engram 0.7.4 → 0.8.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/consolidation/consolidator.d.ts.map +1 -1
- package/dist/index.js +236 -4
- package/dist/retrieval/hybrid.d.ts.map +1 -1
- package/dist/storage/database.d.ts.map +1 -1
- package/dist/web/chat-handler.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/consolidation/consolidator.ts +50 -10
- package/src/index.ts +260 -4
- package/src/retrieval/hybrid.ts +156 -15
- package/src/storage/database.ts +325 -0
- package/src/web/chat-handler.ts +61 -58
- package/src/web/server.ts +2 -2
package/src/web/chat-handler.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Chat Handler for Engram Web Interface
|
|
3
|
-
* Uses Claude
|
|
3
|
+
* Uses Claude Opus 4.5 with tools for entity/memory management
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import Anthropic from "@anthropic-ai/sdk";
|
|
@@ -9,22 +9,22 @@ import { KnowledgeGraph } from "../graph/knowledge-graph.js";
|
|
|
9
9
|
import { HybridSearch } from "../retrieval/hybrid.js";
|
|
10
10
|
import { getAnthropicApiKey } from "../settings.js";
|
|
11
11
|
|
|
12
|
-
// Tool definitions for Claude
|
|
12
|
+
// Tool definitions for Claude - optimized for LLM consumption
|
|
13
13
|
const TOOLS: Anthropic.Tool[] = [
|
|
14
14
|
{
|
|
15
15
|
name: "list_entities",
|
|
16
|
-
description: "
|
|
16
|
+
description: "Returns array of {name, type, id}. Filters: type (person|organization|place), limit. Default limit=50.",
|
|
17
17
|
input_schema: {
|
|
18
18
|
type: "object" as const,
|
|
19
19
|
properties: {
|
|
20
20
|
type: {
|
|
21
21
|
type: "string",
|
|
22
22
|
enum: ["person", "organization", "place"],
|
|
23
|
-
description: "Filter
|
|
23
|
+
description: "Filter: person, organization, or place",
|
|
24
24
|
},
|
|
25
25
|
limit: {
|
|
26
|
-
type: "
|
|
27
|
-
description: "
|
|
26
|
+
type: "integer",
|
|
27
|
+
description: "Max results (default: 50)",
|
|
28
28
|
},
|
|
29
29
|
},
|
|
30
30
|
required: [],
|
|
@@ -32,13 +32,13 @@ const TOOLS: Anthropic.Tool[] = [
|
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
name: "get_entity",
|
|
35
|
-
description: "
|
|
35
|
+
description: "Returns {name, type, observations[], relationships_from[], relationships_to[]} or {error}.",
|
|
36
36
|
input_schema: {
|
|
37
37
|
type: "object" as const,
|
|
38
38
|
properties: {
|
|
39
39
|
name: {
|
|
40
40
|
type: "string",
|
|
41
|
-
description: "
|
|
41
|
+
description: "Exact entity name (case-sensitive)",
|
|
42
42
|
},
|
|
43
43
|
},
|
|
44
44
|
required: ["name"],
|
|
@@ -46,13 +46,13 @@ const TOOLS: Anthropic.Tool[] = [
|
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
name: "delete_entity",
|
|
49
|
-
description: "
|
|
49
|
+
description: "Permanently removes entity + all observations + all relationships. Returns {success, deleted} or {error}.",
|
|
50
50
|
input_schema: {
|
|
51
51
|
type: "object" as const,
|
|
52
52
|
properties: {
|
|
53
53
|
name: {
|
|
54
54
|
type: "string",
|
|
55
|
-
description: "
|
|
55
|
+
description: "Exact entity name to delete",
|
|
56
56
|
},
|
|
57
57
|
},
|
|
58
58
|
required: ["name"],
|
|
@@ -60,17 +60,17 @@ const TOOLS: Anthropic.Tool[] = [
|
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
62
|
name: "merge_entities",
|
|
63
|
-
description: "
|
|
63
|
+
description: "Moves all data from 'merge' into 'keep', then deletes 'merge'. Use for deduplication. Returns {success, kept, merged, observations_moved, relations_moved} or {error}.",
|
|
64
64
|
input_schema: {
|
|
65
65
|
type: "object" as const,
|
|
66
66
|
properties: {
|
|
67
67
|
keep: {
|
|
68
68
|
type: "string",
|
|
69
|
-
description: "
|
|
69
|
+
description: "Entity name to preserve (target)",
|
|
70
70
|
},
|
|
71
71
|
merge: {
|
|
72
72
|
type: "string",
|
|
73
|
-
description: "
|
|
73
|
+
description: "Entity name to merge then delete (source)",
|
|
74
74
|
},
|
|
75
75
|
},
|
|
76
76
|
required: ["keep", "merge"],
|
|
@@ -78,17 +78,17 @@ const TOOLS: Anthropic.Tool[] = [
|
|
|
78
78
|
},
|
|
79
79
|
{
|
|
80
80
|
name: "rename_entity",
|
|
81
|
-
description: "
|
|
81
|
+
description: "Changes entity name. Returns {success, old_name, new_name} or {error}.",
|
|
82
82
|
input_schema: {
|
|
83
83
|
type: "object" as const,
|
|
84
84
|
properties: {
|
|
85
85
|
old_name: {
|
|
86
86
|
type: "string",
|
|
87
|
-
description: "Current
|
|
87
|
+
description: "Current exact name",
|
|
88
88
|
},
|
|
89
89
|
new_name: {
|
|
90
90
|
type: "string",
|
|
91
|
-
description: "New
|
|
91
|
+
description: "New name",
|
|
92
92
|
},
|
|
93
93
|
},
|
|
94
94
|
required: ["old_name", "new_name"],
|
|
@@ -96,7 +96,7 @@ const TOOLS: Anthropic.Tool[] = [
|
|
|
96
96
|
},
|
|
97
97
|
{
|
|
98
98
|
name: "delete_relationship",
|
|
99
|
-
description: "
|
|
99
|
+
description: "Removes specific relationship. All 3 params must match exactly. Returns {success, deleted} or {error}.",
|
|
100
100
|
input_schema: {
|
|
101
101
|
type: "object" as const,
|
|
102
102
|
properties: {
|
|
@@ -110,7 +110,7 @@ const TOOLS: Anthropic.Tool[] = [
|
|
|
110
110
|
},
|
|
111
111
|
type: {
|
|
112
112
|
type: "string",
|
|
113
|
-
description: "Relationship type (e.g.,
|
|
113
|
+
description: "Relationship type (e.g., works_at, knows, lives_in)",
|
|
114
114
|
},
|
|
115
115
|
},
|
|
116
116
|
required: ["from", "to", "type"],
|
|
@@ -118,17 +118,17 @@ const TOOLS: Anthropic.Tool[] = [
|
|
|
118
118
|
},
|
|
119
119
|
{
|
|
120
120
|
name: "search_memories",
|
|
121
|
-
description: "
|
|
121
|
+
description: "Hybrid BM25+semantic search. Returns {results[{id, content, timestamp, score}], count}.",
|
|
122
122
|
input_schema: {
|
|
123
123
|
type: "object" as const,
|
|
124
124
|
properties: {
|
|
125
125
|
query: {
|
|
126
126
|
type: "string",
|
|
127
|
-
description: "Search query",
|
|
127
|
+
description: "Search query (keywords or natural language)",
|
|
128
128
|
},
|
|
129
129
|
limit: {
|
|
130
|
-
type: "
|
|
131
|
-
description: "
|
|
130
|
+
type: "integer",
|
|
131
|
+
description: "Max results (default: 10)",
|
|
132
132
|
},
|
|
133
133
|
},
|
|
134
134
|
required: ["query"],
|
|
@@ -136,13 +136,13 @@ const TOOLS: Anthropic.Tool[] = [
|
|
|
136
136
|
},
|
|
137
137
|
{
|
|
138
138
|
name: "delete_memory",
|
|
139
|
-
description: "
|
|
139
|
+
description: "Soft-delete (recoverable). Returns {success, disabled_id} or {error}.",
|
|
140
140
|
input_schema: {
|
|
141
141
|
type: "object" as const,
|
|
142
142
|
properties: {
|
|
143
143
|
id: {
|
|
144
144
|
type: "string",
|
|
145
|
-
description: "
|
|
145
|
+
description: "Memory UUID",
|
|
146
146
|
},
|
|
147
147
|
},
|
|
148
148
|
required: ["id"],
|
|
@@ -150,13 +150,13 @@ const TOOLS: Anthropic.Tool[] = [
|
|
|
150
150
|
},
|
|
151
151
|
{
|
|
152
152
|
name: "edit_memory",
|
|
153
|
-
description: "
|
|
153
|
+
description: "Updates content and/or importance. Returns {success, memory_id, updated_fields[]} or {error}.",
|
|
154
154
|
input_schema: {
|
|
155
155
|
type: "object" as const,
|
|
156
156
|
properties: {
|
|
157
157
|
id: {
|
|
158
158
|
type: "string",
|
|
159
|
-
description: "
|
|
159
|
+
description: "Memory UUID",
|
|
160
160
|
},
|
|
161
161
|
content: {
|
|
162
162
|
type: "string",
|
|
@@ -164,7 +164,9 @@ const TOOLS: Anthropic.Tool[] = [
|
|
|
164
164
|
},
|
|
165
165
|
importance: {
|
|
166
166
|
type: "number",
|
|
167
|
-
|
|
167
|
+
minimum: 0,
|
|
168
|
+
maximum: 1,
|
|
169
|
+
description: "0-1: 0.9=identity, 0.8=major, 0.5=normal, 0.3=minor",
|
|
168
170
|
},
|
|
169
171
|
},
|
|
170
172
|
required: ["id"],
|
|
@@ -172,17 +174,19 @@ const TOOLS: Anthropic.Tool[] = [
|
|
|
172
174
|
},
|
|
173
175
|
{
|
|
174
176
|
name: "create_memory",
|
|
175
|
-
description: "
|
|
177
|
+
description: "Stores new memory. Returns {success, memory_id, content}.",
|
|
176
178
|
input_schema: {
|
|
177
179
|
type: "object" as const,
|
|
178
180
|
properties: {
|
|
179
181
|
content: {
|
|
180
182
|
type: "string",
|
|
181
|
-
description: "
|
|
183
|
+
description: "Information to store",
|
|
182
184
|
},
|
|
183
185
|
importance: {
|
|
184
186
|
type: "number",
|
|
185
|
-
|
|
187
|
+
minimum: 0,
|
|
188
|
+
maximum: 1,
|
|
189
|
+
description: "0-1: 0.9=identity, 0.8=major, 0.5=normal (default), 0.3=minor",
|
|
186
190
|
},
|
|
187
191
|
},
|
|
188
192
|
required: ["content"],
|
|
@@ -190,13 +194,13 @@ const TOOLS: Anthropic.Tool[] = [
|
|
|
190
194
|
},
|
|
191
195
|
{
|
|
192
196
|
name: "create_entity",
|
|
193
|
-
description: "
|
|
197
|
+
description: "Creates new entity. Returns {success, entity_id, name, type} or {error} if exists.",
|
|
194
198
|
input_schema: {
|
|
195
199
|
type: "object" as const,
|
|
196
200
|
properties: {
|
|
197
201
|
name: {
|
|
198
202
|
type: "string",
|
|
199
|
-
description: "
|
|
203
|
+
description: "Entity name",
|
|
200
204
|
},
|
|
201
205
|
type: {
|
|
202
206
|
type: "string",
|
|
@@ -209,7 +213,7 @@ const TOOLS: Anthropic.Tool[] = [
|
|
|
209
213
|
},
|
|
210
214
|
{
|
|
211
215
|
name: "create_relationship",
|
|
212
|
-
description: "
|
|
216
|
+
description: "Links two entities. Auto-creates entities as 'person' if missing. Returns {success, relationship}.",
|
|
213
217
|
input_schema: {
|
|
214
218
|
type: "object" as const,
|
|
215
219
|
properties: {
|
|
@@ -223,7 +227,7 @@ const TOOLS: Anthropic.Tool[] = [
|
|
|
223
227
|
},
|
|
224
228
|
type: {
|
|
225
229
|
type: "string",
|
|
226
|
-
description: "Relationship type (e.g.,
|
|
230
|
+
description: "Relationship type (e.g., works_at, lives_in, knows, sibling_of, parent_of)",
|
|
227
231
|
},
|
|
228
232
|
},
|
|
229
233
|
required: ["from", "to", "type"],
|
|
@@ -231,7 +235,7 @@ const TOOLS: Anthropic.Tool[] = [
|
|
|
231
235
|
},
|
|
232
236
|
{
|
|
233
237
|
name: "find_duplicates",
|
|
234
|
-
description: "
|
|
238
|
+
description: "Detects similar entity names. Returns {groups[{keep, duplicates[]}], total_duplicates}.",
|
|
235
239
|
input_schema: {
|
|
236
240
|
type: "object" as const,
|
|
237
241
|
properties: {},
|
|
@@ -240,7 +244,7 @@ const TOOLS: Anthropic.Tool[] = [
|
|
|
240
244
|
},
|
|
241
245
|
{
|
|
242
246
|
name: "auto_tidy",
|
|
243
|
-
description: "
|
|
247
|
+
description: "Auto-merges all detected duplicates. Returns {entities_merged, observations_moved, relations_moved}.",
|
|
244
248
|
input_schema: {
|
|
245
249
|
type: "object" as const,
|
|
246
250
|
properties: {},
|
|
@@ -301,6 +305,9 @@ export class ChatHandler {
|
|
|
301
305
|
refreshClient(): void {
|
|
302
306
|
const apiKey = getAnthropicApiKey();
|
|
303
307
|
if (apiKey) {
|
|
308
|
+
if (!this.client) {
|
|
309
|
+
console.error("[Engram] ChatHandler: API key configured");
|
|
310
|
+
}
|
|
304
311
|
this.client = new Anthropic({ apiKey });
|
|
305
312
|
} else {
|
|
306
313
|
this.client = null;
|
|
@@ -308,6 +315,8 @@ export class ChatHandler {
|
|
|
308
315
|
}
|
|
309
316
|
|
|
310
317
|
isConfigured(): boolean {
|
|
318
|
+
// Re-check API key in case it was added after startup
|
|
319
|
+
this.refreshClient();
|
|
311
320
|
return this.client !== null;
|
|
312
321
|
}
|
|
313
322
|
|
|
@@ -341,6 +350,7 @@ export class ChatHandler {
|
|
|
341
350
|
|
|
342
351
|
// Queue-aware chat method
|
|
343
352
|
async chat(userMessage: string): Promise<string> {
|
|
353
|
+
this.refreshClient();
|
|
344
354
|
if (!this.client) {
|
|
345
355
|
return "Chat is not configured. Set ANTHROPIC_API_KEY environment variable.";
|
|
346
356
|
}
|
|
@@ -357,6 +367,7 @@ export class ChatHandler {
|
|
|
357
367
|
|
|
358
368
|
// Streaming chat with callbacks for real-time updates
|
|
359
369
|
async *chatStream(userMessage: string): AsyncGenerator<StreamEvent> {
|
|
370
|
+
this.refreshClient();
|
|
360
371
|
if (!this.client) {
|
|
361
372
|
yield { type: "error", content: "Chat is not configured. Set ANTHROPIC_API_KEY environment variable." };
|
|
362
373
|
return;
|
|
@@ -377,11 +388,10 @@ export class ChatHandler {
|
|
|
377
388
|
}
|
|
378
389
|
|
|
379
390
|
let continueLoop = true;
|
|
380
|
-
let fullResponse = "";
|
|
381
391
|
|
|
382
392
|
while (continueLoop) {
|
|
383
393
|
const stream = this.client.messages.stream({
|
|
384
|
-
model: "claude-
|
|
394
|
+
model: "claude-opus-4-5-20251101",
|
|
385
395
|
max_tokens: 1024,
|
|
386
396
|
system: SYSTEM_PROMPT,
|
|
387
397
|
tools: TOOLS,
|
|
@@ -402,25 +412,13 @@ export class ChatHandler {
|
|
|
402
412
|
}
|
|
403
413
|
} else if (event.type === "content_block_delta") {
|
|
404
414
|
if (event.delta.type === "text_delta") {
|
|
405
|
-
fullResponse += event.delta.text;
|
|
406
415
|
yield { type: "text", content: event.delta.text };
|
|
407
416
|
} else if (event.delta.type === "input_json_delta" && currentToolUse) {
|
|
408
417
|
currentToolUse.input += event.delta.partial_json;
|
|
409
418
|
}
|
|
410
419
|
} else if (event.type === "content_block_stop") {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
let toolInput: Record<string, unknown> = {};
|
|
414
|
-
try {
|
|
415
|
-
toolInput = JSON.parse(currentToolUse.input || "{}");
|
|
416
|
-
} catch {
|
|
417
|
-
toolInput = {};
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const result = await this.executeTool(currentToolUse.name, toolInput);
|
|
421
|
-
yield { type: "tool_end", tool: currentToolUse.name, result };
|
|
422
|
-
currentToolUse = null;
|
|
423
|
-
}
|
|
420
|
+
// Don't execute tools here - wait for finalMessage to avoid double execution
|
|
421
|
+
currentToolUse = null;
|
|
424
422
|
}
|
|
425
423
|
}
|
|
426
424
|
|
|
@@ -428,7 +426,7 @@ export class ChatHandler {
|
|
|
428
426
|
const finalMessage = await stream.finalMessage();
|
|
429
427
|
|
|
430
428
|
if (finalMessage.stop_reason === "tool_use") {
|
|
431
|
-
// Process tool results
|
|
429
|
+
// Process tool results (execute only once, here)
|
|
432
430
|
const toolUseBlocks = finalMessage.content.filter(
|
|
433
431
|
(block): block is Anthropic.ToolUseBlock => block.type === "tool_use"
|
|
434
432
|
);
|
|
@@ -436,10 +434,13 @@ export class ChatHandler {
|
|
|
436
434
|
const toolResults: Anthropic.ToolResultBlockParam[] = [];
|
|
437
435
|
for (const toolUse of toolUseBlocks) {
|
|
438
436
|
const result = await this.executeTool(toolUse.name, toolUse.input as Record<string, unknown>);
|
|
437
|
+
const isError = typeof result === "object" && result !== null && "error" in result;
|
|
438
|
+
yield { type: "tool_end", tool: toolUse.name, result };
|
|
439
439
|
toolResults.push({
|
|
440
440
|
type: "tool_result",
|
|
441
441
|
tool_use_id: toolUse.id,
|
|
442
442
|
content: JSON.stringify(result),
|
|
443
|
+
is_error: isError,
|
|
443
444
|
});
|
|
444
445
|
}
|
|
445
446
|
|
|
@@ -492,7 +493,7 @@ export class ChatHandler {
|
|
|
492
493
|
}
|
|
493
494
|
|
|
494
495
|
let response = await this.client.messages.create({
|
|
495
|
-
model: "claude-
|
|
496
|
+
model: "claude-opus-4-5-20251101",
|
|
496
497
|
max_tokens: 1024,
|
|
497
498
|
system: SYSTEM_PROMPT,
|
|
498
499
|
tools: TOOLS,
|
|
@@ -508,10 +509,12 @@ export class ChatHandler {
|
|
|
508
509
|
const toolResults: Anthropic.ToolResultBlockParam[] = [];
|
|
509
510
|
for (const toolUse of toolUseBlocks) {
|
|
510
511
|
const result = await this.executeTool(toolUse.name, toolUse.input as Record<string, unknown>);
|
|
512
|
+
const isError = typeof result === "object" && result !== null && "error" in result;
|
|
511
513
|
toolResults.push({
|
|
512
514
|
type: "tool_result",
|
|
513
515
|
tool_use_id: toolUse.id,
|
|
514
516
|
content: JSON.stringify(result),
|
|
517
|
+
is_error: isError,
|
|
515
518
|
});
|
|
516
519
|
}
|
|
517
520
|
|
|
@@ -527,7 +530,7 @@ export class ChatHandler {
|
|
|
527
530
|
|
|
528
531
|
// Continue the conversation
|
|
529
532
|
response = await this.client.messages.create({
|
|
530
|
-
model: "claude-
|
|
533
|
+
model: "claude-opus-4-5-20251101",
|
|
531
534
|
max_tokens: 1024,
|
|
532
535
|
system: SYSTEM_PROMPT,
|
|
533
536
|
tools: TOOLS,
|
|
@@ -669,15 +672,15 @@ export class ChatHandler {
|
|
|
669
672
|
const query = input.query as string;
|
|
670
673
|
const limit = (input.limit as number) || 10;
|
|
671
674
|
|
|
672
|
-
const
|
|
675
|
+
const response = await this.search.search(query, { limit });
|
|
673
676
|
return {
|
|
674
|
-
results: results.map((r) => ({
|
|
677
|
+
results: response.results.map((r) => ({
|
|
675
678
|
id: r.memory.id,
|
|
676
679
|
content: r.memory.content.substring(0, 300) + (r.memory.content.length > 300 ? "..." : ""),
|
|
677
680
|
timestamp: r.memory.timestamp.toISOString(),
|
|
678
681
|
score: r.score.toFixed(3),
|
|
679
682
|
})),
|
|
680
|
-
count: results.length,
|
|
683
|
+
count: response.results.length,
|
|
681
684
|
};
|
|
682
685
|
}
|
|
683
686
|
|
package/src/web/server.ts
CHANGED
|
@@ -241,9 +241,9 @@ export class EngramWebServer {
|
|
|
241
241
|
const offset = parseInt(url.searchParams.get("offset") || "0");
|
|
242
242
|
|
|
243
243
|
if (query) {
|
|
244
|
-
const
|
|
244
|
+
const response = await this.search.search(query, { limit });
|
|
245
245
|
res.end(JSON.stringify({
|
|
246
|
-
memories: results.map(r => ({
|
|
246
|
+
memories: response.results.map(r => ({
|
|
247
247
|
...r.memory,
|
|
248
248
|
score: r.score,
|
|
249
249
|
sources: r.sources,
|