5a558eb09e
TypeScript SDK Compatibility V1.x E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / TypeScript SDK Compatibility V1.x E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
TypeScript SDK E2E Tests / TypeScript SDK E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
Python SDK E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK E2E Tests / Python SDK E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK E2E Tests / build-opik (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Python SDK Compatibility V1.x E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK E2E Tests / build-opik (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer E2E Tests Python ${{matrix.python_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer Integration Smoke Tests (push) Has been cancelled
🐙 Code Quality / detect (push) Has been cancelled
🐙 Code Quality / lint (${{ matrix.leg.name }}) (push) Has been cancelled
🐙 Code Quality / summary (push) Has been cancelled
TypeScript SDK Library Integration Tests / Check Secrets (push) Has been cancelled
TypeScript SDK Library Integration Tests / opik-vercel (Vercel AI SDK / eve) (push) Has been cancelled
SDK Library Integration Tests Runner / Check Secrets (push) Has been cancelled
SDK Library Integration Tests Runner / Missed OpenAI API Key Warning (push) Has been cancelled
SDK Library Integration Tests Runner / Build (push) Has been cancelled
SDK Library Integration Tests Runner / openai_tests (push) Has been cancelled
SDK Library Integration Tests Runner / langchain_tests (push) Has been cancelled
SDK Library Integration Tests Runner / langchain_legacy_tests (push) Has been cancelled
SDK Library Integration Tests Runner / llama_index_tests (push) Has been cancelled
SDK Library Integration Tests Runner / anthropic_tests (push) Has been cancelled
SDK Library Integration Tests Runner / mistral_tests (push) Has been cancelled
SDK Library Integration Tests Runner / groq_tests (push) Has been cancelled
SDK Library Integration Tests Runner / aisuite_tests (push) Has been cancelled
SDK Library Integration Tests Runner / haystack_tests (push) Has been cancelled
SDK Library Integration Tests Runner / dspy_tests (push) Has been cancelled
SDK Library Integration Tests Runner / crewai_v0_tests (push) Has been cancelled
SDK Library Integration Tests Runner / crewai_v1_tests (push) Has been cancelled
SDK Library Integration Tests Runner / genai_tests (push) Has been cancelled
SDK Library Integration Tests Runner / adk_tests (push) Has been cancelled
SDK Library Integration Tests Runner / adk_legacy_1_3_0_tests (push) Has been cancelled
SDK Library Integration Tests Runner / evaluation_metrics_tests (push) Has been cancelled
SDK Library Integration Tests Runner / bedrock_tests (push) Has been cancelled
SDK Library Integration Tests Runner / litellm_tests (push) Has been cancelled
SDK Library Integration Tests Runner / harbor_tests (push) Has been cancelled
SDK Library Integration Tests Runner / Slack Notification (push) Has been cancelled
Lint Opik Helm Chart / render-equality (push) Has been cancelled
Opik Optimizer - Unit Tests / Opik Optimizer Unit Tests Python ${{matrix.python_version}} (push) Has been cancelled
Python BE E2E Tests / Python BE E2E (push) Has been cancelled
Python Backend Tests / run-python-backend-tests (push) Has been cancelled
Python SDK Unit Tests / Python SDK Unit Tests ${{matrix.python_version}} (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
SDK E2E Libraries Integration Tests / Check Secrets (push) Has been cancelled
SDK E2E Libraries Integration Tests / Missed OpenAI API Key Warning (push) Has been cancelled
SDK E2E Libraries Integration Tests / build-opik (push) Has been cancelled
SDK E2E Libraries Integration Tests / E2E Lib Integration Python ${{matrix.python_version}} (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-gemini) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-langchain) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-openai) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-otel) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-vercel) (push) Has been cancelled
TypeScript SDK Build & Publish / build-and-publish (push) Has been cancelled
TypeScript SDK Unit Tests / Test on Node ${{ matrix.node-version }} (push) Has been cancelled
Backend Tests / discover-tests (push) Has been cancelled
Backend Tests / ${{ matrix.name }} (push) Has been cancelled
Build and Publish SDK / build-and-publish (push) Has been cancelled
Build Opik Docker Images / set-version (push) Has been cancelled
Build Opik Docker Images / build-backend (push) Has been cancelled
Build Opik Docker Images / build-sandbox-executor-python (push) Has been cancelled
Build Opik Docker Images / build-python-backend (push) Has been cancelled
Build Opik Docker Images / build-frontend (push) Has been cancelled
Build Opik Docker Images / create-git-tag (push) Has been cancelled
ClickHouse Migration Cluster Check / validate-clickhouse-migrations (push) Has been cancelled
Docs - Publish / run (push) Has been cancelled
E2E Tests - Post Merge (v2) / 🧪 E2E v2 Tests (${{ github.event.inputs.tier || 't1' }}) (push) Has been cancelled
E2E Tests - Post Merge (v2) / 📢 Slack Notification (push) Has been cancelled
Frontend Unit Tests / Test on Node 20 (push) Has been cancelled
Guardrails E2E Tests / Select Python version matrix (push) Has been cancelled
Guardrails E2E Tests / Guardrails E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Guardrails E2E Tests / 📢 Slack Notification (push) Has been cancelled
Guardrails Backend Unit Tests / Guardrails Backend Unit Tests (push) Has been cancelled
Guardrails Backend Unit Tests / 📢 Slack Notification (push) Has been cancelled
Lint Opik Helm Chart / lint-helm-chart (Helm v3.21.0) (push) Has been cancelled
Lint Opik Helm Chart / lint-helm-chart (Helm v4.2.0) (push) Has been cancelled
Lint Opik Helm Chart / unittest-helm-chart (push) Has been cancelled
390 lines
14 KiB
Python
390 lines
14 KiB
Python
from __future__ import annotations
|
|
|
|
import functools
|
|
import hashlib
|
|
import logging
|
|
import os
|
|
import re
|
|
import sys
|
|
from datetime import datetime
|
|
from typing import TYPE_CHECKING, Any
|
|
from collections.abc import Callable
|
|
|
|
from rich import box
|
|
from rich.console import Group
|
|
from rich.live import Live
|
|
from rich.padding import Padding
|
|
from rich.panel import Panel
|
|
from rich.progress import (
|
|
BarColumn,
|
|
Progress,
|
|
SpinnerColumn,
|
|
TaskProgressColumn,
|
|
TextColumn,
|
|
TimeElapsedColumn,
|
|
TimeRemainingColumn,
|
|
)
|
|
from rich.rule import Rule
|
|
from rich.table import Table
|
|
from rich.text import Text
|
|
|
|
from benchmarks.utils.sinks import BenchmarkEvent, EventSink, NullSink
|
|
from opik_optimizer.utils.reporting import get_console
|
|
|
|
if TYPE_CHECKING:
|
|
from benchmarks.core.types import TaskResult
|
|
|
|
console = get_console(width=120, soft_wrap=True)
|
|
|
|
PROGRESS_COLUMNS = (
|
|
SpinnerColumn(),
|
|
TextColumn("[progress.description]{task.description}"),
|
|
BarColumn(bar_width=40),
|
|
TaskProgressColumn(),
|
|
TextColumn("•"),
|
|
TimeElapsedColumn(),
|
|
TextColumn("•"),
|
|
TimeRemainingColumn(),
|
|
)
|
|
|
|
|
|
class BenchmarkLogger:
|
|
def __init__(self, event_sink: EventSink | None = None) -> None:
|
|
self.event_sink = event_sink or NullSink()
|
|
|
|
def setup_logger(
|
|
self,
|
|
demo_datasets: list[str],
|
|
optimizers: list[str],
|
|
models: list[str],
|
|
test_mode: bool,
|
|
run_id: str,
|
|
) -> None:
|
|
self.demo_datasets = demo_datasets
|
|
self.optimizers = optimizers
|
|
self.models = models
|
|
self.test_mode = test_mode
|
|
self.run_id = run_id
|
|
self.tasks_status: dict[Any, dict[str, Any]] = {}
|
|
self.completed_tasks_count = {"Success": 0, "Failed": 0}
|
|
self.task_results: list[TaskResult] = []
|
|
|
|
def print_benchmark_header(self) -> None:
|
|
console.print(Rule("[bold blue]Benchmark Configuration[/bold blue]"))
|
|
|
|
table = Table(box=box.ROUNDED, show_header=False, padding=(0, 1))
|
|
table.add_row("Run ID", self.run_id)
|
|
table.add_row("Datasets", ", ".join(self.demo_datasets))
|
|
table.add_row("Optimizers", ", ".join(self.optimizers))
|
|
table.add_row("Models", ", ".join(self.models))
|
|
table.add_row("Test mode", str(self.test_mode))
|
|
|
|
console.print(Panel(table, border_style="blue", padding=(1, 2)))
|
|
console.print()
|
|
|
|
total_tasks = len(self.demo_datasets) * len(self.optimizers) * len(self.models)
|
|
console.print(Rule("Phase 2: Running Optimizations", style="dim blue"))
|
|
console.print(
|
|
f"Preparing to run [bold cyan]{total_tasks}[/bold cyan] optimization tasks..."
|
|
)
|
|
|
|
self.progress = Progress(
|
|
*PROGRESS_COLUMNS, console=console, transient=False, expand=True
|
|
)
|
|
self.progress_task_id = self.progress.add_task(
|
|
"[bold blue]Overall Progress[/bold blue]", total=total_tasks
|
|
)
|
|
self.total_tasks = total_tasks
|
|
|
|
def update_active_task_status(
|
|
self,
|
|
future: Any,
|
|
dataset_name: str,
|
|
optimizer_name: str,
|
|
model_name: str,
|
|
status: str,
|
|
short_id: str | None = None,
|
|
) -> None:
|
|
self.tasks_status[future] = {
|
|
"dataset_name": dataset_name,
|
|
"optimizer_name": optimizer_name,
|
|
"model_name": model_name,
|
|
"status": status,
|
|
"short_id": short_id,
|
|
}
|
|
self.event_sink.emit(
|
|
BenchmarkEvent(
|
|
name="task_status_updated",
|
|
payload={
|
|
"dataset_name": dataset_name,
|
|
"optimizer_name": optimizer_name,
|
|
"model_name": model_name,
|
|
"status": status,
|
|
"short_id": short_id,
|
|
},
|
|
)
|
|
)
|
|
|
|
def remove_active_task_status(
|
|
self, future: Any, final_status: str | None = None
|
|
) -> None:
|
|
if future in self.tasks_status:
|
|
if final_status in ("Success", "Failed"):
|
|
self.completed_tasks_count[final_status] += 1
|
|
self.progress.advance(self.progress_task_id, 1)
|
|
self.event_sink.emit(
|
|
BenchmarkEvent(
|
|
name="task_finished", payload={"status": final_status}
|
|
)
|
|
)
|
|
del self.tasks_status[future]
|
|
|
|
def _generate_live_display_message(self) -> Group:
|
|
active_lines: list[Text] = []
|
|
for status_info in self.tasks_status.values():
|
|
if status_info.get("status") != "Running":
|
|
continue
|
|
dataset_name = status_info.get("dataset_name", "Unknown")
|
|
optimizer_name = status_info.get("optimizer_name", "?")
|
|
model_name = status_info.get("model_name", "?")
|
|
short_id = status_info.get("short_id")
|
|
if short_id:
|
|
line = Text.assemble(
|
|
" • ",
|
|
(f"#{short_id} ", "dim"),
|
|
(dataset_name, "yellow"),
|
|
(f" [{optimizer_name}]", "dim"),
|
|
(f" ({model_name})", "dim"),
|
|
)
|
|
else:
|
|
line = Text.assemble(
|
|
" • ",
|
|
(dataset_name, "yellow"),
|
|
(f" [{optimizer_name}]", "dim"),
|
|
(f" ({model_name})", "dim"),
|
|
)
|
|
active_lines.append(line)
|
|
|
|
active_tasks_content = (
|
|
Group(*active_lines)
|
|
if active_lines
|
|
else Group(Text("Waiting for tasks...", style="dim"))
|
|
)
|
|
active_panel = Panel(
|
|
active_tasks_content,
|
|
title="Active Tasks",
|
|
border_style="blue",
|
|
padding=(0, 1),
|
|
)
|
|
|
|
nb_active_tasks = len(
|
|
[x for x in self.tasks_status.values() if x["status"] == "Running"]
|
|
)
|
|
nb_success = self.completed_tasks_count["Success"]
|
|
nb_failed = self.completed_tasks_count["Failed"]
|
|
summary_line = Text(
|
|
f"Run: {self.run_id} | Tasks: {nb_success + nb_failed}/{self.total_tasks} | Success: {nb_success} | Failed: {nb_failed} | Active: {nb_active_tasks}",
|
|
style="dim",
|
|
)
|
|
|
|
return Group(self.progress, Padding(summary_line, (0, 0, 1, 0)), active_panel)
|
|
|
|
def create_live_panel(self) -> Live:
|
|
return Live(console=console, refresh_per_second=4, vertical_overflow="visible")
|
|
|
|
def add_result_panel(
|
|
self,
|
|
dataset_name: str,
|
|
optimizer_name: str,
|
|
task_detail_data: TaskResult | None = None,
|
|
) -> None:
|
|
del dataset_name
|
|
del optimizer_name
|
|
if task_detail_data is not None:
|
|
self.task_results.append(task_detail_data)
|
|
|
|
@staticmethod
|
|
def _extract_primary_metric(result: TaskResult) -> tuple[str, str]:
|
|
evals = result.evaluations or {}
|
|
initial_set = evals.get("initial")
|
|
final_set = evals.get("final")
|
|
|
|
def _metric_value(eval_set: Any, split: str) -> tuple[str, float] | None:
|
|
entry = getattr(eval_set, split, None) if eval_set else None
|
|
eval_result = getattr(entry, "result", None) if entry else None
|
|
metrics = getattr(eval_result, "metrics", None) if eval_result else None
|
|
if not metrics:
|
|
return None
|
|
first = metrics[0]
|
|
name = str(first.get("metric_name", "metric"))
|
|
score = first.get("score")
|
|
if isinstance(score, (int, float)):
|
|
return name, float(score)
|
|
return None
|
|
|
|
for split in ("validation", "train", "test"):
|
|
initial = _metric_value(initial_set, split)
|
|
final = _metric_value(final_set, split)
|
|
if initial and final:
|
|
metric_name = initial[0]
|
|
improvement = final[1] - initial[1]
|
|
return (
|
|
metric_name,
|
|
f"{initial[1]:.4f} -> {final[1]:.4f} ({improvement:+.4f})",
|
|
)
|
|
if final:
|
|
return final[0], f"{final[1]:.4f}"
|
|
return "metric", "N/A"
|
|
|
|
def _render_task_detail(self, task: TaskResult) -> None:
|
|
rich_candidate = task.optimization_summary or task.optimization_raw_result
|
|
if rich_candidate is not None and hasattr(rich_candidate, "__rich__"):
|
|
console.print(rich_candidate)
|
|
if task.error_message:
|
|
console.print(
|
|
Panel(task.error_message, title="Task Error", border_style="red")
|
|
)
|
|
return
|
|
|
|
status_style = "green" if task.status == "Success" else "red"
|
|
metric_name, metric_summary = self._extract_primary_metric(task)
|
|
table = Table(show_header=False, box=None, padding=(0, 1))
|
|
table.add_row("Task", task.id)
|
|
table.add_row("Status", f"[{status_style}]{task.status}[/{status_style}]")
|
|
table.add_row("Dataset", task.dataset_name)
|
|
table.add_row("Optimizer", task.optimizer_name)
|
|
table.add_row("Model", task.model_name)
|
|
table.add_row(metric_name, metric_summary)
|
|
if task.llm_calls_total_optimization is not None:
|
|
table.add_row("LLM calls", str(task.llm_calls_total_optimization))
|
|
if task.error_message:
|
|
table.add_row("Error", task.error_message)
|
|
|
|
console.print(
|
|
Panel(table, title=f"Task Result: {task.id}", border_style=status_style)
|
|
)
|
|
|
|
def print_benchmark_footer(
|
|
self, results: list[TaskResult], total_duration: float
|
|
) -> None:
|
|
successful_tasks = len([x for x in results if x.status == "Success"])
|
|
failed_tasks = len([x for x in results if x.status == "Failed"])
|
|
|
|
console.print(Rule("[bold blue]Benchmark Run Complete[/bold blue]"))
|
|
|
|
summary_table = Table(box=box.ROUNDED, show_header=False, padding=(0, 1))
|
|
summary_table.add_row("Total", str(successful_tasks + failed_tasks))
|
|
summary_table.add_row("Success", f"[green]{successful_tasks}[/green]")
|
|
summary_table.add_row("Failed", f"[red]{failed_tasks}[/red]")
|
|
summary_table.add_row("Duration", f"{total_duration:.2f}s")
|
|
console.print(Panel(summary_table, title="Summary", border_style="blue"))
|
|
|
|
if results:
|
|
results_table = Table(box=box.SIMPLE, show_header=True)
|
|
results_table.add_column("ID", style="dim", no_wrap=True)
|
|
results_table.add_column("Dataset")
|
|
results_table.add_column("Optimizer")
|
|
results_table.add_column("Model")
|
|
results_table.add_column("Status", no_wrap=True)
|
|
results_table.add_column("LLM Calls", justify="right")
|
|
for task in sorted(results, key=lambda x: x.id):
|
|
status_style = "green" if task.status == "Success" else "red"
|
|
short_id = hashlib.sha1(
|
|
f"{self.run_id}:{task.id}".encode()
|
|
).hexdigest()[:5]
|
|
results_table.add_row(
|
|
short_id,
|
|
task.dataset_name,
|
|
task.optimizer_name,
|
|
task.model_name,
|
|
f"[{status_style}]{task.status}[/{status_style}]",
|
|
str(task.llm_calls_total_optimization or "-"),
|
|
)
|
|
console.print(Panel(results_table, title="Tasks", border_style="blue"))
|
|
|
|
if self.task_results:
|
|
console.print(Rule("Task Details", style="dim blue"))
|
|
for task in self.task_results:
|
|
self._render_task_detail(task)
|
|
console.print()
|
|
|
|
|
|
def log_console_output_to_file() -> Callable:
|
|
"""Capture stdout/stderr to per-task logs while keeping normal terminal output."""
|
|
|
|
class TeeOutput:
|
|
def __init__(self, file: Any, original_stream: Any) -> None:
|
|
self.file = file
|
|
self.original_stream = original_stream
|
|
|
|
def write(self, data: str) -> None:
|
|
ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
|
clean_data = ansi_escape.sub("", data)
|
|
self.file.write(clean_data)
|
|
|
|
def flush(self) -> None:
|
|
self.file.flush()
|
|
self.original_stream.flush()
|
|
|
|
def decorator(func: Callable) -> Callable:
|
|
@functools.wraps(func)
|
|
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
if "checkpoint_folder" in kwargs:
|
|
checkpoint_folder = kwargs["checkpoint_folder"]
|
|
kwargs.pop("checkpoint_folder")
|
|
else:
|
|
checkpoint_folder = os.path.abspath(
|
|
os.path.join(
|
|
os.path.expanduser("~"),
|
|
".opik_optimizer",
|
|
"benchmark_results",
|
|
)
|
|
)
|
|
|
|
dataset_name = kwargs.get(
|
|
"dataset_name", args[0] if len(args) > 0 else "unknown"
|
|
)
|
|
optimizer_name = kwargs.get(
|
|
"optimizer_name", args[1] if len(args) > 1 else "unknown"
|
|
)
|
|
model_name = kwargs.get(
|
|
"model_name", args[2] if len(args) > 2 else "unknown"
|
|
)
|
|
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
sanitized_dataset_name = str(dataset_name).replace("/", "_")
|
|
sanitized_optimizer_name = str(optimizer_name).replace("/", "_")
|
|
sanitized_model_name = str(model_name).replace("/", "_")
|
|
file_path = os.path.join(
|
|
checkpoint_folder,
|
|
(
|
|
"optimization_"
|
|
f"{sanitized_dataset_name}_"
|
|
f"{sanitized_optimizer_name}_"
|
|
f"{sanitized_model_name}_"
|
|
f"{timestamp}.log"
|
|
),
|
|
)
|
|
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
|
|
|
original_stdout = sys.stdout
|
|
original_stderr = sys.stderr
|
|
opik_optimizer_logger = logging.getLogger("opik_optimizer")
|
|
original_level = opik_optimizer_logger.level
|
|
|
|
try:
|
|
opik_optimizer_logger.setLevel(logging.INFO)
|
|
with open(file_path, "w", encoding="utf-8") as f:
|
|
sys.stdout = TeeOutput(f, original_stdout) # type: ignore[assignment]
|
|
sys.stderr = TeeOutput(f, original_stderr) # type: ignore[assignment]
|
|
return func(*args, **kwargs)
|
|
finally:
|
|
opik_optimizer_logger.setLevel(original_level)
|
|
sys.stdout = original_stdout
|
|
sys.stderr = original_stderr
|
|
print(f"Console output has been saved to: {file_path}")
|
|
|
|
return wrapper
|
|
|
|
return decorator
|