use std::sync::Arc;
use serde_json::{json, Value};
use super::base::BaseTool;
use super::simple::SimpleTool;
use super::structured::StructuredTool;
use crate::runnables::base::Runnable;
pub fn convert_to_openai_tool(tool: &dyn BaseTool) -> Value {
let mut function = json!({
"name": tool.name(),
"description": tool.description()
});
if let Some(schema) = tool.args_schema() {
function["parameters"] = schema;
}
json!({
"type": "function",
"function": function
})
}
pub fn convert_to_openai_tools(tools: &[&dyn BaseTool]) -> Vec<Value> {
tools.iter().map(|t| convert_to_openai_tool(*t)).collect()
}
pub fn convert_runnable_to_tool(
runnable: Arc<dyn Runnable>,
name: Option<&str>,
description: Option<&str>,
schema: Option<Value>,
) -> Box<dyn BaseTool> {
let tool_name = name
.map(|n| n.to_string())
.unwrap_or_else(|| runnable.name().to_string());
let tool_description = description
.map(|d| d.to_string())
.unwrap_or_else(|| format!("Wrapper around {}", tool_name));
let is_string_schema = schema
.as_ref()
.and_then(|s| s.get("type"))
.and_then(|t| t.as_str())
.map(|t| t == "string")
.unwrap_or(false);
if is_string_schema || schema.is_none() {
let runnable_clone = Arc::clone(&runnable);
let simple = SimpleTool::new_async(tool_name, tool_description, move |input: String| {
let r = Arc::clone(&runnable_clone);
async move {
let result = r.invoke(Value::String(input), None).await?;
match result {
Value::String(s) => Ok(s),
other => Ok(other.to_string()),
}
}
});
Box::new(simple)
} else {
let tool_schema = schema.unwrap_or_else(|| {
json!({
"type": "object",
"properties": {}
})
});
let runnable_clone = Arc::clone(&runnable);
let structured =
StructuredTool::new(tool_name, tool_description, tool_schema, move |args| {
let r = Arc::clone(&runnable_clone);
async move {
let input =
Value::Object(args.into_iter().collect::<serde_json::Map<String, Value>>());
r.invoke(input, None).await
}
});
Box::new(structured)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Result;
use crate::runnables::config::RunnableConfig;
use async_trait::async_trait;
use serde_json::json;
struct EchoRunnable;
#[async_trait]
impl Runnable for EchoRunnable {
fn name(&self) -> &str {
"echo"
}
async fn invoke(&self, input: Value, _config: Option<&RunnableConfig>) -> Result<Value> {
Ok(input)
}
}
struct AddRunnable;
#[async_trait]
impl Runnable for AddRunnable {
fn name(&self) -> &str {
"add"
}
async fn invoke(&self, input: Value, _config: Option<&RunnableConfig>) -> Result<Value> {
let a = input.get("a").and_then(|v| v.as_f64()).unwrap_or(0.0);
let b = input.get("b").and_then(|v| v.as_f64()).unwrap_or(0.0);
Ok(json!(a + b))
}
}
#[test]
fn test_convert_to_openai_tool_format() {
let tool = SimpleTool::new("search", "Search the web", |_: &str| {
Ok("result".to_string())
});
let openai_tool = convert_to_openai_tool(&tool);
assert_eq!(openai_tool["type"], "function");
assert_eq!(openai_tool["function"]["name"], "search");
assert_eq!(openai_tool["function"]["description"], "Search the web");
assert!(openai_tool["function"]["parameters"].is_object());
}
#[test]
fn test_convert_to_openai_tools_multiple() {
let tool1 = SimpleTool::new("t1", "Tool 1", |_: &str| Ok("r1".to_string()));
let tool2 = SimpleTool::new("t2", "Tool 2", |_: &str| Ok("r2".to_string()));
let tools: Vec<&dyn BaseTool> = vec![&tool1, &tool2];
let result = convert_to_openai_tools(&tools);
assert_eq!(result.len(), 2);
assert_eq!(result[0]["function"]["name"], "t1");
assert_eq!(result[1]["function"]["name"], "t2");
}
#[tokio::test]
async fn test_convert_runnable_to_simple_tool() {
let runnable = Arc::new(EchoRunnable);
let tool =
convert_runnable_to_tool(runnable, Some("echo_tool"), Some("Echoes input"), None);
assert_eq!(tool.name(), "echo_tool");
assert_eq!(tool.description(), "Echoes input");
use crate::tools::types::ToolInput;
let result = tool
._run(ToolInput::Text("hello".to_string()))
.await
.unwrap();
match result {
crate::tools::types::ToolOutput::Content(v) => {
assert_eq!(v, Value::String("hello".to_string()));
}
_ => panic!("Expected Content output"),
}
}
#[tokio::test]
async fn test_convert_runnable_to_structured_tool() {
let runnable = Arc::new(AddRunnable);
let schema = json!({
"type": "object",
"properties": {
"a": { "type": "number" },
"b": { "type": "number" }
},
"required": ["a", "b"]
});
let tool = convert_runnable_to_tool(
runnable,
Some("add_tool"),
Some("Add numbers"),
Some(schema),
);
assert_eq!(tool.name(), "add_tool");
use crate::tools::types::ToolInput;
let mut args = std::collections::HashMap::new();
args.insert("a".to_string(), json!(3));
args.insert("b".to_string(), json!(7));
let result = tool._run(ToolInput::Structured(args)).await.unwrap();
match result {
crate::tools::types::ToolOutput::Content(v) => {
assert_eq!(v, json!(10.0));
}
_ => panic!("Expected Content output"),
}
}
#[tokio::test]
async fn test_convert_runnable_defaults_name_and_description() {
let runnable = Arc::new(EchoRunnable);
let tool = convert_runnable_to_tool(runnable, None, None, None);
assert_eq!(tool.name(), "echo");
assert_eq!(tool.description(), "Wrapper around echo");
}
}