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 +2 -1
- weco/api.py +117 -41
- weco/cli.py +211 -23
- weco/panels.py +1 -1
- weco/utils.py +32 -0
- {weco-0.2.17.dist-info → weco-0.2.19.dist-info}/METADATA +30 -51
- weco-0.2.19.dist-info/RECORD +12 -0
- {weco-0.2.17.dist-info → weco-0.2.19.dist-info}/WHEEL +1 -1
- weco-0.2.17.dist-info/RECORD +0 -12
- {weco-0.2.17.dist-info → weco-0.2.19.dist-info}/entry_points.txt +0 -0
- {weco-0.2.17.dist-info → weco-0.2.19.dist-info}/licenses/LICENSE +0 -0
- {weco-0.2.17.dist-info → weco-0.2.19.dist-info}/top_level.txt +0 -0
weco/__init__.py
CHANGED
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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
261
|
+
"-c",
|
|
262
|
+
"--eval-command",
|
|
183
263
|
type=str,
|
|
184
|
-
choices=["true", "false"],
|
|
185
264
|
required=True,
|
|
186
|
-
help="
|
|
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
|
-
|
|
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.
|
|
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,
|
|
293
|
-
auth_headers=auth_headers,
|
|
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
|
-
#
|
|
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
|
-
|
|
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.
|
|
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
|
|
26
|
+
# Weco: The Platform for Self-Improving Code
|
|
26
27
|
|
|
27
28
|
[](https://www.python.org)
|
|
28
29
|
[](https://docs.weco.ai/)
|
|
29
30
|
[](https://badge.fury.io/py/weco)
|
|
30
31
|
[](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
|
|
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
|

|
|
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
|
-
##
|
|
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
|
|
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
|
-
|
|
110
|
+
### Arguments for `weco run`
|
|
137
111
|
|
|
138
|
-
|
|
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
|
-
|
|
121
|
+
<br>
|
|
152
122
|
|
|
153
|
-
|
|
123
|
+
**Optional:**
|
|
154
124
|
|
|
155
|
-
|
|
156
|
-
|
|
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
|
+

|
|
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,,
|
weco-0.2.17.dist-info/RECORD
DELETED
|
@@ -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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|