agent_chain_core/tools/
convert.rs

1//! Convert functions and runnables to tools.
2//!
3//! This module provides utilities for converting functions and runnables
4//! into tools, mirroring `langchain_core.tools.convert`.
5
6use std::collections::HashMap;
7use std::future::Future;
8use std::sync::Arc;
9
10use serde_json::Value;
11
12use crate::error::{Error, Result};
13use crate::runnables::Runnable;
14
15use super::base::{ArgsSchema, ResponseFormat};
16use super::simple::Tool;
17use super::structured::{StructuredTool, create_args_schema};
18
19/// Configuration for creating a tool from a function.
20#[derive(Debug, Clone, Default)]
21pub struct ToolConfig {
22    /// Optional name for the tool. If not provided, uses the function name.
23    pub name: Option<String>,
24    /// Optional description for the tool.
25    pub description: Option<String>,
26    /// Whether to return the tool's output directly.
27    pub return_direct: bool,
28    /// Optional schema for the tool's input arguments.
29    pub args_schema: Option<ArgsSchema>,
30    /// Whether to infer the schema from the function signature.
31    pub infer_schema: bool,
32    /// The tool response format.
33    pub response_format: ResponseFormat,
34    /// Whether to parse the docstring for parameter descriptions.
35    pub parse_docstring: bool,
36    /// Whether to raise an error on invalid docstring.
37    pub error_on_invalid_docstring: bool,
38    /// Optional provider-specific extras.
39    pub extras: Option<HashMap<String, Value>>,
40}
41
42impl ToolConfig {
43    /// Create a new ToolConfig with defaults.
44    pub fn new() -> Self {
45        Self {
46            infer_schema: true,
47            ..Default::default()
48        }
49    }
50
51    /// Set the name.
52    pub fn with_name(mut self, name: impl Into<String>) -> Self {
53        self.name = Some(name.into());
54        self
55    }
56
57    /// Set the description.
58    pub fn with_description(mut self, description: impl Into<String>) -> Self {
59        self.description = Some(description.into());
60        self
61    }
62
63    /// Set return_direct.
64    pub fn with_return_direct(mut self, return_direct: bool) -> Self {
65        self.return_direct = return_direct;
66        self
67    }
68
69    /// Set the args schema.
70    pub fn with_args_schema(mut self, schema: ArgsSchema) -> Self {
71        self.args_schema = Some(schema);
72        self
73    }
74
75    /// Set infer_schema.
76    pub fn with_infer_schema(mut self, infer_schema: bool) -> Self {
77        self.infer_schema = infer_schema;
78        self
79    }
80
81    /// Set the response format.
82    pub fn with_response_format(mut self, format: ResponseFormat) -> Self {
83        self.response_format = format;
84        self
85    }
86
87    /// Set parse_docstring.
88    pub fn with_parse_docstring(mut self, parse: bool) -> Self {
89        self.parse_docstring = parse;
90        self
91    }
92
93    /// Set extras.
94    pub fn with_extras(mut self, extras: HashMap<String, Value>) -> Self {
95        self.extras = Some(extras);
96        self
97    }
98}
99
100/// Create a simple string-to-string tool from a function.
101///
102/// This is useful for tools that take a single string input and return a string.
103pub fn create_simple_tool<F>(
104    name: impl Into<String>,
105    description: impl Into<String>,
106    func: F,
107) -> Tool
108where
109    F: Fn(String) -> Result<String> + Send + Sync + 'static,
110{
111    Tool::from_function(func, name, description)
112}
113
114/// Create a simple tool with async support.
115pub fn create_simple_tool_async<F, AF, Fut>(
116    name: impl Into<String>,
117    description: impl Into<String>,
118    func: F,
119    coroutine: AF,
120) -> Tool
121where
122    F: Fn(String) -> Result<String> + Send + Sync + 'static,
123    AF: Fn(String) -> Fut + Send + Sync + 'static,
124    Fut: Future<Output = Result<String>> + Send + 'static,
125{
126    Tool::from_function_with_async(func, coroutine, name, description)
127}
128
129/// Create a structured tool from a function.
130///
131/// This is useful for tools that take multiple typed arguments.
132pub fn create_structured_tool<F>(
133    name: impl Into<String>,
134    description: impl Into<String>,
135    args_schema: ArgsSchema,
136    func: F,
137) -> StructuredTool
138where
139    F: Fn(HashMap<String, Value>) -> Result<Value> + Send + Sync + 'static,
140{
141    StructuredTool::from_function(func, name, description, args_schema)
142}
143
144/// Create a structured tool with async support.
145pub fn create_structured_tool_async<F, AF, Fut>(
146    name: impl Into<String>,
147    description: impl Into<String>,
148    args_schema: ArgsSchema,
149    func: F,
150    coroutine: AF,
151) -> StructuredTool
152where
153    F: Fn(HashMap<String, Value>) -> Result<Value> + Send + Sync + 'static,
154    AF: Fn(HashMap<String, Value>) -> Fut + Send + Sync + 'static,
155    Fut: Future<Output = Result<Value>> + Send + 'static,
156{
157    StructuredTool::from_function_with_async(func, coroutine, name, description, args_schema)
158}
159
160/// Create a tool using a configuration object.
161pub fn create_tool_with_config<F>(func: F, config: ToolConfig) -> Result<StructuredTool>
162where
163    F: Fn(HashMap<String, Value>) -> Result<Value> + Send + Sync + 'static,
164{
165    let name = config
166        .name
167        .ok_or_else(|| Error::InvalidConfig("Tool name is required".to_string()))?;
168    let description = config.description.unwrap_or_default();
169    let args_schema = config.args_schema.unwrap_or_default();
170
171    let mut tool = StructuredTool::from_function(func, name, description, args_schema);
172
173    if config.return_direct {
174        tool = tool.with_return_direct(true);
175    }
176
177    tool = tool.with_response_format(config.response_format);
178
179    if let Some(extras) = config.extras {
180        tool = tool.with_extras(extras);
181    }
182
183    Ok(tool)
184}
185
186/// Convert a runnable to a tool.
187///
188/// This function converts a Runnable into a BaseTool.
189pub fn convert_runnable_to_tool<R>(
190    runnable: Arc<R>,
191    name: impl Into<String>,
192    description: impl Into<String>,
193) -> StructuredTool
194where
195    R: Runnable<Input = HashMap<String, Value>, Output = Value> + Send + Sync + 'static,
196{
197    let name = name.into();
198    let description = description.into();
199
200    let runnable_clone = runnable.clone();
201    let func = move |args: HashMap<String, Value>| runnable_clone.invoke(args, None);
202
203    // Create a simple schema based on what we know
204    let schema = ArgsSchema::JsonSchema(serde_json::json!({
205        "type": "object",
206        "properties": {},
207        "additionalProperties": true
208    }));
209
210    StructuredTool::from_function(func, name, description, schema)
211}
212
213/// Type alias for the tool function used in tool_from_schema.
214pub type ToolFromSchemaFn = Box<dyn Fn(HashMap<String, Value>) -> Result<Value> + Send + Sync>;
215
216/// Helper macro-like function to define a tool with a schema.
217///
218/// In Rust, we can't use decorators like Python's @tool,
219/// but we can provide helper functions that make tool creation easier.
220pub fn tool_from_schema(
221    name: impl Into<String>,
222    description: impl Into<String>,
223    properties: Vec<(&str, &str, &str, bool)>, // (name, type, description, required)
224) -> impl FnOnce(ToolFromSchemaFn) -> StructuredTool {
225    let name = name.into();
226    let description = description.into();
227
228    let mut props = HashMap::new();
229    let mut required = Vec::new();
230
231    for (prop_name, prop_type, prop_desc, is_required) in properties {
232        props.insert(
233            prop_name.to_string(),
234            serde_json::json!({
235                "type": prop_type,
236                "description": prop_desc
237            }),
238        );
239        if is_required {
240            required.push(prop_name.to_string());
241        }
242    }
243
244    let schema = create_args_schema(&name, props, required, Some(&description));
245
246    move |func| StructuredTool::from_function(func, name, description, schema)
247}
248
249/// Generate a placeholder description for a runnable.
250pub fn get_description_from_runnable<R>(_runnable: &R) -> String
251where
252    R: Runnable,
253{
254    "Takes an input and produces an output.".to_string()
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260    use crate::tools::base::BaseTool;
261
262    #[test]
263    fn test_create_simple_tool() {
264        let tool = create_simple_tool("echo", "Echoes the input", |input| {
265            Ok(format!("Echo: {}", input))
266        });
267
268        assert_eq!(tool.name(), "echo");
269        assert_eq!(tool.description(), "Echoes the input");
270    }
271
272    #[test]
273    fn test_create_structured_tool() {
274        let schema = create_args_schema(
275            "add",
276            {
277                let mut props = HashMap::new();
278                props.insert("a".to_string(), serde_json::json!({"type": "number"}));
279                props.insert("b".to_string(), serde_json::json!({"type": "number"}));
280                props
281            },
282            vec!["a".to_string(), "b".to_string()],
283            None,
284        );
285
286        let tool = create_structured_tool("add", "Adds two numbers", schema, |args| {
287            let a = args.get("a").and_then(|v| v.as_f64()).unwrap_or(0.0);
288            let b = args.get("b").and_then(|v| v.as_f64()).unwrap_or(0.0);
289            Ok(Value::from(a + b))
290        });
291
292        assert_eq!(tool.name(), "add");
293    }
294
295    #[test]
296    fn test_tool_config() {
297        let config = ToolConfig::new()
298            .with_name("test")
299            .with_description("A test tool")
300            .with_return_direct(true)
301            .with_response_format(ResponseFormat::ContentAndArtifact);
302
303        assert_eq!(config.name, Some("test".to_string()));
304        assert!(config.return_direct);
305        assert_eq!(config.response_format, ResponseFormat::ContentAndArtifact);
306    }
307
308    #[test]
309    fn test_create_tool_with_config() {
310        let config = ToolConfig::new()
311            .with_name("configured_tool")
312            .with_description("A configured tool")
313            .with_args_schema(ArgsSchema::JsonSchema(serde_json::json!({
314                "type": "object",
315                "properties": {
316                    "input": {"type": "string"}
317                }
318            })));
319
320        let tool = create_tool_with_config(
321            |args| Ok(args.get("input").cloned().unwrap_or(Value::Null)),
322            config,
323        )
324        .unwrap();
325
326        assert_eq!(tool.name(), "configured_tool");
327    }
328
329    #[test]
330    fn test_tool_from_schema() {
331        let create_tool = tool_from_schema(
332            "greet",
333            "Greets a person",
334            vec![("name", "string", "The person's name", true)],
335        );
336
337        let tool = create_tool(Box::new(|args| {
338            let name = args
339                .get("name")
340                .and_then(|v| v.as_str())
341                .unwrap_or("stranger");
342            Ok(Value::String(format!("Hello, {}!", name)))
343        }));
344
345        assert_eq!(tool.name(), "greet");
346    }
347}