llm_optimizer_processor/decision/
coordinator.rs1use async_trait::async_trait;
6use chrono::Utc;
7use std::collections::HashMap;
8use std::sync::Arc;
9use std::time::Instant;
10use tokio::sync::RwLock;
11use tracing::{debug, info, warn};
12use uuid::Uuid;
13
14use super::config::DecisionEngineConfig;
15use super::error::{DecisionError, DecisionResult, DecisionState};
16use super::traits::{DecisionEngine, OptimizationStrategy};
17use super::types::{Decision, DecisionCriteria, DecisionInput, DecisionOutcome, DecisionStats};
18
19pub struct DecisionCoordinator {
23 config: DecisionEngineConfig,
24 state: DecisionState,
25 strategies: Vec<Arc<dyn OptimizationStrategy>>,
26 stats: Arc<RwLock<DecisionStats>>,
27 active_decisions: Arc<RwLock<HashMap<String, Decision>>>,
28 decision_history: Arc<RwLock<Vec<Decision>>>,
29 outcome_history: Arc<RwLock<Vec<DecisionOutcome>>>,
30 start_time: Instant,
31}
32
33impl DecisionCoordinator {
34 pub fn new(
36 config: DecisionEngineConfig,
37 strategies: Vec<Arc<dyn OptimizationStrategy>>,
38 ) -> Self {
39 Self {
40 config,
41 state: DecisionState::Initialized,
42 strategies,
43 stats: Arc::new(RwLock::new(DecisionStats::default())),
44 active_decisions: Arc::new(RwLock::new(HashMap::new())),
45 decision_history: Arc::new(RwLock::new(Vec::new())),
46 outcome_history: Arc::new(RwLock::new(Vec::new())),
47 start_time: Instant::now(),
48 }
49 }
50
51 async fn validate_decision(
53 &self,
54 decision: &Decision,
55 criteria: &DecisionCriteria,
56 ) -> DecisionResult<()> {
57 if decision.confidence < criteria.min_confidence {
59 return Err(DecisionError::ValidationFailed(format!(
60 "Confidence {:.2} below minimum {:.2}",
61 decision.confidence, criteria.min_confidence
62 )));
63 }
64
65 if let Some(max_cost) = criteria.max_cost_increase_usd {
67 if let Some(cost_change) = decision.expected_impact.cost_change_usd {
68 if cost_change > max_cost {
69 return Err(DecisionError::ConstraintViolation(format!(
70 "Cost increase ${:.2} exceeds maximum ${:.2}",
71 cost_change, max_cost
72 )));
73 }
74 }
75 }
76
77 if let Some(max_latency) = criteria.max_latency_increase_ms {
79 if let Some(latency_change) = decision.expected_impact.latency_change_ms {
80 if latency_change > max_latency {
81 return Err(DecisionError::ConstraintViolation(format!(
82 "Latency increase {:.2}ms exceeds maximum {:.2}ms",
83 latency_change, max_latency
84 )));
85 }
86 }
87 }
88
89 if let Some(min_quality) = criteria.min_quality_score {
91 if let Some(quality_change) = decision.expected_impact.quality_change {
92 if quality_change < 0.0 {
94 let estimated_quality = min_quality + quality_change;
95 if estimated_quality < min_quality {
96 return Err(DecisionError::ConstraintViolation(format!(
97 "Quality drop would result in score {:.2}, below minimum {:.2}",
98 estimated_quality, min_quality
99 )));
100 }
101 }
102 }
103 }
104
105 for required_check in &criteria.required_safety_checks {
107 let check_passed = decision
108 .safety_checks
109 .iter()
110 .any(|check| check.name == *required_check && check.passed);
111
112 if !check_passed {
113 return Err(DecisionError::ValidationFailed(format!(
114 "Required safety check '{}' did not pass",
115 required_check
116 )));
117 }
118 }
119
120 Ok(())
121 }
122
123 async fn merge_decisions(
125 &self,
126 all_decisions: Vec<Vec<Decision>>,
127 criteria: &DecisionCriteria,
128 ) -> Vec<Decision> {
129 let mut merged: Vec<Decision> = all_decisions.into_iter().flatten().collect();
130
131 merged.sort_by(|a, b| b.priority.cmp(&a.priority));
133
134 merged.truncate(criteria.max_concurrent_decisions);
136
137 let mut final_decisions = Vec::new();
139 for decision in merged {
140 let conflicts = final_decisions.iter().any(|d: &Decision| {
142 self.decisions_conflict(&decision, d)
143 });
144
145 if !conflicts {
146 final_decisions.push(decision);
147 } else {
148 debug!(
149 "Skipping decision {} due to conflict",
150 decision.id
151 );
152 }
153 }
154
155 final_decisions
156 }
157
158 fn decisions_conflict(&self, d1: &Decision, d2: &Decision) -> bool {
160 if d1.decision_type == d2.decision_type {
162 return true;
163 }
164
165 use super::types::DecisionType::*;
167 matches!(
168 (&d1.decision_type, &d2.decision_type),
169 (ModelSwitch, OptimizePrompt) | (OptimizePrompt, ModelSwitch)
170 )
171 }
172
173 async fn update_stats(&self, decisions: &[Decision]) {
175 let mut stats = self.stats.write().await;
176
177 stats.total_decisions += decisions.len() as u64;
178
179 for decision in decisions {
180 let decision_type = format!("{:?}", decision.decision_type);
181 *stats.decisions_by_type.entry(decision_type).or_insert(0) += 1;
182
183 *stats
184 .decisions_by_strategy
185 .entry(decision.strategy.clone())
186 .or_insert(0) += 1;
187
188 let total = stats.total_decisions as f64;
190 stats.avg_confidence = ((stats.avg_confidence * (total - 1.0)) + decision.confidence) / total;
191 }
192
193 stats.last_decision_time = Some(Utc::now());
194 }
195}
196
197#[async_trait]
198impl DecisionEngine for DecisionCoordinator {
199 fn name(&self) -> &str {
200 &self.config.id
201 }
202
203 fn state(&self) -> DecisionState {
204 self.state
205 }
206
207 async fn start(&mut self) -> DecisionResult<()> {
208 if !self.state.can_transition_to(DecisionState::Starting) {
209 return Err(DecisionError::InvalidStateTransition {
210 from: self.state,
211 to: DecisionState::Starting,
212 });
213 }
214
215 info!("Starting Decision Engine Coordinator");
216 self.state = DecisionState::Starting;
217
218 info!("Initialized {} strategies", self.strategies.len());
220
221 self.state = DecisionState::Running;
222 info!("Decision Engine Coordinator started successfully");
223
224 Ok(())
225 }
226
227 async fn stop(&mut self) -> DecisionResult<()> {
228 if !self.state.can_transition_to(DecisionState::Draining) {
229 return Err(DecisionError::InvalidStateTransition {
230 from: self.state,
231 to: DecisionState::Draining,
232 });
233 }
234
235 info!("Stopping Decision Engine Coordinator");
236 self.state = DecisionState::Draining;
237
238 let active_count = self.active_decisions.read().await.len();
240 if active_count > 0 {
241 info!("Waiting for {} active decisions to complete", active_count);
242 }
243
244 self.state = DecisionState::Stopped;
245 info!("Decision Engine Coordinator stopped");
246
247 Ok(())
248 }
249
250 async fn make_decisions(
251 &mut self,
252 input: DecisionInput,
253 criteria: &DecisionCriteria,
254 ) -> DecisionResult<Vec<Decision>> {
255 if !self.state.can_process() {
256 return Err(DecisionError::NotRunning);
257 }
258
259 let start = Instant::now();
260
261 debug!(
262 "Making decisions with {} insights and {} recommendations",
263 input.insights.len(),
264 input.recommendations.len()
265 );
266
267 let mut all_decisions = Vec::new();
269
270 for strategy in &self.strategies {
271 if !criteria.enabled_strategies.contains(&strategy.name().to_string()) {
273 continue;
274 }
275
276 if !strategy.is_applicable(&input) {
278 debug!("Strategy {} not applicable", strategy.name());
279 continue;
280 }
281
282 match strategy.evaluate(&input, criteria).await {
284 Ok(decisions) => {
285 debug!(
286 "Strategy {} generated {} decisions",
287 strategy.name(),
288 decisions.len()
289 );
290 all_decisions.push(decisions);
291 }
292 Err(e) => {
293 warn!("Strategy {} evaluation failed: {}", strategy.name(), e);
294 }
295 }
296 }
297
298 let mut merged_decisions = self.merge_decisions(all_decisions, criteria).await;
300
301 let mut validated_decisions = Vec::new();
303 for decision in merged_decisions {
304 match self.validate_decision(&decision, criteria).await {
305 Ok(()) => {
306 validated_decisions.push(decision);
307 }
308 Err(e) => {
309 warn!("Decision {} validation failed: {}", decision.id, e);
310 }
311 }
312 }
313
314 self.update_stats(&validated_decisions).await;
316
317 let mut history = self.decision_history.write().await;
319 history.extend(validated_decisions.clone());
320
321 if history.len() > self.config.monitoring.decision_history_size {
323 let len = history.len();
324 history.drain(0..len - self.config.monitoring.decision_history_size);
325 }
326
327 let mut active = self.active_decisions.write().await;
329 for decision in &validated_decisions {
330 active.insert(decision.id.clone(), decision.clone());
331 }
332
333 let elapsed = start.elapsed();
334 info!(
335 "Generated {} validated decisions in {:?}",
336 validated_decisions.len(),
337 elapsed
338 );
339
340 Ok(validated_decisions)
341 }
342
343 async fn record_outcome(&mut self, outcome: DecisionOutcome) -> DecisionResult<()> {
344 debug!("Recording outcome for decision {}", outcome.decision_id);
345
346 let mut active = self.active_decisions.write().await;
348 let decision = active.remove(&outcome.decision_id);
349
350 let mut stats = self.stats.write().await;
352
353 if outcome.success {
354 stats.successful_decisions += 1;
355
356 if let Some(actual_impact) = &outcome.actual_impact {
358 if actual_impact.cost_change_usd < 0.0 {
359 stats.total_cost_savings_usd += actual_impact.cost_change_usd.abs();
360 }
361 if actual_impact.latency_change_ms < 0.0 {
362 stats.total_latency_improvement_ms += actual_impact.latency_change_ms.abs();
363 }
364 }
365 } else {
366 stats.failed_decisions += 1;
367 }
368
369 if outcome.rolled_back {
370 stats.rolled_back_decisions += 1;
371 }
372
373 drop(stats);
374
375 if decision.is_some() {
379 debug!("Strategy learning not implemented for Arc-wrapped strategies");
380 }
381
382 let mut history = self.outcome_history.write().await;
384 history.push(outcome);
385
386 if history.len() > self.config.monitoring.outcome_history_size {
388 let len = history.len();
389 history.drain(0..len - self.config.monitoring.outcome_history_size);
390 }
391
392 Ok(())
393 }
394
395 fn get_stats(&self) -> DecisionStats {
396 DecisionStats::default()
399 }
400
401 async fn health_check(&self) -> DecisionResult<()> {
402 if !self.state.can_process() {
403 return Err(DecisionError::NotRunning);
404 }
405
406 let strategy_count = self.strategies.len();
408 if strategy_count == 0 {
409 return Err(DecisionError::Internal(
410 "No strategies configured".to_string(),
411 ));
412 }
413
414 Ok(())
415 }
416
417 async fn reset(&mut self) -> DecisionResult<()> {
418 info!("Resetting Decision Engine Coordinator");
419
420 self.active_decisions.write().await.clear();
422
423 self.decision_history.write().await.clear();
425 self.outcome_history.write().await.clear();
426
427 *self.stats.write().await = DecisionStats::default();
429
430 Ok(())
431 }
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437 use crate::analyzer::{AnalysisReport, Insight, InsightCategory, Severity, Confidence};
438 use crate::decision::types::*;
439
440 #[tokio::test]
441 async fn test_coordinator_lifecycle() {
442 let config = DecisionEngineConfig::default();
443 let mut coordinator = DecisionCoordinator::new(config, vec![]);
444
445 assert_eq!(coordinator.state(), DecisionState::Initialized);
446
447 coordinator.start().await.unwrap();
448 assert_eq!(coordinator.state(), DecisionState::Running);
449
450 coordinator.stop().await.unwrap();
451 assert_eq!(coordinator.state(), DecisionState::Stopped);
452 }
453
454 #[tokio::test]
455 async fn test_decision_validation() {
456 let config = DecisionEngineConfig::default();
457 let coordinator = DecisionCoordinator::new(config, vec![]);
458
459 let decision = Decision {
460 id: Uuid::new_v4().to_string(),
461 timestamp: Utc::now(),
462 strategy: "test".to_string(),
463 decision_type: DecisionType::NoAction,
464 confidence: 0.9,
465 expected_impact: ExpectedImpact {
466 cost_change_usd: Some(10.0),
467 latency_change_ms: Some(50.0),
468 quality_change: None,
469 throughput_change_rps: None,
470 success_rate_change_pct: None,
471 time_to_impact: std::time::Duration::from_secs(60),
472 confidence: 0.9,
473 },
474 config_changes: vec![],
475 justification: "Test decision".to_string(),
476 related_insights: vec![],
477 related_recommendations: vec![],
478 priority: 50,
479 requires_approval: false,
480 safety_checks: vec![
481 SafetyCheck {
482 name: "cost_check".to_string(),
483 passed: true,
484 details: "OK".to_string(),
485 timestamp: Utc::now(),
486 },
487 SafetyCheck {
488 name: "latency_check".to_string(),
489 passed: true,
490 details: "OK".to_string(),
491 timestamp: Utc::now(),
492 },
493 SafetyCheck {
494 name: "quality_check".to_string(),
495 passed: true,
496 details: "OK".to_string(),
497 timestamp: Utc::now(),
498 },
499 ],
500 rollback_plan: None,
501 metadata: HashMap::new(),
502 };
503
504 let criteria = DecisionCriteria::default();
505
506 assert!(coordinator.validate_decision(&decision, &criteria).await.is_ok());
508 }
509
510 #[tokio::test]
511 async fn test_validation_failures() {
512 let config = DecisionEngineConfig::default();
513 let coordinator = DecisionCoordinator::new(config, vec![]);
514
515 let mut decision = Decision {
516 id: Uuid::new_v4().to_string(),
517 timestamp: Utc::now(),
518 strategy: "test".to_string(),
519 decision_type: DecisionType::NoAction,
520 confidence: 0.5, expected_impact: ExpectedImpact {
522 cost_change_usd: None,
523 latency_change_ms: None,
524 quality_change: None,
525 throughput_change_rps: None,
526 success_rate_change_pct: None,
527 time_to_impact: std::time::Duration::from_secs(60),
528 confidence: 0.5,
529 },
530 config_changes: vec![],
531 justification: "Test".to_string(),
532 related_insights: vec![],
533 related_recommendations: vec![],
534 priority: 50,
535 requires_approval: false,
536 safety_checks: vec![],
537 rollback_plan: None,
538 metadata: HashMap::new(),
539 };
540
541 let criteria = DecisionCriteria::default();
542
543 assert!(coordinator.validate_decision(&decision, &criteria).await.is_err());
545 }
546
547 #[tokio::test]
548 async fn test_health_check() {
549 let config = DecisionEngineConfig::default();
550 let mut coordinator = DecisionCoordinator::new(config, vec![]);
551
552 assert!(coordinator.health_check().await.is_err());
554
555 coordinator.start().await.unwrap();
557 assert!(coordinator.health_check().await.is_ok());
558 }
559}