easyevolve 0.1.0b2__tar.gz

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.
@@ -0,0 +1,12 @@
1
+ .env
2
+ .venv/
3
+ __pycache__/
4
+ *.py[cod]
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ *.egg-info/
8
+ dist/
9
+ .data/
10
+ *.db
11
+ .DS_Store
12
+ node_modules/
@@ -0,0 +1,87 @@
1
+ Metadata-Version: 2.4
2
+ Name: easyevolve
3
+ Version: 0.1.0b2
4
+ Summary: Give us an evaluator. We'll evolve the code.
5
+ Project-URL: Homepage, https://easyevolve.dev
6
+ Project-URL: Documentation, https://easyevolve.dev/docs
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
11
+ Requires-Python: >=3.11
12
+ Requires-Dist: httpx<1,>=0.28
13
+ Requires-Dist: pydantic<3,>=2.10
14
+ Description-Content-Type: text/markdown
15
+
16
+ # EasyEvolve
17
+
18
+ **Give us an evaluator. We'll evolve the code.**
19
+
20
+ EasyEvolve is a hosted API that improves Python code with LLM-driven evolutionary search.
21
+ You write a function that scores a candidate program; the service generates, mutates and
22
+ evaluates candidates in isolated sandboxes and returns the best one.
23
+
24
+ > **Invite-only beta.** You need an invite token from the EasyEvolve team.
25
+ > The API, pricing and limits may change, and data may be reset during the beta.
26
+
27
+ ## Install
28
+
29
+ ```sh
30
+ pip install --pre easyevolve
31
+ ```
32
+
33
+ Python 3.11 or later.
34
+
35
+ ## Sign in with your invite token
36
+
37
+ ```sh
38
+ easyevolve login
39
+ ```
40
+
41
+ Paste the token when prompted. It is stored at `~/.config/easyevolve/credentials.json`
42
+ with owner-only permissions. You can also set `EASYEVOLVE_TOKEN` instead.
43
+
44
+ ## Write an evaluator
45
+
46
+ Expose `evaluate(candidate_path: str) -> dict` returning a finite `score` (higher is better).
47
+ Use the Python standard library only.
48
+
49
+ ```python
50
+ # eval.py
51
+ """Candidate contract: solve(values: list[int]) -> list[int], sorted ascending."""
52
+ import importlib.util
53
+
54
+
55
+ def evaluate(candidate_path: str) -> dict:
56
+ spec = importlib.util.spec_from_file_location("candidate", candidate_path)
57
+ candidate = importlib.util.module_from_spec(spec)
58
+ spec.loader.exec_module(candidate)
59
+ cases = [[], [1], [3, 1, 2], [5, -1, 5, 0], list(range(30, 0, -1))]
60
+ passed = sum(candidate.solve(values.copy()) == sorted(values) for values in cases)
61
+ return {"score": passed / len(cases)}
62
+ ```
63
+
64
+ ## Run
65
+
66
+ ```python
67
+ from easyevolve import EasyEvolve
68
+
69
+ with EasyEvolve(evaluator="eval.py") as client:
70
+ print(client.estimate(iterations=5, level="low", budget=0.5))
71
+ result = client.run(iterations=5, level="low", budget=0.5)
72
+ print(result.best_score, result.cost)
73
+ print(result.best_code)
74
+ ```
75
+
76
+ Or from the terminal:
77
+
78
+ ```sh
79
+ easyevolve balance
80
+ easyevolve run eval.py --iterations 5 --level low --budget 0.5
81
+ ```
82
+
83
+ `budget` (USD) is a hard cap: the run stops before it would exceed it, and unused budget
84
+ returns to your balance. A client timeout does not stop the job; use
85
+ `client.resume(job_id)` to reconnect or `easyevolve cancel <job_id>` to stop it.
86
+
87
+ Documentation: <https://easyevolve.dev/docs>
@@ -0,0 +1,72 @@
1
+ # EasyEvolve
2
+
3
+ **Give us an evaluator. We'll evolve the code.**
4
+
5
+ EasyEvolve is a hosted API that improves Python code with LLM-driven evolutionary search.
6
+ You write a function that scores a candidate program; the service generates, mutates and
7
+ evaluates candidates in isolated sandboxes and returns the best one.
8
+
9
+ > **Invite-only beta.** You need an invite token from the EasyEvolve team.
10
+ > The API, pricing and limits may change, and data may be reset during the beta.
11
+
12
+ ## Install
13
+
14
+ ```sh
15
+ pip install --pre easyevolve
16
+ ```
17
+
18
+ Python 3.11 or later.
19
+
20
+ ## Sign in with your invite token
21
+
22
+ ```sh
23
+ easyevolve login
24
+ ```
25
+
26
+ Paste the token when prompted. It is stored at `~/.config/easyevolve/credentials.json`
27
+ with owner-only permissions. You can also set `EASYEVOLVE_TOKEN` instead.
28
+
29
+ ## Write an evaluator
30
+
31
+ Expose `evaluate(candidate_path: str) -> dict` returning a finite `score` (higher is better).
32
+ Use the Python standard library only.
33
+
34
+ ```python
35
+ # eval.py
36
+ """Candidate contract: solve(values: list[int]) -> list[int], sorted ascending."""
37
+ import importlib.util
38
+
39
+
40
+ def evaluate(candidate_path: str) -> dict:
41
+ spec = importlib.util.spec_from_file_location("candidate", candidate_path)
42
+ candidate = importlib.util.module_from_spec(spec)
43
+ spec.loader.exec_module(candidate)
44
+ cases = [[], [1], [3, 1, 2], [5, -1, 5, 0], list(range(30, 0, -1))]
45
+ passed = sum(candidate.solve(values.copy()) == sorted(values) for values in cases)
46
+ return {"score": passed / len(cases)}
47
+ ```
48
+
49
+ ## Run
50
+
51
+ ```python
52
+ from easyevolve import EasyEvolve
53
+
54
+ with EasyEvolve(evaluator="eval.py") as client:
55
+ print(client.estimate(iterations=5, level="low", budget=0.5))
56
+ result = client.run(iterations=5, level="low", budget=0.5)
57
+ print(result.best_score, result.cost)
58
+ print(result.best_code)
59
+ ```
60
+
61
+ Or from the terminal:
62
+
63
+ ```sh
64
+ easyevolve balance
65
+ easyevolve run eval.py --iterations 5 --level low --budget 0.5
66
+ ```
67
+
68
+ `budget` (USD) is a hard cap: the run stops before it would exceed it, and unused budget
69
+ returns to your balance. A client timeout does not stop the job; use
70
+ `client.resume(job_id)` to reconnect or `easyevolve cancel <job_id>` to stop it.
71
+
72
+ Documentation: <https://easyevolve.dev/docs>
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "easyevolve"
7
+ version = "0.1.0b2"
8
+ description = "Give us an evaluator. We'll evolve the code."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ dependencies = ["httpx>=0.28,<1", "pydantic>=2.10,<3"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Intended Audience :: Developers",
15
+ "Programming Language :: Python :: 3",
16
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
17
+ ]
18
+
19
+ [project.urls]
20
+ Homepage = "https://easyevolve.dev"
21
+ Documentation = "https://easyevolve.dev/docs"
22
+
23
+ [project.scripts]
24
+ easyevolve = "easyevolve.cli:main"
25
+
26
+ # The SDK source lives in ../src so the service and SDK stay one tested codebase.
27
+ # Metadata 2.4 is accepted by every current uploader and index.
28
+ [tool.hatch.build.targets.sdist]
29
+ core-metadata-version = "2.4"
30
+
31
+ [tool.hatch.build.targets.sdist.force-include]
32
+ "../src/easyevolve" = "src/easyevolve"
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src/easyevolve"]
36
+ core-metadata-version = "2.4"
@@ -0,0 +1,3 @@
1
+ from .client import EasyEvolve, EasyEvolveError, EvolutionResult
2
+
3
+ __all__ = ["EasyEvolve", "EasyEvolveError", "EvolutionResult"]
@@ -0,0 +1,58 @@
1
+ import argparse
2
+ import getpass
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ from .client import EasyEvolve, EasyEvolveError
7
+
8
+
9
+ def main():
10
+ try:
11
+ run_command()
12
+ except EasyEvolveError as exc:
13
+ job = f" (job {exc.result.id})" if exc.result else ""
14
+ sys.exit(f"Error{job}: {exc}")
15
+
16
+
17
+ def run_command():
18
+ parser = argparse.ArgumentParser(description="EasyEvolve hosted code evolution")
19
+ parser.add_argument("--api-url")
20
+ commands = parser.add_subparsers(dest="command", required=True)
21
+ run = commands.add_parser("run")
22
+ run.add_argument("evaluator")
23
+ run.add_argument("--task")
24
+ run.add_argument("--budget", default="2.00")
25
+ run.add_argument("--iterations", type=int, default=100)
26
+ run.add_argument("--level", choices=["low", "middle", "high"], default="middle")
27
+ run.add_argument("--output", default="best.py")
28
+ resume = commands.add_parser("resume")
29
+ resume.add_argument("job_id")
30
+ resume.add_argument("--output", default="best.py")
31
+ cancel = commands.add_parser("cancel")
32
+ cancel.add_argument("job_id")
33
+ commands.add_parser("balance")
34
+ commands.add_parser("login", help="Save an invite token (read from stdin or a prompt)")
35
+ args = parser.parse_args()
36
+ if args.command == "login":
37
+ token = sys.stdin.readline() if not sys.stdin.isatty() else getpass.getpass("Token: ")
38
+ with EasyEvolve(base_url=args.api_url) as client:
39
+ available = client.login(token)
40
+ print(f"Saved token. Available credit: ${available}")
41
+ return
42
+ with EasyEvolve(
43
+ getattr(args, "evaluator", None), task=getattr(args, "task", None), base_url=args.api_url
44
+ ) as client:
45
+ if args.command == "balance":
46
+ print(f"Available credit: ${client.balance()}")
47
+ return
48
+ if args.command == "cancel":
49
+ print(client.cancel(args.job_id))
50
+ return
51
+ result = (
52
+ client.run(iterations=args.iterations, level=args.level, budget=args.budget)
53
+ if args.command == "run"
54
+ else client.resume(args.job_id)
55
+ )
56
+ if result.best_code is not None:
57
+ Path(args.output).write_text(result.best_code)
58
+ print(f"{result.status}: best score={result.best_score}, cost=${result.cost}")
@@ -0,0 +1,255 @@
1
+ import os
2
+ import secrets
3
+ import time
4
+ import uuid
5
+ import webbrowser
6
+ from collections.abc import Callable
7
+ from decimal import ROUND_CEILING, Decimal
8
+ from pathlib import Path
9
+ from urllib.parse import urlparse
10
+
11
+ import httpx
12
+ from pydantic import BaseModel
13
+
14
+ from .credentials import Credentials
15
+
16
+ TERMINAL = {"COMPLETED", "FAILED", "CANCELLED", "BUDGET_EXHAUSTED"}
17
+
18
+
19
+ class EvolutionResult(BaseModel):
20
+ id: str
21
+ status: str
22
+ best_code: str | None
23
+ best_score: float | None
24
+ initial_score: float | None
25
+ cost: Decimal
26
+ budget: Decimal
27
+ completed_iterations: int
28
+ error: str | None = None
29
+ history: list[dict] = []
30
+
31
+
32
+ class EasyEvolveError(RuntimeError):
33
+ def __init__(
34
+ self, message: str, status_code: int | None = None, result: EvolutionResult | None = None
35
+ ):
36
+ super().__init__(message)
37
+ self.status_code = status_code
38
+ self.result = result
39
+
40
+
41
+ class EasyEvolve:
42
+ def __init__(
43
+ self,
44
+ evaluator: str | Path | None = None,
45
+ *,
46
+ task: str | None = None,
47
+ initial_code: str | None = None,
48
+ base_url: str | None = None,
49
+ token: str | None = None,
50
+ credentials_path: str | Path | None = None,
51
+ poll_interval: float = 2.0,
52
+ client: httpx.Client | None = None,
53
+ ):
54
+ self.evaluator = Path(evaluator) if evaluator is not None else None
55
+ self.task = task
56
+ self.initial_code = initial_code
57
+ self.base_url = (
58
+ base_url or os.environ.get("EASYEVOLVE_API_URL") or "https://api.easyevolve.dev"
59
+ ).rstrip("/")
60
+ parsed = urlparse(self.base_url)
61
+ if parsed.scheme != "https" and not (
62
+ parsed.scheme == "http" and parsed.hostname in {"localhost", "127.0.0.1", "::1"}
63
+ ):
64
+ raise ValueError("Use HTTPS, or HTTP on localhost for development")
65
+ if poll_interval < 0:
66
+ raise ValueError("poll_interval must be nonnegative")
67
+ self.credentials = Credentials(self.base_url, credentials_path)
68
+ self.token = (
69
+ token or os.environ.get("EASYEVOLVE_TOKEN") or self.credentials.read().get("token")
70
+ )
71
+ self.poll_interval = poll_interval
72
+ self.client = client or httpx.Client(base_url=self.base_url, timeout=30)
73
+ self.owns_client = client is None
74
+ self.job_id: str | None = None
75
+
76
+ def __enter__(self):
77
+ return self
78
+
79
+ def __exit__(self, *_):
80
+ self.close()
81
+
82
+ def close(self):
83
+ if self.owns_client:
84
+ self.client.close()
85
+
86
+ def request(self, method: str, path: str, **kwargs) -> dict:
87
+ headers = kwargs.pop("headers", {})
88
+ if self.token:
89
+ headers["Authorization"] = f"Bearer {self.token}"
90
+ try:
91
+ response = self.client.request(method, path, headers=headers, **kwargs)
92
+ except httpx.HTTPError as exc:
93
+ raise EasyEvolveError(
94
+ f"API connection failed. Resume job {self.job_id} if one was created."
95
+ ) from exc
96
+ if response.is_error:
97
+ try:
98
+ detail = response.json().get("detail", "API request failed")
99
+ except ValueError:
100
+ detail = "API request failed"
101
+ raise EasyEvolveError(str(detail), response.status_code)
102
+ return response.json() if response.content else {}
103
+
104
+ def estimate(self, *, iterations: int = 100, level: str = "middle", budget=2.0) -> dict:
105
+ return self.request(
106
+ "POST",
107
+ "/v1/estimate",
108
+ json={
109
+ "iterations": iterations,
110
+ "level": level,
111
+ "budget": str(budget),
112
+ },
113
+ )
114
+
115
+ def balance(self) -> Decimal:
116
+ return Decimal(self.request("GET", "/v1/billing/balance")["available"])
117
+
118
+ def purchase_credits(self, amount=5.0, *, open_browser: bool = True, timeout: float = 900):
119
+ stored = self.credentials.read()
120
+ pending = stored.get("pending_checkout")
121
+ if pending is None:
122
+ pending = self.request("POST", "/v1/auth/checkout", json={"amount": str(amount)})
123
+ pending["device_token"] = "ee_" + secrets.token_urlsafe(32)
124
+ self.credentials.write({**stored, "pending_checkout": pending})
125
+ print(f"Payment required. Complete Stripe Checkout:\n{pending['url']}")
126
+ if open_browser:
127
+ webbrowser.open(pending["url"])
128
+ deadline = time.monotonic() + timeout
129
+ while time.monotonic() < deadline:
130
+ claimed = self.request(
131
+ "POST",
132
+ f"/v1/auth/checkout/{pending['checkout_id']}/claim",
133
+ json={
134
+ "claim_token": pending["claim_token"],
135
+ "device_token": pending["device_token"],
136
+ },
137
+ )
138
+ if claimed["status"] == "paid":
139
+ self.token = claimed["token"]
140
+ self.credentials.write({"token": self.token})
141
+ return
142
+ time.sleep(max(self.poll_interval, 0.1))
143
+ raise EasyEvolveError(
144
+ "Checkout timed out. Call purchase_credits again to resume this checkout."
145
+ )
146
+
147
+ def submit(
148
+ self,
149
+ *,
150
+ iterations: int = 100,
151
+ level: str = "middle",
152
+ budget=2.0,
153
+ idempotency_key: str | None = None,
154
+ ) -> str:
155
+ if self.evaluator is None:
156
+ raise ValueError("evaluator is required to submit a new job")
157
+ body = {
158
+ "evaluator": self.evaluator.read_text(encoding="utf-8"),
159
+ "task": self.task,
160
+ "initial_code": self.initial_code,
161
+ "iterations": iterations,
162
+ "level": level,
163
+ "budget": str(budget),
164
+ }
165
+ key = idempotency_key or str(uuid.uuid4())
166
+ # POST is safe to retry with the same key if the response was lost.
167
+ for attempt in range(2):
168
+ try:
169
+ job = self.request("POST", "/v1/jobs", json=body, headers={"Idempotency-Key": key})
170
+ self.job_id = job["id"]
171
+ print(f"Job ID: {self.job_id}")
172
+ return self.job_id
173
+ except EasyEvolveError as exc:
174
+ if exc.status_code is not None or attempt:
175
+ raise
176
+ raise AssertionError("Unreachable")
177
+
178
+ def progress(self, job_id: str) -> dict:
179
+ return self.request("GET", f"/v1/jobs/{job_id}")
180
+
181
+ def result(self, job_id: str) -> EvolutionResult:
182
+ return EvolutionResult.model_validate(self.request("GET", f"/v1/jobs/{job_id}/result"))
183
+
184
+ def cancel(self, job_id: str | None = None) -> dict:
185
+ job_id = job_id or self.job_id
186
+ if not job_id:
187
+ raise ValueError("job_id is required")
188
+ return self.request("POST", f"/v1/jobs/{job_id}/cancel")
189
+
190
+ def resume(
191
+ self,
192
+ job_id: str,
193
+ *,
194
+ timeout: float | None = None,
195
+ on_progress: Callable[[dict], None] | None = None,
196
+ ) -> EvolutionResult:
197
+ self.job_id = job_id
198
+ start = time.monotonic()
199
+ last = None
200
+ while True:
201
+ job = self.progress(job_id)
202
+ if job != last and on_progress:
203
+ on_progress(job)
204
+ last = job
205
+ if job["status"] in TERMINAL:
206
+ result = self.result(job_id)
207
+ if result.status == "FAILED":
208
+ raise EasyEvolveError(result.error or "Evolution failed", result=result)
209
+ return result
210
+ if timeout is not None and time.monotonic() - start >= timeout:
211
+ raise TimeoutError(f"Job {job_id} is still running; reconnect with resume(job_id)")
212
+ time.sleep(self.poll_interval)
213
+
214
+ def run(
215
+ self,
216
+ *,
217
+ iterations: int = 100,
218
+ level: str = "middle",
219
+ budget=2.0,
220
+ auto_checkout: bool = True,
221
+ timeout: float | None = None,
222
+ on_progress: Callable[[dict], None] | None = None,
223
+ ) -> EvolutionResult:
224
+ estimate = self.estimate(iterations=iterations, level=level, budget=budget)
225
+ print(f"Estimated cap: ${estimate['estimated_cost']}\nMaximum budget: ${budget}")
226
+ if auto_checkout and (self.token is None or self.balance() < Decimal(str(budget))):
227
+ amount = max(Decimal("5"), Decimal(str(budget))).quantize(
228
+ Decimal("0.01"), rounding=ROUND_CEILING
229
+ )
230
+ self.purchase_credits(amount)
231
+ job_id = self.submit(iterations=iterations, level=level, budget=budget)
232
+
233
+ def display(job):
234
+ print(
235
+ f"{job['status']} | Iteration {job['completed_iterations']}/{job['iterations']} "
236
+ f"| Best score: {job['best_score']} | Cost: ${job['cost']}"
237
+ )
238
+
239
+ return self.resume(job_id, timeout=timeout, on_progress=on_progress or display)
240
+
241
+ def login(self, token: str) -> Decimal:
242
+ """Verify an invite token and store it for this API origin."""
243
+ self.token = token.strip()
244
+ available = self.balance()
245
+ self.credentials.write({"token": self.token})
246
+ return available
247
+
248
+ def rotate_token(self):
249
+ self.token = self.request("POST", "/v1/auth/token/rotate")["token"]
250
+ self.credentials.write({"token": self.token})
251
+
252
+ def revoke_token(self):
253
+ self.request("POST", "/v1/auth/token/revoke")
254
+ self.token = None
255
+ self.credentials.write({})
@@ -0,0 +1,35 @@
1
+ import json
2
+ import os
3
+ import tempfile
4
+ from pathlib import Path
5
+
6
+
7
+ class Credentials:
8
+ def __init__(self, base_url: str, path: str | Path | None = None):
9
+ self.url = base_url.rstrip("/")
10
+ self.path = (
11
+ Path(path)
12
+ if path
13
+ else Path(os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config")))
14
+ / "easyevolve"
15
+ / "credentials.json"
16
+ )
17
+
18
+ def read(self) -> dict:
19
+ if not self.path.exists():
20
+ return {}
21
+ return json.loads(self.path.read_text()).get(self.url, {})
22
+
23
+ def write(self, data: dict):
24
+ self.path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
25
+ entries = json.loads(self.path.read_text()) if self.path.exists() else {}
26
+ entries[self.url] = data
27
+ fd, temporary = tempfile.mkstemp(dir=self.path.parent)
28
+ try:
29
+ os.fchmod(fd, 0o600)
30
+ with os.fdopen(fd, "w") as output:
31
+ json.dump(entries, output)
32
+ os.replace(temporary, self.path)
33
+ finally:
34
+ if os.path.exists(temporary):
35
+ os.unlink(temporary)