weco 0.2.28__py3-none-any.whl → 0.3.1__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 +164 -59
- weco/auth.py +12 -14
- weco/chatbot.py +18 -3
- weco/cli.py +75 -1
- weco/constants.py +6 -3
- weco/credits.py +172 -0
- weco/optimizer.py +415 -89
- weco/panels.py +6 -18
- weco/utils.py +9 -54
- {weco-0.2.28.dist-info → weco-0.3.1.dist-info}/METADATA +52 -55
- weco-0.3.1.dist-info/RECORD +16 -0
- weco-0.2.28.dist-info/RECORD +0 -15
- {weco-0.2.28.dist-info → weco-0.3.1.dist-info}/WHEEL +0 -0
- {weco-0.2.28.dist-info → weco-0.3.1.dist-info}/entry_points.txt +0 -0
- {weco-0.2.28.dist-info → weco-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {weco-0.2.28.dist-info → weco-0.3.1.dist-info}/top_level.txt +0 -0
weco/optimizer.py
CHANGED
|
@@ -11,13 +11,14 @@ from typing import Optional
|
|
|
11
11
|
from rich.console import Console
|
|
12
12
|
from rich.live import Live
|
|
13
13
|
from rich.panel import Panel
|
|
14
|
-
|
|
14
|
+
from rich.prompt import Confirm
|
|
15
15
|
from .api import (
|
|
16
16
|
start_optimization_run,
|
|
17
17
|
evaluate_feedback_then_suggest_next_solution,
|
|
18
18
|
get_optimization_run_status,
|
|
19
19
|
send_heartbeat,
|
|
20
20
|
report_termination,
|
|
21
|
+
resume_optimization_run,
|
|
21
22
|
)
|
|
22
23
|
from .auth import handle_authentication
|
|
23
24
|
from .panels import (
|
|
@@ -29,16 +30,7 @@ from .panels import (
|
|
|
29
30
|
create_optimization_layout,
|
|
30
31
|
create_end_optimization_layout,
|
|
31
32
|
)
|
|
32
|
-
from .utils import
|
|
33
|
-
read_api_keys_from_env,
|
|
34
|
-
read_additional_instructions,
|
|
35
|
-
read_from_path,
|
|
36
|
-
write_to_path,
|
|
37
|
-
run_evaluation,
|
|
38
|
-
smooth_update,
|
|
39
|
-
format_number,
|
|
40
|
-
)
|
|
41
|
-
from .constants import DEFAULT_API_TIMEOUT
|
|
33
|
+
from .utils import read_additional_instructions, read_from_path, write_to_path, run_evaluation, smooth_update
|
|
42
34
|
|
|
43
35
|
|
|
44
36
|
def save_execution_output(runs_dir: pathlib.Path, step: int, output: str) -> None:
|
|
@@ -100,6 +92,36 @@ class HeartbeatSender(threading.Thread):
|
|
|
100
92
|
# The loop will break due to the exception, and thread will terminate via finally.
|
|
101
93
|
|
|
102
94
|
|
|
95
|
+
def get_best_node_from_status(status_response: dict) -> Optional[Node]:
|
|
96
|
+
"""Extract the best node from a status response as a panels.Node instance."""
|
|
97
|
+
if status_response.get("best_result") is not None:
|
|
98
|
+
return Node(
|
|
99
|
+
id=status_response["best_result"]["solution_id"],
|
|
100
|
+
parent_id=status_response["best_result"]["parent_id"],
|
|
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
|
+
|
|
103
125
|
def execute_optimization(
|
|
104
126
|
source: str,
|
|
105
127
|
eval_command: str,
|
|
@@ -146,8 +168,8 @@ def execute_optimization(
|
|
|
146
168
|
reason=f"user_terminated_{signal_name.lower()}",
|
|
147
169
|
details=f"Process terminated by signal {signal_name} ({signum}).",
|
|
148
170
|
auth_headers=current_auth_headers_for_heartbeat,
|
|
149
|
-
timeout=3,
|
|
150
171
|
)
|
|
172
|
+
console.print(f"\n[cyan]To resume this run, use:[/] [bold cyan]weco resume {current_run_id_for_heartbeat}[/]")
|
|
151
173
|
|
|
152
174
|
# Exit gracefully
|
|
153
175
|
sys.exit(0)
|
|
@@ -161,12 +183,10 @@ def execute_optimization(
|
|
|
161
183
|
user_stop_requested_flag = False
|
|
162
184
|
|
|
163
185
|
try:
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if weco_api_key is None and not llm_api_keys:
|
|
169
|
-
# Authentication failed and no LLM keys available
|
|
186
|
+
# --- Login/Authentication Handling (now mandatory) ---
|
|
187
|
+
weco_api_key, auth_headers = handle_authentication(console)
|
|
188
|
+
if weco_api_key is None:
|
|
189
|
+
# Authentication failed or user declined
|
|
170
190
|
return False
|
|
171
191
|
|
|
172
192
|
current_auth_headers_for_heartbeat = auth_headers
|
|
@@ -176,9 +196,8 @@ def execute_optimization(
|
|
|
176
196
|
|
|
177
197
|
# Determine the model to use
|
|
178
198
|
if model is None:
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
model = determine_default_model(llm_api_keys)
|
|
199
|
+
# Default to o4-mini with credit-based billing
|
|
200
|
+
model = "o4-mini"
|
|
182
201
|
|
|
183
202
|
code_generator_config = {"model": model}
|
|
184
203
|
evaluator_config = {"model": model, "include_analysis": True}
|
|
@@ -187,7 +206,6 @@ def execute_optimization(
|
|
|
187
206
|
"debug_prob": 0.5,
|
|
188
207
|
"max_debug_depth": max(1, math.ceil(0.1 * steps)),
|
|
189
208
|
}
|
|
190
|
-
api_timeout = DEFAULT_API_TIMEOUT
|
|
191
209
|
processed_additional_instructions = read_additional_instructions(additional_instructions=additional_instructions)
|
|
192
210
|
source_fp = pathlib.Path(source)
|
|
193
211
|
source_code = read_from_path(fp=source_fp, is_json=False)
|
|
@@ -204,6 +222,7 @@ def execute_optimization(
|
|
|
204
222
|
run_response = start_optimization_run(
|
|
205
223
|
console=console,
|
|
206
224
|
source_code=source_code,
|
|
225
|
+
source_path=str(source_fp),
|
|
207
226
|
evaluation_command=eval_command,
|
|
208
227
|
metric_name=metric,
|
|
209
228
|
maximize=maximize,
|
|
@@ -212,9 +231,10 @@ def execute_optimization(
|
|
|
212
231
|
evaluator_config=evaluator_config,
|
|
213
232
|
search_policy_config=search_policy_config,
|
|
214
233
|
additional_instructions=processed_additional_instructions,
|
|
215
|
-
|
|
234
|
+
eval_timeout=eval_timeout,
|
|
235
|
+
save_logs=save_logs,
|
|
236
|
+
log_dir=log_dir,
|
|
216
237
|
auth_headers=auth_headers,
|
|
217
|
-
timeout=api_timeout,
|
|
218
238
|
)
|
|
219
239
|
# Indicate the endpoint failed to return a response and the optimization was unsuccessful
|
|
220
240
|
if run_response is None:
|
|
@@ -263,8 +283,6 @@ def execute_optimization(
|
|
|
263
283
|
summary_panel.set_run_name(run_name=run_name)
|
|
264
284
|
# Set the step of the progress bar
|
|
265
285
|
summary_panel.set_step(step=0)
|
|
266
|
-
# Update the token counts
|
|
267
|
-
summary_panel.update_token_counts(usage=run_response["usage"])
|
|
268
286
|
summary_panel.update_thinking(thinking=run_response["plan"])
|
|
269
287
|
# Build the metric tree
|
|
270
288
|
tree_panel.build_metric_tree(
|
|
@@ -324,7 +342,7 @@ def execute_optimization(
|
|
|
324
342
|
if run_id:
|
|
325
343
|
try:
|
|
326
344
|
current_status_response = get_optimization_run_status(
|
|
327
|
-
console=console, run_id=run_id, include_history=False,
|
|
345
|
+
console=console, run_id=run_id, include_history=False, auth_headers=auth_headers
|
|
328
346
|
)
|
|
329
347
|
current_run_status_val = current_status_response.get("status")
|
|
330
348
|
if current_run_status_val == "stopping":
|
|
@@ -339,23 +357,21 @@ def execute_optimization(
|
|
|
339
357
|
# Send feedback and get next suggestion
|
|
340
358
|
eval_and_next_solution_response = evaluate_feedback_then_suggest_next_solution(
|
|
341
359
|
console=console,
|
|
360
|
+
step=step,
|
|
342
361
|
run_id=run_id,
|
|
343
362
|
execution_output=term_out,
|
|
344
363
|
additional_instructions=current_additional_instructions,
|
|
345
|
-
api_keys=llm_api_keys,
|
|
346
364
|
auth_headers=auth_headers,
|
|
347
|
-
timeout=api_timeout,
|
|
348
365
|
)
|
|
349
366
|
# Save next solution (.runs/<run-id>/step_<step>.<extension>)
|
|
350
367
|
write_to_path(fp=runs_dir / f"step_{step}{source_fp.suffix}", content=eval_and_next_solution_response["code"])
|
|
351
368
|
# Write the next solution to the source file
|
|
352
369
|
write_to_path(fp=source_fp, content=eval_and_next_solution_response["code"])
|
|
353
370
|
status_response = get_optimization_run_status(
|
|
354
|
-
console=console, run_id=run_id, include_history=True,
|
|
371
|
+
console=console, run_id=run_id, include_history=True, auth_headers=auth_headers
|
|
355
372
|
)
|
|
356
|
-
# Update the step of the progress bar,
|
|
373
|
+
# Update the step of the progress bar, plan and metric tree
|
|
357
374
|
summary_panel.set_step(step=step)
|
|
358
|
-
summary_panel.update_token_counts(usage=eval_and_next_solution_response["usage"])
|
|
359
375
|
summary_panel.update_thinking(thinking=eval_and_next_solution_response["plan"])
|
|
360
376
|
|
|
361
377
|
nodes_list_from_status = status_response.get("nodes")
|
|
@@ -364,32 +380,10 @@ def execute_optimization(
|
|
|
364
380
|
|
|
365
381
|
# Update the solution panels with the next solution and best solution (and score)
|
|
366
382
|
# Figure out if we have a best solution so far
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
code=status_response["best_result"]["code"],
|
|
372
|
-
metric=status_response["best_result"]["metric_value"],
|
|
373
|
-
is_buggy=status_response["best_result"]["is_buggy"],
|
|
374
|
-
)
|
|
375
|
-
else:
|
|
376
|
-
best_solution_node = None
|
|
377
|
-
|
|
378
|
-
current_solution_node = None
|
|
379
|
-
if status_response.get("nodes"):
|
|
380
|
-
for node_data in status_response["nodes"]:
|
|
381
|
-
if node_data["solution_id"] == eval_and_next_solution_response["solution_id"]:
|
|
382
|
-
current_solution_node = Node(
|
|
383
|
-
id=node_data["solution_id"],
|
|
384
|
-
parent_id=node_data["parent_id"],
|
|
385
|
-
code=node_data["code"],
|
|
386
|
-
metric=node_data["metric_value"],
|
|
387
|
-
is_buggy=node_data["is_buggy"],
|
|
388
|
-
)
|
|
389
|
-
if current_solution_node is None:
|
|
390
|
-
raise ValueError(
|
|
391
|
-
"Current solution node not found in the optimization status response. This may indicate a synchronization issue with the backend."
|
|
392
|
-
)
|
|
383
|
+
best_solution_node = get_best_node_from_status(status_response=status_response)
|
|
384
|
+
current_solution_node = get_node_from_status(
|
|
385
|
+
status_response=status_response, solution_id=eval_and_next_solution_response["solution_id"]
|
|
386
|
+
)
|
|
393
387
|
|
|
394
388
|
# Update the solution panels with the current and best solution
|
|
395
389
|
solution_panels.update(current_node=current_solution_node, best_node=best_solution_node)
|
|
@@ -426,17 +420,15 @@ def execute_optimization(
|
|
|
426
420
|
# Evaluate the final solution thats been generated
|
|
427
421
|
eval_and_next_solution_response = evaluate_feedback_then_suggest_next_solution(
|
|
428
422
|
console=console,
|
|
423
|
+
step=steps,
|
|
429
424
|
run_id=run_id,
|
|
430
425
|
execution_output=term_out,
|
|
431
426
|
additional_instructions=current_additional_instructions,
|
|
432
|
-
api_keys=llm_api_keys,
|
|
433
|
-
timeout=api_timeout,
|
|
434
427
|
auth_headers=auth_headers,
|
|
435
428
|
)
|
|
436
429
|
summary_panel.set_step(step=steps)
|
|
437
|
-
summary_panel.update_token_counts(usage=eval_and_next_solution_response["usage"])
|
|
438
430
|
status_response = get_optimization_run_status(
|
|
439
|
-
console=console, run_id=run_id, include_history=True,
|
|
431
|
+
console=console, run_id=run_id, include_history=True, auth_headers=auth_headers
|
|
440
432
|
)
|
|
441
433
|
# No need to update the plan panel since we have finished the optimization
|
|
442
434
|
# Get the optimization run status for
|
|
@@ -450,16 +442,7 @@ def execute_optimization(
|
|
|
450
442
|
# No neeed to update the current solution panel since we have finished the optimization
|
|
451
443
|
# We only need to update the best solution panel
|
|
452
444
|
# Figure out if we have a best solution so far
|
|
453
|
-
|
|
454
|
-
best_solution_node = Node(
|
|
455
|
-
id=status_response["best_result"]["solution_id"],
|
|
456
|
-
parent_id=status_response["best_result"]["parent_id"],
|
|
457
|
-
code=status_response["best_result"]["code"],
|
|
458
|
-
metric=status_response["best_result"]["metric_value"],
|
|
459
|
-
is_buggy=status_response["best_result"]["is_buggy"],
|
|
460
|
-
)
|
|
461
|
-
else:
|
|
462
|
-
best_solution_node = None
|
|
445
|
+
best_solution_node = get_best_node_from_status(status_response=status_response)
|
|
463
446
|
solution_panels.update(current_node=None, best_node=best_solution_node)
|
|
464
447
|
_, best_solution_panel = solution_panels.get_display(current_step=steps)
|
|
465
448
|
# Update the end optimization layout
|
|
@@ -476,24 +459,10 @@ def execute_optimization(
|
|
|
476
459
|
# If the best solution does not exist or is has not been measured at the end of the optimization
|
|
477
460
|
# save the original solution as the best solution
|
|
478
461
|
if best_solution_node is not None:
|
|
479
|
-
|
|
480
|
-
best_solution_score = best_solution_node.metric
|
|
462
|
+
best_solution_content = best_solution_node.code
|
|
481
463
|
else:
|
|
482
|
-
|
|
483
|
-
best_solution_score = None
|
|
464
|
+
best_solution_content = read_from_path(fp=runs_dir / f"step_0{source_fp.suffix}", is_json=False)
|
|
484
465
|
|
|
485
|
-
if best_solution_code is None or best_solution_score is None:
|
|
486
|
-
best_solution_content = f"# Weco could not find a better solution\n\n{read_from_path(fp=runs_dir / f'step_0{source_fp.suffix}', is_json=False)}"
|
|
487
|
-
else:
|
|
488
|
-
# Format score for the comment
|
|
489
|
-
best_score_str = (
|
|
490
|
-
format_number(best_solution_score)
|
|
491
|
-
if best_solution_score is not None and isinstance(best_solution_score, (int, float))
|
|
492
|
-
else "N/A"
|
|
493
|
-
)
|
|
494
|
-
best_solution_content = (
|
|
495
|
-
f"# Best solution from Weco with a score of {best_score_str}\n\n{best_solution_code}"
|
|
496
|
-
)
|
|
497
466
|
# Save best solution to .runs/<run-id>/best.<extension>
|
|
498
467
|
write_to_path(fp=runs_dir / f"best{source_fp.suffix}", content=best_solution_content)
|
|
499
468
|
# write the best solution to the source file
|
|
@@ -509,6 +478,7 @@ def execute_optimization(
|
|
|
509
478
|
except Exception:
|
|
510
479
|
error_message = str(e)
|
|
511
480
|
console.print(Panel(f"[bold red]Error: {error_message}", title="[bold red]Optimization Error", border_style="red"))
|
|
481
|
+
console.print(f"\n[cyan]To resume this run, use:[/] [bold cyan]weco resume {run_id}[/]")
|
|
512
482
|
# Ensure optimization_completed_normally is False
|
|
513
483
|
optimization_completed_normally = False
|
|
514
484
|
finally:
|
|
@@ -535,10 +505,366 @@ def execute_optimization(
|
|
|
535
505
|
else "CLI terminated unexpectedly without a specific exception captured."
|
|
536
506
|
)
|
|
537
507
|
|
|
538
|
-
report_termination(
|
|
508
|
+
report_termination(
|
|
509
|
+
run_id=run_id,
|
|
510
|
+
status_update=status,
|
|
511
|
+
reason=reason,
|
|
512
|
+
details=details,
|
|
513
|
+
auth_headers=current_auth_headers_for_heartbeat,
|
|
514
|
+
)
|
|
539
515
|
|
|
540
516
|
# Handle exit
|
|
541
517
|
if user_stop_requested_flag:
|
|
542
518
|
console.print("[yellow]Run terminated by user request.[/]")
|
|
519
|
+
console.print(f"\n[cyan]To resume this run, use:[/] [bold cyan]weco resume {run_id}[/]")
|
|
520
|
+
|
|
521
|
+
return optimization_completed_normally or user_stop_requested_flag
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def resume_optimization(run_id: str, console: Optional[Console] = None) -> bool:
|
|
525
|
+
"""Resume an interrupted run from the most recent node and continue optimization."""
|
|
526
|
+
if console is None:
|
|
527
|
+
console = Console()
|
|
528
|
+
|
|
529
|
+
# Globals for this optimization run
|
|
530
|
+
heartbeat_thread = None
|
|
531
|
+
stop_heartbeat_event = threading.Event()
|
|
532
|
+
current_run_id_for_heartbeat = None
|
|
533
|
+
current_auth_headers_for_heartbeat = {}
|
|
534
|
+
|
|
535
|
+
# Signal handler for this optimization run
|
|
536
|
+
def signal_handler(signum, frame):
|
|
537
|
+
signal_name = signal.Signals(signum).name
|
|
538
|
+
console.print(f"\n[bold yellow]Termination signal ({signal_name}) received. Shutting down...[/]")
|
|
539
|
+
stop_heartbeat_event.set()
|
|
540
|
+
if heartbeat_thread and heartbeat_thread.is_alive():
|
|
541
|
+
heartbeat_thread.join(timeout=2)
|
|
542
|
+
if current_run_id_for_heartbeat:
|
|
543
|
+
report_termination(
|
|
544
|
+
run_id=current_run_id_for_heartbeat,
|
|
545
|
+
status_update="terminated",
|
|
546
|
+
reason=f"user_terminated_{signal_name.lower()}",
|
|
547
|
+
details=f"Process terminated by signal {signal_name} ({signum}).",
|
|
548
|
+
auth_headers=current_auth_headers_for_heartbeat,
|
|
549
|
+
)
|
|
550
|
+
console.print(f"\n[cyan]To resume this run, use:[/] [bold cyan]weco resume {current_run_id_for_heartbeat}[/]")
|
|
551
|
+
sys.exit(0)
|
|
552
|
+
|
|
553
|
+
# Set up signal handlers for this run
|
|
554
|
+
original_sigint_handler = signal.signal(signal.SIGINT, signal_handler)
|
|
555
|
+
original_sigterm_handler = signal.signal(signal.SIGTERM, signal_handler)
|
|
556
|
+
|
|
557
|
+
optimization_completed_normally = False
|
|
558
|
+
user_stop_requested_flag = False
|
|
559
|
+
|
|
560
|
+
try:
|
|
561
|
+
# --- Login/Authentication Handling (now mandatory) ---
|
|
562
|
+
weco_api_key, auth_headers = handle_authentication(console)
|
|
563
|
+
if weco_api_key is None:
|
|
564
|
+
# Authentication failed or user declined
|
|
565
|
+
return False
|
|
566
|
+
|
|
567
|
+
current_auth_headers_for_heartbeat = auth_headers
|
|
568
|
+
|
|
569
|
+
# Fetch status first for validation and to display confirmation info
|
|
570
|
+
try:
|
|
571
|
+
status = get_optimization_run_status(
|
|
572
|
+
console=console, run_id=run_id, include_history=True, auth_headers=auth_headers
|
|
573
|
+
)
|
|
574
|
+
except Exception as e:
|
|
575
|
+
console.print(
|
|
576
|
+
Panel(f"[bold red]Error fetching run status: {e}", title="[bold red]Resume Error", border_style="red")
|
|
577
|
+
)
|
|
578
|
+
return False
|
|
579
|
+
|
|
580
|
+
run_status_val = status.get("status")
|
|
581
|
+
if run_status_val not in ("error", "terminated"):
|
|
582
|
+
console.print(
|
|
583
|
+
Panel(
|
|
584
|
+
f"Run {run_id} cannot be resumed (status: {run_status_val}). Only 'error' or 'terminated' runs can be resumed.",
|
|
585
|
+
title="[bold yellow]Resume Not Allowed",
|
|
586
|
+
border_style="yellow",
|
|
587
|
+
)
|
|
588
|
+
)
|
|
589
|
+
return False
|
|
590
|
+
|
|
591
|
+
objective = status.get("objective", {})
|
|
592
|
+
metric_name = objective.get("metric_name", "metric")
|
|
593
|
+
maximize = bool(objective.get("maximize", True))
|
|
594
|
+
|
|
595
|
+
optimizer = status.get("optimizer", {})
|
|
596
|
+
|
|
597
|
+
console.print("[cyan]Resume Run Confirmation[/]")
|
|
598
|
+
console.print(f" Run ID: {run_id}")
|
|
599
|
+
console.print(f" Run Name: {status.get('run_name', 'N/A')}")
|
|
600
|
+
console.print(f" Status: {run_status_val}")
|
|
601
|
+
# Objective and model
|
|
602
|
+
console.print(f" Objective: {metric_name} ({'maximize' if maximize else 'minimize'})")
|
|
603
|
+
model_name = (
|
|
604
|
+
(optimizer.get("code_generator") or {}).get("model")
|
|
605
|
+
or (optimizer.get("evaluator") or {}).get("model")
|
|
606
|
+
or "unknown"
|
|
607
|
+
)
|
|
608
|
+
console.print(f" Model: {model_name}")
|
|
609
|
+
console.print(f" Eval Command: {objective.get('evaluation_command', 'N/A')}")
|
|
610
|
+
# Steps summary
|
|
611
|
+
total_steps = optimizer.get("steps")
|
|
612
|
+
current_step = int(status["current_step"])
|
|
613
|
+
steps_remaining = int(total_steps) - int(current_step)
|
|
614
|
+
console.print(f" Total Steps: {total_steps} | Resume Step: {current_step} | Steps Remaining: {steps_remaining}")
|
|
615
|
+
console.print(f" Last Updated: {status.get('updated_at', 'N/A')}")
|
|
616
|
+
unchanged = Confirm.ask(
|
|
617
|
+
"Have you kept the source file and evaluation command unchanged since the original run?", default=True
|
|
618
|
+
)
|
|
619
|
+
if not unchanged:
|
|
620
|
+
console.print("[yellow]Resume cancelled. Please start a new run if the environment changed.[/]")
|
|
621
|
+
return False
|
|
622
|
+
|
|
623
|
+
# Call backend to prepare resume
|
|
624
|
+
resume_resp = resume_optimization_run(console=console, run_id=run_id, auth_headers=auth_headers)
|
|
625
|
+
if resume_resp is None:
|
|
626
|
+
return False
|
|
627
|
+
|
|
628
|
+
eval_command = resume_resp["evaluation_command"]
|
|
629
|
+
source_path = resume_resp.get("source_path")
|
|
630
|
+
|
|
631
|
+
# Use backend-saved values
|
|
632
|
+
log_dir = resume_resp.get("log_dir", ".runs")
|
|
633
|
+
save_logs = bool(resume_resp.get("save_logs", False))
|
|
634
|
+
eval_timeout = resume_resp.get("eval_timeout")
|
|
635
|
+
additional_instructions = resume_resp.get("additional_instructions")
|
|
636
|
+
|
|
637
|
+
# Write last solution code to source path
|
|
638
|
+
source_fp = pathlib.Path(source_path)
|
|
639
|
+
source_fp.parent.mkdir(parents=True, exist_ok=True)
|
|
640
|
+
code_to_restore = resume_resp.get("code") or resume_resp.get("source_code") or ""
|
|
641
|
+
write_to_path(fp=source_fp, content=code_to_restore)
|
|
642
|
+
|
|
643
|
+
# Prepare UI panels
|
|
644
|
+
summary_panel = SummaryPanel(
|
|
645
|
+
maximize=maximize, metric_name=metric_name, total_steps=total_steps, model=model_name, runs_dir=log_dir
|
|
646
|
+
)
|
|
647
|
+
summary_panel.set_run_id(run_id=resume_resp["run_id"])
|
|
648
|
+
if resume_resp.get("run_name"):
|
|
649
|
+
summary_panel.set_run_name(resume_resp.get("run_name"))
|
|
650
|
+
summary_panel.set_step(step=current_step)
|
|
651
|
+
summary_panel.update_thinking(resume_resp.get("plan"))
|
|
652
|
+
|
|
653
|
+
solution_panels = SolutionPanels(metric_name=metric_name, source_fp=source_fp)
|
|
654
|
+
eval_output_panel = EvaluationOutputPanel()
|
|
655
|
+
tree_panel = MetricTreePanel(maximize=maximize)
|
|
656
|
+
layout = create_optimization_layout()
|
|
657
|
+
end_optimization_layout = create_end_optimization_layout()
|
|
658
|
+
|
|
659
|
+
# Build tree from nodes returned by status (history)
|
|
660
|
+
nodes_list_from_status = status.get("nodes") or []
|
|
661
|
+
tree_panel.build_metric_tree(nodes=nodes_list_from_status)
|
|
662
|
+
|
|
663
|
+
# Compute best and current nodes
|
|
664
|
+
best_solution_node = get_best_node_from_status(status_response=status)
|
|
665
|
+
current_solution_node = get_node_from_status(status_response=status, solution_id=resume_resp.get("solution_id"))
|
|
666
|
+
|
|
667
|
+
# Ensure runs dir exists
|
|
668
|
+
runs_dir = pathlib.Path(log_dir) / resume_resp["run_id"]
|
|
669
|
+
runs_dir.mkdir(parents=True, exist_ok=True)
|
|
670
|
+
# Persist last step's code into logs as step_<current_step>
|
|
671
|
+
write_to_path(fp=runs_dir / f"step_{current_step}{source_fp.suffix}", content=code_to_restore)
|
|
672
|
+
|
|
673
|
+
# Start Heartbeat Thread
|
|
674
|
+
stop_heartbeat_event.clear()
|
|
675
|
+
heartbeat_thread = HeartbeatSender(resume_resp["run_id"], auth_headers, stop_heartbeat_event)
|
|
676
|
+
heartbeat_thread.start()
|
|
677
|
+
current_run_id_for_heartbeat = resume_resp["run_id"]
|
|
678
|
+
|
|
679
|
+
# Seed solution panels with current and best nodes
|
|
680
|
+
solution_panels.update(current_node=current_solution_node, best_node=best_solution_node)
|
|
681
|
+
|
|
682
|
+
# --- Live UI ---
|
|
683
|
+
refresh_rate = 4
|
|
684
|
+
with Live(layout, refresh_per_second=refresh_rate) as live:
|
|
685
|
+
# Initial panels
|
|
686
|
+
current_solution_panel, best_solution_panel = solution_panels.get_display(current_step=current_step)
|
|
687
|
+
# Use backend-provided execution output only (no fallback)
|
|
688
|
+
term_out = resume_resp.get("execution_output") or ""
|
|
689
|
+
eval_output_panel.update(output=term_out)
|
|
690
|
+
|
|
691
|
+
# Update the initial panels
|
|
692
|
+
smooth_update(
|
|
693
|
+
live=live,
|
|
694
|
+
layout=layout,
|
|
695
|
+
sections_to_update=[
|
|
696
|
+
("summary", summary_panel.get_display()),
|
|
697
|
+
("tree", tree_panel.get_display(is_done=False)),
|
|
698
|
+
("current_solution", current_solution_panel),
|
|
699
|
+
("best_solution", best_solution_panel),
|
|
700
|
+
("eval_output", eval_output_panel.get_display()),
|
|
701
|
+
],
|
|
702
|
+
transition_delay=0.1,
|
|
703
|
+
)
|
|
543
704
|
|
|
705
|
+
# If missing output, evaluate once before first suggest
|
|
706
|
+
if term_out is None or len(term_out.strip()) == 0:
|
|
707
|
+
term_out = run_evaluation(eval_command=eval_command, timeout=eval_timeout)
|
|
708
|
+
eval_output_panel.update(output=term_out)
|
|
709
|
+
# Update the evaluation output panel
|
|
710
|
+
smooth_update(
|
|
711
|
+
live=live,
|
|
712
|
+
layout=layout,
|
|
713
|
+
sections_to_update=[("eval_output", eval_output_panel.get_display())],
|
|
714
|
+
transition_delay=0.1,
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
if save_logs:
|
|
718
|
+
save_execution_output(runs_dir, step=current_step, output=term_out)
|
|
719
|
+
|
|
720
|
+
# Continue optimization: steps current_step+1..total_steps
|
|
721
|
+
for step in range(current_step + 1, total_steps + 1):
|
|
722
|
+
# Stop polling
|
|
723
|
+
try:
|
|
724
|
+
current_status_response = get_optimization_run_status(
|
|
725
|
+
console=console, run_id=resume_resp["run_id"], include_history=False, auth_headers=auth_headers
|
|
726
|
+
)
|
|
727
|
+
if current_status_response.get("status") == "stopping":
|
|
728
|
+
console.print("\n[bold yellow]Stop request received. Terminating run gracefully...[/]")
|
|
729
|
+
user_stop_requested_flag = True
|
|
730
|
+
break
|
|
731
|
+
except requests.exceptions.RequestException as e:
|
|
732
|
+
console.print(f"\n[bold red]Warning: Unable to check run status: {e}. Continuing optimization...[/]")
|
|
733
|
+
except Exception as e:
|
|
734
|
+
console.print(f"\n[bold red]Warning: Error checking run status: {e}. Continuing optimization...[/]")
|
|
735
|
+
|
|
736
|
+
# Suggest next
|
|
737
|
+
eval_and_next_solution_response = evaluate_feedback_then_suggest_next_solution(
|
|
738
|
+
console=console,
|
|
739
|
+
step=step,
|
|
740
|
+
run_id=resume_resp["run_id"],
|
|
741
|
+
execution_output=term_out,
|
|
742
|
+
additional_instructions=additional_instructions,
|
|
743
|
+
auth_headers=auth_headers,
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
# Save next solution file(s)
|
|
747
|
+
write_to_path(fp=runs_dir / f"step_{step}{source_fp.suffix}", content=eval_and_next_solution_response["code"])
|
|
748
|
+
write_to_path(fp=source_fp, content=eval_and_next_solution_response["code"])
|
|
749
|
+
|
|
750
|
+
# Refresh status with history and update panels
|
|
751
|
+
status_response = get_optimization_run_status(
|
|
752
|
+
console=console, run_id=resume_resp["run_id"], include_history=True, auth_headers=auth_headers
|
|
753
|
+
)
|
|
754
|
+
summary_panel.set_step(step=step)
|
|
755
|
+
summary_panel.update_thinking(thinking=eval_and_next_solution_response.get("plan", ""))
|
|
756
|
+
nodes_list = status_response.get("nodes") or []
|
|
757
|
+
tree_panel.build_metric_tree(nodes=nodes_list)
|
|
758
|
+
tree_panel.set_unevaluated_node(node_id=eval_and_next_solution_response["solution_id"])
|
|
759
|
+
best_solution_node = get_best_node_from_status(status_response=status_response)
|
|
760
|
+
current_solution_node = get_node_from_status(
|
|
761
|
+
status_response=status_response, solution_id=eval_and_next_solution_response["solution_id"]
|
|
762
|
+
)
|
|
763
|
+
solution_panels.update(current_node=current_solution_node, best_node=best_solution_node)
|
|
764
|
+
current_solution_panel, best_solution_panel = solution_panels.get_display(current_step=step)
|
|
765
|
+
eval_output_panel.clear()
|
|
766
|
+
smooth_update(
|
|
767
|
+
live=live,
|
|
768
|
+
layout=layout,
|
|
769
|
+
sections_to_update=[
|
|
770
|
+
("summary", summary_panel.get_display()),
|
|
771
|
+
("tree", tree_panel.get_display(is_done=False)),
|
|
772
|
+
("current_solution", current_solution_panel),
|
|
773
|
+
("best_solution", best_solution_panel),
|
|
774
|
+
("eval_output", eval_output_panel.get_display()),
|
|
775
|
+
],
|
|
776
|
+
transition_delay=0.08,
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
# Evaluate this new solution
|
|
780
|
+
term_out = run_evaluation(eval_command=eval_command, timeout=eval_timeout)
|
|
781
|
+
if save_logs:
|
|
782
|
+
save_execution_output(runs_dir, step=step, output=term_out)
|
|
783
|
+
eval_output_panel.update(output=term_out)
|
|
784
|
+
smooth_update(
|
|
785
|
+
live=live,
|
|
786
|
+
layout=layout,
|
|
787
|
+
sections_to_update=[("eval_output", eval_output_panel.get_display())],
|
|
788
|
+
transition_delay=0.1,
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
# Final flush if not stopped
|
|
792
|
+
if not user_stop_requested_flag:
|
|
793
|
+
eval_and_next_solution_response = evaluate_feedback_then_suggest_next_solution(
|
|
794
|
+
console=console,
|
|
795
|
+
step=total_steps,
|
|
796
|
+
run_id=resume_resp["run_id"],
|
|
797
|
+
execution_output=term_out,
|
|
798
|
+
additional_instructions=additional_instructions,
|
|
799
|
+
auth_headers=auth_headers,
|
|
800
|
+
)
|
|
801
|
+
summary_panel.set_step(step=total_steps)
|
|
802
|
+
status_response = get_optimization_run_status(
|
|
803
|
+
console=console, run_id=resume_resp["run_id"], include_history=True, auth_headers=auth_headers
|
|
804
|
+
)
|
|
805
|
+
nodes_final = status_response.get("nodes") or []
|
|
806
|
+
tree_panel.build_metric_tree(nodes=nodes_final)
|
|
807
|
+
# Best solution panel and final message
|
|
808
|
+
best_solution_node = get_best_node_from_status(status_response=status_response)
|
|
809
|
+
solution_panels.update(current_node=None, best_node=best_solution_node)
|
|
810
|
+
_, best_solution_panel = solution_panels.get_display(current_step=total_steps)
|
|
811
|
+
final_message = (
|
|
812
|
+
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']}[/] 🏆"
|
|
813
|
+
if best_solution_node is not None and best_solution_node.metric is not None
|
|
814
|
+
else "[red] No valid solution found.[/]"
|
|
815
|
+
)
|
|
816
|
+
end_optimization_layout["summary"].update(summary_panel.get_display(final_message=final_message))
|
|
817
|
+
end_optimization_layout["tree"].update(tree_panel.get_display(is_done=True))
|
|
818
|
+
end_optimization_layout["best_solution"].update(best_solution_panel)
|
|
819
|
+
|
|
820
|
+
# Save best
|
|
821
|
+
if best_solution_node is not None:
|
|
822
|
+
best_solution_content = best_solution_node.code
|
|
823
|
+
else:
|
|
824
|
+
best_solution_content = read_from_path(fp=runs_dir / f"step_0{source_fp.suffix}", is_json=False)
|
|
825
|
+
|
|
826
|
+
write_to_path(fp=runs_dir / f"best{source_fp.suffix}", content=best_solution_content)
|
|
827
|
+
write_to_path(fp=source_fp, content=best_solution_content)
|
|
828
|
+
optimization_completed_normally = True
|
|
829
|
+
live.update(end_optimization_layout)
|
|
830
|
+
|
|
831
|
+
except Exception as e:
|
|
832
|
+
try:
|
|
833
|
+
error_message = e.response.json()["detail"]
|
|
834
|
+
except Exception:
|
|
835
|
+
error_message = str(e)
|
|
836
|
+
console.print(Panel(f"[bold red]Error: {error_message}", title="[bold red]Optimization Error", border_style="red"))
|
|
837
|
+
console.print(f"\n[cyan]To resume this run, use:[/] [bold cyan]weco resume {run_id}[/]")
|
|
838
|
+
optimization_completed_normally = False
|
|
839
|
+
finally:
|
|
840
|
+
signal.signal(signal.SIGINT, original_sigint_handler)
|
|
841
|
+
signal.signal(signal.SIGTERM, original_sigterm_handler)
|
|
842
|
+
stop_heartbeat_event.set()
|
|
843
|
+
if heartbeat_thread and heartbeat_thread.is_alive():
|
|
844
|
+
heartbeat_thread.join(timeout=2)
|
|
845
|
+
|
|
846
|
+
run_id = resume_resp.get("run_id")
|
|
847
|
+
# Report final status if run exists
|
|
848
|
+
if run_id:
|
|
849
|
+
if optimization_completed_normally:
|
|
850
|
+
status, reason, details = "completed", "completed_successfully", None
|
|
851
|
+
elif user_stop_requested_flag:
|
|
852
|
+
status, reason, details = "terminated", "user_requested_stop", "Run stopped by user request via dashboard."
|
|
853
|
+
else:
|
|
854
|
+
status, reason = "error", "error_cli_internal"
|
|
855
|
+
details = locals().get("error_details") or (
|
|
856
|
+
traceback.format_exc()
|
|
857
|
+
if "e" in locals() and isinstance(locals()["e"], Exception)
|
|
858
|
+
else "CLI terminated unexpectedly without a specific exception captured."
|
|
859
|
+
)
|
|
860
|
+
report_termination(
|
|
861
|
+
run_id=run_id,
|
|
862
|
+
status_update=status,
|
|
863
|
+
reason=reason,
|
|
864
|
+
details=details,
|
|
865
|
+
auth_headers=current_auth_headers_for_heartbeat,
|
|
866
|
+
)
|
|
867
|
+
if user_stop_requested_flag:
|
|
868
|
+
console.print("[yellow]Run terminated by user request.[/]")
|
|
869
|
+
console.print(f"\n[cyan]To resume this run, use:[/] [bold cyan]weco resume {run_id}[/]")
|
|
544
870
|
return optimization_completed_normally or user_stop_requested_flag
|