weco 0.3.7__py3-none-any.whl → 0.3.8__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/api.py +125 -0
- weco/browser.py +29 -0
- weco/cli.py +49 -7
- weco/optimizer.py +510 -817
- weco/ui.py +315 -0
- weco/validation.py +112 -0
- {weco-0.3.7.dist-info → weco-0.3.8.dist-info}/METADATA +2 -2
- weco-0.3.8.dist-info/RECORD +18 -0
- {weco-0.3.7.dist-info → weco-0.3.8.dist-info}/WHEEL +1 -1
- weco-0.3.7.dist-info/RECORD +0 -15
- {weco-0.3.7.dist-info → weco-0.3.8.dist-info}/entry_points.txt +0 -0
- {weco-0.3.7.dist-info → weco-0.3.8.dist-info}/licenses/LICENSE +0 -0
- {weco-0.3.7.dist-info → weco-0.3.8.dist-info}/top_level.txt +0 -0
weco/api.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import sys
|
|
2
|
+
from dataclasses import dataclass
|
|
2
3
|
from typing import Dict, Any, Optional, Union, Tuple
|
|
3
4
|
import requests
|
|
4
5
|
from rich.console import Console
|
|
@@ -7,6 +8,24 @@ from weco import __pkg_version__, __base_url__
|
|
|
7
8
|
from .utils import truncate_output
|
|
8
9
|
|
|
9
10
|
|
|
11
|
+
@dataclass
|
|
12
|
+
class RunSummary:
|
|
13
|
+
"""Brief run summary from execution task response."""
|
|
14
|
+
|
|
15
|
+
id: str
|
|
16
|
+
status: str
|
|
17
|
+
name: Optional[str] = None
|
|
18
|
+
require_review: bool = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ExecutionTasksResult:
|
|
23
|
+
"""Result from get_execution_tasks containing tasks and run info."""
|
|
24
|
+
|
|
25
|
+
tasks: list
|
|
26
|
+
run: Optional[RunSummary] = None
|
|
27
|
+
|
|
28
|
+
|
|
10
29
|
def handle_api_error(e: requests.exceptions.HTTPError, console: Console) -> None:
|
|
11
30
|
"""Extract and display error messages from API responses in a structured format."""
|
|
12
31
|
status = getattr(e.response, "status_code", None)
|
|
@@ -110,6 +129,7 @@ def start_optimization_run(
|
|
|
110
129
|
auth_headers: dict = {},
|
|
111
130
|
timeout: Union[int, Tuple[int, int]] = (10, 3650),
|
|
112
131
|
api_keys: Optional[Dict[str, str]] = None,
|
|
132
|
+
require_review: bool = False,
|
|
113
133
|
) -> Optional[Dict[str, Any]]:
|
|
114
134
|
"""Start the optimization run."""
|
|
115
135
|
with console.status("[bold green]Starting Optimization..."):
|
|
@@ -128,6 +148,7 @@ def start_optimization_run(
|
|
|
128
148
|
"eval_timeout": eval_timeout,
|
|
129
149
|
"save_logs": save_logs,
|
|
130
150
|
"log_dir": log_dir,
|
|
151
|
+
"require_review": require_review,
|
|
131
152
|
"metadata": {"client_name": "cli", "client_version": __pkg_version__},
|
|
132
153
|
}
|
|
133
154
|
if api_keys:
|
|
@@ -315,3 +336,107 @@ def report_termination(
|
|
|
315
336
|
except Exception as e:
|
|
316
337
|
print(f"Warning: Failed to report termination to backend for run {run_id}: {e}", file=sys.stderr)
|
|
317
338
|
return False
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# --- Execution Queue API Functions ---
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def get_execution_tasks(
|
|
345
|
+
run_id: str, auth_headers: dict = {}, timeout: Union[int, Tuple[int, int]] = (5, 30)
|
|
346
|
+
) -> Optional[ExecutionTasksResult]:
|
|
347
|
+
"""Poll for ready execution tasks.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
run_id: The run ID to get tasks for.
|
|
351
|
+
auth_headers: Authentication headers.
|
|
352
|
+
timeout: Request timeout.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
ExecutionTasksResult with tasks and run summary, or None if request failed.
|
|
356
|
+
"""
|
|
357
|
+
try:
|
|
358
|
+
response = requests.get(
|
|
359
|
+
f"{__base_url__}/execution-tasks/", params={"run_id": run_id}, headers=auth_headers, timeout=timeout
|
|
360
|
+
)
|
|
361
|
+
response.raise_for_status()
|
|
362
|
+
data = response.json()
|
|
363
|
+
|
|
364
|
+
# Extract run summary from top-level run field
|
|
365
|
+
run_summary = None
|
|
366
|
+
if data.get("run"):
|
|
367
|
+
run_data = data["run"]
|
|
368
|
+
run_summary = RunSummary(
|
|
369
|
+
id=run_data["id"],
|
|
370
|
+
status=run_data["status"],
|
|
371
|
+
name=run_data.get("name"),
|
|
372
|
+
require_review=run_data.get("require_review", False),
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
return ExecutionTasksResult(tasks=data.get("tasks", []), run=run_summary)
|
|
376
|
+
except requests.exceptions.HTTPError:
|
|
377
|
+
return None
|
|
378
|
+
except Exception:
|
|
379
|
+
return None
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def claim_execution_task(
|
|
383
|
+
task_id: str, auth_headers: dict = {}, timeout: Union[int, Tuple[int, int]] = (5, 30)
|
|
384
|
+
) -> Optional[Dict[str, Any]]:
|
|
385
|
+
"""Claim an execution task.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
task_id: The task ID to claim.
|
|
389
|
+
auth_headers: Authentication headers.
|
|
390
|
+
timeout: Request timeout.
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
The claimed task with revision, or None if already claimed or error.
|
|
394
|
+
"""
|
|
395
|
+
try:
|
|
396
|
+
response = requests.post(f"{__base_url__}/execution-tasks/{task_id}/claim", headers=auth_headers, timeout=timeout)
|
|
397
|
+
if response.status_code == 409:
|
|
398
|
+
return None # Already claimed
|
|
399
|
+
response.raise_for_status()
|
|
400
|
+
return response.json()
|
|
401
|
+
except requests.exceptions.HTTPError:
|
|
402
|
+
return None
|
|
403
|
+
except Exception:
|
|
404
|
+
return None
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def submit_execution_result(
|
|
408
|
+
run_id: str,
|
|
409
|
+
task_id: str,
|
|
410
|
+
execution_output: str,
|
|
411
|
+
auth_headers: dict = {},
|
|
412
|
+
timeout: Union[int, Tuple[int, int]] = (10, 3650),
|
|
413
|
+
api_keys: Optional[Dict[str, str]] = None,
|
|
414
|
+
) -> Optional[Dict[str, Any]]:
|
|
415
|
+
"""Submit execution result for a task.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
run_id: The run ID.
|
|
419
|
+
task_id: The task ID being completed.
|
|
420
|
+
execution_output: The execution output to submit.
|
|
421
|
+
auth_headers: Authentication headers.
|
|
422
|
+
timeout: Request timeout.
|
|
423
|
+
api_keys: Optional API keys for LLM providers.
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
The suggest response, or None if request failed.
|
|
427
|
+
"""
|
|
428
|
+
try:
|
|
429
|
+
truncated_output = truncate_output(execution_output)
|
|
430
|
+
request_json = {"execution_output": truncated_output, "task_id": task_id, "metadata": {}}
|
|
431
|
+
if api_keys:
|
|
432
|
+
request_json["api_keys"] = api_keys
|
|
433
|
+
|
|
434
|
+
response = requests.post(
|
|
435
|
+
f"{__base_url__}/runs/{run_id}/suggest", json=request_json, headers=auth_headers, timeout=timeout
|
|
436
|
+
)
|
|
437
|
+
response.raise_for_status()
|
|
438
|
+
return response.json()
|
|
439
|
+
except requests.exceptions.HTTPError:
|
|
440
|
+
return None
|
|
441
|
+
except Exception:
|
|
442
|
+
return None
|
weco/browser.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Cross-platform browser utilities."""
|
|
2
|
+
|
|
3
|
+
import webbrowser
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def open_browser(url: str) -> bool:
|
|
7
|
+
"""
|
|
8
|
+
Open a URL in the user's default web browser.
|
|
9
|
+
|
|
10
|
+
This function is cross-platform compatible (Windows, macOS, Linux).
|
|
11
|
+
It uses Python's built-in webbrowser module which handles platform
|
|
12
|
+
detection automatically.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
url: The URL to open in the browser.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
True if the browser was opened successfully, False otherwise.
|
|
19
|
+
"""
|
|
20
|
+
try:
|
|
21
|
+
# webbrowser.open() is cross-platform and uses the default browser
|
|
22
|
+
# - On macOS: uses 'open' command
|
|
23
|
+
# - On Windows: uses 'start' command
|
|
24
|
+
# - On Linux: tries common browsers (xdg-open, gnome-open, etc.)
|
|
25
|
+
return webbrowser.open(url)
|
|
26
|
+
except Exception:
|
|
27
|
+
# Silently fail - browser opening is a convenience feature
|
|
28
|
+
# and should not interrupt the optimization flow
|
|
29
|
+
return False
|
weco/cli.py
CHANGED
|
@@ -3,9 +3,10 @@ import sys
|
|
|
3
3
|
from rich.console import Console
|
|
4
4
|
from rich.traceback import install
|
|
5
5
|
|
|
6
|
-
from .auth import clear_api_key
|
|
6
|
+
from .auth import clear_api_key, perform_login, load_weco_api_key
|
|
7
7
|
from .constants import DEFAULT_MODELS
|
|
8
8
|
from .utils import check_for_cli_updates, get_default_model, UnrecognizedAPIKeysError, DefaultModelNotFoundError
|
|
9
|
+
from .validation import validate_source_file, validate_log_directory, ValidationError, print_validation_error
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
install(show_locals=True)
|
|
@@ -108,6 +109,11 @@ def configure_run_parser(run_parser: argparse.ArgumentParser) -> None:
|
|
|
108
109
|
action="store_true",
|
|
109
110
|
help="Automatically apply the best solution to the source file without prompting",
|
|
110
111
|
)
|
|
112
|
+
run_parser.add_argument(
|
|
113
|
+
"--require-review",
|
|
114
|
+
action="store_true",
|
|
115
|
+
help="Require manual review and approval of each proposed change before execution",
|
|
116
|
+
)
|
|
111
117
|
|
|
112
118
|
default_api_keys = " ".join([f"{provider}=xxx" for provider, _ in DEFAULT_MODELS])
|
|
113
119
|
supported_providers = ", ".join([provider for provider, _ in DEFAULT_MODELS])
|
|
@@ -206,7 +212,15 @@ Supported provider names: {supported_providers}.
|
|
|
206
212
|
|
|
207
213
|
def execute_run_command(args: argparse.Namespace) -> None:
|
|
208
214
|
"""Execute the 'weco run' command with all its logic."""
|
|
209
|
-
from .optimizer import
|
|
215
|
+
from .optimizer import optimize
|
|
216
|
+
|
|
217
|
+
# Early validation — fail fast with helpful errors
|
|
218
|
+
try:
|
|
219
|
+
validate_source_file(args.source)
|
|
220
|
+
validate_log_directory(args.log_dir)
|
|
221
|
+
except ValidationError as e:
|
|
222
|
+
print_validation_error(e, console)
|
|
223
|
+
sys.exit(1)
|
|
210
224
|
|
|
211
225
|
try:
|
|
212
226
|
api_keys = parse_api_keys(args.api_key)
|
|
@@ -225,7 +239,7 @@ def execute_run_command(args: argparse.Namespace) -> None:
|
|
|
225
239
|
if api_keys:
|
|
226
240
|
console.print(f"[bold yellow]Custom API keys provided. Using default model: {model} for the run.[/]")
|
|
227
241
|
|
|
228
|
-
success =
|
|
242
|
+
success = optimize(
|
|
229
243
|
source=args.source,
|
|
230
244
|
eval_command=args.eval_command,
|
|
231
245
|
metric=args.metric,
|
|
@@ -234,12 +248,13 @@ def execute_run_command(args: argparse.Namespace) -> None:
|
|
|
234
248
|
steps=args.steps,
|
|
235
249
|
log_dir=args.log_dir,
|
|
236
250
|
additional_instructions=args.additional_instructions,
|
|
237
|
-
console=console,
|
|
238
251
|
eval_timeout=args.eval_timeout,
|
|
239
252
|
save_logs=args.save_logs,
|
|
240
|
-
apply_change=args.apply_change,
|
|
241
253
|
api_keys=api_keys,
|
|
254
|
+
apply_change=args.apply_change,
|
|
255
|
+
require_review=args.require_review,
|
|
242
256
|
)
|
|
257
|
+
|
|
243
258
|
exit_code = 0 if success else 1
|
|
244
259
|
sys.exit(exit_code)
|
|
245
260
|
|
|
@@ -254,12 +269,23 @@ def execute_resume_command(args: argparse.Namespace) -> None:
|
|
|
254
269
|
console.print(f"[bold red]Error parsing API keys: {e}[/]")
|
|
255
270
|
sys.exit(1)
|
|
256
271
|
|
|
257
|
-
success = resume_optimization(run_id=args.run_id,
|
|
272
|
+
success = resume_optimization(run_id=args.run_id, api_keys=api_keys, apply_change=args.apply_change)
|
|
273
|
+
|
|
258
274
|
sys.exit(0 if success else 1)
|
|
259
275
|
|
|
260
276
|
|
|
261
277
|
def main() -> None:
|
|
262
278
|
"""Main function for the Weco CLI."""
|
|
279
|
+
try:
|
|
280
|
+
_main()
|
|
281
|
+
except KeyboardInterrupt:
|
|
282
|
+
# Clean exit on Ctrl+C without traceback
|
|
283
|
+
console.print("\n[yellow]Interrupted.[/]")
|
|
284
|
+
sys.exit(130) # Standard exit code for SIGINT
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _main() -> None:
|
|
288
|
+
"""Internal main function containing the CLI logic."""
|
|
263
289
|
check_for_cli_updates()
|
|
264
290
|
|
|
265
291
|
parser = argparse.ArgumentParser(
|
|
@@ -277,6 +303,9 @@ def main() -> None:
|
|
|
277
303
|
)
|
|
278
304
|
configure_run_parser(run_parser) # Use the helper to add arguments
|
|
279
305
|
|
|
306
|
+
# --- Login Command Parser Setup ---
|
|
307
|
+
_ = subparsers.add_parser("login", help="Log in to Weco and save your API key.")
|
|
308
|
+
|
|
280
309
|
# --- Logout Command Parser Setup ---
|
|
281
310
|
_ = subparsers.add_parser("logout", help="Log out from Weco and clear saved API key.")
|
|
282
311
|
|
|
@@ -295,7 +324,20 @@ def main() -> None:
|
|
|
295
324
|
|
|
296
325
|
args = parser.parse_args()
|
|
297
326
|
|
|
298
|
-
if args.command == "
|
|
327
|
+
if args.command == "login":
|
|
328
|
+
# Check if already logged in
|
|
329
|
+
existing_key = load_weco_api_key()
|
|
330
|
+
if existing_key:
|
|
331
|
+
console.print("[bold green]You are already logged in.[/]")
|
|
332
|
+
console.print("[dim]Use 'weco logout' to log out first if you want to switch accounts.[/]")
|
|
333
|
+
sys.exit(0)
|
|
334
|
+
|
|
335
|
+
# Perform the login flow
|
|
336
|
+
if perform_login(console):
|
|
337
|
+
sys.exit(0)
|
|
338
|
+
else:
|
|
339
|
+
sys.exit(1)
|
|
340
|
+
elif args.command == "logout":
|
|
299
341
|
clear_api_key()
|
|
300
342
|
sys.exit(0)
|
|
301
343
|
elif args.command == "run":
|