weco 0.2.19__py3-none-any.whl → 0.2.22__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 +245 -48
- weco/auth.py +164 -3
- weco/chatbot.py +797 -0
- weco/cli.py +129 -685
- weco/optimizer.py +479 -0
- weco/panels.py +59 -10
- weco/utils.py +31 -3
- {weco-0.2.19.dist-info → weco-0.2.22.dist-info}/METADATA +110 -32
- weco-0.2.22.dist-info/RECORD +14 -0
- {weco-0.2.19.dist-info → weco-0.2.22.dist-info}/WHEEL +1 -1
- weco-0.2.19.dist-info/RECORD +0 -12
- {weco-0.2.19.dist-info → weco-0.2.22.dist-info}/entry_points.txt +0 -0
- {weco-0.2.19.dist-info → weco-0.2.22.dist-info}/licenses/LICENSE +0 -0
- {weco-0.2.19.dist-info → weco-0.2.22.dist-info}/top_level.txt +0 -0
weco/cli.py
CHANGED
|
@@ -1,255 +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_session,
|
|
18
|
-
evaluate_feedback_then_suggest_next_solution,
|
|
19
|
-
get_optimization_session_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_session_id_for_heartbeat = None
|
|
55
|
-
current_auth_headers_for_heartbeat = {}
|
|
56
13
|
|
|
57
|
-
|
|
58
|
-
#
|
|
59
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
def perform_login(console: Console):
|
|
114
|
-
"""Handles the device login flow."""
|
|
115
|
-
try:
|
|
116
|
-
# 1. Initiate device login
|
|
117
|
-
console.print("Initiating login...")
|
|
118
|
-
init_response = requests.post(f"{__base_url__}/auth/device/initiate")
|
|
119
|
-
init_response.raise_for_status()
|
|
120
|
-
init_data = init_response.json()
|
|
121
|
-
|
|
122
|
-
device_code = init_data["device_code"]
|
|
123
|
-
verification_uri = init_data["verification_uri"]
|
|
124
|
-
expires_in = init_data["expires_in"]
|
|
125
|
-
interval = init_data["interval"]
|
|
126
|
-
|
|
127
|
-
# 2. Display instructions
|
|
128
|
-
console.print("\n[bold yellow]Action Required:[/]")
|
|
129
|
-
console.print("Please open the following URL in your browser to authenticate:")
|
|
130
|
-
console.print(f"[link={verification_uri}]{verification_uri}[/link]")
|
|
131
|
-
console.print(f"This request will expire in {expires_in // 60} minutes.")
|
|
132
|
-
console.print("Attempting to open the authentication page in your default browser...") # Notify user
|
|
133
|
-
|
|
134
|
-
# Automatically open the browser
|
|
135
|
-
try:
|
|
136
|
-
if not webbrowser.open(verification_uri):
|
|
137
|
-
console.print("[yellow]Could not automatically open the browser. Please open the link manually.[/]")
|
|
138
|
-
except Exception as browser_err:
|
|
139
|
-
console.print(
|
|
140
|
-
f"[yellow]Could not automatically open the browser ({browser_err}). Please open the link manually.[/]"
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
console.print("Waiting for authentication...", end="")
|
|
144
|
-
|
|
145
|
-
# 3. Poll for token
|
|
146
|
-
start_time = time.time()
|
|
147
|
-
# Use a simple text update instead of Spinner within Live for potentially better compatibility
|
|
148
|
-
polling_status = "Waiting..."
|
|
149
|
-
with Live(polling_status, refresh_per_second=1, transient=True, console=console) as live_status:
|
|
150
|
-
while True:
|
|
151
|
-
# Check for timeout
|
|
152
|
-
if time.time() - start_time > expires_in:
|
|
153
|
-
console.print("\n[bold red]Error:[/] Login request timed out.")
|
|
154
|
-
return False
|
|
155
|
-
|
|
156
|
-
time.sleep(interval)
|
|
157
|
-
live_status.update("Waiting... (checking status)")
|
|
158
|
-
|
|
159
|
-
try:
|
|
160
|
-
token_response = requests.post(
|
|
161
|
-
f"{__base_url__}/auth/device/token", # REMOVED /v1 prefix
|
|
162
|
-
json={"grant_type": "urn:ietf:params:oauth:grant-type:device_code", "device_code": device_code},
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
# Check for 202 Accepted - Authorization Pending
|
|
166
|
-
if token_response.status_code == 202:
|
|
167
|
-
token_data = token_response.json()
|
|
168
|
-
if token_data.get("error") == "authorization_pending":
|
|
169
|
-
live_status.update("Waiting... (authorization pending)")
|
|
170
|
-
continue # Continue polling
|
|
171
|
-
else:
|
|
172
|
-
# Unexpected 202 response format
|
|
173
|
-
console.print(f"\n[bold red]Error:[/] Received unexpected 202 response: {token_data}")
|
|
174
|
-
return False
|
|
175
|
-
|
|
176
|
-
# Check for standard OAuth2 errors (often 400 Bad Request)
|
|
177
|
-
elif token_response.status_code == 400:
|
|
178
|
-
token_data = token_response.json()
|
|
179
|
-
error_code = token_data.get("error", "unknown_error")
|
|
180
|
-
# NOTE: Removed "authorization_pending" check from here
|
|
181
|
-
if error_code == "slow_down":
|
|
182
|
-
interval += 5 # Increase polling interval if instructed
|
|
183
|
-
live_status.update(f"Waiting... (slowing down polling to {interval}s)")
|
|
184
|
-
continue
|
|
185
|
-
elif error_code == "expired_token":
|
|
186
|
-
console.print("\n[bold red]Error:[/] Login request expired.")
|
|
187
|
-
return False
|
|
188
|
-
elif error_code == "access_denied":
|
|
189
|
-
console.print("\n[bold red]Error:[/] Authorization denied by user.")
|
|
190
|
-
return False
|
|
191
|
-
else: # invalid_grant, etc.
|
|
192
|
-
error_desc = token_data.get("error_description", "Unknown error during polling.")
|
|
193
|
-
console.print(f"\n[bold red]Error:[/] {error_desc} ({error_code})")
|
|
194
|
-
return False
|
|
195
|
-
|
|
196
|
-
# Check for other non-200/non-202/non-400 HTTP errors
|
|
197
|
-
token_response.raise_for_status()
|
|
198
|
-
|
|
199
|
-
# If successful (200 OK and no 'error' field)
|
|
200
|
-
token_data = token_response.json()
|
|
201
|
-
if "access_token" in token_data:
|
|
202
|
-
api_key = token_data["access_token"]
|
|
203
|
-
save_api_key(api_key)
|
|
204
|
-
console.print("\n[bold green]Login successful![/]")
|
|
205
|
-
return True
|
|
206
|
-
else:
|
|
207
|
-
# Unexpected successful response format
|
|
208
|
-
console.print("\n[bold red]Error:[/] Received unexpected response from server during polling.")
|
|
209
|
-
print(token_data) # Log for debugging
|
|
210
|
-
return False
|
|
211
|
-
|
|
212
|
-
except requests.exceptions.RequestException as e:
|
|
213
|
-
# Handle network errors during polling gracefully
|
|
214
|
-
live_status.update("Waiting... (network error, retrying)")
|
|
215
|
-
console.print(f"\n[bold yellow]Warning:[/] Network error during polling: {e}. Retrying...")
|
|
216
|
-
# Optional: implement backoff strategy
|
|
217
|
-
time.sleep(interval * 2) # Simple backoff
|
|
218
|
-
|
|
219
|
-
except requests.exceptions.HTTPError as e: # Catch HTTPError specifically for handle_api_error
|
|
220
|
-
handle_api_error(e, console)
|
|
221
|
-
except requests.exceptions.RequestException as e: # Catch other request errors
|
|
222
|
-
console.print(f"\n[bold red]Network Error:[/] {e}")
|
|
223
|
-
return False
|
|
224
|
-
except Exception as e:
|
|
225
|
-
console.print(f"\n[bold red]An unexpected error occurred during login:[/] {e}")
|
|
226
|
-
return False
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
def main() -> None:
|
|
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
|
-
|
|
241
|
-
# --- Argument Parsing ---
|
|
242
|
-
parser = argparse.ArgumentParser(
|
|
243
|
-
description="[bold cyan]Weco CLI[/]", formatter_class=argparse.RawDescriptionHelpFormatter
|
|
244
|
-
)
|
|
245
|
-
# Add subparsers for commands like 'run' and 'logout'
|
|
246
|
-
subparsers = parser.add_subparsers(dest="command", help="Available commands", required=True) # Make command required
|
|
247
|
-
|
|
248
|
-
# --- Run Command ---
|
|
249
|
-
run_parser = subparsers.add_parser(
|
|
250
|
-
"run", help="Run code optimization", formatter_class=argparse.RawDescriptionHelpFormatter, allow_abbrev=False
|
|
251
|
-
)
|
|
252
|
-
# 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:
|
|
253
17
|
run_parser.add_argument(
|
|
254
18
|
"-s",
|
|
255
19
|
"--source",
|
|
@@ -285,7 +49,7 @@ def main() -> None:
|
|
|
285
49
|
"--model",
|
|
286
50
|
type=str,
|
|
287
51
|
default=None,
|
|
288
|
-
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`.",
|
|
289
53
|
)
|
|
290
54
|
run_parser.add_argument(
|
|
291
55
|
"-l", "--log-dir", type=str, default=".runs", help="Directory to store logs and results. Defaults to `.runs`."
|
|
@@ -298,459 +62,139 @@ def main() -> None:
|
|
|
298
62
|
help="Description of additional instruction or path to a file containing additional instructions. Defaults to None.",
|
|
299
63
|
)
|
|
300
64
|
|
|
301
|
-
# --- Logout Command ---
|
|
302
|
-
_ = subparsers.add_parser("logout", help="Log out from Weco and clear saved API key.")
|
|
303
|
-
|
|
304
|
-
args = parser.parse_args()
|
|
305
|
-
|
|
306
|
-
# --- Handle Logout Command ---
|
|
307
|
-
if args.command == "logout":
|
|
308
|
-
clear_api_key()
|
|
309
|
-
sys.exit(0)
|
|
310
|
-
|
|
311
|
-
# --- Handle Run Command ---
|
|
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
|
|
317
|
-
# --- Check Authentication ---
|
|
318
|
-
weco_api_key = load_weco_api_key()
|
|
319
|
-
llm_api_keys = read_api_keys_from_env() # Read keys from client environment
|
|
320
|
-
|
|
321
|
-
if not weco_api_key:
|
|
322
|
-
login_choice = Prompt.ask(
|
|
323
|
-
"Log in to Weco to save run history or use anonymously? ([bold]L[/]ogin / [bold]S[/]kip)",
|
|
324
|
-
choices=["l", "s"],
|
|
325
|
-
default="s",
|
|
326
|
-
).lower()
|
|
327
|
-
|
|
328
|
-
if login_choice == "l":
|
|
329
|
-
console.print("[cyan]Starting login process...[/]")
|
|
330
|
-
if not perform_login(console):
|
|
331
|
-
console.print("[bold red]Login process failed or was cancelled.[/]")
|
|
332
|
-
sys.exit(1)
|
|
333
|
-
weco_api_key = load_weco_api_key()
|
|
334
|
-
if not weco_api_key:
|
|
335
|
-
console.print("[bold red]Error: Login completed but failed to retrieve API key.[/]")
|
|
336
|
-
sys.exit(1)
|
|
337
|
-
|
|
338
|
-
elif login_choice == "s":
|
|
339
|
-
console.print("[yellow]Proceeding anonymously. LLM API keys must be provided via environment variables.[/]")
|
|
340
|
-
if not llm_api_keys:
|
|
341
|
-
console.print(
|
|
342
|
-
"[bold red]Error:[/] No LLM API keys found in environment (e.g., OPENAI_API_KEY). Cannot proceed anonymously."
|
|
343
|
-
)
|
|
344
|
-
sys.exit(1)
|
|
345
|
-
|
|
346
|
-
# --- Prepare API Call Arguments ---
|
|
347
|
-
auth_headers = {}
|
|
348
|
-
if weco_api_key:
|
|
349
|
-
auth_headers["Authorization"] = f"Bearer {weco_api_key}"
|
|
350
|
-
current_auth_headers_for_heartbeat = auth_headers # Store for signal handler
|
|
351
|
-
|
|
352
|
-
# --- Main Run Logic ---
|
|
353
|
-
try:
|
|
354
|
-
# --- Configuration Loading ---
|
|
355
|
-
evaluation_command = args.eval_command
|
|
356
|
-
metric_name = args.metric
|
|
357
|
-
maximize = args.goal in ["maximize", "max"]
|
|
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
|
-
)
|
|
371
|
-
code_generator_config = {"model": args.model}
|
|
372
|
-
evaluator_config = {"model": args.model, "include_analysis": True}
|
|
373
|
-
search_policy_config = {
|
|
374
|
-
"num_drafts": max(1, math.ceil(0.15 * steps)),
|
|
375
|
-
"debug_prob": 0.5,
|
|
376
|
-
"max_debug_depth": max(1, math.ceil(0.1 * steps)),
|
|
377
|
-
}
|
|
378
|
-
# API request timeout
|
|
379
|
-
timeout = 800
|
|
380
|
-
# Read additional instructions
|
|
381
|
-
additional_instructions = read_additional_instructions(additional_instructions=args.additional_instructions)
|
|
382
|
-
# Read source code path
|
|
383
|
-
source_fp = pathlib.Path(args.source)
|
|
384
|
-
# Read source code content
|
|
385
|
-
source_code = read_from_path(fp=source_fp, is_json=False)
|
|
386
65
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
maximize=maximize,
|
|
405
|
-
steps=steps,
|
|
406
|
-
code_generator_config=code_generator_config,
|
|
407
|
-
evaluator_config=evaluator_config,
|
|
408
|
-
search_policy_config=search_policy_config,
|
|
409
|
-
additional_instructions=additional_instructions,
|
|
410
|
-
api_keys=llm_api_keys,
|
|
411
|
-
auth_headers=auth_headers,
|
|
412
|
-
timeout=timeout,
|
|
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()
|
|
421
|
-
|
|
422
|
-
# --- Live Update Loop ---
|
|
423
|
-
refresh_rate = 4
|
|
424
|
-
with Live(layout, refresh_per_second=refresh_rate, screen=True) as live:
|
|
425
|
-
# Define the runs directory (.runs/<session-id>)
|
|
426
|
-
runs_dir = pathlib.Path(args.log_dir) / session_id
|
|
427
|
-
runs_dir.mkdir(parents=True, exist_ok=True)
|
|
428
|
-
|
|
429
|
-
# Write the initial code string to the logs
|
|
430
|
-
write_to_path(fp=runs_dir / f"step_0{source_fp.suffix}", content=session_response["code"])
|
|
431
|
-
|
|
432
|
-
# Write the initial code string to the source file path
|
|
433
|
-
write_to_path(fp=source_fp, content=session_response["code"])
|
|
434
|
-
|
|
435
|
-
# Update the panels with the initial solution
|
|
436
|
-
summary_panel.set_session_id(session_id=session_id) # Add session id now that we have it
|
|
437
|
-
# Set the step of the progress bar
|
|
438
|
-
summary_panel.set_step(step=0)
|
|
439
|
-
# Update the token counts
|
|
440
|
-
summary_panel.update_token_counts(usage=session_response["usage"])
|
|
441
|
-
# Update the plan
|
|
442
|
-
plan_panel.update(plan=session_response["plan"])
|
|
443
|
-
# Build the metric tree
|
|
444
|
-
tree_panel.build_metric_tree(
|
|
445
|
-
nodes=[
|
|
446
|
-
{
|
|
447
|
-
"solution_id": session_response["solution_id"],
|
|
448
|
-
"parent_id": None,
|
|
449
|
-
"code": session_response["code"],
|
|
450
|
-
"step": 0,
|
|
451
|
-
"metric_value": None,
|
|
452
|
-
"is_buggy": False,
|
|
453
|
-
}
|
|
454
|
-
]
|
|
455
|
-
)
|
|
456
|
-
# Set the current solution as unevaluated since we haven't run the evaluation function and fed it back to the model yet
|
|
457
|
-
tree_panel.set_unevaluated_node(node_id=session_response["solution_id"])
|
|
458
|
-
# Update the solution panels with the initial solution and get the panel displays
|
|
459
|
-
solution_panels.update(
|
|
460
|
-
current_node=Node(
|
|
461
|
-
id=session_response["solution_id"],
|
|
462
|
-
parent_id=None,
|
|
463
|
-
code=session_response["code"],
|
|
464
|
-
metric=None,
|
|
465
|
-
is_buggy=False,
|
|
466
|
-
),
|
|
467
|
-
best_node=None,
|
|
468
|
-
)
|
|
469
|
-
current_solution_panel, best_solution_panel = solution_panels.get_display(current_step=0)
|
|
470
|
-
# Update the live layout with the initial solution panels
|
|
471
|
-
smooth_update(
|
|
472
|
-
live=live,
|
|
473
|
-
layout=layout,
|
|
474
|
-
sections_to_update=[
|
|
475
|
-
("summary", summary_panel.get_display()),
|
|
476
|
-
("plan", plan_panel.get_display()),
|
|
477
|
-
("tree", tree_panel.get_display(is_done=False)),
|
|
478
|
-
("current_solution", current_solution_panel),
|
|
479
|
-
("best_solution", best_solution_panel),
|
|
480
|
-
("eval_output", eval_output_panel.get_display()),
|
|
481
|
-
],
|
|
482
|
-
transition_delay=0.1,
|
|
483
|
-
)
|
|
484
|
-
|
|
485
|
-
# # Send initial heartbeat immediately after starting
|
|
486
|
-
# send_heartbeat(session_id, auth_headers)
|
|
487
|
-
|
|
488
|
-
# Run evaluation on the initial solution
|
|
489
|
-
term_out = run_evaluation(eval_command=args.eval_command)
|
|
490
|
-
|
|
491
|
-
# Update the evaluation output panel
|
|
492
|
-
eval_output_panel.update(output=term_out)
|
|
493
|
-
smooth_update(
|
|
494
|
-
live=live,
|
|
495
|
-
layout=layout,
|
|
496
|
-
sections_to_update=[("eval_output", eval_output_panel.get_display())],
|
|
497
|
-
transition_delay=0.1,
|
|
498
|
-
)
|
|
499
|
-
|
|
500
|
-
# Starting from step 1 to steps (inclusive) because the baseline solution is step 0, so we want to optimize for steps worth of steps
|
|
501
|
-
for step in range(1, steps + 1):
|
|
502
|
-
# Re-read instructions from the original source (file path or string) BEFORE each suggest call
|
|
503
|
-
current_additional_instructions = read_additional_instructions(
|
|
504
|
-
additional_instructions=args.additional_instructions
|
|
505
|
-
)
|
|
506
|
-
|
|
507
|
-
# Send feedback and get next suggestion
|
|
508
|
-
eval_and_next_solution_response = evaluate_feedback_then_suggest_next_solution(
|
|
509
|
-
session_id=session_id,
|
|
510
|
-
execution_output=term_out,
|
|
511
|
-
additional_instructions=current_additional_instructions, # Pass current instructions
|
|
512
|
-
api_keys=llm_api_keys, # Pass client LLM keys
|
|
513
|
-
auth_headers=auth_headers, # Pass Weco key if logged in
|
|
514
|
-
timeout=timeout,
|
|
515
|
-
)
|
|
516
|
-
|
|
517
|
-
# Save next solution (.runs/<session-id>/step_<step>.<extension>)
|
|
518
|
-
write_to_path(
|
|
519
|
-
fp=runs_dir / f"step_{step}{source_fp.suffix}", content=eval_and_next_solution_response["code"]
|
|
520
|
-
)
|
|
521
|
-
|
|
522
|
-
# Write the next solution to the source file
|
|
523
|
-
write_to_path(fp=source_fp, content=eval_and_next_solution_response["code"])
|
|
524
|
-
|
|
525
|
-
# Get the optimization session status for
|
|
526
|
-
# the best solution, its score, and the history to plot the tree
|
|
527
|
-
status_response = get_optimization_session_status(
|
|
528
|
-
session_id=session_id, include_history=True, timeout=timeout, auth_headers=auth_headers
|
|
529
|
-
)
|
|
530
|
-
|
|
531
|
-
# Update the step of the progress bar
|
|
532
|
-
summary_panel.set_step(step=step)
|
|
533
|
-
# Update the token counts
|
|
534
|
-
summary_panel.update_token_counts(usage=eval_and_next_solution_response["usage"])
|
|
535
|
-
# Update the plan
|
|
536
|
-
plan_panel.update(plan=eval_and_next_solution_response["plan"])
|
|
537
|
-
# Build the metric tree
|
|
538
|
-
tree_panel.build_metric_tree(nodes=status_response["history"])
|
|
539
|
-
# Set the current solution as unevaluated since we haven't run the evaluation function and fed it back to the model yet
|
|
540
|
-
tree_panel.set_unevaluated_node(node_id=eval_and_next_solution_response["solution_id"])
|
|
541
|
-
|
|
542
|
-
# Update the solution panels with the next solution and best solution (and score)
|
|
543
|
-
# Figure out if we have a best solution so far
|
|
544
|
-
if status_response["best_result"] is not None:
|
|
545
|
-
best_solution_node = Node(
|
|
546
|
-
id=status_response["best_result"]["solution_id"],
|
|
547
|
-
parent_id=status_response["best_result"]["parent_id"],
|
|
548
|
-
code=status_response["best_result"]["code"],
|
|
549
|
-
metric=status_response["best_result"]["metric_value"],
|
|
550
|
-
is_buggy=status_response["best_result"]["is_buggy"],
|
|
551
|
-
)
|
|
552
|
-
else:
|
|
553
|
-
best_solution_node = None
|
|
554
|
-
|
|
555
|
-
# Create a node for the current solution
|
|
556
|
-
current_solution_node = None
|
|
557
|
-
for node in status_response["history"]:
|
|
558
|
-
if node["solution_id"] == eval_and_next_solution_response["solution_id"]:
|
|
559
|
-
current_solution_node = Node(
|
|
560
|
-
id=node["solution_id"],
|
|
561
|
-
parent_id=node["parent_id"],
|
|
562
|
-
code=node["code"],
|
|
563
|
-
metric=node["metric_value"],
|
|
564
|
-
is_buggy=node["is_buggy"],
|
|
565
|
-
)
|
|
566
|
-
if current_solution_node is None:
|
|
567
|
-
raise ValueError("Current solution node not found in history")
|
|
568
|
-
# Update the solution panels with the current and best solution
|
|
569
|
-
solution_panels.update(current_node=current_solution_node, best_node=best_solution_node)
|
|
570
|
-
current_solution_panel, best_solution_panel = solution_panels.get_display(current_step=step)
|
|
571
|
-
|
|
572
|
-
# Clear evaluation output since we are running a evaluation on a new solution
|
|
573
|
-
eval_output_panel.clear()
|
|
574
|
-
|
|
575
|
-
# Update displays with smooth transitions
|
|
576
|
-
smooth_update(
|
|
577
|
-
live=live,
|
|
578
|
-
layout=layout,
|
|
579
|
-
sections_to_update=[
|
|
580
|
-
("summary", summary_panel.get_display()),
|
|
581
|
-
("plan", plan_panel.get_display()),
|
|
582
|
-
("tree", tree_panel.get_display(is_done=False)),
|
|
583
|
-
("current_solution", current_solution_panel),
|
|
584
|
-
("best_solution", best_solution_panel),
|
|
585
|
-
("eval_output", eval_output_panel.get_display()),
|
|
586
|
-
],
|
|
587
|
-
transition_delay=0.08, # Slightly longer delay for more noticeable transitions
|
|
588
|
-
)
|
|
589
|
-
|
|
590
|
-
# Run evaluation on the current solution
|
|
591
|
-
term_out = run_evaluation(eval_command=args.eval_command)
|
|
592
|
-
eval_output_panel.update(output=term_out)
|
|
593
|
-
|
|
594
|
-
# Update evaluation output with a smooth transition
|
|
595
|
-
smooth_update(
|
|
596
|
-
live=live,
|
|
597
|
-
layout=layout,
|
|
598
|
-
sections_to_update=[("eval_output", eval_output_panel.get_display())],
|
|
599
|
-
transition_delay=0.1, # Slightly longer delay for evaluation results
|
|
600
|
-
)
|
|
601
|
-
|
|
602
|
-
# Re-read instructions from the original source (file path or string) BEFORE each suggest call
|
|
603
|
-
current_additional_instructions = read_additional_instructions(
|
|
604
|
-
additional_instructions=args.additional_instructions
|
|
605
|
-
)
|
|
606
|
-
|
|
607
|
-
# Final evaluation report
|
|
608
|
-
eval_and_next_solution_response = evaluate_feedback_then_suggest_next_solution(
|
|
609
|
-
session_id=session_id,
|
|
610
|
-
execution_output=term_out,
|
|
611
|
-
additional_instructions=current_additional_instructions,
|
|
612
|
-
api_keys=llm_api_keys,
|
|
613
|
-
timeout=timeout,
|
|
614
|
-
auth_headers=auth_headers,
|
|
615
|
-
)
|
|
616
|
-
|
|
617
|
-
# Update the progress bar
|
|
618
|
-
summary_panel.set_step(step=steps)
|
|
619
|
-
# Update the token counts
|
|
620
|
-
summary_panel.update_token_counts(usage=eval_and_next_solution_response["usage"])
|
|
621
|
-
# No need to update the plan panel since we have finished the optimization
|
|
622
|
-
# Get the optimization session status for
|
|
623
|
-
# the best solution, its score, and the history to plot the tree
|
|
624
|
-
status_response = get_optimization_session_status(
|
|
625
|
-
session_id=session_id, include_history=True, timeout=timeout, auth_headers=auth_headers
|
|
626
|
-
)
|
|
627
|
-
# Build the metric tree
|
|
628
|
-
tree_panel.build_metric_tree(nodes=status_response["history"])
|
|
629
|
-
# No need to set any solution to unevaluated since we have finished the optimization
|
|
630
|
-
# and all solutions have been evaluated
|
|
631
|
-
# No neeed to update the current solution panel since we have finished the optimization
|
|
632
|
-
# We only need to update the best solution panel
|
|
633
|
-
# Figure out if we have a best solution so far
|
|
634
|
-
if status_response["best_result"] is not None:
|
|
635
|
-
best_solution_node = Node(
|
|
636
|
-
id=status_response["best_result"]["solution_id"],
|
|
637
|
-
parent_id=status_response["best_result"]["parent_id"],
|
|
638
|
-
code=status_response["best_result"]["code"],
|
|
639
|
-
metric=status_response["best_result"]["metric_value"],
|
|
640
|
-
is_buggy=status_response["best_result"]["is_buggy"],
|
|
641
|
-
)
|
|
642
|
-
else:
|
|
643
|
-
best_solution_node = None
|
|
644
|
-
solution_panels.update(current_node=None, best_node=best_solution_node)
|
|
645
|
-
_, best_solution_panel = solution_panels.get_display(current_step=steps)
|
|
646
|
-
|
|
647
|
-
# Update the end optimization layout
|
|
648
|
-
final_message = (
|
|
649
|
-
f"{summary_panel.metric_name.capitalize()} {'maximized' if summary_panel.maximize else 'minimized'}! Best solution {summary_panel.metric_name.lower()} = [green]{status_response['best_result']['metric_value']}[/] 🏆"
|
|
650
|
-
if best_solution_node is not None and best_solution_node.metric is not None
|
|
651
|
-
else "[red] No valid solution found.[/]"
|
|
652
|
-
)
|
|
653
|
-
end_optimization_layout["summary"].update(summary_panel.get_display(final_message=final_message))
|
|
654
|
-
end_optimization_layout["tree"].update(tree_panel.get_display(is_done=True))
|
|
655
|
-
end_optimization_layout["best_solution"].update(best_solution_panel)
|
|
656
|
-
|
|
657
|
-
# Save optimization results
|
|
658
|
-
# If the best solution does not exist or is has not been measured at the end of the optimization
|
|
659
|
-
# save the original solution as the best solution
|
|
660
|
-
if best_solution_node is not None:
|
|
661
|
-
best_solution_code = best_solution_node.code
|
|
662
|
-
best_solution_score = best_solution_node.metric
|
|
663
|
-
else:
|
|
664
|
-
best_solution_code = None
|
|
665
|
-
best_solution_score = None
|
|
666
|
-
|
|
667
|
-
if best_solution_code is None or best_solution_score is None:
|
|
668
|
-
best_solution_content = f"# Weco could not find a better solution\n\n{read_from_path(fp=runs_dir / f'step_0{source_fp.suffix}', is_json=False)}"
|
|
669
|
-
else:
|
|
670
|
-
# Format score for the comment
|
|
671
|
-
best_score_str = (
|
|
672
|
-
format_number(best_solution_score)
|
|
673
|
-
if best_solution_score is not None and isinstance(best_solution_score, (int, float))
|
|
674
|
-
else "N/A"
|
|
675
|
-
)
|
|
676
|
-
best_solution_content = (
|
|
677
|
-
f"# Best solution from Weco with a score of {best_score_str}\n\n{best_solution_code}"
|
|
678
|
-
)
|
|
679
|
-
|
|
680
|
-
# Save best solution to .runs/<session-id>/best.<extension>
|
|
681
|
-
write_to_path(fp=runs_dir / f"best{source_fp.suffix}", content=best_solution_content)
|
|
682
|
-
|
|
683
|
-
# write the best solution to the source file
|
|
684
|
-
write_to_path(fp=source_fp, content=best_solution_content)
|
|
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)
|
|
685
83
|
|
|
686
|
-
# Mark as completed normally for the finally block
|
|
687
|
-
optimization_completed_normally = True
|
|
688
84
|
|
|
689
|
-
|
|
85
|
+
def main() -> None:
|
|
86
|
+
"""Main function for the Weco CLI."""
|
|
87
|
+
check_for_cli_updates()
|
|
690
88
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
except Exception:
|
|
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)
|
|
699
|
-
# console.print_exception(show_locals=False)
|
|
89
|
+
parser = argparse.ArgumentParser(
|
|
90
|
+
description="[bold cyan]Weco CLI[/]\nEnhance your code with AI-driven optimization.",
|
|
91
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
92
|
+
)
|
|
700
93
|
|
|
701
|
-
|
|
702
|
-
|
|
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
|
+
)
|
|
703
102
|
|
|
704
|
-
|
|
705
|
-
|
|
103
|
+
subparsers = parser.add_subparsers(
|
|
104
|
+
dest="command", help="Available commands"
|
|
105
|
+
) # Removed required=True for now to handle chatbot case easily
|
|
706
106
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
|
710
112
|
|
|
711
|
-
|
|
712
|
-
|
|
113
|
+
# --- Logout Command Parser Setup ---
|
|
114
|
+
_ = subparsers.add_parser("logout", help="Log out from Weco and clear saved API key.")
|
|
713
115
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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
|
|
718
177
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
final_details = 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)
|
|
724
182
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
|
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)
|
|
736
186
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
-
)
|
|
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()
|
|
747
190
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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)
|