agentic-evals 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/CHANGELOG.md +26 -0
  2. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/PKG-INFO +39 -1
  3. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/README.md +38 -0
  4. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/pyproject.toml +1 -1
  5. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/src/agentic_evals/__init__.py +6 -1
  6. agentic_evals-0.3.0/src/agentic_evals/packs/__init__.py +15 -0
  7. agentic_evals-0.3.0/src/agentic_evals/packs/builtin/factual-qa.yaml +27 -0
  8. agentic_evals-0.3.0/src/agentic_evals/packs/builtin/json-output-contract.yaml +23 -0
  9. agentic_evals-0.3.0/src/agentic_evals/packs/builtin/tool-use-correctness.yaml +27 -0
  10. agentic_evals-0.3.0/src/agentic_evals/packs/library.py +17 -0
  11. agentic_evals-0.3.0/src/agentic_evals/packs/loader.py +48 -0
  12. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/src/agentic_evals/scorers/rubric.py +9 -2
  13. agentic_evals-0.3.0/src/agentic_evals/skills/define-a-release-gate/SKILL.md +59 -0
  14. agentic_evals-0.3.0/src/agentic_evals/skills/instrument-a-trace/SKILL.md +67 -0
  15. agentic_evals-0.3.0/src/agentic_evals/skills/size-a-test-suite/SKILL.md +59 -0
  16. agentic_evals-0.3.0/src/agentic_evals/skills/write-a-scorer/SKILL.md +64 -0
  17. agentic_evals-0.3.0/tests/test_packs.py +71 -0
  18. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/tests/test_scorers_rubric.py +11 -7
  19. agentic_evals-0.3.0/tests/test_skills.py +40 -0
  20. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/uv.lock +1 -1
  21. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/.github/workflows/ci.yml +0 -0
  22. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/.github/workflows/release-pypi.yml +0 -0
  23. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/.github/workflows/release-testpypi.yml +0 -0
  24. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/.gitignore +0 -0
  25. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/LICENSE +0 -0
  26. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/src/agentic_evals/evaluators.py +0 -0
  27. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/src/agentic_evals/gate.py +0 -0
  28. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/src/agentic_evals/models.py +0 -0
  29. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/src/agentic_evals/py.typed +0 -0
  30. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/src/agentic_evals/runner.py +0 -0
  31. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/src/agentic_evals/scorers/__init__.py +0 -0
  32. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/src/agentic_evals/scorers/registry.py +0 -0
  33. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/src/agentic_evals/scorers/text.py +0 -0
  34. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/src/agentic_evals/scorers/trajectory.py +0 -0
  35. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/tests/live_eval_target.py +0 -0
  36. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/tests/test_evaluators.py +0 -0
  37. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/tests/test_gate.py +0 -0
  38. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/tests/test_models.py +0 -0
  39. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/tests/test_runner.py +0 -0
  40. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/tests/test_scorers_registry.py +0 -0
  41. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/tests/test_scorers_text.py +0 -0
  42. {agentic_evals-0.2.0 → agentic_evals-0.3.0}/tests/test_scorers_trajectory.py +0 -0
@@ -4,6 +4,32 @@ All notable changes to this project will be documented here.
4
4
 
5
5
  This project follows [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## 0.3.0 - 2026-09-13
8
+
9
+ ### Added
10
+
11
+ - `agentic_evals.skills`: methodology playbooks (`SKILL.md` cards --
12
+ Trigger/Do/Avoid/Check/Risk, matching the format Braintrust's
13
+ `eval-library/skills` uses), scoped to this package's own API:
14
+ `write-a-scorer`, `define-a-release-gate`, `size-a-test-suite`,
15
+ `instrument-a-trace`. These are guidance documents, not runnable code --
16
+ consumable by a human or by a coding agent's own skill mechanism.
17
+ - `agentic_evals.packs`: runnable eval-config bundles -- a `TestSuite`
18
+ paired with the scorer names it needs, loaded from a single YAML/JSON
19
+ file. `load_pack()`/`load_builtin_pack()` validate that every
20
+ `required_scorers` entry resolves in the registry before returning.
21
+ Ships 3 built-in packs: `tool-use-correctness`, `factual-qa`,
22
+ `json-output-contract`.
23
+
24
+ ### Fixed
25
+
26
+ - `LLMRubricEvaluator` now reads its reference material (the expert
27
+ answer for `FACTUALITY`, candidate B for `BATTLE`) from
28
+ `EvaluatorConfig.config["reference"]` instead of `case.expected_output`.
29
+ Reusing `expected_output` collided with `evaluate_suite`'s hardcoded
30
+ exact-match check, which fires whenever that field is set -- silently
31
+ forcing every rubric-graded case to also require a literal string match.
32
+
7
33
  ## 0.2.0 - 2026-09-13
8
34
 
9
35
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentic-evals
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: A standalone, framework-agnostic evaluation and scoring engine for LLM/agent outputs — extracted from AgenticLens's evaluation module.
5
5
  Project-URL: Homepage, https://github.com/DeepAgentLabs/agentic-evals
6
6
  Project-URL: Repository, https://github.com/DeepAgentLabs/agentic-evals
@@ -275,6 +275,44 @@ Python targets execute local code and HTTP targets can reach arbitrary
275
275
  URLs. Only point them at trusted suite files and trusted target
276
276
  definitions.
277
277
 
278
+ ## Eval packs
279
+
280
+ A pack bundles a `TestSuite` with the scorer names it needs into one
281
+ shareable YAML/JSON file:
282
+
283
+ ```python
284
+ from agentic_evals import (
285
+ EvalSpan,
286
+ EvalTrace,
287
+ EvaluationSample,
288
+ default_registry,
289
+ evaluate_suite,
290
+ load_builtin_pack,
291
+ )
292
+
293
+ pack = load_builtin_pack("tool-use-correctness") # validates required_scorers up front
294
+ sample = EvaluationSample(
295
+ case_id="refund-status-lookup",
296
+ output="Your refund is on its way.",
297
+ trace=EvalTrace(spans=[EvalSpan(tool_name="lookup_refund")]),
298
+ )
299
+ report = evaluate_suite(pack.to_suite(), [sample], registry=default_registry())
300
+ ```
301
+
302
+ Ships 3 built-in packs (`list_builtin_packs()`): `tool-use-correctness`,
303
+ `json-output-contract` (both runnable with `default_registry()`), and
304
+ `factual-qa` (needs a registry with an `LLMRubricEvaluator` registered,
305
+ since it uses the `FACTUALITY` rubric). Load your own with `load_pack(path)`.
306
+
307
+ ## Skills
308
+
309
+ `agentic_evals/skills/` ships methodology playbooks (`SKILL.md` cards --
310
+ Trigger/Do/Avoid/Check/Risk), not runnable code: `write-a-scorer`,
311
+ `define-a-release-gate`, `size-a-test-suite`, `instrument-a-trace`. These
312
+ document how to use this package well and are meant to be read directly,
313
+ or picked up by a coding agent's own skill mechanism -- distinct from
314
+ "packs" above, which are runnable configuration.
315
+
278
316
  ## Using it with AgenticLens's own traces
279
317
 
280
318
  If you already have an AgenticLens `Run` (from its instrumentation API or
@@ -219,6 +219,44 @@ Python targets execute local code and HTTP targets can reach arbitrary
219
219
  URLs. Only point them at trusted suite files and trusted target
220
220
  definitions.
221
221
 
222
+ ## Eval packs
223
+
224
+ A pack bundles a `TestSuite` with the scorer names it needs into one
225
+ shareable YAML/JSON file:
226
+
227
+ ```python
228
+ from agentic_evals import (
229
+ EvalSpan,
230
+ EvalTrace,
231
+ EvaluationSample,
232
+ default_registry,
233
+ evaluate_suite,
234
+ load_builtin_pack,
235
+ )
236
+
237
+ pack = load_builtin_pack("tool-use-correctness") # validates required_scorers up front
238
+ sample = EvaluationSample(
239
+ case_id="refund-status-lookup",
240
+ output="Your refund is on its way.",
241
+ trace=EvalTrace(spans=[EvalSpan(tool_name="lookup_refund")]),
242
+ )
243
+ report = evaluate_suite(pack.to_suite(), [sample], registry=default_registry())
244
+ ```
245
+
246
+ Ships 3 built-in packs (`list_builtin_packs()`): `tool-use-correctness`,
247
+ `json-output-contract` (both runnable with `default_registry()`), and
248
+ `factual-qa` (needs a registry with an `LLMRubricEvaluator` registered,
249
+ since it uses the `FACTUALITY` rubric). Load your own with `load_pack(path)`.
250
+
251
+ ## Skills
252
+
253
+ `agentic_evals/skills/` ships methodology playbooks (`SKILL.md` cards --
254
+ Trigger/Do/Avoid/Check/Risk), not runnable code: `write-a-scorer`,
255
+ `define-a-release-gate`, `size-a-test-suite`, `instrument-a-trace`. These
256
+ document how to use this package well and are meant to be read directly,
257
+ or picked up by a coding agent's own skill mechanism -- distinct from
258
+ "packs" above, which are runnable configuration.
259
+
222
260
  ## Using it with AgenticLens's own traces
223
261
 
224
262
  If you already have an AgenticLens `Run` (from its instrumentation API or
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agentic-evals"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "A standalone, framework-agnostic evaluation and scoring engine for LLM/agent outputs — extracted from AgenticLens's evaluation module."
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -29,6 +29,7 @@ from agentic_evals.models import (
29
29
  TestCase,
30
30
  TestSuite,
31
31
  )
32
+ from agentic_evals.packs import EvalPack, list_builtin_packs, load_builtin_pack, load_pack
32
33
  from agentic_evals.runner import (
33
34
  evaluate_suite,
34
35
  load_samples,
@@ -59,7 +60,7 @@ from agentic_evals.scorers import (
59
60
  valid_json,
60
61
  )
61
62
 
62
- __version__ = "0.2.0"
63
+ __version__ = "0.3.0"
63
64
 
64
65
  __all__ = [
65
66
  "BATTLE",
@@ -70,6 +71,7 @@ __all__ = [
70
71
  "BusinessRuleEvaluator",
71
72
  "CallableEvaluator",
72
73
  "CaseEvaluation",
74
+ "EvalPack",
73
75
  "EvalSpan",
74
76
  "EvalTrace",
75
77
  "EvaluationContext",
@@ -101,6 +103,9 @@ __all__ = [
101
103
  "exact_match",
102
104
  "json_diff",
103
105
  "levenshtein_similarity",
106
+ "list_builtin_packs",
107
+ "load_builtin_pack",
108
+ "load_pack",
104
109
  "load_samples",
105
110
  "load_suite",
106
111
  "no_redundant_tool_calls",
@@ -0,0 +1,15 @@
1
+ """Runnable eval packs -- a `TestSuite` bundled with the scorer names it needs.
2
+
3
+ Distinct from `agentic_evals.skills`, which are methodology playbooks, not
4
+ runnable configuration.
5
+ """
6
+
7
+ from agentic_evals.packs.library import list_builtin_packs, load_builtin_pack
8
+ from agentic_evals.packs.loader import EvalPack, load_pack
9
+
10
+ __all__ = [
11
+ "EvalPack",
12
+ "list_builtin_packs",
13
+ "load_builtin_pack",
14
+ "load_pack",
15
+ ]
@@ -0,0 +1,27 @@
1
+ name: factual-qa
2
+ description: >
3
+ Grades a submitted answer against a reference answer for factual
4
+ consistency using the FACTUALITY rubric. Requires the caller to
5
+ register LLMRubricEvaluator("factuality", FACTUALITY, complete_fn=...)
6
+ since grading needs a real model call.
7
+ required_scorers:
8
+ - factuality
9
+ suite:
10
+ name: factual-qa
11
+ version: "1"
12
+ description: >
13
+ Compares a submitted answer against a reference answer for factual
14
+ consistency.
15
+ cases:
16
+ - id: capital-of-france
17
+ name: Answer states the correct capital
18
+ input: What is the capital of France?
19
+ # No expected_output here on purpose: evaluate_suite runs a
20
+ # hardcoded exact-match check whenever it's set, which would fight
21
+ # a rubric meant to allow near-matches. The reference answer for
22
+ # the rubric lives in the evaluator's own config instead.
23
+ evaluators:
24
+ - name: factuality
25
+ threshold: 0.8
26
+ config:
27
+ reference: Paris is the capital of France.
@@ -0,0 +1,23 @@
1
+ name: json-output-contract
2
+ description: >
3
+ Checks that an agent's output is valid JSON and matches an expected
4
+ structure -- catches both malformed JSON and structurally wrong
5
+ responses.
6
+ required_scorers:
7
+ - valid_json
8
+ - json_diff
9
+ suite:
10
+ name: json-output-contract
11
+ version: "1"
12
+ description: >
13
+ The agent must return a JSON object with the exact structure the
14
+ caller expects.
15
+ cases:
16
+ - id: structured-answer
17
+ name: Output is valid JSON matching the expected structure
18
+ expected_output: '{"answer": "42", "confidence": 0.9}'
19
+ evaluators:
20
+ - name: valid_json
21
+ threshold: 1.0
22
+ - name: json_diff
23
+ threshold: 1.0
@@ -0,0 +1,27 @@
1
+ name: tool-use-correctness
2
+ description: >
3
+ Checks that an agent calls exactly the tools a refund-lookup task
4
+ needs -- the required lookup tool, and never a destructive delete tool.
5
+ required_scorers:
6
+ - tool_call_precision
7
+ suite:
8
+ name: tool-use-correctness
9
+ version: "1"
10
+ description: >
11
+ An agent answering a refund-status question must call lookup_refund
12
+ and never delete_order.
13
+ cases:
14
+ - id: refund-status-lookup
15
+ name: Agent looks up the refund without deleting anything
16
+ input:
17
+ order_id: A123
18
+ required_tools:
19
+ - lookup_refund
20
+ forbidden_tools:
21
+ - delete_order
22
+ evaluators:
23
+ - name: tool_call_precision
24
+ threshold: 1.0
25
+ config:
26
+ allowed_tools:
27
+ - lookup_refund
@@ -0,0 +1,17 @@
1
+ from pathlib import Path
2
+
3
+ from agentic_evals.evaluators import EvaluatorRegistry
4
+ from agentic_evals.packs.loader import EvalPack, load_pack
5
+
6
+ _BUILTIN_DIR = Path(__file__).parent / "builtin"
7
+
8
+
9
+ def list_builtin_packs() -> tuple[str, ...]:
10
+ return tuple(sorted(path.stem for path in _BUILTIN_DIR.glob("*.yaml")))
11
+
12
+
13
+ def load_builtin_pack(name: str, *, registry: EvaluatorRegistry | None = None) -> EvalPack:
14
+ path = _BUILTIN_DIR / f"{name}.yaml"
15
+ if not path.is_file():
16
+ raise ValueError(f"unknown builtin eval pack {name!r}; available: {list_builtin_packs()}")
17
+ return load_pack(path, registry=registry)
@@ -0,0 +1,48 @@
1
+ """Runnable eval packs: a `TestSuite` bundled with the scorer names it needs.
2
+
3
+ Distinct from `agentic_evals.skills` (methodology playbooks) -- a pack is a
4
+ small, shareable, immediately-runnable configuration.
5
+ """
6
+
7
+ import json
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import yaml
12
+ from pydantic import BaseModel, Field
13
+
14
+ from agentic_evals.evaluators import EvaluatorRegistry
15
+ from agentic_evals.models import TestSuite
16
+ from agentic_evals.scorers.registry import default_registry
17
+
18
+
19
+ class EvalPack(BaseModel):
20
+ name: str
21
+ description: str = ""
22
+ suite: TestSuite
23
+ required_scorers: list[str] = Field(default_factory=list)
24
+
25
+ def to_suite(self) -> TestSuite:
26
+ return self.suite
27
+
28
+
29
+ def _load_data(path: Path) -> Any:
30
+ text = path.read_text(encoding="utf-8")
31
+ return json.loads(text) if path.suffix.lower() == ".json" else yaml.safe_load(text)
32
+
33
+
34
+ def load_pack(path: Path, *, registry: EvaluatorRegistry | None = None) -> EvalPack:
35
+ """Load an `EvalPack` from a YAML/JSON file, failing fast if a required scorer is missing.
36
+
37
+ `registry` defaults to `default_registry()`; pass your own (e.g. one
38
+ with an `LLMRubricEvaluator` registered) if the pack needs a scorer
39
+ that isn't in the default set.
40
+ """
41
+ pack = EvalPack.model_validate(_load_data(path))
42
+ available = (registry or default_registry()).names()
43
+ missing = [name for name in pack.required_scorers if name not in available]
44
+ if missing:
45
+ raise ValueError(
46
+ f"eval pack {pack.name!r} requires scorer(s) not available in the registry: {missing}"
47
+ )
48
+ return pack
@@ -139,7 +139,14 @@ MODERATION = RubricTemplate(
139
139
 
140
140
 
141
141
  class LLMRubricEvaluator(CallableEvaluator):
142
- """Grade a sample against a `RubricTemplate` using a caller-supplied model call."""
142
+ """Grade a sample against a `RubricTemplate` using a caller-supplied model call.
143
+
144
+ The rubric's reference material (the expert answer for `FACTUALITY`,
145
+ candidate B for `BATTLE`) comes from `EvaluatorConfig.config["reference"]`
146
+ -- deliberately *not* `case.expected_output`, since `evaluate_suite`
147
+ runs a hardcoded exact-match check whenever `expected_output` is set,
148
+ which would fight a rubric that's meant to allow near-matches.
149
+ """
143
150
 
144
151
  def __init__(
145
152
  self, name: str, template: RubricTemplate, complete_fn: Callable[[str], str]
@@ -147,7 +154,7 @@ class LLMRubricEvaluator(CallableEvaluator):
147
154
  def _judge(context: EvaluationContext) -> Score:
148
155
  prompt = template.render(
149
156
  output=context.sample.output,
150
- expected=context.case.expected_output,
157
+ expected=context.config.config.get("reference"),
151
158
  input=context.case.input,
152
159
  extra=context.config.config.get("extra", {}),
153
160
  )
@@ -0,0 +1,59 @@
1
+ ---
2
+ name: define-a-release-gate
3
+ description: Choose GateConfig thresholds and understand the cost/latency edge cases before gating a release on an EvaluationReport.
4
+ ---
5
+
6
+ # Define a release gate
7
+
8
+ ## Trigger
9
+
10
+ Use this when you're wiring `evaluate_gate(report, GateConfig(...))` into a
11
+ CI check or release decision and need to pick real threshold values, not
12
+ just accept the defaults.
13
+
14
+ ## Do
15
+
16
+ 1. Start from the strictest sensible defaults (`min_pass_rate=1.0`,
17
+ `min_average_score=1.0`, `max_failed_cases=0`) and loosen deliberately —
18
+ it's easier to justify relaxing a gate later than to explain why it was
19
+ never strict.
20
+ 2. Set `max_average_latency_ms`/`max_total_cost_usd` only when you have a
21
+ real budget in mind; leaving them `None` skips that check entirely
22
+ rather than silently passing with a made-up number.
23
+ 3. Read `GateDecision.reasons` and `.observed` in CI output, not just
24
+ `.passed` — `reasons` names exactly which threshold failed, which is
25
+ what someone debugging a red build actually needs.
26
+ 4. Treat a gate failure on cost/latency as at least as actionable as a
27
+ pass-rate failure — regressions there are often the first sign of a
28
+ prompt or retry-loop change before quality visibly degrades.
29
+
30
+ ## Avoid
31
+
32
+ - Don't set `max_total_cost_usd` on a suite where cost is routinely
33
+ unavailable for some cases (e.g. a live target that doesn't report
34
+ per-span cost) — the gate will fail every run with "cost is unavailable
35
+ or incomplete," which looks like a real regression but isn't.
36
+ - Don't loosen `min_pass_rate` to make a flaky suite pass — fix the
37
+ flaky case or evaluator instead; a gate that's routinely overridden
38
+ stops being trusted.
39
+ - Don't gate on a suite that mixes exploratory and release-blocking cases
40
+ in one `TestSuite` — split them, since one flaky exploratory case
41
+ shouldn't block a release.
42
+
43
+ ## Check
44
+
45
+ - `evaluate_gate`'s cost check treats an incomplete cost picture
46
+ (`summary.total_cost_usd is None`, or any case missing `cost_usd`) as
47
+ "unavailable," never as `$0.00` — confirm your traces actually populate
48
+ `estimated_cost_usd` before relying on this gate for cost control.
49
+ - The gate config you shipped to CI matches the one you tested locally —
50
+ `GateConfig` is a plain Pydantic model, so diff it explicitly rather than
51
+ trusting two configs "look the same."
52
+
53
+ ## Risk
54
+
55
+ A gate that only checks `pass_rate` can pass while cost or latency drifts
56
+ upward every release, since neither is checked unless you explicitly set
57
+ `max_average_latency_ms`/`max_total_cost_usd`. If cost or latency matters
58
+ for the product, set both thresholds, don't just default to the pass-rate
59
+ check.
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: instrument-a-trace
3
+ description: Build EvalTrace/EvalSpan correctly from your own agent's data, including how to normalize legacy or differently-shaped trace payloads with a trace_adapter.
4
+ ---
5
+
6
+ # Instrument a trace
7
+
8
+ ## Trigger
9
+
10
+ Use this when you're producing the `trace` field of an `EvaluationSample`
11
+ for the first time, or when a `TestCase`'s latency/cost/tool checks are
12
+ silently passing or failing in a way that doesn't match reality.
13
+
14
+ ## Do
15
+
16
+ 1. Populate `EvalTrace.total_latency_ms` and `.estimated_cost_usd`
17
+ directly as plain numbers — this is not a computed property here (it
18
+ is on some richer trace schemas). If you're deriving it from
19
+ timestamps or per-call costs, do that arithmetic yourself before
20
+ constructing the `EvalTrace`.
21
+ 2. Give every tool-calling `EvalSpan` a `tool_name` — `required_tools`,
22
+ `forbidden_tools`, `tool_call_precision`, and `tool_call_recall` all
23
+ filter on `span.tool_name is not None`; a span with `tool_name=None`
24
+ is invisible to every tool-based check.
25
+ 3. Put arguments a `required_tool_arguments` check needs under
26
+ `span.attributes["tool_args"]` — that's the exact key the built-in
27
+ check reads.
28
+ 4. If your traces come from a system with a different native shape (e.g.
29
+ timestamp-derived latency, per-span rather than trace-level cost), use
30
+ the `trace_adapter` parameter on `load_samples()`/`run_live_suite()`:
31
+ write one small function that maps your raw trace dict into
32
+ `EvalTrace`'s shape, and pass it once at the call site rather than
33
+ converting ad hoc everywhere you build a sample.
34
+ 5. Set `estimated_cost_usd=None` (the default) when cost genuinely isn't
35
+ known for a sample — every cost check in this package treats `None` as
36
+ "unavailable," never as `$0.00`. Never pass `0.0` to mean "unknown."
37
+
38
+ ## Avoid
39
+
40
+ - Don't leave `total_latency_ms` at its default `0.0` for a real sample —
41
+ every `max_latency_ms` check will trivially pass, which looks like a
42
+ release gate working when it's actually blind.
43
+ - Don't put tool arguments somewhere other than
44
+ `attributes["tool_args"]` and expect `required_tool_arguments` to find
45
+ them — it looks at that exact key, not the whole `attributes` dict.
46
+ - Don't silently drop a trace field your source system provides just
47
+ because `EvalSpan`/`EvalTrace` doesn't ask for it — pydantic ignores
48
+ unknown fields by default, so a payload shaped for a different schema
49
+ validates "successfully" while quietly losing the data you needed.
50
+
51
+ ## Check
52
+
53
+ - Build one `EvalTrace` by hand from a real sample and print it — confirm
54
+ `total_latency_ms`, `estimated_cost_usd`, and every span's `tool_name`
55
+ match what you know actually happened, not just that validation passed.
56
+ - If you wrote a `trace_adapter`, unit test it directly against a raw
57
+ payload sample, independent of `evaluate_suite` — a normalization bug
58
+ there fails silently (defaults to 0/None) rather than raising.
59
+
60
+ ## Risk
61
+
62
+ A trace payload that predates `EvalTrace`'s shape (timestamps instead of
63
+ `total_latency_ms`, per-span cost instead of trace-level) validates fine
64
+ and produces a *plausible-looking* report with latency/cost silently
65
+ defaulted to 0/None — this doesn't error, so it can pass code review and
66
+ still gate a release on wrong numbers. Always adapt at the boundary
67
+ (a `trace_adapter`), not by hoping downstream fields line up.
@@ -0,0 +1,59 @@
1
+ ---
2
+ name: size-a-test-suite
3
+ description: Decide how many TestCases a TestSuite needs and how to split criteria across them without bundling unrelated checks into one case.
4
+ ---
5
+
6
+ # Size a test suite
7
+
8
+ ## Trigger
9
+
10
+ Use this when you're building a new `TestSuite` and unsure whether to add
11
+ more `TestCase`s, add more `evaluators` to an existing case, or split one
12
+ case into several.
13
+
14
+ ## Do
15
+
16
+ 1. One `TestCase` per input/scenario, not per criterion — if the same
17
+ input needs three checks (contains a fact, calls the right tool, stays
18
+ under a latency budget), that's one `TestCase` with three entries in
19
+ `evaluators`/threshold fields, not three cases.
20
+ 2. Cover the boundary the criterion actually claims to check: a
21
+ `max_turns` case needs both a passing sample at the limit and a failing
22
+ one just over it, not just one happy-path sample.
23
+ 3. Include at least one adversarial or edge-case input per suite (empty
24
+ input, a refusal, a malformed tool response) — `TestCase.require
25
+ expectation`'s validator forces every case to declare *something* to
26
+ check, so use that to your advantage: even an edge case needs a real
27
+ expectation, not a placeholder.
28
+ 4. Give every `TestCase.id` a stable, descriptive slug (`refund-large-amount`,
29
+ not `case-7`) — `TestSuite` rejects duplicate IDs, and stable IDs are
30
+ what `EvaluationSample.case_id` and dataset splits key off of.
31
+
32
+ ## Avoid
33
+
34
+ - Don't cram every acceptance criterion for a feature into one
35
+ mega-`TestCase` with a dozen `evaluators` entries — when it fails, a
36
+ report should point at one clear thing, not require reading six scores
37
+ to find the one that mattered.
38
+ - Don't size a suite by "how many cases feels thorough" — size it by
39
+ "which distinct behaviors would a regression actually break," and stop
40
+ once you've covered them once each plus their edges.
41
+ - Don't let a suite silently grow stale: a case with an `expected_output`
42
+ that references a value your system no longer produces will fail forever
43
+ and gets ignored, which is worse than not having the case.
44
+
45
+ ## Check
46
+
47
+ - Every `TestCase` has at least one expectation set (`TestCase`'s own
48
+ validator enforces this) — if you're stuck picking one, that's a signal
49
+ the case doesn't yet test anything specific.
50
+ - Running the suite against a known-good sample set produces a 100% pass
51
+ rate; running it against a known-bad set fails on the case(s) you'd
52
+ expect, not a different one.
53
+
54
+ ## Risk
55
+
56
+ A suite that's easy to make pass by construction (loose thresholds, only
57
+ happy-path cases) gives false confidence in a release gate — it's the
58
+ gate's job to fail sometimes, so a suite that never produces a genuine
59
+ failure during development hasn't been tested itself.
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: write-a-scorer
3
+ description: Choose and implement the right kind of scorer (deterministic, embedding, LLM rubric, or trajectory) for a new eval criterion in agentic_evals.
4
+ ---
5
+
6
+ # Write a scorer
7
+
8
+ ## Trigger
9
+
10
+ Use this when you're adding a new grading criterion to a `TestCase` and
11
+ aren't sure whether to reach for a built-in scorer, write a custom
12
+ `CallableEvaluator`, or reach for an LLM judge.
13
+
14
+ ## Do
15
+
16
+ 1. Name one criterion per scorer. If you're tempted to score "correctness
17
+ and tone" in a single evaluator, that's two evaluators.
18
+ 2. Prefer `agentic_evals.scorers.text` first: `exact_match`,
19
+ `contains_all`/`contains_any`, `valid_json`, `json_diff`, `numeric_diff`
20
+ are deterministic, free, and never flaky — use them whenever the
21
+ criterion has an objectively checkable answer.
22
+ 3. Reach for `embedding_similarity` only when near-miss wording should
23
+ still pass and you already have an embedding call available; it needs
24
+ an `embed_fn` in `EvaluatorConfig.config` since this package never
25
+ calls an embedding API itself.
26
+ 4. Reach for `LLMRubricEvaluator` + a `RubricTemplate` only when the
27
+ criterion is genuinely subjective (factual consistency, summary
28
+ quality, pairwise preference). Supply your own `complete_fn` —
29
+ `agentic_evals` never calls a model.
30
+ 5. Reach for `agentic_evals.scorers.trajectory` (`tool_call_precision`,
31
+ `tool_call_recall`, `no_redundant_tool_calls`, `trajectory_efficiency`)
32
+ when the criterion is about *how* the agent got the answer, not just
33
+ the final text.
34
+ 6. If none of the above fit, write a plain `CallableEvaluator(name, fn)` —
35
+ `fn` takes an `EvaluationContext` and returns a `Score` or `list[Score]`.
36
+
37
+ ## Avoid
38
+
39
+ - Don't wrap an LLM judge around something a deterministic check already
40
+ covers (`valid_json`, `json_diff`) — it's slower, costs money, and can
41
+ disagree with itself between runs.
42
+ - Don't average unrelated criteria into a single `Score.value` — register
43
+ them as separate evaluators so a report shows exactly what failed.
44
+ - Don't hardcode a specific model or embedding provider inside a scorer
45
+ function — take `complete_fn`/`embed_fn` as a parameter, the way every
46
+ built-in rubric/embedding scorer does.
47
+
48
+ ## Check
49
+
50
+ - The scorer raises a clear `ValueError` when its required input
51
+ (`expected_output`, `case.input`, a trace with tool calls) is missing,
52
+ rather than silently returning a meaningless score.
53
+ - `Score.value` stays in `[0, 1]`; a hard pass/fail scorer returns exactly
54
+ `0.0` or `1.0`, a graded one returns a genuine gradient.
55
+ - You've tested the scorer directly against at least one passing and one
56
+ failing sample (call it with a hand-built `EvaluationContext`), not only
57
+ through a full suite run.
58
+
59
+ ## Risk
60
+
61
+ An LLM-graded rubric is only as good as its verdict parsing —
62
+ `RubricTemplate.parse_verdict` raises if the model's completion doesn't
63
+ contain one of the expected tokens. Don't swallow that exception upstream:
64
+ a silently defaulted score is worse than a visible failure.
@@ -0,0 +1,71 @@
1
+ import pytest
2
+
3
+ from agentic_evals import (
4
+ FACTUALITY,
5
+ EvalSpan,
6
+ EvalTrace,
7
+ EvaluationSample,
8
+ EvaluatorRegistry,
9
+ LLMRubricEvaluator,
10
+ default_registry,
11
+ evaluate_suite,
12
+ list_builtin_packs,
13
+ load_builtin_pack,
14
+ )
15
+
16
+
17
+ def test_list_builtin_packs_includes_the_shipped_packs() -> None:
18
+ names = list_builtin_packs()
19
+ assert names == tuple(sorted(names))
20
+ assert set(names) >= {"tool-use-correctness", "factual-qa", "json-output-contract"}
21
+
22
+
23
+ def test_load_builtin_pack_rejects_unknown_name() -> None:
24
+ with pytest.raises(ValueError, match="unknown builtin eval pack"):
25
+ load_builtin_pack("does-not-exist")
26
+
27
+
28
+ def test_tool_use_correctness_pack_runs_end_to_end() -> None:
29
+ pack = load_builtin_pack("tool-use-correctness")
30
+ sample = EvaluationSample(
31
+ case_id="refund-status-lookup",
32
+ output="Your refund is on its way.",
33
+ trace=EvalTrace(spans=[EvalSpan(tool_name="lookup_refund")]),
34
+ )
35
+
36
+ report = evaluate_suite(pack.to_suite(), [sample], registry=default_registry())
37
+
38
+ assert report.summary.pass_rate == 1.0
39
+
40
+
41
+ def test_json_output_contract_pack_runs_end_to_end() -> None:
42
+ pack = load_builtin_pack("json-output-contract")
43
+ sample = EvaluationSample(
44
+ case_id="structured-answer",
45
+ output='{"answer": "42", "confidence": 0.9}',
46
+ trace=EvalTrace(),
47
+ )
48
+
49
+ report = evaluate_suite(pack.to_suite(), [sample], registry=default_registry())
50
+
51
+ assert report.summary.pass_rate == 1.0
52
+
53
+
54
+ def test_factual_qa_pack_requires_a_registry_with_factuality_registered() -> None:
55
+ with pytest.raises(ValueError, match="factuality"):
56
+ load_builtin_pack("factual-qa") # default_registry() has no LLM-judge scorers
57
+
58
+ registry = EvaluatorRegistry()
59
+ registry.register(
60
+ LLMRubricEvaluator("factuality", FACTUALITY, complete_fn=lambda prompt: "(A)")
61
+ )
62
+ pack = load_builtin_pack("factual-qa", registry=registry)
63
+
64
+ sample = EvaluationSample(
65
+ case_id="capital-of-france",
66
+ output="Paris",
67
+ trace=EvalTrace(),
68
+ )
69
+ report = evaluate_suite(pack.to_suite(), [sample], registry=registry)
70
+
71
+ assert report.summary.pass_rate == 1.0
@@ -14,13 +14,17 @@ from agentic_evals import (
14
14
 
15
15
 
16
16
  def _context(
17
- *, output: str, expected_output: str | None, input_: object = None
17
+ *, output: str, reference: str | None = None, input_: object = None, threshold: float = 0.8
18
18
  ) -> EvaluationContext:
19
- case = TestCase(id="case-1", name="case", expected_output=expected_output, input=input_)
19
+ # Filler expectation unrelated to the rubric under test: TestCase
20
+ # requires at least one, and expected_output is deliberately not used
21
+ # here (see LLMRubricEvaluator's docstring on why).
22
+ case = TestCase(id="case-1", name="case", input=input_, max_turns=1_000_000)
20
23
  sample = EvaluationSample(case_id="case-1", output=output, trace=EvalTrace())
21
- return EvaluationContext(
22
- case=case, sample=sample, config=EvaluatorConfig(name="judge", threshold=0.8)
24
+ config = EvaluatorConfig(
25
+ name="judge", threshold=threshold, config={"reference": reference} if reference else {}
23
26
  )
27
+ return EvaluationContext(case=case, sample=sample, config=config)
24
28
 
25
29
 
26
30
  def test_rubric_template_renders_all_fields() -> None:
@@ -67,7 +71,7 @@ def test_battle_template_distinguishes_win_and_tie_tokens() -> None:
67
71
 
68
72
  def test_llm_rubric_evaluator_scores_using_complete_fn() -> None:
69
73
  evaluator = LLMRubricEvaluator("factuality", FACTUALITY, complete_fn=lambda prompt: "(A)")
70
- context = _context(output="Paris", expected_output="Paris is the capital of France")
74
+ context = _context(output="Paris", reference="Paris is the capital of France")
71
75
 
72
76
  scores = evaluator.evaluate(context)
73
77
 
@@ -81,7 +85,7 @@ def test_llm_rubric_evaluator_scores_using_complete_fn() -> None:
81
85
 
82
86
  def test_llm_rubric_evaluator_applies_threshold_from_config() -> None:
83
87
  evaluator = LLMRubricEvaluator("factuality", FACTUALITY, complete_fn=lambda prompt: "(C)")
84
- context = _context(output="Paris", expected_output="Paris is the capital of France")
88
+ context = _context(output="Paris", reference="Paris is the capital of France")
85
89
 
86
90
  score = evaluator.evaluate(context)[0]
87
91
 
@@ -91,7 +95,7 @@ def test_llm_rubric_evaluator_applies_threshold_from_config() -> None:
91
95
 
92
96
  def test_llm_rubric_evaluator_propagates_unparseable_completion() -> None:
93
97
  evaluator = LLMRubricEvaluator("factuality", FACTUALITY, complete_fn=lambda prompt: "unclear")
94
- context = _context(output="Paris", expected_output="Paris is the capital of France")
98
+ context = _context(output="Paris", reference="Paris is the capital of France")
95
99
 
96
100
  with pytest.raises(ValueError, match="could not parse a verdict"):
97
101
  evaluator.evaluate(context)
@@ -0,0 +1,40 @@
1
+ """Structure checks for the methodology skills shipped under `agentic_evals/skills/`.
2
+
3
+ These are documentation/guidance cards (Trigger/Do/Avoid/Check/Risk), not
4
+ runnable code -- this test verifies the format is intact, not the content.
5
+ """
6
+
7
+ from pathlib import Path
8
+
9
+ import yaml
10
+
11
+ SKILLS_DIR = Path(__file__).parent.parent / "src" / "agentic_evals" / "skills"
12
+ REQUIRED_SECTIONS = ["## Trigger", "## Do", "## Avoid", "## Check", "## Risk"]
13
+
14
+
15
+ def _skill_dirs() -> list[Path]:
16
+ return sorted(p for p in SKILLS_DIR.iterdir() if p.is_dir())
17
+
18
+
19
+ def test_at_least_one_skill_is_shipped() -> None:
20
+ assert _skill_dirs(), "expected at least one skill directory under agentic_evals/skills/"
21
+
22
+
23
+ def test_every_skill_has_a_well_formed_skill_md() -> None:
24
+ for skill_dir in _skill_dirs():
25
+ skill_md = skill_dir / "SKILL.md"
26
+ assert skill_md.is_file(), f"{skill_dir} is missing SKILL.md"
27
+
28
+ text = skill_md.read_text(encoding="utf-8")
29
+ assert text.startswith("---\n"), f"{skill_md} must start with YAML frontmatter"
30
+
31
+ _, frontmatter_raw, body = text.split("---\n", 2)
32
+ frontmatter = yaml.safe_load(frontmatter_raw)
33
+
34
+ assert frontmatter.get("name") == skill_dir.name, (
35
+ f"{skill_md}'s frontmatter name must match its directory name"
36
+ )
37
+ assert frontmatter.get("description"), f"{skill_md} must set a non-empty description"
38
+
39
+ for section in REQUIRED_SECTIONS:
40
+ assert section in body, f"{skill_md} is missing the {section!r} section"
@@ -9,7 +9,7 @@ resolution-markers = [
9
9
 
10
10
  [[package]]
11
11
  name = "agentic-evals"
12
- version = "0.2.0"
12
+ version = "0.3.0"
13
13
  source = { editable = "." }
14
14
  dependencies = [
15
15
  { name = "jsonschema" },
File without changes
File without changes