weco 0.3.2__py3-none-any.whl → 0.3.4__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 +1 -2
- weco/cli.py +14 -3
- weco/optimizer.py +101 -50
- weco/panels.py +26 -0
- weco/utils.py +35 -0
- {weco-0.3.2.dist-info → weco-0.3.4.dist-info}/METADATA +31 -95
- weco-0.3.4.dist-info/RECORD +16 -0
- weco-0.3.2.dist-info/RECORD +0 -16
- {weco-0.3.2.dist-info → weco-0.3.4.dist-info}/WHEEL +0 -0
- {weco-0.3.2.dist-info → weco-0.3.4.dist-info}/entry_points.txt +0 -0
- {weco-0.3.2.dist-info → weco-0.3.4.dist-info}/licenses/LICENSE +0 -0
- {weco-0.3.2.dist-info → weco-0.3.4.dist-info}/top_level.txt +0 -0
weco/api.py
CHANGED
|
@@ -178,7 +178,6 @@ def evaluate_feedback_then_suggest_next_solution(
|
|
|
178
178
|
run_id: str,
|
|
179
179
|
step: int,
|
|
180
180
|
execution_output: str,
|
|
181
|
-
additional_instructions: str = None,
|
|
182
181
|
auth_headers: dict = {},
|
|
183
182
|
timeout: Union[int, Tuple[int, int]] = (10, 3650),
|
|
184
183
|
) -> Dict[str, Any]:
|
|
@@ -189,7 +188,7 @@ def evaluate_feedback_then_suggest_next_solution(
|
|
|
189
188
|
|
|
190
189
|
response = requests.post(
|
|
191
190
|
f"{__base_url__}/runs/{run_id}/suggest",
|
|
192
|
-
json={"execution_output": truncated_output, "
|
|
191
|
+
json={"execution_output": truncated_output, "metadata": {}},
|
|
193
192
|
headers=auth_headers,
|
|
194
193
|
timeout=timeout,
|
|
195
194
|
)
|
weco/cli.py
CHANGED
|
@@ -49,7 +49,7 @@ def configure_run_parser(run_parser: argparse.ArgumentParser) -> None:
|
|
|
49
49
|
"--model",
|
|
50
50
|
type=str,
|
|
51
51
|
default=None,
|
|
52
|
-
help="Model to use for optimization. Defaults to `o4-mini
|
|
52
|
+
help="Model to use for optimization. Defaults to `o4-mini`. See full list at https://docs.weco.ai/cli/supported-models",
|
|
53
53
|
)
|
|
54
54
|
run_parser.add_argument(
|
|
55
55
|
"-l", "--log-dir", type=str, default=".runs", help="Directory to store logs and results. Defaults to `.runs`."
|
|
@@ -72,6 +72,11 @@ def configure_run_parser(run_parser: argparse.ArgumentParser) -> None:
|
|
|
72
72
|
action="store_true",
|
|
73
73
|
help="Save execution output to .runs/<run-id>/outputs/step_<n>.out.txt with JSONL index",
|
|
74
74
|
)
|
|
75
|
+
run_parser.add_argument(
|
|
76
|
+
"--apply-change",
|
|
77
|
+
action="store_true",
|
|
78
|
+
help="Automatically apply the best solution to the source file without prompting",
|
|
79
|
+
)
|
|
75
80
|
|
|
76
81
|
|
|
77
82
|
def configure_credits_parser(credits_parser: argparse.ArgumentParser) -> None:
|
|
@@ -118,6 +123,11 @@ def configure_resume_parser(resume_parser: argparse.ArgumentParser) -> None:
|
|
|
118
123
|
resume_parser.add_argument(
|
|
119
124
|
"run_id", type=str, help="The UUID of the run to resume (e.g., '0002e071-1b67-411f-a514-36947f0c4b31')"
|
|
120
125
|
)
|
|
126
|
+
resume_parser.add_argument(
|
|
127
|
+
"--apply-change",
|
|
128
|
+
action="store_true",
|
|
129
|
+
help="Automatically apply the best solution to the source file without prompting",
|
|
130
|
+
)
|
|
121
131
|
|
|
122
132
|
|
|
123
133
|
def execute_run_command(args: argparse.Namespace) -> None:
|
|
@@ -136,6 +146,7 @@ def execute_run_command(args: argparse.Namespace) -> None:
|
|
|
136
146
|
console=console,
|
|
137
147
|
eval_timeout=args.eval_timeout,
|
|
138
148
|
save_logs=args.save_logs,
|
|
149
|
+
apply_change=args.apply_change,
|
|
139
150
|
)
|
|
140
151
|
exit_code = 0 if success else 1
|
|
141
152
|
sys.exit(exit_code)
|
|
@@ -145,7 +156,7 @@ def execute_resume_command(args: argparse.Namespace) -> None:
|
|
|
145
156
|
"""Execute the 'weco resume' command with all its logic."""
|
|
146
157
|
from .optimizer import resume_optimization
|
|
147
158
|
|
|
148
|
-
success = resume_optimization(run_id=args.run_id, console=console)
|
|
159
|
+
success = resume_optimization(run_id=args.run_id, console=console, apply_change=args.apply_change)
|
|
149
160
|
sys.exit(0 if success else 1)
|
|
150
161
|
|
|
151
162
|
|
|
@@ -164,7 +175,7 @@ def main() -> None:
|
|
|
164
175
|
"--model",
|
|
165
176
|
type=str,
|
|
166
177
|
default=None,
|
|
167
|
-
help="Model to use for optimization. Defaults to `o4-mini
|
|
178
|
+
help="Model to use for optimization. Defaults to `o4-mini`. See full list at docs.weco.ai/cli/supported-models",
|
|
168
179
|
)
|
|
169
180
|
|
|
170
181
|
subparsers = parser.add_subparsers(
|
weco/optimizer.py
CHANGED
|
@@ -30,7 +30,7 @@ from .panels import (
|
|
|
30
30
|
create_optimization_layout,
|
|
31
31
|
create_end_optimization_layout,
|
|
32
32
|
)
|
|
33
|
-
from .utils import read_additional_instructions, read_from_path, write_to_path,
|
|
33
|
+
from .utils import read_additional_instructions, read_from_path, write_to_path, run_evaluation_with_file_swap, smooth_update
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
def save_execution_output(runs_dir: pathlib.Path, step: int, output: str) -> None:
|
|
@@ -134,6 +134,7 @@ def execute_optimization(
|
|
|
134
134
|
console: Optional[Console] = None,
|
|
135
135
|
eval_timeout: Optional[int] = None,
|
|
136
136
|
save_logs: bool = False,
|
|
137
|
+
apply_change: bool = False,
|
|
137
138
|
) -> bool:
|
|
138
139
|
"""
|
|
139
140
|
Execute the core optimization logic.
|
|
@@ -182,6 +183,8 @@ def execute_optimization(
|
|
|
182
183
|
optimization_completed_normally = False
|
|
183
184
|
user_stop_requested_flag = False
|
|
184
185
|
|
|
186
|
+
best_solution_code = None
|
|
187
|
+
original_source_code = None # Make available to the finally block
|
|
185
188
|
try:
|
|
186
189
|
# --- Login/Authentication Handling (now mandatory) ---
|
|
187
190
|
weco_api_key, auth_headers = handle_authentication(console)
|
|
@@ -209,6 +212,7 @@ def execute_optimization(
|
|
|
209
212
|
processed_additional_instructions = read_additional_instructions(additional_instructions=additional_instructions)
|
|
210
213
|
source_fp = pathlib.Path(source)
|
|
211
214
|
source_code = read_from_path(fp=source_fp, is_json=False)
|
|
215
|
+
original_source_code = source_code
|
|
212
216
|
|
|
213
217
|
# --- Panel Initialization ---
|
|
214
218
|
summary_panel = SummaryPanel(maximize=maximize, metric_name=metric, total_steps=steps, model=model, runs_dir=log_dir)
|
|
@@ -272,10 +276,6 @@ def execute_optimization(
|
|
|
272
276
|
}
|
|
273
277
|
with open(jsonl_file, "w", encoding="utf-8") as f:
|
|
274
278
|
f.write(json.dumps(metadata) + "\n")
|
|
275
|
-
# Write the initial code string to the logs
|
|
276
|
-
write_to_path(fp=runs_dir / f"step_0{source_fp.suffix}", content=run_response["code"])
|
|
277
|
-
# Write the initial code string to the source file path
|
|
278
|
-
write_to_path(fp=source_fp, content=run_response["code"])
|
|
279
279
|
|
|
280
280
|
# Update the panels with the initial solution
|
|
281
281
|
# Add run id and run name now that we have it
|
|
@@ -307,6 +307,7 @@ def execute_optimization(
|
|
|
307
307
|
best_node=None,
|
|
308
308
|
)
|
|
309
309
|
current_solution_panel, best_solution_panel = solution_panels.get_display(current_step=0)
|
|
310
|
+
|
|
310
311
|
# Update the live layout with the initial solution panels
|
|
311
312
|
smooth_update(
|
|
312
313
|
live=live,
|
|
@@ -321,8 +322,17 @@ def execute_optimization(
|
|
|
321
322
|
transition_delay=0.1,
|
|
322
323
|
)
|
|
323
324
|
|
|
324
|
-
#
|
|
325
|
-
|
|
325
|
+
# Write the initial code string to the logs
|
|
326
|
+
write_to_path(fp=runs_dir / f"step_0{source_fp.suffix}", content=run_response["code"])
|
|
327
|
+
# Run evaluation on the initial solution (file swap ensures original is restored)
|
|
328
|
+
term_out = run_evaluation_with_file_swap(
|
|
329
|
+
file_path=source_fp,
|
|
330
|
+
new_content=run_response["code"],
|
|
331
|
+
original_content=source_code,
|
|
332
|
+
eval_command=eval_command,
|
|
333
|
+
timeout=eval_timeout,
|
|
334
|
+
)
|
|
335
|
+
|
|
326
336
|
# Save logs if requested
|
|
327
337
|
if save_logs:
|
|
328
338
|
save_execution_output(runs_dir, step=0, output=term_out)
|
|
@@ -337,8 +347,6 @@ def execute_optimization(
|
|
|
337
347
|
|
|
338
348
|
# Starting from step 1 to steps (inclusive) because the baseline solution is step 0, so we want to optimize for steps worth of steps
|
|
339
349
|
for step in range(1, steps + 1):
|
|
340
|
-
# Re-read instructions from the original source (file path or string) BEFORE each suggest call
|
|
341
|
-
current_additional_instructions = read_additional_instructions(additional_instructions=additional_instructions)
|
|
342
350
|
if run_id:
|
|
343
351
|
try:
|
|
344
352
|
current_status_response = get_optimization_run_status(
|
|
@@ -356,17 +364,11 @@ def execute_optimization(
|
|
|
356
364
|
|
|
357
365
|
# Send feedback and get next suggestion
|
|
358
366
|
eval_and_next_solution_response = evaluate_feedback_then_suggest_next_solution(
|
|
359
|
-
console=console,
|
|
360
|
-
step=step,
|
|
361
|
-
run_id=run_id,
|
|
362
|
-
execution_output=term_out,
|
|
363
|
-
additional_instructions=current_additional_instructions,
|
|
364
|
-
auth_headers=auth_headers,
|
|
367
|
+
console=console, step=step, run_id=run_id, execution_output=term_out, auth_headers=auth_headers
|
|
365
368
|
)
|
|
366
369
|
# Save next solution (.runs/<run-id>/step_<step>.<extension>)
|
|
367
370
|
write_to_path(fp=runs_dir / f"step_{step}{source_fp.suffix}", content=eval_and_next_solution_response["code"])
|
|
368
|
-
|
|
369
|
-
write_to_path(fp=source_fp, content=eval_and_next_solution_response["code"])
|
|
371
|
+
|
|
370
372
|
status_response = get_optimization_run_status(
|
|
371
373
|
console=console, run_id=run_id, include_history=True, auth_headers=auth_headers
|
|
372
374
|
)
|
|
@@ -402,7 +404,16 @@ def execute_optimization(
|
|
|
402
404
|
],
|
|
403
405
|
transition_delay=0.08, # Slightly longer delay for more noticeable transitions
|
|
404
406
|
)
|
|
405
|
-
|
|
407
|
+
|
|
408
|
+
# Run evaluation and restore original code after
|
|
409
|
+
term_out = run_evaluation_with_file_swap(
|
|
410
|
+
file_path=source_fp,
|
|
411
|
+
new_content=eval_and_next_solution_response["code"],
|
|
412
|
+
original_content=source_code,
|
|
413
|
+
eval_command=eval_command,
|
|
414
|
+
timeout=eval_timeout,
|
|
415
|
+
)
|
|
416
|
+
|
|
406
417
|
# Save logs if requested
|
|
407
418
|
if save_logs:
|
|
408
419
|
save_execution_output(runs_dir, step=step, output=term_out)
|
|
@@ -415,16 +426,9 @@ def execute_optimization(
|
|
|
415
426
|
)
|
|
416
427
|
|
|
417
428
|
if not user_stop_requested_flag:
|
|
418
|
-
# Re-read instructions from the original source (file path or string) BEFORE each suggest call
|
|
419
|
-
current_additional_instructions = read_additional_instructions(additional_instructions=additional_instructions)
|
|
420
429
|
# Evaluate the final solution thats been generated
|
|
421
430
|
eval_and_next_solution_response = evaluate_feedback_then_suggest_next_solution(
|
|
422
|
-
console=console,
|
|
423
|
-
step=steps,
|
|
424
|
-
run_id=run_id,
|
|
425
|
-
execution_output=term_out,
|
|
426
|
-
additional_instructions=current_additional_instructions,
|
|
427
|
-
auth_headers=auth_headers,
|
|
431
|
+
console=console, step=steps, run_id=run_id, execution_output=term_out, auth_headers=auth_headers
|
|
428
432
|
)
|
|
429
433
|
summary_panel.set_step(step=steps)
|
|
430
434
|
status_response = get_optimization_run_status(
|
|
@@ -458,15 +462,14 @@ def execute_optimization(
|
|
|
458
462
|
# Save optimization results
|
|
459
463
|
# If the best solution does not exist or is has not been measured at the end of the optimization
|
|
460
464
|
# save the original solution as the best solution
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
+
try:
|
|
466
|
+
best_solution_code = best_solution_node.code
|
|
467
|
+
except AttributeError:
|
|
468
|
+
best_solution_code = read_from_path(fp=runs_dir / f"step_0{source_fp.suffix}", is_json=False)
|
|
465
469
|
|
|
466
470
|
# Save best solution to .runs/<run-id>/best.<extension>
|
|
467
|
-
write_to_path(fp=runs_dir / f"best{source_fp.suffix}", content=
|
|
468
|
-
|
|
469
|
-
write_to_path(fp=source_fp, content=best_solution_content)
|
|
471
|
+
write_to_path(fp=runs_dir / f"best{source_fp.suffix}", content=best_solution_code)
|
|
472
|
+
|
|
470
473
|
# Mark as completed normally for the finally block
|
|
471
474
|
optimization_completed_normally = True
|
|
472
475
|
live.update(end_optimization_layout)
|
|
@@ -505,6 +508,21 @@ def execute_optimization(
|
|
|
505
508
|
else "CLI terminated unexpectedly without a specific exception captured."
|
|
506
509
|
)
|
|
507
510
|
|
|
511
|
+
# raise Exception(best_solution_code, original_source_code)
|
|
512
|
+
if best_solution_code and best_solution_code != original_source_code:
|
|
513
|
+
# Determine whether to apply: automatically if --apply-change is set, otherwise ask user
|
|
514
|
+
should_apply = apply_change or summary_panel.ask_user_feedback(
|
|
515
|
+
live=live,
|
|
516
|
+
layout=end_optimization_layout,
|
|
517
|
+
question="Would you like to apply the best solution to the source file?",
|
|
518
|
+
default=True,
|
|
519
|
+
)
|
|
520
|
+
if should_apply:
|
|
521
|
+
write_to_path(fp=source_fp, content=best_solution_code)
|
|
522
|
+
console.print("[green]Best solution applied to the source file.[/]\n")
|
|
523
|
+
else:
|
|
524
|
+
console.print("[green]A better solution was not found. No changes to apply.[/]\n")
|
|
525
|
+
|
|
508
526
|
report_termination(
|
|
509
527
|
run_id=run_id,
|
|
510
528
|
status_update=status,
|
|
@@ -521,7 +539,7 @@ def execute_optimization(
|
|
|
521
539
|
return optimization_completed_normally or user_stop_requested_flag
|
|
522
540
|
|
|
523
541
|
|
|
524
|
-
def resume_optimization(run_id: str, console: Optional[Console] = None) -> bool:
|
|
542
|
+
def resume_optimization(run_id: str, console: Optional[Console] = None, apply_change: bool = False) -> bool:
|
|
525
543
|
"""Resume an interrupted run from the most recent node and continue optimization."""
|
|
526
544
|
if console is None:
|
|
527
545
|
console = Console()
|
|
@@ -557,6 +575,9 @@ def resume_optimization(run_id: str, console: Optional[Console] = None) -> bool:
|
|
|
557
575
|
optimization_completed_normally = False
|
|
558
576
|
user_stop_requested_flag = False
|
|
559
577
|
|
|
578
|
+
best_solution_code = None
|
|
579
|
+
original_source_code = None
|
|
580
|
+
|
|
560
581
|
try:
|
|
561
582
|
# --- Login/Authentication Handling (now mandatory) ---
|
|
562
583
|
weco_api_key, auth_headers = handle_authentication(console)
|
|
@@ -632,11 +653,13 @@ def resume_optimization(run_id: str, console: Optional[Console] = None) -> bool:
|
|
|
632
653
|
log_dir = resume_resp.get("log_dir", ".runs")
|
|
633
654
|
save_logs = bool(resume_resp.get("save_logs", False))
|
|
634
655
|
eval_timeout = resume_resp.get("eval_timeout")
|
|
635
|
-
additional_instructions = resume_resp.get("additional_instructions")
|
|
636
656
|
|
|
637
|
-
#
|
|
657
|
+
# Read the original source code from the file before we start modifying it
|
|
638
658
|
source_fp = pathlib.Path(source_path)
|
|
639
659
|
source_fp.parent.mkdir(parents=True, exist_ok=True)
|
|
660
|
+
# Store the original content to restore after each evaluation
|
|
661
|
+
original_source_code = read_from_path(fp=source_fp, is_json=False) if source_fp.exists() else ""
|
|
662
|
+
|
|
640
663
|
code_to_restore = resume_resp.get("code") or resume_resp.get("source_code") or ""
|
|
641
664
|
write_to_path(fp=source_fp, content=code_to_restore)
|
|
642
665
|
|
|
@@ -704,7 +727,13 @@ def resume_optimization(run_id: str, console: Optional[Console] = None) -> bool:
|
|
|
704
727
|
|
|
705
728
|
# If missing output, evaluate once before first suggest
|
|
706
729
|
if term_out is None or len(term_out.strip()) == 0:
|
|
707
|
-
term_out =
|
|
730
|
+
term_out = run_evaluation_with_file_swap(
|
|
731
|
+
file_path=source_fp,
|
|
732
|
+
new_content=code_to_restore,
|
|
733
|
+
original_content=original_source_code,
|
|
734
|
+
eval_command=eval_command,
|
|
735
|
+
timeout=eval_timeout,
|
|
736
|
+
)
|
|
708
737
|
eval_output_panel.update(output=term_out)
|
|
709
738
|
# Update the evaluation output panel
|
|
710
739
|
smooth_update(
|
|
@@ -739,13 +768,11 @@ def resume_optimization(run_id: str, console: Optional[Console] = None) -> bool:
|
|
|
739
768
|
step=step,
|
|
740
769
|
run_id=resume_resp["run_id"],
|
|
741
770
|
execution_output=term_out,
|
|
742
|
-
additional_instructions=additional_instructions,
|
|
743
771
|
auth_headers=auth_headers,
|
|
744
772
|
)
|
|
745
773
|
|
|
746
|
-
# Save next solution
|
|
774
|
+
# Save next solution to logs
|
|
747
775
|
write_to_path(fp=runs_dir / f"step_{step}{source_fp.suffix}", content=eval_and_next_solution_response["code"])
|
|
748
|
-
write_to_path(fp=source_fp, content=eval_and_next_solution_response["code"])
|
|
749
776
|
|
|
750
777
|
# Refresh status with history and update panels
|
|
751
778
|
status_response = get_optimization_run_status(
|
|
@@ -760,6 +787,7 @@ def resume_optimization(run_id: str, console: Optional[Console] = None) -> bool:
|
|
|
760
787
|
current_solution_node = get_node_from_status(
|
|
761
788
|
status_response=status_response, solution_id=eval_and_next_solution_response["solution_id"]
|
|
762
789
|
)
|
|
790
|
+
|
|
763
791
|
solution_panels.update(current_node=current_solution_node, best_node=best_solution_node)
|
|
764
792
|
current_solution_panel, best_solution_panel = solution_panels.get_display(current_step=step)
|
|
765
793
|
eval_output_panel.clear()
|
|
@@ -776,8 +804,14 @@ def resume_optimization(run_id: str, console: Optional[Console] = None) -> bool:
|
|
|
776
804
|
transition_delay=0.08,
|
|
777
805
|
)
|
|
778
806
|
|
|
779
|
-
# Evaluate this new solution
|
|
780
|
-
term_out =
|
|
807
|
+
# Evaluate this new solution and restore original code after
|
|
808
|
+
term_out = run_evaluation_with_file_swap(
|
|
809
|
+
file_path=source_fp,
|
|
810
|
+
new_content=eval_and_next_solution_response["code"],
|
|
811
|
+
original_content=original_source_code,
|
|
812
|
+
eval_command=eval_command,
|
|
813
|
+
timeout=eval_timeout,
|
|
814
|
+
)
|
|
781
815
|
if save_logs:
|
|
782
816
|
save_execution_output(runs_dir, step=step, output=term_out)
|
|
783
817
|
eval_output_panel.update(output=term_out)
|
|
@@ -795,7 +829,6 @@ def resume_optimization(run_id: str, console: Optional[Console] = None) -> bool:
|
|
|
795
829
|
step=total_steps,
|
|
796
830
|
run_id=resume_resp["run_id"],
|
|
797
831
|
execution_output=term_out,
|
|
798
|
-
additional_instructions=additional_instructions,
|
|
799
832
|
auth_headers=auth_headers,
|
|
800
833
|
)
|
|
801
834
|
summary_panel.set_step(step=total_steps)
|
|
@@ -818,13 +851,13 @@ def resume_optimization(run_id: str, console: Optional[Console] = None) -> bool:
|
|
|
818
851
|
end_optimization_layout["best_solution"].update(best_solution_panel)
|
|
819
852
|
|
|
820
853
|
# Save best
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
854
|
+
try:
|
|
855
|
+
best_solution_code = best_solution_node.code
|
|
856
|
+
except AttributeError:
|
|
857
|
+
best_solution_code = read_from_path(fp=runs_dir / f"step_0{source_fp.suffix}", is_json=False)
|
|
858
|
+
|
|
859
|
+
write_to_path(fp=runs_dir / f"best{source_fp.suffix}", content=best_solution_code)
|
|
825
860
|
|
|
826
|
-
write_to_path(fp=runs_dir / f"best{source_fp.suffix}", content=best_solution_content)
|
|
827
|
-
write_to_path(fp=source_fp, content=best_solution_content)
|
|
828
861
|
optimization_completed_normally = True
|
|
829
862
|
live.update(end_optimization_layout)
|
|
830
863
|
|
|
@@ -843,7 +876,11 @@ def resume_optimization(run_id: str, console: Optional[Console] = None) -> bool:
|
|
|
843
876
|
if heartbeat_thread and heartbeat_thread.is_alive():
|
|
844
877
|
heartbeat_thread.join(timeout=2)
|
|
845
878
|
|
|
846
|
-
|
|
879
|
+
try:
|
|
880
|
+
run_id = resume_resp.get("run_id")
|
|
881
|
+
except Exception:
|
|
882
|
+
run_id = None
|
|
883
|
+
|
|
847
884
|
# Report final status if run exists
|
|
848
885
|
if run_id:
|
|
849
886
|
if optimization_completed_normally:
|
|
@@ -857,6 +894,20 @@ def resume_optimization(run_id: str, console: Optional[Console] = None) -> bool:
|
|
|
857
894
|
if "e" in locals() and isinstance(locals()["e"], Exception)
|
|
858
895
|
else "CLI terminated unexpectedly without a specific exception captured."
|
|
859
896
|
)
|
|
897
|
+
|
|
898
|
+
if best_solution_code and best_solution_code != original_source_code:
|
|
899
|
+
should_apply = apply_change or summary_panel.ask_user_feedback(
|
|
900
|
+
live=live,
|
|
901
|
+
layout=end_optimization_layout,
|
|
902
|
+
question="Would you like to apply the best solution to the source file?",
|
|
903
|
+
default=True,
|
|
904
|
+
)
|
|
905
|
+
if should_apply:
|
|
906
|
+
write_to_path(fp=source_fp, content=best_solution_code)
|
|
907
|
+
console.print("[green]Best solution applied to the source file.[/]\n")
|
|
908
|
+
else:
|
|
909
|
+
console.print("[green]A better solution was not found. No changes to apply.[/]\n")
|
|
910
|
+
|
|
860
911
|
report_termination(
|
|
861
912
|
run_id=run_id,
|
|
862
913
|
status_update=status,
|
weco/panels.py
CHANGED
|
@@ -5,6 +5,10 @@ from rich.layout import Layout
|
|
|
5
5
|
from rich.panel import Panel
|
|
6
6
|
from rich.syntax import Syntax
|
|
7
7
|
from rich import box
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.live import Live
|
|
10
|
+
|
|
11
|
+
from rich.prompt import Confirm
|
|
8
12
|
from typing import Dict, List, Optional, Union, Tuple
|
|
9
13
|
from pathlib import Path
|
|
10
14
|
from .__init__ import __dashboard_url__
|
|
@@ -22,6 +26,7 @@ class SummaryPanel:
|
|
|
22
26
|
runs_dir: str,
|
|
23
27
|
run_id: str = None,
|
|
24
28
|
run_name: str = None,
|
|
29
|
+
console: Optional[Console] = None,
|
|
25
30
|
):
|
|
26
31
|
self.maximize = maximize
|
|
27
32
|
self.metric_name = metric_name
|
|
@@ -32,6 +37,8 @@ class SummaryPanel:
|
|
|
32
37
|
self.run_name = run_name if run_name is not None else "N/A"
|
|
33
38
|
self.dashboard_url = "N/A"
|
|
34
39
|
self.thinking_content = ""
|
|
40
|
+
self.user_input = ""
|
|
41
|
+
self.console = Console()
|
|
35
42
|
self.progress = Progress(
|
|
36
43
|
TextColumn("[progress.description]{task.description}"),
|
|
37
44
|
BarColumn(bar_width=20),
|
|
@@ -67,6 +74,25 @@ class SummaryPanel:
|
|
|
67
74
|
"""Clear the thinking content."""
|
|
68
75
|
self.thinking_content = ""
|
|
69
76
|
|
|
77
|
+
def ask_user_feedback(self, live: Live, layout: Layout, question: str, default: bool = True) -> bool:
|
|
78
|
+
"""
|
|
79
|
+
Ask a yes/no question while keeping the main layout fixed.
|
|
80
|
+
Uses Rich's Confirm for a clean user experience.
|
|
81
|
+
"""
|
|
82
|
+
# Stop live updates temporarily to prevent layout from moving
|
|
83
|
+
live.stop()
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
# Use Rich's built-in Confirm
|
|
87
|
+
result = Confirm.ask(question, default=default)
|
|
88
|
+
except (KeyboardInterrupt, EOFError):
|
|
89
|
+
result = default
|
|
90
|
+
finally:
|
|
91
|
+
# Resume live updates
|
|
92
|
+
live.start()
|
|
93
|
+
|
|
94
|
+
return result
|
|
95
|
+
|
|
70
96
|
def get_display(self, final_message: Optional[str] = None) -> Panel:
|
|
71
97
|
"""Return a Rich panel summarising the current run."""
|
|
72
98
|
# ───────────────────── summary grid ──────────────────────
|
weco/utils.py
CHANGED
|
@@ -106,6 +106,41 @@ def truncate_output(output: str) -> str:
|
|
|
106
106
|
return output
|
|
107
107
|
|
|
108
108
|
|
|
109
|
+
def run_evaluation_with_file_swap(
|
|
110
|
+
file_path: pathlib.Path, new_content: str, original_content: str, eval_command: str, timeout: int | None = None
|
|
111
|
+
) -> str:
|
|
112
|
+
"""
|
|
113
|
+
Temporarily write new content to a file, run evaluation, then restore original.
|
|
114
|
+
|
|
115
|
+
This function ensures the file is always restored to its original state,
|
|
116
|
+
even if an exception occurs during evaluation.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
file_path: Path to the file to temporarily modify
|
|
120
|
+
new_content: The new content to write for evaluation
|
|
121
|
+
original_content: The original content to restore after evaluation
|
|
122
|
+
eval_command: The shell command to run for evaluation
|
|
123
|
+
timeout: Optional timeout for the evaluation command
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
The output from running the evaluation command
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
Any exception raised by run_evaluation will be re-raised after
|
|
130
|
+
the file is restored to its original state.
|
|
131
|
+
"""
|
|
132
|
+
# Write the new content
|
|
133
|
+
write_to_path(fp=file_path, content=new_content)
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
# Run the evaluation
|
|
137
|
+
output = run_evaluation(eval_command=eval_command, timeout=timeout)
|
|
138
|
+
return output
|
|
139
|
+
finally:
|
|
140
|
+
# Always restore the original file, even if evaluation fails
|
|
141
|
+
write_to_path(fp=file_path, content=original_content)
|
|
142
|
+
|
|
143
|
+
|
|
109
144
|
def run_evaluation(eval_command: str, timeout: int | None = None) -> str:
|
|
110
145
|
"""Run the evaluation command on the code and return the output."""
|
|
111
146
|
process = subprocess.Popen(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: weco
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
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:
|
|
@@ -230,7 +230,7 @@ Dynamic: license-file
|
|
|
230
230
|
|
|
231
231
|
<div align="center">
|
|
232
232
|
<img src="assets/weco.svg" alt="Weco Logo" width="120" height="120" style="margin-bottom: 20px;">
|
|
233
|
-
<h1>Weco: The
|
|
233
|
+
<h1>Weco: The Code Optimization Agent</h1>
|
|
234
234
|
</div>
|
|
235
235
|
|
|
236
236
|
[](https://www.python.org)
|
|
@@ -238,7 +238,7 @@ Dynamic: license-file
|
|
|
238
238
|
[](https://docs.weco.ai/)
|
|
239
239
|
[](https://pepy.tech/projects/weco)
|
|
240
240
|
[](https://arxiv.org/abs/2502.13138)
|
|
241
|
-
[](https://colab.research.google.com/github/WecoAI/weco-cli/blob/main/examples/hello-
|
|
241
|
+
[](https://colab.research.google.com/github/WecoAI/weco-cli/blob/main/examples/hello-world/colab_notebook_walkthrough.ipynb)
|
|
242
242
|
|
|
243
243
|
`pip install weco`
|
|
244
244
|
|
|
@@ -262,77 +262,32 @@ Example applications include:
|
|
|
262
262
|
|
|
263
263
|
The `weco` CLI leverages a tree search approach guided by 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.
|
|
264
264
|
|
|
265
|
-

|
|
266
265
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
## Setup
|
|
270
|
-
|
|
271
|
-
1. **Install the Package:**
|
|
272
|
-
|
|
273
|
-
```bash
|
|
274
|
-
pip install weco
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
2. **Authenticate (Required):**
|
|
278
|
-
|
|
279
|
-
`weco` now uses a **credit-based billing system** with centralized LLM access. You need to authenticate to use the service:
|
|
280
|
-
|
|
281
|
-
- **Run the CLI**: `weco` will prompt you to authenticate via your web browser
|
|
282
|
-
- **Free Credits**: New users receive **free credits** upon signup
|
|
283
|
-
- **Centralized Keys**: All LLM provider API keys are managed by Weco (no BYOK required)
|
|
284
|
-
- **Credit Top-ups**: Purchase additional credits through the dashboard at [dashboard.weco.ai](https://dashboard.weco.ai)
|
|
285
|
-
|
|
286
|
-
---
|
|
287
|
-
|
|
288
|
-
## Get Started
|
|
289
|
-
|
|
290
|
-
### Quick Start (Recommended for New Users)
|
|
291
|
-
|
|
292
|
-
The easiest way to get started with Weco is to use the **interactive copilot**. Simply navigate to your project directory and run:
|
|
293
|
-
|
|
294
|
-
```bash
|
|
295
|
-
weco
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
Or specify a project path:
|
|
266
|
+
## Install the Package
|
|
299
267
|
|
|
300
268
|
```bash
|
|
301
|
-
weco
|
|
269
|
+
pip install weco
|
|
302
270
|
```
|
|
303
271
|
|
|
304
|
-
|
|
272
|
+
## Getting Started
|
|
305
273
|
|
|
306
|
-
|
|
307
|
-
2. **Suggest specific optimizations** tailored to your code (e.g., GPU kernel optimization, model improvements, prompt engineering)
|
|
308
|
-
3. **Generate evaluation scripts** automatically or help you configure existing ones
|
|
309
|
-
4. **Set up the complete optimization pipeline** with appropriate metrics and commands
|
|
310
|
-
5. **Run the optimization** or provide you with the exact command to execute
|
|
311
|
-
|
|
312
|
-
<div style="background-color: #fff3cd; border: 1px solid #ffeeba; padding: 15px; border-radius: 4px; margin-bottom: 15px;">
|
|
313
|
-
<strong>⚠️ Warning: Code Modification</strong><br>
|
|
314
|
-
<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.
|
|
315
|
-
</div>
|
|
316
|
-
|
|
317
|
-
### Manual Setup
|
|
274
|
+
### Quickstart with an example project
|
|
318
275
|
|
|
319
276
|
**Configure optimization parameters yourself** - If you need precise control over the optimization parameters, you can use the direct `weco run` command:
|
|
320
277
|
|
|
321
278
|
**Example: Optimizing Simple PyTorch Operations**
|
|
322
279
|
|
|
323
280
|
```bash
|
|
324
|
-
|
|
325
|
-
cd examples/hello-
|
|
326
|
-
|
|
327
|
-
# Install dependencies
|
|
328
|
-
pip install torch
|
|
281
|
+
git clone https://github.com/WecoAI/weco-cli.git
|
|
282
|
+
cd weco-cli/examples/hello-world/
|
|
283
|
+
pip install -r requirements.txt
|
|
329
284
|
|
|
330
|
-
# Run Weco with
|
|
331
|
-
weco run --source
|
|
332
|
-
--eval-command "python evaluate.py --
|
|
285
|
+
# Run Weco with configuration
|
|
286
|
+
weco run --source module.py \
|
|
287
|
+
--eval-command "python evaluate.py --path module.py" \
|
|
333
288
|
--metric speedup \
|
|
334
289
|
--goal maximize \
|
|
335
|
-
--steps
|
|
290
|
+
--steps 10 \
|
|
336
291
|
--additional-instructions "Fuse operations in the forward method while ensuring the max float deviation remains small. Maintain the same format of the code."
|
|
337
292
|
```
|
|
338
293
|
|
|
@@ -362,34 +317,12 @@ For more advanced examples, including [Triton](/examples/triton/README.md), [CUD
|
|
|
362
317
|
| Argument | Description | Default | Example |
|
|
363
318
|
| :----------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------ |
|
|
364
319
|
| `-n, --steps` | Number of optimization steps (LLM iterations) to run. | 100 | `-n 50` |
|
|
365
|
-
| `-M, --model` | Model identifier for the LLM to use (e.g., `o4-mini`, `claude-sonnet-4-
|
|
320
|
+
| `-M, --model` | Model identifier for the LLM to use (e.g., `o4-mini`, `claude-sonnet-4-5`, `gpt-5`). | `o4-mini` | `-M o4-mini` |
|
|
366
321
|
| `-i, --additional-instructions`| Natural language description of specific instructions **or** path to a file containing detailed instructions to guide the LLM. Supported file formats include - `.txt`, `.md`, and `.rst`. | `None` | `-i instructions.md` or `-i "Optimize the model for faster inference"`|
|
|
367
322
|
| `-l, --log-dir` | Path to the directory to log intermediate steps and final optimization result. | `.runs/` | `-l ./logs/` |
|
|
368
323
|
| `--eval-timeout` | Timeout in seconds for each step in evaluation. | No timeout (unlimited) | `--eval-timeout 3600` |
|
|
369
324
|
| `--save-logs` | Save execution output from each optimization step to disk. Creates timestamped directories with raw output files and a JSONL index for tracking execution history. | `False` | `--save-logs` |
|
|
370
|
-
|
|
371
|
-
---
|
|
372
|
-
|
|
373
|
-
### Authentication & Dashboard
|
|
374
|
-
|
|
375
|
-
The CLI requires a Weco account for authentication and billing.
|
|
376
|
-
|
|
377
|
-
#### Credit-Based Authentication (Required)
|
|
378
|
-
Weco now requires authentication for all operations. This enables our credit-based billing system and provides access to powerful optimizations:
|
|
379
|
-
|
|
380
|
-
1. **During onboarding**: When you run `weco` for the first time, you'll be prompted to log in
|
|
381
|
-
2. **Manual login**: Use `weco logout` to clear credentials, then run `weco` again to re-authenticate
|
|
382
|
-
3. **Device flow**: Weco will open your browser automatically and guide you through a secure OAuth-style authentication
|
|
383
|
-
|
|
384
|
-

|
|
385
|
-
|
|
386
|
-
**Benefits:**
|
|
387
|
-
- **No API Key Management**: All LLM provider keys are managed centrally
|
|
388
|
-
- **Cost Transparency**: See exactly how many credits each optimization consumes
|
|
389
|
-
- **Free Trial**: Free credits to get started with optimization projects
|
|
390
|
-
- **Run History**: View all your optimization runs on the Weco dashboard
|
|
391
|
-
- **Progress Tracking**: Monitor long-running optimizations remotely
|
|
392
|
-
- **Budget Control**: Set spending limits and auto top-up preferences
|
|
325
|
+
| `--apply-change` | Automatically apply the best solution to the source file without prompting. | `False` | `--apply-change` |
|
|
393
326
|
|
|
394
327
|
---
|
|
395
328
|
|
|
@@ -399,8 +332,6 @@ Weco now requires authentication for all operations. This enables our credit-bas
|
|
|
399
332
|
|
|
400
333
|
| Command | Description | When to Use |
|
|
401
334
|
|---------|-------------|-------------|
|
|
402
|
-
| `weco` | Launch interactive onboarding | **Recommended for beginners** - Analyzes your codebase and guides you through setup |
|
|
403
|
-
| `weco /path/to/project` | Launch onboarding for specific project | When working with a project in a different directory |
|
|
404
335
|
| `weco run [options]` | Direct optimization execution | **For advanced users** - When you know exactly what to optimize and how |
|
|
405
336
|
| `weco resume <run-id>` | Resume an interrupted run | Continue from the last completed step |
|
|
406
337
|
| `weco logout` | Clear authentication credentials | To switch accounts or troubleshoot authentication issues |
|
|
@@ -410,19 +341,23 @@ Weco now requires authentication for all operations. This enables our credit-bas
|
|
|
410
341
|
You can specify which LLM model to use with the `-M` or `--model` flag:
|
|
411
342
|
|
|
412
343
|
```bash
|
|
413
|
-
|
|
414
|
-
weco --model gpt-4o
|
|
415
|
-
|
|
416
|
-
# Use with direct execution
|
|
417
|
-
weco run --model claude-3.5-sonnet --source optimize.py [other options...]
|
|
344
|
+
weco run --model gpt-5 --source optimize.py [other options...]
|
|
418
345
|
```
|
|
419
346
|
|
|
420
|
-
**Available models:**
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
- `
|
|
347
|
+
**Available models (30 total):**
|
|
348
|
+
|
|
349
|
+
**OpenAI Models:**
|
|
350
|
+
- GPT-5 Series: `gpt-5.1`, `gpt-5.1-codex`, `gpt-5.1-codex-mini`, `gpt-5-codex`, `gpt-5-pro`, `gpt-5`, `gpt-5-mini`, `gpt-5-nano`
|
|
351
|
+
- O-Series Reasoning: `o3-pro`, `o3`, `o3-mini`, `o4-mini`, `o1-pro`, `o1`, `codex-mini-latest`
|
|
352
|
+
- GPT-4 Series: `gpt-4.1`, `gpt-4.1-mini`, `gpt-4.1-nano`, `gpt-4o`, `gpt-4o-mini`
|
|
353
|
+
|
|
354
|
+
**Anthropic Claude (via Vertex AI):**
|
|
355
|
+
- `claude-opus-4-5`, `claude-opus-4-1`, `claude-opus-4`, `claude-sonnet-4-5`, `claude-sonnet-4`, `claude-haiku-4-5`
|
|
356
|
+
|
|
357
|
+
**Google Gemini:**
|
|
358
|
+
- `gemini-3-pro-preview`, `gemini-2.5-pro`, `gemini-2.5-flash`, `gemini-2.5-flash-lite`
|
|
424
359
|
|
|
425
|
-
All models are available through Weco
|
|
360
|
+
All models are available through Weco. If no model is specified, Weco automatically selects the best model for your optimization task.
|
|
426
361
|
|
|
427
362
|
---
|
|
428
363
|
|
|
@@ -441,6 +376,7 @@ Arguments for `weco resume`:
|
|
|
441
376
|
| Argument | Description | Example |
|
|
442
377
|
|----------|-------------|---------|
|
|
443
378
|
| `run-id` | The UUID of the run to resume (shown at the start of each run) | `0002e071-1b67-411f-a514-36947f0c4b31` |
|
|
379
|
+
| `--apply-change` | Automatically apply the best solution to the source file without prompting | `--apply-change` |
|
|
444
380
|
|
|
445
381
|
Notes:
|
|
446
382
|
- Works only for interrupted runs (status: `error`, `terminated`, etc.).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
weco/__init__.py,sha256=ClO0uT6GKOA0iSptvP0xbtdycf0VpoPTq37jHtvlhtw,303
|
|
2
|
+
weco/api.py,sha256=zKcI4riwruK6CjV_vcL8RlsJGRXO40iP0WxeETtzPIY,18430
|
|
3
|
+
weco/auth.py,sha256=O31Hoj-Loi8DWJJG2LfeWgUMuNqAUeGDpd2ZGjA9Ah0,9997
|
|
4
|
+
weco/chatbot.py,sha256=EIK2WaOul9gn_yHLThjsZV7RnE8t3XQPwgRkO5tybSU,38415
|
|
5
|
+
weco/cli.py,sha256=7Q6ZwkD8N2x_PRovaFkF9OvtKkKX3hs_CobvS8xKXkQ,11535
|
|
6
|
+
weco/constants.py,sha256=V6yFugTznKm5EC2_jr4I_whd7sqI80HiPggRn0az580,406
|
|
7
|
+
weco/credits.py,sha256=C08x-TRcLg3ccfKqMGNRY7zBn7t3r7LZ119bxgfztaI,7629
|
|
8
|
+
weco/optimizer.py,sha256=fvxpKHeET84vIBWEM_50iCInbxMYs0e44sJqe7zVzK0,43848
|
|
9
|
+
weco/panels.py,sha256=OLss2PLgJqdo9w-Gq0TozyE42lsJvgjtqmfyCHpHGtA,16966
|
|
10
|
+
weco/utils.py,sha256=SMFh7ngo6gsjb2g20wpPe0O0T8xR7U_7X4-kqxJHkGc,8308
|
|
11
|
+
weco-0.3.4.dist-info/licenses/LICENSE,sha256=9LUfoGHjLPtak2zps2kL2tm65HAZIICx_FbLaRuS4KU,11337
|
|
12
|
+
weco-0.3.4.dist-info/METADATA,sha256=TGIKIIAl_0xQ9UBIjr3opE45ni_ZImzhLe29aBWQo3Q,29244
|
|
13
|
+
weco-0.3.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
+
weco-0.3.4.dist-info/entry_points.txt,sha256=ixJ2uClALbCpBvnIR6BXMNck8SHAab8eVkM9pIUowcs,39
|
|
15
|
+
weco-0.3.4.dist-info/top_level.txt,sha256=F0N7v6e2zBSlsorFv-arAq2yDxQbzX3KVO8GxYhPUeE,5
|
|
16
|
+
weco-0.3.4.dist-info/RECORD,,
|
weco-0.3.2.dist-info/RECORD
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
weco/__init__.py,sha256=ClO0uT6GKOA0iSptvP0xbtdycf0VpoPTq37jHtvlhtw,303
|
|
2
|
-
weco/api.py,sha256=dUjzuOKKvayzZ_1B4j40eK9Ofk264jsc6vOR1afsszY,18523
|
|
3
|
-
weco/auth.py,sha256=O31Hoj-Loi8DWJJG2LfeWgUMuNqAUeGDpd2ZGjA9Ah0,9997
|
|
4
|
-
weco/chatbot.py,sha256=EIK2WaOul9gn_yHLThjsZV7RnE8t3XQPwgRkO5tybSU,38415
|
|
5
|
-
weco/cli.py,sha256=579f6jf-ZWuFAmNXDisRY7zWr7vw2YZQuC_QX8-qxx0,11460
|
|
6
|
-
weco/constants.py,sha256=V6yFugTznKm5EC2_jr4I_whd7sqI80HiPggRn0az580,406
|
|
7
|
-
weco/credits.py,sha256=C08x-TRcLg3ccfKqMGNRY7zBn7t3r7LZ119bxgfztaI,7629
|
|
8
|
-
weco/optimizer.py,sha256=mJU8_0bo_6dS2PEj1E3dQHvNH9V4e8NSLNE55tmvspw,42291
|
|
9
|
-
weco/panels.py,sha256=fnGPtmvxpx21AuBCtCFu1f_BpSxybNr2lhjIIKIutrY,16133
|
|
10
|
-
weco/utils.py,sha256=erDDrA_g3KSlel6YEAGALlV_k8ftT-VQnPT1BrmzK8k,7021
|
|
11
|
-
weco-0.3.2.dist-info/licenses/LICENSE,sha256=9LUfoGHjLPtak2zps2kL2tm65HAZIICx_FbLaRuS4KU,11337
|
|
12
|
-
weco-0.3.2.dist-info/METADATA,sha256=TDJIvT1vw3VFrjEj9o8VkLuxis2MWWAL0pnDYqpFfak,31878
|
|
13
|
-
weco-0.3.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
-
weco-0.3.2.dist-info/entry_points.txt,sha256=ixJ2uClALbCpBvnIR6BXMNck8SHAab8eVkM9pIUowcs,39
|
|
15
|
-
weco-0.3.2.dist-info/top_level.txt,sha256=F0N7v6e2zBSlsorFv-arAq2yDxQbzX3KVO8GxYhPUeE,5
|
|
16
|
-
weco-0.3.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|