1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct ToolParameter {
21 #[serde(rename = "type")]
23 pub param_type: String,
24 pub description: String,
26 #[serde(skip)]
28 pub required: Option<bool>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct ToolDefinition {
34 #[serde(rename = "type")]
36 pub tool_type: String,
37 pub function: FunctionDefinition,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct FunctionDefinition {
44 pub name: String,
46 pub description: String,
48 pub parameters: ParametersSchema,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct ParametersSchema {
55 #[serde(rename = "type")]
57 pub schema_type: String,
58 pub properties: HashMap<String, ToolParameter>,
60 #[serde(skip_serializing_if = "Option::is_none")]
62 pub required: Option<Vec<String>>,
63}
64
65#[derive(Debug, Clone)]
67pub struct ToolResult {
68 pub success: bool,
70 pub output: String,
72}
73
74impl ToolResult {
75 pub fn success(output: impl Into<String>) -> Self {
77 Self {
78 success: true,
79 output: output.into(),
80 }
81 }
82
83 pub fn error(message: impl Into<String>) -> Self {
85 Self {
86 success: false,
87 output: message.into(),
88 }
89 }
90}
91
92#[async_trait]
94pub trait Tool: Send + Sync {
95 fn name(&self) -> &str;
97 fn description(&self) -> &str;
99 fn parameters(&self) -> HashMap<String, ToolParameter>;
101 async fn execute(&self, args: Value) -> Result<ToolResult>;
103
104 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
132pub struct ToolRegistry {
134 tools: HashMap<String, Box<dyn Tool>>,
135}
136
137impl ToolRegistry {
138 pub fn new() -> Self {
140 Self {
141 tools: HashMap::new(),
142 }
143 }
144
145 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 pub fn get(&self, name: &str) -> Option<&dyn Tool> {
153 self.tools.get(name).map(|b| &**b)
154 }
155
156 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 pub fn get_definitions(&self) -> Vec<ToolDefinition> {
168 self.tools
169 .values()
170 .map(|tool| tool.to_definition())
171 .collect()
172 }
173
174 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
186pub 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 let result = evaluate_expression(expression)?;
222 Ok(ToolResult::success(result.to_string()))
223 }
224}
225
226fn evaluate_expression(expr: &str) -> Result<f64> {
228 let expr = expr.replace(" ", "");
229
230 for op in &['*', '/', '+', '-'] {
232 if let Some(pos) = expr.rfind(*op) {
233 if pos == 0 {
234 continue; }
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
261pub 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
297pub 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 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 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 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 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 let matching_lines: Vec<(usize, &str)> = content
433 .lines()
434 .enumerate()
435 .filter(|(_, line)| line.contains(search_term))
436 .take(3) .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
474pub 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
565pub 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 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
628pub 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 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 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 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 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 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 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
771fn 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 replaced += write_with_replacements(writer, &carry, needle, replacement)?;
807 Ok(replaced)
808}
809
810fn 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
835fn 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#[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#[derive(Debug, Serialize, Deserialize)]
858struct QdrantPoint {
859 id: String,
860 vector: Vec<f32>,
861 payload: HashMap<String, serde_json::Value>,
862}
863
864#[derive(Debug, Serialize, Deserialize)]
866struct QdrantSearchRequest {
867 vector: Vec<f32>,
868 limit: usize,
869 with_payload: bool,
870 with_vector: bool,
871}
872
873#[derive(Debug, Serialize, Deserialize)]
875struct QdrantSearchResponse {
876 result: Vec<QdrantSearchResult>,
877}
878
879#[derive(Debug, Serialize, Deserialize)]
881struct QdrantSearchResult {
882 id: String,
883 score: f64,
884 payload: Option<HashMap<String, serde_json::Value>>,
885}
886
887#[derive(Debug, Serialize, Deserialize)]
889struct EmbeddingRequest {
890 input: String,
891 model: String,
892}
893
894#[derive(Debug, Serialize, Deserialize)]
896struct EmbeddingResponse {
897 data: Vec<EmbeddingData>,
898}
899
900#[derive(Debug, Serialize, Deserialize)]
902struct EmbeddingData {
903 embedding: Vec<f32>,
904}
905
906impl QdrantRAGTool {
907 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 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 async fn ensure_collection(&self) -> Result<()> {
967 let collection_url = format!("{}/collections/{}", self.qdrant_url, self.collection_name);
968
969 let response = self.client.get(&collection_url).send().await;
971
972 if response.is_ok() && response.unwrap().status().is_success() {
973 return Ok(()); }
975
976 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 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 let embedding = self.generate_embedding(text).await?;
1016
1017 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 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 async fn search(&self, query: &str, limit: usize) -> Result<Vec<(String, f64, String)>> {
1065 let query_embedding = self.generate_embedding(query).await?;
1067
1068 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 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 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 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
1316pub struct MemoryDBTool {
1321 db: std::sync::Arc<std::sync::Mutex<HashMap<String, String>>>,
1322}
1323
1324impl MemoryDBTool {
1325 pub fn new() -> Self {
1327 Self {
1328 db: std::sync::Arc::new(std::sync::Mutex::new(HashMap::new())),
1329 }
1330 }
1331
1332 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
1486pub struct WebScraperTool;
1488
1489#[async_trait]
1490impl Tool for WebScraperTool {
1491 fn name(&self) -> &str {
1492 "web_scraper"
1493 }
1494
1495 fn description(&self) -> &str {
1496 "Fetch and extract content from web URLs. Supports HTML text extraction and basic web scraping."
1497 }
1498
1499 fn parameters(&self) -> HashMap<String, ToolParameter> {
1500 let mut params = HashMap::new();
1501 params.insert(
1502 "url".to_string(),
1503 ToolParameter {
1504 param_type: "string".to_string(),
1505 description: "The URL to scrape content from".to_string(),
1506 required: Some(true),
1507 },
1508 );
1509 params.insert(
1510 "extract_text".to_string(),
1511 ToolParameter {
1512 param_type: "boolean".to_string(),
1513 description: "Whether to extract readable text from HTML (default: true)"
1514 .to_string(),
1515 required: Some(false),
1516 },
1517 );
1518 params.insert(
1519 "timeout_seconds".to_string(),
1520 ToolParameter {
1521 param_type: "number".to_string(),
1522 description: "Request timeout in seconds (default: 30)".to_string(),
1523 required: Some(false),
1524 },
1525 );
1526 params
1527 }
1528
1529 async fn execute(&self, args: Value) -> Result<ToolResult> {
1530 let url = args
1531 .get("url")
1532 .and_then(|v| v.as_str())
1533 .ok_or_else(|| HeliosError::ToolError("Missing 'url' parameter".to_string()))?;
1534
1535 let extract_text = args
1536 .get("extract_text")
1537 .and_then(|v| v.as_bool())
1538 .unwrap_or(true);
1539
1540 let timeout_seconds = args
1541 .get("timeout_seconds")
1542 .and_then(|v| v.as_u64())
1543 .unwrap_or(30);
1544
1545 let client = reqwest::Client::builder()
1546 .timeout(std::time::Duration::from_secs(timeout_seconds))
1547 .user_agent("Helios-WebScraper/1.0")
1548 .build()
1549 .map_err(|e| HeliosError::ToolError(format!("Failed to create HTTP client: {}", e)))?;
1550
1551 let response = client
1552 .get(url)
1553 .send()
1554 .await
1555 .map_err(|e| HeliosError::ToolError(format!("HTTP request failed: {}", e)))?;
1556
1557 if !response.status().is_success() {
1558 return Err(HeliosError::ToolError(format!(
1559 "HTTP request failed with status: {}",
1560 response.status()
1561 )));
1562 }
1563
1564 let headers = response.headers().clone();
1565 let content_type = headers
1566 .get("content-type")
1567 .and_then(|ct| ct.to_str().ok())
1568 .unwrap_or("");
1569
1570 let body = response
1571 .text()
1572 .await
1573 .map_err(|e| HeliosError::ToolError(format!("Failed to read response body: {}", e)))?;
1574
1575 let result = if extract_text && content_type.contains("text/html") {
1576 extract_text_from_html(&body)
1578 } else {
1579 body
1580 };
1581
1582 Ok(ToolResult::success(format!(
1583 "Content fetched from: {}\nContent-Type: {}\n\n{}",
1584 url, content_type, result
1585 )))
1586 }
1587}
1588
1589fn extract_text_from_html(html: &str) -> String {
1591 let mut result = String::new();
1593 let mut in_tag = false;
1594
1595 for ch in html.chars() {
1596 match ch {
1597 '<' => in_tag = true,
1598 '>' => in_tag = false,
1599 _ if !in_tag => result.push(ch),
1600 _ => {}
1601 }
1602 }
1603
1604 result
1606 .lines()
1607 .map(|line| line.trim())
1608 .filter(|line| !line.is_empty())
1609 .collect::<Vec<_>>()
1610 .join("\n")
1611}
1612
1613pub struct JsonParserTool;
1615
1616#[async_trait]
1617impl Tool for JsonParserTool {
1618 fn name(&self) -> &str {
1619 "json_parser"
1620 }
1621
1622 fn description(&self) -> &str {
1623 "Parse, validate, format, and manipulate JSON data. Supports operations: parse, stringify, get_value, set_value, validate"
1624 }
1625
1626 fn parameters(&self) -> HashMap<String, ToolParameter> {
1627 let mut params = HashMap::new();
1628 params.insert(
1629 "operation".to_string(),
1630 ToolParameter {
1631 param_type: "string".to_string(),
1632 description: "Operation to perform: 'parse', 'stringify', 'get_value', 'set_value', 'validate'".to_string(),
1633 required: Some(true),
1634 },
1635 );
1636 params.insert(
1637 "json".to_string(),
1638 ToolParameter {
1639 param_type: "string".to_string(),
1640 description: "JSON string for parse/stringify/validate operations".to_string(),
1641 required: Some(false),
1642 },
1643 );
1644 params.insert(
1645 "path".to_string(),
1646 ToolParameter {
1647 param_type: "string".to_string(),
1648 description:
1649 "JSON path for get_value/set_value operations (e.g., '$.key' or 'key.subkey')"
1650 .to_string(),
1651 required: Some(false),
1652 },
1653 );
1654 params.insert(
1655 "value".to_string(),
1656 ToolParameter {
1657 param_type: "string".to_string(),
1658 description: "Value to set for set_value operation (JSON string)".to_string(),
1659 required: Some(false),
1660 },
1661 );
1662 params.insert(
1663 "indent".to_string(),
1664 ToolParameter {
1665 param_type: "number".to_string(),
1666 description: "Indentation spaces for stringify operation (default: 2)".to_string(),
1667 required: Some(false),
1668 },
1669 );
1670 params
1671 }
1672
1673 async fn execute(&self, args: Value) -> Result<ToolResult> {
1674 let operation = args
1675 .get("operation")
1676 .and_then(|v| v.as_str())
1677 .ok_or_else(|| HeliosError::ToolError("Missing 'operation' parameter".to_string()))?;
1678
1679 match operation {
1680 "parse" => {
1681 let json_str = args
1682 .get("json")
1683 .and_then(|v| v.as_str())
1684 .ok_or_else(|| HeliosError::ToolError("Missing 'json' parameter for parse operation".to_string()))?;
1685
1686 let parsed: Value = serde_json::from_str(json_str)
1687 .map_err(|e| HeliosError::ToolError(format!("JSON parse error: {}", e)))?;
1688
1689 Ok(ToolResult::success(format!(
1690 "✓ JSON parsed successfully\nType: {}\nKeys: {}",
1691 get_json_type(&parsed),
1692 get_json_keys(&parsed)
1693 )))
1694 }
1695 "stringify" => {
1696 let json_str = args
1697 .get("json")
1698 .and_then(|v| v.as_str())
1699 .ok_or_else(|| HeliosError::ToolError("Missing 'json' parameter for stringify operation".to_string()))?;
1700
1701 let parsed: Value = serde_json::from_str(json_str)
1702 .map_err(|e| HeliosError::ToolError(format!("Invalid JSON for stringify: {}", e)))?;
1703
1704 let indent = args
1705 .get("indent")
1706 .and_then(|v| v.as_u64())
1707 .unwrap_or(2) as usize;
1708
1709 let formatted = if indent == 0 {
1710 serde_json::to_string(&parsed)
1711 } else {
1712 serde_json::to_string_pretty(&parsed)
1713 }
1714 .map_err(|e| HeliosError::ToolError(format!("JSON stringify error: {}", e)))?;
1715
1716 Ok(ToolResult::success(formatted))
1717 }
1718 "get_value" => {
1719 let json_str = args
1720 .get("json")
1721 .and_then(|v| v.as_str())
1722 .ok_or_else(|| HeliosError::ToolError("Missing 'json' parameter for get_value operation".to_string()))?;
1723
1724 let path = args
1725 .get("path")
1726 .and_then(|v| v.as_str())
1727 .ok_or_else(|| HeliosError::ToolError("Missing 'path' parameter for get_value operation".to_string()))?;
1728
1729 let parsed: Value = serde_json::from_str(json_str)
1730 .map_err(|e| HeliosError::ToolError(format!("Invalid JSON for get_value: {}", e)))?;
1731
1732 let value = get_value_by_path(&parsed, path)?;
1733 Ok(ToolResult::success(format!(
1734 "Value at path '{}': {}",
1735 path,
1736 serde_json::to_string_pretty(&value).unwrap_or_else(|_| value.to_string())
1737 )))
1738 }
1739 "set_value" => {
1740 let json_str = args
1741 .get("json")
1742 .and_then(|v| v.as_str())
1743 .ok_or_else(|| HeliosError::ToolError("Missing 'json' parameter for set_value operation".to_string()))?;
1744
1745 let path = args
1746 .get("path")
1747 .and_then(|v| v.as_str())
1748 .ok_or_else(|| HeliosError::ToolError("Missing 'path' parameter for set_value operation".to_string()))?;
1749
1750 let value_str = args
1751 .get("value")
1752 .and_then(|v| v.as_str())
1753 .ok_or_else(|| HeliosError::ToolError("Missing 'value' parameter for set_value operation".to_string()))?;
1754
1755 let new_value: Value = serde_json::from_str(value_str)
1756 .map_err(|e| HeliosError::ToolError(format!("Invalid value JSON: {}", e)))?;
1757
1758 let mut parsed: Value = serde_json::from_str(json_str)
1759 .map_err(|e| HeliosError::ToolError(format!("Invalid JSON for set_value: {}", e)))?;
1760
1761 set_value_by_path(&mut parsed, path, new_value)?;
1762 let result = serde_json::to_string_pretty(&parsed)
1763 .map_err(|e| HeliosError::ToolError(format!("JSON stringify error: {}", e)))?;
1764
1765 Ok(ToolResult::success(format!(
1766 "✓ Value set at path '{}'\n{}",
1767 path, result
1768 )))
1769 }
1770 "validate" => {
1771 let json_str = args
1772 .get("json")
1773 .and_then(|v| v.as_str())
1774 .ok_or_else(|| HeliosError::ToolError("Missing 'json' parameter for validate operation".to_string()))?;
1775
1776 match serde_json::from_str::<Value>(json_str) {
1777 Ok(_) => Ok(ToolResult::success("✓ JSON is valid".to_string())),
1778 Err(e) => Ok(ToolResult::error(format!("✗ JSON validation failed: {}", e))),
1779 }
1780 }
1781 _ => Err(HeliosError::ToolError(format!(
1782 "Unknown operation '{}'. Valid operations: parse, stringify, get_value, set_value, validate",
1783 operation
1784 ))),
1785 }
1786 }
1787}
1788
1789fn get_json_type(value: &Value) -> &'static str {
1791 match value {
1792 Value::Null => "null",
1793 Value::Bool(_) => "boolean",
1794 Value::Number(_) => "number",
1795 Value::String(_) => "string",
1796 Value::Array(_) => "array",
1797 Value::Object(_) => "object",
1798 }
1799}
1800
1801fn get_json_keys(value: &Value) -> String {
1803 match value {
1804 Value::Object(obj) => {
1805 let keys: Vec<&String> = obj.keys().collect();
1806 format!("{{{}}}", keys.len())
1807 }
1808 Value::Array(arr) => format!("[{}]", arr.len()),
1809 _ => "-".to_string(),
1810 }
1811}
1812
1813fn get_value_by_path(value: &Value, path: &str) -> Result<Value> {
1815 let path = path.trim_start_matches("$.");
1816 let keys: Vec<&str> = path.split('.').collect();
1817
1818 let mut current = value;
1819 for key in keys {
1820 match current {
1821 Value::Object(obj) => {
1822 current = obj
1823 .get(key)
1824 .ok_or_else(|| HeliosError::ToolError(format!("Key '{}' not found", key)))?;
1825 }
1826 _ => {
1827 return Err(HeliosError::ToolError(format!(
1828 "Cannot access '{}' on non-object",
1829 key
1830 )))
1831 }
1832 }
1833 }
1834
1835 Ok(current.clone())
1836}
1837
1838fn set_value_by_path(value: &mut Value, path: &str, new_value: Value) -> Result<()> {
1840 let path = path.trim_start_matches("$.");
1841 let keys: Vec<&str> = path.split('.').collect();
1842
1843 if keys.is_empty() {
1844 return Err(HeliosError::ToolError("Empty path".to_string()));
1845 }
1846
1847 let mut current = value;
1848 for (i, key) in keys.iter().enumerate() {
1849 if i == keys.len() - 1 {
1850 match current {
1852 Value::Object(obj) => {
1853 obj.insert(key.to_string(), new_value);
1854 return Ok(());
1855 }
1856 _ => {
1857 return Err(HeliosError::ToolError(format!(
1858 "Cannot set '{}' on non-object",
1859 key
1860 )))
1861 }
1862 }
1863 } else {
1864 match current {
1866 Value::Object(obj) => {
1867 if !obj.contains_key(*key) {
1868 obj.insert(key.to_string(), Value::Object(serde_json::Map::new()));
1869 }
1870 current = obj.get_mut(*key).unwrap();
1871 }
1872 _ => {
1873 return Err(HeliosError::ToolError(format!(
1874 "Cannot access '{}' on non-object",
1875 key
1876 )))
1877 }
1878 }
1879 }
1880 }
1881
1882 Ok(())
1883}
1884
1885pub struct TimestampTool;
1887
1888#[async_trait]
1889impl Tool for TimestampTool {
1890 fn name(&self) -> &str {
1891 "timestamp"
1892 }
1893
1894 fn description(&self) -> &str {
1895 "Work with timestamps and date/time operations. Supports current time, formatting, parsing, and time arithmetic."
1896 }
1897
1898 fn parameters(&self) -> HashMap<String, ToolParameter> {
1899 let mut params = HashMap::new();
1900 params.insert(
1901 "operation".to_string(),
1902 ToolParameter {
1903 param_type: "string".to_string(),
1904 description: "Operation: 'now', 'format', 'parse', 'add', 'subtract', 'diff'"
1905 .to_string(),
1906 required: Some(true),
1907 },
1908 );
1909 params.insert(
1910 "timestamp".to_string(),
1911 ToolParameter {
1912 param_type: "string".to_string(),
1913 description: "Timestamp string for parse/format operations".to_string(),
1914 required: Some(false),
1915 },
1916 );
1917 params.insert(
1918 "format".to_string(),
1919 ToolParameter {
1920 param_type: "string".to_string(),
1921 description: "Date format string (default: RFC3339)".to_string(),
1922 required: Some(false),
1923 },
1924 );
1925 params.insert(
1926 "unit".to_string(),
1927 ToolParameter {
1928 param_type: "string".to_string(),
1929 description: "Time unit for arithmetic: 'seconds', 'minutes', 'hours', 'days'"
1930 .to_string(),
1931 required: Some(false),
1932 },
1933 );
1934 params.insert(
1935 "amount".to_string(),
1936 ToolParameter {
1937 param_type: "number".to_string(),
1938 description: "Amount for add/subtract operations".to_string(),
1939 required: Some(false),
1940 },
1941 );
1942 params.insert(
1943 "timestamp1".to_string(),
1944 ToolParameter {
1945 param_type: "string".to_string(),
1946 description: "First timestamp for diff operation".to_string(),
1947 required: Some(false),
1948 },
1949 );
1950 params.insert(
1951 "timestamp2".to_string(),
1952 ToolParameter {
1953 param_type: "string".to_string(),
1954 description: "Second timestamp for diff operation".to_string(),
1955 required: Some(false),
1956 },
1957 );
1958 params
1959 }
1960
1961 async fn execute(&self, args: Value) -> Result<ToolResult> {
1962 let operation = args
1963 .get("operation")
1964 .and_then(|v| v.as_str())
1965 .ok_or_else(|| HeliosError::ToolError("Missing 'operation' parameter".to_string()))?;
1966
1967 match operation {
1968 "now" => {
1969 let now = chrono::Utc::now();
1970 let timestamp = now.timestamp();
1971 let rfc3339 = now.to_rfc3339();
1972
1973 Ok(ToolResult::success(format!(
1974 "Current time:\nUnix timestamp: {}\nRFC3339: {}\nLocal: {}",
1975 timestamp,
1976 rfc3339,
1977 now.with_timezone(&chrono::Local::now().timezone())
1978 )))
1979 }
1980 "format" => {
1981 let timestamp_str =
1982 args.get("timestamp")
1983 .and_then(|v| v.as_str())
1984 .ok_or_else(|| {
1985 HeliosError::ToolError(
1986 "Missing 'timestamp' parameter for format operation".to_string(),
1987 )
1988 })?;
1989
1990 let format_str = args
1991 .get("format")
1992 .and_then(|v| v.as_str())
1993 .unwrap_or("%Y-%m-%d %H:%M:%S");
1994
1995 let dt = if let Ok(ts) = timestamp_str.parse::<i64>() {
1997 chrono::DateTime::from_timestamp(ts, 0).ok_or_else(|| {
1998 HeliosError::ToolError("Invalid unix timestamp".to_string())
1999 })?
2000 } else {
2001 chrono::DateTime::parse_from_rfc3339(timestamp_str)
2002 .map(|dt| dt.with_timezone(&chrono::Utc))
2003 .or_else(|_| {
2004 chrono::NaiveDateTime::parse_from_str(timestamp_str, format_str)
2005 .map(|ndt| ndt.and_utc())
2006 .map_err(|e| {
2007 HeliosError::ToolError(format!(
2008 "Failed to parse timestamp: {}",
2009 e
2010 ))
2011 })
2012 })
2013 .map_err(|e| {
2014 HeliosError::ToolError(format!("Failed to parse timestamp: {}", e))
2015 })?
2016 };
2017
2018 let formatted = dt.format(format_str).to_string();
2019 Ok(ToolResult::success(format!(
2020 "Formatted timestamp: {}",
2021 formatted
2022 )))
2023 }
2024 "parse" => {
2025 let timestamp_str =
2026 args.get("timestamp")
2027 .and_then(|v| v.as_str())
2028 .ok_or_else(|| {
2029 HeliosError::ToolError(
2030 "Missing 'timestamp' parameter for parse operation".to_string(),
2031 )
2032 })?;
2033
2034 let format_str = args
2035 .get("format")
2036 .and_then(|v| v.as_str())
2037 .unwrap_or("%Y-%m-%d %H:%M:%S");
2038
2039 let dt = chrono::DateTime::parse_from_rfc3339(timestamp_str)
2041 .map(|dt| dt.with_timezone(&chrono::Utc))
2042 .or_else(|_| {
2043 chrono::NaiveDateTime::parse_from_str(timestamp_str, format_str)
2044 .map(|ndt| ndt.and_utc())
2045 })
2046 .map_err(|e| {
2047 HeliosError::ToolError(format!("Failed to parse timestamp: {}", e))
2048 })?;
2049
2050 let unix_ts = dt.timestamp();
2051 let rfc3339 = dt.to_rfc3339();
2052
2053 Ok(ToolResult::success(format!(
2054 "Parsed timestamp:\nUnix: {}\nRFC3339: {}\nFormatted: {}",
2055 unix_ts,
2056 rfc3339,
2057 dt.format("%Y-%m-%d %H:%M:%S UTC")
2058 )))
2059 }
2060 "add" | "subtract" => {
2061 let default_timestamp = chrono::Utc::now().to_rfc3339();
2062 let timestamp_str = args
2063 .get("timestamp")
2064 .and_then(|v| v.as_str())
2065 .unwrap_or(&default_timestamp);
2066
2067 let unit = args.get("unit").and_then(|v| v.as_str()).ok_or_else(|| {
2068 HeliosError::ToolError(
2069 "Missing 'unit' parameter for arithmetic operation".to_string(),
2070 )
2071 })?;
2072
2073 let amount = args.get("amount").and_then(|v| v.as_i64()).ok_or_else(|| {
2074 HeliosError::ToolError(
2075 "Missing 'amount' parameter for arithmetic operation".to_string(),
2076 )
2077 })?;
2078
2079 let dt = chrono::DateTime::parse_from_rfc3339(timestamp_str)
2080 .or_else(|_| {
2081 if let Ok(ts) = timestamp_str.parse::<i64>() {
2082 chrono::DateTime::from_timestamp(ts, 0)
2083 .ok_or_else(|| {
2084 HeliosError::ToolError("Invalid unix timestamp".to_string())
2085 })
2086 .map(|dt| dt.into())
2087 } else {
2088 Err(HeliosError::ToolError(
2089 "Invalid timestamp format".to_string(),
2090 ))
2091 }
2092 })
2093 .map_err(|e| {
2094 HeliosError::ToolError(format!("Failed to parse timestamp: {}", e))
2095 })?;
2096
2097 let duration = match unit {
2098 "seconds" => chrono::Duration::seconds(amount),
2099 "minutes" => chrono::Duration::minutes(amount),
2100 "hours" => chrono::Duration::hours(amount),
2101 "days" => chrono::Duration::days(amount),
2102 _ => {
2103 return Err(HeliosError::ToolError(format!(
2104 "Unknown unit '{}'. Use: seconds, minutes, hours, days",
2105 unit
2106 )))
2107 }
2108 };
2109
2110 let result_dt = if operation == "add" {
2111 dt + duration
2112 } else {
2113 dt - duration
2114 };
2115
2116 Ok(ToolResult::success(format!(
2117 "{} {} {} to {}\nResult: {}\nUnix: {}",
2118 if operation == "add" {
2119 "Added"
2120 } else {
2121 "Subtracted"
2122 },
2123 amount.abs(),
2124 unit,
2125 timestamp_str,
2126 result_dt.to_rfc3339(),
2127 result_dt.timestamp()
2128 )))
2129 }
2130 "diff" => {
2131 let ts1_str = args
2132 .get("timestamp1")
2133 .and_then(|v| v.as_str())
2134 .ok_or_else(|| {
2135 HeliosError::ToolError(
2136 "Missing 'timestamp1' parameter for diff operation".to_string(),
2137 )
2138 })?;
2139
2140 let ts2_str = args
2141 .get("timestamp2")
2142 .and_then(|v| v.as_str())
2143 .ok_or_else(|| {
2144 HeliosError::ToolError(
2145 "Missing 'timestamp2' parameter for diff operation".to_string(),
2146 )
2147 })?;
2148
2149 let dt1 = parse_timestamp(ts1_str)?;
2150 let dt2 = parse_timestamp(ts2_str)?;
2151
2152 let duration = if dt1 > dt2 { dt1 - dt2 } else { dt2 - dt1 };
2153 let seconds = duration.num_seconds();
2154 let minutes = duration.num_minutes();
2155 let hours = duration.num_hours();
2156 let days = duration.num_days();
2157
2158 Ok(ToolResult::success(format!(
2159 "Time difference between {} and {}:\n{} seconds\n{} minutes\n{} hours\n{} days",
2160 ts1_str, ts2_str, seconds, minutes, hours, days
2161 )))
2162 }
2163 _ => Err(HeliosError::ToolError(format!(
2164 "Unknown operation '{}'. Valid operations: now, format, parse, add, subtract, diff",
2165 operation
2166 ))),
2167 }
2168 }
2169}
2170
2171fn parse_timestamp(ts_str: &str) -> Result<chrono::DateTime<chrono::Utc>> {
2173 if let Ok(ts) = ts_str.parse::<i64>() {
2174 chrono::DateTime::from_timestamp(ts, 0)
2175 .ok_or_else(|| HeliosError::ToolError("Invalid unix timestamp".to_string()))
2176 } else {
2177 chrono::DateTime::parse_from_rfc3339(ts_str)
2178 .map(|dt| dt.with_timezone(&chrono::Utc))
2179 .map_err(|_| HeliosError::ToolError("Invalid timestamp format".to_string()))
2180 }
2181}
2182
2183pub struct FileIOTool;
2185
2186#[async_trait]
2187impl Tool for FileIOTool {
2188 fn name(&self) -> &str {
2189 "file_io"
2190 }
2191
2192 fn description(&self) -> &str {
2193 "Basic file operations: read, write, append, delete, copy, move. Unified interface for common file I/O tasks. Delete operation is safe by default (only empty directories)."
2194 }
2195
2196 fn parameters(&self) -> HashMap<String, ToolParameter> {
2197 let mut params = HashMap::new();
2198 params.insert(
2199 "operation".to_string(),
2200 ToolParameter {
2201 param_type: "string".to_string(),
2202 description: "Operation: 'read', 'write', 'append', 'delete', 'copy', 'move', 'exists', 'size' (delete is safe by default)".to_string(),
2203 required: Some(true),
2204 },
2205 );
2206 params.insert(
2207 "path".to_string(),
2208 ToolParameter {
2209 param_type: "string".to_string(),
2210 description: "File path for operations".to_string(),
2211 required: Some(false),
2212 },
2213 );
2214 params.insert(
2215 "src_path".to_string(),
2216 ToolParameter {
2217 param_type: "string".to_string(),
2218 description: "Source path for copy/move operations".to_string(),
2219 required: Some(false),
2220 },
2221 );
2222 params.insert(
2223 "dst_path".to_string(),
2224 ToolParameter {
2225 param_type: "string".to_string(),
2226 description: "Destination path for copy/move operations".to_string(),
2227 required: Some(false),
2228 },
2229 );
2230 params.insert(
2231 "content".to_string(),
2232 ToolParameter {
2233 param_type: "string".to_string(),
2234 description: "Content for write/append operations".to_string(),
2235 required: Some(false),
2236 },
2237 );
2238 params.insert(
2239 "recursive".to_string(),
2240 ToolParameter {
2241 param_type: "boolean".to_string(),
2242 description: "Allow recursive directory deletion (default: false for safety)"
2243 .to_string(),
2244 required: Some(false),
2245 },
2246 );
2247 params
2248 }
2249
2250 async fn execute(&self, args: Value) -> Result<ToolResult> {
2251 let operation = args
2252 .get("operation")
2253 .and_then(|v| v.as_str())
2254 .ok_or_else(|| HeliosError::ToolError("Missing 'operation' parameter".to_string()))?;
2255
2256 match operation {
2257 "read" => {
2258 let path = args
2259 .get("path")
2260 .and_then(|v| v.as_str())
2261 .ok_or_else(|| HeliosError::ToolError("Missing 'path' parameter for read operation".to_string()))?;
2262
2263 let content = std::fs::read_to_string(path)
2264 .map_err(|e| HeliosError::ToolError(format!("Failed to read file: {}", e)))?;
2265
2266 Ok(ToolResult::success(format!(
2267 "File: {}\nSize: {} bytes\n\n{}",
2268 path,
2269 content.len(),
2270 content
2271 )))
2272 }
2273 "write" => {
2274 let path = args
2275 .get("path")
2276 .and_then(|v| v.as_str())
2277 .ok_or_else(|| HeliosError::ToolError("Missing 'path' parameter for write operation".to_string()))?;
2278
2279 let content = args
2280 .get("content")
2281 .and_then(|v| v.as_str())
2282 .ok_or_else(|| HeliosError::ToolError("Missing 'content' parameter for write operation".to_string()))?;
2283
2284 if let Some(parent) = std::path::Path::new(path).parent() {
2286 std::fs::create_dir_all(parent).map_err(|e| {
2287 HeliosError::ToolError(format!("Failed to create directories: {}", e))
2288 })?;
2289 }
2290
2291 std::fs::write(path, content)
2292 .map_err(|e| HeliosError::ToolError(format!("Failed to write file: {}", e)))?;
2293
2294 Ok(ToolResult::success(format!(
2295 "✓ Wrote {} bytes to {}",
2296 content.len(),
2297 path
2298 )))
2299 }
2300 "append" => {
2301 let path = args
2302 .get("path")
2303 .and_then(|v| v.as_str())
2304 .ok_or_else(|| HeliosError::ToolError("Missing 'path' parameter for append operation".to_string()))?;
2305
2306 let content = args
2307 .get("content")
2308 .and_then(|v| v.as_str())
2309 .ok_or_else(|| HeliosError::ToolError("Missing 'content' parameter for append operation".to_string()))?;
2310
2311 std::fs::OpenOptions::new()
2312 .create(true)
2313 .append(true)
2314 .open(path)
2315 .and_then(|mut file| std::io::Write::write_all(&mut file, content.as_bytes()))
2316 .map_err(|e| HeliosError::ToolError(format!("Failed to append to file: {}", e)))?;
2317
2318 Ok(ToolResult::success(format!(
2319 "✓ Appended {} bytes to {}",
2320 content.len(),
2321 path
2322 )))
2323 }
2324 "delete" => {
2325 let path = args
2326 .get("path")
2327 .and_then(|v| v.as_str())
2328 .ok_or_else(|| HeliosError::ToolError("Missing 'path' parameter for delete operation".to_string()))?;
2329
2330 let recursive = args.get("recursive").and_then(|v| v.as_bool()).unwrap_or(false);
2331
2332 let metadata = std::fs::metadata(path)
2333 .map_err(|e| HeliosError::ToolError(format!("Cannot access file: {}", e)))?;
2334
2335 let file_type = if metadata.is_file() { "file" } else { "directory" };
2336
2337 if metadata.is_file() {
2338 std::fs::remove_file(path)
2339 .map_err(|e| HeliosError::ToolError(format!("Failed to delete file: {}", e)))?;
2340 } else if recursive {
2341 std::fs::remove_dir_all(path)
2343 .map_err(|e| HeliosError::ToolError(format!("Failed to delete directory recursively: {}", e)))?;
2344 } else {
2345 std::fs::remove_dir(path)
2347 .map_err(|e| HeliosError::ToolError(format!("Failed to delete directory (must be empty, or set recursive=true): {}", e)))?;
2348 }
2349
2350 let delete_type = if recursive && !metadata.is_file() { "recursively" } else { "" };
2351 let separator = if delete_type.is_empty() { "" } else { ": " };
2352 Ok(ToolResult::success(format!(
2353 "✓ Deleted {} {}{}{}",
2354 file_type, delete_type, separator, path
2355 )))
2356 }
2357 "copy" => {
2358 let src_path = args
2359 .get("src_path")
2360 .and_then(|v| v.as_str())
2361 .ok_or_else(|| HeliosError::ToolError("Missing 'src_path' parameter for copy operation".to_string()))?;
2362
2363 let dst_path = args
2364 .get("dst_path")
2365 .and_then(|v| v.as_str())
2366 .ok_or_else(|| HeliosError::ToolError("Missing 'dst_path' parameter for copy operation".to_string()))?;
2367
2368 std::fs::copy(src_path, dst_path)
2369 .map_err(|e| HeliosError::ToolError(format!("Failed to copy file: {}", e)))?;
2370
2371 Ok(ToolResult::success(format!(
2372 "✓ Copied {} to {}",
2373 src_path, dst_path
2374 )))
2375 }
2376 "move" => {
2377 let src_path = args
2378 .get("src_path")
2379 .and_then(|v| v.as_str())
2380 .ok_or_else(|| HeliosError::ToolError("Missing 'src_path' parameter for move operation".to_string()))?;
2381
2382 let dst_path = args
2383 .get("dst_path")
2384 .and_then(|v| v.as_str())
2385 .ok_or_else(|| HeliosError::ToolError("Missing 'dst_path' parameter for move operation".to_string()))?;
2386
2387 std::fs::rename(src_path, dst_path)
2388 .map_err(|e| HeliosError::ToolError(format!("Failed to move file: {}", e)))?;
2389
2390 Ok(ToolResult::success(format!(
2391 "✓ Moved {} to {}",
2392 src_path, dst_path
2393 )))
2394 }
2395 "exists" => {
2396 let path = args
2397 .get("path")
2398 .and_then(|v| v.as_str())
2399 .ok_or_else(|| HeliosError::ToolError("Missing 'path' parameter for exists operation".to_string()))?;
2400
2401 let exists = std::path::Path::new(path).exists();
2402 let file_type = if exists {
2403 if std::fs::metadata(path).map(|m| m.is_file()).unwrap_or(false) {
2404 "file"
2405 } else {
2406 "directory"
2407 }
2408 } else {
2409 "nonexistent"
2410 };
2411
2412 Ok(ToolResult::success(format!(
2413 "Path '{}' exists: {} ({})",
2414 path, exists, file_type
2415 )))
2416 }
2417 "size" => {
2418 let path = args
2419 .get("path")
2420 .and_then(|v| v.as_str())
2421 .ok_or_else(|| HeliosError::ToolError("Missing 'path' parameter for size operation".to_string()))?;
2422
2423 let metadata = std::fs::metadata(path)
2424 .map_err(|e| HeliosError::ToolError(format!("Cannot access file: {}", e)))?;
2425
2426 let size = metadata.len();
2427 Ok(ToolResult::success(format!(
2428 "Size of '{}': {} bytes",
2429 path, size
2430 )))
2431 }
2432 _ => Err(HeliosError::ToolError(format!(
2433 "Unknown operation '{}'. Valid operations: read, write, append, delete, copy, move, exists, size",
2434 operation
2435 ))),
2436 }
2437 }
2438}
2439
2440pub struct ShellCommandTool;
2442
2443#[async_trait]
2444impl Tool for ShellCommandTool {
2445 fn name(&self) -> &str {
2446 "shell_command"
2447 }
2448
2449 fn description(&self) -> &str {
2450 "Execute shell commands with safety restrictions. Limited to basic commands, no destructive operations allowed."
2451 }
2452
2453 fn parameters(&self) -> HashMap<String, ToolParameter> {
2454 let mut params = HashMap::new();
2455 params.insert(
2456 "command".to_string(),
2457 ToolParameter {
2458 param_type: "string".to_string(),
2459 description: "Shell command to execute".to_string(),
2460 required: Some(true),
2461 },
2462 );
2463 params.insert(
2464 "timeout_seconds".to_string(),
2465 ToolParameter {
2466 param_type: "number".to_string(),
2467 description: "Command timeout in seconds (default: 30, max: 60)".to_string(),
2468 required: Some(false),
2469 },
2470 );
2471 params
2472 }
2473
2474 async fn execute(&self, args: Value) -> Result<ToolResult> {
2475 let command = args
2476 .get("command")
2477 .and_then(|v| v.as_str())
2478 .ok_or_else(|| HeliosError::ToolError("Missing 'command' parameter".to_string()))?;
2479
2480 let timeout_seconds = args
2481 .get("timeout_seconds")
2482 .and_then(|v| v.as_u64())
2483 .unwrap_or(30)
2484 .min(60); let dangerous_patterns = [
2488 "rm ",
2489 "rmdir",
2490 "del ",
2491 "format",
2492 "fdisk",
2493 "mkfs",
2494 "dd ",
2495 "shred",
2496 "wipe",
2497 "sudo",
2498 "su ",
2499 "chmod 777",
2500 "chown root",
2501 "passwd",
2502 "usermod",
2503 "userdel",
2504 ">",
2505 ">>",
2506 "|",
2507 ";",
2508 "&&",
2509 "||",
2510 "`",
2511 "$(",
2512 ];
2513
2514 for pattern in &dangerous_patterns {
2515 if command.contains(pattern) {
2516 return Err(HeliosError::ToolError(format!(
2517 "Command blocked for safety: contains '{}'",
2518 pattern
2519 )));
2520 }
2521 }
2522
2523 let output = tokio::time::timeout(
2525 std::time::Duration::from_secs(timeout_seconds),
2526 tokio::process::Command::new("sh")
2527 .arg("-c")
2528 .arg(command)
2529 .output(),
2530 )
2531 .await
2532 .map_err(|_| {
2533 HeliosError::ToolError(format!(
2534 "Command timed out after {} seconds",
2535 timeout_seconds
2536 ))
2537 })?
2538 .map_err(|e| HeliosError::ToolError(format!("Failed to execute command: {}", e)))?;
2539
2540 let stdout = String::from_utf8_lossy(&output.stdout);
2541 let stderr = String::from_utf8_lossy(&output.stderr);
2542
2543 let exit_code = output.status.code().unwrap_or(-1);
2544
2545 let mut result = format!("Command: {}\nExit code: {}\n", command, exit_code);
2546
2547 if !stdout.is_empty() {
2548 result.push_str(&format!("Stdout:\n{}\n", stdout));
2549 }
2550
2551 if !stderr.is_empty() {
2552 result.push_str(&format!("Stderr:\n{}\n", stderr));
2553 }
2554
2555 if exit_code == 0 {
2556 Ok(ToolResult::success(result))
2557 } else {
2558 Ok(ToolResult::error(result))
2559 }
2560 }
2561}
2562
2563pub struct HttpRequestTool;
2565
2566#[async_trait]
2567impl Tool for HttpRequestTool {
2568 fn name(&self) -> &str {
2569 "http_request"
2570 }
2571
2572 fn description(&self) -> &str {
2573 "Make HTTP requests with various methods. Supports GET, POST, PUT, DELETE with custom headers and body."
2574 }
2575
2576 fn parameters(&self) -> HashMap<String, ToolParameter> {
2577 let mut params = HashMap::new();
2578 params.insert(
2579 "method".to_string(),
2580 ToolParameter {
2581 param_type: "string".to_string(),
2582 description: "HTTP method: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS"
2583 .to_string(),
2584 required: Some(true),
2585 },
2586 );
2587 params.insert(
2588 "url".to_string(),
2589 ToolParameter {
2590 param_type: "string".to_string(),
2591 description: "Request URL".to_string(),
2592 required: Some(true),
2593 },
2594 );
2595 params.insert(
2596 "headers".to_string(),
2597 ToolParameter {
2598 param_type: "object".to_string(),
2599 description: "Request headers as JSON object (optional)".to_string(),
2600 required: Some(false),
2601 },
2602 );
2603 params.insert(
2604 "body".to_string(),
2605 ToolParameter {
2606 param_type: "string".to_string(),
2607 description: "Request body for POST/PUT/PATCH methods".to_string(),
2608 required: Some(false),
2609 },
2610 );
2611 params.insert(
2612 "timeout_seconds".to_string(),
2613 ToolParameter {
2614 param_type: "number".to_string(),
2615 description: "Request timeout in seconds (default: 30)".to_string(),
2616 required: Some(false),
2617 },
2618 );
2619 params
2620 }
2621
2622 async fn execute(&self, args: Value) -> Result<ToolResult> {
2623 let method = args
2624 .get("method")
2625 .and_then(|v| v.as_str())
2626 .ok_or_else(|| HeliosError::ToolError("Missing 'method' parameter".to_string()))?;
2627
2628 let url = args
2629 .get("url")
2630 .and_then(|v| v.as_str())
2631 .ok_or_else(|| HeliosError::ToolError("Missing 'url' parameter".to_string()))?;
2632
2633 let timeout_seconds = args
2634 .get("timeout_seconds")
2635 .and_then(|v| v.as_u64())
2636 .unwrap_or(30);
2637
2638 let client = reqwest::Client::builder()
2639 .timeout(std::time::Duration::from_secs(timeout_seconds))
2640 .build()
2641 .map_err(|e| HeliosError::ToolError(format!("Failed to create HTTP client: {}", e)))?;
2642
2643 let mut request = match method.to_uppercase().as_str() {
2644 "GET" => client.get(url),
2645 "POST" => client.post(url),
2646 "PUT" => client.put(url),
2647 "DELETE" => client.delete(url),
2648 "PATCH" => client.patch(url),
2649 "HEAD" => client.head(url),
2650 _ => {
2651 return Err(HeliosError::ToolError(format!(
2652 "Unsupported HTTP method: {}",
2653 method
2654 )))
2655 }
2656 };
2657
2658 if let Some(headers) = args.get("headers") {
2660 if let Some(headers_obj) = headers.as_object() {
2661 for (key, value) in headers_obj {
2662 if let Some(value_str) = value.as_str() {
2663 request = request.header(key, value_str);
2664 }
2665 }
2666 }
2667 }
2668
2669 if matches!(method.to_uppercase().as_str(), "POST" | "PUT" | "PATCH") {
2671 if let Some(body) = args.get("body").and_then(|v| v.as_str()) {
2672 request = request.body(body.to_string());
2673 }
2674 }
2675
2676 let response = request
2677 .send()
2678 .await
2679 .map_err(|e| HeliosError::ToolError(format!("HTTP request failed: {}", e)))?;
2680
2681 let status = response.status();
2682 let headers = response.headers().clone();
2683 let body = response
2684 .text()
2685 .await
2686 .unwrap_or_else(|_| "Binary content".to_string());
2687
2688 let mut result = format!(
2689 "HTTP {} {}\nStatus: {}\n\n",
2690 method.to_uppercase(),
2691 url,
2692 status
2693 );
2694
2695 result.push_str("Response Headers:\n");
2697 for (name, value) in headers.iter() {
2698 if let Ok(value_str) = value.to_str() {
2699 result.push_str(&format!("{}: {}\n", name, value_str));
2700 }
2701 }
2702 result.push_str("\nResponse Body:\n");
2703 result.push_str(&body);
2704
2705 if status.is_success() {
2706 Ok(ToolResult::success(result))
2707 } else {
2708 Ok(ToolResult::error(result))
2709 }
2710 }
2711}
2712
2713pub struct FileListTool;
2715
2716#[async_trait]
2717impl Tool for FileListTool {
2718 fn name(&self) -> &str {
2719 "file_list"
2720 }
2721
2722 fn description(&self) -> &str {
2723 "List directory contents with detailed information including file sizes, types, and modification times."
2724 }
2725
2726 fn parameters(&self) -> HashMap<String, ToolParameter> {
2727 let mut params = HashMap::new();
2728 params.insert(
2729 "path".to_string(),
2730 ToolParameter {
2731 param_type: "string".to_string(),
2732 description: "Directory path to list (default: current directory)".to_string(),
2733 required: Some(false),
2734 },
2735 );
2736 params.insert(
2737 "show_hidden".to_string(),
2738 ToolParameter {
2739 param_type: "boolean".to_string(),
2740 description: "Show hidden files/directories (default: false)".to_string(),
2741 required: Some(false),
2742 },
2743 );
2744 params.insert(
2745 "recursive".to_string(),
2746 ToolParameter {
2747 param_type: "boolean".to_string(),
2748 description: "List contents recursively (default: false)".to_string(),
2749 required: Some(false),
2750 },
2751 );
2752 params.insert(
2753 "max_depth".to_string(),
2754 ToolParameter {
2755 param_type: "number".to_string(),
2756 description: "Maximum recursion depth (default: 3)".to_string(),
2757 required: Some(false),
2758 },
2759 );
2760 params
2761 }
2762
2763 async fn execute(&self, args: Value) -> Result<ToolResult> {
2764 let base_path = args.get("path").and_then(|v| v.as_str()).unwrap_or(".");
2765 let show_hidden = args
2766 .get("show_hidden")
2767 .and_then(|v| v.as_bool())
2768 .unwrap_or(false);
2769 let recursive = args
2770 .get("recursive")
2771 .and_then(|v| v.as_bool())
2772 .unwrap_or(false);
2773 let max_depth = args.get("max_depth").and_then(|v| v.as_u64()).unwrap_or(3) as usize;
2774
2775 let mut results = Vec::new();
2776
2777 if recursive {
2778 for entry in walkdir::WalkDir::new(base_path)
2779 .max_depth(max_depth)
2780 .into_iter()
2781 .filter_map(|e| e.ok())
2782 {
2783 if let Some(entry_info) = format_walkdir_entry(&entry, show_hidden) {
2784 results.push(entry_info);
2785 }
2786 }
2787 } else {
2788 let entries = std::fs::read_dir(base_path)
2789 .map_err(|e| HeliosError::ToolError(format!("Failed to read directory: {}", e)))?;
2790
2791 for entry in entries.filter_map(|e| e.ok()) {
2792 if let Some(entry_info) = format_entry(&entry, show_hidden) {
2793 results.push(entry_info);
2794 }
2795 }
2796 }
2797
2798 results.sort();
2799
2800 let mut output = format!("Directory listing for: {}\n\n", base_path);
2801 output.push_str(&format!("Total items: {}\n\n", results.len()));
2802
2803 for entry in results {
2804 output.push_str(&entry);
2805 output.push('\n');
2806 }
2807
2808 Ok(ToolResult::success(output))
2809 }
2810}
2811
2812fn format_walkdir_entry(entry: &walkdir::DirEntry, show_hidden: bool) -> Option<String> {
2814 let path = entry.path();
2815 let file_name = path.file_name()?.to_str()?;
2816
2817 if !show_hidden && file_name.starts_with('.') {
2819 return None;
2820 }
2821
2822 let metadata = entry.metadata().ok()?;
2823
2824 let file_type = if metadata.is_dir() { "DIR" } else { "FILE" };
2825 let size = metadata.len();
2826 let modified = metadata.modified().ok()?;
2827 let modified_dt = chrono::DateTime::<chrono::Local>::from(modified);
2828 let modified_str = modified_dt.format("%Y-%m-%d %H:%M:%S").to_string();
2829
2830 Some(format!(
2831 "{:4} {:>8} {} {}",
2832 file_type,
2833 size,
2834 modified_str,
2835 path.display()
2836 ))
2837}
2838
2839fn format_entry(entry: &std::fs::DirEntry, show_hidden: bool) -> Option<String> {
2841 let path = entry.path();
2842 let file_name = path.file_name()?.to_str()?;
2843
2844 if !show_hidden && file_name.starts_with('.') {
2846 return None;
2847 }
2848
2849 let metadata = entry.metadata().ok()?;
2850
2851 let file_type = if metadata.is_dir() { "DIR" } else { "FILE" };
2852 let size = metadata.len();
2853 let modified = metadata.modified().ok()?;
2854 let modified_dt = chrono::DateTime::<chrono::Local>::from(modified);
2855 let modified_str = modified_dt.format("%Y-%m-%d %H:%M:%S").to_string();
2856
2857 Some(format!(
2858 "{:4} {:>8} {} {}",
2859 file_type,
2860 size,
2861 modified_str,
2862 path.display()
2863 ))
2864}
2865
2866pub struct SystemInfoTool;
2868
2869#[async_trait]
2870impl Tool for SystemInfoTool {
2871 fn name(&self) -> &str {
2872 "system_info"
2873 }
2874
2875 fn description(&self) -> &str {
2876 "Retrieve system information including OS, CPU, memory, disk usage, and network interfaces."
2877 }
2878
2879 fn parameters(&self) -> HashMap<String, ToolParameter> {
2880 let mut params = HashMap::new();
2881 params.insert(
2882 "category".to_string(),
2883 ToolParameter {
2884 param_type: "string".to_string(),
2885 description:
2886 "Info category: 'all', 'os', 'cpu', 'memory', 'disk', 'network' (default: all)"
2887 .to_string(),
2888 required: Some(false),
2889 },
2890 );
2891 params
2892 }
2893
2894 async fn execute(&self, args: Value) -> Result<ToolResult> {
2895 let category = args
2896 .get("category")
2897 .and_then(|v| v.as_str())
2898 .unwrap_or("all");
2899
2900 let mut system = sysinfo::System::new_all();
2901 system.refresh_all();
2902
2903 let disks = sysinfo::Disks::new_with_refreshed_list();
2904 let networks = sysinfo::Networks::new_with_refreshed_list();
2905
2906 let mut output = String::new();
2907
2908 match category {
2909 "all" => {
2910 output.push_str(&get_os_info(&system));
2911 output.push_str(&get_cpu_info(&system));
2912 output.push_str(&get_memory_info(&system));
2913 output.push_str(&get_disk_info(&disks));
2914 output.push_str(&get_network_info(&networks));
2915 }
2916 "os" => output.push_str(&get_os_info(&system)),
2917 "cpu" => output.push_str(&get_cpu_info(&system)),
2918 "memory" => output.push_str(&get_memory_info(&system)),
2919 "disk" => output.push_str(&get_disk_info(&disks)),
2920 "network" => output.push_str(&get_network_info(&networks)),
2921 _ => {
2922 return Err(HeliosError::ToolError(format!(
2923 "Unknown category '{}'. Use: all, os, cpu, memory, disk, network",
2924 category
2925 )))
2926 }
2927 }
2928
2929 Ok(ToolResult::success(output))
2930 }
2931}
2932
2933fn get_os_info(_system: &sysinfo::System) -> String {
2935 let mut info = String::from("=== Operating System ===\n");
2936
2937 info.push_str(&format!("OS: {}\n", std::env::consts::OS));
2938 info.push_str(&format!("Architecture: {}\n", std::env::consts::ARCH));
2939 info.push_str(&format!("Family: {}\n", std::env::consts::FAMILY));
2940
2941 if let Ok(hostname) = hostname::get() {
2942 if let Some(hostname_str) = hostname.to_str() {
2943 info.push_str(&format!("Hostname: {}\n", hostname_str));
2944 }
2945 }
2946
2947 info.push_str(&format!("Uptime: {} seconds\n", sysinfo::System::uptime()));
2948 info.push('\n');
2949
2950 info
2951}
2952
2953fn get_cpu_info(system: &sysinfo::System) -> String {
2955 let mut info = String::from("=== CPU Information ===\n");
2956
2957 info.push_str(&format!(
2958 "Physical cores: {}\n",
2959 sysinfo::System::physical_core_count().unwrap_or(0)
2960 ));
2961 info.push_str(&format!("Logical cores: {}\n", system.cpus().len()));
2962
2963 for (i, cpu) in system.cpus().iter().enumerate() {
2964 if i >= 4 {
2965 info.push_str("... and more CPUs\n");
2967 break;
2968 }
2969 info.push_str(&format!("CPU {}: {:.1}% usage\n", i, cpu.cpu_usage()));
2970 }
2971
2972 info.push('\n');
2973 info
2974}
2975
2976fn get_memory_info(system: &sysinfo::System) -> String {
2978 let mut info = String::from("=== Memory Information ===\n");
2979
2980 let total_memory = system.total_memory();
2981 let used_memory = system.used_memory();
2982 let available_memory = system.available_memory();
2983
2984 info.push_str(&format!(
2985 "Total memory: {} MB\n",
2986 total_memory / 1024 / 1024
2987 ));
2988 info.push_str(&format!("Used memory: {} MB\n", used_memory / 1024 / 1024));
2989 info.push_str(&format!(
2990 "Available memory: {} MB\n",
2991 available_memory / 1024 / 1024
2992 ));
2993 info.push_str(&format!(
2994 "Memory usage: {:.1}%\n",
2995 (used_memory as f64 / total_memory as f64) * 100.0
2996 ));
2997
2998 info.push('\n');
2999 info
3000}
3001
3002fn get_disk_info(disks: &sysinfo::Disks) -> String {
3004 let mut info = String::from("=== Disk Information ===\n");
3005
3006 for disk in disks.list() {
3007 let total_space = disk.total_space();
3008 let available_space = disk.available_space();
3009 let used_space = total_space - available_space;
3010
3011 info.push_str(&format!("Mount point: {}\n", disk.mount_point().display()));
3012 info.push_str(&format!(
3013 "File system: {}\n",
3014 disk.file_system().to_string_lossy()
3015 ));
3016 info.push_str(&format!(
3017 "Total space: {} GB\n",
3018 total_space / 1024 / 1024 / 1024
3019 ));
3020 info.push_str(&format!(
3021 "Used space: {} GB\n",
3022 used_space / 1024 / 1024 / 1024
3023 ));
3024 info.push_str(&format!(
3025 "Available space: {} GB\n",
3026 available_space / 1024 / 1024 / 1024
3027 ));
3028 info.push_str(&format!(
3029 "Usage: {:.1}%\n\n",
3030 (used_space as f64 / total_space as f64) * 100.0
3031 ));
3032 }
3033
3034 info
3035}
3036
3037fn get_network_info(networks: &sysinfo::Networks) -> String {
3039 let mut info = String::from("=== Network Information ===\n");
3040
3041 for (interface_name, data) in networks.list() {
3042 info.push_str(&format!("Interface: {}\n", interface_name));
3043
3044 info.push_str(&format!("Received: {} bytes\n", data.received()));
3046 info.push_str(&format!("Transmitted: {} bytes\n", data.transmitted()));
3047 info.push('\n');
3048 }
3049
3050 info
3051}
3052
3053pub struct TextProcessorTool;
3055
3056#[async_trait]
3057impl Tool for TextProcessorTool {
3058 fn name(&self) -> &str {
3059 "text_processor"
3060 }
3061
3062 fn description(&self) -> &str {
3063 "Process and manipulate text with operations like search, replace, split, join, count, and format."
3064 }
3065
3066 fn parameters(&self) -> HashMap<String, ToolParameter> {
3067 let mut params = HashMap::new();
3068 params.insert(
3069 "operation".to_string(),
3070 ToolParameter {
3071 param_type: "string".to_string(),
3072 description: "Operation: 'search', 'replace', 'split', 'join', 'count', 'uppercase', 'lowercase', 'trim', 'lines', 'words'".to_string(),
3073 required: Some(true),
3074 },
3075 );
3076 params.insert(
3077 "text".to_string(),
3078 ToolParameter {
3079 param_type: "string".to_string(),
3080 description: "Input text for processing".to_string(),
3081 required: Some(true),
3082 },
3083 );
3084 params.insert(
3085 "pattern".to_string(),
3086 ToolParameter {
3087 param_type: "string".to_string(),
3088 description: "Search pattern for search/replace/split operations".to_string(),
3089 required: Some(false),
3090 },
3091 );
3092 params.insert(
3093 "replacement".to_string(),
3094 ToolParameter {
3095 param_type: "string".to_string(),
3096 description: "Replacement text for replace operation".to_string(),
3097 required: Some(false),
3098 },
3099 );
3100 params.insert(
3101 "separator".to_string(),
3102 ToolParameter {
3103 param_type: "string".to_string(),
3104 description: "Separator for join/split operations (default: space)".to_string(),
3105 required: Some(false),
3106 },
3107 );
3108 params.insert(
3109 "case_sensitive".to_string(),
3110 ToolParameter {
3111 param_type: "boolean".to_string(),
3112 description: "Case sensitive search (default: true)".to_string(),
3113 required: Some(false),
3114 },
3115 );
3116 params
3117 }
3118
3119 async fn execute(&self, args: Value) -> Result<ToolResult> {
3120 let operation = args
3121 .get("operation")
3122 .and_then(|v| v.as_str())
3123 .ok_or_else(|| HeliosError::ToolError("Missing 'operation' parameter".to_string()))?;
3124
3125 let text = args
3126 .get("text")
3127 .and_then(|v| v.as_str())
3128 .ok_or_else(|| HeliosError::ToolError("Missing 'text' parameter".to_string()))?;
3129
3130 match operation {
3131 "search" => {
3132 let pattern = args
3133 .get("pattern")
3134 .and_then(|v| v.as_str())
3135 .ok_or_else(|| HeliosError::ToolError("Missing 'pattern' parameter for search operation".to_string()))?;
3136
3137 let case_sensitive = args.get("case_sensitive").and_then(|v| v.as_bool()).unwrap_or(true);
3138
3139 let regex = if case_sensitive {
3140 regex::Regex::new(pattern)
3141 } else {
3142 regex::RegexBuilder::new(pattern).case_insensitive(true).build()
3143 }
3144 .map_err(|e| HeliosError::ToolError(format!("Invalid regex pattern: {}", e)))?;
3145
3146 let matches: Vec<(usize, &str)> = regex
3147 .find_iter(text)
3148 .map(|m| (m.start(), m.as_str()))
3149 .collect();
3150
3151 let result = if matches.is_empty() {
3152 "No matches found".to_string()
3153 } else {
3154 let mut output = format!("Found {} match(es):\n", matches.len());
3155 for (i, (pos, match_text)) in matches.iter().enumerate() {
3156 output.push_str(&format!("{}. Position {}: '{}'\n", i + 1, pos, match_text));
3157 }
3158 output
3159 };
3160
3161 Ok(ToolResult::success(result))
3162 }
3163 "replace" => {
3164 let pattern = args
3165 .get("pattern")
3166 .and_then(|v| v.as_str())
3167 .ok_or_else(|| HeliosError::ToolError("Missing 'pattern' parameter for replace operation".to_string()))?;
3168
3169 let replacement = args
3170 .get("replacement")
3171 .and_then(|v| v.as_str())
3172 .unwrap_or("");
3173
3174 let case_sensitive = args.get("case_sensitive").and_then(|v| v.as_bool()).unwrap_or(true);
3175
3176 let regex = if case_sensitive {
3177 regex::Regex::new(pattern)
3178 } else {
3179 regex::RegexBuilder::new(pattern).case_insensitive(true).build()
3180 }
3181 .map_err(|e| HeliosError::ToolError(format!("Invalid regex pattern: {}", e)))?;
3182
3183 let result = regex.replace_all(text, replacement).to_string();
3184 let count = regex.find_iter(text).count();
3185
3186 Ok(ToolResult::success(format!(
3187 "Replaced {} occurrence(s):\n\n{}",
3188 count, result
3189 )))
3190 }
3191 "split" => {
3192 let separator = args
3193 .get("separator")
3194 .and_then(|v| v.as_str())
3195 .unwrap_or(" ");
3196
3197 let parts: Vec<&str> = text.split(separator).collect();
3198 let result = format!(
3199 "Split into {} parts:\n{}",
3200 parts.len(),
3201 parts.iter().enumerate()
3202 .map(|(i, part)| format!("{}. '{}'", i + 1, part))
3203 .collect::<Vec<_>>()
3204 .join("\n")
3205 );
3206
3207 Ok(ToolResult::success(result))
3208 }
3209 "join" => {
3210 let separator = args
3211 .get("separator")
3212 .and_then(|v| v.as_str())
3213 .unwrap_or(" ");
3214
3215 let lines: Vec<&str> = text.lines().collect();
3216 let result = lines.join(separator);
3217
3218 Ok(ToolResult::success(format!(
3219 "Joined {} lines with '{}':\n{}",
3220 lines.len(), separator, result
3221 )))
3222 }
3223 "count" => {
3224 let chars = text.chars().count();
3225 let bytes = text.len();
3226 let lines = text.lines().count();
3227 let words = text.split_whitespace().count();
3228
3229 Ok(ToolResult::success(format!(
3230 "Text statistics:\nCharacters: {}\nBytes: {}\nLines: {}\nWords: {}",
3231 chars, bytes, lines, words
3232 )))
3233 }
3234 "uppercase" => {
3235 Ok(ToolResult::success(text.to_uppercase()))
3236 }
3237 "lowercase" => {
3238 Ok(ToolResult::success(text.to_lowercase()))
3239 }
3240 "trim" => {
3241 Ok(ToolResult::success(text.trim().to_string()))
3242 }
3243 "lines" => {
3244 let lines: Vec<String> = text.lines()
3245 .enumerate()
3246 .map(|(i, line)| format!("{:4}: {}", i + 1, line))
3247 .collect();
3248
3249 Ok(ToolResult::success(format!(
3250 "Text with line numbers ({} lines):\n{}",
3251 lines.len(),
3252 lines.join("\n")
3253 )))
3254 }
3255 "words" => {
3256 let words: Vec<String> = text.split_whitespace()
3257 .enumerate()
3258 .map(|(i, word)| format!("{:4}: {}", i + 1, word))
3259 .collect();
3260
3261 Ok(ToolResult::success(format!(
3262 "Words ({} total):\n{}",
3263 words.len(),
3264 words.iter()
3265 .collect::<Vec<_>>()
3266 .chunks(10)
3267 .enumerate()
3268 .map(|(chunk_i, chunk)| {
3269 format!("Line {}: {}", chunk_i + 1,
3270 chunk.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(" "))
3271 })
3272 .collect::<Vec<_>>()
3273 .join("\n")
3274 )))
3275 }
3276 _ => Err(HeliosError::ToolError(format!(
3277 "Unknown operation '{}'. Valid operations: search, replace, split, join, count, uppercase, lowercase, trim, lines, words",
3278 operation
3279 ))),
3280 }
3281 }
3282}
3283
3284#[cfg(test)]
3285mod tests {
3286 use super::*;
3287 use serde_json::json;
3288
3289 #[test]
3291 fn test_tool_result_success() {
3292 let result = ToolResult::success("test output");
3293 assert!(result.success);
3294 assert_eq!(result.output, "test output");
3295 }
3296
3297 #[tokio::test]
3299 async fn test_file_search_tool_glob_pattern_precompiled_regex() {
3300 use std::time::{SystemTime, UNIX_EPOCH};
3301 let base_tmp = std::env::temp_dir();
3302 let pid = std::process::id();
3303 let nanos = SystemTime::now()
3304 .duration_since(UNIX_EPOCH)
3305 .unwrap()
3306 .as_nanos();
3307 let test_dir = base_tmp.join(format!("helios_fs_test_{}_{}", pid, nanos));
3308 std::fs::create_dir_all(&test_dir).unwrap();
3309
3310 let file_rs = test_dir.join("a.rs");
3312 let file_txt = test_dir.join("b.txt");
3313 let subdir = test_dir.join("subdir");
3314 std::fs::create_dir_all(&subdir).unwrap();
3315 let file_sub_rs = subdir.join("mod.rs");
3316 std::fs::write(&file_rs, "fn main() {}\n").unwrap();
3317 std::fs::write(&file_txt, "hello\n").unwrap();
3318 std::fs::write(&file_sub_rs, "pub fn x() {}\n").unwrap();
3319
3320 let tool = FileSearchTool;
3322 let args = json!({
3323 "path": test_dir.to_string_lossy(),
3324 "pattern": "*.rs",
3325 "max_results": 50
3326 });
3327 let result = tool.execute(args).await.unwrap();
3328 assert!(result.success);
3329 let out = result.output;
3330 assert!(out.contains(&file_rs.to_string_lossy().to_string()));
3332 assert!(out.contains(&file_sub_rs.to_string_lossy().to_string()));
3333 assert!(!out.contains(&file_txt.to_string_lossy().to_string()));
3335
3336 let _ = std::fs::remove_dir_all(&test_dir);
3338 }
3339
3340 #[tokio::test]
3342 async fn test_file_search_tool_invalid_pattern_fallback_contains() {
3343 use std::time::{SystemTime, UNIX_EPOCH};
3344 let base_tmp = std::env::temp_dir();
3345 let pid = std::process::id();
3346 let nanos = SystemTime::now()
3347 .duration_since(UNIX_EPOCH)
3348 .unwrap()
3349 .as_nanos();
3350 let test_dir = base_tmp.join(format!("helios_fs_test_invalid_{}_{}", pid, nanos));
3351 std::fs::create_dir_all(&test_dir).unwrap();
3352
3353 let special = test_dir.join("foo(bar).txt");
3355 std::fs::write(&special, "content\n").unwrap();
3356
3357 let tool = FileSearchTool;
3358 let args = json!({
3359 "path": test_dir.to_string_lossy(),
3360 "pattern": "(",
3361 "max_results": 50
3362 });
3363 let result = tool.execute(args).await.unwrap();
3364 assert!(result.success);
3365 let out = result.output;
3366 assert!(out.contains(&special.to_string_lossy().to_string()));
3367
3368 let _ = std::fs::remove_dir_all(&test_dir);
3370 }
3371
3372 #[test]
3374 fn test_tool_result_error() {
3375 let result = ToolResult::error("test error");
3376 assert!(!result.success);
3377 assert_eq!(result.output, "test error");
3378 }
3379
3380 #[tokio::test]
3382 async fn test_calculator_tool() {
3383 let tool = CalculatorTool;
3384 assert_eq!(tool.name(), "calculator");
3385 assert_eq!(
3386 tool.description(),
3387 "Perform basic arithmetic operations. Supports +, -, *, / operations."
3388 );
3389
3390 let args = json!({"expression": "2 + 2"});
3391 let result = tool.execute(args).await.unwrap();
3392 assert!(result.success);
3393 assert_eq!(result.output, "4");
3394 }
3395
3396 #[tokio::test]
3398 async fn test_calculator_tool_multiplication() {
3399 let tool = CalculatorTool;
3400 let args = json!({"expression": "3 * 4"});
3401 let result = tool.execute(args).await.unwrap();
3402 assert!(result.success);
3403 assert_eq!(result.output, "12");
3404 }
3405
3406 #[tokio::test]
3408 async fn test_calculator_tool_division() {
3409 let tool = CalculatorTool;
3410 let args = json!({"expression": "8 / 2"});
3411 let result = tool.execute(args).await.unwrap();
3412 assert!(result.success);
3413 assert_eq!(result.output, "4");
3414 }
3415
3416 #[tokio::test]
3418 async fn test_calculator_tool_division_by_zero() {
3419 let tool = CalculatorTool;
3420 let args = json!({"expression": "8 / 0"});
3421 let result = tool.execute(args).await;
3422 assert!(result.is_err());
3423 }
3424
3425 #[tokio::test]
3427 async fn test_calculator_tool_invalid_expression() {
3428 let tool = CalculatorTool;
3429 let args = json!({"expression": "invalid"});
3430 let result = tool.execute(args).await;
3431 assert!(result.is_err());
3432 }
3433
3434 #[tokio::test]
3436 async fn test_echo_tool() {
3437 let tool = EchoTool;
3438 assert_eq!(tool.name(), "echo");
3439 assert_eq!(tool.description(), "Echo back the provided message.");
3440
3441 let args = json!({"message": "Hello, world!"});
3442 let result = tool.execute(args).await.unwrap();
3443 assert!(result.success);
3444 assert_eq!(result.output, "Echo: Hello, world!");
3445 }
3446
3447 #[tokio::test]
3449 async fn test_echo_tool_missing_parameter() {
3450 let tool = EchoTool;
3451 let args = json!({});
3452 let result = tool.execute(args).await;
3453 assert!(result.is_err());
3454 }
3455
3456 #[test]
3458 fn test_tool_registry_new() {
3459 let registry = ToolRegistry::new();
3460 assert!(registry.tools.is_empty());
3461 }
3462
3463 #[tokio::test]
3465 async fn test_tool_registry_register_and_get() {
3466 let mut registry = ToolRegistry::new();
3467 registry.register(Box::new(CalculatorTool));
3468
3469 let tool = registry.get("calculator");
3470 assert!(tool.is_some());
3471 assert_eq!(tool.unwrap().name(), "calculator");
3472 }
3473
3474 #[tokio::test]
3476 async fn test_tool_registry_execute() {
3477 let mut registry = ToolRegistry::new();
3478 registry.register(Box::new(CalculatorTool));
3479
3480 let args = json!({"expression": "5 * 6"});
3481 let result = registry.execute("calculator", args).await.unwrap();
3482 assert!(result.success);
3483 assert_eq!(result.output, "30");
3484 }
3485
3486 #[tokio::test]
3488 async fn test_tool_registry_execute_nonexistent_tool() {
3489 let registry = ToolRegistry::new();
3490 let args = json!({"expression": "5 * 6"});
3491 let result = registry.execute("nonexistent", args).await;
3492 assert!(result.is_err());
3493 }
3494
3495 #[test]
3497 fn test_tool_registry_get_definitions() {
3498 let mut registry = ToolRegistry::new();
3499 registry.register(Box::new(CalculatorTool));
3500 registry.register(Box::new(EchoTool));
3501
3502 let definitions = registry.get_definitions();
3503 assert_eq!(definitions.len(), 2);
3504
3505 let names: Vec<String> = definitions
3507 .iter()
3508 .map(|d| d.function.name.clone())
3509 .collect();
3510 assert!(names.contains(&"calculator".to_string()));
3511 assert!(names.contains(&"echo".to_string()));
3512 }
3513
3514 #[test]
3516 fn test_tool_registry_list_tools() {
3517 let mut registry = ToolRegistry::new();
3518 registry.register(Box::new(CalculatorTool));
3519 registry.register(Box::new(EchoTool));
3520
3521 let tools = registry.list_tools();
3522 assert_eq!(tools.len(), 2);
3523 assert!(tools.contains(&"calculator".to_string()));
3524 assert!(tools.contains(&"echo".to_string()));
3525 }
3526
3527 #[tokio::test]
3529 async fn test_memory_db_set_and_get() {
3530 let tool = MemoryDBTool::new();
3531
3532 let set_args = json!({
3534 "operation": "set",
3535 "key": "name",
3536 "value": "Alice"
3537 });
3538 let result = tool.execute(set_args).await.unwrap();
3539 assert!(result.success);
3540 assert!(result.output.contains("Set 'name' = 'Alice'"));
3541
3542 let get_args = json!({
3544 "operation": "get",
3545 "key": "name"
3546 });
3547 let result = tool.execute(get_args).await.unwrap();
3548 assert!(result.success);
3549 assert!(result.output.contains("Alice"));
3550 }
3551
3552 #[tokio::test]
3554 async fn test_memory_db_delete() {
3555 let tool = MemoryDBTool::new();
3556
3557 let set_args = json!({
3559 "operation": "set",
3560 "key": "temp",
3561 "value": "data"
3562 });
3563 tool.execute(set_args).await.unwrap();
3564
3565 let delete_args = json!({
3567 "operation": "delete",
3568 "key": "temp"
3569 });
3570 let result = tool.execute(delete_args).await.unwrap();
3571 assert!(result.success);
3572 assert!(result.output.contains("Deleted 'temp'"));
3573
3574 let get_args = json!({
3576 "operation": "get",
3577 "key": "temp"
3578 });
3579 let result = tool.execute(get_args).await.unwrap();
3580 assert!(!result.success);
3581 assert!(result.output.contains("not found"));
3582 }
3583
3584 #[tokio::test]
3586 async fn test_memory_db_exists() {
3587 let tool = MemoryDBTool::new();
3588
3589 let exists_args = json!({
3591 "operation": "exists",
3592 "key": "test"
3593 });
3594 let result = tool.execute(exists_args).await.unwrap();
3595 assert!(result.success);
3596 assert!(result.output.contains("false"));
3597
3598 let set_args = json!({
3600 "operation": "set",
3601 "key": "test",
3602 "value": "value"
3603 });
3604 tool.execute(set_args).await.unwrap();
3605
3606 let exists_args = json!({
3608 "operation": "exists",
3609 "key": "test"
3610 });
3611 let result = tool.execute(exists_args).await.unwrap();
3612 assert!(result.success);
3613 assert!(result.output.contains("true"));
3614 }
3615
3616 #[tokio::test]
3618 async fn test_memory_db_list() {
3619 let tool = MemoryDBTool::new();
3620
3621 let list_args = json!({
3623 "operation": "list"
3624 });
3625 let result = tool.execute(list_args).await.unwrap();
3626 assert!(result.success);
3627 assert!(result.output.contains("empty"));
3628
3629 tool.execute(json!({
3631 "operation": "set",
3632 "key": "key1",
3633 "value": "value1"
3634 }))
3635 .await
3636 .unwrap();
3637
3638 tool.execute(json!({
3639 "operation": "set",
3640 "key": "key2",
3641 "value": "value2"
3642 }))
3643 .await
3644 .unwrap();
3645
3646 let list_args = json!({
3648 "operation": "list"
3649 });
3650 let result = tool.execute(list_args).await.unwrap();
3651 assert!(result.success);
3652 assert!(result.output.contains("2 items"));
3653 assert!(result.output.contains("key1"));
3654 assert!(result.output.contains("key2"));
3655 }
3656
3657 #[tokio::test]
3659 async fn test_memory_db_clear() {
3660 let tool = MemoryDBTool::new();
3661
3662 tool.execute(json!({
3664 "operation": "set",
3665 "key": "key1",
3666 "value": "value1"
3667 }))
3668 .await
3669 .unwrap();
3670
3671 tool.execute(json!({
3672 "operation": "set",
3673 "key": "key2",
3674 "value": "value2"
3675 }))
3676 .await
3677 .unwrap();
3678
3679 let clear_args = json!({
3681 "operation": "clear"
3682 });
3683 let result = tool.execute(clear_args).await.unwrap();
3684 assert!(result.success);
3685 assert!(result.output.contains("2 items removed"));
3686
3687 let list_args = json!({
3689 "operation": "list"
3690 });
3691 let result = tool.execute(list_args).await.unwrap();
3692 assert!(result.output.contains("empty"));
3693 }
3694
3695 #[tokio::test]
3697 async fn test_memory_db_invalid_operation() {
3698 let tool = MemoryDBTool::new();
3699
3700 let args = json!({
3701 "operation": "invalid_op"
3702 });
3703 let result = tool.execute(args).await;
3704 assert!(result.is_err());
3705 }
3706
3707 #[tokio::test]
3709 async fn test_memory_db_shared_instance() {
3710 use std::sync::{Arc, Mutex};
3711
3712 let shared_db = Arc::new(Mutex::new(HashMap::new()));
3714 let tool1 = MemoryDBTool::with_shared_db(shared_db.clone());
3715 let tool2 = MemoryDBTool::with_shared_db(shared_db.clone());
3716
3717 tool1
3719 .execute(json!({
3720 "operation": "set",
3721 "key": "shared",
3722 "value": "data"
3723 }))
3724 .await
3725 .unwrap();
3726
3727 let result = tool2
3729 .execute(json!({
3730 "operation": "get",
3731 "key": "shared"
3732 }))
3733 .await
3734 .unwrap();
3735 assert!(result.success);
3736 assert!(result.output.contains("data"));
3737 }
3738
3739 #[tokio::test]
3741 async fn test_web_scraper_tool() {
3742 let tool = WebScraperTool;
3743 assert_eq!(tool.name(), "web_scraper");
3744
3745 let args = json!({});
3747 let result = tool.execute(args).await;
3748 assert!(result.is_err());
3749
3750 }
3753
3754 #[tokio::test]
3756 async fn test_json_parser_tool_parse() {
3757 let tool = JsonParserTool;
3758 assert_eq!(tool.name(), "json_parser");
3759
3760 let args = json!({
3761 "operation": "parse",
3762 "json": "{\"key\": \"value\", \"number\": 42}"
3763 });
3764 let result = tool.execute(args).await.unwrap();
3765 assert!(result.success);
3766 assert!(result.output.contains("✓ JSON parsed successfully"));
3767 assert!(result.output.contains("Type: object"));
3768 }
3769
3770 #[tokio::test]
3772 async fn test_json_parser_tool_stringify() {
3773 let tool = JsonParserTool;
3774
3775 let args = json!({
3776 "operation": "stringify",
3777 "json": " {\"key\": \"value\"} "
3778 });
3779 let result = tool.execute(args).await.unwrap();
3780 assert!(result.success);
3781 assert!(result.output.contains("key"));
3782 assert!(result.output.contains("value"));
3783 }
3784
3785 #[tokio::test]
3787 async fn test_json_parser_tool_get_value() {
3788 let tool = JsonParserTool;
3789
3790 let args = json!({
3791 "operation": "get_value",
3792 "json": "{\"user\": {\"name\": \"Alice\", \"age\": 30}}",
3793 "path": "user.name"
3794 });
3795 let result = tool.execute(args).await.unwrap();
3796 assert!(result.success);
3797 assert!(result.output.contains("Alice"));
3798 }
3799
3800 #[tokio::test]
3802 async fn test_json_parser_tool_validate() {
3803 let tool = JsonParserTool;
3804
3805 let args = json!({
3807 "operation": "validate",
3808 "json": "{\"valid\": true}"
3809 });
3810 let result = tool.execute(args).await.unwrap();
3811 assert!(result.success);
3812 assert!(result.output.contains("✓ JSON is valid"));
3813
3814 let args = json!({
3816 "operation": "validate",
3817 "json": "{\"invalid\": }"
3818 });
3819 let result = tool.execute(args).await;
3820 assert!(result.is_ok()); assert!(result.unwrap().output.contains("✗ JSON validation failed"));
3822 }
3823
3824 #[tokio::test]
3826 async fn test_timestamp_tool_now() {
3827 let tool = TimestampTool;
3828 assert_eq!(tool.name(), "timestamp");
3829
3830 let args = json!({
3831 "operation": "now"
3832 });
3833 let result = tool.execute(args).await.unwrap();
3834 assert!(result.success);
3835 assert!(result.output.contains("Current time"));
3836 assert!(result.output.contains("Unix timestamp"));
3837 assert!(result.output.contains("RFC3339"));
3838 }
3839
3840 #[tokio::test]
3842 async fn test_timestamp_tool_format() {
3843 let tool = TimestampTool;
3844
3845 let args = json!({
3846 "operation": "format",
3847 "timestamp": "1640995200", "format": "%Y-%m-%d"
3849 });
3850 let result = tool.execute(args).await.unwrap();
3851 assert!(result.success);
3852 assert!(result.output.contains("2022-01-01"));
3853 }
3854
3855 #[tokio::test]
3857 async fn test_timestamp_tool_add() {
3858 let tool = TimestampTool;
3859
3860 let args = json!({
3861 "operation": "add",
3862 "timestamp": "2022-01-01T00:00:00Z",
3863 "unit": "days",
3864 "amount": 5
3865 });
3866 let result = tool.execute(args).await.unwrap();
3867 assert!(result.success);
3868 assert!(result.output.contains("Added 5 days"));
3869 }
3870
3871 #[tokio::test]
3873 async fn test_timestamp_tool_diff() {
3874 let tool = TimestampTool;
3875
3876 let args = json!({
3877 "operation": "diff",
3878 "timestamp1": "2022-01-01T00:00:00Z",
3879 "timestamp2": "2022-01-02T00:00:00Z"
3880 });
3881 let result = tool.execute(args).await.unwrap();
3882 assert!(result.success);
3883 assert!(result.output.contains("86400 seconds")); }
3885
3886 #[tokio::test]
3888 async fn test_file_io_tool_read() {
3889 let tool = FileIOTool;
3890 assert_eq!(tool.name(), "file_io");
3891
3892 let temp_file = tempfile::NamedTempFile::new().unwrap();
3894 let file_path = temp_file.path().to_string_lossy().to_string();
3895 std::fs::write(&file_path, "Hello, World!").unwrap();
3896
3897 let args = json!({
3898 "operation": "read",
3899 "path": file_path
3900 });
3901 let result = tool.execute(args).await.unwrap();
3902 assert!(result.success);
3903 assert!(result.output.contains("Hello, World!"));
3904 }
3905
3906 #[tokio::test]
3908 async fn test_file_io_tool_write() {
3909 let tool = FileIOTool;
3910
3911 let temp_file = tempfile::NamedTempFile::new().unwrap();
3912 let file_path = temp_file.path().to_string_lossy().to_string();
3913
3914 let args = json!({
3915 "operation": "write",
3916 "path": file_path,
3917 "content": "Test content"
3918 });
3919 let result = tool.execute(args).await.unwrap();
3920 assert!(result.success);
3921 assert!(result.output.contains("Wrote 12 bytes"));
3922
3923 let content = std::fs::read_to_string(&file_path).unwrap();
3925 assert_eq!(content, "Test content");
3926 }
3927
3928 #[tokio::test]
3930 async fn test_file_io_tool_exists() {
3931 let tool = FileIOTool;
3932
3933 let temp_file = tempfile::NamedTempFile::new().unwrap();
3934 let file_path = temp_file.path().to_string_lossy().to_string();
3935
3936 let args = json!({
3937 "operation": "exists",
3938 "path": file_path
3939 });
3940 let result = tool.execute(args).await.unwrap();
3941 assert!(result.success);
3942 assert!(result.output.contains("exists: true"));
3943 assert!(result.output.contains("(file)"));
3944 }
3945
3946 #[tokio::test]
3948 async fn test_file_io_tool_safe_delete() {
3949 let tool = FileIOTool;
3950
3951 let temp_dir = tempfile::tempdir().unwrap();
3953 let dir_path = temp_dir.path().to_string_lossy().to_string();
3954
3955 let args = json!({
3957 "operation": "delete",
3958 "path": dir_path
3959 });
3960 let result = tool.execute(args).await.unwrap();
3961 assert!(result.success);
3962 assert!(result.output.contains("✓ Deleted directory"));
3963
3964 let temp_dir2 = tempfile::tempdir().unwrap();
3966 let dir_path2 = temp_dir2.path().to_string_lossy().to_string();
3967 let file_path = temp_dir2.path().join("test.txt");
3968 std::fs::write(&file_path, "test content").unwrap();
3969
3970 let args2 = json!({
3972 "operation": "delete",
3973 "path": dir_path2
3974 });
3975 let result2 = tool.execute(args2).await;
3976 assert!(result2.is_err()); assert!(result2.unwrap_err().to_string().contains("must be empty"));
3978
3979 let args3 = json!({
3981 "operation": "delete",
3982 "path": temp_dir2.path().to_string_lossy(),
3983 "recursive": true
3984 });
3985 let result3 = tool.execute(args3).await.unwrap();
3986 assert!(result3.success);
3987 assert!(result3.output.contains("✓ Deleted directory recursively:"));
3988 }
3989
3990 #[tokio::test]
3992 async fn test_shell_command_tool_safe() {
3993 let tool = ShellCommandTool;
3994 assert_eq!(tool.name(), "shell_command");
3995
3996 let args = json!({
3998 "command": "echo 'hello world'"
3999 });
4000 let result = tool.execute(args).await.unwrap();
4001 assert!(result.success);
4002 assert!(result.output.contains("hello world"));
4003 }
4004
4005 #[tokio::test]
4007 async fn test_shell_command_tool_blocked() {
4008 let tool = ShellCommandTool;
4009
4010 let args = json!({
4011 "command": "rm -rf /"
4012 });
4013 let result = tool.execute(args).await;
4014 assert!(result.is_err());
4015 assert!(result.unwrap_err().to_string().contains("Command blocked"));
4016 }
4017
4018 #[tokio::test]
4020 async fn test_http_request_tool_missing_method() {
4021 let tool = HttpRequestTool;
4022 assert_eq!(tool.name(), "http_request");
4023
4024 let args = json!({
4025 "url": "https://httpbin.org/get"
4026 });
4027 let result = tool.execute(args).await;
4028 assert!(result.is_err());
4029 }
4030
4031 #[tokio::test]
4033 async fn test_file_list_tool() {
4034 let tool = FileListTool;
4035 assert_eq!(tool.name(), "file_list");
4036
4037 let args = json!({
4038 "path": ".",
4039 "show_hidden": false
4040 });
4041 let result = tool.execute(args).await.unwrap();
4042 assert!(result.success);
4043 assert!(result.output.contains("Directory listing"));
4044 assert!(result.output.contains("Total items"));
4045 }
4046
4047 #[tokio::test]
4049 async fn test_system_info_tool() {
4050 let tool = SystemInfoTool;
4051 assert_eq!(tool.name(), "system_info");
4052
4053 let args = json!({
4054 "category": "os"
4055 });
4056 let result = tool.execute(args).await.unwrap();
4057 assert!(result.success);
4058 assert!(result.output.contains("Operating System"));
4059 assert!(result.output.contains("OS:"));
4060 }
4061
4062 #[tokio::test]
4064 async fn test_text_processor_tool_search() {
4065 let tool = TextProcessorTool;
4066 assert_eq!(tool.name(), "text_processor");
4067
4068 let args = json!({
4069 "operation": "search",
4070 "text": "Hello world, hello universe",
4071 "pattern": "hello",
4072 "case_sensitive": false
4073 });
4074 let result = tool.execute(args).await.unwrap();
4075 assert!(result.success);
4076 assert!(result.output.contains("Found 2 match(es)"));
4077 }
4078
4079 #[tokio::test]
4081 async fn test_text_processor_tool_replace() {
4082 let tool = TextProcessorTool;
4083
4084 let args = json!({
4085 "operation": "replace",
4086 "text": "Hello world",
4087 "pattern": "world",
4088 "replacement": "universe"
4089 });
4090 let result = tool.execute(args).await.unwrap();
4091 assert!(result.success);
4092 assert!(result.output.contains("Replaced 1 occurrence"));
4093 assert!(result.output.contains("Hello universe"));
4094 }
4095
4096 #[tokio::test]
4098 async fn test_text_processor_tool_count() {
4099 let tool = TextProcessorTool;
4100
4101 let args = json!({
4102 "operation": "count",
4103 "text": "Hello\nworld"
4104 });
4105 let result = tool.execute(args).await.unwrap();
4106 assert!(result.success);
4107 assert!(result.output.contains("Characters: 11"));
4108 assert!(result.output.contains("Lines: 2"));
4109 assert!(result.output.contains("Words: 2"));
4110 }
4111
4112 #[tokio::test]
4114 async fn test_text_processor_tool_uppercase() {
4115 let tool = TextProcessorTool;
4116
4117 let args = json!({
4118 "operation": "uppercase",
4119 "text": "hello world"
4120 });
4121 let result = tool.execute(args).await.unwrap();
4122 assert!(result.success);
4123 assert_eq!(result.output, "HELLO WORLD");
4124 }
4125
4126 #[tokio::test]
4128 async fn test_text_processor_tool_trim() {
4129 let tool = TextProcessorTool;
4130
4131 let args = json!({
4132 "operation": "trim",
4133 "text": " hello world "
4134 });
4135 let result = tool.execute(args).await.unwrap();
4136 assert!(result.success);
4137 assert_eq!(result.output, "hello world");
4138 }
4139}