Evaluation YAML & CLI¶
Every key in a case file, every command-line flag of kavalai-eval, and the
Python API underneath both. For what these are for, read
Evaluation & acceptance testing.
On this page
The case file¶
One file, one suite. It is a EvalSuite, and it is
validated in full before a single case runs.
name: green-village
judge_model: openai/gpt-5.6-luna # optional
cases:
- name: president
input:
user_message: Who is the president of Green Village?
expected:
agent_response: {contains: [Thomas Cook]}
- name: no_budget
type: judge
input:
user_message: What is the village's annual budget?
expected: >-
The answer says the information is not available instead of
inventing a figure.
Key |
Meaning |
|---|---|
|
Names the suite in the run’s header line. Required. |
|
|
|
The cases, run in the order they are written. |
There is deliberately no base_url key. Which agent a suite grades is
a property of the run, not of the cases — see
the guide.
Unknown keys are refused, in the suite and in every case. A silently ignored key is a case that never ran.
A case¶
Key |
Meaning |
|---|---|
|
How the case is reported, and what its session is recorded under. Required. |
|
|
|
Field values for the agent’s input type. Validated against that type before the call, so a mistyped field is an error rather than a puzzling answer. |
|
A mapping of output field to expected value or matcher for a simple case; a plain-language criterion for a judged one. |
Two combinations are refused by EvalCase itself, because
both would otherwise pass on any answer whatsoever:
Case 'x' is judged, so `expected` must be a plain-language criterion.
Case 'y' is simple, so `expected` must map output fields to expected
values. Use `type: judge` to grade a plain-language criterion.
Matchers¶
A simple case’s expected maps an output field to either a literal value or a
mapping of matcher names. A bare value is shorthand for equals:
expected:
status: needs_details # equals
order_id: {equals: ''}
missing: {equals: ['items[0].quantity']}
agent_response:
contains: ["1.2"]
not_contains: ["approximately"]
Matcher |
Argument |
Passes when |
|---|---|---|
|
Any value |
The field equals it exactly. |
|
A value, or a list of them |
Text: every argument appears as a substring, case-insensitively. List, tuple or set: every argument is a member; mapping: every argument is a key. Any other type never contains anything. |
|
A value, or a list of them |
No argument is contained, by the same rule. |
|
A pattern |
|
|
A list of values |
The field is one of them. |
Matchers on one field are all checked, and each failure is reported separately. Fields the expectation does not mention are ignored, so a case states what it cares about and nothing more. Naming a field the agent’s output does not have is a failure, not a skip.
A mapping is read as matchers only when every key is a matcher name, so an
agent that genuinely answers with a dictionary can still be compared with
equals:
expected:
totals: {cases: 26, passed: 26} # a literal dict, not matchers
An empty or absent expected asserts only that the agent answered.
kavalai-eval¶
$ kavalai-eval <cases.yaml> --port <port> [options]
Flag |
Meaning |
|---|---|
|
Path to the YAML file of cases. Positional, required. |
|
Agent server port. Required: which agent is being evaluated is never left to a default. |
|
Agent server host. Default |
|
Names this run inside each case’s |
|
HTTP basic auth, when the server has
|
|
|
|
Seconds to wait for one agent run. Default |
The run prints a header, one line per case as it finishes, and a count:
bakery-email-assistant: 26 cases tagged baseline against http://localhost:25100
PASS order_single_item
FAIL missing_quantity — order_id: expected '', got 'ord-0007'
...
25/26 passed
Exit |
Constant |
Meaning |
|---|---|---|
|
|
Every case passed. |
|
|
At least one case failed. |
|
|
The run never reached a verdict: the file would not load, or the run itself broke. |
The constants live in kavalai.eval.eval_runner, so a test asserting on an
exit code names the meaning rather than the number.
A failing agent call is not an EXIT_ERROR. It fails its own case with the
error as the reason and the run continues, so one unreachable case cannot end a
suite.
From Python¶
The console script is a thin wrapper over four public pieces.
load_suite and run_suite¶
from kavalai.eval import load_suite, run_suite
suite = load_suite("examples/bakery/eval_cases.yaml")
results = await run_suite(
suite,
base_url="http://localhost:25100",
tag="ci",
on_result=lambda r: print(r.name, "ok" if r else r.reason),
)
run_suite() takes base_url, username, password,
timeout, judge_model, tag, transport and on_result, and
returns one EvalResult per case in file order.
on_result is called with each verdict as it arrives — it is how the CLI
prints progress, and how a test can stream one.
The evaluators¶
from kavalai.eval import JudgeEvaluator, SimpleEvaluator
simple = SimpleEvaluator("http://localhost:25000", tag="ci")
judge = JudgeEvaluator(
"http://localhost:25000",
tag="ci",
model="openai/gpt-5.6-luna",
)
Both take base_url (no default), username, password, timeout,
tag and transport; JudgeEvaluator adds model,
llm_client and prompt. transport is an httpx transport, which is
what lets a test serve the requests with no network at all.
evaluate(inputs, expected, name=...) runs one case and returns its verdict.
It raises only for a judged case with no criterion; everything else — a refused
connection, a rejected input, a judge that fell over — comes back as a failed
result with the reason attached.
Overriding the judge:
modelA
provider/modelname resolved throughmake_client()on first use (Model providers). Nothing is built until a case is actually judged, so a run of literal cases needs no API key.llm_clientA ready-made
BaseLlmClient, used instead ofmodel.promptThe grading prompt, which must accept
{inputs},{output}and{criterion}. The default instructs the judge to grade the stated criterion and nothing else, and to answer with aJudgeVerdict—passedand a one-sentencereasonwhen it is false.
EvalResult¶
Field |
Meaning |
|---|---|
|
The case name. |
|
Whether the answer satisfied the expectation. |
|
Why it failed; empty when it passed. Every failing matcher, joined with
|
|
What was sent to the agent. |
|
What the agent answered, or |
check_output¶
The matcher engine is exported on its own, for asserting on a payload you already have — a recorded answer, a fixture, an object built in a test — without a server:
from kavalai.eval import check_output
failures = check_output(
{"status": "needs_details", "order_id": ""},
{"status": "needs_details", "order_id": {"equals": ""}},
)
assert not failures, failures
It returns a list of failure messages, empty when everything matched.
Sessions¶
Each case runs in a fresh session, recorded by the agent server under
eval:{tag}:{case} # with --tag
eval:{case} # without
built by AgentEvaluator.external_id. Sessions are written only when the
server has an AgentService; the evaluators
behave identically when it does not. eval: is the reserved prefix the
backoffice Conversations page filters on — see Observability.
Environment¶
kavalai.eval reads no environment variables of its own. The base URL,
the auth pair, the judging model and the timeout are all arguments, which is
what lets a suite run from a notebook or a test without a hidden dependency on
the shell.
What does read the environment is the provider client a judged case builds —
OPENAI_API_KEY and its equivalents (Configuration). That is why a run with
judged cases is written dotenv run kavalai-eval … while a run of purely
literal cases needs nothing at all.