Files
ragflow/internal/ingestion/component/extractor.go

1240 lines
43 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 component — Extractor component (Phase 2.5 of
// port-rag-flow-pipeline-to-go.md §4 row 2.5).
//
// SCOPE (honest):
//
// - PROVIDER-AGNOSTIC (§8 Q1): the Extractor does NOT depend on any
// specific LLM provider. It dispatches every chat call through
// internal/entity/models — the same factory routes 48 of the 56
// Python ChatModel providers registered there (factory.go switch,
// lines 36-156). The 8 providers NOT yet in the Go switch (LeptonAI,
// Gemini LiteLLM path, PerfXCloud, 01.AI / Lingyi, DeerAPI,
// Astraflow-CN, RAGcon, New API) ARE unreachable from this
// component — an llm_id resolving to one of those falls through to
// NewDummyModel and the chat call returns a deterministic "dummy"
// response. We DO NOT panic: errors are surfaced as a clean
// "no driver for %q" wrap that callers can log and route.
//
// - LLM CALL SHAPE: one chat call per chunk (no batching). LLM
// calls are inherently serial; sequential per-chunk processing
// keeps test ordering deterministic under -race.
//
// - TIMEOUT / ELAPSED: the call is wrapped in
// runtime.WithTimeout(60s) and runtime.TrackElapsed so the
// upstream pipeline gets _created_time / _elapsed_time stamps
// matching the python ProcessBase contract (base.py:42, 58).
//
// - JSON PARSING: the prompt asks the LLM to return a JSON object;
// we best-effort parse the response into map[string]any. A
// non-JSON response is NOT a hard error — it's surfaced as the
// raw string under the same field name so downstream callers
// can decide what to do.
//
// - WHAT IS NOT YET PORTED: the python _build_TOC branch
// (rag/flow/extractor/extractor.py:40-72) requires the TOC
// generator (rag.prompts.generator.run_toc_from_text). That
// service has no Go counterpart yet; the current Extractor
// short-circuits with a clear error when field_name == "toc"
// so a future Phase 2.5+ task can fill the gap without a
// silent regression.
//
// - SINGLE-CHUNK FAST PATH: when no chunk list is wired in,
// the LLM is called once with the resolved args directly (no
// chunk substitution). Matches python _invoke path
// (line 108: msg, sys_prompt = self._sys_prompt_and_msg([], args)).
package component
import (
"context"
"encoding/json"
fix(ingestion): improve Extractor/LLM robustness and Python->Go parity (#17470) ## Summary This PR hardens the Go ingestion **Extractor** and **LLM retry** paths and closes several Python->Go parity gaps in the keyword/question/tag extraction flow. - **Generic retry utility** (`internal/common/retry.go`): `RetryWithBackoff` with exponential backoff (default 3 retries, 2s initial delay, capped at 1m), context-aware sleep, and a `maxRetries<=0` fast path. Covered by `internal/common/retry_test.go`. - **LLM retry reuse**: `agent/component/llm_retry.go` now delegates to `common.RetryWithBackoff` instead of an inline loop (behavior preserved: ctx cancellation short-circuits the backoff). - **Extractor LLM calls** (`extractor.go`): - `call()` now retries transient LLM failures via `RetryWithBackoff` (retry exhaustion fails the chunk instead of silently skipping). - Sets `temperature = 0.2`, matching Python `generator.py:230,245`. - Runs keyword and question extraction **concurrently** per chunk when both are enabled (`task_executor.py:444-448`), with mutex-guarded map writes to avoid data races. - Substitutes `{field_name}` placeholders (including `{chunks}` -> chunk text) in `prompt`/`system_prompt` before the call, mirroring Python `string_format` (`extractor.py:102-103`); unmatched placeholders are left as-is. - Falls back to the **tenant default chat model** when `llm_id` is empty (`task_executor.py:573-574`). - Strips `` **greedily** (`strings.LastIndex`) in `cleanExtractionResult`. - **Auto-tagging** (`extractor_tag.go`): drops the `in.llmID != ""` guards so an empty `llm_id` no longer skips tagging (uses the tenant default model), and strips `` greedily in `parseTaggerResponse`. - **Docs**: fixes a misleading `PresentationChunker` docstring that claimed per-slide `image`/`position` output (the PPTX path emits none — unlike PDF), and removes a stale `docs/migration_python_go_diff.md` reference in `media_dispatch.go`. ## Test plan - `bash build.sh --test ./internal/common/...` — passes (new retry utility + tests). - `bash build.sh --test ./internal/ingestion/component/...` — passes (extractor/chunker/schema). - `gofmt` and lefthook pre-commit checks pass. Note: the personal `docs/migration_python_go_diff.md` working notebook in the tree is intentionally **not** part of this PR. --------- Co-authored-by: CodeBuddy <noreply@codebuddy.ai>
2026-07-28 19:22:18 +08:00
"errors"
"fmt"
"regexp"
"sort"
"strings"
"sync"
"time"
eschema "github.com/cloudwego/eino/schema"
"go.uber.org/zap"
"gorm.io/gorm"
"ragflow/internal/agent/runtime"
"ragflow/internal/common"
"ragflow/internal/entity"
"ragflow/internal/entity/models"
"ragflow/internal/ingestion/component/schema"
"ragflow/internal/tokenizer"
)
const componentNameExtractor = "Extractor"
// extractorTimeout bounds one LLM chat call. Matches the python
// `@timeout(60)` default at rag/flow/base.py:60. The pipeline
// orchestrator (Phase 3) overrides this if a stage-level ceiling
// is configured.
fix(ingestion): align laws DSL with Python — heading fallback, colon-title, short-line filter, remove_toc, and image extension mapping (#17200) ## Summary This PR aligns the Go ingestion pipeline's **Laws** DSL template with the Python implementation by fixing heading-detection gaps, adds image-extension support, refactors the **Extractor** component's LLM resolution, hardens heading detection for CJK text, and makes the Extractor accept the Python DSL prompt key names (`sys_prompt`/`prompts`) alongside the Go names. ## Changes ### 1. Picture file-type detection (`internal/utility/file.go`) Adds explicit mapping for common image extensions (png, jpg, jpeg, gif, bmp, tiff, tif, webp, svg, ico, avif, heic, apng) → `FileTypeVISUAL`, with regression tests. ### 2. Laws DSL heading-detection alignment (`internal/ingestion/component/chunker/`) Four fixes to `resolveTitleLevels`: | Fix | What changed | Why | |-----|-------------|-----| | **DOCX `ck_type` fallback** | `ckType` field on `lineRecord`, propagated from `ChunkDoc.CKType` in `recordsFromStructured`. When `ck_type=="heading"`, assign `fallbackLevel`. | office_oxide extracts DOCX heading metadata, but the info was lost before reaching the heading detector. Word headings whose text doesn't match any regex (e.g. "Introduction") were treated as body. | | **`make_colon_as_title` promotion** | `isColonTitle()`: promotes lines ending with `:`/`:` that have sentence-ending punctuation before the colon and ≥32 runes between them. | Mirrors Python's `make_colon_as_title` in `rag/nlp/__init__.py`. Triple guard prevents false positives. | | **Short/numeric line filter** | Lines with ≤1 rune or purely numeric are pinned to body level. | Mirrors Python `tree_merge`'s filter of `sections` where `len(...) <= 1` or `re.match(r"[0-9]+$", ...)`. | | **PDF `remove_toc`** | `"remove_toc": true` added to the PDF parser setup in `ingestion_pipeline_laws.json`. | The Go PDF parser already supports TOC removal; the Book template already enables it. | ### 3. Extractor llm_id resolution (`internal/ingestion/component/extractor.go`) Refactored to handle both **bare tenant_model UUIDs** and **composite model@provider** strings via the shared `resolveModelConfig` (`dispatch_model.go`): - **`resolveExtractorChatConfig`** — UUID path calls `resolveModelConfigByID` directly (one DB hit); composite path goes through `resolveModelConfig`. Added `isBareTenantModelID` pre-check for clear errors when a UUID doesn't exist. - **`resolveExtractorChatTarget`** — propagates resolution errors instead of silently returning empty driver. - **`Chat()`** — removed `driver = "dummy"` fallback. Missing driver is now an explicit error. - **Removed dead code**: `splitExtractorLLID`, `findExtractorSoleActiveInstance`. ### 4. `InjectExtractorLLMID` — fallback when no user config (`internal/common/parser_config.go`) Injects the tenant's global default LLM into extractor components **only when their `llm_id` is empty**. Preserves user-selected UUID or model@provider values. Priority: user-configured llm_id > tenant global default > error (no silent dummy fallback). ### 5. `ResponseHeaderTimeout` increase (`internal/entity/models/base_model.go`) `ResponseHeaderTimeout` 60s → 120s in `NewDriverHTTPClient`. Reasoning models with large extraction prompts can take longer than 60s to produce the first response token. ### 6. CJK rune-aware heading detection (`internal/ingestion/component/chunker/title.go`) Two byte-vs-rune bugs that only manifest on CJK text: | Fix | What changed | Why | |-----|-------------|-----| | **`isColonTitle` byte offset** | `body[lastPunct+1:]` → `body[lastPunct+runeLen:]` via `utf8.DecodeRuneInString` | `strings.LastIndexAny` returns a byte index; `+1` skips only 1 byte, corrupting multi-byte CJK punctuation (e.g. `。` = 3 bytes) and inflating the rune count past the 32-rune threshold → false-positive heading promotion. | | **Short-line filter byte count** | `len(text) <= 1` → `utf8.RuneCountInString(text) <= 1` | Go `len` is UTF-8 bytes; a single CJK char (3 bytes) passed the filter, but Python's `len` returns 1 → mismatch. | ### 7. `extractor_tag.go` — log error when llm fails When `resolveExtractorChatTarget` returned an error, `runAutoTags` will log error. ### 8. Python DSL prompt-key compatibility (`internal/ingestion/component/extractor.go`) The Resume DSL template uses Python-side key names (`sys_prompt`, `prompts`). `NewExtractorComponent` now accepts them as fallbacks alongside the Go names: - `system_prompt` (Go) ← `sys_prompt` (Python) as fallback - `prompt` (Go string) ← `prompts` (Python array `[{"role","content"}]`, takes `[0].content`) as fallback Mirrors the alias pattern already in `internal/agent/component/llm.go`. `resolveInputs` accepts per-call `sys_prompt` override too. ## Remaining gaps vs Python | Gap | Scope | Impact | |-----|-------|--------| | **TOC removal for TXT/MD/HTML** | Python's `remove_contents_table` works on all text formats; Go's `remove_toc` is PDF-only. | Low — plain-text documents rarely contain structured TOCs. | | **Regex pattern details** | Minor differences in quantifiers, missing H5/H6 markdown patterns, missing 4-level numbering pattern. | Low — Go's variants are stricter; DOCX headings are covered by `ck_type` fallback. | ## Testing - `TestHierarchyTitleChunker_CKTypeHeadingFallback` — DOCX `ck_type` heading promotion - `TestHierarchyTitleChunker_ColonTitlePromotion` / `_ColonTitleShortLine_Negative` — colon-title promotion + guard - `TestIsColonTitle_CJKEdgeCase` / `TestIsColonTitle_ASCII_NoRegression` — CJK byte-offset fix + ASCII regression - `TestHierarchyTitleChunker_ColonTitlePromotion_CJK_EdgeCase` — CJK colon edge case through full pipeline - `TestHierarchyTitleChunker_ShortSingleCJKLineFilter` — single CJK char filtered to body - `TestHierarchyTitleChunker_ShortNumericLineFilter` — purely numeric lines filtered - `TestGetFileType_ImageExtensions` / `_ExistingFormats_NoRegression` — image extension mapping - `TestInjectExtractorLLMID_SkipWhenUUID` / `_SkipWhenComposite` / `_InjectWhenEmpty` — llm_id injection guard - `TestIsBareTenantModelID` — UUID detection - `TestResolveExtractorChatTarget_AtSplitFallback` / `_NoDriver` — @ split fallback without DB - `TestNewExtractorComponent_SysPromptAlias` / `_PromptsArray` / `_PromptsArray_PromptWins` / `_SystemPromptWinsOverSysPrompt` — Python key compatibility - `TestBuildDOCXJSONSections_List` / `_TextBox` / `_MixedWithList` — DOCX list/text_box parsing - Full ingestion test suite passes (chunker, pipeline, task, service, component packages)
2026-07-22 19:14:32 +08:00
const extractorTimeout = 600 * time.Second
fix(ingestion): improve Extractor/LLM robustness and Python->Go parity (#17470) ## Summary This PR hardens the Go ingestion **Extractor** and **LLM retry** paths and closes several Python->Go parity gaps in the keyword/question/tag extraction flow. - **Generic retry utility** (`internal/common/retry.go`): `RetryWithBackoff` with exponential backoff (default 3 retries, 2s initial delay, capped at 1m), context-aware sleep, and a `maxRetries<=0` fast path. Covered by `internal/common/retry_test.go`. - **LLM retry reuse**: `agent/component/llm_retry.go` now delegates to `common.RetryWithBackoff` instead of an inline loop (behavior preserved: ctx cancellation short-circuits the backoff). - **Extractor LLM calls** (`extractor.go`): - `call()` now retries transient LLM failures via `RetryWithBackoff` (retry exhaustion fails the chunk instead of silently skipping). - Sets `temperature = 0.2`, matching Python `generator.py:230,245`. - Runs keyword and question extraction **concurrently** per chunk when both are enabled (`task_executor.py:444-448`), with mutex-guarded map writes to avoid data races. - Substitutes `{field_name}` placeholders (including `{chunks}` -> chunk text) in `prompt`/`system_prompt` before the call, mirroring Python `string_format` (`extractor.py:102-103`); unmatched placeholders are left as-is. - Falls back to the **tenant default chat model** when `llm_id` is empty (`task_executor.py:573-574`). - Strips `` **greedily** (`strings.LastIndex`) in `cleanExtractionResult`. - **Auto-tagging** (`extractor_tag.go`): drops the `in.llmID != ""` guards so an empty `llm_id` no longer skips tagging (uses the tenant default model), and strips `` greedily in `parseTaggerResponse`. - **Docs**: fixes a misleading `PresentationChunker` docstring that claimed per-slide `image`/`position` output (the PPTX path emits none — unlike PDF), and removes a stale `docs/migration_python_go_diff.md` reference in `media_dispatch.go`. ## Test plan - `bash build.sh --test ./internal/common/...` — passes (new retry utility + tests). - `bash build.sh --test ./internal/ingestion/component/...` — passes (extractor/chunker/schema). - `gofmt` and lefthook pre-commit checks pass. Note: the personal `docs/migration_python_go_diff.md` working notebook in the tree is intentionally **not** part of this PR. --------- Co-authored-by: CodeBuddy <noreply@codebuddy.ai>
2026-07-28 19:22:18 +08:00
// extractorRetryMax and extractorRetryDelay are package-level vars
// so tests can override them (extractorRetryDelay → time.Millisecond)
// to avoid multi-second retry sleeps. Production defaults match
// common.RetryWithBackoff defaults (3 retries, 2s initial delay).
var (
extractorRetryMax = common.DefaultRetryMax
extractorRetryDelay = common.DefaultRetryDelay
)
// extractorTemperature mirrors Python's 0.2 default for keyword
// and question extraction calls (generator.py:230,245).
const extractorTemperature = 0.2
const (
autoKeywordPrompt = `## Role
You are a text analyzer.
## Task
Extract the most important keywords/phrases of a given piece of text content.
## Requirements
- Summarize the text content, and give the top %d important keywords/phrases.
- The keywords MUST be in the same language as the given piece of text content.
- The keywords are delimited by ENGLISH COMMA.
- Output keywords ONLY.
---
## Text Content
%s`
autoQuestionPrompt = `## Role
You are a text analyzer.
## Task
Propose questions about a given piece of text content.
## Requirements
- Understand and summarize the text content, and propose the top %d important questions.
- The questions SHOULD NOT have overlapping meanings.
- The questions SHOULD cover the main content of the text as much as possible.
- The questions MUST be in the same language as the given piece of text content.
- One question per line.
- Output questions ONLY.
---
## Text Content
%s`
)
// ExtractorComponent performs LLM-based extraction over a chunk
// list (or a single empty call when no chunks are wired in).
//
// The instance is safe for concurrent invocation: each Invoke
// reads Param read-only (Param is set at construction; per-call
// overrides flow through the inputs map). The single mutable
// package-level seam (extractorChatInvoker) is guarded by a
// RWMutex; tests swap it via SetExtractorChatInvoker.
type ExtractorComponent struct {
Param schema.ExtractorParam
}
// NewExtractorComponent constructs an Extractor from a DSL param
// map. Missing keys fall back to schema.ExtractorParam.Defaults().
//
// Param map shape (all keys optional; missing → Defaults()):
//
// {
// "field_name": string, — optional; key the extraction lands under
// "llm_id": string, — optional; resolves via models.NewModelFactory
// "system_prompt": string, — optional override
// "prompt": string, — optional user prompt
// }
//
// errors here surface as canvas compile failures so a malformed
// param is caught at build time rather than mid-run.
func NewExtractorComponent(params map[string]any) (runtime.Component, error) {
p := schema.ExtractorParam{}.Defaults()
if params != nil {
if v, ok := params["field_name"].(string); ok {
p.FieldName = v
}
if v, ok := params["llm_id"].(string); ok {
p.LLMID = v
}
if v, ok := params["system_prompt"].(string); ok {
p.SystemPrompt = v
fix(ingestion): align laws DSL with Python — heading fallback, colon-title, short-line filter, remove_toc, and image extension mapping (#17200) ## Summary This PR aligns the Go ingestion pipeline's **Laws** DSL template with the Python implementation by fixing heading-detection gaps, adds image-extension support, refactors the **Extractor** component's LLM resolution, hardens heading detection for CJK text, and makes the Extractor accept the Python DSL prompt key names (`sys_prompt`/`prompts`) alongside the Go names. ## Changes ### 1. Picture file-type detection (`internal/utility/file.go`) Adds explicit mapping for common image extensions (png, jpg, jpeg, gif, bmp, tiff, tif, webp, svg, ico, avif, heic, apng) → `FileTypeVISUAL`, with regression tests. ### 2. Laws DSL heading-detection alignment (`internal/ingestion/component/chunker/`) Four fixes to `resolveTitleLevels`: | Fix | What changed | Why | |-----|-------------|-----| | **DOCX `ck_type` fallback** | `ckType` field on `lineRecord`, propagated from `ChunkDoc.CKType` in `recordsFromStructured`. When `ck_type=="heading"`, assign `fallbackLevel`. | office_oxide extracts DOCX heading metadata, but the info was lost before reaching the heading detector. Word headings whose text doesn't match any regex (e.g. "Introduction") were treated as body. | | **`make_colon_as_title` promotion** | `isColonTitle()`: promotes lines ending with `:`/`:` that have sentence-ending punctuation before the colon and ≥32 runes between them. | Mirrors Python's `make_colon_as_title` in `rag/nlp/__init__.py`. Triple guard prevents false positives. | | **Short/numeric line filter** | Lines with ≤1 rune or purely numeric are pinned to body level. | Mirrors Python `tree_merge`'s filter of `sections` where `len(...) <= 1` or `re.match(r"[0-9]+$", ...)`. | | **PDF `remove_toc`** | `"remove_toc": true` added to the PDF parser setup in `ingestion_pipeline_laws.json`. | The Go PDF parser already supports TOC removal; the Book template already enables it. | ### 3. Extractor llm_id resolution (`internal/ingestion/component/extractor.go`) Refactored to handle both **bare tenant_model UUIDs** and **composite model@provider** strings via the shared `resolveModelConfig` (`dispatch_model.go`): - **`resolveExtractorChatConfig`** — UUID path calls `resolveModelConfigByID` directly (one DB hit); composite path goes through `resolveModelConfig`. Added `isBareTenantModelID` pre-check for clear errors when a UUID doesn't exist. - **`resolveExtractorChatTarget`** — propagates resolution errors instead of silently returning empty driver. - **`Chat()`** — removed `driver = "dummy"` fallback. Missing driver is now an explicit error. - **Removed dead code**: `splitExtractorLLID`, `findExtractorSoleActiveInstance`. ### 4. `InjectExtractorLLMID` — fallback when no user config (`internal/common/parser_config.go`) Injects the tenant's global default LLM into extractor components **only when their `llm_id` is empty**. Preserves user-selected UUID or model@provider values. Priority: user-configured llm_id > tenant global default > error (no silent dummy fallback). ### 5. `ResponseHeaderTimeout` increase (`internal/entity/models/base_model.go`) `ResponseHeaderTimeout` 60s → 120s in `NewDriverHTTPClient`. Reasoning models with large extraction prompts can take longer than 60s to produce the first response token. ### 6. CJK rune-aware heading detection (`internal/ingestion/component/chunker/title.go`) Two byte-vs-rune bugs that only manifest on CJK text: | Fix | What changed | Why | |-----|-------------|-----| | **`isColonTitle` byte offset** | `body[lastPunct+1:]` → `body[lastPunct+runeLen:]` via `utf8.DecodeRuneInString` | `strings.LastIndexAny` returns a byte index; `+1` skips only 1 byte, corrupting multi-byte CJK punctuation (e.g. `。` = 3 bytes) and inflating the rune count past the 32-rune threshold → false-positive heading promotion. | | **Short-line filter byte count** | `len(text) <= 1` → `utf8.RuneCountInString(text) <= 1` | Go `len` is UTF-8 bytes; a single CJK char (3 bytes) passed the filter, but Python's `len` returns 1 → mismatch. | ### 7. `extractor_tag.go` — log error when llm fails When `resolveExtractorChatTarget` returned an error, `runAutoTags` will log error. ### 8. Python DSL prompt-key compatibility (`internal/ingestion/component/extractor.go`) The Resume DSL template uses Python-side key names (`sys_prompt`, `prompts`). `NewExtractorComponent` now accepts them as fallbacks alongside the Go names: - `system_prompt` (Go) ← `sys_prompt` (Python) as fallback - `prompt` (Go string) ← `prompts` (Python array `[{"role","content"}]`, takes `[0].content`) as fallback Mirrors the alias pattern already in `internal/agent/component/llm.go`. `resolveInputs` accepts per-call `sys_prompt` override too. ## Remaining gaps vs Python | Gap | Scope | Impact | |-----|-------|--------| | **TOC removal for TXT/MD/HTML** | Python's `remove_contents_table` works on all text formats; Go's `remove_toc` is PDF-only. | Low — plain-text documents rarely contain structured TOCs. | | **Regex pattern details** | Minor differences in quantifiers, missing H5/H6 markdown patterns, missing 4-level numbering pattern. | Low — Go's variants are stricter; DOCX headings are covered by `ck_type` fallback. | ## Testing - `TestHierarchyTitleChunker_CKTypeHeadingFallback` — DOCX `ck_type` heading promotion - `TestHierarchyTitleChunker_ColonTitlePromotion` / `_ColonTitleShortLine_Negative` — colon-title promotion + guard - `TestIsColonTitle_CJKEdgeCase` / `TestIsColonTitle_ASCII_NoRegression` — CJK byte-offset fix + ASCII regression - `TestHierarchyTitleChunker_ColonTitlePromotion_CJK_EdgeCase` — CJK colon edge case through full pipeline - `TestHierarchyTitleChunker_ShortSingleCJKLineFilter` — single CJK char filtered to body - `TestHierarchyTitleChunker_ShortNumericLineFilter` — purely numeric lines filtered - `TestGetFileType_ImageExtensions` / `_ExistingFormats_NoRegression` — image extension mapping - `TestInjectExtractorLLMID_SkipWhenUUID` / `_SkipWhenComposite` / `_InjectWhenEmpty` — llm_id injection guard - `TestIsBareTenantModelID` — UUID detection - `TestResolveExtractorChatTarget_AtSplitFallback` / `_NoDriver` — @ split fallback without DB - `TestNewExtractorComponent_SysPromptAlias` / `_PromptsArray` / `_PromptsArray_PromptWins` / `_SystemPromptWinsOverSysPrompt` — Python key compatibility - `TestBuildDOCXJSONSections_List` / `_TextBox` / `_MixedWithList` — DOCX list/text_box parsing - Full ingestion test suite passes (chunker, pipeline, task, service, component packages)
2026-07-22 19:14:32 +08:00
} else if v, ok := params["sys_prompt"].(string); ok {
p.SystemPrompt = v
}
if v, ok := params["prompt"].(string); ok {
p.Prompt = v
Fix(go): align ingestion pipeline with Python (parser/media dispatch + PDF coordinate chain + Chunker) (#17349) ## Summary Aligns the Go ingestion pipeline with the Python implementation, closing several behavioral gaps found during the Python→Go migration (tracked in `docs/migration_python_go_diff.md`). Covers parser/media dispatch alignment, the PDF coordinate-chain (preview images, outline→title, chunk coordinate finalization), and the Chunker Token/QA batches below. Commits are grouped as follows. ### 1. Fix parser params (c524f450e) Fixes parser/media wiring and several dispatch gaps: - **docx/pdf vision dispatch**: correct parameter handling and VLM invocation. - **markdown vision (diff 2.5)**: also enhance items whose `doc_type_kwd` is `table`, not only `image` (parser/utils.py:181). - **media audio (diff 2.11)**: when `output_format` is `json`, carry the ASR transcription as a JSON item instead of only the `Text` field (the Invoke switch had no `json` branch and dropped it). - **email (diff 2.2)**: default `output_format` is `json` (parser.py:212), not `text`. - **tokenizer**: handle empty/whitespace-only names; trim before embedding. - **extractor**: tag-matching parameter wiring. - **split**: keyword-split regex now covers CJK/English separators. - **parser.go**: parser-param plumbing. ### 2. fix parser gap (373537da1) Image dispatch now mirrors `rag/app/picture.py:chunk()`: - Always OCR the image (PaddleOCR or local ONNX). - When OCR text is short, also call VLM (`describe`) and combine `OCR + VLM` text. - Emits a **structured JSON item** carrying the image data-URI and `doc_type_kwd:"image"`, instead of a bare `Text` string. This fixes the payload being rejected downstream by OneChunker/TokenChunker (JSON=nil). ### 3. PDF coordinate-chain fixes (55367a820, 727f8167c) Closes three items from the migration tracker in the chunker/tokenizer/task layer: - **(Chunker-1.3) `restore_pdf_text_previews`** — `needsCrop` now also returns true for `text` chunks that carry PDF positions (`pdfcrop_cgo.go`), so text blocks get a rendered preview image uploaded to storage via `imageUploadDecorator`/`ChunkImageUploader`, matching Python `restore_pdf_text_previews` + `image2id`. - **(Chunker-1.5) PDF outline → title levels** — `title.go` adds `outlineSimilarity` (rune-bigram Jaccard, mirroring `common.py:_outline_similarity`), `resolveOutlineLevels` (matches text lines to outline entries at similarity > 0.8, with a sparse guard `len(outline)/len(records) <= 0.03`), and `outlineFromInputs` (reads `file.outline`). Wired into `newLevelContext` in both `group.go` and `hierarchy.go`; falls back to the title-shape heuristic when no outline is present. - **(Tokenizer-(T)1) `finalize_pdf_chunk`** — the coordinate → `position_int`/`page_num_int`/`top_int` conversion is owned by the task layer (`processChunkPositions`→`AddPositions`), which runs *after* the tokenizer and consumes the tokenizer-owned fields. The tokenizer only preserves the raw `positions`/`_pdf_positions` (no duplicate conversion), pinned by `TestChunkDocsToMaps_PreservesPDFPositions`. ### 4. Integration test made environment-free (`internal/ingestion/task/pipeline_real_integration_test.go`) - Removed the `//go:build integration` tag so the contract tests run under the default `build.sh --test` (which does not pass `-tags integration`). - External dependencies replaced with in-memory substitutes so no MySQL/MinIO/ES is required: - MySQL → on-disk sqlite (`glebarez/sqlite`) with the needed tables auto-migrated. - MinIO → `storage.NewMemoryStorage()`. - Elasticsearch → chunks captured via `WithInsertFunc` instead of `engine.InsertChunks`/`Search`. - `requireTokenizerPool` still skips gracefully when the native tokenizer pool is unavailable; `WithLogCreateFunc(noop)` avoids depending on the operation-log table. - Added `taskChunkFieldEqualsStr` to tolerate `kb_id` being a `[]string`/`[]any` in the raw chunk payload (the search engine flattens it to a string on read). ### 5. TokenChunker alignment — Batch 1 (`internal/ingestion/component/chunker/token.go`) Closes four Chunker items from the migration tracker: - **(Chunker-2.1) sentence delimiter** — the boundary regex now also breaks on ASCII `!`/`?`. Extracted to a package-level `var sentenceDelimiter` and used in `mergeByTokenSize`, matching Python's full delimiter set. - **(Chunker-2.2) overlap tag leakage** — when a new chunk starts, its overlap prefix is taken from the previous chunk *after* `removeTag`, in both the text path (`mergeByTokenSize`) and the JSON path (`mergeByTokenSizeFromJSON`). Parser tags (`@@…##`) no longer leak into the overlap region (mirrors `nlp/__init__.py:1181`). - **(Chunker-2.11) empty-text merge** — merging a non-empty chunk into an empty previous chunk now assigns the text directly instead of being skipped (`mergeByTokenSizeFromJSON`), mirroring `token_chunker.py:236-239`. - **(Chunker-2.4) overlap token counting** — `takeFromEnd`/`takeFromStart` now count tokens exactly via `tokenizeStr` instead of the 4-bytes/token heuristic, fixing over-counting for CJK text. ### 6. QA Chunker alignment — Batch 2 (`internal/ingestion/component/chunker/qa.go` + `schema`) Closes three Chunker items from the migration tracker: - **(Chunker-2.13) default language** — an empty `lang` now defaults to Chinese prefixes (`问题:`/`回答:`) instead of English, matching `qa.py:299`. - **(Chunker-2.12) `rmQAPrefix` regex** — the separator is changed to `[\t:: ]+` (one-or-more), matching `qa.py:241`, so multiple separators (e.g. `Q:: answer`) are fully stripped. - **(Chunker-1.8 QA) missing chunk fields** — QA chunks now preserve: - `top_int` — the source row/record index, threaded through the tab/csv/markdown extractors (mirrors `qa.py` `beAdoc(..., row_num=i)`); - `image` + `doc_type_kwd:"image"`; - `_pdf_positions` / `positions` carried from the upstream JSON item. `schema.ChunkDoc` gains a `TopInt []int` field (serialized as `top_int`, registered in `UnmarshalJSON`). Note: the Tag/Table/Presentation/One chunker field gaps under 1.8 remain pending. ## Test plan - Added/updated unit tests: `pdfcrop_cgo_test.go` (`TestNeedsCrop`, `TestRestorePDFTextPreview`), `title_test.go` (`TestResolveOutlineLevels`, `TestResolveOutlineLevels_SparseGuard`, `TestNewLevelContext_OutlineBranch`, `TestOutlineFromInputs`), `tokenizer_unit_test.go` (`TestChunkDocsToMaps_PreservesPDFPositions`), `token_pdfpos_test.go`. - **Batch 1** — `token_batch1_test.go`: `TestSentenceDelimiterMatchesBangAndQuestion`, `TestMergeByTokenSizeFromJSON_OverlapStripsTags`, `TestMergeByTokenSizeFromJSON_EmptyPrevKeepsChunk`, `TestTakeFromEndRespectsTokenCount`, `TestTakeFromStartRespectsTokenCount`. - **Batch 2** — `qa_batch2_test.go`: `TestQAChunker_DefaultLangIsChinese`, `TestRmQAPrefixStripsMultipleSeparators`, `TestQAChunker_SetsTopInt`, `TestQAChunker_CarriesImageAndPositions`. Existing `qa_test.go` expectations were updated to the corrected language default / separator behavior. - `pipeline_real_integration_test.go` (`TestPipelineExecutor_Run_RealCanvasDSL_UsesGeneralPipeline`, `TestPipelineExecutor_Run_RealPDF_ProducesIndexedChunks`, `TestRunPipeline_RealPipelineOutput_ProducesIndexFields`) now runs without any external service. - `bash build.sh --test ./internal/ingestion/...` passes. - No files deleted.
2026-07-24 21:06:38 +08:00
} else if v, ok := params["prompts"].(string); ok && v != "" {
// Python agent/component/llm.py:119-120 normalizes a bare-string
// prompts into [{"role":"user","content":prompts}]. Mirror that
// here so a front-end/template that emits prompts as a string
// (the graph.nodes form / dsl testdata) is not silently dropped
// by the .([]any) assertion on the list branch below.
p.Prompt = v
fix(ingestion): align laws DSL with Python — heading fallback, colon-title, short-line filter, remove_toc, and image extension mapping (#17200) ## Summary This PR aligns the Go ingestion pipeline's **Laws** DSL template with the Python implementation by fixing heading-detection gaps, adds image-extension support, refactors the **Extractor** component's LLM resolution, hardens heading detection for CJK text, and makes the Extractor accept the Python DSL prompt key names (`sys_prompt`/`prompts`) alongside the Go names. ## Changes ### 1. Picture file-type detection (`internal/utility/file.go`) Adds explicit mapping for common image extensions (png, jpg, jpeg, gif, bmp, tiff, tif, webp, svg, ico, avif, heic, apng) → `FileTypeVISUAL`, with regression tests. ### 2. Laws DSL heading-detection alignment (`internal/ingestion/component/chunker/`) Four fixes to `resolveTitleLevels`: | Fix | What changed | Why | |-----|-------------|-----| | **DOCX `ck_type` fallback** | `ckType` field on `lineRecord`, propagated from `ChunkDoc.CKType` in `recordsFromStructured`. When `ck_type=="heading"`, assign `fallbackLevel`. | office_oxide extracts DOCX heading metadata, but the info was lost before reaching the heading detector. Word headings whose text doesn't match any regex (e.g. "Introduction") were treated as body. | | **`make_colon_as_title` promotion** | `isColonTitle()`: promotes lines ending with `:`/`:` that have sentence-ending punctuation before the colon and ≥32 runes between them. | Mirrors Python's `make_colon_as_title` in `rag/nlp/__init__.py`. Triple guard prevents false positives. | | **Short/numeric line filter** | Lines with ≤1 rune or purely numeric are pinned to body level. | Mirrors Python `tree_merge`'s filter of `sections` where `len(...) <= 1` or `re.match(r"[0-9]+$", ...)`. | | **PDF `remove_toc`** | `"remove_toc": true` added to the PDF parser setup in `ingestion_pipeline_laws.json`. | The Go PDF parser already supports TOC removal; the Book template already enables it. | ### 3. Extractor llm_id resolution (`internal/ingestion/component/extractor.go`) Refactored to handle both **bare tenant_model UUIDs** and **composite model@provider** strings via the shared `resolveModelConfig` (`dispatch_model.go`): - **`resolveExtractorChatConfig`** — UUID path calls `resolveModelConfigByID` directly (one DB hit); composite path goes through `resolveModelConfig`. Added `isBareTenantModelID` pre-check for clear errors when a UUID doesn't exist. - **`resolveExtractorChatTarget`** — propagates resolution errors instead of silently returning empty driver. - **`Chat()`** — removed `driver = "dummy"` fallback. Missing driver is now an explicit error. - **Removed dead code**: `splitExtractorLLID`, `findExtractorSoleActiveInstance`. ### 4. `InjectExtractorLLMID` — fallback when no user config (`internal/common/parser_config.go`) Injects the tenant's global default LLM into extractor components **only when their `llm_id` is empty**. Preserves user-selected UUID or model@provider values. Priority: user-configured llm_id > tenant global default > error (no silent dummy fallback). ### 5. `ResponseHeaderTimeout` increase (`internal/entity/models/base_model.go`) `ResponseHeaderTimeout` 60s → 120s in `NewDriverHTTPClient`. Reasoning models with large extraction prompts can take longer than 60s to produce the first response token. ### 6. CJK rune-aware heading detection (`internal/ingestion/component/chunker/title.go`) Two byte-vs-rune bugs that only manifest on CJK text: | Fix | What changed | Why | |-----|-------------|-----| | **`isColonTitle` byte offset** | `body[lastPunct+1:]` → `body[lastPunct+runeLen:]` via `utf8.DecodeRuneInString` | `strings.LastIndexAny` returns a byte index; `+1` skips only 1 byte, corrupting multi-byte CJK punctuation (e.g. `。` = 3 bytes) and inflating the rune count past the 32-rune threshold → false-positive heading promotion. | | **Short-line filter byte count** | `len(text) <= 1` → `utf8.RuneCountInString(text) <= 1` | Go `len` is UTF-8 bytes; a single CJK char (3 bytes) passed the filter, but Python's `len` returns 1 → mismatch. | ### 7. `extractor_tag.go` — log error when llm fails When `resolveExtractorChatTarget` returned an error, `runAutoTags` will log error. ### 8. Python DSL prompt-key compatibility (`internal/ingestion/component/extractor.go`) The Resume DSL template uses Python-side key names (`sys_prompt`, `prompts`). `NewExtractorComponent` now accepts them as fallbacks alongside the Go names: - `system_prompt` (Go) ← `sys_prompt` (Python) as fallback - `prompt` (Go string) ← `prompts` (Python array `[{"role","content"}]`, takes `[0].content`) as fallback Mirrors the alias pattern already in `internal/agent/component/llm.go`. `resolveInputs` accepts per-call `sys_prompt` override too. ## Remaining gaps vs Python | Gap | Scope | Impact | |-----|-------|--------| | **TOC removal for TXT/MD/HTML** | Python's `remove_contents_table` works on all text formats; Go's `remove_toc` is PDF-only. | Low — plain-text documents rarely contain structured TOCs. | | **Regex pattern details** | Minor differences in quantifiers, missing H5/H6 markdown patterns, missing 4-level numbering pattern. | Low — Go's variants are stricter; DOCX headings are covered by `ck_type` fallback. | ## Testing - `TestHierarchyTitleChunker_CKTypeHeadingFallback` — DOCX `ck_type` heading promotion - `TestHierarchyTitleChunker_ColonTitlePromotion` / `_ColonTitleShortLine_Negative` — colon-title promotion + guard - `TestIsColonTitle_CJKEdgeCase` / `TestIsColonTitle_ASCII_NoRegression` — CJK byte-offset fix + ASCII regression - `TestHierarchyTitleChunker_ColonTitlePromotion_CJK_EdgeCase` — CJK colon edge case through full pipeline - `TestHierarchyTitleChunker_ShortSingleCJKLineFilter` — single CJK char filtered to body - `TestHierarchyTitleChunker_ShortNumericLineFilter` — purely numeric lines filtered - `TestGetFileType_ImageExtensions` / `_ExistingFormats_NoRegression` — image extension mapping - `TestInjectExtractorLLMID_SkipWhenUUID` / `_SkipWhenComposite` / `_InjectWhenEmpty` — llm_id injection guard - `TestIsBareTenantModelID` — UUID detection - `TestResolveExtractorChatTarget_AtSplitFallback` / `_NoDriver` — @ split fallback without DB - `TestNewExtractorComponent_SysPromptAlias` / `_PromptsArray` / `_PromptsArray_PromptWins` / `_SystemPromptWinsOverSysPrompt` — Python key compatibility - `TestBuildDOCXJSONSections_List` / `_TextBox` / `_MixedWithList` — DOCX list/text_box parsing - Full ingestion test suite passes (chunker, pipeline, task, service, component packages)
2026-07-22 19:14:32 +08:00
} else if promptsRaw, ok := params["prompts"].([]any); ok && len(promptsRaw) > 0 {
if first, ok := promptsRaw[0].(map[string]any); ok {
if content, ok := first["content"].(string); ok {
p.Prompt = content
}
}
}
if v, ok := params["auto_keywords"]; ok {
p.AutoKeywords = mapInt(v)
}
if v, ok := params["auto_questions"]; ok {
p.AutoQuestions = mapInt(v)
}
if v, ok := params["auto_tags"]; ok {
p.AutoTags = mapInt(v)
}
if v, ok := params["tag_file_id"].(string); ok {
p.TagFileID = v
}
}
if err := p.Validate(); err != nil {
return nil, fmt.Errorf("extractor: param check: %w", err)
}
return &ExtractorComponent{Param: p}, nil
}
// Inputs returns the parameter metadata. Matches the python
// Extractor._invoke kwargs plus the optional per-call llm_id
// override (python: args["llm_id"] path is implicit via
// self.chat_mdl; the Go port exposes it explicitly).
func (c *ExtractorComponent) Inputs() map[string]string {
return map[string]string{
"chunks": "List of map[string]any from upstream Tokenizer. Each entry must carry a string 'text' (or 'content_with_weight') field. Optional — when absent the LLM is called once with the resolved args.",
"prompt": "Optional user prompt template. Falls back to Param.Prompt when absent.",
"llm_id": "Optional per-call LLM id override. Falls back to Param.LLMID when absent.",
"system_prompt": "Optional per-call system prompt override. Falls back to Param.SystemPrompt.",
}
}
// Outputs returns the public surface downstream ingestion
// consumers can wire into. Mirrors schema.ExtractorOutputs.
//
// chunks []map[string]any — input chunks, each augmented
// with field_name=<LLM result>.
// When the input chunks list is
// absent, the slice contains a
// single map with the same shape.
// output_format string — always "chunks". Parity with
// python set_output contract.
// _ERROR string — populated on a short-circuit
// error (matches python
// set_output("_ERROR", ...)).
func (c *ExtractorComponent) Outputs() map[string]string {
return map[string]string{
"chunks": "Extraction results — input chunks (or a single-element slice when no chunks were supplied), each enriched with field_name=<LLM response>.",
"output_format": "Always \"chunks\". Parity marker for downstream consumers.",
"_ERROR": "Optional short-circuit error message (reserved for the future TOC branch and other error paths).",
}
}
// extractorChatInvoker is the seam the Extractor uses to dispatch
// its chat call. The production implementation
// (einoExtractorChatInvoker below) mirrors
// internal/agent/component/llm.go:einoChatInvoker — same factory,
// same driver resolution, but kept self-contained so the
// ingestion package does NOT pull in agent/component for a
// one-method interface.
//
// Tests swap the package-level defaultExtractorChatInvoker to inject a
// canned-response stub (see SetExtractorChatInvoker and the test
// helpers in extractor_test.go). This is the testability seam the
// Phase 2.5 spec calls out as a hard rule.
type extractorChatInvoker interface {
Chat(ctx context.Context, req extractorChatRequest) (*extractorChatResponse, error)
}
// extractorChatRequest is the minimal surface the Extractor needs
// to dispatch a chat call. Driver is the provider key
// (e.g. "openai"); ModelName is the model id alone or composite
// "model@provider". APIKey / BaseURL are passed through so the
// driver can authenticate without re-reading the tenant config.
type extractorChatRequest struct {
Driver string
ModelName string
APIKey string
BaseURL string
Messages []eschema.Message
Temperature *float64
}
// extractorChatResponse holds the LLM's text answer. Token /
// stopped flags are not consumed by the Extractor yet, so they
// remain optional / 0-valued.
type extractorChatResponse struct {
Content string
}
// extractorChatInvokerMu guards defaultExtractorChatInvoker swaps.
var extractorChatInvokerMu sync.RWMutex
// defaultExtractorChatInvoker is the package-level seam. Production
// uses einoExtractorChatInvoker; tests inject a stub.
var defaultExtractorChatInvoker extractorChatInvoker = &einoExtractorChatInvoker{}
var extractorChatTargetResolverMu sync.RWMutex
// extractorChatTargetResolverOverride is a narrow test seam for
// integration tests that need to supply real credentials without
// teaching the production Extractor a tenant-credential lookup path.
// When set, resolveExtractorChatTarget consults it first.
var extractorChatTargetResolverOverride func(llmID string) (driver, modelName, apiKey, baseURL string, ok bool)
// SetExtractorChatInvoker swaps the package-level chat invoker
// for tests. Pass nil to restore the default. Concurrent-safe.
func SetExtractorChatInvoker(inv extractorChatInvoker) {
extractorChatInvokerMu.Lock()
defer extractorChatInvokerMu.Unlock()
defaultExtractorChatInvoker = inv
}
// SetExtractorChatTargetResolverOverride swaps the package-level
// llm_id target resolver override for tests. Pass nil to restore
// the default split-only resolver. Concurrent-safe.
func SetExtractorChatTargetResolverOverride(fn func(llmID string) (driver, modelName, apiKey, baseURL string, ok bool)) {
extractorChatTargetResolverMu.Lock()
defer extractorChatTargetResolverMu.Unlock()
extractorChatTargetResolverOverride = fn
}
func getExtractorChatTargetResolverOverride() func(llmID string) (driver, modelName, apiKey, baseURL string, ok bool) {
extractorChatTargetResolverMu.RLock()
defer extractorChatTargetResolverMu.RUnlock()
return extractorChatTargetResolverOverride
}
// getExtractorChatInvoker returns the current default invoker.
func getExtractorChatInvoker() extractorChatInvoker {
extractorChatInvokerMu.RLock()
defer extractorChatInvokerMu.RUnlock()
if defaultExtractorChatInvoker == nil {
return &einoExtractorChatInvoker{}
}
return defaultExtractorChatInvoker
}
// einoExtractorChatInvoker is the production seam. It dispatches
// through the entity/models factory (which knows 48 of 56
// providers) and returns the assistant text via
// models.EinoChatModel.Generate. An unknown provider falls
// through to NewDummyModel in the factory's default branch — we
// surface that as a typed "no driver for %q" wrap so callers can
// decide whether to retry, route around, or log.
type einoExtractorChatInvoker struct{}
// Chat implements extractorChatInvoker for the production path.
func (e *einoExtractorChatInvoker) Chat(ctx context.Context, req extractorChatRequest) (*extractorChatResponse, error) {
if req.ModelName == "" {
return nil, fmt.Errorf("extractor: chat: model_name is required")
}
driver := strings.ToLower(strings.TrimSpace(req.Driver))
modelName := req.ModelName
if driver == "" && modelName != "" {
if bare, provider, ok := splitExtractorLLIDPair(modelName); ok {
driver = provider
modelName = bare
}
}
if driver == "" {
fix(ingestion): align laws DSL with Python — heading fallback, colon-title, short-line filter, remove_toc, and image extension mapping (#17200) ## Summary This PR aligns the Go ingestion pipeline's **Laws** DSL template with the Python implementation by fixing heading-detection gaps, adds image-extension support, refactors the **Extractor** component's LLM resolution, hardens heading detection for CJK text, and makes the Extractor accept the Python DSL prompt key names (`sys_prompt`/`prompts`) alongside the Go names. ## Changes ### 1. Picture file-type detection (`internal/utility/file.go`) Adds explicit mapping for common image extensions (png, jpg, jpeg, gif, bmp, tiff, tif, webp, svg, ico, avif, heic, apng) → `FileTypeVISUAL`, with regression tests. ### 2. Laws DSL heading-detection alignment (`internal/ingestion/component/chunker/`) Four fixes to `resolveTitleLevels`: | Fix | What changed | Why | |-----|-------------|-----| | **DOCX `ck_type` fallback** | `ckType` field on `lineRecord`, propagated from `ChunkDoc.CKType` in `recordsFromStructured`. When `ck_type=="heading"`, assign `fallbackLevel`. | office_oxide extracts DOCX heading metadata, but the info was lost before reaching the heading detector. Word headings whose text doesn't match any regex (e.g. "Introduction") were treated as body. | | **`make_colon_as_title` promotion** | `isColonTitle()`: promotes lines ending with `:`/`:` that have sentence-ending punctuation before the colon and ≥32 runes between them. | Mirrors Python's `make_colon_as_title` in `rag/nlp/__init__.py`. Triple guard prevents false positives. | | **Short/numeric line filter** | Lines with ≤1 rune or purely numeric are pinned to body level. | Mirrors Python `tree_merge`'s filter of `sections` where `len(...) <= 1` or `re.match(r"[0-9]+$", ...)`. | | **PDF `remove_toc`** | `"remove_toc": true` added to the PDF parser setup in `ingestion_pipeline_laws.json`. | The Go PDF parser already supports TOC removal; the Book template already enables it. | ### 3. Extractor llm_id resolution (`internal/ingestion/component/extractor.go`) Refactored to handle both **bare tenant_model UUIDs** and **composite model@provider** strings via the shared `resolveModelConfig` (`dispatch_model.go`): - **`resolveExtractorChatConfig`** — UUID path calls `resolveModelConfigByID` directly (one DB hit); composite path goes through `resolveModelConfig`. Added `isBareTenantModelID` pre-check for clear errors when a UUID doesn't exist. - **`resolveExtractorChatTarget`** — propagates resolution errors instead of silently returning empty driver. - **`Chat()`** — removed `driver = "dummy"` fallback. Missing driver is now an explicit error. - **Removed dead code**: `splitExtractorLLID`, `findExtractorSoleActiveInstance`. ### 4. `InjectExtractorLLMID` — fallback when no user config (`internal/common/parser_config.go`) Injects the tenant's global default LLM into extractor components **only when their `llm_id` is empty**. Preserves user-selected UUID or model@provider values. Priority: user-configured llm_id > tenant global default > error (no silent dummy fallback). ### 5. `ResponseHeaderTimeout` increase (`internal/entity/models/base_model.go`) `ResponseHeaderTimeout` 60s → 120s in `NewDriverHTTPClient`. Reasoning models with large extraction prompts can take longer than 60s to produce the first response token. ### 6. CJK rune-aware heading detection (`internal/ingestion/component/chunker/title.go`) Two byte-vs-rune bugs that only manifest on CJK text: | Fix | What changed | Why | |-----|-------------|-----| | **`isColonTitle` byte offset** | `body[lastPunct+1:]` → `body[lastPunct+runeLen:]` via `utf8.DecodeRuneInString` | `strings.LastIndexAny` returns a byte index; `+1` skips only 1 byte, corrupting multi-byte CJK punctuation (e.g. `。` = 3 bytes) and inflating the rune count past the 32-rune threshold → false-positive heading promotion. | | **Short-line filter byte count** | `len(text) <= 1` → `utf8.RuneCountInString(text) <= 1` | Go `len` is UTF-8 bytes; a single CJK char (3 bytes) passed the filter, but Python's `len` returns 1 → mismatch. | ### 7. `extractor_tag.go` — log error when llm fails When `resolveExtractorChatTarget` returned an error, `runAutoTags` will log error. ### 8. Python DSL prompt-key compatibility (`internal/ingestion/component/extractor.go`) The Resume DSL template uses Python-side key names (`sys_prompt`, `prompts`). `NewExtractorComponent` now accepts them as fallbacks alongside the Go names: - `system_prompt` (Go) ← `sys_prompt` (Python) as fallback - `prompt` (Go string) ← `prompts` (Python array `[{"role","content"}]`, takes `[0].content`) as fallback Mirrors the alias pattern already in `internal/agent/component/llm.go`. `resolveInputs` accepts per-call `sys_prompt` override too. ## Remaining gaps vs Python | Gap | Scope | Impact | |-----|-------|--------| | **TOC removal for TXT/MD/HTML** | Python's `remove_contents_table` works on all text formats; Go's `remove_toc` is PDF-only. | Low — plain-text documents rarely contain structured TOCs. | | **Regex pattern details** | Minor differences in quantifiers, missing H5/H6 markdown patterns, missing 4-level numbering pattern. | Low — Go's variants are stricter; DOCX headings are covered by `ck_type` fallback. | ## Testing - `TestHierarchyTitleChunker_CKTypeHeadingFallback` — DOCX `ck_type` heading promotion - `TestHierarchyTitleChunker_ColonTitlePromotion` / `_ColonTitleShortLine_Negative` — colon-title promotion + guard - `TestIsColonTitle_CJKEdgeCase` / `TestIsColonTitle_ASCII_NoRegression` — CJK byte-offset fix + ASCII regression - `TestHierarchyTitleChunker_ColonTitlePromotion_CJK_EdgeCase` — CJK colon edge case through full pipeline - `TestHierarchyTitleChunker_ShortSingleCJKLineFilter` — single CJK char filtered to body - `TestHierarchyTitleChunker_ShortNumericLineFilter` — purely numeric lines filtered - `TestGetFileType_ImageExtensions` / `_ExistingFormats_NoRegression` — image extension mapping - `TestInjectExtractorLLMID_SkipWhenUUID` / `_SkipWhenComposite` / `_InjectWhenEmpty` — llm_id injection guard - `TestIsBareTenantModelID` — UUID detection - `TestResolveExtractorChatTarget_AtSplitFallback` / `_NoDriver` — @ split fallback without DB - `TestNewExtractorComponent_SysPromptAlias` / `_PromptsArray` / `_PromptsArray_PromptWins` / `_SystemPromptWinsOverSysPrompt` — Python key compatibility - `TestBuildDOCXJSONSections_List` / `_TextBox` / `_MixedWithList` — DOCX list/text_box parsing - Full ingestion test suite passes (chunker, pipeline, task, service, component packages)
2026-07-22 19:14:32 +08:00
return nil, fmt.Errorf("extractor: chat: no driver resolved for model %q", modelName)
}
fix(ingestion): align laws DSL with Python — heading fallback, colon-title, short-line filter, remove_toc, and image extension mapping (#17200) ## Summary This PR aligns the Go ingestion pipeline's **Laws** DSL template with the Python implementation by fixing heading-detection gaps, adds image-extension support, refactors the **Extractor** component's LLM resolution, hardens heading detection for CJK text, and makes the Extractor accept the Python DSL prompt key names (`sys_prompt`/`prompts`) alongside the Go names. ## Changes ### 1. Picture file-type detection (`internal/utility/file.go`) Adds explicit mapping for common image extensions (png, jpg, jpeg, gif, bmp, tiff, tif, webp, svg, ico, avif, heic, apng) → `FileTypeVISUAL`, with regression tests. ### 2. Laws DSL heading-detection alignment (`internal/ingestion/component/chunker/`) Four fixes to `resolveTitleLevels`: | Fix | What changed | Why | |-----|-------------|-----| | **DOCX `ck_type` fallback** | `ckType` field on `lineRecord`, propagated from `ChunkDoc.CKType` in `recordsFromStructured`. When `ck_type=="heading"`, assign `fallbackLevel`. | office_oxide extracts DOCX heading metadata, but the info was lost before reaching the heading detector. Word headings whose text doesn't match any regex (e.g. "Introduction") were treated as body. | | **`make_colon_as_title` promotion** | `isColonTitle()`: promotes lines ending with `:`/`:` that have sentence-ending punctuation before the colon and ≥32 runes between them. | Mirrors Python's `make_colon_as_title` in `rag/nlp/__init__.py`. Triple guard prevents false positives. | | **Short/numeric line filter** | Lines with ≤1 rune or purely numeric are pinned to body level. | Mirrors Python `tree_merge`'s filter of `sections` where `len(...) <= 1` or `re.match(r"[0-9]+$", ...)`. | | **PDF `remove_toc`** | `"remove_toc": true` added to the PDF parser setup in `ingestion_pipeline_laws.json`. | The Go PDF parser already supports TOC removal; the Book template already enables it. | ### 3. Extractor llm_id resolution (`internal/ingestion/component/extractor.go`) Refactored to handle both **bare tenant_model UUIDs** and **composite model@provider** strings via the shared `resolveModelConfig` (`dispatch_model.go`): - **`resolveExtractorChatConfig`** — UUID path calls `resolveModelConfigByID` directly (one DB hit); composite path goes through `resolveModelConfig`. Added `isBareTenantModelID` pre-check for clear errors when a UUID doesn't exist. - **`resolveExtractorChatTarget`** — propagates resolution errors instead of silently returning empty driver. - **`Chat()`** — removed `driver = "dummy"` fallback. Missing driver is now an explicit error. - **Removed dead code**: `splitExtractorLLID`, `findExtractorSoleActiveInstance`. ### 4. `InjectExtractorLLMID` — fallback when no user config (`internal/common/parser_config.go`) Injects the tenant's global default LLM into extractor components **only when their `llm_id` is empty**. Preserves user-selected UUID or model@provider values. Priority: user-configured llm_id > tenant global default > error (no silent dummy fallback). ### 5. `ResponseHeaderTimeout` increase (`internal/entity/models/base_model.go`) `ResponseHeaderTimeout` 60s → 120s in `NewDriverHTTPClient`. Reasoning models with large extraction prompts can take longer than 60s to produce the first response token. ### 6. CJK rune-aware heading detection (`internal/ingestion/component/chunker/title.go`) Two byte-vs-rune bugs that only manifest on CJK text: | Fix | What changed | Why | |-----|-------------|-----| | **`isColonTitle` byte offset** | `body[lastPunct+1:]` → `body[lastPunct+runeLen:]` via `utf8.DecodeRuneInString` | `strings.LastIndexAny` returns a byte index; `+1` skips only 1 byte, corrupting multi-byte CJK punctuation (e.g. `。` = 3 bytes) and inflating the rune count past the 32-rune threshold → false-positive heading promotion. | | **Short-line filter byte count** | `len(text) <= 1` → `utf8.RuneCountInString(text) <= 1` | Go `len` is UTF-8 bytes; a single CJK char (3 bytes) passed the filter, but Python's `len` returns 1 → mismatch. | ### 7. `extractor_tag.go` — log error when llm fails When `resolveExtractorChatTarget` returned an error, `runAutoTags` will log error. ### 8. Python DSL prompt-key compatibility (`internal/ingestion/component/extractor.go`) The Resume DSL template uses Python-side key names (`sys_prompt`, `prompts`). `NewExtractorComponent` now accepts them as fallbacks alongside the Go names: - `system_prompt` (Go) ← `sys_prompt` (Python) as fallback - `prompt` (Go string) ← `prompts` (Python array `[{"role","content"}]`, takes `[0].content`) as fallback Mirrors the alias pattern already in `internal/agent/component/llm.go`. `resolveInputs` accepts per-call `sys_prompt` override too. ## Remaining gaps vs Python | Gap | Scope | Impact | |-----|-------|--------| | **TOC removal for TXT/MD/HTML** | Python's `remove_contents_table` works on all text formats; Go's `remove_toc` is PDF-only. | Low — plain-text documents rarely contain structured TOCs. | | **Regex pattern details** | Minor differences in quantifiers, missing H5/H6 markdown patterns, missing 4-level numbering pattern. | Low — Go's variants are stricter; DOCX headings are covered by `ck_type` fallback. | ## Testing - `TestHierarchyTitleChunker_CKTypeHeadingFallback` — DOCX `ck_type` heading promotion - `TestHierarchyTitleChunker_ColonTitlePromotion` / `_ColonTitleShortLine_Negative` — colon-title promotion + guard - `TestIsColonTitle_CJKEdgeCase` / `TestIsColonTitle_ASCII_NoRegression` — CJK byte-offset fix + ASCII regression - `TestHierarchyTitleChunker_ColonTitlePromotion_CJK_EdgeCase` — CJK colon edge case through full pipeline - `TestHierarchyTitleChunker_ShortSingleCJKLineFilter` — single CJK char filtered to body - `TestHierarchyTitleChunker_ShortNumericLineFilter` — purely numeric lines filtered - `TestGetFileType_ImageExtensions` / `_ExistingFormats_NoRegression` — image extension mapping - `TestInjectExtractorLLMID_SkipWhenUUID` / `_SkipWhenComposite` / `_InjectWhenEmpty` — llm_id injection guard - `TestIsBareTenantModelID` — UUID detection - `TestResolveExtractorChatTarget_AtSplitFallback` / `_NoDriver` — @ split fallback without DB - `TestNewExtractorComponent_SysPromptAlias` / `_PromptsArray` / `_PromptsArray_PromptWins` / `_SystemPromptWinsOverSysPrompt` — Python key compatibility - `TestBuildDOCXJSONSections_List` / `_TextBox` / `_MixedWithList` — DOCX list/text_box parsing - Full ingestion test suite passes (chunker, pipeline, task, service, component packages)
2026-07-22 19:14:32 +08:00
common.Info(fmt.Sprintf("extractor: chat: driver=%s modelName=%s baseUrl=%s", driver, modelName, req.BaseURL))
fix: docx/email parsing, extractor LLM driver, and chunker alignment (#17144) ## Summary Three groups of changes across the Go ingestion pipeline: ### 1. DOCX parsing improvements - **docx_parser.go**: Enhanced DOCX parsing with better structure extraction and media handling - **docx_parser_cgo_test.go**, **docx_parser_test.go**: Companion tests - **office_parsers_no_cgo.go**: Stub sync for non-CGO builds ### 2. Email (.eml) parsing: base64 Content-Transfer-Encoding decoding - **email_parser.go** (`decodeCTE`): Added Content-Transfer-Encoding decoding for base64 and quoted-printable. Go's `mime/multipart.Reader` does not decode Content-Transfer-Encoding automatically, so attachments with `Content-Transfer-Encoding: base64` remained base64-encoded in the output. The new `decodeCTE` helper is called after reading each multipart part's raw bytes in `readMailBody`, mirroring Python's `part.get_payload(decode=True)`. - **email_parser_test.go**: Two new tests — simple base64 attachment and nested multipart/alternative with base64 attachment. ### 3. Extractor LLM driver fix + ModelDriver consolidation - **extractor.go**: Fixed a bug where the Extractor component used `ModelFactory.CreateModelDriver()`, which creates bare model instances without API keys or provider configuration. Switched to `models.GetPreconfiguredDriver()` which resolves the actual pre-configured driver from `ProviderManager`, matching the codepath used by `llm.go`. This fixes auto keyword/question extraction in DSL pipelines that require LLM calls. - **get_driver.go** (new): Extracted shared `GetPreconfiguredDriver()` from `llm.go:newChatModelDriver()` so both `llm.go` and `extractor.go` use the same codepath. - **get_driver_test.go** (new): Tests for the shared driver resolution. - **llm.go**: Replaced inline driver resolution with `models.GetPreconfiguredDriver()`. ### 4. Chunker fixes and observability - **group.go** (`extractLineRecords`): Fixed to also read `markdown` and `html` payload keys — previously it only read `text`/`content`, causing GroupTitleChunker to silently return empty results for markdown-format parser output. - **common.go** (`compileDelimPattern`): Aligned with Python's `_compile_delimiter_pattern` — only backtick-wrapped delimiters produce an active regex pattern; plain delimiters are not compiled into the split regex. - **token.go** (`applyChildrenDelim`): Set `DocType` and `CKType` to `"text"` on created ChunkDocs so the token-size merge path correctly identifies and merges text segments. - **parser.go**, **extractor.go**, **tokenizer.go**, **group.go**, **hierarchy.go**: Added debug-level logging for pipeline diagnostics. - **parser_dispatch_test.go**, **group_test.go**: New tests. ## Verification - All Go tests pass: `bash build.sh --test ./internal/parser/parser/...` and `bash build.sh --test ./internal/ingestion/component/...` - Build succeeds: `bash build.sh --go`
2026-07-21 13:51:17 +08:00
d, err := models.GetPreconfiguredDriver(driver, req.BaseURL)
if err != nil {
return nil, fmt.Errorf("extractor: resolve driver %q: %w", driver, err)
}
apiKey := req.APIKey
cfg := &models.APIConfig{ApiKey: &apiKey}
cm := models.NewChatModel(d, &modelName, cfg)
var chatCfg *models.ChatConfig
if req.Temperature != nil {
temp := *req.Temperature
chatCfg = &models.ChatConfig{Temperature: &temp}
}
wrapper := models.NewEinoChatModel(cm, chatCfg)
// Honour ctx cancel up front so the caller's WithTimeout(...)
// is observed even when the driver layer doesn't take a ctx.
fix(ingestion): align laws DSL with Python — heading fallback, colon-title, short-line filter, remove_toc, and image extension mapping (#17200) ## Summary This PR aligns the Go ingestion pipeline's **Laws** DSL template with the Python implementation by fixing heading-detection gaps, adds image-extension support, refactors the **Extractor** component's LLM resolution, hardens heading detection for CJK text, and makes the Extractor accept the Python DSL prompt key names (`sys_prompt`/`prompts`) alongside the Go names. ## Changes ### 1. Picture file-type detection (`internal/utility/file.go`) Adds explicit mapping for common image extensions (png, jpg, jpeg, gif, bmp, tiff, tif, webp, svg, ico, avif, heic, apng) → `FileTypeVISUAL`, with regression tests. ### 2. Laws DSL heading-detection alignment (`internal/ingestion/component/chunker/`) Four fixes to `resolveTitleLevels`: | Fix | What changed | Why | |-----|-------------|-----| | **DOCX `ck_type` fallback** | `ckType` field on `lineRecord`, propagated from `ChunkDoc.CKType` in `recordsFromStructured`. When `ck_type=="heading"`, assign `fallbackLevel`. | office_oxide extracts DOCX heading metadata, but the info was lost before reaching the heading detector. Word headings whose text doesn't match any regex (e.g. "Introduction") were treated as body. | | **`make_colon_as_title` promotion** | `isColonTitle()`: promotes lines ending with `:`/`:` that have sentence-ending punctuation before the colon and ≥32 runes between them. | Mirrors Python's `make_colon_as_title` in `rag/nlp/__init__.py`. Triple guard prevents false positives. | | **Short/numeric line filter** | Lines with ≤1 rune or purely numeric are pinned to body level. | Mirrors Python `tree_merge`'s filter of `sections` where `len(...) <= 1` or `re.match(r"[0-9]+$", ...)`. | | **PDF `remove_toc`** | `"remove_toc": true` added to the PDF parser setup in `ingestion_pipeline_laws.json`. | The Go PDF parser already supports TOC removal; the Book template already enables it. | ### 3. Extractor llm_id resolution (`internal/ingestion/component/extractor.go`) Refactored to handle both **bare tenant_model UUIDs** and **composite model@provider** strings via the shared `resolveModelConfig` (`dispatch_model.go`): - **`resolveExtractorChatConfig`** — UUID path calls `resolveModelConfigByID` directly (one DB hit); composite path goes through `resolveModelConfig`. Added `isBareTenantModelID` pre-check for clear errors when a UUID doesn't exist. - **`resolveExtractorChatTarget`** — propagates resolution errors instead of silently returning empty driver. - **`Chat()`** — removed `driver = "dummy"` fallback. Missing driver is now an explicit error. - **Removed dead code**: `splitExtractorLLID`, `findExtractorSoleActiveInstance`. ### 4. `InjectExtractorLLMID` — fallback when no user config (`internal/common/parser_config.go`) Injects the tenant's global default LLM into extractor components **only when their `llm_id` is empty**. Preserves user-selected UUID or model@provider values. Priority: user-configured llm_id > tenant global default > error (no silent dummy fallback). ### 5. `ResponseHeaderTimeout` increase (`internal/entity/models/base_model.go`) `ResponseHeaderTimeout` 60s → 120s in `NewDriverHTTPClient`. Reasoning models with large extraction prompts can take longer than 60s to produce the first response token. ### 6. CJK rune-aware heading detection (`internal/ingestion/component/chunker/title.go`) Two byte-vs-rune bugs that only manifest on CJK text: | Fix | What changed | Why | |-----|-------------|-----| | **`isColonTitle` byte offset** | `body[lastPunct+1:]` → `body[lastPunct+runeLen:]` via `utf8.DecodeRuneInString` | `strings.LastIndexAny` returns a byte index; `+1` skips only 1 byte, corrupting multi-byte CJK punctuation (e.g. `。` = 3 bytes) and inflating the rune count past the 32-rune threshold → false-positive heading promotion. | | **Short-line filter byte count** | `len(text) <= 1` → `utf8.RuneCountInString(text) <= 1` | Go `len` is UTF-8 bytes; a single CJK char (3 bytes) passed the filter, but Python's `len` returns 1 → mismatch. | ### 7. `extractor_tag.go` — log error when llm fails When `resolveExtractorChatTarget` returned an error, `runAutoTags` will log error. ### 8. Python DSL prompt-key compatibility (`internal/ingestion/component/extractor.go`) The Resume DSL template uses Python-side key names (`sys_prompt`, `prompts`). `NewExtractorComponent` now accepts them as fallbacks alongside the Go names: - `system_prompt` (Go) ← `sys_prompt` (Python) as fallback - `prompt` (Go string) ← `prompts` (Python array `[{"role","content"}]`, takes `[0].content`) as fallback Mirrors the alias pattern already in `internal/agent/component/llm.go`. `resolveInputs` accepts per-call `sys_prompt` override too. ## Remaining gaps vs Python | Gap | Scope | Impact | |-----|-------|--------| | **TOC removal for TXT/MD/HTML** | Python's `remove_contents_table` works on all text formats; Go's `remove_toc` is PDF-only. | Low — plain-text documents rarely contain structured TOCs. | | **Regex pattern details** | Minor differences in quantifiers, missing H5/H6 markdown patterns, missing 4-level numbering pattern. | Low — Go's variants are stricter; DOCX headings are covered by `ck_type` fallback. | ## Testing - `TestHierarchyTitleChunker_CKTypeHeadingFallback` — DOCX `ck_type` heading promotion - `TestHierarchyTitleChunker_ColonTitlePromotion` / `_ColonTitleShortLine_Negative` — colon-title promotion + guard - `TestIsColonTitle_CJKEdgeCase` / `TestIsColonTitle_ASCII_NoRegression` — CJK byte-offset fix + ASCII regression - `TestHierarchyTitleChunker_ColonTitlePromotion_CJK_EdgeCase` — CJK colon edge case through full pipeline - `TestHierarchyTitleChunker_ShortSingleCJKLineFilter` — single CJK char filtered to body - `TestHierarchyTitleChunker_ShortNumericLineFilter` — purely numeric lines filtered - `TestGetFileType_ImageExtensions` / `_ExistingFormats_NoRegression` — image extension mapping - `TestInjectExtractorLLMID_SkipWhenUUID` / `_SkipWhenComposite` / `_InjectWhenEmpty` — llm_id injection guard - `TestIsBareTenantModelID` — UUID detection - `TestResolveExtractorChatTarget_AtSplitFallback` / `_NoDriver` — @ split fallback without DB - `TestNewExtractorComponent_SysPromptAlias` / `_PromptsArray` / `_PromptsArray_PromptWins` / `_SystemPromptWinsOverSysPrompt` — Python key compatibility - `TestBuildDOCXJSONSections_List` / `_TextBox` / `_MixedWithList` — DOCX list/text_box parsing - Full ingestion test suite passes (chunker, pipeline, task, service, component packages)
2026-07-22 19:14:32 +08:00
common.Info(fmt.Sprintf("try to chat with message: %v", req.Messages))
if err := ctx.Err(); err != nil {
return nil, err
}
out, err := wrapper.Generate(ctx, toExtractorEinoMessages(req.Messages))
if err != nil {
fix(ingestion): align laws DSL with Python — heading fallback, colon-title, short-line filter, remove_toc, and image extension mapping (#17200) ## Summary This PR aligns the Go ingestion pipeline's **Laws** DSL template with the Python implementation by fixing heading-detection gaps, adds image-extension support, refactors the **Extractor** component's LLM resolution, hardens heading detection for CJK text, and makes the Extractor accept the Python DSL prompt key names (`sys_prompt`/`prompts`) alongside the Go names. ## Changes ### 1. Picture file-type detection (`internal/utility/file.go`) Adds explicit mapping for common image extensions (png, jpg, jpeg, gif, bmp, tiff, tif, webp, svg, ico, avif, heic, apng) → `FileTypeVISUAL`, with regression tests. ### 2. Laws DSL heading-detection alignment (`internal/ingestion/component/chunker/`) Four fixes to `resolveTitleLevels`: | Fix | What changed | Why | |-----|-------------|-----| | **DOCX `ck_type` fallback** | `ckType` field on `lineRecord`, propagated from `ChunkDoc.CKType` in `recordsFromStructured`. When `ck_type=="heading"`, assign `fallbackLevel`. | office_oxide extracts DOCX heading metadata, but the info was lost before reaching the heading detector. Word headings whose text doesn't match any regex (e.g. "Introduction") were treated as body. | | **`make_colon_as_title` promotion** | `isColonTitle()`: promotes lines ending with `:`/`:` that have sentence-ending punctuation before the colon and ≥32 runes between them. | Mirrors Python's `make_colon_as_title` in `rag/nlp/__init__.py`. Triple guard prevents false positives. | | **Short/numeric line filter** | Lines with ≤1 rune or purely numeric are pinned to body level. | Mirrors Python `tree_merge`'s filter of `sections` where `len(...) <= 1` or `re.match(r"[0-9]+$", ...)`. | | **PDF `remove_toc`** | `"remove_toc": true` added to the PDF parser setup in `ingestion_pipeline_laws.json`. | The Go PDF parser already supports TOC removal; the Book template already enables it. | ### 3. Extractor llm_id resolution (`internal/ingestion/component/extractor.go`) Refactored to handle both **bare tenant_model UUIDs** and **composite model@provider** strings via the shared `resolveModelConfig` (`dispatch_model.go`): - **`resolveExtractorChatConfig`** — UUID path calls `resolveModelConfigByID` directly (one DB hit); composite path goes through `resolveModelConfig`. Added `isBareTenantModelID` pre-check for clear errors when a UUID doesn't exist. - **`resolveExtractorChatTarget`** — propagates resolution errors instead of silently returning empty driver. - **`Chat()`** — removed `driver = "dummy"` fallback. Missing driver is now an explicit error. - **Removed dead code**: `splitExtractorLLID`, `findExtractorSoleActiveInstance`. ### 4. `InjectExtractorLLMID` — fallback when no user config (`internal/common/parser_config.go`) Injects the tenant's global default LLM into extractor components **only when their `llm_id` is empty**. Preserves user-selected UUID or model@provider values. Priority: user-configured llm_id > tenant global default > error (no silent dummy fallback). ### 5. `ResponseHeaderTimeout` increase (`internal/entity/models/base_model.go`) `ResponseHeaderTimeout` 60s → 120s in `NewDriverHTTPClient`. Reasoning models with large extraction prompts can take longer than 60s to produce the first response token. ### 6. CJK rune-aware heading detection (`internal/ingestion/component/chunker/title.go`) Two byte-vs-rune bugs that only manifest on CJK text: | Fix | What changed | Why | |-----|-------------|-----| | **`isColonTitle` byte offset** | `body[lastPunct+1:]` → `body[lastPunct+runeLen:]` via `utf8.DecodeRuneInString` | `strings.LastIndexAny` returns a byte index; `+1` skips only 1 byte, corrupting multi-byte CJK punctuation (e.g. `。` = 3 bytes) and inflating the rune count past the 32-rune threshold → false-positive heading promotion. | | **Short-line filter byte count** | `len(text) <= 1` → `utf8.RuneCountInString(text) <= 1` | Go `len` is UTF-8 bytes; a single CJK char (3 bytes) passed the filter, but Python's `len` returns 1 → mismatch. | ### 7. `extractor_tag.go` — log error when llm fails When `resolveExtractorChatTarget` returned an error, `runAutoTags` will log error. ### 8. Python DSL prompt-key compatibility (`internal/ingestion/component/extractor.go`) The Resume DSL template uses Python-side key names (`sys_prompt`, `prompts`). `NewExtractorComponent` now accepts them as fallbacks alongside the Go names: - `system_prompt` (Go) ← `sys_prompt` (Python) as fallback - `prompt` (Go string) ← `prompts` (Python array `[{"role","content"}]`, takes `[0].content`) as fallback Mirrors the alias pattern already in `internal/agent/component/llm.go`. `resolveInputs` accepts per-call `sys_prompt` override too. ## Remaining gaps vs Python | Gap | Scope | Impact | |-----|-------|--------| | **TOC removal for TXT/MD/HTML** | Python's `remove_contents_table` works on all text formats; Go's `remove_toc` is PDF-only. | Low — plain-text documents rarely contain structured TOCs. | | **Regex pattern details** | Minor differences in quantifiers, missing H5/H6 markdown patterns, missing 4-level numbering pattern. | Low — Go's variants are stricter; DOCX headings are covered by `ck_type` fallback. | ## Testing - `TestHierarchyTitleChunker_CKTypeHeadingFallback` — DOCX `ck_type` heading promotion - `TestHierarchyTitleChunker_ColonTitlePromotion` / `_ColonTitleShortLine_Negative` — colon-title promotion + guard - `TestIsColonTitle_CJKEdgeCase` / `TestIsColonTitle_ASCII_NoRegression` — CJK byte-offset fix + ASCII regression - `TestHierarchyTitleChunker_ColonTitlePromotion_CJK_EdgeCase` — CJK colon edge case through full pipeline - `TestHierarchyTitleChunker_ShortSingleCJKLineFilter` — single CJK char filtered to body - `TestHierarchyTitleChunker_ShortNumericLineFilter` — purely numeric lines filtered - `TestGetFileType_ImageExtensions` / `_ExistingFormats_NoRegression` — image extension mapping - `TestInjectExtractorLLMID_SkipWhenUUID` / `_SkipWhenComposite` / `_InjectWhenEmpty` — llm_id injection guard - `TestIsBareTenantModelID` — UUID detection - `TestResolveExtractorChatTarget_AtSplitFallback` / `_NoDriver` — @ split fallback without DB - `TestNewExtractorComponent_SysPromptAlias` / `_PromptsArray` / `_PromptsArray_PromptWins` / `_SystemPromptWinsOverSysPrompt` — Python key compatibility - `TestBuildDOCXJSONSections_List` / `_TextBox` / `_MixedWithList` — DOCX list/text_box parsing - Full ingestion test suite passes (chunker, pipeline, task, service, component packages)
2026-07-22 19:14:32 +08:00
common.Error(fmt.Sprintf("error when chat with message: %v", req.Messages), err)
return nil, err
}
return &extractorChatResponse{Content: out.Content}, nil
}
// splitExtractorLLIDPair parses a composite llm_id "model@provider"
// mirroring agent/component/llm_credentials.go:parseLLMIDParts
// (the canonical composite form throughout the codebase). Returns
// ok=false when no "@" is present or the id is malformed.
//
// "gpt-4o-mini@openai" -> ("gpt-4o-mini", "openai", true)
// "gpt-4o-mini" -> ("gpt-4o-mini", "", false)
//
// Kept local so the ingestion package doesn't import
// agent/component.
func splitExtractorLLIDPair(s string) (modelName, provider string, ok bool) {
parts := strings.Split(strings.TrimSpace(s), "@")
switch len(parts) {
case 2:
return parts[0], parts[1], true
default:
return s, "", false
}
}
// toExtractorEinoMessages converts eschema.Message → *eschema.Message
// for the eino bridge. The user / system / assistant roles pass
// through; multi-modal content is intentionally not propagated —
// extraction prompts are text-only today.
func toExtractorEinoMessages(msgs []eschema.Message) []*eschema.Message {
out := make([]*eschema.Message, 0, len(msgs))
for i := range msgs {
m := msgs[i]
role := m.Role
if role == "" {
role = eschema.User
}
out = append(out, &eschema.Message{
Role: role,
Content: m.Content,
})
}
return out
}
// extractorInputs is the post-Validation view of the upstream
// input map. Computed once at the top of Invoke so the rest of
// the function reads as straight-line code.
type extractorInputs struct {
fieldName string
llmID string
systemPrompt string
prompt string
lang string
chunks []map[string]any
fix(ingestion): improve Extractor/LLM robustness and Python->Go parity (#17470) ## Summary This PR hardens the Go ingestion **Extractor** and **LLM retry** paths and closes several Python->Go parity gaps in the keyword/question/tag extraction flow. - **Generic retry utility** (`internal/common/retry.go`): `RetryWithBackoff` with exponential backoff (default 3 retries, 2s initial delay, capped at 1m), context-aware sleep, and a `maxRetries<=0` fast path. Covered by `internal/common/retry_test.go`. - **LLM retry reuse**: `agent/component/llm_retry.go` now delegates to `common.RetryWithBackoff` instead of an inline loop (behavior preserved: ctx cancellation short-circuits the backoff). - **Extractor LLM calls** (`extractor.go`): - `call()` now retries transient LLM failures via `RetryWithBackoff` (retry exhaustion fails the chunk instead of silently skipping). - Sets `temperature = 0.2`, matching Python `generator.py:230,245`. - Runs keyword and question extraction **concurrently** per chunk when both are enabled (`task_executor.py:444-448`), with mutex-guarded map writes to avoid data races. - Substitutes `{field_name}` placeholders (including `{chunks}` -> chunk text) in `prompt`/`system_prompt` before the call, mirroring Python `string_format` (`extractor.py:102-103`); unmatched placeholders are left as-is. - Falls back to the **tenant default chat model** when `llm_id` is empty (`task_executor.py:573-574`). - Strips `` **greedily** (`strings.LastIndex`) in `cleanExtractionResult`. - **Auto-tagging** (`extractor_tag.go`): drops the `in.llmID != ""` guards so an empty `llm_id` no longer skips tagging (uses the tenant default model), and strips `` greedily in `parseTaggerResponse`. - **Docs**: fixes a misleading `PresentationChunker` docstring that claimed per-slide `image`/`position` output (the PPTX path emits none — unlike PDF), and removes a stale `docs/migration_python_go_diff.md` reference in `media_dispatch.go`. ## Test plan - `bash build.sh --test ./internal/common/...` — passes (new retry utility + tests). - `bash build.sh --test ./internal/ingestion/component/...` — passes (extractor/chunker/schema). - `gofmt` and lefthook pre-commit checks pass. Note: the personal `docs/migration_python_go_diff.md` working notebook in the tree is intentionally **not** part of this PR. --------- Co-authored-by: CodeBuddy <noreply@codebuddy.ai>
2026-07-28 19:22:18 +08:00
// temperature overrides the LLM temperature for this call. A
// nil value leaves the request's Temperature unset so the model
// (or the chat-model default) decides, matching Python's generic
// Extractor path. The keyword/question helpers set it to
// extractorTemperature (0.2) to mirror generator.py.
temperature *float64
}
// resolveInputs overlays per-call inputs on top of the
// component's static Param. Missing keys fall back to the
// Param-level values; per-call values win on conflict (so a
// canvas can override LLM_ID at runtime). The python
// Extractor reads inputs directly from get_input_elements(); the
// Go port normalizes to extractorInputs once at the top so the
// rest of Invoke reads straight-line.
func (c *ExtractorComponent) resolveInputs(inputs map[string]any) extractorInputs {
out := extractorInputs{
fieldName: c.Param.FieldName,
llmID: c.Param.LLMID,
systemPrompt: c.Param.SystemPrompt,
prompt: c.Param.Prompt,
}
if inputs == nil {
return out
}
if v, ok := inputs["llm_id"].(string); ok && v != "" {
out.llmID = v
}
if v, ok := inputs["prompt"].(string); ok && v != "" {
out.prompt = v
}
if v, ok := inputs["system_prompt"].(string); ok && v != "" {
out.systemPrompt = v
fix(ingestion): align laws DSL with Python — heading fallback, colon-title, short-line filter, remove_toc, and image extension mapping (#17200) ## Summary This PR aligns the Go ingestion pipeline's **Laws** DSL template with the Python implementation by fixing heading-detection gaps, adds image-extension support, refactors the **Extractor** component's LLM resolution, hardens heading detection for CJK text, and makes the Extractor accept the Python DSL prompt key names (`sys_prompt`/`prompts`) alongside the Go names. ## Changes ### 1. Picture file-type detection (`internal/utility/file.go`) Adds explicit mapping for common image extensions (png, jpg, jpeg, gif, bmp, tiff, tif, webp, svg, ico, avif, heic, apng) → `FileTypeVISUAL`, with regression tests. ### 2. Laws DSL heading-detection alignment (`internal/ingestion/component/chunker/`) Four fixes to `resolveTitleLevels`: | Fix | What changed | Why | |-----|-------------|-----| | **DOCX `ck_type` fallback** | `ckType` field on `lineRecord`, propagated from `ChunkDoc.CKType` in `recordsFromStructured`. When `ck_type=="heading"`, assign `fallbackLevel`. | office_oxide extracts DOCX heading metadata, but the info was lost before reaching the heading detector. Word headings whose text doesn't match any regex (e.g. "Introduction") were treated as body. | | **`make_colon_as_title` promotion** | `isColonTitle()`: promotes lines ending with `:`/`:` that have sentence-ending punctuation before the colon and ≥32 runes between them. | Mirrors Python's `make_colon_as_title` in `rag/nlp/__init__.py`. Triple guard prevents false positives. | | **Short/numeric line filter** | Lines with ≤1 rune or purely numeric are pinned to body level. | Mirrors Python `tree_merge`'s filter of `sections` where `len(...) <= 1` or `re.match(r"[0-9]+$", ...)`. | | **PDF `remove_toc`** | `"remove_toc": true` added to the PDF parser setup in `ingestion_pipeline_laws.json`. | The Go PDF parser already supports TOC removal; the Book template already enables it. | ### 3. Extractor llm_id resolution (`internal/ingestion/component/extractor.go`) Refactored to handle both **bare tenant_model UUIDs** and **composite model@provider** strings via the shared `resolveModelConfig` (`dispatch_model.go`): - **`resolveExtractorChatConfig`** — UUID path calls `resolveModelConfigByID` directly (one DB hit); composite path goes through `resolveModelConfig`. Added `isBareTenantModelID` pre-check for clear errors when a UUID doesn't exist. - **`resolveExtractorChatTarget`** — propagates resolution errors instead of silently returning empty driver. - **`Chat()`** — removed `driver = "dummy"` fallback. Missing driver is now an explicit error. - **Removed dead code**: `splitExtractorLLID`, `findExtractorSoleActiveInstance`. ### 4. `InjectExtractorLLMID` — fallback when no user config (`internal/common/parser_config.go`) Injects the tenant's global default LLM into extractor components **only when their `llm_id` is empty**. Preserves user-selected UUID or model@provider values. Priority: user-configured llm_id > tenant global default > error (no silent dummy fallback). ### 5. `ResponseHeaderTimeout` increase (`internal/entity/models/base_model.go`) `ResponseHeaderTimeout` 60s → 120s in `NewDriverHTTPClient`. Reasoning models with large extraction prompts can take longer than 60s to produce the first response token. ### 6. CJK rune-aware heading detection (`internal/ingestion/component/chunker/title.go`) Two byte-vs-rune bugs that only manifest on CJK text: | Fix | What changed | Why | |-----|-------------|-----| | **`isColonTitle` byte offset** | `body[lastPunct+1:]` → `body[lastPunct+runeLen:]` via `utf8.DecodeRuneInString` | `strings.LastIndexAny` returns a byte index; `+1` skips only 1 byte, corrupting multi-byte CJK punctuation (e.g. `。` = 3 bytes) and inflating the rune count past the 32-rune threshold → false-positive heading promotion. | | **Short-line filter byte count** | `len(text) <= 1` → `utf8.RuneCountInString(text) <= 1` | Go `len` is UTF-8 bytes; a single CJK char (3 bytes) passed the filter, but Python's `len` returns 1 → mismatch. | ### 7. `extractor_tag.go` — log error when llm fails When `resolveExtractorChatTarget` returned an error, `runAutoTags` will log error. ### 8. Python DSL prompt-key compatibility (`internal/ingestion/component/extractor.go`) The Resume DSL template uses Python-side key names (`sys_prompt`, `prompts`). `NewExtractorComponent` now accepts them as fallbacks alongside the Go names: - `system_prompt` (Go) ← `sys_prompt` (Python) as fallback - `prompt` (Go string) ← `prompts` (Python array `[{"role","content"}]`, takes `[0].content`) as fallback Mirrors the alias pattern already in `internal/agent/component/llm.go`. `resolveInputs` accepts per-call `sys_prompt` override too. ## Remaining gaps vs Python | Gap | Scope | Impact | |-----|-------|--------| | **TOC removal for TXT/MD/HTML** | Python's `remove_contents_table` works on all text formats; Go's `remove_toc` is PDF-only. | Low — plain-text documents rarely contain structured TOCs. | | **Regex pattern details** | Minor differences in quantifiers, missing H5/H6 markdown patterns, missing 4-level numbering pattern. | Low — Go's variants are stricter; DOCX headings are covered by `ck_type` fallback. | ## Testing - `TestHierarchyTitleChunker_CKTypeHeadingFallback` — DOCX `ck_type` heading promotion - `TestHierarchyTitleChunker_ColonTitlePromotion` / `_ColonTitleShortLine_Negative` — colon-title promotion + guard - `TestIsColonTitle_CJKEdgeCase` / `TestIsColonTitle_ASCII_NoRegression` — CJK byte-offset fix + ASCII regression - `TestHierarchyTitleChunker_ColonTitlePromotion_CJK_EdgeCase` — CJK colon edge case through full pipeline - `TestHierarchyTitleChunker_ShortSingleCJKLineFilter` — single CJK char filtered to body - `TestHierarchyTitleChunker_ShortNumericLineFilter` — purely numeric lines filtered - `TestGetFileType_ImageExtensions` / `_ExistingFormats_NoRegression` — image extension mapping - `TestInjectExtractorLLMID_SkipWhenUUID` / `_SkipWhenComposite` / `_InjectWhenEmpty` — llm_id injection guard - `TestIsBareTenantModelID` — UUID detection - `TestResolveExtractorChatTarget_AtSplitFallback` / `_NoDriver` — @ split fallback without DB - `TestNewExtractorComponent_SysPromptAlias` / `_PromptsArray` / `_PromptsArray_PromptWins` / `_SystemPromptWinsOverSysPrompt` — Python key compatibility - `TestBuildDOCXJSONSections_List` / `_TextBox` / `_MixedWithList` — DOCX list/text_box parsing - Full ingestion test suite passes (chunker, pipeline, task, service, component packages)
2026-07-22 19:14:32 +08:00
} else if v, ok := inputs["sys_prompt"].(string); ok && v != "" {
out.systemPrompt = v
}
if v, ok := inputs["lang"].(string); ok && v != "" {
out.lang = v
}
for _, key := range extractorChunkInputOrder(inputs) {
if chunks, ok := extractorChunkList(inputs[key]); ok {
out.chunks = chunks
break
}
}
return out
}
func extractorChunkInputOrder(inputs map[string]any) []string {
order := make([]string, 0, len(inputs))
for _, preferred := range []string{"chunks", "json"} {
if _, ok := inputs[preferred]; ok {
order = append(order, preferred)
}
}
var extra []string
for key := range inputs {
if key == "chunks" || key == "json" {
continue
}
extra = append(extra, key)
}
sort.Strings(extra)
order = append(order, extra...)
return order
}
func extractorChunkList(v any) ([]map[string]any, bool) {
switch list := v.(type) {
case []map[string]any:
return list, true
case []any:
out := make([]map[string]any, 0, len(list))
for _, item := range list {
m, ok := item.(map[string]any)
if !ok {
continue
}
out = append(out, m)
}
return out, true
default:
return nil, false
}
}
// Invoke performs LLM-based extraction. Inputs:
//
// chunks (optional, []map[string]any) — upstream chunks; each must
// carry a string "text".
// prompt (optional, string) — overrides Param.Prompt.
// system_prompt (optional, string) — overrides Param.SystemPrompt.
// llm_id (optional, string) — overrides Param.LLMID.
//
// Outputs:
//
// chunks ([]map[string]any) — input chunks augmented with
// field_name=<LLM result>. When
// the input list is empty, the
// slice contains a single map.
// output_format (string) — always "chunks".
// _ERROR (string, reserved) — populated when the component
// short-circuits with an error.
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
// _created_time, _elapsed_time — stamped by the canvas framework
// (realComponentBody), not here.
func (c *ExtractorComponent) Invoke(ctx context.Context, db *gorm.DB, inputs map[string]any) (map[string]any, error) {
if err := c.Param.Validate(); err != nil {
return nil, fmt.Errorf("extractor: %w", err)
}
in := c.resolveInputs(inputs)
fix: docx/email parsing, extractor LLM driver, and chunker alignment (#17144) ## Summary Three groups of changes across the Go ingestion pipeline: ### 1. DOCX parsing improvements - **docx_parser.go**: Enhanced DOCX parsing with better structure extraction and media handling - **docx_parser_cgo_test.go**, **docx_parser_test.go**: Companion tests - **office_parsers_no_cgo.go**: Stub sync for non-CGO builds ### 2. Email (.eml) parsing: base64 Content-Transfer-Encoding decoding - **email_parser.go** (`decodeCTE`): Added Content-Transfer-Encoding decoding for base64 and quoted-printable. Go's `mime/multipart.Reader` does not decode Content-Transfer-Encoding automatically, so attachments with `Content-Transfer-Encoding: base64` remained base64-encoded in the output. The new `decodeCTE` helper is called after reading each multipart part's raw bytes in `readMailBody`, mirroring Python's `part.get_payload(decode=True)`. - **email_parser_test.go**: Two new tests — simple base64 attachment and nested multipart/alternative with base64 attachment. ### 3. Extractor LLM driver fix + ModelDriver consolidation - **extractor.go**: Fixed a bug where the Extractor component used `ModelFactory.CreateModelDriver()`, which creates bare model instances without API keys or provider configuration. Switched to `models.GetPreconfiguredDriver()` which resolves the actual pre-configured driver from `ProviderManager`, matching the codepath used by `llm.go`. This fixes auto keyword/question extraction in DSL pipelines that require LLM calls. - **get_driver.go** (new): Extracted shared `GetPreconfiguredDriver()` from `llm.go:newChatModelDriver()` so both `llm.go` and `extractor.go` use the same codepath. - **get_driver_test.go** (new): Tests for the shared driver resolution. - **llm.go**: Replaced inline driver resolution with `models.GetPreconfiguredDriver()`. ### 4. Chunker fixes and observability - **group.go** (`extractLineRecords`): Fixed to also read `markdown` and `html` payload keys — previously it only read `text`/`content`, causing GroupTitleChunker to silently return empty results for markdown-format parser output. - **common.go** (`compileDelimPattern`): Aligned with Python's `_compile_delimiter_pattern` — only backtick-wrapped delimiters produce an active regex pattern; plain delimiters are not compiled into the split regex. - **token.go** (`applyChildrenDelim`): Set `DocType` and `CKType` to `"text"` on created ChunkDocs so the token-size merge path correctly identifies and merges text segments. - **parser.go**, **extractor.go**, **tokenizer.go**, **group.go**, **hierarchy.go**: Added debug-level logging for pipeline diagnostics. - **parser_dispatch_test.go**, **group_test.go**: New tests. ## Verification - All Go tests pass: `bash build.sh --test ./internal/parser/parser/...` and `bash build.sh --test ./internal/ingestion/component/...` - Build succeeds: `bash build.sh --go`
2026-07-21 13:51:17 +08:00
common.Debug("extractor stage",
zap.String("component", "Extractor"),
zap.Int("input_chunks", len(in.chunks)),
)
if in.fieldName == "toc" {
return nil, fmt.Errorf("extractor: field_name %q requires the TOC prompt generator which is not yet ported to Go", "toc")
}
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
if err := runtime.WithTimeout(ctx, extractorTimeout, func(timeoutCtx context.Context) error {
// Tag phase: run when auto_tags > 0 and we have chunks.
if c.Param.AutoTags > 0 && len(in.chunks) > 0 {
tagged, tagErr := c.runAutoTags(timeoutCtx, db, in)
if tagErr != nil {
return tagErr
}
in.chunks = tagged
}
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
if len(in.chunks) == 0 {
ans, callErr := c.call(timeoutCtx, db, in, "")
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
if callErr != nil {
return callErr
}
in.chunks = []map[string]any{{in.fieldName: ans}}
return nil
}
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
for i, ck := range in.chunks {
text, _ := ck["content_with_weight"].(string)
if strings.TrimSpace(text) == "" {
text, _ = ck["text"].(string)
}
fix(ingestion): improve Extractor/LLM robustness and Python->Go parity (#17470) ## Summary This PR hardens the Go ingestion **Extractor** and **LLM retry** paths and closes several Python->Go parity gaps in the keyword/question/tag extraction flow. - **Generic retry utility** (`internal/common/retry.go`): `RetryWithBackoff` with exponential backoff (default 3 retries, 2s initial delay, capped at 1m), context-aware sleep, and a `maxRetries<=0` fast path. Covered by `internal/common/retry_test.go`. - **LLM retry reuse**: `agent/component/llm_retry.go` now delegates to `common.RetryWithBackoff` instead of an inline loop (behavior preserved: ctx cancellation short-circuits the backoff). - **Extractor LLM calls** (`extractor.go`): - `call()` now retries transient LLM failures via `RetryWithBackoff` (retry exhaustion fails the chunk instead of silently skipping). - Sets `temperature = 0.2`, matching Python `generator.py:230,245`. - Runs keyword and question extraction **concurrently** per chunk when both are enabled (`task_executor.py:444-448`), with mutex-guarded map writes to avoid data races. - Substitutes `{field_name}` placeholders (including `{chunks}` -> chunk text) in `prompt`/`system_prompt` before the call, mirroring Python `string_format` (`extractor.py:102-103`); unmatched placeholders are left as-is. - Falls back to the **tenant default chat model** when `llm_id` is empty (`task_executor.py:573-574`). - Strips `` **greedily** (`strings.LastIndex`) in `cleanExtractionResult`. - **Auto-tagging** (`extractor_tag.go`): drops the `in.llmID != ""` guards so an empty `llm_id` no longer skips tagging (uses the tenant default model), and strips `` greedily in `parseTaggerResponse`. - **Docs**: fixes a misleading `PresentationChunker` docstring that claimed per-slide `image`/`position` output (the PPTX path emits none — unlike PDF), and removes a stale `docs/migration_python_go_diff.md` reference in `media_dispatch.go`. ## Test plan - `bash build.sh --test ./internal/common/...` — passes (new retry utility + tests). - `bash build.sh --test ./internal/ingestion/component/...` — passes (extractor/chunker/schema). - `gofmt` and lefthook pre-commit checks pass. Note: the personal `docs/migration_python_go_diff.md` working notebook in the tree is intentionally **not** part of this PR. --------- Co-authored-by: CodeBuddy <noreply@codebuddy.ai>
2026-07-28 19:22:18 +08:00
if c.Param.AutoKeywords > 0 && c.Param.AutoQuestions > 0 {
// Run keyword and question extraction concurrently
// per chunk (mirrors Python's ThreadPoolExecutor:
// task_executor.py:444-448). The shared chunk map is
// guarded by mu inside runAutoKeywords/runAutoQuestions,
// which also short-circuit when the key already exists.
var kwErr, qErr error
var mu sync.Mutex
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
if err := c.runAutoKeywords(timeoutCtx, db, in, ck, text, &mu); err != nil {
kwErr = err
}
}()
go func() {
defer wg.Done()
if err := c.runAutoQuestions(timeoutCtx, db, in, ck, text, &mu); err != nil {
qErr = err
}
}()
wg.Wait()
if kwErr != nil {
return fmt.Errorf("chunk %d keywords: %w", i, kwErr)
}
fix(ingestion): improve Extractor/LLM robustness and Python->Go parity (#17470) ## Summary This PR hardens the Go ingestion **Extractor** and **LLM retry** paths and closes several Python->Go parity gaps in the keyword/question/tag extraction flow. - **Generic retry utility** (`internal/common/retry.go`): `RetryWithBackoff` with exponential backoff (default 3 retries, 2s initial delay, capped at 1m), context-aware sleep, and a `maxRetries<=0` fast path. Covered by `internal/common/retry_test.go`. - **LLM retry reuse**: `agent/component/llm_retry.go` now delegates to `common.RetryWithBackoff` instead of an inline loop (behavior preserved: ctx cancellation short-circuits the backoff). - **Extractor LLM calls** (`extractor.go`): - `call()` now retries transient LLM failures via `RetryWithBackoff` (retry exhaustion fails the chunk instead of silently skipping). - Sets `temperature = 0.2`, matching Python `generator.py:230,245`. - Runs keyword and question extraction **concurrently** per chunk when both are enabled (`task_executor.py:444-448`), with mutex-guarded map writes to avoid data races. - Substitutes `{field_name}` placeholders (including `{chunks}` -> chunk text) in `prompt`/`system_prompt` before the call, mirroring Python `string_format` (`extractor.py:102-103`); unmatched placeholders are left as-is. - Falls back to the **tenant default chat model** when `llm_id` is empty (`task_executor.py:573-574`). - Strips `` **greedily** (`strings.LastIndex`) in `cleanExtractionResult`. - **Auto-tagging** (`extractor_tag.go`): drops the `in.llmID != ""` guards so an empty `llm_id` no longer skips tagging (uses the tenant default model), and strips `` greedily in `parseTaggerResponse`. - **Docs**: fixes a misleading `PresentationChunker` docstring that claimed per-slide `image`/`position` output (the PPTX path emits none — unlike PDF), and removes a stale `docs/migration_python_go_diff.md` reference in `media_dispatch.go`. ## Test plan - `bash build.sh --test ./internal/common/...` — passes (new retry utility + tests). - `bash build.sh --test ./internal/ingestion/component/...` — passes (extractor/chunker/schema). - `gofmt` and lefthook pre-commit checks pass. Note: the personal `docs/migration_python_go_diff.md` working notebook in the tree is intentionally **not** part of this PR. --------- Co-authored-by: CodeBuddy <noreply@codebuddy.ai>
2026-07-28 19:22:18 +08:00
if qErr != nil {
return fmt.Errorf("chunk %d questions: %w", i, qErr)
}
} else {
if c.Param.AutoKeywords > 0 {
if err := c.runAutoKeywords(timeoutCtx, db, in, ck, text, nil); err != nil {
return fmt.Errorf("chunk %d keywords: %w", i, err)
}
}
if c.Param.AutoQuestions > 0 {
if err := c.runAutoQuestions(timeoutCtx, db, in, ck, text, nil); err != nil {
return fmt.Errorf("chunk %d questions: %w", i, err)
}
}
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
}
if in.fieldName != "" {
fix(ingestion): improve Extractor/LLM robustness and Python->Go parity (#17470) ## Summary This PR hardens the Go ingestion **Extractor** and **LLM retry** paths and closes several Python->Go parity gaps in the keyword/question/tag extraction flow. - **Generic retry utility** (`internal/common/retry.go`): `RetryWithBackoff` with exponential backoff (default 3 retries, 2s initial delay, capped at 1m), context-aware sleep, and a `maxRetries<=0` fast path. Covered by `internal/common/retry_test.go`. - **LLM retry reuse**: `agent/component/llm_retry.go` now delegates to `common.RetryWithBackoff` instead of an inline loop (behavior preserved: ctx cancellation short-circuits the backoff). - **Extractor LLM calls** (`extractor.go`): - `call()` now retries transient LLM failures via `RetryWithBackoff` (retry exhaustion fails the chunk instead of silently skipping). - Sets `temperature = 0.2`, matching Python `generator.py:230,245`. - Runs keyword and question extraction **concurrently** per chunk when both are enabled (`task_executor.py:444-448`), with mutex-guarded map writes to avoid data races. - Substitutes `{field_name}` placeholders (including `{chunks}` -> chunk text) in `prompt`/`system_prompt` before the call, mirroring Python `string_format` (`extractor.py:102-103`); unmatched placeholders are left as-is. - Falls back to the **tenant default chat model** when `llm_id` is empty (`task_executor.py:573-574`). - Strips `` **greedily** (`strings.LastIndex`) in `cleanExtractionResult`. - **Auto-tagging** (`extractor_tag.go`): drops the `in.llmID != ""` guards so an empty `llm_id` no longer skips tagging (uses the tenant default model), and strips `` greedily in `parseTaggerResponse`. - **Docs**: fixes a misleading `PresentationChunker` docstring that claimed per-slide `image`/`position` output (the PPTX path emits none — unlike PDF), and removes a stale `docs/migration_python_go_diff.md` reference in `media_dispatch.go`. ## Test plan - `bash build.sh --test ./internal/common/...` — passes (new retry utility + tests). - `bash build.sh --test ./internal/ingestion/component/...` — passes (extractor/chunker/schema). - `gofmt` and lefthook pre-commit checks pass. Note: the personal `docs/migration_python_go_diff.md` working notebook in the tree is intentionally **not** part of this PR. --------- Co-authored-by: CodeBuddy <noreply@codebuddy.ai>
2026-07-28 19:22:18 +08:00
// Substitute {field_name} placeholders with current
// chunk field values, mirroring Python's
// string_format at extractor.py:103.
callIn := in
callIn.prompt = substituteChunkPlaceholders(in.prompt, ck, text)
callIn.systemPrompt = substituteChunkPlaceholders(in.systemPrompt, ck, text)
// buildExtractorMessages appends chunkText to the user
// message unconditionally. When the chunk text was already
// embedded via a {text}/{chunks} placeholder above, passing
// it again would duplicate the content in the final prompt.
// Suppress the automatic append in that case (the keyword/
// question helper paths do the same by passing "").
callChunkText := text
if strings.Contains(in.prompt, "{text}") || strings.Contains(in.prompt, "{chunks}") ||
strings.Contains(in.systemPrompt, "{text}") || strings.Contains(in.systemPrompt, "{chunks}") {
callChunkText = ""
}
ans, callErr := c.call(timeoutCtx, db, callIn, callChunkText)
if callErr != nil {
return fmt.Errorf("chunk %d: %w", i, callErr)
}
ck[in.fieldName] = ans
}
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
}
return nil
}); err != nil {
return nil, fmt.Errorf("extractor: %w", err)
}
fix: docx/email parsing, extractor LLM driver, and chunker alignment (#17144) ## Summary Three groups of changes across the Go ingestion pipeline: ### 1. DOCX parsing improvements - **docx_parser.go**: Enhanced DOCX parsing with better structure extraction and media handling - **docx_parser_cgo_test.go**, **docx_parser_test.go**: Companion tests - **office_parsers_no_cgo.go**: Stub sync for non-CGO builds ### 2. Email (.eml) parsing: base64 Content-Transfer-Encoding decoding - **email_parser.go** (`decodeCTE`): Added Content-Transfer-Encoding decoding for base64 and quoted-printable. Go's `mime/multipart.Reader` does not decode Content-Transfer-Encoding automatically, so attachments with `Content-Transfer-Encoding: base64` remained base64-encoded in the output. The new `decodeCTE` helper is called after reading each multipart part's raw bytes in `readMailBody`, mirroring Python's `part.get_payload(decode=True)`. - **email_parser_test.go**: Two new tests — simple base64 attachment and nested multipart/alternative with base64 attachment. ### 3. Extractor LLM driver fix + ModelDriver consolidation - **extractor.go**: Fixed a bug where the Extractor component used `ModelFactory.CreateModelDriver()`, which creates bare model instances without API keys or provider configuration. Switched to `models.GetPreconfiguredDriver()` which resolves the actual pre-configured driver from `ProviderManager`, matching the codepath used by `llm.go`. This fixes auto keyword/question extraction in DSL pipelines that require LLM calls. - **get_driver.go** (new): Extracted shared `GetPreconfiguredDriver()` from `llm.go:newChatModelDriver()` so both `llm.go` and `extractor.go` use the same codepath. - **get_driver_test.go** (new): Tests for the shared driver resolution. - **llm.go**: Replaced inline driver resolution with `models.GetPreconfiguredDriver()`. ### 4. Chunker fixes and observability - **group.go** (`extractLineRecords`): Fixed to also read `markdown` and `html` payload keys — previously it only read `text`/`content`, causing GroupTitleChunker to silently return empty results for markdown-format parser output. - **common.go** (`compileDelimPattern`): Aligned with Python's `_compile_delimiter_pattern` — only backtick-wrapped delimiters produce an active regex pattern; plain delimiters are not compiled into the split regex. - **token.go** (`applyChildrenDelim`): Set `DocType` and `CKType` to `"text"` on created ChunkDocs so the token-size merge path correctly identifies and merges text segments. - **parser.go**, **extractor.go**, **tokenizer.go**, **group.go**, **hierarchy.go**: Added debug-level logging for pipeline diagnostics. - **parser_dispatch_test.go**, **group_test.go**: New tests. ## Verification - All Go tests pass: `bash build.sh --test ./internal/parser/parser/...` and `bash build.sh --test ./internal/ingestion/component/...` - Build succeeds: `bash build.sh --go`
2026-07-21 13:51:17 +08:00
common.Debug("extractor stage",
zap.String("component", "Extractor"),
zap.Int("output_chunks", len(in.chunks)),
)
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
return map[string]any{
"chunks": in.chunks,
"output_format": "chunks",
}, nil
}
fix(ingestion): improve Extractor/LLM robustness and Python->Go parity (#17470) ## Summary This PR hardens the Go ingestion **Extractor** and **LLM retry** paths and closes several Python->Go parity gaps in the keyword/question/tag extraction flow. - **Generic retry utility** (`internal/common/retry.go`): `RetryWithBackoff` with exponential backoff (default 3 retries, 2s initial delay, capped at 1m), context-aware sleep, and a `maxRetries<=0` fast path. Covered by `internal/common/retry_test.go`. - **LLM retry reuse**: `agent/component/llm_retry.go` now delegates to `common.RetryWithBackoff` instead of an inline loop (behavior preserved: ctx cancellation short-circuits the backoff). - **Extractor LLM calls** (`extractor.go`): - `call()` now retries transient LLM failures via `RetryWithBackoff` (retry exhaustion fails the chunk instead of silently skipping). - Sets `temperature = 0.2`, matching Python `generator.py:230,245`. - Runs keyword and question extraction **concurrently** per chunk when both are enabled (`task_executor.py:444-448`), with mutex-guarded map writes to avoid data races. - Substitutes `{field_name}` placeholders (including `{chunks}` -> chunk text) in `prompt`/`system_prompt` before the call, mirroring Python `string_format` (`extractor.py:102-103`); unmatched placeholders are left as-is. - Falls back to the **tenant default chat model** when `llm_id` is empty (`task_executor.py:573-574`). - Strips `` **greedily** (`strings.LastIndex`) in `cleanExtractionResult`. - **Auto-tagging** (`extractor_tag.go`): drops the `in.llmID != ""` guards so an empty `llm_id` no longer skips tagging (uses the tenant default model), and strips `` greedily in `parseTaggerResponse`. - **Docs**: fixes a misleading `PresentationChunker` docstring that claimed per-slide `image`/`position` output (the PPTX path emits none — unlike PDF), and removes a stale `docs/migration_python_go_diff.md` reference in `media_dispatch.go`. ## Test plan - `bash build.sh --test ./internal/common/...` — passes (new retry utility + tests). - `bash build.sh --test ./internal/ingestion/component/...` — passes (extractor/chunker/schema). - `gofmt` and lefthook pre-commit checks pass. Note: the personal `docs/migration_python_go_diff.md` working notebook in the tree is intentionally **not** part of this PR. --------- Co-authored-by: CodeBuddy <noreply@codebuddy.ai>
2026-07-28 19:22:18 +08:00
// runAutoKeywords extracts keywords for the current chunk and stores
// them on ck["important_kwd"]. mu may be nil (sequential path); when
// non-nil it serializes the shared chunk-map accesses so concurrent
// keyword/question goroutines stay race-free. The existence check and
// the map writes are both guarded by mu. Keyword extraction pins
// temperature to extractorTemperature (0.2) to mirror generator.py.
func (c *ExtractorComponent) runAutoKeywords(ctx context.Context, db *gorm.DB, in extractorInputs, ck map[string]any, chunkText string, mu *sync.Mutex) error {
if mu != nil {
mu.Lock()
}
_, exists := ck["important_kwd"]
if mu != nil {
mu.Unlock()
}
if exists {
return nil
}
fix(ingestion): improve Extractor/LLM robustness and Python->Go parity (#17470) ## Summary This PR hardens the Go ingestion **Extractor** and **LLM retry** paths and closes several Python->Go parity gaps in the keyword/question/tag extraction flow. - **Generic retry utility** (`internal/common/retry.go`): `RetryWithBackoff` with exponential backoff (default 3 retries, 2s initial delay, capped at 1m), context-aware sleep, and a `maxRetries<=0` fast path. Covered by `internal/common/retry_test.go`. - **LLM retry reuse**: `agent/component/llm_retry.go` now delegates to `common.RetryWithBackoff` instead of an inline loop (behavior preserved: ctx cancellation short-circuits the backoff). - **Extractor LLM calls** (`extractor.go`): - `call()` now retries transient LLM failures via `RetryWithBackoff` (retry exhaustion fails the chunk instead of silently skipping). - Sets `temperature = 0.2`, matching Python `generator.py:230,245`. - Runs keyword and question extraction **concurrently** per chunk when both are enabled (`task_executor.py:444-448`), with mutex-guarded map writes to avoid data races. - Substitutes `{field_name}` placeholders (including `{chunks}` -> chunk text) in `prompt`/`system_prompt` before the call, mirroring Python `string_format` (`extractor.py:102-103`); unmatched placeholders are left as-is. - Falls back to the **tenant default chat model** when `llm_id` is empty (`task_executor.py:573-574`). - Strips `` **greedily** (`strings.LastIndex`) in `cleanExtractionResult`. - **Auto-tagging** (`extractor_tag.go`): drops the `in.llmID != ""` guards so an empty `llm_id` no longer skips tagging (uses the tenant default model), and strips `` greedily in `parseTaggerResponse`. - **Docs**: fixes a misleading `PresentationChunker` docstring that claimed per-slide `image`/`position` output (the PPTX path emits none — unlike PDF), and removes a stale `docs/migration_python_go_diff.md` reference in `media_dispatch.go`. ## Test plan - `bash build.sh --test ./internal/common/...` — passes (new retry utility + tests). - `bash build.sh --test ./internal/ingestion/component/...` — passes (extractor/chunker/schema). - `gofmt` and lefthook pre-commit checks pass. Note: the personal `docs/migration_python_go_diff.md` working notebook in the tree is intentionally **not** part of this PR. --------- Co-authored-by: CodeBuddy <noreply@codebuddy.ai>
2026-07-28 19:22:18 +08:00
kwTemp := extractorTemperature
kwIn := extractorInputs{
llmID: in.llmID,
systemPrompt: fmt.Sprintf(autoKeywordPrompt, c.Param.AutoKeywords, chunkText),
prompt: "Output: ",
temperature: &kwTemp,
}
result, err := c.call(ctx, db, kwIn, "")
if err != nil {
return err
}
resultStr, _ := result.(string)
resultStr = cleanExtractionResult(resultStr)
if resultStr == "" {
return nil
}
kwds := splitKeywords(resultStr)
if len(kwds) == 0 {
return nil
}
tok := tokenizer.New(in.lang)
tks, tkErr := tok.Tokenize(strings.Join(kwds, " "))
fix(ingestion): improve Extractor/LLM robustness and Python->Go parity (#17470) ## Summary This PR hardens the Go ingestion **Extractor** and **LLM retry** paths and closes several Python->Go parity gaps in the keyword/question/tag extraction flow. - **Generic retry utility** (`internal/common/retry.go`): `RetryWithBackoff` with exponential backoff (default 3 retries, 2s initial delay, capped at 1m), context-aware sleep, and a `maxRetries<=0` fast path. Covered by `internal/common/retry_test.go`. - **LLM retry reuse**: `agent/component/llm_retry.go` now delegates to `common.RetryWithBackoff` instead of an inline loop (behavior preserved: ctx cancellation short-circuits the backoff). - **Extractor LLM calls** (`extractor.go`): - `call()` now retries transient LLM failures via `RetryWithBackoff` (retry exhaustion fails the chunk instead of silently skipping). - Sets `temperature = 0.2`, matching Python `generator.py:230,245`. - Runs keyword and question extraction **concurrently** per chunk when both are enabled (`task_executor.py:444-448`), with mutex-guarded map writes to avoid data races. - Substitutes `{field_name}` placeholders (including `{chunks}` -> chunk text) in `prompt`/`system_prompt` before the call, mirroring Python `string_format` (`extractor.py:102-103`); unmatched placeholders are left as-is. - Falls back to the **tenant default chat model** when `llm_id` is empty (`task_executor.py:573-574`). - Strips `` **greedily** (`strings.LastIndex`) in `cleanExtractionResult`. - **Auto-tagging** (`extractor_tag.go`): drops the `in.llmID != ""` guards so an empty `llm_id` no longer skips tagging (uses the tenant default model), and strips `` greedily in `parseTaggerResponse`. - **Docs**: fixes a misleading `PresentationChunker` docstring that claimed per-slide `image`/`position` output (the PPTX path emits none — unlike PDF), and removes a stale `docs/migration_python_go_diff.md` reference in `media_dispatch.go`. ## Test plan - `bash build.sh --test ./internal/common/...` — passes (new retry utility + tests). - `bash build.sh --test ./internal/ingestion/component/...` — passes (extractor/chunker/schema). - `gofmt` and lefthook pre-commit checks pass. Note: the personal `docs/migration_python_go_diff.md` working notebook in the tree is intentionally **not** part of this PR. --------- Co-authored-by: CodeBuddy <noreply@codebuddy.ai>
2026-07-28 19:22:18 +08:00
if mu != nil {
mu.Lock()
}
ck["important_kwd"] = kwds
if tkErr == nil {
ck["important_tks"] = tks
}
fix(ingestion): improve Extractor/LLM robustness and Python->Go parity (#17470) ## Summary This PR hardens the Go ingestion **Extractor** and **LLM retry** paths and closes several Python->Go parity gaps in the keyword/question/tag extraction flow. - **Generic retry utility** (`internal/common/retry.go`): `RetryWithBackoff` with exponential backoff (default 3 retries, 2s initial delay, capped at 1m), context-aware sleep, and a `maxRetries<=0` fast path. Covered by `internal/common/retry_test.go`. - **LLM retry reuse**: `agent/component/llm_retry.go` now delegates to `common.RetryWithBackoff` instead of an inline loop (behavior preserved: ctx cancellation short-circuits the backoff). - **Extractor LLM calls** (`extractor.go`): - `call()` now retries transient LLM failures via `RetryWithBackoff` (retry exhaustion fails the chunk instead of silently skipping). - Sets `temperature = 0.2`, matching Python `generator.py:230,245`. - Runs keyword and question extraction **concurrently** per chunk when both are enabled (`task_executor.py:444-448`), with mutex-guarded map writes to avoid data races. - Substitutes `{field_name}` placeholders (including `{chunks}` -> chunk text) in `prompt`/`system_prompt` before the call, mirroring Python `string_format` (`extractor.py:102-103`); unmatched placeholders are left as-is. - Falls back to the **tenant default chat model** when `llm_id` is empty (`task_executor.py:573-574`). - Strips `` **greedily** (`strings.LastIndex`) in `cleanExtractionResult`. - **Auto-tagging** (`extractor_tag.go`): drops the `in.llmID != ""` guards so an empty `llm_id` no longer skips tagging (uses the tenant default model), and strips `` greedily in `parseTaggerResponse`. - **Docs**: fixes a misleading `PresentationChunker` docstring that claimed per-slide `image`/`position` output (the PPTX path emits none — unlike PDF), and removes a stale `docs/migration_python_go_diff.md` reference in `media_dispatch.go`. ## Test plan - `bash build.sh --test ./internal/common/...` — passes (new retry utility + tests). - `bash build.sh --test ./internal/ingestion/component/...` — passes (extractor/chunker/schema). - `gofmt` and lefthook pre-commit checks pass. Note: the personal `docs/migration_python_go_diff.md` working notebook in the tree is intentionally **not** part of this PR. --------- Co-authored-by: CodeBuddy <noreply@codebuddy.ai>
2026-07-28 19:22:18 +08:00
if mu != nil {
mu.Unlock()
}
return nil
}
fix(ingestion): improve Extractor/LLM robustness and Python->Go parity (#17470) ## Summary This PR hardens the Go ingestion **Extractor** and **LLM retry** paths and closes several Python->Go parity gaps in the keyword/question/tag extraction flow. - **Generic retry utility** (`internal/common/retry.go`): `RetryWithBackoff` with exponential backoff (default 3 retries, 2s initial delay, capped at 1m), context-aware sleep, and a `maxRetries<=0` fast path. Covered by `internal/common/retry_test.go`. - **LLM retry reuse**: `agent/component/llm_retry.go` now delegates to `common.RetryWithBackoff` instead of an inline loop (behavior preserved: ctx cancellation short-circuits the backoff). - **Extractor LLM calls** (`extractor.go`): - `call()` now retries transient LLM failures via `RetryWithBackoff` (retry exhaustion fails the chunk instead of silently skipping). - Sets `temperature = 0.2`, matching Python `generator.py:230,245`. - Runs keyword and question extraction **concurrently** per chunk when both are enabled (`task_executor.py:444-448`), with mutex-guarded map writes to avoid data races. - Substitutes `{field_name}` placeholders (including `{chunks}` -> chunk text) in `prompt`/`system_prompt` before the call, mirroring Python `string_format` (`extractor.py:102-103`); unmatched placeholders are left as-is. - Falls back to the **tenant default chat model** when `llm_id` is empty (`task_executor.py:573-574`). - Strips `` **greedily** (`strings.LastIndex`) in `cleanExtractionResult`. - **Auto-tagging** (`extractor_tag.go`): drops the `in.llmID != ""` guards so an empty `llm_id` no longer skips tagging (uses the tenant default model), and strips `` greedily in `parseTaggerResponse`. - **Docs**: fixes a misleading `PresentationChunker` docstring that claimed per-slide `image`/`position` output (the PPTX path emits none — unlike PDF), and removes a stale `docs/migration_python_go_diff.md` reference in `media_dispatch.go`. ## Test plan - `bash build.sh --test ./internal/common/...` — passes (new retry utility + tests). - `bash build.sh --test ./internal/ingestion/component/...` — passes (extractor/chunker/schema). - `gofmt` and lefthook pre-commit checks pass. Note: the personal `docs/migration_python_go_diff.md` working notebook in the tree is intentionally **not** part of this PR. --------- Co-authored-by: CodeBuddy <noreply@codebuddy.ai>
2026-07-28 19:22:18 +08:00
// runAutoQuestions extracts questions for the current chunk and stores
// them on ck["question_kwd"]. See runAutoKeywords for the mu contract
// and the temperature pin.
func (c *ExtractorComponent) runAutoQuestions(ctx context.Context, db *gorm.DB, in extractorInputs, ck map[string]any, chunkText string, mu *sync.Mutex) error {
if mu != nil {
mu.Lock()
}
_, exists := ck["question_kwd"]
if mu != nil {
mu.Unlock()
}
if exists {
return nil
}
fix(ingestion): improve Extractor/LLM robustness and Python->Go parity (#17470) ## Summary This PR hardens the Go ingestion **Extractor** and **LLM retry** paths and closes several Python->Go parity gaps in the keyword/question/tag extraction flow. - **Generic retry utility** (`internal/common/retry.go`): `RetryWithBackoff` with exponential backoff (default 3 retries, 2s initial delay, capped at 1m), context-aware sleep, and a `maxRetries<=0` fast path. Covered by `internal/common/retry_test.go`. - **LLM retry reuse**: `agent/component/llm_retry.go` now delegates to `common.RetryWithBackoff` instead of an inline loop (behavior preserved: ctx cancellation short-circuits the backoff). - **Extractor LLM calls** (`extractor.go`): - `call()` now retries transient LLM failures via `RetryWithBackoff` (retry exhaustion fails the chunk instead of silently skipping). - Sets `temperature = 0.2`, matching Python `generator.py:230,245`. - Runs keyword and question extraction **concurrently** per chunk when both are enabled (`task_executor.py:444-448`), with mutex-guarded map writes to avoid data races. - Substitutes `{field_name}` placeholders (including `{chunks}` -> chunk text) in `prompt`/`system_prompt` before the call, mirroring Python `string_format` (`extractor.py:102-103`); unmatched placeholders are left as-is. - Falls back to the **tenant default chat model** when `llm_id` is empty (`task_executor.py:573-574`). - Strips `` **greedily** (`strings.LastIndex`) in `cleanExtractionResult`. - **Auto-tagging** (`extractor_tag.go`): drops the `in.llmID != ""` guards so an empty `llm_id` no longer skips tagging (uses the tenant default model), and strips `` greedily in `parseTaggerResponse`. - **Docs**: fixes a misleading `PresentationChunker` docstring that claimed per-slide `image`/`position` output (the PPTX path emits none — unlike PDF), and removes a stale `docs/migration_python_go_diff.md` reference in `media_dispatch.go`. ## Test plan - `bash build.sh --test ./internal/common/...` — passes (new retry utility + tests). - `bash build.sh --test ./internal/ingestion/component/...` — passes (extractor/chunker/schema). - `gofmt` and lefthook pre-commit checks pass. Note: the personal `docs/migration_python_go_diff.md` working notebook in the tree is intentionally **not** part of this PR. --------- Co-authored-by: CodeBuddy <noreply@codebuddy.ai>
2026-07-28 19:22:18 +08:00
qTemp := extractorTemperature
qIn := extractorInputs{
llmID: in.llmID,
systemPrompt: fmt.Sprintf(autoQuestionPrompt, c.Param.AutoQuestions, chunkText),
prompt: "Output: ",
temperature: &qTemp,
}
result, err := c.call(ctx, db, qIn, "")
if err != nil {
return err
}
resultStr, _ := result.(string)
resultStr = cleanExtractionResult(resultStr)
if resultStr == "" {
return nil
}
qs := strings.Split(resultStr, "\n")
// Filter empty lines
var filtered []string
for _, q := range qs {
q = strings.TrimSpace(q)
if q != "" {
filtered = append(filtered, q)
}
}
if len(filtered) == 0 {
return nil
}
tok := tokenizer.New(in.lang)
tks, tkErr := tok.Tokenize(strings.Join(filtered, "\n"))
fix(ingestion): improve Extractor/LLM robustness and Python->Go parity (#17470) ## Summary This PR hardens the Go ingestion **Extractor** and **LLM retry** paths and closes several Python->Go parity gaps in the keyword/question/tag extraction flow. - **Generic retry utility** (`internal/common/retry.go`): `RetryWithBackoff` with exponential backoff (default 3 retries, 2s initial delay, capped at 1m), context-aware sleep, and a `maxRetries<=0` fast path. Covered by `internal/common/retry_test.go`. - **LLM retry reuse**: `agent/component/llm_retry.go` now delegates to `common.RetryWithBackoff` instead of an inline loop (behavior preserved: ctx cancellation short-circuits the backoff). - **Extractor LLM calls** (`extractor.go`): - `call()` now retries transient LLM failures via `RetryWithBackoff` (retry exhaustion fails the chunk instead of silently skipping). - Sets `temperature = 0.2`, matching Python `generator.py:230,245`. - Runs keyword and question extraction **concurrently** per chunk when both are enabled (`task_executor.py:444-448`), with mutex-guarded map writes to avoid data races. - Substitutes `{field_name}` placeholders (including `{chunks}` -> chunk text) in `prompt`/`system_prompt` before the call, mirroring Python `string_format` (`extractor.py:102-103`); unmatched placeholders are left as-is. - Falls back to the **tenant default chat model** when `llm_id` is empty (`task_executor.py:573-574`). - Strips `` **greedily** (`strings.LastIndex`) in `cleanExtractionResult`. - **Auto-tagging** (`extractor_tag.go`): drops the `in.llmID != ""` guards so an empty `llm_id` no longer skips tagging (uses the tenant default model), and strips `` greedily in `parseTaggerResponse`. - **Docs**: fixes a misleading `PresentationChunker` docstring that claimed per-slide `image`/`position` output (the PPTX path emits none — unlike PDF), and removes a stale `docs/migration_python_go_diff.md` reference in `media_dispatch.go`. ## Test plan - `bash build.sh --test ./internal/common/...` — passes (new retry utility + tests). - `bash build.sh --test ./internal/ingestion/component/...` — passes (extractor/chunker/schema). - `gofmt` and lefthook pre-commit checks pass. Note: the personal `docs/migration_python_go_diff.md` working notebook in the tree is intentionally **not** part of this PR. --------- Co-authored-by: CodeBuddy <noreply@codebuddy.ai>
2026-07-28 19:22:18 +08:00
if mu != nil {
mu.Lock()
}
ck["question_kwd"] = filtered
if tkErr == nil {
ck["question_tks"] = tks
}
fix(ingestion): improve Extractor/LLM robustness and Python->Go parity (#17470) ## Summary This PR hardens the Go ingestion **Extractor** and **LLM retry** paths and closes several Python->Go parity gaps in the keyword/question/tag extraction flow. - **Generic retry utility** (`internal/common/retry.go`): `RetryWithBackoff` with exponential backoff (default 3 retries, 2s initial delay, capped at 1m), context-aware sleep, and a `maxRetries<=0` fast path. Covered by `internal/common/retry_test.go`. - **LLM retry reuse**: `agent/component/llm_retry.go` now delegates to `common.RetryWithBackoff` instead of an inline loop (behavior preserved: ctx cancellation short-circuits the backoff). - **Extractor LLM calls** (`extractor.go`): - `call()` now retries transient LLM failures via `RetryWithBackoff` (retry exhaustion fails the chunk instead of silently skipping). - Sets `temperature = 0.2`, matching Python `generator.py:230,245`. - Runs keyword and question extraction **concurrently** per chunk when both are enabled (`task_executor.py:444-448`), with mutex-guarded map writes to avoid data races. - Substitutes `{field_name}` placeholders (including `{chunks}` -> chunk text) in `prompt`/`system_prompt` before the call, mirroring Python `string_format` (`extractor.py:102-103`); unmatched placeholders are left as-is. - Falls back to the **tenant default chat model** when `llm_id` is empty (`task_executor.py:573-574`). - Strips `` **greedily** (`strings.LastIndex`) in `cleanExtractionResult`. - **Auto-tagging** (`extractor_tag.go`): drops the `in.llmID != ""` guards so an empty `llm_id` no longer skips tagging (uses the tenant default model), and strips `` greedily in `parseTaggerResponse`. - **Docs**: fixes a misleading `PresentationChunker` docstring that claimed per-slide `image`/`position` output (the PPTX path emits none — unlike PDF), and removes a stale `docs/migration_python_go_diff.md` reference in `media_dispatch.go`. ## Test plan - `bash build.sh --test ./internal/common/...` — passes (new retry utility + tests). - `bash build.sh --test ./internal/ingestion/component/...` — passes (extractor/chunker/schema). - `gofmt` and lefthook pre-commit checks pass. Note: the personal `docs/migration_python_go_diff.md` working notebook in the tree is intentionally **not** part of this PR. --------- Co-authored-by: CodeBuddy <noreply@codebuddy.ai>
2026-07-28 19:22:18 +08:00
if mu != nil {
mu.Unlock()
}
return nil
}
// cleanExtractionResult strips `</think>` tags and rejects `**ERROR**` responses,
// matching Python's keyword_extraction and question_proposal post-processing.
func cleanExtractionResult(s string) string {
fix(ingestion): improve Extractor/LLM robustness and Python->Go parity (#17470) ## Summary This PR hardens the Go ingestion **Extractor** and **LLM retry** paths and closes several Python->Go parity gaps in the keyword/question/tag extraction flow. - **Generic retry utility** (`internal/common/retry.go`): `RetryWithBackoff` with exponential backoff (default 3 retries, 2s initial delay, capped at 1m), context-aware sleep, and a `maxRetries<=0` fast path. Covered by `internal/common/retry_test.go`. - **LLM retry reuse**: `agent/component/llm_retry.go` now delegates to `common.RetryWithBackoff` instead of an inline loop (behavior preserved: ctx cancellation short-circuits the backoff). - **Extractor LLM calls** (`extractor.go`): - `call()` now retries transient LLM failures via `RetryWithBackoff` (retry exhaustion fails the chunk instead of silently skipping). - Sets `temperature = 0.2`, matching Python `generator.py:230,245`. - Runs keyword and question extraction **concurrently** per chunk when both are enabled (`task_executor.py:444-448`), with mutex-guarded map writes to avoid data races. - Substitutes `{field_name}` placeholders (including `{chunks}` -> chunk text) in `prompt`/`system_prompt` before the call, mirroring Python `string_format` (`extractor.py:102-103`); unmatched placeholders are left as-is. - Falls back to the **tenant default chat model** when `llm_id` is empty (`task_executor.py:573-574`). - Strips `` **greedily** (`strings.LastIndex`) in `cleanExtractionResult`. - **Auto-tagging** (`extractor_tag.go`): drops the `in.llmID != ""` guards so an empty `llm_id` no longer skips tagging (uses the tenant default model), and strips `` greedily in `parseTaggerResponse`. - **Docs**: fixes a misleading `PresentationChunker` docstring that claimed per-slide `image`/`position` output (the PPTX path emits none — unlike PDF), and removes a stale `docs/migration_python_go_diff.md` reference in `media_dispatch.go`. ## Test plan - `bash build.sh --test ./internal/common/...` — passes (new retry utility + tests). - `bash build.sh --test ./internal/ingestion/component/...` — passes (extractor/chunker/schema). - `gofmt` and lefthook pre-commit checks pass. Note: the personal `docs/migration_python_go_diff.md` working notebook in the tree is intentionally **not** part of this PR. --------- Co-authored-by: CodeBuddy <noreply@codebuddy.ai>
2026-07-28 19:22:18 +08:00
if i := strings.LastIndex(s, "</think>"); i >= 0 {
s = s[i+len("</think>"):]
}
s = strings.TrimSpace(s)
if strings.Contains(s, "**ERROR**") {
return ""
}
return s
}
// splitKeywords splits a comma-delimited keyword string.
func splitKeywords(s string) []string {
parts := strings.FieldsFunc(s, func(r rune) bool {
return r == ',' || r == '' || r == ';' || r == '' || r == '、' || r == '\r' || r == '\n'
})
result := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
result = append(result, p)
}
}
return result
}
fix(ingestion): improve Extractor/LLM robustness and Python->Go parity (#17470) ## Summary This PR hardens the Go ingestion **Extractor** and **LLM retry** paths and closes several Python->Go parity gaps in the keyword/question/tag extraction flow. - **Generic retry utility** (`internal/common/retry.go`): `RetryWithBackoff` with exponential backoff (default 3 retries, 2s initial delay, capped at 1m), context-aware sleep, and a `maxRetries<=0` fast path. Covered by `internal/common/retry_test.go`. - **LLM retry reuse**: `agent/component/llm_retry.go` now delegates to `common.RetryWithBackoff` instead of an inline loop (behavior preserved: ctx cancellation short-circuits the backoff). - **Extractor LLM calls** (`extractor.go`): - `call()` now retries transient LLM failures via `RetryWithBackoff` (retry exhaustion fails the chunk instead of silently skipping). - Sets `temperature = 0.2`, matching Python `generator.py:230,245`. - Runs keyword and question extraction **concurrently** per chunk when both are enabled (`task_executor.py:444-448`), with mutex-guarded map writes to avoid data races. - Substitutes `{field_name}` placeholders (including `{chunks}` -> chunk text) in `prompt`/`system_prompt` before the call, mirroring Python `string_format` (`extractor.py:102-103`); unmatched placeholders are left as-is. - Falls back to the **tenant default chat model** when `llm_id` is empty (`task_executor.py:573-574`). - Strips `` **greedily** (`strings.LastIndex`) in `cleanExtractionResult`. - **Auto-tagging** (`extractor_tag.go`): drops the `in.llmID != ""` guards so an empty `llm_id` no longer skips tagging (uses the tenant default model), and strips `` greedily in `parseTaggerResponse`. - **Docs**: fixes a misleading `PresentationChunker` docstring that claimed per-slide `image`/`position` output (the PPTX path emits none — unlike PDF), and removes a stale `docs/migration_python_go_diff.md` reference in `media_dispatch.go`. ## Test plan - `bash build.sh --test ./internal/common/...` — passes (new retry utility + tests). - `bash build.sh --test ./internal/ingestion/component/...` — passes (extractor/chunker/schema). - `gofmt` and lefthook pre-commit checks pass. Note: the personal `docs/migration_python_go_diff.md` working notebook in the tree is intentionally **not** part of this PR. --------- Co-authored-by: CodeBuddy <noreply@codebuddy.ai>
2026-07-28 19:22:18 +08:00
// nonRetryableStatusRE matches HTTP client-error status codes that
// signal a permanent (non-transient) condition and therefore must
// NOT be retried. The word boundaries are essential: a bare
// substring check on "400" would wrongly flag phrasing such as
// "context deadline exceeded after 400ms" (a transient timeout,
// retryable) as non-retryable. \b ensures we only match a standalone
// 3-digit status token, so "400ms" / "4000" do not match. 429 and
// 5xx are deliberately absent — they stay retryable.
var nonRetryableStatusRE = regexp.MustCompile(`\b(?:400|401|403|404|405|422)\b`)
// isRetryableLLMError classifies an LLM chat error as worth
// retrying. The production chat invoker returns opaque errors:
// configuration failures (missing model/driver) before any API
// call, and the provider SDK's raw error after the call. We treat
// context cancellation/deadline as terminal, plus a lightweight
// heuristic for non-transient auth/client errors. Anything
// unrecognized defaults to retryable so genuinely transient 5xx /
// 429 / network blips keep retrying (matching the prior blind-retry
// behavior).
func isRetryableLLMError(err error) bool {
if err == nil {
return true
}
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return false
}
msg := strings.ToLower(err.Error())
for _, s := range []string{
"unauthorized", "authentication", "api key",
"bad request", "not found", "model not found", "content filter",
"no driver resolved", "model_name is required", "resolve driver",
} {
if strings.Contains(msg, s) {
return false
}
}
if nonRetryableStatusRE.MatchString(msg) {
return false
}
return true
}
// call dispatches one LLM chat call for the supplied chunk text
// (empty string in the no-chunk fast path). The result is the
// raw string from the model — JSON parsing happens here so
// callers can rely on a structured value downstream.
func (c *ExtractorComponent) call(ctx context.Context, db *gorm.DB, in extractorInputs, chunkText string) (any, error) {
driver, modelName, apiKey, baseURL, err := resolveExtractorChatTarget(ctx, db, in.llmID)
fix(ingestion): align laws DSL with Python — heading fallback, colon-title, short-line filter, remove_toc, and image extension mapping (#17200) ## Summary This PR aligns the Go ingestion pipeline's **Laws** DSL template with the Python implementation by fixing heading-detection gaps, adds image-extension support, refactors the **Extractor** component's LLM resolution, hardens heading detection for CJK text, and makes the Extractor accept the Python DSL prompt key names (`sys_prompt`/`prompts`) alongside the Go names. ## Changes ### 1. Picture file-type detection (`internal/utility/file.go`) Adds explicit mapping for common image extensions (png, jpg, jpeg, gif, bmp, tiff, tif, webp, svg, ico, avif, heic, apng) → `FileTypeVISUAL`, with regression tests. ### 2. Laws DSL heading-detection alignment (`internal/ingestion/component/chunker/`) Four fixes to `resolveTitleLevels`: | Fix | What changed | Why | |-----|-------------|-----| | **DOCX `ck_type` fallback** | `ckType` field on `lineRecord`, propagated from `ChunkDoc.CKType` in `recordsFromStructured`. When `ck_type=="heading"`, assign `fallbackLevel`. | office_oxide extracts DOCX heading metadata, but the info was lost before reaching the heading detector. Word headings whose text doesn't match any regex (e.g. "Introduction") were treated as body. | | **`make_colon_as_title` promotion** | `isColonTitle()`: promotes lines ending with `:`/`:` that have sentence-ending punctuation before the colon and ≥32 runes between them. | Mirrors Python's `make_colon_as_title` in `rag/nlp/__init__.py`. Triple guard prevents false positives. | | **Short/numeric line filter** | Lines with ≤1 rune or purely numeric are pinned to body level. | Mirrors Python `tree_merge`'s filter of `sections` where `len(...) <= 1` or `re.match(r"[0-9]+$", ...)`. | | **PDF `remove_toc`** | `"remove_toc": true` added to the PDF parser setup in `ingestion_pipeline_laws.json`. | The Go PDF parser already supports TOC removal; the Book template already enables it. | ### 3. Extractor llm_id resolution (`internal/ingestion/component/extractor.go`) Refactored to handle both **bare tenant_model UUIDs** and **composite model@provider** strings via the shared `resolveModelConfig` (`dispatch_model.go`): - **`resolveExtractorChatConfig`** — UUID path calls `resolveModelConfigByID` directly (one DB hit); composite path goes through `resolveModelConfig`. Added `isBareTenantModelID` pre-check for clear errors when a UUID doesn't exist. - **`resolveExtractorChatTarget`** — propagates resolution errors instead of silently returning empty driver. - **`Chat()`** — removed `driver = "dummy"` fallback. Missing driver is now an explicit error. - **Removed dead code**: `splitExtractorLLID`, `findExtractorSoleActiveInstance`. ### 4. `InjectExtractorLLMID` — fallback when no user config (`internal/common/parser_config.go`) Injects the tenant's global default LLM into extractor components **only when their `llm_id` is empty**. Preserves user-selected UUID or model@provider values. Priority: user-configured llm_id > tenant global default > error (no silent dummy fallback). ### 5. `ResponseHeaderTimeout` increase (`internal/entity/models/base_model.go`) `ResponseHeaderTimeout` 60s → 120s in `NewDriverHTTPClient`. Reasoning models with large extraction prompts can take longer than 60s to produce the first response token. ### 6. CJK rune-aware heading detection (`internal/ingestion/component/chunker/title.go`) Two byte-vs-rune bugs that only manifest on CJK text: | Fix | What changed | Why | |-----|-------------|-----| | **`isColonTitle` byte offset** | `body[lastPunct+1:]` → `body[lastPunct+runeLen:]` via `utf8.DecodeRuneInString` | `strings.LastIndexAny` returns a byte index; `+1` skips only 1 byte, corrupting multi-byte CJK punctuation (e.g. `。` = 3 bytes) and inflating the rune count past the 32-rune threshold → false-positive heading promotion. | | **Short-line filter byte count** | `len(text) <= 1` → `utf8.RuneCountInString(text) <= 1` | Go `len` is UTF-8 bytes; a single CJK char (3 bytes) passed the filter, but Python's `len` returns 1 → mismatch. | ### 7. `extractor_tag.go` — log error when llm fails When `resolveExtractorChatTarget` returned an error, `runAutoTags` will log error. ### 8. Python DSL prompt-key compatibility (`internal/ingestion/component/extractor.go`) The Resume DSL template uses Python-side key names (`sys_prompt`, `prompts`). `NewExtractorComponent` now accepts them as fallbacks alongside the Go names: - `system_prompt` (Go) ← `sys_prompt` (Python) as fallback - `prompt` (Go string) ← `prompts` (Python array `[{"role","content"}]`, takes `[0].content`) as fallback Mirrors the alias pattern already in `internal/agent/component/llm.go`. `resolveInputs` accepts per-call `sys_prompt` override too. ## Remaining gaps vs Python | Gap | Scope | Impact | |-----|-------|--------| | **TOC removal for TXT/MD/HTML** | Python's `remove_contents_table` works on all text formats; Go's `remove_toc` is PDF-only. | Low — plain-text documents rarely contain structured TOCs. | | **Regex pattern details** | Minor differences in quantifiers, missing H5/H6 markdown patterns, missing 4-level numbering pattern. | Low — Go's variants are stricter; DOCX headings are covered by `ck_type` fallback. | ## Testing - `TestHierarchyTitleChunker_CKTypeHeadingFallback` — DOCX `ck_type` heading promotion - `TestHierarchyTitleChunker_ColonTitlePromotion` / `_ColonTitleShortLine_Negative` — colon-title promotion + guard - `TestIsColonTitle_CJKEdgeCase` / `TestIsColonTitle_ASCII_NoRegression` — CJK byte-offset fix + ASCII regression - `TestHierarchyTitleChunker_ColonTitlePromotion_CJK_EdgeCase` — CJK colon edge case through full pipeline - `TestHierarchyTitleChunker_ShortSingleCJKLineFilter` — single CJK char filtered to body - `TestHierarchyTitleChunker_ShortNumericLineFilter` — purely numeric lines filtered - `TestGetFileType_ImageExtensions` / `_ExistingFormats_NoRegression` — image extension mapping - `TestInjectExtractorLLMID_SkipWhenUUID` / `_SkipWhenComposite` / `_InjectWhenEmpty` — llm_id injection guard - `TestIsBareTenantModelID` — UUID detection - `TestResolveExtractorChatTarget_AtSplitFallback` / `_NoDriver` — @ split fallback without DB - `TestNewExtractorComponent_SysPromptAlias` / `_PromptsArray` / `_PromptsArray_PromptWins` / `_SystemPromptWinsOverSysPrompt` — Python key compatibility - `TestBuildDOCXJSONSections_List` / `_TextBox` / `_MixedWithList` — DOCX list/text_box parsing - Full ingestion test suite passes (chunker, pipeline, task, service, component packages)
2026-07-22 19:14:32 +08:00
if err != nil {
return nil, err
}
msgs := buildExtractorMessages(in.systemPrompt, in.prompt, chunkText, in.chunks)
inv := getExtractorChatInvoker()
fix(ingestion): improve Extractor/LLM robustness and Python->Go parity (#17470) ## Summary This PR hardens the Go ingestion **Extractor** and **LLM retry** paths and closes several Python->Go parity gaps in the keyword/question/tag extraction flow. - **Generic retry utility** (`internal/common/retry.go`): `RetryWithBackoff` with exponential backoff (default 3 retries, 2s initial delay, capped at 1m), context-aware sleep, and a `maxRetries<=0` fast path. Covered by `internal/common/retry_test.go`. - **LLM retry reuse**: `agent/component/llm_retry.go` now delegates to `common.RetryWithBackoff` instead of an inline loop (behavior preserved: ctx cancellation short-circuits the backoff). - **Extractor LLM calls** (`extractor.go`): - `call()` now retries transient LLM failures via `RetryWithBackoff` (retry exhaustion fails the chunk instead of silently skipping). - Sets `temperature = 0.2`, matching Python `generator.py:230,245`. - Runs keyword and question extraction **concurrently** per chunk when both are enabled (`task_executor.py:444-448`), with mutex-guarded map writes to avoid data races. - Substitutes `{field_name}` placeholders (including `{chunks}` -> chunk text) in `prompt`/`system_prompt` before the call, mirroring Python `string_format` (`extractor.py:102-103`); unmatched placeholders are left as-is. - Falls back to the **tenant default chat model** when `llm_id` is empty (`task_executor.py:573-574`). - Strips `` **greedily** (`strings.LastIndex`) in `cleanExtractionResult`. - **Auto-tagging** (`extractor_tag.go`): drops the `in.llmID != ""` guards so an empty `llm_id` no longer skips tagging (uses the tenant default model), and strips `` greedily in `parseTaggerResponse`. - **Docs**: fixes a misleading `PresentationChunker` docstring that claimed per-slide `image`/`position` output (the PPTX path emits none — unlike PDF), and removes a stale `docs/migration_python_go_diff.md` reference in `media_dispatch.go`. ## Test plan - `bash build.sh --test ./internal/common/...` — passes (new retry utility + tests). - `bash build.sh --test ./internal/ingestion/component/...` — passes (extractor/chunker/schema). - `gofmt` and lefthook pre-commit checks pass. Note: the personal `docs/migration_python_go_diff.md` working notebook in the tree is intentionally **not** part of this PR. --------- Co-authored-by: CodeBuddy <noreply@codebuddy.ai>
2026-07-28 19:22:18 +08:00
req := extractorChatRequest{
Driver: driver,
ModelName: modelName,
APIKey: apiKey,
BaseURL: baseURL,
Messages: msgs,
fix(ingestion): improve Extractor/LLM robustness and Python->Go parity (#17470) ## Summary This PR hardens the Go ingestion **Extractor** and **LLM retry** paths and closes several Python->Go parity gaps in the keyword/question/tag extraction flow. - **Generic retry utility** (`internal/common/retry.go`): `RetryWithBackoff` with exponential backoff (default 3 retries, 2s initial delay, capped at 1m), context-aware sleep, and a `maxRetries<=0` fast path. Covered by `internal/common/retry_test.go`. - **LLM retry reuse**: `agent/component/llm_retry.go` now delegates to `common.RetryWithBackoff` instead of an inline loop (behavior preserved: ctx cancellation short-circuits the backoff). - **Extractor LLM calls** (`extractor.go`): - `call()` now retries transient LLM failures via `RetryWithBackoff` (retry exhaustion fails the chunk instead of silently skipping). - Sets `temperature = 0.2`, matching Python `generator.py:230,245`. - Runs keyword and question extraction **concurrently** per chunk when both are enabled (`task_executor.py:444-448`), with mutex-guarded map writes to avoid data races. - Substitutes `{field_name}` placeholders (including `{chunks}` -> chunk text) in `prompt`/`system_prompt` before the call, mirroring Python `string_format` (`extractor.py:102-103`); unmatched placeholders are left as-is. - Falls back to the **tenant default chat model** when `llm_id` is empty (`task_executor.py:573-574`). - Strips `` **greedily** (`strings.LastIndex`) in `cleanExtractionResult`. - **Auto-tagging** (`extractor_tag.go`): drops the `in.llmID != ""` guards so an empty `llm_id` no longer skips tagging (uses the tenant default model), and strips `` greedily in `parseTaggerResponse`. - **Docs**: fixes a misleading `PresentationChunker` docstring that claimed per-slide `image`/`position` output (the PPTX path emits none — unlike PDF), and removes a stale `docs/migration_python_go_diff.md` reference in `media_dispatch.go`. ## Test plan - `bash build.sh --test ./internal/common/...` — passes (new retry utility + tests). - `bash build.sh --test ./internal/ingestion/component/...` — passes (extractor/chunker/schema). - `gofmt` and lefthook pre-commit checks pass. Note: the personal `docs/migration_python_go_diff.md` working notebook in the tree is intentionally **not** part of this PR. --------- Co-authored-by: CodeBuddy <noreply@codebuddy.ai>
2026-07-28 19:22:18 +08:00
}
// Only override the temperature when the caller set one. A nil
// Temperature lets the model / chat-model default decide, matching
// Python's generic Extractor path; keyword/question helpers set
// extractorTemperature (0.2) to mirror generator.py.
if in.temperature != nil {
temp := *in.temperature
req.Temperature = &temp
}
var resp *extractorChatResponse
if err := common.RetryWithBackoff(ctx, extractorRetryMax, extractorRetryDelay, func() error {
r, e := inv.Chat(ctx, req)
resp = r
return e
}, isRetryableLLMError); err != nil {
return nil, err
}
raw := strings.TrimSpace(resp.Content)
if raw == "" {
// No response — emit empty string so downstream code
// can distinguish from "LLM errored" via the error
// path above.
return "", nil
}
// Best-effort JSON parse: a JSON object response is the
// canonical structured-extraction shape. Other shapes are
// returned verbatim so the caller can decide.
if parsed, ok := tryParseJSONObject(raw); ok {
return parsed, nil
}
return raw, nil
}
fix(ingestion): align laws DSL with Python — heading fallback, colon-title, short-line filter, remove_toc, and image extension mapping (#17200) ## Summary This PR aligns the Go ingestion pipeline's **Laws** DSL template with the Python implementation by fixing heading-detection gaps, adds image-extension support, refactors the **Extractor** component's LLM resolution, hardens heading detection for CJK text, and makes the Extractor accept the Python DSL prompt key names (`sys_prompt`/`prompts`) alongside the Go names. ## Changes ### 1. Picture file-type detection (`internal/utility/file.go`) Adds explicit mapping for common image extensions (png, jpg, jpeg, gif, bmp, tiff, tif, webp, svg, ico, avif, heic, apng) → `FileTypeVISUAL`, with regression tests. ### 2. Laws DSL heading-detection alignment (`internal/ingestion/component/chunker/`) Four fixes to `resolveTitleLevels`: | Fix | What changed | Why | |-----|-------------|-----| | **DOCX `ck_type` fallback** | `ckType` field on `lineRecord`, propagated from `ChunkDoc.CKType` in `recordsFromStructured`. When `ck_type=="heading"`, assign `fallbackLevel`. | office_oxide extracts DOCX heading metadata, but the info was lost before reaching the heading detector. Word headings whose text doesn't match any regex (e.g. "Introduction") were treated as body. | | **`make_colon_as_title` promotion** | `isColonTitle()`: promotes lines ending with `:`/`:` that have sentence-ending punctuation before the colon and ≥32 runes between them. | Mirrors Python's `make_colon_as_title` in `rag/nlp/__init__.py`. Triple guard prevents false positives. | | **Short/numeric line filter** | Lines with ≤1 rune or purely numeric are pinned to body level. | Mirrors Python `tree_merge`'s filter of `sections` where `len(...) <= 1` or `re.match(r"[0-9]+$", ...)`. | | **PDF `remove_toc`** | `"remove_toc": true` added to the PDF parser setup in `ingestion_pipeline_laws.json`. | The Go PDF parser already supports TOC removal; the Book template already enables it. | ### 3. Extractor llm_id resolution (`internal/ingestion/component/extractor.go`) Refactored to handle both **bare tenant_model UUIDs** and **composite model@provider** strings via the shared `resolveModelConfig` (`dispatch_model.go`): - **`resolveExtractorChatConfig`** — UUID path calls `resolveModelConfigByID` directly (one DB hit); composite path goes through `resolveModelConfig`. Added `isBareTenantModelID` pre-check for clear errors when a UUID doesn't exist. - **`resolveExtractorChatTarget`** — propagates resolution errors instead of silently returning empty driver. - **`Chat()`** — removed `driver = "dummy"` fallback. Missing driver is now an explicit error. - **Removed dead code**: `splitExtractorLLID`, `findExtractorSoleActiveInstance`. ### 4. `InjectExtractorLLMID` — fallback when no user config (`internal/common/parser_config.go`) Injects the tenant's global default LLM into extractor components **only when their `llm_id` is empty**. Preserves user-selected UUID or model@provider values. Priority: user-configured llm_id > tenant global default > error (no silent dummy fallback). ### 5. `ResponseHeaderTimeout` increase (`internal/entity/models/base_model.go`) `ResponseHeaderTimeout` 60s → 120s in `NewDriverHTTPClient`. Reasoning models with large extraction prompts can take longer than 60s to produce the first response token. ### 6. CJK rune-aware heading detection (`internal/ingestion/component/chunker/title.go`) Two byte-vs-rune bugs that only manifest on CJK text: | Fix | What changed | Why | |-----|-------------|-----| | **`isColonTitle` byte offset** | `body[lastPunct+1:]` → `body[lastPunct+runeLen:]` via `utf8.DecodeRuneInString` | `strings.LastIndexAny` returns a byte index; `+1` skips only 1 byte, corrupting multi-byte CJK punctuation (e.g. `。` = 3 bytes) and inflating the rune count past the 32-rune threshold → false-positive heading promotion. | | **Short-line filter byte count** | `len(text) <= 1` → `utf8.RuneCountInString(text) <= 1` | Go `len` is UTF-8 bytes; a single CJK char (3 bytes) passed the filter, but Python's `len` returns 1 → mismatch. | ### 7. `extractor_tag.go` — log error when llm fails When `resolveExtractorChatTarget` returned an error, `runAutoTags` will log error. ### 8. Python DSL prompt-key compatibility (`internal/ingestion/component/extractor.go`) The Resume DSL template uses Python-side key names (`sys_prompt`, `prompts`). `NewExtractorComponent` now accepts them as fallbacks alongside the Go names: - `system_prompt` (Go) ← `sys_prompt` (Python) as fallback - `prompt` (Go string) ← `prompts` (Python array `[{"role","content"}]`, takes `[0].content`) as fallback Mirrors the alias pattern already in `internal/agent/component/llm.go`. `resolveInputs` accepts per-call `sys_prompt` override too. ## Remaining gaps vs Python | Gap | Scope | Impact | |-----|-------|--------| | **TOC removal for TXT/MD/HTML** | Python's `remove_contents_table` works on all text formats; Go's `remove_toc` is PDF-only. | Low — plain-text documents rarely contain structured TOCs. | | **Regex pattern details** | Minor differences in quantifiers, missing H5/H6 markdown patterns, missing 4-level numbering pattern. | Low — Go's variants are stricter; DOCX headings are covered by `ck_type` fallback. | ## Testing - `TestHierarchyTitleChunker_CKTypeHeadingFallback` — DOCX `ck_type` heading promotion - `TestHierarchyTitleChunker_ColonTitlePromotion` / `_ColonTitleShortLine_Negative` — colon-title promotion + guard - `TestIsColonTitle_CJKEdgeCase` / `TestIsColonTitle_ASCII_NoRegression` — CJK byte-offset fix + ASCII regression - `TestHierarchyTitleChunker_ColonTitlePromotion_CJK_EdgeCase` — CJK colon edge case through full pipeline - `TestHierarchyTitleChunker_ShortSingleCJKLineFilter` — single CJK char filtered to body - `TestHierarchyTitleChunker_ShortNumericLineFilter` — purely numeric lines filtered - `TestGetFileType_ImageExtensions` / `_ExistingFormats_NoRegression` — image extension mapping - `TestInjectExtractorLLMID_SkipWhenUUID` / `_SkipWhenComposite` / `_InjectWhenEmpty` — llm_id injection guard - `TestIsBareTenantModelID` — UUID detection - `TestResolveExtractorChatTarget_AtSplitFallback` / `_NoDriver` — @ split fallback without DB - `TestNewExtractorComponent_SysPromptAlias` / `_PromptsArray` / `_PromptsArray_PromptWins` / `_SystemPromptWinsOverSysPrompt` — Python key compatibility - `TestBuildDOCXJSONSections_List` / `_TextBox` / `_MixedWithList` — DOCX list/text_box parsing - Full ingestion test suite passes (chunker, pipeline, task, service, component packages)
2026-07-22 19:14:32 +08:00
// resolveExtractorChatTarget resolves the llm_id into driver / model /
// api_key / base_url. The llm_id may be a bare tenant_model UUID or
// a composite "model@provider" string. Errors from DAO resolution are
// propagated so the caller sees the real failure reason.
func resolveExtractorChatTarget(ctx context.Context, db *gorm.DB, llmID string) (driver, modelName, apiKey, baseURL string, err error) {
if override := getExtractorChatTargetResolverOverride(); override != nil {
if driver, modelName, apiKey, baseURL, ok := override(llmID); ok {
fix(ingestion): align laws DSL with Python — heading fallback, colon-title, short-line filter, remove_toc, and image extension mapping (#17200) ## Summary This PR aligns the Go ingestion pipeline's **Laws** DSL template with the Python implementation by fixing heading-detection gaps, adds image-extension support, refactors the **Extractor** component's LLM resolution, hardens heading detection for CJK text, and makes the Extractor accept the Python DSL prompt key names (`sys_prompt`/`prompts`) alongside the Go names. ## Changes ### 1. Picture file-type detection (`internal/utility/file.go`) Adds explicit mapping for common image extensions (png, jpg, jpeg, gif, bmp, tiff, tif, webp, svg, ico, avif, heic, apng) → `FileTypeVISUAL`, with regression tests. ### 2. Laws DSL heading-detection alignment (`internal/ingestion/component/chunker/`) Four fixes to `resolveTitleLevels`: | Fix | What changed | Why | |-----|-------------|-----| | **DOCX `ck_type` fallback** | `ckType` field on `lineRecord`, propagated from `ChunkDoc.CKType` in `recordsFromStructured`. When `ck_type=="heading"`, assign `fallbackLevel`. | office_oxide extracts DOCX heading metadata, but the info was lost before reaching the heading detector. Word headings whose text doesn't match any regex (e.g. "Introduction") were treated as body. | | **`make_colon_as_title` promotion** | `isColonTitle()`: promotes lines ending with `:`/`:` that have sentence-ending punctuation before the colon and ≥32 runes between them. | Mirrors Python's `make_colon_as_title` in `rag/nlp/__init__.py`. Triple guard prevents false positives. | | **Short/numeric line filter** | Lines with ≤1 rune or purely numeric are pinned to body level. | Mirrors Python `tree_merge`'s filter of `sections` where `len(...) <= 1` or `re.match(r"[0-9]+$", ...)`. | | **PDF `remove_toc`** | `"remove_toc": true` added to the PDF parser setup in `ingestion_pipeline_laws.json`. | The Go PDF parser already supports TOC removal; the Book template already enables it. | ### 3. Extractor llm_id resolution (`internal/ingestion/component/extractor.go`) Refactored to handle both **bare tenant_model UUIDs** and **composite model@provider** strings via the shared `resolveModelConfig` (`dispatch_model.go`): - **`resolveExtractorChatConfig`** — UUID path calls `resolveModelConfigByID` directly (one DB hit); composite path goes through `resolveModelConfig`. Added `isBareTenantModelID` pre-check for clear errors when a UUID doesn't exist. - **`resolveExtractorChatTarget`** — propagates resolution errors instead of silently returning empty driver. - **`Chat()`** — removed `driver = "dummy"` fallback. Missing driver is now an explicit error. - **Removed dead code**: `splitExtractorLLID`, `findExtractorSoleActiveInstance`. ### 4. `InjectExtractorLLMID` — fallback when no user config (`internal/common/parser_config.go`) Injects the tenant's global default LLM into extractor components **only when their `llm_id` is empty**. Preserves user-selected UUID or model@provider values. Priority: user-configured llm_id > tenant global default > error (no silent dummy fallback). ### 5. `ResponseHeaderTimeout` increase (`internal/entity/models/base_model.go`) `ResponseHeaderTimeout` 60s → 120s in `NewDriverHTTPClient`. Reasoning models with large extraction prompts can take longer than 60s to produce the first response token. ### 6. CJK rune-aware heading detection (`internal/ingestion/component/chunker/title.go`) Two byte-vs-rune bugs that only manifest on CJK text: | Fix | What changed | Why | |-----|-------------|-----| | **`isColonTitle` byte offset** | `body[lastPunct+1:]` → `body[lastPunct+runeLen:]` via `utf8.DecodeRuneInString` | `strings.LastIndexAny` returns a byte index; `+1` skips only 1 byte, corrupting multi-byte CJK punctuation (e.g. `。` = 3 bytes) and inflating the rune count past the 32-rune threshold → false-positive heading promotion. | | **Short-line filter byte count** | `len(text) <= 1` → `utf8.RuneCountInString(text) <= 1` | Go `len` is UTF-8 bytes; a single CJK char (3 bytes) passed the filter, but Python's `len` returns 1 → mismatch. | ### 7. `extractor_tag.go` — log error when llm fails When `resolveExtractorChatTarget` returned an error, `runAutoTags` will log error. ### 8. Python DSL prompt-key compatibility (`internal/ingestion/component/extractor.go`) The Resume DSL template uses Python-side key names (`sys_prompt`, `prompts`). `NewExtractorComponent` now accepts them as fallbacks alongside the Go names: - `system_prompt` (Go) ← `sys_prompt` (Python) as fallback - `prompt` (Go string) ← `prompts` (Python array `[{"role","content"}]`, takes `[0].content`) as fallback Mirrors the alias pattern already in `internal/agent/component/llm.go`. `resolveInputs` accepts per-call `sys_prompt` override too. ## Remaining gaps vs Python | Gap | Scope | Impact | |-----|-------|--------| | **TOC removal for TXT/MD/HTML** | Python's `remove_contents_table` works on all text formats; Go's `remove_toc` is PDF-only. | Low — plain-text documents rarely contain structured TOCs. | | **Regex pattern details** | Minor differences in quantifiers, missing H5/H6 markdown patterns, missing 4-level numbering pattern. | Low — Go's variants are stricter; DOCX headings are covered by `ck_type` fallback. | ## Testing - `TestHierarchyTitleChunker_CKTypeHeadingFallback` — DOCX `ck_type` heading promotion - `TestHierarchyTitleChunker_ColonTitlePromotion` / `_ColonTitleShortLine_Negative` — colon-title promotion + guard - `TestIsColonTitle_CJKEdgeCase` / `TestIsColonTitle_ASCII_NoRegression` — CJK byte-offset fix + ASCII regression - `TestHierarchyTitleChunker_ColonTitlePromotion_CJK_EdgeCase` — CJK colon edge case through full pipeline - `TestHierarchyTitleChunker_ShortSingleCJKLineFilter` — single CJK char filtered to body - `TestHierarchyTitleChunker_ShortNumericLineFilter` — purely numeric lines filtered - `TestGetFileType_ImageExtensions` / `_ExistingFormats_NoRegression` — image extension mapping - `TestInjectExtractorLLMID_SkipWhenUUID` / `_SkipWhenComposite` / `_InjectWhenEmpty` — llm_id injection guard - `TestIsBareTenantModelID` — UUID detection - `TestResolveExtractorChatTarget_AtSplitFallback` / `_NoDriver` — @ split fallback without DB - `TestNewExtractorComponent_SysPromptAlias` / `_PromptsArray` / `_PromptsArray_PromptWins` / `_SystemPromptWinsOverSysPrompt` — Python key compatibility - `TestBuildDOCXJSONSections_List` / `_TextBox` / `_MixedWithList` — DOCX list/text_box parsing - Full ingestion test suite passes (chunker, pipeline, task, service, component packages)
2026-07-22 19:14:32 +08:00
return driver, modelName, apiKey, baseURL, nil
}
}
fix(ingestion): improve Extractor/LLM robustness and Python->Go parity (#17470) ## Summary This PR hardens the Go ingestion **Extractor** and **LLM retry** paths and closes several Python->Go parity gaps in the keyword/question/tag extraction flow. - **Generic retry utility** (`internal/common/retry.go`): `RetryWithBackoff` with exponential backoff (default 3 retries, 2s initial delay, capped at 1m), context-aware sleep, and a `maxRetries<=0` fast path. Covered by `internal/common/retry_test.go`. - **LLM retry reuse**: `agent/component/llm_retry.go` now delegates to `common.RetryWithBackoff` instead of an inline loop (behavior preserved: ctx cancellation short-circuits the backoff). - **Extractor LLM calls** (`extractor.go`): - `call()` now retries transient LLM failures via `RetryWithBackoff` (retry exhaustion fails the chunk instead of silently skipping). - Sets `temperature = 0.2`, matching Python `generator.py:230,245`. - Runs keyword and question extraction **concurrently** per chunk when both are enabled (`task_executor.py:444-448`), with mutex-guarded map writes to avoid data races. - Substitutes `{field_name}` placeholders (including `{chunks}` -> chunk text) in `prompt`/`system_prompt` before the call, mirroring Python `string_format` (`extractor.py:102-103`); unmatched placeholders are left as-is. - Falls back to the **tenant default chat model** when `llm_id` is empty (`task_executor.py:573-574`). - Strips `` **greedily** (`strings.LastIndex`) in `cleanExtractionResult`. - **Auto-tagging** (`extractor_tag.go`): drops the `in.llmID != ""` guards so an empty `llm_id` no longer skips tagging (uses the tenant default model), and strips `` greedily in `parseTaggerResponse`. - **Docs**: fixes a misleading `PresentationChunker` docstring that claimed per-slide `image`/`position` output (the PPTX path emits none — unlike PDF), and removes a stale `docs/migration_python_go_diff.md` reference in `media_dispatch.go`. ## Test plan - `bash build.sh --test ./internal/common/...` — passes (new retry utility + tests). - `bash build.sh --test ./internal/ingestion/component/...` — passes (extractor/chunker/schema). - `gofmt` and lefthook pre-commit checks pass. Note: the personal `docs/migration_python_go_diff.md` working notebook in the tree is intentionally **not** part of this PR. --------- Co-authored-by: CodeBuddy <noreply@codebuddy.ai>
2026-07-28 19:22:18 +08:00
// When llmID is empty, try tenant default chat model
// (mirrors Python task_executor.py:573-574 fallback).
if llmID == "" {
if cfg := resolveExtractorChatDefaultConfig(ctx, db); cfg.driver != "" {
return cfg.driver, cfg.modelName, cfg.apiKey, cfg.baseURL, nil
}
}
cfg, cfgErr := resolveExtractorChatConfig(ctx, db, llmID)
fix(ingestion): align laws DSL with Python — heading fallback, colon-title, short-line filter, remove_toc, and image extension mapping (#17200) ## Summary This PR aligns the Go ingestion pipeline's **Laws** DSL template with the Python implementation by fixing heading-detection gaps, adds image-extension support, refactors the **Extractor** component's LLM resolution, hardens heading detection for CJK text, and makes the Extractor accept the Python DSL prompt key names (`sys_prompt`/`prompts`) alongside the Go names. ## Changes ### 1. Picture file-type detection (`internal/utility/file.go`) Adds explicit mapping for common image extensions (png, jpg, jpeg, gif, bmp, tiff, tif, webp, svg, ico, avif, heic, apng) → `FileTypeVISUAL`, with regression tests. ### 2. Laws DSL heading-detection alignment (`internal/ingestion/component/chunker/`) Four fixes to `resolveTitleLevels`: | Fix | What changed | Why | |-----|-------------|-----| | **DOCX `ck_type` fallback** | `ckType` field on `lineRecord`, propagated from `ChunkDoc.CKType` in `recordsFromStructured`. When `ck_type=="heading"`, assign `fallbackLevel`. | office_oxide extracts DOCX heading metadata, but the info was lost before reaching the heading detector. Word headings whose text doesn't match any regex (e.g. "Introduction") were treated as body. | | **`make_colon_as_title` promotion** | `isColonTitle()`: promotes lines ending with `:`/`:` that have sentence-ending punctuation before the colon and ≥32 runes between them. | Mirrors Python's `make_colon_as_title` in `rag/nlp/__init__.py`. Triple guard prevents false positives. | | **Short/numeric line filter** | Lines with ≤1 rune or purely numeric are pinned to body level. | Mirrors Python `tree_merge`'s filter of `sections` where `len(...) <= 1` or `re.match(r"[0-9]+$", ...)`. | | **PDF `remove_toc`** | `"remove_toc": true` added to the PDF parser setup in `ingestion_pipeline_laws.json`. | The Go PDF parser already supports TOC removal; the Book template already enables it. | ### 3. Extractor llm_id resolution (`internal/ingestion/component/extractor.go`) Refactored to handle both **bare tenant_model UUIDs** and **composite model@provider** strings via the shared `resolveModelConfig` (`dispatch_model.go`): - **`resolveExtractorChatConfig`** — UUID path calls `resolveModelConfigByID` directly (one DB hit); composite path goes through `resolveModelConfig`. Added `isBareTenantModelID` pre-check for clear errors when a UUID doesn't exist. - **`resolveExtractorChatTarget`** — propagates resolution errors instead of silently returning empty driver. - **`Chat()`** — removed `driver = "dummy"` fallback. Missing driver is now an explicit error. - **Removed dead code**: `splitExtractorLLID`, `findExtractorSoleActiveInstance`. ### 4. `InjectExtractorLLMID` — fallback when no user config (`internal/common/parser_config.go`) Injects the tenant's global default LLM into extractor components **only when their `llm_id` is empty**. Preserves user-selected UUID or model@provider values. Priority: user-configured llm_id > tenant global default > error (no silent dummy fallback). ### 5. `ResponseHeaderTimeout` increase (`internal/entity/models/base_model.go`) `ResponseHeaderTimeout` 60s → 120s in `NewDriverHTTPClient`. Reasoning models with large extraction prompts can take longer than 60s to produce the first response token. ### 6. CJK rune-aware heading detection (`internal/ingestion/component/chunker/title.go`) Two byte-vs-rune bugs that only manifest on CJK text: | Fix | What changed | Why | |-----|-------------|-----| | **`isColonTitle` byte offset** | `body[lastPunct+1:]` → `body[lastPunct+runeLen:]` via `utf8.DecodeRuneInString` | `strings.LastIndexAny` returns a byte index; `+1` skips only 1 byte, corrupting multi-byte CJK punctuation (e.g. `。` = 3 bytes) and inflating the rune count past the 32-rune threshold → false-positive heading promotion. | | **Short-line filter byte count** | `len(text) <= 1` → `utf8.RuneCountInString(text) <= 1` | Go `len` is UTF-8 bytes; a single CJK char (3 bytes) passed the filter, but Python's `len` returns 1 → mismatch. | ### 7. `extractor_tag.go` — log error when llm fails When `resolveExtractorChatTarget` returned an error, `runAutoTags` will log error. ### 8. Python DSL prompt-key compatibility (`internal/ingestion/component/extractor.go`) The Resume DSL template uses Python-side key names (`sys_prompt`, `prompts`). `NewExtractorComponent` now accepts them as fallbacks alongside the Go names: - `system_prompt` (Go) ← `sys_prompt` (Python) as fallback - `prompt` (Go string) ← `prompts` (Python array `[{"role","content"}]`, takes `[0].content`) as fallback Mirrors the alias pattern already in `internal/agent/component/llm.go`. `resolveInputs` accepts per-call `sys_prompt` override too. ## Remaining gaps vs Python | Gap | Scope | Impact | |-----|-------|--------| | **TOC removal for TXT/MD/HTML** | Python's `remove_contents_table` works on all text formats; Go's `remove_toc` is PDF-only. | Low — plain-text documents rarely contain structured TOCs. | | **Regex pattern details** | Minor differences in quantifiers, missing H5/H6 markdown patterns, missing 4-level numbering pattern. | Low — Go's variants are stricter; DOCX headings are covered by `ck_type` fallback. | ## Testing - `TestHierarchyTitleChunker_CKTypeHeadingFallback` — DOCX `ck_type` heading promotion - `TestHierarchyTitleChunker_ColonTitlePromotion` / `_ColonTitleShortLine_Negative` — colon-title promotion + guard - `TestIsColonTitle_CJKEdgeCase` / `TestIsColonTitle_ASCII_NoRegression` — CJK byte-offset fix + ASCII regression - `TestHierarchyTitleChunker_ColonTitlePromotion_CJK_EdgeCase` — CJK colon edge case through full pipeline - `TestHierarchyTitleChunker_ShortSingleCJKLineFilter` — single CJK char filtered to body - `TestHierarchyTitleChunker_ShortNumericLineFilter` — purely numeric lines filtered - `TestGetFileType_ImageExtensions` / `_ExistingFormats_NoRegression` — image extension mapping - `TestInjectExtractorLLMID_SkipWhenUUID` / `_SkipWhenComposite` / `_InjectWhenEmpty` — llm_id injection guard - `TestIsBareTenantModelID` — UUID detection - `TestResolveExtractorChatTarget_AtSplitFallback` / `_NoDriver` — @ split fallback without DB - `TestNewExtractorComponent_SysPromptAlias` / `_PromptsArray` / `_PromptsArray_PromptWins` / `_SystemPromptWinsOverSysPrompt` — Python key compatibility - `TestBuildDOCXJSONSections_List` / `_TextBox` / `_MixedWithList` — DOCX list/text_box parsing - Full ingestion test suite passes (chunker, pipeline, task, service, component packages)
2026-07-22 19:14:32 +08:00
if cfgErr != nil {
return "", "", "", "", cfgErr
}
if cfg.driver != "" {
fix(ingestion): align laws DSL with Python — heading fallback, colon-title, short-line filter, remove_toc, and image extension mapping (#17200) ## Summary This PR aligns the Go ingestion pipeline's **Laws** DSL template with the Python implementation by fixing heading-detection gaps, adds image-extension support, refactors the **Extractor** component's LLM resolution, hardens heading detection for CJK text, and makes the Extractor accept the Python DSL prompt key names (`sys_prompt`/`prompts`) alongside the Go names. ## Changes ### 1. Picture file-type detection (`internal/utility/file.go`) Adds explicit mapping for common image extensions (png, jpg, jpeg, gif, bmp, tiff, tif, webp, svg, ico, avif, heic, apng) → `FileTypeVISUAL`, with regression tests. ### 2. Laws DSL heading-detection alignment (`internal/ingestion/component/chunker/`) Four fixes to `resolveTitleLevels`: | Fix | What changed | Why | |-----|-------------|-----| | **DOCX `ck_type` fallback** | `ckType` field on `lineRecord`, propagated from `ChunkDoc.CKType` in `recordsFromStructured`. When `ck_type=="heading"`, assign `fallbackLevel`. | office_oxide extracts DOCX heading metadata, but the info was lost before reaching the heading detector. Word headings whose text doesn't match any regex (e.g. "Introduction") were treated as body. | | **`make_colon_as_title` promotion** | `isColonTitle()`: promotes lines ending with `:`/`:` that have sentence-ending punctuation before the colon and ≥32 runes between them. | Mirrors Python's `make_colon_as_title` in `rag/nlp/__init__.py`. Triple guard prevents false positives. | | **Short/numeric line filter** | Lines with ≤1 rune or purely numeric are pinned to body level. | Mirrors Python `tree_merge`'s filter of `sections` where `len(...) <= 1` or `re.match(r"[0-9]+$", ...)`. | | **PDF `remove_toc`** | `"remove_toc": true` added to the PDF parser setup in `ingestion_pipeline_laws.json`. | The Go PDF parser already supports TOC removal; the Book template already enables it. | ### 3. Extractor llm_id resolution (`internal/ingestion/component/extractor.go`) Refactored to handle both **bare tenant_model UUIDs** and **composite model@provider** strings via the shared `resolveModelConfig` (`dispatch_model.go`): - **`resolveExtractorChatConfig`** — UUID path calls `resolveModelConfigByID` directly (one DB hit); composite path goes through `resolveModelConfig`. Added `isBareTenantModelID` pre-check for clear errors when a UUID doesn't exist. - **`resolveExtractorChatTarget`** — propagates resolution errors instead of silently returning empty driver. - **`Chat()`** — removed `driver = "dummy"` fallback. Missing driver is now an explicit error. - **Removed dead code**: `splitExtractorLLID`, `findExtractorSoleActiveInstance`. ### 4. `InjectExtractorLLMID` — fallback when no user config (`internal/common/parser_config.go`) Injects the tenant's global default LLM into extractor components **only when their `llm_id` is empty**. Preserves user-selected UUID or model@provider values. Priority: user-configured llm_id > tenant global default > error (no silent dummy fallback). ### 5. `ResponseHeaderTimeout` increase (`internal/entity/models/base_model.go`) `ResponseHeaderTimeout` 60s → 120s in `NewDriverHTTPClient`. Reasoning models with large extraction prompts can take longer than 60s to produce the first response token. ### 6. CJK rune-aware heading detection (`internal/ingestion/component/chunker/title.go`) Two byte-vs-rune bugs that only manifest on CJK text: | Fix | What changed | Why | |-----|-------------|-----| | **`isColonTitle` byte offset** | `body[lastPunct+1:]` → `body[lastPunct+runeLen:]` via `utf8.DecodeRuneInString` | `strings.LastIndexAny` returns a byte index; `+1` skips only 1 byte, corrupting multi-byte CJK punctuation (e.g. `。` = 3 bytes) and inflating the rune count past the 32-rune threshold → false-positive heading promotion. | | **Short-line filter byte count** | `len(text) <= 1` → `utf8.RuneCountInString(text) <= 1` | Go `len` is UTF-8 bytes; a single CJK char (3 bytes) passed the filter, but Python's `len` returns 1 → mismatch. | ### 7. `extractor_tag.go` — log error when llm fails When `resolveExtractorChatTarget` returned an error, `runAutoTags` will log error. ### 8. Python DSL prompt-key compatibility (`internal/ingestion/component/extractor.go`) The Resume DSL template uses Python-side key names (`sys_prompt`, `prompts`). `NewExtractorComponent` now accepts them as fallbacks alongside the Go names: - `system_prompt` (Go) ← `sys_prompt` (Python) as fallback - `prompt` (Go string) ← `prompts` (Python array `[{"role","content"}]`, takes `[0].content`) as fallback Mirrors the alias pattern already in `internal/agent/component/llm.go`. `resolveInputs` accepts per-call `sys_prompt` override too. ## Remaining gaps vs Python | Gap | Scope | Impact | |-----|-------|--------| | **TOC removal for TXT/MD/HTML** | Python's `remove_contents_table` works on all text formats; Go's `remove_toc` is PDF-only. | Low — plain-text documents rarely contain structured TOCs. | | **Regex pattern details** | Minor differences in quantifiers, missing H5/H6 markdown patterns, missing 4-level numbering pattern. | Low — Go's variants are stricter; DOCX headings are covered by `ck_type` fallback. | ## Testing - `TestHierarchyTitleChunker_CKTypeHeadingFallback` — DOCX `ck_type` heading promotion - `TestHierarchyTitleChunker_ColonTitlePromotion` / `_ColonTitleShortLine_Negative` — colon-title promotion + guard - `TestIsColonTitle_CJKEdgeCase` / `TestIsColonTitle_ASCII_NoRegression` — CJK byte-offset fix + ASCII regression - `TestHierarchyTitleChunker_ColonTitlePromotion_CJK_EdgeCase` — CJK colon edge case through full pipeline - `TestHierarchyTitleChunker_ShortSingleCJKLineFilter` — single CJK char filtered to body - `TestHierarchyTitleChunker_ShortNumericLineFilter` — purely numeric lines filtered - `TestGetFileType_ImageExtensions` / `_ExistingFormats_NoRegression` — image extension mapping - `TestInjectExtractorLLMID_SkipWhenUUID` / `_SkipWhenComposite` / `_InjectWhenEmpty` — llm_id injection guard - `TestIsBareTenantModelID` — UUID detection - `TestResolveExtractorChatTarget_AtSplitFallback` / `_NoDriver` — @ split fallback without DB - `TestNewExtractorComponent_SysPromptAlias` / `_PromptsArray` / `_PromptsArray_PromptWins` / `_SystemPromptWinsOverSysPrompt` — Python key compatibility - `TestBuildDOCXJSONSections_List` / `_TextBox` / `_MixedWithList` — DOCX list/text_box parsing - Full ingestion test suite passes (chunker, pipeline, task, service, component packages)
2026-07-22 19:14:32 +08:00
return cfg.driver, cfg.modelName, cfg.apiKey, cfg.baseURL, nil
}
fix(ingestion): align laws DSL with Python — heading fallback, colon-title, short-line filter, remove_toc, and image extension mapping (#17200) ## Summary This PR aligns the Go ingestion pipeline's **Laws** DSL template with the Python implementation by fixing heading-detection gaps, adds image-extension support, refactors the **Extractor** component's LLM resolution, hardens heading detection for CJK text, and makes the Extractor accept the Python DSL prompt key names (`sys_prompt`/`prompts`) alongside the Go names. ## Changes ### 1. Picture file-type detection (`internal/utility/file.go`) Adds explicit mapping for common image extensions (png, jpg, jpeg, gif, bmp, tiff, tif, webp, svg, ico, avif, heic, apng) → `FileTypeVISUAL`, with regression tests. ### 2. Laws DSL heading-detection alignment (`internal/ingestion/component/chunker/`) Four fixes to `resolveTitleLevels`: | Fix | What changed | Why | |-----|-------------|-----| | **DOCX `ck_type` fallback** | `ckType` field on `lineRecord`, propagated from `ChunkDoc.CKType` in `recordsFromStructured`. When `ck_type=="heading"`, assign `fallbackLevel`. | office_oxide extracts DOCX heading metadata, but the info was lost before reaching the heading detector. Word headings whose text doesn't match any regex (e.g. "Introduction") were treated as body. | | **`make_colon_as_title` promotion** | `isColonTitle()`: promotes lines ending with `:`/`:` that have sentence-ending punctuation before the colon and ≥32 runes between them. | Mirrors Python's `make_colon_as_title` in `rag/nlp/__init__.py`. Triple guard prevents false positives. | | **Short/numeric line filter** | Lines with ≤1 rune or purely numeric are pinned to body level. | Mirrors Python `tree_merge`'s filter of `sections` where `len(...) <= 1` or `re.match(r"[0-9]+$", ...)`. | | **PDF `remove_toc`** | `"remove_toc": true` added to the PDF parser setup in `ingestion_pipeline_laws.json`. | The Go PDF parser already supports TOC removal; the Book template already enables it. | ### 3. Extractor llm_id resolution (`internal/ingestion/component/extractor.go`) Refactored to handle both **bare tenant_model UUIDs** and **composite model@provider** strings via the shared `resolveModelConfig` (`dispatch_model.go`): - **`resolveExtractorChatConfig`** — UUID path calls `resolveModelConfigByID` directly (one DB hit); composite path goes through `resolveModelConfig`. Added `isBareTenantModelID` pre-check for clear errors when a UUID doesn't exist. - **`resolveExtractorChatTarget`** — propagates resolution errors instead of silently returning empty driver. - **`Chat()`** — removed `driver = "dummy"` fallback. Missing driver is now an explicit error. - **Removed dead code**: `splitExtractorLLID`, `findExtractorSoleActiveInstance`. ### 4. `InjectExtractorLLMID` — fallback when no user config (`internal/common/parser_config.go`) Injects the tenant's global default LLM into extractor components **only when their `llm_id` is empty**. Preserves user-selected UUID or model@provider values. Priority: user-configured llm_id > tenant global default > error (no silent dummy fallback). ### 5. `ResponseHeaderTimeout` increase (`internal/entity/models/base_model.go`) `ResponseHeaderTimeout` 60s → 120s in `NewDriverHTTPClient`. Reasoning models with large extraction prompts can take longer than 60s to produce the first response token. ### 6. CJK rune-aware heading detection (`internal/ingestion/component/chunker/title.go`) Two byte-vs-rune bugs that only manifest on CJK text: | Fix | What changed | Why | |-----|-------------|-----| | **`isColonTitle` byte offset** | `body[lastPunct+1:]` → `body[lastPunct+runeLen:]` via `utf8.DecodeRuneInString` | `strings.LastIndexAny` returns a byte index; `+1` skips only 1 byte, corrupting multi-byte CJK punctuation (e.g. `。` = 3 bytes) and inflating the rune count past the 32-rune threshold → false-positive heading promotion. | | **Short-line filter byte count** | `len(text) <= 1` → `utf8.RuneCountInString(text) <= 1` | Go `len` is UTF-8 bytes; a single CJK char (3 bytes) passed the filter, but Python's `len` returns 1 → mismatch. | ### 7. `extractor_tag.go` — log error when llm fails When `resolveExtractorChatTarget` returned an error, `runAutoTags` will log error. ### 8. Python DSL prompt-key compatibility (`internal/ingestion/component/extractor.go`) The Resume DSL template uses Python-side key names (`sys_prompt`, `prompts`). `NewExtractorComponent` now accepts them as fallbacks alongside the Go names: - `system_prompt` (Go) ← `sys_prompt` (Python) as fallback - `prompt` (Go string) ← `prompts` (Python array `[{"role","content"}]`, takes `[0].content`) as fallback Mirrors the alias pattern already in `internal/agent/component/llm.go`. `resolveInputs` accepts per-call `sys_prompt` override too. ## Remaining gaps vs Python | Gap | Scope | Impact | |-----|-------|--------| | **TOC removal for TXT/MD/HTML** | Python's `remove_contents_table` works on all text formats; Go's `remove_toc` is PDF-only. | Low — plain-text documents rarely contain structured TOCs. | | **Regex pattern details** | Minor differences in quantifiers, missing H5/H6 markdown patterns, missing 4-level numbering pattern. | Low — Go's variants are stricter; DOCX headings are covered by `ck_type` fallback. | ## Testing - `TestHierarchyTitleChunker_CKTypeHeadingFallback` — DOCX `ck_type` heading promotion - `TestHierarchyTitleChunker_ColonTitlePromotion` / `_ColonTitleShortLine_Negative` — colon-title promotion + guard - `TestIsColonTitle_CJKEdgeCase` / `TestIsColonTitle_ASCII_NoRegression` — CJK byte-offset fix + ASCII regression - `TestHierarchyTitleChunker_ColonTitlePromotion_CJK_EdgeCase` — CJK colon edge case through full pipeline - `TestHierarchyTitleChunker_ShortSingleCJKLineFilter` — single CJK char filtered to body - `TestHierarchyTitleChunker_ShortNumericLineFilter` — purely numeric lines filtered - `TestGetFileType_ImageExtensions` / `_ExistingFormats_NoRegression` — image extension mapping - `TestInjectExtractorLLMID_SkipWhenUUID` / `_SkipWhenComposite` / `_InjectWhenEmpty` — llm_id injection guard - `TestIsBareTenantModelID` — UUID detection - `TestResolveExtractorChatTarget_AtSplitFallback` / `_NoDriver` — @ split fallback without DB - `TestNewExtractorComponent_SysPromptAlias` / `_PromptsArray` / `_PromptsArray_PromptWins` / `_SystemPromptWinsOverSysPrompt` — Python key compatibility - `TestBuildDOCXJSONSections_List` / `_TextBox` / `_MixedWithList` — DOCX list/text_box parsing - Full ingestion test suite passes (chunker, pipeline, task, service, component packages)
2026-07-22 19:14:32 +08:00
// Fallback: when tenant credentials are not available
// (no canvas state / no DB), fall back to the basic @ split
// so callers can still use model@provider format in tests.
if bare, provider, ok := splitExtractorLLIDPair(llmID); ok {
return strings.ToLower(provider), bare, "", "", nil
}
// Nothing left to try — let Chat() surface a clear error when
// the driver ends up empty.
return "", llmID, "", "", nil
}
// extractorChatConfig holds the resolved chat model configuration.
type extractorChatConfig struct {
driver string // llm_factory
modelName string // llm_name
apiKey string
baseURL string // api_base
}
// resolveExtractorChatConfig resolves tenant-scoped credentials for
fix(ingestion): align laws DSL with Python — heading fallback, colon-title, short-line filter, remove_toc, and image extension mapping (#17200) ## Summary This PR aligns the Go ingestion pipeline's **Laws** DSL template with the Python implementation by fixing heading-detection gaps, adds image-extension support, refactors the **Extractor** component's LLM resolution, hardens heading detection for CJK text, and makes the Extractor accept the Python DSL prompt key names (`sys_prompt`/`prompts`) alongside the Go names. ## Changes ### 1. Picture file-type detection (`internal/utility/file.go`) Adds explicit mapping for common image extensions (png, jpg, jpeg, gif, bmp, tiff, tif, webp, svg, ico, avif, heic, apng) → `FileTypeVISUAL`, with regression tests. ### 2. Laws DSL heading-detection alignment (`internal/ingestion/component/chunker/`) Four fixes to `resolveTitleLevels`: | Fix | What changed | Why | |-----|-------------|-----| | **DOCX `ck_type` fallback** | `ckType` field on `lineRecord`, propagated from `ChunkDoc.CKType` in `recordsFromStructured`. When `ck_type=="heading"`, assign `fallbackLevel`. | office_oxide extracts DOCX heading metadata, but the info was lost before reaching the heading detector. Word headings whose text doesn't match any regex (e.g. "Introduction") were treated as body. | | **`make_colon_as_title` promotion** | `isColonTitle()`: promotes lines ending with `:`/`:` that have sentence-ending punctuation before the colon and ≥32 runes between them. | Mirrors Python's `make_colon_as_title` in `rag/nlp/__init__.py`. Triple guard prevents false positives. | | **Short/numeric line filter** | Lines with ≤1 rune or purely numeric are pinned to body level. | Mirrors Python `tree_merge`'s filter of `sections` where `len(...) <= 1` or `re.match(r"[0-9]+$", ...)`. | | **PDF `remove_toc`** | `"remove_toc": true` added to the PDF parser setup in `ingestion_pipeline_laws.json`. | The Go PDF parser already supports TOC removal; the Book template already enables it. | ### 3. Extractor llm_id resolution (`internal/ingestion/component/extractor.go`) Refactored to handle both **bare tenant_model UUIDs** and **composite model@provider** strings via the shared `resolveModelConfig` (`dispatch_model.go`): - **`resolveExtractorChatConfig`** — UUID path calls `resolveModelConfigByID` directly (one DB hit); composite path goes through `resolveModelConfig`. Added `isBareTenantModelID` pre-check for clear errors when a UUID doesn't exist. - **`resolveExtractorChatTarget`** — propagates resolution errors instead of silently returning empty driver. - **`Chat()`** — removed `driver = "dummy"` fallback. Missing driver is now an explicit error. - **Removed dead code**: `splitExtractorLLID`, `findExtractorSoleActiveInstance`. ### 4. `InjectExtractorLLMID` — fallback when no user config (`internal/common/parser_config.go`) Injects the tenant's global default LLM into extractor components **only when their `llm_id` is empty**. Preserves user-selected UUID or model@provider values. Priority: user-configured llm_id > tenant global default > error (no silent dummy fallback). ### 5. `ResponseHeaderTimeout` increase (`internal/entity/models/base_model.go`) `ResponseHeaderTimeout` 60s → 120s in `NewDriverHTTPClient`. Reasoning models with large extraction prompts can take longer than 60s to produce the first response token. ### 6. CJK rune-aware heading detection (`internal/ingestion/component/chunker/title.go`) Two byte-vs-rune bugs that only manifest on CJK text: | Fix | What changed | Why | |-----|-------------|-----| | **`isColonTitle` byte offset** | `body[lastPunct+1:]` → `body[lastPunct+runeLen:]` via `utf8.DecodeRuneInString` | `strings.LastIndexAny` returns a byte index; `+1` skips only 1 byte, corrupting multi-byte CJK punctuation (e.g. `。` = 3 bytes) and inflating the rune count past the 32-rune threshold → false-positive heading promotion. | | **Short-line filter byte count** | `len(text) <= 1` → `utf8.RuneCountInString(text) <= 1` | Go `len` is UTF-8 bytes; a single CJK char (3 bytes) passed the filter, but Python's `len` returns 1 → mismatch. | ### 7. `extractor_tag.go` — log error when llm fails When `resolveExtractorChatTarget` returned an error, `runAutoTags` will log error. ### 8. Python DSL prompt-key compatibility (`internal/ingestion/component/extractor.go`) The Resume DSL template uses Python-side key names (`sys_prompt`, `prompts`). `NewExtractorComponent` now accepts them as fallbacks alongside the Go names: - `system_prompt` (Go) ← `sys_prompt` (Python) as fallback - `prompt` (Go string) ← `prompts` (Python array `[{"role","content"}]`, takes `[0].content`) as fallback Mirrors the alias pattern already in `internal/agent/component/llm.go`. `resolveInputs` accepts per-call `sys_prompt` override too. ## Remaining gaps vs Python | Gap | Scope | Impact | |-----|-------|--------| | **TOC removal for TXT/MD/HTML** | Python's `remove_contents_table` works on all text formats; Go's `remove_toc` is PDF-only. | Low — plain-text documents rarely contain structured TOCs. | | **Regex pattern details** | Minor differences in quantifiers, missing H5/H6 markdown patterns, missing 4-level numbering pattern. | Low — Go's variants are stricter; DOCX headings are covered by `ck_type` fallback. | ## Testing - `TestHierarchyTitleChunker_CKTypeHeadingFallback` — DOCX `ck_type` heading promotion - `TestHierarchyTitleChunker_ColonTitlePromotion` / `_ColonTitleShortLine_Negative` — colon-title promotion + guard - `TestIsColonTitle_CJKEdgeCase` / `TestIsColonTitle_ASCII_NoRegression` — CJK byte-offset fix + ASCII regression - `TestHierarchyTitleChunker_ColonTitlePromotion_CJK_EdgeCase` — CJK colon edge case through full pipeline - `TestHierarchyTitleChunker_ShortSingleCJKLineFilter` — single CJK char filtered to body - `TestHierarchyTitleChunker_ShortNumericLineFilter` — purely numeric lines filtered - `TestGetFileType_ImageExtensions` / `_ExistingFormats_NoRegression` — image extension mapping - `TestInjectExtractorLLMID_SkipWhenUUID` / `_SkipWhenComposite` / `_InjectWhenEmpty` — llm_id injection guard - `TestIsBareTenantModelID` — UUID detection - `TestResolveExtractorChatTarget_AtSplitFallback` / `_NoDriver` — @ split fallback without DB - `TestNewExtractorComponent_SysPromptAlias` / `_PromptsArray` / `_PromptsArray_PromptWins` / `_SystemPromptWinsOverSysPrompt` — Python key compatibility - `TestBuildDOCXJSONSections_List` / `_TextBox` / `_MixedWithList` — DOCX list/text_box parsing - Full ingestion test suite passes (chunker, pipeline, task, service, component packages)
2026-07-22 19:14:32 +08:00
// the given llm_id via the shared resolveModelConfig helper.
//
// - Bare UUID → DAO lookup via resolveModelConfigByID.
// - "model@provider" → parsed via resolveModelConfigFromProviderInstance.
//
// Returns nil error when there is no canvas state (unit tests) —
// the caller's @ split fallback handles that case.
func resolveExtractorChatConfig(ctx context.Context, db *gorm.DB, compositeLLMID string) (extractorChatConfig, error) {
state, _, err := runtime.GetStateFromContext[*runtime.CanvasState](ctx)
if err != nil || state == nil {
fix(ingestion): align laws DSL with Python — heading fallback, colon-title, short-line filter, remove_toc, and image extension mapping (#17200) ## Summary This PR aligns the Go ingestion pipeline's **Laws** DSL template with the Python implementation by fixing heading-detection gaps, adds image-extension support, refactors the **Extractor** component's LLM resolution, hardens heading detection for CJK text, and makes the Extractor accept the Python DSL prompt key names (`sys_prompt`/`prompts`) alongside the Go names. ## Changes ### 1. Picture file-type detection (`internal/utility/file.go`) Adds explicit mapping for common image extensions (png, jpg, jpeg, gif, bmp, tiff, tif, webp, svg, ico, avif, heic, apng) → `FileTypeVISUAL`, with regression tests. ### 2. Laws DSL heading-detection alignment (`internal/ingestion/component/chunker/`) Four fixes to `resolveTitleLevels`: | Fix | What changed | Why | |-----|-------------|-----| | **DOCX `ck_type` fallback** | `ckType` field on `lineRecord`, propagated from `ChunkDoc.CKType` in `recordsFromStructured`. When `ck_type=="heading"`, assign `fallbackLevel`. | office_oxide extracts DOCX heading metadata, but the info was lost before reaching the heading detector. Word headings whose text doesn't match any regex (e.g. "Introduction") were treated as body. | | **`make_colon_as_title` promotion** | `isColonTitle()`: promotes lines ending with `:`/`:` that have sentence-ending punctuation before the colon and ≥32 runes between them. | Mirrors Python's `make_colon_as_title` in `rag/nlp/__init__.py`. Triple guard prevents false positives. | | **Short/numeric line filter** | Lines with ≤1 rune or purely numeric are pinned to body level. | Mirrors Python `tree_merge`'s filter of `sections` where `len(...) <= 1` or `re.match(r"[0-9]+$", ...)`. | | **PDF `remove_toc`** | `"remove_toc": true` added to the PDF parser setup in `ingestion_pipeline_laws.json`. | The Go PDF parser already supports TOC removal; the Book template already enables it. | ### 3. Extractor llm_id resolution (`internal/ingestion/component/extractor.go`) Refactored to handle both **bare tenant_model UUIDs** and **composite model@provider** strings via the shared `resolveModelConfig` (`dispatch_model.go`): - **`resolveExtractorChatConfig`** — UUID path calls `resolveModelConfigByID` directly (one DB hit); composite path goes through `resolveModelConfig`. Added `isBareTenantModelID` pre-check for clear errors when a UUID doesn't exist. - **`resolveExtractorChatTarget`** — propagates resolution errors instead of silently returning empty driver. - **`Chat()`** — removed `driver = "dummy"` fallback. Missing driver is now an explicit error. - **Removed dead code**: `splitExtractorLLID`, `findExtractorSoleActiveInstance`. ### 4. `InjectExtractorLLMID` — fallback when no user config (`internal/common/parser_config.go`) Injects the tenant's global default LLM into extractor components **only when their `llm_id` is empty**. Preserves user-selected UUID or model@provider values. Priority: user-configured llm_id > tenant global default > error (no silent dummy fallback). ### 5. `ResponseHeaderTimeout` increase (`internal/entity/models/base_model.go`) `ResponseHeaderTimeout` 60s → 120s in `NewDriverHTTPClient`. Reasoning models with large extraction prompts can take longer than 60s to produce the first response token. ### 6. CJK rune-aware heading detection (`internal/ingestion/component/chunker/title.go`) Two byte-vs-rune bugs that only manifest on CJK text: | Fix | What changed | Why | |-----|-------------|-----| | **`isColonTitle` byte offset** | `body[lastPunct+1:]` → `body[lastPunct+runeLen:]` via `utf8.DecodeRuneInString` | `strings.LastIndexAny` returns a byte index; `+1` skips only 1 byte, corrupting multi-byte CJK punctuation (e.g. `。` = 3 bytes) and inflating the rune count past the 32-rune threshold → false-positive heading promotion. | | **Short-line filter byte count** | `len(text) <= 1` → `utf8.RuneCountInString(text) <= 1` | Go `len` is UTF-8 bytes; a single CJK char (3 bytes) passed the filter, but Python's `len` returns 1 → mismatch. | ### 7. `extractor_tag.go` — log error when llm fails When `resolveExtractorChatTarget` returned an error, `runAutoTags` will log error. ### 8. Python DSL prompt-key compatibility (`internal/ingestion/component/extractor.go`) The Resume DSL template uses Python-side key names (`sys_prompt`, `prompts`). `NewExtractorComponent` now accepts them as fallbacks alongside the Go names: - `system_prompt` (Go) ← `sys_prompt` (Python) as fallback - `prompt` (Go string) ← `prompts` (Python array `[{"role","content"}]`, takes `[0].content`) as fallback Mirrors the alias pattern already in `internal/agent/component/llm.go`. `resolveInputs` accepts per-call `sys_prompt` override too. ## Remaining gaps vs Python | Gap | Scope | Impact | |-----|-------|--------| | **TOC removal for TXT/MD/HTML** | Python's `remove_contents_table` works on all text formats; Go's `remove_toc` is PDF-only. | Low — plain-text documents rarely contain structured TOCs. | | **Regex pattern details** | Minor differences in quantifiers, missing H5/H6 markdown patterns, missing 4-level numbering pattern. | Low — Go's variants are stricter; DOCX headings are covered by `ck_type` fallback. | ## Testing - `TestHierarchyTitleChunker_CKTypeHeadingFallback` — DOCX `ck_type` heading promotion - `TestHierarchyTitleChunker_ColonTitlePromotion` / `_ColonTitleShortLine_Negative` — colon-title promotion + guard - `TestIsColonTitle_CJKEdgeCase` / `TestIsColonTitle_ASCII_NoRegression` — CJK byte-offset fix + ASCII regression - `TestHierarchyTitleChunker_ColonTitlePromotion_CJK_EdgeCase` — CJK colon edge case through full pipeline - `TestHierarchyTitleChunker_ShortSingleCJKLineFilter` — single CJK char filtered to body - `TestHierarchyTitleChunker_ShortNumericLineFilter` — purely numeric lines filtered - `TestGetFileType_ImageExtensions` / `_ExistingFormats_NoRegression` — image extension mapping - `TestInjectExtractorLLMID_SkipWhenUUID` / `_SkipWhenComposite` / `_InjectWhenEmpty` — llm_id injection guard - `TestIsBareTenantModelID` — UUID detection - `TestResolveExtractorChatTarget_AtSplitFallback` / `_NoDriver` — @ split fallback without DB - `TestNewExtractorComponent_SysPromptAlias` / `_PromptsArray` / `_PromptsArray_PromptWins` / `_SystemPromptWinsOverSysPrompt` — Python key compatibility - `TestBuildDOCXJSONSections_List` / `_TextBox` / `_MixedWithList` — DOCX list/text_box parsing - Full ingestion test suite passes (chunker, pipeline, task, service, component packages)
2026-07-22 19:14:32 +08:00
return extractorChatConfig{}, nil
}
tidVal, _ := state.GetGlobal("tenant_id")
tid, _ := tidVal.(string)
if tid == "" {
fix(ingestion): align laws DSL with Python — heading fallback, colon-title, short-line filter, remove_toc, and image extension mapping (#17200) ## Summary This PR aligns the Go ingestion pipeline's **Laws** DSL template with the Python implementation by fixing heading-detection gaps, adds image-extension support, refactors the **Extractor** component's LLM resolution, hardens heading detection for CJK text, and makes the Extractor accept the Python DSL prompt key names (`sys_prompt`/`prompts`) alongside the Go names. ## Changes ### 1. Picture file-type detection (`internal/utility/file.go`) Adds explicit mapping for common image extensions (png, jpg, jpeg, gif, bmp, tiff, tif, webp, svg, ico, avif, heic, apng) → `FileTypeVISUAL`, with regression tests. ### 2. Laws DSL heading-detection alignment (`internal/ingestion/component/chunker/`) Four fixes to `resolveTitleLevels`: | Fix | What changed | Why | |-----|-------------|-----| | **DOCX `ck_type` fallback** | `ckType` field on `lineRecord`, propagated from `ChunkDoc.CKType` in `recordsFromStructured`. When `ck_type=="heading"`, assign `fallbackLevel`. | office_oxide extracts DOCX heading metadata, but the info was lost before reaching the heading detector. Word headings whose text doesn't match any regex (e.g. "Introduction") were treated as body. | | **`make_colon_as_title` promotion** | `isColonTitle()`: promotes lines ending with `:`/`:` that have sentence-ending punctuation before the colon and ≥32 runes between them. | Mirrors Python's `make_colon_as_title` in `rag/nlp/__init__.py`. Triple guard prevents false positives. | | **Short/numeric line filter** | Lines with ≤1 rune or purely numeric are pinned to body level. | Mirrors Python `tree_merge`'s filter of `sections` where `len(...) <= 1` or `re.match(r"[0-9]+$", ...)`. | | **PDF `remove_toc`** | `"remove_toc": true` added to the PDF parser setup in `ingestion_pipeline_laws.json`. | The Go PDF parser already supports TOC removal; the Book template already enables it. | ### 3. Extractor llm_id resolution (`internal/ingestion/component/extractor.go`) Refactored to handle both **bare tenant_model UUIDs** and **composite model@provider** strings via the shared `resolveModelConfig` (`dispatch_model.go`): - **`resolveExtractorChatConfig`** — UUID path calls `resolveModelConfigByID` directly (one DB hit); composite path goes through `resolveModelConfig`. Added `isBareTenantModelID` pre-check for clear errors when a UUID doesn't exist. - **`resolveExtractorChatTarget`** — propagates resolution errors instead of silently returning empty driver. - **`Chat()`** — removed `driver = "dummy"` fallback. Missing driver is now an explicit error. - **Removed dead code**: `splitExtractorLLID`, `findExtractorSoleActiveInstance`. ### 4. `InjectExtractorLLMID` — fallback when no user config (`internal/common/parser_config.go`) Injects the tenant's global default LLM into extractor components **only when their `llm_id` is empty**. Preserves user-selected UUID or model@provider values. Priority: user-configured llm_id > tenant global default > error (no silent dummy fallback). ### 5. `ResponseHeaderTimeout` increase (`internal/entity/models/base_model.go`) `ResponseHeaderTimeout` 60s → 120s in `NewDriverHTTPClient`. Reasoning models with large extraction prompts can take longer than 60s to produce the first response token. ### 6. CJK rune-aware heading detection (`internal/ingestion/component/chunker/title.go`) Two byte-vs-rune bugs that only manifest on CJK text: | Fix | What changed | Why | |-----|-------------|-----| | **`isColonTitle` byte offset** | `body[lastPunct+1:]` → `body[lastPunct+runeLen:]` via `utf8.DecodeRuneInString` | `strings.LastIndexAny` returns a byte index; `+1` skips only 1 byte, corrupting multi-byte CJK punctuation (e.g. `。` = 3 bytes) and inflating the rune count past the 32-rune threshold → false-positive heading promotion. | | **Short-line filter byte count** | `len(text) <= 1` → `utf8.RuneCountInString(text) <= 1` | Go `len` is UTF-8 bytes; a single CJK char (3 bytes) passed the filter, but Python's `len` returns 1 → mismatch. | ### 7. `extractor_tag.go` — log error when llm fails When `resolveExtractorChatTarget` returned an error, `runAutoTags` will log error. ### 8. Python DSL prompt-key compatibility (`internal/ingestion/component/extractor.go`) The Resume DSL template uses Python-side key names (`sys_prompt`, `prompts`). `NewExtractorComponent` now accepts them as fallbacks alongside the Go names: - `system_prompt` (Go) ← `sys_prompt` (Python) as fallback - `prompt` (Go string) ← `prompts` (Python array `[{"role","content"}]`, takes `[0].content`) as fallback Mirrors the alias pattern already in `internal/agent/component/llm.go`. `resolveInputs` accepts per-call `sys_prompt` override too. ## Remaining gaps vs Python | Gap | Scope | Impact | |-----|-------|--------| | **TOC removal for TXT/MD/HTML** | Python's `remove_contents_table` works on all text formats; Go's `remove_toc` is PDF-only. | Low — plain-text documents rarely contain structured TOCs. | | **Regex pattern details** | Minor differences in quantifiers, missing H5/H6 markdown patterns, missing 4-level numbering pattern. | Low — Go's variants are stricter; DOCX headings are covered by `ck_type` fallback. | ## Testing - `TestHierarchyTitleChunker_CKTypeHeadingFallback` — DOCX `ck_type` heading promotion - `TestHierarchyTitleChunker_ColonTitlePromotion` / `_ColonTitleShortLine_Negative` — colon-title promotion + guard - `TestIsColonTitle_CJKEdgeCase` / `TestIsColonTitle_ASCII_NoRegression` — CJK byte-offset fix + ASCII regression - `TestHierarchyTitleChunker_ColonTitlePromotion_CJK_EdgeCase` — CJK colon edge case through full pipeline - `TestHierarchyTitleChunker_ShortSingleCJKLineFilter` — single CJK char filtered to body - `TestHierarchyTitleChunker_ShortNumericLineFilter` — purely numeric lines filtered - `TestGetFileType_ImageExtensions` / `_ExistingFormats_NoRegression` — image extension mapping - `TestInjectExtractorLLMID_SkipWhenUUID` / `_SkipWhenComposite` / `_InjectWhenEmpty` — llm_id injection guard - `TestIsBareTenantModelID` — UUID detection - `TestResolveExtractorChatTarget_AtSplitFallback` / `_NoDriver` — @ split fallback without DB - `TestNewExtractorComponent_SysPromptAlias` / `_PromptsArray` / `_PromptsArray_PromptWins` / `_SystemPromptWinsOverSysPrompt` — Python key compatibility - `TestBuildDOCXJSONSections_List` / `_TextBox` / `_MixedWithList` — DOCX list/text_box parsing - Full ingestion test suite passes (chunker, pipeline, task, service, component packages)
2026-07-22 19:14:32 +08:00
return extractorChatConfig{}, nil
}
var driver models.ModelDriver
var modelName string
var apiConfig *models.APIConfig
if isBareTenantModelID(compositeLLMID) {
// UUID path: resolveModelConfigByID does a single GetByID and
// returns a clear error if the record doesn't exist. No need
// for a separate pre-check — resolveModelConfig's redundant
// GetByID dispatch check is also bypassed.
driver, modelName, apiConfig, _, err = resolveModelConfigByID(ctx, db, tid, entity.ModelTypeChat, compositeLLMID)
fix(ingestion): align laws DSL with Python — heading fallback, colon-title, short-line filter, remove_toc, and image extension mapping (#17200) ## Summary This PR aligns the Go ingestion pipeline's **Laws** DSL template with the Python implementation by fixing heading-detection gaps, adds image-extension support, refactors the **Extractor** component's LLM resolution, hardens heading detection for CJK text, and makes the Extractor accept the Python DSL prompt key names (`sys_prompt`/`prompts`) alongside the Go names. ## Changes ### 1. Picture file-type detection (`internal/utility/file.go`) Adds explicit mapping for common image extensions (png, jpg, jpeg, gif, bmp, tiff, tif, webp, svg, ico, avif, heic, apng) → `FileTypeVISUAL`, with regression tests. ### 2. Laws DSL heading-detection alignment (`internal/ingestion/component/chunker/`) Four fixes to `resolveTitleLevels`: | Fix | What changed | Why | |-----|-------------|-----| | **DOCX `ck_type` fallback** | `ckType` field on `lineRecord`, propagated from `ChunkDoc.CKType` in `recordsFromStructured`. When `ck_type=="heading"`, assign `fallbackLevel`. | office_oxide extracts DOCX heading metadata, but the info was lost before reaching the heading detector. Word headings whose text doesn't match any regex (e.g. "Introduction") were treated as body. | | **`make_colon_as_title` promotion** | `isColonTitle()`: promotes lines ending with `:`/`:` that have sentence-ending punctuation before the colon and ≥32 runes between them. | Mirrors Python's `make_colon_as_title` in `rag/nlp/__init__.py`. Triple guard prevents false positives. | | **Short/numeric line filter** | Lines with ≤1 rune or purely numeric are pinned to body level. | Mirrors Python `tree_merge`'s filter of `sections` where `len(...) <= 1` or `re.match(r"[0-9]+$", ...)`. | | **PDF `remove_toc`** | `"remove_toc": true` added to the PDF parser setup in `ingestion_pipeline_laws.json`. | The Go PDF parser already supports TOC removal; the Book template already enables it. | ### 3. Extractor llm_id resolution (`internal/ingestion/component/extractor.go`) Refactored to handle both **bare tenant_model UUIDs** and **composite model@provider** strings via the shared `resolveModelConfig` (`dispatch_model.go`): - **`resolveExtractorChatConfig`** — UUID path calls `resolveModelConfigByID` directly (one DB hit); composite path goes through `resolveModelConfig`. Added `isBareTenantModelID` pre-check for clear errors when a UUID doesn't exist. - **`resolveExtractorChatTarget`** — propagates resolution errors instead of silently returning empty driver. - **`Chat()`** — removed `driver = "dummy"` fallback. Missing driver is now an explicit error. - **Removed dead code**: `splitExtractorLLID`, `findExtractorSoleActiveInstance`. ### 4. `InjectExtractorLLMID` — fallback when no user config (`internal/common/parser_config.go`) Injects the tenant's global default LLM into extractor components **only when their `llm_id` is empty**. Preserves user-selected UUID or model@provider values. Priority: user-configured llm_id > tenant global default > error (no silent dummy fallback). ### 5. `ResponseHeaderTimeout` increase (`internal/entity/models/base_model.go`) `ResponseHeaderTimeout` 60s → 120s in `NewDriverHTTPClient`. Reasoning models with large extraction prompts can take longer than 60s to produce the first response token. ### 6. CJK rune-aware heading detection (`internal/ingestion/component/chunker/title.go`) Two byte-vs-rune bugs that only manifest on CJK text: | Fix | What changed | Why | |-----|-------------|-----| | **`isColonTitle` byte offset** | `body[lastPunct+1:]` → `body[lastPunct+runeLen:]` via `utf8.DecodeRuneInString` | `strings.LastIndexAny` returns a byte index; `+1` skips only 1 byte, corrupting multi-byte CJK punctuation (e.g. `。` = 3 bytes) and inflating the rune count past the 32-rune threshold → false-positive heading promotion. | | **Short-line filter byte count** | `len(text) <= 1` → `utf8.RuneCountInString(text) <= 1` | Go `len` is UTF-8 bytes; a single CJK char (3 bytes) passed the filter, but Python's `len` returns 1 → mismatch. | ### 7. `extractor_tag.go` — log error when llm fails When `resolveExtractorChatTarget` returned an error, `runAutoTags` will log error. ### 8. Python DSL prompt-key compatibility (`internal/ingestion/component/extractor.go`) The Resume DSL template uses Python-side key names (`sys_prompt`, `prompts`). `NewExtractorComponent` now accepts them as fallbacks alongside the Go names: - `system_prompt` (Go) ← `sys_prompt` (Python) as fallback - `prompt` (Go string) ← `prompts` (Python array `[{"role","content"}]`, takes `[0].content`) as fallback Mirrors the alias pattern already in `internal/agent/component/llm.go`. `resolveInputs` accepts per-call `sys_prompt` override too. ## Remaining gaps vs Python | Gap | Scope | Impact | |-----|-------|--------| | **TOC removal for TXT/MD/HTML** | Python's `remove_contents_table` works on all text formats; Go's `remove_toc` is PDF-only. | Low — plain-text documents rarely contain structured TOCs. | | **Regex pattern details** | Minor differences in quantifiers, missing H5/H6 markdown patterns, missing 4-level numbering pattern. | Low — Go's variants are stricter; DOCX headings are covered by `ck_type` fallback. | ## Testing - `TestHierarchyTitleChunker_CKTypeHeadingFallback` — DOCX `ck_type` heading promotion - `TestHierarchyTitleChunker_ColonTitlePromotion` / `_ColonTitleShortLine_Negative` — colon-title promotion + guard - `TestIsColonTitle_CJKEdgeCase` / `TestIsColonTitle_ASCII_NoRegression` — CJK byte-offset fix + ASCII regression - `TestHierarchyTitleChunker_ColonTitlePromotion_CJK_EdgeCase` — CJK colon edge case through full pipeline - `TestHierarchyTitleChunker_ShortSingleCJKLineFilter` — single CJK char filtered to body - `TestHierarchyTitleChunker_ShortNumericLineFilter` — purely numeric lines filtered - `TestGetFileType_ImageExtensions` / `_ExistingFormats_NoRegression` — image extension mapping - `TestInjectExtractorLLMID_SkipWhenUUID` / `_SkipWhenComposite` / `_InjectWhenEmpty` — llm_id injection guard - `TestIsBareTenantModelID` — UUID detection - `TestResolveExtractorChatTarget_AtSplitFallback` / `_NoDriver` — @ split fallback without DB - `TestNewExtractorComponent_SysPromptAlias` / `_PromptsArray` / `_PromptsArray_PromptWins` / `_SystemPromptWinsOverSysPrompt` — Python key compatibility - `TestBuildDOCXJSONSections_List` / `_TextBox` / `_MixedWithList` — DOCX list/text_box parsing - Full ingestion test suite passes (chunker, pipeline, task, service, component packages)
2026-07-22 19:14:32 +08:00
if err != nil {
return extractorChatConfig{}, fmt.Errorf("extractor: tenant model %q not found or not usable: %w", compositeLLMID, err)
}
} else {
// Composite "model@provider" path: delegate to the shared dispatcher.
driver, modelName, apiConfig, _, err = resolveModelConfig(ctx, db, tid, entity.ModelTypeChat, compositeLLMID)
fix(ingestion): align laws DSL with Python — heading fallback, colon-title, short-line filter, remove_toc, and image extension mapping (#17200) ## Summary This PR aligns the Go ingestion pipeline's **Laws** DSL template with the Python implementation by fixing heading-detection gaps, adds image-extension support, refactors the **Extractor** component's LLM resolution, hardens heading detection for CJK text, and makes the Extractor accept the Python DSL prompt key names (`sys_prompt`/`prompts`) alongside the Go names. ## Changes ### 1. Picture file-type detection (`internal/utility/file.go`) Adds explicit mapping for common image extensions (png, jpg, jpeg, gif, bmp, tiff, tif, webp, svg, ico, avif, heic, apng) → `FileTypeVISUAL`, with regression tests. ### 2. Laws DSL heading-detection alignment (`internal/ingestion/component/chunker/`) Four fixes to `resolveTitleLevels`: | Fix | What changed | Why | |-----|-------------|-----| | **DOCX `ck_type` fallback** | `ckType` field on `lineRecord`, propagated from `ChunkDoc.CKType` in `recordsFromStructured`. When `ck_type=="heading"`, assign `fallbackLevel`. | office_oxide extracts DOCX heading metadata, but the info was lost before reaching the heading detector. Word headings whose text doesn't match any regex (e.g. "Introduction") were treated as body. | | **`make_colon_as_title` promotion** | `isColonTitle()`: promotes lines ending with `:`/`:` that have sentence-ending punctuation before the colon and ≥32 runes between them. | Mirrors Python's `make_colon_as_title` in `rag/nlp/__init__.py`. Triple guard prevents false positives. | | **Short/numeric line filter** | Lines with ≤1 rune or purely numeric are pinned to body level. | Mirrors Python `tree_merge`'s filter of `sections` where `len(...) <= 1` or `re.match(r"[0-9]+$", ...)`. | | **PDF `remove_toc`** | `"remove_toc": true` added to the PDF parser setup in `ingestion_pipeline_laws.json`. | The Go PDF parser already supports TOC removal; the Book template already enables it. | ### 3. Extractor llm_id resolution (`internal/ingestion/component/extractor.go`) Refactored to handle both **bare tenant_model UUIDs** and **composite model@provider** strings via the shared `resolveModelConfig` (`dispatch_model.go`): - **`resolveExtractorChatConfig`** — UUID path calls `resolveModelConfigByID` directly (one DB hit); composite path goes through `resolveModelConfig`. Added `isBareTenantModelID` pre-check for clear errors when a UUID doesn't exist. - **`resolveExtractorChatTarget`** — propagates resolution errors instead of silently returning empty driver. - **`Chat()`** — removed `driver = "dummy"` fallback. Missing driver is now an explicit error. - **Removed dead code**: `splitExtractorLLID`, `findExtractorSoleActiveInstance`. ### 4. `InjectExtractorLLMID` — fallback when no user config (`internal/common/parser_config.go`) Injects the tenant's global default LLM into extractor components **only when their `llm_id` is empty**. Preserves user-selected UUID or model@provider values. Priority: user-configured llm_id > tenant global default > error (no silent dummy fallback). ### 5. `ResponseHeaderTimeout` increase (`internal/entity/models/base_model.go`) `ResponseHeaderTimeout` 60s → 120s in `NewDriverHTTPClient`. Reasoning models with large extraction prompts can take longer than 60s to produce the first response token. ### 6. CJK rune-aware heading detection (`internal/ingestion/component/chunker/title.go`) Two byte-vs-rune bugs that only manifest on CJK text: | Fix | What changed | Why | |-----|-------------|-----| | **`isColonTitle` byte offset** | `body[lastPunct+1:]` → `body[lastPunct+runeLen:]` via `utf8.DecodeRuneInString` | `strings.LastIndexAny` returns a byte index; `+1` skips only 1 byte, corrupting multi-byte CJK punctuation (e.g. `。` = 3 bytes) and inflating the rune count past the 32-rune threshold → false-positive heading promotion. | | **Short-line filter byte count** | `len(text) <= 1` → `utf8.RuneCountInString(text) <= 1` | Go `len` is UTF-8 bytes; a single CJK char (3 bytes) passed the filter, but Python's `len` returns 1 → mismatch. | ### 7. `extractor_tag.go` — log error when llm fails When `resolveExtractorChatTarget` returned an error, `runAutoTags` will log error. ### 8. Python DSL prompt-key compatibility (`internal/ingestion/component/extractor.go`) The Resume DSL template uses Python-side key names (`sys_prompt`, `prompts`). `NewExtractorComponent` now accepts them as fallbacks alongside the Go names: - `system_prompt` (Go) ← `sys_prompt` (Python) as fallback - `prompt` (Go string) ← `prompts` (Python array `[{"role","content"}]`, takes `[0].content`) as fallback Mirrors the alias pattern already in `internal/agent/component/llm.go`. `resolveInputs` accepts per-call `sys_prompt` override too. ## Remaining gaps vs Python | Gap | Scope | Impact | |-----|-------|--------| | **TOC removal for TXT/MD/HTML** | Python's `remove_contents_table` works on all text formats; Go's `remove_toc` is PDF-only. | Low — plain-text documents rarely contain structured TOCs. | | **Regex pattern details** | Minor differences in quantifiers, missing H5/H6 markdown patterns, missing 4-level numbering pattern. | Low — Go's variants are stricter; DOCX headings are covered by `ck_type` fallback. | ## Testing - `TestHierarchyTitleChunker_CKTypeHeadingFallback` — DOCX `ck_type` heading promotion - `TestHierarchyTitleChunker_ColonTitlePromotion` / `_ColonTitleShortLine_Negative` — colon-title promotion + guard - `TestIsColonTitle_CJKEdgeCase` / `TestIsColonTitle_ASCII_NoRegression` — CJK byte-offset fix + ASCII regression - `TestHierarchyTitleChunker_ColonTitlePromotion_CJK_EdgeCase` — CJK colon edge case through full pipeline - `TestHierarchyTitleChunker_ShortSingleCJKLineFilter` — single CJK char filtered to body - `TestHierarchyTitleChunker_ShortNumericLineFilter` — purely numeric lines filtered - `TestGetFileType_ImageExtensions` / `_ExistingFormats_NoRegression` — image extension mapping - `TestInjectExtractorLLMID_SkipWhenUUID` / `_SkipWhenComposite` / `_InjectWhenEmpty` — llm_id injection guard - `TestIsBareTenantModelID` — UUID detection - `TestResolveExtractorChatTarget_AtSplitFallback` / `_NoDriver` — @ split fallback without DB - `TestNewExtractorComponent_SysPromptAlias` / `_PromptsArray` / `_PromptsArray_PromptWins` / `_SystemPromptWinsOverSysPrompt` — Python key compatibility - `TestBuildDOCXJSONSections_List` / `_TextBox` / `_MixedWithList` — DOCX list/text_box parsing - Full ingestion test suite passes (chunker, pipeline, task, service, component packages)
2026-07-22 19:14:32 +08:00
if err != nil {
return extractorChatConfig{}, fmt.Errorf("extractor: resolve model %q: %w", compositeLLMID, err)
}
}
fix(ingestion): align laws DSL with Python — heading fallback, colon-title, short-line filter, remove_toc, and image extension mapping (#17200) ## Summary This PR aligns the Go ingestion pipeline's **Laws** DSL template with the Python implementation by fixing heading-detection gaps, adds image-extension support, refactors the **Extractor** component's LLM resolution, hardens heading detection for CJK text, and makes the Extractor accept the Python DSL prompt key names (`sys_prompt`/`prompts`) alongside the Go names. ## Changes ### 1. Picture file-type detection (`internal/utility/file.go`) Adds explicit mapping for common image extensions (png, jpg, jpeg, gif, bmp, tiff, tif, webp, svg, ico, avif, heic, apng) → `FileTypeVISUAL`, with regression tests. ### 2. Laws DSL heading-detection alignment (`internal/ingestion/component/chunker/`) Four fixes to `resolveTitleLevels`: | Fix | What changed | Why | |-----|-------------|-----| | **DOCX `ck_type` fallback** | `ckType` field on `lineRecord`, propagated from `ChunkDoc.CKType` in `recordsFromStructured`. When `ck_type=="heading"`, assign `fallbackLevel`. | office_oxide extracts DOCX heading metadata, but the info was lost before reaching the heading detector. Word headings whose text doesn't match any regex (e.g. "Introduction") were treated as body. | | **`make_colon_as_title` promotion** | `isColonTitle()`: promotes lines ending with `:`/`:` that have sentence-ending punctuation before the colon and ≥32 runes between them. | Mirrors Python's `make_colon_as_title` in `rag/nlp/__init__.py`. Triple guard prevents false positives. | | **Short/numeric line filter** | Lines with ≤1 rune or purely numeric are pinned to body level. | Mirrors Python `tree_merge`'s filter of `sections` where `len(...) <= 1` or `re.match(r"[0-9]+$", ...)`. | | **PDF `remove_toc`** | `"remove_toc": true` added to the PDF parser setup in `ingestion_pipeline_laws.json`. | The Go PDF parser already supports TOC removal; the Book template already enables it. | ### 3. Extractor llm_id resolution (`internal/ingestion/component/extractor.go`) Refactored to handle both **bare tenant_model UUIDs** and **composite model@provider** strings via the shared `resolveModelConfig` (`dispatch_model.go`): - **`resolveExtractorChatConfig`** — UUID path calls `resolveModelConfigByID` directly (one DB hit); composite path goes through `resolveModelConfig`. Added `isBareTenantModelID` pre-check for clear errors when a UUID doesn't exist. - **`resolveExtractorChatTarget`** — propagates resolution errors instead of silently returning empty driver. - **`Chat()`** — removed `driver = "dummy"` fallback. Missing driver is now an explicit error. - **Removed dead code**: `splitExtractorLLID`, `findExtractorSoleActiveInstance`. ### 4. `InjectExtractorLLMID` — fallback when no user config (`internal/common/parser_config.go`) Injects the tenant's global default LLM into extractor components **only when their `llm_id` is empty**. Preserves user-selected UUID or model@provider values. Priority: user-configured llm_id > tenant global default > error (no silent dummy fallback). ### 5. `ResponseHeaderTimeout` increase (`internal/entity/models/base_model.go`) `ResponseHeaderTimeout` 60s → 120s in `NewDriverHTTPClient`. Reasoning models with large extraction prompts can take longer than 60s to produce the first response token. ### 6. CJK rune-aware heading detection (`internal/ingestion/component/chunker/title.go`) Two byte-vs-rune bugs that only manifest on CJK text: | Fix | What changed | Why | |-----|-------------|-----| | **`isColonTitle` byte offset** | `body[lastPunct+1:]` → `body[lastPunct+runeLen:]` via `utf8.DecodeRuneInString` | `strings.LastIndexAny` returns a byte index; `+1` skips only 1 byte, corrupting multi-byte CJK punctuation (e.g. `。` = 3 bytes) and inflating the rune count past the 32-rune threshold → false-positive heading promotion. | | **Short-line filter byte count** | `len(text) <= 1` → `utf8.RuneCountInString(text) <= 1` | Go `len` is UTF-8 bytes; a single CJK char (3 bytes) passed the filter, but Python's `len` returns 1 → mismatch. | ### 7. `extractor_tag.go` — log error when llm fails When `resolveExtractorChatTarget` returned an error, `runAutoTags` will log error. ### 8. Python DSL prompt-key compatibility (`internal/ingestion/component/extractor.go`) The Resume DSL template uses Python-side key names (`sys_prompt`, `prompts`). `NewExtractorComponent` now accepts them as fallbacks alongside the Go names: - `system_prompt` (Go) ← `sys_prompt` (Python) as fallback - `prompt` (Go string) ← `prompts` (Python array `[{"role","content"}]`, takes `[0].content`) as fallback Mirrors the alias pattern already in `internal/agent/component/llm.go`. `resolveInputs` accepts per-call `sys_prompt` override too. ## Remaining gaps vs Python | Gap | Scope | Impact | |-----|-------|--------| | **TOC removal for TXT/MD/HTML** | Python's `remove_contents_table` works on all text formats; Go's `remove_toc` is PDF-only. | Low — plain-text documents rarely contain structured TOCs. | | **Regex pattern details** | Minor differences in quantifiers, missing H5/H6 markdown patterns, missing 4-level numbering pattern. | Low — Go's variants are stricter; DOCX headings are covered by `ck_type` fallback. | ## Testing - `TestHierarchyTitleChunker_CKTypeHeadingFallback` — DOCX `ck_type` heading promotion - `TestHierarchyTitleChunker_ColonTitlePromotion` / `_ColonTitleShortLine_Negative` — colon-title promotion + guard - `TestIsColonTitle_CJKEdgeCase` / `TestIsColonTitle_ASCII_NoRegression` — CJK byte-offset fix + ASCII regression - `TestHierarchyTitleChunker_ColonTitlePromotion_CJK_EdgeCase` — CJK colon edge case through full pipeline - `TestHierarchyTitleChunker_ShortSingleCJKLineFilter` — single CJK char filtered to body - `TestHierarchyTitleChunker_ShortNumericLineFilter` — purely numeric lines filtered - `TestGetFileType_ImageExtensions` / `_ExistingFormats_NoRegression` — image extension mapping - `TestInjectExtractorLLMID_SkipWhenUUID` / `_SkipWhenComposite` / `_InjectWhenEmpty` — llm_id injection guard - `TestIsBareTenantModelID` — UUID detection - `TestResolveExtractorChatTarget_AtSplitFallback` / `_NoDriver` — @ split fallback without DB - `TestNewExtractorComponent_SysPromptAlias` / `_PromptsArray` / `_PromptsArray_PromptWins` / `_SystemPromptWinsOverSysPrompt` — Python key compatibility - `TestBuildDOCXJSONSections_List` / `_TextBox` / `_MixedWithList` — DOCX list/text_box parsing - Full ingestion test suite passes (chunker, pipeline, task, service, component packages)
2026-07-22 19:14:32 +08:00
apiKey := ""
baseURL := ""
fix(ingestion): align laws DSL with Python — heading fallback, colon-title, short-line filter, remove_toc, and image extension mapping (#17200) ## Summary This PR aligns the Go ingestion pipeline's **Laws** DSL template with the Python implementation by fixing heading-detection gaps, adds image-extension support, refactors the **Extractor** component's LLM resolution, hardens heading detection for CJK text, and makes the Extractor accept the Python DSL prompt key names (`sys_prompt`/`prompts`) alongside the Go names. ## Changes ### 1. Picture file-type detection (`internal/utility/file.go`) Adds explicit mapping for common image extensions (png, jpg, jpeg, gif, bmp, tiff, tif, webp, svg, ico, avif, heic, apng) → `FileTypeVISUAL`, with regression tests. ### 2. Laws DSL heading-detection alignment (`internal/ingestion/component/chunker/`) Four fixes to `resolveTitleLevels`: | Fix | What changed | Why | |-----|-------------|-----| | **DOCX `ck_type` fallback** | `ckType` field on `lineRecord`, propagated from `ChunkDoc.CKType` in `recordsFromStructured`. When `ck_type=="heading"`, assign `fallbackLevel`. | office_oxide extracts DOCX heading metadata, but the info was lost before reaching the heading detector. Word headings whose text doesn't match any regex (e.g. "Introduction") were treated as body. | | **`make_colon_as_title` promotion** | `isColonTitle()`: promotes lines ending with `:`/`:` that have sentence-ending punctuation before the colon and ≥32 runes between them. | Mirrors Python's `make_colon_as_title` in `rag/nlp/__init__.py`. Triple guard prevents false positives. | | **Short/numeric line filter** | Lines with ≤1 rune or purely numeric are pinned to body level. | Mirrors Python `tree_merge`'s filter of `sections` where `len(...) <= 1` or `re.match(r"[0-9]+$", ...)`. | | **PDF `remove_toc`** | `"remove_toc": true` added to the PDF parser setup in `ingestion_pipeline_laws.json`. | The Go PDF parser already supports TOC removal; the Book template already enables it. | ### 3. Extractor llm_id resolution (`internal/ingestion/component/extractor.go`) Refactored to handle both **bare tenant_model UUIDs** and **composite model@provider** strings via the shared `resolveModelConfig` (`dispatch_model.go`): - **`resolveExtractorChatConfig`** — UUID path calls `resolveModelConfigByID` directly (one DB hit); composite path goes through `resolveModelConfig`. Added `isBareTenantModelID` pre-check for clear errors when a UUID doesn't exist. - **`resolveExtractorChatTarget`** — propagates resolution errors instead of silently returning empty driver. - **`Chat()`** — removed `driver = "dummy"` fallback. Missing driver is now an explicit error. - **Removed dead code**: `splitExtractorLLID`, `findExtractorSoleActiveInstance`. ### 4. `InjectExtractorLLMID` — fallback when no user config (`internal/common/parser_config.go`) Injects the tenant's global default LLM into extractor components **only when their `llm_id` is empty**. Preserves user-selected UUID or model@provider values. Priority: user-configured llm_id > tenant global default > error (no silent dummy fallback). ### 5. `ResponseHeaderTimeout` increase (`internal/entity/models/base_model.go`) `ResponseHeaderTimeout` 60s → 120s in `NewDriverHTTPClient`. Reasoning models with large extraction prompts can take longer than 60s to produce the first response token. ### 6. CJK rune-aware heading detection (`internal/ingestion/component/chunker/title.go`) Two byte-vs-rune bugs that only manifest on CJK text: | Fix | What changed | Why | |-----|-------------|-----| | **`isColonTitle` byte offset** | `body[lastPunct+1:]` → `body[lastPunct+runeLen:]` via `utf8.DecodeRuneInString` | `strings.LastIndexAny` returns a byte index; `+1` skips only 1 byte, corrupting multi-byte CJK punctuation (e.g. `。` = 3 bytes) and inflating the rune count past the 32-rune threshold → false-positive heading promotion. | | **Short-line filter byte count** | `len(text) <= 1` → `utf8.RuneCountInString(text) <= 1` | Go `len` is UTF-8 bytes; a single CJK char (3 bytes) passed the filter, but Python's `len` returns 1 → mismatch. | ### 7. `extractor_tag.go` — log error when llm fails When `resolveExtractorChatTarget` returned an error, `runAutoTags` will log error. ### 8. Python DSL prompt-key compatibility (`internal/ingestion/component/extractor.go`) The Resume DSL template uses Python-side key names (`sys_prompt`, `prompts`). `NewExtractorComponent` now accepts them as fallbacks alongside the Go names: - `system_prompt` (Go) ← `sys_prompt` (Python) as fallback - `prompt` (Go string) ← `prompts` (Python array `[{"role","content"}]`, takes `[0].content`) as fallback Mirrors the alias pattern already in `internal/agent/component/llm.go`. `resolveInputs` accepts per-call `sys_prompt` override too. ## Remaining gaps vs Python | Gap | Scope | Impact | |-----|-------|--------| | **TOC removal for TXT/MD/HTML** | Python's `remove_contents_table` works on all text formats; Go's `remove_toc` is PDF-only. | Low — plain-text documents rarely contain structured TOCs. | | **Regex pattern details** | Minor differences in quantifiers, missing H5/H6 markdown patterns, missing 4-level numbering pattern. | Low — Go's variants are stricter; DOCX headings are covered by `ck_type` fallback. | ## Testing - `TestHierarchyTitleChunker_CKTypeHeadingFallback` — DOCX `ck_type` heading promotion - `TestHierarchyTitleChunker_ColonTitlePromotion` / `_ColonTitleShortLine_Negative` — colon-title promotion + guard - `TestIsColonTitle_CJKEdgeCase` / `TestIsColonTitle_ASCII_NoRegression` — CJK byte-offset fix + ASCII regression - `TestHierarchyTitleChunker_ColonTitlePromotion_CJK_EdgeCase` — CJK colon edge case through full pipeline - `TestHierarchyTitleChunker_ShortSingleCJKLineFilter` — single CJK char filtered to body - `TestHierarchyTitleChunker_ShortNumericLineFilter` — purely numeric lines filtered - `TestGetFileType_ImageExtensions` / `_ExistingFormats_NoRegression` — image extension mapping - `TestInjectExtractorLLMID_SkipWhenUUID` / `_SkipWhenComposite` / `_InjectWhenEmpty` — llm_id injection guard - `TestIsBareTenantModelID` — UUID detection - `TestResolveExtractorChatTarget_AtSplitFallback` / `_NoDriver` — @ split fallback without DB - `TestNewExtractorComponent_SysPromptAlias` / `_PromptsArray` / `_PromptsArray_PromptWins` / `_SystemPromptWinsOverSysPrompt` — Python key compatibility - `TestBuildDOCXJSONSections_List` / `_TextBox` / `_MixedWithList` — DOCX list/text_box parsing - Full ingestion test suite passes (chunker, pipeline, task, service, component packages)
2026-07-22 19:14:32 +08:00
if apiConfig != nil {
if apiConfig.ApiKey != nil {
apiKey = *apiConfig.ApiKey
}
if apiConfig.BaseURL != nil {
baseURL = *apiConfig.BaseURL
}
}
return extractorChatConfig{
fix(ingestion): align laws DSL with Python — heading fallback, colon-title, short-line filter, remove_toc, and image extension mapping (#17200) ## Summary This PR aligns the Go ingestion pipeline's **Laws** DSL template with the Python implementation by fixing heading-detection gaps, adds image-extension support, refactors the **Extractor** component's LLM resolution, hardens heading detection for CJK text, and makes the Extractor accept the Python DSL prompt key names (`sys_prompt`/`prompts`) alongside the Go names. ## Changes ### 1. Picture file-type detection (`internal/utility/file.go`) Adds explicit mapping for common image extensions (png, jpg, jpeg, gif, bmp, tiff, tif, webp, svg, ico, avif, heic, apng) → `FileTypeVISUAL`, with regression tests. ### 2. Laws DSL heading-detection alignment (`internal/ingestion/component/chunker/`) Four fixes to `resolveTitleLevels`: | Fix | What changed | Why | |-----|-------------|-----| | **DOCX `ck_type` fallback** | `ckType` field on `lineRecord`, propagated from `ChunkDoc.CKType` in `recordsFromStructured`. When `ck_type=="heading"`, assign `fallbackLevel`. | office_oxide extracts DOCX heading metadata, but the info was lost before reaching the heading detector. Word headings whose text doesn't match any regex (e.g. "Introduction") were treated as body. | | **`make_colon_as_title` promotion** | `isColonTitle()`: promotes lines ending with `:`/`:` that have sentence-ending punctuation before the colon and ≥32 runes between them. | Mirrors Python's `make_colon_as_title` in `rag/nlp/__init__.py`. Triple guard prevents false positives. | | **Short/numeric line filter** | Lines with ≤1 rune or purely numeric are pinned to body level. | Mirrors Python `tree_merge`'s filter of `sections` where `len(...) <= 1` or `re.match(r"[0-9]+$", ...)`. | | **PDF `remove_toc`** | `"remove_toc": true` added to the PDF parser setup in `ingestion_pipeline_laws.json`. | The Go PDF parser already supports TOC removal; the Book template already enables it. | ### 3. Extractor llm_id resolution (`internal/ingestion/component/extractor.go`) Refactored to handle both **bare tenant_model UUIDs** and **composite model@provider** strings via the shared `resolveModelConfig` (`dispatch_model.go`): - **`resolveExtractorChatConfig`** — UUID path calls `resolveModelConfigByID` directly (one DB hit); composite path goes through `resolveModelConfig`. Added `isBareTenantModelID` pre-check for clear errors when a UUID doesn't exist. - **`resolveExtractorChatTarget`** — propagates resolution errors instead of silently returning empty driver. - **`Chat()`** — removed `driver = "dummy"` fallback. Missing driver is now an explicit error. - **Removed dead code**: `splitExtractorLLID`, `findExtractorSoleActiveInstance`. ### 4. `InjectExtractorLLMID` — fallback when no user config (`internal/common/parser_config.go`) Injects the tenant's global default LLM into extractor components **only when their `llm_id` is empty**. Preserves user-selected UUID or model@provider values. Priority: user-configured llm_id > tenant global default > error (no silent dummy fallback). ### 5. `ResponseHeaderTimeout` increase (`internal/entity/models/base_model.go`) `ResponseHeaderTimeout` 60s → 120s in `NewDriverHTTPClient`. Reasoning models with large extraction prompts can take longer than 60s to produce the first response token. ### 6. CJK rune-aware heading detection (`internal/ingestion/component/chunker/title.go`) Two byte-vs-rune bugs that only manifest on CJK text: | Fix | What changed | Why | |-----|-------------|-----| | **`isColonTitle` byte offset** | `body[lastPunct+1:]` → `body[lastPunct+runeLen:]` via `utf8.DecodeRuneInString` | `strings.LastIndexAny` returns a byte index; `+1` skips only 1 byte, corrupting multi-byte CJK punctuation (e.g. `。` = 3 bytes) and inflating the rune count past the 32-rune threshold → false-positive heading promotion. | | **Short-line filter byte count** | `len(text) <= 1` → `utf8.RuneCountInString(text) <= 1` | Go `len` is UTF-8 bytes; a single CJK char (3 bytes) passed the filter, but Python's `len` returns 1 → mismatch. | ### 7. `extractor_tag.go` — log error when llm fails When `resolveExtractorChatTarget` returned an error, `runAutoTags` will log error. ### 8. Python DSL prompt-key compatibility (`internal/ingestion/component/extractor.go`) The Resume DSL template uses Python-side key names (`sys_prompt`, `prompts`). `NewExtractorComponent` now accepts them as fallbacks alongside the Go names: - `system_prompt` (Go) ← `sys_prompt` (Python) as fallback - `prompt` (Go string) ← `prompts` (Python array `[{"role","content"}]`, takes `[0].content`) as fallback Mirrors the alias pattern already in `internal/agent/component/llm.go`. `resolveInputs` accepts per-call `sys_prompt` override too. ## Remaining gaps vs Python | Gap | Scope | Impact | |-----|-------|--------| | **TOC removal for TXT/MD/HTML** | Python's `remove_contents_table` works on all text formats; Go's `remove_toc` is PDF-only. | Low — plain-text documents rarely contain structured TOCs. | | **Regex pattern details** | Minor differences in quantifiers, missing H5/H6 markdown patterns, missing 4-level numbering pattern. | Low — Go's variants are stricter; DOCX headings are covered by `ck_type` fallback. | ## Testing - `TestHierarchyTitleChunker_CKTypeHeadingFallback` — DOCX `ck_type` heading promotion - `TestHierarchyTitleChunker_ColonTitlePromotion` / `_ColonTitleShortLine_Negative` — colon-title promotion + guard - `TestIsColonTitle_CJKEdgeCase` / `TestIsColonTitle_ASCII_NoRegression` — CJK byte-offset fix + ASCII regression - `TestHierarchyTitleChunker_ColonTitlePromotion_CJK_EdgeCase` — CJK colon edge case through full pipeline - `TestHierarchyTitleChunker_ShortSingleCJKLineFilter` — single CJK char filtered to body - `TestHierarchyTitleChunker_ShortNumericLineFilter` — purely numeric lines filtered - `TestGetFileType_ImageExtensions` / `_ExistingFormats_NoRegression` — image extension mapping - `TestInjectExtractorLLMID_SkipWhenUUID` / `_SkipWhenComposite` / `_InjectWhenEmpty` — llm_id injection guard - `TestIsBareTenantModelID` — UUID detection - `TestResolveExtractorChatTarget_AtSplitFallback` / `_NoDriver` — @ split fallback without DB - `TestNewExtractorComponent_SysPromptAlias` / `_PromptsArray` / `_PromptsArray_PromptWins` / `_SystemPromptWinsOverSysPrompt` — Python key compatibility - `TestBuildDOCXJSONSections_List` / `_TextBox` / `_MixedWithList` — DOCX list/text_box parsing - Full ingestion test suite passes (chunker, pipeline, task, service, component packages)
2026-07-22 19:14:32 +08:00
driver: strings.ToLower(driver.Name()),
modelName: modelName,
apiKey: apiKey,
baseURL: baseURL,
fix(ingestion): align laws DSL with Python — heading fallback, colon-title, short-line filter, remove_toc, and image extension mapping (#17200) ## Summary This PR aligns the Go ingestion pipeline's **Laws** DSL template with the Python implementation by fixing heading-detection gaps, adds image-extension support, refactors the **Extractor** component's LLM resolution, hardens heading detection for CJK text, and makes the Extractor accept the Python DSL prompt key names (`sys_prompt`/`prompts`) alongside the Go names. ## Changes ### 1. Picture file-type detection (`internal/utility/file.go`) Adds explicit mapping for common image extensions (png, jpg, jpeg, gif, bmp, tiff, tif, webp, svg, ico, avif, heic, apng) → `FileTypeVISUAL`, with regression tests. ### 2. Laws DSL heading-detection alignment (`internal/ingestion/component/chunker/`) Four fixes to `resolveTitleLevels`: | Fix | What changed | Why | |-----|-------------|-----| | **DOCX `ck_type` fallback** | `ckType` field on `lineRecord`, propagated from `ChunkDoc.CKType` in `recordsFromStructured`. When `ck_type=="heading"`, assign `fallbackLevel`. | office_oxide extracts DOCX heading metadata, but the info was lost before reaching the heading detector. Word headings whose text doesn't match any regex (e.g. "Introduction") were treated as body. | | **`make_colon_as_title` promotion** | `isColonTitle()`: promotes lines ending with `:`/`:` that have sentence-ending punctuation before the colon and ≥32 runes between them. | Mirrors Python's `make_colon_as_title` in `rag/nlp/__init__.py`. Triple guard prevents false positives. | | **Short/numeric line filter** | Lines with ≤1 rune or purely numeric are pinned to body level. | Mirrors Python `tree_merge`'s filter of `sections` where `len(...) <= 1` or `re.match(r"[0-9]+$", ...)`. | | **PDF `remove_toc`** | `"remove_toc": true` added to the PDF parser setup in `ingestion_pipeline_laws.json`. | The Go PDF parser already supports TOC removal; the Book template already enables it. | ### 3. Extractor llm_id resolution (`internal/ingestion/component/extractor.go`) Refactored to handle both **bare tenant_model UUIDs** and **composite model@provider** strings via the shared `resolveModelConfig` (`dispatch_model.go`): - **`resolveExtractorChatConfig`** — UUID path calls `resolveModelConfigByID` directly (one DB hit); composite path goes through `resolveModelConfig`. Added `isBareTenantModelID` pre-check for clear errors when a UUID doesn't exist. - **`resolveExtractorChatTarget`** — propagates resolution errors instead of silently returning empty driver. - **`Chat()`** — removed `driver = "dummy"` fallback. Missing driver is now an explicit error. - **Removed dead code**: `splitExtractorLLID`, `findExtractorSoleActiveInstance`. ### 4. `InjectExtractorLLMID` — fallback when no user config (`internal/common/parser_config.go`) Injects the tenant's global default LLM into extractor components **only when their `llm_id` is empty**. Preserves user-selected UUID or model@provider values. Priority: user-configured llm_id > tenant global default > error (no silent dummy fallback). ### 5. `ResponseHeaderTimeout` increase (`internal/entity/models/base_model.go`) `ResponseHeaderTimeout` 60s → 120s in `NewDriverHTTPClient`. Reasoning models with large extraction prompts can take longer than 60s to produce the first response token. ### 6. CJK rune-aware heading detection (`internal/ingestion/component/chunker/title.go`) Two byte-vs-rune bugs that only manifest on CJK text: | Fix | What changed | Why | |-----|-------------|-----| | **`isColonTitle` byte offset** | `body[lastPunct+1:]` → `body[lastPunct+runeLen:]` via `utf8.DecodeRuneInString` | `strings.LastIndexAny` returns a byte index; `+1` skips only 1 byte, corrupting multi-byte CJK punctuation (e.g. `。` = 3 bytes) and inflating the rune count past the 32-rune threshold → false-positive heading promotion. | | **Short-line filter byte count** | `len(text) <= 1` → `utf8.RuneCountInString(text) <= 1` | Go `len` is UTF-8 bytes; a single CJK char (3 bytes) passed the filter, but Python's `len` returns 1 → mismatch. | ### 7. `extractor_tag.go` — log error when llm fails When `resolveExtractorChatTarget` returned an error, `runAutoTags` will log error. ### 8. Python DSL prompt-key compatibility (`internal/ingestion/component/extractor.go`) The Resume DSL template uses Python-side key names (`sys_prompt`, `prompts`). `NewExtractorComponent` now accepts them as fallbacks alongside the Go names: - `system_prompt` (Go) ← `sys_prompt` (Python) as fallback - `prompt` (Go string) ← `prompts` (Python array `[{"role","content"}]`, takes `[0].content`) as fallback Mirrors the alias pattern already in `internal/agent/component/llm.go`. `resolveInputs` accepts per-call `sys_prompt` override too. ## Remaining gaps vs Python | Gap | Scope | Impact | |-----|-------|--------| | **TOC removal for TXT/MD/HTML** | Python's `remove_contents_table` works on all text formats; Go's `remove_toc` is PDF-only. | Low — plain-text documents rarely contain structured TOCs. | | **Regex pattern details** | Minor differences in quantifiers, missing H5/H6 markdown patterns, missing 4-level numbering pattern. | Low — Go's variants are stricter; DOCX headings are covered by `ck_type` fallback. | ## Testing - `TestHierarchyTitleChunker_CKTypeHeadingFallback` — DOCX `ck_type` heading promotion - `TestHierarchyTitleChunker_ColonTitlePromotion` / `_ColonTitleShortLine_Negative` — colon-title promotion + guard - `TestIsColonTitle_CJKEdgeCase` / `TestIsColonTitle_ASCII_NoRegression` — CJK byte-offset fix + ASCII regression - `TestHierarchyTitleChunker_ColonTitlePromotion_CJK_EdgeCase` — CJK colon edge case through full pipeline - `TestHierarchyTitleChunker_ShortSingleCJKLineFilter` — single CJK char filtered to body - `TestHierarchyTitleChunker_ShortNumericLineFilter` — purely numeric lines filtered - `TestGetFileType_ImageExtensions` / `_ExistingFormats_NoRegression` — image extension mapping - `TestInjectExtractorLLMID_SkipWhenUUID` / `_SkipWhenComposite` / `_InjectWhenEmpty` — llm_id injection guard - `TestIsBareTenantModelID` — UUID detection - `TestResolveExtractorChatTarget_AtSplitFallback` / `_NoDriver` — @ split fallback without DB - `TestNewExtractorComponent_SysPromptAlias` / `_PromptsArray` / `_PromptsArray_PromptWins` / `_SystemPromptWinsOverSysPrompt` — Python key compatibility - `TestBuildDOCXJSONSections_List` / `_TextBox` / `_MixedWithList` — DOCX list/text_box parsing - Full ingestion test suite passes (chunker, pipeline, task, service, component packages)
2026-07-22 19:14:32 +08:00
}, nil
}
fix(ingestion): improve Extractor/LLM robustness and Python->Go parity (#17470) ## Summary This PR hardens the Go ingestion **Extractor** and **LLM retry** paths and closes several Python->Go parity gaps in the keyword/question/tag extraction flow. - **Generic retry utility** (`internal/common/retry.go`): `RetryWithBackoff` with exponential backoff (default 3 retries, 2s initial delay, capped at 1m), context-aware sleep, and a `maxRetries<=0` fast path. Covered by `internal/common/retry_test.go`. - **LLM retry reuse**: `agent/component/llm_retry.go` now delegates to `common.RetryWithBackoff` instead of an inline loop (behavior preserved: ctx cancellation short-circuits the backoff). - **Extractor LLM calls** (`extractor.go`): - `call()` now retries transient LLM failures via `RetryWithBackoff` (retry exhaustion fails the chunk instead of silently skipping). - Sets `temperature = 0.2`, matching Python `generator.py:230,245`. - Runs keyword and question extraction **concurrently** per chunk when both are enabled (`task_executor.py:444-448`), with mutex-guarded map writes to avoid data races. - Substitutes `{field_name}` placeholders (including `{chunks}` -> chunk text) in `prompt`/`system_prompt` before the call, mirroring Python `string_format` (`extractor.py:102-103`); unmatched placeholders are left as-is. - Falls back to the **tenant default chat model** when `llm_id` is empty (`task_executor.py:573-574`). - Strips `` **greedily** (`strings.LastIndex`) in `cleanExtractionResult`. - **Auto-tagging** (`extractor_tag.go`): drops the `in.llmID != ""` guards so an empty `llm_id` no longer skips tagging (uses the tenant default model), and strips `` greedily in `parseTaggerResponse`. - **Docs**: fixes a misleading `PresentationChunker` docstring that claimed per-slide `image`/`position` output (the PPTX path emits none — unlike PDF), and removes a stale `docs/migration_python_go_diff.md` reference in `media_dispatch.go`. ## Test plan - `bash build.sh --test ./internal/common/...` — passes (new retry utility + tests). - `bash build.sh --test ./internal/ingestion/component/...` — passes (extractor/chunker/schema). - `gofmt` and lefthook pre-commit checks pass. Note: the personal `docs/migration_python_go_diff.md` working notebook in the tree is intentionally **not** part of this PR. --------- Co-authored-by: CodeBuddy <noreply@codebuddy.ai>
2026-07-28 19:22:18 +08:00
// resolveExtractorChatDefaultConfig resolves the tenant's default chat
// model when no explicit llm_id is provided. Returns empty config when
// no canvas state or tenant_id is available (unit-test context).
func resolveExtractorChatDefaultConfig(ctx context.Context, db *gorm.DB) extractorChatConfig {
state, _, err := runtime.GetStateFromContext[*runtime.CanvasState](ctx)
if err != nil || state == nil {
return extractorChatConfig{}
}
tidVal, _ := state.GetGlobal("tenant_id")
tid, _ := tidVal.(string)
if tid == "" {
return extractorChatConfig{}
}
driver, modelName, apiConfig, _, err := resolveTenantModelByType(ctx, db, tid, entity.ModelTypeChat)
if err != nil || driver == nil {
return extractorChatConfig{}
}
apiKey := ""
baseURL := ""
if apiConfig != nil {
if apiConfig.ApiKey != nil {
apiKey = *apiConfig.ApiKey
}
if apiConfig.BaseURL != nil {
baseURL = *apiConfig.BaseURL
}
}
return extractorChatConfig{
driver: strings.ToLower(driver.Name()),
modelName: modelName,
apiKey: apiKey,
baseURL: baseURL,
}
}
fix(ingestion): align laws DSL with Python — heading fallback, colon-title, short-line filter, remove_toc, and image extension mapping (#17200) ## Summary This PR aligns the Go ingestion pipeline's **Laws** DSL template with the Python implementation by fixing heading-detection gaps, adds image-extension support, refactors the **Extractor** component's LLM resolution, hardens heading detection for CJK text, and makes the Extractor accept the Python DSL prompt key names (`sys_prompt`/`prompts`) alongside the Go names. ## Changes ### 1. Picture file-type detection (`internal/utility/file.go`) Adds explicit mapping for common image extensions (png, jpg, jpeg, gif, bmp, tiff, tif, webp, svg, ico, avif, heic, apng) → `FileTypeVISUAL`, with regression tests. ### 2. Laws DSL heading-detection alignment (`internal/ingestion/component/chunker/`) Four fixes to `resolveTitleLevels`: | Fix | What changed | Why | |-----|-------------|-----| | **DOCX `ck_type` fallback** | `ckType` field on `lineRecord`, propagated from `ChunkDoc.CKType` in `recordsFromStructured`. When `ck_type=="heading"`, assign `fallbackLevel`. | office_oxide extracts DOCX heading metadata, but the info was lost before reaching the heading detector. Word headings whose text doesn't match any regex (e.g. "Introduction") were treated as body. | | **`make_colon_as_title` promotion** | `isColonTitle()`: promotes lines ending with `:`/`:` that have sentence-ending punctuation before the colon and ≥32 runes between them. | Mirrors Python's `make_colon_as_title` in `rag/nlp/__init__.py`. Triple guard prevents false positives. | | **Short/numeric line filter** | Lines with ≤1 rune or purely numeric are pinned to body level. | Mirrors Python `tree_merge`'s filter of `sections` where `len(...) <= 1` or `re.match(r"[0-9]+$", ...)`. | | **PDF `remove_toc`** | `"remove_toc": true` added to the PDF parser setup in `ingestion_pipeline_laws.json`. | The Go PDF parser already supports TOC removal; the Book template already enables it. | ### 3. Extractor llm_id resolution (`internal/ingestion/component/extractor.go`) Refactored to handle both **bare tenant_model UUIDs** and **composite model@provider** strings via the shared `resolveModelConfig` (`dispatch_model.go`): - **`resolveExtractorChatConfig`** — UUID path calls `resolveModelConfigByID` directly (one DB hit); composite path goes through `resolveModelConfig`. Added `isBareTenantModelID` pre-check for clear errors when a UUID doesn't exist. - **`resolveExtractorChatTarget`** — propagates resolution errors instead of silently returning empty driver. - **`Chat()`** — removed `driver = "dummy"` fallback. Missing driver is now an explicit error. - **Removed dead code**: `splitExtractorLLID`, `findExtractorSoleActiveInstance`. ### 4. `InjectExtractorLLMID` — fallback when no user config (`internal/common/parser_config.go`) Injects the tenant's global default LLM into extractor components **only when their `llm_id` is empty**. Preserves user-selected UUID or model@provider values. Priority: user-configured llm_id > tenant global default > error (no silent dummy fallback). ### 5. `ResponseHeaderTimeout` increase (`internal/entity/models/base_model.go`) `ResponseHeaderTimeout` 60s → 120s in `NewDriverHTTPClient`. Reasoning models with large extraction prompts can take longer than 60s to produce the first response token. ### 6. CJK rune-aware heading detection (`internal/ingestion/component/chunker/title.go`) Two byte-vs-rune bugs that only manifest on CJK text: | Fix | What changed | Why | |-----|-------------|-----| | **`isColonTitle` byte offset** | `body[lastPunct+1:]` → `body[lastPunct+runeLen:]` via `utf8.DecodeRuneInString` | `strings.LastIndexAny` returns a byte index; `+1` skips only 1 byte, corrupting multi-byte CJK punctuation (e.g. `。` = 3 bytes) and inflating the rune count past the 32-rune threshold → false-positive heading promotion. | | **Short-line filter byte count** | `len(text) <= 1` → `utf8.RuneCountInString(text) <= 1` | Go `len` is UTF-8 bytes; a single CJK char (3 bytes) passed the filter, but Python's `len` returns 1 → mismatch. | ### 7. `extractor_tag.go` — log error when llm fails When `resolveExtractorChatTarget` returned an error, `runAutoTags` will log error. ### 8. Python DSL prompt-key compatibility (`internal/ingestion/component/extractor.go`) The Resume DSL template uses Python-side key names (`sys_prompt`, `prompts`). `NewExtractorComponent` now accepts them as fallbacks alongside the Go names: - `system_prompt` (Go) ← `sys_prompt` (Python) as fallback - `prompt` (Go string) ← `prompts` (Python array `[{"role","content"}]`, takes `[0].content`) as fallback Mirrors the alias pattern already in `internal/agent/component/llm.go`. `resolveInputs` accepts per-call `sys_prompt` override too. ## Remaining gaps vs Python | Gap | Scope | Impact | |-----|-------|--------| | **TOC removal for TXT/MD/HTML** | Python's `remove_contents_table` works on all text formats; Go's `remove_toc` is PDF-only. | Low — plain-text documents rarely contain structured TOCs. | | **Regex pattern details** | Minor differences in quantifiers, missing H5/H6 markdown patterns, missing 4-level numbering pattern. | Low — Go's variants are stricter; DOCX headings are covered by `ck_type` fallback. | ## Testing - `TestHierarchyTitleChunker_CKTypeHeadingFallback` — DOCX `ck_type` heading promotion - `TestHierarchyTitleChunker_ColonTitlePromotion` / `_ColonTitleShortLine_Negative` — colon-title promotion + guard - `TestIsColonTitle_CJKEdgeCase` / `TestIsColonTitle_ASCII_NoRegression` — CJK byte-offset fix + ASCII regression - `TestHierarchyTitleChunker_ColonTitlePromotion_CJK_EdgeCase` — CJK colon edge case through full pipeline - `TestHierarchyTitleChunker_ShortSingleCJKLineFilter` — single CJK char filtered to body - `TestHierarchyTitleChunker_ShortNumericLineFilter` — purely numeric lines filtered - `TestGetFileType_ImageExtensions` / `_ExistingFormats_NoRegression` — image extension mapping - `TestInjectExtractorLLMID_SkipWhenUUID` / `_SkipWhenComposite` / `_InjectWhenEmpty` — llm_id injection guard - `TestIsBareTenantModelID` — UUID detection - `TestResolveExtractorChatTarget_AtSplitFallback` / `_NoDriver` — @ split fallback without DB - `TestNewExtractorComponent_SysPromptAlias` / `_PromptsArray` / `_PromptsArray_PromptWins` / `_SystemPromptWinsOverSysPrompt` — Python key compatibility - `TestBuildDOCXJSONSections_List` / `_TextBox` / `_MixedWithList` — DOCX list/text_box parsing - Full ingestion test suite passes (chunker, pipeline, task, service, component packages)
2026-07-22 19:14:32 +08:00
// isBareTenantModelID reports whether s is a 32-character hex string
// (a tenant_model primary key), as opposed to a composite "model@provider".
func isBareTenantModelID(s string) bool {
s = strings.TrimSpace(s)
if len(s) != 32 {
return false
}
fix(ingestion): align laws DSL with Python — heading fallback, colon-title, short-line filter, remove_toc, and image extension mapping (#17200) ## Summary This PR aligns the Go ingestion pipeline's **Laws** DSL template with the Python implementation by fixing heading-detection gaps, adds image-extension support, refactors the **Extractor** component's LLM resolution, hardens heading detection for CJK text, and makes the Extractor accept the Python DSL prompt key names (`sys_prompt`/`prompts`) alongside the Go names. ## Changes ### 1. Picture file-type detection (`internal/utility/file.go`) Adds explicit mapping for common image extensions (png, jpg, jpeg, gif, bmp, tiff, tif, webp, svg, ico, avif, heic, apng) → `FileTypeVISUAL`, with regression tests. ### 2. Laws DSL heading-detection alignment (`internal/ingestion/component/chunker/`) Four fixes to `resolveTitleLevels`: | Fix | What changed | Why | |-----|-------------|-----| | **DOCX `ck_type` fallback** | `ckType` field on `lineRecord`, propagated from `ChunkDoc.CKType` in `recordsFromStructured`. When `ck_type=="heading"`, assign `fallbackLevel`. | office_oxide extracts DOCX heading metadata, but the info was lost before reaching the heading detector. Word headings whose text doesn't match any regex (e.g. "Introduction") were treated as body. | | **`make_colon_as_title` promotion** | `isColonTitle()`: promotes lines ending with `:`/`:` that have sentence-ending punctuation before the colon and ≥32 runes between them. | Mirrors Python's `make_colon_as_title` in `rag/nlp/__init__.py`. Triple guard prevents false positives. | | **Short/numeric line filter** | Lines with ≤1 rune or purely numeric are pinned to body level. | Mirrors Python `tree_merge`'s filter of `sections` where `len(...) <= 1` or `re.match(r"[0-9]+$", ...)`. | | **PDF `remove_toc`** | `"remove_toc": true` added to the PDF parser setup in `ingestion_pipeline_laws.json`. | The Go PDF parser already supports TOC removal; the Book template already enables it. | ### 3. Extractor llm_id resolution (`internal/ingestion/component/extractor.go`) Refactored to handle both **bare tenant_model UUIDs** and **composite model@provider** strings via the shared `resolveModelConfig` (`dispatch_model.go`): - **`resolveExtractorChatConfig`** — UUID path calls `resolveModelConfigByID` directly (one DB hit); composite path goes through `resolveModelConfig`. Added `isBareTenantModelID` pre-check for clear errors when a UUID doesn't exist. - **`resolveExtractorChatTarget`** — propagates resolution errors instead of silently returning empty driver. - **`Chat()`** — removed `driver = "dummy"` fallback. Missing driver is now an explicit error. - **Removed dead code**: `splitExtractorLLID`, `findExtractorSoleActiveInstance`. ### 4. `InjectExtractorLLMID` — fallback when no user config (`internal/common/parser_config.go`) Injects the tenant's global default LLM into extractor components **only when their `llm_id` is empty**. Preserves user-selected UUID or model@provider values. Priority: user-configured llm_id > tenant global default > error (no silent dummy fallback). ### 5. `ResponseHeaderTimeout` increase (`internal/entity/models/base_model.go`) `ResponseHeaderTimeout` 60s → 120s in `NewDriverHTTPClient`. Reasoning models with large extraction prompts can take longer than 60s to produce the first response token. ### 6. CJK rune-aware heading detection (`internal/ingestion/component/chunker/title.go`) Two byte-vs-rune bugs that only manifest on CJK text: | Fix | What changed | Why | |-----|-------------|-----| | **`isColonTitle` byte offset** | `body[lastPunct+1:]` → `body[lastPunct+runeLen:]` via `utf8.DecodeRuneInString` | `strings.LastIndexAny` returns a byte index; `+1` skips only 1 byte, corrupting multi-byte CJK punctuation (e.g. `。` = 3 bytes) and inflating the rune count past the 32-rune threshold → false-positive heading promotion. | | **Short-line filter byte count** | `len(text) <= 1` → `utf8.RuneCountInString(text) <= 1` | Go `len` is UTF-8 bytes; a single CJK char (3 bytes) passed the filter, but Python's `len` returns 1 → mismatch. | ### 7. `extractor_tag.go` — log error when llm fails When `resolveExtractorChatTarget` returned an error, `runAutoTags` will log error. ### 8. Python DSL prompt-key compatibility (`internal/ingestion/component/extractor.go`) The Resume DSL template uses Python-side key names (`sys_prompt`, `prompts`). `NewExtractorComponent` now accepts them as fallbacks alongside the Go names: - `system_prompt` (Go) ← `sys_prompt` (Python) as fallback - `prompt` (Go string) ← `prompts` (Python array `[{"role","content"}]`, takes `[0].content`) as fallback Mirrors the alias pattern already in `internal/agent/component/llm.go`. `resolveInputs` accepts per-call `sys_prompt` override too. ## Remaining gaps vs Python | Gap | Scope | Impact | |-----|-------|--------| | **TOC removal for TXT/MD/HTML** | Python's `remove_contents_table` works on all text formats; Go's `remove_toc` is PDF-only. | Low — plain-text documents rarely contain structured TOCs. | | **Regex pattern details** | Minor differences in quantifiers, missing H5/H6 markdown patterns, missing 4-level numbering pattern. | Low — Go's variants are stricter; DOCX headings are covered by `ck_type` fallback. | ## Testing - `TestHierarchyTitleChunker_CKTypeHeadingFallback` — DOCX `ck_type` heading promotion - `TestHierarchyTitleChunker_ColonTitlePromotion` / `_ColonTitleShortLine_Negative` — colon-title promotion + guard - `TestIsColonTitle_CJKEdgeCase` / `TestIsColonTitle_ASCII_NoRegression` — CJK byte-offset fix + ASCII regression - `TestHierarchyTitleChunker_ColonTitlePromotion_CJK_EdgeCase` — CJK colon edge case through full pipeline - `TestHierarchyTitleChunker_ShortSingleCJKLineFilter` — single CJK char filtered to body - `TestHierarchyTitleChunker_ShortNumericLineFilter` — purely numeric lines filtered - `TestGetFileType_ImageExtensions` / `_ExistingFormats_NoRegression` — image extension mapping - `TestInjectExtractorLLMID_SkipWhenUUID` / `_SkipWhenComposite` / `_InjectWhenEmpty` — llm_id injection guard - `TestIsBareTenantModelID` — UUID detection - `TestResolveExtractorChatTarget_AtSplitFallback` / `_NoDriver` — @ split fallback without DB - `TestNewExtractorComponent_SysPromptAlias` / `_PromptsArray` / `_PromptsArray_PromptWins` / `_SystemPromptWinsOverSysPrompt` — Python key compatibility - `TestBuildDOCXJSONSections_List` / `_TextBox` / `_MixedWithList` — DOCX list/text_box parsing - Full ingestion test suite passes (chunker, pipeline, task, service, component packages)
2026-07-22 19:14:32 +08:00
for _, r := range s {
if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')) {
return false
}
}
fix(ingestion): align laws DSL with Python — heading fallback, colon-title, short-line filter, remove_toc, and image extension mapping (#17200) ## Summary This PR aligns the Go ingestion pipeline's **Laws** DSL template with the Python implementation by fixing heading-detection gaps, adds image-extension support, refactors the **Extractor** component's LLM resolution, hardens heading detection for CJK text, and makes the Extractor accept the Python DSL prompt key names (`sys_prompt`/`prompts`) alongside the Go names. ## Changes ### 1. Picture file-type detection (`internal/utility/file.go`) Adds explicit mapping for common image extensions (png, jpg, jpeg, gif, bmp, tiff, tif, webp, svg, ico, avif, heic, apng) → `FileTypeVISUAL`, with regression tests. ### 2. Laws DSL heading-detection alignment (`internal/ingestion/component/chunker/`) Four fixes to `resolveTitleLevels`: | Fix | What changed | Why | |-----|-------------|-----| | **DOCX `ck_type` fallback** | `ckType` field on `lineRecord`, propagated from `ChunkDoc.CKType` in `recordsFromStructured`. When `ck_type=="heading"`, assign `fallbackLevel`. | office_oxide extracts DOCX heading metadata, but the info was lost before reaching the heading detector. Word headings whose text doesn't match any regex (e.g. "Introduction") were treated as body. | | **`make_colon_as_title` promotion** | `isColonTitle()`: promotes lines ending with `:`/`:` that have sentence-ending punctuation before the colon and ≥32 runes between them. | Mirrors Python's `make_colon_as_title` in `rag/nlp/__init__.py`. Triple guard prevents false positives. | | **Short/numeric line filter** | Lines with ≤1 rune or purely numeric are pinned to body level. | Mirrors Python `tree_merge`'s filter of `sections` where `len(...) <= 1` or `re.match(r"[0-9]+$", ...)`. | | **PDF `remove_toc`** | `"remove_toc": true` added to the PDF parser setup in `ingestion_pipeline_laws.json`. | The Go PDF parser already supports TOC removal; the Book template already enables it. | ### 3. Extractor llm_id resolution (`internal/ingestion/component/extractor.go`) Refactored to handle both **bare tenant_model UUIDs** and **composite model@provider** strings via the shared `resolveModelConfig` (`dispatch_model.go`): - **`resolveExtractorChatConfig`** — UUID path calls `resolveModelConfigByID` directly (one DB hit); composite path goes through `resolveModelConfig`. Added `isBareTenantModelID` pre-check for clear errors when a UUID doesn't exist. - **`resolveExtractorChatTarget`** — propagates resolution errors instead of silently returning empty driver. - **`Chat()`** — removed `driver = "dummy"` fallback. Missing driver is now an explicit error. - **Removed dead code**: `splitExtractorLLID`, `findExtractorSoleActiveInstance`. ### 4. `InjectExtractorLLMID` — fallback when no user config (`internal/common/parser_config.go`) Injects the tenant's global default LLM into extractor components **only when their `llm_id` is empty**. Preserves user-selected UUID or model@provider values. Priority: user-configured llm_id > tenant global default > error (no silent dummy fallback). ### 5. `ResponseHeaderTimeout` increase (`internal/entity/models/base_model.go`) `ResponseHeaderTimeout` 60s → 120s in `NewDriverHTTPClient`. Reasoning models with large extraction prompts can take longer than 60s to produce the first response token. ### 6. CJK rune-aware heading detection (`internal/ingestion/component/chunker/title.go`) Two byte-vs-rune bugs that only manifest on CJK text: | Fix | What changed | Why | |-----|-------------|-----| | **`isColonTitle` byte offset** | `body[lastPunct+1:]` → `body[lastPunct+runeLen:]` via `utf8.DecodeRuneInString` | `strings.LastIndexAny` returns a byte index; `+1` skips only 1 byte, corrupting multi-byte CJK punctuation (e.g. `。` = 3 bytes) and inflating the rune count past the 32-rune threshold → false-positive heading promotion. | | **Short-line filter byte count** | `len(text) <= 1` → `utf8.RuneCountInString(text) <= 1` | Go `len` is UTF-8 bytes; a single CJK char (3 bytes) passed the filter, but Python's `len` returns 1 → mismatch. | ### 7. `extractor_tag.go` — log error when llm fails When `resolveExtractorChatTarget` returned an error, `runAutoTags` will log error. ### 8. Python DSL prompt-key compatibility (`internal/ingestion/component/extractor.go`) The Resume DSL template uses Python-side key names (`sys_prompt`, `prompts`). `NewExtractorComponent` now accepts them as fallbacks alongside the Go names: - `system_prompt` (Go) ← `sys_prompt` (Python) as fallback - `prompt` (Go string) ← `prompts` (Python array `[{"role","content"}]`, takes `[0].content`) as fallback Mirrors the alias pattern already in `internal/agent/component/llm.go`. `resolveInputs` accepts per-call `sys_prompt` override too. ## Remaining gaps vs Python | Gap | Scope | Impact | |-----|-------|--------| | **TOC removal for TXT/MD/HTML** | Python's `remove_contents_table` works on all text formats; Go's `remove_toc` is PDF-only. | Low — plain-text documents rarely contain structured TOCs. | | **Regex pattern details** | Minor differences in quantifiers, missing H5/H6 markdown patterns, missing 4-level numbering pattern. | Low — Go's variants are stricter; DOCX headings are covered by `ck_type` fallback. | ## Testing - `TestHierarchyTitleChunker_CKTypeHeadingFallback` — DOCX `ck_type` heading promotion - `TestHierarchyTitleChunker_ColonTitlePromotion` / `_ColonTitleShortLine_Negative` — colon-title promotion + guard - `TestIsColonTitle_CJKEdgeCase` / `TestIsColonTitle_ASCII_NoRegression` — CJK byte-offset fix + ASCII regression - `TestHierarchyTitleChunker_ColonTitlePromotion_CJK_EdgeCase` — CJK colon edge case through full pipeline - `TestHierarchyTitleChunker_ShortSingleCJKLineFilter` — single CJK char filtered to body - `TestHierarchyTitleChunker_ShortNumericLineFilter` — purely numeric lines filtered - `TestGetFileType_ImageExtensions` / `_ExistingFormats_NoRegression` — image extension mapping - `TestInjectExtractorLLMID_SkipWhenUUID` / `_SkipWhenComposite` / `_InjectWhenEmpty` — llm_id injection guard - `TestIsBareTenantModelID` — UUID detection - `TestResolveExtractorChatTarget_AtSplitFallback` / `_NoDriver` — @ split fallback without DB - `TestNewExtractorComponent_SysPromptAlias` / `_PromptsArray` / `_PromptsArray_PromptWins` / `_SystemPromptWinsOverSysPrompt` — Python key compatibility - `TestBuildDOCXJSONSections_List` / `_TextBox` / `_MixedWithList` — DOCX list/text_box parsing - Full ingestion test suite passes (chunker, pipeline, task, service, component packages)
2026-07-22 19:14:32 +08:00
return true
}
// buildExtractorMessages assembles system + user messages for
// one extraction call. The user prompt is rendered as
// "<prompt>\n\n<chunkText>" so the python behavior of
// substituting the chunk text into the args dict is preserved
// without invoking a template engine.
//
// Prompt placeholders of the form `{ComponentName:ParamName@chunks}`
// are substituted with the joined text of all upstream chunks
// when chunks is non-empty. The python rag/flow/extractor/extractor.py
// build_existing_prompt path performs the same substitution at
// runtime; the Go port surfaces it as a regex on the prompt
// template so the resume template's `{TitleChunker:FlatMiceFix@chunks}`
// reference resolves without invoking a template engine.
//
// Substitution is opt-in: when chunks is nil/empty the placeholder
// is left intact so a misconfigured template surfaces as a
// clear pattern rather than silently disappearing.
func buildExtractorMessages(system, prompt, chunkText string, chunks []map[string]any) []eschema.Message {
out := make([]eschema.Message, 0, 2)
if system != "" {
out = append(out, eschema.Message{Role: eschema.System, Content: system})
}
user := prompt
if chunkText != "" {
if user != "" {
user += "\n\n"
}
user += chunkText
}
if user == "" {
// An empty prompt + empty chunk is a degenerate call.
// The LLM driver returns an error; we surface that
// unchanged.
user = " "
}
user = substitutePromptPlaceholders(user, chunks)
out = append(out, eschema.Message{Role: eschema.User, Content: user})
return out
}
// substitutePromptPlaceholders replaces `{ComponentName:ParamName@chunks}`
// patterns in the user prompt with the joined text of all upstream
// chunks. The python rag/flow/extractor/extractor.py:build_existing_prompt
// path performs the same substitution at runtime using a Jinja
// template; the Go port keeps the regex form because the LLM
// driver does not require Jinja and the surface is small enough to
// avoid pulling in a template engine.
//
// Pattern grammar:
//
// {CmpName:ParamName@chunks}
//
// The CmpName and ParamName are both matched but ignored — the
// substitute is always "the joined chunk text" today, because the
// only @chunks reference in production templates is the resume
// template's `{TitleChunker:FlatMiceFix@chunks}` pattern. The
// CmpName/ParamName parsing exists so a future per-component
// substitution can extend the function without breaking the
// existing call sites.
func substitutePromptPlaceholders(prompt string, chunks []map[string]any) string {
if prompt == "" || len(chunks) == 0 {
return prompt
}
// Build the substitution payload once. Each chunk's text is
// joined with a blank line so a downstream LLM sees clear
// chunk boundaries.
var b strings.Builder
for i, ck := range chunks {
t, _ := ck["text"].(string)
if t == "" {
t, _ = ck["content_with_weight"].(string)
}
if t == "" {
continue
}
if i > 0 {
b.WriteString("\n\n")
}
b.WriteString(t)
}
repl := b.String()
if repl == "" {
return prompt
}
return placeholderRE.ReplaceAllString(prompt, repl)
}
// placeholderRE matches `{CmpName:ParamName@chunks}` patterns in
// Extractor user prompts. The CMP / Param groups are ignored for
// the @chunks variant but kept so the regex rejects arbitrary
// placeholders (a future per-component substitution extends here).
var placeholderRE = regexp.MustCompile(`\{[A-Za-z0-9_]+:[A-Za-z0-9_]+@chunks\}`)
fix(ingestion): improve Extractor/LLM robustness and Python->Go parity (#17470) ## Summary This PR hardens the Go ingestion **Extractor** and **LLM retry** paths and closes several Python->Go parity gaps in the keyword/question/tag extraction flow. - **Generic retry utility** (`internal/common/retry.go`): `RetryWithBackoff` with exponential backoff (default 3 retries, 2s initial delay, capped at 1m), context-aware sleep, and a `maxRetries<=0` fast path. Covered by `internal/common/retry_test.go`. - **LLM retry reuse**: `agent/component/llm_retry.go` now delegates to `common.RetryWithBackoff` instead of an inline loop (behavior preserved: ctx cancellation short-circuits the backoff). - **Extractor LLM calls** (`extractor.go`): - `call()` now retries transient LLM failures via `RetryWithBackoff` (retry exhaustion fails the chunk instead of silently skipping). - Sets `temperature = 0.2`, matching Python `generator.py:230,245`. - Runs keyword and question extraction **concurrently** per chunk when both are enabled (`task_executor.py:444-448`), with mutex-guarded map writes to avoid data races. - Substitutes `{field_name}` placeholders (including `{chunks}` -> chunk text) in `prompt`/`system_prompt` before the call, mirroring Python `string_format` (`extractor.py:102-103`); unmatched placeholders are left as-is. - Falls back to the **tenant default chat model** when `llm_id` is empty (`task_executor.py:573-574`). - Strips `` **greedily** (`strings.LastIndex`) in `cleanExtractionResult`. - **Auto-tagging** (`extractor_tag.go`): drops the `in.llmID != ""` guards so an empty `llm_id` no longer skips tagging (uses the tenant default model), and strips `` greedily in `parseTaggerResponse`. - **Docs**: fixes a misleading `PresentationChunker` docstring that claimed per-slide `image`/`position` output (the PPTX path emits none — unlike PDF), and removes a stale `docs/migration_python_go_diff.md` reference in `media_dispatch.go`. ## Test plan - `bash build.sh --test ./internal/common/...` — passes (new retry utility + tests). - `bash build.sh --test ./internal/ingestion/component/...` — passes (extractor/chunker/schema). - `gofmt` and lefthook pre-commit checks pass. Note: the personal `docs/migration_python_go_diff.md` working notebook in the tree is intentionally **not** part of this PR. --------- Co-authored-by: CodeBuddy <noreply@codebuddy.ai>
2026-07-28 19:22:18 +08:00
// simplePlaceholderRE matches simple {field_name} placeholders
// (no colons, no @chunks). Used by substituteChunkPlaceholders to
// replace {text}, {content_with_weight}, etc. with the current
// chunk's field values, mirroring Python's string_format
// (agent/component/base.py:602-609).
var simplePlaceholderRE = regexp.MustCompile(`\{[A-Za-z_][A-Za-z0-9_]*\}`)
// substituteChunkPlaceholders replaces {field_name} placeholders in
// the prompt with values from the current chunk map. The special
// alias "chunks" maps to chunkText (the current chunk's primary
// text), matching Python's `args[chunks_key] = ck["text"]` at
// extractor.py:102. Unmatched placeholders are left as-is.
func substituteChunkPlaceholders(prompt string, ck map[string]any, chunkText string) string {
if prompt == "" || ck == nil {
return prompt
}
// Build lookup: chunk fields + "chunks" alias
lookup := make(map[string]string, len(ck)+1)
for k, v := range ck {
lookup[k] = fmt.Sprintf("%v", v)
}
if _, has := lookup["chunks"]; !has {
lookup["chunks"] = chunkText
}
return simplePlaceholderRE.ReplaceAllStringFunc(prompt, func(match string) string {
key := match[1 : len(match)-1] // strip { }
if val, ok := lookup[key]; ok {
return val
}
return match // leave unknown placeholders as-is
})
}
// tryParseJSONObject tries to parse s as a JSON object. Returns
// (parsed, true) on success; (nil, false) on parse error or when
// s is not a JSON object. Trims common markdown code fences
// (```json ... ```) before parsing.
func tryParseJSONObject(s string) (map[string]any, bool) {
trimmed := strings.TrimSpace(s)
// Strip a single ``` fence pair if present.
if strings.HasPrefix(trimmed, "```") {
if idx := strings.Index(trimmed, "\n"); idx >= 0 {
trimmed = trimmed[idx+1:]
}
if strings.HasSuffix(trimmed, "```") {
trimmed = trimmed[:len(trimmed)-3]
}
trimmed = strings.TrimSpace(trimmed)
}
var out map[string]any
if err := json.Unmarshal([]byte(trimmed), &out); err != nil {
return nil, false
}
if out == nil {
return nil, false
}
// An empty object carries no information the caller can act on;
// surface as "could not extract" so downstream code can route
// it to the same fallback it would use for malformed text.
if len(out) == 0 {
return nil, false
}
return out, true
}
// init registers Extractor under CategoryIngestion (per plan §4
// Phase 2.5). Metadata is derived from the Inputs()/Outputs()
// methods on ExtractorComponent so the API layer (Phase 4) can
// enumerate the catalog without instantiating the component.
// mapInt converts a JSON-compatible value to int.
func mapInt(v interface{}) int {
switch n := v.(type) {
case float64:
return int(n)
case int:
return n
case int64:
return int(n)
}
return 0
}
// init registers Extractor under CategoryIngestion (per plan §4
// Phase 2.5). Metadata is derived from the Inputs()/Outputs()
// methods on ExtractorComponent so the API layer (Phase 4) can
// enumerate the catalog without instantiating the component.
func init() {
c := &ExtractorComponent{}
runtime.MustRegister(componentNameExtractor, runtime.CategoryIngestion,
func(_ string, params map[string]any) (runtime.Component, error) {
return NewExtractorComponent(params)
},
runtime.Metadata{
Version: "1.0.0",
Inputs: c.Inputs(),
Outputs: c.Outputs(),
})
}