1use serde::{Deserialize, Serialize};
11use serde_json::{json, Value};
12use std::collections::{HashMap, VecDeque};
13use std::sync::Arc;
14use tokio::sync::RwLock;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ToolRecommendation {
19 pub tool: String,
21 pub reason: String,
23 pub confidence: f32,
25 pub example: String,
27 pub companions: Vec<String>,
29}
30
31#[derive(Debug, Clone)]
33pub struct UsagePattern {
34 pub sequence: Vec<String>,
36 pub frequency: u32,
38 pub context: String,
40}
41
42pub struct McpAssistant {
44 call_history: Arc<RwLock<VecDeque<String>>>,
46 patterns: Arc<RwLock<Vec<UsagePattern>>>,
48 project_context: Arc<RwLock<ProjectContext>>,
50 usage_stats: Arc<RwLock<HashMap<String, u32>>>,
52}
53
54#[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, Small, Medium, Large, Huge, }
84
85impl Default for McpAssistant {
86 fn default() -> Self {
87 Self::new()
88 }
89}
90
91impl McpAssistant {
92 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 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 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 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 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 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 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 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 recommendations.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap());
286 recommendations.truncate(3); recommendations
289 }
290
291 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 pub async fn update_context(&self, analysis: Value) {
309 let mut context = self.project_context.write().await;
310
311 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 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 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 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 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 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 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 if !tips.is_empty() {
427 enhanced["_tips"] = json!(tips);
428 }
429
430 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 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 pub async fn learn_pattern(&self, sequence: Vec<String>, context: String) {
468 let mut patterns = self.patterns.write().await;
469
470 for pattern in patterns.iter_mut() {
472 if pattern.sequence == sequence {
473 pattern.frequency += 1;
474 return;
475 }
476 }
477
478 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
501pub fn make_helpful(tool_name: &str, basic_response: Value) -> Value {
503 let mut response = basic_response;
504
505 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 assistant.record_call("overview").await;
530
531 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 assistant
545 .learn_pattern(
546 vec!["custom1".into(), "custom2".into()],
547 "Custom workflow".into(),
548 )
549 .await;
550
551 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}