Skip to content

Commit 57a9408

Browse files
authored
Merge pull request #96 from WecoAI/dev (bump version 0.3.5)
Version control fixes: handling of interrupts, more reliable printing of messages
2 parents 1718023 + 5faf6fb commit 57a9408

File tree

3 files changed

+78
-73
lines changed

3 files changed

+78
-73
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ name = "weco"
88
authors = [{ name = "Weco AI Team", email = "contact@weco.ai" }]
99
description = "Documentation for `weco`, a CLI for using Weco AI's code optimizer."
1010
readme = "README.md"
11-
version = "0.3.4"
11+
version = "0.3.5"
1212
license = { file = "LICENSE" }
1313
requires-python = ">=3.8"
1414
dependencies = [

weco/optimizer.py

Lines changed: 77 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -144,17 +144,25 @@ def execute_optimization(
144144
"""
145145
if console is None:
146146
console = Console()
147-
148147
# Global variables for this optimization run
149148
heartbeat_thread = None
150149
stop_heartbeat_event = threading.Event()
151150
current_run_id_for_heartbeat = None
152151
current_auth_headers_for_heartbeat = {}
152+
live_ref = None # Reference to the Live object for the optimization run
153+
154+
best_solution_code = None
155+
original_source_code = None
153156

154157
# --- Signal Handler for this optimization run ---
155158
def signal_handler(signum, frame):
159+
nonlocal live_ref
160+
161+
if live_ref is not None:
162+
live_ref.stop() # Stop the live update loop so that messages are printed to the console
163+
156164
signal_name = signal.Signals(signum).name
157-
console.print(f"\n[bold yellow]Termination signal ({signal_name}) received. Shutting down...[/]")
165+
console.print(f"\n[bold yellow]Termination signal ({signal_name}) received. Shutting down...[/]\n")
158166

159167
# Stop heartbeat thread
160168
stop_heartbeat_event.set()
@@ -170,7 +178,7 @@ def signal_handler(signum, frame):
170178
details=f"Process terminated by signal {signal_name} ({signum}).",
171179
auth_headers=current_auth_headers_for_heartbeat,
172180
)
173-
console.print(f"\n[cyan]To resume this run, use:[/] [bold cyan]weco resume {current_run_id_for_heartbeat}[/]")
181+
console.print(f"[cyan]To resume this run, use:[/] [bold cyan]weco resume {current_run_id_for_heartbeat}[/]\n")
174182

175183
# Exit gracefully
176184
sys.exit(0)
@@ -183,8 +191,6 @@ def signal_handler(signum, frame):
183191
optimization_completed_normally = False
184192
user_stop_requested_flag = False
185193

186-
best_solution_code = None
187-
original_source_code = None # Make available to the finally block
188194
try:
189195
# --- Login/Authentication Handling (now mandatory) ---
190196
weco_api_key, auth_headers = handle_authentication(console)
@@ -256,6 +262,7 @@ def signal_handler(signum, frame):
256262
# --- Live Update Loop ---
257263
refresh_rate = 4
258264
with Live(layout, refresh_per_second=refresh_rate) as live:
265+
live_ref = live
259266
# Define the runs directory (.runs/<run-id>) to store logs and results
260267
runs_dir = pathlib.Path(log_dir) / run_id
261268
runs_dir.mkdir(parents=True, exist_ok=True)
@@ -387,6 +394,16 @@ def signal_handler(signum, frame):
387394
status_response=status_response, solution_id=eval_and_next_solution_response["solution_id"]
388395
)
389396

397+
# Set best solution and save optimization results
398+
try:
399+
best_solution_code = best_solution_node.code
400+
except AttributeError:
401+
# Can happen if the code was buggy
402+
best_solution_code = read_from_path(fp=runs_dir / f"step_0{source_fp.suffix}", is_json=False)
403+
404+
# Save best solution to .runs/<run-id>/best.<extension>
405+
write_to_path(fp=runs_dir / f"best{source_fp.suffix}", content=best_solution_code)
406+
390407
# Update the solution panels with the current and best solution
391408
solution_panels.update(current_node=current_solution_node, best_node=best_solution_node)
392409
current_solution_panel, best_solution_panel = solution_panels.get_display(current_step=step)
@@ -443,10 +460,13 @@ def signal_handler(signum, frame):
443460
)
444461
# No need to set any solution to unevaluated since we have finished the optimization
445462
# and all solutions have been evaluated
446-
# No neeed to update the current solution panel since we have finished the optimization
463+
# No need to update the current solution panel since we have finished the optimization
447464
# We only need to update the best solution panel
448465
# Figure out if we have a best solution so far
449466
best_solution_node = get_best_node_from_status(status_response=status_response)
467+
best_solution_code = best_solution_node.code
468+
# Save best solution to .runs/<run-id>/best.<extension>
469+
write_to_path(fp=runs_dir / f"best{source_fp.suffix}", content=best_solution_code)
450470
solution_panels.update(current_node=None, best_node=best_solution_node)
451471
_, best_solution_panel = solution_panels.get_display(current_step=steps)
452472
# Update the end optimization layout
@@ -459,17 +479,6 @@ def signal_handler(signum, frame):
459479
end_optimization_layout["tree"].update(tree_panel.get_display(is_done=True))
460480
end_optimization_layout["best_solution"].update(best_solution_panel)
461481

462-
# Save optimization results
463-
# If the best solution does not exist or is has not been measured at the end of the optimization
464-
# save the original solution as the best solution
465-
try:
466-
best_solution_code = best_solution_node.code
467-
except AttributeError:
468-
best_solution_code = read_from_path(fp=runs_dir / f"step_0{source_fp.suffix}", is_json=False)
469-
470-
# Save best solution to .runs/<run-id>/best.<extension>
471-
write_to_path(fp=runs_dir / f"best{source_fp.suffix}", content=best_solution_code)
472-
473482
# Mark as completed normally for the finally block
474483
optimization_completed_normally = True
475484
live.update(end_optimization_layout)
@@ -481,7 +490,7 @@ def signal_handler(signum, frame):
481490
except Exception:
482491
error_message = str(e)
483492
console.print(Panel(f"[bold red]Error: {error_message}", title="[bold red]Optimization Error", border_style="red"))
484-
console.print(f"\n[cyan]To resume this run, use:[/] [bold cyan]weco resume {run_id}[/]")
493+
console.print(f"\n[cyan]To resume this run, use:[/] [bold cyan]weco resume {run_id}[/]\n")
485494
# Ensure optimization_completed_normally is False
486495
optimization_completed_normally = False
487496
finally:
@@ -508,20 +517,16 @@ def signal_handler(signum, frame):
508517
else "CLI terminated unexpectedly without a specific exception captured."
509518
)
510519

511-
# raise Exception(best_solution_code, original_source_code)
512520
if best_solution_code and best_solution_code != original_source_code:
513521
# Determine whether to apply: automatically if --apply-change is set, otherwise ask user
514-
should_apply = apply_change or summary_panel.ask_user_feedback(
515-
live=live,
516-
layout=end_optimization_layout,
517-
question="Would you like to apply the best solution to the source file?",
518-
default=True,
522+
should_apply = apply_change or Confirm.ask(
523+
"Would you like to apply the best solution to the source file?", default=True
519524
)
520525
if should_apply:
521526
write_to_path(fp=source_fp, content=best_solution_code)
522-
console.print("[green]Best solution applied to the source file.[/]\n")
527+
console.print("\n[green]Best solution applied to the source file.[/]\n")
523528
else:
524-
console.print("[green]A better solution was not found. No changes to apply.[/]\n")
529+
console.print("\n[green]A better solution was not found. No changes to apply.[/]\n")
525530

526531
report_termination(
527532
run_id=run_id,
@@ -534,7 +539,7 @@ def signal_handler(signum, frame):
534539
# Handle exit
535540
if user_stop_requested_flag:
536541
console.print("[yellow]Run terminated by user request.[/]")
537-
console.print(f"\n[cyan]To resume this run, use:[/] [bold cyan]weco resume {run_id}[/]")
542+
console.print(f"\n[cyan]To resume this run, use:[/] [bold cyan]weco resume {run_id}[/]\n")
538543

539544
return optimization_completed_normally or user_stop_requested_flag
540545

@@ -549,11 +554,19 @@ def resume_optimization(run_id: str, console: Optional[Console] = None, apply_ch
549554
stop_heartbeat_event = threading.Event()
550555
current_run_id_for_heartbeat = None
551556
current_auth_headers_for_heartbeat = {}
557+
live_ref = None # Reference to the Live object for the optimization run
558+
559+
best_solution_code = None
560+
original_source_code = None
552561

553562
# Signal handler for this optimization run
554563
def signal_handler(signum, frame):
564+
nonlocal live_ref
565+
if live_ref is not None:
566+
live_ref.stop() # Stop the live update loop so that messages are printed to the console
567+
555568
signal_name = signal.Signals(signum).name
556-
console.print(f"\n[bold yellow]Termination signal ({signal_name}) received. Shutting down...[/]")
569+
console.print(f"\n[bold yellow]Termination signal ({signal_name}) received. Shutting down...[/]\n")
557570
stop_heartbeat_event.set()
558571
if heartbeat_thread and heartbeat_thread.is_alive():
559572
heartbeat_thread.join(timeout=2)
@@ -565,7 +578,7 @@ def signal_handler(signum, frame):
565578
details=f"Process terminated by signal {signal_name} ({signum}).",
566579
auth_headers=current_auth_headers_for_heartbeat,
567580
)
568-
console.print(f"\n[cyan]To resume this run, use:[/] [bold cyan]weco resume {current_run_id_for_heartbeat}[/]")
581+
console.print(f"\n[cyan]To resume this run, use:[/] [bold cyan]weco resume {current_run_id_for_heartbeat}[/]\n")
569582
sys.exit(0)
570583

571584
# Set up signal handlers for this run
@@ -575,9 +588,6 @@ def signal_handler(signum, frame):
575588
optimization_completed_normally = False
576589
user_stop_requested_flag = False
577590

578-
best_solution_code = None
579-
original_source_code = None
580-
581591
try:
582592
# --- Login/Authentication Handling (now mandatory) ---
583593
weco_api_key, auth_headers = handle_authentication(console)
@@ -659,9 +669,8 @@ def signal_handler(signum, frame):
659669
source_fp.parent.mkdir(parents=True, exist_ok=True)
660670
# Store the original content to restore after each evaluation
661671
original_source_code = read_from_path(fp=source_fp, is_json=False) if source_fp.exists() else ""
662-
672+
# The code to restore is the code from the last step of the previous run
663673
code_to_restore = resume_resp.get("code") or resume_resp.get("source_code") or ""
664-
write_to_path(fp=source_fp, content=code_to_restore)
665674

666675
# Prepare UI panels
667676
summary_panel = SummaryPanel(
@@ -687,12 +696,25 @@ def signal_handler(signum, frame):
687696
best_solution_node = get_best_node_from_status(status_response=status)
688697
current_solution_node = get_node_from_status(status_response=status, solution_id=resume_resp.get("solution_id"))
689698

699+
# If there's no best solution yet (baseline evaluation didn't complete),
700+
# mark the current node as unevaluated so the tree renders correctly
701+
if best_solution_node is None:
702+
tree_panel.set_unevaluated_node(node_id=resume_resp.get("solution_id"))
703+
690704
# Ensure runs dir exists
691705
runs_dir = pathlib.Path(log_dir) / resume_resp["run_id"]
692706
runs_dir.mkdir(parents=True, exist_ok=True)
693707
# Persist last step's code into logs as step_<current_step>
694708
write_to_path(fp=runs_dir / f"step_{current_step}{source_fp.suffix}", content=code_to_restore)
695709

710+
# Initialize best solution code
711+
try:
712+
best_solution_code = best_solution_node.code
713+
except AttributeError:
714+
# Edge case: best solution node is not available.
715+
# This can happen if the user has cancelled the run before even running the baseline solution
716+
pass # Leave best solution code as None
717+
696718
# Start Heartbeat Thread
697719
stop_heartbeat_event.clear()
698720
heartbeat_thread = HeartbeatSender(resume_resp["run_id"], auth_headers, stop_heartbeat_event)
@@ -705,6 +727,7 @@ def signal_handler(signum, frame):
705727
# --- Live UI ---
706728
refresh_rate = 4
707729
with Live(layout, refresh_per_second=refresh_rate) as live:
730+
live_ref = live
708731
# Initial panels
709732
current_solution_panel, best_solution_panel = solution_panels.get_display(current_step=current_step)
710733
# Use backend-provided execution output only (no fallback)
@@ -788,6 +811,16 @@ def signal_handler(signum, frame):
788811
status_response=status_response, solution_id=eval_and_next_solution_response["solution_id"]
789812
)
790813

814+
# Set best solution and save optimization results
815+
try:
816+
best_solution_code = best_solution_node.code
817+
except AttributeError:
818+
# Can happen if the code was buggy
819+
best_solution_code = read_from_path(fp=runs_dir / f"step_0{source_fp.suffix}", is_json=False)
820+
821+
# Save best solution to .runs/<run-id>/best.<extension>
822+
write_to_path(fp=runs_dir / f"best{source_fp.suffix}", content=best_solution_code)
823+
791824
solution_panels.update(current_node=current_solution_node, best_node=best_solution_node)
792825
current_solution_panel, best_solution_panel = solution_panels.get_display(current_step=step)
793826
eval_output_panel.clear()
@@ -839,6 +872,10 @@ def signal_handler(signum, frame):
839872
tree_panel.build_metric_tree(nodes=nodes_final)
840873
# Best solution panel and final message
841874
best_solution_node = get_best_node_from_status(status_response=status_response)
875+
best_solution_code = best_solution_node.code
876+
# Save best solution to .runs/<run-id>/best.<extension>
877+
write_to_path(fp=runs_dir / f"best{source_fp.suffix}", content=best_solution_code)
878+
842879
solution_panels.update(current_node=None, best_node=best_solution_node)
843880
_, best_solution_panel = solution_panels.get_display(current_step=total_steps)
844881
final_message = (
@@ -850,14 +887,6 @@ def signal_handler(signum, frame):
850887
end_optimization_layout["tree"].update(tree_panel.get_display(is_done=True))
851888
end_optimization_layout["best_solution"].update(best_solution_panel)
852889

853-
# Save best
854-
try:
855-
best_solution_code = best_solution_node.code
856-
except AttributeError:
857-
best_solution_code = read_from_path(fp=runs_dir / f"step_0{source_fp.suffix}", is_json=False)
858-
859-
write_to_path(fp=runs_dir / f"best{source_fp.suffix}", content=best_solution_code)
860-
861890
optimization_completed_normally = True
862891
live.update(end_optimization_layout)
863892

@@ -867,7 +896,7 @@ def signal_handler(signum, frame):
867896
except Exception:
868897
error_message = str(e)
869898
console.print(Panel(f"[bold red]Error: {error_message}", title="[bold red]Optimization Error", border_style="red"))
870-
console.print(f"\n[cyan]To resume this run, use:[/] [bold cyan]weco resume {run_id}[/]")
899+
console.print(f"\n[cyan]To resume this run, use:[/] [bold cyan]weco resume {run_id}[/]\n")
871900
optimization_completed_normally = False
872901
finally:
873902
signal.signal(signal.SIGINT, original_sigint_handler)
@@ -896,17 +925,14 @@ def signal_handler(signum, frame):
896925
)
897926

898927
if best_solution_code and best_solution_code != original_source_code:
899-
should_apply = apply_change or summary_panel.ask_user_feedback(
900-
live=live,
901-
layout=end_optimization_layout,
902-
question="Would you like to apply the best solution to the source file?",
903-
default=True,
928+
should_apply = apply_change or Confirm.ask(
929+
"Would you like to apply the best solution to the source file?", default=True
904930
)
905931
if should_apply:
906932
write_to_path(fp=source_fp, content=best_solution_code)
907-
console.print("[green]Best solution applied to the source file.[/]\n")
933+
console.print("\n[green]Best solution applied to the source file.[/]\n")
908934
else:
909-
console.print("[green]A better solution was not found. No changes to apply.[/]\n")
935+
console.print("\n[green]A better solution was not found. No changes to apply.[/]\n")
910936

911937
report_termination(
912938
run_id=run_id,
@@ -917,5 +943,5 @@ def signal_handler(signum, frame):
917943
)
918944
if user_stop_requested_flag:
919945
console.print("[yellow]Run terminated by user request.[/]")
920-
console.print(f"\n[cyan]To resume this run, use:[/] [bold cyan]weco resume {run_id}[/]")
946+
console.print(f"\n[cyan]To resume this run, use:[/] [bold cyan]weco resume {run_id}[/]\n")
921947
return optimization_completed_normally or user_stop_requested_flag

weco/panels.py

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66
from rich.syntax import Syntax
77
from rich import box
88
from rich.console import Console
9-
from rich.live import Live
109

11-
from rich.prompt import Confirm
1210
from typing import Dict, List, Optional, Union, Tuple
1311
from pathlib import Path
1412
from .__init__ import __dashboard_url__
@@ -74,25 +72,6 @@ def clear_thinking(self):
7472
"""Clear the thinking content."""
7573
self.thinking_content = ""
7674

77-
def ask_user_feedback(self, live: Live, layout: Layout, question: str, default: bool = True) -> bool:
78-
"""
79-
Ask a yes/no question while keeping the main layout fixed.
80-
Uses Rich's Confirm for a clean user experience.
81-
"""
82-
# Stop live updates temporarily to prevent layout from moving
83-
live.stop()
84-
85-
try:
86-
# Use Rich's built-in Confirm
87-
result = Confirm.ask(question, default=default)
88-
except (KeyboardInterrupt, EOFError):
89-
result = default
90-
finally:
91-
# Resume live updates
92-
live.start()
93-
94-
return result
95-
9675
def get_display(self, final_message: Optional[str] = None) -> Panel:
9776
"""Return a Rich panel summarising the current run."""
9877
# ───────────────────── summary grid ──────────────────────

0 commit comments

Comments
 (0)