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,440 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
import json
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import wandb
|
|
7
|
+
# Define the project name directly here
|
|
8
|
+
PROJECT_NAME = "bloom-evals" # Replace with your actual project name
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def generate_id() -> str:
|
|
12
|
+
"""Generate a unique message ID."""
|
|
13
|
+
return str(uuid.uuid4())
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def add_transcript_event(
|
|
17
|
+
transcript_events: List[Dict[str, Any]],
|
|
18
|
+
view: List[str],
|
|
19
|
+
role: str,
|
|
20
|
+
content: Optional[str] = None,
|
|
21
|
+
reasoning: Optional[str] = None,
|
|
22
|
+
model: Optional[str] = None,
|
|
23
|
+
tool_calls: Optional[List] = None,
|
|
24
|
+
tool_call_id: Optional[str] = None,
|
|
25
|
+
error: Optional[Dict[str, Any]] = None,
|
|
26
|
+
source: Optional[str] = None
|
|
27
|
+
):
|
|
28
|
+
"""Add an event to the transcript in v3.0 format (nested with transcript_event wrapper).
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
transcript_events: List to append events to
|
|
32
|
+
view: List of views this event applies to (e.g., ["target"], ["evaluator"], ["combined"])
|
|
33
|
+
role: Message role - "system", "user", "assistant", or "tool"
|
|
34
|
+
content: Main content of the message (optional for certain cases)
|
|
35
|
+
reasoning: Reasoning/thinking content (will be added as separate content block)
|
|
36
|
+
model: Model that generated this message (for assistant messages)
|
|
37
|
+
tool_calls: List of tool calls (for assistant messages)
|
|
38
|
+
tool_call_id: ID for tool response messages
|
|
39
|
+
error: Error information for tool messages
|
|
40
|
+
source: Source of the message ("input" or "generate")
|
|
41
|
+
"""
|
|
42
|
+
message = {
|
|
43
|
+
"role": role,
|
|
44
|
+
"id": generate_id(),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Build content array based on what's provided
|
|
48
|
+
content_array = []
|
|
49
|
+
|
|
50
|
+
# Add reasoning block first if present
|
|
51
|
+
if reasoning:
|
|
52
|
+
content_array.append({
|
|
53
|
+
"type": "reasoning",
|
|
54
|
+
"reasoning": reasoning
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
# Add main text content if present
|
|
58
|
+
if content is not None:
|
|
59
|
+
content_array.append({
|
|
60
|
+
"type": "text",
|
|
61
|
+
"text": content
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
# Set content based on what we have
|
|
65
|
+
if len(content_array) == 1 and content_array[0]["type"] == "text":
|
|
66
|
+
# If only text content, use simple string format
|
|
67
|
+
message["content"] = content_array[0]["text"]
|
|
68
|
+
elif content_array:
|
|
69
|
+
# Use array format for multiple content blocks or reasoning
|
|
70
|
+
message["content"] = content_array
|
|
71
|
+
else:
|
|
72
|
+
# Default to empty string if no content
|
|
73
|
+
message["content"] = ""
|
|
74
|
+
|
|
75
|
+
# Add optional fields based on role and parameters
|
|
76
|
+
if model:
|
|
77
|
+
message["model"] = model
|
|
78
|
+
|
|
79
|
+
if source:
|
|
80
|
+
message["source"] = source
|
|
81
|
+
|
|
82
|
+
if tool_calls:
|
|
83
|
+
message["tool_calls"] = tool_calls
|
|
84
|
+
|
|
85
|
+
if role == "tool":
|
|
86
|
+
if tool_call_id:
|
|
87
|
+
message["tool_call_id"] = tool_call_id
|
|
88
|
+
else:
|
|
89
|
+
# Tool messages require tool_call_id
|
|
90
|
+
print(f"Warning: tool message missing required tool_call_id")
|
|
91
|
+
message["tool_call_id"] = generate_id()
|
|
92
|
+
|
|
93
|
+
if error:
|
|
94
|
+
message["error"] = error
|
|
95
|
+
|
|
96
|
+
event = {
|
|
97
|
+
"type": "transcript_event",
|
|
98
|
+
"id": generate_id(),
|
|
99
|
+
"timestamp": datetime.now().isoformat(),
|
|
100
|
+
"view": view,
|
|
101
|
+
"edit": {
|
|
102
|
+
"operation": "add",
|
|
103
|
+
"message": message
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
transcript_events.append(event)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def save_transcript(output_path: Path, variation_number: int, repetition_number: int, transcript_events: List[Dict[str, Any]], transcript_metadata: Optional[Dict[str, Any]] = None):
|
|
114
|
+
"""Save transcript events to a file with the specified naming pattern."""
|
|
115
|
+
# Ensure the output directory exists
|
|
116
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
|
|
118
|
+
# Get the current run name from bloom module
|
|
119
|
+
try:
|
|
120
|
+
from bloom import get_current_run_name
|
|
121
|
+
run_name = get_current_run_name()
|
|
122
|
+
if run_name:
|
|
123
|
+
# Create filename with run name: transcript_{run_name}_scenario{variation_number}-rep{repetition_number}
|
|
124
|
+
filename = f"transcript_{run_name}_scenario{variation_number}-rep{repetition_number}.json"
|
|
125
|
+
else:
|
|
126
|
+
# Fallback to old naming if no run name available
|
|
127
|
+
filename = f"transcript_{variation_number}_{repetition_number}.json"
|
|
128
|
+
except ImportError:
|
|
129
|
+
# Fallback to old naming if bloom module not available
|
|
130
|
+
filename = f"transcript_{variation_number}_{repetition_number}.json"
|
|
131
|
+
|
|
132
|
+
file_path = output_path / filename
|
|
133
|
+
|
|
134
|
+
# Create the full transcript object with metadata and events
|
|
135
|
+
if transcript_metadata:
|
|
136
|
+
transcript = get_transcript_format(transcript_metadata, transcript_events)
|
|
137
|
+
else:
|
|
138
|
+
# If no metadata provided, just save events in old format
|
|
139
|
+
transcript = transcript_events
|
|
140
|
+
|
|
141
|
+
# Save the full transcript
|
|
142
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
|
143
|
+
json.dump(transcript, f, indent=2, ensure_ascii=False)
|
|
144
|
+
|
|
145
|
+
print(f"📝 Transcript saved to: {file_path}")
|
|
146
|
+
return str(file_path)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def init_transcript_metadata(transcript_id: str, evaluator_model: str, target_model: str) -> Dict[str, Any]:
|
|
150
|
+
"""Initialize transcript metadata in the new format (v3.0)."""
|
|
151
|
+
current_time = datetime.now()
|
|
152
|
+
return {
|
|
153
|
+
"transcript_id": transcript_id,
|
|
154
|
+
"auditor_model": evaluator_model, # Map evaluator to auditor for compatibility
|
|
155
|
+
"target_model": target_model,
|
|
156
|
+
"created_at": current_time.isoformat(),
|
|
157
|
+
"updated_at": current_time.isoformat(),
|
|
158
|
+
"version": "v3.0",
|
|
159
|
+
"description": "Conversation orchestrator rollout"
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def get_transcript_format(transcript_metadata: Dict[str, Any], transcript_events: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
164
|
+
"""Get the transcript in the new format."""
|
|
165
|
+
# Update metadata timestamp
|
|
166
|
+
transcript_metadata["updated_at"] = datetime.now().isoformat()
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
"metadata": transcript_metadata,
|
|
170
|
+
"events": transcript_events
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def load_transcript(transcript_path: Path) -> Dict[str, Any]:
|
|
175
|
+
"""Load a transcript from a file."""
|
|
176
|
+
with open(transcript_path, 'r', encoding='utf-8') as f:
|
|
177
|
+
data = json.load(f)
|
|
178
|
+
# Handle both old format (just events array) and new format (with metadata)
|
|
179
|
+
if isinstance(data, list):
|
|
180
|
+
# Old format - just events
|
|
181
|
+
return {"events": data}
|
|
182
|
+
return data
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def append_judge_output_to_transcript(transcript_path: Path, judge_output: Dict[str, Any]):
|
|
186
|
+
"""Append judge output to transcript metadata and save."""
|
|
187
|
+
# Load existing transcript
|
|
188
|
+
transcript = load_transcript(transcript_path)
|
|
189
|
+
|
|
190
|
+
# Initialize metadata if not present
|
|
191
|
+
if "metadata" not in transcript:
|
|
192
|
+
transcript["metadata"] = {}
|
|
193
|
+
|
|
194
|
+
# Add judge output to metadata
|
|
195
|
+
transcript["metadata"]["judge_output"] = judge_output
|
|
196
|
+
transcript["metadata"]["updated_at"] = datetime.now().isoformat()
|
|
197
|
+
|
|
198
|
+
# Save updated transcript
|
|
199
|
+
with open(transcript_path, 'w', encoding='utf-8') as f:
|
|
200
|
+
json.dump(transcript, f, indent=2, ensure_ascii=False)
|
|
201
|
+
|
|
202
|
+
print(f"📝 Judge output appended to: {transcript_path}")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def format_transcript_for_judge(transcript_events: List[Dict[str, Any]], redaction_tags: Optional[str] = None) -> str:
|
|
206
|
+
"""Format transcript events for judge, showing the full conversation from the target's view.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
transcript_events: List of transcript events
|
|
210
|
+
redaction_tags: Optional tag name for redacting content. Content between <TAG>...</TAG> will be removed.
|
|
211
|
+
"""
|
|
212
|
+
formatted_lines = []
|
|
213
|
+
|
|
214
|
+
# First, find the target system prompt (it's in the combined or target view)
|
|
215
|
+
system_prompt = None
|
|
216
|
+
for event in transcript_events:
|
|
217
|
+
view = event.get('view', [])
|
|
218
|
+
message = event.get('edit', {}).get('message', {})
|
|
219
|
+
|
|
220
|
+
# Check for both 'role' (new v3.0 format) and 'type' (old format)
|
|
221
|
+
msg_role = message.get('role') or message.get('type', '')
|
|
222
|
+
|
|
223
|
+
if ('target' in view and msg_role == 'system'):
|
|
224
|
+
content = message.get('content', '')
|
|
225
|
+
# Handle content that might be array of content blocks
|
|
226
|
+
if isinstance(content, list):
|
|
227
|
+
text_parts = []
|
|
228
|
+
for block in content:
|
|
229
|
+
if block.get('type') == 'text':
|
|
230
|
+
text_parts.append(block.get('text', ''))
|
|
231
|
+
system_prompt = ' '.join(text_parts)
|
|
232
|
+
else:
|
|
233
|
+
system_prompt = content
|
|
234
|
+
break
|
|
235
|
+
|
|
236
|
+
# Add system prompt at the beginning if found
|
|
237
|
+
if system_prompt:
|
|
238
|
+
formatted_lines.append("=== SYSTEM PROMPT ===")
|
|
239
|
+
formatted_lines.append(system_prompt)
|
|
240
|
+
formatted_lines.append("")
|
|
241
|
+
formatted_lines.append("=== CONVERSATION ===")
|
|
242
|
+
formatted_lines.append("")
|
|
243
|
+
|
|
244
|
+
# Filter events that have 'target' in their view (what the target sees)
|
|
245
|
+
target_events = [
|
|
246
|
+
event for event in transcript_events
|
|
247
|
+
if 'target' in event.get('view', [])
|
|
248
|
+
]
|
|
249
|
+
|
|
250
|
+
# Sort by timestamp to ensure chronological order
|
|
251
|
+
target_events.sort(key=lambda x: x.get('timestamp', ''))
|
|
252
|
+
|
|
253
|
+
for event in target_events:
|
|
254
|
+
message = event.get('edit', {}).get('message', {})
|
|
255
|
+
# Support both 'role' (v3.0) and 'type' (old format)
|
|
256
|
+
role = message.get('role') or message.get('type', '')
|
|
257
|
+
content = message.get('content', '')
|
|
258
|
+
|
|
259
|
+
# Handle content that might be array of content blocks
|
|
260
|
+
reasoning_content = None
|
|
261
|
+
if isinstance(content, list):
|
|
262
|
+
# Extract text and reasoning from content blocks
|
|
263
|
+
text_parts = []
|
|
264
|
+
for block in content:
|
|
265
|
+
if block.get('type') == 'text':
|
|
266
|
+
text_parts.append(block.get('text', ''))
|
|
267
|
+
elif block.get('type') == 'reasoning':
|
|
268
|
+
# Only extract reasoning from assistant (target model), not from user (evaluator)
|
|
269
|
+
if role == 'assistant':
|
|
270
|
+
reasoning_content = block.get('reasoning', '')
|
|
271
|
+
content = ' '.join(text_parts)
|
|
272
|
+
|
|
273
|
+
# Skip empty content
|
|
274
|
+
if not content or (isinstance(content, str) and not content.strip()):
|
|
275
|
+
# But don't skip if there's reasoning to show
|
|
276
|
+
if not reasoning_content:
|
|
277
|
+
continue
|
|
278
|
+
|
|
279
|
+
# Skip system messages (already handled above)
|
|
280
|
+
if role == 'system':
|
|
281
|
+
continue
|
|
282
|
+
|
|
283
|
+
# Format based on role - using consistent format for all transcript types
|
|
284
|
+
if role == 'user':
|
|
285
|
+
formatted_lines.append(f"USER (EVALUATOR): {content}")
|
|
286
|
+
elif role == 'assistant':
|
|
287
|
+
# Show target's thinking first if present
|
|
288
|
+
if reasoning_content and reasoning_content.strip():
|
|
289
|
+
formatted_lines.append(f"TARGET THINKING: {reasoning_content}")
|
|
290
|
+
formatted_lines.append("") # Empty line for readability
|
|
291
|
+
|
|
292
|
+
# Check if this has tool calls (check both message level and event level for compatibility)
|
|
293
|
+
tool_calls = message.get('tool_calls', []) or event.get('tool_calls', [])
|
|
294
|
+
if tool_calls:
|
|
295
|
+
# Show the target's response content first if present
|
|
296
|
+
if content and content.strip() and content != "No response.":
|
|
297
|
+
formatted_lines.append(f"ASSISTANT: {content}")
|
|
298
|
+
|
|
299
|
+
# Show each tool call
|
|
300
|
+
for tool_call in tool_calls:
|
|
301
|
+
# Handle both formats: {"name": "...", "args": {...}} and {"function": "...", "arguments": {...}}
|
|
302
|
+
tool_name = tool_call.get('name') or tool_call.get('function', 'unknown_tool')
|
|
303
|
+
tool_args = tool_call.get('args') or tool_call.get('arguments', {})
|
|
304
|
+
# Convert args dict to JSON string for display
|
|
305
|
+
if isinstance(tool_args, dict):
|
|
306
|
+
tool_args_str = json.dumps(tool_args, ensure_ascii=False)
|
|
307
|
+
else:
|
|
308
|
+
tool_args_str = str(tool_args)
|
|
309
|
+
formatted_lines.append(f"ASSISTANT: [CALLING {tool_name}] {tool_args_str}")
|
|
310
|
+
else:
|
|
311
|
+
# Regular response without tool calls
|
|
312
|
+
if content and content.strip():
|
|
313
|
+
formatted_lines.append(f"ASSISTANT: {content}")
|
|
314
|
+
elif role == 'tool':
|
|
315
|
+
# Tool response - extract tool name from tool_call_id if available
|
|
316
|
+
tool_call_id = message.get('tool_call_id', '')
|
|
317
|
+
if tool_call_id and '_' in tool_call_id:
|
|
318
|
+
# Extract tool name from tool_call_id (e.g., "tool_0_document_analyzer" -> "document_analyzer")
|
|
319
|
+
tool_name = tool_call_id.split('_', 2)[-1] if tool_call_id.count('_') >= 2 else tool_call_id
|
|
320
|
+
else:
|
|
321
|
+
tool_name = message.get('name', 'unknown_tool')
|
|
322
|
+
formatted_lines.append(f"(EVALUATOR-SIMULATED) TOOL RESPONSE [{tool_name}]: {content}")
|
|
323
|
+
|
|
324
|
+
formatted_lines.append("") # Empty line for readability
|
|
325
|
+
|
|
326
|
+
# Join the formatted lines
|
|
327
|
+
formatted_transcript = "\n".join(formatted_lines).strip()
|
|
328
|
+
|
|
329
|
+
# Apply redaction if tags are specified
|
|
330
|
+
if redaction_tags:
|
|
331
|
+
import re
|
|
332
|
+
# Pattern to match <TAG>...</TAG> (opening and closing with same tag name)
|
|
333
|
+
pattern = rf'<{re.escape(redaction_tags)}>.*?</{re.escape(redaction_tags)}>'
|
|
334
|
+
formatted_transcript = re.sub(pattern, '', formatted_transcript, flags=re.DOTALL)
|
|
335
|
+
|
|
336
|
+
return formatted_transcript
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def download_artifact_file(run_id: str, artifact_name: str, output_dir: Path) -> None:
|
|
340
|
+
"""Download a specific artifact file for a given run ID to the specified output directory."""
|
|
341
|
+
api = wandb.Api()
|
|
342
|
+
run = api.run(f"{PROJECT_NAME}/{run_id}")
|
|
343
|
+
artifact = run.use_artifact(artifact_name)
|
|
344
|
+
artifact_dir = artifact.download(root=output_dir)
|
|
345
|
+
print(f"Artifact {artifact_name} downloaded to {artifact_dir}")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def retrieve_artifacts_for_resume(resume_run_id: str, resume_stage: str, example_name: str) -> Dict[str, Any]:
|
|
349
|
+
"""
|
|
350
|
+
Retrieve and download artifacts from a previous run for resuming.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
resume_run_id: WandB run ID to resume from
|
|
354
|
+
resume_stage: Stage to resume from ("ideation", "variation", "rollout", "judgment")
|
|
355
|
+
example_name: Example name for organizing results
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
Dictionary with paths to downloaded artifacts
|
|
359
|
+
"""
|
|
360
|
+
if not wandb:
|
|
361
|
+
raise ImportError("wandb is required for resume functionality")
|
|
362
|
+
|
|
363
|
+
# Map stages to their dependencies (stages that need to be completed before this stage)
|
|
364
|
+
stage_dependencies = {
|
|
365
|
+
"ideation": ["understanding"],
|
|
366
|
+
"rollout": ["understanding", "ideation"],
|
|
367
|
+
"judgment": ["understanding", "ideation", "rollout"]
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if resume_stage not in stage_dependencies:
|
|
371
|
+
raise ValueError(f"Invalid resume_stage: {resume_stage}. Must be one of: {list(stage_dependencies.keys())}")
|
|
372
|
+
|
|
373
|
+
# Get list of stages we need to retrieve artifacts for
|
|
374
|
+
required_stages = stage_dependencies[resume_stage]
|
|
375
|
+
|
|
376
|
+
print(f"🔄 Resuming from run {resume_run_id} at stage '{resume_stage}'")
|
|
377
|
+
print(f"📥 Need to retrieve artifacts from stages: {required_stages}")
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
api = wandb.Api()
|
|
381
|
+
run = api.run(f"{PROJECT_NAME}/{resume_run_id}")
|
|
382
|
+
|
|
383
|
+
# Get the results directory where we'll place the artifacts
|
|
384
|
+
from utils import get_results_dir
|
|
385
|
+
results_dir = get_results_dir(example_name)
|
|
386
|
+
results_dir.mkdir(parents=True, exist_ok=True)
|
|
387
|
+
|
|
388
|
+
retrieved_artifacts = {}
|
|
389
|
+
|
|
390
|
+
# Look for artifacts from the run
|
|
391
|
+
artifacts = run.logged_artifacts()
|
|
392
|
+
|
|
393
|
+
for artifact in artifacts:
|
|
394
|
+
artifact_name = artifact.name
|
|
395
|
+
artifact_type = artifact.type
|
|
396
|
+
|
|
397
|
+
print(f"📦 Found artifact: {artifact_name} (type: {artifact_type})")
|
|
398
|
+
|
|
399
|
+
# Download rollout results artifacts which contain all stage outputs
|
|
400
|
+
if artifact_type == "rollout_results":
|
|
401
|
+
print(f"📥 Downloading rollout results artifact: {artifact_name}")
|
|
402
|
+
artifact_dir = artifact.download(root=results_dir / "downloaded_artifacts")
|
|
403
|
+
|
|
404
|
+
# Copy the required stage files to the main results directory
|
|
405
|
+
artifact_path = Path(artifact_dir)
|
|
406
|
+
|
|
407
|
+
for stage in required_stages:
|
|
408
|
+
stage_file = artifact_path / f"{stage}.json"
|
|
409
|
+
if stage_file.exists():
|
|
410
|
+
import shutil
|
|
411
|
+
dest_file = results_dir / f"{stage}.json"
|
|
412
|
+
shutil.copy2(stage_file, dest_file)
|
|
413
|
+
retrieved_artifacts[stage] = str(dest_file)
|
|
414
|
+
print(f"✅ Retrieved {stage}.json")
|
|
415
|
+
else:
|
|
416
|
+
print(f"⚠️ {stage}.json not found in artifact")
|
|
417
|
+
|
|
418
|
+
# Also copy any transcript files if we're resuming from rollout
|
|
419
|
+
if resume_stage == "judgment":
|
|
420
|
+
transcript_files = list(artifact_path.glob("transcript_*.json"))
|
|
421
|
+
for transcript_file in transcript_files:
|
|
422
|
+
import shutil
|
|
423
|
+
dest_file = results_dir / transcript_file.name
|
|
424
|
+
shutil.copy2(transcript_file, dest_file)
|
|
425
|
+
print(f"✅ Retrieved {transcript_file.name}")
|
|
426
|
+
|
|
427
|
+
if transcript_files:
|
|
428
|
+
retrieved_artifacts["transcripts"] = [str(results_dir / f.name) for f in transcript_files]
|
|
429
|
+
|
|
430
|
+
# Verify we got all required artifacts
|
|
431
|
+
missing_stages = [stage for stage in required_stages if stage not in retrieved_artifacts]
|
|
432
|
+
if missing_stages:
|
|
433
|
+
raise RuntimeError(f"Could not retrieve artifacts for stages: {missing_stages}")
|
|
434
|
+
|
|
435
|
+
print(f"✅ Successfully retrieved artifacts for resume from stage '{resume_stage}'")
|
|
436
|
+
return retrieved_artifacts
|
|
437
|
+
|
|
438
|
+
except Exception as e:
|
|
439
|
+
print(f"❌ Failed to retrieve artifacts from run {resume_run_id}: {e}")
|
|
440
|
+
raise
|