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`.
This commit is contained in:
Yiorgis Gozadinos 2026-08-21 12:49:45 +03:00
parent def24a2434
commit 971ec0a5b0
No known key found for this signature in database
8 changed files with 46 additions and 11 deletions

View file

@ -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.

View file

@ -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"

View file

@ -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,

View file

@ -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,

View file

@ -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.

View file

@ -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)

View file

@ -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.")

View file

@ -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