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,11 @@
|
|
|
1
|
+
"""Code scoring utilities for reference implementation comparison.
|
|
2
|
+
|
|
3
|
+
This package provides three scorer classes for evaluating agent-generated code
|
|
4
|
+
against reference implementations using static analysis:
|
|
5
|
+
|
|
6
|
+
- SimilarityScorer: AST, token, and signature similarity
|
|
7
|
+
- ComplexityScorer: Cyclomatic complexity, LOC, function count
|
|
8
|
+
- QualityScorer: Type hints, docstrings, error handling
|
|
9
|
+
|
|
10
|
+
All scorers produce scores in the range [0.0, 1.0] where 1.0 is best.
|
|
11
|
+
"""
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""AST-based code similarity scoring."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from difflib import SequenceMatcher
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def score_ast_similarity(agent_code: str, reference_code: str) -> float:
|
|
8
|
+
"""Compare AST structures using node type sequences.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
agent_code: Agent's implementation source code
|
|
12
|
+
reference_code: Reference implementation source code
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Similarity score [0.0, 1.0] based on node type sequence matching
|
|
16
|
+
|
|
17
|
+
Raises:
|
|
18
|
+
SyntaxError: If either code cannot be parsed
|
|
19
|
+
"""
|
|
20
|
+
agent_ast = ast.parse(agent_code)
|
|
21
|
+
ref_ast = ast.parse(reference_code)
|
|
22
|
+
|
|
23
|
+
agent_nodes = _get_node_types(agent_ast)
|
|
24
|
+
ref_nodes = _get_node_types(ref_ast)
|
|
25
|
+
|
|
26
|
+
return SequenceMatcher(None, agent_nodes, ref_nodes).ratio()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _get_node_types(tree: ast.AST) -> list[str]:
|
|
30
|
+
"""Extract node types from AST in depth-first order.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
tree: Parsed AST
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
List of node type names (e.g., ['Module', 'FunctionDef', 'Return'])
|
|
37
|
+
"""
|
|
38
|
+
return [type(node).__name__ for node in ast.walk(tree)]
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Code complexity scoring using radon metrics.
|
|
2
|
+
|
|
3
|
+
This module provides the ComplexityScorer class for evaluating cyclomatic
|
|
4
|
+
complexity, lines of code, and function count metrics.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from radon.complexity import cc_visit
|
|
8
|
+
from radon.raw import analyze
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ComplexityScorer:
|
|
12
|
+
"""Calculate code complexity metrics using radon.
|
|
13
|
+
|
|
14
|
+
Measures:
|
|
15
|
+
- Cyclomatic complexity (control flow branches)
|
|
16
|
+
- Lines of code (total, source, comments, blank)
|
|
17
|
+
- Function count
|
|
18
|
+
|
|
19
|
+
Scores agent code relative to reference baseline, where simpler code
|
|
20
|
+
gets higher scores (within reason - 1.5x reference is threshold).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def calculate_metrics(self, code: str) -> dict[str, int]:
|
|
24
|
+
"""Calculate all complexity metrics for code.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
code: Python source code
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Dictionary with metrics:
|
|
31
|
+
{
|
|
32
|
+
'cyclomatic_complexity': int,
|
|
33
|
+
'lines_of_code': int,
|
|
34
|
+
'source_lines': int,
|
|
35
|
+
'comment_lines': int,
|
|
36
|
+
'blank_lines': int,
|
|
37
|
+
'function_count': int
|
|
38
|
+
}
|
|
39
|
+
"""
|
|
40
|
+
# Cyclomatic complexity per function
|
|
41
|
+
cc_results = cc_visit(code)
|
|
42
|
+
cyclomatic = sum(r.complexity for r in cc_results)
|
|
43
|
+
|
|
44
|
+
# Raw metrics (LOC, comments, etc.)
|
|
45
|
+
raw = analyze(code)
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
"cyclomatic_complexity": cyclomatic,
|
|
49
|
+
"lines_of_code": raw.loc,
|
|
50
|
+
"source_lines": raw.sloc,
|
|
51
|
+
"comment_lines": raw.comments,
|
|
52
|
+
"blank_lines": raw.blank,
|
|
53
|
+
"function_count": len(cc_results),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
def score_complexity(
|
|
57
|
+
self, agent_code: str, reference_code: str, reference_baseline: dict[str, int]
|
|
58
|
+
) -> dict[str, dict[str, int] | dict[str, float]]:
|
|
59
|
+
"""Score agent complexity relative to reference.
|
|
60
|
+
|
|
61
|
+
Uses formula: score = 1.0 - min(1.0, agent_metric / (ref_metric * 1.5))
|
|
62
|
+
|
|
63
|
+
This means:
|
|
64
|
+
- Agent simpler than reference → score closer to 1.0
|
|
65
|
+
- Agent 50% more complex → score = 0.5
|
|
66
|
+
- Agent 2x+ more complex → score = 0.0
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
agent_code: Agent's implementation source code
|
|
70
|
+
reference_code: Reference implementation (unused, for signature consistency)
|
|
71
|
+
reference_baseline: Expected metrics from reference-metadata.json
|
|
72
|
+
{
|
|
73
|
+
'cyclomatic': int,
|
|
74
|
+
'lines_of_code': int,
|
|
75
|
+
'function_count': int
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Dictionary with agent metrics and scores:
|
|
80
|
+
{
|
|
81
|
+
'agent_metrics': {...},
|
|
82
|
+
'reference_baseline': {...},
|
|
83
|
+
'scores': {
|
|
84
|
+
'cyclomatic_score': float,
|
|
85
|
+
'loc_score': float,
|
|
86
|
+
'function_score': float,
|
|
87
|
+
'overall_complexity': float
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
"""
|
|
91
|
+
agent_metrics = self.calculate_metrics(agent_code)
|
|
92
|
+
|
|
93
|
+
scores = {}
|
|
94
|
+
|
|
95
|
+
# Cyclomatic complexity score
|
|
96
|
+
ref_cc = max(reference_baseline.get("cyclomatic", 10), 1)
|
|
97
|
+
agent_cc = agent_metrics["cyclomatic_complexity"]
|
|
98
|
+
scores["cyclomatic_score"] = max(0.0, 1.0 - min(1.0, agent_cc / (ref_cc * 1.5)))
|
|
99
|
+
|
|
100
|
+
# LOC score
|
|
101
|
+
ref_loc = max(reference_baseline.get("lines_of_code", 50), 1)
|
|
102
|
+
agent_loc = agent_metrics["lines_of_code"]
|
|
103
|
+
scores["loc_score"] = max(0.0, 1.0 - min(1.0, agent_loc / (ref_loc * 1.5)))
|
|
104
|
+
|
|
105
|
+
# Function count score (penalize deviation in either direction)
|
|
106
|
+
ref_funcs = reference_baseline.get("function_count", 3)
|
|
107
|
+
agent_funcs = agent_metrics["function_count"]
|
|
108
|
+
scores["function_score"] = max(0.0, 1.0 - abs(agent_funcs - ref_funcs) / max(ref_funcs, 1))
|
|
109
|
+
|
|
110
|
+
# Weighted average: complexity (50%), LOC (30%), function count (20%)
|
|
111
|
+
scores["overall_complexity"] = (
|
|
112
|
+
0.5 * scores["cyclomatic_score"] + 0.3 * scores["loc_score"] + 0.2 * scores["function_score"]
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return {"agent_metrics": agent_metrics, "reference_baseline": reference_baseline, "scores": scores}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Code quality scoring using AST analysis.
|
|
2
|
+
|
|
3
|
+
This module provides the QualityScorer class for evaluating type hints,
|
|
4
|
+
docstrings, and error handling coverage.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import ast
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class QualityScorer:
|
|
11
|
+
"""Score code quality using AST analysis.
|
|
12
|
+
|
|
13
|
+
Evaluates:
|
|
14
|
+
- Type hints: Parameter and return type annotations
|
|
15
|
+
- Docstrings: Module and function documentation
|
|
16
|
+
- Error handling: Try/except blocks
|
|
17
|
+
|
|
18
|
+
All scores in range [0.0, 1.0] where 1.0 is highest quality.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def score_type_hints(self, code: str) -> float:
|
|
22
|
+
"""Check type annotation coverage.
|
|
23
|
+
|
|
24
|
+
Scores each function based on:
|
|
25
|
+
- Return type annotation (50% weight)
|
|
26
|
+
- Parameter type annotations (50% weight)
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
code: Python source code
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Type hint coverage score [0.0, 1.0]
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
SyntaxError: If code cannot be parsed
|
|
36
|
+
"""
|
|
37
|
+
tree = ast.parse(code)
|
|
38
|
+
functions = [n for n in ast.walk(tree) if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]
|
|
39
|
+
|
|
40
|
+
if not functions:
|
|
41
|
+
return 1.0 # No functions to annotate
|
|
42
|
+
|
|
43
|
+
total_score = 0.0
|
|
44
|
+
for func in functions:
|
|
45
|
+
# Return type (50% weight)
|
|
46
|
+
has_return = func.returns is not None
|
|
47
|
+
|
|
48
|
+
# Parameter types (50% weight) - exclude self/cls which don't need annotations
|
|
49
|
+
params = [arg for arg in func.args.args if arg.arg not in ("self", "cls")]
|
|
50
|
+
total_params = len(params)
|
|
51
|
+
if total_params > 0:
|
|
52
|
+
params_annotated = sum(1 for arg in params if arg.annotation)
|
|
53
|
+
param_coverage = params_annotated / total_params
|
|
54
|
+
else:
|
|
55
|
+
param_coverage = 1.0 # No params to annotate
|
|
56
|
+
|
|
57
|
+
func_score = 0.5 * (1.0 if has_return else 0.0) + 0.5 * param_coverage
|
|
58
|
+
total_score += func_score
|
|
59
|
+
|
|
60
|
+
return total_score / len(functions)
|
|
61
|
+
|
|
62
|
+
def score_docstrings(self, code: str) -> float:
|
|
63
|
+
"""Check docstring coverage.
|
|
64
|
+
|
|
65
|
+
Scores based on:
|
|
66
|
+
- Module docstring (30% weight)
|
|
67
|
+
- Function docstrings (70% weight)
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
code: Python source code
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Docstring coverage score [0.0, 1.0]
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
SyntaxError: If code cannot be parsed
|
|
77
|
+
"""
|
|
78
|
+
tree = ast.parse(code)
|
|
79
|
+
|
|
80
|
+
# Module docstring
|
|
81
|
+
module_doc = ast.get_docstring(tree) is not None
|
|
82
|
+
|
|
83
|
+
# Function docstrings
|
|
84
|
+
functions = [n for n in ast.walk(tree) if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]
|
|
85
|
+
if not functions:
|
|
86
|
+
return 1.0 if module_doc else 0.5
|
|
87
|
+
|
|
88
|
+
documented = sum(1 for func in functions if ast.get_docstring(func))
|
|
89
|
+
|
|
90
|
+
# 30% module doc, 70% function docs
|
|
91
|
+
return 0.3 * (1.0 if module_doc else 0.0) + 0.7 * (documented / len(functions))
|
|
92
|
+
|
|
93
|
+
def score_error_handling(self, code: str) -> float:
|
|
94
|
+
"""Check error handling presence.
|
|
95
|
+
|
|
96
|
+
Looks for try/except blocks relative to function count.
|
|
97
|
+
More try blocks (up to 1:1 ratio with functions) = better score.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
code: Python source code
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Error handling score [0.0, 1.0]
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
SyntaxError: If code cannot be parsed
|
|
107
|
+
"""
|
|
108
|
+
tree = ast.parse(code)
|
|
109
|
+
|
|
110
|
+
functions = [n for n in ast.walk(tree) if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]
|
|
111
|
+
try_blocks = [n for n in ast.walk(tree) if isinstance(n, ast.Try)]
|
|
112
|
+
|
|
113
|
+
if not functions:
|
|
114
|
+
return 0.5 # Neutral score for no functions
|
|
115
|
+
|
|
116
|
+
if not try_blocks:
|
|
117
|
+
return 0.3 # Partial credit (some tasks don't need error handling)
|
|
118
|
+
|
|
119
|
+
# More try blocks = better (up to 1.0)
|
|
120
|
+
return min(1.0, 0.5 + 0.5 * (len(try_blocks) / len(functions)))
|
|
121
|
+
|
|
122
|
+
def calculate_quality(self, code: str) -> dict[str, float]:
|
|
123
|
+
"""Calculate overall quality score with detailed breakdown.
|
|
124
|
+
|
|
125
|
+
Combines three quality metrics:
|
|
126
|
+
- Type hints (30% weight)
|
|
127
|
+
- Docstrings (30% weight)
|
|
128
|
+
- Error handling (40% weight)
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
code: Python source code
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Dictionary with individual scores and overall weighted average:
|
|
135
|
+
{
|
|
136
|
+
'type_hints': float,
|
|
137
|
+
'docstrings': float,
|
|
138
|
+
'error_handling': float,
|
|
139
|
+
'overall_quality': float
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
SyntaxError: If code cannot be parsed
|
|
144
|
+
"""
|
|
145
|
+
type_hints = self.score_type_hints(code)
|
|
146
|
+
docstrings = self.score_docstrings(code)
|
|
147
|
+
error_handling = self.score_error_handling(code)
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
"type_hints": type_hints,
|
|
151
|
+
"docstrings": docstrings,
|
|
152
|
+
"error_handling": error_handling,
|
|
153
|
+
"overall_quality": (0.3 * type_hints + 0.3 * docstrings + 0.4 * error_handling),
|
|
154
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Function signature-based code similarity scoring."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def score_function_signatures(agent_code: str, reference_code: str) -> float:
|
|
7
|
+
"""Compare function signatures using Jaccard similarity.
|
|
8
|
+
|
|
9
|
+
Extracts function signatures in format: 'name(arg1, arg2) -> return_type'
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
agent_code: Agent's implementation source code
|
|
13
|
+
reference_code: Reference implementation source code
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
Jaccard similarity [0.0, 1.0] of signature sets
|
|
17
|
+
|
|
18
|
+
Raises:
|
|
19
|
+
SyntaxError: If either code cannot be parsed
|
|
20
|
+
"""
|
|
21
|
+
agent_sigs = _extract_signatures(agent_code)
|
|
22
|
+
ref_sigs = _extract_signatures(reference_code)
|
|
23
|
+
|
|
24
|
+
# Jaccard similarity: |intersection| / |union|
|
|
25
|
+
if not agent_sigs and not ref_sigs:
|
|
26
|
+
return 1.0 # Both have no functions
|
|
27
|
+
|
|
28
|
+
common = len(agent_sigs & ref_sigs)
|
|
29
|
+
total = len(agent_sigs | ref_sigs)
|
|
30
|
+
return common / total if total > 0 else 0.0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _extract_signatures(code: str) -> set[str]:
|
|
34
|
+
"""Extract function signatures from code.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
code: Python source code
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Set of signatures like 'main(input: Input) -> Output'
|
|
41
|
+
"""
|
|
42
|
+
tree = ast.parse(code)
|
|
43
|
+
signatures = set()
|
|
44
|
+
|
|
45
|
+
for node in ast.walk(tree):
|
|
46
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
47
|
+
# Get argument names
|
|
48
|
+
args = [arg.arg for arg in node.args.args]
|
|
49
|
+
|
|
50
|
+
# Get return type annotation
|
|
51
|
+
ret = ast.unparse(node.returns) if node.returns else "None"
|
|
52
|
+
|
|
53
|
+
# Format: name(arg1, arg2) -> return_type
|
|
54
|
+
sig = f"{node.name}({', '.join(args)}) -> {ret}"
|
|
55
|
+
signatures.add(sig)
|
|
56
|
+
|
|
57
|
+
return signatures
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Code similarity scoring using AST and token analysis.
|
|
2
|
+
|
|
3
|
+
This module provides the SimilarityScorer class for evaluating structural
|
|
4
|
+
similarity between agent-generated code and reference implementations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .ast_similarity import score_ast_similarity
|
|
8
|
+
from .signature_similarity import score_function_signatures
|
|
9
|
+
from .token_similarity import score_token_similarity
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SimilarityScorer:
|
|
13
|
+
"""Calculate structural similarity between code files.
|
|
14
|
+
|
|
15
|
+
Compares code structure using:
|
|
16
|
+
- AST node types and tree structure
|
|
17
|
+
- Token sequences (types, not values)
|
|
18
|
+
- Function signatures (names, parameters, return types)
|
|
19
|
+
|
|
20
|
+
All scores are in range [0.0, 1.0] where 1.0 means identical structure.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def score_ast_similarity(self, agent_code: str, reference_code: str) -> float:
|
|
24
|
+
"""Compare AST structures using node type sequences.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
agent_code: Agent's implementation source code
|
|
28
|
+
reference_code: Reference implementation source code
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Similarity score [0.0, 1.0] based on node type sequence matching
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
SyntaxError: If either code cannot be parsed
|
|
35
|
+
"""
|
|
36
|
+
return score_ast_similarity(agent_code, reference_code)
|
|
37
|
+
|
|
38
|
+
def score_token_similarity(self, agent_code: str, reference_code: str) -> float:
|
|
39
|
+
"""Compare tokenized code sequences.
|
|
40
|
+
|
|
41
|
+
Uses token types (not literal values) for flexibility. For example,
|
|
42
|
+
different variable names or string literals won't affect the score.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
agent_code: Agent's implementation source code
|
|
46
|
+
reference_code: Reference implementation source code
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Similarity score [0.0, 1.0] based on token type sequence matching
|
|
50
|
+
"""
|
|
51
|
+
return score_token_similarity(agent_code, reference_code)
|
|
52
|
+
|
|
53
|
+
def score_function_signatures(self, agent_code: str, reference_code: str) -> float:
|
|
54
|
+
"""Compare function signatures using Jaccard similarity.
|
|
55
|
+
|
|
56
|
+
Extracts function signatures in format: 'name(arg1, arg2) -> return_type'
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
agent_code: Agent's implementation source code
|
|
60
|
+
reference_code: Reference implementation source code
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Jaccard similarity [0.0, 1.0] of signature sets
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
SyntaxError: If either code cannot be parsed
|
|
67
|
+
"""
|
|
68
|
+
return score_function_signatures(agent_code, reference_code)
|
|
69
|
+
|
|
70
|
+
def calculate_similarity(self, agent_code: str, reference_code: str) -> dict[str, float]:
|
|
71
|
+
"""Calculate overall similarity score with detailed breakdown.
|
|
72
|
+
|
|
73
|
+
Combines three similarity metrics:
|
|
74
|
+
- AST similarity (40% weight): Structural similarity
|
|
75
|
+
- Token similarity (30% weight): Implementation similarity
|
|
76
|
+
- Signature similarity (30% weight): API similarity
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
agent_code: Agent's implementation source code
|
|
80
|
+
reference_code: Reference implementation source code
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Dictionary with individual scores and overall weighted average:
|
|
84
|
+
{
|
|
85
|
+
'ast_similarity': float,
|
|
86
|
+
'token_similarity': float,
|
|
87
|
+
'signature_similarity': float,
|
|
88
|
+
'overall_similarity': float
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
SyntaxError: If either code cannot be parsed
|
|
93
|
+
"""
|
|
94
|
+
ast_sim = self.score_ast_similarity(agent_code, reference_code)
|
|
95
|
+
token_sim = self.score_token_similarity(agent_code, reference_code)
|
|
96
|
+
sig_sim = self.score_function_signatures(agent_code, reference_code)
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
"ast_similarity": ast_sim,
|
|
100
|
+
"token_similarity": token_sim,
|
|
101
|
+
"signature_similarity": sig_sim,
|
|
102
|
+
"overall_similarity": (0.4 * ast_sim + 0.3 * token_sim + 0.3 * sig_sim),
|
|
103
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Token-based code similarity scoring."""
|
|
2
|
+
|
|
3
|
+
import tokenize
|
|
4
|
+
from difflib import SequenceMatcher
|
|
5
|
+
from io import BytesIO
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def score_token_similarity(agent_code: str, reference_code: str) -> float:
|
|
9
|
+
"""Compare tokenized code sequences.
|
|
10
|
+
|
|
11
|
+
Uses token types (not literal values) for flexibility. For example,
|
|
12
|
+
different variable names or string literals won't affect the score.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
agent_code: Agent's implementation source code
|
|
16
|
+
reference_code: Reference implementation source code
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Similarity score [0.0, 1.0] based on token type sequence matching
|
|
20
|
+
"""
|
|
21
|
+
agent_tokens = _tokenize(agent_code)
|
|
22
|
+
ref_tokens = _tokenize(reference_code)
|
|
23
|
+
|
|
24
|
+
return SequenceMatcher(None, agent_tokens, ref_tokens).ratio()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _tokenize(code: str) -> list[str]:
|
|
28
|
+
"""Extract token types from code.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
code: Python source code
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
List of token type names (e.g., ['NAME', 'OP', 'NUMBER'])
|
|
35
|
+
"""
|
|
36
|
+
tokens = []
|
|
37
|
+
try:
|
|
38
|
+
readline = BytesIO(code.encode()).readline
|
|
39
|
+
for tok in tokenize.tokenize(readline):
|
|
40
|
+
tokens.append(tokenize.tok_name[tok.type])
|
|
41
|
+
except tokenize.TokenError:
|
|
42
|
+
# Incomplete code - return what we have
|
|
43
|
+
pass
|
|
44
|
+
return tokens
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Multi-turn user simulation for interactive coding-agent evaluation.
|
|
2
|
+
|
|
3
|
+
This package adds a dialog loop on top of the single-shot evaluation:
|
|
4
|
+
instead of one prompt → one agent response → criteria check, the coding
|
|
5
|
+
agent converses with a simulated user (a second LLM driven by persona +
|
|
6
|
+
goal) until the task is complete, the simulator emits a stop token, or a
|
|
7
|
+
turn / token budget is exhausted.
|
|
8
|
+
|
|
9
|
+
Public API:
|
|
10
|
+
- ``UserSimulator`` — wraps an LLM invoker (Anthropic direct or AWS
|
|
11
|
+
Bedrock) and produces the next simulated-user utterance.
|
|
12
|
+
- ``SimulatorResult`` — simulator output + token accounting + stop flag.
|
|
13
|
+
- ``DialogStopReason`` — enum for why a dialog ended.
|
|
14
|
+
- ``evaluate_stop`` — pure predicate that decides whether a dialog
|
|
15
|
+
should terminate after a given turn.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from coder_eval.simulation.termination import DialogStopReason, evaluate_stop
|
|
19
|
+
from coder_eval.simulation.user_simulator import SimulatorResult, UserSimulator
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"DialogStopReason",
|
|
24
|
+
"SimulatorResult",
|
|
25
|
+
"UserSimulator",
|
|
26
|
+
"evaluate_stop",
|
|
27
|
+
]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Termination predicate for simulated dialogs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
|
|
8
|
+
from coder_eval.models import SimulationConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DialogStopReason(StrEnum):
|
|
12
|
+
"""Why a simulated dialog ended."""
|
|
13
|
+
|
|
14
|
+
CRITERIA_PASSED = "criteria_passed"
|
|
15
|
+
STOP_TOKEN = "stop_token"
|
|
16
|
+
MAX_TURNS = "max_turns"
|
|
17
|
+
BUDGET = "budget"
|
|
18
|
+
ERROR = "error"
|
|
19
|
+
RUN_LIMIT_EXCEEDED = "run_limit_exceeded"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class StopDecision:
|
|
24
|
+
"""Outcome of a termination check.
|
|
25
|
+
|
|
26
|
+
``stop=False`` means "keep dialoguing"; ``reason`` is meaningful only when
|
|
27
|
+
``stop=True``.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
stop: bool
|
|
31
|
+
reason: DialogStopReason | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def evaluate_stop(
|
|
35
|
+
*,
|
|
36
|
+
config: SimulationConfig,
|
|
37
|
+
turns_completed: int,
|
|
38
|
+
total_tokens_used: int,
|
|
39
|
+
criteria_all_passed: bool,
|
|
40
|
+
) -> StopDecision:
|
|
41
|
+
"""Decide whether the current dialog should terminate.
|
|
42
|
+
|
|
43
|
+
Precedence (first matching wins):
|
|
44
|
+
1. criteria passed AND ``stop_on_criteria_pass`` — CRITERIA_PASSED
|
|
45
|
+
2. ``turns_completed >= max_turns`` — MAX_TURNS
|
|
46
|
+
3. ``max_total_tokens`` exceeded — BUDGET
|
|
47
|
+
4. otherwise — keep going
|
|
48
|
+
|
|
49
|
+
Stop-token detection is NOT handled here: the orchestrator checks
|
|
50
|
+
``SimulatorResult.stop_requested`` directly on the fresh simulator output
|
|
51
|
+
each turn, which is strictly stronger than re-scanning the previous turn's
|
|
52
|
+
message. Keeping stop-token logic in one place avoids a redundant branch.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
config: Simulation configuration.
|
|
56
|
+
turns_completed: Number of user↔agent exchanges completed so far.
|
|
57
|
+
A turn is considered "completed" once the agent has replied to a
|
|
58
|
+
user prompt.
|
|
59
|
+
total_tokens_used: Running total of input+output tokens consumed by
|
|
60
|
+
both the simulator and the coding agent across the dialog.
|
|
61
|
+
criteria_all_passed: Whether all task success criteria currently
|
|
62
|
+
evaluate to a passing score (meaningful only when the caller
|
|
63
|
+
checks criteria at this point — callers using
|
|
64
|
+
``check_criteria='end_of_dialog'`` must pass False).
|
|
65
|
+
"""
|
|
66
|
+
if config.stop_on_criteria_pass and criteria_all_passed:
|
|
67
|
+
return StopDecision(stop=True, reason=DialogStopReason.CRITERIA_PASSED)
|
|
68
|
+
|
|
69
|
+
if turns_completed >= config.max_turns:
|
|
70
|
+
return StopDecision(stop=True, reason=DialogStopReason.MAX_TURNS)
|
|
71
|
+
|
|
72
|
+
if config.max_total_tokens is not None and total_tokens_used >= config.max_total_tokens:
|
|
73
|
+
return StopDecision(stop=True, reason=DialogStopReason.BUDGET)
|
|
74
|
+
|
|
75
|
+
return StopDecision(stop=False)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def strip_stop_token(message: str, stop_token: str) -> str:
|
|
79
|
+
"""Remove the stop token from a simulator message, preserving surrounding text.
|
|
80
|
+
|
|
81
|
+
The stop token is a terminator, not part of the user's utterance — the
|
|
82
|
+
agent should never see it. Returns the message with the token (and any
|
|
83
|
+
adjacent whitespace that would be left stranded) removed.
|
|
84
|
+
"""
|
|
85
|
+
if stop_token not in message:
|
|
86
|
+
return message
|
|
87
|
+
cleaned = message.replace(stop_token, "")
|
|
88
|
+
return cleaned.strip()
|