use crate::types::{
CallToolResult, ContentBlock, Example, Icon, RequestContext, Tool as ToolDefinition,
ToolAnnotations,
};
use anyhow::Result;
use schemars::{
JsonSchema, Schema,
generate::SchemaSettings,
transform::{RecursiveTransform, Transform},
};
use serde::{Serialize, de::DeserializeOwned};
use serde_json::Value;
pub trait ToolMeta: Sized + Serialize {
fn examples() -> Vec<Example<Self>> {
vec![]
}
fn annotations() -> Option<ToolAnnotations> {
None
}
fn title() -> Option<&'static str> {
None
}
fn icons() -> Option<Vec<Icon>> {
None
}
}
#[deprecated(since = "0.3.0", note = "renamed to ToolMeta")]
pub use ToolMeta as WithExamples;
pub trait ToolOutput: Serialize {
fn output_schema() -> Option<Value> {
None
}
fn to_content(&self) -> Vec<ContentBlock> {
vec![ContentBlock::text(
serde_json::to_string(self).unwrap_or_default(),
)]
}
fn structured_content(&self) -> Option<Value> {
serde_json::to_value(self).ok()
}
fn to_text(&self) -> String {
self.to_content()
.iter()
.filter_map(|block| match block {
ContentBlock::Text { text, .. } => Some(text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n")
}
}
impl ToolOutput for String {
fn to_content(&self) -> Vec<ContentBlock> {
vec![ContentBlock::text(self)]
}
fn structured_content(&self) -> Option<Value> {
None
}
fn to_text(&self) -> String {
self.clone()
}
}
impl ToolOutput for Vec<ContentBlock> {
fn to_content(&self) -> Vec<ContentBlock> {
self.clone()
}
fn structured_content(&self) -> Option<Value> {
None
}
}
pub fn schema_for<T: JsonSchema>() -> Schema {
let settings = SchemaSettings::draft2020_12().with(|settings| {
settings.meta_schema = None;
settings.inline_subschemas = true;
});
let mut schema = settings.into_generator().into_root_schema_for::<T>();
RecursiveTransform(remove_null).transform(&mut schema);
schema.remove("$schema");
schema
}
#[macro_export]
macro_rules! structured_output {
($($type:ty),+ $(,)?) => {
$(
impl $crate::traits::ToolOutput for $type {
fn output_schema() -> Option<$crate::serde_json::Value> {
Some($crate::traits::schema_for::<Self>().into())
}
}
)+
};
}
fn remove_null(schema: &mut Schema) {
if let Some(a @ Value::Array(_)) = schema.get_mut("type") {
let arr = a.as_array_mut().unwrap();
arr.retain(|v| matches!(v, Value::String(s) if s != "null"));
if arr.len() == 1 {
*a = arr.pop().unwrap();
}
}
if let Some(a @ Value::Array(_)) = schema.get_mut("enum") {
let arr = a.as_array_mut().unwrap();
arr.retain(|v| matches!(v, Value::String(s) if s != "null"));
}
}
pub trait Tool<State>: Serialize + DeserializeOwned {
type Output: ToolOutput;
fn execute(self, state: &mut State, context: &RequestContext) -> Result<Self::Output>;
}
pub trait Dispatch<State>: Sized + DeserializeOwned {
fn call(self, state: &mut State, context: &RequestContext) -> Result<CallToolResult>;
fn call_to_text(self, state: &mut State, context: &RequestContext) -> Result<String>;
}
pub trait AsToolSchema<State> {
fn schema() -> ToolDefinition;
}
pub trait AsToolsList {
fn tools_list() -> Vec<ToolDefinition>;
}
impl<T, State> AsToolSchema<State> for T
where
T: JsonSchema + ToolMeta + Tool<State>,
{
fn schema() -> ToolDefinition {
let mut schema = schema_for::<Self>();
let name = schema
.remove("title")
.unwrap()
.as_str()
.unwrap()
.to_string();
let description = schema
.remove("description")
.unwrap()
.as_str()
.unwrap()
.to_string();
let examples = Self::examples();
if !examples.is_empty() {
schema.insert(
"examples".to_string(),
serde_json::to_value(examples).unwrap(),
);
}
let mut tool = ToolDefinition::new(name, schema.into());
tool.description = Some(description);
tool.output_schema = <Self as Tool<State>>::Output::output_schema();
tool.annotations = Self::annotations();
tool.title = Self::title().map(String::from);
tool.icons = Self::icons();
tool
}
}