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,667 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
# CodeOptiX: Fixed imports for vendored Bloom
|
|
10
|
+
from codeoptix.vendor.bloom.globals import models
|
|
11
|
+
from codeoptix.vendor.bloom.utils import (get_reasoning_effort,
|
|
12
|
+
parse_message, get_results_dir,
|
|
13
|
+
litellm_chat, load_config, load_understanding_results,
|
|
14
|
+
save_results_locally, setup_project_path)
|
|
15
|
+
from codeoptix.vendor.bloom.prompts.step2_ideation import (make_system_prompt,
|
|
16
|
+
make_all_scenarios_prompt,
|
|
17
|
+
make_variation_system_prompt, make_variation_prompt)
|
|
18
|
+
|
|
19
|
+
# Simple debug print function (replaces bloom.debug_print)
|
|
20
|
+
def debug_print(*args, **kwargs):
|
|
21
|
+
"""Debug print function for Bloom scripts."""
|
|
22
|
+
import logging
|
|
23
|
+
logging.debug(*args, **kwargs)
|
|
24
|
+
|
|
25
|
+
def get_model_max_output_tokens(model_id):
|
|
26
|
+
"""
|
|
27
|
+
Get the maximum output tokens for a model using litellm.
|
|
28
|
+
|
|
29
|
+
Note: Some older models (Claude 3.7) report inflated limits. We cap those based on observed behavior.
|
|
30
|
+
For newer models, we trust litellm's reported limits.
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
import litellm
|
|
34
|
+
model_info = litellm.get_model_info(model_id)
|
|
35
|
+
max_output = model_info.get("max_output_tokens")
|
|
36
|
+
|
|
37
|
+
if max_output and isinstance(max_output, int) and max_output > 0:
|
|
38
|
+
debug_print(f" litellm reports max_output_tokens: {max_output}")
|
|
39
|
+
|
|
40
|
+
# Only apply caps for models with known discrepancies
|
|
41
|
+
model_lower = model_id.lower()
|
|
42
|
+
|
|
43
|
+
# Claude 3.7 and earlier 3.x models report inflated limits - cap at 8K (observed limit ~6-8K)
|
|
44
|
+
if ("claude-3-7" in model_lower or "claude-3-5" in model_lower or
|
|
45
|
+
"claude-3.7" in model_lower or "claude-3.5" in model_lower or
|
|
46
|
+
"claude-3-opus" in model_lower or "claude-3-sonnet" in model_lower):
|
|
47
|
+
practical_max = min(max_output, 8192)
|
|
48
|
+
if practical_max < max_output:
|
|
49
|
+
debug_print(f" Applied cap for Claude 3.x: {practical_max} (observed limit)")
|
|
50
|
+
else:
|
|
51
|
+
# For all other models (Claude 4+, GPT, Gemini, etc.), trust litellm's reported limit
|
|
52
|
+
practical_max = max_output
|
|
53
|
+
|
|
54
|
+
return practical_max
|
|
55
|
+
|
|
56
|
+
except Exception as e:
|
|
57
|
+
debug_print(f"⚠️ Could not get max output tokens from litellm for {model_id}: {e}")
|
|
58
|
+
|
|
59
|
+
# Fallback to conservative defaults if litellm lookup fails
|
|
60
|
+
debug_print(f"⚠️ Using fallback max output tokens for {model_id}")
|
|
61
|
+
return 8192 # Conservative default
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def calculate_batch_size(total_scenarios, model_id, modality="conversation", reasoning_effort="none", safety_margin=0.8):
|
|
65
|
+
"""
|
|
66
|
+
Calculate optimal batch size for scenario generation.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
total_scenarios: Total number of scenarios to generate
|
|
70
|
+
model_id: Model identifier
|
|
71
|
+
modality: "conversation" or "simenv"
|
|
72
|
+
reasoning_effort: Extended thinking level ("none", "low", "medium", "high")
|
|
73
|
+
safety_margin: Safety factor to avoid hitting limits (0.8 = use 80% of max)
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
batch_size: Number of scenarios per batch
|
|
77
|
+
num_batches: Number of batches needed
|
|
78
|
+
"""
|
|
79
|
+
# Get model's max output tokens
|
|
80
|
+
max_output_tokens = get_model_max_output_tokens(model_id)
|
|
81
|
+
|
|
82
|
+
# Subtract thinking tokens based on reasoning effort
|
|
83
|
+
# Extended thinking uses FIXED token budgets (not proportional):
|
|
84
|
+
# Source: https://docs.litellm.ai/docs/providers/anthropic
|
|
85
|
+
thinking_tokens_fixed = {
|
|
86
|
+
"none": 0,
|
|
87
|
+
"low": 1024, # Fixed 1,024 token budget
|
|
88
|
+
"medium": 2048, # Fixed 2,048 token budget (updated from docs)
|
|
89
|
+
"high": 4096 # Fixed 4,096 token budget (updated from docs)
|
|
90
|
+
}.get(reasoning_effort, 0)
|
|
91
|
+
|
|
92
|
+
# Content tokens = total - fixed thinking budget
|
|
93
|
+
content_tokens = max_output_tokens - thinking_tokens_fixed
|
|
94
|
+
|
|
95
|
+
debug_print(f" Thinking allocation ({reasoning_effort}): {thinking_tokens_fixed} tokens (fixed)")
|
|
96
|
+
debug_print(f" Content tokens available: {content_tokens}")
|
|
97
|
+
|
|
98
|
+
# Estimate tokens per scenario (based on observed actual usage)
|
|
99
|
+
# SimEnv scenarios: ~850 tokens on average (observed from actual runs)
|
|
100
|
+
# Conversation scenarios: ~500 tokens on average estimate
|
|
101
|
+
# Adding 20% buffer for longer scenarios
|
|
102
|
+
tokens_per_scenario = 1000 if modality == "simenv" else 600
|
|
103
|
+
|
|
104
|
+
# Reserve buffer for formatting overhead and longer-than-expected scenarios
|
|
105
|
+
overhead_tokens = 2000
|
|
106
|
+
|
|
107
|
+
# Calculate how many scenarios fit in one batch
|
|
108
|
+
usable_tokens = int((content_tokens - overhead_tokens) * safety_margin)
|
|
109
|
+
batch_size = max(1, usable_tokens // tokens_per_scenario)
|
|
110
|
+
|
|
111
|
+
# Don't make batches larger than total scenarios
|
|
112
|
+
batch_size = min(batch_size, total_scenarios)
|
|
113
|
+
|
|
114
|
+
# Calculate number of batches
|
|
115
|
+
num_batches = (total_scenarios + batch_size - 1) // batch_size # Ceiling division
|
|
116
|
+
|
|
117
|
+
debug_print(f"📊 Batch calculation:")
|
|
118
|
+
debug_print(f" Model max output tokens: {max_output_tokens}")
|
|
119
|
+
debug_print(f" Tokens per scenario ({modality}): {tokens_per_scenario}")
|
|
120
|
+
debug_print(f" Usable tokens (with {int(safety_margin*100)}% safety margin): {usable_tokens}")
|
|
121
|
+
debug_print(f" Calculated batch size: {batch_size} scenarios")
|
|
122
|
+
debug_print(f" Number of batches: {num_batches}")
|
|
123
|
+
|
|
124
|
+
return batch_size, num_batches
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def parse_scenarios_response(response_text):
|
|
128
|
+
"""Parse the scenarios response to extract individual scenarios with tools and thinking."""
|
|
129
|
+
scenarios = []
|
|
130
|
+
overall_thinking = ""
|
|
131
|
+
|
|
132
|
+
# Extract overall thinking from <thinking> tags
|
|
133
|
+
thinking_match = re.search(r'<thinking>(.*?)</thinking>', response_text, re.DOTALL)
|
|
134
|
+
if thinking_match:
|
|
135
|
+
overall_thinking = thinking_match.group(1).strip()
|
|
136
|
+
|
|
137
|
+
# First try to extract scenarios from <scenario> tags
|
|
138
|
+
scenario_matches = re.findall(r'<scenario>(.*?)</scenario>', response_text, re.DOTALL)
|
|
139
|
+
|
|
140
|
+
if scenario_matches:
|
|
141
|
+
# If we found scenario tags, use those
|
|
142
|
+
for match in scenario_matches:
|
|
143
|
+
scenario_text = match.strip()
|
|
144
|
+
|
|
145
|
+
# Extract tool signatures from this scenario
|
|
146
|
+
tool_signatures = re.findall(r'<tool_signature>(.*?)</tool_signature>', scenario_text, re.DOTALL)
|
|
147
|
+
|
|
148
|
+
# Remove tool signatures from scenario description to get clean text
|
|
149
|
+
clean_scenario = re.sub(r'<tool_signature>.*?</tool_signature>', '', scenario_text, flags=re.DOTALL)
|
|
150
|
+
clean_scenario = re.sub(r'\s+', ' ', clean_scenario.strip())
|
|
151
|
+
|
|
152
|
+
if clean_scenario:
|
|
153
|
+
scenario_data = {
|
|
154
|
+
"description": clean_scenario,
|
|
155
|
+
"tools": tool_signatures,
|
|
156
|
+
"thinking": overall_thinking
|
|
157
|
+
}
|
|
158
|
+
scenarios.append(scenario_data)
|
|
159
|
+
else:
|
|
160
|
+
# If no scenario tags, try to split by "Scenario X:" patterns
|
|
161
|
+
scenario_splits = re.split(r'\*\*Scenario \d+:', response_text)
|
|
162
|
+
if len(scenario_splits) > 1:
|
|
163
|
+
# Skip the first split (it's before the first scenario)
|
|
164
|
+
for split in scenario_splits[1:]:
|
|
165
|
+
scenario_text = split.strip()
|
|
166
|
+
|
|
167
|
+
# Extract tool signatures from this scenario
|
|
168
|
+
tool_signatures = re.findall(r'<tool_signature>.*?</tool_signature>', scenario_text, re.DOTALL)
|
|
169
|
+
|
|
170
|
+
# Remove tool signatures from scenario description
|
|
171
|
+
clean_scenario = re.sub(r'<tool_signature>.*?</tool_signature>', '', scenario_text, flags=re.DOTALL)
|
|
172
|
+
clean_scenario = re.sub(r'\s+', ' ', clean_scenario.strip())
|
|
173
|
+
|
|
174
|
+
if clean_scenario:
|
|
175
|
+
scenario_data = {
|
|
176
|
+
"description": clean_scenario,
|
|
177
|
+
"tools": tool_signatures,
|
|
178
|
+
"thinking": overall_thinking
|
|
179
|
+
}
|
|
180
|
+
scenarios.append(scenario_data)
|
|
181
|
+
else:
|
|
182
|
+
# Fallback: try to split by numbered patterns
|
|
183
|
+
scenario_splits = re.split(r'\d+\.\s*', response_text)
|
|
184
|
+
if len(scenario_splits) > 1:
|
|
185
|
+
for split in scenario_splits[1:]:
|
|
186
|
+
scenario_text = split.strip()
|
|
187
|
+
|
|
188
|
+
# Extract tool signatures from this scenario
|
|
189
|
+
tool_signatures = re.findall(r'<tool_signature>.*?</tool_signature>', scenario_text, re.DOTALL)
|
|
190
|
+
|
|
191
|
+
# Remove tool signatures from scenario description
|
|
192
|
+
clean_scenario = re.sub(r'<tool_signature>.*?</tool_signature>', '', scenario_text, flags=re.DOTALL)
|
|
193
|
+
clean_scenario = re.sub(r'\s+', ' ', clean_scenario.strip())
|
|
194
|
+
|
|
195
|
+
if clean_scenario:
|
|
196
|
+
scenario_data = {
|
|
197
|
+
"description": clean_scenario,
|
|
198
|
+
"tools": tool_signatures,
|
|
199
|
+
"thinking": overall_thinking
|
|
200
|
+
}
|
|
201
|
+
scenarios.append(scenario_data)
|
|
202
|
+
|
|
203
|
+
return scenarios, overall_thinking
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def parse_variations_response(response_text, original_tools=None):
|
|
207
|
+
"""Parse the variations response to extract individual variations with tools and thinking."""
|
|
208
|
+
variations = []
|
|
209
|
+
thinking = ""
|
|
210
|
+
|
|
211
|
+
# Extract thinking from <thinking> tags
|
|
212
|
+
thinking_match = re.search(r'<thinking>(.*?)</thinking>', response_text, re.DOTALL)
|
|
213
|
+
if thinking_match:
|
|
214
|
+
thinking = thinking_match.group(1).strip()
|
|
215
|
+
|
|
216
|
+
# Clean up the response text by removing thinking content
|
|
217
|
+
clean_response = re.sub(r'<thinking>.*?</thinking>', '', response_text, flags=re.DOTALL)
|
|
218
|
+
|
|
219
|
+
# Extract variations from <variation> tags
|
|
220
|
+
variation_matches = re.findall(r'<variation>(.*?)</variation>', clean_response, re.DOTALL)
|
|
221
|
+
|
|
222
|
+
if variation_matches:
|
|
223
|
+
# If we found variation tags, use those
|
|
224
|
+
for match in variation_matches:
|
|
225
|
+
variation_text = match.strip()
|
|
226
|
+
|
|
227
|
+
# Extract tool signatures from this variation (if any)
|
|
228
|
+
tool_signatures = re.findall(r'<tool_signature>(.*?)</tool_signature>', variation_text, re.DOTALL)
|
|
229
|
+
|
|
230
|
+
# Remove tool signatures from variation description to get clean text
|
|
231
|
+
clean_variation = re.sub(r'<tool_signature>.*?</tool_signature>', '', variation_text, flags=re.DOTALL)
|
|
232
|
+
clean_variation = re.sub(r'\s+', ' ', clean_variation.strip())
|
|
233
|
+
|
|
234
|
+
if clean_variation and len(clean_variation) > 10: # Filter out very short matches
|
|
235
|
+
# Use tools from variation if present, otherwise use original tools
|
|
236
|
+
variation_tools = tool_signatures if tool_signatures else (original_tools or [])
|
|
237
|
+
variation_data = {
|
|
238
|
+
"description": clean_variation,
|
|
239
|
+
"tools": variation_tools
|
|
240
|
+
}
|
|
241
|
+
variations.append(variation_data)
|
|
242
|
+
else:
|
|
243
|
+
# If no variation tags, try to split by numbered patterns
|
|
244
|
+
variation_splits = re.split(r'(?:\d+\.|Variant\s+\d+:?|Perturbed\s+Variant:?)\s*', clean_response)
|
|
245
|
+
if len(variation_splits) > 1:
|
|
246
|
+
for split in variation_splits[1:]: # Skip the first split (before any number)
|
|
247
|
+
variation_text = split.strip()
|
|
248
|
+
|
|
249
|
+
# Extract tool signatures from this variation (if any)
|
|
250
|
+
tool_signatures = re.findall(r'<tool_signature>(.*?)</tool_signature>', variation_text, re.DOTALL)
|
|
251
|
+
|
|
252
|
+
# Remove tool signatures from variation description
|
|
253
|
+
clean_variation = re.sub(r'<tool_signature>.*?</tool_signature>', '', variation_text, flags=re.DOTALL)
|
|
254
|
+
clean_variation = re.sub(r'\s+', ' ', clean_variation.strip())
|
|
255
|
+
|
|
256
|
+
if clean_variation and len(clean_variation) > 20: # Filter out very short matches
|
|
257
|
+
# Use tools from variation if present, otherwise use original tools
|
|
258
|
+
variation_tools = tool_signatures if tool_signatures else (original_tools or [])
|
|
259
|
+
variation_data = {
|
|
260
|
+
"description": clean_variation,
|
|
261
|
+
"tools": variation_tools
|
|
262
|
+
}
|
|
263
|
+
variations.append(variation_data)
|
|
264
|
+
|
|
265
|
+
return variations, thinking
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def run_ideation(config_path="seed.yaml", config=None):
|
|
269
|
+
"""Run the ideation process using understanding results."""
|
|
270
|
+
|
|
271
|
+
debug_print("💡 Starting ideation process...")
|
|
272
|
+
|
|
273
|
+
# Load configuration
|
|
274
|
+
if config is None:
|
|
275
|
+
config = load_config(config_path)
|
|
276
|
+
debug_print(f"📋 Loaded config from: {config_path}")
|
|
277
|
+
|
|
278
|
+
# Load configurable prompts once
|
|
279
|
+
from utils import load_configurable_prompts
|
|
280
|
+
prompts = load_configurable_prompts(config)
|
|
281
|
+
|
|
282
|
+
# Print all configuration parameters when in debug mode
|
|
283
|
+
debug_print("=" * 60)
|
|
284
|
+
debug_print("📊 IDEATION STAGE CONFIGURATION")
|
|
285
|
+
debug_print("=" * 60)
|
|
286
|
+
debug_print("📁 Behavior Parameters:")
|
|
287
|
+
debug_print(f" - Name: {config['behavior']['name']}")
|
|
288
|
+
debug_print(f" - Examples: {config['behavior']['examples']}")
|
|
289
|
+
debug_print("📁 Global Parameters:")
|
|
290
|
+
debug_print(f" - Temperature: {config.get('temperature', 1.0)}")
|
|
291
|
+
debug_print(f" - Max Concurrent: {config.get('max_concurrent', 15)}")
|
|
292
|
+
debug_print(f" - Debug: {config.get('debug', False)}")
|
|
293
|
+
debug_print(f" - Evaluator Reasoning Effort: {config.get('evaluator_reasoning_effort', 'high')}")
|
|
294
|
+
debug_print(f" - Target Reasoning Effort: {config.get('target_reasoning_effort', 'medium')}")
|
|
295
|
+
debug_print("📁 Ideation Parameters:")
|
|
296
|
+
debug_print(f" - Model: {config['ideation']['model']}")
|
|
297
|
+
debug_print(f" - Total Evals: {config['ideation']['total_evals']}")
|
|
298
|
+
debug_print(f" - Diversity: {config['ideation']['diversity']}")
|
|
299
|
+
debug_print(f" - Max Tokens: {config['ideation'].get('max_tokens', 4000)}")
|
|
300
|
+
debug_print(f" - Web Search: {config['ideation'].get('web_search', False)}")
|
|
301
|
+
from utils import get_reasoning_effort
|
|
302
|
+
debug_print(f" - Reasoning Effort (computed): {get_reasoning_effort(config, 'ideation')}")
|
|
303
|
+
debug_print("=" * 60)
|
|
304
|
+
|
|
305
|
+
# Handle example as list (though we'll use behavior_name for directory)
|
|
306
|
+
example_list = config["behavior"]["examples"]
|
|
307
|
+
if not isinstance(example_list, list):
|
|
308
|
+
# Backward compatibility: convert single string to list
|
|
309
|
+
example_list = [example_list] if example_list else []
|
|
310
|
+
|
|
311
|
+
behavior_name = config["behavior"]["name"]
|
|
312
|
+
ideation_model = config.get("ideation", {}).get("model", "claude-sonnet-4")
|
|
313
|
+
reasoning_effort = get_reasoning_effort(config, "ideation")
|
|
314
|
+
temperature = config.get("temperature", None)
|
|
315
|
+
total_evals = config.get("ideation", {}).get("total_evals", 10)
|
|
316
|
+
diversity = config.get("ideation", {}).get("diversity", 0.2)
|
|
317
|
+
max_tokens = config.get("ideation", {}).get("max_tokens", 50000)
|
|
318
|
+
web_search = config.get("ideation", {}).get("web_search", False)
|
|
319
|
+
|
|
320
|
+
# Handle incompatibility between web search and extended thinking
|
|
321
|
+
# According to Anthropic documentation, web search and extended thinking are designed for different use cases
|
|
322
|
+
# and should not be used together. Web search is for factual queries, extended thinking is for complex reasoning.
|
|
323
|
+
if web_search and reasoning_effort != "none":
|
|
324
|
+
debug_print(f"⚠️ Web search is enabled. Automatically disabling extended thinking (reasoning_effort).")
|
|
325
|
+
debug_print(f" Note: Web search and extended thinking are incompatible per Anthropic's guidance.")
|
|
326
|
+
debug_print(f" Original reasoning_effort: {reasoning_effort} → Setting to: none")
|
|
327
|
+
reasoning_effort = "none"
|
|
328
|
+
|
|
329
|
+
# Calculate base scenarios and perturbations per scenario
|
|
330
|
+
num_base_scenarios = max(1, int(total_evals * diversity)) # Number of diverse base scenarios
|
|
331
|
+
num_perturbations = max(1, int(1 / diversity)) if diversity > 0 else 1 # Variations per scenario (including original)
|
|
332
|
+
|
|
333
|
+
modality = config.get("rollout", {}).get("modality", "conversation")
|
|
334
|
+
no_user_mode = config.get("rollout", {}).get("no_user_mode", False)
|
|
335
|
+
max_turns = config.get("rollout", {}).get("max_turns", 20)
|
|
336
|
+
|
|
337
|
+
debug_print(f"🎯 Behavior: {behavior_name}")
|
|
338
|
+
debug_print(f"🤖 Ideation model: {ideation_model}")
|
|
339
|
+
debug_print(f"🧠 Reasoning effort: {reasoning_effort}")
|
|
340
|
+
debug_print(f"🌐 Web search: {web_search}")
|
|
341
|
+
debug_print(f"🌡️ Temperature: {temperature}")
|
|
342
|
+
debug_print(f"📝 Max tokens: {max_tokens}")
|
|
343
|
+
debug_print(f"📊 Total evals: {total_evals}")
|
|
344
|
+
debug_print(f"🎲 Diversity: {diversity}")
|
|
345
|
+
debug_print(f"📈 Number of base scenarios: {num_base_scenarios}")
|
|
346
|
+
debug_print(f"🔄 Perturbations per scenario (including original): {num_perturbations}")
|
|
347
|
+
debug_print(f"🔄 Modality: {modality}")
|
|
348
|
+
debug_print(f"👤 No user mode: {no_user_mode}")
|
|
349
|
+
debug_print(f"🔄 Max turns: {max_turns}")
|
|
350
|
+
|
|
351
|
+
# Load understanding results from behavior directory
|
|
352
|
+
debug_print(f"📚 Loading understanding results for behavior: {behavior_name}")
|
|
353
|
+
understanding_results = load_understanding_results(behavior_name)
|
|
354
|
+
debug_print("✅ Understanding results loaded successfully")
|
|
355
|
+
|
|
356
|
+
# Extract required fields from understanding
|
|
357
|
+
behavior_understanding = understanding_results["understanding"]
|
|
358
|
+
scientific_motivation = understanding_results["scientific_motivation"]
|
|
359
|
+
|
|
360
|
+
# Extract transcript analyses (list of summaries and attributions)
|
|
361
|
+
transcript_analyses = understanding_results.get("transcript_analyses", [])
|
|
362
|
+
debug_print(f"📊 Found {len(transcript_analyses)} transcript analyses from understanding")
|
|
363
|
+
debug_print(f"📖 Behavior understanding length: {len(behavior_understanding)} characters")
|
|
364
|
+
debug_print(f"🔬 Scientific motivation length: {len(scientific_motivation)} characters")
|
|
365
|
+
|
|
366
|
+
# Model selection and thinking support
|
|
367
|
+
if ideation_model in models:
|
|
368
|
+
model_id = models[ideation_model]["id"]
|
|
369
|
+
debug_print(f"🔧 Using model: {ideation_model} (ID: {model_id})")
|
|
370
|
+
else:
|
|
371
|
+
model_id = ideation_model
|
|
372
|
+
debug_print(f"🔧 Using custom model: {ideation_model}")
|
|
373
|
+
|
|
374
|
+
# Get target model name for prompts (only if not anonymous)
|
|
375
|
+
anonymous_target = config.get("anonymous_target", False)
|
|
376
|
+
target_model_name = None
|
|
377
|
+
if not anonymous_target:
|
|
378
|
+
target_model_key = config.get("rollout", {}).get("target", "unknown")
|
|
379
|
+
target_model_name = models.get(target_model_key, {}).get("name", target_model_key)
|
|
380
|
+
debug_print(f"🎯 Target model being evaluated: {target_model_name}")
|
|
381
|
+
else:
|
|
382
|
+
debug_print(f"🎯 Target model identity will remain anonymous")
|
|
383
|
+
|
|
384
|
+
# Create system prompt
|
|
385
|
+
debug_print("📝 Creating system prompt for ideation...")
|
|
386
|
+
system_prompt = make_system_prompt(
|
|
387
|
+
behavior_understanding,
|
|
388
|
+
scientific_motivation,
|
|
389
|
+
transcript_analyses,
|
|
390
|
+
behavior_name,
|
|
391
|
+
modality=modality,
|
|
392
|
+
prompts=prompts,
|
|
393
|
+
target_model_name=target_model_name
|
|
394
|
+
)
|
|
395
|
+
debug_print(f"✅ System prompt created ({len(system_prompt)} characters)")
|
|
396
|
+
|
|
397
|
+
# Cap max_tokens to model's actual maximum to avoid API errors
|
|
398
|
+
model_max_tokens = get_model_max_output_tokens(model_id)
|
|
399
|
+
if max_tokens > model_max_tokens:
|
|
400
|
+
debug_print(f"⚠️ Capping max_tokens from {max_tokens} to model max {model_max_tokens}")
|
|
401
|
+
max_tokens = model_max_tokens
|
|
402
|
+
|
|
403
|
+
# Initialize for base scenario generation
|
|
404
|
+
all_base_scenarios = []
|
|
405
|
+
|
|
406
|
+
# STEP 1: Calculate optimal batch size based on model capabilities
|
|
407
|
+
batch_size, num_batches = calculate_batch_size(
|
|
408
|
+
total_scenarios=num_base_scenarios,
|
|
409
|
+
model_id=model_id,
|
|
410
|
+
modality=modality,
|
|
411
|
+
reasoning_effort=reasoning_effort,
|
|
412
|
+
safety_margin=0.75 # Use 75% of max to be safe
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
debug_print(f"🚀 Starting base scenario generation for {num_base_scenarios} scenarios in {num_batches} batch(es)...")
|
|
416
|
+
|
|
417
|
+
# Initialize conversation history (accumulated across batches)
|
|
418
|
+
messages = []
|
|
419
|
+
|
|
420
|
+
# Generate scenarios in batches
|
|
421
|
+
for batch_num in range(num_batches):
|
|
422
|
+
# Calculate scenario range for this batch
|
|
423
|
+
start_idx = batch_num * batch_size + 1
|
|
424
|
+
end_idx = min(start_idx + batch_size - 1, num_base_scenarios)
|
|
425
|
+
batch_count = end_idx - start_idx + 1
|
|
426
|
+
|
|
427
|
+
debug_print(f"\n{'=' * 80}")
|
|
428
|
+
debug_print(f"📦 BATCH {batch_num + 1}/{num_batches}: Generating scenarios {start_idx}-{end_idx} ({batch_count} scenarios)")
|
|
429
|
+
debug_print(f"{'=' * 80}")
|
|
430
|
+
|
|
431
|
+
# Create prompt for this batch
|
|
432
|
+
debug_print(f"📋 Creating prompt for batch {batch_num + 1}...")
|
|
433
|
+
batch_prompt = make_all_scenarios_prompt(
|
|
434
|
+
behavior_name,
|
|
435
|
+
num_base_scenarios,
|
|
436
|
+
behavior_understanding=behavior_understanding,
|
|
437
|
+
scientific_motivation=scientific_motivation,
|
|
438
|
+
transcript_analyses=transcript_analyses,
|
|
439
|
+
modality=modality,
|
|
440
|
+
no_user_mode=no_user_mode,
|
|
441
|
+
max_turns=max_turns,
|
|
442
|
+
prompts=prompts,
|
|
443
|
+
start_idx=start_idx,
|
|
444
|
+
end_idx=end_idx,
|
|
445
|
+
target_model_name=target_model_name
|
|
446
|
+
)
|
|
447
|
+
debug_print(f"📝 Batch prompt created ({len(batch_prompt)} characters)")
|
|
448
|
+
|
|
449
|
+
# Add user message to ongoing conversation
|
|
450
|
+
messages.append({"role": "user", "content": batch_prompt})
|
|
451
|
+
|
|
452
|
+
# Print the exact prompt being sent (only for first batch to avoid clutter)
|
|
453
|
+
if batch_num == 0:
|
|
454
|
+
debug_print("=" * 80)
|
|
455
|
+
debug_print("📤 EXACT SYSTEM PROMPT TO IDEATION MODEL:")
|
|
456
|
+
debug_print("=" * 80)
|
|
457
|
+
debug_print(system_prompt)
|
|
458
|
+
debug_print("=" * 80)
|
|
459
|
+
# Make API call for this batch
|
|
460
|
+
debug_print(f"🤖 Calling model for batch {batch_num + 1} (scenarios {start_idx}-{end_idx})...")
|
|
461
|
+
|
|
462
|
+
# Build kwargs for litellm_chat
|
|
463
|
+
chat_kwargs = {
|
|
464
|
+
"model_id": model_id,
|
|
465
|
+
"messages": messages,
|
|
466
|
+
"system_prompt": system_prompt,
|
|
467
|
+
"max_tokens": max_tokens,
|
|
468
|
+
"reasoning_effort": reasoning_effort,
|
|
469
|
+
"temperature": temperature
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
# Add web_search_options if enabled
|
|
473
|
+
if web_search:
|
|
474
|
+
chat_kwargs["web_search_options"] = {"search_context_size": "medium"}
|
|
475
|
+
debug_print(f"🌐 Web search enabled for base scenario generation")
|
|
476
|
+
|
|
477
|
+
response = litellm_chat(**chat_kwargs)
|
|
478
|
+
debug_print(f"✅ Model response received for batch {batch_num + 1}")
|
|
479
|
+
|
|
480
|
+
# Debug: Check finish reason and token usage
|
|
481
|
+
if hasattr(response, 'choices') and response.choices:
|
|
482
|
+
choice = response.choices[0]
|
|
483
|
+
finish_reason = getattr(choice, 'finish_reason', 'N/A')
|
|
484
|
+
debug_print(f" Finish reason: {finish_reason}")
|
|
485
|
+
if finish_reason == "length":
|
|
486
|
+
debug_print(f" ⚠️ WARNING: Batch {batch_num + 1} hit token limit! May have incomplete scenarios.")
|
|
487
|
+
if hasattr(response, 'usage'):
|
|
488
|
+
usage = response.usage
|
|
489
|
+
debug_print(f" Token usage: prompt={getattr(usage, 'prompt_tokens', 'N/A')}, completion={getattr(usage, 'completion_tokens', 'N/A')}, total={getattr(usage, 'total_tokens', 'N/A')}")
|
|
490
|
+
|
|
491
|
+
parsed = parse_message(response)
|
|
492
|
+
ideation_response = parsed["content"] or ""
|
|
493
|
+
reasoning_content = parsed["reasoning"]
|
|
494
|
+
|
|
495
|
+
debug_print(f" Parsed content length: {len(ideation_response)}")
|
|
496
|
+
debug_print(f" Parsed reasoning length: {len(reasoning_content) if reasoning_content else 0}")
|
|
497
|
+
|
|
498
|
+
if reasoning_content:
|
|
499
|
+
ideation_response = f"<thinking>\n{reasoning_content.strip()}\n</thinking>\n\n{ideation_response}"
|
|
500
|
+
debug_print(f"🧠 Reasoning content extracted ({len(reasoning_content)} characters)")
|
|
501
|
+
|
|
502
|
+
# Print the exact response from the model
|
|
503
|
+
debug_print("=" * 80)
|
|
504
|
+
debug_print(f"📥 EXACT RESPONSE FROM IDEATION MODEL (BATCH {batch_num + 1}):")
|
|
505
|
+
debug_print("=" * 80)
|
|
506
|
+
debug_print(ideation_response)
|
|
507
|
+
debug_print("=" * 80)
|
|
508
|
+
|
|
509
|
+
# Add assistant response to conversation history for next batch
|
|
510
|
+
messages.append({"role": "assistant", "content": ideation_response})
|
|
511
|
+
debug_print(f"💾 Added assistant response to conversation history (conversation now has {len(messages)} messages)")
|
|
512
|
+
|
|
513
|
+
# Parse scenarios from this batch
|
|
514
|
+
debug_print(f"🔍 Parsing scenarios from batch {batch_num + 1}...")
|
|
515
|
+
batch_scenarios, _ = parse_scenarios_response(ideation_response)
|
|
516
|
+
debug_print(f"📊 Found {len(batch_scenarios)} scenarios in batch {batch_num + 1} (expected {batch_count})")
|
|
517
|
+
|
|
518
|
+
# Warn if we didn't get the expected number for this batch
|
|
519
|
+
if len(batch_scenarios) != batch_count:
|
|
520
|
+
debug_print(f"⚠️ WARNING: Expected {batch_count} scenarios in batch {batch_num + 1} but got {len(batch_scenarios)}")
|
|
521
|
+
debug_print(f" This may be due to parsing issues or the model not following instructions.")
|
|
522
|
+
else:
|
|
523
|
+
debug_print(f"✅ Successfully generated exactly {batch_count} scenarios in batch {batch_num + 1}!")
|
|
524
|
+
|
|
525
|
+
# Add batch scenarios to collection
|
|
526
|
+
all_base_scenarios.extend(batch_scenarios)
|
|
527
|
+
debug_print(f"📈 Total scenarios so far: {len(all_base_scenarios)}/{num_base_scenarios}")
|
|
528
|
+
|
|
529
|
+
debug_print(f"\n{'=' * 80}")
|
|
530
|
+
debug_print(f"📊 Base scenario generation completed!")
|
|
531
|
+
debug_print(f" Total scenarios generated: {len(all_base_scenarios)}")
|
|
532
|
+
debug_print(f" Expected: {num_base_scenarios}")
|
|
533
|
+
debug_print(f" Batches used: {num_batches}")
|
|
534
|
+
debug_print(f"{'=' * 80}")
|
|
535
|
+
|
|
536
|
+
# Final warning if we didn't get the expected total
|
|
537
|
+
if len(all_base_scenarios) != num_base_scenarios:
|
|
538
|
+
debug_print(f"⚠️ WARNING: Expected {num_base_scenarios} total scenarios but got {len(all_base_scenarios)}")
|
|
539
|
+
else:
|
|
540
|
+
debug_print(f"✅ Successfully generated exactly {num_base_scenarios} scenarios across all batches!")
|
|
541
|
+
|
|
542
|
+
# STEP 2: Generate variations for each base scenario
|
|
543
|
+
debug_print(f"🔄 Starting variation generation ({num_perturbations - 1} variations per base scenario)...")
|
|
544
|
+
|
|
545
|
+
all_variations = []
|
|
546
|
+
variation_system_prompt = make_variation_system_prompt(num_perturbations, prompts)
|
|
547
|
+
|
|
548
|
+
for idx, base_scenario in enumerate(all_base_scenarios, 1):
|
|
549
|
+
debug_print(f"🔄 Processing base scenario {idx}/{len(all_base_scenarios)}...")
|
|
550
|
+
|
|
551
|
+
# Extract scenario details
|
|
552
|
+
scenario_description = base_scenario.get("description", "")
|
|
553
|
+
scenario_tools = base_scenario.get("tools", [])
|
|
554
|
+
|
|
555
|
+
# First, add the original scenario to variations
|
|
556
|
+
all_variations.append({
|
|
557
|
+
"description": scenario_description,
|
|
558
|
+
"tools": scenario_tools
|
|
559
|
+
})
|
|
560
|
+
debug_print(f"✅ Added original scenario {idx} to variations")
|
|
561
|
+
|
|
562
|
+
# Only generate variations if num_perturbations > 1
|
|
563
|
+
if num_perturbations > 1:
|
|
564
|
+
debug_print(f"🤖 Generating {num_perturbations - 1} variations for scenario {idx}...")
|
|
565
|
+
|
|
566
|
+
# Create variation prompt
|
|
567
|
+
variation_prompt = make_variation_prompt(
|
|
568
|
+
behavior_name,
|
|
569
|
+
behavior_understanding,
|
|
570
|
+
scientific_motivation,
|
|
571
|
+
scenario_description,
|
|
572
|
+
num_perturbations,
|
|
573
|
+
modality=modality,
|
|
574
|
+
tools=scenario_tools,
|
|
575
|
+
prompts=prompts
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
# Call model to generate variations
|
|
579
|
+
variation_messages = [{"role": "user", "content": variation_prompt}]
|
|
580
|
+
|
|
581
|
+
variation_response = litellm_chat(
|
|
582
|
+
model_id=model_id,
|
|
583
|
+
messages=variation_messages,
|
|
584
|
+
system_prompt=variation_system_prompt,
|
|
585
|
+
max_tokens=max_tokens,
|
|
586
|
+
reasoning_effort=reasoning_effort,
|
|
587
|
+
temperature=temperature
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
# Debug: Check finish reason and token usage for variations
|
|
591
|
+
if hasattr(variation_response, 'choices') and variation_response.choices:
|
|
592
|
+
choice = variation_response.choices[0]
|
|
593
|
+
debug_print(f" Variation finish reason: {getattr(choice, 'finish_reason', 'N/A')}")
|
|
594
|
+
if hasattr(variation_response, 'usage'):
|
|
595
|
+
usage = variation_response.usage
|
|
596
|
+
debug_print(f" Variation token usage: prompt={getattr(usage, 'prompt_tokens', 'N/A')}, completion={getattr(usage, 'completion_tokens', 'N/A')}, total={getattr(usage, 'total_tokens', 'N/A')}")
|
|
597
|
+
|
|
598
|
+
parsed_var = parse_message(variation_response)
|
|
599
|
+
variation_response_text = parsed_var["content"] or ""
|
|
600
|
+
reasoning_content_var = parsed_var["reasoning"]
|
|
601
|
+
|
|
602
|
+
debug_print(f" Variation parsed content length: {len(variation_response_text)}")
|
|
603
|
+
debug_print(f" Variation parsed reasoning length: {len(reasoning_content_var) if reasoning_content_var else 0}")
|
|
604
|
+
|
|
605
|
+
if reasoning_content_var:
|
|
606
|
+
variation_response_text = f"<thinking>\n{reasoning_content_var.strip()}\n</thinking>\n\n{variation_response_text}"
|
|
607
|
+
|
|
608
|
+
# Debug: Print the variation response to see what was generated
|
|
609
|
+
debug_print("=" * 60)
|
|
610
|
+
debug_print(f"VARIATION RESPONSE FOR SCENARIO {idx}:")
|
|
611
|
+
debug_print(f"Total length: {len(variation_response_text)} characters")
|
|
612
|
+
debug_print("=" * 60)
|
|
613
|
+
debug_print(variation_response_text) # Print FULL response
|
|
614
|
+
debug_print("=" * 60)
|
|
615
|
+
|
|
616
|
+
# Parse variations
|
|
617
|
+
variations, _ = parse_variations_response(variation_response_text, original_tools=scenario_tools)
|
|
618
|
+
debug_print(f"📊 Found {len(variations)} variations for scenario {idx} (expected {num_perturbations - 1})")
|
|
619
|
+
|
|
620
|
+
# Add variations to collection
|
|
621
|
+
all_variations.extend(variations)
|
|
622
|
+
debug_print(f"✅ Total variations so far: {len(all_variations)}")
|
|
623
|
+
else:
|
|
624
|
+
debug_print(f"⏭️ Skipping variation generation (num_perturbations = 1)")
|
|
625
|
+
|
|
626
|
+
debug_print(f"📊 Variation generation completed! Total variations (including originals): {len(all_variations)}")
|
|
627
|
+
|
|
628
|
+
# Save results with "variations" key for backward compatibility
|
|
629
|
+
debug_print("💾 Preparing results for saving...")
|
|
630
|
+
results = {
|
|
631
|
+
"behavior_name": behavior_name,
|
|
632
|
+
"examples": example_list,
|
|
633
|
+
"model": ideation_model,
|
|
634
|
+
"temperature": temperature,
|
|
635
|
+
"reasoning_effort": reasoning_effort,
|
|
636
|
+
"num_base_scenarios": num_base_scenarios,
|
|
637
|
+
"num_perturbations_per_scenario": num_perturbations,
|
|
638
|
+
"total_evals": total_evals,
|
|
639
|
+
"diversity": diversity,
|
|
640
|
+
"variations": all_variations # Use "variations" key for compatibility with rollout/judgment
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
# Ensure results directory exists and save using behavior name
|
|
644
|
+
results_dir = get_results_dir(behavior_name)
|
|
645
|
+
|
|
646
|
+
output_file = results_dir / "ideation.json"
|
|
647
|
+
debug_print(f"📁 Saving results to: {output_file}")
|
|
648
|
+
save_results_locally(results, str(output_file), behavior_name)
|
|
649
|
+
|
|
650
|
+
debug_print(f"✅ Ideation (with variations) completed successfully!")
|
|
651
|
+
debug_print(f"📈 Generated {len(all_base_scenarios)} base scenarios")
|
|
652
|
+
debug_print(f"🔄 Generated {len(all_variations)} total variations (including originals)")
|
|
653
|
+
debug_print(f"🎲 Diversity: {diversity}")
|
|
654
|
+
debug_print(f"📊 Perturbations per scenario: {num_perturbations}")
|
|
655
|
+
print("Ideation done")
|
|
656
|
+
|
|
657
|
+
if __name__ == "__main__":
|
|
658
|
+
# Parse command line arguments for debug mode
|
|
659
|
+
debug_mode = False
|
|
660
|
+
if '--debug' in sys.argv:
|
|
661
|
+
debug_mode = True
|
|
662
|
+
sys.argv.remove('--debug') # Remove debug flag from argv
|
|
663
|
+
from bloom import set_debug_mode
|
|
664
|
+
set_debug_mode(True)
|
|
665
|
+
|
|
666
|
+
config_path = sys.argv[1] if len(sys.argv) > 1 else "seed.yaml"
|
|
667
|
+
run_ideation(config_path)
|