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_n(pad_char, width - char_count).collect();
324 format!("{}{}", padding, text)
325 };
326
327 let output = StringOutput { result };
328 self.to_result(&output)
329 }
330
331 fn handle_pad_right(&self, input: &TextInput) -> ToolResult {
332 let text = input.text.as_deref().unwrap_or("");
333 let width = input.width.unwrap_or(0);
334 let pad_char = input
335 .pad_char
336 .as_deref()
337 .and_then(|s| s.chars().next())
338 .unwrap_or(' ');
339
340 let char_count = text.chars().count();
341 let result = if char_count >= width {
342 text.to_string()
343 } else {
344 let padding: String = std::iter::repeat_n(pad_char, width - char_count).collect();
345 format!("{}{}", text, padding)
346 };
347
348 let output = StringOutput { result };
349 self.to_result(&output)
350 }
351
352 fn handle_truncate(&self, input: &TextInput) -> ToolResult {
353 let text = input.text.as_deref().unwrap_or("");
354 let width = input.width.unwrap_or(text.chars().count());
355 let suffix = input.suffix.as_deref().unwrap_or("...");
356
357 let chars: Vec<char> = text.chars().collect();
358 let result = if chars.len() <= width {
359 text.to_string()
360 } else {
361 let suffix_len = suffix.chars().count();
362 if width <= suffix_len {
363 chars[..width].iter().collect()
364 } else {
365 let truncated: String = chars[..(width - suffix_len)].iter().collect();
366 format!("{}{}", truncated, suffix)
367 }
368 };
369
370 let output = StringOutput { result };
371 self.to_result(&output)
372 }
373
374 fn handle_lines(&self, input: &TextInput) -> ToolResult {
375 let text = input.text.as_deref().unwrap_or("");
376 let lines: Vec<String> = text.lines().map(|s| s.to_string()).collect();
377
378 let output = LinesOutput {
379 count: lines.len(),
380 lines,
381 };
382 self.to_result(&output)
383 }
384
385 fn handle_words(&self, input: &TextInput) -> ToolResult {
386 let text = input.text.as_deref().unwrap_or("");
387 let words: Vec<String> = text.split_whitespace().map(|s| s.to_string()).collect();
388
389 let output = SplitOutput {
390 count: words.len(),
391 parts: words,
392 };
393 self.to_result(&output)
394 }
395
396 fn handle_char_at(&self, input: &TextInput) -> ToolResult {
397 let text = input.text.as_deref().unwrap_or("");
398 let index = input.index.unwrap_or(0);
399
400 let chars: Vec<char> = text.chars().collect();
401 let output = if index < chars.len() {
402 CharAtOutput {
403 char: Some(chars[index].to_string()),
404 found: true,
405 }
406 } else {
407 CharAtOutput {
408 char: None,
409 found: false,
410 }
411 };
412 self.to_result(&output)
413 }
414
415 fn handle_index_of(&self, input: &TextInput) -> ToolResult {
416 let text = input.text.as_deref().unwrap_or("");
417 let find = input.find.as_deref().unwrap_or("");
418
419 let output = match text.find(find) {
420 Some(byte_index) => {
421 let char_index = text[..byte_index].chars().count();
422 IndexOfOutput {
423 index: Some(char_index),
424 found: true,
425 }
426 }
427 None => IndexOfOutput {
428 index: None,
429 found: false,
430 },
431 };
432 self.to_result(&output)
433 }
434
435 fn to_result<T: Serialize>(&self, output: &T) -> ToolResult {
436 match serde_json::to_string(output) {
437 Ok(json) => ToolResult::ok(json),
438 Err(e) => ToolResult::error(format!("Serialization error: {}", e)),
439 }
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446
447 #[tokio::test]
448 async fn test_length_unicode() {
449 let tool = TextTool::new();
450 let result = tool
451 .execute(
452 serde_json::json!({
453 "operation": "length",
454 "text": "안녕하세요"
455 }),
456 ai_agents_core::ToolExecutionContext::test("test"),
457 )
458 .await;
459 assert!(result.success);
460 let output: LengthOutput = serde_json::from_str(&result.output).unwrap();
461 assert_eq!(output.length, 5);
462 }
463
464 #[tokio::test]
465 async fn test_substring() {
466 let tool = TextTool::new();
467 let result = tool
468 .execute(
469 serde_json::json!({
470 "operation": "substring",
471 "text": "hello world",
472 "start": 0,
473 "end": 5
474 }),
475 ai_agents_core::ToolExecutionContext::test("test"),
476 )
477 .await;
478 assert!(result.success);
479 let output: StringOutput = serde_json::from_str(&result.output).unwrap();
480 assert_eq!(output.result, "hello");
481 }
482
483 #[tokio::test]
484 async fn test_uppercase_lowercase() {
485 let tool = TextTool::new();
486
487 let result = tool
488 .execute(
489 serde_json::json!({
490 "operation": "uppercase",
491 "text": "hello"
492 }),
493 ai_agents_core::ToolExecutionContext::test("test"),
494 )
495 .await;
496 assert!(result.success);
497 assert!(result.output.contains("HELLO"));
498
499 let result = tool
500 .execute(
501 serde_json::json!({
502 "operation": "lowercase",
503 "text": "HELLO"
504 }),
505 ai_agents_core::ToolExecutionContext::test("test"),
506 )
507 .await;
508 assert!(result.success);
509 assert!(result.output.contains("hello"));
510 }
511
512 #[tokio::test]
513 async fn test_trim() {
514 let tool = TextTool::new();
515 let result = tool
516 .execute(
517 serde_json::json!({
518 "operation": "trim",
519 "text": " hello "
520 }),
521 ai_agents_core::ToolExecutionContext::test("test"),
522 )
523 .await;
524 assert!(result.success);
525 let output: StringOutput = serde_json::from_str(&result.output).unwrap();
526 assert_eq!(output.result, "hello");
527 }
528
529 #[tokio::test]
530 async fn test_split_join() {
531 let tool = TextTool::new();
532
533 let result = tool
534 .execute(
535 serde_json::json!({
536 "operation": "split",
537 "text": "a,b,c",
538 "delimiter": ","
539 }),
540 ai_agents_core::ToolExecutionContext::test("test"),
541 )
542 .await;
543 assert!(result.success);
544 let output: SplitOutput = serde_json::from_str(&result.output).unwrap();
545 assert_eq!(output.parts, vec!["a", "b", "c"]);
546 assert_eq!(output.count, 3);
547
548 let result = tool
549 .execute(
550 serde_json::json!({
551 "operation": "join",
552 "items": ["a", "b", "c"],
553 "delimiter": "-"
554 }),
555 ai_agents_core::ToolExecutionContext::test("test"),
556 )
557 .await;
558 assert!(result.success);
559 let output: StringOutput = serde_json::from_str(&result.output).unwrap();
560 assert_eq!(output.result, "a-b-c");
561 }
562
563 #[tokio::test]
564 async fn test_replace() {
565 let tool = TextTool::new();
566 let result = tool
567 .execute(
568 serde_json::json!({
569 "operation": "replace",
570 "text": "hello world",
571 "find": "world",
572 "replace_with": "rust"
573 }),
574 ai_agents_core::ToolExecutionContext::test("test"),
575 )
576 .await;
577 assert!(result.success);
578 let output: StringOutput = serde_json::from_str(&result.output).unwrap();
579 assert_eq!(output.result, "hello rust");
580 }
581
582 #[tokio::test]
583 async fn test_contains() {
584 let tool = TextTool::new();
585 let result = tool
586 .execute(
587 serde_json::json!({
588 "operation": "contains",
589 "text": "hello world",
590 "find": "world"
591 }),
592 ai_agents_core::ToolExecutionContext::test("test"),
593 )
594 .await;
595 assert!(result.success);
596 let output: BoolOutput = serde_json::from_str(&result.output).unwrap();
597 assert!(output.result);
598 }
599
600 #[tokio::test]
601 async fn test_repeat() {
602 let tool = TextTool::new();
603 let result = tool
604 .execute(
605 serde_json::json!({
606 "operation": "repeat",
607 "text": "ab",
608 "count": 3
609 }),
610 ai_agents_core::ToolExecutionContext::test("test"),
611 )
612 .await;
613 assert!(result.success);
614 let output: StringOutput = serde_json::from_str(&result.output).unwrap();
615 assert_eq!(output.result, "ababab");
616 }
617
618 #[tokio::test]
619 async fn test_reverse() {
620 let tool = TextTool::new();
621 let result = tool
622 .execute(
623 serde_json::json!({
624 "operation": "reverse",
625 "text": "hello"
626 }),
627 ai_agents_core::ToolExecutionContext::test("test"),
628 )
629 .await;
630 assert!(result.success);
631 let output: StringOutput = serde_json::from_str(&result.output).unwrap();
632 assert_eq!(output.result, "olleh");
633 }
634
635 #[tokio::test]
636 async fn test_pad() {
637 let tool = TextTool::new();
638
639 let result = tool
640 .execute(
641 serde_json::json!({
642 "operation": "pad_left",
643 "text": "5",
644 "width": 3,
645 "pad_char": "0"
646 }),
647 ai_agents_core::ToolExecutionContext::test("test"),
648 )
649 .await;
650 assert!(result.success);
651 let output: StringOutput = serde_json::from_str(&result.output).unwrap();
652 assert_eq!(output.result, "005");
653
654 let result = tool
655 .execute(
656 serde_json::json!({
657 "operation": "pad_right",
658 "text": "hi",
659 "width": 5
660 }),
661 ai_agents_core::ToolExecutionContext::test("test"),
662 )
663 .await;
664 assert!(result.success);
665 let output: StringOutput = serde_json::from_str(&result.output).unwrap();
666 assert_eq!(output.result, "hi ");
667 }
668
669 #[tokio::test]
670 async fn test_truncate() {
671 let tool = TextTool::new();
672 let result = tool
673 .execute(
674 serde_json::json!({
675 "operation": "truncate",
676 "text": "hello world",
677 "width": 8
678 }),
679 ai_agents_core::ToolExecutionContext::test("test"),
680 )
681 .await;
682 assert!(result.success);
683 let output: StringOutput = serde_json::from_str(&result.output).unwrap();
684 assert_eq!(output.result, "hello...");
685 }
686
687 #[tokio::test]
688 async fn test_lines() {
689 let tool = TextTool::new();
690 let result = tool
691 .execute(
692 serde_json::json!({
693 "operation": "lines",
694 "text": "line1\nline2\nline3"
695 }),
696 ai_agents_core::ToolExecutionContext::test("test"),
697 )
698 .await;
699 assert!(result.success);
700 let output: LinesOutput = serde_json::from_str(&result.output).unwrap();
701 assert_eq!(output.count, 3);
702 }
703
704 #[tokio::test]
705 async fn test_words() {
706 let tool = TextTool::new();
707 let result = tool
708 .execute(
709 serde_json::json!({
710 "operation": "words",
711 "text": "hello world test"
712 }),
713 ai_agents_core::ToolExecutionContext::test("test"),
714 )
715 .await;
716 assert!(result.success);
717 let output: SplitOutput = serde_json::from_str(&result.output).unwrap();
718 assert_eq!(output.count, 3);
719 assert_eq!(output.parts, vec!["hello", "world", "test"]);
720 }
721
722 #[tokio::test]
723 async fn test_invalid_operation() {
724 let tool = TextTool::new();
725 let result = tool
726 .execute(
727 serde_json::json!({
728 "operation": "invalid"
729 }),
730 ai_agents_core::ToolExecutionContext::test("test"),
731 )
732 .await;
733 assert!(!result.success);
734 }
735}