openevolve 0.0.1__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.
openevolve/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ """
2
+ OpenEvolve: An open-source implementation of AlphaEvolve
3
+ """
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ from openevolve.controller import OpenEvolve
8
+
9
+ __all__ = ["OpenEvolve"]
openevolve/cli.py ADDED
@@ -0,0 +1,179 @@
1
+ """
2
+ Command-line interface for OpenEvolve
3
+ """
4
+
5
+ import argparse
6
+ import asyncio
7
+ import logging
8
+ import os
9
+ import sys
10
+ from typing import Dict, List, Optional
11
+
12
+ from openevolve import OpenEvolve
13
+ from openevolve.config import Config, load_config
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def parse_args() -> argparse.Namespace:
19
+ """Parse command-line arguments"""
20
+ parser = argparse.ArgumentParser(description="OpenEvolve - Evolutionary coding agent")
21
+
22
+ parser.add_argument("initial_program", help="Path to the initial program file")
23
+
24
+ parser.add_argument(
25
+ "evaluation_file", help="Path to the evaluation file containing an 'evaluate' function"
26
+ )
27
+
28
+ parser.add_argument("--config", "-c", help="Path to configuration file (YAML)", default=None)
29
+
30
+ parser.add_argument("--output", "-o", help="Output directory for results", default=None)
31
+
32
+ parser.add_argument(
33
+ "--iterations", "-i", help="Maximum number of iterations", type=int, default=None
34
+ )
35
+
36
+ parser.add_argument(
37
+ "--target-score", "-t", help="Target score to reach", type=float, default=None
38
+ )
39
+
40
+ parser.add_argument(
41
+ "--log-level",
42
+ "-l",
43
+ help="Logging level",
44
+ choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
45
+ default="INFO",
46
+ )
47
+
48
+ parser.add_argument(
49
+ "--checkpoint",
50
+ help="Path to checkpoint directory to resume from (e.g., openevolve_output/checkpoints/checkpoint_50)",
51
+ default=None,
52
+ )
53
+
54
+ parser.add_argument("--api-base", help="Base URL for the LLM API", default=None)
55
+
56
+ parser.add_argument("--primary-model", help="Primary LLM model name", default=None)
57
+
58
+ parser.add_argument("--secondary-model", help="Secondary LLM model name", default=None)
59
+
60
+ return parser.parse_args()
61
+
62
+
63
+ async def main_async() -> int:
64
+ """
65
+ Main asynchronous entry point
66
+
67
+ Returns:
68
+ Exit code
69
+ """
70
+ args = parse_args()
71
+
72
+ # Check if files exist
73
+ if not os.path.exists(args.initial_program):
74
+ print(f"Error: Initial program file '{args.initial_program}' not found")
75
+ return 1
76
+
77
+ if not os.path.exists(args.evaluation_file):
78
+ print(f"Error: Evaluation file '{args.evaluation_file}' not found")
79
+ return 1
80
+
81
+ # Create config object with command-line overrides
82
+ config = None
83
+ if args.api_base or args.primary_model or args.secondary_model:
84
+ # Load base config from file or defaults
85
+ config = load_config(args.config)
86
+
87
+ # Apply command-line overrides
88
+ if args.api_base:
89
+ config.llm.api_base = args.api_base
90
+ print(f"Using API base: {config.llm.api_base}")
91
+
92
+ if args.primary_model:
93
+ config.llm.primary_model = args.primary_model
94
+ print(f"Using primary model: {config.llm.primary_model}")
95
+
96
+ if args.secondary_model:
97
+ config.llm.secondary_model = args.secondary_model
98
+ print(f"Using secondary model: {config.llm.secondary_model}")
99
+
100
+ # Initialize OpenEvolve
101
+ try:
102
+ openevolve = OpenEvolve(
103
+ initial_program_path=args.initial_program,
104
+ evaluation_file=args.evaluation_file,
105
+ config=config,
106
+ config_path=args.config if config is None else None,
107
+ output_dir=args.output,
108
+ )
109
+
110
+ # Load from checkpoint if specified
111
+ if args.checkpoint:
112
+ if not os.path.exists(args.checkpoint):
113
+ print(f"Error: Checkpoint directory '{args.checkpoint}' not found")
114
+ return 1
115
+ print(f"Loading checkpoint from {args.checkpoint}")
116
+ openevolve.database.load(args.checkpoint)
117
+ print(
118
+ f"Checkpoint loaded successfully (iteration {openevolve.database.last_iteration})"
119
+ )
120
+
121
+ # Override log level if specified
122
+ if args.log_level:
123
+ logging.getLogger().setLevel(getattr(logging, args.log_level))
124
+
125
+ # Run evolution
126
+ best_program = await openevolve.run(
127
+ iterations=args.iterations,
128
+ target_score=args.target_score,
129
+ )
130
+
131
+ # Get the checkpoint path
132
+ checkpoint_dir = os.path.join(openevolve.output_dir, "checkpoints")
133
+ latest_checkpoint = None
134
+ if os.path.exists(checkpoint_dir):
135
+ checkpoints = [
136
+ os.path.join(checkpoint_dir, d)
137
+ for d in os.listdir(checkpoint_dir)
138
+ if os.path.isdir(os.path.join(checkpoint_dir, d))
139
+ ]
140
+ if checkpoints:
141
+ latest_checkpoint = sorted(
142
+ checkpoints, key=lambda x: int(x.split("_")[-1]) if "_" in x else 0
143
+ )[-1]
144
+
145
+ print(f"\nEvolution complete!")
146
+ print(f"Best program metrics:")
147
+ for name, value in best_program.metrics.items():
148
+ # Handle mixed types: format numbers as floats, others as strings
149
+ if isinstance(value, (int, float)):
150
+ print(f" {name}: {value:.4f}")
151
+ else:
152
+ print(f" {name}: {value}")
153
+
154
+ if latest_checkpoint:
155
+ print(f"\nLatest checkpoint saved at: {latest_checkpoint}")
156
+ print(f"To resume, use: --checkpoint {latest_checkpoint}")
157
+
158
+ return 0
159
+
160
+ except Exception as e:
161
+ print(f"Error: {str(e)}")
162
+ import traceback
163
+
164
+ traceback.print_exc()
165
+ return 1
166
+
167
+
168
+ def main() -> int:
169
+ """
170
+ Main entry point
171
+
172
+ Returns:
173
+ Exit code
174
+ """
175
+ return asyncio.run(main_async())
176
+
177
+
178
+ if __name__ == "__main__":
179
+ sys.exit(main())
openevolve/config.py ADDED
@@ -0,0 +1,354 @@
1
+ """
2
+ Configuration handling for OpenEvolve
3
+ """
4
+
5
+ import os
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+ from typing import Any, Dict, List, Optional, Union
9
+
10
+ import yaml
11
+
12
+
13
+ @dataclass
14
+ class LLMModelConfig:
15
+ """Configuration for a single LLM model"""
16
+
17
+ # API configuration
18
+ api_base: str = None
19
+ api_key: Optional[str] = None
20
+ name: str = None
21
+
22
+ # Weight for model in ensemble
23
+ weight: float = 1.0
24
+
25
+ # Generation parameters
26
+ system_message: Optional[str] = None
27
+ temperature: float = None
28
+ top_p: float = None
29
+ max_tokens: int = None
30
+
31
+ # Request parameters
32
+ timeout: int = None
33
+ retries: int = None
34
+ retry_delay: int = None
35
+
36
+
37
+ @dataclass
38
+ class LLMConfig(LLMModelConfig):
39
+ """Configuration for LLM models"""
40
+
41
+ # API configuration
42
+ api_base: str = "https://api.openai.com/v1"
43
+
44
+ # Generation parameters
45
+ system_message: Optional[str] = "system_message"
46
+ temperature: float = 0.7
47
+ top_p: float = 0.95
48
+ max_tokens: int = 4096
49
+
50
+ # Request parameters
51
+ timeout: int = 60
52
+ retries: int = 3
53
+ retry_delay: int = 5
54
+
55
+ # n-model configuration for evolution LLM ensemble
56
+ models: List[LLMModelConfig] = field(default_factory=lambda: [LLMModelConfig()])
57
+
58
+ # n-model configuration for evaluator LLM ensemble
59
+ evaluator_models: List[LLMModelConfig] = field(default_factory=lambda: [])
60
+
61
+ # Backwardes compatibility with primary_model(_weight) options
62
+ primary_model: str = None
63
+ primary_model_weight: float = None
64
+ secondary_model: str = None
65
+ secondary_model_weight: float = None
66
+
67
+ def __post_init__(self):
68
+ """Post-initialization to set up model configurations"""
69
+ # Handle backward compatibility for primary_model(_weight) and secondary_model(_weight).
70
+ if (self.primary_model or self.primary_model_weight) and len(self.models) < 1:
71
+ # Ensure we have a primary model
72
+ self.models.append(LLMModelConfig())
73
+ if self.primary_model:
74
+ self.models[0].name = self.primary_model
75
+ if self.primary_model_weight:
76
+ self.models[0].weight = self.primary_model_weight
77
+
78
+ if (self.secondary_model or self.secondary_model_weight) and len(self.models) < 2:
79
+ # Ensure we have a second model
80
+ self.models.append(LLMModelConfig())
81
+ if self.secondary_model:
82
+ self.models[1].name = self.secondary_model
83
+ if self.secondary_model_weight:
84
+ self.models[1].weight = self.secondary_model_weight
85
+
86
+ # If no evaluator models are defined, use the same models as for evolution
87
+ if not self.evaluator_models or len(self.evaluator_models) < 1:
88
+ self.evaluator_models = self.models.copy()
89
+
90
+ # Update models with shared configuration values
91
+ shared_config = {
92
+ "api_base": self.api_base,
93
+ "api_key": self.api_key,
94
+ "temperature": self.temperature,
95
+ "top_p": self.top_p,
96
+ "max_tokens": self.max_tokens,
97
+ "timeout": self.timeout,
98
+ "retries": self.retries,
99
+ "retry_delay": self.retry_delay,
100
+ }
101
+ self.update_model_params(shared_config)
102
+
103
+ def update_model_params(self, args: Dict[str, Any], overwrite: bool = False) -> None:
104
+ """Update model parameters for all models"""
105
+ for model in self.models + self.evaluator_models:
106
+ for key, value in args.items():
107
+ if overwrite or getattr(model, key, None) is None:
108
+ setattr(model, key, value)
109
+
110
+
111
+ @dataclass
112
+ class PromptConfig:
113
+ """Configuration for prompt generation"""
114
+
115
+ template_dir: Optional[str] = None
116
+ system_message: str = "system_message"
117
+ evaluator_system_message: str = "evaluator_system_message"
118
+
119
+ # Number of examples to include in the prompt
120
+ num_top_programs: int = 3
121
+ num_diverse_programs: int = 2
122
+
123
+ # Template stochasticity
124
+ use_template_stochasticity: bool = True
125
+ template_variations: Dict[str, List[str]] = field(default_factory=dict)
126
+
127
+ # Meta-prompting
128
+ use_meta_prompting: bool = False
129
+ meta_prompt_weight: float = 0.1
130
+
131
+ # Artifact rendering
132
+ include_artifacts: bool = True
133
+ max_artifact_bytes: int = 20 * 1024 # 20KB in prompt
134
+ artifact_security_filter: bool = True
135
+
136
+
137
+ @dataclass
138
+ class DatabaseConfig:
139
+ """Configuration for the program database"""
140
+
141
+ # General settings
142
+ db_path: Optional[str] = None # Path to store database on disk
143
+ in_memory: bool = True
144
+
145
+ # Evolutionary parameters
146
+ population_size: int = 1000
147
+ archive_size: int = 100
148
+ num_islands: int = 5
149
+
150
+ # Selection parameters
151
+ elite_selection_ratio: float = 0.1
152
+ exploration_ratio: float = 0.2
153
+ exploitation_ratio: float = 0.7
154
+ diversity_metric: str = "edit_distance" # Options: "edit_distance", "feature_based"
155
+
156
+ # Feature map dimensions for MAP-Elites
157
+ feature_dimensions: List[str] = field(default_factory=lambda: ["score", "complexity"])
158
+ feature_bins: int = 10
159
+
160
+ # Migration parameters for island-based evolution
161
+ migration_interval: int = 50 # Migrate every N generations
162
+ migration_rate: float = 0.1 # Fraction of population to migrate
163
+
164
+ # Random seed for reproducible sampling
165
+ random_seed: Optional[int] = None
166
+
167
+ # Artifact storage
168
+ artifacts_base_path: Optional[str] = None # Defaults to db_path/artifacts
169
+ artifact_size_threshold: int = 32 * 1024 # 32KB threshold
170
+ cleanup_old_artifacts: bool = True
171
+ artifact_retention_days: int = 30
172
+
173
+
174
+ @dataclass
175
+ class EvaluatorConfig:
176
+ """Configuration for program evaluation"""
177
+
178
+ # General settings
179
+ timeout: int = 300 # Maximum evaluation time in seconds
180
+ max_retries: int = 3
181
+
182
+ # Resource limits for evaluation
183
+ memory_limit_mb: Optional[int] = None
184
+ cpu_limit: Optional[float] = None
185
+
186
+ # Evaluation strategies
187
+ cascade_evaluation: bool = True
188
+ cascade_thresholds: List[float] = field(default_factory=lambda: [0.5, 0.75, 0.9])
189
+
190
+ # Parallel evaluation
191
+ parallel_evaluations: int = 4
192
+ distributed: bool = False
193
+
194
+ # LLM-based feedback
195
+ use_llm_feedback: bool = False
196
+ llm_feedback_weight: float = 0.1
197
+
198
+ # Artifact handling
199
+ enable_artifacts: bool = True
200
+ max_artifact_storage: int = 100 * 1024 * 1024 # 100MB per program
201
+
202
+
203
+ @dataclass
204
+ class Config:
205
+ """Master configuration for OpenEvolve"""
206
+
207
+ # General settings
208
+ max_iterations: int = 10000
209
+ checkpoint_interval: int = 100
210
+ log_level: str = "INFO"
211
+ log_dir: Optional[str] = None
212
+ random_seed: Optional[int] = None
213
+
214
+ # Component configurations
215
+ llm: LLMConfig = field(default_factory=LLMConfig)
216
+ prompt: PromptConfig = field(default_factory=PromptConfig)
217
+ database: DatabaseConfig = field(default_factory=DatabaseConfig)
218
+ evaluator: EvaluatorConfig = field(default_factory=EvaluatorConfig)
219
+
220
+ # Evolution settings
221
+ diff_based_evolution: bool = True
222
+ allow_full_rewrites: bool = False
223
+ max_code_length: int = 10000
224
+
225
+ @classmethod
226
+ def from_yaml(cls, path: Union[str, Path]) -> "Config":
227
+ """Load configuration from a YAML file"""
228
+ with open(path, "r") as f:
229
+ config_dict = yaml.safe_load(f)
230
+ return cls.from_dict(config_dict)
231
+
232
+ @classmethod
233
+ def from_dict(cls, config_dict: Dict[str, Any]) -> "Config":
234
+ """Create configuration from a dictionary"""
235
+ # Handle nested configurations
236
+ config = Config()
237
+
238
+ # Update top-level fields
239
+ for key, value in config_dict.items():
240
+ if key not in ["llm", "prompt", "database", "evaluator"] and hasattr(config, key):
241
+ setattr(config, key, value)
242
+
243
+ # Update nested configs
244
+ if "llm" in config_dict:
245
+ llm_dict = config_dict["llm"]
246
+ if "models" in llm_dict:
247
+ llm_dict["models"] = [LLMModelConfig(**m) for m in llm_dict["models"]]
248
+ if "evaluator_models" in llm_dict:
249
+ llm_dict["evaluator_models"] = [
250
+ LLMModelConfig(**m) for m in llm_dict["evaluator_models"]
251
+ ]
252
+ config.llm = LLMConfig(**llm_dict)
253
+ if "prompt" in config_dict:
254
+ config.prompt = PromptConfig(**config_dict["prompt"])
255
+ if "database" in config_dict:
256
+ config.database = DatabaseConfig(**config_dict["database"])
257
+ if "evaluator" in config_dict:
258
+ config.evaluator = EvaluatorConfig(**config_dict["evaluator"])
259
+
260
+ return config
261
+
262
+ def to_dict(self) -> Dict[str, Any]:
263
+ """Convert configuration to a dictionary"""
264
+ return {
265
+ # General settings
266
+ "max_iterations": self.max_iterations,
267
+ "checkpoint_interval": self.checkpoint_interval,
268
+ "log_level": self.log_level,
269
+ "log_dir": self.log_dir,
270
+ "random_seed": self.random_seed,
271
+ # Component configurations
272
+ "llm": {
273
+ "models": self.llm.models,
274
+ "evaluator_models": self.llm.evaluator_models,
275
+ "api_base": self.llm.api_base,
276
+ "temperature": self.llm.temperature,
277
+ "top_p": self.llm.top_p,
278
+ "max_tokens": self.llm.max_tokens,
279
+ "timeout": self.llm.timeout,
280
+ "retries": self.llm.retries,
281
+ "retry_delay": self.llm.retry_delay,
282
+ },
283
+ "prompt": {
284
+ "template_dir": self.prompt.template_dir,
285
+ "system_message": self.prompt.system_message,
286
+ "evaluator_system_message": self.prompt.evaluator_system_message,
287
+ "num_top_programs": self.prompt.num_top_programs,
288
+ "num_diverse_programs": self.prompt.num_diverse_programs,
289
+ "use_template_stochasticity": self.prompt.use_template_stochasticity,
290
+ "template_variations": self.prompt.template_variations,
291
+ # Note: meta-prompting features not implemented
292
+ # "use_meta_prompting": self.prompt.use_meta_prompting,
293
+ # "meta_prompt_weight": self.prompt.meta_prompt_weight,
294
+ },
295
+ "database": {
296
+ "db_path": self.database.db_path,
297
+ "in_memory": self.database.in_memory,
298
+ "population_size": self.database.population_size,
299
+ "archive_size": self.database.archive_size,
300
+ "num_islands": self.database.num_islands,
301
+ "elite_selection_ratio": self.database.elite_selection_ratio,
302
+ "exploration_ratio": self.database.exploration_ratio,
303
+ "exploitation_ratio": self.database.exploitation_ratio,
304
+ # Note: diversity_metric fixed to "edit_distance"
305
+ # "diversity_metric": self.database.diversity_metric,
306
+ "feature_dimensions": self.database.feature_dimensions,
307
+ "feature_bins": self.database.feature_bins,
308
+ "migration_interval": self.database.migration_interval,
309
+ "migration_rate": self.database.migration_rate,
310
+ "random_seed": self.database.random_seed,
311
+ },
312
+ "evaluator": {
313
+ "timeout": self.evaluator.timeout,
314
+ "max_retries": self.evaluator.max_retries,
315
+ # Note: resource limits not implemented
316
+ # "memory_limit_mb": self.evaluator.memory_limit_mb,
317
+ # "cpu_limit": self.evaluator.cpu_limit,
318
+ "cascade_evaluation": self.evaluator.cascade_evaluation,
319
+ "cascade_thresholds": self.evaluator.cascade_thresholds,
320
+ "parallel_evaluations": self.evaluator.parallel_evaluations,
321
+ # Note: distributed evaluation not implemented
322
+ # "distributed": self.evaluator.distributed,
323
+ "use_llm_feedback": self.evaluator.use_llm_feedback,
324
+ "llm_feedback_weight": self.evaluator.llm_feedback_weight,
325
+ },
326
+ # Evolution settings
327
+ "diff_based_evolution": self.diff_based_evolution,
328
+ "allow_full_rewrites": self.allow_full_rewrites,
329
+ "max_code_length": self.max_code_length,
330
+ }
331
+
332
+ def to_yaml(self, path: Union[str, Path]) -> None:
333
+ """Save configuration to a YAML file"""
334
+ with open(path, "w") as f:
335
+ yaml.dump(self.to_dict(), f, default_flow_style=False)
336
+
337
+
338
+ def load_config(config_path: Optional[Union[str, Path]] = None) -> Config:
339
+ """Load configuration from a YAML file or use defaults"""
340
+ if config_path and os.path.exists(config_path):
341
+ config = Config.from_yaml(config_path)
342
+ else:
343
+ config = Config()
344
+
345
+ # Use environment variables if available
346
+ api_key = os.environ.get("OPENAI_API_KEY")
347
+ api_base = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1")
348
+
349
+ config.llm.update_model_params({"api_key": api_key, "api_base": api_base})
350
+
351
+ # Make the system message available to the individual models, in case it is not provided from the prompt sampler
352
+ config.llm.update_model_params({"system_message": config.prompt.system_message})
353
+
354
+ return config