1use std::{borrow::Cow, collections::HashMap};
4
5#[cfg(feature = "toolkits")]
6use std::sync::{Arc, LazyLock};
7
8use crate::toolkits::error::{ToolResult, error_context};
9use async_trait::async_trait;
10use serde::Serialize;
11
12#[cfg(feature = "toolkits")]
15type CompiledSchema = Arc<jsonschema::Validator>;
16
17#[async_trait]
19pub trait DynTool: Send + Sync {
20 fn metadata(&self) -> &ToolMetadata;
22
23 async fn execute_json(&self, input: serde_json::Value) -> ToolResult<serde_json::Value>;
25
26 fn input_schema(&self) -> serde_json::Value;
28
29 fn name(&self) -> &str {
31 self.metadata().name()
32 }
33}
34
35#[cfg(feature = "toolkits")]
37static SCHEMA_CACHE: LazyLock<std::sync::RwLock<HashMap<String, Arc<jsonschema::Validator>>>> =
38 LazyLock::new(|| std::sync::RwLock::new(HashMap::new()));
39
40#[cfg(feature = "toolkits")]
42const SCHEMA_CACHE_MAX_SIZE: usize = 256;
43
44#[derive(Debug, Clone, Serialize)]
46pub struct ToolMetadata {
47 name: Cow<'static, str>,
49
50 description: Cow<'static, str>,
52
53 version: Cow<'static, str>,
55
56 author: Option<Cow<'static, str>>,
58
59 tags: Vec<Cow<'static, str>>,
61
62 enabled: bool,
64
65 metadata: HashMap<Cow<'static, str>, serde_json::Value>,
67}
68
69impl ToolMetadata {
70 pub fn new(name: impl Into<String>, description: impl Into<String>) -> ToolResult<Self> {
72 let name = name.into();
73 let description = description.into();
74
75 validate_tool_name(&name)?;
76
77 Ok(Self {
78 name: Cow::Owned(name),
79 description: Cow::Owned(description),
80 version: Cow::Borrowed("1.0.0"),
81 author: None,
82 tags: Vec::new(),
83 enabled: true,
84 metadata: HashMap::new(),
85 })
86 }
87
88 pub fn name(&self) -> &str {
90 &self.name
91 }
92
93 pub fn description(&self) -> &str {
95 &self.description
96 }
97
98 pub fn version(&self) -> &str {
100 &self.version
101 }
102
103 pub fn author(&self) -> Option<&str> {
105 self.author.as_deref()
106 }
107
108 pub fn tags(&self) -> &[Cow<'static, str>] {
110 &self.tags
111 }
112
113 pub fn is_enabled(&self) -> bool {
115 self.enabled
116 }
117
118 pub fn additional_metadata(&self) -> &HashMap<Cow<'static, str>, serde_json::Value> {
120 &self.metadata
121 }
122
123 pub fn with_version(mut self, version: impl Into<Cow<'static, str>>) -> Self {
125 self.version = version.into();
126 self
127 }
128
129 pub fn with_author(mut self, author: impl Into<Cow<'static, str>>) -> Self {
131 self.author = Some(author.into());
132 self
133 }
134
135 pub fn with_tags<T: Into<Cow<'static, str>>>(
137 mut self,
138 tags: impl IntoIterator<Item = T>,
139 ) -> Self {
140 self.tags = tags.into_iter().map(Into::into).collect();
141 self
142 }
143
144 pub fn with_enabled(mut self, enabled: bool) -> Self {
146 self.enabled = enabled;
147 self
148 }
149
150 pub fn with_metadata(
152 mut self,
153 key: impl Into<Cow<'static, str>>,
154 value: serde_json::Value,
155 ) -> Self {
156 self.metadata.insert(key.into(), value);
157 self
158 }
159}
160
161pub(crate) fn validate_tool_name(name: &str) -> ToolResult<()> {
162 if name.trim().is_empty() {
163 return Err(error_context().invalid_parameters("Tool name cannot be empty"));
164 }
165 if name.len() > 64 {
166 return Err(error_context().invalid_parameters("Tool name cannot exceed 64 characters"));
167 }
168 if !name
169 .bytes()
170 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
171 {
172 return Err(error_context().invalid_parameters(
173 "Tool name must contain only ASCII letters, digits, underscores, and hyphens",
174 ));
175 }
176 Ok(())
177}
178
179pub mod conversions {
181 use crate::toolkits::error::{ToolResult, error_context};
182
183 pub fn to_json<T: serde::Serialize>(value: T) -> ToolResult<serde_json::Value> {
185 serde_json::to_value(value).map_err(|e| error_context().serialization_error(e))
186 }
187
188 pub fn from_json_string(value: serde_json::Value) -> ToolResult<String> {
190 match value {
191 serde_json::Value::String(s) => Ok(s),
192 _ => Err(error_context().invalid_parameters("Expected string value")),
193 }
194 }
195
196 pub fn from_json_i32(value: serde_json::Value) -> ToolResult<i32> {
198 match value {
199 serde_json::Value::Number(n) => n
200 .as_i64()
201 .and_then(|i| i.try_into().ok())
202 .ok_or_else(|| error_context().invalid_parameters("Expected i32 value")),
203 _ => Err(error_context().invalid_parameters("Expected number value")),
204 }
205 }
206
207 pub fn from_json_f64(value: serde_json::Value) -> ToolResult<f64> {
209 match value {
210 serde_json::Value::Number(n) => n
211 .as_f64()
212 .ok_or_else(|| error_context().invalid_parameters("Expected f64 value")),
213 _ => Err(error_context().invalid_parameters("Expected number value")),
214 }
215 }
216
217 pub fn from_json_bool(value: serde_json::Value) -> ToolResult<bool> {
219 match value {
220 serde_json::Value::Bool(b) => Ok(b),
221 _ => Err(error_context().invalid_parameters("Expected boolean value")),
222 }
223 }
224}
225
226pub type ToolHandler = std::sync::Arc<
233 dyn Fn(
234 serde_json::Value,
235 ) -> std::pin::Pin<
236 Box<dyn std::future::Future<Output = ToolResult<serde_json::Value>> + Send>,
237 > + Send
238 + Sync,
239>;
240
241pub struct FunctionTool {
244 metadata: ToolMetadata,
245 input_schema: serde_json::Value,
246 #[cfg(feature = "toolkits")]
247 compiled_schema: CompiledSchema,
248 handler: ToolHandler,
249}
250
251impl Clone for FunctionTool {
252 fn clone(&self) -> Self {
253 Self {
254 metadata: self.metadata.clone(),
255 input_schema: self.input_schema.clone(),
256 #[cfg(feature = "toolkits")]
257 compiled_schema: Arc::clone(&self.compiled_schema),
258 handler: self.handler.clone(),
259 }
260 }
261}
262
263impl FunctionTool {
264 pub fn builder(name: impl Into<String>, description: impl Into<String>) -> FunctionToolBuilder {
266 FunctionToolBuilder::new(name, description)
267 }
268 pub fn from_schema<F, Fut>(
271 name: impl Into<String>,
272 description: impl Into<String>,
273 schema: serde_json::Value,
274 f: F,
275 ) -> ToolResult<FunctionTool>
276 where
277 F: Fn(serde_json::Value) -> Fut + Send + Sync + 'static,
278 Fut: std::future::Future<Output = ToolResult<serde_json::Value>> + Send + 'static,
279 {
280 Self::builder(name, description)
281 .schema(schema)
282 .handler(f)
283 .build()
284 }
285 pub fn from_function_spec<F, Fut>(spec: serde_json::Value, f: F) -> ToolResult<FunctionTool>
290 where
291 F: Fn(serde_json::Value) -> Fut + Send + Sync + 'static,
292 Fut: std::future::Future<Output = ToolResult<serde_json::Value>> + Send + 'static,
293 {
294 let (name, description, parameters) = parse_function_spec_details(&spec)?;
295 let mut builder = Self::builder(name, description);
296 if let Some(p) = parameters {
297 builder = builder.schema(p);
298 }
299 builder.handler(f).build()
300 }
301
302 pub fn from_function_spec_file<F, Fut>(
304 path: impl AsRef<std::path::Path>,
305 f: F,
306 ) -> ToolResult<FunctionTool>
307 where
308 F: Fn(serde_json::Value) -> Fut + Send + Sync + 'static,
309 Fut: std::future::Future<Output = ToolResult<serde_json::Value>> + Send + 'static,
310 {
311 let content = std::fs::read_to_string(path).map_err(|e| {
312 error_context().invalid_parameters(format!("Failed to read spec file: {e}"))
313 })?;
314 let spec: serde_json::Value = serde_json::from_str(&content)
315 .map_err(|e| error_context().invalid_parameters(format!("Invalid JSON: {e}")))?;
316 Self::from_function_spec(spec, f)
317 }
318}
319
320#[cfg(feature = "toolkits")]
321fn compile_schema_cached(
323 schema: &serde_json::Value,
324 tool_name: &str,
325) -> ToolResult<Arc<jsonschema::Validator>> {
326 let cache_key = schema.to_string();
329
330 {
332 let cache = SCHEMA_CACHE
335 .read()
336 .unwrap_or_else(std::sync::PoisonError::into_inner);
337 if let Some(cached) = cache.get(&cache_key) {
338 return Ok(Arc::clone(cached));
339 }
340 }
341
342 let validator = jsonschema::validator_for(schema).map_err(|e| {
344 error_context()
345 .with_tool(tool_name)
346 .schema_validation(format!("Failed to compile schema: {e}"))
347 })?;
348
349 let validator = Arc::new(validator);
350
351 {
352 let mut cache = SCHEMA_CACHE
353 .write()
354 .unwrap_or_else(std::sync::PoisonError::into_inner);
355 if cache.len() >= SCHEMA_CACHE_MAX_SIZE {
357 let remove_count = (SCHEMA_CACHE_MAX_SIZE / 10).max(1);
361 let keys: Vec<String> = cache.keys().take(remove_count).cloned().collect();
362 for k in keys {
363 cache.remove(&k);
364 }
365 }
366 cache.insert(cache_key, Arc::clone(&validator));
367 }
368
369 Ok(validator)
370}
371
372pub(crate) fn parse_function_spec_details(
375 spec: &serde_json::Value,
376) -> ToolResult<(String, String, Option<serde_json::Value>)> {
377 use serde_json::Value;
378 let obj = match spec {
379 Value::Object(map) => map,
380 _ => return Err(error_context().invalid_parameters("Function spec must be a JSON object")),
381 };
382 let (name, desc, params) = if obj.get("type").and_then(|v| v.as_str()) == Some("function") {
384 let f = obj
385 .get("function")
386 .and_then(|v| v.as_object())
387 .ok_or_else(|| error_context().invalid_parameters("Missing 'function' object"))?;
388 let name = f
389 .get("name")
390 .and_then(|v| v.as_str())
391 .ok_or_else(|| error_context().invalid_parameters("Missing function.name"))?
392 .to_string();
393 let desc = f
394 .get("description")
395 .and_then(|v| v.as_str())
396 .unwrap_or("")
397 .to_string();
398 let params = f.get("parameters").cloned();
399 (name, desc, params)
400 } else {
401 let name = obj
403 .get("name")
404 .and_then(|v| v.as_str())
405 .ok_or_else(|| error_context().invalid_parameters("Missing name"))?
406 .to_string();
407 let desc = obj
408 .get("description")
409 .and_then(|v| v.as_str())
410 .unwrap_or("")
411 .to_string();
412 let params = obj.get("parameters").cloned();
413 (name, desc, params)
414 };
415 Ok((name, desc, params))
416}
417
418pub struct FunctionToolBuilder {
420 metadata: ToolMetadata,
421 input_schema: Option<serde_json::Value>,
422 staged_properties: Option<serde_json::Map<String, serde_json::Value>>,
424 staged_required: Vec<String>,
425 handler: Option<ToolHandler>,
426}
427
428impl FunctionToolBuilder {
429 pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
431 let metadata = ToolMetadata {
434 name: Cow::Owned(name.into()),
435 description: Cow::Owned(description.into()),
436 version: Cow::Borrowed("1.0.0"),
437 author: None,
438 tags: Vec::new(),
439 enabled: true,
440 metadata: HashMap::new(),
441 };
442 Self {
443 metadata,
444 input_schema: None,
445 staged_properties: None,
446 staged_required: Vec::new(),
447 handler: None,
448 }
449 }
450
451 pub fn schema(mut self, schema: serde_json::Value) -> Self {
453 self.input_schema = Some(schema);
454 self
455 }
456
457 pub fn metadata(mut self, f: impl FnOnce(ToolMetadata) -> ToolMetadata) -> Self {
459 self.metadata = f(self.metadata);
460 self
461 }
462
463 pub fn handler<F, Fut>(mut self, f: F) -> Self
465 where
466 F: Fn(serde_json::Value) -> Fut + Send + Sync + 'static,
467 Fut: std::future::Future<Output = ToolResult<serde_json::Value>> + Send + 'static,
468 {
469 let wrapped = move |args: serde_json::Value| -> std::pin::Pin<
470 Box<dyn std::future::Future<Output = ToolResult<serde_json::Value>> + Send>,
471 > { Box::pin(f(args)) };
472 self.handler = Some(std::sync::Arc::new(wrapped));
473 self
474 }
475
476 pub fn property(mut self, name: impl Into<String>, schema: serde_json::Value) -> Self {
480 let name = name.into();
481 let entry = self
482 .staged_properties
483 .get_or_insert_with(serde_json::Map::new);
484 entry.insert(name, schema);
485 self
486 }
487
488 pub fn required(mut self, name: impl Into<String>) -> Self {
491 self.staged_required.push(name.into());
492 self
493 }
494
495 pub fn build(mut self) -> ToolResult<FunctionTool> {
498 validate_tool_name(&self.metadata.name)?;
499 let handler = self
500 .handler
501 .ok_or_else(|| error_context().invalid_parameters("FunctionTool handler not set"))?;
502 let mut schema = self
503 .input_schema
504 .take()
505 .unwrap_or_else(|| serde_json::json!({}));
506
507 if let serde_json::Value::Object(ref mut obj) = schema {
508 if obj
509 .get("type")
510 .is_some_and(|schema_type| schema_type.as_str() != Some("object"))
511 {
512 return Err(error_context()
513 .with_tool(self.metadata.name.clone())
514 .invalid_parameters("tool input schema type must be 'object'"));
515 }
516 obj.entry("type")
517 .or_insert(serde_json::Value::String("object".to_string()));
518 obj.entry("additionalProperties")
519 .or_insert(serde_json::Value::Bool(false));
520
521 if let Some(staged) = self.staged_properties.take() {
522 let props = obj
523 .entry("properties")
524 .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
525 let props = props.as_object_mut().ok_or_else(|| {
526 error_context()
527 .with_tool(self.metadata.name.clone())
528 .invalid_parameters("schema.properties must be an object")
529 })?;
530 for (name, property_schema) in staged {
531 props.insert(name, property_schema);
532 }
533 }
534
535 if !self.staged_required.is_empty() {
536 use std::collections::BTreeSet;
537 let mut required = BTreeSet::new();
538 if let Some(existing) = obj.get("required") {
539 let entries = existing.as_array().ok_or_else(|| {
540 error_context()
541 .with_tool(self.metadata.name.clone())
542 .invalid_parameters("schema.required must be an array of strings")
543 })?;
544 for entry in entries {
545 let name = entry.as_str().ok_or_else(|| {
546 error_context()
547 .with_tool(self.metadata.name.clone())
548 .invalid_parameters("schema.required must contain only strings")
549 })?;
550 required.insert(name.to_string());
551 }
552 }
553 required.extend(self.staged_required);
554 obj.insert(
555 "required".to_string(),
556 serde_json::Value::Array(
557 required
558 .into_iter()
559 .map(serde_json::Value::String)
560 .collect(),
561 ),
562 );
563 }
564 obj.entry("properties")
565 .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
566 } else {
567 return Err(error_context()
568 .with_tool(self.metadata.name.clone())
569 .invalid_parameters("tool input schema must be a JSON object"));
570 }
571
572 #[cfg(feature = "toolkits")]
573 let compiled_schema: CompiledSchema = compile_schema_cached(&schema, &self.metadata.name)?;
574 Ok(FunctionTool {
575 metadata: self.metadata,
576 input_schema: schema,
577 #[cfg(feature = "toolkits")]
578 compiled_schema,
579 handler,
580 })
581 }
582}
583
584#[async_trait]
585impl DynTool for FunctionTool {
586 fn metadata(&self) -> &ToolMetadata {
587 &self.metadata
588 }
589
590 async fn execute_json(&self, input: serde_json::Value) -> ToolResult<serde_json::Value> {
591 #[cfg(feature = "toolkits")]
593 if let Err(validation_error) = self.compiled_schema.validate(&input) {
594 return Err(error_context()
595 .with_tool(self.name())
596 .invalid_parameters(format!("Input validation failed: {validation_error}")));
597 }
598
599 (self.handler)(input).await
601 }
602
603 fn input_schema(&self) -> serde_json::Value {
604 self.input_schema.clone()
605 }
606}
607
608#[cfg(test)]
609mod tests {
610 use super::*;
611 use crate::toolkits::ToolError;
612
613 #[test]
614 fn test_tool_metadata_new() {
615 let metadata = ToolMetadata::new("test_tool", "A test tool").unwrap();
616 assert_eq!(metadata.name(), "test_tool");
617 assert_eq!(metadata.description(), "A test tool");
618 assert_eq!(metadata.version(), "1.0.0");
619 assert!(metadata.is_enabled());
620
621 let hyphenated = ToolMetadata::new("test-tool", "A test tool").unwrap();
622 assert_eq!(hyphenated.name(), "test-tool");
623 }
624
625 #[test]
626 fn test_tool_metadata_invalid_name_empty() {
627 let result = ToolMetadata::new("", "A test tool");
628 assert!(result.is_err());
629 match result.unwrap_err() {
630 ToolError::InvalidParameters { .. } => {},
631 _ => panic!("Expected InvalidParameters error"),
632 }
633 }
634
635 #[test]
636 fn test_tool_metadata_invalid_name_special_chars() {
637 let result = ToolMetadata::new("test-tool!", "A test tool");
638 assert!(result.is_err());
639 match result.unwrap_err() {
640 ToolError::InvalidParameters { .. } => {},
641 _ => panic!("Expected InvalidParameters error"),
642 }
643 }
644
645 #[test]
646 fn test_tool_metadata_builder() {
647 let metadata = ToolMetadata::new("test_tool", "A test tool")
648 .unwrap()
649 .with_version("2.0.0")
650 .with_author("Test Author")
651 .with_tags(["tag1", "tag2"])
652 .with_enabled(false);
653
654 assert_eq!(metadata.version(), "2.0.0");
655 assert_eq!(metadata.author(), Some("Test Author"));
656 assert_eq!(metadata.tags().len(), 2);
657 assert!(!metadata.is_enabled());
658 }
659
660 #[test]
661 fn test_conversions_to_json() {
662 let value = conversions::to_json(42).unwrap();
663 assert_eq!(value, 42);
664 }
665
666 #[test]
667 fn test_conversions_from_json_string() {
668 let value = serde_json::Value::String("hello".to_string());
669 let result = conversions::from_json_string(value).unwrap();
670 assert_eq!(result, "hello");
671 }
672
673 #[test]
674 fn test_conversions_from_json_string_invalid() {
675 let value = serde_json::Value::Number(42.into());
676 let result = conversions::from_json_string(value);
677 assert!(result.is_err());
678 }
679
680 #[test]
681 fn test_conversions_from_json_i32() {
682 let value = serde_json::Value::Number(42.into());
683 let result = conversions::from_json_i32(value).unwrap();
684 assert_eq!(result, 42);
685 }
686
687 #[test]
688 fn test_conversions_from_json_f64() {
689 let value = serde_json::json!(3.5);
690 let result = conversions::from_json_f64(value).unwrap();
691 assert_eq!(result, 3.5);
692 }
693
694 #[test]
695 fn test_conversions_from_json_bool() {
696 let value = serde_json::Value::Bool(true);
697 let result = conversions::from_json_bool(value).unwrap();
698 assert!(result);
699 }
700
701 #[test]
702 fn test_function_tool_builder() {
703 let tool = FunctionTool::builder("test_tool", "A test tool")
704 .property("param1", serde_json::json!({"type": "string"}))
705 .property("param2", serde_json::json!({"type": "number"}))
706 .required("param1")
707 .handler(|_args| async move { Ok(serde_json::json!({"result": "ok"})) })
708 .build();
709
710 assert!(tool.is_ok());
711 let tool = tool.unwrap();
712 assert_eq!(tool.name(), "test_tool");
713 }
714
715 #[test]
716 fn test_function_tool_clone() {
717 let tool1 = FunctionTool::builder("test_tool", "A test tool")
718 .property("param1", serde_json::json!({"type": "string"}))
719 .required("param1")
720 .handler(|_args| async move { Ok(serde_json::json!({"result": "ok"})) })
721 .build()
722 .unwrap();
723
724 let tool2 = tool1.clone();
725 assert_eq!(tool1.name(), tool2.name());
726 assert_eq!(tool1.input_schema(), tool2.input_schema());
727 }
728
729 #[test]
730 fn function_tool_rejects_non_object_input_schemas() {
731 for schema in [
732 serde_json::json!(true),
733 serde_json::json!({"type": "string"}),
734 serde_json::json!([{"type": "object"}]),
735 ] {
736 let result = FunctionTool::builder("invalid_schema", "test fixture")
737 .schema(schema)
738 .handler(|input| async move { Ok(input) })
739 .build();
740 assert!(matches!(result, Err(ToolError::InvalidParameters { .. })));
741 }
742 }
743
744 #[test]
745 fn test_parse_function_spec_shape1() {
746 let spec = serde_json::json!({
747 "name": "test_tool",
748 "description": "A test tool",
749 "parameters": {
750 "type": "object",
751 "properties": {
752 "param1": {"type": "string"}
753 }
754 }
755 });
756
757 let (name, description, parameters) = parse_function_spec_details(&spec).unwrap();
758 assert_eq!(name, "test_tool");
759 assert_eq!(description, "A test tool");
760 assert!(parameters.is_some());
761 }
762
763 #[test]
764 fn test_parse_function_spec_shape2() {
765 let spec = serde_json::json!({
766 "type": "function",
767 "function": {
768 "name": "test_tool",
769 "description": "A test tool",
770 "parameters": {
771 "type": "object",
772 "properties": {
773 "param1": {"type": "string"}
774 }
775 }
776 }
777 });
778
779 let (name, description, parameters) = parse_function_spec_details(&spec).unwrap();
780 assert_eq!(name, "test_tool");
781 assert_eq!(description, "A test tool");
782 assert!(parameters.is_some());
783 }
784
785 #[test]
786 fn test_parse_function_spec_invalid() {
787 let spec = serde_json::Value::String("invalid".to_string());
788 let result = parse_function_spec_details(&spec);
789 assert!(result.is_err());
790 }
791}