Use rich rendering for citations in cli

This commit is contained in:
Yiorgis Gozadinos 2025-12-12 09:03:07 +02:00
parent 457354d45b
commit 011f527579
No known key found for this signature in database
3 changed files with 79 additions and 18 deletions

View file

@ -11,6 +11,9 @@
- `EvaluationResult`: `confidence``confidence_score`, `should_continue``is_sufficient`, `gaps_identified``gaps`, `follow_up_questions``new_questions`, added `key_insights`
- `ResearchReport`: `question``title`, `summary``executive_summary`, `findings``main_findings`, removed `insights_used`/`methodology`, added `limitations`/`recommendations`/`sources_summary`
- Updated Final Report UI to display new fields (Recommendations, Limitations, Sources)
- **Citation Formatting**: Citations in CLI now render properly with Rich panels
- Content is rendered as markdown with proper code block formatting
- No longer truncates or flattens newlines in citation content
## [0.20.1] - 2025-12-11

View file

@ -29,7 +29,7 @@ from haiku.rag.store.models.document import Document
if TYPE_CHECKING:
from haiku.rag.store.models import SearchResult
from haiku.rag.utils import format_bytes, format_citations
from haiku.rag.utils import format_bytes, format_citations_rich
logger = logging.getLogger(__name__)
@ -370,7 +370,8 @@ class HaikuRAGApp:
self.console.print("[bold green]Answer:[/bold green]")
self.console.print(Markdown(answer))
if cite and citations:
self.console.print(Markdown(format_citations(citations)))
for renderable in format_citations_rich(citations):
self.console.print(renderable)
except Exception as e:
self.console.print(f"[red]Error: {e}[/red]")

View file

@ -6,6 +6,8 @@ from typing import TYPE_CHECKING, Any
from packaging.version import Version, parse
if TYPE_CHECKING:
from rich.console import RenderableType
from haiku.rag.graph.common.models import Citation
@ -272,18 +274,20 @@ def format_bytes(num_bytes: int) -> str:
def format_citations(citations: "list[Citation]") -> str:
"""Format citations as markdown string."""
"""Format citations as plain text with preserved formatting.
Used by things like the MCP server where Rich renderables are not available.
"""
if not citations:
return ""
lines = ["## Citations\n"]
for c in citations:
# Build citation header with document_id and chunk_id
parts = [
f"- document_id: `{c.document_id}` chunk_id: `{c.chunk_id}` "
f"uri: **{c.document_uri}**"
]
if c.document_title:
parts.append(f' - "{c.document_title}"')
# Header line
header = f"[{c.document_id}:{c.chunk_id}]"
# Location info
location_parts = []
if c.page_numbers:
if len(c.page_numbers) == 1:
@ -292,16 +296,71 @@ def format_citations(citations: "list[Citation]") -> str:
location_parts.append(f"pp. {c.page_numbers[0]}-{c.page_numbers[-1]}")
if c.headings:
location_parts.append(f"Section: {c.headings[-1]}")
source = c.document_uri
if c.document_title:
source = f"{c.document_title} ({c.document_uri})"
if location_parts:
parts.append(f" ({', '.join(location_parts)})")
lines.append("".join(parts))
# Add truncated content excerpt
excerpt = c.content[:500] + "" if len(c.content) > 500 else c.content
excerpt = excerpt.replace("\r\n", " ").replace("\n", " ").replace("\r", " ")
lines.append(f"\n {excerpt}\n")
source += f" - {', '.join(location_parts)}"
lines.append(f"{header} {source}")
lines.append(c.content)
lines.append("")
return "\n".join(lines)
def format_citations_rich(citations: "list[Citation]") -> "list[RenderableType]":
"""Format citations as Rich renderables.
Returns a list of Rich Panel objects for direct console printing,
with content rendered as markdown for syntax highlighting.
"""
from rich.markdown import Markdown
from rich.panel import Panel
from rich.text import Text
if not citations:
return []
renderables: list[RenderableType] = []
renderables.append(Text("Citations", style="bold"))
for c in citations:
# Build header with IDs
header = Text()
header.append("doc: ", style="dim")
header.append(c.document_id, style="cyan")
header.append(" chunk: ", style="dim")
header.append(c.chunk_id, style="cyan")
# Location info for subtitle
location_parts = []
if c.page_numbers:
if len(c.page_numbers) == 1:
location_parts.append(f"p. {c.page_numbers[0]}")
else:
location_parts.append(f"pp. {c.page_numbers[0]}-{c.page_numbers[-1]}")
if c.headings:
location_parts.append(f"Section: {c.headings[-1]}")
subtitle = c.document_uri
if c.document_title:
subtitle = f"{c.document_title} ({c.document_uri})"
if location_parts:
subtitle += f" - {', '.join(location_parts)}"
panel = Panel(
Markdown(c.content),
title=header,
subtitle=subtitle,
subtitle_align="left",
border_style="dim",
)
renderables.append(panel)
return renderables
def get_default_data_dir() -> Path:
"""Get the user data directory for the current system platform.
@ -345,5 +404,3 @@ async def is_up_to_date() -> tuple[bool, Version, Version]:
# If no network connection, do not raise alarms.
pypi_version = running_version
return running_version >= pypi_version, running_version, pypi_version