info command in CLI
This commit is contained in:
parent
b46765e197
commit
52ea1ebb70
5 changed files with 206 additions and 3 deletions
15
docs/cli.md
15
docs/cli.md
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
79
tests/test_info.py
Normal 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"]
|
||||
Loading…
Reference in a new issue