275 lines
6.8 KiB
Rust
Raw Normal View History

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
// vapora-backend: Workflow YAML parser
// Phase 3: Parse workflow definitions from YAML
use crate::workflow::state::{Phase, StepStatus, Workflow, WorkflowStep};
use serde::{Deserialize, Serialize};
use std::fs;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ParserError {
#[error("Failed to read file: {0}")]
FileError(#[from] std::io::Error),
#[error("Failed to parse YAML: {0}")]
YamlError(#[from] serde_yaml::Error),
#[error("Invalid workflow definition: {0}")]
ValidationError(String),
}
#[derive(Debug, Deserialize, Serialize)]
pub struct WorkflowYaml {
pub workflow: WorkflowDef,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct WorkflowDef {
pub id: String,
pub title: String,
pub phases: Vec<PhaseDef>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct PhaseDef {
pub id: String,
pub name: String,
#[serde(default)]
pub parallel: bool,
#[serde(default = "default_estimated_hours")]
pub estimated_hours: f32,
pub steps: Vec<StepDef>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct StepDef {
pub id: String,
pub name: String,
pub agent: String,
#[serde(default)]
pub depends_on: Vec<String>,
#[serde(default)]
pub parallelizable: bool,
}
fn default_estimated_hours() -> f32 {
1.0
}
pub struct WorkflowParser;
impl WorkflowParser {
/// Parse workflow from YAML file
pub fn parse_file(path: &str) -> Result<Workflow, ParserError> {
let content = fs::read_to_string(path)?;
Self::parse_string(&content)
}
/// Parse workflow from YAML string
pub fn parse_string(yaml: &str) -> Result<Workflow, ParserError> {
let workflow_yaml: WorkflowYaml = serde_yaml::from_str(yaml)?;
Self::validate_and_convert(workflow_yaml)
}
/// Validate and convert YAML definition to runtime Workflow
fn validate_and_convert(yaml: WorkflowYaml) -> Result<Workflow, ParserError> {
let def = yaml.workflow;
// Validate workflow has phases
if def.phases.is_empty() {
return Err(ParserError::ValidationError(
"Workflow must have at least one phase".to_string(),
));
}
// Convert phases
let mut phases = Vec::new();
for phase_def in def.phases {
// Validate phase has steps
if phase_def.steps.is_empty() {
return Err(ParserError::ValidationError(format!(
"Phase '{}' must have at least one step",
phase_def.id
)));
}
// Convert steps
let steps: Vec<WorkflowStep> = phase_def
.steps
.into_iter()
.map(|step_def| WorkflowStep {
id: step_def.id,
name: step_def.name,
agent_role: step_def.agent,
status: StepStatus::Pending,
depends_on: step_def.depends_on,
can_parallelize: step_def.parallelizable,
started_at: None,
completed_at: None,
result: None,
error: None,
})
.collect();
// Validate dependencies exist
Self::validate_dependencies(&steps)?;
phases.push(Phase {
id: phase_def.id,
name: phase_def.name,
status: StepStatus::Pending,
steps,
parallel: phase_def.parallel,
estimated_hours: phase_def.estimated_hours,
});
}
Ok(Workflow::new(def.id, def.title, phases))
}
/// Validate that all step dependencies exist
fn validate_dependencies(steps: &[WorkflowStep]) -> Result<(), ParserError> {
let step_ids: std::collections::HashSet<_> = steps.iter().map(|s| &s.id).collect();
for step in steps {
for dep in &step.depends_on {
if !step_ids.contains(dep) {
return Err(ParserError::ValidationError(format!(
"Step '{}' depends on non-existent step '{}'",
step.id, dep
)));
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_workflow_yaml() {
let yaml = r#"
workflow:
id: feature-auth
title: Implement MFA
phases:
- id: phase_1
name: Design
parallel: false
estimated_hours: 2.0
steps:
- id: step_1_1
name: Architect Design
agent: architect
depends_on: []
parallelizable: false
- id: phase_2
name: Implementation
parallel: true
estimated_hours: 8.0
steps:
- id: step_2_1
name: Backend API
agent: developer
depends_on: []
parallelizable: true
- id: step_2_2
name: Frontend UI
agent: developer
depends_on: []
parallelizable: true
"#;
let result = WorkflowParser::parse_string(yaml);
assert!(result.is_ok());
let workflow = result.unwrap();
assert_eq!(workflow.id, "feature-auth");
assert_eq!(workflow.title, "Implement MFA");
assert_eq!(workflow.phases.len(), 2);
assert_eq!(workflow.phases[0].steps.len(), 1);
assert_eq!(workflow.phases[1].steps.len(), 2);
assert!(workflow.phases[1].parallel);
}
#[test]
fn test_empty_phases_error() {
let yaml = r#"
workflow:
id: test
title: Test
phases: []
"#;
let result = WorkflowParser::parse_string(yaml);
assert!(result.is_err());
}
#[test]
fn test_empty_steps_error() {
let yaml = r#"
workflow:
id: test
title: Test
phases:
- id: phase_1
name: Phase
steps: []
"#;
let result = WorkflowParser::parse_string(yaml);
assert!(result.is_err());
}
#[test]
fn test_invalid_dependency() {
let yaml = r#"
workflow:
id: test
title: Test
phases:
- id: phase_1
name: Phase
steps:
- id: step_1
name: Step 1
agent: developer
depends_on: [nonexistent]
"#;
let result = WorkflowParser::parse_string(yaml);
assert!(result.is_err());
}
#[test]
fn test_valid_dependencies() {
let yaml = r#"
workflow:
id: test
title: Test
phases:
- id: phase_1
name: Phase
steps:
- id: step_1
name: Step 1
agent: developer
depends_on: []
- id: step_2
name: Step 2
agent: developer
depends_on: [step_1]
"#;
let result = WorkflowParser::parse_string(yaml);
assert!(result.is_ok());
let workflow = result.unwrap();
assert_eq!(workflow.phases[0].steps[1].depends_on, vec!["step_1"]);
}
}