use alloc::{borrow::Cow, boxed::Box, format, string::ToString};
use core::{future::Future, pin::Pin};
use super::types::{
OutputSanitization, ToolContext, ToolDefinition, ToolEffect, ToolError, ToolOutput,
};
#[derive(
Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
)]
pub struct EmptyParams {}
pub trait RustTool: Send + Sync {
type Params: serde::de::DeserializeOwned + schemars::JsonSchema + Send;
const NAME: &'static str;
const DESCRIPTION: &'static str;
const EFFECT: ToolEffect = if Self::IDEMPOTENT {
ToolEffect::ReadOnly
} else {
ToolEffect::Mutating
};
const IDEMPOTENT: bool = false;
const SANITIZE_OUTPUT: bool = true;
const QUARANTINE_OUTPUT: bool = false;
const OUTPUT_SANITIZATION: OutputSanitization = if Self::QUARANTINE_OUTPUT {
OutputSanitization::Quarantine
} else if Self::SANITIZE_OUTPUT {
OutputSanitization::SanitizeTokens
} else {
OutputSanitization::None
};
fn effect(&self) -> ToolEffect {
Self::EFFECT
}
fn is_idempotent(&self) -> bool {
matches!(self.effect(), ToolEffect::ReadOnly)
}
fn output_sanitization(&self) -> OutputSanitization {
Self::OUTPUT_SANITIZATION
}
fn description(&self) -> Cow<'static, str> {
Cow::Borrowed(Self::DESCRIPTION)
}
fn call(
&self,
params: Self::Params,
ctx: &ToolContext,
) -> impl Future<Output = Result<ToolOutput, ToolError>> + Send;
}
#[derive(Debug)]
pub struct SharedRustTool<T>(pub alloc::sync::Arc<T>);
impl<T> Clone for SharedRustTool<T> {
fn clone(&self) -> Self {
Self(alloc::sync::Arc::clone(&self.0))
}
}
impl<T> SharedRustTool<T> {
#[must_use]
pub fn new(tool: T) -> Self {
Self(alloc::sync::Arc::new(tool))
}
#[must_use]
pub fn as_inner(&self) -> &T {
&self.0
}
}
impl<T> From<alloc::sync::Arc<T>> for SharedRustTool<T> {
fn from(arc: alloc::sync::Arc<T>) -> Self {
Self(arc)
}
}
impl<T: RustTool> RustTool for alloc::sync::Arc<T> {
type Params = T::Params;
const NAME: &'static str = T::NAME;
const DESCRIPTION: &'static str = T::DESCRIPTION;
const EFFECT: ToolEffect = T::EFFECT;
const IDEMPOTENT: bool = T::IDEMPOTENT;
const SANITIZE_OUTPUT: bool = T::SANITIZE_OUTPUT;
const QUARANTINE_OUTPUT: bool = T::QUARANTINE_OUTPUT;
const OUTPUT_SANITIZATION: OutputSanitization = T::OUTPUT_SANITIZATION;
fn effect(&self) -> ToolEffect {
(**self).effect()
}
fn is_idempotent(&self) -> bool {
(**self).is_idempotent()
}
fn output_sanitization(&self) -> OutputSanitization {
(**self).output_sanitization()
}
fn description(&self) -> Cow<'static, str> {
(**self).description()
}
fn call(
&self,
params: Self::Params,
ctx: &ToolContext,
) -> impl Future<Output = Result<ToolOutput, ToolError>> + Send {
(**self).call(params, ctx)
}
}
impl<T: RustTool> RustTool for SharedRustTool<T> {
type Params = T::Params;
const NAME: &'static str = T::NAME;
const DESCRIPTION: &'static str = T::DESCRIPTION;
const EFFECT: ToolEffect = T::EFFECT;
const IDEMPOTENT: bool = T::IDEMPOTENT;
const SANITIZE_OUTPUT: bool = T::SANITIZE_OUTPUT;
const QUARANTINE_OUTPUT: bool = T::QUARANTINE_OUTPUT;
const OUTPUT_SANITIZATION: OutputSanitization = T::OUTPUT_SANITIZATION;
fn effect(&self) -> ToolEffect {
self.0.effect()
}
fn is_idempotent(&self) -> bool {
self.0.is_idempotent()
}
fn output_sanitization(&self) -> OutputSanitization {
self.0.output_sanitization()
}
fn description(&self) -> Cow<'static, str> {
self.0.description()
}
fn call(
&self,
params: Self::Params,
ctx: &ToolContext,
) -> impl Future<Output = Result<ToolOutput, ToolError>> + Send {
self.0.call(params, ctx)
}
}
pub fn definition_of<T: RustTool>(tool: &T) -> Result<ToolDefinition, ToolError> {
let schema = schemars::schema_for!(T::Params);
let mut parameter_schema = serde_json::to_value(schema).map_err(|e| {
ToolError::new(format!(
"Failed to serialize schema for tool '{}': {e}",
T::NAME
))
})?;
sanitize_schema_types(&mut parameter_schema);
Ok(ToolDefinition::with_effect(
T::NAME,
tool.description().into_owned(),
parameter_schema,
tool.effect(),
)
.with_output_sanitization(tool.output_sanitization()))
}
fn sanitize_schema_types(value: &mut serde_json::Value) {
match value {
serde_json::Value::Object(map) => {
let replacement = match map.get("type") {
Some(serde_json::Value::Array(arr)) => {
let non_null = arr.iter().find(|v| v.as_str() != Some("null")).cloned();
non_null.or_else(|| arr.first().cloned())
}
_ => None,
};
if let Some(val) = replacement {
map.insert("type".to_string(), val);
}
for val in map.values_mut() {
sanitize_schema_types(val);
}
}
serde_json::Value::Array(arr) => {
for item in arr {
sanitize_schema_types(item);
}
}
_ => {}
}
}
pub type BoxToolFuture<'a> =
Pin<Box<dyn Future<Output = Result<ToolOutput, ToolError>> + Send + 'a>>;
pub(crate) trait ErasedTool: Send + Sync {
fn call_erased<'a>(
&'a self,
args: serde_json::Value,
ctx: &'a ToolContext,
) -> BoxToolFuture<'a>;
fn call_erased_str<'a>(&'a self, args_json: &'a str, ctx: &'a ToolContext)
-> BoxToolFuture<'a>;
}
impl<T: RustTool> ErasedTool for T {
fn call_erased<'a>(
&'a self,
args: serde_json::Value,
ctx: &'a ToolContext,
) -> BoxToolFuture<'a> {
Box::pin(async move {
let params: T::Params = serde_json::from_value(args).map_err(|e| {
ToolError::new(format!("Failed to deserialize tool parameters: {e}"))
})?;
let output = self.call(params, ctx).await?;
Ok(self.output_sanitization().apply(output))
})
}
fn call_erased_str<'a>(
&'a self,
args_json: &'a str,
ctx: &'a ToolContext,
) -> BoxToolFuture<'a> {
Box::pin(async move {
let params: T::Params = serde_json::from_str(args_json).map_err(|e| {
ToolError::new(format!("Failed to deserialize tool parameters: {e}"))
})?;
let output = self.call(params, ctx).await?;
Ok(self.output_sanitization().apply(output))
})
}
}