Skip to main content

bamboo_agent_core/tools/
smart_code_review.rs

1//! Smart Code Review Tool - An example Agentic Tool
2//!
3//! This tool demonstrates autonomous decision-making capabilities:
4//! - Automatically detects code language
5//! - Decides review strategy based on complexity
6//! - Asks for user clarification when critical issues are found
7
8use async_trait::async_trait;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12use crate::tools::agentic::{
13    AgenticContext, AgenticTool, AgenticToolResult, InteractionRole, ToolGoal,
14};
15use crate::tools::ToolError;
16
17/// Language detection result
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct LanguageInfo {
20    /// Detected programming language
21    pub language: String,
22    /// Confidence score (0.0-1.0)
23    pub confidence: f64,
24    /// File extension detected
25    pub file_extension: String,
26}
27
28/// Complexity metrics for code
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ComplexityMetrics {
31    /// Total lines of code
32    pub lines_of_code: usize,
33    /// Cyclomatic complexity score
34    pub cyclomatic_complexity: usize,
35    /// Number of functions/methods
36    pub function_count: usize,
37    /// Lines in the largest function
38    pub max_function_lines: usize,
39}
40
41/// Review strategy determined by the tool
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub enum ReviewStrategy {
44    /// Quick review for simple code
45    Quick,
46    /// Standard review with common checks
47    Standard,
48    /// Deep review with comprehensive analysis
49    Deep,
50}
51
52/// A smart code review tool that makes autonomous decisions
53pub struct SmartCodeReviewTool {
54    /// Tool name
55    name: String,
56    /// Tool description
57    description: String,
58    /// Language detection patterns (language -> file extensions)
59    language_patterns: HashMap<String, Vec<String>>,
60}
61
62impl Default for SmartCodeReviewTool {
63    fn default() -> Self {
64        let mut language_patterns = HashMap::new();
65        language_patterns.insert("rust".to_string(), vec![".rs".to_string()]);
66        language_patterns.insert("python".to_string(), vec![".py".to_string()]);
67        language_patterns.insert(
68            "javascript".to_string(),
69            vec![".js".to_string(), ".jsx".to_string()],
70        );
71        language_patterns.insert(
72            "typescript".to_string(),
73            vec![".ts".to_string(), ".tsx".to_string()],
74        );
75        language_patterns.insert("go".to_string(), vec![".go".to_string()]);
76        language_patterns.insert("java".to_string(), vec![".java".to_string()]);
77        language_patterns.insert("c".to_string(), vec![".c".to_string(), ".h".to_string()]);
78        language_patterns.insert(
79            "cpp".to_string(),
80            vec![".cpp".to_string(), ".hpp".to_string(), ".cc".to_string()],
81        );
82
83        Self {
84            name: "smart_code_review".to_string(),
85            description: "Autonomous code review tool that adapts its strategy based on code complexity and language".to_string(),
86            language_patterns,
87        }
88    }
89}
90
91impl SmartCodeReviewTool {
92    /// Create a new smart code review tool
93    pub fn new() -> Self {
94        Self::default()
95    }
96
97    /// Detect the programming language from file path or content
98    fn detect_language(&self, file_path: Option<&str>, content: &str) -> LanguageInfo {
99        // Try to detect from file extension first
100        if let Some(path) = file_path {
101            let path_lower = path.to_lowercase();
102            for (lang, extensions) in &self.language_patterns {
103                for ext in extensions {
104                    if path_lower.ends_with(ext) {
105                        return LanguageInfo {
106                            language: lang.clone(),
107                            confidence: 0.95,
108                            file_extension: ext.clone(),
109                        };
110                    }
111                }
112            }
113        }
114
115        // Fallback to content-based detection
116        self.detect_language_from_content(content)
117    }
118
119    /// Detect language from content heuristics
120    fn detect_language_from_content(&self, content: &str) -> LanguageInfo {
121        let content = content.trim();
122
123        // Rust indicators
124        if content.contains("fn ") && content.contains("use std::") {
125            return LanguageInfo {
126                language: "rust".to_string(),
127                confidence: 0.85,
128                file_extension: ".rs".to_string(),
129            };
130        }
131
132        // Python indicators
133        if content.contains("def ") && (content.contains(":") && content.contains("import ")) {
134            return LanguageInfo {
135                language: "python".to_string(),
136                confidence: 0.85,
137                file_extension: ".py".to_string(),
138            };
139        }
140
141        // JavaScript/TypeScript indicators
142        if content.contains("const ") || content.contains("let ") || content.contains("function ") {
143            if content.contains(": ") && content.contains("interface ") {
144                return LanguageInfo {
145                    language: "typescript".to_string(),
146                    confidence: 0.80,
147                    file_extension: ".ts".to_string(),
148                };
149            }
150            return LanguageInfo {
151                language: "javascript".to_string(),
152                confidence: 0.80,
153                file_extension: ".js".to_string(),
154            };
155        }
156
157        // Go indicators
158        if content.contains("package ") && content.contains("func ") {
159            return LanguageInfo {
160                language: "go".to_string(),
161                confidence: 0.85,
162                file_extension: ".go".to_string(),
163            };
164        }
165
166        // Default to unknown
167        LanguageInfo {
168            language: "unknown".to_string(),
169            confidence: 0.0,
170            file_extension: ".txt".to_string(),
171        }
172    }
173
174    /// Calculate complexity metrics for the code
175    fn calculate_complexity(&self, content: &str) -> ComplexityMetrics {
176        let lines: Vec<&str> = content.lines().collect();
177        let lines_of_code = lines.len();
178
179        // Count functions (rough approximation)
180        let function_keywords = ["fn ", "def ", "function ", "func "];
181        let function_count = lines
182            .iter()
183            .filter(|line| {
184                let trimmed = line.trim();
185                function_keywords.iter().any(|kw| trimmed.starts_with(kw))
186            })
187            .count();
188
189        // Estimate cyclomatic complexity by counting control flow keywords
190        let control_flow = [
191            "if ", "for ", "while ", "match ", "switch ", "?", "&&", "||",
192        ];
193        let cyclomatic_complexity = lines
194            .iter()
195            .map(|line| {
196                control_flow
197                    .iter()
198                    .map(|kw| line.matches(kw).count())
199                    .sum::<usize>()
200            })
201            .sum::<usize>()
202            + 1; // Base complexity is 1
203
204        // Find max function length (rough approximation)
205        let max_function_lines = if function_count > 0 {
206            lines_of_code / function_count.max(1)
207        } else {
208            lines_of_code
209        };
210
211        ComplexityMetrics {
212            lines_of_code,
213            cyclomatic_complexity,
214            function_count,
215            max_function_lines,
216        }
217    }
218
219    /// Determine review strategy based on complexity
220    fn determine_strategy(&self, metrics: &ComplexityMetrics) -> ReviewStrategy {
221        if metrics.lines_of_code < 50 && metrics.function_count <= 2 {
222            ReviewStrategy::Quick
223        } else if metrics.cyclomatic_complexity > 20 || metrics.lines_of_code > 500 {
224            ReviewStrategy::Deep
225        } else {
226            ReviewStrategy::Standard
227        }
228    }
229
230    /// Perform quick review
231    fn quick_review(&self, content: &str, lang: &LanguageInfo) -> Vec<String> {
232        let mut issues = Vec::new();
233
234        // Basic checks for all languages
235        if content.len() > 10000 {
236            issues.push("⚠️ File is quite long, consider splitting".to_string());
237        }
238
239        // Language-specific quick checks
240        match lang.language.as_str() {
241            "rust" if !content.contains("///") && !content.contains("//") => {
242                issues.push("⚠️ No documentation comments found".to_string());
243            }
244            "python" if !content.contains("\"\"\"") && !content.contains("#") => {
245                issues.push("⚠️ No docstrings or comments found".to_string());
246            }
247            _ => {}
248        }
249
250        if issues.is_empty() {
251            issues.push("✅ Quick review passed".to_string());
252        }
253
254        issues
255    }
256
257    /// Perform standard review
258    fn standard_review(&self, content: &str, lang: &LanguageInfo) -> Vec<String> {
259        let mut issues = self.quick_review(content, lang);
260
261        // Additional standard checks
262        let lines: Vec<&str> = content.lines().collect();
263
264        // Check for TODO/FIXME
265        let todo_count = lines
266            .iter()
267            .filter(|l| l.contains("TODO") || l.contains("FIXME"))
268            .count();
269        if todo_count > 0 {
270            issues.push(format!("📋 Found {} TODO/FIXME comments", todo_count));
271        }
272
273        // Check for long lines
274        let long_lines = lines
275            .iter()
276            .enumerate()
277            .filter(|(_, l)| l.len() > 120)
278            .count();
279        if long_lines > 0 {
280            issues.push(format!(
281                "📏 Found {} lines exceeding 120 characters",
282                long_lines
283            ));
284        }
285
286        // Language-specific standard checks
287        match lang.language.as_str() {
288            "rust" => {
289                if content.contains("unwrap()") {
290                    let unwrap_count = content.matches("unwrap()").count();
291                    issues.push(format!(
292                        "⚠️ Found {} uses of unwrap() - consider proper error handling",
293                        unwrap_count
294                    ));
295                }
296                if content.contains("panic!") {
297                    issues.push("⚠️ Found panic! macro - ensure these are justified".to_string());
298                }
299            }
300            "python" if content.contains("except:") && !content.contains("except ") => {
301                issues.push("⚠️ Found bare except: - use specific exceptions".to_string());
302            }
303            _ => {}
304        }
305
306        issues
307    }
308
309    /// Perform deep review
310    fn deep_review(&self, content: &str, lang: &LanguageInfo) -> Vec<String> {
311        let mut issues = self.standard_review(content, lang);
312
313        // Deep analysis checks
314        let lines: Vec<&str> = content.lines().collect();
315
316        // Check for code duplication (simple heuristic)
317        let mut line_counts: HashMap<String, usize> = HashMap::new();
318        for line in &lines {
319            let trimmed = line.trim().to_string();
320            if trimmed.len() > 20 {
321                *line_counts.entry(trimmed).or_insert(0) += 1;
322            }
323        }
324        let duplicates: Vec<_> = line_counts.iter().filter(|(_, c)| **c > 2).collect();
325        if !duplicates.is_empty() {
326            issues.push(format!(
327                "🔍 Found {} potentially duplicated code blocks",
328                duplicates.len()
329            ));
330        }
331
332        // Check for complex functions
333        let metrics = self.calculate_complexity(content);
334        if metrics.cyclomatic_complexity > 20 {
335            issues.push(format!(
336                "🚨 High cyclomatic complexity: {}. Consider refactoring into smaller functions",
337                metrics.cyclomatic_complexity
338            ));
339        }
340
341        // Check for security issues (basic patterns)
342        let security_issues = self.check_security_issues(content, lang);
343        issues.extend(security_issues);
344
345        issues
346    }
347
348    /// Check for basic security issues
349    fn check_security_issues(&self, content: &str, lang: &LanguageInfo) -> Vec<String> {
350        let mut issues = Vec::new();
351
352        match lang.language.as_str() {
353            "rust" if content.contains("unsafe ") => {
354                let unsafe_count = content.matches("unsafe ").count();
355                issues.push(format!(
356                    "🚨 Found {} unsafe blocks - ensure memory safety is maintained",
357                    unsafe_count
358                ));
359            }
360            "python" => {
361                if content.contains("eval(") {
362                    issues.push("🚨 Found eval() - potential security risk".to_string());
363                }
364                if content.contains("exec(") {
365                    issues.push("🚨 Found exec() - potential security risk".to_string());
366                }
367            }
368            "javascript" | "typescript" => {
369                if content.contains("eval(") {
370                    issues.push("🚨 Found eval() - potential security risk".to_string());
371                }
372                if content.contains("innerHTML") {
373                    issues.push("⚠️ Found innerHTML - potential XSS risk".to_string());
374                }
375            }
376            _ => {}
377        }
378
379        issues
380    }
381
382    /// Check if critical issues require user clarification
383    fn has_critical_issues(&self, issues: &[String]) -> bool {
384        issues.iter().any(|i| i.starts_with("🚨"))
385    }
386
387    /// Count issue severity
388    fn count_by_severity(&self, issues: &[String]) -> (usize, usize, usize) {
389        let critical = issues.iter().filter(|i| i.starts_with("🚨")).count();
390        let warning = issues.iter().filter(|i| i.starts_with("⚠️")).count();
391        let info = issues
392            .iter()
393            .filter(|i| {
394                i.starts_with("✅")
395                    || i.starts_with("📋")
396                    || i.starts_with("📏")
397                    || i.starts_with("🔍")
398            })
399            .count();
400        (critical, warning, info)
401    }
402}
403
404#[async_trait]
405impl AgenticTool for SmartCodeReviewTool {
406    fn name(&self) -> &str {
407        &self.name
408    }
409
410    fn description(&self) -> &str {
411        &self.description
412    }
413
414    async fn execute(
415        &self,
416        goal: ToolGoal,
417        context: &mut AgenticContext,
418    ) -> Result<AgenticToolResult, ToolError> {
419        // Extract parameters
420        let file_path = goal.params.get("file_path").and_then(|v| v.as_str());
421        let content = goal
422            .params
423            .get("content")
424            .and_then(|v| v.as_str())
425            .ok_or_else(|| {
426                ToolError::InvalidArguments("Missing 'content' parameter".to_string())
427            })?;
428
429        // Record the start of execution
430        context.record_interaction(
431            InteractionRole::System,
432            format!("Starting smart code review for goal: {}", goal.intent),
433        );
434
435        // Step 1: Detect language
436        let language = self.detect_language(file_path, content);
437        context.record_interaction_with_metadata(
438            InteractionRole::Assistant,
439            format!(
440                "Detected language: {} (confidence: {})",
441                language.language, language.confidence
442            ),
443            serde_json::to_value(&language).unwrap_or_default(),
444        );
445
446        // Step 2: Calculate complexity
447        let complexity = self.calculate_complexity(content);
448        context.record_interaction_with_metadata(
449            InteractionRole::Assistant,
450            format!(
451                "Calculated complexity: {} lines, {} functions, complexity score {}",
452                complexity.lines_of_code,
453                complexity.function_count,
454                complexity.cyclomatic_complexity
455            ),
456            serde_json::to_value(&complexity).unwrap_or_default(),
457        );
458
459        // Step 3: Determine strategy
460        let strategy = self.determine_strategy(&complexity);
461        let strategy_str = match strategy {
462            ReviewStrategy::Quick => "Quick",
463            ReviewStrategy::Standard => "Standard",
464            ReviewStrategy::Deep => "Deep",
465        };
466        context.record_interaction(
467            InteractionRole::Assistant,
468            format!("Selected review strategy: {}", strategy_str),
469        );
470
471        // Step 4: Perform review based on strategy
472        let issues = match strategy {
473            ReviewStrategy::Quick => self.quick_review(content, &language),
474            ReviewStrategy::Standard => self.standard_review(content, &language),
475            ReviewStrategy::Deep => self.deep_review(content, &language),
476        };
477
478        // Step 5: Check if we need clarification for critical issues
479        let (critical, warning, info) = self.count_by_severity(&issues);
480
481        if self.has_critical_issues(&issues) && context.is_first_iteration() {
482            // Store review results in state for potential continuation
483            let review_state = serde_json::json!({
484                "language": language,
485                "complexity": complexity,
486                "strategy": strategy_str,
487                "issues": issues,
488                "critical_count": critical,
489                "warning_count": warning,
490                "info_count": info,
491            });
492            context.update_state(review_state).await;
493
494            // Request clarification from user about critical issues
495            return Ok(AgenticToolResult::need_clarification_with_options(
496                format!(
497                    "Found {} critical issue(s) and {} warning(s) in the code. \
498                     The critical issues may require immediate attention. \
499                     How would you like to proceed?",
500                    critical, warning
501                ),
502                vec![
503                    "Fix critical issues automatically (if possible)".to_string(),
504                    "Show me detailed explanations of the issues".to_string(),
505                    "Continue with current code (I understand the risks)".to_string(),
506                    "Generate refactoring suggestions".to_string(),
507                ],
508            ));
509        }
510
511        // Step 6: Compile final result
512        let result = serde_json::json!({
513            "language": language,
514            "complexity": complexity,
515            "strategy": strategy_str,
516            "summary": {
517                "critical": critical,
518                "warning": warning,
519                "info": info,
520                "total": issues.len(),
521            },
522            "issues": issues,
523        });
524
525        // Store final state
526        context.update_state(result.clone()).await;
527
528        Ok(AgenticToolResult::success(
529            serde_json::to_string_pretty(&result).unwrap_or_default(),
530        ))
531    }
532}
533
534#[cfg(test)]
535mod tests {
536    use super::*;
537    use crate::tools::agentic::{AgenticContext, AgenticToolExecutor};
538    use crate::tools::ToolCall;
539    use std::sync::Arc;
540
541    struct MockExecutor;
542
543    #[async_trait]
544    impl AgenticToolExecutor for MockExecutor {
545        async fn execute(&self, _call: &ToolCall) -> Result<AgenticToolResult, ToolError> {
546            Ok(AgenticToolResult::success("mock"))
547        }
548    }
549
550    #[test]
551    fn test_language_detection_from_extension() {
552        let tool = SmartCodeReviewTool::new();
553
554        let lang = tool.detect_language(Some("test.rs"), "");
555        assert_eq!(lang.language, "rust");
556        assert_eq!(lang.confidence, 0.95);
557
558        let lang = tool.detect_language(Some("test.py"), "");
559        assert_eq!(lang.language, "python");
560    }
561
562    #[test]
563    fn test_language_detection_from_content() {
564        let tool = SmartCodeReviewTool::new();
565
566        let rust_code = r#"
567            use std::collections::HashMap;
568            fn main() {
569                println!("Hello");
570            }
571        "#;
572        let lang = tool.detect_language(None, rust_code);
573        assert_eq!(lang.language, "rust");
574
575        let python_code = r#"
576            import os
577            def main():
578                print("Hello")
579        "#;
580        let lang = tool.detect_language(None, python_code);
581        assert_eq!(lang.language, "python");
582    }
583
584    #[test]
585    fn test_complexity_calculation() {
586        let tool = SmartCodeReviewTool::new();
587
588        let code = r#"
589            fn main() {
590                if true {
591                    for i in 0..10 {
592                        while false {}
593                    }
594                }
595            }
596        "#;
597
598        let metrics = tool.calculate_complexity(code);
599        assert!(metrics.lines_of_code > 0);
600        assert!(metrics.cyclomatic_complexity >= 4); // if + for + while + base
601    }
602
603    #[test]
604    fn test_strategy_selection() {
605        let tool = SmartCodeReviewTool::new();
606
607        let simple = ComplexityMetrics {
608            lines_of_code: 30,
609            cyclomatic_complexity: 2,
610            function_count: 1,
611            max_function_lines: 30,
612        };
613        assert!(matches!(
614            tool.determine_strategy(&simple),
615            ReviewStrategy::Quick
616        ));
617
618        let complex = ComplexityMetrics {
619            lines_of_code: 600,
620            cyclomatic_complexity: 30,
621            function_count: 10,
622            max_function_lines: 60,
623        };
624        assert!(matches!(
625            tool.determine_strategy(&complex),
626            ReviewStrategy::Deep
627        ));
628    }
629
630    #[test]
631    fn test_security_issue_detection() {
632        let tool = SmartCodeReviewTool::new();
633
634        let rust_code = "unsafe { *ptr }";
635        let lang = LanguageInfo {
636            language: "rust".to_string(),
637            confidence: 1.0,
638            file_extension: ".rs".to_string(),
639        };
640        let issues = tool.check_security_issues(rust_code, &lang);
641        assert!(issues.iter().any(|i| i.contains("unsafe")));
642
643        let python_code = "eval(user_input)";
644        let lang = LanguageInfo {
645            language: "python".to_string(),
646            confidence: 1.0,
647            file_extension: ".py".to_string(),
648        };
649        let issues = tool.check_security_issues(python_code, &lang);
650        assert!(issues.iter().any(|i| i.contains("eval")));
651    }
652
653    #[tokio::test]
654    async fn test_smart_review_execution() {
655        let tool = SmartCodeReviewTool::new();
656        let executor: Arc<dyn AgenticToolExecutor> = Arc::new(MockExecutor);
657        let mut context = AgenticContext::new(executor);
658
659        let goal = ToolGoal::new(
660            "Review this Rust code",
661            serde_json::json!({
662                "file_path": "test.rs",
663                "content": r#"
664                    fn main() {
665                        println!("Hello");
666                    }
667                "#
668            }),
669        );
670
671        let result = tool.execute(goal, &mut context).await;
672        assert!(result.is_ok());
673
674        let agentic_result = result.unwrap();
675        assert!(agentic_result.is_success());
676
677        // Check that interactions were recorded
678        assert!(!context.interaction_history.is_empty());
679    }
680
681    #[tokio::test]
682    async fn test_critical_issues_trigger_clarification() {
683        let tool = SmartCodeReviewTool::new();
684        let executor: Arc<dyn AgenticToolExecutor> = Arc::new(MockExecutor);
685        let mut context = AgenticContext::new(executor);
686
687        // Create a larger code block with complex control flow to trigger Deep strategy
688        let goal = ToolGoal::new(
689            "Review this code with security issues",
690            serde_json::json!({
691                "file_path": "test.rs",
692                "content": r#"
693                    // Line 1
694                    // Line 2
695                    // Line 3
696                    // Line 4
697                    // Line 5
698                    unsafe fn dangerous() {
699                        let ptr: *const i32 = std::ptr::null();
700                        unsafe { *ptr }
701                    }
702
703                    fn complex_function(x: i32) -> i32 {
704                        if x > 0 {
705                            if x < 10 {
706                                for i in 0..x {
707                                    while i < x {
708                                        if i == 5 {
709                                            return i;
710                                        }
711                                    }
712                                }
713                            }
714                        }
715                        x
716                    }
717
718                    fn another_complex(y: i32) -> i32 {
719                        match y {
720                            1 => 1,
721                            2 => 2,
722                            3 => 3,
723                            4 => 4,
724                            5 => 5,
725                            _ => 0,
726                        }
727                    }
728                "#
729            }),
730        );
731
732        let result = tool.execute(goal, &mut context).await;
733        assert!(result.is_ok());
734
735        let agentic_result = result.unwrap();
736        // Should request clarification due to unsafe block (when using Deep strategy)
737        assert!(
738            agentic_result.needs_clarification() || agentic_result.is_success(),
739            "Expected either clarification request (Deep strategy) or success (Standard strategy)"
740        );
741    }
742}