codeoptix 0.1.3__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 +221 -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 +1468 -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 +334 -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.3.dist-info/METADATA +295 -0
- codeoptix-0.1.3.dist-info/RECORD +92 -0
- codeoptix-0.1.3.dist-info/WHEEL +5 -0
- codeoptix-0.1.3.dist-info/entry_points.txt +2 -0
- codeoptix-0.1.3.dist-info/licenses/LICENSE +203 -0
- codeoptix-0.1.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
import litellm
|
|
13
|
+
from litellm import completion_with_retries
|
|
14
|
+
|
|
15
|
+
from codeoptix.vendor.bloom.globals import models, NUM_RETRIES
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def setup_project_path():
|
|
19
|
+
"""Add the project root to Python path for imports."""
|
|
20
|
+
# Get the project root (assuming utils.py is in the project root)
|
|
21
|
+
project_root = Path(__file__).parent
|
|
22
|
+
if str(project_root) not in sys.path:
|
|
23
|
+
sys.path.insert(0, str(project_root))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_config(config_path="seed.yaml"):
|
|
27
|
+
"""Load configuration from YAML file."""
|
|
28
|
+
with open(config_path, 'r') as f:
|
|
29
|
+
config = yaml.safe_load(f)
|
|
30
|
+
|
|
31
|
+
# Check if this is a sweep config (has 'parameters' key)
|
|
32
|
+
if 'parameters' in config:
|
|
33
|
+
# Convert sweep config to runtime config
|
|
34
|
+
params = {}
|
|
35
|
+
for key, value in config['parameters'].items():
|
|
36
|
+
# Extract the 'value' field from sweep parameter format
|
|
37
|
+
if isinstance(value, dict) and 'value' in value:
|
|
38
|
+
params[key] = value['value']
|
|
39
|
+
else:
|
|
40
|
+
params[key] = value
|
|
41
|
+
|
|
42
|
+
# Convert to runtime config using the same function as wandb
|
|
43
|
+
return create_config_from_wandb_params(params)
|
|
44
|
+
|
|
45
|
+
return config
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load_configurable_prompts(config: Dict) -> Dict[str, str]:
|
|
49
|
+
"""
|
|
50
|
+
Load configurable prompts from a JSON file specified in config.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
config: Config dictionary containing 'configurable_prompts' key
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Dictionary containing all prompt strings from the JSON file.
|
|
57
|
+
"""
|
|
58
|
+
# Get the prompts file name from config, default to "default"
|
|
59
|
+
prompts_file = config.get('configurable_prompts', 'default')
|
|
60
|
+
|
|
61
|
+
# Remove extension if provided
|
|
62
|
+
if prompts_file.endswith('.json'):
|
|
63
|
+
prompts_file = prompts_file[:-5]
|
|
64
|
+
|
|
65
|
+
# Construct path - always look in prompts/configurable_prompts/
|
|
66
|
+
project_root = Path(__file__).parent
|
|
67
|
+
prompts_path = project_root / 'prompts' / 'configurable_prompts' / f'{prompts_file}.json'
|
|
68
|
+
|
|
69
|
+
# Load the JSON file
|
|
70
|
+
try:
|
|
71
|
+
with open(prompts_path, 'r', encoding='utf-8') as f:
|
|
72
|
+
return json.load(f)
|
|
73
|
+
except FileNotFoundError:
|
|
74
|
+
# Fallback to default.json
|
|
75
|
+
default_path = project_root / 'prompts' / 'configurable_prompts' / 'default.json'
|
|
76
|
+
with open(default_path, 'r', encoding='utf-8') as f:
|
|
77
|
+
return json.load(f)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_reasoning_effort(config, stage_name, model_type="evaluator"):
|
|
81
|
+
# Get the appropriate reasoning effort based on stage and model type
|
|
82
|
+
if stage_name == "rollout" and model_type == "target":
|
|
83
|
+
return config.get("target_reasoning_effort", "none")
|
|
84
|
+
else:
|
|
85
|
+
return config.get("evaluator_reasoning_effort", "high")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def create_config_from_wandb_params(wandb_params):
|
|
89
|
+
"""Create a config dictionary from wandb parameters using nested structure."""
|
|
90
|
+
config = {}
|
|
91
|
+
|
|
92
|
+
# Helper function to get nested parameter value
|
|
93
|
+
def get_nested_param(param_name, default_value):
|
|
94
|
+
# Handle both nested (e.g., "behavior.name") and flat (e.g., "behavior_name") parameter names
|
|
95
|
+
if param_name in wandb_params:
|
|
96
|
+
return wandb_params[param_name]
|
|
97
|
+
|
|
98
|
+
# Try to find nested parameter
|
|
99
|
+
for key, value in wandb_params.items():
|
|
100
|
+
if key == param_name:
|
|
101
|
+
return value
|
|
102
|
+
|
|
103
|
+
return default_value
|
|
104
|
+
|
|
105
|
+
# behavior parameters
|
|
106
|
+
config["behavior"] = {
|
|
107
|
+
"name": get_nested_param("behavior.name", "sycophancy"),
|
|
108
|
+
"examples": get_nested_param("behavior.examples", ["sycophancy-sonnet4"])
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# Global parameters
|
|
112
|
+
config["temperature"] = get_nested_param("temperature", 1.0)
|
|
113
|
+
# Get reasoning effort parameters directly (no thinking_budget fallback)
|
|
114
|
+
config["evaluator_reasoning_effort"] = get_nested_param("evaluator_reasoning_effort", "high")
|
|
115
|
+
config["target_reasoning_effort"] = get_nested_param("target_reasoning_effort", "none")
|
|
116
|
+
config["debug"] = get_nested_param("debug", False)
|
|
117
|
+
config["max_concurrent"] = get_nested_param("max_concurrent", 15)
|
|
118
|
+
config["configurable_prompts"] = get_nested_param("configurable_prompts", "default")
|
|
119
|
+
|
|
120
|
+
# Understanding parameters
|
|
121
|
+
config["understanding"] = {
|
|
122
|
+
"model": get_nested_param("understanding.model", "deepseek-r1"),
|
|
123
|
+
"max_tokens": get_nested_param("understanding.max_tokens", 4000)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# Ideation parameters
|
|
127
|
+
config["ideation"] = {
|
|
128
|
+
"model": get_nested_param("ideation.model", "gpt-4.1"),
|
|
129
|
+
"total_evals": get_nested_param("ideation.total_evals", 10),
|
|
130
|
+
"diversity": get_nested_param("ideation.diversity", 0.2),
|
|
131
|
+
"max_tokens": get_nested_param("ideation.max_tokens", 4000),
|
|
132
|
+
"web_search": get_nested_param("ideation.web_search", False)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# Variation parameters
|
|
136
|
+
config["variation"] = {
|
|
137
|
+
"model": get_nested_param("variation.model", "claude-sonnet-4"),
|
|
138
|
+
"max_tokens": get_nested_param("variation.max_tokens", 4000)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# Rollout parameters
|
|
142
|
+
config["rollout"] = {
|
|
143
|
+
"model": get_nested_param("rollout.model", "gemini-2.5-flash"),
|
|
144
|
+
"target": get_nested_param("rollout.target", "gpt-4o"),
|
|
145
|
+
"modality": get_nested_param("rollout.modality", "simenv"),
|
|
146
|
+
"max_turns": get_nested_param("rollout.max_turns", 5),
|
|
147
|
+
"num_reps": get_nested_param("rollout.num_reps", 1),
|
|
148
|
+
"max_tokens": get_nested_param("rollout.max_tokens", 4000),
|
|
149
|
+
"selected_variations": get_nested_param("rollout.selected_variations", None),
|
|
150
|
+
"no_user_mode": get_nested_param("rollout.no_user_mode", False)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# Judgment parameters
|
|
154
|
+
config["judgment"] = {
|
|
155
|
+
"model": get_nested_param("judgment.model", "gpt-5-mini"),
|
|
156
|
+
"max_tokens": get_nested_param("judgment.max_tokens", 4000),
|
|
157
|
+
"num_samples": get_nested_param("judgment.num_samples", 1),
|
|
158
|
+
"additional_qualities": get_nested_param("judgment.additional_qualities", ["unrealism", "evaluator-forcefulness", "evaluation-awareness"]),
|
|
159
|
+
"metajudgment_qualities": get_nested_param("judgment.metajudgment_qualities", ["diversity"])
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# Resume parameters (optional)
|
|
163
|
+
resume_run_id = get_nested_param("resume", None)
|
|
164
|
+
resume_stage = get_nested_param("resume_stage", None)
|
|
165
|
+
if resume_run_id:
|
|
166
|
+
config["resume"] = resume_run_id
|
|
167
|
+
if resume_stage:
|
|
168
|
+
config["resume_stage"] = resume_stage
|
|
169
|
+
|
|
170
|
+
return config
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def load_behaviors(behaviors_path="behaviors/behaviors.json"):
|
|
174
|
+
"""Load behavior descriptions."""
|
|
175
|
+
with open(behaviors_path, 'r') as f:
|
|
176
|
+
return json.load(f)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def load_example(example_name):
|
|
180
|
+
"""Load behavior example from JSON file."""
|
|
181
|
+
example_path = f"behaviors/examples/{example_name}.json"
|
|
182
|
+
with open(example_path, 'r') as f:
|
|
183
|
+
return json.load(f)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def extract_transcript(example_data):
|
|
187
|
+
"""Extract the conversation transcript from example data, including system prompt at the start.
|
|
188
|
+
|
|
189
|
+
Supports multiple input shapes:
|
|
190
|
+
- transcript.schema.json: { "events": [ { type: "transcript_event", edit: { message: {...} } } ] }
|
|
191
|
+
- behavior.schema.json: { "events": [ { event: "evaluator_message" | "target_message" | ... } ] }
|
|
192
|
+
- OSS examples: [ { "type": "system" | "user" | "assistant" | "tool", ... }, ... ]
|
|
193
|
+
- legacy: { "conversation": [ { role: "user" | "assistant", content: str }, ... ] }
|
|
194
|
+
"""
|
|
195
|
+
transcript_lines = []
|
|
196
|
+
|
|
197
|
+
# Helper to normalize content that may be string or list of blocks
|
|
198
|
+
def _normalize_content(content):
|
|
199
|
+
if isinstance(content, list):
|
|
200
|
+
text_parts = []
|
|
201
|
+
for block in content:
|
|
202
|
+
if isinstance(block, dict):
|
|
203
|
+
if "text" in block:
|
|
204
|
+
text_parts.append(str(block.get("text", "")))
|
|
205
|
+
elif isinstance(block, str):
|
|
206
|
+
text_parts.append(block)
|
|
207
|
+
return "\n".join([part for part in text_parts if part])
|
|
208
|
+
return content if isinstance(content, str) else ""
|
|
209
|
+
|
|
210
|
+
# Look for target system prompt at top level (for transcript.schema.json format)
|
|
211
|
+
if isinstance(example_data, dict) and "target_system_prompt" in example_data:
|
|
212
|
+
transcript_lines.append(f"TARGET SYSTEM PROMPT: {example_data['target_system_prompt']}")
|
|
213
|
+
|
|
214
|
+
# Look for system prompt in various possible locations (for other formats)
|
|
215
|
+
system_prompt = None
|
|
216
|
+
if isinstance(example_data, dict):
|
|
217
|
+
if "system_prompt" in example_data:
|
|
218
|
+
system_prompt = example_data["system_prompt"]
|
|
219
|
+
elif "system" in example_data:
|
|
220
|
+
system_prompt = example_data["system"]
|
|
221
|
+
elif "metadata" in example_data and isinstance(example_data["metadata"], dict):
|
|
222
|
+
metadata = example_data["metadata"]
|
|
223
|
+
if "system_prompt" in metadata:
|
|
224
|
+
system_prompt = metadata["system_prompt"]
|
|
225
|
+
elif "system" in metadata:
|
|
226
|
+
system_prompt = metadata["system"]
|
|
227
|
+
|
|
228
|
+
# Add system prompt at the beginning if found
|
|
229
|
+
if system_prompt:
|
|
230
|
+
transcript_lines.append(f"SYSTEM: {system_prompt}")
|
|
231
|
+
|
|
232
|
+
# Branch 1: transcript.schema.json format
|
|
233
|
+
if isinstance(example_data, dict) and "events" in example_data:
|
|
234
|
+
events = example_data["events"]
|
|
235
|
+
|
|
236
|
+
# Check if this is the transcript.schema.json format (with transcript_event type)
|
|
237
|
+
if events and len(events) > 0 and events[0].get("type") == "transcript_event":
|
|
238
|
+
# Process transcript events (transcript.schema.json format)
|
|
239
|
+
for event in events:
|
|
240
|
+
if event.get("type") == "transcript_event" and "edit" in event:
|
|
241
|
+
edit = event["edit"]
|
|
242
|
+
if edit.get("operation") == "add" and "message" in edit:
|
|
243
|
+
message = edit["message"]
|
|
244
|
+
msg_type = message.get("type", "")
|
|
245
|
+
content = message.get("content", "")
|
|
246
|
+
|
|
247
|
+
# Map message types to roles
|
|
248
|
+
if msg_type == "user":
|
|
249
|
+
transcript_lines.append(f"EVALUATOR: {content}")
|
|
250
|
+
elif msg_type == "assistant":
|
|
251
|
+
# Check if this is a tool call
|
|
252
|
+
if "tool_calls" in message and message["tool_calls"]:
|
|
253
|
+
for tool_call in message["tool_calls"]:
|
|
254
|
+
tool_name = tool_call.get("name", "unknown_tool")
|
|
255
|
+
args = tool_call.get("args", {})
|
|
256
|
+
transcript_lines.append(f"TARGET: [CALLING {tool_name.upper()}] {args}")
|
|
257
|
+
else:
|
|
258
|
+
transcript_lines.append(f"TARGET: {content}")
|
|
259
|
+
elif msg_type == "tool":
|
|
260
|
+
tool_name = message.get("name", "unknown_tool")
|
|
261
|
+
transcript_lines.append(f"EVALUATOR: [RESPONSE FROM {tool_name.upper()}] {content}")
|
|
262
|
+
elif msg_type == "system":
|
|
263
|
+
# Skip system messages in the event stream as we've already added the target system prompt
|
|
264
|
+
pass
|
|
265
|
+
else:
|
|
266
|
+
transcript_lines.append(f"{msg_type.upper()}: {content}")
|
|
267
|
+
|
|
268
|
+
else:
|
|
269
|
+
# Process events in behavior.schema.json format
|
|
270
|
+
for event in events:
|
|
271
|
+
event_type = event.get("event", "")
|
|
272
|
+
content = event.get("content", "")
|
|
273
|
+
|
|
274
|
+
if event_type == "evaluator_message":
|
|
275
|
+
transcript_lines.append(f"EVALUATOR: {content}")
|
|
276
|
+
elif event_type == "target_message":
|
|
277
|
+
# Add the message content if present
|
|
278
|
+
if content:
|
|
279
|
+
transcript_lines.append(f"TARGET: {content}")
|
|
280
|
+
# Check for tool_calls field at the event level
|
|
281
|
+
if "tool_calls" in event and event["tool_calls"]:
|
|
282
|
+
for tool_call in event["tool_calls"]:
|
|
283
|
+
tool_name = tool_call.get("name", "unknown_tool")
|
|
284
|
+
args = tool_call.get("args", {})
|
|
285
|
+
tool_id = tool_call.get("id", "")
|
|
286
|
+
transcript_lines.append(f"TARGET: [CALLING {tool_name.upper()}] (id: {tool_id}) {args}")
|
|
287
|
+
elif event_type == "tool":
|
|
288
|
+
# Tool response events have content as string directly
|
|
289
|
+
transcript_lines.append(f"TOOL RESPONSE: {content}")
|
|
290
|
+
elif event_type == "tool_call":
|
|
291
|
+
# Legacy format support if needed
|
|
292
|
+
tool_name = content.get("tool_name", "unknown_tool") if isinstance(content, dict) else "unknown_tool"
|
|
293
|
+
arguments = content.get("arguments", {}) if isinstance(content, dict) else {}
|
|
294
|
+
transcript_lines.append(f"TARGET: [CALLING {tool_name.upper()}] {arguments}")
|
|
295
|
+
elif event_type == "tool_response":
|
|
296
|
+
# Legacy format support if needed
|
|
297
|
+
tool_name = content.get("tool_name", "unknown_tool") if isinstance(content, dict) else "unknown_tool"
|
|
298
|
+
result = content.get("result", "") if isinstance(content, dict) else ""
|
|
299
|
+
transcript_lines.append(f"TOOL RESPONSE: {result}")
|
|
300
|
+
|
|
301
|
+
# Branch 2: legacy conversation array under dict
|
|
302
|
+
elif isinstance(example_data, dict) and "conversation" in example_data:
|
|
303
|
+
# Old behavior schema format - map roles to rollout context
|
|
304
|
+
for message in example_data["conversation"]:
|
|
305
|
+
role = message["role"]
|
|
306
|
+
content = message["content"]
|
|
307
|
+
tool_calls = message.get("tool_calls", [])
|
|
308
|
+
|
|
309
|
+
# Map traditional roles to rollout context
|
|
310
|
+
if role.lower() == "assistant":
|
|
311
|
+
display_role = "TARGET"
|
|
312
|
+
elif role.lower() == "user":
|
|
313
|
+
display_role = "EVALUATOR"
|
|
314
|
+
elif role.lower() == "tool":
|
|
315
|
+
display_role = "TOOL_RESPONSE"
|
|
316
|
+
else:
|
|
317
|
+
display_role = role.upper()
|
|
318
|
+
|
|
319
|
+
# Handle tool calls for assistant messages
|
|
320
|
+
if role.lower() == "assistant" and tool_calls:
|
|
321
|
+
# Add the content first
|
|
322
|
+
if content:
|
|
323
|
+
transcript_lines.append(f"{display_role}: {content}")
|
|
324
|
+
|
|
325
|
+
# Add tool calls
|
|
326
|
+
for tool_call in tool_calls:
|
|
327
|
+
if isinstance(tool_call, dict):
|
|
328
|
+
tool_name = tool_call.get("function", {}).get("name", "unknown_tool")
|
|
329
|
+
args = tool_call.get("function", {}).get("arguments", "{}")
|
|
330
|
+
transcript_lines.append(f"{display_role}: [CALLING {tool_name.upper()}] {args}")
|
|
331
|
+
else:
|
|
332
|
+
# Regular message
|
|
333
|
+
transcript_lines.append(f"{display_role}: {content}")
|
|
334
|
+
|
|
335
|
+
# Branch 3: OSS list-based messages format
|
|
336
|
+
elif isinstance(example_data, list):
|
|
337
|
+
for message in example_data:
|
|
338
|
+
msg_type = message.get("type", "")
|
|
339
|
+
content = _normalize_content(message.get("content", ""))
|
|
340
|
+
|
|
341
|
+
if msg_type == "system":
|
|
342
|
+
# Only include if we hadn't already from system_prompt
|
|
343
|
+
if content and not system_prompt:
|
|
344
|
+
transcript_lines.append(f"SYSTEM: {content}")
|
|
345
|
+
elif msg_type == "user":
|
|
346
|
+
if content:
|
|
347
|
+
transcript_lines.append(f"EVALUATOR: {content}")
|
|
348
|
+
elif msg_type == "assistant":
|
|
349
|
+
# Assistant messages may include tool calls
|
|
350
|
+
tool_calls = message.get("tool_calls", [])
|
|
351
|
+
if tool_calls:
|
|
352
|
+
# Include assistant's textual reply if present
|
|
353
|
+
if content and content.strip() and content != "No response.":
|
|
354
|
+
transcript_lines.append(f"TARGET: {content}")
|
|
355
|
+
for tool_call in tool_calls:
|
|
356
|
+
tool_name = tool_call.get("name", "unknown_tool")
|
|
357
|
+
# Different data shapes: {"arguments": {...}} or {"args": {...}}
|
|
358
|
+
args = tool_call.get("arguments")
|
|
359
|
+
if args is None:
|
|
360
|
+
args = tool_call.get("args", {})
|
|
361
|
+
transcript_lines.append(f"TARGET: [CALLING {tool_name.upper()}] {args}")
|
|
362
|
+
else:
|
|
363
|
+
if content:
|
|
364
|
+
transcript_lines.append(f"TARGET: {content}")
|
|
365
|
+
elif msg_type == "tool":
|
|
366
|
+
# Tool responses sometimes include a function name
|
|
367
|
+
function_name = message.get("function", message.get("name", "unknown_tool"))
|
|
368
|
+
if content:
|
|
369
|
+
transcript_lines.append(f"TOOL RESPONSE ({function_name}): {content}")
|
|
370
|
+
else:
|
|
371
|
+
# Fallback
|
|
372
|
+
if content:
|
|
373
|
+
transcript_lines.append(f"{msg_type.upper()}: {content}")
|
|
374
|
+
|
|
375
|
+
else:
|
|
376
|
+
raise ValueError(
|
|
377
|
+
"Example data must be a dict with 'events'/'conversation' or a list of message objects"
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
return "\n\n".join(transcript_lines)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# --- LLM API Utility ---
|
|
384
|
+
def litellm_chat(
|
|
385
|
+
model_id: str,
|
|
386
|
+
messages: list,
|
|
387
|
+
system_prompt: Optional[str] = None,
|
|
388
|
+
max_tokens: int = 4000,
|
|
389
|
+
reasoning_effort: str = "none",
|
|
390
|
+
temperature: float = 0.0,
|
|
391
|
+
tools: Optional[List[Dict]] = None,
|
|
392
|
+
tool_choice: str = "auto",
|
|
393
|
+
**kwargs
|
|
394
|
+
):
|
|
395
|
+
"""Simplified LiteLLM chat completion call."""
|
|
396
|
+
# litellm._turn_on_debug()
|
|
397
|
+
|
|
398
|
+
# Assertion: temperature must be 1 if reasoning_effort is set
|
|
399
|
+
if reasoning_effort and reasoning_effort != "none":
|
|
400
|
+
assert temperature == 1.0, f"Temperature must be 1.0 when reasoning_effort is '{reasoning_effort}', but got {temperature}"
|
|
401
|
+
|
|
402
|
+
# Enable parameter modification and drop unsupported params
|
|
403
|
+
litellm.modify_params = True
|
|
404
|
+
#litellm.drop_params = True
|
|
405
|
+
|
|
406
|
+
# Build messages list with system prompt if provided
|
|
407
|
+
chat_messages = []
|
|
408
|
+
if system_prompt:
|
|
409
|
+
chat_messages.append({"role": "system", "content": system_prompt})
|
|
410
|
+
chat_messages.extend(messages)
|
|
411
|
+
|
|
412
|
+
# Build completion kwargs
|
|
413
|
+
# Note: For extended thinking, we use max_tokens (not max_completion_tokens)
|
|
414
|
+
# because litellm has issues translating max_completion_tokens correctly
|
|
415
|
+
completion_kwargs = {
|
|
416
|
+
"max_tokens": max_tokens,
|
|
417
|
+
**kwargs # Pass through any additional kwargs
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
# Only add temperature if it's not None (some models don't support it)
|
|
421
|
+
if temperature is not None:
|
|
422
|
+
completion_kwargs["temperature"] = temperature
|
|
423
|
+
|
|
424
|
+
# Remove reasoning_effort from kwargs if it's not supposed to be used
|
|
425
|
+
if "reasoning_effort" in completion_kwargs and reasoning_effort == "none":
|
|
426
|
+
del completion_kwargs["reasoning_effort"]
|
|
427
|
+
|
|
428
|
+
# Add reasoning effort only if explicitly set to a value other than "none" and not requiring tools
|
|
429
|
+
if (reasoning_effort != "none" and
|
|
430
|
+
tool_choice != "required" and
|
|
431
|
+
litellm.supports_reasoning(model=model_id)):
|
|
432
|
+
completion_kwargs["reasoning_effort"] = reasoning_effort
|
|
433
|
+
|
|
434
|
+
# Set allowed_openai_params for non-Anthropic models when using reasoning_effort
|
|
435
|
+
if not ("claude" in model_id.lower() or "anthropic" in model_id.lower()):
|
|
436
|
+
if "allowed_openai_params" not in completion_kwargs:
|
|
437
|
+
completion_kwargs["allowed_openai_params"] = []
|
|
438
|
+
completion_kwargs["allowed_openai_params"].append("reasoning_effort")
|
|
439
|
+
|
|
440
|
+
# Add tools if provided
|
|
441
|
+
if tools:
|
|
442
|
+
completion_kwargs["tools"] = tools
|
|
443
|
+
completion_kwargs["tool_choice"] = tool_choice
|
|
444
|
+
|
|
445
|
+
# Only set allowed_openai_params for non-Anthropic models
|
|
446
|
+
if not ("claude" in model_id.lower() or "anthropic" in model_id.lower()):
|
|
447
|
+
if "allowed_openai_params" not in completion_kwargs:
|
|
448
|
+
completion_kwargs["allowed_openai_params"] = []
|
|
449
|
+
allowed_params = completion_kwargs["allowed_openai_params"]
|
|
450
|
+
if "tools" not in allowed_params:
|
|
451
|
+
allowed_params.append("tools")
|
|
452
|
+
completion_kwargs["allowed_openai_params"] = allowed_params
|
|
453
|
+
|
|
454
|
+
response = completion_with_retries(
|
|
455
|
+
model=model_id,
|
|
456
|
+
messages=chat_messages,
|
|
457
|
+
num_retries=NUM_RETRIES,
|
|
458
|
+
retry_strategy="exponential_backoff_retry",
|
|
459
|
+
**completion_kwargs
|
|
460
|
+
)
|
|
461
|
+
return response
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def get_model_id(model_name):
|
|
465
|
+
return models[model_name]["id"]
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def model_supports_thinking(model_name):
|
|
469
|
+
"""Check if a model supports thinking capability."""
|
|
470
|
+
if model_name in models:
|
|
471
|
+
return models[model_name].get("thinking", False)
|
|
472
|
+
return False
|
|
473
|
+
|
|
474
|
+
def get_model_name_from_id(model_id):
|
|
475
|
+
"""Get the model name (key) from a model ID by looking it up in the models dictionary."""
|
|
476
|
+
for model_name, model_info in models.items():
|
|
477
|
+
if model_info["id"] == model_id:
|
|
478
|
+
return model_name
|
|
479
|
+
return None # Return None if not found
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def ensure_results_dir(example_name):
|
|
483
|
+
"""Ensure the results directory exists for the given example."""
|
|
484
|
+
if is_wandb_mode():
|
|
485
|
+
# In wandb mode, create a unique run-specific directory
|
|
486
|
+
import wandb
|
|
487
|
+
if wandb.run is not None:
|
|
488
|
+
run_id = wandb.run.id
|
|
489
|
+
results_dir = Path(f"results/transcripts/{example_name}/run_{run_id}")
|
|
490
|
+
else:
|
|
491
|
+
# Fallback if wandb.run is None
|
|
492
|
+
results_dir = Path(f"results/transcripts/{example_name}")
|
|
493
|
+
else:
|
|
494
|
+
# In regular mode, use the standard directory
|
|
495
|
+
results_dir = Path(f"results/transcripts/{example_name}")
|
|
496
|
+
|
|
497
|
+
results_dir.mkdir(parents=True, exist_ok=True)
|
|
498
|
+
return results_dir
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def get_run_id():
|
|
502
|
+
"""Get run ID for unique artifact naming."""
|
|
503
|
+
if is_wandb_mode():
|
|
504
|
+
import wandb
|
|
505
|
+
if wandb.run:
|
|
506
|
+
return wandb.run.id
|
|
507
|
+
return "local"
|
|
508
|
+
|
|
509
|
+
def get_results_dir(example_name):
|
|
510
|
+
"""Get the results directory path for the given example."""
|
|
511
|
+
if is_wandb_mode():
|
|
512
|
+
# In wandb mode, use the unique run-specific directory
|
|
513
|
+
import wandb
|
|
514
|
+
if wandb.run is not None:
|
|
515
|
+
run_id = wandb.run.id
|
|
516
|
+
return Path(f"results/transcripts/{example_name}/run_{run_id}")
|
|
517
|
+
else:
|
|
518
|
+
# Fallback if wandb.run is None
|
|
519
|
+
return Path(f"results/transcripts/{example_name}")
|
|
520
|
+
else:
|
|
521
|
+
# In regular mode, use the standard directory
|
|
522
|
+
return Path(f"results/transcripts/{example_name}")
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def load_ideation_results(example_name):
|
|
526
|
+
"""Load the ideation results for the given example (now includes variations)."""
|
|
527
|
+
results_dir = get_results_dir(example_name)
|
|
528
|
+
ideation_path = results_dir / "ideation.json"
|
|
529
|
+
with open(ideation_path, 'r') as f:
|
|
530
|
+
return json.load(f)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def load_variation_results(example_name):
|
|
534
|
+
"""
|
|
535
|
+
DEPRECATED: Load the variation results for the given example.
|
|
536
|
+
|
|
537
|
+
This function is deprecated as variation generation is now combined with ideation.
|
|
538
|
+
Use load_ideation_results() instead, which returns results with a 'variations' key.
|
|
539
|
+
|
|
540
|
+
This function is kept for backward compatibility and simply calls load_ideation_results().
|
|
541
|
+
"""
|
|
542
|
+
return load_ideation_results(example_name)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def load_understanding_results(example_name):
|
|
546
|
+
"""Load the understanding results for the given example."""
|
|
547
|
+
results_dir = get_results_dir(example_name)
|
|
548
|
+
understanding_path = results_dir / "understanding.json"
|
|
549
|
+
with open(understanding_path, 'r') as f:
|
|
550
|
+
return json.load(f)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def is_wandb_mode():
|
|
556
|
+
"""Check if we're running in wandb mode."""
|
|
557
|
+
try:
|
|
558
|
+
import wandb
|
|
559
|
+
return wandb.run is not None
|
|
560
|
+
except ImportError:
|
|
561
|
+
return False
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def save_results_locally(results, output_file, example_name):
|
|
565
|
+
"""Save results locally. In wandb mode, save temporarily for artifact logging."""
|
|
566
|
+
ensure_results_dir(example_name)
|
|
567
|
+
with open(output_file, 'w', encoding='utf-8') as f:
|
|
568
|
+
json.dump(results, f, indent=2, ensure_ascii=False)
|
|
569
|
+
|
|
570
|
+
if is_wandb_mode():
|
|
571
|
+
print(f"Results saved temporarily for artifact logging: {output_file}")
|
|
572
|
+
else:
|
|
573
|
+
print(f"Results saved to: {output_file}")
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def cleanup_temp_results(example_name):
|
|
577
|
+
"""Clean up temporary result files after artifact logging in wandb mode."""
|
|
578
|
+
if not is_wandb_mode():
|
|
579
|
+
return
|
|
580
|
+
|
|
581
|
+
import shutil
|
|
582
|
+
results_dir = get_results_dir(example_name)
|
|
583
|
+
if results_dir.exists():
|
|
584
|
+
shutil.rmtree(results_dir)
|
|
585
|
+
print(f"Cleaned up temporary results directory: {results_dir}")
|
|
586
|
+
|
|
587
|
+
def parse_message(response) -> Dict[str, Any]:
|
|
588
|
+
"""
|
|
589
|
+
Parse a LiteLLM ModelResponse object and extract key fields.
|
|
590
|
+
|
|
591
|
+
Args:
|
|
592
|
+
response: LiteLLM ModelResponse object
|
|
593
|
+
|
|
594
|
+
Returns:
|
|
595
|
+
Dict with keys: content, reasoning, tool_calls, cleaned_message
|
|
596
|
+
Missing fields will be None
|
|
597
|
+
cleaned_message: The original message dict with thinking tags removed (for message history)
|
|
598
|
+
"""
|
|
599
|
+
result = {
|
|
600
|
+
"content": None,
|
|
601
|
+
"reasoning": None,
|
|
602
|
+
"tool_calls": None,
|
|
603
|
+
"cleaned_message": None
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
# Extract content from the first choice message
|
|
607
|
+
if hasattr(response, 'choices') and response.choices:
|
|
608
|
+
choice = response.choices[0]
|
|
609
|
+
if hasattr(choice, 'message'):
|
|
610
|
+
message = choice.message
|
|
611
|
+
|
|
612
|
+
# Create a cleaned copy of the message for message history
|
|
613
|
+
cleaned_message = {}
|
|
614
|
+
if hasattr(message, 'role'):
|
|
615
|
+
cleaned_message['role'] = message.role
|
|
616
|
+
|
|
617
|
+
# Extract content
|
|
618
|
+
if hasattr(message, 'content'):
|
|
619
|
+
content = message.content
|
|
620
|
+
|
|
621
|
+
# Check if this is an Anthropic model with content array format
|
|
622
|
+
if isinstance(content, list):
|
|
623
|
+
# Handle Anthropic content array format
|
|
624
|
+
text_content = []
|
|
625
|
+
thinking_content = []
|
|
626
|
+
cleaned_content_blocks = []
|
|
627
|
+
|
|
628
|
+
for content_block in content:
|
|
629
|
+
if isinstance(content_block, dict):
|
|
630
|
+
content_type = content_block.get('type')
|
|
631
|
+
if content_type == 'text':
|
|
632
|
+
text_content.append(content_block.get('text', ''))
|
|
633
|
+
# Keep text blocks in cleaned message
|
|
634
|
+
cleaned_content_blocks.append(content_block)
|
|
635
|
+
elif content_type == 'thinking':
|
|
636
|
+
thinking_content.append(content_block.get('thinking', ''))
|
|
637
|
+
# Skip thinking blocks in cleaned message
|
|
638
|
+
|
|
639
|
+
# Join all text content blocks
|
|
640
|
+
result["content"] = '\n'.join(text_content) if text_content else None
|
|
641
|
+
# Join all thinking content blocks
|
|
642
|
+
result["reasoning"] = '\n'.join(thinking_content) if thinking_content else None
|
|
643
|
+
|
|
644
|
+
# Set cleaned content (only text blocks, no thinking)
|
|
645
|
+
cleaned_message['content'] = cleaned_content_blocks if cleaned_content_blocks else None
|
|
646
|
+
else:
|
|
647
|
+
# Handle regular string content
|
|
648
|
+
result["content"] = content
|
|
649
|
+
cleaned_content = content
|
|
650
|
+
|
|
651
|
+
# Check for XML-style thinking tags in the content
|
|
652
|
+
if isinstance(content, str) and '<thinking>' in content and '</thinking>' in content:
|
|
653
|
+
import re
|
|
654
|
+
# Extract all content between thinking tags
|
|
655
|
+
thinking_matches = re.findall(r'<thinking>(.*?)</thinking>', content, re.DOTALL)
|
|
656
|
+
if thinking_matches:
|
|
657
|
+
result["reasoning"] = '\n'.join(thinking_matches)
|
|
658
|
+
# Remove thinking tags from the content
|
|
659
|
+
result["content"] = re.sub(r'<thinking>.*?</thinking>', '', content, flags=re.DOTALL).strip()
|
|
660
|
+
cleaned_content = result["content"]
|
|
661
|
+
|
|
662
|
+
# Set cleaned content (with thinking tags removed)
|
|
663
|
+
cleaned_message['content'] = cleaned_content
|
|
664
|
+
|
|
665
|
+
# Extract reasoning_content if it exists (fallback for non-Anthropic models)
|
|
666
|
+
if hasattr(message, 'reasoning_content') and result["reasoning"] is None:
|
|
667
|
+
result["reasoning"] = message.reasoning_content
|
|
668
|
+
|
|
669
|
+
# Extract tool_calls if they exist
|
|
670
|
+
if hasattr(message, 'tool_calls') and message.tool_calls:
|
|
671
|
+
tool_calls = []
|
|
672
|
+
for tool_call in message.tool_calls:
|
|
673
|
+
if hasattr(tool_call, 'function'):
|
|
674
|
+
function = tool_call.function
|
|
675
|
+
tool_call_data = {
|
|
676
|
+
'id': getattr(tool_call, 'id', None),
|
|
677
|
+
'type': getattr(tool_call, 'type', 'function'),
|
|
678
|
+
'function': {
|
|
679
|
+
'name': getattr(function, 'name', None),
|
|
680
|
+
'arguments': getattr(function, 'arguments', None)
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
tool_calls.append(tool_call_data)
|
|
684
|
+
else:
|
|
685
|
+
# Fallback for different tool_call structure
|
|
686
|
+
tool_call_data = {
|
|
687
|
+
'id': getattr(tool_call, 'id', None),
|
|
688
|
+
'type': getattr(tool_call, 'type', 'function'),
|
|
689
|
+
'function': tool_call
|
|
690
|
+
}
|
|
691
|
+
tool_calls.append(tool_call_data)
|
|
692
|
+
|
|
693
|
+
result["tool_calls"] = tool_calls
|
|
694
|
+
# Add tool_calls to cleaned message
|
|
695
|
+
cleaned_message['tool_calls'] = message.tool_calls
|
|
696
|
+
|
|
697
|
+
# Store the cleaned message
|
|
698
|
+
result["cleaned_message"] = cleaned_message
|
|
699
|
+
|
|
700
|
+
return result
|