Vapora/crates/vapora-agents/src/profile_adapter.rs
Jesús Pérez d14150da75 feat: Phase 5.3 - Multi-Agent Learning Infrastructure
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.
2026-01-11 13:03:53 +00:00

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
}
}