Spaces:
Sleeping
Sleeping
| """Parser 工厂: 智能路由 + 降级链. | |
| 智能路由逻辑: | |
| 1. 查 settings.parser_primary (默认 docling) | |
| 2. 主 parser 失败 -> 降级到 settings.parser_fallback | |
| 3. 全部失败 -> 抛 IngestionFailedError | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| from pathlib import Path | |
| from app.config import settings | |
| from app.core.errors import IngestionFailedError | |
| from app.services.parsers.base_parser import BaseParser, ParsedDocument | |
| logger = logging.getLogger(__name__) | |
| # 延迟注册: 实际 import 在 _build_parser 内 | |
| _REGISTRY: dict[str, type[BaseParser]] = {} | |
| def _register_default() -> None: | |
| """懒加载各 parser. 失败的 (未装) 仅记 warning, 不抛.""" | |
| if _REGISTRY: | |
| return | |
| try: | |
| from app.services.parsers.docling_parser import DoclingParser | |
| _REGISTRY["docling"] = DoclingParser | |
| except ImportError as e: | |
| logger.warning("docling not installed: %s", e) | |
| try: | |
| from app.services.parsers.simple_parser import SimpleParser | |
| _REGISTRY["simple"] = SimpleParser | |
| except ImportError as e: | |
| logger.warning("simple parser not installed: %s", e) | |
| # Markdown parser — 零依赖, 始终可用 | |
| from app.services.parsers.markdown_parser import MarkdownParser | |
| _REGISTRY["markdown"] = MarkdownParser | |
| # mineru / vlm 留 hook (按需装) | |
| def _build_parser(name: str) -> BaseParser: | |
| _register_default() | |
| cls = _REGISTRY.get(name) | |
| if cls is None: | |
| raise IngestionFailedError( | |
| f"Parser '{name}' is not installed. pip install docling / marker-pdf.", | |
| code="parser_unavailable", | |
| ) | |
| return cls() | |
| def get_parser(name: str | None = None) -> BaseParser: | |
| """获取单个 parser 实例 (按名字).""" | |
| return _build_parser(name or settings.parser_primary) | |
| def get_parser_chain() -> list[BaseParser]: | |
| """按 settings 配置返回 [primary, fallback] 链. | |
| 此外, 始终把零依赖的 markdown parser 追加到链尾 — 它支持的扩展名 | |
| (md/markdown) 跟 primary/fallback 都不重叠, 互不干扰. 这样 | |
| PARSER_PRIMARY=docling + PARSER_FALLBACK=simple 的生产配置, 用户 | |
| 上传 .md 文件也能直接被 markdown parser 接管, 不需要改 env. | |
| """ | |
| chain: list[BaseParser] = [] | |
| for name in (settings.parser_primary, settings.parser_fallback): | |
| if name and name not in {p.name for p in chain}: | |
| try: | |
| chain.append(_build_parser(name)) | |
| except IngestionFailedError: | |
| # 跳过未装的, 继续 | |
| continue | |
| # 始终补一个 markdown parser (零依赖, 必装), 除非已存在于链 | |
| if "markdown" not in {p.name for p in chain}: | |
| try: | |
| chain.append(_build_parser("markdown")) | |
| except IngestionFailedError: | |
| pass | |
| return chain | |
| async def parse_with_fallback(file_path: Path) -> ParsedDocument: | |
| """按链逐个尝试, 全部失败抛 IngestionFailedError.""" | |
| chain = get_parser_chain() | |
| if not chain: | |
| raise IngestionFailedError( | |
| "No parser available. Install at least one of: docling, marker-pdf.", | |
| code="no_parser_available", | |
| ) | |
| # 选能处理该扩展名的 parser | |
| candidates = [p for p in chain if p.can_handle(file_path)] | |
| if not candidates: | |
| raise IngestionFailedError( | |
| f"No parser in chain supports {file_path.suffix}", | |
| code="unsupported_format", | |
| detail={"suffix": file_path.suffix, "chain": [p.name for p in chain]}, | |
| ) | |
| last_err: Exception | None = None | |
| last_traceback: str | None = None | |
| for parser in candidates: | |
| try: | |
| return await parser.parse(file_path) | |
| except Exception as e: # noqa: BLE001 | |
| import traceback | |
| tb = traceback.format_exc() | |
| logger.warning("Parser %s failed for %s: %s\n%s", parser.name, file_path.name, e, tb) | |
| last_err = e | |
| last_traceback = tb | |
| # 把最后一个 parser 的具体异常信息暴露给前端, 方便诊断 | |
| err_msg = f"All parsers failed for {file_path.name}" | |
| if last_err: | |
| err_msg += f" (last: {type(last_err).__name__}: {last_err})" | |
| raise IngestionFailedError( | |
| err_msg, | |
| code="all_parsers_failed", | |
| detail={ | |
| "chain": [p.name for p in candidates], | |
| "last_error_type": type(last_err).__name__ if last_err else None, | |
| "last_error_msg": str(last_err)[:500] if last_err else None, | |
| "last_traceback": (last_traceback or "")[-1500:], # 末 1.5KB, 防爆 | |
| }, | |
| ) from last_err | |
| __all__ = [ | |
| "BaseParser", | |
| "ParsedDocument", | |
| "PageContent", | |
| "get_parser", | |
| "get_parser_chain", | |
| "parse_with_fallback", | |
| ] | |