use std::collections::BTreeMap;
use std::fmt::Debug;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use dyn_clone::DynClone;
use llmy_client::req::{
ChatCompletionRequestMessageRaw, ChatCompletionRequestToolMessageContent,
ChatCompletionRequestToolMessageRaw, ChatCompletionTool, ChatCompletionToolRaw,
ChatCompletionTools, ChatCompletionToolsRaw, FunctionObjectRaw,
};
use llmy_types::error::{GeneralToolCall, LLMYError};
use llmy_types::other::WithOtherFields;
use schemars::schema_for;
use serde::de::DeserializeOwned;
use tokio::task::JoinSet;
use tracing::debug;
pub trait ToolDyn: DynClone + Debug + Send + Sync + std::any::Any {
fn name(&self) -> String;
fn description(&self) -> Option<String>;
fn schema(&self) -> schemars::Schema;
fn strict(&self) -> bool {
false
}
fn to_openai_obejct(&self) -> ChatCompletionTool {
WithOtherFields::new(ChatCompletionToolRaw {
function: WithOtherFields::new(FunctionObjectRaw {
name: self.name(),
description: self.description(),
parameters: Some(self.schema().to_value()),
strict: Some(self.strict()),
}),
})
}
fn to_mcp_tool(&self) -> rmcp::model::Tool {
let input_schema = self.schema().to_value();
let input_schema = input_schema.as_object().cloned().unwrap_or_default();
rmcp::model::Tool::new_with_raw(
self.name(),
self.description().map(Into::into),
Arc::new(input_schema),
)
}
fn validate(
&self,
arguments: serde_json::Value,
) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send + '_>> {
let _ = arguments;
Box::pin(async { Ok(()) })
}
fn call(
&self,
arguments: String,
) -> Pin<Box<dyn Future<Output = Result<String, LLMYError>> + Send + '_>> {
Box::pin(async move {
match serde_json::from_str::<serde_json::Value>(&arguments) {
Ok(value) => self.run(value).await,
Err(_) => Err(LLMYError::IncorrectToolCall(
self.name(),
arguments,
self.schema(),
)),
}
})
}
fn run(
&self,
arguments: serde_json::Value,
) -> Pin<Box<dyn Future<Output = Result<String, LLMYError>> + Send + '_>>;
}
pub fn downcast_tool<T: 'static>(tool: &dyn ToolDyn) -> &T {
(tool as &dyn std::any::Any)
.downcast_ref::<T>()
.expect("can not downcast")
}
dyn_clone::clone_trait_object!(ToolDyn);
pub trait Tool: Send + Sync + DynClone + Debug {
type ARGUMENTS: DeserializeOwned + schemars::JsonSchema + Sized + Send;
const NAME: &str;
const DESCRIPTION: Option<&str>;
const STRICT: bool = false;
fn invoke(
&self,
arguments: Self::ARGUMENTS,
) -> impl Future<Output = Result<String, LLMYError>> + Send;
fn validate(
&self,
arguments: Self::ARGUMENTS,
) -> impl Future<Output = Result<(), String>> + Send {
let _ = arguments;
async { Ok(()) }
}
}
impl<T: Tool + DynClone + 'static> ToolDyn for T {
fn name(&self) -> String {
Self::NAME.to_string()
}
fn description(&self) -> Option<String> {
Self::DESCRIPTION.map(|v| v.to_string())
}
fn schema(&self) -> schemars::Schema {
schema_for!(T::ARGUMENTS)
}
fn strict(&self) -> bool {
T::STRICT
}
fn run(
&self,
arguments: serde_json::Value,
) -> Pin<Box<dyn Future<Output = Result<String, LLMYError>> + Send + '_>> {
Box::pin(async move {
match serde_json::from_value::<T::ARGUMENTS>(arguments.clone()) {
Ok(args) => self.invoke(args).await,
Err(_) => Err(LLMYError::IncorrectToolCall(
T::NAME.to_string(),
arguments.to_string(),
schema_for!(T::ARGUMENTS),
)),
}
})
}
fn validate(
&self,
arguments: serde_json::Value,
) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send + '_>> {
Box::pin(async move {
match serde_json::from_value::<T::ARGUMENTS>(arguments) {
Ok(args) => Tool::validate(self, args).await,
Err(e) => {
tracing::error!(
"validate for {} got arguments its schema admits but its type refuses: {}",
T::NAME,
e
);
Ok(())
}
}
})
}
}
struct ToolEntryInner {
tool: Box<dyn ToolDyn>,
validator: Option<jsonschema::Validator>,
}
#[derive(Clone)]
pub struct ToolEntry {
inner: Arc<ToolEntryInner>,
}
impl Debug for ToolEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ToolEntry")
.field("tool", &self.inner.tool.name())
.field("has_validator", &self.inner.validator.is_some())
.finish()
}
}
impl ToolEntry {
fn compile(tool: &dyn ToolDyn) -> Result<jsonschema::Validator, LLMYError> {
jsonschema::validator_for(tool.schema().as_value()).map_err(|error| {
color_eyre::eyre::eyre!(
"schema of tool {} is not a valid JSON Schema document: {}",
tool.name(),
error
)
.into()
})
}
fn strict(tool: Box<dyn ToolDyn>) -> Result<Self, LLMYError> {
let validator = Self::compile(tool.as_ref())?;
Ok(Self {
inner: Arc::new(ToolEntryInner {
tool,
validator: Some(validator),
}),
})
}
fn lenient(tool: Box<dyn ToolDyn>) -> Self {
let validator = match Self::compile(tool.as_ref()) {
Ok(validator) => Some(validator),
Err(error) => {
tracing::error!(
"typed tool produced an uncompilable schema, validation abstains for it: {}",
error
);
None
}
};
Self {
inner: Arc::new(ToolEntryInner { tool, validator }),
}
}
pub fn tool(&self) -> &dyn ToolDyn {
self.inner.tool.as_ref()
}
fn validator(&self) -> Option<&jsonschema::Validator> {
self.inner.validator.as_ref()
}
}
#[derive(Default, Clone, Debug)]
pub struct ToolBox {
tools: BTreeMap<String, ToolEntry>,
}
impl ToolBox {
pub fn new() -> Self {
Self::default()
}
pub fn len(&self) -> usize {
self.tools.len()
}
pub fn entries(&self) -> impl Iterator<Item = (&String, &ToolEntry)> {
self.tools.iter()
}
pub async fn validate_calls(
&self,
calls: &[GeneralToolCall],
) -> Result<Vec<Option<serde_json::Value>>, LLMYError> {
let mut parsed = Vec::with_capacity(calls.len());
for call in calls {
let Some(entry) = self.tools.get(&call.tool_name) else {
parsed.push(None);
continue;
};
let Ok(arguments) = serde_json::from_str::<serde_json::Value>(&call.tool_args) else {
return Err(LLMYError::IncorrectToolCall(
call.tool_name.clone(),
call.tool_args.clone(),
entry.tool().schema(),
));
};
if let Some(validator) = entry.validator()
&& !validator.is_valid(&arguments)
{
return Err(LLMYError::IncorrectToolCall(
call.tool_name.clone(),
call.tool_args.clone(),
entry.tool().schema(),
));
}
if let Err(reason) = entry.tool().validate(arguments.clone()).await {
return Err(LLMYError::ToolCallRejected(call.clone(), reason));
}
parsed.push(Some(arguments));
}
Ok(parsed)
}
pub fn render_tools(&self, details: bool) -> Vec<String> {
self.tools
.iter()
.map(|(name, entry)| {
if details {
format!(
"`{}`: {:?}", name,
entry
.tool()
.description()
.unwrap_or_else(|| "no description is provided".to_string())
)
} else {
name.clone()
}
})
.collect()
}
pub fn extend(&mut self, rhs: Self) {
self.tools.extend(rhs.tools.into_iter());
}
pub fn has_tool(&self, tool: &String) -> bool {
self.tools.contains_key(tool)
}
pub fn mcp_tools(&self) -> Vec<rmcp::model::Tool> {
self.tools
.values()
.map(|entry| entry.tool().to_mcp_tool())
.collect()
}
pub fn openai_objects(&self) -> Vec<ChatCompletionTools> {
self.tools
.values()
.map(|entry| {
WithOtherFields::new(ChatCompletionToolsRaw::Function(
entry.tool().to_openai_obejct(),
))
})
.collect()
}
pub fn add_tool<T: Tool + 'static>(&mut self, tool: T) {
self.insert_entry(ToolEntry::lenient(Box::new(tool) as _));
}
pub fn add_dyn_tool(&mut self, tool: Box<dyn ToolDyn>) -> Result<(), LLMYError> {
self.insert_entry(ToolEntry::strict(tool)?);
Ok(())
}
fn insert_entry(&mut self, entry: ToolEntry) {
self.tools.insert(entry.tool().name(), entry);
}
pub fn remove_tool(&mut self, name: &str) -> bool {
self.tools.remove(name).is_some()
}
pub async fn invoke(
&self,
tool_name: String,
arguments: String,
) -> Option<Result<String, LLMYError>> {
if let Some(entry) = self.tools.get(&tool_name) {
debug!("Invoking tool {} with arguments {}", &tool_name, &arguments);
Some(entry.tool().call(arguments).await)
} else {
None
}
}
pub async fn invoke_value(
&self,
tool_name: String,
arguments: serde_json::Value,
) -> Option<Result<String, LLMYError>> {
if let Some(entry) = self.tools.get(&tool_name) {
debug!("Invoking tool {} with arguments {}", &tool_name, &arguments);
Some(entry.tool().run(arguments).await)
} else {
None
}
}
pub async fn invoke_many(
&self,
calls: Vec<GeneralToolCall>,
) -> Vec<(GeneralToolCall, Option<Result<String, LLMYError>>)> {
self.invoke_many_parsed(calls.into_iter().map(|call| (call, None)).collect())
.await
}
pub async fn invoke_many_parsed(
&self,
calls: Vec<(GeneralToolCall, Option<serde_json::Value>)>,
) -> Vec<(GeneralToolCall, Option<Result<String, LLMYError>>)> {
let mut js = JoinSet::new();
for (call, parsed) in calls {
let tb = self.clone();
js.spawn(async move {
tracing::info!("Calling {}", &call);
let result = match parsed {
Some(value) => tb.invoke_value(call.tool_name.clone(), value).await,
None => {
tb.invoke(call.tool_name.clone(), call.tool_args.clone())
.await
}
};
(call, result)
});
}
js.join_all().await
}
pub async fn invoke_many_sequential(
&self,
calls: Vec<GeneralToolCall>,
) -> Vec<(GeneralToolCall, Option<Result<String, LLMYError>>)> {
self.invoke_many_parsed_sequential(calls.into_iter().map(|call| (call, None)).collect())
.await
}
pub async fn invoke_many_parsed_sequential(
&self,
calls: Vec<(GeneralToolCall, Option<serde_json::Value>)>,
) -> Vec<(GeneralToolCall, Option<Result<String, LLMYError>>)> {
let mut out = Vec::with_capacity(calls.len());
for (call, parsed) in calls {
tracing::debug!("Calling {}", &call);
let result = match parsed {
Some(value) => self.invoke_value(call.tool_name.clone(), value).await,
None => {
self.invoke(call.tool_name.clone(), call.tool_args.clone())
.await
}
};
out.push((call, result));
}
out
}
pub async fn agent_invoke_many(
&self,
calls: Vec<GeneralToolCall>,
) -> Vec<(
GeneralToolCall,
Option<Result<ChatCompletionRequestMessageRaw, LLMYError>>,
)> {
let invokes = self.invoke_many(calls).await;
Self::agent_messages_from_invokes(invokes)
}
pub async fn agent_invoke_many_parsed(
&self,
calls: Vec<(GeneralToolCall, Option<serde_json::Value>)>,
) -> Vec<(
GeneralToolCall,
Option<Result<ChatCompletionRequestMessageRaw, LLMYError>>,
)> {
let invokes = self.invoke_many_parsed(calls).await;
Self::agent_messages_from_invokes(invokes)
}
pub async fn agent_invoke_many_sequential(
&self,
calls: Vec<GeneralToolCall>,
) -> Vec<(
GeneralToolCall,
Option<Result<ChatCompletionRequestMessageRaw, LLMYError>>,
)> {
let invokes = self.invoke_many_sequential(calls).await;
Self::agent_messages_from_invokes(invokes)
}
pub async fn agent_invoke_many_parsed_sequential(
&self,
calls: Vec<(GeneralToolCall, Option<serde_json::Value>)>,
) -> Vec<(
GeneralToolCall,
Option<Result<ChatCompletionRequestMessageRaw, LLMYError>>,
)> {
let invokes = self.invoke_many_parsed_sequential(calls).await;
Self::agent_messages_from_invokes(invokes)
}
fn agent_messages_from_invokes(
invokes: Vec<(GeneralToolCall, Option<Result<String, LLMYError>>)>,
) -> Vec<(
GeneralToolCall,
Option<Result<ChatCompletionRequestMessageRaw, LLMYError>>,
)> {
let mut out = vec![];
for (call, result) in invokes {
let id = call.tool_id.clone();
let result = result.map(|v| {
v.map(|s| {
let tool_msg = ChatCompletionRequestToolMessageRaw {
content: ChatCompletionRequestToolMessageContent::Text(s),
tool_call_id: id,
};
ChatCompletionRequestMessageRaw::Tool(WithOtherFields::new(tool_msg))
})
});
out.push((call, result));
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Clone)]
struct BadSchemaTool;
impl ToolDyn for BadSchemaTool {
fn name(&self) -> String {
"bad_schema_tool".to_string()
}
fn description(&self) -> Option<String> {
None
}
fn schema(&self) -> schemars::Schema {
serde_json::from_str(r#"{"type": "no-such-type"}"#)
.expect("the schema wrapper accepts any value")
}
fn run(
&self,
_arguments: serde_json::Value,
) -> Pin<Box<dyn Future<Output = Result<String, LLMYError>> + Send + '_>> {
Box::pin(async { Ok("ok".to_string()) })
}
}
#[test]
fn a_dyn_tool_with_an_invalid_schema_is_refused() {
let mut tools = ToolBox::new();
let error = tools
.add_dyn_tool(Box::new(BadSchemaTool))
.expect_err("invalid schema must refuse registration");
assert!(error.to_string().contains("bad_schema_tool"), "{error}");
assert_eq!(tools.len(), 0);
}
}