feat(cli): add Rich output formatters

Add formatters for validation results (table/json/simple) and
benchmark history display with regression report panels.
This commit is contained in:
2026-02-03 18:17:33 +00:00
parent 9853b57843
commit f713d5e8a6

View File

@@ -0,0 +1,170 @@
"""Rich output formatters for CLI display."""
import json
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from veritext.benchmark.models import BenchmarkRun, RegressionReport
console = Console()
def format_validation_table(
results: dict[str, float],
threshold: float | None = None,
) -> Table:
"""
Format validation results as a Rich table.
Args:
results: Dictionary of metric names to scores.
threshold: Optional threshold for pass/fail colouring.
Returns:
Rich Table object.
"""
table = Table(title="Validation Results", show_header=True, header_style="bold")
table.add_column("Metric", style="cyan")
table.add_column("Score", justify="right")
if threshold is not None:
table.add_column("Status", justify="center")
for metric, score in sorted(results.items()):
score_str = f"{score:.4f}"
if threshold is not None:
status = "[green]PASS[/green]" if score >= threshold else "[red]FAIL[/red]"
table.add_row(metric, score_str, status)
else:
table.add_row(metric, score_str)
return table
def format_validation_json(results: dict[str, float]) -> str:
"""
Format validation results as JSON.
Args:
results: Dictionary of metric names to scores.
Returns:
JSON string.
"""
return json.dumps(results, indent=2)
def format_validation_simple(results: dict[str, float]) -> str:
"""
Format validation results as simple text output.
Args:
results: Dictionary of metric names to scores.
Returns:
Simple text string with one metric per line.
"""
lines = [f"{metric}: {score:.4f}" for metric, score in sorted(results.items())]
return "\n".join(lines)
def format_benchmark_history(runs: list[BenchmarkRun]) -> Table:
"""
Format benchmark run history as a Rich table.
Args:
runs: List of BenchmarkRun objects (most recent first).
Returns:
Rich Table object.
"""
if not runs:
table = Table(title="Benchmark History")
table.add_column("No runs found")
return table
# Get all metric names from the runs
metric_names: set[str] = set()
for run in runs:
metric_names.update(run.metrics.keys())
sorted_metrics = sorted(metric_names)
table = Table(title="Benchmark History", show_header=True, header_style="bold")
table.add_column("Timestamp", style="cyan")
table.add_column("Samples", justify="right")
for metric in sorted_metrics:
table.add_column(metric, justify="right")
for run in runs:
timestamp = run.timestamp.strftime("%Y-%m-%d %H:%M")
samples = str(run.sample_count)
metric_values = [f"{run.metrics.get(m, 0.0):.4f}" for m in sorted_metrics]
table.add_row(timestamp, samples, *metric_values)
return table
def format_regression_report(report: RegressionReport) -> Panel:
"""
Format a regression report as a Rich panel.
Args:
report: RegressionReport object.
Returns:
Rich Panel object with formatted report.
"""
if not report.detected:
content = (
f"[green]No regression detected.[/green]\nTolerance: {report.tolerance:.2%}"
)
return Panel(content, title="Regression Check", border_style="green")
# Build regression details
lines = [
"[red]Regression detected![/red]",
f"Tolerance: {report.tolerance:.2%}",
"",
"Metric details:",
]
for metric in sorted(report.deltas.keys()):
baseline = report.baseline.get(metric, 0.0)
current = report.current.get(metric, 0.0)
delta = report.deltas[metric]
if delta < -report.tolerance:
status = "[red]REGRESSED[/red]"
else:
status = "[green]OK[/green]"
lines.append(
f" {metric}: {current:.4f} (baseline: {baseline:.4f}, "
f"delta: {delta:+.4f}) {status}"
)
return Panel("\n".join(lines), title="Regression Check", border_style="red")
def print_validation_output(
results: dict[str, float],
output_format: str = "table",
threshold: float | None = None,
) -> None:
"""
Print validation results in the specified format.
Args:
results: Dictionary of metric names to scores.
output_format: Output format ('table', 'json', or 'simple').
threshold: Optional threshold for pass/fail colouring (table only).
"""
if output_format == "json":
console.print(format_validation_json(results))
elif output_format == "simple":
console.print(format_validation_simple(results))
else:
console.print(format_validation_table(results, threshold))