1818from openevolve .evolution_trace import EvolutionTracer
1919from openevolve .llm .ensemble import LLMEnsemble
2020from openevolve .process_parallel import ProcessParallelController
21+ from openevolve .prompt .meta_evolution import PromptArchive , evolve_prompt
2122from openevolve .prompt .sampler import PromptSampler
2223from openevolve .utils .code_utils import extract_code_language
2324from 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