Skip to main content

jules_core/tool/
mod.rs

1//! Tool calling support and abstractions.
2
3use crate::errors::ToolError;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::future::Future;
7use std::pin::Pin;
8
9/// A tool parameter schema.
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub struct ToolParameter {
12    /// The parameter type (e.g., "string", "number", "boolean", "object").
13    #[serde(rename = "type")]
14    pub param_type: String,
15    /// An optional description of the parameter.
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub description: Option<String>,
18}
19
20/// The parameters expected by a tool.
21#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
22pub struct ToolParameters {
23    /// The map of parameter names to their schemas.
24    pub properties: HashMap<String, ToolParameter>,
25    /// The list of required parameters.
26    pub required: Vec<String>,
27}
28
29/// A generic trait for a tool that can be called by an LLM model.
30pub trait Tool: Send + Sync {
31    /// Returns the name of the tool.
32    fn name(&self) -> &str;
33
34    /// Returns a description of what the tool does.
35    fn description(&self) -> &str;
36
37    /// Returns the parameters expected by this tool.
38    fn parameters(&self) -> ToolParameters;
39
40    /// Calls the tool with the given arguments.
41    /// The arguments are typically a JSON string generated by the LLM.
42    fn call(&self, args: &str) -> impl Future<Output = Result<String, ToolError>> + Send;
43}
44
45/// A wrapper trait for tools that is object-safe (`dyn Tool`).
46pub trait DynTool: Send + Sync {
47    /// Returns the name of the tool.
48    fn name(&self) -> &str;
49
50    /// Returns a description of what the tool does.
51    fn description(&self) -> &str;
52
53    /// Returns the parameters expected by this tool.
54    fn parameters(&self) -> ToolParameters;
55
56    /// Calls the tool with the given arguments.
57    fn call_dyn<'a>(
58        &'a self,
59        args: &'a str,
60    ) -> Pin<Box<dyn Future<Output = Result<String, ToolError>> + Send + 'a>>;
61}
62
63impl<T: Tool> DynTool for T {
64    fn name(&self) -> &str {
65        Tool::name(self)
66    }
67
68    fn description(&self) -> &str {
69        Tool::description(self)
70    }
71
72    fn parameters(&self) -> ToolParameters {
73        Tool::parameters(self)
74    }
75
76    fn call_dyn<'a>(
77        &'a self,
78        args: &'a str,
79    ) -> Pin<Box<dyn Future<Output = Result<String, ToolError>> + Send + 'a>> {
80        Box::pin(Tool::call(self, args))
81    }
82}
83
84/// A registry that stores and manages tools.
85pub struct ToolRegistry {
86    tools: HashMap<String, Box<dyn DynTool>>,
87}
88
89impl Default for ToolRegistry {
90    fn default() -> Self {
91        Self::new()
92    }
93}
94
95impl ToolRegistry {
96    /// Creates a new, empty `ToolRegistry`.
97    #[must_use]
98    pub fn new() -> Self {
99        Self {
100            tools: HashMap::new(),
101        }
102    }
103
104    /// Registers a new tool in the registry.
105    pub fn register<T: Tool + 'static>(&mut self, tool: T) {
106        self.tools.insert(tool.name().to_string(), Box::new(tool));
107    }
108
109    /// Retrieves a tool by name from the registry, if it exists.
110    #[must_use]
111    pub fn get(&self, name: &str) -> Option<&dyn DynTool> {
112        self.tools.get(name).map(std::convert::AsRef::as_ref)
113    }
114
115    /// Returns a list of all tools in the registry.
116    #[must_use]
117    pub fn list(&self) -> Vec<&dyn DynTool> {
118        self.tools
119            .values()
120            .map(std::convert::AsRef::as_ref)
121            .collect()
122    }
123}
124
125/// Information about a requested tool call.
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127pub struct ToolCallInfo {
128    /// The ID of the tool call.
129    pub id: String,
130    /// The name of the tool to call.
131    pub name: String,
132    /// The arguments to pass to the tool, usually as a JSON string.
133    pub arguments: String,
134}
135
136/// A parsed tool call requested by the model.
137#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
138pub struct ToolCall {
139    /// Information about the tool call.
140    pub function: ToolCallInfo,
141    /// The type of tool call (e.g., "function").
142    #[serde(rename = "type")]
143    pub tool_type: String,
144}
145
146/// The result of a tool call to be returned to the conversation.
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
148pub struct ToolResult {
149    /// The ID of the original tool call.
150    pub tool_call_id: String,
151    /// The result of the tool call, usually as a string.
152    pub content: String,
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use crate::errors::ToolError;
159
160    struct EchoTool;
161
162    impl Tool for EchoTool {
163        fn name(&self) -> &'static str {
164            "echo"
165        }
166
167        fn description(&self) -> &'static str {
168            "Echoes the input."
169        }
170
171        fn parameters(&self) -> ToolParameters {
172            let mut properties = HashMap::new();
173            properties.insert(
174                "input".to_string(),
175                ToolParameter {
176                    param_type: "string".to_string(),
177                    description: Some("The text to echo".to_string()),
178                },
179            );
180            ToolParameters {
181                properties,
182                required: vec!["input".to_string()],
183            }
184        }
185
186        fn call(&self, args: &str) -> impl Future<Output = Result<String, ToolError>> + Send {
187            let args_owned = args.to_string();
188            async move { Ok(args_owned) }
189        }
190    }
191
192    #[test]
193    fn test_tool_registry() {
194        let mut registry = ToolRegistry::new();
195        registry.register(EchoTool);
196
197        assert!(registry.get("echo").is_some());
198        assert!(registry.get("missing").is_none());
199        assert_eq!(registry.list().len(), 1);
200    }
201
202    #[tokio::test]
203    async fn test_tool_execution() {
204        let tool = EchoTool;
205        let dyn_tool: &dyn DynTool = &tool;
206
207        let result = dyn_tool.call_dyn("hello world").await.unwrap();
208        assert_eq!(result, "hello world");
209    }
210}