@gmickel/gno 1.45.1 → 2.0.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 +1 -1
- package/THIRD_PARTY_NOTICES.md +46 -0
- package/assets/skill/SKILL.md +7 -6
- package/assets/skill/cli-reference.md +14 -6
- package/assets/skill/mcp-reference.md +4 -1
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.0.0.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.0.0.zip.sha256 +1 -0
- package/browser-extension/dist/chunk-4tc9v0ja.js +74 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/browser-extension/dist/preview.html +1 -1
- package/browser-extension/dist/service-worker.js +32 -33
- package/bunfig.toml +2 -0
- package/package.json +40 -26
- package/spec/cli.md +30 -11
- package/spec/db/schema.sql +146 -1
- package/spec/mcp.md +26 -0
- package/src/app/context-runtime-types.ts +3 -0
- package/src/app/context-runtime.ts +2 -0
- package/src/cli/commands/ask.ts +6 -1
- package/src/cli/commands/daemon.ts +21 -8
- package/src/cli/commands/embed.ts +77 -41
- package/src/cli/commands/mcp/install.ts +20 -0
- package/src/cli/commands/mcp/paths.ts +25 -0
- package/src/cli/commands/mcp/status.ts +6 -0
- package/src/cli/detach.ts +3 -2
- package/src/cli/program.ts +6 -0
- package/src/config/types.ts +3 -3
- package/src/converters/adapters/markitdownTs/adapter.ts +1 -2
- package/src/converters/adapters/officeparser/adapter.ts +1 -2
- package/src/converters/versions.ts +6 -8
- package/src/core/context-evidence.ts +8 -4
- package/src/core/job-manager.ts +95 -13
- package/src/core/network-boundary-inventory.ts +10 -0
- package/src/core/shutdown-budget.ts +45 -0
- package/src/embed/backlog.ts +107 -4
- package/src/embed/batch.ts +42 -2
- package/src/embed/fingerprint.ts +16 -0
- package/src/embed/retry.ts +113 -5
- package/src/embed/variant-backlog.ts +105 -0
- package/src/embed/variant-plan.ts +62 -0
- package/src/embed/variant-retry.ts +113 -0
- package/src/ingestion/graph-reconciliation.ts +327 -0
- package/src/ingestion/sync.ts +9 -272
- package/src/llm/http-inference.ts +6 -0
- package/src/llm/httpEmbedding.ts +37 -6
- package/src/llm/httpGeneration.ts +18 -3
- package/src/llm/httpRerank.ts +23 -5
- package/src/llm/inference-cancellation.ts +168 -0
- package/src/llm/inference-scope.ts +202 -0
- package/src/llm/lazy-ports.ts +115 -0
- package/src/llm/native-worker/client.ts +541 -0
- package/src/llm/native-worker/dispatcher.ts +228 -0
- package/src/llm/native-worker/embedding-identity.ts +33 -0
- package/src/llm/native-worker/entry.ts +173 -0
- package/src/llm/native-worker/errors.ts +32 -0
- package/src/llm/native-worker/evaluation.ts +16 -0
- package/src/llm/native-worker/owned-exit.ts +108 -0
- package/src/llm/native-worker/owner.ts +141 -0
- package/src/llm/native-worker/ports.ts +317 -0
- package/src/llm/native-worker/protocol.ts +442 -0
- package/src/llm/native-worker/runtime-config.ts +92 -0
- package/src/llm/nodeLlamaCpp/adapter.ts +77 -20
- package/src/llm/nodeLlamaCpp/embedding.ts +130 -46
- package/src/llm/nodeLlamaCpp/generation.ts +34 -5
- package/src/llm/nodeLlamaCpp/lifecycle-options.ts +99 -0
- package/src/llm/nodeLlamaCpp/lifecycle.ts +209 -204
- package/src/llm/nodeLlamaCpp/rerank-capacity.ts +111 -0
- package/src/llm/nodeLlamaCpp/rerank.ts +118 -27
- package/src/llm/nodeLlamaCpp/simulator-handle.ts +73 -0
- package/src/llm/nodeLlamaCpp/simulator-install.ts +124 -0
- package/src/llm/nodeLlamaCpp/simulator-session.ts +240 -0
- package/src/llm/nodeLlamaCpp/simulator-types.ts +80 -0
- package/src/llm/types.ts +35 -5
- package/src/mcp/context.ts +27 -0
- package/src/mcp/http-transport.ts +12 -10
- package/src/mcp/server.ts +3 -0
- package/src/mcp/tool-profile.ts +30 -8
- package/src/mcp/tools/context.ts +8 -11
- package/src/mcp/tools/embed.ts +1 -1
- package/src/mcp/tools/index-cmd.ts +1 -1
- package/src/mcp/tools/index.ts +10 -8
- package/src/mcp/tools/query.ts +14 -30
- package/src/mcp/tools/vsearch.ts +1 -1
- package/src/pipeline/answer.ts +23 -3
- package/src/pipeline/claim-verifier.ts +6 -0
- package/src/pipeline/expansion.ts +43 -40
- package/src/pipeline/explain.ts +6 -2
- package/src/pipeline/filters.ts +63 -0
- package/src/pipeline/fusion.ts +29 -9
- package/src/pipeline/graph-retrieval.ts +29 -9
- package/src/pipeline/hybrid.ts +198 -55
- package/src/pipeline/hydration.ts +161 -0
- package/src/pipeline/owner-fusion.ts +87 -0
- package/src/pipeline/rerank.ts +35 -11
- package/src/pipeline/search.ts +13 -2
- package/src/pipeline/types.ts +5 -3
- package/src/pipeline/vsearch.ts +87 -7
- package/src/sdk/client.ts +47 -3
- package/src/sdk/embed.ts +63 -39
- package/src/serve/background-runtime.ts +1 -1
- package/src/serve/context.ts +41 -56
- package/src/serve/embed-scheduler.ts +58 -35
- package/src/serve/public/components/IndexingProgress.tsx +46 -60
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/lib/shiki-language-ids.ts +14 -0
- package/src/serve/resident-admission.ts +36 -36
- package/src/serve/resident-background-work.ts +20 -2
- package/src/serve/resident-request.ts +11 -5
- package/src/serve/resident-runtime.ts +97 -61
- package/src/serve/resident-shutdown.ts +153 -0
- package/src/serve/routes/api.ts +3 -1
- package/src/serve/server.ts +47 -26
- package/src/store/migrations/028-vector-variants.ts +54 -0
- package/src/store/migrations/029-graph-reference-state.ts +77 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +251 -183
- package/src/store/sqlite/eligibility.ts +174 -0
- package/src/store/sqlite/graph-edge-application.ts +66 -0
- package/src/store/sqlite/graph-reference-state.ts +194 -0
- package/src/store/sqlite/legacy-vector-ownership.ts +79 -0
- package/src/store/types.ts +80 -12
- package/src/store/vector/eligibility.ts +36 -0
- package/src/store/vector/freshness.ts +33 -6
- package/src/store/vector/lazy.ts +81 -0
- package/src/store/vector/sqlite-vec.ts +106 -54
- package/src/store/vector/stats.ts +14 -3
- package/src/store/vector/types.ts +35 -2
- package/src/store/vector/variant-search.ts +192 -0
- package/src/store/vector/variants.ts +451 -0
- package/vendor/converters/markitdown-ts/LICENSE +21 -0
- package/vendor/converters/markitdown-ts/dist/index.cjs +1180 -0
- package/vendor/converters/markitdown-ts/dist/index.d.cts +46 -0
- package/vendor/converters/markitdown-ts/dist/index.d.mts +46 -0
- package/vendor/converters/markitdown-ts/dist/index.d.ts +46 -0
- package/vendor/converters/markitdown-ts/dist/index.mjs +1152 -0
- package/vendor/converters/markitdown-ts/package.json +77 -0
- package/vendor/converters/officeparser/LICENSE +21 -0
- package/vendor/converters/officeparser/dist/OfficeConverter.d.ts +47 -0
- package/vendor/converters/officeparser/dist/OfficeConverter.js +76 -0
- package/vendor/converters/officeparser/dist/OfficeGenerator.d.ts +23 -0
- package/vendor/converters/officeparser/dist/OfficeGenerator.js +73 -0
- package/vendor/converters/officeparser/dist/OfficeParser.d.ts +106 -0
- package/vendor/converters/officeparser/dist/OfficeParser.js +332 -0
- package/vendor/converters/officeparser/dist/cli.d.ts +28 -0
- package/vendor/converters/officeparser/dist/cli.js +381 -0
- package/vendor/converters/officeparser/dist/defaults.d.ts +41 -0
- package/vendor/converters/officeparser/dist/defaults.js +218 -0
- package/vendor/converters/officeparser/dist/generators/BaseGenerator.d.ts +107 -0
- package/vendor/converters/officeparser/dist/generators/BaseGenerator.js +248 -0
- package/vendor/converters/officeparser/dist/generators/ChunkingGenerator.d.ts +82 -0
- package/vendor/converters/officeparser/dist/generators/ChunkingGenerator.js +797 -0
- package/vendor/converters/officeparser/dist/generators/CsvGenerator.d.ts +38 -0
- package/vendor/converters/officeparser/dist/generators/CsvGenerator.js +245 -0
- package/vendor/converters/officeparser/dist/generators/EpubGenerator.d.ts +43 -0
- package/vendor/converters/officeparser/dist/generators/EpubGenerator.js +315 -0
- package/vendor/converters/officeparser/dist/generators/HtmlGenerator.d.ts +59 -0
- package/vendor/converters/officeparser/dist/generators/HtmlGenerator.js +1942 -0
- package/vendor/converters/officeparser/dist/generators/MarkdownGenerator.d.ts +96 -0
- package/vendor/converters/officeparser/dist/generators/MarkdownGenerator.js +1175 -0
- package/vendor/converters/officeparser/dist/generators/PdfGenerator.d.ts +22 -0
- package/vendor/converters/officeparser/dist/generators/PdfGenerator.js +194 -0
- package/vendor/converters/officeparser/dist/generators/RtfGenerator.d.ts +29 -0
- package/vendor/converters/officeparser/dist/generators/RtfGenerator.js +316 -0
- package/vendor/converters/officeparser/dist/generators/TextGenerator.d.ts +13 -0
- package/vendor/converters/officeparser/dist/generators/TextGenerator.js +201 -0
- package/vendor/converters/officeparser/dist/index.d.ts +60 -0
- package/vendor/converters/officeparser/dist/index.js +72 -0
- package/vendor/converters/officeparser/dist/index.mjs +18 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.d.ts +2621 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.iife.js +1336 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.mjs +1335 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.slim.d.ts +2621 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.slim.iife.js +1336 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.slim.mjs +1335 -0
- package/vendor/converters/officeparser/dist/parsers/CsvParser.d.ts +9 -0
- package/vendor/converters/officeparser/dist/parsers/CsvParser.js +115 -0
- package/vendor/converters/officeparser/dist/parsers/EpubParser.d.ts +8 -0
- package/vendor/converters/officeparser/dist/parsers/EpubParser.js +217 -0
- package/vendor/converters/officeparser/dist/parsers/ExcelParser.d.ts +32 -0
- package/vendor/converters/officeparser/dist/parsers/ExcelParser.js +736 -0
- package/vendor/converters/officeparser/dist/parsers/HtmlParser.d.ts +2 -0
- package/vendor/converters/officeparser/dist/parsers/HtmlParser.js +1287 -0
- package/vendor/converters/officeparser/dist/parsers/MarkdownParser.d.ts +2 -0
- package/vendor/converters/officeparser/dist/parsers/MarkdownParser.js +1272 -0
- package/vendor/converters/officeparser/dist/parsers/OpenOfficeParser.d.ts +31 -0
- package/vendor/converters/officeparser/dist/parsers/OpenOfficeParser.js +1819 -0
- package/vendor/converters/officeparser/dist/parsers/PdfParser.d.ts +67 -0
- package/vendor/converters/officeparser/dist/parsers/PdfParser.js +848 -0
- package/vendor/converters/officeparser/dist/parsers/PowerPointParser.d.ts +32 -0
- package/vendor/converters/officeparser/dist/parsers/PowerPointParser.js +950 -0
- package/vendor/converters/officeparser/dist/parsers/RtfParser.d.ts +187 -0
- package/vendor/converters/officeparser/dist/parsers/RtfParser.js +1801 -0
- package/vendor/converters/officeparser/dist/parsers/WordParser.d.ts +79 -0
- package/vendor/converters/officeparser/dist/parsers/WordParser.js +1177 -0
- package/vendor/converters/officeparser/dist/sbom.cdx.json +1763 -0
- package/vendor/converters/officeparser/dist/types.d.ts +2507 -0
- package/vendor/converters/officeparser/dist/types.js +107 -0
- package/vendor/converters/officeparser/dist/utils/astUtils.d.ts +16 -0
- package/vendor/converters/officeparser/dist/utils/astUtils.js +33 -0
- package/vendor/converters/officeparser/dist/utils/chartUtils.d.ts +6 -0
- package/vendor/converters/officeparser/dist/utils/chartUtils.js +257 -0
- package/vendor/converters/officeparser/dist/utils/configUtils.d.ts +44 -0
- package/vendor/converters/officeparser/dist/utils/configUtils.js +315 -0
- package/vendor/converters/officeparser/dist/utils/dateUtils.d.ts +17 -0
- package/vendor/converters/officeparser/dist/utils/dateUtils.js +69 -0
- package/vendor/converters/officeparser/dist/utils/envUtils.d.ts +29 -0
- package/vendor/converters/officeparser/dist/utils/envUtils.js +152 -0
- package/vendor/converters/officeparser/dist/utils/errorUtils.d.ts +72 -0
- package/vendor/converters/officeparser/dist/utils/errorUtils.js +245 -0
- package/vendor/converters/officeparser/dist/utils/imageUtils.d.ts +66 -0
- package/vendor/converters/officeparser/dist/utils/imageUtils.js +133 -0
- package/vendor/converters/officeparser/dist/utils/mathUtils.d.ts +42 -0
- package/vendor/converters/officeparser/dist/utils/mathUtils.js +385 -0
- package/vendor/converters/officeparser/dist/utils/moduleLoader.d.ts +18 -0
- package/vendor/converters/officeparser/dist/utils/moduleLoader.js +106 -0
- package/vendor/converters/officeparser/dist/utils/ocrUtils.d.ts +42 -0
- package/vendor/converters/officeparser/dist/utils/ocrUtils.js +428 -0
- package/vendor/converters/officeparser/dist/utils/sanitize.d.ts +148 -0
- package/vendor/converters/officeparser/dist/utils/sanitize.js +344 -0
- package/vendor/converters/officeparser/dist/utils/sheetUtils.d.ts +7 -0
- package/vendor/converters/officeparser/dist/utils/sheetUtils.js +35 -0
- package/vendor/converters/officeparser/dist/utils/styleMapper.d.ts +36 -0
- package/vendor/converters/officeparser/dist/utils/styleMapper.js +224 -0
- package/vendor/converters/officeparser/dist/utils/xmlUtils.d.ts +163 -0
- package/vendor/converters/officeparser/dist/utils/xmlUtils.js +461 -0
- package/vendor/converters/officeparser/dist/utils/zipUtils.d.ts +134 -0
- package/vendor/converters/officeparser/dist/utils/zipUtils.js +337 -0
- package/vendor/converters/officeparser/package.json +147 -0
- package/vendor/converters/upstream-manifest.json +124 -0
- package/vendor/dependency-fixes/README.md +77 -0
- package/vendor/dependency-fixes/vendor-converters.py +83 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.45.1.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.45.1.zip.sha256 +0 -1
- package/browser-extension/dist/chunk-627emwpj.js +0 -75
- /package/browser-extension/dist/{chunk-ydfx5d7p.css → chunk-z74y8n8c.css} +0 -0
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ChunkingGenerator = void 0;
|
|
4
|
+
const defaults_js_1 = require("../defaults.js");
|
|
5
|
+
const types_js_1 = require("../types.js");
|
|
6
|
+
const errorUtils_js_1 = require("../utils/errorUtils.js");
|
|
7
|
+
const BaseGenerator_js_1 = require("./BaseGenerator.js");
|
|
8
|
+
/** Node types whose text is block-level, so a boundary between two of them is a real break. */
|
|
9
|
+
const BLOCK_NODE_TYPES = new Set([
|
|
10
|
+
'paragraph', 'heading', 'list', 'table', 'row', 'cell', 'code', 'note', 'admonition',
|
|
11
|
+
'definitionList', 'definitionTerm', 'definitionDescription', 'sheet', 'slide', 'page', 'embed',
|
|
12
|
+
]);
|
|
13
|
+
/**
|
|
14
|
+
* The visible text of a content node: its own `.text` when set, otherwise its descendants' text,
|
|
15
|
+
* plus any footnote/endnote bodies hanging off `node.notes`.
|
|
16
|
+
* HTML- and Markdown-origin parsers build paragraphs as `{ children: [...] }` with no `.text`, so
|
|
17
|
+
* reading `node.text` alone dropped their content from every chunk. Block-level children are joined
|
|
18
|
+
* with a newline so words don't merge across paragraphs/list items (matching the text generator);
|
|
19
|
+
* inline runs join with no separator. Notes live on `node.notes`, off the children tree, and a
|
|
20
|
+
* content node is emitted as a chunk without recursing into them - so their text was silently
|
|
21
|
+
* absent from the RAG index. Fold them in here (joined as block content) so a footnote's body is
|
|
22
|
+
* searchable alongside the paragraph that references it.
|
|
23
|
+
*/
|
|
24
|
+
function collectNodeText(node) {
|
|
25
|
+
let out = '';
|
|
26
|
+
if (typeof node.text === 'string' && node.text.length > 0) {
|
|
27
|
+
out = node.text;
|
|
28
|
+
// The `.text` fast-path above skips the children walk, but DOCX/ODT/RTF set `.text` on the
|
|
29
|
+
// paragraph while the footnote hangs off a nested text child - so its body would be missed.
|
|
30
|
+
// Fold in descendant note bodies (visible text already covered by `.text`, not re-added).
|
|
31
|
+
const descendantNotes = collectDescendantNoteText(node);
|
|
32
|
+
if (descendantNotes)
|
|
33
|
+
out += '\n' + descendantNotes;
|
|
34
|
+
}
|
|
35
|
+
else if (node.children && node.children.length > 0) {
|
|
36
|
+
for (const child of node.children) {
|
|
37
|
+
if (out && BLOCK_NODE_TYPES.has(child.type))
|
|
38
|
+
out += '\n';
|
|
39
|
+
out += collectNodeText(child);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (node.notes && node.notes.length > 0) {
|
|
43
|
+
for (const note of node.notes) {
|
|
44
|
+
const noteText = collectNodeText(note);
|
|
45
|
+
if (noteText)
|
|
46
|
+
out += (out ? '\n' : '') + noteText;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Footnote/endnote bodies hanging off a node's descendants, without the descendants' own visible
|
|
53
|
+
* text (the caller already has that via `.text`). Only reached from the `.text` fast-path above, to
|
|
54
|
+
* recover notes that office-origin parsers attach to a nested text child of a `.text`-bearing node.
|
|
55
|
+
*/
|
|
56
|
+
function collectDescendantNoteText(node) {
|
|
57
|
+
if (!node.children || node.children.length === 0)
|
|
58
|
+
return '';
|
|
59
|
+
let out = '';
|
|
60
|
+
for (const child of node.children) {
|
|
61
|
+
if (child.notes) {
|
|
62
|
+
for (const note of child.notes) {
|
|
63
|
+
const t = collectNodeText(note);
|
|
64
|
+
if (t)
|
|
65
|
+
out += (out ? '\n' : '') + t;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const deeper = collectDescendantNoteText(child);
|
|
69
|
+
if (deeper)
|
|
70
|
+
out += (out ? '\n' : '') + deeper;
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Generates a list of OfficeChunk objects from an AST for use in RAG pipelines.
|
|
76
|
+
* Supports three strategies: 'fixed-size', 'document-structure', and 'semantic'.
|
|
77
|
+
*/
|
|
78
|
+
class ChunkingGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
79
|
+
/** The resolved chunking config (with defaults applied). */
|
|
80
|
+
chunkConfig;
|
|
81
|
+
/** Whether the user provided an explicit sentence boundary regex. */
|
|
82
|
+
isCustomRegex;
|
|
83
|
+
constructor(ast, config) {
|
|
84
|
+
super('chunks', ast, config);
|
|
85
|
+
this.chunkConfig = this.resolveChunkingConfig(this.config.chunksConfig);
|
|
86
|
+
// Track if the user explicitly provided a regex (vs using the library default)
|
|
87
|
+
this.isCustomRegex = !!config?.chunksConfig?.sentenceBoundaryRegex;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Merges the user's chunking config with the appropriate defaults for the chosen strategy.
|
|
91
|
+
*/
|
|
92
|
+
resolveChunkingConfig(userChunkConfig) {
|
|
93
|
+
const strategy = userChunkConfig?.strategy ?? 'document-structure';
|
|
94
|
+
switch (strategy) {
|
|
95
|
+
case 'fixed-size':
|
|
96
|
+
return { ...defaults_js_1.DEFAULT_FIXED_SIZE_CHUNKING_CONFIG, ...userChunkConfig };
|
|
97
|
+
case 'semantic':
|
|
98
|
+
return { ...defaults_js_1.DEFAULT_SEMANTIC_CHUNKING_CONFIG, ...userChunkConfig };
|
|
99
|
+
case 'document-structure':
|
|
100
|
+
return { ...defaults_js_1.DEFAULT_DOCUMENT_STRUCTURE_CHUNKING_CONFIG, ...userChunkConfig };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Main entry point. Routes to the correct strategy implementation.
|
|
105
|
+
* Note: ConversionResult.value is a real OfficeChunk[] array for the 'chunks' destination, not
|
|
106
|
+
* a JSON string. Consumers serialize it to JSON/JSONL themselves.
|
|
107
|
+
*/
|
|
108
|
+
async generate() {
|
|
109
|
+
(0, errorUtils_js_1.checkAbortSignal)(this.config.abortSignal);
|
|
110
|
+
let chunks;
|
|
111
|
+
switch (this.chunkConfig.strategy) {
|
|
112
|
+
case 'fixed-size':
|
|
113
|
+
chunks = await this.generateFixedSize(this.chunkConfig);
|
|
114
|
+
break;
|
|
115
|
+
case 'semantic':
|
|
116
|
+
chunks = await this.generateSemantic(this.chunkConfig);
|
|
117
|
+
break;
|
|
118
|
+
case 'document-structure':
|
|
119
|
+
chunks = await this.generateDocumentStructure(this.chunkConfig);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
value: chunks,
|
|
124
|
+
messages: this.messages,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// ─── Strategy 1: Fixed-Size ────────────────────────────────────────────────
|
|
128
|
+
/**
|
|
129
|
+
* Splits the full document text into fixed-size chunks with optional overlap.
|
|
130
|
+
* Attempts to split on natural separators before hard-cutting.
|
|
131
|
+
*/
|
|
132
|
+
async generateFixedSize(config) {
|
|
133
|
+
const { chunkSize, chunkOverlap, separators, lengthFunction: measure } = config;
|
|
134
|
+
// Build a flat text with positional map from top-level nodes
|
|
135
|
+
const { text: fullText, nodeMap } = await this.buildFlatTextWithPositions();
|
|
136
|
+
const rawChunks = this.splitTextRecursively(fullText, chunkSize, chunkOverlap, separators, measure);
|
|
137
|
+
return rawChunks.map(({ text, start, end }) => {
|
|
138
|
+
const chunk = { text, metadata: { sourceType: this.ast.type } };
|
|
139
|
+
if (config.includeMetadata ?? true) {
|
|
140
|
+
this.enrichMetadataFromPosition(chunk, nodeMap, start);
|
|
141
|
+
}
|
|
142
|
+
if (config.addStartIndex) {
|
|
143
|
+
chunk.startIndex = start;
|
|
144
|
+
chunk.endIndex = end;
|
|
145
|
+
}
|
|
146
|
+
return chunk;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Recursively tries separators to split text into chunks of at most `chunkSize`,
|
|
151
|
+
* with `chunkOverlap` characters of overlap between consecutive chunks.
|
|
152
|
+
*/
|
|
153
|
+
splitTextRecursively(text, chunkSize, chunkOverlap, separators, measure) {
|
|
154
|
+
if (measure(text) <= chunkSize) {
|
|
155
|
+
return text.trim() ? [{ text, start: 0, end: text.length }] : [];
|
|
156
|
+
}
|
|
157
|
+
let chosenSep;
|
|
158
|
+
let nextSeparators = [];
|
|
159
|
+
let parts = [];
|
|
160
|
+
let actualSep = '';
|
|
161
|
+
// 1. Try splitting into sentences first
|
|
162
|
+
const sentences = this.splitIntoSentences(text);
|
|
163
|
+
if (sentences.length > 1) {
|
|
164
|
+
parts = sentences;
|
|
165
|
+
actualSep = ' ';
|
|
166
|
+
chosenSep = 'SENTENCE_SPLIT'; // Internal marker to avoid hard-cut
|
|
167
|
+
nextSeparators = separators;
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
// 2. Fallback to the character separators provided in config
|
|
171
|
+
for (let i = 0; i < separators.length; i++) {
|
|
172
|
+
const sep = separators[i];
|
|
173
|
+
parts = sep ? text.split(sep) : [...text];
|
|
174
|
+
if (parts.length > 1) {
|
|
175
|
+
chosenSep = sep;
|
|
176
|
+
actualSep = sep;
|
|
177
|
+
nextSeparators = separators.slice(i + 1);
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (chosenSep === undefined) {
|
|
183
|
+
// Last resort: hard cut
|
|
184
|
+
const results = [];
|
|
185
|
+
let offset = 0;
|
|
186
|
+
while (offset < text.length) {
|
|
187
|
+
const slice = text.slice(offset, offset + chunkSize);
|
|
188
|
+
results.push({ text: slice, start: offset, end: offset + slice.length });
|
|
189
|
+
offset += Math.max(1, chunkSize - chunkOverlap);
|
|
190
|
+
}
|
|
191
|
+
return results;
|
|
192
|
+
}
|
|
193
|
+
const results = [];
|
|
194
|
+
let currentChunk = '';
|
|
195
|
+
let currentStart = 0;
|
|
196
|
+
let absoluteOffset = 0;
|
|
197
|
+
for (let i = 0; i < parts.length; i++) {
|
|
198
|
+
const part = parts[i];
|
|
199
|
+
const candidate = currentChunk ? currentChunk + actualSep + part : part;
|
|
200
|
+
if (measure(candidate) <= chunkSize) {
|
|
201
|
+
currentChunk = candidate;
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
if (currentChunk.trim()) {
|
|
205
|
+
// If currentChunk is still too big (should only happen if it's a single part), recurse!
|
|
206
|
+
if (measure(currentChunk) > chunkSize) {
|
|
207
|
+
const subResults = this.splitTextRecursively(currentChunk, chunkSize, chunkOverlap, nextSeparators, measure);
|
|
208
|
+
for (const r of subResults) {
|
|
209
|
+
results.push({ text: r.text, start: currentStart + r.start, end: currentStart + r.end });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
results.push({ text: currentChunk, start: currentStart, end: currentStart + currentChunk.length });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Start next chunk
|
|
217
|
+
if (chunkOverlap > 0 && currentChunk.length > chunkOverlap && measure(currentChunk) <= chunkSize) {
|
|
218
|
+
const overlapText = currentChunk.slice(-chunkOverlap);
|
|
219
|
+
currentStart = absoluteOffset - chunkOverlap;
|
|
220
|
+
currentChunk = overlapText + actualSep + part;
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
currentStart = absoluteOffset;
|
|
224
|
+
currentChunk = part;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
absoluteOffset += part.length + actualSep.length;
|
|
228
|
+
}
|
|
229
|
+
if (currentChunk.trim()) {
|
|
230
|
+
if (measure(currentChunk) > chunkSize) {
|
|
231
|
+
const subResults = this.splitTextRecursively(currentChunk, chunkSize, chunkOverlap, nextSeparators, measure);
|
|
232
|
+
for (const r of subResults) {
|
|
233
|
+
results.push({ text: r.text, start: currentStart + r.start, end: currentStart + r.end });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
results.push({ text: currentChunk, start: currentStart, end: currentStart + currentChunk.length });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return results;
|
|
241
|
+
}
|
|
242
|
+
// ─── Strategy 2: Document Structure ───────────────────────────────────────
|
|
243
|
+
/**
|
|
244
|
+
* Walks the AST and splits at the designated structural boundaries (slide, page, heading, paragraph).
|
|
245
|
+
*/
|
|
246
|
+
async generateDocumentStructure(config) {
|
|
247
|
+
const { splitBy, maxChunkSize, lengthFunction: measure } = config;
|
|
248
|
+
const chunks = [];
|
|
249
|
+
const contextStack = {};
|
|
250
|
+
// Walk top-level nodes and decide where to cut
|
|
251
|
+
for (const node of this.ast.content) {
|
|
252
|
+
await this.processNodeForStructure(node, config, splitBy, maxChunkSize, measure, chunks, contextStack);
|
|
253
|
+
}
|
|
254
|
+
return this.finalizeChunks(chunks, config);
|
|
255
|
+
}
|
|
256
|
+
async processNodeForStructure(node, config, splitBy, maxChunkSize, measure, chunks, contextStack) {
|
|
257
|
+
(0, errorUtils_js_1.checkAbortSignal)(this.config.abortSignal);
|
|
258
|
+
// Check for node override or skip
|
|
259
|
+
const override = await this.handleOnNode(node);
|
|
260
|
+
if (override === false)
|
|
261
|
+
return;
|
|
262
|
+
// Update context from structural container nodes
|
|
263
|
+
if (node.type === 'slide') {
|
|
264
|
+
const meta = node.metadata;
|
|
265
|
+
contextStack.slideNumber = meta?.slideNumber;
|
|
266
|
+
}
|
|
267
|
+
else if (node.type === 'page') {
|
|
268
|
+
const meta = node.metadata;
|
|
269
|
+
contextStack.pageNumber = meta?.pageNumber;
|
|
270
|
+
}
|
|
271
|
+
else if (node.type === 'sheet') {
|
|
272
|
+
const meta = node.metadata;
|
|
273
|
+
contextStack.sheetName = meta?.sheetName;
|
|
274
|
+
}
|
|
275
|
+
else if (node.type === 'heading') {
|
|
276
|
+
contextStack.heading = node.text;
|
|
277
|
+
}
|
|
278
|
+
const isStructuralBoundary = this.isStructuralBoundary(node, splitBy);
|
|
279
|
+
const isForcedSplit = splitBy === 'slide' && node.type === 'slide'
|
|
280
|
+
|| splitBy === 'page' && node.type === 'page'
|
|
281
|
+
|| splitBy === 'sheet' && node.type === 'sheet';
|
|
282
|
+
if (isForcedSplit) {
|
|
283
|
+
// Process children of the container as individual chunks within the boundary
|
|
284
|
+
if (node.children || node.notes) {
|
|
285
|
+
const innerChunks = [];
|
|
286
|
+
if (node.children) {
|
|
287
|
+
for (const child of node.children) {
|
|
288
|
+
await this.processNodeForStructure(child, config, 'paragraph', maxChunkSize, measure, innerChunks, contextStack);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (node.notes) {
|
|
292
|
+
for (const note of node.notes) {
|
|
293
|
+
await this.processNodeForStructure(note, config, 'paragraph', maxChunkSize, measure, innerChunks, contextStack);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
for (const ic of innerChunks) {
|
|
297
|
+
ic.metadata.slideNumber = contextStack.slideNumber;
|
|
298
|
+
ic.metadata.pageNumber = contextStack.pageNumber;
|
|
299
|
+
ic.metadata.sheetName = contextStack.sheetName;
|
|
300
|
+
chunks.push(ic);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (node.type === 'table') {
|
|
306
|
+
await this.processTableNode(node, config, maxChunkSize, measure, chunks, contextStack);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const isContentNode = node.type === 'paragraph' || node.type === 'heading' || node.type === 'list' || node.type === 'code' || node.type === 'cell' || (node.text && (!node.children || node.children.length === 0));
|
|
310
|
+
if (isStructuralBoundary || isContentNode) {
|
|
311
|
+
const text = typeof override === 'string' ? override : collectNodeText(node);
|
|
312
|
+
const isWhitespaceOnly = !text.trim() && !text.includes('\u00A0');
|
|
313
|
+
if (isWhitespaceOnly && text.length > 0) {
|
|
314
|
+
// Log skipped empty nodes if debugging
|
|
315
|
+
// Only warn for non-cell nodes to reduce spreadsheet noise
|
|
316
|
+
if (node.type !== 'cell') {
|
|
317
|
+
this.warn(types_js_1.OfficeWarningType.WHITESPACE_NODE_SKIPPED, node.type, node);
|
|
318
|
+
}
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (node.type === 'heading')
|
|
322
|
+
contextStack.heading = text;
|
|
323
|
+
const chunk = {
|
|
324
|
+
text,
|
|
325
|
+
metadata: {
|
|
326
|
+
sourceType: this.ast.type,
|
|
327
|
+
closestHeading: contextStack.heading,
|
|
328
|
+
slideNumber: contextStack.slideNumber,
|
|
329
|
+
pageNumber: contextStack.pageNumber,
|
|
330
|
+
sheetName: contextStack.sheetName,
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
// If this chunk is too big, further split it
|
|
334
|
+
if (measure(text) > maxChunkSize) {
|
|
335
|
+
const subChunks = this.splitTextRecursively(text, maxChunkSize, 0, ['\n\n', '\n', ' ', ''], measure);
|
|
336
|
+
for (const sub of subChunks) {
|
|
337
|
+
chunks.push({ ...chunk, text: sub.text });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
chunks.push(chunk);
|
|
342
|
+
}
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
// Recurse into children and notes for container nodes
|
|
346
|
+
if (node.children) {
|
|
347
|
+
for (const child of node.children) {
|
|
348
|
+
await this.processNodeForStructure(child, config, splitBy, maxChunkSize, measure, chunks, contextStack);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (node.notes) {
|
|
352
|
+
for (const note of node.notes) {
|
|
353
|
+
await this.processNodeForStructure(note, config, splitBy, maxChunkSize, measure, chunks, contextStack);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
isStructuralBoundary(node, splitBy) {
|
|
358
|
+
if (splitBy === 'heading')
|
|
359
|
+
return node.type === 'heading';
|
|
360
|
+
if (splitBy === 'paragraph')
|
|
361
|
+
return node.type === 'paragraph' || node.type === 'heading' || node.type === 'cell';
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Handles table chunking with the configured tableSplitStrategy.
|
|
366
|
+
* 'row': keeps header row attached to every chunk.
|
|
367
|
+
* 'flatten': converts table to text and splits normally.
|
|
368
|
+
*/
|
|
369
|
+
async processTableNode(node, config, maxChunkSize, measure, chunks, contextStack) {
|
|
370
|
+
const strategy = config.tableSplitStrategy;
|
|
371
|
+
if (strategy === 'flatten' || !node.children || node.children.length === 0) {
|
|
372
|
+
// Flatten: treat as plain text (collect from children for HTML/MD-origin tables).
|
|
373
|
+
const text = collectNodeText(node);
|
|
374
|
+
if (!text.trim())
|
|
375
|
+
return;
|
|
376
|
+
chunks.push({
|
|
377
|
+
text,
|
|
378
|
+
metadata: {
|
|
379
|
+
sourceType: this.ast.type,
|
|
380
|
+
closestHeading: contextStack.heading,
|
|
381
|
+
slideNumber: contextStack.slideNumber,
|
|
382
|
+
pageNumber: contextStack.pageNumber,
|
|
383
|
+
sheetName: contextStack.sheetName,
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
// 'row' strategy: extract header row(s) and chunk remaining rows
|
|
389
|
+
const rows = node.children; // Each child is a 'row' node
|
|
390
|
+
const headerRows = [];
|
|
391
|
+
const dataRows = [];
|
|
392
|
+
// Heuristic: first row is the header
|
|
393
|
+
if (rows.length > 0) {
|
|
394
|
+
const firstRow = rows[0];
|
|
395
|
+
const override = await this.handleOnNode(firstRow);
|
|
396
|
+
if (override !== false) {
|
|
397
|
+
// If overridden, we use the string as header, but we still treat it as a header
|
|
398
|
+
headerRows.push(firstRow); // We still add the node to get metadata later if needed, but renderRowsAsText will handle it?
|
|
399
|
+
// Actually renderRowsAsText needs to be updated too.
|
|
400
|
+
}
|
|
401
|
+
for (let i = 1; i < rows.length; i++) {
|
|
402
|
+
dataRows.push(rows[i]);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const headerText = await this.renderRowsAsText(headerRows);
|
|
406
|
+
const baseMetadata = {
|
|
407
|
+
sourceType: this.ast.type,
|
|
408
|
+
closestHeading: contextStack.heading,
|
|
409
|
+
slideNumber: contextStack.slideNumber,
|
|
410
|
+
pageNumber: contextStack.pageNumber,
|
|
411
|
+
sheetName: contextStack.sheetName,
|
|
412
|
+
isTableChunk: true,
|
|
413
|
+
};
|
|
414
|
+
// Group data rows into chunks
|
|
415
|
+
let currentRows = [];
|
|
416
|
+
let currentSize = headerText ? measure(headerText) : 0;
|
|
417
|
+
const flushCurrentRows = async () => {
|
|
418
|
+
if (currentRows.length === 0)
|
|
419
|
+
return;
|
|
420
|
+
const rowText = await this.renderRowsAsText(currentRows);
|
|
421
|
+
const chunkText = headerText ? `${headerText}\n${rowText}` : rowText;
|
|
422
|
+
if (chunkText.trim()) {
|
|
423
|
+
if (measure(chunkText) > maxChunkSize) {
|
|
424
|
+
// Fallback to recursive splitting for oversized table chunks
|
|
425
|
+
const subChunks = this.splitTextRecursively(chunkText, maxChunkSize, 0, ['\n', ' ', ''], measure);
|
|
426
|
+
for (const sub of subChunks) {
|
|
427
|
+
chunks.push({ text: sub.text, metadata: { ...baseMetadata } });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
chunks.push({ text: chunkText, metadata: { ...baseMetadata } });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
currentRows = [];
|
|
435
|
+
currentSize = headerText ? measure(headerText) : 0;
|
|
436
|
+
};
|
|
437
|
+
for (const row of dataRows) {
|
|
438
|
+
const override = await this.handleOnNode(row);
|
|
439
|
+
if (override === false)
|
|
440
|
+
continue;
|
|
441
|
+
const rowText = typeof override === 'string' ? override : await this.renderRowsAsText([row]);
|
|
442
|
+
const rowSize = measure(rowText);
|
|
443
|
+
if (currentSize + rowSize > maxChunkSize && currentRows.length > 0) {
|
|
444
|
+
await flushCurrentRows();
|
|
445
|
+
}
|
|
446
|
+
if (typeof override === 'string') {
|
|
447
|
+
// If row was overridden, we flush current, then add the override as a chunk.
|
|
448
|
+
await flushCurrentRows();
|
|
449
|
+
const chunkText = headerText ? `${headerText}\n${override}` : override;
|
|
450
|
+
chunks.push({ text: chunkText, metadata: { ...baseMetadata } });
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
currentRows.push(row);
|
|
454
|
+
currentSize += rowSize;
|
|
455
|
+
}
|
|
456
|
+
await flushCurrentRows();
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Renders a list of row nodes as a pipe-separated text string.
|
|
460
|
+
*/
|
|
461
|
+
async renderRowsAsText(rows) {
|
|
462
|
+
const renderedRows = [];
|
|
463
|
+
for (const row of rows) {
|
|
464
|
+
const override = await this.handleOnNode(row);
|
|
465
|
+
if (override === false)
|
|
466
|
+
continue;
|
|
467
|
+
if (typeof override === 'string') {
|
|
468
|
+
renderedRows.push(override);
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
if (!row.children) {
|
|
472
|
+
renderedRows.push(row.text ?? '');
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
const getCellText = (cell) => {
|
|
476
|
+
if (cell.text)
|
|
477
|
+
return cell.text;
|
|
478
|
+
if (!cell.children || cell.children.length === 0)
|
|
479
|
+
return '';
|
|
480
|
+
return cell.children.map(c => getCellText(c)).join(' ');
|
|
481
|
+
};
|
|
482
|
+
const cells = row.children.map(cell => getCellText(cell).replace(/\n/g, ' ').trim());
|
|
483
|
+
renderedRows.push(`| ${cells.join(' | ')} |`);
|
|
484
|
+
}
|
|
485
|
+
return renderedRows.join('\n');
|
|
486
|
+
}
|
|
487
|
+
// ─── Strategy 3: Semantic ─────────────────────────────────────────────────
|
|
488
|
+
/**
|
|
489
|
+
* Splits document into semantically coherent chunks using cosine similarity
|
|
490
|
+
* between sentence embeddings. A new chunk begins when similarity drops
|
|
491
|
+
* below `similarityThreshold`.
|
|
492
|
+
*/
|
|
493
|
+
async generateSemantic(config) {
|
|
494
|
+
const { similarityThreshold: threshold, maxChunkSize, bufferSize, lengthFunction: measure, embeddingBatchSize: batchSize } = config;
|
|
495
|
+
if (typeof config.embeddingFunction !== 'function') {
|
|
496
|
+
throw (0, errorUtils_js_1.getOfficeError)(types_js_1.OfficeErrorType.MISSING_EMBEDDING_FUNCTION, this.config);
|
|
497
|
+
}
|
|
498
|
+
// Extract all leaf-level text sentences from the AST
|
|
499
|
+
const sentences = await this.extractSentences();
|
|
500
|
+
if (sentences.length === 0)
|
|
501
|
+
return [];
|
|
502
|
+
// Embed all sentences in batches to avoid rate limiting
|
|
503
|
+
const embeddings = await this.batchEmbeddings(sentences, config.embeddingFunction, batchSize, config.timeout);
|
|
504
|
+
// Calculate cosine similarity between adjacent sentence windows
|
|
505
|
+
const chunks = [];
|
|
506
|
+
let currentSentences = [];
|
|
507
|
+
let currentSize = 0;
|
|
508
|
+
for (let i = 0; i < sentences.length; i++) {
|
|
509
|
+
const sentenceText = sentences[i].text;
|
|
510
|
+
currentSentences.push(sentences[i]);
|
|
511
|
+
currentSize += measure(sentenceText);
|
|
512
|
+
// Check if we should split here
|
|
513
|
+
const isLast = i === sentences.length - 1;
|
|
514
|
+
const exceedsMax = currentSize > maxChunkSize;
|
|
515
|
+
let shouldSplit = isLast || exceedsMax;
|
|
516
|
+
if (!shouldSplit && i < sentences.length - 1) {
|
|
517
|
+
// Compare the current window with the next window
|
|
518
|
+
const currentWindowEnd = Math.min(i + bufferSize, sentences.length - 1);
|
|
519
|
+
const nextWindowStart = i + 1;
|
|
520
|
+
const nextWindowEnd = Math.min(i + 1 + bufferSize, sentences.length - 1);
|
|
521
|
+
const currentEmbedding = this.averageEmbeddings(embeddings.slice(Math.max(0, i - bufferSize + 1), currentWindowEnd + 1));
|
|
522
|
+
const nextEmbedding = this.averageEmbeddings(embeddings.slice(nextWindowStart, nextWindowEnd + 1));
|
|
523
|
+
const similarity = this.cosineSimilarity(currentEmbedding, nextEmbedding);
|
|
524
|
+
if (similarity < threshold) {
|
|
525
|
+
shouldSplit = true;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (shouldSplit && currentSentences.length > 0) {
|
|
529
|
+
const chunkText = currentSentences.map(s => s.text).join(' ');
|
|
530
|
+
const firstSentence = currentSentences[0];
|
|
531
|
+
const baseMetadata = {
|
|
532
|
+
sourceType: this.ast.type,
|
|
533
|
+
closestHeading: firstSentence.closestHeading,
|
|
534
|
+
slideNumber: firstSentence.slideNumber,
|
|
535
|
+
pageNumber: firstSentence.pageNumber,
|
|
536
|
+
sheetName: firstSentence.sheetName,
|
|
537
|
+
};
|
|
538
|
+
if (measure(chunkText) > maxChunkSize) {
|
|
539
|
+
// Fallback to recursive splitting for oversized semantic chunks
|
|
540
|
+
const subChunks = this.splitTextRecursively(chunkText, maxChunkSize, 0, ['\n', ' ', ''], measure);
|
|
541
|
+
for (const sub of subChunks) {
|
|
542
|
+
chunks.push({
|
|
543
|
+
text: sub.text,
|
|
544
|
+
metadata: { ...baseMetadata }
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
chunks.push({
|
|
550
|
+
text: chunkText,
|
|
551
|
+
metadata: baseMetadata
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
currentSentences = [];
|
|
555
|
+
currentSize = 0;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return this.finalizeChunks(chunks, config);
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Extracts all text sentences from the AST with their contextual metadata.
|
|
562
|
+
*/
|
|
563
|
+
async extractSentences() {
|
|
564
|
+
const results = [];
|
|
565
|
+
let currentHeading;
|
|
566
|
+
let currentSlide;
|
|
567
|
+
let currentPage;
|
|
568
|
+
let currentSheet;
|
|
569
|
+
const walk = async (node) => {
|
|
570
|
+
(0, errorUtils_js_1.checkAbortSignal)(this.config.abortSignal);
|
|
571
|
+
const override = await this.handleOnNode(node);
|
|
572
|
+
if (override === false)
|
|
573
|
+
return;
|
|
574
|
+
if (node.type === 'heading')
|
|
575
|
+
currentHeading = node.text;
|
|
576
|
+
if (node.type === 'slide')
|
|
577
|
+
currentSlide = node.metadata?.slideNumber;
|
|
578
|
+
if (node.type === 'page')
|
|
579
|
+
currentPage = node.metadata?.pageNumber;
|
|
580
|
+
if (node.type === 'sheet')
|
|
581
|
+
currentSheet = node.metadata?.sheetName;
|
|
582
|
+
const isContentNode = node.type === 'paragraph' || node.type === 'heading' || node.type === 'list' || node.type === 'cell' || (node.text && (!node.children || node.children.length === 0));
|
|
583
|
+
if (isContentNode) {
|
|
584
|
+
const text = (typeof override === 'string' ? override : collectNodeText(node)).trim();
|
|
585
|
+
if (!text)
|
|
586
|
+
return;
|
|
587
|
+
// Split paragraph text into individual sentences for finer-grained similarity
|
|
588
|
+
const sentences = this.splitIntoSentences(text);
|
|
589
|
+
for (const sentence of sentences) {
|
|
590
|
+
if (sentence.trim()) {
|
|
591
|
+
results.push({
|
|
592
|
+
text: sentence.trim(),
|
|
593
|
+
closestHeading: currentHeading,
|
|
594
|
+
slideNumber: currentSlide,
|
|
595
|
+
pageNumber: currentPage,
|
|
596
|
+
sheetName: currentSheet,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return; // don't recurse into children; we already have the text
|
|
601
|
+
}
|
|
602
|
+
if (node.children) {
|
|
603
|
+
for (const child of node.children)
|
|
604
|
+
await walk(child);
|
|
605
|
+
}
|
|
606
|
+
if (node.notes) {
|
|
607
|
+
for (const note of node.notes)
|
|
608
|
+
await walk(note);
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
for (const node of this.ast.content)
|
|
612
|
+
await walk(node);
|
|
613
|
+
return results;
|
|
614
|
+
}
|
|
615
|
+
// ─── Shared Utilities ─────────────────────────────────────────────────────
|
|
616
|
+
/**
|
|
617
|
+
* Builds a flat text string from the entire document and a map of
|
|
618
|
+
* character offsets to AST node metadata for position-based metadata lookups.
|
|
619
|
+
*/
|
|
620
|
+
async buildFlatTextWithPositions() {
|
|
621
|
+
const parts = [];
|
|
622
|
+
const nodeMap = [];
|
|
623
|
+
let offset = 0;
|
|
624
|
+
let currentHeading;
|
|
625
|
+
let currentSlide;
|
|
626
|
+
let currentPage;
|
|
627
|
+
let currentSheet;
|
|
628
|
+
const walk = async (node) => {
|
|
629
|
+
(0, errorUtils_js_1.checkAbortSignal)(this.config.abortSignal);
|
|
630
|
+
const override = await this.handleOnNode(node);
|
|
631
|
+
if (override === false)
|
|
632
|
+
return;
|
|
633
|
+
if (node.type === 'heading')
|
|
634
|
+
currentHeading = node.text;
|
|
635
|
+
if (node.type === 'slide')
|
|
636
|
+
currentSlide = node.metadata?.slideNumber;
|
|
637
|
+
if (node.type === 'page')
|
|
638
|
+
currentPage = node.metadata?.pageNumber;
|
|
639
|
+
if (node.type === 'sheet')
|
|
640
|
+
currentSheet = node.metadata?.sheetName;
|
|
641
|
+
const isContentNode = node.type === 'paragraph' || node.type === 'heading' || node.type === 'list' || node.type === 'code' || node.type === 'cell' || (node.text && (!node.children || node.children.length === 0));
|
|
642
|
+
if (isContentNode) {
|
|
643
|
+
const nodeText = typeof override === 'string' ? override : collectNodeText(node);
|
|
644
|
+
const txt = nodeText + '\n';
|
|
645
|
+
nodeMap.push({ offset, heading: currentHeading, slideNumber: currentSlide, pageNumber: currentPage, sheetName: currentSheet });
|
|
646
|
+
parts.push(txt);
|
|
647
|
+
offset += txt.length;
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (node.children) {
|
|
651
|
+
for (const child of node.children)
|
|
652
|
+
await walk(child);
|
|
653
|
+
}
|
|
654
|
+
if (node.notes) {
|
|
655
|
+
for (const note of node.notes)
|
|
656
|
+
await walk(note);
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
for (const node of this.ast.content)
|
|
660
|
+
await walk(node);
|
|
661
|
+
return { text: parts.join(''), nodeMap };
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Finds the closest AST metadata for a given character position.
|
|
665
|
+
*/
|
|
666
|
+
enrichMetadataFromPosition(chunk, nodeMap, charOffset) {
|
|
667
|
+
let best = nodeMap[0];
|
|
668
|
+
for (const entry of nodeMap) {
|
|
669
|
+
if (entry.offset <= charOffset)
|
|
670
|
+
best = entry;
|
|
671
|
+
else
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
674
|
+
if (best) {
|
|
675
|
+
chunk.metadata.closestHeading = best.heading;
|
|
676
|
+
chunk.metadata.slideNumber = best.slideNumber;
|
|
677
|
+
chunk.metadata.pageNumber = best.pageNumber;
|
|
678
|
+
chunk.metadata.sheetName = best.sheetName;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Applies final post-processing: strips whitespace, sets sourceType.
|
|
683
|
+
*/
|
|
684
|
+
finalizeChunks(chunks, config) {
|
|
685
|
+
if (chunks.length === 0) {
|
|
686
|
+
this.warn(types_js_1.OfficeWarningType.EMPTY_CHUNK_GENERATED, this.chunkConfig.strategy);
|
|
687
|
+
}
|
|
688
|
+
return chunks
|
|
689
|
+
.map(chunk => {
|
|
690
|
+
const text = config.stripWhitespace !== false ? chunk.text.trim() : chunk.text;
|
|
691
|
+
const result = { text, metadata: { sourceType: this.ast.type } };
|
|
692
|
+
if (config.includeMetadata !== false) {
|
|
693
|
+
result.metadata = chunk.metadata;
|
|
694
|
+
}
|
|
695
|
+
return result;
|
|
696
|
+
})
|
|
697
|
+
.filter(chunk => chunk.text.length > 0);
|
|
698
|
+
}
|
|
699
|
+
// ─── Embedding Math Utilities ──────────────────────────────────────────────
|
|
700
|
+
/**
|
|
701
|
+
* Helper to process embeddings in sequential batches to avoid API rate limits and memory issues.
|
|
702
|
+
*/
|
|
703
|
+
async batchEmbeddings(sentences, embedFn, batchSize = 50, timeoutMs) {
|
|
704
|
+
const results = [];
|
|
705
|
+
for (let i = 0; i < sentences.length; i += batchSize) {
|
|
706
|
+
(0, errorUtils_js_1.checkAbortSignal)(this.config.abortSignal);
|
|
707
|
+
const batch = sentences.slice(i, i + batchSize);
|
|
708
|
+
const batchPromises = batch.map(s => {
|
|
709
|
+
const call = embedFn(s.text);
|
|
710
|
+
if (timeoutMs !== undefined && timeoutMs > 0) {
|
|
711
|
+
let timerId;
|
|
712
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
713
|
+
timerId = setTimeout(() => {
|
|
714
|
+
reject((0, errorUtils_js_1.getOfficeError)(types_js_1.OfficeErrorType.EMBEDDING_TIMEOUT, this.config, timeoutMs));
|
|
715
|
+
}, timeoutMs);
|
|
716
|
+
});
|
|
717
|
+
return Promise.race([call, timeoutPromise]).finally(() => {
|
|
718
|
+
clearTimeout(timerId);
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
return call;
|
|
722
|
+
});
|
|
723
|
+
const batchResults = await Promise.all(batchPromises);
|
|
724
|
+
results.push(...batchResults);
|
|
725
|
+
}
|
|
726
|
+
return results;
|
|
727
|
+
}
|
|
728
|
+
cosineSimilarity(a, b) {
|
|
729
|
+
if (a.length !== b.length || a.length === 0)
|
|
730
|
+
return 0;
|
|
731
|
+
let dot = 0, normA = 0, normB = 0;
|
|
732
|
+
for (let i = 0; i < a.length; i++) {
|
|
733
|
+
dot += a[i] * b[i];
|
|
734
|
+
normA += a[i] * a[i];
|
|
735
|
+
normB += b[i] * b[i];
|
|
736
|
+
}
|
|
737
|
+
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
738
|
+
return denom === 0 ? 0 : dot / denom;
|
|
739
|
+
}
|
|
740
|
+
averageEmbeddings(embeddings) {
|
|
741
|
+
if (embeddings.length === 0)
|
|
742
|
+
return [];
|
|
743
|
+
const len = embeddings[0].length;
|
|
744
|
+
const avg = new Array(len).fill(0);
|
|
745
|
+
for (const emb of embeddings) {
|
|
746
|
+
for (let i = 0; i < len; i++)
|
|
747
|
+
avg[i] += emb[i];
|
|
748
|
+
}
|
|
749
|
+
return avg.map(v => v / embeddings.length);
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Robustly splits text into sentences, respecting abbreviations and non-Western punctuation.
|
|
753
|
+
*/
|
|
754
|
+
splitIntoSentences(text) {
|
|
755
|
+
if (this.isCustomRegex) {
|
|
756
|
+
const userRegex = this.chunkConfig.sentenceBoundaryRegex;
|
|
757
|
+
const regex = typeof userRegex === 'string' ? new RegExp(userRegex, 'g') : userRegex;
|
|
758
|
+
// Split while keeping the separator if possible, or just split
|
|
759
|
+
return text.split(regex).map(s => s.trim()).filter(Boolean);
|
|
760
|
+
}
|
|
761
|
+
const abbreviations = this.chunkConfig.abbreviations;
|
|
762
|
+
const sentences = [];
|
|
763
|
+
let start = 0;
|
|
764
|
+
// Japanese full stop: 。 Exclamation: ! Question: ?
|
|
765
|
+
// Western: . ! ?
|
|
766
|
+
const markRegex = /[.!?。!?]/g;
|
|
767
|
+
let match;
|
|
768
|
+
while ((match = markRegex.exec(text)) !== null) {
|
|
769
|
+
const mark = match[0];
|
|
770
|
+
const pos = match.index;
|
|
771
|
+
const nextChar = text[pos + 1];
|
|
772
|
+
const isAtEnd = pos === text.length - 1;
|
|
773
|
+
const isFollowedByWhitespace = !nextChar || /\s/.test(nextChar);
|
|
774
|
+
const isJapaneseMark = /[。!?]/.test(mark);
|
|
775
|
+
if (isFollowedByWhitespace || isJapaneseMark) {
|
|
776
|
+
// Check for abbreviations (only for period)
|
|
777
|
+
if (mark === '.') {
|
|
778
|
+
const prevSpace = text.lastIndexOf(' ', pos - 1);
|
|
779
|
+
let lastWord = text.substring(prevSpace + 1, pos);
|
|
780
|
+
// Strip punctuation like quotes, parentheses, brackets
|
|
781
|
+
lastWord = lastWord.replace(/^[^\w]+|[^\w]+$/g, '');
|
|
782
|
+
if (abbreviations.includes(lastWord))
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
sentences.push(text.substring(start, pos + 1).trim());
|
|
786
|
+
start = pos + 1;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
if (start < text.length) {
|
|
790
|
+
const remaining = text.substring(start).trim();
|
|
791
|
+
if (remaining)
|
|
792
|
+
sentences.push(remaining);
|
|
793
|
+
}
|
|
794
|
+
return sentences.length > 0 ? sentences : [text];
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
exports.ChunkingGenerator = ChunkingGenerator;
|