From 971ec0a5b0922f179428216f77a693935e15a6be Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Fri, 21 Aug 2026 12:49:45 +0300 Subject: [PATCH] Coerce a string db_path to Path at the Store and capability boundaries The documented `HaikuRAG("knowledge.lancedb")` and `rag(db_path="my.lancedb")` forms both raised `AttributeError: 'str' object has no attribute 'exists'`. `Store.__init__` assigned its argument to a `Path`-annotated attribute without coercing, and `resolve_db_path` returned a non-None argument unchanged. Every runnable example wraps the path in `Path(...)`, which is why it survived. Coerce in `Store.__init__` and `resolve_db_path`; widen the annotations on `Store`, `HaikuRAG` and both `create_capability` factories to accept `str`. --- CHANGELOG.md | 2 ++ .../haiku/rag/capabilities/_base.py | 4 ++-- .../haiku/rag/capabilities/analysis.py | 2 +- haiku_rag_slim/haiku/rag/capabilities/rag.py | 2 +- haiku_rag_slim/haiku/rag/client/__init__.py | 5 ++-- haiku_rag_slim/haiku/rag/store/engine.py | 10 ++++---- tests/capabilities/test_capabilities.py | 23 +++++++++++++++++++ tests/test_client.py | 9 ++++++++ 8 files changed, 46 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ef9df7e..20827af2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ### Fixed +- `Store`, `HaikuRAG` and `create_capability` coerce a string `db_path` to `Path`, as the documented + `HaikuRAG("knowledge.lancedb")` and `rag(db_path="my.lancedb")` forms require. - A capability search that matches nothing returns `No results found.` instead of an empty string. - Repeating a search query within a question accumulates the results of both calls instead of replacing the earlier ones. - `file://` URIs resolve to a Windows path through `url2pathname`: `file:///C:/docs/a.pdf` was read as `\C:\docs\a.pdf`, so ingestion reported `File does not exist` for every discovered file. A URI authority is kept as a UNC server/share (`file://server/share/a.pdf`) except `localhost`, which is dropped. diff --git a/haiku_rag_slim/haiku/rag/capabilities/_base.py b/haiku_rag_slim/haiku/rag/capabilities/_base.py index b486de63..30d8865c 100644 --- a/haiku_rag_slim/haiku/rag/capabilities/_base.py +++ b/haiku_rag_slim/haiku/rag/capabilities/_base.py @@ -70,9 +70,9 @@ def _nearest_known_id(chunk_id: str, known_ids: list[str]) -> str: return match[0] if match else chunk_id -def resolve_db_path(db_path: Path | None, config: AppConfig) -> Path: +def resolve_db_path(db_path: Path | str | None, config: AppConfig) -> Path: if db_path is not None: - return db_path + return Path(db_path) if env_db := os.environ.get("HAIKU_RAG_DB"): return Path(env_db).expanduser() return config.storage.data_dir / "haiku.rag.lancedb" diff --git a/haiku_rag_slim/haiku/rag/capabilities/analysis.py b/haiku_rag_slim/haiku/rag/capabilities/analysis.py index 843f208c..e7cfeb3f 100644 --- a/haiku_rag_slim/haiku/rag/capabilities/analysis.py +++ b/haiku_rag_slim/haiku/rag/capabilities/analysis.py @@ -156,7 +156,7 @@ class AnalysisCapability(RAGCapabilityBase[AnalysisState]): def create_capability( - db_path: Path | None = None, + db_path: Path | str | None = None, config: AppConfig | None = None, *, defer_loading: bool = True, diff --git a/haiku_rag_slim/haiku/rag/capabilities/rag.py b/haiku_rag_slim/haiku/rag/capabilities/rag.py index c45ff6a7..6c7b2fca 100644 --- a/haiku_rag_slim/haiku/rag/capabilities/rag.py +++ b/haiku_rag_slim/haiku/rag/capabilities/rag.py @@ -64,7 +64,7 @@ class RAGCapability(RAGCapabilityBase[RAGState]): def create_capability( - db_path: Path | None = None, + db_path: Path | str | None = None, config: AppConfig | None = None, *, defer_loading: bool = True, diff --git a/haiku_rag_slim/haiku/rag/client/__init__.py b/haiku_rag_slim/haiku/rag/client/__init__.py index 4cf6ba48..e97dd695 100644 --- a/haiku_rag_slim/haiku/rag/client/__init__.py +++ b/haiku_rag_slim/haiku/rag/client/__init__.py @@ -66,7 +66,7 @@ class HaikuRAG: def __init__( self, - db_path: Path | None = None, + db_path: Path | str | None = None, config: AppConfig | None = None, skip_validation: bool = False, create: bool = False, @@ -75,7 +75,8 @@ class HaikuRAG: """Initialize the RAG client with a database path. Args: - db_path: Path to the database file. If None, uses config.storage.data_dir. + db_path: Path or string path to the database file. If None, uses + config.storage.data_dir. 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. diff --git a/haiku_rag_slim/haiku/rag/store/engine.py b/haiku_rag_slim/haiku/rag/store/engine.py index 03a57d87..992bfc8d 100644 --- a/haiku_rag_slim/haiku/rag/store/engine.py +++ b/haiku_rag_slim/haiku/rag/store/engine.py @@ -170,14 +170,14 @@ class TagInfo: class Store: def __init__( self, - db_path: Path, + db_path: Path | str, 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.db_path: Path = Path(db_path) self._config = config if config is not None else get_config() self._read_only = read_only self._create = create @@ -191,7 +191,7 @@ class Store: self._is_new_db = False if self._connection_mode == ConnectionMode.LOCAL: - if not db_path.exists(): + if not self.db_path.exists(): if not create: raise FileNotFoundError( f"Database does not exist at {self.db_path.absolute()}. " @@ -199,8 +199,8 @@ class Store: ) 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) + if not self.db_path.parent.exists(): + Path.mkdir(self.db_path.parent, parents=True) # Create embedder (sync — no LanceDB needed) self.embedder = get_embedder(config=self._config) diff --git a/tests/capabilities/test_capabilities.py b/tests/capabilities/test_capabilities.py index 30cb657e..25727289 100644 --- a/tests/capabilities/test_capabilities.py +++ b/tests/capabilities/test_capabilities.py @@ -1,4 +1,5 @@ from dataclasses import dataclass, field +from pathlib import Path from types import SimpleNamespace from typing import Any, cast from unittest.mock import AsyncMock, patch @@ -102,11 +103,33 @@ def test_capability_factories_resolve_environment_and_defaults( config.storage.data_dir / "haiku.rag.lancedb" ) + for factory in (create_rag, create_analysis): + db_path = factory(db_path=str(temp_db_path), config=config).db_path + assert db_path == temp_db_path + assert isinstance(db_path, Path) + with patch("haiku.rag.config.get_config", return_value=config): assert create_rag().config is config assert create_analysis().config is config +@pytest.mark.asyncio +async def test_a_string_db_path_opens_a_store(temp_db_path): + """Store calls `absolute()` and `exists()` on db_path, which a str lacks.""" + from haiku.rag.client import HaikuRAG + + config = AppConfig() + async with HaikuRAG(temp_db_path, config, create=True): + pass + + capability = create_rag(db_path=str(temp_db_path), config=config) + try: + rag = await capability._ensure_rag() + assert rag.store.db_path == temp_db_path + finally: + await capability._close() + + def test_domain_preamble_is_added_to_capability_instructions(temp_db_path): config = AppConfig( prompts=PromptsConfig(domain_preamble="The corpus contains solar manuals.") diff --git a/tests/test_client.py b/tests/test_client.py index f7be1022..8636a99a 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -30,6 +30,15 @@ def vcr_cassette_dir(): return str(Path(__file__).parent / "cassettes" / "test_client") +@pytest.mark.asyncio +async def test_a_string_db_path_is_accepted(temp_db_path): + """The documented `HaikuRAG("knowledge.lancedb")` form: Store calls + `exists()` and `absolute()` on db_path, which a str lacks.""" + async with HaikuRAG(str(temp_db_path), create=True) as client: + assert client.store.db_path == temp_db_path + assert isinstance(client.store.db_path, Path) + + @pytest.mark.asyncio async def test_prepare_document_from_docling_runs_off_event_loop_thread(monkeypatch): import haiku.rag.client.documents as documents