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