Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 61 additions & 3 deletions examples/alphaevolve_interop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,66 @@ equivalent OE controls; OE's worker count stays at its default of one unless
you configure it separately. Model-specific extra fields also warn and are not
forwarded. Warning messages include field paths, not their values.

This is a subset of inbound configuration support (A3 in #468), not `.env`
translation, an experiment/controller shim, or a cloud-compatible export.
Use the returned `Config` directly with the Python API.
The dictionary helper provides a subset of inbound configuration support (A3
in #468). It does not read `.env` files, emulate the experiment/controller SDK,
or export cloud-compatible bundles. Use the returned `Config` directly with
the Python API; explicit `.env` import is described below.

## Import an explicit example .env file

Install the optional parser (the base OpenEvolve install is unchanged):

```bash
python -m pip install "openevolve[alphaevolve-env]"
# From a source checkout: python -m pip install -e ".[alphaevolve-env]"
```

```python
from pathlib import Path
from openevolve.compat.alphaevolve_env import config_from_alphaevolve_env

config = config_from_alphaevolve_env(
"../alphaevolve-reference/examples/tsp/example.env",
problem_description=Path(
"../alphaevolve-reference/examples/tsp/instructions.md"
).read_text(encoding="utf-8"),
api_base="http://localhost:8000/v1",
api_key="local-placeholder",
)
# Pass config to run_evolution with the original seed and adapted evaluator.
# Importing alone does not construct clients or call the endpoint.
```

Only the named file is read. The helper does not find a default `.env`, read
other environment variables to fill missing settings, interpolate variable
references, change `os.environ`, or execute shell commands or the original
`run_evolution.py`. The problem text and OE endpoint/key remain explicit caller
arguments. A missing `api_key` still allows OE's usual provider behavior when
a model client is later constructed, as described above.

| .env setting | Mapping / behavior |
| --- | --- |
| `MODEL`, optional `MODEL_WEIGHT` | Single model; missing weight becomes 1 |
| `MODEL_1..N`, optional `MODEL_i_WEIGHT` | Weighted model list in numeric index order; gaps allowed |
| `MAX_PROGRAMS_GENERATED` | Required positive integer mapped to `max_iterations` |
| `MAX_PROGRAMS_EVALUATED` | If present, must equal the generation budget; always warns that it is not a hard evaluation cap |
| Other non-null variables, including cloud credentials/endpoints and concurrency | Not imported; warnings contain names, not values |

Either model form may be used, but not both. Empty names, orphan weights,
non-positive/non-finite weights, duplicate assignments, malformed syntax and
variable/command references in model names raise errors. Quoting, comments,
`export` prefixes and multiline quoted values are parsed with `python-dotenv`;
expansion is intentionally disabled. No model names are guessed or remapped.
Some upstream examples, including signal processing, do not declare models in
their `example.env`: add an explicit model or use the dictionary helper instead.

An evaluation-only budget, or different generation and evaluation budgets,
is rejected rather than translated into an allegedly equivalent limit. Even
equal budgets are not equivalent: OE counts generation attempts, additionally
evaluates the seed, and may retry. This helper does not impose a strict
evaluation, request, token, time or cost limit. The existing dictionary helper
performs the final configuration validation; existing config loading and the
example command-line runner are unchanged.

## Validation

Expand All @@ -150,6 +207,7 @@ callable-serialization path, without Google's SDK, keys, or network access:
```bash
python -m unittest discover -s tests -p test_alphaevolve_compat.py
python -m unittest discover -s tests -p test_alphaevolve_config.py
python -m unittest discover -s tests -p test_alphaevolve_env.py
```

The walkthrough additionally permits direct validation against an upstream
Expand Down
130 changes: 130 additions & 0 deletions openevolve/compat/alphaevolve_env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
"""Opt-in .env configuration import for local Python AlphaEvolve examples."""

import math
import re
import warnings
from pathlib import Path
from typing import Optional, Union

from openevolve.compat.alphaevolve import _warn_unmapped_fields, config_from_alphaevolve
from openevolve.config import Config


def _read_env(path: Union[str, Path]) -> dict:
try:
from dotenv.parser import parse_stream
except ImportError:
raise ImportError(
'AlphaEvolve .env import requires: pip install "openevolve[alphaevolve-env]"'
) from None

values = {}
# Use dotenv's parser bindings so malformed lines and duplicate assignments
# can fail explicitly, without interpolation, global environment changes,
# implicit file discovery, or logging source lines that may contain secrets.
with Path(path).open(encoding="utf-8-sig") as stream:
for binding in parse_stream(stream):
if binding.error:
raise ValueError(f"Invalid .env syntax near line {binding.original.line}")
key = binding.key
if key is None:
continue
if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", key):
raise ValueError(f"Invalid .env variable name near line {binding.original.line}")
if key in values:
raise ValueError(f"Duplicate .env assignment near line {binding.original.line}")
values[key] = binding.value
return values


def _positive_integer(values: dict, key: str) -> int:
value = values.get(key)
if value is None or not re.fullmatch(r"[0-9]+", value) or int(value) < 1:
raise ValueError(f"{key} must be an explicit positive integer")
return int(value)


def config_from_alphaevolve_env(
env_path: Union[str, Path],
*,
problem_description: str,
api_base: str,
api_key: Optional[str] = None,
) -> Config:
"""Import explicit .env model/budget settings using the dictionary converter.

Requires the optional alphaevolve-env extra. Reads only env_path; does not
search for other files, interpolate variables, modify os.environ, execute
shell/script code, or construct clients. The caller provides the problem text
(e.g. instructions.md) and OE endpoint/key. Cloud credentials are not copied.

Accept either MODEL with optional MODEL_WEIGHT, or MODEL_1..N with optional
MODEL_i_WEIGHT (positive indices, numeric order, gaps allowed). Mixing the
forms, missing model names, malformed/duplicate assignments, and variable or
command references in model names are rejected. Missing weights become 1.

MAX_PROGRAMS_GENERATED is required and maps to OE generation attempts. A
present MAX_PROGRAMS_EVALUATED must be equal, otherwise import fails rather
than discarding a different limit. Even equal limits warn: OE also evaluates
the seed and may retry, so this is not a hard evaluation/request/cost cap.
Other variables, including concurrency and GCP settings, warn by name only.
"""
values = _read_env(env_path)
indices = set()
for key in values:
match = re.fullmatch(r"MODEL_([0-9]+)(?:_WEIGHT)?", key)
if match:
index = match.group(1)
if index.startswith("0"):
raise ValueError("MODEL indices must be positive integers without leading zeros")
indices.add(int(index))
if indices and ("MODEL" in values or "MODEL_WEIGHT" in values):
raise ValueError("Use MODEL or numbered MODEL_i variables, not both")
model_keys = [f"MODEL_{i}" for i in sorted(indices)] if indices else ["MODEL"]
supported = {"MAX_PROGRAMS_GENERATED", "MAX_PROGRAMS_EVALUATED"}
models = []
for key in model_keys:
name = values.get(key)
if not name or not name.strip() or "$" in name or "`" in name:
raise ValueError(f"{key} must contain a literal, non-empty model name")
weight_key = f"{key}_WEIGHT"
weight = 1.0
if weight_key in values:
try:
weight = float(values[weight_key])
except (TypeError, ValueError, OverflowError):
raise ValueError(f"{weight_key} must be finite and positive") from None
if not math.isfinite(weight) or weight <= 0:
raise ValueError(f"{weight_key} must be finite and positive")
models.append({"name": name, "weight": weight})
supported.update({key, weight_key})

iterations = _positive_integer(values, "MAX_PROGRAMS_GENERATED")
if "MAX_PROGRAMS_EVALUATED" in values:
evaluated = _positive_integer(values, "MAX_PROGRAMS_EVALUATED")
if evaluated != iterations:
raise ValueError(
"MAX_PROGRAMS_EVALUATED differs from MAX_PROGRAMS_GENERATED; "
"separate generation/evaluation limits cannot be enforced by this import"
)

config = config_from_alphaevolve(
{
"program_language": "python",
"problem_description": problem_description,
"run_settings": {"max_programs": iterations},
"generation_settings": {"models": models},
},
api_base=api_base,
api_key=api_key,
)
if "MAX_PROGRAMS_EVALUATED" in values:
warnings.warn(
"MAX_PROGRAMS_EVALUATED is not enforced as a hard cap: "
"MAX_PROGRAMS_GENERATED maps to OE generation attempts, "
"plus a seed evaluation and possible retries",
UserWarning,
stacklevel=2,
)
_warn_unmapped_fields(values, supported)
return config
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ dependencies = [
]

[project.optional-dependencies]
alphaevolve-env = ["python-dotenv>=1.0,<2"]
dev = [
"pytest>=7.0.0",
"pytest-asyncio>=0.21.0",
Expand All @@ -29,6 +30,7 @@ dev = [
"mypy>=0.950",
"requests>=2.28.0",
"pre-commit>=4.5.1",
"python-dotenv>=1.0,<2",
]

[tool.black]
Expand Down
Loading
Loading