coder-eval 0.8.2__py3-none-any.whl
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.
- coder_eval/.gitattributes +2 -0
- coder_eval/__init__.py +3 -0
- coder_eval/agent.py +302 -0
- coder_eval/agents/__init__.py +41 -0
- coder_eval/agents/_logging.py +89 -0
- coder_eval/agents/antigravity_agent.py +811 -0
- coder_eval/agents/claude_code_agent.py +1701 -0
- coder_eval/agents/codex_agent.py +2055 -0
- coder_eval/agents/noop_agent.py +100 -0
- coder_eval/agents/registry.py +163 -0
- coder_eval/agents/watchdog.py +116 -0
- coder_eval/analysis.py +88 -0
- coder_eval/cli/__init__.py +87 -0
- coder_eval/cli/aggregate_command.py +153 -0
- coder_eval/cli/console.py +7 -0
- coder_eval/cli/evaluate_command.py +164 -0
- coder_eval/cli/plan_command.py +152 -0
- coder_eval/cli/report_command.py +121 -0
- coder_eval/cli/run_command.py +686 -0
- coder_eval/cli/run_helpers.py +137 -0
- coder_eval/cli/run_task_internal_command.py +210 -0
- coder_eval/cli/utils.py +37 -0
- coder_eval/config.py +212 -0
- coder_eval/criteria/__init__.py +163 -0
- coder_eval/criteria/_classification_aggregate.py +106 -0
- coder_eval/criteria/agent_judge.py +425 -0
- coder_eval/criteria/base.py +248 -0
- coder_eval/criteria/classification_match.py +107 -0
- coder_eval/criteria/command_executed.py +181 -0
- coder_eval/criteria/commands_efficiency.py +73 -0
- coder_eval/criteria/file_check.py +119 -0
- coder_eval/criteria/file_contains.py +85 -0
- coder_eval/criteria/file_exists.py +45 -0
- coder_eval/criteria/file_matches_regex.py +86 -0
- coder_eval/criteria/json_check.py +184 -0
- coder_eval/criteria/llm_judge.py +260 -0
- coder_eval/criteria/reference_comparison.py +130 -0
- coder_eval/criteria/run_command.py +220 -0
- coder_eval/criteria/skill_triggered.py +111 -0
- coder_eval/criteria/uipath_eval.py +223 -0
- coder_eval/errors/__init__.py +26 -0
- coder_eval/errors/agent.py +26 -0
- coder_eval/errors/budget.py +28 -0
- coder_eval/errors/categories.py +205 -0
- coder_eval/errors/categorization.py +240 -0
- coder_eval/errors/executor.py +124 -0
- coder_eval/errors/judge.py +12 -0
- coder_eval/errors/retry.py +211 -0
- coder_eval/errors/timeout.py +63 -0
- coder_eval/evaluation/__init__.py +10 -0
- coder_eval/evaluation/checker.py +260 -0
- coder_eval/evaluation/judge_anthropic.py +55 -0
- coder_eval/evaluation/judge_bedrock.py +114 -0
- coder_eval/evaluation/judge_context.py +497 -0
- coder_eval/evaluation/judge_models.py +53 -0
- coder_eval/evaluation/judge_persistence.py +291 -0
- coder_eval/evaluation/judge_usage.py +50 -0
- coder_eval/evaluation/sub_agent.py +231 -0
- coder_eval/evaluation/summaries.py +48 -0
- coder_eval/evaluation/verdict_tool.py +213 -0
- coder_eval/formatting.py +191 -0
- coder_eval/isolation/__init__.py +15 -0
- coder_eval/isolation/docker_runner.py +1253 -0
- coder_eval/logging_config.py +399 -0
- coder_eval/models/__init__.py +337 -0
- coder_eval/models/agent_config.py +373 -0
- coder_eval/models/container_paths.py +26 -0
- coder_eval/models/criteria.py +942 -0
- coder_eval/models/enums.py +121 -0
- coder_eval/models/experiment.py +314 -0
- coder_eval/models/judge.py +90 -0
- coder_eval/models/judge_defaults.py +11 -0
- coder_eval/models/limits.py +89 -0
- coder_eval/models/merge_strategy.py +132 -0
- coder_eval/models/mutations.py +95 -0
- coder_eval/models/results.py +787 -0
- coder_eval/models/routing.py +160 -0
- coder_eval/models/sandbox.py +371 -0
- coder_eval/models/tasks.py +631 -0
- coder_eval/models/telemetry.py +454 -0
- coder_eval/models/templates.py +108 -0
- coder_eval/orchestration/__init__.py +13 -0
- coder_eval/orchestration/batch.py +690 -0
- coder_eval/orchestration/config.py +111 -0
- coder_eval/orchestration/config_merge.py +431 -0
- coder_eval/orchestration/evaluation.py +94 -0
- coder_eval/orchestration/experiment.py +837 -0
- coder_eval/orchestration/overrides.py +206 -0
- coder_eval/orchestration/task_loader.py +437 -0
- coder_eval/orchestrator.py +2078 -0
- coder_eval/path_utils.py +79 -0
- coder_eval/plugins.py +81 -0
- coder_eval/pricing.py +169 -0
- coder_eval/py.typed +0 -0
- coder_eval/reports.py +1066 -0
- coder_eval/reports_experiment.py +761 -0
- coder_eval/reports_html.py +1659 -0
- coder_eval/reports_stats.py +250 -0
- coder_eval/resources/__init__.py +137 -0
- coder_eval/resources/default_experiment.yaml +85 -0
- coder_eval/resources/default_ignore_patterns.yaml +60 -0
- coder_eval/resources/tags.yaml +55 -0
- coder_eval/sandbox.py +1127 -0
- coder_eval/scoring/__init__.py +11 -0
- coder_eval/scoring/ast_similarity.py +38 -0
- coder_eval/scoring/complexity.py +115 -0
- coder_eval/scoring/quality.py +154 -0
- coder_eval/scoring/signature_similarity.py +57 -0
- coder_eval/scoring/similarity.py +103 -0
- coder_eval/scoring/token_similarity.py +44 -0
- coder_eval/simulation/__init__.py +27 -0
- coder_eval/simulation/termination.py +88 -0
- coder_eval/simulation/user_simulator.py +324 -0
- coder_eval/streaming/__init__.py +44 -0
- coder_eval/streaming/callbacks.py +57 -0
- coder_eval/streaming/collector.py +193 -0
- coder_eval/streaming/events.py +198 -0
- coder_eval/streaming/renderers.py +248 -0
- coder_eval/streaming/wire.py +115 -0
- coder_eval/telemetry.py +407 -0
- coder_eval/utils.py +517 -0
- coder_eval-0.8.2.dist-info/METADATA +242 -0
- coder_eval-0.8.2.dist-info/RECORD +127 -0
- coder_eval-0.8.2.dist-info/WHEEL +4 -0
- coder_eval-0.8.2.dist-info/entry_points.txt +5 -0
- coder_eval-0.8.2.dist-info/licenses/LICENSE +201 -0
- coder_eval-0.8.2.dist-info/licenses/NOTICE +18 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Base criterion checker interface with error handling."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import statistics
|
|
6
|
+
import traceback
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from functools import wraps
|
|
11
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
12
|
+
|
|
13
|
+
from coder_eval.errors import JudgeInfrastructureError
|
|
14
|
+
from coder_eval.models import BaseSuccessCriterion, CriterionAggregate, CriterionResult
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from coder_eval.models.results import TurnRecord
|
|
21
|
+
from coder_eval.models.routing import ApiRoute
|
|
22
|
+
from coder_eval.sandbox import Sandbox
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class CheckContext:
|
|
29
|
+
"""Live run context forwarded to every criterion checker's ``_check_impl``.
|
|
30
|
+
|
|
31
|
+
Bundles the pieces of orchestrator state that judge-style criteria
|
|
32
|
+
(``llm_judge`` / ``agent_judge``) need to route their own LLM calls. Carries
|
|
33
|
+
a live object (the resolved ``route``), so it is NOT a ``coder_eval.models``
|
|
34
|
+
Pydantic model — it never gets serialized into a result record.
|
|
35
|
+
|
|
36
|
+
Non-judge checkers receive it too (uniform ``_check_impl`` signature) and
|
|
37
|
+
ignore it.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
route: "ApiRoute | None" = None
|
|
41
|
+
reference_dir: "Path | None" = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def handle_criterion_errors(func: Callable[..., CriterionResult]) -> Callable[..., CriterionResult]:
|
|
45
|
+
"""Decorator to handle errors in criterion checkers.
|
|
46
|
+
|
|
47
|
+
Wraps checker methods to catch exceptions and return a failed
|
|
48
|
+
CriterionResult with error details instead of raising.
|
|
49
|
+
|
|
50
|
+
This is the CENTRALIZED error handling that was in evaluator.py.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
@wraps(func)
|
|
54
|
+
def wrapper(
|
|
55
|
+
self: Any,
|
|
56
|
+
criterion: BaseSuccessCriterion,
|
|
57
|
+
sandbox: "Sandbox",
|
|
58
|
+
reference_code: str | None = None,
|
|
59
|
+
turn_records: list["TurnRecord"] | None = None,
|
|
60
|
+
context: "CheckContext | None" = None,
|
|
61
|
+
) -> CriterionResult:
|
|
62
|
+
try:
|
|
63
|
+
return func(
|
|
64
|
+
self,
|
|
65
|
+
criterion,
|
|
66
|
+
sandbox,
|
|
67
|
+
reference_code,
|
|
68
|
+
turn_records=turn_records,
|
|
69
|
+
context=context,
|
|
70
|
+
)
|
|
71
|
+
except JudgeInfrastructureError:
|
|
72
|
+
# Judge infra failure is NOT an agent failure — do not score it 0.0.
|
|
73
|
+
# Propagates to Orchestrator.run()'s broad except → FinalStatus.ERROR.
|
|
74
|
+
raise
|
|
75
|
+
except Exception as e:
|
|
76
|
+
exc_info = f"{e.__class__.__name__}: {e}"
|
|
77
|
+
tb = ""
|
|
78
|
+
if os.getenv("CODER_EVAL_DEBUG") == "1":
|
|
79
|
+
tb = "\n" + "".join(traceback.format_exc(limit=5))
|
|
80
|
+
|
|
81
|
+
criterion_type = criterion.type
|
|
82
|
+
logger.error(
|
|
83
|
+
f"Error in {self.__class__.__name__}.check() for criterion type '{criterion_type}': {exc_info}",
|
|
84
|
+
exc_info=True, # Adds full stack trace to logs
|
|
85
|
+
)
|
|
86
|
+
return CriterionResult(
|
|
87
|
+
criterion_type=criterion_type,
|
|
88
|
+
description=criterion.description,
|
|
89
|
+
score=0.0,
|
|
90
|
+
details=f"Error during check: {exc_info}{tb}",
|
|
91
|
+
error=exc_info, # Include exception type and message
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return wrapper
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class BaseCriterion[C: BaseSuccessCriterion](ABC):
|
|
98
|
+
"""Abstract base class for all criterion checkers.
|
|
99
|
+
|
|
100
|
+
Each criterion checker must:
|
|
101
|
+
1. Define `criterion_type` as a ClassVar[str] matching the discriminator
|
|
102
|
+
2. Implement `_check_impl()` with the actual checking logic
|
|
103
|
+
|
|
104
|
+
The `check()` method is final and applies centralized error handling.
|
|
105
|
+
|
|
106
|
+
Type parameter C binds the checker to its specific criterion model for
|
|
107
|
+
better IDE support and static type checking.
|
|
108
|
+
|
|
109
|
+
Example:
|
|
110
|
+
@register_criterion
|
|
111
|
+
class FileExistsChecker(BaseCriterion[FileExistsCriterion]):
|
|
112
|
+
criterion_type = "file_exists"
|
|
113
|
+
|
|
114
|
+
def _check_impl(
|
|
115
|
+
self,
|
|
116
|
+
criterion: FileExistsCriterion,
|
|
117
|
+
sandbox: Sandbox,
|
|
118
|
+
reference_code: str | None = None,
|
|
119
|
+
) -> CriterionResult:
|
|
120
|
+
# Implementation here
|
|
121
|
+
pass
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
# Subclasses MUST define this as a class variable
|
|
125
|
+
criterion_type: ClassVar[str]
|
|
126
|
+
|
|
127
|
+
@handle_criterion_errors
|
|
128
|
+
def check(
|
|
129
|
+
self,
|
|
130
|
+
criterion: C,
|
|
131
|
+
sandbox: "Sandbox",
|
|
132
|
+
reference_code: str | None = None,
|
|
133
|
+
turn_records: list["TurnRecord"] | None = None,
|
|
134
|
+
context: "CheckContext | None" = None,
|
|
135
|
+
) -> CriterionResult:
|
|
136
|
+
"""Execute the criterion check with centralized error handling.
|
|
137
|
+
|
|
138
|
+
This method is FINAL - subclasses must NOT override it.
|
|
139
|
+
Implement _check_impl() instead.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
criterion: The specific criterion definition (Pydantic model)
|
|
143
|
+
sandbox: Sandbox instance for file access and command execution
|
|
144
|
+
reference_code: Optional reference code string for comparison
|
|
145
|
+
turn_records: Optional list of turn records for command inspection
|
|
146
|
+
context: Optional :class:`CheckContext` carrying the live run state
|
|
147
|
+
(``route`` / ``reference_dir``) that judge criteria
|
|
148
|
+
(``agent_judge``, ``llm_judge``) consume. Non-judge checkers
|
|
149
|
+
accept the uniform signature and ignore it.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
CriterionResult with score (0.0-1.0), details, and error info
|
|
153
|
+
"""
|
|
154
|
+
return self._check_impl(
|
|
155
|
+
criterion,
|
|
156
|
+
sandbox,
|
|
157
|
+
reference_code,
|
|
158
|
+
turn_records=turn_records,
|
|
159
|
+
context=context,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
@abstractmethod
|
|
163
|
+
def _check_impl(
|
|
164
|
+
self,
|
|
165
|
+
criterion: C,
|
|
166
|
+
sandbox: "Sandbox",
|
|
167
|
+
reference_code: str | None = None,
|
|
168
|
+
*,
|
|
169
|
+
turn_records: list["TurnRecord"] | None = None,
|
|
170
|
+
context: "CheckContext | None" = None,
|
|
171
|
+
) -> CriterionResult:
|
|
172
|
+
"""Implement the actual criterion checking logic.
|
|
173
|
+
|
|
174
|
+
Subclasses override this method, NOT check(). Every checker shares this
|
|
175
|
+
uniform signature; non-judge checkers accept ``context`` and ignore it.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
criterion: The specific criterion definition (Pydantic model)
|
|
179
|
+
sandbox: Sandbox instance for file access and command execution
|
|
180
|
+
reference_code: Optional reference code string for comparison
|
|
181
|
+
turn_records: Optional list of turn records for command inspection
|
|
182
|
+
context: Optional :class:`CheckContext` (route / reference_dir).
|
|
183
|
+
Consumed by ``llm_judge`` / ``agent_judge``; ignored by
|
|
184
|
+
the rest.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
CriterionResult with score (0.0-1.0), details, and error info
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
Any exception - will be caught by @handle_criterion_errors decorator
|
|
191
|
+
"""
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
def aggregate(
|
|
195
|
+
self,
|
|
196
|
+
criterion: C,
|
|
197
|
+
per_row_results: list[CriterionResult],
|
|
198
|
+
) -> CriterionAggregate | None:
|
|
199
|
+
"""Across-row aggregate for dataset-backed tasks.
|
|
200
|
+
|
|
201
|
+
Default implementation emits summary statistics over the per-row scores:
|
|
202
|
+
``count``, ``mean``, ``median``, ``std``, ``min``, ``max``. This gives
|
|
203
|
+
every criterion a thresholdable baseline for free (e.g. a ``file_exists``
|
|
204
|
+
task can gate on ``suite_thresholds: {mean: 0.9}``).
|
|
205
|
+
|
|
206
|
+
Returns ``None`` only when there are no per-row results (empty dataset).
|
|
207
|
+
|
|
208
|
+
Subclasses with richer signals (classification, per-label metrics, etc.)
|
|
209
|
+
should override and call ``super().aggregate(...)`` to inherit these
|
|
210
|
+
baseline stats, then merge their own metrics and details on top.
|
|
211
|
+
"""
|
|
212
|
+
if not per_row_results:
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
scores = [r.score for r in per_row_results]
|
|
216
|
+
std = statistics.pstdev(scores) if len(scores) > 1 else 0.0
|
|
217
|
+
metrics: dict[str, float] = {
|
|
218
|
+
"count": float(len(scores)),
|
|
219
|
+
"mean": statistics.fmean(scores),
|
|
220
|
+
"median": statistics.median(scores),
|
|
221
|
+
"std": std,
|
|
222
|
+
"min": min(scores),
|
|
223
|
+
"max": max(scores),
|
|
224
|
+
}
|
|
225
|
+
return CriterionAggregate(
|
|
226
|
+
criterion_type=criterion.type,
|
|
227
|
+
metrics=metrics,
|
|
228
|
+
threshold_checks=[],
|
|
229
|
+
passed=True,
|
|
230
|
+
details={},
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# Decorator for registration (defined here to avoid circular imports)
|
|
235
|
+
def register_criterion(cls: type[BaseCriterion[Any]]) -> type[BaseCriterion[Any]]:
|
|
236
|
+
"""Decorator to register a criterion checker.
|
|
237
|
+
|
|
238
|
+
Moved here from __init__.py to prevent circular import issues.
|
|
239
|
+
|
|
240
|
+
Usage:
|
|
241
|
+
@register_criterion
|
|
242
|
+
class MyChecker(BaseCriterion[MyCriterion]):
|
|
243
|
+
criterion_type = "my_type"
|
|
244
|
+
...
|
|
245
|
+
"""
|
|
246
|
+
from coder_eval.criteria import CriterionRegistry
|
|
247
|
+
|
|
248
|
+
return CriterionRegistry.register(cls)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Classification-match criterion checker."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from coder_eval.criteria._classification_aggregate import overlay_classification_metrics
|
|
9
|
+
from coder_eval.criteria.base import BaseCriterion, register_criterion
|
|
10
|
+
from coder_eval.models import (
|
|
11
|
+
ClassificationCriterionResult,
|
|
12
|
+
ClassificationMatchCriterion,
|
|
13
|
+
CriterionAggregate,
|
|
14
|
+
CriterionResult,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from coder_eval.criteria.base import CheckContext
|
|
20
|
+
from coder_eval.models.results import TurnRecord
|
|
21
|
+
from coder_eval.sandbox import Sandbox
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
# Sentinels for the observed label — surfaced as their own classes in the
|
|
26
|
+
# suite rollup's confusion matrix so "didn't write anything" and "wrote
|
|
27
|
+
# something unrecognisable" are visible failure modes rather than vanishing.
|
|
28
|
+
_SENTINEL_NONE = "(none)"
|
|
29
|
+
_SENTINEL_OTHER = "(other)"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@register_criterion
|
|
33
|
+
class ClassificationMatchChecker(BaseCriterion[ClassificationMatchCriterion]):
|
|
34
|
+
"""Checker for ClassificationMatchCriterion.
|
|
35
|
+
|
|
36
|
+
Reads a file written by the agent, extracts its single-line label, and
|
|
37
|
+
compares it to the ground-truth label. Populates ``observed_label`` and
|
|
38
|
+
``expected_label`` on the result so the suite rollup can compute per-class
|
|
39
|
+
precision / recall / F1 and a confusion matrix.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
criterion_type = "classification_match"
|
|
43
|
+
|
|
44
|
+
def _check_impl(
|
|
45
|
+
self,
|
|
46
|
+
criterion: ClassificationMatchCriterion,
|
|
47
|
+
sandbox: Sandbox,
|
|
48
|
+
reference_code: str | None = None,
|
|
49
|
+
*,
|
|
50
|
+
turn_records: list[TurnRecord] | None = None,
|
|
51
|
+
context: CheckContext | None = None,
|
|
52
|
+
) -> CriterionResult:
|
|
53
|
+
expected = self._canonicalize(criterion.expected_label, criterion)
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
raw = sandbox.get_file_content(criterion.path)
|
|
57
|
+
except FileNotFoundError:
|
|
58
|
+
observed = _SENTINEL_NONE
|
|
59
|
+
else:
|
|
60
|
+
content = raw.strip()
|
|
61
|
+
observed = self._canonicalize(content, criterion) if content else _SENTINEL_NONE
|
|
62
|
+
if observed not in {*criterion.allowed_labels, _SENTINEL_NONE}:
|
|
63
|
+
# Content didn't canonicalize to a known label.
|
|
64
|
+
observed = _SENTINEL_OTHER
|
|
65
|
+
|
|
66
|
+
score = 1.0 if observed == expected else 0.0
|
|
67
|
+
details = f"observed={observed!r}, expected={expected!r}"
|
|
68
|
+
|
|
69
|
+
return ClassificationCriterionResult(
|
|
70
|
+
criterion_type=criterion.type,
|
|
71
|
+
description=criterion.description,
|
|
72
|
+
score=score,
|
|
73
|
+
details=details,
|
|
74
|
+
observed_label=observed,
|
|
75
|
+
expected_label=expected,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
def _canonicalize(value: str, criterion: ClassificationMatchCriterion) -> str:
|
|
80
|
+
"""Map a raw string to its canonical form in ``allowed_labels``.
|
|
81
|
+
|
|
82
|
+
When ``case_sensitive=False`` and the value matches an allowed label
|
|
83
|
+
up to case, return the canonical casing from ``allowed_labels``.
|
|
84
|
+
Otherwise return the value as-is; callers decide whether to replace
|
|
85
|
+
it with a sentinel.
|
|
86
|
+
"""
|
|
87
|
+
value = value.strip()
|
|
88
|
+
if criterion.case_sensitive:
|
|
89
|
+
return value
|
|
90
|
+
lower_map = {label.lower(): label for label in criterion.allowed_labels}
|
|
91
|
+
return lower_map.get(value.lower(), value)
|
|
92
|
+
|
|
93
|
+
def aggregate(
|
|
94
|
+
self,
|
|
95
|
+
criterion: ClassificationMatchCriterion,
|
|
96
|
+
per_row_results: list[CriterionResult],
|
|
97
|
+
) -> CriterionAggregate | None:
|
|
98
|
+
"""Baseline stats (from super) + sklearn-style classification metrics.
|
|
99
|
+
|
|
100
|
+
Delegates the classification math to
|
|
101
|
+
``overlay_classification_metrics`` so skill_triggered and any future
|
|
102
|
+
label-emitting criterion share one implementation.
|
|
103
|
+
"""
|
|
104
|
+
base = super().aggregate(criterion, per_row_results)
|
|
105
|
+
if base is None:
|
|
106
|
+
return None
|
|
107
|
+
return overlay_classification_metrics(base, per_row_results)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Command executed criterion checker."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import re
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from coder_eval.criteria.base import BaseCriterion, CheckContext, register_criterion
|
|
9
|
+
from coder_eval.models import CommandExecutedCriterion, CriterionResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from coder_eval.models.results import TurnRecord
|
|
14
|
+
from coder_eval.sandbox import Sandbox
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# Limit regex search input length to mitigate ReDoS on large command strings
|
|
19
|
+
_MAX_PATTERN_SEARCH_LEN = 2000
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@register_criterion
|
|
23
|
+
class CommandExecutedChecker(BaseCriterion[CommandExecutedCriterion]):
|
|
24
|
+
"""Checker for CommandExecutedCriterion.
|
|
25
|
+
|
|
26
|
+
Inspects agent CommandTelemetry records to verify specific
|
|
27
|
+
tools/commands were used during evaluation.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
criterion_type = "command_executed"
|
|
31
|
+
|
|
32
|
+
def _check_impl(
|
|
33
|
+
self,
|
|
34
|
+
criterion: CommandExecutedCriterion,
|
|
35
|
+
sandbox: "Sandbox",
|
|
36
|
+
reference_code: str | None = None,
|
|
37
|
+
*,
|
|
38
|
+
turn_records: list["TurnRecord"] | None = None,
|
|
39
|
+
context: CheckContext | None = None,
|
|
40
|
+
) -> CriterionResult:
|
|
41
|
+
"""Check if the agent executed commands matching the criterion filters.
|
|
42
|
+
|
|
43
|
+
Scoring is fractional: min(1.0, matching_count / min_count).
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
criterion: Command executed criterion with filters
|
|
47
|
+
sandbox: Sandbox instance (not used for this criterion)
|
|
48
|
+
reference_code: Not used for this criterion
|
|
49
|
+
turn_records: List of turn records containing command telemetry
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
CriterionResult with fractional score based on matching commands
|
|
53
|
+
"""
|
|
54
|
+
if turn_records is None:
|
|
55
|
+
return CriterionResult(
|
|
56
|
+
criterion_type=criterion.type,
|
|
57
|
+
description=criterion.description,
|
|
58
|
+
score=0.0,
|
|
59
|
+
details="No turn records available",
|
|
60
|
+
error="turn_records not provided to checker",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
all_commands = [cmd for turn in turn_records for cmd in turn.commands]
|
|
64
|
+
|
|
65
|
+
# Note: do NOT short-circuit when ``all_commands`` is empty. A
|
|
66
|
+
# negative-assertion criterion (``min_count: 0`` + ``max_count: 0``)
|
|
67
|
+
# SHOULD pass here — zero commands trivially satisfies "must not
|
|
68
|
+
# call X". Falling through into the matching loop sets
|
|
69
|
+
# ``match_count = 0`` and the scoring branch handles both shapes.
|
|
70
|
+
|
|
71
|
+
# Compile regex patterns if provided.
|
|
72
|
+
# re.DOTALL so `.` matches newlines — agents commonly write multi-line bash
|
|
73
|
+
# commands using backslash line-continuation (e.g. `uip ... \\\n --body ...`),
|
|
74
|
+
# and patterns like `foo.*--body` must span those line breaks.
|
|
75
|
+
pattern: re.Pattern[str] | None = None
|
|
76
|
+
if criterion.command_pattern is not None:
|
|
77
|
+
try:
|
|
78
|
+
pattern = re.compile(criterion.command_pattern, re.DOTALL)
|
|
79
|
+
except re.error as e:
|
|
80
|
+
return CriterionResult(
|
|
81
|
+
criterion_type=criterion.type,
|
|
82
|
+
description=criterion.description,
|
|
83
|
+
score=0.0,
|
|
84
|
+
details=f"Invalid command_pattern regex: {e}",
|
|
85
|
+
error=f"Invalid regex: {e}",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
exclude_re: re.Pattern[str] | None = None
|
|
89
|
+
if criterion.exclude_pattern is not None:
|
|
90
|
+
try:
|
|
91
|
+
exclude_re = re.compile(criterion.exclude_pattern, re.DOTALL)
|
|
92
|
+
except re.error as e:
|
|
93
|
+
return CriterionResult(
|
|
94
|
+
criterion_type=criterion.type,
|
|
95
|
+
description=criterion.description,
|
|
96
|
+
score=0.0,
|
|
97
|
+
details=f"Invalid exclude_pattern regex: {e}",
|
|
98
|
+
error=f"Invalid regex: {e}",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Filter and count matching commands
|
|
102
|
+
matching_commands: list[str] = []
|
|
103
|
+
for cmd in all_commands:
|
|
104
|
+
# Filter by tool name
|
|
105
|
+
if criterion.tool_name is not None and cmd.tool_name != criterion.tool_name:
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
# Filter by success status
|
|
109
|
+
if criterion.require_success and cmd.result_status != "success":
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
# Extract text for pattern matching (Bash: command param; others: JSON-serialized params)
|
|
113
|
+
if cmd.tool_name == "Bash" and cmd.parameters.get("command"):
|
|
114
|
+
cmd_text = cmd.parameters["command"]
|
|
115
|
+
else:
|
|
116
|
+
cmd_text = json.dumps(cmd.parameters)
|
|
117
|
+
|
|
118
|
+
# Truncate to mitigate ReDoS on large command strings
|
|
119
|
+
if len(cmd_text) > _MAX_PATTERN_SEARCH_LEN:
|
|
120
|
+
cmd_text = cmd_text[:_MAX_PATTERN_SEARCH_LEN]
|
|
121
|
+
|
|
122
|
+
# Filter by command pattern
|
|
123
|
+
if pattern is not None and not pattern.search(cmd_text):
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
# Apply exclusion pattern (skip commands matching the exclusion)
|
|
127
|
+
if exclude_re is not None and exclude_re.search(cmd_text):
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
# Build a display label for the matched command
|
|
131
|
+
if cmd.tool_name == "Bash" and cmd.parameters.get("command"):
|
|
132
|
+
label = cmd.parameters["command"]
|
|
133
|
+
else:
|
|
134
|
+
label = f"{cmd.tool_name}({json.dumps(cmd.parameters)[:80]})"
|
|
135
|
+
matching_commands.append(label)
|
|
136
|
+
|
|
137
|
+
match_count = len(matching_commands)
|
|
138
|
+
|
|
139
|
+
# Score model:
|
|
140
|
+
# max_count is None → fractional towards min_count (legacy behavior).
|
|
141
|
+
# When min_count == 0, the criterion is trivially
|
|
142
|
+
# satisfied (no minimum to hit) and scores 1.0.
|
|
143
|
+
# max_count is set → binary in-range. Pass iff
|
|
144
|
+
# min_count <= match_count <= max_count.
|
|
145
|
+
# The "negative assertion" pattern (min_count: 0, max_count: 0) drops
|
|
146
|
+
# naturally out of the binary branch — it now expresses "must NOT match"
|
|
147
|
+
# exactly. The model validator already rejects max_count < min_count.
|
|
148
|
+
if criterion.max_count is None:
|
|
149
|
+
score = 1.0 if criterion.min_count == 0 else min(1.0, match_count / criterion.min_count)
|
|
150
|
+
else:
|
|
151
|
+
score = 1.0 if criterion.min_count <= match_count <= criterion.max_count else 0.0
|
|
152
|
+
|
|
153
|
+
# Build details
|
|
154
|
+
filters = []
|
|
155
|
+
if criterion.tool_name is not None:
|
|
156
|
+
filters.append(f"tool_name={criterion.tool_name}")
|
|
157
|
+
if criterion.command_pattern is not None:
|
|
158
|
+
filters.append(f"pattern=/{criterion.command_pattern}/")
|
|
159
|
+
if criterion.require_success:
|
|
160
|
+
filters.append("require_success=True")
|
|
161
|
+
if criterion.exclude_pattern is not None:
|
|
162
|
+
filters.append(f"exclude=/{criterion.exclude_pattern}/")
|
|
163
|
+
filter_text = ", ".join(filters) if filters else "none"
|
|
164
|
+
|
|
165
|
+
if criterion.max_count is None:
|
|
166
|
+
range_text = f"{match_count}/{criterion.min_count} required"
|
|
167
|
+
else:
|
|
168
|
+
range_text = f"{match_count} matches (allowed range {criterion.min_count}..{criterion.max_count})"
|
|
169
|
+
details = f"Matched {range_text} commands (filters: {filter_text})"
|
|
170
|
+
if matching_commands:
|
|
171
|
+
# Show up to 3 example matches
|
|
172
|
+
examples = matching_commands[:3]
|
|
173
|
+
truncated = [ex[:120] for ex in examples]
|
|
174
|
+
details += f"\nExamples: {truncated}"
|
|
175
|
+
|
|
176
|
+
return CriterionResult(
|
|
177
|
+
criterion_type=criterion.type,
|
|
178
|
+
description=criterion.description,
|
|
179
|
+
score=score,
|
|
180
|
+
details=details,
|
|
181
|
+
)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Commands efficiency criterion checker."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
5
|
+
|
|
6
|
+
from coder_eval.criteria.base import BaseCriterion, CheckContext, register_criterion
|
|
7
|
+
from coder_eval.models import CommandsEfficiencyCriterion, CriterionResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from coder_eval.models.results import TurnRecord
|
|
12
|
+
from coder_eval.sandbox import Sandbox
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def compute_commands_efficiency(actual: int, expected: int) -> float:
|
|
18
|
+
"""Compute commands efficiency score.
|
|
19
|
+
|
|
20
|
+
Returns expected / max(actual, expected), or 0.0 if actual is 0.
|
|
21
|
+
Raises ValueError if expected < 1 (callers must validate).
|
|
22
|
+
"""
|
|
23
|
+
if expected < 1:
|
|
24
|
+
raise ValueError(f"expected must be >= 1, got {expected}")
|
|
25
|
+
if actual == 0:
|
|
26
|
+
return 0.0
|
|
27
|
+
return expected / max(actual, expected)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@register_criterion
|
|
31
|
+
class CommandsEfficiencyChecker(BaseCriterion[CommandsEfficiencyCriterion]):
|
|
32
|
+
"""Checker for CommandsEfficiencyCriterion."""
|
|
33
|
+
|
|
34
|
+
criterion_type: ClassVar[str] = "commands_efficiency"
|
|
35
|
+
|
|
36
|
+
def _check_impl(
|
|
37
|
+
self,
|
|
38
|
+
criterion: CommandsEfficiencyCriterion,
|
|
39
|
+
sandbox: "Sandbox",
|
|
40
|
+
reference_code: str | None = None,
|
|
41
|
+
*,
|
|
42
|
+
turn_records: list["TurnRecord"] | None = None,
|
|
43
|
+
context: CheckContext | None = None,
|
|
44
|
+
) -> CriterionResult:
|
|
45
|
+
if turn_records is None:
|
|
46
|
+
return CriterionResult(
|
|
47
|
+
criterion_type=criterion.type,
|
|
48
|
+
description=criterion.description,
|
|
49
|
+
score=0.0,
|
|
50
|
+
error="turn_records not provided to checker",
|
|
51
|
+
details="No turn records available",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
actual = sum(len(t.commands) for t in turn_records)
|
|
55
|
+
if actual == 0:
|
|
56
|
+
return CriterionResult(
|
|
57
|
+
criterion_type=criterion.type,
|
|
58
|
+
description=criterion.description,
|
|
59
|
+
score=0.0,
|
|
60
|
+
details="No commands recorded (agent may not have run)",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
score = compute_commands_efficiency(actual, criterion.expected_commands)
|
|
64
|
+
over = actual - criterion.expected_commands
|
|
65
|
+
budget_note = " (at or under budget)" if over <= 0 else f" (over budget by {over} commands)"
|
|
66
|
+
details = f"Commands: {actual}, expected: {criterion.expected_commands}, efficiency: {score:.3f}{budget_note}"
|
|
67
|
+
|
|
68
|
+
return CriterionResult(
|
|
69
|
+
criterion_type=criterion.type,
|
|
70
|
+
description=criterion.description,
|
|
71
|
+
score=score,
|
|
72
|
+
details=details,
|
|
73
|
+
)
|