Open read CLI verbs read-only
This commit is contained in:
parent
0e3d53791f
commit
414be551fb
4 changed files with 70 additions and 10 deletions
|
|
@ -8,6 +8,7 @@
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Opening a database no longer writes to it: reads no longer rewrite the stored embedding settings or change the stored version, and the version is never downgraded. Embedding provider/name drift (matching `vector_dim`) warns on read-only opens and raises `ConfigMismatchError` on writable opens; reconcile with `rebuild --set-embedder`.
|
- Opening a database no longer writes to it: reads no longer rewrite the stored embedding settings or change the stored version, and the version is never downgraded. Embedding provider/name drift (matching `vector_dim`) warns on read-only opens and raises `ConfigMismatchError` on writable opens; reconcile with `rebuild --set-embedder`.
|
||||||
|
- Read CLI verbs (`list`, `get`, `search`, `visualize`, `ask`, `analyze`, `inspect`, `chat`, `info`, `history`) open the database read-only.
|
||||||
|
|
||||||
## [0.54.0] - 2026-06-04
|
## [0.54.0] - 2026-06-04
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -276,7 +276,7 @@ class HaikuRAGApp: # pragma: no cover
|
||||||
async with HaikuRAG(
|
async with HaikuRAG(
|
||||||
db_path=self.db_path,
|
db_path=self.db_path,
|
||||||
config=self.config,
|
config=self.config,
|
||||||
read_only=self.read_only,
|
read_only=True,
|
||||||
before=self.before,
|
before=self.before,
|
||||||
) as self.client:
|
) as self.client:
|
||||||
documents = await self.client.list_documents(filter=filter)
|
documents = await self.client.list_documents(filter=filter)
|
||||||
|
|
@ -328,7 +328,7 @@ class HaikuRAGApp: # pragma: no cover
|
||||||
async with HaikuRAG(
|
async with HaikuRAG(
|
||||||
db_path=self.db_path,
|
db_path=self.db_path,
|
||||||
config=self.config,
|
config=self.config,
|
||||||
read_only=self.read_only,
|
read_only=True,
|
||||||
before=self.before,
|
before=self.before,
|
||||||
) as self.client:
|
) as self.client:
|
||||||
doc = await self.client.get_document_by_id(doc_id)
|
doc = await self.client.get_document_by_id(doc_id)
|
||||||
|
|
@ -385,7 +385,7 @@ class HaikuRAGApp: # pragma: no cover
|
||||||
async with HaikuRAG(
|
async with HaikuRAG(
|
||||||
db_path=self.db_path,
|
db_path=self.db_path,
|
||||||
config=self.config,
|
config=self.config,
|
||||||
read_only=self.read_only,
|
read_only=True,
|
||||||
before=self.before,
|
before=self.before,
|
||||||
) as self.client:
|
) as self.client:
|
||||||
results = await self.client.search(
|
results = await self.client.search(
|
||||||
|
|
@ -407,7 +407,7 @@ class HaikuRAGApp: # pragma: no cover
|
||||||
async with HaikuRAG(
|
async with HaikuRAG(
|
||||||
db_path=self.db_path,
|
db_path=self.db_path,
|
||||||
config=self.config,
|
config=self.config,
|
||||||
read_only=self.read_only,
|
read_only=True,
|
||||||
before=self.before,
|
before=self.before,
|
||||||
) as self.client:
|
) as self.client:
|
||||||
chunk = await self.client.get_chunk_by_id(chunk_id)
|
chunk = await self.client.get_chunk_by_id(chunk_id)
|
||||||
|
|
@ -451,7 +451,7 @@ class HaikuRAGApp: # pragma: no cover
|
||||||
async with HaikuRAG(
|
async with HaikuRAG(
|
||||||
db_path=self.db_path,
|
db_path=self.db_path,
|
||||||
config=self.config,
|
config=self.config,
|
||||||
read_only=self.read_only,
|
read_only=True,
|
||||||
before=self.before,
|
before=self.before,
|
||||||
) as self.client:
|
) as self.client:
|
||||||
answer, citations = await self.client.ask(question, filter=filter)
|
answer, citations = await self.client.ask(question, filter=filter)
|
||||||
|
|
@ -479,7 +479,7 @@ class HaikuRAGApp: # pragma: no cover
|
||||||
async with HaikuRAG(
|
async with HaikuRAG(
|
||||||
db_path=self.db_path,
|
db_path=self.db_path,
|
||||||
config=self.config,
|
config=self.config,
|
||||||
read_only=self.read_only,
|
read_only=True,
|
||||||
before=self.before,
|
before=self.before,
|
||||||
) as self.client:
|
) as self.client:
|
||||||
self.console.print(f"[bold blue]Question:[/bold blue] {question}")
|
self.console.print(f"[bold blue]Question:[/bold blue] {question}")
|
||||||
|
|
@ -566,6 +566,7 @@ class HaikuRAGApp: # pragma: no cover
|
||||||
config=self.config,
|
config=self.config,
|
||||||
skip_validation=True,
|
skip_validation=True,
|
||||||
skip_migration_check=True,
|
skip_migration_check=True,
|
||||||
|
read_only=self.read_only,
|
||||||
) as store:
|
) as store:
|
||||||
return await store.migrate()
|
return await store.migrate()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -406,7 +406,7 @@ def analyze( # pragma: no cover
|
||||||
@_cli.command("settings", help="Display current configuration settings")
|
@_cli.command("settings", help="Display current configuration settings")
|
||||||
def settings(): # pragma: no cover
|
def settings(): # pragma: no cover
|
||||||
config = get_config()
|
config = get_config()
|
||||||
app = HaikuRAGApp(db_path=Path(), config=config)
|
app = HaikuRAGApp(db_path=Path(), config=config, read_only=True)
|
||||||
app.show_settings()
|
app.show_settings()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -612,7 +612,7 @@ def history( # pragma: no cover
|
||||||
|
|
||||||
@_cli.command("download-models", help="Download Docling and Ollama models per config")
|
@_cli.command("download-models", help="Download Docling and Ollama models per config")
|
||||||
def download_models_cmd(): # pragma: no cover
|
def download_models_cmd(): # pragma: no cover
|
||||||
app = HaikuRAGApp(db_path=Path(), config=get_config())
|
app = HaikuRAGApp(db_path=Path(), config=get_config(), read_only=True)
|
||||||
try:
|
try:
|
||||||
asyncio.run(app.download_models())
|
asyncio.run(app.download_models())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -636,7 +636,7 @@ def inspect( # pragma: no cover
|
||||||
raise typer.Exit(1) from e
|
raise typer.Exit(1) from e
|
||||||
|
|
||||||
db_path = db if db else get_config().storage.data_dir / "haiku.rag.lancedb"
|
db_path = db if db else get_config().storage.data_dir / "haiku.rag.lancedb"
|
||||||
run_inspector(db_path, read_only=_read_only, before=_before)
|
run_inspector(db_path, read_only=True, before=_before)
|
||||||
|
|
||||||
|
|
||||||
@_cli.command("chat", help="Launch interactive chat TUI for conversational RAG")
|
@_cli.command("chat", help="Launch interactive chat TUI for conversational RAG")
|
||||||
|
|
@ -666,7 +666,7 @@ def chat( # pragma: no cover
|
||||||
|
|
||||||
run_chat(
|
run_chat(
|
||||||
db_path,
|
db_path,
|
||||||
read_only=_read_only,
|
read_only=True,
|
||||||
before=_before,
|
before=_before,
|
||||||
model=model,
|
model=model,
|
||||||
skills=skills,
|
skills=skills,
|
||||||
|
|
|
||||||
|
|
@ -298,3 +298,61 @@ class TestClientReadOnly:
|
||||||
async with HaikuRAG(temp_db_path, read_only=True) as client:
|
async with HaikuRAG(temp_db_path, read_only=True) as client:
|
||||||
docs = await client.list_documents()
|
docs = await client.list_documents()
|
||||||
assert len(docs) == 1
|
assert len(docs) == 1
|
||||||
|
|
||||||
|
|
||||||
|
class TestAppReadVerbsDoNotWrite:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_read_verb_leaves_settings_and_version_unchanged_on_drift(
|
||||||
|
self, temp_db_path
|
||||||
|
):
|
||||||
|
"""A read CLI verb opens read-only: drift warns but never writes."""
|
||||||
|
from haiku.rag.app import HaikuRAGApp
|
||||||
|
from haiku.rag.config import AppConfig
|
||||||
|
|
||||||
|
async with Store(temp_db_path, create=True) as store:
|
||||||
|
stored_name_before = (
|
||||||
|
await SettingsRepository(store).get_current_settings()
|
||||||
|
)["embeddings"]["model"]["name"]
|
||||||
|
version_before = await store.get_haiku_version()
|
||||||
|
|
||||||
|
drift = AppConfig()
|
||||||
|
drift.embeddings.model.name = "different-model"
|
||||||
|
|
||||||
|
# list is a read verb — must open read-only and not raise on drift
|
||||||
|
app = HaikuRAGApp(db_path=temp_db_path, config=drift)
|
||||||
|
await app.list_documents()
|
||||||
|
|
||||||
|
async with Store(temp_db_path, skip_validation=True, read_only=True) as store:
|
||||||
|
stored_name_after = (
|
||||||
|
await SettingsRepository(store).get_current_settings()
|
||||||
|
)["embeddings"]["model"]["name"]
|
||||||
|
version_after = await store.get_haiku_version()
|
||||||
|
|
||||||
|
assert stored_name_after == stored_name_before
|
||||||
|
assert version_after == version_before
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_write_verb_raises_on_drift_without_writing(self, temp_db_path):
|
||||||
|
"""A write CLI verb opens writable: drift raises before any write."""
|
||||||
|
from haiku.rag.app import HaikuRAGApp
|
||||||
|
from haiku.rag.config import AppConfig
|
||||||
|
from haiku.rag.store.repositories.settings import ConfigMismatchError
|
||||||
|
|
||||||
|
async with Store(temp_db_path, create=True) as store:
|
||||||
|
stored_name_before = (
|
||||||
|
await SettingsRepository(store).get_current_settings()
|
||||||
|
)["embeddings"]["model"]["name"]
|
||||||
|
|
||||||
|
drift = AppConfig()
|
||||||
|
drift.embeddings.model.name = "different-model"
|
||||||
|
|
||||||
|
app = HaikuRAGApp(db_path=temp_db_path, config=drift)
|
||||||
|
with pytest.raises(ConfigMismatchError):
|
||||||
|
await app.add_document_from_text("hello")
|
||||||
|
|
||||||
|
async with Store(temp_db_path, skip_validation=True, read_only=True) as store:
|
||||||
|
stored_name_after = (
|
||||||
|
await SettingsRepository(store).get_current_settings()
|
||||||
|
)["embeddings"]["model"]["name"]
|
||||||
|
|
||||||
|
assert stored_name_after == stored_name_before
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue