Skip to main content

limit_cli/tools/browser/
tool.rs

1//! Browser tool for LLM agent
2//!
3//! Implements the Tool trait for browser automation in the agent system.
4
5use super::action::BrowserAction;
6use super::client::BrowserClient;
7use super::config::{BrowserConfig, BrowserEngine};
8use super::executor::{BrowserExecutor, CliExecutor};
9use super::handlers;
10use async_trait::async_trait;
11use limit_agent::error::AgentError;
12use limit_agent::Tool;
13use serde_json::Value;
14use std::str::FromStr;
15use std::sync::Arc;
16
17/// Browser automation tool for the LLM agent
18pub struct BrowserTool {
19    client: BrowserClient,
20}
21
22impl BrowserTool {
23    /// Create a new browser tool with default configuration
24    pub fn new() -> Self {
25        Self::with_config(BrowserConfig::default())
26    }
27
28    /// Create a browser tool with custom configuration
29    pub fn with_config(config: BrowserConfig) -> Self {
30        let executor = Arc::new(CliExecutor::new(config));
31        Self {
32            client: BrowserClient::new(executor),
33        }
34    }
35
36    /// Create a browser tool with a custom executor (for testing)
37    pub fn with_executor(executor: Arc<dyn BrowserExecutor>) -> Self {
38        Self {
39            client: BrowserClient::new(executor),
40        }
41    }
42
43    /// Parse engine from string
44    #[allow(dead_code)]
45    fn parse_engine(s: &str) -> Option<BrowserEngine> {
46        match s.to_lowercase().as_str() {
47            "chrome" => Some(BrowserEngine::Chrome),
48            "lightpanda" => Some(BrowserEngine::Lightpanda),
49            _ => None,
50        }
51    }
52}
53
54impl Default for BrowserTool {
55    fn default() -> Self {
56        Self::new()
57    }
58}
59
60impl std::fmt::Debug for BrowserTool {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        f.debug_struct("BrowserTool").finish()
63    }
64}
65
66#[async_trait]
67impl Tool for BrowserTool {
68    fn name(&self) -> &str {
69        "browser"
70    }
71
72    async fn execute(&self, args: Value) -> Result<Value, AgentError> {
73        let action = args
74            .get("action")
75            .and_then(|v| v.as_str())
76            .ok_or_else(|| AgentError::ToolError("Missing 'action' argument".to_string()))?;
77
78        match BrowserAction::from_str(action)? {
79            BrowserAction::Open => handlers::navigation::open(&self.client, &args).await,
80            BrowserAction::Close => handlers::navigation::close(&self.client, &args).await,
81            BrowserAction::Snapshot => handlers::navigation::snapshot(&self.client, &args).await,
82            BrowserAction::Screenshot => {
83                handlers::navigation::screenshot(&self.client, &args).await
84            }
85            BrowserAction::Back => handlers::navigation::back(&self.client, &args).await,
86            BrowserAction::Forward => handlers::navigation::forward(&self.client, &args).await,
87            BrowserAction::Reload => handlers::navigation::reload(&self.client, &args).await,
88            BrowserAction::Click => handlers::interaction::click(&self.client, &args).await,
89            BrowserAction::Fill => handlers::interaction::fill(&self.client, &args).await,
90            BrowserAction::Type => handlers::interaction::type_(&self.client, &args).await,
91            BrowserAction::Press => handlers::interaction::press(&self.client, &args).await,
92            BrowserAction::Hover => handlers::interaction::hover(&self.client, &args).await,
93            BrowserAction::Select => handlers::interaction::select(&self.client, &args).await,
94            BrowserAction::Dblclick => handlers::interaction::dblclick(&self.client, &args).await,
95            BrowserAction::Focus => handlers::interaction::focus(&self.client, &args).await,
96            BrowserAction::Check => handlers::interaction::check(&self.client, &args).await,
97            BrowserAction::Uncheck => handlers::interaction::uncheck(&self.client, &args).await,
98            BrowserAction::Scrollintoview => {
99                handlers::interaction::scrollintoview(&self.client, &args).await
100            }
101            BrowserAction::Drag => handlers::interaction::drag(&self.client, &args).await,
102            BrowserAction::Upload => handlers::interaction::upload(&self.client, &args).await,
103            BrowserAction::Pdf => handlers::interaction::pdf(&self.client, &args).await,
104            BrowserAction::Get => handlers::query::get(&self.client, &args).await,
105            BrowserAction::GetAttr => handlers::query::get_attr(&self.client, &args).await,
106            BrowserAction::GetCount => handlers::query::get_count(&self.client, &args).await,
107            BrowserAction::GetBox => handlers::query::get_box(&self.client, &args).await,
108            BrowserAction::GetStyles => handlers::query::get_styles(&self.client, &args).await,
109            BrowserAction::Wait => handlers::wait::wait(&self.client, &args).await,
110            BrowserAction::WaitForText => handlers::wait::wait_for_text(&self.client, &args).await,
111            BrowserAction::WaitForUrl => handlers::wait::wait_for_url(&self.client, &args).await,
112            BrowserAction::WaitForLoad => handlers::wait::wait_for_load(&self.client, &args).await,
113            BrowserAction::WaitForDownload => {
114                handlers::wait::wait_for_download(&self.client, &args).await
115            }
116            BrowserAction::WaitForFn => handlers::wait::wait_for_fn(&self.client, &args).await,
117            BrowserAction::WaitForState => {
118                handlers::wait::wait_for_state(&self.client, &args).await
119            }
120            BrowserAction::Find => handlers::state::find(&self.client, &args).await,
121            BrowserAction::Scroll => handlers::state::scroll(&self.client, &args).await,
122            BrowserAction::Is => handlers::state::is(&self.client, &args).await,
123            BrowserAction::Download => handlers::state::download(&self.client, &args).await,
124            BrowserAction::TabList => handlers::tabs::tab_list(&self.client, &args).await,
125            BrowserAction::TabNew => handlers::tabs::tab_new(&self.client, &args).await,
126            BrowserAction::TabClose => handlers::tabs::tab_close(&self.client, &args).await,
127            BrowserAction::TabSelect => handlers::tabs::tab_select(&self.client, &args).await,
128            BrowserAction::DialogAccept => {
129                handlers::dialog::dialog_accept(&self.client, &args).await
130            }
131            BrowserAction::DialogDismiss => {
132                handlers::dialog::dialog_dismiss(&self.client, &args).await
133            }
134            BrowserAction::Cookies => handlers::storage::cookies(&self.client, &args).await,
135            BrowserAction::CookiesSet => handlers::storage::cookies_set(&self.client, &args).await,
136            BrowserAction::StorageGet => handlers::storage::storage_get(&self.client, &args).await,
137            BrowserAction::StorageSet => handlers::storage::storage_set(&self.client, &args).await,
138            BrowserAction::NetworkRequests => {
139                handlers::storage::network_requests(&self.client, &args).await
140            }
141            BrowserAction::SetViewport => {
142                handlers::settings::set_viewport(&self.client, &args).await
143            }
144            BrowserAction::SetDevice => handlers::settings::set_device(&self.client, &args).await,
145            BrowserAction::SetGeo => handlers::settings::set_geo(&self.client, &args).await,
146            BrowserAction::Eval => handlers::query::eval(&self.client, &args).await,
147        }
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn test_browser_tool_name() {
157        let tool = BrowserTool::new();
158        assert_eq!(tool.name(), "browser");
159    }
160
161    #[test]
162    fn test_browser_tool_default() {
163        let tool = BrowserTool::default();
164        assert_eq!(tool.name(), "browser");
165    }
166
167    #[test]
168    fn test_parse_engine() {
169        assert_eq!(
170            BrowserTool::parse_engine("chrome"),
171            Some(BrowserEngine::Chrome)
172        );
173        assert_eq!(
174            BrowserTool::parse_engine("CHROME"),
175            Some(BrowserEngine::Chrome)
176        );
177        assert_eq!(
178            BrowserTool::parse_engine("lightpanda"),
179            Some(BrowserEngine::Lightpanda)
180        );
181        assert_eq!(
182            BrowserTool::parse_engine("LightPanda"),
183            Some(BrowserEngine::Lightpanda)
184        );
185        assert_eq!(BrowserTool::parse_engine("invalid"), None);
186    }
187
188    #[tokio::test]
189    async fn test_browser_tool_missing_action() {
190        let tool = BrowserTool::new();
191        let args = serde_json::json!({
192            "url": "https://example.com"
193        });
194
195        let result = tool.execute(args).await;
196        assert!(result.is_err());
197        let err = result.unwrap_err();
198        assert!(err.to_string().contains("Missing 'action'"));
199    }
200
201    #[tokio::test]
202    async fn test_browser_tool_unknown_action() {
203        let tool = BrowserTool::new();
204        let args = serde_json::json!({
205            "action": "unknown"
206        });
207
208        let result = tool.execute(args).await;
209        assert!(result.is_err());
210        let err = result.unwrap_err();
211        assert!(err.to_string().contains("Unknown browser action"));
212    }
213
214    #[tokio::test]
215    async fn test_browser_tool_open_missing_url() {
216        let tool = BrowserTool::new();
217        let args = serde_json::json!({
218            "action": "open"
219        });
220
221        let result = tool.execute(args).await;
222        assert!(result.is_err());
223        let err = result.unwrap_err();
224        assert!(err.to_string().contains("Missing 'url'"));
225    }
226
227    #[tokio::test]
228    async fn test_browser_tool_click_missing_selector() {
229        let tool = BrowserTool::new();
230        let args = serde_json::json!({
231            "action": "click"
232        });
233
234        let result = tool.execute(args).await;
235        assert!(result.is_err());
236        let err = result.unwrap_err();
237        assert!(err.to_string().contains("Missing 'selector'"));
238    }
239
240    #[tokio::test]
241    async fn test_browser_tool_screenshot_missing_path() {
242        let tool = BrowserTool::new();
243        let args = serde_json::json!({
244            "action": "screenshot"
245        });
246
247        let result = tool.execute(args).await;
248        assert!(result.is_err());
249        let err = result.unwrap_err();
250        assert!(err.to_string().contains("Missing 'path'"));
251    }
252}