Files
2026-07-13 12:08:54 +08:00

156 lines
5.1 KiB
Python

"""Unified parser contract for native + external parser engines.
Every engine (native docx, mineru, docling, legacy, plus the internal
``reuse``/``passthrough`` format handlers) implements :class:`BaseParser`.
The pipeline dispatches through the registry
(:mod:`lightrag.parser.registry`) instead of a growing ``if engine == …``
chain.
Design notes:
- ``BaseParser`` carries *behaviour only* (the ``parse`` coroutine and any
engine-private hooks). Capability metadata (supported suffixes, queue
group, endpoint requirements) lives in the registry's lightweight
``ParserSpec`` table so capability queries never import a parser
implementation.
- ``ParseResult.to_dict()`` emits only semantically-present fields so the
returned dict stays byte-for-byte compatible with the pre-refactor
``parse_native``/``parse_mineru``/``parse_docling`` return shapes (the
worker reads these by ``.get(...)``).
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from lightrag.sidecar.ir import IRDoc # noqa: F401
@dataclass
class ResolvedSource:
"""The common parse preamble shared by every engine."""
source_path: Path
document_name: str
parsed_dir: Path
@dataclass
class ParseContext:
"""Inputs handed to :meth:`BaseParser.parse`.
Wraps the LightRAG instance plus the per-document handles so a parser
does not receive a bare ``self``. Convenience helpers lazily import
pipeline-layer functions at call time, keeping this module import-cheap
and free of import cycles.
"""
rag: Any
doc_id: str
file_path: str
content_data: dict[str, Any]
def source_path(self, parser_engine: str) -> Path:
"""Resolve the on-disk source file for this document."""
from lightrag.pipeline import (
_call_source_file_resolver,
_read_source_file,
)
return Path(
_call_source_file_resolver(
self.rag,
self.file_path,
source_file=_read_source_file(self.content_data),
parser_engine=parser_engine,
)
)
def resolve(self, parser_engine: str) -> ResolvedSource:
"""Resolve ``(source_path, document_name, parsed_dir)``.
Mirrors the preamble shared by ``parse_mineru``/``parse_docling``/
``parse_native``: canonicalize the document name defensively (so
direct callers may pass absolute or hint-bearing paths) and derive
the ``__parsed__/<base>.parsed/`` output directory.
"""
from lightrag.utils_pipeline import (
normalize_document_file_path,
parsed_artifact_dir_for,
)
source_path = self.source_path(parser_engine)
document_name = normalize_document_file_path(self.file_path)
if document_name == "unknown_source":
document_name = source_path.name or f"{self.doc_id}.bin"
parsed_dir = parsed_artifact_dir_for(
document_name, parent_hint=source_path.parent
)
return ResolvedSource(source_path, document_name, parsed_dir)
async def archive_source(self, source_path: str) -> str | None:
"""Archive the source after a successful parse + full_docs sync.
Resolved through the pipeline module's namespace (where the function
is imported) so existing tests that patch
``lightrag.pipeline.archive_docx_source_after_full_docs_sync`` keep
intercepting it now that the call site lives in the parser layer.
"""
import lightrag.pipeline as _pipeline
return await _pipeline.archive_docx_source_after_full_docs_sync(source_path)
@dataclass
class ParseResult:
"""Structured parser output.
``to_dict`` only emits fields that carry meaning so the dict matches the
pre-refactor return shapes exactly (no spurious ``None``/``False`` keys).
"""
doc_id: str
file_path: str
parse_format: str
content: str
blocks_path: str = ""
parse_engine: str | None = None
parse_stage_skipped: bool = False
parse_warnings: dict[str, Any] | None = None
def to_dict(self) -> dict[str, Any]:
out: dict[str, Any] = {
"doc_id": self.doc_id,
"file_path": self.file_path,
"parse_format": self.parse_format,
"content": self.content,
"blocks_path": self.blocks_path,
}
if self.parse_engine is not None:
out["parse_engine"] = self.parse_engine
if self.parse_stage_skipped:
out["parse_stage_skipped"] = True
if self.parse_warnings:
out["parse_warnings"] = self.parse_warnings
return out
class BaseParser(ABC):
"""Abstract base for every parser engine.
Subclasses set ``engine_name`` and implement :meth:`parse`. Capability
metadata (suffixes/queue group/endpoint) is declared in the registry
``ParserSpec``, not here.
"""
engine_name: str
@abstractmethod
async def parse(self, ctx: ParseContext) -> ParseResult:
"""Parse one document and return its :class:`ParseResult`."""
...