Skip to content
Open
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
45 changes: 45 additions & 0 deletions examples/function_minimization/config_meta_evolution.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Configuration for testing prompt meta-evolution feature
max_iterations: 25
checkpoint_interval: 5
log_level: INFO

# LLM configuration
llm:
primary_model: "gpt-4o-mini"
primary_model_weight: 1.0
api_base: "https://api.openai.com/v1"
temperature: 0.7
max_tokens: 16000
timeout: 120

# Prompt configuration
prompt:
system_message: "You are an expert programmer specializing in optimization algorithms. Your task is to improve a function minimization algorithm to find the global minimum of a complex function with many local minima. The function is f(x, y) = sin(x) * cos(y) + sin(x*y) + (x^2 + y^2)/20. Focus on improving the search_algorithm function to reliably find the global minimum, escaping local minima that might trap simple algorithms."

# Prompt meta-evolution - ENABLED for testing
prompt_meta_evolution:
enabled: true
archive_size: 20
min_uses_for_evolution: 5 # Lower for testing
evolution_interval: 20 # Trigger at iteration 20
exploration_rate: 0.2
elite_fraction: 0.3

# Database configuration
database:
population_size: 50
archive_size: 20
num_islands: 3
elite_selection_ratio: 0.2
exploitation_ratio: 0.7
similarity_threshold: 0.99

# Evaluator configuration
evaluator:
timeout: 60
cascade_thresholds: [1.3]
parallel_evaluations: 3

# Evolution settings
diff_based_evolution: true
max_code_length: 20000
53 changes: 53 additions & 0 deletions openevolve/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,56 @@ class EvolutionTraceConfig:
compress: bool = False


@dataclass
class PromptMetaEvolutionConfig:
"""Configuration for meta-evolution of prompt templates.

When enabled, OpenEvolve maintains an archive of prompt templates,
tracks their success rates, and evolves them over time to improve
mutation quality.
"""

# Master switch
enabled: bool = False

# Archive settings
archive_size: int = 20 # Max templates to keep in archive

# Evolution triggers
min_uses_for_evolution: int = 10 # Min uses before template can be evolved
evolution_interval: int = 20 # Trigger evolution every N iterations

# Sampling behavior
exploration_rate: float = 0.2 # Probability of sampling random template
elite_fraction: float = 0.3 # Fraction of top templates protected from pruning

# Scoring weights (must sum to 1.0)
# score = w_success * success_rate + w_improvement * improvement_rate + w_fitness * normalized_fitness_delta
score_weight_success: float = 0.3 # Weight for success rate (mutations accepted)
score_weight_improvement: float = 0.4 # Weight for improvement rate (fitness increased)
score_weight_fitness_delta: float = 0.3 # Weight for avg fitness delta magnitude

# Scoring parameters
score_min_uses: int = 5 # Min uses before score is calculated (else neutral prior)
score_neutral_prior: float = 0.5 # Score returned when uses < min_uses

def __post_init__(self):
"""Validate configuration after initialization."""
weight_sum = (
self.score_weight_success
+ self.score_weight_improvement
+ self.score_weight_fitness_delta
)
tolerance = 1e-6
if abs(weight_sum - 1.0) > tolerance:
raise ValueError(
f"Scoring weights must sum to 1.0, got {weight_sum:.6f} "
f"(success={self.score_weight_success}, "
f"improvement={self.score_weight_improvement}, "
f"fitness_delta={self.score_weight_fitness_delta})"
)


@dataclass
class Config:
"""Master configuration for OpenEvolve"""
Expand All @@ -416,6 +466,9 @@ class Config:
database: DatabaseConfig = field(default_factory=DatabaseConfig)
evaluator: EvaluatorConfig = field(default_factory=EvaluatorConfig)
evolution_trace: EvolutionTraceConfig = field(default_factory=EvolutionTraceConfig)
prompt_meta_evolution: PromptMetaEvolutionConfig = field(
default_factory=PromptMetaEvolutionConfig
)

# Evolution settings
diff_based_evolution: bool = True
Expand Down
178 changes: 175 additions & 3 deletions openevolve/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import time
import uuid
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
from typing import Any, Dict, List, Optional, Union

from openevolve.config import Config, load_config
Expand All @@ -18,6 +19,7 @@
from openevolve.evolution_trace import EvolutionTracer
from openevolve.llm.ensemble import LLMEnsemble
from openevolve.process_parallel import ProcessParallelController
from openevolve.prompt.meta_evolution import PromptArchive, evolve_prompt
from openevolve.prompt.sampler import PromptSampler
from openevolve.utils.code_utils import extract_code_language
from openevolve.utils.format_utils import format_improvement_safe, format_metrics_safe
Expand Down Expand Up @@ -188,6 +190,25 @@ def __init__(
# Initialize improved parallel processing components
self.parallel_controller = None

# Initialize prompt meta-evolution if enabled
self.prompt_archive = None
if self.config.prompt_meta_evolution.enabled:
self.prompt_archive = PromptArchive(
max_size=self.config.prompt_meta_evolution.archive_size,
min_uses_for_evolution=self.config.prompt_meta_evolution.min_uses_for_evolution,
elite_fraction=self.config.prompt_meta_evolution.elite_fraction,
exploration_rate=self.config.prompt_meta_evolution.exploration_rate,
# Scoring configuration
score_weight_success=self.config.prompt_meta_evolution.score_weight_success,
score_weight_improvement=self.config.prompt_meta_evolution.score_weight_improvement,
score_weight_fitness_delta=self.config.prompt_meta_evolution.score_weight_fitness_delta,
score_min_uses=self.config.prompt_meta_evolution.score_min_uses,
score_neutral_prior=self.config.prompt_meta_evolution.score_neutral_prior,
)
self._initialize_default_prompt_templates()
self.prompt_sampler.set_prompt_archive(self.prompt_archive)
logger.info("Prompt meta-evolution enabled")

def _setup_logging(self) -> None:
"""Set up logging"""
log_dir = self.config.log_dir or os.path.join(self.output_dir, "logs")
Expand Down Expand Up @@ -225,7 +246,7 @@ def _setup_manual_mode_queue(self) -> None:
if not bool(getattr(self.config.llm, "manual_mode", False)):
return

qdir = (Path(self.output_dir).expanduser().resolve() / "manual_tasks_queue")
qdir = Path(self.output_dir).expanduser().resolve() / "manual_tasks_queue"

# Clear stale tasks from previous runs
if qdir.exists():
Expand All @@ -246,6 +267,34 @@ def _load_initial_program(self) -> str:
with open(self.initial_program_path, "r") as f:
return f.read()

def _initialize_default_prompt_templates(self) -> None:
"""Initialize the prompt archive with default templates from TemplateManager."""
if self.prompt_archive is None:
return

# Get default templates from the sampler's template manager
tm = self.prompt_sampler.template_manager

# Get system template
system_template = self.config.prompt.system_message
if system_template in tm.templates:
system_template = tm.get_template(system_template)

# Get user template (diff-based or full rewrite)
if self.config.diff_based_evolution:
user_template = tm.get_template("diff_user")
else:
user_template = tm.get_template("full_rewrite_user")

# Add as the default template
self.prompt_archive.add_template(
system_template=system_template,
user_template=user_template,
is_default=True,
metadata={"source": "default"},
)
logger.info("Added default prompt template to archive")

async def run(
self,
iterations: Optional[int] = None,
Expand Down Expand Up @@ -333,6 +382,7 @@ async def run(
self.database,
self.evolution_tracer,
file_suffix=self.config.file_suffix,
prompt_archive=self.prompt_archive,
)

# Set up signal handlers for graceful shutdown
Expand Down Expand Up @@ -493,6 +543,20 @@ def _save_checkpoint(self, iteration: int) -> None:
f"{format_metrics_safe(best_program.metrics)}"
)

# Save prompt archive if meta-evolution is enabled
if self.prompt_archive is not None:
import json

prompt_archive_path = os.path.join(checkpoint_path, "prompt_archive.json")
with open(prompt_archive_path, "w") as f:
json.dump(self.prompt_archive.to_dict(), f, indent=2)
stats = self.prompt_archive.get_statistics()
logger.info(
f"Saved prompt archive (size={stats['size']}, "
f"total_uses={stats['total_uses']}, "
f"success_rate={stats['overall_success_rate']:.1%})"
)

logger.info(f"Saved checkpoint at iteration {iteration} to {checkpoint_path}")

def _load_checkpoint(self, checkpoint_path: str) -> None:
Expand All @@ -504,16 +568,124 @@ def _load_checkpoint(self, checkpoint_path: str) -> None:
self.database.load(checkpoint_path)
logger.info(f"Checkpoint loaded successfully (iteration {self.database.last_iteration})")

# Load prompt archive if meta-evolution is enabled
if self.prompt_archive is not None:
import json

prompt_archive_path = os.path.join(checkpoint_path, "prompt_archive.json")
if os.path.exists(prompt_archive_path):
with open(prompt_archive_path, "r") as f:
self.prompt_archive = PromptArchive.from_dict(json.load(f))
# Re-inject into sampler and parallel controller
self.prompt_sampler.set_prompt_archive(self.prompt_archive)
stats = self.prompt_archive.get_statistics()
logger.info(
f"Loaded prompt archive (size={stats['size']}, "
f"total_uses={stats['total_uses']})"
)

def _maybe_evolve_prompts(self, iteration: int) -> None:
"""
Periodically evolve prompt templates if meta-evolution is enabled.

Args:
iteration: Current iteration number
"""
if self.prompt_archive is None:
return

# Only evolve at configured intervals
interval = self.config.prompt_meta_evolution.evolution_interval
if iteration == 0 or iteration % interval != 0:
return

# Get templates ready for evolution
templates_to_evolve = self.prompt_archive.get_templates_for_evolution()
if not templates_to_evolve:
logger.debug("No templates ready for evolution yet")
return

top_templates = self.prompt_archive.get_top_templates(5)

# Evolve the top template that's ready for evolution
# Sort by score descending
templates_to_evolve.sort(key=lambda t: t.score, reverse=True)
template = templates_to_evolve[0]

logger.info(
f"Evolving prompt template {template.id} "
f"(score={template.score:.3f}, uses={template.uses})"
)

# Create a sync wrapper for LLM generation that works within an async context.
# We run the async LLM call in a separate thread with its own event loop
# to avoid conflicts with the main event loop.
def llm_generate_sync(system: str, user: str) -> str:
def _run_async_in_thread():
# asyncio.run() creates a new event loop, runs the coroutine,
# and cleans up the loop automatically
return asyncio.run(
self.llm_ensemble.generate_with_context(
system_message=system,
messages=[{"role": "user", "content": user}],
)
)

with ThreadPoolExecutor(max_workers=1) as executor:
future = executor.submit(_run_async_in_thread)
return future.result()

# Evolve the template
result = evolve_prompt(
template,
top_templates,
llm_generate_sync,
score_fn=self.prompt_archive.get_template_score,
)
if result:
new_system, new_user = result
new_template = self.prompt_archive.add_template(
system_template=new_system,
user_template=new_user,
parent_id=template.id,
metadata={"evolved_at_iteration": iteration},
)
logger.info(
f"Created evolved template {new_template.id} "
f"(generation {new_template.generation})"
)
else:
logger.warning(f"Failed to evolve template {template.id}")

async def _run_evolution_with_checkpoints(
self, start_iteration: int, max_iterations: int, target_score: Optional[float]
) -> None:
"""Run evolution with checkpoint saving support"""
logger.info(f"Using island-based evolution with {self.config.database.num_islands} islands")
self.database.log_island_status()

# Run the evolution process with checkpoint callback
# Track last prompt evolution for catching up between checkpoints
last_prompt_evolution = [start_iteration] # Use list for closure mutability

# Create a combined callback that handles checkpoints and prompt evolution
def combined_callback(iteration: int) -> None:
self._save_checkpoint(iteration)

# Trigger prompt evolution - catch up on any missed intervals
if self.prompt_archive is not None:
evolution_interval = self.config.prompt_meta_evolution.evolution_interval
# Find all evolution points between last_prompt_evolution and current iteration
next_evolution = (
last_prompt_evolution[0] // evolution_interval + 1
) * evolution_interval
while next_evolution <= iteration:
self._maybe_evolve_prompts(next_evolution)
next_evolution += evolution_interval
last_prompt_evolution[0] = iteration

# Run the evolution process with combined callback
await self.parallel_controller.run_evolution(
start_iteration, max_iterations, target_score, checkpoint_callback=self._save_checkpoint
start_iteration, max_iterations, target_score, checkpoint_callback=combined_callback
)

# Check if shutdown or early stopping was triggered
Expand Down
Loading