Skip to main content

do_memory_mcp/mcp/tools/
pattern_search.rs

1//! Pattern search MCP tool implementation
2
3use do_memory_core::memory::{PatternSearchResult, SearchConfig};
4use do_memory_core::{SelfLearningMemory, TaskContext};
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8/// Input parameters for search_patterns tool
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct SearchPatternsInput {
11    /// Natural language query describing what pattern to search for
12    pub query: String,
13    /// Domain to search in (e.g., "web-api", "cli")
14    pub domain: String,
15    /// Optional tags for filtering
16    #[serde(default)]
17    pub tags: Vec<String>,
18    /// Maximum number of results (default: 5)
19    #[serde(default = "default_limit")]
20    pub limit: usize,
21    /// Minimum relevance score (0.0 to 1.0, default: 0.3)
22    #[serde(default = "default_min_relevance")]
23    pub min_relevance: f32,
24    /// Whether to filter by domain (default: false)
25    #[serde(default)]
26    pub filter_by_domain: bool,
27}
28
29fn default_limit() -> usize {
30    5
31}
32
33fn default_min_relevance() -> f32 {
34    0.3
35}
36
37/// Output from search_patterns tool
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct SearchPatternsOutput {
40    /// Found patterns with relevance scores
41    pub results: Vec<PatternResult>,
42    /// Total patterns searched
43    pub total_searched: usize,
44    /// Query that was executed
45    pub query: String,
46}
47
48/// Individual pattern result
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct PatternResult {
51    /// Pattern ID
52    pub id: String,
53    /// Pattern type
54    pub pattern_type: String,
55    /// Pattern description/summary
56    pub description: String,
57    /// Relevance score (0.0 to 1.0)
58    pub relevance_score: f32,
59    /// Score breakdown
60    pub score_breakdown: ScoreBreakdownResult,
61    /// Success rate of pattern
62    pub success_rate: f32,
63    /// Domain this pattern is from
64    pub domain: Option<String>,
65    /// Times this pattern has been applied
66    pub times_applied: usize,
67}
68
69/// Score breakdown for pattern result
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct ScoreBreakdownResult {
72    pub semantic_similarity: f32,
73    pub context_match: f32,
74    pub effectiveness: f32,
75    pub recency: f32,
76    pub success_rate: f32,
77}
78
79/// Execute search_patterns tool
80pub async fn execute(
81    memory: &SelfLearningMemory,
82    input: SearchPatternsInput,
83) -> anyhow::Result<Value> {
84    // Build context
85    let context = TaskContext {
86        domain: input.domain.clone(),
87        language: None,
88        framework: None,
89        complexity: do_memory_core::ComplexityLevel::Moderate,
90        tags: input.tags.clone(),
91    };
92
93    // Build config
94    let config = SearchConfig {
95        min_relevance: input.min_relevance,
96        filter_by_domain: input.filter_by_domain,
97        ..SearchConfig::default()
98    };
99
100    // Execute search
101    let results = memory
102        .search_patterns_with_config(&input.query, context, config, input.limit)
103        .await?;
104
105    // Convert results
106    let pattern_results: Vec<PatternResult> = results
107        .iter()
108        .map(pattern_search_result_to_pattern_result)
109        .collect();
110
111    let output = SearchPatternsOutput {
112        results: pattern_results,
113        total_searched: results.len(),
114        query: input.query.clone(),
115    };
116
117    Ok(serde_json::to_value(output)?)
118}
119
120/// Convert PatternSearchResult to PatternResult
121fn pattern_search_result_to_pattern_result(result: &PatternSearchResult) -> PatternResult {
122    use do_memory_core::Pattern;
123
124    let pattern = &result.pattern;
125    let (id, pattern_type, description, domain) = match pattern {
126        Pattern::ToolSequence {
127            id, tools, context, ..
128        } => (
129            id.to_string(),
130            "tool_sequence".to_string(),
131            format!("Tool sequence: {}", tools.join(" → ")),
132            Some(context.domain.clone()),
133        ),
134        Pattern::DecisionPoint {
135            id,
136            condition,
137            action,
138            context,
139            ..
140        } => (
141            id.to_string(),
142            "decision_point".to_string(),
143            format!("Decision: {} → {}", condition, action),
144            Some(context.domain.clone()),
145        ),
146        Pattern::ErrorRecovery {
147            id,
148            error_type,
149            recovery_steps,
150            context,
151            ..
152        } => (
153            id.to_string(),
154            "error_recovery".to_string(),
155            format!(
156                "Error recovery for {}: {}",
157                error_type,
158                recovery_steps.join(", ")
159            ),
160            Some(context.domain.clone()),
161        ),
162        Pattern::ContextPattern {
163            id,
164            recommended_approach,
165            ..
166        } => (
167            id.to_string(),
168            "context_pattern".to_string(),
169            format!("Context pattern: {}", recommended_approach),
170            None,
171        ),
172    };
173
174    let effectiveness = pattern.effectiveness();
175
176    PatternResult {
177        id,
178        pattern_type,
179        description,
180        relevance_score: result.relevance_score,
181        score_breakdown: ScoreBreakdownResult {
182            semantic_similarity: result.score_breakdown.semantic_similarity,
183            context_match: result.score_breakdown.context_match,
184            effectiveness: result.score_breakdown.effectiveness,
185            recency: result.score_breakdown.recency,
186            success_rate: result.score_breakdown.success_rate,
187        },
188        success_rate: pattern.success_rate(),
189        domain,
190        times_applied: effectiveness.times_applied,
191    }
192}
193
194/// Input parameters for recommend_patterns tool
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct RecommendPatternsInput {
197    /// Description of the task you're working on
198    pub task_description: String,
199    /// Domain of the task
200    pub domain: String,
201    /// Optional tags for context
202    #[serde(default)]
203    pub tags: Vec<String>,
204    /// Maximum number of recommendations (default: 3)
205    #[serde(default = "default_recommendation_limit")]
206    pub limit: usize,
207}
208
209fn default_recommendation_limit() -> usize {
210    3
211}
212
213/// Execute recommend_patterns tool
214pub async fn execute_recommend(
215    memory: &SelfLearningMemory,
216    input: RecommendPatternsInput,
217) -> anyhow::Result<Value> {
218    // Build context
219    let context = TaskContext {
220        domain: input.domain.clone(),
221        language: None,
222        framework: None,
223        complexity: do_memory_core::ComplexityLevel::Moderate,
224        tags: input.tags.clone(),
225    };
226
227    // Execute recommendation
228    let results = memory
229        .recommend_patterns_for_task(&input.task_description, context, input.limit)
230        .await?;
231
232    // Convert results
233    let pattern_results: Vec<PatternResult> = results
234        .iter()
235        .map(pattern_search_result_to_pattern_result)
236        .collect();
237
238    let output = SearchPatternsOutput {
239        results: pattern_results,
240        total_searched: results.len(),
241        query: input.task_description.clone(),
242    };
243
244    Ok(serde_json::to_value(output)?)
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[test]
252    fn test_search_patterns_input_defaults() {
253        let input = SearchPatternsInput {
254            query: "test".to_string(),
255            domain: "test".to_string(),
256            tags: vec![],
257            limit: default_limit(),
258            min_relevance: default_min_relevance(),
259            filter_by_domain: false,
260        };
261
262        assert_eq!(input.limit, 5);
263        assert_eq!(input.min_relevance, 0.3);
264    }
265
266    #[test]
267    fn test_recommend_patterns_input_defaults() {
268        let input = RecommendPatternsInput {
269            task_description: "test".to_string(),
270            domain: "test".to_string(),
271            tags: vec![],
272            limit: default_recommendation_limit(),
273        };
274
275        assert_eq!(input.limit, 3);
276    }
277}