1use crate::{CostEstimator, NodeId, NodeKind, TimePredictor, Workflow};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct OptimizationReport {
13 pub score: f64,
15
16 pub suggestions: Vec<OptimizationSuggestion>,
18
19 pub issues: Vec<WorkflowIssue>,
21
22 pub improvements: ImprovementSummary,
24
25 pub complexity: ComplexityMetrics,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct OptimizationSuggestion {
32 pub category: SuggestionCategory,
34
35 pub severity: Severity,
37
38 pub description: String,
40
41 pub affected_nodes: Vec<String>,
43
44 pub benefit: Benefit,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
50pub enum SuggestionCategory {
51 CostReduction,
53
54 Performance,
56
57 Parallelization,
59
60 Redundancy,
62
63 ModelSelection,
65
66 ResourceOptimization,
68
69 Architecture,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
75pub enum Severity {
76 Low,
77 Medium,
78 High,
79 Critical,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct Benefit {
85 pub cost_savings_usd: Option<f64>,
87
88 pub time_savings_ms: Option<u64>,
90
91 pub quality_improvement: Option<f64>,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct WorkflowIssue {
98 pub issue_type: IssueType,
100
101 pub description: String,
103
104 pub affected_nodes: Vec<String>,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
110pub enum IssueType {
111 RedundantNodes,
113
114 InefficientSequencing,
116
117 ExpensiveOperation,
119
120 SlowOperation,
122
123 MissingErrorHandling,
125
126 InefficientModel,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct ImprovementSummary {
133 pub total_cost_savings_usd: f64,
135
136 pub total_time_savings_ms: u64,
138
139 pub parallelization_opportunities: usize,
141
142 pub redundant_nodes: usize,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct ComplexityMetrics {
149 pub total_nodes: usize,
151
152 pub total_edges: usize,
154
155 pub max_depth: usize,
157
158 pub avg_branching_factor: f64,
160
161 pub cyclomatic_complexity: usize,
163
164 pub llm_nodes: usize,
166
167 pub conditional_nodes: usize,
169}
170
171pub struct WorkflowOptimizer;
173
174impl WorkflowOptimizer {
175 pub fn analyze(workflow: &Workflow) -> OptimizationReport {
177 let mut suggestions = Vec::new();
178 let mut issues = Vec::new();
179
180 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 issues.extend(Self::detect_issues(workflow));
190
191 let improvements = Self::calculate_improvements(&suggestions);
193
194 let complexity = Self::calculate_complexity(workflow);
196
197 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 fn analyze_cost(workflow: &Workflow) -> Vec<OptimizationSuggestion> {
211 let mut suggestions = Vec::new();
212 let cost_estimate = CostEstimator::estimate(workflow);
213
214 for (node_id, node_cost) in &cost_estimate.node_costs {
216 if node_cost.cost_usd > 0.01 {
217 if let Some(node) = workflow.nodes.iter().find(|n| n.id.to_string() == *node_id) {
219 if let NodeKind::LLM(config) = &node.kind {
220 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), },
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 fn analyze_performance(workflow: &Workflow) -> Vec<OptimizationSuggestion> {
262 let mut suggestions = Vec::new();
263 let time_estimate = TimePredictor::new().predict(workflow);
264
265 let slowest = time_estimate.slowest_nodes(3);
267 for node_time in slowest {
268 if node_time.avg_ms > 5000 {
269 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), quality_improvement: None,
286 },
287 });
288 }
289 }
290
291 suggestions
292 }
293
294 fn analyze_parallelization(workflow: &Workflow) -> Vec<OptimizationSuggestion> {
296 let mut suggestions = Vec::new();
297
298 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 for (node_id, child_ids) in &children {
306 if child_ids.len() > 1 {
307 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), quality_improvement: None,
323 },
324 });
325 }
326 }
327 }
328
329 suggestions
330 }
331
332 fn analyze_redundancy(workflow: &Workflow) -> Vec<OptimizationSuggestion> {
334 let mut suggestions = Vec::new();
335
336 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 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 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 fn analyze_caching(workflow: &Workflow) -> Vec<OptimizationSuggestion> {
411 let mut suggestions = Vec::new();
412
413 for node in &workflow.nodes {
415 match &node.kind {
416 NodeKind::LLM(config) => {
417 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), time_savings_ms: Some(2000), quality_improvement: None,
434 },
435 });
436 }
437
438 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), time_savings_ms: Some(5000), quality_improvement: None,
452 },
453 });
454 }
455 }
456 NodeKind::Retriever(_config) => {
457 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), time_savings_ms: Some(100), quality_improvement: None,
470 },
471 });
472
473 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), quality_improvement: None,
487 },
488 });
489 }
490 }
491 NodeKind::Code(config) => {
492 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), quality_improvement: None,
506 },
507 });
508 }
509 }
510 _ => {}
511 }
512 }
513
514 suggestions
515 }
516
517 fn is_node_in_loop(workflow: &Workflow, _node_id: &NodeId) -> bool {
519 workflow
523 .nodes
524 .iter()
525 .any(|n| matches!(n.kind, NodeKind::Loop(_)))
526 }
527
528 fn detect_issues(workflow: &Workflow) -> Vec<WorkflowIssue> {
530 let mut issues = Vec::new();
531
532 for node in &workflow.nodes {
534 if matches!(
535 node.kind,
536 NodeKind::LLM(_) | NodeKind::Retriever(_) | NodeKind::Code(_) | NodeKind::Tool(_)
537 ) {
538 let has_error_handling = node.retry_config.is_some()
540 || workflow.nodes.iter().any(|n| {
541 if let NodeKind::TryCatch(_) = &n.kind {
542 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 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 fn calculate_complexity(workflow: &Workflow) -> ComplexityMetrics {
597 let total_nodes = workflow.nodes.len();
598 let total_edges = workflow.edges.len();
599
600 let max_depth = total_nodes; 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 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 let cyclomatic_complexity = if total_edges >= total_nodes {
634 total_edges - total_nodes + 2
635 } else {
636 1 };
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 fn calculate_score(
652 _workflow: &Workflow,
653 suggestions: &[OptimizationSuggestion],
654 issues: &[WorkflowIssue],
655 ) -> f64 {
656 let mut score = 1.0;
657
658 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 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 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 score -= issues.len() as f64 * 0.05;
681
682 score.clamp(0.0, 1.0)
684 }
685}
686
687impl OptimizationReport {
688 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 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 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), 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 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 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(), 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 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 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 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 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}