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,132 @@
|
|
|
1
|
+
"""Per-field merge-strategy marker + reader.
|
|
2
|
+
|
|
3
|
+
A single declarative mechanism for how a config field merges across the 5
|
|
4
|
+
resolution layers (default -> experiment-defaults -> task -> variant -> CLI).
|
|
5
|
+
The strategy is stored on the Pydantic ``FieldInfo`` via ``json_schema_extra``
|
|
6
|
+
and read back by :func:`merge_strategy_of`.
|
|
7
|
+
|
|
8
|
+
A field WITHOUT an explicit ``MergeField`` annotation falls back to a
|
|
9
|
+
type-aware default (:func:`_default_strategy_for`): a nested ``BaseModel`` or a
|
|
10
|
+
free-form ``dict`` field merges ``deep``; a ``list`` or scalar field merges
|
|
11
|
+
``replace``. ``MergeField(strategy=...)`` is only for a *deliberate* override of
|
|
12
|
+
that default — most commonly a ``list`` that should ``append`` rather than
|
|
13
|
+
``replace`` (the one genuinely ambiguous case, enforced by lint rule CE014).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import types
|
|
19
|
+
from typing import Annotated, Any, Literal, Union, get_args, get_origin
|
|
20
|
+
|
|
21
|
+
from pydantic import BaseModel, Field
|
|
22
|
+
from pydantic.fields import FieldInfo
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
MERGE_STRATEGY_KEY = "x_merge_strategy"
|
|
26
|
+
"""``json_schema_extra`` key under which a field's explicit merge strategy lives."""
|
|
27
|
+
|
|
28
|
+
APPEND_ORDER_KEY = "x_merge_append_order"
|
|
29
|
+
"""``json_schema_extra`` key for an ``append`` field's contribution order."""
|
|
30
|
+
|
|
31
|
+
MergeStrategy = Literal["deep", "append", "replace"]
|
|
32
|
+
"""``deep`` = recurse (per-field for nested models, recursive-dict for free-form
|
|
33
|
+
dicts); ``append`` = concatenate lists in layer order; ``replace`` = last layer wins."""
|
|
34
|
+
|
|
35
|
+
AppendOrder = Literal["forward", "reverse"]
|
|
36
|
+
"""For ``append`` fields: ``forward`` (default) concatenates in layer order
|
|
37
|
+
(lower precedence first); ``reverse`` puts each higher layer's items FIRST (used
|
|
38
|
+
by ``post_run``, where experiment-defaults cleanup must run after the task's)."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def MergeField(*, strategy: MergeStrategy, append_order: AppendOrder = "forward", **kwargs: Any) -> Any: # noqa: N802 — Field-style factory
|
|
42
|
+
"""A ``Field(...)`` that also records an explicit merge ``strategy``.
|
|
43
|
+
|
|
44
|
+
``strategy`` is REQUIRED — ``MergeField`` exists only to override the
|
|
45
|
+
type-aware default. A field that wants the default uses a plain ``Field``.
|
|
46
|
+
``append_order`` only applies to ``strategy="append"`` (``"reverse"`` flips
|
|
47
|
+
the contribution order). Any caller-supplied ``json_schema_extra`` is merged,
|
|
48
|
+
not clobbered.
|
|
49
|
+
"""
|
|
50
|
+
extra = dict(kwargs.pop("json_schema_extra", {}) or {})
|
|
51
|
+
extra[MERGE_STRATEGY_KEY] = strategy
|
|
52
|
+
if append_order != "forward":
|
|
53
|
+
extra[APPEND_ORDER_KEY] = append_order
|
|
54
|
+
return Field(json_schema_extra=extra, **kwargs)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def append_order_of(field_info: FieldInfo) -> AppendOrder:
|
|
58
|
+
"""Return an ``append`` field's contribution order (default ``forward``)."""
|
|
59
|
+
extra = field_info.json_schema_extra
|
|
60
|
+
if isinstance(extra, dict) and APPEND_ORDER_KEY in extra:
|
|
61
|
+
order = extra[APPEND_ORDER_KEY]
|
|
62
|
+
# The value is written only by MergeField (its `append_order` param is
|
|
63
|
+
# typed AppendOrder), but guard a hand-written
|
|
64
|
+
# `Field(json_schema_extra={...})` with a bogus value so it fails loud
|
|
65
|
+
# here instead of silently mis-ordering an append.
|
|
66
|
+
if order not in get_args(AppendOrder):
|
|
67
|
+
raise ValueError(f"invalid append order {order!r}; expected one of {get_args(AppendOrder)}")
|
|
68
|
+
return order # type: ignore[return-value] # validated against AppendOrder above
|
|
69
|
+
return "forward"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def merge_strategy_of(field_info: FieldInfo) -> MergeStrategy:
|
|
73
|
+
"""Return the field's explicit strategy if annotated, else the type-aware default."""
|
|
74
|
+
extra = field_info.json_schema_extra
|
|
75
|
+
if isinstance(extra, dict) and MERGE_STRATEGY_KEY in extra:
|
|
76
|
+
strategy = extra[MERGE_STRATEGY_KEY]
|
|
77
|
+
# The value is written only by MergeField (its `strategy` param is typed
|
|
78
|
+
# MergeStrategy) and CE014 enforces that every list field declares one,
|
|
79
|
+
# but guard a hand-written `Field(json_schema_extra={...})` with a bogus
|
|
80
|
+
# value so it fails loud here instead of silently falling through to
|
|
81
|
+
# "replace" in the merge engine.
|
|
82
|
+
if strategy not in get_args(MergeStrategy):
|
|
83
|
+
raise ValueError(f"invalid merge strategy {strategy!r}; expected one of {get_args(MergeStrategy)}")
|
|
84
|
+
return strategy # type: ignore[return-value] # validated against MergeStrategy above
|
|
85
|
+
return _default_strategy_for(field_info.annotation)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _default_strategy_for(annotation: Any) -> MergeStrategy:
|
|
89
|
+
"""Type-aware default: nested ``BaseModel`` / free-form ``dict`` -> ``deep``;
|
|
90
|
+
``list`` / scalar -> ``replace``. Unwraps ``Optional`` / ``Annotated`` / union.
|
|
91
|
+
"""
|
|
92
|
+
models, is_free_form_dict = classify_annotation(annotation)
|
|
93
|
+
return "deep" if (models or is_free_form_dict) else "replace"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def classify_annotation(annotation: Any) -> tuple[list[type[BaseModel]], bool]:
|
|
97
|
+
"""Return ``(nested_model_types, is_free_form_dict)`` for a field annotation.
|
|
98
|
+
|
|
99
|
+
Unwraps ``Optional`` / ``Annotated`` / unions and collects directly-nested
|
|
100
|
+
``BaseModel`` subclasses (so a resolver can descend into them). A ``dict[...]``
|
|
101
|
+
origin sets the free-form flag (descent stops there; any key is accepted).
|
|
102
|
+
``list[...]`` and scalars contribute neither — they are leaves.
|
|
103
|
+
"""
|
|
104
|
+
if annotation is None:
|
|
105
|
+
return [], False
|
|
106
|
+
|
|
107
|
+
if hasattr(annotation, "__value__"): # PEP 695 ``type X = ...`` alias
|
|
108
|
+
annotation = annotation.__value__
|
|
109
|
+
if get_origin(annotation) is Annotated:
|
|
110
|
+
annotation = get_args(annotation)[0]
|
|
111
|
+
|
|
112
|
+
origin = get_origin(annotation)
|
|
113
|
+
args = get_args(annotation) if origin in (Union, types.UnionType) else (annotation,)
|
|
114
|
+
|
|
115
|
+
models: list[type[BaseModel]] = []
|
|
116
|
+
is_free_form_dict = False
|
|
117
|
+
for arg in args:
|
|
118
|
+
if hasattr(arg, "__value__"):
|
|
119
|
+
arg = arg.__value__
|
|
120
|
+
if get_origin(arg) is Annotated:
|
|
121
|
+
arg = get_args(arg)[0]
|
|
122
|
+
arg_origin = get_origin(arg)
|
|
123
|
+
if arg_origin is dict or arg is dict: # parameterized ``dict[...]`` or the bare ``dict``
|
|
124
|
+
is_free_form_dict = True
|
|
125
|
+
elif arg_origin in (Union, types.UnionType):
|
|
126
|
+
sub_models, sub_dict = classify_annotation(arg)
|
|
127
|
+
models.extend(sub_models)
|
|
128
|
+
is_free_form_dict = is_free_form_dict or sub_dict
|
|
129
|
+
elif arg_origin is None and isinstance(arg, type) and issubclass(arg, BaseModel):
|
|
130
|
+
models.append(arg)
|
|
131
|
+
# list / other origins -> leaf, ignored
|
|
132
|
+
return models, is_free_form_dict
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Prompt mutation models and application function.
|
|
2
|
+
|
|
3
|
+
Prompt mutations are ordered transforms applied to a task's base initial_prompt
|
|
4
|
+
at variant resolution time. They enable A/B testing of prompt phrasing without
|
|
5
|
+
duplicating task definitions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from typing import Annotated, Literal
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PromptPrefix(BaseModel):
|
|
17
|
+
"""Prepend text to the prompt."""
|
|
18
|
+
|
|
19
|
+
model_config = ConfigDict(extra="forbid")
|
|
20
|
+
|
|
21
|
+
type: Literal["prefix"] = "prefix"
|
|
22
|
+
content: str = Field(description="Text to prepend before the base prompt")
|
|
23
|
+
separator: str = Field(default="\n\n", description="Separator between prefix and base prompt")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PromptSuffix(BaseModel):
|
|
27
|
+
"""Append text to the prompt."""
|
|
28
|
+
|
|
29
|
+
model_config = ConfigDict(extra="forbid")
|
|
30
|
+
|
|
31
|
+
type: Literal["suffix"] = "suffix"
|
|
32
|
+
content: str = Field(description="Text to append after the base prompt")
|
|
33
|
+
separator: str = Field(default="\n\n", description="Separator between base prompt and suffix")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class PromptReplace(BaseModel):
|
|
37
|
+
"""Find and replace text in the prompt."""
|
|
38
|
+
|
|
39
|
+
model_config = ConfigDict(extra="forbid")
|
|
40
|
+
|
|
41
|
+
type: Literal["replace"] = "replace"
|
|
42
|
+
pattern: str = Field(description="Text or regex pattern to find")
|
|
43
|
+
replacement: str = Field(description="Replacement text")
|
|
44
|
+
regex: bool = Field(default=False, description="Whether pattern is a regular expression")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class PromptTemplate(BaseModel):
|
|
48
|
+
"""Substitute template variables in the prompt using {variable_name} syntax."""
|
|
49
|
+
|
|
50
|
+
model_config = ConfigDict(extra="forbid")
|
|
51
|
+
|
|
52
|
+
type: Literal["template"] = "template"
|
|
53
|
+
variables: dict[str, str] = Field(description="Mapping of variable names to values")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
PromptMutation = Annotated[
|
|
57
|
+
PromptPrefix | PromptSuffix | PromptReplace | PromptTemplate,
|
|
58
|
+
Field(discriminator="type"),
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def apply_prompt_mutations(
|
|
63
|
+
base_prompt: str,
|
|
64
|
+
mutations: list[PromptMutation],
|
|
65
|
+
) -> str:
|
|
66
|
+
"""Apply an ordered list of mutations to a base prompt string.
|
|
67
|
+
|
|
68
|
+
Mutations are applied sequentially — each operates on the result of the previous.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
base_prompt: The original prompt text.
|
|
72
|
+
mutations: Ordered list of mutation operations.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
The transformed prompt string.
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
re.error: If a regex replace has an invalid pattern.
|
|
79
|
+
"""
|
|
80
|
+
result = base_prompt
|
|
81
|
+
for m in mutations:
|
|
82
|
+
match m:
|
|
83
|
+
case PromptPrefix():
|
|
84
|
+
result = m.content + m.separator + result
|
|
85
|
+
case PromptSuffix():
|
|
86
|
+
result = result + m.separator + m.content
|
|
87
|
+
case PromptReplace():
|
|
88
|
+
if m.regex:
|
|
89
|
+
result = re.sub(m.pattern, m.replacement, result)
|
|
90
|
+
else:
|
|
91
|
+
result = result.replace(m.pattern, m.replacement)
|
|
92
|
+
case PromptTemplate():
|
|
93
|
+
for key, val in m.variables.items():
|
|
94
|
+
result = result.replace(f"{{{key}}}", val)
|
|
95
|
+
return result
|