Skip to content

Commit 3d65557

Browse files
authored
Merge pull request #103 from WecoAI/features/require-review
Features/require review
2 parents 669d987 + 48740bb commit 3d65557

File tree

6 files changed

+1140
-824
lines changed

6 files changed

+1140
-824
lines changed

weco/api.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import sys
2+
from dataclasses import dataclass
23
from typing import Dict, Any, Optional, Union, Tuple
34
import requests
45
from rich.console import Console
@@ -7,6 +8,24 @@
78
from .utils import truncate_output
89

910

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+
1029
def handle_api_error(e: requests.exceptions.HTTPError, console: Console) -> None:
1130
"""Extract and display error messages from API responses in a structured format."""
1231
status = getattr(e.response, "status_code", None)
@@ -110,6 +129,7 @@ def start_optimization_run(
110129
auth_headers: dict = {},
111130
timeout: Union[int, Tuple[int, int]] = (10, 3650),
112131
api_keys: Optional[Dict[str, str]] = None,
132+
require_review: bool = False,
113133
) -> Optional[Dict[str, Any]]:
114134
"""Start the optimization run."""
115135
with console.status("[bold green]Starting Optimization..."):
@@ -128,6 +148,7 @@ def start_optimization_run(
128148
"eval_timeout": eval_timeout,
129149
"save_logs": save_logs,
130150
"log_dir": log_dir,
151+
"require_review": require_review,
131152
"metadata": {"client_name": "cli", "client_version": __pkg_version__},
132153
}
133154
if api_keys:
@@ -315,3 +336,107 @@ def report_termination(
315336
except Exception as e:
316337
print(f"Warning: Failed to report termination to backend for run {run_id}: {e}", file=sys.stderr)
317338
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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
from rich.console import Console
44
from rich.traceback import install
55

6-
from .auth import clear_api_key
6+
from .auth import clear_api_key, perform_login, load_weco_api_key
77
from .constants import DEFAULT_MODELS
88
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
910

1011

1112
install(show_locals=True)
@@ -108,6 +109,11 @@ def configure_run_parser(run_parser: argparse.ArgumentParser) -> None:
108109
action="store_true",
109110
help="Automatically apply the best solution to the source file without prompting",
110111
)
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+
)
111117

112118
default_api_keys = " ".join([f"{provider}=xxx" for provider, _ in DEFAULT_MODELS])
113119
supported_providers = ", ".join([provider for provider, _ in DEFAULT_MODELS])
@@ -206,7 +212,15 @@ def configure_resume_parser(resume_parser: argparse.ArgumentParser) -> None:
206212

207213
def execute_run_command(args: argparse.Namespace) -> None:
208214
"""Execute the 'weco run' command with all its logic."""
209-
from .optimizer import execute_optimization
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)
210224

211225
try:
212226
api_keys = parse_api_keys(args.api_key)
@@ -225,7 +239,7 @@ def execute_run_command(args: argparse.Namespace) -> None:
225239
if api_keys:
226240
console.print(f"[bold yellow]Custom API keys provided. Using default model: {model} for the run.[/]")
227241

228-
success = execute_optimization(
242+
success = optimize(
229243
source=args.source,
230244
eval_command=args.eval_command,
231245
metric=args.metric,
@@ -234,12 +248,13 @@ def execute_run_command(args: argparse.Namespace) -> None:
234248
steps=args.steps,
235249
log_dir=args.log_dir,
236250
additional_instructions=args.additional_instructions,
237-
console=console,
238251
eval_timeout=args.eval_timeout,
239252
save_logs=args.save_logs,
240-
apply_change=args.apply_change,
241253
api_keys=api_keys,
254+
apply_change=args.apply_change,
255+
require_review=args.require_review,
242256
)
257+
243258
exit_code = 0 if success else 1
244259
sys.exit(exit_code)
245260

@@ -254,12 +269,23 @@ def execute_resume_command(args: argparse.Namespace) -> None:
254269
console.print(f"[bold red]Error parsing API keys: {e}[/]")
255270
sys.exit(1)
256271

257-
success = resume_optimization(run_id=args.run_id, console=console, api_keys=api_keys, apply_change=args.apply_change)
272+
success = resume_optimization(run_id=args.run_id, api_keys=api_keys, apply_change=args.apply_change)
273+
258274
sys.exit(0 if success else 1)
259275

260276

261277
def main() -> None:
262278
"""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."""
263289
check_for_cli_updates()
264290

265291
parser = argparse.ArgumentParser(
@@ -277,6 +303,9 @@ def main() -> None:
277303
)
278304
configure_run_parser(run_parser) # Use the helper to add arguments
279305

306+
# --- Login Command Parser Setup ---
307+
_ = subparsers.add_parser("login", help="Log in to Weco and save your API key.")
308+
280309
# --- Logout Command Parser Setup ---
281310
_ = subparsers.add_parser("logout", help="Log out from Weco and clear saved API key.")
282311

@@ -295,7 +324,20 @@ def main() -> None:
295324

296325
args = parser.parse_args()
297326

298-
if args.command == "logout":
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":
299341
clear_api_key()
300342
sys.exit(0)
301343
elif args.command == "run":

0 commit comments

Comments
 (0)