weco 0.3.6__py3-none-any.whl → 0.3.8__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.
- weco/api.py +125 -0
- weco/browser.py +29 -0
- weco/cli.py +49 -7
- weco/constants.py +1 -1
- weco/optimizer.py +510 -817
- weco/ui.py +315 -0
- weco/validation.py +112 -0
- {weco-0.3.6.dist-info → weco-0.3.8.dist-info}/METADATA +2 -2
- weco-0.3.8.dist-info/RECORD +18 -0
- {weco-0.3.6.dist-info → weco-0.3.8.dist-info}/WHEEL +1 -1
- weco-0.3.6.dist-info/RECORD +0 -15
- {weco-0.3.6.dist-info → weco-0.3.8.dist-info}/entry_points.txt +0 -0
- {weco-0.3.6.dist-info → weco-0.3.8.dist-info}/licenses/LICENSE +0 -0
- {weco-0.3.6.dist-info → weco-0.3.8.dist-info}/top_level.txt +0 -0
weco/optimizer.py
CHANGED
|
@@ -1,36 +1,43 @@
|
|
|
1
|
-
import
|
|
1
|
+
import json
|
|
2
2
|
import math
|
|
3
|
-
import
|
|
4
|
-
import threading
|
|
5
|
-
import signal
|
|
3
|
+
import pathlib
|
|
6
4
|
import sys
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
7
|
import traceback
|
|
8
|
-
import
|
|
8
|
+
from dataclasses import dataclass
|
|
9
9
|
from datetime import datetime
|
|
10
10
|
from typing import Optional
|
|
11
|
+
|
|
11
12
|
from rich.console import Console
|
|
12
|
-
from rich.live import Live
|
|
13
|
-
from rich.panel import Panel
|
|
14
13
|
from rich.prompt import Confirm
|
|
14
|
+
|
|
15
|
+
from . import __dashboard_url__
|
|
15
16
|
from .api import (
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
claim_execution_task,
|
|
18
|
+
get_execution_tasks,
|
|
18
19
|
get_optimization_run_status,
|
|
19
|
-
send_heartbeat,
|
|
20
20
|
report_termination,
|
|
21
21
|
resume_optimization_run,
|
|
22
|
+
send_heartbeat,
|
|
23
|
+
start_optimization_run,
|
|
24
|
+
submit_execution_result,
|
|
22
25
|
)
|
|
23
26
|
from .auth import handle_authentication
|
|
24
|
-
from .
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
from .browser import open_browser
|
|
28
|
+
from .ui import OptimizationUI, LiveOptimizationUI
|
|
29
|
+
from .utils import read_additional_instructions, read_from_path, write_to_path, run_evaluation_with_file_swap
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class OptimizationResult:
|
|
34
|
+
"""Result from a queue-based optimization loop."""
|
|
35
|
+
|
|
36
|
+
success: bool
|
|
37
|
+
final_step: int
|
|
38
|
+
status: str # "completed", "terminated", "error"
|
|
39
|
+
reason: str # e.g. "completed_successfully", "user_terminated_sigint"
|
|
40
|
+
details: Optional[str] = None
|
|
34
41
|
|
|
35
42
|
|
|
36
43
|
def save_execution_output(runs_dir: pathlib.Path, step: int, output: str) -> None:
|
|
@@ -92,867 +99,553 @@ class HeartbeatSender(threading.Thread):
|
|
|
92
99
|
# The loop will break due to the exception, and thread will terminate via finally.
|
|
93
100
|
|
|
94
101
|
|
|
95
|
-
def
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
code=status_response["best_result"]["code"],
|
|
102
|
-
metric=status_response["best_result"]["metric_value"],
|
|
103
|
-
is_buggy=status_response["best_result"]["is_buggy"],
|
|
104
|
-
)
|
|
105
|
-
return None
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def get_node_from_status(status_response: dict, solution_id: str) -> Node:
|
|
109
|
-
"""Find the node with the given solution_id from a status response; raise if not found."""
|
|
110
|
-
nodes = status_response.get("nodes") or []
|
|
111
|
-
for node_data in nodes:
|
|
112
|
-
if node_data.get("solution_id") == solution_id:
|
|
113
|
-
return Node(
|
|
114
|
-
id=node_data["solution_id"],
|
|
115
|
-
parent_id=node_data["parent_id"],
|
|
116
|
-
code=node_data["code"],
|
|
117
|
-
metric=node_data["metric_value"],
|
|
118
|
-
is_buggy=node_data["is_buggy"],
|
|
119
|
-
)
|
|
120
|
-
raise ValueError(
|
|
121
|
-
"Current solution node not found in the optimization status response. This may indicate a synchronization issue with the backend."
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def execute_optimization(
|
|
126
|
-
source: str,
|
|
102
|
+
def _run_optimization_loop(
|
|
103
|
+
ui: OptimizationUI,
|
|
104
|
+
run_id: str,
|
|
105
|
+
auth_headers: dict,
|
|
106
|
+
source_fp: pathlib.Path,
|
|
107
|
+
source_code: str,
|
|
127
108
|
eval_command: str,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
save_logs: bool = False,
|
|
137
|
-
apply_change: bool = False,
|
|
138
|
-
api_keys: Optional[dict[str, str]] = None,
|
|
139
|
-
) -> bool:
|
|
109
|
+
eval_timeout: Optional[int],
|
|
110
|
+
runs_dir: pathlib.Path,
|
|
111
|
+
save_logs: bool,
|
|
112
|
+
start_step: int = 0,
|
|
113
|
+
poll_interval: float = 2.0,
|
|
114
|
+
max_poll_attempts: int = 300,
|
|
115
|
+
api_keys: Optional[dict] = None,
|
|
116
|
+
) -> OptimizationResult:
|
|
140
117
|
"""
|
|
141
|
-
|
|
118
|
+
Shared queue-based execution loop for optimize and resume.
|
|
119
|
+
|
|
120
|
+
Polls for execution tasks, executes locally, and submits results.
|
|
121
|
+
This function handles the core optimization loop and returns a result
|
|
122
|
+
object describing the outcome.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
ui: UI handler for displaying progress and events.
|
|
126
|
+
run_id: The optimization run ID.
|
|
127
|
+
auth_headers: Authentication headers.
|
|
128
|
+
source_fp: Path to the source file.
|
|
129
|
+
source_code: Original source code content.
|
|
130
|
+
eval_command: Evaluation command to run.
|
|
131
|
+
eval_timeout: Timeout for evaluation in seconds.
|
|
132
|
+
runs_dir: Directory for logs.
|
|
133
|
+
save_logs: Whether to save execution logs.
|
|
134
|
+
start_step: Initial step number (0 for new runs, current_step for resume).
|
|
135
|
+
poll_interval: Seconds between polling attempts.
|
|
136
|
+
max_poll_attempts: Max polls before timeout (~10 min with 2s interval).
|
|
137
|
+
api_keys: Optional API keys for LLM providers.
|
|
142
138
|
|
|
143
139
|
Returns:
|
|
144
|
-
|
|
140
|
+
OptimizationResult with success status and termination info.
|
|
145
141
|
"""
|
|
146
|
-
|
|
147
|
-
console = Console()
|
|
148
|
-
# Global variables for this optimization run
|
|
149
|
-
heartbeat_thread = None
|
|
150
|
-
stop_heartbeat_event = threading.Event()
|
|
151
|
-
current_run_id_for_heartbeat = None
|
|
152
|
-
current_auth_headers_for_heartbeat = {}
|
|
153
|
-
live_ref = None # Reference to the Live object for the optimization run
|
|
142
|
+
step = start_step
|
|
154
143
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
144
|
+
try:
|
|
145
|
+
while True:
|
|
146
|
+
# Check if run has been stopped via dashboard
|
|
147
|
+
try:
|
|
148
|
+
status_response = get_optimization_run_status(
|
|
149
|
+
console=None, run_id=run_id, include_history=False, auth_headers=auth_headers
|
|
150
|
+
)
|
|
151
|
+
if status_response.get("status") == "stopping":
|
|
152
|
+
ui.on_stop_requested()
|
|
153
|
+
return OptimizationResult(
|
|
154
|
+
success=False,
|
|
155
|
+
final_step=step,
|
|
156
|
+
status="terminated",
|
|
157
|
+
reason="user_requested_stop",
|
|
158
|
+
details="Run stopped by user request via dashboard.",
|
|
159
|
+
)
|
|
160
|
+
except Exception as e:
|
|
161
|
+
ui.on_warning(f"Unable to check run status: {e}")
|
|
162
|
+
|
|
163
|
+
# Poll for ready tasks
|
|
164
|
+
ui.on_polling(step)
|
|
165
|
+
tasks_result = None
|
|
166
|
+
poll_attempts = 0
|
|
167
|
+
|
|
168
|
+
while not tasks_result or not tasks_result.tasks:
|
|
169
|
+
tasks_result = get_execution_tasks(run_id, auth_headers)
|
|
170
|
+
|
|
171
|
+
# Check if run was stopped (from run summary in response)
|
|
172
|
+
if tasks_result and tasks_result.run:
|
|
173
|
+
run_status = tasks_result.run.status
|
|
174
|
+
if run_status in ("stopping", "stopped", "terminated", "error", "completed"):
|
|
175
|
+
ui.on_stop_requested()
|
|
176
|
+
return OptimizationResult(
|
|
177
|
+
success=False,
|
|
178
|
+
final_step=step,
|
|
179
|
+
status="terminated",
|
|
180
|
+
reason="user_requested_stop",
|
|
181
|
+
details=f"Run status is '{run_status}'.",
|
|
182
|
+
)
|
|
161
183
|
|
|
162
|
-
|
|
163
|
-
|
|
184
|
+
if not tasks_result or not tasks_result.tasks:
|
|
185
|
+
poll_attempts += 1
|
|
186
|
+
if poll_attempts >= max_poll_attempts:
|
|
187
|
+
ui.on_error("Timeout waiting for execution tasks")
|
|
188
|
+
return OptimizationResult(
|
|
189
|
+
success=False,
|
|
190
|
+
final_step=step,
|
|
191
|
+
status="error",
|
|
192
|
+
reason="timeout_waiting_for_tasks",
|
|
193
|
+
details="Timeout waiting for execution tasks",
|
|
194
|
+
)
|
|
195
|
+
time.sleep(poll_interval)
|
|
164
196
|
|
|
165
|
-
|
|
166
|
-
|
|
197
|
+
task = tasks_result.tasks[0]
|
|
198
|
+
task_id = task["id"]
|
|
167
199
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
# Report termination (best effort)
|
|
174
|
-
if current_run_id_for_heartbeat:
|
|
175
|
-
report_termination(
|
|
176
|
-
run_id=current_run_id_for_heartbeat,
|
|
177
|
-
status_update="terminated",
|
|
178
|
-
reason=f"user_terminated_{signal_name.lower()}",
|
|
179
|
-
details=f"Process terminated by signal {signal_name} ({signum}).",
|
|
180
|
-
auth_headers=current_auth_headers_for_heartbeat,
|
|
181
|
-
)
|
|
182
|
-
console.print(f"[cyan]To resume this run, use:[/] [bold cyan]weco resume {current_run_id_for_heartbeat}[/]\n")
|
|
200
|
+
# Claim the task
|
|
201
|
+
claimed = claim_execution_task(task_id, auth_headers)
|
|
202
|
+
if claimed is None:
|
|
203
|
+
ui.on_warning(f"Task {task_id} already claimed, retrying...")
|
|
204
|
+
continue
|
|
183
205
|
|
|
184
|
-
|
|
185
|
-
|
|
206
|
+
code = claimed["revision"]["code"]
|
|
207
|
+
plan = claimed["revision"]["plan"]
|
|
186
208
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
original_sigterm_handler = signal.signal(signal.SIGTERM, signal_handler)
|
|
209
|
+
ui.on_executing(step)
|
|
210
|
+
ui.on_task_claimed(task_id, plan)
|
|
190
211
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
user_stop_requested_flag = False
|
|
212
|
+
# Save code to log
|
|
213
|
+
write_to_path(fp=runs_dir / f"step_{step}{source_fp.suffix}", content=code)
|
|
194
214
|
|
|
195
|
-
|
|
196
|
-
# --- Login/Authentication Handling (now mandatory) ---
|
|
197
|
-
weco_api_key, auth_headers = handle_authentication(console)
|
|
198
|
-
if weco_api_key is None:
|
|
199
|
-
# Authentication failed or user declined
|
|
200
|
-
return False
|
|
201
|
-
|
|
202
|
-
current_auth_headers_for_heartbeat = auth_headers
|
|
203
|
-
|
|
204
|
-
# --- Process Parameters ---
|
|
205
|
-
maximize = goal.lower() in ["maximize", "max"]
|
|
206
|
-
|
|
207
|
-
code_generator_config = {"model": model}
|
|
208
|
-
evaluator_config = {"model": model, "include_analysis": True}
|
|
209
|
-
search_policy_config = {
|
|
210
|
-
"num_drafts": max(1, math.ceil(0.15 * steps)),
|
|
211
|
-
"debug_prob": 0.5,
|
|
212
|
-
"max_debug_depth": max(1, math.ceil(0.1 * steps)),
|
|
213
|
-
}
|
|
214
|
-
processed_additional_instructions = read_additional_instructions(additional_instructions=additional_instructions)
|
|
215
|
-
source_fp = pathlib.Path(source)
|
|
216
|
-
source_code = read_from_path(fp=source_fp, is_json=False)
|
|
217
|
-
original_source_code = source_code
|
|
218
|
-
|
|
219
|
-
# --- Panel Initialization ---
|
|
220
|
-
summary_panel = SummaryPanel(maximize=maximize, metric_name=metric, total_steps=steps, model=model, runs_dir=log_dir)
|
|
221
|
-
solution_panels = SolutionPanels(metric_name=metric, source_fp=source_fp)
|
|
222
|
-
eval_output_panel = EvaluationOutputPanel()
|
|
223
|
-
tree_panel = MetricTreePanel(maximize=maximize)
|
|
224
|
-
layout = create_optimization_layout()
|
|
225
|
-
end_optimization_layout = create_end_optimization_layout()
|
|
226
|
-
|
|
227
|
-
# --- Start Optimization Run ---
|
|
228
|
-
run_response = start_optimization_run(
|
|
229
|
-
console=console,
|
|
230
|
-
source_code=source_code,
|
|
231
|
-
source_path=str(source_fp),
|
|
232
|
-
evaluation_command=eval_command,
|
|
233
|
-
metric_name=metric,
|
|
234
|
-
maximize=maximize,
|
|
235
|
-
steps=steps,
|
|
236
|
-
code_generator_config=code_generator_config,
|
|
237
|
-
evaluator_config=evaluator_config,
|
|
238
|
-
search_policy_config=search_policy_config,
|
|
239
|
-
additional_instructions=processed_additional_instructions,
|
|
240
|
-
eval_timeout=eval_timeout,
|
|
241
|
-
save_logs=save_logs,
|
|
242
|
-
log_dir=log_dir,
|
|
243
|
-
auth_headers=auth_headers,
|
|
244
|
-
api_keys=api_keys,
|
|
245
|
-
)
|
|
246
|
-
# Indicate the endpoint failed to return a response and the optimization was unsuccessful
|
|
247
|
-
if run_response is None:
|
|
248
|
-
return False
|
|
249
|
-
|
|
250
|
-
run_id = run_response["run_id"]
|
|
251
|
-
run_name = run_response["run_name"]
|
|
252
|
-
current_run_id_for_heartbeat = run_id
|
|
253
|
-
|
|
254
|
-
# --- Start Heartbeat Thread ---
|
|
255
|
-
stop_heartbeat_event.clear()
|
|
256
|
-
heartbeat_thread = HeartbeatSender(run_id, auth_headers, stop_heartbeat_event)
|
|
257
|
-
heartbeat_thread.start()
|
|
258
|
-
|
|
259
|
-
# --- Live Update Loop ---
|
|
260
|
-
refresh_rate = 4
|
|
261
|
-
with Live(layout, refresh_per_second=refresh_rate) as live:
|
|
262
|
-
live_ref = live
|
|
263
|
-
# Define the runs directory (.runs/<run-id>) to store logs and results
|
|
264
|
-
runs_dir = pathlib.Path(log_dir) / run_id
|
|
265
|
-
runs_dir.mkdir(parents=True, exist_ok=True)
|
|
266
|
-
|
|
267
|
-
# Initialize logging structure if save_logs is enabled
|
|
268
|
-
if save_logs:
|
|
269
|
-
# Initialize JSONL index with metadata
|
|
270
|
-
jsonl_file = runs_dir / "exec_output.jsonl"
|
|
271
|
-
metadata = {
|
|
272
|
-
"type": "metadata",
|
|
273
|
-
"run_id": run_id,
|
|
274
|
-
"run_name": run_name,
|
|
275
|
-
"started": datetime.now().isoformat(),
|
|
276
|
-
"eval_command": eval_command,
|
|
277
|
-
"metric": metric,
|
|
278
|
-
"goal": "maximize" if maximize else "minimize",
|
|
279
|
-
"total_steps": steps,
|
|
280
|
-
}
|
|
281
|
-
with open(jsonl_file, "w", encoding="utf-8") as f:
|
|
282
|
-
f.write(json.dumps(metadata) + "\n")
|
|
283
|
-
|
|
284
|
-
# Update the panels with the initial solution
|
|
285
|
-
# Add run id and run name now that we have it
|
|
286
|
-
summary_panel.set_run_id(run_id=run_id)
|
|
287
|
-
summary_panel.set_run_name(run_name=run_name)
|
|
288
|
-
# Set the step of the progress bar
|
|
289
|
-
summary_panel.set_step(step=0)
|
|
290
|
-
summary_panel.update_thinking(thinking=run_response["plan"])
|
|
291
|
-
# Build the metric tree
|
|
292
|
-
tree_panel.build_metric_tree(
|
|
293
|
-
nodes=[
|
|
294
|
-
{
|
|
295
|
-
"solution_id": run_response["solution_id"],
|
|
296
|
-
"parent_id": None,
|
|
297
|
-
"code": run_response["code"],
|
|
298
|
-
"step": 0,
|
|
299
|
-
"metric_value": None,
|
|
300
|
-
"is_buggy": None,
|
|
301
|
-
}
|
|
302
|
-
]
|
|
303
|
-
)
|
|
304
|
-
# Set the current solution as unevaluated since we haven't run the evaluation function and fed it back to the model yet
|
|
305
|
-
tree_panel.set_unevaluated_node(node_id=run_response["solution_id"])
|
|
306
|
-
# Update the solution panels with the initial solution and get the panel displays
|
|
307
|
-
solution_panels.update(
|
|
308
|
-
current_node=Node(
|
|
309
|
-
id=run_response["solution_id"], parent_id=None, code=run_response["code"], metric=None, is_buggy=None
|
|
310
|
-
),
|
|
311
|
-
best_node=None,
|
|
312
|
-
)
|
|
313
|
-
current_solution_panel, best_solution_panel = solution_panels.get_display(current_step=0)
|
|
314
|
-
|
|
315
|
-
# Update the live layout with the initial solution panels
|
|
316
|
-
smooth_update(
|
|
317
|
-
live=live,
|
|
318
|
-
layout=layout,
|
|
319
|
-
sections_to_update=[
|
|
320
|
-
("summary", summary_panel.get_display()),
|
|
321
|
-
("tree", tree_panel.get_display(is_done=False)),
|
|
322
|
-
("current_solution", current_solution_panel),
|
|
323
|
-
("best_solution", best_solution_panel),
|
|
324
|
-
("eval_output", eval_output_panel.get_display()),
|
|
325
|
-
],
|
|
326
|
-
transition_delay=0.1,
|
|
327
|
-
)
|
|
328
|
-
|
|
329
|
-
# Write the initial code string to the logs
|
|
330
|
-
write_to_path(fp=runs_dir / f"step_0{source_fp.suffix}", content=run_response["code"])
|
|
331
|
-
# Run evaluation on the initial solution (file swap ensures original is restored)
|
|
215
|
+
# Execute locally
|
|
332
216
|
term_out = run_evaluation_with_file_swap(
|
|
333
217
|
file_path=source_fp,
|
|
334
|
-
new_content=
|
|
218
|
+
new_content=code,
|
|
335
219
|
original_content=source_code,
|
|
336
220
|
eval_command=eval_command,
|
|
337
221
|
timeout=eval_timeout,
|
|
338
222
|
)
|
|
339
223
|
|
|
340
|
-
# Save logs if requested
|
|
341
224
|
if save_logs:
|
|
342
|
-
save_execution_output(runs_dir, step=
|
|
343
|
-
# Update the evaluation output panel
|
|
344
|
-
eval_output_panel.update(output=term_out)
|
|
345
|
-
smooth_update(
|
|
346
|
-
live=live,
|
|
347
|
-
layout=layout,
|
|
348
|
-
sections_to_update=[("eval_output", eval_output_panel.get_display())],
|
|
349
|
-
transition_delay=0.1,
|
|
350
|
-
)
|
|
225
|
+
save_execution_output(runs_dir, step=step, output=term_out)
|
|
351
226
|
|
|
352
|
-
|
|
353
|
-
for step in range(1, steps + 1):
|
|
354
|
-
if run_id:
|
|
355
|
-
try:
|
|
356
|
-
current_status_response = get_optimization_run_status(
|
|
357
|
-
console=console, run_id=run_id, include_history=False, auth_headers=auth_headers
|
|
358
|
-
)
|
|
359
|
-
current_run_status_val = current_status_response.get("status")
|
|
360
|
-
if current_run_status_val == "stopping":
|
|
361
|
-
console.print("\n[bold yellow]Stop request received. Terminating run gracefully...[/]")
|
|
362
|
-
user_stop_requested_flag = True
|
|
363
|
-
break
|
|
364
|
-
except requests.exceptions.RequestException as e:
|
|
365
|
-
console.print(f"\n[bold red]Warning: Unable to check run status: {e}. Continuing optimization...[/]")
|
|
366
|
-
except Exception as e:
|
|
367
|
-
console.print(f"\n[bold red]Warning: Error checking run status: {e}. Continuing optimization...[/]")
|
|
368
|
-
|
|
369
|
-
# Send feedback and get next suggestion
|
|
370
|
-
eval_and_next_solution_response = evaluate_feedback_then_suggest_next_solution(
|
|
371
|
-
console=console,
|
|
372
|
-
step=step,
|
|
373
|
-
run_id=run_id,
|
|
374
|
-
execution_output=term_out,
|
|
375
|
-
auth_headers=auth_headers,
|
|
376
|
-
api_keys=api_keys,
|
|
377
|
-
)
|
|
378
|
-
# Save next solution (.runs/<run-id>/step_<step>.<extension>)
|
|
379
|
-
write_to_path(fp=runs_dir / f"step_{step}{source_fp.suffix}", content=eval_and_next_solution_response["code"])
|
|
227
|
+
ui.on_output(term_out)
|
|
380
228
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
summary_panel.update_thinking(thinking=eval_and_next_solution_response["plan"])
|
|
387
|
-
|
|
388
|
-
nodes_list_from_status = status_response.get("nodes")
|
|
389
|
-
tree_panel.build_metric_tree(nodes=nodes_list_from_status if nodes_list_from_status is not None else [])
|
|
390
|
-
tree_panel.set_unevaluated_node(node_id=eval_and_next_solution_response["solution_id"])
|
|
391
|
-
|
|
392
|
-
# Update the solution panels with the next solution and best solution (and score)
|
|
393
|
-
# Figure out if we have a best solution so far
|
|
394
|
-
best_solution_node = get_best_node_from_status(status_response=status_response)
|
|
395
|
-
current_solution_node = get_node_from_status(
|
|
396
|
-
status_response=status_response, solution_id=eval_and_next_solution_response["solution_id"]
|
|
397
|
-
)
|
|
229
|
+
# Submit result
|
|
230
|
+
ui.on_submitting()
|
|
231
|
+
result = submit_execution_result(
|
|
232
|
+
run_id=run_id, task_id=task_id, execution_output=term_out, auth_headers=auth_headers, api_keys=api_keys
|
|
233
|
+
)
|
|
398
234
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
write_to_path(fp=runs_dir / f"best{source_fp.suffix}", content=best_solution_code)
|
|
408
|
-
|
|
409
|
-
# Update the solution panels with the current and best solution
|
|
410
|
-
solution_panels.update(current_node=current_solution_node, best_node=best_solution_node)
|
|
411
|
-
current_solution_panel, best_solution_panel = solution_panels.get_display(current_step=step)
|
|
412
|
-
# Clear evaluation output since we are running a evaluation on a new solution
|
|
413
|
-
eval_output_panel.clear()
|
|
414
|
-
smooth_update(
|
|
415
|
-
live=live,
|
|
416
|
-
layout=layout,
|
|
417
|
-
sections_to_update=[
|
|
418
|
-
("summary", summary_panel.get_display()),
|
|
419
|
-
("tree", tree_panel.get_display(is_done=False)),
|
|
420
|
-
("current_solution", current_solution_panel),
|
|
421
|
-
("best_solution", best_solution_panel),
|
|
422
|
-
("eval_output", eval_output_panel.get_display()),
|
|
423
|
-
],
|
|
424
|
-
transition_delay=0.08, # Slightly longer delay for more noticeable transitions
|
|
235
|
+
if result is None:
|
|
236
|
+
ui.on_error("Failed to submit result")
|
|
237
|
+
return OptimizationResult(
|
|
238
|
+
success=False,
|
|
239
|
+
final_step=step,
|
|
240
|
+
status="error",
|
|
241
|
+
reason="submit_failed",
|
|
242
|
+
details="Failed to submit execution result",
|
|
425
243
|
)
|
|
426
244
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
file_path=source_fp,
|
|
430
|
-
new_content=eval_and_next_solution_response["code"],
|
|
431
|
-
original_content=source_code,
|
|
432
|
-
eval_command=eval_command,
|
|
433
|
-
timeout=eval_timeout,
|
|
434
|
-
)
|
|
245
|
+
is_done = result.get("is_done", False)
|
|
246
|
+
prev_metric = result.get("previous_solution_metric_value")
|
|
435
247
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
save_execution_output(runs_dir, step=step, output=term_out)
|
|
439
|
-
eval_output_panel.update(output=term_out)
|
|
440
|
-
smooth_update(
|
|
441
|
-
live=live,
|
|
442
|
-
layout=layout,
|
|
443
|
-
sections_to_update=[("eval_output", eval_output_panel.get_display())],
|
|
444
|
-
transition_delay=0.1,
|
|
445
|
-
)
|
|
248
|
+
if prev_metric is not None:
|
|
249
|
+
ui.on_metric(step, prev_metric)
|
|
446
250
|
|
|
447
|
-
|
|
448
|
-
# Evaluate the final solution thats been generated
|
|
449
|
-
eval_and_next_solution_response = evaluate_feedback_then_suggest_next_solution(
|
|
450
|
-
console=console,
|
|
451
|
-
step=steps,
|
|
452
|
-
run_id=run_id,
|
|
453
|
-
execution_output=term_out,
|
|
454
|
-
auth_headers=auth_headers,
|
|
455
|
-
api_keys=api_keys,
|
|
456
|
-
)
|
|
457
|
-
summary_panel.set_step(step=steps)
|
|
458
|
-
status_response = get_optimization_run_status(
|
|
459
|
-
console=console, run_id=run_id, include_history=True, auth_headers=auth_headers
|
|
460
|
-
)
|
|
461
|
-
# No need to update the plan panel since we have finished the optimization
|
|
462
|
-
# Get the optimization run status for
|
|
463
|
-
# the best solution, its score, and the history to plot the tree
|
|
464
|
-
nodes_list_from_status_final = status_response.get("nodes")
|
|
465
|
-
tree_panel.build_metric_tree(
|
|
466
|
-
nodes=nodes_list_from_status_final if nodes_list_from_status_final is not None else []
|
|
467
|
-
)
|
|
468
|
-
# No need to set any solution to unevaluated since we have finished the optimization
|
|
469
|
-
# and all solutions have been evaluated
|
|
470
|
-
# No need to update the current solution panel since we have finished the optimization
|
|
471
|
-
# We only need to update the best solution panel
|
|
472
|
-
# Figure out if we have a best solution so far
|
|
473
|
-
best_solution_node = get_best_node_from_status(status_response=status_response)
|
|
474
|
-
best_solution_code = best_solution_node.code
|
|
475
|
-
# Save best solution to .runs/<run-id>/best.<extension>
|
|
476
|
-
write_to_path(fp=runs_dir / f"best{source_fp.suffix}", content=best_solution_code)
|
|
477
|
-
solution_panels.update(current_node=None, best_node=best_solution_node)
|
|
478
|
-
_, best_solution_panel = solution_panels.get_display(current_step=steps)
|
|
479
|
-
# Update the end optimization layout
|
|
480
|
-
final_message = (
|
|
481
|
-
f"{summary_panel.metric_name.capitalize()} {'maximized' if summary_panel.maximize else 'minimized'}! Best solution {summary_panel.metric_name.lower()} = [green]{status_response['best_result']['metric_value']}[/] 🏆"
|
|
482
|
-
if best_solution_node is not None and best_solution_node.metric is not None
|
|
483
|
-
else "[red] No valid solution found.[/]"
|
|
484
|
-
)
|
|
485
|
-
end_optimization_layout["summary"].update(summary_panel.get_display(final_message=final_message))
|
|
486
|
-
end_optimization_layout["tree"].update(tree_panel.get_display(is_done=True))
|
|
487
|
-
end_optimization_layout["best_solution"].update(best_solution_panel)
|
|
251
|
+
step += 1
|
|
488
252
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
253
|
+
if is_done:
|
|
254
|
+
ui.on_complete(step)
|
|
255
|
+
return OptimizationResult(success=True, final_step=step, status="completed", reason="completed_successfully")
|
|
492
256
|
|
|
257
|
+
except KeyboardInterrupt:
|
|
258
|
+
ui.on_interrupted()
|
|
259
|
+
return OptimizationResult(success=False, final_step=step, status="terminated", reason="user_terminated_sigint")
|
|
493
260
|
except Exception as e:
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
error_message = e.response.json()["detail"]
|
|
497
|
-
except Exception:
|
|
498
|
-
error_message = str(e)
|
|
499
|
-
console.print(Panel(f"[bold red]Error: {error_message}", title="[bold red]Optimization Error", border_style="red"))
|
|
500
|
-
console.print(f"\n[cyan]To resume this run, use:[/] [bold cyan]weco resume {run_id}[/]\n")
|
|
501
|
-
# Ensure optimization_completed_normally is False
|
|
502
|
-
optimization_completed_normally = False
|
|
503
|
-
finally:
|
|
504
|
-
# Restore original signal handlers
|
|
505
|
-
signal.signal(signal.SIGINT, original_sigint_handler)
|
|
506
|
-
signal.signal(signal.SIGTERM, original_sigterm_handler)
|
|
261
|
+
ui.on_error(f"Error: {e}")
|
|
262
|
+
return OptimizationResult(success=False, final_step=step, status="error", reason="unknown", details=str(e))
|
|
507
263
|
|
|
508
|
-
# Stop heartbeat thread
|
|
509
|
-
stop_heartbeat_event.set()
|
|
510
|
-
if heartbeat_thread and heartbeat_thread.is_alive():
|
|
511
|
-
heartbeat_thread.join(timeout=2)
|
|
512
|
-
|
|
513
|
-
# Report final status if run exists
|
|
514
|
-
if run_id:
|
|
515
|
-
if optimization_completed_normally:
|
|
516
|
-
status, reason, details = "completed", "completed_successfully", None
|
|
517
|
-
elif user_stop_requested_flag:
|
|
518
|
-
status, reason, details = "terminated", "user_requested_stop", "Run stopped by user request via dashboard."
|
|
519
|
-
else:
|
|
520
|
-
status, reason = "error", "error_cli_internal"
|
|
521
|
-
details = locals().get("error_details") or (
|
|
522
|
-
traceback.format_exc()
|
|
523
|
-
if "e" in locals() and isinstance(locals()["e"], Exception)
|
|
524
|
-
else "CLI terminated unexpectedly without a specific exception captured."
|
|
525
|
-
)
|
|
526
264
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
265
|
+
def _offer_apply_best_solution(
|
|
266
|
+
console: Console,
|
|
267
|
+
run_id: str,
|
|
268
|
+
source_fp: pathlib.Path,
|
|
269
|
+
source_code: str,
|
|
270
|
+
runs_dir: pathlib.Path,
|
|
271
|
+
auth_headers: dict,
|
|
272
|
+
apply_change: bool = False,
|
|
273
|
+
) -> None:
|
|
274
|
+
"""
|
|
275
|
+
Fetch the best solution from the backend and offer to apply it to the source file.
|
|
537
276
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
277
|
+
Args:
|
|
278
|
+
console: Rich console for output.
|
|
279
|
+
run_id: The optimization run ID.
|
|
280
|
+
source_fp: Path to the source file.
|
|
281
|
+
source_code: Original source code content.
|
|
282
|
+
runs_dir: Directory for run logs.
|
|
283
|
+
auth_headers: Authentication headers.
|
|
284
|
+
apply_change: If True, apply automatically without prompting.
|
|
285
|
+
"""
|
|
286
|
+
try:
|
|
287
|
+
# Fetch final status to get best solution
|
|
288
|
+
status = get_optimization_run_status(console=console, run_id=run_id, include_history=False, auth_headers=auth_headers)
|
|
289
|
+
best_result = status.get("best_result")
|
|
290
|
+
|
|
291
|
+
if best_result is None:
|
|
292
|
+
console.print("\n[yellow]No solution found. No changes to apply.[/]\n")
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
best_code = best_result.get("code")
|
|
296
|
+
best_metric = best_result.get("metric_value")
|
|
545
297
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
console.print(f"\n[cyan]To resume this run, use:[/] [bold cyan]weco resume {run_id}[/]\n")
|
|
298
|
+
if not best_code or best_code == source_code:
|
|
299
|
+
console.print("\n[green]Best solution is the same as original. No changes to apply.[/]\n")
|
|
300
|
+
return
|
|
550
301
|
|
|
551
|
-
|
|
302
|
+
# Save best solution to logs
|
|
303
|
+
write_to_path(fp=runs_dir / f"best{source_fp.suffix}", content=best_code)
|
|
304
|
+
|
|
305
|
+
# Show summary
|
|
306
|
+
console.print("\n[bold green]Optimization complete![/]")
|
|
307
|
+
if best_metric is not None:
|
|
308
|
+
console.print(f"[green]Best metric value: {best_metric}[/]")
|
|
309
|
+
|
|
310
|
+
# Ask user or auto-apply
|
|
311
|
+
if apply_change:
|
|
312
|
+
should_apply = True
|
|
313
|
+
else:
|
|
314
|
+
should_apply = Confirm.ask("Would you like to apply the best solution to your source file?", default=True)
|
|
315
|
+
|
|
316
|
+
if should_apply:
|
|
317
|
+
write_to_path(fp=source_fp, content=best_code)
|
|
318
|
+
console.print(f"[green]Best solution applied to {source_fp}[/]\n")
|
|
319
|
+
else:
|
|
320
|
+
console.print(f"[dim]Best solution saved to {runs_dir / f'best{source_fp.suffix}'}[/]\n")
|
|
321
|
+
|
|
322
|
+
except Exception as e:
|
|
323
|
+
console.print(f"[yellow]Could not fetch best solution: {e}[/]")
|
|
552
324
|
|
|
553
325
|
|
|
554
326
|
def resume_optimization(
|
|
555
|
-
run_id: str,
|
|
327
|
+
run_id: str, api_keys: Optional[dict] = None, poll_interval: float = 2.0, apply_change: bool = False
|
|
556
328
|
) -> bool:
|
|
557
|
-
"""
|
|
558
|
-
|
|
559
|
-
console = Console()
|
|
329
|
+
"""
|
|
330
|
+
Resume an interrupted run using the queue-based optimization loop.
|
|
560
331
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
stop_heartbeat_event = threading.Event()
|
|
564
|
-
current_run_id_for_heartbeat = None
|
|
565
|
-
current_auth_headers_for_heartbeat = {}
|
|
566
|
-
live_ref = None # Reference to the Live object for the optimization run
|
|
332
|
+
Polls for execution tasks, executes locally, and submits results.
|
|
333
|
+
Uses the execution queue flow instead of the legacy direct flow.
|
|
567
334
|
|
|
568
|
-
|
|
569
|
-
|
|
335
|
+
Args:
|
|
336
|
+
run_id: The UUID of the run to resume.
|
|
337
|
+
api_keys: Optional API keys for LLM providers.
|
|
338
|
+
poll_interval: Seconds between polling attempts.
|
|
339
|
+
apply_change: If True, automatically apply best solution; if False, prompt user.
|
|
570
340
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
live_ref.stop() # Stop the live update loop so that messages are printed to the console
|
|
341
|
+
Returns:
|
|
342
|
+
True if optimization completed successfully, False otherwise.
|
|
343
|
+
"""
|
|
344
|
+
console = Console()
|
|
576
345
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
heartbeat_thread.join(timeout=2)
|
|
582
|
-
if current_run_id_for_heartbeat:
|
|
583
|
-
report_termination(
|
|
584
|
-
run_id=current_run_id_for_heartbeat,
|
|
585
|
-
status_update="terminated",
|
|
586
|
-
reason=f"user_terminated_{signal_name.lower()}",
|
|
587
|
-
details=f"Process terminated by signal {signal_name} ({signum}).",
|
|
588
|
-
auth_headers=current_auth_headers_for_heartbeat,
|
|
589
|
-
)
|
|
590
|
-
console.print(f"\n[cyan]To resume this run, use:[/] [bold cyan]weco resume {current_run_id_for_heartbeat}[/]\n")
|
|
591
|
-
sys.exit(0)
|
|
346
|
+
# Authenticate
|
|
347
|
+
weco_api_key, auth_headers = handle_authentication(console)
|
|
348
|
+
if weco_api_key is None:
|
|
349
|
+
return False
|
|
592
350
|
|
|
593
|
-
#
|
|
594
|
-
|
|
595
|
-
|
|
351
|
+
# Fetch status first for validation and to display confirmation info
|
|
352
|
+
try:
|
|
353
|
+
status = get_optimization_run_status(console=console, run_id=run_id, include_history=True, auth_headers=auth_headers)
|
|
354
|
+
except Exception as e:
|
|
355
|
+
console.print(f"[bold red]Error fetching run status: {e}[/]")
|
|
356
|
+
return False
|
|
357
|
+
|
|
358
|
+
run_status_val = status.get("status")
|
|
359
|
+
if run_status_val not in ("error", "terminated"):
|
|
360
|
+
console.print(
|
|
361
|
+
f"[yellow]Run {run_id} cannot be resumed (status: {run_status_val}). "
|
|
362
|
+
f"Only 'error' or 'terminated' runs can be resumed.[/]"
|
|
363
|
+
)
|
|
364
|
+
return False
|
|
596
365
|
|
|
597
|
-
|
|
598
|
-
|
|
366
|
+
objective = status.get("objective", {})
|
|
367
|
+
metric_name = objective.get("metric_name", "metric")
|
|
368
|
+
maximize = bool(objective.get("maximize", True))
|
|
369
|
+
eval_command = objective.get("evaluation_command", "")
|
|
599
370
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
# Authentication failed or user declined
|
|
605
|
-
return False
|
|
371
|
+
optimizer = status.get("optimizer", {})
|
|
372
|
+
total_steps = optimizer.get("steps", 0)
|
|
373
|
+
current_step = int(status.get("current_step", 0))
|
|
374
|
+
steps_remaining = int(total_steps) - current_step
|
|
606
375
|
|
|
607
|
-
|
|
376
|
+
model_name = (
|
|
377
|
+
(optimizer.get("code_generator") or {}).get("model") or (optimizer.get("evaluator") or {}).get("model") or "unknown"
|
|
378
|
+
)
|
|
608
379
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
380
|
+
# Display confirmation info
|
|
381
|
+
console.print("[cyan]Resume Run Confirmation[/]")
|
|
382
|
+
console.print(f" Run ID: {run_id}")
|
|
383
|
+
console.print(f" Run Name: {status.get('run_name', 'N/A')}")
|
|
384
|
+
console.print(f" Status: {run_status_val}")
|
|
385
|
+
console.print(f" Objective: {metric_name} ({'maximize' if maximize else 'minimize'})")
|
|
386
|
+
console.print(f" Model: {model_name}")
|
|
387
|
+
console.print(f" Eval Command: {eval_command}")
|
|
388
|
+
console.print(f" Total Steps: {total_steps} | Current Step: {current_step} | Steps Remaining: {steps_remaining}")
|
|
389
|
+
console.print(f" Last Updated: {status.get('updated_at', 'N/A')}")
|
|
390
|
+
|
|
391
|
+
unchanged = Confirm.ask(
|
|
392
|
+
"Have you kept the source file and evaluation command unchanged since the original run?", default=True
|
|
393
|
+
)
|
|
394
|
+
if not unchanged:
|
|
395
|
+
console.print("[yellow]Resume cancelled. Please start a new run if the environment changed.[/]")
|
|
396
|
+
return False
|
|
397
|
+
|
|
398
|
+
# Call backend to prepare resume (this sets status to 'running')
|
|
399
|
+
resume_resp = resume_optimization_run(console=console, run_id=run_id, auth_headers=auth_headers)
|
|
400
|
+
if resume_resp is None:
|
|
401
|
+
return False
|
|
402
|
+
|
|
403
|
+
source_path = resume_resp.get("source_path")
|
|
404
|
+
log_dir = resume_resp.get("log_dir", ".runs")
|
|
405
|
+
save_logs = bool(resume_resp.get("save_logs", False))
|
|
406
|
+
eval_timeout = resume_resp.get("eval_timeout")
|
|
407
|
+
|
|
408
|
+
# Read the original source code
|
|
409
|
+
source_fp = pathlib.Path(source_path)
|
|
410
|
+
source_fp.parent.mkdir(parents=True, exist_ok=True)
|
|
411
|
+
source_code = read_from_path(fp=source_fp, is_json=False) if source_fp.exists() else ""
|
|
412
|
+
|
|
413
|
+
dashboard_url = f"{__dashboard_url__}/runs/{run_id}"
|
|
414
|
+
run_name = resume_resp.get("run_name", run_id)
|
|
415
|
+
|
|
416
|
+
# Open dashboard in the user's browser
|
|
417
|
+
open_browser(dashboard_url)
|
|
418
|
+
|
|
419
|
+
# Setup logging directory
|
|
420
|
+
runs_dir = pathlib.Path(log_dir) / run_id
|
|
421
|
+
runs_dir.mkdir(parents=True, exist_ok=True)
|
|
422
|
+
|
|
423
|
+
# Start heartbeat thread
|
|
424
|
+
stop_heartbeat_event = threading.Event()
|
|
425
|
+
heartbeat_thread = HeartbeatSender(run_id, auth_headers, stop_heartbeat_event)
|
|
426
|
+
heartbeat_thread.start()
|
|
427
|
+
|
|
428
|
+
# Extract best solution info from resume response (if available)
|
|
429
|
+
best_metric_value = resume_resp.get("best_metric_value")
|
|
430
|
+
best_step = resume_resp.get("best_step")
|
|
431
|
+
|
|
432
|
+
result: Optional[OptimizationResult] = None
|
|
433
|
+
try:
|
|
434
|
+
with LiveOptimizationUI(
|
|
435
|
+
console, run_id, run_name, total_steps, dashboard_url, model=model_name, metric_name=metric_name
|
|
436
|
+
) as ui:
|
|
437
|
+
# Populate UI with best solution from previous run if available
|
|
438
|
+
if best_metric_value is not None and best_step is not None:
|
|
439
|
+
ui.on_metric(best_step, best_metric_value)
|
|
440
|
+
|
|
441
|
+
result = _run_optimization_loop(
|
|
442
|
+
ui=ui,
|
|
443
|
+
run_id=run_id,
|
|
444
|
+
auth_headers=auth_headers,
|
|
445
|
+
source_fp=source_fp,
|
|
446
|
+
source_code=source_code,
|
|
447
|
+
eval_command=eval_command,
|
|
448
|
+
eval_timeout=eval_timeout,
|
|
449
|
+
runs_dir=runs_dir,
|
|
450
|
+
save_logs=save_logs,
|
|
451
|
+
start_step=current_step,
|
|
452
|
+
poll_interval=poll_interval,
|
|
453
|
+
api_keys=api_keys,
|
|
628
454
|
)
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
)
|
|
648
|
-
console.print(f" Model: {model_name}")
|
|
649
|
-
console.print(f" Eval Command: {objective.get('evaluation_command', 'N/A')}")
|
|
650
|
-
# Steps summary
|
|
651
|
-
total_steps = optimizer.get("steps")
|
|
652
|
-
current_step = int(status["current_step"])
|
|
653
|
-
steps_remaining = int(total_steps) - int(current_step)
|
|
654
|
-
console.print(f" Total Steps: {total_steps} | Resume Step: {current_step} | Steps Remaining: {steps_remaining}")
|
|
655
|
-
console.print(f" Last Updated: {status.get('updated_at', 'N/A')}")
|
|
656
|
-
unchanged = Confirm.ask(
|
|
657
|
-
"Have you kept the source file and evaluation command unchanged since the original run?", default=True
|
|
658
|
-
)
|
|
659
|
-
if not unchanged:
|
|
660
|
-
console.print("[yellow]Resume cancelled. Please start a new run if the environment changed.[/]")
|
|
661
|
-
return False
|
|
662
|
-
|
|
663
|
-
# Call backend to prepare resume
|
|
664
|
-
resume_resp = resume_optimization_run(console=console, run_id=run_id, auth_headers=auth_headers)
|
|
665
|
-
if resume_resp is None:
|
|
666
|
-
return False
|
|
667
|
-
|
|
668
|
-
eval_command = resume_resp["evaluation_command"]
|
|
669
|
-
source_path = resume_resp.get("source_path")
|
|
670
|
-
|
|
671
|
-
# Use backend-saved values
|
|
672
|
-
log_dir = resume_resp.get("log_dir", ".runs")
|
|
673
|
-
save_logs = bool(resume_resp.get("save_logs", False))
|
|
674
|
-
eval_timeout = resume_resp.get("eval_timeout")
|
|
675
|
-
|
|
676
|
-
# Read the original source code from the file before we start modifying it
|
|
677
|
-
source_fp = pathlib.Path(source_path)
|
|
678
|
-
source_fp.parent.mkdir(parents=True, exist_ok=True)
|
|
679
|
-
# Store the original content to restore after each evaluation
|
|
680
|
-
original_source_code = read_from_path(fp=source_fp, is_json=False) if source_fp.exists() else ""
|
|
681
|
-
# The code to restore is the code from the last step of the previous run
|
|
682
|
-
code_to_restore = resume_resp.get("code") or resume_resp.get("source_code") or ""
|
|
683
|
-
|
|
684
|
-
# Prepare UI panels
|
|
685
|
-
summary_panel = SummaryPanel(
|
|
686
|
-
maximize=maximize, metric_name=metric_name, total_steps=total_steps, model=model_name, runs_dir=log_dir
|
|
455
|
+
|
|
456
|
+
# Stop heartbeat immediately after loop completes
|
|
457
|
+
stop_heartbeat_event.set()
|
|
458
|
+
heartbeat_thread.join(timeout=2)
|
|
459
|
+
|
|
460
|
+
# Show resume message if interrupted
|
|
461
|
+
if result.status == "terminated":
|
|
462
|
+
console.print(f"\n[cyan]To resume this run, use:[/] [bold]weco resume {run_id}[/]\n")
|
|
463
|
+
|
|
464
|
+
# Offer to apply best solution
|
|
465
|
+
_offer_apply_best_solution(
|
|
466
|
+
console=console,
|
|
467
|
+
run_id=run_id,
|
|
468
|
+
source_fp=source_fp,
|
|
469
|
+
source_code=source_code,
|
|
470
|
+
runs_dir=runs_dir,
|
|
471
|
+
auth_headers=auth_headers,
|
|
472
|
+
apply_change=apply_change,
|
|
687
473
|
)
|
|
688
|
-
summary_panel.set_run_id(run_id=resume_resp["run_id"])
|
|
689
|
-
if resume_resp.get("run_name"):
|
|
690
|
-
summary_panel.set_run_name(resume_resp.get("run_name"))
|
|
691
|
-
summary_panel.set_step(step=current_step)
|
|
692
|
-
summary_panel.update_thinking(resume_resp.get("plan"))
|
|
693
|
-
|
|
694
|
-
solution_panels = SolutionPanels(metric_name=metric_name, source_fp=source_fp)
|
|
695
|
-
eval_output_panel = EvaluationOutputPanel()
|
|
696
|
-
tree_panel = MetricTreePanel(maximize=maximize)
|
|
697
|
-
layout = create_optimization_layout()
|
|
698
|
-
end_optimization_layout = create_end_optimization_layout()
|
|
699
|
-
|
|
700
|
-
# Build tree from nodes returned by status (history)
|
|
701
|
-
nodes_list_from_status = status.get("nodes") or []
|
|
702
|
-
tree_panel.build_metric_tree(nodes=nodes_list_from_status)
|
|
703
|
-
|
|
704
|
-
# Compute best and current nodes
|
|
705
|
-
best_solution_node = get_best_node_from_status(status_response=status)
|
|
706
|
-
current_solution_node = get_node_from_status(status_response=status, solution_id=resume_resp.get("solution_id"))
|
|
707
|
-
|
|
708
|
-
# If there's no best solution yet (baseline evaluation didn't complete),
|
|
709
|
-
# mark the current node as unevaluated so the tree renders correctly
|
|
710
|
-
if best_solution_node is None:
|
|
711
|
-
tree_panel.set_unevaluated_node(node_id=resume_resp.get("solution_id"))
|
|
712
|
-
|
|
713
|
-
# Ensure runs dir exists
|
|
714
|
-
runs_dir = pathlib.Path(log_dir) / resume_resp["run_id"]
|
|
715
|
-
runs_dir.mkdir(parents=True, exist_ok=True)
|
|
716
|
-
# Persist last step's code into logs as step_<current_step>
|
|
717
|
-
write_to_path(fp=runs_dir / f"step_{current_step}{source_fp.suffix}", content=code_to_restore)
|
|
718
|
-
|
|
719
|
-
# Initialize best solution code
|
|
720
|
-
try:
|
|
721
|
-
best_solution_code = best_solution_node.code
|
|
722
|
-
except AttributeError:
|
|
723
|
-
# Edge case: best solution node is not available.
|
|
724
|
-
# This can happen if the user has cancelled the run before even running the baseline solution
|
|
725
|
-
pass # Leave best solution code as None
|
|
726
|
-
|
|
727
|
-
# Start Heartbeat Thread
|
|
728
|
-
stop_heartbeat_event.clear()
|
|
729
|
-
heartbeat_thread = HeartbeatSender(resume_resp["run_id"], auth_headers, stop_heartbeat_event)
|
|
730
|
-
heartbeat_thread.start()
|
|
731
|
-
current_run_id_for_heartbeat = resume_resp["run_id"]
|
|
732
|
-
|
|
733
|
-
# Seed solution panels with current and best nodes
|
|
734
|
-
solution_panels.update(current_node=current_solution_node, best_node=best_solution_node)
|
|
735
|
-
|
|
736
|
-
# --- Live UI ---
|
|
737
|
-
refresh_rate = 4
|
|
738
|
-
with Live(layout, refresh_per_second=refresh_rate) as live:
|
|
739
|
-
live_ref = live
|
|
740
|
-
# Initial panels
|
|
741
|
-
current_solution_panel, best_solution_panel = solution_panels.get_display(current_step=current_step)
|
|
742
|
-
# Use backend-provided execution output only (no fallback)
|
|
743
|
-
term_out = resume_resp.get("execution_output") or ""
|
|
744
|
-
eval_output_panel.update(output=term_out)
|
|
745
|
-
|
|
746
|
-
# Update the initial panels
|
|
747
|
-
smooth_update(
|
|
748
|
-
live=live,
|
|
749
|
-
layout=layout,
|
|
750
|
-
sections_to_update=[
|
|
751
|
-
("summary", summary_panel.get_display()),
|
|
752
|
-
("tree", tree_panel.get_display(is_done=False)),
|
|
753
|
-
("current_solution", current_solution_panel),
|
|
754
|
-
("best_solution", best_solution_panel),
|
|
755
|
-
("eval_output", eval_output_panel.get_display()),
|
|
756
|
-
],
|
|
757
|
-
transition_delay=0.1,
|
|
758
|
-
)
|
|
759
474
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
original_content=original_source_code,
|
|
766
|
-
eval_command=eval_command,
|
|
767
|
-
timeout=eval_timeout,
|
|
768
|
-
)
|
|
769
|
-
eval_output_panel.update(output=term_out)
|
|
770
|
-
# Update the evaluation output panel
|
|
771
|
-
smooth_update(
|
|
772
|
-
live=live,
|
|
773
|
-
layout=layout,
|
|
774
|
-
sections_to_update=[("eval_output", eval_output_panel.get_display())],
|
|
775
|
-
transition_delay=0.1,
|
|
776
|
-
)
|
|
475
|
+
return result.success
|
|
476
|
+
finally:
|
|
477
|
+
# Ensure heartbeat is stopped (in case of early exit/exception)
|
|
478
|
+
stop_heartbeat_event.set()
|
|
479
|
+
heartbeat_thread.join(timeout=2)
|
|
777
480
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
console=console, run_id=resume_resp["run_id"], include_history=False, auth_headers=auth_headers
|
|
787
|
-
)
|
|
788
|
-
if current_status_response.get("status") == "stopping":
|
|
789
|
-
console.print("\n[bold yellow]Stop request received. Terminating run gracefully...[/]")
|
|
790
|
-
user_stop_requested_flag = True
|
|
791
|
-
break
|
|
792
|
-
except requests.exceptions.RequestException as e:
|
|
793
|
-
console.print(f"\n[bold red]Warning: Unable to check run status: {e}. Continuing optimization...[/]")
|
|
794
|
-
except Exception as e:
|
|
795
|
-
console.print(f"\n[bold red]Warning: Error checking run status: {e}. Continuing optimization...[/]")
|
|
796
|
-
|
|
797
|
-
# Suggest next
|
|
798
|
-
eval_and_next_solution_response = evaluate_feedback_then_suggest_next_solution(
|
|
799
|
-
console=console,
|
|
800
|
-
step=step,
|
|
801
|
-
run_id=resume_resp["run_id"],
|
|
802
|
-
execution_output=term_out,
|
|
481
|
+
# Report termination to backend
|
|
482
|
+
if result is not None:
|
|
483
|
+
try:
|
|
484
|
+
report_termination(
|
|
485
|
+
run_id=run_id,
|
|
486
|
+
status_update=result.status,
|
|
487
|
+
reason=result.reason,
|
|
488
|
+
details=result.details,
|
|
803
489
|
auth_headers=auth_headers,
|
|
804
|
-
api_keys=api_keys,
|
|
805
490
|
)
|
|
491
|
+
except Exception:
|
|
492
|
+
pass # Best effort
|
|
806
493
|
|
|
807
|
-
# Save next solution to logs
|
|
808
|
-
write_to_path(fp=runs_dir / f"step_{step}{source_fp.suffix}", content=eval_and_next_solution_response["code"])
|
|
809
494
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
495
|
+
def optimize(
|
|
496
|
+
source: str,
|
|
497
|
+
eval_command: str,
|
|
498
|
+
metric: str,
|
|
499
|
+
goal: str = "maximize",
|
|
500
|
+
model: str = "o4-mini",
|
|
501
|
+
steps: int = 5,
|
|
502
|
+
additional_instructions: Optional[str] = None,
|
|
503
|
+
eval_timeout: Optional[int] = None,
|
|
504
|
+
save_logs: bool = False,
|
|
505
|
+
log_dir: str = ".runs",
|
|
506
|
+
api_keys: Optional[dict] = None,
|
|
507
|
+
poll_interval: float = 2.0,
|
|
508
|
+
apply_change: bool = False,
|
|
509
|
+
require_review: bool = False,
|
|
510
|
+
) -> bool:
|
|
511
|
+
"""
|
|
512
|
+
Simplified queue-based optimization loop.
|
|
823
513
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
best_solution_code = best_solution_node.code
|
|
827
|
-
except AttributeError:
|
|
828
|
-
# Can happen if the code was buggy
|
|
829
|
-
best_solution_code = read_from_path(fp=runs_dir / f"step_0{source_fp.suffix}", is_json=False)
|
|
830
|
-
|
|
831
|
-
# Save best solution to .runs/<run-id>/best.<extension>
|
|
832
|
-
write_to_path(fp=runs_dir / f"best{source_fp.suffix}", content=best_solution_code)
|
|
833
|
-
|
|
834
|
-
solution_panels.update(current_node=current_solution_node, best_node=best_solution_node)
|
|
835
|
-
current_solution_panel, best_solution_panel = solution_panels.get_display(current_step=step)
|
|
836
|
-
eval_output_panel.clear()
|
|
837
|
-
smooth_update(
|
|
838
|
-
live=live,
|
|
839
|
-
layout=layout,
|
|
840
|
-
sections_to_update=[
|
|
841
|
-
("summary", summary_panel.get_display()),
|
|
842
|
-
("tree", tree_panel.get_display(is_done=False)),
|
|
843
|
-
("current_solution", current_solution_panel),
|
|
844
|
-
("best_solution", best_solution_panel),
|
|
845
|
-
("eval_output", eval_output_panel.get_display()),
|
|
846
|
-
],
|
|
847
|
-
transition_delay=0.08,
|
|
848
|
-
)
|
|
514
|
+
Polls for execution tasks, executes locally, and submits results.
|
|
515
|
+
Uses the new execution queue flow instead of the legacy direct flow.
|
|
849
516
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
sections_to_update=[("eval_output", eval_output_panel.get_display())],
|
|
865
|
-
transition_delay=0.1,
|
|
866
|
-
)
|
|
517
|
+
Args:
|
|
518
|
+
source: Path to the source file to optimize.
|
|
519
|
+
eval_command: Command to run for evaluation.
|
|
520
|
+
metric: Name of the metric to optimize.
|
|
521
|
+
goal: "maximize" or "minimize".
|
|
522
|
+
model: LLM model to use.
|
|
523
|
+
steps: Number of optimization steps.
|
|
524
|
+
additional_instructions: Optional instructions for the optimizer.
|
|
525
|
+
eval_timeout: Timeout for evaluation command in seconds.
|
|
526
|
+
save_logs: Whether to save execution logs.
|
|
527
|
+
log_dir: Directory for logs.
|
|
528
|
+
api_keys: Optional API keys for LLM providers.
|
|
529
|
+
poll_interval: Seconds between polling attempts.
|
|
530
|
+
apply_change: If True, automatically apply best solution; if False, prompt user.
|
|
867
531
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
532
|
+
Returns:
|
|
533
|
+
True if optimization completed successfully, False otherwise.
|
|
534
|
+
"""
|
|
535
|
+
console = Console()
|
|
536
|
+
|
|
537
|
+
# Authenticate
|
|
538
|
+
weco_api_key, auth_headers = handle_authentication(console)
|
|
539
|
+
if weco_api_key is None:
|
|
540
|
+
# Authentication failed or user declined
|
|
541
|
+
return False
|
|
542
|
+
|
|
543
|
+
# Process parameters
|
|
544
|
+
maximize = goal.lower() in ["maximize", "max"]
|
|
545
|
+
source_fp = pathlib.Path(source)
|
|
546
|
+
source_code = read_from_path(fp=source_fp, is_json=False)
|
|
547
|
+
|
|
548
|
+
code_generator_config = {"model": model}
|
|
549
|
+
evaluator_config = {"model": model, "include_analysis": True}
|
|
550
|
+
search_policy_config = {
|
|
551
|
+
"num_drafts": max(1, math.ceil(0.15 * steps)),
|
|
552
|
+
"debug_prob": 0.5,
|
|
553
|
+
"max_debug_depth": max(1, math.ceil(0.1 * steps)),
|
|
554
|
+
}
|
|
555
|
+
processed_instructions = read_additional_instructions(additional_instructions)
|
|
556
|
+
|
|
557
|
+
# Start the run
|
|
558
|
+
run_response = start_optimization_run(
|
|
559
|
+
console=console,
|
|
560
|
+
source_code=source_code,
|
|
561
|
+
source_path=str(source_fp),
|
|
562
|
+
evaluation_command=eval_command,
|
|
563
|
+
metric_name=metric,
|
|
564
|
+
maximize=maximize,
|
|
565
|
+
steps=steps,
|
|
566
|
+
code_generator_config=code_generator_config,
|
|
567
|
+
evaluator_config=evaluator_config,
|
|
568
|
+
search_policy_config=search_policy_config,
|
|
569
|
+
additional_instructions=processed_instructions,
|
|
570
|
+
eval_timeout=eval_timeout,
|
|
571
|
+
save_logs=save_logs,
|
|
572
|
+
log_dir=log_dir,
|
|
573
|
+
auth_headers=auth_headers,
|
|
574
|
+
api_keys=api_keys,
|
|
575
|
+
require_review=require_review,
|
|
576
|
+
)
|
|
900
577
|
|
|
901
|
-
|
|
902
|
-
|
|
578
|
+
if run_response is None:
|
|
579
|
+
return False
|
|
903
580
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
except Exception:
|
|
908
|
-
error_message = str(e)
|
|
909
|
-
console.print(Panel(f"[bold red]Error: {error_message}", title="[bold red]Optimization Error", border_style="red"))
|
|
910
|
-
console.print(f"\n[cyan]To resume this run, use:[/] [bold cyan]weco resume {run_id}[/]\n")
|
|
911
|
-
optimization_completed_normally = False
|
|
912
|
-
finally:
|
|
913
|
-
signal.signal(signal.SIGINT, original_sigint_handler)
|
|
914
|
-
signal.signal(signal.SIGTERM, original_sigterm_handler)
|
|
915
|
-
stop_heartbeat_event.set()
|
|
916
|
-
if heartbeat_thread and heartbeat_thread.is_alive():
|
|
917
|
-
heartbeat_thread.join(timeout=2)
|
|
581
|
+
run_id = run_response["run_id"]
|
|
582
|
+
run_name = run_response["run_name"]
|
|
583
|
+
dashboard_url = f"{__dashboard_url__}/runs/{run_id}"
|
|
918
584
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
except Exception:
|
|
922
|
-
run_id = None
|
|
923
|
-
|
|
924
|
-
# Report final status if run exists
|
|
925
|
-
if run_id:
|
|
926
|
-
if optimization_completed_normally:
|
|
927
|
-
status, reason, details = "completed", "completed_successfully", None
|
|
928
|
-
elif user_stop_requested_flag:
|
|
929
|
-
status, reason, details = "terminated", "user_requested_stop", "Run stopped by user request via dashboard."
|
|
930
|
-
else:
|
|
931
|
-
status, reason = "error", "error_cli_internal"
|
|
932
|
-
details = locals().get("error_details") or (
|
|
933
|
-
traceback.format_exc()
|
|
934
|
-
if "e" in locals() and isinstance(locals()["e"], Exception)
|
|
935
|
-
else "CLI terminated unexpectedly without a specific exception captured."
|
|
936
|
-
)
|
|
585
|
+
# Open dashboard in the user's browser
|
|
586
|
+
open_browser(dashboard_url)
|
|
937
587
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
console.print("\n[green]A better solution was not found. No changes to apply.[/]\n")
|
|
588
|
+
# Setup logging directory
|
|
589
|
+
runs_dir = pathlib.Path(log_dir) / run_id
|
|
590
|
+
runs_dir.mkdir(parents=True, exist_ok=True)
|
|
591
|
+
|
|
592
|
+
# Start heartbeat thread
|
|
593
|
+
stop_heartbeat_event = threading.Event()
|
|
594
|
+
heartbeat_thread = HeartbeatSender(run_id, auth_headers, stop_heartbeat_event)
|
|
595
|
+
heartbeat_thread.start()
|
|
947
596
|
|
|
948
|
-
|
|
597
|
+
result: Optional[OptimizationResult] = None
|
|
598
|
+
try:
|
|
599
|
+
with LiveOptimizationUI(console, run_id, run_name, steps, dashboard_url, model=model, metric_name=metric) as ui:
|
|
600
|
+
result = _run_optimization_loop(
|
|
601
|
+
ui=ui,
|
|
949
602
|
run_id=run_id,
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
603
|
+
auth_headers=auth_headers,
|
|
604
|
+
source_fp=source_fp,
|
|
605
|
+
source_code=source_code,
|
|
606
|
+
eval_command=eval_command,
|
|
607
|
+
eval_timeout=eval_timeout,
|
|
608
|
+
runs_dir=runs_dir,
|
|
609
|
+
save_logs=save_logs,
|
|
610
|
+
start_step=0,
|
|
611
|
+
poll_interval=poll_interval,
|
|
612
|
+
api_keys=api_keys,
|
|
954
613
|
)
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
614
|
+
|
|
615
|
+
# Stop heartbeat immediately after loop completes
|
|
616
|
+
stop_heartbeat_event.set()
|
|
617
|
+
heartbeat_thread.join(timeout=2)
|
|
618
|
+
|
|
619
|
+
# Show resume message if interrupted
|
|
620
|
+
if result.status == "terminated":
|
|
621
|
+
console.print(f"\n[cyan]To resume this run, use:[/] [bold]weco resume {run_id}[/]\n")
|
|
622
|
+
|
|
623
|
+
# Offer to apply best solution
|
|
624
|
+
_offer_apply_best_solution(
|
|
625
|
+
console=console,
|
|
626
|
+
run_id=run_id,
|
|
627
|
+
source_fp=source_fp,
|
|
628
|
+
source_code=source_code,
|
|
629
|
+
runs_dir=runs_dir,
|
|
630
|
+
auth_headers=auth_headers,
|
|
631
|
+
apply_change=apply_change,
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
return result.success
|
|
635
|
+
finally:
|
|
636
|
+
# Ensure heartbeat is stopped (in case of early exit/exception)
|
|
637
|
+
stop_heartbeat_event.set()
|
|
638
|
+
heartbeat_thread.join(timeout=2)
|
|
639
|
+
|
|
640
|
+
# Report termination to backend
|
|
641
|
+
if result is not None:
|
|
642
|
+
try:
|
|
643
|
+
report_termination(
|
|
644
|
+
run_id=run_id,
|
|
645
|
+
status_update=result.status,
|
|
646
|
+
reason=result.reason,
|
|
647
|
+
details=result.details,
|
|
648
|
+
auth_headers=auth_headers,
|
|
649
|
+
)
|
|
650
|
+
except Exception:
|
|
651
|
+
pass # Best effort
|