weco 0.2.11__py3-none-any.whl → 0.2.13__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 +12 -2
- weco/api.py +18 -4
- weco/auth.py +64 -0
- weco/cli.py +471 -276
- weco/panels.py +20 -2
- {weco-0.2.11.dist-info → weco-0.2.13.dist-info}/METADATA +61 -23
- weco-0.2.13.dist-info/RECORD +12 -0
- weco-0.2.11.dist-info/RECORD +0 -11
- {weco-0.2.11.dist-info → weco-0.2.13.dist-info}/WHEEL +0 -0
- {weco-0.2.11.dist-info → weco-0.2.13.dist-info}/entry_points.txt +0 -0
- {weco-0.2.11.dist-info → weco-0.2.13.dist-info}/licenses/LICENSE +0 -0
- {weco-0.2.11.dist-info → weco-0.2.13.dist-info}/top_level.txt +0 -0
weco/__init__.py
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
1
3
|
# DO NOT EDIT
|
|
2
|
-
__pkg_version__ = "0.2.
|
|
4
|
+
__pkg_version__ = "0.2.13"
|
|
3
5
|
__api_version__ = "v1"
|
|
4
|
-
|
|
6
|
+
|
|
7
|
+
__base_url__ = f"https://api.weco.ai/{__api_version__}"
|
|
8
|
+
# If user specifies a custom base URL, use that instead
|
|
9
|
+
if os.environ.get("WECO_BASE_URL"):
|
|
10
|
+
__base_url__ = os.environ.get("WECO_BASE_URL")
|
|
11
|
+
|
|
12
|
+
__dashboard_url__ = "https://dashboard.weco.ai"
|
|
13
|
+
if os.environ.get("WECO_DASHBOARD_URL"):
|
|
14
|
+
__dashboard_url__ = os.environ.get("WECO_DASHBOARD_URL")
|
weco/api.py
CHANGED
|
@@ -24,13 +24,15 @@ def start_optimization_session(
|
|
|
24
24
|
search_policy_config: Dict[str, Any],
|
|
25
25
|
additional_instructions: str = None,
|
|
26
26
|
api_keys: Dict[str, Any] = {},
|
|
27
|
+
auth_headers: dict = {}, # Add auth_headers
|
|
27
28
|
timeout: int = 800,
|
|
28
29
|
) -> Dict[str, Any]:
|
|
29
30
|
"""Start the optimization session."""
|
|
30
31
|
with console.status("[bold green]Starting Optimization..."):
|
|
31
32
|
try:
|
|
33
|
+
# __base_url__ already contains /v1
|
|
32
34
|
response = requests.post(
|
|
33
|
-
f"{__base_url__}/sessions",
|
|
35
|
+
f"{__base_url__}/sessions", # Path is relative to base_url
|
|
34
36
|
json={
|
|
35
37
|
"source_code": source_code,
|
|
36
38
|
"additional_instructions": additional_instructions,
|
|
@@ -43,6 +45,7 @@ def start_optimization_session(
|
|
|
43
45
|
},
|
|
44
46
|
"metadata": {"client_name": "cli", "client_version": __pkg_version__, **api_keys},
|
|
45
47
|
},
|
|
48
|
+
headers=auth_headers, # Add headers
|
|
46
49
|
timeout=timeout,
|
|
47
50
|
)
|
|
48
51
|
response.raise_for_status()
|
|
@@ -57,17 +60,20 @@ def evaluate_feedback_then_suggest_next_solution(
|
|
|
57
60
|
execution_output: str,
|
|
58
61
|
additional_instructions: str = None,
|
|
59
62
|
api_keys: Dict[str, Any] = {},
|
|
63
|
+
auth_headers: dict = {}, # Add auth_headers
|
|
60
64
|
timeout: int = 800,
|
|
61
65
|
) -> Dict[str, Any]:
|
|
62
66
|
"""Evaluate the feedback and suggest the next solution."""
|
|
63
67
|
try:
|
|
68
|
+
# __base_url__ already contains /v1
|
|
64
69
|
response = requests.post(
|
|
65
|
-
f"{__base_url__}/sessions/{session_id}/suggest",
|
|
70
|
+
f"{__base_url__}/sessions/{session_id}/suggest", # Path is relative to base_url
|
|
66
71
|
json={
|
|
67
72
|
"execution_output": execution_output,
|
|
68
73
|
"additional_instructions": additional_instructions,
|
|
69
74
|
"metadata": {**api_keys},
|
|
70
75
|
},
|
|
76
|
+
headers=auth_headers, # Add headers
|
|
71
77
|
timeout=timeout,
|
|
72
78
|
)
|
|
73
79
|
response.raise_for_status()
|
|
@@ -77,12 +83,20 @@ def evaluate_feedback_then_suggest_next_solution(
|
|
|
77
83
|
|
|
78
84
|
|
|
79
85
|
def get_optimization_session_status(
|
|
80
|
-
console: rich.console.Console,
|
|
86
|
+
console: rich.console.Console,
|
|
87
|
+
session_id: str,
|
|
88
|
+
include_history: bool = False,
|
|
89
|
+
auth_headers: dict = {},
|
|
90
|
+
timeout: int = 800, # Add auth_headers
|
|
81
91
|
) -> Dict[str, Any]:
|
|
82
92
|
"""Get the current status of the optimization session."""
|
|
83
93
|
try:
|
|
94
|
+
# __base_url__ already contains /v1
|
|
84
95
|
response = requests.get(
|
|
85
|
-
f"{__base_url__}/sessions/{session_id}",
|
|
96
|
+
f"{__base_url__}/sessions/{session_id}", # Path is relative to base_url
|
|
97
|
+
params={"include_history": include_history},
|
|
98
|
+
headers=auth_headers, # Add headers
|
|
99
|
+
timeout=timeout,
|
|
86
100
|
)
|
|
87
101
|
response.raise_for_status()
|
|
88
102
|
return response.json()
|
weco/auth.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# weco/auth.py
|
|
2
|
+
import os
|
|
3
|
+
import pathlib
|
|
4
|
+
import json
|
|
5
|
+
import stat
|
|
6
|
+
|
|
7
|
+
CONFIG_DIR = pathlib.Path.home() / ".config" / "weco"
|
|
8
|
+
CREDENTIALS_FILE = CONFIG_DIR / "credentials.json"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def ensure_config_dir():
|
|
12
|
+
"""Ensures the configuration directory exists."""
|
|
13
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
14
|
+
# Ensure directory permissions are secure (optional but good practice)
|
|
15
|
+
try:
|
|
16
|
+
os.chmod(CONFIG_DIR, stat.S_IRWXU) # Read/Write/Execute for owner only
|
|
17
|
+
except OSError as e:
|
|
18
|
+
print(f"Warning: Could not set permissions on {CONFIG_DIR}: {e}")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def save_api_key(api_key: str):
|
|
22
|
+
"""Saves the Weco API key securely."""
|
|
23
|
+
ensure_config_dir()
|
|
24
|
+
credentials = {"api_key": api_key}
|
|
25
|
+
try:
|
|
26
|
+
with open(CREDENTIALS_FILE, "w") as f:
|
|
27
|
+
json.dump(credentials, f)
|
|
28
|
+
# Set file permissions to read/write for owner only (600)
|
|
29
|
+
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
|
+
except OSError as e:
|
|
33
|
+
print(f"Warning: Could not set permissions on {CREDENTIALS_FILE}: {e}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def load_weco_api_key() -> str | None:
|
|
37
|
+
"""Loads the Weco API key."""
|
|
38
|
+
if not CREDENTIALS_FILE.exists():
|
|
39
|
+
return None
|
|
40
|
+
try:
|
|
41
|
+
# Check permissions before reading (optional but safer)
|
|
42
|
+
file_stat = os.stat(CREDENTIALS_FILE)
|
|
43
|
+
if file_stat.st_mode & (stat.S_IRWXG | stat.S_IRWXO): # Check if group/other have permissions
|
|
44
|
+
print(f"Warning: Credentials file {CREDENTIALS_FILE} has insecure permissions. Please set to 600.")
|
|
45
|
+
# Optionally, refuse to load or try to fix permissions
|
|
46
|
+
|
|
47
|
+
with open(CREDENTIALS_FILE, "r") as f:
|
|
48
|
+
credentials = json.load(f)
|
|
49
|
+
return credentials.get("api_key")
|
|
50
|
+
except (IOError, json.JSONDecodeError, OSError) as e:
|
|
51
|
+
print(f"Warning: Could not read or parse credentials file at {CREDENTIALS_FILE}: {e}")
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def clear_api_key():
|
|
56
|
+
"""Removes the stored API key."""
|
|
57
|
+
if CREDENTIALS_FILE.exists():
|
|
58
|
+
try:
|
|
59
|
+
os.remove(CREDENTIALS_FILE)
|
|
60
|
+
print("Logged out successfully.")
|
|
61
|
+
except OSError as e:
|
|
62
|
+
print(f"Error: Could not remove credentials file at {CREDENTIALS_FILE}: {e}")
|
|
63
|
+
else:
|
|
64
|
+
print("Already logged out.")
|
weco/cli.py
CHANGED
|
@@ -2,11 +2,23 @@ import argparse
|
|
|
2
2
|
import sys
|
|
3
3
|
import pathlib
|
|
4
4
|
import math
|
|
5
|
+
import time
|
|
6
|
+
import requests
|
|
7
|
+
import webbrowser
|
|
5
8
|
from rich.console import Console
|
|
6
9
|
from rich.live import Live
|
|
7
10
|
from rich.panel import Panel
|
|
8
11
|
from rich.traceback import install
|
|
9
|
-
from .
|
|
12
|
+
from rich.prompt import Prompt
|
|
13
|
+
from .api import (
|
|
14
|
+
start_optimization_session,
|
|
15
|
+
evaluate_feedback_then_suggest_next_solution,
|
|
16
|
+
get_optimization_session_status,
|
|
17
|
+
handle_api_error,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from . import __base_url__
|
|
21
|
+
from .auth import load_weco_api_key, save_api_key, clear_api_key
|
|
10
22
|
from .panels import (
|
|
11
23
|
SummaryPanel,
|
|
12
24
|
PlanPanel,
|
|
@@ -31,42 +43,215 @@ install(show_locals=True)
|
|
|
31
43
|
console = Console()
|
|
32
44
|
|
|
33
45
|
|
|
46
|
+
def perform_login(console: Console):
|
|
47
|
+
"""Handles the device login flow."""
|
|
48
|
+
try:
|
|
49
|
+
# 1. Initiate device login
|
|
50
|
+
console.print("Initiating login...")
|
|
51
|
+
init_response = requests.post(f"{__base_url__}/auth/device/initiate")
|
|
52
|
+
init_response.raise_for_status()
|
|
53
|
+
init_data = init_response.json()
|
|
54
|
+
|
|
55
|
+
device_code = init_data["device_code"]
|
|
56
|
+
verification_uri = init_data["verification_uri"]
|
|
57
|
+
expires_in = init_data["expires_in"]
|
|
58
|
+
interval = init_data["interval"]
|
|
59
|
+
|
|
60
|
+
# 2. Display instructions
|
|
61
|
+
console.print("\n[bold yellow]Action Required:[/]")
|
|
62
|
+
console.print("Please open the following URL in your browser to authenticate:")
|
|
63
|
+
console.print(f"[link={verification_uri}]{verification_uri}[/link]")
|
|
64
|
+
console.print(f"This request will expire in {expires_in // 60} minutes.")
|
|
65
|
+
console.print("Attempting to open the authentication page in your default browser...") # Notify user
|
|
66
|
+
|
|
67
|
+
# Automatically open the browser
|
|
68
|
+
try:
|
|
69
|
+
if not webbrowser.open(verification_uri):
|
|
70
|
+
console.print("[yellow]Could not automatically open the browser. Please open the link manually.[/]")
|
|
71
|
+
except Exception as browser_err:
|
|
72
|
+
console.print(
|
|
73
|
+
f"[yellow]Could not automatically open the browser ({browser_err}). Please open the link manually.[/]"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
console.print("Waiting for authentication...", end="")
|
|
77
|
+
|
|
78
|
+
# 3. Poll for token
|
|
79
|
+
start_time = time.time()
|
|
80
|
+
# Use a simple text update instead of Spinner within Live for potentially better compatibility
|
|
81
|
+
polling_status = "Waiting..."
|
|
82
|
+
with Live(polling_status, refresh_per_second=1, transient=True, console=console) as live_status:
|
|
83
|
+
while True:
|
|
84
|
+
# Check for timeout
|
|
85
|
+
if time.time() - start_time > expires_in:
|
|
86
|
+
console.print("\n[bold red]Error:[/] Login request timed out.")
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
time.sleep(interval)
|
|
90
|
+
live_status.update("Waiting... (checking status)")
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
token_response = requests.post(
|
|
94
|
+
f"{__base_url__}/auth/device/token", # REMOVED /v1 prefix
|
|
95
|
+
json={"grant_type": "urn:ietf:params:oauth:grant-type:device_code", "device_code": device_code},
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Check for 202 Accepted - Authorization Pending
|
|
99
|
+
if token_response.status_code == 202:
|
|
100
|
+
token_data = token_response.json()
|
|
101
|
+
if token_data.get("error") == "authorization_pending":
|
|
102
|
+
live_status.update("Waiting... (authorization pending)")
|
|
103
|
+
continue # Continue polling
|
|
104
|
+
else:
|
|
105
|
+
# Unexpected 202 response format
|
|
106
|
+
console.print(f"\n[bold red]Error:[/] Received unexpected 202 response: {token_data}")
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
# Check for standard OAuth2 errors (often 400 Bad Request)
|
|
110
|
+
elif token_response.status_code == 400:
|
|
111
|
+
token_data = token_response.json()
|
|
112
|
+
error_code = token_data.get("error", "unknown_error")
|
|
113
|
+
# NOTE: Removed "authorization_pending" check from here
|
|
114
|
+
if error_code == "slow_down":
|
|
115
|
+
interval += 5 # Increase polling interval if instructed
|
|
116
|
+
live_status.update(f"Waiting... (slowing down polling to {interval}s)")
|
|
117
|
+
continue
|
|
118
|
+
elif error_code == "expired_token":
|
|
119
|
+
console.print("\n[bold red]Error:[/] Login request expired.")
|
|
120
|
+
return False
|
|
121
|
+
elif error_code == "access_denied":
|
|
122
|
+
console.print("\n[bold red]Error:[/] Authorization denied by user.")
|
|
123
|
+
return False
|
|
124
|
+
else: # invalid_grant, etc.
|
|
125
|
+
error_desc = token_data.get("error_description", "Unknown error during polling.")
|
|
126
|
+
console.print(f"\n[bold red]Error:[/] {error_desc} ({error_code})")
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
# Check for other non-200/non-202/non-400 HTTP errors
|
|
130
|
+
token_response.raise_for_status()
|
|
131
|
+
|
|
132
|
+
# If successful (200 OK and no 'error' field)
|
|
133
|
+
token_data = token_response.json()
|
|
134
|
+
if "access_token" in token_data:
|
|
135
|
+
api_key = token_data["access_token"]
|
|
136
|
+
save_api_key(api_key)
|
|
137
|
+
console.print("\n[bold green]Login successful![/]")
|
|
138
|
+
return True
|
|
139
|
+
else:
|
|
140
|
+
# Unexpected successful response format
|
|
141
|
+
console.print("\n[bold red]Error:[/] Received unexpected response from server during polling.")
|
|
142
|
+
print(token_data) # Log for debugging
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
except requests.exceptions.RequestException as e:
|
|
146
|
+
# Handle network errors during polling gracefully
|
|
147
|
+
live_status.update("Waiting... (network error, retrying)")
|
|
148
|
+
console.print(f"\n[bold yellow]Warning:[/] Network error during polling: {e}. Retrying...")
|
|
149
|
+
# Optional: implement backoff strategy
|
|
150
|
+
time.sleep(interval * 2) # Simple backoff
|
|
151
|
+
|
|
152
|
+
except requests.exceptions.HTTPError as e: # Catch HTTPError specifically for handle_api_error
|
|
153
|
+
handle_api_error(e, console)
|
|
154
|
+
except requests.exceptions.RequestException as e: # Catch other request errors
|
|
155
|
+
console.print(f"\n[bold red]Network Error:[/] {e}")
|
|
156
|
+
return False
|
|
157
|
+
except Exception as e:
|
|
158
|
+
console.print(f"\n[bold red]An unexpected error occurred during login:[/] {e}")
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
|
|
34
162
|
def main() -> None:
|
|
35
163
|
"""Main function for the Weco CLI."""
|
|
164
|
+
# --- Argument Parsing ---
|
|
36
165
|
parser = argparse.ArgumentParser(
|
|
37
166
|
description="[bold cyan]Weco CLI[/]", formatter_class=argparse.RawDescriptionHelpFormatter
|
|
38
167
|
)
|
|
39
|
-
|
|
40
|
-
parser.
|
|
168
|
+
# Add subparsers for commands like 'run' and 'logout'
|
|
169
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands", required=True) # Make command required
|
|
170
|
+
|
|
171
|
+
# --- Run Command ---
|
|
172
|
+
run_parser = subparsers.add_parser(
|
|
173
|
+
"run", help="Run code optimization", formatter_class=argparse.RawDescriptionHelpFormatter
|
|
174
|
+
)
|
|
175
|
+
# Add arguments specific to the 'run' command to the run_parser
|
|
176
|
+
run_parser.add_argument("--source", type=str, required=True, help="Path to the source code file (e.g. optimize.py)")
|
|
177
|
+
run_parser.add_argument(
|
|
41
178
|
"--eval-command", type=str, required=True, help="Command to run for evaluation (e.g. 'python eval.py --arg1=val1')"
|
|
42
179
|
)
|
|
43
|
-
|
|
44
|
-
|
|
180
|
+
run_parser.add_argument("--metric", type=str, required=True, help="Metric to optimize")
|
|
181
|
+
run_parser.add_argument(
|
|
45
182
|
"--maximize",
|
|
46
183
|
type=str,
|
|
47
184
|
choices=["true", "false"],
|
|
48
185
|
required=True,
|
|
49
186
|
help="Specify 'true' to maximize the metric or 'false' to minimize.",
|
|
50
187
|
)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
188
|
+
run_parser.add_argument("--steps", type=int, required=True, help="Number of steps to run")
|
|
189
|
+
run_parser.add_argument("--model", type=str, required=True, help="Model to use for optimization")
|
|
190
|
+
run_parser.add_argument("--log-dir", type=str, default=".runs", help="Directory to store logs and results")
|
|
191
|
+
run_parser.add_argument(
|
|
55
192
|
"--additional-instructions",
|
|
56
193
|
default=None,
|
|
57
194
|
type=str,
|
|
58
195
|
help="Description of additional instruction or path to a file containing additional instructions",
|
|
59
196
|
)
|
|
60
|
-
|
|
197
|
+
run_parser.add_argument(
|
|
61
198
|
"--preserve-source",
|
|
62
199
|
action="store_true",
|
|
63
200
|
help="If set, do not overwrite the original source file; only save modified versions in the runs directory",
|
|
64
201
|
)
|
|
202
|
+
|
|
203
|
+
# --- Logout Command ---
|
|
204
|
+
logout_parser = subparsers.add_parser("logout", help="Log out from Weco and clear saved API key.") # noqa F841
|
|
205
|
+
|
|
65
206
|
args = parser.parse_args()
|
|
66
207
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
208
|
+
# --- Handle Logout Command ---
|
|
209
|
+
if args.command == "logout":
|
|
210
|
+
clear_api_key()
|
|
211
|
+
console.print("[green]Logged out successfully.[/]") # Added feedback
|
|
212
|
+
sys.exit(0)
|
|
213
|
+
|
|
214
|
+
# --- Handle Run Command ---
|
|
215
|
+
elif args.command == "run":
|
|
216
|
+
# --- Check Authentication ---
|
|
217
|
+
weco_api_key = load_weco_api_key()
|
|
218
|
+
llm_api_keys = read_api_keys_from_env() # Read keys from client environment
|
|
219
|
+
|
|
220
|
+
if not weco_api_key:
|
|
221
|
+
login_choice = Prompt.ask(
|
|
222
|
+
"Log in to Weco to save run history or use anonymously? ([bold]L[/]ogin / [bold]S[/]kip)",
|
|
223
|
+
choices=["l", "s"],
|
|
224
|
+
default="s",
|
|
225
|
+
).lower()
|
|
226
|
+
|
|
227
|
+
if login_choice == "l":
|
|
228
|
+
console.print("[cyan]Starting login process...[/]")
|
|
229
|
+
if not perform_login(console):
|
|
230
|
+
console.print("[bold red]Login process failed or was cancelled.[/]")
|
|
231
|
+
sys.exit(1)
|
|
232
|
+
weco_api_key = load_weco_api_key()
|
|
233
|
+
if not weco_api_key:
|
|
234
|
+
console.print("[bold red]Error: Login completed but failed to retrieve API key.[/]")
|
|
235
|
+
sys.exit(1)
|
|
236
|
+
|
|
237
|
+
elif login_choice == "s":
|
|
238
|
+
console.print("[yellow]Proceeding anonymously. LLM API keys must be provided via environment variables.[/]")
|
|
239
|
+
if not llm_api_keys:
|
|
240
|
+
console.print(
|
|
241
|
+
"[bold red]Error:[/] No LLM API keys found in environment (e.g., OPENAI_API_KEY). Cannot proceed anonymously."
|
|
242
|
+
)
|
|
243
|
+
sys.exit(1)
|
|
244
|
+
|
|
245
|
+
# --- Prepare API Call Arguments ---
|
|
246
|
+
auth_headers = {}
|
|
247
|
+
|
|
248
|
+
if weco_api_key:
|
|
249
|
+
auth_headers["Authorization"] = f"Bearer {weco_api_key}"
|
|
250
|
+
# Backend will decide whether to use client keys based on auth status
|
|
251
|
+
|
|
252
|
+
# --- Main Run Logic ---
|
|
253
|
+
try:
|
|
254
|
+
# --- Configuration Loading ---
|
|
70
255
|
evaluation_command = args.eval_command
|
|
71
256
|
metric_name = args.metric
|
|
72
257
|
maximize = args.maximize == "true"
|
|
@@ -74,168 +259,263 @@ def main() -> None:
|
|
|
74
259
|
code_generator_config = {"model": args.model}
|
|
75
260
|
evaluator_config = {"model": args.model}
|
|
76
261
|
search_policy_config = {
|
|
77
|
-
"num_drafts": max(1, math.ceil(0.15 * steps)),
|
|
262
|
+
"num_drafts": max(1, math.ceil(0.15 * steps)),
|
|
78
263
|
"debug_prob": 0.5,
|
|
79
|
-
"max_debug_depth": max(1, math.ceil(0.1 * steps)),
|
|
264
|
+
"max_debug_depth": max(1, math.ceil(0.1 * steps)),
|
|
80
265
|
}
|
|
81
|
-
# Read API keys
|
|
82
|
-
api_keys = read_api_keys_from_env()
|
|
83
|
-
# API request timeout
|
|
84
|
-
timeout = 800
|
|
85
|
-
|
|
86
266
|
# Read additional instructions
|
|
87
267
|
additional_instructions = read_additional_instructions(additional_instructions=args.additional_instructions)
|
|
88
|
-
# Read source code
|
|
268
|
+
# Read source code path
|
|
89
269
|
source_fp = pathlib.Path(args.source)
|
|
270
|
+
# Read source code content
|
|
90
271
|
source_code = read_from_path(fp=source_fp, is_json=False)
|
|
272
|
+
# API request timeout
|
|
273
|
+
timeout = 800
|
|
91
274
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
)
|
|
96
|
-
plan_panel = PlanPanel()
|
|
97
|
-
solution_panels = SolutionPanels(metric_name=metric_name, source_fp=source_fp)
|
|
98
|
-
eval_output_panel = EvaluationOutputPanel()
|
|
99
|
-
tree_panel = MetricTreePanel(maximize=maximize)
|
|
100
|
-
layout = create_optimization_layout()
|
|
101
|
-
end_optimization_layout = create_end_optimization_layout()
|
|
102
|
-
|
|
103
|
-
# Start optimization session
|
|
104
|
-
session_response = start_optimization_session(
|
|
105
|
-
console=console,
|
|
106
|
-
source_code=source_code,
|
|
107
|
-
evaluation_command=evaluation_command,
|
|
108
|
-
metric_name=metric_name,
|
|
109
|
-
maximize=maximize,
|
|
110
|
-
steps=steps,
|
|
111
|
-
code_generator_config=code_generator_config,
|
|
112
|
-
evaluator_config=evaluator_config,
|
|
113
|
-
search_policy_config=search_policy_config,
|
|
114
|
-
additional_instructions=additional_instructions,
|
|
115
|
-
api_keys=api_keys,
|
|
116
|
-
timeout=timeout,
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
# Define the refresh rate
|
|
120
|
-
refresh_rate = 4
|
|
121
|
-
with Live(layout, refresh_per_second=refresh_rate, screen=True) as live:
|
|
122
|
-
# Define the runs directory (.runs/<session-id>)
|
|
123
|
-
session_id = session_response["session_id"]
|
|
124
|
-
runs_dir = pathlib.Path(args.log_dir) / session_id
|
|
125
|
-
runs_dir.mkdir(parents=True, exist_ok=True)
|
|
126
|
-
|
|
127
|
-
# Save the original code (.runs/<session-id>/original.<extension>)
|
|
128
|
-
runs_copy_source_fp = runs_dir / f"original{source_fp.suffix}"
|
|
129
|
-
write_to_path(fp=runs_copy_source_fp, content=source_code)
|
|
130
|
-
|
|
131
|
-
# Write the code string to the source file path
|
|
132
|
-
# Do this after the original code is saved
|
|
133
|
-
if not args.preserve_source:
|
|
134
|
-
write_to_path(fp=source_fp, content=session_response["code"])
|
|
135
|
-
|
|
136
|
-
# Update the panels with the initial solution
|
|
137
|
-
# Add session id now that we have it
|
|
138
|
-
summary_panel.session_id = session_id
|
|
139
|
-
# Set the step of the progress bar
|
|
140
|
-
summary_panel.set_step(step=0)
|
|
141
|
-
# Update the token counts
|
|
142
|
-
summary_panel.update_token_counts(usage=session_response["usage"])
|
|
143
|
-
# Update the plan
|
|
144
|
-
plan_panel.update(plan=session_response["plan"])
|
|
145
|
-
# Build the metric tree
|
|
146
|
-
tree_panel.build_metric_tree(
|
|
147
|
-
nodes=[
|
|
148
|
-
{
|
|
149
|
-
"solution_id": session_response["solution_id"],
|
|
150
|
-
"parent_id": None,
|
|
151
|
-
"code": session_response["code"],
|
|
152
|
-
"step": 0,
|
|
153
|
-
"metric_value": None,
|
|
154
|
-
"is_buggy": False,
|
|
155
|
-
}
|
|
156
|
-
]
|
|
275
|
+
# --- Panel Initialization ---
|
|
276
|
+
summary_panel = SummaryPanel(
|
|
277
|
+
maximize=maximize, metric_name=metric_name, total_steps=steps, model=args.model, runs_dir=args.log_dir
|
|
157
278
|
)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
("tree", tree_panel.get_display()),
|
|
181
|
-
("current_solution", current_solution_panel),
|
|
182
|
-
("best_solution", best_solution_panel),
|
|
183
|
-
("eval_output", eval_output_panel.get_display()),
|
|
184
|
-
],
|
|
185
|
-
transition_delay=0.1, # Slightly longer delay for initial display
|
|
279
|
+
plan_panel = PlanPanel()
|
|
280
|
+
solution_panels = SolutionPanels(metric_name=metric_name, source_fp=source_fp)
|
|
281
|
+
eval_output_panel = EvaluationOutputPanel()
|
|
282
|
+
tree_panel = MetricTreePanel(maximize=maximize)
|
|
283
|
+
layout = create_optimization_layout()
|
|
284
|
+
end_optimization_layout = create_end_optimization_layout()
|
|
285
|
+
|
|
286
|
+
# --- Start Optimization Session ---
|
|
287
|
+
session_response = start_optimization_session(
|
|
288
|
+
console=console,
|
|
289
|
+
source_code=source_code,
|
|
290
|
+
evaluation_command=evaluation_command,
|
|
291
|
+
metric_name=metric_name,
|
|
292
|
+
maximize=maximize,
|
|
293
|
+
steps=steps,
|
|
294
|
+
code_generator_config=code_generator_config,
|
|
295
|
+
evaluator_config=evaluator_config,
|
|
296
|
+
search_policy_config=search_policy_config,
|
|
297
|
+
additional_instructions=additional_instructions,
|
|
298
|
+
api_keys=llm_api_keys, # Pass client LLM keys
|
|
299
|
+
auth_headers=auth_headers, # Pass Weco key if logged in
|
|
300
|
+
timeout=timeout,
|
|
186
301
|
)
|
|
187
302
|
|
|
188
|
-
#
|
|
189
|
-
|
|
303
|
+
# --- Live Update Loop ---
|
|
304
|
+
refresh_rate = 4
|
|
305
|
+
with Live(layout, refresh_per_second=refresh_rate, screen=True) as live:
|
|
306
|
+
# Define the runs directory (.runs/<session-id>)
|
|
307
|
+
session_id = session_response["session_id"]
|
|
308
|
+
runs_dir = pathlib.Path(args.log_dir) / session_id
|
|
309
|
+
runs_dir.mkdir(parents=True, exist_ok=True)
|
|
190
310
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
311
|
+
# Save the original code (.runs/<session-id>/original.<extension>)
|
|
312
|
+
runs_copy_source_fp = runs_dir / f"original{source_fp.suffix}" # Use correct suffix
|
|
313
|
+
write_to_path(fp=runs_copy_source_fp, content=source_code)
|
|
314
|
+
|
|
315
|
+
# Write the initial code string to the source file path (if not preserving)
|
|
316
|
+
if not args.preserve_source:
|
|
317
|
+
write_to_path(fp=source_fp, content=session_response["code"])
|
|
318
|
+
|
|
319
|
+
# Update the panels with the initial solution
|
|
320
|
+
summary_panel.set_session_id(session_id=session_id) # Add session id now that we have it
|
|
321
|
+
# Set the step of the progress bar
|
|
322
|
+
summary_panel.set_step(step=0)
|
|
323
|
+
# Update the token counts
|
|
324
|
+
summary_panel.update_token_counts(usage=session_response["usage"])
|
|
325
|
+
# Update the plan
|
|
326
|
+
plan_panel.update(plan=session_response["plan"])
|
|
327
|
+
# Build the metric tree
|
|
328
|
+
tree_panel.build_metric_tree(
|
|
329
|
+
nodes=[
|
|
330
|
+
{
|
|
331
|
+
"solution_id": session_response["solution_id"],
|
|
332
|
+
"parent_id": None,
|
|
333
|
+
"code": session_response["code"],
|
|
334
|
+
"step": 0,
|
|
335
|
+
"metric_value": None,
|
|
336
|
+
"is_buggy": False,
|
|
337
|
+
}
|
|
338
|
+
]
|
|
339
|
+
)
|
|
340
|
+
# Set the current solution as unevaluated since we haven't run the evaluation function and fed it back to the model yet
|
|
341
|
+
tree_panel.set_unevaluated_node(node_id=session_response["solution_id"])
|
|
342
|
+
# Update the solution panels with the initial solution and get the panel displays
|
|
343
|
+
solution_panels.update(
|
|
344
|
+
current_node=Node(
|
|
345
|
+
id=session_response["solution_id"],
|
|
346
|
+
parent_id=None,
|
|
347
|
+
code=session_response["code"],
|
|
348
|
+
metric=None,
|
|
349
|
+
is_buggy=False,
|
|
350
|
+
),
|
|
351
|
+
best_node=None,
|
|
352
|
+
)
|
|
353
|
+
current_solution_panel, best_solution_panel = solution_panels.get_display(current_step=0)
|
|
354
|
+
# Update the live layout with the initial solution panels
|
|
355
|
+
smooth_update(
|
|
356
|
+
live=live,
|
|
357
|
+
layout=layout,
|
|
358
|
+
sections_to_update=[
|
|
359
|
+
("summary", summary_panel.get_display()),
|
|
360
|
+
("plan", plan_panel.get_display()),
|
|
361
|
+
("tree", tree_panel.get_display(is_done=False)),
|
|
362
|
+
("current_solution", current_solution_panel),
|
|
363
|
+
("best_solution", best_solution_panel),
|
|
364
|
+
("eval_output", eval_output_panel.get_display()),
|
|
365
|
+
],
|
|
366
|
+
transition_delay=0.1,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# Run evaluation on the initial solution
|
|
370
|
+
term_out = run_evaluation(eval_command=args.eval_command)
|
|
371
|
+
|
|
372
|
+
# Update the evaluation output panel
|
|
373
|
+
eval_output_panel.update(output=term_out)
|
|
374
|
+
smooth_update(
|
|
375
|
+
live=live,
|
|
376
|
+
layout=layout,
|
|
377
|
+
sections_to_update=[("eval_output", eval_output_panel.get_display())],
|
|
378
|
+
transition_delay=0.1,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
for step in range(1, steps):
|
|
382
|
+
# Re-read instructions from the original source (file path or string) BEFORE each suggest call
|
|
383
|
+
current_additional_instructions = read_additional_instructions(
|
|
384
|
+
additional_instructions=args.additional_instructions
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# Send feedback and get next suggestion
|
|
388
|
+
eval_and_next_solution_response = evaluate_feedback_then_suggest_next_solution(
|
|
389
|
+
console=console,
|
|
390
|
+
session_id=session_id,
|
|
391
|
+
execution_output=term_out,
|
|
392
|
+
additional_instructions=current_additional_instructions, # Pass current instructions
|
|
393
|
+
api_keys=llm_api_keys, # Pass client LLM keys
|
|
394
|
+
auth_headers=auth_headers, # Pass Weco key if logged in
|
|
395
|
+
timeout=timeout,
|
|
396
|
+
)
|
|
397
|
+
# Save next solution (.runs/<session-id>/step_<step>.<extension>)
|
|
398
|
+
write_to_path(
|
|
399
|
+
fp=runs_dir / f"step_{step}{source_fp.suffix}", content=eval_and_next_solution_response["code"]
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
# Write the next solution to the source file
|
|
403
|
+
if not args.preserve_source:
|
|
404
|
+
write_to_path(fp=source_fp, content=eval_and_next_solution_response["code"])
|
|
405
|
+
|
|
406
|
+
# Get the optimization session status for
|
|
407
|
+
# the best solution, its score, and the history to plot the tree
|
|
408
|
+
status_response = get_optimization_session_status(
|
|
409
|
+
console=console,
|
|
410
|
+
session_id=session_id,
|
|
411
|
+
include_history=True,
|
|
412
|
+
timeout=timeout,
|
|
413
|
+
auth_headers=auth_headers,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
# Update the step of the progress bar
|
|
417
|
+
summary_panel.set_step(step=step)
|
|
418
|
+
# Update the token counts
|
|
419
|
+
summary_panel.update_token_counts(usage=eval_and_next_solution_response["usage"])
|
|
420
|
+
# Update the plan
|
|
421
|
+
plan_panel.update(plan=eval_and_next_solution_response["plan"])
|
|
422
|
+
# Build the metric tree
|
|
423
|
+
tree_panel.build_metric_tree(nodes=status_response["history"])
|
|
424
|
+
# Set the current solution as unevaluated since we haven't run the evaluation function and fed it back to the model yet
|
|
425
|
+
tree_panel.set_unevaluated_node(node_id=eval_and_next_solution_response["solution_id"])
|
|
426
|
+
|
|
427
|
+
# Update the solution panels with the next solution and best solution (and score)
|
|
428
|
+
# Figure out if we have a best solution so far
|
|
429
|
+
if status_response["best_result"] is not None:
|
|
430
|
+
best_solution_node = Node(
|
|
431
|
+
id=status_response["best_result"]["solution_id"],
|
|
432
|
+
parent_id=status_response["best_result"]["parent_id"],
|
|
433
|
+
code=status_response["best_result"]["code"],
|
|
434
|
+
metric=status_response["best_result"]["metric_value"],
|
|
435
|
+
is_buggy=status_response["best_result"]["is_buggy"],
|
|
436
|
+
)
|
|
437
|
+
else:
|
|
438
|
+
best_solution_node = None
|
|
439
|
+
|
|
440
|
+
# Create a node for the current solution
|
|
441
|
+
current_solution_node = None
|
|
442
|
+
for node in status_response["history"]:
|
|
443
|
+
if node["solution_id"] == eval_and_next_solution_response["solution_id"]:
|
|
444
|
+
current_solution_node = Node(
|
|
445
|
+
id=node["solution_id"],
|
|
446
|
+
parent_id=node["parent_id"],
|
|
447
|
+
code=node["code"],
|
|
448
|
+
metric=node["metric_value"],
|
|
449
|
+
is_buggy=node["is_buggy"],
|
|
450
|
+
)
|
|
451
|
+
if current_solution_node is None:
|
|
452
|
+
raise ValueError("Current solution node not found in history")
|
|
453
|
+
# Update the solution panels with the current and best solution
|
|
454
|
+
solution_panels.update(current_node=current_solution_node, best_node=best_solution_node)
|
|
455
|
+
current_solution_panel, best_solution_panel = solution_panels.get_display(current_step=step)
|
|
456
|
+
|
|
457
|
+
# Clear evaluation output since we are running a evaluation on a new solution
|
|
458
|
+
eval_output_panel.clear()
|
|
459
|
+
|
|
460
|
+
# Update displays with smooth transitions
|
|
461
|
+
smooth_update(
|
|
462
|
+
live=live,
|
|
463
|
+
layout=layout,
|
|
464
|
+
sections_to_update=[
|
|
465
|
+
("summary", summary_panel.get_display()),
|
|
466
|
+
("plan", plan_panel.get_display()),
|
|
467
|
+
("tree", tree_panel.get_display(is_done=False)),
|
|
468
|
+
("current_solution", current_solution_panel),
|
|
469
|
+
("best_solution", best_solution_panel),
|
|
470
|
+
("eval_output", eval_output_panel.get_display()),
|
|
471
|
+
],
|
|
472
|
+
transition_delay=0.08, # Slightly longer delay for more noticeable transitions
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
# Run evaluation on the current solution
|
|
476
|
+
term_out = run_evaluation(eval_command=args.eval_command)
|
|
477
|
+
eval_output_panel.update(output=term_out)
|
|
478
|
+
|
|
479
|
+
# Update evaluation output with a smooth transition
|
|
480
|
+
smooth_update(
|
|
481
|
+
live=live,
|
|
482
|
+
layout=layout,
|
|
483
|
+
sections_to_update=[("eval_output", eval_output_panel.get_display())],
|
|
484
|
+
transition_delay=0.1, # Slightly longer delay for evaluation results
|
|
485
|
+
)
|
|
199
486
|
|
|
200
|
-
for step in range(1, steps):
|
|
201
487
|
# Re-read instructions from the original source (file path or string) BEFORE each suggest call
|
|
202
488
|
current_additional_instructions = read_additional_instructions(
|
|
203
489
|
additional_instructions=args.additional_instructions
|
|
204
490
|
)
|
|
205
|
-
|
|
491
|
+
|
|
492
|
+
# Ensure we pass evaluation results for the last step's generated solution
|
|
206
493
|
eval_and_next_solution_response = evaluate_feedback_then_suggest_next_solution(
|
|
207
494
|
console=console,
|
|
208
495
|
session_id=session_id,
|
|
209
496
|
execution_output=term_out,
|
|
210
497
|
additional_instructions=current_additional_instructions,
|
|
211
|
-
api_keys=
|
|
498
|
+
api_keys=llm_api_keys,
|
|
212
499
|
timeout=timeout,
|
|
500
|
+
auth_headers=auth_headers,
|
|
213
501
|
)
|
|
214
|
-
# Save next solution (.runs/<session-id>/step_<step>.<extension>)
|
|
215
|
-
write_to_path(fp=runs_dir / f"step_{step}{source_fp.suffix}", content=eval_and_next_solution_response["code"])
|
|
216
|
-
|
|
217
|
-
# Write the next solution to the source file
|
|
218
|
-
if not args.preserve_source:
|
|
219
|
-
write_to_path(fp=source_fp, content=eval_and_next_solution_response["code"])
|
|
220
502
|
|
|
503
|
+
# Update the progress bar
|
|
504
|
+
summary_panel.set_step(step=steps)
|
|
505
|
+
# Update the token counts
|
|
506
|
+
summary_panel.update_token_counts(usage=eval_and_next_solution_response["usage"])
|
|
507
|
+
# No need to update the plan panel since we have finished the optimization
|
|
221
508
|
# Get the optimization session status for
|
|
222
509
|
# the best solution, its score, and the history to plot the tree
|
|
223
510
|
status_response = get_optimization_session_status(
|
|
224
|
-
console=console, session_id=session_id, include_history=True, timeout=timeout
|
|
511
|
+
console=console, session_id=session_id, include_history=True, timeout=timeout, auth_headers=auth_headers
|
|
225
512
|
)
|
|
226
|
-
|
|
227
|
-
# Update the step of the progress bar
|
|
228
|
-
summary_panel.set_step(step=step)
|
|
229
|
-
# Update the token counts
|
|
230
|
-
summary_panel.update_token_counts(usage=eval_and_next_solution_response["usage"])
|
|
231
|
-
# Update the plan
|
|
232
|
-
plan_panel.update(plan=eval_and_next_solution_response["plan"])
|
|
233
513
|
# Build the metric tree
|
|
234
514
|
tree_panel.build_metric_tree(nodes=status_response["history"])
|
|
235
|
-
#
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
#
|
|
515
|
+
# No need to set any solution to unevaluated since we have finished the optimization
|
|
516
|
+
# and all solutions have been evaluated
|
|
517
|
+
# No neeed to update the current solution panel since we have finished the optimization
|
|
518
|
+
# We only need to update the best solution panel
|
|
239
519
|
# Figure out if we have a best solution so far
|
|
240
520
|
if status_response["best_result"] is not None:
|
|
241
521
|
best_solution_node = Node(
|
|
@@ -247,140 +527,55 @@ def main() -> None:
|
|
|
247
527
|
)
|
|
248
528
|
else:
|
|
249
529
|
best_solution_node = None
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
code=node["code"],
|
|
259
|
-
metric=node["metric_value"],
|
|
260
|
-
is_buggy=node["is_buggy"],
|
|
261
|
-
)
|
|
262
|
-
if current_solution_node is None:
|
|
263
|
-
raise ValueError("Current solution node not found in history")
|
|
264
|
-
# Update the solution panels with the current and best solution
|
|
265
|
-
solution_panels.update(current_node=current_solution_node, best_node=best_solution_node)
|
|
266
|
-
current_solution_panel, best_solution_panel = solution_panels.get_display(current_step=step)
|
|
267
|
-
|
|
268
|
-
# Clear evaluation output since we are running a evaluation on a new solution
|
|
269
|
-
eval_output_panel.clear()
|
|
270
|
-
|
|
271
|
-
# Update displays with smooth transitions
|
|
272
|
-
smooth_update(
|
|
273
|
-
live=live,
|
|
274
|
-
layout=layout,
|
|
275
|
-
sections_to_update=[
|
|
276
|
-
("summary", summary_panel.get_display()),
|
|
277
|
-
("plan", plan_panel.get_display()),
|
|
278
|
-
("tree", tree_panel.get_display()),
|
|
279
|
-
("current_solution", current_solution_panel),
|
|
280
|
-
("best_solution", best_solution_panel),
|
|
281
|
-
("eval_output", eval_output_panel.get_display()),
|
|
282
|
-
],
|
|
283
|
-
transition_delay=0.08, # Slightly longer delay for more noticeable transitions
|
|
530
|
+
solution_panels.update(current_node=None, best_node=best_solution_node)
|
|
531
|
+
_, best_solution_panel = solution_panels.get_display(current_step=steps)
|
|
532
|
+
|
|
533
|
+
# Update the end optimization layout
|
|
534
|
+
final_message = (
|
|
535
|
+
f"{summary_panel.metric_name.capitalize()} {'maximized' if summary_panel.maximize else 'minimized'}! Best solution {summary_panel.metric_name.lower()} = [green]{status_response['best_result']['metric_value']}[/] 🏆"
|
|
536
|
+
if best_solution_node is not None and best_solution_node.metric is not None
|
|
537
|
+
else "[red] No valid solution found.[/]"
|
|
284
538
|
)
|
|
539
|
+
end_optimization_layout["summary"].update(summary_panel.get_display(final_message=final_message))
|
|
540
|
+
end_optimization_layout["tree"].update(tree_panel.get_display(is_done=True))
|
|
541
|
+
end_optimization_layout["best_solution"].update(best_solution_panel)
|
|
542
|
+
|
|
543
|
+
# Save optimization results
|
|
544
|
+
# If the best solution does not exist or is has not been measured at the end of the optimization
|
|
545
|
+
# save the original solution as the best solution
|
|
546
|
+
if best_solution_node is not None:
|
|
547
|
+
best_solution_code = best_solution_node.code
|
|
548
|
+
best_solution_score = best_solution_node.metric
|
|
549
|
+
else:
|
|
550
|
+
best_solution_code = None
|
|
551
|
+
best_solution_score = None
|
|
285
552
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
additional_instructions=args.additional_instructions
|
|
301
|
-
)
|
|
302
|
-
# Ensure we pass evaluation results for the last step's generated solution
|
|
303
|
-
eval_and_next_solution_response = evaluate_feedback_then_suggest_next_solution(
|
|
304
|
-
console=console,
|
|
305
|
-
session_id=session_id,
|
|
306
|
-
execution_output=term_out,
|
|
307
|
-
additional_instructions=current_additional_instructions,
|
|
308
|
-
api_keys=api_keys,
|
|
309
|
-
timeout=timeout,
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
# Update the progress bar
|
|
313
|
-
summary_panel.set_step(step=steps)
|
|
314
|
-
# Update the token counts
|
|
315
|
-
summary_panel.update_token_counts(usage=eval_and_next_solution_response["usage"])
|
|
316
|
-
# No need to update the plan panel since we have finished the optimization
|
|
317
|
-
# Get the optimization session status for
|
|
318
|
-
# the best solution, its score, and the history to plot the tree
|
|
319
|
-
status_response = get_optimization_session_status(
|
|
320
|
-
console=console, session_id=session_id, include_history=True, timeout=timeout
|
|
321
|
-
)
|
|
322
|
-
# Build the metric tree
|
|
323
|
-
tree_panel.build_metric_tree(nodes=status_response["history"])
|
|
324
|
-
# No need to set any solution to unevaluated since we have finished the optimization
|
|
325
|
-
# and all solutions have been evaluated
|
|
326
|
-
# No neeed to update the current solution panel since we have finished the optimization
|
|
327
|
-
# We only need to update the best solution panel
|
|
328
|
-
# Figure out if we have a best solution so far
|
|
329
|
-
if status_response["best_result"] is not None:
|
|
330
|
-
best_solution_node = Node(
|
|
331
|
-
id=status_response["best_result"]["solution_id"],
|
|
332
|
-
parent_id=status_response["best_result"]["parent_id"],
|
|
333
|
-
code=status_response["best_result"]["code"],
|
|
334
|
-
metric=status_response["best_result"]["metric_value"],
|
|
335
|
-
is_buggy=status_response["best_result"]["is_buggy"],
|
|
336
|
-
)
|
|
337
|
-
else:
|
|
338
|
-
best_solution_node = None
|
|
339
|
-
solution_panels.update(current_node=None, best_node=best_solution_node)
|
|
340
|
-
_, best_solution_panel = solution_panels.get_display(current_step=steps)
|
|
341
|
-
|
|
342
|
-
# Update the end optimization layout
|
|
343
|
-
final_message = (
|
|
344
|
-
f"{summary_panel.metric_name.capitalize()} {'maximized' if summary_panel.maximize else 'minimized'}! Best solution {summary_panel.metric_name.lower()} = [green]{status_response['best_result']['metric_value']}[/] 🏆"
|
|
345
|
-
if best_solution_node is not None and best_solution_node.metric is not None
|
|
346
|
-
else "[red] No valid solution found.[/]"
|
|
347
|
-
)
|
|
348
|
-
end_optimization_layout["summary"].update(summary_panel.get_display(final_message=final_message))
|
|
349
|
-
end_optimization_layout["tree"].update(tree_panel.get_display())
|
|
350
|
-
end_optimization_layout["best_solution"].update(best_solution_panel)
|
|
351
|
-
|
|
352
|
-
# Save optimization results
|
|
353
|
-
# If the best solution does not exist or is has not been measured at the end of the optimization
|
|
354
|
-
# save the original solution as the best solution
|
|
355
|
-
if best_solution_node is not None:
|
|
356
|
-
best_solution_code = best_solution_node.code
|
|
357
|
-
best_solution_score = best_solution_node.metric
|
|
358
|
-
else:
|
|
359
|
-
best_solution_code = None
|
|
360
|
-
best_solution_score = None
|
|
361
|
-
|
|
362
|
-
if best_solution_code is None or best_solution_score is None:
|
|
363
|
-
best_solution_content = (
|
|
364
|
-
f"# Weco could not find a better solution\n\n{read_from_path(fp=runs_copy_source_fp, is_json=False)}"
|
|
365
|
-
)
|
|
366
|
-
else:
|
|
367
|
-
# Format score for the comment
|
|
368
|
-
best_score_str = (
|
|
369
|
-
format_number(best_solution_score)
|
|
370
|
-
if best_solution_score is not None and isinstance(best_solution_score, (int, float))
|
|
371
|
-
else "N/A"
|
|
372
|
-
)
|
|
373
|
-
best_solution_content = f"# Best solution from Weco with a score of {best_score_str}\n\n{best_solution_code}"
|
|
553
|
+
if best_solution_code is None or best_solution_score is None:
|
|
554
|
+
best_solution_content = (
|
|
555
|
+
f"# Weco could not find a better solution\n\n{read_from_path(fp=runs_copy_source_fp, is_json=False)}"
|
|
556
|
+
)
|
|
557
|
+
else:
|
|
558
|
+
# Format score for the comment
|
|
559
|
+
best_score_str = (
|
|
560
|
+
format_number(best_solution_score)
|
|
561
|
+
if best_solution_score is not None and isinstance(best_solution_score, (int, float))
|
|
562
|
+
else "N/A"
|
|
563
|
+
)
|
|
564
|
+
best_solution_content = (
|
|
565
|
+
f"# Best solution from Weco with a score of {best_score_str}\n\n{best_solution_code}"
|
|
566
|
+
)
|
|
374
567
|
|
|
375
|
-
|
|
376
|
-
|
|
568
|
+
# Save best solution to .runs/<session-id>/best.<extension>
|
|
569
|
+
write_to_path(fp=runs_dir / f"best{source_fp.suffix}", content=best_solution_content)
|
|
377
570
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
571
|
+
# write the best solution to the source file
|
|
572
|
+
if not args.preserve_source:
|
|
573
|
+
write_to_path(fp=source_fp, content=best_solution_content)
|
|
381
574
|
|
|
382
|
-
|
|
575
|
+
console.print(end_optimization_layout)
|
|
383
576
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
577
|
+
except Exception as e:
|
|
578
|
+
console.print(Panel(f"[bold red]Error: {str(e)}", title="[bold red]Error", border_style="red"))
|
|
579
|
+
# Print traceback for debugging
|
|
580
|
+
console.print_exception(show_locals=True)
|
|
581
|
+
sys.exit(1)
|
weco/panels.py
CHANGED
|
@@ -7,6 +7,7 @@ from rich.syntax import Syntax
|
|
|
7
7
|
from typing import Dict, List, Optional, Union, Tuple
|
|
8
8
|
from .utils import format_number
|
|
9
9
|
import pathlib
|
|
10
|
+
from .__init__ import __dashboard_url__
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class SummaryPanel:
|
|
@@ -22,6 +23,7 @@ class SummaryPanel:
|
|
|
22
23
|
self.model = model
|
|
23
24
|
self.runs_dir = runs_dir
|
|
24
25
|
self.session_id = session_id if session_id is not None else "N/A"
|
|
26
|
+
self.dashboard_url = "N/A"
|
|
25
27
|
self.progress = Progress(
|
|
26
28
|
TextColumn("[progress.description]{task.description}"),
|
|
27
29
|
BarColumn(bar_width=20),
|
|
@@ -32,6 +34,15 @@ class SummaryPanel:
|
|
|
32
34
|
)
|
|
33
35
|
self.task_id = self.progress.add_task("", total=total_steps)
|
|
34
36
|
|
|
37
|
+
def set_session_id(self, session_id: str):
|
|
38
|
+
"""Set the session ID."""
|
|
39
|
+
self.session_id = session_id
|
|
40
|
+
self.set_dashboard_url(session_id=session_id)
|
|
41
|
+
|
|
42
|
+
def set_dashboard_url(self, session_id: str):
|
|
43
|
+
"""Set the dashboard URL."""
|
|
44
|
+
self.dashboard_url = f"{__dashboard_url__}/runs/{session_id}"
|
|
45
|
+
|
|
35
46
|
def set_step(self, step: int):
|
|
36
47
|
"""Set the current step."""
|
|
37
48
|
self.progress.update(self.task_id, completed=step)
|
|
@@ -61,6 +72,9 @@ class SummaryPanel:
|
|
|
61
72
|
# Log directory
|
|
62
73
|
summary_table.add_row(f"[bold cyan]Logs:[/] [blue underline]{self.runs_dir}/{self.session_id}[/]")
|
|
63
74
|
summary_table.add_row("")
|
|
75
|
+
# Dashboard link
|
|
76
|
+
summary_table.add_row(f"[bold cyan]Dashboard:[/] [blue underline]{self.dashboard_url}[/]")
|
|
77
|
+
summary_table.add_row("")
|
|
64
78
|
# Token counts
|
|
65
79
|
summary_table.add_row(
|
|
66
80
|
f"[bold cyan]Tokens:[/] ↑[yellow]{format_number(self.total_input_tokens)}[/] ↓[yellow]{format_number(self.total_output_tokens)}[/] = [green]{format_number(self.total_input_tokens + self.total_output_tokens)}[/]"
|
|
@@ -229,11 +243,15 @@ class MetricTreePanel:
|
|
|
229
243
|
|
|
230
244
|
return tree
|
|
231
245
|
|
|
232
|
-
def get_display(self) -> Panel:
|
|
246
|
+
def get_display(self, is_done: bool) -> Panel:
|
|
233
247
|
"""Get a panel displaying the solution tree."""
|
|
234
248
|
# Make sure the metric tree is built before calling build_rich_tree
|
|
235
249
|
return Panel(
|
|
236
|
-
self._build_rich_tree(),
|
|
250
|
+
self._build_rich_tree(),
|
|
251
|
+
title="[bold]🔎 Exploring Solutions..." if not is_done else "[bold]🔎 Optimization Complete!",
|
|
252
|
+
border_style="green",
|
|
253
|
+
expand=True,
|
|
254
|
+
padding=(0, 1),
|
|
237
255
|
)
|
|
238
256
|
|
|
239
257
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: weco
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.13
|
|
4
4
|
Summary: Documentation for `weco`, a CLI for using Weco AI's code optimizer.
|
|
5
5
|
Author-email: Weco AI Team <contact@weco.ai>
|
|
6
6
|
License: MIT
|
|
@@ -32,7 +32,7 @@ Example applications include:
|
|
|
32
32
|
|
|
33
33
|
- **GPU Kernel Optimization**: Reimplement PyTorch functions using CUDA, Triton or Metal, optimizing for `latency`, `throughput`, or `memory_bandwidth`.
|
|
34
34
|
- **Model Development**: Tune feature transformations or architectures, optimizing for `validation_accuracy`, `AUC`, or `Sharpe Ratio`.
|
|
35
|
-
- **Prompt Engineering**: Refine prompts for LLMs, optimizing for
|
|
35
|
+
- **Prompt Engineering**: Refine prompts for LLMs, optimizing for `win_rate`, `relevance`, or `format_adherence`
|
|
36
36
|
|
|
37
37
|
https://github.com/user-attachments/assets/cb724ef1-bff6-4757-b457-d3b2201ede81
|
|
38
38
|
|
|
@@ -42,7 +42,7 @@ https://github.com/user-attachments/assets/cb724ef1-bff6-4757-b457-d3b2201ede81
|
|
|
42
42
|
|
|
43
43
|
The `weco` CLI leverages a tree search approach guided by Large Language Models (LLMs) to iteratively explore and refine your code. It automatically applies changes, runs your evaluation script, parses the results, and proposes further improvements based on the specified goal.
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
[image](https://github.com/user-attachments/assets/a6ed63fa-9c40-498e-aa98-a873e5786509)
|
|
46
46
|
|
|
47
47
|
---
|
|
48
48
|
|
|
@@ -54,17 +54,38 @@ The `weco` CLI leverages a tree search approach guided by Large Language Models
|
|
|
54
54
|
pip install weco
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
2. **
|
|
57
|
+
2. **Set Up LLM API Keys (Required):**
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
`weco` requires API keys for the Large Language Models (LLMs) it uses internally. You **must** provide these keys via environment variables:
|
|
60
60
|
|
|
61
|
-
-
|
|
62
|
-
-
|
|
63
|
-
-
|
|
61
|
+
- **OpenAI:** `export OPENAI_API_KEY="your_key_here"`
|
|
62
|
+
- **Anthropic:** `export ANTHROPIC_API_KEY="your_key_here"`
|
|
63
|
+
- **Google DeepMind:** `export GEMINI_API_KEY="your_key_here"` (Google AI Studio has a free API usage quota. Create a key [here](https://aistudio.google.com/apikey) to use `weco` for free.)
|
|
64
|
+
|
|
65
|
+
The optimization process will fail if the necessary keys for the chosen model are not found in your environment.
|
|
66
|
+
|
|
67
|
+
3. **Log In to Weco (Optional):**
|
|
68
|
+
|
|
69
|
+
To associate your optimization runs with your Weco account and view them on the Weco dashboard, you can log in. `weco` uses a device authentication flow:
|
|
70
|
+
|
|
71
|
+
- When you first run `weco run`, you'll be prompted if you want to log in or proceed anonymously.
|
|
72
|
+
- If you choose to log in (by pressing `l`), you'll be shown a URL and `weco` will attempt to open it in your default web browser.
|
|
73
|
+
- You then authenticate in the browser. Once authenticated, the CLI will detect this and complete the login.
|
|
74
|
+
- This saves a Weco-specific API key locally (typically at `~/.config/weco/credentials.json`).
|
|
75
|
+
|
|
76
|
+
If you choose to skip login (by pressing Enter or `s`), `weco` will still function using the environment variable LLM keys, but the run history will not be linked to a Weco account.
|
|
77
|
+
|
|
78
|
+
To log out and remove your saved Weco API key, use the `weco logout` command.
|
|
64
79
|
|
|
65
80
|
---
|
|
66
81
|
|
|
67
82
|
## Usage
|
|
83
|
+
|
|
84
|
+
The CLI has two main commands:
|
|
85
|
+
|
|
86
|
+
- `weco run`: Initiates the code optimization process.
|
|
87
|
+
- `weco logout`: Logs you out of your Weco account.
|
|
88
|
+
|
|
68
89
|
<div style="background-color: #fff3cd; border: 1px solid #ffeeba; padding: 15px; border-radius: 4px; margin-bottom: 15px;">
|
|
69
90
|
<strong>⚠️ Warning: Code Modification</strong><br>
|
|
70
91
|
<code>weco</code> directly modifies the file specified by <code>--source</code> during the optimization process. It is <strong>strongly recommended</strong> to use version control (like Git) to track changes and revert if needed. Alternatively, ensure you have a backup of your original file before running the command. Upon completion, the file will contain the best-performing version of the code found during the run.
|
|
@@ -72,7 +93,11 @@ The `weco` CLI leverages a tree search approach guided by Large Language Models
|
|
|
72
93
|
|
|
73
94
|
---
|
|
74
95
|
|
|
75
|
-
###
|
|
96
|
+
### `weco run` Command
|
|
97
|
+
|
|
98
|
+
This command starts the optimization process.
|
|
99
|
+
|
|
100
|
+
**Example: Optimizing Simple PyTorch Operations**
|
|
76
101
|
|
|
77
102
|
This basic example shows how to optimize a simple PyTorch function for speedup.
|
|
78
103
|
|
|
@@ -86,7 +111,7 @@ cd examples/hello-kernel-world
|
|
|
86
111
|
pip install torch
|
|
87
112
|
|
|
88
113
|
# Run Weco
|
|
89
|
-
weco --source optimize.py \
|
|
114
|
+
weco run --source optimize.py \
|
|
90
115
|
--eval-command "python evaluate.py --solution-path optimize.py --device cpu" \
|
|
91
116
|
--metric speedup \
|
|
92
117
|
--maximize true \
|
|
@@ -99,19 +124,29 @@ weco --source optimize.py \
|
|
|
99
124
|
|
|
100
125
|
---
|
|
101
126
|
|
|
102
|
-
|
|
127
|
+
**Arguments for `weco run`:**
|
|
103
128
|
|
|
104
|
-
| Argument | Description
|
|
105
|
-
| :-------------------------- |
|
|
106
|
-
| `--source` | Path to the source code file that will be optimized (e.g., `optimize.py`).
|
|
107
|
-
| `--eval-command` | Command to run for evaluating the code in `--source`. This command should print the target `--metric` and its value to the terminal (stdout/stderr). See note below.
|
|
108
|
-
| `--metric` | The name of the metric you want to optimize (e.g., 'accuracy', 'speedup', 'loss'). This metric name should match what's printed by your `--eval-command`.
|
|
109
|
-
| `--maximize` | Whether to maximize (`true`) or minimize (`false`) the metric.
|
|
110
|
-
| `--steps` | Number of optimization steps (LLM iterations) to run.
|
|
111
|
-
| `--model` | Model identifier for the LLM to use (e.g., `gpt-4o`, `claude-3.
|
|
112
|
-
| `--additional-instructions` | (Optional) Natural language description of specific instructions OR path to a file containing detailed instructions to guide the LLM.
|
|
113
|
-
| `--log-dir` | (Optional) Path to the directory to log intermediate steps and final optimization result. Defaults to `.runs/`.
|
|
114
|
-
| `--preserve-source` | (Optional) If set, do not overwrite the original `--source` file. Modifications and the best solution will still be saved in the `--log-dir`.
|
|
129
|
+
| Argument | Description | Required |
|
|
130
|
+
| :-------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------- |
|
|
131
|
+
| `--source` | Path to the source code file that will be optimized (e.g., `optimize.py`). | Yes |
|
|
132
|
+
| `--eval-command` | Command to run for evaluating the code in `--source`. This command should print the target `--metric` and its value to the terminal (stdout/stderr). See note below. | Yes |
|
|
133
|
+
| `--metric` | The name of the metric you want to optimize (e.g., 'accuracy', 'speedup', 'loss'). This metric name should match what's printed by your `--eval-command`. | Yes |
|
|
134
|
+
| `--maximize` | Whether to maximize (`true`) or minimize (`false`) the metric. | Yes |
|
|
135
|
+
| `--steps` | Number of optimization steps (LLM iterations) to run. | Yes |
|
|
136
|
+
| `--model` | Model identifier for the LLM to use (e.g., `gpt-4o`, `claude-3.5-sonnet`). Recommended models to try include `o3-mini`, `claude-3-haiku`, and `gemini-2.5-pro-exp-03-25`. | Yes |
|
|
137
|
+
| `--additional-instructions` | (Optional) Natural language description of specific instructions OR path to a file containing detailed instructions to guide the LLM. | No |
|
|
138
|
+
| `--log-dir` | (Optional) Path to the directory to log intermediate steps and final optimization result. Defaults to `.runs/`. | No |
|
|
139
|
+
| `--preserve-source` | (Optional) If set, do not overwrite the original `--source` file. Modifications and the best solution will still be saved in the `--log-dir`. | No |
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### `weco logout` Command
|
|
144
|
+
|
|
145
|
+
This command logs you out by removing the locally stored Weco API key.
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
weco logout
|
|
149
|
+
```
|
|
115
150
|
|
|
116
151
|
---
|
|
117
152
|
|
|
@@ -120,6 +155,7 @@ weco --source optimize.py \
|
|
|
120
155
|
Weco, powered by the AIDE algorithm, optimizes code iteratively based on your evaluation results. Achieving significant improvements, especially on complex research-level tasks, often requires substantial exploration time.
|
|
121
156
|
|
|
122
157
|
The following plot from the independent [Research Engineering Benchmark (RE-Bench)](https://metr.org/AI_R_D_Evaluation_Report.pdf) report shows the performance of AIDE (the algorithm behind Weco) on challenging ML research engineering tasks over different time budgets.
|
|
158
|
+
|
|
123
159
|
<p align="center">
|
|
124
160
|
<img src="https://github.com/user-attachments/assets/ff0e471d-2f50-4e2d-b718-874862f533df" alt="RE-Bench Performance Across Time" width="60%"/>
|
|
125
161
|
</p>
|
|
@@ -146,23 +182,25 @@ Final speedup value = 1.5
|
|
|
146
182
|
|
|
147
183
|
Weco will parse this output to extract the numerical value (1.5 in this case) associated with the metric name ('speedup').
|
|
148
184
|
|
|
149
|
-
|
|
150
185
|
## Contributing
|
|
151
186
|
|
|
152
187
|
We welcome contributions! To get started:
|
|
153
188
|
|
|
154
189
|
1. **Fork and Clone the Repository:**
|
|
190
|
+
|
|
155
191
|
```bash
|
|
156
192
|
git clone https://github.com/WecoAI/weco-cli.git
|
|
157
193
|
cd weco-cli
|
|
158
194
|
```
|
|
159
195
|
|
|
160
196
|
2. **Install Development Dependencies:**
|
|
197
|
+
|
|
161
198
|
```bash
|
|
162
199
|
pip install -e ".[dev]"
|
|
163
200
|
```
|
|
164
201
|
|
|
165
202
|
3. **Create a Feature Branch:**
|
|
203
|
+
|
|
166
204
|
```bash
|
|
167
205
|
git checkout -b feature/your-feature-name
|
|
168
206
|
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
weco/__init__.py,sha256=DvJyE-y8upXub4ecCkEZlC5tD5-XonqMe6x8ZtQmnQ0,426
|
|
2
|
+
weco/api.py,sha256=_zgQ4AHDBgqfU2569W63c10qKuIWA5y63XT0qqvUag8,3881
|
|
3
|
+
weco/auth.py,sha256=IPfiLthcNRkPyM8pWHTyDLvikw83sigacpY1PmeA03Y,2343
|
|
4
|
+
weco/cli.py,sha256=rvUqxdJov_R74lkr0Gx-802RNsBYxhLahTue54n2bqc,28967
|
|
5
|
+
weco/panels.py,sha256=gB4rZbCvqzewUCBcILvyyU4fnOQLwFgHCGmtn-ZlgSo,13385
|
|
6
|
+
weco/utils.py,sha256=hhIebUPnetFMfNSFfcsKVw1TSpeu_Zw3rBPPnxDie0U,3911
|
|
7
|
+
weco-0.2.13.dist-info/licenses/LICENSE,sha256=p_GQqJBvuZgkLNboYKyH-5dhpTDlKs2wq2TVM55WrWE,1065
|
|
8
|
+
weco-0.2.13.dist-info/METADATA,sha256=QD0MYkpAfdETUUtQA2PC1HTuvaMf2zeAOEWyqP_lxrk,10851
|
|
9
|
+
weco-0.2.13.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
|
|
10
|
+
weco-0.2.13.dist-info/entry_points.txt,sha256=ixJ2uClALbCpBvnIR6BXMNck8SHAab8eVkM9pIUowcs,39
|
|
11
|
+
weco-0.2.13.dist-info/top_level.txt,sha256=F0N7v6e2zBSlsorFv-arAq2yDxQbzX3KVO8GxYhPUeE,5
|
|
12
|
+
weco-0.2.13.dist-info/RECORD,,
|
weco-0.2.11.dist-info/RECORD
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
weco/__init__.py,sha256=FRQQiNgU5a_qo5d4NNNgMXZ5YvwxlvISipBZeXH4wjc,125
|
|
2
|
-
weco/api.py,sha256=89lB2572jApAxkA0DDppDnJKBwvZTa3kH9jFpC0LFDQ,3313
|
|
3
|
-
weco/cli.py,sha256=fisH7Gvhupu-mgpC4Dmt4x5y0KtVo_o-LjYU_iTymW4,18481
|
|
4
|
-
weco/panels.py,sha256=R_df-VAbWyLoqCA9A6UzbIGZ9sm2IgJO4idnyjmrHQk,12701
|
|
5
|
-
weco/utils.py,sha256=hhIebUPnetFMfNSFfcsKVw1TSpeu_Zw3rBPPnxDie0U,3911
|
|
6
|
-
weco-0.2.11.dist-info/licenses/LICENSE,sha256=p_GQqJBvuZgkLNboYKyH-5dhpTDlKs2wq2TVM55WrWE,1065
|
|
7
|
-
weco-0.2.11.dist-info/METADATA,sha256=XqDYBWUfDcLowdk1Ycib5SKLHOi4vQ8Ogbt6M1go7Wk,9379
|
|
8
|
-
weco-0.2.11.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
|
|
9
|
-
weco-0.2.11.dist-info/entry_points.txt,sha256=ixJ2uClALbCpBvnIR6BXMNck8SHAab8eVkM9pIUowcs,39
|
|
10
|
-
weco-0.2.11.dist-info/top_level.txt,sha256=F0N7v6e2zBSlsorFv-arAq2yDxQbzX3KVO8GxYhPUeE,5
|
|
11
|
-
weco-0.2.11.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|