diff --git a/CHANGELOG.md b/CHANGELOG.md index bba86814..3e15c489 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,17 @@ # Changelog ## [Unreleased] +### Changed + +- **Native async LanceDB**: all table I/O now uses LanceDB's async API (`connect_async`, `AsyncConnection`, `AsyncTable`). Previously, repository methods were declared `async def` but called blocking sync LanceDB under the hood, stalling the event loop on every read/write. No change to the documented `async with HaikuRAG(...) as client:` usage pattern. +- **BREAKING (internal): `HaikuRAG` must be used via `async with`.** Store initialization now happens in `__aenter__`; constructing `HaikuRAG(...)` and calling methods directly without entering the context manager no longer works. +- **BREAKING (internal): `download_models` is no longer a method on `HaikuRAG`.** It's now a module-level function: `from haiku.rag.client.downloads import download_models; async for progress in download_models(config): ...`. The CLI and in-repo consumers are updated. +- **Concurrency: background vacuum tracked as a task** on the client. `__aexit__` and `rebuild_database` now await it explicitly, preventing `CreateIndex transaction was preempted` commit conflicts when destructive operations follow a `create_document` that scheduled a background vacuum. + ### Fixed - **Chat TUI now renders citations again.** After the 0.42.1 flattening of skill state `citations` to `list[str]`, the TUI still indexed `citations[-1]` and iterated the resulting chunk-id string character-by-character, so no citations resolved through `citation_index` and the citation panel stayed empty. Fixed by iterating `state.citations` directly. +- **`search(..., filter=...)` no longer silently under-returns.** The filter path used to materialize LanceDB's top-N window, filter to matching `document_id`s in pandas, and `head(limit)`. When matching chunks lived outside that top-N window (selective filters, broad queries), the caller got fewer than `limit` results even though plenty of matching chunks existed in the index. The document filter is now pushed down into the chunk query as `document_id IN (...)` so `.limit(limit)` applies to matching chunks directly. Behavior change: searches that previously under-returned will start returning the requested count. ## [0.42.1] - 2026-04-22 diff --git a/app/backend/main.py b/app/backend/main.py index cfbd7f0b..6aead42e 100644 --- a/app/backend/main.py +++ b/app/backend/main.py @@ -1,5 +1,7 @@ +import asyncio import logging import os +from contextlib import asynccontextmanager from pathlib import Path from dotenv import find_dotenv, load_dotenv @@ -58,13 +60,23 @@ logger.info(f"QA Provider: {Config.qa.model.provider}, Model: {Config.qa.model.n # Only HaikuRAG client is a singleton (expensive to create) _client: HaikuRAG | None = None +_client_lock = asyncio.Lock() -def get_client() -> HaikuRAG: - """Get or create cached client.""" +async def get_client() -> HaikuRAG: + """Get or create the cached client. + + Guarded by a lock because the first request after startup can race with + itself: two concurrent callers would both pass the None check, each build + and enter a HaikuRAG, and the loser would leak its LanceDB connection. + """ global _client if _client is None: - _client = HaikuRAG(db_path=db_path, config=Config, create=True) + async with _client_lock: + if _client is None: + client = HaikuRAG(db_path=db_path, config=Config, create=True) + await client.__aenter__() + _client = client return _client @@ -126,7 +138,7 @@ async def list_documents(_: Request) -> JSONResponse: if not db_path.exists(): return JSONResponse({"documents": [], "error": "Database not found"}) - client = get_client() + client = await get_client() docs = await client.document_repository.list_all() return JSONResponse( { @@ -151,8 +163,8 @@ async def db_info(_: Request) -> JSONResponse: from haiku.rag.store.engine import get_database_stats - client = get_client() - stats = get_database_stats(client.store.db) + client = await get_client() + stats = await get_database_stats(client.store.db) return JSONResponse( { @@ -177,7 +189,7 @@ async def visualize_chunk(request: Request) -> JSONResponse: if not db_path.exists(): return JSONResponse({"error": "Database not found"}, status_code=404) - client = get_client() + client = await get_client() chunk = await client.chunk_repository.get_by_id(chunk_id) if not chunk: @@ -203,6 +215,21 @@ async def visualize_chunk(request: Request) -> JSONResponse: ) +@asynccontextmanager +async def lifespan(app: Starlette): + """Shut down the cached HaikuRAG client cleanly on app exit. + + Awaits any in-flight background vacuum tasks and closes the LanceDB + connection. Without this, vacuum tasks are cancelled abruptly and the + connection is never closed on process shutdown. + """ + yield + global _client + if _client is not None: + await _client.__aexit__(None, None, None) + _client = None + + # Create Starlette app app = Starlette( routes=[ @@ -221,6 +248,7 @@ app = Starlette( allow_headers=["*"], ) ], + lifespan=lifespan, ) if __name__ == "__main__": diff --git a/app/frontend/components/Chat.tsx b/app/frontend/components/Chat.tsx index 17a50eca..a2f2f6b5 100644 --- a/app/frontend/components/Chat.tsx +++ b/app/frontend/components/Chat.tsx @@ -376,7 +376,7 @@ function MessageViewWithCitations({ if (latestCitations.length > 0) { result.push( , ); diff --git a/app/frontend/components/CitationBlock.tsx b/app/frontend/components/CitationBlock.tsx index b4c463bd..3221e0ec 100644 --- a/app/frontend/components/CitationBlock.tsx +++ b/app/frontend/components/CitationBlock.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import type { Citation } from "../lib/sessionStorage"; interface CitationBlockProps { @@ -73,8 +73,20 @@ export default function CitationBlock({ citations }: CitationBlockProps) { loading: false, error: null, }); + // AbortController for the in-flight visualize fetch so a rapid close/reopen + // doesn't let a stale response overwrite the new request's state. + const abortRef = useRef(null); + + // Abort any in-flight request on unmount. + useEffect(() => { + return () => abortRef.current?.abort(); + }, []); const fetchVisualGrounding = useCallback(async (chunkId: string) => { + abortRef.current?.abort(); + const controller = new AbortController(); + abortRef.current = controller; + setVisualGrounding({ isOpen: true, chunkId, @@ -84,10 +96,12 @@ export default function CitationBlock({ citations }: CitationBlockProps) { }); try { - const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || ""; - const response = await fetch(`${backendUrl}/api/visualize/${chunkId}`); + const response = await fetch(`/api/visualize/${chunkId}`, { + signal: controller.signal, + }); const data = await response.json(); + if (controller.signal.aborted) return; if (!response.ok) { throw new Error(data.error || "Failed to fetch visual grounding"); } @@ -99,6 +113,7 @@ export default function CitationBlock({ citations }: CitationBlockProps) { error: data.images?.length === 0 ? data.message : null, })); } catch (err) { + if (controller.signal.aborted) return; setVisualGrounding((prev) => ({ ...prev, loading: false, @@ -108,6 +123,8 @@ export default function CitationBlock({ citations }: CitationBlockProps) { }, []); const closeVisualGrounding = useCallback(() => { + abortRef.current?.abort(); + abortRef.current = null; setVisualGrounding({ isOpen: false, chunkId: null, diff --git a/app/frontend/components/DbInfo.tsx b/app/frontend/components/DbInfo.tsx index 8f50ffa8..7d0869d3 100644 --- a/app/frontend/components/DbInfo.tsx +++ b/app/frontend/components/DbInfo.tsx @@ -25,8 +25,7 @@ export default function DbInfo() { const [error, setError] = useState(null); useEffect(() => { - const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || ""; - fetch(`${backendUrl}/api/info`) + fetch("/api/info") .then((res) => res.json()) .then(setInfo) .catch((err) => setError(err.message)); diff --git a/app/frontend/components/DocumentFilter.tsx b/app/frontend/components/DocumentFilter.tsx index d2296961..1fa56d46 100644 --- a/app/frontend/components/DocumentFilter.tsx +++ b/app/frontend/components/DocumentFilter.tsx @@ -16,6 +16,8 @@ interface DocumentFilterProps { onApply: (selected: string[]) => void; } +const getDisplayName = (doc: Document) => doc.title || doc.uri || doc.id; + export default function DocumentFilter({ isOpen, onClose, @@ -26,33 +28,40 @@ export default function DocumentFilter({ const [documents, setDocuments] = useState([]); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(""); - const [localSelected, setLocalSelected] = useState>( - new Set(selected), - ); + // Track selection by document id — two docs can share a title, but ids + // are unique. Display names are only used for rendering and for the + // filter string returned to the parent. + const [localSelected, setLocalSelected] = useState>(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: 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 + 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: uEb6ubRiGD300dE7tzRMPZWPILsrkS49OAQqPcmoj7ynDpW7hF4Vvcn9UL3i8hk9QUN+Ok4eKLwuEiw9juZuvMKmN71Zwvi8p+CAvJjEYzolUx28GzYSPDzUUT25Cjm9oIHZvH6elrwkYaG8w8govRyiBTwq7AY8vNrpOW9kSr2cBgY9988vvfNejjsNTNm8u9iwvA2fD7x4XzO8UquhPEPG5DzMgSW7+eEHOTJPCTwed6W8kjenvJ5JFzzi84S8wBIKvVZaILwPD+U7DfScPLtAjbx1iNa8xRlSvC8rS7sET3+8cFxMNxrJ37xsbSo8IrfKu3z/jbwEDuk8CtAOOiVLIzpbQtG83fiau2xZObtl+V88C3uevO2gyDxm1pW7QLZYOVu7ST14D3282ahEvVdHA7yTK7g8TKcxPetrPDowUSA9liukuv4uxrpeg6M8ZRLJO0lp7DvweqU8OiHAOwbu6rwieA87AAqGu3INkjwDvRQ891WGPNxCJLtTerI7BTEFvPeM1rxKMc27y1o8PFhdGbywHQo8fmSzPHApBDw46Xc83HZqvCx5hjsZ/8q73DSWuji5szpT1rI7k6jMO/5Ga7yOI7+8fychvZkbXDtMRmM85eeFPNXEvztpTWY9avAyutFtXzwS7qM7RYDru3PI+jvHSu28b6iGPMh6a7p8WNg864Iau1KZoLssJPE6fhLqO9CYvbseoLS8OZJavJz7ljpsB768rBgUO9niTjz/6y08m78EulviRzyp0vY80Z8evKb53rywRu+7zuHrOkp1Gjw32cg7f0z1PIpZE72T8YG7Z9dDPNimS7yuaqE89q1pvNqNdDzsW1s57KxUvC16hzvpxnE8VUeovOae2zyhGpw7DInguwVb2bxPnra83sEwvDh21Lx0i8u79q8+PMS0YzyEZOO7788uvOB/zTtdL6+8hltGPJa+C7zv8GC58h9BvNX/jjynzWC9Ww0bvMdH+zyIkDg8/5IqPJemZrxtEYM84S1zPM9gFTx87R092/EtO+ieWzykRbG8otuCvJiinLvgYBc9hqO5vLREFTpjrpE8q7J+OzI9iLy9lYI8T3ecu+fwo7tUcQ07H/kIvHTAszvWYXK8siAhO1Vtnbw4G7+7CRGXPIp7Ojv58Sc83zosvHz9Ibx6yaw8JGrFPIUkkrmLAZ48clprvCamsDqchKO8C3hBugQrIjs1w/y7Q4RCun5E5LsCZMo8N48tPJyDhTwCa2+8nbQrPGx6PzzgSVi8PaDZO1MB/DmIHti8SQ3ou+EiL7zGGwg9YxGsPO8DFDt81B+8RdPbOSFLPTwkJmO8i83IvIHnALwL2rO61LW/u9bujDoUigo8MT6DO2X+rbt50AU9CXN7u3nMCrxfvK87JLLhPELdMbw1+c671BsuOrv3L7v3u2g76TNjvMEM5DscaRG9F5z5POPAZ7u1upI87LK9O/kVRDu096S8CtM3u72/SDyzcoQ8M7ZJPBZvsbyve3K79dISvZPhrjpy74q8b+22uw1vQby7xa26uZRkvKgE+Lq0O8E8oangvNE4TTyJZ6W8+O/SvEu/UzzcOxA7p8jVvJOGzrun3dk7VD9jvP3kBLzQ00a8rgu+vEDifjyCp/g80LD2ulbT6jw/D287zDaIvI5dBrpNnqW6Fgn9Oyp9wLs7y2a6thsMvS6UGLmUkJu7zp45vKyj2rxLClU7Bw99uySswDpFTgq8b7aTPPBVhzuvCh47n62EPCngZL2Bmio83heeu02DKz3dYjm9UjQiO1tEXzo+ENm7NyUoPBkEHD0KT4o8xxPRO2iwHjskILA7+GkpvLwIOT3xHt47FikcPIltPj0cfKs7ZeGWuycXa738ToO8ewYdu4+mb71NrOq7gO2oPEjEajy3iOC7HM+IPNJsIzuihTe8qnyRu6FYtbySg4S74Jy7O/Egh7xQreG69CuMvLBnvjyeWEc8eVspvYIl0zxR5jK8385cPGkQoTtWP2o8NHsiPBkdPDzr5+A5Doq2vA9FaTztA4I8X9EVvZHU5zsTed88dc4RvLE8b7yqMIA7t4MKPX2IDztxzdg7ADAJu1fzSLyUcG07u9TAO23AezzqDj08u5lPPLE9czz3vG08KM4fuwfXVzwBNRM8Xr9TvX3OpDzNn2q8+EA9vIKmVDrqyWA9Jh7xPNu9LbxI/ZG8zgj0O3fTIrutgxW7Qs/qvE0s0DwdY7c7QIkPPcwSGr0avVK8yQ+2PBB6pjq+TTI6QuXMO/Tj1DvOkKW8PgRQPOcG/DyUdGQ8aB+APOfdhby3LbS7zU9oOysJHT0iIo+8TZsZPf27f7tHqEE8sx0Vu21Ngby+oW68DMvNutu2/zyVCHY78VBEvQplpjyxq+I8+rmCPMGa+7soOyq76fbqvCAtFzs1Uu06lzb3O7puKDtTpuk8T7LGPJI4zjssxq+7n1HMvO5vnL0rW428yZxEOxQptbxWH947AmQhvD+QrTikyic8MqfyPJS2Az1Hj7E7XDuLO47VojzrE4A8l1+JvUWquDylkpU7S2ueO9nBL7v92r88GWm8PPq+c7wRqpE8xEHEPI0tIzzPfZM8DX7BPNINbzxS6Mw8NRMSvGnEZzwhf4Q8e3PHvJ2gBjwGEec6PeOOO8tLFjzWsoG8IG3uO5XDYzzms2w897/Gu9QKNr1ggy+8fxr4uxtN+TzaPeU8DD+ivKPQWbyT0Vg9V6vwPBhYCDyf9Qu998U3vUvZbT2qYVW8rbHHu6HaBr3YXiS9oOM6O9DNR7zU/qI8T0GoPCGQFDsZUZO8doSVPDAsAT259ts84giAOyprlTyTxZq8eV8dPPwfhTy7kp87DxCQPHdyBb3Zede8bhc0PLi80TkGYf86qTZ7vEAIXDz69wY800sYvLx4Djvibnc8vwX5u+k60jyOeEq804IWPOxeALsiBII7eleSvK+XvjuYrGK7Ik6SPHWcq7weUnO8X1/qPMH2Tbwh3WI7ocfxu/nmNzzHtt88MQlwu2aAuLwAtKi8aVWJPOCrV7yvL9G82AH9ueW1TD0R/AU8MNsKPHu6yDwJLEG8Z4CpPGlrDb2FgJm8gCWCPOH4lTpXmgk92scEPBM85bydMSo87WzwO7HPKrx/Mss79tXeO/ChzrzaOCS8GI/vvL4RnDxcZ1O8DQE2vIHfeLy0B4u9AKxTvZrOXLw+jWs7O3ElPWRnoLvWidi8XUmDvDTvqLzxCLE74gcaPcVZjTw/viy91wKfO55r17uVPjG8HakHPAIeqTtTrOy8lrXLvOjU87yqLb6862lvO4UvED2oNVE8whq8PDlDwDtHiKK889zSu2x5B72i+7y8dPrrvBPEvzt7uMA6Q4EaPK9VIzyoTAo99TSxvDFZG7uMGXy8E4KBvVJo+zyyMUo7wqbLvBWN5bvEeeA8uiKXPPPaGz1xbcc8/K0SPAyTtLxy1Ba9UsqEut9JMT0JO4O8vZunPOQ2tTzh3Ym87UiEPEkemrzjLRY8XCGCu+bdxjyQSXs8/5VxPF1YErxz5rI8Rl+6PBDhUDyZ5Hg8R9HEvKMrEb1VH/87bOsfu+mVXjs0lgO80qUqPcd9I7tHg2Q8ogAavRpinzwO0Ai9qFaqu5iV5DuJxKA8byA7O4fXbjuKFUa8TBeCPLrPbLtsFGU8Ys6nuq+bzTzFpzw9z+gcux1JPTyP2zA97e+JPNv6JD0rHio9EQcDu8MuhDuIrQG8EuEbvNxCULw0jqA8VNEJPT70CjwHzrA80emUvO+IALxHf6G8t8awPK2qkTzhpzI84skfveu+Wrz0HS28VzTwusXbibyB8Gq8C0bTu99ALTtyc8c8ArNLt7I3pzwt/RY89uQYvNUCtjwXQ6a7FWSFu2dsnTyJRhe9KPpmu+hW2zz/xI06a1qfO/tOGzwwiCu98kGeu1werbymRhu9YCbjuwZymzzkRMU7eKCkPP2pjjtg66w8+kqJvAB/qzzNmRk8mY5TuwxMfbv6WaO8cOGfO2X2XLy8lgM7h9HKPMOqkzoN3gs9rucxPQt+UDy1bUC8wZSSvOjhWrolKsa88iizuYrIFTu+kOm8flEZO++gSrw2Nwo8sUEJvLbjMD1FGGY8ua/0PMR59Dvlgyo7AO2YO8yuv7wT/la8S9JQvP8opjsEEHI8oc6dvFwkTrwhcPw8gseNO6ucYruDkpc8U1bJvHZ6Q7w6JFc8fjIpu7FmUrwr6PK52wKHvHf/xTzT5Ak7gfsLvf4h7bpLMcE8Ub3cvCuTobzhoFm87vlFO2BWNr11G888uSlmPMHcFzzqUN08AyKzurOnvzxLUV08XX0EPVcV/LweLKS7SHENPAYMO7yxdlq8ePgEPRGqoLshAkw8tkuhOxVFiLuAtyM7pYRMvJo2nbvoy2m7ZoOxO/uTkjxPAdK8RYESvEnASj2kLbY8/1S/OvdAHrqAWM67sDigu52j8LqUwJ68HicuvEe6FL0N1Hc7zJEIPaY7A714LSw97fkOvPT7ebygC+s58aL4Oy8u8roh/rm7t0CCvHk8+Tw6Vu88gwuyPB0Z47uYmv+3h7IFPQUW97tsr4S6TNbDPBTKJLusZQs9JIbOu2S8ZLyx4Pk8aU23vLhYmDxRzfA7KwGvvBc/UT0JeqO9E4VbPHX7njxa/B681nLDPNoKg7zeyRQ7OZVQvKB93bzM6/q8oUXgPFG1QDsls1S80TFzPFGuCbwc2Wc8HArBvG3nCD33xy27nglxPFqNIbyGoSa4esV3PJquurtaqd47G8fFPC9Zezx/6pm7h3u0vBy+yDxnIsm8RjdrvAjZoTzYLeA52Fgevb2OULtNiBQ8ap+HPGXPkbxtaBA79WyDvO39mbxUUOA6WOAcugUiUjy7Btm8gncBvS+o/rutQxM7lUNMvaiRuLxkauo887mvPOa4ebwQAvs8ysSCOx+VvTvIZ2o607QsvBVBcLxgmSk6LSu+vNVfpbvFxmg8roKYvCFLWr1yUDo9KL6CupEDbTyCrcg83vhRu0z7GrzPr5m8EbpBPJaSFz3Q/EK8SNyWOJTbIT05Y1k8MO8SvMdVtjwN5ZU8aiDLvGsNgryuzJ+8zAdLPH7OMb3fqbw7mC77OznXNDx7DT46l0+WPGe9CjutzYA7RWk9O8VH5TyiQY07YOiqOx9DlruYH248OtQmvGq+lzy7LZo8AqGWvAfQCru6TRO8b8bZu2QfhLtWgVM9eOIuuwxhA7tUzzO8Jy9Eug9NLDrv4ZG8I0F/PDR9YTuydBO9qGsON4nE2jqqgh+9bTcFvXnyKbzqP+s8fiCZPEWF17uvNtU8MNJkPI7ngrudz587kOltvL33uLyus267KHIPPeFhpLvtthK9jc1HvQ3bAjxULf87wmalPGPEUrtHDJO8Q0Q6OmzWy7u90r68XQpkPB1VprvZhYi7DJ9pPSYVtLwvMvE7skz3OlWhbbyRuMa8hLwAvHnEnbwI5FE7FWzHPCnIljxthL+8wQbxPGhF4bvjMaU6D0bsu0ceiryeHKk8sysDvZu0UrsRInM8wVQKvXWLCjw8NMu8jJqePOUYu7xznXc7/gsXPPzqrLsjgC28aNFtvFJmV7zuN648rLGCO4uLjjsGg7I8UV7Lu3BlzLzohWA8qKyQPGPbILw6YVI9Z1eMPP3ouTwpQDu9VXKTvASR4Ly6SjS7A/VbvFWGOjrFOs47VcSgPD+NPzyM9im7mjsAPBgw7Lv/FzM80mivPAnqT7xVl4E7vv6GPCc6iLwF6qo8Vj7DvPvMrLvXYmO8p/1SPCx477vWUSm9NFDEPCmKh7vkUmM8uMiOPFnAs7wpX4+8fCWtvCOJpjvbJQK7gfx9Oxt1sbzuRvq6rclBPAzyPTx8ACO8agwFuwkGYDw2nxw9rDZAuqTJLjw9nog7ZgkEPcUS37wIwUM8xvsdvDjqQTxggvW7zTyoOgzEorvzOyG8bPR7vMDFOTtHtDa9Xgfuu5Nyvbx9cl480EUvOVk7xzvp+TI8bWiDPKLxKLst4r68IBN1PBqx5TzsaC+8QmwFPNNknjvWTW079sbhPG1HbjxLQP67RFa3OivTCT3ZZzi84AgkvNMaC7y7s9S80AmbvOMUZjxh/iM91rkFvMlWxTyjq4w7MfqCvOnUkLwiqck8heyjPHXyjrywH+u8PE4VPTYTMD3iKB88AfvePDMYgDvNUYO8LnthPI/DND2BMK28edQQvaWI5LnhYTM6yyGdvEOo6buv9v27qGDzvON1hbzVbKk8XjQhuw3ctDyVgh885jroPLrqi7wd09u78G6rvBYYCrzZTbS8PQCZO4SRx7xpAie8OUW1u/sAXTqWrQY90QaYPNXjfbzkYI48qnxGOzns2zzNhBG8XgZ7PATfBb2IAC277OG5vDrRmrz9C428r5i3vH5zy7yLqrQ7fbC2vMDhi7w5RxQ8Pu24uytvv7rrFSc8fmmVPMq9Nbxs8Aw8kPDAvHe8qTyCcwI7BJAXvftzrDwTtlu9PGjDOxytRjxf27C8YHuruy0nTjz111a80LdxuwcWCr1lo5Y8NG9CPBqaKTwbJAQ8eGKdvF0UKD3w8OI8BN4YPbM3jry6kvk71wX+vBd3/bzm9Ow6GpfzurerBjyNHQe7PN9GPBcd0bz77T49nWKnPLHmEDu63E28P3cIPKrXnTpayqO8cm69u3uah7xzYUa8GLzGO+Rp0jyIWmg7bJsBPF7OlrzWdD89VJzfOoF1p7z4NV28CC2IvGO0mjx0v7S8VT7XOyXsnbwPM5c8bhS7vBunFTwx7pE7szwMO0pmxDzYfN+8WasivL7VTDsDeBm8AYgXvduTnDxUUoe8ygrTPD1nhbxEBFO8bTEdvDckGb0yqdi8NQIAvNqqW7yQS8q8anGJOvPU1jx6N4g8PIK/PGeQp7vklPM79vA0POFRJDw/szW8bY43vAfPPDvW0uy7Lav6u13FUrx9FLW8E+RGu4bDabxQlD28xRcTvI0CBb0vVNm7eWJUPJOudDyiPVM9MkVYOojttTuTQdK4+8+IvBLj7LypBw+9F5SlvBArJ7yu28a8+3CnvGbXV7sWPpA8KrqEO6LM8rt3jw08l8KdOy/K9ru78ME7qxQVPWKxzLttvGQ8olwQvMUHDz0GJkw8iQjcPC+jrDzeFAW96DuPvH5Sa7s+6u87Y4aVPMYuqjzmJ+28PzgGOm3UCD13mMe8hwiEPAOenLxR6gS9tm40vDcpwzy4ExI8dKorvEIdPTwoEtW6URoJvPBgCTyGlpG7QYzbu3DYbbxzj1e8wjsBPVCXgzzChPY7j8pcvF3U0Dv//AY8GJRCPVI2tLvZuwk9mQ0LPURRaLwbHkq8MMsQO3sok7tWzfG7ozOqPE7S5btUu3y6mo+nPIeLFLvM/rA8rPrEuZmhGLvqyGU8uak+vFvaeLy0/R88Ul/VvNXVibvHsMi6ozOIvChc1jxCblK7BhaXPIPRRTytz645i1P2uzrInryyXou6OBp/vJOfFryjAgO8kCdgvI8HcbyszGy88akVPMiE/jzFJCs8YYDcuuzOO7xEyD88W5R2PKMQML3LEpI86w8VvdJ+tjwtSvC80VoePJqbnrwAboK8GZYQvKGH9jxKQxe85+vQue1AsDo9IBA8oXIYvPwGk7yyW/s6MtfXO2LF07x/erg7SdYYPKoOyjyplFw8gQSPvCr837pgGc88N8wIvV6amLuULjG8r7UwvPaMKjtWJpo7j7XrOxwS1bxlThw8tXOju4GYCr3nGD+8SKPxPLGSNjxgjPk74cqVO9YslDwTJKI73IwoPUqjLLumGYo8+5JYPPfJAbyrPRy8DuiBu73FmDzqsBa9kbMavHpCm7qj7s08/OS5vIqR/Twl5uS7ZrFHO9jjMbzvieK7SNsYvLjnu7yfS5o8bT/JPDe53Ds4Mka8ZvsQvIBM/7xVW0W8klQQuxqRyTwbLCi85MSTO4WbqDyJ6D68npgNPSp7VrvHDMm84HYpPI5vRjxQwvq8p6iwuqCyj7xa9pU89DabPDYegDwenw+9d9IEPVLOj7yAO4e8jF/dvETJZTzEk5c7cZ1LPJICkDxgs1A8i695PIoahDw2Eye8NP6zvBkgIrxwnES8vlntvNIDvDyuj3+8HELZuy4iAb1XO9o84Dl6u9yzBj0yUfs8+JvgOxkmA7wOuJk8AWyevCXb77y4aQM7Z781OyXwrTufWlO84YGnOysdBTxuxUc7Y7AwPVPdn7rGZ608MT6Bu6t/Qjybxvc8zjSRvHYwA7qEH3U8frPkOmJFCz1PgDE9+BTovC3FSDwjo708/B18vNdQ6btEPgI8gvomuwVTQD3mW4u8miGkO6L+njxS+gS9veUavLQRKjwzJzs8AkIavHsBBb0Olrq6/2D8PHQHmLsZx0I9Jk74OURMMDtFQwe7TplKu1dcRDwtJYc8JxRcPPpDnzxmQYe8l7exvHT+NLwITVs88y+gOl+iq7xRQRS869EKvN8oibyywSs8OUMXuyhUSTxdXY68Y7/CuuSUGTts2RI94fW6O84xjLwWOvK6SVPMuxlJMT05pi+9k8VgPBg9Nzouc3e8hE/YvG5e8Tq9IQu8npg4PJ29zLuPXA69HMytu5MkiDv+CBi9NZPdu1FbJrtUPRa8jo0QPMcLqbsvgom8Vae2vIQPvTyMfL88W3unuewdtLy7IFI8P/cwPLUPmTx4uuG8vlU2vOFMLrsd8h69nw/Ru36hlrys0Ow8ZrXvvCNhMDywZZg78EDSvGu9BDxsKx+9xxqBvOsiSDxPuyO7vhWpuzWeYjx8Vye8C1PaPOTXEzxHmgs7x08pPFvljbzNRXw762XJu1HkvDmerPy8DUNSvF2Rv7ppUia9ZFEJPGVFCbxKIxG96603vH8YC7uOBQE8ENxyu8exHD2TQnY7fQ0IvTg+pjyvGAA85YELvOroqLsaGpm8oBIIPSNN77ugDo28G4kjvJlSALwZ1E683Jj5PLvhHbxjFXi8GXoKvDh7czyWUhc7rpa+Ow8CnLyxzP88Qrk6O/Z8Dj0eCt289cnqPAK02jx1Vp46XjwpvRwyVrx8nq+8Sve/PJvHeLvzmmm6eisePS6DmbvK6Mq89RK7u79Tzbz1cYo7oKoIOzwCLzwZYdW8zc4bvG/2BjuTgVw7Vg8fukYMDzsJTwk9XSGivNUxsTwUvmo71e/TvNxb5bsOcu08V3jnPMyLMzzw/6k7w+7AvLmqlzzeEbe8TDunOw3y+TuMvz28A6IAPPQjXLwP2uu80Fnju0D4MrxCVDQ8StSYvGTYobv1rTW87fuYOmlKSLwmJl86D1DZOwU3dDl2AXG7v0gPPJSLqTviojW9uWOivLT7rbzJK9S8aOH7Oq9qijtm0so8Y4j9O8HT8bqa5+M8bXapvOWMOjxKuCE8x4iKvEbjc7zOsA28TEhqPV3/3rwPzww9CTcOPJjgn7smYc28VMEqPcqDw7wg+G88LNzFu2B5czxoQYw8+nr/u55SjLx8i+u82Xc3vGacfDyeyyE73wSSOn1JPLy8myQ9iDo1PHyPJT2wvC+6e3mcudd3y7xN56O7Y708u6Ww1TxjRSO625gfvLhxpbwwk6k8MTQdvWGey7wxhjS7Vj3dvCNpJz23tGE83qZoPDI7/7yUkek7fakPvAN0kDvcASs8Sqfsu3cs4TwtObS7HkHWOphz6LsjDrw6mvE6vMhuXjye9Si8LEofvdHyEr0uxcC7G87ivPlH77yQd+871MoMPDgEwjtCxde7KY2mvAhcPDxVK+K8v6+Cu/io9Ty6IyS9wg/nvHlDIr1+iBI8M2m4u/WmcrwvVYG87Uesuyhv6zwRxJm80Q5SO9sHdjzh6RI8QPLfu0ph2Twq8oQ8woRGvCYWH7saPH08HHmYu79Tg7wVNxI8BVvQu29mpDwcKoa8YuvyvHwoKbwd+K28svVWu6LW0jtgNke8/4y8vBu2yjwUFya8GgnXvObMOj3BPjC8tF8JPIv6BT12y6G7DCE/POWqLzxeTKu7i1H6PMu6dLxQQpw8Ba8+PKScBT1wgDm8N9A7PLbFBzuxtxW9Gt4DPYYRgTziwnY6p/qHvEpgwrsWYv47WUJbO8jS0rxjOws83uScPKluqLkVfb48RdH9PEZNfTqsko68mDSWvF8OvzswZiU8/GBqOxizkDvglxC7DHX/vAQ0mzxfD3A6ER0Zu6i3ybw3tl48nNp0PPwNqjs0rfi7hmW2Owrnc7zLC4S8e+hEvOuz5TyBwo48BSM8vLv9r7tX5sk8U1+YPA71QTwhKBk8pdOBvGBWg7u8tzo83f+qupdbY7ywPJ68amkSPI2C1zsP3rK8mw+HOz+2UTxHy6M8dND7PF3AODxRyHi85OxIvBWbvrwiEKE7pqmWPDl8vzyC8OK7SMUbvGKBmrz/H5K8iwShPPyTkTkc3KC8emqJvGJI7rzG3O86e0ahu6q9z7u0XbC7PO0RvZkZRjxlhoa8f8XMPO44STygpIe8Ko63uywlZLziU6g8FT4hPGou1bu8eUg8njLXPKmUuTxJjrq84G3OOIEUsryGv0y9vjogPYIDFLyVyVW8KdozO4yHlTpY8k06dQGQO89A67yWC7O8sxqmPFPFh7yBnTY8jMs1O+L4kTwIggc7aXfDvBK7FzvXmdI8kQvsOXfhmjyuPJm8lqvxvCaNCj39naM8VRWXPIXumDzIYPm8jMsfPXnafzzaeau7FyWfvNDsdLycCL87RGcJuzGiAjzEqqq8mJU9OU3OlbtHnZu8mQ0EPMLZjLvtE0c8x+5LPP9Yjjx/8tO8HrNKvc5d5zxvDHu8z04SvWZSA7vA69g8u++su+eI+bx1Vig7tVsOu8U6D7z/EWe7gkFEvM2DDDzEaoY8JBxJvFojI7xWENo7TUtmvMrGeDyloAG78mw0POswDLseYR28DrvBPEmuRzxZoss8YfRPPKU4qjwcuQO9LLikO4MtFj0GVLe6bv16PM1o7Dx7Nw+8jJ+wPNUBAzy051i8BEuXOyAu1jsHwCA809IzvAXfRryWSdg5UCBAPO2PRTxVw/G8gCdSO72s2Duip4G8+x8QvXtcT7ujU1Q7XJI9vNdoz7wYTOY72GUlvYQU6DzFage9CmYDu5VLQDw7LtI8j+ejvJ+Z8LzQpz+8S33hPJBoFLrXy988naQtPA1f/DyuVK88NqNdvD4JdDwn5i07hrXEPHH7dTxxtt67LFstPL1NKDwWp3K8yCGVPL4IxjvblZI8RSFlPKlGAb0k4qs8COaiPJOuHTyMeSa756kGvR+NdryYGiU8EENUvHY34zvc+3+7Lk2MvA80Ar0FaSg8fWenPCGt5TxFcxS92WOHu9Bl9bknVG29wM/lvMnVa7xzy3+7LN5DPJqsjzwmBOC6o3UfO6NslrtCFc47U7VsPEX5cTyCVOW8+qQHPR0RbzwI8wo9viEAvClWmDzw2BW97VcfPHUMjDrQrwa9KL5Surn49LtKfmE8YpIGPBpqKr1skwo9ErNYPc96HrxH10c8JiaGPMaAXrucCCg8uUsjvCY5PbzEe/m7cejDvA3fFL1oJCe8VFWjvN7AgLuV8PQ7PA2CPOe5cby9egE9p1A6u1nbiLvvxHA8e/6Fukv/vLthU3W9ZtLAvMf/D7xvRoq7TdovPFkxIbxlc1g8rwbcvDYD4jzB5Kk8n49Xu7zjUzw6SLk8tsUSvdYaK7xMawE6Z7GUPAkgA71yulC8/EkavVAx0Lwq9ag85teGvMn/ODtvtoe7E248PHguujwrHMG8yBfIPK1dMrsxING8BNN2vFCFK71w25A8nOMKPcZDyTysLjk9AuLgOQ8wpryiFoG75wpePdn/Dr3LMEY8XK2qvGIGDzwHRsy40HC0PDxoEjwxb3o8d8qhPHmoV7wG7Yq7OMfkvA+rBrzkZwM93VOIPCHOEb1sK7+8zxaTPLqBmTwrYgk8n56PPPjGfb0pD0q8BJ11PPonzLxMNMm80P23vF1D1bz8rmI7CDHZvCR4u7wzP5G73E5jPHGTGTtE4jM8gifrPI9uezwY/S89nMuaPF6EWjtbERY8X9LxPKPdET0L60A65+IFvKrkiTzlLiM8VS2QvMeEDr23dY68tiP1u5Rflrs+f0K7hsMCPbf66DyVdQK8TsMvvMopYDzH2ZG85kphvHD+p7zgREI8utk8PF+DwLwhC+m816DRvP31Qztathy8H8ciOhJ9nrsxI2Y8o2jju144Zrw1tjC7o2U5vH/MUTwcAL67JzEOvc6nyDwSday7+/bdO/Krgjx/uK68/dl8vHddCD3ALxi9oF3gvJKSEb37RGA87aY1PIE2oTxAg6y7x3NAu7g1Er2tgDq8c2PlvCv7ujzH5Zs4He2WvPa1jzy3/ke8++RnPDvfBTz1GMi7nQm3uyXMmjo0Ky09WGkYPF8fYTxyRsu83HoUve+YhbvS20y7MN7hO2c8OD3pVJu7b2mrvGFeszxKPpu7hAc6PKMzv7tLBZA87F8vuQ7nLb3RYku7OIKLu/T5pLzC/oE8ReimPEhLhrgEwyu8hqEPvFqHqTwIuey8D0PXPCDMzTzIcJ08vFbnvPKrnjvnzWC8HwePPBbI0Twfafs8f8qIPM8YJbyvo5o8emZeu9X707zxzQC9m1+EPBxPiLwxZUq8JGpAPPrSAjwfg6O74bUbvKy5ITzYfRM8wRGXO840vrqcbcy71wTvPDqzTTzOFaC8aqcJvZdtMz0WAXK8080PPOX4MT3EYgc91XIuu9WmHjzBNfi8HHJGPEmrzLyV0Pm7GIV3OmAGErxrDsy8NCmNPKmWqbhLbSW8o40Gu7N957ybhL28NN2SvMIAojrePs47ubAcPF6k9Dz59Zm80x4avaEUDb3r53i8JpFQvPd4zzydGIi7OVC5vIMmOruwPIO9J1YyvTnCurstMt08vaTXuyOSHr3nX0O8L/bTvJ4JRzwDrri73lPzPFU6SbxyTLW8pDMUvWmYJ719uOq88OnDPIfC9rvP4kM7W8/Au0kipDwW32u8aD+sPAkQkzryZQY9sNciPO8ZLb1LUxw8/bxuu3z3Az2lvSS86ROAPNi67Lws+1q7+jf1urXLo7zDPzu8nK4APGChvTwpJkA8sstjvCQt4zy+xeg8/qrAu1KLHDyks1k8P32QvOW06TwuISg6kXqJvKB1/Dt5BAQ9v8H2u6qrrjyTvkS75GXPO6Ae1jx/VRo8vfZZOzMJuDwAujE6GvGrPNZXpzy0kZ68geOdvOvVxLz3kYm6tusXvJKZzjxoppI7R2QQPN+CITztB5k8YbCHPA0g5LupQea8XJIZvIqUkDsbVCy88NXzu2MJtrqWCNU7WAmBOjSbyrs3rJm8TNdZu+f8sjyIUYk8it9EO9IjJz3hNA280swLPcF+c7xjckM84Fj7ux9r/DrBAKg73kJCPCmniLwAy6+8oWEQvXoBHLxyPNM8w1Q3PH25NLvqqPM7V9OXO9lrg7yV2fO8vzQlvE/n8jvaCli9vDetu2j78DlHaIG957/VO2jN5TzcVvQ87dXtvKjRcLzw9ji8hn1SvG+WgbxS2+08f0nTPHY0J7z0ISC9sD9eOTJDPTybhVK8wFsIvfGsZzzIqAQ8QjcZPOQkNTy4cm07X4D0vCnsLL3RLs+87LZGvA== + 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: j3S6uegpmDy4oW+9K9iFu9u0BLvqBoI9zWgDPWmroDyhB7Y8/41ou0GF2rwtBRU8pN2zO/MUHb358Sg89ANZPDZsVDv5wDE9l0VFvFwij7n3QWK88UK7PAfWHDwhI4m8F2AZvS8SHDwymO+8R/eKvRWTsDwWF627eq9avc7TDL2kKDk8IV0svLPcOjv4SE28mB+nvMCdoLvA2A29ACOBuaVrlrwvSoK9Y0FIPDLz+zrWAIw8GA2dPObGhjwmWA29d2QovPGhm7xWnFs7u2OAO+CABD1JgIu8C4hgva0YoryKIJ+7PGyPuqyWQLyJSoa6sr9PuwP6Tbln1sQ8XhcyPIrufrt1FOO771/zvGL/mLyi+7o7dOiJvIYL4bu0TB89eqwmPOBWnju/h2O7aVoJvUYHHbxyA0s8HhzAPBQLETovVKC8RoPMPM/wpjxLwDs7GG9RPAckAr1hEm272u/mu8t0VrzkbhC8Yq+yPGeCsrxVMqo7O4IvPUXoM7y4Djg6RLzRul1WhryTEpy8r84kPBE/Obsj8fi6s2z7u7wrk7x4lQM8BiCLvDNM2Lw7blW75/zVvByJxzqkIAW8DuLXOyMSQT0834C7MlqjuzSrlTtvIv87YIW5PEJa0DsRZRk7VkHRulD7qTw1o1s8hmPgO6WCUzw0wZO7yxqbuvakz7vZLgA99mfXO6udjbqqf/s58adGvRvqdjyK9LS83aQlPGqqiTxkHp67zeMWvANMizxayRu8WomFOzgEHTw4goy8cGyrvO2quTuJswE8y4ApPK5enDya7Fw8rQTGPJC6Hzzp2pW7WwUhPNlEort1I/c8fGNHvL6a2jy/88I8WzTPOz29gzv4Djk8S+OvPDsx8Tx87ZE800ixO+0hUb0mMPg7+V2DvPT5gTspe7o7XuYyO4Nam7ti5Ay87hTlvACl/boWnBS9OIKmOS2ZzbvssM08g3gIvF7Yejpc/oi873pKvHAjqTwegZ+7ZZm6O94qmDvQL0088k/zO/x1sLx7JgQ91N6RPDjAETt86l+8rHCgvKlrubzj0PU8RcPbvA54ojy8U8k82LYMPKjKb7yjoEu8kKhiO/WI/buUtoQ693aUulzxAzz0c2G8/O2YPPyVUL3DJbq76OeEvH7f97vVqeU7WTNsvMOPjLspba88edT+O0xBmrv3SBQ8iskmPPwgKTxNCia947+3OY/xtru6uMK8BXdiuj5EnLzND348RBL9POHJ3LuqdXK6DRdptYmjJzxSmSU8OQxTu/U4pjymXXS84xgrPIpJxLtqlVC8NYjbPMEPnzwjb328m6GOPPPPsTv85BE81h5kvEpaELzZOAi8KC/LO3sC4ryehj28g9J6u5jps7rBqc27RGRrvLZS87vxXOc7+YPnPKGYeLx6JQS7SmtJvHAte7y4/gK9SKwtukDMALvKMQC8lN5aPRm6ZbzUqTY7toZTPCrtFT2teJs7oD9avAcAcjyAeBM8dyMePTjKa7yVwFQ88ebXPARk4jzYja48GLFiu1v1yrxExgY8el8avILHDjzGh7k7CCKDu6n/sTyXHpS8H72RvBvkAzyWaZ25EEOsvAQckbwUJmO6ndcdvENGtDuytJI7zUkVvXRpT7x48Aa7d5cSPG87bjvsO0488ibJu6/fhLzz5cu8y7dCO5WobLtWfxY7VenavHxZ77o8yXm8iG7kvGgbGb1Zsnc8IB5pvHt2ML0uyss6mt0NPZNWRDwLsgI8s0isPFqVkLwdUyM9yC5nvOTE3DswLm68or+ROwKWnbwe8hm7NEevu+RR0Tvo/yc7pUMNu+wMJrwIW7c7PkjFPLdkUTzwmdy8jr0nPD+03Twmw7G7pB5YvemwEb3CELE8QD2GvEwEKT0Y/mm8/Y2RPKvHljtKXke6yo2uO4o5AD2EitU8x46LvMZiHb3vuBu6KOK4PESbh7vFYqm8tTvYvKURkzzB1hK66TbTvDnd4TuTTSu7j7aaPD3UprtRQbS8D3KVu+/mtDwBni29J5qbvE45JDt3OpI89dr4O32TJ7zC5pk7j1pOvLqgGLz8fzA8xHASvR5HkTw5BuM763hSvBPrvzoT/os7ttFEumOrNriX2li7loXrO9Nvsjzi5dK8Vp7Au2P3JD330Qq8oZgKva32FDs94ei7W5nlO0RkZTs8XN88ULYWPExdabxk0aw8dxYNPLr9ADw3JV+9EUaDvP88FD2ld+Y7migAvRpUc70uGQk8QOspvGgzsjvvS6E8VqTNvHsX9zxQHg+80XnYvPsnNLw3JfM7HG1+PLipPzyWVOA8GlezPEEU4DyfNF68kVF+PMNaFz0G8ww8vzWwvDdVt7wCPQy9jcXjO7nWojvOxQ49VziJurTlsTyDUnA37+ymO+Bq1ju4CGq8rHoOvSHWwLuj99M7IYLruwrhQzyVymm8H9FnPG/Xa7zHqIK9xvEOO+1M1728uoa8ueS+u0Y/qLyzrEy7v5TmPN5BHbwkLKa8BJ8CPbdpGjwJ61+83QaLO8KvT7xllbI8jcrWvKwDhDugmQE9XxCxvHpfuTtKmLM8GR4fvLwzULxhGko8SRbQPA6iibwkohY9h+MSPJQyFz1o65O7erEVPJWyoTy3EkY8EIf2PO0DDb32eAG7zQsNPBppDD1TDnS8C5y6vNNB8jsQAfC7waMEvJl/wLz7h+S7YpELvW5n27tI6eo8i2YCvH0X1LtFydI8u1SiPGPmtDxjz3U8aHNLvSIU5zxC3B+87GDwu9QuiLwwav46vHxrPCiTQLrK3xI9S0w0PN0O+rtP3iE7oeSxO+QlNju+c429SaQvPZN9fLuJSpO8kha+PDwqizylo9u7MF6dvJ9+H7xBP9473etrvP/mDDxFFtw8IM0JvLeqUDynQJk84WxePAMqKby6mRg9v96gvFCvmjqdVhq9x5GRPNgj1Tz6+3Q8c+MwvKodbDtg0Ks8hO40PGwD6LxvSIM7JF1DPDqWfjxCtrk8DOKbOzntwbsBqmA8+SaqPIlwwrxJuMg8AVYKPZH71jqShV68i3uiO68ZejyjGcc7O4pyPDG23LtmcMq7kvwzvC2/hbwYyoW8zW7au1VYTrxE12w8ciQeO1TbvroU/ao83vsZvePyz7zGYTi87JzfvDyFjLrvFc+8hoQwvEuvXTyycy28ZQfiPDXatTzC+As8BDSyvK6dMjusoj+8jX6LPKWiNrwYfa073RRqvPVBirw3+os8BI34PJZPljzoBDK8pjmkPBXSuTtcQkC7XRkivZ9UBTyBW9Y8dvc1PEWWuLwmuiu79QLDvHhBDr2ud0i9bg7fvDWmPjxU1w084JoLvUNFfLxwT7e8UIVQuxcn27wysCS9TBfuvHJJVTuDOOE83t0BvRdjlDvx/VC7M9EUvSrzHD2y1708TQ8APKjn3TzlUuI8QmkTPU0Dirvtz4c8ZfUavfAIlTzmfAo8DnoBPeTZ3Tu2OxK7b6yQO6ivO73NgDk9GQU5PIAsPL0x+xo91AeTvXvLEz21LN+7FVeFvNQk4jvIroU8kW0PPY3KTb1hcni8FWIWvXG2OTwSnXM9VZ7jvOf9ULxos1i5uHlcOqzQl7q1Wly7y+k0vUYOLzwDnyW8sA1YPFKLxzuw0Ws82F84OwrzhjyGhu48q93qO3bS+7sLmt08f7+hOx0xirwfAFA9aWjaOx5nojyIL5M81v2hPK4YJbyiEMm5W7w1vLd+Pzvavx+99BYHPOI7DL2j6sq85eiWPEBSijsw16E8TOYFOviTETwRhRe8G/zbvM3PrzzM2As9zsFWvHXzYL2Dn0W9QKabvMS6mTxPnDC95P57O6y8QbyV/dk8SlHFOwdkfDzwxaa8GlwcPM4OfLy4yxm9eW0kvOXwbrypnRq9fbXLPI3Cpzvhmgu96MmbvMJXGz0SDRK8v22EPEouXDyMrjg8BpuOPKN6EbvadYw87uRSvJ4+mjxhK888a83MvGkLwzsTc7K8Yb2YvLJ4D7zx0AK9V1UyPUj5mTwG8647y6TzvE53bDyJsRw9deIrPcxkgrk7K908GTkZvL5MD71JGwe8yTWrPKlxarpcmZO7XVG1vCzrKzy81QO91rKUvPXMUbz1TiK9TQibPFII4zyiMQa93CmxPF37v7tfeoC8/UkFvDvOIT3gA7258kwHPQc9CDyH+5Y78i2MuhoqSrwWBOG5/weevDc2srwKBH+7yXzovGmxKzxHUmY7KzY6vKgXLbwtBpU5Fx0GvZMS9jz9ogi8lVmHPDIfZLsHoQi8kEGvvE+Stbx0EwQ8/1cFvNpi07yb8/s8DBG0PLweELzjIoo7uyPbPOuAe7wTAIS8k72Iu8oYTLx5g+i8Y9/aO3I4grwA5Cc8mvAsPQrPIbwTr408LooPvGyDXbtl/uY85ZSou5VgjjwGAZG8f42hvMBNfzvmIMe7W58PvCCLCjz/WE48DZtFu+GLq7zmpiA85UoFvcx0rLwH1y88wmwUvAmaMLzIAUk9g/43PBWvNbyHMCA873VxvDN+q7ksQJO8riDluy8a4Lu8ZYE8JMS+vL3p2jvwQ+Y8JJyxPD51vrtGssC8pRriPA98cryWdB88eRyTvKEnHL1TmwC9yg8ePCrvsbwwYSU8xBmZuuW2mjyXQwg7bQVZPGHchzzKy1Q80wvSPJGKsDyODNw8rwIMPbmyxzt0mTy9ukHVvEkKJz210pW7Iai/PAddhDxRqau82MXpO862PzyhQ5u5JKBiPasoBjp64w85EJzbPKyFubzTP4u8rT+8vAPpi7x8Bem83VIuO5FeqjwAcp+8VBNwvOh84DyC7sA8gGvBu+sCHDxG6m46kZlbvKARtjxSnTo86C91vHQYEDzxeJY8yfoRPAgFjLjVfkq9TunAu035/LxY3pc8ovaKPNSUMLwP8HY9fNtpPGL5sbwKLL28g2GtvCld0jxIFlK8afHwu/HIOb314mU8jbMyuodanjwHOfy7NPszOyPAmbzrDwk9PsPAPDJYIjzjdWe4T2VjOyVAGr2X9y8861KuO3dBgT09AlG8jeW0OqMWATy4ws67L+qgPPfXGz2fJxG7wu09vKL61Lzqyhc7anI4vAEmKr2Vxww9UNBmPBMstjt47Cw8Fsfou9QvgTzl2W+8pShhPLGMxjxCfPI6PIDCvAKJiLy/2OW8hZlBPAUHfLv7Xhg8hy6kurC0C7wOY/g6PHuxu0es07pgcQY9QmOuPBAf7jvmGni8XN6xO3W3YDwRV9s7N6KRO0t6YDsH2Ek8hpXVu/O+EbzXrCI8nHEJPG/+obuEMfg8w4PQvAwA0Dp3n6k6oST/u3cY1bvJcMs8mh7xO/Qes7o7ZSk9dLT5PPJuBLwl4y09Ax1LvE3imzxSrU48qlyVu8sNp7xGjJC7vy6bO6XFfbx2ieK7MTupvOXZIrtWV/y7Bks4O8E+qzvzoCo7x7ytvIdxs7tye9i8wj49u7FokTr86+G8tI4FPfEaoTyquQ29Fm9JPA05FDxgbQg6g2CUPB7QCTxdsXE8ExKHt/bA9zzEyq88Xnr0Ogt+47kzrJy86JntPG9xqLyEN/K8f2adPA1U0busgaA8o/lqPDk4MbxlrY888WR+O6tfgTxU4ok7zfOMvBW6fzpzAxM74w+qOylIGLtS/wq8PohCPFPECz26PYq89HU9vGWDqLmHTgu7MPWBPEUfcbyouZ48Z7kZu0f39jwsOhW9GsI2PGIGqjxz4MI6zIGOOzCcu7tMJgS91K2dO8CBDL0aJuk7+g08vWYuMTyUPbe845zdvGz6/7vm6+u7FSoIPfUHljgS2cq7FzrpOsQHgTy4AyQ8EH8ROx4vlzw6HMU8BrCuvJ4K0btz2428DiAuPOQu2zzBJe06HGIfPcfrhLynLxW8Cm69u+F8hbtRmw8903HXu+1Bvjparac80DWgvAVVozzCoiC9qLIoPNRuFztICRm8vKNMPCEQ2bzQtze943s0O/PXlLxRems7tbifvPUULTvdwj27rOWbu2eeVjxSCKW7DPKBvJi68TxoPEe818ARu1Bo+jyEEbG7oncJPULFtrui7Q29Bk/FPCV8BLzM77w8GFSPvHa06zxUUwo98PTmvANNfzzBwny6Jf1YvNNSyDy1UgU8S9ZdPGZqOrwuGuI8FzPFvPVLJbycj7i7VpqDPNDxjDy4hbu5zflMPDYVNbzsIBI7yxvUvIHl1rt4lk488IcTvGy3RbwClgK8UPfyuZVG87y2sZW8CZ0EvdqLw7y3DcQ7V/3RPPMHT7wD41g8eq4PPaJ9ybx5i3G80bHrvP56Db0PSR29peOrPPmsqbwZh/q7NbuLO7CU1TydB7G8607pO/dEEz1mQCA9PULMvKQpY7xJeku7x3rlPIAhwrxYY2G8sndZvGuyQ7xvoBW9CUjaPAWEIbxE7Xs8l3/1u+JipDx7+sS7V9V4vJDLIzzumLw8DoU3PWiitTxjV5S8I9qQPKtpZTz1bH68yHh/vJjQsjsT1gA9utnZPO2I07xTGOW8OJqtOhMF5rp3HAq9eMhYvE6gGrzi4Ee8SdmGPBcl+rvfO208qZ9XPA+nJjwKLX28smgVPJlreTmlUbC84uJkvHDdobcoRhY9/p7tPEAUArzb+YM7BUY9PNQHBTxdEAc9jbDCO+pOJL3b95Q5YFS9PDHjJL396M66pigOPSPc1bwE00E4TTYjPO2YlLwPQ/A8tlXOvDpRlTyj2K48llpxPLO0iTy4BTq8ABG0vK2ZJDqRJPK79SGlPGKRdzooP4A8XPRIvDUzRz3rmQY9PAjTPBd6gDtFA+q8YtELuGjtcrz3wbW8IdsJvL3Ys7yu5Ti76XOkPO6gaTzH0y28KWiuuy0I67zgo7E8HfkePU4PKby80b+8fLO7PKqSYby0mnq8ls+IPMr6d7whvX+89OwBvJYrcbyum4A7nLDRulnoFLwl/NG8PcrkPN2TADuS54w8Lj0bvNBbCz2EHMe8BOeMu8zpprvi0Nc8Dz3wu1hdETzG90I8lpU1vLEwX7yMady8R4E9vBmJE7tdgyi8fujMOyy3dru2qfQ7VfSTu3EAd7sCysC7tzmoOksrrrzGYoI827EIPHVM8bzeDxI7g6q3uzv8Cz0bQMs7cGrQu6YMHzxoeSY7NFLbPDYEobzCk/m8gMsaPFQTFb0+WIA8OKKeulKEuTx67R69+FiXPMmzuDxZN3c7T9ssPe+mcjwd6hQ8D+hnPG3ouzxR5iS8CHzUvMTGVToZFwQ8LAElvHc8yzyJmxG9gJBxOzbWJzx6oLw7BVAbPYMjhjzdtrM8HPZkO4IWsTxvhrI8Hy/ZPIoMSLuqj4s8xmQ/PaHPjDz88388T1HBuiO/obv4mko8/OG4u1Igg7zJ/W081QbbOp0F8Dy9Nfq7vj+gOUkoGT0wFUo8btqaPJKMNr1wxSI80M0VO24U8jxa1VI8HSVxvE04Bz06kTO9hzjNvM5EobuR+I88H+HtvPXWOL1Wpye8IgZjvHX71bxjdhC8/TmGOwgTbDxt+7286WttPOi59Tyiwfi8wepdvKyYnDxvpZ27H7CfO7xp4rxd7jY82wpZOkd4Kz2wmo682FMIPDXQIbwkg4O7LoixOQdAJz1RUNm6yvE8PAh30rxssMU7/zQHvEs7A710vsG8DiwyvLuGBD2HVlA8PXsWPV/jmTyi0nA81yM5PM1+4Ln8ENk7+iG4vBudULoRJ4K8w32OvMIOFjwLg6m6yk50vFppNzp1UsO7678WPHW+TbuJYeo8WWiZPOXyUzxDi0C7keVpvOMysTyN9Xo8iZeOPH6SFDzEewE9nX8pPXimwDtB4Yy8Buu0POR0tTp/CYy8lueIu3U+NrwDFB69IeF8Oxs6lbyL1z88xQ4KPfPBjbwqv4u8iqD4vFYZp7zpPkE9B3N2vE3zKby6fwM62CCku9xIybzqt7k7jq1zPHFeujp60rU8RB/FOxNdSDzd3sk8oN8TPQBNYTxZwmC8L+2KvFclXbsm4SS9a05EvBSaFroLDoa65mvju2heNLvtewK9PjT7u4H0bjwShqY6hKbGujLWnDycuIS8yuwDPNHF1zyaCgG9zd62PCpxAzw/Vnk8AnO/u5NoMjyLs1k8e9nivCp2lDz2GXg7wxpXPDb0m7u+KFi7QWXuPLD60TviNYQ8NEZIvFPyZ7tG6a48Nnbnu52DTTwnwwm89cfOPI54ozxxHJM6D1oNO1gIKL3t0Tw8JcYWvJG1SDuwq3i77QNVu1hF8jznpFy8DTiTPBOxqzyJsrQ8oKGmvESlqTyJeDQ9rwMHO6I80Tx8Yfg8f9CqO5z68Lu7ZTw8Dn3Sum1PuTwAEaC8/KBQO63Umzu+41O8p4wCPfXK4rkNw+G8ELjjvDh2R73MIIU8x7yjPAKY9zwT/5Q6wgcAvPskHbuz8iK8wqPbt6WopLoqKGE8S9dbu4W0ljvVkda8+p5CvPykqjvcCyS8WDJiu4tbvDx9CiC8FAKNvHelqbuDzyY9geuBPL2fTjztNLK8EHUOPN/CH7xW2KA8y9kPPUllJj3xXsu84Q7bPJ4KWTpUgom8U16fu30bRDxKYfG8djiBuZ6XxDx16oa7reaBPK8J8rzykQC9/eKku+1YSLzRN1y9AYKOPEgDoDxNB6E8OKnKPK5+BDx5K0+7dYgzvD9m6bot07o70ONKPIn9Q7uuzJU76ksFvFnXwDwJfN27quIdO5iWrLwUQdW8KlDUu11L7rwCIrk85ROkO+d0C7qD6h+8PODIvMpH2TwsjK28qdkwO9pONrwf7NU6j9BVO4ozRjyiZag7jzCYPNN31zu2kjw7A4MbPKrViLu90uQ7auK7ux+rD7z2r5q7VW9SvDWFvDzIaBm9B8bDPDxksjxqG+K8My36PEsQbzzluqy76oS9OylVQTrSv4a8C7cevS97rTwA8646NpSzu1K0uLtnXM28+26hPB2jFr1rXq+8UJU+vPS9bLqyXxE9teKYPJAyEDyrQBW9IDqZPHlykDzpdaC7Agzvu/9GXLv9ZJm8c4z+uyDOJzwVZ608d4FquzpKXTxt7yE9pMKhu67KYLnPIwi8fae8u6ODDT2flmm8vzLFu+rIbztbWzq8Vj8VvTn8H7yQ2QC9a0mwO02kITyF7fO8ReyTvP8mCTrYdFa8byhkuuczjryeiT68ZAaIO9VUr7oxX1G8i8eKOzi11LvVe2I84XZ5PG4NUbuVt7i8Yl73PC3k8zzfQa08Rf63PK2HCT0EQDS8OgkEPKrF7LxxkPC82Oq/PA9Ek7x8e8i7Ipehu2njxjyCE6S8Ps++vHIFxDz+Mwe9enePvEC8eDu4XNE80LQDPGPzMj0K+Ao7LwAHPO6DQzzcY7I7auoOPRP/aTzbA1A8S6EcvROE+zvhEYA7yJnvuXRFPDstseC8vOckvA/xDbvkO7y7LfpMPDLw6rxEJSM70B+XvFdWbLxg59K85tNAPb6+bTuWkkC6kB0YvELZMT0equw7StCtvM9cCrw/DBm8v5HFvHTgBb3HnZa7CvkQPGafATzrWAU9glAcPQzvqbrzmRG9xitgu/FBW7yM4Aw8hVEPPOQt3TrdIBS8XDvVus5Pgbwnedo6DV6fvAfB/LtnHXK8WFDguw8zTzy/9oi7z7n6PPwlLLy5kWO855PBvJdvtTyUI6c86SlZPDz+jrvrGYW8UoSCPNZ9Cro/yY88xexAPIU/urx110A86NYwvOmzy7xG71w6c2MLPGU55LtjgQw931XSO2bXx7zc6fG8df8NvKaXDjvvII685tYevRBJFjo4JJa8trbSPAg3Hr0/5uU8/Uu4u7fTDT3+wYQ6L7S3uBh33rxV92G8u/kPPVACDjp13nm8SFT/uj/zOzvDvpE87cJ7vKAVWDnmMfI8tEGvOokjKTygFLm8x4TaPPjBezul4vW7i18rvLxqoLxY1eM8JvSsO3B4WDypsvo7y8yBOxSUoLzll7y7Tg0vOy3f7jsGfse8tc1ePHpVPr1lhDW85+EXPDr5ET2sLsA8u7kIvU4CSzzVGou87IgYPEr1IDz6yjs8zDWCPEXAtbyyiO668rGtPMK6xDyDKwK9PB/xO733MTw2e0w8dsIwPQl6NTuOco086W0WPIRMyjsHE5e63GZ7O7LsjDwpQMa7QW4IOdmlijxjYYg6naOKu7W43bwXgIs8/UKRu4aK9TzdqhI9sbdsvNnZJTxr+mw8u80DPJ5UdT3NEAA8tbUBPPK1gzmP+tu76AhsuwoXqjt9DYe8I/yHOcVskjw6i+k8c/gtPEacebswbdq67qg3PPPBVrxgXay8ItZWPJ3Vc7sdBqK7RiRVPMd8rDzX02K6hBj8PIzooTwPO928RQ2zPH97G71n+Rg8AZhWvPnYqrsGt1i85X2GPP2FWzw1zIQ7AcAUPP/Mo7wX9wK9N7ZWPH+ZqzzTVmU8iYPPvA2M6LzcJBe8EGokO9cioTw60NC6GXo1vcxYubwyDJG7wewQuhjXpzzhgB+8QUrKvE76F7yAvKy8nkFWvFK0LTydPLs8tSYtPBalbbs45yK8nvWpvDxL0Lvyt4q6S9K4O0Y1pLymY1+8r4eeu26FJTpgiIY7uV0sPXGjabws1Bg6hLw8PTdmB7zJpPq8vX/6vLKxB7p8r/w8XewOPPbmgTyh1mI9GgzXvKhWnzzZn888qQxRu+6hHj2E1Qu8hQ9tPBoRArx0n0E79OupPII+NbwjnKG8KbqOPMTDqLz+oMI8lDJUvOg4DL178b48qDfsPPvr0bxxYri8QjiTvOG5nry1SSk8y9UXvCrXrLqyBdG7cZ+fvIyHtzz0jY+8mHYovCDX4Dxn/Xi7Tj8rvPwotboKdJk78GLgPA6Zbzy4H+W8TfCHOpYZLbwHSZ27WiLRO1Or8zubyGE8+mu+urxvPzxjInC8bvIfvDmP1Ls+f5+8YRhiPAmKHTyFmKw8HQZqPEjQYLzPz4m812bFvMBQKzxbr6I8V1pHvC0MO7ylx+S8WQhLPRcaODxJtBW9HwMxvKt4Rjy6myS8B5ixvJSar7s6Wg687TawvMMKqrsq8cK8B4XwO6Rrsbwqr3W8CzwGvNN9WbxhRxS5hh2kvBRpFb2Upum7skiUvAQcoTuRuve8E9v/uZRFDjw0oFU5GzO0vHYhUroDzOk75PC0PN4mMjzMAJE8oqoRPBcSwzw8S6s7hgCxPGwRHTwzrJM71o+pO44LoLoQmb+7iRAZPAPEuTzTQIy8JVcdPFMpeLxAnVi8UjoFvH2WgbyJE4i87l8KPKcNGD2uIdm8fEEUPLs4x7x4nuU7OH5SPE9ds7xShew7Sd8/vNrun7vilK48qGa9PEdu4DsXDPs8+1FUvCaWcrsOEp48jF4svOyEBL257I2700EtO0030jwjufi7x3jGvK9cNLyiNs87U/2JuxvMkLzci+e7UReyPHmMs7s8Tv08AYpLPDlSyDy78xw8KQooPHl6KjvCA/A7bZZnPB7URbyCbpU8q5ISvDpSQb1wtwQ7fmPcPCZojbyABp08QYitPPfxtTwdx6y7eMFyPBPf8LtY3bK8krNLvD+3Yr0/uE889JOGvPSJErzRPzG8B968uyQphzxLccU8YvDyvEsxF72C0vY6FWAxPKADGrzQ172893lsO//wBr1yEyA89H7dvKreODpEbLm85yGgvDNHgTycTo07AMmoPDjTZb19KdS7yqowvfro5TzAFtk8y6mWPIVyELzsVNS70hWjO0dZ9Tve1Es8vrOSu07lxjx6zM27/1ijPGwyX7pqz6S8nvBOPGXqZby4Q7e6Fe6zOzOQm7xs9xA9FhIhvAdggLteHQE9RVlWPGvntLvJUdE7iPwjO/SgZbsEKSU9lwrxu73az7uYJYu84B0guyAshzzkV707B+EvuxEpSbxZ/AQ8LtyOu6abA70jsdA8l3AiPemDpbwDRsw78R5pvETdljlNgc08f8/HO9He9rzvKfI76J7ovAuFNjzNF0q7wZPnvJsmj7wsn128vDeAvE2dMr3lmm67mLWnvEgqKDwDBYS8g2LfvPY38jwVFhk8gzPavJtADzs2iHq6FjT2uXXsSTyy21+8N62+uuNeDjxonz69L1kBvQIfuLySd9y8AI25POK9ALw653c8Xj3VPMUSBjxI7Em3LJXvO/ocuTzGG2I7sZK1vLLF/Ts+wpK75qQgPPffmrzrXwK9R5YLuZ3sn7vl1GO8zLXeuro5FL0RPug8xA6aO9I19rqaqWC99p3+u7aDrrtSuJQ85LEEvbardTuPtJ88Vf0ivOWSyjyadka7gTyMOqSo2jtsiYW8jbjOvDbZurxWD9g8SyiLPPIjN7rOV1A8MQeTOt5nt7xzsw29zaWtvKDNZDwdpka7OkwHvYRHGLz43V07fo2UO0y/nLx2D+K7H1gVvG33HLqEiGI7mNSbPJTNazy2aAm9TFExvdfPsTtau6478CWQvL4K4jxu3zG80jRpuxk+eDzfD/Q7Q4n8O7StN7wsIOS7IYz9O64IrbteYc+8k2WfvLpf2bxc1Dy7GQpmO69C1TsvOAG8D5YHvZeFOLz1ooo84ePduzeEZjz+S6A7HSRsvJtiiTyW79C8fMhWvHzUOT3Hp2S7ZK2hPA38wLxW4Ja8gIGIPH/IVbxE9yc8G9CcvIY4MT1D2By8TkIjPervmrzFpOE6SMogvEyKg7weuFy77/kavDqDMbwJYMQ8OdVVvP5PPDo/bQ68IViLvPii9zvx0Ui8oleQvLwFhrzqeZE8QS1vuhEizrtYlTE9AX2wPI86s7t14ig93SOVvLuCBLwNwN05tpTXOhigHjxDT7S63yhnPD+WNTz6yB250Tz0vMlfDLtOM2Q8U5lJvLYgU7q2SJE8mBS+vMr4BTtJErA5ymYVPbCvfzybkkS7fm4MPDrJbLyKwF08qrCcvGWsVzyX2xk9ZHeMu6ZkGb26HqU87WwJvFRVzDxRO5y8MLV8PM/ZnDwSYTC8XI0pvSDBRLnrIr67UewsPLb7MryX2Du8nyNeu5rqRjxO/807vAQHvaPOAj0j3qk8PRibPM8ssbybDJw8BDiVPOcslzwZiRm8tLPYPOAC/LytMYs7pctIvPpNILtMweK8BPG3PCzw+Tqd6/s6JveQu0Dmr7uvpbE8sdE0Pc6P+Tw7ca479QbBvOlQSrv/U7W7585YO1oxpLw7W3c73TjevBwkAT3hIfe8tpqmu/knpjw1jF086sveu9ro5TwPYlm8qmynO2OuuzyEqpC7d8I8vG9jADvWxhC9bzwKvFUblrzZEqK69DpBPb0yJrtX+QQ9it5+PLaKmDtz9rO7VCqSPE0qbbxf5Ag9r68BPF60BTy9S5A7foV5PAvYUbmNslk8o7ECvAlXBzvpvJc7h4HgvNduiDx9PQc8gfl/PHHvxzwwJlQ8M30TOjirITzce3c8MzxoPCFFwjuYdlI85vTAPHpS2Dza0bW8gaFFvFprEzy/O7m2s8iivMw147ujHCe88D9CvHKKPbwmXjO89ChKvfpICjtAJg48ec41vOzMpjol1vM7t/yJu95x7jxxnFw8C+YcPbKOtrsGflw8/+wQuwE7p7vS2WA8kZ4kOv5XAj2w6A48HhoFvQwcqryg5tm8gUG8PGv4jDsRPwQ8nDaRvBPi77xtIK+7UMTwvA== + 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}" + )