Eval runners¶
evaltrack has no eval runner of its own. It records, gates and tracks the results of whichever one you already use.
A translator turns a runner's results into evaltrack's own model, and everything downstream of the eval reads that model. Translators for pydantic-evals and DeepEval ship with evaltrack. To connect another runner, write a small translator.
The only thing that differs is how you hand evaltrack the eval. pydantic-evals' evaluate is a
coroutine, so use run_async. DeepEval's is not, so use run. After that, the gate, the recorded
run, the history and the dashboard work the same for both.
Installing a runner¶
Install the runner you already write your evals in, and evaltrack reads what it produces. evaltrack depends on no runner and imports one only when that runner's results show up.
An extra exists for each shipped translator, so the install is one line instead of two:
uv add "evaltrack[deepeval]" # the same as adding deepeval yourself
| extra | installs |
|---|---|
evaltrack[pydantic-evals] |
pydantic-evals>=2 |
evaltrack[deepeval] |
deepeval>=4.2 |
The floors are the versions the test suite runs against, not compatibility claims.
Running an eval¶
import evaltrack
@pytest.mark.evaltrack(score_bars={"helpfulness": 0.8}, flake_reruns=2)
def test_my_eval():
evaltrack.run(my_runner.evaluate, dataset)
You hand run the eval, not its result, so evaltrack can call it again for another round. run
takes no configuration of its own. Configuration lives on
the marker, which decides what is recorded, gated and rerun.
Everything you pass after the eval is passed to it on every round.
pydantic-evals¶
await evaltrack.run_async(dataset.evaluate, task)
On Python 3.12 and 3.13, evaluate_sync() emits a harmless pydantic-evals
DeprecationWarning: There is no current event loop, which run_async() does not hit. Under
filterwarnings = error it fails the test, so ignore that one and keep error for the rest:
[tool.pytest.ini_options]
filterwarnings = [
"error",
"ignore:There is no current event loop:DeprecationWarning",
]
DeepEval¶
DeepEval's evaluate() scores a batch of test cases and returns the scores. It fails nothing, so
evaltrack gates them. DeepEval also ships assert_test(), which gates by raising inside the test.
evaltrack replaces that gate rather than adding to it, so use one or the other.
from deepeval.evaluate import evaluate
evaltrack.run(lambda: evaluate(test_cases=cases, metrics=[AnswerRelevancyMetric()]))
A MetricData is one result. Its threshold is the bar, so a metric with a threshold gates the
case on its own, and a score_bars entry for that name replaces it.
Configure a metric through its constructor. Under the default AsyncConfig(run_async=True),
evaluate() rebuilds every metric from the arguments its __init__ accepts, and drops anything set
on the instance afterwards, threshold included.
evaluate() runs each case once and has no repeat of its own, so repeats=N on the marker is
evaltrack calling the eval N times. There is no second count to set.
evaluate() writes its last run to ./.deepeval, beside whatever evaltrack records. Add
.deepeval/ to your .gitignore.
Three runnable examples:
test_08_deepeval.py
records and gates a run,
test_09_deepeval_repeats.py
adds the marker's repeats=, and
test_10_deepeval_traced.py
scores the parts of a traced app with @observe and dataset.evals_iterator().
What each runner calls these¶
evaltrack uses one set of words for every runner, defined in the shape of an eval. A translator maps each runner's own words onto them. The dashboard and the pass-rate history then read the same, whichever runner produced the run.
| evaltrack | pydantic-evals | DeepEval |
|---|---|---|
| case | Case |
LLMTestCase |
| attempt | one case, or one of evaluate(repeat=N) |
one case of one evaluate() call |
| evaluator | Evaluator |
a metric |
| result | one entry of assertions or scores |
one MetricData |
| round | one evaluate() call |
one evaluate() call |
| run | (none, it spans the whole pytest session) | a test_run_id, kept only as a detail |
Related: Translators · The evaltrack marker · Flakiness & reliability