Sixty-three comments said what the next statement already said: # Connect to LanceDB above connect_lancedb, # Path object above isinstance(source, Path), # Get page numbers from provenance above the prov loop, # Clear and populate results above list_view.clear(). They cost a read and carry nothing. The line is whether a comment restates one statement or labels a phase. Phase labels stay: the migrations keep # Create staging table with new schema and # Copy from staging to final table in batches, each heading ten lines of a long procedure. So do comments carrying a fact the code cannot: the merge_insert update-only note on document_meta, why the poller builds sources eagerly, why create_document_from_source returns a list for directories, that indexes need training data, the field-group markers in the config models, and the file:// URL-encoding note in create_document_from_source. capabilities/ is untouched. Its docstrings sit next to prompt surface, and changing them needs an eval to back it. The cassette-recording docs were wrong three ways. They named tests/test_qa.py::test_qa_anthropic, which no longer exists; they targeted whole modules, so a rewrite would re-record cassettes for services the recorder is not running; and they used COHERE_API_KEY where the SDK reads CO_API_KEY. docs/development.md now names exact tests with -n0, and the keyed example is test_cohere_reranker, which owns the one cassette recording api.cohere.com.
156 lines
5.6 KiB
Python
156 lines
5.6 KiB
Python
import asyncio
|
|
import json
|
|
from collections.abc import AsyncGenerator
|
|
from dataclasses import dataclass
|
|
|
|
import httpx
|
|
|
|
from haiku.rag.config import AppConfig
|
|
|
|
|
|
@dataclass
|
|
class DownloadProgress:
|
|
"""Progress event for model downloads."""
|
|
|
|
model: str
|
|
status: str
|
|
completed: int = 0
|
|
total: int = 0
|
|
digest: str = ""
|
|
|
|
|
|
async def download_models(
|
|
config: AppConfig,
|
|
) -> AsyncGenerator[DownloadProgress, None]:
|
|
"""Download required models per config, yielding progress events.
|
|
|
|
Yields DownloadProgress events for:
|
|
- Docling models
|
|
- HuggingFace tokenizer
|
|
- Sentence-transformers embedder (if configured)
|
|
- HuggingFace reranker models (cross-encoder, jina-local)
|
|
- Ollama models
|
|
"""
|
|
try:
|
|
from docling.utils.model_downloader import download_models
|
|
|
|
yield DownloadProgress(model="docling", status="start")
|
|
await asyncio.to_thread(download_models)
|
|
yield DownloadProgress(model="docling", status="done")
|
|
except ImportError: # pragma: no cover - docling installed in test env
|
|
pass
|
|
|
|
# HuggingFace tokenizer
|
|
from transformers import AutoTokenizer
|
|
|
|
tokenizer_name = config.processing.chunking_tokenizer
|
|
yield DownloadProgress(model=tokenizer_name, status="start")
|
|
await asyncio.to_thread(AutoTokenizer.from_pretrained, tokenizer_name)
|
|
yield DownloadProgress(model=tokenizer_name, status="done")
|
|
|
|
# Sentence-transformers embedder
|
|
if config.embeddings.model.provider == "sentence-transformers": # pragma: no cover
|
|
try:
|
|
from sentence_transformers import ( # type: ignore[import-not-found]
|
|
SentenceTransformer,
|
|
)
|
|
|
|
model_name = config.embeddings.model.name
|
|
yield DownloadProgress(model=model_name, status="start")
|
|
# Wrap in lambda: ty loses ParamSpec inference on third-party __init__.
|
|
await asyncio.to_thread(lambda: SentenceTransformer(model_name))
|
|
yield DownloadProgress(model=model_name, status="done")
|
|
except ImportError:
|
|
pass
|
|
|
|
# HuggingFace reranker models
|
|
if config.reranking.model: # pragma: no cover
|
|
provider = config.reranking.model.provider
|
|
model_name = config.reranking.model.name
|
|
|
|
if provider == "cross-encoder":
|
|
try:
|
|
from sentence_transformers import ( # type: ignore[import-not-found]
|
|
CrossEncoder,
|
|
)
|
|
|
|
yield DownloadProgress(model=model_name, status="start")
|
|
await asyncio.to_thread(lambda: CrossEncoder(model_name))
|
|
yield DownloadProgress(model=model_name, status="done")
|
|
except ImportError:
|
|
pass
|
|
|
|
elif provider == "jina-local":
|
|
try:
|
|
from transformers import AutoModel
|
|
|
|
yield DownloadProgress(model=model_name, status="start")
|
|
await asyncio.to_thread(
|
|
AutoModel.from_pretrained,
|
|
model_name,
|
|
trust_remote_code=True,
|
|
)
|
|
yield DownloadProgress(model=model_name, status="done")
|
|
except ImportError:
|
|
pass
|
|
|
|
# Collect Ollama models from config
|
|
required_models: set[str] = set()
|
|
if config.embeddings.model.provider == "ollama":
|
|
required_models.add(config.embeddings.model.name)
|
|
if config.qa.model.provider == "ollama":
|
|
required_models.add(config.qa.model.name)
|
|
if config.reranking.model and config.reranking.model.provider == "ollama":
|
|
required_models.add(config.reranking.model.name)
|
|
pic_desc = config.processing.conversion_options.picture_description
|
|
if (
|
|
config.processing.pictures == "description"
|
|
and pic_desc.model.provider == "ollama"
|
|
):
|
|
required_models.add(pic_desc.model.name)
|
|
if (
|
|
config.processing.auto_title
|
|
and config.processing.title_model.provider == "ollama"
|
|
):
|
|
required_models.add(config.processing.title_model.name)
|
|
|
|
if not required_models:
|
|
return
|
|
|
|
base_url = config.providers.ollama.base_url
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=None) as client:
|
|
for model in sorted(required_models):
|
|
yield DownloadProgress(model=model, status="pulling")
|
|
|
|
async with client.stream(
|
|
"POST", f"{base_url}/api/pull", json={"model": model}
|
|
) as r:
|
|
async for line in r.aiter_lines():
|
|
if not line:
|
|
continue
|
|
try:
|
|
data = json.loads(line)
|
|
status = data.get("status", "")
|
|
digest = data.get("digest", "")
|
|
|
|
if digest and "total" in data:
|
|
yield DownloadProgress(
|
|
model=model,
|
|
status="downloading",
|
|
total=data.get("total", 0),
|
|
completed=data.get("completed", 0),
|
|
digest=digest,
|
|
)
|
|
elif status:
|
|
yield DownloadProgress(model=model, status=status)
|
|
except json.JSONDecodeError:
|
|
pass
|
|
|
|
yield DownloadProgress(model=model, status="done")
|
|
except httpx.ConnectError:
|
|
raise ConnectionError(
|
|
f"Cannot connect to Ollama at {base_url}. "
|
|
"Is Ollama running? Start it with 'ollama serve'."
|
|
)
|