@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.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Chat Handler for Engram Web Interface
3
- * Uses Claude Haiku 4.5 with tools for entity/memory management
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: "List all entities in the knowledge graph. Use this to see what people, organizations, and places are stored.",
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 by entity type (optional)",
23
+ description: "Filter: person, organization, or place",
24
24
  },
25
25
  limit: {
26
- type: "number",
27
- description: "Maximum number of entities to return (default: 50)",
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: "Get detailed information about an entity including its observations and relationships.",
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: "The entity name to look up",
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: "Delete an entity and all its observations and relationships. Use this to remove incorrect or duplicate entities.",
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: "The entity name to delete",
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: "Merge one entity into another. All observations and relationships from the source will be moved to the target, then the source is deleted. Use this to fix duplicates.",
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: "The entity name to keep (target)",
69
+ description: "Entity name to preserve (target)",
70
70
  },
71
71
  merge: {
72
72
  type: "string",
73
- description: "The entity name to merge and delete (source)",
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: "Rename an entity to a new name.",
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 entity name",
87
+ description: "Current exact name",
88
88
  },
89
89
  new_name: {
90
90
  type: "string",
91
- description: "New entity name",
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: "Delete a relationship between two entities.",
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., 'works_at', 'knows')",
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: "Search through stored memories.",
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: "number",
131
- description: "Maximum results (default: 10)",
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: "Delete a memory by its ID (soft-delete, can be recovered).",
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: "The memory ID to delete",
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: "Edit an existing memory's content or importance.",
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: "The memory ID to edit",
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
- description: "New importance (0-1): 0.9=core identity, 0.8=major, 0.5=normal, 0.3=minor",
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: "Create a new memory. Use for storing user information, preferences, or facts.",
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: "The information to store",
183
+ description: "Information to store",
182
184
  },
183
185
  importance: {
184
186
  type: "number",
185
- description: "0-1 score: 0.9=core identity, 0.8=major, 0.5=normal (default), 0.3=minor",
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: "Create a new entity (person, organization, or place).",
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: "The entity name",
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: "Create a relationship between two entities.",
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., 'works_at', 'lives_in', 'knows', 'sibling_of')",
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: "Find potential duplicate entities that could be merged.",
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: "Automatically merge all detected duplicate entities.",
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-haiku-4-5-20241022",
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
- if (currentToolUse) {
412
- // Execute the tool
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 and continue
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-haiku-4-5-20241022",
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-haiku-4-5-20241022",
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 results = await this.search.search(query, { limit });
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 results = await this.search.search(query, { limit });
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,