Files
OpenJarvis/docs/api/evals.md
T
Jon Saad-FalconandClaude Opus 4.6 9f5e97eed9 Add eval framework efficiency metrics: MFU, MBU, IPW, IPJ
Extend the eval framework to compute per-sample hardware efficiency
metrics when model hardware parameters are provided in TOML config:

- ModelConfig gains param_count_b, active_params_b, gpu_peak_tflops,
  gpu_peak_bandwidth_gb_s, num_gpus fields
- RunConfig gains metadata dict, populated by expand_suite() from
  model hardware params
- EvalRunner._process_one() computes IPW (Intelligence Per Watt),
  IPJ (Intelligence Per Joule), MFU, and MBU per sample
- All telemetry fields (energy, power, GPU util, throughput, MFU, MBU,
  IPW, IPJ) written to JSONL output and summary JSON
- RunSummary includes MetricStats (mean/median/min/max/std) for all
  telemetry metrics plus total_energy_joules
- GLM-4.7-Flash eval config enriched with A100 SXM hardware params
- 146 eval tests pass (26 new tests for telemetry, efficiency metrics,
  metadata flow, MetricStats helpers)
- User guide and API docs updated with new fields and output format

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 21:17:53 +00:00

1214 lines
36 KiB
Markdown

# Evaluation Framework
The `openjarvis-evals` package provides a structured harness for measuring model quality
across research benchmarks. It is a separate package from the main `openjarvis` library,
installed from the `evals/` directory, and exposes a CLI (`openjarvis-eval`) plus a
Python API for programmatic use.
The framework is organized around four ABCs — `InferenceBackend`, `DatasetProvider`,
`Scorer`, and the concrete `EvalRunner` — wired together by `RunConfig`. A TOML-based
suite configuration system expands a models-by-benchmarks matrix into individual
`RunConfig` objects so an entire comparison table can be launched from a single file.
!!! note "Installation"
The evaluation framework is a separate package. Install it from the repository root:
```bash
cd evals/
uv pip install -e ".[dev]"
# or
pip install -e ".[dev]"
```
The package requires Python 3.10+, `openjarvis>=1.0.0`, and `datasets>=2.14`.
---
## Core Types (`evals.core.types`)
These dataclasses are the shared vocabulary for every component in the framework.
### EvalRecord
A single evaluation sample loaded from a dataset.
```python
from evals.core.types import EvalRecord
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `record_id` | `str` | — | Unique identifier for this sample |
| `problem` | `str` | — | The prompt or question presented to the model |
| `reference` | `str` | — | Ground-truth answer used for scoring |
| `category` | `str` | — | Task category: `"chat"`, `"reasoning"`, `"rag"`, or `"agentic"` |
| `subject` | `str` | `""` | Subject area or sub-topic within the benchmark |
| `metadata` | `Dict[str, Any]` | `{}` | Benchmark-specific extra fields (options, difficulty, file paths, etc.) |
```python
record = EvalRecord(
record_id="supergpqa-0",
problem="What is the capital of France?\nOptions:\nA. Berlin\nB. Paris\nC. Madrid",
reference="B",
category="reasoning",
subject="geography",
metadata={"difficulty": "easy", "options": ["Berlin", "Paris", "Madrid"]},
)
```
---
### EvalResult
The result of running inference on a single `EvalRecord`.
```python
from evals.core.types import EvalResult
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `record_id` | `str` | — | Matches the source `EvalRecord.record_id` |
| `model_answer` | `str` | — | Raw text output from the model |
| `is_correct` | `Optional[bool]` | `None` | Scoring verdict; `None` if scoring could not be determined |
| `score` | `Optional[float]` | `None` | Numeric score (typically `1.0` / `0.0`); may be `None` if `is_correct` is `None` |
| `latency_seconds` | `float` | `0.0` | Wall-clock generation time |
| `prompt_tokens` | `int` | `0` | Input token count from usage metadata |
| `completion_tokens` | `int` | `0` | Output token count from usage metadata |
| `cost_usd` | `float` | `0.0` | Estimated inference cost in USD |
| `error` | `Optional[str]` | `None` | Exception message if inference or scoring failed |
| `scoring_metadata` | `Dict[str, Any]` | `{}` | Scorer-specific details (extracted letter, judge output, match type, etc.) |
| `ttft` | `float` | `0.0` | Time to first token (seconds) |
| `energy_joules` | `float` | `0.0` | GPU energy consumed (joules) |
| `power_watts` | `float` | `0.0` | Average GPU power draw (watts) |
| `gpu_utilization_pct` | `float` | `0.0` | Average GPU utilization (%) |
| `throughput_tok_per_sec` | `float` | `0.0` | Output token throughput (tokens/sec) |
| `mfu_pct` | `float` | `0.0` | Model FLOPs Utilization (%) |
| `mbu_pct` | `float` | `0.0` | Memory Bandwidth Utilization (%) |
| `ipw` | `float` | `0.0` | Intelligence Per Watt: `accuracy / power_watts` |
| `ipj` | `float` | `0.0` | Intelligence Per Joule: `accuracy / energy_joules` |
!!! tip "Distinguishing errors from wrong answers"
A non-`None` `error` field means inference itself failed. When `error` is `None` but
`is_correct` is `None`, scoring was attempted but the scorer could not determine a
verdict (for example, the judge returned an unparseable response).
---
### RunConfig
Configuration for a single evaluation run (one model on one benchmark).
```python
from evals.core.types import RunConfig
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `benchmark` | `str` | — | Benchmark name: `"supergpqa"`, `"gaia"`, `"frames"`, or `"wildchat"` |
| `backend` | `str` | — | Backend identifier: `"jarvis-direct"` or `"jarvis-agent"` |
| `model` | `str` | — | Model identifier passed to the backend (e.g., `"qwen3:8b"`, `"gpt-4o"`) |
| `max_samples` | `Optional[int]` | `None` | Limit the dataset to this many records; `None` uses the full dataset |
| `max_workers` | `int` | `4` | Number of parallel threads for inference |
| `temperature` | `float` | `0.0` | Sampling temperature |
| `max_tokens` | `int` | `2048` | Maximum output tokens per sample |
| `judge_model` | `str` | `"gpt-5-mini-2025-08-07"` | Model identifier used by the LLM judge scorer |
| `engine_key` | `Optional[str]` | `None` | Override the OpenJarvis engine (`"ollama"`, `"vllm"`, `"cloud"`, etc.) |
| `agent_name` | `Optional[str]` | `None` | Agent name for `jarvis-agent` backend; defaults to `"orchestrator"` |
| `tools` | `List[str]` | `[]` | Tool names enabled for the agent (e.g., `["calculator", "file_read"]`) |
| `output_path` | `Optional[str]` | `None` | JSONL output file path; auto-generated from benchmark and model name if `None` |
| `seed` | `int` | `42` | Random seed for dataset shuffling |
| `dataset_split` | `Optional[str]` | `None` | Override the dataset split (e.g., `"validation"`, `"test"`) |
| `telemetry` | `bool` | `False` | Enable GPU telemetry capture |
| `gpu_metrics` | `bool` | `False` | Enable GPU metric polling via `pynvml` |
| `metadata` | `Dict[str, Any]` | `{}` | Model hardware metadata for efficiency calculations (populated by `expand_suite()`) |
```python
config = RunConfig(
benchmark="supergpqa",
backend="jarvis-direct",
model="qwen3:8b",
max_samples=100,
max_workers=8,
engine_key="ollama",
output_path="results/supergpqa_qwen3-8b.jsonl",
)
```
---
### RunSummary
Aggregate statistics produced by `EvalRunner.run()` at the end of a completed run.
```python
from evals.core.types import RunSummary
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `benchmark` | `str` | — | Benchmark name |
| `category` | `str` | — | Task category (inferred from records; falls back to `benchmark` name) |
| `backend` | `str` | — | Backend used |
| `model` | `str` | — | Model identifier |
| `total_samples` | `int` | — | Total records processed (including errors) |
| `scored_samples` | `int` | — | Records where `is_correct` is not `None` |
| `correct` | `int` | — | Records where `is_correct` is `True` |
| `accuracy` | `float` | — | `correct / scored_samples`; rounded to 4 decimal places |
| `errors` | `int` | — | Records where inference or scoring raised an exception |
| `mean_latency_seconds` | `float` | — | Mean wall-clock latency across all successful inferences |
| `total_cost_usd` | `float` | — | Sum of `cost_usd` across all records |
| `per_subject` | `Dict[str, Dict[str, float]]` | `{}` | Per-subject breakdown: `{subject: {accuracy, total, scored, correct}}` |
| `started_at` | `float` | `0.0` | Unix timestamp at run start |
| `ended_at` | `float` | `0.0` | Unix timestamp at run end |
| `accuracy_stats` | `Optional[MetricStats]` | `None` | Descriptive statistics for per-sample accuracy (binary 0/1) |
| `latency_stats` | `Optional[MetricStats]` | `None` | Descriptive statistics for inference latency |
| `ttft_stats` | `Optional[MetricStats]` | `None` | Descriptive statistics for time-to-first-token |
| `energy_stats` | `Optional[MetricStats]` | `None` | Descriptive statistics for GPU energy (joules) |
| `power_stats` | `Optional[MetricStats]` | `None` | Descriptive statistics for GPU power (watts) |
| `gpu_utilization_stats` | `Optional[MetricStats]` | `None` | Descriptive statistics for GPU utilization (%) |
| `throughput_stats` | `Optional[MetricStats]` | `None` | Descriptive statistics for token throughput |
| `mfu_stats` | `Optional[MetricStats]` | `None` | Descriptive statistics for Model FLOPs Utilization (%) |
| `mbu_stats` | `Optional[MetricStats]` | `None` | Descriptive statistics for Memory Bandwidth Utilization (%) |
| `ipw_stats` | `Optional[MetricStats]` | `None` | Descriptive statistics for Intelligence Per Watt |
| `ipj_stats` | `Optional[MetricStats]` | `None` | Descriptive statistics for Intelligence Per Joule |
| `total_energy_joules` | `float` | `0.0` | Total GPU energy consumed across all samples |
The runner also writes a `.summary.json` file alongside the JSONL output, containing
the serialized `RunSummary`.
---
### MetricStats
Descriptive statistics for a single metric across samples.
```python
from evals.core.types import MetricStats
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `mean` | `float` | `0.0` | Arithmetic mean |
| `median` | `float` | `0.0` | Median value |
| `min` | `float` | `0.0` | Minimum value |
| `max` | `float` | `0.0` | Maximum value |
| `std` | `float` | `0.0` | Standard deviation (0.0 for single-element lists) |
`MetricStats` is computed by `_metric_stats()` in the runner and serialized to
JSON by `_metric_stats_to_dict()`. Fields in `RunSummary` like `accuracy_stats`,
`energy_stats`, `mfu_stats`, etc. are `Optional[MetricStats]` — they are `None`
when no positive values were observed for that metric.
---
## Suite Config Types (`evals.core.types`)
These dataclasses map directly to sections in a TOML eval suite config file.
They are populated by `load_eval_config()` and consumed by `expand_suite()`.
### MetaConfig
```python
@dataclass
class MetaConfig:
name: str = ""
description: str = ""
```
Maps to the `[meta]` TOML section. Both fields are optional and used only for
display output in the CLI.
---
### DefaultsConfig
```python
@dataclass
class DefaultsConfig:
temperature: float = 0.0
max_tokens: int = 2048
```
Maps to `[defaults]`. These values are the lowest-priority settings in the merge
precedence: `benchmark-level > model-level > [defaults] > built-in defaults`.
---
### JudgeConfig
```python
@dataclass
class JudgeConfig:
model: str = "gpt-4o"
provider: Optional[str] = None
temperature: float = 0.0
max_tokens: int = 1024
```
Maps to `[judge]`. The judge model is used by LLM-as-judge scorers (GAIA, FRAMES,
WildChat, SuperGPQA). The `provider` field is reserved for future routing; currently
the judge backend is always constructed with `engine_key="cloud"`.
---
### ExecutionConfig
```python
@dataclass
class ExecutionConfig:
max_workers: int = 4
output_dir: str = "results/"
seed: int = 42
telemetry: bool = False
gpu_metrics: bool = False
```
Maps to `[run]`. `output_dir` is the base directory for all JSONL output files;
individual filenames are auto-generated as `{benchmark}_{model-slug}.jsonl`.
When `telemetry` is enabled, the runner captures GPU energy, power, utilization,
and throughput per sample via `InstrumentedEngine`. When `gpu_metrics` is enabled,
`GpuMonitor` polls GPU sensors via `pynvml` during inference.
---
### ModelConfig
```python
@dataclass
class ModelConfig:
name: str = ""
engine: Optional[str] = None
provider: Optional[str] = None
temperature: Optional[float] = None
max_tokens: Optional[int] = None
param_count_b: float = 0.0
active_params_b: Optional[float] = None
gpu_peak_tflops: float = 0.0
gpu_peak_bandwidth_gb_s: float = 0.0
num_gpus: int = 1
```
Maps to each `[[models]]` entry. `name` is required. `temperature` and `max_tokens`
override `[defaults]` for every benchmark this model runs against, unless a
benchmark-level override also exists. The hardware parameters (`param_count_b`,
`active_params_b`, `gpu_peak_tflops`, `gpu_peak_bandwidth_gb_s`, `num_gpus`) are
used to compute MFU (Model FLOPs Utilization) and MBU (Memory Bandwidth Utilization)
per sample. These are flowed into `RunConfig.metadata` by `expand_suite()`.
---
### BenchmarkConfig
```python
@dataclass
class BenchmarkConfig:
name: str = ""
backend: str = "jarvis-direct"
max_samples: Optional[int] = None
split: Optional[str] = None
agent: Optional[str] = None
tools: List[str] = field(default_factory=list)
judge_model: Optional[str] = None
temperature: Optional[float] = None
max_tokens: Optional[int] = None
```
Maps to each `[[benchmarks]]` entry. `name` is required. `backend` must be one of
`"jarvis-direct"` or `"jarvis-agent"`. `judge_model` overrides `[judge].model` for
this benchmark only.
---
### EvalSuiteConfig
The top-level config object returned by `load_eval_config()`.
```python
@dataclass
class EvalSuiteConfig:
meta: MetaConfig
defaults: DefaultsConfig
judge: JudgeConfig
run: ExecutionConfig
models: List[ModelConfig]
benchmarks: List[BenchmarkConfig]
```
`expand_suite(suite)` iterates over `models x benchmarks` to produce one `RunConfig`
per pair, applying the merge precedence rules documented in `DefaultsConfig`.
---
## Config Module (`evals.core.config`)
```python
from evals.core.config import load_eval_config, expand_suite, EvalConfigError
```
### EvalConfigError
```python
class EvalConfigError(Exception): ...
```
Raised by `load_eval_config()` for structural validation failures: missing required
fields, invalid backend names, or empty `[[models]]` / `[[benchmarks]]` lists.
---
### load_eval_config
```python
def load_eval_config(path: str | Path) -> EvalSuiteConfig
```
Load and validate an eval suite configuration from a TOML file.
Uses the standard library `tomllib` on Python 3.11+ and the `tomli` backport on
Python 3.10.
**Parameters:**
| Parameter | Type | Description |
|-----------|------|-------------|
| `path` | `str \| Path` | Path to the TOML config file |
**Returns:** `EvalSuiteConfig`
**Raises:**
- `EvalConfigError` — structural validation failures (missing `name`, invalid backend, no models/benchmarks defined)
- `FileNotFoundError` — if the config file does not exist
```python
from evals.core.config import load_eval_config
suite = load_eval_config("evals/configs/full-suite.toml")
print(f"{len(suite.models)} models, {len(suite.benchmarks)} benchmarks")
```
---
### expand_suite
```python
def expand_suite(suite: EvalSuiteConfig) -> List[RunConfig]
```
Expand an `EvalSuiteConfig` into a flat list of `RunConfig` objects, one per
model-benchmark pair, with all override layers merged.
**Merge precedence (highest wins):**
1. Benchmark-level (`BenchmarkConfig.temperature`, `.max_tokens`, `.judge_model`)
2. Model-level (`ModelConfig.temperature`, `.max_tokens`)
3. Suite defaults (`DefaultsConfig`)
4. Built-in dataclass defaults
Output paths are auto-generated as `{output_dir}/{benchmark}_{model-slug}.jsonl`,
where `model-slug` replaces `/` and `:` with `-`.
**Parameters:**
| Parameter | Type | Description |
|-----------|------|-------------|
| `suite` | `EvalSuiteConfig` | Parsed suite configuration |
**Returns:** `List[RunConfig]` — one entry per model-benchmark combination.
```python
from evals.core.config import load_eval_config, expand_suite
suite = load_eval_config("evals/configs/full-suite.toml")
run_configs = expand_suite(suite) # e.g., 3 models x 4 benchmarks = 12 RunConfigs
for rc in run_configs:
print(f"{rc.benchmark} / {rc.model} -> {rc.output_path}")
```
---
## Abstract Base Classes
### InferenceBackend (`evals.core.backend`)
```python
from evals.core.backend import InferenceBackend
```
Base class for all inference backends. A backend wraps an engine or agent and
provides a uniform text-in / text-out interface for the runner.
```python
class InferenceBackend(ABC):
backend_id: str
```
**Class attribute:**
| Attribute | Type | Description |
|-----------|------|-------------|
| `backend_id` | `str` | Registry identifier (e.g., `"jarvis-direct"`, `"jarvis-agent"`) |
**Abstract methods:**
#### generate
```python
@abstractmethod
def generate(
self,
prompt: str,
*,
model: str,
system: str = "",
temperature: float = 0.0,
max_tokens: int = 2048,
) -> str
```
Generate a response and return the text content only.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `prompt` | `str` | — | User message or formatted problem text |
| `model` | `str` | — | Model identifier |
| `system` | `str` | `""` | Optional system prompt |
| `temperature` | `float` | `0.0` | Sampling temperature |
| `max_tokens` | `int` | `2048` | Maximum output tokens |
**Returns:** `str` — model text output.
---
#### generate_full
```python
@abstractmethod
def generate_full(
self,
prompt: str,
*,
model: str,
system: str = "",
temperature: float = 0.0,
max_tokens: int = 2048,
) -> Dict[str, Any]
```
Generate a response and return full details including usage and cost metadata.
**Returns:** `dict` with keys:
| Key | Type | Description |
|-----|------|-------------|
| `content` | `str` | Model text output |
| `usage` | `dict` | Token usage (`prompt_tokens`, `completion_tokens`) |
| `model` | `str` | Model identifier used |
| `latency_seconds` | `float` | Wall-clock generation time |
| `cost_usd` | `float` | Estimated inference cost |
---
#### close
```python
def close(self) -> None
```
Release resources held by the backend (connections, engine handles, etc.).
The default implementation is a no-op; subclasses override as needed.
---
### DatasetProvider (`evals.core.dataset`)
```python
from evals.core.dataset import DatasetProvider
```
Base class for all evaluation dataset providers. Datasets are loaded lazily via
`load()` and then consumed record-by-record through `iter_records()`.
```python
class DatasetProvider(ABC):
dataset_id: str
dataset_name: str
```
**Class attributes:**
| Attribute | Type | Description |
|-----------|------|-------------|
| `dataset_id` | `str` | Short identifier matching the CLI benchmark name |
| `dataset_name` | `str` | Human-readable display name |
**Abstract methods:**
#### load
```python
@abstractmethod
def load(
self,
*,
max_samples: Optional[int] = None,
split: Optional[str] = None,
seed: Optional[int] = None,
) -> None
```
Load the dataset, optionally downloading from HuggingFace Hub. Must be called
before `iter_records()`.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `max_samples` | `Optional[int]` | `None` | Truncate to this many records after shuffling |
| `split` | `Optional[str]` | `None` | Dataset split override (e.g., `"test"`, `"validation"`) |
| `seed` | `Optional[int]` | `None` | Shuffle seed; `None` preserves original order |
---
#### iter_records
```python
@abstractmethod
def iter_records(self) -> Iterable[EvalRecord]
```
Iterate over the loaded `EvalRecord` objects. Raises if called before `load()`.
---
#### size
```python
@abstractmethod
def size(self) -> int
```
Return the count of loaded records.
---
### Scorer (`evals.core.scorer`)
```python
from evals.core.scorer import Scorer, LLMJudgeScorer
```
Base class for all scorers. A scorer compares a model's answer to the reference
in an `EvalRecord` and returns a correctness verdict with optional metadata.
```python
class Scorer(ABC):
scorer_id: str
```
**Class attribute:**
| Attribute | Type | Description |
|-----------|------|-------------|
| `scorer_id` | `str` | Short identifier matching the benchmark name |
**Abstract method:**
#### score
```python
@abstractmethod
def score(
self,
record: EvalRecord,
model_answer: str,
) -> Tuple[Optional[bool], Dict[str, Any]]
```
Score a model answer against the reference.
| Parameter | Type | Description |
|-----------|------|-------------|
| `record` | `EvalRecord` | The source sample including `reference` and `metadata` |
| `model_answer` | `str` | Raw text output from the model |
**Returns:** `(is_correct, metadata)` tuple where:
- `is_correct` is `True`, `False`, or `None` (if scoring could not be determined)
- `metadata` is a `dict` of scorer-specific details stored in `EvalResult.scoring_metadata`
---
### LLMJudgeScorer
```python
class LLMJudgeScorer(Scorer):
def __init__(self, judge_backend: InferenceBackend, judge_model: str) -> None
```
Convenience base class for scorers that call an LLM to evaluate answers.
Exposes `_ask_judge()` to subclasses.
```python
def _ask_judge(
self,
prompt: str,
*,
system: str = "",
temperature: float = 0.0,
max_tokens: int = 1024,
) -> str
```
Send a prompt to the judge LLM and return the response text. Delegates to
`judge_backend.generate()`.
---
## EvalRunner (`evals.core.runner`)
```python
from evals.core.runner import EvalRunner
```
The `EvalRunner` wires together a `RunConfig`, `DatasetProvider`, `InferenceBackend`,
and `Scorer` and executes the benchmark. Inference is parallelized using a
`ThreadPoolExecutor`. Results are written to JSONL incrementally so progress is
not lost if the run is interrupted.
### Constructor
```python
class EvalRunner:
def __init__(
self,
config: RunConfig,
dataset: DatasetProvider,
backend: InferenceBackend,
scorer: Scorer,
) -> None
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `config` | `RunConfig` | Run parameters (model, workers, output path, etc.) |
| `dataset` | `DatasetProvider` | Dataset to evaluate against |
| `backend` | `InferenceBackend` | Inference backend for generation |
| `scorer` | `Scorer` | Scorer for comparing model answers to references |
### run
```python
def run(self) -> RunSummary
```
Execute the full evaluation and return aggregate statistics.
The method:
1. Calls `dataset.load()` with the `RunConfig` sampling parameters
2. Submits all records to a `ThreadPoolExecutor` with `config.max_workers` threads
3. For each record, calls `backend.generate_full()` then `scorer.score()`
4. Writes each `EvalResult` to a JSONL file as it completes
5. Writes a `.summary.json` alongside the JSONL at the end
**Returns:** `RunSummary`
```python title="programmatic_eval.py"
from evals.core.types import RunConfig
from evals.core.runner import EvalRunner
from evals.datasets.supergpqa import SuperGPQADataset
from evals.backends.jarvis_direct import JarvisDirectBackend
from evals.scorers.supergpqa_mcq import SuperGPQAScorer
config = RunConfig(
benchmark="supergpqa",
backend="jarvis-direct",
model="qwen3:8b",
max_samples=50,
engine_key="ollama",
)
dataset = SuperGPQADataset()
backend = JarvisDirectBackend(engine_key="ollama")
judge_backend = JarvisDirectBackend(engine_key="cloud")
scorer = SuperGPQAScorer(judge_backend=judge_backend, judge_model="gpt-4o")
runner = EvalRunner(config, dataset, backend, scorer)
summary = runner.run()
print(f"Accuracy: {summary.accuracy:.4f} ({summary.correct}/{summary.scored_samples})")
print(f"Mean latency: {summary.mean_latency_seconds:.2f}s")
print(f"Total cost: ${summary.total_cost_usd:.4f}")
backend.close()
judge_backend.close()
```
---
## Backends
### JarvisDirectBackend (`evals.backends.jarvis_direct`)
```python
from evals.backends.jarvis_direct import JarvisDirectBackend
```
Engine-level inference via `SystemBuilder`. Routes directly to the configured
`InferenceEngine` without an agent loop, making it the fastest backend and
appropriate for benchmarks that do not require tool use.
```python
class JarvisDirectBackend(InferenceBackend):
backend_id = "jarvis-direct"
def __init__(self, engine_key: Optional[str] = None) -> None
```
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `engine_key` | `Optional[str]` | `None` | OpenJarvis engine identifier. `None` uses the auto-discovered engine from `~/.openjarvis/config.toml` |
Telemetry and traces are disabled for eval runs. The backend calls
`SystemBuilder().engine(engine_key).telemetry(False).traces(False).build()`.
**Compatible benchmarks:** `supergpqa`, `frames`, `wildchat` (any benchmark that
does not require multi-step tool calling).
=== "Local model"
```python
backend = JarvisDirectBackend(engine_key="ollama")
text = backend.generate("What is 2+2?", model="qwen3:8b")
```
=== "Cloud model"
```python
backend = JarvisDirectBackend(engine_key="cloud")
text = backend.generate("What is 2+2?", model="gpt-4o")
```
---
### JarvisAgentBackend (`evals.backends.jarvis_agent`)
```python
from evals.backends.jarvis_agent import JarvisAgentBackend
```
Agent-level inference via `JarvisSystem.ask()`. Wraps the full OpenJarvis agent
harness, enabling multi-turn tool-calling loops for agentic benchmarks.
```python
class JarvisAgentBackend(InferenceBackend):
backend_id = "jarvis-agent"
def __init__(
self,
engine_key: Optional[str] = None,
agent_name: str = "orchestrator",
tools: Optional[List[str]] = None,
) -> None
```
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `engine_key` | `Optional[str]` | `None` | OpenJarvis engine identifier |
| `agent_name` | `str` | `"orchestrator"` | Agent to use (`"orchestrator"`, `"react"`, etc.) |
| `tools` | `Optional[List[str]]` | `None` | Tool names to enable (e.g., `["calculator", "file_read"]`) |
The `generate_full()` return dict includes two additional keys beyond the standard
`InferenceBackend` contract:
| Key | Type | Description |
|-----|------|-------------|
| `turns` | `int` | Number of agent turns completed |
| `tool_results` | `list` | Tool call results from the agent loop |
**Compatible benchmarks:** `gaia` (requires file reading and multi-step reasoning).
```python
backend = JarvisAgentBackend(
engine_key="ollama",
agent_name="orchestrator",
tools=["file_read", "calculator"],
)
result = backend.generate_full(
"How many pages is the attached PDF?",
model="qwen3:8b",
)
print(result["content"])
print(f"Completed in {result['turns']} turn(s)")
backend.close()
```
---
## Dataset Providers
### SuperGPQADataset (`evals.datasets.supergpqa`)
```python
from evals.datasets.supergpqa import SuperGPQADataset
```
Loads the SuperGPQA multiple-choice benchmark from HuggingFace (`m-a-p/SuperGPQA`).
Records have `category="reasoning"` and `subject` set to the discipline subfield.
```python
class SuperGPQADataset(DatasetProvider):
dataset_id = "supergpqa"
dataset_name = "SuperGPQA"
```
- **Default split:** `"train"`
- **HuggingFace path:** `m-a-p/SuperGPQA`
- Each problem is formatted with lettered options (A, B, C, ...) and the instruction
"Respond with the correct letter only."
- `record.reference` is the correct answer letter (e.g., `"B"`).
---
### GAIADataset (`evals.datasets.gaia`)
```python
from evals.datasets.gaia import GAIADataset
```
Loads the GAIA agentic benchmark from HuggingFace (`gaia-benchmark/GAIA`).
Records have `category="agentic"` and `subject` set to `level_1`, `level_2`, or
`level_3`.
```python
class GAIADataset(DatasetProvider):
dataset_id = "gaia"
dataset_name = "GAIA"
def __init__(self, cache_dir: Optional[str] = None) -> None
```
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `cache_dir` | `Optional[str]` | `~/.cache/gaia_benchmark` | Local directory for HuggingFace snapshot download |
- **Default split:** `"validation"`
- **Default subset:** `"2023_all"`
- Downloads the full dataset snapshot including associated files (PDFs, images, CSVs)
referenced in questions. File paths are embedded in the problem prompt.
!!! warning "Dataset access"
GAIA requires accepting the HuggingFace dataset terms of service and being logged
in with `huggingface-cli login` before the snapshot download can proceed.
---
### FRAMESDataset (`evals.datasets.frames`)
```python
from evals.datasets.frames import FRAMESDataset
```
Loads the FRAMES multi-hop factual retrieval benchmark from HuggingFace
(`google/frames-benchmark`). Records have `category="rag"` and `subject` set to
the reasoning type(s) (e.g., `"multi-hop, temporal"`).
```python
class FRAMESDataset(DatasetProvider):
dataset_id = "frames"
dataset_name = "FRAMES"
```
- **Default split:** `"test"`
- Wikipedia article links referenced in each question are included in the problem prompt.
---
### WildChatDataset (`evals.datasets.wildchat`)
```python
from evals.datasets.wildchat import WildChatDataset
```
Loads the WildChat-1M dataset (`allenai/WildChat-1M`) and filters to English
single-turn conversations for chat quality evaluation. Records have
`category="chat"` and `subject="conversation"`.
```python
class WildChatDataset(DatasetProvider):
dataset_id = "wildchat"
dataset_name = "WildChat"
```
- **Default split:** `"train"`
- Filters by `language == "english"` and exactly two turns (one user + one assistant).
- `record.problem` is the user message; `record.reference` is the original assistant
response used as the quality baseline by the judge scorer.
---
## Scorers
### SuperGPQAScorer (`evals.scorers.supergpqa_mcq`)
```python
from evals.scorers.supergpqa_mcq import SuperGPQAScorer
```
LLM-based letter extraction followed by exact match against the reference letter.
The judge LLM extracts the final answer letter from potentially verbose model
responses, then compares it to `record.reference`.
```python
class SuperGPQAScorer(LLMJudgeScorer):
scorer_id = "supergpqa"
```
**Scoring metadata keys:**
| Key | Description |
|-----|-------------|
| `reference_letter` | Correct answer letter from the dataset |
| `candidate_letter` | Letter extracted by the judge LLM |
| `valid_letters` | Valid answer letters for this question (e.g., `"ABCD"`) |
| `reason` | Set to `"missing_reference_letter"` or `"no_choice_letter_extracted"` on failure |
---
### GAIAScorer (`evals.scorers.gaia_exact`)
```python
from evals.scorers.gaia_exact import GAIAScorer, exact_match
```
Normalized exact match with an LLM fallback for semantic comparison. Tries exact
match first (no API call); falls back to the judge LLM only when exact match fails.
```python
class GAIAScorer(LLMJudgeScorer):
scorer_id = "gaia"
```
**Normalization rules for exact match:**
- Numbers: strips `$`, `%`, `,` then compares as `float`
- Lists (comma- or semicolon-separated): splits and compares element-by-element
- Strings: lowercases, strips whitespace and punctuation
**Scoring metadata keys:**
| Key | Description |
|-----|-------------|
| `match_type` | `"exact"` or `"llm_fallback"` |
| `raw_judge_output` | Full LLM judge response (llm_fallback only) |
| `extracted_answer` | Answer extracted by the judge (llm_fallback only) |
The `exact_match` helper function is also exported and can be used independently:
```python
from evals.scorers.gaia_exact import exact_match
assert exact_match("$1,000", "1000") is True
assert exact_match("paris", "Paris") is True
assert exact_match("3, 5", "3,5") is True
```
---
### FRAMESScorer (`evals.scorers.frames_judge`)
```python
from evals.scorers.frames_judge import FRAMESScorer
```
LLM-as-judge scorer for FRAMES multi-hop factual retrieval. Uses a structured
grading rubric that focuses on semantic equivalence, ignoring formatting and
capitalization differences.
```python
class FRAMESScorer(LLMJudgeScorer):
scorer_id = "frames"
```
**Scoring metadata keys:**
| Key | Description |
|-----|-------------|
| `raw_judge_output` | Full LLM judge response |
| `extracted_answer` | Answer extracted by the judge |
---
### WildChatScorer (`evals.scorers.wildchat_judge`)
```python
from evals.scorers.wildchat_judge import WildChatScorer
```
Dual-comparison LLM-as-judge for chat quality. Runs two comparisons — once with
the model answer as Assistant A and once as Assistant B — to reduce position bias.
The model answer is considered correct if it wins or ties in either comparison.
```python
class WildChatScorer(LLMJudgeScorer):
scorer_id = "wildchat"
```
The judge uses a five-point verdict scale: `[[A>>B]]`, `[[A>B]]`, `[[A=B]]`,
`[[B>A]]`, `[[B>>A]]`. A tie (`A=B`) is counted as correct.
**Scoring metadata keys:**
| Key | Description |
|-----|-------------|
| `generated_as_a` | `{verdict, response}` from the first comparison pass |
| `generated_as_b` | `{verdict, response}` from the second comparison pass |
---
## CLI Reference
The evaluation framework ships a `openjarvis-eval` CLI built with Click.
### openjarvis-eval run
Run a single benchmark or a full suite from a TOML config.
```bash title="Single run"
openjarvis-eval run \
--benchmark supergpqa \
--model qwen3:8b \
--engine ollama \
--max-samples 100 \
--max-workers 8 \
--output results/supergpqa_qwen3-8b.jsonl
```
```bash title="Suite run from TOML"
openjarvis-eval run --config evals/configs/full-suite.toml
```
| Option | Short | Default | Description |
|--------|-------|---------|-------------|
| `--config` | `-c` | `None` | TOML suite config file; enables suite mode |
| `--benchmark` | `-b` | — | Benchmark name (required in single-run mode) |
| `--backend` | | `jarvis-direct` | `jarvis-direct` or `jarvis-agent` |
| `--model` | `-m` | — | Model identifier (required in single-run mode) |
| `--engine` | `-e` | `None` | Engine key override |
| `--agent` | | `orchestrator` | Agent name for `jarvis-agent` backend |
| `--tools` | | `""` | Comma-separated tool names |
| `--max-samples` | `-n` | `None` | Sample limit |
| `--max-workers` | `-w` | `4` | Parallel threads |
| `--judge-model` | | `gpt-4o` | LLM judge model |
| `--output` | `-o` | auto | JSONL output path |
| `--seed` | | `42` | Shuffle seed |
| `--split` | | `None` | Dataset split override |
| `--temperature` | | `0.0` | Sampling temperature |
| `--max-tokens` | | `2048` | Maximum output tokens |
| `--verbose` | `-v` | `False` | Enable debug logging |
### openjarvis-eval run-all
Run all four benchmarks against a single model.
```bash
openjarvis-eval run-all \
--model qwen3:8b \
--engine ollama \
--max-samples 50 \
--output-dir results/
```
### openjarvis-eval summarize
Recompute summary statistics from an existing JSONL output file.
```bash
openjarvis-eval summarize results/supergpqa_qwen3-8b.jsonl
```
### openjarvis-eval list
List all available benchmarks and backends.
```bash
openjarvis-eval list
```
---
## TOML Suite Config Format
A suite config drives a full `models x benchmarks` comparison matrix with a single
command. All sections except `[[models]]` and `[[benchmarks]]` are optional.
```toml title="evals/configs/full-suite.toml"
[meta]
name = "full-suite-v1"
description = "Evaluate all benchmarks against production models"
[defaults]
temperature = 0.0
max_tokens = 2048
[judge]
model = "gpt-4o"
temperature = 0.0
max_tokens = 1024
[run]
max_workers = 4
output_dir = "results/"
seed = 42
# One [[models]] entry per model to evaluate
[[models]]
name = "qwen3:8b"
engine = "ollama"
temperature = 0.3
[[models]]
name = "gpt-4o"
provider = "openai"
# One [[benchmarks]] entry per benchmark
[[benchmarks]]
name = "supergpqa"
backend = "jarvis-direct"
max_samples = 200
[[benchmarks]]
name = "gaia"
backend = "jarvis-agent"
agent = "orchestrator"
tools = ["file_read", "calculator"]
max_samples = 50
judge_model = "claude-sonnet-4-20250514" # override judge for this benchmark
[[benchmarks]]
name = "frames"
backend = "jarvis-direct"
max_samples = 100
[[benchmarks]]
name = "wildchat"
backend = "jarvis-direct"
max_samples = 150
temperature = 0.7
```
```bash
openjarvis-eval run --config evals/configs/full-suite.toml
# Suite: full-suite-v1
# 2 model(s) x 4 benchmark(s) = 8 run(s)
```
---
## See Also
- [Benchmarks Module](bench.md) — `openjarvis.bench` performance benchmarks (latency, throughput) for the inference engine, separate from the eval framework
- [Telemetry & Traces](telemetry.md) — `openjarvis.telemetry` and `openjarvis.traces` for production monitoring
- [Python SDK](../user-guide/python-sdk.md) — `Jarvis` class used internally by eval backends
- [Agents](../user-guide/agents.md) — Agent implementations invoked by `JarvisAgentBackend`