@levalicious/server-memory 0.0.17 → 0.0.18
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/server.js +2 -2
- package/dist/src/kb_load.js +27 -33
- package/dist/tests/memory-server.test.js +14 -7
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -953,7 +953,7 @@ export function createServer(memoryFilePath) {
|
|
|
953
953
|
sizes: ["any"]
|
|
954
954
|
}
|
|
955
955
|
],
|
|
956
|
-
version: "0.0.
|
|
956
|
+
version: "0.0.18",
|
|
957
957
|
}, {
|
|
958
958
|
capabilities: {
|
|
959
959
|
tools: {},
|
|
@@ -1276,7 +1276,7 @@ Use this to build chains of reasoning that persist in the graph. Each thought ca
|
|
|
1276
1276
|
},
|
|
1277
1277
|
{
|
|
1278
1278
|
name: "kb_load",
|
|
1279
|
-
description: `Load a plaintext document into the knowledge graph. Chunks the text into entities connected by a doubly-linked chain, runs sentence TextRank to identify the most important sentences, and creates an index entity that links directly to the
|
|
1279
|
+
description: `Load a plaintext document into the knowledge graph. Chunks the text into entities connected by a doubly-linked chain, runs sentence TextRank to identify the most important sentences, and creates an index entity per key phrase that links directly to the chunk containing that sentence.
|
|
1280
1280
|
|
|
1281
1281
|
The file MUST be plaintext (.txt, .tex, .md, source code, etc.). For PDFs, use pdftotext first. For other binary formats, convert to text before calling this tool.`,
|
|
1282
1282
|
inputSchema: {
|
package/dist/src/kb_load.js
CHANGED
|
@@ -317,28 +317,18 @@ export function loadDocument(text, title, st, topK = 15) {
|
|
|
317
317
|
seenChunks.add(chunk.id);
|
|
318
318
|
highlights.push({ chunk, sentence, score });
|
|
319
319
|
}
|
|
320
|
-
// 6. Build index
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
332
|
-
else {
|
|
333
|
-
if (current)
|
|
334
|
-
indexObs.push(current);
|
|
335
|
-
if (indexObs.length >= MAX_OBS_PER_ENTITY)
|
|
336
|
-
break;
|
|
337
|
-
current = preview.length <= MAX_OBS_LENGTH ? preview : preview.slice(0, MAX_OBS_LENGTH);
|
|
338
|
-
}
|
|
320
|
+
// 6. Build index entities — one per highlighted phrase
|
|
321
|
+
// Each DocumentIndex entity holds a single extracted key phrase as its
|
|
322
|
+
// observation, and links to the chunk that contains it. This gives the
|
|
323
|
+
// walker discrete semantic entry points into the document chain.
|
|
324
|
+
const indexEntities = [];
|
|
325
|
+
for (const { chunk, sentence } of highlights) {
|
|
326
|
+
const phrase = sentence.text.length <= MAX_OBS_LENGTH
|
|
327
|
+
? sentence.text
|
|
328
|
+
: sentence.text.slice(0, MAX_OBS_LENGTH - 3) + '...';
|
|
329
|
+
const indexId = `${title}__idx_${indexEntities.length}`;
|
|
330
|
+
indexEntities.push({ id: indexId, phrase, chunk });
|
|
339
331
|
}
|
|
340
|
-
if (current && indexObs.length < MAX_OBS_PER_ENTITY)
|
|
341
|
-
indexObs.push(current);
|
|
342
332
|
// ─── Assemble entities ──────────────────────────────────────────
|
|
343
333
|
const entities = [];
|
|
344
334
|
const relations = [];
|
|
@@ -352,12 +342,14 @@ export function loadDocument(text, title, st, topK = 15) {
|
|
|
352
342
|
observations: chunk.observations.map(o => o.text),
|
|
353
343
|
});
|
|
354
344
|
}
|
|
355
|
-
// Index
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
345
|
+
// Index entities — one per key phrase
|
|
346
|
+
for (const idx of indexEntities) {
|
|
347
|
+
entities.push({
|
|
348
|
+
name: idx.id,
|
|
349
|
+
entityType: 'DocumentIndex',
|
|
350
|
+
observations: [idx.phrase],
|
|
351
|
+
});
|
|
352
|
+
}
|
|
361
353
|
// ─── Assemble relations ─────────────────────────────────────────
|
|
362
354
|
// Document → chain endpoints
|
|
363
355
|
if (chunks.length > 0) {
|
|
@@ -373,13 +365,15 @@ export function loadDocument(text, title, st, topK = 15) {
|
|
|
373
365
|
relations.push({ from: chunks[i].id, to: chunks[i + 1].id, relationType: 'follows' });
|
|
374
366
|
relations.push({ from: chunks[i + 1].id, to: chunks[i].id, relationType: 'preceded_by' });
|
|
375
367
|
}
|
|
376
|
-
// Document → index
|
|
377
|
-
|
|
378
|
-
|
|
368
|
+
// Document → index entries
|
|
369
|
+
for (const idx of indexEntities) {
|
|
370
|
+
relations.push({ from: title, to: idx.id, relationType: 'has_index' });
|
|
371
|
+
relations.push({ from: idx.id, to: title, relationType: 'indexes' });
|
|
372
|
+
}
|
|
379
373
|
// Index → highlighted chunks
|
|
380
|
-
for (const
|
|
381
|
-
relations.push({ from:
|
|
382
|
-
relations.push({ from: chunk.id, to:
|
|
374
|
+
for (const idx of indexEntities) {
|
|
375
|
+
relations.push({ from: idx.id, to: idx.chunk.id, relationType: 'highlights' });
|
|
376
|
+
relations.push({ from: idx.chunk.id, to: idx.id, relationType: 'highlighted_by' });
|
|
383
377
|
}
|
|
384
378
|
return {
|
|
385
379
|
entities,
|
|
@@ -1068,10 +1068,14 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
1068
1068
|
const docResult = await callTool(client, 'open_nodes', { names: ['test-doc'] });
|
|
1069
1069
|
expect(docResult.entities.items).toHaveLength(1);
|
|
1070
1070
|
expect(docResult.entities.items[0].entityType).toBe('Document');
|
|
1071
|
-
// Check index
|
|
1072
|
-
const
|
|
1073
|
-
expect(
|
|
1074
|
-
|
|
1071
|
+
// Check index entities (one per key phrase)
|
|
1072
|
+
const indexEntities = await callTool(client, 'get_entities_by_type', { entityType: 'DocumentIndex' });
|
|
1073
|
+
expect(indexEntities.items.length).toBeGreaterThan(0);
|
|
1074
|
+
for (const idx of indexEntities.items) {
|
|
1075
|
+
expect(idx.entityType).toBe('DocumentIndex');
|
|
1076
|
+
expect(idx.observations.length).toBe(1);
|
|
1077
|
+
expect(idx.observations[0].length).toBeLessThanOrEqual(140);
|
|
1078
|
+
}
|
|
1075
1079
|
// Check TextChunk entities exist via type query
|
|
1076
1080
|
const chunks = await callTool(client, 'get_entities_by_type', { entityType: 'TextChunk' });
|
|
1077
1081
|
expect(chunks.items.length).toBeGreaterThan(0);
|
|
@@ -1103,8 +1107,11 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
1103
1107
|
await fs.writeFile(docFile, sentences.join(' '));
|
|
1104
1108
|
const result = await callTool(client, 'kb_load', { filePath: docFile });
|
|
1105
1109
|
expect(result.stats.indexHighlights).toBeGreaterThan(0);
|
|
1106
|
-
//
|
|
1107
|
-
const
|
|
1110
|
+
// Find index entities and verify they have highlight relations
|
|
1111
|
+
const indexEntities = await callTool(client, 'get_entities_by_type', { entityType: 'DocumentIndex' });
|
|
1112
|
+
expect(indexEntities.items.length).toBeGreaterThan(0);
|
|
1113
|
+
const indexNames = indexEntities.items.map(e => e.name);
|
|
1114
|
+
const indexResult = await callTool(client, 'open_nodes', { names: indexNames });
|
|
1108
1115
|
const highlightRels = indexResult.relations.items.filter(r => r.relationType === 'highlights');
|
|
1109
1116
|
expect(highlightRels.length).toBeGreaterThan(0);
|
|
1110
1117
|
});
|
|
@@ -1123,7 +1130,7 @@ describe('MCP Memory Server E2E Tests', () => {
|
|
|
1123
1130
|
await callTool(client, 'kb_load', { filePath: docFile });
|
|
1124
1131
|
// Second load with different content but same title — Document entity
|
|
1125
1132
|
// already exists with entityType 'Document' and no observations,
|
|
1126
|
-
// so it gets silently skipped. But the index
|
|
1133
|
+
// so it gets silently skipped. But the index entities already exist
|
|
1127
1134
|
// with different observations, so it should error.
|
|
1128
1135
|
await fs.writeFile(docFile, 'Completely different content for dedup testing now.');
|
|
1129
1136
|
await expect(callTool(client, 'kb_load', { filePath: docFile })).rejects.toThrow(/already exists/);
|