Skip to content

Commit 2f9361b

Browse files
fangchenliclaude
andcommitted
Add prompt meta-evolution feature
Implements meta-evolution of prompt templates inspired by the Darwin Gödel Machine paper. The system maintains an archive of prompt templates, tracks their success rates, and evolves them over time to improve mutation quality. Key features: - PromptTemplate dataclass with success/improvement tracking - PromptArchive for managing template population with sampling - Configurable scoring weights for template quality assessment - Automatic template evolution at configurable intervals - Checkpoint persistence for prompt archives - Validation to ensure scoring weights sum to 1.0 New config options under `prompt_meta_evolution`: - enabled: Master switch (default: false) - archive_size: Max templates to keep - evolution_interval: Iterations between evolutions - exploration_rate: Random sampling probability - score_weight_*: Configurable scoring formula weights Closes #170 Related to #53 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent bde6d15 commit 2f9361b

7 files changed

Lines changed: 1389 additions & 58 deletions

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Configuration for testing prompt meta-evolution feature
2+
max_iterations: 25
3+
checkpoint_interval: 5
4+
log_level: INFO
5+
6+
# LLM configuration
7+
llm:
8+
primary_model: "gpt-4o-mini"
9+
primary_model_weight: 1.0
10+
api_base: "https://api.openai.com/v1"
11+
temperature: 0.7
12+
max_tokens: 16000
13+
timeout: 120
14+
15+
# Prompt configuration
16+
prompt:
17+
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."
18+
19+
# Prompt meta-evolution - ENABLED for testing
20+
prompt_meta_evolution:
21+
enabled: true
22+
archive_size: 20
23+
min_uses_for_evolution: 5 # Lower for testing
24+
evolution_interval: 20 # Trigger at iteration 20
25+
exploration_rate: 0.2
26+
elite_fraction: 0.3
27+
28+
# Database configuration
29+
database:
30+
population_size: 50
31+
archive_size: 20
32+
num_islands: 3
33+
elite_selection_ratio: 0.2
34+
exploitation_ratio: 0.7
35+
similarity_threshold: 0.99
36+
37+
# Evaluator configuration
38+
evaluator:
39+
timeout: 60
40+
cascade_thresholds: [1.3]
41+
parallel_evaluations: 3
42+
43+
# Evolution settings
44+
diff_based_evolution: true
45+
max_code_length: 20000

openevolve/config.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,56 @@ class EvolutionTraceConfig:
397397
compress: bool = False
398398

399399

400+
@dataclass
401+
class PromptMetaEvolutionConfig:
402+
"""Configuration for meta-evolution of prompt templates.
403+
404+
When enabled, OpenEvolve maintains an archive of prompt templates,
405+
tracks their success rates, and evolves them over time to improve
406+
mutation quality.
407+
"""
408+
409+
# Master switch
410+
enabled: bool = False
411+
412+
# Archive settings
413+
archive_size: int = 20 # Max templates to keep in archive
414+
415+
# Evolution triggers
416+
min_uses_for_evolution: int = 10 # Min uses before template can be evolved
417+
evolution_interval: int = 20 # Trigger evolution every N iterations
418+
419+
# Sampling behavior
420+
exploration_rate: float = 0.2 # Probability of sampling random template
421+
elite_fraction: float = 0.3 # Fraction of top templates protected from pruning
422+
423+
# Scoring weights (must sum to 1.0)
424+
# score = w_success * success_rate + w_improvement * improvement_rate + w_fitness * normalized_fitness_delta
425+
score_weight_success: float = 0.3 # Weight for success rate (mutations accepted)
426+
score_weight_improvement: float = 0.4 # Weight for improvement rate (fitness increased)
427+
score_weight_fitness_delta: float = 0.3 # Weight for avg fitness delta magnitude
428+
429+
# Scoring parameters
430+
score_min_uses: int = 5 # Min uses before score is calculated (else neutral prior)
431+
score_neutral_prior: float = 0.5 # Score returned when uses < min_uses
432+
433+
def __post_init__(self):
434+
"""Validate configuration after initialization."""
435+
weight_sum = (
436+
self.score_weight_success
437+
+ self.score_weight_improvement
438+
+ self.score_weight_fitness_delta
439+
)
440+
tolerance = 1e-6
441+
if abs(weight_sum - 1.0) > tolerance:
442+
raise ValueError(
443+
f"Scoring weights must sum to 1.0, got {weight_sum:.6f} "
444+
f"(success={self.score_weight_success}, "
445+
f"improvement={self.score_weight_improvement}, "
446+
f"fitness_delta={self.score_weight_fitness_delta})"
447+
)
448+
449+
400450
@dataclass
401451
class Config:
402452
"""Master configuration for OpenEvolve"""
@@ -416,6 +466,9 @@ class Config:
416466
database: DatabaseConfig = field(default_factory=DatabaseConfig)
417467
evaluator: EvaluatorConfig = field(default_factory=EvaluatorConfig)
418468
evolution_trace: EvolutionTraceConfig = field(default_factory=EvolutionTraceConfig)
469+
prompt_meta_evolution: PromptMetaEvolutionConfig = field(
470+
default_factory=PromptMetaEvolutionConfig
471+
)
419472

420473
# Evolution settings
421474
diff_based_evolution: bool = True

openevolve/controller.py

Lines changed: 181 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from openevolve.evolution_trace import EvolutionTracer
1919
from openevolve.llm.ensemble import LLMEnsemble
2020
from openevolve.process_parallel import ProcessParallelController
21+
from openevolve.prompt.meta_evolution import PromptArchive, evolve_prompt
2122
from openevolve.prompt.sampler import PromptSampler
2223
from openevolve.utils.code_utils import extract_code_language
2324
from openevolve.utils.format_utils import format_improvement_safe, format_metrics_safe
@@ -188,6 +189,25 @@ def __init__(
188189
# Initialize improved parallel processing components
189190
self.parallel_controller = None
190191

192+
# Initialize prompt meta-evolution if enabled
193+
self.prompt_archive = None
194+
if self.config.prompt_meta_evolution.enabled:
195+
self.prompt_archive = PromptArchive(
196+
max_size=self.config.prompt_meta_evolution.archive_size,
197+
min_uses_for_evolution=self.config.prompt_meta_evolution.min_uses_for_evolution,
198+
elite_fraction=self.config.prompt_meta_evolution.elite_fraction,
199+
exploration_rate=self.config.prompt_meta_evolution.exploration_rate,
200+
# Scoring configuration
201+
score_weight_success=self.config.prompt_meta_evolution.score_weight_success,
202+
score_weight_improvement=self.config.prompt_meta_evolution.score_weight_improvement,
203+
score_weight_fitness_delta=self.config.prompt_meta_evolution.score_weight_fitness_delta,
204+
score_min_uses=self.config.prompt_meta_evolution.score_min_uses,
205+
score_neutral_prior=self.config.prompt_meta_evolution.score_neutral_prior,
206+
)
207+
self._initialize_default_prompt_templates()
208+
self.prompt_sampler.set_prompt_archive(self.prompt_archive)
209+
logger.info("Prompt meta-evolution enabled")
210+
191211
def _setup_logging(self) -> None:
192212
"""Set up logging"""
193213
log_dir = self.config.log_dir or os.path.join(self.output_dir, "logs")
@@ -225,7 +245,7 @@ def _setup_manual_mode_queue(self) -> None:
225245
if not bool(getattr(self.config.llm, "manual_mode", False)):
226246
return
227247

228-
qdir = (Path(self.output_dir).expanduser().resolve() / "manual_tasks_queue")
248+
qdir = Path(self.output_dir).expanduser().resolve() / "manual_tasks_queue"
229249

230250
# Clear stale tasks from previous runs
231251
if qdir.exists():
@@ -246,6 +266,34 @@ def _load_initial_program(self) -> str:
246266
with open(self.initial_program_path, "r") as f:
247267
return f.read()
248268

269+
def _initialize_default_prompt_templates(self) -> None:
270+
"""Initialize the prompt archive with default templates from TemplateManager."""
271+
if self.prompt_archive is None:
272+
return
273+
274+
# Get default templates from the sampler's template manager
275+
tm = self.prompt_sampler.template_manager
276+
277+
# Get system template
278+
system_template = self.config.prompt.system_message
279+
if system_template in tm.templates:
280+
system_template = tm.get_template(system_template)
281+
282+
# Get user template (diff-based or full rewrite)
283+
if self.config.diff_based_evolution:
284+
user_template = tm.get_template("diff_user")
285+
else:
286+
user_template = tm.get_template("full_rewrite_user")
287+
288+
# Add as the default template
289+
self.prompt_archive.add_template(
290+
system_template=system_template,
291+
user_template=user_template,
292+
is_default=True,
293+
metadata={"source": "default"},
294+
)
295+
logger.info("Added default prompt template to archive")
296+
249297
async def run(
250298
self,
251299
iterations: Optional[int] = None,
@@ -333,6 +381,7 @@ async def run(
333381
self.database,
334382
self.evolution_tracer,
335383
file_suffix=self.config.file_suffix,
384+
prompt_archive=self.prompt_archive,
336385
)
337386

338387
# Set up signal handlers for graceful shutdown
@@ -493,6 +542,20 @@ def _save_checkpoint(self, iteration: int) -> None:
493542
f"{format_metrics_safe(best_program.metrics)}"
494543
)
495544

545+
# Save prompt archive if meta-evolution is enabled
546+
if self.prompt_archive is not None:
547+
import json
548+
549+
prompt_archive_path = os.path.join(checkpoint_path, "prompt_archive.json")
550+
with open(prompt_archive_path, "w") as f:
551+
json.dump(self.prompt_archive.to_dict(), f, indent=2)
552+
stats = self.prompt_archive.get_statistics()
553+
logger.info(
554+
f"Saved prompt archive (size={stats['size']}, "
555+
f"total_uses={stats['total_uses']}, "
556+
f"success_rate={stats['overall_success_rate']:.1%})"
557+
)
558+
496559
logger.info(f"Saved checkpoint at iteration {iteration} to {checkpoint_path}")
497560

498561
def _load_checkpoint(self, checkpoint_path: str) -> None:
@@ -504,16 +567,131 @@ def _load_checkpoint(self, checkpoint_path: str) -> None:
504567
self.database.load(checkpoint_path)
505568
logger.info(f"Checkpoint loaded successfully (iteration {self.database.last_iteration})")
506569

570+
# Load prompt archive if meta-evolution is enabled
571+
if self.prompt_archive is not None:
572+
import json
573+
574+
prompt_archive_path = os.path.join(checkpoint_path, "prompt_archive.json")
575+
if os.path.exists(prompt_archive_path):
576+
with open(prompt_archive_path, "r") as f:
577+
self.prompt_archive = PromptArchive.from_dict(json.load(f))
578+
# Re-inject into sampler and parallel controller
579+
self.prompt_sampler.set_prompt_archive(self.prompt_archive)
580+
stats = self.prompt_archive.get_statistics()
581+
logger.info(
582+
f"Loaded prompt archive (size={stats['size']}, "
583+
f"total_uses={stats['total_uses']})"
584+
)
585+
586+
def _maybe_evolve_prompts(self, iteration: int) -> None:
587+
"""
588+
Periodically evolve prompt templates if meta-evolution is enabled.
589+
590+
Args:
591+
iteration: Current iteration number
592+
"""
593+
if self.prompt_archive is None:
594+
return
595+
596+
# Only evolve at configured intervals
597+
interval = self.config.prompt_meta_evolution.evolution_interval
598+
if iteration == 0 or iteration % interval != 0:
599+
return
600+
601+
# Get templates ready for evolution
602+
templates_to_evolve = self.prompt_archive.get_templates_for_evolution()
603+
if not templates_to_evolve:
604+
logger.debug("No templates ready for evolution yet")
605+
return
606+
607+
top_templates = self.prompt_archive.get_top_templates(5)
608+
609+
# Evolve the top template that's ready for evolution
610+
# Sort by score descending
611+
templates_to_evolve.sort(key=lambda t: t.score, reverse=True)
612+
template = templates_to_evolve[0]
613+
614+
logger.info(
615+
f"Evolving prompt template {template.id} "
616+
f"(score={template.score:.3f}, uses={template.uses})"
617+
)
618+
619+
# Create a sync wrapper for LLM generation that works in async context
620+
# We use a thread pool to avoid event loop conflicts
621+
import concurrent.futures
622+
623+
def llm_generate_sync(system: str, user: str) -> str:
624+
import asyncio
625+
626+
# Create a new event loop in a thread to avoid conflicts
627+
def run_in_new_loop():
628+
loop = asyncio.new_event_loop()
629+
asyncio.set_event_loop(loop)
630+
try:
631+
return loop.run_until_complete(
632+
self.llm_ensemble.generate_with_context(
633+
system_message=system,
634+
messages=[{"role": "user", "content": user}],
635+
)
636+
)
637+
finally:
638+
loop.close()
639+
640+
with concurrent.futures.ThreadPoolExecutor() as executor:
641+
future = executor.submit(run_in_new_loop)
642+
return future.result()
643+
644+
# Evolve the template
645+
result = evolve_prompt(
646+
template,
647+
top_templates,
648+
llm_generate_sync,
649+
score_fn=self.prompt_archive.get_template_score,
650+
)
651+
if result:
652+
new_system, new_user = result
653+
new_template = self.prompt_archive.add_template(
654+
system_template=new_system,
655+
user_template=new_user,
656+
parent_id=template.id,
657+
metadata={"evolved_at_iteration": iteration},
658+
)
659+
logger.info(
660+
f"Created evolved template {new_template.id} "
661+
f"(generation {new_template.generation})"
662+
)
663+
else:
664+
logger.warning(f"Failed to evolve template {template.id}")
665+
507666
async def _run_evolution_with_checkpoints(
508667
self, start_iteration: int, max_iterations: int, target_score: Optional[float]
509668
) -> None:
510669
"""Run evolution with checkpoint saving support"""
511670
logger.info(f"Using island-based evolution with {self.config.database.num_islands} islands")
512671
self.database.log_island_status()
513672

514-
# Run the evolution process with checkpoint callback
673+
# Track last prompt evolution for catching up between checkpoints
674+
last_prompt_evolution = [start_iteration] # Use list for closure mutability
675+
676+
# Create a combined callback that handles checkpoints and prompt evolution
677+
def combined_callback(iteration: int) -> None:
678+
self._save_checkpoint(iteration)
679+
680+
# Trigger prompt evolution - catch up on any missed intervals
681+
if self.prompt_archive is not None:
682+
evolution_interval = self.config.prompt_meta_evolution.evolution_interval
683+
# Find all evolution points between last_prompt_evolution and current iteration
684+
next_evolution = (
685+
last_prompt_evolution[0] // evolution_interval + 1
686+
) * evolution_interval
687+
while next_evolution <= iteration:
688+
self._maybe_evolve_prompts(next_evolution)
689+
next_evolution += evolution_interval
690+
last_prompt_evolution[0] = iteration
691+
692+
# Run the evolution process with combined callback
515693
await self.parallel_controller.run_evolution(
516-
start_iteration, max_iterations, target_score, checkpoint_callback=self._save_checkpoint
694+
start_iteration, max_iterations, target_score, checkpoint_callback=combined_callback
517695
)
518696

519697
# Check if shutdown or early stopping was triggered

0 commit comments

Comments
 (0)