weco 0.2.17__py3-none-any.whl → 0.2.19__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/__init__.py CHANGED
@@ -1,7 +1,8 @@
1
1
  import os
2
+ import importlib.metadata
2
3
 
3
4
  # DO NOT EDIT
4
- __pkg_version__ = "0.2.17"
5
+ __pkg_version__ = importlib.metadata.version("weco")
5
6
  __api_version__ = "v1"
6
7
 
7
8
  __base_url__ = f"https://api.weco.ai/{__api_version__}"
weco/api.py CHANGED
@@ -1,14 +1,20 @@
1
- from typing import Dict, Any
1
+ from typing import Dict, Any, Optional
2
2
  import rich
3
3
  import requests
4
4
  from weco import __pkg_version__, __base_url__
5
5
  import sys
6
+ from rich.console import Console
6
7
 
7
8
 
8
9
  def handle_api_error(e: requests.exceptions.HTTPError, console: rich.console.Console) -> None:
9
10
  """Extract and display error messages from API responses in a structured format."""
10
- console.print(f"[bold red]{e.response.json()['detail']}[/]")
11
- sys.exit(1)
11
+ try:
12
+ detail = e.response.json()["detail"]
13
+ except (ValueError, KeyError): # Handle cases where response is not JSON or detail key is missing
14
+ detail = f"HTTP {e.response.status_code} Error: {e.response.text}"
15
+ console.print(f"[bold red]{detail}[/]")
16
+ # Avoid exiting here, let the caller decide if the error is fatal
17
+ # sys.exit(1)
12
18
 
13
19
 
14
20
  def start_optimization_session(
@@ -28,25 +34,32 @@ def start_optimization_session(
28
34
  ) -> Dict[str, Any]:
29
35
  """Start the optimization session."""
30
36
  with console.status("[bold green]Starting Optimization..."):
31
- response = requests.post(
32
- f"{__base_url__}/sessions", # Path is relative to base_url
33
- json={
34
- "source_code": source_code,
35
- "additional_instructions": additional_instructions,
36
- "objective": {"evaluation_command": evaluation_command, "metric_name": metric_name, "maximize": maximize},
37
- "optimizer": {
38
- "steps": steps,
39
- "code_generator": code_generator_config,
40
- "evaluator": evaluator_config,
41
- "search_policy": search_policy_config,
37
+ try:
38
+ response = requests.post(
39
+ f"{__base_url__}/sessions", # Path is relative to base_url
40
+ json={
41
+ "source_code": source_code,
42
+ "additional_instructions": additional_instructions,
43
+ "objective": {"evaluation_command": evaluation_command, "metric_name": metric_name, "maximize": maximize},
44
+ "optimizer": {
45
+ "steps": steps,
46
+ "code_generator": code_generator_config,
47
+ "evaluator": evaluator_config,
48
+ "search_policy": search_policy_config,
49
+ },
50
+ "metadata": {"client_name": "cli", "client_version": __pkg_version__, **api_keys},
42
51
  },
43
- "metadata": {"client_name": "cli", "client_version": __pkg_version__, **api_keys},
44
- },
45
- headers=auth_headers, # Add headers
46
- timeout=timeout,
47
- )
48
- response.raise_for_status()
49
- return response.json()
52
+ headers=auth_headers, # Add headers
53
+ timeout=timeout,
54
+ )
55
+ response.raise_for_status()
56
+ return response.json()
57
+ except requests.exceptions.HTTPError as e:
58
+ handle_api_error(e, console)
59
+ sys.exit(1) # Exit if starting session fails
60
+ except requests.exceptions.RequestException as e:
61
+ console.print(f"[bold red]Network Error starting session: {e}[/]")
62
+ sys.exit(1)
50
63
 
51
64
 
52
65
  def evaluate_feedback_then_suggest_next_solution(
@@ -58,29 +71,92 @@ def evaluate_feedback_then_suggest_next_solution(
58
71
  timeout: int = 800,
59
72
  ) -> Dict[str, Any]:
60
73
  """Evaluate the feedback and suggest the next solution."""
61
- response = requests.post(
62
- f"{__base_url__}/sessions/{session_id}/suggest", # Path is relative to base_url
63
- json={
64
- "execution_output": execution_output,
65
- "additional_instructions": additional_instructions,
66
- "metadata": {**api_keys},
67
- },
68
- headers=auth_headers, # Add headers
69
- timeout=timeout,
70
- )
71
- response.raise_for_status()
72
- return response.json()
74
+ try:
75
+ response = requests.post(
76
+ f"{__base_url__}/sessions/{session_id}/suggest", # Path is relative to base_url
77
+ json={
78
+ "execution_output": execution_output,
79
+ "additional_instructions": additional_instructions,
80
+ "metadata": {**api_keys},
81
+ },
82
+ headers=auth_headers, # Add headers
83
+ timeout=timeout,
84
+ )
85
+ response.raise_for_status()
86
+ return response.json()
87
+ except requests.exceptions.HTTPError as e:
88
+ # Allow caller to handle suggest errors, maybe retry or terminate
89
+ handle_api_error(e, Console()) # Use default console if none passed
90
+ raise # Re-raise the exception
91
+ except requests.exceptions.RequestException as e:
92
+ print(f"Network Error during suggest: {e}") # Use print as console might not be available
93
+ raise # Re-raise the exception
73
94
 
74
95
 
75
96
  def get_optimization_session_status(
76
97
  session_id: str, include_history: bool = False, auth_headers: dict = {}, timeout: int = 800
77
98
  ) -> Dict[str, Any]:
78
99
  """Get the current status of the optimization session."""
79
- response = requests.get(
80
- f"{__base_url__}/sessions/{session_id}", # Path is relative to base_url
81
- params={"include_history": include_history},
82
- headers=auth_headers,
83
- timeout=timeout,
84
- )
85
- response.raise_for_status()
86
- return response.json()
100
+ try:
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,
106
+ )
107
+ response.raise_for_status()
108
+ return response.json()
109
+ except requests.exceptions.HTTPError as e:
110
+ handle_api_error(e, Console()) # Use default console
111
+ raise # Re-raise
112
+ except requests.exceptions.RequestException as e:
113
+ print(f"Network Error getting status: {e}")
114
+ raise # Re-raise
115
+
116
+
117
+ def send_heartbeat(
118
+ session_id: str,
119
+ auth_headers: dict = {},
120
+ timeout: int = 10, # Shorter timeout for non-critical heartbeat
121
+ ) -> bool:
122
+ """Send a heartbeat signal to the backend."""
123
+ 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)
126
+ return True
127
+ except requests.exceptions.HTTPError as e:
128
+ # Log non-critical errors like 409 Conflict (session not running)
129
+ if e.response.status_code == 409:
130
+ print(f"Heartbeat ignored: Session {session_id} is not running.", file=sys.stderr)
131
+ 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
134
+ return False
135
+ 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)
138
+ return False
139
+
140
+
141
+ 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
148
+ ) -> bool:
149
+ """Report the termination reason to the backend."""
150
+ try:
151
+ response = requests.post(
152
+ f"{__base_url__}/sessions/{session_id}/terminate",
153
+ json={"status_update": status_update, "termination_reason": reason, "termination_details": details},
154
+ headers=auth_headers,
155
+ timeout=timeout,
156
+ )
157
+ response.raise_for_status()
158
+ return True
159
+ 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)
162
+ return False
weco/cli.py CHANGED
@@ -5,6 +5,9 @@ import math
5
5
  import time
6
6
  import requests
7
7
  import webbrowser
8
+ import threading
9
+ import signal
10
+ import traceback
8
11
  from rich.console import Console
9
12
  from rich.live import Live
10
13
  from rich.panel import Panel
@@ -15,6 +18,8 @@ from .api import (
15
18
  evaluate_feedback_then_suggest_next_solution,
16
19
  get_optimization_session_status,
17
20
  handle_api_error,
21
+ send_heartbeat,
22
+ report_termination,
18
23
  )
19
24
 
20
25
  from . import __base_url__
@@ -37,11 +42,73 @@ from .utils import (
37
42
  run_evaluation,
38
43
  smooth_update,
39
44
  format_number,
45
+ check_for_cli_updates,
40
46
  )
41
47
 
42
48
  install(show_locals=True)
43
49
  console = Console()
44
50
 
51
+ # --- Global variable for heartbeat thread ---
52
+ heartbeat_thread = None
53
+ stop_heartbeat_event = threading.Event()
54
+ current_session_id_for_heartbeat = None
55
+ current_auth_headers_for_heartbeat = {}
56
+
57
+
58
+ # --- Heartbeat Sender Class ---
59
+ class HeartbeatSender(threading.Thread):
60
+ def __init__(self, session_id: str, auth_headers: dict, stop_event: threading.Event, interval: int = 30):
61
+ super().__init__(daemon=True) # Daemon thread exits when main thread exits
62
+ self.session_id = session_id
63
+ self.auth_headers = auth_headers
64
+ self.interval = interval
65
+ self.stop_event = stop_event
66
+
67
+ def run(self):
68
+ try:
69
+ while not self.stop_event.is_set():
70
+ if not send_heartbeat(self.session_id, self.auth_headers):
71
+ # send_heartbeat itself prints errors to stderr if it returns False
72
+ # No explicit HeartbeatSender log needed here unless more detail is desired for a False return
73
+ pass # Continue trying as per original logic
74
+
75
+ if self.stop_event.is_set(): # Check before waiting for responsiveness
76
+ break
77
+
78
+ self.stop_event.wait(self.interval) # Wait for interval or stop signal
79
+
80
+ except Exception as e:
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
+ )
85
+ traceback.print_exc(file=sys.stderr)
86
+ # The loop will break due to the exception, and thread will terminate via finally.
87
+
88
+
89
+ # --- Signal Handling ---
90
+ def signal_handler(signum, frame):
91
+ signal_name = signal.Signals(signum).name
92
+ console.print(f"\n[bold yellow]Termination signal ({signal_name}) received. Shutting down...[/]")
93
+
94
+ # Stop heartbeat thread
95
+ stop_heartbeat_event.set()
96
+ if heartbeat_thread and heartbeat_thread.is_alive():
97
+ heartbeat_thread.join(timeout=2) # Give it a moment to stop
98
+
99
+ # Report termination (best effort)
100
+ if current_session_id_for_heartbeat:
101
+ report_termination(
102
+ session_id=current_session_id_for_heartbeat,
103
+ status_update="terminated",
104
+ reason=f"user_terminated_{signal_name.lower()}",
105
+ details=f"Process terminated by signal {signal_name} ({signum}).",
106
+ auth_headers=current_auth_headers_for_heartbeat,
107
+ )
108
+
109
+ # Exit gracefully
110
+ sys.exit(0)
111
+
45
112
 
46
113
  def perform_login(console: Console):
47
114
  """Handles the device login flow."""
@@ -161,6 +228,16 @@ def perform_login(console: Console):
161
228
 
162
229
  def main() -> None:
163
230
  """Main function for the Weco CLI."""
231
+ # Setup signal handlers
232
+ signal.signal(signal.SIGINT, signal_handler)
233
+ signal.signal(signal.SIGTERM, signal_handler)
234
+
235
+ # --- 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
+ from . import __pkg_version__
238
+
239
+ check_for_cli_updates(__pkg_version__) # Call the imported function
240
+
164
241
  # --- Argument Parsing ---
165
242
  parser = argparse.ArgumentParser(
166
243
  description="[bold cyan]Weco CLI[/]", formatter_class=argparse.RawDescriptionHelpFormatter
@@ -170,29 +247,55 @@ def main() -> None:
170
247
 
171
248
  # --- Run Command ---
172
249
  run_parser = subparsers.add_parser(
173
- "run", help="Run code optimization", formatter_class=argparse.RawDescriptionHelpFormatter
250
+ "run", help="Run code optimization", formatter_class=argparse.RawDescriptionHelpFormatter, allow_abbrev=False
174
251
  )
175
252
  # Add arguments specific to the 'run' command to the run_parser
176
- run_parser.add_argument("--source", type=str, required=True, help="Path to the source code file (e.g. optimize.py)")
177
253
  run_parser.add_argument(
178
- "--eval-command", type=str, required=True, help="Command to run for evaluation (e.g. 'python eval.py --arg1=val1')"
254
+ "-s",
255
+ "--source",
256
+ type=str,
257
+ required=True,
258
+ help="Path to the source code file that will be optimized (e.g., `optimize.py`)",
179
259
  )
180
- run_parser.add_argument("--metric", type=str, required=True, help="Metric to optimize")
181
260
  run_parser.add_argument(
182
- "--maximize",
261
+ "-c",
262
+ "--eval-command",
183
263
  type=str,
184
- choices=["true", "false"],
185
264
  required=True,
186
- help="Specify 'true' to maximize the metric or 'false' to minimize.",
265
+ help="Command to run for evaluation (e.g. 'python eval.py --arg1=val1').",
187
266
  )
188
- run_parser.add_argument("--steps", type=int, required=True, help="Number of steps to run")
189
- run_parser.add_argument("--model", type=str, required=True, help="Model to use for optimization")
190
- run_parser.add_argument("--log-dir", type=str, default=".runs", help="Directory to store logs and results")
191
267
  run_parser.add_argument(
268
+ "-m",
269
+ "--metric",
270
+ type=str,
271
+ required=True,
272
+ help="Metric to optimize (e.g. 'accuracy', 'loss', 'f1_score') that is printed to the terminal by the eval command.",
273
+ )
274
+ run_parser.add_argument(
275
+ "-g",
276
+ "--goal",
277
+ type=str,
278
+ choices=["maximize", "max", "minimize", "min"],
279
+ required=True,
280
+ help="Specify 'maximize'/'max' to maximize the metric or 'minimize'/'min' to minimize it.",
281
+ )
282
+ run_parser.add_argument("-n", "--steps", type=int, default=100, help="Number of steps to run. Defaults to 100.")
283
+ run_parser.add_argument(
284
+ "-M",
285
+ "--model",
286
+ type=str,
287
+ default=None,
288
+ help="Model to use for optimization. Defaults to `o4-mini` when `OPENAI_API_KEY` is set, `claude-3-7-sonnet-20250219` when `ANTHROPIC_API_KEY` is set, and `gemini-2.5-pro-exp-03-25` when `GEMINI_API_KEY` is set. When multiple keys are set, the priority is `OPENAI_API_KEY` > `ANTHROPIC_API_KEY` > `GEMINI_API_KEY`.",
289
+ )
290
+ run_parser.add_argument(
291
+ "-l", "--log-dir", type=str, default=".runs", help="Directory to store logs and results. Defaults to `.runs`."
292
+ )
293
+ run_parser.add_argument(
294
+ "-i",
192
295
  "--additional-instructions",
193
296
  default=None,
194
297
  type=str,
195
- help="Description of additional instruction or path to a file containing additional instructions",
298
+ help="Description of additional instruction or path to a file containing additional instructions. Defaults to None.",
196
299
  )
197
300
 
198
301
  # --- Logout Command ---
@@ -207,6 +310,10 @@ def main() -> None:
207
310
 
208
311
  # --- Handle Run Command ---
209
312
  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
210
317
  # --- Check Authentication ---
211
318
  weco_api_key = load_weco_api_key()
212
319
  llm_api_keys = read_api_keys_from_env() # Read keys from client environment
@@ -238,18 +345,29 @@ def main() -> None:
238
345
 
239
346
  # --- Prepare API Call Arguments ---
240
347
  auth_headers = {}
241
-
242
348
  if weco_api_key:
243
349
  auth_headers["Authorization"] = f"Bearer {weco_api_key}"
244
- # Backend will decide whether to use client keys based on auth status
350
+ current_auth_headers_for_heartbeat = auth_headers # Store for signal handler
245
351
 
246
352
  # --- Main Run Logic ---
247
353
  try:
248
354
  # --- Configuration Loading ---
249
355
  evaluation_command = args.eval_command
250
356
  metric_name = args.metric
251
- maximize = args.maximize == "true"
357
+ maximize = args.goal in ["maximize", "max"]
252
358
  steps = args.steps
359
+ # Determine the model to use
360
+ if args.model is None:
361
+ if "OPENAI_API_KEY" in llm_api_keys:
362
+ args.model = "o4-mini"
363
+ elif "ANTHROPIC_API_KEY" in llm_api_keys:
364
+ args.model = "claude-3-7-sonnet-20250219"
365
+ elif "GEMINI_API_KEY" in llm_api_keys:
366
+ args.model = "gemini-2.5-pro-exp-03-25"
367
+ else:
368
+ raise ValueError(
369
+ "No LLM API keys found in environment. Please set one of the following: OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY."
370
+ )
253
371
  code_generator_config = {"model": args.model}
254
372
  evaluator_config = {"model": args.model, "include_analysis": True}
255
373
  search_policy_config = {
@@ -289,16 +407,22 @@ def main() -> None:
289
407
  evaluator_config=evaluator_config,
290
408
  search_policy_config=search_policy_config,
291
409
  additional_instructions=additional_instructions,
292
- api_keys=llm_api_keys, # Pass client LLM keys
293
- auth_headers=auth_headers, # Pass Weco key if logged in
410
+ api_keys=llm_api_keys,
411
+ auth_headers=auth_headers,
294
412
  timeout=timeout,
295
413
  )
414
+ session_id = session_response["session_id"]
415
+ current_session_id_for_heartbeat = session_id # Store for signal handler/finally
416
+
417
+ # --- 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)
420
+ heartbeat_thread.start()
296
421
 
297
422
  # --- Live Update Loop ---
298
423
  refresh_rate = 4
299
424
  with Live(layout, refresh_per_second=refresh_rate, screen=True) as live:
300
425
  # Define the runs directory (.runs/<session-id>)
301
- session_id = session_response["session_id"]
302
426
  runs_dir = pathlib.Path(args.log_dir) / session_id
303
427
  runs_dir.mkdir(parents=True, exist_ok=True)
304
428
 
@@ -358,6 +482,9 @@ def main() -> None:
358
482
  transition_delay=0.1,
359
483
  )
360
484
 
485
+ # # Send initial heartbeat immediately after starting
486
+ # send_heartbeat(session_id, auth_headers)
487
+
361
488
  # Run evaluation on the initial solution
362
489
  term_out = run_evaluation(eval_command=args.eval_command)
363
490
 
@@ -386,6 +513,7 @@ def main() -> None:
386
513
  auth_headers=auth_headers, # Pass Weco key if logged in
387
514
  timeout=timeout,
388
515
  )
516
+
389
517
  # Save next solution (.runs/<session-id>/step_<step>.<extension>)
390
518
  write_to_path(
391
519
  fp=runs_dir / f"step_{step}{source_fp.suffix}", content=eval_and_next_solution_response["code"]
@@ -476,7 +604,7 @@ def main() -> None:
476
604
  additional_instructions=args.additional_instructions
477
605
  )
478
606
 
479
- # Ensure we pass evaluation results for the last step's generated solution
607
+ # Final evaluation report
480
608
  eval_and_next_solution_response = evaluate_feedback_then_suggest_next_solution(
481
609
  session_id=session_id,
482
610
  execution_output=term_out,
@@ -555,14 +683,74 @@ def main() -> None:
555
683
  # write the best solution to the source file
556
684
  write_to_path(fp=source_fp, content=best_solution_content)
557
685
 
686
+ # Mark as completed normally for the finally block
687
+ optimization_completed_normally = True
688
+
558
689
  console.print(end_optimization_layout)
559
690
 
560
691
  except Exception as e:
692
+ # Catch errors during the main optimization loop or setup
561
693
  try:
562
- error_message = e.response.json()["detail"]
694
+ error_message = e.response.json()["detail"] # Try to get API error detail
563
695
  except Exception:
564
- error_message = str(e)
565
- console.print(Panel(f"[bold red]Error: {error_message}", title="[bold red]Error", border_style="red"))
566
- # Print traceback for debugging
696
+ error_message = str(e) # Otherwise, use the exception string
697
+ 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)
567
699
  # console.print_exception(show_locals=False)
568
- sys.exit(1)
700
+
701
+ # Ensure optimization_completed_normally is False
702
+ optimization_completed_normally = False
703
+
704
+ # Prepare details for termination report
705
+ 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
+
711
+ finally:
712
+ # This block runs whether the try block completed normally or raised an exception
713
+
714
+ # Stop heartbeat thread
715
+ stop_heartbeat_event.set()
716
+ if heartbeat_thread and heartbeat_thread.is_alive():
717
+ heartbeat_thread.join(timeout=2) # Give it a moment to stop
718
+
719
+ # Report final status if a session was started
720
+ if session_id:
721
+ final_status = "unknown"
722
+ final_reason = "unknown_termination"
723
+ final_details = None
724
+
725
+ if optimization_completed_normally:
726
+ final_status = "completed"
727
+ final_reason = "completed_successfully"
728
+ else:
729
+ # If an exception was caught and we have details
730
+ 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
+
737
+ # Avoid reporting if terminated by signal handler (already reported)
738
+ # Check a flag or rely on status not being 'unknown'
739
+ if final_status != "unknown":
740
+ report_termination(
741
+ session_id=session_id,
742
+ status_update=final_status,
743
+ reason=final_reason,
744
+ details=final_details,
745
+ auth_headers=auth_headers,
746
+ )
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
756
+ sys.exit(0)
weco/panels.py CHANGED
@@ -253,7 +253,7 @@ class MetricTreePanel:
253
253
  # Make sure the metric tree is built before calling build_rich_tree
254
254
  return Panel(
255
255
  self._build_rich_tree(),
256
- title="[bold]🔎 Exploring Solutions..." if not is_done else "[bold]🔎 Optimization Complete!",
256
+ title=("[bold]🔎 Exploring Solutions..." if not is_done else "[bold]🔎 Optimization Complete!"),
257
257
  border_style="green",
258
258
  expand=True,
259
259
  padding=(0, 1),
weco/utils.py CHANGED
@@ -7,6 +7,8 @@ from rich.layout import Layout
7
7
  from rich.live import Live
8
8
  from rich.panel import Panel
9
9
  import pathlib
10
+ import requests
11
+ from packaging.version import parse as parse_version
10
12
 
11
13
 
12
14
  # Env/arg helper functions
@@ -109,3 +111,33 @@ def run_evaluation(eval_command: str) -> str:
109
111
  output += "\n"
110
112
  output += result.stdout
111
113
  return output
114
+
115
+
116
+ # Update Check Function
117
+ def check_for_cli_updates(current_version_str: str):
118
+ """Checks PyPI for a newer version of the weco package and notifies the user."""
119
+ try:
120
+ pypi_url = "https://pypi.org/pypi/weco/json"
121
+ response = requests.get(pypi_url, timeout=5) # Short timeout for non-critical check
122
+ response.raise_for_status()
123
+ latest_version_str = response.json()["info"]["version"]
124
+
125
+ current_version = parse_version(current_version_str)
126
+ latest_version = parse_version(latest_version_str)
127
+
128
+ if latest_version > current_version:
129
+ yellow_start = "\033[93m"
130
+ reset_color = "\033[0m"
131
+ message = f"WARNING: New weco version ({latest_version_str}) available (you have {current_version_str}). Run: pip install --upgrade weco"
132
+ print(f"{yellow_start}{message}{reset_color}")
133
+ time.sleep(2) # Wait for 2 second
134
+
135
+ except requests.exceptions.RequestException:
136
+ # Silently fail on network errors, etc. Don't disrupt user.
137
+ pass
138
+ except (KeyError, ValueError):
139
+ # Handle cases where the PyPI response format might be unexpected
140
+ pass
141
+ except Exception:
142
+ # Catch any other unexpected error during the check
143
+ pass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: weco
3
- Version: 0.2.17
3
+ Version: 0.2.19
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
@@ -14,6 +14,7 @@ Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
15
  Requires-Dist: requests
16
16
  Requires-Dist: rich
17
+ Requires-Dist: packaging
17
18
  Provides-Extra: dev
18
19
  Requires-Dist: ruff; extra == "dev"
19
20
  Requires-Dist: build; extra == "dev"
@@ -22,15 +23,13 @@ Dynamic: license-file
22
23
 
23
24
  <div align="center">
24
25
 
25
- # Weco: The AI Code Optimizer
26
+ # Weco: The Platform for Self-Improving Code
26
27
 
27
28
  [![Python](https://img.shields.io/badge/Python-3.8.0+-blue)](https://www.python.org)
28
29
  [![docs](https://img.shields.io/website?url=https://docs.weco.ai/&label=docs)](https://docs.weco.ai/)
29
30
  [![PyPI version](https://badge.fury.io/py/weco.svg)](https://badge.fury.io/py/weco)
30
31
  [![AIDE](https://img.shields.io/badge/AI--Driven_Exploration-arXiv-orange?style=flat-square&logo=arxiv)](https://arxiv.org/abs/2502.13138)
31
32
 
32
- <code>pip install weco</code>
33
-
34
33
  </div>
35
34
 
36
35
  ---
@@ -39,9 +38,9 @@ Weco systematically optimizes your code, guided directly by your evaluation metr
39
38
 
40
39
  Example applications include:
41
40
 
42
- - **GPU Kernel Optimization**: Reimplement PyTorch functions using CUDA or Triton optimizing for `latency`, `throughput`, or `memory_bandwidth`.
43
- - **Model Development**: Tune feature transformations or architectures, optimizing for `validation_accuracy`, `AUC`, or `Sharpe Ratio`.
44
- - **Prompt Engineering**: Refine prompts for LLMs, optimizing for `win_rate`, `relevance`, or `format_adherence`
41
+ - **GPU Kernel Optimization**: Reimplement PyTorch functions using [CUDA](/examples/cuda/README.md) or [Triton](/examples/triton/README.md), optimizing for `latency`, `throughput`, or `memory_bandwidth`.
42
+ - **Model Development**: Tune feature transformations, architectures or [the whole training pipeline](/examples/spaceship-titanic/README.md), optimizing for `validation_accuracy`, `AUC`, or `Sharpe Ratio`.
43
+ - **Prompt Engineering**: Refine prompts for LLMs (e.g., for [math problems](/examples/prompt/README.md)), optimizing for `win_rate`, `relevance`, or `format_adherence`
45
44
 
46
45
  ![image](assets/example-optimization.gif)
47
46
 
@@ -71,29 +70,9 @@ The `weco` CLI leverages a tree search approach guided by Large Language Models
71
70
  - **Anthropic:** `export ANTHROPIC_API_KEY="your_key_here"`
72
71
  - **Google DeepMind:** `export GEMINI_API_KEY="your_key_here"` (Google AI Studio has a free API usage quota. Create a key [here](https://aistudio.google.com/apikey) to use `weco` for free.)
73
72
 
74
- The optimization process will fail if the necessary keys for the chosen model are not found in your environment.
75
-
76
- 3. **Log In to Weco (Optional):**
77
-
78
- To associate your optimization runs with your Weco account and view them on the Weco dashboard, you can log in. `weco` uses a device authentication flow:
79
-
80
- - When you first run `weco run`, you'll be prompted if you want to log in or proceed anonymously.
81
- - If you choose to log in (by pressing `l`), you'll be shown a URL and `weco` will attempt to open it in your default web browser.
82
- - You then authenticate in the browser. Once authenticated, the CLI will detect this and complete the login.
83
- - This saves a Weco-specific API key locally (typically at `~/.config/weco/credentials.json`).
84
-
85
- If you choose to skip login (by pressing Enter or `s`), `weco` will still function using the environment variable LLM keys, but the run history will not be linked to a Weco account.
86
-
87
- To log out and remove your saved Weco API key, use the `weco logout` command.
88
-
89
73
  ---
90
74
 
91
- ## Usage
92
-
93
- The CLI has two main commands:
94
-
95
- - `weco run`: Initiates the code optimization process.
96
- - `weco logout`: Logs you out of your Weco account.
75
+ ## Get Started
97
76
 
98
77
  <div style="background-color: #fff3cd; border: 1px solid #ffeeba; padding: 15px; border-radius: 4px; margin-bottom: 15px;">
99
78
  <strong>⚠️ Warning: Code Modification</strong><br>
@@ -102,10 +81,6 @@ The CLI has two main commands:
102
81
 
103
82
  ---
104
83
 
105
- ### `weco run` Command
106
-
107
- This command starts the optimization process.
108
-
109
84
  **Example: Optimizing Simple PyTorch Operations**
110
85
 
111
86
  This basic example shows how to optimize a simple PyTorch function for speedup.
@@ -123,9 +98,8 @@ pip install torch
123
98
  weco run --source optimize.py \
124
99
  --eval-command "python evaluate.py --solution-path optimize.py --device cpu" \
125
100
  --metric speedup \
126
- --maximize true \
101
+ --goal maximize \
127
102
  --steps 15 \
128
- --model gemini-2.5-pro-exp-03-25 \
129
103
  --additional-instructions "Fuse operations in the forward method while ensuring the max float deviation remains small. Maintain the same format of the code."
130
104
  ```
131
105
 
@@ -133,28 +107,33 @@ weco run --source optimize.py \
133
107
 
134
108
  ---
135
109
 
136
- **Arguments for `weco run`:**
110
+ ### Arguments for `weco run`
137
111
 
138
- | Argument | Description | Required |
139
- | :-------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------- |
140
- | `--source` | Path to the source code file that will be optimized (e.g., `optimize.py`). | Yes |
141
- | `--eval-command` | Command to run for evaluating the code in `--source`. This command should print the target `--metric` and its value to the terminal (stdout/stderr). See note below. | Yes |
142
- | `--metric` | The name of the metric you want to optimize (e.g., 'accuracy', 'speedup', 'loss'). This metric name should match what's printed by your `--eval-command`. | Yes |
143
- | `--maximize` | Whether to maximize (`true`) or minimize (`false`) the metric. | Yes |
144
- | `--steps` | Number of optimization steps (LLM iterations) to run. | Yes |
145
- | `--model` | Model identifier for the LLM to use (e.g., `gpt-4o`, `claude-3.5-sonnet`). Recommended models to try include `o3-mini`, `claude-3-haiku`, and `gemini-2.5-pro-exp-03-25`. | Yes |
146
- | `--additional-instructions` | (Optional) Natural language description of specific instructions OR path to a file containing detailed instructions to guide the LLM. | No |
147
- | `--log-dir` | (Optional) Path to the directory to log intermediate steps and final optimization result. Defaults to `.runs/`. | No |
112
+ **Required:**
148
113
 
149
- ---
114
+ | Argument | Description |
115
+ | :------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
116
+ | `-s, --source` | Path to the source code file that will be optimized (e.g., `optimize.py`). |
117
+ | `-c, --eval-command`| Command to run for evaluating the code in `--source`. This command should print the target `--metric` and its value to the terminal (stdout/stderr). See note below. |
118
+ | `-m, --metric` | The name of the metric you want to optimize (e.g., 'accuracy', 'speedup', 'loss'). This metric name should match what's printed by your `--eval-command`. |
119
+ | `-g, --goal` | `maximize`/`max` to maximize the `--metric` or `minimize`/`min` to minimize it. |
150
120
 
151
- ### `weco logout` Command
121
+ <br>
152
122
 
153
- This command logs you out by removing the locally stored Weco API key.
123
+ **Optional:**
154
124
 
155
- ```bash
156
- weco logout
157
- ```
125
+ | Argument | Description | Default |
126
+ | :----------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------ |
127
+ | `-n, --steps` | Number of optimization steps (LLM iterations) to run. | 100 |
128
+ | `-M, --model` | Model identifier for the LLM to use (e.g., `gpt-4o`, `claude-3.5-sonnet`). | `o4-mini` when `OPENAI_API_KEY` is set; `claude-3-7-sonnet-20250219` when `ANTHROPIC_API_KEY` is set; `gemini-2.5-pro-exp-03-25` when `GEMINI_API_KEY` is set (priority: `OPENAI_API_KEY` > `ANTHROPIC_API_KEY` > `GEMINI_API_KEY`). |
129
+ | `-i, --additional-instructions`| Natural language description of specific instructions **or** path to a file containing detailed instructions to guide the LLM. | `None` |
130
+ | `-l, --log-dir` | Path to the directory to log intermediate steps and final optimization result. | `.runs/` |
131
+
132
+ ---
133
+
134
+ ### Weco Dashboard
135
+ To associate your optimization runs with your Weco account and view them on the Weco dashboard, you can log in. `weco` uses a device authentication flow
136
+ ![image (16)](https://github.com/user-attachments/assets/8a0a285b-4894-46fa-b6a2-4990017ca0c6)
158
137
 
159
138
  ---
160
139
 
@@ -0,0 +1,12 @@
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.3.1)
2
+ Generator: setuptools (80.7.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,12 +0,0 @@
1
- weco/__init__.py,sha256=aMh5ZK_EMG8kGfy59ah_5gRVfWeEXgBhcfReKNsqNmQ,426
2
- weco/api.py,sha256=0OU1hEhN7sbIZ1zj8TeeB0tMaxXk6n6qw82FmcdK0ec,3111
3
- weco/auth.py,sha256=IPfiLthcNRkPyM8pWHTyDLvikw83sigacpY1PmeA03Y,2343
4
- weco/cli.py,sha256=j8EyHVIIl2zNAjfVUoOFtJBZbDV69LrfnFA2WlDgbao,28488
5
- weco/panels.py,sha256=8DoTQC-epGpGjn-xDBcqelC5BKaX7JXnrJ97LInEbRU,13561
6
- weco/utils.py,sha256=hhIebUPnetFMfNSFfcsKVw1TSpeu_Zw3rBPPnxDie0U,3911
7
- weco-0.2.17.dist-info/licenses/LICENSE,sha256=p_GQqJBvuZgkLNboYKyH-5dhpTDlKs2wq2TVM55WrWE,1065
8
- weco-0.2.17.dist-info/METADATA,sha256=G2unWMKcPFan2r5a1kGlcizCQr1nVayJBVVLb-e-9fE,10795
9
- weco-0.2.17.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
10
- weco-0.2.17.dist-info/entry_points.txt,sha256=ixJ2uClALbCpBvnIR6BXMNck8SHAab8eVkM9pIUowcs,39
11
- weco-0.2.17.dist-info/top_level.txt,sha256=F0N7v6e2zBSlsorFv-arAq2yDxQbzX3KVO8GxYhPUeE,5
12
- weco-0.2.17.dist-info/RECORD,,