diff --git a/CHANGELOG.md b/CHANGELOG.md index 7160ea0f..47256b91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ ### Removed +- `haiku.rag.config.Config`, the eagerly loaded configuration instance. Use `get_config()` for the current global config, or pass an `AppConfig`. Every internal default (`get_embedder`, `get_converter`, `get_chunker`, `get_reranker`, `embed_chunks`, `HaikuRAG`, `Store`, `HaikuRAGApp`, `create_mcp_server`) now takes `config: AppConfig | None = None` and resolves it per call, so `set_config()` reaches them. `RerankerBase._model` no longer defaults to the configured reranker name; `CohereReranker` takes its model name as an argument. - `wix` evaluation dataset and its reference config `evaluations/configs/wix.yaml`. ### Fixed diff --git a/app/backend/main.py b/app/backend/main.py index 109fb659..ec67be0b 100644 --- a/app/backend/main.py +++ b/app/backend/main.py @@ -44,16 +44,16 @@ logger = logging.getLogger(__name__) config_path = Path("/app/haiku.rag.yaml") if config_path.exists(): yaml_data = load_yaml_config(config_path) - Config = AppConfig.model_validate(yaml_data) + config = AppConfig.model_validate(yaml_data) else: - Config = AppConfig() + config = AppConfig() # Get DB path from environment db_path_str = os.getenv("DB_PATH", "haiku_rag.lancedb") db_path = Path(db_path_str) logger.info(f"Database path: {db_path}") -logger.info(f"QA Provider: {Config.qa.model.provider}, Model: {Config.qa.model.name}") +logger.info(f"QA Provider: {config.qa.model.provider}, Model: {config.qa.model.name}") # Only HaikuRAG client is a singleton (expensive to create) _client: HaikuRAG | None = None @@ -71,7 +71,7 @@ async def get_client() -> HaikuRAG: if _client is None: async with _client_lock: if _client is None: - client = HaikuRAG(db_path=db_path, config=Config, create=True) + client = HaikuRAG(db_path=db_path, config=config, create=True) await client.__aenter__() _client = client return _client @@ -82,10 +82,10 @@ class AppDeps: state: dict[str, Any] = field(default_factory=dict) -capability = create_capability(db_path=db_path, config=Config, defer_loading=False) +capability = create_capability(db_path=db_path, config=config, defer_loading=False) agent = Agent( - get_model(Config.qa.model, Config), + get_model(config.qa.model, config), instructions=AGENT_PREAMBLE, # Conversations here are multi-turn, so earlier questions are reduced to the # evidence they cited rather than carried whole, and every answer declares @@ -136,8 +136,8 @@ async def health_check(_: Request) -> JSONResponse: return JSONResponse( { "status": "healthy", - "qa_provider": Config.qa.model.provider, - "qa_model": Config.qa.model.name, + "qa_provider": config.qa.model.provider, + "qa_model": config.qa.model.name, "db_path": str(db_path), "db_exists": db_path.exists(), } diff --git a/haiku_rag_slim/haiku/rag/app.py b/haiku_rag_slim/haiku/rag/app.py index 47866555..546f9e17 100644 --- a/haiku_rag_slim/haiku/rag/app.py +++ b/haiku_rag_slim/haiku/rag/app.py @@ -16,7 +16,7 @@ from rich.progress import ( ) from haiku.rag.client import HaikuRAG, RebuildMode -from haiku.rag.config import AppConfig, Config +from haiku.rag.config import AppConfig, get_config from haiku.rag.mcp import create_mcp_server from haiku.rag.store.models.chunk import SearchType from haiku.rag.store.models.document import Document @@ -33,11 +33,11 @@ class HaikuRAGApp: # pragma: no cover def __init__( self, db_path: Path, - config: AppConfig = Config, + config: AppConfig | None = None, read_only: bool = False, ): self.db_path = db_path - self.config = config + self.config = config if config is not None else get_config() self.read_only = read_only self.console = Console() diff --git a/haiku_rag_slim/haiku/rag/chunkers/__init__.py b/haiku_rag_slim/haiku/rag/chunkers/__init__.py index 38d43f22..256fd704 100644 --- a/haiku_rag_slim/haiku/rag/chunkers/__init__.py +++ b/haiku_rag_slim/haiku/rag/chunkers/__init__.py @@ -1,16 +1,16 @@ """Document chunker abstraction for haiku.rag.""" from haiku.rag.chunkers.base import DocumentChunker -from haiku.rag.config import AppConfig, Config +from haiku.rag.config import AppConfig, get_config __all__ = ["DocumentChunker", "get_chunker"] -def get_chunker(config: AppConfig = Config) -> DocumentChunker: +def get_chunker(config: AppConfig | None = None) -> DocumentChunker: """Get a document chunker instance based on configuration. Args: - config: Configuration to use. Defaults to global Config. + config: Configuration to use. Defaults to the current global config. Returns: DocumentChunker instance configured according to the config. @@ -18,6 +18,7 @@ def get_chunker(config: AppConfig = Config) -> DocumentChunker: Raises: ValueError: If the chunker provider is not recognized. """ + config = config if config is not None else get_config() if config.processing.chunker == "docling-local": from haiku.rag.chunkers.docling_local import DoclingLocalChunker diff --git a/haiku_rag_slim/haiku/rag/chunkers/docling_local.py b/haiku_rag_slim/haiku/rag/chunkers/docling_local.py index 85b7f690..5691c839 100644 --- a/haiku_rag_slim/haiku/rag/chunkers/docling_local.py +++ b/haiku_rag_slim/haiku/rag/chunkers/docling_local.py @@ -3,7 +3,7 @@ from functools import cache from typing import TYPE_CHECKING, cast from haiku.rag.chunkers.base import DocumentChunker -from haiku.rag.config import AppConfig, Config +from haiku.rag.config import AppConfig, get_config from haiku.rag.store.models.chunk import Chunk, ChunkMetadata if TYPE_CHECKING: @@ -68,7 +68,7 @@ class DoclingLocalChunker(DocumentChunker): config: Application configuration. """ - def __init__(self, config: AppConfig = Config): + def __init__(self, config: AppConfig | None = None): from docling_core.transforms.chunker.hierarchical_chunker import ( HierarchicalChunker, ) @@ -77,10 +77,10 @@ class DoclingLocalChunker(DocumentChunker): HuggingFaceTokenizer, ) - self.config = config - self.chunk_size = config.processing.chunk_size - self.chunker_type = config.processing.chunker_type - self.tokenizer_name = config.processing.chunking_tokenizer + self.config = config if config is not None else get_config() + self.chunk_size = self.config.processing.chunk_size + self.chunker_type = self.config.processing.chunker_type + self.tokenizer_name = self.config.processing.chunking_tokenizer if self.chunker_type == "hybrid": hf_tokenizer = _get_tokenizer(self.tokenizer_name) @@ -88,16 +88,16 @@ class DoclingLocalChunker(DocumentChunker): tokenizer=hf_tokenizer, max_tokens=self.chunk_size ) serializer_provider = _create_markdown_serializer_provider( - use_markdown_tables=config.processing.chunking_use_markdown_tables + use_markdown_tables=self.config.processing.chunking_use_markdown_tables ) self.chunker = HybridChunker( tokenizer=tokenizer, - merge_peers=config.processing.chunking_merge_peers, + merge_peers=self.config.processing.chunking_merge_peers, serializer_provider=serializer_provider, ) elif self.chunker_type == "hierarchical": serializer_provider = _create_markdown_serializer_provider( - use_markdown_tables=config.processing.chunking_use_markdown_tables + use_markdown_tables=self.config.processing.chunking_use_markdown_tables ) self.chunker = HierarchicalChunker(serializer_provider=serializer_provider) else: diff --git a/haiku_rag_slim/haiku/rag/chunkers/docling_serve.py b/haiku_rag_slim/haiku/rag/chunkers/docling_serve.py index 6398692b..842ec439 100644 --- a/haiku_rag_slim/haiku/rag/chunkers/docling_serve.py +++ b/haiku_rag_slim/haiku/rag/chunkers/docling_serve.py @@ -4,7 +4,7 @@ from io import BytesIO from typing import TYPE_CHECKING from haiku.rag.chunkers.base import DocumentChunker -from haiku.rag.config import AppConfig, Config +from haiku.rag.config import AppConfig, get_config from haiku.rag.providers.docling_serve import DoclingServeClient from haiku.rag.store.models.chunk import Chunk, ChunkMetadata @@ -54,10 +54,12 @@ class DoclingServeChunker(DocumentChunker): config: Application configuration containing docling-serve settings. """ - def __init__(self, config: AppConfig = Config): - self.config = config - self.client = DoclingServeClient.from_config(config.providers.docling_serve) - self.chunker_type = config.processing.chunker_type + def __init__(self, config: AppConfig | None = None): + self.config = config if config is not None else get_config() + self.client = DoclingServeClient.from_config( + self.config.providers.docling_serve + ) + self.chunker_type = self.config.processing.chunker_type def _build_chunking_data(self) -> dict[str, str | list[str]]: """Build form data for chunking request.""" diff --git a/haiku_rag_slim/haiku/rag/client/__init__.py b/haiku_rag_slim/haiku/rag/client/__init__.py index 278e3627..308987b5 100644 --- a/haiku_rag_slim/haiku/rag/client/__init__.py +++ b/haiku_rag_slim/haiku/rag/client/__init__.py @@ -15,7 +15,7 @@ from urllib.parse import urlparse import httpx from haiku.rag.client.documents import DocumentImport -from haiku.rag.config import AppConfig, Config +from haiku.rag.config import AppConfig, get_config from haiku.rag.converters import get_converter from haiku.rag.reranking import get_reranker from haiku.rag.store.engine import Store @@ -67,7 +67,7 @@ class HaikuRAG: def __init__( self, db_path: Path | None = None, - config: AppConfig = Config, + config: AppConfig | None = None, skip_validation: bool = False, create: bool = False, read_only: bool = False, @@ -76,12 +76,12 @@ class HaikuRAG: Args: db_path: Path to the database file. If None, uses config.storage.data_dir. - config: Configuration to use. Defaults to global Config. + config: Configuration to use. Defaults to the current 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. """ - self._config = config + self._config = config if config is not None else get_config() if db_path is None: db_path = self._config.storage.data_dir / "haiku.rag.lancedb" diff --git a/haiku_rag_slim/haiku/rag/config/__init__.py b/haiku_rag_slim/haiku/rag/config/__init__.py index c2b79f0d..1ba11f79 100644 --- a/haiku_rag_slim/haiku/rag/config/__init__.py +++ b/haiku_rag_slim/haiku/rag/config/__init__.py @@ -36,7 +36,6 @@ from haiku.rag.config.models import ( ) __all__ = [ - "Config", "APIConfig", "AppConfig", "CircuitBreakerConfig", @@ -78,7 +77,7 @@ _config: AppConfig | None = None def _load_default_config() -> AppConfig: - """Load config from default locations (used at import time).""" + """Load config from the default locations.""" config_path = find_config_file(None) if config_path: yaml_data = load_yaml_config(config_path) @@ -93,12 +92,8 @@ def set_config(config: AppConfig) -> None: def get_config() -> AppConfig: - """Get the current config instance.""" + """Get the current config instance, loading it on first use.""" global _config if _config is None: _config = _load_default_config() return _config - - -# Legacy compatibility - Config is the default instance -Config = _load_default_config() diff --git a/haiku_rag_slim/haiku/rag/converters/__init__.py b/haiku_rag_slim/haiku/rag/converters/__init__.py index 1ec4c4d3..82edf647 100644 --- a/haiku_rag_slim/haiku/rag/converters/__init__.py +++ b/haiku_rag_slim/haiku/rag/converters/__init__.py @@ -1,16 +1,16 @@ """Document converter abstraction for haiku.rag.""" -from haiku.rag.config import AppConfig, Config +from haiku.rag.config import AppConfig, get_config from haiku.rag.converters.base import DocumentConverter __all__ = ["DocumentConverter", "get_converter"] -def get_converter(config: AppConfig = Config) -> DocumentConverter: +def get_converter(config: AppConfig | None = None) -> DocumentConverter: """Get a document converter instance based on configuration. Args: - config: Configuration to use. Defaults to global Config. + config: Configuration to use. Defaults to the current global config. Returns: DocumentConverter instance configured according to the config. @@ -18,6 +18,7 @@ def get_converter(config: AppConfig = Config) -> DocumentConverter: Raises: ValueError: If the converter provider is not recognized. """ + config = config if config is not None else get_config() if config.processing.converter == "docling-local": from haiku.rag.converters.docling_local import DoclingLocalConverter diff --git a/haiku_rag_slim/haiku/rag/embeddings/__init__.py b/haiku_rag_slim/haiku/rag/embeddings/__init__.py index 2b548745..2df7860c 100644 --- a/haiku_rag_slim/haiku/rag/embeddings/__init__.py +++ b/haiku_rag_slim/haiku/rag/embeddings/__init__.py @@ -7,7 +7,7 @@ from pydantic_ai.embeddings.openai import OpenAIEmbeddingModel from pydantic_ai.providers.ollama import OllamaProvider from pydantic_ai.providers.openai import OpenAIProvider -from haiku.rag.config import AppConfig, Config +from haiku.rag.config import AppConfig, get_config if TYPE_CHECKING: from PIL import Image as PILImage @@ -118,7 +118,9 @@ def contextualize(chunks: list["Chunk"]) -> list[str]: async def embed_chunks( - chunks: list["Chunk"], embedder: "EmbedderWrapper", config: AppConfig = Config + chunks: list["Chunk"], + embedder: "EmbedderWrapper", + config: AppConfig | None = None, ) -> list["Chunk"]: """Generate embeddings for chunks, dispatching text vs picture variants. @@ -127,6 +129,7 @@ async def embed_chunks( are routed through ``embed_images`` and require a multimodal embedder. Vectors land in the original chunk order. """ + config = config if config is not None else get_config() if not chunks: return [] @@ -181,15 +184,16 @@ async def embed_chunks( ] -def get_embedder(config: AppConfig = Config) -> EmbedderWrapper: +def get_embedder(config: AppConfig | None = None) -> EmbedderWrapper: """Factory function to get the appropriate embedder based on the configuration. Args: - config: Configuration to use. Defaults to global Config. + config: Configuration to use. Defaults to the current global config. Returns: An embedder instance configured according to the config. """ + config = config if config is not None else get_config() embedding_model = config.embeddings.model provider = embedding_model.provider model_name = embedding_model.name diff --git a/haiku_rag_slim/haiku/rag/mcp.py b/haiku_rag_slim/haiku/rag/mcp.py index a8a44b50..16e97d29 100644 --- a/haiku_rag_slim/haiku/rag/mcp.py +++ b/haiku_rag_slim/haiku/rag/mcp.py @@ -7,7 +7,7 @@ from typing import Any from fastmcp import FastMCP from haiku.rag.client import HaikuRAG -from haiku.rag.config import AppConfig, Config +from haiku.rag.config import AppConfig, get_config from haiku.rag.store.models import Document, SearchResult from haiku.rag.tools.document import DocumentInfo from haiku.rag.utils import format_citations @@ -22,7 +22,7 @@ def _decode_images(images_base64: list[str] | None) -> list[bytes] | None: def create_mcp_server( - db_path: Path, config: AppConfig = Config, read_only: bool = False + db_path: Path, config: AppConfig | None = None, read_only: bool = False ) -> FastMCP: """Create an MCP server with the specified database path. @@ -31,6 +31,7 @@ def create_mcp_server( config: Configuration to use. read_only: If True, write tools (add_document_*, delete_document) are not registered. """ + config = config if config is not None else get_config() client: HaikuRAG | None = None stack = AsyncExitStack() client_lock = asyncio.Lock() diff --git a/haiku_rag_slim/haiku/rag/reranking/__init__.py b/haiku_rag_slim/haiku/rag/reranking/__init__.py index e02e64db..eca838cc 100644 --- a/haiku_rag_slim/haiku/rag/reranking/__init__.py +++ b/haiku_rag_slim/haiku/rag/reranking/__init__.py @@ -1,10 +1,11 @@ -from haiku.rag.config import AppConfig, Config +from haiku.rag.config import AppConfig, get_config from haiku.rag.reranking.base import RerankerBase -def get_reranker(config: AppConfig = Config) -> RerankerBase | None: +def get_reranker(config: AppConfig | None = None) -> RerankerBase | None: """Build the configured reranker, or None if reranking is disabled or its optional dependency is not installed.""" + config = config if config is not None else get_config() model = config.reranking.model if model is None: return None @@ -16,7 +17,7 @@ def get_reranker(config: AppConfig = Config) -> RerankerBase | None: if model.provider == "cohere": from haiku.rag.reranking.cohere import CohereReranker - return CohereReranker() + return CohereReranker(model.name) if model.provider == "vllm": if not model.base_url: diff --git a/haiku_rag_slim/haiku/rag/reranking/base.py b/haiku_rag_slim/haiku/rag/reranking/base.py index 703ed912..61aa092f 100644 --- a/haiku_rag_slim/haiku/rag/reranking/base.py +++ b/haiku_rag_slim/haiku/rag/reranking/base.py @@ -1,9 +1,8 @@ -from haiku.rag.config import Config from haiku.rag.store.models.chunk import Chunk class RerankerBase: - _model: str | None = Config.reranking.model.name if Config.reranking.model else None + _model: str | None = None async def rerank( self, query: str, chunks: list[Chunk], top_n: int = 10 diff --git a/haiku_rag_slim/haiku/rag/reranking/cohere.py b/haiku_rag_slim/haiku/rag/reranking/cohere.py index 25571bd7..29442052 100644 --- a/haiku_rag_slim/haiku/rag/reranking/cohere.py +++ b/haiku_rag_slim/haiku/rag/reranking/cohere.py @@ -10,7 +10,8 @@ except ImportError as e: # pragma: no cover class CohereReranker(RerankerBase): # pragma: no cover - def __init__(self): + def __init__(self, model: str | None = None): + self._model = model # Cohere SDK reads CO_API_KEY from environment by default self._client = cohere.AsyncClientV2() diff --git a/haiku_rag_slim/haiku/rag/store/engine.py b/haiku_rag_slim/haiku/rag/store/engine.py index c40dbbdd..240cfe13 100644 --- a/haiku_rag_slim/haiku/rag/store/engine.py +++ b/haiku_rag_slim/haiku/rag/store/engine.py @@ -18,7 +18,7 @@ from lancedb.pydantic import LanceModel, Vector from packaging.version import parse from pydantic import BaseModel, Field -from haiku.rag.config import AppConfig, Config +from haiku.rag.config import AppConfig, get_config from haiku.rag.embeddings import get_embedder from haiku.rag.store.exceptions import MigrationRequiredError, ReadOnlyError @@ -497,14 +497,14 @@ class Store: def __init__( self, db_path: Path, - config: AppConfig = Config, + config: AppConfig | None = None, skip_validation: bool = False, create: bool = False, read_only: bool = False, skip_migration_check: bool = False, ): self.db_path: Path = db_path - self._config = config + self._config = config if config is not None else get_config() self._read_only = read_only self._create = create self._skip_validation = skip_validation diff --git a/haiku_rag_slim/haiku/rag/utils.py b/haiku_rag_slim/haiku/rag/utils.py index 98812deb..cac1896a 100644 --- a/haiku_rag_slim/haiku/rag/utils.py +++ b/haiku_rag_slim/haiku/rag/utils.py @@ -119,7 +119,7 @@ def get_model( Args: model_config: ModelConfig with provider, model, and settings - app_config: AppConfig for provider base URLs (defaults to global Config) + app_config: AppConfig for provider base URLs (defaults to the current global config) Returns: A configured model instance @@ -129,9 +129,9 @@ def get_model( from pydantic_ai.providers.openai import OpenAIProvider if app_config is None: - from haiku.rag.config import Config + from haiku.rag.config import get_config - app_config = Config + app_config = get_config() provider = model_config.provider model = model_config.name diff --git a/tests/test_chunk.py b/tests/test_chunk.py index d052299f..aa5f503a 100644 --- a/tests/test_chunk.py +++ b/tests/test_chunk.py @@ -1,7 +1,7 @@ import pytest from haiku.rag.client import HaikuRAG -from haiku.rag.config import Config +from haiku.rag.config import get_config from haiku.rag.store.models.chunk import Chunk, ChunkMetadata, SearchResult from tests.conftest import capture_logs @@ -11,7 +11,9 @@ async def test_chunk_repository_operations( qa_corpus: list[dict[str, str]], temp_db_path ): """Test ChunkRepository operations.""" - async with HaikuRAG(db_path=temp_db_path, config=Config, create=True) as client: + async with HaikuRAG( + db_path=temp_db_path, config=get_config(), create=True + ) as client: # Get the first document from the corpus first_doc = qa_corpus[0] document_text = first_doc["document_extracted"] @@ -52,7 +54,9 @@ async def test_chunk_repository_pagination( qa_corpus: list[dict[str, str]], temp_db_path ): """Test ChunkRepository pagination with get_by_document_id and count_by_document_id.""" - async with HaikuRAG(db_path=temp_db_path, config=Config, create=True) as client: + async with HaikuRAG( + db_path=temp_db_path, config=get_config(), create=True + ) as client: # Get the first document from the corpus (should produce multiple chunks) first_doc = qa_corpus[0] document_text = first_doc["document_extracted"] @@ -443,7 +447,9 @@ async def test_chunk_content_fts(temp_db_path, metadata, content, expected_conte """content_fts holds the contextualized content while content stays raw.""" from haiku.rag.embeddings import get_embedder - async with HaikuRAG(db_path=temp_db_path, config=Config, create=True) as client: + async with HaikuRAG( + db_path=temp_db_path, config=get_config(), create=True + ) as client: chunk = Chunk( document_id="test-doc", content=content, @@ -451,7 +457,7 @@ async def test_chunk_content_fts(temp_db_path, metadata, content, expected_conte order=0, ) - embedder = get_embedder(Config) + embedder = get_embedder(get_config()) embedding = (await embedder.embed_documents([chunk.content]))[0] chunk.embedding = embedding @@ -477,7 +483,9 @@ async def test_ensure_fts_index_warns_on_failure(temp_db_path): from haiku.rag.store.repositories import chunk as chunk_module - async with HaikuRAG(db_path=temp_db_path, config=Config, create=True) as client: + async with HaikuRAG( + db_path=temp_db_path, config=get_config(), create=True + ) as client: repo = client.chunk_repository async def _boom(*_args, **_kwargs): @@ -497,7 +505,9 @@ async def test_chunk_repository_get_by_id_and_list_all_pagination( qa_corpus: list[dict[str, str]], temp_db_path ): """get_by_id resolves a stored chunk; list_all honours limit and offset.""" - async with HaikuRAG(db_path=temp_db_path, config=Config, create=True) as client: + async with HaikuRAG( + db_path=temp_db_path, config=get_config(), create=True + ) as client: # A corpus document is long enough to chunk more than once, which is # what makes the offset assertion below meaningful. doc = await client.create_document(content=qa_corpus[0]["document_extracted"]) @@ -531,7 +541,9 @@ async def test_chunk_repository_get_by_id_and_list_all_pagination( @pytest.mark.vcr() async def test_chunk_search_returns_empty_for_blank_query(temp_db_path): """A blank query with no precomputed vector short-circuits before searching.""" - async with HaikuRAG(db_path=temp_db_path, config=Config, create=True) as client: + async with HaikuRAG( + db_path=temp_db_path, config=get_config(), create=True + ) as client: await client.create_document(content="Searchable body about elections.") # Positive control: the corpus is non-empty, so [] is a real decision @@ -543,7 +555,9 @@ async def test_chunk_search_returns_empty_for_blank_query(temp_db_path): @pytest.mark.vcr() async def test_chunk_search_with_precomputed_vector_skips_text_query(temp_db_path): """The image-as-query path searches vector-only using a stored embedding.""" - async with HaikuRAG(db_path=temp_db_path, config=Config, create=True) as client: + async with HaikuRAG( + db_path=temp_db_path, config=get_config(), create=True + ) as client: doc = await client.create_document(content="Vector-only search target.") assert doc.id is not None @@ -557,7 +571,9 @@ async def test_chunk_search_with_precomputed_vector_skips_text_query(temp_db_pat async def test_get_chunk_ids_by_self_ref_grouped_without_documents(temp_db_path): - async with HaikuRAG(db_path=temp_db_path, config=Config, create=True) as client: + async with HaikuRAG( + db_path=temp_db_path, config=get_config(), create=True + ) as client: assert await client.chunk_repository.get_chunk_ids_by_self_ref_grouped([]) == {} @@ -565,7 +581,9 @@ async def test_process_search_results_rejects_unknown_score_column(temp_db_path) """A result frame with no recognised score column is a programming error.""" import pandas as pd - async with HaikuRAG(db_path=temp_db_path, config=Config, create=True) as client: + async with HaikuRAG( + db_path=temp_db_path, config=get_config(), create=True + ) as client: class _Frame: async def to_pandas(self): diff --git a/tests/test_chunker.py b/tests/test_chunker.py index c39f5831..b7dfa9fa 100644 --- a/tests/test_chunker.py +++ b/tests/test_chunker.py @@ -7,7 +7,7 @@ from transformers import AutoTokenizer from haiku.rag.chunkers import get_chunker from haiku.rag.chunkers.docling_local import DoclingLocalChunker from haiku.rag.chunkers.docling_serve import DoclingServeChunker -from haiku.rag.config import AppConfig, Config +from haiku.rag.config import AppConfig, get_config from haiku.rag.converters import get_converter @@ -23,7 +23,7 @@ async def test_local_chunker(qa_corpus: list[dict[str, str]]): doc_text = qa_corpus[0]["document_extracted"] # Convert text to DoclingDocument - converter = get_converter(Config) + converter = get_converter(get_config()) doc = await converter.convert_text(doc_text, name="test.md") chunks = await chunker.chunk(doc) @@ -79,7 +79,7 @@ async def test_local_chunker_runs_off_event_loop_thread(): called_from.append(threading.current_thread()) return original(document) - converter = get_converter(Config) + converter = get_converter(get_config()) doc = await converter.convert_text("# Hello\n\nWorld", name="test.md") with patch.object(DoclingLocalChunker, "_chunk_sync", recording_chunk_sync): @@ -149,7 +149,7 @@ async def test_local_chunker_hierarchical(qa_corpus: list[dict[str, str]]): chunker = DoclingLocalChunker(config) doc_text = qa_corpus[0]["document_extracted"] - converter = get_converter(Config) + converter = get_converter(get_config()) doc = await converter.convert_text(doc_text, name="test.md") chunks = await chunker.chunk(doc) @@ -180,7 +180,7 @@ async def test_local_chunker_markdown_tables(): | Value D | Value E | """ - converter = get_converter(Config) + converter = get_converter(get_config()) doc = await converter.convert_text(markdown_with_table, name="test.md") # Test with markdown tables enabled @@ -222,7 +222,7 @@ Second paragraph. Third paragraph. """ - converter = get_converter(Config) + converter = get_converter(get_config()) doc = await converter.convert_text(sample_md, name="test.md") chunker = DoclingLocalChunker() @@ -249,7 +249,7 @@ Here is some background information. |----------|----------| | Value 1 | Value 2 | """ - converter = get_converter(Config) + converter = get_converter(get_config()) doc = await converter.convert_text(sample_md, name="test.md") chunker = DoclingLocalChunker() @@ -347,7 +347,7 @@ class TestDoclingServeChunker: mock_client_class.return_value.__aenter__.return_value = mock_client # Create a simple document - converter = get_converter(Config) + converter = get_converter(get_config()) doc = await converter.convert_text("# Test\n\nContent", name="test.md") chunks = await chunker.chunk(doc) @@ -371,7 +371,7 @@ class TestDoclingServeChunker: mock_client.get = AsyncMock(side_effect=[poll_resp, result_resp]) mock_client_class.return_value.__aenter__.return_value = mock_client - converter = get_converter(Config) + converter = get_converter(get_config()) doc = await converter.convert_text("# Test", name="test.md") await chunker.chunk(doc) @@ -394,7 +394,7 @@ class TestDoclingServeChunker: mock_client.get = AsyncMock(side_effect=[poll_resp, result_resp]) mock_client_class.return_value.__aenter__.return_value = mock_client - converter = get_converter(Config) + converter = get_converter(get_config()) doc = await converter.convert_text("# Test", name="test.md") await chunker.chunk(doc) @@ -422,7 +422,7 @@ class TestDoclingServeChunker: mock_client.get = AsyncMock(side_effect=[poll_resp, result_resp]) mock_client_class.return_value.__aenter__.return_value = mock_client - converter = get_converter(Config) + converter = get_converter(get_config()) doc = await converter.convert_text("# Test", name="test.md") await chunker.chunk(doc) @@ -453,7 +453,7 @@ class TestDoclingServeChunker: mock_client.get = AsyncMock(side_effect=[poll_resp, result_resp]) mock_client_class.return_value.__aenter__.return_value = mock_client - converter = get_converter(Config) + converter = get_converter(get_config()) doc = await converter.convert_text("# Test", name="test.md") await chunker.chunk(doc) @@ -476,7 +476,7 @@ class TestDoclingServeChunker: mock_client.post.side_effect = httpx.ConnectError("Connection failed") mock_client_class.return_value.__aenter__.return_value = mock_client - converter = get_converter(Config) + converter = get_converter(get_config()) doc = await converter.convert_text("# Test", name="test.md") with pytest.raises(httpx.ConnectError): @@ -492,7 +492,7 @@ class TestDoclingServeChunker: mock_client.post.side_effect = httpx.TimeoutException("Timeout") mock_client_class.return_value.__aenter__.return_value = mock_client - converter = get_converter(Config) + converter = get_converter(get_config()) doc = await converter.convert_text("# Test", name="test.md") with pytest.raises(httpx.TimeoutException): @@ -517,7 +517,7 @@ class TestDoclingServeChunker: mock_client.post.return_value = mock_response mock_client_class.return_value.__aenter__.return_value = mock_client - converter = get_converter(Config) + converter = get_converter(get_config()) doc = await converter.convert_text("# Test", name="test.md") with pytest.raises(httpx.HTTPStatusError) as exc_info: @@ -545,7 +545,7 @@ class TestDoclingServeChunker: mock_client.get = AsyncMock(side_effect=[poll_resp, result_resp]) mock_client_class.return_value.__aenter__.return_value = mock_client - converter = get_converter(Config) + converter = get_converter(get_config()) doc = await converter.convert_text("# Test", name="test.md") with pytest.raises(ValueError, match="Chunking failed"): @@ -574,7 +574,7 @@ class TestDoclingServeChunker: mock_client.get = AsyncMock(side_effect=[poll_resp, result_resp]) mock_client_class.return_value.__aenter__.return_value = mock_client - converter = get_converter(Config) + converter = get_converter(get_config()) doc = await converter.convert_text("# Test", name="test.md") chunks = await chunker.chunk(doc) @@ -612,7 +612,7 @@ class TestDoclingServeChunker: mock_client_class.return_value.__aenter__.return_value = mock_client # Create a document with texts and tables that match the mocked refs - converter = get_converter(Config) + converter = get_converter(get_config()) doc = await converter.convert_text( """# Chapter 1 @@ -700,7 +700,7 @@ async def test_local_and_serve_chunkers_produce_same_output(doclaynet_first_page from haiku.rag.converters.docling_serve import DoclingServeConverter # Use docling-serve to convert the PDF (ensures same conversion for both chunkers) - converter = DoclingServeConverter(Config) + converter = DoclingServeConverter(get_config()) pdf_path = doclaynet_first_page_pdf doc = await converter.convert_file(pdf_path) diff --git a/tests/test_client.py b/tests/test_client.py index 2b29a146..75164823 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -17,7 +17,7 @@ from haiku.rag.client.documents import ( _write_fetch_body, check_source_accessible, ) -from haiku.rag.config import Config +from haiku.rag.config import get_config from haiku.rag.embeddings import EmbedderWrapper from haiku.rag.ingester.sources.base import FetchResult from haiku.rag.store.compression import decompress_json @@ -813,7 +813,7 @@ async def test_client_import_document_with_custom_chunks(temp_db_path): Chunk( content="This is the second chunk", metadata={"custom": "metadata2"}, - embedding=[0.1] * Config.embeddings.model.vector_dim, + embedding=[0.1] * get_config().embeddings.model.vector_dim, order=1, ), # With embedding Chunk( @@ -856,7 +856,7 @@ def _docling_doc(name: str, text: str): def _import(name: str, text: str, **overrides) -> "DocumentImport": """Build a DocumentImport with one pre-embedded chunk (no embedder call).""" - dim = Config.embeddings.model.vector_dim + dim = get_config().embeddings.model.vector_dim chunk = Chunk(content=text, embedding=[0.1] * dim, order=0) return DocumentImport( docling_document=_docling_doc(name, text), @@ -868,7 +868,7 @@ def _import(name: str, text: str, **overrides) -> "DocumentImport": async def test_client_import_documents_single_version_per_table(temp_db_path): """import_documents writes documents/chunks/document_items once for the whole batch (issue #287).""" - config = Config.model_copy(deep=True) + config = get_config().model_copy(deep=True) config.storage.auto_vacuum = False async with HaikuRAG(temp_db_path, config=config, create=True) as client: @@ -902,7 +902,7 @@ async def test_client_import_documents_single_version_per_table(temp_db_path): async def test_client_import_documents_rolls_back_on_failure(temp_db_path): """A failure mid-batch restores all tables: nothing is persisted.""" - dim = Config.embeddings.model.vector_dim + dim = get_config().embeddings.model.vector_dim async with HaikuRAG(temp_db_path, create=True) as client: good = _import("good", "Good document body", uri="mem://good") @@ -943,7 +943,7 @@ async def test_client_import_documents_batches_embeddings(temp_db_path): """Chunks missing embeddings are embedded in one pass across the whole batch, not one embedder call per document. Duplicate chunk texts across documents keep their per-document embeddings.""" - dim = Config.embeddings.model.vector_dim + dim = get_config().embeddings.model.vector_dim embedder = _CountingEmbedder(dim) async with HaikuRAG(temp_db_path, create=True) as client: @@ -977,7 +977,7 @@ async def test_client_import_documents_batches_embeddings(temp_db_path): async def test_client_import_documents_mixed_embeddings(temp_db_path): """Pre-embedded chunks keep their vectors; only the unembedded ones go through the embedder, in one batch.""" - dim = Config.embeddings.model.vector_dim + dim = get_config().embeddings.model.vector_dim embedder = _CountingEmbedder(dim) async with HaikuRAG(temp_db_path, create=True) as client: @@ -1024,8 +1024,8 @@ async def test_client_update_document_replaces_rows_with_bounded_versions( auto_vacuum is off: its writes would land inside the measured window. """ - dim = Config.embeddings.model.vector_dim - config = Config.model_copy(deep=True) + dim = get_config().embeddings.model.vector_dim + config = get_config().model_copy(deep=True) config.storage.auto_vacuum = False async with HaikuRAG(temp_db_path, config=config, create=True) as client: @@ -1076,8 +1076,8 @@ async def test_metadata_only_update_does_not_advance_documents_table(temp_db_pat live in document_meta, so the documents table version must stay frozen while only metadata/title change — and reads must still hydrate the full Document. """ - dim = Config.embeddings.model.vector_dim - config = Config.model_copy(deep=True) + dim = get_config().embeddings.model.vector_dim + config = get_config().model_copy(deep=True) config.storage.auto_vacuum = False async with HaikuRAG(temp_db_path, config=config, create=True) as client: @@ -1121,7 +1121,7 @@ async def test_metadata_only_update_does_not_advance_documents_table(temp_db_pat async def test_delete_marks_vacuum_dirty(temp_db_path): """A delete adds tombstone/table versions, so it must enter the auto-vacuum lifecycle — otherwise a delete-only run closes without a final vacuum.""" - dim = Config.embeddings.model.vector_dim + dim = get_config().embeddings.model.vector_dim async with HaikuRAG(temp_db_path, create=True) as client: doc = await client.import_document( _docling_doc("d", "body"), @@ -1138,8 +1138,8 @@ async def test_delete_marks_vacuum_dirty(temp_db_path): async def test_delete_rolls_back_on_partial_failure(temp_db_path, monkeypatch): """A multi-table delete is atomic: if a later table delete fails, the write lock + version restore bring every table back, leaving no orphaned rows.""" - dim = Config.embeddings.model.vector_dim - config = Config.model_copy(deep=True) + dim = get_config().embeddings.model.vector_dim + config = get_config().model_copy(deep=True) config.storage.auto_vacuum = False async with HaikuRAG(temp_db_path, config=config, create=True) as client: @@ -1175,8 +1175,8 @@ async def test_cascade_delete_is_atomic(temp_db_path, monkeypatch): """Deleting a parent cascades to children under one lock + snapshot. If any delete in the subtree fails, the whole subtree is restored — a child isn't left deleted while its parent survives.""" - dim = Config.embeddings.model.vector_dim - config = Config.model_copy(deep=True) + dim = get_config().embeddings.model.vector_dim + config = get_config().model_copy(deep=True) config.storage.auto_vacuum = False async with HaikuRAG(temp_db_path, config=config, create=True) as client: @@ -2390,7 +2390,7 @@ async def test_metadata_only_update_waits_for_write_lock(temp_db_path): create_tag's version snapshot and its per-table tag creation).""" import asyncio - dim = Config.embeddings.model.vector_dim + dim = get_config().embeddings.model.vector_dim docling_doc = DoclingDocument(name="d") docling_doc.add_text(label=DocItemLabel.TEXT, text="body") diff --git a/tests/test_config.py b/tests/test_config.py index 19383411..fc431bc2 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -3,7 +3,7 @@ from pathlib import Path import pytest import yaml -from haiku.rag.config import AppConfig +from haiku.rag.config import AppConfig, set_config from haiku.rag.config.loader import ( find_config_file, generate_default_config, @@ -577,3 +577,48 @@ def test_example_configs_are_present(): ) def test_example_config_validates(path: Path): AppConfig.model_validate(yaml.safe_load(path.read_text()) or {}) + + +def _use_config(monkeypatch, cfg): + """Install cfg as the global config, restored by monkeypatch teardown.""" + import haiku.rag.config as config_module + + monkeypatch.setattr(config_module, "_config", None) + set_config(cfg) + + +def test_set_config_reaches_the_factories(monkeypatch, tmp_path): + """A config installed after import must reach every factory and + constructor that resolves the global config itself.""" + from haiku.rag.chunkers import get_chunker + from haiku.rag.chunkers.docling_serve import DoclingServeChunker + from haiku.rag.client import HaikuRAG + from haiku.rag.config.models import ModelConfig, RerankingConfig + from haiku.rag.converters import get_converter + from haiku.rag.converters.docling_serve import DoclingServeConverter + from haiku.rag.embeddings import get_embedder + from haiku.rag.reranking import get_reranker + from haiku.rag.reranking.vllm import VLLMReranker + from haiku.rag.store.engine import Store + + cfg = AppConfig() + cfg.processing.converter = "docling-serve" + cfg.processing.chunker = "docling-serve" + cfg.embeddings.model.vector_dim = 7 + cfg.reranking.model = ModelConfig( + provider="vllm", name="reranker-x", base_url="http://localhost:9/v1" + ) + assert isinstance(cfg.reranking, RerankingConfig) + + _use_config(monkeypatch, cfg) + + assert isinstance(get_converter(), DoclingServeConverter) + assert isinstance(get_chunker(), DoclingServeChunker) + assert get_embedder().vector_dim == 7 + + reranker = get_reranker() + assert isinstance(reranker, VLLMReranker) + assert reranker._model == "reranker-x" + + assert HaikuRAG(tmp_path / "db")._config is cfg + assert Store(tmp_path / "db", create=True)._config is cfg diff --git a/tests/test_context_enhancement.py b/tests/test_context_enhancement.py index 9fa11e72..2d68a481 100644 --- a/tests/test_context_enhancement.py +++ b/tests/test_context_enhancement.py @@ -69,7 +69,7 @@ def create_code_document() -> DoclingDocument: # Multiple adjacent code blocks - type-aware expansion should group them doc.add_text(label=DocItemLabel.CODE, text="# Part 1: Setup\nimport os\nimport sys") doc.add_text( - label=DocItemLabel.CODE, text='# Part 2: Config\nCONFIG = {"debug": True}' + label=DocItemLabel.CODE, text='# Part 2: get_config()\nCONFIG = {"debug": True}' ) doc.add_text( label=DocItemLabel.CODE, text="# Part 3: Main\ndef main():\n print(CONFIG)" @@ -81,7 +81,7 @@ def create_code_document() -> DoclingDocument: @pytest.fixture def small_chunk_config() -> AppConfig: - """Config with small chunk size to force splitting.""" + """get_config() with small chunk size to force splitting.""" config = AppConfig() config.processing.chunk_size = 32 config.search.max_context_chars = 10000 diff --git a/tests/test_download_models.py b/tests/test_download_models.py index 41e571c5..1b01f8c3 100644 --- a/tests/test_download_models.py +++ b/tests/test_download_models.py @@ -5,7 +5,7 @@ import httpx import pytest from haiku.rag.client.downloads import download_models -from haiku.rag.config import Config +from haiku.rag.config import get_config from haiku.rag.config.models import ModelConfig @@ -39,7 +39,7 @@ async def test_download_models_ollama_connect_error(mock_to_thread): with pytest.raises( ConnectionError, match="Cannot connect to Ollama" ) as exc_info: - async for _ in download_models(Config): + async for _ in download_models(get_config()): pass assert "ollama serve" in str(exc_info.value) @@ -74,7 +74,7 @@ async def test_download_models_ollama_pulls_models(mock_to_thread): return_value=_mock_httpx_client(mock_stream), ): events = [] - async for progress in download_models(Config): + async for progress in download_models(get_config()): events.append(progress) # Default config has embeddings=qwen3-embedding:4b, qa=gpt-oss diff --git a/tests/test_embedder.py b/tests/test_embedder.py index bcb82da1..4c96b311 100644 --- a/tests/test_embedder.py +++ b/tests/test_embedder.py @@ -5,9 +5,9 @@ import pytest from haiku.rag.config import ( AppConfig, - Config, EmbeddingModelConfig, EmbeddingsConfig, + get_config, ) from haiku.rag.embeddings import ( EmbedderWrapper, @@ -197,7 +197,7 @@ async def test_embed_chunks_basic(allow_model_requests): ), ] - embedded_chunks = await embed_chunks(chunks, get_embedder(Config)) + embedded_chunks = await embed_chunks(chunks, get_embedder(get_config())) assert len(embedded_chunks) == 2 # Check that all original fields are preserved @@ -216,7 +216,7 @@ async def test_embed_chunks_basic(allow_model_requests): async def test_embed_chunks_returns_new_objects(allow_model_requests): """Test that embed_chunks returns new Chunk objects (immutable pattern).""" original = Chunk(id="orig", content="Test content.") - embedded = await embed_chunks([original], get_embedder(Config)) + embedded = await embed_chunks([original], get_embedder(get_config())) # Original should be unchanged assert original.embedding is None @@ -302,7 +302,7 @@ async def test_embed_chunks_preserves_all_fields(allow_model_requests): document_meta={"author": "Test"}, ) - embedded = await embed_chunks([chunk], get_embedder(Config)) + embedded = await embed_chunks([chunk], get_embedder(get_config())) assert embedded[0].id == "test-id" assert embedded[0].document_id == "doc-id" diff --git a/tests/test_lancedb_connection.py b/tests/test_lancedb_connection.py index ed65a5f9..2bdc5040 100644 --- a/tests/test_lancedb_connection.py +++ b/tests/test_lancedb_connection.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, patch import pytest from pydantic import ValidationError -from haiku.rag.config import Config +from haiku.rag.config import get_config from haiku.rag.config.models import AppConfig, LanceDBConfig from haiku.rag.store.engine import ConnectionMode, Store, connect_lancedb @@ -139,16 +139,16 @@ class TestStoreConnectionMode: 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"), + patch.object(get_config().lancedb, "uri", "db://test-database"), + patch.object(get_config().lancedb, "api_key", "test-api-key"), + patch.object(get_config().lancedb, "region", "us-east-1"), ): assert store._connection_mode == ConnectionMode.CLOUD @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"): + with patch.object(get_config().lancedb, "uri", "s3://bucket/path"): assert store._connection_mode == ConnectionMode.OBJECT_STORAGE @@ -157,9 +157,9 @@ class TestVacuumByConnectionMode: async def test_cloud_skips_vacuum(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"), + patch.object(get_config().lancedb, "uri", "db://test-database"), + patch.object(get_config().lancedb, "api_key", "test-api-key"), + patch.object(get_config().lancedb, "region", "us-east-1"), ): with patch.object( store.chunks_table, "optimize", new_callable=AsyncMock @@ -170,7 +170,7 @@ class TestVacuumByConnectionMode: @pytest.mark.asyncio async def test_object_storage_runs_vacuum(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(get_config().lancedb, "uri", "s3://bucket/path"): with patch.object( store.chunks_table, "optimize", new_callable=AsyncMock ) as mock_optimize: @@ -180,7 +180,7 @@ class TestVacuumByConnectionMode: @pytest.mark.asyncio async def test_local_runs_vacuum(self, temp_db_path): async with Store(temp_db_path, create=True) as store: - with patch.object(Config.lancedb, "uri", ""): + with patch.object(get_config().lancedb, "uri", ""): with patch.object( store.chunks_table, "optimize", new_callable=AsyncMock ) as mock_optimize: @@ -193,9 +193,9 @@ class TestVectorIndexByConnectionMode: 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"), + patch.object(get_config().lancedb, "uri", "db://test-database"), + patch.object(get_config().lancedb, "api_key", "test-api-key"), + patch.object(get_config().lancedb, "region", "us-east-1"), ): with patch.object( store.chunks_table, "count_rows", new_callable=AsyncMock @@ -206,7 +206,7 @@ class TestVectorIndexByConnectionMode: @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(get_config().lancedb, "uri", "s3://bucket/path"): with patch.object( store.chunks_table, "count_rows", diff --git a/tests/test_mcp.py b/tests/test_mcp.py index 044c5779..860bf2a7 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -644,10 +644,10 @@ class TestMCPClientLifetime: """Validation is unchanged: same-dimension identity drift warns in read-only mode and raises in writable mode. The MCP server no longer opts out of it for deletion.""" - from haiku.rag.config import Config + from haiku.rag.config import get_config from haiku.rag.store.repositories.settings import ConfigMismatchError - drifted = Config.model_copy(deep=True) + drifted = get_config().model_copy(deep=True) drifted.embeddings.model.name = "a-different-model" async with create_mcp_server( diff --git a/tests/test_picture_in_context.py b/tests/test_picture_in_context.py index c2c4c497..3ef84811 100644 --- a/tests/test_picture_in_context.py +++ b/tests/test_picture_in_context.py @@ -15,7 +15,7 @@ from pydantic_ai.usage import RunUsage from haiku.rag.capabilities.rag import RAGState, create_capability from haiku.rag.client import HaikuRAG from haiku.rag.client.search import _populate_image_data -from haiku.rag.config import AppConfig, Config +from haiku.rag.config import AppConfig, get_config from haiku.rag.store.models.chunk import Chunk, SearchResult from haiku.rag.store.models.document_item import DocumentItem from haiku.rag.tools.search import create_search_toolset @@ -980,7 +980,7 @@ async def test_search_tool_returns_plain_string_when_no_pictures(): fake_client.search = AsyncMock(return_value=[text_result]) fake_client.expand_context = AsyncMock(return_value=[text_result]) - toolset = create_search_toolset(Config, expand_context=False) + toolset = create_search_toolset(get_config(), expand_context=False) func = toolset.tools["search"].function ctx = RunContext( diff --git a/tests/test_reader.py b/tests/test_reader.py index 34d0637c..1f9b8f44 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -3,7 +3,7 @@ from pathlib import Path import pytest -from haiku.rag.config import Config +from haiku.rag.config import get_config from haiku.rag.converters import get_converter @@ -19,7 +19,7 @@ async def test_code_file_wrapped_in_code_block(): f.flush() temp_path = Path(f.name) - converter = get_converter(Config) + converter = get_converter(get_config()) document = await converter.convert_file(temp_path) result = document.export_to_markdown() diff --git a/tests/test_rebuild.py b/tests/test_rebuild.py index 01c29a55..278065d0 100644 --- a/tests/test_rebuild.py +++ b/tests/test_rebuild.py @@ -6,7 +6,7 @@ from typing import TypedDict import pytest from haiku.rag.client import HaikuRAG, RebuildMode -from haiku.rag.config import Config +from haiku.rag.config import get_config from tests.conftest import capture_logs @@ -194,7 +194,7 @@ async def test_rebuild_resumes_phase2_from_staging_after_crash( # crash, where no background vacuum would be in flight. Leaving it on lets # create_document's scheduled optimize race the raw drop_table ("Directory # not empty"). - config = Config.model_copy(deep=True) + config = get_config().model_copy(deep=True) config.storage.auto_vacuum = False async with HaikuRAG(temp_db_path, config=config, create=True) as client: @@ -1093,7 +1093,7 @@ async def test_rebuild_blocks_tag_operations(temp_db_path, monkeypatch): monkeypatch.setattr(EmbedderWrapper, "embed_documents", fake_embed_documents) - dim = Config.embeddings.model.vector_dim + dim = get_config().embeddings.model.vector_dim docling_doc = DoclingDocument(name="d") docling_doc.add_text(label=DocItemLabel.TEXT, text="body") diff --git a/tests/test_reranker.py b/tests/test_reranker.py index 1dea945f..eb4046eb 100644 --- a/tests/test_reranker.py +++ b/tests/test_reranker.py @@ -38,11 +38,9 @@ chunks = [ @pytest.mark.asyncio async def test_reranker_base(): - from haiku.rag.config import Config - reranker = RerankerBase() - expected_model = Config.reranking.model.name if Config.reranking.model else None - assert reranker._model == expected_model + # The base carries no model: each reranker takes its own from the factory. + assert reranker._model is None # Empty input short-circuits in the base class without dispatching to _rerank. assert await reranker.rerank("query", []) == [] @@ -156,12 +154,12 @@ class TestGetReranker: [ ( "cohere", - "rerank-v3.5", + "rerank-english-v3.0", "haiku.rag.reranking.cohere", "CohereReranker", {}, - {}, - {}, + {"_model": "rerank-english-v3.0"}, + {"CO_API_KEY": "test-api-key"}, ), ( "vllm", diff --git a/tests/test_search.py b/tests/test_search.py index 6a6e429f..26f883f2 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -1,14 +1,16 @@ import pytest from haiku.rag.client import HaikuRAG -from haiku.rag.config import Config +from haiku.rag.config import get_config from haiku.rag.store.models import SearchResult @pytest.mark.vcr() async def test_search_qa_corpus(qa_corpus: list[dict[str, str]], temp_db_path): """Test that documents can be found by searching with their associated questions.""" - async with HaikuRAG(db_path=temp_db_path, config=Config, create=True) as client: + async with HaikuRAG( + db_path=temp_db_path, config=get_config(), create=True + ) as client: # Load unique documents (limited to 10) seen_documents = set() documents = [] @@ -57,7 +59,9 @@ async def test_search_qa_corpus(qa_corpus: list[dict[str, str]], temp_db_path): @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.""" - async with HaikuRAG(db_path=temp_db_path, config=Config, create=True) as client: + async with HaikuRAG( + db_path=temp_db_path, config=get_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.", @@ -92,7 +96,9 @@ async def test_search_chunk_includes_document_provenance(temp_db_path): @pytest.mark.vcr() async def test_search_score_types(temp_db_path): """Test that different search types return appropriate score ranges.""" - async with HaikuRAG(db_path=temp_db_path, config=Config, create=True) as client: + async with HaikuRAG( + db_path=temp_db_path, config=get_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.", @@ -159,7 +165,9 @@ async def test_search_score_types(temp_db_path): @pytest.mark.vcr() async def test_search_returns_search_result(temp_db_path): """Test that client.search() returns SearchResult with provenance info.""" - async with HaikuRAG(db_path=temp_db_path, config=Config, create=True) as client: + async with HaikuRAG( + db_path=temp_db_path, config=get_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", @@ -188,7 +196,9 @@ async def test_search_graceful_degradation(temp_db_path): """Test search works when docling data is unavailable.""" from haiku.rag.store.models import Chunk - async with HaikuRAG(db_path=temp_db_path, config=Config, create=True) as client: + async with HaikuRAG( + db_path=temp_db_path, config=get_config(), create=True + ) as client: # Import document with custom chunks (no docling document) custom_chunks = [ Chunk(content="Custom chunk without docling metadata", metadata={}), diff --git a/tests/test_settings.py b/tests/test_settings.py index f30e2756..704b9eda 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -2,7 +2,7 @@ import logging import pytest -from haiku.rag.config import AppConfig, Config +from haiku.rag.config import AppConfig, get_config from haiku.rag.store.repositories.settings import ConfigMismatchError @@ -16,7 +16,7 @@ async def test_settings_table_populated_on_store_init(temp_db_path): settings_repo = SettingsRepository(store) db_settings = await settings_repo.get_current_settings() - config_dict = Config.model_dump(mode="json") + config_dict = get_config().model_dump(mode="json") # Remove version from db_settings since it's added automatically db_settings_without_version = { @@ -34,14 +34,14 @@ async def test_settings_save_and_retrieve(temp_db_path): 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 = get_config().processing.chunk_size + get_config().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 + get_config().processing.chunk_size = original_chunk_size @pytest.mark.asyncio @@ -52,7 +52,7 @@ async def test_set_haiku_version_recreates_row_from_store_config(temp_db_path): from haiku.rag.store.repositories.settings import SettingsRepository config = AppConfig() - config.processing.chunk_size = Config.processing.chunk_size + 512 + config.processing.chunk_size = get_config().processing.chunk_size + 512 async with Store(temp_db_path, config=config, create=True) as store: settings_repo = SettingsRepository(store) diff --git a/tests/test_utils.py b/tests/test_utils.py index ad3c78b1..f8e71049 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -3,7 +3,7 @@ import importlib.util import pytest from pydantic_ai.models.openai import OpenAIChatModel -from haiku.rag.config import Config +from haiku.rag.config import get_config from haiku.rag.config.models import ModelConfig from haiku.rag.converters import get_converter from haiku.rag.utils import get_model @@ -20,7 +20,7 @@ async def test_text_to_docling_document(): """Test text to DoclingDocument conversion.""" # Test basic text conversion simple_text = "This is a simple text document." - converter = get_converter(Config) + converter = get_converter(get_config()) doc = await converter.convert_text(simple_text) # Verify it returns a DoclingDocument @@ -44,7 +44,7 @@ def hello(): return True ```""" - converter = get_converter(Config) + converter = get_converter(get_config()) doc = await converter.convert_text(code_text, name="hello.md") # Verify it's a valid DoclingDocument @@ -77,7 +77,7 @@ def test(): **Bold text** and *italic text*.""" - converter = get_converter(Config) + converter = get_converter(get_config()) doc = await converter.convert_text(markdown_text, name="test.md") # Verify it's a DoclingDocument @@ -95,7 +95,7 @@ def test(): @pytest.mark.asyncio async def test_text_to_docling_document_empty_content(): """Test text to DoclingDocument conversion with empty content.""" - converter = get_converter(Config) + converter = get_converter(get_config()) doc = await converter.convert_text("") # Should still create a valid DoclingDocument @@ -124,7 +124,7 @@ function saludar() { Emoji test: 🚀 ✅ 📝""" - converter = get_converter(Config) + converter = get_converter(get_config()) doc = await converter.convert_text(unicode_text, name="unicode.md") # Verify it's a DoclingDocument diff --git a/tests/test_vacuum_debounce.py b/tests/test_vacuum_debounce.py index 83a96dbc..6e6de526 100644 --- a/tests/test_vacuum_debounce.py +++ b/tests/test_vacuum_debounce.py @@ -5,7 +5,7 @@ import pytest import haiku.rag.client as client_mod from haiku.rag.client import HaikuRAG from haiku.rag.client.documents import _refresh_doc_metadata -from haiku.rag.config import Config +from haiku.rag.config import get_config from haiku.rag.store.models.chunk import Chunk @@ -73,7 +73,7 @@ async def test_metadata_refresh_sweep_schedules_vacuum(temp_db_path): """A source re-sweep that only rolls source_revision (MD5/revision short-circuit) writes document_meta and must still schedule the (debounced) vacuum, so that tiny churn gets reclaimed instead of accumulating.""" - dim = Config.embeddings.model.vector_dim + dim = get_config().embeddings.model.vector_dim async with HaikuRAG(temp_db_path, create=True) as client: doc = await client.import_document( _docling_doc("d", "body"), @@ -99,7 +99,7 @@ async def test_metadata_refresh_waits_for_write_lock(temp_db_path): """The revision/MD5 short-circuit write serializes with other writers so it cannot land inside another writer's critical section (e.g. between create_tag's version snapshot and its per-table tag creation).""" - dim = Config.embeddings.model.vector_dim + dim = get_config().embeddings.model.vector_dim async with HaikuRAG(temp_db_path, create=True) as client: doc = await client.import_document( _docling_doc("d", "body"), diff --git a/tests/test_versioning.py b/tests/test_versioning.py index 2be4c855..9e8e7953 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -271,10 +271,10 @@ async def test_vacuum_with_retention_threshold(temp_db_path): @pytest.mark.vcr() async def test_vacuum_completes_before_context_exit(temp_db_path, monkeypatch): """Test that background vacuum completes when context manager exits.""" - from haiku.rag.config import Config + from haiku.rag.config import get_config # Set aggressive vacuum retention for this test - monkeypatch.setattr(Config.storage, "vacuum_retention_seconds", 0) + monkeypatch.setattr(get_config().storage, "vacuum_retention_seconds", 0) async with HaikuRAG(db_path=temp_db_path, create=True) as client: # Create multiple documents - each creation triggers automatic vacuum with retention=0 @@ -302,9 +302,9 @@ async def test_aexit_awaits_background_vacuum(temp_db_path, monkeypatch): 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 + from haiku.rag.config import get_config - monkeypatch.setattr(Config.storage, "auto_vacuum", True) + monkeypatch.setattr(get_config().storage, "auto_vacuum", True) vacuum_started = asyncio.Event() vacuum_completed = asyncio.Event() @@ -339,9 +339,9 @@ async def test_aexit_awaits_all_background_vacuums(temp_db_path, monkeypatch): __aexit__ awaits the fast no-op B and closes the connection while Task A is still running. """ - from haiku.rag.config import Config + from haiku.rag.config import get_config - monkeypatch.setattr(Config.storage, "auto_vacuum", True) + monkeypatch.setattr(get_config().storage, "auto_vacuum", True) first_vacuum_completed = asyncio.Event() @@ -378,10 +378,10 @@ async def test_aexit_awaits_all_background_vacuums(temp_db_path, monkeypatch): @pytest.mark.vcr() async def test_auto_vacuum_disabled_skips_vacuum(temp_db_path, monkeypatch): """Test that auto_vacuum=False prevents automatic vacuum after operations.""" - from haiku.rag.config import Config + from haiku.rag.config import get_config # Disable auto-vacuum - monkeypatch.setattr(Config.storage, "auto_vacuum", False) + monkeypatch.setattr(get_config().storage, "auto_vacuum", False) async with HaikuRAG(db_path=temp_db_path, create=True) as client: # Create multiple documents @@ -404,11 +404,11 @@ async def test_auto_vacuum_disabled_skips_vacuum(temp_db_path, monkeypatch): @pytest.mark.vcr() async def test_auto_vacuum_enabled_triggers_vacuum(temp_db_path, monkeypatch): """Test that auto_vacuum=True (default) triggers vacuum after operations.""" - from haiku.rag.config import Config + from haiku.rag.config import get_config # Enable auto-vacuum with aggressive retention - monkeypatch.setattr(Config.storage, "auto_vacuum", True) - monkeypatch.setattr(Config.storage, "vacuum_retention_seconds", 0) + monkeypatch.setattr(get_config().storage, "auto_vacuum", True) + monkeypatch.setattr(get_config().storage, "vacuum_retention_seconds", 0) async with HaikuRAG(db_path=temp_db_path, create=True) as client: # Create multiple documents diff --git a/tests/tools/test_document.py b/tests/tools/test_document.py index 967a1bf8..fc0ede4f 100644 --- a/tests/tools/test_document.py +++ b/tests/tools/test_document.py @@ -238,6 +238,6 @@ async def doc_client(temp_db_path): @pytest.fixture def doc_config(): """Default AppConfig for document tests.""" - from haiku.rag.config import Config + from haiku.rag.config import get_config - return Config + return get_config() diff --git a/tests/tools/test_search.py b/tests/tools/test_search.py index 8f09ad04..eb3efa99 100644 --- a/tests/tools/test_search.py +++ b/tests/tools/test_search.py @@ -205,9 +205,9 @@ async def search_client(temp_db_path): @pytest.fixture def search_config(): """Default AppConfig for search tests.""" - from haiku.rag.config import Config + from haiku.rag.config import get_config - return Config + return get_config() def _png_b64():