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
1486#[cfg(test)]
1487mod tests {
1488 use super::*;
1489 use serde_json::json;
1490
1491 #[test]
1493 fn test_tool_result_success() {
1494 let result = ToolResult::success("test output");
1495 assert!(result.success);
1496 assert_eq!(result.output, "test output");
1497 }
1498
1499 #[tokio::test]
1501 async fn test_file_search_tool_glob_pattern_precompiled_regex() {
1502 use std::time::{SystemTime, UNIX_EPOCH};
1503 let base_tmp = std::env::temp_dir();
1504 let pid = std::process::id();
1505 let nanos = SystemTime::now()
1506 .duration_since(UNIX_EPOCH)
1507 .unwrap()
1508 .as_nanos();
1509 let test_dir = base_tmp.join(format!("helios_fs_test_{}_{}", pid, nanos));
1510 std::fs::create_dir_all(&test_dir).unwrap();
1511
1512 let file_rs = test_dir.join("a.rs");
1514 let file_txt = test_dir.join("b.txt");
1515 let subdir = test_dir.join("subdir");
1516 std::fs::create_dir_all(&subdir).unwrap();
1517 let file_sub_rs = subdir.join("mod.rs");
1518 std::fs::write(&file_rs, "fn main() {}\n").unwrap();
1519 std::fs::write(&file_txt, "hello\n").unwrap();
1520 std::fs::write(&file_sub_rs, "pub fn x() {}\n").unwrap();
1521
1522 let tool = FileSearchTool;
1524 let args = json!({
1525 "path": test_dir.to_string_lossy(),
1526 "pattern": "*.rs",
1527 "max_results": 50
1528 });
1529 let result = tool.execute(args).await.unwrap();
1530 assert!(result.success);
1531 let out = result.output;
1532 assert!(out.contains(&file_rs.to_string_lossy().to_string()));
1534 assert!(out.contains(&file_sub_rs.to_string_lossy().to_string()));
1535 assert!(!out.contains(&file_txt.to_string_lossy().to_string()));
1537
1538 let _ = std::fs::remove_dir_all(&test_dir);
1540 }
1541
1542 #[tokio::test]
1544 async fn test_file_search_tool_invalid_pattern_fallback_contains() {
1545 use std::time::{SystemTime, UNIX_EPOCH};
1546 let base_tmp = std::env::temp_dir();
1547 let pid = std::process::id();
1548 let nanos = SystemTime::now()
1549 .duration_since(UNIX_EPOCH)
1550 .unwrap()
1551 .as_nanos();
1552 let test_dir = base_tmp.join(format!("helios_fs_test_invalid_{}_{}", pid, nanos));
1553 std::fs::create_dir_all(&test_dir).unwrap();
1554
1555 let special = test_dir.join("foo(bar).txt");
1557 std::fs::write(&special, "content\n").unwrap();
1558
1559 let tool = FileSearchTool;
1560 let args = json!({
1561 "path": test_dir.to_string_lossy(),
1562 "pattern": "(",
1563 "max_results": 50
1564 });
1565 let result = tool.execute(args).await.unwrap();
1566 assert!(result.success);
1567 let out = result.output;
1568 assert!(out.contains(&special.to_string_lossy().to_string()));
1569
1570 let _ = std::fs::remove_dir_all(&test_dir);
1572 }
1573
1574 #[test]
1576 fn test_tool_result_error() {
1577 let result = ToolResult::error("test error");
1578 assert!(!result.success);
1579 assert_eq!(result.output, "test error");
1580 }
1581
1582 #[tokio::test]
1584 async fn test_calculator_tool() {
1585 let tool = CalculatorTool;
1586 assert_eq!(tool.name(), "calculator");
1587 assert_eq!(
1588 tool.description(),
1589 "Perform basic arithmetic operations. Supports +, -, *, / operations."
1590 );
1591
1592 let args = json!({"expression": "2 + 2"});
1593 let result = tool.execute(args).await.unwrap();
1594 assert!(result.success);
1595 assert_eq!(result.output, "4");
1596 }
1597
1598 #[tokio::test]
1600 async fn test_calculator_tool_multiplication() {
1601 let tool = CalculatorTool;
1602 let args = json!({"expression": "3 * 4"});
1603 let result = tool.execute(args).await.unwrap();
1604 assert!(result.success);
1605 assert_eq!(result.output, "12");
1606 }
1607
1608 #[tokio::test]
1610 async fn test_calculator_tool_division() {
1611 let tool = CalculatorTool;
1612 let args = json!({"expression": "8 / 2"});
1613 let result = tool.execute(args).await.unwrap();
1614 assert!(result.success);
1615 assert_eq!(result.output, "4");
1616 }
1617
1618 #[tokio::test]
1620 async fn test_calculator_tool_division_by_zero() {
1621 let tool = CalculatorTool;
1622 let args = json!({"expression": "8 / 0"});
1623 let result = tool.execute(args).await;
1624 assert!(result.is_err());
1625 }
1626
1627 #[tokio::test]
1629 async fn test_calculator_tool_invalid_expression() {
1630 let tool = CalculatorTool;
1631 let args = json!({"expression": "invalid"});
1632 let result = tool.execute(args).await;
1633 assert!(result.is_err());
1634 }
1635
1636 #[tokio::test]
1638 async fn test_echo_tool() {
1639 let tool = EchoTool;
1640 assert_eq!(tool.name(), "echo");
1641 assert_eq!(tool.description(), "Echo back the provided message.");
1642
1643 let args = json!({"message": "Hello, world!"});
1644 let result = tool.execute(args).await.unwrap();
1645 assert!(result.success);
1646 assert_eq!(result.output, "Echo: Hello, world!");
1647 }
1648
1649 #[tokio::test]
1651 async fn test_echo_tool_missing_parameter() {
1652 let tool = EchoTool;
1653 let args = json!({});
1654 let result = tool.execute(args).await;
1655 assert!(result.is_err());
1656 }
1657
1658 #[test]
1660 fn test_tool_registry_new() {
1661 let registry = ToolRegistry::new();
1662 assert!(registry.tools.is_empty());
1663 }
1664
1665 #[tokio::test]
1667 async fn test_tool_registry_register_and_get() {
1668 let mut registry = ToolRegistry::new();
1669 registry.register(Box::new(CalculatorTool));
1670
1671 let tool = registry.get("calculator");
1672 assert!(tool.is_some());
1673 assert_eq!(tool.unwrap().name(), "calculator");
1674 }
1675
1676 #[tokio::test]
1678 async fn test_tool_registry_execute() {
1679 let mut registry = ToolRegistry::new();
1680 registry.register(Box::new(CalculatorTool));
1681
1682 let args = json!({"expression": "5 * 6"});
1683 let result = registry.execute("calculator", args).await.unwrap();
1684 assert!(result.success);
1685 assert_eq!(result.output, "30");
1686 }
1687
1688 #[tokio::test]
1690 async fn test_tool_registry_execute_nonexistent_tool() {
1691 let registry = ToolRegistry::new();
1692 let args = json!({"expression": "5 * 6"});
1693 let result = registry.execute("nonexistent", args).await;
1694 assert!(result.is_err());
1695 }
1696
1697 #[test]
1699 fn test_tool_registry_get_definitions() {
1700 let mut registry = ToolRegistry::new();
1701 registry.register(Box::new(CalculatorTool));
1702 registry.register(Box::new(EchoTool));
1703
1704 let definitions = registry.get_definitions();
1705 assert_eq!(definitions.len(), 2);
1706
1707 let names: Vec<String> = definitions
1709 .iter()
1710 .map(|d| d.function.name.clone())
1711 .collect();
1712 assert!(names.contains(&"calculator".to_string()));
1713 assert!(names.contains(&"echo".to_string()));
1714 }
1715
1716 #[test]
1718 fn test_tool_registry_list_tools() {
1719 let mut registry = ToolRegistry::new();
1720 registry.register(Box::new(CalculatorTool));
1721 registry.register(Box::new(EchoTool));
1722
1723 let tools = registry.list_tools();
1724 assert_eq!(tools.len(), 2);
1725 assert!(tools.contains(&"calculator".to_string()));
1726 assert!(tools.contains(&"echo".to_string()));
1727 }
1728
1729 #[tokio::test]
1731 async fn test_memory_db_set_and_get() {
1732 let tool = MemoryDBTool::new();
1733
1734 let set_args = json!({
1736 "operation": "set",
1737 "key": "name",
1738 "value": "Alice"
1739 });
1740 let result = tool.execute(set_args).await.unwrap();
1741 assert!(result.success);
1742 assert!(result.output.contains("Set 'name' = 'Alice'"));
1743
1744 let get_args = json!({
1746 "operation": "get",
1747 "key": "name"
1748 });
1749 let result = tool.execute(get_args).await.unwrap();
1750 assert!(result.success);
1751 assert!(result.output.contains("Alice"));
1752 }
1753
1754 #[tokio::test]
1756 async fn test_memory_db_delete() {
1757 let tool = MemoryDBTool::new();
1758
1759 let set_args = json!({
1761 "operation": "set",
1762 "key": "temp",
1763 "value": "data"
1764 });
1765 tool.execute(set_args).await.unwrap();
1766
1767 let delete_args = json!({
1769 "operation": "delete",
1770 "key": "temp"
1771 });
1772 let result = tool.execute(delete_args).await.unwrap();
1773 assert!(result.success);
1774 assert!(result.output.contains("Deleted 'temp'"));
1775
1776 let get_args = json!({
1778 "operation": "get",
1779 "key": "temp"
1780 });
1781 let result = tool.execute(get_args).await.unwrap();
1782 assert!(!result.success);
1783 assert!(result.output.contains("not found"));
1784 }
1785
1786 #[tokio::test]
1788 async fn test_memory_db_exists() {
1789 let tool = MemoryDBTool::new();
1790
1791 let exists_args = json!({
1793 "operation": "exists",
1794 "key": "test"
1795 });
1796 let result = tool.execute(exists_args).await.unwrap();
1797 assert!(result.success);
1798 assert!(result.output.contains("false"));
1799
1800 let set_args = json!({
1802 "operation": "set",
1803 "key": "test",
1804 "value": "value"
1805 });
1806 tool.execute(set_args).await.unwrap();
1807
1808 let exists_args = json!({
1810 "operation": "exists",
1811 "key": "test"
1812 });
1813 let result = tool.execute(exists_args).await.unwrap();
1814 assert!(result.success);
1815 assert!(result.output.contains("true"));
1816 }
1817
1818 #[tokio::test]
1820 async fn test_memory_db_list() {
1821 let tool = MemoryDBTool::new();
1822
1823 let list_args = json!({
1825 "operation": "list"
1826 });
1827 let result = tool.execute(list_args).await.unwrap();
1828 assert!(result.success);
1829 assert!(result.output.contains("empty"));
1830
1831 tool.execute(json!({
1833 "operation": "set",
1834 "key": "key1",
1835 "value": "value1"
1836 }))
1837 .await
1838 .unwrap();
1839
1840 tool.execute(json!({
1841 "operation": "set",
1842 "key": "key2",
1843 "value": "value2"
1844 }))
1845 .await
1846 .unwrap();
1847
1848 let list_args = json!({
1850 "operation": "list"
1851 });
1852 let result = tool.execute(list_args).await.unwrap();
1853 assert!(result.success);
1854 assert!(result.output.contains("2 items"));
1855 assert!(result.output.contains("key1"));
1856 assert!(result.output.contains("key2"));
1857 }
1858
1859 #[tokio::test]
1861 async fn test_memory_db_clear() {
1862 let tool = MemoryDBTool::new();
1863
1864 tool.execute(json!({
1866 "operation": "set",
1867 "key": "key1",
1868 "value": "value1"
1869 }))
1870 .await
1871 .unwrap();
1872
1873 tool.execute(json!({
1874 "operation": "set",
1875 "key": "key2",
1876 "value": "value2"
1877 }))
1878 .await
1879 .unwrap();
1880
1881 let clear_args = json!({
1883 "operation": "clear"
1884 });
1885 let result = tool.execute(clear_args).await.unwrap();
1886 assert!(result.success);
1887 assert!(result.output.contains("2 items removed"));
1888
1889 let list_args = json!({
1891 "operation": "list"
1892 });
1893 let result = tool.execute(list_args).await.unwrap();
1894 assert!(result.output.contains("empty"));
1895 }
1896
1897 #[tokio::test]
1899 async fn test_memory_db_invalid_operation() {
1900 let tool = MemoryDBTool::new();
1901
1902 let args = json!({
1903 "operation": "invalid_op"
1904 });
1905 let result = tool.execute(args).await;
1906 assert!(result.is_err());
1907 }
1908
1909 #[tokio::test]
1911 async fn test_memory_db_shared_instance() {
1912 use std::sync::{Arc, Mutex};
1913
1914 let shared_db = Arc::new(Mutex::new(HashMap::new()));
1916 let tool1 = MemoryDBTool::with_shared_db(shared_db.clone());
1917 let tool2 = MemoryDBTool::with_shared_db(shared_db.clone());
1918
1919 tool1
1921 .execute(json!({
1922 "operation": "set",
1923 "key": "shared",
1924 "value": "data"
1925 }))
1926 .await
1927 .unwrap();
1928
1929 let result = tool2
1931 .execute(json!({
1932 "operation": "get",
1933 "key": "shared"
1934 }))
1935 .await
1936 .unwrap();
1937 assert!(result.success);
1938 assert!(result.output.contains("data"));
1939 }
1940}