helios_engine/
tools.rs

1use crate::error::{HeliosError, Result};
2use async_trait::async_trait;
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::collections::HashMap;
6use std::io::{BufReader, BufWriter, Read, Write};
7use std::path::Path;
8use std::time::{SystemTime, UNIX_EPOCH};
9use uuid::Uuid;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ToolParameter {
13    #[serde(rename = "type")]
14    pub param_type: String,
15    pub description: String,
16    #[serde(skip)]
17    pub required: Option<bool>,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ToolDefinition {
22    #[serde(rename = "type")]
23    pub tool_type: String,
24    pub function: FunctionDefinition,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct FunctionDefinition {
29    pub name: String,
30    pub description: String,
31    pub parameters: ParametersSchema,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ParametersSchema {
36    #[serde(rename = "type")]
37    pub schema_type: String,
38    pub properties: HashMap<String, ToolParameter>,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub required: Option<Vec<String>>,
41}
42
43#[derive(Debug, Clone)]
44pub struct ToolResult {
45    pub success: bool,
46    pub output: String,
47}
48
49impl ToolResult {
50    pub fn success(output: impl Into<String>) -> Self {
51        Self {
52            success: true,
53            output: output.into(),
54        }
55    }
56
57    pub fn error(message: impl Into<String>) -> Self {
58        Self {
59            success: false,
60            output: message.into(),
61        }
62    }
63}
64
65#[async_trait]
66pub trait Tool: Send + Sync {
67    fn name(&self) -> &str;
68    fn description(&self) -> &str;
69    fn parameters(&self) -> HashMap<String, ToolParameter>;
70    async fn execute(&self, args: Value) -> Result<ToolResult>;
71
72    fn to_definition(&self) -> ToolDefinition {
73        let required: Vec<String> = self
74            .parameters()
75            .iter()
76            .filter(|(_, param)| param.required.unwrap_or(false))
77            .map(|(name, _)| name.clone())
78            .collect();
79
80        ToolDefinition {
81            tool_type: "function".to_string(),
82            function: FunctionDefinition {
83                name: self.name().to_string(),
84                description: self.description().to_string(),
85                parameters: ParametersSchema {
86                    schema_type: "object".to_string(),
87                    properties: self.parameters(),
88                    required: if required.is_empty() {
89                        None
90                    } else {
91                        Some(required)
92                    },
93                },
94            },
95        }
96    }
97}
98
99pub struct ToolRegistry {
100    tools: HashMap<String, Box<dyn Tool>>,
101}
102
103impl ToolRegistry {
104    pub fn new() -> Self {
105        Self {
106            tools: HashMap::new(),
107        }
108    }
109
110    pub fn register(&mut self, tool: Box<dyn Tool>) {
111        let name = tool.name().to_string();
112        self.tools.insert(name, tool);
113    }
114
115    pub fn get(&self, name: &str) -> Option<&dyn Tool> {
116        self.tools.get(name).map(|b| &**b)
117    }
118
119    pub async fn execute(&self, name: &str, args: Value) -> Result<ToolResult> {
120        let tool = self
121            .tools
122            .get(name)
123            .ok_or_else(|| HeliosError::ToolError(format!("Tool '{}' not found", name)))?;
124
125        tool.execute(args).await
126    }
127
128    pub fn get_definitions(&self) -> Vec<ToolDefinition> {
129        self.tools
130            .values()
131            .map(|tool| tool.to_definition())
132            .collect()
133    }
134
135    pub fn list_tools(&self) -> Vec<String> {
136        self.tools.keys().cloned().collect()
137    }
138}
139
140impl Default for ToolRegistry {
141    fn default() -> Self {
142        Self::new()
143    }
144}
145
146// Example built-in tools
147
148pub struct CalculatorTool;
149
150#[async_trait]
151impl Tool for CalculatorTool {
152    fn name(&self) -> &str {
153        "calculator"
154    }
155
156    fn description(&self) -> &str {
157        "Perform basic arithmetic operations. Supports +, -, *, / operations."
158    }
159
160    fn parameters(&self) -> HashMap<String, ToolParameter> {
161        let mut params = HashMap::new();
162        params.insert(
163            "expression".to_string(),
164            ToolParameter {
165                param_type: "string".to_string(),
166                description: "Mathematical expression to evaluate (e.g., '2 + 2')".to_string(),
167                required: Some(true),
168            },
169        );
170        params
171    }
172
173    async fn execute(&self, args: Value) -> Result<ToolResult> {
174        let expression = args
175            .get("expression")
176            .and_then(|v| v.as_str())
177            .ok_or_else(|| HeliosError::ToolError("Missing 'expression' parameter".to_string()))?;
178
179        // Simple expression evaluator
180        let result = evaluate_expression(expression)?;
181        Ok(ToolResult::success(result.to_string()))
182    }
183}
184
185fn evaluate_expression(expr: &str) -> Result<f64> {
186    let expr = expr.replace(" ", "");
187
188    // Simple parsing for basic operations
189    for op in &['*', '/', '+', '-'] {
190        if let Some(pos) = expr.rfind(*op) {
191            if pos == 0 {
192                continue; // Skip if operator is at the beginning (negative number)
193            }
194            let left = &expr[..pos];
195            let right = &expr[pos + 1..];
196
197            let left_val = evaluate_expression(left)?;
198            let right_val = evaluate_expression(right)?;
199
200            return Ok(match op {
201                '+' => left_val + right_val,
202                '-' => left_val - right_val,
203                '*' => left_val * right_val,
204                '/' => {
205                    if right_val == 0.0 {
206                        return Err(HeliosError::ToolError("Division by zero".to_string()));
207                    }
208                    left_val / right_val
209                }
210                _ => unreachable!(),
211            });
212        }
213    }
214
215    expr.parse::<f64>()
216        .map_err(|_| HeliosError::ToolError(format!("Invalid expression: {}", expr)))
217}
218
219pub struct EchoTool;
220
221#[async_trait]
222impl Tool for EchoTool {
223    fn name(&self) -> &str {
224        "echo"
225    }
226
227    fn description(&self) -> &str {
228        "Echo back the provided message."
229    }
230
231    fn parameters(&self) -> HashMap<String, ToolParameter> {
232        let mut params = HashMap::new();
233        params.insert(
234            "message".to_string(),
235            ToolParameter {
236                param_type: "string".to_string(),
237                description: "The message to echo back".to_string(),
238                required: Some(true),
239            },
240        );
241        params
242    }
243
244    async fn execute(&self, args: Value) -> Result<ToolResult> {
245        let message = args
246            .get("message")
247            .and_then(|v| v.as_str())
248            .ok_or_else(|| HeliosError::ToolError("Missing 'message' parameter".to_string()))?;
249
250        Ok(ToolResult::success(format!("Echo: {}", message)))
251    }
252}
253
254pub struct FileSearchTool;
255
256#[async_trait]
257impl Tool for FileSearchTool {
258    fn name(&self) -> &str {
259        "file_search"
260    }
261
262    fn description(&self) -> &str {
263        "Search for files by name pattern or search for content within files. Can search recursively in directories."
264    }
265
266    fn parameters(&self) -> HashMap<String, ToolParameter> {
267        let mut params = HashMap::new();
268        params.insert(
269            "path".to_string(),
270            ToolParameter {
271                param_type: "string".to_string(),
272                description: "The directory path to search in (default: current directory)".to_string(),
273                required: Some(false),
274            },
275        );
276        params.insert(
277            "pattern".to_string(),
278            ToolParameter {
279                param_type: "string".to_string(),
280                description: "File name pattern to search for (supports wildcards like *.rs)".to_string(),
281                required: Some(false),
282            },
283        );
284        params.insert(
285            "content".to_string(),
286            ToolParameter {
287                param_type: "string".to_string(),
288                description: "Text content to search for within files".to_string(),
289                required: Some(false),
290            },
291        );
292        params.insert(
293            "max_results".to_string(),
294            ToolParameter {
295                param_type: "number".to_string(),
296                description: "Maximum number of results to return (default: 50)".to_string(),
297                required: Some(false),
298            },
299        );
300        params
301    }
302
303    async fn execute(&self, args: Value) -> Result<ToolResult> {
304        use walkdir::WalkDir;
305
306        let base_path = args
307            .get("path")
308            .and_then(|v| v.as_str())
309            .unwrap_or(".");
310        
311        let pattern = args.get("pattern").and_then(|v| v.as_str());
312        let content_search = args.get("content").and_then(|v| v.as_str());
313        let max_results = args
314            .get("max_results")
315            .and_then(|v| v.as_u64())
316            .unwrap_or(50) as usize;
317
318        if pattern.is_none() && content_search.is_none() {
319            return Err(HeliosError::ToolError(
320                "Either 'pattern' or 'content' parameter is required".to_string(),
321            ));
322        }
323
324        let mut results = Vec::new();
325        
326        // Precompile filename pattern to avoid compiling per file
327        let compiled_re = if let Some(pat) = pattern {
328            let re_pattern = pat
329                .replace(".", r"\.")
330                .replace("*", ".*")
331                .replace("?", ".");
332            match regex::Regex::new(&format!("^{}$", re_pattern)) {
333                Ok(re) => Some(re),
334                Err(e) => {
335                    tracing::warn!(
336                        "Invalid glob pattern '{}' ({}). Falling back to substring matching.",
337                        pat,
338                        e
339                    );
340                    None
341                }
342            }
343        } else {
344            None
345        };
346        
347        for entry in WalkDir::new(base_path)
348            .max_depth(10)
349            .follow_links(false)
350            .into_iter()
351            .filter_map(|e| e.ok())
352        {
353            if results.len() >= max_results {
354                break;
355            }
356
357            let path = entry.path();
358            
359            // Skip hidden files and common ignore directories
360            if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
361                if file_name.starts_with('.') || 
362                   file_name == "target" || 
363                   file_name == "node_modules" ||
364                   file_name == "__pycache__" {
365                    continue;
366                }
367            }
368
369            // Pattern matching for file names
370            if let Some(pat) = pattern {
371                if path.is_file() {
372                    if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
373                        let is_match = if let Some(re) = &compiled_re {
374                            re.is_match(file_name)
375                        } else {
376                            file_name.contains(pat)
377                        };
378                        if is_match {
379                            results.push(format!("📄 {}", path.display()));
380                        }
381                    }
382                }
383            }
384
385            // Content search within files
386            if let Some(search_term) = content_search {
387                if path.is_file() {
388                    if let Ok(content) = std::fs::read_to_string(path) {
389                        if content.contains(search_term) {
390                            // Find line numbers where content appears
391                            let matching_lines: Vec<(usize, &str)> = content
392                                .lines()
393                                .enumerate()
394                                .filter(|(_, line)| line.contains(search_term))
395                                .take(3) // Show up to 3 matching lines per file
396                                .collect();
397                            
398                            if !matching_lines.is_empty() {
399                                results.push(format!("📄 {} (found in {} lines)", 
400                                    path.display(), matching_lines.len()));
401                                for (line_num, line) in matching_lines {
402                                    results.push(format!("  Line {}: {}", line_num + 1, line.trim()));
403                                }
404                            }
405                        }
406                    }
407                }
408            }
409        }
410
411        if results.is_empty() {
412            Ok(ToolResult::success("No files found matching the criteria.".to_string()))
413        } else {
414            let output = format!(
415                "Found {} result(s):\n\n{}",
416                results.len(),
417                results.join("\n")
418            );
419            Ok(ToolResult::success(output))
420        }
421    }
422}
423
424// (removed) glob_match helper – logic moved to precompiled regex in FileSearchTool::execute
425
426pub struct FileReadTool;
427
428#[async_trait]
429impl Tool for FileReadTool {
430    fn name(&self) -> &str {
431        "file_read"
432    }
433
434    fn description(&self) -> &str {
435        "Read the contents of a file. Returns the full file content or specific lines."
436    }
437
438    fn parameters(&self) -> HashMap<String, ToolParameter> {
439        let mut params = HashMap::new();
440        params.insert(
441            "path".to_string(),
442            ToolParameter {
443                param_type: "string".to_string(),
444                description: "The file path to read".to_string(),
445                required: Some(true),
446            },
447        );
448        params.insert(
449            "start_line".to_string(),
450            ToolParameter {
451                param_type: "number".to_string(),
452                description: "Starting line number (1-indexed, optional)".to_string(),
453                required: Some(false),
454            },
455        );
456        params.insert(
457            "end_line".to_string(),
458            ToolParameter {
459                param_type: "number".to_string(),
460                description: "Ending line number (1-indexed, optional)".to_string(),
461                required: Some(false),
462            },
463        );
464        params
465    }
466
467    async fn execute(&self, args: Value) -> Result<ToolResult> {
468        let file_path = args
469            .get("path")
470            .and_then(|v| v.as_str())
471            .ok_or_else(|| HeliosError::ToolError("Missing 'path' parameter".to_string()))?;
472
473        let content = std::fs::read_to_string(file_path)
474            .map_err(|e| HeliosError::ToolError(format!("Failed to read file: {}", e)))?;
475
476        let start_line = args.get("start_line").and_then(|v| v.as_u64()).map(|n| n as usize);
477        let end_line = args.get("end_line").and_then(|v| v.as_u64()).map(|n| n as usize);
478
479        let output = if let (Some(start), Some(end)) = (start_line, end_line) {
480            let lines: Vec<&str> = content.lines().collect();
481            let start_idx = start.saturating_sub(1);
482            let end_idx = end.min(lines.len());
483            
484            if start_idx >= lines.len() {
485                return Err(HeliosError::ToolError(format!(
486                    "Start line {} is beyond file length ({})",
487                    start, lines.len()
488                )));
489            }
490            
491            let selected_lines = &lines[start_idx..end_idx];
492            format!(
493                "File: {} (lines {}-{}):\n\n{}",
494                file_path,
495                start,
496                end_idx,
497                selected_lines.join("\n")
498            )
499        } else {
500            format!("File: {}:\n\n{}", file_path, content)
501        };
502
503        Ok(ToolResult::success(output))
504    }
505}
506
507pub struct FileWriteTool;
508
509#[async_trait]
510impl Tool for FileWriteTool {
511    fn name(&self) -> &str {
512        "file_write"
513    }
514
515    fn description(&self) -> &str {
516        "Write content to a file. Creates new file or overwrites existing file."
517    }
518
519    fn parameters(&self) -> HashMap<String, ToolParameter> {
520        let mut params = HashMap::new();
521        params.insert(
522            "path".to_string(),
523            ToolParameter {
524                param_type: "string".to_string(),
525                description: "The file path to write to".to_string(),
526                required: Some(true),
527            },
528        );
529        params.insert(
530            "content".to_string(),
531            ToolParameter {
532                param_type: "string".to_string(),
533                description: "The content to write to the file".to_string(),
534                required: Some(true),
535            },
536        );
537        params
538    }
539
540    async fn execute(&self, args: Value) -> Result<ToolResult> {
541        let file_path = args
542            .get("path")
543            .and_then(|v| v.as_str())
544            .ok_or_else(|| HeliosError::ToolError("Missing 'path' parameter".to_string()))?;
545
546        let content = args
547            .get("content")
548            .and_then(|v| v.as_str())
549            .ok_or_else(|| HeliosError::ToolError("Missing 'content' parameter".to_string()))?;
550
551        // Create parent directories if they don't exist
552        if let Some(parent) = std::path::Path::new(file_path).parent() {
553            std::fs::create_dir_all(parent)
554                .map_err(|e| HeliosError::ToolError(format!("Failed to create directories: {}", e)))?;
555        }
556
557        std::fs::write(file_path, content)
558            .map_err(|e| HeliosError::ToolError(format!("Failed to write file: {}", e)))?;
559
560        Ok(ToolResult::success(format!(
561            "Successfully wrote {} bytes to {}",
562            content.len(),
563            file_path
564        )))
565    }
566}
567
568pub struct FileEditTool;
569
570#[async_trait]
571impl Tool for FileEditTool {
572    fn name(&self) -> &str {
573        "file_edit"
574    }
575
576    fn description(&self) -> &str {
577        "Edit a file by replacing specific text or lines. Use this to make targeted changes to existing files."
578    }
579
580    fn parameters(&self) -> HashMap<String, ToolParameter> {
581        let mut params = HashMap::new();
582        params.insert(
583            "path".to_string(),
584            ToolParameter {
585                param_type: "string".to_string(),
586                description: "The file path to edit".to_string(),
587                required: Some(true),
588            },
589        );
590        params.insert(
591            "find".to_string(),
592            ToolParameter {
593                param_type: "string".to_string(),
594                description: "The text to find and replace".to_string(),
595                required: Some(true),
596            },
597        );
598        params.insert(
599            "replace".to_string(),
600            ToolParameter {
601                param_type: "string".to_string(),
602                description: "The replacement text".to_string(),
603                required: Some(true),
604            },
605        );
606        params
607    }
608
609    async fn execute(&self, args: Value) -> Result<ToolResult> {
610        let file_path = args
611            .get("path")
612            .and_then(|v| v.as_str())
613            .ok_or_else(|| HeliosError::ToolError("Missing 'path' parameter".to_string()))?;
614
615        let find_text = args
616            .get("find")
617            .and_then(|v| v.as_str())
618            .ok_or_else(|| HeliosError::ToolError("Missing 'find' parameter".to_string()))?;
619
620        let replace_text = args
621            .get("replace")
622            .and_then(|v| v.as_str())
623            .ok_or_else(|| HeliosError::ToolError("Missing 'replace' parameter".to_string()))?;
624
625        if find_text.is_empty() {
626            return Err(HeliosError::ToolError("'find' parameter cannot be empty".to_string()));
627        }
628
629        let path = Path::new(file_path);
630        let parent = path.parent().ok_or_else(|| {
631            HeliosError::ToolError(format!("Invalid target path: {}", file_path))
632        })?;
633        let file_name = path.file_name().ok_or_else(|| {
634            HeliosError::ToolError(format!("Invalid target path: {}", file_path))
635        })?;
636
637        // Build a temp file path in the same directory for atomic rename
638        let pid = std::process::id();
639        let nanos = SystemTime::now()
640            .duration_since(UNIX_EPOCH)
641            .map_err(|e| HeliosError::ToolError(format!("Clock error: {}", e)))?
642            .as_nanos();
643        let tmp_name = format!("{}.tmp.{}.{}", file_name.to_string_lossy(), pid, nanos);
644        let tmp_path = parent.join(tmp_name);
645
646        // Open files
647        let input_file = std::fs::File::open(&path)
648            .map_err(|e| HeliosError::ToolError(format!("Failed to open file for read: {}", e)))?;
649        let mut reader = BufReader::new(input_file);
650
651        let tmp_file = std::fs::File::create(&tmp_path).map_err(|e| {
652            HeliosError::ToolError(format!("Failed to create temp file {}: {}", tmp_path.display(), e))
653        })?;
654        let mut writer = BufWriter::new(&tmp_file);
655
656        // Streamed find/replace to avoid loading entire file into memory
657        let replaced_count = replace_streaming(
658            &mut reader,
659            &mut writer,
660            find_text.as_bytes(),
661            replace_text.as_bytes(),
662        )
663        .map_err(|e| HeliosError::ToolError(format!("I/O error while replacing: {}", e)))?;
664
665        // Ensure all data is flushed and synced before rename
666        writer.flush().map_err(|e| HeliosError::ToolError(format!("Failed to flush temp file: {}", e)))?;
667        tmp_file.sync_all().map_err(|e| HeliosError::ToolError(format!("Failed to sync temp file: {}", e)))?;
668
669        // Preserve permissions
670        if let Ok(meta) = std::fs::metadata(&path) {
671            if let Err(e) = std::fs::set_permissions(&tmp_path, meta.permissions()) {
672                let _ = std::fs::remove_file(&tmp_path);
673                return Err(HeliosError::ToolError(format!("Failed to set permissions: {}", e)));
674            }
675        }
676
677        // Atomic replace
678        std::fs::rename(&tmp_path, &path).map_err(|e| {
679            let _ = std::fs::remove_file(&tmp_path);
680            HeliosError::ToolError(format!("Failed to replace original file: {}", e))
681        })?;
682
683        if replaced_count == 0 {
684            return Ok(ToolResult::error(format!(
685                "Text '{}' not found in file {}",
686                find_text, file_path
687            )));
688        }
689
690        Ok(ToolResult::success(format!(
691            "Successfully replaced {} occurrence(s) in {}",
692            replaced_count, file_path
693        )))
694    }
695}
696
697// Streamed replacement helpers
698fn replace_streaming<R: Read, W: Write>(reader: &mut R, writer: &mut W, needle: &[u8], replacement: &[u8]) -> std::io::Result<usize> {
699    let mut replaced = 0usize;
700    let mut carry: Vec<u8> = Vec::new();
701    let mut buf = [0u8; 8192];
702
703    let tail = if needle.len() > 1 { needle.len() - 1 } else { 0 };
704
705    loop {
706        let n = reader.read(&mut buf)?;
707        if n == 0 {
708            break;
709        }
710
711        let mut combined = Vec::with_capacity(carry.len() + n);
712        combined.extend_from_slice(&carry);
713        combined.extend_from_slice(&buf[..n]);
714
715        let process_len = combined.len().saturating_sub(tail);
716        let (to_process, new_carry) = combined.split_at(process_len);
717        replaced += write_with_replacements(writer, to_process, needle, replacement)?;
718        carry.clear();
719        carry.extend_from_slice(new_carry);
720    }
721
722    // Process remaining carry fully
723    replaced += write_with_replacements(writer, &carry, needle, replacement)?;
724    Ok(replaced)
725}
726
727fn write_with_replacements<W: Write>(writer: &mut W, haystack: &[u8], needle: &[u8], replacement: &[u8]) -> std::io::Result<usize> {
728    if needle.is_empty() {
729        writer.write_all(haystack)?;
730        return Ok(0);
731    }
732
733    let mut count = 0usize;
734    let mut i = 0usize;
735    while let Some(pos) = find_subslice(&haystack[i..], needle) {
736        let idx = i + pos;
737        writer.write_all(&haystack[i..idx])?;
738        writer.write_all(replacement)?;
739        count += 1;
740        i = idx + needle.len();
741    }
742    writer.write_all(&haystack[i..])?;
743    Ok(count)
744}
745
746fn find_subslice(h: &[u8], n: &[u8]) -> Option<usize> {
747    if n.is_empty() {
748        return Some(0);
749    }
750    h.windows(n.len()).position(|w| w == n)
751}
752
753/// RAG (Retrieval-Augmented Generation) Tool with Qdrant Vector Database
754/// 
755/// Provides document embedding, storage, retrieval, and reranking capabilities.
756/// Supports operations: add_document, search, delete, list, clear
757#[derive(Clone)]
758pub struct QdrantRAGTool {
759    qdrant_url: String,
760    collection_name: String,
761    embedding_api_url: String,
762    embedding_api_key: String,
763    client: reqwest::Client,
764}
765
766#[derive(Debug, Serialize, Deserialize)]
767struct QdrantPoint {
768    id: String,
769    vector: Vec<f32>,
770    payload: HashMap<String, serde_json::Value>,
771}
772
773#[derive(Debug, Serialize, Deserialize)]
774struct QdrantSearchRequest {
775    vector: Vec<f32>,
776    limit: usize,
777    with_payload: bool,
778    with_vector: bool,
779}
780
781#[derive(Debug, Serialize, Deserialize)]
782struct QdrantSearchResponse {
783    result: Vec<QdrantSearchResult>,
784}
785
786#[derive(Debug, Serialize, Deserialize)]
787struct QdrantSearchResult {
788    id: String,
789    score: f64,
790    payload: Option<HashMap<String, serde_json::Value>>,
791}
792
793#[derive(Debug, Serialize, Deserialize)]
794struct EmbeddingRequest {
795    input: String,
796    model: String,
797}
798
799#[derive(Debug, Serialize, Deserialize)]
800struct EmbeddingResponse {
801    data: Vec<EmbeddingData>,
802}
803
804#[derive(Debug, Serialize, Deserialize)]
805struct EmbeddingData {
806    embedding: Vec<f32>,
807}
808
809impl QdrantRAGTool {
810    /// Create a new RAG tool with Qdrant backend
811    pub fn new(
812        qdrant_url: impl Into<String>,
813        collection_name: impl Into<String>,
814        embedding_api_url: impl Into<String>,
815        embedding_api_key: impl Into<String>,
816    ) -> Self {
817        Self {
818            qdrant_url: qdrant_url.into(),
819            collection_name: collection_name.into(),
820            embedding_api_url: embedding_api_url.into(),
821            embedding_api_key: embedding_api_key.into(),
822            client: reqwest::Client::new(),
823        }
824    }
825
826    /// Generate embeddings using OpenAI-compatible API
827    async fn generate_embedding(&self, text: &str) -> Result<Vec<f32>> {
828        let request = EmbeddingRequest {
829            input: text.to_string(),
830            model: "text-embedding-ada-002".to_string(),
831        };
832
833        let response = self
834            .client
835            .post(&self.embedding_api_url)
836            .header("Authorization", format!("Bearer {}", self.embedding_api_key))
837            .json(&request)
838            .send()
839            .await
840            .map_err(|e| HeliosError::ToolError(format!("Embedding API error: {}", e)))?;
841
842        if !response.status().is_success() {
843            let error_text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
844            return Err(HeliosError::ToolError(format!("Embedding failed: {}", error_text)));
845        }
846
847        let embedding_response: EmbeddingResponse = response
848            .json()
849            .await
850            .map_err(|e| HeliosError::ToolError(format!("Failed to parse embedding response: {}", e)))?;
851
852        embedding_response
853            .data
854            .into_iter()
855            .next()
856            .map(|d| d.embedding)
857            .ok_or_else(|| HeliosError::ToolError("No embedding returned".to_string()))
858    }
859
860    /// Ensure collection exists in Qdrant
861    async fn ensure_collection(&self) -> Result<()> {
862        let collection_url = format!("{}/collections/{}", self.qdrant_url, self.collection_name);
863        
864        // Check if collection exists
865        let response = self.client.get(&collection_url).send().await;
866        
867        if response.is_ok() && response.unwrap().status().is_success() {
868            return Ok(()); // Collection exists
869        }
870
871        // Create collection with 1536 dimensions (OpenAI embedding size)
872        let create_payload = serde_json::json!({
873            "vectors": {
874                "size": 1536,
875                "distance": "Cosine"
876            }
877        });
878
879        let response = self
880            .client
881            .put(&collection_url)
882            .json(&create_payload)
883            .send()
884            .await
885            .map_err(|e| HeliosError::ToolError(format!("Failed to create collection: {}", e)))?;
886
887        if !response.status().is_success() {
888            let error_text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
889            return Err(HeliosError::ToolError(format!("Collection creation failed: {}", error_text)));
890        }
891
892        Ok(())
893    }
894
895    /// Add a document to Qdrant
896    async fn add_document(&self, text: &str, metadata: HashMap<String, serde_json::Value>) -> Result<String> {
897        self.ensure_collection().await?;
898
899        // Generate embedding
900        let embedding = self.generate_embedding(text).await?;
901
902        // Create point with metadata
903        let point_id = Uuid::new_v4().to_string();
904        let mut payload = metadata;
905        payload.insert("text".to_string(), serde_json::json!(text));
906        payload.insert("timestamp".to_string(), serde_json::json!(chrono::Utc::now().to_rfc3339()));
907
908        let point = QdrantPoint {
909            id: point_id.clone(),
910            vector: embedding,
911            payload,
912        };
913
914        // Upload point to Qdrant
915        let upsert_url = format!("{}/collections/{}/points", self.qdrant_url, self.collection_name);
916        let upsert_payload = serde_json::json!({
917            "points": [point]
918        });
919
920        let response = self
921            .client
922            .put(&upsert_url)
923            .json(&upsert_payload)
924            .send()
925            .await
926            .map_err(|e| HeliosError::ToolError(format!("Failed to upload document: {}", e)))?;
927
928        if !response.status().is_success() {
929            let error_text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
930            return Err(HeliosError::ToolError(format!("Document upload failed: {}", error_text)));
931        }
932
933        Ok(point_id)
934    }
935
936    /// Search for similar documents
937    async fn search(&self, query: &str, limit: usize) -> Result<Vec<(String, f64, String)>> {
938        // Generate query embedding
939        let query_embedding = self.generate_embedding(query).await?;
940
941        // Search in Qdrant
942        let search_url = format!("{}/collections/{}/points/search", self.qdrant_url, self.collection_name);
943        let search_request = QdrantSearchRequest {
944            vector: query_embedding,
945            limit,
946            with_payload: true,
947            with_vector: false,
948        };
949
950        let response = self
951            .client
952            .post(&search_url)
953            .json(&search_request)
954            .send()
955            .await
956            .map_err(|e| HeliosError::ToolError(format!("Search failed: {}", e)))?;
957
958        if !response.status().is_success() {
959            let error_text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
960            return Err(HeliosError::ToolError(format!("Search request failed: {}", error_text)));
961        }
962
963        let search_response: QdrantSearchResponse = response
964            .json()
965            .await
966            .map_err(|e| HeliosError::ToolError(format!("Failed to parse search response: {}", e)))?;
967
968        // Extract results
969        let results: Vec<(String, f64, String)> = search_response
970            .result
971            .into_iter()
972            .filter_map(|r| {
973                r.payload.and_then(|p| {
974                    p.get("text")
975                        .and_then(|t| t.as_str())
976                        .map(|text| (r.id, r.score, text.to_string()))
977                })
978            })
979            .collect();
980
981        Ok(results)
982    }
983
984    /// Delete a document by ID
985    async fn delete_document(&self, doc_id: &str) -> Result<()> {
986        let delete_url = format!("{}/collections/{}/points/delete", self.qdrant_url, self.collection_name);
987        let delete_payload = serde_json::json!({
988            "points": [doc_id]
989        });
990
991        let response = self
992            .client
993            .post(&delete_url)
994            .json(&delete_payload)
995            .send()
996            .await
997            .map_err(|e| HeliosError::ToolError(format!("Delete failed: {}", e)))?;
998
999        if !response.status().is_success() {
1000            let error_text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
1001            return Err(HeliosError::ToolError(format!("Delete request failed: {}", error_text)));
1002        }
1003
1004        Ok(())
1005    }
1006
1007    /// Clear all documents in the collection
1008    async fn clear_collection(&self) -> Result<()> {
1009        let delete_url = format!("{}/collections/{}", self.qdrant_url, self.collection_name);
1010        
1011        let response = self
1012            .client
1013            .delete(&delete_url)
1014            .send()
1015            .await
1016            .map_err(|e| HeliosError::ToolError(format!("Clear failed: {}", e)))?;
1017
1018        if !response.status().is_success() {
1019            let error_text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
1020            return Err(HeliosError::ToolError(format!("Clear collection failed: {}", error_text)));
1021        }
1022
1023        Ok(())
1024    }
1025}
1026
1027#[async_trait]
1028impl Tool for QdrantRAGTool {
1029    fn name(&self) -> &str {
1030        "rag_qdrant"
1031    }
1032
1033    fn description(&self) -> &str {
1034        "RAG (Retrieval-Augmented Generation) tool with vector database. Operations: add_document, search, delete, clear"
1035    }
1036
1037    fn parameters(&self) -> HashMap<String, ToolParameter> {
1038        let mut params = HashMap::new();
1039        params.insert(
1040            "operation".to_string(),
1041            ToolParameter {
1042                param_type: "string".to_string(),
1043                description: "Operation: 'add_document', 'search', 'delete', 'clear'".to_string(),
1044                required: Some(true),
1045            },
1046        );
1047        params.insert(
1048            "text".to_string(),
1049            ToolParameter {
1050                param_type: "string".to_string(),
1051                description: "Text content for add_document or search query".to_string(),
1052                required: Some(false),
1053            },
1054        );
1055        params.insert(
1056            "doc_id".to_string(),
1057            ToolParameter {
1058                param_type: "string".to_string(),
1059                description: "Document ID for delete operation".to_string(),
1060                required: Some(false),
1061            },
1062        );
1063        params.insert(
1064            "limit".to_string(),
1065            ToolParameter {
1066                param_type: "number".to_string(),
1067                description: "Number of results for search (default: 5)".to_string(),
1068                required: Some(false),
1069            },
1070        );
1071        params.insert(
1072            "metadata".to_string(),
1073            ToolParameter {
1074                param_type: "object".to_string(),
1075                description: "Additional metadata for the document (JSON object)".to_string(),
1076                required: Some(false),
1077            },
1078        );
1079        params
1080    }
1081
1082    async fn execute(&self, args: Value) -> Result<ToolResult> {
1083        let operation = args
1084            .get("operation")
1085            .and_then(|v| v.as_str())
1086            .ok_or_else(|| HeliosError::ToolError("Missing 'operation' parameter".to_string()))?;
1087
1088        match operation {
1089            "add_document" => {
1090                let text = args
1091                    .get("text")
1092                    .and_then(|v| v.as_str())
1093                    .ok_or_else(|| HeliosError::ToolError("Missing 'text' for add_document".to_string()))?;
1094
1095                let metadata: HashMap<String, serde_json::Value> = args
1096                    .get("metadata")
1097                    .and_then(|v| serde_json::from_value(v.clone()).ok())
1098                    .unwrap_or_default();
1099
1100                let doc_id = self.add_document(text, metadata).await?;
1101                Ok(ToolResult::success(format!(
1102                    "✓ Document added successfully\nID: {}\nText preview: {}",
1103                    doc_id,
1104                    &text[..text.len().min(100)]
1105                )))
1106            }
1107            "search" => {
1108                let query = args
1109                    .get("text")
1110                    .and_then(|v| v.as_str())
1111                    .ok_or_else(|| HeliosError::ToolError("Missing 'text' for search".to_string()))?;
1112
1113                let limit = args
1114                    .get("limit")
1115                    .and_then(|v| v.as_u64())
1116                    .unwrap_or(5) as usize;
1117
1118                let results = self.search(query, limit).await?;
1119
1120                if results.is_empty() {
1121                    Ok(ToolResult::success("No matching documents found".to_string()))
1122                } else {
1123                    let formatted_results: Vec<String> = results
1124                        .iter()
1125                        .enumerate()
1126                        .map(|(i, (id, score, text))| {
1127                            format!(
1128                                "{}. [Score: {:.4}] {}\n   ID: {}\n",
1129                                i + 1,
1130                                score,
1131                                &text[..text.len().min(150)],
1132                                id
1133                            )
1134                        })
1135                        .collect();
1136
1137                    Ok(ToolResult::success(format!(
1138                        "Found {} result(s):\n\n{}",
1139                        results.len(),
1140                        formatted_results.join("\n")
1141                    )))
1142                }
1143            }
1144            "delete" => {
1145                let doc_id = args
1146                    .get("doc_id")
1147                    .and_then(|v| v.as_str())
1148                    .ok_or_else(|| HeliosError::ToolError("Missing 'doc_id' for delete".to_string()))?;
1149
1150                self.delete_document(doc_id).await?;
1151                Ok(ToolResult::success(format!("✓ Document '{}' deleted", doc_id)))
1152            }
1153            "clear" => {
1154                self.clear_collection().await?;
1155                Ok(ToolResult::success("✓ All documents cleared from collection".to_string()))
1156            }
1157            _ => Err(HeliosError::ToolError(format!(
1158                "Unknown operation '{}'. Valid: add_document, search, delete, clear",
1159                operation
1160            ))),
1161        }
1162    }
1163}
1164
1165/// In-Memory Database Tool
1166/// 
1167/// Provides a simple key-value store for agents to cache data during conversations.
1168/// Supports set, get, delete, list keys, and clear operations.
1169pub struct MemoryDBTool {
1170    db: std::sync::Arc<std::sync::Mutex<HashMap<String, String>>>,
1171}
1172
1173impl MemoryDBTool {
1174    pub fn new() -> Self {
1175        Self {
1176            db: std::sync::Arc::new(std::sync::Mutex::new(HashMap::new())),
1177        }
1178    }
1179
1180    pub fn with_shared_db(db: std::sync::Arc<std::sync::Mutex<HashMap<String, String>>>) -> Self {
1181        Self { db }
1182    }
1183}
1184
1185impl Default for MemoryDBTool {
1186    fn default() -> Self {
1187        Self::new()
1188    }
1189}
1190
1191#[async_trait]
1192impl Tool for MemoryDBTool {
1193    fn name(&self) -> &str {
1194        "memory_db"
1195    }
1196
1197    fn description(&self) -> &str {
1198        "In-memory key-value database for caching data. Operations: set, get, delete, list, clear, exists"
1199    }
1200
1201    fn parameters(&self) -> HashMap<String, ToolParameter> {
1202        let mut params = HashMap::new();
1203        params.insert(
1204            "operation".to_string(),
1205            ToolParameter {
1206                param_type: "string".to_string(),
1207                description: "Operation to perform: 'set', 'get', 'delete', 'list', 'clear', 'exists'".to_string(),
1208                required: Some(true),
1209            },
1210        );
1211        params.insert(
1212            "key".to_string(),
1213            ToolParameter {
1214                param_type: "string".to_string(),
1215                description: "Key for set, get, delete, exists operations".to_string(),
1216                required: Some(false),
1217            },
1218        );
1219        params.insert(
1220            "value".to_string(),
1221            ToolParameter {
1222                param_type: "string".to_string(),
1223                description: "Value for set operation".to_string(),
1224                required: Some(false),
1225            },
1226        );
1227        params
1228    }
1229
1230    async fn execute(&self, args: Value) -> Result<ToolResult> {
1231        let operation = args
1232            .get("operation")
1233            .and_then(|v| v.as_str())
1234            .ok_or_else(|| HeliosError::ToolError("Missing 'operation' parameter".to_string()))?;
1235
1236        let mut db = self.db.lock().map_err(|e| {
1237            HeliosError::ToolError(format!("Failed to lock database: {}", e))
1238        })?;
1239
1240        match operation {
1241            "set" => {
1242                let key = args
1243                    .get("key")
1244                    .and_then(|v| v.as_str())
1245                    .ok_or_else(|| HeliosError::ToolError("Missing 'key' parameter for set operation".to_string()))?;
1246                let value = args
1247                    .get("value")
1248                    .and_then(|v| v.as_str())
1249                    .ok_or_else(|| HeliosError::ToolError("Missing 'value' parameter for set operation".to_string()))?;
1250
1251                db.insert(key.to_string(), value.to_string());
1252                Ok(ToolResult::success(format!("✓ Set '{}' = '{}'", key, value)))
1253            }
1254            "get" => {
1255                let key = args
1256                    .get("key")
1257                    .and_then(|v| v.as_str())
1258                    .ok_or_else(|| HeliosError::ToolError("Missing 'key' parameter for get operation".to_string()))?;
1259
1260                match db.get(key) {
1261                    Some(value) => Ok(ToolResult::success(format!("Value for '{}': {}", key, value))),
1262                    None => Ok(ToolResult::error(format!("Key '{}' not found", key))),
1263                }
1264            }
1265            "delete" => {
1266                let key = args
1267                    .get("key")
1268                    .and_then(|v| v.as_str())
1269                    .ok_or_else(|| HeliosError::ToolError("Missing 'key' parameter for delete operation".to_string()))?;
1270
1271                match db.remove(key) {
1272                    Some(value) => Ok(ToolResult::success(format!("✓ Deleted '{}' (was: '{}')", key, value))),
1273                    None => Ok(ToolResult::error(format!("Key '{}' not found", key))),
1274                }
1275            }
1276            "list" => {
1277                if db.is_empty() {
1278                    Ok(ToolResult::success("Database is empty".to_string()))
1279                } else {
1280                    let mut items: Vec<String> = db
1281                        .iter()
1282                        .map(|(k, v)| format!("  • {} = {}", k, v))
1283                        .collect();
1284                    items.sort();
1285                    Ok(ToolResult::success(format!(
1286                        "Database contents ({} items):\n{}",
1287                        db.len(),
1288                        items.join("\n")
1289                    )))
1290                }
1291            }
1292            "clear" => {
1293                let count = db.len();
1294                db.clear();
1295                Ok(ToolResult::success(format!("✓ Cleared database ({} items removed)", count)))
1296            }
1297            "exists" => {
1298                let key = args
1299                    .get("key")
1300                    .and_then(|v| v.as_str())
1301                    .ok_or_else(|| HeliosError::ToolError("Missing 'key' parameter for exists operation".to_string()))?;
1302
1303                let exists = db.contains_key(key);
1304                Ok(ToolResult::success(format!("Key '{}' exists: {}", key, exists)))
1305            }
1306            _ => Err(HeliosError::ToolError(format!(
1307                "Unknown operation '{}'. Valid operations: set, get, delete, list, clear, exists",
1308                operation
1309            ))),
1310        }
1311    }
1312}
1313
1314#[cfg(test)]
1315mod tests {
1316    use super::*;
1317    use serde_json::json;
1318
1319    #[test]
1320    fn test_tool_result_success() {
1321        let result = ToolResult::success("test output");
1322        assert!(result.success);
1323        assert_eq!(result.output, "test output");
1324    }
1325
1326    #[tokio::test]
1327    async fn test_file_search_tool_glob_pattern_precompiled_regex() {
1328        use std::time::{SystemTime, UNIX_EPOCH};
1329        let base_tmp = std::env::temp_dir();
1330        let pid = std::process::id();
1331        let nanos = SystemTime::now()
1332            .duration_since(UNIX_EPOCH)
1333            .unwrap()
1334            .as_nanos();
1335        let test_dir = base_tmp.join(format!("helios_fs_test_{}_{}", pid, nanos));
1336        std::fs::create_dir_all(&test_dir).unwrap();
1337
1338        // Create files
1339        let file_rs = test_dir.join("a.rs");
1340        let file_txt = test_dir.join("b.txt");
1341        let subdir = test_dir.join("subdir");
1342        std::fs::create_dir_all(&subdir).unwrap();
1343        let file_sub_rs = subdir.join("mod.rs");
1344        std::fs::write(&file_rs, "fn main() {}\n").unwrap();
1345        std::fs::write(&file_txt, "hello\n").unwrap();
1346        std::fs::write(&file_sub_rs, "pub fn x() {}\n").unwrap();
1347
1348        // Execute search with glob pattern
1349        let tool = FileSearchTool;
1350        let args = json!({
1351            "path": test_dir.to_string_lossy(),
1352            "pattern": "*.rs",
1353            "max_results": 50
1354        });
1355        let result = tool.execute(args).await.unwrap();
1356        assert!(result.success);
1357        let out = result.output;
1358        // Should find .rs files
1359        assert!(out.contains(&file_rs.to_string_lossy().to_string()));
1360        assert!(out.contains(&file_sub_rs.to_string_lossy().to_string()));
1361        // Should not include .txt
1362        assert!(!out.contains(&file_txt.to_string_lossy().to_string()));
1363
1364        // Cleanup
1365        let _ = std::fs::remove_dir_all(&test_dir);
1366    }
1367
1368    #[tokio::test]
1369    async fn test_file_search_tool_invalid_pattern_fallback_contains() {
1370        use std::time::{SystemTime, UNIX_EPOCH};
1371        let base_tmp = std::env::temp_dir();
1372        let pid = std::process::id();
1373        let nanos = SystemTime::now()
1374            .duration_since(UNIX_EPOCH)
1375            .unwrap()
1376            .as_nanos();
1377        let test_dir = base_tmp.join(format!("helios_fs_test_invalid_{}_{}", pid, nanos));
1378        std::fs::create_dir_all(&test_dir).unwrap();
1379
1380        // Create file with '(' to be matched by substring fallback
1381        let special = test_dir.join("foo(bar).txt");
1382        std::fs::write(&special, "content\n").unwrap();
1383
1384        let tool = FileSearchTool;
1385        let args = json!({
1386            "path": test_dir.to_string_lossy(),
1387            "pattern": "(",
1388            "max_results": 50
1389        });
1390        let result = tool.execute(args).await.unwrap();
1391        assert!(result.success);
1392        let out = result.output;
1393        assert!(out.contains(&special.to_string_lossy().to_string()));
1394
1395        // Cleanup
1396        let _ = std::fs::remove_dir_all(&test_dir);
1397    }
1398
1399    #[test]
1400    fn test_tool_result_error() {
1401        let result = ToolResult::error("test error");
1402        assert!(!result.success);
1403        assert_eq!(result.output, "test error");
1404    }
1405
1406    #[tokio::test]
1407    async fn test_calculator_tool() {
1408        let tool = CalculatorTool;
1409        assert_eq!(tool.name(), "calculator");
1410        assert_eq!(
1411            tool.description(),
1412            "Perform basic arithmetic operations. Supports +, -, *, / operations."
1413);
1414
1415        let args = json!({"expression": "2 + 2"});
1416        let result = tool.execute(args).await.unwrap();
1417        assert!(result.success);
1418        assert_eq!(result.output, "4");
1419    }
1420
1421    #[tokio::test]
1422    async fn test_calculator_tool_multiplication() {
1423        let tool = CalculatorTool;
1424        let args = json!({"expression": "3 * 4"});
1425        let result = tool.execute(args).await.unwrap();
1426        assert!(result.success);
1427        assert_eq!(result.output, "12");
1428    }
1429
1430    #[tokio::test]
1431    async fn test_calculator_tool_division() {
1432        let tool = CalculatorTool;
1433        let args = json!({"expression": "8 / 2"});
1434        let result = tool.execute(args).await.unwrap();
1435        assert!(result.success);
1436        assert_eq!(result.output, "4");
1437    }
1438
1439    #[tokio::test]
1440    async fn test_calculator_tool_division_by_zero() {
1441        let tool = CalculatorTool;
1442        let args = json!({"expression": "8 / 0"});
1443        let result = tool.execute(args).await;
1444        assert!(result.is_err());
1445    }
1446
1447    #[tokio::test]
1448    async fn test_calculator_tool_invalid_expression() {
1449        let tool = CalculatorTool;
1450        let args = json!({"expression": "invalid"});
1451        let result = tool.execute(args).await;
1452        assert!(result.is_err());
1453    }
1454
1455    #[tokio::test]
1456    async fn test_echo_tool() {
1457        let tool = EchoTool;
1458        assert_eq!(tool.name(), "echo");
1459        assert_eq!(tool.description(), "Echo back the provided message.");
1460
1461        let args = json!({"message": "Hello, world!"});
1462        let result = tool.execute(args).await.unwrap();
1463        assert!(result.success);
1464        assert_eq!(result.output, "Echo: Hello, world!");
1465    }
1466
1467    #[tokio::test]
1468    async fn test_echo_tool_missing_parameter() {
1469        let tool = EchoTool;
1470        let args = json!({});
1471        let result = tool.execute(args).await;
1472        assert!(result.is_err());
1473    }
1474
1475    #[test]
1476    fn test_tool_registry_new() {
1477        let registry = ToolRegistry::new();
1478        assert!(registry.tools.is_empty());
1479    }
1480
1481    #[tokio::test]
1482    async fn test_tool_registry_register_and_get() {
1483        let mut registry = ToolRegistry::new();
1484        registry.register(Box::new(CalculatorTool));
1485
1486        let tool = registry.get("calculator");
1487        assert!(tool.is_some());
1488        assert_eq!(tool.unwrap().name(), "calculator");
1489    }
1490
1491    #[tokio::test]
1492    async fn test_tool_registry_execute() {
1493        let mut registry = ToolRegistry::new();
1494        registry.register(Box::new(CalculatorTool));
1495
1496        let args = json!({"expression": "5 * 6"});
1497        let result = registry.execute("calculator", args).await.unwrap();
1498        assert!(result.success);
1499        assert_eq!(result.output, "30");
1500    }
1501
1502    #[tokio::test]
1503    async fn test_tool_registry_execute_nonexistent_tool() {
1504        let registry = ToolRegistry::new();
1505        let args = json!({"expression": "5 * 6"});
1506        let result = registry.execute("nonexistent", args).await;
1507        assert!(result.is_err());
1508    }
1509
1510    #[test]
1511    fn test_tool_registry_get_definitions() {
1512        let mut registry = ToolRegistry::new();
1513        registry.register(Box::new(CalculatorTool));
1514        registry.register(Box::new(EchoTool));
1515
1516        let definitions = registry.get_definitions();
1517        assert_eq!(definitions.len(), 2);
1518
1519        // Check that we have both tools
1520        let names: Vec<String> = definitions
1521            .iter()
1522            .map(|d| d.function.name.clone())
1523            .collect();
1524        assert!(names.contains(&"calculator".to_string()));
1525        assert!(names.contains(&"echo".to_string()));
1526    }
1527
1528    #[test]
1529    fn test_tool_registry_list_tools() {
1530        let mut registry = ToolRegistry::new();
1531        registry.register(Box::new(CalculatorTool));
1532        registry.register(Box::new(EchoTool));
1533
1534        let tools = registry.list_tools();
1535        assert_eq!(tools.len(), 2);
1536        assert!(tools.contains(&"calculator".to_string()));
1537        assert!(tools.contains(&"echo".to_string()));
1538    }
1539
1540    #[tokio::test]
1541    async fn test_memory_db_set_and_get() {
1542        let tool = MemoryDBTool::new();
1543        
1544        // Set a value
1545        let set_args = json!({
1546            "operation": "set",
1547            "key": "name",
1548            "value": "Alice"
1549        });
1550        let result = tool.execute(set_args).await.unwrap();
1551        assert!(result.success);
1552        assert!(result.output.contains("Set 'name' = 'Alice'"));
1553        
1554        // Get the value
1555        let get_args = json!({
1556            "operation": "get",
1557            "key": "name"
1558        });
1559        let result = tool.execute(get_args).await.unwrap();
1560        assert!(result.success);
1561        assert!(result.output.contains("Alice"));
1562    }
1563
1564    #[tokio::test]
1565    async fn test_memory_db_delete() {
1566        let tool = MemoryDBTool::new();
1567        
1568        // Set a value
1569        let set_args = json!({
1570            "operation": "set",
1571            "key": "temp",
1572            "value": "data"
1573        });
1574        tool.execute(set_args).await.unwrap();
1575        
1576        // Delete the value
1577        let delete_args = json!({
1578            "operation": "delete",
1579            "key": "temp"
1580        });
1581        let result = tool.execute(delete_args).await.unwrap();
1582        assert!(result.success);
1583        assert!(result.output.contains("Deleted 'temp'"));
1584        
1585        // Try to get deleted value
1586        let get_args = json!({
1587            "operation": "get",
1588            "key": "temp"
1589        });
1590        let result = tool.execute(get_args).await.unwrap();
1591        assert!(!result.success);
1592        assert!(result.output.contains("not found"));
1593    }
1594
1595    #[tokio::test]
1596    async fn test_memory_db_exists() {
1597        let tool = MemoryDBTool::new();
1598        
1599        // Check non-existent key
1600        let exists_args = json!({
1601            "operation": "exists",
1602            "key": "test"
1603        });
1604        let result = tool.execute(exists_args).await.unwrap();
1605        assert!(result.success);
1606        assert!(result.output.contains("false"));
1607        
1608        // Set a value
1609        let set_args = json!({
1610            "operation": "set",
1611            "key": "test",
1612            "value": "value"
1613        });
1614        tool.execute(set_args).await.unwrap();
1615        
1616        // Check existing key
1617        let exists_args = json!({
1618            "operation": "exists",
1619            "key": "test"
1620        });
1621        let result = tool.execute(exists_args).await.unwrap();
1622        assert!(result.success);
1623        assert!(result.output.contains("true"));
1624    }
1625
1626    #[tokio::test]
1627    async fn test_memory_db_list() {
1628        let tool = MemoryDBTool::new();
1629        
1630        // List empty database
1631        let list_args = json!({
1632            "operation": "list"
1633        });
1634        let result = tool.execute(list_args).await.unwrap();
1635        assert!(result.success);
1636        assert!(result.output.contains("empty"));
1637        
1638        // Add some items
1639        tool.execute(json!({
1640            "operation": "set",
1641            "key": "key1",
1642            "value": "value1"
1643        })).await.unwrap();
1644        
1645        tool.execute(json!({
1646            "operation": "set",
1647            "key": "key2",
1648            "value": "value2"
1649        })).await.unwrap();
1650        
1651        // List items
1652        let list_args = json!({
1653            "operation": "list"
1654        });
1655        let result = tool.execute(list_args).await.unwrap();
1656        assert!(result.success);
1657        assert!(result.output.contains("2 items"));
1658        assert!(result.output.contains("key1"));
1659        assert!(result.output.contains("key2"));
1660    }
1661
1662    #[tokio::test]
1663    async fn test_memory_db_clear() {
1664        let tool = MemoryDBTool::new();
1665        
1666        // Add some items
1667        tool.execute(json!({
1668            "operation": "set",
1669            "key": "key1",
1670            "value": "value1"
1671        })).await.unwrap();
1672        
1673        tool.execute(json!({
1674            "operation": "set",
1675            "key": "key2",
1676            "value": "value2"
1677        })).await.unwrap();
1678        
1679        // Clear database
1680        let clear_args = json!({
1681            "operation": "clear"
1682        });
1683        let result = tool.execute(clear_args).await.unwrap();
1684        assert!(result.success);
1685        assert!(result.output.contains("2 items removed"));
1686        
1687        // Verify database is empty
1688        let list_args = json!({
1689            "operation": "list"
1690        });
1691        let result = tool.execute(list_args).await.unwrap();
1692        assert!(result.output.contains("empty"));
1693    }
1694
1695    #[tokio::test]
1696    async fn test_memory_db_invalid_operation() {
1697        let tool = MemoryDBTool::new();
1698        
1699        let args = json!({
1700            "operation": "invalid_op"
1701        });
1702        let result = tool.execute(args).await;
1703        assert!(result.is_err());
1704    }
1705
1706    #[tokio::test]
1707    async fn test_memory_db_shared_instance() {
1708        use std::sync::{Arc, Mutex};
1709        
1710        // Create a shared database
1711        let shared_db = Arc::new(Mutex::new(HashMap::new()));
1712        let tool1 = MemoryDBTool::with_shared_db(shared_db.clone());
1713        let tool2 = MemoryDBTool::with_shared_db(shared_db.clone());
1714        
1715        // Set value with tool1
1716        tool1.execute(json!({
1717            "operation": "set",
1718            "key": "shared",
1719            "value": "data"
1720        })).await.unwrap();
1721        
1722        // Get value with tool2
1723        let result = tool2.execute(json!({
1724            "operation": "get",
1725            "key": "shared"
1726        })).await.unwrap();
1727        assert!(result.success);
1728        assert!(result.output.contains("data"));
1729    }
1730}