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,260 @@
|
|
|
1
|
+
"""Success criterion checking with pluggable criterion types.
|
|
2
|
+
|
|
3
|
+
This module provides the SuccessChecker class which orchestrates
|
|
4
|
+
criterion checking using the registered criterion checkers from
|
|
5
|
+
the criteria registry.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
from ..criteria import BaseCriterion, CriterionRegistry, init_criteria
|
|
13
|
+
from ..criteria.base import CheckContext
|
|
14
|
+
from ..errors import JudgeInfrastructureError
|
|
15
|
+
from ..models import CriteriaResults, CriterionResult, SuccessCriteria, SuccessCriterion, TurnRecords
|
|
16
|
+
from ..sandbox import Sandbox
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from ..models.routing import ApiRoute
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Get module logger
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _short_failure_reason(result: CriterionResult, max_len: int = 200) -> str:
|
|
28
|
+
"""Return a one-line, length-capped failure reason for a criterion result.
|
|
29
|
+
|
|
30
|
+
Prefers ``result.error``; then falls back to the first ``stderr:`` line in
|
|
31
|
+
``result.details``; then the first non-empty details line. Multi-line text
|
|
32
|
+
is collapsed with ``" | "`` separators before truncation. Returns
|
|
33
|
+
``"no details"`` when nothing usable is available.
|
|
34
|
+
|
|
35
|
+
Shared by ``SuccessChecker`` (console fail log) and
|
|
36
|
+
``orchestrator._emit_criteria_event`` (streamed ``failure_reason`` field)
|
|
37
|
+
so both surfaces show identical text.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def _collapse(text: str) -> str:
|
|
41
|
+
lines = [line.strip() for line in text.splitlines()]
|
|
42
|
+
collapsed = " | ".join(line for line in lines if line)
|
|
43
|
+
if len(collapsed) > max_len:
|
|
44
|
+
return collapsed[: max_len - 1] + "…"
|
|
45
|
+
return collapsed
|
|
46
|
+
|
|
47
|
+
if result.error:
|
|
48
|
+
return _collapse(result.error) or "no details"
|
|
49
|
+
if result.details:
|
|
50
|
+
for line in result.details.splitlines():
|
|
51
|
+
stripped = line.strip()
|
|
52
|
+
if stripped.lower().startswith("stderr:"):
|
|
53
|
+
reason = stripped[len("stderr:") :].strip()
|
|
54
|
+
if reason:
|
|
55
|
+
return _collapse(reason)
|
|
56
|
+
for line in result.details.splitlines():
|
|
57
|
+
stripped = line.strip()
|
|
58
|
+
if stripped:
|
|
59
|
+
return _collapse(stripped)
|
|
60
|
+
return "no details"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class SuccessChecker:
|
|
64
|
+
"""Orchestrates criterion checking using registered checkers."""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
sandbox: Sandbox,
|
|
69
|
+
init_registry: bool = True,
|
|
70
|
+
validate_registry: bool = True,
|
|
71
|
+
route: "ApiRoute | None" = None,
|
|
72
|
+
):
|
|
73
|
+
"""Initialize the success checker.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
sandbox: Sandbox instance for running checks
|
|
77
|
+
init_registry: Whether to initialize the criteria registry
|
|
78
|
+
validate_registry: Whether to validate all expected types are registered
|
|
79
|
+
route: Resolved ``ApiRoute`` from the orchestrator. Forwarded to every
|
|
80
|
+
checker's ``check()`` so criteria that spawn sub-agents (e.g.
|
|
81
|
+
``agent_judge``) can route through the same backend (Direct /
|
|
82
|
+
Bedrock) as the main coding agent. ``None`` is acceptable
|
|
83
|
+
for non-sub-agent criteria; ``agent_judge`` requires a route.
|
|
84
|
+
"""
|
|
85
|
+
self.sandbox = sandbox
|
|
86
|
+
self._checker_instances: dict[str, BaseCriterion[Any]] = {}
|
|
87
|
+
# Cached reference code - automatically set by check()/check_all() when provided
|
|
88
|
+
# Used by subsequent check() calls that don't explicitly pass reference_code
|
|
89
|
+
self._reference_code: str | None = None
|
|
90
|
+
# Cached reference directory path (resolved). Set by check()/check_all() when provided.
|
|
91
|
+
# Mutually exclusive with self._reference_code at the task level — task.reference
|
|
92
|
+
# is exactly one of code/file/directory.
|
|
93
|
+
self._reference_dir: Path | None = None
|
|
94
|
+
# Cached turn records - set by check()/check_all() when provided
|
|
95
|
+
self._turn_records: TurnRecords | None = None
|
|
96
|
+
self.route = route
|
|
97
|
+
|
|
98
|
+
# V3: Lazy initialization - registry loaded here, not at import
|
|
99
|
+
if init_registry:
|
|
100
|
+
init_criteria(validate=validate_registry)
|
|
101
|
+
|
|
102
|
+
def check(
|
|
103
|
+
self,
|
|
104
|
+
criterion: SuccessCriterion,
|
|
105
|
+
reference_code: str | None = None,
|
|
106
|
+
turn_records: TurnRecords | None = None,
|
|
107
|
+
reference_dir: Path | None = None,
|
|
108
|
+
) -> CriterionResult:
|
|
109
|
+
"""Check a single criterion (backward compatibility wrapper).
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
criterion: Criterion definition
|
|
113
|
+
reference_code: Optional reference code (string form: from
|
|
114
|
+
``task.reference.code`` or ``task.reference.file``).
|
|
115
|
+
turn_records: Optional turn records for command inspection
|
|
116
|
+
reference_dir: Optional resolved path to a reference directory
|
|
117
|
+
(from ``task.reference.directory``). Only consumed by
|
|
118
|
+
``agent_judge``; non-judge criteria ignore it.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
CriterionResult with score
|
|
122
|
+
"""
|
|
123
|
+
# Persist reference_code / reference_dir for subsequent calls (backward compat)
|
|
124
|
+
if reference_code is not None:
|
|
125
|
+
self._reference_code = reference_code
|
|
126
|
+
if reference_dir is not None:
|
|
127
|
+
self._reference_dir = reference_dir
|
|
128
|
+
if turn_records is not None:
|
|
129
|
+
self._turn_records = turn_records
|
|
130
|
+
ref_code = reference_code if reference_code is not None else self._reference_code
|
|
131
|
+
ref_dir = reference_dir if reference_dir is not None else self._reference_dir
|
|
132
|
+
records = turn_records if turn_records is not None else self._turn_records
|
|
133
|
+
return self._check_single(criterion, ref_code, records, ref_dir)
|
|
134
|
+
|
|
135
|
+
def check_all(
|
|
136
|
+
self,
|
|
137
|
+
criteria: SuccessCriteria,
|
|
138
|
+
reference_code: str | None = None,
|
|
139
|
+
turn_records: TurnRecords | None = None,
|
|
140
|
+
reference_dir: Path | None = None,
|
|
141
|
+
) -> CriteriaResults:
|
|
142
|
+
"""Check all success criteria.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
criteria: List of criterion definitions
|
|
146
|
+
reference_code: Optional reference code (string form).
|
|
147
|
+
turn_records: Optional turn records for command inspection
|
|
148
|
+
reference_dir: Optional resolved path to a reference directory.
|
|
149
|
+
Only consumed by ``agent_judge``; non-judge criteria ignore it.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
List of criterion results with scores
|
|
153
|
+
"""
|
|
154
|
+
# Persist for subsequent check() calls
|
|
155
|
+
if reference_code is not None:
|
|
156
|
+
self._reference_code = reference_code
|
|
157
|
+
if reference_dir is not None:
|
|
158
|
+
self._reference_dir = reference_dir
|
|
159
|
+
if turn_records is not None:
|
|
160
|
+
self._turn_records = turn_records
|
|
161
|
+
ref_code = reference_code if reference_code is not None else self._reference_code
|
|
162
|
+
ref_dir = reference_dir if reference_dir is not None else self._reference_dir
|
|
163
|
+
records = turn_records if turn_records is not None else self._turn_records
|
|
164
|
+
results = []
|
|
165
|
+
for criterion in criteria:
|
|
166
|
+
result = self._check_single(criterion, ref_code, records, ref_dir)
|
|
167
|
+
results.append(result)
|
|
168
|
+
return results
|
|
169
|
+
|
|
170
|
+
def _get_checker_instance(self, criterion_type: str) -> BaseCriterion[Any]:
|
|
171
|
+
"""Get or create a checker instance (V3: cached).
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
criterion_type: The criterion type
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Checker instance (reused within this evaluation run)
|
|
178
|
+
"""
|
|
179
|
+
if criterion_type not in self._checker_instances:
|
|
180
|
+
checker_class = CriterionRegistry.get_checker(criterion_type)
|
|
181
|
+
self._checker_instances[criterion_type] = checker_class()
|
|
182
|
+
return self._checker_instances[criterion_type]
|
|
183
|
+
|
|
184
|
+
def _check_single(
|
|
185
|
+
self,
|
|
186
|
+
criterion: SuccessCriterion,
|
|
187
|
+
reference_code: str | None,
|
|
188
|
+
turn_records: TurnRecords | None = None,
|
|
189
|
+
reference_dir: Path | None = None,
|
|
190
|
+
) -> CriterionResult:
|
|
191
|
+
"""Check a single criterion using registered checker.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
criterion: Criterion definition (discriminated union)
|
|
195
|
+
reference_code: Optional reference code (string form)
|
|
196
|
+
turn_records: Optional turn records for command inspection
|
|
197
|
+
reference_dir: Optional resolved path to a reference directory.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
CriterionResult with score
|
|
201
|
+
"""
|
|
202
|
+
criterion_type = criterion.type
|
|
203
|
+
|
|
204
|
+
# V3: Broader exception handling - catches checker constructor failures too
|
|
205
|
+
try:
|
|
206
|
+
# Get cached instance
|
|
207
|
+
checker = self._get_checker_instance(criterion_type)
|
|
208
|
+
context = CheckContext(route=self.route, reference_dir=reference_dir)
|
|
209
|
+
result = checker.check(
|
|
210
|
+
criterion,
|
|
211
|
+
self.sandbox,
|
|
212
|
+
reference_code,
|
|
213
|
+
turn_records=turn_records,
|
|
214
|
+
context=context,
|
|
215
|
+
)
|
|
216
|
+
result.pass_threshold = criterion.pass_threshold
|
|
217
|
+
|
|
218
|
+
logger.debug(f"Criterion '{criterion_type}' score: {result.score:.2f}")
|
|
219
|
+
if result.score < criterion.pass_threshold:
|
|
220
|
+
logger.info(
|
|
221
|
+
"Criterion '%s' FAILED (score=%.2f, threshold=%.2f): %s",
|
|
222
|
+
criterion_type,
|
|
223
|
+
result.score,
|
|
224
|
+
criterion.pass_threshold,
|
|
225
|
+
_short_failure_reason(result),
|
|
226
|
+
)
|
|
227
|
+
return result
|
|
228
|
+
|
|
229
|
+
except KeyError:
|
|
230
|
+
# No checker registered for this type - return failed result for consistency.
|
|
231
|
+
# ERROR record carries enough context — skip the companion FAILED line.
|
|
232
|
+
logger.error(f"No checker found for criterion type '{criterion_type}'")
|
|
233
|
+
return CriterionResult(
|
|
234
|
+
criterion_type=criterion_type,
|
|
235
|
+
description=criterion.description,
|
|
236
|
+
score=0.0,
|
|
237
|
+
details=f"No checker registered for criterion type '{criterion_type}'",
|
|
238
|
+
error=f"Unsupported criterion type: '{criterion_type}'",
|
|
239
|
+
pass_threshold=criterion.pass_threshold,
|
|
240
|
+
)
|
|
241
|
+
except JudgeInfrastructureError:
|
|
242
|
+
raise # judge infra failure escalates to FinalStatus.ERROR; do not score it
|
|
243
|
+
except Exception as e:
|
|
244
|
+
# V3: Catch ALL exceptions, including checker __init__ failures
|
|
245
|
+
logger.exception(f"Checker failure for criterion '{criterion_type}': {e}")
|
|
246
|
+
failed = CriterionResult(
|
|
247
|
+
criterion_type=criterion_type,
|
|
248
|
+
description=criterion.description,
|
|
249
|
+
score=0.0,
|
|
250
|
+
details="Error running checker",
|
|
251
|
+
error=str(e),
|
|
252
|
+
pass_threshold=criterion.pass_threshold,
|
|
253
|
+
)
|
|
254
|
+
logger.info(
|
|
255
|
+
"Criterion '%s' FAILED (score=0.00, threshold=%.2f): %s",
|
|
256
|
+
criterion_type,
|
|
257
|
+
criterion.pass_threshold,
|
|
258
|
+
_short_failure_reason(failed),
|
|
259
|
+
)
|
|
260
|
+
return failed
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Single-completion Anthropic-SDK invoker for the Direct backend.
|
|
2
|
+
|
|
3
|
+
Hits api.anthropic.com using ANTHROPIC_API_KEY from env.
|
|
4
|
+
|
|
5
|
+
The judge issues a forced ``submit_verdict`` tool call; the SDK response is
|
|
6
|
+
converted to a dict via ``model_dump`` so the caller can reuse
|
|
7
|
+
``extract_verdict_from_anthropic_response`` — Anthropic SDK and Bedrock share
|
|
8
|
+
Anthropic's native message shape (content blocks with ``type: tool_use``).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from anthropic import Anthropic, APIError
|
|
16
|
+
|
|
17
|
+
from coder_eval.errors import JudgeInfrastructureError
|
|
18
|
+
from coder_eval.evaluation.judge_models import to_anthropic_alias
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def invoke_anthropic_judge(
|
|
22
|
+
*,
|
|
23
|
+
model: str,
|
|
24
|
+
system: str,
|
|
25
|
+
user: str,
|
|
26
|
+
temperature: float,
|
|
27
|
+
max_tokens: int,
|
|
28
|
+
tool_spec: dict[str, Any],
|
|
29
|
+
timeout_seconds: float = 120.0,
|
|
30
|
+
) -> dict[str, Any]:
|
|
31
|
+
"""One Messages call with ``tools`` + forced ``tool_choice``.
|
|
32
|
+
|
|
33
|
+
Returns the SDK response converted to a dict via ``model_dump`` so the
|
|
34
|
+
caller can reuse ``extract_verdict_from_anthropic_response`` — Anthropic's
|
|
35
|
+
native shape (content blocks with ``type: tool_use``) is identical
|
|
36
|
+
between this SDK call and the Bedrock httpx-direct call.
|
|
37
|
+
"""
|
|
38
|
+
alias = to_anthropic_alias(model)
|
|
39
|
+
client = Anthropic(timeout=timeout_seconds)
|
|
40
|
+
with client:
|
|
41
|
+
try:
|
|
42
|
+
response = client.messages.create(
|
|
43
|
+
model=alias,
|
|
44
|
+
system=system,
|
|
45
|
+
messages=[{"role": "user", "content": user}],
|
|
46
|
+
max_tokens=max_tokens,
|
|
47
|
+
temperature=temperature,
|
|
48
|
+
tools=[tool_spec], # type: ignore[arg-type]
|
|
49
|
+
tool_choice={"type": "tool", "name": tool_spec["name"]},
|
|
50
|
+
)
|
|
51
|
+
except APIError as e:
|
|
52
|
+
# The SDK already retries transient failures internally (2 attempts
|
|
53
|
+
# by default) — do not add another retry loop here.
|
|
54
|
+
raise JudgeInfrastructureError(f"Anthropic judge API error: {e}") from e
|
|
55
|
+
return response.model_dump()
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Single-completion Bedrock invoker for judge-style criteria.
|
|
2
|
+
|
|
3
|
+
Narrow on purpose: one POST to bedrock-runtime, bearer-token auth, no streaming.
|
|
4
|
+
The judge issues a forced ``submit_verdict`` tool call; we return the parsed
|
|
5
|
+
response dict so the caller can walk ``content`` for ``tool_use`` blocks via
|
|
6
|
+
``extract_verdict_from_anthropic_response``.
|
|
7
|
+
|
|
8
|
+
Transient failures (transport errors, 429 throttles, 5xx) are retried with
|
|
9
|
+
jittered exponential backoff; exhaustion and non-retryable failures raise
|
|
10
|
+
``JudgeInfrastructureError`` so the row escalates to ``FinalStatus.ERROR``
|
|
11
|
+
instead of being scored 0.0.
|
|
12
|
+
|
|
13
|
+
agent_judge uses the Claude Code SDK subprocess instead — the two paths
|
|
14
|
+
intentionally do not share an HTTP client.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
import time
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
import httpx
|
|
24
|
+
|
|
25
|
+
from coder_eval.errors import JudgeInfrastructureError
|
|
26
|
+
from coder_eval.errors.categories import RetryConfig
|
|
27
|
+
from coder_eval.errors.retry import compute_backoff
|
|
28
|
+
from coder_eval.evaluation.judge_models import to_bedrock_model
|
|
29
|
+
from coder_eval.models import BedrockRoute
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
_JUDGE_RETRY = RetryConfig(max_retries=3, initial_delay=2.0, backoff_multiplier=2.0)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _is_retryable_status(status_code: int) -> bool:
|
|
38
|
+
"""Throttles and server-side failures are worth retrying; 4xx (auth, bad request) are not."""
|
|
39
|
+
return status_code == 429 or status_code >= 500
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def invoke_bedrock_judge(
|
|
43
|
+
*,
|
|
44
|
+
route: BedrockRoute,
|
|
45
|
+
model: str,
|
|
46
|
+
system: str,
|
|
47
|
+
user: str,
|
|
48
|
+
temperature: float,
|
|
49
|
+
max_tokens: int,
|
|
50
|
+
tool_spec: dict[str, Any],
|
|
51
|
+
timeout_seconds: float = 120.0,
|
|
52
|
+
) -> dict[str, Any]:
|
|
53
|
+
"""POST a Bedrock Messages request with ``tools`` + forced ``tool_choice``.
|
|
54
|
+
|
|
55
|
+
Returns the parsed response dict so the caller can walk ``content`` for
|
|
56
|
+
``tool_use`` blocks via ``extract_verdict_from_anthropic_response``.
|
|
57
|
+
|
|
58
|
+
Transport errors, 429, and 5xx responses are retried up to
|
|
59
|
+
``_JUDGE_RETRY.max_retries`` times with jittered exponential backoff.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
ValueError: ``model`` empty.
|
|
63
|
+
JudgeInfrastructureError: retries exhausted, non-retryable HTTP failure
|
|
64
|
+
(e.g. 400/401/403), or a non-dict JSON body.
|
|
65
|
+
"""
|
|
66
|
+
qualified = to_bedrock_model(model, route.region)
|
|
67
|
+
url = f"https://bedrock-runtime.{route.region}.amazonaws.com/model/{qualified}/invoke"
|
|
68
|
+
body = {
|
|
69
|
+
"anthropic_version": "bedrock-2023-05-31",
|
|
70
|
+
"max_tokens": max_tokens,
|
|
71
|
+
"temperature": temperature,
|
|
72
|
+
"system": system,
|
|
73
|
+
"messages": [{"role": "user", "content": user}],
|
|
74
|
+
"tools": [tool_spec],
|
|
75
|
+
"tool_choice": {"type": "tool", "name": tool_spec["name"]},
|
|
76
|
+
}
|
|
77
|
+
headers = {
|
|
78
|
+
"Authorization": f"Bearer {route.bearer_token}",
|
|
79
|
+
"Content-Type": "application/json",
|
|
80
|
+
"Accept": "application/json",
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# invoke_bedrock_judge is sync (called via asyncio.to_thread), so a plain
|
|
84
|
+
# time.sleep between attempts is correct here.
|
|
85
|
+
attempts = _JUDGE_RETRY.max_retries + 1
|
|
86
|
+
last_failure = ""
|
|
87
|
+
last_exc: Exception | None = None
|
|
88
|
+
for attempt in range(attempts):
|
|
89
|
+
if attempt:
|
|
90
|
+
time.sleep(compute_backoff(_JUDGE_RETRY, attempt - 1))
|
|
91
|
+
try:
|
|
92
|
+
response = httpx.post(url, headers=headers, json=body, timeout=timeout_seconds)
|
|
93
|
+
except httpx.HTTPError as e:
|
|
94
|
+
last_failure = f"Bedrock invoke transport error: {e}"
|
|
95
|
+
last_exc = e
|
|
96
|
+
logger.warning("Bedrock judge attempt %d/%d failed: %s", attempt + 1, attempts, last_failure)
|
|
97
|
+
continue
|
|
98
|
+
if _is_retryable_status(response.status_code):
|
|
99
|
+
last_failure = f"Bedrock invoke failed: {response.status_code} {response.text[:500]}"
|
|
100
|
+
last_exc = None
|
|
101
|
+
logger.warning("Bedrock judge attempt %d/%d failed: %s", attempt + 1, attempts, last_failure)
|
|
102
|
+
continue
|
|
103
|
+
if response.status_code >= 300:
|
|
104
|
+
raise JudgeInfrastructureError(f"Bedrock invoke failed: {response.status_code} {response.text[:500]}")
|
|
105
|
+
try:
|
|
106
|
+
data = response.json()
|
|
107
|
+
except ValueError as e:
|
|
108
|
+
# A malformed/truncated 2xx body (flaky proxy/gateway) is infra, not
|
|
109
|
+
# agent quality — escalate like the non-dict arm below.
|
|
110
|
+
raise JudgeInfrastructureError(f"Bedrock response is not valid JSON: {e}") from e
|
|
111
|
+
if not isinstance(data, dict):
|
|
112
|
+
raise JudgeInfrastructureError(f"Bedrock response is not a JSON object: {str(data)[:500]}")
|
|
113
|
+
return data
|
|
114
|
+
raise JudgeInfrastructureError(f"{last_failure} (after {attempts} attempts)") from last_exc
|