weco 0.3.3__py3-none-any.whl → 0.3.5__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/cli.py CHANGED
@@ -72,6 +72,11 @@ def configure_run_parser(run_parser: argparse.ArgumentParser) -> None:
72
72
  action="store_true",
73
73
  help="Save execution output to .runs/<run-id>/outputs/step_<n>.out.txt with JSONL index",
74
74
  )
75
+ run_parser.add_argument(
76
+ "--apply-change",
77
+ action="store_true",
78
+ help="Automatically apply the best solution to the source file without prompting",
79
+ )
75
80
 
76
81
 
77
82
  def configure_credits_parser(credits_parser: argparse.ArgumentParser) -> None:
@@ -118,6 +123,11 @@ def configure_resume_parser(resume_parser: argparse.ArgumentParser) -> None:
118
123
  resume_parser.add_argument(
119
124
  "run_id", type=str, help="The UUID of the run to resume (e.g., '0002e071-1b67-411f-a514-36947f0c4b31')"
120
125
  )
126
+ resume_parser.add_argument(
127
+ "--apply-change",
128
+ action="store_true",
129
+ help="Automatically apply the best solution to the source file without prompting",
130
+ )
121
131
 
122
132
 
123
133
  def execute_run_command(args: argparse.Namespace) -> None:
@@ -136,6 +146,7 @@ def execute_run_command(args: argparse.Namespace) -> None:
136
146
  console=console,
137
147
  eval_timeout=args.eval_timeout,
138
148
  save_logs=args.save_logs,
149
+ apply_change=args.apply_change,
139
150
  )
140
151
  exit_code = 0 if success else 1
141
152
  sys.exit(exit_code)
@@ -145,7 +156,7 @@ def execute_resume_command(args: argparse.Namespace) -> None:
145
156
  """Execute the 'weco resume' command with all its logic."""
146
157
  from .optimizer import resume_optimization
147
158
 
148
- success = resume_optimization(run_id=args.run_id, console=console)
159
+ success = resume_optimization(run_id=args.run_id, console=console, apply_change=args.apply_change)
149
160
  sys.exit(0 if success else 1)
150
161
 
151
162
 
weco/optimizer.py CHANGED
@@ -30,7 +30,7 @@ from .panels import (
30
30
  create_optimization_layout,
31
31
  create_end_optimization_layout,
32
32
  )
33
- from .utils import read_additional_instructions, read_from_path, write_to_path, run_evaluation, smooth_update
33
+ from .utils import read_additional_instructions, read_from_path, write_to_path, run_evaluation_with_file_swap, smooth_update
34
34
 
35
35
 
36
36
  def save_execution_output(runs_dir: pathlib.Path, step: int, output: str) -> None:
@@ -134,6 +134,7 @@ def execute_optimization(
134
134
  console: Optional[Console] = None,
135
135
  eval_timeout: Optional[int] = None,
136
136
  save_logs: bool = False,
137
+ apply_change: bool = False,
137
138
  ) -> bool:
138
139
  """
139
140
  Execute the core optimization logic.
@@ -143,17 +144,25 @@ def execute_optimization(
143
144
  """
144
145
  if console is None:
145
146
  console = Console()
146
-
147
147
  # Global variables for this optimization run
148
148
  heartbeat_thread = None
149
149
  stop_heartbeat_event = threading.Event()
150
150
  current_run_id_for_heartbeat = None
151
151
  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
152
156
 
153
157
  # --- Signal Handler for this optimization run ---
154
158
  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
+
155
164
  signal_name = signal.Signals(signum).name
156
- 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")
157
166
 
158
167
  # Stop heartbeat thread
159
168
  stop_heartbeat_event.set()
@@ -169,7 +178,7 @@ def execute_optimization(
169
178
  details=f"Process terminated by signal {signal_name} ({signum}).",
170
179
  auth_headers=current_auth_headers_for_heartbeat,
171
180
  )
172
- 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")
173
182
 
174
183
  # Exit gracefully
175
184
  sys.exit(0)
@@ -209,6 +218,7 @@ def execute_optimization(
209
218
  processed_additional_instructions = read_additional_instructions(additional_instructions=additional_instructions)
210
219
  source_fp = pathlib.Path(source)
211
220
  source_code = read_from_path(fp=source_fp, is_json=False)
221
+ original_source_code = source_code
212
222
 
213
223
  # --- Panel Initialization ---
214
224
  summary_panel = SummaryPanel(maximize=maximize, metric_name=metric, total_steps=steps, model=model, runs_dir=log_dir)
@@ -252,6 +262,7 @@ def execute_optimization(
252
262
  # --- Live Update Loop ---
253
263
  refresh_rate = 4
254
264
  with Live(layout, refresh_per_second=refresh_rate) as live:
265
+ live_ref = live
255
266
  # Define the runs directory (.runs/<run-id>) to store logs and results
256
267
  runs_dir = pathlib.Path(log_dir) / run_id
257
268
  runs_dir.mkdir(parents=True, exist_ok=True)
@@ -272,10 +283,6 @@ def execute_optimization(
272
283
  }
273
284
  with open(jsonl_file, "w", encoding="utf-8") as f:
274
285
  f.write(json.dumps(metadata) + "\n")
275
- # Write the initial code string to the logs
276
- write_to_path(fp=runs_dir / f"step_0{source_fp.suffix}", content=run_response["code"])
277
- # Write the initial code string to the source file path
278
- write_to_path(fp=source_fp, content=run_response["code"])
279
286
 
280
287
  # Update the panels with the initial solution
281
288
  # Add run id and run name now that we have it
@@ -307,6 +314,7 @@ def execute_optimization(
307
314
  best_node=None,
308
315
  )
309
316
  current_solution_panel, best_solution_panel = solution_panels.get_display(current_step=0)
317
+
310
318
  # Update the live layout with the initial solution panels
311
319
  smooth_update(
312
320
  live=live,
@@ -321,8 +329,17 @@ def execute_optimization(
321
329
  transition_delay=0.1,
322
330
  )
323
331
 
324
- # Run evaluation on the initial solution
325
- term_out = run_evaluation(eval_command=eval_command, timeout=eval_timeout)
332
+ # Write the initial code string to the logs
333
+ write_to_path(fp=runs_dir / f"step_0{source_fp.suffix}", content=run_response["code"])
334
+ # Run evaluation on the initial solution (file swap ensures original is restored)
335
+ term_out = run_evaluation_with_file_swap(
336
+ file_path=source_fp,
337
+ new_content=run_response["code"],
338
+ original_content=source_code,
339
+ eval_command=eval_command,
340
+ timeout=eval_timeout,
341
+ )
342
+
326
343
  # Save logs if requested
327
344
  if save_logs:
328
345
  save_execution_output(runs_dir, step=0, output=term_out)
@@ -358,8 +375,7 @@ def execute_optimization(
358
375
  )
359
376
  # Save next solution (.runs/<run-id>/step_<step>.<extension>)
360
377
  write_to_path(fp=runs_dir / f"step_{step}{source_fp.suffix}", content=eval_and_next_solution_response["code"])
361
- # Write the next solution to the source file
362
- write_to_path(fp=source_fp, content=eval_and_next_solution_response["code"])
378
+
363
379
  status_response = get_optimization_run_status(
364
380
  console=console, run_id=run_id, include_history=True, auth_headers=auth_headers
365
381
  )
@@ -378,6 +394,16 @@ def execute_optimization(
378
394
  status_response=status_response, solution_id=eval_and_next_solution_response["solution_id"]
379
395
  )
380
396
 
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
+
381
407
  # Update the solution panels with the current and best solution
382
408
  solution_panels.update(current_node=current_solution_node, best_node=best_solution_node)
383
409
  current_solution_panel, best_solution_panel = solution_panels.get_display(current_step=step)
@@ -395,7 +421,16 @@ def execute_optimization(
395
421
  ],
396
422
  transition_delay=0.08, # Slightly longer delay for more noticeable transitions
397
423
  )
398
- term_out = run_evaluation(eval_command=eval_command, timeout=eval_timeout)
424
+
425
+ # Run evaluation and restore original code after
426
+ term_out = run_evaluation_with_file_swap(
427
+ file_path=source_fp,
428
+ new_content=eval_and_next_solution_response["code"],
429
+ original_content=source_code,
430
+ eval_command=eval_command,
431
+ timeout=eval_timeout,
432
+ )
433
+
399
434
  # Save logs if requested
400
435
  if save_logs:
401
436
  save_execution_output(runs_dir, step=step, output=term_out)
@@ -425,10 +460,13 @@ def execute_optimization(
425
460
  )
426
461
  # No need to set any solution to unevaluated since we have finished the optimization
427
462
  # and all solutions have been evaluated
428
- # 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
429
464
  # We only need to update the best solution panel
430
465
  # Figure out if we have a best solution so far
431
466
  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)
432
470
  solution_panels.update(current_node=None, best_node=best_solution_node)
433
471
  _, best_solution_panel = solution_panels.get_display(current_step=steps)
434
472
  # Update the end optimization layout
@@ -441,18 +479,6 @@ def execute_optimization(
441
479
  end_optimization_layout["tree"].update(tree_panel.get_display(is_done=True))
442
480
  end_optimization_layout["best_solution"].update(best_solution_panel)
443
481
 
444
- # Save optimization results
445
- # If the best solution does not exist or is has not been measured at the end of the optimization
446
- # save the original solution as the best solution
447
- if best_solution_node is not None:
448
- best_solution_content = best_solution_node.code
449
- else:
450
- best_solution_content = read_from_path(fp=runs_dir / f"step_0{source_fp.suffix}", is_json=False)
451
-
452
- # Save best solution to .runs/<run-id>/best.<extension>
453
- write_to_path(fp=runs_dir / f"best{source_fp.suffix}", content=best_solution_content)
454
- # write the best solution to the source file
455
- write_to_path(fp=source_fp, content=best_solution_content)
456
482
  # Mark as completed normally for the finally block
457
483
  optimization_completed_normally = True
458
484
  live.update(end_optimization_layout)
@@ -464,7 +490,7 @@ def execute_optimization(
464
490
  except Exception:
465
491
  error_message = str(e)
466
492
  console.print(Panel(f"[bold red]Error: {error_message}", title="[bold red]Optimization Error", border_style="red"))
467
- 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")
468
494
  # Ensure optimization_completed_normally is False
469
495
  optimization_completed_normally = False
470
496
  finally:
@@ -491,6 +517,17 @@ def execute_optimization(
491
517
  else "CLI terminated unexpectedly without a specific exception captured."
492
518
  )
493
519
 
520
+ if best_solution_code and best_solution_code != original_source_code:
521
+ # Determine whether to apply: automatically if --apply-change is set, otherwise ask user
522
+ should_apply = apply_change or Confirm.ask(
523
+ "Would you like to apply the best solution to the source file?", default=True
524
+ )
525
+ if should_apply:
526
+ write_to_path(fp=source_fp, content=best_solution_code)
527
+ console.print("\n[green]Best solution applied to the source file.[/]\n")
528
+ else:
529
+ console.print("\n[green]A better solution was not found. No changes to apply.[/]\n")
530
+
494
531
  report_termination(
495
532
  run_id=run_id,
496
533
  status_update=status,
@@ -502,12 +539,12 @@ def execute_optimization(
502
539
  # Handle exit
503
540
  if user_stop_requested_flag:
504
541
  console.print("[yellow]Run terminated by user request.[/]")
505
- 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")
506
543
 
507
544
  return optimization_completed_normally or user_stop_requested_flag
508
545
 
509
546
 
510
- def resume_optimization(run_id: str, console: Optional[Console] = None) -> bool:
547
+ def resume_optimization(run_id: str, console: Optional[Console] = None, apply_change: bool = False) -> bool:
511
548
  """Resume an interrupted run from the most recent node and continue optimization."""
512
549
  if console is None:
513
550
  console = Console()
@@ -517,11 +554,19 @@ def resume_optimization(run_id: str, console: Optional[Console] = None) -> bool:
517
554
  stop_heartbeat_event = threading.Event()
518
555
  current_run_id_for_heartbeat = None
519
556
  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
520
561
 
521
562
  # Signal handler for this optimization run
522
563
  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
+
523
568
  signal_name = signal.Signals(signum).name
524
- 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")
525
570
  stop_heartbeat_event.set()
526
571
  if heartbeat_thread and heartbeat_thread.is_alive():
527
572
  heartbeat_thread.join(timeout=2)
@@ -533,7 +578,7 @@ def resume_optimization(run_id: str, console: Optional[Console] = None) -> bool:
533
578
  details=f"Process terminated by signal {signal_name} ({signum}).",
534
579
  auth_headers=current_auth_headers_for_heartbeat,
535
580
  )
536
- 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")
537
582
  sys.exit(0)
538
583
 
539
584
  # Set up signal handlers for this run
@@ -619,11 +664,13 @@ def resume_optimization(run_id: str, console: Optional[Console] = None) -> bool:
619
664
  save_logs = bool(resume_resp.get("save_logs", False))
620
665
  eval_timeout = resume_resp.get("eval_timeout")
621
666
 
622
- # Write last solution code to source path
667
+ # Read the original source code from the file before we start modifying it
623
668
  source_fp = pathlib.Path(source_path)
624
669
  source_fp.parent.mkdir(parents=True, exist_ok=True)
670
+ # Store the original content to restore after each evaluation
671
+ original_source_code = read_from_path(fp=source_fp, is_json=False) if source_fp.exists() else ""
672
+ # The code to restore is the code from the last step of the previous run
625
673
  code_to_restore = resume_resp.get("code") or resume_resp.get("source_code") or ""
626
- write_to_path(fp=source_fp, content=code_to_restore)
627
674
 
628
675
  # Prepare UI panels
629
676
  summary_panel = SummaryPanel(
@@ -649,12 +696,25 @@ def resume_optimization(run_id: str, console: Optional[Console] = None) -> bool:
649
696
  best_solution_node = get_best_node_from_status(status_response=status)
650
697
  current_solution_node = get_node_from_status(status_response=status, solution_id=resume_resp.get("solution_id"))
651
698
 
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
+
652
704
  # Ensure runs dir exists
653
705
  runs_dir = pathlib.Path(log_dir) / resume_resp["run_id"]
654
706
  runs_dir.mkdir(parents=True, exist_ok=True)
655
707
  # Persist last step's code into logs as step_<current_step>
656
708
  write_to_path(fp=runs_dir / f"step_{current_step}{source_fp.suffix}", content=code_to_restore)
657
709
 
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
+
658
718
  # Start Heartbeat Thread
659
719
  stop_heartbeat_event.clear()
660
720
  heartbeat_thread = HeartbeatSender(resume_resp["run_id"], auth_headers, stop_heartbeat_event)
@@ -667,6 +727,7 @@ def resume_optimization(run_id: str, console: Optional[Console] = None) -> bool:
667
727
  # --- Live UI ---
668
728
  refresh_rate = 4
669
729
  with Live(layout, refresh_per_second=refresh_rate) as live:
730
+ live_ref = live
670
731
  # Initial panels
671
732
  current_solution_panel, best_solution_panel = solution_panels.get_display(current_step=current_step)
672
733
  # Use backend-provided execution output only (no fallback)
@@ -689,7 +750,13 @@ def resume_optimization(run_id: str, console: Optional[Console] = None) -> bool:
689
750
 
690
751
  # If missing output, evaluate once before first suggest
691
752
  if term_out is None or len(term_out.strip()) == 0:
692
- term_out = run_evaluation(eval_command=eval_command, timeout=eval_timeout)
753
+ term_out = run_evaluation_with_file_swap(
754
+ file_path=source_fp,
755
+ new_content=code_to_restore,
756
+ original_content=original_source_code,
757
+ eval_command=eval_command,
758
+ timeout=eval_timeout,
759
+ )
693
760
  eval_output_panel.update(output=term_out)
694
761
  # Update the evaluation output panel
695
762
  smooth_update(
@@ -727,9 +794,8 @@ def resume_optimization(run_id: str, console: Optional[Console] = None) -> bool:
727
794
  auth_headers=auth_headers,
728
795
  )
729
796
 
730
- # Save next solution file(s)
797
+ # Save next solution to logs
731
798
  write_to_path(fp=runs_dir / f"step_{step}{source_fp.suffix}", content=eval_and_next_solution_response["code"])
732
- write_to_path(fp=source_fp, content=eval_and_next_solution_response["code"])
733
799
 
734
800
  # Refresh status with history and update panels
735
801
  status_response = get_optimization_run_status(
@@ -744,6 +810,17 @@ def resume_optimization(run_id: str, console: Optional[Console] = None) -> bool:
744
810
  current_solution_node = get_node_from_status(
745
811
  status_response=status_response, solution_id=eval_and_next_solution_response["solution_id"]
746
812
  )
813
+
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
+
747
824
  solution_panels.update(current_node=current_solution_node, best_node=best_solution_node)
748
825
  current_solution_panel, best_solution_panel = solution_panels.get_display(current_step=step)
749
826
  eval_output_panel.clear()
@@ -760,8 +837,14 @@ def resume_optimization(run_id: str, console: Optional[Console] = None) -> bool:
760
837
  transition_delay=0.08,
761
838
  )
762
839
 
763
- # Evaluate this new solution
764
- term_out = run_evaluation(eval_command=eval_command, timeout=eval_timeout)
840
+ # Evaluate this new solution and restore original code after
841
+ term_out = run_evaluation_with_file_swap(
842
+ file_path=source_fp,
843
+ new_content=eval_and_next_solution_response["code"],
844
+ original_content=original_source_code,
845
+ eval_command=eval_command,
846
+ timeout=eval_timeout,
847
+ )
765
848
  if save_logs:
766
849
  save_execution_output(runs_dir, step=step, output=term_out)
767
850
  eval_output_panel.update(output=term_out)
@@ -789,6 +872,10 @@ def resume_optimization(run_id: str, console: Optional[Console] = None) -> bool:
789
872
  tree_panel.build_metric_tree(nodes=nodes_final)
790
873
  # Best solution panel and final message
791
874
  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
+
792
879
  solution_panels.update(current_node=None, best_node=best_solution_node)
793
880
  _, best_solution_panel = solution_panels.get_display(current_step=total_steps)
794
881
  final_message = (
@@ -800,14 +887,6 @@ def resume_optimization(run_id: str, console: Optional[Console] = None) -> bool:
800
887
  end_optimization_layout["tree"].update(tree_panel.get_display(is_done=True))
801
888
  end_optimization_layout["best_solution"].update(best_solution_panel)
802
889
 
803
- # Save best
804
- if best_solution_node is not None:
805
- best_solution_content = best_solution_node.code
806
- else:
807
- best_solution_content = read_from_path(fp=runs_dir / f"step_0{source_fp.suffix}", is_json=False)
808
-
809
- write_to_path(fp=runs_dir / f"best{source_fp.suffix}", content=best_solution_content)
810
- write_to_path(fp=source_fp, content=best_solution_content)
811
890
  optimization_completed_normally = True
812
891
  live.update(end_optimization_layout)
813
892
 
@@ -817,7 +896,7 @@ def resume_optimization(run_id: str, console: Optional[Console] = None) -> bool:
817
896
  except Exception:
818
897
  error_message = str(e)
819
898
  console.print(Panel(f"[bold red]Error: {error_message}", title="[bold red]Optimization Error", border_style="red"))
820
- 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")
821
900
  optimization_completed_normally = False
822
901
  finally:
823
902
  signal.signal(signal.SIGINT, original_sigint_handler)
@@ -826,7 +905,11 @@ def resume_optimization(run_id: str, console: Optional[Console] = None) -> bool:
826
905
  if heartbeat_thread and heartbeat_thread.is_alive():
827
906
  heartbeat_thread.join(timeout=2)
828
907
 
829
- run_id = resume_resp.get("run_id")
908
+ try:
909
+ run_id = resume_resp.get("run_id")
910
+ except Exception:
911
+ run_id = None
912
+
830
913
  # Report final status if run exists
831
914
  if run_id:
832
915
  if optimization_completed_normally:
@@ -840,6 +923,17 @@ def resume_optimization(run_id: str, console: Optional[Console] = None) -> bool:
840
923
  if "e" in locals() and isinstance(locals()["e"], Exception)
841
924
  else "CLI terminated unexpectedly without a specific exception captured."
842
925
  )
926
+
927
+ if best_solution_code and best_solution_code != original_source_code:
928
+ should_apply = apply_change or Confirm.ask(
929
+ "Would you like to apply the best solution to the source file?", default=True
930
+ )
931
+ if should_apply:
932
+ write_to_path(fp=source_fp, content=best_solution_code)
933
+ console.print("\n[green]Best solution applied to the source file.[/]\n")
934
+ else:
935
+ console.print("\n[green]A better solution was not found. No changes to apply.[/]\n")
936
+
843
937
  report_termination(
844
938
  run_id=run_id,
845
939
  status_update=status,
@@ -849,5 +943,5 @@ def resume_optimization(run_id: str, console: Optional[Console] = None) -> bool:
849
943
  )
850
944
  if user_stop_requested_flag:
851
945
  console.print("[yellow]Run terminated by user request.[/]")
852
- 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")
853
947
  return optimization_completed_normally or user_stop_requested_flag
weco/panels.py CHANGED
@@ -5,6 +5,8 @@ from rich.layout import Layout
5
5
  from rich.panel import Panel
6
6
  from rich.syntax import Syntax
7
7
  from rich import box
8
+ from rich.console import Console
9
+
8
10
  from typing import Dict, List, Optional, Union, Tuple
9
11
  from pathlib import Path
10
12
  from .__init__ import __dashboard_url__
@@ -22,6 +24,7 @@ class SummaryPanel:
22
24
  runs_dir: str,
23
25
  run_id: str = None,
24
26
  run_name: str = None,
27
+ console: Optional[Console] = None,
25
28
  ):
26
29
  self.maximize = maximize
27
30
  self.metric_name = metric_name
@@ -32,6 +35,8 @@ class SummaryPanel:
32
35
  self.run_name = run_name if run_name is not None else "N/A"
33
36
  self.dashboard_url = "N/A"
34
37
  self.thinking_content = ""
38
+ self.user_input = ""
39
+ self.console = Console()
35
40
  self.progress = Progress(
36
41
  TextColumn("[progress.description]{task.description}"),
37
42
  BarColumn(bar_width=20),
weco/utils.py CHANGED
@@ -106,6 +106,41 @@ def truncate_output(output: str) -> str:
106
106
  return output
107
107
 
108
108
 
109
+ def run_evaluation_with_file_swap(
110
+ file_path: pathlib.Path, new_content: str, original_content: str, eval_command: str, timeout: int | None = None
111
+ ) -> str:
112
+ """
113
+ Temporarily write new content to a file, run evaluation, then restore original.
114
+
115
+ This function ensures the file is always restored to its original state,
116
+ even if an exception occurs during evaluation.
117
+
118
+ Args:
119
+ file_path: Path to the file to temporarily modify
120
+ new_content: The new content to write for evaluation
121
+ original_content: The original content to restore after evaluation
122
+ eval_command: The shell command to run for evaluation
123
+ timeout: Optional timeout for the evaluation command
124
+
125
+ Returns:
126
+ The output from running the evaluation command
127
+
128
+ Raises:
129
+ Any exception raised by run_evaluation will be re-raised after
130
+ the file is restored to its original state.
131
+ """
132
+ # Write the new content
133
+ write_to_path(fp=file_path, content=new_content)
134
+
135
+ try:
136
+ # Run the evaluation
137
+ output = run_evaluation(eval_command=eval_command, timeout=timeout)
138
+ return output
139
+ finally:
140
+ # Always restore the original file, even if evaluation fails
141
+ write_to_path(fp=file_path, content=original_content)
142
+
143
+
109
144
  def run_evaluation(eval_command: str, timeout: int | None = None) -> str:
110
145
  """Run the evaluation command on the code and return the output."""
111
146
  process = subprocess.Popen(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: weco
3
- Version: 0.3.3
3
+ Version: 0.3.5
4
4
  Summary: Documentation for `weco`, a CLI for using Weco AI's code optimizer.
5
5
  Author-email: Weco AI Team <contact@weco.ai>
6
6
  License:
@@ -322,6 +322,7 @@ For more advanced examples, including [Triton](/examples/triton/README.md), [CUD
322
322
  | `-l, --log-dir` | Path to the directory to log intermediate steps and final optimization result. | `.runs/` | `-l ./logs/` |
323
323
  | `--eval-timeout` | Timeout in seconds for each step in evaluation. | No timeout (unlimited) | `--eval-timeout 3600` |
324
324
  | `--save-logs` | Save execution output from each optimization step to disk. Creates timestamped directories with raw output files and a JSONL index for tracking execution history. | `False` | `--save-logs` |
325
+ | `--apply-change` | Automatically apply the best solution to the source file without prompting. | `False` | `--apply-change` |
325
326
 
326
327
  ---
327
328
 
@@ -375,6 +376,7 @@ Arguments for `weco resume`:
375
376
  | Argument | Description | Example |
376
377
  |----------|-------------|---------|
377
378
  | `run-id` | The UUID of the run to resume (shown at the start of each run) | `0002e071-1b67-411f-a514-36947f0c4b31` |
379
+ | `--apply-change` | Automatically apply the best solution to the source file without prompting | `--apply-change` |
378
380
 
379
381
  Notes:
380
382
  - Works only for interrupted runs (status: `error`, `terminated`, etc.).
@@ -0,0 +1,16 @@
1
+ weco/__init__.py,sha256=ClO0uT6GKOA0iSptvP0xbtdycf0VpoPTq37jHtvlhtw,303
2
+ weco/api.py,sha256=zKcI4riwruK6CjV_vcL8RlsJGRXO40iP0WxeETtzPIY,18430
3
+ weco/auth.py,sha256=O31Hoj-Loi8DWJJG2LfeWgUMuNqAUeGDpd2ZGjA9Ah0,9997
4
+ weco/chatbot.py,sha256=EIK2WaOul9gn_yHLThjsZV7RnE8t3XQPwgRkO5tybSU,38415
5
+ weco/cli.py,sha256=7Q6ZwkD8N2x_PRovaFkF9OvtKkKX3hs_CobvS8xKXkQ,11535
6
+ weco/constants.py,sha256=V6yFugTznKm5EC2_jr4I_whd7sqI80HiPggRn0az580,406
7
+ weco/credits.py,sha256=C08x-TRcLg3ccfKqMGNRY7zBn7t3r7LZ119bxgfztaI,7629
8
+ weco/optimizer.py,sha256=wG8_ZMF-SszWK-9rPXyaWvl60Q7EMTJbksSpKxf2pUk,45217
9
+ weco/panels.py,sha256=POHt0MdRKDykwUJYXcry92O41lpB9gxna55wFI9abWU,16272
10
+ weco/utils.py,sha256=SMFh7ngo6gsjb2g20wpPe0O0T8xR7U_7X4-kqxJHkGc,8308
11
+ weco-0.3.5.dist-info/licenses/LICENSE,sha256=9LUfoGHjLPtak2zps2kL2tm65HAZIICx_FbLaRuS4KU,11337
12
+ weco-0.3.5.dist-info/METADATA,sha256=OvCAH5Rg2rTFlUkqBkHYEAYx09ZTErUdL4npETqQdnk,29244
13
+ weco-0.3.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ weco-0.3.5.dist-info/entry_points.txt,sha256=ixJ2uClALbCpBvnIR6BXMNck8SHAab8eVkM9pIUowcs,39
15
+ weco-0.3.5.dist-info/top_level.txt,sha256=F0N7v6e2zBSlsorFv-arAq2yDxQbzX3KVO8GxYhPUeE,5
16
+ weco-0.3.5.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- weco/__init__.py,sha256=ClO0uT6GKOA0iSptvP0xbtdycf0VpoPTq37jHtvlhtw,303
2
- weco/api.py,sha256=zKcI4riwruK6CjV_vcL8RlsJGRXO40iP0WxeETtzPIY,18430
3
- weco/auth.py,sha256=O31Hoj-Loi8DWJJG2LfeWgUMuNqAUeGDpd2ZGjA9Ah0,9997
4
- weco/chatbot.py,sha256=EIK2WaOul9gn_yHLThjsZV7RnE8t3XQPwgRkO5tybSU,38415
5
- weco/cli.py,sha256=5TusCKQ8o4CUdqxOSkvWyjMzt86O9sj21SJoeU0Ni5w,11098
6
- weco/constants.py,sha256=V6yFugTznKm5EC2_jr4I_whd7sqI80HiPggRn0az580,406
7
- weco/credits.py,sha256=C08x-TRcLg3ccfKqMGNRY7zBn7t3r7LZ119bxgfztaI,7629
8
- weco/optimizer.py,sha256=nOKFmwPdFLcQ7RF4ielsD7iRfPMxvGr07pS9ocbW9C8,41282
9
- weco/panels.py,sha256=fnGPtmvxpx21AuBCtCFu1f_BpSxybNr2lhjIIKIutrY,16133
10
- weco/utils.py,sha256=erDDrA_g3KSlel6YEAGALlV_k8ftT-VQnPT1BrmzK8k,7021
11
- weco-0.3.3.dist-info/licenses/LICENSE,sha256=9LUfoGHjLPtak2zps2kL2tm65HAZIICx_FbLaRuS4KU,11337
12
- weco-0.3.3.dist-info/METADATA,sha256=aAE9oMp_SKJiQSG189k7fsb5ovvidj2t1DTuqNfvc68,28700
13
- weco-0.3.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- weco-0.3.3.dist-info/entry_points.txt,sha256=ixJ2uClALbCpBvnIR6BXMNck8SHAab8eVkM9pIUowcs,39
15
- weco-0.3.3.dist-info/top_level.txt,sha256=F0N7v6e2zBSlsorFv-arAq2yDxQbzX3KVO8GxYhPUeE,5
16
- weco-0.3.3.dist-info/RECORD,,
File without changes