Skip to content
Merged
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
119 changes: 116 additions & 3 deletions examples/alphaevolve_interop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,120 @@ 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 is
unchanged. The example runner can use either helper as described below.

## Run with imported configuration from the command line

The runner accepts one of `--config` (OE YAML), `--alphaevolve-config`
(experiment JSON) or `--alphaevolve-env` (an explicitly named `.env`). The two
AlphaEvolve options **only validate the configuration and evaluate the seed by
default**. Add `--evolve` to generate candidates using the selected endpoint.
The legacy `--config` option still starts evolution, with its existing default
of one iteration.

For `.env`, install the optional parser above and prepare a copy such as
`./local.env`. Set `MODEL` or the numbered model names to models actually served
by your OE endpoint; importing a Gemini name does not make it available locally.
Provide a problem-text file explicitly rather than running the upstream
entrypoint to extract it:

```bash
# Import/validate and evaluate the original TSP seed; no model requests.
python examples/alphaevolve_interop/run.py ../alphaevolve-reference/examples/tsp \
--alphaevolve-env ./local.env \
--instructions ../alphaevolve-reference/examples/tsp/instructions.md \
--api-base http://localhost:8000/v1

# Explicit model-driven evolution; may incur charges at the selected endpoint.
python examples/alphaevolve_interop/run.py ../alphaevolve-reference/examples/tsp \
--alphaevolve-env ./local.env \
--instructions ../alphaevolve-reference/examples/tsp/instructions.md \
--api-base http://localhost:8000/v1 \
--evolve --iterations 1 --output-dir ./interop-output
```

Alternatively, save an experiment dictionary (including `program_language`,
`problem_description`, `run_settings` and `generation_settings`) as JSON:

```bash
python examples/alphaevolve_interop/run.py ../alphaevolve-reference/examples/circle_packing \
--alphaevolve-config ./experiment.json --api-base http://localhost:8000/v1 \
--evolve --iterations 1 --output-dir ./interop-output
```

Both imports require an explicit `--api-base`. To select a key without putting
its value in command arguments, add `--api-key-env OE_API_KEY` after setting
that environment variable yourself. An explicitly selected missing/empty key
variable is an error. If this option is omitted, the existing OE provider's
key behavior (including `OPENAI_API_KEY` fallback) applies when a client is
constructed. Nothing is copied from Google credential fields in the source.

With `--evolve`, omitting `--iterations` uses the imported generation budget.
An explicit positive `--iterations` may only reduce that budget. As above,
generation attempts are not a strict request/evaluation/cost cap. `--iterations`
without an evolution mode is rejected rather than ignored. `--instructions`
is required only for `.env`; JSON already contains the problem description.
Invalid option combinations and import errors are reported before loading
the example evaluator. The original cloud `run_evolution.py` is not imported.

## Validation

Expand All @@ -150,6 +261,8 @@ 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
python -m unittest discover -s tests -p test_alphaevolve_runner.py
```

The walkthrough additionally permits direct validation against an upstream
Expand Down
88 changes: 78 additions & 10 deletions examples/alphaevolve_interop/run.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""Evaluate an unchanged AlphaEvolve example; optionally evolve it with an OE config."""
"""Evaluate an unchanged AlphaEvolve example; explicitly opt in to model-driven evolution."""

import argparse
import importlib.util
import json
import os
from pathlib import Path

from openevolve import run_evolution
from openevolve.compat.alphaevolve import adapt_evaluator
from openevolve.compat.alphaevolve import adapt_evaluator, config_from_alphaevolve

EXAMPLES = {
"circle_packing": ("circle_packing_evaluation", "sum_of_radii"),
Expand All @@ -30,34 +31,101 @@ def load_evaluator(example_dir):
return adapt_evaluator(getattr(module, function_name), objective_metric=objective)


def main():
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("example_dir", type=Path, help="Path to the upstream example directory")
sources = parser.add_mutually_exclusive_group()
sources.add_argument(
"--config", type=Path, help="OE YAML config; starts evolution (legacy mode)"
)
sources.add_argument("--alphaevolve-config", type=Path, help="AlphaEvolve experiment JSON")
sources.add_argument("--alphaevolve-env", type=Path, help="Explicit AlphaEvolve .env file")
parser.add_argument(
"--instructions", type=Path, help="Problem text file; required with --alphaevolve-env"
)
parser.add_argument("--api-base", help="Explicit OE model endpoint for AlphaEvolve imports")
parser.add_argument(
"--api-key-env", help="Read the OE API key from this named environment variable"
)
parser.add_argument(
"--config", type=Path, help="OE YAML config; omit to evaluate only (no LLM)"
"--evolve",
action="store_true",
help="Run imported AlphaEvolve config using the chosen model (may incur charges)",
)
parser.add_argument(
"--iterations", type=int, default=1, help="Evolution iterations with --config"
"--iterations",
type=int,
help="Positive generation attempts; may only reduce an imported budget (legacy --config default: 1)",
)
parser.add_argument("--output-dir", type=Path, default=Path("alphaevolve_output"))
args = parser.parse_args()
args = parser.parse_args(argv)
example_dir = args.example_dir.resolve()
if example_dir.name not in EXAMPLES:
parser.error(f"Supported example names: {', '.join(EXAMPLES)}")
if args.iterations < 1:
imported = args.alphaevolve_config is not None or args.alphaevolve_env is not None
evolving = args.config is not None or args.evolve
if args.evolve and not (args.config or imported):
parser.error("--evolve requires a configuration source")
if args.iterations is not None and args.iterations < 1:
parser.error("--iterations must be positive")
if args.iterations is not None and not evolving:
parser.error("--iterations requires --evolve or legacy --config")
if not imported and (
args.api_base is not None or args.api_key_env is not None or args.instructions is not None
):
parser.error("--api-base, --api-key-env and --instructions require an AlphaEvolve source")
if imported and not args.api_base:
parser.error("--api-base is required for AlphaEvolve imports")
if args.alphaevolve_env is not None and args.instructions is None:
parser.error("--instructions is required with --alphaevolve-env")
if args.instructions is not None and args.alphaevolve_env is None:
parser.error(
"--instructions is only used with --alphaevolve-env; JSON includes problem_description"
)

config = args.config
if imported:
api_key = None
if args.api_key_env is not None:
api_key = os.environ.get(args.api_key_env)
if not api_key or not api_key.strip():
parser.error("--api-key-env must name a non-empty environment variable")
try:
if args.alphaevolve_env is not None:
from openevolve.compat.alphaevolve_env import config_from_alphaevolve_env

config = config_from_alphaevolve_env(
args.alphaevolve_env,
problem_description=args.instructions.read_text(encoding="utf-8"),
api_base=args.api_base,
api_key=api_key,
)
else:
config = config_from_alphaevolve(
json.loads(args.alphaevolve_config.read_text(encoding="utf-8-sig")),
api_base=args.api_base,
api_key=api_key,
)
except (ValueError, OSError, ImportError) as exc:
parser.error(str(exc))
if args.iterations is not None and args.iterations > config.max_iterations:
parser.error("--iterations cannot exceed the imported generation budget")

program_path = example_dir / "src" / "program.py"
for path in (program_path, example_dir / "src" / "evaluate.py"):
if not path.is_file():
parser.error(f"Required example file not found: {path}")
evaluator = load_evaluator(example_dir)
if args.config is None:
if not evolving:
result = evaluator(program_path)
print(json.dumps({"metrics": result.metrics, "artifacts": result.artifacts}, indent=2))
return

result = run_evolution(
initial_program=program_path,
evaluator=evaluator,
config=args.config,
iterations=args.iterations,
config=config,
iterations=args.iterations if imported else (args.iterations or 1),
output_dir=str(args.output_dir),
cleanup=True,
)
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
Loading
Loading