Files
ragflow/internal/ingestion/component/schema/chunker.go
T

473 lines
18 KiB
Go
Raw Normal View History

//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package schema
Chunker: port Python overlapped_percent normalization and BULLET_PATTERN title fallback (#17396) ## Summary Closes two Chunker migration gaps documented in `docs/migration_python_go_diff.md` (diffs **2.6** and **1.7**), improving parity with the Python ingestion pipeline. This is the code portion of commit `261e1fd0b` on branch `fix/batch-3-4`; the migration document itself is tracked separately (untracked in this commit). ### Diff 2.6 — `overlapped_percent` missing Python normalization Python's `normalize_overlapped_percent` (`common/float_utils.py:50-58`) accepts a `[0,1)` fraction (the flow-canvas UI validates `[0,1)`), multiplies it by 100, `int()`-truncates, and clamps to `[0,90]`. Go previously only accepted a raw `[0,90]` percentage and **rejected** out-of-range input, so a Python config passing `0.1` (meaning 10%) silently produced ~0% overlap. Added `normalizeOverlappedPercent` (`internal/ingestion/component/chunker/common.go`) mirroring the Python helper: - parses numbers and numeric strings (mirrors Python `float()`; bad / `NaN` / `Inf` → `0`), - `0 < v < 1 → v *= 100`, - `int()` truncation, - clamps to `[0,90]`. Wired into `tokenChunkerParam.Update` (`token.go`); the merge math (`token.go:705`, `(100-x)/100`) already matched Python. `TokenChunkerParam.Validate` (`schema/chunker.go`) now only guards direct struct construction. Tests: `TestNormalizeOverlappedPercent`, extended `TestTokenChunker_NewAcceptsPythonOverlappedRange` (adds fraction/clamp inputs), new `TestTokenChunker_NormalizesOverlappedPercent`. The existing reject-test cases for `<0` / `>90` were removed because they are now normalized/clamped (Python parity). ### Diff 1.7 — missing `BULLET_PATTERN` title-level fallback `resolveTitleLevels` (`title.go`) now applies a 4th-level fallback: when outline + regex + layout all yield body level, `bulletsCategory` selects the best-matching bullet-pattern group (Chinese legal / numbering / Chinese numbering / English legal — mirroring `rag/nlp/__init__.py:258-320`) and assigns structural levels. Guarded by `allBodyLevel` so it never overrides an existing outline/regex level. Tests: `TestResolveTitleLevels_BulletFallback` (4 subtests). ### Incidental test adjustments included in the commit - `token_batch1_test.go`: overlap input changed `0.3` → `30.0` to reflect the post-normalization 30% semantics. - `real_consumer_test.go`: updated `LoadFromIngestionTask(task)` → `LoadFromIngestionTask(ctx, task)` for the new context-first signature. ## Verification `bash build.sh --test ./internal/ingestion/component/...` — chunker + schema suites pass, no regression (CGO build). ## Migration doc reference `docs/migration_python_go_diff.md` §Chunker 1.7 and 2.6 are marked **Fixed** for these changes.
2026-07-27 14:42:44 +08:00
import (
"fmt"
"math"
"strconv"
"strings"
)
// ChunkerFromUpstream is the shared upstream payload consumed by all four
// chunker variants (TokenChunker, TitleChunker, GroupTitleChunker,
// HierarchyTitleChunker).
//
// It mirrors the wire shape defined across two equivalent Pydantic
// schemas in the Python codebase:
//
// rag/flow/chunker/schema.py:TokenChunkerFromUpstream
// rag/flow/chunker/title_chunker/schema.py:TitleChunkerFromUpstream
//
// Both classes are field-identical (apart from the class name) — the
// variants share one upstream payload; their *Param structs differ.
//
// Wire shape (Pydantic):
//
// created_time: float | None (alias _created_time)
// elapsed_time: float | None (alias _elapsed_time)
// name: str (required)
// file: dict | None
// chunks: list[dict] | None
// output_format: Literal["json","markdown","text","html","chunks"] | None
// json_result: list[dict] | None (alias "json")
// markdown_result: str | None (alias "markdown")
// text_result: str | None (alias "text")
// html_result: str | None (alias "html")
type ChunkerFromUpstream struct {
CreatedTime *float64 `json:"_created_time,omitempty"`
ElapsedTime *float64 `json:"_elapsed_time,omitempty"`
// Name is the source document name. Required.
Name string `json:"name"`
feat(agent): Go ingestion pipeline progress mirroring and DeepDOC parser hardening (#16795) feat(ingestion): mirror Go pipeline progress into the document table; harden resume guards - pipeline: bind the owning document via WithDocumentID; after each TrackProgress event aggregate ingestion_task_log progress and mirror progress/run/progress_msg back into the document table, so GET /api/v1/datasets/{dataset_id}/documents reflects live Go pipeline progress without a bespoke endpoint. - canvas: extend the S3 resume guard to reject legacy no-op nodes (e.g. ExitLoop) so component_total equals the count of progress-reporting components and the aggregate percent can reach 100%. - runtime/canvas: route progress through TrackProgress; add interrupt test coverage (r3_interrupt_test.go). - dao/entity: add IngestionTask.DocumentID column and AggregateProgress support used by the mirror; IngestionTaskLog keeps a Checkpoint column alongside the progress fields. feat(deepdoc): cache DocAnalyzer inference results in Redis (1h TTL) - Redis-backed DocAnalyzerCache decorator over inference.Client; cache key = "ddoc:cache:<method>:" + sha256 of the JPEG-encoded image bytes (deterministic). - TTL = 1h; hits skip the inner HTTP call and return cached JSON; inner errors are not cached. refactor(deepdoc): align figure cropping with Python cropout + bounded page caches - CropSectionByDLA mirrors Python cropout: best-overlap DLA figure/equation region, fallback to section bbox per page, vertical concat on gray background. - sliding-window page-image cache bounds peak memory to the recent window instead of the whole PDF. - rename DLADebug -> DLARegions across parser/chunker/tests. refactor(parser): drop lib_type selector; align NewXxxParser with NewPDFParser - remove config["lib_type"] lookup and the libType param/field/switch from all nine constructors; surface the CGO-required error at ParseWithResult time instead of construction time; drop resolveLibType, its test, and the four lib_type constants. feat(utility): add a reusable workerpool for bounded concurrent execution - internal/utility/workerpool.go (+ tests). refactor: translate Chinese prose comments to English in non-harness Go files. chore: upgrade github.com/cloudwego/eino from v0.9.9 to v0.9.12.
2026-07-10 10:36:10 +08:00
// DocID is the originating document ID. When set, the chunker can
// re-resolve the source PDF from storage to crop section images on
// demand, instead of carrying the binary across the component
// boundary.
DocID string `json:"doc_id,omitempty"`
// Bucket and Path are the explicit storage location of the source
// PDF. They take precedence over DocID-driven resolution.
Bucket string `json:"bucket,omitempty"`
Path string `json:"path,omitempty"`
// File is the optional upstream file descriptor.
File *ChunkerFileMeta `json:"file,omitempty"`
// Chunks is the upstream chunk list, set when output_format == "chunks".
Chunks []ChunkDoc `json:"chunks,omitempty"`
// OutputFormat controls which of the *Result fields below is the
// active payload. Allowed values:
// "json" -> JSONResult
// "markdown" -> MarkdownResult
// "text" -> TextResult
// "html" -> HTMLResult
// "chunks" -> Chunks
OutputFormat PayloadFormat `json:"output_format,omitempty"`
// JSONResult is the upstream structured JSON list (alias "json" in
// Python). Set when OutputFormat == "json".
JSONResult []ChunkDoc `json:"json,omitempty"`
// MarkdownResult is the upstream markdown payload (alias "markdown").
// Set when OutputFormat == "markdown".
MarkdownResult *string `json:"markdown,omitempty"`
// TextResult is the upstream plain-text payload (alias "text").
// Set when OutputFormat == "text".
TextResult *string `json:"text,omitempty"`
// HTMLResult is the upstream HTML payload (alias "html").
// Set when OutputFormat == "html".
HTMLResult *string `json:"html,omitempty"`
}
// Validate enforces the only required field in ChunkerFromUpstream: Name.
// OutputFormat is not strictly required (defaults to "" and the
// component decides what to do with an empty payload), but the
// combination of `OutputFormat == "chunks"` with a non-nil Chunks is the
// happy path.
func (c *ChunkerFromUpstream) Validate() error {
if c.Name == "" {
return errRequiredField{Field: "name"}
}
if !c.OutputFormat.isKnown() {
return errInvalidValue{Field: "output_format", Value: string(c.OutputFormat)}
}
switch c.OutputFormat {
case PayloadFormatChunks:
return nil
case PayloadFormatJSON:
if c.JSONResult == nil {
return errRequiredField{Field: "json"}
}
case PayloadFormatMarkdown:
if c.MarkdownResult == nil {
return errRequiredField{Field: "markdown"}
}
case PayloadFormatText:
if c.TextResult == nil {
return errRequiredField{Field: "text"}
}
case PayloadFormatHTML:
if c.HTMLResult == nil {
return errRequiredField{Field: "html"}
}
}
return nil
}
// ChunkerOutputs is the result of invoking any chunker variant. All
// four chunker components emit the same shape: a list of chunk maps
// under the "chunks" key, plus a marker output_format = "chunks".
//
// Mirrors what each chunker sets at the end of _invoke:
//
// self.set_output("output_format", "chunks")
// self.set_output("chunks", chunks)
//
// Unlike the Python runtime (which auto-merges every input kwarg into the
// output via ProcessBase.invoke), the Go runtime only forwards the explicit
// component output to the next node. The run-level metadata that downstream
// consumers still need (e.g. Tokenizer reads `name` for title embedding, and
// tenant_id/kb_id for embedding-model resolution) therefore lives in the
// workflow-wide CanvasState.Globals bag — seeded at pipeline start and
// published by the File component — and is read directly from ctx, not
// re-emitted by each chunker. The fields below are the chunker-owned outputs.
type ChunkerOutputs struct {
// OutputFormat is always "chunks" on success.
OutputFormat PayloadFormat `json:"output_format,omitempty"`
// Chunks is the produced chunk list. Each entry is a free-form map
// mirroring the dict shape the Python code builds (text, doc_type_kwd,
// tk_nums, PDF_POSITIONS_KEY, mom, img_id, etc.).
Chunks []ChunkDoc `json:"chunks,omitempty"`
// Error is set when the component short-circuits with an error
// message (Python: set_output("_ERROR", ...)).
Error string `json:"_ERROR,omitempty"`
}
// ---------------------------------------------------------------------------
// TokenChunkerParam
// ---------------------------------------------------------------------------
//
// Mirrors rag/flow/chunker/token_chunker.py:TokenChunkerParam.__init__.
// All fields are user-tunable; defaults match the Python values.
type TokenChunkerParam struct {
// DelimiterMode selects the chunking strategy.
// Allowed value: "delimiter". The legacy "token_size" value is accepted for
// backward compatibility and normalized to "delimiter" (it is behaviorally
// identical at runtime; see Validate). The single-chunk "one" behavior is
// provided by the separate OneChunker component.
DelimiterMode string `json:"delimiter_mode"`
// ChunkTokenSize is the target chunk size in tokens.
ChunkTokenSize int `json:"chunk_token_size"`
// Delimiters is the list of split tokens. Strings wrapped in
// backticks (e.g., "`\\n`") denote user-defined regex split points.
Delimiters []string `json:"delimiters"`
Chunker: port Python overlapped_percent normalization and BULLET_PATTERN title fallback (#17396) ## Summary Closes two Chunker migration gaps documented in `docs/migration_python_go_diff.md` (diffs **2.6** and **1.7**), improving parity with the Python ingestion pipeline. This is the code portion of commit `261e1fd0b` on branch `fix/batch-3-4`; the migration document itself is tracked separately (untracked in this commit). ### Diff 2.6 — `overlapped_percent` missing Python normalization Python's `normalize_overlapped_percent` (`common/float_utils.py:50-58`) accepts a `[0,1)` fraction (the flow-canvas UI validates `[0,1)`), multiplies it by 100, `int()`-truncates, and clamps to `[0,90]`. Go previously only accepted a raw `[0,90]` percentage and **rejected** out-of-range input, so a Python config passing `0.1` (meaning 10%) silently produced ~0% overlap. Added `normalizeOverlappedPercent` (`internal/ingestion/component/chunker/common.go`) mirroring the Python helper: - parses numbers and numeric strings (mirrors Python `float()`; bad / `NaN` / `Inf` → `0`), - `0 < v < 1 → v *= 100`, - `int()` truncation, - clamps to `[0,90]`. Wired into `tokenChunkerParam.Update` (`token.go`); the merge math (`token.go:705`, `(100-x)/100`) already matched Python. `TokenChunkerParam.Validate` (`schema/chunker.go`) now only guards direct struct construction. Tests: `TestNormalizeOverlappedPercent`, extended `TestTokenChunker_NewAcceptsPythonOverlappedRange` (adds fraction/clamp inputs), new `TestTokenChunker_NormalizesOverlappedPercent`. The existing reject-test cases for `<0` / `>90` were removed because they are now normalized/clamped (Python parity). ### Diff 1.7 — missing `BULLET_PATTERN` title-level fallback `resolveTitleLevels` (`title.go`) now applies a 4th-level fallback: when outline + regex + layout all yield body level, `bulletsCategory` selects the best-matching bullet-pattern group (Chinese legal / numbering / Chinese numbering / English legal — mirroring `rag/nlp/__init__.py:258-320`) and assigns structural levels. Guarded by `allBodyLevel` so it never overrides an existing outline/regex level. Tests: `TestResolveTitleLevels_BulletFallback` (4 subtests). ### Incidental test adjustments included in the commit - `token_batch1_test.go`: overlap input changed `0.3` → `30.0` to reflect the post-normalization 30% semantics. - `real_consumer_test.go`: updated `LoadFromIngestionTask(task)` → `LoadFromIngestionTask(ctx, task)` for the new context-first signature. ## Verification `bash build.sh --test ./internal/ingestion/component/...` — chunker + schema suites pass, no regression (CGO build). ## Migration doc reference `docs/migration_python_go_diff.md` §Chunker 1.7 and 2.6 are marked **Fixed** for these changes.
2026-07-27 14:42:44 +08:00
// OverlappedPercent is the overlap percentage in [0, 90]. Mirrors
// Python common/float_utils.py:50-58 — an integer-like float in the
// same range so DSL templates written for the Python pipeline work
fix: Go ingestion migration batch 5 (Parser 1.1/1.7/2.11, Chunker 1.7/1.8/2.6/2.7, Tokenizer 6x fixes) (#17419) ## Summary Continuation of the Python→Go ingestion pipeline migration (File → Parser → Chunker → Extractor → Tokenizer). Fixes cover Parser, Chunker, and Tokenizer gaps identified. Fix page number (0-indexed and 1-index mixed before fix; use 1-indexed after fix) and chunk order issues. ### Parser - **Slides TCADP (1.7):** `pptx_tcadp.go` + TCADP branch in `pptx_parser.go`/`ppt_parser.go` — PowerPoint files now support `parse_method="tcadp"` via the TCADP cloud service, matching the spreadsheet-family TCADP pattern. PPT containers pass `"PPT"` as fileType (not hardcoded `"PPTX"`). - **Audio default output_format (2.11):** `defaultSetups()` audio default changed from `"text"` to `"json"`, aligning with Python `parser.py:232` and `AllowedOutputFormat["audio"]={"json"}`. - **PDF VLM enhancement (1.1):** `maybeDispatchPDFVisionEnhancement` in `pdf_vision_dispatch.go` enriches image/table items with IMAGE2TEXT model descriptions after PDF parsing, mirroring Python `enhance_media_sections_with_vision`. Semaphore fix: acquire before goroutine start to prevent unbounded goroutine creation. - **json family (2.3):** reclassified as Keep Go — `json_parser.go` is a functional enhancement, not a parity gap. - **page number:** changed from "mixed use of 1-indexed & 0-indexed" to "1-indexed" ### Chunker - **BULLET_PATTERN fallback (1.7):** 4th-level fallback in `resolveTitleLevels` (`title.go`) detects bullet/numbered-list patterns (Chinese legal, numbering, English) when outline + regex levels produce only bodyLevel. Guarded by `allBodyLevel` to never override existing structure. - **Tag/One chunker fields (1.8):** `tag.go` sets `TopInt` from source row index; `one.go` preserves `Positions`/`PDFPositions` from source items. TSV multi-line RowNum fix: tracks `contentStart` for correct row attribution. - **Overlapped_percent normalization (2.6):** `NormalizeOverlappedPercent` in `schema/chunker.go` mirrors Python `common/float_utils.py:50-58` — accepts `[0,1)` fraction or `[0,90]` percent, normalizes to canonical `[0,90]`. - **Paragraph splitting (2.7):** aligned to Python flow `naive_merge` — `CRLF` normalization, `splitKeepingDelimiter` preserves sentence delimiters, single-section merge with token-budget-governed chunking. - **chunk order:** sort by reading order ### Tokenizer - **Phantom chunk filtering (Omission 2):** `isPhantomChunk` + filter loop in `chunksFromTokenizerUpstream` skips zero-value ChunkDocs. - **Batch size env var (Omission 3):** `embeddingBatchSize()` reads `TOKENIZER_EMBEDDING_BATCH_SIZE`, defaults to 16. - **Summary empty check (Diff 5):** `TrimSpace(s) != ""` → `s != ""`, matching Python truthy check. - **chunk_order_int all paths (Diff 8):** set unconditionally before full_text/embedding branching. - **Timeout default (Diff 10):** `600s` → `60s`, matching Python `@timeout(60)`. - **Small maxTokens truncation (Diff 14):** `truncateForEmbedding` returns `""` when `maxTokens <= 10`, matching Python. ### Code review fixes - Semaphore acquire moved before goroutine in `pdf_vision_dispatch.go` (concurrency control) - Context propagation in `pptx_tcadp.go` (cancellation support) - Test resolver leak fix in `media_dispatch_test.go` (defer restore) - Migration history comments removed per AGENTS.md ## Test plan ``` bash build.sh --test ./internal/parser/parser/... ./internal/ingestion/component/... ``` ## Notes - Migration diff tracking: `docs/migration_python_go_diff.md` - Remaining gaps: Extractor component only (21 items)
2026-07-28 11:12:52 +08:00
// out of the box A [0,1) fraction input is also
Chunker: port Python overlapped_percent normalization and BULLET_PATTERN title fallback (#17396) ## Summary Closes two Chunker migration gaps documented in `docs/migration_python_go_diff.md` (diffs **2.6** and **1.7**), improving parity with the Python ingestion pipeline. This is the code portion of commit `261e1fd0b` on branch `fix/batch-3-4`; the migration document itself is tracked separately (untracked in this commit). ### Diff 2.6 — `overlapped_percent` missing Python normalization Python's `normalize_overlapped_percent` (`common/float_utils.py:50-58`) accepts a `[0,1)` fraction (the flow-canvas UI validates `[0,1)`), multiplies it by 100, `int()`-truncates, and clamps to `[0,90]`. Go previously only accepted a raw `[0,90]` percentage and **rejected** out-of-range input, so a Python config passing `0.1` (meaning 10%) silently produced ~0% overlap. Added `normalizeOverlappedPercent` (`internal/ingestion/component/chunker/common.go`) mirroring the Python helper: - parses numbers and numeric strings (mirrors Python `float()`; bad / `NaN` / `Inf` → `0`), - `0 < v < 1 → v *= 100`, - `int()` truncation, - clamps to `[0,90]`. Wired into `tokenChunkerParam.Update` (`token.go`); the merge math (`token.go:705`, `(100-x)/100`) already matched Python. `TokenChunkerParam.Validate` (`schema/chunker.go`) now only guards direct struct construction. Tests: `TestNormalizeOverlappedPercent`, extended `TestTokenChunker_NewAcceptsPythonOverlappedRange` (adds fraction/clamp inputs), new `TestTokenChunker_NormalizesOverlappedPercent`. The existing reject-test cases for `<0` / `>90` were removed because they are now normalized/clamped (Python parity). ### Diff 1.7 — missing `BULLET_PATTERN` title-level fallback `resolveTitleLevels` (`title.go`) now applies a 4th-level fallback: when outline + regex + layout all yield body level, `bulletsCategory` selects the best-matching bullet-pattern group (Chinese legal / numbering / Chinese numbering / English legal — mirroring `rag/nlp/__init__.py:258-320`) and assigns structural levels. Guarded by `allBodyLevel` so it never overrides an existing outline/regex level. Tests: `TestResolveTitleLevels_BulletFallback` (4 subtests). ### Incidental test adjustments included in the commit - `token_batch1_test.go`: overlap input changed `0.3` → `30.0` to reflect the post-normalization 30% semantics. - `real_consumer_test.go`: updated `LoadFromIngestionTask(task)` → `LoadFromIngestionTask(ctx, task)` for the new context-first signature. ## Verification `bash build.sh --test ./internal/ingestion/component/...` — chunker + schema suites pass, no regression (CGO build). ## Migration doc reference `docs/migration_python_go_diff.md` §Chunker 1.7 and 2.6 are marked **Fixed** for these changes.
2026-07-27 14:42:44 +08:00
// accepted and normalized to this scale by tokenChunkerParam.Update
// (via normalizeOverlappedPercent).
OverlappedPercent float64 `json:"overlapped_percent"`
// ChildrenDelimiters is the secondary split applied to text chunks.
ChildrenDelimiters []string `json:"children_delimiters"`
// TableContextSize is the number of surrounding tokens to attach
// to table chunks. 0 disables.
TableContextSize int `json:"table_context_size"`
// ImageContextSize is the number of surrounding tokens to attach
// to image chunks. 0 disables.
ImageContextSize int `json:"image_context_size"`
// UnderCap selects the merge strategy when greedily accumulating
// adjacent units (token_chunker UNDER_CAP vs OVER_CAP).
// - false (default): OVER_CAP — mirrors Python's canonical default.
// A chunk may exceed the token target by at most one incoming unit
// (a boundary overflow then closes the chunk).
// - true: UNDER_CAP — strictly never exceed the target; when the
// projected join would overflow, start a fresh chunk instead.
refactor(chunker): replace allowBoundaryOverflow bool with MergeStrategy enum (#17851) ## Summary Follow-up to #17835 (merged). The OVER_CAP / UNDER_CAP merge strategy was threaded through `mergeDecision` and `mergeByTokenSizeFromJSON` as an inlined `allowBoundaryOverflow bool` derived from `!c.param.UnderCap` at three call sites. This replaces that with a named `schema.MergeStrategy` enum. ## Why - The `!c.param.UnderCap` inversion was hand-written in three places, so a future strategy addition could silently drift between the JSON path (`invokeTextPayload` / `invokeJSONPayload`) and the text path (`mergeByTokenSize`) — no compile error, and the existing tests don't cover all three sites with both strategies. - The strategy concept was never named; `allowBoundaryOverflow` (true = OVER_CAP) is a double-negation of `UnderCap` and reads opaquely at the 5th positional argument. ## What changed - Add `schema.MergeStrategy` (`MergeOverCap` / `MergeUnderCap`) mirroring Python's `rag/nlp/__init__.py` `MergeStrategy`, so Go and Python stay on the same vocabulary. - Expose `TokenChunkerParam.MergeStrategy()` derived from the wire-facing `UnderCap bool` (existing `"under_cap"` configs keep working — no schema break). - `mergeDecision` and `mergeByTokenSizeFromJSON` now take `schema.MergeStrategy` instead of `allowBoundaryOverflow bool`; the three call sites pass `c.param.MergeStrategy()` (no `!`). - Tests updated to pass the enum; added a guard test for the `UnderCap` -> `MergeStrategy` mapping and an end-to-end test for UNDER_CAP on the JSON path. No behavior change: default remains OVER_CAP, `under_cap=true` still selects UNDER_CAP. ## Test plan `bash build.sh --test ./internal/ingestion/component/chunker/... ./internal/ingestion/component/schema/...` — all green, including `TestMergeByTokenSizeFromJSON_UnderCapNoOverflow`, `TestMergeByTokenSize_UnderCapNoOverflow`, `TestInvokeJSONPayload_UnderCapEndToEnd`, and `TestTokenChunkerParamMergeStrategy`. ## Related issues - Relates to #17835 — wired UNDER_CAP as a tested merge-strategy seam (merged) - Relates to #17799 — contract doc for token-chunker cap/delimiter alignment - Relates to #17808 — related chunker alignment work --------- Co-authored-by: CodeBuddy <noreply@cnb.cool>
2026-08-05 14:59:53 +08:00
// This is the wire-facing bool; the active strategy is exposed as
// MergeStrategy so callers never have to invert it.
UnderCap bool `json:"under_cap"`
}
refactor(chunker): replace allowBoundaryOverflow bool with MergeStrategy enum (#17851) ## Summary Follow-up to #17835 (merged). The OVER_CAP / UNDER_CAP merge strategy was threaded through `mergeDecision` and `mergeByTokenSizeFromJSON` as an inlined `allowBoundaryOverflow bool` derived from `!c.param.UnderCap` at three call sites. This replaces that with a named `schema.MergeStrategy` enum. ## Why - The `!c.param.UnderCap` inversion was hand-written in three places, so a future strategy addition could silently drift between the JSON path (`invokeTextPayload` / `invokeJSONPayload`) and the text path (`mergeByTokenSize`) — no compile error, and the existing tests don't cover all three sites with both strategies. - The strategy concept was never named; `allowBoundaryOverflow` (true = OVER_CAP) is a double-negation of `UnderCap` and reads opaquely at the 5th positional argument. ## What changed - Add `schema.MergeStrategy` (`MergeOverCap` / `MergeUnderCap`) mirroring Python's `rag/nlp/__init__.py` `MergeStrategy`, so Go and Python stay on the same vocabulary. - Expose `TokenChunkerParam.MergeStrategy()` derived from the wire-facing `UnderCap bool` (existing `"under_cap"` configs keep working — no schema break). - `mergeDecision` and `mergeByTokenSizeFromJSON` now take `schema.MergeStrategy` instead of `allowBoundaryOverflow bool`; the three call sites pass `c.param.MergeStrategy()` (no `!`). - Tests updated to pass the enum; added a guard test for the `UnderCap` -> `MergeStrategy` mapping and an end-to-end test for UNDER_CAP on the JSON path. No behavior change: default remains OVER_CAP, `under_cap=true` still selects UNDER_CAP. ## Test plan `bash build.sh --test ./internal/ingestion/component/chunker/... ./internal/ingestion/component/schema/...` — all green, including `TestMergeByTokenSizeFromJSON_UnderCapNoOverflow`, `TestMergeByTokenSize_UnderCapNoOverflow`, `TestInvokeJSONPayload_UnderCapEndToEnd`, and `TestTokenChunkerParamMergeStrategy`. ## Related issues - Relates to #17835 — wired UNDER_CAP as a tested merge-strategy seam (merged) - Relates to #17799 — contract doc for token-chunker cap/delimiter alignment - Relates to #17808 — related chunker alignment work --------- Co-authored-by: CodeBuddy <noreply@cnb.cool>
2026-08-05 14:59:53 +08:00
// MergeStrategy selects how the TokenChunker greedily accumulates adjacent
// units into a chunk. It mirrors Python's rag/nlp/__init__.py MergeStrategy so
// the Go and Python implementations stay on the same vocabulary (OVER_CAP /
// UNDER_CAP) instead of a bare inlined bool.
type MergeStrategy int
const (
// MergeOverCap is the canonical default (Python OVER_CAP): a chunk may
// exceed the token target by at most one incoming unit (a boundary
// overflow then closes the chunk).
MergeOverCap MergeStrategy = iota
// MergeUnderCap never exceeds the target; when the projected join would
// overflow, a fresh chunk is started instead (Python UNDER_CAP).
MergeUnderCap
)
// MergeStrategy reports the active merge strategy for this param. It is derived
// from UnderCap so existing configs that set "under_cap" keep working without a
// schema break, and callers read the strategy directly instead of inverting a
// bool at every call site.
func (p TokenChunkerParam) MergeStrategy() MergeStrategy {
if p.UnderCap {
return MergeUnderCap
}
return MergeOverCap
}
// Defaults returns the Python default TokenChunkerParam.
func (TokenChunkerParam) Defaults() TokenChunkerParam {
return TokenChunkerParam{
DelimiterMode: "delimiter",
ChunkTokenSize: 512,
Delimiters: []string{"\n"},
OverlappedPercent: 0,
ChildrenDelimiters: []string{},
TableContextSize: 0,
ImageContextSize: 0,
}
}
Chunker: port Python overlapped_percent normalization and BULLET_PATTERN title fallback (#17396) ## Summary Closes two Chunker migration gaps documented in `docs/migration_python_go_diff.md` (diffs **2.6** and **1.7**), improving parity with the Python ingestion pipeline. This is the code portion of commit `261e1fd0b` on branch `fix/batch-3-4`; the migration document itself is tracked separately (untracked in this commit). ### Diff 2.6 — `overlapped_percent` missing Python normalization Python's `normalize_overlapped_percent` (`common/float_utils.py:50-58`) accepts a `[0,1)` fraction (the flow-canvas UI validates `[0,1)`), multiplies it by 100, `int()`-truncates, and clamps to `[0,90]`. Go previously only accepted a raw `[0,90]` percentage and **rejected** out-of-range input, so a Python config passing `0.1` (meaning 10%) silently produced ~0% overlap. Added `normalizeOverlappedPercent` (`internal/ingestion/component/chunker/common.go`) mirroring the Python helper: - parses numbers and numeric strings (mirrors Python `float()`; bad / `NaN` / `Inf` → `0`), - `0 < v < 1 → v *= 100`, - `int()` truncation, - clamps to `[0,90]`. Wired into `tokenChunkerParam.Update` (`token.go`); the merge math (`token.go:705`, `(100-x)/100`) already matched Python. `TokenChunkerParam.Validate` (`schema/chunker.go`) now only guards direct struct construction. Tests: `TestNormalizeOverlappedPercent`, extended `TestTokenChunker_NewAcceptsPythonOverlappedRange` (adds fraction/clamp inputs), new `TestTokenChunker_NormalizesOverlappedPercent`. The existing reject-test cases for `<0` / `>90` were removed because they are now normalized/clamped (Python parity). ### Diff 1.7 — missing `BULLET_PATTERN` title-level fallback `resolveTitleLevels` (`title.go`) now applies a 4th-level fallback: when outline + regex + layout all yield body level, `bulletsCategory` selects the best-matching bullet-pattern group (Chinese legal / numbering / Chinese numbering / English legal — mirroring `rag/nlp/__init__.py:258-320`) and assigns structural levels. Guarded by `allBodyLevel` so it never overrides an existing outline/regex level. Tests: `TestResolveTitleLevels_BulletFallback` (4 subtests). ### Incidental test adjustments included in the commit - `token_batch1_test.go`: overlap input changed `0.3` → `30.0` to reflect the post-normalization 30% semantics. - `real_consumer_test.go`: updated `LoadFromIngestionTask(task)` → `LoadFromIngestionTask(ctx, task)` for the new context-first signature. ## Verification `bash build.sh --test ./internal/ingestion/component/...` — chunker + schema suites pass, no regression (CGO build). ## Migration doc reference `docs/migration_python_go_diff.md` §Chunker 1.7 and 2.6 are marked **Fixed** for these changes.
2026-07-27 14:42:44 +08:00
// NumericFromAny normalises JSON-decoded ints to float64 so the
// schema-defaults-vs-Param-Update convention doesn't depend on the
// encoding source (yaml/toml/json all behave the same). It is shared by
// the chunker config decoders and NormalizeOverlappedPercent.
func NumericFromAny(v any) (float64, bool) {
switch x := v.(type) {
case float64:
return x, true
case float32:
return float64(x), true
case int:
return float64(x), true
case int32:
return float64(x), true
case int64:
return float64(x), true
case uint:
return float64(x), true
case uint32:
return float64(x), true
case uint64:
return float64(x), true
}
return 0, false
}
// NormalizeOverlappedPercent mirrors Python common/float_utils.py:50-58
// (normalize_overlapped_percent). Python's user-facing input is a [0,1)
// fraction (token_chunker.py validates "[0, 1)"); this converts it to the
// [0,90] integer-percentage scale that the merge math (token.go:705,
// `(100-x)/100`) expects, matching Python's canonical norm. Diff Chunker-2.6.
//
// Behaviour matches Python exactly:
// - parse like float(): numbers, or numeric strings (Python also accepts
// strings via float()); on any failure / NaN / Inf, return 0
// (Python: except (TypeError, ValueError, OverflowError) -> 0).
// - 0 < v < 1 -> v *= 100 (fraction -> percentage)
// - v = int(v) (truncation toward zero, matches Python for
// non-negatives)
// - clamp to [0, 90] (Python: max(0, min(v, 90)))
//
// Exported so TokenChunkerParam.Validate can normalize directly-constructed
// structs (not just the config-path Update), keeping both paths identical.
func NormalizeOverlappedPercent(v any) float64 {
value, ok := NumericFromAny(v)
if !ok {
if s, isStr := v.(string); isStr {
if f, err := strconv.ParseFloat(strings.TrimSpace(s), 64); err == nil {
value, ok = f, true
}
}
}
if !ok || math.IsNaN(value) || math.IsInf(value, 0) {
return 0
}
return clampOverlappedPercent(value)
}
// clampOverlappedPercent applies the [0,1) fraction -> [0,90] percent
// conversion, clamps to [0,90], then truncates to an integer (Python
// faithfulness). Clamping MUST happen before the int conversion: Go's
// float->int is implementation-defined when the value is out of int's range
// (e.g. 1e300), whereas Python's int() is arbitrary-precision, so clamping
// first keeps us aligned with Python's max(0, min(int(v), 90)). Used by the
// lenient config path (NormalizeOverlappedPercent).
func clampOverlappedPercent(value float64) float64 {
if 0 < value && value < 1 {
value *= 100
}
if value < 0 {
value = 0
}
if value > 90 {
value = 90
}
// Round rather than truncate so fraction inputs like 0.29 normalize
// to 29 (user expectation) rather than 28. Mirrors Python's
// common/float_utils.py:50-58 fix for #17418. We still clamp
// before rounding so out-of-int-range values (e.g. 1e300) land on
// 90, not an implementation-defined int.
value = math.Round(value)
Chunker: port Python overlapped_percent normalization and BULLET_PATTERN title fallback (#17396) ## Summary Closes two Chunker migration gaps documented in `docs/migration_python_go_diff.md` (diffs **2.6** and **1.7**), improving parity with the Python ingestion pipeline. This is the code portion of commit `261e1fd0b` on branch `fix/batch-3-4`; the migration document itself is tracked separately (untracked in this commit). ### Diff 2.6 — `overlapped_percent` missing Python normalization Python's `normalize_overlapped_percent` (`common/float_utils.py:50-58`) accepts a `[0,1)` fraction (the flow-canvas UI validates `[0,1)`), multiplies it by 100, `int()`-truncates, and clamps to `[0,90]`. Go previously only accepted a raw `[0,90]` percentage and **rejected** out-of-range input, so a Python config passing `0.1` (meaning 10%) silently produced ~0% overlap. Added `normalizeOverlappedPercent` (`internal/ingestion/component/chunker/common.go`) mirroring the Python helper: - parses numbers and numeric strings (mirrors Python `float()`; bad / `NaN` / `Inf` → `0`), - `0 < v < 1 → v *= 100`, - `int()` truncation, - clamps to `[0,90]`. Wired into `tokenChunkerParam.Update` (`token.go`); the merge math (`token.go:705`, `(100-x)/100`) already matched Python. `TokenChunkerParam.Validate` (`schema/chunker.go`) now only guards direct struct construction. Tests: `TestNormalizeOverlappedPercent`, extended `TestTokenChunker_NewAcceptsPythonOverlappedRange` (adds fraction/clamp inputs), new `TestTokenChunker_NormalizesOverlappedPercent`. The existing reject-test cases for `<0` / `>90` were removed because they are now normalized/clamped (Python parity). ### Diff 1.7 — missing `BULLET_PATTERN` title-level fallback `resolveTitleLevels` (`title.go`) now applies a 4th-level fallback: when outline + regex + layout all yield body level, `bulletsCategory` selects the best-matching bullet-pattern group (Chinese legal / numbering / Chinese numbering / English legal — mirroring `rag/nlp/__init__.py:258-320`) and assigns structural levels. Guarded by `allBodyLevel` so it never overrides an existing outline/regex level. Tests: `TestResolveTitleLevels_BulletFallback` (4 subtests). ### Incidental test adjustments included in the commit - `token_batch1_test.go`: overlap input changed `0.3` → `30.0` to reflect the post-normalization 30% semantics. - `real_consumer_test.go`: updated `LoadFromIngestionTask(task)` → `LoadFromIngestionTask(ctx, task)` for the new context-first signature. ## Verification `bash build.sh --test ./internal/ingestion/component/...` — chunker + schema suites pass, no regression (CGO build). ## Migration doc reference `docs/migration_python_go_diff.md` §Chunker 1.7 and 2.6 are marked **Fixed** for these changes.
2026-07-27 14:42:44 +08:00
return value
}
// Validate enforces the same enum/range checks the runtime component expects
// at construction time, keeping the schema and component decoder aligned.
Chunker: port Python overlapped_percent normalization and BULLET_PATTERN title fallback (#17396) ## Summary Closes two Chunker migration gaps documented in `docs/migration_python_go_diff.md` (diffs **2.6** and **1.7**), improving parity with the Python ingestion pipeline. This is the code portion of commit `261e1fd0b` on branch `fix/batch-3-4`; the migration document itself is tracked separately (untracked in this commit). ### Diff 2.6 — `overlapped_percent` missing Python normalization Python's `normalize_overlapped_percent` (`common/float_utils.py:50-58`) accepts a `[0,1)` fraction (the flow-canvas UI validates `[0,1)`), multiplies it by 100, `int()`-truncates, and clamps to `[0,90]`. Go previously only accepted a raw `[0,90]` percentage and **rejected** out-of-range input, so a Python config passing `0.1` (meaning 10%) silently produced ~0% overlap. Added `normalizeOverlappedPercent` (`internal/ingestion/component/chunker/common.go`) mirroring the Python helper: - parses numbers and numeric strings (mirrors Python `float()`; bad / `NaN` / `Inf` → `0`), - `0 < v < 1 → v *= 100`, - `int()` truncation, - clamps to `[0,90]`. Wired into `tokenChunkerParam.Update` (`token.go`); the merge math (`token.go:705`, `(100-x)/100`) already matched Python. `TokenChunkerParam.Validate` (`schema/chunker.go`) now only guards direct struct construction. Tests: `TestNormalizeOverlappedPercent`, extended `TestTokenChunker_NewAcceptsPythonOverlappedRange` (adds fraction/clamp inputs), new `TestTokenChunker_NormalizesOverlappedPercent`. The existing reject-test cases for `<0` / `>90` were removed because they are now normalized/clamped (Python parity). ### Diff 1.7 — missing `BULLET_PATTERN` title-level fallback `resolveTitleLevels` (`title.go`) now applies a 4th-level fallback: when outline + regex + layout all yield body level, `bulletsCategory` selects the best-matching bullet-pattern group (Chinese legal / numbering / Chinese numbering / English legal — mirroring `rag/nlp/__init__.py:258-320`) and assigns structural levels. Guarded by `allBodyLevel` so it never overrides an existing outline/regex level. Tests: `TestResolveTitleLevels_BulletFallback` (4 subtests). ### Incidental test adjustments included in the commit - `token_batch1_test.go`: overlap input changed `0.3` → `30.0` to reflect the post-normalization 30% semantics. - `real_consumer_test.go`: updated `LoadFromIngestionTask(task)` → `LoadFromIngestionTask(ctx, task)` for the new context-first signature. ## Verification `bash build.sh --test ./internal/ingestion/component/...` — chunker + schema suites pass, no regression (CGO build). ## Migration doc reference `docs/migration_python_go_diff.md` §Chunker 1.7 and 2.6 are marked **Fixed** for these changes.
2026-07-27 14:42:44 +08:00
func (p *TokenChunkerParam) Validate() error {
// Strict overlap normalization for directly-constructed structs: scale a
// [0,1) fraction to a [0,90] percent (matching the config path's
// semantics) and truncate toward zero, then REJECT anything still outside
// [0,90]. The config path (Update) clamps instead, so a fraction like 0.3
// means 30% here too, while an out-of-range value like 95 or -5 is caught
// rather than silently clamped — eliminating the latent footgun where a
// fraction passed to a struct bypassed normalization.
if v := p.OverlappedPercent; 0 < v && v < 1 {
// Round rather than truncate so fraction inputs like 0.29 normalize
// to 29 (user expectation) rather than 28. Mirrors Python's
// common/float_utils.py:50-58 fix for #17418.
p.OverlappedPercent = math.Round(v * 100)
Chunker: port Python overlapped_percent normalization and BULLET_PATTERN title fallback (#17396) ## Summary Closes two Chunker migration gaps documented in `docs/migration_python_go_diff.md` (diffs **2.6** and **1.7**), improving parity with the Python ingestion pipeline. This is the code portion of commit `261e1fd0b` on branch `fix/batch-3-4`; the migration document itself is tracked separately (untracked in this commit). ### Diff 2.6 — `overlapped_percent` missing Python normalization Python's `normalize_overlapped_percent` (`common/float_utils.py:50-58`) accepts a `[0,1)` fraction (the flow-canvas UI validates `[0,1)`), multiplies it by 100, `int()`-truncates, and clamps to `[0,90]`. Go previously only accepted a raw `[0,90]` percentage and **rejected** out-of-range input, so a Python config passing `0.1` (meaning 10%) silently produced ~0% overlap. Added `normalizeOverlappedPercent` (`internal/ingestion/component/chunker/common.go`) mirroring the Python helper: - parses numbers and numeric strings (mirrors Python `float()`; bad / `NaN` / `Inf` → `0`), - `0 < v < 1 → v *= 100`, - `int()` truncation, - clamps to `[0,90]`. Wired into `tokenChunkerParam.Update` (`token.go`); the merge math (`token.go:705`, `(100-x)/100`) already matched Python. `TokenChunkerParam.Validate` (`schema/chunker.go`) now only guards direct struct construction. Tests: `TestNormalizeOverlappedPercent`, extended `TestTokenChunker_NewAcceptsPythonOverlappedRange` (adds fraction/clamp inputs), new `TestTokenChunker_NormalizesOverlappedPercent`. The existing reject-test cases for `<0` / `>90` were removed because they are now normalized/clamped (Python parity). ### Diff 1.7 — missing `BULLET_PATTERN` title-level fallback `resolveTitleLevels` (`title.go`) now applies a 4th-level fallback: when outline + regex + layout all yield body level, `bulletsCategory` selects the best-matching bullet-pattern group (Chinese legal / numbering / Chinese numbering / English legal — mirroring `rag/nlp/__init__.py:258-320`) and assigns structural levels. Guarded by `allBodyLevel` so it never overrides an existing outline/regex level. Tests: `TestResolveTitleLevels_BulletFallback` (4 subtests). ### Incidental test adjustments included in the commit - `token_batch1_test.go`: overlap input changed `0.3` → `30.0` to reflect the post-normalization 30% semantics. - `real_consumer_test.go`: updated `LoadFromIngestionTask(task)` → `LoadFromIngestionTask(ctx, task)` for the new context-first signature. ## Verification `bash build.sh --test ./internal/ingestion/component/...` — chunker + schema suites pass, no regression (CGO build). ## Migration doc reference `docs/migration_python_go_diff.md` §Chunker 1.7 and 2.6 are marked **Fixed** for these changes.
2026-07-27 14:42:44 +08:00
}
// Backward-compat: Python removed the standalone "token_size" mode and
// treats it as identical to "delimiter" at runtime, normalizing it in
// check() (rag/flow/chunker/token_chunker.py). A dataset configured under
// the old scheme may still carry delimiter_mode="token_size"; normalize it
// here instead of rejecting it so those pipelines keep working (mirrors
// the Python behaviour exactly).
if p.DelimiterMode == "token_size" {
p.DelimiterMode = "delimiter"
}
switch p.DelimiterMode {
case "delimiter":
default:
return errInvalidValue{Field: "delimiter_mode", Value: p.DelimiterMode}
}
if p.ChunkTokenSize <= 0 {
return errInvalidValue{Field: "chunk_token_size", Value: fmt.Sprintf("%d", p.ChunkTokenSize)}
}
Chunker: port Python overlapped_percent normalization and BULLET_PATTERN title fallback (#17396) ## Summary Closes two Chunker migration gaps documented in `docs/migration_python_go_diff.md` (diffs **2.6** and **1.7**), improving parity with the Python ingestion pipeline. This is the code portion of commit `261e1fd0b` on branch `fix/batch-3-4`; the migration document itself is tracked separately (untracked in this commit). ### Diff 2.6 — `overlapped_percent` missing Python normalization Python's `normalize_overlapped_percent` (`common/float_utils.py:50-58`) accepts a `[0,1)` fraction (the flow-canvas UI validates `[0,1)`), multiplies it by 100, `int()`-truncates, and clamps to `[0,90]`. Go previously only accepted a raw `[0,90]` percentage and **rejected** out-of-range input, so a Python config passing `0.1` (meaning 10%) silently produced ~0% overlap. Added `normalizeOverlappedPercent` (`internal/ingestion/component/chunker/common.go`) mirroring the Python helper: - parses numbers and numeric strings (mirrors Python `float()`; bad / `NaN` / `Inf` → `0`), - `0 < v < 1 → v *= 100`, - `int()` truncation, - clamps to `[0,90]`. Wired into `tokenChunkerParam.Update` (`token.go`); the merge math (`token.go:705`, `(100-x)/100`) already matched Python. `TokenChunkerParam.Validate` (`schema/chunker.go`) now only guards direct struct construction. Tests: `TestNormalizeOverlappedPercent`, extended `TestTokenChunker_NewAcceptsPythonOverlappedRange` (adds fraction/clamp inputs), new `TestTokenChunker_NormalizesOverlappedPercent`. The existing reject-test cases for `<0` / `>90` were removed because they are now normalized/clamped (Python parity). ### Diff 1.7 — missing `BULLET_PATTERN` title-level fallback `resolveTitleLevels` (`title.go`) now applies a 4th-level fallback: when outline + regex + layout all yield body level, `bulletsCategory` selects the best-matching bullet-pattern group (Chinese legal / numbering / Chinese numbering / English legal — mirroring `rag/nlp/__init__.py:258-320`) and assigns structural levels. Guarded by `allBodyLevel` so it never overrides an existing outline/regex level. Tests: `TestResolveTitleLevels_BulletFallback` (4 subtests). ### Incidental test adjustments included in the commit - `token_batch1_test.go`: overlap input changed `0.3` → `30.0` to reflect the post-normalization 30% semantics. - `real_consumer_test.go`: updated `LoadFromIngestionTask(task)` → `LoadFromIngestionTask(ctx, task)` for the new context-first signature. ## Verification `bash build.sh --test ./internal/ingestion/component/...` — chunker + schema suites pass, no regression (CGO build). ## Migration doc reference `docs/migration_python_go_diff.md` §Chunker 1.7 and 2.6 are marked **Fixed** for these changes.
2026-07-27 14:42:44 +08:00
if p.OverlappedPercent < 0 || p.OverlappedPercent > 90 {
return errInvalidValue{Field: "overlapped_percent", Value: fmt.Sprintf("%v", p.OverlappedPercent)}
}
if p.TableContextSize < 0 {
return errInvalidValue{Field: "table_context_size", Value: fmt.Sprintf("%d", p.TableContextSize)}
}
if p.ImageContextSize < 0 {
return errInvalidValue{Field: "image_context_size", Value: fmt.Sprintf("%d", p.ImageContextSize)}
}
return nil
}
// ---------------------------------------------------------------------------
// TitleChunkerParam
// ---------------------------------------------------------------------------
//
// Mirrors rag/flow/chunker/title_chunker/common.py:TitleChunkerParam.
// The class also reads `self.method` (set externally — see Python
// title_chunker.py:31-37 routing on `self._param.method`). The Go port
// captures it explicitly here. The component's `check()` enforces enum
// values.
type TitleChunkerParam struct {
// Method routes to the right title-chunker strategy.
// Allowed values: "hierarchy", "group".
Method string `json:"method,omitempty"`
// Levels is a list of regex-list groups, one per hierarchy level.
// Each group is a list of regex strings; the component picks the
// best-matching group at runtime.
Levels [][]string `json:"levels"`
// Hierarchy is the heading depth used by HierarchyTitleChunker.
// Stored as a pointer to distinguish nil (unset) from 0.
Hierarchy *int `json:"hierarchy,omitempty"`
// IncludeHeadingContent, when true, makes the heading text part
// of each emitted chunk.
IncludeHeadingContent bool `json:"include_heading_content"`
// RootChunkAsHeading, when true, prepends the root chunk's text
// to every emitted chunk (and drops the root chunk itself).
RootChunkAsHeading bool `json:"root_chunk_as_heading"`
}
// Defaults returns the Python default TitleChunkerParam. `Method` is
// not initialized in the Python `__init__` (it is set externally); the
// default is left as the empty string and the component must supply it.
func (TitleChunkerParam) Defaults() TitleChunkerParam {
return TitleChunkerParam{
Levels: [][]string{},
Hierarchy: nil,
IncludeHeadingContent: false,
RootChunkAsHeading: false,
}
}
// Validate enforces the Python `check()` invariants that are also
// expressible in pure-data terms: when Method == "hierarchy" the
// hierarchy depth and level config must be present.
func (p *TitleChunkerParam) Validate() error {
switch p.Method {
case "hierarchy", "group":
case "":
return nil
default:
return errInvalidValue{Field: "method", Value: p.Method}
}
switch p.Method {
case "hierarchy", "group":
if len(p.Levels) == 0 {
return errRequiredField{Field: "levels"}
}
}
if p.Method == "hierarchy" && (p.Hierarchy == nil || *p.Hierarchy <= 0) {
return errRequiredField{Field: "hierarchy"}
}
return nil
}
// ---------------------------------------------------------------------------
// GroupTitleChunkerParam / HierarchyTitleChunkerParam
// ---------------------------------------------------------------------------
//
// In the Python codebase, both variants share the SAME
// `TitleChunkerParam` class — there is no per-variant param. The
// dispatch happens in title_chunker.py:31-37 by reading
// `self._param.method`.
//
// In Go we model the shared class as `TitleChunkerParam` and expose
// type aliases so component files can name the param type they
// actually use. This keeps the wire schema faithful to Python while
// giving each variant a self-documenting entry point in the registry.
type GroupTitleChunkerParam = TitleChunkerParam
type HierarchyTitleChunkerParam = TitleChunkerParam