From 110accb8e75f97c79a093c084a3454c7b3f96cae Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Fri, 3 Apr 2026 11:43:51 +0300 Subject: [PATCH 1/8] Add ConnectionMode enum (LOCAL/CLOUD/OBJECT_STORAGE) and connect_lancedb() utility to support S3, GCS, Azure, and HDFS backends via storage_options. --- haiku_rag_slim/haiku/rag/config/models.py | 1 + haiku_rag_slim/haiku/rag/store/engine.py | 70 ++++--- tests/test_lancedb_connection.py | 231 ++++++++++++++++++---- 3 files changed, 243 insertions(+), 59 deletions(-) diff --git a/haiku_rag_slim/haiku/rag/config/models.py b/haiku_rag_slim/haiku/rag/config/models.py index de1d6cad..d4727f8f 100644 --- a/haiku_rag_slim/haiku/rag/config/models.py +++ b/haiku_rag_slim/haiku/rag/config/models.py @@ -60,6 +60,7 @@ class LanceDBConfig(BaseModel): uri: str = "" api_key: str = "" region: str = "" + storage_options: dict[str, str] = Field(default_factory=dict) class EmbeddingsConfig(BaseModel): diff --git a/haiku_rag_slim/haiku/rag/store/engine.py b/haiku_rag_slim/haiku/rag/store/engine.py index f97e35de..5ca06037 100644 --- a/haiku_rag_slim/haiku/rag/store/engine.py +++ b/haiku_rag_slim/haiku/rag/store/engine.py @@ -2,6 +2,7 @@ import asyncio import json import logging from datetime import datetime, timedelta +from enum import Enum from importlib import metadata from pathlib import Path from typing import Any @@ -18,6 +19,42 @@ from haiku.rag.store.exceptions import MigrationRequiredError, ReadOnlyError logger = logging.getLogger(__name__) +OBJECT_STORAGE_PREFIXES = ("s3://", "gs://", "az://", "hdfs://") + + +class ConnectionMode(Enum): + LOCAL = "local" + CLOUD = "cloud" + OBJECT_STORAGE = "object_storage" + + @staticmethod + def from_config(config: AppConfig) -> "ConnectionMode": + uri = config.lancedb.uri + if not uri: + return ConnectionMode.LOCAL + if uri.startswith("db://"): + return ConnectionMode.CLOUD + return ConnectionMode.OBJECT_STORAGE + + +def connect_lancedb(config: AppConfig, db_path: Path | None = None): + mode = ConnectionMode.from_config(config) + if mode == ConnectionMode.CLOUD: + return lancedb.connect( + uri=config.lancedb.uri, + api_key=config.lancedb.api_key, + region=config.lancedb.region, + ) + elif mode == ConnectionMode.OBJECT_STORAGE: + kwargs: dict[str, Any] = {"uri": config.lancedb.uri} + if config.lancedb.storage_options: + kwargs["storage_options"] = config.lancedb.storage_options + return lancedb.connect(**kwargs) + else: + if db_path is None: + raise ValueError("No lancedb.uri configured and no db_path provided") + return lancedb.connect(db_path) + class DocumentRecord(LanceModel): id: str = Field(default_factory=lambda: str(uuid4())) @@ -97,7 +134,7 @@ class Store: # Check if database exists (for local filesystem only) is_new_db = False - if not self._has_cloud_config(): + if self._connection_mode == ConnectionMode.LOCAL: if not db_path.exists(): if not create: raise FileNotFoundError( @@ -110,7 +147,7 @@ class Store: Path.mkdir(db_path.parent, parents=True) # Connect to LanceDB - self.db = self._connect_to_lancedb(db_path) + self.db = connect_lancedb(self._config, db_path) # For existing databases, read stored vector dimension to create ChunkRecord # that can read existing chunks. For new databases, use config's dimension. @@ -198,9 +235,7 @@ class Store: """ self._assert_writable() - if self._has_cloud_config() and str(self._config.lancedb.uri).startswith( - "db://" - ): + if self._connection_mode == ConnectionMode.CLOUD: return # Skip if already running (non-blocking) @@ -224,26 +259,9 @@ class Store: # Handle resource errors gracefully logger.debug(f"Vacuum skipped due to resource constraints: {e}") - def _connect_to_lancedb(self, db_path: Path): - """Establish connection to LanceDB (local, cloud, or object storage).""" - # Check if we have cloud configuration - if self._has_cloud_config(): - return lancedb.connect( - uri=self._config.lancedb.uri, - api_key=self._config.lancedb.api_key, - region=self._config.lancedb.region, - ) - else: - # Local file system connection - return lancedb.connect(db_path) - - def _has_cloud_config(self) -> bool: - """Check if cloud configuration is complete.""" - return bool( - self._config.lancedb.uri - and self._config.lancedb.api_key - and self._config.lancedb.region - ) + @property + def _connection_mode(self) -> ConnectionMode: + return ConnectionMode.from_config(self._config) def get_stats(self) -> dict: """Get comprehensive table statistics. @@ -298,7 +316,7 @@ class Store: it will be replaced (using replace=True parameter). Note: Index creation requires sufficient training data. """ - if self._has_cloud_config(): + if self._connection_mode == ConnectionMode.CLOUD: return try: diff --git a/tests/test_lancedb_connection.py b/tests/test_lancedb_connection.py index e9b21ad5..a181b1f1 100644 --- a/tests/test_lancedb_connection.py +++ b/tests/test_lancedb_connection.py @@ -3,46 +3,211 @@ from unittest.mock import patch import pytest from haiku.rag.config import Config -from haiku.rag.store.engine import Store +from haiku.rag.config.models import AppConfig, LanceDBConfig +from haiku.rag.store.engine import ConnectionMode, Store, connect_lancedb -@pytest.mark.asyncio -async def test_lancedb_cloud_skips_optimization(temp_db_path): - """Test that vacuum is skipped when using LanceDB Cloud (db:// URI).""" - # Create a store - store = Store(temp_db_path, create=True) +class TestConnectionMode: + def test_local_when_uri_empty(self): + config = AppConfig(lancedb=LanceDBConfig(uri="")) + assert ConnectionMode.from_config(config) == ConnectionMode.LOCAL - # Mock all cloud config to simulate LanceDB Cloud usage - 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"), - ): - # Mock the optimize method to track if it's called - with patch.object(store.chunks_table, "optimize") as mock_optimize: - # Call vacuum - this should skip optimization for LanceDB Cloud - await store.vacuum() + def test_cloud_when_db_uri(self): + config = AppConfig( + lancedb=LanceDBConfig( + uri="db://my-database", api_key="key", region="us-east-1" + ) + ) + assert ConnectionMode.from_config(config) == ConnectionMode.CLOUD - # The optimize method should NOT have been called for LanceDB Cloud - mock_optimize.assert_not_called() + def test_object_storage_s3(self): + config = AppConfig(lancedb=LanceDBConfig(uri="s3://bucket/path")) + assert ConnectionMode.from_config(config) == ConnectionMode.OBJECT_STORAGE - store.close() + def test_object_storage_gs(self): + config = AppConfig(lancedb=LanceDBConfig(uri="gs://bucket/path")) + assert ConnectionMode.from_config(config) == ConnectionMode.OBJECT_STORAGE + + def test_object_storage_az(self): + config = AppConfig(lancedb=LanceDBConfig(uri="az://container/path")) + assert ConnectionMode.from_config(config) == ConnectionMode.OBJECT_STORAGE + + def test_object_storage_hdfs(self): + config = AppConfig(lancedb=LanceDBConfig(uri="hdfs://namenode/path")) + assert ConnectionMode.from_config(config) == ConnectionMode.OBJECT_STORAGE + + def test_unknown_uri_treated_as_object_storage(self): + config = AppConfig(lancedb=LanceDBConfig(uri="custom://something")) + assert ConnectionMode.from_config(config) == ConnectionMode.OBJECT_STORAGE -@pytest.mark.asyncio -async def test_local_storage_calls_optimization(temp_db_path): - """Test that vacuum calls optimization for local storage.""" - # Create a store - store = Store(temp_db_path, create=True) +class TestConnectLancedb: + 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) + mock_connect.assert_called_once_with(temp_db_path) - # Ensure uri is empty (local storage) - with patch.object(Config.lancedb, "uri", ""): - # Mock the optimize method to track if it's called - with patch.object(store.chunks_table, "optimize") as mock_optimize: - # Call vacuum - this should optimize all tables for local storage - await store.vacuum() + 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) + mock_connect.assert_called_once_with( + uri="db://my-database", api_key="test-key", region="us-west-2" + ) - # The optimize method SHOULD have been called for local storage - mock_optimize.assert_called() + def test_object_storage_passes_uri_and_storage_options(self): + config = AppConfig( + lancedb=LanceDBConfig( + uri="s3://bucket/path", + storage_options={ + "endpoint": "http://minio:9000", + "region": "us-east-1", + }, + ) + ) + with patch("haiku.rag.store.engine.lancedb.connect") as mock_connect: + connect_lancedb(config) + mock_connect.assert_called_once_with( + uri="s3://bucket/path", + storage_options={ + "endpoint": "http://minio:9000", + "region": "us-east-1", + }, + ) - store.close() + 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) + mock_connect.assert_called_once_with(uri="s3://bucket/path") + + 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) + + +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() + + 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() + + 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() + + +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() + + @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() + + @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() + + +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() + + 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() + + +class TestStoreSkipsPathValidationForRemote: + 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( + nonexistent, + config=config, + create=True, + skip_validation=True, + skip_migration_check=True, + ) + store.close() + + def test_skips_path_check_for_object_storage(self, tmp_path): + nonexistent = tmp_path / "does_not_exist" / "db.lancedb" + config = AppConfig( + lancedb=LanceDBConfig( + uri="s3://bucket/path", + storage_options={"endpoint": "http://localhost:9000"}, + ) + ) + with patch("haiku.rag.store.engine.lancedb.connect"): + with patch.object(Store, "_init_tables"): + store = Store( + nonexistent, + config=config, + create=True, + skip_validation=True, + skip_migration_check=True, + ) + store.close() From 343bfd7199ae33ec39118e21d469873c4ebceb5e Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Fri, 3 Apr 2026 12:02:20 +0300 Subject: [PATCH 2/8] guard app/UI filesystem checks for remote storage --- haiku_rag_slim/haiku/rag/app.py | 18 ++-- .../haiku/rag/inspector/widgets/info_modal.py | 10 +- tests/test_info.py | 92 +++++++++++++++++++ 3 files changed, 110 insertions(+), 10 deletions(-) diff --git a/haiku_rag_slim/haiku/rag/app.py b/haiku_rag_slim/haiku/rag/app.py index a0213a6f..e9d968d5 100644 --- a/haiku_rag_slim/haiku/rag/app.py +++ b/haiku_rag_slim/haiku/rag/app.py @@ -45,9 +45,15 @@ class HaikuRAGApp: # pragma: no cover self.before = before self.console = Console() + @property + def _is_local(self) -> bool: + from haiku.rag.store.engine import ConnectionMode + + return ConnectionMode.from_config(self.config) == ConnectionMode.LOCAL + async def init(self): """Initialize a new database.""" - if self.db_path.exists(): + if self._is_local and self.db_path.exists(): self.console.print( f"[yellow]Database already exists at {self.db_path}[/yellow]" ) @@ -63,7 +69,7 @@ class HaikuRAGApp: # pragma: no cover async def info(self): """Display read-only information about the database without modifying it.""" - import lancedb + from haiku.rag.store.engine import Store, connect_lancedb # Basic: show path self.console.print("[bold]haiku.rag database info[/bold]") @@ -71,17 +77,15 @@ class HaikuRAGApp: # pragma: no cover f" [repr.attrib_name]path[/repr.attrib_name]: {self.db_path}" ) - if not self.db_path.exists(): + if self._is_local and not self.db_path.exists(): self.console.print("[red]Database path does not exist.[/red]") return # Connect without going through Store to avoid upgrades/validation writes - db = lancedb.connect(self.db_path) + db = connect_lancedb(self.config, self.db_path) versions = get_package_versions() - from haiku.rag.store.engine import Store - store = Store( self.db_path, config=self.config, @@ -201,7 +205,7 @@ class HaikuRAGApp: # pragma: no cover """ from haiku.rag.store.engine import Store - if not self.db_path.exists(): + if self._is_local and not self.db_path.exists(): self.console.print("[red]Database path does not exist.[/red]") return 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 4621c0e3..23dc4f8f 100644 --- a/haiku_rag_slim/haiku/rag/inspector/widgets/info_modal.py +++ b/haiku_rag_slim/haiku/rag/inspector/widgets/info_modal.py @@ -64,21 +64,25 @@ class InfoModal(ModalScreen): async def on_mount(self) -> None: """Load and display database info.""" - import lancedb + from haiku.rag.store.engine import ConnectionMode, connect_lancedb lines: list[str] = [] # Path lines.append(f"[bold $accent]path[/bold $accent]: {self.db_path}") - if not self.db_path.exists(): + is_local = ( + ConnectionMode.from_config(self.client.store._config) + == ConnectionMode.LOCAL + ) + if is_local and not self.db_path.exists(): lines.append("[red]Database path does not exist.[/red]") self._content_widget.update("\n".join(lines)) return # Connect to get table info try: - db = lancedb.connect(self.db_path) + db = connect_lancedb(self.client.store._config, self.db_path) table_names = set(db.list_tables().tables) except Exception as e: lines.append(f"[red]Failed to open database: {e}[/red]") diff --git a/tests/test_info.py b/tests/test_info.py index cd24bac4..ffbdf2ca 100644 --- a/tests/test_info.py +++ b/tests/test_info.py @@ -1,8 +1,10 @@ import json +from unittest.mock import patch import pytest from haiku.rag.app import HaikuRAGApp +from haiku.rag.config.models import AppConfig, LanceDBConfig @pytest.mark.asyncio @@ -153,3 +155,93 @@ async def test_app_info_with_vector_index(temp_db_path, capsys): # Check basic info still present assert "documents: 1" in out assert "chunks: 512" in out + + +@pytest.mark.asyncio +async def test_app_info_uses_connect_lancedb_for_remote(tmp_path, capsys): + """info() should use connect_lancedb() instead of direct lancedb.connect() for remote URIs.""" + nonexistent = tmp_path / "does_not_exist" / "db.lancedb" + config = AppConfig( + lancedb=LanceDBConfig( + uri="s3://bucket/path", + storage_options={"endpoint": "http://localhost:9000"}, + ) + ) + app = HaikuRAGApp(db_path=nonexistent, config=config) + + with patch("haiku.rag.store.engine.connect_lancedb") as mock_connect: + # Make the mock return something that lets info() proceed minimally + mock_db = mock_connect.return_value + mock_table = mock_db.open_table.return_value + mock_table.search.return_value.where.return_value.limit.return_value.to_arrow.return_value.to_pylist.return_value = [ + { + "settings": json.dumps( + { + "version": "1.0.0", + "embeddings": { + "model": { + "provider": "test", + "name": "test", + "vector_dim": 3, + } + }, + } + ) + } + ] + + with patch("haiku.rag.store.engine.Store") as mock_store_cls: + mock_store = mock_store_cls.return_value + mock_store.get_stats.return_value = { + "documents": {"exists": True, "num_rows": 0, "total_bytes": 0}, + "chunks": { + "exists": True, + "num_rows": 0, + "total_bytes": 0, + "has_vector_index": False, + "num_indexed_rows": 0, + "num_unindexed_rows": 0, + }, + } + await app.info() + + mock_connect.assert_called_once_with(config, nonexistent) + + +@pytest.mark.asyncio +async def test_app_init_skips_exists_check_for_remote(tmp_path): + """init() should not check db_path.exists() for remote URIs.""" + nonexistent = tmp_path / "does_not_exist" / "db.lancedb" + config = AppConfig( + lancedb=LanceDBConfig( + uri="s3://bucket/path", + storage_options={"endpoint": "http://localhost:9000"}, + ) + ) + app = HaikuRAGApp(db_path=nonexistent, config=config) + + with patch("haiku.rag.app.HaikuRAG") as mock_client_cls: + await app.init() + # Should have called HaikuRAG to create, not returned early + mock_client_cls.assert_called_once() + + +@pytest.mark.asyncio +async def test_app_history_skips_exists_check_for_remote(tmp_path): + """history() should not check db_path.exists() for remote URIs.""" + nonexistent = tmp_path / "does_not_exist" / "db.lancedb" + config = AppConfig( + lancedb=LanceDBConfig( + uri="s3://bucket/path", + storage_options={"endpoint": "http://localhost:9000"}, + ) + ) + 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 = [] + await app.history() + mock_store_cls.assert_called_once() From 2522d46304af3cee734efc0dcdb09118042b2274 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Fri, 3 Apr 2026 15:59:19 +0300 Subject: [PATCH 3/8] support remote storage in skill generator --- haiku_rag_slim/haiku/rag/cli.py | 8 ++-- .../haiku/rag/skill_generator/__init__.py | 20 ++++++-- .../skill_generator/templates/__init__.py.j2 | 4 ++ tests/test_skill_generator.py | 48 +++++++++++++++++++ 4 files changed, 73 insertions(+), 7 deletions(-) diff --git a/haiku_rag_slim/haiku/rag/cli.py b/haiku_rag_slim/haiku/rag/cli.py index 5e6f974e..08c4ed7b 100644 --- a/haiku_rag_slim/haiku/rag/cli.py +++ b/haiku_rag_slim/haiku/rag/cli.py @@ -718,7 +718,7 @@ def serve( @_cli.command( "create-skill", - help="Generate a standalone skill package with an embedded database", + help="Generate a standalone skill package with an embedded or remote database", ) def create_skill_cmd( # pragma: no cover name: str = typer.Option( @@ -726,10 +726,10 @@ def create_skill_cmd( # pragma: no cover "--name", help="Skill name (lowercase alphanumeric and hyphens)", ), - db: Path = typer.Option( - ..., + db: Path | None = typer.Option( + None, "--db", - help="Path to the LanceDB database to embed", + help="Path to the LanceDB database to embed (omit for remote storage)", ), description: str | None = typer.Option( None, diff --git a/haiku_rag_slim/haiku/rag/skill_generator/__init__.py b/haiku_rag_slim/haiku/rag/skill_generator/__init__.py index 9b09b514..50c50019 100644 --- a/haiku_rag_slim/haiku/rag/skill_generator/__init__.py +++ b/haiku_rag_slim/haiku/rag/skill_generator/__init__.py @@ -74,6 +74,7 @@ def render_templates( description: str, tool_names: list[str], preamble: str | None = None, + remote: bool = False, ) -> pathlib.Path: if preamble is None: preamble = DEFAULT_PREAMBLE @@ -88,6 +89,7 @@ def render_templates( "tool_names": tool_names, "preamble": preamble, "rag_version": rag_version, + "remote": remote, } result_dir = output_dir / f"{name}-skill" @@ -115,7 +117,7 @@ def render_templates( def generate_skill( - db_path: pathlib.Path, + db_path: pathlib.Path | None, output_dir: pathlib.Path, name: str, description: str, @@ -125,7 +127,16 @@ def generate_skill( ) -> pathlib.Path: validate_metadata(name, description) validate_tools(tool_names) - validate_db_path(db_path) + + if db_path is None: + if config_path is None: + raise ValueError( + "config_path is required when db_path is not provided " + "(remote storage needs connection config)" + ) + else: + validate_db_path(db_path) + validate_output_dir(output_dir, name) result = render_templates( @@ -134,11 +145,14 @@ def generate_skill( description=description, tool_names=tool_names, preamble=preamble, + remote=db_path is None, ) pkg_name = name.replace("-", "_") assets_dir = result / f"{pkg_name}_skill" / "assets" - shutil.copytree(db_path, assets_dir / f"{name}.lancedb") + + if db_path is not None: + shutil.copytree(db_path, assets_dir / f"{name}.lancedb") if config_path is not None: shutil.copy2(config_path, assets_dir / "haiku.rag.yaml") diff --git a/haiku_rag_slim/haiku/rag/skill_generator/templates/__init__.py.j2 b/haiku_rag_slim/haiku/rag/skill_generator/templates/__init__.py.j2 index fe597b15..a82437af 100644 --- a/haiku_rag_slim/haiku/rag/skill_generator/templates/__init__.py.j2 +++ b/haiku_rag_slim/haiku/rag/skill_generator/templates/__init__.py.j2 @@ -27,7 +27,11 @@ from haiku.rag.skills._tools import AnalysisEntry _TOOL_NAMES = {{ tool_names | tojson }} _ASSETS_DIR = Path(__file__).resolve().parent / "assets" +{% if remote %} +_DB_PATH = None +{% else %} _DB_PATH = _ASSETS_DIR / "{{ name }}.lancedb" +{% endif %} _CONFIG_PATH = _ASSETS_DIR / "haiku.rag.yaml" diff --git a/tests/test_skill_generator.py b/tests/test_skill_generator.py index 3127342c..d76f053b 100644 --- a/tests/test_skill_generator.py +++ b/tests/test_skill_generator.py @@ -1,8 +1,10 @@ import shutil import subprocess import zipfile +from pathlib import Path import pytest +import yaml from haiku.rag.skill_generator import ( AVAILABLE_TOOLS, @@ -449,3 +451,49 @@ class TestGenerateSkill: assert any(n.endswith("SKILL.md") for n in names) assert any("assets/" in n and n.endswith("data.lance") for n in names) assert any(n.endswith("haiku.rag.yaml") for n in names) + + +def _make_remote_config(tmp_path: Path) -> Path: + config_file = tmp_path / "haiku.rag.yaml" + config_file.write_text( + yaml.dump( + { + "lancedb": { + "uri": "s3://my-bucket/haiku-rag", + "storage_options": { + "endpoint": "http://minio:9000", + "region": "us-east-1", + }, + } + } + ) + ) + return config_file + + +class TestGenerateSkillRemote: + def test_remote_skips_copytree(self, tmp_path): + config_file = _make_remote_config(tmp_path) + result = generate_skill( + db_path=None, + output_dir=tmp_path, + name="recipes", + description="A recipe skill.", + tool_names=["search", "ask"], + config_path=config_file, + ) + assets = result / "recipes_skill" / "assets" + # No bundled database + assert not (assets / "recipes.lancedb").exists() + # Config must be copied + assert (assets / "haiku.rag.yaml").is_file() + + def test_remote_requires_config_path(self, tmp_path): + with pytest.raises(ValueError, match="config_path.*required"): + generate_skill( + db_path=None, + output_dir=tmp_path, + name="recipes", + description="A recipe skill.", + tool_names=["search"], + ) From 8e876ef28d11b6747afa099e24db0dd2b11c8b54 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Fri, 3 Apr 2026 16:22:42 +0300 Subject: [PATCH 4/8] For remote stores, detect new db by checking if tables exist --- haiku_rag_slim/haiku/rag/store/engine.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/haiku_rag_slim/haiku/rag/store/engine.py b/haiku_rag_slim/haiku/rag/store/engine.py index 5ca06037..3620b8ff 100644 --- a/haiku_rag_slim/haiku/rag/store/engine.py +++ b/haiku_rag_slim/haiku/rag/store/engine.py @@ -149,6 +149,12 @@ class Store: # Connect to LanceDB self.db = connect_lancedb(self._config, db_path) + # 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 + if not existing_tables: + is_new_db = True + # For existing databases, read stored vector dimension to create ChunkRecord # that can read existing chunks. For new databases, use config's dimension. stored_vector_dim = None From 98641c24e4b27205c8070e47cd62f4601b8f6cb2 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Fri, 3 Apr 2026 16:55:11 +0300 Subject: [PATCH 5/8] S3 storage tests --- haiku_rag_slim/haiku/rag/app.py | 14 ++- tests/docker/docker-compose.minio.yml | 22 ++++ tests/test_s3_integration.py | 152 ++++++++++++++++++++++++++ 3 files changed, 185 insertions(+), 3 deletions(-) create mode 100644 tests/docker/docker-compose.minio.yml create mode 100644 tests/test_s3_integration.py diff --git a/haiku_rag_slim/haiku/rag/app.py b/haiku_rag_slim/haiku/rag/app.py index e9d968d5..1b09d38a 100644 --- a/haiku_rag_slim/haiku/rag/app.py +++ b/haiku_rag_slim/haiku/rag/app.py @@ -62,8 +62,9 @@ class HaikuRAGApp: # pragma: no cover # Create the database client = HaikuRAG(db_path=self.db_path, config=self.config, create=True) client.close() + display_path = self.config.lancedb.uri if not self._is_local else self.db_path self.console.print( - f"[bold green]Database initialized at {self.db_path}[/bold green]" + f"[bold green]Database initialized at {display_path}[/bold green]" ) async def info(self): @@ -71,10 +72,11 @@ class HaikuRAGApp: # pragma: no cover from haiku.rag.store.engine import Store, connect_lancedb - # Basic: show path + # Basic: show path/URI self.console.print("[bold]haiku.rag database info[/bold]") + display_path = self.config.lancedb.uri if not self._is_local else self.db_path self.console.print( - f" [repr.attrib_name]path[/repr.attrib_name]: {self.db_path}" + f" [repr.attrib_name]path[/repr.attrib_name]: {display_path}" ) if self._is_local and not self.db_path.exists(): @@ -84,6 +86,12 @@ class HaikuRAGApp: # pragma: no cover # Connect without going through Store to avoid upgrades/validation writes db = connect_lancedb(self.config, self.db_path) + if not db.list_tables().tables: + self.console.print( + "[red]Database is empty. Use 'haiku-rag init' to initialize.[/red]" + ) + return + versions = get_package_versions() store = Store( diff --git a/tests/docker/docker-compose.minio.yml b/tests/docker/docker-compose.minio.yml new file mode 100644 index 00000000..2928d0c7 --- /dev/null +++ b/tests/docker/docker-compose.minio.yml @@ -0,0 +1,22 @@ +services: + minio: + image: minio/minio + ports: + - "9000:9000" + - "9001:9001" + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin + command: server /data --console-address ":9001" + + createbucket: + image: minio/mc + depends_on: + - minio + entrypoint: > + /bin/sh -c " + sleep 2; + mc alias set myminio http://minio:9000 minioadmin minioadmin; + mc mb myminio/test-bucket; + exit 0; + " diff --git a/tests/test_s3_integration.py b/tests/test_s3_integration.py new file mode 100644 index 00000000..85a7e08f --- /dev/null +++ b/tests/test_s3_integration.py @@ -0,0 +1,152 @@ +# Start MinIO before running: +# docker compose -f tests/docker/docker-compose.minio.yml up -d +# Stop after: +# docker compose -f tests/docker/docker-compose.minio.yml down -v + +import socket +from uuid import uuid4 + +import pytest + +from haiku.rag.app import HaikuRAGApp +from haiku.rag.client import HaikuRAG +from haiku.rag.config.models import AppConfig, LanceDBConfig +from haiku.rag.store.engine import Store + +MINIO_ENDPOINT = "http://localhost:9000" +MINIO_BUCKET = "test-bucket" +MINIO_STORAGE_OPTIONS = { + "aws_access_key_id": "minioadmin", + "aws_secret_access_key": "minioadmin", + "endpoint": MINIO_ENDPOINT, + "region": "us-east-1", + "allow_http": "true", +} + + +def _minio_available() -> bool: + try: + s = socket.create_connection(("localhost", 9000), timeout=1) + s.close() + return True + except OSError: + return False + + +pytestmark = [ + pytest.mark.integration, + pytest.mark.skipif( + not _minio_available(), reason="MinIO not running on localhost:9000" + ), +] + + +def _make_config() -> AppConfig: + unique_prefix = uuid4().hex[:8] + return AppConfig( + lancedb=LanceDBConfig( + uri=f"s3://{MINIO_BUCKET}/test-{unique_prefix}", + storage_options=MINIO_STORAGE_OPTIONS, + ) + ) + + +def test_store_connect_and_create(tmp_path): + config = _make_config() + store = Store(tmp_path / "unused", config=config, create=True) + stats = store.get_stats() + assert stats["documents"]["exists"] + assert stats["chunks"]["exists"] + store.close() + + +@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() + + +def test_store_add_document(tmp_path): + from haiku.rag.store.engine import DocumentRecord + + config = _make_config() + store = Store(tmp_path / "unused", config=config, create=True) + + doc = DocumentRecord(content="The quick brown fox jumps over the lazy dog.") + store.documents_table.add([doc]) + + stats = store.get_stats() + assert stats["documents"]["num_rows"] == 1 + store.close() + + +@pytest.mark.asyncio +async def test_client_create_document(tmp_path): + config = _make_config() + async with HaikuRAG(tmp_path / "unused", config=config, create=True) as rag: + doc = await rag.create_document( + "Python is a programming language.", uri="test://python" + ) + assert doc.id + assert doc.uri == "test://python" + + +@pytest.mark.asyncio +async def test_client_list_documents(tmp_path): + config = _make_config() + async with HaikuRAG(tmp_path / "unused", config=config, create=True) as rag: + await rag.create_document("First document.", uri="test://first") + await rag.create_document("Second document.", uri="test://second") + + docs = await rag.list_documents() + assert len(docs) == 2 + + +@pytest.mark.asyncio +async def test_client_search(tmp_path): + config = _make_config() + async with HaikuRAG(tmp_path / "unused", config=config, create=True) as rag: + await rag.create_document( + "The Eiffel Tower is located in Paris, France.", uri="test://eiffel" + ) + results = await rag.search("Eiffel Tower") + assert len(results) > 0 + assert "Eiffel" in results[0].content + + +@pytest.mark.asyncio +async def test_client_delete_document(tmp_path): + config = _make_config() + async with HaikuRAG(tmp_path / "unused", config=config, create=True) as rag: + doc = await rag.create_document("Temporary document.", uri="test://temp") + await rag.delete_document(doc.id) + docs = await rag.list_documents() + assert len(docs) == 0 + + +@pytest.mark.asyncio +async def test_app_info(tmp_path, capsys): + config = _make_config() + # Initialize the database first + async with HaikuRAG(tmp_path / "unused", config=config, create=True) as rag: + await rag.create_document("Info test document.", uri="test://info") + + app = HaikuRAGApp(db_path=tmp_path / "unused", config=config) + await app.info() + + out = capsys.readouterr().out + assert "path:" in out + assert config.lancedb.uri in out + assert "documents: 1" in out + + +@pytest.mark.asyncio +async def test_app_info_empty_db(tmp_path, capsys): + config = _make_config() + app = HaikuRAGApp(db_path=tmp_path / "unused", config=config) + await app.info() + + out = capsys.readouterr().out + assert "Database is empty" in out From 5e7a4ebae552e671ecb49069c06390d9e8548b52 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Fri, 3 Apr 2026 16:59:08 +0300 Subject: [PATCH 6/8] Update docs --- CHANGELOG.md | 5 +++++ docs/configuration/storage.md | 31 ++++++++++++++++++++++++++----- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 271c23be..44945886 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Changelog ## [Unreleased] +### Added + +- **S3/Object storage support**: Connect to LanceDB on S3, GCS, Azure Blob, or HDFS via `lancedb.uri` and `storage_options` config. Supports S3-compatible stores (MinIO, Tigris) with custom endpoints. +- **Remote skill generation**: `create-skill` now supports remote databases — omit `--db` and provide `--config-file` to generate skills that connect to object storage at runtime instead of bundling the database. + ## [0.38.0] - 2026-04-07 ### Added diff --git a/docs/configuration/storage.md b/docs/configuration/storage.md index 0dec6fa1..a2dd1592 100644 --- a/docs/configuration/storage.md +++ b/docs/configuration/storage.md @@ -32,26 +32,47 @@ lancedb: # Amazon S3 lancedb: uri: s3://my-bucket/my-table -# Use AWS credentials or IAM roles + storage_options: + region: us-east-1 + +# Amazon S3 with explicit credentials +lancedb: + uri: s3://my-bucket/my-table + storage_options: + aws_access_key_id: YOUR_ACCESS_KEY + aws_secret_access_key: YOUR_SECRET_KEY + region: us-east-1 + +# S3-compatible (MinIO, Tigris, etc.) +lancedb: + uri: s3://my-bucket/my-table + storage_options: + endpoint: http://localhost:9000 + aws_access_key_id: minioadmin + aws_secret_access_key: minioadmin + region: us-east-1 + allow_http: "true" # Azure Blob Storage lancedb: uri: az://my-container/my-table -# Use Azure credentials # Google Cloud Storage lancedb: uri: gs://my-bucket/my-table -# Use GCP credentials # HDFS lancedb: uri: hdfs://namenode:port/path/to/table ``` -Authentication is handled through standard cloud provider credentials (AWS CLI, Azure CLI, gcloud, etc.) or by setting `api_key` for LanceDB Cloud. +- **LanceDB Cloud** (`db://`): Requires `api_key` and `region`. Table optimization and indexing are managed server-side. +- **Object storage** (`s3://`, `gs://`, `az://`, `hdfs://`): Uses `storage_options` for credentials and endpoint configuration. Authentication can also be provided via environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, etc.) or cloud provider SDK defaults (AWS CLI, Azure CLI, gcloud). +- **S3-compatible stores** (MinIO, Tigris, etc.): Set `endpoint` in `storage_options`. When using `http://` endpoints, also set `allow_http: "true"`. -**Note:** Table optimization is automatically handled by LanceDB Cloud (`db://` URIs) and is disabled for better performance. For object storage backends (S3, Azure, GCS), optimization is still performed locally. +The `storage_options` keys are case-insensitive and passed directly to the underlying object store library. Available keys depend on the backend — see the [LanceDB storage docs](https://lancedb.com/docs/storage/) for details. + +**Note:** Table optimization is automatically handled by LanceDB Cloud (`db://` URIs) and is disabled for better performance. For object storage backends (S3, Azure, GCS), optimization and vector indexing are still performed normally. ## Database Creation From 367accb8145906ad0ff15118a9ac1bfa32bbf110 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Fri, 3 Apr 2026 18:20:25 +0300 Subject: [PATCH 7/8] Replace MinIO with SeaweedFS for S3 integration tests --- docs/configuration/storage.md | 8 ++++---- tests/docker/docker-compose.minio.yml | 22 -------------------- tests/docker/docker-compose.s3.yml | 18 +++++++++++++++++ tests/docker/s3-config.json | 14 +++++++++++++ tests/test_s3_integration.py | 29 +++++++++++++-------------- 5 files changed, 50 insertions(+), 41 deletions(-) delete mode 100644 tests/docker/docker-compose.minio.yml create mode 100644 tests/docker/docker-compose.s3.yml create mode 100644 tests/docker/s3-config.json diff --git a/docs/configuration/storage.md b/docs/configuration/storage.md index a2dd1592..7b954766 100644 --- a/docs/configuration/storage.md +++ b/docs/configuration/storage.md @@ -43,13 +43,13 @@ lancedb: aws_secret_access_key: YOUR_SECRET_KEY region: us-east-1 -# S3-compatible (MinIO, Tigris, etc.) +# S3-compatible (SeaweedFS, Tigris, etc.) lancedb: uri: s3://my-bucket/my-table storage_options: - endpoint: http://localhost:9000 - aws_access_key_id: minioadmin - aws_secret_access_key: minioadmin + endpoint: http://localhost:8333 + aws_access_key_id: YOUR_ACCESS_KEY + aws_secret_access_key: YOUR_SECRET_KEY region: us-east-1 allow_http: "true" diff --git a/tests/docker/docker-compose.minio.yml b/tests/docker/docker-compose.minio.yml deleted file mode 100644 index 2928d0c7..00000000 --- a/tests/docker/docker-compose.minio.yml +++ /dev/null @@ -1,22 +0,0 @@ -services: - minio: - image: minio/minio - ports: - - "9000:9000" - - "9001:9001" - environment: - MINIO_ROOT_USER: minioadmin - MINIO_ROOT_PASSWORD: minioadmin - command: server /data --console-address ":9001" - - createbucket: - image: minio/mc - depends_on: - - minio - entrypoint: > - /bin/sh -c " - sleep 2; - mc alias set myminio http://minio:9000 minioadmin minioadmin; - mc mb myminio/test-bucket; - exit 0; - " diff --git a/tests/docker/docker-compose.s3.yml b/tests/docker/docker-compose.s3.yml new file mode 100644 index 00000000..facf5be7 --- /dev/null +++ b/tests/docker/docker-compose.s3.yml @@ -0,0 +1,18 @@ +services: + seaweedfs: + image: chrislusf/seaweedfs + ports: + - "8333:8333" + command: server -s3 -s3.config=/etc/seaweedfs/s3-config.json + volumes: + - ./s3-config.json:/etc/seaweedfs/s3-config.json:ro + + createbucket: + image: chrislusf/seaweedfs + depends_on: + - seaweedfs + entrypoint: > + /bin/sh -c " + sleep 3 && + echo 's3.bucket.create -name test-bucket' | weed shell -master seaweedfs:9333 + " diff --git a/tests/docker/s3-config.json b/tests/docker/s3-config.json new file mode 100644 index 00000000..1d9ec9ce --- /dev/null +++ b/tests/docker/s3-config.json @@ -0,0 +1,14 @@ +{ + "identities": [ + { + "name": "admin", + "credentials": [ + { + "accessKey": "testkey", + "secretKey": "testsecret" + } + ], + "actions": ["Admin", "Read", "Write", "List", "Tagging"] + } + ] +} diff --git a/tests/test_s3_integration.py b/tests/test_s3_integration.py index 85a7e08f..b763c1df 100644 --- a/tests/test_s3_integration.py +++ b/tests/test_s3_integration.py @@ -1,7 +1,7 @@ -# Start MinIO before running: -# docker compose -f tests/docker/docker-compose.minio.yml up -d +# Start SeaweedFS before running: +# docker compose -f tests/docker/docker-compose.s3.yml up -d # Stop after: -# docker compose -f tests/docker/docker-compose.minio.yml down -v +# docker compose -f tests/docker/docker-compose.s3.yml down -v import socket from uuid import uuid4 @@ -13,20 +13,20 @@ from haiku.rag.client import HaikuRAG from haiku.rag.config.models import AppConfig, LanceDBConfig from haiku.rag.store.engine import Store -MINIO_ENDPOINT = "http://localhost:9000" -MINIO_BUCKET = "test-bucket" -MINIO_STORAGE_OPTIONS = { - "aws_access_key_id": "minioadmin", - "aws_secret_access_key": "minioadmin", - "endpoint": MINIO_ENDPOINT, +S3_ENDPOINT = "http://localhost:8333" +S3_BUCKET = "test-bucket" +S3_STORAGE_OPTIONS = { + "endpoint": S3_ENDPOINT, "region": "us-east-1", "allow_http": "true", + "aws_access_key_id": "testkey", + "aws_secret_access_key": "testsecret", } -def _minio_available() -> bool: +def _s3_available() -> bool: try: - s = socket.create_connection(("localhost", 9000), timeout=1) + s = socket.create_connection(("localhost", 8333), timeout=1) s.close() return True except OSError: @@ -36,7 +36,7 @@ def _minio_available() -> bool: pytestmark = [ pytest.mark.integration, pytest.mark.skipif( - not _minio_available(), reason="MinIO not running on localhost:9000" + not _s3_available(), reason="SeaweedFS not running on localhost:8333" ), ] @@ -45,8 +45,8 @@ def _make_config() -> AppConfig: unique_prefix = uuid4().hex[:8] return AppConfig( lancedb=LanceDBConfig( - uri=f"s3://{MINIO_BUCKET}/test-{unique_prefix}", - storage_options=MINIO_STORAGE_OPTIONS, + uri=f"s3://{S3_BUCKET}/test-{unique_prefix}", + storage_options=S3_STORAGE_OPTIONS, ) ) @@ -129,7 +129,6 @@ async def test_client_delete_document(tmp_path): @pytest.mark.asyncio async def test_app_info(tmp_path, capsys): config = _make_config() - # Initialize the database first async with HaikuRAG(tmp_path / "unused", config=config, create=True) as rag: await rag.create_document("Info test document.", uri="test://info") From a118ee7aaa55ce687a5fe4ba9ea4501d1a7cd0e0 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Fri, 3 Apr 2026 19:54:35 +0300 Subject: [PATCH 8/8] Cleanup --- CHANGELOG.md | 2 +- haiku_rag_slim/haiku/rag/app.py | 11 ++++------- .../haiku/rag/inspector/widgets/info_modal.py | 8 +++----- haiku_rag_slim/haiku/rag/store/engine.py | 2 -- 4 files changed, 8 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44945886..21b38a42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ### Added -- **S3/Object storage support**: Connect to LanceDB on S3, GCS, Azure Blob, or HDFS via `lancedb.uri` and `storage_options` config. Supports S3-compatible stores (MinIO, Tigris) with custom endpoints. +- **S3/Object storage support**: Connect to LanceDB on S3, GCS, Azure Blob, or HDFS via `lancedb.uri` and `storage_options` config. Supports S3-compatible stores with custom endpoints. - **Remote skill generation**: `create-skill` now supports remote databases — omit `--db` and provide `--config-file` to generate skills that connect to object storage at runtime instead of bundling the database. ## [0.38.0] - 2026-04-07 diff --git a/haiku_rag_slim/haiku/rag/app.py b/haiku_rag_slim/haiku/rag/app.py index 1b09d38a..b06eef1b 100644 --- a/haiku_rag_slim/haiku/rag/app.py +++ b/haiku_rag_slim/haiku/rag/app.py @@ -45,11 +45,10 @@ class HaikuRAGApp: # pragma: no cover self.before = before self.console = Console() - @property - def _is_local(self) -> bool: from haiku.rag.store.engine import ConnectionMode - return ConnectionMode.from_config(self.config) == ConnectionMode.LOCAL + self._is_local = ConnectionMode.from_config(self.config) == ConnectionMode.LOCAL + self._display_path = self.db_path if self._is_local else self.config.lancedb.uri async def init(self): """Initialize a new database.""" @@ -62,9 +61,8 @@ class HaikuRAGApp: # pragma: no cover # Create the database client = HaikuRAG(db_path=self.db_path, config=self.config, create=True) client.close() - display_path = self.config.lancedb.uri if not self._is_local else self.db_path self.console.print( - f"[bold green]Database initialized at {display_path}[/bold green]" + f"[bold green]Database initialized at {self._display_path}[/bold green]" ) async def info(self): @@ -74,9 +72,8 @@ class HaikuRAGApp: # pragma: no cover # Basic: show path/URI self.console.print("[bold]haiku.rag database info[/bold]") - display_path = self.config.lancedb.uri if not self._is_local else self.db_path self.console.print( - f" [repr.attrib_name]path[/repr.attrib_name]: {display_path}" + f" [repr.attrib_name]path[/repr.attrib_name]: {self._display_path}" ) if self._is_local and not self.db_path.exists(): 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 23dc4f8f..38eb1d76 100644 --- a/haiku_rag_slim/haiku/rag/inspector/widgets/info_modal.py +++ b/haiku_rag_slim/haiku/rag/inspector/widgets/info_modal.py @@ -71,18 +71,16 @@ class InfoModal(ModalScreen): # Path lines.append(f"[bold $accent]path[/bold $accent]: {self.db_path}") - is_local = ( - ConnectionMode.from_config(self.client.store._config) - == ConnectionMode.LOCAL - ) + is_local = self.client.store._connection_mode == ConnectionMode.LOCAL if is_local and not self.db_path.exists(): lines.append("[red]Database path does not exist.[/red]") self._content_widget.update("\n".join(lines)) return # Connect to get table info + config = self.client.store._config try: - db = connect_lancedb(self.client.store._config, self.db_path) + db = connect_lancedb(config, self.db_path) table_names = set(db.list_tables().tables) except Exception as e: lines.append(f"[red]Failed to open database: {e}[/red]") diff --git a/haiku_rag_slim/haiku/rag/store/engine.py b/haiku_rag_slim/haiku/rag/store/engine.py index 3620b8ff..26cd4b71 100644 --- a/haiku_rag_slim/haiku/rag/store/engine.py +++ b/haiku_rag_slim/haiku/rag/store/engine.py @@ -19,8 +19,6 @@ from haiku.rag.store.exceptions import MigrationRequiredError, ReadOnlyError logger = logging.getLogger(__name__) -OBJECT_STORAGE_PREFIXES = ("s3://", "gs://", "az://", "hdfs://") - class ConnectionMode(Enum): LOCAL = "local"