use std::collections::HashMap; use crate::models::*; /// Reasoning engine for inferring insights from execution history pub struct ReasoningEngine; impl ReasoningEngine { /// Analyze execution records for patterns pub fn find_patterns(records: &[ExecutionRecord]) -> Vec { let mut patterns = Vec::new(); // Pattern 1: Task type failure rate let mut task_failure_rates: HashMap = HashMap::new(); for record in records { task_failure_rates .entry(record.task_type.clone()) .or_insert((0, 0)); let entry = task_failure_rates.get_mut(&record.task_type).unwrap(); entry.0 += 1; if !record.success { entry.1 += 1; } } for (task_type, (total, failures)) in task_failure_rates.iter() { let failure_rate = *failures as f64 / *total as f64; if failure_rate > 0.3 { patterns.push(format!( "High failure rate detected for '{}': {:.1}%", task_type, failure_rate * 100.0 )); } } // Pattern 2: Duration anomalies if records.len() > 3 { let mut durations: Vec = records.iter().map(|r| r.duration_ms).collect(); durations.sort_unstable(); let median = durations[durations.len() / 2]; let avg = durations.iter().sum::() as f64 / durations.len() as f64; if avg > median as f64 * 2.0 { patterns.push(format!( "High latency variance detected: avg={:.0}ms, median={}ms", avg, median )); } } // Pattern 3: Agent expertise misalignment let mut agent_task_types: HashMap> = HashMap::new(); for record in records { agent_task_types .entry(record.agent_id.clone()) .or_default() .entry(record.task_type.clone()) .and_modify(|c| *c += 1) .or_insert(1); } for (agent_id, task_map) in agent_task_types { if task_map.len() > 5 { patterns.push(format!( "Agent '{}' is assigned to {} different task types (possible overallocation)", agent_id, task_map.len() )); } } patterns } /// Predict success probability for a task pub fn predict_success(task_type: &str, similar_records: &[ExecutionRecord]) -> (f64, String) { if similar_records.is_empty() { return (0.5, "No historical data available".to_string()); } let success_count = similar_records.iter().filter(|r| r.success).count() as f64; let success_rate = success_count / similar_records.len() as f64; let reasoning = if success_rate > 0.9 { format!( "High success probability based on {} successful similar tasks", success_count as usize ) } else if success_rate > 0.7 { "Moderate success probability with some historical challenges".to_string() } else if success_rate > 0.5 { "Below-average success rate - recommend expert agent".to_string() } else { format!( "Critical: {} only {} successful of {} similar tasks", task_type, success_count as usize, similar_records.len() ) }; (success_rate, reasoning) } /// Estimate task duration pub fn estimate_duration(similar_records: &[ExecutionRecord]) -> (u64, String) { if similar_records.is_empty() { return ( 300_000, "No historical data - using default 5 minutes".to_string(), ); } let mut durations: Vec = similar_records.iter().map(|r| r.duration_ms).collect(); durations.sort_unstable(); let median = durations[durations.len() / 2]; let avg = durations.iter().sum::() / durations.len() as u64; let max = durations[durations.len() - 1]; let estimate = (avg + median) / 2; let reasoning = if max > estimate * 3 { format!( "High variance in execution time: avg={}ms, max={}ms", avg, max ) } else { format!("Based on {} similar tasks: avg={}ms", durations.len(), avg) }; (estimate, reasoning) } /// Recommend best agent for a task pub fn recommend_agent( task_type: &str, agent_profiles: &[AgentProfile], ) -> Option<(String, String)> { agent_profiles .iter() .filter(|profile| { profile.primary_task_types.contains(&task_type.to_string()) || profile.expertise_score > 75.0 }) .max_by(|a, b| { a.expertise_score .partial_cmp(&b.expertise_score) .unwrap_or(std::cmp::Ordering::Equal) }) .map(|profile| { let reasoning = format!( "Agent '{}' has {:.1}% expertise in {} tasks", profile.agent_id, profile.expertise_score, profile.primary_task_types.join(", ") ); (profile.agent_id.clone(), reasoning) }) } /// Chain reasoning - find root cause chains pub fn find_root_cause_chain(records: &[ExecutionRecord]) -> Vec> { let mut chains: Vec> = Vec::new(); let mut visited = std::collections::HashSet::new(); for record in records .iter() .filter(|r| !r.success && r.root_cause.is_some()) { if visited.contains(&record.id) { continue; } let mut chain = vec![record.root_cause.clone().unwrap()]; visited.insert(record.id.clone()); // Find similar errors in history for other in records .iter() .filter(|r| !visited.contains(&r.id) && r.solution.is_some()) { if let Some(ref root) = other.root_cause { if root.contains(chain.last().unwrap()) { chain.push(other.solution.clone().unwrap()); visited.insert(other.id.clone()); break; } } } if chain.len() > 1 { chains.push(chain); } } chains } /// Generate actionable insights pub fn generate_insights( stats: &GraphStatistics, patterns: &[String], profiles: &[AgentProfile], ) -> Vec { let mut insights = Vec::new(); // Insight 1: Overall system health if stats.success_rate > 0.95 { insights.push("✓ System is very reliable (>95% success rate)".to_string()); } else if stats.success_rate < 0.75 { insights.push(format!( "⚠ System reliability is concerning ({:.1}% success rate)", stats.success_rate * 100.0 )); } // Insight 2: Performance metrics if stats.avg_duration_ms > 60_000.0 { insights.push(format!( "⚠ Average task duration is high ({:.0}ms)", stats.avg_duration_ms )); } // Insight 3: Coverage insights.push(format!( "ℹ Knowledge graph contains {} execution records across {} agents", stats.total_records, stats.distinct_agents )); // Insight 4: Agent expertise distribution let avg_expertise = profiles.iter().map(|p| p.expertise_score).sum::() / profiles.len().max(1) as f64; if avg_expertise < 70.0 { insights.push(format!( "⚠ Average agent expertise is below target ({:.0}%)", avg_expertise )); } // Insight 5: Patterns insights.extend(patterns.iter().cloned()); insights } } #[cfg(test)] mod tests { use chrono::Utc; use super::*; #[test] fn test_predict_success() { let records = vec![ ExecutionRecord { id: "1".to_string(), task_id: "t1".to_string(), agent_id: "a1".to_string(), agent_role: None, task_type: "dev".to_string(), description: "test".to_string(), root_cause: None, solution: None, duration_ms: 1000, input_tokens: 100, output_tokens: 50, cost_cents: 40, provider: "claude".to_string(), success: true, error: None, timestamp: Utc::now(), }, ExecutionRecord { id: "2".to_string(), task_id: "t2".to_string(), agent_id: "a1".to_string(), agent_role: None, task_type: "dev".to_string(), description: "test".to_string(), root_cause: None, solution: None, duration_ms: 1000, input_tokens: 100, output_tokens: 50, cost_cents: 45, provider: "claude".to_string(), success: true, error: None, timestamp: Utc::now(), }, ]; let (prob, _) = ReasoningEngine::predict_success("dev", &records); assert_eq!(prob, 1.0); } #[test] fn test_estimate_duration() { let records = vec![ExecutionRecord { id: "1".to_string(), task_id: "t1".to_string(), agent_id: "a1".to_string(), agent_role: None, task_type: "dev".to_string(), description: "test".to_string(), root_cause: None, solution: None, duration_ms: 1000, input_tokens: 100, output_tokens: 50, cost_cents: 50, provider: "claude".to_string(), success: true, error: None, timestamp: Utc::now(), }]; let (duration, _) = ReasoningEngine::estimate_duration(&records); assert_eq!(duration, 1000); } }