info command in CLI

This commit is contained in:
Yiorgis Gozadinos 2025-09-23 12:06:09 +03:00
parent b46765e197
commit 52ea1ebb70
No known key found for this signature in database
5 changed files with 206 additions and 3 deletions

View file

@ -126,6 +126,21 @@ haiku-rag settings
## Maintenance
### Info (Read-only)
Display database metadata without upgrading or modifying it:
```bash
haiku-rag info [--db /path/to/your.lancedb]
```
Shows:
- path to the database
- stored haiku.rag version (from settings)
- embeddings provider/model and vector dimension
- LanceDB version
- number of documents
### Vacuum (Optimize and Cleanup)
Reduce disk usage by optimizing and pruning old table versions across all tables:

View file

@ -1,4 +1,6 @@
import asyncio
import json
from importlib.metadata import version as pkg_version
from pathlib import Path
from rich.console import Console
@ -25,6 +27,89 @@ class HaikuRAGApp:
self.db_path = db_path
self.console = Console()
async def info(self):
"""Display read-only information about the database without modifying it."""
import lancedb
# Basic: show path
self.console.print("[bold]haiku.rag database info[/bold]")
self.console.print(
f" [repr.attrib_name]path[/repr.attrib_name]: {self.db_path}"
)
# Prevent accidental creation: require existing local path
# (For cloud/object storage users, info should be invoked with proper env vars and
# an existing local cache/path if applicable.)
if 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
try:
db = lancedb.connect(self.db_path)
table_names = set(db.table_names())
except Exception as e:
self.console.print(f"[red]Failed to open database: {e}[/red]")
return
# Resolve LanceDB version (best-effort)
try:
ldb_version = pkg_version("lancedb")
except Exception:
ldb_version = "unknown"
# Read settings (if present) to find stored haiku.rag version and embedding config
stored_version = "unknown"
embed_provider: str | None = None
embed_model: str | None = None
vector_dim: int | None = None
if "settings" in table_names:
settings_tbl = db.open_table("settings")
arrow = settings_tbl.search().where("id = 'settings'").limit(1).to_arrow()
rows = arrow.to_pylist() if arrow is not None else []
if rows:
raw = rows[0].get("settings") or "{}"
data = json.loads(raw) if isinstance(raw, str) else (raw or {})
stored_version = str(data.get("version", stored_version))
embed_provider = data.get("EMBEDDINGS_PROVIDER")
embed_model = data.get("EMBEDDINGS_MODEL")
vector_dim = (
int(data.get("EMBEDDINGS_VECTOR_DIM")) # pyright: ignore[reportArgumentType]
if data.get("EMBEDDINGS_VECTOR_DIM") is not None
else None
)
# Count documents efficiently (best-effort, avoiding full scans)
num_docs = 0
if "documents" in table_names:
docs_tbl = db.open_table("documents")
num_docs = int(docs_tbl.count_rows()) # type: ignore[attr-defined]
# Render collected info
self.console.print(
f" [repr.attrib_name]haiku.rag version (db)[/repr.attrib_name]: {stored_version}"
)
if embed_provider or embed_model or vector_dim:
provider_part = embed_provider or "unknown"
model_part = embed_model or "unknown"
dim_part = f"{vector_dim}" if vector_dim is not None else "unknown"
self.console.print(
" [repr.attrib_name]embeddings[/repr.attrib_name]: "
f"{provider_part}/{model_part} (dim: {dim_part})"
)
else:
self.console.print(
" [repr.attrib_name]embeddings[/repr.attrib_name]: unknown"
)
self.console.print(
f" [repr.attrib_name]lancedb[/repr.attrib_name]: {ldb_version}"
)
self.console.print(
f" [repr.attrib_name]documents[/repr.attrib_name]: {num_docs}"
)
async def list_documents(self):
async with HaikuRAG(db_path=self.db_path) as self.client:
documents = await self.client.list_documents()

View file

@ -347,6 +347,20 @@ def vacuum(
asyncio.run(app.vacuum())
@cli.command("info", help="Show read-only database info (no upgrades or writes)")
def info(
db: Path = typer.Option(
Config.DEFAULT_DATA_DIR / "haiku.rag.lancedb",
"--db",
help="Path to the LanceDB database file",
),
):
from haiku.rag.app import HaikuRAGApp
app = HaikuRAGApp(db_path=db)
asyncio.run(app.info())
@cli.command(
"serve", help="Start the haiku.rag MCP server (by default in streamable HTTP mode)"
)

View file

@ -144,6 +144,16 @@ def test_ask_with_cite():
result = runner.invoke(cli, ["ask", "What is Python?", "--cite"])
assert result.exit_code == 0
mock_app_instance.ask.assert_called_once_with(
question="What is Python?", cite=True
)
mock_app_instance.ask.assert_called_once_with(question="What is Python?", cite=True)
def test_info():
with patch("haiku.rag.app.HaikuRAGApp") as mock_app:
mock_app_instance = MagicMock()
mock_app_instance.info = AsyncMock()
mock_app.return_value = mock_app_instance
result = runner.invoke(cli, ["info"])
assert result.exit_code == 0
mock_app_instance.info.assert_called_once()

79
tests/test_info.py Normal file
View file

@ -0,0 +1,79 @@
import json
import pytest
from haiku.rag.app import HaikuRAGApp
@pytest.mark.asyncio
async def test_app_info_outputs_and_read_only(temp_db_path, capsys):
# Build a minimal LanceDB with settings, documents, and chunks without using Store
import lancedb
from lancedb.pydantic import LanceModel, Vector
from pydantic import Field
db = lancedb.connect(temp_db_path)
class SettingsRecord(LanceModel):
id: str = Field(default="settings")
settings: str = Field(default="{}")
class DocumentRecord(LanceModel):
id: str
content: str
class ChunkRecord(LanceModel):
id: str
document_id: str
content: str
vector: Vector(3) # type: ignore
settings_tbl = db.create_table("settings", schema=SettingsRecord)
docs_tbl = db.create_table("documents", schema=DocumentRecord)
chunks_tbl = db.create_table("chunks", schema=ChunkRecord)
# Insert one of each
settings_tbl.add(
[
SettingsRecord(
id="settings",
settings=json.dumps(
{
"version": "1.2.3",
"EMBEDDINGS_PROVIDER": "openai",
"EMBEDDINGS_MODEL": "text-embedding-3-small",
"EMBEDDINGS_VECTOR_DIM": 3,
}
),
)
]
)
docs_tbl.add([DocumentRecord(id="doc-1", content="hello")])
chunks_tbl.add(
[ChunkRecord(id="c1", document_id="doc-1", content="c", vector=[0.1, 0.2, 0.3])]
)
# Capture versions before
before_versions = {
"settings": int(settings_tbl.version),
"documents": int(docs_tbl.version),
"chunks": int(chunks_tbl.version),
}
app = HaikuRAGApp(db_path=temp_db_path)
await app.info()
out = capsys.readouterr().out
# Validate expected content substrings
assert f"path: \n{temp_db_path}" in out
assert "haiku.rag version (db): 1.2.3" in out
assert "embeddings: openai/text-embedding-3-small (dim: 3)" in out
assert "lancedb:" in out
assert "documents: 1" in out
# Verify no versions changed (read-only)
# Re-open to ensure fresh view
db2 = lancedb.connect(temp_db_path)
assert int(db2.open_table("settings").version) == before_versions["settings"]
assert int(db2.open_table("documents").version) == before_versions["documents"]
assert int(db2.open_table("chunks").version) == before_versions["chunks"]