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/optimizer.py ADDED
@@ -0,0 +1,479 @@
1
+ import pathlib
2
+ import math
3
+ import requests
4
+ import threading
5
+ import signal
6
+ import sys
7
+ import traceback
8
+ from typing import Optional
9
+ from rich.console import Console
10
+ from rich.live import Live
11
+ from rich.panel import Panel
12
+
13
+ from .api import (
14
+ start_optimization_run,
15
+ evaluate_feedback_then_suggest_next_solution,
16
+ get_optimization_run_status,
17
+ send_heartbeat,
18
+ report_termination,
19
+ )
20
+ from .auth import handle_authentication
21
+ from .panels import (
22
+ SummaryPanel,
23
+ PlanPanel,
24
+ Node,
25
+ MetricTreePanel,
26
+ EvaluationOutputPanel,
27
+ SolutionPanels,
28
+ create_optimization_layout,
29
+ create_end_optimization_layout,
30
+ )
31
+ from .utils import (
32
+ read_api_keys_from_env,
33
+ read_additional_instructions,
34
+ read_from_path,
35
+ write_to_path,
36
+ run_evaluation,
37
+ smooth_update,
38
+ format_number,
39
+ )
40
+
41
+
42
+ # --- Heartbeat Sender Class ---
43
+ class HeartbeatSender(threading.Thread):
44
+ def __init__(self, run_id: str, auth_headers: dict, stop_event: threading.Event, interval: int = 30):
45
+ super().__init__(daemon=True) # Daemon thread exits when main thread exits
46
+ self.run_id = run_id
47
+ self.auth_headers = auth_headers
48
+ self.interval = interval
49
+ self.stop_event = stop_event
50
+
51
+ def run(self):
52
+ try:
53
+ while not self.stop_event.is_set():
54
+ if not send_heartbeat(self.run_id, self.auth_headers):
55
+ # send_heartbeat itself prints errors to stderr if it returns False
56
+ # No explicit HeartbeatSender log needed here unless more detail is desired for a False return
57
+ pass
58
+
59
+ if self.stop_event.is_set(): # Check before waiting for responsiveness
60
+ break
61
+
62
+ self.stop_event.wait(self.interval) # Wait for interval or stop signal
63
+
64
+ except Exception as e:
65
+ # Catch any unexpected error in the loop to prevent silent thread death
66
+ print(f"[ERROR HeartbeatSender] Unhandled exception in run loop for run {self.run_id}: {e}", file=sys.stderr)
67
+ traceback.print_exc(file=sys.stderr)
68
+ # The loop will break due to the exception, and thread will terminate via finally.
69
+
70
+
71
+ def execute_optimization(
72
+ source: str,
73
+ eval_command: str,
74
+ metric: str,
75
+ goal: str, # "maximize" or "minimize"
76
+ steps: int = 100,
77
+ model: Optional[str] = None,
78
+ log_dir: str = ".runs",
79
+ additional_instructions: Optional[str] = None,
80
+ console: Optional[Console] = None,
81
+ ) -> bool:
82
+ """
83
+ Execute the core optimization logic.
84
+
85
+ Returns:
86
+ bool: True if optimization completed successfully, False otherwise
87
+ """
88
+ if console is None:
89
+ console = Console()
90
+
91
+ # Global variables for this optimization run
92
+ heartbeat_thread = None
93
+ stop_heartbeat_event = threading.Event()
94
+ current_run_id_for_heartbeat = None
95
+ current_auth_headers_for_heartbeat = {}
96
+
97
+ # --- Signal Handler for this optimization run ---
98
+ def signal_handler(signum, frame):
99
+ signal_name = signal.Signals(signum).name
100
+ console.print(f"\n[bold yellow]Termination signal ({signal_name}) received. Shutting down...[/]")
101
+
102
+ # Stop heartbeat thread
103
+ stop_heartbeat_event.set()
104
+ if heartbeat_thread and heartbeat_thread.is_alive():
105
+ heartbeat_thread.join(timeout=2) # Give it a moment to stop
106
+
107
+ # Report termination (best effort)
108
+ if current_run_id_for_heartbeat:
109
+ report_termination(
110
+ run_id=current_run_id_for_heartbeat,
111
+ status_update="terminated",
112
+ reason=f"user_terminated_{signal_name.lower()}",
113
+ details=f"Process terminated by signal {signal_name} ({signum}).",
114
+ auth_headers=current_auth_headers_for_heartbeat,
115
+ timeout=3,
116
+ )
117
+
118
+ # Exit gracefully
119
+ sys.exit(0)
120
+
121
+ # Set up signal handlers for this run
122
+ original_sigint_handler = signal.signal(signal.SIGINT, signal_handler)
123
+ original_sigterm_handler = signal.signal(signal.SIGTERM, signal_handler)
124
+
125
+ run_id = None
126
+ optimization_completed_normally = False
127
+ user_stop_requested_flag = False
128
+
129
+ try:
130
+ llm_api_keys = read_api_keys_from_env()
131
+
132
+ # --- Login/Authentication Handling ---
133
+ weco_api_key, auth_headers = handle_authentication(console, llm_api_keys)
134
+ if weco_api_key is None and not llm_api_keys:
135
+ # Authentication failed and no LLM keys available
136
+ return False
137
+
138
+ current_auth_headers_for_heartbeat = auth_headers
139
+
140
+ # --- Process Parameters ---
141
+ maximize = goal.lower() in ["maximize", "max"]
142
+
143
+ # Determine the model to use
144
+ if model is None:
145
+ from .utils import determine_default_model
146
+
147
+ model = determine_default_model(llm_api_keys)
148
+
149
+ code_generator_config = {"model": model}
150
+ evaluator_config = {"model": model, "include_analysis": True}
151
+ search_policy_config = {
152
+ "num_drafts": max(1, math.ceil(0.15 * steps)),
153
+ "debug_prob": 0.5,
154
+ "max_debug_depth": max(1, math.ceil(0.1 * steps)),
155
+ }
156
+ timeout = 800
157
+ processed_additional_instructions = read_additional_instructions(additional_instructions=additional_instructions)
158
+ source_fp = pathlib.Path(source)
159
+ source_code = read_from_path(fp=source_fp, is_json=False)
160
+
161
+ # --- Panel Initialization ---
162
+ summary_panel = SummaryPanel(maximize=maximize, metric_name=metric, total_steps=steps, model=model, runs_dir=log_dir)
163
+ plan_panel = PlanPanel()
164
+ solution_panels = SolutionPanels(metric_name=metric, source_fp=source_fp)
165
+ eval_output_panel = EvaluationOutputPanel()
166
+ tree_panel = MetricTreePanel(maximize=maximize)
167
+ layout = create_optimization_layout()
168
+ end_optimization_layout = create_end_optimization_layout()
169
+
170
+ # --- Start Optimization Run ---
171
+ run_response = start_optimization_run(
172
+ console=console,
173
+ source_code=source_code,
174
+ evaluation_command=eval_command,
175
+ metric_name=metric,
176
+ maximize=maximize,
177
+ steps=steps,
178
+ code_generator_config=code_generator_config,
179
+ evaluator_config=evaluator_config,
180
+ search_policy_config=search_policy_config,
181
+ additional_instructions=processed_additional_instructions,
182
+ api_keys=llm_api_keys,
183
+ auth_headers=auth_headers,
184
+ timeout=timeout,
185
+ )
186
+ run_id = run_response["run_id"]
187
+ current_run_id_for_heartbeat = run_id
188
+
189
+ # --- Start Heartbeat Thread ---
190
+ stop_heartbeat_event.clear()
191
+ heartbeat_thread = HeartbeatSender(run_id, auth_headers, stop_heartbeat_event)
192
+ heartbeat_thread.start()
193
+
194
+ # --- Live Update Loop ---
195
+ refresh_rate = 4
196
+ with Live(layout, refresh_per_second=refresh_rate) as live:
197
+ # Define the runs directory (.runs/<run-id>) to store logs and results
198
+ runs_dir = pathlib.Path(log_dir) / run_id
199
+ runs_dir.mkdir(parents=True, exist_ok=True)
200
+ # Write the initial code string to the logs
201
+ write_to_path(fp=runs_dir / f"step_0{source_fp.suffix}", content=run_response["code"])
202
+ # Write the initial code string to the source file path
203
+ write_to_path(fp=source_fp, content=run_response["code"])
204
+
205
+ # Update the panels with the initial solution
206
+ summary_panel.set_run_id(run_id=run_id) # Add run id now that we have it
207
+ # Set the step of the progress bar
208
+ summary_panel.set_step(step=0)
209
+ # Update the token counts
210
+ summary_panel.update_token_counts(usage=run_response["usage"])
211
+ plan_panel.update(plan=run_response["plan"])
212
+ # Build the metric tree
213
+ tree_panel.build_metric_tree(
214
+ nodes=[
215
+ {
216
+ "solution_id": run_response["solution_id"],
217
+ "parent_id": None,
218
+ "code": run_response["code"],
219
+ "step": 0,
220
+ "metric_value": None,
221
+ "is_buggy": False,
222
+ }
223
+ ]
224
+ )
225
+ # Set the current solution as unevaluated since we haven't run the evaluation function and fed it back to the model yet
226
+ tree_panel.set_unevaluated_node(node_id=run_response["solution_id"])
227
+ # Update the solution panels with the initial solution and get the panel displays
228
+ solution_panels.update(
229
+ current_node=Node(
230
+ id=run_response["solution_id"], parent_id=None, code=run_response["code"], metric=None, is_buggy=False
231
+ ),
232
+ best_node=None,
233
+ )
234
+ current_solution_panel, best_solution_panel = solution_panels.get_display(current_step=0)
235
+ # Update the live layout with the initial solution panels
236
+ smooth_update(
237
+ live=live,
238
+ layout=layout,
239
+ sections_to_update=[
240
+ ("summary", summary_panel.get_display()),
241
+ ("plan", plan_panel.get_display()),
242
+ ("tree", tree_panel.get_display(is_done=False)),
243
+ ("current_solution", current_solution_panel),
244
+ ("best_solution", best_solution_panel),
245
+ ("eval_output", eval_output_panel.get_display()),
246
+ ],
247
+ transition_delay=0.1,
248
+ )
249
+
250
+ # Run evaluation on the initial solution
251
+ term_out = run_evaluation(eval_command=eval_command)
252
+ # Update the evaluation output panel
253
+ eval_output_panel.update(output=term_out)
254
+ smooth_update(
255
+ live=live,
256
+ layout=layout,
257
+ sections_to_update=[("eval_output", eval_output_panel.get_display())],
258
+ transition_delay=0.1,
259
+ )
260
+
261
+ # Starting from step 1 to steps (inclusive) because the baseline solution is step 0, so we want to optimize for steps worth of steps
262
+ for step in range(1, steps + 1):
263
+ # Re-read instructions from the original source (file path or string) BEFORE each suggest call
264
+ current_additional_instructions = read_additional_instructions(additional_instructions=additional_instructions)
265
+ if run_id:
266
+ try:
267
+ current_status_response = get_optimization_run_status(
268
+ run_id=run_id, include_history=False, timeout=30, auth_headers=auth_headers
269
+ )
270
+ current_run_status_val = current_status_response.get("status")
271
+ if current_run_status_val == "stopping":
272
+ console.print("\n[bold yellow]Stop request received. Terminating run gracefully...[/]")
273
+ user_stop_requested_flag = True
274
+ break
275
+ except requests.exceptions.RequestException as e:
276
+ console.print(f"\n[bold red]Warning: Could not check run status: {e}. Continuing optimization...[/]")
277
+ except Exception as e:
278
+ console.print(f"\n[bold red]Warning: Error checking run status: {e}. Continuing optimization...[/]")
279
+
280
+ # Send feedback and get next suggestion
281
+ eval_and_next_solution_response = evaluate_feedback_then_suggest_next_solution(
282
+ run_id=run_id,
283
+ execution_output=term_out,
284
+ additional_instructions=current_additional_instructions,
285
+ api_keys=llm_api_keys,
286
+ auth_headers=auth_headers,
287
+ timeout=timeout,
288
+ )
289
+ # Save next solution (.runs/<run-id>/step_<step>.<extension>)
290
+ write_to_path(fp=runs_dir / f"step_{step}{source_fp.suffix}", content=eval_and_next_solution_response["code"])
291
+ # Write the next solution to the source file
292
+ write_to_path(fp=source_fp, content=eval_and_next_solution_response["code"])
293
+ status_response = get_optimization_run_status(
294
+ run_id=run_id, include_history=True, timeout=timeout, auth_headers=auth_headers
295
+ )
296
+ # Update the step of the progress bar, token counts, plan and metric tree
297
+ summary_panel.set_step(step=step)
298
+ summary_panel.update_token_counts(usage=eval_and_next_solution_response["usage"])
299
+ plan_panel.update(plan=eval_and_next_solution_response["plan"])
300
+
301
+ nodes_list_from_status = status_response.get("nodes")
302
+ tree_panel.build_metric_tree(nodes=nodes_list_from_status if nodes_list_from_status is not None else [])
303
+ tree_panel.set_unevaluated_node(node_id=eval_and_next_solution_response["solution_id"])
304
+
305
+ # Update the solution panels with the next solution and best solution (and score)
306
+ # Figure out if we have a best solution so far
307
+ if status_response["best_result"] is not None:
308
+ best_solution_node = Node(
309
+ id=status_response["best_result"]["solution_id"],
310
+ parent_id=status_response["best_result"]["parent_id"],
311
+ code=status_response["best_result"]["code"],
312
+ metric=status_response["best_result"]["metric_value"],
313
+ is_buggy=status_response["best_result"]["is_buggy"],
314
+ )
315
+ else:
316
+ best_solution_node = None
317
+
318
+ current_solution_node = None
319
+ if status_response.get("nodes"):
320
+ for node_data in status_response["nodes"]:
321
+ if node_data["solution_id"] == eval_and_next_solution_response["solution_id"]:
322
+ current_solution_node = Node(
323
+ id=node_data["solution_id"],
324
+ parent_id=node_data["parent_id"],
325
+ code=node_data["code"],
326
+ metric=node_data["metric_value"],
327
+ is_buggy=node_data["is_buggy"],
328
+ )
329
+ if current_solution_node is None:
330
+ raise ValueError("Current solution node not found in nodes list from status response")
331
+
332
+ # Update the solution panels with the current and best solution
333
+ solution_panels.update(current_node=current_solution_node, best_node=best_solution_node)
334
+ current_solution_panel, best_solution_panel = solution_panels.get_display(current_step=step)
335
+ # Clear evaluation output since we are running a evaluation on a new solution
336
+ eval_output_panel.clear()
337
+ smooth_update(
338
+ live=live,
339
+ layout=layout,
340
+ sections_to_update=[
341
+ ("summary", summary_panel.get_display()),
342
+ ("plan", plan_panel.get_display()),
343
+ ("tree", tree_panel.get_display(is_done=False)),
344
+ ("current_solution", current_solution_panel),
345
+ ("best_solution", best_solution_panel),
346
+ ("eval_output", eval_output_panel.get_display()),
347
+ ],
348
+ transition_delay=0.08, # Slightly longer delay for more noticeable transitions
349
+ )
350
+ term_out = run_evaluation(eval_command=eval_command)
351
+ eval_output_panel.update(output=term_out)
352
+ smooth_update(
353
+ live=live,
354
+ layout=layout,
355
+ sections_to_update=[("eval_output", eval_output_panel.get_display())],
356
+ transition_delay=0.1,
357
+ )
358
+
359
+ if not user_stop_requested_flag:
360
+ # Re-read instructions from the original source (file path or string) BEFORE each suggest call
361
+ current_additional_instructions = read_additional_instructions(additional_instructions=additional_instructions)
362
+ # Evaluate the final solution thats been generated
363
+ eval_and_next_solution_response = evaluate_feedback_then_suggest_next_solution(
364
+ run_id=run_id,
365
+ execution_output=term_out,
366
+ additional_instructions=current_additional_instructions,
367
+ api_keys=llm_api_keys,
368
+ timeout=timeout,
369
+ auth_headers=auth_headers,
370
+ )
371
+ summary_panel.set_step(step=steps)
372
+ summary_panel.update_token_counts(usage=eval_and_next_solution_response["usage"])
373
+ status_response = get_optimization_run_status(
374
+ run_id=run_id, include_history=True, timeout=timeout, auth_headers=auth_headers
375
+ )
376
+ # No need to update the plan panel since we have finished the optimization
377
+ # Get the optimization run status for
378
+ # the best solution, its score, and the history to plot the tree
379
+ nodes_list_from_status_final = status_response.get("nodes")
380
+ tree_panel.build_metric_tree(
381
+ nodes=nodes_list_from_status_final if nodes_list_from_status_final is not None else []
382
+ )
383
+ # No need to set any solution to unevaluated since we have finished the optimization
384
+ # and all solutions have been evaluated
385
+ # No neeed to update the current solution panel since we have finished the optimization
386
+ # We only need to update the best solution panel
387
+ # Figure out if we have a best solution so far
388
+ if status_response["best_result"] is not None:
389
+ best_solution_node = Node(
390
+ id=status_response["best_result"]["solution_id"],
391
+ parent_id=status_response["best_result"]["parent_id"],
392
+ code=status_response["best_result"]["code"],
393
+ metric=status_response["best_result"]["metric_value"],
394
+ is_buggy=status_response["best_result"]["is_buggy"],
395
+ )
396
+ else:
397
+ best_solution_node = None
398
+ solution_panels.update(current_node=None, best_node=best_solution_node)
399
+ _, best_solution_panel = solution_panels.get_display(current_step=steps)
400
+ # Update the end optimization layout
401
+ final_message = (
402
+ 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']}[/] 🏆"
403
+ if best_solution_node is not None and best_solution_node.metric is not None
404
+ else "[red] No valid solution found.[/]"
405
+ )
406
+ end_optimization_layout["summary"].update(summary_panel.get_display(final_message=final_message))
407
+ end_optimization_layout["tree"].update(tree_panel.get_display(is_done=True))
408
+ end_optimization_layout["best_solution"].update(best_solution_panel)
409
+
410
+ # Save optimization results
411
+ # If the best solution does not exist or is has not been measured at the end of the optimization
412
+ # save the original solution as the best solution
413
+ if best_solution_node is not None:
414
+ best_solution_code = best_solution_node.code
415
+ best_solution_score = best_solution_node.metric
416
+ else:
417
+ best_solution_code = None
418
+ best_solution_score = None
419
+
420
+ if best_solution_code is None or best_solution_score is None:
421
+ 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)}"
422
+ else:
423
+ # Format score for the comment
424
+ best_score_str = (
425
+ format_number(best_solution_score)
426
+ if best_solution_score is not None and isinstance(best_solution_score, (int, float))
427
+ else "N/A"
428
+ )
429
+ best_solution_content = (
430
+ f"# Best solution from Weco with a score of {best_score_str}\n\n{best_solution_code}"
431
+ )
432
+ # Save best solution to .runs/<run-id>/best.<extension>
433
+ write_to_path(fp=runs_dir / f"best{source_fp.suffix}", content=best_solution_content)
434
+ # write the best solution to the source file
435
+ write_to_path(fp=source_fp, content=best_solution_content)
436
+ # Mark as completed normally for the finally block
437
+ optimization_completed_normally = True
438
+ live.update(end_optimization_layout)
439
+
440
+ except Exception as e:
441
+ # Catch errors during the main optimization loop or setup
442
+ try:
443
+ error_message = e.response.json()["detail"]
444
+ except Exception:
445
+ error_message = str(e)
446
+ console.print(Panel(f"[bold red]Error: {error_message}", title="[bold red]Optimization Error", border_style="red"))
447
+ # Ensure optimization_completed_normally is False
448
+ optimization_completed_normally = False
449
+ finally:
450
+ # Restore original signal handlers
451
+ signal.signal(signal.SIGINT, original_sigint_handler)
452
+ signal.signal(signal.SIGTERM, original_sigterm_handler)
453
+
454
+ # Stop heartbeat thread
455
+ stop_heartbeat_event.set()
456
+ if heartbeat_thread and heartbeat_thread.is_alive():
457
+ heartbeat_thread.join(timeout=2)
458
+
459
+ # Report final status if run exists
460
+ if run_id:
461
+ if optimization_completed_normally:
462
+ status, reason, details = "completed", "completed_successfully", None
463
+ elif user_stop_requested_flag:
464
+ status, reason, details = "terminated", "user_requested_stop", "Run stopped by user request via dashboard."
465
+ else:
466
+ status, reason = "error", "error_cli_internal"
467
+ details = locals().get("error_details") or (
468
+ traceback.format_exc()
469
+ if "e" in locals() and isinstance(locals()["e"], Exception)
470
+ else "CLI terminated unexpectedly without a specific exception captured."
471
+ )
472
+
473
+ report_termination(run_id, status, reason, details, current_auth_headers_for_heartbeat)
474
+
475
+ # Handle exit
476
+ if user_stop_requested_flag:
477
+ console.print("[yellow]Run terminated by user request.[/]")
478
+
479
+ return optimization_completed_normally or user_stop_requested_flag
weco/panels.py CHANGED
@@ -4,6 +4,7 @@ from rich.progress import BarColumn, Progress, TextColumn
4
4
  from rich.layout import Layout
5
5
  from rich.panel import Panel
6
6
  from rich.syntax import Syntax
7
+ from rich import box
7
8
  from typing import Dict, List, Optional, Union, Tuple
8
9
  from .utils import format_number
9
10
  import pathlib
@@ -367,3 +368,48 @@ def create_end_optimization_layout() -> Layout:
367
368
  layout["bottom_section"].split_row(Layout(name="best_solution", ratio=1), Layout(name="tree", ratio=1))
368
369
 
369
370
  return layout
371
+
372
+
373
+ class OptimizationOptionsPanel:
374
+ """Panel for displaying optimization options in a table.
375
+
376
+ Creates a formatted table showing optimization suggestions with details
377
+ like target file, description, estimated cost, and predicted gains.
378
+ """
379
+
380
+ def get_display(self, options: List[Dict[str, str]]) -> Table:
381
+ """Create optimization options table as a renderable object."""
382
+ table = Table(title="Optimization Options", show_lines=True, box=box.ROUNDED, border_style="cyan", padding=(1, 1))
383
+ table.add_column("No.", style="bold white", width=5, header_style="bold white", justify="center")
384
+ table.add_column("Target File", style="cyan", width=20, header_style="bold white")
385
+ table.add_column("Description", style="magenta", width=40, header_style="bold white")
386
+ table.add_column("Est. Token Cost", style="yellow", width=15, header_style="bold white")
387
+ table.add_column("Pred. Perf. Gain", style="green", width=20, header_style="bold white")
388
+
389
+ for i, opt in enumerate(options):
390
+ table.add_row(
391
+ str(i + 1),
392
+ opt["target_file"],
393
+ opt["description"],
394
+ opt["estimated_token_cost"],
395
+ opt["predicted_performance_gain"],
396
+ )
397
+ return table
398
+
399
+
400
+ class EvaluationScriptPanel:
401
+ """Panel for displaying evaluation scripts with syntax highlighting.
402
+
403
+ Shows Python evaluation scripts with proper syntax highlighting,
404
+ line numbers, and a descriptive title.
405
+ """
406
+
407
+ def get_display(self, script_content: str, script_path: str = "evaluate.py") -> Panel:
408
+ """Create a panel displaying the evaluation script with syntax highlighting."""
409
+ return Panel(
410
+ Syntax(script_content, "python", theme="monokai", line_numbers=True),
411
+ title=f"[bold]📄 Evaluation Script: {script_path}",
412
+ border_style="cyan",
413
+ expand=True,
414
+ padding=(0, 1),
415
+ )
weco/utils.py CHANGED
@@ -23,6 +23,32 @@ def read_api_keys_from_env() -> Dict[str, Any]:
23
23
  return keys
24
24
 
25
25
 
26
+ def determine_default_model(llm_api_keys: Dict[str, Any]) -> str:
27
+ """Determine the default model based on available API keys.
28
+
29
+ Uses priority: OpenAI > Anthropic > Gemini
30
+
31
+ Args:
32
+ llm_api_keys: Dictionary of available LLM API keys
33
+
34
+ Returns:
35
+ str: The default model name to use
36
+
37
+ Raises:
38
+ ValueError: If no LLM API keys are found
39
+ """
40
+ if "OPENAI_API_KEY" in llm_api_keys:
41
+ return "o4-mini"
42
+ elif "ANTHROPIC_API_KEY" in llm_api_keys:
43
+ return "claude-sonnet-4-0"
44
+ elif "GEMINI_API_KEY" in llm_api_keys:
45
+ return "gemini-2.5-pro"
46
+ else:
47
+ raise ValueError(
48
+ "No LLM API keys found in environment. Please set one of the following: OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY."
49
+ )
50
+
51
+
26
52
  def read_additional_instructions(additional_instructions: str | None) -> str | None:
27
53
  """Read additional instructions from a file path string or return the string itself."""
28
54
  if additional_instructions is None:
@@ -114,21 +140,23 @@ def run_evaluation(eval_command: str) -> str:
114
140
 
115
141
 
116
142
  # Update Check Function
117
- def check_for_cli_updates(current_version_str: str):
143
+ def check_for_cli_updates():
118
144
  """Checks PyPI for a newer version of the weco package and notifies the user."""
119
145
  try:
146
+ from . import __pkg_version__
147
+
120
148
  pypi_url = "https://pypi.org/pypi/weco/json"
121
149
  response = requests.get(pypi_url, timeout=5) # Short timeout for non-critical check
122
150
  response.raise_for_status()
123
151
  latest_version_str = response.json()["info"]["version"]
124
152
 
125
- current_version = parse_version(current_version_str)
153
+ current_version = parse_version(__pkg_version__)
126
154
  latest_version = parse_version(latest_version_str)
127
155
 
128
156
  if latest_version > current_version:
129
157
  yellow_start = "\033[93m"
130
158
  reset_color = "\033[0m"
131
- message = f"WARNING: New weco version ({latest_version_str}) available (you have {current_version_str}). Run: pip install --upgrade weco"
159
+ message = f"WARNING: New weco version ({latest_version_str}) available (you have {__pkg_version__}). Run: pip install --upgrade weco"
132
160
  print(f"{yellow_start}{message}{reset_color}")
133
161
  time.sleep(2) # Wait for 2 second
134
162