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/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 __base_url__
26
- from .auth import load_weco_api_key, save_api_key, clear_api_key
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
- # --- Heartbeat Sender Class ---
59
- class HeartbeatSender(threading.Thread):
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-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`.",
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
- if args.command == "logout":
297
- clear_api_key()
298
- sys.exit(0)
299
- elif args.command == "run":
300
- global heartbeat_thread, current_run_id_for_heartbeat, current_auth_headers_for_heartbeat # Allow modification of globals
301
- run_id = None # Initialize run_id (we receive this from the API after starting the run)
302
- optimization_completed_normally = False
303
- user_stop_requested_flag = False
304
- weco_api_key = load_weco_api_key()
305
- llm_api_keys = read_api_keys_from_env() # Read keys from client environment
306
-
307
- # --- Login/Authentication Handling ---
308
- if not weco_api_key:
309
- login_choice = Prompt.ask(
310
- "Log in to Weco to save run history or use anonymously? ([bold]L[/]ogin / [bold]S[/]kip)",
311
- choices=["l", "s"],
312
- default="s",
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
- # Starting from step 1 to steps (inclusive) because the baseline solution is step 0, so we want to optimize for steps worth of steps
470
- for step in range(1, steps + 1):
471
- # Re-read instructions from the original source (file path or string) BEFORE each suggest call
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
- # Send feedback and get next suggestion
495
- eval_and_next_solution_response = evaluate_feedback_then_suggest_next_solution(
496
- run_id=run_id,
497
- execution_output=term_out,
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
- nodes_list_from_status = status_response.get("nodes")
518
- tree_panel.build_metric_tree(nodes=nodes_list_from_status if nodes_list_from_status is not None else [])
519
- tree_panel.set_unevaluated_node(node_id=eval_and_next_solution_response["solution_id"])
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
- # Update the solution panels with the next solution and best solution (and score)
522
- # Figure out if we have a best solution so far
523
- if status_response["best_result"] is not None:
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
- current_solution_node = None
535
- if status_response.get("nodes"):
536
- for node_data in status_response["nodes"]:
537
- if node_data["solution_id"] == eval_and_next_solution_response["solution_id"]:
538
- current_solution_node = Node(
539
- id=node_data["solution_id"],
540
- parent_id=node_data["parent_id"],
541
- code=node_data["code"],
542
- metric=node_data["metric_value"],
543
- is_buggy=node_data["is_buggy"],
544
- )
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
- # Update the solution panels with the current and best solution
549
- solution_panels.update(current_node=current_solution_node, best_node=best_solution_node)
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
- if not user_stop_requested_flag:
576
- # Re-read instructions from the original source (file path or string) BEFORE each suggest call
577
- current_additional_instructions = read_additional_instructions(
578
- additional_instructions=args.additional_instructions
579
- )
580
- # Evaluate the final solution thats been generated
581
- eval_and_next_solution_response = evaluate_feedback_then_suggest_next_solution(
582
- run_id=run_id,
583
- execution_output=term_out,
584
- additional_instructions=current_additional_instructions,
585
- api_keys=llm_api_keys,
586
- timeout=timeout,
587
- auth_headers=auth_headers,
588
- )
589
- summary_panel.set_step(step=steps)
590
- summary_panel.update_token_counts(usage=eval_and_next_solution_response["usage"])
591
- status_response = get_optimization_run_status(
592
- run_id=run_id, include_history=True, timeout=timeout, auth_headers=auth_headers
593
- )
594
- # No need to update the plan panel since we have finished the optimization
595
- # Get the optimization run status for
596
- # the best solution, its score, and the history to plot the tree
597
- nodes_list_from_status_final = status_response.get("nodes")
598
- tree_panel.build_metric_tree(
599
- nodes=nodes_list_from_status_final if nodes_list_from_status_final is not None else []
600
- )
601
- # No need to set any solution to unevaluated since we have finished the optimization
602
- # and all solutions have been evaluated
603
- # No neeed to update the current solution panel since we have finished the optimization
604
- # We only need to update the best solution panel
605
- # Figure out if we have a best solution so far
606
- if status_response["best_result"] is not None:
607
- best_solution_node = Node(
608
- id=status_response["best_result"]["solution_id"],
609
- parent_id=status_response["best_result"]["parent_id"],
610
- code=status_response["best_result"]["code"],
611
- metric=status_response["best_result"]["metric_value"],
612
- is_buggy=status_response["best_result"]["is_buggy"],
613
- )
614
- else:
615
- best_solution_node = None
616
- solution_panels.update(current_node=None, best_node=best_solution_node)
617
- _, best_solution_panel = solution_panels.get_display(current_step=steps)
618
- # Update the end optimization layout
619
- final_message = (
620
- f"{summary_panel.metric_name.capitalize()} {'maximized' if summary_panel.maximize else 'minimized'}! Best solution {summary_panel.metric_name.lower()} = [green]{status_response['best_result']['metric_value']}[/] 🏆"
621
- if best_solution_node is not None and best_solution_node.metric is not None
622
- else "[red] No valid solution found.[/]"
623
- )
624
- end_optimization_layout["summary"].update(summary_panel.get_display(final_message=final_message))
625
- end_optimization_layout["tree"].update(tree_panel.get_display(is_done=True))
626
- end_optimization_layout["best_solution"].update(best_solution_panel)
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
- # Save optimization results
629
- # If the best solution does not exist or is has not been measured at the end of the optimization
630
- # save the original solution as the best solution
631
- if best_solution_node is not None:
632
- best_solution_code = best_solution_node.code
633
- best_solution_score = best_solution_node.metric
634
- else:
635
- best_solution_code = None
636
- best_solution_score = None
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
- if best_solution_code is None or best_solution_score is None:
639
- best_solution_content = f"# Weco could not find a better solution\n\n{read_from_path(fp=runs_dir / f'step_0{source_fp.suffix}', is_json=False)}"
640
- else:
641
- # Format score for the comment
642
- best_score_str = (
643
- format_number(best_solution_score)
644
- if best_solution_score is not None and isinstance(best_solution_score, (int, float))
645
- else "N/A"
646
- )
647
- best_solution_content = (
648
- f"# Best solution from Weco with a score of {best_score_str}\n\n{best_solution_code}"
649
- )
650
- # Save best solution to .runs/<run-id>/best.<extension>
651
- write_to_path(fp=runs_dir / f"best{source_fp.suffix}", content=best_solution_content)
652
- # write the best solution to the source file
653
- write_to_path(fp=source_fp, content=best_solution_content)
654
- # Mark as completed normally for the finally block
655
- optimization_completed_normally = True
656
- console.print(end_optimization_layout)
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
- except Exception as e:
659
- # Catch errors during the main optimization loop or setup
660
- try:
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
- # Report final status if a run was started
677
- if run_id:
678
- final_status_update = "unknown"
679
- final_reason_code = "unknown_termination"
680
- final_details = None
681
- if optimization_completed_normally:
682
- final_status_update = "completed"
683
- final_reason_code = "completed_successfully"
684
- elif user_stop_requested_flag:
685
- final_status_update = "terminated"
686
- final_reason_code = "user_requested_stop"
687
- final_details = "Run stopped by user request via dashboard."
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)