"""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__/.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`.""" ...