""" Fitness-Guided Crossover Operator. Adapts LLEGO's fitness-guided crossover for text prompts. Based on: Decision Tree Induction Through LLMs via Semantically-Aware Evolution (ICLR 2025) """ from typing import List, Callable, TYPE_CHECKING import logging from .base_operator import BaseCrossoverOperator if TYPE_CHECKING: from .models import PromptCandidate logger = logging.getLogger(__name__) class FitnessGuidedCrossover(BaseCrossoverOperator): """ Fitness-guided crossover for text prompts. Combines high-performing parent prompts to generate offspring that target specific fitness levels using LLM semantic understanding. From LLEGO paper: "Fitness-guided crossover exploits high-performing regions of the search space by combining parent trees targeting a desired fitness level f* = f_max + α(f_max - f_min)" Reference: https://github.com/nicolashuynh/LLEGO """ def __init__(self, alpha: float = 0.1): """ Initialize crossover operator. Args: alpha: Fitness extrapolation parameter. Higher α = target higher fitness than parents. Default 0.1 from LLEGO paper (target 10% above best parent). """ self.alpha = alpha logger.debug(f"FitnessGuidedCrossover initialized with α={alpha}") def __call__( self, parents: List["PromptCandidate"], target_fitness: float, llm: Callable[[str], str] ) -> str: """ Combine parent prompts targeting specific fitness. Args: parents: List of PromptCandidate objects (2+ parents) target_fitness: Desired fitness for offspring llm: Language model callable Returns: str: Offspring prompt Raises: ValueError: If fewer than 2 parents provided """ if len(parents) < 2: raise ValueError("Crossover requires at least 2 parents") # Sort parents by fitness (best first) sorted_parents = sorted(parents, key=lambda p: p.fitness, reverse=True) logger.debug(f"Crossover: {len(parents)} parents, target fitness={target_fitness:.3f}") # Build crossover prompt and call LLM crossover_prompt = self._build_prompt(sorted_parents, target_fitness) new_prompt = llm(crossover_prompt) return new_prompt def _build_prompt( self, parents: List["PromptCandidate"], target_fitness: float ) -> str: """ Build LLM prompt for crossover operation. Args: parents: Sorted list of parent candidates (best first) target_fitness: Target fitness for offspring Returns: str: Prompt for LLM """ # Truncate parents to prevent safety filter issues MAX_PARENT_LENGTH = 350 # Build parent descriptions (limit to top 2) parent_descriptions = [] for i, parent in enumerate(parents[:2]): truncated = parent.prompt[:MAX_PARENT_LENGTH] if len(parent.prompt) > MAX_PARENT_LENGTH: truncated += "..." parent_descriptions.append( f"P{i+1} (f={parent.fitness:.2f}): {truncated}\n" ) prompt = f"""Combine these prompts into ONE improved version (target fitness: {target_fitness:.2f}). {' '.join(parent_descriptions)} Instructions: 1. Merge the best rules/principles from both parents 2. Organize logic clearly (e.g., "For X tasks: do Y", "If Z: then A") 3. Add structure to handle different cases systematically 4. Keep output format (Element: X, Description:, Reason:) 5. Max 600 chars Output ONLY the combined prompt:""" return prompt