Skip to main content

st/mcp/
assistant.rs

1//! Intelligent MCP Assistant - The best helper of all time!
2//!
3//! This module makes Smart Tree MCP incredibly helpful by:
4//! - Suggesting the next best tools based on context
5//! - Learning from usage patterns
6//! - Providing helpful hints and tips
7//! - Anticipating needs before they're expressed
8
9// use anyhow::Result; // TODO: Use when needed
10use serde::{Deserialize, Serialize};
11use serde_json::{json, Value};
12use std::collections::{HashMap, VecDeque};
13use std::sync::Arc;
14use tokio::sync::RwLock;
15
16/// Tool recommendation based on context
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ToolRecommendation {
19    /// The tool name
20    pub tool: String,
21    /// Why this tool is recommended
22    pub reason: String,
23    /// Confidence score (0.0 to 1.0)
24    pub confidence: f32,
25    /// Example usage
26    pub example: String,
27    /// Related tools that work well together
28    pub companions: Vec<String>,
29}
30
31/// Pattern of tool usage that we've learned
32#[derive(Debug, Clone)]
33pub struct UsagePattern {
34    /// Sequence of tools commonly used together
35    pub sequence: Vec<String>,
36    /// How often this pattern occurs
37    pub frequency: u32,
38    /// Context where this pattern is useful
39    pub context: String,
40}
41
42/// The intelligent assistant that helps users
43pub struct McpAssistant {
44    /// History of recent tool calls
45    call_history: Arc<RwLock<VecDeque<String>>>,
46    /// Learned patterns of tool usage
47    patterns: Arc<RwLock<Vec<UsagePattern>>>,
48    /// Current project context
49    project_context: Arc<RwLock<ProjectContext>>,
50    /// Tool usage statistics
51    usage_stats: Arc<RwLock<HashMap<String, u32>>>,
52}
53
54/// Project context for smarter recommendations
55#[derive(Debug, Clone)]
56pub struct ProjectContext {
57    pub language: ProjectLanguage,
58    pub size: ProjectSize,
59    pub has_tests: bool,
60    pub has_git: bool,
61    pub has_ci: bool,
62    pub file_count: usize,
63    pub recent_changes: Vec<String>,
64}
65
66#[derive(Debug, Clone, PartialEq)]
67pub enum ProjectLanguage {
68    Rust,
69    Python,
70    JavaScript,
71    TypeScript,
72    Mixed,
73    Unknown,
74}
75
76#[derive(Debug, Clone, PartialEq)]
77pub enum ProjectSize {
78    Tiny,   // < 10 files
79    Small,  // 10-50 files
80    Medium, // 50-200 files
81    Large,  // 200-1000 files
82    Huge,   // 1000+ files
83}
84
85impl Default for McpAssistant {
86    fn default() -> Self {
87        Self::new()
88    }
89}
90
91impl McpAssistant {
92    /// Create a new assistant
93    pub fn new() -> Self {
94        Self {
95            call_history: Arc::new(RwLock::new(VecDeque::with_capacity(20))),
96            patterns: Arc::new(RwLock::new(Self::load_default_patterns())),
97            project_context: Arc::new(RwLock::new(ProjectContext::default())),
98            usage_stats: Arc::new(RwLock::new(HashMap::new())),
99        }
100    }
101
102    /// Load default patterns that we know work well
103    fn load_default_patterns() -> Vec<UsagePattern> {
104        vec![
105            UsagePattern {
106                sequence: vec!["overview".into(), "find".into(), "search".into()],
107                frequency: 100,
108                context: "Initial project exploration".into(),
109            },
110            UsagePattern {
111                sequence: vec!["find_code_files".into(), "analyze".into(), "edit".into()],
112                frequency: 80,
113                context: "Code modification workflow".into(),
114            },
115            UsagePattern {
116                sequence: vec!["history".into(), "compare".into(), "analyze".into()],
117                frequency: 60,
118                context: "Understanding recent changes".into(),
119            },
120            UsagePattern {
121                sequence: vec!["find_tests".into(), "analyze".into(), "edit".into()],
122                frequency: 70,
123                context: "Test development workflow".into(),
124            },
125            UsagePattern {
126                sequence: vec![
127                    "find_config_files".into(),
128                    "edit".into(),
129                    "verify_permissions".into(),
130                ],
131                frequency: 50,
132                context: "Configuration management".into(),
133            },
134        ]
135    }
136
137    /// Record a tool call and update statistics
138    pub async fn record_call(&self, tool_name: &str) {
139        let mut history = self.call_history.write().await;
140        history.push_back(tool_name.to_string());
141        if history.len() > 20 {
142            history.pop_front();
143        }
144
145        let mut stats = self.usage_stats.write().await;
146        *stats.entry(tool_name.to_string()).or_insert(0) += 1;
147    }
148
149    /// Get intelligent recommendations for next tools
150    pub async fn get_recommendations(&self, last_tool: &str) -> Vec<ToolRecommendation> {
151        let mut recommendations = Vec::new();
152
153        let history = self.call_history.read().await;
154        let patterns = self.patterns.read().await;
155        let context = self.project_context.read().await;
156
157        // Check if we're in a known pattern
158        for pattern in patterns.iter() {
159            if let Some(pos) = pattern.sequence.iter().position(|t| t == last_tool) {
160                if pos < pattern.sequence.len() - 1 {
161                    // Suggest the next tool in the pattern
162                    let next_tool = &pattern.sequence[pos + 1];
163                    recommendations.push(ToolRecommendation {
164                        tool: next_tool.clone(),
165                        reason: format!("Part of {} workflow", pattern.context),
166                        confidence: pattern.frequency as f32 / 100.0,
167                        example: self.get_example_for_tool(next_tool),
168                        companions: pattern.sequence.clone(),
169                    });
170                }
171            }
172        }
173
174        // Context-based recommendations
175        match last_tool {
176            "overview" | "quick_tree" => {
177                recommendations.push(ToolRecommendation {
178                    tool: "find".into(),
179                    reason: "Natural next step: find specific files after overview".into(),
180                    confidence: 0.9,
181                    example: r#"{"type": "code", "languages": ["rust"]}"#.into(),
182                    companions: vec!["search".into(), "analyze".into()],
183                });
184
185                if context.has_tests {
186                    recommendations.push(ToolRecommendation {
187                        tool: "find_tests".into(),
188                        reason: "Project has tests - explore test structure".into(),
189                        confidence: 0.7,
190                        example: r#"{"path": "."}"#.into(),
191                        companions: vec!["analyze".into()],
192                    });
193                }
194            }
195
196            "find" | "find_files" => {
197                recommendations.push(ToolRecommendation {
198                    tool: "search".into(),
199                    reason: "Search within the files you found".into(),
200                    confidence: 0.85,
201                    example: r#"{"keyword": "TODO", "include_content": true}"#.into(),
202                    companions: vec!["analyze".into(), "edit".into()],
203                });
204
205                recommendations.push(ToolRecommendation {
206                    tool: "analyze".into(),
207                    reason: "Analyze the structure of found files".into(),
208                    confidence: 0.8,
209                    example: r#"{"mode": "semantic"}"#.into(),
210                    companions: vec!["edit".into()],
211                });
212            }
213
214            "search" => {
215                recommendations.push(ToolRecommendation {
216                    tool: "edit".into(),
217                    reason: "Edit files containing search results".into(),
218                    confidence: 0.75,
219                    example: r#"{"operation": "smart_edit", "file_path": "src/main.rs"}"#.into(),
220                    companions: vec!["history".into()],
221                });
222
223                recommendations.push(ToolRecommendation {
224                    tool: "context".into(),
225                    reason: "Get broader context around search results".into(),
226                    confidence: 0.7,
227                    example: r#"{"operation": "gather_project"}"#.into(),
228                    companions: vec!["memory".into()],
229                });
230            }
231
232            "edit" => {
233                recommendations.push(ToolRecommendation {
234                    tool: "history".into(),
235                    reason: "Track changes you've made".into(),
236                    confidence: 0.9,
237                    example: r#"{"operation": "get_file", "file_path": "src/main.rs"}"#.into(),
238                    companions: vec!["compare".into()],
239                });
240
241                recommendations.push(ToolRecommendation {
242                    tool: "verify_permissions".into(),
243                    reason: "Verify file permissions after edit".into(),
244                    confidence: 0.6,
245                    example: r#"{"path": "."}"#.into(),
246                    companions: vec!["analyze".into()],
247                });
248            }
249
250            "analyze" => {
251                if context.size == ProjectSize::Large || context.size == ProjectSize::Huge {
252                    recommendations.push(ToolRecommendation {
253                        tool: "analyze".into(),
254                        reason: "Try quantum compression for this large project".into(),
255                        confidence: 0.85,
256                        example: r#"{"mode": "quantum_semantic"}"#.into(),
257                        companions: vec!["memory".into()],
258                    });
259                }
260
261                recommendations.push(ToolRecommendation {
262                    tool: "memory".into(),
263                    reason: "Save important insights from analysis".into(),
264                    confidence: 0.7,
265                    example: r#"{"operation": "anchor", "keywords": ["architecture"]}"#.into(),
266                    companions: vec!["context".into()],
267                });
268            }
269
270            _ => {
271                // Generic helpful recommendations
272                if context.has_git && !history.contains(&"history".to_string()) {
273                    recommendations.push(ToolRecommendation {
274                        tool: "history".into(),
275                        reason: "Explore git history for context".into(),
276                        confidence: 0.6,
277                        example: r#"{"operation": "get_project"}"#.into(),
278                        companions: vec!["compare".into()],
279                    });
280                }
281            }
282        }
283
284        // Sort by confidence
285        recommendations.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap());
286        recommendations.truncate(3); // Top 3 recommendations
287
288        recommendations
289    }
290
291    /// Get example usage for a tool
292    fn get_example_for_tool(&self, tool: &str) -> String {
293        match tool {
294            "overview" => r#"{"mode": "project", "path": "."}"#,
295            "find" => r#"{"type": "code", "languages": ["rust", "python"]}"#,
296            "search" => r#"{"keyword": "TODO|FIXME", "include_content": true}"#,
297            "analyze" => r#"{"mode": "semantic", "path": "."}"#,
298            "edit" => r#"{"operation": "smart_edit", "file_path": "src/lib.rs"}"#,
299            "history" => r#"{"operation": "get_project", "project_path": "."}"#,
300            "context" => r#"{"operation": "gather_project"}"#,
301            "memory" => r#"{"operation": "find", "keywords": ["performance"]}"#,
302            _ => "{}",
303        }
304        .to_string()
305    }
306
307    /// Update project context from analysis
308    pub async fn update_context(&self, analysis: Value) {
309        let mut context = self.project_context.write().await;
310
311        // Extract information from analysis
312        if let Some(files) = analysis.get("file_count").and_then(|v| v.as_u64()) {
313            context.file_count = files as usize;
314            context.size = match files {
315                0..=10 => ProjectSize::Tiny,
316                11..=50 => ProjectSize::Small,
317                51..=200 => ProjectSize::Medium,
318                201..=1000 => ProjectSize::Large,
319                _ => ProjectSize::Huge,
320            };
321        }
322
323        if let Some(lang) = analysis.get("primary_language").and_then(|v| v.as_str()) {
324            context.language = match lang {
325                "rust" => ProjectLanguage::Rust,
326                "python" => ProjectLanguage::Python,
327                "javascript" => ProjectLanguage::JavaScript,
328                "typescript" => ProjectLanguage::TypeScript,
329                _ => ProjectLanguage::Unknown,
330            };
331        }
332
333        context.has_git = analysis
334            .get("has_git")
335            .and_then(|v| v.as_bool())
336            .unwrap_or(false);
337        context.has_tests = analysis
338            .get("has_tests")
339            .and_then(|v| v.as_bool())
340            .unwrap_or(false);
341    }
342
343    /// Get helpful tips based on current context
344    pub async fn get_helpful_tips(&self) -> Vec<String> {
345        let mut tips = Vec::new();
346        let context = self.project_context.read().await;
347        let stats = self.usage_stats.read().await;
348
349        // Size-based tips
350        match context.size {
351            ProjectSize::Huge => {
352                tips.push("💡 Large project detected! Use 'quantum-semantic' mode for maximum compression".into());
353                tips.push("🚀 Try streaming mode with --stream flag for faster results".into());
354            }
355            ProjectSize::Large => {
356                tips.push("📊 Consider using 'quantum' mode for better token efficiency".into());
357            }
358            _ => {}
359        }
360
361        // Language-specific tips
362        match context.language {
363            ProjectLanguage::Rust => {
364                tips.push("🦀 Rust project! Use 'find_tests' to locate test modules".into());
365                tips.push(
366                    "📦 Try 'analyze' with mode 'relations' to see module dependencies".into(),
367                );
368            }
369            ProjectLanguage::Python => {
370                tips.push(
371                    "🐍 Python project! Use 'find_config_files' to locate setup.py/pyproject.toml"
372                        .into(),
373                );
374            }
375            _ => {}
376        }
377
378        // Usage-based tips
379        if !stats.contains_key("memory") {
380            tips.push(
381                "💭 Haven't used memory yet? Try 'memory' tool to save important insights!".into(),
382            );
383        }
384
385        if !stats.contains_key("compare") && context.has_git {
386            tips.push(
387                "🔄 Git repo detected! Use 'compare' to see differences between directories".into(),
388            );
389        }
390
391        tips
392    }
393
394    /// Generate a friendly, helpful response
395    pub async fn enhance_response(&self, tool: &str, response: Value) -> Value {
396        let recommendations = self.get_recommendations(tool).await;
397        let tips = self.get_helpful_tips().await;
398
399        let mut enhanced = response;
400
401        // Add recommendations if we have them
402        if !recommendations.is_empty() {
403            let recs: Vec<Value> = recommendations
404                .iter()
405                .map(|r| {
406                    json!({
407                        "tool": r.tool,
408                        "reason": r.reason,
409                        "confidence": r.confidence,
410                        "example": r.example,
411                    })
412                })
413                .collect();
414
415            enhanced["_suggestions"] = json!({
416                "next_tools": recs,
417                "message": format!(
418                    "🎯 Based on '{}', you might want to try '{}' next!",
419                    tool,
420                    recommendations[0].tool
421                ),
422            });
423        }
424
425        // Add helpful tips
426        if !tips.is_empty() {
427            enhanced["_tips"] = json!(tips);
428        }
429
430        // Add friendly message
431        enhanced["_assistant"] = json!({
432            "message": self.get_friendly_message(tool).await,
433            "confidence": "I'm learning from your usage patterns to provide better suggestions!",
434        });
435
436        enhanced
437    }
438
439    /// Get a friendly message based on the tool
440    async fn get_friendly_message(&self, tool: &str) -> String {
441        let history = self.call_history.read().await;
442        let call_count = history.len();
443
444        match tool {
445            "overview" if call_count == 0 => {
446                "🌟 Great start! I've analyzed your project structure. What would you like to explore next?".into()
447            },
448            "find" => {
449                "🔍 Found what you're looking for? I can help you search within these files or analyze their structure!".into()
450            },
451            "search" => {
452                "🎯 Search complete! Would you like to edit these files or get more context around the results?".into()
453            },
454            "edit" => {
455                "✏️ Edit successful! I'm tracking your changes. Want to see the history or make more edits?".into()
456            },
457            "analyze" => {
458                "📊 Analysis complete! This data is now cached for faster access. Consider saving important insights with the memory tool!".into()
459            },
460            _ => {
461                "✨ Operation complete! Check out my suggestions for what to do next!".into()
462            }
463        }
464    }
465
466    /// Learn from a successful sequence of operations
467    pub async fn learn_pattern(&self, sequence: Vec<String>, context: String) {
468        let mut patterns = self.patterns.write().await;
469
470        // Check if this pattern already exists
471        for pattern in patterns.iter_mut() {
472            if pattern.sequence == sequence {
473                pattern.frequency += 1;
474                return;
475            }
476        }
477
478        // Add new pattern
479        patterns.push(UsagePattern {
480            sequence,
481            frequency: 1,
482            context,
483        });
484    }
485}
486
487impl Default for ProjectContext {
488    fn default() -> Self {
489        Self {
490            language: ProjectLanguage::Unknown,
491            size: ProjectSize::Small,
492            has_tests: false,
493            has_git: false,
494            has_ci: false,
495            file_count: 0,
496            recent_changes: Vec::new(),
497        }
498    }
499}
500
501/// Helper function to make tool responses more helpful
502pub fn make_helpful(tool_name: &str, basic_response: Value) -> Value {
503    let mut response = basic_response;
504
505    // Add quick tips based on tool
506    let quick_tip = match tool_name {
507        "overview" => "Try 'find' next to locate specific file types!",
508        "find" => "Use 'search' to look for patterns within these files!",
509        "search" => "Found something interesting? Use 'edit' to modify it!",
510        "edit" => "Check 'history' to track your changes!",
511        "analyze" => "Save insights with 'memory' for future reference!",
512        _ => "I'm here to help! Check suggestions for next steps!",
513    };
514
515    response["_quick_tip"] = json!(quick_tip);
516
517    response
518}
519
520#[cfg(test)]
521mod tests {
522    use super::*;
523
524    #[tokio::test]
525    async fn test_recommendations() {
526        let assistant = McpAssistant::new();
527
528        // Record some calls
529        assistant.record_call("overview").await;
530
531        // Get recommendations
532        let recs = assistant.get_recommendations("overview").await;
533
534        assert!(!recs.is_empty());
535        assert_eq!(recs[0].tool, "find");
536        assert!(recs[0].confidence > 0.5);
537    }
538
539    #[tokio::test]
540    async fn test_pattern_learning() {
541        let assistant = McpAssistant::new();
542
543        // Learn a new pattern
544        assistant
545            .learn_pattern(
546                vec!["custom1".into(), "custom2".into()],
547                "Custom workflow".into(),
548            )
549            .await;
550
551        // Should recommend custom2 after custom1
552        assistant.record_call("custom1").await;
553        let recs = assistant.get_recommendations("custom1").await;
554
555        assert!(recs.iter().any(|r| r.tool == "custom2"));
556    }
557}