rag-memory-epf-mcp 3.3.2 → 3.3.4
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 +46 -6
- package/dist/src/migrations/migrations.js +93 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1136,11 +1136,18 @@ class RAGKnowledgeGraphManager {
|
|
|
1136
1136
|
// Slicing token arrays at arbitrary boundaries can leave incomplete UTF-8
|
|
1137
1137
|
// prefix/suffix bytes, which TextDecoder replaces with U+FFFD (�). Trim the
|
|
1138
1138
|
// incomplete sequences at chunk boundaries; overlap covers the removed bytes.
|
|
1139
|
+
//
|
|
1140
|
+
// Each chunk records both token-space offsets (start_token/end_token from the
|
|
1141
|
+
// BPE encoder loop) and char-space offsets (start_pos/end_pos into the original
|
|
1142
|
+
// text). Char offsets are recovered with indexOf using a running cursor at the
|
|
1143
|
+
// previous chunk's start, so substr(text, start_pos, end_pos - start_pos)
|
|
1144
|
+
// equals chunk.text. On a coincidental indexOf miss the char offsets are NULL.
|
|
1139
1145
|
chunkText(text, maxTokens = 800, overlap = 160) {
|
|
1140
1146
|
if (!this.encoding)
|
|
1141
1147
|
throw new Error('Tokenizer not initialized');
|
|
1142
1148
|
const tokens = this.encoding.encode(text);
|
|
1143
1149
|
const chunks = [];
|
|
1150
|
+
let charCursor = 0;
|
|
1144
1151
|
for (let i = 0; i < tokens.length; i += maxTokens - overlap) {
|
|
1145
1152
|
const chunkTokens = tokens.slice(i, i + maxTokens);
|
|
1146
1153
|
const decodedBytes = this.encoding.decode(chunkTokens);
|
|
@@ -1148,13 +1155,40 @@ class RAGKnowledgeGraphManager {
|
|
|
1148
1155
|
const isLast = i + chunkTokens.length >= tokens.length;
|
|
1149
1156
|
const safeBytes = trimIncompleteUtf8(decodedBytes, !isFirst, !isLast);
|
|
1150
1157
|
const chunkText = new TextDecoder('utf-8').decode(safeBytes);
|
|
1158
|
+
// Recover char-space offsets. First chunk is anchored at 0 because
|
|
1159
|
+
// trimIncompleteUtf8 leaves the leading bytes intact when isFirst is true.
|
|
1160
|
+
let startPos;
|
|
1161
|
+
let endPos;
|
|
1162
|
+
if (isFirst) {
|
|
1163
|
+
startPos = 0;
|
|
1164
|
+
endPos = chunkText.length;
|
|
1165
|
+
charCursor = 0;
|
|
1166
|
+
}
|
|
1167
|
+
else if (chunkText.length === 0) {
|
|
1168
|
+
startPos = null;
|
|
1169
|
+
endPos = null;
|
|
1170
|
+
}
|
|
1171
|
+
else {
|
|
1172
|
+
const idx = text.indexOf(chunkText, charCursor);
|
|
1173
|
+
if (idx >= 0) {
|
|
1174
|
+
startPos = idx;
|
|
1175
|
+
endPos = idx + chunkText.length;
|
|
1176
|
+
charCursor = idx;
|
|
1177
|
+
}
|
|
1178
|
+
else {
|
|
1179
|
+
startPos = null;
|
|
1180
|
+
endPos = null;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1151
1183
|
chunks.push({
|
|
1152
1184
|
id: '',
|
|
1153
1185
|
document_id: '',
|
|
1154
1186
|
chunk_index: chunks.length,
|
|
1155
1187
|
text: chunkText,
|
|
1156
|
-
start_pos:
|
|
1157
|
-
end_pos:
|
|
1188
|
+
start_pos: startPos,
|
|
1189
|
+
end_pos: endPos,
|
|
1190
|
+
start_token: i,
|
|
1191
|
+
end_token: i + chunkTokens.length
|
|
1158
1192
|
});
|
|
1159
1193
|
}
|
|
1160
1194
|
return chunks;
|
|
@@ -1230,14 +1264,16 @@ class RAGKnowledgeGraphManager {
|
|
|
1230
1264
|
// Store chunk metadata (no embedding yet)
|
|
1231
1265
|
this.db.prepare(`
|
|
1232
1266
|
INSERT INTO chunk_metadata (
|
|
1233
|
-
chunk_id, document_id, chunk_index, text, start_pos, end_pos
|
|
1234
|
-
) VALUES (?, ?, ?, ?, ?, ?)
|
|
1235
|
-
`).run(chunkId, documentId, chunk.chunk_index, chunk.text, chunk.start_pos, chunk.end_pos);
|
|
1267
|
+
chunk_id, document_id, chunk_index, text, start_pos, end_pos, start_token, end_token
|
|
1268
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1269
|
+
`).run(chunkId, documentId, chunk.chunk_index, chunk.text, chunk.start_pos, chunk.end_pos, chunk.start_token, chunk.end_token);
|
|
1236
1270
|
resultChunks.push({
|
|
1237
1271
|
id: chunkId,
|
|
1238
1272
|
text: chunk.text,
|
|
1239
1273
|
startPos: chunk.start_pos,
|
|
1240
|
-
endPos: chunk.end_pos
|
|
1274
|
+
endPos: chunk.end_pos,
|
|
1275
|
+
startToken: chunk.start_token,
|
|
1276
|
+
endToken: chunk.end_token
|
|
1241
1277
|
});
|
|
1242
1278
|
}
|
|
1243
1279
|
console.error(`✅ Document chunked: ${chunks.length} chunks created`);
|
|
@@ -1713,6 +1749,8 @@ class RAGKnowledgeGraphManager {
|
|
|
1713
1749
|
m.text,
|
|
1714
1750
|
m.start_pos,
|
|
1715
1751
|
m.end_pos,
|
|
1752
|
+
m.start_token,
|
|
1753
|
+
m.end_token,
|
|
1716
1754
|
COALESCE(m.metadata, '{}') as chunk_metadata,
|
|
1717
1755
|
c.distance,
|
|
1718
1756
|
COALESCE(d.metadata, '{}') as doc_metadata
|
|
@@ -1793,6 +1831,8 @@ class RAGKnowledgeGraphManager {
|
|
|
1793
1831
|
cm.text,
|
|
1794
1832
|
cm.start_pos,
|
|
1795
1833
|
cm.end_pos,
|
|
1834
|
+
cm.start_token,
|
|
1835
|
+
cm.end_token,
|
|
1796
1836
|
COALESCE(cm.metadata, '{}') as chunk_metadata,
|
|
1797
1837
|
COALESCE(d.metadata, '{}') as doc_metadata
|
|
1798
1838
|
FROM chunk_metadata cm
|
|
@@ -427,5 +427,98 @@ export const migrations = [
|
|
|
427
427
|
db.exec(`DROP TABLE IF EXISTS entities_fts`);
|
|
428
428
|
db.exec(`DROP TABLE IF EXISTS chunks_fts`);
|
|
429
429
|
}
|
|
430
|
+
},
|
|
431
|
+
// Migration 10: Separate token-space vs char-space chunk offsets.
|
|
432
|
+
// (Slot 9 is intentionally skipped — some user databases from early v3.x
|
|
433
|
+
// experiments have an unrelated migration recorded at version 9 (Ollama
|
|
434
|
+
// dimension swap). Reusing that slot would silently no-op against those
|
|
435
|
+
// databases. Version 10 ensures the migration runs everywhere.)
|
|
436
|
+
// Before this migration, chunk_metadata.start_pos/end_pos held *token* indices
|
|
437
|
+
// for document chunks (a leftover from the BPE tokenizer-based chunkText loop)
|
|
438
|
+
// but already held character lengths (0..text.length) for entity/relationship
|
|
439
|
+
// chunks. Same column, two meanings — and a column name (`*_pos`) that implies
|
|
440
|
+
// char offsets in `documents.content`. This migration adds explicit
|
|
441
|
+
// start_token/end_token columns and reinterprets start_pos/end_pos as character
|
|
442
|
+
// offsets going forward. Existing document chunks: token data is moved to the
|
|
443
|
+
// new columns and char offsets are recomputed from documents.content via
|
|
444
|
+
// indexOf with a running cursor (NULL on miss — caller can re-chunk to fill).
|
|
445
|
+
// Existing entity/relationship chunks: leave start_pos/end_pos as-is
|
|
446
|
+
// (already a valid 0..text.length char range against the chunk text itself);
|
|
447
|
+
// token columns stay NULL since these chunks have no token-space concept.
|
|
448
|
+
{
|
|
449
|
+
version: 10,
|
|
450
|
+
description: 'Add start_token/end_token; reinterpret start_pos/end_pos as char offsets',
|
|
451
|
+
up: (db) => {
|
|
452
|
+
// 1) Add columns (idempotent — some databases may have been touched by a
|
|
453
|
+
// pre-release v9 attempt; tolerate the column already existing).
|
|
454
|
+
const cols = db.prepare(`PRAGMA table_info(chunk_metadata)`).all()
|
|
455
|
+
.map(c => c.name);
|
|
456
|
+
if (!cols.includes('start_token')) {
|
|
457
|
+
db.exec(`ALTER TABLE chunk_metadata ADD COLUMN start_token INTEGER`);
|
|
458
|
+
}
|
|
459
|
+
if (!cols.includes('end_token')) {
|
|
460
|
+
db.exec(`ALTER TABLE chunk_metadata ADD COLUMN end_token INTEGER`);
|
|
461
|
+
}
|
|
462
|
+
// 2) Move token data into new columns for document chunks (only if not
|
|
463
|
+
// already moved — guard against re-running in the rare case a prior
|
|
464
|
+
// partial run already touched some rows).
|
|
465
|
+
db.exec(`
|
|
466
|
+
UPDATE chunk_metadata
|
|
467
|
+
SET start_token = start_pos,
|
|
468
|
+
end_token = end_pos,
|
|
469
|
+
start_pos = NULL,
|
|
470
|
+
end_pos = NULL
|
|
471
|
+
WHERE chunk_type = 'document'
|
|
472
|
+
AND start_token IS NULL
|
|
473
|
+
AND start_pos IS NOT NULL
|
|
474
|
+
`);
|
|
475
|
+
// 3) Recompute char offsets via indexOf with a running cursor per document
|
|
476
|
+
const docRows = db.prepare(`
|
|
477
|
+
SELECT DISTINCT document_id FROM chunk_metadata
|
|
478
|
+
WHERE chunk_type = 'document' AND document_id IS NOT NULL
|
|
479
|
+
`).all();
|
|
480
|
+
const docContentStmt = db.prepare(`SELECT content FROM documents WHERE id = ?`);
|
|
481
|
+
const chunksStmt = db.prepare(`
|
|
482
|
+
SELECT rowid, text FROM chunk_metadata
|
|
483
|
+
WHERE document_id = ? AND chunk_type = 'document'
|
|
484
|
+
ORDER BY chunk_index ASC
|
|
485
|
+
`);
|
|
486
|
+
const updateStmt = db.prepare(`
|
|
487
|
+
UPDATE chunk_metadata SET start_pos = ?, end_pos = ? WHERE rowid = ?
|
|
488
|
+
`);
|
|
489
|
+
for (const { document_id } of docRows) {
|
|
490
|
+
const doc = docContentStmt.get(document_id);
|
|
491
|
+
if (!doc)
|
|
492
|
+
continue;
|
|
493
|
+
const content = doc.content;
|
|
494
|
+
const chunks = chunksStmt.all(document_id);
|
|
495
|
+
// Cursor advances by the previous chunk's *start*, not its end, so we can
|
|
496
|
+
// still locate overlapping chunks. Token-space stride guarantees each
|
|
497
|
+
// chunk's start is strictly forward of the previous chunk's start.
|
|
498
|
+
let cursor = 0;
|
|
499
|
+
for (const c of chunks) {
|
|
500
|
+
if (!c.text)
|
|
501
|
+
continue;
|
|
502
|
+
const idx = content.indexOf(c.text, cursor);
|
|
503
|
+
if (idx >= 0) {
|
|
504
|
+
updateStmt.run(idx, idx + c.text.length, c.rowid);
|
|
505
|
+
cursor = idx;
|
|
506
|
+
}
|
|
507
|
+
// miss: leave NULL — caller can re-chunk to repair
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
down: (db) => {
|
|
512
|
+
// SQLite cannot DROP COLUMN cleanly. Best-effort: copy token data back into
|
|
513
|
+
// start_pos/end_pos for document chunks so a downgrade leaves the legacy
|
|
514
|
+
// token-space semantics in place.
|
|
515
|
+
db.exec(`
|
|
516
|
+
UPDATE chunk_metadata
|
|
517
|
+
SET start_pos = start_token, end_pos = end_token
|
|
518
|
+
WHERE chunk_type = 'document' AND start_token IS NOT NULL
|
|
519
|
+
`);
|
|
520
|
+
// start_token/end_token columns remain (no DROP COLUMN); they will be
|
|
521
|
+
// ignored by older code.
|
|
522
|
+
}
|
|
430
523
|
}
|
|
431
524
|
];
|