Coverage for src/qdrant_loader/core/conversion/outcome.py: 95%
41 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-20 10:15 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-20 10:15 +0000
1"""The conversion -> chunking handoff types, and the typed failure model.
3Conversion produces a *structured* artifact — a ``DoclingDocument`` wrapped in
4:class:`ConvertedDocument` — or a typed :class:`ConversionOutcome`. It never
5returns a fake markdown stub carrying an error string: failure is a value the caller
6inspects, not content that flows into embedding.
8Per the conversion/chunking boundary, the structured document is the contract;
9``to_markdown`` is a convenience for the prose path, and the chunker chooses its
10own view (structured table traversal vs. markdown) per format.
11"""
13from __future__ import annotations
15from dataclasses import dataclass
16from enum import StrEnum
17from typing import TYPE_CHECKING
19if TYPE_CHECKING:
20 from docling.datamodel.document import ConversionResult
21 from docling_core.types.doc import DoclingDocument
24class ConversionStatus(StrEnum):
25 SUCCESS = "success"
26 PARTIAL = "partial" # docling PARTIAL_SUCCESS (e.g. document_timeout mid-doc)
27 FAILED = "failed"
28 SKIPPED = "skipped" # rejected by policy (format not enabled / too large)
31@dataclass(frozen=True, slots=True)
32class ConvertedDocument:
33 """The canonical conversion artifact handed to the chunking layer.
35 Holds the structured ``DoclingDocument`` so consumers can read cells/tables
36 directly — no serialize-then-reparse round-trip.
37 """
39 document: DoclingDocument
40 source_format: str # the InputFormat value, e.g. "pdf"
42 def to_markdown(self) -> str:
43 """One-way serialization for the prose path. Not the primary contract."""
44 return self.document.export_to_markdown()
47@dataclass(frozen=True, slots=True)
48class ConversionOutcome:
49 """The result value of a conversion attempt; the caller owns skip-vs-stub policy."""
51 status: ConversionStatus
52 document: ConvertedDocument | None = None
53 error: str | None = None
55 @property
56 def succeeded(self) -> bool:
57 return self.status in (ConversionStatus.SUCCESS, ConversionStatus.PARTIAL)
59 @classmethod
60 def skipped(cls, reason: str) -> ConversionOutcome:
61 """A source the policy declined to convert (format/size)."""
62 return cls(status=ConversionStatus.SKIPPED, error=reason)
64 @classmethod
65 def failed(cls, error: str) -> ConversionOutcome:
66 """The engine attempted conversion and failed."""
67 return cls(status=ConversionStatus.FAILED, error=error)
69 @classmethod
70 def from_docling_result(cls, docling_result: ConversionResult) -> ConversionOutcome:
71 """Translate a docling ``ConversionResult`` into our typed outcome."""
72 from docling.datamodel.base_models import ConversionStatus as DoclingStatus
74 if docling_result.status in (
75 DoclingStatus.SUCCESS,
76 DoclingStatus.PARTIAL_SUCCESS,
77 ):
78 status = (
79 ConversionStatus.SUCCESS
80 if docling_result.status is DoclingStatus.SUCCESS
81 else ConversionStatus.PARTIAL
82 )
83 source_format = docling_result.input.format.value
84 # Repair docling's XLSX defects (merged-cell duplication, dropped
85 # sheet names) once on the structured document, before either the
86 # markdown export or the chunker reads it. No-op for other formats.
87 from .postprocess import postprocess_spreadsheet
89 postprocess_spreadsheet(docling_result.document, source_format)
90 document = ConvertedDocument(
91 document=docling_result.document,
92 source_format=source_format,
93 )
94 return cls(status=status, document=document)
96 message = (
97 "; ".join(error.error_message for error in docling_result.errors)
98 or "conversion failed"
99 )
100 return cls(status=ConversionStatus.FAILED, error=message)