Skip to main content

adk_browser/tools/
evaluate.rs

1//! JavaScript evaluation tool.
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 executing JavaScript in the browser.
10pub struct EvaluateJsTool {
11    browser: Arc<BrowserSession>,
12}
13
14impl EvaluateJsTool {
15    /// Create a new JavaScript evaluation 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 EvaluateJsTool {
23    fn name(&self) -> &str {
24        "browser_evaluate_js"
25    }
26
27    fn description(&self) -> &str {
28        "Execute JavaScript code in the browser and return the result. Use for complex interactions or data extraction."
29    }
30
31    fn parameters_schema(&self) -> Option<Value> {
32        Some(json!({
33            "type": "object",
34            "properties": {
35                "script": {
36                    "type": "string",
37                    "description": "JavaScript code to execute. Use 'return' to get a value back."
38                },
39                "async": {
40                    "type": "boolean",
41                    "description": "Whether the script is async (uses a callback). Default: false"
42                }
43            },
44            "required": ["script"]
45        }))
46    }
47
48    fn response_schema(&self) -> Option<Value> {
49        Some(json!({
50            "type": "object",
51            "properties": {
52                "success": { "type": "boolean" },
53                "result": {}
54            }
55        }))
56    }
57
58    async fn execute(&self, _ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
59        let script = args
60            .get("script")
61            .and_then(|v| v.as_str())
62            .ok_or_else(|| adk_core::AdkError::Tool("Missing 'script' parameter".to_string()))?;
63
64        let is_async = args.get("async").and_then(|v| v.as_bool()).unwrap_or(false);
65
66        let result = if is_async {
67            self.browser.execute_async_script(script).await?
68        } else {
69            self.browser.execute_script(script).await?
70        };
71
72        Ok(json!({
73            "success": true,
74            "result": result
75        }))
76    }
77}
78
79/// Tool for scrolling the page.
80pub struct ScrollTool {
81    browser: Arc<BrowserSession>,
82}
83
84impl ScrollTool {
85    pub fn new(browser: Arc<BrowserSession>) -> Self {
86        Self { browser }
87    }
88}
89
90#[async_trait]
91impl Tool for ScrollTool {
92    fn name(&self) -> &str {
93        "browser_scroll"
94    }
95
96    fn description(&self) -> &str {
97        "Scroll the page in a direction or to a specific element."
98    }
99
100    fn parameters_schema(&self) -> Option<Value> {
101        Some(json!({
102            "type": "object",
103            "properties": {
104                "direction": {
105                    "type": "string",
106                    "enum": ["up", "down", "top", "bottom"],
107                    "description": "Direction to scroll"
108                },
109                "selector": {
110                    "type": "string",
111                    "description": "CSS selector of element to scroll into view"
112                },
113                "amount": {
114                    "type": "integer",
115                    "description": "Pixels to scroll (for up/down). Default: 500"
116                }
117            }
118        }))
119    }
120
121    async fn execute(&self, _ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
122        let direction = args.get("direction").and_then(|v| v.as_str());
123        let selector = args.get("selector").and_then(|v| v.as_str());
124        let amount = args.get("amount").and_then(|v| v.as_i64()).unwrap_or(500);
125
126        if let Some(sel) = selector {
127            // Scroll element into view
128            let escaped = crate::escape::escape_js_string(sel);
129            let script = format!(
130                "document.querySelector('{escaped}').scrollIntoView({{ behavior: 'smooth', block: 'center' }})"
131            );
132            self.browser.execute_script(&script).await?;
133
134            return Ok(json!({
135                "success": true,
136                "scrolled_to": sel
137            }));
138        }
139
140        if let Some(dir) = direction {
141            let script = match dir {
142                "up" => format!("window.scrollBy(0, -{amount})"),
143                "down" => format!("window.scrollBy(0, {amount})"),
144                "top" => "window.scrollTo(0, 0)".to_string(),
145                "bottom" => "window.scrollTo(0, document.body.scrollHeight)".to_string(),
146                _ => return Err(adk_core::AdkError::Tool(format!("Invalid direction: {dir}"))),
147            };
148
149            self.browser.execute_script(&script).await?;
150
151            return Ok(json!({
152                "success": true,
153                "scrolled": dir
154            }));
155        }
156
157        Err(adk_core::AdkError::Tool("Must specify either 'direction' or 'selector'".to_string()))
158    }
159}
160
161/// Tool for hovering over elements.
162pub struct HoverTool {
163    browser: Arc<BrowserSession>,
164}
165
166impl HoverTool {
167    pub fn new(browser: Arc<BrowserSession>) -> Self {
168        Self { browser }
169    }
170}
171
172#[async_trait]
173impl Tool for HoverTool {
174    fn name(&self) -> &str {
175        "browser_hover"
176    }
177
178    fn description(&self) -> &str {
179        "Hover over an element to trigger hover effects or tooltips."
180    }
181
182    fn parameters_schema(&self) -> Option<Value> {
183        Some(json!({
184            "type": "object",
185            "properties": {
186                "selector": {
187                    "type": "string",
188                    "description": "CSS selector for the element to hover over"
189                }
190            },
191            "required": ["selector"]
192        }))
193    }
194
195    async fn execute(&self, _ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
196        let selector = args
197            .get("selector")
198            .and_then(|v| v.as_str())
199            .ok_or_else(|| adk_core::AdkError::Tool("Missing 'selector' parameter".to_string()))?;
200
201        let escaped = crate::escape::escape_js_string(selector);
202
203        // Dispatch both mouseenter and mouseover for proper hover behavior
204        let script = format!(
205            r#"
206            var element = document.querySelector('{escaped}');
207            if (element) {{
208                element.dispatchEvent(new MouseEvent('mouseenter', {{
209                    'view': window, 'bubbles': true, 'cancelable': true
210                }}));
211                element.dispatchEvent(new MouseEvent('mouseover', {{
212                    'view': window, 'bubbles': true, 'cancelable': true
213                }}));
214                return true;
215            }}
216            return false;
217            "#,
218        );
219
220        let result = self.browser.execute_script(&script).await?;
221
222        if result.as_bool() == Some(true) {
223            Ok(json!({
224                "success": true,
225                "hovered": selector
226            }))
227        } else {
228            Err(adk_core::AdkError::Tool(format!("Element not found: {selector}")))
229        }
230    }
231}
232
233/// Tool for handling alerts/dialogs.
234pub struct AlertTool {
235    browser: Arc<BrowserSession>,
236}
237
238impl AlertTool {
239    pub fn new(browser: Arc<BrowserSession>) -> Self {
240        Self { browser }
241    }
242}
243
244#[async_trait]
245impl Tool for AlertTool {
246    fn name(&self) -> &str {
247        "browser_handle_alert"
248    }
249
250    fn description(&self) -> &str {
251        "Handle JavaScript alerts, confirms, and prompts. Accepts or dismisses the active dialog."
252    }
253
254    fn parameters_schema(&self) -> Option<Value> {
255        Some(json!({
256            "type": "object",
257            "properties": {
258                "action": {
259                    "type": "string",
260                    "enum": ["accept", "dismiss"],
261                    "description": "Action to take on the alert"
262                },
263                "text": {
264                    "type": "string",
265                    "description": "Text to enter for prompt dialogs"
266                }
267            },
268            "required": ["action"]
269        }))
270    }
271
272    async fn execute(&self, _ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
273        let action = args
274            .get("action")
275            .and_then(|v| v.as_str())
276            .ok_or_else(|| adk_core::AdkError::Tool("Missing 'action' parameter".to_string()))?;
277
278        let prompt_text = args.get("text").and_then(|v| v.as_str());
279
280        // Try the real WebDriver alert API first. If no alert is present,
281        // fall back to overriding window.alert/confirm/prompt for future dialogs.
282        let real_alert_result = self.browser.execute_script("return 'no_alert';").await;
283
284        // Attempt to interact with a real alert via JS bridge.
285        // thirtyfour's alert API: driver.switch_to().alert()
286        // We use execute_script to detect if an alert is blocking — if it fails
287        // with an "unexpected alert" error, we know there's a real alert.
288        let has_real_alert = real_alert_result.is_err();
289
290        if has_real_alert {
291            // There's a real alert blocking. Use JS to handle it on next attempt.
292            // The WebDriver will auto-dismiss on the next command depending on
293            // unhandledPromptBehavior capability. We override for explicit control.
294            let handle_script = match action {
295                "accept" => {
296                    if let Some(txt) = prompt_text {
297                        let escaped = crate::escape::escape_js_string(txt);
298                        format!(
299                            "window.__adk_prompt_response = '{escaped}'; \
300                             window.prompt = function() {{ return window.__adk_prompt_response; }}; \
301                             window.confirm = function() {{ return true; }}; \
302                             window.alert = function() {{}};"
303                        )
304                    } else {
305                        "window.confirm = function() { return true; }; \
306                         window.alert = function() {}; \
307                         window.prompt = function() { return ''; };"
308                            .to_string()
309                    }
310                }
311                "dismiss" => "window.confirm = function() { return false; }; \
312                     window.alert = function() {}; \
313                     window.prompt = function() { return null; };"
314                    .to_string(),
315                _ => return Err(adk_core::AdkError::Tool(format!("Invalid action: {action}"))),
316            };
317
318            // The override will take effect for future alerts
319            let _ = self.browser.execute_script(&handle_script).await;
320
321            Ok(json!({
322                "success": true,
323                "action": action,
324                "had_active_alert": true
325            }))
326        } else {
327            // No active alert — set up overrides for future alerts
328            let script = match action {
329                "accept" => {
330                    if let Some(txt) = prompt_text {
331                        let escaped = crate::escape::escape_js_string(txt);
332                        format!(
333                            "window.__adk_prompt_response = '{escaped}'; \
334                             window.prompt = function() {{ return window.__adk_prompt_response; }}; \
335                             window.confirm = function() {{ return true; }}; \
336                             window.alert = function() {{}}; \
337                             return 'ok';"
338                        )
339                    } else {
340                        "window.confirm = function() { return true; }; \
341                         window.alert = function() {}; \
342                         window.prompt = function() { return ''; }; \
343                         return 'ok';"
344                            .to_string()
345                    }
346                }
347                "dismiss" => "window.confirm = function() { return false; }; \
348                     window.alert = function() {}; \
349                     window.prompt = function() { return null; }; \
350                     return 'ok';"
351                    .to_string(),
352                _ => return Err(adk_core::AdkError::Tool(format!("Invalid action: {action}"))),
353            };
354
355            self.browser.execute_script(&script).await?;
356
357            Ok(json!({
358                "success": true,
359                "action": action,
360                "had_active_alert": false
361            }))
362        }
363    }
364}