abb-opencode-local-rag 0.1.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/LICENSE +21 -0
- package/README.de.md +416 -0
- package/README.es.md +416 -0
- package/README.fr.md +416 -0
- package/README.md +491 -0
- package/README.pt-BR.md +416 -0
- package/README.zh-CN.md +416 -0
- package/dist/bin/install-skills.d.ts +20 -0
- package/dist/bin/install-skills.d.ts.map +1 -0
- package/dist/bin/install-skills.js +195 -0
- package/dist/bin/install-skills.js.map +1 -0
- package/dist/chunker/index.d.ts +24 -0
- package/dist/chunker/index.d.ts.map +1 -0
- package/dist/chunker/index.js +2 -0
- package/dist/chunker/index.js.map +1 -0
- package/dist/chunker/semantic-chunker.d.ts +97 -0
- package/dist/chunker/semantic-chunker.d.ts.map +1 -0
- package/dist/chunker/semantic-chunker.js +294 -0
- package/dist/chunker/semantic-chunker.js.map +1 -0
- package/dist/chunker/sentence-splitter.d.ts +28 -0
- package/dist/chunker/sentence-splitter.d.ts.map +1 -0
- package/dist/chunker/sentence-splitter.js +219 -0
- package/dist/chunker/sentence-splitter.js.map +1 -0
- package/dist/cli/common.d.ts +65 -0
- package/dist/cli/common.d.ts.map +1 -0
- package/dist/cli/common.js +138 -0
- package/dist/cli/common.js.map +1 -0
- package/dist/cli/delete.d.ts +8 -0
- package/dist/cli/delete.d.ts.map +1 -0
- package/dist/cli/delete.js +173 -0
- package/dist/cli/delete.js.map +1 -0
- package/dist/cli/file-collection.d.ts +2 -0
- package/dist/cli/file-collection.d.ts.map +1 -0
- package/dist/cli/file-collection.js +53 -0
- package/dist/cli/file-collection.js.map +1 -0
- package/dist/cli/ingest.d.ts +100 -0
- package/dist/cli/ingest.d.ts.map +1 -0
- package/dist/cli/ingest.js +363 -0
- package/dist/cli/ingest.js.map +1 -0
- package/dist/cli/list.d.ts +35 -0
- package/dist/cli/list.d.ts.map +1 -0
- package/dist/cli/list.js +210 -0
- package/dist/cli/list.js.map +1 -0
- package/dist/cli/options.d.ts +100 -0
- package/dist/cli/options.d.ts.map +1 -0
- package/dist/cli/options.js +241 -0
- package/dist/cli/options.js.map +1 -0
- package/dist/cli/query.d.ts +24 -0
- package/dist/cli/query.d.ts.map +1 -0
- package/dist/cli/query.js +191 -0
- package/dist/cli/query.js.map +1 -0
- package/dist/cli/read-neighbors.d.ts +11 -0
- package/dist/cli/read-neighbors.d.ts.map +1 -0
- package/dist/cli/read-neighbors.js +224 -0
- package/dist/cli/read-neighbors.js.map +1 -0
- package/dist/cli/status.d.ts +8 -0
- package/dist/cli/status.d.ts.map +1 -0
- package/dist/cli/status.js +80 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/cli/sync.d.ts +8 -0
- package/dist/cli/sync.d.ts.map +1 -0
- package/dist/cli/sync.js +244 -0
- package/dist/cli/sync.js.map +1 -0
- package/dist/cli-main.d.ts +12 -0
- package/dist/cli-main.d.ts.map +1 -0
- package/dist/cli-main.js +63 -0
- package/dist/cli-main.js.map +1 -0
- package/dist/embedder/index.d.ts +85 -0
- package/dist/embedder/index.d.ts.map +1 -0
- package/dist/embedder/index.js +284 -0
- package/dist/embedder/index.js.map +1 -0
- package/dist/features/list.d.ts +37 -0
- package/dist/features/list.d.ts.map +1 -0
- package/dist/features/list.js +40 -0
- package/dist/features/list.js.map +1 -0
- package/dist/features/sync.d.ts +207 -0
- package/dist/features/sync.d.ts.map +1 -0
- package/dist/features/sync.js +380 -0
- package/dist/features/sync.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest/compute.d.ts +86 -0
- package/dist/ingest/compute.d.ts.map +1 -0
- package/dist/ingest/compute.js +177 -0
- package/dist/ingest/compute.js.map +1 -0
- package/dist/ingest/file.d.ts +27 -0
- package/dist/ingest/file.d.ts.map +1 -0
- package/dist/ingest/file.js +67 -0
- package/dist/ingest/file.js.map +1 -0
- package/dist/ingest/visual.d.ts +45 -0
- package/dist/ingest/visual.d.ts.map +1 -0
- package/dist/ingest/visual.js +234 -0
- package/dist/ingest/visual.js.map +1 -0
- package/dist/parser/docx-parser.d.ts +12 -0
- package/dist/parser/docx-parser.d.ts.map +1 -0
- package/dist/parser/docx-parser.js +328 -0
- package/dist/parser/docx-parser.js.map +1 -0
- package/dist/parser/html-parser.d.ts +18 -0
- package/dist/parser/html-parser.d.ts.map +1 -0
- package/dist/parser/html-parser.js +102 -0
- package/dist/parser/html-parser.js.map +1 -0
- package/dist/parser/index.d.ts +214 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +454 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/pdf-extract.d.ts +81 -0
- package/dist/parser/pdf-extract.d.ts.map +1 -0
- package/dist/parser/pdf-extract.js +112 -0
- package/dist/parser/pdf-extract.js.map +1 -0
- package/dist/parser/pdf-filter.d.ts +117 -0
- package/dist/parser/pdf-filter.d.ts.map +1 -0
- package/dist/parser/pdf-filter.js +528 -0
- package/dist/parser/pdf-filter.js.map +1 -0
- package/dist/parser/title-extractor.d.ts +69 -0
- package/dist/parser/title-extractor.d.ts.map +1 -0
- package/dist/parser/title-extractor.js +145 -0
- package/dist/parser/title-extractor.js.map +1 -0
- package/dist/pdf-visual/captioner.d.ts +16 -0
- package/dist/pdf-visual/captioner.d.ts.map +1 -0
- package/dist/pdf-visual/captioner.js +63 -0
- package/dist/pdf-visual/captioner.js.map +1 -0
- package/dist/pdf-visual/captioners/fast.d.ts +7 -0
- package/dist/pdf-visual/captioners/fast.d.ts.map +1 -0
- package/dist/pdf-visual/captioners/fast.js +103 -0
- package/dist/pdf-visual/captioners/fast.js.map +1 -0
- package/dist/pdf-visual/captioners/quality.d.ts +7 -0
- package/dist/pdf-visual/captioners/quality.d.ts.map +1 -0
- package/dist/pdf-visual/captioners/quality.js +127 -0
- package/dist/pdf-visual/captioners/quality.js.map +1 -0
- package/dist/pdf-visual/captioners/shared.d.ts +44 -0
- package/dist/pdf-visual/captioners/shared.d.ts.map +1 -0
- package/dist/pdf-visual/captioners/shared.js +104 -0
- package/dist/pdf-visual/captioners/shared.js.map +1 -0
- package/dist/pdf-visual/detector.d.ts +9 -0
- package/dist/pdf-visual/detector.d.ts.map +1 -0
- package/dist/pdf-visual/detector.js +234 -0
- package/dist/pdf-visual/detector.js.map +1 -0
- package/dist/pdf-visual/index.d.ts +13 -0
- package/dist/pdf-visual/index.d.ts.map +1 -0
- package/dist/pdf-visual/index.js +45 -0
- package/dist/pdf-visual/index.js.map +1 -0
- package/dist/pdf-visual/renderer.d.ts +9 -0
- package/dist/pdf-visual/renderer.d.ts.map +1 -0
- package/dist/pdf-visual/renderer.js +177 -0
- package/dist/pdf-visual/renderer.js.map +1 -0
- package/dist/pdf-visual/types.d.ts +62 -0
- package/dist/pdf-visual/types.d.ts.map +1 -0
- package/dist/pdf-visual/types.js +32 -0
- package/dist/pdf-visual/types.js.map +1 -0
- package/dist/server/error-utils.d.ts +79 -0
- package/dist/server/error-utils.d.ts.map +1 -0
- package/dist/server/error-utils.js +148 -0
- package/dist/server/error-utils.js.map +1 -0
- package/dist/server/index.d.ts +258 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +1104 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/list-scanner.d.ts +52 -0
- package/dist/server/list-scanner.d.ts.map +1 -0
- package/dist/server/list-scanner.js +72 -0
- package/dist/server/list-scanner.js.map +1 -0
- package/dist/server/tool-definitions.d.ts +8 -0
- package/dist/server/tool-definitions.d.ts.map +1 -0
- package/dist/server/tool-definitions.js +181 -0
- package/dist/server/tool-definitions.js.map +1 -0
- package/dist/server/tool-input.d.ts +37 -0
- package/dist/server/tool-input.d.ts.map +1 -0
- package/dist/server/tool-input.js +216 -0
- package/dist/server/tool-input.js.map +1 -0
- package/dist/server/types.d.ts +331 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server/types.js +3 -0
- package/dist/server/types.js.map +1 -0
- package/dist/server-main.d.ts +46 -0
- package/dist/server-main.d.ts.map +1 -0
- package/dist/server-main.js +242 -0
- package/dist/server-main.js.map +1 -0
- package/dist/utils/base-dirs.d.ts +212 -0
- package/dist/utils/base-dirs.d.ts.map +1 -0
- package/dist/utils/base-dirs.js +422 -0
- package/dist/utils/base-dirs.js.map +1 -0
- package/dist/utils/errors.d.ts +24 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +53 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/limits.d.ts +26 -0
- package/dist/utils/limits.d.ts.map +1 -0
- package/dist/utils/limits.js +28 -0
- package/dist/utils/limits.js.map +1 -0
- package/dist/utils/list-sources.d.ts +47 -0
- package/dist/utils/list-sources.d.ts.map +1 -0
- package/dist/utils/list-sources.js +50 -0
- package/dist/utils/list-sources.js.map +1 -0
- package/dist/utils/raw-data-utils.d.ts +131 -0
- package/dist/utils/raw-data-utils.d.ts.map +1 -0
- package/dist/utils/raw-data-utils.js +255 -0
- package/dist/utils/raw-data-utils.js.map +1 -0
- package/dist/utils/scan.d.ts +126 -0
- package/dist/utils/scan.d.ts.map +1 -0
- package/dist/utils/scan.js +221 -0
- package/dist/utils/scan.js.map +1 -0
- package/dist/utils/scope-match.d.ts +43 -0
- package/dist/utils/scope-match.d.ts.map +1 -0
- package/dist/utils/scope-match.js +87 -0
- package/dist/utils/scope-match.js.map +1 -0
- package/dist/utils/sensitive-path.d.ts +23 -0
- package/dist/utils/sensitive-path.d.ts.map +1 -0
- package/dist/utils/sensitive-path.js +91 -0
- package/dist/utils/sensitive-path.js.map +1 -0
- package/dist/utils/sync-path-key.d.ts +20 -0
- package/dist/utils/sync-path-key.d.ts.map +1 -0
- package/dist/utils/sync-path-key.js +33 -0
- package/dist/utils/sync-path-key.js.map +1 -0
- package/dist/vectordb/index.d.ts +168 -0
- package/dist/vectordb/index.d.ts.map +1 -0
- package/dist/vectordb/index.js +619 -0
- package/dist/vectordb/index.js.map +1 -0
- package/dist/vectordb/search-filters.d.ts +39 -0
- package/dist/vectordb/search-filters.d.ts.map +1 -0
- package/dist/vectordb/search-filters.js +136 -0
- package/dist/vectordb/search-filters.js.map +1 -0
- package/dist/vectordb/types.d.ts +196 -0
- package/dist/vectordb/types.d.ts.map +1 -0
- package/dist/vectordb/types.js +224 -0
- package/dist/vectordb/types.js.map +1 -0
- package/package.json +105 -0
- package/skills/mcp-local-rag/SKILL.md +308 -0
- package/skills/mcp-local-rag/references/cli-reference.md +175 -0
- package/skills/mcp-local-rag/references/html-ingestion.md +78 -0
- package/skills/mcp-local-rag/references/query-optimization.md +57 -0
- package/skills/mcp-local-rag/references/result-refinement.md +56 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { AtomicTextRange, TextChunk } from './index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Semantic Chunker configuration
|
|
4
|
+
* Based on paper recommendations: hardThreshold=0.6, initConst=1.5, c=0.9
|
|
5
|
+
*/
|
|
6
|
+
export interface SemanticChunkerConfig {
|
|
7
|
+
/** Hard threshold for minimum similarity (default: 0.6) */
|
|
8
|
+
hardThreshold: number;
|
|
9
|
+
/** Initial constant for first sentence pair (default: 1.5) */
|
|
10
|
+
initConst: number;
|
|
11
|
+
/** Scaling constant for threshold calculation (default: 0.9) */
|
|
12
|
+
c: number;
|
|
13
|
+
/** Minimum chunk length in characters (default: 50) */
|
|
14
|
+
minChunkLength: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Embedder interface for generating embeddings
|
|
18
|
+
*/
|
|
19
|
+
export interface EmbedderInterface {
|
|
20
|
+
embedBatch(texts: string[]): Promise<number[][]>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Check if a chunk is garbage (should be filtered out)
|
|
24
|
+
*
|
|
25
|
+
* Criteria (language-agnostic):
|
|
26
|
+
* 1. Empty after trimming
|
|
27
|
+
* 2. Contains alphanumeric -> valid content (keep)
|
|
28
|
+
* 3. Only decoration characters (----, ====, etc.) -> garbage
|
|
29
|
+
* 4. Single character repeated >80% of text -> garbage
|
|
30
|
+
*
|
|
31
|
+
* Note: Applied after minChunkLength filter
|
|
32
|
+
*
|
|
33
|
+
* @param text - Chunk text to check
|
|
34
|
+
* @returns true if chunk is garbage and should be removed
|
|
35
|
+
*/
|
|
36
|
+
export declare function isGarbageChunk(text: string): boolean;
|
|
37
|
+
/** Default minimum chunk length in characters */
|
|
38
|
+
export declare const DEFAULT_MIN_CHUNK_LENGTH = 50;
|
|
39
|
+
/**
|
|
40
|
+
* Semantic chunker using Max-Min algorithm
|
|
41
|
+
*
|
|
42
|
+
* The algorithm groups consecutive sentences based on semantic similarity:
|
|
43
|
+
* 1. Split text into sentences
|
|
44
|
+
* 2. Generate embeddings for all sentences
|
|
45
|
+
* 3. For each sentence, decide whether to add to current chunk or start new chunk
|
|
46
|
+
* 4. Decision is based on comparing max similarity with new sentence vs min similarity within chunk
|
|
47
|
+
*
|
|
48
|
+
* Key insight: A sentence belongs to a chunk if its maximum similarity to any chunk member
|
|
49
|
+
* is greater than the minimum similarity between existing chunk members (with threshold adjustment)
|
|
50
|
+
*/
|
|
51
|
+
export declare class SemanticChunker {
|
|
52
|
+
private readonly config;
|
|
53
|
+
constructor(config?: Partial<SemanticChunkerConfig>);
|
|
54
|
+
/**
|
|
55
|
+
* Split text into semantically coherent chunks
|
|
56
|
+
*
|
|
57
|
+
* @param text - The text to chunk
|
|
58
|
+
* @param embedder - Embedder to generate sentence embeddings
|
|
59
|
+
* @returns Array of text chunks
|
|
60
|
+
*/
|
|
61
|
+
chunkText(text: string, embedder: EmbedderInterface, atomicRanges?: readonly AtomicTextRange[]): Promise<TextChunk[]>;
|
|
62
|
+
/**
|
|
63
|
+
* Group sentences into chunks using Max-Min algorithm
|
|
64
|
+
*/
|
|
65
|
+
private groupSentences;
|
|
66
|
+
/**
|
|
67
|
+
* Decide if a sentence should be added to the current chunk
|
|
68
|
+
* Based on Max-Min algorithm from the paper
|
|
69
|
+
*/
|
|
70
|
+
private shouldAddToChunk;
|
|
71
|
+
/**
|
|
72
|
+
* Get minimum pairwise similarity within a chunk.
|
|
73
|
+
* Only compares the last WINDOW_SIZE sentences for O(1) complexity.
|
|
74
|
+
* This approximation is valid because recent sentences are most relevant
|
|
75
|
+
* for determining chunk coherence (per Max-Min paper's experimental setup).
|
|
76
|
+
*/
|
|
77
|
+
private getMinSimilarity;
|
|
78
|
+
/**
|
|
79
|
+
* Get maximum similarity between a sentence and any sentence in the chunk
|
|
80
|
+
*/
|
|
81
|
+
private getMaxSimilarity;
|
|
82
|
+
/**
|
|
83
|
+
* Calculate dynamic threshold based on chunk size
|
|
84
|
+
* threshold = max(c * minSim * sigmoid(|C|), hardThreshold)
|
|
85
|
+
*/
|
|
86
|
+
private calculateThreshold;
|
|
87
|
+
/**
|
|
88
|
+
* Sigmoid function
|
|
89
|
+
*/
|
|
90
|
+
private sigmoid;
|
|
91
|
+
/**
|
|
92
|
+
* Calculate cosine similarity between two vectors
|
|
93
|
+
* Public for testing
|
|
94
|
+
*/
|
|
95
|
+
cosineSimilarity(vec1: number[], vec2: number[]): number;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=semantic-chunker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"semantic-chunker.d.ts","sourceRoot":"","sources":["../../src/chunker/semantic-chunker.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAO5D;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,2DAA2D;IAC3D,aAAa,EAAE,MAAM,CAAA;IACrB,8DAA8D;IAC9D,SAAS,EAAE,MAAM,CAAA;IACjB,gEAAgE;IAChE,CAAC,EAAE,MAAM,CAAA;IACT,uDAAuD;IACvD,cAAc,EAAE,MAAM,CAAA;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;CACjD;AAoBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAmBpD;AAMD,iDAAiD;AACjD,eAAO,MAAM,wBAAwB,KAAK,CAAA;AAa1C;;;;;;;;;;;GAWG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuB;IAE9C,YAAY,MAAM,GAAE,OAAO,CAAC,qBAAqB,CAAM,EAEtD;IAED;;;;;;OAMG;IACG,SAAS,CACb,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,iBAAiB,EAC3B,YAAY,GAAE,SAAS,eAAe,EAAO,GAC5C,OAAO,CAAC,SAAS,EAAE,CAAC,CAgDtB;IAED;;OAEG;IACH,OAAO,CAAC,cAAc;IAsEtB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAaxB;;;;;OAKG;IACH,OAAO,CAAC,gBAAgB;IAuBxB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAWxB;;;OAGG;IACH,OAAO,CAAC,kBAAkB;IAM1B;;OAEG;IACH,OAAO,CAAC,OAAO;IAIf;;;OAGG;IACH,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAqBvD;CACF"}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// Semantic Chunker implementation using Max-Min algorithm
|
|
2
|
+
// Based on: "Max–Min semantic chunking of documents for RAG application" (Springer, 2025)
|
|
3
|
+
import { splitIntoSentenceUnits } from './sentence-splitter.js';
|
|
4
|
+
// ============================================
|
|
5
|
+
// Performance Optimization Constants
|
|
6
|
+
// ============================================
|
|
7
|
+
/**
|
|
8
|
+
* Number of recent sentences to compare in getMinSimilarity.
|
|
9
|
+
* Based on Max-Min paper's experimental conditions (median 5 sentences per chunk).
|
|
10
|
+
* Reduces complexity from O(k²) to O(WINDOW_SIZE²) = O(25) = O(1).
|
|
11
|
+
*/
|
|
12
|
+
const WINDOW_SIZE = 5;
|
|
13
|
+
/**
|
|
14
|
+
* Maximum number of sentences per chunk before forced split.
|
|
15
|
+
* Safety limit to prevent computational explosion on homogeneous documents.
|
|
16
|
+
* Set to 3x the paper's median chunk size for reasonable margin.
|
|
17
|
+
*/
|
|
18
|
+
const MAX_SENTENCES = 15;
|
|
19
|
+
/**
|
|
20
|
+
* Check if a chunk is garbage (should be filtered out)
|
|
21
|
+
*
|
|
22
|
+
* Criteria (language-agnostic):
|
|
23
|
+
* 1. Empty after trimming
|
|
24
|
+
* 2. Contains alphanumeric -> valid content (keep)
|
|
25
|
+
* 3. Only decoration characters (----, ====, etc.) -> garbage
|
|
26
|
+
* 4. Single character repeated >80% of text -> garbage
|
|
27
|
+
*
|
|
28
|
+
* Note: Applied after minChunkLength filter
|
|
29
|
+
*
|
|
30
|
+
* @param text - Chunk text to check
|
|
31
|
+
* @returns true if chunk is garbage and should be removed
|
|
32
|
+
*/
|
|
33
|
+
export function isGarbageChunk(text) {
|
|
34
|
+
const trimmed = text.trim();
|
|
35
|
+
if (trimmed.length === 0)
|
|
36
|
+
return true;
|
|
37
|
+
// If contains any alphanumeric, consider valid content
|
|
38
|
+
if (/[a-zA-Z0-9]/.test(trimmed))
|
|
39
|
+
return false;
|
|
40
|
+
// Decoration line patterns only (----, ====, ****, etc.)
|
|
41
|
+
if (/^[-=_.*#|~`@!%^&*()[\]{}\\/<>:+\s]+$/.test(trimmed))
|
|
42
|
+
return true;
|
|
43
|
+
// Excessive repetition of single character (>80%)
|
|
44
|
+
const charCounts = new Map();
|
|
45
|
+
for (const char of trimmed) {
|
|
46
|
+
charCounts.set(char, (charCounts.get(char) ?? 0) + 1);
|
|
47
|
+
}
|
|
48
|
+
const maxCount = Math.max(...charCounts.values());
|
|
49
|
+
if (maxCount / trimmed.length > 0.8)
|
|
50
|
+
return true;
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
// ============================================
|
|
54
|
+
// Default Configuration
|
|
55
|
+
// ============================================
|
|
56
|
+
/** Default minimum chunk length in characters */
|
|
57
|
+
export const DEFAULT_MIN_CHUNK_LENGTH = 50;
|
|
58
|
+
const DEFAULT_SEMANTIC_CHUNKER_CONFIG = {
|
|
59
|
+
hardThreshold: 0.6,
|
|
60
|
+
initConst: 1.5,
|
|
61
|
+
c: 0.9,
|
|
62
|
+
minChunkLength: DEFAULT_MIN_CHUNK_LENGTH,
|
|
63
|
+
};
|
|
64
|
+
// ============================================
|
|
65
|
+
// SemanticChunker Class
|
|
66
|
+
// ============================================
|
|
67
|
+
/**
|
|
68
|
+
* Semantic chunker using Max-Min algorithm
|
|
69
|
+
*
|
|
70
|
+
* The algorithm groups consecutive sentences based on semantic similarity:
|
|
71
|
+
* 1. Split text into sentences
|
|
72
|
+
* 2. Generate embeddings for all sentences
|
|
73
|
+
* 3. For each sentence, decide whether to add to current chunk or start new chunk
|
|
74
|
+
* 4. Decision is based on comparing max similarity with new sentence vs min similarity within chunk
|
|
75
|
+
*
|
|
76
|
+
* Key insight: A sentence belongs to a chunk if its maximum similarity to any chunk member
|
|
77
|
+
* is greater than the minimum similarity between existing chunk members (with threshold adjustment)
|
|
78
|
+
*/
|
|
79
|
+
export class SemanticChunker {
|
|
80
|
+
config;
|
|
81
|
+
constructor(config = {}) {
|
|
82
|
+
this.config = { ...DEFAULT_SEMANTIC_CHUNKER_CONFIG, ...config };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Split text into semantically coherent chunks
|
|
86
|
+
*
|
|
87
|
+
* @param text - The text to chunk
|
|
88
|
+
* @param embedder - Embedder to generate sentence embeddings
|
|
89
|
+
* @returns Array of text chunks
|
|
90
|
+
*/
|
|
91
|
+
async chunkText(text, embedder, atomicRanges = []) {
|
|
92
|
+
// Handle empty input
|
|
93
|
+
if (!text || text.trim().length === 0) {
|
|
94
|
+
// Supplied ranges are programmer contracts and must fail fast even when
|
|
95
|
+
// ordinary empty text would otherwise return before sentence splitting.
|
|
96
|
+
if (atomicRanges.length > 0)
|
|
97
|
+
splitIntoSentenceUnits(text, atomicRanges);
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
// Split into sentences
|
|
101
|
+
const sentenceUnits = splitIntoSentenceUnits(text, atomicRanges);
|
|
102
|
+
if (sentenceUnits.length === 0) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
// Generate embeddings for all sentences
|
|
106
|
+
const embeddings = await embedder.embedBatch(sentenceUnits.map((unit) => unit.text));
|
|
107
|
+
// Apply Max-Min algorithm to group sentences into chunks
|
|
108
|
+
const sentenceGroups = this.groupSentences(sentenceUnits, embeddings);
|
|
109
|
+
// Convert groups to TextChunks
|
|
110
|
+
const chunks = [];
|
|
111
|
+
let chunkIndex = 0;
|
|
112
|
+
for (const group of sentenceGroups) {
|
|
113
|
+
const chunkText = group.map((unit) => unit.text).join(' ');
|
|
114
|
+
const containsAtomicUnit = group.some((unit) => unit.atomic);
|
|
115
|
+
// Filter out chunks that are too short or garbage
|
|
116
|
+
if ((containsAtomicUnit || chunkText.length >= this.config.minChunkLength) &&
|
|
117
|
+
!isGarbageChunk(chunkText)) {
|
|
118
|
+
const firstUnit = group[0];
|
|
119
|
+
const lastUnit = group[group.length - 1];
|
|
120
|
+
if (!firstUnit || !lastUnit)
|
|
121
|
+
continue;
|
|
122
|
+
chunks.push({
|
|
123
|
+
text: chunkText,
|
|
124
|
+
index: chunkIndex,
|
|
125
|
+
sourceStart: firstUnit.sourceStart,
|
|
126
|
+
sourceEnd: lastUnit.sourceEnd,
|
|
127
|
+
});
|
|
128
|
+
chunkIndex++;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return chunks;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Group sentences into chunks using Max-Min algorithm
|
|
135
|
+
*/
|
|
136
|
+
groupSentences(sentences, embeddings) {
|
|
137
|
+
if (sentences.length === 0)
|
|
138
|
+
return [];
|
|
139
|
+
if (sentences.length === 1) {
|
|
140
|
+
const sentence = sentences[0];
|
|
141
|
+
return sentence ? [[sentence]] : [];
|
|
142
|
+
}
|
|
143
|
+
const groups = [];
|
|
144
|
+
let currentGroup = [];
|
|
145
|
+
let currentGroupEmbeddings = [];
|
|
146
|
+
for (let i = 0; i < sentences.length; i++) {
|
|
147
|
+
const sentence = sentences[i];
|
|
148
|
+
const embedding = embeddings[i];
|
|
149
|
+
if (!sentence || !embedding)
|
|
150
|
+
continue;
|
|
151
|
+
if (currentGroup.length === 0) {
|
|
152
|
+
// Start new group with first sentence
|
|
153
|
+
currentGroup.push(sentence);
|
|
154
|
+
currentGroupEmbeddings.push(embedding);
|
|
155
|
+
}
|
|
156
|
+
else if (currentGroup.length === 1) {
|
|
157
|
+
// Special case for second sentence (init phase)
|
|
158
|
+
const firstEmbedding = currentGroupEmbeddings[0];
|
|
159
|
+
if (!firstEmbedding)
|
|
160
|
+
continue;
|
|
161
|
+
const similarity = this.cosineSimilarity(firstEmbedding, embedding);
|
|
162
|
+
if (this.config.initConst * similarity > this.config.hardThreshold) {
|
|
163
|
+
// Add to current group
|
|
164
|
+
currentGroup.push(sentence);
|
|
165
|
+
currentGroupEmbeddings.push(embedding);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
// Start new group
|
|
169
|
+
groups.push([...currentGroup]);
|
|
170
|
+
currentGroup = [sentence];
|
|
171
|
+
currentGroupEmbeddings = [embedding];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
// Force split if chunk reaches MAX_SENTENCES (safety limit for performance)
|
|
176
|
+
if (currentGroup.length >= MAX_SENTENCES) {
|
|
177
|
+
groups.push([...currentGroup]);
|
|
178
|
+
currentGroup = [sentence];
|
|
179
|
+
currentGroupEmbeddings = [embedding];
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
// Normal case: check if sentence should join current group
|
|
183
|
+
const shouldAdd = this.shouldAddToChunk(embedding, currentGroupEmbeddings);
|
|
184
|
+
if (shouldAdd) {
|
|
185
|
+
currentGroup.push(sentence);
|
|
186
|
+
currentGroupEmbeddings.push(embedding);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
// Start new group
|
|
190
|
+
groups.push([...currentGroup]);
|
|
191
|
+
currentGroup = [sentence];
|
|
192
|
+
currentGroupEmbeddings = [embedding];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Don't forget the last group
|
|
197
|
+
if (currentGroup.length > 0) {
|
|
198
|
+
groups.push(currentGroup);
|
|
199
|
+
}
|
|
200
|
+
return groups;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Decide if a sentence should be added to the current chunk
|
|
204
|
+
* Based on Max-Min algorithm from the paper
|
|
205
|
+
*/
|
|
206
|
+
shouldAddToChunk(newEmbedding, chunkEmbeddings) {
|
|
207
|
+
// Calculate min similarity within current chunk
|
|
208
|
+
const minSim = this.getMinSimilarity(chunkEmbeddings);
|
|
209
|
+
// Calculate max similarity between new sentence and chunk
|
|
210
|
+
const maxSim = this.getMaxSimilarity(newEmbedding, chunkEmbeddings);
|
|
211
|
+
// Calculate dynamic threshold
|
|
212
|
+
const threshold = this.calculateThreshold(minSim, chunkEmbeddings.length);
|
|
213
|
+
return maxSim > threshold;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Get minimum pairwise similarity within a chunk.
|
|
217
|
+
* Only compares the last WINDOW_SIZE sentences for O(1) complexity.
|
|
218
|
+
* This approximation is valid because recent sentences are most relevant
|
|
219
|
+
* for determining chunk coherence (per Max-Min paper's experimental setup).
|
|
220
|
+
*/
|
|
221
|
+
getMinSimilarity(embeddings) {
|
|
222
|
+
if (embeddings.length < 2)
|
|
223
|
+
return 1.0;
|
|
224
|
+
// Only compare the last WINDOW_SIZE embeddings to reduce O(k²) to O(1)
|
|
225
|
+
const startIdx = Math.max(0, embeddings.length - WINDOW_SIZE);
|
|
226
|
+
const windowEmbeddings = embeddings.slice(startIdx);
|
|
227
|
+
let minSim = 1.0;
|
|
228
|
+
for (let i = 0; i < windowEmbeddings.length; i++) {
|
|
229
|
+
for (let j = i + 1; j < windowEmbeddings.length; j++) {
|
|
230
|
+
const embI = windowEmbeddings[i];
|
|
231
|
+
const embJ = windowEmbeddings[j];
|
|
232
|
+
if (!embI || !embJ)
|
|
233
|
+
continue;
|
|
234
|
+
const sim = this.cosineSimilarity(embI, embJ);
|
|
235
|
+
if (sim < minSim) {
|
|
236
|
+
minSim = sim;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return minSim;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Get maximum similarity between a sentence and any sentence in the chunk
|
|
244
|
+
*/
|
|
245
|
+
getMaxSimilarity(embedding, chunkEmbeddings) {
|
|
246
|
+
let maxSim = -1.0;
|
|
247
|
+
for (const chunkEmb of chunkEmbeddings) {
|
|
248
|
+
const sim = this.cosineSimilarity(embedding, chunkEmb);
|
|
249
|
+
if (sim > maxSim) {
|
|
250
|
+
maxSim = sim;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return maxSim;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Calculate dynamic threshold based on chunk size
|
|
257
|
+
* threshold = max(c * minSim * sigmoid(|C|), hardThreshold)
|
|
258
|
+
*/
|
|
259
|
+
calculateThreshold(minSim, chunkSize) {
|
|
260
|
+
const sigmoidValue = this.sigmoid(chunkSize);
|
|
261
|
+
const dynamicThreshold = this.config.c * minSim * sigmoidValue;
|
|
262
|
+
return Math.max(dynamicThreshold, this.config.hardThreshold);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Sigmoid function
|
|
266
|
+
*/
|
|
267
|
+
sigmoid(x) {
|
|
268
|
+
return 1 / (1 + Math.exp(-x));
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Calculate cosine similarity between two vectors
|
|
272
|
+
* Public for testing
|
|
273
|
+
*/
|
|
274
|
+
cosineSimilarity(vec1, vec2) {
|
|
275
|
+
if (vec1.length !== vec2.length || vec1.length === 0) {
|
|
276
|
+
return 0;
|
|
277
|
+
}
|
|
278
|
+
let dotProduct = 0;
|
|
279
|
+
let norm1 = 0;
|
|
280
|
+
let norm2 = 0;
|
|
281
|
+
for (let i = 0; i < vec1.length; i++) {
|
|
282
|
+
const v1 = vec1[i] ?? 0;
|
|
283
|
+
const v2 = vec2[i] ?? 0;
|
|
284
|
+
dotProduct += v1 * v2;
|
|
285
|
+
norm1 += v1 * v1;
|
|
286
|
+
norm2 += v2 * v2;
|
|
287
|
+
}
|
|
288
|
+
const denominator = Math.sqrt(norm1) * Math.sqrt(norm2);
|
|
289
|
+
if (denominator === 0)
|
|
290
|
+
return 0;
|
|
291
|
+
return dotProduct / denominator;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
//# sourceMappingURL=semantic-chunker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"semantic-chunker.js","sourceRoot":"","sources":["../../src/chunker/semantic-chunker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AtomicTextRange } from './index.js';
|
|
2
|
+
export interface SentenceUnit {
|
|
3
|
+
text: string;
|
|
4
|
+
atomic: boolean;
|
|
5
|
+
sourceStart: number;
|
|
6
|
+
sourceEnd: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Split text into sentences using Intl.Segmenter
|
|
10
|
+
*
|
|
11
|
+
* Uses the Unicode Text Segmentation standard (UAX #29) via Intl.Segmenter.
|
|
12
|
+
* This provides multilingual support for sentence boundary detection.
|
|
13
|
+
*
|
|
14
|
+
* Note: Intl.Segmenter may split on abbreviations like "Mr." or "e.g."
|
|
15
|
+
* These edge cases are acceptable for semantic chunking as:
|
|
16
|
+
* 1. Short fragments will be grouped with adjacent sentences by similarity
|
|
17
|
+
* 2. Fragments below minChunkLength are filtered out
|
|
18
|
+
*
|
|
19
|
+
* @param text - The text to split into sentences
|
|
20
|
+
* @returns Array of sentences
|
|
21
|
+
*/
|
|
22
|
+
export declare function splitIntoSentences(text: string): string[];
|
|
23
|
+
/**
|
|
24
|
+
* Split ordinary text while preserving the supplied positional ranges as
|
|
25
|
+
* indivisible sentence units.
|
|
26
|
+
*/
|
|
27
|
+
export declare function splitIntoSentenceUnits(text: string, atomicRanges?: readonly AtomicTextRange[]): SentenceUnit[];
|
|
28
|
+
//# sourceMappingURL=sentence-splitter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sentence-splitter.d.ts","sourceRoot":"","sources":["../../src/chunker/sentence-splitter.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAyBjD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,OAAO,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;CAClB;AAoKD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAKzD;AAuBD;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,EACZ,YAAY,GAAE,SAAS,eAAe,EAAO,GAC5C,YAAY,EAAE,CA0BhB"}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// Sentence Splitter for Semantic Chunking
|
|
2
|
+
// Created: 2025-12-27
|
|
3
|
+
// Purpose: Split text into sentences using Intl.Segmenter (Unicode standard)
|
|
4
|
+
// ============================================
|
|
5
|
+
// Constants
|
|
6
|
+
// ============================================
|
|
7
|
+
/**
|
|
8
|
+
* Placeholder for code blocks during processing
|
|
9
|
+
*/
|
|
10
|
+
const CODE_BLOCK_PLACEHOLDER = '\u0000CODE_BLOCK\u0000';
|
|
11
|
+
/**
|
|
12
|
+
* Placeholder for inline code during processing
|
|
13
|
+
*/
|
|
14
|
+
const INLINE_CODE_PLACEHOLDER = '\u0000INLINE_CODE\u0000';
|
|
15
|
+
// ============================================
|
|
16
|
+
// Helper Functions
|
|
17
|
+
// ============================================
|
|
18
|
+
function maskCode(text, sourceStart) {
|
|
19
|
+
const blocks = [];
|
|
20
|
+
const fenced = [...text.matchAll(/```[\s\S]*?```/g)].flatMap((match) => match.index === undefined ? [] : [{ start: match.index, end: match.index + match[0].length }]);
|
|
21
|
+
const matchedRanges = [...fenced];
|
|
22
|
+
let gapStart = 0;
|
|
23
|
+
for (const fencedRange of [...fenced, { start: text.length, end: text.length }]) {
|
|
24
|
+
const gap = text.slice(gapStart, fencedRange.start);
|
|
25
|
+
for (const match of gap.matchAll(/`[^`]+`/g)) {
|
|
26
|
+
if (match.index === undefined)
|
|
27
|
+
continue;
|
|
28
|
+
matchedRanges.push({
|
|
29
|
+
start: gapStart + match.index,
|
|
30
|
+
end: gapStart + match.index + match[0].length,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
gapStart = fencedRange.end;
|
|
34
|
+
}
|
|
35
|
+
matchedRanges.sort((left, right) => left.start - right.start);
|
|
36
|
+
let mappedText = '';
|
|
37
|
+
let cursor = 0;
|
|
38
|
+
const segments = [];
|
|
39
|
+
const append = (value, start, end, linear) => {
|
|
40
|
+
const mappedStart = mappedText.length;
|
|
41
|
+
mappedText += value;
|
|
42
|
+
segments.push({
|
|
43
|
+
mappedStart,
|
|
44
|
+
mappedEnd: mappedText.length,
|
|
45
|
+
sourceStart: sourceStart + start,
|
|
46
|
+
sourceEnd: sourceStart + end,
|
|
47
|
+
linear,
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
for (const [index, range] of matchedRanges.entries()) {
|
|
51
|
+
if (range.start > cursor)
|
|
52
|
+
append(text.slice(cursor, range.start), cursor, range.start, true);
|
|
53
|
+
const content = text.slice(range.start, range.end);
|
|
54
|
+
const placeholderPrefix = content.startsWith('```')
|
|
55
|
+
? CODE_BLOCK_PLACEHOLDER
|
|
56
|
+
: INLINE_CODE_PLACEHOLDER;
|
|
57
|
+
const placeholder = `${placeholderPrefix}${index}${placeholderPrefix}`;
|
|
58
|
+
blocks.push({ placeholder, content });
|
|
59
|
+
append(placeholder, range.start, range.end, false);
|
|
60
|
+
cursor = range.end;
|
|
61
|
+
}
|
|
62
|
+
if (cursor < text.length)
|
|
63
|
+
append(text.slice(cursor), cursor, text.length, true);
|
|
64
|
+
return { mapped: { text: mappedText, segments }, blocks };
|
|
65
|
+
}
|
|
66
|
+
function sourceOffsetAt(mapped, offset, fallback) {
|
|
67
|
+
let low = 0;
|
|
68
|
+
let high = mapped.segments.length - 1;
|
|
69
|
+
while (low <= high) {
|
|
70
|
+
const middle = Math.floor((low + high) / 2);
|
|
71
|
+
const segment = mapped.segments[middle];
|
|
72
|
+
if (offset < segment.mappedStart) {
|
|
73
|
+
high = middle - 1;
|
|
74
|
+
}
|
|
75
|
+
else if (offset > segment.mappedEnd) {
|
|
76
|
+
low = middle + 1;
|
|
77
|
+
}
|
|
78
|
+
else if (offset === segment.mappedEnd) {
|
|
79
|
+
return segment.sourceEnd;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
return segment.linear
|
|
83
|
+
? segment.sourceStart + (offset - segment.mappedStart)
|
|
84
|
+
: segment.sourceStart;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return fallback;
|
|
88
|
+
}
|
|
89
|
+
function restoreCode(text, blocks) {
|
|
90
|
+
let restored = text;
|
|
91
|
+
for (const block of blocks) {
|
|
92
|
+
restored = restored.replace(block.placeholder, () => block.content);
|
|
93
|
+
}
|
|
94
|
+
return restored;
|
|
95
|
+
}
|
|
96
|
+
function trimmedRange(text, start, end) {
|
|
97
|
+
const value = text.slice(start, end);
|
|
98
|
+
const trimmedStart = start + (value.length - value.trimStart().length);
|
|
99
|
+
const trimmedEnd = start + value.trimEnd().length;
|
|
100
|
+
return trimmedStart < trimmedEnd ? [trimmedStart, trimmedEnd] : null;
|
|
101
|
+
}
|
|
102
|
+
function splitOrdinaryRange(text, sourceStart, sourceEnd) {
|
|
103
|
+
if (sourceStart >= sourceEnd)
|
|
104
|
+
return [];
|
|
105
|
+
const { mapped, blocks } = maskCode(text.slice(sourceStart, sourceEnd), sourceStart);
|
|
106
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: NUL delimiters identify masked code placeholders.
|
|
107
|
+
const paragraphSeparator = /\n{2,}|\n(?=\S)|(?<=\u0000)\n/g;
|
|
108
|
+
const paragraphRanges = [];
|
|
109
|
+
let cursor = 0;
|
|
110
|
+
for (const match of mapped.text.matchAll(paragraphSeparator)) {
|
|
111
|
+
const separatorStart = match.index;
|
|
112
|
+
if (separatorStart === undefined)
|
|
113
|
+
continue;
|
|
114
|
+
paragraphRanges.push([cursor, separatorStart]);
|
|
115
|
+
cursor = separatorStart + match[0].length;
|
|
116
|
+
}
|
|
117
|
+
paragraphRanges.push([cursor, mapped.text.length]);
|
|
118
|
+
const units = [];
|
|
119
|
+
const appendUnit = (start, end) => {
|
|
120
|
+
const range = trimmedRange(mapped.text, start, end);
|
|
121
|
+
if (!range)
|
|
122
|
+
return;
|
|
123
|
+
const [trimmedStart, trimmedEnd] = range;
|
|
124
|
+
const restored = restoreCode(mapped.text.slice(trimmedStart, trimmedEnd), blocks).trim();
|
|
125
|
+
if (!restored)
|
|
126
|
+
return;
|
|
127
|
+
units.push({
|
|
128
|
+
text: restored,
|
|
129
|
+
atomic: false,
|
|
130
|
+
sourceStart: sourceOffsetAt(mapped, trimmedStart, sourceStart),
|
|
131
|
+
sourceEnd: sourceOffsetAt(mapped, trimmedEnd, sourceEnd),
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
for (const [paragraphStart, paragraphEnd] of paragraphRanges) {
|
|
135
|
+
const range = trimmedRange(mapped.text, paragraphStart, paragraphEnd);
|
|
136
|
+
if (!range)
|
|
137
|
+
continue;
|
|
138
|
+
const [trimmedStart, trimmedEnd] = range;
|
|
139
|
+
const paragraph = mapped.text.slice(trimmedStart, trimmedEnd);
|
|
140
|
+
if (/^#{1,6}\s/.test(paragraph)) {
|
|
141
|
+
appendUnit(trimmedStart, trimmedEnd);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
for (const segment of segmenter.segment(paragraph)) {
|
|
145
|
+
appendUnit(trimmedStart + segment.index, trimmedStart + segment.index + segment.segment.length);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return units;
|
|
149
|
+
}
|
|
150
|
+
// ============================================
|
|
151
|
+
// Intl.Segmenter-based splitting
|
|
152
|
+
// ============================================
|
|
153
|
+
// Create segmenters for supported languages
|
|
154
|
+
// Using 'und' (undetermined) as fallback for general Unicode support
|
|
155
|
+
const segmenter = new Intl.Segmenter('und', { granularity: 'sentence' });
|
|
156
|
+
/**
|
|
157
|
+
* Split text into sentences using Intl.Segmenter
|
|
158
|
+
*
|
|
159
|
+
* Uses the Unicode Text Segmentation standard (UAX #29) via Intl.Segmenter.
|
|
160
|
+
* This provides multilingual support for sentence boundary detection.
|
|
161
|
+
*
|
|
162
|
+
* Note: Intl.Segmenter may split on abbreviations like "Mr." or "e.g."
|
|
163
|
+
* These edge cases are acceptable for semantic chunking as:
|
|
164
|
+
* 1. Short fragments will be grouped with adjacent sentences by similarity
|
|
165
|
+
* 2. Fragments below minChunkLength are filtered out
|
|
166
|
+
*
|
|
167
|
+
* @param text - The text to split into sentences
|
|
168
|
+
* @returns Array of sentences
|
|
169
|
+
*/
|
|
170
|
+
export function splitIntoSentences(text) {
|
|
171
|
+
if (!text || text.trim().length === 0) {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
return splitOrdinaryRange(text, 0, text.length).map((unit) => unit.text);
|
|
175
|
+
}
|
|
176
|
+
function validateAtomicRanges(text, atomicRanges) {
|
|
177
|
+
let previousEnd = 0;
|
|
178
|
+
for (const range of atomicRanges) {
|
|
179
|
+
const validOffsets = Number.isInteger(range.start) &&
|
|
180
|
+
Number.isInteger(range.end) &&
|
|
181
|
+
range.start >= 0 &&
|
|
182
|
+
range.start < range.end &&
|
|
183
|
+
range.end <= text.length;
|
|
184
|
+
const orderedAndNonOverlapping = range.start >= previousEnd;
|
|
185
|
+
if (!validOffsets || !orderedAndNonOverlapping) {
|
|
186
|
+
throw new Error(`Invalid atomic range [${range.start}, ${range.end}) for text length ${text.length}`);
|
|
187
|
+
}
|
|
188
|
+
previousEnd = range.end;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Split ordinary text while preserving the supplied positional ranges as
|
|
193
|
+
* indivisible sentence units.
|
|
194
|
+
*/
|
|
195
|
+
export function splitIntoSentenceUnits(text, atomicRanges = []) {
|
|
196
|
+
validateAtomicRanges(text, atomicRanges);
|
|
197
|
+
if (atomicRanges.length === 0) {
|
|
198
|
+
return splitOrdinaryRange(text, 0, text.length);
|
|
199
|
+
}
|
|
200
|
+
const units = [];
|
|
201
|
+
let cursor = 0;
|
|
202
|
+
for (const range of atomicRanges) {
|
|
203
|
+
units.push(...splitOrdinaryRange(text, cursor, range.start));
|
|
204
|
+
const atomicText = text.slice(range.start, range.end).trim();
|
|
205
|
+
if (!atomicText) {
|
|
206
|
+
throw new Error(`Invalid atomic range [${range.start}, ${range.end}): empty text`);
|
|
207
|
+
}
|
|
208
|
+
units.push({
|
|
209
|
+
text: atomicText,
|
|
210
|
+
atomic: true,
|
|
211
|
+
sourceStart: range.start,
|
|
212
|
+
sourceEnd: range.end,
|
|
213
|
+
});
|
|
214
|
+
cursor = range.end;
|
|
215
|
+
}
|
|
216
|
+
units.push(...splitOrdinaryRange(text, cursor, text.length));
|
|
217
|
+
return units;
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=sentence-splitter.js.map
|