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:
170
src/veritext/cli/formatters.py
Normal file
170
src/veritext/cli/formatters.py
Normal 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))
|
||||||
Reference in New Issue
Block a user