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