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.
Files changed (91) hide show
  1. codeoptix/__init__.py +8 -0
  2. codeoptix/acp/__init__.py +33 -0
  3. codeoptix/acp/agent.py +209 -0
  4. codeoptix/acp/bridge.py +402 -0
  5. codeoptix/acp/client_adapter.py +312 -0
  6. codeoptix/acp/code_extractor.py +125 -0
  7. codeoptix/acp/orchestrator.py +349 -0
  8. codeoptix/acp/registry.py +294 -0
  9. codeoptix/adapters/__init__.py +18 -0
  10. codeoptix/adapters/base.py +50 -0
  11. codeoptix/adapters/basic.py +195 -0
  12. codeoptix/adapters/claude_code.py +218 -0
  13. codeoptix/adapters/codex.py +327 -0
  14. codeoptix/adapters/factory.py +56 -0
  15. codeoptix/adapters/gemini_cli.py +370 -0
  16. codeoptix/artifacts/__init__.py +5 -0
  17. codeoptix/artifacts/manager.py +193 -0
  18. codeoptix/behaviors/__init__.py +45 -0
  19. codeoptix/behaviors/base.py +81 -0
  20. codeoptix/behaviors/insecure_code.py +129 -0
  21. codeoptix/behaviors/plan_drift.py +192 -0
  22. codeoptix/behaviors/vacuous_tests.py +198 -0
  23. codeoptix/cli.py +1472 -0
  24. codeoptix/evaluation/__init__.py +23 -0
  25. codeoptix/evaluation/bloom_integration.py +271 -0
  26. codeoptix/evaluation/engine.py +274 -0
  27. codeoptix/evaluation/evaluators.py +308 -0
  28. codeoptix/evaluation/scenario_generator.py +222 -0
  29. codeoptix/evolution/__init__.py +7 -0
  30. codeoptix/evolution/engine.py +206 -0
  31. codeoptix/evolution/gepa_integration.py +149 -0
  32. codeoptix/evolution/proposer.py +185 -0
  33. codeoptix/linters/__init__.py +13 -0
  34. codeoptix/linters/bandit_linter.py +172 -0
  35. codeoptix/linters/base.py +105 -0
  36. codeoptix/linters/coverage_linter.py +156 -0
  37. codeoptix/linters/flake8_linter.py +156 -0
  38. codeoptix/linters/html_accessibility_linter.py +374 -0
  39. codeoptix/linters/language_detector.py +150 -0
  40. codeoptix/linters/mypy_linter.py +184 -0
  41. codeoptix/linters/pip_audit_linter.py +152 -0
  42. codeoptix/linters/pylint_linter.py +198 -0
  43. codeoptix/linters/ruff_linter.py +206 -0
  44. codeoptix/linters/runner.py +186 -0
  45. codeoptix/linters/safety_linter.py +184 -0
  46. codeoptix/reflection/__init__.py +6 -0
  47. codeoptix/reflection/engine.py +70 -0
  48. codeoptix/reflection/generator.py +209 -0
  49. codeoptix/utils/__init__.py +1 -0
  50. codeoptix/utils/config.py +91 -0
  51. codeoptix/utils/llm.py +332 -0
  52. codeoptix/utils/retry.py +133 -0
  53. codeoptix/vendor/__init__.py +2 -0
  54. codeoptix/vendor/bloom/README.md +26 -0
  55. codeoptix/vendor/bloom/__init__.py +11 -0
  56. codeoptix/vendor/bloom/globals.py +39 -0
  57. codeoptix/vendor/bloom/orchestrators/ConversationOrchestrator.py +450 -0
  58. codeoptix/vendor/bloom/orchestrators/SimEnvOrchestrator.py +839 -0
  59. codeoptix/vendor/bloom/prompts/configurable_prompts/README.md +85 -0
  60. codeoptix/vendor/bloom/prompts/configurable_prompts/default.json +18 -0
  61. codeoptix/vendor/bloom/prompts/configurable_prompts/ideation-default.json +18 -0
  62. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_animal-welfare.json +18 -0
  63. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_contextual-optimism.json +18 -0
  64. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_defend-objects.json +18 -0
  65. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_defer-to-users.json +18 -0
  66. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_emotional-bond.json +18 -0
  67. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_flattery.json +18 -0
  68. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_hardcode-test-cases.json +18 -0
  69. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_increasing-pep.json +18 -0
  70. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_research-sandbagging.json +18 -0
  71. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_self-promotion.json +18 -0
  72. codeoptix/vendor/bloom/prompts/configurable_prompts/sandbag.json +18 -0
  73. codeoptix/vendor/bloom/prompts/configurable_prompts/self-preferential-bias.json +18 -0
  74. codeoptix/vendor/bloom/prompts/configurable_prompts/static-prompts.yaml +72 -0
  75. codeoptix/vendor/bloom/prompts/configurable_prompts/web-search.json +18 -0
  76. codeoptix/vendor/bloom/prompts/step1_understanding.py +63 -0
  77. codeoptix/vendor/bloom/prompts/step2_ideation.py +254 -0
  78. codeoptix/vendor/bloom/prompts/step3_rollout.py +120 -0
  79. codeoptix/vendor/bloom/prompts/step4_judgment.py +183 -0
  80. codeoptix/vendor/bloom/schemas/behavior.schema.json +160 -0
  81. codeoptix/vendor/bloom/schemas/conversation.schema.json +51 -0
  82. codeoptix/vendor/bloom/schemas/transcript_schema.json +2225 -0
  83. codeoptix/vendor/bloom/scripts/step2_ideation.py +667 -0
  84. codeoptix/vendor/bloom/scripts/step4_judgment.py +811 -0
  85. codeoptix/vendor/bloom/transcript_utils.py +440 -0
  86. codeoptix/vendor/bloom/utils.py +700 -0
  87. codeoptix-0.1.0.dist-info/METADATA +304 -0
  88. codeoptix-0.1.0.dist-info/RECORD +91 -0
  89. codeoptix-0.1.0.dist-info/WHEEL +4 -0
  90. codeoptix-0.1.0.dist-info/entry_points.txt +2 -0
  91. 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)