weco 0.2.19__py3-none-any.whl → 0.2.20__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 CHANGED
@@ -17,7 +17,7 @@ def handle_api_error(e: requests.exceptions.HTTPError, console: rich.console.Con
17
17
  # sys.exit(1)
18
18
 
19
19
 
20
- def start_optimization_session(
20
+ def start_optimization_run(
21
21
  console: rich.console.Console,
22
22
  source_code: str,
23
23
  evaluation_command: str,
@@ -29,14 +29,14 @@ def start_optimization_session(
29
29
  search_policy_config: Dict[str, Any],
30
30
  additional_instructions: str = None,
31
31
  api_keys: Dict[str, Any] = {},
32
- auth_headers: dict = {}, # Add auth_headers
32
+ auth_headers: dict = {},
33
33
  timeout: int = 800,
34
34
  ) -> Dict[str, Any]:
35
- """Start the optimization session."""
35
+ """Start the optimization run."""
36
36
  with console.status("[bold green]Starting Optimization..."):
37
37
  try:
38
38
  response = requests.post(
39
- f"{__base_url__}/sessions", # Path is relative to base_url
39
+ f"{__base_url__}/runs",
40
40
  json={
41
41
  "source_code": source_code,
42
42
  "additional_instructions": additional_instructions,
@@ -49,37 +49,37 @@ def start_optimization_session(
49
49
  },
50
50
  "metadata": {"client_name": "cli", "client_version": __pkg_version__, **api_keys},
51
51
  },
52
- headers=auth_headers, # Add headers
52
+ headers=auth_headers,
53
53
  timeout=timeout,
54
54
  )
55
55
  response.raise_for_status()
56
56
  return response.json()
57
57
  except requests.exceptions.HTTPError as e:
58
58
  handle_api_error(e, console)
59
- sys.exit(1) # Exit if starting session fails
59
+ sys.exit(1)
60
60
  except requests.exceptions.RequestException as e:
61
- console.print(f"[bold red]Network Error starting session: {e}[/]")
61
+ console.print(f"[bold red]Network Error starting run: {e}[/]")
62
62
  sys.exit(1)
63
63
 
64
64
 
65
65
  def evaluate_feedback_then_suggest_next_solution(
66
- session_id: str,
66
+ run_id: str,
67
67
  execution_output: str,
68
68
  additional_instructions: str = None,
69
69
  api_keys: Dict[str, Any] = {},
70
- auth_headers: dict = {}, # Add auth_headers
70
+ auth_headers: dict = {},
71
71
  timeout: int = 800,
72
72
  ) -> Dict[str, Any]:
73
73
  """Evaluate the feedback and suggest the next solution."""
74
74
  try:
75
75
  response = requests.post(
76
- f"{__base_url__}/sessions/{session_id}/suggest", # Path is relative to base_url
76
+ f"{__base_url__}/runs/{run_id}/suggest",
77
77
  json={
78
78
  "execution_output": execution_output,
79
79
  "additional_instructions": additional_instructions,
80
80
  "metadata": {**api_keys},
81
81
  },
82
- headers=auth_headers, # Add headers
82
+ headers=auth_headers,
83
83
  timeout=timeout,
84
84
  )
85
85
  response.raise_for_status()
@@ -93,16 +93,13 @@ def evaluate_feedback_then_suggest_next_solution(
93
93
  raise # Re-raise the exception
94
94
 
95
95
 
96
- def get_optimization_session_status(
97
- session_id: str, include_history: bool = False, auth_headers: dict = {}, timeout: int = 800
96
+ def get_optimization_run_status(
97
+ run_id: str, include_history: bool = False, auth_headers: dict = {}, timeout: int = 800
98
98
  ) -> Dict[str, Any]:
99
- """Get the current status of the optimization session."""
99
+ """Get the current status of the optimization run."""
100
100
  try:
101
101
  response = requests.get(
102
- f"{__base_url__}/sessions/{session_id}", # Path is relative to base_url
103
- params={"include_history": include_history},
104
- headers=auth_headers,
105
- timeout=timeout,
102
+ f"{__base_url__}/runs/{run_id}", params={"include_history": include_history}, headers=auth_headers, timeout=timeout
106
103
  )
107
104
  response.raise_for_status()
108
105
  return response.json()
@@ -114,42 +111,30 @@ def get_optimization_session_status(
114
111
  raise # Re-raise
115
112
 
116
113
 
117
- def send_heartbeat(
118
- session_id: str,
119
- auth_headers: dict = {},
120
- timeout: int = 10, # Shorter timeout for non-critical heartbeat
121
- ) -> bool:
114
+ def send_heartbeat(run_id: str, auth_headers: dict = {}, timeout: int = 10) -> bool:
122
115
  """Send a heartbeat signal to the backend."""
123
116
  try:
124
- response = requests.put(f"{__base_url__}/sessions/{session_id}/heartbeat", headers=auth_headers, timeout=timeout)
125
- response.raise_for_status() # Raises HTTPError for bad responses (4xx or 5xx)
117
+ response = requests.put(f"{__base_url__}/runs/{run_id}/heartbeat", headers=auth_headers, timeout=timeout)
118
+ response.raise_for_status()
126
119
  return True
127
120
  except requests.exceptions.HTTPError as e:
128
- # Log non-critical errors like 409 Conflict (session not running)
129
121
  if e.response.status_code == 409:
130
- print(f"Heartbeat ignored: Session {session_id} is not running.", file=sys.stderr)
122
+ print(f"Heartbeat ignored: Run {run_id} is not running.", file=sys.stderr)
131
123
  else:
132
- print(f"Heartbeat failed for session {session_id}: HTTP {e.response.status_code}", file=sys.stderr)
133
- # Don't exit, just report failure
124
+ print(f"Heartbeat failed for run {run_id}: HTTP {e.response.status_code}", file=sys.stderr)
134
125
  return False
135
126
  except requests.exceptions.RequestException as e:
136
- # Network errors are also non-fatal for heartbeats
137
- print(f"Heartbeat network error for session {session_id}: {e}", file=sys.stderr)
127
+ print(f"Heartbeat network error for run {run_id}: {e}", file=sys.stderr)
138
128
  return False
139
129
 
140
130
 
141
131
  def report_termination(
142
- session_id: str,
143
- status_update: str,
144
- reason: str,
145
- details: Optional[str] = None,
146
- auth_headers: dict = {},
147
- timeout: int = 30, # Reasonably longer timeout for important termination message
132
+ run_id: str, status_update: str, reason: str, details: Optional[str] = None, auth_headers: dict = {}, timeout: int = 30
148
133
  ) -> bool:
149
134
  """Report the termination reason to the backend."""
150
135
  try:
151
136
  response = requests.post(
152
- f"{__base_url__}/sessions/{session_id}/terminate",
137
+ f"{__base_url__}/runs/{run_id}/terminate",
153
138
  json={"status_update": status_update, "termination_reason": reason, "termination_details": details},
154
139
  headers=auth_headers,
155
140
  timeout=timeout,
@@ -157,6 +142,5 @@ def report_termination(
157
142
  response.raise_for_status()
158
143
  return True
159
144
  except requests.exceptions.RequestException as e:
160
- # Log failure, but don't prevent CLI exit
161
- print(f"Warning: Failed to report termination to backend for session {session_id}: {e}", file=sys.stderr)
145
+ print(f"Warning: Failed to report termination to backend for run {run_id}: {e}", file=sys.stderr)
162
146
  return False
weco/cli.py CHANGED
@@ -14,9 +14,9 @@ from rich.panel import Panel
14
14
  from rich.traceback import install
15
15
  from rich.prompt import Prompt
16
16
  from .api import (
17
- start_optimization_session,
17
+ start_optimization_run,
18
18
  evaluate_feedback_then_suggest_next_solution,
19
- get_optimization_session_status,
19
+ get_optimization_run_status,
20
20
  handle_api_error,
21
21
  send_heartbeat,
22
22
  report_termination,
@@ -51,15 +51,15 @@ console = Console()
51
51
  # --- Global variable for heartbeat thread ---
52
52
  heartbeat_thread = None
53
53
  stop_heartbeat_event = threading.Event()
54
- current_session_id_for_heartbeat = None
54
+ current_run_id_for_heartbeat = None
55
55
  current_auth_headers_for_heartbeat = {}
56
56
 
57
57
 
58
58
  # --- Heartbeat Sender Class ---
59
59
  class HeartbeatSender(threading.Thread):
60
- def __init__(self, session_id: str, auth_headers: dict, stop_event: threading.Event, interval: int = 30):
60
+ def __init__(self, run_id: str, auth_headers: dict, stop_event: threading.Event, interval: int = 30):
61
61
  super().__init__(daemon=True) # Daemon thread exits when main thread exits
62
- self.session_id = session_id
62
+ self.run_id = run_id
63
63
  self.auth_headers = auth_headers
64
64
  self.interval = interval
65
65
  self.stop_event = stop_event
@@ -67,7 +67,7 @@ class HeartbeatSender(threading.Thread):
67
67
  def run(self):
68
68
  try:
69
69
  while not self.stop_event.is_set():
70
- if not send_heartbeat(self.session_id, self.auth_headers):
70
+ if not send_heartbeat(self.run_id, self.auth_headers):
71
71
  # send_heartbeat itself prints errors to stderr if it returns False
72
72
  # No explicit HeartbeatSender log needed here unless more detail is desired for a False return
73
73
  pass # Continue trying as per original logic
@@ -79,9 +79,7 @@ class HeartbeatSender(threading.Thread):
79
79
 
80
80
  except Exception as e:
81
81
  # Catch any unexpected error in the loop to prevent silent thread death
82
- print(
83
- f"[ERROR HeartbeatSender] Unhandled exception in run loop for session {self.session_id}: {e}", file=sys.stderr
84
- )
82
+ print(f"[ERROR HeartbeatSender] Unhandled exception in run loop for run {self.run_id}: {e}", file=sys.stderr)
85
83
  traceback.print_exc(file=sys.stderr)
86
84
  # The loop will break due to the exception, and thread will terminate via finally.
87
85
 
@@ -97,13 +95,14 @@ def signal_handler(signum, frame):
97
95
  heartbeat_thread.join(timeout=2) # Give it a moment to stop
98
96
 
99
97
  # Report termination (best effort)
100
- if current_session_id_for_heartbeat:
98
+ if current_run_id_for_heartbeat:
101
99
  report_termination(
102
- session_id=current_session_id_for_heartbeat,
100
+ run_id=current_run_id_for_heartbeat,
103
101
  status_update="terminated",
104
102
  reason=f"user_terminated_{signal_name.lower()}",
105
103
  details=f"Process terminated by signal {signal_name} ({signum}).",
106
104
  auth_headers=current_auth_headers_for_heartbeat,
105
+ timeout=3,
107
106
  )
108
107
 
109
108
  # Exit gracefully
@@ -158,7 +157,7 @@ def perform_login(console: Console):
158
157
 
159
158
  try:
160
159
  token_response = requests.post(
161
- f"{__base_url__}/auth/device/token", # REMOVED /v1 prefix
160
+ f"{__base_url__}/auth/device/token",
162
161
  json={"grant_type": "urn:ietf:params:oauth:grant-type:device_code", "device_code": device_code},
163
162
  )
164
163
 
@@ -172,12 +171,10 @@ def perform_login(console: Console):
172
171
  # Unexpected 202 response format
173
172
  console.print(f"\n[bold red]Error:[/] Received unexpected 202 response: {token_data}")
174
173
  return False
175
-
176
174
  # Check for standard OAuth2 errors (often 400 Bad Request)
177
175
  elif token_response.status_code == 400:
178
176
  token_data = token_response.json()
179
177
  error_code = token_data.get("error", "unknown_error")
180
- # NOTE: Removed "authorization_pending" check from here
181
178
  if error_code == "slow_down":
182
179
  interval += 5 # Increase polling interval if instructed
183
180
  live_status.update(f"Waiting... (slowing down polling to {interval}s)")
@@ -195,7 +192,6 @@ def perform_login(console: Console):
195
192
 
196
193
  # Check for other non-200/non-202/non-400 HTTP errors
197
194
  token_response.raise_for_status()
198
-
199
195
  # If successful (200 OK and no 'error' field)
200
196
  token_data = token_response.json()
201
197
  if "access_token" in token_data:
@@ -206,19 +202,17 @@ def perform_login(console: Console):
206
202
  else:
207
203
  # Unexpected successful response format
208
204
  console.print("\n[bold red]Error:[/] Received unexpected response from server during polling.")
209
- print(token_data) # Log for debugging
205
+ print(token_data)
210
206
  return False
211
-
212
207
  except requests.exceptions.RequestException as e:
213
208
  # Handle network errors during polling gracefully
214
209
  live_status.update("Waiting... (network error, retrying)")
215
210
  console.print(f"\n[bold yellow]Warning:[/] Network error during polling: {e}. Retrying...")
216
- # Optional: implement backoff strategy
217
211
  time.sleep(interval * 2) # Simple backoff
218
-
219
- except requests.exceptions.HTTPError as e: # Catch HTTPError specifically for handle_api_error
212
+ except requests.exceptions.HTTPError as e:
220
213
  handle_api_error(e, console)
221
- except requests.exceptions.RequestException as e: # Catch other request errors
214
+ except requests.exceptions.RequestException as e:
215
+ # Catch other request errors
222
216
  console.print(f"\n[bold red]Network Error:[/] {e}")
223
217
  return False
224
218
  except Exception as e:
@@ -233,17 +227,15 @@ def main() -> None:
233
227
  signal.signal(signal.SIGTERM, signal_handler)
234
228
 
235
229
  # --- Perform Update Check ---
236
- # Import __pkg_version__ here to avoid circular import issues if it's also used in modules imported by cli.py
237
230
  from . import __pkg_version__
238
231
 
239
- check_for_cli_updates(__pkg_version__) # Call the imported function
232
+ check_for_cli_updates(__pkg_version__)
240
233
 
241
234
  # --- Argument Parsing ---
242
235
  parser = argparse.ArgumentParser(
243
236
  description="[bold cyan]Weco CLI[/]", formatter_class=argparse.RawDescriptionHelpFormatter
244
237
  )
245
- # Add subparsers for commands like 'run' and 'logout'
246
- subparsers = parser.add_subparsers(dest="command", help="Available commands", required=True) # Make command required
238
+ subparsers = parser.add_subparsers(dest="command", help="Available commands", required=True)
247
239
 
248
240
  # --- Run Command ---
249
241
  run_parser = subparsers.add_parser(
@@ -298,33 +290,27 @@ def main() -> None:
298
290
  help="Description of additional instruction or path to a file containing additional instructions. Defaults to None.",
299
291
  )
300
292
 
301
- # --- Logout Command ---
302
293
  _ = subparsers.add_parser("logout", help="Log out from Weco and clear saved API key.")
303
-
304
294
  args = parser.parse_args()
305
295
 
306
- # --- Handle Logout Command ---
307
296
  if args.command == "logout":
308
297
  clear_api_key()
309
298
  sys.exit(0)
310
-
311
- # --- Handle Run Command ---
312
299
  elif args.command == "run":
313
- global heartbeat_thread, current_session_id_for_heartbeat, current_auth_headers_for_heartbeat # Allow modification of globals
314
-
315
- session_id = None # Initialize session_id
316
- optimization_completed_normally = False # Flag for finally block
317
- # --- Check Authentication ---
300
+ global heartbeat_thread, current_run_id_for_heartbeat, current_auth_headers_for_heartbeat # Allow modification of globals
301
+ run_id = None # Initialize run_id (we receive this from the API after starting the run)
302
+ optimization_completed_normally = False
303
+ user_stop_requested_flag = False
318
304
  weco_api_key = load_weco_api_key()
319
305
  llm_api_keys = read_api_keys_from_env() # Read keys from client environment
320
306
 
307
+ # --- Login/Authentication Handling ---
321
308
  if not weco_api_key:
322
309
  login_choice = Prompt.ask(
323
310
  "Log in to Weco to save run history or use anonymously? ([bold]L[/]ogin / [bold]S[/]kip)",
324
311
  choices=["l", "s"],
325
312
  default="s",
326
313
  ).lower()
327
-
328
314
  if login_choice == "l":
329
315
  console.print("[cyan]Starting login process...[/]")
330
316
  if not perform_login(console):
@@ -334,7 +320,6 @@ def main() -> None:
334
320
  if not weco_api_key:
335
321
  console.print("[bold red]Error: Login completed but failed to retrieve API key.[/]")
336
322
  sys.exit(1)
337
-
338
323
  elif login_choice == "s":
339
324
  console.print("[yellow]Proceeding anonymously. LLM API keys must be provided via environment variables.[/]")
340
325
  if not llm_api_keys:
@@ -343,7 +328,6 @@ def main() -> None:
343
328
  )
344
329
  sys.exit(1)
345
330
 
346
- # --- Prepare API Call Arguments ---
347
331
  auth_headers = {}
348
332
  if weco_api_key:
349
333
  auth_headers["Authorization"] = f"Bearer {weco_api_key}"
@@ -351,7 +335,7 @@ def main() -> None:
351
335
 
352
336
  # --- Main Run Logic ---
353
337
  try:
354
- # --- Configuration Loading ---
338
+ # --- Read Command Line Arguments ---
355
339
  evaluation_command = args.eval_command
356
340
  metric_name = args.metric
357
341
  maximize = args.goal in ["maximize", "max"]
@@ -375,13 +359,9 @@ def main() -> None:
375
359
  "debug_prob": 0.5,
376
360
  "max_debug_depth": max(1, math.ceil(0.1 * steps)),
377
361
  }
378
- # API request timeout
379
362
  timeout = 800
380
- # Read additional instructions
381
363
  additional_instructions = read_additional_instructions(additional_instructions=args.additional_instructions)
382
- # Read source code path
383
364
  source_fp = pathlib.Path(args.source)
384
- # Read source code content
385
365
  source_code = read_from_path(fp=source_fp, is_json=False)
386
366
 
387
367
  # --- Panel Initialization ---
@@ -395,8 +375,8 @@ def main() -> None:
395
375
  layout = create_optimization_layout()
396
376
  end_optimization_layout = create_end_optimization_layout()
397
377
 
398
- # --- Start Optimization Session ---
399
- session_response = start_optimization_session(
378
+ # --- Start Optimization Run ---
379
+ run_response = start_optimization_run(
400
380
  console=console,
401
381
  source_code=source_code,
402
382
  evaluation_command=evaluation_command,
@@ -411,42 +391,39 @@ def main() -> None:
411
391
  auth_headers=auth_headers,
412
392
  timeout=timeout,
413
393
  )
414
- session_id = session_response["session_id"]
415
- current_session_id_for_heartbeat = session_id # Store for signal handler/finally
394
+ run_id = run_response["run_id"]
395
+ current_run_id_for_heartbeat = run_id
416
396
 
417
397
  # --- Start Heartbeat Thread ---
418
- stop_heartbeat_event.clear() # Ensure event is clear before starting
419
- heartbeat_thread = HeartbeatSender(session_id, auth_headers, stop_heartbeat_event)
398
+ stop_heartbeat_event.clear()
399
+ heartbeat_thread = HeartbeatSender(run_id, auth_headers, stop_heartbeat_event)
420
400
  heartbeat_thread.start()
421
401
 
422
402
  # --- Live Update Loop ---
423
403
  refresh_rate = 4
424
404
  with Live(layout, refresh_per_second=refresh_rate, screen=True) as live:
425
- # Define the runs directory (.runs/<session-id>)
426
- runs_dir = pathlib.Path(args.log_dir) / session_id
405
+ # Define the runs directory (.runs/<run-id>) to store logs and results
406
+ runs_dir = pathlib.Path(args.log_dir) / run_id
427
407
  runs_dir.mkdir(parents=True, exist_ok=True)
428
-
429
408
  # Write the initial code string to the logs
430
- write_to_path(fp=runs_dir / f"step_0{source_fp.suffix}", content=session_response["code"])
431
-
409
+ write_to_path(fp=runs_dir / f"step_0{source_fp.suffix}", content=run_response["code"])
432
410
  # Write the initial code string to the source file path
433
- write_to_path(fp=source_fp, content=session_response["code"])
411
+ write_to_path(fp=source_fp, content=run_response["code"])
434
412
 
435
413
  # Update the panels with the initial solution
436
- summary_panel.set_session_id(session_id=session_id) # Add session id now that we have it
414
+ summary_panel.set_run_id(run_id=run_id) # Add run id now that we have it
437
415
  # Set the step of the progress bar
438
416
  summary_panel.set_step(step=0)
439
417
  # Update the token counts
440
- summary_panel.update_token_counts(usage=session_response["usage"])
441
- # Update the plan
442
- plan_panel.update(plan=session_response["plan"])
418
+ summary_panel.update_token_counts(usage=run_response["usage"])
419
+ plan_panel.update(plan=run_response["plan"])
443
420
  # Build the metric tree
444
421
  tree_panel.build_metric_tree(
445
422
  nodes=[
446
423
  {
447
- "solution_id": session_response["solution_id"],
424
+ "solution_id": run_response["solution_id"],
448
425
  "parent_id": None,
449
- "code": session_response["code"],
426
+ "code": run_response["code"],
450
427
  "step": 0,
451
428
  "metric_value": None,
452
429
  "is_buggy": False,
@@ -454,15 +431,11 @@ def main() -> None:
454
431
  ]
455
432
  )
456
433
  # Set the current solution as unevaluated since we haven't run the evaluation function and fed it back to the model yet
457
- tree_panel.set_unevaluated_node(node_id=session_response["solution_id"])
434
+ tree_panel.set_unevaluated_node(node_id=run_response["solution_id"])
458
435
  # Update the solution panels with the initial solution and get the panel displays
459
436
  solution_panels.update(
460
437
  current_node=Node(
461
- id=session_response["solution_id"],
462
- parent_id=None,
463
- code=session_response["code"],
464
- metric=None,
465
- is_buggy=False,
438
+ id=run_response["solution_id"], parent_id=None, code=run_response["code"], metric=None, is_buggy=False
466
439
  ),
467
440
  best_node=None,
468
441
  )
@@ -482,12 +455,8 @@ def main() -> None:
482
455
  transition_delay=0.1,
483
456
  )
484
457
 
485
- # # Send initial heartbeat immediately after starting
486
- # send_heartbeat(session_id, auth_headers)
487
-
488
458
  # Run evaluation on the initial solution
489
459
  term_out = run_evaluation(eval_command=args.eval_command)
490
-
491
460
  # Update the evaluation output panel
492
461
  eval_output_panel.update(output=term_out)
493
462
  smooth_update(
@@ -503,40 +472,50 @@ def main() -> None:
503
472
  current_additional_instructions = read_additional_instructions(
504
473
  additional_instructions=args.additional_instructions
505
474
  )
475
+ if run_id:
476
+ try:
477
+ current_status_response = get_optimization_run_status(
478
+ run_id=run_id, include_history=False, timeout=30, auth_headers=auth_headers
479
+ )
480
+ current_run_status_val = current_status_response.get("status")
481
+ if current_run_status_val == "stopping":
482
+ console.print("\n[bold yellow]Stop request received. Terminating run gracefully...[/]")
483
+ user_stop_requested_flag = True
484
+ break
485
+ except requests.exceptions.RequestException as e:
486
+ console.print(
487
+ f"\n[bold red]Warning: Could not check run status: {e}. Continuing optimization...[/]"
488
+ )
489
+ except Exception as e:
490
+ console.print(
491
+ f"\n[bold red]Warning: Error checking run status: {e}. Continuing optimization...[/]"
492
+ )
506
493
 
507
494
  # Send feedback and get next suggestion
508
495
  eval_and_next_solution_response = evaluate_feedback_then_suggest_next_solution(
509
- session_id=session_id,
496
+ run_id=run_id,
510
497
  execution_output=term_out,
511
- additional_instructions=current_additional_instructions, # Pass current instructions
512
- api_keys=llm_api_keys, # Pass client LLM keys
513
- auth_headers=auth_headers, # Pass Weco key if logged in
498
+ additional_instructions=current_additional_instructions,
499
+ api_keys=llm_api_keys,
500
+ auth_headers=auth_headers,
514
501
  timeout=timeout,
515
502
  )
516
-
517
- # Save next solution (.runs/<session-id>/step_<step>.<extension>)
503
+ # Save next solution (.runs/<run-id>/step_<step>.<extension>)
518
504
  write_to_path(
519
505
  fp=runs_dir / f"step_{step}{source_fp.suffix}", content=eval_and_next_solution_response["code"]
520
506
  )
521
-
522
507
  # Write the next solution to the source file
523
508
  write_to_path(fp=source_fp, content=eval_and_next_solution_response["code"])
524
-
525
- # Get the optimization session status for
526
- # the best solution, its score, and the history to plot the tree
527
- status_response = get_optimization_session_status(
528
- session_id=session_id, include_history=True, timeout=timeout, auth_headers=auth_headers
509
+ status_response = get_optimization_run_status(
510
+ run_id=run_id, include_history=True, timeout=timeout, auth_headers=auth_headers
529
511
  )
530
-
531
- # Update the step of the progress bar
512
+ # Update the step of the progress bar, token counts, plan and metric tree
532
513
  summary_panel.set_step(step=step)
533
- # Update the token counts
534
514
  summary_panel.update_token_counts(usage=eval_and_next_solution_response["usage"])
535
- # Update the plan
536
515
  plan_panel.update(plan=eval_and_next_solution_response["plan"])
537
- # Build the metric tree
538
- tree_panel.build_metric_tree(nodes=status_response["history"])
539
- # Set the current solution as unevaluated since we haven't run the evaluation function and fed it back to the model yet
516
+
517
+ nodes_list_from_status = status_response.get("nodes")
518
+ tree_panel.build_metric_tree(nodes=nodes_list_from_status if nodes_list_from_status is not None else [])
540
519
  tree_panel.set_unevaluated_node(node_id=eval_and_next_solution_response["solution_id"])
541
520
 
542
521
  # Update the solution panels with the next solution and best solution (and score)
@@ -552,27 +531,25 @@ def main() -> None:
552
531
  else:
553
532
  best_solution_node = None
554
533
 
555
- # Create a node for the current solution
556
534
  current_solution_node = None
557
- for node in status_response["history"]:
558
- if node["solution_id"] == eval_and_next_solution_response["solution_id"]:
559
- current_solution_node = Node(
560
- id=node["solution_id"],
561
- parent_id=node["parent_id"],
562
- code=node["code"],
563
- metric=node["metric_value"],
564
- is_buggy=node["is_buggy"],
565
- )
535
+ if status_response.get("nodes"):
536
+ for node_data in status_response["nodes"]:
537
+ if node_data["solution_id"] == eval_and_next_solution_response["solution_id"]:
538
+ current_solution_node = Node(
539
+ id=node_data["solution_id"],
540
+ parent_id=node_data["parent_id"],
541
+ code=node_data["code"],
542
+ metric=node_data["metric_value"],
543
+ is_buggy=node_data["is_buggy"],
544
+ )
566
545
  if current_solution_node is None:
567
- raise ValueError("Current solution node not found in history")
546
+ raise ValueError("Current solution node not found in nodes list from status response")
547
+
568
548
  # Update the solution panels with the current and best solution
569
549
  solution_panels.update(current_node=current_solution_node, best_node=best_solution_node)
570
550
  current_solution_panel, best_solution_panel = solution_panels.get_display(current_step=step)
571
-
572
551
  # Clear evaluation output since we are running a evaluation on a new solution
573
552
  eval_output_panel.clear()
574
-
575
- # Update displays with smooth transitions
576
553
  smooth_update(
577
554
  live=live,
578
555
  layout=layout,
@@ -586,171 +563,152 @@ def main() -> None:
586
563
  ],
587
564
  transition_delay=0.08, # Slightly longer delay for more noticeable transitions
588
565
  )
589
-
590
- # Run evaluation on the current solution
591
566
  term_out = run_evaluation(eval_command=args.eval_command)
592
567
  eval_output_panel.update(output=term_out)
593
-
594
- # Update evaluation output with a smooth transition
595
568
  smooth_update(
596
569
  live=live,
597
570
  layout=layout,
598
571
  sections_to_update=[("eval_output", eval_output_panel.get_display())],
599
- transition_delay=0.1, # Slightly longer delay for evaluation results
572
+ transition_delay=0.1,
600
573
  )
601
574
 
602
- # Re-read instructions from the original source (file path or string) BEFORE each suggest call
603
- current_additional_instructions = read_additional_instructions(
604
- additional_instructions=args.additional_instructions
605
- )
606
-
607
- # Final evaluation report
608
- eval_and_next_solution_response = evaluate_feedback_then_suggest_next_solution(
609
- session_id=session_id,
610
- execution_output=term_out,
611
- additional_instructions=current_additional_instructions,
612
- api_keys=llm_api_keys,
613
- timeout=timeout,
614
- auth_headers=auth_headers,
615
- )
616
-
617
- # Update the progress bar
618
- summary_panel.set_step(step=steps)
619
- # Update the token counts
620
- summary_panel.update_token_counts(usage=eval_and_next_solution_response["usage"])
621
- # No need to update the plan panel since we have finished the optimization
622
- # Get the optimization session status for
623
- # the best solution, its score, and the history to plot the tree
624
- status_response = get_optimization_session_status(
625
- session_id=session_id, include_history=True, timeout=timeout, auth_headers=auth_headers
626
- )
627
- # Build the metric tree
628
- tree_panel.build_metric_tree(nodes=status_response["history"])
629
- # No need to set any solution to unevaluated since we have finished the optimization
630
- # and all solutions have been evaluated
631
- # No neeed to update the current solution panel since we have finished the optimization
632
- # We only need to update the best solution panel
633
- # Figure out if we have a best solution so far
634
- if status_response["best_result"] is not None:
635
- best_solution_node = Node(
636
- id=status_response["best_result"]["solution_id"],
637
- parent_id=status_response["best_result"]["parent_id"],
638
- code=status_response["best_result"]["code"],
639
- metric=status_response["best_result"]["metric_value"],
640
- is_buggy=status_response["best_result"]["is_buggy"],
575
+ if not user_stop_requested_flag:
576
+ # Re-read instructions from the original source (file path or string) BEFORE each suggest call
577
+ current_additional_instructions = read_additional_instructions(
578
+ additional_instructions=args.additional_instructions
641
579
  )
642
- else:
643
- best_solution_node = None
644
- solution_panels.update(current_node=None, best_node=best_solution_node)
645
- _, best_solution_panel = solution_panels.get_display(current_step=steps)
646
-
647
- # Update the end optimization layout
648
- final_message = (
649
- 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']}[/] 🏆"
650
- if best_solution_node is not None and best_solution_node.metric is not None
651
- else "[red] No valid solution found.[/]"
652
- )
653
- end_optimization_layout["summary"].update(summary_panel.get_display(final_message=final_message))
654
- end_optimization_layout["tree"].update(tree_panel.get_display(is_done=True))
655
- end_optimization_layout["best_solution"].update(best_solution_panel)
656
-
657
- # Save optimization results
658
- # If the best solution does not exist or is has not been measured at the end of the optimization
659
- # save the original solution as the best solution
660
- if best_solution_node is not None:
661
- best_solution_code = best_solution_node.code
662
- best_solution_score = best_solution_node.metric
663
- else:
664
- best_solution_code = None
665
- best_solution_score = None
666
-
667
- if best_solution_code is None or best_solution_score is None:
668
- 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)}"
669
- else:
670
- # Format score for the comment
671
- best_score_str = (
672
- format_number(best_solution_score)
673
- if best_solution_score is not None and isinstance(best_solution_score, (int, float))
674
- else "N/A"
580
+ # Evaluate the final solution thats been generated
581
+ eval_and_next_solution_response = evaluate_feedback_then_suggest_next_solution(
582
+ run_id=run_id,
583
+ execution_output=term_out,
584
+ additional_instructions=current_additional_instructions,
585
+ api_keys=llm_api_keys,
586
+ timeout=timeout,
587
+ auth_headers=auth_headers,
675
588
  )
676
- best_solution_content = (
677
- f"# Best solution from Weco with a score of {best_score_str}\n\n{best_solution_code}"
589
+ summary_panel.set_step(step=steps)
590
+ summary_panel.update_token_counts(usage=eval_and_next_solution_response["usage"])
591
+ status_response = get_optimization_run_status(
592
+ run_id=run_id, include_history=True, timeout=timeout, auth_headers=auth_headers
678
593
  )
594
+ # No need to update the plan panel since we have finished the optimization
595
+ # Get the optimization run status for
596
+ # the best solution, its score, and the history to plot the tree
597
+ nodes_list_from_status_final = status_response.get("nodes")
598
+ tree_panel.build_metric_tree(
599
+ nodes=nodes_list_from_status_final if nodes_list_from_status_final is not None else []
600
+ )
601
+ # No need to set any solution to unevaluated since we have finished the optimization
602
+ # and all solutions have been evaluated
603
+ # No neeed to update the current solution panel since we have finished the optimization
604
+ # We only need to update the best solution panel
605
+ # Figure out if we have a best solution so far
606
+ if status_response["best_result"] is not None:
607
+ best_solution_node = Node(
608
+ id=status_response["best_result"]["solution_id"],
609
+ parent_id=status_response["best_result"]["parent_id"],
610
+ code=status_response["best_result"]["code"],
611
+ metric=status_response["best_result"]["metric_value"],
612
+ is_buggy=status_response["best_result"]["is_buggy"],
613
+ )
614
+ else:
615
+ best_solution_node = None
616
+ solution_panels.update(current_node=None, best_node=best_solution_node)
617
+ _, best_solution_panel = solution_panels.get_display(current_step=steps)
618
+ # Update the end optimization layout
619
+ final_message = (
620
+ 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']}[/] 🏆"
621
+ if best_solution_node is not None and best_solution_node.metric is not None
622
+ else "[red] No valid solution found.[/]"
623
+ )
624
+ end_optimization_layout["summary"].update(summary_panel.get_display(final_message=final_message))
625
+ end_optimization_layout["tree"].update(tree_panel.get_display(is_done=True))
626
+ end_optimization_layout["best_solution"].update(best_solution_panel)
627
+
628
+ # Save optimization results
629
+ # If the best solution does not exist or is has not been measured at the end of the optimization
630
+ # save the original solution as the best solution
631
+ if best_solution_node is not None:
632
+ best_solution_code = best_solution_node.code
633
+ best_solution_score = best_solution_node.metric
634
+ else:
635
+ best_solution_code = None
636
+ best_solution_score = None
679
637
 
680
- # Save best solution to .runs/<session-id>/best.<extension>
681
- write_to_path(fp=runs_dir / f"best{source_fp.suffix}", content=best_solution_content)
682
-
683
- # write the best solution to the source file
684
- write_to_path(fp=source_fp, content=best_solution_content)
685
-
686
- # Mark as completed normally for the finally block
687
- optimization_completed_normally = True
688
-
689
- console.print(end_optimization_layout)
638
+ if best_solution_code is None or best_solution_score is None:
639
+ 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)}"
640
+ else:
641
+ # Format score for the comment
642
+ best_score_str = (
643
+ format_number(best_solution_score)
644
+ if best_solution_score is not None and isinstance(best_solution_score, (int, float))
645
+ else "N/A"
646
+ )
647
+ best_solution_content = (
648
+ f"# Best solution from Weco with a score of {best_score_str}\n\n{best_solution_code}"
649
+ )
650
+ # Save best solution to .runs/<run-id>/best.<extension>
651
+ write_to_path(fp=runs_dir / f"best{source_fp.suffix}", content=best_solution_content)
652
+ # write the best solution to the source file
653
+ write_to_path(fp=source_fp, content=best_solution_content)
654
+ # Mark as completed normally for the finally block
655
+ optimization_completed_normally = True
656
+ console.print(end_optimization_layout)
690
657
 
691
658
  except Exception as e:
692
659
  # Catch errors during the main optimization loop or setup
693
660
  try:
694
- error_message = e.response.json()["detail"] # Try to get API error detail
661
+ error_message = e.response.json()["detail"]
695
662
  except Exception:
696
- error_message = str(e) # Otherwise, use the exception string
663
+ error_message = str(e)
697
664
  console.print(Panel(f"[bold red]Error: {error_message}", title="[bold red]Optimization Error", border_style="red"))
698
- # Print traceback for debugging if needed (can be noisy)
699
- # console.print_exception(show_locals=False)
700
-
701
665
  # Ensure optimization_completed_normally is False
702
666
  optimization_completed_normally = False
703
-
704
- # Prepare details for termination report
705
667
  error_details = traceback.format_exc()
706
-
707
- # Exit code will be handled by finally block or sys.exit below
708
- exit_code = 1 # Indicate error
709
- # No sys.exit here, let finally block run
710
-
668
+ exit_code = 1
711
669
  finally:
712
670
  # This block runs whether the try block completed normally or raised an exception
713
-
714
671
  # Stop heartbeat thread
715
672
  stop_heartbeat_event.set()
716
673
  if heartbeat_thread and heartbeat_thread.is_alive():
717
- heartbeat_thread.join(timeout=2) # Give it a moment to stop
674
+ heartbeat_thread.join(timeout=2)
718
675
 
719
- # Report final status if a session was started
720
- if session_id:
721
- final_status = "unknown"
722
- final_reason = "unknown_termination"
676
+ # Report final status if a run was started
677
+ if run_id:
678
+ final_status_update = "unknown"
679
+ final_reason_code = "unknown_termination"
723
680
  final_details = None
724
-
725
681
  if optimization_completed_normally:
726
- final_status = "completed"
727
- final_reason = "completed_successfully"
682
+ final_status_update = "completed"
683
+ final_reason_code = "completed_successfully"
684
+ elif user_stop_requested_flag:
685
+ final_status_update = "terminated"
686
+ final_reason_code = "user_requested_stop"
687
+ final_details = "Run stopped by user request via dashboard."
728
688
  else:
729
- # If an exception was caught and we have details
689
+ final_status_update = "error"
690
+ final_reason_code = "error_cli_internal"
730
691
  if "error_details" in locals():
731
- final_status = "error"
732
- final_reason = "error_cli_internal"
733
- final_details = error_details
734
- # else: # Should have been handled by signal handler if terminated by user
735
- # Keep default 'unknown' if we somehow end up here without error/completion/signal
736
-
692
+ final_details = locals()["error_details"]
693
+ elif "e" in locals() and isinstance(locals()["e"], Exception):
694
+ final_details = traceback.format_exc()
695
+ else:
696
+ final_details = "CLI terminated unexpectedly without a specific exception captured."
697
+ # Keep default 'unknown' if we somehow end up here without error/completion/signal
737
698
  # Avoid reporting if terminated by signal handler (already reported)
738
699
  # Check a flag or rely on status not being 'unknown'
739
- if final_status != "unknown":
700
+ if final_status_update != "unknown":
740
701
  report_termination(
741
- session_id=session_id,
742
- status_update=final_status,
743
- reason=final_reason,
702
+ run_id=run_id,
703
+ status_update=final_status_update,
704
+ reason=final_reason_code,
744
705
  details=final_details,
745
- auth_headers=auth_headers,
706
+ auth_headers=current_auth_headers_for_heartbeat,
746
707
  )
747
-
748
- # Ensure proper exit code if an error occurred
749
- if not optimization_completed_normally and "exit_code" in locals() and exit_code != 0:
750
- sys.exit(exit_code)
751
- elif not optimization_completed_normally:
752
- # Generic error exit if no specific code was set but try block failed
753
- sys.exit(1)
754
- else:
755
- # Normal exit
708
+ if optimization_completed_normally:
756
709
  sys.exit(0)
710
+ elif user_stop_requested_flag:
711
+ console.print("[yellow]Run terminated by user request.[/]")
712
+ sys.exit(0)
713
+ else:
714
+ sys.exit(locals().get("exit_code", 1))
weco/panels.py CHANGED
@@ -11,9 +11,9 @@ from .__init__ import __dashboard_url__
11
11
 
12
12
 
13
13
  class SummaryPanel:
14
- """Holds a summary of the optimization session."""
14
+ """Holds a summary of the optimization run."""
15
15
 
16
- def __init__(self, maximize: bool, metric_name: str, total_steps: int, model: str, runs_dir: str, session_id: str = None):
16
+ def __init__(self, maximize: bool, metric_name: str, total_steps: int, model: str, runs_dir: str, run_id: str = None):
17
17
  self.maximize = maximize
18
18
  self.metric_name = metric_name
19
19
  self.goal = ("Maximizing" if self.maximize else "Minimizing") + f" {self.metric_name}..."
@@ -22,7 +22,7 @@ class SummaryPanel:
22
22
  self.total_steps = total_steps
23
23
  self.model = model
24
24
  self.runs_dir = runs_dir
25
- self.session_id = session_id if session_id is not None else "N/A"
25
+ self.run_id = run_id if run_id is not None else "N/A"
26
26
  self.dashboard_url = "N/A"
27
27
  self.progress = Progress(
28
28
  TextColumn("[progress.description]{task.description}"),
@@ -34,14 +34,14 @@ class SummaryPanel:
34
34
  )
35
35
  self.task_id = self.progress.add_task("", total=total_steps)
36
36
 
37
- def set_session_id(self, session_id: str):
38
- """Set the session ID."""
39
- self.session_id = session_id
40
- self.set_dashboard_url(session_id=session_id)
37
+ def set_run_id(self, run_id: str):
38
+ """Set the run ID."""
39
+ self.run_id = run_id
40
+ self.set_dashboard_url(run_id=run_id)
41
41
 
42
- def set_dashboard_url(self, session_id: str):
42
+ def set_dashboard_url(self, run_id: str):
43
43
  """Set the dashboard URL."""
44
- self.dashboard_url = f"{__dashboard_url__}/runs/{session_id}"
44
+ self.dashboard_url = f"{__dashboard_url__}/runs/{run_id}"
45
45
 
46
46
  def set_step(self, step: int):
47
47
  """Set the current step."""
@@ -70,7 +70,7 @@ class SummaryPanel:
70
70
  summary_table.add_row(f"[bold cyan]Model:[/] {self.model}")
71
71
  summary_table.add_row("")
72
72
  # Log directory
73
- summary_table.add_row(f"[bold cyan]Logs:[/] [blue underline]{self.runs_dir}/{self.session_id}[/]")
73
+ summary_table.add_row(f"[bold cyan]Logs:[/] [blue underline]{self.runs_dir}/{self.run_id}[/]")
74
74
  summary_table.add_row("")
75
75
  # Dashboard link
76
76
  summary_table.add_row(f"[bold cyan]Dashboard:[/] [blue underline]{self.dashboard_url}[/]")
@@ -175,6 +175,9 @@ class MetricTreePanel:
175
175
 
176
176
  def build_metric_tree(self, nodes: List[dict]):
177
177
  """Build the tree from the list of nodes."""
178
+ # Defensive: treat None as empty list
179
+ if nodes is None:
180
+ nodes = []
178
181
  # First clear then tree
179
182
  self.metric_tree.clear()
180
183
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: weco
3
- Version: 0.2.19
3
+ Version: 0.2.20
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: MIT
@@ -29,6 +29,9 @@ Dynamic: license-file
29
29
  [![docs](https://img.shields.io/website?url=https://docs.weco.ai/&label=docs)](https://docs.weco.ai/)
30
30
  [![PyPI version](https://badge.fury.io/py/weco.svg)](https://badge.fury.io/py/weco)
31
31
  [![AIDE](https://img.shields.io/badge/AI--Driven_Exploration-arXiv-orange?style=flat-square&logo=arxiv)](https://arxiv.org/abs/2502.13138)
32
+ [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/WecoAI/weco-cli/blob/main/examples/hello-kernel-world/colab_notebook_walkthrough.ipynb)
33
+
34
+ `pip install weco`
32
35
 
33
36
  </div>
34
37
 
@@ -0,0 +1,12 @@
1
+ weco/__init__.py,sha256=npWmRgLxfVK69GdyxIujnI87xqmPCBrZWxxAxL_QQOc,478
2
+ weco/api.py,sha256=xHCyQPto1Lv9QysiOFwVf5NnWDh6LBCNfPLyq-L7nys,5873
3
+ weco/auth.py,sha256=IPfiLthcNRkPyM8pWHTyDLvikw83sigacpY1PmeA03Y,2343
4
+ weco/cli.py,sha256=e4h5bxeg2n95AlYXanfxLbcURWchjWTES2Kwx5AjKn0,36115
5
+ weco/panels.py,sha256=lsTHTh-XdYMH3ZV_WBteEcIt2hTWGGtqfUjGlYRHl70,13598
6
+ weco/utils.py,sha256=LVTBo3dduJmhlbotcYoUW2nLx6IRtKs4eDFR52Qltcg,5244
7
+ weco-0.2.20.dist-info/licenses/LICENSE,sha256=p_GQqJBvuZgkLNboYKyH-5dhpTDlKs2wq2TVM55WrWE,1065
8
+ weco-0.2.20.dist-info/METADATA,sha256=rK-Y9Q0zwaKUBS0bNZfUNvL82RXUiijVcIbW3i_IKKk,10955
9
+ weco-0.2.20.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
+ weco-0.2.20.dist-info/entry_points.txt,sha256=ixJ2uClALbCpBvnIR6BXMNck8SHAab8eVkM9pIUowcs,39
11
+ weco-0.2.20.dist-info/top_level.txt,sha256=F0N7v6e2zBSlsorFv-arAq2yDxQbzX3KVO8GxYhPUeE,5
12
+ weco-0.2.20.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.7.1)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,12 +0,0 @@
1
- weco/__init__.py,sha256=npWmRgLxfVK69GdyxIujnI87xqmPCBrZWxxAxL_QQOc,478
2
- weco/api.py,sha256=lJJ0j0-bABiQXDlRb43fCo7ky0N_HwfZgFdMktRKQ90,6635
3
- weco/auth.py,sha256=IPfiLthcNRkPyM8pWHTyDLvikw83sigacpY1PmeA03Y,2343
4
- weco/cli.py,sha256=eI468fxpMTfGPL-aX6EMYxh0NuaRxpaLVF_Jj2DiFhU,36383
5
- weco/panels.py,sha256=pM_YGnmcXM_1CBcxo_EAzOV3g_4NFdLS4MqDqx7THbA,13563
6
- weco/utils.py,sha256=LVTBo3dduJmhlbotcYoUW2nLx6IRtKs4eDFR52Qltcg,5244
7
- weco-0.2.19.dist-info/licenses/LICENSE,sha256=p_GQqJBvuZgkLNboYKyH-5dhpTDlKs2wq2TVM55WrWE,1065
8
- weco-0.2.19.dist-info/METADATA,sha256=3VBVsCqr7p332A10KsLr168GvOIKcCOWWfGDv8ViF7I,10729
9
- weco-0.2.19.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
10
- weco-0.2.19.dist-info/entry_points.txt,sha256=ixJ2uClALbCpBvnIR6BXMNck8SHAab8eVkM9pIUowcs,39
11
- weco-0.2.19.dist-info/top_level.txt,sha256=F0N7v6e2zBSlsorFv-arAq2yDxQbzX3KVO8GxYhPUeE,5
12
- weco-0.2.19.dist-info/RECORD,,