gemini_adk_fluent_rs/compose/
tools.rs

1//! T — Tool composition.
2//!
3//! Compose tools in any order with `|`.
4
5use std::future::Future;
6use std::pin::Pin;
7use std::sync::Arc;
8
9use gemini_adk_rs::text::TextAgent;
10use gemini_adk_rs::tool::{SimpleTool, ToolFunction};
11use gemini_genai_rs::prelude::Tool;
12
13/// A tool composite — one or more tool entries.
14#[derive(Clone)]
15pub struct ToolComposite {
16    /// The tool entries in this composite.
17    pub entries: Vec<ToolCompositeEntry>,
18}
19
20/// An entry in a tool composite.
21#[derive(Clone)]
22pub enum ToolCompositeEntry {
23    /// A runtime tool function.
24    Function(Arc<dyn ToolFunction>),
25    /// A built-in Gemini tool declaration.
26    BuiltIn(Tool),
27    /// A text agent wrapped as a tool.
28    Agent {
29        /// Tool name exposed to the model.
30        name: String,
31        /// Tool description exposed to the model.
32        description: String,
33        /// The text agent to invoke.
34        agent: Arc<dyn TextAgent>,
35    },
36    /// An MCP (Model Context Protocol) toolset connection.
37    Mcp {
38        /// Connection params (e.g. URL or command string).
39        params: String,
40    },
41    /// A remote agent-to-agent tool.
42    A2a {
43        /// URL of the remote agent.
44        url: String,
45        /// Skill to invoke on the remote agent.
46        skill: String,
47    },
48    /// A mock tool that returns a fixed response (useful for testing).
49    Mock {
50        /// Tool name.
51        name: String,
52        /// Tool description.
53        description: String,
54        /// Fixed response to return.
55        response: serde_json::Value,
56    },
57    /// An OpenAPI spec-driven tool (placeholder/marker).
58    OpenApi {
59        /// Tool name.
60        name: String,
61        /// URL to the OpenAPI spec.
62        spec_url: String,
63    },
64    /// A BM25 search tool (placeholder/marker).
65    Search {
66        /// Tool name.
67        name: String,
68        /// Tool description.
69        description: String,
70    },
71    /// A schema-defined tool (placeholder/marker).
72    Schema {
73        /// Tool name.
74        name: String,
75        /// JSON Schema defining the tool's parameters.
76        schema: serde_json::Value,
77    },
78    /// A tool wrapped with a result transformer.
79    Transform {
80        /// The inner tool entry.
81        inner: Box<ToolCompositeEntry>,
82        /// Transformer function applied to the tool result.
83        transformer: Arc<
84            dyn Fn(serde_json::Value) -> Pin<Box<dyn Future<Output = serde_json::Value> + Send>>
85                + Send
86                + Sync,
87        >,
88    },
89}
90
91impl ToolComposite {
92    /// Create a composite containing a single runtime tool function.
93    pub fn from_function(f: Arc<dyn ToolFunction>) -> Self {
94        Self {
95            entries: vec![ToolCompositeEntry::Function(f)],
96        }
97    }
98
99    /// Create a composite containing a single built-in tool declaration.
100    pub fn from_built_in(tool: Tool) -> Self {
101        Self {
102            entries: vec![ToolCompositeEntry::BuiltIn(tool)],
103        }
104    }
105
106    /// Number of tool entries.
107    pub fn len(&self) -> usize {
108        self.entries.len()
109    }
110
111    /// Whether empty.
112    pub fn is_empty(&self) -> bool {
113        self.entries.is_empty()
114    }
115}
116
117/// Compose two tool composites with `|`.
118impl std::ops::BitOr for ToolComposite {
119    type Output = ToolComposite;
120
121    fn bitor(mut self, rhs: ToolComposite) -> Self::Output {
122        self.entries.extend(rhs.entries);
123        self
124    }
125}
126
127/// The `T` namespace — static factory methods for tool composition.
128pub struct T;
129
130impl T {
131    /// Register a function tool.
132    pub fn function(f: Arc<dyn ToolFunction>) -> ToolComposite {
133        ToolComposite::from_function(f)
134    }
135
136    /// Add Google Search built-in tool.
137    pub fn google_search() -> ToolComposite {
138        ToolComposite::from_built_in(Tool::google_search())
139    }
140
141    /// Add URL context built-in tool.
142    pub fn url_context() -> ToolComposite {
143        ToolComposite::from_built_in(Tool::url_context())
144    }
145
146    /// Add code execution built-in tool.
147    pub fn code_execution() -> ToolComposite {
148        ToolComposite::from_built_in(Tool::code_execution())
149    }
150
151    /// Create a simple tool from a name, description, and async closure.
152    pub fn simple<F, Fut>(
153        name: impl Into<String>,
154        description: impl Into<String>,
155        f: F,
156    ) -> ToolComposite
157    where
158        F: Fn(serde_json::Value) -> Fut + Send + Sync + 'static,
159        Fut: Future<Output = Result<serde_json::Value, gemini_adk_rs::ToolError>> + Send + 'static,
160    {
161        let tool = SimpleTool::new(name, description, None, f);
162        ToolComposite::from_function(Arc::new(tool))
163    }
164
165    /// Alias for [`simple`](Self::simple) — matches upstream Python `T.fn()`.
166    ///
167    /// Named `fn_tool` because `fn` is a reserved keyword in Rust.
168    pub fn fn_tool<F, Fut>(
169        name: impl Into<String>,
170        description: impl Into<String>,
171        f: F,
172    ) -> ToolComposite
173    where
174        F: Fn(serde_json::Value) -> Fut + Send + Sync + 'static,
175        Fut: Future<Output = Result<serde_json::Value, gemini_adk_rs::ToolError>> + Send + 'static,
176    {
177        Self::simple(name, description, f)
178    }
179
180    /// Wrap a tool with a confirmation requirement (marker for runtime).
181    pub fn confirm(tool: ToolComposite, _message: &str) -> ToolComposite {
182        // Confirmation enforcement happens at runtime. This is a declarative marker.
183        tool
184    }
185
186    /// Wrap a tool with a timeout (marker for runtime).
187    pub fn timeout(tool: ToolComposite, _duration: std::time::Duration) -> ToolComposite {
188        // Timeout enforcement happens at runtime.
189        tool
190    }
191
192    /// Wrap a tool with caching (marker for runtime).
193    pub fn cached(tool: ToolComposite) -> ToolComposite {
194        // Cache enforcement happens at runtime.
195        tool
196    }
197
198    /// Combine multiple tool functions into a single composite.
199    pub fn toolset(tools: Vec<Arc<dyn ToolFunction>>) -> ToolComposite {
200        ToolComposite {
201            entries: tools
202                .into_iter()
203                .map(ToolCompositeEntry::Function)
204                .collect(),
205        }
206    }
207
208    /// Wrap a [`TextAgent`] as a tool (shorthand for creating an agent tool entry).
209    ///
210    /// When invoked, the agent runs via `BaseLlm::generate()` and returns its
211    /// text output as the tool result. State is shared with the parent session.
212    pub fn agent(
213        name: impl Into<String>,
214        description: impl Into<String>,
215        agent: impl TextAgent + 'static,
216    ) -> ToolComposite {
217        ToolComposite {
218            entries: vec![ToolCompositeEntry::Agent {
219                name: name.into(),
220                description: description.into(),
221                agent: Arc::new(agent),
222            }],
223        }
224    }
225
226    /// Create an MCP (Model Context Protocol) toolset entry.
227    ///
228    /// `params` is the connection string (e.g. a URL or command) used to
229    /// establish the MCP session at runtime.
230    pub fn mcp(params: impl Into<String>) -> ToolComposite {
231        ToolComposite {
232            entries: vec![ToolCompositeEntry::Mcp {
233                params: params.into(),
234            }],
235        }
236    }
237
238    /// Create a remote agent-to-agent tool.
239    ///
240    /// Routes tool calls to a remote agent at `url`, invoking the given `skill`.
241    pub fn a2a(url: impl Into<String>, skill: impl Into<String>) -> ToolComposite {
242        ToolComposite {
243            entries: vec![ToolCompositeEntry::A2a {
244                url: url.into(),
245                skill: skill.into(),
246            }],
247        }
248    }
249
250    /// Create a mock tool that returns a fixed response.
251    ///
252    /// Useful for testing and prototyping without real tool implementations.
253    pub fn mock(
254        name: impl Into<String>,
255        description: impl Into<String>,
256        response: serde_json::Value,
257    ) -> ToolComposite {
258        ToolComposite {
259            entries: vec![ToolCompositeEntry::Mock {
260                name: name.into(),
261                description: description.into(),
262                response,
263            }],
264        }
265    }
266
267    /// Create an OpenAPI spec-driven tool (placeholder/marker).
268    ///
269    /// At runtime, the spec at `spec_url` is fetched and used to generate
270    /// tool declarations and HTTP call routing.
271    pub fn openapi(name: impl Into<String>, spec_url: impl Into<String>) -> ToolComposite {
272        ToolComposite {
273            entries: vec![ToolCompositeEntry::OpenApi {
274                name: name.into(),
275                spec_url: spec_url.into(),
276            }],
277        }
278    }
279
280    /// Create a BM25 search tool (placeholder/marker).
281    ///
282    /// Declares a search tool that performs BM25 retrieval at runtime.
283    pub fn search(name: impl Into<String>, description: impl Into<String>) -> ToolComposite {
284        ToolComposite {
285            entries: vec![ToolCompositeEntry::Search {
286                name: name.into(),
287                description: description.into(),
288            }],
289        }
290    }
291
292    /// Create a schema-defined tool (placeholder/marker).
293    ///
294    /// The tool's parameters are defined by the given JSON Schema value.
295    pub fn schema(name: impl Into<String>, schema: serde_json::Value) -> ToolComposite {
296        ToolComposite {
297            entries: vec![ToolCompositeEntry::Schema {
298                name: name.into(),
299                schema,
300            }],
301        }
302    }
303
304    /// Wrap each tool entry in a composite with a result transformer.
305    ///
306    /// The transformer function is applied to the tool's output value before
307    /// it is returned to the model.
308    pub fn transform<F, Fut>(tool: ToolComposite, f: F) -> ToolComposite
309    where
310        F: Fn(serde_json::Value) -> Fut + Send + Sync + 'static,
311        Fut: Future<Output = serde_json::Value> + Send + 'static,
312    {
313        let f: Arc<
314            dyn Fn(serde_json::Value) -> Pin<Box<dyn Future<Output = serde_json::Value> + Send>>
315                + Send
316                + Sync,
317        > = Arc::new(
318            move |v: serde_json::Value| -> Pin<Box<dyn Future<Output = serde_json::Value> + Send>> {
319                Box::pin(f(v))
320            },
321        );
322        ToolComposite {
323            entries: tool
324                .entries
325                .into_iter()
326                .map(|entry| ToolCompositeEntry::Transform {
327                    inner: Box::new(entry),
328                    transformer: Arc::clone(&f),
329                })
330                .collect(),
331        }
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338
339    #[test]
340    fn google_search_creates_composite() {
341        let t = T::google_search();
342        assert_eq!(t.len(), 1);
343    }
344
345    #[test]
346    fn url_context_creates_composite() {
347        let t = T::url_context();
348        assert_eq!(t.len(), 1);
349    }
350
351    #[test]
352    fn code_execution_creates_composite() {
353        let t = T::code_execution();
354        assert_eq!(t.len(), 1);
355    }
356
357    #[test]
358    fn compose_with_bitor() {
359        let t = T::google_search() | T::url_context() | T::code_execution();
360        assert_eq!(t.len(), 3);
361    }
362
363    #[test]
364    fn simple_creates_tool() {
365        let t = T::simple("greet", "Greets the user", |_args| async {
366            Ok(serde_json::json!({"message": "hello"}))
367        });
368        assert_eq!(t.len(), 1);
369        match &t.entries[0] {
370            ToolCompositeEntry::Function(f) => assert_eq!(f.name(), "greet"),
371            _ => panic!("expected Function entry"),
372        }
373    }
374
375    #[test]
376    fn toolset_combines_functions() {
377        let tool_a: Arc<dyn ToolFunction> =
378            Arc::new(SimpleTool::new("a", "tool a", None, |_| async {
379                Ok(serde_json::json!(null))
380            }));
381        let tool_b: Arc<dyn ToolFunction> =
382            Arc::new(SimpleTool::new("b", "tool b", None, |_| async {
383                Ok(serde_json::json!(null))
384            }));
385        let t = T::toolset(vec![tool_a, tool_b]);
386        assert_eq!(t.len(), 2);
387    }
388}