haiku.rag/haiku_rag_slim/haiku/rag/converters/base.py
Yiorgis Gozadinos 7f7223e0ac
Default to ollama:qwen3.8
Replaces gpt-oss on ModelConfig, qa.model and processing.title_model, and
ministral-3 on the picture-description model. qa.model.vision follows the
model and is now true.

enable_thinking was gated on the gpt-oss name, so it did nothing for
qwen3.8. With title_model's max_tokens of 100 the reasoning consumed the
whole budget and title generation returned an empty string. The mapping
now applies to any ollama model via reasoning_effort(): false sends
"none", true sends "high". Measured on qwen3.8:27b-mlx, "low" does not
disable thinking and "none" does; gpt-oss is the inverse, its template
has no "none" level, so it keeps "low".

Picture description bypasses get_model -- docling posts the request
itself from a params dict -- so the flag was inert on that path too.
vlm_api_params() carries reasoning_effort into both converters' request
bodies. At max_tokens 200 the description survived either way, but the
switch cut completion tokens from 141 to 45.

test_search_tool_skips_binary_content_when_qa_model_is_text_only asserted
the vision default rather than setting it; it now configures vision=False
itself.

docs/benchmarks.md keeps ministral-3: those are recorded measurements.
2026-09-04 12:36:36 +03:00

124 lines
4.1 KiB
Python

"""Base class for document converters."""
import os
from abc import ABC, abstractmethod
from pathlib import Path
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from docling_core.types.doc.document import DoclingDocument
from haiku.rag.config import AppConfig
from haiku.rag.config.models import ModelConfig
def vlm_api_url(config: "AppConfig", model: "ModelConfig") -> str:
"""Construct the VLM chat-completions URL for a picture-description model."""
if model.base_url:
return f"{model.base_url.rstrip('/')}/v1/chat/completions"
if model.provider == "ollama":
return f"{config.providers.ollama.base_url.rstrip('/')}/v1/chat/completions"
if model.provider == "openai":
return "https://api.openai.com/v1/chat/completions"
raise ValueError(f"Unsupported VLM provider: {model.provider}")
def vlm_api_headers(model: "ModelConfig") -> dict[str, str]:
"""Auth headers for the picture-description VLM endpoint. Docling posts to
it directly, so the key travels as a header rather than through an SDK.
The public OpenAI endpoint falls back to ``OPENAI_API_KEY``. A custom
``base_url`` never does: that key belongs to api.openai.com, not to
whatever self-hosted server the model points at.
"""
key = model.api_key
if not key and model.provider == "openai" and not model.base_url:
key = os.environ.get("OPENAI_API_KEY")
if key:
return {"Authorization": f"Bearer {key}"}
return {}
def vlm_api_params(model: "ModelConfig", max_tokens: int) -> dict[str, object]:
"""Request body fields docling posts alongside the picture."""
from haiku.rag.utils import reasoning_effort
params: dict[str, object] = {
"model": model.name,
"max_completion_tokens": max_tokens,
}
effort = reasoning_effort(model)
if effort is not None:
params["reasoning_effort"] = effort
return params
class DocumentConverter(ABC):
"""Abstract base class for document converters.
Document converters are responsible for converting various document formats
(PDF, DOCX, HTML, etc.) into DoclingDocument format for further processing.
"""
@property
@abstractmethod
def supported_extensions(self) -> list[str]:
"""Return list of file extensions supported by this converter.
Returns:
List of file extensions (including the dot, e.g., [".pdf", ".docx"]).
"""
pass
@abstractmethod
async def convert_file(
self, path: Path, source_uri: str | None = None
) -> "DoclingDocument":
"""Convert a file to DoclingDocument format.
Args:
path: Path to the file to convert.
source_uri: Optional origin URI (e.g. the URL the file was
downloaded from) used by docling's HTML/Markdown backends to
resolve relative `<img src="/path">` references. Ignored by
converters that have no equivalent backend option (notably
docling-serve).
Returns:
DoclingDocument representation of the file.
Raises:
ValueError: If the file cannot be converted.
"""
pass
SUPPORTED_FORMATS = ("md", "html", "plain")
@abstractmethod
async def convert_text(
self,
text: str,
name: str = "content.md",
format: str = "md",
source_uri: str | None = None,
) -> "DoclingDocument":
"""Convert text content to DoclingDocument format.
Args:
text: The text content to convert.
name: The name to use for the document (defaults to "content.md").
format: The format of the text content ("md", "html", or "plain").
Defaults to "md". Use "plain" for plain text without parsing.
source_uri: Optional origin URI used by docling's HTML/Markdown
backends to resolve relative image references.
Returns:
DoclingDocument representation of the text.
Raises:
ValueError: If the text cannot be converted or format is unsupported.
"""
pass