Do not duplicate metadata items in inspector
This commit is contained in:
parent
47feeb32ee
commit
b221b4ac03
2 changed files with 60 additions and 16 deletions
|
|
@ -4,7 +4,7 @@ from textual.app import ComposeResult
|
||||||
from textual.containers import VerticalScroll
|
from textual.containers import VerticalScroll
|
||||||
from textual.widgets import Markdown, Static
|
from textual.widgets import Markdown, Static
|
||||||
|
|
||||||
from haiku.rag.store.models import Chunk, Document, SearchResult
|
from haiku.rag.store.models import Chunk, ChunkMetadata, Document, SearchResult
|
||||||
|
|
||||||
|
|
||||||
class ProvenanceData(Protocol):
|
class ProvenanceData(Protocol):
|
||||||
|
|
@ -50,6 +50,21 @@ class DetailView(VerticalScroll):
|
||||||
parts.append(f"**DocItem Refs:** `{refs_str}`")
|
parts.append(f"**DocItem Refs:** `{refs_str}`")
|
||||||
return parts
|
return parts
|
||||||
|
|
||||||
|
def _format_extra_metadata(self, metadata: dict) -> list[str]:
|
||||||
|
"""Format raw metadata keys not already shown by `_format_provenance`.
|
||||||
|
|
||||||
|
`ChunkMetadata`'s own fields (page_numbers, headings, labels,
|
||||||
|
doc_item_refs) are excluded here since `_format_provenance` already
|
||||||
|
renders them, with its own truncation for long ref lists.
|
||||||
|
"""
|
||||||
|
extra = {
|
||||||
|
k: v for k, v in metadata.items() if k not in ChunkMetadata.model_fields
|
||||||
|
}
|
||||||
|
if not extra:
|
||||||
|
return []
|
||||||
|
metadata_str = "\n".join(f" - {k}: {v}" for k, v in extra.items())
|
||||||
|
return [f"**Metadata:**\n{metadata_str}"]
|
||||||
|
|
||||||
async def show_document(self, document: Document) -> None:
|
async def show_document(self, document: Document) -> None:
|
||||||
"""Display document details."""
|
"""Display document details."""
|
||||||
title = document.title or document.uri or "Untitled Document"
|
title = document.title or document.uri or "Untitled Document"
|
||||||
|
|
@ -92,10 +107,7 @@ class DetailView(VerticalScroll):
|
||||||
|
|
||||||
chunk_meta = chunk.get_chunk_metadata()
|
chunk_meta = chunk.get_chunk_metadata()
|
||||||
content_parts.extend(self._format_provenance(chunk_meta))
|
content_parts.extend(self._format_provenance(chunk_meta))
|
||||||
|
content_parts.extend(self._format_extra_metadata(chunk.metadata))
|
||||||
if chunk.metadata:
|
|
||||||
metadata_str = "\n".join(f" - {k}: {v}" for k, v in chunk.metadata.items())
|
|
||||||
content_parts.append(f"**Metadata:**\n{metadata_str}")
|
|
||||||
|
|
||||||
if chunk.embedding:
|
if chunk.embedding:
|
||||||
content_parts.append(f"**Embedding:** {len(chunk.embedding)} dimensions")
|
content_parts.append(f"**Embedding:** {len(chunk.embedding)} dimensions")
|
||||||
|
|
@ -124,12 +136,7 @@ class DetailView(VerticalScroll):
|
||||||
content_parts.append(f"**Score:** {search_result.score:.4f}")
|
content_parts.append(f"**Score:** {search_result.score:.4f}")
|
||||||
|
|
||||||
content_parts.extend(self._format_provenance(search_result))
|
content_parts.extend(self._format_provenance(search_result))
|
||||||
|
content_parts.extend(self._format_extra_metadata(search_result.chunk_meta))
|
||||||
if search_result.chunk_meta:
|
|
||||||
metadata_str = "\n".join(
|
|
||||||
f" - {k}: {v}" for k, v in search_result.chunk_meta.items()
|
|
||||||
)
|
|
||||||
content_parts.append(f"**Metadata:**\n{metadata_str}")
|
|
||||||
|
|
||||||
if chunk.embedding:
|
if chunk.embedding:
|
||||||
content_parts.append(f"**Embedding:** {len(chunk.embedding)} dimensions")
|
content_parts.append(f"**Embedding:** {len(chunk.embedding)} dimensions")
|
||||||
|
|
|
||||||
|
|
@ -167,8 +167,9 @@ async def test_document_list_tracks_has_more():
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_detail_view_shows_chunk_metadata():
|
async def test_detail_view_shows_chunk_metadata():
|
||||||
"""show_chunk renders the raw chunk.metadata dict verbatim, not just the
|
"""show_chunk renders metadata keys _format_provenance doesn't already
|
||||||
typed provenance fields _format_provenance already covers."""
|
cover, but not a duplicate of the standard fields it does (headings,
|
||||||
|
page_numbers, labels, doc_item_refs)."""
|
||||||
from textual.app import App
|
from textual.app import App
|
||||||
|
|
||||||
from haiku.rag.inspector.widgets.detail_view import DetailView
|
from haiku.rag.inspector.widgets.detail_view import DetailView
|
||||||
|
|
@ -191,12 +192,40 @@ async def test_detail_view_shows_chunk_metadata():
|
||||||
source = detail_view.content_widget.source
|
source = detail_view.content_widget.source
|
||||||
assert "**Metadata:**" in source
|
assert "**Metadata:**" in source
|
||||||
assert "para_no: 12" in source
|
assert "para_no: 12" in source
|
||||||
|
assert "headings:" not in source # already shown as **Section:**
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_detail_view_omits_metadata_block_when_only_standard_fields():
|
||||||
|
"""No **Metadata:** block at all when chunk.metadata holds nothing
|
||||||
|
beyond what _format_provenance already renders."""
|
||||||
|
from textual.app import App
|
||||||
|
|
||||||
|
from haiku.rag.inspector.widgets.detail_view import DetailView
|
||||||
|
|
||||||
|
chunk = Chunk(
|
||||||
|
id="chunk-1",
|
||||||
|
document_id="doc-1",
|
||||||
|
content="raw chunk text",
|
||||||
|
metadata={"headings": ["Chapter 1"], "page_numbers": [1]},
|
||||||
|
)
|
||||||
|
|
||||||
|
class TestApp(App):
|
||||||
|
def compose(self):
|
||||||
|
yield DetailView(id="detail")
|
||||||
|
|
||||||
|
app = TestApp()
|
||||||
|
async with app.run_test():
|
||||||
|
detail_view = app.query_one(DetailView)
|
||||||
|
await detail_view.show_chunk(chunk)
|
||||||
|
source = detail_view.content_widget.source
|
||||||
|
assert "**Metadata:**" not in source
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_detail_view_shows_search_result_chunk_meta():
|
async def test_detail_view_shows_search_result_chunk_meta():
|
||||||
"""show_search_result renders SearchResult.chunk_meta, the anchor
|
"""show_search_result renders SearchResult.chunk_meta's non-standard
|
||||||
chunk's raw metadata carried through search/expansion."""
|
keys, the anchor chunk's own metadata carried through search/expansion."""
|
||||||
from textual.app import App
|
from textual.app import App
|
||||||
|
|
||||||
from haiku.rag.inspector.widgets.detail_view import DetailView
|
from haiku.rag.inspector.widgets.detail_view import DetailView
|
||||||
|
|
@ -206,7 +235,11 @@ async def test_detail_view_shows_search_result_chunk_meta():
|
||||||
content="raw chunk text",
|
content="raw chunk text",
|
||||||
score=0.5,
|
score=0.5,
|
||||||
chunk_id="chunk-1",
|
chunk_id="chunk-1",
|
||||||
chunk_meta={"para_no": "12"},
|
doc_item_refs=[f"#/texts/{i}" for i in range(7)],
|
||||||
|
chunk_meta={
|
||||||
|
"para_no": "12",
|
||||||
|
"doc_item_refs": [f"#/texts/{i}" for i in range(7)],
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
class TestApp(App):
|
class TestApp(App):
|
||||||
|
|
@ -220,6 +253,10 @@ async def test_detail_view_shows_search_result_chunk_meta():
|
||||||
source = detail_view.content_widget.source
|
source = detail_view.content_widget.source
|
||||||
assert "**Metadata:**" in source
|
assert "**Metadata:**" in source
|
||||||
assert "para_no: 12" in source
|
assert "para_no: 12" in source
|
||||||
|
# _format_provenance's own 5-item truncation for doc_item_refs is
|
||||||
|
# untouched by the filtered-out duplicate in chunk_meta.
|
||||||
|
assert "+2 more" in source
|
||||||
|
assert "doc_item_refs:" not in source
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue