Skip to main content

adk_browser/
toolset.rs

1//! Browser toolset that provides all browser tools as a collection.
2
3use crate::pool::BrowserSessionPool;
4use crate::session::BrowserSession;
5use crate::tools::*;
6use adk_core::{ReadonlyContext, Result, Tool, Toolset};
7use async_trait::async_trait;
8use std::sync::Arc;
9
10/// Internal abstraction for session acquisition.
11/// Not exposed in the public API.
12enum SessionResolver {
13    /// Fixed session — always returns the same session.
14    Fixed(Arc<BrowserSession>),
15    /// Pool-backed — resolves session from pool using user_id from context.
16    Pool(Arc<BrowserSessionPool>),
17}
18
19impl SessionResolver {
20    async fn resolve(&self, ctx: &Arc<dyn ReadonlyContext>) -> Result<Arc<BrowserSession>> {
21        match self {
22            SessionResolver::Fixed(session) => Ok(session.clone()),
23            SessionResolver::Pool(pool) => pool.get_or_create(ctx.user_id()).await,
24        }
25    }
26}
27
28/// Pre-configured tool profiles for common use cases.
29///
30/// Instead of using all 46 tools (which overwhelms LLM context windows),
31/// select a profile that matches your agent's task.
32///
33/// # Example
34///
35/// ```rust,ignore
36/// use adk_browser::{BrowserToolset, BrowserProfile, BrowserSession, BrowserConfig};
37/// use std::sync::Arc;
38///
39/// let browser = Arc::new(BrowserSession::new(BrowserConfig::default()));
40/// let toolset = BrowserToolset::with_profile(browser, BrowserProfile::FormFilling);
41/// let tools = toolset.all_tools(); // 8 tools instead of 46
42/// ```
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum BrowserProfile {
45    /// 6 tools: navigate, click, type, extract_text, wait_for_element, screenshot.
46    /// Best for simple browsing tasks.
47    Minimal,
48    /// 8 tools: Minimal + select, clear.
49    /// Best for form-filling agents.
50    FormFilling,
51    /// 7 tools: navigate, extract_text, extract_attribute, extract_links, page_info, screenshot, scroll.
52    /// Best for data extraction / scraping agents.
53    Scraping,
54    /// All 46 tools. Use only when the agent needs full browser control.
55    Full,
56}
57
58/// A toolset that provides all browser automation tools.
59///
60/// Use this to add all browser tools to an agent at once, or use
61/// individual tools for more control.
62pub struct BrowserToolset {
63    resolver: SessionResolver,
64    /// Include navigation tools (navigate, back, forward, refresh)
65    include_navigation: bool,
66    /// Include interaction tools (click, type, select)
67    include_interaction: bool,
68    /// Include extraction tools (extract text, attributes, links, page info)
69    include_extraction: bool,
70    /// Include wait tools
71    include_wait: bool,
72    /// Include screenshot tool
73    include_screenshot: bool,
74    /// Include JavaScript evaluation tools
75    include_js: bool,
76    /// Include cookie management tools
77    include_cookies: bool,
78    /// Include window/tab management tools
79    include_windows: bool,
80    /// Include frame/iframe management tools
81    include_frames: bool,
82    /// Include advanced action tools (drag-drop, focus, file upload, etc.)
83    include_actions: bool,
84}
85
86impl BrowserToolset {
87    /// Create a new toolset with all tools enabled.
88    pub fn new(browser: Arc<BrowserSession>) -> Self {
89        Self {
90            resolver: SessionResolver::Fixed(browser),
91            include_navigation: true,
92            include_interaction: true,
93            include_extraction: true,
94            include_wait: true,
95            include_screenshot: true,
96            include_js: true,
97            include_cookies: true,
98            include_windows: true,
99            include_frames: true,
100            include_actions: true,
101        }
102    }
103
104    /// Create a pool-backed toolset with all tools enabled.
105    ///
106    /// Sessions are resolved per-user at runtime via `Toolset::tools(ctx)`.
107    /// The pool calls `get_or_create(ctx.user_id())` to obtain an isolated
108    /// browser session for each user.
109    ///
110    /// # Example
111    ///
112    /// ```rust,ignore
113    /// use adk_browser::{BrowserToolset, BrowserSessionPool, BrowserConfig};
114    /// use std::sync::Arc;
115    ///
116    /// let pool = Arc::new(BrowserSessionPool::new(BrowserConfig::default()));
117    /// let toolset = BrowserToolset::with_pool(pool);
118    /// ```
119    pub fn with_pool(pool: Arc<BrowserSessionPool>) -> Self {
120        Self {
121            resolver: SessionResolver::Pool(pool),
122            include_navigation: true,
123            include_interaction: true,
124            include_extraction: true,
125            include_wait: true,
126            include_screenshot: true,
127            include_js: true,
128            include_cookies: true,
129            include_windows: true,
130            include_frames: true,
131            include_actions: true,
132        }
133    }
134
135    /// Create a pool-backed toolset with a pre-configured profile.
136    ///
137    /// Combines pool-backed session resolution with profile-based tool
138    /// category selection.
139    ///
140    /// # Example
141    ///
142    /// ```rust,ignore
143    /// use adk_browser::{BrowserToolset, BrowserProfile, BrowserSessionPool, BrowserConfig};
144    /// use std::sync::Arc;
145    ///
146    /// let pool = Arc::new(BrowserSessionPool::new(BrowserConfig::default()));
147    /// let toolset = BrowserToolset::with_pool_and_profile(pool, BrowserProfile::Minimal);
148    /// ```
149    pub fn with_pool_and_profile(pool: Arc<BrowserSessionPool>, profile: BrowserProfile) -> Self {
150        match profile {
151            BrowserProfile::Minimal => Self {
152                resolver: SessionResolver::Pool(pool),
153                include_navigation: true,
154                include_interaction: true,
155                include_extraction: true,
156                include_wait: true,
157                include_screenshot: true,
158                include_js: false,
159                include_cookies: false,
160                include_windows: false,
161                include_frames: false,
162                include_actions: false,
163            },
164            BrowserProfile::FormFilling => Self {
165                resolver: SessionResolver::Pool(pool),
166                include_navigation: true,
167                include_interaction: true,
168                include_extraction: true,
169                include_wait: true,
170                include_screenshot: true,
171                include_js: false,
172                include_cookies: false,
173                include_windows: false,
174                include_frames: false,
175                include_actions: false,
176            },
177            BrowserProfile::Scraping => Self {
178                resolver: SessionResolver::Pool(pool),
179                include_navigation: true,
180                include_interaction: false,
181                include_extraction: true,
182                include_wait: false,
183                include_screenshot: true,
184                include_js: true,
185                include_cookies: false,
186                include_windows: false,
187                include_frames: false,
188                include_actions: false,
189            },
190            BrowserProfile::Full => Self::with_pool(pool),
191        }
192    }
193
194    /// Create a toolset with a pre-configured profile.
195    ///
196    /// This is the recommended way to create a toolset for most agents.
197    /// Using `BrowserProfile::Full` is equivalent to `BrowserToolset::new()`.
198    pub fn with_profile(browser: Arc<BrowserSession>, profile: BrowserProfile) -> Self {
199        match profile {
200            BrowserProfile::Minimal => Self {
201                resolver: SessionResolver::Fixed(browser),
202                include_navigation: true,
203                include_interaction: true,
204                include_extraction: true,
205                include_wait: true,
206                include_screenshot: true,
207                include_js: false,
208                include_cookies: false,
209                include_windows: false,
210                include_frames: false,
211                include_actions: false,
212            },
213            BrowserProfile::FormFilling => Self {
214                resolver: SessionResolver::Fixed(browser),
215                include_navigation: true,
216                include_interaction: true,
217                include_extraction: true,
218                include_wait: true,
219                include_screenshot: true,
220                include_js: false,
221                include_cookies: false,
222                include_windows: false,
223                include_frames: false,
224                include_actions: false,
225            },
226            BrowserProfile::Scraping => Self {
227                resolver: SessionResolver::Fixed(browser),
228                include_navigation: true,
229                include_interaction: false,
230                include_extraction: true,
231                include_wait: false,
232                include_screenshot: true,
233                include_js: true, // scroll only
234                include_cookies: false,
235                include_windows: false,
236                include_frames: false,
237                include_actions: false,
238            },
239            BrowserProfile::Full => Self::new(browser),
240        }
241    }
242
243    /// Enable or disable navigation tools.
244    pub fn with_navigation(mut self, enabled: bool) -> Self {
245        self.include_navigation = enabled;
246        self
247    }
248
249    /// Enable or disable interaction tools.
250    pub fn with_interaction(mut self, enabled: bool) -> Self {
251        self.include_interaction = enabled;
252        self
253    }
254
255    /// Enable or disable extraction tools.
256    pub fn with_extraction(mut self, enabled: bool) -> Self {
257        self.include_extraction = enabled;
258        self
259    }
260
261    /// Enable or disable wait tools.
262    pub fn with_wait(mut self, enabled: bool) -> Self {
263        self.include_wait = enabled;
264        self
265    }
266
267    /// Enable or disable screenshot tool.
268    pub fn with_screenshot(mut self, enabled: bool) -> Self {
269        self.include_screenshot = enabled;
270        self
271    }
272
273    /// Enable or disable JavaScript tools.
274    pub fn with_js(mut self, enabled: bool) -> Self {
275        self.include_js = enabled;
276        self
277    }
278
279    /// Enable or disable cookie management tools.
280    pub fn with_cookies(mut self, enabled: bool) -> Self {
281        self.include_cookies = enabled;
282        self
283    }
284
285    /// Enable or disable window/tab management tools.
286    pub fn with_windows(mut self, enabled: bool) -> Self {
287        self.include_windows = enabled;
288        self
289    }
290
291    /// Enable or disable frame/iframe management tools.
292    pub fn with_frames(mut self, enabled: bool) -> Self {
293        self.include_frames = enabled;
294        self
295    }
296
297    /// Enable or disable advanced action tools.
298    pub fn with_actions(mut self, enabled: bool) -> Self {
299        self.include_actions = enabled;
300        self
301    }
302
303    /// Get all tools as a vector (synchronous version).
304    ///
305    /// Works for fixed-session toolsets. For pool-backed toolsets,
306    /// returns an empty vec with a warning — use `Toolset::tools(ctx)` or
307    /// `try_all_tools()` instead.
308    pub fn all_tools(&self) -> Vec<Arc<dyn Tool>> {
309        match &self.resolver {
310            SessionResolver::Fixed(session) => self.build_tools(session.clone()),
311            SessionResolver::Pool(_) => {
312                tracing::warn!(
313                    "BrowserToolset::all_tools() called on a pool-backed toolset. \
314                     Returns empty vec. Use Toolset::tools(ctx) instead."
315                );
316                Vec::new()
317            }
318        }
319    }
320
321    /// Try to get all tools synchronously. Returns an error for pool-backed toolsets.
322    ///
323    /// Prefer `Toolset::tools(ctx)` for pool-backed toolsets.
324    pub fn try_all_tools(&self) -> Result<Vec<Arc<dyn Tool>>> {
325        match &self.resolver {
326            SessionResolver::Fixed(session) => Ok(self.build_tools(session.clone())),
327            SessionResolver::Pool(_) => Err(adk_core::AdkError::Tool(
328                "Cannot resolve tools synchronously for a pool-backed BrowserToolset. \
329                 Use Toolset::tools(ctx) instead."
330                    .into(),
331            )),
332        }
333    }
334
335    /// Internal: build tool instances from a resolved session.
336    fn build_tools(&self, browser: Arc<BrowserSession>) -> Vec<Arc<dyn Tool>> {
337        let mut tools: Vec<Arc<dyn Tool>> = Vec::new();
338
339        if self.include_navigation {
340            tools.push(Arc::new(NavigateTool::new(browser.clone())));
341            tools.push(Arc::new(BackTool::new(browser.clone())));
342            tools.push(Arc::new(ForwardTool::new(browser.clone())));
343            tools.push(Arc::new(RefreshTool::new(browser.clone())));
344        }
345
346        if self.include_interaction {
347            tools.push(Arc::new(ClickTool::new(browser.clone())));
348            tools.push(Arc::new(DoubleClickTool::new(browser.clone())));
349            tools.push(Arc::new(TypeTool::new(browser.clone())));
350            tools.push(Arc::new(ClearTool::new(browser.clone())));
351            tools.push(Arc::new(SelectTool::new(browser.clone())));
352        }
353
354        if self.include_extraction {
355            tools.push(Arc::new(ExtractTextTool::new(browser.clone())));
356            tools.push(Arc::new(ExtractAttributeTool::new(browser.clone())));
357            tools.push(Arc::new(ExtractLinksTool::new(browser.clone())));
358            tools.push(Arc::new(PageInfoTool::new(browser.clone())));
359            tools.push(Arc::new(PageSourceTool::new(browser.clone())));
360        }
361
362        if self.include_wait {
363            tools.push(Arc::new(WaitForElementTool::new(browser.clone())));
364            tools.push(Arc::new(WaitTool::new()));
365            tools.push(Arc::new(WaitForPageLoadTool::new(browser.clone())));
366            tools.push(Arc::new(WaitForTextTool::new(browser.clone())));
367        }
368
369        if self.include_screenshot {
370            tools.push(Arc::new(ScreenshotTool::new(browser.clone())));
371        }
372
373        if self.include_js {
374            tools.push(Arc::new(EvaluateJsTool::new(browser.clone())));
375            tools.push(Arc::new(ScrollTool::new(browser.clone())));
376            tools.push(Arc::new(HoverTool::new(browser.clone())));
377            tools.push(Arc::new(AlertTool::new(browser.clone())));
378        }
379
380        if self.include_cookies {
381            tools.push(Arc::new(GetCookiesTool::new(browser.clone())));
382            tools.push(Arc::new(GetCookieTool::new(browser.clone())));
383            tools.push(Arc::new(AddCookieTool::new(browser.clone())));
384            tools.push(Arc::new(DeleteCookieTool::new(browser.clone())));
385            tools.push(Arc::new(DeleteAllCookiesTool::new(browser.clone())));
386        }
387
388        if self.include_windows {
389            tools.push(Arc::new(ListWindowsTool::new(browser.clone())));
390            tools.push(Arc::new(NewTabTool::new(browser.clone())));
391            tools.push(Arc::new(NewWindowTool::new(browser.clone())));
392            tools.push(Arc::new(SwitchWindowTool::new(browser.clone())));
393            tools.push(Arc::new(CloseWindowTool::new(browser.clone())));
394            tools.push(Arc::new(MaximizeWindowTool::new(browser.clone())));
395            tools.push(Arc::new(MinimizeWindowTool::new(browser.clone())));
396            tools.push(Arc::new(SetWindowSizeTool::new(browser.clone())));
397        }
398
399        if self.include_frames {
400            tools.push(Arc::new(SwitchToFrameTool::new(browser.clone())));
401            tools.push(Arc::new(SwitchToParentFrameTool::new(browser.clone())));
402            tools.push(Arc::new(SwitchToDefaultContentTool::new(browser.clone())));
403        }
404
405        if self.include_actions {
406            tools.push(Arc::new(DragAndDropTool::new(browser.clone())));
407            tools.push(Arc::new(RightClickTool::new(browser.clone())));
408            tools.push(Arc::new(FocusTool::new(browser.clone())));
409            tools.push(Arc::new(ElementStateTool::new(browser.clone())));
410            tools.push(Arc::new(PressKeyTool::new(browser.clone())));
411            tools.push(Arc::new(FileUploadTool::new(browser.clone())));
412            tools.push(Arc::new(PrintToPdfTool::new(browser)));
413        }
414
415        tools
416    }
417}
418
419#[async_trait]
420impl Toolset for BrowserToolset {
421    fn name(&self) -> &str {
422        "browser"
423    }
424
425    async fn tools(&self, ctx: Arc<dyn ReadonlyContext>) -> Result<Vec<Arc<dyn Tool>>> {
426        let session = self.resolver.resolve(&ctx).await?;
427        Ok(self.build_tools(session))
428    }
429}
430
431/// Helper function to create a minimal browser toolset with only essential tools.
432pub fn minimal_browser_tools(browser: Arc<BrowserSession>) -> Vec<Arc<dyn Tool>> {
433    vec![
434        Arc::new(NavigateTool::new(browser.clone())),
435        Arc::new(ClickTool::new(browser.clone())),
436        Arc::new(TypeTool::new(browser.clone())),
437        Arc::new(ExtractTextTool::new(browser.clone())),
438        Arc::new(WaitForElementTool::new(browser.clone())),
439        Arc::new(ScreenshotTool::new(browser)),
440    ]
441}
442
443/// Helper function to create a read-only browser toolset (no interaction).
444pub fn readonly_browser_tools(browser: Arc<BrowserSession>) -> Vec<Arc<dyn Tool>> {
445    vec![
446        Arc::new(NavigateTool::new(browser.clone())),
447        Arc::new(ExtractTextTool::new(browser.clone())),
448        Arc::new(ExtractAttributeTool::new(browser.clone())),
449        Arc::new(ExtractLinksTool::new(browser.clone())),
450        Arc::new(PageInfoTool::new(browser.clone())),
451        Arc::new(ScreenshotTool::new(browser.clone())),
452        Arc::new(ScrollTool::new(browser)),
453    ]
454}
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459    use crate::config::BrowserConfig;
460
461    #[test]
462    fn test_toolset_all_tools() {
463        let browser = Arc::new(BrowserSession::new(BrowserConfig::default()));
464        let toolset = BrowserToolset::new(browser);
465        let tools = toolset.all_tools();
466
467        // Should have 46 tools total
468        assert!(tools.len() > 40);
469
470        // Check some tool names exist
471        let tool_names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
472        assert!(tool_names.contains(&"browser_navigate"));
473        assert!(tool_names.contains(&"browser_click"));
474        assert!(tool_names.contains(&"browser_type"));
475        assert!(tool_names.contains(&"browser_screenshot"));
476        // New tools
477        assert!(tool_names.contains(&"browser_get_cookies"));
478        assert!(tool_names.contains(&"browser_new_tab"));
479        assert!(tool_names.contains(&"browser_switch_to_frame"));
480        assert!(tool_names.contains(&"browser_drag_and_drop"));
481    }
482
483    #[test]
484    fn test_toolset_selective() {
485        let browser = Arc::new(BrowserSession::new(BrowserConfig::default()));
486        let toolset = BrowserToolset::new(browser)
487            .with_navigation(true)
488            .with_interaction(false)
489            .with_extraction(false)
490            .with_wait(false)
491            .with_screenshot(false)
492            .with_js(false)
493            .with_cookies(false)
494            .with_windows(false)
495            .with_frames(false)
496            .with_actions(false);
497
498        let tools = toolset.all_tools();
499
500        // Should only have navigation tools
501        assert_eq!(tools.len(), 4);
502    }
503
504    #[test]
505    fn test_minimal_tools() {
506        let browser = Arc::new(BrowserSession::new(BrowserConfig::default()));
507        let tools = minimal_browser_tools(browser);
508
509        assert_eq!(tools.len(), 6);
510    }
511
512    #[test]
513    fn test_profile_form_filling() {
514        let browser = Arc::new(BrowserSession::new(BrowserConfig::default()));
515        let toolset = BrowserToolset::with_profile(browser, BrowserProfile::FormFilling);
516        let tools = toolset.all_tools();
517
518        // Navigation (4) + Interaction (5) + Extraction (5) + Wait (4) + Screenshot (1) = 19
519        let tool_names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
520        assert!(tool_names.contains(&"browser_navigate"));
521        assert!(tool_names.contains(&"browser_click"));
522        assert!(tool_names.contains(&"browser_type"));
523        assert!(tool_names.contains(&"browser_select"));
524        assert!(tool_names.contains(&"browser_clear"));
525        assert!(tool_names.contains(&"browser_screenshot"));
526        // Should NOT include cookies, windows, frames, actions
527        assert!(!tool_names.contains(&"browser_get_cookies"));
528        assert!(!tool_names.contains(&"browser_new_tab"));
529        assert!(!tool_names.contains(&"browser_switch_to_frame"));
530        assert!(!tool_names.contains(&"browser_drag_and_drop"));
531    }
532
533    #[test]
534    fn test_profile_scraping() {
535        let browser = Arc::new(BrowserSession::new(BrowserConfig::default()));
536        let toolset = BrowserToolset::with_profile(browser, BrowserProfile::Scraping);
537        let tools = toolset.all_tools();
538
539        let tool_names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
540        assert!(tool_names.contains(&"browser_navigate"));
541        assert!(tool_names.contains(&"browser_extract_text"));
542        assert!(tool_names.contains(&"browser_extract_links"));
543        assert!(tool_names.contains(&"browser_screenshot"));
544        assert!(tool_names.contains(&"browser_scroll"));
545        // Should NOT include interaction tools
546        assert!(!tool_names.contains(&"browser_click"));
547        assert!(!tool_names.contains(&"browser_type"));
548    }
549
550    #[test]
551    fn test_profile_full_matches_new() {
552        let browser1 = Arc::new(BrowserSession::new(BrowserConfig::default()));
553        let browser2 = Arc::new(BrowserSession::new(BrowserConfig::default()));
554        let full = BrowserToolset::with_profile(browser1, BrowserProfile::Full);
555        let default = BrowserToolset::new(browser2);
556        assert_eq!(full.all_tools().len(), default.all_tools().len());
557    }
558}