From c765cea93c9c45b6cfa14318710c5a7e98574536 Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Sat, 10 May 2025 10:12:17 +0000 Subject: [PATCH] rich output formatting Add formatters for validation results (table/json/simple) and benchmark history display with regression report panels. --- src/veritext/cli/formatters.py | 150 +++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 src/veritext/cli/formatters.py diff --git a/src/veritext/cli/formatters.py b/src/veritext/cli/formatters.py new file mode 100644 index 0000000..4453099 --- /dev/null +++ b/src/veritext/cli/formatters.py @@ -0,0 +1,150 @@ +"""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: + return json.dumps(results, indent=2) + + +def format_validation_simple(results: dict[str, float]) -> str: + 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 + + 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") + + 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))