1use rand::Rng;
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
14pub struct Fitness(pub f64);
15
16impl Fitness {
17 pub fn new(value: f64) -> Self {
19 Self(value.clamp(0.0, 1.0))
20 }
21
22 pub fn perfect() -> Self {
24 Self(1.0)
25 }
26
27 pub fn zero() -> Self {
29 Self(0.0)
30 }
31
32 pub fn value(&self) -> f64 {
34 self.0
35 }
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct Genome {
41 pub prompt: String,
43 pub traits: Vec<f64>,
45 pub trait_names: Vec<String>,
47}
48
49impl Genome {
50 pub fn new(prompt: &str) -> Self {
52 Self {
53 prompt: prompt.to_string(),
54 traits: vec![0.5; 5], trait_names: vec![
56 "exploration".to_string(),
57 "precision".to_string(),
58 "creativity".to_string(),
59 "skepticism".to_string(),
60 "verbosity".to_string(),
61 ],
62 }
63 }
64
65 pub fn with_traits(prompt: &str, traits: Vec<(String, f64)>) -> Self {
67 let (names, values): (Vec<_>, Vec<_>) = traits.into_iter().unzip();
68 Self {
69 prompt: prompt.to_string(),
70 traits: values,
71 trait_names: names,
72 }
73 }
74
75 pub fn get_trait(&self, name: &str) -> Option<f64> {
77 self.trait_names
78 .iter()
79 .position(|n| n == name)
80 .map(|i| self.traits[i])
81 }
82
83 pub fn set_trait(&mut self, name: &str, value: f64) {
85 if let Some(i) = self.trait_names.iter().position(|n| n == name) {
86 self.traits[i] = value.clamp(0.0, 1.0);
87 }
88 }
89
90 pub fn to_llm_params(&self) -> LlmParams {
92 self.to_llm_params_with_config(&TraitMappingConfig::default())
93 }
94
95 pub fn to_llm_params_with_config(&self, config: &TraitMappingConfig) -> LlmParams {
103 let exploration = self.get_trait("exploration").unwrap_or(0.5);
104 let precision = self.get_trait("precision").unwrap_or(0.5);
105 let creativity = self.get_trait("creativity").unwrap_or(0.5);
106 let skepticism = self.get_trait("skepticism").unwrap_or(0.5);
107 let verbosity = self.get_trait("verbosity").unwrap_or(0.5);
108
109 let (temp_min, temp_max) = config.temperature_range;
110 let (tok_min, tok_max) = config.max_tokens_range;
111
112 LlmParams {
113 temperature: temp_min + exploration * (temp_max - temp_min),
114 top_p: 1.0 - (precision * config.top_p_reduction),
115 presence_penalty: creativity,
116 frequency_penalty: skepticism * config.frequency_penalty_max,
117 max_tokens_multiplier: tok_min + verbosity * (tok_max - tok_min),
118 }
119 }
120}
121
122#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
125pub struct TraitMappingConfig {
126 pub temperature_range: (f64, f64),
128 pub top_p_reduction: f64,
130 pub frequency_penalty_max: f64,
132 pub max_tokens_range: (f64, f64),
134}
135
136impl Default for TraitMappingConfig {
137 fn default() -> Self {
138 Self {
139 temperature_range: (0.1, 1.5),
140 top_p_reduction: 0.5,
141 frequency_penalty_max: 0.5,
142 max_tokens_range: (0.5, 2.0),
143 }
144 }
145}
146
147#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
149pub struct LlmParams {
150 pub temperature: f64,
152 pub top_p: f64,
154 pub presence_penalty: f64,
156 pub frequency_penalty: f64,
158 pub max_tokens_multiplier: f64,
160}
161
162impl LlmParams {
163 pub fn max_tokens(&self, base: u32) -> u32 {
165 ((base as f64) * self.max_tokens_multiplier) as u32
166 }
167
168 pub fn conservative() -> Self {
170 Self {
171 temperature: 0.3,
172 top_p: 0.9,
173 presence_penalty: 0.0,
174 frequency_penalty: 0.1,
175 max_tokens_multiplier: 1.0,
176 }
177 }
178
179 pub fn creative() -> Self {
181 Self {
182 temperature: 1.2,
183 top_p: 0.95,
184 presence_penalty: 0.5,
185 frequency_penalty: 0.0,
186 max_tokens_multiplier: 1.5,
187 }
188 }
189}
190
191pub trait GeneticOperator {
193 fn crossover(&self, parent_a: &Genome, parent_b: &Genome) -> Genome;
195
196 fn mutate(&self, genome: &mut Genome, mutation_rate: f64);
198}
199
200#[derive(Debug, Clone, Default)]
202pub struct StandardOperator;
203
204impl GeneticOperator for StandardOperator {
205 fn crossover(&self, parent_a: &Genome, parent_b: &Genome) -> Genome {
206 let mut rng = rand::thread_rng();
207
208 let crossover_point = rng.gen_range(0..parent_a.traits.len());
210 let mut child_traits = Vec::with_capacity(parent_a.traits.len());
211
212 for i in 0..parent_a.traits.len() {
213 if i < crossover_point {
214 child_traits.push(parent_a.traits[i]);
215 } else {
216 child_traits.push(parent_b.traits[i]);
217 }
218 }
219
220 let prompt = if rng.gen_bool(0.5) {
222 parent_a.prompt.clone()
223 } else {
224 parent_b.prompt.clone()
225 };
226
227 Genome {
228 prompt,
229 traits: child_traits,
230 trait_names: parent_a.trait_names.clone(),
231 }
232 }
233
234 fn mutate(&self, genome: &mut Genome, mutation_rate: f64) {
235 let mut rng = rand::thread_rng();
236
237 for trait_val in &mut genome.traits {
238 if rng.gen_bool(mutation_rate) {
239 let delta: f64 = rng.gen_range(-0.2..0.2);
241 *trait_val = (*trait_val + delta).clamp(0.0, 1.0);
242 }
243 }
244 }
245}
246
247pub fn tournament_select(population: &[(Genome, Fitness)], tournament_size: usize) -> &Genome {
249 let mut rng = rand::thread_rng();
250 let mut best: Option<&(Genome, Fitness)> = None;
251
252 for _ in 0..tournament_size {
253 let idx = rng.gen_range(0..population.len());
254 let candidate = &population[idx];
255
256 if best.is_none() || candidate.1 > best.unwrap().1 {
257 best = Some(candidate);
258 }
259 }
260
261 &best.unwrap().0
262}
263
264impl StandardOperator {
265 pub fn produce_next_generation(
267 &self,
268 population: &[(Genome, Fitness)],
269 size: usize,
270 mutation_rate: f64,
271 tournament_size: usize,
272 ) -> Vec<Genome> {
273 use rayon::prelude::*;
274
275 (0..size)
276 .into_par_iter()
277 .map(|_| {
278 let parent_a = tournament_select(population, tournament_size);
279 let parent_b = tournament_select(population, tournament_size);
280
281 let mut child = self.crossover(parent_a, parent_b);
282 self.mutate(&mut child, mutation_rate);
283 child
284 })
285 .collect()
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 #[test]
294 fn test_genome_traits() {
295 let mut genome = Genome::new("Test agent");
296 assert_eq!(genome.get_trait("exploration"), Some(0.5));
297
298 genome.set_trait("exploration", 0.8);
299 assert_eq!(genome.get_trait("exploration"), Some(0.8));
300 }
301
302 #[test]
303 fn test_crossover() {
304 let parent_a =
305 Genome::with_traits("A", vec![("a".to_string(), 0.0), ("b".to_string(), 0.0)]);
306 let parent_b =
307 Genome::with_traits("B", vec![("a".to_string(), 1.0), ("b".to_string(), 1.0)]);
308
309 let operator = StandardOperator;
310 let child = operator.crossover(&parent_a, &parent_b);
311
312 assert!(child.traits.iter().all(|&t| t == 0.0 || t == 1.0));
314 }
315
316 #[test]
317 fn test_mutation_preserves_bounds() {
318 let operator = StandardOperator;
319 for _ in 0..100 {
320 let mut genome = Genome::new("Test");
321 operator.mutate(&mut genome, 1.0);
322 for &t in &genome.traits {
323 assert!((0.0..=1.0).contains(&t), "Trait {} out of bounds", t);
324 }
325 }
326 }
327
328 #[test]
329 fn test_crossover_identical_parents() {
330 let parent = Genome::with_traits("P", vec![("a".to_string(), 0.5), ("b".to_string(), 0.7)]);
331 let operator = StandardOperator;
332 let child = operator.crossover(&parent, &parent);
333 assert_eq!(child.traits, parent.traits);
335 }
336
337 #[test]
338 fn test_llm_params_defaults() {
339 let genome = Genome::new("Test");
340 let params = genome.to_llm_params();
341 assert!(params.temperature > 0.0 && params.temperature < 2.0);
343 assert!(params.top_p > 0.0 && params.top_p <= 1.0);
344 assert!(params.presence_penalty >= 0.0 && params.presence_penalty <= 1.0);
345 assert!(params.frequency_penalty >= 0.0);
346 assert!(params.max_tokens_multiplier > 0.0);
347 }
348
349 #[test]
350 fn test_mutation() {
351 let mut genome = Genome::new("Test");
352 let original_traits = genome.traits.clone();
353
354 let operator = StandardOperator;
355 operator.mutate(&mut genome, 1.0); assert!(genome
359 .traits
360 .iter()
361 .zip(original_traits.iter())
362 .any(|(a, b)| a != b));
363 }
364}