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 +2 -8
- weco/api.py +193 -20
- 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.23.dist-info}/METADATA +111 -38
- weco-0.2.23.dist-info/RECORD +14 -0
- weco-0.2.20.dist-info/RECORD +0 -12
- {weco-0.2.20.dist-info → weco-0.2.23.dist-info}/WHEEL +0 -0
- {weco-0.2.20.dist-info → weco-0.2.23.dist-info}/entry_points.txt +0 -0
- {weco-0.2.20.dist-info → weco-0.2.23.dist-info}/licenses/LICENSE +0 -0
- {weco-0.2.20.dist-info → weco-0.2.23.dist-info}/top_level.txt +0 -0
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
61
|
-
console.print(f"[bold red]
|
|
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
|
|
92
|
-
print(f"
|
|
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
|
|
110
|
-
print(f"
|
|
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
|
|
127
|
-
print(f"
|
|
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,
|
|
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
|
|
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"
|
|
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
|