weco 0.2.20__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 +229 -16
- weco/auth.py +164 -3
- weco/chatbot.py +797 -0
- weco/cli.py +129 -643
- weco/optimizer.py +479 -0
- weco/panels.py +46 -0
- weco/utils.py +31 -3
- {weco-0.2.20.dist-info → weco-0.2.22.dist-info}/METADATA +107 -32
- weco-0.2.22.dist-info/RECORD +14 -0
- weco-0.2.20.dist-info/RECORD +0 -12
- {weco-0.2.20.dist-info → weco-0.2.22.dist-info}/WHEEL +0 -0
- {weco-0.2.20.dist-info → weco-0.2.22.dist-info}/entry_points.txt +0 -0
- {weco-0.2.20.dist-info → weco-0.2.22.dist-info}/licenses/LICENSE +0 -0
- {weco-0.2.20.dist-info → weco-0.2.22.dist-info}/top_level.txt +0 -0
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(
|
|
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(
|
|
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 {
|
|
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
|
|