Add --no-expand to the visualize command for chunk-only grounding

This commit is contained in:
Yiorgis Gozadinos 2026-07-09 12:49:42 +03:00
parent 995d081aa1
commit 18c0f6c5e8
No known key found for this signature in database
7 changed files with 79 additions and 8 deletions

View file

@ -6,6 +6,7 @@
- `SearchResult.chunk_ids` and `Citation.chunk_ids` carry the chunk ids merged into an expanded result. - `SearchResult.chunk_ids` and `Citation.chunk_ids` carry the chunk ids merged into an expanded result.
- `Citation.doc_item_refs` carries the items the model saw; `visualize_chunk` accepts a `refs` argument and resolves bounding boxes from them, so visualizations match the cited content instead of re-expanding. - `Citation.doc_item_refs` carries the items the model saw; `visualize_chunk` accepts a `refs` argument and resolves bounding boxes from them, so visualizations match the cited content instead of re-expanding.
- Chunk visualizations draw matched content in a stronger highlight than surrounding context. - Chunk visualizations draw matched content in a stronger highlight than surrounding context.
- `haiku-rag visualize --no-expand` highlights only the chunk itself, without its expanded context (`visualize_chunk` gains an `expand` argument).
### Changed ### Changed

View file

@ -248,6 +248,8 @@ haiku-rag visualize <chunk_id>
This renders the source document pages with the chunk's location highlighted. The chunk itself draws in a strong highlight, while surrounding context swept in by expansion draws fainter. Useful for verifying chunk boundaries and understanding document structure. This renders the source document pages with the chunk's location highlighted. The chunk itself draws in a strong highlight, while surrounding context swept in by expansion draws fainter. Useful for verifying chunk boundaries and understanding document structure.
Pass `--no-expand` to highlight only the chunk itself, without its expanded context.
!!! note !!! note
Requires a terminal with image support (iTerm2, Kitty, WezTerm, etc.) and documents processed with docling that have page images stored. Requires a terminal with image support (iTerm2, Kitty, WezTerm, etc.) and documents processed with docling that have page images stored.

View file

@ -457,7 +457,7 @@ class HaikuRAGApp: # pragma: no cover
for result in results: for result in results:
self._rich_print_search_result(result) self._rich_print_search_result(result)
async def visualize_chunk(self, chunk_id: str): async def visualize_chunk(self, chunk_id: str, expand: bool = True):
"""Display visual grounding images for a chunk.""" """Display visual grounding images for a chunk."""
from textual_image.renderable import Image as RichImage from textual_image.renderable import Image as RichImage
@ -472,7 +472,7 @@ class HaikuRAGApp: # pragma: no cover
self.console.print(f"[red]Chunk with id {chunk_id} not found.[/red]") self.console.print(f"[red]Chunk with id {chunk_id} not found.[/red]")
return return
images = await self.client.visualize_chunk(chunk) images = await self.client.visualize_chunk(chunk, expand=expand)
if not images: if not images:
self.console.print( self.console.print(
"[yellow]No visual grounding available for this chunk.[/yellow]" "[yellow]No visual grounding available for this chunk.[/yellow]"

View file

@ -346,9 +346,14 @@ def visualize( # pragma: no cover
"--db", "--db",
help="Path to the LanceDB database file", help="Path to the LanceDB database file",
), ),
no_expand: bool = typer.Option(
False,
"--no-expand",
help="Highlight only the chunk itself, without its expanded context",
),
): ):
app = create_app(db) app = create_app(db)
asyncio.run(app.visualize_chunk(chunk_id=chunk_id)) asyncio.run(app.visualize_chunk(chunk_id=chunk_id, expand=not no_expand))
@_cli.command("ask", help="Ask a question using the QA agent") @_cli.command("ask", help="Ask a question using the QA agent")

View file

@ -505,11 +505,14 @@ class HaikuRAG:
return await analyze(self, question, filter) return await analyze(self, question, filter)
async def visualize_chunk( async def visualize_chunk(
self, chunk: Chunk | Sequence[Chunk], refs: list[str] | None = None self,
chunk: Chunk | Sequence[Chunk],
refs: list[str] | None = None,
expand: bool = True,
) -> list: ) -> list:
from haiku.rag.client.search import visualize_chunk from haiku.rag.client.search import visualize_chunk
return await visualize_chunk(self, chunk, refs) return await visualize_chunk(self, chunk, refs, expand)
async def rebuild_database( async def rebuild_database(
self, mode: RebuildMode = RebuildMode.FULL self, mode: RebuildMode = RebuildMode.FULL

View file

@ -228,14 +228,16 @@ async def visualize_chunk(
client: "HaikuRAG", client: "HaikuRAG",
chunk: "Chunk | Sequence[Chunk]", chunk: "Chunk | Sequence[Chunk]",
refs: list[str] | None = None, refs: list[str] | None = None,
expand: bool = True,
) -> list: ) -> list:
"""Render page images with bounding box highlights for one or more chunks. """Render page images with bounding box highlights for one or more chunks.
When ``refs`` is given (the ``doc_item_refs`` of the citation, i.e. the When ``refs`` is given (the ``doc_item_refs`` of the citation, i.e. the
exact items the model saw), bounding boxes are resolved from them directly exact items the model saw), bounding boxes are resolved from them directly
so the visualization matches the cited context precisely. When ``refs`` is so the visualization matches the cited context precisely. Otherwise, with
``None`` (e.g. the CLI or inspector, where there is no stored context), the ``expand=True`` (default) the chunks' context is re-expanded to recover the
chunks' context is re-expanded to recover the surrounding section. surrounding section; with ``expand=False`` only the chunks' own items are
drawn, so the visualization shows just the retrieved chunk with no context.
The chunks' own items draw in a strong highlight; the remaining items draw The chunks' own items draw in a strong highlight; the remaining items draw
fainter, so the matched content stands out from its surrounding context. fainter, so the matched content stands out from its surrounding context.
@ -270,6 +272,9 @@ async def visualize_chunk(
if refs is not None: if refs is not None:
all_refs = list(refs) all_refs = list(refs)
elif not expand:
# Chunk-only: draw just the retrieved chunks' own items, no context.
all_refs = list(matched_refs)
else: else:
# No stored context: re-expand the chunks to recover their section. # No stored context: re-expand the chunks to recover their section.
search_results = [ search_results = [

View file

@ -1798,6 +1798,61 @@ async def test_client_visualize_chunk_uses_given_refs(temp_db_path):
assert len(await client.visualize_chunk(chunk, refs=["#/texts/0"])) == 1 assert len(await client.visualize_chunk(chunk, refs=["#/texts/0"])) == 1
async def test_client_visualize_chunk_no_expand_shows_only_chunk(temp_db_path):
"""expand=False draws only the chunk's own items, not the expanded section."""
from docling_core.types.doc.base import BoundingBox, Size
from docling_core.types.doc.document import (
DoclingDocument,
ImageRef,
ProvenanceItem,
)
from docling_core.types.doc.labels import DocItemLabel
from PIL import Image as PilImageModule
docling_doc = DoclingDocument(name="no-expand-test")
page_size = Size(width=612.0, height=792.0)
for page_no in (1, 2):
docling_doc.add_page(
page_no=page_no,
size=page_size,
image=ImageRef.from_pil(
PilImageModule.new("RGB", (612, 792), color="white"), dpi=72
),
)
for page_no in (1, 2):
docling_doc.add_text(
label=DocItemLabel.PARAGRAPH,
text=f"Short paragraph on page {page_no}.",
prov=ProvenanceItem(
page_no=page_no,
bbox=BoundingBox(l=50, t=700, r=550, b=650),
charspan=(0, 20),
),
)
chunks = [
Chunk(
content="Short paragraph on page 1.",
metadata={
"doc_item_refs": ["#/texts/0"],
"page_numbers": [1],
"labels": ["paragraph"],
},
order=0,
embedding=[0.1] * 2560,
)
]
async with HaikuRAG(temp_db_path, create=True) as client:
doc = await client.import_document(docling_doc, chunks, uri="test://no-expand")
chunk = (await client.chunk_repository.get_by_document_id(doc.id))[0]
# Default expands the chunk's context outward → reaches page two.
assert len(await client.visualize_chunk(chunk)) == 2
# expand=False draws only the chunk's own page-one item.
assert len(await client.visualize_chunk(chunk, expand=False)) == 1
# ============================================================================= # =============================================================================
# convert() method tests # convert() method tests
# ============================================================================= # =============================================================================