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` - `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` - `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) - 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 ## [0.20.1] - 2025-12-11

View file

@ -29,7 +29,7 @@ from haiku.rag.store.models.document import Document
if TYPE_CHECKING: if TYPE_CHECKING:
from haiku.rag.store.models import SearchResult 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__) logger = logging.getLogger(__name__)
@ -370,7 +370,8 @@ class HaikuRAGApp:
self.console.print("[bold green]Answer:[/bold green]") self.console.print("[bold green]Answer:[/bold green]")
self.console.print(Markdown(answer)) self.console.print(Markdown(answer))
if cite and citations: 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: except Exception as e:
self.console.print(f"[red]Error: {e}[/red]") 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 from packaging.version import Version, parse
if TYPE_CHECKING: if TYPE_CHECKING:
from rich.console import RenderableType
from haiku.rag.graph.common.models import Citation 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: 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: if not citations:
return "" return ""
lines = ["## Citations\n"] lines = ["## Citations\n"]
for c in citations: for c in citations:
# Build citation header with document_id and chunk_id # Header line
parts = [ header = f"[{c.document_id}:{c.chunk_id}]"
f"- document_id: `{c.document_id}` chunk_id: `{c.chunk_id}` "
f"uri: **{c.document_uri}**" # Location info
]
if c.document_title:
parts.append(f' - "{c.document_title}"')
location_parts = [] location_parts = []
if c.page_numbers: if c.page_numbers:
if len(c.page_numbers) == 1: 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]}") location_parts.append(f"pp. {c.page_numbers[0]}-{c.page_numbers[-1]}")
if c.headings: if c.headings:
location_parts.append(f"Section: {c.headings[-1]}") 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: if location_parts:
parts.append(f" ({', '.join(location_parts)})") source += f" - {', '.join(location_parts)}"
lines.append("".join(parts))
# Add truncated content excerpt lines.append(f"{header} {source}")
excerpt = c.content[:500] + "" if len(c.content) > 500 else c.content lines.append(c.content)
excerpt = excerpt.replace("\r\n", " ").replace("\n", " ").replace("\r", " ") lines.append("")
lines.append(f"\n {excerpt}\n")
return "\n".join(lines) 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: def get_default_data_dir() -> Path:
"""Get the user data directory for the current system platform. """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. # If no network connection, do not raise alarms.
pypi_version = running_version pypi_version = running_version
return running_version >= pypi_version, running_version, pypi_version return running_version >= pypi_version, running_version, pypi_version