Evaluation API¶
kavalai.eval grades an agent server that is already running. It
speaks HTTP and discovers the agent’s input and output types from the server’s
OpenAPI specification through AgentClient, so it knows
nothing about the workflow engine, the YAML graph or the database behind it.
Two evaluators share that plumbing and differ only in how they judge an answer:
SimpleEvaluator compares it with expected values and
calls no model at all, while JudgeEvaluator asks a model
whether it satisfies a plain-language criterion. Either can be called straight
from a test.
For the ideas, read Evaluation & acceptance testing; for the case-file keys and the command-line flags, Evaluation YAML & CLI.
Shared plumbing¶
Shared plumbing for the evaluators: one agent call, one result.
Copyright 2026 OÜ KAVAL AI (registry code 17393877)
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
-
class kavalai.eval.base.EvalResult(*, name: str, passed: bool, reason: str =
'', inputs: dict[str, Any] ={}, output: dict[str, Any] | None =None)[source]¶ Bases:
BaseModelThe verdict on a single case.
- Variables:¶
- name : str¶
Name of the case, for reporting.
- passed : bool¶
Whether the agent’s answer satisfied the expectation.
- reason : str¶
Why it failed; empty when it passed.
- inputs : dict[str, Any]¶
What was sent to the agent.
- output : dict[str, Any] | None¶
What the agent answered, or
Noneif the run never got that far (a connection error, an input that does not fit the agent’s input type).
-
model_config : ClassVar[ConfigDict] =
{}¶ Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
-
class kavalai.eval.base.AgentEvaluator(base_url: str, username: str | None =
None, password: str | None =None, timeout: float =120.0, tag: str | None =None, transport: AsyncBaseTransport | None =None)[source]¶ Bases:
objectBase class for evaluators that call a running agent server.
It owns one
AgentClient, which discovers the agent’s input and output types from the server’s OpenAPI spec on first use. Subclasses only have to decide whether an answer is good.- Parameters:¶
- base_url: str¶
Where the agent server listens. There is no default: the agent under evaluation is named on the command line, never in a case file, so a suite cannot quietly grade the wrong server.
- username: str | None =
None¶ HTTP Basic Auth user, if the server requires one.
- password: str | None =
None¶ HTTP Basic Auth password.
- timeout: float =
120.0¶ Seconds to wait for one agent run.
- tag: str | None =
None¶ Names this evaluation run inside the
external_ideach case is recorded under — a model version, a prompt variant, a build. It is what tells one run’s sessions from another’s afterwards.- transport: AsyncBaseTransport | None =
None¶ Optional httpx transport — in tests, this serves the requests without a network.
- external_id(name: str) str[source]¶
The identifier one case’s session is recorded under.
eval:is the reserved prefix the backoffice sessions page filters on; the tag, when there is one, separates this run’s sessions from the next run’s, which is what makes two variants comparable after the fact.
-
async run_agent(inputs: dict[str, Any], external_id: str | None =
None) BaseModel[source]¶ Send one input to the agent and return its output.
Each call starts a fresh session, so cases cannot leak conversation history into each other.
inputsis validated against the agent’s own input type before it is sent, which turns a mistyped field into a clear error instead of a puzzling answer.
Literal comparison¶
Check an agent’s answer against expected values, without a model.
Copyright 2026 OÜ KAVAL AI (registry code 17393877)
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
- kavalai.eval.simple_evaluator.is_matcher_spec(spec: Any) bool[source]¶
Whether
specis a matcher mapping rather than a literal value.A dict is read as matchers only when every key is a known matcher name, so an agent that genuinely answers with a dict can still be compared for equality.
- kavalai.eval.simple_evaluator.check_field(name: str, actual: Any, spec: Any) list[str][source]¶
Check one output field against its expectation.
- kavalai.eval.simple_evaluator.check_output(output: dict[str, Any], expected: dict[str, Any] | None) list[str][source]¶
Check every expected field of one agent answer.
Fields the expectation does not mention are ignored, so a case states what it cares about and nothing more.
-
class kavalai.eval.simple_evaluator.SimpleEvaluator(base_url: str, username: str | None =
None, password: str | None =None, timeout: float =120.0, tag: str | None =None, transport: AsyncBaseTransport | None =None)[source]¶ Bases:
AgentEvaluatorSend one input to a running agent and check the answer literally.
Use it wherever the right answer is a fact rather than a matter of phrasing — an extracted field, an id, a classification, a number that has to appear. It calls no model of its own, so it is fast, free and gives the same verdict every time.
Example
evaluator = SimpleEvaluator("http://localhost:25000") result = await evaluator.evaluate( {"user_message": "Who is the president of Green Village?"}, {"agent_response": {"contains": "Thomas Cook"}}, ) assert result.passed, result.reason-
async evaluate(inputs: dict[str, Any], expected: dict[str, Any] | None =
None, name: str ='case') EvalResult[source]¶ Run one case and compare the answer with
expected.- Parameters:¶
- inputs: dict[str, Any]¶
Field values for the agent’s input type.
- expected: dict[str, Any] | None =
None¶ Output field name to expected value or matcher mapping. An empty expectation asserts only that the agent answered at all, which is a useful smoke test in its own right.
- name: str =
'case'¶ Case name, carried into the result.
- Returns:¶
The
EvalResultfor this case. A failed agent call is reported as a failure rather than raised, so one broken case cannot end a whole run.
-
async evaluate(inputs: dict[str, Any], expected: dict[str, Any] | None =
Model-graded comparison¶
Let a model decide whether an agent’s answer is acceptable.
Copyright 2026 OÜ KAVAL AI (registry code 17393877)
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
-
class kavalai.eval.judge_evaluator.JudgeVerdict(*, passed: bool, reason: str =
'')[source]¶ Bases:
BaseModelWhat the judging model answers.
-
model_config : ClassVar[ConfigDict] =
{}¶ Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
-
model_config : ClassVar[ConfigDict] =
-
class kavalai.eval.judge_evaluator.JudgeEvaluator(base_url: str, username: str | None =
None, password: str | None =None, timeout: float =120.0, tag: str | None =None, transport: AsyncBaseTransport | None =None, model: str ='openai/gpt-5.6-luna', llm_client: BaseLlmClient | None =None, prompt: str ='You are grading one response of an AI agent under test.\n\nThe agent was given this input:\n{inputs}\n\nThe agent responded with:\n{output}\n\nThe response passes if, and only if, it satisfies this criterion:\n{criterion}\n\nJudge the criterion above and nothing else — do not add requirements of your\nown, and do not reward or punish style, length or politeness unless the\ncriterion asks about them. Set `passed` to true or false. When it is false,\n`reason` must say in one sentence what the response got wrong; when it is\ntrue, leave `reason` empty.\n', llm_parameters: dict | None =None)[source]¶ Bases:
AgentEvaluatorSend one input to a running agent and have a model grade the answer.
Use it where the right answer cannot be written down in advance — an explanation, a refusal, a comparison, anything where wording varies but the substance must hold. The expectation is a plain-language criterion, and a failing case comes back with the judge’s reason.
The judging model is only built when a case is actually judged, so a run of purely
SimpleEvaluatorcases needs no API key.- Parameters:¶
- base_url: str¶
Where the agent server listens.
- username: str | None =
None¶ HTTP Basic Auth user, if the server requires one.
- password: str | None =
None¶ HTTP Basic Auth password.
- timeout: float =
120.0¶ Seconds to wait for one agent run.
- tag: str | None =
None¶ Names this run inside each case’s
external_id.- transport: AsyncBaseTransport | None =
None¶ Optional httpx transport, used by tests.
- model: str =
'openai/gpt-5.6-luna'¶ provider/modelof the judge.- llm_client: BaseLlmClient | None =
None¶ A ready-made judge client, overriding
model.- llm_parameters: dict | None =
None¶ llm_kwargsfor the judge (temperature, timeouts, …) —kavalai-evalpasses theKAVALAI_LLM_*defaults here.- prompt: str =
'You are grading one response of an AI agent under test.\n\nThe agent was given this input:\n{inputs}\n\nThe agent responded with:\n{output}\n\nThe response passes if, and only if, it satisfies this criterion:\n{criterion}\n\nJudge the criterion above and nothing else — do not add requirements of your\nown, and do not reward or punish style, length or politeness unless the\ncriterion asks about them. Set `passed` to true or false. When it is false,\n`reason` must say in one sentence what the response got wrong; when it is\ntrue, leave `reason` empty.\n'¶ The grading prompt; must accept
{inputs},{output}and{criterion}.
Example
evaluator = JudgeEvaluator("http://localhost:25000") result = await evaluator.evaluate( {"user_message": "What is the village's annual budget?"}, "The answer says the information is not available " "instead of inventing a number.", ) assert result.passed, result.reason- property llm_client : BaseLlmClient¶
The judging model, built on first use.
- build_prompt(inputs: dict[str, Any], output: dict[str, Any], criterion: str) str[source]¶
Render the grading prompt for one case.
- async judge(inputs: dict[str, Any], output: dict[str, Any], criterion: str) JudgeVerdict[source]¶
Ask the judging model whether one answer meets the criterion.
-
async evaluate(inputs: dict[str, Any], expected: str | None =
None, name: str ='case') EvalResult[source]¶ Run one case and let the judging model grade the answer.
- Parameters:¶
- Returns:¶
The
EvalResultfor this case, itsreasontaken from the judge when the case failed.- Raises:¶
ValueError –
expectedis missing. A judged case with nothing to judge against would pass on anything at all.
Running a file of cases¶
Run a YAML file of cases against a running agent server.
Copyright 2026 OÜ KAVAL AI (registry code 17393877)
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
The file looks like this:
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 it does not know instead of inventing one.
The file says nothing about which server to run against — that is named on the command line, so the same cases can be pointed at a laptop, a staging deployment or two model versions in turn without being edited:
uv run --env-file .env kavalai-eval cases.yaml --port 25000 --tag gpt-5.6-luna
-
class kavalai.eval.eval_runner.EvalCase(*, name: str, type: 'simple' | 'judge' =
'simple', input: dict[str, Any], expected: dict[str, Any] | str | None =None)[source]¶ Bases:
BaseModelOne input, and what its answer has to look like.
- Variables:¶
- name : str¶
How the case is reported.
- type : Literal['simple', 'judge']¶
simplecompares the answer with expected values;judgeasks a model whether the answer is acceptable.- input : dict[str, Any]¶
Field values for the agent’s input type.
- expected : dict[str, Any] | str | None¶
A mapping of output field to expected value or matcher for a simple case; a plain-language criterion for a judged one.
-
model_config : ClassVar[ConfigDict] =
{'extra': 'forbid'}¶ Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
-
class kavalai.eval.eval_runner.EvalSuite(*, name: str, judge_model: str =
'openai/gpt-5.6-luna', cases: list[EvalCase])[source]¶ Bases:
BaseModelA named list of cases, and the model that judges the judged ones.
Deliberately no
base_url: which agent a suite is run against is a property of the run, not of the cases.-
model_config : ClassVar[ConfigDict] =
{'extra': 'forbid'}¶ Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
-
model_config : ClassVar[ConfigDict] =
- kavalai.eval.eval_runner.load_suite(path: str | Path) EvalSuite[source]¶
Read and validate a suite file.
-
async kavalai.eval.eval_runner.run_suite(suite: EvalSuite, base_url: str, username: str | None =
None, password: str | None =None, timeout: float =120.0, judge_model: str | None =None, judge_parameters: dict | None =None, tag: str | None =None, transport: Any | None =None, on_result: Callable[[EvalResult], None] | None =None) list[EvalResult][source]¶ Run every case in
suite, in order, against a running agent.The cases run one at a time: an evaluation that is easy to read while it runs is worth more than one that finishes a few seconds sooner.
- Parameters:¶
- suite: EvalSuite¶
The cases to run.
- base_url: str¶
Where the agent server listens.
- username: str | None =
None¶ HTTP Basic Auth user, if the server requires one.
- password: str | None =
None¶ HTTP Basic Auth password.
- timeout: float =
120.0¶ Seconds to wait for one agent run.
- judge_model: str | None =
None¶ provider/modelof the judge, overriding the suite’s.- judge_parameters: dict | None =
None¶ llm_kwargsfor the judge.- tag: str | None =
None¶ Names this run inside each case’s
external_id, so the sessions of two runs — two model versions, two prompts — can be told apart in the agent database afterwards.- transport: Any | None =
None¶ Optional httpx transport, used by tests.
- on_result: Callable[[EvalResult], None] | None =
None¶ Called with each result as it arrives, for progress output.
- Returns:¶
One
EvalResultper case, in file order.
- kavalai.eval.eval_runner.format_result(result: EvalResult) str[source]¶
One result as a single report line.
- kavalai.eval.eval_runner.format_summary(results: list[EvalResult]) str[source]¶
The closing line of a run.
-
kavalai.eval.eval_runner.parse_args(argv: list[str] | None =
None) Namespace[source]¶ Parse the command line.
- kavalai.eval.eval_runner.resolve_base_url(host: str, port: int) str[source]¶
Build a base URL from the host and port flags.
-
kavalai.eval.eval_runner.main(argv: list[str] | None =
None) int[source]¶ Entry point of the
kavalai-evalconsole script.- Returns:¶
0when every case passed,1when a case failed, and2when the run never reached a verdict — a CI job needs the third to tell “the suite is broken” from “the agent is wrong”.