@199-bio/engram 0.7.0 → 0.7.2
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/index.js +29 -2
- package/dist/storage/database.d.ts.map +1 -1
- package/dist/web/chat-handler.d.ts.map +1 -1
- package/dist/web/server.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +30 -2
- package/src/storage/database.ts +4 -4
- package/src/web/chat-handler.ts +337 -3
- package/src/web/server.ts +166 -9
- package/src/web/static/app.js +201 -32
- package/src/web/static/index.html +4 -0
- package/src/web/static/style.css +147 -0
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import { EngramDatabase } from "./storage/database.js";
|
|
|
14
14
|
import { KnowledgeGraph } from "./graph/knowledge-graph.js";
|
|
15
15
|
import { createRetriever } from "./retrieval/colbert.js";
|
|
16
16
|
import { HybridSearch } from "./retrieval/hybrid.js";
|
|
17
|
-
import { EngramWebServer } from "./web/server.js";
|
|
17
|
+
import { EngramWebServer, getRunningServerUrl } from "./web/server.js";
|
|
18
18
|
import { Consolidator } from "./consolidation/consolidator.js";
|
|
19
19
|
// ============ Configuration ============
|
|
20
20
|
const DB_PATH = process.env.ENGRAM_DB_PATH
|
|
@@ -44,11 +44,21 @@ async function initialize() {
|
|
|
44
44
|
if (consolidator.isConfigured()) {
|
|
45
45
|
console.error(`[Engram] Consolidation enabled (ANTHROPIC_API_KEY found)`);
|
|
46
46
|
}
|
|
47
|
+
// Start web server automatically (unless another instance is already running)
|
|
48
|
+
const existingUrl = getRunningServerUrl();
|
|
49
|
+
if (!existingUrl) {
|
|
50
|
+
webServer = new EngramWebServer({ db, graph, search });
|
|
51
|
+
const url = await webServer.start();
|
|
52
|
+
console.error(`[Engram] Web interface: ${url}`);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
console.error(`[Engram] Web interface already running: ${existingUrl}`);
|
|
56
|
+
}
|
|
47
57
|
}
|
|
48
58
|
// ============ MCP Server ============
|
|
49
59
|
const server = new Server({
|
|
50
60
|
name: "engram",
|
|
51
|
-
version: "0.
|
|
61
|
+
version: "0.7.2",
|
|
52
62
|
}, {
|
|
53
63
|
capabilities: {
|
|
54
64
|
tools: {},
|
|
@@ -410,6 +420,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
410
420
|
}
|
|
411
421
|
case "engram_web": {
|
|
412
422
|
const { port = 3847 } = args;
|
|
423
|
+
// Check if a server is already running (from any MCP instance)
|
|
424
|
+
const existingUrl = getRunningServerUrl();
|
|
425
|
+
if (existingUrl) {
|
|
426
|
+
return {
|
|
427
|
+
content: [
|
|
428
|
+
{
|
|
429
|
+
type: "text",
|
|
430
|
+
text: JSON.stringify({
|
|
431
|
+
success: true,
|
|
432
|
+
url: existingUrl,
|
|
433
|
+
message: `Web interface already running at ${existingUrl}`,
|
|
434
|
+
reused: true,
|
|
435
|
+
}, null, 2),
|
|
436
|
+
},
|
|
437
|
+
],
|
|
438
|
+
};
|
|
439
|
+
}
|
|
413
440
|
// Create or reuse web server
|
|
414
441
|
if (!webServer) {
|
|
415
442
|
webServer = new EngramWebServer({ db, graph, search, port });
|
|
@@ -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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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-handler.d.ts","sourceRoot":"","sources":["../../src/web/chat-handler.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,cAAc,EAAkB,MAAM,wBAAwB,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"chat-handler.d.ts","sourceRoot":"","sources":["../../src/web/chat-handler.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,cAAc,EAAkB,MAAM,wBAAwB,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAmQtD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,UAAU,GAAG,OAAO,GAAG,MAAM,CAAC;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,EAAE,CAAiB;IAC3B,OAAO,CAAC,KAAK,CAAiB;IAC9B,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,mBAAmB,CAAgC;IAC3D,OAAO,CAAC,YAAY,CAAkB;IACtC,OAAO,CAAC,YAAY,CAAoG;gBAE5G,OAAO,EAAE;QACnB,EAAE,EAAE,cAAc,CAAC;QACnB,KAAK,EAAE,cAAc,CAAC;QACtB,MAAM,EAAE,YAAY,CAAC;KACtB;IAWD,YAAY,IAAI,OAAO;IAIvB,MAAM,IAAI,OAAO;IAIjB,cAAc,IAAI,MAAM;IAIxB,YAAY,IAAI,IAAI;YAKN,YAAY;IAgBpB,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAgBzC,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,cAAc,CAAC,WAAW,CAAC;YAsHrD,cAAc;YAgFd,WAAW;CAoQ1B"}
|
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;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/web/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAwBtD;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,GAAG,IAAI,CAmBnD;AAED,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,CAAc;IAC1B,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,IAAI,CAAS;gBAET,OAAO,EAAE,gBAAgB;IAa/B,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAwD9B,OAAO,CAAC,aAAa;IAkBrB,OAAO,CAAC,cAAc;IActB,IAAI,IAAI,IAAI;YAQE,aAAa;YA+Bb,SAAS;YAoWT,WAAW;IAgCzB,OAAO,CAAC,SAAS;CAclB"}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { EngramDatabase } from "./storage/database.js";
|
|
|
19
19
|
import { KnowledgeGraph } from "./graph/knowledge-graph.js";
|
|
20
20
|
import { createRetriever } from "./retrieval/colbert.js";
|
|
21
21
|
import { HybridSearch } from "./retrieval/hybrid.js";
|
|
22
|
-
import { EngramWebServer } from "./web/server.js";
|
|
22
|
+
import { EngramWebServer, getRunningServerUrl } from "./web/server.js";
|
|
23
23
|
import { Consolidator } from "./consolidation/consolidator.js";
|
|
24
24
|
|
|
25
25
|
// ============ Configuration ============
|
|
@@ -59,6 +59,16 @@ async function initialize(): Promise<void> {
|
|
|
59
59
|
if (consolidator.isConfigured()) {
|
|
60
60
|
console.error(`[Engram] Consolidation enabled (ANTHROPIC_API_KEY found)`);
|
|
61
61
|
}
|
|
62
|
+
|
|
63
|
+
// Start web server automatically (unless another instance is already running)
|
|
64
|
+
const existingUrl = getRunningServerUrl();
|
|
65
|
+
if (!existingUrl) {
|
|
66
|
+
webServer = new EngramWebServer({ db, graph, search });
|
|
67
|
+
const url = await webServer.start();
|
|
68
|
+
console.error(`[Engram] Web interface: ${url}`);
|
|
69
|
+
} else {
|
|
70
|
+
console.error(`[Engram] Web interface already running: ${existingUrl}`);
|
|
71
|
+
}
|
|
62
72
|
}
|
|
63
73
|
|
|
64
74
|
// ============ MCP Server ============
|
|
@@ -66,7 +76,7 @@ async function initialize(): Promise<void> {
|
|
|
66
76
|
const server = new Server(
|
|
67
77
|
{
|
|
68
78
|
name: "engram",
|
|
69
|
-
version: "0.
|
|
79
|
+
version: "0.7.2",
|
|
70
80
|
},
|
|
71
81
|
{
|
|
72
82
|
capabilities: {
|
|
@@ -480,6 +490,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
480
490
|
case "engram_web": {
|
|
481
491
|
const { port = 3847 } = args as { port?: number };
|
|
482
492
|
|
|
493
|
+
// Check if a server is already running (from any MCP instance)
|
|
494
|
+
const existingUrl = getRunningServerUrl();
|
|
495
|
+
if (existingUrl) {
|
|
496
|
+
return {
|
|
497
|
+
content: [
|
|
498
|
+
{
|
|
499
|
+
type: "text" as const,
|
|
500
|
+
text: JSON.stringify({
|
|
501
|
+
success: true,
|
|
502
|
+
url: existingUrl,
|
|
503
|
+
message: `Web interface already running at ${existingUrl}`,
|
|
504
|
+
reused: true,
|
|
505
|
+
}, null, 2),
|
|
506
|
+
},
|
|
507
|
+
],
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
483
511
|
// Create or reuse web server
|
|
484
512
|
if (!webServer) {
|
|
485
513
|
webServer = new EngramWebServer({ db, graph, search, port });
|
package/src/storage/database.ts
CHANGED
|
@@ -372,11 +372,11 @@ export class EngramDatabase {
|
|
|
372
372
|
`).run(id);
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
-
getAllMemories(limit: number = 1000, includeDisabled: boolean = false): Memory[] {
|
|
375
|
+
getAllMemories(limit: number = 1000, includeDisabled: boolean = false, offset: number = 0): Memory[] {
|
|
376
376
|
const sql = includeDisabled
|
|
377
|
-
? "SELECT * FROM memories ORDER BY timestamp DESC LIMIT ?"
|
|
378
|
-
: "SELECT * FROM memories WHERE disabled = 0 ORDER BY timestamp DESC LIMIT ?";
|
|
379
|
-
const rows = this.stmt(sql).all(limit) as Record<string, unknown>[];
|
|
377
|
+
? "SELECT * FROM memories ORDER BY timestamp DESC LIMIT ? OFFSET ?"
|
|
378
|
+
: "SELECT * FROM memories WHERE disabled = 0 ORDER BY timestamp DESC LIMIT ? OFFSET ?";
|
|
379
|
+
const rows = this.stmt(sql).all(limit, offset) as Record<string, unknown>[];
|
|
380
380
|
return rows.map((row) => this.rowToMemory(row));
|
|
381
381
|
}
|
|
382
382
|
|
package/src/web/chat-handler.ts
CHANGED
|
@@ -135,7 +135,7 @@ const TOOLS: Anthropic.Tool[] = [
|
|
|
135
135
|
},
|
|
136
136
|
{
|
|
137
137
|
name: "delete_memory",
|
|
138
|
-
description: "Delete a memory by its ID.",
|
|
138
|
+
description: "Delete a memory by its ID (soft-delete, can be recovered).",
|
|
139
139
|
input_schema: {
|
|
140
140
|
type: "object" as const,
|
|
141
141
|
properties: {
|
|
@@ -147,6 +147,87 @@ const TOOLS: Anthropic.Tool[] = [
|
|
|
147
147
|
required: ["id"],
|
|
148
148
|
},
|
|
149
149
|
},
|
|
150
|
+
{
|
|
151
|
+
name: "edit_memory",
|
|
152
|
+
description: "Edit an existing memory's content or importance.",
|
|
153
|
+
input_schema: {
|
|
154
|
+
type: "object" as const,
|
|
155
|
+
properties: {
|
|
156
|
+
id: {
|
|
157
|
+
type: "string",
|
|
158
|
+
description: "The memory ID to edit",
|
|
159
|
+
},
|
|
160
|
+
content: {
|
|
161
|
+
type: "string",
|
|
162
|
+
description: "New content (replaces existing)",
|
|
163
|
+
},
|
|
164
|
+
importance: {
|
|
165
|
+
type: "number",
|
|
166
|
+
description: "New importance (0-1): 0.9=core identity, 0.8=major, 0.5=normal, 0.3=minor",
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
required: ["id"],
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: "create_memory",
|
|
174
|
+
description: "Create a new memory. Use for storing user information, preferences, or facts.",
|
|
175
|
+
input_schema: {
|
|
176
|
+
type: "object" as const,
|
|
177
|
+
properties: {
|
|
178
|
+
content: {
|
|
179
|
+
type: "string",
|
|
180
|
+
description: "The information to store",
|
|
181
|
+
},
|
|
182
|
+
importance: {
|
|
183
|
+
type: "number",
|
|
184
|
+
description: "0-1 score: 0.9=core identity, 0.8=major, 0.5=normal (default), 0.3=minor",
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
required: ["content"],
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: "create_entity",
|
|
192
|
+
description: "Create a new entity (person, organization, or place).",
|
|
193
|
+
input_schema: {
|
|
194
|
+
type: "object" as const,
|
|
195
|
+
properties: {
|
|
196
|
+
name: {
|
|
197
|
+
type: "string",
|
|
198
|
+
description: "The entity name",
|
|
199
|
+
},
|
|
200
|
+
type: {
|
|
201
|
+
type: "string",
|
|
202
|
+
enum: ["person", "organization", "place"],
|
|
203
|
+
description: "Entity type",
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
required: ["name", "type"],
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: "create_relationship",
|
|
211
|
+
description: "Create a relationship between two entities.",
|
|
212
|
+
input_schema: {
|
|
213
|
+
type: "object" as const,
|
|
214
|
+
properties: {
|
|
215
|
+
from: {
|
|
216
|
+
type: "string",
|
|
217
|
+
description: "Source entity name",
|
|
218
|
+
},
|
|
219
|
+
to: {
|
|
220
|
+
type: "string",
|
|
221
|
+
description: "Target entity name",
|
|
222
|
+
},
|
|
223
|
+
type: {
|
|
224
|
+
type: "string",
|
|
225
|
+
description: "Relationship type (e.g., 'works_at', 'lives_in', 'knows', 'sibling_of')",
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
required: ["from", "to", "type"],
|
|
229
|
+
},
|
|
230
|
+
},
|
|
150
231
|
{
|
|
151
232
|
name: "find_duplicates",
|
|
152
233
|
description: "Find potential duplicate entities that could be merged.",
|
|
@@ -183,12 +264,22 @@ interface ChatMessage {
|
|
|
183
264
|
content: string;
|
|
184
265
|
}
|
|
185
266
|
|
|
267
|
+
// Stream event types for SSE
|
|
268
|
+
export interface StreamEvent {
|
|
269
|
+
type: "text" | "tool_start" | "tool_end" | "error" | "done";
|
|
270
|
+
content?: string;
|
|
271
|
+
tool?: string;
|
|
272
|
+
result?: unknown;
|
|
273
|
+
}
|
|
274
|
+
|
|
186
275
|
export class ChatHandler {
|
|
187
276
|
private client: Anthropic | null = null;
|
|
188
277
|
private db: EngramDatabase;
|
|
189
278
|
private graph: KnowledgeGraph;
|
|
190
279
|
private search: HybridSearch;
|
|
191
280
|
private conversationHistory: Anthropic.MessageParam[] = [];
|
|
281
|
+
private isProcessing: boolean = false;
|
|
282
|
+
private messageQueue: Array<{ message: string; resolve: (value: string) => void; reject: (error: Error) => void }> = [];
|
|
192
283
|
|
|
193
284
|
constructor(options: {
|
|
194
285
|
db: EngramDatabase;
|
|
@@ -209,15 +300,174 @@ export class ChatHandler {
|
|
|
209
300
|
return this.client !== null;
|
|
210
301
|
}
|
|
211
302
|
|
|
303
|
+
isBusy(): boolean {
|
|
304
|
+
return this.isProcessing;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
getQueueLength(): number {
|
|
308
|
+
return this.messageQueue.length;
|
|
309
|
+
}
|
|
310
|
+
|
|
212
311
|
clearHistory(): void {
|
|
213
312
|
this.conversationHistory = [];
|
|
214
313
|
}
|
|
215
314
|
|
|
315
|
+
// Process message queue
|
|
316
|
+
private async processQueue(): Promise<void> {
|
|
317
|
+
if (this.isProcessing || this.messageQueue.length === 0) return;
|
|
318
|
+
|
|
319
|
+
const { message, resolve, reject } = this.messageQueue.shift()!;
|
|
320
|
+
try {
|
|
321
|
+
const result = await this.processMessage(message);
|
|
322
|
+
resolve(result);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Process next in queue
|
|
328
|
+
this.processQueue();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Queue-aware chat method
|
|
216
332
|
async chat(userMessage: string): Promise<string> {
|
|
217
333
|
if (!this.client) {
|
|
218
334
|
return "Chat is not configured. Set ANTHROPIC_API_KEY environment variable.";
|
|
219
335
|
}
|
|
220
336
|
|
|
337
|
+
// If busy, queue the message
|
|
338
|
+
if (this.isProcessing) {
|
|
339
|
+
return new Promise((resolve, reject) => {
|
|
340
|
+
this.messageQueue.push({ message: userMessage, resolve, reject });
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return this.processMessage(userMessage);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Streaming chat with callbacks for real-time updates
|
|
348
|
+
async *chatStream(userMessage: string): AsyncGenerator<StreamEvent> {
|
|
349
|
+
if (!this.client) {
|
|
350
|
+
yield { type: "error", content: "Chat is not configured. Set ANTHROPIC_API_KEY environment variable." };
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
this.isProcessing = true;
|
|
355
|
+
|
|
356
|
+
// Add user message to history
|
|
357
|
+
this.conversationHistory.push({
|
|
358
|
+
role: "user",
|
|
359
|
+
content: userMessage,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
// Keep conversation history manageable
|
|
364
|
+
if (this.conversationHistory.length > 20) {
|
|
365
|
+
this.conversationHistory = this.conversationHistory.slice(-20);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
let continueLoop = true;
|
|
369
|
+
let fullResponse = "";
|
|
370
|
+
|
|
371
|
+
while (continueLoop) {
|
|
372
|
+
const stream = this.client.messages.stream({
|
|
373
|
+
model: "claude-haiku-4-5-20241022",
|
|
374
|
+
max_tokens: 1024,
|
|
375
|
+
system: SYSTEM_PROMPT,
|
|
376
|
+
tools: TOOLS,
|
|
377
|
+
messages: this.conversationHistory,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
let currentToolUse: { id: string; name: string; input: string } | null = null;
|
|
381
|
+
|
|
382
|
+
for await (const event of stream) {
|
|
383
|
+
if (event.type === "content_block_start") {
|
|
384
|
+
if (event.content_block.type === "tool_use") {
|
|
385
|
+
currentToolUse = {
|
|
386
|
+
id: event.content_block.id,
|
|
387
|
+
name: event.content_block.name,
|
|
388
|
+
input: "",
|
|
389
|
+
};
|
|
390
|
+
yield { type: "tool_start", tool: event.content_block.name };
|
|
391
|
+
}
|
|
392
|
+
} else if (event.type === "content_block_delta") {
|
|
393
|
+
if (event.delta.type === "text_delta") {
|
|
394
|
+
fullResponse += event.delta.text;
|
|
395
|
+
yield { type: "text", content: event.delta.text };
|
|
396
|
+
} else if (event.delta.type === "input_json_delta" && currentToolUse) {
|
|
397
|
+
currentToolUse.input += event.delta.partial_json;
|
|
398
|
+
}
|
|
399
|
+
} else if (event.type === "content_block_stop") {
|
|
400
|
+
if (currentToolUse) {
|
|
401
|
+
// Execute the tool
|
|
402
|
+
let toolInput: Record<string, unknown> = {};
|
|
403
|
+
try {
|
|
404
|
+
toolInput = JSON.parse(currentToolUse.input || "{}");
|
|
405
|
+
} catch {
|
|
406
|
+
toolInput = {};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const result = await this.executeTool(currentToolUse.name, toolInput);
|
|
410
|
+
yield { type: "tool_end", tool: currentToolUse.name, result };
|
|
411
|
+
currentToolUse = null;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Get final message to check stop reason
|
|
417
|
+
const finalMessage = await stream.finalMessage();
|
|
418
|
+
|
|
419
|
+
if (finalMessage.stop_reason === "tool_use") {
|
|
420
|
+
// Process tool results and continue
|
|
421
|
+
const toolUseBlocks = finalMessage.content.filter(
|
|
422
|
+
(block): block is Anthropic.ToolUseBlock => block.type === "tool_use"
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
const toolResults: Anthropic.ToolResultBlockParam[] = [];
|
|
426
|
+
for (const toolUse of toolUseBlocks) {
|
|
427
|
+
const result = await this.executeTool(toolUse.name, toolUse.input as Record<string, unknown>);
|
|
428
|
+
toolResults.push({
|
|
429
|
+
type: "tool_result",
|
|
430
|
+
tool_use_id: toolUse.id,
|
|
431
|
+
content: JSON.stringify(result),
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Add to history
|
|
436
|
+
this.conversationHistory.push({
|
|
437
|
+
role: "assistant",
|
|
438
|
+
content: finalMessage.content,
|
|
439
|
+
});
|
|
440
|
+
this.conversationHistory.push({
|
|
441
|
+
role: "user",
|
|
442
|
+
content: toolResults,
|
|
443
|
+
});
|
|
444
|
+
} else {
|
|
445
|
+
// Done - add final response to history
|
|
446
|
+
this.conversationHistory.push({
|
|
447
|
+
role: "assistant",
|
|
448
|
+
content: finalMessage.content,
|
|
449
|
+
});
|
|
450
|
+
continueLoop = false;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
yield { type: "done" };
|
|
455
|
+
} catch (error) {
|
|
456
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
457
|
+
yield { type: "error", content: message };
|
|
458
|
+
} finally {
|
|
459
|
+
this.isProcessing = false;
|
|
460
|
+
// Process any queued messages
|
|
461
|
+
this.processQueue();
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Original non-streaming method for backwards compatibility
|
|
466
|
+
private async processMessage(userMessage: string): Promise<string> {
|
|
467
|
+
if (!this.client) {
|
|
468
|
+
return "Chat is not configured. Set ANTHROPIC_API_KEY environment variable.";
|
|
469
|
+
}
|
|
470
|
+
|
|
221
471
|
// Add user message to history
|
|
222
472
|
this.conversationHistory.push({
|
|
223
473
|
role: "user",
|
|
@@ -427,9 +677,93 @@ export class ChatHandler {
|
|
|
427
677
|
return { error: `Memory not found: ${id}` };
|
|
428
678
|
}
|
|
429
679
|
|
|
680
|
+
// Soft-delete: remove from index and disable
|
|
430
681
|
await this.search.removeFromIndex(id);
|
|
431
|
-
this.db.
|
|
432
|
-
return { success: true,
|
|
682
|
+
this.db.updateMemory(id, { disabled: true });
|
|
683
|
+
return { success: true, disabled_id: id, message: "Memory disabled (soft-deleted)" };
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
case "edit_memory": {
|
|
687
|
+
const id = input.id as string;
|
|
688
|
+
const content = input.content as string | undefined;
|
|
689
|
+
const importance = input.importance as number | undefined;
|
|
690
|
+
|
|
691
|
+
const memory = this.db.getMemory(id);
|
|
692
|
+
if (!memory) {
|
|
693
|
+
return { error: `Memory not found: ${id}` };
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const updates: { content?: string; importance?: number } = {};
|
|
697
|
+
if (content !== undefined) updates.content = content;
|
|
698
|
+
if (importance !== undefined) updates.importance = importance;
|
|
699
|
+
|
|
700
|
+
if (Object.keys(updates).length === 0) {
|
|
701
|
+
return { error: "No updates provided" };
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const updated = this.db.updateMemory(id, updates);
|
|
705
|
+
|
|
706
|
+
// Re-index if content changed
|
|
707
|
+
if (content !== undefined && updated) {
|
|
708
|
+
await this.search.removeFromIndex(id);
|
|
709
|
+
await this.search.indexMemory(updated);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return {
|
|
713
|
+
success: true,
|
|
714
|
+
memory_id: id,
|
|
715
|
+
updated_fields: Object.keys(updates),
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
case "create_memory": {
|
|
720
|
+
const content = input.content as string;
|
|
721
|
+
const importance = (input.importance as number) || 0.5;
|
|
722
|
+
|
|
723
|
+
const memory = this.db.createMemory(content, "chat", importance);
|
|
724
|
+
await this.search.indexMemory(memory);
|
|
725
|
+
|
|
726
|
+
return {
|
|
727
|
+
success: true,
|
|
728
|
+
memory_id: memory.id,
|
|
729
|
+
content: memory.content.substring(0, 100) + (memory.content.length > 100 ? "..." : ""),
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
case "create_entity": {
|
|
734
|
+
const name = input.name as string;
|
|
735
|
+
const type = input.type as "person" | "organization" | "place";
|
|
736
|
+
|
|
737
|
+
// Check if entity already exists
|
|
738
|
+
const existing = this.db.findEntityByName(name);
|
|
739
|
+
if (existing) {
|
|
740
|
+
return { error: `Entity already exists: ${name}`, existing_id: existing.id };
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const entity = this.graph.getOrCreateEntity(name, type);
|
|
744
|
+
return {
|
|
745
|
+
success: true,
|
|
746
|
+
entity_id: entity.id,
|
|
747
|
+
name: entity.name,
|
|
748
|
+
type: entity.type,
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
case "create_relationship": {
|
|
753
|
+
const fromName = input.from as string;
|
|
754
|
+
const toName = input.to as string;
|
|
755
|
+
const type = input.type as string;
|
|
756
|
+
|
|
757
|
+
// Get or create both entities (default to person type if not existing)
|
|
758
|
+
const fromEntity = this.graph.getOrCreateEntity(fromName, "person");
|
|
759
|
+
const toEntity = this.graph.getOrCreateEntity(toName, "person");
|
|
760
|
+
|
|
761
|
+
this.graph.relate(fromEntity.name, toEntity.name, type);
|
|
762
|
+
|
|
763
|
+
return {
|
|
764
|
+
success: true,
|
|
765
|
+
relationship: `${fromName} -[${type}]-> ${toName}`,
|
|
766
|
+
};
|
|
433
767
|
}
|
|
434
768
|
|
|
435
769
|
case "find_duplicates": {
|
package/src/web/server.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import http from "http";
|
|
7
7
|
import fs from "fs";
|
|
8
8
|
import path from "path";
|
|
9
|
+
import os from "os";
|
|
9
10
|
import { fileURLToPath } from "url";
|
|
10
11
|
import { EngramDatabase } from "../storage/database.js";
|
|
11
12
|
import { KnowledgeGraph } from "../graph/knowledge-graph.js";
|
|
@@ -18,6 +19,12 @@ const __dirname = path.dirname(__filename);
|
|
|
18
19
|
|
|
19
20
|
const STATIC_DIR = path.join(__dirname, "..", "..", "src", "web", "static");
|
|
20
21
|
|
|
22
|
+
// Port file for discovery - allows finding the running web server
|
|
23
|
+
const PORT_FILE = path.join(
|
|
24
|
+
process.env.ENGRAM_DB_PATH?.replace("~", os.homedir()) || path.join(os.homedir(), ".engram"),
|
|
25
|
+
"web-server.json"
|
|
26
|
+
);
|
|
27
|
+
|
|
21
28
|
const MIME_TYPES: Record<string, string> = {
|
|
22
29
|
".html": "text/html",
|
|
23
30
|
".css": "text/css",
|
|
@@ -27,6 +34,31 @@ const MIME_TYPES: Record<string, string> = {
|
|
|
27
34
|
".svg": "image/svg+xml",
|
|
28
35
|
};
|
|
29
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Get the URL of a currently running Engram web server
|
|
39
|
+
* Returns null if no server is running
|
|
40
|
+
*/
|
|
41
|
+
export function getRunningServerUrl(): string | null {
|
|
42
|
+
try {
|
|
43
|
+
if (fs.existsSync(PORT_FILE)) {
|
|
44
|
+
const data = JSON.parse(fs.readFileSync(PORT_FILE, "utf-8"));
|
|
45
|
+
const { port, pid } = data;
|
|
46
|
+
|
|
47
|
+
// Check if process is still running
|
|
48
|
+
try {
|
|
49
|
+
process.kill(pid, 0); // Signal 0 = check if process exists
|
|
50
|
+
return `http://localhost:${port}`;
|
|
51
|
+
} catch {
|
|
52
|
+
// Process not running, clean up stale file
|
|
53
|
+
fs.unlinkSync(PORT_FILE);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// File doesn't exist or can't be read
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
30
62
|
interface WebServerOptions {
|
|
31
63
|
db: EngramDatabase;
|
|
32
64
|
graph: KnowledgeGraph;
|
|
@@ -61,21 +93,92 @@ export class EngramWebServer {
|
|
|
61
93
|
return `http://localhost:${this.port}`;
|
|
62
94
|
}
|
|
63
95
|
|
|
96
|
+
// Check if another server is already running
|
|
97
|
+
const existingUrl = getRunningServerUrl();
|
|
98
|
+
if (existingUrl) {
|
|
99
|
+
console.error(`[Engram] Web interface already running at ${existingUrl}`);
|
|
100
|
+
return existingUrl;
|
|
101
|
+
}
|
|
102
|
+
|
|
64
103
|
this.server = http.createServer((req, res) => this.handleRequest(req, res));
|
|
65
104
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
console.error(`[Engram] Web interface running at ${url}`);
|
|
70
|
-
resolve(url);
|
|
71
|
-
});
|
|
105
|
+
// Try to start on preferred port, auto-increment if taken
|
|
106
|
+
const maxAttempts = 10;
|
|
107
|
+
let currentPort = this.port;
|
|
72
108
|
|
|
73
|
-
|
|
74
|
-
|
|
109
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
110
|
+
try {
|
|
111
|
+
await new Promise<void>((resolve, reject) => {
|
|
112
|
+
this.server!.once("error", (err: NodeJS.ErrnoException) => {
|
|
113
|
+
if (err.code === "EADDRINUSE") {
|
|
114
|
+
currentPort++;
|
|
115
|
+
resolve(); // Try next port
|
|
116
|
+
} else {
|
|
117
|
+
reject(err);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
this.server!.listen(currentPort, () => {
|
|
122
|
+
resolve();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// If we get here without error, server is listening
|
|
127
|
+
if (this.server!.listening) {
|
|
128
|
+
this.port = currentPort;
|
|
129
|
+
const url = `http://localhost:${this.port}`;
|
|
130
|
+
|
|
131
|
+
// Write port file for discovery
|
|
132
|
+
this.writePortFile();
|
|
133
|
+
|
|
134
|
+
console.error(`[Engram] Web interface running at ${url}`);
|
|
135
|
+
return url;
|
|
136
|
+
}
|
|
137
|
+
} catch (err) {
|
|
138
|
+
if (attempt === maxAttempts - 1) {
|
|
139
|
+
throw err;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
throw new Error(`Could not find available port after ${maxAttempts} attempts`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private writePortFile(): void {
|
|
148
|
+
try {
|
|
149
|
+
// Ensure directory exists
|
|
150
|
+
const dir = path.dirname(PORT_FILE);
|
|
151
|
+
if (!fs.existsSync(dir)) {
|
|
152
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
fs.writeFileSync(PORT_FILE, JSON.stringify({
|
|
156
|
+
port: this.port,
|
|
157
|
+
pid: process.pid,
|
|
158
|
+
started: new Date().toISOString(),
|
|
159
|
+
}));
|
|
160
|
+
} catch (err) {
|
|
161
|
+
console.error("[Engram] Failed to write port file:", err);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private removePortFile(): void {
|
|
166
|
+
try {
|
|
167
|
+
if (fs.existsSync(PORT_FILE)) {
|
|
168
|
+
const data = JSON.parse(fs.readFileSync(PORT_FILE, "utf-8"));
|
|
169
|
+
// Only remove if we wrote it
|
|
170
|
+
if (data.pid === process.pid) {
|
|
171
|
+
fs.unlinkSync(PORT_FILE);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
// Ignore cleanup errors
|
|
176
|
+
}
|
|
75
177
|
}
|
|
76
178
|
|
|
77
179
|
stop(): void {
|
|
78
180
|
if (this.server) {
|
|
181
|
+
this.removePortFile();
|
|
79
182
|
this.server.close();
|
|
80
183
|
this.server = null;
|
|
81
184
|
}
|
|
@@ -134,6 +237,7 @@ export class EngramWebServer {
|
|
|
134
237
|
if (pathname === "/api/memories" && method === "GET") {
|
|
135
238
|
const query = url.searchParams.get("q");
|
|
136
239
|
const limit = parseInt(url.searchParams.get("limit") || "50");
|
|
240
|
+
const offset = parseInt(url.searchParams.get("offset") || "0");
|
|
137
241
|
|
|
138
242
|
if (query) {
|
|
139
243
|
const results = await this.search.search(query, { limit });
|
|
@@ -145,7 +249,7 @@ export class EngramWebServer {
|
|
|
145
249
|
})),
|
|
146
250
|
}));
|
|
147
251
|
} else {
|
|
148
|
-
const memories = this.db.getAllMemories(limit);
|
|
252
|
+
const memories = this.db.getAllMemories(limit, false, offset);
|
|
149
253
|
res.end(JSON.stringify({ memories }));
|
|
150
254
|
}
|
|
151
255
|
return;
|
|
@@ -307,6 +411,59 @@ export class EngramWebServer {
|
|
|
307
411
|
return;
|
|
308
412
|
}
|
|
309
413
|
|
|
414
|
+
// POST /api/chat/stream - streaming chat with SSE
|
|
415
|
+
if (pathname === "/api/chat/stream" && method === "POST") {
|
|
416
|
+
const { message } = body as { message: string };
|
|
417
|
+
if (!message) {
|
|
418
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
419
|
+
res.end(JSON.stringify({ error: "Message is required" }));
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Check if chat is busy
|
|
424
|
+
if (this.chat.isBusy()) {
|
|
425
|
+
res.writeHead(429, { "Content-Type": "application/json" });
|
|
426
|
+
res.end(JSON.stringify({
|
|
427
|
+
error: "Chat is busy",
|
|
428
|
+
queue_length: this.chat.getQueueLength(),
|
|
429
|
+
}));
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Set up SSE headers
|
|
434
|
+
res.writeHead(200, {
|
|
435
|
+
"Content-Type": "text/event-stream",
|
|
436
|
+
"Cache-Control": "no-cache",
|
|
437
|
+
"Connection": "keep-alive",
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// Stream events to client
|
|
441
|
+
try {
|
|
442
|
+
for await (const event of this.chat.chatStream(message)) {
|
|
443
|
+
const data = JSON.stringify(event);
|
|
444
|
+
res.write(`data: ${data}\n\n`);
|
|
445
|
+
}
|
|
446
|
+
} catch (error) {
|
|
447
|
+
const errorEvent = {
|
|
448
|
+
type: "error",
|
|
449
|
+
content: error instanceof Error ? error.message : String(error),
|
|
450
|
+
};
|
|
451
|
+
res.write(`data: ${JSON.stringify(errorEvent)}\n\n`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
res.end();
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// GET /api/chat/queue - check queue status
|
|
459
|
+
if (pathname === "/api/chat/queue" && method === "GET") {
|
|
460
|
+
res.end(JSON.stringify({
|
|
461
|
+
busy: this.chat.isBusy(),
|
|
462
|
+
queue_length: this.chat.getQueueLength(),
|
|
463
|
+
}));
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
310
467
|
// ============ Consolidation Endpoints ============
|
|
311
468
|
|
|
312
469
|
// GET /api/consolidation/status - get consolidation status
|
package/src/web/static/app.js
CHANGED
|
@@ -8,6 +8,8 @@ const API_BASE = '';
|
|
|
8
8
|
// State
|
|
9
9
|
let currentView = 'memories';
|
|
10
10
|
let editingMemoryId = null;
|
|
11
|
+
let memoriesOffset = 0;
|
|
12
|
+
const MEMORIES_PAGE_SIZE = 25;
|
|
11
13
|
|
|
12
14
|
// DOM Elements
|
|
13
15
|
const views = {
|
|
@@ -72,17 +74,28 @@ async function loadStats() {
|
|
|
72
74
|
statsEl.textContent = text;
|
|
73
75
|
}
|
|
74
76
|
|
|
75
|
-
// Load memories
|
|
76
|
-
async function loadMemories(query = '') {
|
|
77
|
-
|
|
77
|
+
// Load memories with pagination
|
|
78
|
+
async function loadMemories(query = '', append = false) {
|
|
79
|
+
if (!append) {
|
|
80
|
+
memoriesOffset = 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const limit = MEMORIES_PAGE_SIZE;
|
|
84
|
+
let path;
|
|
85
|
+
if (query) {
|
|
86
|
+
path = `/api/memories?q=${encodeURIComponent(query)}&limit=${limit}`;
|
|
87
|
+
} else {
|
|
88
|
+
path = `/api/memories?limit=${limit}&offset=${memoriesOffset}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
78
91
|
const data = await api(path);
|
|
79
92
|
|
|
80
|
-
if (data.memories.length === 0) {
|
|
93
|
+
if (data.memories.length === 0 && !append) {
|
|
81
94
|
memoriesList.innerHTML = '<div class="empty-state">No memories found</div>';
|
|
82
95
|
return;
|
|
83
96
|
}
|
|
84
97
|
|
|
85
|
-
|
|
98
|
+
const memoriesHtml = data.memories.map(m => `
|
|
86
99
|
<div class="list-item memory-item" data-id="${m.id}">
|
|
87
100
|
<div class="content">${escapeHtml(m.content)}</div>
|
|
88
101
|
<div class="meta">
|
|
@@ -98,7 +111,29 @@ async function loadMemories(query = '') {
|
|
|
98
111
|
</div>
|
|
99
112
|
`).join('');
|
|
100
113
|
|
|
101
|
-
|
|
114
|
+
if (append) {
|
|
115
|
+
// Remove old "Load More" button if exists
|
|
116
|
+
const oldLoadMore = memoriesList.querySelector('.load-more-container');
|
|
117
|
+
if (oldLoadMore) oldLoadMore.remove();
|
|
118
|
+
memoriesList.insertAdjacentHTML('beforeend', memoriesHtml);
|
|
119
|
+
} else {
|
|
120
|
+
memoriesList.innerHTML = memoriesHtml;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Add "Load More" button if we got a full page and not searching
|
|
124
|
+
if (!query && data.memories.length === MEMORIES_PAGE_SIZE) {
|
|
125
|
+
memoriesList.insertAdjacentHTML('beforeend', `
|
|
126
|
+
<div class="load-more-container">
|
|
127
|
+
<button class="load-more-btn">Load More</button>
|
|
128
|
+
</div>
|
|
129
|
+
`);
|
|
130
|
+
memoriesList.querySelector('.load-more-btn').addEventListener('click', () => {
|
|
131
|
+
memoriesOffset += MEMORIES_PAGE_SIZE;
|
|
132
|
+
loadMemories('', true);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Attach event listeners to new items
|
|
102
137
|
memoriesList.querySelectorAll('.edit-btn').forEach(btn => {
|
|
103
138
|
btn.addEventListener('click', (e) => {
|
|
104
139
|
e.stopPropagation();
|
|
@@ -402,7 +437,7 @@ async function loadContradictions() {
|
|
|
402
437
|
}
|
|
403
438
|
}
|
|
404
439
|
|
|
405
|
-
// Load digests
|
|
440
|
+
// Load digests with hierarchy visualization
|
|
406
441
|
async function loadDigests() {
|
|
407
442
|
try {
|
|
408
443
|
const data = await api('/api/digests');
|
|
@@ -412,17 +447,59 @@ async function loadDigests() {
|
|
|
412
447
|
return;
|
|
413
448
|
}
|
|
414
449
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
450
|
+
// Group digests by level
|
|
451
|
+
const byLevel = { 1: [], 2: [], 3: [] };
|
|
452
|
+
data.digests.forEach(d => {
|
|
453
|
+
const level = d.level || 1;
|
|
454
|
+
if (!byLevel[level]) byLevel[level] = [];
|
|
455
|
+
byLevel[level].push(d);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
const levelLabels = {
|
|
459
|
+
1: 'Session Summaries',
|
|
460
|
+
2: 'Topic Digests',
|
|
461
|
+
3: 'Entity Profiles'
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
const levelDescs = {
|
|
465
|
+
1: 'Summaries of individual conversations',
|
|
466
|
+
2: 'Consolidated topic-based knowledge',
|
|
467
|
+
3: 'High-level entity profiles and patterns'
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
let html = '';
|
|
471
|
+
for (const level of [3, 2, 1]) { // Show highest level first
|
|
472
|
+
const digests = byLevel[level];
|
|
473
|
+
if (digests.length === 0) continue;
|
|
474
|
+
|
|
475
|
+
html += `
|
|
476
|
+
<div class="digest-level">
|
|
477
|
+
<h3 class="level-header">
|
|
478
|
+
<span class="level-badge">L${level}</span>
|
|
479
|
+
${levelLabels[level]}
|
|
480
|
+
<span class="level-count">(${digests.length})</span>
|
|
481
|
+
</h3>
|
|
482
|
+
<p class="level-desc">${levelDescs[level]}</p>
|
|
483
|
+
<div class="digest-list">
|
|
484
|
+
`;
|
|
485
|
+
|
|
486
|
+
digests.forEach(d => {
|
|
487
|
+
html += `
|
|
488
|
+
<div class="list-item digest-item" data-level="${level}">
|
|
489
|
+
<div class="content">${escapeHtml(d.content)}</div>
|
|
490
|
+
<div class="meta">
|
|
491
|
+
${d.topic ? `<span class="topic">${escapeHtml(d.topic)}</span>` : ''}
|
|
492
|
+
<span>${d.source_count} sources</span>
|
|
493
|
+
<span>${formatDate(d.created_at)}</span>
|
|
494
|
+
</div>
|
|
495
|
+
</div>
|
|
496
|
+
`;
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
html += '</div></div>';
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
digestsList.innerHTML = html;
|
|
426
503
|
} catch (e) {
|
|
427
504
|
console.error('Failed to load digests', e);
|
|
428
505
|
digestsList.innerHTML = '<div class="empty-state">Failed to load digests</div>';
|
|
@@ -637,7 +714,7 @@ function addChatMessage(content, role) {
|
|
|
637
714
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
638
715
|
}
|
|
639
716
|
|
|
640
|
-
// Send chat message
|
|
717
|
+
// Send chat message with streaming
|
|
641
718
|
async function sendChatMessage(message) {
|
|
642
719
|
if (!message.trim()) return;
|
|
643
720
|
|
|
@@ -646,24 +723,80 @@ async function sendChatMessage(message) {
|
|
|
646
723
|
chatInput.value = '';
|
|
647
724
|
chatInput.disabled = true;
|
|
648
725
|
|
|
649
|
-
//
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
chatMessages.appendChild(
|
|
726
|
+
// Create assistant message div for streaming
|
|
727
|
+
const responseDiv = document.createElement('div');
|
|
728
|
+
responseDiv.className = 'chat-message assistant streaming';
|
|
729
|
+
responseDiv.innerHTML = '<p></p>';
|
|
730
|
+
chatMessages.appendChild(responseDiv);
|
|
654
731
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
655
732
|
|
|
733
|
+
const contentEl = responseDiv.querySelector('p');
|
|
734
|
+
let currentContent = '';
|
|
735
|
+
|
|
656
736
|
try {
|
|
657
|
-
const
|
|
737
|
+
const response = await fetch('/api/chat/stream', {
|
|
658
738
|
method: 'POST',
|
|
659
|
-
|
|
739
|
+
headers: { 'Content-Type': 'application/json' },
|
|
740
|
+
body: JSON.stringify({ message }),
|
|
660
741
|
});
|
|
661
742
|
|
|
662
|
-
|
|
663
|
-
|
|
743
|
+
if (!response.ok) {
|
|
744
|
+
const errorData = await response.json();
|
|
745
|
+
throw new Error(errorData.error || 'Stream request failed');
|
|
746
|
+
}
|
|
664
747
|
|
|
665
|
-
|
|
666
|
-
|
|
748
|
+
const reader = response.body.getReader();
|
|
749
|
+
const decoder = new TextDecoder();
|
|
750
|
+
|
|
751
|
+
while (true) {
|
|
752
|
+
const { done, value } = await reader.read();
|
|
753
|
+
if (done) break;
|
|
754
|
+
|
|
755
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
756
|
+
const lines = chunk.split('\n');
|
|
757
|
+
|
|
758
|
+
for (const line of lines) {
|
|
759
|
+
if (line.startsWith('data: ')) {
|
|
760
|
+
try {
|
|
761
|
+
const event = JSON.parse(line.slice(6));
|
|
762
|
+
|
|
763
|
+
switch (event.type) {
|
|
764
|
+
case 'text':
|
|
765
|
+
currentContent += event.content;
|
|
766
|
+
contentEl.innerHTML = formatChatContent(currentContent);
|
|
767
|
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
768
|
+
break;
|
|
769
|
+
|
|
770
|
+
case 'tool_start':
|
|
771
|
+
// Show tool execution indicator
|
|
772
|
+
const toolIndicator = document.createElement('span');
|
|
773
|
+
toolIndicator.className = 'tool-indicator';
|
|
774
|
+
toolIndicator.textContent = `Using ${event.tool}...`;
|
|
775
|
+
contentEl.appendChild(toolIndicator);
|
|
776
|
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
777
|
+
break;
|
|
778
|
+
|
|
779
|
+
case 'tool_end':
|
|
780
|
+
// Remove tool indicator
|
|
781
|
+
const indicators = contentEl.querySelectorAll('.tool-indicator');
|
|
782
|
+
indicators.forEach(ind => ind.remove());
|
|
783
|
+
break;
|
|
784
|
+
|
|
785
|
+
case 'error':
|
|
786
|
+
currentContent += `\n\nError: ${event.content}`;
|
|
787
|
+
contentEl.innerHTML = formatChatContent(currentContent);
|
|
788
|
+
break;
|
|
789
|
+
|
|
790
|
+
case 'done':
|
|
791
|
+
responseDiv.classList.remove('streaming');
|
|
792
|
+
break;
|
|
793
|
+
}
|
|
794
|
+
} catch (e) {
|
|
795
|
+
// Ignore JSON parse errors for incomplete chunks
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
667
800
|
|
|
668
801
|
// Refresh data in case something changed
|
|
669
802
|
loadStats();
|
|
@@ -672,14 +805,23 @@ async function sendChatMessage(message) {
|
|
|
672
805
|
if (currentView === 'memories') loadMemories(searchInput.value);
|
|
673
806
|
|
|
674
807
|
} catch (e) {
|
|
675
|
-
|
|
676
|
-
|
|
808
|
+
responseDiv.classList.remove('streaming');
|
|
809
|
+
contentEl.innerHTML = formatChatContent(`Error: ${e.message || 'Failed to get response'}`);
|
|
677
810
|
}
|
|
678
811
|
|
|
679
812
|
chatInput.disabled = false;
|
|
680
813
|
chatInput.focus();
|
|
681
814
|
}
|
|
682
815
|
|
|
816
|
+
// Format chat content (markdown-like)
|
|
817
|
+
function formatChatContent(content) {
|
|
818
|
+
return content
|
|
819
|
+
.replace(/\n\n/g, '</p><p>')
|
|
820
|
+
.replace(/\n/g, '<br>')
|
|
821
|
+
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
822
|
+
.replace(/`(.+?)`/g, '<code>$1</code>');
|
|
823
|
+
}
|
|
824
|
+
|
|
683
825
|
// Clear chat history
|
|
684
826
|
async function clearChatHistory() {
|
|
685
827
|
try {
|
|
@@ -713,6 +855,33 @@ chatForm.addEventListener('submit', (e) => {
|
|
|
713
855
|
sendChatMessage(chatInput.value);
|
|
714
856
|
});
|
|
715
857
|
|
|
858
|
+
// ============ API Status Indicator ============
|
|
859
|
+
|
|
860
|
+
const apiStatusEl = document.getElementById('api-status');
|
|
861
|
+
|
|
862
|
+
async function checkApiStatus() {
|
|
863
|
+
apiStatusEl.classList.remove('connected', 'disconnected');
|
|
864
|
+
apiStatusEl.classList.add('checking');
|
|
865
|
+
apiStatusEl.title = 'Checking API status...';
|
|
866
|
+
|
|
867
|
+
try {
|
|
868
|
+
const data = await api('/api/chat/status');
|
|
869
|
+
apiStatusEl.classList.remove('checking');
|
|
870
|
+
if (data.configured) {
|
|
871
|
+
apiStatusEl.classList.add('connected');
|
|
872
|
+
apiStatusEl.title = 'Anthropic API connected';
|
|
873
|
+
} else {
|
|
874
|
+
apiStatusEl.classList.add('disconnected');
|
|
875
|
+
apiStatusEl.title = 'API key not configured - set ANTHROPIC_API_KEY';
|
|
876
|
+
}
|
|
877
|
+
} catch (e) {
|
|
878
|
+
apiStatusEl.classList.remove('checking');
|
|
879
|
+
apiStatusEl.classList.add('disconnected');
|
|
880
|
+
apiStatusEl.title = 'Failed to check API status';
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
716
884
|
// Initialize
|
|
885
|
+
checkApiStatus();
|
|
717
886
|
loadStats();
|
|
718
887
|
loadMemories();
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
<button class="nav-btn" data-view="consolidation">Consolidation</button>
|
|
17
17
|
</nav>
|
|
18
18
|
<div class="stats" id="stats"></div>
|
|
19
|
+
<div class="api-status" id="api-status" title="Anthropic API Status">
|
|
20
|
+
<span class="api-dot"></span>
|
|
21
|
+
<span class="api-label">API</span>
|
|
22
|
+
</div>
|
|
19
23
|
<button id="chat-toggle" class="chat-toggle" title="Open Chat Assistant">Chat</button>
|
|
20
24
|
</header>
|
|
21
25
|
|
package/src/web/static/style.css
CHANGED
|
@@ -98,6 +98,45 @@ nav {
|
|
|
98
98
|
font-variant-numeric: tabular-nums;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/* API Status indicator */
|
|
102
|
+
.api-status {
|
|
103
|
+
display: flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
gap: 0.375rem;
|
|
106
|
+
font-size: 0.75rem;
|
|
107
|
+
color: var(--text-muted);
|
|
108
|
+
padding: 0.25rem 0.625rem;
|
|
109
|
+
background: var(--bg-tertiary);
|
|
110
|
+
border-radius: 2px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.api-dot {
|
|
114
|
+
width: 8px;
|
|
115
|
+
height: 8px;
|
|
116
|
+
border-radius: 50%;
|
|
117
|
+
background: var(--text-muted);
|
|
118
|
+
transition: background 0.2s ease;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.api-status.connected .api-dot {
|
|
122
|
+
background: var(--success);
|
|
123
|
+
box-shadow: 0 0 4px var(--success);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.api-status.disconnected .api-dot {
|
|
127
|
+
background: var(--danger);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.api-status.checking .api-dot {
|
|
131
|
+
background: var(--accent);
|
|
132
|
+
animation: pulse 1s infinite;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@keyframes pulse {
|
|
136
|
+
0%, 100% { opacity: 1; }
|
|
137
|
+
50% { opacity: 0.4; }
|
|
138
|
+
}
|
|
139
|
+
|
|
101
140
|
/* Main content */
|
|
102
141
|
main {
|
|
103
142
|
max-width: 960px;
|
|
@@ -418,6 +457,25 @@ button:disabled {
|
|
|
418
457
|
font-style: italic;
|
|
419
458
|
}
|
|
420
459
|
|
|
460
|
+
/* Load more button */
|
|
461
|
+
.load-more-container {
|
|
462
|
+
display: flex;
|
|
463
|
+
justify-content: center;
|
|
464
|
+
padding: 1.5rem 0;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.load-more-btn {
|
|
468
|
+
background: var(--bg-tertiary);
|
|
469
|
+
color: var(--text-secondary);
|
|
470
|
+
padding: 0.75rem 2rem;
|
|
471
|
+
font-size: 0.875rem;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.load-more-btn:hover {
|
|
475
|
+
background: var(--border);
|
|
476
|
+
color: var(--text-primary);
|
|
477
|
+
}
|
|
478
|
+
|
|
421
479
|
/* Score badge */
|
|
422
480
|
.score {
|
|
423
481
|
display: inline-block;
|
|
@@ -572,6 +630,42 @@ button:disabled {
|
|
|
572
630
|
font-style: italic;
|
|
573
631
|
}
|
|
574
632
|
|
|
633
|
+
/* Streaming message indicator */
|
|
634
|
+
.chat-message.streaming {
|
|
635
|
+
position: relative;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.chat-message.streaming::after {
|
|
639
|
+
content: '';
|
|
640
|
+
display: inline-block;
|
|
641
|
+
width: 0.5rem;
|
|
642
|
+
height: 1rem;
|
|
643
|
+
background: var(--accent);
|
|
644
|
+
margin-left: 0.25rem;
|
|
645
|
+
animation: blink 0.7s infinite;
|
|
646
|
+
vertical-align: text-bottom;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
@keyframes blink {
|
|
650
|
+
0%, 50% { opacity: 1; }
|
|
651
|
+
51%, 100% { opacity: 0; }
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/* Tool indicator in streaming */
|
|
655
|
+
.tool-indicator {
|
|
656
|
+
display: block;
|
|
657
|
+
font-size: 0.75rem;
|
|
658
|
+
color: var(--accent);
|
|
659
|
+
font-style: italic;
|
|
660
|
+
padding: 0.5rem 0;
|
|
661
|
+
border-top: 1px dashed var(--border);
|
|
662
|
+
margin-top: 0.5rem;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
.tool-indicator::before {
|
|
666
|
+
content: '⚙ ';
|
|
667
|
+
}
|
|
668
|
+
|
|
575
669
|
.chat-status {
|
|
576
670
|
padding: 0.5rem 1rem;
|
|
577
671
|
font-size: 0.75rem;
|
|
@@ -750,6 +844,51 @@ body.chat-open main {
|
|
|
750
844
|
color: var(--bg-primary);
|
|
751
845
|
}
|
|
752
846
|
|
|
847
|
+
/* Digest hierarchy */
|
|
848
|
+
.digest-level {
|
|
849
|
+
margin-bottom: 2rem;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
.level-header {
|
|
853
|
+
display: flex;
|
|
854
|
+
align-items: center;
|
|
855
|
+
gap: 0.5rem;
|
|
856
|
+
font-size: 1rem;
|
|
857
|
+
font-weight: 500;
|
|
858
|
+
margin-bottom: 0.25rem;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
.level-badge {
|
|
862
|
+
display: inline-flex;
|
|
863
|
+
align-items: center;
|
|
864
|
+
justify-content: center;
|
|
865
|
+
width: 1.75rem;
|
|
866
|
+
height: 1.75rem;
|
|
867
|
+
background: var(--accent);
|
|
868
|
+
color: white;
|
|
869
|
+
font-size: 0.6875rem;
|
|
870
|
+
font-weight: 600;
|
|
871
|
+
border-radius: 2px;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
.level-count {
|
|
875
|
+
font-size: 0.8125rem;
|
|
876
|
+
font-weight: 400;
|
|
877
|
+
color: var(--text-muted);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
.level-desc {
|
|
881
|
+
font-size: 0.8125rem;
|
|
882
|
+
color: var(--text-muted);
|
|
883
|
+
margin-bottom: 1rem;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
.digest-list {
|
|
887
|
+
display: flex;
|
|
888
|
+
flex-direction: column;
|
|
889
|
+
gap: 0.75rem;
|
|
890
|
+
}
|
|
891
|
+
|
|
753
892
|
/* Digest item */
|
|
754
893
|
.digest-item {
|
|
755
894
|
background: var(--bg-secondary);
|
|
@@ -757,6 +896,14 @@ body.chat-open main {
|
|
|
757
896
|
padding: 1.25rem 1.5rem;
|
|
758
897
|
}
|
|
759
898
|
|
|
899
|
+
.digest-item[data-level="3"] {
|
|
900
|
+
border-left: 3px solid var(--accent);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
.digest-item[data-level="2"] {
|
|
904
|
+
border-left: 3px solid var(--success);
|
|
905
|
+
}
|
|
906
|
+
|
|
760
907
|
.digest-item .content {
|
|
761
908
|
font-size: 1rem;
|
|
762
909
|
line-height: 1.7;
|