weco 0.2.28__py3-none-any.whl → 0.3.1__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 +164 -59
- weco/auth.py +12 -14
- weco/chatbot.py +18 -3
- weco/cli.py +75 -1
- weco/constants.py +6 -3
- weco/credits.py +172 -0
- weco/optimizer.py +415 -89
- weco/panels.py +6 -18
- weco/utils.py +9 -54
- {weco-0.2.28.dist-info → weco-0.3.1.dist-info}/METADATA +52 -55
- weco-0.3.1.dist-info/RECORD +16 -0
- weco-0.2.28.dist-info/RECORD +0 -15
- {weco-0.2.28.dist-info → weco-0.3.1.dist-info}/WHEEL +0 -0
- {weco-0.2.28.dist-info → weco-0.3.1.dist-info}/entry_points.txt +0 -0
- {weco-0.2.28.dist-info → weco-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {weco-0.2.28.dist-info → weco-0.3.1.dist-info}/top_level.txt +0 -0
weco/api.py
CHANGED
|
@@ -4,22 +4,98 @@ import requests
|
|
|
4
4
|
from rich.console import Console
|
|
5
5
|
|
|
6
6
|
from weco import __pkg_version__, __base_url__
|
|
7
|
-
from .
|
|
8
|
-
from .utils import truncate_output
|
|
7
|
+
from .utils import truncate_output, determine_model_for_onboarding
|
|
9
8
|
|
|
10
9
|
|
|
11
10
|
def handle_api_error(e: requests.exceptions.HTTPError, console: Console) -> None:
|
|
12
11
|
"""Extract and display error messages from API responses in a structured format."""
|
|
12
|
+
status = getattr(e.response, "status_code", None)
|
|
13
13
|
try:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
payload = e.response.json()
|
|
15
|
+
detail = payload.get("detail", payload)
|
|
16
|
+
except (ValueError, AttributeError):
|
|
17
|
+
detail = getattr(e.response, "text", "") or f"HTTP {status} Error"
|
|
18
|
+
|
|
19
|
+
def _render(detail_obj: Any) -> None:
|
|
20
|
+
if isinstance(detail_obj, str):
|
|
21
|
+
console.print(f"[bold red]{detail_obj}[/]")
|
|
22
|
+
elif isinstance(detail_obj, dict):
|
|
23
|
+
# Try common message keys in order of preference
|
|
24
|
+
message_keys = ("message", "error", "msg", "detail")
|
|
25
|
+
message = next((detail_obj.get(key) for key in message_keys if detail_obj.get(key)), None)
|
|
26
|
+
suggestion = detail_obj.get("suggestion")
|
|
27
|
+
if message:
|
|
28
|
+
console.print(f"[bold red]{message}[/]")
|
|
29
|
+
else:
|
|
30
|
+
console.print(f"[bold red]HTTP {status} Error[/]")
|
|
31
|
+
if suggestion:
|
|
32
|
+
console.print(f"[yellow]{suggestion}[/]")
|
|
33
|
+
extras = {
|
|
34
|
+
k: v
|
|
35
|
+
for k, v in detail_obj.items()
|
|
36
|
+
if k not in {"message", "error", "msg", "detail", "suggestion"} and v not in (None, "")
|
|
37
|
+
}
|
|
38
|
+
for key, value in extras.items():
|
|
39
|
+
console.print(f"[dim]{key}: {value}[/]")
|
|
40
|
+
elif isinstance(detail_obj, list) and detail_obj:
|
|
41
|
+
_render(detail_obj[0])
|
|
42
|
+
for extra in detail_obj[1:]:
|
|
43
|
+
console.print(f"[yellow]{extra}[/]")
|
|
44
|
+
else:
|
|
45
|
+
console.print(f"[bold red]{detail_obj or f'HTTP {status} Error'}[/]")
|
|
46
|
+
|
|
47
|
+
_render(detail)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _recover_suggest_after_transport_error(
|
|
51
|
+
console: Console, run_id: str, step: int, auth_headers: dict
|
|
52
|
+
) -> Optional[Dict[str, Any]]:
|
|
53
|
+
"""
|
|
54
|
+
Try to reconstruct the /suggest response after a transport error (ReadTimeout/502/RemoteDisconnected)
|
|
55
|
+
by fetching run status and using the latest nodes.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
console: The console object to use for logging.
|
|
59
|
+
run_id: The ID of the run to recover.
|
|
60
|
+
step: The step of the solution to recover.
|
|
61
|
+
auth_headers: The authentication headers to use for the request.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
The recovered response if the run is in a valid state, otherwise None.
|
|
65
|
+
"""
|
|
66
|
+
run_status_recovery_response = get_optimization_run_status(
|
|
67
|
+
console=console, run_id=run_id, include_history=True, auth_headers=auth_headers
|
|
68
|
+
)
|
|
69
|
+
current_step = run_status_recovery_response.get("current_step")
|
|
70
|
+
current_status = run_status_recovery_response.get("status")
|
|
71
|
+
# The run should be "running" and the current step should correspond to the solution step we are attempting to generate
|
|
72
|
+
is_valid_run_state = current_status is not None and current_status == "running"
|
|
73
|
+
is_valid_step = current_step is not None and current_step == step
|
|
74
|
+
if is_valid_run_state and is_valid_step:
|
|
75
|
+
nodes = run_status_recovery_response.get("nodes") or []
|
|
76
|
+
# We need at least 2 nodes to reconstruct the expected response i.e., the last two nodes
|
|
77
|
+
if len(nodes) >= 2:
|
|
78
|
+
nodes_sorted_ascending = sorted(nodes, key=lambda n: n["step"])
|
|
79
|
+
latest_node = nodes_sorted_ascending[-1]
|
|
80
|
+
penultimate_node = nodes_sorted_ascending[-2]
|
|
81
|
+
# If the server finished generating the next candidate, it should be exactly this step
|
|
82
|
+
if latest_node and latest_node["step"] == step:
|
|
83
|
+
# Try to reconstruct the expected response from the /suggest endpoint using the run status info
|
|
84
|
+
return {
|
|
85
|
+
"run_id": run_id,
|
|
86
|
+
"previous_solution_metric_value": penultimate_node.get("metric_value"),
|
|
87
|
+
"solution_id": latest_node.get("solution_id"),
|
|
88
|
+
"code": latest_node.get("code"),
|
|
89
|
+
"plan": latest_node.get("plan"),
|
|
90
|
+
"is_done": False,
|
|
91
|
+
}
|
|
92
|
+
return None
|
|
18
93
|
|
|
19
94
|
|
|
20
95
|
def start_optimization_run(
|
|
21
96
|
console: Console,
|
|
22
97
|
source_code: str,
|
|
98
|
+
source_path: str,
|
|
23
99
|
evaluation_command: str,
|
|
24
100
|
metric_name: str,
|
|
25
101
|
maximize: bool,
|
|
@@ -28,9 +104,11 @@ def start_optimization_run(
|
|
|
28
104
|
evaluator_config: Dict[str, Any],
|
|
29
105
|
search_policy_config: Dict[str, Any],
|
|
30
106
|
additional_instructions: str = None,
|
|
31
|
-
|
|
107
|
+
eval_timeout: Optional[int] = None,
|
|
108
|
+
save_logs: bool = False,
|
|
109
|
+
log_dir: str = ".runs",
|
|
32
110
|
auth_headers: dict = {},
|
|
33
|
-
timeout: Union[int, Tuple[int, int]] =
|
|
111
|
+
timeout: Union[int, Tuple[int, int]] = (10, 3650),
|
|
34
112
|
) -> Optional[Dict[str, Any]]:
|
|
35
113
|
"""Start the optimization run."""
|
|
36
114
|
with console.status("[bold green]Starting Optimization..."):
|
|
@@ -39,6 +117,7 @@ def start_optimization_run(
|
|
|
39
117
|
f"{__base_url__}/runs/",
|
|
40
118
|
json={
|
|
41
119
|
"source_code": source_code,
|
|
120
|
+
"source_path": source_path,
|
|
42
121
|
"additional_instructions": additional_instructions,
|
|
43
122
|
"objective": {"evaluation_command": evaluation_command, "metric_name": metric_name, "maximize": maximize},
|
|
44
123
|
"optimizer": {
|
|
@@ -47,7 +126,10 @@ def start_optimization_run(
|
|
|
47
126
|
"evaluator": evaluator_config,
|
|
48
127
|
"search_policy": search_policy_config,
|
|
49
128
|
},
|
|
50
|
-
"
|
|
129
|
+
"eval_timeout": eval_timeout,
|
|
130
|
+
"save_logs": save_logs,
|
|
131
|
+
"log_dir": log_dir,
|
|
132
|
+
"metadata": {"client_name": "cli", "client_version": __pkg_version__},
|
|
51
133
|
},
|
|
52
134
|
headers=auth_headers,
|
|
53
135
|
timeout=timeout,
|
|
@@ -68,14 +150,37 @@ def start_optimization_run(
|
|
|
68
150
|
return None
|
|
69
151
|
|
|
70
152
|
|
|
153
|
+
def resume_optimization_run(
|
|
154
|
+
console: Console, run_id: str, auth_headers: dict = {}, timeout: Union[int, Tuple[int, int]] = (5, 10)
|
|
155
|
+
) -> Optional[Dict[str, Any]]:
|
|
156
|
+
"""Request the backend to resume an interrupted run."""
|
|
157
|
+
with console.status("[bold green]Resuming run..."):
|
|
158
|
+
try:
|
|
159
|
+
response = requests.post(
|
|
160
|
+
f"{__base_url__}/runs/{run_id}/resume",
|
|
161
|
+
json={"metadata": {"client_name": "cli", "client_version": __pkg_version__}},
|
|
162
|
+
headers=auth_headers,
|
|
163
|
+
timeout=timeout,
|
|
164
|
+
)
|
|
165
|
+
response.raise_for_status()
|
|
166
|
+
result = response.json()
|
|
167
|
+
return result
|
|
168
|
+
except requests.exceptions.HTTPError as e:
|
|
169
|
+
handle_api_error(e, console)
|
|
170
|
+
return None
|
|
171
|
+
except Exception as e:
|
|
172
|
+
console.print(f"[bold red]Error resuming run: {e}[/]")
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
|
|
71
176
|
def evaluate_feedback_then_suggest_next_solution(
|
|
72
177
|
console: Console,
|
|
73
178
|
run_id: str,
|
|
179
|
+
step: int,
|
|
74
180
|
execution_output: str,
|
|
75
181
|
additional_instructions: str = None,
|
|
76
|
-
api_keys: Dict[str, Any] = {},
|
|
77
182
|
auth_headers: dict = {},
|
|
78
|
-
timeout: Union[int, Tuple[int, int]] =
|
|
183
|
+
timeout: Union[int, Tuple[int, int]] = (10, 3650),
|
|
79
184
|
) -> Dict[str, Any]:
|
|
80
185
|
"""Evaluate the feedback and suggest the next solution."""
|
|
81
186
|
try:
|
|
@@ -84,11 +189,7 @@ def evaluate_feedback_then_suggest_next_solution(
|
|
|
84
189
|
|
|
85
190
|
response = requests.post(
|
|
86
191
|
f"{__base_url__}/runs/{run_id}/suggest",
|
|
87
|
-
json={
|
|
88
|
-
"execution_output": truncated_output,
|
|
89
|
-
"additional_instructions": additional_instructions,
|
|
90
|
-
"metadata": {**api_keys},
|
|
91
|
-
},
|
|
192
|
+
json={"execution_output": truncated_output, "additional_instructions": additional_instructions, "metadata": {}},
|
|
92
193
|
headers=auth_headers,
|
|
93
194
|
timeout=timeout,
|
|
94
195
|
)
|
|
@@ -99,10 +200,40 @@ def evaluate_feedback_then_suggest_next_solution(
|
|
|
99
200
|
result["plan"] = ""
|
|
100
201
|
if result.get("code") is None:
|
|
101
202
|
result["code"] = ""
|
|
102
|
-
|
|
103
203
|
return result
|
|
204
|
+
except requests.exceptions.ReadTimeout as e:
|
|
205
|
+
# ReadTimeout can mean either:
|
|
206
|
+
# 1) the server truly didn't finish before the client's read timeout, or
|
|
207
|
+
# 2) the server finished but an intermediary (proxy/LB) dropped the response.
|
|
208
|
+
# We only try to recover case (2): fetch run status to confirm the step completed and reconstruct the response.
|
|
209
|
+
recovered = _recover_suggest_after_transport_error(
|
|
210
|
+
console=console, run_id=run_id, step=step, auth_headers=auth_headers
|
|
211
|
+
)
|
|
212
|
+
if recovered is not None:
|
|
213
|
+
return recovered
|
|
214
|
+
# If we cannot confirm completion, bubble up the timeout so the caller can resume later.
|
|
215
|
+
raise requests.exceptions.ReadTimeout(e)
|
|
104
216
|
except requests.exceptions.HTTPError as e:
|
|
105
|
-
#
|
|
217
|
+
# Treat only 502 Bad Gateway as a transient transport/gateway issue (akin to a dropped response).
|
|
218
|
+
# For 502, attempt the status-based recovery method used for ReadTimeout errors; otherwise render the HTTP error normally.
|
|
219
|
+
if (resp := getattr(e, "response", None)) is not None and resp.status_code == 502:
|
|
220
|
+
recovered = _recover_suggest_after_transport_error(
|
|
221
|
+
console=console, run_id=run_id, step=step, auth_headers=auth_headers
|
|
222
|
+
)
|
|
223
|
+
if recovered is not None:
|
|
224
|
+
return recovered
|
|
225
|
+
# Surface non-502 HTTP errors to the user.
|
|
226
|
+
handle_api_error(e, console)
|
|
227
|
+
raise
|
|
228
|
+
except requests.exceptions.ConnectionError as e:
|
|
229
|
+
# Covers connection resets with no HTTP response (e.g., RemoteDisconnected).
|
|
230
|
+
# Treat as a potential "response lost after completion": try status-based recovery first similar to how ReadTimeout errors are handled.
|
|
231
|
+
recovered = _recover_suggest_after_transport_error(
|
|
232
|
+
console=console, run_id=run_id, step=step, auth_headers=auth_headers
|
|
233
|
+
)
|
|
234
|
+
if recovered is not None:
|
|
235
|
+
return recovered
|
|
236
|
+
# Surface the connection error to the user.
|
|
106
237
|
handle_api_error(e, console)
|
|
107
238
|
raise
|
|
108
239
|
except Exception as e:
|
|
@@ -115,7 +246,7 @@ def get_optimization_run_status(
|
|
|
115
246
|
run_id: str,
|
|
116
247
|
include_history: bool = False,
|
|
117
248
|
auth_headers: dict = {},
|
|
118
|
-
timeout: Union[int, Tuple[int, int]] =
|
|
249
|
+
timeout: Union[int, Tuple[int, int]] = (5, 10),
|
|
119
250
|
) -> Dict[str, Any]:
|
|
120
251
|
"""Get the current status of the optimization run."""
|
|
121
252
|
try:
|
|
@@ -146,7 +277,7 @@ def get_optimization_run_status(
|
|
|
146
277
|
raise
|
|
147
278
|
|
|
148
279
|
|
|
149
|
-
def send_heartbeat(run_id: str, auth_headers: dict = {}, timeout: Union[int, Tuple[int, int]] = (
|
|
280
|
+
def send_heartbeat(run_id: str, auth_headers: dict = {}, timeout: Union[int, Tuple[int, int]] = (5, 10)) -> bool:
|
|
150
281
|
"""Send a heartbeat signal to the backend."""
|
|
151
282
|
try:
|
|
152
283
|
response = requests.put(f"{__base_url__}/runs/{run_id}/heartbeat", headers=auth_headers, timeout=timeout)
|
|
@@ -169,7 +300,7 @@ def report_termination(
|
|
|
169
300
|
reason: str,
|
|
170
301
|
details: Optional[str] = None,
|
|
171
302
|
auth_headers: dict = {},
|
|
172
|
-
timeout: Union[int, Tuple[int, int]] = (
|
|
303
|
+
timeout: Union[int, Tuple[int, int]] = (5, 10),
|
|
173
304
|
) -> bool:
|
|
174
305
|
"""Report the termination reason to the backend."""
|
|
175
306
|
try:
|
|
@@ -186,43 +317,17 @@ def report_termination(
|
|
|
186
317
|
return False
|
|
187
318
|
|
|
188
319
|
|
|
189
|
-
# --- Chatbot API Functions ---
|
|
190
|
-
def _determine_model_and_api_key() -> tuple[str, dict[str, str]]:
|
|
191
|
-
"""Determine the model and API key to use based on available environment variables.
|
|
192
|
-
|
|
193
|
-
Uses the shared model selection logic to maintain consistency.
|
|
194
|
-
Returns (model_name, api_key_dict)
|
|
195
|
-
"""
|
|
196
|
-
from .utils import read_api_keys_from_env, determine_default_model
|
|
197
|
-
|
|
198
|
-
llm_api_keys = read_api_keys_from_env()
|
|
199
|
-
model = determine_default_model(llm_api_keys)
|
|
200
|
-
|
|
201
|
-
# Create API key dictionary with only the key for the selected model
|
|
202
|
-
if model == "o4-mini":
|
|
203
|
-
api_key_dict = {"OPENAI_API_KEY": llm_api_keys["OPENAI_API_KEY"]}
|
|
204
|
-
elif model == "claude-sonnet-4-0":
|
|
205
|
-
api_key_dict = {"ANTHROPIC_API_KEY": llm_api_keys["ANTHROPIC_API_KEY"]}
|
|
206
|
-
elif model == "gemini-2.5-pro":
|
|
207
|
-
api_key_dict = {"GEMINI_API_KEY": llm_api_keys["GEMINI_API_KEY"]}
|
|
208
|
-
else:
|
|
209
|
-
# This should never happen if determine_default_model works correctly
|
|
210
|
-
raise ValueError(f"Unknown default model choice: {model}")
|
|
211
|
-
|
|
212
|
-
return model, api_key_dict
|
|
213
|
-
|
|
214
|
-
|
|
215
320
|
def get_optimization_suggestions_from_codebase(
|
|
216
321
|
console: Console,
|
|
217
322
|
gitingest_summary: str,
|
|
218
323
|
gitingest_tree: str,
|
|
219
324
|
gitingest_content_str: str,
|
|
220
325
|
auth_headers: dict = {},
|
|
221
|
-
timeout: Union[int, Tuple[int, int]] =
|
|
326
|
+
timeout: Union[int, Tuple[int, int]] = (10, 3650),
|
|
222
327
|
) -> Optional[List[Dict[str, Any]]]:
|
|
223
328
|
"""Analyze codebase and get optimization suggestions using the model-agnostic backend API."""
|
|
224
329
|
try:
|
|
225
|
-
model
|
|
330
|
+
model = determine_model_for_onboarding()
|
|
226
331
|
response = requests.post(
|
|
227
332
|
f"{__base_url__}/onboard/analyze-codebase",
|
|
228
333
|
json={
|
|
@@ -230,7 +335,7 @@ def get_optimization_suggestions_from_codebase(
|
|
|
230
335
|
"gitingest_tree": gitingest_tree,
|
|
231
336
|
"gitingest_content": gitingest_content_str,
|
|
232
337
|
"model": model,
|
|
233
|
-
"metadata":
|
|
338
|
+
"metadata": {},
|
|
234
339
|
},
|
|
235
340
|
headers=auth_headers,
|
|
236
341
|
timeout=timeout,
|
|
@@ -253,11 +358,11 @@ def generate_evaluation_script_and_metrics(
|
|
|
253
358
|
description: str,
|
|
254
359
|
gitingest_content_str: str,
|
|
255
360
|
auth_headers: dict = {},
|
|
256
|
-
timeout: Union[int, Tuple[int, int]] =
|
|
361
|
+
timeout: Union[int, Tuple[int, int]] = (10, 3650),
|
|
257
362
|
) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str]]:
|
|
258
363
|
"""Generate evaluation script and determine metrics using the model-agnostic backend API."""
|
|
259
364
|
try:
|
|
260
|
-
model
|
|
365
|
+
model = determine_model_for_onboarding()
|
|
261
366
|
response = requests.post(
|
|
262
367
|
f"{__base_url__}/onboard/generate-script",
|
|
263
368
|
json={
|
|
@@ -265,7 +370,7 @@ def generate_evaluation_script_and_metrics(
|
|
|
265
370
|
"description": description,
|
|
266
371
|
"gitingest_content": gitingest_content_str,
|
|
267
372
|
"model": model,
|
|
268
|
-
"metadata":
|
|
373
|
+
"metadata": {},
|
|
269
374
|
},
|
|
270
375
|
headers=auth_headers,
|
|
271
376
|
timeout=timeout,
|
|
@@ -289,11 +394,11 @@ def analyze_evaluation_environment(
|
|
|
289
394
|
gitingest_tree: str,
|
|
290
395
|
gitingest_content_str: str,
|
|
291
396
|
auth_headers: dict = {},
|
|
292
|
-
timeout: Union[int, Tuple[int, int]] =
|
|
397
|
+
timeout: Union[int, Tuple[int, int]] = (10, 3650),
|
|
293
398
|
) -> Optional[Dict[str, Any]]:
|
|
294
399
|
"""Analyze existing evaluation scripts and environment using the model-agnostic backend API."""
|
|
295
400
|
try:
|
|
296
|
-
model
|
|
401
|
+
model = determine_model_for_onboarding()
|
|
297
402
|
response = requests.post(
|
|
298
403
|
f"{__base_url__}/onboard/analyze-environment",
|
|
299
404
|
json={
|
|
@@ -303,7 +408,7 @@ def analyze_evaluation_environment(
|
|
|
303
408
|
"gitingest_tree": gitingest_tree,
|
|
304
409
|
"gitingest_content": gitingest_content_str,
|
|
305
410
|
"model": model,
|
|
306
|
-
"metadata":
|
|
411
|
+
"metadata": {},
|
|
307
412
|
},
|
|
308
413
|
headers=auth_headers,
|
|
309
414
|
timeout=timeout,
|
|
@@ -325,11 +430,11 @@ def analyze_script_execution_requirements(
|
|
|
325
430
|
script_path: str,
|
|
326
431
|
target_file: str,
|
|
327
432
|
auth_headers: dict = {},
|
|
328
|
-
timeout: Union[int, Tuple[int, int]] =
|
|
433
|
+
timeout: Union[int, Tuple[int, int]] = (10, 3650),
|
|
329
434
|
) -> Optional[str]:
|
|
330
435
|
"""Analyze script to determine proper execution command using the model-agnostic backend API."""
|
|
331
436
|
try:
|
|
332
|
-
model
|
|
437
|
+
model = determine_model_for_onboarding()
|
|
333
438
|
response = requests.post(
|
|
334
439
|
f"{__base_url__}/onboard/analyze-script",
|
|
335
440
|
json={
|
|
@@ -337,7 +442,7 @@ def analyze_script_execution_requirements(
|
|
|
337
442
|
"script_path": script_path,
|
|
338
443
|
"target_file": target_file,
|
|
339
444
|
"model": model,
|
|
340
|
-
"metadata":
|
|
445
|
+
"metadata": {},
|
|
341
446
|
},
|
|
342
447
|
headers=auth_headers,
|
|
343
448
|
timeout=timeout,
|
weco/auth.py
CHANGED
|
@@ -184,9 +184,9 @@ def perform_login(console: Console):
|
|
|
184
184
|
return False
|
|
185
185
|
|
|
186
186
|
|
|
187
|
-
def handle_authentication(console: Console
|
|
187
|
+
def handle_authentication(console: Console) -> tuple[str | None, dict]:
|
|
188
188
|
"""
|
|
189
|
-
Handle the complete authentication flow.
|
|
189
|
+
Handle the complete authentication flow. Authentication is now mandatory.
|
|
190
190
|
|
|
191
191
|
Returns:
|
|
192
192
|
tuple: (weco_api_key, auth_headers)
|
|
@@ -194,13 +194,16 @@ def handle_authentication(console: Console, llm_api_keys: dict) -> tuple[str | N
|
|
|
194
194
|
weco_api_key = load_weco_api_key()
|
|
195
195
|
|
|
196
196
|
if not weco_api_key:
|
|
197
|
+
console.print("[bold yellow]Authentication Required[/]")
|
|
198
|
+
console.print("With our new credit-based billing system, authentication is required to use Weco.")
|
|
199
|
+
console.print("You'll receive free credits to get started!")
|
|
200
|
+
console.print("")
|
|
201
|
+
|
|
197
202
|
login_choice = Prompt.ask(
|
|
198
|
-
"
|
|
199
|
-
choices=["l", "s"],
|
|
200
|
-
default="s",
|
|
203
|
+
"Would you like to log in now? ([bold]Y[/]es / [bold]N[/]o)", choices=["y", "n"], default="y"
|
|
201
204
|
).lower()
|
|
202
205
|
|
|
203
|
-
if login_choice == "
|
|
206
|
+
if login_choice == "y":
|
|
204
207
|
console.print("[cyan]Starting login process...[/]")
|
|
205
208
|
if not perform_login(console):
|
|
206
209
|
console.print("[bold red]Login process failed or was cancelled.[/]")
|
|
@@ -210,14 +213,9 @@ def handle_authentication(console: Console, llm_api_keys: dict) -> tuple[str | N
|
|
|
210
213
|
if not weco_api_key:
|
|
211
214
|
console.print("[bold red]Error: Login completed but failed to retrieve API key.[/]")
|
|
212
215
|
return None, {}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if not llm_api_keys:
|
|
217
|
-
console.print(
|
|
218
|
-
"[bold red]Error:[/] No LLM API keys found in environment (e.g., OPENAI_API_KEY). Cannot proceed anonymously."
|
|
219
|
-
)
|
|
220
|
-
return None, {}
|
|
216
|
+
else:
|
|
217
|
+
console.print("[yellow]Authentication is required to use Weco. Please run 'weco' again when ready to log in.[/]")
|
|
218
|
+
return None, {}
|
|
221
219
|
|
|
222
220
|
# Build auth headers
|
|
223
221
|
auth_headers = {}
|
weco/chatbot.py
CHANGED
|
@@ -220,6 +220,7 @@ class Chatbot:
|
|
|
220
220
|
gitingest_summary=self.gitingest_summary,
|
|
221
221
|
gitingest_tree=self.gitingest_tree,
|
|
222
222
|
gitingest_content_str=self.gitingest_content_str,
|
|
223
|
+
auth_headers=getattr(self, "auth_headers", {}),
|
|
223
224
|
)
|
|
224
225
|
|
|
225
226
|
if result and isinstance(result, list):
|
|
@@ -332,6 +333,7 @@ class Chatbot:
|
|
|
332
333
|
target_file=selected_option["target_file"],
|
|
333
334
|
description=selected_option["description"],
|
|
334
335
|
gitingest_content_str=self.gitingest_content_str,
|
|
336
|
+
auth_headers=getattr(self, "auth_headers", {}),
|
|
335
337
|
)
|
|
336
338
|
if result and result[0]:
|
|
337
339
|
eval_script_content, metric_name, goal, reasoning = result
|
|
@@ -381,6 +383,7 @@ class Chatbot:
|
|
|
381
383
|
script_content=eval_script_content,
|
|
382
384
|
script_path=eval_script_path_str,
|
|
383
385
|
target_file=selected_option["target_file"],
|
|
386
|
+
auth_headers=getattr(self, "auth_headers", {}),
|
|
384
387
|
)
|
|
385
388
|
|
|
386
389
|
return {
|
|
@@ -401,6 +404,7 @@ class Chatbot:
|
|
|
401
404
|
gitingest_summary=self.gitingest_summary,
|
|
402
405
|
gitingest_tree=self.gitingest_tree,
|
|
403
406
|
gitingest_content_str=self.gitingest_content_str,
|
|
407
|
+
auth_headers=getattr(self, "auth_headers", {}),
|
|
404
408
|
)
|
|
405
409
|
|
|
406
410
|
if not analysis:
|
|
@@ -542,6 +546,7 @@ class Chatbot:
|
|
|
542
546
|
script_content=script_content,
|
|
543
547
|
script_path=script_path,
|
|
544
548
|
target_file=selected_option["target_file"],
|
|
549
|
+
auth_headers=getattr(self, "auth_headers", {}),
|
|
545
550
|
)
|
|
546
551
|
|
|
547
552
|
self.current_step = "confirmation"
|
|
@@ -749,10 +754,9 @@ class Chatbot:
|
|
|
749
754
|
self.resolved_model = self.user_specified_model
|
|
750
755
|
else:
|
|
751
756
|
# Use same default model selection as weco run
|
|
752
|
-
from .utils import
|
|
757
|
+
from .utils import determine_model_for_onboarding
|
|
753
758
|
|
|
754
|
-
|
|
755
|
-
self.resolved_model = determine_default_model(llm_api_keys)
|
|
759
|
+
self.resolved_model = determine_model_for_onboarding()
|
|
756
760
|
|
|
757
761
|
target_file = selected_option["target_file"]
|
|
758
762
|
additional_instructions = selected_option["description"]
|
|
@@ -766,6 +770,17 @@ class Chatbot:
|
|
|
766
770
|
self.console.print("[bold cyan]Welcome to Weco![/]")
|
|
767
771
|
self.console.print(f"Let's optimize your codebase in: [cyan]{self.project_path}[/]\n")
|
|
768
772
|
|
|
773
|
+
# Mandatory authentication as per PLAN.md
|
|
774
|
+
from .auth import handle_authentication
|
|
775
|
+
|
|
776
|
+
weco_api_key, auth_headers = handle_authentication(self.console)
|
|
777
|
+
if not weco_api_key:
|
|
778
|
+
self.console.print("[yellow]Authentication is required to use Weco. Exiting...[/]")
|
|
779
|
+
return
|
|
780
|
+
|
|
781
|
+
# Store auth headers for API calls
|
|
782
|
+
self.auth_headers = auth_headers
|
|
783
|
+
|
|
769
784
|
options = self.analyze_codebase_and_get_optimization_options()
|
|
770
785
|
if not options:
|
|
771
786
|
return
|
weco/cli.py
CHANGED
|
@@ -74,6 +74,52 @@ def configure_run_parser(run_parser: argparse.ArgumentParser) -> None:
|
|
|
74
74
|
)
|
|
75
75
|
|
|
76
76
|
|
|
77
|
+
def configure_credits_parser(credits_parser: argparse.ArgumentParser) -> None:
|
|
78
|
+
"""Configure the credits command parser and all its subcommands."""
|
|
79
|
+
credits_subparsers = credits_parser.add_subparsers(dest="credits_command", help="Credit management commands")
|
|
80
|
+
|
|
81
|
+
# Credits balance command
|
|
82
|
+
_ = credits_subparsers.add_parser("balance", help="Check your current credit balance")
|
|
83
|
+
|
|
84
|
+
# Coerce CLI input into a float with two decimal precision for the API payload.
|
|
85
|
+
def _parse_credit_amount(value: str) -> float:
|
|
86
|
+
try:
|
|
87
|
+
amount = float(value)
|
|
88
|
+
except ValueError as exc:
|
|
89
|
+
raise argparse.ArgumentTypeError("Amount must be a number.") from exc
|
|
90
|
+
|
|
91
|
+
return round(amount, 2)
|
|
92
|
+
|
|
93
|
+
# Credits topup command
|
|
94
|
+
topup_parser = credits_subparsers.add_parser("topup", help="Purchase additional credits")
|
|
95
|
+
topup_parser.add_argument(
|
|
96
|
+
"amount",
|
|
97
|
+
nargs="?",
|
|
98
|
+
type=_parse_credit_amount,
|
|
99
|
+
default=_parse_credit_amount("10"),
|
|
100
|
+
metavar="CREDITS",
|
|
101
|
+
help="Amount of credits to purchase (minimum 2, defaults to 10)",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Credits autotopup command
|
|
105
|
+
autotopup_parser = credits_subparsers.add_parser("autotopup", help="Configure automatic top-up")
|
|
106
|
+
autotopup_parser.add_argument("--enable", action="store_true", help="Enable automatic top-up")
|
|
107
|
+
autotopup_parser.add_argument("--disable", action="store_true", help="Disable automatic top-up")
|
|
108
|
+
autotopup_parser.add_argument(
|
|
109
|
+
"--threshold", type=float, default=4.0, help="Balance threshold to trigger auto top-up (default: 4.0 credits)"
|
|
110
|
+
)
|
|
111
|
+
autotopup_parser.add_argument(
|
|
112
|
+
"--amount", type=float, default=50.0, help="Amount to top up when threshold is reached (default: 50.0 credits)"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def configure_resume_parser(resume_parser: argparse.ArgumentParser) -> None:
|
|
117
|
+
"""Configure arguments for the resume command."""
|
|
118
|
+
resume_parser.add_argument(
|
|
119
|
+
"run_id", type=str, help="The UUID of the run to resume (e.g., '0002e071-1b67-411f-a514-36947f0c4b31')"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
77
123
|
def execute_run_command(args: argparse.Namespace) -> None:
|
|
78
124
|
"""Execute the 'weco run' command with all its logic."""
|
|
79
125
|
from .optimizer import execute_optimization
|
|
@@ -95,6 +141,14 @@ def execute_run_command(args: argparse.Namespace) -> None:
|
|
|
95
141
|
sys.exit(exit_code)
|
|
96
142
|
|
|
97
143
|
|
|
144
|
+
def execute_resume_command(args: argparse.Namespace) -> None:
|
|
145
|
+
"""Execute the 'weco resume' command with all its logic."""
|
|
146
|
+
from .optimizer import resume_optimization
|
|
147
|
+
|
|
148
|
+
success = resume_optimization(run_id=args.run_id, console=console)
|
|
149
|
+
sys.exit(0 if success else 1)
|
|
150
|
+
|
|
151
|
+
|
|
98
152
|
def main() -> None:
|
|
99
153
|
"""Main function for the Weco CLI."""
|
|
100
154
|
check_for_cli_updates()
|
|
@@ -126,6 +180,19 @@ def main() -> None:
|
|
|
126
180
|
# --- Logout Command Parser Setup ---
|
|
127
181
|
_ = subparsers.add_parser("logout", help="Log out from Weco and clear saved API key.")
|
|
128
182
|
|
|
183
|
+
# --- Credits Command Parser Setup ---
|
|
184
|
+
credits_parser = subparsers.add_parser("credits", help="Manage your Weco credits")
|
|
185
|
+
configure_credits_parser(credits_parser) # Use the helper to add subcommands and arguments
|
|
186
|
+
|
|
187
|
+
# --- Resume Command Parser Setup ---
|
|
188
|
+
resume_parser = subparsers.add_parser(
|
|
189
|
+
"resume",
|
|
190
|
+
help="Resume an interrupted optimization run",
|
|
191
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
192
|
+
allow_abbrev=False,
|
|
193
|
+
)
|
|
194
|
+
configure_resume_parser(resume_parser)
|
|
195
|
+
|
|
129
196
|
# Check if we should run the chatbot
|
|
130
197
|
# This logic needs to be robust. If 'run' or 'logout' is present, or -h/--help, don't run chatbot.
|
|
131
198
|
# Otherwise, if it's just 'weco' or 'weco <path>' (with optional --model), run chatbot.
|
|
@@ -157,7 +224,7 @@ def main() -> None:
|
|
|
157
224
|
return None
|
|
158
225
|
|
|
159
226
|
first_non_option = get_first_non_option_arg()
|
|
160
|
-
is_known_command = first_non_option in ["run", "logout"]
|
|
227
|
+
is_known_command = first_non_option in ["run", "logout", "credits"]
|
|
161
228
|
is_help_command = len(sys.argv) > 1 and sys.argv[1] in ["-h", "--help"] # Check for global help
|
|
162
229
|
|
|
163
230
|
should_run_chatbot_result = should_run_chatbot(sys.argv[1:])
|
|
@@ -208,6 +275,13 @@ def main() -> None:
|
|
|
208
275
|
sys.exit(0)
|
|
209
276
|
elif args.command == "run":
|
|
210
277
|
execute_run_command(args)
|
|
278
|
+
elif args.command == "credits":
|
|
279
|
+
from .credits import handle_credits_command
|
|
280
|
+
|
|
281
|
+
handle_credits_command(args, console)
|
|
282
|
+
sys.exit(0)
|
|
283
|
+
elif args.command == "resume":
|
|
284
|
+
execute_resume_command(args)
|
|
211
285
|
else:
|
|
212
286
|
# This case should be hit if 'weco' is run alone and chatbot logic didn't catch it,
|
|
213
287
|
# or if an invalid command is provided.
|
weco/constants.py
CHANGED
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
Constants for the Weco CLI package.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
# API timeout configuration (connect_timeout, read_timeout) in seconds
|
|
7
|
-
DEFAULT_API_TIMEOUT = (10, 3650)
|
|
8
|
-
|
|
9
6
|
# Output truncation configuration
|
|
10
7
|
TRUNCATION_THRESHOLD = 51000 # Maximum length before truncation
|
|
11
8
|
TRUNCATION_KEEP_LENGTH = 25000 # Characters to keep from beginning and end
|
|
9
|
+
|
|
10
|
+
# Default model configuration
|
|
11
|
+
DEFAULT_MODEL = "o4-mini"
|
|
12
|
+
|
|
13
|
+
# Supported file extensions for additional instructions
|
|
14
|
+
SUPPORTED_FILE_EXTENSIONS = [".md", ".txt", ".rst"]
|