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
|
|
|
use std::collections::HashMap;
|
|
|
|
|
use std::path::Path;
|
|
|
|
|
use std::sync::Arc;
|
2026-01-11 21:46:08 +00:00
|
|
|
|
|
|
|
|
use chrono::{Datelike, Utc};
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
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
|
|
|
use thiserror::Error;
|
|
|
|
|
use tokio::sync::RwLock;
|
|
|
|
|
|
|
|
|
|
/// Budget configuration errors
|
|
|
|
|
#[derive(Debug, Error)]
|
|
|
|
|
pub enum BudgetConfigError {
|
|
|
|
|
#[error("Failed to read budget config file: {0}")]
|
|
|
|
|
ReadError(#[from] std::io::Error),
|
|
|
|
|
|
|
|
|
|
#[error("Failed to parse TOML: {0}")]
|
|
|
|
|
ParseError(#[from] toml::de::Error),
|
|
|
|
|
|
|
|
|
|
#[error("Invalid budget configuration: {0}")]
|
|
|
|
|
ValidationError(String),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Role-specific budget allocation and spending limits.
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct RoleBudget {
|
|
|
|
|
/// Agent role (e.g., "developer", "architect", "reviewer")
|
|
|
|
|
pub role: String,
|
|
|
|
|
/// Monthly spending limit in cents ($500 = 50000 cents)
|
|
|
|
|
pub monthly_limit_cents: u32,
|
|
|
|
|
/// Weekly spending limit in cents ($125 = 12500 cents)
|
|
|
|
|
pub weekly_limit_cents: u32,
|
|
|
|
|
/// Fallback provider when budget is exceeded (cheaper alternative)
|
|
|
|
|
pub fallback_provider: String,
|
|
|
|
|
/// Alert threshold as fraction (0.8 = alert at 80% utilization)
|
|
|
|
|
pub alert_threshold: f32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Current budget status for a role.
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct BudgetStatus {
|
|
|
|
|
pub role: String,
|
|
|
|
|
pub monthly_remaining_cents: u32,
|
|
|
|
|
pub weekly_remaining_cents: u32,
|
|
|
|
|
pub monthly_utilization: f32,
|
|
|
|
|
pub weekly_utilization: f32,
|
|
|
|
|
pub exceeded: bool,
|
|
|
|
|
pub near_threshold: bool,
|
|
|
|
|
pub fallback_provider: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Budget manager for enforcing role-based spending limits.
|
|
|
|
|
/// Tracks monthly and weekly spend, enforces caps, and triggers alerts.
|
|
|
|
|
pub struct BudgetManager {
|
|
|
|
|
budgets: Arc<RwLock<HashMap<String, RoleBudget>>>,
|
|
|
|
|
spending: Arc<RwLock<HashMap<String, RoleSpending>>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
struct RoleSpending {
|
|
|
|
|
#[allow(dead_code)]
|
|
|
|
|
role: String,
|
|
|
|
|
current_month: MonthBudget,
|
|
|
|
|
current_week: WeekBudget,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
struct MonthBudget {
|
|
|
|
|
year: i32,
|
|
|
|
|
month: u32,
|
|
|
|
|
spent_cents: u32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
struct WeekBudget {
|
|
|
|
|
year: i32,
|
|
|
|
|
week: u32,
|
|
|
|
|
spent_cents: u32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Budget configuration file structure
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
|
|
|
pub struct BudgetConfig {
|
|
|
|
|
pub budgets: HashMap<String, RoleBudget>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl BudgetConfig {
|
|
|
|
|
/// Load budget configuration from TOML file
|
|
|
|
|
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, BudgetConfigError> {
|
|
|
|
|
let content = std::fs::read_to_string(path)?;
|
|
|
|
|
let config: Self = toml::from_str(&content)?;
|
|
|
|
|
config.validate()?;
|
|
|
|
|
Ok(config)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Load from TOML with default fallback if file doesn't exist
|
|
|
|
|
pub fn load_or_default<P: AsRef<Path>>(path: P) -> Result<Self, BudgetConfigError> {
|
|
|
|
|
match Self::load(&path) {
|
|
|
|
|
Ok(config) => Ok(config),
|
|
|
|
|
Err(BudgetConfigError::ReadError(_)) => {
|
|
|
|
|
// File doesn't exist, use defaults
|
|
|
|
|
Ok(BudgetConfig {
|
|
|
|
|
budgets: HashMap::new(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
Err(e) => Err(e),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Validate budget configuration
|
|
|
|
|
fn validate(&self) -> Result<(), BudgetConfigError> {
|
|
|
|
|
if self.budgets.is_empty() {
|
|
|
|
|
return Err(BudgetConfigError::ValidationError(
|
|
|
|
|
"At least one role budget must be configured".to_string(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (role, budget) in &self.budgets {
|
|
|
|
|
if budget.monthly_limit_cents == 0 {
|
2026-01-11 21:32:56 +00:00
|
|
|
return Err(BudgetConfigError::ValidationError(format!(
|
|
|
|
|
"Role {} has zero monthly limit",
|
|
|
|
|
role
|
|
|
|
|
)));
|
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
|
|
|
}
|
|
|
|
|
if budget.weekly_limit_cents == 0 {
|
2026-01-11 21:32:56 +00:00
|
|
|
return Err(BudgetConfigError::ValidationError(format!(
|
|
|
|
|
"Role {} has zero weekly limit",
|
|
|
|
|
role
|
|
|
|
|
)));
|
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
|
|
|
}
|
|
|
|
|
if budget.alert_threshold < 0.0 || budget.alert_threshold > 1.0 {
|
2026-01-11 21:32:56 +00:00
|
|
|
return Err(BudgetConfigError::ValidationError(format!(
|
|
|
|
|
"Role {} has invalid alert_threshold: {}",
|
|
|
|
|
role, budget.alert_threshold
|
|
|
|
|
)));
|
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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl BudgetManager {
|
|
|
|
|
/// Create new budget manager with role budgets.
|
|
|
|
|
pub fn new(budgets: HashMap<String, RoleBudget>) -> Self {
|
|
|
|
|
let spending = budgets
|
|
|
|
|
.keys()
|
|
|
|
|
.map(|role| {
|
|
|
|
|
(
|
|
|
|
|
role.clone(),
|
|
|
|
|
RoleSpending {
|
|
|
|
|
role: role.clone(),
|
|
|
|
|
current_month: MonthBudget {
|
|
|
|
|
year: Utc::now().year(),
|
|
|
|
|
month: Utc::now().month(),
|
|
|
|
|
spent_cents: 0,
|
|
|
|
|
},
|
|
|
|
|
current_week: WeekBudget {
|
|
|
|
|
year: Utc::now().year(),
|
|
|
|
|
week: Utc::now().iso_week().week(),
|
|
|
|
|
spent_cents: 0,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
Self {
|
|
|
|
|
budgets: Arc::new(RwLock::new(budgets)),
|
|
|
|
|
spending: Arc::new(RwLock::new(spending)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Check budget status for role.
|
2026-01-11 21:46:08 +00:00
|
|
|
/// Returns BudgetStatus with remaining balance, utilization %, and alert
|
|
|
|
|
/// flags.
|
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
|
|
|
pub async fn check_budget(&self, role: &str) -> Result<BudgetStatus, String> {
|
|
|
|
|
let budgets = self.budgets.read().await;
|
|
|
|
|
let mut spending = self.spending.write().await;
|
|
|
|
|
|
2026-01-11 21:32:56 +00:00
|
|
|
let budget = budgets
|
|
|
|
|
.get(role)
|
|
|
|
|
.ok_or_else(|| format!("Unknown role: {}", role))?;
|
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
|
|
|
let spending_entry = spending
|
|
|
|
|
.entry(role.to_string())
|
|
|
|
|
.or_insert_with(|| RoleSpending {
|
|
|
|
|
role: role.to_string(),
|
|
|
|
|
current_month: MonthBudget {
|
|
|
|
|
year: Utc::now().year(),
|
|
|
|
|
month: Utc::now().month(),
|
|
|
|
|
spent_cents: 0,
|
|
|
|
|
},
|
|
|
|
|
current_week: WeekBudget {
|
|
|
|
|
year: Utc::now().year(),
|
|
|
|
|
week: Utc::now().iso_week().week(),
|
|
|
|
|
spent_cents: 0,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Reset month if new month
|
|
|
|
|
let now = Utc::now();
|
|
|
|
|
if now.year() != spending_entry.current_month.year
|
|
|
|
|
|| now.month() != spending_entry.current_month.month
|
|
|
|
|
{
|
|
|
|
|
spending_entry.current_month = MonthBudget {
|
|
|
|
|
year: now.year(),
|
|
|
|
|
month: now.month(),
|
|
|
|
|
spent_cents: 0,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reset week if new week
|
|
|
|
|
let current_week = now.iso_week().week();
|
|
|
|
|
if now.year() != spending_entry.current_week.year
|
|
|
|
|
|| current_week != spending_entry.current_week.week
|
|
|
|
|
{
|
|
|
|
|
spending_entry.current_week = WeekBudget {
|
|
|
|
|
year: now.year(),
|
|
|
|
|
week: current_week,
|
|
|
|
|
spent_cents: 0,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let monthly_remaining = budget
|
|
|
|
|
.monthly_limit_cents
|
|
|
|
|
.saturating_sub(spending_entry.current_month.spent_cents);
|
|
|
|
|
let weekly_remaining = budget
|
|
|
|
|
.weekly_limit_cents
|
|
|
|
|
.saturating_sub(spending_entry.current_week.spent_cents);
|
|
|
|
|
|
|
|
|
|
let monthly_utilization = if budget.monthly_limit_cents > 0 {
|
|
|
|
|
spending_entry.current_month.spent_cents as f32 / budget.monthly_limit_cents as f32
|
|
|
|
|
} else {
|
|
|
|
|
1.0
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let weekly_utilization = if budget.weekly_limit_cents > 0 {
|
|
|
|
|
spending_entry.current_week.spent_cents as f32 / budget.weekly_limit_cents as f32
|
|
|
|
|
} else {
|
|
|
|
|
1.0
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let exceeded = monthly_remaining == 0 || weekly_remaining == 0;
|
|
|
|
|
let near_threshold = monthly_utilization >= budget.alert_threshold
|
|
|
|
|
|| weekly_utilization >= budget.alert_threshold;
|
|
|
|
|
|
|
|
|
|
Ok(BudgetStatus {
|
|
|
|
|
role: role.to_string(),
|
|
|
|
|
monthly_remaining_cents: monthly_remaining,
|
|
|
|
|
weekly_remaining_cents: weekly_remaining,
|
|
|
|
|
monthly_utilization,
|
|
|
|
|
weekly_utilization,
|
|
|
|
|
exceeded,
|
|
|
|
|
near_threshold,
|
|
|
|
|
fallback_provider: budget.fallback_provider.clone(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Record spending against role budget.
|
|
|
|
|
/// Automatically resets month/week tracking if boundaries crossed.
|
|
|
|
|
pub async fn record_spend(&self, role: &str, cost_cents: u32) -> Result<(), String> {
|
|
|
|
|
let mut spending = self.spending.write().await;
|
|
|
|
|
let entry = spending
|
|
|
|
|
.get_mut(role)
|
|
|
|
|
.ok_or_else(|| format!("Unknown role: {}", role))?;
|
|
|
|
|
|
|
|
|
|
let now = Utc::now();
|
|
|
|
|
|
|
|
|
|
// Reset month if needed
|
|
|
|
|
if now.year() != entry.current_month.year || now.month() != entry.current_month.month {
|
|
|
|
|
entry.current_month = MonthBudget {
|
|
|
|
|
year: now.year(),
|
|
|
|
|
month: now.month(),
|
|
|
|
|
spent_cents: 0,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reset week if needed
|
|
|
|
|
let current_week = now.iso_week().week();
|
|
|
|
|
if now.year() != entry.current_week.year || current_week != entry.current_week.week {
|
|
|
|
|
entry.current_week = WeekBudget {
|
|
|
|
|
year: now.year(),
|
|
|
|
|
week: current_week,
|
|
|
|
|
spent_cents: 0,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
entry.current_month.spent_cents =
|
|
|
|
|
entry.current_month.spent_cents.saturating_add(cost_cents);
|
|
|
|
|
entry.current_week.spent_cents = entry.current_week.spent_cents.saturating_add(cost_cents);
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get fallback provider for role when budget exceeded.
|
|
|
|
|
pub async fn get_fallback_provider(&self, role: &str) -> Result<String, String> {
|
|
|
|
|
let budgets = self.budgets.read().await;
|
|
|
|
|
budgets
|
|
|
|
|
.get(role)
|
|
|
|
|
.map(|b| b.fallback_provider.clone())
|
|
|
|
|
.ok_or_else(|| format!("Unknown role: {}", role))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get all budget statuses (for monitoring dashboards).
|
|
|
|
|
pub async fn get_all_budgets(&self) -> Vec<BudgetStatus> {
|
|
|
|
|
let budgets = self.budgets.read().await;
|
|
|
|
|
let spending = self.spending.read().await;
|
|
|
|
|
|
|
|
|
|
budgets
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|(role, budget)| {
|
|
|
|
|
spending.get(role).map(|sp| {
|
|
|
|
|
let monthly_remaining = budget
|
|
|
|
|
.monthly_limit_cents
|
|
|
|
|
.saturating_sub(sp.current_month.spent_cents);
|
2026-01-11 21:32:56 +00:00
|
|
|
let weekly_remaining = budget
|
|
|
|
|
.weekly_limit_cents
|
|
|
|
|
.saturating_sub(sp.current_week.spent_cents);
|
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
|
|
|
|
|
|
|
|
let monthly_utilization = if budget.monthly_limit_cents > 0 {
|
|
|
|
|
sp.current_month.spent_cents as f32 / budget.monthly_limit_cents as f32
|
|
|
|
|
} else {
|
|
|
|
|
1.0
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let weekly_utilization = if budget.weekly_limit_cents > 0 {
|
|
|
|
|
sp.current_week.spent_cents as f32 / budget.weekly_limit_cents as f32
|
|
|
|
|
} else {
|
|
|
|
|
1.0
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let exceeded = monthly_remaining == 0 || weekly_remaining == 0;
|
2026-01-11 21:32:56 +00:00
|
|
|
let near_threshold = monthly_utilization >= budget.alert_threshold
|
|
|
|
|
|| weekly_utilization >= budget.alert_threshold;
|
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
|
|
|
|
|
|
|
|
BudgetStatus {
|
|
|
|
|
role: role.clone(),
|
|
|
|
|
monthly_remaining_cents: monthly_remaining,
|
|
|
|
|
weekly_remaining_cents: weekly_remaining,
|
|
|
|
|
monthly_utilization,
|
|
|
|
|
weekly_utilization,
|
|
|
|
|
exceeded,
|
|
|
|
|
near_threshold,
|
|
|
|
|
fallback_provider: budget.fallback_provider.clone(),
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// List all configured role budgets
|
|
|
|
|
pub async fn list_budgets(&self) -> Vec<RoleBudget> {
|
|
|
|
|
let budgets = self.budgets.read().await;
|
|
|
|
|
budgets.values().cloned().collect()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
fn create_test_budgets() -> HashMap<String, RoleBudget> {
|
|
|
|
|
let mut budgets = HashMap::new();
|
|
|
|
|
budgets.insert(
|
|
|
|
|
"developer".to_string(),
|
|
|
|
|
RoleBudget {
|
|
|
|
|
role: "developer".to_string(),
|
|
|
|
|
monthly_limit_cents: 30000, // $300
|
|
|
|
|
weekly_limit_cents: 7500, // $75
|
|
|
|
|
fallback_provider: "ollama".to_string(),
|
|
|
|
|
alert_threshold: 0.8,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
budgets
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_budget_manager_creation() {
|
|
|
|
|
let budgets = create_test_budgets();
|
|
|
|
|
let manager = BudgetManager::new(budgets);
|
|
|
|
|
|
|
|
|
|
let status = manager.check_budget("developer").await.unwrap();
|
|
|
|
|
assert_eq!(status.role, "developer");
|
|
|
|
|
assert_eq!(status.monthly_remaining_cents, 30000);
|
|
|
|
|
assert!(!status.exceeded);
|
|
|
|
|
assert!(!status.near_threshold);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_record_spend() {
|
|
|
|
|
let budgets = create_test_budgets();
|
|
|
|
|
let manager = BudgetManager::new(budgets);
|
|
|
|
|
|
|
|
|
|
manager.record_spend("developer", 5000).await.unwrap();
|
|
|
|
|
let status = manager.check_budget("developer").await.unwrap();
|
|
|
|
|
assert_eq!(status.monthly_remaining_cents, 25000);
|
|
|
|
|
assert!((status.monthly_utilization - 0.1667).abs() < 0.01);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_threshold_alert() {
|
|
|
|
|
let budgets = create_test_budgets();
|
|
|
|
|
let manager = BudgetManager::new(budgets);
|
|
|
|
|
|
|
|
|
|
// Spend 81% of weekly budget (7500 * 0.81 = 6075)
|
|
|
|
|
// This triggers near_threshold (> 80%) but not exceeded
|
|
|
|
|
manager.record_spend("developer", 6075).await.unwrap();
|
|
|
|
|
let status = manager.check_budget("developer").await.unwrap();
|
|
|
|
|
assert!(!status.exceeded);
|
|
|
|
|
assert!(status.near_threshold); // 81% > 80% threshold
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_budget_exceeded() {
|
|
|
|
|
let budgets = create_test_budgets();
|
|
|
|
|
let manager = BudgetManager::new(budgets);
|
|
|
|
|
|
|
|
|
|
// Exceed monthly budget
|
|
|
|
|
manager.record_spend("developer", 30000).await.unwrap();
|
|
|
|
|
let status = manager.check_budget("developer").await.unwrap();
|
|
|
|
|
assert!(status.exceeded);
|
|
|
|
|
assert_eq!(status.monthly_remaining_cents, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_fallback_provider() {
|
|
|
|
|
let budgets = create_test_budgets();
|
|
|
|
|
let manager = BudgetManager::new(budgets);
|
|
|
|
|
|
|
|
|
|
let fallback = manager.get_fallback_provider("developer").await.unwrap();
|
|
|
|
|
assert_eq!(fallback, "ollama");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_unknown_role() {
|
|
|
|
|
let budgets = create_test_budgets();
|
|
|
|
|
let manager = BudgetManager::new(budgets);
|
|
|
|
|
|
|
|
|
|
let result = manager.check_budget("unknown_role").await;
|
|
|
|
|
assert!(result.is_err());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_get_all_budgets() {
|
|
|
|
|
let budgets = create_test_budgets();
|
|
|
|
|
let manager = BudgetManager::new(budgets);
|
|
|
|
|
|
|
|
|
|
manager.record_spend("developer", 3000).await.unwrap();
|
|
|
|
|
let all_statuses = manager.get_all_budgets().await;
|
|
|
|
|
assert_eq!(all_statuses.len(), 1);
|
|
|
|
|
assert_eq!(all_statuses[0].monthly_remaining_cents, 27000);
|
|
|
|
|
}
|
|
|
|
|
}
|