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,119 @@
|
|
|
1
|
+
"""Unified file check criterion checker (existence + contents + regex)."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from coder_eval.criteria.base import BaseCriterion, CheckContext, register_criterion
|
|
8
|
+
from coder_eval.models import CriterionResult, FileCheckCriterion
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from coder_eval.models.results import TurnRecord
|
|
13
|
+
from coder_eval.sandbox import Sandbox
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@register_criterion
|
|
19
|
+
class FileCheckChecker(BaseCriterion[FileCheckCriterion]):
|
|
20
|
+
"""Checker for FileCheckCriterion."""
|
|
21
|
+
|
|
22
|
+
criterion_type = "file_check"
|
|
23
|
+
|
|
24
|
+
def _check_impl(
|
|
25
|
+
self,
|
|
26
|
+
criterion: FileCheckCriterion,
|
|
27
|
+
sandbox: "Sandbox",
|
|
28
|
+
reference_code: str | None = None,
|
|
29
|
+
*,
|
|
30
|
+
turn_records: list["TurnRecord"] | None = None,
|
|
31
|
+
context: CheckContext | None = None,
|
|
32
|
+
) -> CriterionResult:
|
|
33
|
+
"""Unified file check: existence, string includes/excludes, regex patterns.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
CriterionResult with fractional score based on active sub-checks
|
|
37
|
+
"""
|
|
38
|
+
# 1. File existence (implicit)
|
|
39
|
+
if not sandbox.file_exists(criterion.path):
|
|
40
|
+
return CriterionResult(
|
|
41
|
+
criterion_type=criterion.type,
|
|
42
|
+
description=criterion.description,
|
|
43
|
+
score=0.0,
|
|
44
|
+
error=f"File '{criterion.path}' does not exist",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
has_includes = len(criterion.includes) > 0
|
|
48
|
+
has_excludes = len(criterion.excludes) > 0
|
|
49
|
+
has_patterns = len(criterion.patterns) > 0
|
|
50
|
+
|
|
51
|
+
# 2. Pure existence check (no sub-checks specified)
|
|
52
|
+
if not has_includes and not has_excludes and not has_patterns:
|
|
53
|
+
return CriterionResult(
|
|
54
|
+
criterion_type=criterion.type,
|
|
55
|
+
description=criterion.description,
|
|
56
|
+
score=1.0,
|
|
57
|
+
details=f"File '{criterion.path}' exists",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# 3. Read file content
|
|
61
|
+
content = sandbox.get_file_content(criterion.path)
|
|
62
|
+
|
|
63
|
+
scores: list[float] = []
|
|
64
|
+
details_parts: list[str] = []
|
|
65
|
+
|
|
66
|
+
# 4a. Includes score
|
|
67
|
+
if has_includes:
|
|
68
|
+
found = sum(1 for inc in criterion.includes if inc in content)
|
|
69
|
+
total = len(criterion.includes)
|
|
70
|
+
includes_score = found / total
|
|
71
|
+
scores.append(includes_score)
|
|
72
|
+
details_parts.append(f"Includes: {found}/{total} found")
|
|
73
|
+
|
|
74
|
+
# 4b. Excludes score
|
|
75
|
+
if has_excludes:
|
|
76
|
+
present = sum(1 for exc in criterion.excludes if exc in content)
|
|
77
|
+
total = len(criterion.excludes)
|
|
78
|
+
excludes_score = 1.0 - (present / total)
|
|
79
|
+
scores.append(excludes_score)
|
|
80
|
+
absent = total - present
|
|
81
|
+
details_parts.append(f"Excludes: {absent}/{total} absent")
|
|
82
|
+
|
|
83
|
+
# 4c. Pattern scores
|
|
84
|
+
if has_patterns:
|
|
85
|
+
pattern_scores: list[float] = []
|
|
86
|
+
pattern_details: list[str] = []
|
|
87
|
+
for p in criterion.patterns:
|
|
88
|
+
try:
|
|
89
|
+
regex = re.compile(p.pattern, p.flags)
|
|
90
|
+
match = regex.search(content)
|
|
91
|
+
except re.error as e:
|
|
92
|
+
logger.warning("Invalid regex pattern '%s': %s", p.pattern, e)
|
|
93
|
+
pattern_scores.append(0.0)
|
|
94
|
+
pattern_details.append(f"Invalid regex '{p.pattern}': {e}")
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
if p.must_match:
|
|
98
|
+
pattern_scores.append(1.0 if match else 0.0)
|
|
99
|
+
else:
|
|
100
|
+
pattern_scores.append(1.0 if match is None else 0.0)
|
|
101
|
+
|
|
102
|
+
matched = sum(1 for s in pattern_scores if s == 1.0)
|
|
103
|
+
patterns_score = sum(pattern_scores) / len(pattern_scores)
|
|
104
|
+
scores.append(patterns_score)
|
|
105
|
+
detail = f"Patterns: {matched}/{len(pattern_scores)} matched"
|
|
106
|
+
if pattern_details:
|
|
107
|
+
detail += f" ({'; '.join(pattern_details)})"
|
|
108
|
+
details_parts.append(detail)
|
|
109
|
+
|
|
110
|
+
# 5. Combine active category scores
|
|
111
|
+
score = sum(scores) / len(scores)
|
|
112
|
+
details_parts.append(f"Score: {score:.2f}")
|
|
113
|
+
|
|
114
|
+
return CriterionResult(
|
|
115
|
+
criterion_type=criterion.type,
|
|
116
|
+
description=criterion.description,
|
|
117
|
+
score=score,
|
|
118
|
+
details="; ".join(details_parts),
|
|
119
|
+
)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""File contains criterion checker."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from coder_eval.criteria.base import BaseCriterion, CheckContext, register_criterion
|
|
7
|
+
from coder_eval.models import CriterionResult, FileContainsCriterion
|
|
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
|
+
@register_criterion
|
|
18
|
+
class FileContainsChecker(BaseCriterion[FileContainsCriterion]):
|
|
19
|
+
"""Checker for FileContainsCriterion."""
|
|
20
|
+
|
|
21
|
+
criterion_type = "file_contains"
|
|
22
|
+
|
|
23
|
+
def _check_impl(
|
|
24
|
+
self,
|
|
25
|
+
criterion: FileContainsCriterion,
|
|
26
|
+
sandbox: "Sandbox",
|
|
27
|
+
reference_code: str | None = None,
|
|
28
|
+
*,
|
|
29
|
+
turn_records: list["TurnRecord"] | None = None,
|
|
30
|
+
context: CheckContext | None = None,
|
|
31
|
+
) -> CriterionResult:
|
|
32
|
+
"""Check if file contains required strings and excludes forbidden ones.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
CriterionResult with fractional score based on includes/excludes matched
|
|
36
|
+
"""
|
|
37
|
+
if not sandbox.file_exists(criterion.path):
|
|
38
|
+
return CriterionResult(
|
|
39
|
+
criterion_type=criterion.type,
|
|
40
|
+
description=criterion.description,
|
|
41
|
+
score=0.0,
|
|
42
|
+
error=f"File '{criterion.path}' does not exist",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
content = sandbox.get_file_content(criterion.path)
|
|
46
|
+
|
|
47
|
+
# Calculate includes score (fraction of includes found)
|
|
48
|
+
includes_found = sum(1 for inc in criterion.includes if inc in content)
|
|
49
|
+
includes_total = len(criterion.includes)
|
|
50
|
+
includes_score = includes_found / includes_total if includes_total > 0 else 1.0
|
|
51
|
+
|
|
52
|
+
# Calculate excludes score (fraction of excludes absent)
|
|
53
|
+
if criterion.excludes:
|
|
54
|
+
excludes_found = sum(1 for exc in criterion.excludes if exc in content)
|
|
55
|
+
excludes_total = len(criterion.excludes)
|
|
56
|
+
excludes_score = 1.0 - (excludes_found / excludes_total)
|
|
57
|
+
else:
|
|
58
|
+
excludes_score = 1.0
|
|
59
|
+
|
|
60
|
+
# Combined score: only average when both categories are active
|
|
61
|
+
has_includes = includes_total > 0
|
|
62
|
+
has_excludes = bool(criterion.excludes)
|
|
63
|
+
if has_includes and has_excludes:
|
|
64
|
+
score = (includes_score + excludes_score) / 2.0
|
|
65
|
+
elif has_includes:
|
|
66
|
+
score = includes_score
|
|
67
|
+
elif has_excludes:
|
|
68
|
+
score = excludes_score
|
|
69
|
+
else:
|
|
70
|
+
score = 1.0
|
|
71
|
+
|
|
72
|
+
# Build details
|
|
73
|
+
details_parts = []
|
|
74
|
+
details_parts.append(f"Includes: {includes_found}/{includes_total} found")
|
|
75
|
+
if criterion.excludes:
|
|
76
|
+
excludes_absent = len(criterion.excludes) - sum(1 for exc in criterion.excludes if exc in content)
|
|
77
|
+
details_parts.append(f"Excludes: {excludes_absent}/{len(criterion.excludes)} absent")
|
|
78
|
+
details_parts.append(f"Score: {score:.2f}")
|
|
79
|
+
|
|
80
|
+
return CriterionResult(
|
|
81
|
+
criterion_type=criterion.type,
|
|
82
|
+
description=criterion.description,
|
|
83
|
+
score=score,
|
|
84
|
+
details="; ".join(details_parts),
|
|
85
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""File existence criterion checker."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from coder_eval.criteria.base import BaseCriterion, CheckContext, register_criterion
|
|
7
|
+
from coder_eval.models import CriterionResult, FileExistsCriterion
|
|
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
|
+
@register_criterion
|
|
18
|
+
class FileExistsChecker(BaseCriterion[FileExistsCriterion]):
|
|
19
|
+
"""Checker for FileExistsCriterion."""
|
|
20
|
+
|
|
21
|
+
criterion_type = "file_exists"
|
|
22
|
+
|
|
23
|
+
def _check_impl(
|
|
24
|
+
self,
|
|
25
|
+
criterion: FileExistsCriterion,
|
|
26
|
+
sandbox: "Sandbox",
|
|
27
|
+
reference_code: str | None = None,
|
|
28
|
+
*,
|
|
29
|
+
turn_records: list["TurnRecord"] | None = None,
|
|
30
|
+
context: CheckContext | None = None,
|
|
31
|
+
) -> CriterionResult:
|
|
32
|
+
"""Check if a file exists in the sandbox.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
CriterionResult with score 1.0 if exists, 0.0 if not
|
|
36
|
+
"""
|
|
37
|
+
exists = sandbox.file_exists(criterion.path)
|
|
38
|
+
score = 1.0 if exists else 0.0
|
|
39
|
+
|
|
40
|
+
return CriterionResult(
|
|
41
|
+
criterion_type=criterion.type,
|
|
42
|
+
description=criterion.description,
|
|
43
|
+
score=score,
|
|
44
|
+
details=f"File '{criterion.path}' {'exists' if exists else 'does not exist'}",
|
|
45
|
+
)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""File matches regex criterion checker."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from coder_eval.criteria.base import BaseCriterion, CheckContext, register_criterion
|
|
8
|
+
from coder_eval.models import CriterionResult, FileMatchesRegexCriterion
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from coder_eval.models.results import TurnRecord
|
|
13
|
+
from coder_eval.sandbox import Sandbox
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@register_criterion
|
|
19
|
+
class FileMatchesRegexChecker(BaseCriterion[FileMatchesRegexCriterion]):
|
|
20
|
+
"""Checker for FileMatchesRegexCriterion."""
|
|
21
|
+
|
|
22
|
+
criterion_type = "file_matches_regex"
|
|
23
|
+
|
|
24
|
+
def _check_impl(
|
|
25
|
+
self,
|
|
26
|
+
criterion: FileMatchesRegexCriterion,
|
|
27
|
+
sandbox: "Sandbox",
|
|
28
|
+
reference_code: str | None = None,
|
|
29
|
+
*,
|
|
30
|
+
turn_records: list["TurnRecord"] | None = None,
|
|
31
|
+
context: CheckContext | None = None,
|
|
32
|
+
) -> CriterionResult:
|
|
33
|
+
"""Check if file content matches the regex pattern.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
criterion: File matches regex criterion
|
|
37
|
+
sandbox: Sandbox instance for file access
|
|
38
|
+
reference_code: Not used for this criterion
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Result with binary score (1.0 if pattern matches as expected, 0.0 otherwise)
|
|
42
|
+
"""
|
|
43
|
+
if not sandbox.file_exists(criterion.path):
|
|
44
|
+
return CriterionResult(
|
|
45
|
+
criterion_type=criterion.type,
|
|
46
|
+
description=criterion.description,
|
|
47
|
+
score=0.0,
|
|
48
|
+
error=f"File '{criterion.path}' does not exist",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
content = sandbox.get_file_content(criterion.path)
|
|
52
|
+
|
|
53
|
+
# Compile and search for pattern
|
|
54
|
+
try:
|
|
55
|
+
regex = re.compile(criterion.pattern, criterion.flags)
|
|
56
|
+
match = regex.search(content)
|
|
57
|
+
except re.error as e:
|
|
58
|
+
return CriterionResult(
|
|
59
|
+
criterion_type=criterion.type,
|
|
60
|
+
description=criterion.description,
|
|
61
|
+
score=0.0,
|
|
62
|
+
error=f"Invalid regex pattern: {e}",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Check based on must_match flag
|
|
66
|
+
if criterion.must_match:
|
|
67
|
+
score = 1.0 if match is not None else 0.0
|
|
68
|
+
if match:
|
|
69
|
+
matched_text = match.group()[:100]
|
|
70
|
+
details = f"Pattern '{criterion.pattern}' found in file (matched: '{matched_text}')"
|
|
71
|
+
else:
|
|
72
|
+
details = f"Pattern '{criterion.pattern}' not found in file"
|
|
73
|
+
else:
|
|
74
|
+
score = 1.0 if match is None else 0.0
|
|
75
|
+
if match is None:
|
|
76
|
+
details = f"Pattern '{criterion.pattern}' correctly absent from file"
|
|
77
|
+
else:
|
|
78
|
+
matched_text = match.group()[:100]
|
|
79
|
+
details = f"Pattern '{criterion.pattern}' found but should not be present (matched: '{matched_text}')"
|
|
80
|
+
|
|
81
|
+
return CriterionResult(
|
|
82
|
+
criterion_type=criterion.type,
|
|
83
|
+
description=criterion.description,
|
|
84
|
+
score=score,
|
|
85
|
+
details=details,
|
|
86
|
+
)
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""JSON check criterion checker (existence + parse + schema + JMESPath assertions)."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
import jmespath
|
|
10
|
+
import jsonschema
|
|
11
|
+
|
|
12
|
+
from coder_eval.criteria.base import BaseCriterion, CheckContext, register_criterion
|
|
13
|
+
from coder_eval.models import CriterionResult, JMESPathAssertion, JsonCheckCriterion
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from coder_eval.models.results import TurnRecord
|
|
18
|
+
from coder_eval.sandbox import Sandbox
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# JSON type name -> Python type(s) mapping for the "type" operator.
|
|
23
|
+
# Task authors use JSON names (string, number, array, object, boolean, null).
|
|
24
|
+
_JSON_TYPE_MAP: dict[str, type | tuple[type, ...]] = {
|
|
25
|
+
"string": str,
|
|
26
|
+
"number": (int, float),
|
|
27
|
+
"boolean": bool,
|
|
28
|
+
"array": list,
|
|
29
|
+
"object": dict,
|
|
30
|
+
"null": type(None),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Operator dispatch table. Each operator takes (actual, expected) and returns bool.
|
|
34
|
+
# All exceptions are caught per-assertion by the caller.
|
|
35
|
+
_OPERATORS: dict[str, Callable[[Any, Any], bool]] = {
|
|
36
|
+
"equals": lambda actual, expected: actual == expected,
|
|
37
|
+
"not_equals": lambda actual, expected: actual != expected,
|
|
38
|
+
"contains": lambda actual, expected: expected in actual,
|
|
39
|
+
"gt": lambda actual, expected: actual > expected,
|
|
40
|
+
"gte": lambda actual, expected: actual >= expected,
|
|
41
|
+
"lt": lambda actual, expected: actual < expected,
|
|
42
|
+
"lte": lambda actual, expected: actual <= expected,
|
|
43
|
+
"type": lambda actual, expected: (
|
|
44
|
+
expected in _JSON_TYPE_MAP
|
|
45
|
+
and (
|
|
46
|
+
(isinstance(actual, bool) and expected == "boolean")
|
|
47
|
+
or (not isinstance(actual, bool) and isinstance(actual, _JSON_TYPE_MAP[expected]))
|
|
48
|
+
)
|
|
49
|
+
),
|
|
50
|
+
"regex": lambda actual, expected: bool(re.search(expected, actual)),
|
|
51
|
+
"exists": lambda actual, _: actual is not None,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@register_criterion
|
|
56
|
+
class JsonCheckChecker(BaseCriterion[JsonCheckCriterion]):
|
|
57
|
+
"""Checker for JsonCheckCriterion."""
|
|
58
|
+
|
|
59
|
+
criterion_type = "json_check"
|
|
60
|
+
|
|
61
|
+
def _check_impl(
|
|
62
|
+
self,
|
|
63
|
+
criterion: JsonCheckCriterion,
|
|
64
|
+
sandbox: "Sandbox",
|
|
65
|
+
reference_code: str | None = None,
|
|
66
|
+
*,
|
|
67
|
+
turn_records: list["TurnRecord"] | None = None,
|
|
68
|
+
context: CheckContext | None = None,
|
|
69
|
+
) -> CriterionResult:
|
|
70
|
+
"""JSON check: existence, parse, schema validation, JMESPath assertions."""
|
|
71
|
+
# 1. File existence
|
|
72
|
+
if not sandbox.file_exists(criterion.path):
|
|
73
|
+
return CriterionResult(
|
|
74
|
+
criterion_type=criterion.type,
|
|
75
|
+
description=criterion.description,
|
|
76
|
+
score=0.0,
|
|
77
|
+
error=f"File '{criterion.path}' does not exist",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# 2. Parse JSON
|
|
81
|
+
content = sandbox.get_file_content(criterion.path)
|
|
82
|
+
try:
|
|
83
|
+
data = json.loads(content)
|
|
84
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
85
|
+
return CriterionResult(
|
|
86
|
+
criterion_type=criterion.type,
|
|
87
|
+
description=criterion.description,
|
|
88
|
+
score=0.0,
|
|
89
|
+
error=f"Invalid JSON in '{criterion.path}': {e}",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
has_schema = criterion.json_schema is not None
|
|
93
|
+
has_assertions = len(criterion.assertions) > 0
|
|
94
|
+
|
|
95
|
+
# 3. Pure validity check
|
|
96
|
+
if not has_schema and not has_assertions:
|
|
97
|
+
return CriterionResult(
|
|
98
|
+
criterion_type=criterion.type,
|
|
99
|
+
description=criterion.description,
|
|
100
|
+
score=1.0,
|
|
101
|
+
details=f"'{criterion.path}' is valid JSON",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
scores: list[float] = []
|
|
105
|
+
details_parts: list[str] = []
|
|
106
|
+
|
|
107
|
+
# 4. Schema validation (gates assertions — if schema fails, skip assertions)
|
|
108
|
+
if has_schema:
|
|
109
|
+
assert criterion.json_schema is not None # narrowing for pyright
|
|
110
|
+
schema_score, schema_detail = self._check_schema(criterion.json_schema, data, sandbox)
|
|
111
|
+
scores.append(schema_score)
|
|
112
|
+
details_parts.append(schema_detail)
|
|
113
|
+
|
|
114
|
+
if schema_score == 0.0 and has_assertions:
|
|
115
|
+
details_parts.append("Assertions: skipped (schema failed)")
|
|
116
|
+
score = 0.0
|
|
117
|
+
details_parts.append(f"Score: {score:.2f}")
|
|
118
|
+
return CriterionResult(
|
|
119
|
+
criterion_type=criterion.type,
|
|
120
|
+
description=criterion.description,
|
|
121
|
+
score=score,
|
|
122
|
+
details="; ".join(details_parts),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# 5. JMESPath assertions
|
|
126
|
+
if has_assertions:
|
|
127
|
+
assertions_score, assertions_detail = self._check_assertions(criterion.assertions, data)
|
|
128
|
+
scores.append(assertions_score)
|
|
129
|
+
details_parts.append(assertions_detail)
|
|
130
|
+
|
|
131
|
+
# 6. Average active categories
|
|
132
|
+
score = sum(scores) / len(scores)
|
|
133
|
+
details_parts.append(f"Score: {score:.2f}")
|
|
134
|
+
|
|
135
|
+
return CriterionResult(
|
|
136
|
+
criterion_type=criterion.type,
|
|
137
|
+
description=criterion.description,
|
|
138
|
+
score=score,
|
|
139
|
+
details="; ".join(details_parts),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def _check_schema(self, schema_path: str, data: Any, sandbox: "Sandbox") -> tuple[float, str]:
|
|
143
|
+
"""Validate data against a JSON Schema file."""
|
|
144
|
+
if not sandbox.file_exists(schema_path):
|
|
145
|
+
return 0.0, f"Schema file '{schema_path}' not found"
|
|
146
|
+
|
|
147
|
+
schema_content = sandbox.get_file_content(schema_path)
|
|
148
|
+
try:
|
|
149
|
+
schema = json.loads(schema_content)
|
|
150
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
151
|
+
return 0.0, f"Invalid JSON in schema file '{schema_path}': {e}"
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
jsonschema.validate(instance=data, schema=schema)
|
|
155
|
+
return 1.0, "Schema: valid"
|
|
156
|
+
except jsonschema.ValidationError as e:
|
|
157
|
+
return 0.0, f"Schema: invalid — {e.message}"
|
|
158
|
+
except jsonschema.SchemaError as e:
|
|
159
|
+
return 0.0, f"Schema: malformed schema — {e.message}"
|
|
160
|
+
|
|
161
|
+
def _check_assertions(self, assertions: list[JMESPathAssertion], data: Any) -> tuple[float, str]:
|
|
162
|
+
"""Evaluate JMESPath assertions against parsed JSON data."""
|
|
163
|
+
passed = 0
|
|
164
|
+
total = len(assertions)
|
|
165
|
+
failed_details: list[str] = []
|
|
166
|
+
|
|
167
|
+
for assertion in assertions:
|
|
168
|
+
try:
|
|
169
|
+
actual = jmespath.search(assertion.expression, data)
|
|
170
|
+
op_func = _OPERATORS[assertion.operator]
|
|
171
|
+
if op_func(actual, assertion.expected):
|
|
172
|
+
passed += 1
|
|
173
|
+
else:
|
|
174
|
+
expr = assertion.expression
|
|
175
|
+
op = assertion.operator
|
|
176
|
+
failed_details.append(f"'{expr}' {op}: expected {assertion.expected!r}, got {actual!r}")
|
|
177
|
+
except Exception as e:
|
|
178
|
+
failed_details.append(f"'{assertion.expression}': error — {e}")
|
|
179
|
+
|
|
180
|
+
score = passed / total
|
|
181
|
+
detail = f"Assertions: {passed}/{total} passed"
|
|
182
|
+
if failed_details:
|
|
183
|
+
detail += f" ({'; '.join(failed_details)})"
|
|
184
|
+
return score, detail
|