Render database names and failures verbatim in the inspector
A database name, a location and an exception message all reach the info modal as configuration-derived text, and Rich reads `[...]` in any of them as markup: a name like `beta [prod]` disappeared, and a message carrying `[Errno 2]` or a stray closing tag could break the line it sat in.
This commit is contained in:
parent
7dc853606f
commit
1de1b662ae
2 changed files with 54 additions and 12 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from rich.markup import escape
|
||||||
from textual.app import ComposeResult
|
from textual.app import ComposeResult
|
||||||
from textual.binding import Binding
|
from textual.binding import Binding
|
||||||
from textual.containers import Vertical, VerticalScroll
|
from textual.containers import Vertical, VerticalScroll
|
||||||
|
|
@ -35,12 +36,11 @@ async def database_lines(client: "HaikuRAG") -> list[str]:
|
||||||
db = client.store.db
|
db = client.store.db
|
||||||
stats = await get_database_stats(db)
|
stats = await get_database_stats(db)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return [f"[red]Failed to open database: {e}[/red]"]
|
return [f"[red]Failed to open database: {escape(str(e))}[/red]"]
|
||||||
|
|
||||||
# The store read these on open; a second query would re-read and re-parse
|
# The store read and parsed these on open.
|
||||||
# the same blob.
|
|
||||||
settings = client.store.stored_settings
|
settings = client.store.stored_settings
|
||||||
stored_version = str(settings.get("version", "unknown"))
|
stored_version = escape(str(settings.get("version", "unknown")))
|
||||||
embed_model_obj = settings.get("embeddings", {}).get("model", {})
|
embed_model_obj = settings.get("embeddings", {}).get("model", {})
|
||||||
embed_provider = embed_model_obj.get("provider")
|
embed_provider = embed_model_obj.get("provider")
|
||||||
embed_model = embed_model_obj.get("name")
|
embed_model = embed_model_obj.get("name")
|
||||||
|
|
@ -56,9 +56,9 @@ async def database_lines(client: "HaikuRAG") -> list[str]:
|
||||||
)
|
)
|
||||||
|
|
||||||
if embed_provider or embed_model or vector_dim:
|
if embed_provider or embed_model or vector_dim:
|
||||||
provider_part = embed_provider or "unknown"
|
provider_part = escape(str(embed_provider or "unknown"))
|
||||||
model_part = embed_model or "unknown"
|
model_part = escape(str(embed_model or "unknown"))
|
||||||
dim_part = f"{vector_dim}" if vector_dim is not None else "unknown"
|
dim_part = escape(str(vector_dim)) if vector_dim is not None else "unknown"
|
||||||
lines.append(
|
lines.append(
|
||||||
f"[bold $accent]embeddings[/bold $accent]: "
|
f"[bold $accent]embeddings[/bold $accent]: "
|
||||||
f"{provider_part}/{model_part} (dim: {dim_part})"
|
f"{provider_part}/{model_part} (dim: {dim_part})"
|
||||||
|
|
@ -172,11 +172,15 @@ class InfoModal(ModalScreen):
|
||||||
for block in blocks:
|
for block in blocks:
|
||||||
lines.extend(block)
|
lines.extend(block)
|
||||||
else:
|
else:
|
||||||
lines.append(f"[bold $accent]location[/bold $accent]: {location}")
|
lines.append(
|
||||||
|
f"[bold $accent]location[/bold $accent]: {escape(str(location))}"
|
||||||
|
)
|
||||||
lines.extend(await database_lines(self.client))
|
lines.extend(await database_lines(self.client))
|
||||||
|
|
||||||
lines.append("[bold]Versions[/bold]")
|
lines.append("[bold]Versions[/bold]")
|
||||||
versions = get_package_versions()
|
versions = {
|
||||||
|
key: escape(str(value)) for key, value in get_package_versions().items()
|
||||||
|
}
|
||||||
lines.append(f"[bold $accent]haiku.rag[/bold $accent]: {versions['haiku_rag']}")
|
lines.append(f"[bold $accent]haiku.rag[/bold $accent]: {versions['haiku_rag']}")
|
||||||
lines.append(f"[bold $accent]lancedb[/bold $accent]: {versions['lancedb']}")
|
lines.append(f"[bold $accent]lancedb[/bold $accent]: {versions['lancedb']}")
|
||||||
lines.append(f"[bold $accent]docling[/bold $accent]: {versions['docling']}")
|
lines.append(f"[bold $accent]docling[/bold $accent]: {versions['docling']}")
|
||||||
|
|
@ -192,13 +196,13 @@ class InfoModal(ModalScreen):
|
||||||
|
|
||||||
async def _report(self, name: str) -> list[str]:
|
async def _report(self, name: str) -> list[str]:
|
||||||
"""One database's block, including its own failure to open."""
|
"""One database's block, including its own failure to open."""
|
||||||
lines = [f"[bold]{name}[/bold]"]
|
lines = [f"[bold]{escape(name)}[/bold]"]
|
||||||
try:
|
try:
|
||||||
(owner,) = await self.client.clients_for([name])
|
(owner,) = await self.client.clients_for([name])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# The client names a configured database by name and never by
|
# The client names a configured database by name and never by
|
||||||
# location, so its message is safe to show.
|
# location, so its message is safe to show.
|
||||||
return [*lines, f"[red]{e}[/red]", ""]
|
return [*lines, f"[red]{escape(str(e))}[/red]", ""]
|
||||||
return [*lines, *await database_lines(owner)]
|
return [*lines, *await database_lines(owner)]
|
||||||
|
|
||||||
async def action_dismiss(self, result=None) -> None:
|
async def action_dismiss(self, result=None) -> None:
|
||||||
|
|
|
||||||
|
|
@ -446,9 +446,47 @@ class TestReportingEachDatabase:
|
||||||
"database 'beta' could not be opened: OSError"
|
"database 'beta' could not be opened: OSError"
|
||||||
)
|
)
|
||||||
modal.client = client
|
modal.client = client
|
||||||
modal.db_path = None
|
|
||||||
|
|
||||||
lines = await modal._report("beta")
|
lines = await modal._report("beta")
|
||||||
|
|
||||||
assert lines[0] == "[bold]beta[/bold]"
|
assert lines[0] == "[bold]beta[/bold]"
|
||||||
assert "could not be opened" in lines[1]
|
assert "could not be opened" in lines[1]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_a_failure_with_bracketed_text_renders_literally(self):
|
||||||
|
"""Error text and database names render verbatim, not as Rich markup."""
|
||||||
|
from rich.text import Text
|
||||||
|
|
||||||
|
from haiku.rag.inspector.widgets.info_modal import InfoModal
|
||||||
|
from haiku.rag.store.exceptions import SourceUnavailableError
|
||||||
|
|
||||||
|
modal = InfoModal.__new__(InfoModal)
|
||||||
|
client = AsyncMock()
|
||||||
|
message = "database 'beta [prod]' could not be opened: [/red] [Errno 2]"
|
||||||
|
client.clients_for.side_effect = SourceUnavailableError(message)
|
||||||
|
modal.client = client
|
||||||
|
|
||||||
|
lines = await modal._report("beta [prod]")
|
||||||
|
|
||||||
|
assert Text.from_markup(lines[0]).plain == "beta [prod]"
|
||||||
|
assert message in Text.from_markup(lines[1]).plain
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_an_open_failure_with_bracketed_text_renders_literally(
|
||||||
|
self, tmp_path
|
||||||
|
):
|
||||||
|
"""A stats-read failure renders its message verbatim, not as markup."""
|
||||||
|
from rich.text import Text
|
||||||
|
|
||||||
|
from haiku.rag.inspector.widgets.info_modal import database_lines
|
||||||
|
from haiku.rag.store.engine import ConnectionMode
|
||||||
|
|
||||||
|
client = MagicMock()
|
||||||
|
client.store.db_path = tmp_path
|
||||||
|
client.store._connection_mode = ConnectionMode.LOCAL
|
||||||
|
message = "Schema error: no field named [vector, text] [/red]"
|
||||||
|
type(client.store).db = property(MagicMock(side_effect=RuntimeError(message)))
|
||||||
|
|
||||||
|
lines = await database_lines(client)
|
||||||
|
|
||||||
|
assert message in Text.from_markup(lines[0]).plain
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue