>(new Set());
- // Reset local state when modal opens
+ // Refetch on every open so newly-added or deleted documents show up.
useEffect(() => {
- if (isOpen) {
- setLocalSelected(new Set(selected));
- setSearchTerm("");
- }
- }, [isOpen, selected]);
+ if (!isOpen) return;
+ setLoading(true);
+ fetch("/api/documents")
+ .then((res) => res.json())
+ .then((data) => {
+ setDocuments(data.documents || []);
+ setLoading(false);
+ })
+ .catch(() => {
+ setLoading(false);
+ });
+ }, [isOpen]);
- // Fetch documents when modal opens
+ // Seed local selection from the parent's display-name list once documents
+ // are available. Any doc whose display name is in `selected` starts checked.
useEffect(() => {
- if (isOpen && documents.length === 0) {
- setLoading(true);
- fetch("/api/documents")
- .then((res) => res.json())
- .then((data) => {
- setDocuments(data.documents || []);
- setLoading(false);
- })
- .catch(() => {
- setLoading(false);
- });
- }
- }, [isOpen, documents.length]);
+ if (!isOpen) return;
+ const selectedNames = new Set(selected);
+ setLocalSelected(
+ new Set(
+ documents
+ .filter((d) => selectedNames.has(getDisplayName(d)))
+ .map((d) => d.id),
+ ),
+ );
+ setSearchTerm("");
+ }, [isOpen, selected, documents]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
@@ -63,20 +72,24 @@ export default function DocumentFilter({
[onClose],
);
- const toggleDocument = (displayName: string) => {
+ const toggleDocument = (docId: string) => {
setLocalSelected((prev) => {
const next = new Set(prev);
- if (next.has(displayName)) {
- next.delete(displayName);
+ if (next.has(docId)) {
+ next.delete(docId);
} else {
- next.add(displayName);
+ next.add(docId);
}
return next;
});
};
const handleApply = () => {
- onApply(Array.from(localSelected));
+ const names = documents
+ .filter((d) => localSelected.has(d.id))
+ .map(getDisplayName);
+ // Dedupe: two selected docs sharing a title collapse to one filter term.
+ onApply(Array.from(new Set(names)));
onClose();
};
@@ -84,8 +97,6 @@ export default function DocumentFilter({
setLocalSelected(new Set());
};
- const getDisplayName = (doc: Document) => doc.title || doc.uri || doc.id;
-
const filteredDocuments = documents.filter((doc) => {
if (!searchTerm) return true;
const displayName = getDisplayName(doc).toLowerCase();
@@ -144,8 +155,8 @@ export default function DocumentFilter({
diff --git a/docs/configuration/index.md b/docs/configuration/index.md
index 9db9e9cc..56995b44 100644
--- a/docs/configuration/index.md
+++ b/docs/configuration/index.md
@@ -171,7 +171,8 @@ custom_config = AppConfig(
)
# Pass configuration to the client
-client = HaikuRAG(config=custom_config)
+async with HaikuRAG(config=custom_config) as client:
+ ...
```
If you don't pass a config, the client uses the global configuration loaded from your YAML file or defaults.
diff --git a/docs/development.md b/docs/development.md
index e72e4ddf..b3da4687 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -102,7 +102,7 @@ Integration tests are skipped in CI but run locally when you have the required s
```bash
uv run ruff check
uv run ruff format
-uv run pyright
+uv run ty check
```
## Mock API Keys
diff --git a/haiku_rag_slim/haiku/rag/app.py b/haiku_rag_slim/haiku/rag/app.py
index ec98136c..768a6e5c 100644
--- a/haiku_rag_slim/haiku/rag/app.py
+++ b/haiku_rag_slim/haiku/rag/app.py
@@ -59,8 +59,8 @@ class HaikuRAGApp: # pragma: no cover
return
# Create the database
- client = HaikuRAG(db_path=self.db_path, config=self.config, create=True)
- client.close()
+ async with HaikuRAG(db_path=self.db_path, config=self.config, create=True):
+ pass
self.console.print(
f"[bold green]Database initialized at {self._display_path}[/bold green]"
)
@@ -88,8 +88,8 @@ class HaikuRAGApp: # pragma: no cover
# Connect directly. Don't go through Store so a database that is
# missing tables (e.g. pre-migration) still reports what it can.
- db = connect_lancedb(self.config, self.db_path)
- stats = get_database_stats(db)
+ db = await connect_lancedb(self.config, self.db_path)
+ stats = await get_database_stats(db)
if not any(entry["exists"] for entry in stats.values()):
self.console.print(
@@ -104,14 +104,10 @@ class HaikuRAGApp: # pragma: no cover
embed_model = "unknown"
vector_dim = None
if stats["settings"]["exists"]:
- settings_tbl = db.open_table("settings")
+ settings_tbl = await db.open_table("settings")
rows = (
- settings_tbl.search()
- .where("id = 'settings'")
- .limit(1)
- .to_arrow()
- .to_pylist()
- )
+ await settings_tbl.query().where("id = 'settings'").limit(1).to_arrow()
+ ).to_pylist()
if rows:
raw = rows[0].get("settings") or "{}"
data = json.loads(raw) if isinstance(raw, str) else (raw or {})
@@ -237,50 +233,46 @@ class HaikuRAGApp: # pragma: no cover
self.console.print("[red]Database path does not exist.[/red]")
return
- store = Store(
+ async with Store(
self.db_path,
config=self.config,
skip_validation=True,
read_only=True,
skip_migration_check=True,
before=self.before,
- )
+ ) as store:
+ tables = ["documents", "chunks", "settings"]
+ if table:
+ if table not in tables:
+ self.console.print(
+ f"[red]Unknown table: {table}. Must be one of: {', '.join(tables)}[/red]"
+ )
+ return
+ tables = [table]
- tables = ["documents", "chunks", "settings"]
- if table:
- if table not in tables:
- self.console.print(
- f"[red]Unknown table: {table}. Must be one of: {', '.join(tables)}[/red]"
- )
- store.close()
- return
- tables = [table]
+ self.console.print("[bold]Version History[/bold]")
- self.console.print("[bold]Version History[/bold]")
+ for table_name in tables:
+ versions = await store.list_table_versions(table_name)
- for table_name in tables:
- versions = store.list_table_versions(table_name)
+ # Sort by version descending (newest first)
+ versions = sorted(versions, key=lambda v: v["version"], reverse=True)
- # Sort by version descending (newest first)
- versions = sorted(versions, key=lambda v: v["version"], reverse=True)
+ if limit:
+ versions = versions[:limit]
- if limit:
- versions = versions[:limit]
+ self.console.print(f"\n[bold cyan]{table_name}[/bold cyan]")
- self.console.print(f"\n[bold cyan]{table_name}[/bold cyan]")
+ if not versions:
+ self.console.print(" [dim]No versions found[/dim]")
+ continue
- if not versions:
- self.console.print(" [dim]No versions found[/dim]")
- continue
-
- for v in versions:
- version_num = v["version"]
- timestamp = v["timestamp"]
- self.console.print(
- f" [repr.attrib_name]v{version_num}[/repr.attrib_name]: {timestamp}"
- )
-
- store.close()
+ for v in versions:
+ version_num = v["version"]
+ timestamp = v["timestamp"]
+ self.console.print(
+ f" [repr.attrib_name]v{version_num}[/repr.attrib_name]: {timestamp}"
+ )
async def list_documents(self, filter: str | None = None):
async with HaikuRAG(
@@ -601,7 +593,7 @@ class HaikuRAGApp: # pragma: no cover
await client.vacuum()
self.console.print("[bold green]Vacuum completed successfully.[/bold green]")
- def migrate(self) -> list[str]:
+ async def migrate(self) -> list[str]:
"""Run pending database migrations.
Returns:
@@ -609,17 +601,13 @@ class HaikuRAGApp: # pragma: no cover
"""
from haiku.rag.store.engine import Store
- store = Store(
+ async with Store(
self.db_path,
config=self.config,
skip_validation=True,
skip_migration_check=True,
- )
- try:
- applied = store.migrate()
- return applied
- finally:
- store.close()
+ ) as store:
+ return await store.migrate()
async def create_index(self):
"""Create vector index on the chunks table."""
@@ -630,7 +618,7 @@ class HaikuRAGApp: # pragma: no cover
read_only=self.read_only,
before=self.before,
) as client:
- row_count = client.store.chunks_table.count_rows()
+ row_count = await client.store.chunks_table.count_rows()
self.console.print(f"Chunks in database: {row_count}")
if row_count < 256:
@@ -640,7 +628,7 @@ class HaikuRAGApp: # pragma: no cover
return
# Check if index already exists
- indices = client.store.chunks_table.list_indices()
+ indices = await client.store.chunks_table.list_indices()
has_vector_index = any("vector" in str(idx).lower() for idx in indices)
if has_vector_index:
@@ -650,23 +638,21 @@ class HaikuRAGApp: # pragma: no cover
else:
self.console.print("[bold]Creating vector index...[/bold]")
- client.store._ensure_vector_index()
+ await client.store._ensure_vector_index()
self.console.print(
"[bold green]Vector index created successfully.[/bold green]"
)
async def download_models(self):
"""Download Docling, HuggingFace tokenizer, and Ollama models per config."""
- from haiku.rag.client import HaikuRAG
-
- client = HaikuRAG(db_path=None, config=self.config)
+ from haiku.rag.client.downloads import download_models
progress: Progress | None = None
task_id: TaskID | None = None
current_model = ""
current_digest = ""
- async for event in client.download_models():
+ async for event in download_models(self.config):
if event.status == "start":
self.console.print(
f"[bold blue]Downloading {event.model}...[/bold blue]"
diff --git a/haiku_rag_slim/haiku/rag/cli.py b/haiku_rag_slim/haiku/rag/cli.py
index 1210d73b..d103cda9 100644
--- a/haiku_rag_slim/haiku/rag/cli.py
+++ b/haiku_rag_slim/haiku/rag/cli.py
@@ -529,7 +529,7 @@ def migrate( # pragma: no cover
):
app = create_app(db)
try:
- applied = app.migrate()
+ applied = asyncio.run(app.migrate())
if applied:
typer.echo(f"Applied {len(applied)} migration(s):")
for desc in applied:
diff --git a/haiku_rag_slim/haiku/rag/client.py b/haiku_rag_slim/haiku/rag/client.py
deleted file mode 100644
index f905c8fc..00000000
--- a/haiku_rag_slim/haiku/rag/client.py
+++ /dev/null
@@ -1,1839 +0,0 @@
-import asyncio
-import hashlib
-import json
-import logging
-import mimetypes
-import tempfile
-from collections.abc import AsyncGenerator
-from dataclasses import dataclass
-from datetime import datetime
-from enum import Enum
-from pathlib import Path
-from typing import TYPE_CHECKING, overload
-from urllib.parse import urlparse
-
-import httpx
-
-from haiku.rag.config import AppConfig, Config
-from haiku.rag.converters import get_converter
-from haiku.rag.reranking import get_reranker
-from haiku.rag.store.engine import Store
-from haiku.rag.store.models.chunk import Chunk, SearchResult
-from haiku.rag.store.models.document import Document
-from haiku.rag.store.models.document_item import extract_items
-from haiku.rag.store.repositories.chunk import ChunkRepository
-from haiku.rag.store.repositories.document import DocumentRepository
-from haiku.rag.store.repositories.document_item import DocumentItemRepository
-from haiku.rag.store.repositories.settings import SettingsRepository
-from haiku.rag.utils import escape_sql_string
-
-if TYPE_CHECKING:
- from docling_core.types.doc.document import DoclingDocument
-
- from haiku.rag.agents.analysis.models import AnalysisResult
- from haiku.rag.agents.research.models import (
- Citation,
- ResearchReport,
- )
-
-logger = logging.getLogger(__name__)
-
-
-class RebuildMode(Enum):
- """Mode for rebuilding the database."""
-
- FULL = "full" # Re-convert from source, re-chunk, re-embed
- RECHUNK = "rechunk" # Re-chunk from existing content, re-embed
- EMBED_ONLY = "embed_only" # Keep chunks, only regenerate embeddings
- TITLE_ONLY = "title_only" # Only generate titles for untitled documents
-
-
-@dataclass
-class DownloadProgress:
- """Progress event for model downloads."""
-
- model: str
- status: str
- completed: int = 0
- total: int = 0
- digest: str = ""
-
-
-class HaikuRAG:
- """High-level haiku-rag client."""
-
- def __init__(
- self,
- db_path: Path | None = None,
- config: AppConfig = Config,
- skip_validation: bool = False,
- create: bool = False,
- read_only: bool = False,
- before: datetime | None = None,
- ):
- """Initialize the RAG client with a database path.
-
- Args:
- db_path: Path to the database file. If None, uses config.storage.data_dir.
- config: Configuration to use. Defaults to global Config.
- skip_validation: Whether to skip configuration validation on database load.
- create: Whether to create the database if it doesn't exist.
- read_only: Whether to open the database in read-only mode.
- before: Query the database as it existed at this datetime.
- Implies read_only=True.
- """
- self._config = config
- if db_path is None:
- db_path = self._config.storage.data_dir / "haiku.rag.lancedb"
-
- self.store = Store(
- db_path,
- config=self._config,
- skip_validation=skip_validation,
- create=create,
- read_only=read_only,
- before=before,
- )
- self.document_repository = DocumentRepository(self.store)
- self.chunk_repository = ChunkRepository(self.store)
- self.document_item_repository = DocumentItemRepository(self.store)
-
- @property
- def is_read_only(self) -> bool:
- """Whether the client is in read-only mode."""
- return self.store.is_read_only
-
- async def __aenter__(self):
- """Async context manager entry."""
- return self
-
- async def __aexit__(self, exc_type, exc_val, exc_tb): # noqa: ARG002
- """Async context manager exit."""
- # Wait for any pending vacuum to complete before closing
- async with self.store._vacuum_lock:
- pass
- self.close()
- return False
-
- # =========================================================================
- # Processing Primitives
- # =========================================================================
-
- @overload
- async def convert(self, source: Path) -> "DoclingDocument": ...
-
- @overload
- async def convert(
- self, source: str, *, format: str = "md"
- ) -> "DoclingDocument": ...
-
- async def convert(
- self, source: Path | str, *, format: str = "md"
- ) -> "DoclingDocument":
- """Convert a file, URL, or text to DoclingDocument.
-
- Args:
- source: One of:
- - Path: Local file path to convert
- - str (URL): HTTP/HTTPS URL to download and convert
- - str (text): Raw text content to convert
- format: The format of text content ("md", "html", or "plain").
- Defaults to "md". Use "plain" for plain text without parsing.
- Only used when source is raw text (not a file path or URL).
- Files and URLs determine format from extension/content-type.
-
- Returns:
- DoclingDocument from the converted source.
-
- Raises:
- ValueError: If the file doesn't exist or has unsupported extension.
- httpx.RequestError: If URL download fails.
- """
- converter = get_converter(self._config)
-
- # Path object - convert file directly
- if isinstance(source, Path):
- if not source.exists():
- raise ValueError(f"File does not exist: {source}")
- if source.suffix.lower() not in converter.supported_extensions:
- raise ValueError(f"Unsupported file extension: {source.suffix}")
- return await converter.convert_file(source)
-
- # String - check if URL or text
- parsed = urlparse(source)
-
- if parsed.scheme in ("http", "https"):
- # URL - download and convert
- async with httpx.AsyncClient() as http:
- response = await http.get(source)
- response.raise_for_status()
-
- content_type = response.headers.get("content-type", "").lower()
- file_extension = self._get_extension_from_content_type_or_url(
- source, content_type
- )
-
- if file_extension not in converter.supported_extensions:
- raise ValueError(
- f"Unsupported content type/extension: {content_type}/{file_extension}"
- )
-
- with tempfile.NamedTemporaryFile(
- mode="wb", suffix=file_extension, delete=False
- ) as temp_file:
- temp_file.write(response.content)
- temp_file.flush()
- temp_path = Path(temp_file.name)
-
- try:
- return await converter.convert_file(temp_path)
- finally:
- temp_path.unlink(missing_ok=True)
-
- elif parsed.scheme == "file":
- # file:// URI
- file_path = Path(parsed.path)
- if not file_path.exists():
- raise ValueError(f"File does not exist: {file_path}")
- if file_path.suffix.lower() not in converter.supported_extensions:
- raise ValueError(f"Unsupported file extension: {file_path.suffix}")
- return await converter.convert_file(file_path)
-
- else:
- # Treat as text content
- return await converter.convert_text(source, format=format)
-
- async def chunk(self, docling_document: "DoclingDocument") -> list[Chunk]:
- """Chunk a DoclingDocument into Chunks.
-
- Args:
- docling_document: The DoclingDocument to chunk.
-
- Returns:
- List of Chunk objects (without embeddings, without document_id).
- Each chunk has its `order` field set to its position in the list.
- """
- from haiku.rag.chunkers import get_chunker
-
- chunker = get_chunker(self._config)
- return await chunker.chunk(docling_document)
-
- async def _ensure_chunks_embedded(self, chunks: list[Chunk]) -> list[Chunk]:
- """Ensure all chunks have embeddings, embedding any that don't.
-
- Args:
- chunks: List of chunks, some may have embeddings already.
-
- Returns:
- List of chunks with all embeddings populated.
- """
- from haiku.rag.embeddings import embed_chunks
-
- # Find chunks that need embedding
- chunks_to_embed = [c for c in chunks if c.embedding is None]
-
- if not chunks_to_embed:
- return chunks
-
- # Embed chunks that don't have embeddings (returns new Chunk objects)
- embedded = await embed_chunks(chunks_to_embed, self._config)
-
- # Build result maintaining original order
- embedded_map = {(c.content, c.order): c for c in embedded}
- result = []
- for chunk in chunks:
- if chunk.embedding is not None:
- result.append(chunk)
- else:
- result.append(embedded_map[(chunk.content, chunk.order)])
-
- return result
-
- # =========================================================================
- # Title Generation
- # =========================================================================
-
- def _extract_structural_title(
- self, docling_document: "DoclingDocument"
- ) -> str | None:
- """Extract a title from DoclingDocument structural metadata.
-
- Priority: FURNITURE TITLE > BODY TITLE > first SECTION_HEADER.
- """
- from docling_core.types.doc.document import ContentLayer
- from docling_core.types.doc.labels import DocItemLabel
-
- furniture_title = None
- body_title = None
- first_section_header = None
-
- for item in docling_document.texts:
- if item.label == DocItemLabel.TITLE:
- text = item.text.strip()
- if not text:
- continue
- if item.content_layer == ContentLayer.FURNITURE:
- furniture_title = text
- elif body_title is None:
- body_title = text
- elif (
- item.label == DocItemLabel.SECTION_HEADER
- and first_section_header is None
- ):
- text = item.text.strip()
- if text:
- first_section_header = text
-
- return furniture_title or body_title or first_section_header
-
- async def _generate_title_with_llm(self, content: str) -> str | None:
- """Generate a title using LLM from document content."""
- from pydantic_ai import Agent
-
- from haiku.rag.utils import get_model
-
- truncated = content[:2000]
-
- model = get_model(self._config.processing.title_model, self._config)
- agent: Agent[None, str] = Agent(
- model=model,
- output_type=str,
- instructions=(
- "Generate a concise, descriptive title for the following document. "
- "The title should be at most 10 words. "
- "Return ONLY the title text, nothing else."
- ),
- )
- result = await agent.run(truncated)
- title = result.output.strip()
- return title if title else None
-
- async def _resolve_title(
- self,
- docling_document: "DoclingDocument",
- content: str,
- ) -> str | None:
- """Auto-generate a title from document structure or LLM.
-
- Returns None if auto_title is disabled or generation fails.
- """
- if not self._config.processing.auto_title:
- return None
-
- structural = self._extract_structural_title(docling_document)
- if structural:
- return structural
-
- try:
- return await self._generate_title_with_llm(content)
- except Exception:
- logger.warning(
- "LLM title generation failed during ingestion", exc_info=True
- )
- return None
-
- async def generate_title(self, document: Document) -> str | None:
- """Generate a title for a document.
-
- Attempts structural extraction from the stored DoclingDocument,
- then falls back to LLM generation. Bypasses the auto_title config
- since this is an explicit call.
-
- Does NOT update the document — caller decides.
- """
- docling_doc = document.get_docling_document()
- content = document.content or ""
-
- if docling_doc is not None:
- structural = self._extract_structural_title(docling_doc)
- if structural:
- return structural
-
- return await self._generate_title_with_llm(content)
-
- async def _store_document_with_chunks(
- self,
- document: Document,
- chunks: list[Chunk],
- docling_document: "DoclingDocument",
- ) -> Document:
- """Store a document with chunks, embedding any that lack embeddings.
-
- Handles versioning/rollback on failure.
-
- Args:
- document: The document to store (will be created).
- chunks: Chunks to store (will be embedded if lacking embeddings).
- docling_document: The DoclingDocument to extract items from.
-
- Returns:
- The created Document instance with ID set.
- """
- import asyncio
-
- # Ensure all chunks have embeddings before storing
- chunks = await self._ensure_chunks_embedded(chunks)
-
- # Snapshot table versions for versioned rollback (if supported)
- versions = self.store.current_table_versions()
-
- # Create the document
- created_doc = await self.document_repository.create(document)
-
- try:
- assert created_doc.id is not None, (
- "Document ID should not be None after creation"
- )
- # Set document_id and order for all chunks
- for order, chunk in enumerate(chunks):
- chunk.document_id = created_doc.id
- chunk.order = order
-
- # Batch create all chunks in a single operation
- await self.chunk_repository.create(chunks)
-
- # Extract and store document items for context expansion
- items = extract_items(created_doc.id, docling_document)
- await self.document_item_repository.create_items(created_doc.id, items)
-
- # Vacuum old versions in background (non-blocking) if auto_vacuum enabled
- if self._config.storage.auto_vacuum:
- asyncio.create_task(self.store.vacuum())
-
- return created_doc
- except Exception:
- # Roll back to the captured versions and re-raise
- self.store.restore_table_versions(versions)
- raise
-
- async def _update_document_with_chunks(
- self,
- document: Document,
- chunks: list[Chunk],
- docling_document: "DoclingDocument | None" = None,
- ) -> Document:
- """Update a document and replace its chunks, embedding any that lack embeddings.
-
- Handles versioning/rollback on failure.
-
- Args:
- document: The document to update (must have ID set).
- chunks: Chunks to replace existing (will be embedded if lacking embeddings).
- docling_document: The DoclingDocument to extract items from.
- When None, existing items are preserved.
-
- Returns:
- The updated Document instance.
- """
- import asyncio
-
- assert document.id is not None, "Document ID is required for update"
-
- # Ensure all chunks have embeddings before storing
- chunks = await self._ensure_chunks_embedded(chunks)
-
- # Snapshot table versions for versioned rollback
- versions = self.store.current_table_versions()
-
- # Delete existing chunks before writing new ones
- await self.chunk_repository.delete_by_document_id(document.id)
-
- try:
- # Update the document
- updated_doc = await self.document_repository.update(document)
-
- # Set document_id and order for all chunks
- assert updated_doc.id is not None
- for order, chunk in enumerate(chunks):
- chunk.document_id = updated_doc.id
- chunk.order = order
-
- # Batch create all chunks in a single operation
- await self.chunk_repository.create(chunks)
-
- # Replace document items when a new DoclingDocument is provided
- if docling_document is not None:
- await self.document_item_repository.delete_by_document_id(
- updated_doc.id
- )
- items = extract_items(updated_doc.id, docling_document)
- await self.document_item_repository.create_items(updated_doc.id, items)
-
- # Vacuum old versions in background (non-blocking) if auto_vacuum enabled
- if self._config.storage.auto_vacuum:
- asyncio.create_task(self.store.vacuum())
-
- return updated_doc
- except Exception:
- # Roll back to the captured versions and re-raise
- self.store.restore_table_versions(versions)
- raise
-
- async def create_document(
- self,
- content: str,
- uri: str | None = None,
- title: str | None = None,
- metadata: dict | None = None,
- format: str = "md",
- ) -> Document:
- """Create a new document from text content.
-
- Converts the content, chunks it, and generates embeddings.
-
- Args:
- content: The text content of the document.
- uri: Optional URI identifier for the document.
- title: Optional title for the document.
- metadata: Optional metadata dictionary.
- format: The format of the content ("md", "html", or "plain").
- Defaults to "md". Use "plain" for plain text without parsing.
-
- Returns:
- The created Document instance.
- """
- from haiku.rag.embeddings import embed_chunks
-
- # Convert → Chunk → Embed using primitives
- converter = get_converter(self._config)
- docling_document = await converter.convert_text(content, format=format)
- chunks = await self.chunk(docling_document)
- embedded_chunks = await embed_chunks(chunks, self._config)
-
- # Store markdown export as content for better display/readability
- # The original content is preserved in docling_document
- stored_content = docling_document.export_to_markdown()
-
- if title is None:
- title = await self._resolve_title(docling_document, stored_content)
-
- # Create document model
- document = Document(
- content=stored_content,
- uri=uri,
- title=title,
- metadata=metadata or {},
- )
- document.set_docling(docling_document)
-
- # Store document and chunks
- return await self._store_document_with_chunks(
- document, embedded_chunks, docling_document
- )
-
- async def import_document(
- self,
- docling_document: "DoclingDocument",
- chunks: list[Chunk],
- uri: str | None = None,
- title: str | None = None,
- metadata: dict | None = None,
- ) -> Document:
- """Import a pre-processed document with chunks.
-
- Use this when document conversion, chunking, and embedding were done
- externally and you want to store the results in haiku.rag.
-
- Args:
- docling_document: The DoclingDocument to import.
- chunks: Pre-created chunks. Chunks without embeddings will be
- automatically embedded.
- uri: Optional URI identifier for the document.
- title: Optional title for the document.
- metadata: Optional metadata dictionary.
-
- Returns:
- The created Document instance.
- """
- content = docling_document.export_to_markdown()
- if title is None:
- title = await self._resolve_title(docling_document, content)
-
- document = Document(
- content=content,
- uri=uri,
- title=title,
- metadata=metadata or {},
- )
- document.set_docling(docling_document)
-
- return await self._store_document_with_chunks(
- document, chunks, docling_document
- )
-
- async def create_document_from_source(
- self, source: str | Path, title: str | None = None, metadata: dict | None = None
- ) -> Document | list[Document]:
- """Create or update document(s) from a file path, directory, or URL.
-
- Checks if a document with the same URI already exists:
- - If MD5 is unchanged, returns existing document
- - If MD5 changed, updates the document
- - If no document exists, creates a new one
-
- Args:
- source: File path, directory (as string or Path), or URL to parse
- title: Optional title (only used for single files, not directories)
- metadata: Optional metadata dictionary
-
- Returns:
- Document instance (created, updated, or existing) for single files/URLs
- List of Document instances for directories
-
- Raises:
- ValueError: If the file/URL cannot be parsed or doesn't exist
- httpx.RequestError: If URL request fails
- """
- # Normalize metadata
- metadata = metadata or {}
-
- # Check if it's a URL
- source_str = str(source)
- parsed_url = urlparse(source_str)
- if parsed_url.scheme in ("http", "https"):
- return await self._create_or_update_document_from_url(
- source_str, title=title, metadata=metadata
- )
- elif parsed_url.scheme == "file":
- # Handle file:// URI by converting to path
- source_path = Path(parsed_url.path)
- else:
- # Handle as regular file path
- source_path = Path(source) if isinstance(source, str) else source
-
- # Handle directories
- if source_path.is_dir():
- from haiku.rag.monitor import FileFilter
-
- documents = []
- filter = FileFilter(
- ignore_patterns=self._config.monitor.ignore_patterns or None,
- include_patterns=self._config.monitor.include_patterns or None,
- )
- for path in source_path.rglob("*"):
- if path.is_file() and filter.include_file(str(path)):
- doc = await self._create_document_from_file(
- path, title=None, metadata=metadata
- )
- documents.append(doc)
- return documents
-
- # Handle single file
- return await self._create_document_from_file(
- source_path, title=title, metadata=metadata
- )
-
- async def _create_document_from_file(
- self, source_path: Path, title: str | None = None, metadata: dict | None = None
- ) -> Document:
- """Create or update a document from a single file path.
-
- Args:
- source_path: Path to the file
- title: Optional title
- metadata: Optional metadata dictionary
-
- Returns:
- Document instance (created, updated, or existing)
-
- Raises:
- ValueError: If the file cannot be parsed or doesn't exist
- """
- from haiku.rag.embeddings import embed_chunks
-
- metadata = metadata or {}
-
- converter = get_converter(self._config)
- if source_path.suffix.lower() not in converter.supported_extensions:
- raise ValueError(f"Unsupported file extension: {source_path.suffix}")
-
- if not source_path.exists():
- raise ValueError(f"File does not exist: {source_path}")
-
- uri = source_path.absolute().as_uri()
- md5_hash = hashlib.md5(
- source_path.read_bytes(), usedforsecurity=False
- ).hexdigest()
-
- # Get content type from file extension (do before early return)
- content_type, _ = mimetypes.guess_type(str(source_path))
- if not content_type:
- content_type = "application/octet-stream"
- # Merge metadata with contentType and md5
- metadata.update({"contentType": content_type, "md5": md5_hash})
-
- # Check if document already exists
- existing_doc = await self.get_document_by_uri(uri)
- if existing_doc and existing_doc.metadata.get("md5") == md5_hash:
- # MD5 unchanged; update title/metadata if provided
- updated = False
- if title is not None and title != existing_doc.title:
- existing_doc.title = title
- updated = True
-
- # Check if metadata actually changed (beyond contentType and md5)
- merged_metadata = {**(existing_doc.metadata or {}), **metadata}
- if merged_metadata != existing_doc.metadata:
- existing_doc.metadata = merged_metadata
- updated = True
-
- if updated:
- return await self.document_repository.update(existing_doc)
- return existing_doc
-
- # Convert → Chunk → Embed using primitives
- docling_document = await self.convert(source_path)
- chunks = await self.chunk(docling_document)
- embedded_chunks = await embed_chunks(chunks, self._config)
-
- stored_content = docling_document.export_to_markdown()
-
- if existing_doc:
- # Update existing document and rechunk
- existing_doc.content = stored_content
- existing_doc.metadata = metadata
- existing_doc.set_docling(docling_document)
- if title is not None:
- existing_doc.title = title
- elif existing_doc.title is None:
- existing_doc.title = await self._resolve_title(
- docling_document, stored_content
- )
- return await self._update_document_with_chunks(
- existing_doc, embedded_chunks, docling_document
- )
- else:
- # Create new document
- if title is None:
- title = await self._resolve_title(docling_document, stored_content)
- document = Document(
- content=stored_content,
- uri=uri,
- title=title,
- metadata=metadata,
- )
- document.set_docling(docling_document)
- return await self._store_document_with_chunks(
- document, embedded_chunks, docling_document
- )
-
- async def _create_or_update_document_from_url(
- self, url: str, title: str | None = None, metadata: dict | None = None
- ) -> Document:
- """Create or update a document from a URL by downloading and parsing the content.
-
- Checks if a document with the same URI already exists:
- - If MD5 is unchanged, returns existing document
- - If MD5 changed, updates the document
- - If no document exists, creates a new one
-
- Args:
- url: URL to download and parse
- metadata: Optional metadata dictionary
-
- Returns:
- Document instance (created, updated, or existing)
-
- Raises:
- ValueError: If the content cannot be parsed
- httpx.RequestError: If URL request fails
- """
- from haiku.rag.embeddings import embed_chunks
-
- metadata = metadata or {}
-
- converter = get_converter(self._config)
- supported_extensions = converter.supported_extensions
-
- async with httpx.AsyncClient() as client:
- response = await client.get(url)
- response.raise_for_status()
-
- md5_hash = hashlib.md5(response.content).hexdigest()
-
- # Get content type early (used for potential no-op update)
- content_type = response.headers.get("content-type", "").lower()
-
- # Check if document already exists
- existing_doc = await self.get_document_by_uri(url)
- if existing_doc and existing_doc.metadata.get("md5") == md5_hash:
- # MD5 unchanged; update title/metadata if provided
- updated = False
- if title is not None and title != existing_doc.title:
- existing_doc.title = title
- updated = True
-
- metadata.update({"contentType": content_type, "md5": md5_hash})
- # Check if metadata actually changed (beyond contentType and md5)
- merged_metadata = {**(existing_doc.metadata or {}), **metadata}
- if merged_metadata != existing_doc.metadata:
- existing_doc.metadata = merged_metadata
- updated = True
-
- if updated:
- return await self.document_repository.update(existing_doc)
- return existing_doc
- file_extension = self._get_extension_from_content_type_or_url(
- url, content_type
- )
-
- if file_extension not in supported_extensions:
- raise ValueError(
- f"Unsupported content type/extension: {content_type}/{file_extension}"
- )
-
- # Create a temporary file with the appropriate extension
- with tempfile.NamedTemporaryFile(
- mode="wb", suffix=file_extension, delete=False
- ) as temp_file:
- temp_file.write(response.content)
- temp_file.flush()
- temp_path = Path(temp_file.name)
-
- try:
- # Convert → Chunk → Embed using primitives
- docling_document = await self.convert(temp_path)
- chunks = await self.chunk(docling_document)
- embedded_chunks = await embed_chunks(chunks, self._config)
- finally:
- temp_path.unlink(missing_ok=True)
-
- # Merge metadata with contentType and md5
- metadata.update({"contentType": content_type, "md5": md5_hash})
-
- stored_content = docling_document.export_to_markdown()
-
- if existing_doc:
- # Update existing document and rechunk
- existing_doc.content = stored_content
- existing_doc.metadata = metadata
- existing_doc.set_docling(docling_document)
- if title is not None:
- existing_doc.title = title
- elif existing_doc.title is None:
- existing_doc.title = await self._resolve_title(
- docling_document, stored_content
- )
- return await self._update_document_with_chunks(
- existing_doc, embedded_chunks, docling_document
- )
- else:
- # Create new document
- if title is None:
- title = await self._resolve_title(docling_document, stored_content)
- document = Document(
- content=stored_content,
- uri=url,
- title=title,
- metadata=metadata,
- )
- document.set_docling(docling_document)
- return await self._store_document_with_chunks(
- document, embedded_chunks, docling_document
- )
-
- def _get_extension_from_content_type_or_url(
- self, url: str, content_type: str
- ) -> str:
- """Determine file extension from content type or URL."""
- # Common content type mappings
- content_type_map = {
- "text/html": ".html",
- "text/plain": ".txt",
- "text/markdown": ".md",
- "application/pdf": ".pdf",
- "application/json": ".json",
- "text/csv": ".csv",
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
- "application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
- "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
- }
-
- # Try content type first
- for ct, ext in content_type_map.items():
- if ct in content_type:
- return ext
-
- # Try URL extension
- parsed_url = urlparse(url)
- path = Path(parsed_url.path)
- if path.suffix:
- return path.suffix.lower()
-
- # Default to .html for web content
- return ".html"
-
- async def get_document_by_id(self, document_id: str) -> Document | None:
- """Get a document by its ID.
-
- Args:
- document_id: The unique identifier of the document.
-
- Returns:
- The Document instance if found, None otherwise.
- """
- return await self.document_repository.get_by_id(document_id)
-
- async def get_chunk_by_id(self, chunk_id: str) -> Chunk | None:
- """Get a chunk by its ID.
-
- Args:
- chunk_id: The unique identifier of the chunk.
-
- Returns:
- The Chunk instance if found, None otherwise.
- """
- return await self.chunk_repository.get_by_id(chunk_id)
-
- async def get_document_by_uri(self, uri: str) -> Document | None:
- """Get a document by its URI.
-
- Args:
- uri: The URI identifier of the document.
-
- Returns:
- The Document instance if found, None otherwise.
- """
- return await self.document_repository.get_by_uri(uri)
-
- async def resolve_document(self, id_or_title: str) -> Document | None:
- """Resolve a document by ID, title, or URI (in that order).
-
- Args:
- id_or_title: Document ID, title, or URI to look up.
-
- Returns:
- The Document instance if found, None otherwise.
- """
- doc = await self.get_document_by_id(id_or_title)
- if doc:
- return doc
-
- safe_input = escape_sql_string(id_or_title)
- docs = await self.list_documents(filter=f"title = '{safe_input}'")
- if docs and docs[0].id:
- return await self.get_document_by_id(docs[0].id)
-
- docs = await self.list_documents(filter=f"uri = '{safe_input}'")
- if docs and docs[0].id:
- return await self.get_document_by_id(docs[0].id)
-
- return None
-
- async def update_document(
- self,
- document_id: str,
- content: str | None = None,
- metadata: dict | None = None,
- chunks: list[Chunk] | None = None,
- title: str | None = None,
- docling_document: "DoclingDocument | None" = None,
- ) -> Document:
- """Update a document by ID.
-
- Updates specified fields. When content or docling_document is provided,
- the document is rechunked and re-embedded. Updates to only metadata or title
- skip rechunking for efficiency.
-
- Args:
- document_id: The ID of the document to update.
- content: New content (mutually exclusive with docling_document).
- metadata: New metadata dict.
- chunks: Custom chunks (will be embedded if missing embeddings).
- title: New title.
- docling_document: DoclingDocument to replace content (mutually exclusive with content).
-
- Returns:
- The updated Document instance.
-
- Raises:
- ValueError: If document not found, or if both content and docling_document
- are provided.
- """
- from haiku.rag.embeddings import embed_chunks
-
- # Validate: content and docling_document are mutually exclusive
- if content is not None and docling_document is not None:
- raise ValueError(
- "content and docling_document are mutually exclusive. "
- "Provide one or the other, not both."
- )
-
- # Fetch the existing document
- existing_doc = await self.get_document_by_id(document_id)
- if existing_doc is None:
- raise ValueError(f"Document with ID {document_id} not found")
-
- # Update metadata/title fields
- if title is not None:
- existing_doc.title = title
- if metadata is not None:
- existing_doc.metadata = metadata
-
- # Only metadata/title update - no rechunking needed
- if content is None and chunks is None and docling_document is None:
- return await self.document_repository.update(existing_doc)
-
- # Custom chunks provided - use them as-is
- if chunks is not None:
- # Store docling data if provided
- if docling_document is not None:
- existing_doc.content = docling_document.export_to_markdown()
- existing_doc.set_docling(docling_document)
- elif content is not None:
- existing_doc.content = content
-
- return await self._update_document_with_chunks(
- existing_doc, chunks, docling_document
- )
-
- # DoclingDocument provided without chunks - chunk and embed using primitives
- if docling_document is not None:
- existing_doc.content = docling_document.export_to_markdown()
- existing_doc.set_docling(docling_document)
-
- new_chunks = await self.chunk(docling_document)
- embedded_chunks = await embed_chunks(new_chunks, self._config)
- return await self._update_document_with_chunks(
- existing_doc, embedded_chunks, docling_document
- )
-
- # Content provided without chunks - convert, chunk, and embed using primitives
- assert content is not None
- existing_doc.content = content
- converter = get_converter(self._config)
- converted_docling = await converter.convert_text(
- existing_doc.content, format="md"
- )
- existing_doc.set_docling(converted_docling)
-
- new_chunks = await self.chunk(converted_docling)
- embedded_chunks = await embed_chunks(new_chunks, self._config)
- return await self._update_document_with_chunks(
- existing_doc, embedded_chunks, converted_docling
- )
-
- async def delete_document(self, document_id: str) -> bool:
- """Delete a document by its ID."""
- return await self.document_repository.delete(document_id)
-
- async def list_documents(
- self,
- limit: int | None = None,
- offset: int | None = None,
- filter: str | None = None,
- include_content: bool = False,
- ) -> list[Document]:
- """List all documents with optional pagination and filtering.
-
- Args:
- limit: Maximum number of documents to return.
- offset: Number of documents to skip.
- filter: Optional SQL WHERE clause to filter documents.
- include_content: Whether to load content and docling_document.
- Defaults to False to avoid loading large blobs.
-
- Returns:
- List of Document instances matching the criteria.
- """
- return await self.document_repository.list_all(
- limit=limit, offset=offset, filter=filter, include_content=include_content
- )
-
- async def count_documents(self, filter: str | None = None) -> int:
- """Count documents with optional filtering.
-
- Args:
- filter: Optional SQL WHERE clause to filter documents.
-
- Returns:
- Number of documents matching the criteria.
- """
- return await self.document_repository.count(filter=filter)
-
- async def search(
- self,
- query: str,
- limit: int | None = None,
- search_type: str = "hybrid",
- filter: str | None = None,
- ) -> list[SearchResult]:
- """Search for relevant chunks using the specified search method with optional reranking.
-
- Args:
- query: The search query string.
- limit: Maximum number of results to return. Defaults to config.search.default_limit.
- search_type: Type of search - "vector", "fts", or "hybrid" (default).
- filter: Optional SQL WHERE clause to filter documents before searching chunks.
-
- Returns:
- List of SearchResult objects ordered by relevance.
- """
- if limit is None:
- limit = self._config.search.limit
-
- reranker = get_reranker(config=self._config)
-
- if reranker is None:
- chunk_results = await self.chunk_repository.search(
- query, limit, search_type, filter
- )
- else:
- search_limit = limit * 10
- raw_results = await self.chunk_repository.search(
- query, search_limit, search_type, filter
- )
- chunks = [chunk for chunk, _ in raw_results]
- chunk_results = await reranker.rerank(query, chunks, top_n=limit)
-
- return [SearchResult.from_chunk(chunk, score) for chunk, score in chunk_results]
-
- async def expand_context(
- self,
- search_results: list[SearchResult],
- ) -> list[SearchResult]:
- """Expand search results with surrounding content from the document.
-
- Uses the document_items table for section-bounded expansion.
- See haiku.rag.context for the algorithm description.
-
- Results without doc_item_refs pass through unexpanded. This happens
- when chunks were created without docling metadata (e.g., custom chunks
- passed to import_document).
-
- Args:
- search_results: List of SearchResult objects from search.
-
- Returns:
- List of SearchResult objects with expanded content.
- """
- from haiku.rag.context import expand_with_items
-
- max_chars = self._config.search.max_context_chars
-
- # Group by document_id for efficient processing
- document_groups: dict[str | None, list[SearchResult]] = {}
- for result in search_results:
- doc_id = result.document_id
- if doc_id not in document_groups:
- document_groups[doc_id] = []
- document_groups[doc_id].append(result)
-
- expanded_results = []
-
- for doc_id, doc_results in document_groups.items():
- if doc_id is None:
- expanded_results.extend(doc_results)
- continue
-
- has_refs = any(r.doc_item_refs for r in doc_results)
- if not has_refs:
- expanded_results.extend(doc_results)
- continue
-
- expanded = await expand_with_items(
- self.document_item_repository,
- doc_id,
- doc_results,
- max_chars,
- )
- expanded_results.extend(expanded)
-
- expanded_results.sort(key=lambda r: r.score, reverse=True)
- return expanded_results
-
- async def ask(
- self,
- question: str,
- system_prompt: str | None = None,
- filter: str | None = None,
- ) -> "tuple[str, list[Citation]]":
- """Ask a question using the configured QA agent.
-
- Args:
- question: The question to ask.
- system_prompt: Optional custom system prompt for the QA agent.
- filter: SQL WHERE clause to filter documents.
-
- Returns:
- Tuple of (answer text, list of resolved citations).
- """
- from haiku.rag.agents.qa import get_qa_agent
-
- qa_agent = get_qa_agent(self, config=self._config, system_prompt=system_prompt)
- return await qa_agent.answer(question, filter=filter)
-
- async def research(
- self,
- question: str,
- *,
- filter: str | None = None,
- max_iterations: int | None = None,
- ) -> "ResearchReport":
- """Run multi-agent research to investigate a question.
-
- Args:
- question: The research question to investigate.
- filter: SQL WHERE clause to filter documents.
- max_iterations: Override max iterations (None uses config default).
-
- Returns:
- ResearchReport with structured findings.
- """
- from haiku.rag.agents.research.dependencies import ResearchContext
- from haiku.rag.agents.research.graph import build_research_graph
- from haiku.rag.agents.research.state import ResearchDeps, ResearchState
-
- graph = build_research_graph(config=self._config)
- context = ResearchContext(original_question=question)
- state = ResearchState.from_config(
- context=context, config=self._config, max_iterations=max_iterations
- )
- state.search_filter = filter
- deps = ResearchDeps(client=self)
-
- return await graph.run(state=state, deps=deps)
-
- async def analyze(
- self,
- question: str,
- documents: list[str] | None = None,
- filter: str | None = None,
- ) -> "AnalysisResult":
- """Answer a question using the analysis agent with code execution.
-
- The analysis agent can write and execute Python code in a sandboxed
- environment to solve problems that require computation, aggregation,
- or complex traversal across documents.
-
- Args:
- question: The question to answer.
- documents: Optional list of document IDs or titles to pre-load.
- filter: SQL WHERE clause to filter documents during searches.
-
- Returns:
- AnalysisResult with the answer and the final consolidated program.
- """
- from haiku.rag.agents.analysis import (
- AnalysisContext,
- AnalysisDeps,
- Sandbox,
- create_analysis_agent,
- )
-
- context = AnalysisContext(filter=filter)
-
- if documents:
- loaded_docs = []
- for doc_ref in documents:
- doc = await self.resolve_document(doc_ref)
- if doc:
- loaded_docs.append(doc)
- context.documents = loaded_docs if loaded_docs else None
-
- sandbox = Sandbox(
- db_path=self.store.db_path,
- config=self._config,
- context=context,
- )
- deps = AnalysisDeps(
- sandbox=sandbox,
- context=context,
- )
-
- from haiku.rag.agents.analysis.models import AnalysisResult
- from haiku.rag.agents.research.models import Citation
-
- agent = create_analysis_agent(self._config)
- result = await agent.run(question, deps=deps)
-
- output = result.output
- seen: set[str] = set()
- citations: list[Citation] = []
- for sr in sandbox._search_results:
- if sr.chunk_id and sr.chunk_id not in seen:
- seen.add(sr.chunk_id)
- citations.append(
- Citation(
- index=len(seen),
- document_id=sr.document_id or "",
- chunk_id=sr.chunk_id,
- document_uri=sr.document_uri or "",
- document_title=sr.document_title,
- page_numbers=sr.page_numbers,
- headings=sr.headings,
- content=sr.content,
- )
- )
- return AnalysisResult(
- answer=output.answer,
- program=output.program,
- citations=citations,
- )
-
- async def visualize_chunk(self, chunk: Chunk) -> list:
- """Render page images with bounding box highlights for a chunk.
-
- Expands the chunk's context to find the full section, then resolves
- bounding boxes from all items in the expanded range. This ensures
- visualization covers all pages the expanded content spans.
-
- Args:
- chunk: The chunk to visualize.
-
- Returns:
- List of PIL Image objects, one per page with bounding boxes.
- Empty list if no bounding boxes or page images available.
- """
- from copy import deepcopy
-
- from PIL import ImageDraw
-
- from haiku.rag.store.models.chunk import ChunkMetadata
-
- # Get the document structure (from cache if available)
- if not chunk.document_id:
- return []
-
- doc = await self.document_repository.get_docling_data(chunk.document_id)
- if not doc:
- return []
-
- docling_doc = doc.get_docling_document()
- if not docling_doc:
- return []
-
- # Expand context to get all doc_item_refs in the section
- chunk_meta = chunk.get_chunk_metadata()
- if chunk_meta.doc_item_refs:
- search_result = SearchResult(
- content=chunk.content,
- score=1.0,
- chunk_id=chunk.id,
- document_id=chunk.document_id,
- doc_item_refs=chunk_meta.doc_item_refs,
- page_numbers=chunk_meta.page_numbers,
- )
- expanded = await self.expand_context([search_result])
- refs = expanded[0].doc_item_refs if expanded else chunk_meta.doc_item_refs
- meta = ChunkMetadata(doc_item_refs=refs)
- else:
- meta = chunk_meta
- bounding_boxes = meta.resolve_bounding_boxes(docling_doc)
- if not bounding_boxes:
- return []
-
- # Group bounding boxes by page
- boxes_by_page: dict[int, list] = {}
- for bbox in bounding_boxes:
- if bbox.page_no not in boxes_by_page:
- boxes_by_page[bbox.page_no] = []
- boxes_by_page[bbox.page_no].append(bbox)
-
- # Load only the needed page images
- pages_doc = await self.document_repository.get_pages_data(chunk.document_id)
- if not pages_doc:
- return []
- page_images = pages_doc.get_page_images(list(boxes_by_page.keys()))
-
- # Render each page with its bounding boxes
- images = []
- for page_no in sorted(boxes_by_page.keys()):
- if page_no not in page_images:
- continue
-
- page = page_images[page_no]
- if page.image is None or page.image.pil_image is None:
- continue
-
- pil_image = page.image.pil_image
- page_height = page.size.height
-
- # Calculate scale factor (image pixels vs document coordinates)
- scale_x = pil_image.width / page.size.width
- scale_y = pil_image.height / page.size.height
-
- # Draw bounding boxes
- image = deepcopy(pil_image)
- draw = ImageDraw.Draw(image, "RGBA")
-
- for bbox in boxes_by_page[page_no]:
- # Convert from document coordinates to image coordinates
- # Document coords are bottom-left origin, PIL uses top-left
- x0 = bbox.left * scale_x
- y0 = (page_height - bbox.top) * scale_y
- x1 = bbox.right * scale_x
- y1 = (page_height - bbox.bottom) * scale_y
-
- # Ensure proper ordering (y0 should be less than y1 for PIL)
- if y0 > y1:
- y0, y1 = y1, y0
-
- # Draw filled rectangle with transparency
- fill_color = (255, 255, 0, 40) # Yellow with transparency
- outline_color = (255, 165, 0, 100) # Orange outline
-
- draw.rectangle([(x0, y0), (x1, y1)], fill=fill_color, outline=None)
- draw.rectangle([(x0, y0), (x1, y1)], outline=outline_color, width=1)
-
- images.append(image)
-
- return images
-
- async def rebuild_database(
- self, mode: RebuildMode = RebuildMode.FULL
- ) -> AsyncGenerator[str, None]:
- """Rebuild the database with the specified mode.
-
- Args:
- mode: The rebuild mode to use:
- - FULL: Re-convert from source files, re-chunk, re-embed (default)
- - RECHUNK: Re-chunk from existing content, re-embed (no source access)
- - EMBED_ONLY: Keep existing chunks, only regenerate embeddings
- - TITLE_ONLY: Only generate titles for untitled documents
-
- Yields:
- The ID of the document currently being processed.
- """
- # Update settings to current config
- settings_repo = SettingsRepository(self.store)
- settings_repo.save_current_settings()
-
- documents = await self.list_documents(include_content=True)
-
- if mode == RebuildMode.TITLE_ONLY:
- async for doc_id in self._rebuild_title_only(documents):
- yield doc_id
- elif mode == RebuildMode.EMBED_ONLY:
- async for doc_id in self._rebuild_embed_only(documents):
- yield doc_id
- elif mode == RebuildMode.RECHUNK:
- await self.chunk_repository.delete_all()
- self.store.recreate_embeddings_table()
- async for doc_id in self._rebuild_rechunk(documents):
- yield doc_id
- else: # FULL
- await self.chunk_repository.delete_all()
- self.store.recreate_embeddings_table()
- async for doc_id in self._rebuild_full(documents):
- yield doc_id
-
- # Final maintenance if auto_vacuum enabled
- if self._config.storage.auto_vacuum:
- try:
- await self.store.vacuum()
- except Exception:
- pass
-
- async def _rebuild_title_only(
- self, documents: list[Document]
- ) -> AsyncGenerator[str, None]:
- """Generate titles for documents that don't have one."""
- for doc in documents:
- if doc.title is not None:
- continue
- assert doc.id is not None
- try:
- title = await self.generate_title(doc)
- except Exception:
- logger.warning(
- "Failed to generate title for document %s", doc.id, exc_info=True
- )
- continue
- if title is not None:
- doc.title = title
- await self.document_repository.update(doc)
- yield doc.id
-
- async def _rebuild_embed_only(
- self, documents: list[Document]
- ) -> AsyncGenerator[str, None]:
- """Re-embed all chunks without changing chunk boundaries."""
- from haiku.rag.embeddings import contextualize
-
- # Collect all chunks with new embeddings
- all_chunk_data: list[tuple[str, dict]] = []
-
- for doc in documents:
- assert doc.id is not None
- chunks = await self.chunk_repository.get_by_document_id(doc.id)
- if not chunks:
- continue
-
- texts = contextualize(chunks)
- embeddings = await self.chunk_repository.embedder.embed_documents(texts)
-
- for chunk, content_fts, embedding in zip(chunks, texts, embeddings):
- all_chunk_data.append(
- (
- doc.id,
- {
- "id": chunk.id,
- "document_id": chunk.document_id,
- "content": chunk.content,
- "content_fts": content_fts,
- "metadata": json.dumps(chunk.metadata),
- "order": chunk.order,
- "vector": embedding,
- },
- )
- )
-
- # Recreate chunks table (handles dimension changes)
- self.store.recreate_embeddings_table()
-
- # Insert all chunks
- if all_chunk_data:
- records = [self.store.ChunkRecord(**data) for _, data in all_chunk_data]
- self.store.chunks_table.add(records)
-
- # Yield all processed doc IDs
- yielded_docs: set[str] = set()
- for doc_id, _ in all_chunk_data:
- if doc_id not in yielded_docs:
- yielded_docs.add(doc_id)
- yield doc_id
-
- # Yield docs with no chunks
- for doc in documents:
- if doc.id and doc.id not in yielded_docs:
- yield doc.id
-
- async def _flush_rebuild_batch(
- self, documents: list[Document], chunks: list[Chunk]
- ) -> None:
- """Batch write documents and chunks during rebuild.
-
- This performs two writes: one for all document updates, one for all chunks.
- Also repopulates document items from the stored docling document.
- Used by RECHUNK and FULL modes after the chunks table has been cleared.
- """
- from haiku.rag.store.engine import DocumentRecord
-
- if not documents:
- return
-
- now = datetime.now().isoformat()
-
- # Batch update documents using merge_insert (single LanceDB version)
- doc_records = []
- for doc in documents:
- assert doc.id is not None
- doc_records.append(
- DocumentRecord(
- id=doc.id,
- content=doc.content,
- uri=doc.uri,
- title=doc.title,
- metadata=json.dumps(doc.metadata),
- docling_document=doc.docling_document,
- docling_pages=doc.docling_pages,
- docling_version=doc.docling_version,
- created_at=doc.created_at.isoformat() if doc.created_at else now,
- updated_at=now,
- )
- )
-
- self.store.documents_table.merge_insert("id").when_matched_update_all().execute(
- doc_records
- )
-
- # Batch create all chunks (single LanceDB version)
- if chunks:
- await self.chunk_repository.create(chunks)
-
- # Repopulate document items from stored docling data
- for doc in documents:
- assert doc.id is not None
- docling_doc = doc.get_docling_document()
- if docling_doc is not None:
- await self.document_item_repository.delete_by_document_id(doc.id)
- items = extract_items(doc.id, docling_doc)
- await self.document_item_repository.create_items(doc.id, items)
-
- async def _rebuild_rechunk(
- self, documents: list[Document]
- ) -> AsyncGenerator[str, None]:
- """Re-chunk and re-embed from existing document content."""
- from haiku.rag.embeddings import embed_chunks
-
- batch_size = 50
- pending_chunks: list[Chunk] = []
- pending_docs: list[Document] = []
- pending_doc_ids: list[str] = []
-
- converter = get_converter(self._config)
-
- for doc in documents:
- assert doc.id is not None
-
- # Convert stored markdown to DoclingDocument
- docling_document = await converter.convert_text(doc.content, format="md")
-
- # Chunk and embed
- chunks = await self.chunk(docling_document)
- embedded_chunks = await embed_chunks(chunks, self._config)
-
- # Update document fields
- doc.set_docling(docling_document)
-
- # Prepare chunks with document_id and order
- for order, chunk in enumerate(embedded_chunks):
- chunk.document_id = doc.id
- chunk.order = order
-
- pending_chunks.extend(embedded_chunks)
- pending_docs.append(doc)
- pending_doc_ids.append(doc.id)
-
- # Flush batch when size reached
- if len(pending_docs) >= batch_size:
- await self._flush_rebuild_batch(pending_docs, pending_chunks)
- for doc_id in pending_doc_ids:
- yield doc_id
- pending_chunks = []
- pending_docs = []
- pending_doc_ids = []
-
- # Flush remaining
- if pending_docs:
- await self._flush_rebuild_batch(pending_docs, pending_chunks)
- for doc_id in pending_doc_ids:
- yield doc_id
-
- async def _rebuild_full(
- self, documents: list[Document]
- ) -> AsyncGenerator[str, None]:
- """Full rebuild: re-convert from source, re-chunk, re-embed."""
- from haiku.rag.embeddings import embed_chunks
-
- batch_size = 50
- pending_chunks: list[Chunk] = []
- pending_docs: list[Document] = []
- pending_doc_ids: list[str] = []
- converter = get_converter(self._config)
-
- for doc in documents:
- assert doc.id is not None
-
- # Try to rebuild from source if available
- if doc.uri and self._check_source_accessible(doc.uri):
- try:
- # Flush pending batch before source rebuild (creates new doc)
- if pending_docs:
- await self._flush_rebuild_batch(pending_docs, pending_chunks)
- for doc_id in pending_doc_ids:
- yield doc_id
- pending_chunks = []
- pending_docs = []
- pending_doc_ids = []
-
- await self.delete_document(doc.id)
- new_doc = await self.create_document_from_source(
- source=doc.uri, metadata=doc.metadata or {}
- )
- assert isinstance(new_doc, Document)
- assert new_doc.id is not None
- yield new_doc.id
- continue
- except Exception as e:
- logger.error(
- "Error recreating document from source %s: %s",
- doc.uri,
- e,
- )
- continue
-
- # Fallback: rebuild from stored content
- if doc.uri:
- logger.warning(
- "Source missing for %s, re-embedding from content", doc.uri
- )
-
- docling_document = await converter.convert_text(doc.content, format="md")
- chunks = await self.chunk(docling_document)
- embedded_chunks = await embed_chunks(chunks, self._config)
-
- doc.set_docling(docling_document)
-
- # Prepare chunks with document_id and order
- for order, chunk in enumerate(embedded_chunks):
- chunk.document_id = doc.id
- chunk.order = order
-
- pending_chunks.extend(embedded_chunks)
- pending_docs.append(doc)
- pending_doc_ids.append(doc.id)
-
- # Flush batch when size reached
- if len(pending_docs) >= batch_size:
- await self._flush_rebuild_batch(pending_docs, pending_chunks)
- for doc_id in pending_doc_ids:
- yield doc_id
- pending_chunks = []
- pending_docs = []
- pending_doc_ids = []
-
- # Flush remaining
- if pending_docs:
- await self._flush_rebuild_batch(pending_docs, pending_chunks)
- for doc_id in pending_doc_ids:
- yield doc_id
-
- def _check_source_accessible(self, uri: str) -> bool:
- """Check if a document's source URI is accessible."""
- parsed_url = urlparse(uri)
- try:
- if parsed_url.scheme == "file":
- return Path(parsed_url.path).exists()
- elif parsed_url.scheme in ("http", "https"):
- return True
- return False
- except Exception:
- return False
-
- async def vacuum(self) -> None:
- """Optimize and clean up old versions across all tables."""
- await self.store.vacuum()
-
- async def download_models(self) -> AsyncGenerator[DownloadProgress, None]:
- """Download required models, yielding progress events.
-
- Yields DownloadProgress events for:
- - Docling models
- - HuggingFace tokenizer
- - Sentence-transformers embedder (if configured)
- - HuggingFace reranker models (mxbai, jina-local)
- - Ollama models
- """
- # Docling 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:
- pass
-
- # HuggingFace tokenizer
- from transformers import AutoTokenizer
-
- tokenizer_name = self._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 (
- self._config.embeddings.model.provider == "sentence-transformers"
- ): # pragma: no cover
- try:
- from sentence_transformers import ( # type: ignore[import-not-found] # ty: ignore[unresolved-import]
- SentenceTransformer,
- )
-
- model_name = self._config.embeddings.model.name
- yield DownloadProgress(model=model_name, status="start")
- await asyncio.to_thread(SentenceTransformer, model_name)
- yield DownloadProgress(model=model_name, status="done")
- except ImportError:
- pass
-
- # HuggingFace reranker models
- if self._config.reranking.model: # pragma: no cover
- provider = self._config.reranking.model.provider
- model_name = self._config.reranking.model.name
-
- if provider == "mxbai":
- try:
- from mxbai_rerank import MxbaiRerankV2
-
- yield DownloadProgress(model=model_name, status="start")
- await asyncio.to_thread(
- MxbaiRerankV2, model_name, disable_transformers_warnings=True
- )
- 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 self._config.embeddings.model.provider == "ollama":
- required_models.add(self._config.embeddings.model.name)
- if self._config.qa.model.provider == "ollama":
- required_models.add(self._config.qa.model.name)
- if self._config.research.model.provider == "ollama":
- required_models.add(self._config.research.model.name)
- if (
- self._config.reranking.model
- and self._config.reranking.model.provider == "ollama"
- ):
- required_models.add(self._config.reranking.model.name)
- pic_desc = self._config.processing.conversion_options.picture_description
- if pic_desc.enabled and pic_desc.model.provider == "ollama":
- required_models.add(pic_desc.model.name)
- if (
- self._config.processing.auto_title
- and self._config.processing.title_model.provider == "ollama"
- ):
- required_models.add(self._config.processing.title_model.name)
-
- if not required_models:
- return
-
- base_url = self._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'."
- )
-
- def close(self):
- """Close the underlying store connection."""
- self.store.close()
diff --git a/haiku_rag_slim/haiku/rag/client/__init__.py b/haiku_rag_slim/haiku/rag/client/__init__.py
new file mode 100644
index 00000000..2b9c4e9e
--- /dev/null
+++ b/haiku_rag_slim/haiku/rag/client/__init__.py
@@ -0,0 +1,390 @@
+import asyncio
+import hashlib
+import json
+import logging
+import mimetypes
+import tempfile
+from collections.abc import AsyncGenerator
+from datetime import datetime
+from enum import Enum
+from pathlib import Path
+from typing import TYPE_CHECKING, overload
+from urllib.parse import urlparse
+
+import httpx
+
+from haiku.rag.config import AppConfig, Config
+from haiku.rag.converters import get_converter
+from haiku.rag.reranking import get_reranker
+from haiku.rag.store.engine import Store
+from haiku.rag.store.models.chunk import Chunk, SearchResult
+from haiku.rag.store.models.document import Document
+from haiku.rag.store.models.document_item import extract_items
+from haiku.rag.store.repositories.chunk import ChunkRepository
+from haiku.rag.store.repositories.document import DocumentRepository
+from haiku.rag.store.repositories.document_item import DocumentItemRepository
+from haiku.rag.store.repositories.settings import SettingsRepository
+from haiku.rag.utils import escape_sql_string
+
+if TYPE_CHECKING:
+ from docling_core.types.doc.document import DoclingDocument
+
+ from haiku.rag.agents.analysis.models import AnalysisResult
+ from haiku.rag.agents.research.models import (
+ Citation,
+ ResearchReport,
+ )
+
+logger = logging.getLogger(__name__)
+
+
+class RebuildMode(Enum):
+ """Mode for rebuilding the database."""
+
+ FULL = "full" # Re-convert from source, re-chunk, re-embed
+ RECHUNK = "rechunk" # Re-chunk from existing content, re-embed
+ EMBED_ONLY = "embed_only" # Keep chunks, only regenerate embeddings
+ TITLE_ONLY = "title_only" # Only generate titles for untitled documents
+
+
+class HaikuRAG:
+ """High-level haiku-rag client."""
+
+ def __init__(
+ self,
+ db_path: Path | None = None,
+ config: AppConfig = Config,
+ skip_validation: bool = False,
+ create: bool = False,
+ read_only: bool = False,
+ before: datetime | None = None,
+ ):
+ """Initialize the RAG client with a database path.
+
+ Args:
+ db_path: Path to the database file. If None, uses config.storage.data_dir.
+ config: Configuration to use. Defaults to global Config.
+ skip_validation: Whether to skip configuration validation on database load.
+ create: Whether to create the database if it doesn't exist.
+ read_only: Whether to open the database in read-only mode.
+ before: Query the database as it existed at this datetime.
+ Implies read_only=True.
+ """
+ self._config = config
+ if db_path is None:
+ db_path = self._config.storage.data_dir / "haiku.rag.lancedb"
+
+ self._db_path = db_path
+ self._skip_validation = skip_validation
+ self._create = create
+ self._read_only = read_only
+ self._before = before
+ self._vacuum_tasks: set[asyncio.Task] = set()
+
+ @property
+ def is_read_only(self) -> bool:
+ """Whether the client is in read-only mode."""
+ return self.store.is_read_only
+
+ async def __aenter__(self):
+ """Async context manager entry — initializes store and repositories."""
+ self.store = Store(
+ self._db_path,
+ config=self._config,
+ skip_validation=self._skip_validation,
+ create=self._create,
+ read_only=self._read_only,
+ before=self._before,
+ )
+ # If _initialize fails mid-way (e.g. migration check raises after
+ # connect), close the store so we don't leak the LanceDB connection —
+ # __aexit__ won't run because the `async with` never entered.
+ try:
+ await self.store._initialize()
+ except BaseException:
+ self.store.close()
+ raise
+ self.document_repository = DocumentRepository(self.store)
+ self.chunk_repository = ChunkRepository(self.store)
+ self.document_item_repository = DocumentItemRepository(self.store)
+ return self
+
+ async def __aexit__(self, exc_type, exc_val, exc_tb): # noqa: ARG002
+ """Async context manager exit."""
+ await self._await_vacuum_tasks()
+ self.close()
+ return False
+
+ async def _await_vacuum_tasks(self) -> None:
+ """Wait for all in-flight background vacuum tasks to complete.
+
+ Each create_document / update_document can schedule its own vacuum task;
+ all must be awaited before tearing down the connection, not just the
+ most recently scheduled one.
+ """
+ if self._vacuum_tasks:
+ await asyncio.gather(*self._vacuum_tasks, return_exceptions=True)
+
+ def _schedule_vacuum(self) -> None:
+ """Schedule a background vacuum and track the task for later awaiting."""
+ task = asyncio.create_task(self.store.vacuum())
+ self._vacuum_tasks.add(task)
+ task.add_done_callback(self._vacuum_tasks.discard)
+
+ # =========================================================================
+ # Processing Primitives
+ # =========================================================================
+
+ @overload
+ async def convert(self, source: Path) -> "DoclingDocument": ...
+
+ @overload
+ async def convert(
+ self, source: str, *, format: str = "md"
+ ) -> "DoclingDocument": ...
+
+ async def convert(
+ self, source: Path | str, *, format: str = "md"
+ ) -> "DoclingDocument":
+ from haiku.rag.client.processing import convert
+
+ return await convert(self._config, source, format=format)
+
+ async def chunk(self, docling_document: "DoclingDocument") -> list[Chunk]:
+ from haiku.rag.client.processing import chunk
+
+ return await chunk(self._config, docling_document)
+
+ # =========================================================================
+ # Title Generation
+ # =========================================================================
+
+ async def generate_title(self, document: Document) -> str | None:
+ from haiku.rag.client.titles import generate_title
+
+ return await generate_title(self._config, document)
+
+ async def create_document(
+ self,
+ content: str,
+ uri: str | None = None,
+ title: str | None = None,
+ metadata: dict | None = None,
+ format: str = "md",
+ ) -> Document:
+ from haiku.rag.client.documents import create_document
+
+ return await create_document(self, content, uri, title, metadata, format)
+
+ async def import_document(
+ self,
+ docling_document: "DoclingDocument",
+ chunks: list[Chunk],
+ uri: str | None = None,
+ title: str | None = None,
+ metadata: dict | None = None,
+ ) -> Document:
+ from haiku.rag.client.documents import import_document
+
+ return await import_document(
+ self, docling_document, chunks, uri, title, metadata
+ )
+
+ async def create_document_from_source(
+ self,
+ source: str | Path,
+ title: str | None = None,
+ metadata: dict | None = None,
+ ) -> Document | list[Document]:
+ from haiku.rag.client.documents import create_document_from_source
+
+ return await create_document_from_source(self, source, title, metadata)
+
+ async def update_document(
+ self,
+ document_id: str,
+ content: str | None = None,
+ metadata: dict | None = None,
+ chunks: list[Chunk] | None = None,
+ title: str | None = None,
+ docling_document: "DoclingDocument | None" = None,
+ ) -> Document:
+ from haiku.rag.client.documents import update_document
+
+ return await update_document(
+ self,
+ document_id,
+ content,
+ metadata,
+ chunks,
+ title,
+ docling_document,
+ )
+
+ async def get_document_by_id(self, document_id: str) -> Document | None:
+ """Get a document by its ID.
+
+ Args:
+ document_id: The unique identifier of the document.
+
+ Returns:
+ The Document instance if found, None otherwise.
+ """
+ return await self.document_repository.get_by_id(document_id)
+
+ async def get_chunk_by_id(self, chunk_id: str) -> Chunk | None:
+ """Get a chunk by its ID.
+
+ Args:
+ chunk_id: The unique identifier of the chunk.
+
+ Returns:
+ The Chunk instance if found, None otherwise.
+ """
+ return await self.chunk_repository.get_by_id(chunk_id)
+
+ async def get_document_by_uri(self, uri: str) -> Document | None:
+ """Get a document by its URI.
+
+ Args:
+ uri: The URI identifier of the document.
+
+ Returns:
+ The Document instance if found, None otherwise.
+ """
+ return await self.document_repository.get_by_uri(uri)
+
+ async def resolve_document(self, id_or_title: str) -> Document | None:
+ """Resolve a document by ID, title, or URI (in that order).
+
+ Args:
+ id_or_title: Document ID, title, or URI to look up.
+
+ Returns:
+ The Document instance if found, None otherwise.
+ """
+ doc = await self.get_document_by_id(id_or_title)
+ if doc:
+ return doc
+
+ safe_input = escape_sql_string(id_or_title)
+ docs = await self.list_documents(filter=f"title = '{safe_input}'")
+ if docs and docs[0].id:
+ return await self.get_document_by_id(docs[0].id)
+
+ docs = await self.list_documents(filter=f"uri = '{safe_input}'")
+ if docs and docs[0].id:
+ return await self.get_document_by_id(docs[0].id)
+
+ return None
+
+ async def delete_document(self, document_id: str) -> bool:
+ """Delete a document by its ID."""
+ return await self.document_repository.delete(document_id)
+
+ async def list_documents(
+ self,
+ limit: int | None = None,
+ offset: int | None = None,
+ filter: str | None = None,
+ include_content: bool = False,
+ ) -> list[Document]:
+ """List all documents with optional pagination and filtering.
+
+ Args:
+ limit: Maximum number of documents to return.
+ offset: Number of documents to skip.
+ filter: Optional SQL WHERE clause to filter documents.
+ include_content: Whether to load content and docling_document.
+ Defaults to False to avoid loading large blobs.
+
+ Returns:
+ List of Document instances matching the criteria.
+ """
+ return await self.document_repository.list_all(
+ limit=limit, offset=offset, filter=filter, include_content=include_content
+ )
+
+ async def count_documents(self, filter: str | None = None) -> int:
+ """Count documents with optional filtering.
+
+ Args:
+ filter: Optional SQL WHERE clause to filter documents.
+
+ Returns:
+ Number of documents matching the criteria.
+ """
+ return await self.document_repository.count(filter=filter)
+
+ async def search(
+ self,
+ query: str,
+ limit: int | None = None,
+ search_type: str = "hybrid",
+ filter: str | None = None,
+ ) -> list[SearchResult]:
+ from haiku.rag.client.search import search
+
+ return await search(self, query, limit, search_type, filter)
+
+ async def expand_context(
+ self,
+ search_results: list[SearchResult],
+ ) -> list[SearchResult]:
+ from haiku.rag.client.search import expand_context
+
+ return await expand_context(self, search_results)
+
+ async def ask(
+ self,
+ question: str,
+ system_prompt: str | None = None,
+ filter: str | None = None,
+ ) -> "tuple[str, list[Citation]]":
+ from haiku.rag.client.agents import ask
+
+ return await ask(self, question, system_prompt, filter)
+
+ async def research(
+ self,
+ question: str,
+ *,
+ filter: str | None = None,
+ max_iterations: int | None = None,
+ ) -> "ResearchReport":
+ from haiku.rag.client.agents import research
+
+ return await research(
+ self, question, filter=filter, max_iterations=max_iterations
+ )
+
+ async def analyze(
+ self,
+ question: str,
+ documents: list[str] | None = None,
+ filter: str | None = None,
+ ) -> "AnalysisResult":
+ from haiku.rag.client.agents import analyze
+
+ return await analyze(self, question, documents, filter)
+
+ async def visualize_chunk(self, chunk: Chunk) -> list:
+ from haiku.rag.client.search import visualize_chunk
+
+ return await visualize_chunk(self, chunk)
+
+ async def rebuild_database(
+ self, mode: RebuildMode = RebuildMode.FULL
+ ) -> AsyncGenerator[str, None]:
+ from haiku.rag.client.rebuild import rebuild_database
+
+ async for doc_id in rebuild_database(self, mode):
+ yield doc_id
+
+ async def vacuum(self) -> None:
+ """Optimize and clean up old versions across all tables."""
+ await self.store.vacuum()
+
+ def close(self):
+ """Close the underlying store connection."""
+ self.store.close()
diff --git a/haiku_rag_slim/haiku/rag/client/agents.py b/haiku_rag_slim/haiku/rag/client/agents.py
new file mode 100644
index 00000000..87ebc145
--- /dev/null
+++ b/haiku_rag_slim/haiku/rag/client/agents.py
@@ -0,0 +1,140 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from haiku.rag.agents.analysis.models import AnalysisResult
+ from haiku.rag.agents.research.models import Citation, ResearchReport
+ from haiku.rag.client import HaikuRAG
+
+
+async def ask(
+ client: "HaikuRAG",
+ question: str,
+ system_prompt: str | None = None,
+ filter: str | None = None,
+) -> "tuple[str, list[Citation]]":
+ """Ask a question using the configured QA agent.
+
+ Args:
+ client: The HaikuRAG client.
+ question: The question to ask.
+ system_prompt: Optional custom system prompt for the QA agent.
+ filter: SQL WHERE clause to filter documents.
+
+ Returns:
+ Tuple of (answer text, list of resolved citations).
+ """
+ from haiku.rag.agents.qa import get_qa_agent
+
+ qa_agent = get_qa_agent(client, config=client._config, system_prompt=system_prompt)
+ return await qa_agent.answer(question, filter=filter)
+
+
+async def research(
+ client: "HaikuRAG",
+ question: str,
+ *,
+ filter: str | None = None,
+ max_iterations: int | None = None,
+) -> "ResearchReport":
+ """Run multi-agent research to investigate a question.
+
+ Args:
+ client: The HaikuRAG client.
+ question: The research question to investigate.
+ filter: SQL WHERE clause to filter documents.
+ max_iterations: Override max iterations (None uses config default).
+
+ Returns:
+ ResearchReport with structured findings.
+ """
+ from haiku.rag.agents.research.dependencies import ResearchContext
+ from haiku.rag.agents.research.graph import build_research_graph
+ from haiku.rag.agents.research.state import ResearchDeps, ResearchState
+
+ graph = build_research_graph(config=client._config)
+ context = ResearchContext(original_question=question)
+ state = ResearchState.from_config(
+ context=context, config=client._config, max_iterations=max_iterations
+ )
+ state.search_filter = filter
+ deps = ResearchDeps(client=client)
+
+ return await graph.run(state=state, deps=deps)
+
+
+async def analyze(
+ client: "HaikuRAG",
+ question: str,
+ documents: list[str] | None = None,
+ filter: str | None = None,
+) -> "AnalysisResult":
+ """Answer a question using the analysis agent with code execution.
+
+ The analysis agent can write and execute Python code in a sandboxed
+ environment to solve problems that require computation, aggregation, or
+ complex traversal across documents.
+
+ Args:
+ client: The HaikuRAG client.
+ question: The question to answer.
+ documents: Optional list of document IDs or titles to pre-load.
+ filter: SQL WHERE clause to filter documents during searches.
+
+ Returns:
+ AnalysisResult with the answer and the final consolidated program.
+ """
+ from haiku.rag.agents.analysis import (
+ AnalysisContext,
+ AnalysisDeps,
+ Sandbox,
+ create_analysis_agent,
+ )
+ from haiku.rag.agents.analysis.models import AnalysisResult
+ from haiku.rag.agents.research.models import Citation
+
+ context = AnalysisContext(filter=filter)
+
+ if documents:
+ loaded_docs = []
+ for doc_ref in documents:
+ doc = await client.resolve_document(doc_ref)
+ if doc:
+ loaded_docs.append(doc)
+ context.documents = loaded_docs if loaded_docs else None
+
+ sandbox = Sandbox(
+ db_path=client.store.db_path,
+ config=client._config,
+ context=context,
+ )
+ deps = AnalysisDeps(
+ sandbox=sandbox,
+ context=context,
+ )
+
+ agent = create_analysis_agent(client._config)
+ result = await agent.run(question, deps=deps)
+
+ output = result.output
+ seen: set[str] = set()
+ citations: list[Citation] = []
+ for sr in sandbox._search_results:
+ if sr.chunk_id and sr.chunk_id not in seen:
+ seen.add(sr.chunk_id)
+ citations.append(
+ Citation(
+ index=len(seen),
+ document_id=sr.document_id or "",
+ chunk_id=sr.chunk_id,
+ document_uri=sr.document_uri or "",
+ document_title=sr.document_title,
+ page_numbers=sr.page_numbers,
+ headings=sr.headings,
+ content=sr.content,
+ )
+ )
+ return AnalysisResult(
+ answer=output.answer,
+ program=output.program,
+ citations=citations,
+ )
diff --git a/haiku_rag_slim/haiku/rag/client/documents.py b/haiku_rag_slim/haiku/rag/client/documents.py
new file mode 100644
index 00000000..1cd0861e
--- /dev/null
+++ b/haiku_rag_slim/haiku/rag/client/documents.py
@@ -0,0 +1,497 @@
+import hashlib
+import mimetypes
+import tempfile
+from pathlib import Path
+from typing import TYPE_CHECKING
+from urllib.parse import urlparse
+
+import httpx
+
+from haiku.rag.client.processing import ensure_chunks_embedded
+from haiku.rag.client.titles import resolve_title
+from haiku.rag.converters import get_converter
+from haiku.rag.store.models.chunk import Chunk
+from haiku.rag.store.models.document import Document
+from haiku.rag.store.models.document_item import extract_items
+
+if TYPE_CHECKING:
+ from docling_core.types.doc.document import DoclingDocument
+
+ from haiku.rag.client import HaikuRAG
+
+
+async def _store_document_with_chunks(
+ client: "HaikuRAG",
+ document: Document,
+ chunks: list[Chunk],
+ docling_document: "DoclingDocument",
+) -> Document:
+ """Store a document with chunks, embedding any that lack embeddings.
+
+ Handles versioning/rollback on failure.
+ """
+ # Ensure all chunks have embeddings before storing
+ chunks = await ensure_chunks_embedded(client._config, chunks)
+
+ # Snapshot table versions for versioned rollback (if supported)
+ versions = await client.store.current_table_versions()
+
+ # Create the document
+ created_doc = await client.document_repository.create(document)
+
+ try:
+ assert created_doc.id is not None, (
+ "Document ID should not be None after creation"
+ )
+ # Set document_id and order for all chunks
+ for order, chunk in enumerate(chunks):
+ chunk.document_id = created_doc.id
+ chunk.order = order
+
+ # Batch create all chunks in a single operation
+ await client.chunk_repository.create(chunks)
+
+ # Extract and store document items for context expansion
+ items = extract_items(created_doc.id, docling_document)
+ await client.document_item_repository.create_items(created_doc.id, items)
+
+ # Vacuum old versions in background (non-blocking) if auto_vacuum enabled
+ if client._config.storage.auto_vacuum:
+ client._schedule_vacuum()
+
+ return created_doc
+ except Exception:
+ # Roll back to the captured versions and re-raise
+ await client.store.restore_table_versions(versions)
+ raise
+
+
+async def _update_document_with_chunks(
+ client: "HaikuRAG",
+ document: Document,
+ chunks: list[Chunk],
+ docling_document: "DoclingDocument | None" = None,
+) -> Document:
+ """Update a document and replace its chunks, embedding any that lack embeddings.
+
+ Handles versioning/rollback on failure. When `docling_document` is None,
+ existing items are preserved.
+ """
+ assert document.id is not None, "Document ID is required for update"
+
+ chunks = await ensure_chunks_embedded(client._config, chunks)
+
+ versions = await client.store.current_table_versions()
+
+ # Delete existing chunks before writing new ones
+ await client.chunk_repository.delete_by_document_id(document.id)
+
+ try:
+ updated_doc = await client.document_repository.update(document)
+
+ assert updated_doc.id is not None
+ for order, chunk in enumerate(chunks):
+ chunk.document_id = updated_doc.id
+ chunk.order = order
+
+ await client.chunk_repository.create(chunks)
+
+ # Replace document items when a new DoclingDocument is provided
+ if docling_document is not None:
+ await client.document_item_repository.delete_by_document_id(updated_doc.id)
+ items = extract_items(updated_doc.id, docling_document)
+ await client.document_item_repository.create_items(updated_doc.id, items)
+
+ if client._config.storage.auto_vacuum:
+ client._schedule_vacuum()
+
+ return updated_doc
+ except Exception:
+ await client.store.restore_table_versions(versions)
+ raise
+
+
+async def create_document(
+ client: "HaikuRAG",
+ content: str,
+ uri: str | None = None,
+ title: str | None = None,
+ metadata: dict | None = None,
+ format: str = "md",
+) -> Document:
+ """Create a new document from text content.
+
+ Converts the content, chunks it, and generates embeddings.
+ """
+ from haiku.rag.embeddings import embed_chunks
+
+ # Convert → Chunk → Embed using primitives
+ converter = get_converter(client._config)
+ docling_document = await converter.convert_text(content, format=format)
+ chunks = await client.chunk(docling_document)
+ embedded_chunks = await embed_chunks(chunks, client._config)
+
+ # Store markdown export as content for better display/readability.
+ # The original is preserved in docling_document.
+ stored_content = docling_document.export_to_markdown()
+
+ if title is None:
+ title = await resolve_title(client._config, docling_document, stored_content)
+
+ document = Document(
+ content=stored_content,
+ uri=uri,
+ title=title,
+ metadata=metadata or {},
+ )
+ document.set_docling(docling_document)
+
+ return await _store_document_with_chunks(
+ client, document, embedded_chunks, docling_document
+ )
+
+
+async def import_document(
+ client: "HaikuRAG",
+ docling_document: "DoclingDocument",
+ chunks: list[Chunk],
+ uri: str | None = None,
+ title: str | None = None,
+ metadata: dict | None = None,
+) -> Document:
+ """Import a pre-processed document with chunks.
+
+ Use this when conversion, chunking, and embedding were done externally.
+ Chunks without embeddings will be automatically embedded.
+ """
+ content = docling_document.export_to_markdown()
+ if title is None:
+ title = await resolve_title(client._config, docling_document, content)
+
+ document = Document(
+ content=content,
+ uri=uri,
+ title=title,
+ metadata=metadata or {},
+ )
+ document.set_docling(docling_document)
+
+ return await _store_document_with_chunks(client, document, chunks, docling_document)
+
+
+async def create_document_from_source(
+ client: "HaikuRAG",
+ source: str | Path,
+ title: str | None = None,
+ metadata: dict | None = None,
+) -> Document | list[Document]:
+ """Create or update document(s) from a file path, directory, or URL.
+
+ Checks if a document with the same URI already exists:
+ - If MD5 is unchanged, returns existing document
+ - If MD5 changed, updates the document
+ - If no document exists, creates a new one
+
+ Returns a single Document for files/URLs, a list for directories.
+ """
+ metadata = metadata or {}
+
+ source_str = str(source)
+ parsed_url = urlparse(source_str)
+ if parsed_url.scheme in ("http", "https"):
+ return await _create_or_update_document_from_url(
+ client, source_str, title=title, metadata=metadata
+ )
+ elif parsed_url.scheme == "file":
+ source_path = Path(parsed_url.path)
+ else:
+ source_path = Path(source) if isinstance(source, str) else source
+
+ if source_path.is_dir():
+ from haiku.rag.monitor import FileFilter
+
+ documents = []
+ filter = FileFilter(
+ ignore_patterns=client._config.monitor.ignore_patterns or None,
+ include_patterns=client._config.monitor.include_patterns or None,
+ )
+ for path in source_path.rglob("*"):
+ if path.is_file() and filter.include_file(str(path)):
+ doc = await _create_document_from_file(
+ client, path, title=None, metadata=metadata
+ )
+ documents.append(doc)
+ return documents
+
+ return await _create_document_from_file(
+ client, source_path, title=title, metadata=metadata
+ )
+
+
+async def _create_document_from_file(
+ client: "HaikuRAG",
+ source_path: Path,
+ title: str | None = None,
+ metadata: dict | None = None,
+) -> Document:
+ """Create or update a document from a single file path."""
+ from haiku.rag.embeddings import embed_chunks
+
+ metadata = metadata or {}
+
+ converter = get_converter(client._config)
+ if source_path.suffix.lower() not in converter.supported_extensions:
+ raise ValueError(f"Unsupported file extension: {source_path.suffix}")
+
+ if not source_path.exists():
+ raise ValueError(f"File does not exist: {source_path}")
+
+ uri = source_path.absolute().as_uri()
+ md5_hash = hashlib.md5(source_path.read_bytes(), usedforsecurity=False).hexdigest()
+
+ content_type, _ = mimetypes.guess_type(str(source_path))
+ if not content_type:
+ content_type = "application/octet-stream"
+ metadata.update({"contentType": content_type, "md5": md5_hash})
+
+ # Check if document already exists
+ existing_doc = await client.get_document_by_uri(uri)
+ if existing_doc and existing_doc.metadata.get("md5") == md5_hash:
+ # MD5 unchanged; update title/metadata if provided
+ updated = False
+ if title is not None and title != existing_doc.title:
+ existing_doc.title = title
+ updated = True
+
+ merged_metadata = {**(existing_doc.metadata or {}), **metadata}
+ if merged_metadata != existing_doc.metadata:
+ existing_doc.metadata = merged_metadata
+ updated = True
+
+ if updated:
+ return await client.document_repository.update(existing_doc)
+ return existing_doc
+
+ # Convert → Chunk → Embed
+ docling_document = await client.convert(source_path)
+ chunks = await client.chunk(docling_document)
+ embedded_chunks = await embed_chunks(chunks, client._config)
+
+ stored_content = docling_document.export_to_markdown()
+
+ if existing_doc:
+ # Update existing document and rechunk
+ existing_doc.content = stored_content
+ existing_doc.metadata = metadata
+ existing_doc.set_docling(docling_document)
+ if title is not None:
+ existing_doc.title = title
+ elif existing_doc.title is None:
+ existing_doc.title = await resolve_title(
+ client._config, docling_document, stored_content
+ )
+ return await _update_document_with_chunks(
+ client, existing_doc, embedded_chunks, docling_document
+ )
+ else:
+ if title is None:
+ title = await resolve_title(
+ client._config, docling_document, stored_content
+ )
+ document = Document(
+ content=stored_content,
+ uri=uri,
+ title=title,
+ metadata=metadata,
+ )
+ document.set_docling(docling_document)
+ return await _store_document_with_chunks(
+ client, document, embedded_chunks, docling_document
+ )
+
+
+async def _create_or_update_document_from_url(
+ client: "HaikuRAG",
+ url: str,
+ title: str | None = None,
+ metadata: dict | None = None,
+) -> Document:
+ """Create or update a document from a URL by downloading and parsing the content."""
+ from haiku.rag.client.processing import get_extension_from_content_type_or_url
+ from haiku.rag.embeddings import embed_chunks
+
+ metadata = metadata or {}
+
+ converter = get_converter(client._config)
+ supported_extensions = converter.supported_extensions
+
+ async with httpx.AsyncClient() as http:
+ response = await http.get(url)
+ response.raise_for_status()
+
+ md5_hash = hashlib.md5(response.content).hexdigest()
+
+ content_type = response.headers.get("content-type", "").lower()
+
+ # Check if document already exists
+ existing_doc = await client.get_document_by_uri(url)
+ if existing_doc and existing_doc.metadata.get("md5") == md5_hash:
+ updated = False
+ if title is not None and title != existing_doc.title:
+ existing_doc.title = title
+ updated = True
+
+ metadata.update({"contentType": content_type, "md5": md5_hash})
+ merged_metadata = {**(existing_doc.metadata or {}), **metadata}
+ if merged_metadata != existing_doc.metadata:
+ existing_doc.metadata = merged_metadata
+ updated = True
+
+ if updated:
+ return await client.document_repository.update(existing_doc)
+ return existing_doc
+
+ file_extension = get_extension_from_content_type_or_url(url, content_type)
+
+ if file_extension not in supported_extensions:
+ raise ValueError(
+ f"Unsupported content type/extension: {content_type}/{file_extension}"
+ )
+
+ with tempfile.NamedTemporaryFile(
+ mode="wb", suffix=file_extension, delete=False
+ ) as temp_file:
+ temp_file.write(response.content)
+ temp_file.flush()
+ temp_path = Path(temp_file.name)
+
+ try:
+ docling_document = await client.convert(temp_path)
+ chunks = await client.chunk(docling_document)
+ embedded_chunks = await embed_chunks(chunks, client._config)
+ finally:
+ temp_path.unlink(missing_ok=True)
+
+ metadata.update({"contentType": content_type, "md5": md5_hash})
+
+ stored_content = docling_document.export_to_markdown()
+
+ if existing_doc:
+ existing_doc.content = stored_content
+ existing_doc.metadata = metadata
+ existing_doc.set_docling(docling_document)
+ if title is not None:
+ existing_doc.title = title
+ elif existing_doc.title is None:
+ existing_doc.title = await resolve_title(
+ client._config, docling_document, stored_content
+ )
+ return await _update_document_with_chunks(
+ client, existing_doc, embedded_chunks, docling_document
+ )
+ else:
+ if title is None:
+ title = await resolve_title(
+ client._config, docling_document, stored_content
+ )
+ document = Document(
+ content=stored_content,
+ uri=url,
+ title=title,
+ metadata=metadata,
+ )
+ document.set_docling(docling_document)
+ return await _store_document_with_chunks(
+ client, document, embedded_chunks, docling_document
+ )
+
+
+async def update_document(
+ client: "HaikuRAG",
+ document_id: str,
+ content: str | None = None,
+ metadata: dict | None = None,
+ chunks: list[Chunk] | None = None,
+ title: str | None = None,
+ docling_document: "DoclingDocument | None" = None,
+) -> Document:
+ """Update a document by ID.
+
+ Updates specified fields. When content or docling_document is provided, the
+ document is rechunked and re-embedded. Updates to only metadata or title
+ skip rechunking for efficiency.
+
+ Raises:
+ ValueError: If document not found, or if both content and
+ docling_document are provided.
+ """
+ from haiku.rag.embeddings import embed_chunks
+
+ # Validate: content and docling_document are mutually exclusive
+ if content is not None and docling_document is not None:
+ raise ValueError(
+ "content and docling_document are mutually exclusive. "
+ "Provide one or the other, not both."
+ )
+
+ existing_doc = await client.get_document_by_id(document_id)
+ if existing_doc is None:
+ raise ValueError(f"Document with ID {document_id} not found")
+
+ if title is not None:
+ existing_doc.title = title
+ if metadata is not None:
+ existing_doc.metadata = metadata
+
+ # Only metadata/title update - no rechunking needed
+ if content is None and chunks is None and docling_document is None:
+ return await client.document_repository.update(existing_doc)
+
+ # Custom chunks provided - use them as-is
+ if chunks is not None:
+ if docling_document is not None:
+ existing_doc.content = docling_document.export_to_markdown()
+ existing_doc.set_docling(docling_document)
+ elif content is not None:
+ existing_doc.content = content
+
+ return await _update_document_with_chunks(
+ client, existing_doc, chunks, docling_document
+ )
+
+ # DoclingDocument provided without chunks - chunk and embed
+ if docling_document is not None:
+ existing_doc.content = docling_document.export_to_markdown()
+ existing_doc.set_docling(docling_document)
+
+ new_chunks = await client.chunk(docling_document)
+ embedded_chunks = await embed_chunks(new_chunks, client._config)
+ return await _update_document_with_chunks(
+ client, existing_doc, embedded_chunks, docling_document
+ )
+
+ # Content provided without chunks - convert, chunk, and embed
+ assert content is not None
+ existing_doc.content = content
+ converter = get_converter(client._config)
+ converted_docling = await converter.convert_text(existing_doc.content, format="md")
+ existing_doc.set_docling(converted_docling)
+
+ new_chunks = await client.chunk(converted_docling)
+ embedded_chunks = await embed_chunks(new_chunks, client._config)
+ return await _update_document_with_chunks(
+ client, existing_doc, embedded_chunks, converted_docling
+ )
+
+
+def check_source_accessible(uri: str) -> bool:
+ """Check if a document's source URI is accessible."""
+ parsed_url = urlparse(uri)
+ try:
+ if parsed_url.scheme == "file":
+ return Path(parsed_url.path).exists()
+ elif parsed_url.scheme in ("http", "https"):
+ return True
+ return False
+ except Exception:
+ return False
diff --git a/haiku_rag_slim/haiku/rag/client/downloads.py b/haiku_rag_slim/haiku/rag/client/downloads.py
new file mode 100644
index 00000000..9578cfbb
--- /dev/null
+++ b/haiku_rag_slim/haiku/rag/client/downloads.py
@@ -0,0 +1,155 @@
+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 (mxbai, jina-local)
+ - Ollama models
+ """
+ # Docling 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:
+ 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] # ty: ignore[unresolved-import]
+ SentenceTransformer,
+ )
+
+ model_name = config.embeddings.model.name
+ yield DownloadProgress(model=model_name, status="start")
+ await asyncio.to_thread(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 == "mxbai":
+ try:
+ from mxbai_rerank import MxbaiRerankV2
+
+ yield DownloadProgress(model=model_name, status="start")
+ await asyncio.to_thread(
+ MxbaiRerankV2, model_name, disable_transformers_warnings=True
+ )
+ 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.research.model.provider == "ollama":
+ required_models.add(config.research.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 pic_desc.enabled 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'."
+ )
diff --git a/haiku_rag_slim/haiku/rag/client/processing.py b/haiku_rag_slim/haiku/rag/client/processing.py
new file mode 100644
index 00000000..b3aa59c5
--- /dev/null
+++ b/haiku_rag_slim/haiku/rag/client/processing.py
@@ -0,0 +1,160 @@
+import tempfile
+from pathlib import Path
+from typing import TYPE_CHECKING
+from urllib.parse import urlparse
+
+import httpx
+
+from haiku.rag.config import AppConfig
+from haiku.rag.converters import get_converter
+from haiku.rag.store.models.chunk import Chunk
+
+if TYPE_CHECKING:
+ from docling_core.types.doc.document import DoclingDocument
+
+
+async def convert(
+ config: AppConfig, source: Path | str, *, format: str = "md"
+) -> "DoclingDocument":
+ """Convert a file, URL, or text to DoclingDocument.
+
+ Args:
+ config: Application configuration.
+ source: One of:
+ - Path: Local file path to convert
+ - str (URL): HTTP/HTTPS URL to download and convert
+ - str (text): Raw text content to convert
+ format: The format of text content ("md", "html", or "plain").
+ Defaults to "md". Use "plain" for plain text without parsing.
+ Only used when source is raw text (not a file path or URL).
+ Files and URLs determine format from extension/content-type.
+
+ Returns:
+ DoclingDocument from the converted source.
+
+ Raises:
+ ValueError: If the file doesn't exist or has unsupported extension.
+ httpx.RequestError: If URL download fails.
+ """
+ converter = get_converter(config)
+
+ # Path object - convert file directly
+ if isinstance(source, Path):
+ if not source.exists():
+ raise ValueError(f"File does not exist: {source}")
+ if source.suffix.lower() not in converter.supported_extensions:
+ raise ValueError(f"Unsupported file extension: {source.suffix}")
+ return await converter.convert_file(source)
+
+ # String - check if URL or text
+ parsed = urlparse(source)
+
+ if parsed.scheme in ("http", "https"):
+ # URL - download and convert
+ async with httpx.AsyncClient() as http:
+ response = await http.get(source)
+ response.raise_for_status()
+
+ content_type = response.headers.get("content-type", "").lower()
+ file_extension = get_extension_from_content_type_or_url(
+ source, content_type
+ )
+
+ if file_extension not in converter.supported_extensions:
+ raise ValueError(
+ f"Unsupported content type/extension: {content_type}/{file_extension}"
+ )
+
+ with tempfile.NamedTemporaryFile(
+ mode="wb", suffix=file_extension, delete=False
+ ) as temp_file:
+ temp_file.write(response.content)
+ temp_file.flush()
+ temp_path = Path(temp_file.name)
+
+ try:
+ return await converter.convert_file(temp_path)
+ finally:
+ temp_path.unlink(missing_ok=True)
+
+ elif parsed.scheme == "file":
+ # file:// URI
+ file_path = Path(parsed.path)
+ if not file_path.exists():
+ raise ValueError(f"File does not exist: {file_path}")
+ if file_path.suffix.lower() not in converter.supported_extensions:
+ raise ValueError(f"Unsupported file extension: {file_path.suffix}")
+ return await converter.convert_file(file_path)
+
+ else:
+ # Treat as text content
+ return await converter.convert_text(source, format=format)
+
+
+async def chunk(config: AppConfig, docling_document: "DoclingDocument") -> list[Chunk]:
+ """Chunk a DoclingDocument into Chunks.
+
+ Returns chunks without embeddings or document_id. Each chunk's `order`
+ field is set to its position in the list.
+ """
+ from haiku.rag.chunkers import get_chunker
+
+ chunker = get_chunker(config)
+ return await chunker.chunk(docling_document)
+
+
+async def ensure_chunks_embedded(config: AppConfig, chunks: list[Chunk]) -> list[Chunk]:
+ """Ensure all chunks have embeddings, embedding any that don't.
+
+ Chunks that already have embeddings are passed through unchanged; missing
+ embeddings are filled in in-place in the returned list (preserving order).
+ """
+ from haiku.rag.embeddings import embed_chunks
+
+ chunks_to_embed = [c for c in chunks if c.embedding is None]
+
+ if not chunks_to_embed:
+ return chunks
+
+ embedded = await embed_chunks(chunks_to_embed, config)
+
+ # Build result maintaining original order
+ embedded_map = {(c.content, c.order): c for c in embedded}
+ result = []
+ for ch in chunks:
+ if ch.embedding is not None:
+ result.append(ch)
+ else:
+ result.append(embedded_map[(ch.content, ch.order)])
+
+ return result
+
+
+def get_extension_from_content_type_or_url(url: str, content_type: str) -> str:
+ """Determine file extension from HTTP Content-Type header or URL suffix.
+
+ Returns the mapped extension for known content types, falling back to the
+ URL path suffix, and finally `.html` for generic web content.
+ """
+ content_type_map = {
+ "text/html": ".html",
+ "text/plain": ".txt",
+ "text/markdown": ".md",
+ "application/pdf": ".pdf",
+ "application/json": ".json",
+ "text/csv": ".csv",
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
+ }
+
+ for ct, ext in content_type_map.items():
+ if ct in content_type:
+ return ext
+
+ parsed_url = urlparse(url)
+ path = Path(parsed_url.path)
+ if path.suffix:
+ return path.suffix.lower()
+
+ return ".html"
diff --git a/haiku_rag_slim/haiku/rag/client/rebuild.py b/haiku_rag_slim/haiku/rag/client/rebuild.py
new file mode 100644
index 00000000..9c8280f5
--- /dev/null
+++ b/haiku_rag_slim/haiku/rag/client/rebuild.py
@@ -0,0 +1,332 @@
+import json
+import logging
+from collections.abc import AsyncGenerator
+from datetime import datetime
+from typing import TYPE_CHECKING
+
+from haiku.rag.client.documents import check_source_accessible
+from haiku.rag.converters import get_converter
+from haiku.rag.store.models.chunk import Chunk
+from haiku.rag.store.models.document import Document
+from haiku.rag.store.models.document_item import extract_items
+from haiku.rag.store.repositories.settings import SettingsRepository
+
+if TYPE_CHECKING:
+ from haiku.rag.client import HaikuRAG, RebuildMode
+
+logger = logging.getLogger(__name__)
+
+_REBUILD_BATCH_SIZE = 50
+
+
+async def rebuild_database(
+ client: "HaikuRAG", mode: "RebuildMode | None" = None
+) -> AsyncGenerator[str, None]:
+ """Rebuild the database with the specified mode.
+
+ Yields the ID of each document as it is processed.
+ """
+ from haiku.rag.client import RebuildMode
+
+ if mode is None:
+ mode = RebuildMode.FULL
+
+ # Wait for any already-scheduled background vacuum before the destructive
+ # table operations at the top of RECHUNK / FULL. Rebuild drops and
+ # recreates tables (and creates indices); a concurrent optimize on the
+ # same table fails with "CreateIndex transaction was preempted" from
+ # lance. Note: FULL calls create_document_from_source inside its loop,
+ # which may schedule *new* background vacuums — those run after the
+ # destructive phase and are fine.
+ await client._await_vacuum_tasks()
+
+ # Update settings to current config
+ settings_repo = SettingsRepository(client.store)
+ await settings_repo.save_current_settings()
+
+ documents = await client.list_documents(include_content=True)
+
+ if mode == RebuildMode.TITLE_ONLY:
+ async for doc_id in _rebuild_title_only(client, documents):
+ yield doc_id
+ elif mode == RebuildMode.EMBED_ONLY:
+ async for doc_id in _rebuild_embed_only(client, documents):
+ yield doc_id
+ elif mode == RebuildMode.RECHUNK:
+ await client.chunk_repository.delete_all()
+ await client.store.recreate_embeddings_table()
+ async for doc_id in _rebuild_rechunk(client, documents):
+ yield doc_id
+ else: # FULL
+ await client.chunk_repository.delete_all()
+ await client.store.recreate_embeddings_table()
+ async for doc_id in _rebuild_full(client, documents):
+ yield doc_id
+
+ # Final maintenance if auto_vacuum enabled. Swallowing only so that a
+ # failed post-rebuild optimize doesn't mask a successful rebuild — but
+ # log it so the failure is visible in the output.
+ if client._config.storage.auto_vacuum:
+ try:
+ await client.store.vacuum()
+ except Exception:
+ logger.warning("Post-rebuild vacuum failed", exc_info=True)
+
+
+async def _rebuild_title_only(
+ client: "HaikuRAG", documents: list[Document]
+) -> AsyncGenerator[str, None]:
+ """Generate titles for documents that don't have one."""
+ for doc in documents:
+ if doc.title is not None:
+ continue
+ assert doc.id is not None
+ try:
+ title = await client.generate_title(doc)
+ except Exception:
+ logger.warning(
+ "Failed to generate title for document %s", doc.id, exc_info=True
+ )
+ continue
+ if title is not None:
+ doc.title = title
+ await client.document_repository.update(doc)
+ yield doc.id
+
+
+async def _rebuild_embed_only(
+ client: "HaikuRAG", documents: list[Document]
+) -> AsyncGenerator[str, None]:
+ """Re-embed all chunks without changing chunk boundaries."""
+ from haiku.rag.embeddings import contextualize
+
+ # Collect all chunks with new embeddings
+ all_chunk_data: list[tuple[str, dict]] = []
+
+ for doc in documents:
+ assert doc.id is not None
+ chunks = await client.chunk_repository.get_by_document_id(doc.id)
+ if not chunks:
+ continue
+
+ texts = contextualize(chunks)
+ embeddings = await client.chunk_repository.embedder.embed_documents(texts)
+
+ for chunk, content_fts, embedding in zip(chunks, texts, embeddings):
+ all_chunk_data.append(
+ (
+ doc.id,
+ {
+ "id": chunk.id,
+ "document_id": chunk.document_id,
+ "content": chunk.content,
+ "content_fts": content_fts,
+ "metadata": json.dumps(chunk.metadata),
+ "order": chunk.order,
+ "vector": embedding,
+ },
+ )
+ )
+
+ # Recreate chunks table (handles dimension changes)
+ await client.store.recreate_embeddings_table()
+
+ # Insert all chunks
+ if all_chunk_data:
+ records = [client.store.ChunkRecord(**data) for _, data in all_chunk_data]
+ await client.store.chunks_table.add(records)
+
+ # Yield all processed doc IDs
+ yielded_docs: set[str] = set()
+ for doc_id, _ in all_chunk_data:
+ if doc_id not in yielded_docs:
+ yielded_docs.add(doc_id)
+ yield doc_id
+
+ # Yield docs with no chunks
+ for doc in documents:
+ if doc.id and doc.id not in yielded_docs:
+ yield doc.id
+
+
+async def _flush_rebuild_batch(
+ client: "HaikuRAG", documents: list[Document], chunks: list[Chunk]
+) -> None:
+ """Batch write documents and chunks during rebuild.
+
+ Performs two writes: one for all document updates (via merge_insert), one
+ for all chunks. Also repopulates document items from the stored docling
+ document. Used by RECHUNK and FULL modes after the chunks table has been
+ cleared.
+ """
+ from haiku.rag.store.engine import DocumentRecord
+
+ if not documents:
+ return
+
+ now = datetime.now().isoformat()
+
+ # Batch update documents using merge_insert (single LanceDB version)
+ doc_records = []
+ for doc in documents:
+ assert doc.id is not None
+ doc_records.append(
+ DocumentRecord(
+ id=doc.id,
+ content=doc.content,
+ uri=doc.uri,
+ title=doc.title,
+ metadata=json.dumps(doc.metadata),
+ docling_document=doc.docling_document,
+ docling_pages=doc.docling_pages,
+ docling_version=doc.docling_version,
+ created_at=doc.created_at.isoformat() if doc.created_at else now,
+ updated_at=now,
+ )
+ )
+
+ await (
+ client.store.documents_table.merge_insert("id")
+ .when_matched_update_all()
+ .execute(doc_records)
+ )
+
+ # Batch create all chunks (single LanceDB version)
+ if chunks:
+ await client.chunk_repository.create(chunks)
+
+ # Repopulate document items from stored docling data
+ for doc in documents:
+ assert doc.id is not None
+ docling_doc = doc.get_docling_document()
+ if docling_doc is not None:
+ await client.document_item_repository.delete_by_document_id(doc.id)
+ items = extract_items(doc.id, docling_doc)
+ await client.document_item_repository.create_items(doc.id, items)
+
+
+async def _rebuild_rechunk(
+ client: "HaikuRAG", documents: list[Document]
+) -> AsyncGenerator[str, None]:
+ """Re-chunk and re-embed from existing document content."""
+ from haiku.rag.embeddings import embed_chunks
+
+ pending_chunks: list[Chunk] = []
+ pending_docs: list[Document] = []
+ pending_doc_ids: list[str] = []
+
+ converter = get_converter(client._config)
+
+ for doc in documents:
+ assert doc.id is not None
+
+ # Convert stored markdown to DoclingDocument
+ docling_document = await converter.convert_text(doc.content, format="md")
+
+ # Chunk and embed
+ chunks = await client.chunk(docling_document)
+ embedded_chunks = await embed_chunks(chunks, client._config)
+
+ # Update document fields
+ doc.set_docling(docling_document)
+
+ # Prepare chunks with document_id and order
+ for order, chunk in enumerate(embedded_chunks):
+ chunk.document_id = doc.id
+ chunk.order = order
+
+ pending_chunks.extend(embedded_chunks)
+ pending_docs.append(doc)
+ pending_doc_ids.append(doc.id)
+
+ # Flush batch when size reached
+ if len(pending_docs) >= _REBUILD_BATCH_SIZE:
+ await _flush_rebuild_batch(client, pending_docs, pending_chunks)
+ for doc_id in pending_doc_ids:
+ yield doc_id
+ pending_chunks = []
+ pending_docs = []
+ pending_doc_ids = []
+
+ # Flush remaining
+ if pending_docs:
+ await _flush_rebuild_batch(client, pending_docs, pending_chunks)
+ for doc_id in pending_doc_ids:
+ yield doc_id
+
+
+async def _rebuild_full(
+ client: "HaikuRAG", documents: list[Document]
+) -> AsyncGenerator[str, None]:
+ """Full rebuild: re-convert from source, re-chunk, re-embed."""
+ from haiku.rag.embeddings import embed_chunks
+
+ pending_chunks: list[Chunk] = []
+ pending_docs: list[Document] = []
+ pending_doc_ids: list[str] = []
+ converter = get_converter(client._config)
+
+ for doc in documents:
+ assert doc.id is not None
+
+ # Try to rebuild from source if available
+ if doc.uri and check_source_accessible(doc.uri):
+ try:
+ # Flush pending batch before source rebuild (creates new doc)
+ if pending_docs:
+ await _flush_rebuild_batch(client, pending_docs, pending_chunks)
+ for doc_id in pending_doc_ids:
+ yield doc_id
+ pending_chunks = []
+ pending_docs = []
+ pending_doc_ids = []
+
+ await client.delete_document(doc.id)
+ new_doc = await client.create_document_from_source(
+ source=doc.uri, metadata=doc.metadata or {}
+ )
+ assert isinstance(new_doc, Document)
+ assert new_doc.id is not None
+ yield new_doc.id
+ continue
+ except Exception as e:
+ logger.error(
+ "Error recreating document from source %s: %s",
+ doc.uri,
+ e,
+ )
+ continue
+
+ # Fallback: rebuild from stored content
+ if doc.uri:
+ logger.warning("Source missing for %s, re-embedding from content", doc.uri)
+
+ docling_document = await converter.convert_text(doc.content, format="md")
+ chunks = await client.chunk(docling_document)
+ embedded_chunks = await embed_chunks(chunks, client._config)
+
+ doc.set_docling(docling_document)
+
+ # Prepare chunks with document_id and order
+ for order, chunk in enumerate(embedded_chunks):
+ chunk.document_id = doc.id
+ chunk.order = order
+
+ pending_chunks.extend(embedded_chunks)
+ pending_docs.append(doc)
+ pending_doc_ids.append(doc.id)
+
+ # Flush batch when size reached
+ if len(pending_docs) >= _REBUILD_BATCH_SIZE:
+ await _flush_rebuild_batch(client, pending_docs, pending_chunks)
+ for doc_id in pending_doc_ids:
+ yield doc_id
+ pending_chunks = []
+ pending_docs = []
+ pending_doc_ids = []
+
+ # Flush remaining
+ if pending_docs:
+ await _flush_rebuild_batch(client, pending_docs, pending_chunks)
+ for doc_id in pending_doc_ids:
+ yield doc_id
diff --git a/haiku_rag_slim/haiku/rag/client/search.py b/haiku_rag_slim/haiku/rag/client/search.py
new file mode 100644
index 00000000..10af0621
--- /dev/null
+++ b/haiku_rag_slim/haiku/rag/client/search.py
@@ -0,0 +1,196 @@
+from typing import TYPE_CHECKING
+
+from haiku.rag.reranking import get_reranker
+from haiku.rag.store.models.chunk import Chunk, SearchResult
+
+if TYPE_CHECKING:
+ from haiku.rag.client import HaikuRAG
+
+
+async def search(
+ client: "HaikuRAG",
+ query: str,
+ limit: int | None = None,
+ search_type: str = "hybrid",
+ filter: str | None = None,
+) -> list[SearchResult]:
+ """Search for relevant chunks with optional reranking.
+
+ Args:
+ client: The HaikuRAG client (provides config + chunk repository).
+ query: The search query string.
+ limit: Maximum number of results to return. Defaults to config.search.limit.
+ search_type: Type of search - "vector", "fts", or "hybrid" (default).
+ filter: Optional SQL WHERE clause to filter documents before searching chunks.
+
+ Returns:
+ List of SearchResult objects ordered by relevance.
+ """
+ if limit is None:
+ limit = client._config.search.limit
+
+ reranker = get_reranker(config=client._config)
+
+ if reranker is None:
+ chunk_results = await client.chunk_repository.search(
+ query, limit, search_type, filter
+ )
+ else:
+ search_limit = limit * 10
+ raw_results = await client.chunk_repository.search(
+ query, search_limit, search_type, filter
+ )
+ chunks = [chunk for chunk, _ in raw_results]
+ chunk_results = await reranker.rerank(query, chunks, top_n=limit)
+
+ return [SearchResult.from_chunk(chunk, score) for chunk, score in chunk_results]
+
+
+async def expand_context(
+ client: "HaikuRAG",
+ search_results: list[SearchResult],
+) -> list[SearchResult]:
+ """Expand search results with surrounding content from the document.
+
+ Uses the document_items table for section-bounded expansion.
+ See haiku.rag.context for the algorithm description.
+
+ Results without doc_item_refs pass through unexpanded. This happens when
+ chunks were created without docling metadata (e.g., custom chunks passed
+ to import_document).
+ """
+ from haiku.rag.context import expand_with_items
+
+ max_chars = client._config.search.max_context_chars
+
+ # Group by document_id for efficient processing
+ document_groups: dict[str | None, list[SearchResult]] = {}
+ for result in search_results:
+ doc_id = result.document_id
+ if doc_id not in document_groups:
+ document_groups[doc_id] = []
+ document_groups[doc_id].append(result)
+
+ expanded_results = []
+
+ for doc_id, doc_results in document_groups.items():
+ if doc_id is None:
+ expanded_results.extend(doc_results)
+ continue
+
+ has_refs = any(r.doc_item_refs for r in doc_results)
+ if not has_refs:
+ expanded_results.extend(doc_results)
+ continue
+
+ expanded = await expand_with_items(
+ client.document_item_repository,
+ doc_id,
+ doc_results,
+ max_chars,
+ )
+ expanded_results.extend(expanded)
+
+ expanded_results.sort(key=lambda r: r.score, reverse=True)
+ return expanded_results
+
+
+async def visualize_chunk(client: "HaikuRAG", chunk: Chunk) -> list:
+ """Render page images with bounding box highlights for a chunk.
+
+ Expands the chunk's context to find the full section, then resolves
+ bounding boxes from all items in the expanded range. This ensures
+ visualization covers all pages the expanded content spans.
+
+ Returns a list of PIL Image objects, one per page with bounding boxes.
+ Empty list if no bounding boxes or page images available.
+ """
+ from copy import deepcopy
+
+ from PIL import ImageDraw
+
+ from haiku.rag.store.models.chunk import ChunkMetadata
+
+ if not chunk.document_id:
+ return []
+
+ doc = await client.document_repository.get_docling_data(chunk.document_id)
+ if not doc:
+ return []
+
+ docling_doc = doc.get_docling_document()
+ if not docling_doc:
+ return []
+
+ # Expand context to get all doc_item_refs in the section
+ chunk_meta = chunk.get_chunk_metadata()
+ if chunk_meta.doc_item_refs:
+ search_result = SearchResult(
+ content=chunk.content,
+ score=1.0,
+ chunk_id=chunk.id,
+ document_id=chunk.document_id,
+ doc_item_refs=chunk_meta.doc_item_refs,
+ page_numbers=chunk_meta.page_numbers,
+ )
+ expanded = await expand_context(client, [search_result])
+ refs = expanded[0].doc_item_refs if expanded else chunk_meta.doc_item_refs
+ meta = ChunkMetadata(doc_item_refs=refs)
+ else:
+ meta = chunk_meta
+ bounding_boxes = meta.resolve_bounding_boxes(docling_doc)
+ if not bounding_boxes:
+ return []
+
+ # Group bounding boxes by page
+ boxes_by_page: dict[int, list] = {}
+ for bbox in bounding_boxes:
+ if bbox.page_no not in boxes_by_page:
+ boxes_by_page[bbox.page_no] = []
+ boxes_by_page[bbox.page_no].append(bbox)
+
+ # Load only the needed page images
+ pages_doc = await client.document_repository.get_pages_data(chunk.document_id)
+ if not pages_doc:
+ return []
+ page_images = pages_doc.get_page_images(list(boxes_by_page.keys()))
+
+ # Render each page with its bounding boxes
+ images = []
+ for page_no in sorted(boxes_by_page.keys()):
+ if page_no not in page_images:
+ continue
+
+ page = page_images[page_no]
+ if page.image is None or page.image.pil_image is None:
+ continue
+
+ pil_image = page.image.pil_image
+ page_height = page.size.height
+
+ # Scale factor: image pixels vs document coordinates
+ scale_x = pil_image.width / page.size.width
+ scale_y = pil_image.height / page.size.height
+
+ image = deepcopy(pil_image)
+ draw = ImageDraw.Draw(image, "RGBA")
+
+ for bbox in boxes_by_page[page_no]:
+ # Document coords are bottom-left origin; PIL uses top-left
+ x0 = bbox.left * scale_x
+ y0 = (page_height - bbox.top) * scale_y
+ x1 = bbox.right * scale_x
+ y1 = (page_height - bbox.bottom) * scale_y
+
+ if y0 > y1:
+ y0, y1 = y1, y0
+
+ fill_color = (255, 255, 0, 40) # Yellow with transparency
+ outline_color = (255, 165, 0, 100) # Orange outline
+
+ draw.rectangle([(x0, y0), (x1, y1)], fill=fill_color, outline=None)
+ draw.rectangle([(x0, y0), (x1, y1)], outline=outline_color, width=1)
+
+ images.append(image)
+
+ return images
diff --git a/haiku_rag_slim/haiku/rag/client/titles.py b/haiku_rag_slim/haiku/rag/client/titles.py
new file mode 100644
index 00000000..6310aa8f
--- /dev/null
+++ b/haiku_rag_slim/haiku/rag/client/titles.py
@@ -0,0 +1,105 @@
+import logging
+from typing import TYPE_CHECKING
+
+from haiku.rag.config import AppConfig
+from haiku.rag.store.models.document import Document
+
+if TYPE_CHECKING:
+ from docling_core.types.doc.document import DoclingDocument
+
+logger = logging.getLogger(__name__)
+
+
+def extract_structural_title(docling_document: "DoclingDocument") -> str | None:
+ """Extract a title from DoclingDocument structural metadata.
+
+ Priority: FURNITURE TITLE > BODY TITLE > first SECTION_HEADER.
+ """
+ from docling_core.types.doc.document import ContentLayer
+ from docling_core.types.doc.labels import DocItemLabel
+
+ furniture_title = None
+ body_title = None
+ first_section_header = None
+
+ for item in docling_document.texts:
+ if item.label == DocItemLabel.TITLE:
+ text = item.text.strip()
+ if not text:
+ continue
+ if item.content_layer == ContentLayer.FURNITURE:
+ furniture_title = text
+ elif body_title is None:
+ body_title = text
+ elif item.label == DocItemLabel.SECTION_HEADER and first_section_header is None:
+ text = item.text.strip()
+ if text:
+ first_section_header = text
+
+ return furniture_title or body_title or first_section_header
+
+
+async def generate_title_with_llm(config: AppConfig, content: str) -> str | None:
+ """Generate a title using LLM from document content."""
+ from pydantic_ai import Agent
+
+ from haiku.rag.utils import get_model
+
+ truncated = content[:2000]
+
+ model = get_model(config.processing.title_model, config)
+ agent: Agent[None, str] = Agent(
+ model=model,
+ output_type=str,
+ instructions=(
+ "Generate a concise, descriptive title for the following document. "
+ "The title should be at most 10 words. "
+ "Return ONLY the title text, nothing else."
+ ),
+ )
+ result = await agent.run(truncated)
+ title = result.output.strip()
+ return title if title else None
+
+
+async def resolve_title(
+ config: AppConfig,
+ docling_document: "DoclingDocument",
+ content: str,
+) -> str | None:
+ """Auto-generate a title from document structure or LLM.
+
+ Returns None if auto_title is disabled or generation fails.
+ """
+ if not config.processing.auto_title:
+ return None
+
+ structural = extract_structural_title(docling_document)
+ if structural:
+ return structural
+
+ try:
+ return await generate_title_with_llm(config, content)
+ except Exception:
+ logger.warning("LLM title generation failed during ingestion", exc_info=True)
+ return None
+
+
+async def generate_title(config: AppConfig, document: Document) -> str | None:
+ """Generate a title for a document.
+
+ Attempts structural extraction from the stored DoclingDocument, then falls
+ back to LLM generation. Bypasses the auto_title config since this is an
+ explicit call.
+
+ Does NOT update the document — caller decides.
+ """
+ docling_doc = document.get_docling_document()
+ content = document.content or ""
+
+ if docling_doc is not None:
+ structural = extract_structural_title(docling_doc)
+ if structural:
+ return structural
+
+ return await generate_title_with_llm(config, content)
diff --git a/haiku_rag_slim/haiku/rag/inspector/widgets/info_modal.py b/haiku_rag_slim/haiku/rag/inspector/widgets/info_modal.py
index d9fd8a31..0da65169 100644
--- a/haiku_rag_slim/haiku/rag/inspector/widgets/info_modal.py
+++ b/haiku_rag_slim/haiku/rag/inspector/widgets/info_modal.py
@@ -84,8 +84,8 @@ class InfoModal(ModalScreen):
# Connect to get table info
config = self.client.store._config
try:
- db = connect_lancedb(config, self.db_path)
- stats = get_database_stats(db)
+ db = await connect_lancedb(config, self.db_path)
+ stats = await get_database_stats(db)
except Exception as e:
lines.append(f"[red]Failed to open database: {e}[/red]")
self._content_widget.update("\n".join(lines))
@@ -101,8 +101,10 @@ class InfoModal(ModalScreen):
vector_dim: int | None = None
if stats["settings"]["exists"]:
- settings_tbl = db.open_table("settings")
- arrow = settings_tbl.search().where("id = 'settings'").limit(1).to_arrow()
+ settings_tbl = await db.open_table("settings")
+ arrow = await (
+ settings_tbl.query().where("id = 'settings'").limit(1).to_arrow()
+ )
rows = arrow.to_pylist() if arrow is not None else []
if rows:
raw = rows[0].get("settings") or "{}"
diff --git a/haiku_rag_slim/haiku/rag/store/engine.py b/haiku_rag_slim/haiku/rag/store/engine.py
index 292e6176..55a328f7 100644
--- a/haiku_rag_slim/haiku/rag/store/engine.py
+++ b/haiku_rag_slim/haiku/rag/store/engine.py
@@ -5,11 +5,12 @@ from datetime import datetime, timedelta
from enum import Enum
from importlib import metadata
from pathlib import Path
-from typing import Any
+from typing import TYPE_CHECKING, Any, cast
from uuid import uuid4
import lancedb
import pyarrow as pa
+from lancedb.index import FTS, BTree, IvfPq
from lancedb.pydantic import LanceModel, Vector
from pydantic import Field
@@ -17,9 +18,25 @@ from haiku.rag.config import AppConfig, Config
from haiku.rag.embeddings import get_embedder
from haiku.rag.store.exceptions import MigrationRequiredError, ReadOnlyError
+if TYPE_CHECKING:
+ from lancedb.query import AsyncQueryBase
+
logger = logging.getLogger(__name__)
+async def query_to_pydantic[T: LanceModel](
+ query: "AsyncQueryBase", model: type[T]
+) -> list[T]:
+ """Typed wrapper around AsyncQueryBase.to_pydantic.
+
+ The upstream stub annotates `.to_pydantic()` as returning `list[LanceModel]`
+ regardless of the concrete model passed in. This helper narrows the return
+ type to the concrete model so attribute access on the results type-checks
+ at call sites without needing per-line cast / ignore comments.
+ """
+ return cast("list[T]", await query.to_pydantic(model))
+
+
class ConnectionMode(Enum):
LOCAL = "local"
CLOUD = "cloud"
@@ -35,10 +52,12 @@ class ConnectionMode(Enum):
return ConnectionMode.OBJECT_STORAGE
-def connect_lancedb(config: AppConfig, db_path: Path | None = None):
+async def connect_lancedb(
+ config: AppConfig, db_path: Path | None = None
+) -> lancedb.AsyncConnection:
mode = ConnectionMode.from_config(config)
if mode == ConnectionMode.CLOUD:
- return lancedb.connect(
+ return await lancedb.connect_async(
uri=config.lancedb.uri,
api_key=config.lancedb.api_key,
region=config.lancedb.region,
@@ -47,11 +66,11 @@ def connect_lancedb(config: AppConfig, db_path: Path | None = None):
kwargs: dict[str, Any] = {"uri": config.lancedb.uri}
if config.lancedb.storage_options:
kwargs["storage_options"] = config.lancedb.storage_options
- return lancedb.connect(**kwargs)
+ return await lancedb.connect_async(**kwargs)
else:
if db_path is None:
raise ValueError("No lancedb.uri configured and no db_path provided")
- return lancedb.connect(db_path)
+ return await lancedb.connect_async(db_path)
class DocumentRecord(LanceModel):
@@ -89,19 +108,26 @@ def get_documents_arrow_schema() -> pa.Schema:
return pa.schema(fields)
-def create_chunk_model(vector_dim: int):
- """Create a ChunkRecord model with the specified vector dimension.
-
- This creates a model with proper vector typing for LanceDB.
+class ChunkRecordBase(LanceModel):
+ """Static base for ChunkRecord — declares the fields so attribute access
+ and constructor calls type-check. The concrete `vector` field is overridden
+ by create_chunk_model() with a Vector(dim) whose fixed-size-list dimension
+ is only known at runtime.
"""
- class ChunkRecord(LanceModel):
- id: str = Field(default_factory=lambda: str(uuid4()))
- document_id: str
- content: str
- content_fts: str = Field(default="")
- metadata: str = Field(default="{}")
- order: int = Field(default=0)
+ id: str = Field(default_factory=lambda: str(uuid4()))
+ document_id: str
+ content: str
+ content_fts: str = Field(default="")
+ metadata: str = Field(default="{}")
+ order: int = Field(default=0)
+ vector: list[float] = Field(default_factory=list)
+
+
+def create_chunk_model(vector_dim: int) -> type[ChunkRecordBase]:
+ """Create a ChunkRecord model with the specified vector dimension."""
+
+ class ChunkRecord(ChunkRecordBase):
vector: Vector(vector_dim) = Field(default_factory=lambda: [0.0] * vector_dim) # type: ignore
return ChunkRecord
@@ -124,7 +150,7 @@ class SettingsRecord(LanceModel):
REQUIRED_TABLES: tuple[str, ...] = ("documents", "chunks", "document_items", "settings")
-def get_database_stats(db: lancedb.DBConnection) -> dict:
+async def get_database_stats(db: lancedb.AsyncConnection) -> dict:
"""Collect stats for every haiku.rag table on the connection.
Missing tables return ``{"exists": False}``. Present tables include
@@ -132,7 +158,7 @@ def get_database_stats(db: lancedb.DBConnection) -> dict:
entry additionally reports vector index status and, when an index
exists, ``num_indexed_rows`` and ``num_unindexed_rows``.
"""
- existing = set(db.list_tables().tables)
+ existing = set((await db.list_tables()).tables)
stats: dict = {}
tables: dict = {}
@@ -140,24 +166,24 @@ def get_database_stats(db: lancedb.DBConnection) -> dict:
if name not in existing:
stats[name] = {"exists": False}
continue
- tbl = db.open_table(name)
+ tbl = await db.open_table(name)
tables[name] = tbl
# lancedb's .stats() stub claims TableStatistics but returns a plain dict at runtime.
- tbl_stats: dict = tbl.stats() # ty: ignore[invalid-assignment]
+ tbl_stats: dict = await tbl.stats() # type: ignore[assignment] # ty: ignore[invalid-assignment]
stats[name] = {
"exists": True,
"num_rows": tbl_stats.get("num_rows", 0),
"total_bytes": tbl_stats.get("total_bytes", 0),
- "num_versions": len(list(tbl.list_versions())),
+ "num_versions": len(await tbl.list_versions()),
}
if stats["chunks"]["exists"]:
chunks_tbl = tables["chunks"]
- indices = chunks_tbl.list_indices()
+ indices = await chunks_tbl.list_indices()
has_vector_index = any("vector" in str(idx).lower() for idx in indices)
stats["chunks"]["has_vector_index"] = has_vector_index
if has_vector_index:
- index_stats = chunks_tbl.index_stats("vector_idx")
+ index_stats = await chunks_tbl.index_stats("vector_idx")
if index_stats is not None:
stats["chunks"]["num_indexed_rows"] = index_stats.num_indexed_rows
stats["chunks"]["num_unindexed_rows"] = index_stats.num_unindexed_rows
@@ -181,10 +207,13 @@ class Store:
self._before = before
# Time-travel mode is always read-only
self._read_only = read_only or (before is not None)
+ self._create = create
+ self._skip_validation = skip_validation
+ self._skip_migration_check = skip_migration_check
self._vacuum_lock = asyncio.Lock()
+ self._is_new_db = False
# Check if database exists (for local filesystem only)
- is_new_db = False
if self._connection_mode == ConnectionMode.LOCAL:
if not db_path.exists():
if not create:
@@ -192,17 +221,27 @@ class Store:
f"Database does not exist at {self.db_path.absolute()}. "
"Use 'haiku-rag init' to create a new database."
)
- is_new_db = True
+ self._is_new_db = True
# Ensure parent directories exist for new databases
if not db_path.parent.exists():
Path.mkdir(db_path.parent, parents=True)
- # Connect to LanceDB
- self.db = connect_lancedb(self._config, db_path)
+ # Create embedder (sync — no LanceDB needed)
+ self.embedder = get_embedder(config=self._config)
- # For remote stores, detect new DB by checking if tables exist
- if not is_new_db and self._connection_mode != ConnectionMode.LOCAL:
- existing_tables = self.db.list_tables().tables
+ async def _initialize(self):
+ """Perform async initialization: connect to LanceDB, init tables, validate."""
+ # Connect to LanceDB
+ self.db: lancedb.AsyncConnection = await connect_lancedb(
+ self._config, self.db_path
+ )
+
+ # For remote stores (and as a safety net for local paths that exist but
+ # have no tables — e.g. a previously failed init), detect new DB by
+ # checking whether any tables exist.
+ is_new_db = self._is_new_db
+ if not is_new_db:
+ existing_tables = (await self.db.list_tables()).tables
if not existing_tables:
is_new_db = True
@@ -210,57 +249,68 @@ class Store:
# that can read existing chunks. For new databases, use config's dimension.
stored_vector_dim = None
if not is_new_db:
- stored_vector_dim = self._get_stored_vector_dim()
-
- # Create embedder with config's dimension (for generating new embeddings)
- self.embedder = get_embedder(config=self._config)
+ stored_vector_dim = await self._get_stored_vector_dim()
# Create ChunkRecord with stored dimension (for reading) or config dimension (for new DB)
chunk_vector_dim = stored_vector_dim or self.embedder._vector_dim
- self.ChunkRecord = create_chunk_model(chunk_vector_dim)
+ self.ChunkRecord: type[ChunkRecordBase] = create_chunk_model(chunk_vector_dim)
# Initialize tables (creates them if they don't exist)
- self._init_tables()
+ await self._init_tables()
# Checkout tables to historical state if before is specified
- if before is not None:
- self._checkout_tables_before(before)
+ if self._before is not None:
+ await self._checkout_tables_before(self._before)
# Set version for new databases, check migrations for existing ones
if is_new_db:
if not self._read_only:
- self._set_initial_version()
- elif not skip_migration_check:
- self._check_migrations()
+ await self._set_initial_version()
+ elif not self._skip_migration_check:
+ await self._check_migrations()
# Validate config compatibility after connection is established
- if not skip_validation:
- self._validate_configuration()
+ if not self._skip_validation:
+ await self._validate_configuration()
+
+ async def __aenter__(self):
+ # If _initialize connects to LanceDB but then fails (e.g. migration
+ # check, config validation), close the connection so it doesn't
+ # leak — __aexit__ won't run because the `async with` never entered.
+ try:
+ await self._initialize()
+ except BaseException:
+ self.close()
+ raise
+ return self
+
+ async def __aexit__(self, exc_type, exc_val, exc_tb): # noqa: ARG002
+ self.close()
+ return False
@property
def is_read_only(self) -> bool:
"""Whether the store is in read-only mode."""
return self._read_only
- def _get_stored_vector_dim(self) -> int | None:
+ async def _get_stored_vector_dim(self) -> int | None:
"""Read the stored vector dimension from the settings table.
Returns:
The stored vector dimension, or None if not found.
"""
try:
- existing_tables = self.db.list_tables().tables
+ existing_tables = (await self.db.list_tables()).tables
if "settings" not in existing_tables:
return None
- settings_table = self.db.open_table("settings")
+ settings_table = await self.db.open_table("settings")
rows = (
- settings_table.search()
+ await settings_table.query()
.where("id = 'settings'")
.limit(1)
.to_arrow()
- .to_pylist()
- )
+ ).to_pylist()
if not rows or not rows[0].get("settings"):
return None
@@ -312,7 +362,7 @@ class Store:
self.document_items_table,
self.settings_table,
]:
- table.optimize(cleanup_older_than=retention)
+ await table.optimize(cleanup_older_than=retention)
except (RuntimeError, OSError) as e:
# Handle resource errors gracefully
logger.debug(f"Vacuum skipped due to resource constraints: {e}")
@@ -321,7 +371,7 @@ class Store:
def _connection_mode(self) -> ConnectionMode:
return ConnectionMode.from_config(self._config)
- def _ensure_vector_index(self) -> None:
+ async def _ensure_vector_index(self) -> None:
"""Create or rebuild vector index on chunks table.
Cloud deployments auto-create indexes, so we skip for those.
@@ -334,7 +384,7 @@ class Store:
try:
# Check if table has enough data (indexes require training data)
- row_count = self.chunks_table.count_rows()
+ row_count = await self.chunks_table.count_rows()
if row_count < 256:
logger.debug(
f"Skipping vector index creation: need at least 256 rows, have {row_count}"
@@ -343,30 +393,34 @@ class Store:
# Create or replace index (replace=True is the default)
logger.info("Creating vector index on chunks table...")
- self.chunks_table.create_index(
- metric=self._config.search.vector_index_metric,
- index_type="IVF_PQ",
- replace=True, # Explicit: replace existing index
+ await self.chunks_table.create_index(
+ "vector",
+ config=IvfPq(
+ distance_type=self._config.search.vector_index_metric,
+ ),
+ replace=True,
)
# Wait for index creation to complete
# Index name is column_name + "_idx"
- self.chunks_table.wait_for_index(["vector_idx"], timeout=timedelta(hours=1))
+ await self.chunks_table.wait_for_index(
+ ["vector_idx"], timeout=timedelta(hours=1)
+ )
logger.info("Vector index created successfully")
except Exception as e:
logger.warning(f"Could not create vector index: {e}")
- def _validate_configuration(self) -> None:
+ async def _validate_configuration(self) -> None:
"""Validate that the configuration is compatible with the database."""
from haiku.rag.store.repositories.settings import SettingsRepository
settings_repo = SettingsRepository(self)
- settings_repo.validate_config_compatibility()
+ await settings_repo.validate_config_compatibility()
- def _init_tables(self):
+ async def _init_tables(self):
"""Initialize database tables (create if they don't exist)."""
- existing_tables = self.db.list_tables().tables
+ existing_tables = (await self.db.list_tables()).tables
missing_tables = set(REQUIRED_TABLES) - set(existing_tables)
if missing_tables and self._read_only:
@@ -377,57 +431,61 @@ class Store:
# Create or open documents table
if "documents" in existing_tables:
- self.documents_table = self.db.open_table("documents")
+ self.documents_table = await self.db.open_table("documents")
else:
- self.documents_table = self.db.create_table(
+ self.documents_table = await self.db.create_table(
"documents", schema=get_documents_arrow_schema()
)
# Create or open chunks table
if "chunks" in existing_tables:
- self.chunks_table = self.db.open_table("chunks")
+ self.chunks_table = await self.db.open_table("chunks")
else:
- self.chunks_table = self.db.create_table("chunks", schema=self.ChunkRecord)
+ self.chunks_table = await self.db.create_table(
+ "chunks", schema=self.ChunkRecord
+ )
# Create FTS index on content_fts (contextualized content) for better search
- self.chunks_table.create_fts_index(
- "content_fts", replace=True, with_position=True, remove_stop_words=False
+ await self.chunks_table.create_index(
+ "content_fts",
+ config=FTS(with_position=True, remove_stop_words=False),
+ replace=True,
)
# Create or open document_items table
if "document_items" in existing_tables:
- self.document_items_table = self.db.open_table("document_items")
+ self.document_items_table = await self.db.open_table("document_items")
else:
- self.document_items_table = self.db.create_table(
+ self.document_items_table = await self.db.create_table(
"document_items", schema=DocumentItemRecord
)
- self.document_items_table.create_scalar_index(
- "document_id", index_type="BTREE", replace=True
+ await self.document_items_table.create_index(
+ "document_id", config=BTree(), replace=True
)
- self.document_items_table.create_scalar_index(
- "position", index_type="BTREE", replace=True
+ await self.document_items_table.create_index(
+ "position", config=BTree(), replace=True
)
- self.document_items_table.create_scalar_index(
- "self_ref", index_type="BTREE", replace=True
+ await self.document_items_table.create_index(
+ "self_ref", config=BTree(), replace=True
)
# Create or open settings table
if "settings" in existing_tables:
- self.settings_table = self.db.open_table("settings")
+ self.settings_table = await self.db.open_table("settings")
else:
- self.settings_table = self.db.create_table(
+ self.settings_table = await self.db.create_table(
"settings", schema=SettingsRecord
)
# Save current settings to the new database
settings_data = self._config.model_dump(mode="json")
- self.settings_table.add(
+ await self.settings_table.add(
[SettingsRecord(id="settings", settings=json.dumps(settings_data))]
)
- def _set_initial_version(self):
+ async def _set_initial_version(self):
"""Set the initial version for a new database."""
- self.set_haiku_version(metadata.version("haiku.rag-slim"))
+ await self.set_haiku_version(metadata.version("haiku.rag-slim"))
- def _check_migrations(self) -> None:
+ async def _check_migrations(self) -> None:
"""Check if migrations are pending and error or update version accordingly.
Raises:
@@ -436,7 +494,7 @@ class Store:
from haiku.rag.store.upgrades import get_pending_upgrades
current_version = metadata.version("haiku.rag-slim")
- db_version = self.get_haiku_version()
+ db_version = await self.get_haiku_version()
pending = get_pending_upgrades(db_version)
@@ -450,9 +508,9 @@ class Store:
# No pending migrations - update version silently if needed (writable only)
if not self._read_only and db_version != current_version:
- self.set_haiku_version(current_version)
+ await self.set_haiku_version(current_version)
- def migrate(self) -> list[str]:
+ async def migrate(self) -> list[str]:
"""Run pending database migrations.
Returns:
@@ -465,21 +523,21 @@ class Store:
from haiku.rag.store.upgrades import run_pending_upgrades
- db_version = self.get_haiku_version()
+ db_version = await self.get_haiku_version()
current_version = metadata.version("haiku.rag-slim")
- applied = run_pending_upgrades(self, db_version)
+ applied = await run_pending_upgrades(self, db_version)
# Update version after successful migration
if applied or db_version != current_version:
- self.set_haiku_version(current_version)
+ await self.set_haiku_version(current_version)
return applied
- def get_haiku_version(self) -> str:
+ async def get_haiku_version(self) -> str:
"""Returns the user version stored in settings."""
- settings_records = list(
- self.settings_table.search().limit(1).to_pydantic(SettingsRecord)
+ settings_records = await query_to_pydantic(
+ self.settings_table.query().limit(1), SettingsRecord
)
if settings_records:
settings = (
@@ -490,15 +548,15 @@ class Store:
return settings.get("version", "0.0.0")
return "0.0.0"
- def set_haiku_version(self, version: str) -> None:
+ async def set_haiku_version(self, version: str) -> None:
"""Updates the user version in settings.
Raises:
ReadOnlyError: If the store is in read-only mode.
"""
self._assert_writable()
- settings_records = list(
- self.settings_table.search().limit(1).to_pydantic(SettingsRecord)
+ settings_records = await query_to_pydantic(
+ self.settings_table.query().limit(1), SettingsRecord
)
if settings_records:
# Only write if version actually changes to avoid creating new table versions
@@ -509,73 +567,74 @@ class Store:
)
if current.get("version") != version:
current["version"] = version
- self.settings_table.update(
+ await self.settings_table.update(
+ {"settings": json.dumps(current)},
where="id = 'settings'",
- values={"settings": json.dumps(current)},
)
else:
# Create new settings record
settings_data = Config.model_dump(mode="json")
settings_data["version"] = version
- self.settings_table.add(
+ await self.settings_table.add(
[SettingsRecord(id="settings", settings=json.dumps(settings_data))]
)
- def recreate_embeddings_table(self) -> None:
+ async def recreate_embeddings_table(self) -> None:
"""Recreate the chunks table with current vector dimensions.
Raises:
ReadOnlyError: If the store is in read-only mode.
"""
self._assert_writable()
- # Drop and recreate chunks table
- try:
- self.db.drop_table("chunks")
- except Exception:
- pass
+ # Drop and recreate chunks table. Check existence first rather than
+ # catching-and-swallowing drop_table's errors — a catch-all would
+ # hide real failures (permissions, storage-backend errors) and then
+ # the subsequent create_table would fail confusingly.
+ if "chunks" in (await self.db.list_tables()).tables:
+ await self.db.drop_table("chunks")
# Update the ChunkRecord model with new vector dimension
self.ChunkRecord = create_chunk_model(self.embedder._vector_dim)
- self.chunks_table = self.db.create_table("chunks", schema=self.ChunkRecord)
+ self.chunks_table = await self.db.create_table(
+ "chunks", schema=self.ChunkRecord
+ )
# Create FTS index on content_fts (contextualized content) for better search
- self.chunks_table.create_fts_index(
- "content_fts", replace=True, with_position=True, remove_stop_words=False
+ await self.chunks_table.create_index(
+ "content_fts",
+ config=FTS(with_position=True, remove_stop_words=False),
+ replace=True,
)
def close(self):
"""Close the database connection."""
- # LanceDB connections are automatically managed
- pass
+ # AsyncConnection.close() is synchronous
+ if hasattr(self, "db"):
+ self.db.close()
- def current_table_versions(self) -> dict[str, int]:
+ async def current_table_versions(self) -> dict[str, int]:
"""Capture current versions of key tables for rollback using LanceDB's API."""
return {
- "documents": int(self.documents_table.version),
- "chunks": int(self.chunks_table.version),
- "document_items": int(self.document_items_table.version),
- "settings": int(self.settings_table.version),
+ "documents": await self.documents_table.version(),
+ "chunks": await self.chunks_table.version(),
+ "document_items": await self.document_items_table.version(),
+ "settings": await self.settings_table.version(),
}
- def restore_table_versions(self, versions: dict[str, int]) -> bool:
+ async def restore_table_versions(self, versions: dict[str, int]) -> bool:
"""Restore tables to the provided versions using LanceDB's API.
Raises:
ReadOnlyError: If the store is in read-only mode.
"""
self._assert_writable()
- self.documents_table.restore(int(versions["documents"]))
- self.chunks_table.restore(int(versions["chunks"]))
- self.document_items_table.restore(int(versions["document_items"]))
- self.settings_table.restore(int(versions["settings"]))
+ await self.documents_table.restore(int(versions["documents"]))
+ await self.chunks_table.restore(int(versions["chunks"]))
+ await self.document_items_table.restore(int(versions["document_items"]))
+ await self.settings_table.restore(int(versions["settings"]))
return True
- @property
- def _connection(self):
- """Compatibility property for repositories expecting _connection."""
- return self
-
- def _checkout_tables_before(self, before: datetime) -> None:
+ async def _checkout_tables_before(self, before: datetime) -> None:
"""Checkout all tables to their state at or before the given datetime.
Args:
@@ -601,7 +660,7 @@ class Store:
]
for table_name, table in tables:
- versions = table.list_versions()
+ versions = await table.list_versions()
# Find the latest version at or before the target datetime
# Versions are sorted by version number, not timestamp, so we need to check all
best_version = None
@@ -634,9 +693,9 @@ class Store:
)
# Checkout to the found version
- table.checkout(best_version)
+ await table.checkout(best_version)
- def list_table_versions(self, table_name: str) -> list[dict[str, Any]]:
+ async def list_table_versions(self, table_name: str) -> list[dict[str, Any]]:
"""List version history for a table.
Args:
@@ -655,4 +714,4 @@ class Store:
if table is None:
raise ValueError(f"Unknown table: {table_name}")
- return list(table.list_versions())
+ return list(await table.list_versions())
diff --git a/haiku_rag_slim/haiku/rag/store/repositories/chunk.py b/haiku_rag_slim/haiku/rag/store/repositories/chunk.py
index 1165950f..ed6286ca 100644
--- a/haiku_rag_slim/haiku/rag/store/repositories/chunk.py
+++ b/haiku_rag_slim/haiku/rag/store/repositories/chunk.py
@@ -1,19 +1,16 @@
import json
import logging
-from typing import TYPE_CHECKING, cast
+from typing import TYPE_CHECKING
from uuid import uuid4
if TYPE_CHECKING:
import pandas as pd
- from lancedb.query import (
- LanceHybridQueryBuilder,
- LanceQueryBuilder,
- LanceVectorQueryBuilder,
- )
+ from lancedb.query import AsyncQueryBase
+from lancedb.index import FTS
from lancedb.rerankers import RRFReranker
-from haiku.rag.store.engine import Store
+from haiku.rag.store.engine import Store, query_to_pydantic
from haiku.rag.store.models.chunk import Chunk
logger = logging.getLogger(__name__)
@@ -26,11 +23,13 @@ class ChunkRepository:
self.store = store
self.embedder = store.embedder
- def _ensure_fts_index(self) -> None:
+ async def _ensure_fts_index(self) -> None:
"""Ensure FTS index exists on the content_fts column."""
try:
- self.store.chunks_table.create_fts_index(
- "content_fts", replace=True, with_position=True, remove_stop_words=False
+ await self.store.chunks_table.create_index(
+ "content_fts",
+ config=FTS(with_position=True, remove_stop_words=False),
+ replace=True,
)
except Exception as e:
# Log the error but don't fail - FTS might already exist
@@ -69,7 +68,7 @@ class ChunkRepository:
vector=entity.embedding,
)
- self.store.chunks_table.add([chunk_record])
+ await self.store.chunks_table.add([chunk_record])
entity.id = chunk_id
return entity
@@ -90,6 +89,7 @@ class ChunkRepository:
chunk_id = str(uuid4())
assert chunk.document_id is not None
+ assert chunk.embedding is not None
chunk_record = self.store.ChunkRecord(
id=chunk_id,
document_id=chunk.document_id,
@@ -105,17 +105,15 @@ class ChunkRepository:
chunk.id = chunk_id
# Single batch insert for all chunks
- self.store.chunks_table.add(chunk_records)
+ await self.store.chunks_table.add(chunk_records)
return chunks
async def get_by_id(self, entity_id: str) -> Chunk | None:
"""Get a chunk by its ID."""
- results = list(
- self.store.chunks_table.search()
- .where(f"id = '{entity_id}'")
- .limit(1)
- .to_pydantic(self.store.ChunkRecord)
+ results = await query_to_pydantic(
+ self.store.chunks_table.query().where(f"id = '{entity_id}'").limit(1),
+ self.store.ChunkRecord,
)
if not results:
@@ -140,9 +138,8 @@ class ChunkRepository:
assert entity.id, "Chunk ID is required for update"
assert entity.embedding is not None, "Chunk must have an embedding"
- self.store.chunks_table.update(
- where=f"id = '{entity.id}'",
- values={
+ await self.store.chunks_table.update(
+ {
"document_id": entity.document_id,
"content": entity.content,
"content_fts": self._contextualize_content(entity),
@@ -152,6 +149,7 @@ class ChunkRepository:
"order": int(entity.order),
"vector": entity.embedding,
},
+ where=f"id = '{entity.id}'",
)
return entity
@@ -162,21 +160,21 @@ class ChunkRepository:
if chunk is None:
return False
- self.store.chunks_table.delete(f"id = '{entity_id}'")
+ await self.store.chunks_table.delete(f"id = '{entity_id}'")
return True
async def list_all(
self, limit: int | None = None, offset: int | None = None
) -> list[Chunk]:
"""List all chunks with optional pagination."""
- query = self.store.chunks_table.search()
+ query = self.store.chunks_table.query()
if offset is not None:
query = query.offset(offset)
if limit is not None:
query = query.limit(limit)
- results = list(query.to_pydantic(self.store.ChunkRecord))
+ results = await query_to_pydantic(query, self.store.ChunkRecord)
chunks: list[Chunk] = []
for rec in results:
@@ -196,13 +194,15 @@ class ChunkRepository:
"""Delete all chunks from the database."""
self.store._assert_writable()
# Drop and recreate table to clear all data
- self.store.db.drop_table("chunks")
- self.store.chunks_table = self.store.db.create_table(
+ await self.store.db.drop_table("chunks")
+ self.store.chunks_table = await self.store.db.create_table(
"chunks", schema=self.store.ChunkRecord
)
# Create FTS index on content_fts (contextualized content) for better search
- self.store.chunks_table.create_fts_index(
- "content_fts", replace=True, with_position=True, remove_stop_words=False
+ await self.store.chunks_table.create_index(
+ "content_fts",
+ config=FTS(with_position=True, remove_stop_words=False),
+ replace=True,
)
async def delete_by_document_id(self, document_id: str) -> bool:
@@ -213,7 +213,7 @@ class ChunkRepository:
if not chunks:
return False
- self.store.chunks_table.delete(f"document_id = '{document_id}'")
+ await self.store.chunks_table.delete(f"document_id = '{document_id}'")
return True
async def search(
@@ -236,63 +236,51 @@ class ChunkRepository:
"""
if not query.strip():
return []
- filtered_doc_ids = None
+
+ chunk_filter: str | None = None
if filter:
- # We perform filtering as a two-step process, first filtering documents, then
- # filtering chunks based on those document IDs.
- # This is because LanceDB does not support joins directly in search queries.
- docs_df = (
- self.store.documents_table.search()
+ # Translate the document-level filter into a chunk-level
+ # document_id IN (...) clause so LanceDB can combine it with
+ # limit. The previous two-step pattern (materialize top-N,
+ # filter in pandas, head(limit)) silently under-returned
+ # whenever the top-N window lacked `limit` matching chunks.
+ docs_df = await (
+ self.store.documents_table.query()
.select(["id"])
.where(filter)
.to_pandas()
)
- # Early exit if no documents match the filter
if docs_df.empty:
return []
- # Keep as pandas Series for efficient vectorized operations
- filtered_doc_ids = docs_df["id"]
+ id_list = ", ".join(f"'{d}'" for d in docs_df["id"])
+ chunk_filter = f"document_id IN ({id_list})"
- # Prepare search query based on search type
if search_type == "vector":
query_embedding = await self.embedder.embed_query(query)
- vector_query = cast(
- "LanceVectorQueryBuilder",
- self.store.chunks_table.search(
- query_embedding, query_type="vector", vector_column_name="vector"
- ),
+ results = (
+ self.store.chunks_table.query()
+ .nearest_to(query_embedding)
+ .column("vector")
+ .refine_factor(self.store._config.search.vector_refine_factor)
)
- results = vector_query.refine_factor(
- self.store._config.search.vector_refine_factor
- )
-
elif search_type == "fts":
- results = self.store.chunks_table.search(query, query_type="fts")
-
+ results = self.store.chunks_table.query().nearest_to_text(
+ query, columns="content_fts"
+ )
else: # hybrid (default)
query_embedding = await self.embedder.embed_query(query)
- # Create RRF reranker
reranker = RRFReranker()
- # Perform native hybrid search with RRF reranking
- hybrid_query = cast(
- "LanceHybridQueryBuilder",
- self.store.chunks_table.search(query_type="hybrid")
- .vector(query_embedding)
- .text(query),
+ results = (
+ self.store.chunks_table.query()
+ .nearest_to(query_embedding)
+ .column("vector")
+ .nearest_to_text(query, columns="content_fts")
+ .refine_factor(self.store._config.search.vector_refine_factor)
+ .rerank(reranker)
)
- results = hybrid_query.refine_factor(
- self.store._config.search.vector_refine_factor
- ).rerank(reranker)
- # Apply filtering if needed (common for all search types)
- if filtered_doc_ids is not None:
- chunks_df = results.to_pandas()
- filtered_chunks_df = chunks_df.loc[
- chunks_df["document_id"].isin(filtered_doc_ids)
- ].head(limit)
- return await self._process_search_results(filtered_chunks_df)
-
- # No filtering needed, apply limit and return
+ if chunk_filter is not None:
+ results = results.where(chunk_filter)
results = results.limit(limit)
return await self._process_search_results(results)
@@ -312,18 +300,18 @@ class ChunkRepository:
Returns:
List of chunks ordered by their order field.
"""
- query = self.store.chunks_table.search().where(f"document_id = '{document_id}'")
+ query = self.store.chunks_table.query().where(f"document_id = '{document_id}'")
if offset is not None:
query = query.offset(offset)
if limit is not None:
query = query.limit(limit)
- results = list(query.to_pydantic(self.store.ChunkRecord))
+ results = await query_to_pydantic(query, self.store.ChunkRecord)
# Get document info (only metadata columns, skip content/docling blobs)
- doc_rows = list(
- self.store.documents_table.search()
+ doc_rows = await (
+ self.store.documents_table.query()
.select(["id", "uri", "title", "metadata"])
.where(f"id = '{document_id}'")
.limit(1)
@@ -355,8 +343,8 @@ class ChunkRepository:
async def count_by_document_id(self, document_id: str) -> int:
"""Count the number of chunks for a specific document."""
- df = (
- self.store.chunks_table.search()
+ df = await (
+ self.store.chunks_table.query()
.select(["id"])
.where(f"document_id = '{document_id}'")
.to_pandas()
@@ -381,10 +369,8 @@ class ChunkRepository:
f" AND `order` >= {min_order}"
f" AND `order` <= {max_order}"
)
- results = list(
- self.store.chunks_table.search()
- .where(where)
- .to_pydantic(self.store.ChunkRecord)
+ results = await query_to_pydantic(
+ self.store.chunks_table.query().where(where), self.store.ChunkRecord
)
return [
Chunk(
@@ -398,12 +384,12 @@ class ChunkRepository:
]
async def _process_search_results(
- self, query_result: "pd.DataFrame | LanceQueryBuilder"
+ self, query_result: "pd.DataFrame | AsyncQueryBase"
) -> list[tuple[Chunk, float]]:
"""Process search results into chunks with document info and scores.
Args:
- query_result: Either a pandas DataFrame or a LanceDB query result
+ query_result: Either a pandas DataFrame or a LanceDB async query result
"""
import pandas as pd
@@ -426,7 +412,7 @@ class ChunkRepository:
df = query_result
else:
# Convert LanceDB query result to DataFrame
- df = query_result.to_pandas()
+ df = await query_result.to_pandas()
# Extract scores
scores = extract_scores(df)
@@ -452,8 +438,8 @@ class ChunkRepository:
if document_ids:
id_list = "', '".join(document_ids)
where_clause = f"id IN ('{id_list}')"
- doc_rows = list(
- self.store.documents_table.search()
+ doc_rows = await (
+ self.store.documents_table.query()
.select(["id", "uri", "title", "metadata"])
.where(where_clause)
.to_list()
diff --git a/haiku_rag_slim/haiku/rag/store/repositories/document.py b/haiku_rag_slim/haiku/rag/store/repositories/document.py
index 4f5e89e1..d2d0b234 100644
--- a/haiku_rag_slim/haiku/rag/store/repositories/document.py
+++ b/haiku_rag_slim/haiku/rag/store/repositories/document.py
@@ -2,7 +2,14 @@ import json
from datetime import datetime
from uuid import uuid4
-from haiku.rag.store.engine import DocumentRecord, Store, get_documents_arrow_schema
+from lancedb.index import BTree
+
+from haiku.rag.store.engine import (
+ DocumentRecord,
+ Store,
+ get_documents_arrow_schema,
+ query_to_pydantic,
+)
from haiku.rag.store.models.document import Document
from haiku.rag.utils import escape_sql_string
@@ -78,7 +85,7 @@ class DocumentRepository:
)
# Add to table
- self.store.documents_table.add([doc_record])
+ await self.store.documents_table.add([doc_record])
entity.id = doc_id
entity.created_at = datetime.fromisoformat(now)
@@ -88,11 +95,9 @@ class DocumentRepository:
async def get_by_id(self, entity_id: str) -> Document | None:
"""Get a document by its ID."""
safe_id = escape_sql_string(entity_id)
- results = list(
- self.store.documents_table.search()
- .where(f"id = '{safe_id}'")
- .limit(1)
- .to_pydantic(DocumentRecord)
+ results = await query_to_pydantic(
+ self.store.documents_table.query().where(f"id = '{safe_id}'").limit(1),
+ DocumentRecord,
)
if not results:
@@ -103,8 +108,8 @@ class DocumentRepository:
async def get_content(self, entity_id: str) -> str | None:
"""Get only the text content of a document (skips docling blobs)."""
safe_id = escape_sql_string(entity_id)
- results = list(
- self.store.documents_table.search()
+ results = await (
+ self.store.documents_table.query()
.select(["content"])
.where(f"id = '{safe_id}'")
.limit(1)
@@ -119,8 +124,8 @@ class DocumentRepository:
async def get_docling_data(self, entity_id: str) -> Document | None:
"""Get a document with only docling data loaded (skips content blob)."""
safe_id = escape_sql_string(entity_id)
- results = list(
- self.store.documents_table.search()
+ results = await (
+ self.store.documents_table.query()
.select(self._DOCLING_COLUMNS)
.where(f"id = '{safe_id}'")
.limit(1)
@@ -141,8 +146,8 @@ class DocumentRepository:
async def get_pages_data(self, entity_id: str) -> Document | None:
"""Get a document with only page image data loaded."""
safe_id = escape_sql_string(entity_id)
- results = list(
- self.store.documents_table.search()
+ results = await (
+ self.store.documents_table.query()
.select(["id", "docling_pages"])
.where(f"id = '{safe_id}'")
.limit(1)
@@ -171,9 +176,8 @@ class DocumentRepository:
# Update the record
safe_id = escape_sql_string(entity.id)
- self.store.documents_table.update(
- where=f"id = '{safe_id}'",
- values={
+ await self.store.documents_table.update(
+ {
"content": entity.content,
"uri": entity.uri,
"title": entity.title,
@@ -183,6 +187,7 @@ class DocumentRepository:
"docling_version": entity.docling_version,
"updated_at": now,
},
+ where=f"id = '{safe_id}'",
)
return entity
@@ -202,7 +207,7 @@ class DocumentRepository:
# Delete the document
safe_id = escape_sql_string(entity_id)
- self.store.documents_table.delete(f"id = '{safe_id}'")
+ await self.store.documents_table.delete(f"id = '{safe_id}'")
return True
_LISTING_COLUMNS = ["id", "title", "uri", "metadata", "created_at", "updated_at"]
@@ -226,7 +231,7 @@ class DocumentRepository:
Returns:
List of Document instances matching the criteria.
"""
- query = self.store.documents_table.search()
+ query = self.store.documents_table.query()
if not include_content:
query = query.select(self._LISTING_COLUMNS)
@@ -238,7 +243,7 @@ class DocumentRepository:
query = query.limit(limit)
if include_content:
- results = list(query.to_pydantic(DocumentRecord))
+ results = await query_to_pydantic(query, DocumentRecord)
return [self._record_to_document(doc) for doc in results]
return [
@@ -255,7 +260,7 @@ class DocumentRepository:
if row.get("updated_at")
else datetime.now(),
)
- for row in query.to_list()
+ for row in await query.to_list()
]
async def count(self, filter: str | None = None) -> int:
@@ -267,16 +272,14 @@ class DocumentRepository:
Returns:
Number of documents matching the criteria.
"""
- return self.store.documents_table.count_rows(filter=filter)
+ return await self.store.documents_table.count_rows(filter=filter)
async def get_by_uri(self, uri: str) -> Document | None:
"""Get a document by its URI."""
escaped_uri = escape_sql_string(uri)
- results = list(
- self.store.documents_table.search()
- .where(f"uri = '{escaped_uri}'")
- .limit(1)
- .to_pydantic(DocumentRecord)
+ results = await query_to_pydantic(
+ self.store.documents_table.query().where(f"uri = '{escaped_uri}'").limit(1),
+ DocumentRecord,
)
if not results:
@@ -291,29 +294,29 @@ class DocumentRepository:
# Delete all chunks and items first
await self.chunk_repository.delete_all()
- self.store.db.drop_table("document_items")
- self.store.document_items_table = self.store.db.create_table(
+ await self.store.db.drop_table("document_items")
+ self.store.document_items_table = await self.store.db.create_table(
"document_items", schema=DocumentItemRecord
)
- self.store.document_items_table.create_scalar_index(
- "document_id", index_type="BTREE", replace=True
+ await self.store.document_items_table.create_index(
+ "document_id", config=BTree(), replace=True
)
- self.store.document_items_table.create_scalar_index(
- "position", index_type="BTREE", replace=True
+ await self.store.document_items_table.create_index(
+ "position", config=BTree(), replace=True
)
- self.store.document_items_table.create_scalar_index(
- "self_ref", index_type="BTREE", replace=True
+ await self.store.document_items_table.create_index(
+ "self_ref", config=BTree(), replace=True
)
# Get count before deletion
count = len(
- list(
- self.store.documents_table.search().limit(1).to_pydantic(DocumentRecord)
+ await query_to_pydantic(
+ self.store.documents_table.query().limit(1), DocumentRecord
)
)
if count > 0:
# Drop and recreate table to clear all data
- self.store.db.drop_table("documents")
- self.store.documents_table = self.store.db.create_table(
+ await self.store.db.drop_table("documents")
+ self.store.documents_table = await self.store.db.create_table(
"documents", schema=get_documents_arrow_schema()
)
diff --git a/haiku_rag_slim/haiku/rag/store/repositories/document_item.py b/haiku_rag_slim/haiku/rag/store/repositories/document_item.py
index 1e68c736..de093a0b 100644
--- a/haiku_rag_slim/haiku/rag/store/repositories/document_item.py
+++ b/haiku_rag_slim/haiku/rag/store/repositories/document_item.py
@@ -38,13 +38,13 @@ class DocumentItemRepository:
)
for item in items
]
- self.store.document_items_table.add(records)
+ await self.store.document_items_table.add(records)
async def get_all_items(self, document_id: str) -> list[DocumentItem]:
"""Get all items for a document, sorted by position."""
safe_id = escape_sql_string(document_id)
- rows = (
- self.store.document_items_table.search()
+ rows = await (
+ self.store.document_items_table.query()
.where(f"document_id = '{safe_id}'")
.to_list()
)
@@ -64,11 +64,11 @@ class DocumentItemRepository:
Returns:
Dict mapping document_id to sorted list of DocumentItem.
"""
- query = self.store.document_items_table.search()
+ query = self.store.document_items_table.query()
if document_ids is not None:
safe_ids = ", ".join(f"'{escape_sql_string(did)}'" for did in document_ids)
query = query.where(f"document_id IN ({safe_ids})")
- rows = query.to_list()
+ rows = await query.to_list()
grouped: dict[str, list[DocumentItem]] = {}
for row in rows:
@@ -83,8 +83,8 @@ class DocumentItemRepository:
) -> list[DocumentItem]:
"""Get items for a document within a position range (inclusive)."""
safe_id = escape_sql_string(document_id)
- rows = (
- self.store.document_items_table.search()
+ rows = await (
+ self.store.document_items_table.query()
.where(
f"document_id = '{safe_id}' "
f"AND position >= {start} AND position <= {end}"
@@ -102,8 +102,8 @@ class DocumentItemRepository:
safe_id = escape_sql_string(document_id)
refs_sql = ", ".join(f"'{escape_sql_string(r)}'" for r in refs)
- rows = (
- self.store.document_items_table.search()
+ rows = await (
+ self.store.document_items_table.query()
.select(["self_ref", "position"])
.where(f"document_id = '{safe_id}' AND self_ref IN ({refs_sql})")
.to_list()
@@ -113,7 +113,7 @@ class DocumentItemRepository:
async def get_item_count(self, document_id: str) -> int:
"""Count items for a document."""
safe_id = escape_sql_string(document_id)
- return self.store.document_items_table.count_rows(
+ return await self.store.document_items_table.count_rows(
filter=f"document_id = '{safe_id}'"
)
@@ -121,4 +121,4 @@ class DocumentItemRepository:
"""Delete all items for a document."""
self.store._assert_writable()
safe_id = escape_sql_string(document_id)
- self.store.document_items_table.delete(f"document_id = '{safe_id}'")
+ await self.store.document_items_table.delete(f"document_id = '{safe_id}'")
diff --git a/haiku_rag_slim/haiku/rag/store/repositories/settings.py b/haiku_rag_slim/haiku/rag/store/repositories/settings.py
index 79f86e4c..40e206f4 100644
--- a/haiku_rag_slim/haiku/rag/store/repositories/settings.py
+++ b/haiku_rag_slim/haiku/rag/store/repositories/settings.py
@@ -1,6 +1,6 @@
import json
-from haiku.rag.store.engine import SettingsRecord, Store
+from haiku.rag.store.engine import SettingsRecord, Store, query_to_pydantic
class ConfigMismatchError(Exception):
@@ -18,16 +18,14 @@ class SettingsRepository:
async def create(self, entity: dict) -> dict:
"""Create settings in the database."""
settings_record = SettingsRecord(id="settings", settings=json.dumps(entity))
- self.store.settings_table.add([settings_record])
+ await self.store.settings_table.add([settings_record])
return entity
async def get_by_id(self, entity_id: str) -> dict | None:
"""Get settings by ID."""
- results = list(
- self.store.settings_table.search()
- .where(f"id = '{entity_id}'")
- .limit(1)
- .to_pydantic(SettingsRecord)
+ results = await query_to_pydantic(
+ self.store.settings_table.query().where(f"id = '{entity_id}'").limit(1),
+ SettingsRecord,
)
if not results:
@@ -37,32 +35,32 @@ class SettingsRepository:
async def update(self, entity: dict) -> dict:
"""Update existing settings."""
- self.store.settings_table.update(
- where="id = 'settings'", values={"settings": json.dumps(entity)}
+ await self.store.settings_table.update(
+ {"settings": json.dumps(entity)}, where="id = 'settings'"
)
return entity
async def delete(self, entity_id: str) -> bool:
"""Delete settings by ID."""
- self.store.settings_table.delete(f"id = '{entity_id}'")
+ await self.store.settings_table.delete(f"id = '{entity_id}'")
return True
async def list_all(
self, limit: int | None = None, offset: int | None = None
) -> list[dict]:
"""List all settings."""
- results = list(self.store.settings_table.search().to_pydantic(SettingsRecord))
+ results = await query_to_pydantic(
+ self.store.settings_table.query(), SettingsRecord
+ )
return [
json.loads(record.settings) if record.settings else {} for record in results
]
- def get_current_settings(self) -> dict:
+ async def get_current_settings(self) -> dict:
"""Get the current settings."""
- results = list(
- self.store.settings_table.search()
- .where("id = 'settings'")
- .limit(1)
- .to_pydantic(SettingsRecord)
+ results = await query_to_pydantic(
+ self.store.settings_table.query().where("id = 'settings'").limit(1),
+ SettingsRecord,
)
if not results:
@@ -70,17 +68,15 @@ class SettingsRepository:
return json.loads(results[0].settings) if results[0].settings else {}
- def save_current_settings(self) -> None:
+ async def save_current_settings(self) -> None:
"""Save the current configuration to the database."""
self.store._assert_writable()
current_config = self.store._config.model_dump(mode="json")
# Check if settings exist
- existing = list(
- self.store.settings_table.search()
- .where("id = 'settings'")
- .limit(1)
- .to_pydantic(SettingsRecord)
+ existing = await query_to_pydantic(
+ self.store.settings_table.query().where("id = 'settings'").limit(1),
+ SettingsRecord,
)
if existing:
@@ -91,24 +87,24 @@ class SettingsRepository:
# Update existing settings
if existing_settings != current_config:
- self.store.settings_table.update(
+ await self.store.settings_table.update(
+ {"settings": json.dumps(current_config)},
where="id = 'settings'",
- values={"settings": json.dumps(current_config)},
)
else:
# Create new settings
settings_record = SettingsRecord(
id="settings", settings=json.dumps(current_config)
)
- self.store.settings_table.add([settings_record])
+ await self.store.settings_table.add([settings_record])
- def validate_config_compatibility(self) -> None:
+ async def validate_config_compatibility(self) -> None:
"""Validate that the current configuration is compatible with stored settings."""
- stored_settings = self.get_current_settings()
+ stored_settings = await self.get_current_settings()
# If no stored settings, this is a new database - save current config and return
if not stored_settings:
- self.save_current_settings()
+ await self.save_current_settings()
return
current_config = self.store._config.model_dump(mode="json")
diff --git a/haiku_rag_slim/haiku/rag/store/upgrades/__init__.py b/haiku_rag_slim/haiku/rag/store/upgrades/__init__.py
index 30d874e9..dd80f8f8 100644
--- a/haiku_rag_slim/haiku/rag/store/upgrades/__init__.py
+++ b/haiku_rag_slim/haiku/rag/store/upgrades/__init__.py
@@ -1,7 +1,7 @@
import logging
-from collections.abc import Callable
+from collections.abc import Callable, Coroutine
from dataclasses import dataclass
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Any
from packaging.version import Version, parse
@@ -16,7 +16,7 @@ class Upgrade:
"""Represents a database upgrade step."""
version: str
- apply: Callable[["Store"], None]
+ apply: Callable[["Store"], Coroutine[Any, Any, None]]
description: str = ""
@@ -36,7 +36,7 @@ def get_pending_upgrades(from_version: str) -> list[Upgrade]:
return [s for s in sorted_steps if v_from < parse(s.version)]
-def run_pending_upgrades(store: "Store", from_version: str) -> list[str]:
+async def run_pending_upgrades(store: "Store", from_version: str) -> list[str]:
"""Run upgrades where from_version < step.version.
Returns:
@@ -58,7 +58,7 @@ def run_pending_upgrades(store: "Store", from_version: str) -> list[str]:
idx,
len(applicable),
)
- step.apply(store)
+ await step.apply(store)
logger.info("Completed upgrade %s", step.version)
applied.append(
f"{step.version}: {step.description}" if step.description else step.version
diff --git a/haiku_rag_slim/haiku/rag/store/upgrades/v0_20_0.py b/haiku_rag_slim/haiku/rag/store/upgrades/v0_20_0.py
index a90259d3..5d23d8ad 100644
--- a/haiku_rag_slim/haiku/rag/store/upgrades/v0_20_0.py
+++ b/haiku_rag_slim/haiku/rag/store/upgrades/v0_20_0.py
@@ -7,12 +7,12 @@ from haiku.rag.store.engine import Store
from haiku.rag.store.upgrades import Upgrade
-def _apply_add_docling_document_columns(store: Store) -> None: # pragma: no cover
+async def _apply_add_docling_document_columns(store: Store) -> None: # pragma: no cover
"""Add 'docling_document_json' and 'docling_version' columns to documents table."""
# Read existing rows using Arrow for schema-agnostic access
try:
- docs_arrow = store.documents_table.search().to_arrow()
+ docs_arrow = await store.documents_table.query().to_arrow()
rows = docs_arrow.to_pylist()
except Exception:
rows = []
@@ -30,11 +30,13 @@ def _apply_add_docling_document_columns(store: Store) -> None: # pragma: no cov
# Drop and recreate documents table with the new schema
try:
- store.db.drop_table("documents")
+ await store.db.drop_table("documents")
except Exception:
pass
- store.documents_table = store.db.create_table("documents", schema=DocumentRecordV3)
+ store.documents_table = await store.db.create_table(
+ "documents", schema=DocumentRecordV3
+ )
# Reinsert previous rows with new columns as None
if rows:
@@ -58,7 +60,7 @@ def _apply_add_docling_document_columns(store: Store) -> None: # pragma: no cov
)
)
- store.documents_table.add(backfilled)
+ await store.documents_table.add(backfilled)
upgrade_add_docling_document = Upgrade(
diff --git a/haiku_rag_slim/haiku/rag/store/upgrades/v0_23_1.py b/haiku_rag_slim/haiku/rag/store/upgrades/v0_23_1.py
index 5797ee7e..9520267e 100644
--- a/haiku_rag_slim/haiku/rag/store/upgrades/v0_23_1.py
+++ b/haiku_rag_slim/haiku/rag/store/upgrades/v0_23_1.py
@@ -1,5 +1,6 @@
import json
+from lancedb.index import FTS
from lancedb.pydantic import LanceModel, Vector
from pydantic import Field
@@ -7,11 +8,11 @@ from haiku.rag.store.engine import Store
from haiku.rag.store.upgrades import Upgrade
-def _apply_add_content_fts(store: Store) -> None: # pragma: no cover
+async def _apply_add_content_fts(store: Store) -> None: # pragma: no cover
"""Add content_fts column with contextualized content for better FTS."""
# Read existing chunks
try:
- chunks_arrow = store.chunks_table.search().to_arrow()
+ chunks_arrow = await store.chunks_table.query().to_arrow()
rows = chunks_arrow.to_pylist()
except Exception:
return
@@ -38,11 +39,11 @@ def _apply_add_content_fts(store: Store) -> None: # pragma: no cover
# Drop and recreate table with new schema
try:
- store.db.drop_table("chunks")
+ await store.db.drop_table("chunks")
except Exception:
pass
- store.chunks_table = store.db.create_table("chunks", schema=ChunkRecord)
+ store.chunks_table = await store.db.create_table("chunks", schema=ChunkRecord)
# Populate content_fts with contextualized content
new_records: list[ChunkRecord] = []
@@ -79,17 +80,19 @@ def _apply_add_content_fts(store: Store) -> None: # pragma: no cover
)
if new_records:
- store.chunks_table.add(new_records)
+ await store.chunks_table.add(new_records)
# Drop old FTS index on content column if it exists
try:
- store.chunks_table.drop_index("content_idx")
+ await store.chunks_table.drop_index("content_idx")
except Exception:
pass
# Create FTS index on content_fts
- store.chunks_table.create_fts_index(
- "content_fts", replace=True, with_position=True, remove_stop_words=False
+ await store.chunks_table.create_index(
+ "content_fts",
+ config=FTS(with_position=True, remove_stop_words=False),
+ replace=True,
)
diff --git a/haiku_rag_slim/haiku/rag/store/upgrades/v0_25_0.py b/haiku_rag_slim/haiku/rag/store/upgrades/v0_25_0.py
index 9e49b853..914e0df0 100644
--- a/haiku_rag_slim/haiku/rag/store/upgrades/v0_25_0.py
+++ b/haiku_rag_slim/haiku/rag/store/upgrades/v0_25_0.py
@@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
BATCH_SIZE = 10
-def _apply_compress_docling_document(store: Store) -> None: # pragma: no cover
+async def _apply_compress_docling_document(store: Store) -> None: # pragma: no cover
"""Migrate docling_document_json (str) to docling_document (compressed bytes)."""
class DocumentRecordV4(LanceModel):
@@ -76,36 +76,33 @@ def _apply_compress_docling_document(store: Store) -> None: # pragma: no cover
# First pass: collect document IDs to process
try:
- ids = [
- row["id"]
- for row in store.documents_table.search()
- .select(["id"])
- .to_arrow()
- .to_pylist()
- ]
+ ids = (
+ await store.documents_table.query().select(["id"]).to_arrow()
+ ).to_pylist()
+ ids = [row["id"] for row in ids]
except Exception:
ids = []
if not ids:
# Check if there's a staging table from a failed migration to recover from
- if "documents_v4_staging" in store.db.list_tables().tables:
- staging_table = store.db.open_table("documents_v4_staging")
- staging_ids = [
- row["id"]
- for row in staging_table.search().select(["id"]).to_arrow().to_pylist()
- ]
+ if "documents_v4_staging" in (await store.db.list_tables()).tables:
+ staging_table = await store.db.open_table("documents_v4_staging")
+ staging_ids = (
+ await staging_table.query().select(["id"]).to_arrow()
+ ).to_pylist()
+ staging_ids = [row["id"] for row in staging_ids]
if staging_ids:
logger.info(
"Recovering %d documents from failed migration", len(staging_ids)
)
# Create new documents table and copy from staging
store.documents_table = None
- if "documents" in store.db.list_tables().tables:
- store.db.drop_table("documents")
- store.documents_table = store.db.create_table(
+ if "documents" in (await store.db.list_tables()).tables:
+ await store.db.drop_table("documents")
+ store.documents_table = await store.db.create_table(
"documents", schema=get_documents_arrow_schema_v4()
)
- # Copy data from staging (reuse the copy logic below by jumping there)
+ # Copy data from staging
total_batches = (len(staging_ids) + BATCH_SIZE - 1) // BATCH_SIZE
for batch_num, i in enumerate(
range(0, len(staging_ids), BATCH_SIZE), 1
@@ -113,11 +110,10 @@ def _apply_compress_docling_document(store: Store) -> None: # pragma: no cover
batch_ids = staging_ids[i : i + BATCH_SIZE]
id_list = ", ".join(f"'{id}'" for id in batch_ids)
batch = (
- staging_table.search()
+ await staging_table.query()
.where(f"id IN ({id_list})")
.to_arrow()
- .to_pylist()
- )
+ ).to_pylist()
records = [
DocumentRecordV4(
id=row["id"],
@@ -133,26 +129,26 @@ def _apply_compress_docling_document(store: Store) -> None: # pragma: no cover
for row in batch
]
if records:
- store.documents_table.add(records)
+ await store.documents_table.add(records)
logger.info("Recovered batch %d/%d", batch_num, total_batches)
# Cleanup staging
- store.db.drop_table("documents_v4_staging")
+ await store.db.drop_table("documents_v4_staging")
logger.info("Recovery complete")
return
# No documents and no staging to recover, just recreate table with new schema
store.documents_table = None
- if "documents" in store.db.list_tables().tables:
- store.db.drop_table("documents")
- store.documents_table = store.db.create_table(
+ if "documents" in (await store.db.list_tables()).tables:
+ await store.db.drop_table("documents")
+ store.documents_table = await store.db.create_table(
"documents", schema=get_documents_arrow_schema_v4()
)
return
# Create staging table with new schema
- if "documents_v4_staging" in store.db.list_tables().tables:
- store.db.drop_table("documents_v4_staging")
- staging_table = store.db.create_table(
+ if "documents_v4_staging" in (await store.db.list_tables()).tables:
+ await store.db.drop_table("documents_v4_staging")
+ staging_table = await store.db.create_table(
"documents_v4_staging", schema=get_documents_arrow_schema_v4()
)
@@ -166,15 +162,12 @@ def _apply_compress_docling_document(store: Store) -> None: # pragma: no cover
id_list = ", ".join(f"'{id}'" for id in batch_ids)
batch = (
- store.documents_table.search()
- .where(f"id IN ({id_list})")
- .to_arrow()
- .to_pylist()
- )
+ await store.documents_table.query().where(f"id IN ({id_list})").to_arrow()
+ ).to_pylist()
migrated_batch = [migrate_row(row) for row in batch]
if migrated_batch:
- staging_table.add(migrated_batch)
+ await staging_table.add(migrated_batch)
logger.info(
"Compressed batch %d/%d (%d documents)",
@@ -185,17 +178,15 @@ def _apply_compress_docling_document(store: Store) -> None: # pragma: no cover
# Replace old table with staging table
store.documents_table = None
- if "documents" in store.db.list_tables().tables:
- store.db.drop_table("documents")
- store.documents_table = store.db.create_table(
+ if "documents" in (await store.db.list_tables()).tables:
+ await store.db.drop_table("documents")
+ store.documents_table = await store.db.create_table(
"documents", schema=get_documents_arrow_schema_v4()
)
# Copy from staging to final table in batches
- staging_ids = [
- row["id"]
- for row in staging_table.search().select(["id"]).to_arrow().to_pylist()
- ]
+ staging_ids = (await staging_table.query().select(["id"]).to_arrow()).to_pylist()
+ staging_ids = [row["id"] for row in staging_ids]
logger.info("Copying %d documents to new table", len(staging_ids))
@@ -204,8 +195,8 @@ def _apply_compress_docling_document(store: Store) -> None: # pragma: no cover
id_list = ", ".join(f"'{id}'" for id in batch_ids)
batch = (
- staging_table.search().where(f"id IN ({id_list})").to_arrow().to_pylist()
- )
+ await staging_table.query().where(f"id IN ({id_list})").to_arrow()
+ ).to_pylist()
records = [
DocumentRecordV4(
id=row["id"],
@@ -221,18 +212,18 @@ def _apply_compress_docling_document(store: Store) -> None: # pragma: no cover
for row in batch
]
if records:
- store.documents_table.add(records)
+ await store.documents_table.add(records)
logger.info("Copied batch %d/%d", batch_num, total_batches)
# Cleanup staging table
- if "documents_v4_staging" in store.db.list_tables().tables:
- store.db.drop_table("documents_v4_staging")
+ if "documents_v4_staging" in (await store.db.list_tables()).tables:
+ await store.db.drop_table("documents_v4_staging")
# Vacuum all tables (destructive migration, no history preserved)
logger.info("Vacuuming database")
for table in [store.documents_table, store.chunks_table, store.settings_table]:
try:
- table.optimize(cleanup_older_than=timedelta(seconds=0))
+ await table.optimize(cleanup_older_than=timedelta(seconds=0))
except Exception:
pass
diff --git a/haiku_rag_slim/haiku/rag/store/upgrades/v0_38_0.py b/haiku_rag_slim/haiku/rag/store/upgrades/v0_38_0.py
index 0c28ed77..0e5fa19e 100644
--- a/haiku_rag_slim/haiku/rag/store/upgrades/v0_38_0.py
+++ b/haiku_rag_slim/haiku/rag/store/upgrades/v0_38_0.py
@@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
BATCH_SIZE = 5
-def _apply_split_pages_zstd(store: Store) -> None: # pragma: no cover
+async def _apply_split_pages_zstd(store: Store) -> None: # pragma: no cover
"""Split docling_document into structure + pages and re-compress with zstd."""
class DocumentRecordV5(LanceModel):
@@ -99,32 +99,29 @@ def _apply_split_pages_zstd(store: Store) -> None: # pragma: no cover
# First pass: collect document IDs to process
try:
- ids = [
- row["id"]
- for row in store.documents_table.search()
- .select(["id"])
- .to_arrow()
- .to_pylist()
- ]
+ ids = (
+ await store.documents_table.query().select(["id"]).to_arrow()
+ ).to_pylist()
+ ids = [row["id"] for row in ids]
except (pa.ArrowInvalid, pa.ArrowNotImplementedError, OSError):
ids = []
if not ids:
# Check for staging table from a failed migration
- if staging_name in store.db.list_tables().tables:
- staging_table = store.db.open_table(staging_name)
- staging_ids = [
- row["id"]
- for row in staging_table.search().select(["id"]).to_arrow().to_pylist()
- ]
+ if staging_name in (await store.db.list_tables()).tables:
+ staging_table = await store.db.open_table(staging_name)
+ staging_ids = (
+ await staging_table.query().select(["id"]).to_arrow()
+ ).to_pylist()
+ staging_ids = [row["id"] for row in staging_ids]
if staging_ids:
logger.info(
"Recovering %d documents from failed migration", len(staging_ids)
)
store.documents_table = None
- if "documents" in store.db.list_tables().tables:
- store.db.drop_table("documents")
- store.documents_table = store.db.create_table(
+ if "documents" in (await store.db.list_tables()).tables:
+ await store.db.drop_table("documents")
+ store.documents_table = await store.db.create_table(
"documents", schema=get_documents_arrow_schema_v5()
)
total_batches = (len(staging_ids) + BATCH_SIZE - 1) // BATCH_SIZE
@@ -134,32 +131,31 @@ def _apply_split_pages_zstd(store: Store) -> None: # pragma: no cover
batch_ids = staging_ids[i : i + BATCH_SIZE]
id_list = ", ".join(f"'{doc_id}'" for doc_id in batch_ids)
batch = (
- staging_table.search()
+ await staging_table.query()
.where(f"id IN ({id_list})")
.to_arrow()
- .to_pylist()
- )
+ ).to_pylist()
records = [copy_staging_row(row) for row in batch]
if records:
- store.documents_table.add(records)
+ await store.documents_table.add(records)
logger.info("Recovered batch %d/%d", batch_num, total_batches)
- store.db.drop_table(staging_name)
+ await store.db.drop_table(staging_name)
logger.info("Recovery complete")
return
# No documents and no staging — recreate table with new schema
store.documents_table = None
- if "documents" in store.db.list_tables().tables:
- store.db.drop_table("documents")
- store.documents_table = store.db.create_table(
+ if "documents" in (await store.db.list_tables()).tables:
+ await store.db.drop_table("documents")
+ store.documents_table = await store.db.create_table(
"documents", schema=get_documents_arrow_schema_v5()
)
return
# Create staging table with new schema
- if staging_name in store.db.list_tables().tables:
- store.db.drop_table(staging_name)
- staging_table = store.db.create_table(
+ if staging_name in (await store.db.list_tables()).tables:
+ await store.db.drop_table(staging_name)
+ staging_table = await store.db.create_table(
staging_name, schema=get_documents_arrow_schema_v5()
)
@@ -177,15 +173,12 @@ def _apply_split_pages_zstd(store: Store) -> None: # pragma: no cover
id_list = ", ".join(f"'{doc_id}'" for doc_id in batch_ids)
batch = (
- store.documents_table.search()
- .where(f"id IN ({id_list})")
- .to_arrow()
- .to_pylist()
- )
+ await store.documents_table.query().where(f"id IN ({id_list})").to_arrow()
+ ).to_pylist()
migrated_batch = [migrate_row(row) for row in batch]
if migrated_batch:
- staging_table.add(migrated_batch)
+ await staging_table.add(migrated_batch)
logger.info(
"Migrated batch %d/%d (%d documents)",
@@ -196,17 +189,15 @@ def _apply_split_pages_zstd(store: Store) -> None: # pragma: no cover
# Replace old table with staging table
store.documents_table = None
- if "documents" in store.db.list_tables().tables:
- store.db.drop_table("documents")
- store.documents_table = store.db.create_table(
+ if "documents" in (await store.db.list_tables()).tables:
+ await store.db.drop_table("documents")
+ store.documents_table = await store.db.create_table(
"documents", schema=get_documents_arrow_schema_v5()
)
# Copy from staging to final table in batches
- staging_ids = [
- row["id"]
- for row in staging_table.search().select(["id"]).to_arrow().to_pylist()
- ]
+ staging_ids = (await staging_table.query().select(["id"]).to_arrow()).to_pylist()
+ staging_ids = [row["id"] for row in staging_ids]
logger.info("Copying %d documents to new table", len(staging_ids))
@@ -215,22 +206,22 @@ def _apply_split_pages_zstd(store: Store) -> None: # pragma: no cover
id_list = ", ".join(f"'{doc_id}'" for doc_id in batch_ids)
batch = (
- staging_table.search().where(f"id IN ({id_list})").to_arrow().to_pylist()
- )
+ await staging_table.query().where(f"id IN ({id_list})").to_arrow()
+ ).to_pylist()
records = [copy_staging_row(row) for row in batch]
if records:
- store.documents_table.add(records)
+ await store.documents_table.add(records)
logger.info("Copied batch %d/%d", batch_num, total_batches)
# Cleanup staging table
- if staging_name in store.db.list_tables().tables:
- store.db.drop_table(staging_name)
+ if staging_name in (await store.db.list_tables()).tables:
+ await store.db.drop_table(staging_name)
# Vacuum all tables
logger.info("Vacuuming database")
for table in [store.documents_table, store.chunks_table, store.settings_table]:
try:
- table.optimize(cleanup_older_than=timedelta(seconds=0))
+ await table.optimize(cleanup_older_than=timedelta(seconds=0))
except Exception:
pass
diff --git a/haiku_rag_slim/haiku/rag/store/upgrades/v0_40_0.py b/haiku_rag_slim/haiku/rag/store/upgrades/v0_40_0.py
index 907069ff..3e61aaad 100644
--- a/haiku_rag_slim/haiku/rag/store/upgrades/v0_40_0.py
+++ b/haiku_rag_slim/haiku/rag/store/upgrades/v0_40_0.py
@@ -8,7 +8,7 @@ from haiku.rag.utils import escape_sql_string
logger = logging.getLogger(__name__)
-def _apply_populate_document_items(store: Store) -> None: # pragma: no cover
+async def _apply_populate_document_items(store: Store) -> None: # pragma: no cover
"""Populate document_items table from existing docling documents."""
from docling_core.types.doc.document import DoclingDocument
@@ -16,10 +16,8 @@ def _apply_populate_document_items(store: Store) -> None: # pragma: no cover
from haiku.rag.store.models.document_item import extract_items
# Get all document IDs that have docling data
- ids = [
- row["id"]
- for row in store.documents_table.search().select(["id"]).to_arrow().to_pylist()
- ]
+ ids = (await store.documents_table.query().select(["id"]).to_arrow()).to_pylist()
+ ids = [row["id"] for row in ids]
if not ids:
logger.info("No documents to migrate")
@@ -33,8 +31,8 @@ def _apply_populate_document_items(store: Store) -> None: # pragma: no cover
for idx, doc_id in enumerate(ids, 1):
# Load only docling data
safe_id = escape_sql_string(doc_id)
- rows = (
- store.documents_table.search()
+ rows = await (
+ store.documents_table.query()
.select(["id", "docling_document"])
.where(f"id = '{safe_id}'")
.limit(1)
@@ -68,7 +66,7 @@ def _apply_populate_document_items(store: Store) -> None: # pragma: no cover
)
for item in items
]
- store.document_items_table.add(records)
+ await store.document_items_table.add(records)
migrated += 1
if idx % 10 == 0 or idx == total:
diff --git a/tests/agents/qa/test_qa.py b/tests/agents/qa/test_qa.py
index a12c8c45..19faebb0 100644
--- a/tests/agents/qa/test_qa.py
+++ b/tests/agents/qa/test_qa.py
@@ -15,56 +15,55 @@ def vcr_cassette_dir():
return str(Path(__file__).parent.parent.parent / "cassettes" / "test_qa")
-def test_get_qa_agent_factory(temp_db_path):
+@pytest.mark.asyncio
+async def test_get_qa_agent_factory(temp_db_path):
"""Test get_qa_agent factory function creates a properly configured agent."""
from haiku.rag.agents.qa import get_qa_agent
- client = HaikuRAG(temp_db_path, create=True)
- agent = get_qa_agent(client, Config)
+ async with HaikuRAG(temp_db_path, create=True) as client:
+ agent = get_qa_agent(client, Config)
- assert agent is not None
- assert isinstance(agent, QuestionAnswerAgent)
- # Verify internal client is set correctly
- assert agent._client is client
-
- client.close()
+ assert agent is not None
+ assert isinstance(agent, QuestionAnswerAgent)
+ # Verify internal client is set correctly
+ assert agent._client is client
-def test_get_qa_agent_with_custom_prompt(temp_db_path):
+@pytest.mark.asyncio
+async def test_get_qa_agent_with_custom_prompt(temp_db_path):
"""Test get_qa_agent factory with custom system prompt."""
from haiku.rag.agents.qa import get_qa_agent
- client = HaikuRAG(temp_db_path, create=True)
- custom_prompt = "You are a custom QA assistant."
- agent = get_qa_agent(client, Config, system_prompt=custom_prompt)
+ async with HaikuRAG(temp_db_path, create=True) as client:
+ custom_prompt = "You are a custom QA assistant."
+ agent = get_qa_agent(client, Config, system_prompt=custom_prompt)
- assert agent is not None
- assert isinstance(agent, QuestionAnswerAgent)
- assert agent._system_prompt == custom_prompt
-
- client.close()
+ assert agent is not None
+ assert isinstance(agent, QuestionAnswerAgent)
+ assert agent._system_prompt == custom_prompt
@pytest.mark.vcr()
async def test_qa_ollama(allow_model_requests, qa_corpus: Dataset, temp_db_path):
"""Test Ollama QA with LLM judge (VCR recorded)."""
- client = HaikuRAG(temp_db_path, create=True)
- qa = QuestionAnswerAgent(
- client, ModelConfig(provider="ollama", name="gpt-oss", enable_thinking=True)
- )
- llm_judge = LLMJudge()
+ async with HaikuRAG(temp_db_path, create=True) as client:
+ qa = QuestionAnswerAgent(
+ client,
+ ModelConfig(provider="ollama", name="gpt-oss", enable_thinking=True),
+ )
+ llm_judge = LLMJudge()
- doc = qa_corpus[1]
- await client.create_document(
- content=doc["document_extracted"], uri=doc["document_id"]
- )
+ doc = qa_corpus[1]
+ await client.create_document(
+ content=doc["document_extracted"], uri=doc["document_id"]
+ )
- question = doc["question"]
- expected_answer = doc["answer"]
+ question = doc["question"]
+ expected_answer = doc["answer"]
- answer, _ = await qa.answer(question)
- is_equivalent = await llm_judge.judge_answers(question, answer, expected_answer)
+ answer, _ = await qa.answer(question)
+ is_equivalent = await llm_judge.judge_answers(question, answer, expected_answer)
- assert is_equivalent, (
- f"Generated answer not equivalent to expected answer.\nQuestion: {question}\nGenerated: {answer}\nExpected: {expected_answer}"
- )
+ assert is_equivalent, (
+ f"Generated answer not equivalent to expected answer.\nQuestion: {question}\nGenerated: {answer}\nExpected: {expected_answer}"
+ )
diff --git a/tests/agents/research/test_research_graph.py b/tests/agents/research/test_research_graph.py
index 9a989653..9f93a758 100644
--- a/tests/agents/research/test_research_graph.py
+++ b/tests/agents/research/test_research_graph.py
@@ -21,28 +21,26 @@ async def test_graph_end_to_end(allow_model_requests, temp_db_path, qa_corpus):
"""Test research graph with real LLM calls recorded via VCR."""
graph = build_research_graph()
- client = HaikuRAG(temp_db_path, create=True)
- doc = qa_corpus[0]
- await client.create_document(
- content=doc["document_extracted"], uri=doc["document_id"]
- )
+ async with HaikuRAG(temp_db_path, create=True) as client:
+ doc = qa_corpus[0]
+ await client.create_document(
+ content=doc["document_extracted"], uri=doc["document_id"]
+ )
- state = ResearchState(
- context=ResearchContext(original_question=doc["question"]),
- max_iterations=1,
- max_concurrency=1,
- )
+ state = ResearchState(
+ context=ResearchContext(original_question=doc["question"]),
+ max_iterations=1,
+ max_concurrency=1,
+ )
- deps = ResearchDeps(client=client)
+ deps = ResearchDeps(client=client)
- result = await graph.run(state=state, deps=deps)
+ result = await graph.run(state=state, deps=deps)
- assert result is not None
- assert isinstance(result, ResearchReport)
- assert result.title
- assert result.executive_summary
-
- client.close()
+ assert result is not None
+ assert isinstance(result, ResearchReport)
+ assert result.title
+ assert result.executive_summary
def test_iterative_plan_result_model():
diff --git a/tests/agents/research/test_search_filter.py b/tests/agents/research/test_search_filter.py
index 67a1d1da..3bfd0b05 100644
--- a/tests/agents/research/test_search_filter.py
+++ b/tests/agents/research/test_search_filter.py
@@ -16,21 +16,18 @@ def vcr_cassette_dir():
@pytest.fixture
async def client_with_docs(temp_db_path):
"""Create a client with two distinct documents."""
- client = HaikuRAG(temp_db_path, create=True)
+ async with HaikuRAG(temp_db_path, create=True) as client:
+ # Add two documents with distinct content
+ doc1 = await client.create_document(
+ "Document about cats: Cats are small furry mammals that purr.",
+ title="Cat Facts",
+ )
+ doc2 = await client.create_document(
+ "Document about dogs: Dogs are loyal companions that bark.",
+ title="Dog Facts",
+ )
- # Add two documents with distinct content
- doc1 = await client.create_document(
- "Document about cats: Cats are small furry mammals that purr.",
- title="Cat Facts",
- )
- doc2 = await client.create_document(
- "Document about dogs: Dogs are loyal companions that bark.",
- title="Dog Facts",
- )
-
- yield client, doc1.id, doc2.id
-
- client.close()
+ yield client, doc1.id, doc2.id
@pytest.mark.vcr()
diff --git a/tests/cassettes/test_filter/test_search_with_filter_returns_full_limit.yaml b/tests/cassettes/test_filter/test_search_with_filter_returns_full_limit.yaml
new file mode 100644
index 00000000..da94b8f6
--- /dev/null
+++ b/tests/cassettes/test_filter/test_search_with_filter_returns_full_limit.yaml
@@ -0,0 +1,534 @@
+interactions:
+- request:
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate, zstd
+ connection:
+ - keep-alive
+ content-length:
+ - '225'
+ content-type:
+ - application/json
+ host:
+ - localhost:11434
+ method: POST
+ parsed_body:
+ encoding_format: base64
+ input:
+ - machine learning neural network deep learning model machine learning neural network deep learning model machine learning
+ neural network deep learning model
+ model: qwen3-embedding:4b
+ uri: http://localhost:11434/v1/embeddings
+ response:
+ headers:
+ content-type:
+ - application/json
+ transfer-encoding:
+ - chunked
+ parsed_body:
+ data:
+ - embedding: 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
+ index: 0
+ object: embedding
+ model: qwen3-embedding:4b
+ object: list
+ usage:
+ prompt_tokens: 22
+ total_tokens: 22
+ status:
+ code: 200
+ message: OK
+- request:
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate, zstd
+ connection:
+ - keep-alive
+ content-length:
+ - '225'
+ content-type:
+ - application/json
+ host:
+ - localhost:11434
+ method: POST
+ parsed_body:
+ encoding_format: base64
+ input:
+ - machine learning neural network deep learning model machine learning neural network deep learning model machine learning
+ neural network deep learning model
+ model: qwen3-embedding:4b
+ uri: http://localhost:11434/v1/embeddings
+ response:
+ headers:
+ content-type:
+ - application/json
+ transfer-encoding:
+ - chunked
+ parsed_body:
+ data:
+ - embedding: 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
+ index: 0
+ object: embedding
+ model: qwen3-embedding:4b
+ object: list
+ usage:
+ prompt_tokens: 22
+ total_tokens: 22
+ status:
+ code: 200
+ message: OK
+- request:
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate, zstd
+ connection:
+ - keep-alive
+ content-length:
+ - '225'
+ content-type:
+ - application/json
+ host:
+ - localhost:11434
+ method: POST
+ parsed_body:
+ encoding_format: base64
+ input:
+ - machine learning neural network deep learning model machine learning neural network deep learning model machine learning
+ neural network deep learning model
+ model: qwen3-embedding:4b
+ uri: http://localhost:11434/v1/embeddings
+ response:
+ headers:
+ content-type:
+ - application/json
+ transfer-encoding:
+ - chunked
+ parsed_body:
+ data:
+ - embedding: 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
+ index: 0
+ object: embedding
+ model: qwen3-embedding:4b
+ object: list
+ usage:
+ prompt_tokens: 22
+ total_tokens: 22
+ status:
+ code: 200
+ message: OK
+- request:
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate, zstd
+ connection:
+ - keep-alive
+ content-length:
+ - '225'
+ content-type:
+ - application/json
+ host:
+ - localhost:11434
+ method: POST
+ parsed_body:
+ encoding_format: base64
+ input:
+ - machine learning neural network deep learning model machine learning neural network deep learning model machine learning
+ neural network deep learning model
+ model: qwen3-embedding:4b
+ uri: http://localhost:11434/v1/embeddings
+ response:
+ headers:
+ content-type:
+ - application/json
+ transfer-encoding:
+ - chunked
+ parsed_body:
+ data:
+ - embedding: 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
+ index: 0
+ object: embedding
+ model: qwen3-embedding:4b
+ object: list
+ usage:
+ prompt_tokens: 22
+ total_tokens: 22
+ status:
+ code: 200
+ message: OK
+- request:
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate, zstd
+ connection:
+ - keep-alive
+ content-length:
+ - '225'
+ content-type:
+ - application/json
+ host:
+ - localhost:11434
+ method: POST
+ parsed_body:
+ encoding_format: base64
+ input:
+ - machine learning neural network deep learning model machine learning neural network deep learning model machine learning
+ neural network deep learning model
+ model: qwen3-embedding:4b
+ uri: http://localhost:11434/v1/embeddings
+ response:
+ headers:
+ content-type:
+ - application/json
+ transfer-encoding:
+ - chunked
+ parsed_body:
+ data:
+ - embedding: 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
+ index: 0
+ object: embedding
+ model: qwen3-embedding:4b
+ object: list
+ usage:
+ prompt_tokens: 22
+ total_tokens: 22
+ status:
+ code: 200
+ message: OK
+- request:
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate, zstd
+ connection:
+ - keep-alive
+ content-length:
+ - '225'
+ content-type:
+ - application/json
+ host:
+ - localhost:11434
+ method: POST
+ parsed_body:
+ encoding_format: base64
+ input:
+ - machine learning neural network deep learning model machine learning neural network deep learning model machine learning
+ neural network deep learning model
+ model: qwen3-embedding:4b
+ uri: http://localhost:11434/v1/embeddings
+ response:
+ headers:
+ content-type:
+ - application/json
+ transfer-encoding:
+ - chunked
+ parsed_body:
+ data:
+ - embedding: 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
+ index: 0
+ object: embedding
+ model: qwen3-embedding:4b
+ object: list
+ usage:
+ prompt_tokens: 22
+ total_tokens: 22
+ status:
+ code: 200
+ message: OK
+- request:
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate, zstd
+ connection:
+ - keep-alive
+ content-length:
+ - '225'
+ content-type:
+ - application/json
+ host:
+ - localhost:11434
+ method: POST
+ parsed_body:
+ encoding_format: base64
+ input:
+ - machine learning neural network deep learning model machine learning neural network deep learning model machine learning
+ neural network deep learning model
+ model: qwen3-embedding:4b
+ uri: http://localhost:11434/v1/embeddings
+ response:
+ headers:
+ content-type:
+ - application/json
+ transfer-encoding:
+ - chunked
+ parsed_body:
+ data:
+ - embedding: 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
+ index: 0
+ object: embedding
+ model: qwen3-embedding:4b
+ object: list
+ usage:
+ prompt_tokens: 22
+ total_tokens: 22
+ status:
+ code: 200
+ message: OK
+- request:
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate, zstd
+ connection:
+ - keep-alive
+ content-length:
+ - '225'
+ content-type:
+ - application/json
+ host:
+ - localhost:11434
+ method: POST
+ parsed_body:
+ encoding_format: base64
+ input:
+ - machine learning neural network deep learning model machine learning neural network deep learning model machine learning
+ neural network deep learning model
+ model: qwen3-embedding:4b
+ uri: http://localhost:11434/v1/embeddings
+ response:
+ headers:
+ content-type:
+ - application/json
+ transfer-encoding:
+ - chunked
+ parsed_body:
+ data:
+ - embedding: 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
+ index: 0
+ object: embedding
+ model: qwen3-embedding:4b
+ object: list
+ usage:
+ prompt_tokens: 22
+ total_tokens: 22
+ status:
+ code: 200
+ message: OK
+- request:
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate, zstd
+ connection:
+ - keep-alive
+ content-length:
+ - '225'
+ content-type:
+ - application/json
+ host:
+ - localhost:11434
+ method: POST
+ parsed_body:
+ encoding_format: base64
+ input:
+ - machine learning neural network deep learning model machine learning neural network deep learning model machine learning
+ neural network deep learning model
+ model: qwen3-embedding:4b
+ uri: http://localhost:11434/v1/embeddings
+ response:
+ headers:
+ content-type:
+ - application/json
+ transfer-encoding:
+ - chunked
+ parsed_body:
+ data:
+ - embedding: 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
+ index: 0
+ object: embedding
+ model: qwen3-embedding:4b
+ object: list
+ usage:
+ prompt_tokens: 22
+ total_tokens: 22
+ status:
+ code: 200
+ message: OK
+- request:
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate, zstd
+ connection:
+ - keep-alive
+ content-length:
+ - '225'
+ content-type:
+ - application/json
+ host:
+ - localhost:11434
+ method: POST
+ parsed_body:
+ encoding_format: base64
+ input:
+ - machine learning neural network deep learning model machine learning neural network deep learning model machine learning
+ neural network deep learning model
+ model: qwen3-embedding:4b
+ uri: http://localhost:11434/v1/embeddings
+ response:
+ headers:
+ content-type:
+ - application/json
+ transfer-encoding:
+ - chunked
+ parsed_body:
+ data:
+ - embedding: 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
+ index: 0
+ object: embedding
+ model: qwen3-embedding:4b
+ object: list
+ usage:
+ prompt_tokens: 22
+ total_tokens: 22
+ status:
+ code: 200
+ message: OK
+- request:
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate, zstd
+ connection:
+ - keep-alive
+ content-length:
+ - '225'
+ content-type:
+ - application/json
+ host:
+ - localhost:11434
+ method: POST
+ parsed_body:
+ encoding_format: base64
+ input:
+ - machine learning neural network deep learning model machine learning neural network deep learning model machine learning
+ neural network deep learning model
+ model: qwen3-embedding:4b
+ uri: http://localhost:11434/v1/embeddings
+ response:
+ headers:
+ content-type:
+ - application/json
+ transfer-encoding:
+ - chunked
+ parsed_body:
+ data:
+ - embedding: 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
+ index: 0
+ object: embedding
+ model: qwen3-embedding:4b
+ object: list
+ usage:
+ prompt_tokens: 22
+ total_tokens: 22
+ status:
+ code: 200
+ message: OK
+- request:
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate, zstd
+ connection:
+ - keep-alive
+ content-length:
+ - '225'
+ content-type:
+ - application/json
+ host:
+ - localhost:11434
+ method: POST
+ parsed_body:
+ encoding_format: base64
+ input:
+ - machine learning neural network deep learning model machine learning neural network deep learning model machine learning
+ neural network deep learning model
+ model: qwen3-embedding:4b
+ uri: http://localhost:11434/v1/embeddings
+ response:
+ headers:
+ content-type:
+ - application/json
+ transfer-encoding:
+ - chunked
+ parsed_body:
+ data:
+ - embedding: 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
+ index: 0
+ object: embedding
+ model: qwen3-embedding:4b
+ object: list
+ usage:
+ prompt_tokens: 22
+ total_tokens: 22
+ status:
+ code: 200
+ message: OK
+- request:
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate, zstd
+ connection:
+ - keep-alive
+ content-length:
+ - '114'
+ content-type:
+ - application/json
+ host:
+ - localhost:11434
+ method: POST
+ parsed_body:
+ encoding_format: base64
+ input:
+ - one passing mention of machine learning here
+ model: qwen3-embedding:4b
+ uri: http://localhost:11434/v1/embeddings
+ response:
+ headers:
+ content-type:
+ - application/json
+ transfer-encoding:
+ - chunked
+ parsed_body:
+ data:
+ - embedding: 0UMzuAgjIbrKFKQ8QF3TPL+rjLlps389KkVMPRPa9ry44a08tNxhvbgN+DxPA5c8VNZjO+ZzD71l5T06+4gfPN7zuzxwx7k7zzRlvFrcj7vZugO8NdaeO9LqAbxtJw09jfbSvJrrVDxKb6C8m+pAvRQVP7xa/HA8x6M2O258cr1XCT49OIAmvIDkZjuymfy8YruFu3vjvLtE8t+5SiT8u4/+UjyUboi9hIy9PL1hlTwJxB28s4r+vE22UTuyXuk8oFhpvMG8tLwI45Y7ZUY3PKVBzTxwd+u8ITcYPao5CTziphO6LGzXu7Nbg7xNqIW8yDxOu6KviryD2Im6AJmxuxzbFrxKiOu8D2ftPLDxHb347SY8FMMvvD62OrzsB/88fjfhvPNejTyKM6o8/BGrvDjuirwoaYU8vGACO/JIBDwXIkK8VhyIOwYrczxIDwg9mka2PAc1NTwgdxw8GETGu0DpsrzOIl27r2gRPPVLdbwoTEW8bMr9O1lEITsZ3n88hyukvAt7nrzO/se89qCHPKeCy7stzpu7WWUfvLx1GLyp10c8LCKEvKTxprzkB247Z5gGvOPOXzsdq0o7QST6Oyc8MryiDdk7aAl9vDswpbzbj3Q7RN8hPShMWzz2QsI8oPcavJML5jyD/za8UeW6vOGsjTwrBia8Cw0xvKgktLtZ0cW74GQNvLCdSDwalYi81rsKvcjq5LydNOg8beIlPPQYO7ysKbu8YWk+uzQbnDv0jey65Dz1O1j9tDtG7Ck7Fjmlu6q0bL25KaE7LACzPNUitTvojw+7MQYiuPS1SrxOZw27vCukPBh2bDyBeL48xXx9vL3HczxoVEw8+fapPMm2IzzE8ww8TAukO/FCbTwomd871WK4PMiRqbwXeyI8htPIut0uVrtlY7K7SX/hu/hcrLy9AjW7aVSXvMIgk7uX2R+8EVVaO+tOMrwXZl670MKivCGiKT0iU4E8wM76uu/Vjzraf1m5knTLuVMtrbo+6YA6TX2vO3AwwLzyGSU9yumnu+8f7rp+HFw7G4mKvPOYF7w8EKq86HTNPJoa+jynT0o7VZVoPOkEILwncyc8eZiCvIy6HbuQmRy8+vl0vI0tEDykNhW8K2w9PO3oRLz9v545PwfjvJOjVbtOUY48K4O5vG1Pe7xzhKQ8hagwPRlvrrsewUA8D5sNvP2DOryTub28BrdbPMkchjt8/gw794PSOtdRi7yogs88supdPAZFyrvp2Bg70jAzOWO0PTyxhwY6UzNau62ApzzZjZo8GCIvvSI6RbyYIVK8qBMfPCqC/Tu6EaS810AjPFxWobwC/0I7QAZnu9rthruZf9m7NxTiOoRU5LwzCgC8AJo9u9UKkbwzcpe8rr7KOvl8XjwLugE6RhgXPDrMFLyyaR67u/Ogu8y4rjyJ8iI8I8DEvOcKojzDpQK91n32u7MX2byeoyA8nQuyO5VdBzzM2u+8P5evu/QtIbwoPRc8kS8KPakxtbtPjpc8n9oZu5Cl/TvCqti8toEJvIZbaj0iD4m7NwvdvJm++7sX3Zg8kGc5vBmJUDyfBJu6SRmYvWAbmrwUckI8abKovHn3Ezgz3Bq9RJOJvG/7nLyGJk279oN2O9iC8ruDJog93sohO2wHATzAgJk8NiYIuxpby7ufdc+7jPuiPBz1nTx7pUW8wGVYvRm+27rJnS68kQv2vCxrDr1KRIC7tvWcvXU2V7wnHqa8PQd5vNluXzvC7II8BtHOPEQGEj0MfZK5A2+xPJm3TzzTbhS9k2KYvBHeiDzZp4q812qPPIJPLD3IYFM8cnPgPDP8ITtGAqA7krnYPI5ah7vJdAa9CldNuktsFz1dWLY6q90xvN2Ed7o9u6q8ZQ5SvDOVEjzaJCa6WJfkOh2Wsjvjtdg6gXKAvJhfvzzLPpk78qtVvN/dgLvdWYS7BGpPPIGu3rzPmO275jOAvIp62Dx9SjQ7wJiHPIidf7ysdIq873tJPBo1Qbv3xg69TI6Vu8FIxzzWAjS8jzKlOwXoMLwu9P07wVpIPK94pLzOOng8ki65vMCADjttj328rg8euywrTrtIvEY83d0BOhlqUDzJKsG7YSaIvGs0O70jVks85lwJPFTKXTzUsy88IHkCvUuS7jyb1Ky8haL0vHlZ/LwpoLc7kNu3u/YQ27sHzV49iIq6PBKzubwZj8w8957SPCqqtjzfCQm9+QTjumjB3rukPgc8DB8jvQRMhzyqunY84ZThOdagcDzF+fq7/apqvFykejzIc588FgGguzzYQTzaMYO8f36pvFXjwjzidxw9TxqQPAA7wzw8vZq76SabPEPhtbyBmrG833v8uibXPr312rw8B+jvumGOezxs8hY9EEBkvVIwCDyYPKs7uY4evKRw6jx1stK8x18DO1A91rv7hG48Cj0HOw5C5Lz3G0w9UBh9PDyvGjyuC887kmAsPdOA372lw4K8oug0vBITF72xbGK8mz4DvULZAL0C8Hy8a+rPPPlwSTzOgpm8I1qQu/rCnzvCPnA8xmZjvJpODDx4m/E7XagruyvxUbhATNy6hR7dujmJMzgC+iM9UlPkO5/N67q0p7i6lhO3u2K49zylo2K8/ySOvNWYATz648i6a6wxOmy2drwgNGa8iOedOpI0WzwzmWC7UHqgvGNwpzyzxIM8j+f8uLQT9jzoCpO7N+O1vDlyFDql45A8xzgVPH2bD7w+Lu88voqbPDEqJbveyGu7utwtvXWkoTq0LcE8aEcCPD8f9LvOrAW8z/5ivAHV+rwNhEy8kDgOPNct7bsNLSK9QKs1PZBLVDx5Czi9hoc8vRW5ZbvZ0n28Us74PAAER7vTNdg6AYjcvNfDJjmJvIM7h9EUPLv5PzrIu4C83eUFvLduZzzroY08kW8XvN1kGD3Wys28fzacPKSH37sg11W9hFQROtxVAz0p3Me7JTsQu/eeurykQNK8yybxPMu6Wzw+6Ho8W/MFPe9HhjrSabS87MGTuuoWfbxPhH+8i2PxPCcj2jsZz6y8stv4PB5IM7w4H1U8TMrIOqL2Bz0Yqi07LSRDPQjWhjvHNLk7KLRyu+9rzbzV9xe94QJeuiccKDyD6ew83mPtOXHNwrx75dA8nABDPJ76Lrzty5m79ODRu3oKubzEAfq8D1BZPKX407wB2Ak8q425vBZoV73HtX+7UzcRvCx/2joWoAk8TE4NvE1TSLyShsi621I7PEIRR7u6yxc87YTmPHBGNz0e8QO9g2eePPH+9Lx/PYg7FlGEu79rUTw8yJA8mjwrPCfqx7v8hhM7pY02vCXTCjxzbgK8Q6DGukHudL3dH0681AWaPMhADb0ikk86jQnZOhddabxpSFG6ZHUevCgR9rz+/Qg9AP46vCb4DDqgkwK85RGAvfjh/jsmHTg8id5vvEyDqDkwuWE80ddKO5D/rLx07Ok8MwnsvD6/ZjuxT2Q5COaDvKnD3DwXU1u8Ra0JPQ4n7bz62ME8XNoXPBsLCLyu5XE8rrjgPBdSALnBVdg7OGYlvMvfAjyU6I285stJPPr1wjv7ixa89RWPvb4Dxjl4vjw9bOICPG+hi7zuJoU8we1OPJwnMjzDSu+7pmn/vCAajjwTGnk86sjGPL3Smbz16X67tkGXO0CyBDyHe9S8shvYO1q5Kzvd2Qe9m8YKvDG9WDw1paQ8ZPkRPJQ3RzwfqDc87dAmPFQ+mjxsaAc87a49O1w6nzzh4eS87QZPvZ6YIzyL9IE6AjcFOzC8m7umJfG8lg0mPdSJa7sdQDG57adGO4avG7pls8e85toXveEdOb3UV5e8FqXgvPNRlzyBIC+8OuYUvLzhBLvZHw29Ulzhu1KpKrwDuk+9Wj4QvL9b/7qUjhk8uFTtPMqVtLyp0ka8tY8IPYsS8DwTx4m8m4xau8uA2Dw6KmY7T3Hou6ZewbtVD588HrbPPDxmjDyFeIM8j1ULveka9TsAhyo9+F+NO7h6nDzzod682wKwvL3y7zvefJm8ih+gvPV/6jyi7Uu8W0F5PBqnqzga7G48huOjPBSA7jvkoBo9Kr5LuzqeFLxMHYq8NGP6u/2rk7zckNC8rUppPLRIqzwZ6V+85A3luybnMzy5TW47y7SsPCMJZbyMSPe8JBzeus/TsLxHlxC98eMhveT38Dzk1Io786KivAs237kaJ4k7pBnpPK9Zubvmx6u8xx4bvbe7+rywk2W6mj6ZvIw0ET0tnL88Rg4Pvc0EsrtLrzA7EyW4u1i8E7kdK4c8hzsmvTczR70Ds8o7uVQju3PPpLvTbs48rlc2PFyiujzmh/k62NZVO0pbdTyZaXQ8ABoVPMBjV7wjBoG9vGeIPKfgk7wqmEm8VP84PaW48LwGU8U8VjEnPSPOEDzKptM8lUIlPBInMbyUISy8CmR2PNB6+Tzp6RW8EwpUvKbvHzwBy+48ElXqO+6bdbwYvns8cJCnvE00YrwBBMY8S4XSvAOqtbwmfDG7gxFeOqirSTy/kqs8PJ5OvITV+7ygB/w74JMmO98LNjzu87C7uzgkuzdcJrqhIKQ9DRGoPG3KAD2N/cY8bPb5OyHMuDzpzYG8jQAnO9Vjl7s/EdE8XCuwvIwwsLwtM6i8yg9RPJGqqLpIOjY8excnvIcAxrxqz4a8fLwCuwZ0LTwmbI07GhzqO3h+qjz/wcU7owsyPGliHL3oKBq8HfQZPCjlcDx4HyQ545DpPMhJBD3EOcO7pOSOPOjdgjwNZnE7D+F0OmFTjjndN9e8UGFQPMPYgbys/4c8D9mGPCzSmby3LEW8kP4dvW3u7DwJaz6893PAPIzBkzywc7y8SxQMPFfYGz1mNI08ziagvNcas7xsEDC7OLANvEWCE73gIfa8z/1RPC4j5jzlSf28uo+Ouy8bijx1xyg9L3mbu7uItbvrHRk9iVSyu9xh6LpQ19C8J1R/PJFJJrytHOa8/+iiOwbuU70Fob28YuQVvePf5Dto6DC8Q+vIO4jm7ToQSEc8/qhFPCp4fzthrIU8w1ALvQkOE7yfJwW8n/6fvFa+Vj0z3Jc8jBZMO3cmiTyhWZ08Z3jhO9k5xTytTAK8yL6bvNryvryyUFs8wQduuwInDr3LUbK8Eqanu4m3Gz119PQ89AEPvPy8UjuLdq+7UAY5PFerzTwKOOu7A8CAO2yIkrukmdm8XquVvElhtLy/SnW82RpBvK8inbyy+l+8Jqc5PIOaLr2XhNM8m2B2PSygtrtwocm8t028PE4uCTwf0Cw8V+xMPAp5f73Odnw8s2Wcu2Th4jxTGJW8K4X9PF/PlLyxdyO7fJ+qvMLoxbvdoAA9lildOlyKSz3ZIVE7y6c2vIcQO7wRAAw9kB9HPS8/Fb3EFQA9O8sSvRd/Pz0v16o5OzjiO58A5rmcwAW7/c4gOyKtoDnun+08+XfCvGGjAbw633u7n+2/PFNK6ztO1T+7qbHcvCKoi7xUgVC8b31JPJvc4Dyxh2a9xyD+PLE5QjwrSJy7ogCWPK7HyDyX8gi8NJa6ut0OYjzJFwW89GyTvOa77rsmBRa8qvqWPLlRnTwr/5C8RX4IO75ZYbpQdJe8UaajvKsWqzuGwAY8dbQwvI/njryacGU8VEMTvFzi1DxLprc8eQR/PNcLwDt/1cM8XXcMPAMXzzytHlY88LqXPJuwt7oV/ik8+LYmOZaMkjsyksa6knuAus98KrvLHfq6hvQMvY5F/Txi6TW9iquYPEi7Qb34KdG7rxWGOS18IL0y6I+852UhO1FPb7zAI/y59LwovRpkKDya9h28yYsxvCFlmryZzsy7jtilPOqV7jxtoVo7MwcRu8QHtzseaBk8WkFFu/ujy7q2sEA9B6TvurXsH7vtN1M8U/7FvFT/P70ZNwQ70FSuvOkSXDwprd86AJ2CvKy2Grw8IR45UtSzO5mhfzx3xVU9gIetunNrjzusA+877yoTPb3HDb3iFrO71VAIvDMdLbvUUVm8IwODvCJ3xTxyOBE9OJHmvAvUuTo0zo+8i1Dfu7G4vTzM7lw8So/xvPMzRzz4auK80u0NPRBOo7wdjja7GsMiPUF9iDxR3MI8nBKjPOs4Kj3b3328TgPZu77TSrwFtIi8x5q/vE0rarxMYXk8fWI0PE7+RjsFnom73ttsu22byDtD1AQ6zsA8O4HPTDzpzBe7WrPHOvmB27sRcpE8yCN1u5XTED3LwgW8im5tPKE4YLvDeIA8cCegvGzSkjxPvRm8fH9ePCZi27vmd9i8IgxJvH0NJr2//z48GIsOuyyQDryZboy7UYr2PIL4OLz7J8k8YoXKvLjNxzydjo66PoSZPB54uTo76ZW8FGZyPFgPcjzKt+a7jOoxOm0BwLzLzQ09d3AxvBan0zzPE828+QbAPGI4frz6fiw8bJVPvfKJqLzvhga9BYwsvF92C71rahi8stZxPCVkD7yDp4Y8fiSAOt7CS7zhG868SNfhPJ/0zruya8Q6QoIFvOHH4jwIo4680HPFu0XTaDuv7pS8XB0QPc2DE73nW6I88fjgu5esrDtqRgW9FZFwvMg1tLuZLt+8FfKIupagMLxhXaU8VaVMPMleQ7yUbpE7dukpPVo1bTzaOQM9ry8fvCUmprvjMwY9w1WEPLWFmbxeXvi75PYCO7oaCjzz6JI9exCDPKeyXbwPD9I7qb89PCz8QL0hI8y8jXZ6O7T5jrxkFJG8ZujJPJDVWzxRUTs8/nIZvH0CNDzA3+Q87rvavOI0ubsWx1+8blw0vKFvXTyV7ba7N0BYvAKlCrw/9cA8zJ66OliH1DyxPR09YvSjuupbKT2ia2q8f9ULujLSEr2Uo767gcqhOociQbxAdWQ7FCvXuURL9Dzoi6O7LNqTun4Qz7wog2m8XATYPDzXtLvNpuq8+KX7O7gFXDzi5iW9zu+avAaTiLrOyLy7XCISPIvLHLyEQ/c7P2uAvMTII7xxwOg8/k6LPIWCxzul+5W8x0ydvM8hRz3SnjM8RrI9vGA8BT0AMds7sXQCvf8Brjm5oSY7vZ6Nu430GryWKRq8JE6TvJq0A737joo8BbjnuU74BLwE+4E7iMLUPGf+azu+hwa7qlrnu9SyerzecM27ETALvZO6jLyCmiI9nhq3uuB8mTywCEo7ZSfeO+jMvjwWfRO775hoPEkbR7ytZJa8d8i6vIs53rwaKIw7yoI8PPe7GLqAIaC8H/JaPHaMODyc8vy8jHM+PCZAQjw10Ru8oQaUPG8CqTvOfCK8XjiAvKcgwbwwDom8wawEPB8E8Dy8zs06/ab5O+g3M7zO1AK9oO2EO1RawrvihZq7rnR0uSAU9Ts4rz08jhAEPSuZD7ycSGM80f0rPVSbxzv9wJg7jw2UPMho9rvoBY68TueHvGez2juYDIc8f2i3O/t/4zz7hiY9x/JNvboBGj3rSkI7LM4/PaHpgzwSn5a8hfAOO2/Enjywzag7rN/zvIIZuDyxhzM6uIFTvDbImTzKbtY7JAmrOt69I7yRhqw8xQATPGeuRbuR8Kg88KFyPEIPB71BkBa93vyFusOrWT3/ECg7LhZivEI5VLsBVkY76JdaOt9wBzvsp6O8VySPurj3ejy03GK86R7CvIfaNbyX9rc7IbKCu63vbTw++mg89qL7u/TmJjxnKom8CdaNPM2xCr3+U0O8O1Q+PCk0+TtW2sE7SXqgPCmZoDwZjWk7VSb8uyyDPL3IdFA9tJP6OV3Pc7w8feS7g1QJPL9Z2LzmAlU8hamKPPxRr7y13jA7h4CPPD6LO7rZMJU8OU4TPJliRryIPee8+CmDO/CM0TytXkQ8n14UPXSfa7y68aY8PYIbPWrL/rwejAS9y6AIvDcfebyndpe8SXUBvXaIqDtDXZm8h0AsPJBgsbu14+U7Z0WRPJG0nrydyOY79P6tvEwCGTsMMbM8GzmZPAVlIzuU8hi9/bOau7jXXr0QN7q7BtD4O4GlAT26XE084THevCTtlTzqsBs8E2A1PA8QDr1kCny85pk4uq+VGzyW5Uy9tt6xO5i9MTvICVI8kmfGuZ7igDwH4vC7n5qcPHgmCjugyhO8goRRvH240zzhk887G0JfPDxFVzzC6Xq7EBcCPP2VHTyNVhI69AP+u/ERfbuOSp68l8WQvB1OfbwOneU7Lqy3PHvETrymkNs88Ke6PKE7OzvGQZA8IAxaPM7iSLzNKgS8gi4APEAVPry7KgY8jf+Tu9nZsDvTwes8XFYMvK96FTtZyAW8pcoNO0TBrrsjzJO7TVEdvIYw0LsKBHk8oWQHvPGSRTs/u/I7zogGvHwWqzu25T48nXnQvBah0zyFBf07vyc5OreUMbydIiA7KLQ+PDo+Gz19mZW8WSo/vC2wUrzeeWY7/ORtO0DULjyxh+K7UunnvKfHAL2G6/07+XEjPfzatzxvq1W7tGxtvIP0izzNafs7I8eYu+XTzzwp2y88obj3vKC2Aby4JpW8/9RfugL3tTvLKYM8F6+zOqqYCLrtwI28Qy6GvOBDozon/xM78BCKPEC5mDlCmGE606STvBhYabv38hg9auY4PPIpOrxPWbu8QpZqvIXQ4TwXPjO8LjV5vFCnxDzGPAO9Q8XFvO8TPryNmg28E24mPb/rKbzXUl482jxiPFPe27wuY5w5xv7TPPdpqry8/QA9gLCgPEaSSjyafji9S0DmvAzhvTus6zU9GlluusTUxLxh2pg8YWmaO74qQT3Xh4s8ej/Ruh/507wQ9lO9naONvN9mxbsA6lM9eTFpvH6LX7xm4ss707ETvK469Dxwgw69T9bQu0Ug7LvXYGY8PInAPApR5zwJjuA861LQPCM82ToM8s08BtpfPGnz4ru+6Z68iU43vCr8mLt2wpE70QT5OxYyXLyUOKW8X3YdPSiDlTvkPPi8e7POO0yGnrzOyIu8HOSwu2rYCzwjgIi8TTuavHgiEz0kvag8E6JkvGW1UDuI+AK9mH5/PJqOjDsecUi8sg04PMBAerxE+OG8kxxiPHewu7s89jK8wPeWu8nxvzuwoqO8uWGoO/fzDDyDr8g6UTwWvFdd5Dwt8Tg8YP2/u/4xs7qYD787uEzkO8D2pbrwDV28mZ0CvI3XEryrhXe8U+ejPDkLPbysUQS9+YhFvCeQAL1G4Ao8a91EO1GfwDxDaQq8yDOevKGlvjxjK8M52QMdvX5wSjwiv6q5VxrFO4Ej37txc10713QbPO0qijv0c688Mcq/u0w4WjwBY6S7AA6uuzUCCj0rt6k8abAou1hTtDxGtgi6tcYEu0/NtrxaL2u8x9AyvT8t5Ltv42E8f8EsvC5duDwE27y8ivWmvFeXgTynVxa7Jr9GvGxxhjynsfU8yCcFvNA7OTqwDIY7C7qiuzKN67tEnpe8hscFu9Z6t7rb1dy7gUi7O+LlwjyypZA8MuX6vOlX1LxIJ8G86dRKvN9uFTu70Zy8pPSmuuflB7wsPps8qm67vGW0Fz1+JB47OM4MPRoSnDvLz9i856yaO1IMljzNhn28fhLsOnWia7xll6C88OsRvWkB9Ls+9jw8eTroO5Y9Az0hwwm7yHGpPFF/Ajx6Hb68Gcw4O7MtgjyDy8473Xg8PGBhWjxX7dy7ImmmPLKUcLxSpQ49/1zGu6Z147y1opQ8T/CiPILhzTyqlf08iWZAvD1rA7xYlAI7R32FvGPAhTz+Wbg8YnoiPEf/jLzWWBS79xJuPPMEeLzbwdQ8fS9JPBLrPbxZPca6PTmLvK0CNL04jo48cdWPvGMkELwev5U8ZDNLO6kVSbxzuZA81RFGvTOLLzyKYWW99OItvX6dmzz0IQC8HdAaPKpfsbxoO/o8M4aJvKYFPzyy7xk74sDMu90pzzxr+au8RAGBuyr/ErwSpxK7WuXIOzCiKDxxcAi962dMvEkGn7zK86W8S4suu2EBjzwtvYK8rZkNu22P/bvOfFY8oiZavGDQB73lLaS6NbpNPCNfdjnDFMO8Ve8surl28rwuMCi9CW/tPJ2xkbs1dSG7VSQWPFDzo7sOYCK7OcHWO1UQrjtolca7LhDLvNY7gjzIfY88EmqQPEJewDzy3ic9asMePanNtTv8QSA99ZoUPHEQ3bw8Odm7NEnFvB0Hijwe6K48w02fPESdDLwrj5A7RpiVu57IiLs0mzO8gDSwPLc81LweuLW7oNj2vPJcxjuNnqQ8j6scOaTznbz15qq8PS4JvRqckTwIpQ8887lfPC2QKzwXByE8RzaNPD4tWj0KKsg8LoRuPNVHnDw2/jG8G+jIvGkKnzziEXM92PBjPLY7pDyPMLo8jEIPvFMyLb1qoyc8PGlPPMhvZryKqyG8w2fZPOujirzolI48Q7GBPOMGrzw/gy083YnwPIq4/LvJSYc6ZTW9uc68jTtnd/s7S3Q6PHLcD70MFnW8eG5ou1YAtzzhn9y8kxc7POMJnrwdK5G8ETFAvDG09rtUTVU7zMr6O8kGFDxr6zY8TGo0PE1IhjsZqL47V1EnvdHZx7zL0Vu8b13APJfqwbvpgk+9rS2hPFAF17ymyW48H8CXvCwfk7wiCq28yb9vPOxTpDoSs/m6AvBXPEJNwrwTKD+9W0oyPJ4SWLxu3hi9cPY7PMGxTjtN5Zm8oMNOPBefFrsOmqa6p2zKPPDQljwEceu7f7tfvMRY2DtKiEs8eDK5vBFLCbiR1ho9vCSEvGeQF7zJ0gw8OcVXu4KvyTw84gI8Roy2vJjoAL37gTy8AwWVvHb/j7wnDRK9YYSGO/FYUr1TmL480gZHvEMlsrzVhTS7Qu1gvKnzarwao7S8mZZmu7mZ7Dx7YBe7wHaOumPjHDzFpKe6+9sKuzo4IDwThwy8MzuKvIkuDbybEV27rdOFPL0Y0LvWOrq7i1W7PIX+a7vHyma8nYvhvLyA2DvtScM66wbGvMZ2zLouNqa8djpDPGfMpDzbsYW8VexoPJN0irzGUDU8FN62PHSvKzzEsjw6f8UOvPS9Jjw8L1Q6cPBrOvBUA7yWogy7BTdbu4Fk6zqgSAa9smMvPKoLBLw4ccW8SLSNvBOP2DwAo+E71PmvPLc2yTv/vzy8b/2evDCmLrzhFdK8MMJcuyxxtbwN9iI9Fg8mvNDzgby3gji9e6eAvFJr+Dsurb+8HvRqPKswobux5GK8P0+9uwP5c7vXbBi9XehZvK4hzbwuBia9UCfmOGht+LwUNlC8B7wnPSHP5zx7N9w6fLKpPCvejbws39m8av1LPC8yGLzsJPA5KvV0vJ85gLg3eUa7+448PYjG9jswpUk7RM7LumW+TryK66K8m82IO/ofJzw0Brc8WKAKvJzrTrw/wuW7WbqbPHwCFD2ZUsY85hrmu0dmljzEZyc9SuaGO+2ptLzc+cG7/2qqvJxUmbxgmzK8bKaRukLXCjuH8bi8+gJuvJ6Ysjwh6QG9VFXAPIffIDuTLIy8dVwnPC089Lz3P+G804y+O20w1jyVtG8862XGO6SUqDwEB6s7eobruyMpYTwSAd08R1VQPBeYsLyJiSK7fA6SPLryab13FLo81SlTOzl6wLwBpjk7bV3mOxQhijxCkgS9KjWyvMe0f7pheiW8BT9IvCU5Db1xAzE7YFcTvbW8MjrM88A6QA6Nuq9kJTxXQA+8+n0NvSZA9bsDYa+6wlyoOpuf7LskQnk70vCYPAPxD703N/w7h7NavHCyjTtuFmM7R3KSPMMG9jwAjds86GcSPPVWIL1JCvM8Br/0u8wyozs4Q4g5Nr6fOxpBhrzYIH28fKm+OxsA8bv8hxy8klSpvFkOOzs/XQK8iFolPHl+pbxFghW9NslrPI7Xh7zojjO8LAQPOyy7/bwGmE49FZ1MvTabITwyp965MZsFPEUNXrz3EwC89J4dPbSarjsC/aY7q4ZBO4Dqcby1VC48VdkRPFAz4ztAO4g5mawZPK/ZKbrQ/PC7nPsKuJ6FSL3T/RI9MUmIPGpD57xAh7Y5nT4NPM3D2zuVVKw6CcPiu8dzHryumEi8+4hgOcgjpDz1jg+8AFxOO/vnjLo2z4i8H8fqvF83ETwvwKE5iHuSuyrB2jyNCZA8qVlhOp/GSTzj4i08G7uCvH1XDDxptqe7ff2zO+bw2jqfeoe85PuZPO+VHryk6S+8BDBEvE3FZjuWc5+7WGkBPWEIgrtl7pg79jbrPM+J5Tt4+N+72iXIPFLz0rx2ry+7qYfLPFfzRDunLmS8tujgPP6T6bzQXyO8vETmu4uoDjzwGSo820I3PLt0gjy9vG486yeLPOG+xjzjrNO8XOO2vBK22jztcF+8qK7lvMAZ9DxysGU7EAI+PIYRxzunoCQ7/C9TPEq0Iz1Kiom85K9tvJ3TDrzwdlA8PueBvLMEvLvZhxE7g30KvDTgnLwyr4U8cVR+OswbkzyPWKE8sUU7vZb/tLza45+7Q/lKvH/Pc7xdSdC8CgUcvKTY3Dz/RxG8xhHPvOkVgLzwhJW8z8mLvINLvjyzUuQ6Q7CPvL+QgTw21dG7w3yEOFdivjzfeG285xXlO8u+tTtVRkS8fhwMvBcqoLkQ25u7rkCPOnTP4TvdsWa8n2dMvO5f+jsBfcs8NkfSPLYsW7tghmy8G8JUvN0MMjx7pcm8Weaju+irjbmzBpM6Og22O5cZjzytNMY7ALsOvWNjdjukTG+6ZLHWupbmDrzPPri8+P3RvHvIvjuwWw+8Nnx1PVzebrzZvDs6qar0OvtpajtqI1i6jOzeO7iAAz238Bo9SA66unbjhTxEkzo8keayvHCVqDyPNsK8hMGzvGHyGj2Xn9g86N+KPAIPZrxJlF09D5VKPcCijLtYyQA8afGfOixSprzpIyY8A1XAPBJzarwAwpy8/ewgvMXpIzvSbzI8y+wJvaTJPzxgChw8x660OXBfg7t74Dw7NTqmvBR9t7yE6QG5vdK/ukS+nTxDp4w81cJgvO8CYDz/+508y4oDvNJaGDzNIlg8K8J7vH3FRb2sze68E4ghvN5iVLutEJm809paPF06pzuZb2g8usiBOsYCjzxhCZo8QIc3PFPR/btCExS7HxO3OiwZfDyvH7i8aC+EO73r3Lk8cjE7a62JPKAGqLs6p9I7y4wRPDVJ3Tzk5IY8Yq8jPBR7yjy9CNY65vS/u1SQD7yAoq06iaXzu9mKzzvvJAy9P+4GPOHZnzzQYTc8cqf/PP+wELx/VgI8bEV3PE+HAjxnUt+8WhqsvEE3STz4xOc7XJ81vC1Wczzwlcq7sETzvDBdeTzgPVO8VZXRuwSPSzzQLDa8bbkAPbzq+Lsf6VY6czzOO5mtqTzI+z48d2HMu/dTd7x3C668MgryPA1Qq7wFJ0Q9xhCSPO1F/bsIZU+8DROQvIgpoLzrwL88T8cWPRPWMLxrLxY7zXx+u06cNbww+YG8mIYVvBAnlDu9elq8ofvOPO03qLwNxje7cuoJPYg2urv5Cx09VCE2vM/vzjsbm6k84WoXvfNw+zukaIU72CzxOiJFGzxjASu8Xk1ZO0axsbsPg9A8gCIaO84iAjxjJXI7p32BvNVVZbyosVU8wiyMvBLrRrzaCgm8zc3LvAT4mDtqbYa8FT4uvFuJ3LrPdrQ8wENjOhqXQbxnTOQ7oua1PCHcljzywrG8Ob+evCnMuTw61j87SlgtPL+IEryfdp27ToTfPHEDzTxqkW88RVs/O0RcNrs+ClW6p4yJOg==
+ index: 0
+ object: embedding
+ model: qwen3-embedding:4b
+ object: list
+ usage:
+ prompt_tokens: 8
+ total_tokens: 8
+ status:
+ code: 200
+ message: OK
+version: 1
diff --git a/tests/cassettes/test_rebuild/test_rebuild_batch_size_flush.yaml b/tests/cassettes/test_rebuild/test_rebuild_batch_size_flush.yaml
new file mode 100644
index 00000000..04c1d921
--- /dev/null
+++ b/tests/cassettes/test_rebuild/test_rebuild_batch_size_flush.yaml
@@ -0,0 +1,242 @@
+interactions:
+- request:
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate, zstd
+ connection:
+ - keep-alive
+ content-length:
+ - '87'
+ content-type:
+ - application/json
+ host:
+ - localhost:11434
+ method: POST
+ parsed_body:
+ encoding_format: base64
+ input:
+ - batch flush doc 0
+ model: qwen3-embedding:4b
+ uri: http://localhost:11434/v1/embeddings
+ response:
+ headers:
+ content-type:
+ - application/json
+ transfer-encoding:
+ - chunked
+ parsed_body:
+ data:
+ - embedding: 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
+ index: 0
+ object: embedding
+ model: qwen3-embedding:4b
+ object: list
+ usage:
+ prompt_tokens: 6
+ total_tokens: 6
+ status:
+ code: 200
+ message: OK
+- request:
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate, zstd
+ connection:
+ - keep-alive
+ content-length:
+ - '87'
+ content-type:
+ - application/json
+ host:
+ - localhost:11434
+ method: POST
+ parsed_body:
+ encoding_format: base64
+ input:
+ - batch flush doc 1
+ model: qwen3-embedding:4b
+ uri: http://localhost:11434/v1/embeddings
+ response:
+ headers:
+ content-type:
+ - application/json
+ transfer-encoding:
+ - chunked
+ parsed_body:
+ data:
+ - embedding: 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
+ index: 0
+ object: embedding
+ model: qwen3-embedding:4b
+ object: list
+ usage:
+ prompt_tokens: 6
+ total_tokens: 6
+ status:
+ code: 200
+ message: OK
+- request:
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate, zstd
+ connection:
+ - keep-alive
+ content-length:
+ - '87'
+ content-type:
+ - application/json
+ host:
+ - localhost:11434
+ method: POST
+ parsed_body:
+ encoding_format: base64
+ input:
+ - batch flush doc 2
+ model: qwen3-embedding:4b
+ uri: http://localhost:11434/v1/embeddings
+ response:
+ headers:
+ content-type:
+ - application/json
+ transfer-encoding:
+ - chunked
+ parsed_body:
+ data:
+ - embedding: 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
+ index: 0
+ object: embedding
+ model: qwen3-embedding:4b
+ object: list
+ usage:
+ prompt_tokens: 6
+ total_tokens: 6
+ status:
+ code: 200
+ message: OK
+- request:
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate, zstd
+ connection:
+ - keep-alive
+ content-length:
+ - '87'
+ content-type:
+ - application/json
+ host:
+ - localhost:11434
+ method: POST
+ parsed_body:
+ encoding_format: base64
+ input:
+ - batch flush doc 0
+ model: qwen3-embedding:4b
+ uri: http://localhost:11434/v1/embeddings
+ response:
+ headers:
+ content-type:
+ - application/json
+ transfer-encoding:
+ - chunked
+ parsed_body:
+ data:
+ - embedding: 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
+ index: 0
+ object: embedding
+ model: qwen3-embedding:4b
+ object: list
+ usage:
+ prompt_tokens: 6
+ total_tokens: 6
+ status:
+ code: 200
+ message: OK
+- request:
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate, zstd
+ connection:
+ - keep-alive
+ content-length:
+ - '87'
+ content-type:
+ - application/json
+ host:
+ - localhost:11434
+ method: POST
+ parsed_body:
+ encoding_format: base64
+ input:
+ - batch flush doc 1
+ model: qwen3-embedding:4b
+ uri: http://localhost:11434/v1/embeddings
+ response:
+ headers:
+ content-type:
+ - application/json
+ transfer-encoding:
+ - chunked
+ parsed_body:
+ data:
+ - embedding: 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
+ index: 0
+ object: embedding
+ model: qwen3-embedding:4b
+ object: list
+ usage:
+ prompt_tokens: 6
+ total_tokens: 6
+ status:
+ code: 200
+ message: OK
+- request:
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate, zstd
+ connection:
+ - keep-alive
+ content-length:
+ - '87'
+ content-type:
+ - application/json
+ host:
+ - localhost:11434
+ method: POST
+ parsed_body:
+ encoding_format: base64
+ input:
+ - batch flush doc 2
+ model: qwen3-embedding:4b
+ uri: http://localhost:11434/v1/embeddings
+ response:
+ headers:
+ content-type:
+ - application/json
+ transfer-encoding:
+ - chunked
+ parsed_body:
+ data:
+ - embedding: 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
+ index: 0
+ object: embedding
+ model: qwen3-embedding:4b
+ object: list
+ usage:
+ prompt_tokens: 6
+ total_tokens: 6
+ status:
+ code: 200
+ message: OK
+version: 1
diff --git a/tests/cassettes/test_rebuild/test_rebuild_full_source_failure_is_logged_and_skipped.yaml b/tests/cassettes/test_rebuild/test_rebuild_full_source_failure_is_logged_and_skipped.yaml
new file mode 100644
index 00000000..6ff93083
--- /dev/null
+++ b/tests/cassettes/test_rebuild/test_rebuild_full_source_failure_is_logged_and_skipped.yaml
@@ -0,0 +1,42 @@
+interactions:
+- request:
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate, zstd
+ connection:
+ - keep-alive
+ content-length:
+ - '111'
+ content-type:
+ - application/json
+ host:
+ - localhost:11434
+ method: POST
+ parsed_body:
+ encoding_format: base64
+ input:
+ - Content that will vanish by rebuild time.
+ model: qwen3-embedding:4b
+ uri: http://localhost:11434/v1/embeddings
+ response:
+ headers:
+ content-type:
+ - application/json
+ transfer-encoding:
+ - chunked
+ parsed_body:
+ data:
+ - embedding: 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
+ index: 0
+ object: embedding
+ model: qwen3-embedding:4b
+ object: list
+ usage:
+ prompt_tokens: 9
+ total_tokens: 9
+ status:
+ code: 200
+ message: OK
+version: 1
diff --git a/tests/cassettes/test_rebuild/test_rebuild_full_with_accessible_source.yaml b/tests/cassettes/test_rebuild/test_rebuild_full_with_accessible_source.yaml
new file mode 100644
index 00000000..c67b4357
--- /dev/null
+++ b/tests/cassettes/test_rebuild/test_rebuild_full_with_accessible_source.yaml
@@ -0,0 +1,82 @@
+interactions:
+- request:
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate, zstd
+ connection:
+ - keep-alive
+ content-length:
+ - '115'
+ content-type:
+ - application/json
+ host:
+ - localhost:11434
+ method: POST
+ parsed_body:
+ encoding_format: base64
+ input:
+ - Fresh content from an accessible file source.
+ model: qwen3-embedding:4b
+ uri: http://localhost:11434/v1/embeddings
+ response:
+ headers:
+ content-type:
+ - application/json
+ transfer-encoding:
+ - chunked
+ parsed_body:
+ data:
+ - embedding: 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
+ index: 0
+ object: embedding
+ model: qwen3-embedding:4b
+ object: list
+ usage:
+ prompt_tokens: 9
+ total_tokens: 9
+ status:
+ code: 200
+ message: OK
+- request:
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate, zstd
+ connection:
+ - keep-alive
+ content-length:
+ - '115'
+ content-type:
+ - application/json
+ host:
+ - localhost:11434
+ method: POST
+ parsed_body:
+ encoding_format: base64
+ input:
+ - Fresh content from an accessible file source.
+ model: qwen3-embedding:4b
+ uri: http://localhost:11434/v1/embeddings
+ response:
+ headers:
+ content-type:
+ - application/json
+ transfer-encoding:
+ - chunked
+ parsed_body:
+ data:
+ - embedding: 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
+ index: 0
+ object: embedding
+ model: qwen3-embedding:4b
+ object: list
+ usage:
+ prompt_tokens: 9
+ total_tokens: 9
+ status:
+ code: 200
+ message: OK
+version: 1
diff --git a/tests/cassettes/test_search/test_fts_search_targets_content_fts_column.yaml b/tests/cassettes/test_search/test_fts_search_targets_content_fts_column.yaml
new file mode 100644
index 00000000..45a29ad6
--- /dev/null
+++ b/tests/cassettes/test_search/test_fts_search_targets_content_fts_column.yaml
@@ -0,0 +1,42 @@
+interactions:
+- request:
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate, zstd
+ connection:
+ - keep-alive
+ content-length:
+ - '74'
+ content-type:
+ - application/json
+ host:
+ - localhost:11434
+ method: POST
+ parsed_body:
+ encoding_format: base64
+ input:
+ - seed
+ model: qwen3-embedding:4b
+ uri: http://localhost:11434/v1/embeddings
+ response:
+ headers:
+ content-type:
+ - application/json
+ transfer-encoding:
+ - chunked
+ parsed_body:
+ data:
+ - embedding: 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
+ index: 0
+ object: embedding
+ model: qwen3-embedding:4b
+ object: list
+ usage:
+ prompt_tokens: 2
+ total_tokens: 2
+ status:
+ code: 200
+ message: OK
+version: 1
diff --git a/tests/cassettes/test_versioning/test_aexit_awaits_all_background_vacuums.yaml b/tests/cassettes/test_versioning/test_aexit_awaits_all_background_vacuums.yaml
new file mode 100644
index 00000000..c0f6b401
--- /dev/null
+++ b/tests/cassettes/test_versioning/test_aexit_awaits_all_background_vacuums.yaml
@@ -0,0 +1,82 @@
+interactions:
+- request:
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate, zstd
+ connection:
+ - keep-alive
+ content-length:
+ - '91'
+ content-type:
+ - application/json
+ host:
+ - localhost:11434
+ method: POST
+ parsed_body:
+ encoding_format: base64
+ input:
+ - triggers first vacuum
+ model: qwen3-embedding:4b
+ uri: http://localhost:11434/v1/embeddings
+ response:
+ headers:
+ content-type:
+ - application/json
+ transfer-encoding:
+ - chunked
+ parsed_body:
+ data:
+ - embedding: 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
+ index: 0
+ object: embedding
+ model: qwen3-embedding:4b
+ object: list
+ usage:
+ prompt_tokens: 5
+ total_tokens: 5
+ status:
+ code: 200
+ message: OK
+- request:
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate, zstd
+ connection:
+ - keep-alive
+ content-length:
+ - '92'
+ content-type:
+ - application/json
+ host:
+ - localhost:11434
+ method: POST
+ parsed_body:
+ encoding_format: base64
+ input:
+ - triggers second vacuum
+ model: qwen3-embedding:4b
+ uri: http://localhost:11434/v1/embeddings
+ response:
+ headers:
+ content-type:
+ - application/json
+ transfer-encoding:
+ - chunked
+ parsed_body:
+ data:
+ - embedding: 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
+ index: 0
+ object: embedding
+ model: qwen3-embedding:4b
+ object: list
+ usage:
+ prompt_tokens: 5
+ total_tokens: 5
+ status:
+ code: 200
+ message: OK
+version: 1
diff --git a/tests/cassettes/test_versioning/test_aexit_awaits_background_vacuum.yaml b/tests/cassettes/test_versioning/test_aexit_awaits_background_vacuum.yaml
new file mode 100644
index 00000000..d9804b82
--- /dev/null
+++ b/tests/cassettes/test_versioning/test_aexit_awaits_background_vacuum.yaml
@@ -0,0 +1,42 @@
+interactions:
+- request:
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate, zstd
+ connection:
+ - keep-alive
+ content-length:
+ - '96'
+ content-type:
+ - application/json
+ host:
+ - localhost:11434
+ method: POST
+ parsed_body:
+ encoding_format: base64
+ input:
+ - triggers background vacuum
+ model: qwen3-embedding:4b
+ uri: http://localhost:11434/v1/embeddings
+ response:
+ headers:
+ content-type:
+ - application/json
+ transfer-encoding:
+ - chunked
+ parsed_body:
+ data:
+ - embedding: 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
+ index: 0
+ object: embedding
+ model: qwen3-embedding:4b
+ object: list
+ usage:
+ prompt_tokens: 5
+ total_tokens: 5
+ status:
+ code: 200
+ message: OK
+version: 1
diff --git a/tests/conftest.py b/tests/conftest.py
index 3c1fb261..152eda38 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -47,13 +47,12 @@ def qa_corpus() -> "Dataset":
@pytest.fixture
-def temp_db_path():
+def temp_db_path(tmp_path):
"""Create a temporary database path for testing.
Note: Tests that need a database should use HaikuRAG with create=True.
"""
- with tempfile.TemporaryDirectory() as temp_dir:
- yield Path(temp_dir) / "test.lancedb"
+ return tmp_path / "test.lancedb"
@pytest.fixture
diff --git a/tests/store/test_document_items.py b/tests/store/test_document_items.py
index c54f31c8..b495353a 100644
--- a/tests/store/test_document_items.py
+++ b/tests/store/test_document_items.py
@@ -1,6 +1,10 @@
import pytest
from haiku.rag.client import HaikuRAG
+from haiku.rag.client.documents import (
+ _store_document_with_chunks,
+ _update_document_with_chunks,
+)
from haiku.rag.store.engine import Store
from haiku.rag.store.models.document_item import (
DocumentItem,
@@ -217,7 +221,7 @@ class TestDocumentItemPopulation:
# Use _store_document_with_chunks directly with empty chunks
# to avoid needing embeddings
- created = await rag._store_document_with_chunks(document, [], docling_doc)
+ created = await _store_document_with_chunks(rag, document, [], docling_doc)
assert created.id is not None
count = await rag.document_item_repository.get_item_count(created.id)
@@ -245,7 +249,7 @@ class TestDocumentItemPopulation:
uri="test://doc",
)
document.set_docling(docling_doc)
- created = await rag._store_document_with_chunks(document, [], docling_doc)
+ created = await _store_document_with_chunks(rag, document, [], docling_doc)
assert created.id is not None
assert await rag.document_item_repository.get_item_count(created.id) == 6
@@ -254,7 +258,7 @@ class TestDocumentItemPopulation:
new_doc.add_text(label=DocItemLabel.PARAGRAPH, text="Only one item now.")
created.set_docling(new_doc)
- await rag._update_document_with_chunks(created, [], new_doc)
+ await _update_document_with_chunks(rag, created, [], new_doc)
assert await rag.document_item_repository.get_item_count(created.id) == 1
async def test_delete_document_cascades_items(self, temp_db_path):
@@ -269,7 +273,7 @@ class TestDocumentItemPopulation:
uri="test://doc",
)
document.set_docling(docling_doc)
- created = await rag._store_document_with_chunks(document, [], docling_doc)
+ created = await _store_document_with_chunks(rag, document, [], docling_doc)
assert created.id is not None
assert await rag.document_item_repository.get_item_count(created.id) == 6
@@ -277,8 +281,9 @@ class TestDocumentItemPopulation:
assert await rag.document_item_repository.get_item_count(created.id) == 0
+@pytest.mark.asyncio
class TestDocumentItemMigration:
- def test_migration_populates_items_for_existing_documents(self, temp_db_path):
+ async def test_migration_populates_items_for_existing_documents(self, temp_db_path):
"""Test that the v0.40.0 migration populates items for pre-existing documents."""
from haiku.rag.store.compression import compress_docling_split
from haiku.rag.store.engine import DocumentRecord
@@ -288,64 +293,59 @@ class TestDocumentItemMigration:
structure, pages = compress_docling_split(json_str)
# Create a database at a pre-migration version with a document
- store = Store(temp_db_path, create=True, skip_migration_check=True)
- store.set_haiku_version("0.39.0")
- doc_record = DocumentRecord(
- id="test-doc-1",
- content="test content",
- uri="test://doc",
- docling_document=structure,
- docling_pages=pages,
- docling_version=docling_doc.version,
- )
- store.documents_table.add([doc_record])
+ async with Store(temp_db_path, create=True, skip_migration_check=True) as store:
+ await store.set_haiku_version("0.39.0")
+ doc_record = DocumentRecord(
+ id="test-doc-1",
+ content="test content",
+ uri="test://doc",
+ docling_document=structure,
+ docling_pages=pages,
+ docling_version=docling_doc.version,
+ )
+ await store.documents_table.add([doc_record])
- # Verify no items exist yet
- assert store.document_items_table.count_rows() == 0
- store.close()
+ # Verify no items exist yet
+ assert await store.document_items_table.count_rows() == 0
# Re-open with skip_migration_check and run migration
- store = Store(temp_db_path, skip_migration_check=True)
- applied = store.migrate()
+ async with Store(temp_db_path, skip_migration_check=True) as store:
+ applied = await store.migrate()
- # Should have applied the v0.40.0 migration
- assert any("document_items" in desc for desc in applied)
+ # Should have applied the v0.40.0 migration
+ assert any("document_items" in desc for desc in applied)
- # Items should now exist
- item_count = store.document_items_table.count_rows(
- filter="document_id = 'test-doc-1'"
- )
- assert item_count == 6
+ # Items should now exist
+ item_count = await store.document_items_table.count_rows(
+ filter="document_id = 'test-doc-1'"
+ )
+ assert item_count == 6
- # Verify item content
- items = (
- store.document_items_table.search()
- .where("document_id = 'test-doc-1'")
- .to_list()
- )
- labels = {row["label"] for row in items}
- assert "section_header" in labels
- assert "paragraph" in labels
- assert "table" in labels
+ # Verify item content
+ items = await (
+ store.document_items_table.query()
+ .where("document_id = 'test-doc-1'")
+ .to_list()
+ )
+ labels = {row["label"] for row in items}
+ assert "section_header" in labels
+ assert "paragraph" in labels
+ assert "table" in labels
- store.close()
-
- def test_migration_skips_documents_without_docling(self, temp_db_path):
+ async def test_migration_skips_documents_without_docling(self, temp_db_path):
"""Test that migration handles documents without docling data."""
from haiku.rag.store.engine import DocumentRecord
- store = Store(temp_db_path, create=True, skip_migration_check=True)
- store.set_haiku_version("0.39.0")
- doc_record = DocumentRecord(
- id="no-docling",
- content="plain text document",
- )
- store.documents_table.add([doc_record])
- store.close()
+ async with Store(temp_db_path, create=True, skip_migration_check=True) as store:
+ await store.set_haiku_version("0.39.0")
+ doc_record = DocumentRecord(
+ id="no-docling",
+ content="plain text document",
+ )
+ await store.documents_table.add([doc_record])
- store = Store(temp_db_path, skip_migration_check=True)
- store.migrate()
+ async with Store(temp_db_path, skip_migration_check=True) as store:
+ await store.migrate()
- # No items should have been created
- assert store.document_items_table.count_rows() == 0
- store.close()
+ # No items should have been created
+ assert await store.document_items_table.count_rows() == 0
diff --git a/tests/store/test_engine.py b/tests/store/test_engine.py
index e4a00256..73737fd4 100644
--- a/tests/store/test_engine.py
+++ b/tests/store/test_engine.py
@@ -5,25 +5,24 @@ from haiku.rag.store.engine import get_database_stats
class TestGetDatabaseStats:
- def test_empty_database_stats(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_empty_database_stats(self, temp_db_path):
"""get_database_stats() on a fresh database reports zero rows and no vector index."""
- store = Store(temp_db_path, create=True)
+ async with Store(temp_db_path, create=True) as store:
+ stats = await get_database_stats(store.db)
- stats = get_database_stats(store.db)
+ for name in ("documents", "chunks", "document_items", "settings"):
+ assert stats[name]["exists"] is True
+ assert stats[name]["num_rows"] >= 0
+ assert stats[name]["total_bytes"] >= 0
+ assert stats[name]["num_versions"] >= 1
- for name in ("documents", "chunks", "document_items", "settings"):
- assert stats[name]["exists"] is True
- assert stats[name]["num_rows"] >= 0
- assert stats[name]["total_bytes"] >= 0
- assert stats[name]["num_versions"] >= 1
+ assert stats["documents"]["num_rows"] == 0
+ assert stats["chunks"]["num_rows"] == 0
+ assert stats["chunks"]["has_vector_index"] is False
- assert stats["documents"]["num_rows"] == 0
- assert stats["chunks"]["num_rows"] == 0
- assert stats["chunks"]["has_vector_index"] is False
-
- store.close()
-
- def test_missing_tables_report_absent(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_missing_tables_report_absent(self, temp_db_path):
"""Tables that don't exist on the connection are reported as absent."""
import lancedb
from lancedb.pydantic import LanceModel
@@ -33,10 +32,10 @@ class TestGetDatabaseStats:
id: str = Field(default="settings")
settings: str = Field(default="{}")
- db = lancedb.connect(temp_db_path)
- db.create_table("settings", schema=SettingsRecord)
+ db = await lancedb.connect_async(temp_db_path)
+ await db.create_table("settings", schema=SettingsRecord)
- stats = get_database_stats(db)
+ stats = await get_database_stats(db)
assert stats["settings"]["exists"] is True
assert stats["documents"] == {"exists": False}
@@ -50,54 +49,57 @@ class TestGetDatabaseStats:
from haiku.rag.store.repositories.chunk import ChunkRepository
from haiku.rag.store.repositories.document import DocumentRepository
- store = Store(temp_db_path, create=True)
- doc_repo = DocumentRepository(store)
- chunk_repo = ChunkRepository(store)
+ async with Store(temp_db_path, create=True) as store:
+ doc_repo = DocumentRepository(store)
+ chunk_repo = ChunkRepository(store)
- doc = await doc_repo.create(Document(content="hello world"))
- assert doc.id is not None
+ doc = await doc_repo.create(Document(content="hello world"))
+ assert doc.id is not None
- await chunk_repo.create(
- Chunk(
- content="hello world",
- document_id=doc.id,
- embedding=[0.0] * store.embedder._vector_dim,
+ await chunk_repo.create(
+ Chunk(
+ content="hello world",
+ document_id=doc.id,
+ embedding=[0.0] * store.embedder._vector_dim,
+ )
)
- )
- stats = get_database_stats(store.db)
- assert stats["documents"]["num_rows"] == 1
- assert stats["chunks"]["num_rows"] == 1
- store.close()
+ stats = await get_database_stats(store.db)
+ assert stats["documents"]["num_rows"] == 1
+ assert stats["chunks"]["num_rows"] == 1
- def test_stats_with_vector_index(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_stats_with_vector_index(self, temp_db_path):
"""get_database_stats() reports vector index details once an index exists."""
from datetime import timedelta
- store = Store(temp_db_path, create=True)
- dim = store.embedder._vector_dim
+ from lancedb.index import IvfPq
- # Need >=256 rows for IVF_PQ training.
- rows = [
- {
- "id": f"chunk-{i}",
- "document_id": "doc-1",
- "content": f"content {i}",
- "content_fts": "",
- "metadata": "{}",
- "order": i,
- "vector": [float(i % 7) + 0.01 * j for j in range(dim)],
- }
- for i in range(256)
- ]
- store.chunks_table.add(rows)
- store.chunks_table.create_index(
- metric="cosine", index_type="IVF_PQ", replace=True
- )
- store.chunks_table.wait_for_index(["vector_idx"], timeout=timedelta(minutes=1))
+ async with Store(temp_db_path, create=True) as store:
+ dim = store.embedder._vector_dim
- stats = get_database_stats(store.db)
- assert stats["chunks"]["has_vector_index"] is True
- assert stats["chunks"]["num_indexed_rows"] >= 0
- assert "num_unindexed_rows" in stats["chunks"]
- store.close()
+ # Need >=256 rows for IVF_PQ training.
+ rows = [
+ {
+ "id": f"chunk-{i}",
+ "document_id": "doc-1",
+ "content": f"content {i}",
+ "content_fts": "",
+ "metadata": "{}",
+ "order": i,
+ "vector": [float(i % 7) + 0.01 * j for j in range(dim)],
+ }
+ for i in range(256)
+ ]
+ await store.chunks_table.add(rows)
+ await store.chunks_table.create_index(
+ "vector", config=IvfPq(distance_type="cosine"), replace=True
+ )
+ await store.chunks_table.wait_for_index(
+ ["vector_idx"], timeout=timedelta(minutes=1)
+ )
+
+ stats = await get_database_stats(store.db)
+ assert stats["chunks"]["has_vector_index"] is True
+ assert stats["chunks"]["num_indexed_rows"] >= 0
+ assert "num_unindexed_rows" in stats["chunks"]
diff --git a/tests/store/test_migrations.py b/tests/store/test_migrations.py
index 30ffdd38..ff8f0f94 100644
--- a/tests/store/test_migrations.py
+++ b/tests/store/test_migrations.py
@@ -19,150 +19,160 @@ class TestMigrationRequiredError:
class TestMigrationCheck:
- def test_new_database_sets_version(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_new_database_sets_version(self, temp_db_path):
"""New database should set the current package version."""
- store = Store(temp_db_path, create=True)
- version = store.get_haiku_version()
- expected = metadata.version("haiku.rag-slim")
- assert version == expected
- store.close()
+ async with Store(temp_db_path, create=True) as store:
+ version = await store.get_haiku_version()
+ expected = metadata.version("haiku.rag-slim")
+ assert version == expected
- def test_existing_database_same_version_no_error(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_existing_database_same_version_no_error(self, temp_db_path):
"""Opening a database with the same version should not error."""
- store = Store(temp_db_path, create=True)
- store.close()
+ async with Store(temp_db_path, create=True):
+ pass
# Re-open - should work without error
- store = Store(temp_db_path)
- store.close()
+ async with Store(temp_db_path):
+ pass
- def test_version_bump_without_pending_migrations_updates_silently(
+ @pytest.mark.asyncio
+ async def test_version_bump_without_pending_migrations_updates_silently(
self, temp_db_path
):
"""When version is outdated but no migrations pending, update version silently."""
- store = Store(temp_db_path, create=True)
- # Set an older version that has no pending migrations
- # (newer than all current upgrade steps)
- store.set_haiku_version("100.0.0")
- store.close()
+ async with Store(temp_db_path, create=True) as store:
+ # Set an older version that has no pending migrations
+ # (newer than all current upgrade steps)
+ await store.set_haiku_version("100.0.0")
# Re-open - should update version silently, no error
- store = Store(temp_db_path)
- # Version should now be current
- version = store.get_haiku_version()
- expected = metadata.version("haiku.rag-slim")
- assert version == expected
- store.close()
+ async with Store(temp_db_path) as store:
+ # Version should now be current
+ version = await store.get_haiku_version()
+ expected = metadata.version("haiku.rag-slim")
+ assert version == expected
- def test_pending_migrations_raises_error(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_pending_migrations_raises_error(self, temp_db_path):
"""When actual migrations are pending, should raise MigrationRequiredError."""
- store = Store(temp_db_path, create=True)
- # Set version to before the first upgrade step
- store.set_haiku_version("0.19.0")
- store.close()
+ async with Store(temp_db_path, create=True) as store:
+ # Set version to before the first upgrade step
+ await store.set_haiku_version("0.19.0")
# Re-open should raise
with pytest.raises(MigrationRequiredError) as exc_info:
- Store(temp_db_path)
+ async with Store(temp_db_path) as store:
+ pass
assert "migrate" in str(exc_info.value).lower()
- def test_pending_migrations_read_only_raises_error(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_pending_migrations_raises_error_with_create_flag(self, temp_db_path):
+ """Opening an existing DB with create=True must still check migrations."""
+ async with Store(temp_db_path, create=True) as store:
+ await store.set_haiku_version("0.19.0")
+
+ # create=True is idempotent — must not mark an existing populated DB as new
+ with pytest.raises(MigrationRequiredError):
+ async with Store(temp_db_path, create=True) as store:
+ pass
+
+ @pytest.mark.asyncio
+ async def test_pending_migrations_read_only_raises_error(self, temp_db_path):
"""Read-only mode with pending migrations should still raise."""
- store = Store(temp_db_path, create=True)
- store.set_haiku_version("0.19.0")
- store.close()
+ async with Store(temp_db_path, create=True) as store:
+ await store.set_haiku_version("0.19.0")
with pytest.raises(MigrationRequiredError):
- Store(temp_db_path, read_only=True)
+ async with Store(temp_db_path, read_only=True) as store:
+ pass
- def test_read_only_version_bump_without_migrations_ok(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_read_only_version_bump_without_migrations_ok(self, temp_db_path):
"""Read-only mode with version bump but no migrations should work."""
- store = Store(temp_db_path, create=True)
- # Set a version newer than all upgrade steps
- store.set_haiku_version("100.0.0")
- store.close()
+ async with Store(temp_db_path, create=True) as store:
+ # Set a version newer than all upgrade steps
+ await store.set_haiku_version("100.0.0")
# Read-only open should work (version not updated, but no error)
- store = Store(temp_db_path, read_only=True)
- # Version should stay at the old value (can't update in read-only)
- assert store.get_haiku_version() == "100.0.0"
- store.close()
+ async with Store(temp_db_path, read_only=True) as store:
+ # Version should stay at the old value (can't update in read-only)
+ assert await store.get_haiku_version() == "100.0.0"
- def test_skip_migration_check_bypasses_error(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_skip_migration_check_bypasses_error(self, temp_db_path):
"""skip_migration_check=True should bypass migration error."""
- store = Store(temp_db_path, create=True)
- store.set_haiku_version("0.19.0")
- store.close()
+ async with Store(temp_db_path, create=True) as store:
+ await store.set_haiku_version("0.19.0")
# Open with skip_migration_check should work
- store = Store(temp_db_path, skip_migration_check=True)
- # Version should remain old (no auto-migration)
- assert store.get_haiku_version() == "0.19.0"
- store.close()
+ async with Store(temp_db_path, skip_migration_check=True) as store:
+ # Version should remain old (no auto-migration)
+ assert await store.get_haiku_version() == "0.19.0"
class TestMigrateMethod:
- def test_migrate_applies_pending_upgrades(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_migrate_applies_pending_upgrades(self, temp_db_path):
"""Store.migrate() should apply pending upgrades and update version."""
- store = Store(temp_db_path, create=True)
- store.set_haiku_version("0.19.0")
- store.close()
+ async with Store(temp_db_path, create=True) as store:
+ await store.set_haiku_version("0.19.0")
# Open with skip_migration_check to avoid error
- store = Store(temp_db_path, skip_migration_check=True)
- old_version = store.get_haiku_version()
- assert old_version == "0.19.0"
+ async with Store(temp_db_path, skip_migration_check=True) as store:
+ old_version = await store.get_haiku_version()
+ assert old_version == "0.19.0"
- # Run migration
- applied = store.migrate()
+ # Run migration
+ applied = await store.migrate()
- # Should have applied migrations
- assert len(applied) > 0
+ # Should have applied migrations
+ assert len(applied) > 0
- # Version should be updated
- new_version = store.get_haiku_version()
- expected = metadata.version("haiku.rag-slim")
- assert new_version == expected
- store.close()
+ # Version should be updated
+ new_version = await store.get_haiku_version()
+ expected = metadata.version("haiku.rag-slim")
+ assert new_version == expected
- def test_migrate_returns_applied_upgrades(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_migrate_returns_applied_upgrades(self, temp_db_path):
"""Store.migrate() should return list of applied upgrade descriptions."""
- store = Store(temp_db_path, create=True)
- store.set_haiku_version("0.19.0")
- store.close()
+ async with Store(temp_db_path, create=True) as store:
+ await store.set_haiku_version("0.19.0")
- store = Store(temp_db_path, skip_migration_check=True)
- applied = store.migrate()
+ async with Store(temp_db_path, skip_migration_check=True) as store:
+ applied = await store.migrate()
- # Should return descriptions of applied upgrades
- assert isinstance(applied, list)
- for item in applied:
- assert isinstance(item, str)
- store.close()
+ # Should return descriptions of applied upgrades
+ assert isinstance(applied, list)
+ for item in applied:
+ assert isinstance(item, str)
- def test_migrate_with_no_pending_returns_empty(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_migrate_with_no_pending_returns_empty(self, temp_db_path):
"""Store.migrate() with no pending migrations returns empty list."""
- store = Store(temp_db_path, create=True)
- # Already at current version
- store.close()
+ async with Store(temp_db_path, create=True) as store:
+ # Already at current version
+ pass
- store = Store(temp_db_path, skip_migration_check=True)
- applied = store.migrate()
- assert applied == []
- store.close()
+ async with Store(temp_db_path, skip_migration_check=True) as store:
+ applied = await store.migrate()
+ assert applied == []
- def test_migrate_raises_read_only_error(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_migrate_raises_read_only_error(self, temp_db_path):
"""Store.migrate() should raise ReadOnlyError in read-only mode."""
from haiku.rag.store.exceptions import ReadOnlyError
- store = Store(temp_db_path, create=True)
- store.set_haiku_version("0.19.0")
- store.close()
+ async with Store(temp_db_path, create=True) as store:
+ await store.set_haiku_version("0.19.0")
- store = Store(temp_db_path, skip_migration_check=True, read_only=True)
- with pytest.raises(ReadOnlyError):
- store.migrate()
- store.close()
+ async with Store(
+ temp_db_path, skip_migration_check=True, read_only=True
+ ) as store:
+ with pytest.raises(ReadOnlyError):
+ await store.migrate()
class TestGetPendingUpgrades:
diff --git a/tests/store/test_read_only.py b/tests/store/test_read_only.py
index c2f0a89a..9a5e191c 100644
--- a/tests/store/test_read_only.py
+++ b/tests/store/test_read_only.py
@@ -28,7 +28,8 @@ class TestReadOnlyError:
class TestStoreReadOnly:
- def test_store_read_only_raises_on_empty_directory(self, tmp_path):
+ @pytest.mark.asyncio
+ async def test_store_read_only_raises_on_empty_directory(self, tmp_path):
"""Opening an empty directory in read-only mode raises ReadOnlyError."""
empty_dir = tmp_path / "empty_db"
empty_dir.mkdir()
@@ -36,155 +37,148 @@ class TestStoreReadOnly:
with pytest.raises(
ReadOnlyError, match="Cannot create tables in read-only mode"
):
- Store(
+ async with Store(
empty_dir,
read_only=True,
skip_validation=True,
skip_migration_check=True,
- )
+ ):
+ pass
- def test_store_default_is_not_read_only(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_store_default_is_not_read_only(self, temp_db_path):
"""Store defaults to not read-only."""
- store = Store(temp_db_path, create=True)
- assert store.is_read_only is False
- store.close()
+ async with Store(temp_db_path, create=True) as store:
+ assert store.is_read_only is False
- def test_store_can_be_created_read_only(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_store_can_be_created_read_only(self, temp_db_path):
"""Store can be created with read_only=True."""
# First create a normal store to initialize the database
- store = Store(temp_db_path, create=True)
- store.close()
+ async with Store(temp_db_path, create=True):
+ pass
# Now open in read-only mode
- store = Store(temp_db_path, read_only=True)
- assert store.is_read_only is True
- store.close()
+ async with Store(temp_db_path, read_only=True) as store:
+ assert store.is_read_only is True
- def test_assert_writable_raises_when_read_only(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_assert_writable_raises_when_read_only(self, temp_db_path):
"""_assert_writable() raises ReadOnlyError when read_only=True."""
- store = Store(temp_db_path, create=True)
- store.close()
+ async with Store(temp_db_path, create=True):
+ pass
- store = Store(temp_db_path, read_only=True)
- with pytest.raises(ReadOnlyError):
- store._assert_writable()
- store.close()
+ async with Store(temp_db_path, read_only=True) as store:
+ with pytest.raises(ReadOnlyError):
+ store._assert_writable()
- def test_assert_writable_passes_when_not_read_only(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_assert_writable_passes_when_not_read_only(self, temp_db_path):
"""_assert_writable() does not raise when read_only=False."""
- store = Store(temp_db_path, create=True)
- store._assert_writable() # Should not raise
- store.close()
+ async with Store(temp_db_path, create=True) as store:
+ store._assert_writable() # Should not raise
@pytest.mark.asyncio
async def test_vacuum_raises_when_read_only(self, temp_db_path):
"""vacuum() raises ReadOnlyError when read_only=True."""
- store = Store(temp_db_path, create=True)
- store.close()
+ async with Store(temp_db_path, create=True):
+ pass
- store = Store(temp_db_path, read_only=True)
- with pytest.raises(ReadOnlyError):
- await store.vacuum()
- store.close()
+ async with Store(temp_db_path, read_only=True) as store:
+ with pytest.raises(ReadOnlyError):
+ await store.vacuum()
- def test_set_haiku_version_raises_when_read_only(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_set_haiku_version_raises_when_read_only(self, temp_db_path):
"""set_haiku_version() raises ReadOnlyError when read_only=True."""
- store = Store(temp_db_path, create=True)
- store.close()
+ async with Store(temp_db_path, create=True):
+ pass
- store = Store(temp_db_path, read_only=True)
- with pytest.raises(ReadOnlyError):
- store.set_haiku_version("1.0.0")
- store.close()
+ async with Store(temp_db_path, read_only=True) as store:
+ with pytest.raises(ReadOnlyError):
+ await store.set_haiku_version("1.0.0")
- def test_recreate_embeddings_table_raises_when_read_only(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_recreate_embeddings_table_raises_when_read_only(self, temp_db_path):
"""recreate_embeddings_table() raises ReadOnlyError when read_only=True."""
- store = Store(temp_db_path, create=True)
- store.close()
+ async with Store(temp_db_path, create=True):
+ pass
- store = Store(temp_db_path, read_only=True)
- with pytest.raises(ReadOnlyError):
- store.recreate_embeddings_table()
- store.close()
+ async with Store(temp_db_path, read_only=True) as store:
+ with pytest.raises(ReadOnlyError):
+ await store.recreate_embeddings_table()
- def test_restore_table_versions_raises_when_read_only(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_restore_table_versions_raises_when_read_only(self, temp_db_path):
"""restore_table_versions() raises ReadOnlyError when read_only=True."""
- store = Store(temp_db_path, create=True)
- versions = store.current_table_versions()
- store.close()
+ async with Store(temp_db_path, create=True) as store:
+ versions = await store.current_table_versions()
- store = Store(temp_db_path, read_only=True)
- with pytest.raises(ReadOnlyError):
- store.restore_table_versions(versions)
- store.close()
+ async with Store(temp_db_path, read_only=True) as store:
+ with pytest.raises(ReadOnlyError):
+ await store.restore_table_versions(versions)
class TestDocumentRepositoryReadOnly:
@pytest.mark.asyncio
async def test_create_raises_when_read_only(self, temp_db_path):
"""DocumentRepository.create() raises ReadOnlyError when read_only=True."""
- store = Store(temp_db_path, create=True)
- store.close()
+ async with Store(temp_db_path, create=True):
+ pass
- store = Store(temp_db_path, read_only=True)
- repo = DocumentRepository(store)
- doc = Document(content="test content")
+ async with Store(temp_db_path, read_only=True) as store:
+ repo = DocumentRepository(store)
+ doc = Document(content="test content")
- with pytest.raises(ReadOnlyError):
- await repo.create(doc)
- store.close()
+ with pytest.raises(ReadOnlyError):
+ await repo.create(doc)
@pytest.mark.asyncio
async def test_update_raises_when_read_only(self, temp_db_path):
"""DocumentRepository.update() raises ReadOnlyError when read_only=True."""
# First create a document
- store = Store(temp_db_path, create=True)
- repo = DocumentRepository(store)
- doc = Document(content="test content")
- created_doc = await repo.create(doc)
- store.close()
+ async with Store(temp_db_path, create=True) as store:
+ repo = DocumentRepository(store)
+ doc = Document(content="test content")
+ created_doc = await repo.create(doc)
# Try to update in read-only mode
- store = Store(temp_db_path, read_only=True)
- repo = DocumentRepository(store)
- created_doc.content = "updated content"
+ async with Store(temp_db_path, read_only=True) as store:
+ repo = DocumentRepository(store)
+ created_doc.content = "updated content"
- with pytest.raises(ReadOnlyError):
- await repo.update(created_doc)
- store.close()
+ with pytest.raises(ReadOnlyError):
+ await repo.update(created_doc)
@pytest.mark.asyncio
async def test_delete_raises_when_read_only(self, temp_db_path):
"""DocumentRepository.delete() raises ReadOnlyError when read_only=True."""
# First create a document
- store = Store(temp_db_path, create=True)
- repo = DocumentRepository(store)
- doc = Document(content="test content")
- created_doc = await repo.create(doc)
- assert created_doc.id is not None
- doc_id = created_doc.id
- store.close()
+ async with Store(temp_db_path, create=True) as store:
+ repo = DocumentRepository(store)
+ doc = Document(content="test content")
+ created_doc = await repo.create(doc)
+ assert created_doc.id is not None
+ doc_id = created_doc.id
# Try to delete in read-only mode
- store = Store(temp_db_path, read_only=True)
- repo = DocumentRepository(store)
+ async with Store(temp_db_path, read_only=True) as store:
+ repo = DocumentRepository(store)
- with pytest.raises(ReadOnlyError):
- await repo.delete(doc_id)
- store.close()
+ with pytest.raises(ReadOnlyError):
+ await repo.delete(doc_id)
@pytest.mark.asyncio
async def test_delete_all_raises_when_read_only(self, temp_db_path):
"""DocumentRepository.delete_all() raises ReadOnlyError when read_only=True."""
- store = Store(temp_db_path, create=True)
- store.close()
+ async with Store(temp_db_path, create=True):
+ pass
- store = Store(temp_db_path, read_only=True)
- repo = DocumentRepository(store)
+ async with Store(temp_db_path, read_only=True) as store:
+ repo = DocumentRepository(store)
- with pytest.raises(ReadOnlyError):
- await repo.delete_all()
- store.close()
+ with pytest.raises(ReadOnlyError):
+ await repo.delete_all()
class TestChunkRepositoryReadOnly:
@@ -192,86 +186,82 @@ class TestChunkRepositoryReadOnly:
async def test_create_raises_when_read_only(self, temp_db_path):
"""ChunkRepository.create() raises ReadOnlyError when read_only=True."""
# First create a document to have a valid document_id
- store = Store(temp_db_path, create=True)
- doc_repo = DocumentRepository(store)
- doc = Document(content="test content")
- created_doc = await doc_repo.create(doc)
- store.close()
+ async with Store(temp_db_path, create=True) as store:
+ doc_repo = DocumentRepository(store)
+ doc = Document(content="test content")
+ created_doc = await doc_repo.create(doc)
- store = Store(temp_db_path, read_only=True)
- repo = ChunkRepository(store)
- chunk = Chunk(
- content="test chunk",
- document_id=created_doc.id,
- embedding=[0.0] * store.embedder._vector_dim,
- )
+ async with Store(temp_db_path, read_only=True) as store:
+ repo = ChunkRepository(store)
+ chunk = Chunk(
+ content="test chunk",
+ document_id=created_doc.id,
+ embedding=[0.0] * store.embedder._vector_dim,
+ )
- with pytest.raises(ReadOnlyError):
- await repo.create(chunk)
- store.close()
+ with pytest.raises(ReadOnlyError):
+ await repo.create(chunk)
@pytest.mark.asyncio
async def test_delete_by_document_id_raises_when_read_only(self, temp_db_path):
"""ChunkRepository.delete_by_document_id() raises ReadOnlyError when read_only=True."""
- store = Store(temp_db_path, create=True)
- store.close()
+ async with Store(temp_db_path, create=True):
+ pass
- store = Store(temp_db_path, read_only=True)
- repo = ChunkRepository(store)
+ async with Store(temp_db_path, read_only=True) as store:
+ repo = ChunkRepository(store)
- with pytest.raises(ReadOnlyError):
- await repo.delete_by_document_id("some-id")
- store.close()
+ with pytest.raises(ReadOnlyError):
+ await repo.delete_by_document_id("some-id")
@pytest.mark.asyncio
async def test_delete_all_raises_when_read_only(self, temp_db_path):
"""ChunkRepository.delete_all() raises ReadOnlyError when read_only=True."""
- store = Store(temp_db_path, create=True)
- store.close()
+ async with Store(temp_db_path, create=True):
+ pass
- store = Store(temp_db_path, read_only=True)
- repo = ChunkRepository(store)
+ async with Store(temp_db_path, read_only=True) as store:
+ repo = ChunkRepository(store)
- with pytest.raises(ReadOnlyError):
- await repo.delete_all()
- store.close()
+ with pytest.raises(ReadOnlyError):
+ await repo.delete_all()
class TestSettingsRepositoryReadOnly:
- def test_save_current_settings_raises_when_read_only(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_save_current_settings_raises_when_read_only(self, temp_db_path):
"""SettingsRepository.save_current_settings() raises ReadOnlyError when read_only=True."""
- store = Store(temp_db_path, create=True)
- store.close()
+ async with Store(temp_db_path, create=True):
+ pass
- store = Store(temp_db_path, read_only=True)
- repo = SettingsRepository(store)
+ async with Store(temp_db_path, read_only=True) as store:
+ repo = SettingsRepository(store)
- with pytest.raises(ReadOnlyError):
- repo.save_current_settings()
- store.close()
+ with pytest.raises(ReadOnlyError):
+ await repo.save_current_settings()
class TestClientReadOnly:
- def test_client_default_is_not_read_only(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_client_default_is_not_read_only(self, temp_db_path):
"""Client defaults to not read-only."""
- client = HaikuRAG(temp_db_path, create=True)
- assert client.is_read_only is False
- client.close()
+ async with HaikuRAG(temp_db_path, create=True) as client:
+ assert client.is_read_only is False
- def test_client_can_be_created_read_only(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_client_can_be_created_read_only(self, temp_db_path):
"""Client can be created with read_only=True."""
- client = HaikuRAG(temp_db_path, create=True)
- client.close()
+ async with HaikuRAG(temp_db_path, create=True):
+ pass
- client = HaikuRAG(temp_db_path, read_only=True)
- assert client.is_read_only is True
- client.close()
+ async with HaikuRAG(temp_db_path, read_only=True) as client:
+ assert client.is_read_only is True
@pytest.mark.vcr()
async def test_client_create_document_raises_when_read_only(self, temp_db_path):
"""Client.create_document() raises ReadOnlyError when read_only=True."""
- client = HaikuRAG(temp_db_path, create=True)
- client.close()
+ async with HaikuRAG(temp_db_path, create=True):
+ pass
async with HaikuRAG(temp_db_path, read_only=True) as client:
with pytest.raises(ReadOnlyError):
diff --git a/tests/store/test_time_travel.py b/tests/store/test_time_travel.py
index 48d75d51..db0a7e85 100644
--- a/tests/store/test_time_travel.py
+++ b/tests/store/test_time_travel.py
@@ -9,104 +9,88 @@ from haiku.rag.store.repositories.document import DocumentRepository
class TestStoreTimeTravel:
- def test_store_with_before_is_read_only(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_store_with_before_is_read_only(self, temp_db_path):
"""Store with before parameter is automatically read-only."""
- # Create a store first
- store = Store(temp_db_path, create=True)
- store.close()
+ async with Store(temp_db_path, create=True):
+ pass
- # Open with before - should be read-only
before = datetime.now(UTC) + timedelta(hours=1)
- store = Store(temp_db_path, before=before)
- assert store.is_read_only is True
- store.close()
+ async with Store(temp_db_path, before=before) as store:
+ assert store.is_read_only is True
- def test_store_before_raises_on_write(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_store_before_raises_on_write(self, temp_db_path):
"""Store with before parameter raises on write operations."""
- store = Store(temp_db_path, create=True)
- store.close()
+ async with Store(temp_db_path, create=True):
+ pass
before = datetime.now(UTC) + timedelta(hours=1)
- store = Store(temp_db_path, before=before)
-
- with pytest.raises(ReadOnlyError):
- store._assert_writable()
- store.close()
+ async with Store(temp_db_path, before=before) as store:
+ with pytest.raises(ReadOnlyError):
+ store._assert_writable()
@pytest.mark.asyncio
async def test_store_before_checks_out_historical_state(self, temp_db_path):
"""Store with before parameter checks out tables to historical state."""
- # Create store and add a document
- store = Store(temp_db_path, create=True)
- repo = DocumentRepository(store)
- await repo.create(Document(content="First document"))
+ async with Store(temp_db_path, create=True) as store:
+ repo = DocumentRepository(store)
+ await repo.create(Document(content="First document"))
- # Get the version timestamp after first document
- versions_after_first = store.list_table_versions("documents")
- # Find the latest version timestamp
- latest_version = max(versions_after_first, key=lambda v: v["version"])
- time_after_first = latest_version["timestamp"]
+ versions_after_first = await store.list_table_versions("documents")
+ latest_version = max(versions_after_first, key=lambda v: v["version"])
+ time_after_first = latest_version["timestamp"]
- # Wait a bit to ensure the next write gets a distinct timestamp
- await asyncio.sleep(0.5)
+ await asyncio.sleep(0.5)
- # Add second document
- await repo.create(Document(content="Second document"))
+ await repo.create(Document(content="Second document"))
- # Verify we have more versions now
- versions_after_second = store.list_table_versions("documents")
- assert len(versions_after_second) > len(versions_after_first)
+ versions_after_second = await store.list_table_versions("documents")
+ assert len(versions_after_second) > len(versions_after_first)
- store.close()
+ async with Store(temp_db_path, before=time_after_first) as store:
+ repo = DocumentRepository(store)
- # Open at historical state (using the timestamp from after first write)
- store = Store(temp_db_path, before=time_after_first)
- repo = DocumentRepository(store)
+ docs = await repo.list_all(include_content=True)
+ assert len(docs) == 1
+ assert docs[0].content == "First document"
- # Should only see first document
- docs = await repo.list_all(include_content=True)
- assert len(docs) == 1
- assert docs[0].content == "First document"
- store.close()
+ async with Store(temp_db_path) as store:
+ repo = DocumentRepository(store)
- # Open at current state
- store = Store(temp_db_path)
- repo = DocumentRepository(store)
+ docs = await repo.list_all()
+ assert len(docs) == 2
- # Should see both documents
- docs = await repo.list_all()
- assert len(docs) == 2
- store.close()
-
- def test_store_before_no_version_raises(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_store_before_no_version_raises(self, temp_db_path):
"""Store with before datetime before any version raises ValueError."""
- store = Store(temp_db_path, create=True)
- store.close()
+ async with Store(temp_db_path, create=True):
+ pass
- # Try to open before the database was created
before = datetime(2000, 1, 1, tzinfo=UTC)
with pytest.raises(ValueError) as exc_info:
- Store(temp_db_path, before=before)
+ async with Store(temp_db_path, before=before):
+ pass
assert "No data exists before" in str(exc_info.value)
- def test_current_table_versions_returns_versions(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_current_table_versions_returns_versions(self, temp_db_path):
"""current_table_versions returns dict of table versions."""
- store = Store(temp_db_path, create=True)
- versions = store.current_table_versions()
+ async with Store(temp_db_path, create=True) as store:
+ versions = await store.current_table_versions()
- assert "documents" in versions
- assert "chunks" in versions
- assert "settings" in versions
- assert all(isinstance(v, int) for v in versions.values())
- store.close()
+ assert "documents" in versions
+ assert "chunks" in versions
+ assert "settings" in versions
+ assert all(isinstance(v, int) for v in versions.values())
- def test_list_table_versions_returns_history(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_list_table_versions_returns_history(self, temp_db_path):
"""list_table_versions returns version history for a table."""
- store = Store(temp_db_path, create=True)
- versions = store.list_table_versions("documents")
+ async with Store(temp_db_path, create=True) as store:
+ versions = await store.list_table_versions("documents")
- assert len(versions) >= 1
- for v in versions:
- assert "version" in v
- assert "timestamp" in v
- store.close()
+ assert len(versions) >= 1
+ for v in versions:
+ assert "version" in v
+ assert "timestamp" in v
diff --git a/tests/test_chunk.py b/tests/test_chunk.py
index 38579965..4d37945e 100644
--- a/tests/test_chunk.py
+++ b/tests/test_chunk.py
@@ -9,42 +9,40 @@ from haiku.rag.store.models.chunk import Chunk, ChunkMetadata, SearchResult
@pytest.mark.vcr()
async def test_chunk_repository_operations(qa_corpus: Dataset, temp_db_path):
"""Test ChunkRepository operations."""
- # Create client
- client = HaikuRAG(db_path=temp_db_path, config=Config, create=True)
+ async with HaikuRAG(db_path=temp_db_path, config=Config, create=True) as client:
+ # Get the first document from the corpus
+ first_doc = qa_corpus[0]
+ document_text = first_doc["document_extracted"]
- # Get the first document from the corpus
- first_doc = qa_corpus[0]
- document_text = first_doc["document_extracted"]
+ # Create a document first with chunks
+ created_document = await client.create_document(
+ content=document_text, metadata={"source": "test"}
+ )
+ assert created_document.id is not None
- # Create a document first with chunks
- created_document = await client.create_document(
- content=document_text, metadata={"source": "test"}
- )
- assert created_document.id is not None
+ # Test getting chunks by document ID
+ chunks = await client.chunk_repository.get_by_document_id(created_document.id)
+ assert len(chunks) > 0
+ assert all(chunk.document_id == created_document.id for chunk in chunks)
- # Test getting chunks by document ID
- chunks = await client.chunk_repository.get_by_document_id(created_document.id)
- assert len(chunks) > 0
- assert all(chunk.document_id == created_document.id for chunk in chunks)
+ # Test chunk search
+ results = await client.chunk_repository.search(
+ "election", limit=2, search_type="vector"
+ )
+ assert len(results) <= 2
+ assert all(hasattr(chunk, "content") for chunk, _ in results)
- # Test chunk search
- results = await client.chunk_repository.search(
- "election", limit=2, search_type="vector"
- )
- assert len(results) <= 2
- assert all(hasattr(chunk, "content") for chunk, _ in results)
+ # Test deleting chunks by document ID
+ deleted = await client.chunk_repository.delete_by_document_id(
+ created_document.id
+ )
+ assert deleted is True
- # Test deleting chunks by document ID
- deleted = await client.chunk_repository.delete_by_document_id(created_document.id)
- assert deleted is True
-
- # Verify chunks are gone
- chunks_after_delete = await client.chunk_repository.get_by_document_id(
- created_document.id
- )
- assert len(chunks_after_delete) == 0
-
- client.close()
+ # Verify chunks are gone
+ chunks_after_delete = await client.chunk_repository.get_by_document_id(
+ created_document.id
+ )
+ assert len(chunks_after_delete) == 0
@pytest.mark.vcr()
@@ -409,13 +407,12 @@ async def test_chunk_content_fts_populated(temp_db_path):
await client.chunk_repository.create(chunk)
# Read the raw record from the database
- records = list(
- client.store.chunks_table.search()
+ records = (
+ await client.store.chunks_table.query()
.where(f"id = '{chunk.id}'")
.limit(1)
.to_arrow()
- .to_pylist()
- )
+ ).to_pylist()
assert len(records) == 1
record = records[0]
@@ -453,13 +450,12 @@ async def test_chunk_content_fts_without_headings(temp_db_path):
await client.chunk_repository.create(chunk)
# Read the raw record from the database
- records = list(
- client.store.chunks_table.search()
+ records = (
+ await client.store.chunks_table.query()
.where(f"id = '{chunk.id}'")
.limit(1)
.to_arrow()
- .to_pylist()
- )
+ ).to_pylist()
assert len(records) == 1
record = records[0]
diff --git a/tests/test_client.py b/tests/test_client.py
index dbb79983..360163dc 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -481,49 +481,35 @@ async def test_client_create_document_from_url_http_error(temp_db_path):
)
-@pytest.mark.vcr()
-async def test_get_extension_from_content_type_or_url(temp_db_path):
- """Test the helper method for determining file extensions."""
- async with HaikuRAG(temp_db_path, create=True) as client:
- # Test content type mappings
- assert (
- client._get_extension_from_content_type_or_url("", "text/html") == ".html"
- )
- assert (
- client._get_extension_from_content_type_or_url("", "application/pdf")
- == ".pdf"
- )
- assert (
- client._get_extension_from_content_type_or_url("", "text/plain") == ".txt"
- )
+def test_get_extension_from_content_type_or_url():
+ """Test the helper function for determining file extensions."""
+ from haiku.rag.client.processing import get_extension_from_content_type_or_url
- # Test URL extension detection
- assert (
- client._get_extension_from_content_type_or_url(
- "https://example.com/doc.pdf", ""
- )
- == ".pdf"
- )
- assert (
- client._get_extension_from_content_type_or_url(
- "https://example.com/data.json", ""
- )
- == ".json"
- )
+ # Content type mappings
+ assert get_extension_from_content_type_or_url("", "text/html") == ".html"
+ assert get_extension_from_content_type_or_url("", "application/pdf") == ".pdf"
+ assert get_extension_from_content_type_or_url("", "text/plain") == ".txt"
- # Test default fallback
- assert (
- client._get_extension_from_content_type_or_url("https://example.com/", "")
- == ".html"
- )
+ # URL extension detection
+ assert (
+ get_extension_from_content_type_or_url("https://example.com/doc.pdf", "")
+ == ".pdf"
+ )
+ assert (
+ get_extension_from_content_type_or_url("https://example.com/data.json", "")
+ == ".json"
+ )
- # Test content type priority over URL extension
- assert (
- client._get_extension_from_content_type_or_url(
- "https://example.com/file.txt", "application/pdf"
- )
- == ".pdf"
+ # Default fallback
+ assert get_extension_from_content_type_or_url("https://example.com/", "") == ".html"
+
+ # Content type priority over URL extension
+ assert (
+ get_extension_from_content_type_or_url(
+ "https://example.com/file.txt", "application/pdf"
)
+ == ".pdf"
+ )
@pytest.mark.vcr()
@@ -1286,6 +1272,39 @@ async def test_client_convert_file_not_found(temp_db_path):
await client.convert(Path("/nonexistent/path/file.txt"))
+async def test_client_convert_from_url(temp_db_path):
+ """convert() with an http(s) URL downloads to a tempfile and converts."""
+ from docling_core.types.doc.document import DoclingDocument
+
+ async with HaikuRAG(temp_db_path, create=True) as client:
+ mock_response = AsyncMock()
+ mock_response.content = (
+ b"URL convert path content.
"
+ )
+ mock_response.headers = {"content-type": "text/html"}
+ mock_response.raise_for_status = AsyncMock()
+
+ with patch("httpx.AsyncClient.get", return_value=mock_response):
+ docling_doc = await client.convert("https://example.com/page.html")
+
+ assert isinstance(docling_doc, DoclingDocument)
+ markdown = docling_doc.export_to_markdown()
+ assert "URL convert path content" in markdown
+
+
+async def test_client_convert_from_url_unsupported_content_type(temp_db_path):
+ """convert() rejects URLs whose content type isn't supported by the converter."""
+ async with HaikuRAG(temp_db_path, create=True) as client:
+ mock_response = AsyncMock()
+ mock_response.content = b"\x00\x01\x02binary"
+ mock_response.headers = {"content-type": "application/octet-stream"}
+ mock_response.raise_for_status = AsyncMock()
+
+ with patch("httpx.AsyncClient.get", return_value=mock_response):
+ with pytest.raises(ValueError, match="Unsupported content type"):
+ await client.convert("https://example.com/blob.bin")
+
+
@pytest.mark.vcr()
async def test_client_convert_unsupported_extension(temp_db_path):
"""Test convert() raises ValueError for unsupported file extension."""
diff --git a/tests/test_context.py b/tests/test_context.py
index 7eb5f1f0..0ad6665a 100644
--- a/tests/test_context.py
+++ b/tests/test_context.py
@@ -1,5 +1,6 @@
import pytest
+from haiku.rag.client.documents import _store_document_with_chunks
from haiku.rag.context import (
_expand_outward,
_find_expansion_range,
@@ -249,7 +250,8 @@ class TestExpandWithItems:
from haiku.rag.store.models.document import Document
async with HaikuRAG(temp_db_path, create=True) as rag:
- doc = await rag._store_document_with_chunks(
+ doc = await _store_document_with_chunks(
+ rag,
Document(content="test"),
[],
__import__(
@@ -262,6 +264,7 @@ class TestExpandWithItems:
document_id=doc.id,
doc_item_refs=["#/texts/999999"],
)
+ assert doc.id is not None
expanded = await expand_with_items(
rag.document_item_repository, doc.id, [result], 5000
)
diff --git a/tests/test_context_enhancement.py b/tests/test_context_enhancement.py
index 6f59ec7c..aad53a6d 100644
--- a/tests/test_context_enhancement.py
+++ b/tests/test_context_enhancement.py
@@ -3,6 +3,8 @@ from docling_core.types.doc.document import DoclingDocument, TableData
from docling_core.types.doc.labels import DocItemLabel
from haiku.rag.client import HaikuRAG
+from haiku.rag.client.documents import _store_document_with_chunks
+from haiku.rag.client.processing import ensure_chunks_embedded
from haiku.rag.config.models import AppConfig
from haiku.rag.store.models import SearchResult
@@ -12,7 +14,7 @@ async def create_document_with_docling(
):
"""Helper to create a document from a DoclingDocument using import_document."""
chunks = await client.chunk(docling_doc)
- embedded_chunks = await client._ensure_chunks_embedded(chunks)
+ embedded_chunks = await ensure_chunks_embedded(client._config, chunks)
return await client.import_document(
docling_document=docling_doc,
chunks=embedded_chunks,
@@ -314,7 +316,7 @@ async def test_expand_context_single_item_document(temp_db_path):
async with HaikuRAG(temp_db_path, create=True) as client:
document = Document(content="Simple test content")
document.set_docling(docling_doc)
- doc = await client._store_document_with_chunks(document, [], docling_doc)
+ doc = await _store_document_with_chunks(client, document, [], docling_doc)
assert doc.id is not None
# Create a search result with a doc_item_ref pointing to the item
diff --git a/tests/test_database_autocreate.py b/tests/test_database_autocreate.py
index 5f28884e..d15bb950 100644
--- a/tests/test_database_autocreate.py
+++ b/tests/test_database_autocreate.py
@@ -1,51 +1,45 @@
-import tempfile
-from pathlib import Path
-
import pytest
from haiku.rag.client import HaikuRAG
from haiku.rag.config import AppConfig
-def test_database_not_created_without_create_flag():
+async def test_database_not_created_without_create_flag(tmp_path):
"""Test that database is not created without create=True."""
- with tempfile.TemporaryDirectory() as tmpdir:
- db_path = Path(tmpdir) / "test.lancedb"
+ db_path = tmp_path / "test.lancedb"
- config = AppConfig()
+ config = AppConfig()
- with pytest.raises(FileNotFoundError, match="Database does not exist"):
- HaikuRAG(db_path=db_path, config=config)
+ with pytest.raises(FileNotFoundError, match="Database does not exist"):
+ async with HaikuRAG(db_path=db_path, config=config):
+ pass
-def test_database_created_with_create_flag():
+async def test_database_created_with_create_flag(tmp_path):
"""Test that database is created with create=True."""
- with tempfile.TemporaryDirectory() as tmpdir:
- db_path = Path(tmpdir) / "test.lancedb"
+ db_path = tmp_path / "test.lancedb"
- config = AppConfig()
+ config = AppConfig()
- client = HaikuRAG(db_path=db_path, config=config, create=True)
+ async with HaikuRAG(db_path=db_path, config=config, create=True):
assert db_path.exists()
- client.close()
@pytest.mark.vcr()
-async def test_operations_work_after_database_created():
+async def test_operations_work_after_database_created(tmp_path):
"""Test that operations work after DB is created."""
- with tempfile.TemporaryDirectory() as tmpdir:
- db_path = Path(tmpdir) / "test.lancedb"
+ db_path = tmp_path / "test.lancedb"
- config = AppConfig()
+ config = AppConfig()
- # First, create DB with create=True and add document
- async with HaikuRAG(db_path=db_path, config=config, create=True) as client:
- await client.create_document("Test content", uri="test://doc1")
+ # First, create DB with create=True and add document
+ async with HaikuRAG(db_path=db_path, config=config, create=True) as client:
+ await client.create_document("Test content", uri="test://doc1")
- # Re-open without create flag and verify we can read the document
- async with HaikuRAG(db_path=db_path, config=config) as client:
- docs = await client.list_documents()
- assert len(docs) == 1
- doc = await client.get_document_by_id(docs[0].id)
- assert doc is not None
- assert doc.content == "Test content"
+ # Re-open without create flag and verify we can read the document
+ async with HaikuRAG(db_path=db_path, config=config) as client:
+ docs = await client.list_documents()
+ assert len(docs) == 1
+ doc = await client.get_document_by_id(docs[0].id)
+ assert doc is not None
+ assert doc.content == "Test content"
diff --git a/tests/test_document.py b/tests/test_document.py
index a19ce115..a8e55dff 100644
--- a/tests/test_document.py
+++ b/tests/test_document.py
@@ -11,27 +11,25 @@ async def test_document_list_excludes_content_by_default(
qa_corpus: Dataset, temp_db_path
):
"""list_all excludes content and docling_document by default."""
- store = Store(temp_db_path, create=True)
- doc_repo = DocumentRepository(store)
+ async with Store(temp_db_path, create=True) as store:
+ doc_repo = DocumentRepository(store)
- doc = Document(
- content=qa_corpus[0]["document_extracted"],
- uri="https://example.com/doc.txt",
- title="Test Document",
- metadata={"key": "value"},
- )
- created = await doc_repo.create(doc)
+ doc = Document(
+ content=qa_corpus[0]["document_extracted"],
+ uri="https://example.com/doc.txt",
+ title="Test Document",
+ metadata={"key": "value"},
+ )
+ created = await doc_repo.create(doc)
- docs = await doc_repo.list_all()
- assert len(docs) == 1
- assert docs[0].id == created.id
- assert docs[0].title == "Test Document"
- assert docs[0].uri == "https://example.com/doc.txt"
- assert docs[0].metadata == {"key": "value"}
- assert docs[0].content == ""
- assert docs[0].docling_document is None
-
- store.close()
+ docs = await doc_repo.list_all()
+ assert len(docs) == 1
+ assert docs[0].id == created.id
+ assert docs[0].title == "Test Document"
+ assert docs[0].uri == "https://example.com/doc.txt"
+ assert docs[0].metadata == {"key": "value"}
+ assert docs[0].content == ""
+ assert docs[0].docling_document is None
@pytest.mark.asyncio
@@ -39,62 +37,61 @@ async def test_document_list_includes_content_when_requested(
qa_corpus: Dataset, temp_db_path
):
"""list_all returns content when include_content=True."""
- store = Store(temp_db_path, create=True)
- doc_repo = DocumentRepository(store)
+ async with Store(temp_db_path, create=True) as store:
+ doc_repo = DocumentRepository(store)
- content = qa_corpus[0]["document_extracted"]
- doc = Document(content=content, uri="https://example.com/doc.txt")
- created = await doc_repo.create(doc)
+ content = qa_corpus[0]["document_extracted"]
+ doc = Document(content=content, uri="https://example.com/doc.txt")
+ created = await doc_repo.create(doc)
- docs = await doc_repo.list_all(include_content=True)
- assert len(docs) == 1
- assert docs[0].id == created.id
- assert docs[0].content == content
-
- store.close()
+ docs = await doc_repo.list_all(include_content=True)
+ assert len(docs) == 1
+ assert docs[0].id == created.id
+ assert docs[0].content == content
@pytest.mark.asyncio
async def test_document_list_with_filter(qa_corpus: Dataset, temp_db_path):
"""Test listing documents with filter clause."""
- store = Store(temp_db_path, create=True)
- doc_repo = DocumentRepository(store)
+ async with Store(temp_db_path, create=True) as store:
+ doc_repo = DocumentRepository(store)
- first_doc = qa_corpus[0]
- document_text = first_doc["document_extracted"]
+ first_doc = qa_corpus[0]
+ document_text = first_doc["document_extracted"]
- doc1 = Document(
- content=document_text,
- uri="https://example.com/doc1.txt",
- metadata={"source": "test", "category": "A"},
- )
- doc2 = Document(
- content=document_text,
- uri="https://arxiv.org/paper.pdf",
- metadata={"source": "test", "category": "B"},
- )
- doc3 = Document(
- content=document_text,
- uri="https://example.com/doc3.txt",
- metadata={"source": "test", "category": "A"},
- )
+ doc1 = Document(
+ content=document_text,
+ uri="https://example.com/doc1.txt",
+ metadata={"source": "test", "category": "A"},
+ )
+ doc2 = Document(
+ content=document_text,
+ uri="https://arxiv.org/paper.pdf",
+ metadata={"source": "test", "category": "B"},
+ )
+ doc3 = Document(
+ content=document_text,
+ uri="https://example.com/doc3.txt",
+ metadata={"source": "test", "category": "A"},
+ )
- created_doc1 = await doc_repo.create(doc1)
- created_doc2 = await doc_repo.create(doc2)
- created_doc3 = await doc_repo.create(doc3)
+ created_doc1 = await doc_repo.create(doc1)
+ created_doc2 = await doc_repo.create(doc2)
+ created_doc3 = await doc_repo.create(doc3)
- all_documents = await doc_repo.list_all()
- assert len(all_documents) == 3
+ all_documents = await doc_repo.list_all()
+ assert len(all_documents) == 3
- arxiv_documents = await doc_repo.list_all(filter="uri LIKE '%arxiv%'")
- assert len(arxiv_documents) == 1
- assert arxiv_documents[0].id == created_doc2.id
+ arxiv_documents = await doc_repo.list_all(filter="uri LIKE '%arxiv%'")
+ assert len(arxiv_documents) == 1
+ assert arxiv_documents[0].id == created_doc2.id
- example_documents = await doc_repo.list_all(filter="uri LIKE '%example.com%'")
- assert len(example_documents) == 2
- assert {doc.id for doc in example_documents} == {created_doc1.id, created_doc3.id}
-
- store.close()
+ example_documents = await doc_repo.list_all(filter="uri LIKE '%example.com%'")
+ assert len(example_documents) == 2
+ assert {doc.id for doc in example_documents} == {
+ created_doc1.id,
+ created_doc3.id,
+ }
def test_document_get_docling_document():
@@ -239,45 +236,43 @@ async def test_get_docling_data_loads_only_docling_columns(
from haiku.rag.store.compression import compress_json
- store = Store(temp_db_path, create=True)
- doc_repo = DocumentRepository(store)
+ async with Store(temp_db_path, create=True) as store:
+ doc_repo = DocumentRepository(store)
- doc_json = {
- "name": "test_doc",
- "texts": [],
- "tables": [],
- "pictures": [],
- "groups": [],
- "body": {"self_ref": "#/body", "children": []},
- "furniture": {"self_ref": "#/furniture", "children": []},
- }
- compressed = compress_json(json.dumps(doc_json))
+ doc_json = {
+ "name": "test_doc",
+ "texts": [],
+ "tables": [],
+ "pictures": [],
+ "groups": [],
+ "body": {"self_ref": "#/body", "children": []},
+ "furniture": {"self_ref": "#/furniture", "children": []},
+ }
+ compressed = compress_json(json.dumps(doc_json))
- doc = Document(
- content=qa_corpus[0]["document_extracted"],
- uri="https://example.com/doc.txt",
- docling_document=compressed,
- docling_version="2.1.0",
- )
- created = await doc_repo.create(doc)
- assert created.id is not None
+ doc = Document(
+ content=qa_corpus[0]["document_extracted"],
+ uri="https://example.com/doc.txt",
+ docling_document=compressed,
+ docling_version="2.1.0",
+ )
+ created = await doc_repo.create(doc)
+ assert created.id is not None
- result = await doc_repo.get_docling_data(created.id)
- assert result is not None
- assert result.id == created.id
- assert result.content == ""
- assert result.docling_document == compressed
- assert result.docling_version == "2.1.0"
+ result = await doc_repo.get_docling_data(created.id)
+ assert result is not None
+ assert result.id == created.id
+ assert result.content == ""
+ assert result.docling_document == compressed
+ assert result.docling_version == "2.1.0"
- # Verify docling document can be parsed
- docling_doc = result.get_docling_document()
- assert docling_doc is not None
- assert docling_doc.name == "test_doc"
+ # Verify docling document can be parsed
+ docling_doc = result.get_docling_document()
+ assert docling_doc is not None
+ assert docling_doc.name == "test_doc"
- # Non-existent ID returns None
- assert await doc_repo.get_docling_data("nonexistent-id") is None
-
- store.close()
+ # Non-existent ID returns None
+ assert await doc_repo.get_docling_data("nonexistent-id") is None
@pytest.mark.asyncio
@@ -291,27 +286,25 @@ async def test_get_pages_data_loads_only_pages_column(qa_corpus: Dataset, temp_d
json.dumps({"1": {"size": {"width": 612, "height": 792}, "page_no": 1}})
)
- store = Store(temp_db_path, create=True)
- doc_repo = DocumentRepository(store)
+ async with Store(temp_db_path, create=True) as store:
+ doc_repo = DocumentRepository(store)
- doc = Document(
- content=qa_corpus[0]["document_extracted"],
- uri="https://example.com/doc.txt",
- docling_pages=pages_blob,
- )
- created = await doc_repo.create(doc)
- assert created.id is not None
+ doc = Document(
+ content=qa_corpus[0]["document_extracted"],
+ uri="https://example.com/doc.txt",
+ docling_pages=pages_blob,
+ )
+ created = await doc_repo.create(doc)
+ assert created.id is not None
- result = await doc_repo.get_pages_data(created.id)
- assert result is not None
- assert result.id == created.id
- assert result.content == ""
- assert result.docling_pages == pages_blob
+ result = await doc_repo.get_pages_data(created.id)
+ assert result is not None
+ assert result.id == created.id
+ assert result.content == ""
+ assert result.docling_pages == pages_blob
- # Non-existent ID returns None
- assert await doc_repo.get_pages_data("nonexistent-id") is None
-
- store.close()
+ # Non-existent ID returns None
+ assert await doc_repo.get_pages_data("nonexistent-id") is None
@pytest.mark.asyncio
@@ -319,22 +312,20 @@ async def test_get_pages_data_none_for_markdown_document(
qa_corpus: Dataset, temp_db_path
):
"""Markdown documents have no page images — get_pages_data returns None pages."""
- store = Store(temp_db_path, create=True)
- doc_repo = DocumentRepository(store)
+ async with Store(temp_db_path, create=True) as store:
+ doc_repo = DocumentRepository(store)
- doc = Document(
- content=qa_corpus[0]["document_extracted"],
- uri="https://example.com/doc.md",
- )
- created = await doc_repo.create(doc)
- assert created.id is not None
+ doc = Document(
+ content=qa_corpus[0]["document_extracted"],
+ uri="https://example.com/doc.md",
+ )
+ created = await doc_repo.create(doc)
+ assert created.id is not None
- result = await doc_repo.get_pages_data(created.id)
- assert result is not None
- assert result.id == created.id
- assert result.docling_pages is None
-
- store.close()
+ result = await doc_repo.get_pages_data(created.id)
+ assert result is not None
+ assert result.id == created.id
+ assert result.docling_pages is None
@pytest.mark.asyncio
@@ -342,23 +333,21 @@ async def test_document_get_by_uri_with_special_characters(
qa_corpus: Dataset, temp_db_path
):
"""Test get_by_uri handles URIs with special characters like single quotes."""
- store = Store(temp_db_path, create=True)
- doc_repo = DocumentRepository(store)
+ async with Store(temp_db_path, create=True) as store:
+ doc_repo = DocumentRepository(store)
- first_doc = qa_corpus[0]
- document_text = first_doc["document_extracted"]
+ first_doc = qa_corpus[0]
+ document_text = first_doc["document_extracted"]
- doc_with_quote = Document(
- content=document_text,
- uri="Hamish and Andy's Gap Year",
- metadata={"source": "test"},
- )
+ doc_with_quote = Document(
+ content=document_text,
+ uri="Hamish and Andy's Gap Year",
+ metadata={"source": "test"},
+ )
- created_doc = await doc_repo.create(doc_with_quote)
+ created_doc = await doc_repo.create(doc_with_quote)
- retrieved = await doc_repo.get_by_uri("Hamish and Andy's Gap Year")
- assert retrieved is not None
- assert retrieved.id == created_doc.id
- assert retrieved.uri == "Hamish and Andy's Gap Year"
-
- store.close()
+ retrieved = await doc_repo.get_by_uri("Hamish and Andy's Gap Year")
+ assert retrieved is not None
+ assert retrieved.id == created_doc.id
+ assert retrieved.uri == "Hamish and Andy's Gap Year"
diff --git a/tests/test_download_models.py b/tests/test_download_models.py
index 448da403..92b20f04 100644
--- a/tests/test_download_models.py
+++ b/tests/test_download_models.py
@@ -4,13 +4,14 @@ from unittest.mock import AsyncMock, patch
import httpx
import pytest
-from haiku.rag.client import HaikuRAG
+from haiku.rag.client.downloads import download_models
+from haiku.rag.config import Config
@pytest.fixture
def mock_to_thread():
"""Patch asyncio.to_thread to skip docling/tokenizer downloads."""
- with patch("haiku.rag.client.asyncio.to_thread", new_callable=AsyncMock):
+ with patch("haiku.rag.client.downloads.asyncio.to_thread", new_callable=AsyncMock):
yield
@@ -22,79 +23,77 @@ async def _mock_httpx_client(stream_fn):
yield mock_client
-async def test_download_models_ollama_connect_error(temp_db_path, mock_to_thread):
+async def test_download_models_ollama_connect_error(mock_to_thread):
"""When Ollama is not running, download_models raises ConnectionError."""
- async with HaikuRAG(temp_db_path, create=True) as client:
- @asynccontextmanager
- async def failing_stream(method, url, **kwargs):
- raise httpx.ConnectError("All connection attempts failed")
- yield # unreachable, but needed for generator syntax
+ @asynccontextmanager
+ async def failing_stream(method, url, **kwargs):
+ raise httpx.ConnectError("All connection attempts failed")
+ yield # unreachable, but needed for generator syntax
- with patch(
- "haiku.rag.client.httpx.AsyncClient",
- return_value=_mock_httpx_client(failing_stream),
- ):
- with pytest.raises(
- ConnectionError, match="Cannot connect to Ollama"
- ) as exc_info:
- async for _ in client.download_models():
- pass
+ with patch(
+ "haiku.rag.client.downloads.httpx.AsyncClient",
+ return_value=_mock_httpx_client(failing_stream),
+ ):
+ with pytest.raises(
+ ConnectionError, match="Cannot connect to Ollama"
+ ) as exc_info:
+ async for _ in download_models(Config):
+ pass
- assert "ollama serve" in str(exc_info.value)
+ assert "ollama serve" in str(exc_info.value)
-async def test_download_models_ollama_pulls_models(temp_db_path, mock_to_thread):
+async def test_download_models_ollama_pulls_models(mock_to_thread):
"""download_models yields correct progress events for Ollama model pulls."""
- async with HaikuRAG(temp_db_path, create=True) as client:
- stream_lines = [
- '{"status": "pulling manifest"}',
- "",
- '{"status": "downloading", "digest": "sha256:abc", "total": 1000, "completed": 500}',
- '{"status": "downloading", "digest": "sha256:abc", "total": 1000, "completed": 1000}',
- "not valid json",
- '{"status": "verifying sha256 digest"}',
- '{"status": "writing manifest"}',
- '{"status": "success"}',
- ]
+ stream_lines = [
+ '{"status": "pulling manifest"}',
+ "",
+ '{"status": "downloading", "digest": "sha256:abc", "total": 1000, "completed": 500}',
+ '{"status": "downloading", "digest": "sha256:abc", "total": 1000, "completed": 1000}',
+ "not valid json",
+ '{"status": "verifying sha256 digest"}',
+ '{"status": "writing manifest"}',
+ '{"status": "success"}',
+ ]
- @asynccontextmanager
- async def mock_stream(method, url, **kwargs):
- mock_resp = AsyncMock()
+ @asynccontextmanager
+ async def mock_stream(method, url, **kwargs):
+ mock_resp = AsyncMock()
- async def aiter_lines():
- for line in stream_lines:
- yield line
+ async def aiter_lines():
+ for line in stream_lines:
+ yield line
- mock_resp.aiter_lines = aiter_lines
- yield mock_resp
+ mock_resp.aiter_lines = aiter_lines
+ yield mock_resp
- with patch(
- "haiku.rag.client.httpx.AsyncClient",
- return_value=_mock_httpx_client(mock_stream),
- ):
- events = []
- async for progress in client.download_models():
- events.append(progress)
+ with patch(
+ "haiku.rag.client.downloads.httpx.AsyncClient",
+ return_value=_mock_httpx_client(mock_stream),
+ ):
+ events = []
+ async for progress in download_models(Config):
+ events.append(progress)
- # Default config has embeddings=qwen3-embedding:4b, qa/research=gpt-oss
- ollama_models = {"gpt-oss", "qwen3-embedding:4b"}
- ollama_events = [e for e in events if e.model in ollama_models]
- pulling_events = [e for e in ollama_events if e.status == "pulling"]
- done_events = [e for e in ollama_events if e.status == "done"]
- download_events = [e for e in ollama_events if e.status == "downloading"]
+ # Default config has embeddings=qwen3-embedding:4b, qa/research=gpt-oss
+ ollama_models = {"gpt-oss", "qwen3-embedding:4b"}
+ ollama_events = [e for e in events if e.model in ollama_models]
+ pulling_events = [e for e in ollama_events if e.status == "pulling"]
+ done_events = [e for e in ollama_events if e.status == "done"]
+ download_events = [e for e in ollama_events if e.status == "downloading"]
- assert len(pulling_events) == 2
- assert len(done_events) == 2
- assert len(download_events) > 0
+ assert len(pulling_events) == 2
+ assert len(done_events) == 2
+ assert len(download_events) > 0
- for de in download_events:
- assert de.digest == "sha256:abc"
- assert de.total == 1000
- assert de.completed > 0
+ for de in download_events:
+ assert de.digest == "sha256:abc"
+ assert de.total == 1000
+ assert de.completed > 0
-async def test_download_models_no_ollama_models(temp_db_path, mock_to_thread):
+async def test_download_models_no_ollama_models(mock_to_thread):
"""When no Ollama models are configured, no Ollama pull events are yielded."""
from haiku.rag.config import AppConfig
@@ -103,10 +102,9 @@ async def test_download_models_no_ollama_models(temp_db_path, mock_to_thread):
config.qa.model.provider = "openai"
config.research.model.provider = "openai"
- async with HaikuRAG(temp_db_path, config=config, create=True) as client:
- events = []
- async for progress in client.download_models():
- events.append(progress)
+ events = []
+ async for progress in download_models(config):
+ events.append(progress)
models = {e.model for e in events}
assert "qwen3-embedding:4b" not in models
diff --git a/tests/test_filter.py b/tests/test_filter.py
index 1c54ad69..4fd96fbb 100644
--- a/tests/test_filter.py
+++ b/tests/test_filter.py
@@ -179,3 +179,45 @@ async def test_search_filter_with_all_search_types(temp_db_path):
for result in results:
assert result.document_uri is not None
assert "other.com" in result.document_uri
+
+
+@pytest.mark.vcr()
+async def test_search_with_filter_returns_full_limit(temp_db_path):
+ """Regression: filter + limit must return up to `limit` matching chunks
+ even when non-matching chunks would dominate the top-N window.
+
+ Previously the filter path materialized LanceDB's default top-N window
+ (~10), filtered to matching document_ids in pandas, then took `head(limit)`.
+ If the top-N window was dominated by non-matching chunks, the caller got
+ silently fewer results than requested — even when plenty of matching
+ chunks existed further down the ranking. This test puts the target
+ document behind many distractor documents and asserts we still get the
+ requested count back.
+ """
+ async with HaikuRAG(db_path=temp_db_path, create=True) as client:
+ for i in range(12):
+ await client.create_document(
+ content=(
+ "machine learning neural network deep learning model "
+ "machine learning neural network deep learning model "
+ "machine learning neural network deep learning model"
+ ),
+ uri=f"https://distractor.com/doc{i}.html",
+ title=f"Distractor {i}",
+ )
+
+ await client.create_document(
+ content="one passing mention of machine learning here",
+ uri="https://target.com/one.html",
+ title="Target One",
+ )
+
+ results = await client.search(
+ "machine learning",
+ limit=5,
+ search_type="fts",
+ filter="uri LIKE '%target.com%'",
+ )
+
+ assert len(results) == 1
+ assert results[0].document_uri == "https://target.com/one.html"
diff --git a/tests/test_info.py b/tests/test_info.py
index 0ef0dadc..42206fef 100644
--- a/tests/test_info.py
+++ b/tests/test_info.py
@@ -1,5 +1,5 @@
import json
-from unittest.mock import patch
+from unittest.mock import AsyncMock, MagicMock, patch
import pytest
@@ -15,7 +15,7 @@ async def test_app_info_outputs(temp_db_path, capsys):
from lancedb.pydantic import LanceModel, Vector
from pydantic import Field
- db = lancedb.connect(temp_db_path)
+ db = await lancedb.connect_async(temp_db_path)
class SettingsRecord(LanceModel):
id: str = Field(default="settings")
@@ -31,13 +31,13 @@ async def test_app_info_outputs(temp_db_path, capsys):
content: str
vector: Vector(3) # type: ignore
- settings_tbl = db.create_table("settings", schema=SettingsRecord)
- docs_tbl = db.create_table("documents", schema=DocumentRecord)
- chunks_tbl = db.create_table("chunks", schema=ChunkRecord)
- db.create_table("document_items", schema=DocumentItemRecord)
+ settings_tbl = await db.create_table("settings", schema=SettingsRecord)
+ docs_tbl = await db.create_table("documents", schema=DocumentRecord)
+ chunks_tbl = await db.create_table("chunks", schema=ChunkRecord)
+ await db.create_table("document_items", schema=DocumentItemRecord)
# Insert one of each - using the new config format
- settings_tbl.add(
+ await settings_tbl.add(
[
SettingsRecord(
id="settings",
@@ -56,8 +56,8 @@ async def test_app_info_outputs(temp_db_path, capsys):
)
]
)
- docs_tbl.add([DocumentRecord(id="doc-1", content="hello")])
- chunks_tbl.add(
+ await docs_tbl.add([DocumentRecord(id="doc-1", content="hello")])
+ await chunks_tbl.add(
[ChunkRecord(id="c1", document_id="doc-1", content="c", vector=[0.1, 0.2, 0.3])]
)
@@ -68,7 +68,9 @@ async def test_app_info_outputs(temp_db_path, capsys):
# Validate expected content substrings
# Note: Rich console may wrap long paths to new lines, so check separately
assert "path:" in out
- assert str(temp_db_path) in out
+ # Rich may wrap long paths across lines — check with newlines stripped
+ out_no_wrap = out.replace("\n", "")
+ assert str(temp_db_path) in out_no_wrap
assert "haiku.rag version (db):" in out
assert "embeddings: openai/text-embedding-3-small (dim: 3)" in out
assert "documents: 1" in out
@@ -93,10 +95,11 @@ async def test_app_info_outputs(temp_db_path, capsys):
async def test_app_info_with_vector_index(temp_db_path, capsys):
# Build a database with enough chunks to create a vector index
import lancedb
+ from lancedb.index import IvfPq
from lancedb.pydantic import LanceModel, Vector
from pydantic import Field
- db = lancedb.connect(temp_db_path)
+ db = await lancedb.connect_async(temp_db_path)
class SettingsRecord(LanceModel):
id: str = Field(default="settings")
@@ -112,13 +115,13 @@ async def test_app_info_with_vector_index(temp_db_path, capsys):
content: str
vector: Vector(3) # type: ignore
- settings_tbl = db.create_table("settings", schema=SettingsRecord)
- docs_tbl = db.create_table("documents", schema=DocumentRecord)
- chunks_tbl = db.create_table("chunks", schema=ChunkRecord)
- db.create_table("document_items", schema=DocumentItemRecord)
+ settings_tbl = await db.create_table("settings", schema=SettingsRecord)
+ docs_tbl = await db.create_table("documents", schema=DocumentRecord)
+ chunks_tbl = await db.create_table("chunks", schema=ChunkRecord)
+ await db.create_table("document_items", schema=DocumentItemRecord)
# Insert settings
- settings_tbl.add(
+ await settings_tbl.add(
[
SettingsRecord(
id="settings",
@@ -128,7 +131,7 @@ async def test_app_info_with_vector_index(temp_db_path, capsys):
)
# Insert document
- docs_tbl.add([DocumentRecord(id="doc-1", content="test")])
+ await docs_tbl.add([DocumentRecord(id="doc-1", content="test")])
# Insert 512 chunks to allow index creation (PQ needs more than 256 for training)
chunks = [
@@ -140,10 +143,10 @@ async def test_app_info_with_vector_index(temp_db_path, capsys):
)
for i in range(512)
]
- chunks_tbl.add(chunks)
+ await chunks_tbl.add(chunks)
# Create vector index
- chunks_tbl.create_index(metric="cosine", index_type="IVF_PQ")
+ await chunks_tbl.create_index("vector", config=IvfPq(distance_type="cosine"))
app = HaikuRAGApp(db_path=temp_db_path)
await app.info()
@@ -172,9 +175,14 @@ async def test_app_info_uses_connect_lancedb_for_remote(tmp_path):
)
app = HaikuRAGApp(db_path=nonexistent, config=config)
- with patch("haiku.rag.store.engine.connect_lancedb") as mock_connect:
+ with patch(
+ "haiku.rag.store.engine.connect_lancedb", new_callable=AsyncMock
+ ) as mock_connect:
# Empty DB triggers the early-return path - enough to prove connect_lancedb was used
- mock_connect.return_value.list_tables.return_value.tables = []
+ mock_db = mock_connect.return_value
+ mock_list_result = MagicMock()
+ mock_list_result.tables = []
+ mock_db.list_tables = AsyncMock(return_value=mock_list_result)
await app.info()
mock_connect.assert_called_once_with(config, nonexistent)
@@ -188,7 +196,7 @@ async def test_app_info_with_missing_document_items_table(temp_db_path, capsys):
from lancedb.pydantic import LanceModel, Vector
from pydantic import Field
- db = lancedb.connect(temp_db_path)
+ db = await lancedb.connect_async(temp_db_path)
class SettingsRecord(LanceModel):
id: str = Field(default="settings")
@@ -204,12 +212,12 @@ async def test_app_info_with_missing_document_items_table(temp_db_path, capsys):
content: str
vector: Vector(3) # type: ignore
- settings_tbl = db.create_table("settings", schema=SettingsRecord)
- docs_tbl = db.create_table("documents", schema=DocumentRecord)
- chunks_tbl = db.create_table("chunks", schema=ChunkRecord)
+ settings_tbl = await db.create_table("settings", schema=SettingsRecord)
+ docs_tbl = await db.create_table("documents", schema=DocumentRecord)
+ chunks_tbl = await db.create_table("chunks", schema=ChunkRecord)
# Intentionally omit document_items (added in 0.40.0)
- settings_tbl.add(
+ await settings_tbl.add(
[
SettingsRecord(
id="settings",
@@ -228,8 +236,8 @@ async def test_app_info_with_missing_document_items_table(temp_db_path, capsys):
)
]
)
- docs_tbl.add([DocumentRecord(id="doc-1", content="hello")])
- chunks_tbl.add(
+ await docs_tbl.add([DocumentRecord(id="doc-1", content="hello")])
+ await chunks_tbl.add(
[ChunkRecord(id="c1", document_id="doc-1", content="c", vector=[0.1, 0.2, 0.3])]
)
@@ -261,7 +269,7 @@ async def test_app_info_reports_up_to_date(temp_db_path, capsys):
from lancedb.pydantic import LanceModel, Vector
from pydantic import Field
- db = lancedb.connect(temp_db_path)
+ db = await lancedb.connect_async(temp_db_path)
class SettingsRecord(LanceModel):
id: str = Field(default="settings")
@@ -277,13 +285,13 @@ async def test_app_info_reports_up_to_date(temp_db_path, capsys):
content: str
vector: Vector(3) # type: ignore
- settings_tbl = db.create_table("settings", schema=SettingsRecord)
- db.create_table("documents", schema=DocumentRecord)
- db.create_table("chunks", schema=ChunkRecord)
- db.create_table("document_items", schema=DocumentItemRecord)
+ settings_tbl = await db.create_table("settings", schema=SettingsRecord)
+ await db.create_table("documents", schema=DocumentRecord)
+ await db.create_table("chunks", schema=ChunkRecord)
+ await db.create_table("document_items", schema=DocumentItemRecord)
current_version = metadata.version("haiku.rag-slim")
- settings_tbl.add(
+ await settings_tbl.add(
[
SettingsRecord(
id="settings",
@@ -324,6 +332,9 @@ async def test_app_init_skips_exists_check_for_remote(tmp_path):
app = HaikuRAGApp(db_path=nonexistent, config=config)
with patch("haiku.rag.app.HaikuRAG") as mock_client_cls:
+ mock_client = AsyncMock()
+ mock_client_cls.return_value.__aenter__ = AsyncMock(return_value=mock_client)
+ mock_client_cls.return_value.__aexit__ = AsyncMock(return_value=False)
await app.init()
# Should have called HaikuRAG to create, not returned early
mock_client_cls.assert_called_once()
@@ -342,9 +353,9 @@ async def test_app_history_skips_exists_check_for_remote(tmp_path):
app = HaikuRAGApp(db_path=nonexistent, config=config)
with patch("haiku.rag.store.engine.Store") as mock_store_cls:
- mock_store = mock_store_cls.return_value
- mock_store.documents_table.list_versions.return_value = []
- mock_store.chunks_table.list_versions.return_value = []
- mock_store.settings_table.list_versions.return_value = []
+ mock_store = AsyncMock()
+ mock_store.list_table_versions = AsyncMock(return_value=[])
+ mock_store_cls.return_value.__aenter__ = AsyncMock(return_value=mock_store)
+ mock_store_cls.return_value.__aexit__ = AsyncMock(return_value=False)
await app.history()
mock_store_cls.assert_called_once()
diff --git a/tests/test_lancedb_connection.py b/tests/test_lancedb_connection.py
index a181b1f1..39301411 100644
--- a/tests/test_lancedb_connection.py
+++ b/tests/test_lancedb_connection.py
@@ -1,4 +1,4 @@
-from unittest.mock import patch
+from unittest.mock import AsyncMock, patch
import pytest
@@ -42,25 +42,32 @@ class TestConnectionMode:
class TestConnectLancedb:
- def test_local_passes_db_path(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_local_passes_db_path(self, temp_db_path):
config = AppConfig(lancedb=LanceDBConfig(uri=""))
- with patch("haiku.rag.store.engine.lancedb.connect") as mock_connect:
- connect_lancedb(config, db_path=temp_db_path)
+ with patch(
+ "haiku.rag.store.engine.lancedb.connect_async", new_callable=AsyncMock
+ ) as mock_connect:
+ await connect_lancedb(config, db_path=temp_db_path)
mock_connect.assert_called_once_with(temp_db_path)
- def test_cloud_passes_uri_api_key_region(self):
+ @pytest.mark.asyncio
+ async def test_cloud_passes_uri_api_key_region(self):
config = AppConfig(
lancedb=LanceDBConfig(
uri="db://my-database", api_key="test-key", region="us-west-2"
)
)
- with patch("haiku.rag.store.engine.lancedb.connect") as mock_connect:
- connect_lancedb(config)
+ with patch(
+ "haiku.rag.store.engine.lancedb.connect_async", new_callable=AsyncMock
+ ) as mock_connect:
+ await connect_lancedb(config)
mock_connect.assert_called_once_with(
uri="db://my-database", api_key="test-key", region="us-west-2"
)
- def test_object_storage_passes_uri_and_storage_options(self):
+ @pytest.mark.asyncio
+ async def test_object_storage_passes_uri_and_storage_options(self):
config = AppConfig(
lancedb=LanceDBConfig(
uri="s3://bucket/path",
@@ -70,8 +77,10 @@ class TestConnectLancedb:
},
)
)
- with patch("haiku.rag.store.engine.lancedb.connect") as mock_connect:
- connect_lancedb(config)
+ with patch(
+ "haiku.rag.store.engine.lancedb.connect_async", new_callable=AsyncMock
+ ) as mock_connect:
+ await connect_lancedb(config)
mock_connect.assert_called_once_with(
uri="s3://bucket/path",
storage_options={
@@ -80,120 +89,136 @@ class TestConnectLancedb:
},
)
- def test_object_storage_without_storage_options(self):
+ @pytest.mark.asyncio
+ async def test_object_storage_without_storage_options(self):
config = AppConfig(lancedb=LanceDBConfig(uri="s3://bucket/path"))
- with patch("haiku.rag.store.engine.lancedb.connect") as mock_connect:
- connect_lancedb(config)
+ with patch(
+ "haiku.rag.store.engine.lancedb.connect_async", new_callable=AsyncMock
+ ) as mock_connect:
+ await connect_lancedb(config)
mock_connect.assert_called_once_with(uri="s3://bucket/path")
- def test_local_without_db_path_raises(self):
+ @pytest.mark.asyncio
+ async def test_local_without_db_path_raises(self):
config = AppConfig(lancedb=LanceDBConfig(uri=""))
with pytest.raises(
ValueError, match="No lancedb.uri configured and no db_path provided"
):
- connect_lancedb(config)
+ await connect_lancedb(config)
class TestStoreConnectionMode:
- def test_store_connection_mode_local(self, temp_db_path):
- store = Store(temp_db_path, create=True)
- assert store._connection_mode == ConnectionMode.LOCAL
- store.close()
+ @pytest.mark.asyncio
+ async def test_store_connection_mode_local(self, temp_db_path):
+ async with Store(temp_db_path, create=True) as store:
+ assert store._connection_mode == ConnectionMode.LOCAL
- def test_store_connection_mode_cloud(self, temp_db_path):
- store = Store(temp_db_path, create=True)
- with (
- patch.object(Config.lancedb, "uri", "db://test-database"),
- patch.object(Config.lancedb, "api_key", "test-api-key"),
- patch.object(Config.lancedb, "region", "us-east-1"),
- ):
- assert store._connection_mode == ConnectionMode.CLOUD
- store.close()
+ @pytest.mark.asyncio
+ async def test_store_connection_mode_cloud(self, temp_db_path):
+ async with Store(temp_db_path, create=True) as store:
+ with (
+ patch.object(Config.lancedb, "uri", "db://test-database"),
+ patch.object(Config.lancedb, "api_key", "test-api-key"),
+ patch.object(Config.lancedb, "region", "us-east-1"),
+ ):
+ assert store._connection_mode == ConnectionMode.CLOUD
- def test_store_connection_mode_object_storage(self, temp_db_path):
- store = Store(temp_db_path, create=True)
- with patch.object(Config.lancedb, "uri", "s3://bucket/path"):
- assert store._connection_mode == ConnectionMode.OBJECT_STORAGE
- store.close()
+ @pytest.mark.asyncio
+ async def test_store_connection_mode_object_storage(self, temp_db_path):
+ async with Store(temp_db_path, create=True) as store:
+ with patch.object(Config.lancedb, "uri", "s3://bucket/path"):
+ assert store._connection_mode == ConnectionMode.OBJECT_STORAGE
class TestVacuumByConnectionMode:
@pytest.mark.asyncio
async def test_cloud_skips_vacuum(self, temp_db_path):
- store = Store(temp_db_path, create=True)
- with (
- patch.object(Config.lancedb, "uri", "db://test-database"),
- patch.object(Config.lancedb, "api_key", "test-api-key"),
- patch.object(Config.lancedb, "region", "us-east-1"),
- ):
- with patch.object(store.chunks_table, "optimize") as mock_optimize:
- await store.vacuum()
- mock_optimize.assert_not_called()
- store.close()
+ async with Store(temp_db_path, create=True) as store:
+ with (
+ patch.object(Config.lancedb, "uri", "db://test-database"),
+ patch.object(Config.lancedb, "api_key", "test-api-key"),
+ patch.object(Config.lancedb, "region", "us-east-1"),
+ ):
+ with patch.object(
+ store.chunks_table, "optimize", new_callable=AsyncMock
+ ) as mock_optimize:
+ await store.vacuum()
+ mock_optimize.assert_not_called()
@pytest.mark.asyncio
async def test_object_storage_runs_vacuum(self, temp_db_path):
- store = Store(temp_db_path, create=True)
- with patch.object(Config.lancedb, "uri", "s3://bucket/path"):
- with patch.object(store.chunks_table, "optimize") as mock_optimize:
- await store.vacuum()
- mock_optimize.assert_called()
- store.close()
+ async with Store(temp_db_path, create=True) as store:
+ with patch.object(Config.lancedb, "uri", "s3://bucket/path"):
+ with patch.object(
+ store.chunks_table, "optimize", new_callable=AsyncMock
+ ) as mock_optimize:
+ await store.vacuum()
+ mock_optimize.assert_called()
@pytest.mark.asyncio
async def test_local_runs_vacuum(self, temp_db_path):
- store = Store(temp_db_path, create=True)
- with patch.object(Config.lancedb, "uri", ""):
- with patch.object(store.chunks_table, "optimize") as mock_optimize:
- await store.vacuum()
- mock_optimize.assert_called()
- store.close()
+ async with Store(temp_db_path, create=True) as store:
+ with patch.object(Config.lancedb, "uri", ""):
+ with patch.object(
+ store.chunks_table, "optimize", new_callable=AsyncMock
+ ) as mock_optimize:
+ await store.vacuum()
+ mock_optimize.assert_called()
class TestVectorIndexByConnectionMode:
- def test_cloud_skips_index_creation(self, temp_db_path):
- store = Store(temp_db_path, create=True)
- with (
- patch.object(Config.lancedb, "uri", "db://test-database"),
- patch.object(Config.lancedb, "api_key", "test-api-key"),
- patch.object(Config.lancedb, "region", "us-east-1"),
- ):
- with patch.object(store.chunks_table, "count_rows") as mock_count:
- store._ensure_vector_index()
- mock_count.assert_not_called()
- store.close()
+ @pytest.mark.asyncio
+ async def test_cloud_skips_index_creation(self, temp_db_path):
+ async with Store(temp_db_path, create=True) as store:
+ with (
+ patch.object(Config.lancedb, "uri", "db://test-database"),
+ patch.object(Config.lancedb, "api_key", "test-api-key"),
+ patch.object(Config.lancedb, "region", "us-east-1"),
+ ):
+ with patch.object(
+ store.chunks_table, "count_rows", new_callable=AsyncMock
+ ) as mock_count:
+ await store._ensure_vector_index()
+ mock_count.assert_not_called()
- def test_object_storage_runs_index_creation(self, temp_db_path):
- store = Store(temp_db_path, create=True)
- with patch.object(Config.lancedb, "uri", "s3://bucket/path"):
- with patch.object(
- store.chunks_table, "count_rows", return_value=0
- ) as mock_count:
- store._ensure_vector_index()
- mock_count.assert_called()
- store.close()
+ @pytest.mark.asyncio
+ async def test_object_storage_runs_index_creation(self, temp_db_path):
+ async with Store(temp_db_path, create=True) as store:
+ with patch.object(Config.lancedb, "uri", "s3://bucket/path"):
+ with patch.object(
+ store.chunks_table,
+ "count_rows",
+ new_callable=AsyncMock,
+ return_value=0,
+ ) as mock_count:
+ await store._ensure_vector_index()
+ mock_count.assert_called()
class TestStoreSkipsPathValidationForRemote:
- def test_skips_path_check_for_cloud(self, tmp_path):
+ @pytest.mark.asyncio
+ async def test_skips_path_check_for_cloud(self, tmp_path):
nonexistent = tmp_path / "does_not_exist" / "db.lancedb"
config = AppConfig(
lancedb=LanceDBConfig(
uri="db://test-database", api_key="key", region="us-east-1"
)
)
- with patch("haiku.rag.store.engine.lancedb.connect"):
- with patch.object(Store, "_init_tables"):
- store = Store(
+ with patch(
+ "haiku.rag.store.engine.lancedb.connect_async", new_callable=AsyncMock
+ ):
+ with patch.object(Store, "_init_tables", new_callable=AsyncMock):
+ async with Store(
nonexistent,
config=config,
create=True,
skip_validation=True,
skip_migration_check=True,
- )
- store.close()
+ ) as store:
+ assert store is not None
- def test_skips_path_check_for_object_storage(self, tmp_path):
+ @pytest.mark.asyncio
+ async def test_skips_path_check_for_object_storage(self, tmp_path):
nonexistent = tmp_path / "does_not_exist" / "db.lancedb"
config = AppConfig(
lancedb=LanceDBConfig(
@@ -201,13 +226,74 @@ class TestStoreSkipsPathValidationForRemote:
storage_options={"endpoint": "http://localhost:9000"},
)
)
- with patch("haiku.rag.store.engine.lancedb.connect"):
- with patch.object(Store, "_init_tables"):
- store = Store(
+ with patch(
+ "haiku.rag.store.engine.lancedb.connect_async", new_callable=AsyncMock
+ ):
+ with patch.object(Store, "_init_tables", new_callable=AsyncMock):
+ async with Store(
nonexistent,
config=config,
create=True,
skip_validation=True,
skip_migration_check=True,
- )
- store.close()
+ ) as store:
+ assert store is not None
+
+
+class TestInitFailureCleanup:
+ @pytest.mark.asyncio
+ async def test_store_aenter_closes_connection_on_init_failure(
+ self, temp_db_path, monkeypatch
+ ):
+ """If _initialize raises after connect, __aenter__ must close the
+ AsyncConnection so it doesn't leak (no __aexit__ runs in that case)."""
+ mock_conn = AsyncMock()
+ mock_conn.close = lambda: mock_conn.close_calls.append(True) # type: ignore[attr-defined]
+ mock_conn.close_calls = [] # type: ignore[attr-defined]
+
+ async def fake_connect(*args, **kwargs):
+ return mock_conn
+
+ async def failing_init_tables(self):
+ raise RuntimeError("simulated table init failure")
+
+ monkeypatch.setattr("haiku.rag.store.engine.connect_lancedb", fake_connect)
+ monkeypatch.setattr(Store, "_init_tables", failing_init_tables)
+
+ with pytest.raises(RuntimeError, match="simulated table init failure"):
+ async with Store(temp_db_path, create=True) as store:
+ assert store is not None
+
+ assert mock_conn.close_calls == [True], (
+ "AsyncConnection.close() was not called on init failure"
+ )
+
+ @pytest.mark.asyncio
+ async def test_client_aenter_closes_store_on_init_failure(
+ self, temp_db_path, monkeypatch
+ ):
+ """HaikuRAG.__aenter__ must close the store if _initialize fails."""
+ from haiku.rag.client import HaikuRAG
+
+ close_calls: list[bool] = []
+
+ original_close = Store.close
+
+ def tracking_close(self):
+ close_calls.append(True)
+ original_close(self)
+
+ async def failing_init(self):
+ # Set db so close() has something to close
+ self.db = AsyncMock()
+ self.db.close = lambda: None
+ raise RuntimeError("simulated initialize failure")
+
+ monkeypatch.setattr(Store, "_initialize", failing_init)
+ monkeypatch.setattr(Store, "close", tracking_close)
+
+ with pytest.raises(RuntimeError, match="simulated initialize failure"):
+ async with HaikuRAG(temp_db_path, create=True):
+ pass
+
+ assert close_calls, "Store.close() was not called when _initialize raised"
diff --git a/tests/test_rebuild.py b/tests/test_rebuild.py
index 43e56304..d283c06f 100644
--- a/tests/test_rebuild.py
+++ b/tests/test_rebuild.py
@@ -1,3 +1,5 @@
+import tempfile
+from pathlib import Path
from typing import TypedDict
import pytest
@@ -89,8 +91,8 @@ async def test_rebuild_embed_only_skips_unchanged(qa_corpus: Dataset, temp_db_pa
assert doc.id is not None
# Get embeddings before rebuild
- records_before = list(
- client.store.chunks_table.search()
+ records_before = await (
+ client.store.chunks_table.query()
.where(f"document_id = '{doc.id}'")
.to_pydantic(client.store.ChunkRecord)
)
@@ -104,8 +106,8 @@ async def test_rebuild_embed_only_skips_unchanged(qa_corpus: Dataset, temp_db_pa
assert doc.id in processed_ids
# Get embeddings after rebuild
- records_after = list(
- client.store.chunks_table.search()
+ records_after = await (
+ client.store.chunks_table.query()
.where(f"document_id = '{doc.id}'")
.to_pydantic(client.store.ChunkRecord)
)
@@ -156,7 +158,7 @@ async def test_rebuild_embed_only_with_changed_vector_dim(
]
# Step 2: Manually recreate chunks table with 4096-dim vectors (simulating old DB)
- db = lancedb.connect(temp_db_path)
+ db = await lancedb.connect_async(temp_db_path)
class ChunkRecord4096(LanceModel):
id: str
@@ -167,8 +169,8 @@ async def test_rebuild_embed_only_with_changed_vector_dim(
order: int = Field(default=0)
vector: Vector(4096) = Field(default_factory=lambda: [0.0] * 4096) # type: ignore
- db.drop_table("chunks")
- chunks_table = db.create_table("chunks", schema=ChunkRecord4096)
+ await db.drop_table("chunks")
+ chunks_table = await db.create_table("chunks", schema=ChunkRecord4096)
# Insert chunks with 4096-dim fake vectors
records_4096 = [
@@ -183,19 +185,20 @@ async def test_rebuild_embed_only_with_changed_vector_dim(
)
for c in chunk_data
]
- chunks_table.add(records_4096)
+ await chunks_table.add(records_4096)
# Update settings to reflect the 4096-dim model used
- settings_table = db.open_table("settings")
+ settings_table = await db.open_table("settings")
rows = (
- settings_table.search().where("id = 'settings'").limit(1).to_arrow().to_pylist()
- )
+ await settings_table.query().where("id = 'settings'").limit(1).to_arrow()
+ ).to_pylist()
settings = json.loads(rows[0]["settings"])
settings["embeddings"]["model"]["vector_dim"] = 4096
settings["embeddings"]["model"]["name"] = "qwen3-embedding:8b"
- settings_table.update(
- where="id = 'settings'", values={"settings": json.dumps(settings)}
+ await settings_table.update(
+ {"settings": json.dumps(settings)}, where="id = 'settings'"
)
+ db.close()
# Step 3: Open with skip_validation (different config) and run embed-only rebuild
# This should work: Store should use stored vector_dim for reading,
@@ -213,11 +216,10 @@ async def test_rebuild_embed_only_with_changed_vector_dim(
# Check that embeddings in DB are now 2560-dim
raw_chunks = (
- client.store.chunks_table.search()
+ await client.store.chunks_table.query()
.where(f"document_id = '{doc.id}'")
.to_arrow()
- .to_pylist()
- )
+ ).to_pylist()
for raw_chunk in raw_chunks:
assert len(raw_chunk["vector"]) == 2560
@@ -263,3 +265,163 @@ async def test_rebuild_rechunk(qa_corpus: Dataset, temp_db_path):
# Chunk IDs should change (chunks are recreated)
assert chunk_ids_before.isdisjoint(chunk_ids_after)
+
+
+@pytest.mark.vcr()
+async def test_rebuild_full_with_accessible_source(temp_db_path):
+ """FULL rebuild re-ingests from source when the URI is accessible.
+
+ Covers the main path in _rebuild_full (source-accessible branch): the
+ document is deleted and re-created from its URI, producing a new ID.
+ """
+ async with HaikuRAG(temp_db_path, create=True) as client:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ source_path = Path(temp_dir) / "source.txt"
+ source_path.write_text("Fresh content from an accessible file source.")
+
+ original = await client.create_document_from_source(source=source_path)
+ assert not isinstance(original, list)
+ assert original.id is not None
+ original_id = original.id
+
+ processed_ids = [
+ doc_id
+ async for doc_id in client.rebuild_database(mode=RebuildMode.FULL)
+ ]
+
+ # Original doc was deleted and a new one created; the old ID
+ # must not appear, and exactly one new ID must have been yielded.
+ assert original_id not in processed_ids
+ assert len(processed_ids) == 1
+
+ new_doc = await client.get_document_by_id(processed_ids[0])
+ assert new_doc is not None
+ assert new_doc.uri == source_path.as_uri()
+ assert "Fresh content" in new_doc.content
+
+
+async def test_rebuild_title_only_handles_llm_failure(temp_db_path, monkeypatch):
+ """TITLE_ONLY: a failure on one document does not abort the generator.
+
+ The first document raises during title generation (simulated LLM error);
+ the second succeeds. Rebuild must log-and-skip the failure, yield only
+ the successful document, and persist its new title.
+ """
+ from haiku.rag.store.models.document import Document
+
+ async with HaikuRAG(temp_db_path, create=True) as client:
+ # Skip embedding — TITLE_ONLY only touches documents.
+ doc1 = await client.document_repository.create(
+ Document(content="doc one body", metadata={})
+ )
+ doc2 = await client.document_repository.create(
+ Document(content="doc two body", metadata={})
+ )
+ assert doc1.id is not None and doc2.id is not None
+
+ async def fake_generate_title(doc):
+ if doc.id == doc1.id:
+ raise RuntimeError("simulated LLM failure")
+ return "Second Title"
+
+ monkeypatch.setattr(client, "generate_title", fake_generate_title)
+
+ processed_ids = [
+ doc_id
+ async for doc_id in client.rebuild_database(mode=RebuildMode.TITLE_ONLY)
+ ]
+
+ assert processed_ids == [doc2.id]
+
+ refreshed = await client.get_document_by_id(doc2.id)
+ assert refreshed is not None
+ assert refreshed.title == "Second Title"
+
+ untouched = await client.get_document_by_id(doc1.id)
+ assert untouched is not None
+ assert untouched.title is None
+
+
+@pytest.mark.vcr()
+async def test_rebuild_full_source_failure_is_logged_and_skipped(
+ temp_db_path, monkeypatch
+):
+ """FULL rebuild logs-and-continues when re-ingesting from source fails.
+
+ Covers _rebuild_full's `except Exception` branch: when
+ create_document_from_source raises, the doc is skipped (no yield) and
+ the error is logged. Regression guard against silent failures.
+ """
+ import logging
+
+ from haiku.rag.client import rebuild as rebuild_module
+
+ async with HaikuRAG(temp_db_path, create=True) as client:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ source_path = Path(temp_dir) / "source.txt"
+ source_path.write_text("Content that will vanish by rebuild time.")
+
+ original = await client.create_document_from_source(source=source_path)
+ assert not isinstance(original, list)
+ assert original.id is not None
+
+ # Force the source rebuild branch to raise.
+ async def failing_create(*args, **kwargs):
+ raise RuntimeError("simulated ingestion failure")
+
+ monkeypatch.setattr(client, "create_document_from_source", failing_create)
+
+ # Attach directly to the rebuild module's logger rather than
+ # relying on caplog — `haiku.rag.logging.get_logger()` (invoked
+ # by other tests) sets `propagate=False` on the `haiku.rag`
+ # logger, which breaks caplog under xdist ordering.
+ records: list[logging.LogRecord] = []
+
+ class _ListHandler(logging.Handler):
+ def emit(self, record: logging.LogRecord) -> None:
+ records.append(record)
+
+ handler = _ListHandler(level=logging.ERROR)
+ rebuild_module.logger.addHandler(handler)
+ try:
+ processed_ids = [
+ doc_id
+ async for doc_id in client.rebuild_database(mode=RebuildMode.FULL)
+ ]
+ finally:
+ rebuild_module.logger.removeHandler(handler)
+
+ assert processed_ids == []
+ assert any(
+ "Error recreating document from source" in rec.getMessage()
+ for rec in records
+ )
+
+
+@pytest.mark.vcr()
+async def test_rebuild_batch_size_flush(temp_db_path, monkeypatch):
+ """RECHUNK flushes in batches and yields every document.
+
+ Forces a tiny batch size so three docs trigger at least one mid-loop
+ flush plus the final flush. Regression guard for the batched-write path
+ in _rebuild_rechunk.
+ """
+ from haiku.rag.client import rebuild as rebuild_module
+
+ monkeypatch.setattr(rebuild_module, "_REBUILD_BATCH_SIZE", 2)
+
+ async with HaikuRAG(temp_db_path, create=True) as client:
+ ids: list[str] = []
+ for i in range(3):
+ doc = await client.create_document(content=f"batch flush doc {i}")
+ assert doc.id is not None
+ ids.append(doc.id)
+
+ processed = [
+ doc_id async for doc_id in client.rebuild_database(mode=RebuildMode.RECHUNK)
+ ]
+
+ assert sorted(processed) == sorted(ids)
+ for doc_id in ids:
+ chunks = await client.chunk_repository.get_by_document_id(doc_id)
+ assert len(chunks) > 0
diff --git a/tests/test_s3_integration.py b/tests/test_s3_integration.py
index ba482949..c13cf029 100644
--- a/tests/test_s3_integration.py
+++ b/tests/test_s3_integration.py
@@ -51,37 +51,35 @@ def _make_config() -> AppConfig:
)
-def test_store_connect_and_create(tmp_path):
+@pytest.mark.asyncio
+async def test_store_connect_and_create(tmp_path):
from haiku.rag.store.engine import get_database_stats
config = _make_config()
- store = Store(tmp_path / "unused", config=config, create=True)
- stats = get_database_stats(store.db)
- assert stats["documents"]["exists"]
- assert stats["chunks"]["exists"]
- store.close()
+ async with Store(tmp_path / "unused", config=config, create=True) as store:
+ stats = await get_database_stats(store.db)
+ assert stats["documents"]["exists"]
+ assert stats["chunks"]["exists"]
@pytest.mark.asyncio
async def test_store_vacuum(tmp_path):
config = _make_config()
- store = Store(tmp_path / "unused", config=config, create=True)
- await store.vacuum()
- store.close()
+ async with Store(tmp_path / "unused", config=config, create=True) as store:
+ await store.vacuum()
-def test_store_add_document(tmp_path):
+@pytest.mark.asyncio
+async def test_store_add_document(tmp_path):
from haiku.rag.store.engine import DocumentRecord, get_database_stats
config = _make_config()
- store = Store(tmp_path / "unused", config=config, create=True)
+ async with Store(tmp_path / "unused", config=config, create=True) as store:
+ doc = DocumentRecord(content="The quick brown fox jumps over the lazy dog.")
+ await store.documents_table.add([doc])
- doc = DocumentRecord(content="The quick brown fox jumps over the lazy dog.")
- store.documents_table.add([doc])
-
- stats = get_database_stats(store.db)
- assert stats["documents"]["num_rows"] == 1
- store.close()
+ stats = await get_database_stats(store.db)
+ assert stats["documents"]["num_rows"] == 1
@pytest.mark.asyncio
diff --git a/tests/test_search.py b/tests/test_search.py
index d684d2c8..d235bfb1 100644
--- a/tests/test_search.py
+++ b/tests/test_search.py
@@ -9,187 +9,179 @@ from haiku.rag.store.models import SearchResult
@pytest.mark.vcr()
async def test_search_qa_corpus(qa_corpus: Dataset, temp_db_path):
"""Test that documents can be found by searching with their associated questions."""
- # Create client
- client = HaikuRAG(db_path=temp_db_path, config=Config, create=True)
+ async with HaikuRAG(db_path=temp_db_path, config=Config, create=True) as client:
+ # Load unique documents (limited to 10)
+ seen_documents = set()
+ documents = []
- # Load unique documents (limited to 10)
- seen_documents = set()
- documents = []
+ for doc_data in qa_corpus:
+ if len(seen_documents) >= 10:
+ break
+ document_text = doc_data["document_extracted"]
+ document_id = doc_data.get("document_id", "")
- for doc_data in qa_corpus:
- if len(seen_documents) >= 10:
- break
- document_text = doc_data["document_extracted"]
- document_id = doc_data.get("document_id", "")
+ if document_id in seen_documents:
+ continue
+ seen_documents.add(document_id)
- if document_id in seen_documents:
- continue
- seen_documents.add(document_id)
+ # Create the document with chunks and embeddings
+ created_document = await client.create_document(content=document_text)
+ documents.append((created_document, doc_data))
- # Create the document with chunks and embeddings
- created_document = await client.create_document(content=document_text)
- documents.append((created_document, doc_data))
+ # Test with first few unique documents
- # Test with first few unique documents
+ for target_document, doc_data in documents:
+ question = doc_data["question"]
- for target_document, doc_data in documents:
- question = doc_data["question"]
+ # Test vector search (limit=10 to accommodate different embedding models)
+ vector_results = await client.chunk_repository.search(
+ question, limit=10, search_type="vector"
+ )
+ target_document_ids = {chunk.document_id for chunk, _ in vector_results}
+ assert target_document.id in target_document_ids
- # Test vector search (limit=10 to accommodate different embedding models)
- vector_results = await client.chunk_repository.search(
- question, limit=10, search_type="vector"
- )
- target_document_ids = {chunk.document_id for chunk, _ in vector_results}
- assert target_document.id in target_document_ids
+ # Test FTS search
+ fts_results = await client.chunk_repository.search(
+ question, limit=10, search_type="fts"
+ )
+ target_document_ids = {chunk.document_id for chunk, _ in fts_results}
+ assert target_document.id in target_document_ids
- # Test FTS search
- fts_results = await client.chunk_repository.search(
- question, limit=10, search_type="fts"
- )
- target_document_ids = {chunk.document_id for chunk, _ in fts_results}
- assert target_document.id in target_document_ids
-
- # Test hybrid search
- hybrid_results = await client.chunk_repository.search(
- question, limit=10, search_type="hybrid"
- )
- target_document_ids = {chunk.document_id for chunk, _ in hybrid_results}
- assert target_document.id in target_document_ids
-
- client.close()
+ # Test hybrid search
+ hybrid_results = await client.chunk_repository.search(
+ question, limit=10, search_type="hybrid"
+ )
+ target_document_ids = {chunk.document_id for chunk, _ in hybrid_results}
+ assert target_document.id in target_document_ids
@pytest.mark.vcr()
async def test_search_chunk_includes_document_provenance(temp_db_path):
"""Test that raw chunk search results include document URI, metadata, and ID."""
- client = HaikuRAG(db_path=temp_db_path, config=Config, create=True)
+ async with HaikuRAG(db_path=temp_db_path, config=Config, create=True) as client:
+ # Create a document with URI and metadata but no title
+ created_document = await client.create_document(
+ content="This is a test document with some content for searching.",
+ uri="https://example.com/test.html",
+ metadata={"title": "Test Document", "author": "Test Author"},
+ )
- # Create a document with URI and metadata but no title
- created_document = await client.create_document(
- content="This is a test document with some content for searching.",
- uri="https://example.com/test.html",
- metadata={"title": "Test Document", "author": "Test Author"},
- )
+ # Search for chunks
+ results = await client.chunk_repository.search(
+ "test document", limit=1, search_type="hybrid"
+ )
- # Search for chunks
- results = await client.chunk_repository.search(
- "test document", limit=1, search_type="hybrid"
- )
+ assert len(results) > 0
+ chunk, score = results[0]
- assert len(results) > 0
- chunk, score = results[0]
+ # Test that score is valid
+ assert isinstance(score, int | float), (
+ f"Score should be numeric, got {type(score)}"
+ )
+ assert score >= 0, f"Score should be non-negative, got {score}"
- # Test that score is valid
- assert isinstance(score, int | float), f"Score should be numeric, got {type(score)}"
- assert score >= 0, f"Score should be non-negative, got {score}"
-
- # Verify the chunk includes document information
- assert chunk.document_uri == "https://example.com/test.html"
- assert chunk.document_meta == {"title": "Test Document", "author": "Test Author"}
- assert chunk.document_id == created_document.id
- assert chunk.document_title is None
-
- client.close()
+ # Verify the chunk includes document information
+ assert chunk.document_uri == "https://example.com/test.html"
+ assert chunk.document_meta == {
+ "title": "Test Document",
+ "author": "Test Author",
+ }
+ assert chunk.document_id == created_document.id
+ assert chunk.document_title is None
@pytest.mark.vcr()
async def test_search_score_types(temp_db_path):
"""Test that different search types return appropriate score ranges."""
- client = HaikuRAG(db_path=temp_db_path, config=Config, create=True)
+ async with HaikuRAG(db_path=temp_db_path, config=Config, create=True) as client:
+ # Create multiple documents with different content
+ documents_content = [
+ "Machine learning algorithms are powerful tools for data analysis and pattern recognition.",
+ "Deep learning neural networks can process complex datasets and identify hidden patterns.",
+ "Natural language processing enables computers to understand and generate human text.",
+ "Computer vision systems can interpret and analyze visual information from images.",
+ ]
- # Create multiple documents with different content
- documents_content = [
- "Machine learning algorithms are powerful tools for data analysis and pattern recognition.",
- "Deep learning neural networks can process complex datasets and identify hidden patterns.",
- "Natural language processing enables computers to understand and generate human text.",
- "Computer vision systems can interpret and analyze visual information from images.",
- ]
+ for content in documents_content:
+ await client.create_document(content=content)
- for content in documents_content:
- await client.create_document(content=content)
+ query = "machine learning"
- query = "machine learning"
+ # Test vector search scores (should be converted from distances)
+ vector_results = await client.chunk_repository.search(
+ query, limit=3, search_type="vector"
+ )
+ assert len(vector_results) > 0
+ vector_scores = [score for _, score in vector_results]
- # Test vector search scores (should be converted from distances)
- vector_results = await client.chunk_repository.search(
- query, limit=3, search_type="vector"
- )
- assert len(vector_results) > 0
- vector_scores = [score for _, score in vector_results]
+ # Test FTS search scores (should be native LanceDB FTS scores)
+ fts_results = await client.chunk_repository.search(
+ query, limit=3, search_type="fts"
+ )
+ assert len(fts_results) > 0
+ fts_scores = [score for _, score in fts_results]
- # Test FTS search scores (should be native LanceDB FTS scores)
- fts_results = await client.chunk_repository.search(
- query, limit=3, search_type="fts"
- )
- assert len(fts_results) > 0
- fts_scores = [score for _, score in fts_results]
+ # Test hybrid search scores (should be native LanceDB relevance scores)
+ hybrid_results = await client.chunk_repository.search(
+ query, limit=3, search_type="hybrid"
+ )
+ assert len(hybrid_results) > 0
+ hybrid_scores = [score for _, score in hybrid_results]
- # Test hybrid search scores (should be native LanceDB relevance scores)
- hybrid_results = await client.chunk_repository.search(
- query, limit=3, search_type="hybrid"
- )
- assert len(hybrid_results) > 0
- hybrid_scores = [score for _, score in hybrid_results]
+ # All scores should be numeric and non-negative
+ for scores, search_type in [
+ (vector_scores, "vector"),
+ (fts_scores, "fts"),
+ (hybrid_scores, "hybrid"),
+ ]:
+ for score in scores:
+ assert isinstance(score, int | float), (
+ f"{search_type} score should be numeric"
+ )
+ assert score >= 0, f"{search_type} score should be non-negative"
- # All scores should be numeric and non-negative
- for scores, search_type in [
- (vector_scores, "vector"),
- (fts_scores, "fts"),
- (hybrid_scores, "hybrid"),
- ]:
- for score in scores:
- assert isinstance(score, int | float), (
- f"{search_type} score should be numeric"
- )
- assert score >= 0, f"{search_type} score should be non-negative"
+ # Vector scores should typically be small (0-1 range due to distance conversion)
+ assert all(0 <= score <= 1 for score in vector_scores), (
+ "Vector scores should be in 0-1 range"
+ )
- # Vector scores should typically be small (0-1 range due to distance conversion)
- assert all(0 <= score <= 1 for score in vector_scores), (
- "Vector scores should be in 0-1 range"
- )
-
- # Scores should be sorted in descending order (most relevant first)
- for scores, search_type in [
- (vector_scores, "vector"),
- (fts_scores, "fts"),
- (hybrid_scores, "hybrid"),
- ]:
- for i in range(len(scores) - 1):
- assert scores[i] >= scores[i + 1], (
- f"{search_type} results should be sorted by score descending"
- )
-
- client.close()
+ # Scores should be sorted in descending order (most relevant first)
+ for scores, search_type in [
+ (vector_scores, "vector"),
+ (fts_scores, "fts"),
+ (hybrid_scores, "hybrid"),
+ ]:
+ for i in range(len(scores) - 1):
+ assert scores[i] >= scores[i + 1], (
+ f"{search_type} results should be sorted by score descending"
+ )
@pytest.mark.vcr()
async def test_search_returns_search_result(temp_db_path):
"""Test that client.search() returns SearchResult with provenance info."""
- client = HaikuRAG(db_path=temp_db_path, config=Config, create=True)
+ async with HaikuRAG(db_path=temp_db_path, config=Config, create=True) as client:
+ await client.create_document(
+ content="Machine learning models can classify images with high accuracy.",
+ uri="https://example.com/ml.html",
+ title="ML Guide",
+ )
- await client.create_document(
- content="Machine learning models can classify images with high accuracy.",
- uri="https://example.com/ml.html",
- title="ML Guide",
- )
+ results = await client.search("machine learning", limit=3)
- results = await client.search("machine learning", limit=3)
-
- assert len(results) > 0
- result = results[0]
- assert isinstance(result, SearchResult)
- assert result.content
- assert result.score > 0
- assert result.document_uri == "https://example.com/ml.html"
- assert result.document_title == "ML Guide"
- assert result.chunk_id is not None
- assert result.document_id is not None
- # page_numbers and headings come from chunk metadata
- assert isinstance(result.page_numbers, list)
- assert isinstance(result.labels, list)
- assert len(result.labels) > 0
-
- client.close()
+ assert len(results) > 0
+ result = results[0]
+ assert isinstance(result, SearchResult)
+ assert result.content
+ assert result.score > 0
+ assert result.document_uri == "https://example.com/ml.html"
+ assert result.document_title == "ML Guide"
+ assert result.chunk_id is not None
+ assert result.document_id is not None
+ # page_numbers and headings come from chunk metadata
+ assert isinstance(result.page_numbers, list)
+ assert isinstance(result.labels, list)
+ assert len(result.labels) > 0
@pytest.mark.vcr()
@@ -197,30 +189,27 @@ async def test_search_graceful_degradation(temp_db_path):
"""Test search works when docling data is unavailable."""
from haiku.rag.store.models import Chunk
- client = HaikuRAG(db_path=temp_db_path, config=Config, create=True)
+ async with HaikuRAG(db_path=temp_db_path, config=Config, create=True) as client:
+ # Import document with custom chunks (no docling document)
+ custom_chunks = [
+ Chunk(content="Custom chunk without docling metadata", metadata={}),
+ ]
+ docling_doc = await client.convert("Document with custom chunks")
+ await client.import_document(
+ docling_document=docling_doc,
+ chunks=custom_chunks,
+ uri="https://example.com/custom.html",
+ )
- # Import document with custom chunks (no docling document)
- custom_chunks = [
- Chunk(content="Custom chunk without docling metadata", metadata={}),
- ]
- docling_doc = await client.convert("Document with custom chunks")
- await client.import_document(
- docling_document=docling_doc,
- chunks=custom_chunks,
- uri="https://example.com/custom.html",
- )
+ results = await client.search("custom chunk", limit=3)
- results = await client.search("custom chunk", limit=3)
-
- assert len(results) > 0
- result = results[0]
- assert isinstance(result, SearchResult)
- assert result.content
- # Metadata defaults should still work
- assert result.page_numbers == []
- assert result.labels == []
-
- client.close()
+ assert len(results) > 0
+ result = results[0]
+ assert isinstance(result, SearchResult)
+ assert result.content
+ # Metadata defaults should still work
+ assert result.page_numbers == []
+ assert result.labels == []
@pytest.mark.vcr()
@@ -252,6 +241,41 @@ async def test_search_result_format_includes_metadata(temp_db_path):
assert "machine learning" in formatted.lower()
+@pytest.mark.vcr()
+async def test_fts_search_targets_content_fts_column(temp_db_path):
+ """FTS search must target the content_fts column (where the FTS index
+ lives and where contextualized heading prefixes end up) — not the raw
+ content column. Regression guard against upstream default-column changes
+ in LanceDB's nearest_to_text().
+ """
+ from haiku.rag.store.models.chunk import Chunk
+
+ async with HaikuRAG(temp_db_path, create=True) as client:
+ doc = await client.create_document(content="seed", uri="test://doc")
+ assert doc.id is not None
+
+ # Heading-only term — contextualization will prepend headings to the
+ # body when populating content_fts, so this word ends up ONLY in
+ # content_fts, not in the content column.
+ heading_only_term = "zxqvjfoowizardry"
+ chunk = Chunk(
+ content="unrelated body text",
+ document_id=doc.id,
+ metadata={"headings": [heading_only_term]},
+ embedding=[0.0] * client.store.embedder._vector_dim,
+ )
+ await client.chunk_repository.create(chunk)
+
+ # FTS on the heading-only word must match via content_fts.
+ results = await client.chunk_repository.search(
+ heading_only_term, limit=5, search_type="fts"
+ )
+ assert any(c.content == "unrelated body text" for c, _ in results), (
+ "FTS did not match a heading-only term — nearest_to_text is not "
+ "targeting the content_fts column"
+ )
+
+
def test_search_result_primary_label_prioritizes_structural_types():
"""Test _get_primary_label prioritizes structural labels correctly."""
# Table should be prioritized
diff --git a/tests/test_settings.py b/tests/test_settings.py
index c888e582..00aef484 100644
--- a/tests/test_settings.py
+++ b/tests/test_settings.py
@@ -4,43 +4,42 @@ from haiku.rag.config import AppConfig, Config
from haiku.rag.store.repositories.settings import ConfigMismatchError
-def test_settings_table_populated_on_store_init(temp_db_path):
+@pytest.mark.asyncio
+async def test_settings_table_populated_on_store_init(temp_db_path):
"""Test that settings table is populated with current config when store is initialized."""
from haiku.rag.store.engine import Store
from haiku.rag.store.repositories.settings import SettingsRepository
- store = Store(temp_db_path, create=True)
- settings_repo = SettingsRepository(store)
+ async with Store(temp_db_path, create=True) as store:
+ settings_repo = SettingsRepository(store)
- db_settings = settings_repo.get_current_settings()
- config_dict = Config.model_dump(mode="json")
+ db_settings = await settings_repo.get_current_settings()
+ config_dict = Config.model_dump(mode="json")
- # Remove version from db_settings since it's added automatically
- db_settings_without_version = {
- k: v for k, v in db_settings.items() if k != "version"
- }
- assert db_settings_without_version == config_dict
-
- store.close()
+ # Remove version from db_settings since it's added automatically
+ db_settings_without_version = {
+ k: v for k, v in db_settings.items() if k != "version"
+ }
+ assert db_settings_without_version == config_dict
-def test_settings_save_and_retrieve(temp_db_path):
+@pytest.mark.asyncio
+async def test_settings_save_and_retrieve(temp_db_path):
"""Test saving and retrieving settings after config change."""
from haiku.rag.store.engine import Store
from haiku.rag.store.repositories.settings import SettingsRepository
- store = Store(temp_db_path, create=True)
- settings_repo = SettingsRepository(store)
+ async with Store(temp_db_path, create=True) as store:
+ settings_repo = SettingsRepository(store)
- original_chunk_size = Config.processing.chunk_size
- Config.processing.chunk_size = 2 * original_chunk_size
+ original_chunk_size = Config.processing.chunk_size
+ Config.processing.chunk_size = 2 * original_chunk_size
- settings_repo.save_current_settings()
- retrieved_settings = settings_repo.get_current_settings()
- assert retrieved_settings["processing"]["chunk_size"] == 2 * original_chunk_size
+ await settings_repo.save_current_settings()
+ retrieved_settings = await settings_repo.get_current_settings()
+ assert retrieved_settings["processing"]["chunk_size"] == 2 * original_chunk_size
- Config.processing.chunk_size = original_chunk_size
- store.close()
+ Config.processing.chunk_size = original_chunk_size
def test_monitor_filter_patterns_config():
@@ -56,103 +55,111 @@ def test_monitor_filter_patterns_config():
class TestValidateConfigCompatibility:
"""Tests for validate_config_compatibility method."""
- def test_empty_settings_saves_config(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_empty_settings_saves_config(self, temp_db_path):
"""When settings row is missing, validation saves current config."""
from haiku.rag.store.engine import Store
from haiku.rag.store.repositories.settings import SettingsRepository
- store = Store(temp_db_path, create=True, skip_validation=True)
- settings_repo = SettingsRepository(store)
+ async with Store(temp_db_path, create=True, skip_validation=True) as store:
+ settings_repo = SettingsRepository(store)
- # Clear settings to simulate empty state
- store.settings_table.delete("id = 'settings'")
- assert settings_repo.get_current_settings() == {}
+ # Clear settings to simulate empty state
+ await store.settings_table.delete("id = 'settings'")
+ assert await settings_repo.get_current_settings() == {}
- # Validation should save settings
- settings_repo.validate_config_compatibility()
+ # Validation should save settings
+ await settings_repo.validate_config_compatibility()
- # Now settings should exist
- saved = settings_repo.get_current_settings()
- assert saved.get("embeddings", {}).get("model", {}).get("provider") is not None
- store.close()
+ # Now settings should exist
+ saved = await settings_repo.get_current_settings()
+ assert (
+ saved.get("embeddings", {}).get("model", {}).get("provider") is not None
+ )
- def test_compatible_config_no_error(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_compatible_config_no_error(self, temp_db_path):
"""Compatible config does not raise error."""
from haiku.rag.store.engine import Store
from haiku.rag.store.repositories.settings import SettingsRepository
- store = Store(temp_db_path, create=True)
- settings_repo = SettingsRepository(store)
+ async with Store(temp_db_path, create=True) as store:
+ settings_repo = SettingsRepository(store)
- # Should not raise - same config
- settings_repo.validate_config_compatibility()
- store.close()
+ # Should not raise - same config
+ await settings_repo.validate_config_compatibility()
- def test_provider_mismatch_raises_error(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_provider_mismatch_raises_error(self, temp_db_path):
"""Different embedding provider raises ConfigMismatchError."""
from haiku.rag.store.engine import Store
from haiku.rag.store.repositories.settings import SettingsRepository
# Create store with default config (ollama)
- store = Store(temp_db_path, create=True)
- store.close()
+ async with Store(temp_db_path, create=True):
+ pass
# Create new config with different provider
new_config = AppConfig()
new_config.embeddings.model.provider = "openai"
- store2 = Store(temp_db_path, config=new_config, skip_validation=True)
- settings_repo = SettingsRepository(store2)
+ async with Store(
+ temp_db_path, config=new_config, skip_validation=True
+ ) as store2:
+ settings_repo = SettingsRepository(store2)
- with pytest.raises(ConfigMismatchError) as exc_info:
- settings_repo.validate_config_compatibility()
+ with pytest.raises(ConfigMismatchError) as exc_info:
+ await settings_repo.validate_config_compatibility()
- assert "embedding provider" in str(exc_info.value)
- assert "ollama" in str(exc_info.value)
- assert "openai" in str(exc_info.value)
- store2.close()
+ assert "embedding provider" in str(exc_info.value)
+ assert "ollama" in str(exc_info.value)
+ assert "openai" in str(exc_info.value)
- def test_model_mismatch_raises_error(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_model_mismatch_raises_error(self, temp_db_path):
"""Different embedding model raises ConfigMismatchError."""
from haiku.rag.store.engine import Store
from haiku.rag.store.repositories.settings import SettingsRepository
# Create store with default config
- store = Store(temp_db_path, create=True)
- store.close()
+ async with Store(temp_db_path, create=True):
+ pass
# Create new config with different model
new_config = AppConfig()
new_config.embeddings.model.name = "different-model"
- store2 = Store(temp_db_path, config=new_config, skip_validation=True)
- settings_repo = SettingsRepository(store2)
+ async with Store(
+ temp_db_path, config=new_config, skip_validation=True
+ ) as store2:
+ settings_repo = SettingsRepository(store2)
- with pytest.raises(ConfigMismatchError) as exc_info:
- settings_repo.validate_config_compatibility()
+ with pytest.raises(ConfigMismatchError) as exc_info:
+ await settings_repo.validate_config_compatibility()
- assert "embedding model" in str(exc_info.value)
- store2.close()
+ assert "embedding model" in str(exc_info.value)
- def test_vector_dim_mismatch_raises_error(self, temp_db_path):
+ @pytest.mark.asyncio
+ async def test_vector_dim_mismatch_raises_error(self, temp_db_path):
"""Different vector dimension raises ConfigMismatchError."""
from haiku.rag.store.engine import Store
from haiku.rag.store.repositories.settings import SettingsRepository
# Create store with default config
- store = Store(temp_db_path, create=True)
- store.close()
+ async with Store(temp_db_path, create=True):
+ pass
# Create new config with different vector dimension
new_config = AppConfig()
new_config.embeddings.model.vector_dim = 9999
- store2 = Store(temp_db_path, config=new_config, skip_validation=True)
- settings_repo = SettingsRepository(store2)
+ async with Store(
+ temp_db_path, config=new_config, skip_validation=True
+ ) as store2:
+ settings_repo = SettingsRepository(store2)
- with pytest.raises(ConfigMismatchError) as exc_info:
- settings_repo.validate_config_compatibility()
+ with pytest.raises(ConfigMismatchError) as exc_info:
+ await settings_repo.validate_config_compatibility()
- assert "vector dimension" in str(exc_info.value)
- assert "9999" in str(exc_info.value)
- store2.close()
+ assert "vector dimension" in str(exc_info.value)
+ assert "9999" in str(exc_info.value)
diff --git a/tests/test_title_generation.py b/tests/test_title_generation.py
index 7411c674..c13f736b 100644
--- a/tests/test_title_generation.py
+++ b/tests/test_title_generation.py
@@ -5,6 +5,7 @@ from docling_core.types.doc.document import ContentLayer, DoclingDocument
from docling_core.types.doc.labels import DocItemLabel
from haiku.rag.client import HaikuRAG
+from haiku.rag.client.titles import extract_structural_title, resolve_title
from haiku.rag.config import AppConfig
from haiku.rag.config.models import ProcessingConfig
from haiku.rag.embeddings import EmbedderWrapper
@@ -35,11 +36,7 @@ def mock_embedder(monkeypatch):
class TestExtractStructuralTitle:
- def _make_client(self, tmp_path):
- config = AppConfig(processing=ProcessingConfig(auto_title=True))
- return HaikuRAG(tmp_path / "test.lancedb", config=config, create=True)
-
- def test_furniture_title(self, tmp_path):
+ def test_furniture_title(self):
"""TITLE on FURNITURE layer (HTML ) is extracted."""
doc = DoclingDocument(name="test")
doc.add_text(
@@ -49,11 +46,9 @@ class TestExtractStructuralTitle:
)
doc.add_text(label=DocItemLabel.PARAGRAPH, text="Body text")
- client = self._make_client(tmp_path)
- result = client._extract_structural_title(doc)
- assert result == "Website Page Title"
+ assert extract_structural_title(doc) == "Website Page Title"
- def test_body_title(self, tmp_path):
+ def test_body_title(self):
"""TITLE on BODY layer (h1, PDF title) is extracted."""
doc = DoclingDocument(name="test")
doc.add_text(
@@ -63,31 +58,25 @@ class TestExtractStructuralTitle:
)
doc.add_text(label=DocItemLabel.PARAGRAPH, text="Body text")
- client = self._make_client(tmp_path)
- result = client._extract_structural_title(doc)
- assert result == "Document Heading"
+ assert extract_structural_title(doc) == "Document Heading"
- def test_section_header_fallback(self, tmp_path):
+ def test_section_header_fallback(self):
"""First SECTION_HEADER is used when no TITLE exists."""
doc = DoclingDocument(name="test")
doc.add_text(label=DocItemLabel.SECTION_HEADER, text="Introduction")
doc.add_text(label=DocItemLabel.SECTION_HEADER, text="Background")
doc.add_text(label=DocItemLabel.PARAGRAPH, text="Body text")
- client = self._make_client(tmp_path)
- result = client._extract_structural_title(doc)
- assert result == "Introduction"
+ assert extract_structural_title(doc) == "Introduction"
- def test_no_title_or_headers(self, tmp_path):
+ def test_no_title_or_headers(self):
"""Returns None when no TITLE or SECTION_HEADER exists."""
doc = DoclingDocument(name="test")
doc.add_text(label=DocItemLabel.PARAGRAPH, text="Just a paragraph")
- client = self._make_client(tmp_path)
- result = client._extract_structural_title(doc)
- assert result is None
+ assert extract_structural_title(doc) is None
- def test_furniture_title_preferred_over_body_title(self, tmp_path):
+ def test_furniture_title_preferred_over_body_title(self):
"""FURNITURE TITLE takes priority over BODY TITLE."""
doc = DoclingDocument(name="test")
doc.add_text(
@@ -101,11 +90,9 @@ class TestExtractStructuralTitle:
content_layer=ContentLayer.FURNITURE,
)
- client = self._make_client(tmp_path)
- result = client._extract_structural_title(doc)
- assert result == "HTML Page Title"
+ assert extract_structural_title(doc) == "HTML Page Title"
- def test_whitespace_stripped(self, tmp_path):
+ def test_whitespace_stripped(self):
"""Whitespace is stripped from extracted titles."""
doc = DoclingDocument(name="test")
doc.add_text(
@@ -114,11 +101,9 @@ class TestExtractStructuralTitle:
content_layer=ContentLayer.BODY,
)
- client = self._make_client(tmp_path)
- result = client._extract_structural_title(doc)
- assert result == "Padded Title"
+ assert extract_structural_title(doc) == "Padded Title"
- def test_empty_title_text_skipped(self, tmp_path):
+ def test_empty_title_text_skipped(self):
"""Empty or whitespace-only TITLE text is skipped."""
doc = DoclingDocument(name="test")
doc.add_text(
@@ -128,54 +113,49 @@ class TestExtractStructuralTitle:
)
doc.add_text(label=DocItemLabel.SECTION_HEADER, text="Actual Heading")
- client = self._make_client(tmp_path)
- result = client._extract_structural_title(doc)
- assert result == "Actual Heading"
+ assert extract_structural_title(doc) == "Actual Heading"
# =========================================================================
-# _resolve_title
+# resolve_title
# =========================================================================
class TestResolveTitle:
- def _make_client(self, tmp_path, auto_title=True):
- config = AppConfig(processing=ProcessingConfig(auto_title=auto_title))
- return HaikuRAG(tmp_path / "test.lancedb", config=config, create=True)
-
@pytest.mark.asyncio
- async def test_auto_title_disabled_returns_none(self, tmp_path):
+ async def test_auto_title_disabled_returns_none(self):
"""When auto_title is False, returns None (no title generation)."""
doc = DoclingDocument(name="test")
doc.add_text(label=DocItemLabel.TITLE, text="Structural Title")
- client = self._make_client(tmp_path, auto_title=False)
- result = await client._resolve_title(doc, "some content")
+ config = AppConfig(processing=ProcessingConfig(auto_title=False))
+ result = await resolve_title(config, doc, "some content")
assert result is None
@pytest.mark.asyncio
- async def test_structural_title_extracted(self, tmp_path):
+ async def test_structural_title_extracted(self):
"""Structural title is extracted when auto_title is enabled."""
doc = DoclingDocument(name="test")
doc.add_text(label=DocItemLabel.TITLE, text="Auto Extracted Title")
- client = self._make_client(tmp_path)
- result = await client._resolve_title(doc, "some content")
+ config = AppConfig(processing=ProcessingConfig(auto_title=True))
+ result = await resolve_title(config, doc, "some content")
assert result == "Auto Extracted Title"
@pytest.mark.asyncio
- async def test_llm_failure_returns_none(self, tmp_path, monkeypatch):
+ async def test_llm_failure_returns_none(self, monkeypatch):
"""LLM failure during ingestion returns None instead of raising."""
doc = DoclingDocument(name="test")
doc.add_text(label=DocItemLabel.PARAGRAPH, text="Just text")
- client = self._make_client(tmp_path)
-
- async def exploding_llm(self, content):
+ async def exploding_llm(config, content):
raise RuntimeError("LLM is down")
- monkeypatch.setattr(HaikuRAG, "_generate_title_with_llm", exploding_llm)
- result = await client._resolve_title(doc, "some content")
+ monkeypatch.setattr(
+ "haiku.rag.client.titles.generate_title_with_llm", exploding_llm
+ )
+ config = AppConfig(processing=ProcessingConfig(auto_title=True))
+ result = await resolve_title(config, doc, "some content")
assert result is None
diff --git a/tests/test_versioning.py b/tests/test_versioning.py
index 2e7be9f6..5d0a0198 100644
--- a/tests/test_versioning.py
+++ b/tests/test_versioning.py
@@ -1,3 +1,5 @@
+import asyncio
+
import pytest
from haiku.rag.client import HaikuRAG
@@ -60,7 +62,7 @@ async def test_version_rollback_on_update_failure(temp_db_path):
assert len(original_chunks) > 0
-def test_new_database_does_not_run_upgrades(monkeypatch, temp_db_path):
+async def test_new_database_does_not_run_upgrades(monkeypatch, temp_db_path):
def fail_if_called(*_args, **_kwargs):
raise AssertionError("run_pending_upgrades should not be called for new DB")
@@ -69,11 +71,13 @@ def test_new_database_does_not_run_upgrades(monkeypatch, temp_db_path):
fail_if_called,
)
- Store(temp_db_path, create=True)
+ async with Store(temp_db_path, create=True):
+ pass
-def test_existing_database_checks_migrations(monkeypatch, temp_db_path):
- Store(temp_db_path, create=True)
+async def test_existing_database_checks_migrations(monkeypatch, temp_db_path):
+ async with Store(temp_db_path, create=True):
+ pass
from haiku.rag.store import upgrades
@@ -90,25 +94,33 @@ def test_existing_database_checks_migrations(monkeypatch, temp_db_path):
)
# Opening an existing database should check for pending migrations
- Store(temp_db_path)
+ async with Store(temp_db_path):
+ pass
assert called["value"]
+async def _wait_for_background_vacuum(client):
+ """Wait for any in-flight background vacuum tasks to complete."""
+ await client._await_vacuum_tasks()
+
+
@pytest.mark.vcr()
async def test_vacuum_with_retention_threshold(temp_db_path):
async with HaikuRAG(db_path=temp_db_path, create=True) as client:
# Create first document
await client.create_document(content="First document")
+ await _wait_for_background_vacuum(client)
# Create second document
await client.create_document(content="Second document")
+ await _wait_for_background_vacuum(client)
store = client.store
# Get initial version counts (should have multiple versions from creates)
- initial_doc_versions = len(list(store.documents_table.list_versions()))
- initial_chunk_versions = len(list(store.chunks_table.list_versions()))
+ initial_doc_versions = len(await store.documents_table.list_versions())
+ initial_chunk_versions = len(await store.chunks_table.list_versions())
assert initial_doc_versions > 1, "Should have multiple document table versions"
assert initial_chunk_versions > 1, "Should have multiple chunk table versions"
@@ -117,8 +129,8 @@ async def test_vacuum_with_retention_threshold(temp_db_path):
# Note: vacuum may create new versions even when not cleaning up old ones
await store.vacuum()
- after_default_doc_versions = len(list(store.documents_table.list_versions()))
- after_default_chunk_versions = len(list(store.chunks_table.list_versions()))
+ after_default_doc_versions = len(await store.documents_table.list_versions())
+ after_default_chunk_versions = len(await store.chunks_table.list_versions())
# After vacuum with retention, version count should stay the same or increase
# (optimize may create new versions) but not decrease
@@ -132,8 +144,8 @@ async def test_vacuum_with_retention_threshold(temp_db_path):
# Vacuum with 0 threshold - should significantly reduce versions
await store.vacuum(retention_seconds=0)
- after_zero_doc_versions = len(list(store.documents_table.list_versions()))
- after_zero_chunk_versions = len(list(store.chunks_table.list_versions()))
+ after_zero_doc_versions = len(await store.documents_table.list_versions())
+ after_zero_chunk_versions = len(await store.chunks_table.list_versions())
# After aggressive vacuum, should have minimal versions (1-2)
# Note: optimize operation may create a version after cleanup
@@ -168,16 +180,96 @@ async def test_vacuum_completes_before_context_exit(temp_db_path, monkeypatch):
await client.create_document(content=f"Test document {i}")
# After context exit, automatic vacuum should have kept versions minimal
- store = Store(temp_db_path, create=True)
- final_versions = len(list(store.documents_table.list_versions()))
+ async with Store(temp_db_path, create=True) as store:
+ final_versions = len(await store.documents_table.list_versions())
- # With retention_seconds=0, vacuum aggressively cleans up between operations
- # Should have very few versions remaining (1-2)
- assert final_versions <= 2, (
- f"Aggressive vacuum should keep minimal versions, got {final_versions}"
+ # With retention_seconds=0, vacuum aggressively cleans up between operations
+ # Should have very few versions remaining (1-2)
+ assert final_versions <= 2, (
+ f"Aggressive vacuum should keep minimal versions, got {final_versions}"
+ )
+ assert final_versions >= 1, "Should have at least one version remaining"
+
+
+@pytest.mark.vcr()
+async def test_aexit_awaits_background_vacuum(temp_db_path, monkeypatch):
+ """__aexit__ must await any in-flight background vacuum, not just release the lock.
+
+ Background vacuum runs as an asyncio task; the event loop may not have scheduled
+ it yet when __aexit__ runs. Simply acquiring the vacuum lock (which is free until
+ the task actually starts) would let close() proceed before vacuum runs.
+ """
+ from haiku.rag.config import Config
+
+ monkeypatch.setattr(Config.storage, "auto_vacuum", True)
+
+ vacuum_started = asyncio.Event()
+ vacuum_completed = asyncio.Event()
+
+ async with HaikuRAG(db_path=temp_db_path, create=True) as client:
+ original_vacuum = client.store.vacuum
+
+ async def instrumented_vacuum(*args, **kwargs):
+ vacuum_started.set()
+ # Delay so __aexit__ would see an unstarted/incomplete task if it
+ # relied on the lock rather than awaiting the task directly.
+ await asyncio.sleep(0.05)
+ await original_vacuum(*args, **kwargs)
+ vacuum_completed.set()
+
+ client.store.vacuum = instrumented_vacuum
+
+ await client.create_document(content="triggers background vacuum")
+
+ assert vacuum_started.is_set(), "Background vacuum task never ran"
+ assert vacuum_completed.is_set(), "__aexit__ exited before vacuum finished"
+
+
+@pytest.mark.vcr()
+async def test_aexit_awaits_all_background_vacuums(temp_db_path, monkeypatch):
+ """Multiple create_document calls schedule multiple vacuum tasks; __aexit__
+ must await all of them, not just the last-scheduled one.
+
+ Scenario: Task A acquires the vacuum lock and is slow. Task B is scheduled
+ while Task A still holds the lock — Task B sees the lock held and returns
+ immediately. If the client only tracks the most recently scheduled task,
+ __aexit__ awaits the fast no-op B and closes the connection while Task A
+ is still running.
+ """
+ from haiku.rag.config import Config
+
+ monkeypatch.setattr(Config.storage, "auto_vacuum", True)
+
+ first_vacuum_completed = asyncio.Event()
+
+ async with HaikuRAG(db_path=temp_db_path, create=True) as client:
+ call_count = 0
+
+ async def slow_vacuum(*_args, **_kwargs):
+ nonlocal call_count
+ call_count += 1
+ my_num = call_count
+ # Mimic the real vacuum's skip-if-running behavior.
+ if client.store._vacuum_lock.locked():
+ return
+ async with client.store._vacuum_lock:
+ if my_num == 1:
+ # Hold the lock longer than any other operation in the
+ # test so Task A cannot finish incidentally. __aexit__
+ # must explicitly wait for this task.
+ await asyncio.sleep(2.0)
+ first_vacuum_completed.set()
+
+ client.store.vacuum = slow_vacuum
+
+ await client.create_document(content="triggers first vacuum")
+ # Let Task A start and acquire the vacuum lock before scheduling B.
+ await asyncio.sleep(0.02)
+ await client.create_document(content="triggers second vacuum")
+
+ assert first_vacuum_completed.is_set(), (
+ "__aexit__ returned before the first vacuum task finished"
)
- assert final_versions >= 1, "Should have at least one version remaining"
- store.close()
@pytest.mark.vcr()
@@ -194,8 +286,8 @@ async def test_auto_vacuum_disabled_skips_vacuum(temp_db_path, monkeypatch):
await client.create_document(content=f"Test document {i}")
# Count versions - should accumulate without vacuum
- doc_versions = len(list(client.store.documents_table.list_versions()))
- chunk_versions = len(list(client.store.chunks_table.list_versions()))
+ doc_versions = len(await client.store.documents_table.list_versions())
+ chunk_versions = len(await client.store.chunks_table.list_versions())
# Without auto-vacuum, versions should accumulate (more than 3 from creates)
assert doc_versions >= 3, (
@@ -221,11 +313,10 @@ async def test_auto_vacuum_enabled_triggers_vacuum(temp_db_path, monkeypatch):
await client.create_document(content=f"Test document {i}")
# After context exit, vacuum should have cleaned up
- store = Store(temp_db_path, create=True)
- final_versions = len(list(store.documents_table.list_versions()))
+ async with Store(temp_db_path, create=True) as store:
+ final_versions = len(await store.documents_table.list_versions())
- # With auto_vacuum=True and retention=0, should have minimal versions
- assert final_versions <= 2, (
- f"With auto-vacuum enabled, should have minimal versions, got {final_versions}"
- )
- store.close()
+ # With auto_vacuum=True and retention=0, should have minimal versions
+ assert final_versions <= 2, (
+ f"With auto-vacuum enabled, should have minimal versions, got {final_versions}"
+ )