use rand::Rng;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
pub struct Fitness(pub f64);
impl Fitness {
pub fn new(value: f64) -> Self {
Self(value.clamp(0.0, 1.0))
}
pub fn perfect() -> Self {
Self(1.0)
}
pub fn zero() -> Self {
Self(0.0)
}
pub fn value(&self) -> f64 {
self.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Genome {
pub prompt: String,
pub traits: Vec<f64>,
pub trait_names: Vec<String>,
}
impl Genome {
pub fn new(prompt: &str) -> Self {
Self {
prompt: prompt.to_string(),
traits: vec![0.5; 5], trait_names: vec![
"exploration".to_string(),
"precision".to_string(),
"creativity".to_string(),
"skepticism".to_string(),
"verbosity".to_string(),
],
}
}
pub fn with_traits(prompt: &str, traits: Vec<(String, f64)>) -> Self {
let (names, values): (Vec<_>, Vec<_>) = traits.into_iter().unzip();
Self {
prompt: prompt.to_string(),
traits: values,
trait_names: names,
}
}
pub fn get_trait(&self, name: &str) -> Option<f64> {
self.trait_names
.iter()
.position(|n| n == name)
.map(|i| self.traits[i])
}
pub fn set_trait(&mut self, name: &str, value: f64) {
if let Some(i) = self.trait_names.iter().position(|n| n == name) {
self.traits[i] = value.clamp(0.0, 1.0);
}
}
pub fn to_llm_params(&self) -> LlmParams {
let exploration = self.get_trait("exploration").unwrap_or(0.5);
let precision = self.get_trait("precision").unwrap_or(0.5);
let creativity = self.get_trait("creativity").unwrap_or(0.5);
let skepticism = self.get_trait("skepticism").unwrap_or(0.5);
let verbosity = self.get_trait("verbosity").unwrap_or(0.5);
LlmParams {
temperature: 0.1 + exploration * 1.4,
top_p: 1.0 - (precision * 0.5),
presence_penalty: creativity,
frequency_penalty: skepticism * 0.5,
max_tokens_multiplier: 0.5 + verbosity * 1.5,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
pub struct LlmParams {
pub temperature: f64,
pub top_p: f64,
pub presence_penalty: f64,
pub frequency_penalty: f64,
pub max_tokens_multiplier: f64,
}
impl LlmParams {
pub fn max_tokens(&self, base: u32) -> u32 {
((base as f64) * self.max_tokens_multiplier) as u32
}
pub fn conservative() -> Self {
Self {
temperature: 0.3,
top_p: 0.9,
presence_penalty: 0.0,
frequency_penalty: 0.1,
max_tokens_multiplier: 1.0,
}
}
pub fn creative() -> Self {
Self {
temperature: 1.2,
top_p: 0.95,
presence_penalty: 0.5,
frequency_penalty: 0.0,
max_tokens_multiplier: 1.5,
}
}
}
pub trait GeneticOperator {
fn crossover(&self, parent_a: &Genome, parent_b: &Genome) -> Genome;
fn mutate(&self, genome: &mut Genome, mutation_rate: f64);
}
#[derive(Debug, Clone, Default)]
pub struct StandardOperator;
impl GeneticOperator for StandardOperator {
fn crossover(&self, parent_a: &Genome, parent_b: &Genome) -> Genome {
let mut rng = rand::thread_rng();
let crossover_point = rng.gen_range(0..parent_a.traits.len());
let mut child_traits = Vec::with_capacity(parent_a.traits.len());
for i in 0..parent_a.traits.len() {
if i < crossover_point {
child_traits.push(parent_a.traits[i]);
} else {
child_traits.push(parent_b.traits[i]);
}
}
let prompt = if rng.gen_bool(0.5) {
parent_a.prompt.clone()
} else {
parent_b.prompt.clone()
};
Genome {
prompt,
traits: child_traits,
trait_names: parent_a.trait_names.clone(),
}
}
fn mutate(&self, genome: &mut Genome, mutation_rate: f64) {
let mut rng = rand::thread_rng();
for trait_val in &mut genome.traits {
if rng.gen_bool(mutation_rate) {
let delta: f64 = rng.gen_range(-0.2..0.2);
*trait_val = (*trait_val + delta).clamp(0.0, 1.0);
}
}
}
}
pub fn tournament_select(population: &[(Genome, Fitness)], tournament_size: usize) -> &Genome {
let mut rng = rand::thread_rng();
let mut best: Option<&(Genome, Fitness)> = None;
for _ in 0..tournament_size {
let idx = rng.gen_range(0..population.len());
let candidate = &population[idx];
if best.is_none() || candidate.1 > best.unwrap().1 {
best = Some(candidate);
}
}
&best.unwrap().0
}
impl StandardOperator {
pub fn produce_next_generation(
&self,
population: &[(Genome, Fitness)],
size: usize,
mutation_rate: f64,
tournament_size: usize,
) -> Vec<Genome> {
use rayon::prelude::*;
(0..size)
.into_par_iter()
.map(|_| {
let parent_a = tournament_select(population, tournament_size);
let parent_b = tournament_select(population, tournament_size);
let mut child = self.crossover(parent_a, parent_b);
self.mutate(&mut child, mutation_rate);
child
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_genome_traits() {
let mut genome = Genome::new("Test agent");
assert_eq!(genome.get_trait("exploration"), Some(0.5));
genome.set_trait("exploration", 0.8);
assert_eq!(genome.get_trait("exploration"), Some(0.8));
}
#[test]
fn test_crossover() {
let parent_a =
Genome::with_traits("A", vec![("a".to_string(), 0.0), ("b".to_string(), 0.0)]);
let parent_b =
Genome::with_traits("B", vec![("a".to_string(), 1.0), ("b".to_string(), 1.0)]);
let operator = StandardOperator;
let child = operator.crossover(&parent_a, &parent_b);
assert!(child.traits.iter().all(|&t| t == 0.0 || t == 1.0));
}
#[test]
fn test_mutation() {
let mut genome = Genome::new("Test");
let original_traits = genome.traits.clone();
let operator = StandardOperator;
operator.mutate(&mut genome, 1.0);
assert!(genome
.traits
.iter()
.zip(original_traits.iter())
.any(|(a, b)| a != b));
}
}