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.
Files changed (127) hide show
  1. coder_eval/.gitattributes +2 -0
  2. coder_eval/__init__.py +3 -0
  3. coder_eval/agent.py +302 -0
  4. coder_eval/agents/__init__.py +41 -0
  5. coder_eval/agents/_logging.py +89 -0
  6. coder_eval/agents/antigravity_agent.py +811 -0
  7. coder_eval/agents/claude_code_agent.py +1701 -0
  8. coder_eval/agents/codex_agent.py +2055 -0
  9. coder_eval/agents/noop_agent.py +100 -0
  10. coder_eval/agents/registry.py +163 -0
  11. coder_eval/agents/watchdog.py +116 -0
  12. coder_eval/analysis.py +88 -0
  13. coder_eval/cli/__init__.py +87 -0
  14. coder_eval/cli/aggregate_command.py +153 -0
  15. coder_eval/cli/console.py +7 -0
  16. coder_eval/cli/evaluate_command.py +164 -0
  17. coder_eval/cli/plan_command.py +152 -0
  18. coder_eval/cli/report_command.py +121 -0
  19. coder_eval/cli/run_command.py +686 -0
  20. coder_eval/cli/run_helpers.py +137 -0
  21. coder_eval/cli/run_task_internal_command.py +210 -0
  22. coder_eval/cli/utils.py +37 -0
  23. coder_eval/config.py +212 -0
  24. coder_eval/criteria/__init__.py +163 -0
  25. coder_eval/criteria/_classification_aggregate.py +106 -0
  26. coder_eval/criteria/agent_judge.py +425 -0
  27. coder_eval/criteria/base.py +248 -0
  28. coder_eval/criteria/classification_match.py +107 -0
  29. coder_eval/criteria/command_executed.py +181 -0
  30. coder_eval/criteria/commands_efficiency.py +73 -0
  31. coder_eval/criteria/file_check.py +119 -0
  32. coder_eval/criteria/file_contains.py +85 -0
  33. coder_eval/criteria/file_exists.py +45 -0
  34. coder_eval/criteria/file_matches_regex.py +86 -0
  35. coder_eval/criteria/json_check.py +184 -0
  36. coder_eval/criteria/llm_judge.py +260 -0
  37. coder_eval/criteria/reference_comparison.py +130 -0
  38. coder_eval/criteria/run_command.py +220 -0
  39. coder_eval/criteria/skill_triggered.py +111 -0
  40. coder_eval/criteria/uipath_eval.py +223 -0
  41. coder_eval/errors/__init__.py +26 -0
  42. coder_eval/errors/agent.py +26 -0
  43. coder_eval/errors/budget.py +28 -0
  44. coder_eval/errors/categories.py +205 -0
  45. coder_eval/errors/categorization.py +240 -0
  46. coder_eval/errors/executor.py +124 -0
  47. coder_eval/errors/judge.py +12 -0
  48. coder_eval/errors/retry.py +211 -0
  49. coder_eval/errors/timeout.py +63 -0
  50. coder_eval/evaluation/__init__.py +10 -0
  51. coder_eval/evaluation/checker.py +260 -0
  52. coder_eval/evaluation/judge_anthropic.py +55 -0
  53. coder_eval/evaluation/judge_bedrock.py +114 -0
  54. coder_eval/evaluation/judge_context.py +497 -0
  55. coder_eval/evaluation/judge_models.py +53 -0
  56. coder_eval/evaluation/judge_persistence.py +291 -0
  57. coder_eval/evaluation/judge_usage.py +50 -0
  58. coder_eval/evaluation/sub_agent.py +231 -0
  59. coder_eval/evaluation/summaries.py +48 -0
  60. coder_eval/evaluation/verdict_tool.py +213 -0
  61. coder_eval/formatting.py +191 -0
  62. coder_eval/isolation/__init__.py +15 -0
  63. coder_eval/isolation/docker_runner.py +1253 -0
  64. coder_eval/logging_config.py +399 -0
  65. coder_eval/models/__init__.py +337 -0
  66. coder_eval/models/agent_config.py +373 -0
  67. coder_eval/models/container_paths.py +26 -0
  68. coder_eval/models/criteria.py +942 -0
  69. coder_eval/models/enums.py +121 -0
  70. coder_eval/models/experiment.py +314 -0
  71. coder_eval/models/judge.py +90 -0
  72. coder_eval/models/judge_defaults.py +11 -0
  73. coder_eval/models/limits.py +89 -0
  74. coder_eval/models/merge_strategy.py +132 -0
  75. coder_eval/models/mutations.py +95 -0
  76. coder_eval/models/results.py +787 -0
  77. coder_eval/models/routing.py +160 -0
  78. coder_eval/models/sandbox.py +371 -0
  79. coder_eval/models/tasks.py +631 -0
  80. coder_eval/models/telemetry.py +454 -0
  81. coder_eval/models/templates.py +108 -0
  82. coder_eval/orchestration/__init__.py +13 -0
  83. coder_eval/orchestration/batch.py +690 -0
  84. coder_eval/orchestration/config.py +111 -0
  85. coder_eval/orchestration/config_merge.py +431 -0
  86. coder_eval/orchestration/evaluation.py +94 -0
  87. coder_eval/orchestration/experiment.py +837 -0
  88. coder_eval/orchestration/overrides.py +206 -0
  89. coder_eval/orchestration/task_loader.py +437 -0
  90. coder_eval/orchestrator.py +2078 -0
  91. coder_eval/path_utils.py +79 -0
  92. coder_eval/plugins.py +81 -0
  93. coder_eval/pricing.py +169 -0
  94. coder_eval/py.typed +0 -0
  95. coder_eval/reports.py +1066 -0
  96. coder_eval/reports_experiment.py +761 -0
  97. coder_eval/reports_html.py +1659 -0
  98. coder_eval/reports_stats.py +250 -0
  99. coder_eval/resources/__init__.py +137 -0
  100. coder_eval/resources/default_experiment.yaml +85 -0
  101. coder_eval/resources/default_ignore_patterns.yaml +60 -0
  102. coder_eval/resources/tags.yaml +55 -0
  103. coder_eval/sandbox.py +1127 -0
  104. coder_eval/scoring/__init__.py +11 -0
  105. coder_eval/scoring/ast_similarity.py +38 -0
  106. coder_eval/scoring/complexity.py +115 -0
  107. coder_eval/scoring/quality.py +154 -0
  108. coder_eval/scoring/signature_similarity.py +57 -0
  109. coder_eval/scoring/similarity.py +103 -0
  110. coder_eval/scoring/token_similarity.py +44 -0
  111. coder_eval/simulation/__init__.py +27 -0
  112. coder_eval/simulation/termination.py +88 -0
  113. coder_eval/simulation/user_simulator.py +324 -0
  114. coder_eval/streaming/__init__.py +44 -0
  115. coder_eval/streaming/callbacks.py +57 -0
  116. coder_eval/streaming/collector.py +193 -0
  117. coder_eval/streaming/events.py +198 -0
  118. coder_eval/streaming/renderers.py +248 -0
  119. coder_eval/streaming/wire.py +115 -0
  120. coder_eval/telemetry.py +407 -0
  121. coder_eval/utils.py +517 -0
  122. coder_eval-0.8.2.dist-info/METADATA +242 -0
  123. coder_eval-0.8.2.dist-info/RECORD +127 -0
  124. coder_eval-0.8.2.dist-info/WHEEL +4 -0
  125. coder_eval-0.8.2.dist-info/entry_points.txt +5 -0
  126. coder_eval-0.8.2.dist-info/licenses/LICENSE +201 -0
  127. 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()