weco 0.2.20__py3-none-any.whl → 0.2.23__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 -8
- weco/api.py +193 -20
- weco/auth.py +164 -3
- weco/chatbot.py +797 -0
- weco/cli.py +129 -643
- weco/optimizer.py +479 -0
- weco/panels.py +46 -0
- weco/utils.py +31 -3
- {weco-0.2.20.dist-info → weco-0.2.23.dist-info}/METADATA +111 -38
- weco-0.2.23.dist-info/RECORD +14 -0
- weco-0.2.20.dist-info/RECORD +0 -12
- {weco-0.2.20.dist-info → weco-0.2.23.dist-info}/WHEEL +0 -0
- {weco-0.2.20.dist-info → weco-0.2.23.dist-info}/entry_points.txt +0 -0
- {weco-0.2.20.dist-info → weco-0.2.23.dist-info}/licenses/LICENSE +0 -0
- {weco-0.2.20.dist-info → weco-0.2.23.dist-info}/top_level.txt +0 -0
weco/cli.py
CHANGED
|
@@ -1,247 +1,19 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import sys
|
|
3
3
|
import pathlib
|
|
4
|
-
import math
|
|
5
|
-
import time
|
|
6
|
-
import requests
|
|
7
|
-
import webbrowser
|
|
8
|
-
import threading
|
|
9
|
-
import signal
|
|
10
|
-
import traceback
|
|
11
4
|
from rich.console import Console
|
|
12
|
-
from rich.live import Live
|
|
13
|
-
from rich.panel import Panel
|
|
14
5
|
from rich.traceback import install
|
|
15
|
-
from rich.prompt import Prompt
|
|
16
|
-
from .api import (
|
|
17
|
-
start_optimization_run,
|
|
18
|
-
evaluate_feedback_then_suggest_next_solution,
|
|
19
|
-
get_optimization_run_status,
|
|
20
|
-
handle_api_error,
|
|
21
|
-
send_heartbeat,
|
|
22
|
-
report_termination,
|
|
23
|
-
)
|
|
24
6
|
|
|
25
|
-
from . import
|
|
26
|
-
from .
|
|
27
|
-
from .panels import (
|
|
28
|
-
SummaryPanel,
|
|
29
|
-
PlanPanel,
|
|
30
|
-
Node,
|
|
31
|
-
MetricTreePanel,
|
|
32
|
-
EvaluationOutputPanel,
|
|
33
|
-
SolutionPanels,
|
|
34
|
-
create_optimization_layout,
|
|
35
|
-
create_end_optimization_layout,
|
|
36
|
-
)
|
|
37
|
-
from .utils import (
|
|
38
|
-
read_api_keys_from_env,
|
|
39
|
-
read_additional_instructions,
|
|
40
|
-
read_from_path,
|
|
41
|
-
write_to_path,
|
|
42
|
-
run_evaluation,
|
|
43
|
-
smooth_update,
|
|
44
|
-
format_number,
|
|
45
|
-
check_for_cli_updates,
|
|
46
|
-
)
|
|
7
|
+
from .auth import clear_api_key
|
|
8
|
+
from .utils import check_for_cli_updates
|
|
47
9
|
|
|
48
10
|
install(show_locals=True)
|
|
49
11
|
console = Console()
|
|
50
12
|
|
|
51
|
-
# --- Global variable for heartbeat thread ---
|
|
52
|
-
heartbeat_thread = None
|
|
53
|
-
stop_heartbeat_event = threading.Event()
|
|
54
|
-
current_run_id_for_heartbeat = None
|
|
55
|
-
current_auth_headers_for_heartbeat = {}
|
|
56
13
|
|
|
57
|
-
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
def __init__(self, run_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.run_id = run_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.run_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(f"[ERROR HeartbeatSender] Unhandled exception in run loop for run {self.run_id}: {e}", file=sys.stderr)
|
|
83
|
-
traceback.print_exc(file=sys.stderr)
|
|
84
|
-
# The loop will break due to the exception, and thread will terminate via finally.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
# --- Signal Handling ---
|
|
88
|
-
def signal_handler(signum, frame):
|
|
89
|
-
signal_name = signal.Signals(signum).name
|
|
90
|
-
console.print(f"\n[bold yellow]Termination signal ({signal_name}) received. Shutting down...[/]")
|
|
91
|
-
|
|
92
|
-
# Stop heartbeat thread
|
|
93
|
-
stop_heartbeat_event.set()
|
|
94
|
-
if heartbeat_thread and heartbeat_thread.is_alive():
|
|
95
|
-
heartbeat_thread.join(timeout=2) # Give it a moment to stop
|
|
96
|
-
|
|
97
|
-
# Report termination (best effort)
|
|
98
|
-
if current_run_id_for_heartbeat:
|
|
99
|
-
report_termination(
|
|
100
|
-
run_id=current_run_id_for_heartbeat,
|
|
101
|
-
status_update="terminated",
|
|
102
|
-
reason=f"user_terminated_{signal_name.lower()}",
|
|
103
|
-
details=f"Process terminated by signal {signal_name} ({signum}).",
|
|
104
|
-
auth_headers=current_auth_headers_for_heartbeat,
|
|
105
|
-
timeout=3,
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
# Exit gracefully
|
|
109
|
-
sys.exit(0)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def perform_login(console: Console):
|
|
113
|
-
"""Handles the device login flow."""
|
|
114
|
-
try:
|
|
115
|
-
# 1. Initiate device login
|
|
116
|
-
console.print("Initiating login...")
|
|
117
|
-
init_response = requests.post(f"{__base_url__}/auth/device/initiate")
|
|
118
|
-
init_response.raise_for_status()
|
|
119
|
-
init_data = init_response.json()
|
|
120
|
-
|
|
121
|
-
device_code = init_data["device_code"]
|
|
122
|
-
verification_uri = init_data["verification_uri"]
|
|
123
|
-
expires_in = init_data["expires_in"]
|
|
124
|
-
interval = init_data["interval"]
|
|
125
|
-
|
|
126
|
-
# 2. Display instructions
|
|
127
|
-
console.print("\n[bold yellow]Action Required:[/]")
|
|
128
|
-
console.print("Please open the following URL in your browser to authenticate:")
|
|
129
|
-
console.print(f"[link={verification_uri}]{verification_uri}[/link]")
|
|
130
|
-
console.print(f"This request will expire in {expires_in // 60} minutes.")
|
|
131
|
-
console.print("Attempting to open the authentication page in your default browser...") # Notify user
|
|
132
|
-
|
|
133
|
-
# Automatically open the browser
|
|
134
|
-
try:
|
|
135
|
-
if not webbrowser.open(verification_uri):
|
|
136
|
-
console.print("[yellow]Could not automatically open the browser. Please open the link manually.[/]")
|
|
137
|
-
except Exception as browser_err:
|
|
138
|
-
console.print(
|
|
139
|
-
f"[yellow]Could not automatically open the browser ({browser_err}). Please open the link manually.[/]"
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
console.print("Waiting for authentication...", end="")
|
|
143
|
-
|
|
144
|
-
# 3. Poll for token
|
|
145
|
-
start_time = time.time()
|
|
146
|
-
# Use a simple text update instead of Spinner within Live for potentially better compatibility
|
|
147
|
-
polling_status = "Waiting..."
|
|
148
|
-
with Live(polling_status, refresh_per_second=1, transient=True, console=console) as live_status:
|
|
149
|
-
while True:
|
|
150
|
-
# Check for timeout
|
|
151
|
-
if time.time() - start_time > expires_in:
|
|
152
|
-
console.print("\n[bold red]Error:[/] Login request timed out.")
|
|
153
|
-
return False
|
|
154
|
-
|
|
155
|
-
time.sleep(interval)
|
|
156
|
-
live_status.update("Waiting... (checking status)")
|
|
157
|
-
|
|
158
|
-
try:
|
|
159
|
-
token_response = requests.post(
|
|
160
|
-
f"{__base_url__}/auth/device/token",
|
|
161
|
-
json={"grant_type": "urn:ietf:params:oauth:grant-type:device_code", "device_code": device_code},
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
# Check for 202 Accepted - Authorization Pending
|
|
165
|
-
if token_response.status_code == 202:
|
|
166
|
-
token_data = token_response.json()
|
|
167
|
-
if token_data.get("error") == "authorization_pending":
|
|
168
|
-
live_status.update("Waiting... (authorization pending)")
|
|
169
|
-
continue # Continue polling
|
|
170
|
-
else:
|
|
171
|
-
# Unexpected 202 response format
|
|
172
|
-
console.print(f"\n[bold red]Error:[/] Received unexpected 202 response: {token_data}")
|
|
173
|
-
return False
|
|
174
|
-
# Check for standard OAuth2 errors (often 400 Bad Request)
|
|
175
|
-
elif token_response.status_code == 400:
|
|
176
|
-
token_data = token_response.json()
|
|
177
|
-
error_code = token_data.get("error", "unknown_error")
|
|
178
|
-
if error_code == "slow_down":
|
|
179
|
-
interval += 5 # Increase polling interval if instructed
|
|
180
|
-
live_status.update(f"Waiting... (slowing down polling to {interval}s)")
|
|
181
|
-
continue
|
|
182
|
-
elif error_code == "expired_token":
|
|
183
|
-
console.print("\n[bold red]Error:[/] Login request expired.")
|
|
184
|
-
return False
|
|
185
|
-
elif error_code == "access_denied":
|
|
186
|
-
console.print("\n[bold red]Error:[/] Authorization denied by user.")
|
|
187
|
-
return False
|
|
188
|
-
else: # invalid_grant, etc.
|
|
189
|
-
error_desc = token_data.get("error_description", "Unknown error during polling.")
|
|
190
|
-
console.print(f"\n[bold red]Error:[/] {error_desc} ({error_code})")
|
|
191
|
-
return False
|
|
192
|
-
|
|
193
|
-
# Check for other non-200/non-202/non-400 HTTP errors
|
|
194
|
-
token_response.raise_for_status()
|
|
195
|
-
# If successful (200 OK and no 'error' field)
|
|
196
|
-
token_data = token_response.json()
|
|
197
|
-
if "access_token" in token_data:
|
|
198
|
-
api_key = token_data["access_token"]
|
|
199
|
-
save_api_key(api_key)
|
|
200
|
-
console.print("\n[bold green]Login successful![/]")
|
|
201
|
-
return True
|
|
202
|
-
else:
|
|
203
|
-
# Unexpected successful response format
|
|
204
|
-
console.print("\n[bold red]Error:[/] Received unexpected response from server during polling.")
|
|
205
|
-
print(token_data)
|
|
206
|
-
return False
|
|
207
|
-
except requests.exceptions.RequestException as e:
|
|
208
|
-
# Handle network errors during polling gracefully
|
|
209
|
-
live_status.update("Waiting... (network error, retrying)")
|
|
210
|
-
console.print(f"\n[bold yellow]Warning:[/] Network error during polling: {e}. Retrying...")
|
|
211
|
-
time.sleep(interval * 2) # Simple backoff
|
|
212
|
-
except requests.exceptions.HTTPError as e:
|
|
213
|
-
handle_api_error(e, console)
|
|
214
|
-
except requests.exceptions.RequestException as e:
|
|
215
|
-
# Catch other request errors
|
|
216
|
-
console.print(f"\n[bold red]Network Error:[/] {e}")
|
|
217
|
-
return False
|
|
218
|
-
except Exception as e:
|
|
219
|
-
console.print(f"\n[bold red]An unexpected error occurred during login:[/] {e}")
|
|
220
|
-
return False
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
def main() -> None:
|
|
224
|
-
"""Main function for the Weco CLI."""
|
|
225
|
-
# Setup signal handlers
|
|
226
|
-
signal.signal(signal.SIGINT, signal_handler)
|
|
227
|
-
signal.signal(signal.SIGTERM, signal_handler)
|
|
228
|
-
|
|
229
|
-
# --- Perform Update Check ---
|
|
230
|
-
from . import __pkg_version__
|
|
231
|
-
|
|
232
|
-
check_for_cli_updates(__pkg_version__)
|
|
233
|
-
|
|
234
|
-
# --- Argument Parsing ---
|
|
235
|
-
parser = argparse.ArgumentParser(
|
|
236
|
-
description="[bold cyan]Weco CLI[/]", formatter_class=argparse.RawDescriptionHelpFormatter
|
|
237
|
-
)
|
|
238
|
-
subparsers = parser.add_subparsers(dest="command", help="Available commands", required=True)
|
|
239
|
-
|
|
240
|
-
# --- Run Command ---
|
|
241
|
-
run_parser = subparsers.add_parser(
|
|
242
|
-
"run", help="Run code optimization", formatter_class=argparse.RawDescriptionHelpFormatter, allow_abbrev=False
|
|
243
|
-
)
|
|
244
|
-
# Add arguments specific to the 'run' command to the run_parser
|
|
14
|
+
# Function to define and return the run_parser (or configure it on a passed subparser object)
|
|
15
|
+
# This helps keep main() cleaner and centralizes run command arg definitions.
|
|
16
|
+
def configure_run_parser(run_parser: argparse.ArgumentParser) -> None:
|
|
245
17
|
run_parser.add_argument(
|
|
246
18
|
"-s",
|
|
247
19
|
"--source",
|
|
@@ -277,7 +49,7 @@ def main() -> None:
|
|
|
277
49
|
"--model",
|
|
278
50
|
type=str,
|
|
279
51
|
default=None,
|
|
280
|
-
help="Model to use for optimization. Defaults to `o4-mini` when `OPENAI_API_KEY` is set, `claude-
|
|
52
|
+
help="Model to use for optimization. Defaults to `o4-mini` when `OPENAI_API_KEY` is set, `claude-sonnet-4-0` when `ANTHROPIC_API_KEY` is set, and `gemini-2.5-pro` when `GEMINI_API_KEY` is set. When multiple keys are set, the priority is `OPENAI_API_KEY` > `ANTHROPIC_API_KEY` > `GEMINI_API_KEY`.",
|
|
281
53
|
)
|
|
282
54
|
run_parser.add_argument(
|
|
283
55
|
"-l", "--log-dir", type=str, default=".runs", help="Directory to store logs and results. Defaults to `.runs`."
|
|
@@ -290,425 +62,139 @@ def main() -> None:
|
|
|
290
62
|
help="Description of additional instruction or path to a file containing additional instructions. Defaults to None.",
|
|
291
63
|
)
|
|
292
64
|
|
|
293
|
-
_ = subparsers.add_parser("logout", help="Log out from Weco and clear saved API key.")
|
|
294
|
-
args = parser.parse_args()
|
|
295
65
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
).lower()
|
|
314
|
-
if login_choice == "l":
|
|
315
|
-
console.print("[cyan]Starting login process...[/]")
|
|
316
|
-
if not perform_login(console):
|
|
317
|
-
console.print("[bold red]Login process failed or was cancelled.[/]")
|
|
318
|
-
sys.exit(1)
|
|
319
|
-
weco_api_key = load_weco_api_key()
|
|
320
|
-
if not weco_api_key:
|
|
321
|
-
console.print("[bold red]Error: Login completed but failed to retrieve API key.[/]")
|
|
322
|
-
sys.exit(1)
|
|
323
|
-
elif login_choice == "s":
|
|
324
|
-
console.print("[yellow]Proceeding anonymously. LLM API keys must be provided via environment variables.[/]")
|
|
325
|
-
if not llm_api_keys:
|
|
326
|
-
console.print(
|
|
327
|
-
"[bold red]Error:[/] No LLM API keys found in environment (e.g., OPENAI_API_KEY). Cannot proceed anonymously."
|
|
328
|
-
)
|
|
329
|
-
sys.exit(1)
|
|
330
|
-
|
|
331
|
-
auth_headers = {}
|
|
332
|
-
if weco_api_key:
|
|
333
|
-
auth_headers["Authorization"] = f"Bearer {weco_api_key}"
|
|
334
|
-
current_auth_headers_for_heartbeat = auth_headers # Store for signal handler
|
|
335
|
-
|
|
336
|
-
# --- Main Run Logic ---
|
|
337
|
-
try:
|
|
338
|
-
# --- Read Command Line Arguments ---
|
|
339
|
-
evaluation_command = args.eval_command
|
|
340
|
-
metric_name = args.metric
|
|
341
|
-
maximize = args.goal in ["maximize", "max"]
|
|
342
|
-
steps = args.steps
|
|
343
|
-
# Determine the model to use
|
|
344
|
-
if args.model is None:
|
|
345
|
-
if "OPENAI_API_KEY" in llm_api_keys:
|
|
346
|
-
args.model = "o4-mini"
|
|
347
|
-
elif "ANTHROPIC_API_KEY" in llm_api_keys:
|
|
348
|
-
args.model = "claude-3-7-sonnet-20250219"
|
|
349
|
-
elif "GEMINI_API_KEY" in llm_api_keys:
|
|
350
|
-
args.model = "gemini-2.5-pro-exp-03-25"
|
|
351
|
-
else:
|
|
352
|
-
raise ValueError(
|
|
353
|
-
"No LLM API keys found in environment. Please set one of the following: OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY."
|
|
354
|
-
)
|
|
355
|
-
code_generator_config = {"model": args.model}
|
|
356
|
-
evaluator_config = {"model": args.model, "include_analysis": True}
|
|
357
|
-
search_policy_config = {
|
|
358
|
-
"num_drafts": max(1, math.ceil(0.15 * steps)),
|
|
359
|
-
"debug_prob": 0.5,
|
|
360
|
-
"max_debug_depth": max(1, math.ceil(0.1 * steps)),
|
|
361
|
-
}
|
|
362
|
-
timeout = 800
|
|
363
|
-
additional_instructions = read_additional_instructions(additional_instructions=args.additional_instructions)
|
|
364
|
-
source_fp = pathlib.Path(args.source)
|
|
365
|
-
source_code = read_from_path(fp=source_fp, is_json=False)
|
|
366
|
-
|
|
367
|
-
# --- Panel Initialization ---
|
|
368
|
-
summary_panel = SummaryPanel(
|
|
369
|
-
maximize=maximize, metric_name=metric_name, total_steps=steps, model=args.model, runs_dir=args.log_dir
|
|
370
|
-
)
|
|
371
|
-
plan_panel = PlanPanel()
|
|
372
|
-
solution_panels = SolutionPanels(metric_name=metric_name, source_fp=source_fp)
|
|
373
|
-
eval_output_panel = EvaluationOutputPanel()
|
|
374
|
-
tree_panel = MetricTreePanel(maximize=maximize)
|
|
375
|
-
layout = create_optimization_layout()
|
|
376
|
-
end_optimization_layout = create_end_optimization_layout()
|
|
377
|
-
|
|
378
|
-
# --- Start Optimization Run ---
|
|
379
|
-
run_response = start_optimization_run(
|
|
380
|
-
console=console,
|
|
381
|
-
source_code=source_code,
|
|
382
|
-
evaluation_command=evaluation_command,
|
|
383
|
-
metric_name=metric_name,
|
|
384
|
-
maximize=maximize,
|
|
385
|
-
steps=steps,
|
|
386
|
-
code_generator_config=code_generator_config,
|
|
387
|
-
evaluator_config=evaluator_config,
|
|
388
|
-
search_policy_config=search_policy_config,
|
|
389
|
-
additional_instructions=additional_instructions,
|
|
390
|
-
api_keys=llm_api_keys,
|
|
391
|
-
auth_headers=auth_headers,
|
|
392
|
-
timeout=timeout,
|
|
393
|
-
)
|
|
394
|
-
run_id = run_response["run_id"]
|
|
395
|
-
current_run_id_for_heartbeat = run_id
|
|
396
|
-
|
|
397
|
-
# --- Start Heartbeat Thread ---
|
|
398
|
-
stop_heartbeat_event.clear()
|
|
399
|
-
heartbeat_thread = HeartbeatSender(run_id, auth_headers, stop_heartbeat_event)
|
|
400
|
-
heartbeat_thread.start()
|
|
401
|
-
|
|
402
|
-
# --- Live Update Loop ---
|
|
403
|
-
refresh_rate = 4
|
|
404
|
-
with Live(layout, refresh_per_second=refresh_rate, screen=True) as live:
|
|
405
|
-
# Define the runs directory (.runs/<run-id>) to store logs and results
|
|
406
|
-
runs_dir = pathlib.Path(args.log_dir) / run_id
|
|
407
|
-
runs_dir.mkdir(parents=True, exist_ok=True)
|
|
408
|
-
# Write the initial code string to the logs
|
|
409
|
-
write_to_path(fp=runs_dir / f"step_0{source_fp.suffix}", content=run_response["code"])
|
|
410
|
-
# Write the initial code string to the source file path
|
|
411
|
-
write_to_path(fp=source_fp, content=run_response["code"])
|
|
412
|
-
|
|
413
|
-
# Update the panels with the initial solution
|
|
414
|
-
summary_panel.set_run_id(run_id=run_id) # Add run id now that we have it
|
|
415
|
-
# Set the step of the progress bar
|
|
416
|
-
summary_panel.set_step(step=0)
|
|
417
|
-
# Update the token counts
|
|
418
|
-
summary_panel.update_token_counts(usage=run_response["usage"])
|
|
419
|
-
plan_panel.update(plan=run_response["plan"])
|
|
420
|
-
# Build the metric tree
|
|
421
|
-
tree_panel.build_metric_tree(
|
|
422
|
-
nodes=[
|
|
423
|
-
{
|
|
424
|
-
"solution_id": run_response["solution_id"],
|
|
425
|
-
"parent_id": None,
|
|
426
|
-
"code": run_response["code"],
|
|
427
|
-
"step": 0,
|
|
428
|
-
"metric_value": None,
|
|
429
|
-
"is_buggy": False,
|
|
430
|
-
}
|
|
431
|
-
]
|
|
432
|
-
)
|
|
433
|
-
# Set the current solution as unevaluated since we haven't run the evaluation function and fed it back to the model yet
|
|
434
|
-
tree_panel.set_unevaluated_node(node_id=run_response["solution_id"])
|
|
435
|
-
# Update the solution panels with the initial solution and get the panel displays
|
|
436
|
-
solution_panels.update(
|
|
437
|
-
current_node=Node(
|
|
438
|
-
id=run_response["solution_id"], parent_id=None, code=run_response["code"], metric=None, is_buggy=False
|
|
439
|
-
),
|
|
440
|
-
best_node=None,
|
|
441
|
-
)
|
|
442
|
-
current_solution_panel, best_solution_panel = solution_panels.get_display(current_step=0)
|
|
443
|
-
# Update the live layout with the initial solution panels
|
|
444
|
-
smooth_update(
|
|
445
|
-
live=live,
|
|
446
|
-
layout=layout,
|
|
447
|
-
sections_to_update=[
|
|
448
|
-
("summary", summary_panel.get_display()),
|
|
449
|
-
("plan", plan_panel.get_display()),
|
|
450
|
-
("tree", tree_panel.get_display(is_done=False)),
|
|
451
|
-
("current_solution", current_solution_panel),
|
|
452
|
-
("best_solution", best_solution_panel),
|
|
453
|
-
("eval_output", eval_output_panel.get_display()),
|
|
454
|
-
],
|
|
455
|
-
transition_delay=0.1,
|
|
456
|
-
)
|
|
66
|
+
def execute_run_command(args: argparse.Namespace) -> None:
|
|
67
|
+
"""Execute the 'weco run' command with all its logic."""
|
|
68
|
+
from .optimizer import execute_optimization # Moved import inside
|
|
69
|
+
|
|
70
|
+
success = execute_optimization(
|
|
71
|
+
source=args.source,
|
|
72
|
+
eval_command=args.eval_command,
|
|
73
|
+
metric=args.metric,
|
|
74
|
+
goal=args.goal,
|
|
75
|
+
steps=args.steps,
|
|
76
|
+
model=args.model,
|
|
77
|
+
log_dir=args.log_dir,
|
|
78
|
+
additional_instructions=args.additional_instructions,
|
|
79
|
+
console=console,
|
|
80
|
+
)
|
|
81
|
+
exit_code = 0 if success else 1
|
|
82
|
+
sys.exit(exit_code)
|
|
457
83
|
|
|
458
|
-
# Run evaluation on the initial solution
|
|
459
|
-
term_out = run_evaluation(eval_command=args.eval_command)
|
|
460
|
-
# Update the evaluation output panel
|
|
461
|
-
eval_output_panel.update(output=term_out)
|
|
462
|
-
smooth_update(
|
|
463
|
-
live=live,
|
|
464
|
-
layout=layout,
|
|
465
|
-
sections_to_update=[("eval_output", eval_output_panel.get_display())],
|
|
466
|
-
transition_delay=0.1,
|
|
467
|
-
)
|
|
468
84
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
current_additional_instructions = read_additional_instructions(
|
|
473
|
-
additional_instructions=args.additional_instructions
|
|
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
|
-
)
|
|
85
|
+
def main() -> None:
|
|
86
|
+
"""Main function for the Weco CLI."""
|
|
87
|
+
check_for_cli_updates()
|
|
493
88
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
additional_instructions=current_additional_instructions,
|
|
499
|
-
api_keys=llm_api_keys,
|
|
500
|
-
auth_headers=auth_headers,
|
|
501
|
-
timeout=timeout,
|
|
502
|
-
)
|
|
503
|
-
# Save next solution (.runs/<run-id>/step_<step>.<extension>)
|
|
504
|
-
write_to_path(
|
|
505
|
-
fp=runs_dir / f"step_{step}{source_fp.suffix}", content=eval_and_next_solution_response["code"]
|
|
506
|
-
)
|
|
507
|
-
# Write the next solution to the source file
|
|
508
|
-
write_to_path(fp=source_fp, content=eval_and_next_solution_response["code"])
|
|
509
|
-
status_response = get_optimization_run_status(
|
|
510
|
-
run_id=run_id, include_history=True, timeout=timeout, auth_headers=auth_headers
|
|
511
|
-
)
|
|
512
|
-
# Update the step of the progress bar, token counts, plan and metric tree
|
|
513
|
-
summary_panel.set_step(step=step)
|
|
514
|
-
summary_panel.update_token_counts(usage=eval_and_next_solution_response["usage"])
|
|
515
|
-
plan_panel.update(plan=eval_and_next_solution_response["plan"])
|
|
89
|
+
parser = argparse.ArgumentParser(
|
|
90
|
+
description="[bold cyan]Weco CLI[/]\nEnhance your code with AI-driven optimization.",
|
|
91
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
92
|
+
)
|
|
516
93
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
94
|
+
# Add global model argument
|
|
95
|
+
parser.add_argument(
|
|
96
|
+
"-M",
|
|
97
|
+
"--model",
|
|
98
|
+
type=str,
|
|
99
|
+
default=None,
|
|
100
|
+
help="Model to use for optimization. Defaults to `o4-mini` when `OPENAI_API_KEY` is set, `claude-sonnet-4-0` when `ANTHROPIC_API_KEY` is set, and `gemini-2.5-pro` when `GEMINI_API_KEY` is set. When multiple keys are set, the priority is `OPENAI_API_KEY` > `ANTHROPIC_API_KEY` > `GEMINI_API_KEY`.",
|
|
101
|
+
)
|
|
520
102
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
best_solution_node = Node(
|
|
525
|
-
id=status_response["best_result"]["solution_id"],
|
|
526
|
-
parent_id=status_response["best_result"]["parent_id"],
|
|
527
|
-
code=status_response["best_result"]["code"],
|
|
528
|
-
metric=status_response["best_result"]["metric_value"],
|
|
529
|
-
is_buggy=status_response["best_result"]["is_buggy"],
|
|
530
|
-
)
|
|
531
|
-
else:
|
|
532
|
-
best_solution_node = None
|
|
103
|
+
subparsers = parser.add_subparsers(
|
|
104
|
+
dest="command", help="Available commands"
|
|
105
|
+
) # Removed required=True for now to handle chatbot case easily
|
|
533
106
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
-
)
|
|
545
|
-
if current_solution_node is None:
|
|
546
|
-
raise ValueError("Current solution node not found in nodes list from status response")
|
|
107
|
+
# --- Run Command Parser Setup ---
|
|
108
|
+
run_parser = subparsers.add_parser(
|
|
109
|
+
"run", help="Run code optimization", formatter_class=argparse.RawDescriptionHelpFormatter, allow_abbrev=False
|
|
110
|
+
)
|
|
111
|
+
configure_run_parser(run_parser) # Use the helper to add arguments
|
|
547
112
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
current_solution_panel, best_solution_panel = solution_panels.get_display(current_step=step)
|
|
551
|
-
# Clear evaluation output since we are running a evaluation on a new solution
|
|
552
|
-
eval_output_panel.clear()
|
|
553
|
-
smooth_update(
|
|
554
|
-
live=live,
|
|
555
|
-
layout=layout,
|
|
556
|
-
sections_to_update=[
|
|
557
|
-
("summary", summary_panel.get_display()),
|
|
558
|
-
("plan", plan_panel.get_display()),
|
|
559
|
-
("tree", tree_panel.get_display(is_done=False)),
|
|
560
|
-
("current_solution", current_solution_panel),
|
|
561
|
-
("best_solution", best_solution_panel),
|
|
562
|
-
("eval_output", eval_output_panel.get_display()),
|
|
563
|
-
],
|
|
564
|
-
transition_delay=0.08, # Slightly longer delay for more noticeable transitions
|
|
565
|
-
)
|
|
566
|
-
term_out = run_evaluation(eval_command=args.eval_command)
|
|
567
|
-
eval_output_panel.update(output=term_out)
|
|
568
|
-
smooth_update(
|
|
569
|
-
live=live,
|
|
570
|
-
layout=layout,
|
|
571
|
-
sections_to_update=[("eval_output", eval_output_panel.get_display())],
|
|
572
|
-
transition_delay=0.1,
|
|
573
|
-
)
|
|
113
|
+
# --- Logout Command Parser Setup ---
|
|
114
|
+
_ = subparsers.add_parser("logout", help="Log out from Weco and clear saved API key.")
|
|
574
115
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
116
|
+
# Check if we should run the chatbot
|
|
117
|
+
# This logic needs to be robust. If 'run' or 'logout' is present, or -h/--help, don't run chatbot.
|
|
118
|
+
# Otherwise, if it's just 'weco' or 'weco <path>' (with optional --model), run chatbot.
|
|
119
|
+
|
|
120
|
+
def should_run_chatbot(args_list):
|
|
121
|
+
"""Determine if we should run chatbot by filtering out model arguments."""
|
|
122
|
+
filtered = []
|
|
123
|
+
i = 0
|
|
124
|
+
while i < len(args_list):
|
|
125
|
+
if args_list[i] in ["-M", "--model"]:
|
|
126
|
+
# Skip the model argument and its value (if it exists)
|
|
127
|
+
i += 1 # Skip the model flag
|
|
128
|
+
if i < len(args_list): # Skip the model value if it exists
|
|
129
|
+
i += 1
|
|
130
|
+
elif args_list[i].startswith("--model="):
|
|
131
|
+
i += 1 # Skip --model=value format
|
|
132
|
+
else:
|
|
133
|
+
filtered.append(args_list[i])
|
|
134
|
+
i += 1
|
|
135
|
+
|
|
136
|
+
# Apply existing chatbot detection logic to filtered args
|
|
137
|
+
return len(filtered) == 0 or (len(filtered) == 1 and not filtered[0].startswith("-"))
|
|
138
|
+
|
|
139
|
+
# Check for known commands by looking at the first non-option argument
|
|
140
|
+
def get_first_non_option_arg():
|
|
141
|
+
for arg in sys.argv[1:]:
|
|
142
|
+
if not arg.startswith("-"):
|
|
143
|
+
return arg
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
first_non_option = get_first_non_option_arg()
|
|
147
|
+
is_known_command = first_non_option in ["run", "logout"]
|
|
148
|
+
is_help_command = len(sys.argv) > 1 and sys.argv[1] in ["-h", "--help"] # Check for global help
|
|
149
|
+
|
|
150
|
+
should_run_chatbot_result = should_run_chatbot(sys.argv[1:])
|
|
151
|
+
should_run_chatbot_flag = not is_known_command and not is_help_command and should_run_chatbot_result
|
|
152
|
+
|
|
153
|
+
if should_run_chatbot_flag:
|
|
154
|
+
from .chatbot import run_onboarding_chatbot # Moved import inside
|
|
155
|
+
|
|
156
|
+
# Create a simple parser just for extracting the model argument
|
|
157
|
+
model_parser = argparse.ArgumentParser(add_help=False)
|
|
158
|
+
model_parser.add_argument("-M", "--model", type=str, default=None)
|
|
159
|
+
|
|
160
|
+
# Parse args to extract model
|
|
161
|
+
args, unknown = model_parser.parse_known_args()
|
|
162
|
+
|
|
163
|
+
# Determine project path from remaining arguments
|
|
164
|
+
filtered_args = []
|
|
165
|
+
i = 1
|
|
166
|
+
while i < len(sys.argv):
|
|
167
|
+
if sys.argv[i] in ["-M", "--model"]:
|
|
168
|
+
# Skip the model argument and its value (if it exists)
|
|
169
|
+
i += 1 # Skip the model flag
|
|
170
|
+
if i < len(sys.argv): # Skip the model value if it exists
|
|
171
|
+
i += 1
|
|
172
|
+
elif sys.argv[i].startswith("--model="):
|
|
173
|
+
i += 1 # Skip --model=value format
|
|
174
|
+
else:
|
|
175
|
+
filtered_args.append(sys.argv[i])
|
|
176
|
+
i += 1
|
|
627
177
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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
|
|
178
|
+
project_path = pathlib.Path(filtered_args[0]) if filtered_args else pathlib.Path.cwd()
|
|
179
|
+
if not project_path.is_dir():
|
|
180
|
+
console.print(f"[bold red]Error:[/] Path '{project_path}' is not a valid directory.")
|
|
181
|
+
sys.exit(1)
|
|
637
182
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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)
|
|
183
|
+
# Pass the run_parser and model to the chatbot
|
|
184
|
+
run_onboarding_chatbot(project_path, console, run_parser, model=args.model)
|
|
185
|
+
sys.exit(0)
|
|
657
186
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
error_message = e.response.json()["detail"]
|
|
662
|
-
except Exception:
|
|
663
|
-
error_message = str(e)
|
|
664
|
-
console.print(Panel(f"[bold red]Error: {error_message}", title="[bold red]Optimization Error", border_style="red"))
|
|
665
|
-
# Ensure optimization_completed_normally is False
|
|
666
|
-
optimization_completed_normally = False
|
|
667
|
-
error_details = traceback.format_exc()
|
|
668
|
-
exit_code = 1
|
|
669
|
-
finally:
|
|
670
|
-
# This block runs whether the try block completed normally or raised an exception
|
|
671
|
-
# Stop heartbeat thread
|
|
672
|
-
stop_heartbeat_event.set()
|
|
673
|
-
if heartbeat_thread and heartbeat_thread.is_alive():
|
|
674
|
-
heartbeat_thread.join(timeout=2)
|
|
187
|
+
# If not running chatbot, proceed with normal arg parsing
|
|
188
|
+
# If we reached here, a command (run, logout) or help is expected.
|
|
189
|
+
args = parser.parse_args()
|
|
675
190
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
final_reason_code = "user_requested_stop"
|
|
687
|
-
final_details = "Run stopped by user request via dashboard."
|
|
688
|
-
else:
|
|
689
|
-
final_status_update = "error"
|
|
690
|
-
final_reason_code = "error_cli_internal"
|
|
691
|
-
if "error_details" in locals():
|
|
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
|
|
698
|
-
# Avoid reporting if terminated by signal handler (already reported)
|
|
699
|
-
# Check a flag or rely on status not being 'unknown'
|
|
700
|
-
if final_status_update != "unknown":
|
|
701
|
-
report_termination(
|
|
702
|
-
run_id=run_id,
|
|
703
|
-
status_update=final_status_update,
|
|
704
|
-
reason=final_reason_code,
|
|
705
|
-
details=final_details,
|
|
706
|
-
auth_headers=current_auth_headers_for_heartbeat,
|
|
707
|
-
)
|
|
708
|
-
if optimization_completed_normally:
|
|
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))
|
|
191
|
+
if args.command == "logout":
|
|
192
|
+
clear_api_key()
|
|
193
|
+
sys.exit(0)
|
|
194
|
+
elif args.command == "run":
|
|
195
|
+
execute_run_command(args)
|
|
196
|
+
else:
|
|
197
|
+
# This case should be hit if 'weco' is run alone and chatbot logic didn't catch it,
|
|
198
|
+
# or if an invalid command is provided.
|
|
199
|
+
parser.print_help() # Default action if no command given and not chatbot.
|
|
200
|
+
sys.exit(1)
|