mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-30 10:52:15 +00:00
Evaluation framework (evals/): benchmarking system for measuring accuracy
across four categories — Chat (WildChat), Reasoning (SuperGPQA), RAG (FRAMES),
and Agentic (GAIA). Two backends: jarvis-direct (engine-level) and jarvis-agent
(agent-level with tool calling), both supporting local and cloud models.
Datasets adapted from IPW, scorers include exact match, LLM letter extraction,
and LLM-as-judge. Parallel execution via ThreadPoolExecutor with incremental
JSONL output. CLI: python -m evals {run,run-all,summarize,list}. 57 tests pass.
SVG fix: center logo content within viewBox by wrapping icon+text in a
translate(90,0) group, eliminating the left-shift visible in the README.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
302 lines
11 KiB
Python
302 lines
11 KiB
Python
"""CLI for the OpenJarvis evaluation framework."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
import click
|
|
|
|
# Registry of available benchmarks and their metadata
|
|
BENCHMARKS = {
|
|
"supergpqa": {"category": "reasoning", "description": "SuperGPQA multiple-choice"},
|
|
"gaia": {"category": "agentic", "description": "GAIA agentic benchmark"},
|
|
"frames": {"category": "rag", "description": "FRAMES multi-hop RAG"},
|
|
"wildchat": {"category": "chat", "description": "WildChat conversation quality"},
|
|
}
|
|
|
|
BACKENDS = {
|
|
"jarvis-direct": "Engine-level inference (local or cloud)",
|
|
"jarvis-agent": "Agent-level inference with tool calling",
|
|
}
|
|
|
|
|
|
def _setup_logging(verbose: bool) -> None:
|
|
level = logging.DEBUG if verbose else logging.INFO
|
|
logging.basicConfig(
|
|
level=level,
|
|
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
|
datefmt="%H:%M:%S",
|
|
)
|
|
|
|
|
|
def _build_backend(backend_name: str, engine_key: Optional[str],
|
|
agent_name: str, tools: list[str]):
|
|
"""Construct the appropriate backend."""
|
|
if backend_name == "jarvis-agent":
|
|
from evals.backends.jarvis_agent import JarvisAgentBackend
|
|
return JarvisAgentBackend(
|
|
engine_key=engine_key,
|
|
agent_name=agent_name,
|
|
tools=tools,
|
|
)
|
|
else:
|
|
from evals.backends.jarvis_direct import JarvisDirectBackend
|
|
return JarvisDirectBackend(engine_key=engine_key)
|
|
|
|
|
|
def _build_dataset(benchmark: str):
|
|
"""Construct the dataset provider for a benchmark."""
|
|
if benchmark == "supergpqa":
|
|
from evals.datasets.supergpqa import SuperGPQADataset
|
|
return SuperGPQADataset()
|
|
elif benchmark == "gaia":
|
|
from evals.datasets.gaia import GAIADataset
|
|
return GAIADataset()
|
|
elif benchmark == "frames":
|
|
from evals.datasets.frames import FRAMESDataset
|
|
return FRAMESDataset()
|
|
elif benchmark == "wildchat":
|
|
from evals.datasets.wildchat import WildChatDataset
|
|
return WildChatDataset()
|
|
else:
|
|
raise click.ClickException(f"Unknown benchmark: {benchmark}")
|
|
|
|
|
|
def _build_scorer(benchmark: str, judge_backend, judge_model: str):
|
|
"""Construct the scorer for a benchmark."""
|
|
if benchmark == "supergpqa":
|
|
from evals.scorers.supergpqa_mcq import SuperGPQAScorer
|
|
return SuperGPQAScorer(judge_backend, judge_model)
|
|
elif benchmark == "gaia":
|
|
from evals.scorers.gaia_exact import GAIAScorer
|
|
return GAIAScorer(judge_backend, judge_model)
|
|
elif benchmark == "frames":
|
|
from evals.scorers.frames_judge import FRAMESScorer
|
|
return FRAMESScorer(judge_backend, judge_model)
|
|
elif benchmark == "wildchat":
|
|
from evals.scorers.wildchat_judge import WildChatScorer
|
|
return WildChatScorer(judge_backend, judge_model)
|
|
else:
|
|
raise click.ClickException(f"Unknown benchmark: {benchmark}")
|
|
|
|
|
|
def _build_judge_backend(judge_model: str):
|
|
"""Build the judge backend (always cloud for LLM-as-judge)."""
|
|
from evals.backends.jarvis_direct import JarvisDirectBackend
|
|
return JarvisDirectBackend(engine_key="cloud")
|
|
|
|
|
|
@click.group()
|
|
def main():
|
|
"""OpenJarvis Evaluation Framework."""
|
|
|
|
|
|
@main.command()
|
|
@click.option("-b", "--benchmark", required=True,
|
|
type=click.Choice(list(BENCHMARKS.keys())),
|
|
help="Benchmark to run")
|
|
@click.option("--backend", default="jarvis-direct",
|
|
type=click.Choice(list(BACKENDS.keys())),
|
|
help="Inference backend")
|
|
@click.option("-m", "--model", required=True, help="Model identifier")
|
|
@click.option("-e", "--engine", "engine_key", default=None,
|
|
help="Engine key (ollama, vllm, cloud, ...)")
|
|
@click.option("--agent", "agent_name", default="orchestrator",
|
|
help="Agent name for jarvis-agent backend")
|
|
@click.option("--tools", default="", help="Comma-separated tool names")
|
|
@click.option("-n", "--max-samples", type=int, default=None,
|
|
help="Maximum samples to evaluate")
|
|
@click.option("-w", "--max-workers", type=int, default=4,
|
|
help="Parallel workers")
|
|
@click.option("--judge-model", default="gpt-4o",
|
|
help="LLM judge model")
|
|
@click.option("-o", "--output", "output_path", default=None,
|
|
help="Output JSONL path")
|
|
@click.option("--seed", type=int, default=42, help="Random seed")
|
|
@click.option("--split", "dataset_split", default=None,
|
|
help="Dataset split override")
|
|
@click.option("--temperature", type=float, default=0.0,
|
|
help="Generation temperature")
|
|
@click.option("--max-tokens", type=int, default=2048,
|
|
help="Max output tokens")
|
|
@click.option("-v", "--verbose", is_flag=True, help="Verbose logging")
|
|
def run(benchmark, backend, model, engine_key, agent_name, tools,
|
|
max_samples, max_workers, judge_model, output_path, seed,
|
|
dataset_split, temperature, max_tokens, verbose):
|
|
"""Run a single benchmark evaluation."""
|
|
_setup_logging(verbose)
|
|
|
|
from evals.core.runner import EvalRunner
|
|
from evals.core.types import RunConfig
|
|
|
|
tool_list = [t.strip() for t in tools.split(",") if t.strip()] if tools else []
|
|
|
|
config = RunConfig(
|
|
benchmark=benchmark,
|
|
backend=backend,
|
|
model=model,
|
|
max_samples=max_samples,
|
|
max_workers=max_workers,
|
|
temperature=temperature,
|
|
max_tokens=max_tokens,
|
|
judge_model=judge_model,
|
|
engine_key=engine_key,
|
|
agent_name=agent_name,
|
|
tools=tool_list,
|
|
output_path=output_path,
|
|
seed=seed,
|
|
dataset_split=dataset_split,
|
|
)
|
|
|
|
eval_backend = _build_backend(backend, engine_key, agent_name, tool_list)
|
|
dataset = _build_dataset(benchmark)
|
|
judge_backend = _build_judge_backend(judge_model)
|
|
scorer = _build_scorer(benchmark, judge_backend, judge_model)
|
|
|
|
runner = EvalRunner(config, dataset, eval_backend, scorer)
|
|
|
|
try:
|
|
summary = runner.run()
|
|
finally:
|
|
eval_backend.close()
|
|
judge_backend.close()
|
|
|
|
# Print summary
|
|
click.echo(f"\n{'=' * 60}")
|
|
click.echo(f"Benchmark: {summary.benchmark}")
|
|
click.echo(f"Model: {summary.model}")
|
|
click.echo(f"Backend: {summary.backend}")
|
|
click.echo(f"Samples: {summary.total_samples}")
|
|
click.echo(f"Scored: {summary.scored_samples}")
|
|
click.echo(f"Correct: {summary.correct}")
|
|
click.echo(f"Accuracy: {summary.accuracy:.4f}")
|
|
click.echo(f"Errors: {summary.errors}")
|
|
click.echo(f"Latency: {summary.mean_latency_seconds:.2f}s (mean)")
|
|
click.echo(f"Cost: ${summary.total_cost_usd:.4f}")
|
|
if summary.per_subject:
|
|
click.echo("\nPer-subject breakdown:")
|
|
for subj, stats in sorted(summary.per_subject.items()):
|
|
click.echo(f" {subj}: {stats['accuracy']:.4f} "
|
|
f"({int(stats['correct'])}/{int(stats['scored'])})")
|
|
click.echo(f"{'=' * 60}")
|
|
|
|
|
|
@main.command("run-all")
|
|
@click.option("-m", "--model", required=True, help="Model identifier")
|
|
@click.option("-e", "--engine", "engine_key", default=None,
|
|
help="Engine key")
|
|
@click.option("-n", "--max-samples", type=int, default=None,
|
|
help="Max samples per benchmark")
|
|
@click.option("-w", "--max-workers", type=int, default=4,
|
|
help="Parallel workers")
|
|
@click.option("--judge-model", default="gpt-4o", help="LLM judge model")
|
|
@click.option("--output-dir", default="results/",
|
|
help="Output directory for results")
|
|
@click.option("--seed", type=int, default=42, help="Random seed")
|
|
@click.option("-v", "--verbose", is_flag=True, help="Verbose logging")
|
|
def run_all(model, engine_key, max_samples, max_workers, judge_model,
|
|
output_dir, seed, verbose):
|
|
"""Run all benchmarks."""
|
|
_setup_logging(verbose)
|
|
|
|
from evals.core.runner import EvalRunner
|
|
from evals.core.types import RunConfig
|
|
|
|
output_dir_path = Path(output_dir)
|
|
output_dir_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
model_slug = model.replace("/", "-").replace(":", "-")
|
|
summaries = []
|
|
|
|
for bench_name in BENCHMARKS:
|
|
click.echo(f"\n--- Running {bench_name} ---")
|
|
output_path = output_dir_path / f"{bench_name}_{model_slug}.jsonl"
|
|
|
|
config = RunConfig(
|
|
benchmark=bench_name,
|
|
backend="jarvis-direct",
|
|
model=model,
|
|
max_samples=max_samples,
|
|
max_workers=max_workers,
|
|
judge_model=judge_model,
|
|
engine_key=engine_key,
|
|
output_path=str(output_path),
|
|
seed=seed,
|
|
)
|
|
|
|
eval_backend = _build_backend("jarvis-direct", engine_key, "orchestrator", [])
|
|
dataset = _build_dataset(bench_name)
|
|
judge_backend = _build_judge_backend(judge_model)
|
|
scorer = _build_scorer(bench_name, judge_backend, judge_model)
|
|
|
|
runner = EvalRunner(config, dataset, eval_backend, scorer)
|
|
try:
|
|
summary = runner.run()
|
|
summaries.append(summary)
|
|
click.echo(f" {bench_name}: {summary.accuracy:.4f} "
|
|
f"({summary.correct}/{summary.scored_samples})")
|
|
except Exception as exc:
|
|
click.echo(f" {bench_name}: FAILED — {exc}", err=True)
|
|
finally:
|
|
eval_backend.close()
|
|
judge_backend.close()
|
|
|
|
# Print overall summary
|
|
if summaries:
|
|
click.echo(f"\n{'=' * 60}")
|
|
click.echo("Overall Results:")
|
|
for s in summaries:
|
|
click.echo(f" {s.benchmark:12s} {s.accuracy:.4f} "
|
|
f"({s.correct}/{s.scored_samples})")
|
|
click.echo(f"{'=' * 60}")
|
|
|
|
|
|
@main.command()
|
|
@click.argument("jsonl_path", type=click.Path(exists=True))
|
|
def summarize(jsonl_path):
|
|
"""Summarize results from a JSONL output file."""
|
|
records = []
|
|
with open(jsonl_path) as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if line:
|
|
records.append(json.loads(line))
|
|
|
|
if not records:
|
|
click.echo("No records found.")
|
|
return
|
|
|
|
total = len(records)
|
|
scored = [r for r in records if r.get("is_correct") is not None]
|
|
correct = [r for r in scored if r["is_correct"]]
|
|
errors = [r for r in records if r.get("error")]
|
|
accuracy = len(correct) / len(scored) if scored else 0.0
|
|
|
|
click.echo(f"File: {jsonl_path}")
|
|
click.echo(f"Benchmark: {records[0].get('benchmark', '?')}")
|
|
click.echo(f"Model: {records[0].get('model', '?')}")
|
|
click.echo(f"Total: {total}")
|
|
click.echo(f"Scored: {len(scored)}")
|
|
click.echo(f"Correct: {len(correct)}")
|
|
click.echo(f"Accuracy: {accuracy:.4f}")
|
|
click.echo(f"Errors: {len(errors)}")
|
|
|
|
|
|
@main.command("list")
|
|
def list_cmd():
|
|
"""List available benchmarks and backends."""
|
|
click.echo("Benchmarks:")
|
|
for name, info in BENCHMARKS.items():
|
|
click.echo(f" {name:12s} [{info['category']:10s}] {info['description']}")
|
|
|
|
click.echo("\nBackends:")
|
|
for name, desc in BACKENDS.items():
|
|
click.echo(f" {name:16s} {desc}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|