use std::{future::Future, sync::Arc};
use serde::{Deserialize, Serialize};
use crate::{
completion::{ToolAnnotations, ToolDefinition, message::ToolName},
serve::{ErasedHandler, adapters::ToolCallback},
wasm_compat::{WasmBoxedFuture, WasmCompatSend, WasmCompatSync},
};
use super::{IntoToolOutput, ToolContext, ToolExecutionError, ToolOutput, ToolResult, Touches};
pub trait Tool: Sized + WasmCompatSend + WasmCompatSync {
const NAME: &'static str;
type Args: for<'de> Deserialize<'de> + WasmCompatSend + WasmCompatSync;
type Output: IntoToolOutput;
type Error: std::error::Error + WasmCompatSend + WasmCompatSync + 'static;
fn description(&self) -> String;
fn parameters(&self) -> serde_json::Value;
fn annotations(&self) -> ToolAnnotations {
ToolAnnotations::default()
}
fn footprint(&self, args: &Self::Args) -> Touches {
let _ = args;
Touches::from_annotations(&self.annotations())
}
fn map_error(&self, error: Self::Error) -> ToolExecutionError {
ToolExecutionError::from_error(error)
}
fn call(
&self,
context: &mut ToolContext,
args: Self::Args,
) -> impl Future<Output = Result<Self::Output, Self::Error>> + WasmCompatSend;
}
pub trait ToolEmbedding: Tool {
type InitError: std::error::Error + WasmCompatSend + WasmCompatSync + 'static;
type Context: for<'de> Deserialize<'de> + Serialize;
type State: WasmCompatSend;
fn embedding_docs(&self) -> Vec<String>;
fn context(&self) -> Self::Context;
fn init(state: Self::State, context: Self::Context) -> Result<Self, Self::InitError>;
}
pub(crate) fn parse_tool_args<A>(args: &str) -> Result<A, ToolExecutionError>
where
A: serde::de::DeserializeOwned,
{
match serde_json::from_str(args) {
Ok(parsed) => Ok(parsed),
Err(original) if args.trim() == "null" => serde_json::from_str("{}").map_err(|_| {
ToolExecutionError::invalid_args(format!("failed to parse tool arguments: {original}"))
.with_source(original)
}),
Err(error) => Err(ToolExecutionError::invalid_args(format!(
"failed to parse tool arguments: {error}"
))
.with_source(error)),
}
}
pub(crate) fn tool_result_from<O>(outcome: Result<O, ToolExecutionError>) -> ToolResult
where
O: IntoToolOutput,
{
match outcome.and_then(IntoToolOutput::into_tool_output) {
Ok(output) => ToolResult::success(output),
Err(error) => ToolResult::failed(error),
}
}
#[cfg(not(target_family = "wasm"))]
pub type LivenessFn = Arc<dyn Fn() -> bool + Send + Sync>;
#[cfg(target_family = "wasm")]
pub type LivenessFn = Arc<dyn Fn() -> bool>;
#[derive(Clone)]
pub struct DynamicTool {
definition: ToolDefinition,
callback: Arc<dyn ToolCallback>,
liveness: Option<LivenessFn>,
}
impl DynamicTool {
pub fn new<F>(
name: ToolName,
description: impl Into<String>,
parameters: serde_json::Value,
callback: F,
) -> Self
where
F: Fn(
serde_json::Value,
) -> WasmBoxedFuture<'static, Result<ToolOutput, ToolExecutionError>>
+ WasmCompatSend
+ WasmCompatSync
+ 'static,
{
Self::new_with_context(
name,
description,
parameters,
move |_context: &mut ToolContext, arguments| callback(arguments),
)
}
pub fn new_with_context<F>(
name: ToolName,
description: impl Into<String>,
parameters: serde_json::Value,
callback: F,
) -> Self
where
F: ToolCallback + 'static,
{
Self {
definition: ToolDefinition::new(name, description, parameters),
callback: Arc::new(callback),
liveness: None,
}
}
pub fn with_annotations(mut self, annotations: ToolAnnotations) -> Self {
self.definition.annotations = annotations;
self
}
pub fn with_liveness<F>(mut self, is_live: F) -> Self
where
F: Fn() -> bool + WasmCompatSend + WasmCompatSync + 'static,
{
self.liveness = Some(Arc::new(is_live));
self
}
pub fn name(&self) -> &ToolName {
&self.definition.name
}
pub fn definition(&self) -> ToolDefinition {
self.definition.clone()
}
pub fn into_parts(self) -> (ToolDefinition, ErasedHandler, Option<LivenessFn>) {
let Self {
definition,
callback,
liveness,
} = self;
let handler = ErasedHandler::new(Self {
definition: definition.clone(),
callback,
liveness: None,
});
(definition, handler, liveness)
}
pub async fn run(&self, args: serde_json::Value) -> Result<ToolOutput, ToolExecutionError> {
self.run_with(&mut ToolContext::new(), args).await
}
pub async fn run_with(
&self,
context: &mut ToolContext,
args: serde_json::Value,
) -> Result<ToolOutput, ToolExecutionError> {
crate::serve::adapters::run_direct(context, args, async |context, args| {
(self.callback)(context, args).await
})
.await
}
pub fn is_live(&self) -> bool {
self.liveness.as_ref().is_none_or(|probe| probe())
}
}
impl crate::serve::Serve for DynamicTool {
type Family = crate::effect::family::Tool;
fn descriptor(&self) -> crate::effect::HandlerDescriptor {
let ToolDefinition {
name,
description,
parameters,
..
} = &self.definition;
crate::effect::HandlerDescriptor::tool(name, description, parameters.clone())
}
fn key(&self) -> crate::effect::HandlerKey {
crate::effect::tool_key(&self.definition.name)
}
async fn serve(
&self,
kind: crate::effect::EffectKind,
dispatch: crate::serve::Dispatch,
) -> crate::serve::Reply {
crate::serve::adapters::call_tool(kind, dispatch, None, async |context, args| {
(self.callback)(context, args).await
})
.await
}
}
impl std::fmt::Debug for DynamicTool {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DynamicTool")
.field("name", &self.definition.name)
.finish_non_exhaustive()
}
}
pub fn tool_name<T: Tool>() -> ToolName {
const { assert!(!T::NAME.is_empty(), "Tool::NAME cannot be empty") };
ToolName::new_unchecked(T::NAME)
}
pub fn tool_definition<T: Tool>(tool: &T) -> ToolDefinition {
ToolDefinition::new(tool_name::<T>(), tool.description(), tool.parameters())
.with_annotations(tool.annotations())
}