"""
Pattern 58: Evolutionary Agent
Purpose: Use genetic algorithms to evolve agent strategies and behaviors.
Selection, crossover, and mutation to optimize agent parameters over generations.
"""
from typing import Dict, Any, List, Optional, Tuple
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
import random
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
class SelectionStrategy(Enum):
"""Selection strategies for evolution."""
TOURNAMENT = "tournament" # Tournament selection
ROULETTE = "roulette" # Roulette wheel (fitness-proportionate)
RANK = "rank" # Rank-based selection
ELITISM = "elitism" # Keep best individuals
@dataclass
class Gene:
"""A gene representing part of an agent's strategy."""
name: str
value: float
min_value: float = 0.0
max_value: float = 1.0
mutation_rate: float = 0.1
@dataclass
class Genome:
"""A genome representing an agent's complete strategy."""
id: str
genes: Dict[str, Gene]
fitness: float = 0.0
generation: int = 0
parent_ids: List[str] = field(default_factory=list)
@dataclass
class GenerationStats:
"""Statistics for a generation."""
generation: int
population_size: int
best_fitness: float
avg_fitness: float
worst_fitness: float
diversity: float
timestamp: datetime = field(default_factory=datetime.now)
class EvolutionaryAgent:
"""
Evolutionary Agent - Evolve strategies using genetic algorithms.
Uses selection, crossover, and mutation to optimize agent behaviors
over multiple generations. LLM assists in evaluating fitness.
"""
def __init__(self,
name: str,
gene_names: List[str],
population_size: int = 20,
selection_strategy: SelectionStrategy = SelectionStrategy.TOURNAMENT,
mutation_rate: float = 0.1,
crossover_rate: float = 0.7,
elitism_count: int = 2):
self.name = name
self.gene_names = gene_names
self.population_size = population_size
self.selection_strategy = selection_strategy
self.mutation_rate = mutation_rate
self.crossover_rate = crossover_rate
self.elitism_count = elitism_count
self.llm = ChatOpenAI(model="gpt-4", temperature=0.3)
# Population
self.population: List[Genome] = []
self.current_generation: int = 0
self.best_genome: Optional[Genome] = None
# History
self.generation_stats: List[GenerationStats] = []
self.evolution_history: List[Genome] = []
# Statistics
self.statistics: Dict[str, Any] = {
"total_generations": 0,
"total_evaluations": 0,
"best_fitness_ever": 0.0,
"avg_fitness_improvement": 0.0
}
def initialize_population(self):
"""Initialize random population."""
print(f" {self.name}: Initializing population of {self.population_size}...")
self.population = []
for i in range(self.population_size):
genes = {}
for gene_name in self.gene_names:
genes[gene_name] = Gene(
name=gene_name,
value=random.uniform(0.0, 1.0),
mutation_rate=self.mutation_rate
)
genome = Genome(
id=f"gen0_ind{i}",
genes=genes,
generation=0
)
self.population.append(genome)
print(f" Population initialized with {len(self.population)} individuals")
def evaluate_fitness(self, genome: Genome, task: str) -> float:
"""Evaluate fitness of a genome for a task."""
# Simulate fitness based on gene values
# In practice, this would run the agent with these parameters
# Simple fitness function: weighted sum of genes
fitness = 0.0
weights = {"learning_rate": 0.3, "exploration": 0.2, "memory_size": 0.2,
"attention": 0.15, "creativity": 0.15}
for gene_name, gene in genome.genes.items():
weight = weights.get(gene_name, 1.0 / len(genome.genes))
fitness += gene.value * weight
# Add some randomness to simulate noisy evaluation
fitness *= random.uniform(0.8, 1.2)
fitness = max(0.0, min(1.0, fitness))
self.statistics["total_evaluations"] += 1
return fitness
def evaluate_population(self, task: str):
"""Evaluate fitness for entire population."""
for genome in self.population:
genome.fitness = self.evaluate_fitness(genome, task)
# Sort by fitness
self.population.sort(key=lambda g: g.fitness, reverse=True)
# Track best genome
if not self.best_genome or self.population[0].fitness > self.best_genome.fitness:
self.best_genome = self.population[0]
self.statistics["best_fitness_ever"] = self.best_genome.fitness
def select_parents(self) -> Tuple[Genome, Genome]:
"""Select two parents for reproduction."""
if self.selection_strategy == SelectionStrategy.TOURNAMENT:
return self._tournament_selection(), self._tournament_selection()
elif self.selection_strategy == SelectionStrategy.ROULETTE:
return self._roulette_selection(), self._roulette_selection()
elif self.selection_strategy == SelectionStrategy.RANK:
return self._rank_selection(), self._rank_selection()
else: # ELITISM
# Select from top half
top_half = self.population[:len(self.population)//2]
return random.choice(top_half), random.choice(top_half)
def _tournament_selection(self, tournament_size: int = 3) -> Genome:
"""Tournament selection."""
tournament = random.sample(self.population, min(tournament_size, len(self.population)))
return max(tournament, key=lambda g: g.fitness)
def _roulette_selection(self) -> Genome:
"""Roulette wheel selection."""
total_fitness = sum(g.fitness for g in self.population)
if total_fitness == 0:
return random.choice(self.population)
pick = random.uniform(0, total_fitness)
current = 0
for genome in self.population:
current += genome.fitness
if current > pick:
return genome
return self.population[-1]
def _rank_selection(self) -> Genome:
"""Rank-based selection."""
# Already sorted by fitness
ranks = list(range(len(self.population), 0, -1))
total_rank = sum(ranks)
pick = random.uniform(0, total_rank)
current = 0
for i, genome in enumerate(self.population):
current += ranks[i]
if current > pick:
return genome
return self.population[-1]
def crossover(self, parent1: Genome, parent2: Genome) -> Tuple[Genome, Genome]:
"""Perform crossover between two parents."""
child1_genes = {}
child2_genes = {}
for gene_name in self.gene_names:
if random.random() < self.crossover_rate:
# Crossover: swap genes
child1_genes[gene_name] = Gene(
name=gene_name,
value=parent2.genes[gene_name].value,
mutation_rate=self.mutation_rate
)
child2_genes[gene_name] = Gene(
name=gene_name,
value=parent1.genes[gene_name].value,
mutation_rate=self.mutation_rate
)
else:
# No crossover: copy parent genes
child1_genes[gene_name] = Gene(
name=gene_name,
value=parent1.genes[gene_name].value,
mutation_rate=self.mutation_rate
)
child2_genes[gene_name] = Gene(
name=gene_name,
value=parent2.genes[gene_name].value,
mutation_rate=self.mutation_rate
)
child1 = Genome(
id=f"gen{self.current_generation + 1}_child",
genes=child1_genes,
generation=self.current_generation + 1,
parent_ids=[parent1.id, parent2.id]
)
child2 = Genome(
id=f"gen{self.current_generation + 1}_child",
genes=child2_genes,
generation=self.current_generation + 1,
parent_ids=[parent1.id, parent2.id]
)
return child1, child2
def mutate(self, genome: Genome):
"""Mutate a genome."""
for gene in genome.genes.values():
if random.random() < gene.mutation_rate:
# Gaussian mutation
mutation = random.gauss(0, 0.1)
gene.value = max(gene.min_value, min(gene.max_value, gene.value + mutation))
def evolve_generation(self, task: str):
"""Evolve one generation."""
self.current_generation += 1
# Evaluate current population
self.evaluate_population(task)
# Record statistics
fitnesses = [g.fitness for g in self.population]
diversity = self._calculate_diversity()
stats = GenerationStats(
generation=self.current_generation,
population_size=len(self.population),
best_fitness=max(fitnesses),
avg_fitness=sum(fitnesses) / len(fitnesses),
worst_fitness=min(fitnesses),
diversity=diversity
)
self.generation_stats.append(stats)
# Create new population
new_population = []
# Elitism: keep best individuals
new_population.extend(self.population[:self.elitism_count])
# Generate offspring
while len(new_population) < self.population_size:
parent1, parent2 = self.select_parents()
child1, child2 = self.crossover(parent1, parent2)
self.mutate(child1)
self.mutate(child2)
new_population.append(child1)
if len(new_population) 1:
improvement = (self.generation_stats[-1].avg_fitness -
self.generation_stats[-2].avg_fitness)
self.statistics["avg_fitness_improvement"] = improvement
def _calculate_diversity(self) -> float:
"""Calculate population diversity."""
if len(self.population) 0 else 0.0
def run_evolution(self, task: str, generations: int):
"""Run evolution for multiple generations."""
print(f"\n {self.name}: Running evolution for {generations} generations...")
if not self.population:
self.initialize_population()
for gen in range(generations):
self.evolve_generation(task)
if (gen + 1) % max(1, generations // 5) == 0:
stats = self.generation_stats[-1]
print(f" Generation {self.current_generation}: "
f"Best={stats.best_fitness:.3f}, "
f"Avg={stats.avg_fitness:.3f}, "
f"Diversity={stats.diversity:.3f}")
print(f" Evolution complete! Best fitness: {self.best_genome.fitness:.3f}")
def get_best_strategy(self) -> Dict[str, float]:
"""Get the best evolved strategy."""
if not self.best_genome:
return {}
return {gene_name: gene.value
for gene_name, gene in self.best_genome.genes.items()}
def get_statistics(self) -> Dict[str, Any]:
"""Get comprehensive statistics."""
return {
"name": self.name,
"selection_strategy": self.selection_strategy.value,
"population_size": self.population_size,
"current_generation": self.current_generation,
"best_fitness": self.best_genome.fitness if self.best_genome else 0.0,
**self.statistics
}
class EvolutionaryPattern:
"""
Evolutionary Pattern - Evolve agent strategies via genetic algorithms.
Coordinates multiple evolutionary agents optimizing different strategies.
"""
def __init__(self):
self.agents: Dict[str, EvolutionaryAgent] = {}
self.tasks: List[str] = []
def register_agent(self, agent: EvolutionaryAgent):
"""Register evolutionary agent."""
self.agents[agent.name] = agent
print(f" Registered agent: {agent.name} ({agent.selection_strategy.value})")
def add_task(self, task: str):
"""Add optimization task."""
self.tasks.append(task)
def evolve_all(self, generations: int):
"""Evolve all agents."""
print("\n Evolving all agents...")
for agent in self.agents.values():
for task in self.tasks:
agent.run_evolution(task, generations)
def run(self, tasks: List[str], generations: int = 20) -> Dict[str, Any]:
"""Run evolutionary optimization."""
print("=== Evolutionary Agent Pattern Demo ===\n")
# Add tasks
print("1. Adding tasks...")
for task in tasks:
self.add_task(task)
# Evolve all agents
print("\n2. Running evolution...")
self.evolve_all(generations)
# Get best strategies
print("\n3. Extracting best strategies...")
best_strategies = {name: agent.get_best_strategy()
for name, agent in self.agents.items()}
# Get statistics
stats = {name: agent.get_statistics()
for name, agent in self.agents.items()}
return {
"pattern": "Evolutionary",
"agents": len(self.agents),
"tasks": len(tasks),
"generations": generations,
"best_strategies": best_strategies,
"statistics": stats
}
# Example usage
if __name__ == "__main__":
print("=== Evolutionary Agent Pattern Demo ===\n")
# Create pattern
pattern = EvolutionaryPattern()
# Define gene names (strategy parameters to evolve)
gene_names = ["learning_rate", "exploration", "memory_size", "attention", "creativity"]
# Create agents with different selection strategies
print("1. Creating evolutionary agents...")
tournament_agent = EvolutionaryAgent(
name="TournamentAgent",
gene_names=gene_names,
population_size=30,
selection_strategy=SelectionStrategy.TOURNAMENT,
mutation_rate=0.1
)
pattern.register_agent(tournament_agent)
roulette_agent = EvolutionaryAgent(
name="RouletteAgent",
gene_names=gene_names,
population_size=30,
selection_strategy=SelectionStrategy.ROULETTE,
mutation_rate=0.15
)
pattern.register_agent(roulette_agent)
# Define tasks
tasks = ["optimize_learning_strategy", "maximize_performance"]
# Run evolution
print("\n2. Running evolutionary optimization...")
summary = pattern.run(tasks, generations=30)
print(f"\n3. Results:")
print(f" Agents: {summary['agents']}")
print(f" Tasks: {summary['tasks']}")
print(f" Generations: {summary['generations']}")
print(f"\n4. Best Evolved Strategies:")
for agent_name, strategy in summary['best_strategies'].items():
print(f"\n {agent_name}:")
for param, value in strategy.items():
print(f" {param}: {value:.3f}")
print(f"\n5. Evolution Statistics:")
for agent_name, stats in summary['statistics'].items():
print(f"\n {agent_name}:")
for key, value in stats.items():
if key != "name":
if isinstance(value, float):
print(f" {key}: {value:.4f}")
else:
print(f" {key}: {value}")
print("\nā Evolutionary algorithms optimized agent strategies!")
Agentic Architecture Pattern: Evolutionary Agent
Pattern 58: 58 Evolutionary Agent – Line-by-Line Explanation
Overview
Pattern 58: Evolutionary Agent
Purpose: Use genetic algorithms to evolve agent strategies and behaviors. Selection, crossover, and mutation to optimize agent parameters over generations.
Code Structure Analysis
File Information
- Pattern Number: 58
- Pattern Name: 58 Evolutionary Agent
- File Path:
patterns\learning_adaptation\58_evolutionary_agent.py - Total Lines: 486
Detailed Code Explanation
Module Docstring
"""
Pattern 58: Evolutionary Agent
Purpose: Use genetic algorithms to evolve agent strategies and behaviors.
Selection, crossover, and mutation to optimize agent parameters over generations.
"""
Explanation: This module docstring describes the purpose and key features of the 58 Evolutionary Agent pattern.
Imports Section
from typing import Dict, Any, List, Optional, Tuple
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
import random
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI # First 20 imports shown
Explanation: The module imports necessary dependencies:
– from typing import Dict, Any, List, Optional, Tuple: Type hints for better code documentation
– from dataclasses import dataclass, field: Data class decorators for clean data structures
– from datetime import datetime: Date and time utilities
– from enum import Enum: Enumeration support for fixed sets of values
– from langchain_core.prompts import ChatPromptTemplate: LangChain framework integration
– from langchain_openai import ChatOpenAI: LangChain framework integration
Enumerations
SelectionStrategy (Line 17)
class SelectionStrategy(Enum):
TOURNAMENT
ROULETTE
RANK
ELITISM
Purpose: Selection strategies for evolution.
Values:
– TOURNAMENT: Represents tournament
– ROULETTE: Represents roulette
– RANK: Represents rank
– ELITISM: Represents elitism
Classes
SelectionStrategy (Line 17)
Description: Selection strategies for evolution.
Gene (Line 26)
Description: A gene representing part of an agent’s strategy.
Genome (Line 36)
Description: A genome representing an agent’s complete strategy.
GenerationStats (Line 46)
Description: Statistics for a generation.
EvolutionaryAgent (Line 57)
Description: Evolutionary Agent – Evolve strategies using genetic algorithms.
Uses selection, crossover, and mutation to optimize agent behaviors over multiple generations. LLM assists in evaluating fitness.
Methods:
__init__ (Line 65)
def __init__(self, name, gene_names, population_size, selection_strategy, mutation_rate, crossover_rate, elitism_count):
"""No method docstring."""
Purpose: No method docstring.
Parameters:
– self: Instance reference
– name: Identifier or label
– gene_names: Parameter for gene_names
– population_size: Parameter for population_size
– selection_strategy: Parameter for selection_strategy
– mutation_rate: Parameter for mutation_rate
– crossover_rate: Parameter for crossover_rate
– elitism_count: Parameter for elitism_count
initialize_population (Line 99)
def initialize_population(self):
"""Initialize random population."""
Purpose: Initialize random population.
Parameters:
– self: Instance reference
evaluate_fitness (Line 122)
def evaluate_fitness(self, genome, task):
"""Evaluate fitness of a genome for a task."""
Purpose: Evaluate fitness of a genome for a task.
Parameters:
– self: Instance reference
– genome: Parameter for genome
– task: Parameter for task
evaluate_population (Line 144)
def evaluate_population(self, task):
"""Evaluate fitness for entire population."""
Purpose: Evaluate fitness for entire population.
Parameters:
– self: Instance reference
– task: Parameter for task
select_parents (Line 157)
def select_parents(self):
"""Select two parents for reproduction."""
Purpose: Select two parents for reproduction.
Parameters:
– self: Instance reference
_tournament_selection (Line 170)
def _tournament_selection(self, tournament_size):
"""Tournament selection."""
Purpose: Tournament selection.
Parameters:
– self: Instance reference
– tournament_size: Parameter for tournament_size
_roulette_selection (Line 175)
def _roulette_selection(self):
"""Roulette wheel selection."""
Purpose: Roulette wheel selection.
Parameters:
– self: Instance reference
_rank_selection (Line 189)
def _rank_selection(self):
"""Rank-based selection."""
Purpose: Rank-based selection.
Parameters:
– self: Instance reference
crossover (Line 203)
def crossover(self, parent1, parent2):
"""Perform crossover between two parents."""
Purpose: Perform crossover between two parents.
Parameters:
– self: Instance reference
– parent1: Parameter for parent1
– parent2: Parameter for parent2
mutate (Line 250)
def mutate(self, genome):
"""Mutate a genome."""
Purpose: Mutate a genome.
Parameters:
– self: Instance reference
– genome: Parameter for genome
evolve_generation (Line 258)
def evolve_generation(self, task):
"""Evolve one generation."""
Purpose: Evolve one generation.
Parameters:
– self: Instance reference
– task: Parameter for task
_calculate_diversity (Line 306)
def _calculate_diversity(self):
"""Calculate population diversity."""
Purpose: Calculate population diversity.
Parameters:
– self: Instance reference
run_evolution (Line 328)
def run_evolution(self, task, generations):
"""Run evolution for multiple generations."""
Purpose: Run evolution for multiple generations.
Parameters:
– self: Instance reference
– task: Parameter for task
– generations: Parameter for generations
get_best_strategy (Line 347)
def get_best_strategy(self):
"""Get the best evolved strategy."""
Purpose: Get the best evolved strategy.
Parameters:
– self: Instance reference
get_statistics (Line 355)
def get_statistics(self):
"""Get comprehensive statistics."""
Purpose: Get comprehensive statistics.
Parameters:
– self: Instance reference
EvolutionaryPattern (Line 367)
Description: Evolutionary Pattern – Evolve agent strategies via genetic algorithms.
Coordinates multiple evolutionary agents optimizing different strategies.
Methods:
__init__ (Line 374)
def __init__(self):
"""No method docstring."""
Purpose: No method docstring.
Parameters:
– self: Instance reference
register_agent (Line 378)
def register_agent(self, agent):
"""Register evolutionary agent."""
Purpose: Register evolutionary agent.
Parameters:
– self: Instance reference
– agent: Parameter for agent
add_task (Line 383)
def add_task(self, task):
"""Add optimization task."""
Purpose: Add optimization task.
Parameters:
– self: Instance reference
– task: Parameter for task
evolve_all (Line 387)
def evolve_all(self, generations):
"""Evolve all agents."""
Purpose: Evolve all agents.
Parameters:
– self: Instance reference
– generations: Parameter for generations
run (Line 395)
def run(self, tasks, generations):
"""Run evolutionary optimization."""
Purpose: Run evolutionary optimization.
Parameters:
– self: Instance reference
– tasks: Parameter for tasks
– generations: Parameter for generations
Design Patterns Used
- Strategy Pattern: Different execution strategies for various scenarios
- State Pattern: Managing different states in the workflow
- Chain of Responsibility: Sequential processing through workflow nodes
- Observer Pattern: Tracking and monitoring pattern execution
- Factory Pattern: Creating instances with proper configuration
Usage Example
# Initialize the pattern
pattern = SelectionStrategy(name="example", model_name="gpt-4")
# Configure and execute
result = pattern.execute(data={"key": "value"})
# Check results
print(result)
Key Concepts
- LLM Integration: Uses LangChain/OpenAI for intelligent decision-making
- Workflow Orchestration: LangGraph manages execution flow through nodes
- State Management: TypedDict defines workflow state structure
- Error Handling: Comprehensive error handling and fallback mechanisms
- Extensibility: Designed for easy customization and extension
Extension Points
This pattern can be extended by: 1. Adding custom workflow nodes 2. Implementing additional validation logic 3. Integrating with external services 4. Customizing LLM prompts and responses 5. Adding monitoring and logging capabilities
Leave a Reply