use std::{borrow::Cow, collections::HashMap};
#[cfg(feature = "toolkits")]
use std::sync::{Arc, LazyLock};
use crate::toolkits::error::{ToolResult, error_context};
use async_trait::async_trait;
use serde::Serialize;
#[cfg(feature = "toolkits")]
type CompiledSchema = Arc<jsonschema::Validator>;
#[async_trait]
pub trait DynTool: Send + Sync {
fn metadata(&self) -> &ToolMetadata;
async fn execute_json(&self, input: serde_json::Value) -> ToolResult<serde_json::Value>;
fn input_schema(&self) -> serde_json::Value;
fn name(&self) -> &str {
self.metadata().name()
}
}
#[cfg(feature = "toolkits")]
static SCHEMA_CACHE: LazyLock<std::sync::RwLock<HashMap<String, Arc<jsonschema::Validator>>>> =
LazyLock::new(|| std::sync::RwLock::new(HashMap::new()));
#[cfg(feature = "toolkits")]
const SCHEMA_CACHE_MAX_SIZE: usize = 256;
#[derive(Debug, Clone, Serialize)]
pub struct ToolMetadata {
name: Cow<'static, str>,
description: Cow<'static, str>,
version: Cow<'static, str>,
author: Option<Cow<'static, str>>,
tags: Vec<Cow<'static, str>>,
enabled: bool,
metadata: HashMap<Cow<'static, str>, serde_json::Value>,
}
impl ToolMetadata {
pub fn new(name: impl Into<String>, description: impl Into<String>) -> ToolResult<Self> {
let name = name.into();
let description = description.into();
validate_tool_name(&name)?;
Ok(Self {
name: Cow::Owned(name),
description: Cow::Owned(description),
version: Cow::Borrowed("1.0.0"),
author: None,
tags: Vec::new(),
enabled: true,
metadata: HashMap::new(),
})
}
pub fn name(&self) -> &str {
&self.name
}
pub fn description(&self) -> &str {
&self.description
}
pub fn version(&self) -> &str {
&self.version
}
pub fn author(&self) -> Option<&str> {
self.author.as_deref()
}
pub fn tags(&self) -> &[Cow<'static, str>] {
&self.tags
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn additional_metadata(&self) -> &HashMap<Cow<'static, str>, serde_json::Value> {
&self.metadata
}
pub fn with_version(mut self, version: impl Into<Cow<'static, str>>) -> Self {
self.version = version.into();
self
}
pub fn with_author(mut self, author: impl Into<Cow<'static, str>>) -> Self {
self.author = Some(author.into());
self
}
pub fn with_tags<T: Into<Cow<'static, str>>>(
mut self,
tags: impl IntoIterator<Item = T>,
) -> Self {
self.tags = tags.into_iter().map(Into::into).collect();
self
}
pub fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn with_metadata(
mut self,
key: impl Into<Cow<'static, str>>,
value: serde_json::Value,
) -> Self {
self.metadata.insert(key.into(), value);
self
}
}
pub(crate) fn validate_tool_name(name: &str) -> ToolResult<()> {
if name.trim().is_empty() {
return Err(error_context().invalid_parameters("Tool name cannot be empty"));
}
if name.len() > 64 {
return Err(error_context().invalid_parameters("Tool name cannot exceed 64 characters"));
}
if !name
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
{
return Err(error_context().invalid_parameters(
"Tool name must contain only ASCII letters, digits, underscores, and hyphens",
));
}
Ok(())
}
pub mod conversions {
use crate::toolkits::error::{ToolResult, error_context};
pub fn to_json<T: serde::Serialize>(value: T) -> ToolResult<serde_json::Value> {
serde_json::to_value(value).map_err(|e| error_context().serialization_error(e))
}
pub fn from_json_string(value: serde_json::Value) -> ToolResult<String> {
match value {
serde_json::Value::String(s) => Ok(s),
_ => Err(error_context().invalid_parameters("Expected string value")),
}
}
pub fn from_json_i32(value: serde_json::Value) -> ToolResult<i32> {
match value {
serde_json::Value::Number(n) => n
.as_i64()
.and_then(|i| i.try_into().ok())
.ok_or_else(|| error_context().invalid_parameters("Expected i32 value")),
_ => Err(error_context().invalid_parameters("Expected number value")),
}
}
pub fn from_json_f64(value: serde_json::Value) -> ToolResult<f64> {
match value {
serde_json::Value::Number(n) => n
.as_f64()
.ok_or_else(|| error_context().invalid_parameters("Expected f64 value")),
_ => Err(error_context().invalid_parameters("Expected number value")),
}
}
pub fn from_json_bool(value: serde_json::Value) -> ToolResult<bool> {
match value {
serde_json::Value::Bool(b) => Ok(b),
_ => Err(error_context().invalid_parameters("Expected boolean value")),
}
}
}
pub type ToolHandler = std::sync::Arc<
dyn Fn(
serde_json::Value,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = ToolResult<serde_json::Value>> + Send>,
> + Send
+ Sync,
>;
pub struct FunctionTool {
metadata: ToolMetadata,
input_schema: serde_json::Value,
#[cfg(feature = "toolkits")]
compiled_schema: CompiledSchema,
handler: ToolHandler,
}
impl Clone for FunctionTool {
fn clone(&self) -> Self {
Self {
metadata: self.metadata.clone(),
input_schema: self.input_schema.clone(),
#[cfg(feature = "toolkits")]
compiled_schema: Arc::clone(&self.compiled_schema),
handler: self.handler.clone(),
}
}
}
impl FunctionTool {
pub fn builder(name: impl Into<String>, description: impl Into<String>) -> FunctionToolBuilder {
FunctionToolBuilder::new(name, description)
}
pub fn from_schema<F, Fut>(
name: impl Into<String>,
description: impl Into<String>,
schema: serde_json::Value,
f: F,
) -> ToolResult<FunctionTool>
where
F: Fn(serde_json::Value) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = ToolResult<serde_json::Value>> + Send + 'static,
{
Self::builder(name, description)
.schema(schema)
.handler(f)
.build()
}
pub fn from_function_spec<F, Fut>(spec: serde_json::Value, f: F) -> ToolResult<FunctionTool>
where
F: Fn(serde_json::Value) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = ToolResult<serde_json::Value>> + Send + 'static,
{
let (name, description, parameters) = parse_function_spec_details(&spec)?;
let mut builder = Self::builder(name, description);
if let Some(p) = parameters {
builder = builder.schema(p);
}
builder.handler(f).build()
}
pub fn from_function_spec_file<F, Fut>(
path: impl AsRef<std::path::Path>,
f: F,
) -> ToolResult<FunctionTool>
where
F: Fn(serde_json::Value) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = ToolResult<serde_json::Value>> + Send + 'static,
{
let content = std::fs::read_to_string(path).map_err(|e| {
error_context().invalid_parameters(format!("Failed to read spec file: {e}"))
})?;
let spec: serde_json::Value = serde_json::from_str(&content)
.map_err(|e| error_context().invalid_parameters(format!("Invalid JSON: {e}")))?;
Self::from_function_spec(spec, f)
}
}
#[cfg(feature = "toolkits")]
fn compile_schema_cached(
schema: &serde_json::Value,
tool_name: &str,
) -> ToolResult<Arc<jsonschema::Validator>> {
let cache_key = schema.to_string();
{
let cache = SCHEMA_CACHE
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(cached) = cache.get(&cache_key) {
return Ok(Arc::clone(cached));
}
}
let validator = jsonschema::validator_for(schema).map_err(|e| {
error_context()
.with_tool(tool_name)
.schema_validation(format!("Failed to compile schema: {e}"))
})?;
let validator = Arc::new(validator);
{
let mut cache = SCHEMA_CACHE
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if cache.len() >= SCHEMA_CACHE_MAX_SIZE {
let remove_count = (SCHEMA_CACHE_MAX_SIZE / 10).max(1);
let keys: Vec<String> = cache.keys().take(remove_count).cloned().collect();
for k in keys {
cache.remove(&k);
}
}
cache.insert(cache_key, Arc::clone(&validator));
}
Ok(validator)
}
pub(crate) fn parse_function_spec_details(
spec: &serde_json::Value,
) -> ToolResult<(String, String, Option<serde_json::Value>)> {
use serde_json::Value;
let obj = match spec {
Value::Object(map) => map,
_ => return Err(error_context().invalid_parameters("Function spec must be a JSON object")),
};
let (name, desc, params) = if obj.get("type").and_then(|v| v.as_str()) == Some("function") {
let f = obj
.get("function")
.and_then(|v| v.as_object())
.ok_or_else(|| error_context().invalid_parameters("Missing 'function' object"))?;
let name = f
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| error_context().invalid_parameters("Missing function.name"))?
.to_string();
let desc = f
.get("description")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let params = f.get("parameters").cloned();
(name, desc, params)
} else {
let name = obj
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| error_context().invalid_parameters("Missing name"))?
.to_string();
let desc = obj
.get("description")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let params = obj.get("parameters").cloned();
(name, desc, params)
};
Ok((name, desc, params))
}
pub struct FunctionToolBuilder {
metadata: ToolMetadata,
input_schema: Option<serde_json::Value>,
staged_properties: Option<serde_json::Map<String, serde_json::Value>>,
staged_required: Vec<String>,
handler: Option<ToolHandler>,
}
impl FunctionToolBuilder {
pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
let metadata = ToolMetadata {
name: Cow::Owned(name.into()),
description: Cow::Owned(description.into()),
version: Cow::Borrowed("1.0.0"),
author: None,
tags: Vec::new(),
enabled: true,
metadata: HashMap::new(),
};
Self {
metadata,
input_schema: None,
staged_properties: None,
staged_required: Vec::new(),
handler: None,
}
}
pub fn schema(mut self, schema: serde_json::Value) -> Self {
self.input_schema = Some(schema);
self
}
pub fn metadata(mut self, f: impl FnOnce(ToolMetadata) -> ToolMetadata) -> Self {
self.metadata = f(self.metadata);
self
}
pub fn handler<F, Fut>(mut self, f: F) -> Self
where
F: Fn(serde_json::Value) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = ToolResult<serde_json::Value>> + Send + 'static,
{
let wrapped = move |args: serde_json::Value| -> std::pin::Pin<
Box<dyn std::future::Future<Output = ToolResult<serde_json::Value>> + Send>,
> { Box::pin(f(args)) };
self.handler = Some(std::sync::Arc::new(wrapped));
self
}
pub fn property(mut self, name: impl Into<String>, schema: serde_json::Value) -> Self {
let name = name.into();
let entry = self
.staged_properties
.get_or_insert_with(serde_json::Map::new);
entry.insert(name, schema);
self
}
pub fn required(mut self, name: impl Into<String>) -> Self {
self.staged_required.push(name.into());
self
}
pub fn build(mut self) -> ToolResult<FunctionTool> {
validate_tool_name(&self.metadata.name)?;
let handler = self
.handler
.ok_or_else(|| error_context().invalid_parameters("FunctionTool handler not set"))?;
let mut schema = self
.input_schema
.take()
.unwrap_or_else(|| serde_json::json!({}));
if let serde_json::Value::Object(ref mut obj) = schema {
if obj
.get("type")
.is_some_and(|schema_type| schema_type.as_str() != Some("object"))
{
return Err(error_context()
.with_tool(self.metadata.name.clone())
.invalid_parameters("tool input schema type must be 'object'"));
}
obj.entry("type")
.or_insert(serde_json::Value::String("object".to_string()));
obj.entry("additionalProperties")
.or_insert(serde_json::Value::Bool(false));
if let Some(staged) = self.staged_properties.take() {
let props = obj
.entry("properties")
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
let props = props.as_object_mut().ok_or_else(|| {
error_context()
.with_tool(self.metadata.name.clone())
.invalid_parameters("schema.properties must be an object")
})?;
for (name, property_schema) in staged {
props.insert(name, property_schema);
}
}
if !self.staged_required.is_empty() {
use std::collections::BTreeSet;
let mut required = BTreeSet::new();
if let Some(existing) = obj.get("required") {
let entries = existing.as_array().ok_or_else(|| {
error_context()
.with_tool(self.metadata.name.clone())
.invalid_parameters("schema.required must be an array of strings")
})?;
for entry in entries {
let name = entry.as_str().ok_or_else(|| {
error_context()
.with_tool(self.metadata.name.clone())
.invalid_parameters("schema.required must contain only strings")
})?;
required.insert(name.to_string());
}
}
required.extend(self.staged_required);
obj.insert(
"required".to_string(),
serde_json::Value::Array(
required
.into_iter()
.map(serde_json::Value::String)
.collect(),
),
);
}
obj.entry("properties")
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
} else {
return Err(error_context()
.with_tool(self.metadata.name.clone())
.invalid_parameters("tool input schema must be a JSON object"));
}
#[cfg(feature = "toolkits")]
let compiled_schema: CompiledSchema = compile_schema_cached(&schema, &self.metadata.name)?;
Ok(FunctionTool {
metadata: self.metadata,
input_schema: schema,
#[cfg(feature = "toolkits")]
compiled_schema,
handler,
})
}
}
#[async_trait]
impl DynTool for FunctionTool {
fn metadata(&self) -> &ToolMetadata {
&self.metadata
}
async fn execute_json(&self, input: serde_json::Value) -> ToolResult<serde_json::Value> {
#[cfg(feature = "toolkits")]
if let Err(validation_error) = self.compiled_schema.validate(&input) {
return Err(error_context()
.with_tool(self.name())
.invalid_parameters(format!("Input validation failed: {validation_error}")));
}
(self.handler)(input).await
}
fn input_schema(&self) -> serde_json::Value {
self.input_schema.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::toolkits::ToolError;
#[test]
fn test_tool_metadata_new() {
let metadata = ToolMetadata::new("test_tool", "A test tool").unwrap();
assert_eq!(metadata.name(), "test_tool");
assert_eq!(metadata.description(), "A test tool");
assert_eq!(metadata.version(), "1.0.0");
assert!(metadata.is_enabled());
let hyphenated = ToolMetadata::new("test-tool", "A test tool").unwrap();
assert_eq!(hyphenated.name(), "test-tool");
}
#[test]
fn test_tool_metadata_invalid_name_empty() {
let result = ToolMetadata::new("", "A test tool");
assert!(result.is_err());
match result.unwrap_err() {
ToolError::InvalidParameters { .. } => {},
_ => panic!("Expected InvalidParameters error"),
}
}
#[test]
fn test_tool_metadata_invalid_name_special_chars() {
let result = ToolMetadata::new("test-tool!", "A test tool");
assert!(result.is_err());
match result.unwrap_err() {
ToolError::InvalidParameters { .. } => {},
_ => panic!("Expected InvalidParameters error"),
}
}
#[test]
fn test_tool_metadata_builder() {
let metadata = ToolMetadata::new("test_tool", "A test tool")
.unwrap()
.with_version("2.0.0")
.with_author("Test Author")
.with_tags(["tag1", "tag2"])
.with_enabled(false);
assert_eq!(metadata.version(), "2.0.0");
assert_eq!(metadata.author(), Some("Test Author"));
assert_eq!(metadata.tags().len(), 2);
assert!(!metadata.is_enabled());
}
#[test]
fn test_conversions_to_json() {
let value = conversions::to_json(42).unwrap();
assert_eq!(value, 42);
}
#[test]
fn test_conversions_from_json_string() {
let value = serde_json::Value::String("hello".to_string());
let result = conversions::from_json_string(value).unwrap();
assert_eq!(result, "hello");
}
#[test]
fn test_conversions_from_json_string_invalid() {
let value = serde_json::Value::Number(42.into());
let result = conversions::from_json_string(value);
assert!(result.is_err());
}
#[test]
fn test_conversions_from_json_i32() {
let value = serde_json::Value::Number(42.into());
let result = conversions::from_json_i32(value).unwrap();
assert_eq!(result, 42);
}
#[test]
fn test_conversions_from_json_f64() {
let value = serde_json::json!(3.5);
let result = conversions::from_json_f64(value).unwrap();
assert_eq!(result, 3.5);
}
#[test]
fn test_conversions_from_json_bool() {
let value = serde_json::Value::Bool(true);
let result = conversions::from_json_bool(value).unwrap();
assert!(result);
}
#[test]
fn test_function_tool_builder() {
let tool = FunctionTool::builder("test_tool", "A test tool")
.property("param1", serde_json::json!({"type": "string"}))
.property("param2", serde_json::json!({"type": "number"}))
.required("param1")
.handler(|_args| async move { Ok(serde_json::json!({"result": "ok"})) })
.build();
assert!(tool.is_ok());
let tool = tool.unwrap();
assert_eq!(tool.name(), "test_tool");
}
#[test]
fn test_function_tool_clone() {
let tool1 = FunctionTool::builder("test_tool", "A test tool")
.property("param1", serde_json::json!({"type": "string"}))
.required("param1")
.handler(|_args| async move { Ok(serde_json::json!({"result": "ok"})) })
.build()
.unwrap();
let tool2 = tool1.clone();
assert_eq!(tool1.name(), tool2.name());
assert_eq!(tool1.input_schema(), tool2.input_schema());
}
#[test]
fn function_tool_rejects_non_object_input_schemas() {
for schema in [
serde_json::json!(true),
serde_json::json!({"type": "string"}),
serde_json::json!([{"type": "object"}]),
] {
let result = FunctionTool::builder("invalid_schema", "test fixture")
.schema(schema)
.handler(|input| async move { Ok(input) })
.build();
assert!(matches!(result, Err(ToolError::InvalidParameters { .. })));
}
}
#[test]
fn test_parse_function_spec_shape1() {
let spec = serde_json::json!({
"name": "test_tool",
"description": "A test tool",
"parameters": {
"type": "object",
"properties": {
"param1": {"type": "string"}
}
}
});
let (name, description, parameters) = parse_function_spec_details(&spec).unwrap();
assert_eq!(name, "test_tool");
assert_eq!(description, "A test tool");
assert!(parameters.is_some());
}
#[test]
fn test_parse_function_spec_shape2() {
let spec = serde_json::json!({
"type": "function",
"function": {
"name": "test_tool",
"description": "A test tool",
"parameters": {
"type": "object",
"properties": {
"param1": {"type": "string"}
}
}
}
});
let (name, description, parameters) = parse_function_spec_details(&spec).unwrap();
assert_eq!(name, "test_tool");
assert_eq!(description, "A test tool");
assert!(parameters.is_some());
}
#[test]
fn test_parse_function_spec_invalid() {
let spec = serde_json::Value::String("invalid".to_string());
let result = parse_function_spec_details(&spec);
assert!(result.is_err());
}
}