weco 0.3.7__py3-none-any.whl → 0.3.9__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 +81 -7
- weco/optimizer.py +535 -815
- weco/setup.py +192 -0
- weco/ui.py +421 -0
- weco/validation.py +112 -0
- {weco-0.3.7.dist-info → weco-0.3.9.dist-info}/METADATA +20 -2
- weco-0.3.9.dist-info/RECORD +19 -0
- {weco-0.3.7.dist-info → weco-0.3.9.dist-info}/WHEEL +1 -1
- weco-0.3.7.dist-info/RECORD +0 -15
- {weco-0.3.7.dist-info → weco-0.3.9.dist-info}/entry_points.txt +0 -0
- {weco-0.3.7.dist-info → weco-0.3.9.dist-info}/licenses/LICENSE +0 -0
- {weco-0.3.7.dist-info → weco-0.3.9.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])
|
|
@@ -131,6 +137,13 @@ Default models for providers:
|
|
|
131
137
|
{default_models_for_providers}
|
|
132
138
|
""",
|
|
133
139
|
)
|
|
140
|
+
run_parser.add_argument(
|
|
141
|
+
"--output",
|
|
142
|
+
type=str,
|
|
143
|
+
choices=["rich", "plain"],
|
|
144
|
+
default="rich",
|
|
145
|
+
help="Output mode: 'rich' for interactive terminal UI (default), 'plain' for machine-readable text output suitable for LLM agents.",
|
|
146
|
+
)
|
|
134
147
|
|
|
135
148
|
|
|
136
149
|
def configure_credits_parser(credits_parser: argparse.ArgumentParser) -> None:
|
|
@@ -172,6 +185,12 @@ def configure_credits_parser(credits_parser: argparse.ArgumentParser) -> None:
|
|
|
172
185
|
)
|
|
173
186
|
|
|
174
187
|
|
|
188
|
+
def configure_setup_parser(setup_parser: argparse.ArgumentParser) -> None:
|
|
189
|
+
"""Configure the setup command parser and its subcommands."""
|
|
190
|
+
setup_subparsers = setup_parser.add_subparsers(dest="tool", help="AI tool to set up")
|
|
191
|
+
setup_subparsers.add_parser("claude-code", help="Set up Weco skill for Claude Code")
|
|
192
|
+
|
|
193
|
+
|
|
175
194
|
def configure_resume_parser(resume_parser: argparse.ArgumentParser) -> None:
|
|
176
195
|
"""Configure arguments for the resume command."""
|
|
177
196
|
resume_parser.add_argument(
|
|
@@ -202,11 +221,26 @@ Example:
|
|
|
202
221
|
Supported provider names: {supported_providers}.
|
|
203
222
|
""",
|
|
204
223
|
)
|
|
224
|
+
resume_parser.add_argument(
|
|
225
|
+
"--output",
|
|
226
|
+
type=str,
|
|
227
|
+
choices=["rich", "plain"],
|
|
228
|
+
default="rich",
|
|
229
|
+
help="Output mode: 'rich' for interactive terminal UI (default), 'plain' for machine-readable text output suitable for LLM agents.",
|
|
230
|
+
)
|
|
205
231
|
|
|
206
232
|
|
|
207
233
|
def execute_run_command(args: argparse.Namespace) -> None:
|
|
208
234
|
"""Execute the 'weco run' command with all its logic."""
|
|
209
|
-
from .optimizer import
|
|
235
|
+
from .optimizer import optimize
|
|
236
|
+
|
|
237
|
+
# Early validation — fail fast with helpful errors
|
|
238
|
+
try:
|
|
239
|
+
validate_source_file(args.source)
|
|
240
|
+
validate_log_directory(args.log_dir)
|
|
241
|
+
except ValidationError as e:
|
|
242
|
+
print_validation_error(e, console)
|
|
243
|
+
sys.exit(1)
|
|
210
244
|
|
|
211
245
|
try:
|
|
212
246
|
api_keys = parse_api_keys(args.api_key)
|
|
@@ -225,7 +259,7 @@ def execute_run_command(args: argparse.Namespace) -> None:
|
|
|
225
259
|
if api_keys:
|
|
226
260
|
console.print(f"[bold yellow]Custom API keys provided. Using default model: {model} for the run.[/]")
|
|
227
261
|
|
|
228
|
-
success =
|
|
262
|
+
success = optimize(
|
|
229
263
|
source=args.source,
|
|
230
264
|
eval_command=args.eval_command,
|
|
231
265
|
metric=args.metric,
|
|
@@ -234,12 +268,14 @@ def execute_run_command(args: argparse.Namespace) -> None:
|
|
|
234
268
|
steps=args.steps,
|
|
235
269
|
log_dir=args.log_dir,
|
|
236
270
|
additional_instructions=args.additional_instructions,
|
|
237
|
-
console=console,
|
|
238
271
|
eval_timeout=args.eval_timeout,
|
|
239
272
|
save_logs=args.save_logs,
|
|
240
|
-
apply_change=args.apply_change,
|
|
241
273
|
api_keys=api_keys,
|
|
274
|
+
apply_change=args.apply_change,
|
|
275
|
+
require_review=args.require_review,
|
|
276
|
+
output_mode=args.output,
|
|
242
277
|
)
|
|
278
|
+
|
|
243
279
|
exit_code = 0 if success else 1
|
|
244
280
|
sys.exit(exit_code)
|
|
245
281
|
|
|
@@ -254,12 +290,25 @@ def execute_resume_command(args: argparse.Namespace) -> None:
|
|
|
254
290
|
console.print(f"[bold red]Error parsing API keys: {e}[/]")
|
|
255
291
|
sys.exit(1)
|
|
256
292
|
|
|
257
|
-
success = resume_optimization(
|
|
293
|
+
success = resume_optimization(
|
|
294
|
+
run_id=args.run_id, api_keys=api_keys, apply_change=args.apply_change, output_mode=args.output
|
|
295
|
+
)
|
|
296
|
+
|
|
258
297
|
sys.exit(0 if success else 1)
|
|
259
298
|
|
|
260
299
|
|
|
261
300
|
def main() -> None:
|
|
262
301
|
"""Main function for the Weco CLI."""
|
|
302
|
+
try:
|
|
303
|
+
_main()
|
|
304
|
+
except KeyboardInterrupt:
|
|
305
|
+
# Clean exit on Ctrl+C without traceback
|
|
306
|
+
console.print("\n[yellow]Interrupted.[/]")
|
|
307
|
+
sys.exit(130) # Standard exit code for SIGINT
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _main() -> None:
|
|
311
|
+
"""Internal main function containing the CLI logic."""
|
|
263
312
|
check_for_cli_updates()
|
|
264
313
|
|
|
265
314
|
parser = argparse.ArgumentParser(
|
|
@@ -277,6 +326,9 @@ def main() -> None:
|
|
|
277
326
|
)
|
|
278
327
|
configure_run_parser(run_parser) # Use the helper to add arguments
|
|
279
328
|
|
|
329
|
+
# --- Login Command Parser Setup ---
|
|
330
|
+
_ = subparsers.add_parser("login", help="Log in to Weco and save your API key.")
|
|
331
|
+
|
|
280
332
|
# --- Logout Command Parser Setup ---
|
|
281
333
|
_ = subparsers.add_parser("logout", help="Log out from Weco and clear saved API key.")
|
|
282
334
|
|
|
@@ -293,9 +345,26 @@ def main() -> None:
|
|
|
293
345
|
)
|
|
294
346
|
configure_resume_parser(resume_parser)
|
|
295
347
|
|
|
348
|
+
# --- Setup Command Parser Setup ---
|
|
349
|
+
setup_parser = subparsers.add_parser("setup", help="Set up Weco for use with AI tools")
|
|
350
|
+
configure_setup_parser(setup_parser)
|
|
351
|
+
|
|
296
352
|
args = parser.parse_args()
|
|
297
353
|
|
|
298
|
-
if args.command == "
|
|
354
|
+
if args.command == "login":
|
|
355
|
+
# Check if already logged in
|
|
356
|
+
existing_key = load_weco_api_key()
|
|
357
|
+
if existing_key:
|
|
358
|
+
console.print("[bold green]You are already logged in.[/]")
|
|
359
|
+
console.print("[dim]Use 'weco logout' to log out first if you want to switch accounts.[/]")
|
|
360
|
+
sys.exit(0)
|
|
361
|
+
|
|
362
|
+
# Perform the login flow
|
|
363
|
+
if perform_login(console):
|
|
364
|
+
sys.exit(0)
|
|
365
|
+
else:
|
|
366
|
+
sys.exit(1)
|
|
367
|
+
elif args.command == "logout":
|
|
299
368
|
clear_api_key()
|
|
300
369
|
sys.exit(0)
|
|
301
370
|
elif args.command == "run":
|
|
@@ -307,6 +376,11 @@ def main() -> None:
|
|
|
307
376
|
sys.exit(0)
|
|
308
377
|
elif args.command == "resume":
|
|
309
378
|
execute_resume_command(args)
|
|
379
|
+
elif args.command == "setup":
|
|
380
|
+
from .setup import handle_setup_command
|
|
381
|
+
|
|
382
|
+
handle_setup_command(args, console)
|
|
383
|
+
sys.exit(0)
|
|
310
384
|
else:
|
|
311
385
|
# This case should be hit if 'weco' is run alone and chatbot logic didn't catch it,
|
|
312
386
|
# or if an invalid command is provided.
|