codeoptix 0.1.0__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.
- codeoptix/__init__.py +8 -0
- codeoptix/acp/__init__.py +33 -0
- codeoptix/acp/agent.py +209 -0
- codeoptix/acp/bridge.py +402 -0
- codeoptix/acp/client_adapter.py +312 -0
- codeoptix/acp/code_extractor.py +125 -0
- codeoptix/acp/orchestrator.py +349 -0
- codeoptix/acp/registry.py +294 -0
- codeoptix/adapters/__init__.py +18 -0
- codeoptix/adapters/base.py +50 -0
- codeoptix/adapters/basic.py +195 -0
- codeoptix/adapters/claude_code.py +218 -0
- codeoptix/adapters/codex.py +327 -0
- codeoptix/adapters/factory.py +56 -0
- codeoptix/adapters/gemini_cli.py +370 -0
- codeoptix/artifacts/__init__.py +5 -0
- codeoptix/artifacts/manager.py +193 -0
- codeoptix/behaviors/__init__.py +45 -0
- codeoptix/behaviors/base.py +81 -0
- codeoptix/behaviors/insecure_code.py +129 -0
- codeoptix/behaviors/plan_drift.py +192 -0
- codeoptix/behaviors/vacuous_tests.py +198 -0
- codeoptix/cli.py +1472 -0
- codeoptix/evaluation/__init__.py +23 -0
- codeoptix/evaluation/bloom_integration.py +271 -0
- codeoptix/evaluation/engine.py +274 -0
- codeoptix/evaluation/evaluators.py +308 -0
- codeoptix/evaluation/scenario_generator.py +222 -0
- codeoptix/evolution/__init__.py +7 -0
- codeoptix/evolution/engine.py +206 -0
- codeoptix/evolution/gepa_integration.py +149 -0
- codeoptix/evolution/proposer.py +185 -0
- codeoptix/linters/__init__.py +13 -0
- codeoptix/linters/bandit_linter.py +172 -0
- codeoptix/linters/base.py +105 -0
- codeoptix/linters/coverage_linter.py +156 -0
- codeoptix/linters/flake8_linter.py +156 -0
- codeoptix/linters/html_accessibility_linter.py +374 -0
- codeoptix/linters/language_detector.py +150 -0
- codeoptix/linters/mypy_linter.py +184 -0
- codeoptix/linters/pip_audit_linter.py +152 -0
- codeoptix/linters/pylint_linter.py +198 -0
- codeoptix/linters/ruff_linter.py +206 -0
- codeoptix/linters/runner.py +186 -0
- codeoptix/linters/safety_linter.py +184 -0
- codeoptix/reflection/__init__.py +6 -0
- codeoptix/reflection/engine.py +70 -0
- codeoptix/reflection/generator.py +209 -0
- codeoptix/utils/__init__.py +1 -0
- codeoptix/utils/config.py +91 -0
- codeoptix/utils/llm.py +332 -0
- codeoptix/utils/retry.py +133 -0
- codeoptix/vendor/__init__.py +2 -0
- codeoptix/vendor/bloom/README.md +26 -0
- codeoptix/vendor/bloom/__init__.py +11 -0
- codeoptix/vendor/bloom/globals.py +39 -0
- codeoptix/vendor/bloom/orchestrators/ConversationOrchestrator.py +450 -0
- codeoptix/vendor/bloom/orchestrators/SimEnvOrchestrator.py +839 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/README.md +85 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/default.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/ideation-default.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_animal-welfare.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_contextual-optimism.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_defend-objects.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_defer-to-users.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_emotional-bond.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_flattery.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_hardcode-test-cases.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_increasing-pep.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_research-sandbagging.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_self-promotion.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/sandbag.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/self-preferential-bias.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/static-prompts.yaml +72 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/web-search.json +18 -0
- codeoptix/vendor/bloom/prompts/step1_understanding.py +63 -0
- codeoptix/vendor/bloom/prompts/step2_ideation.py +254 -0
- codeoptix/vendor/bloom/prompts/step3_rollout.py +120 -0
- codeoptix/vendor/bloom/prompts/step4_judgment.py +183 -0
- codeoptix/vendor/bloom/schemas/behavior.schema.json +160 -0
- codeoptix/vendor/bloom/schemas/conversation.schema.json +51 -0
- codeoptix/vendor/bloom/schemas/transcript_schema.json +2225 -0
- codeoptix/vendor/bloom/scripts/step2_ideation.py +667 -0
- codeoptix/vendor/bloom/scripts/step4_judgment.py +811 -0
- codeoptix/vendor/bloom/transcript_utils.py +440 -0
- codeoptix/vendor/bloom/utils.py +700 -0
- codeoptix-0.1.0.dist-info/METADATA +304 -0
- codeoptix-0.1.0.dist-info/RECORD +91 -0
- codeoptix-0.1.0.dist-info/WHEEL +4 -0
- codeoptix-0.1.0.dist-info/entry_points.txt +2 -0
- codeoptix-0.1.0.dist-info/licenses/LICENSE +203 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Evaluation engine for CodeOptix."""
|
|
2
|
+
|
|
3
|
+
from codeoptix.evaluation.engine import EvaluationEngine
|
|
4
|
+
from codeoptix.evaluation.evaluators import (
|
|
5
|
+
ArtifactComparator,
|
|
6
|
+
LLMEvaluator,
|
|
7
|
+
StaticAnalyzer,
|
|
8
|
+
TestRunner,
|
|
9
|
+
)
|
|
10
|
+
from codeoptix.evaluation.scenario_generator import (
|
|
11
|
+
BloomScenarioGenerator,
|
|
12
|
+
ScenarioGenerator,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"ArtifactComparator",
|
|
17
|
+
"BloomScenarioGenerator",
|
|
18
|
+
"EvaluationEngine",
|
|
19
|
+
"LLMEvaluator",
|
|
20
|
+
"ScenarioGenerator",
|
|
21
|
+
"StaticAnalyzer",
|
|
22
|
+
"TestRunner",
|
|
23
|
+
]
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Full Bloom integration for scenario generation."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from codeoptix.utils.llm import LLMClient
|
|
7
|
+
from codeoptix.vendor.bloom.prompts.step2_ideation import (
|
|
8
|
+
make_all_scenarios_prompt,
|
|
9
|
+
make_system_prompt,
|
|
10
|
+
make_variation_prompt,
|
|
11
|
+
make_variation_system_prompt,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BloomIdeationIntegration:
|
|
16
|
+
"""
|
|
17
|
+
Full Bloom ideation integration for scenario generation.
|
|
18
|
+
|
|
19
|
+
Uses vendored Bloom scripts for sophisticated scenario ideation
|
|
20
|
+
and variation generation.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, llm_client: LLMClient, config: dict[str, Any] | None = None):
|
|
24
|
+
"""
|
|
25
|
+
Initialize Bloom ideation integration.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
llm_client: LLM client for scenario generation
|
|
29
|
+
config: Configuration dictionary
|
|
30
|
+
"""
|
|
31
|
+
self.llm_client = llm_client
|
|
32
|
+
self.config = config or {}
|
|
33
|
+
self.model = self.config.get("model", "gpt-4o")
|
|
34
|
+
self.num_base_scenarios = self.config.get("num_base_scenarios", 3)
|
|
35
|
+
self.num_variations = self.config.get("num_variations", 2)
|
|
36
|
+
|
|
37
|
+
def generate_scenarios(
|
|
38
|
+
self,
|
|
39
|
+
behavior_name: str,
|
|
40
|
+
behavior_description: str,
|
|
41
|
+
examples: list[dict[str, Any]] | None = None,
|
|
42
|
+
) -> list[dict[str, Any]]:
|
|
43
|
+
"""
|
|
44
|
+
Generate scenarios using full Bloom ideation pipeline.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
behavior_name: Name of the behavior
|
|
48
|
+
behavior_description: Description of the behavior
|
|
49
|
+
examples: Optional example scenarios
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
List of scenario dictionaries
|
|
53
|
+
"""
|
|
54
|
+
# Step 1: Generate base scenarios using Bloom ideation
|
|
55
|
+
base_scenarios = self._generate_base_scenarios(
|
|
56
|
+
behavior_name=behavior_name,
|
|
57
|
+
behavior_description=behavior_description,
|
|
58
|
+
examples=examples or [],
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Step 2: Generate variations for each base scenario
|
|
62
|
+
all_scenarios = []
|
|
63
|
+
for base_scenario in base_scenarios:
|
|
64
|
+
all_scenarios.append(base_scenario)
|
|
65
|
+
|
|
66
|
+
# Generate variations
|
|
67
|
+
variations = self._generate_variations(
|
|
68
|
+
base_scenario=base_scenario,
|
|
69
|
+
behavior_name=behavior_name,
|
|
70
|
+
behavior_description=behavior_description,
|
|
71
|
+
)
|
|
72
|
+
all_scenarios.extend(variations)
|
|
73
|
+
|
|
74
|
+
return all_scenarios[: self.num_base_scenarios * (1 + self.num_variations)]
|
|
75
|
+
|
|
76
|
+
def _generate_base_scenarios(
|
|
77
|
+
self, behavior_name: str, behavior_description: str, examples: list[dict[str, Any]]
|
|
78
|
+
) -> list[dict[str, Any]]:
|
|
79
|
+
"""Generate base scenarios using Bloom ideation prompts."""
|
|
80
|
+
# Create system prompt using Bloom's ideation system prompt
|
|
81
|
+
system_prompt = make_system_prompt(
|
|
82
|
+
behavior_name=behavior_name, behavior_description=behavior_description
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Create scenarios prompt
|
|
86
|
+
scenarios_prompt = make_all_scenarios_prompt(
|
|
87
|
+
behavior_name=behavior_name,
|
|
88
|
+
behavior_description=behavior_description,
|
|
89
|
+
total_scenarios=self.num_base_scenarios,
|
|
90
|
+
examples=examples,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Use LLM client to generate scenarios
|
|
94
|
+
try:
|
|
95
|
+
# Convert to litellm format for Bloom compatibility
|
|
96
|
+
response = self._call_llm_with_bloom_format(
|
|
97
|
+
system_prompt=system_prompt, user_prompt=scenarios_prompt
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Parse scenarios from response
|
|
101
|
+
scenarios = self._parse_scenarios_from_response(response, behavior_name)
|
|
102
|
+
return scenarios[: self.num_base_scenarios]
|
|
103
|
+
except Exception:
|
|
104
|
+
# Fallback to simple scenarios on error
|
|
105
|
+
return self._fallback_scenarios(behavior_name, behavior_description)
|
|
106
|
+
|
|
107
|
+
def _generate_variations(
|
|
108
|
+
self, base_scenario: dict[str, Any], behavior_name: str, behavior_description: str
|
|
109
|
+
) -> list[dict[str, Any]]:
|
|
110
|
+
"""Generate variations of a base scenario using Bloom variation prompts."""
|
|
111
|
+
variations = []
|
|
112
|
+
|
|
113
|
+
# Create variation system prompt
|
|
114
|
+
variation_system_prompt = make_variation_system_prompt(
|
|
115
|
+
behavior_name=behavior_name, behavior_description=behavior_description
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Create variation prompt
|
|
119
|
+
variation_prompt = make_variation_prompt(
|
|
120
|
+
base_scenario_description=base_scenario.get("prompt", ""),
|
|
121
|
+
num_perturbations=self.num_variations + 1, # +1 because Bloom counts differently
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
response = self._call_llm_with_bloom_format(
|
|
126
|
+
system_prompt=variation_system_prompt, user_prompt=variation_prompt
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Parse variations from response
|
|
130
|
+
parsed_variations = self._parse_variations_from_response(
|
|
131
|
+
response, base_scenario, behavior_name
|
|
132
|
+
)
|
|
133
|
+
variations.extend(parsed_variations[: self.num_variations])
|
|
134
|
+
except Exception:
|
|
135
|
+
# If variation generation fails, just return the base scenario
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
return variations
|
|
139
|
+
|
|
140
|
+
def _call_llm_with_bloom_format(self, system_prompt: str, user_prompt: str) -> str:
|
|
141
|
+
"""
|
|
142
|
+
Call LLM using Bloom's expected format (litellm-compatible).
|
|
143
|
+
|
|
144
|
+
This bridges CodeOptix's LLM client with Bloom's expected interface.
|
|
145
|
+
"""
|
|
146
|
+
# Use CodeOptix's LLM client, but format for Bloom compatibility
|
|
147
|
+
messages = [
|
|
148
|
+
{"role": "system", "content": system_prompt},
|
|
149
|
+
{"role": "user", "content": user_prompt},
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
return self.llm_client.chat_completion(
|
|
153
|
+
messages=messages,
|
|
154
|
+
model=self.model,
|
|
155
|
+
temperature=0.8, # Higher temperature for creative generation
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def _parse_scenarios_from_response(
|
|
159
|
+
self, response: str, behavior_name: str
|
|
160
|
+
) -> list[dict[str, Any]]:
|
|
161
|
+
"""Parse scenarios from LLM response."""
|
|
162
|
+
scenarios = []
|
|
163
|
+
|
|
164
|
+
# Try to extract JSON from response
|
|
165
|
+
try:
|
|
166
|
+
# Look for JSON array
|
|
167
|
+
import re
|
|
168
|
+
|
|
169
|
+
json_match = re.search(r"\[.*\]", response, re.DOTALL)
|
|
170
|
+
if json_match:
|
|
171
|
+
scenarios = json.loads(json_match.group(0))
|
|
172
|
+
return [self._normalize_scenario(s, behavior_name) for s in scenarios]
|
|
173
|
+
except json.JSONDecodeError:
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
# Fallback: try to parse structured text
|
|
177
|
+
# Look for scenario markers
|
|
178
|
+
scenario_pattern = r"<scenario>|Scenario \d+:|## Scenario"
|
|
179
|
+
if re.search(scenario_pattern, response, re.IGNORECASE):
|
|
180
|
+
# Parse structured scenarios
|
|
181
|
+
parts = re.split(scenario_pattern, response, flags=re.IGNORECASE)
|
|
182
|
+
for part in parts[1:]: # Skip first part (before first scenario)
|
|
183
|
+
scenario = self._extract_scenario_from_text(part, behavior_name)
|
|
184
|
+
if scenario:
|
|
185
|
+
scenarios.append(scenario)
|
|
186
|
+
|
|
187
|
+
return scenarios if scenarios else self._fallback_scenarios(behavior_name, "")
|
|
188
|
+
|
|
189
|
+
def _parse_variations_from_response(
|
|
190
|
+
self, response: str, base_scenario: dict[str, Any], behavior_name: str
|
|
191
|
+
) -> list[dict[str, Any]]:
|
|
192
|
+
"""Parse variations from LLM response."""
|
|
193
|
+
variations = []
|
|
194
|
+
|
|
195
|
+
# Look for variation markers
|
|
196
|
+
import re
|
|
197
|
+
|
|
198
|
+
variation_pattern = r"<variation>|Variation \d+:|## Variation"
|
|
199
|
+
if re.search(variation_pattern, response, re.IGNORECASE):
|
|
200
|
+
parts = re.split(variation_pattern, response, flags=re.IGNORECASE)
|
|
201
|
+
for part in parts[1:]: # Skip first part
|
|
202
|
+
variation = base_scenario.copy()
|
|
203
|
+
variation["prompt"] = part.strip()[:500] # Limit length
|
|
204
|
+
variation["task"] = f"Variation: {base_scenario.get('task', '')}"
|
|
205
|
+
variations.append(variation)
|
|
206
|
+
|
|
207
|
+
return variations
|
|
208
|
+
|
|
209
|
+
def _normalize_scenario(self, scenario: Any, behavior_name: str) -> dict[str, Any]:
|
|
210
|
+
"""Normalize scenario to standard format."""
|
|
211
|
+
if isinstance(scenario, dict):
|
|
212
|
+
return {
|
|
213
|
+
"task": scenario.get("task", scenario.get("description", "")),
|
|
214
|
+
"prompt": scenario.get("prompt", scenario.get("instruction", "")),
|
|
215
|
+
"expected_issues": scenario.get("expected_issues", []),
|
|
216
|
+
"behavior": behavior_name,
|
|
217
|
+
}
|
|
218
|
+
if isinstance(scenario, str):
|
|
219
|
+
return {
|
|
220
|
+
"task": f"Test {behavior_name}",
|
|
221
|
+
"prompt": scenario,
|
|
222
|
+
"expected_issues": [],
|
|
223
|
+
"behavior": behavior_name,
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
"task": f"Test {behavior_name}",
|
|
227
|
+
"prompt": str(scenario),
|
|
228
|
+
"expected_issues": [],
|
|
229
|
+
"behavior": behavior_name,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
def _extract_scenario_from_text(self, text: str, behavior_name: str) -> dict[str, Any] | None:
|
|
233
|
+
"""Extract scenario information from text."""
|
|
234
|
+
lines = [l.strip() for l in text.split("\n") if l.strip()]
|
|
235
|
+
if not lines:
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
# Try to extract task and prompt
|
|
239
|
+
task = ""
|
|
240
|
+
prompt = ""
|
|
241
|
+
|
|
242
|
+
for i, line in enumerate(lines):
|
|
243
|
+
if "task:" in line.lower() or "description:" in line.lower():
|
|
244
|
+
task = line.split(":", 1)[1].strip() if ":" in line else line
|
|
245
|
+
elif "prompt:" in line.lower() or "instruction:" in line.lower():
|
|
246
|
+
prompt = line.split(":", 1)[1].strip() if ":" in line else line
|
|
247
|
+
elif i == 0 and not task:
|
|
248
|
+
task = line
|
|
249
|
+
|
|
250
|
+
if not prompt:
|
|
251
|
+
prompt = "\n".join(lines)
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
"task": task or f"Test {behavior_name}",
|
|
255
|
+
"prompt": prompt[:500], # Limit length
|
|
256
|
+
"expected_issues": [],
|
|
257
|
+
"behavior": behavior_name,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
def _fallback_scenarios(
|
|
261
|
+
self, behavior_name: str, behavior_description: str
|
|
262
|
+
) -> list[dict[str, Any]]:
|
|
263
|
+
"""Generate fallback scenarios if Bloom generation fails."""
|
|
264
|
+
return [
|
|
265
|
+
{
|
|
266
|
+
"task": f"Test {behavior_name}",
|
|
267
|
+
"prompt": f"Write code that should be evaluated for: {behavior_description}",
|
|
268
|
+
"expected_issues": [f"Behavior: {behavior_name}"],
|
|
269
|
+
"behavior": behavior_name,
|
|
270
|
+
}
|
|
271
|
+
]
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""Evaluation engine for CodeOptix."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from codeoptix.adapters.base import AgentAdapter, AgentOutput
|
|
6
|
+
from codeoptix.behaviors import BehaviorSpec, create_behavior
|
|
7
|
+
from codeoptix.evaluation.evaluators import (
|
|
8
|
+
ArtifactComparator,
|
|
9
|
+
LLMEvaluator,
|
|
10
|
+
StaticAnalyzer,
|
|
11
|
+
TestRunner,
|
|
12
|
+
)
|
|
13
|
+
from codeoptix.evaluation.scenario_generator import BloomScenarioGenerator, ScenarioGenerator
|
|
14
|
+
from codeoptix.utils.llm import LLMClient
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EvaluationEngine:
|
|
18
|
+
"""
|
|
19
|
+
Main evaluation engine that orchestrates behavior evaluation.
|
|
20
|
+
|
|
21
|
+
Supports:
|
|
22
|
+
- Bloom-style scenario generation
|
|
23
|
+
- Multi-modal evaluation (static analysis, LLM, tests)
|
|
24
|
+
- Custom scenario generators
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self, adapter: AgentAdapter, llm_client: LLMClient, config: dict[str, Any] | None = None
|
|
29
|
+
):
|
|
30
|
+
"""
|
|
31
|
+
Initialize evaluation engine.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
adapter: Agent adapter for executing tasks
|
|
35
|
+
llm_client: LLM client for evaluation and scenario generation
|
|
36
|
+
config: Configuration dictionary
|
|
37
|
+
"""
|
|
38
|
+
self.adapter = adapter
|
|
39
|
+
self.llm_client = llm_client
|
|
40
|
+
self.config = config or {}
|
|
41
|
+
|
|
42
|
+
# Initialize evaluators
|
|
43
|
+
self.static_analyzer = StaticAnalyzer(self.config.get("static_analysis", {}))
|
|
44
|
+
self.test_runner = TestRunner(self.config.get("test_runner", {}))
|
|
45
|
+
self.llm_evaluator = LLMEvaluator(llm_client, self.config.get("llm_evaluator", {}))
|
|
46
|
+
self.artifact_comparator = ArtifactComparator(self.config.get("artifact_comparator", {}))
|
|
47
|
+
|
|
48
|
+
# Initialize scenario generator
|
|
49
|
+
scenario_config = self.config.get("scenario_generator", {})
|
|
50
|
+
use_bloom = scenario_config.get("use_bloom", True)
|
|
51
|
+
|
|
52
|
+
if use_bloom:
|
|
53
|
+
self.scenario_generator = BloomScenarioGenerator(llm_client, scenario_config)
|
|
54
|
+
else:
|
|
55
|
+
self.scenario_generator = ScenarioGenerator(llm_client, scenario_config)
|
|
56
|
+
|
|
57
|
+
def evaluate_behaviors(
|
|
58
|
+
self,
|
|
59
|
+
behavior_names: list[str],
|
|
60
|
+
scenarios: list[dict[str, Any]] | None = None,
|
|
61
|
+
context: dict[str, Any] | None = None,
|
|
62
|
+
) -> dict[str, Any]:
|
|
63
|
+
"""
|
|
64
|
+
Evaluate agent against multiple behaviors.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
behavior_names: List of behavior names to evaluate
|
|
68
|
+
scenarios: Optional pre-generated scenarios
|
|
69
|
+
context: Optional context (planning artifacts, etc.)
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Dictionary with evaluation results
|
|
73
|
+
"""
|
|
74
|
+
context = context or {}
|
|
75
|
+
results = {
|
|
76
|
+
"behaviors": {},
|
|
77
|
+
"scenarios": scenarios or [],
|
|
78
|
+
"overall_score": 0.0,
|
|
79
|
+
"metadata": {},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# Generate scenarios if not provided
|
|
83
|
+
if not scenarios:
|
|
84
|
+
scenarios = self._generate_scenarios_for_behaviors(behavior_names)
|
|
85
|
+
results["scenarios"] = scenarios
|
|
86
|
+
|
|
87
|
+
# Validate behavior names (keep list in sync with BEHAVIOR_REGISTRY)
|
|
88
|
+
valid_behaviors = [
|
|
89
|
+
"insecure-code",
|
|
90
|
+
"vacuous-tests",
|
|
91
|
+
"plan-drift",
|
|
92
|
+
]
|
|
93
|
+
invalid_behaviors = [b for b in behavior_names if b not in valid_behaviors]
|
|
94
|
+
if invalid_behaviors:
|
|
95
|
+
raise ValueError(
|
|
96
|
+
f"Invalid behavior name(s): {', '.join(invalid_behaviors)}. "
|
|
97
|
+
f"Available behaviors: {', '.join(valid_behaviors)}"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if not behavior_names:
|
|
101
|
+
raise ValueError("At least one behavior must be specified")
|
|
102
|
+
|
|
103
|
+
# Evaluate each behavior
|
|
104
|
+
behavior_results = {}
|
|
105
|
+
enabled_behaviors = []
|
|
106
|
+
for behavior_name in behavior_names:
|
|
107
|
+
try:
|
|
108
|
+
behavior = create_behavior(
|
|
109
|
+
behavior_name, self.config.get("behaviors", {}).get(behavior_name, {})
|
|
110
|
+
)
|
|
111
|
+
except Exception as e:
|
|
112
|
+
raise ValueError(
|
|
113
|
+
f"Failed to create behavior '{behavior_name}': {e}. "
|
|
114
|
+
f"Please check the behavior name and configuration."
|
|
115
|
+
) from e
|
|
116
|
+
|
|
117
|
+
if not behavior.is_enabled():
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
enabled_behaviors.append(behavior_name)
|
|
121
|
+
|
|
122
|
+
# Get scenarios for this behavior
|
|
123
|
+
behavior_scenarios = [s for s in scenarios if s.get("behavior") == behavior_name]
|
|
124
|
+
|
|
125
|
+
# Run evaluation
|
|
126
|
+
behavior_result = self._evaluate_behavior(
|
|
127
|
+
behavior=behavior, scenarios=behavior_scenarios, context=context
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
behavior_results[behavior_name] = behavior_result
|
|
131
|
+
|
|
132
|
+
if not enabled_behaviors:
|
|
133
|
+
raise ValueError(
|
|
134
|
+
"No enabled behaviors found. "
|
|
135
|
+
"Please ensure at least one behavior is enabled in the configuration."
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
results["behaviors"] = behavior_results
|
|
139
|
+
|
|
140
|
+
# Calculate overall score
|
|
141
|
+
if behavior_results:
|
|
142
|
+
scores = [r["score"] for r in behavior_results.values()]
|
|
143
|
+
results["overall_score"] = sum(scores) / len(scores)
|
|
144
|
+
|
|
145
|
+
return results
|
|
146
|
+
|
|
147
|
+
def _generate_scenarios_for_behaviors(self, behavior_names: list[str]) -> list[dict[str, Any]]:
|
|
148
|
+
"""Generate scenarios for all behaviors."""
|
|
149
|
+
all_scenarios = []
|
|
150
|
+
|
|
151
|
+
for behavior_name in behavior_names:
|
|
152
|
+
behavior = create_behavior(behavior_name)
|
|
153
|
+
behavior_description = behavior.get_description()
|
|
154
|
+
|
|
155
|
+
# Generate scenarios for this behavior
|
|
156
|
+
scenarios = self.scenario_generator.generate_scenarios(
|
|
157
|
+
behavior_name=behavior_name, behavior_description=behavior_description
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Tag scenarios with behavior name
|
|
161
|
+
for scenario in scenarios:
|
|
162
|
+
scenario["behavior"] = behavior_name
|
|
163
|
+
|
|
164
|
+
all_scenarios.extend(scenarios)
|
|
165
|
+
|
|
166
|
+
return all_scenarios
|
|
167
|
+
|
|
168
|
+
def _evaluate_behavior(
|
|
169
|
+
self, behavior: BehaviorSpec, scenarios: list[dict[str, Any]], context: dict[str, Any]
|
|
170
|
+
) -> dict[str, Any]:
|
|
171
|
+
"""
|
|
172
|
+
Evaluate a single behavior across scenarios.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
behavior: Behavior spec to evaluate
|
|
176
|
+
scenarios: Scenarios to test
|
|
177
|
+
context: Evaluation context
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Dictionary with behavior evaluation results
|
|
181
|
+
"""
|
|
182
|
+
if not scenarios:
|
|
183
|
+
# Generate default scenario
|
|
184
|
+
scenarios = [
|
|
185
|
+
{
|
|
186
|
+
"task": f"Test {behavior.get_name()}",
|
|
187
|
+
"prompt": f"Write code that should be evaluated for: {behavior.get_description()}",
|
|
188
|
+
"behavior": behavior.get_name(),
|
|
189
|
+
}
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
scenario_results = []
|
|
193
|
+
all_evidence = []
|
|
194
|
+
scores = []
|
|
195
|
+
|
|
196
|
+
for scenario in scenarios:
|
|
197
|
+
# Execute agent with scenario prompt
|
|
198
|
+
prompt = scenario.get("prompt", "")
|
|
199
|
+
agent_output = self.adapter.execute(prompt, context=context)
|
|
200
|
+
|
|
201
|
+
# Run behavior evaluation
|
|
202
|
+
behavior_result = behavior.evaluate(agent_output, context=context)
|
|
203
|
+
|
|
204
|
+
# Run additional evaluators
|
|
205
|
+
evaluator_results = self._run_evaluators(agent_output, behavior, context)
|
|
206
|
+
|
|
207
|
+
# Combine results
|
|
208
|
+
scenario_result = {
|
|
209
|
+
"scenario": scenario,
|
|
210
|
+
"behavior_result": {
|
|
211
|
+
"passed": behavior_result.passed,
|
|
212
|
+
"score": behavior_result.score,
|
|
213
|
+
"severity": behavior_result.severity.value,
|
|
214
|
+
"evidence": behavior_result.evidence,
|
|
215
|
+
},
|
|
216
|
+
"evaluator_results": evaluator_results,
|
|
217
|
+
"agent_output": {
|
|
218
|
+
"code_length": len(agent_output.code or ""),
|
|
219
|
+
"has_tests": bool(agent_output.tests),
|
|
220
|
+
},
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
scenario_results.append(scenario_result)
|
|
224
|
+
all_evidence.extend(behavior_result.evidence)
|
|
225
|
+
scores.append(behavior_result.score)
|
|
226
|
+
|
|
227
|
+
# Aggregate results
|
|
228
|
+
avg_score = sum(scores) / len(scores) if scores else 0.0
|
|
229
|
+
passed_count = sum(1 for r in scenario_results if r["behavior_result"]["passed"])
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
"behavior_name": behavior.get_name(),
|
|
233
|
+
"scenarios_tested": len(scenarios),
|
|
234
|
+
"scenarios_passed": passed_count,
|
|
235
|
+
"score": avg_score,
|
|
236
|
+
"evidence": all_evidence,
|
|
237
|
+
"scenario_results": scenario_results,
|
|
238
|
+
"metadata": {
|
|
239
|
+
"total_evidence_items": len(all_evidence),
|
|
240
|
+
"min_score": min(scores) if scores else 0.0,
|
|
241
|
+
"max_score": max(scores) if scores else 0.0,
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
def _run_evaluators(
|
|
246
|
+
self, agent_output: AgentOutput, behavior: BehaviorSpec, context: dict[str, Any]
|
|
247
|
+
) -> dict[str, Any]:
|
|
248
|
+
"""Run additional evaluators (static analysis, tests, LLM)."""
|
|
249
|
+
results = {}
|
|
250
|
+
|
|
251
|
+
code = agent_output.code or ""
|
|
252
|
+
tests = agent_output.tests or ""
|
|
253
|
+
|
|
254
|
+
# Static analysis
|
|
255
|
+
if code:
|
|
256
|
+
results["static_analysis"] = self.static_analyzer.analyze(code)
|
|
257
|
+
|
|
258
|
+
# Test execution
|
|
259
|
+
if code and tests:
|
|
260
|
+
results["test_execution"] = self.test_runner.run_tests(code, tests)
|
|
261
|
+
|
|
262
|
+
# LLM evaluation
|
|
263
|
+
if code:
|
|
264
|
+
results["llm_evaluation"] = self.llm_evaluator.evaluate(
|
|
265
|
+
code=code, behavior_description=behavior.get_description(), context=context
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Artifact comparison
|
|
269
|
+
if context.get("plan") or context.get("requirements"):
|
|
270
|
+
results["artifact_comparison"] = self.artifact_comparator.compare(
|
|
271
|
+
code=code, artifacts=context
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
return results
|