use crate::learning_profile::LearningProfile; use vapora_swarm::messages::AgentProfile; /// Unified agent score combining SwarmCoordinator metrics and learning expertise. #[derive(Debug, Clone)] pub struct AgentScore { /// Agent identifier pub agent_id: String, /// Base score from SwarmCoordinator: success_rate / (1 + current_load) pub base_score: f64, /// Expertise score from LearningProfile for specific task type (0.0-1.0) pub expertise_score: f64, /// Confidence in expertise score based on execution count (0.0-1.0) pub confidence: f64, /// Final combined score: 0.3*base + 0.5*expertise + 0.2*confidence pub final_score: f64, /// Human-readable explanation of scoring breakdown pub reasoning: String, } /// Service for ranking agents based on learning profiles and swarm metrics. pub struct AgentScoringService; impl AgentScoringService { /// Rank candidate agents for task assignment using combined scoring. /// /// Scoring formula: /// - base_score = success_rate / (1 + current_load) [from SwarmCoordinator] /// - expertise_score = learned success rate for task_type /// - confidence = min(1.0, total_executions / 20) [prevents overfitting] /// - final_score = 0.3*base + 0.5*expertise + 0.2*confidence /// /// Returns agents ranked by final_score (highest first). pub fn rank_agents( candidates: Vec, task_type: &str, learning_profiles: &[(String, LearningProfile)], ) -> Vec { let mut scores: Vec = candidates .into_iter() .map(|agent| { let base_score = agent.success_rate / (1.0 + agent.current_load); let (expertise_score, confidence) = learning_profiles .iter() .find(|(id, _)| id == &agent.id) .map(|(_, profile)| { ( profile.get_task_type_score(task_type), profile.get_confidence(task_type), ) }) .unwrap_or((agent.success_rate, 0.0)); let final_score = 0.3 * base_score + 0.5 * expertise_score + 0.2 * confidence; let reasoning = format!( "{}(base={:.2}, load={:.2}, expertise={:.2}, confidence={:.2})", agent.id, base_score, agent.current_load, expertise_score, confidence ); AgentScore { agent_id: agent.id.clone(), base_score, expertise_score, confidence, final_score, reasoning, } }) .collect(); scores.sort_by(|a, b| { b.final_score .partial_cmp(&a.final_score) .unwrap_or(std::cmp::Ordering::Equal) }); scores } /// Select best agent from candidates for task assignment. /// Returns the top-ranked agent or None if no candidates available. pub fn select_best( candidates: Vec, task_type: &str, learning_profiles: &[(String, LearningProfile)], ) -> Option { Self::rank_agents(candidates, task_type, learning_profiles) .into_iter() .next() } /// Calculate blended score prioritizing task-type expertise. /// Uses recent_success_rate if available (recency bias from learning profile). pub fn rank_agents_with_recency( candidates: Vec, task_type: &str, learning_profiles: &[(String, LearningProfile)], ) -> Vec { let mut scores: Vec = candidates .into_iter() .map(|agent| { let base_score = agent.success_rate / (1.0 + agent.current_load); let (expertise_score, confidence) = learning_profiles .iter() .find(|(id, _)| id == &agent.id) .map(|(_, profile)| { // Use recent_success_rate if available (weighted 3x for last 7 days) let recent = profile.get_recent_score(task_type); let conf = profile.get_confidence(task_type); (recent, conf) }) .unwrap_or((agent.success_rate, 0.0)); let final_score = 0.3 * base_score + 0.5 * expertise_score + 0.2 * confidence; let reasoning = format!( "{}(recent={:.2}, confidence={:.2})", agent.id, expertise_score, confidence ); AgentScore { agent_id: agent.id.clone(), base_score, expertise_score, confidence, final_score, reasoning, } }) .collect(); scores.sort_by(|a, b| { b.final_score .partial_cmp(&a.final_score) .unwrap_or(std::cmp::Ordering::Equal) }); scores } } #[cfg(test)] mod tests { use super::*; fn create_mock_agent(id: &str, success_rate: f64, load: f64) -> AgentProfile { AgentProfile { id: id.to_string(), roles: vec![], capabilities: vec![], current_load: load, success_rate, availability: true, } } fn create_mock_learning(agent_id: &str, expertise: f64, confidence: f64) -> LearningProfile { use crate::learning_profile::TaskTypeExpertise; let mut profile = LearningProfile::new(agent_id.to_string()); let exp = TaskTypeExpertise { success_rate: expertise, total_executions: (confidence * 20.0) as u32, recent_success_rate: expertise, avg_duration_ms: 100.0, learning_curve: Vec::new(), confidence, }; profile.set_task_type_expertise("coding".to_string(), exp); profile } #[test] fn test_rank_agents_basic() { let candidates = vec![ create_mock_agent("agent-a", 0.8, 0.4), create_mock_agent("agent-b", 0.6, 0.2), create_mock_agent("agent-c", 0.9, 0.5), ]; let learning = vec![ ("agent-a".to_string(), create_mock_learning("agent-a", 0.85, 0.8)), ("agent-b".to_string(), create_mock_learning("agent-b", 0.70, 0.6)), ("agent-c".to_string(), create_mock_learning("agent-c", 0.75, 0.5)), ]; let ranked = AgentScoringService::rank_agents(candidates, "coding", &learning); assert_eq!(ranked.len(), 3); // Verify sorted by final_score descending for i in 1..ranked.len() { assert!(ranked[i - 1].final_score >= ranked[i].final_score); } } #[test] fn test_select_best() { let candidates = vec![ create_mock_agent("agent-a", 0.8, 0.4), create_mock_agent("agent-b", 0.6, 0.2), ]; let learning = vec![ ("agent-a".to_string(), create_mock_learning("agent-a", 0.85, 0.8)), ("agent-b".to_string(), create_mock_learning("agent-b", 0.70, 0.6)), ]; let best = AgentScoringService::select_best(candidates, "coding", &learning); assert!(best.is_some()); assert_eq!(best.unwrap().agent_id, "agent-a"); } #[test] fn test_rank_agents_no_learning_data() { let candidates = vec![ create_mock_agent("agent-a", 0.8, 0.4), create_mock_agent("agent-b", 0.6, 0.2), ]; let ranked = AgentScoringService::rank_agents(candidates, "coding", &[]); assert_eq!(ranked.len(), 2); // Should still rank by base score when no learning data assert!(ranked[0].final_score > 0.0); } #[test] fn test_recency_bias_scoring() { let candidates = vec![ create_mock_agent("agent-a", 0.5, 0.3), create_mock_agent("agent-b", 0.5, 0.3), ]; let mut learning_a = LearningProfile::new("agent-a".to_string()); use crate::learning_profile::TaskTypeExpertise; learning_a.set_task_type_expertise( "coding".to_string(), TaskTypeExpertise { success_rate: 0.7, total_executions: 20, recent_success_rate: 0.95, // Recent success much higher avg_duration_ms: 100.0, learning_curve: Vec::new(), confidence: 1.0, }, ); let learning = vec![("agent-a".to_string(), learning_a)]; let ranked = AgentScoringService::rank_agents_with_recency(candidates, "coding", &learning); assert_eq!(ranked.len(), 2); // agent-a should rank higher due to recent success assert_eq!(ranked[0].agent_id, "agent-a"); } #[test] fn test_confidence_weights_low_sample_count() { let candidates = vec![ create_mock_agent("agent-a", 0.9, 0.0), // High success but... create_mock_agent("agent-b", 0.8, 0.0), // Moderate success ]; let learning = vec![ ("agent-a".to_string(), create_mock_learning("agent-a", 0.9, 0.05)), // Low confidence ("agent-b".to_string(), create_mock_learning("agent-b", 0.8, 0.95)), // High confidence ]; let ranked = AgentScoringService::rank_agents(candidates, "coding", &learning); // agent-b should rank higher due to higher confidence despite lower expertise assert_eq!(ranked[0].agent_id, "agent-b"); } #[test] fn test_empty_candidates() { let ranked = AgentScoringService::rank_agents(Vec::new(), "coding", &[]); assert_eq!(ranked.len(), 0); } }