use crate::core::language_model::{LanguageModelOptions, LanguageModelStreamChunkType};
use crate::error::{Error, Result};
use crate::extensions::Extensions;
use derive_builder::Builder;
use schemars::Schema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::fmt::Debug;
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use tokio::sync::mpsc::UnboundedSender;
use uuid::Uuid;
pub type ToolStreamSender = UnboundedSender<LanguageModelStreamChunkType>;
pub type ToolEmitError = Box<tokio::sync::mpsc::error::SendError<LanguageModelStreamChunkType>>;
#[derive(Clone, Debug, Default)]
pub struct ToolContext {
options: Arc<LanguageModelOptions>,
stream_tx: Option<ToolStreamSender>,
}
impl ToolContext {
pub fn new(options: LanguageModelOptions) -> Self {
Self {
options: Arc::new(options),
stream_tx: None,
}
}
pub fn with_stream_tx(mut self, tx: ToolStreamSender) -> Self {
self.stream_tx = Some(tx);
self
}
pub fn options(&self) -> &LanguageModelOptions {
&self.options
}
pub fn stream_tx(&self) -> Option<&ToolStreamSender> {
self.stream_tx.as_ref()
}
pub fn emit(
&self,
chunk: LanguageModelStreamChunkType,
) -> std::result::Result<(), ToolEmitError> {
match &self.stream_tx {
Some(tx) => tx.send(chunk).map_err(Box::new),
None => Ok(()),
}
}
}
pub type ToolOutput = std::result::Result<String, String>;
pub type ToolFuture = Pin<Box<dyn Future<Output = ToolOutput> + Send>>;
type SyncToolFn = dyn Fn(ToolContext, Value) -> ToolOutput + Send + Sync;
type AsyncToolFn = dyn Fn(ToolContext, Value) -> ToolFuture + Send + Sync;
#[derive(Clone)]
enum ToolExecuteInner {
Sync(Arc<SyncToolFn>),
Async(Arc<AsyncToolFn>),
}
#[derive(Clone)]
pub struct ToolExecute {
inner: ToolExecuteInner,
}
impl ToolExecute {
pub async fn call(&self, context: ToolContext, map: Value) -> Result<String> {
match &self.inner {
ToolExecuteInner::Sync(f) => (f)(context, map).map_err(Error::ToolCallError),
ToolExecuteInner::Async(f) => (f)(context, map).await.map_err(Error::ToolCallError),
}
}
pub fn from_sync<F>(f: F) -> Self
where
F: Fn(ToolContext, Value) -> ToolOutput + Send + Sync + 'static,
{
Self {
inner: ToolExecuteInner::Sync(Arc::new(f)),
}
}
pub fn from_async<F, Fut>(f: F) -> Self
where
F: Fn(ToolContext, Value) -> Fut + Send + Sync + 'static,
Fut: Future<Output = ToolOutput> + Send + 'static,
{
Self {
inner: ToolExecuteInner::Async(Arc::new(move |context, input| {
Box::pin(f(context, input))
})),
}
}
}
impl Default for ToolExecute {
fn default() -> Self {
Self::from_sync(|_, _| Ok("".to_string()))
}
}
impl Serialize for ToolExecute {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str("ToolExecuteCall")
}
}
impl<'de> Deserialize<'de> for ToolExecute {
fn deserialize<D>(_: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(Self::default())
}
}
#[derive(Builder, Clone, Default)]
#[builder(pattern = "owned", setter(into), build_fn(error = "Error"))]
pub struct Tool {
pub name: String,
pub description: String,
pub input_schema: Schema,
pub execute: ToolExecute,
}
impl Debug for Tool {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Tool")
.field("name", &self.name)
.field("description", &self.description)
.finish()
}
}
impl Tool {
pub fn builder() -> ToolBuilder {
ToolBuilder::default()
}
}
#[derive(Debug, Clone, Default)]
pub struct ToolList {
pub tools: Arc<Mutex<Vec<Tool>>>,
}
impl ToolList {
pub fn new(tools: Vec<Tool>) -> Self {
Self {
tools: Arc::new(Mutex::new(tools)),
}
}
pub fn add_tool(&mut self, tool: Tool) {
self.tools
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.push(tool);
}
pub async fn execute(&self, context: ToolContext, tool_info: ToolCallInfo) -> Result<String> {
let tool = self
.tools
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.iter()
.find(|tool| tool.name == tool_info.tool.name)
.cloned();
match tool {
Some(tool) => tool.execute.call(context, tool_info.input).await,
None => Err(crate::error::Error::ToolCallError(
"Tool not found".to_string(),
)),
}
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct ToolDetails {
pub name: String,
pub id: String,
}
#[derive(Debug, Clone)]
pub struct ToolCallInfo {
pub call_id: Uuid,
pub tool: ToolDetails,
pub input: serde_json::Value,
pub extensions: Extensions,
}
impl Default for ToolCallInfo {
fn default() -> Self {
Self {
call_id: Uuid::new_v4(),
tool: ToolDetails::default(),
input: serde_json::Value::Null,
extensions: Extensions::default(),
}
}
}
impl PartialEq for ToolCallInfo {
fn eq(&self, other: &Self) -> bool {
self.tool == other.tool && self.input == other.input
}
}
impl ToolCallInfo {
pub fn new(name: impl Into<String>) -> Self {
Self {
tool: ToolDetails {
name: name.into(),
..Default::default()
},
extensions: Extensions::default(),
..Default::default()
}
}
pub fn name(&mut self, name: impl Into<String>) {
self.tool.name = name.into();
}
pub fn id(&mut self, id: impl Into<String>) {
self.tool.id = id.into();
}
pub fn call_id(&mut self, id: Uuid) {
self.call_id = id;
}
pub fn input(&mut self, inp: serde_json::Value) {
self.input = inp;
}
}
#[derive(Debug, Clone)]
pub struct ToolResultInfo {
pub call_id: Uuid,
pub tool: ToolDetails,
pub output: Result<serde_json::Value>,
}
impl Default for ToolResultInfo {
fn default() -> Self {
Self {
call_id: Uuid::new_v4(),
tool: ToolDetails::default(),
output: Ok(serde_json::Value::Null),
}
}
}
impl ToolResultInfo {
pub fn new(name: impl Into<String>) -> Self {
Self {
tool: ToolDetails {
name: name.into(),
..Default::default()
},
..Default::default()
}
}
pub fn name(&mut self, name: impl Into<String>) {
self.tool.name = name.into();
}
pub fn id(&mut self, id: impl Into<String>) {
self.tool.id = id.into();
}
pub fn call_id(&mut self, id: Uuid) {
self.call_id = id;
}
pub fn output(&mut self, inp: serde_json::Value) {
self.output = Ok(inp);
}
}
#[cfg(test)]
mod tests {
use super::{Tool, ToolCallInfo, ToolContext, ToolExecute, ToolList};
use crate::core::language_model::{
LanguageModelOptions, LanguageModelStream, LanguageModelStreamChunkType,
};
use futures::StreamExt;
use schemars::schema_for;
use serde::Serialize;
use serde_json::json;
#[derive(Serialize, schemars::JsonSchema)]
struct ToolInput {
value: String,
}
#[tokio::test]
async fn test_tool_list_executes_sync_and_async_tools() {
let sync_tool = Tool::builder()
.name("sync-tool")
.description("sync")
.input_schema(schema_for!(ToolInput))
.execute(ToolExecute::from_sync(|_ctx, input| {
Ok(format!("sync:{}", input["value"].as_str().unwrap()))
}))
.build()
.unwrap();
let async_tool = Tool::builder()
.name("async-tool")
.description("async")
.input_schema(schema_for!(ToolInput))
.execute(ToolExecute::from_async(|_ctx, input| async move {
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
Ok(format!("async:{}", input["value"].as_str().unwrap()))
}))
.build()
.unwrap();
let tools = ToolList::new(vec![sync_tool, async_tool]);
let mut sync_call = ToolCallInfo::new("sync-tool");
sync_call.input(json!({ "value": "a" }));
let mut async_call = ToolCallInfo::new("async-tool");
async_call.input(json!({ "value": "b" }));
let context = ToolContext::new(LanguageModelOptions::default());
assert_eq!(
tools.execute(context.clone(), sync_call).await.unwrap(),
"sync:a"
);
assert_eq!(tools.execute(context, async_call).await.unwrap(), "async:b");
}
#[tokio::test]
async fn test_tool_execute_with_context_exposes_options() {
let tool = Tool::builder()
.name("context-tool")
.description("context")
.input_schema(schema_for!(ToolInput))
.execute(ToolExecute::from_sync(|context, input| {
Ok(format!(
"{}:{}",
context.options().system.as_deref().unwrap_or_default(),
input["value"].as_str().unwrap()
))
}))
.build()
.unwrap();
let mut call = ToolCallInfo::new("context-tool");
call.input(json!({ "value": "payload" }));
let context = ToolContext::new(LanguageModelOptions {
system: Some("system prompt".to_string()),
..Default::default()
});
assert_eq!(
ToolList::new(vec![tool])
.execute(context, call)
.await
.unwrap(),
"system prompt:payload"
);
}
#[tokio::test]
async fn test_tool_execute_with_context_can_emit_stream_chunks() {
let tool = Tool::builder()
.name("stream-tool")
.description("stream")
.input_schema(schema_for!(ToolInput))
.execute(ToolExecute::from_async(|context, input| async move {
let _ = context.emit(LanguageModelStreamChunkType::TextDelta(format!(
"chunk:{}",
input["value"].as_str().unwrap()
)));
Ok("done".to_string())
}))
.build()
.unwrap();
let mut call = ToolCallInfo::new("stream-tool");
call.input(json!({ "value": "payload" }));
let (tx, mut stream) = LanguageModelStream::new();
let context = ToolContext::new(LanguageModelOptions::default()).with_stream_tx(tx);
assert_eq!(
ToolList::new(vec![tool])
.execute(context, call)
.await
.unwrap(),
"done"
);
match stream.next().await {
Some(LanguageModelStreamChunkType::TextDelta(text)) => {
assert_eq!(text, "chunk:payload")
}
other => panic!("expected tool-emitted text chunk, got {other:?}"),
}
}
}