Use rich rendering for citations in cli
This commit is contained in:
parent
457354d45b
commit
011f527579
3 changed files with 79 additions and 18 deletions
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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]")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue