use std::{fmt, future::Future, pin::Pin, sync::Arc};
use schemars::JsonSchema as SchemarsJsonSchema;
use serde::{Serialize, de::DeserializeOwned};
use tokio::sync::{mpsc, oneshot};
use crate::{
ToolError,
declaration::ToolInfo,
execution::{ToolInvocation, ToolResult},
interaction::{
InteractionResolution, ToolInteractionCapabilities, ToolInteractionId, ToolInteractionKind,
ToolInteractionRequest, ToolInteractionResponse,
},
name::ToolName,
schema::JsonSchema,
};
#[derive(Clone)]
pub struct ToolRegistration {
inner: Arc<RegisteredTool>,
}
pub(crate) struct RegisteredTool {
info: ToolInfo,
callback: Arc<dyn RegisteredToolCallback>,
}
pub(crate) struct RegisteredToolCall {
future: Pin<Box<dyn Future<Output = Result<ToolResult, ToolError>> + Send>>,
}
impl RegisteredToolCall {
pub(crate) async fn run(self) -> Result<ToolResult, ToolError> {
self.future.await
}
}
trait RegisteredToolCallback: Send + Sync {
fn start(
&self,
invocation: ToolInvocation,
context: ToolInvocationContext,
) -> RegisteredToolCall;
}
impl<F, Fut> RegisteredToolCallback for F
where
F: Fn(ToolInvocation, ToolInvocationContext) -> Fut + Send + Sync,
Fut: Future<Output = Result<ToolResult, ToolError>> + Send + 'static,
{
fn start(
&self,
invocation: ToolInvocation,
context: ToolInvocationContext,
) -> RegisteredToolCall {
RegisteredToolCall {
future: Box::pin(self(invocation, context)),
}
}
}
impl RegisteredTool {
pub(crate) fn info(&self) -> &ToolInfo {
&self.info
}
pub(crate) fn call(
&self,
invocation: ToolInvocation,
context: ToolInvocationContext,
) -> Result<RegisteredToolCall, ToolError> {
let issues = self.info.input_schema.validate(&invocation.arguments)?;
if !issues.is_empty() {
return Err(ToolError::InvalidArguments {
tool: self.info.name.clone(),
message: "tool arguments failed schema validation".to_owned(),
issues,
});
}
Ok(self.callback.start(invocation, context))
}
}
#[derive(Clone)]
pub struct ToolInvocationContext {
tool_name: ToolName,
call_id: Option<String>,
interactions: mpsc::Sender<LocalInteractionEnvelope>,
capabilities: ToolInteractionCapabilities,
}
pub(crate) struct LocalInteractionEnvelope {
request: ToolInteractionRequest,
response: oneshot::Sender<ToolInteractionResponse>,
}
impl LocalInteractionEnvelope {
pub(crate) fn into_parts(
self,
) -> (
ToolInteractionRequest,
oneshot::Sender<ToolInteractionResponse>,
) {
(self.request, self.response)
}
}
impl ToolRegistration {
pub(crate) fn into_registered(self) -> RegisteredTool {
Arc::try_unwrap(self.inner).unwrap_or_else(|inner| (*inner).clone())
}
#[must_use]
pub fn function<F, Fut>(info: ToolInfo, handler: F) -> Self
where
F: Fn(ToolInvocation) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<ToolResult, ToolError>> + Send + 'static,
{
let callback = move |invocation, _context| handler(invocation);
Self::new(info, callback)
}
#[must_use]
pub fn interactive<F, Fut>(info: ToolInfo, handler: F) -> Self
where
F: Fn(ToolInvocation, ToolInvocationContext) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<ToolResult, ToolError>> + Send + 'static,
{
let callback = move |invocation, context| handler(invocation, context);
Self::new(info, callback)
}
pub fn typed<N, TArgs, TOutput, F, Fut>(name: N, handler: F) -> Result<Self, ToolError>
where
N: TryInto<ToolName>,
N::Error: fmt::Display,
TArgs: DeserializeOwned + SchemarsJsonSchema + Send + 'static,
TOutput: Serialize + SchemarsJsonSchema + Send + 'static,
F: Fn(TArgs) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<TOutput, ToolError>> + Send + 'static,
{
let name = name
.try_into()
.map_err(|error| ToolError::InvalidConfiguration {
message: error.to_string(),
})?;
let handler_name = name.clone();
let handler = Arc::new(handler);
let input_schema = Self::schema_document::<TArgs>()?;
let output_schema = Self::schema_document::<TOutput>()?;
let mut info = ToolInfo::new(name, input_schema);
info.output_schema = Some(output_schema);
Ok(Self::function(info, move |invocation| {
let handler_name = handler_name.clone();
let handler = Arc::clone(&handler);
async move {
let arguments =
Self::deserialize_arguments::<TArgs>(&handler_name, invocation.arguments)?;
let output = handler(arguments).await?;
let value = serde_json::to_value(output).map_err(|error| ToolError::Protocol {
message: format!("failed to serialize typed tool output: {error}"),
})?;
Ok(ToolResult::json(value))
}
}))
}
fn new<F, Fut>(info: ToolInfo, callback: F) -> Self
where
F: Fn(ToolInvocation, ToolInvocationContext) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<ToolResult, ToolError>> + Send + 'static,
{
Self {
inner: Arc::new(RegisteredTool {
info,
callback: Arc::new(callback),
}),
}
}
#[must_use]
pub fn description(self, description: impl Into<String>) -> Self {
self.map_info(|info| info.description = Some(description.into()))
}
#[must_use]
pub fn interactions(self, interactions: ToolInteractionCapabilities) -> Self {
self.map_info(|info| info.interactions = interactions)
}
fn map_info(self, update: impl FnOnce(&mut ToolInfo)) -> Self {
let mut inner = Arc::try_unwrap(self.inner).unwrap_or_else(|inner| (*inner).clone());
update(&mut inner.info);
Self {
inner: Arc::new(inner),
}
}
fn schema_document<T: SchemarsJsonSchema>() -> Result<JsonSchema, ToolError> {
let schema = serde_json::to_value(schemars::schema_for!(T)).map_err(|error| {
ToolError::Protocol {
message: format!("failed to serialize generated JSON Schema: {error}"),
}
})?;
Ok(JsonSchema::strict(schema))
}
fn deserialize_arguments<TArgs>(
tool: &ToolName,
arguments: serde_json::Value,
) -> Result<TArgs, ToolError>
where
TArgs: DeserializeOwned,
{
let serialized = arguments.to_string();
let mut deserializer = serde_json::Deserializer::from_str(&serialized);
serde_path_to_error::deserialize(&mut deserializer).map_err(|error| {
ToolError::InvalidArguments {
tool: tool.clone(),
message: error.to_string(),
issues: Vec::new(),
}
})
}
}
impl Clone for RegisteredTool {
fn clone(&self) -> Self {
Self {
info: self.info.clone(),
callback: Arc::clone(&self.callback),
}
}
}
impl fmt::Debug for RegisteredTool {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("RegisteredTool")
.field("info", &self.info)
.finish_non_exhaustive()
}
}
impl fmt::Debug for ToolRegistration {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ToolRegistration")
.field("name", &self.inner.info.name)
.finish_non_exhaustive()
}
}
impl ToolInvocationContext {
pub(crate) fn new(
tool_name: ToolName,
call_id: Option<String>,
interactions: mpsc::Sender<LocalInteractionEnvelope>,
capabilities: ToolInteractionCapabilities,
) -> Self {
Self {
tool_name,
call_id,
interactions,
capabilities,
}
}
pub async fn request_text(
&self,
prompt: impl Into<String>,
multiline: bool,
) -> Result<String, ToolError> {
if !self.capabilities.text || (multiline && !self.capabilities.multiline_text) {
return Err(ToolError::UndeclaredInteraction {
tool: self.tool_name.clone(),
kind: "text".to_owned(),
});
}
let response = self
.request(ToolInteractionRequest {
id: ToolInteractionId::new(),
tool_name: self.tool_name.clone(),
call_id: self.call_id.clone(),
prompt: prompt.into(),
kind: ToolInteractionKind::Text { multiline },
})
.await?;
match response.into_resolution() {
InteractionResolution::Submit(content) => content
.as_str()
.map(str::to_owned)
.ok_or_else(|| ToolError::InvalidInteraction {
id: "submitted-text".to_owned(),
message: "text interaction content must be a string".to_owned(),
}),
InteractionResolution::Decline(reason) => Err(ToolError::InteractionDeclined {
tool: self.tool_name.clone(),
reason,
}),
InteractionResolution::Cancel(reason) => Err(ToolError::Cancelled {
tool: self.tool_name.clone(),
reason,
}),
}
}
pub async fn request_form(
&self,
prompt: impl Into<String>,
schema: serde_json::Value,
schema_validation: bool,
) -> Result<serde_json::Value, ToolError> {
if !self.capabilities.form
|| (schema_validation && !self.capabilities.form_schema_validation)
{
return Err(ToolError::UndeclaredInteraction {
tool: self.tool_name.clone(),
kind: "form".to_owned(),
});
}
let response = self
.request(ToolInteractionRequest {
id: ToolInteractionId::new(),
tool_name: self.tool_name.clone(),
call_id: self.call_id.clone(),
prompt: prompt.into(),
kind: ToolInteractionKind::Form {
schema,
schema_validation,
},
})
.await?;
match response.into_resolution() {
InteractionResolution::Submit(content) => Ok(content),
InteractionResolution::Decline(reason) => Err(ToolError::InteractionDeclined {
tool: self.tool_name.clone(),
reason,
}),
InteractionResolution::Cancel(reason) => Err(ToolError::Cancelled {
tool: self.tool_name.clone(),
reason,
}),
}
}
async fn request(
&self,
request: ToolInteractionRequest,
) -> Result<ToolInteractionResponse, ToolError> {
let (response, receiver) = oneshot::channel();
self.interactions
.send(LocalInteractionEnvelope {
request: request.clone(),
response,
})
.await
.map_err(|_| ToolError::Transport {
tool: Some(self.tool_name.clone()),
message: "local interaction channel closed".to_owned(),
})?;
let response = receiver.await.map_err(|_| ToolError::Cancelled {
tool: self.tool_name.clone(),
reason: Some("local interaction response channel closed".to_owned()),
})?;
request.validate_response(&response)?;
Ok(response)
}
}
impl fmt::Debug for ToolInvocationContext {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ToolInvocationContext")
.field("tool_name", &self.tool_name)
.field("capabilities", &self.capabilities)
.finish_non_exhaustive()
}
}