mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-03 22:13:01 +00:00
145 lines
5.1 KiB
Python
145 lines
5.1 KiB
Python
"""Base parser and config class."""
|
|
|
|
import importlib.util
|
|
import logging
|
|
import sys
|
|
from abc import abstractmethod
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Union
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class DocumentParseError(Exception):
|
|
"""A file could not be converted to text.
|
|
|
|
Raised instead of returning the failure message as the document body: a
|
|
parser that hands back its own traceback produces an attachment/source
|
|
whose "content" is an error string, which then reaches the LLM as if it
|
|
were the document. Callers should treat this as a failed upload and tell
|
|
the user, rather than storing anything.
|
|
|
|
Deterministic for a given input, so it is marked non-retryable on the
|
|
Celery tasks that parse (alongside ``DataError`` and
|
|
``AttachmentRejectedError``) — a poison file fails once instead of
|
|
re-failing identically on every retry.
|
|
"""
|
|
|
|
|
|
class NoTextLayerError(DocumentParseError):
|
|
"""A readable document with no text to extract, such as a scanned PDF.
|
|
|
|
Still a failed parse wherever text is the point (source ingestion needs
|
|
something to embed), so it subclasses ``DocumentParseError`` and callers
|
|
that do not know about it keep failing loudly. The file itself is intact,
|
|
though: a chat attachment can still go to a model that reads the format
|
|
natively, which is why the attachment worker catches this type.
|
|
"""
|
|
|
|
|
|
class BaseParser:
|
|
"""Base class for all parsers."""
|
|
|
|
def __init__(self, parser_config: Optional[Dict] = None):
|
|
"""Init params."""
|
|
self._parser_config = parser_config
|
|
|
|
def init_parser(self) -> None:
|
|
"""Init parser and store it."""
|
|
parser_config = self._init_parser()
|
|
self._parser_config = parser_config
|
|
|
|
@property
|
|
def parser_config_set(self) -> bool:
|
|
"""Check if parser config is set."""
|
|
return self._parser_config is not None
|
|
|
|
@property
|
|
def parser_config(self) -> Dict:
|
|
"""Check if parser config is set."""
|
|
if self._parser_config is None:
|
|
raise ValueError("Parser config not set.")
|
|
return self._parser_config
|
|
|
|
@abstractmethod
|
|
def _init_parser(self) -> Dict:
|
|
"""Initialize the parser with the config."""
|
|
|
|
@abstractmethod
|
|
def parse_file(self, file: Path, errors: str = "ignore") -> Union[str, List[str]]:
|
|
"""Parse file."""
|
|
|
|
def get_file_metadata(self, file: Path) -> Dict:
|
|
"""Return parser-specific metadata for the most recently parsed file."""
|
|
_ = file
|
|
return {}
|
|
|
|
|
|
def module_available(name: str) -> bool:
|
|
"""Whether top-level module ``name`` can be imported, without importing it.
|
|
|
|
Cheap enough to call while building a parser map. Honors ``sys.modules``
|
|
first so tests that stub a module in (or block one out with ``None``)
|
|
are seen the same way the import system would see them; a stub with no
|
|
``__spec__`` counts as present rather than raising.
|
|
|
|
Args:
|
|
name: Top-level module name, e.g. ``"docling"``.
|
|
|
|
Returns:
|
|
True when an import of ``name`` would succeed.
|
|
"""
|
|
if name in sys.modules:
|
|
return sys.modules[name] is not None
|
|
try:
|
|
return importlib.util.find_spec(name) is not None
|
|
except (ImportError, ValueError):
|
|
return False
|
|
|
|
|
|
def delegate_parse(
|
|
parser: "BaseParser", file: Path, errors: str
|
|
) -> Union[str, List[str]]:
|
|
"""Run a fallback parser, normalizing its failures to ``DocumentParseError``.
|
|
|
|
Used wherever one parser hands a file to another (docling to the plain
|
|
tabular parsers for oversized sheets, anydoc to docling/legacy parsers
|
|
for files it cannot convert). Wrapping matters twice over: the Celery
|
|
tasks that own these paths list ``DocumentParseError`` in
|
|
``dont_autoretry_for``, so a deterministic content error fails once
|
|
instead of retrying four times; and ``SimpleDirectoryReader.load_data``
|
|
catches only ``DocumentParseError``, so one poison file is skipped into
|
|
``failed_files`` instead of aborting an entire multi-file ingest.
|
|
|
|
Args:
|
|
parser: The parser to delegate to; initialized here if needed.
|
|
file: Path to the file being parsed.
|
|
errors: Decoding error policy, forwarded to the parser.
|
|
|
|
Returns:
|
|
The parsed text, or list of row strings.
|
|
|
|
Raises:
|
|
DocumentParseError: If the fallback parser fails for any reason.
|
|
"""
|
|
try:
|
|
if not parser.parser_config_set:
|
|
parser.init_parser()
|
|
return parser.parse_file(file, errors)
|
|
except DocumentParseError:
|
|
raise
|
|
except ImportError:
|
|
# A fallback whose dependency is missing is a deployment problem, not
|
|
# a bad document: let it reach the ingest task's setup-error path
|
|
# instead of blaming every file with "could not be read".
|
|
raise
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Fallback parse of {Path(file).name} with "
|
|
f"{type(parser).__name__} failed: {e}",
|
|
exc_info=True,
|
|
)
|
|
raise DocumentParseError(
|
|
f"Failed to parse {Path(file).name}: the file could not be read."
|
|
) from e
|