mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-23 17:06:42 +08:00
## 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`
211 lines
6.9 KiB
Go
211 lines
6.9 KiB
Go
//go:build cgo
|
|
|
|
//
|
|
// 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 parser
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestBuildDOCXJSONSections_Paragraphs(t *testing.T) {
|
|
ir := `{"sections":[{"title":"","elements":[
|
|
{"type":"paragraph","content":[{"type":"text","text":"Hello world"}],"style":"Normal"}
|
|
]}]}`
|
|
sections := buildDOCXJSONSections(ir)
|
|
if len(sections) != 1 {
|
|
t.Fatalf("got %d sections, want 1", len(sections))
|
|
}
|
|
item := sections[0]
|
|
if got, ok := item["text"].(string); !ok || got != "Hello world" {
|
|
t.Errorf("text = %q, want %q", got, "Hello world")
|
|
}
|
|
if got, ok := item["doc_type_kwd"].(string); !ok || got != "text" {
|
|
t.Errorf("doc_type_kwd = %q, want %q", got, "text")
|
|
}
|
|
}
|
|
|
|
func TestBuildDOCXJSONSections_Headings(t *testing.T) {
|
|
ir := `{"sections":[{"title":"","elements":[
|
|
{"type":"heading","level":1,"content":[{"type":"text","text":"Chapter 1"}]}
|
|
]}]}`
|
|
sections := buildDOCXJSONSections(ir)
|
|
if len(sections) != 1 {
|
|
t.Fatalf("got %d sections, want 1", len(sections))
|
|
}
|
|
item := sections[0]
|
|
if got, ok := item["text"].(string); !ok || got != "Chapter 1" {
|
|
t.Errorf("text = %q, want %q", got, "Chapter 1")
|
|
}
|
|
if got, ok := item["doc_type_kwd"].(string); !ok || got != "text" {
|
|
t.Errorf("doc_type_kwd = %q, want %q", got, "text")
|
|
}
|
|
if got, ok := item["ck_type"].(string); !ok || got != "heading" {
|
|
t.Errorf("ck_type = %q, want %q", got, "heading")
|
|
}
|
|
}
|
|
|
|
func TestBuildDOCXJSONSections_Images(t *testing.T) {
|
|
b64 := base64.StdEncoding.EncodeToString([]byte("fake-image-data"))
|
|
ir := `{"sections":[{"title":"","elements":[
|
|
{"type":"image","data":"` + b64 + `"}
|
|
]}]}`
|
|
sections := buildDOCXJSONSections(ir)
|
|
if len(sections) != 1 {
|
|
t.Fatalf("got %d sections, want 1", len(sections))
|
|
}
|
|
item := sections[0]
|
|
if got, ok := item["text"].(string); !ok || got != "" {
|
|
t.Errorf("text = %q, want empty", got)
|
|
}
|
|
if got, ok := item["image"].(string); !ok || got != b64 {
|
|
t.Errorf("image = %q, want %q", got, b64)
|
|
}
|
|
if got, ok := item["doc_type_kwd"].(string); !ok || got != "image" {
|
|
t.Errorf("doc_type_kwd = %q, want %q", got, "image")
|
|
}
|
|
}
|
|
|
|
func TestBuildDOCXJSONSections_Tables(t *testing.T) {
|
|
ir := `{"sections":[{"title":"","elements":[
|
|
{"type":"table","rows":[
|
|
{"cells":[{"content":[{"type":"paragraph","content":[{"type":"text","text":"A1"}]}]},{"content":[{"type":"paragraph","content":[{"type":"text","text":"B1"}]}]}]},
|
|
{"cells":[{"content":[{"type":"paragraph","content":[{"type":"text","text":"A2"}]}]},{"content":[{"type":"paragraph","content":[{"type":"text","text":"B2"}]}]}]}
|
|
]}
|
|
]}]}`
|
|
sections := buildDOCXJSONSections(ir)
|
|
if len(sections) != 1 {
|
|
t.Fatalf("got %d sections, want 1", len(sections))
|
|
}
|
|
item := sections[0]
|
|
html, ok := item["text"].(string)
|
|
if !ok {
|
|
t.Fatal("text field missing or not string")
|
|
}
|
|
if !strings.Contains(html, "<table>") || !strings.Contains(html, "</table>") {
|
|
t.Errorf("html = %q, missing <table> tags", html)
|
|
}
|
|
if !strings.Contains(html, "<tr>") || !strings.Contains(html, "</tr>") {
|
|
t.Errorf("html = %q, missing <tr> tags", html)
|
|
}
|
|
if !strings.Contains(html, "<td>A1</td>") || !strings.Contains(html, "<td>B2</td>") {
|
|
t.Errorf("html = %q, missing cell content", html)
|
|
}
|
|
if got, ok := item["doc_type_kwd"].(string); !ok || got != "table" {
|
|
t.Errorf("doc_type_kwd = %q, want %q", got, "table")
|
|
}
|
|
}
|
|
|
|
func TestBuildDOCXJSONSections_MixedContent(t *testing.T) {
|
|
ir := `{"sections":[{"title":"","elements":[
|
|
{"type":"paragraph","content":[{"type":"text","text":"First para"}]},
|
|
{"type":"image","data":"aW1hZ2U="},
|
|
{"type":"table","rows":[{"cells":[{"content":[{"type":"paragraph","content":[{"type":"text","text":"cell"}]}]}]}]},
|
|
{"type":"heading","level":2,"content":[{"type":"text","text":"Sub title"}]}
|
|
]}]}`
|
|
sections := buildDOCXJSONSections(ir)
|
|
if len(sections) != 4 {
|
|
t.Fatalf("got %d sections, want 4", len(sections))
|
|
}
|
|
// paragraph first
|
|
if got, _ := sections[0]["doc_type_kwd"].(string); got != "text" {
|
|
t.Errorf("item[0].doc_type_kwd = %q, want %q", got, "text")
|
|
}
|
|
// image second
|
|
if got, _ := sections[1]["doc_type_kwd"].(string); got != "image" {
|
|
t.Errorf("item[1].doc_type_kwd = %q, want %q", got, "image")
|
|
}
|
|
// table third
|
|
if got, _ := sections[2]["doc_type_kwd"].(string); got != "table" {
|
|
t.Errorf("item[2].doc_type_kwd = %q, want %q", got, "table")
|
|
}
|
|
// heading fourth
|
|
if got, _ := sections[3]["doc_type_kwd"].(string); got != "text" {
|
|
t.Errorf("item[3].doc_type_kwd = %q, want %q", got, "text")
|
|
}
|
|
if got, _ := sections[3]["ck_type"].(string); got != "heading" {
|
|
t.Errorf("item[3].ck_type = %q, want %q", got, "heading")
|
|
}
|
|
}
|
|
|
|
func TestBuildDOCXJSONSections_EmptySkipped(t *testing.T) {
|
|
ir := `{"sections":[{"title":"","elements":[
|
|
{"type":"paragraph","content":[{"type":"text","text":""}]},
|
|
{"type":"paragraph","content":[{"type":"text","text":"Only this matters"}]},
|
|
{"type":"table","rows":[]}
|
|
]}]}`
|
|
sections := buildDOCXJSONSections(ir)
|
|
if len(sections) != 1 {
|
|
t.Fatalf("got %d sections, want 1 (only non-empty paragraph)", len(sections))
|
|
}
|
|
if got, _ := sections[0]["text"].(string); got != "Only this matters" {
|
|
t.Errorf("text = %q, want %q", got, "Only this matters")
|
|
}
|
|
}
|
|
|
|
func TestDocxIRTableToHTML_Empty(t *testing.T) {
|
|
el := docxIRElement{Type: "table", Rows: []docxIRRow{}}
|
|
html := docxIRTableToHTML(el)
|
|
if html != "<table></table>" {
|
|
t.Errorf("empty table html = %q, want %q", html, "<table></table>")
|
|
}
|
|
}
|
|
|
|
func TestDocxIRTableToHTML_Single(t *testing.T) {
|
|
el := docxIRElement{
|
|
Type: "table",
|
|
Rows: []docxIRRow{{
|
|
Cells: []docxIRCell{{
|
|
Content: []docxIRElement{{
|
|
Type: "paragraph",
|
|
Content: []docxIRRun{{Type: "text", Text: "hello"}},
|
|
}},
|
|
}},
|
|
}},
|
|
}
|
|
html := docxIRTableToHTML(el)
|
|
want := "<table><tr><td>hello</td></tr></table>"
|
|
if html != want {
|
|
t.Errorf("single cell html = %q, want %q", html, want)
|
|
}
|
|
}
|
|
|
|
func TestDocxIRTableToHTML_MultiRowCol(t *testing.T) {
|
|
cell := func(text string) docxIRCell {
|
|
return docxIRCell{
|
|
Content: []docxIRElement{{
|
|
Type: "paragraph",
|
|
Content: []docxIRRun{{Type: "text", Text: text}},
|
|
}},
|
|
}
|
|
}
|
|
el := docxIRElement{
|
|
Type: "table",
|
|
Rows: []docxIRRow{
|
|
{Cells: []docxIRCell{cell("A1"), cell("B1")}},
|
|
{Cells: []docxIRCell{cell("A2"), cell("B2")}},
|
|
},
|
|
}
|
|
html := docxIRTableToHTML(el)
|
|
want := "<table><tr><td>A1</td><td>B1</td></tr><tr><td>A2</td><td>B2</td></tr></table>"
|
|
if html != want {
|
|
t.Errorf("multi cell html = %q, want %q", html, want)
|
|
}
|
|
}
|