sofhiaazzhr's picture
[NOTICKET] feat(knowledge-parsing): wire Mistral OCR backend end-to-end
6856ea8
Raw History Blame
3.24 kB
"""Mistral OCR API call — the Mistral counterpart of MinerU's `do_parse`.
Kept out of parse.py so the API client (upload → ocr.process) stays separate
from the caching/cache-key logic. Returns the raw response dict; parse.py owns
persisting and caching it. normalize_mistral.py turns it into chunks.
No GPU: this is a hosted API. The only precondition is `MISTRAL_API_KEY`, read
from the environment (falling back to the repo `.env` if python-dotenv is
present), which is why the GPU check in parse.require_gpu never runs for it.
"""
from __future__ import annotations
import os
from pathlib import Path
from typing import Any
from .config import PipelineConfig
def _api_key() -> str:
key = os.environ.get("MISTRAL_API_KEY")
if not key:
try:
from dotenv import load_dotenv
load_dotenv() # picks up the repo .env, same file the app reads
key = os.environ.get("MISTRAL_API_KEY")
except ImportError:
pass
if not key:
raise RuntimeError(
"MISTRAL_API_KEY is not set.\n"
" -> put it in the repo .env (MISTRAL_API_KEY=...) or the environment."
)
return key
def run_ocr(source: Path, cfg: PipelineConfig) -> dict[str, Any]:
"""Upload one PDF and run Mistral OCR. Returns the response as a dict."""
try:
from mistralai import Mistral # type: ignore[attr-defined]
except ImportError: # SDK 2.x moved the class to a submodule
from mistralai.client import Mistral
client = Mistral(api_key=_api_key())
with source.open("rb") as f:
uploaded = client.files.upload(
file={"file_name": source.name, "content": f}, purpose="ocr"
)
kwargs: dict[str, Any] = {
"model": cfg.mistral_model,
"table_format": cfg.mistral_table_format,
"include_blocks": cfg.mistral_include_blocks,
"include_image_base64": cfg.mistral_describe_images,
}
if cfg.mistral_describe_images:
# Per-image description (generated_description). Guarded: an SDK without
# the annotation helpers must degrade to OCR-without-descriptions, not
# abort — exactly as MinerU's `pipeline` backend produces none.
try:
from mistralai.extra import response_format_from_pydantic_model
from pydantic import BaseModel
class _ImageDescription(BaseModel):
description: str
chart_type: str | None = None
kwargs["bbox_annotation_format"] = response_format_from_pydantic_model(
_ImageDescription
)
except Exception: # noqa: BLE001, S110 - optional capability, never fatal
pass
# file_id is the newer path; if this SDK/model rejects it, fall back to a
# signed URL, which works on every version.
try:
resp = client.ocr.process(document={"file_id": uploaded.id}, **kwargs)
except Exception: # noqa: BLE001
signed = client.files.get_signed_url(file_id=uploaded.id)
resp = client.ocr.process(
document={"type": "document_url", "document_url": signed.url}, **kwargs
)
dumped: dict[str, Any] = resp.model_dump()
return dumped