279 lines
9.8 KiB
Rust
279 lines
9.8 KiB
Rust
|
|
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<AgentProfile>,
|
||
|
|
task_type: &str,
|
||
|
|
learning_profiles: &[(String, LearningProfile)],
|
||
|
|
) -> Vec<AgentScore> {
|
||
|
|
let mut scores: Vec<AgentScore> = 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<AgentProfile>,
|
||
|
|
task_type: &str,
|
||
|
|
learning_profiles: &[(String, LearningProfile)],
|
||
|
|
) -> Option<AgentScore> {
|
||
|
|
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<AgentProfile>,
|
||
|
|
task_type: &str,
|
||
|
|
learning_profiles: &[(String, LearningProfile)],
|
||
|
|
) -> Vec<AgentScore> {
|
||
|
|
let mut scores: Vec<AgentScore> = 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);
|
||
|
|
}
|
||
|
|
}
|