Skip to main content

adk_browser/tools/
type_text.rs

1//! Type tool for entering text into form fields.
2
3use crate::session::BrowserSession;
4use adk_core::{Result, Tool, ToolContext};
5use async_trait::async_trait;
6use serde_json::{Value, json};
7use std::sync::Arc;
8
9/// Tool for typing text into input fields.
10pub struct TypeTool {
11    browser: Arc<BrowserSession>,
12}
13
14impl TypeTool {
15    /// Create a new type tool with a shared browser session.
16    pub fn new(browser: Arc<BrowserSession>) -> Self {
17        Self { browser }
18    }
19}
20
21#[async_trait]
22impl Tool for TypeTool {
23    fn name(&self) -> &str {
24        "browser_type"
25    }
26
27    fn description(&self) -> &str {
28        "Type text into an input field or text area. Can optionally clear the field first."
29    }
30
31    fn parameters_schema(&self) -> Option<Value> {
32        Some(json!({
33            "type": "object",
34            "properties": {
35                "selector": {
36                    "type": "string",
37                    "description": "CSS selector for the input element (e.g., '#username', 'input[name=email]')"
38                },
39                "text": {
40                    "type": "string",
41                    "description": "The text to type into the field"
42                },
43                "clear_first": {
44                    "type": "boolean",
45                    "description": "Whether to clear the field before typing (default: true)"
46                },
47                "press_enter": {
48                    "type": "boolean",
49                    "description": "Whether to press Enter after typing (default: false)"
50                }
51            },
52            "required": ["selector", "text"]
53        }))
54    }
55
56    fn response_schema(&self) -> Option<Value> {
57        Some(json!({
58            "type": "object",
59            "properties": {
60                "success": { "type": "boolean" },
61                "typed_text": { "type": "string" },
62                "field_value": { "type": "string" }
63            }
64        }))
65    }
66
67    async fn execute(&self, _ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
68        let selector = args
69            .get("selector")
70            .and_then(|v| v.as_str())
71            .ok_or_else(|| adk_core::AdkError::Tool("Missing 'selector' parameter".to_string()))?;
72
73        let text = args
74            .get("text")
75            .and_then(|v| v.as_str())
76            .ok_or_else(|| adk_core::AdkError::Tool("Missing 'text' parameter".to_string()))?;
77
78        let clear_first = args.get("clear_first").and_then(|v| v.as_bool()).unwrap_or(true);
79
80        let press_enter = args.get("press_enter").and_then(|v| v.as_bool()).unwrap_or(false);
81
82        // Wait for element
83        let element = self.browser.wait_for_element(selector, 10).await?;
84
85        // Clear if requested
86        if clear_first {
87            element
88                .clear()
89                .await
90                .map_err(|e| adk_core::AdkError::Tool(format!("Clear failed: {}", e)))?;
91        }
92
93        // Type the text
94        element
95            .send_keys(text)
96            .await
97            .map_err(|e| adk_core::AdkError::Tool(format!("Type failed: {}", e)))?;
98
99        // Press Enter if requested
100        if press_enter {
101            element
102                .send_keys("\n")
103                .await
104                .map_err(|e| adk_core::AdkError::Tool(format!("Enter key failed: {}", e)))?;
105        }
106
107        // Get the current value
108        let field_value =
109            element.attr("value").await.ok().flatten().unwrap_or_else(|| text.to_string());
110
111        // Include page context so the agent knows the current state
112        let context = self.browser.page_context().await.unwrap_or_default();
113
114        Ok(json!({
115            "success": true,
116            "typed_text": text,
117            "field_value": field_value,
118            "page": context
119        }))
120    }
121}
122
123/// Tool for clearing input fields.
124pub struct ClearTool {
125    browser: Arc<BrowserSession>,
126}
127
128impl ClearTool {
129    pub fn new(browser: Arc<BrowserSession>) -> Self {
130        Self { browser }
131    }
132}
133
134#[async_trait]
135impl Tool for ClearTool {
136    fn name(&self) -> &str {
137        "browser_clear"
138    }
139
140    fn description(&self) -> &str {
141        "Clear the contents of an input field or text area."
142    }
143
144    fn parameters_schema(&self) -> Option<Value> {
145        Some(json!({
146            "type": "object",
147            "properties": {
148                "selector": {
149                    "type": "string",
150                    "description": "CSS selector for the input element to clear"
151                }
152            },
153            "required": ["selector"]
154        }))
155    }
156
157    async fn execute(&self, _ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
158        let selector = args
159            .get("selector")
160            .and_then(|v| v.as_str())
161            .ok_or_else(|| adk_core::AdkError::Tool("Missing 'selector' parameter".to_string()))?;
162
163        self.browser.clear(selector).await?;
164
165        let context = self.browser.page_context().await.unwrap_or_default();
166
167        Ok(json!({
168            "success": true,
169            "cleared": selector,
170            "page": context
171        }))
172    }
173}
174
175/// Tool for selecting options from dropdown menus.
176pub struct SelectTool {
177    browser: Arc<BrowserSession>,
178}
179
180impl SelectTool {
181    pub fn new(browser: Arc<BrowserSession>) -> Self {
182        Self { browser }
183    }
184}
185
186#[async_trait]
187impl Tool for SelectTool {
188    fn name(&self) -> &str {
189        "browser_select"
190    }
191
192    fn description(&self) -> &str {
193        "Select an option from a dropdown/select element by value, text, or index."
194    }
195
196    fn parameters_schema(&self) -> Option<Value> {
197        Some(json!({
198            "type": "object",
199            "properties": {
200                "selector": {
201                    "type": "string",
202                    "description": "CSS selector for the select element"
203                },
204                "value": {
205                    "type": "string",
206                    "description": "The value attribute of the option to select"
207                },
208                "text": {
209                    "type": "string",
210                    "description": "The visible text of the option to select"
211                },
212                "index": {
213                    "type": "integer",
214                    "description": "The index of the option to select (0-based)"
215                }
216            },
217            "required": ["selector"]
218        }))
219    }
220
221    async fn execute(&self, _ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
222        let selector = args
223            .get("selector")
224            .and_then(|v| v.as_str())
225            .ok_or_else(|| adk_core::AdkError::Tool("Missing 'selector' parameter".to_string()))?;
226
227        let value = args.get("value").and_then(|v| v.as_str());
228        let text = args.get("text").and_then(|v| v.as_str());
229        let index = args.get("index").and_then(|v| v.as_u64());
230
231        let escaped_selector = crate::escape::escape_js_string(selector);
232
233        // Build the appropriate selector for the option
234        let option_selector = if let Some(val) = value {
235            let escaped_val = crate::escape::escape_js_string(val);
236            format!("{selector} option[value='{escaped_val}']")
237        } else if let Some(txt) = text {
238            let escaped_txt = crate::escape::escape_js_string(txt);
239            // Use XPath for text matching isn't available, use JS instead
240            let script = format!(
241                r#"
242                var select = document.querySelector('{escaped_selector}');
243                for (var i = 0; i < select.options.length; i++) {{
244                    if (select.options[i].text === '{escaped_txt}') {{
245                        select.selectedIndex = i;
246                        select.dispatchEvent(new Event('change', {{ bubbles: true }}));
247                        return true;
248                    }}
249                }}
250                return false;
251                "#,
252            );
253
254            let result = self.browser.execute_script(&script).await?;
255            if result.as_bool() == Some(true) {
256                let context = self.browser.page_context().await.unwrap_or_default();
257                return Ok(json!({
258                    "success": true,
259                    "selected_text": txt,
260                    "page": context
261                }));
262            } else {
263                return Err(adk_core::AdkError::Tool(format!(
264                    "Option with text '{}' not found",
265                    txt
266                )));
267            }
268        } else if let Some(idx) = index {
269            let script = format!(
270                r#"
271                var select = document.querySelector('{escaped_selector}');
272                if (select && select.options.length > {idx}) {{
273                    select.selectedIndex = {idx};
274                    select.dispatchEvent(new Event('change', {{ bubbles: true }}));
275                    return select.options[{idx}].text;
276                }}
277                return null;
278                "#,
279            );
280
281            let result = self.browser.execute_script(&script).await?;
282            if let Some(selected_text) = result.as_str() {
283                let context = self.browser.page_context().await.unwrap_or_default();
284                return Ok(json!({
285                    "success": true,
286                    "selected_text": selected_text,
287                    "selected_index": idx,
288                    "page": context
289                }));
290            } else {
291                return Err(adk_core::AdkError::Tool(format!("Option at index {} not found", idx)));
292            }
293        } else {
294            return Err(adk_core::AdkError::Tool(
295                "Must specify 'value', 'text', or 'index'".to_string(),
296            ));
297        };
298
299        // Click the option
300        self.browser.click(&option_selector).await?;
301
302        let context = self.browser.page_context().await.unwrap_or_default();
303
304        Ok(json!({
305            "success": true,
306            "selected_value": value,
307            "page": context
308        }))
309    }
310}