use crate::pool::BrowserSessionPool;
use crate::session::BrowserSession;
use crate::tools::*;
use adk_core::{ReadonlyContext, Result, Tool, Toolset};
use async_trait::async_trait;
use std::sync::Arc;
enum SessionResolver {
Fixed(Arc<BrowserSession>),
Pool(Arc<BrowserSessionPool>),
}
impl SessionResolver {
async fn resolve(&self, ctx: &Arc<dyn ReadonlyContext>) -> Result<Arc<BrowserSession>> {
match self {
SessionResolver::Fixed(session) => Ok(session.clone()),
SessionResolver::Pool(pool) => pool.get_or_create(ctx.user_id()).await,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BrowserProfile {
Minimal,
FormFilling,
Scraping,
Full,
}
pub struct BrowserToolset {
resolver: SessionResolver,
include_navigation: bool,
include_interaction: bool,
include_extraction: bool,
include_wait: bool,
include_screenshot: bool,
include_js: bool,
include_cookies: bool,
include_windows: bool,
include_frames: bool,
include_actions: bool,
}
impl BrowserToolset {
pub fn new(browser: Arc<BrowserSession>) -> Self {
Self {
resolver: SessionResolver::Fixed(browser),
include_navigation: true,
include_interaction: true,
include_extraction: true,
include_wait: true,
include_screenshot: true,
include_js: true,
include_cookies: true,
include_windows: true,
include_frames: true,
include_actions: true,
}
}
pub fn with_pool(pool: Arc<BrowserSessionPool>) -> Self {
Self {
resolver: SessionResolver::Pool(pool),
include_navigation: true,
include_interaction: true,
include_extraction: true,
include_wait: true,
include_screenshot: true,
include_js: true,
include_cookies: true,
include_windows: true,
include_frames: true,
include_actions: true,
}
}
pub fn with_pool_and_profile(pool: Arc<BrowserSessionPool>, profile: BrowserProfile) -> Self {
match profile {
BrowserProfile::Minimal => Self {
resolver: SessionResolver::Pool(pool),
include_navigation: true,
include_interaction: true,
include_extraction: true,
include_wait: true,
include_screenshot: true,
include_js: false,
include_cookies: false,
include_windows: false,
include_frames: false,
include_actions: false,
},
BrowserProfile::FormFilling => Self {
resolver: SessionResolver::Pool(pool),
include_navigation: true,
include_interaction: true,
include_extraction: true,
include_wait: true,
include_screenshot: true,
include_js: false,
include_cookies: false,
include_windows: false,
include_frames: false,
include_actions: false,
},
BrowserProfile::Scraping => Self {
resolver: SessionResolver::Pool(pool),
include_navigation: true,
include_interaction: false,
include_extraction: true,
include_wait: false,
include_screenshot: true,
include_js: true,
include_cookies: false,
include_windows: false,
include_frames: false,
include_actions: false,
},
BrowserProfile::Full => Self::with_pool(pool),
}
}
pub fn with_profile(browser: Arc<BrowserSession>, profile: BrowserProfile) -> Self {
match profile {
BrowserProfile::Minimal => Self {
resolver: SessionResolver::Fixed(browser),
include_navigation: true,
include_interaction: true,
include_extraction: true,
include_wait: true,
include_screenshot: true,
include_js: false,
include_cookies: false,
include_windows: false,
include_frames: false,
include_actions: false,
},
BrowserProfile::FormFilling => Self {
resolver: SessionResolver::Fixed(browser),
include_navigation: true,
include_interaction: true,
include_extraction: true,
include_wait: true,
include_screenshot: true,
include_js: false,
include_cookies: false,
include_windows: false,
include_frames: false,
include_actions: false,
},
BrowserProfile::Scraping => Self {
resolver: SessionResolver::Fixed(browser),
include_navigation: true,
include_interaction: false,
include_extraction: true,
include_wait: false,
include_screenshot: true,
include_js: true, include_cookies: false,
include_windows: false,
include_frames: false,
include_actions: false,
},
BrowserProfile::Full => Self::new(browser),
}
}
pub fn with_navigation(mut self, enabled: bool) -> Self {
self.include_navigation = enabled;
self
}
pub fn with_interaction(mut self, enabled: bool) -> Self {
self.include_interaction = enabled;
self
}
pub fn with_extraction(mut self, enabled: bool) -> Self {
self.include_extraction = enabled;
self
}
pub fn with_wait(mut self, enabled: bool) -> Self {
self.include_wait = enabled;
self
}
pub fn with_screenshot(mut self, enabled: bool) -> Self {
self.include_screenshot = enabled;
self
}
pub fn with_js(mut self, enabled: bool) -> Self {
self.include_js = enabled;
self
}
pub fn with_cookies(mut self, enabled: bool) -> Self {
self.include_cookies = enabled;
self
}
pub fn with_windows(mut self, enabled: bool) -> Self {
self.include_windows = enabled;
self
}
pub fn with_frames(mut self, enabled: bool) -> Self {
self.include_frames = enabled;
self
}
pub fn with_actions(mut self, enabled: bool) -> Self {
self.include_actions = enabled;
self
}
pub fn all_tools(&self) -> Vec<Arc<dyn Tool>> {
match &self.resolver {
SessionResolver::Fixed(session) => self.build_tools(session.clone()),
SessionResolver::Pool(_) => {
tracing::warn!(
"BrowserToolset::all_tools() called on a pool-backed toolset. \
Returns empty vec. Use Toolset::tools(ctx) instead."
);
Vec::new()
}
}
}
pub fn try_all_tools(&self) -> Result<Vec<Arc<dyn Tool>>> {
match &self.resolver {
SessionResolver::Fixed(session) => Ok(self.build_tools(session.clone())),
SessionResolver::Pool(_) => Err(adk_core::AdkError::tool(
"Cannot resolve tools synchronously for a pool-backed BrowserToolset. \
Use Toolset::tools(ctx) instead.",
)),
}
}
fn build_tools(&self, browser: Arc<BrowserSession>) -> Vec<Arc<dyn Tool>> {
let mut tools: Vec<Arc<dyn Tool>> = Vec::new();
if self.include_navigation {
tools.push(Arc::new(NavigateTool::new(browser.clone())));
tools.push(Arc::new(BackTool::new(browser.clone())));
tools.push(Arc::new(ForwardTool::new(browser.clone())));
tools.push(Arc::new(RefreshTool::new(browser.clone())));
}
if self.include_interaction {
tools.push(Arc::new(ClickTool::new(browser.clone())));
tools.push(Arc::new(DoubleClickTool::new(browser.clone())));
tools.push(Arc::new(TypeTool::new(browser.clone())));
tools.push(Arc::new(ClearTool::new(browser.clone())));
tools.push(Arc::new(SelectTool::new(browser.clone())));
}
if self.include_extraction {
tools.push(Arc::new(ExtractTextTool::new(browser.clone())));
tools.push(Arc::new(ExtractAttributeTool::new(browser.clone())));
tools.push(Arc::new(ExtractLinksTool::new(browser.clone())));
tools.push(Arc::new(PageInfoTool::new(browser.clone())));
tools.push(Arc::new(PageSourceTool::new(browser.clone())));
}
if self.include_wait {
tools.push(Arc::new(WaitForElementTool::new(browser.clone())));
tools.push(Arc::new(WaitTool::new()));
tools.push(Arc::new(WaitForPageLoadTool::new(browser.clone())));
tools.push(Arc::new(WaitForTextTool::new(browser.clone())));
}
if self.include_screenshot {
tools.push(Arc::new(ScreenshotTool::new(browser.clone())));
}
if self.include_js {
tools.push(Arc::new(EvaluateJsTool::new(browser.clone())));
tools.push(Arc::new(ScrollTool::new(browser.clone())));
tools.push(Arc::new(HoverTool::new(browser.clone())));
tools.push(Arc::new(AlertTool::new(browser.clone())));
}
if self.include_cookies {
tools.push(Arc::new(GetCookiesTool::new(browser.clone())));
tools.push(Arc::new(GetCookieTool::new(browser.clone())));
tools.push(Arc::new(AddCookieTool::new(browser.clone())));
tools.push(Arc::new(DeleteCookieTool::new(browser.clone())));
tools.push(Arc::new(DeleteAllCookiesTool::new(browser.clone())));
}
if self.include_windows {
tools.push(Arc::new(ListWindowsTool::new(browser.clone())));
tools.push(Arc::new(NewTabTool::new(browser.clone())));
tools.push(Arc::new(NewWindowTool::new(browser.clone())));
tools.push(Arc::new(SwitchWindowTool::new(browser.clone())));
tools.push(Arc::new(CloseWindowTool::new(browser.clone())));
tools.push(Arc::new(MaximizeWindowTool::new(browser.clone())));
tools.push(Arc::new(MinimizeWindowTool::new(browser.clone())));
tools.push(Arc::new(SetWindowSizeTool::new(browser.clone())));
}
if self.include_frames {
tools.push(Arc::new(SwitchToFrameTool::new(browser.clone())));
tools.push(Arc::new(SwitchToParentFrameTool::new(browser.clone())));
tools.push(Arc::new(SwitchToDefaultContentTool::new(browser.clone())));
}
if self.include_actions {
tools.push(Arc::new(DragAndDropTool::new(browser.clone())));
tools.push(Arc::new(RightClickTool::new(browser.clone())));
tools.push(Arc::new(FocusTool::new(browser.clone())));
tools.push(Arc::new(ElementStateTool::new(browser.clone())));
tools.push(Arc::new(PressKeyTool::new(browser.clone())));
tools.push(Arc::new(FileUploadTool::new(browser.clone())));
tools.push(Arc::new(PrintToPdfTool::new(browser)));
}
tools
}
}
#[async_trait]
impl Toolset for BrowserToolset {
fn name(&self) -> &str {
"browser"
}
async fn tools(&self, ctx: Arc<dyn ReadonlyContext>) -> Result<Vec<Arc<dyn Tool>>> {
let session = self.resolver.resolve(&ctx).await?;
Ok(self.build_tools(session))
}
}
pub fn minimal_browser_tools(browser: Arc<BrowserSession>) -> Vec<Arc<dyn Tool>> {
vec![
Arc::new(NavigateTool::new(browser.clone())),
Arc::new(ClickTool::new(browser.clone())),
Arc::new(TypeTool::new(browser.clone())),
Arc::new(ExtractTextTool::new(browser.clone())),
Arc::new(WaitForElementTool::new(browser.clone())),
Arc::new(ScreenshotTool::new(browser)),
]
}
pub fn readonly_browser_tools(browser: Arc<BrowserSession>) -> Vec<Arc<dyn Tool>> {
vec![
Arc::new(NavigateTool::new(browser.clone())),
Arc::new(ExtractTextTool::new(browser.clone())),
Arc::new(ExtractAttributeTool::new(browser.clone())),
Arc::new(ExtractLinksTool::new(browser.clone())),
Arc::new(PageInfoTool::new(browser.clone())),
Arc::new(ScreenshotTool::new(browser.clone())),
Arc::new(ScrollTool::new(browser)),
]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::BrowserConfig;
#[test]
fn test_toolset_all_tools() {
let browser = Arc::new(BrowserSession::new(BrowserConfig::default()));
let toolset = BrowserToolset::new(browser);
let tools = toolset.all_tools();
assert!(tools.len() > 40);
let tool_names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
assert!(tool_names.contains(&"browser_navigate"));
assert!(tool_names.contains(&"browser_click"));
assert!(tool_names.contains(&"browser_type"));
assert!(tool_names.contains(&"browser_screenshot"));
assert!(tool_names.contains(&"browser_get_cookies"));
assert!(tool_names.contains(&"browser_new_tab"));
assert!(tool_names.contains(&"browser_switch_to_frame"));
assert!(tool_names.contains(&"browser_drag_and_drop"));
}
#[test]
fn test_toolset_selective() {
let browser = Arc::new(BrowserSession::new(BrowserConfig::default()));
let toolset = BrowserToolset::new(browser)
.with_navigation(true)
.with_interaction(false)
.with_extraction(false)
.with_wait(false)
.with_screenshot(false)
.with_js(false)
.with_cookies(false)
.with_windows(false)
.with_frames(false)
.with_actions(false);
let tools = toolset.all_tools();
assert_eq!(tools.len(), 4);
}
#[test]
fn test_minimal_tools() {
let browser = Arc::new(BrowserSession::new(BrowserConfig::default()));
let tools = minimal_browser_tools(browser);
assert_eq!(tools.len(), 6);
}
#[test]
fn test_profile_form_filling() {
let browser = Arc::new(BrowserSession::new(BrowserConfig::default()));
let toolset = BrowserToolset::with_profile(browser, BrowserProfile::FormFilling);
let tools = toolset.all_tools();
let tool_names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
assert!(tool_names.contains(&"browser_navigate"));
assert!(tool_names.contains(&"browser_click"));
assert!(tool_names.contains(&"browser_type"));
assert!(tool_names.contains(&"browser_select"));
assert!(tool_names.contains(&"browser_clear"));
assert!(tool_names.contains(&"browser_screenshot"));
assert!(!tool_names.contains(&"browser_get_cookies"));
assert!(!tool_names.contains(&"browser_new_tab"));
assert!(!tool_names.contains(&"browser_switch_to_frame"));
assert!(!tool_names.contains(&"browser_drag_and_drop"));
}
#[test]
fn test_profile_scraping() {
let browser = Arc::new(BrowserSession::new(BrowserConfig::default()));
let toolset = BrowserToolset::with_profile(browser, BrowserProfile::Scraping);
let tools = toolset.all_tools();
let tool_names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
assert!(tool_names.contains(&"browser_navigate"));
assert!(tool_names.contains(&"browser_extract_text"));
assert!(tool_names.contains(&"browser_extract_links"));
assert!(tool_names.contains(&"browser_screenshot"));
assert!(tool_names.contains(&"browser_scroll"));
assert!(!tool_names.contains(&"browser_click"));
assert!(!tool_names.contains(&"browser_type"));
}
#[test]
fn test_profile_full_matches_new() {
let browser1 = Arc::new(BrowserSession::new(BrowserConfig::default()));
let browser2 = Arc::new(BrowserSession::new(BrowserConfig::default()));
let full = BrowserToolset::with_profile(browser1, BrowserProfile::Full);
let default = BrowserToolset::new(browser2);
assert_eq!(full.all_tools().len(), default.all_tools().len());
}
}