1use async_trait::async_trait;
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::generate_schema;
7use ai_agents_core::{Tool, ToolResult, ToolSafetyMetadata};
8
9pub struct TextTool;
10
11impl TextTool {
12 pub fn new() -> Self {
13 Self
14 }
15}
16
17impl Default for TextTool {
18 fn default() -> Self {
19 Self::new()
20 }
21}
22
23#[derive(Debug, Deserialize, JsonSchema)]
24struct TextInput {
25 operation: String,
27 #[serde(default)]
29 text: Option<String>,
30 #[serde(default)]
32 start: Option<usize>,
33 #[serde(default)]
35 end: Option<usize>,
36 #[serde(default)]
38 find: Option<String>,
39 #[serde(default)]
41 replace_with: Option<String>,
42 #[serde(default)]
44 delimiter: Option<String>,
45 #[serde(default)]
47 items: Option<Vec<String>>,
48 #[serde(default)]
50 count: Option<usize>,
51 #[serde(default)]
53 width: Option<usize>,
54 #[serde(default)]
56 pad_char: Option<String>,
57 #[serde(default)]
59 index: Option<usize>,
60 #[serde(default)]
62 suffix: Option<String>,
63}
64
65#[derive(Debug, Serialize, Deserialize)]
66struct LengthOutput {
67 length: usize,
68 bytes: usize,
69}
70
71#[derive(Debug, Serialize, Deserialize)]
72struct StringOutput {
73 result: String,
74}
75
76#[derive(Debug, Serialize, Deserialize)]
77struct BoolOutput {
78 result: bool,
79}
80
81#[derive(Debug, Serialize, Deserialize)]
82struct SplitOutput {
83 parts: Vec<String>,
84 count: usize,
85}
86
87#[derive(Debug, Serialize, Deserialize)]
88struct CharAtOutput {
89 char: Option<String>,
90 found: bool,
91}
92
93#[derive(Debug, Serialize, Deserialize)]
94struct IndexOfOutput {
95 index: Option<usize>,
96 found: bool,
97}
98
99#[derive(Debug, Serialize, Deserialize)]
100struct LinesOutput {
101 lines: Vec<String>,
102 count: usize,
103}
104
105#[async_trait]
106impl Tool for TextTool {
107 fn id(&self) -> &str {
108 "text"
109 }
110
111 fn name(&self) -> &str {
112 "Text Manipulation"
113 }
114
115 fn description(&self) -> &str {
116 "String operations: length (character count), substring, uppercase, lowercase, trim, trim_start, trim_end, replace, split, join, contains, starts_with, ends_with, repeat, reverse, pad_left, pad_right, truncate, lines, words, char_at, index_of. Works with all Unicode text."
117 }
118
119 fn input_schema(&self) -> Value {
120 generate_schema::<TextInput>()
121 }
122
123 fn safety_metadata(&self) -> ToolSafetyMetadata {
124 ToolSafetyMetadata::compute()
125 }
126
127 async fn execute(&self, args: Value, _ctx: ai_agents_core::ToolExecutionContext) -> ToolResult {
128 let input: TextInput = match serde_json::from_value(args) {
129 Ok(input) => input,
130 Err(e) => return ToolResult::error(format!("Invalid input: {}", e)),
131 };
132
133 match input.operation.to_lowercase().as_str() {
134 "length" | "len" => self.handle_length(&input),
135 "substring" | "substr" | "slice" => self.handle_substring(&input),
136 "uppercase" | "upper" => self.handle_uppercase(&input),
137 "lowercase" | "lower" => self.handle_lowercase(&input),
138 "trim" => self.handle_trim(&input),
139 "trim_start" | "ltrim" => self.handle_trim_start(&input),
140 "trim_end" | "rtrim" => self.handle_trim_end(&input),
141 "replace" => self.handle_replace(&input),
142 "split" => self.handle_split(&input),
143 "join" => self.handle_join(&input),
144 "contains" | "includes" => self.handle_contains(&input),
145 "starts_with" => self.handle_starts_with(&input),
146 "ends_with" => self.handle_ends_with(&input),
147 "repeat" => self.handle_repeat(&input),
148 "reverse" => self.handle_reverse(&input),
149 "pad_left" | "lpad" => self.handle_pad_left(&input),
150 "pad_right" | "rpad" => self.handle_pad_right(&input),
151 "truncate" => self.handle_truncate(&input),
152 "lines" => self.handle_lines(&input),
153 "words" => self.handle_words(&input),
154 "char_at" => self.handle_char_at(&input),
155 "index_of" | "find" => self.handle_index_of(&input),
156 _ => ToolResult::error(format!(
157 "Unknown operation: {}. Valid: length, substring, uppercase, lowercase, trim, replace, split, join, contains, starts_with, ends_with, repeat, reverse, pad_left, pad_right, truncate, lines, words, char_at, index_of",
158 input.operation
159 )),
160 }
161 }
162}
163
164impl TextTool {
165 fn handle_length(&self, input: &TextInput) -> ToolResult {
166 let text = input.text.as_deref().unwrap_or("");
167 let output = LengthOutput {
168 length: text.chars().count(),
169 bytes: text.len(),
170 };
171 self.to_result(&output)
172 }
173
174 fn handle_substring(&self, input: &TextInput) -> ToolResult {
175 let text = input.text.as_deref().unwrap_or("");
176 let chars: Vec<char> = text.chars().collect();
177 let start = input.start.unwrap_or(0);
178 let end = input.end.unwrap_or(chars.len());
179
180 let start = start.min(chars.len());
181 let end = end.min(chars.len());
182
183 let result: String = chars[start..end].iter().collect();
184 let output = StringOutput { result };
185 self.to_result(&output)
186 }
187
188 fn handle_uppercase(&self, input: &TextInput) -> ToolResult {
189 let text = input.text.as_deref().unwrap_or("");
190 let output = StringOutput {
191 result: text.to_uppercase(),
192 };
193 self.to_result(&output)
194 }
195
196 fn handle_lowercase(&self, input: &TextInput) -> ToolResult {
197 let text = input.text.as_deref().unwrap_or("");
198 let output = StringOutput {
199 result: text.to_lowercase(),
200 };
201 self.to_result(&output)
202 }
203
204 fn handle_trim(&self, input: &TextInput) -> ToolResult {
205 let text = input.text.as_deref().unwrap_or("");
206 let output = StringOutput {
207 result: text.trim().to_string(),
208 };
209 self.to_result(&output)
210 }
211
212 fn handle_trim_start(&self, input: &TextInput) -> ToolResult {
213 let text = input.text.as_deref().unwrap_or("");
214 let output = StringOutput {
215 result: text.trim_start().to_string(),
216 };
217 self.to_result(&output)
218 }
219
220 fn handle_trim_end(&self, input: &TextInput) -> ToolResult {
221 let text = input.text.as_deref().unwrap_or("");
222 let output = StringOutput {
223 result: text.trim_end().to_string(),
224 };
225 self.to_result(&output)
226 }
227
228 fn handle_replace(&self, input: &TextInput) -> ToolResult {
229 let text = input.text.as_deref().unwrap_or("");
230 let find = input.find.as_deref().unwrap_or("");
231 let replace_with = input.replace_with.as_deref().unwrap_or("");
232
233 let output = StringOutput {
234 result: text.replace(find, replace_with),
235 };
236 self.to_result(&output)
237 }
238
239 fn handle_split(&self, input: &TextInput) -> ToolResult {
240 let text = input.text.as_deref().unwrap_or("");
241 let delimiter = input.delimiter.as_deref().unwrap_or(" ");
242
243 let parts: Vec<String> = text.split(delimiter).map(|s| s.to_string()).collect();
244 let output = SplitOutput {
245 count: parts.len(),
246 parts,
247 };
248 self.to_result(&output)
249 }
250
251 fn handle_join(&self, input: &TextInput) -> ToolResult {
252 let items = input.items.as_deref().unwrap_or(&[]);
253 let delimiter = input.delimiter.as_deref().unwrap_or("");
254
255 let output = StringOutput {
256 result: items.join(delimiter),
257 };
258 self.to_result(&output)
259 }
260
261 fn handle_contains(&self, input: &TextInput) -> ToolResult {
262 let text = input.text.as_deref().unwrap_or("");
263 let find = input.find.as_deref().unwrap_or("");
264
265 let output = BoolOutput {
266 result: text.contains(find),
267 };
268 self.to_result(&output)
269 }
270
271 fn handle_starts_with(&self, input: &TextInput) -> ToolResult {
272 let text = input.text.as_deref().unwrap_or("");
273 let find = input.find.as_deref().unwrap_or("");
274
275 let output = BoolOutput {
276 result: text.starts_with(find),
277 };
278 self.to_result(&output)
279 }
280
281 fn handle_ends_with(&self, input: &TextInput) -> ToolResult {
282 let text = input.text.as_deref().unwrap_or("");
283 let find = input.find.as_deref().unwrap_or("");
284
285 let output = BoolOutput {
286 result: text.ends_with(find),
287 };
288 self.to_result(&output)
289 }
290
291 fn handle_repeat(&self, input: &TextInput) -> ToolResult {
292 let text = input.text.as_deref().unwrap_or("");
293 let count = input.count.unwrap_or(1);
294
295 let output = StringOutput {
296 result: text.repeat(count),
297 };
298 self.to_result(&output)
299 }
300
301 fn handle_reverse(&self, input: &TextInput) -> ToolResult {
302 let text = input.text.as_deref().unwrap_or("");
303
304 let output = StringOutput {
305 result: text.chars().rev().collect(),
306 };
307 self.to_result(&output)
308 }
309
310 fn handle_pad_left(&self, input: &TextInput) -> ToolResult {
311 let text = input.text.as_deref().unwrap_or("");
312 let width = input.width.unwrap_or(0);
313 let pad_char = input
314 .pad_char
315 .as_deref()
316 .and_then(|s| s.chars().next())
317 .unwrap_or(' ');
318
319 let char_count = text.chars().count();
320 let result = if char_count >= width {
321 text.to_string()
322 } else {
323 let padding: String = std::iter::repeat(pad_char)
324 .take(width - char_count)
325 .collect();
326 format!("{}{}", padding, text)
327 };
328
329 let output = StringOutput { result };
330 self.to_result(&output)
331 }
332
333 fn handle_pad_right(&self, input: &TextInput) -> ToolResult {
334 let text = input.text.as_deref().unwrap_or("");
335 let width = input.width.unwrap_or(0);
336 let pad_char = input
337 .pad_char
338 .as_deref()
339 .and_then(|s| s.chars().next())
340 .unwrap_or(' ');
341
342 let char_count = text.chars().count();
343 let result = if char_count >= width {
344 text.to_string()
345 } else {
346 let padding: String = std::iter::repeat(pad_char)
347 .take(width - char_count)
348 .collect();
349 format!("{}{}", text, padding)
350 };
351
352 let output = StringOutput { result };
353 self.to_result(&output)
354 }
355
356 fn handle_truncate(&self, input: &TextInput) -> ToolResult {
357 let text = input.text.as_deref().unwrap_or("");
358 let width = input.width.unwrap_or(text.chars().count());
359 let suffix = input.suffix.as_deref().unwrap_or("...");
360
361 let chars: Vec<char> = text.chars().collect();
362 let result = if chars.len() <= width {
363 text.to_string()
364 } else {
365 let suffix_len = suffix.chars().count();
366 if width <= suffix_len {
367 chars[..width].iter().collect()
368 } else {
369 let truncated: String = chars[..(width - suffix_len)].iter().collect();
370 format!("{}{}", truncated, suffix)
371 }
372 };
373
374 let output = StringOutput { result };
375 self.to_result(&output)
376 }
377
378 fn handle_lines(&self, input: &TextInput) -> ToolResult {
379 let text = input.text.as_deref().unwrap_or("");
380 let lines: Vec<String> = text.lines().map(|s| s.to_string()).collect();
381
382 let output = LinesOutput {
383 count: lines.len(),
384 lines,
385 };
386 self.to_result(&output)
387 }
388
389 fn handle_words(&self, input: &TextInput) -> ToolResult {
390 let text = input.text.as_deref().unwrap_or("");
391 let words: Vec<String> = text.split_whitespace().map(|s| s.to_string()).collect();
392
393 let output = SplitOutput {
394 count: words.len(),
395 parts: words,
396 };
397 self.to_result(&output)
398 }
399
400 fn handle_char_at(&self, input: &TextInput) -> ToolResult {
401 let text = input.text.as_deref().unwrap_or("");
402 let index = input.index.unwrap_or(0);
403
404 let chars: Vec<char> = text.chars().collect();
405 let output = if index < chars.len() {
406 CharAtOutput {
407 char: Some(chars[index].to_string()),
408 found: true,
409 }
410 } else {
411 CharAtOutput {
412 char: None,
413 found: false,
414 }
415 };
416 self.to_result(&output)
417 }
418
419 fn handle_index_of(&self, input: &TextInput) -> ToolResult {
420 let text = input.text.as_deref().unwrap_or("");
421 let find = input.find.as_deref().unwrap_or("");
422
423 let output = match text.find(find) {
424 Some(byte_index) => {
425 let char_index = text[..byte_index].chars().count();
426 IndexOfOutput {
427 index: Some(char_index),
428 found: true,
429 }
430 }
431 None => IndexOfOutput {
432 index: None,
433 found: false,
434 },
435 };
436 self.to_result(&output)
437 }
438
439 fn to_result<T: Serialize>(&self, output: &T) -> ToolResult {
440 match serde_json::to_string(output) {
441 Ok(json) => ToolResult::ok(json),
442 Err(e) => ToolResult::error(format!("Serialization error: {}", e)),
443 }
444 }
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450
451 #[tokio::test]
452 async fn test_length_unicode() {
453 let tool = TextTool::new();
454 let result = tool
455 .execute(
456 serde_json::json!({
457 "operation": "length",
458 "text": "안녕하세요"
459 }),
460 ai_agents_core::ToolExecutionContext::test("test"),
461 )
462 .await;
463 assert!(result.success);
464 let output: LengthOutput = serde_json::from_str(&result.output).unwrap();
465 assert_eq!(output.length, 5);
466 }
467
468 #[tokio::test]
469 async fn test_substring() {
470 let tool = TextTool::new();
471 let result = tool
472 .execute(
473 serde_json::json!({
474 "operation": "substring",
475 "text": "hello world",
476 "start": 0,
477 "end": 5
478 }),
479 ai_agents_core::ToolExecutionContext::test("test"),
480 )
481 .await;
482 assert!(result.success);
483 let output: StringOutput = serde_json::from_str(&result.output).unwrap();
484 assert_eq!(output.result, "hello");
485 }
486
487 #[tokio::test]
488 async fn test_uppercase_lowercase() {
489 let tool = TextTool::new();
490
491 let result = tool
492 .execute(
493 serde_json::json!({
494 "operation": "uppercase",
495 "text": "hello"
496 }),
497 ai_agents_core::ToolExecutionContext::test("test"),
498 )
499 .await;
500 assert!(result.success);
501 assert!(result.output.contains("HELLO"));
502
503 let result = tool
504 .execute(
505 serde_json::json!({
506 "operation": "lowercase",
507 "text": "HELLO"
508 }),
509 ai_agents_core::ToolExecutionContext::test("test"),
510 )
511 .await;
512 assert!(result.success);
513 assert!(result.output.contains("hello"));
514 }
515
516 #[tokio::test]
517 async fn test_trim() {
518 let tool = TextTool::new();
519 let result = tool
520 .execute(
521 serde_json::json!({
522 "operation": "trim",
523 "text": " hello "
524 }),
525 ai_agents_core::ToolExecutionContext::test("test"),
526 )
527 .await;
528 assert!(result.success);
529 let output: StringOutput = serde_json::from_str(&result.output).unwrap();
530 assert_eq!(output.result, "hello");
531 }
532
533 #[tokio::test]
534 async fn test_split_join() {
535 let tool = TextTool::new();
536
537 let result = tool
538 .execute(
539 serde_json::json!({
540 "operation": "split",
541 "text": "a,b,c",
542 "delimiter": ","
543 }),
544 ai_agents_core::ToolExecutionContext::test("test"),
545 )
546 .await;
547 assert!(result.success);
548 let output: SplitOutput = serde_json::from_str(&result.output).unwrap();
549 assert_eq!(output.parts, vec!["a", "b", "c"]);
550 assert_eq!(output.count, 3);
551
552 let result = tool
553 .execute(
554 serde_json::json!({
555 "operation": "join",
556 "items": ["a", "b", "c"],
557 "delimiter": "-"
558 }),
559 ai_agents_core::ToolExecutionContext::test("test"),
560 )
561 .await;
562 assert!(result.success);
563 let output: StringOutput = serde_json::from_str(&result.output).unwrap();
564 assert_eq!(output.result, "a-b-c");
565 }
566
567 #[tokio::test]
568 async fn test_replace() {
569 let tool = TextTool::new();
570 let result = tool
571 .execute(
572 serde_json::json!({
573 "operation": "replace",
574 "text": "hello world",
575 "find": "world",
576 "replace_with": "rust"
577 }),
578 ai_agents_core::ToolExecutionContext::test("test"),
579 )
580 .await;
581 assert!(result.success);
582 let output: StringOutput = serde_json::from_str(&result.output).unwrap();
583 assert_eq!(output.result, "hello rust");
584 }
585
586 #[tokio::test]
587 async fn test_contains() {
588 let tool = TextTool::new();
589 let result = tool
590 .execute(
591 serde_json::json!({
592 "operation": "contains",
593 "text": "hello world",
594 "find": "world"
595 }),
596 ai_agents_core::ToolExecutionContext::test("test"),
597 )
598 .await;
599 assert!(result.success);
600 let output: BoolOutput = serde_json::from_str(&result.output).unwrap();
601 assert!(output.result);
602 }
603
604 #[tokio::test]
605 async fn test_repeat() {
606 let tool = TextTool::new();
607 let result = tool
608 .execute(
609 serde_json::json!({
610 "operation": "repeat",
611 "text": "ab",
612 "count": 3
613 }),
614 ai_agents_core::ToolExecutionContext::test("test"),
615 )
616 .await;
617 assert!(result.success);
618 let output: StringOutput = serde_json::from_str(&result.output).unwrap();
619 assert_eq!(output.result, "ababab");
620 }
621
622 #[tokio::test]
623 async fn test_reverse() {
624 let tool = TextTool::new();
625 let result = tool
626 .execute(
627 serde_json::json!({
628 "operation": "reverse",
629 "text": "hello"
630 }),
631 ai_agents_core::ToolExecutionContext::test("test"),
632 )
633 .await;
634 assert!(result.success);
635 let output: StringOutput = serde_json::from_str(&result.output).unwrap();
636 assert_eq!(output.result, "olleh");
637 }
638
639 #[tokio::test]
640 async fn test_pad() {
641 let tool = TextTool::new();
642
643 let result = tool
644 .execute(
645 serde_json::json!({
646 "operation": "pad_left",
647 "text": "5",
648 "width": 3,
649 "pad_char": "0"
650 }),
651 ai_agents_core::ToolExecutionContext::test("test"),
652 )
653 .await;
654 assert!(result.success);
655 let output: StringOutput = serde_json::from_str(&result.output).unwrap();
656 assert_eq!(output.result, "005");
657
658 let result = tool
659 .execute(
660 serde_json::json!({
661 "operation": "pad_right",
662 "text": "hi",
663 "width": 5
664 }),
665 ai_agents_core::ToolExecutionContext::test("test"),
666 )
667 .await;
668 assert!(result.success);
669 let output: StringOutput = serde_json::from_str(&result.output).unwrap();
670 assert_eq!(output.result, "hi ");
671 }
672
673 #[tokio::test]
674 async fn test_truncate() {
675 let tool = TextTool::new();
676 let result = tool
677 .execute(
678 serde_json::json!({
679 "operation": "truncate",
680 "text": "hello world",
681 "width": 8
682 }),
683 ai_agents_core::ToolExecutionContext::test("test"),
684 )
685 .await;
686 assert!(result.success);
687 let output: StringOutput = serde_json::from_str(&result.output).unwrap();
688 assert_eq!(output.result, "hello...");
689 }
690
691 #[tokio::test]
692 async fn test_lines() {
693 let tool = TextTool::new();
694 let result = tool
695 .execute(
696 serde_json::json!({
697 "operation": "lines",
698 "text": "line1\nline2\nline3"
699 }),
700 ai_agents_core::ToolExecutionContext::test("test"),
701 )
702 .await;
703 assert!(result.success);
704 let output: LinesOutput = serde_json::from_str(&result.output).unwrap();
705 assert_eq!(output.count, 3);
706 }
707
708 #[tokio::test]
709 async fn test_words() {
710 let tool = TextTool::new();
711 let result = tool
712 .execute(
713 serde_json::json!({
714 "operation": "words",
715 "text": "hello world test"
716 }),
717 ai_agents_core::ToolExecutionContext::test("test"),
718 )
719 .await;
720 assert!(result.success);
721 let output: SplitOutput = serde_json::from_str(&result.output).unwrap();
722 assert_eq!(output.count, 3);
723 assert_eq!(output.parts, vec!["hello", "world", "test"]);
724 }
725
726 #[tokio::test]
727 async fn test_invalid_operation() {
728 let tool = TextTool::new();
729 let result = tool
730 .execute(
731 serde_json::json!({
732 "operation": "invalid"
733 }),
734 ai_agents_core::ToolExecutionContext::test("test"),
735 )
736 .await;
737 assert!(!result.success);
738 }
739}