oxify_model/
optimizer.rs

1//! Workflow optimization and analysis
2//!
3//! This module analyzes workflows and provides optimization suggestions
4//! including redundancy detection, parallelization opportunities, and cost/time improvements.
5
6use crate::{CostEstimator, NodeId, NodeKind, TimePredictor, Workflow};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Optimization analysis result
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct OptimizationReport {
13    /// Overall optimization score (0.0 = poor, 1.0 = optimal)
14    pub score: f64,
15
16    /// List of optimization suggestions
17    pub suggestions: Vec<OptimizationSuggestion>,
18
19    /// Detected issues
20    pub issues: Vec<WorkflowIssue>,
21
22    /// Potential improvements summary
23    pub improvements: ImprovementSummary,
24
25    /// Complexity metrics
26    pub complexity: ComplexityMetrics,
27}
28
29/// A single optimization suggestion
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct OptimizationSuggestion {
32    /// Suggestion category
33    pub category: SuggestionCategory,
34
35    /// Severity (Critical, High, Medium, Low)
36    pub severity: Severity,
37
38    /// Human-readable description
39    pub description: String,
40
41    /// Affected node IDs
42    pub affected_nodes: Vec<String>,
43
44    /// Expected benefit
45    pub benefit: Benefit,
46}
47
48/// Category of optimization suggestion
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
50pub enum SuggestionCategory {
51    /// Cost reduction opportunities
52    CostReduction,
53
54    /// Performance improvement opportunities
55    Performance,
56
57    /// Parallelization opportunities
58    Parallelization,
59
60    /// Redundancy elimination
61    Redundancy,
62
63    /// Model selection improvements
64    ModelSelection,
65
66    /// Resource optimization
67    ResourceOptimization,
68
69    /// Architecture improvements
70    Architecture,
71}
72
73/// Severity level
74#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
75pub enum Severity {
76    Low,
77    Medium,
78    High,
79    Critical,
80}
81
82/// Expected benefit from applying suggestion
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct Benefit {
85    /// Estimated cost savings (USD)
86    pub cost_savings_usd: Option<f64>,
87
88    /// Estimated time savings (ms)
89    pub time_savings_ms: Option<u64>,
90
91    /// Estimated quality improvement (0.0 to 1.0)
92    pub quality_improvement: Option<f64>,
93}
94
95/// Detected workflow issue
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct WorkflowIssue {
98    /// Issue type
99    pub issue_type: IssueType,
100
101    /// Description
102    pub description: String,
103
104    /// Affected nodes
105    pub affected_nodes: Vec<String>,
106}
107
108/// Type of workflow issue
109#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
110pub enum IssueType {
111    /// Redundant nodes detected
112    RedundantNodes,
113
114    /// Inefficient sequencing
115    InefficientSequencing,
116
117    /// Expensive operations
118    ExpensiveOperation,
119
120    /// Slow operations
121    SlowOperation,
122
123    /// Missing error handling
124    MissingErrorHandling,
125
126    /// Inefficient model choice
127    InefficientModel,
128}
129
130/// Summary of potential improvements
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct ImprovementSummary {
133    /// Total potential cost savings
134    pub total_cost_savings_usd: f64,
135
136    /// Total potential time savings
137    pub total_time_savings_ms: u64,
138
139    /// Number of parallelization opportunities
140    pub parallelization_opportunities: usize,
141
142    /// Number of redundant nodes
143    pub redundant_nodes: usize,
144}
145
146/// Workflow complexity metrics
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct ComplexityMetrics {
149    /// Total number of nodes
150    pub total_nodes: usize,
151
152    /// Total number of edges
153    pub total_edges: usize,
154
155    /// Maximum depth (longest path)
156    pub max_depth: usize,
157
158    /// Average branching factor
159    pub avg_branching_factor: f64,
160
161    /// Cyclomatic complexity
162    pub cyclomatic_complexity: usize,
163
164    /// Number of LLM nodes
165    pub llm_nodes: usize,
166
167    /// Number of conditional branches
168    pub conditional_nodes: usize,
169}
170
171/// Workflow optimizer and analyzer
172pub struct WorkflowOptimizer;
173
174impl WorkflowOptimizer {
175    /// Analyze a workflow and provide optimization suggestions
176    pub fn analyze(workflow: &Workflow) -> OptimizationReport {
177        let mut suggestions = Vec::new();
178        let mut issues = Vec::new();
179
180        // Analyze various aspects
181        suggestions.extend(Self::analyze_cost(workflow));
182        suggestions.extend(Self::analyze_performance(workflow));
183        suggestions.extend(Self::analyze_parallelization(workflow));
184        suggestions.extend(Self::analyze_redundancy(workflow));
185        suggestions.extend(Self::analyze_model_selection(workflow));
186        suggestions.extend(Self::analyze_caching(workflow));
187
188        // Detect issues
189        issues.extend(Self::detect_issues(workflow));
190
191        // Calculate improvements summary
192        let improvements = Self::calculate_improvements(&suggestions);
193
194        // Calculate complexity metrics
195        let complexity = Self::calculate_complexity(workflow);
196
197        // Calculate overall score
198        let score = Self::calculate_score(workflow, &suggestions, &issues);
199
200        OptimizationReport {
201            score,
202            suggestions,
203            issues,
204            improvements,
205            complexity,
206        }
207    }
208
209    /// Analyze cost optimization opportunities
210    fn analyze_cost(workflow: &Workflow) -> Vec<OptimizationSuggestion> {
211        let mut suggestions = Vec::new();
212        let cost_estimate = CostEstimator::estimate(workflow);
213
214        // Find expensive nodes
215        for (node_id, node_cost) in &cost_estimate.node_costs {
216            if node_cost.cost_usd > 0.01 {
217                // Find the actual node
218                if let Some(node) = workflow.nodes.iter().find(|n| n.id.to_string() == *node_id) {
219                    if let NodeKind::LLM(config) = &node.kind {
220                        // Suggest cheaper models
221                        if config.model.contains("gpt-4") && !config.model.contains("turbo") {
222                            suggestions.push(OptimizationSuggestion {
223                                category: SuggestionCategory::CostReduction,
224                                severity: Severity::Medium,
225                                description: format!(
226                                    "Consider using GPT-4-turbo instead of {} for 67% cost reduction",
227                                    config.model
228                                ),
229                                affected_nodes: vec![node_id.clone()],
230                                benefit: Benefit {
231                                    cost_savings_usd: Some(node_cost.cost_usd * 0.67),
232                                    time_savings_ms: None,
233                                    quality_improvement: Some(-0.05), // Slight quality tradeoff
234                                },
235                            });
236                        } else if config.model.contains("claude-3-opus") {
237                            suggestions.push(OptimizationSuggestion {
238                                category: SuggestionCategory::CostReduction,
239                                severity: Severity::Medium,
240                                description: format!(
241                                    "Consider using Claude-3-Sonnet for 80% cost reduction ({})",
242                                    node.name
243                                ),
244                                affected_nodes: vec![node_id.clone()],
245                                benefit: Benefit {
246                                    cost_savings_usd: Some(node_cost.cost_usd * 0.8),
247                                    time_savings_ms: None,
248                                    quality_improvement: Some(-0.1),
249                                },
250                            });
251                        }
252                    }
253                }
254            }
255        }
256
257        suggestions
258    }
259
260    /// Analyze performance optimization opportunities
261    fn analyze_performance(workflow: &Workflow) -> Vec<OptimizationSuggestion> {
262        let mut suggestions = Vec::new();
263        let time_estimate = TimePredictor::new().predict(workflow);
264
265        // Find slow nodes
266        let slowest = time_estimate.slowest_nodes(3);
267        for node_time in slowest {
268            if node_time.avg_ms > 5000 {
269                // Over 5 seconds
270                suggestions.push(OptimizationSuggestion {
271                    category: SuggestionCategory::Performance,
272                    severity: if node_time.avg_ms > 10000 {
273                        Severity::High
274                    } else {
275                        Severity::Medium
276                    },
277                    description: format!(
278                        "Node '{}' is slow (avg: {}ms). Consider caching or optimization.",
279                        node_time.node_name, node_time.avg_ms
280                    ),
281                    affected_nodes: vec![node_time.node_name.clone()],
282                    benefit: Benefit {
283                        cost_savings_usd: None,
284                        time_savings_ms: Some(node_time.avg_ms / 2), // Assume 50% improvement
285                        quality_improvement: None,
286                    },
287                });
288            }
289        }
290
291        suggestions
292    }
293
294    /// Analyze parallelization opportunities
295    fn analyze_parallelization(workflow: &Workflow) -> Vec<OptimizationSuggestion> {
296        let mut suggestions = Vec::new();
297
298        // Build adjacency map
299        let mut children: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
300        for edge in &workflow.edges {
301            children.entry(edge.from).or_default().push(edge.to);
302        }
303
304        // Find nodes with multiple children that could be parallelized
305        for (node_id, child_ids) in &children {
306            if child_ids.len() > 1 {
307                // Check if children are independent (don't reference each other)
308                let node = workflow.nodes.iter().find(|n| n.id == *node_id);
309                if let Some(node) = node {
310                    suggestions.push(OptimizationSuggestion {
311                        category: SuggestionCategory::Parallelization,
312                        severity: Severity::Medium,
313                        description: format!(
314                            "Node '{}' has {} sequential children that could be parallelized",
315                            node.name,
316                            child_ids.len()
317                        ),
318                        affected_nodes: vec![node_id.to_string()],
319                        benefit: Benefit {
320                            cost_savings_usd: None,
321                            time_savings_ms: Some(5000), // Rough estimate
322                            quality_improvement: None,
323                        },
324                    });
325                }
326            }
327        }
328
329        suggestions
330    }
331
332    /// Analyze redundancy
333    fn analyze_redundancy(workflow: &Workflow) -> Vec<OptimizationSuggestion> {
334        let mut suggestions = Vec::new();
335
336        // Find duplicate LLM configurations (same provider, model, and prompt)
337        let mut llm_configs: HashMap<String, Vec<String>> = HashMap::new();
338
339        for node in &workflow.nodes {
340            if let NodeKind::LLM(config) = &node.kind {
341                let key = format!(
342                    "{}:{}:{}",
343                    config.provider, config.model, config.prompt_template
344                );
345                llm_configs
346                    .entry(key)
347                    .or_default()
348                    .push(node.id.to_string());
349            }
350        }
351
352        for (_config_key, node_ids) in llm_configs {
353            if node_ids.len() > 1 {
354                suggestions.push(OptimizationSuggestion {
355                    category: SuggestionCategory::Redundancy,
356                    severity: Severity::Medium,
357                    description: format!(
358                        "Found {} nodes with identical LLM configuration. Consider caching or deduplication.",
359                        node_ids.len()
360                    ),
361                    affected_nodes: node_ids.clone(),
362                    benefit: Benefit {
363                        cost_savings_usd: Some(0.01 * (node_ids.len() - 1) as f64),
364                        time_savings_ms: Some(1000 * (node_ids.len() - 1) as u64),
365                        quality_improvement: None,
366                    },
367                });
368            }
369        }
370
371        suggestions
372    }
373
374    /// Analyze model selection
375    fn analyze_model_selection(workflow: &Workflow) -> Vec<OptimizationSuggestion> {
376        let mut suggestions = Vec::new();
377
378        for node in &workflow.nodes {
379            if let NodeKind::LLM(config) = &node.kind {
380                // Check if simple prompts are using expensive models
381                let prompt_length = config.prompt_template.len();
382                let max_tokens = config.max_tokens.unwrap_or(1000);
383
384                if prompt_length < 100
385                    && max_tokens < 200
386                    && (config.model.contains("gpt-4") || config.model.contains("claude-3-opus"))
387                {
388                    suggestions.push(OptimizationSuggestion {
389                            category: SuggestionCategory::ModelSelection,
390                            severity: Severity::Low,
391                            description: format!(
392                                "Node '{}' uses expensive model for simple prompt. Consider GPT-3.5 or Claude-Haiku.",
393                                node.name
394                            ),
395                            affected_nodes: vec![node.id.to_string()],
396                            benefit: Benefit {
397                                cost_savings_usd: Some(0.005),
398                                time_savings_ms: Some(500),
399                                quality_improvement: Some(-0.05),
400                            },
401                        });
402                }
403            }
404        }
405
406        suggestions
407    }
408
409    /// Analyze caching opportunities
410    fn analyze_caching(workflow: &Workflow) -> Vec<OptimizationSuggestion> {
411        let mut suggestions = Vec::new();
412
413        // Identify LLM nodes that could benefit from caching
414        for node in &workflow.nodes {
415            match &node.kind {
416                NodeKind::LLM(config) => {
417                    // Check if prompt is deterministic (no templates or simple templates)
418                    let has_simple_template = !config.prompt_template.contains("{{")
419                        || config.prompt_template.matches("{{").count() <= 2;
420
421                    if has_simple_template {
422                        suggestions.push(OptimizationSuggestion {
423                            category: SuggestionCategory::ResourceOptimization,
424                            severity: Severity::Medium,
425                            description: format!(
426                                "Node '{}' could benefit from response caching. Deterministic prompts can be cached to save cost and time.",
427                                node.name
428                            ),
429                            affected_nodes: vec![node.id.to_string()],
430                            benefit: Benefit {
431                                cost_savings_usd: Some(0.01), // Average savings per cached call
432                                time_savings_ms: Some(2000),  // Average LLM latency saved
433                                quality_improvement: None,
434                            },
435                        });
436                    }
437
438                    // Check for nodes inside loops that could benefit from memoization
439                    if Self::is_node_in_loop(workflow, &node.id) {
440                        suggestions.push(OptimizationSuggestion {
441                            category: SuggestionCategory::Performance,
442                            severity: Severity::High,
443                            description: format!(
444                                "Node '{}' is inside a loop. Enable result memoization to avoid redundant LLM calls.",
445                                node.name
446                            ),
447                            affected_nodes: vec![node.id.to_string()],
448                            benefit: Benefit {
449                                cost_savings_usd: Some(0.05), // Could save multiple calls
450                                time_savings_ms: Some(5000),  // Multiple calls avoided
451                                quality_improvement: None,
452                            },
453                        });
454                    }
455                }
456                NodeKind::Retriever(_config) => {
457                    // Vector retrieval results can often be cached
458                    suggestions.push(OptimizationSuggestion {
459                        category: SuggestionCategory::Performance,
460                        severity: Severity::Medium,
461                        description: format!(
462                            "Vector retrieval in node '{}' could use query result caching. Cache TTL: 5-15 minutes recommended.",
463                            node.name
464                        ),
465                        affected_nodes: vec![node.id.to_string()],
466                        benefit: Benefit {
467                            cost_savings_usd: Some(0.001), // Vector DB query costs
468                            time_savings_ms: Some(100),    // DB latency saved
469                            quality_improvement: None,
470                        },
471                    });
472
473                    // Check if retriever is in a loop
474                    if Self::is_node_in_loop(workflow, &node.id) {
475                        suggestions.push(OptimizationSuggestion {
476                            category: SuggestionCategory::Performance,
477                            severity: Severity::High,
478                            description: format!(
479                                "Retriever '{}' is in a loop. Batch retrieval or aggressive caching strongly recommended.",
480                                node.name
481                            ),
482                            affected_nodes: vec![node.id.to_string()],
483                            benefit: Benefit {
484                                cost_savings_usd: Some(0.01),
485                                time_savings_ms: Some(1000), // Multiple DB calls avoided
486                                quality_improvement: None,
487                            },
488                        });
489                    }
490                }
491                NodeKind::Code(config) => {
492                    // Pure functions in code nodes can be memoized
493                    if config.runtime == "rust" || config.runtime == "wasm" {
494                        suggestions.push(OptimizationSuggestion {
495                            category: SuggestionCategory::Performance,
496                            severity: Severity::Low,
497                            description: format!(
498                                "Code node '{}' could use function memoization if it's a pure function.",
499                                node.name
500                            ),
501                            affected_nodes: vec![node.id.to_string()],
502                            benefit: Benefit {
503                                cost_savings_usd: None,
504                                time_savings_ms: Some(50), // Code execution time saved
505                                quality_improvement: None,
506                            },
507                        });
508                    }
509                }
510                _ => {}
511            }
512        }
513
514        suggestions
515    }
516
517    /// Helper: Check if a node is inside a loop
518    fn is_node_in_loop(workflow: &Workflow, _node_id: &NodeId) -> bool {
519        // Check if there's any loop node in the workflow
520        // In a real implementation, this would do graph traversal to check
521        // if the node is reachable from a loop node
522        workflow
523            .nodes
524            .iter()
525            .any(|n| matches!(n.kind, NodeKind::Loop(_)))
526    }
527
528    /// Detect workflow issues
529    fn detect_issues(workflow: &Workflow) -> Vec<WorkflowIssue> {
530        let mut issues = Vec::new();
531
532        // Check for nodes without error handling
533        for node in &workflow.nodes {
534            if matches!(
535                node.kind,
536                NodeKind::LLM(_) | NodeKind::Retriever(_) | NodeKind::Code(_) | NodeKind::Tool(_)
537            ) {
538                // Check if node is wrapped in try-catch
539                let has_error_handling = node.retry_config.is_some()
540                    || workflow.nodes.iter().any(|n| {
541                        if let NodeKind::TryCatch(_) = &n.kind {
542                            // Simplified check - in real implementation would check graph structure
543                            true
544                        } else {
545                            false
546                        }
547                    });
548
549                if !has_error_handling && node.retry_config.is_none() {
550                    issues.push(WorkflowIssue {
551                        issue_type: IssueType::MissingErrorHandling,
552                        description: format!(
553                            "Node '{}' lacks error handling (no retry config or try-catch)",
554                            node.name
555                        ),
556                        affected_nodes: vec![node.id.to_string()],
557                    });
558                }
559            }
560        }
561
562        issues
563    }
564
565    /// Calculate improvements summary
566    fn calculate_improvements(suggestions: &[OptimizationSuggestion]) -> ImprovementSummary {
567        let mut total_cost_savings = 0.0;
568        let mut total_time_savings = 0u64;
569        let mut parallelization_opportunities = 0;
570        let mut redundant_nodes = 0;
571
572        for suggestion in suggestions {
573            if let Some(cost) = suggestion.benefit.cost_savings_usd {
574                total_cost_savings += cost;
575            }
576            if let Some(time) = suggestion.benefit.time_savings_ms {
577                total_time_savings += time;
578            }
579            if suggestion.category == SuggestionCategory::Parallelization {
580                parallelization_opportunities += 1;
581            }
582            if suggestion.category == SuggestionCategory::Redundancy {
583                redundant_nodes += suggestion.affected_nodes.len();
584            }
585        }
586
587        ImprovementSummary {
588            total_cost_savings_usd: total_cost_savings,
589            total_time_savings_ms: total_time_savings,
590            parallelization_opportunities,
591            redundant_nodes,
592        }
593    }
594
595    /// Calculate complexity metrics
596    fn calculate_complexity(workflow: &Workflow) -> ComplexityMetrics {
597        let total_nodes = workflow.nodes.len();
598        let total_edges = workflow.edges.len();
599
600        // Calculate max depth (simplified)
601        let max_depth = total_nodes; // Worst case linear
602
603        // Calculate branching factor
604        let mut out_degrees: HashMap<NodeId, usize> = HashMap::new();
605        for edge in &workflow.edges {
606            *out_degrees.entry(edge.from).or_insert(0) += 1;
607        }
608        let avg_branching_factor = if !out_degrees.is_empty() {
609            out_degrees.values().sum::<usize>() as f64 / out_degrees.len() as f64
610        } else {
611            0.0
612        };
613
614        // Count node types
615        let llm_nodes = workflow
616            .nodes
617            .iter()
618            .filter(|n| matches!(n.kind, NodeKind::LLM(_)))
619            .count();
620
621        let conditional_nodes = workflow
622            .nodes
623            .iter()
624            .filter(|n| {
625                matches!(
626                    n.kind,
627                    NodeKind::IfElse(_) | NodeKind::Switch(_) | NodeKind::Loop(_)
628                )
629            })
630            .count();
631
632        // Cyclomatic complexity (simplified)
633        let cyclomatic_complexity = if total_edges >= total_nodes {
634            total_edges - total_nodes + 2
635        } else {
636            1 // Minimum complexity
637        };
638
639        ComplexityMetrics {
640            total_nodes,
641            total_edges,
642            max_depth,
643            avg_branching_factor,
644            cyclomatic_complexity,
645            llm_nodes,
646            conditional_nodes,
647        }
648    }
649
650    /// Calculate overall optimization score
651    fn calculate_score(
652        _workflow: &Workflow,
653        suggestions: &[OptimizationSuggestion],
654        issues: &[WorkflowIssue],
655    ) -> f64 {
656        let mut score = 1.0;
657
658        // Penalize for critical suggestions
659        let critical_count = suggestions
660            .iter()
661            .filter(|s| s.severity == Severity::Critical)
662            .count();
663        score -= critical_count as f64 * 0.15;
664
665        // Penalize for high severity suggestions
666        let high_count = suggestions
667            .iter()
668            .filter(|s| s.severity == Severity::High)
669            .count();
670        score -= high_count as f64 * 0.10;
671
672        // Penalize for medium severity suggestions
673        let medium_count = suggestions
674            .iter()
675            .filter(|s| s.severity == Severity::Medium)
676            .count();
677        score -= medium_count as f64 * 0.05;
678
679        // Penalize for issues
680        score -= issues.len() as f64 * 0.05;
681
682        // Ensure score is between 0 and 1
683        score.clamp(0.0, 1.0)
684    }
685}
686
687impl OptimizationReport {
688    /// Format report as human-readable string
689    pub fn format_summary(&self) -> String {
690        let mut output = String::new();
691
692        output.push_str(&format!("Optimization Score: {:.0}%\n", self.score * 100.0));
693        output.push_str(&format!(
694            "Potential Savings: ${:.4} | {}ms\n",
695            self.improvements.total_cost_savings_usd, self.improvements.total_time_savings_ms
696        ));
697        output.push_str(&format!(
698            "Opportunities: {} parallelization, {} redundant nodes\n",
699            self.improvements.parallelization_opportunities, self.improvements.redundant_nodes
700        ));
701        output.push_str(&format!(
702            "Complexity: {} nodes, {} edges, depth {}\n",
703            self.complexity.total_nodes, self.complexity.total_edges, self.complexity.max_depth
704        ));
705        output.push_str(&format!("\nSuggestions: {}\n", self.suggestions.len()));
706        for (i, suggestion) in self.suggestions.iter().take(5).enumerate() {
707            output.push_str(&format!(
708                "  {}. [{:?}] {}\n",
709                i + 1,
710                suggestion.severity,
711                suggestion.description
712            ));
713        }
714        if self.suggestions.len() > 5 {
715            output.push_str(&format!("  ... and {} more\n", self.suggestions.len() - 5));
716        }
717
718        output
719    }
720
721    /// Get high-priority suggestions
722    pub fn high_priority_suggestions(&self) -> Vec<&OptimizationSuggestion> {
723        self.suggestions
724            .iter()
725            .filter(|s| s.severity >= Severity::High)
726            .collect()
727    }
728
729    /// Get suggestions by category
730    pub fn suggestions_by_category(
731        &self,
732        category: SuggestionCategory,
733    ) -> Vec<&OptimizationSuggestion> {
734        self.suggestions
735            .iter()
736            .filter(|s| s.category == category)
737            .collect()
738    }
739}
740
741#[cfg(test)]
742mod tests {
743    use super::*;
744    use crate::{LlmConfig, WorkflowBuilder};
745
746    #[test]
747    fn test_optimizer_basic() {
748        let workflow = WorkflowBuilder::new("Test")
749            .start("Start")
750            .llm(
751                "Generate",
752                LlmConfig {
753                    provider: "openai".to_string(),
754                    model: "gpt-4".to_string(),
755                    system_prompt: None,
756                    prompt_template: "Hello".to_string(),
757                    temperature: None,
758                    max_tokens: Some(100),
759                    tools: vec![],
760                    images: vec![],
761                    extra_params: serde_json::Value::Null,
762                },
763            )
764            .end("End")
765            .build();
766
767        let report = WorkflowOptimizer::analyze(&workflow);
768
769        assert!(report.score > 0.0 && report.score <= 1.0);
770        assert!(!report.suggestions.is_empty());
771    }
772
773    #[test]
774    fn test_cost_reduction_suggestion() {
775        let workflow = WorkflowBuilder::new("Expensive")
776            .start("Start")
777            .llm(
778                "GPT4",
779                LlmConfig {
780                    provider: "openai".to_string(),
781                    model: "gpt-4".to_string(),
782                    system_prompt: None,
783                    prompt_template: "test".to_string(),
784                    temperature: None,
785                    max_tokens: Some(1000),
786                    tools: vec![],
787                    images: vec![],
788                    extra_params: serde_json::Value::Null,
789                },
790            )
791            .end("End")
792            .build();
793
794        let report = WorkflowOptimizer::analyze(&workflow);
795        let cost_suggestions: Vec<_> = report
796            .suggestions
797            .iter()
798            .filter(|s| s.category == SuggestionCategory::CostReduction)
799            .collect();
800
801        assert!(!cost_suggestions.is_empty());
802    }
803
804    #[test]
805    fn test_redundancy_detection() {
806        let llm_config = LlmConfig {
807            provider: "openai".to_string(),
808            model: "gpt-3.5-turbo".to_string(),
809            system_prompt: None,
810            prompt_template: "duplicate".to_string(),
811            temperature: None,
812            max_tokens: Some(100),
813            tools: vec![],
814            images: vec![],
815            extra_params: serde_json::Value::Null,
816        };
817
818        let workflow = WorkflowBuilder::new("Redundant")
819            .start("Start")
820            .llm("LLM1", llm_config.clone())
821            .llm("LLM2", llm_config)
822            .end("End")
823            .build();
824
825        let report = WorkflowOptimizer::analyze(&workflow);
826        let redundancy_suggestions: Vec<_> = report
827            .suggestions
828            .iter()
829            .filter(|s| s.category == SuggestionCategory::Redundancy)
830            .collect();
831
832        assert!(!redundancy_suggestions.is_empty());
833    }
834
835    #[test]
836    fn test_complexity_metrics() {
837        let workflow = WorkflowBuilder::new("Complex")
838            .start("Start")
839            .llm(
840                "LLM1",
841                LlmConfig {
842                    provider: "openai".to_string(),
843                    model: "gpt-3.5-turbo".to_string(),
844                    system_prompt: None,
845                    prompt_template: "test1".to_string(),
846                    temperature: None,
847                    max_tokens: Some(100),
848                    tools: vec![],
849                    images: vec![],
850                    extra_params: serde_json::Value::Null,
851                },
852            )
853            .llm(
854                "LLM2",
855                LlmConfig {
856                    provider: "openai".to_string(),
857                    model: "gpt-3.5-turbo".to_string(),
858                    system_prompt: None,
859                    prompt_template: "test2".to_string(),
860                    temperature: None,
861                    max_tokens: Some(100),
862                    tools: vec![],
863                    images: vec![],
864                    extra_params: serde_json::Value::Null,
865                },
866            )
867            .end("End")
868            .build();
869
870        let report = WorkflowOptimizer::analyze(&workflow);
871
872        assert_eq!(report.complexity.total_nodes, 4);
873        assert_eq!(report.complexity.llm_nodes, 2);
874        assert!(report.complexity.total_edges > 0);
875    }
876
877    #[test]
878    fn test_report_format() {
879        let workflow = WorkflowBuilder::new("Test")
880            .start("Start")
881            .end("End")
882            .build();
883
884        let report = WorkflowOptimizer::analyze(&workflow);
885        let summary = report.format_summary();
886
887        assert!(summary.contains("Optimization Score:"));
888        assert!(summary.contains("Potential Savings:"));
889        assert!(summary.contains("Complexity:"));
890    }
891
892    #[test]
893    fn test_high_priority_suggestions() {
894        let workflow = WorkflowBuilder::new("Test")
895            .start("Start")
896            .llm(
897                "Expensive",
898                LlmConfig {
899                    provider: "openai".to_string(),
900                    model: "gpt-4".to_string(),
901                    system_prompt: None,
902                    prompt_template: "x".repeat(10000), // Very long prompt
903                    temperature: None,
904                    max_tokens: Some(4000),
905                    tools: vec![],
906                    images: vec![],
907                    extra_params: serde_json::Value::Null,
908                },
909            )
910            .end("End")
911            .build();
912
913        let report = WorkflowOptimizer::analyze(&workflow);
914        let high_priority = report.high_priority_suggestions();
915
916        // Should have high priority suggestions for expensive operations
917        assert!(!high_priority.is_empty() || !report.suggestions.is_empty());
918    }
919
920    #[test]
921    fn test_improvements_summary() {
922        let workflow = WorkflowBuilder::new("Test")
923            .start("Start")
924            .llm(
925                "GPT4",
926                LlmConfig {
927                    provider: "openai".to_string(),
928                    model: "gpt-4".to_string(),
929                    system_prompt: None,
930                    prompt_template: "test".to_string(),
931                    temperature: None,
932                    max_tokens: Some(1000),
933                    tools: vec![],
934                    images: vec![],
935                    extra_params: serde_json::Value::Null,
936                },
937            )
938            .end("End")
939            .build();
940
941        let report = WorkflowOptimizer::analyze(&workflow);
942
943        assert!(report.improvements.total_cost_savings_usd >= 0.0);
944        // total_time_savings_ms is u64, so always >= 0
945        assert!(report.improvements.total_time_savings_ms < u64::MAX);
946    }
947
948    #[test]
949    fn test_caching_recommendations() {
950        use crate::VectorConfig;
951
952        let workflow = WorkflowBuilder::new("Cache Test")
953            .start("Start")
954            .llm(
955                "SimpleLLM",
956                LlmConfig {
957                    provider: "openai".to_string(),
958                    model: "gpt-3.5-turbo".to_string(),
959                    system_prompt: None,
960                    prompt_template: "What is 2+2?".to_string(), // Simple, deterministic
961                    temperature: None,
962                    max_tokens: Some(50),
963                    tools: vec![],
964                    images: vec![],
965                    extra_params: serde_json::Value::Null,
966                },
967            )
968            .retriever(
969                "VectorSearch",
970                VectorConfig {
971                    db_type: "qdrant".to_string(),
972                    collection: "docs".to_string(),
973                    query: "test query".to_string(),
974                    top_k: 5,
975                    score_threshold: Some(0.7),
976                },
977            )
978            .end("End")
979            .build();
980
981        let report = WorkflowOptimizer::analyze(&workflow);
982
983        // Should have caching suggestions for both LLM and Retriever nodes
984        let caching_suggestions: Vec<_> = report
985            .suggestions
986            .iter()
987            .filter(|s| {
988                s.description.contains("caching")
989                    || s.description.contains("memoization")
990                    || s.description.contains("Cache")
991            })
992            .collect();
993
994        assert!(
995            !caching_suggestions.is_empty(),
996            "Should have at least one caching recommendation"
997        );
998
999        // Check that we have suggestions for the retriever
1000        let retriever_caching: Vec<_> = caching_suggestions
1001            .iter()
1002            .filter(|s| s.description.contains("Vector") || s.description.contains("retrieval"))
1003            .collect();
1004
1005        assert!(
1006            !retriever_caching.is_empty(),
1007            "Should have caching recommendations for retriever"
1008        );
1009    }
1010
1011    #[test]
1012    fn test_caching_in_loop() {
1013        use crate::{LoopConfig, LoopType};
1014
1015        let workflow = WorkflowBuilder::new("Loop Cache Test")
1016            .start("Start")
1017            .loop_node(
1018                "ForEach",
1019                LoopConfig {
1020                    loop_type: LoopType::ForEach {
1021                        collection_path: "items".to_string(),
1022                        item_variable: "item".to_string(),
1023                        index_variable: None,
1024                        body_expression: "process".to_string(),
1025                        parallel: false,
1026                        max_concurrency: None,
1027                    },
1028                    max_iterations: 100,
1029                },
1030            )
1031            .llm(
1032                "InLoopLLM",
1033                LlmConfig {
1034                    provider: "openai".to_string(),
1035                    model: "gpt-3.5-turbo".to_string(),
1036                    system_prompt: None,
1037                    prompt_template: "Process {{item}}".to_string(),
1038                    temperature: None,
1039                    max_tokens: Some(100),
1040                    tools: vec![],
1041                    images: vec![],
1042                    extra_params: serde_json::Value::Null,
1043                },
1044            )
1045            .end("End")
1046            .build();
1047
1048        let report = WorkflowOptimizer::analyze(&workflow);
1049
1050        // Should have high-severity memoization suggestions for nodes in loops
1051        let loop_memoization: Vec<_> = report
1052            .suggestions
1053            .iter()
1054            .filter(|s| s.description.contains("loop") && s.description.contains("memoization"))
1055            .collect();
1056
1057        assert!(
1058            !loop_memoization.is_empty(),
1059            "Should have memoization recommendations for nodes in loops"
1060        );
1061
1062        // Check severity is High for loop memoization
1063        let has_high_severity = loop_memoization
1064            .iter()
1065            .any(|s| s.severity == Severity::High);
1066        assert!(
1067            has_high_severity,
1068            "Loop memoization should have High severity"
1069        );
1070    }
1071}