rag-memory-epf-mcp 5.2.0 → 5.3.0
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/README.md +8 -3
- package/dist/index.js +15 -2
- package/dist/src/migrations/migrations.js +10 -1
- package/dist/src/tools/knowledge-graph-tools.js +15 -14
- package/docs/UPDATING.md +24 -0
- package/package.json +2 -2
- package/dist/src/chunkText.d.ts +0 -10
- package/dist/src/chunkText.js +0 -104
package/README.md
CHANGED
|
@@ -10,9 +10,9 @@ A **project-local RAG memory** MCP server — knowledge graph + multilingual vec
|
|
|
10
10
|
## Key Features
|
|
11
11
|
|
|
12
12
|
- **Project-local isolation** — each project gets its own `.memory/rag-memory.db`. Multiple projects run simultaneously without interference.
|
|
13
|
-
- **
|
|
13
|
+
- **Hybrid search** — vector similarity (bge-m3, 1024-dim) + FTS5 BM25 keyword matching (RRF-fused). Knowledge-graph re-ranking is an **opt-in legacy/experimental** signal since v5.3.0 (measured to hurt known-item retrieval)
|
|
14
14
|
- **100+ languages** — Korean, Chinese, Japanese, Arabic, and more. Cross-lingual search works out of the box.
|
|
15
|
-
- **Graph-
|
|
15
|
+
- **Graph re-ranker (opt-in)** — per-entity geometric decay (0.5^i) with a hard cap 0.4; the cap bounds the boost but was measured (2026-08-17) to let heavily-linked chunks saturate it and outrank the exact chunk — hence off by default
|
|
16
16
|
- **38 MCP tools** — knowledge graph CRUD, observation lifecycle (correct / retract / history), document pipeline, hybrid search, multi-hop traversal, graph analytics (centrality / community detection / structure), export/import, temporal queries
|
|
17
17
|
- **Observations that hold their history** — corrections supersede instead of overwrite, search returns only current facts, and every revision keeps its provenance
|
|
18
18
|
- **Structure-anchored chunking (c1, v5)** — boundaries anchor to markdown structure (fence-aware, H1–H4 first, block-greedy, exact-token fallback), so editing the top of a file no longer re-embeds the whole document: unchanged text reuses its stored vectors at sync time. Chunk offsets are Unicode codepoints, language-neutral across SQL `substr`, Python slicing, and JS `[...str]` iteration; a publish-time invariant gate locks the gap-free partition. `overlap` is retired (omit or 0).
|
|
@@ -77,7 +77,7 @@ Place this `.mcp.json` in each project folder with its own `DB_FILE_PATH`. Each
|
|
|
77
77
|
### Search & Retrieval (9)
|
|
78
78
|
| Tool | Description | Annotation |
|
|
79
79
|
|------|------------|------------|
|
|
80
|
-
| `hybridSearch` | Vector + FTS5 BM25
|
|
80
|
+
| `hybridSearch` | Vector + FTS5 BM25, plus an **opt-in** graph re-ranker (`useGraph: true`; default off since v5.3.0). Degrades to FTS5-only (`search_mode`) when the embedding model is down | readOnly |
|
|
81
81
|
| `searchNodes` | Semantic entity search with `since`/`until` temporal filtering | readOnly |
|
|
82
82
|
| `openNodes` | Retrieve specific entities by name | readOnly |
|
|
83
83
|
| `readGraph` | Get complete knowledge graph | readOnly |
|
|
@@ -153,6 +153,11 @@ storeDocument(id, content, metadata)
|
|
|
153
153
|
|
|
154
154
|
## Changelog
|
|
155
155
|
|
|
156
|
+
### v5.3.0
|
|
157
|
+
- (Published first as `5.3.0-rc.1` on the `next` dist-tag; promoted to `latest` after a canary run of the published artifact against a real project database: default call carries no `graph_boost` and equals explicit `useGraph:false`, the known-item probe from the 2026-08-17 measurement returns the correct gotcha at rank 1, opt-in `true` still exposes `graph_boost`, schema/MCP defaults read `false`.)
|
|
158
|
+
- **Behavior change — `hybridSearch` graph re-ranking is now opt-in** (`useGraph` default `true` → `false`; tool schema, MCP exposure and the manager signature agree). Omitting the argument now means "no graph re-ranking" — a behavior change for callers that relied on the old default, hence a release-candidate first (`next` dist-tag, fleet canary) before stable. Measured 2026-08-17 on three real corpora (self-retrieval, usable samples 120/117/120, summaries off): with the additive graph boost on, the known-item chunk got worse in 46/49/52 samples and better in 3/2/0 (sign test p < 7e-11 per corpus), 106 targets left the top-10 entirely; reproduced on the summaries-on product path (HAL, 20 paired samples: hit@1 10→7, hit@5 18→13). Mechanism: only query-matched/connected entities score, but the per-entity boost saturates the cap quickly, so heavily-linked chunks can outrank the exact chunk even at `vector_similarity` 0. This is a harm-reduced default, not a validated graph improvement: the boost path is unchanged for `useGraph: true` (legacy/experimental re-ranker for back-compat and evaluation; the graph does not generate candidates — for relationship exploration use `openNodes` → `getNeighbors`). Regression lock: `test/search-graph-default.test.mjs`.
|
|
159
|
+
- (v5.0.0–v5.2.0 notes live in the git tags / `docs/UPDATING.md`.)
|
|
160
|
+
|
|
156
161
|
### v4.0.0
|
|
157
162
|
|
|
158
163
|
**Observation lifecycle (schema v13).** Observations used to be a JSON array of strings on the
|
package/dist/index.js
CHANGED
|
@@ -2923,7 +2923,19 @@ export class RAGKnowledgeGraphManager {
|
|
|
2923
2923
|
this.coordinator?.kick();
|
|
2924
2924
|
return { imported, skipped, observation_order_remap: remapReport };
|
|
2925
2925
|
}
|
|
2926
|
-
|
|
2926
|
+
// v5.3.0: the graph re-ranker is OPT-IN (harm-reduced default, not a validated improvement).
|
|
2927
|
+
// Measured 2026-08-17 on three real corpora (self-retrieval, usable samples 120/117/120,
|
|
2928
|
+
// summaries off): with the additive graph boost on, the known-item chunk got WORSE in
|
|
2929
|
+
// 46/49/52 samples and BETTER in 3/2/0 (sign test p < 7e-11 per corpus); 106 targets left the
|
|
2930
|
+
// top-10 entirely. Reproduced on the summaries-on product path (HAL, 20 paired samples:
|
|
2931
|
+
// hit@1 10 -> 7, hit@5 18 -> 13, worse 8 / better 3). Mechanism: only query-matched or
|
|
2932
|
+
// connected entities score, but the per-entity boost (0.5^i decay, cap 0.4) saturates fast, so
|
|
2933
|
+
// heavily-linked chunks get more chances to match and reach the cap — they can outrank the
|
|
2934
|
+
// exact chunk even at vector_similarity 0. Note the graph does not generate candidates: it only
|
|
2935
|
+
// re-orders the vector/FTS candidate pool, so useGraph:true is a legacy/experimental re-ranker
|
|
2936
|
+
// (backward compatibility, controlled evaluation), not a relationship-exploration path — that
|
|
2937
|
+
// contract is openNodes -> getNeighbors. The boost path itself is unchanged.
|
|
2938
|
+
async hybridSearch(query, limit = 5, useGraph = false) {
|
|
2927
2939
|
if (!this.db)
|
|
2928
2940
|
throw new Error('Database not initialized');
|
|
2929
2941
|
if (!this.encoding)
|
|
@@ -3870,7 +3882,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3870
3882
|
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.linkEntitiesToDocument(validatedArgs.documentId, validatedArgs.entityNames), null, 2) }] };
|
|
3871
3883
|
case "hybridSearch":
|
|
3872
3884
|
const limit = typeof validatedArgs.limit === 'number' ? validatedArgs.limit : 5;
|
|
3873
|
-
|
|
3885
|
+
// v5.3.0: graph is opt-in — only an explicit true enables the re-ranker (schema default false).
|
|
3886
|
+
const useGraph = validatedArgs.useGraph === true;
|
|
3874
3887
|
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.hybridSearch(validatedArgs.query, limit, useGraph), null, 2) }] };
|
|
3875
3888
|
case "getDetailedContext":
|
|
3876
3889
|
return { content: [{ type: "text", text: JSON.stringify(await ragKgManager.getDetailedContext(validatedArgs.chunkId, validatedArgs.includeSurrounding !== false), null, 2) }] };
|
|
@@ -9,7 +9,16 @@ export const migrations = [
|
|
|
9
9
|
version: 1,
|
|
10
10
|
description: 'Complete RAG Knowledge Graph schema - all tables and features',
|
|
11
11
|
up: (db) => {
|
|
12
|
-
//
|
|
12
|
+
// ⚠ 이 줄은 **아무 일도 하지 않는다** (2026-08-11 실측). migration-manager 가 각
|
|
13
|
+
// 마이그레이션을 `db.transaction(...)` 으로 감싸는데, SQLite 에서 `PRAGMA foreign_keys`
|
|
14
|
+
// 는 **트랜잭션 안에서 no-op** 이기 때문이다. 실측 = 신규 DB 에 13개 마이그레이션을
|
|
15
|
+
// 전부 적용한 뒤에도 `foreign_keys = 1`.
|
|
16
|
+
//
|
|
17
|
+
// **고치지 말 것.** 이 줄이 실제로 동작하게 만들면(트랜잭션 밖으로 빼는 등) FK 는
|
|
18
|
+
// 마이그레이션 이후 **그 프로세스 수명 내내 꺼진 채로 남는다** — 부팅 게이트(index.ts)
|
|
19
|
+
// 는 `runMigrations()` **앞**에서 판정하므로 그걸 못 잡는다. 그 상태에서 entity 를
|
|
20
|
+
// 지우면 observation 계열이 CASCADE 되지 않아 고아·FK 위반이 쌓인다.
|
|
21
|
+
// 회귀 잠금 = test/observation-cascade.test.mjs T25(신규 DB 부팅 후 FK==1).
|
|
13
22
|
db.pragma('foreign_keys = OFF');
|
|
14
23
|
// Original entities table (enhanced)
|
|
15
24
|
db.exec(`
|
|
@@ -292,22 +292,24 @@ const hybridSearchCapability = {
|
|
|
292
292
|
},
|
|
293
293
|
useGraph: {
|
|
294
294
|
type: 'boolean',
|
|
295
|
-
description: '
|
|
296
|
-
default:
|
|
295
|
+
description: 'Opt-in legacy/experimental graph re-ranker (default false since v5.3.0 — measured to hurt known-item retrieval). It only re-orders the vector/FTS candidate pool; for relationship exploration use openNodes -> getNeighbors instead',
|
|
296
|
+
default: false
|
|
297
297
|
}
|
|
298
298
|
},
|
|
299
299
|
required: ['query'],
|
|
300
300
|
},
|
|
301
301
|
};
|
|
302
302
|
const hybridSearchDescription = () => `<description>
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
303
|
+
Hybrid document search: vector similarity + FTS5 BM25 over document chunks, with an **opt-in** knowledge-graph re-ranker.
|
|
304
|
+
Default (useGraph false) is the harm-reduced default for finding a fact you know exists (known-item retrieval).
|
|
305
|
+
useGraph: true is a legacy/experimental re-ranker kept for backward compatibility and controlled evaluation — it does not
|
|
306
|
+
add candidates, it only re-orders the vector/FTS pool, and measured on three real corpora (2026-08-17) it pushed the exact
|
|
307
|
+
chunk out of the top-5 in ~40% of known-item queries. For relationship exploration use openNodes -> getNeighbors.
|
|
306
308
|
</description>
|
|
307
309
|
|
|
308
310
|
<importantNotes>
|
|
309
|
-
- (!important!) **
|
|
310
|
-
- (!important!) Graph
|
|
311
|
+
- (!important!) **Graph re-ranking is opt-in (default false since v5.3.0)** — it re-orders candidates by matched/connected entity links, which is measured to hurt known-item retrieval; enable it only for backward compatibility or controlled evaluation
|
|
312
|
+
- (!important!) Graph re-ranking does not generate candidates — for "what is connected to X" use openNodes -> getNeighbors
|
|
311
313
|
- (!important!) Results include similarity scores, graph boost, and hybrid rankings
|
|
312
314
|
- (!important!) **Best results when knowledge graph is well-populated** with entities and relationships
|
|
313
315
|
</importantNotes>
|
|
@@ -332,7 +334,7 @@ Perfect for complex queries that benefit from both content matching and conceptu
|
|
|
332
334
|
|
|
333
335
|
<bestPractices>
|
|
334
336
|
- Use natural language queries rather than keywords
|
|
335
|
-
-
|
|
337
|
+
- Keep graph re-ranking off for "find the fact I know exists"; for "what is connected to this" use openNodes -> getNeighbors, not useGraph
|
|
336
338
|
- Start with broader queries, then narrow down based on results
|
|
337
339
|
- Review entity associations to understand why results were selected
|
|
338
340
|
- Use appropriate limits based on your analysis needs
|
|
@@ -342,19 +344,18 @@ Perfect for complex queries that benefit from both content matching and conceptu
|
|
|
342
344
|
<parameters>
|
|
343
345
|
- query: Natural language search query (string, required)
|
|
344
346
|
- limit: Maximum results to return, default 5 (number, optional)
|
|
345
|
-
- useGraph: Enable knowledge graph
|
|
347
|
+
- useGraph: Enable knowledge graph re-ranking, default false / opt-in (boolean, optional)
|
|
346
348
|
</parameters>
|
|
347
349
|
|
|
348
350
|
<examples>
|
|
349
|
-
-
|
|
350
|
-
-
|
|
351
|
-
- Discovery
|
|
352
|
-
- Quick lookup: {"query": "quantum computing advantages", "limit": 3, "useGraph": false}
|
|
351
|
+
- Known-item lookup (default, graph off): {"query": "machine learning applications in healthcare", "limit": 10}
|
|
352
|
+
- Legacy re-ranker (opt-in, evaluation/back-compat only): {"query": "React performance optimization techniques", "useGraph": true}
|
|
353
|
+
- Discovery / relationship exploration: use openNodes then getNeighbors on the entity, not this tool's useGraph
|
|
353
354
|
</examples>`;
|
|
354
355
|
const hybridSearchSchema = {
|
|
355
356
|
query: z.string().describe('The search query to find relevant information'),
|
|
356
357
|
limit: z.number().optional().default(5).describe('Maximum number of results to return'),
|
|
357
|
-
useGraph: z.boolean().optional().default(
|
|
358
|
+
useGraph: z.boolean().optional().default(false).describe('Opt-in legacy/experimental graph re-ranker (default false since v5.3.0; measured to hurt known-item retrieval; for exploration use openNodes -> getNeighbors)'),
|
|
358
359
|
};
|
|
359
360
|
export const hybridSearchTool = {
|
|
360
361
|
capability: hybridSearchCapability,
|
package/docs/UPDATING.md
CHANGED
|
@@ -108,6 +108,30 @@ path and holder pid (e.g. `.download-<key>.lock`). Verify the holder process
|
|
|
108
108
|
is genuinely gone or hung (`ps -p <pid>`), then remove the lock file manually;
|
|
109
109
|
the next start becomes a clean download owner.
|
|
110
110
|
|
|
111
|
+
## v5.3.0 (schema v14, unchanged): `hybridSearch` graph re-ranking is opt-in
|
|
112
|
+
|
|
113
|
+
**What changed.** `useGraph` defaults to `false` (was `true`) — in the manager signature, the tool JSON
|
|
114
|
+
schema, the zod schema (`validateToolArgs` fills `false`) and the MCP dispatch (`=== true`). Nothing else
|
|
115
|
+
in the scoring path moved: `useGraph: true` runs exactly the pre-5.3 graph boost.
|
|
116
|
+
|
|
117
|
+
**Why.** Measured 2026-08-17 on three real corpora (self-retrieval, usable samples 120/117/120, summaries
|
|
118
|
+
off): with the additive graph boost on, the known-item chunk got WORSE in 46/49/52 samples and BETTER in
|
|
119
|
+
3/2/0 (paired sign test p < 7e-11 per corpus); 106 targets fell out of the top-10 entirely. Reproduced on
|
|
120
|
+
the summaries-on product path (HAL, 20 paired samples: hit@1 10→7, hit@5 18→13). Only query-matched or
|
|
121
|
+
connected entities score, but the per-entity boost (0.5^i decay, cap 0.4) saturates fast, so heavily-linked
|
|
122
|
+
chunks can outrank the exact chunk even at `vector_similarity` 0. This is a reversible harm mitigation, not
|
|
123
|
+
a validated graph improvement — the graph's role (candidate generation vs. re-ranking, explicit mode) is
|
|
124
|
+
still open and will be decided on a graph-required query suite. Note that `useGraph: true` does not add
|
|
125
|
+
candidates (it re-orders the vector/FTS pool): for relationship exploration use `openNodes` → `getNeighbors`.
|
|
126
|
+
|
|
127
|
+
**Fleet rollout.** No migration, no schema change. Published first as `5.3.0-rc.1` on the `next`
|
|
128
|
+
dist-tag (omitting an argument changes behavior, so it got a canary before `latest`): the published rc was
|
|
129
|
+
installed fresh and run against a real project database — default call had no `graph_boost` and matched
|
|
130
|
+
explicit `useGraph:false`, the known-item probe returned the right chunk at rank 1, opt-in `true` kept
|
|
131
|
+
`graph_boost`, schema/MCP defaults read `false` — then `5.3.0` was published to `latest`. A caller that
|
|
132
|
+
relied on graph re-ranking by default must now pass `useGraph: true`. Callers that already choose per query
|
|
133
|
+
see no change. Regression lock: `test/search-graph-default.test.mjs`.
|
|
134
|
+
|
|
111
135
|
## v5.1.0 (schema v14, unchanged): destructive-replace reporting + `excludePattern`
|
|
112
136
|
|
|
113
137
|
**What changes on upgrade**: nothing you have to do. No migration, no schema
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rag-memory-epf-mcp",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.0",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=24"
|
|
6
6
|
},
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"prepare": "npm run build",
|
|
46
46
|
"watch": "tsc --watch",
|
|
47
47
|
"verify:invariants": "node test/chunk-invariants.test.mjs",
|
|
48
|
-
"verify:engine": "node test/engine-smoke.test.mjs && node test/launch-smoke.test.mjs && node test/sync-atomicity.test.mjs && node test/dedup.test.mjs && node test/search-degradation.test.mjs && node test/entity-embed-cap.test.mjs && node test/migration12.test.mjs && node test/model-cache.test.mjs && node test/embedding-gate.test.mjs && node test/lazy-boot.test.mjs && node test/reconciliation.test.mjs && node test/backfill.test.mjs && node test/fts-query.test.mjs && node test/search-contracts.test.mjs && node test/tool-contracts.test.mjs && node test/bounded-exit.test.mjs && node test/observation-schema.test.mjs && node test/observation-migration.test.mjs && node test/observation-lifecycle.test.mjs && node test/observation-contracts.test.mjs && node test/observation-search.test.mjs && node test/observation-cascade.test.mjs && node test/observation-realdata.test.mjs && node test/chunker-c.test.mjs && node test/migration14.test.mjs && node test/chunk-params-validation.test.mjs && node test/vector-reuse.test.mjs && node test/entity-range-linking.test.mjs && node test/stats-chunking.test.mjs && node test/migration14-realdata.test.mjs && node test/migration14-realdata-sync.test.mjs && node test/search-summaries-off.test.mjs && node test/document-return-contracts.test.mjs && node test/observation-date-prefix.test.mjs",
|
|
48
|
+
"verify:engine": "node test/engine-smoke.test.mjs && node test/launch-smoke.test.mjs && node test/sync-atomicity.test.mjs && node test/dedup.test.mjs && node test/search-degradation.test.mjs && node test/entity-embed-cap.test.mjs && node test/migration12.test.mjs && node test/model-cache.test.mjs && node test/embedding-gate.test.mjs && node test/lazy-boot.test.mjs && node test/reconciliation.test.mjs && node test/backfill.test.mjs && node test/fts-query.test.mjs && node test/search-contracts.test.mjs && node test/tool-contracts.test.mjs && node test/bounded-exit.test.mjs && node test/observation-schema.test.mjs && node test/search-graph-default.test.mjs && node test/observation-migration.test.mjs && node test/observation-lifecycle.test.mjs && node test/observation-contracts.test.mjs && node test/observation-search.test.mjs && node test/observation-cascade.test.mjs && node test/observation-realdata.test.mjs && node test/chunker-c.test.mjs && node test/migration14.test.mjs && node test/chunk-params-validation.test.mjs && node test/vector-reuse.test.mjs && node test/entity-range-linking.test.mjs && node test/stats-chunking.test.mjs && node test/migration14-realdata.test.mjs && node test/migration14-realdata-sync.test.mjs && node test/search-summaries-off.test.mjs && node test/document-return-contracts.test.mjs && node test/observation-date-prefix.test.mjs && node test/delete-entities-cascade.test.mjs",
|
|
49
49
|
"test": "npm run build && npm run verify:invariants && npm run verify:engine",
|
|
50
50
|
"prepublishOnly": "npm run build && npm run verify:invariants && npm run verify:engine"
|
|
51
51
|
},
|
package/dist/src/chunkText.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { Tiktoken } from 'tiktoken';
|
|
2
|
-
export interface ChunkSegment {
|
|
3
|
-
text: string;
|
|
4
|
-
start_pos: number | null;
|
|
5
|
-
end_pos: number | null;
|
|
6
|
-
start_token: number;
|
|
7
|
-
end_token: number;
|
|
8
|
-
}
|
|
9
|
-
export declare function trimIncompleteUtf8(bytes: Uint8Array, trimHead: boolean, trimTail: boolean): Uint8Array;
|
|
10
|
-
export declare function chunkText(text: string, encoding: Tiktoken, maxTokens?: number, overlap?: number): ChunkSegment[];
|
package/dist/src/chunkText.js
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
// Tokenize and chunk text using a BPE encoder while reporting both token-space
|
|
2
|
-
// and char-space (Unicode codepoint) offsets back into the original string.
|
|
3
|
-
//
|
|
4
|
-
// BPE tokenizers (cl100k_base) split multi-byte UTF-8 sequences across tokens.
|
|
5
|
-
// Slicing token arrays at arbitrary boundaries can leave incomplete UTF-8
|
|
6
|
-
// prefix/suffix bytes, which TextDecoder replaces with U+FFFD (�). We trim the
|
|
7
|
-
// incomplete sequences at chunk boundaries; overlap covers the removed bytes.
|
|
8
|
-
//
|
|
9
|
-
// Each chunk records both token-space offsets (start_token/end_token from the
|
|
10
|
-
// BPE encoder loop) and char-space offsets (start_pos/end_pos into the original
|
|
11
|
-
// text). Char offsets are Unicode codepoint counts — language-neutral, so SQL
|
|
12
|
-
// substr, Python str slicing, and JS [...str] iteration all line up. JS's
|
|
13
|
-
// native UTF-16 indexing differs for supplementary characters (emoji, rare CJK),
|
|
14
|
-
// so the function maintains parallel UTF-16 and codepoint cursors and reports
|
|
15
|
-
// codepoint offsets. On a coincidental indexOf miss the char offsets are NULL.
|
|
16
|
-
//
|
|
17
|
-
// Extracted to a standalone module so publish-time invariant tests can exercise
|
|
18
|
-
// the algorithm directly without booting the full RAG-Memory stack.
|
|
19
|
-
// trimIncompleteUtf8: strip incomplete UTF-8 sequences from the head/tail of a
|
|
20
|
-
// byte buffer produced by decoding an arbitrary token slice. A multi-byte
|
|
21
|
-
// codepoint that begins or ends on the cut edge belongs to an adjacent chunk
|
|
22
|
-
// and must be removed so TextDecoder does not emit U+FFFD. Pass
|
|
23
|
-
// trimHead/trimTail=false to preserve head/tail bytes (first/last chunks).
|
|
24
|
-
export function trimIncompleteUtf8(bytes, trimHead, trimTail) {
|
|
25
|
-
let start = 0;
|
|
26
|
-
let end = bytes.length;
|
|
27
|
-
if (trimHead) {
|
|
28
|
-
while (start < end && (bytes[start] & 0xC0) === 0x80)
|
|
29
|
-
start++;
|
|
30
|
-
}
|
|
31
|
-
if (trimTail) {
|
|
32
|
-
let i = end - 1;
|
|
33
|
-
while (i >= start && (bytes[i] & 0xC0) === 0x80)
|
|
34
|
-
i--;
|
|
35
|
-
if (i >= start) {
|
|
36
|
-
const lead = bytes[i];
|
|
37
|
-
let needed = 1;
|
|
38
|
-
if ((lead & 0x80) === 0)
|
|
39
|
-
needed = 1;
|
|
40
|
-
else if ((lead & 0xE0) === 0xC0)
|
|
41
|
-
needed = 2;
|
|
42
|
-
else if ((lead & 0xF0) === 0xE0)
|
|
43
|
-
needed = 3;
|
|
44
|
-
else if ((lead & 0xF8) === 0xF0)
|
|
45
|
-
needed = 4;
|
|
46
|
-
if (end - i < needed)
|
|
47
|
-
end = i;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return bytes.subarray(start, end);
|
|
51
|
-
}
|
|
52
|
-
export function chunkText(text, encoding, maxTokens = 800, overlap = 160) {
|
|
53
|
-
const tokens = encoding.encode(text);
|
|
54
|
-
const segments = [];
|
|
55
|
-
let utf16Cursor = 0;
|
|
56
|
-
let cpCursor = 0;
|
|
57
|
-
for (let i = 0; i < tokens.length; i += maxTokens - overlap) {
|
|
58
|
-
const chunkTokens = tokens.slice(i, i + maxTokens);
|
|
59
|
-
const decodedBytes = encoding.decode(chunkTokens);
|
|
60
|
-
const isFirst = i === 0;
|
|
61
|
-
const isLast = i + chunkTokens.length >= tokens.length;
|
|
62
|
-
const safeBytes = trimIncompleteUtf8(decodedBytes, !isFirst, !isLast);
|
|
63
|
-
const chunkTextStr = new TextDecoder('utf-8').decode(safeBytes);
|
|
64
|
-
let startPos;
|
|
65
|
-
let endPos;
|
|
66
|
-
if (isFirst) {
|
|
67
|
-
startPos = 0;
|
|
68
|
-
endPos = [...chunkTextStr].length;
|
|
69
|
-
utf16Cursor = 0;
|
|
70
|
-
cpCursor = 0;
|
|
71
|
-
}
|
|
72
|
-
else if (chunkTextStr.length === 0) {
|
|
73
|
-
startPos = null;
|
|
74
|
-
endPos = null;
|
|
75
|
-
}
|
|
76
|
-
else {
|
|
77
|
-
const utfIdx = text.indexOf(chunkTextStr, utf16Cursor);
|
|
78
|
-
if (utfIdx >= 0) {
|
|
79
|
-
// Advance cpCursor by codepoints between the previous cursor and the
|
|
80
|
-
// new chunk's start (handles overlap by anchoring at the previous
|
|
81
|
-
// chunk's start, not its end).
|
|
82
|
-
if (utfIdx > utf16Cursor) {
|
|
83
|
-
cpCursor += [...text.slice(utf16Cursor, utfIdx)].length;
|
|
84
|
-
utf16Cursor = utfIdx;
|
|
85
|
-
}
|
|
86
|
-
const cpLen = [...chunkTextStr].length;
|
|
87
|
-
startPos = cpCursor;
|
|
88
|
-
endPos = cpCursor + cpLen;
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
startPos = null;
|
|
92
|
-
endPos = null;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
segments.push({
|
|
96
|
-
text: chunkTextStr,
|
|
97
|
-
start_pos: startPos,
|
|
98
|
-
end_pos: endPos,
|
|
99
|
-
start_token: i,
|
|
100
|
-
end_token: i + chunkTokens.length
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
return segments;
|
|
104
|
-
}
|