helios_engine/
tools.rs

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