2026-06-04 13:42:58 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import importlib.util
|
fix(docling): detect chunked response by shape, not request payload (#16921)
Fixes #16917.
## Problem
`deepdoc/parser/docling_parser.py::_parse_pdf_remote` decides whether
the
response is chunked based on which payload was sent, not on what came
back.
Docling Serve silently drops unknown fields such as `do_chunking`
(Pydantic
`extra="ignore"`) and returns a standard `{"document": ..., "status":
...}`
conversion response. The code then:
1. sets `is_chunked_response = True` from the request shape,
2. logs `Successfully used native chunking on: <endpoint>`,
3. extracts 0 chunks from `response_json.get("results", [])`,
4. logs `Native chunks received: 0`,
5. falls through to the existing `md_content` fallback.
The `md_content` fallback path is fine. The misleading log lines are the
problem: operators see "Successfully used native chunking" immediately
followed by "Native chunks received: 0" and "No chunk built", which
looks
like an internal regression rather than a server contract gap.
## Fix
Decide chunked-vs-standard from the **response shape**, not the request:
```python
response_is_chunk = self._looks_like_chunk_response(response_json)
is_chunked_response = chunk_flag and response_is_chunk
```
`_looks_like_chunk_response` returns True iff the response is a
non-empty
list or a dict with a non-empty `results` or `chunks` list. A standard
conversion response (`{"document": ..., "status": ...}`) does not match,
so
a server that ignored the chunking flag is correctly classified as
standard
even when the request payload asked for chunking.
When chunking was requested but the server returned a standard response,
log a single WARNING ("Server ignored chunking request on <endpoint>;
treating response as standard conversion.") instead of the INFO success
line. The misleading "Prioritizes native chunking endpoints" docstring
is
replaced with what the code actually does.
## Tests
`test/unit_test/deepdoc/parser/test_docling_parser_remote.py` (6 tests,
all passing):
- `test_remote_chunked_200_standard_payload_falls_back` (existing —
still
passes; the `md_content` path is unchanged)
- `test_chunk_shape_helper_recognises_chunk_payloads`
- `test_chunk_shape_helper_rejects_standard_payloads`
- `test_remote_chunked_request_with_results_list_is_treated_as_chunked`
- `test_remote_top_level_list_response_is_treated_as_chunked`
- `test_remote_chunked_request_with_ignored_flag_does_not_log_success`
```
$ uv run pytest test/unit_test/deepdoc/parser/test_docling_parser_remote.py -v
============================== 6 passed in 0.26s ==============================
```
## Files changed
- `deepdoc/parser/docling_parser.py` (+35 / -5)
- `test/unit_test/deepdoc/parser/test_docling_parser_remote.py` (+89 /
-4)
## Backward compatibility
- All four payload/endpoint combinations continue to be tried in the
same order.
- The bundled-docling happy path (`parse_pdf`, not `_parse_pdf_remote`)
is
untouched.
- A server that returns a real chunked response to a chunked request
still
goes down the chunked branch. A server that returns a standard response
to a chunked request now goes down the standard branch with
`is_chunked_response=False` instead of misleadingly logging success.
## Follow-up (out of scope)
Calling the real Docling-Serve native chunk endpoints
(`/v1/chunk/hybrid/source`, `/v1/chunk/hierarchical/source`) with
`HybridChunkerOptions` is a larger feature change and warrants its own
PR after this lands.
Co-authored-by: Harsh23Kashyap <harsh@example.com>
2026-07-16 06:59:09 +05:30
|
|
|
import logging
|
2026-06-04 13:42:58 +08:00
|
|
|
import sys
|
|
|
|
|
import types
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[4]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _Response:
|
|
|
|
|
status_code = 200
|
|
|
|
|
text = ""
|
|
|
|
|
|
fix(docling): detect chunked response by shape, not request payload (#16921)
Fixes #16917.
## Problem
`deepdoc/parser/docling_parser.py::_parse_pdf_remote` decides whether
the
response is chunked based on which payload was sent, not on what came
back.
Docling Serve silently drops unknown fields such as `do_chunking`
(Pydantic
`extra="ignore"`) and returns a standard `{"document": ..., "status":
...}`
conversion response. The code then:
1. sets `is_chunked_response = True` from the request shape,
2. logs `Successfully used native chunking on: <endpoint>`,
3. extracts 0 chunks from `response_json.get("results", [])`,
4. logs `Native chunks received: 0`,
5. falls through to the existing `md_content` fallback.
The `md_content` fallback path is fine. The misleading log lines are the
problem: operators see "Successfully used native chunking" immediately
followed by "Native chunks received: 0" and "No chunk built", which
looks
like an internal regression rather than a server contract gap.
## Fix
Decide chunked-vs-standard from the **response shape**, not the request:
```python
response_is_chunk = self._looks_like_chunk_response(response_json)
is_chunked_response = chunk_flag and response_is_chunk
```
`_looks_like_chunk_response` returns True iff the response is a
non-empty
list or a dict with a non-empty `results` or `chunks` list. A standard
conversion response (`{"document": ..., "status": ...}`) does not match,
so
a server that ignored the chunking flag is correctly classified as
standard
even when the request payload asked for chunking.
When chunking was requested but the server returned a standard response,
log a single WARNING ("Server ignored chunking request on <endpoint>;
treating response as standard conversion.") instead of the INFO success
line. The misleading "Prioritizes native chunking endpoints" docstring
is
replaced with what the code actually does.
## Tests
`test/unit_test/deepdoc/parser/test_docling_parser_remote.py` (6 tests,
all passing):
- `test_remote_chunked_200_standard_payload_falls_back` (existing —
still
passes; the `md_content` path is unchanged)
- `test_chunk_shape_helper_recognises_chunk_payloads`
- `test_chunk_shape_helper_rejects_standard_payloads`
- `test_remote_chunked_request_with_results_list_is_treated_as_chunked`
- `test_remote_top_level_list_response_is_treated_as_chunked`
- `test_remote_chunked_request_with_ignored_flag_does_not_log_success`
```
$ uv run pytest test/unit_test/deepdoc/parser/test_docling_parser_remote.py -v
============================== 6 passed in 0.26s ==============================
```
## Files changed
- `deepdoc/parser/docling_parser.py` (+35 / -5)
- `test/unit_test/deepdoc/parser/test_docling_parser_remote.py` (+89 /
-4)
## Backward compatibility
- All four payload/endpoint combinations continue to be tried in the
same order.
- The bundled-docling happy path (`parse_pdf`, not `_parse_pdf_remote`)
is
untouched.
- A server that returns a real chunked response to a chunked request
still
goes down the chunked branch. A server that returns a standard response
to a chunked request now goes down the standard branch with
`is_chunked_response=False` instead of misleadingly logging success.
## Follow-up (out of scope)
Calling the real Docling-Serve native chunk endpoints
(`/v1/chunk/hybrid/source`, `/v1/chunk/hierarchical/source`) with
`HybridChunkerOptions` is a larger feature change and warrants its own
PR after this lands.
Co-authored-by: Harsh23Kashyap <harsh@example.com>
2026-07-16 06:59:09 +05:30
|
|
|
def __init__(self, payload, status_code: int = 200):
|
2026-06-04 13:42:58 +08:00
|
|
|
self._payload = payload
|
fix(docling): detect chunked response by shape, not request payload (#16921)
Fixes #16917.
## Problem
`deepdoc/parser/docling_parser.py::_parse_pdf_remote` decides whether
the
response is chunked based on which payload was sent, not on what came
back.
Docling Serve silently drops unknown fields such as `do_chunking`
(Pydantic
`extra="ignore"`) and returns a standard `{"document": ..., "status":
...}`
conversion response. The code then:
1. sets `is_chunked_response = True` from the request shape,
2. logs `Successfully used native chunking on: <endpoint>`,
3. extracts 0 chunks from `response_json.get("results", [])`,
4. logs `Native chunks received: 0`,
5. falls through to the existing `md_content` fallback.
The `md_content` fallback path is fine. The misleading log lines are the
problem: operators see "Successfully used native chunking" immediately
followed by "Native chunks received: 0" and "No chunk built", which
looks
like an internal regression rather than a server contract gap.
## Fix
Decide chunked-vs-standard from the **response shape**, not the request:
```python
response_is_chunk = self._looks_like_chunk_response(response_json)
is_chunked_response = chunk_flag and response_is_chunk
```
`_looks_like_chunk_response` returns True iff the response is a
non-empty
list or a dict with a non-empty `results` or `chunks` list. A standard
conversion response (`{"document": ..., "status": ...}`) does not match,
so
a server that ignored the chunking flag is correctly classified as
standard
even when the request payload asked for chunking.
When chunking was requested but the server returned a standard response,
log a single WARNING ("Server ignored chunking request on <endpoint>;
treating response as standard conversion.") instead of the INFO success
line. The misleading "Prioritizes native chunking endpoints" docstring
is
replaced with what the code actually does.
## Tests
`test/unit_test/deepdoc/parser/test_docling_parser_remote.py` (6 tests,
all passing):
- `test_remote_chunked_200_standard_payload_falls_back` (existing —
still
passes; the `md_content` path is unchanged)
- `test_chunk_shape_helper_recognises_chunk_payloads`
- `test_chunk_shape_helper_rejects_standard_payloads`
- `test_remote_chunked_request_with_results_list_is_treated_as_chunked`
- `test_remote_top_level_list_response_is_treated_as_chunked`
- `test_remote_chunked_request_with_ignored_flag_does_not_log_success`
```
$ uv run pytest test/unit_test/deepdoc/parser/test_docling_parser_remote.py -v
============================== 6 passed in 0.26s ==============================
```
## Files changed
- `deepdoc/parser/docling_parser.py` (+35 / -5)
- `test/unit_test/deepdoc/parser/test_docling_parser_remote.py` (+89 /
-4)
## Backward compatibility
- All four payload/endpoint combinations continue to be tried in the
same order.
- The bundled-docling happy path (`parse_pdf`, not `_parse_pdf_remote`)
is
untouched.
- A server that returns a real chunked response to a chunked request
still
goes down the chunked branch. A server that returns a standard response
to a chunked request now goes down the standard branch with
`is_chunked_response=False` instead of misleadingly logging success.
## Follow-up (out of scope)
Calling the real Docling-Serve native chunk endpoints
(`/v1/chunk/hybrid/source`, `/v1/chunk/hierarchical/source`) with
`HybridChunkerOptions` is a larger feature change and warrants its own
PR after this lands.
Co-authored-by: Harsh23Kashyap <harsh@example.com>
2026-07-16 06:59:09 +05:30
|
|
|
self.status_code = status_code
|
2026-06-04 13:42:58 +08:00
|
|
|
|
|
|
|
|
def json(self):
|
|
|
|
|
return self._payload
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _load_docling_parser(monkeypatch):
|
|
|
|
|
common_pkg = types.ModuleType("common")
|
|
|
|
|
constants_mod = types.ModuleType("common.constants")
|
|
|
|
|
constants_mod.MAXIMUM_PAGE_NUMBER = 1000
|
|
|
|
|
|
|
|
|
|
deepdoc_pkg = types.ModuleType("deepdoc")
|
|
|
|
|
parser_pkg = types.ModuleType("deepdoc.parser")
|
|
|
|
|
parser_pkg.__path__ = []
|
|
|
|
|
utils_mod = types.ModuleType("deepdoc.parser.utils")
|
|
|
|
|
utils_mod.extract_pdf_outlines = lambda _source: []
|
|
|
|
|
|
|
|
|
|
pil_pkg = types.ModuleType("PIL")
|
|
|
|
|
image_mod = types.ModuleType("PIL.Image")
|
|
|
|
|
image_mod.Image = object
|
|
|
|
|
pil_pkg.Image = image_mod
|
|
|
|
|
|
|
|
|
|
monkeypatch.setitem(sys.modules, "common", common_pkg)
|
|
|
|
|
monkeypatch.setitem(sys.modules, "common.constants", constants_mod)
|
|
|
|
|
monkeypatch.setitem(sys.modules, "deepdoc", deepdoc_pkg)
|
|
|
|
|
monkeypatch.setitem(sys.modules, "deepdoc.parser", parser_pkg)
|
|
|
|
|
monkeypatch.setitem(sys.modules, "deepdoc.parser.utils", utils_mod)
|
|
|
|
|
monkeypatch.setitem(sys.modules, "pdfplumber", types.ModuleType("pdfplumber"))
|
|
|
|
|
monkeypatch.setitem(sys.modules, "PIL", pil_pkg)
|
|
|
|
|
monkeypatch.setitem(sys.modules, "PIL.Image", image_mod)
|
|
|
|
|
|
|
|
|
|
spec = importlib.util.spec_from_file_location(
|
|
|
|
|
"_docling_parser_under_test",
|
|
|
|
|
ROOT / "deepdoc" / "parser" / "docling_parser.py",
|
|
|
|
|
)
|
|
|
|
|
module = importlib.util.module_from_spec(spec)
|
|
|
|
|
monkeypatch.setitem(sys.modules, spec.name, module)
|
|
|
|
|
spec.loader.exec_module(module)
|
|
|
|
|
return module
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.p2
|
|
|
|
|
def test_remote_chunked_200_standard_payload_falls_back(monkeypatch):
|
|
|
|
|
module = _load_docling_parser(monkeypatch)
|
|
|
|
|
calls = []
|
|
|
|
|
|
|
|
|
|
def fake_post(_url, json, timeout):
|
|
|
|
|
calls.append((json, timeout))
|
|
|
|
|
return _Response({"document": {"md_content": "# Parsed\n\nbody"}})
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(module.requests, "post", fake_post)
|
|
|
|
|
|
|
|
|
|
parser = module.DoclingParser(docling_server_url="http://docling.local")
|
|
|
|
|
sections, tables = parser._parse_pdf_remote("sample.pdf", binary=b"%PDF", parse_method="raw")
|
|
|
|
|
|
|
|
|
|
assert sections == [("# Parsed\n\nbody", "")]
|
|
|
|
|
assert tables == []
|
|
|
|
|
assert calls[0][0]["options"]["do_chunking"] is True
|
fix(docling): detect chunked response by shape, not request payload (#16921)
Fixes #16917.
## Problem
`deepdoc/parser/docling_parser.py::_parse_pdf_remote` decides whether
the
response is chunked based on which payload was sent, not on what came
back.
Docling Serve silently drops unknown fields such as `do_chunking`
(Pydantic
`extra="ignore"`) and returns a standard `{"document": ..., "status":
...}`
conversion response. The code then:
1. sets `is_chunked_response = True` from the request shape,
2. logs `Successfully used native chunking on: <endpoint>`,
3. extracts 0 chunks from `response_json.get("results", [])`,
4. logs `Native chunks received: 0`,
5. falls through to the existing `md_content` fallback.
The `md_content` fallback path is fine. The misleading log lines are the
problem: operators see "Successfully used native chunking" immediately
followed by "Native chunks received: 0" and "No chunk built", which
looks
like an internal regression rather than a server contract gap.
## Fix
Decide chunked-vs-standard from the **response shape**, not the request:
```python
response_is_chunk = self._looks_like_chunk_response(response_json)
is_chunked_response = chunk_flag and response_is_chunk
```
`_looks_like_chunk_response` returns True iff the response is a
non-empty
list or a dict with a non-empty `results` or `chunks` list. A standard
conversion response (`{"document": ..., "status": ...}`) does not match,
so
a server that ignored the chunking flag is correctly classified as
standard
even when the request payload asked for chunking.
When chunking was requested but the server returned a standard response,
log a single WARNING ("Server ignored chunking request on <endpoint>;
treating response as standard conversion.") instead of the INFO success
line. The misleading "Prioritizes native chunking endpoints" docstring
is
replaced with what the code actually does.
## Tests
`test/unit_test/deepdoc/parser/test_docling_parser_remote.py` (6 tests,
all passing):
- `test_remote_chunked_200_standard_payload_falls_back` (existing —
still
passes; the `md_content` path is unchanged)
- `test_chunk_shape_helper_recognises_chunk_payloads`
- `test_chunk_shape_helper_rejects_standard_payloads`
- `test_remote_chunked_request_with_results_list_is_treated_as_chunked`
- `test_remote_top_level_list_response_is_treated_as_chunked`
- `test_remote_chunked_request_with_ignored_flag_does_not_log_success`
```
$ uv run pytest test/unit_test/deepdoc/parser/test_docling_parser_remote.py -v
============================== 6 passed in 0.26s ==============================
```
## Files changed
- `deepdoc/parser/docling_parser.py` (+35 / -5)
- `test/unit_test/deepdoc/parser/test_docling_parser_remote.py` (+89 /
-4)
## Backward compatibility
- All four payload/endpoint combinations continue to be tried in the
same order.
- The bundled-docling happy path (`parse_pdf`, not `_parse_pdf_remote`)
is
untouched.
- A server that returns a real chunked response to a chunked request
still
goes down the chunked branch. A server that returns a standard response
to a chunked request now goes down the standard branch with
`is_chunked_response=False` instead of misleadingly logging success.
## Follow-up (out of scope)
Calling the real Docling-Serve native chunk endpoints
(`/v1/chunk/hybrid/source`, `/v1/chunk/hierarchical/source`) with
`HybridChunkerOptions` is a larger feature change and warrants its own
PR after this lands.
Co-authored-by: Harsh23Kashyap <harsh@example.com>
2026-07-16 06:59:09 +05:30
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.p2
|
|
|
|
|
def test_chunk_shape_helper_recognises_chunk_payloads(monkeypatch):
|
|
|
|
|
"""A response that is chunk-shaped (list, or dict with non-empty results/chunks)
|
|
|
|
|
is classified as chunked regardless of which payload was sent."""
|
|
|
|
|
module = _load_docling_parser(monkeypatch)
|
|
|
|
|
assert module.DoclingParser._looks_like_chunk_response(
|
|
|
|
|
[{"text": "chunk-1"}]
|
|
|
|
|
) is True
|
|
|
|
|
assert module.DoclingParser._looks_like_chunk_response(
|
|
|
|
|
{"results": [{"text": "chunk-1"}, {"text": "chunk-2"}]}
|
|
|
|
|
) is True
|
|
|
|
|
assert module.DoclingParser._looks_like_chunk_response(
|
|
|
|
|
{"chunks": [{"text": "chunk-1"}]}
|
|
|
|
|
) is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.p2
|
|
|
|
|
def test_chunk_shape_helper_rejects_standard_payloads(monkeypatch):
|
|
|
|
|
"""A standard conversion response, empty containers, and non-payload types
|
|
|
|
|
are correctly classified as not-chunked."""
|
|
|
|
|
module = _load_docling_parser(monkeypatch)
|
|
|
|
|
standard = {"document": {"md_content": "body"}, "status": "success"}
|
|
|
|
|
assert module.DoclingParser._looks_like_chunk_response(standard) is False
|
|
|
|
|
assert module.DoclingParser._looks_like_chunk_response({}) is False
|
|
|
|
|
assert module.DoclingParser._looks_like_chunk_response({"results": []}) is False
|
|
|
|
|
assert module.DoclingParser._looks_like_chunk_response({"chunks": []}) is False
|
|
|
|
|
assert module.DoclingParser._looks_like_chunk_response([]) is False
|
|
|
|
|
assert module.DoclingParser._looks_like_chunk_response("not-a-payload") is False
|
|
|
|
|
assert module.DoclingParser._looks_like_chunk_response(None) is False
|
|
|
|
|
assert module.DoclingParser._looks_like_chunk_response(42) is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.p2
|
|
|
|
|
def test_remote_chunked_request_with_results_list_is_treated_as_chunked(monkeypatch):
|
|
|
|
|
"""A server that returns a ``results`` list (Docling Serve's native chunk
|
|
|
|
|
shape) is treated as chunked and each chunk becomes a section."""
|
|
|
|
|
module = _load_docling_parser(monkeypatch)
|
|
|
|
|
|
|
|
|
|
def fake_post(_url, json, timeout):
|
|
|
|
|
return _Response({"results": [{"text": "alpha"}, {"text": "beta"}]})
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(module.requests, "post", fake_post)
|
|
|
|
|
|
|
|
|
|
parser = module.DoclingParser(docling_server_url="http://docling.local")
|
|
|
|
|
sections, tables = parser._parse_pdf_remote("sample.pdf", binary=b"%PDF", parse_method="raw")
|
|
|
|
|
|
|
|
|
|
assert sections == [("alpha", ""), ("beta", "")]
|
|
|
|
|
assert tables == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.p2
|
|
|
|
|
def test_remote_top_level_list_response_is_treated_as_chunked(monkeypatch):
|
|
|
|
|
"""A server that returns a top-level JSON array of chunks is treated
|
|
|
|
|
as chunked (matches the existing implicit assumption in the code)."""
|
|
|
|
|
module = _load_docling_parser(monkeypatch)
|
|
|
|
|
|
|
|
|
|
def fake_post(_url, json, timeout):
|
|
|
|
|
return _Response([{"text": "first"}, {"text": "second"}])
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(module.requests, "post", fake_post)
|
|
|
|
|
|
|
|
|
|
parser = module.DoclingParser(docling_server_url="http://docling.local")
|
|
|
|
|
sections, _ = parser._parse_pdf_remote("sample.pdf", binary=b"%PDF", parse_method="raw")
|
|
|
|
|
|
|
|
|
|
assert sections == [("first", ""), ("second", "")]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.p2
|
|
|
|
|
def test_remote_chunked_request_with_ignored_flag_does_not_log_success(monkeypatch, caplog):
|
|
|
|
|
"""When Docling Serve silently drops the ``do_chunking`` flag and returns
|
|
|
|
|
a standard conversion response, RAGFlow must not log a chunking-success
|
|
|
|
|
message and must log a warning instead."""
|
|
|
|
|
module = _load_docling_parser(monkeypatch)
|
|
|
|
|
|
|
|
|
|
def fake_post(_url, json, timeout):
|
|
|
|
|
return _Response({"document": {"md_content": "real content"}, "status": "success"})
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(module.requests, "post", fake_post)
|
|
|
|
|
|
|
|
|
|
parser = module.DoclingParser(docling_server_url="http://docling.local")
|
|
|
|
|
with caplog.at_level(logging.DEBUG, logger="DoclingParser"):
|
|
|
|
|
sections, _ = parser._parse_pdf_remote("sample.pdf", binary=b"%PDF", parse_method="raw")
|
|
|
|
|
|
|
|
|
|
assert sections == [("real content", "")]
|
|
|
|
|
flat = " ".join(record.getMessage() for record in caplog.records)
|
|
|
|
|
assert "Successfully used native chunking" not in flat
|
|
|
|
|
assert "Server ignored chunking request" in flat
|