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.
- {agentic_evals-0.2.0 → agentic_evals-0.3.0}/CHANGELOG.md +26 -0
- {agentic_evals-0.2.0 → agentic_evals-0.3.0}/PKG-INFO +39 -1
- {agentic_evals-0.2.0 → agentic_evals-0.3.0}/README.md +38 -0
- {agentic_evals-0.2.0 → agentic_evals-0.3.0}/pyproject.toml +1 -1
- {agentic_evals-0.2.0 → agentic_evals-0.3.0}/src/agentic_evals/__init__.py +6 -1
- agentic_evals-0.3.0/src/agentic_evals/packs/__init__.py +15 -0
- agentic_evals-0.3.0/src/agentic_evals/packs/builtin/factual-qa.yaml +27 -0
- agentic_evals-0.3.0/src/agentic_evals/packs/builtin/json-output-contract.yaml +23 -0
- agentic_evals-0.3.0/src/agentic_evals/packs/builtin/tool-use-correctness.yaml +27 -0
- agentic_evals-0.3.0/src/agentic_evals/packs/library.py +17 -0
- agentic_evals-0.3.0/src/agentic_evals/packs/loader.py +48 -0
- {agentic_evals-0.2.0 → agentic_evals-0.3.0}/src/agentic_evals/scorers/rubric.py +9 -2
- agentic_evals-0.3.0/src/agentic_evals/skills/define-a-release-gate/SKILL.md +59 -0
- agentic_evals-0.3.0/src/agentic_evals/skills/instrument-a-trace/SKILL.md +67 -0
- agentic_evals-0.3.0/src/agentic_evals/skills/size-a-test-suite/SKILL.md +59 -0
- agentic_evals-0.3.0/src/agentic_evals/skills/write-a-scorer/SKILL.md +64 -0
- agentic_evals-0.3.0/tests/test_packs.py +71 -0
- {agentic_evals-0.2.0 → agentic_evals-0.3.0}/tests/test_scorers_rubric.py +11 -7
- agentic_evals-0.3.0/tests/test_skills.py +40 -0
- {agentic_evals-0.2.0 → agentic_evals-0.3.0}/uv.lock +1 -1
- {agentic_evals-0.2.0 → agentic_evals-0.3.0}/.github/workflows/ci.yml +0 -0
- {agentic_evals-0.2.0 → agentic_evals-0.3.0}/.github/workflows/release-pypi.yml +0 -0
- {agentic_evals-0.2.0 → agentic_evals-0.3.0}/.github/workflows/release-testpypi.yml +0 -0
- {agentic_evals-0.2.0 → agentic_evals-0.3.0}/.gitignore +0 -0
- {agentic_evals-0.2.0 → agentic_evals-0.3.0}/LICENSE +0 -0
- {agentic_evals-0.2.0 → agentic_evals-0.3.0}/src/agentic_evals/evaluators.py +0 -0
- {agentic_evals-0.2.0 → agentic_evals-0.3.0}/src/agentic_evals/gate.py +0 -0
- {agentic_evals-0.2.0 → agentic_evals-0.3.0}/src/agentic_evals/models.py +0 -0
- {agentic_evals-0.2.0 → agentic_evals-0.3.0}/src/agentic_evals/py.typed +0 -0
- {agentic_evals-0.2.0 → agentic_evals-0.3.0}/src/agentic_evals/runner.py +0 -0
- {agentic_evals-0.2.0 → agentic_evals-0.3.0}/src/agentic_evals/scorers/__init__.py +0 -0
- {agentic_evals-0.2.0 → agentic_evals-0.3.0}/src/agentic_evals/scorers/registry.py +0 -0
- {agentic_evals-0.2.0 → agentic_evals-0.3.0}/src/agentic_evals/scorers/text.py +0 -0
- {agentic_evals-0.2.0 → agentic_evals-0.3.0}/src/agentic_evals/scorers/trajectory.py +0 -0
- {agentic_evals-0.2.0 → agentic_evals-0.3.0}/tests/live_eval_target.py +0 -0
- {agentic_evals-0.2.0 → agentic_evals-0.3.0}/tests/test_evaluators.py +0 -0
- {agentic_evals-0.2.0 → agentic_evals-0.3.0}/tests/test_gate.py +0 -0
- {agentic_evals-0.2.0 → agentic_evals-0.3.0}/tests/test_models.py +0 -0
- {agentic_evals-0.2.0 → agentic_evals-0.3.0}/tests/test_runner.py +0 -0
- {agentic_evals-0.2.0 → agentic_evals-0.3.0}/tests/test_scorers_registry.py +0 -0
- {agentic_evals-0.2.0 → agentic_evals-0.3.0}/tests/test_scorers_text.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
|
17
|
+
*, output: str, reference: str | None = None, input_: object = None, threshold: float = 0.8
|
|
18
18
|
) -> EvaluationContext:
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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",
|
|
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",
|
|
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",
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|