Implement intelligent agent learning from Knowledge Graph execution history with per-task-type expertise tracking, recency bias, and learning curves. ## Phase 5.3 Implementation ### Learning Infrastructure (✅ Complete) - LearningProfileService with per-task-type expertise metrics - TaskTypeExpertise model tracking success_rate, confidence, learning curves - Recency bias weighting: recent 7 days weighted 3x higher (exponential decay) - Confidence scoring prevents overfitting: min(1.0, executions / 20) - Learning curves computed from daily execution windows ### Agent Scoring Service (✅ Complete) - Unified AgentScore combining SwarmCoordinator + learning profiles - Scoring formula: 0.3*base + 0.5*expertise + 0.2*confidence - Rank agents by combined score for intelligent assignment - Support for recency-biased scoring (recent_success_rate) - Methods: rank_agents, select_best, rank_agents_with_recency ### KG Integration (✅ Complete) - KGPersistence::get_executions_for_task_type() - query by agent + task type - KGPersistence::get_agent_executions() - all executions for agent - Coordinator::load_learning_profile_from_kg() - core KG→Learning integration - Coordinator::load_all_learning_profiles() - batch load for multiple agents - Convert PersistedExecution → ExecutionData for learning calculations ### Agent Assignment Integration (✅ Complete) - AgentCoordinator uses learning profiles for task assignment - extract_task_type() infers task type from title/description - assign_task() scores candidates using AgentScoringService - Fallback to load-based selection if no learning data available - Learning profiles stored in coordinator.learning_profiles RwLock ### Profile Adapter Enhancements (✅ Complete) - create_learning_profile() - initialize empty profiles - add_task_type_expertise() - set task-type expertise - update_profile_with_learning() - update swarm profiles from learning ## Files Modified ### vapora-knowledge-graph/src/persistence.rs (+30 lines) - get_executions_for_task_type(agent_id, task_type, limit) - get_agent_executions(agent_id, limit) ### vapora-agents/src/coordinator.rs (+100 lines) - load_learning_profile_from_kg() - core KG integration method - load_all_learning_profiles() - batch loading for agents - assign_task() already uses learning-based scoring via AgentScoringService ### Existing Complete Implementation - vapora-knowledge-graph/src/learning.rs - calculation functions - vapora-agents/src/learning_profile.rs - data structures and expertise - vapora-agents/src/scoring.rs - unified scoring service - vapora-agents/src/profile_adapter.rs - adapter methods ## Tests Passing - learning_profile: 7 tests ✅ - scoring: 5 tests ✅ - profile_adapter: 6 tests ✅ - coordinator: learning-specific tests ✅ ## Data Flow 1. Task arrives → AgentCoordinator::assign_task() 2. Extract task_type from description 3. Query KG for task-type executions (load_learning_profile_from_kg) 4. Calculate expertise with recency bias 5. Score candidates (SwarmCoordinator + learning) 6. Assign to top-scored agent 7. Execution result → KG → Update learning profiles ## Key Design Decisions ✅ Recency bias: 7-day half-life with 3x weight for recent performance ✅ Confidence scoring: min(1.0, total_executions / 20) prevents overfitting ✅ Hierarchical scoring: 30% base load, 50% expertise, 20% confidence ✅ KG query limit: 100 recent executions per task-type for performance ✅ Async loading: load_learning_profile_from_kg supports concurrent loads ## Next: Phase 5.4 - Cost Optimization Ready to implement budget enforcement and cost-aware provider selection.
219 lines
8.0 KiB
Rust
219 lines
8.0 KiB
Rust
// Profile adapter: AgentMetadata + KG metrics → Swarm AgentProfile
|
|
// Phase 5.2: Bridges agent registry with swarm coordination
|
|
// Phase 5.3: Integrates per-task-type learning profiles from KG
|
|
|
|
use crate::learning_profile::{LearningProfile, TaskTypeExpertise};
|
|
use crate::registry::AgentMetadata;
|
|
use vapora_swarm::messages::AgentProfile;
|
|
|
|
/// Adapter that converts AgentMetadata to SwarmCoordinator AgentProfile
|
|
pub struct ProfileAdapter;
|
|
|
|
impl ProfileAdapter {
|
|
/// Create a swarm profile from agent metadata
|
|
pub fn create_profile(agent: &AgentMetadata) -> AgentProfile {
|
|
// Extract roles from capabilities (simplistic mapping)
|
|
let roles = agent
|
|
.capabilities
|
|
.iter()
|
|
.take(1)
|
|
.cloned()
|
|
.collect();
|
|
|
|
AgentProfile {
|
|
id: agent.id.clone(),
|
|
roles,
|
|
capabilities: agent.capabilities.clone(),
|
|
current_load: agent.current_tasks as f64 / agent.max_concurrent_tasks as f64,
|
|
success_rate: 0.5, // Default: neutral until KG metrics available
|
|
availability: agent.status == crate::registry::AgentStatus::Active,
|
|
}
|
|
}
|
|
|
|
/// Create profiles for multiple agents
|
|
pub fn batch_create_profiles(agents: Vec<AgentMetadata>) -> Vec<AgentProfile> {
|
|
agents.into_iter().map(|agent| Self::create_profile(&agent)).collect()
|
|
}
|
|
|
|
/// Update profile from KG success rate (Phase 5.5 integration)
|
|
pub fn update_with_kg_metrics(mut profile: AgentProfile, success_rate: f64) -> AgentProfile {
|
|
profile.success_rate = success_rate;
|
|
profile
|
|
}
|
|
|
|
/// Create learning profile from agent with task-type expertise.
|
|
/// Integrates per-task-type learning data from KG for intelligent assignment.
|
|
pub fn create_learning_profile(agent_id: String) -> LearningProfile {
|
|
LearningProfile::new(agent_id)
|
|
}
|
|
|
|
/// Enhance learning profile with task-type expertise from KG data.
|
|
/// Updates the profile with calculated expertise for specific task type.
|
|
pub fn add_task_type_expertise(
|
|
mut profile: LearningProfile,
|
|
task_type: String,
|
|
expertise: TaskTypeExpertise,
|
|
) -> LearningProfile {
|
|
profile.set_task_type_expertise(task_type, expertise);
|
|
profile
|
|
}
|
|
|
|
/// Update agent profile success rate from learning profile task-type score.
|
|
/// Uses learned expertise for the specified task type, with fallback to default.
|
|
pub fn update_profile_with_learning(
|
|
mut profile: AgentProfile,
|
|
learning_profile: &LearningProfile,
|
|
task_type: &str,
|
|
) -> AgentProfile {
|
|
profile.success_rate = learning_profile.get_task_type_score(task_type);
|
|
profile
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_profile_creation_from_metadata() {
|
|
let agent = AgentMetadata {
|
|
id: "agent-1".to_string(),
|
|
role: "developer".to_string(),
|
|
name: "Dev Agent 1".to_string(),
|
|
version: "0.1.0".to_string(),
|
|
status: crate::registry::AgentStatus::Active,
|
|
capabilities: vec!["coding".to_string(), "review".to_string()],
|
|
llm_provider: "claude".to_string(),
|
|
llm_model: "claude-sonnet-4".to_string(),
|
|
max_concurrent_tasks: 5,
|
|
current_tasks: 2,
|
|
created_at: chrono::Utc::now(),
|
|
last_heartbeat: chrono::Utc::now(),
|
|
uptime_percentage: 99.5,
|
|
total_tasks_completed: 10,
|
|
};
|
|
|
|
let profile = ProfileAdapter::create_profile(&agent);
|
|
|
|
assert_eq!(profile.id, "agent-1");
|
|
assert_eq!(profile.capabilities.len(), 2);
|
|
assert!((profile.current_load - 0.4).abs() < 0.01); // 2/5 = 0.4
|
|
assert_eq!(profile.success_rate, 0.5); // Default
|
|
assert!(profile.availability);
|
|
}
|
|
|
|
#[test]
|
|
fn test_batch_create_profiles() {
|
|
let agents = vec![
|
|
AgentMetadata {
|
|
id: "agent-1".to_string(),
|
|
role: "developer".to_string(),
|
|
name: "Dev 1".to_string(),
|
|
version: "0.1.0".to_string(),
|
|
status: crate::registry::AgentStatus::Active,
|
|
capabilities: vec!["coding".to_string()],
|
|
llm_provider: "claude".to_string(),
|
|
llm_model: "claude-sonnet-4".to_string(),
|
|
max_concurrent_tasks: 5,
|
|
current_tasks: 1,
|
|
created_at: chrono::Utc::now(),
|
|
last_heartbeat: chrono::Utc::now(),
|
|
uptime_percentage: 99.0,
|
|
total_tasks_completed: 5,
|
|
},
|
|
AgentMetadata {
|
|
id: "agent-2".to_string(),
|
|
role: "reviewer".to_string(),
|
|
name: "Reviewer 1".to_string(),
|
|
version: "0.1.0".to_string(),
|
|
status: crate::registry::AgentStatus::Active,
|
|
capabilities: vec!["review".to_string()],
|
|
llm_provider: "gpt4".to_string(),
|
|
llm_model: "gpt-4".to_string(),
|
|
max_concurrent_tasks: 3,
|
|
current_tasks: 0,
|
|
created_at: chrono::Utc::now(),
|
|
last_heartbeat: chrono::Utc::now(),
|
|
uptime_percentage: 98.5,
|
|
total_tasks_completed: 3,
|
|
},
|
|
];
|
|
|
|
let profiles = ProfileAdapter::batch_create_profiles(agents);
|
|
|
|
assert_eq!(profiles.len(), 2);
|
|
assert_eq!(profiles[0].id, "agent-1");
|
|
assert_eq!(profiles[1].id, "agent-2");
|
|
}
|
|
|
|
#[test]
|
|
fn test_update_with_kg_metrics() {
|
|
let profile = AgentProfile {
|
|
id: "agent-1".to_string(),
|
|
roles: vec!["developer".to_string()],
|
|
capabilities: vec!["coding".to_string()],
|
|
current_load: 0.4,
|
|
success_rate: 0.5,
|
|
availability: true,
|
|
};
|
|
|
|
let updated = ProfileAdapter::update_with_kg_metrics(profile, 0.85);
|
|
assert_eq!(updated.success_rate, 0.85);
|
|
assert_eq!(updated.id, "agent-1"); // Other fields unchanged
|
|
}
|
|
|
|
#[test]
|
|
fn test_create_learning_profile() {
|
|
let learning = ProfileAdapter::create_learning_profile("agent-1".to_string());
|
|
assert_eq!(learning.agent_id, "agent-1");
|
|
assert_eq!(learning.task_type_expertise.len(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_add_task_type_expertise() {
|
|
let learning = ProfileAdapter::create_learning_profile("agent-1".to_string());
|
|
let expertise = TaskTypeExpertise {
|
|
success_rate: 0.85,
|
|
total_executions: 20,
|
|
recent_success_rate: 0.90,
|
|
avg_duration_ms: 150.0,
|
|
learning_curve: Vec::new(),
|
|
confidence: 1.0,
|
|
};
|
|
|
|
let updated = ProfileAdapter::add_task_type_expertise(learning, "coding".to_string(), expertise);
|
|
assert_eq!(updated.get_task_type_score("coding"), 0.85);
|
|
assert_eq!(updated.get_confidence("coding"), 1.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_update_profile_with_learning() {
|
|
let profile = AgentProfile {
|
|
id: "agent-1".to_string(),
|
|
roles: vec!["developer".to_string()],
|
|
capabilities: vec!["coding".to_string()],
|
|
current_load: 0.4,
|
|
success_rate: 0.5,
|
|
availability: true,
|
|
};
|
|
|
|
let mut learning = ProfileAdapter::create_learning_profile("agent-1".to_string());
|
|
let expertise = TaskTypeExpertise {
|
|
success_rate: 0.85,
|
|
total_executions: 20,
|
|
recent_success_rate: 0.90,
|
|
avg_duration_ms: 150.0,
|
|
learning_curve: Vec::new(),
|
|
confidence: 1.0,
|
|
};
|
|
learning = ProfileAdapter::add_task_type_expertise(learning, "coding".to_string(), expertise);
|
|
|
|
let updated = ProfileAdapter::update_profile_with_learning(profile, &learning, "coding");
|
|
assert_eq!(updated.success_rate, 0.85);
|
|
|
|
let unknown_updated =
|
|
ProfileAdapter::update_profile_with_learning(updated, &learning, "unknown");
|
|
assert_eq!(unknown_updated.success_rate, 0.5); // Falls back to default
|
|
}
|
|
}
|