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
90 changes: 90 additions & 0 deletions examples/alphaevolve_interop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,96 @@ This closes the local evaluator-type/import gap, not the complete SDK shim or
the original-entrypoint acceptance criterion for #468. It neither enables cloud
execution nor changes the facade's budget, concurrency or best-only query limits.

## Run the original circle-packing or TSP entrypoint

`run_entrypoint.py` executes the upstream `src/run_evolution.py`'s original
`main()` for **circle_packing** and **tsp** with explicit local connection/model
settings. It stages byte-identical copies of `src/` and optional
`instructions.md`, imports their relative helpers under temporary local aliases,
and swaps imported client/experiment factories and configuration globals. It
does not rewrite the source, replace the evaluator, or contact Google's SDK.

From an installed OE checkout, with a reviewed upstream checkout as above:

```bash
python -m pip install numpy scipy 'matplotlib>=3.9.1,<3.11' nest-asyncio python-dotenv

# Run the original TSP main, including its seed scoring and report generation.
# No generation requests. The output directory must be new and outside the example.
MPLBACKEND=Agg python examples/alphaevolve_interop/run_entrypoint.py \
../alphaevolve-reference/examples/tsp \
--api-base http://localhost:8000/v1 --model my-local-model \
--output-dir ./tsp-local-seed

# Explicitly opt in to one model-driven generation attempt; may incur charges.
MPLBACKEND=Agg python examples/alphaevolve_interop/run_entrypoint.py \
../alphaevolve-reference/examples/circle_packing \
--api-base http://localhost:8000/v1 --model my-local-model \
--evolve --iterations 1 --output-dir ./circle-local-run
```

The endpoint/model are required even for the default seed-only run; their live
availability is not checked until generation is requested. `--evolve` defaults
to one generation attempt. Explicit positive `--iterations` requires `--evolve`.
These are OE generation attempts, not hard evaluation/request/time/cost caps;
seed evaluation, retries and the original reporting code's own evaluations are
additional. An upstream entrypoint can score its seed before OE scores it again.
This runner does not execute signal processing or remote/cloud-evaluation
examples; signal processing still needs its cloud-specific query/controller
calls adapted in a separate step.

Only a named `--api-key-env VARIABLE_NAME` selects a key, which must be non-empty.
Without it, an explicit local placeholder is used, including for provider-client
construction in seed-only mode. The runner does **not** fall back to
`OPENAI_API_KEY` or an example `.env` for credentials. Authenticated endpoints
need an explicitly selected key before using `--evolve`. Google connection
arguments in the upstream main are accepted by the existing facade and warn
that they are ignored.

The runner disables `dotenv.load_dotenv` during entrypoint import and temporarily
supplies the selected model, equal positive source budgets, synchronous
evaluation and concurrency-one configuration. Import-time environment changes
are restored afterwards. It then sets the corresponding entrypoint globals
explicitly, including zero local generation iterations for a seed-only run. This
does not equate cloud generation concurrency with OE worker concurrency: the
facade keeps its existing single-worker behavior and unsupported-field warnings.
Missing expected configuration names cause a layout-drift error rather than a
guessed adaptation.

`.env*` files and bytecode caches are excluded from the staged `src/` copy;
symlinks in the selected source tree or instructions are rejected. Original
source files remain untouched, and reports operate on the staged copy. A new
`--output-dir` retains copied sources, `oe-output/`, and upstream report artifacts,
including partial output after failure. Existing directories/files/symlinks are
never reused; the parent directory must already exist. Without `--output-dir`,
the temporary run and reports are removed on success or failure.

The final stdout is an OE metrics/best-score JSON summary; Python-level upstream
prints are redirected to stderr. Upstream report/plot routines still execute;
`MPLBACKEND=Agg` avoids interactive plot windows. Reports receive **only the
facade's best result**, not the complete archive, so their history charts and
reported program-list counts are not full-run evaluation statistics.
The reviewed circle-packing plot helper uses `matplotlib.cm.get_cmap`, which
[Matplotlib removed in 3.11](https://matplotlib.org/stable/api/prev_api_changes/api_changes_3.11.0.html#matplotlib-cm-get-cmap).
The optional dependency range above retains that upstream plotting API; it is
not a restriction on OE's base dependencies. This runner does not patch plotting
libraries or rewrite upstream visualization code.

Use a fresh, single-threaded main process outside an active asyncio loop.
Temporary package and SDK aliases are removed before `main()` so evaluator
callables serialize by value to workers. Already-loaded cloud SDK namespaces
are refused. Trusted source code, including import-time code and plotting
helpers, executes with normal process privileges; copying and alias cleanup
provide no sandbox or general rollback of external side effects. Dynamic
relative imports after initial loading are outside this reviewed subset.

This walkthrough covers two original-entrypoint paths toward #468. It is not a
full SDK replacement, managed-service validation or completion of the issue.

```bash
python -m unittest discover -s tests -p 'test_alphaevolve_entrypoint.py' -v
```

## Evaluate an upstream seed locally

From an installed OpenEvolve checkout:
Expand Down
224 changes: 224 additions & 0 deletions examples/alphaevolve_interop/run_entrypoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
"""Run supported upstream entrypoints locally with explicit configuration swaps."""

import argparse
import contextlib
import importlib
import json
import os
import shutil
import sys
import tempfile
import uuid
from pathlib import Path
from types import ModuleType
from unittest.mock import patch

from openevolve.compat.alpha_evolve import (
AlphaEvolveClient,
AlphaEvolveExperiment,
local_imports,
)

EXAMPLES = {"circle_packing": "sum_of_radii", "tsp": "neg_tour_length"}


def _load_entrypoint(root, settings):
"""Import relative helpers, then unregister them for by-value worker transport."""
try:
import dotenv
except ImportError as error:
raise ImportError("Install python-dotenv and the upstream example dependencies") from error

name = "_openevolve_ae_entrypoint_" + uuid.uuid4().hex
package = ModuleType(name)
package.__path__ = [str(root)]
package.__package__ = name
sys.modules[name] = package
try:
# The caller supplies OE settings explicitly. Do not discover/load .env
# files while importing the original entrypoint's configuration globals.
with (
local_imports(),
patch.object(dotenv, "load_dotenv", return_value=False),
patch.dict(os.environ, settings),
):
module = importlib.import_module(name + ".src.run_evolution")
return module
finally:
for key in list(sys.modules):
if key == name or key.startswith(name + "."):
del sys.modules[key]


def _validate_source(example_dir):
example_dir = Path(example_dir).resolve()
if example_dir.name not in EXAMPLES:
raise ValueError("Supported upstream entrypoints: circle_packing, tsp")
source = example_dir / "src"
required = [source / name for name in ("run_evolution.py", "program.py", "evaluate.py")]
if example_dir.name == "tsp":
required.append(example_dir / "instructions.md")
for path in required:
if not path.is_file():
raise ValueError("Missing required upstream file: " + str(path))
if source.is_symlink() or any(path.is_symlink() for path in source.rglob("*")):
raise ValueError("Example src must contain regular files/directories, not symlinks")
instructions = example_dir / "instructions.md"
if instructions.is_symlink():
raise ValueError("instructions.md must not be a symlink")
return example_dir


def run_entrypoint(example_dir, *, api_base, model, api_key=None, iterations=0, output_dir=None):
"""Run trusted, byte-identical staged sources; default to seed evaluation only.

Only the two reviewed entrypoints are supported. Imports are process-global;
use this example runner in a fresh, single-threaded main process. Generation
uses the chosen endpoint and may incur charges when iterations is positive.
Reports operate on the facade's best-only result, not a full search history.
"""
example_dir = _validate_source(example_dir)
if not isinstance(model, str) or not model.strip():
raise ValueError("model must explicitly name a model served by the OE endpoint")
if api_key is not None and (not isinstance(api_key, str) or not api_key.strip()):
raise ValueError("api_key must be a non-empty string or None")
# Validate options before copying/importing any trusted upstream code.
options = dict(
api_base=api_base,
objective_metric=EXAMPLES[example_dir.name],
api_key=api_key if api_key is not None else "local-placeholder",
iterations=iterations,
)
AlphaEvolveClient(**options)
destination = Path(output_dir).absolute() if output_dir is not None else None
if destination is not None and destination.resolve().is_relative_to(example_dir):
raise ValueError("output_dir must be outside the upstream example")

experiments = []

def experiment_factory(*args, **kwargs):
experiment = AlphaEvolveExperiment(*args, **kwargs)
experiments.append(experiment)
return experiment

with contextlib.ExitStack() as stack:
if destination is None:
root = Path(
stack.enter_context(tempfile.TemporaryDirectory(prefix="oe_ae_entrypoint_"))
)
else:
# Exclusive creation prevents reports or copied sources overwriting
# an existing run. Keep explicit output, including after failure.
destination.mkdir()
root = destination
shutil.copytree(
example_dir / "src",
root / "src",
ignore=shutil.ignore_patterns(".env*", "__pycache__", "*.pyc"),
)
instructions = example_dir / "instructions.md"
if instructions.is_file():
shutil.copyfile(instructions, root / "instructions.md")
settings = {
"MODEL": model,
"MODEL_1": model,
"MODEL_2": "",
"MODEL_1_WEIGHT": "1",
"MODEL_2_WEIGHT": "0",
"MAX_PROGRAMS_GENERATED": str(max(1, iterations)),
"MAX_PROGRAMS_EVALUATED": str(max(1, iterations)),
"PARALLEL_EVALUATION": "False",
"CONCURRENCY": "1",
"WORKER_CONCURRENCY": "1",
}
entrypoint = _load_entrypoint(root, settings)
required = {
"main",
"AlphaEvolveClient",
"AlphaEvolveExperiment",
"MAX_PROGRAMS_GENERATED",
"MAX_PROGRAMS_EVALUATED",
"PARALLEL_EVALUATION",
"CONCURRENCY",
}
if example_dir.name == "circle_packing":
required.update(
{"MODEL_1", "MODEL_2", "MODEL_1_WEIGHT", "MODEL_2_WEIGHT", "WORKER_CONCURRENCY"}
)
else:
required.add("MODEL")
if any(not hasattr(entrypoint, key) for key in required) or not callable(entrypoint.main):
raise ValueError("Upstream entrypoint layout changed; review its configuration first")

# Override only imported factories and configuration globals. The source
# files and upstream main/evaluator implementations remain unchanged.
def client_factory(*args, **kwargs):
# Explicit OE options take precedence if an upstream constructor
# starts supplying similarly named settings in a later revision.
selected = kwargs | options | {"output_dir": str(root / "oe-output")}
return AlphaEvolveClient(*args, **selected)

entrypoint.AlphaEvolveClient = client_factory
entrypoint.AlphaEvolveExperiment = experiment_factory
entrypoint.MAX_PROGRAMS_GENERATED = max(1, iterations)
entrypoint.MAX_PROGRAMS_EVALUATED = max(1, iterations)
entrypoint.PARALLEL_EVALUATION = False
entrypoint.CONCURRENCY = 1
if example_dir.name == "circle_packing":
entrypoint.MODEL_1, entrypoint.MODEL_2 = model, ""
entrypoint.MODEL_1_WEIGHT, entrypoint.MODEL_2_WEIGHT = 1.0, 0.0
entrypoint.WORKER_CONCURRENCY = 1
else:
entrypoint.MODEL = model

entrypoint.main()
if not experiments or any(experiment.state != "COMPLETED" for experiment in experiments):
raise RuntimeError("Upstream entrypoint returned without completing a local experiment")
result = experiments[-1].result
return {
"example": example_dir.name,
"generation_attempt_limit": iterations,
"metrics": result.metrics,
"best_score": result.best_score,
"output_dir": str(destination) if destination is not None else None,
}


def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("example_dir", type=Path)
parser.add_argument("--api-base", required=True, help="Explicit OpenEvolve model endpoint")
parser.add_argument("--model", required=True, help="Model served by that endpoint")
parser.add_argument("--api-key-env", help="Read a key from this named environment variable")
parser.add_argument(
"--output-dir", type=Path, help="New directory for sources, OE output and reports"
)
parser.add_argument(
"--evolve", action="store_true", help="Opt in to model calls, which may incur charges"
)
parser.add_argument(
"--iterations", type=int, help="Positive generation attempts (default 1 with --evolve)"
)
args = parser.parse_args(argv)
if args.iterations is not None and (not args.evolve or args.iterations < 1):
parser.error("positive --iterations requires --evolve")
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")
# Original reports/import-time prints go to stderr; stdout contains the summary.
with contextlib.redirect_stdout(sys.stderr):
result = run_entrypoint(
args.example_dir,
api_base=args.api_base,
model=args.model,
api_key=api_key,
iterations=(args.iterations or 1) if args.evolve else 0,
output_dir=args.output_dir,
)
print(json.dumps(result, indent=2))


if __name__ == "__main__":
main()
Loading
Loading