From 64b10968652c1e984086c9e779c7ca1ec6ca8f99 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Thu, 27 Aug 2026 16:01:30 +0300 Subject: [PATCH] Mark a picture no database claims `format_citations_rich` asked a client covering a set for a picture whose citation named no database, which raises, losing the whole answer to one figure. Citations recorded before databases could be named carry no source, so the figure marker the caller already renders stands in. --- haiku_rag_slim/haiku/rag/utils.py | 8 +++++++- tests/test_utils.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/haiku_rag_slim/haiku/rag/utils.py b/haiku_rag_slim/haiku/rag/utils.py index f6548d62..50f859bb 100644 --- a/haiku_rag_slim/haiku/rag/utils.py +++ b/haiku_rag_slim/haiku/rag/utils.py @@ -447,9 +447,15 @@ async def format_citations_rich( async def _render_picture( client: "HaikuRAG | None", document_id: str, ref: str, source: str | None = None ) -> "RenderableType | None": - """Fetch a picture and return a Rich renderable, or None on failure/no client.""" + """A picture as a Rich renderable, or None where it cannot be rendered. + + None where no client, no source to place it across databases, or bytes that + do not decode. The caller renders its figure marker instead. + """ if client is None: return None + if source is None and client.covers_multiple: + return None from io import BytesIO from PIL import Image as PILImage diff --git a/tests/test_utils.py b/tests/test_utils.py index eae37462..cd08144b 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,4 +1,5 @@ import importlib.util +from unittest.mock import AsyncMock import pytest from pydantic_ai.models.openai import OpenAIChatModel @@ -793,6 +794,34 @@ async def test_format_citations_rich_names_the_database_when_federating(): assert "papers" in output +async def test_an_unattributable_picture_renders_its_marker(tmp_path): + """Evidence recorded before databases could be named carries no source, so + across databases nothing says which holds the picture. One unrenderable + figure must not cost the answer.""" + from rich.console import Console + + from haiku.rag.store.models.citation import Citation + from haiku.rag.utils import format_citations_rich + + covering = AsyncMock() + covering.covers_multiple = True + citation = Citation( + document_id="d1", + chunk_id="c1", + content="body", + document_uri="test://doc", + picture_refs=["#/pictures/0"], + ) + + renderables = await format_citations_rich([citation], covering) + + console = Console(record=True, width=200) + for renderable in renderables: + console.print(renderable) + assert "[Figure: #/pictures/0]" in console.export_text() + covering.get_picture_bytes.assert_not_awaited() + + def test_truncated_marks_what_it_dropped(): """An unmarked cut reads as the value: a sentence ending "in 1991" becomes one ending "in 1".""" @@ -999,6 +1028,7 @@ async def test_render_picture_handles_stored_bytes(stored, renders): stored = buf.getvalue() client = AsyncMock() + client.covers_multiple = False client.get_picture_bytes = AsyncMock(return_value=stored) result = await _render_picture(client, "doc1", "#/pictures/0")