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/__init__.py CHANGED
@@ -5,11 +5,5 @@ import importlib.metadata
5
5
  __pkg_version__ = importlib.metadata.version("weco")
6
6
  __api_version__ = "v1"
7
7
 
8
- __base_url__ = f"https://api.weco.ai/{__api_version__}"
9
- # If user specifies a custom base URL, use that instead
10
- if os.environ.get("WECO_BASE_URL"):
11
- __base_url__ = os.environ.get("WECO_BASE_URL")
12
-
13
- __dashboard_url__ = "https://dashboard.weco.ai"
14
- if os.environ.get("WECO_DASHBOARD_URL"):
15
- __dashboard_url__ = os.environ.get("WECO_DASHBOARD_URL")
8
+ __base_url__ = os.environ.get("WECO_BASE_URL", f"https://api.weco.ai/{__api_version__}")
9
+ __dashboard_url__ = os.environ.get("WECO_DASHBOARD_URL", "https://dashboard.weco.ai")
weco/api.py CHANGED
@@ -1,12 +1,12 @@
1
- from typing import Dict, Any, Optional
2
- import rich
3
- import requests
4
- from weco import __pkg_version__, __base_url__
5
1
  import sys
2
+ from typing import Dict, Any, Optional, Union, Tuple, List
3
+ import requests
6
4
  from rich.console import Console
7
5
 
6
+ from weco import __pkg_version__, __base_url__
7
+
8
8
 
9
- def handle_api_error(e: requests.exceptions.HTTPError, console: rich.console.Console) -> None:
9
+ def handle_api_error(e: requests.exceptions.HTTPError, console: Console) -> None:
10
10
  """Extract and display error messages from API responses in a structured format."""
11
11
  try:
12
12
  detail = e.response.json()["detail"]
@@ -18,7 +18,7 @@ def handle_api_error(e: requests.exceptions.HTTPError, console: rich.console.Con
18
18
 
19
19
 
20
20
  def start_optimization_run(
21
- console: rich.console.Console,
21
+ console: Console,
22
22
  source_code: str,
23
23
  evaluation_command: str,
24
24
  metric_name: str,
@@ -30,7 +30,7 @@ def start_optimization_run(
30
30
  additional_instructions: str = None,
31
31
  api_keys: Dict[str, Any] = {},
32
32
  auth_headers: dict = {},
33
- timeout: int = 800,
33
+ timeout: Union[int, Tuple[int, int]] = 800,
34
34
  ) -> Dict[str, Any]:
35
35
  """Start the optimization run."""
36
36
  with console.status("[bold green]Starting Optimization..."):
@@ -57,8 +57,8 @@ def start_optimization_run(
57
57
  except requests.exceptions.HTTPError as e:
58
58
  handle_api_error(e, console)
59
59
  sys.exit(1)
60
- except requests.exceptions.RequestException as e:
61
- console.print(f"[bold red]Network Error starting run: {e}[/]")
60
+ except Exception as e:
61
+ console.print(f"[bold red]Error starting run: {e}[/]")
62
62
  sys.exit(1)
63
63
 
64
64
 
@@ -68,7 +68,7 @@ def evaluate_feedback_then_suggest_next_solution(
68
68
  additional_instructions: str = None,
69
69
  api_keys: Dict[str, Any] = {},
70
70
  auth_headers: dict = {},
71
- timeout: int = 800,
71
+ timeout: Union[int, Tuple[int, int]] = 800,
72
72
  ) -> Dict[str, Any]:
73
73
  """Evaluate the feedback and suggest the next solution."""
74
74
  try:
@@ -88,13 +88,13 @@ def evaluate_feedback_then_suggest_next_solution(
88
88
  # Allow caller to handle suggest errors, maybe retry or terminate
89
89
  handle_api_error(e, Console()) # Use default console if none passed
90
90
  raise # Re-raise the exception
91
- except requests.exceptions.RequestException as e:
92
- print(f"Network Error during suggest: {e}") # Use print as console might not be available
91
+ except Exception as e:
92
+ print(f"Error: {e}") # Use print as console might not be available
93
93
  raise # Re-raise the exception
94
94
 
95
95
 
96
96
  def get_optimization_run_status(
97
- run_id: str, include_history: bool = False, auth_headers: dict = {}, timeout: int = 800
97
+ run_id: str, include_history: bool = False, auth_headers: dict = {}, timeout: Union[int, Tuple[int, int]] = 800
98
98
  ) -> Dict[str, Any]:
99
99
  """Get the current status of the optimization run."""
100
100
  try:
@@ -106,12 +106,12 @@ def get_optimization_run_status(
106
106
  except requests.exceptions.HTTPError as e:
107
107
  handle_api_error(e, Console()) # Use default console
108
108
  raise # Re-raise
109
- except requests.exceptions.RequestException as e:
110
- print(f"Network Error getting status: {e}")
109
+ except Exception as e:
110
+ print(f"Error getting run status: {e}")
111
111
  raise # Re-raise
112
112
 
113
113
 
114
- def send_heartbeat(run_id: str, auth_headers: dict = {}, timeout: int = 10) -> bool:
114
+ def send_heartbeat(run_id: str, auth_headers: dict = {}, timeout: Union[int, Tuple[int, int]] = 10) -> bool:
115
115
  """Send a heartbeat signal to the backend."""
116
116
  try:
117
117
  response = requests.put(f"{__base_url__}/runs/{run_id}/heartbeat", headers=auth_headers, timeout=timeout)
@@ -123,13 +123,18 @@ def send_heartbeat(run_id: str, auth_headers: dict = {}, timeout: int = 10) -> b
123
123
  else:
124
124
  print(f"Heartbeat failed for run {run_id}: HTTP {e.response.status_code}", file=sys.stderr)
125
125
  return False
126
- except requests.exceptions.RequestException as e:
127
- print(f"Heartbeat network error for run {run_id}: {e}", file=sys.stderr)
126
+ except Exception as e:
127
+ print(f"Error sending heartbeat for run {run_id}: {e}", file=sys.stderr)
128
128
  return False
129
129
 
130
130
 
131
131
  def report_termination(
132
- run_id: str, status_update: str, reason: str, details: Optional[str] = None, auth_headers: dict = {}, timeout: int = 30
132
+ run_id: str,
133
+ status_update: str,
134
+ reason: str,
135
+ details: Optional[str] = None,
136
+ auth_headers: dict = {},
137
+ timeout: Union[int, Tuple[int, int]] = 30,
133
138
  ) -> bool:
134
139
  """Report the termination reason to the backend."""
135
140
  try:
@@ -141,6 +146,174 @@ def report_termination(
141
146
  )
142
147
  response.raise_for_status()
143
148
  return True
144
- except requests.exceptions.RequestException as e:
149
+ except Exception as e:
145
150
  print(f"Warning: Failed to report termination to backend for run {run_id}: {e}", file=sys.stderr)
146
151
  return False
152
+
153
+
154
+ # --- Chatbot API Functions ---
155
+ def _determine_model_and_api_key() -> tuple[str, dict[str, str]]:
156
+ """Determine the model and API key to use based on available environment variables.
157
+
158
+ Uses the shared model selection logic to maintain consistency.
159
+ Returns (model_name, api_key_dict)
160
+ """
161
+ from .utils import read_api_keys_from_env, determine_default_model
162
+
163
+ llm_api_keys = read_api_keys_from_env()
164
+ model = determine_default_model(llm_api_keys)
165
+
166
+ # Create API key dictionary with only the key for the selected model
167
+ if model == "o4-mini":
168
+ api_key_dict = {"OPENAI_API_KEY": llm_api_keys["OPENAI_API_KEY"]}
169
+ elif model == "claude-sonnet-4-0":
170
+ api_key_dict = {"ANTHROPIC_API_KEY": llm_api_keys["ANTHROPIC_API_KEY"]}
171
+ elif model == "gemini-2.5-pro":
172
+ api_key_dict = {"GEMINI_API_KEY": llm_api_keys["GEMINI_API_KEY"]}
173
+ else:
174
+ # This should never happen if determine_default_model works correctly
175
+ raise ValueError(f"Unknown model returned: {model}")
176
+
177
+ return model, api_key_dict
178
+
179
+
180
+ def get_optimization_suggestions_from_codebase(
181
+ gitingest_summary: str,
182
+ gitingest_tree: str,
183
+ gitingest_content_str: str,
184
+ console: Console,
185
+ auth_headers: dict = {},
186
+ timeout: Union[int, Tuple[int, int]] = 800,
187
+ ) -> Optional[List[Dict[str, Any]]]:
188
+ """Analyze codebase and get optimization suggestions using the model-agnostic backend API."""
189
+ try:
190
+ model, api_key_dict = _determine_model_and_api_key()
191
+ response = requests.post(
192
+ f"{__base_url__}/onboard/analyze-codebase",
193
+ json={
194
+ "gitingest_summary": gitingest_summary,
195
+ "gitingest_tree": gitingest_tree,
196
+ "gitingest_content": gitingest_content_str,
197
+ "model": model,
198
+ "metadata": api_key_dict,
199
+ },
200
+ headers=auth_headers,
201
+ timeout=timeout,
202
+ )
203
+ response.raise_for_status()
204
+ result = response.json()
205
+ return [option for option in result.get("options", [])]
206
+
207
+ except requests.exceptions.HTTPError as e:
208
+ handle_api_error(e, console)
209
+ return None
210
+ except Exception as e:
211
+ console.print(f"[bold red]Error: {e}[/]")
212
+ return None
213
+
214
+
215
+ def generate_evaluation_script_and_metrics(
216
+ target_file: str,
217
+ description: str,
218
+ gitingest_content_str: str,
219
+ console: Console,
220
+ auth_headers: dict = {},
221
+ timeout: Union[int, Tuple[int, int]] = 800,
222
+ ) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str]]:
223
+ """Generate evaluation script and determine metrics using the model-agnostic backend API."""
224
+ try:
225
+ model, api_key_dict = _determine_model_and_api_key()
226
+ response = requests.post(
227
+ f"{__base_url__}/onboard/generate-script",
228
+ json={
229
+ "target_file": target_file,
230
+ "description": description,
231
+ "gitingest_content": gitingest_content_str,
232
+ "model": model,
233
+ "metadata": api_key_dict,
234
+ },
235
+ headers=auth_headers,
236
+ timeout=timeout,
237
+ )
238
+ response.raise_for_status()
239
+ result = response.json()
240
+ return result.get("script_content"), result.get("metric_name"), result.get("goal"), result.get("reasoning")
241
+ except requests.exceptions.HTTPError as e:
242
+ handle_api_error(e, console)
243
+ return None, None, None, None
244
+ except Exception as e:
245
+ console.print(f"[bold red]Error: {e}[/]")
246
+ return None, None, None, None
247
+
248
+
249
+ def analyze_evaluation_environment(
250
+ target_file: str,
251
+ description: str,
252
+ gitingest_summary: str,
253
+ gitingest_tree: str,
254
+ gitingest_content_str: str,
255
+ console: Console,
256
+ auth_headers: dict = {},
257
+ timeout: Union[int, Tuple[int, int]] = 800,
258
+ ) -> Optional[Dict[str, Any]]:
259
+ """Analyze existing evaluation scripts and environment using the model-agnostic backend API."""
260
+ try:
261
+ model, api_key_dict = _determine_model_and_api_key()
262
+ response = requests.post(
263
+ f"{__base_url__}/onboard/analyze-environment",
264
+ json={
265
+ "target_file": target_file,
266
+ "description": description,
267
+ "gitingest_summary": gitingest_summary,
268
+ "gitingest_tree": gitingest_tree,
269
+ "gitingest_content": gitingest_content_str,
270
+ "model": model,
271
+ "metadata": api_key_dict,
272
+ },
273
+ headers=auth_headers,
274
+ timeout=timeout,
275
+ )
276
+ response.raise_for_status()
277
+ return response.json()
278
+
279
+ except requests.exceptions.HTTPError as e:
280
+ handle_api_error(e, console)
281
+ return None
282
+ except Exception as e:
283
+ console.print(f"[bold red]Error: {e}[/]")
284
+ return None
285
+
286
+
287
+ def analyze_script_execution_requirements(
288
+ script_content: str,
289
+ script_path: str,
290
+ target_file: str,
291
+ console: Console,
292
+ auth_headers: dict = {},
293
+ timeout: Union[int, Tuple[int, int]] = 800,
294
+ ) -> Optional[str]:
295
+ """Analyze script to determine proper execution command using the model-agnostic backend API."""
296
+ try:
297
+ model, api_key_dict = _determine_model_and_api_key()
298
+ response = requests.post(
299
+ f"{__base_url__}/onboard/analyze-script",
300
+ json={
301
+ "script_content": script_content,
302
+ "script_path": script_path,
303
+ "target_file": target_file,
304
+ "model": model,
305
+ "metadata": api_key_dict,
306
+ },
307
+ headers=auth_headers,
308
+ timeout=timeout,
309
+ )
310
+ response.raise_for_status()
311
+ result = response.json()
312
+ return result.get("command", f"python {script_path}")
313
+
314
+ except requests.exceptions.HTTPError as e:
315
+ handle_api_error(e, console)
316
+ return f"python {script_path}"
317
+ except Exception as e:
318
+ console.print(f"[bold red]Error: {e}[/]")
319
+ return f"python {script_path}"
weco/auth.py CHANGED
@@ -3,6 +3,13 @@ import os
3
3
  import pathlib
4
4
  import json
5
5
  import stat
6
+ import time
7
+ import requests
8
+ import webbrowser
9
+ from rich.console import Console
10
+ from rich.live import Live
11
+ from rich.prompt import Prompt
12
+ from . import __base_url__
6
13
 
7
14
  CONFIG_DIR = pathlib.Path.home() / ".config" / "weco"
8
15
  CREDENTIALS_FILE = CONFIG_DIR / "credentials.json"
@@ -27,10 +34,8 @@ def save_api_key(api_key: str):
27
34
  json.dump(credentials, f)
28
35
  # Set file permissions to read/write for owner only (600)
29
36
  os.chmod(CREDENTIALS_FILE, stat.S_IRUSR | stat.S_IWUSR)
30
- except IOError as e:
31
- print(f"Error: Could not write credentials file at {CREDENTIALS_FILE}: {e}")
32
37
  except OSError as e:
33
- print(f"Warning: Could not set permissions on {CREDENTIALS_FILE}: {e}")
38
+ print(f"Error: Could not write credentials file or set permissions on {CREDENTIALS_FILE}: {e}")
34
39
 
35
40
 
36
41
  def load_weco_api_key() -> str | None:
@@ -62,3 +67,159 @@ def clear_api_key():
62
67
  print(f"Error: Could not remove credentials file at {CREDENTIALS_FILE}: {e}")
63
68
  else:
64
69
  print("Already logged out.")
70
+
71
+
72
+ def perform_login(console: Console):
73
+ """Handles the device login flow."""
74
+ try:
75
+ # 1. Initiate device login
76
+ console.print("Initiating login...")
77
+ init_response = requests.post(f"{__base_url__}/auth/device/initiate")
78
+ init_response.raise_for_status()
79
+ init_data = init_response.json()
80
+
81
+ device_code = init_data["device_code"]
82
+ verification_uri = init_data["verification_uri"]
83
+ expires_in = init_data["expires_in"]
84
+ interval = init_data["interval"]
85
+
86
+ # 2. Display instructions
87
+ console.print("\n[bold yellow]Action Required:[/]")
88
+ console.print("Please open the following URL in your browser to authenticate:")
89
+ console.print(f"[link={verification_uri}]{verification_uri}[/link]")
90
+ console.print(f"This request will expire in {expires_in // 60} minutes.")
91
+ console.print("Attempting to open the authentication page in your default browser...") # Notify user
92
+
93
+ # Automatically open the browser
94
+ try:
95
+ if not webbrowser.open(verification_uri):
96
+ console.print("[yellow]Could not automatically open the browser. Please open the link manually.[/]")
97
+ except Exception as browser_err:
98
+ console.print(
99
+ f"[yellow]Could not automatically open the browser ({browser_err}). Please open the link manually.[/]"
100
+ )
101
+
102
+ console.print("Waiting for authentication...", end="")
103
+
104
+ # 3. Poll for token
105
+ start_time = time.time()
106
+ # Use a simple text update instead of Spinner within Live for potentially better compatibility
107
+ polling_status = "Waiting..."
108
+ with Live(polling_status, refresh_per_second=1, transient=True, console=console) as live_status:
109
+ while True:
110
+ # Check for timeout
111
+ if time.time() - start_time > expires_in:
112
+ console.print("\n[bold red]Error:[/] Login request timed out.")
113
+ return False
114
+
115
+ time.sleep(interval)
116
+ live_status.update("Waiting... (checking status)")
117
+
118
+ try:
119
+ token_response = requests.post(
120
+ f"{__base_url__}/auth/device/token",
121
+ json={"grant_type": "urn:ietf:params:oauth:grant-type:device_code", "device_code": device_code},
122
+ )
123
+
124
+ # Check for 202 Accepted - Authorization Pending
125
+ if token_response.status_code == 202:
126
+ token_data = token_response.json()
127
+ if token_data.get("error") == "authorization_pending":
128
+ live_status.update("Waiting... (authorization pending)")
129
+ continue # Continue polling
130
+ else:
131
+ # Unexpected 202 response format
132
+ console.print(f"\n[bold red]Error:[/] Received unexpected 202 response: {token_data}")
133
+ return False
134
+ # Check for standard OAuth2 errors (often 400 Bad Request)
135
+ elif token_response.status_code == 400:
136
+ token_data = token_response.json()
137
+ error_code = token_data.get("error", "unknown_error")
138
+ if error_code == "slow_down":
139
+ interval += 5 # Increase polling interval if instructed
140
+ live_status.update(f"Waiting... (slowing down polling to {interval}s)")
141
+ continue
142
+ elif error_code == "expired_token":
143
+ console.print("\n[bold red]Error:[/] Login request expired.")
144
+ return False
145
+ elif error_code == "access_denied":
146
+ console.print("\n[bold red]Error:[/] Authorization denied by user.")
147
+ return False
148
+ else: # invalid_grant, etc.
149
+ error_desc = token_data.get("error_description", "Unknown error during polling.")
150
+ console.print(f"\n[bold red]Error:[/] {error_desc} ({error_code})")
151
+ return False
152
+
153
+ # Check for other non-200/non-202/non-400 HTTP errors
154
+ token_response.raise_for_status()
155
+ # If successful (200 OK and no 'error' field)
156
+ token_data = token_response.json()
157
+ if "access_token" in token_data:
158
+ api_key = token_data["access_token"]
159
+ save_api_key(api_key)
160
+ console.print("\n[bold green]Login successful![/]")
161
+ return True
162
+ else:
163
+ # Unexpected successful response format
164
+ console.print("\n[bold red]Error:[/] Received unexpected response from server during polling.")
165
+ print(token_data)
166
+ return False
167
+ except requests.exceptions.RequestException as e:
168
+ # Handle network errors during polling gracefully
169
+ live_status.update("Waiting... (network error, retrying)")
170
+ console.print(f"\n[bold yellow]Warning:[/] Network error during polling: {e}. Retrying...")
171
+ time.sleep(interval * 2) # Simple backoff
172
+ except requests.exceptions.HTTPError as e:
173
+ from .api import handle_api_error # Import here to avoid circular imports
174
+
175
+ handle_api_error(e, console)
176
+ except requests.exceptions.RequestException as e:
177
+ # Catch other request errors
178
+ console.print(f"\n[bold red]Network Error:[/] {e}")
179
+ return False
180
+ except Exception as e:
181
+ console.print(f"\n[bold red]An unexpected error occurred during login:[/] {e}")
182
+ return False
183
+
184
+
185
+ def handle_authentication(console: Console, llm_api_keys: dict) -> tuple[str | None, dict]:
186
+ """
187
+ Handle the complete authentication flow.
188
+
189
+ Returns:
190
+ tuple: (weco_api_key, auth_headers)
191
+ """
192
+ weco_api_key = load_weco_api_key()
193
+
194
+ if not weco_api_key:
195
+ login_choice = Prompt.ask(
196
+ "Log in to Weco to save run history or use anonymously? ([bold]L[/]ogin / [bold]S[/]kip)",
197
+ choices=["l", "s"],
198
+ default="s",
199
+ ).lower()
200
+
201
+ if login_choice == "l":
202
+ console.print("[cyan]Starting login process...[/]")
203
+ if not perform_login(console):
204
+ console.print("[bold red]Login process failed or was cancelled.[/]")
205
+ return None, {}
206
+
207
+ weco_api_key = load_weco_api_key()
208
+ if not weco_api_key:
209
+ console.print("[bold red]Error: Login completed but failed to retrieve API key.[/]")
210
+ return None, {}
211
+
212
+ elif login_choice == "s":
213
+ console.print("[yellow]Proceeding anonymously. LLM API keys must be provided via environment variables.[/]")
214
+ if not llm_api_keys:
215
+ console.print(
216
+ "[bold red]Error:[/] No LLM API keys found in environment (e.g., OPENAI_API_KEY). Cannot proceed anonymously."
217
+ )
218
+ return None, {}
219
+
220
+ # Build auth headers
221
+ auth_headers = {}
222
+ if weco_api_key:
223
+ auth_headers["Authorization"] = f"Bearer {weco_api_key}"
224
+
225
+ return weco_api_key, auth_headers