browsing 0.1.7

Browser automation: navigate, click, extract, screenshot. Standalone browser control via CDP.
Documentation
//! Tools service for action registry

use crate::agent::memory::AgentMemory;
use crate::agent::views::ActionResult;
use crate::error::{BrowsingError, Result};
use crate::observability::{SessionRecorder, StepResult};
use crate::tools::handlers::{
    AdvancedHandler, ContentHandler, FileHandler, Handler, InteractionHandler, NavigationHandler,
    TabsHandler,
};
use crate::tools::registry::Registry;
use crate::tools::views::{ActionContext, ActionModel, ActionParams};
use crate::traits::BrowserClient;

/// Tools registry for agent actions
pub struct Tools {
    /// Action registry
    pub registry: Registry,
    /// Whether to display files in done text
    pub display_files_in_done_text: bool,
}

impl Tools {
    /// Creates a new tools registry
    pub fn new(exclude_actions: Vec<String>) -> Self {
        let mut registry = Registry::new(exclude_actions);

        // Register default actions
        Self::register_default_actions(&mut registry);

        Self {
            registry,
            display_files_in_done_text: true,
        }
    }

    fn register_default_actions(registry: &mut Registry) {
        // Register basic navigation actions
        registry.register_action(
            "search",
            "Search the web using a search engine",
            None,
        );

        registry.register_action(
            "navigate",
            "Navigate to a URL",
            None,
        );

        registry.register_action(
            "go_back",
            "Go back in browser history",
            None,
        );

        registry.register_action(
            "go_forward",
            "Go forward in browser history",
            None,
        );

        registry.register_action(
            "reload",
            "Reload the current page",
            None,
        );

        registry.register_action(
            "click",
            "Click an element by index",
            None,
        );

        registry.register_action(
            "input",
            "Input text into a field",
            None,
        );

        registry.register_action(
            "done",
            "Mark the task as complete",
            None,
        );

        registry.register_action(
            "switch",
            "Switch to another open tab by tab_id",
            None,
        );

        registry.register_action(
            "close",
            "Close a tab by tab_id",
            None,
        );

        registry.register_action(
            "scroll",
            "Scroll the page up or down by pages",
            None,
        );

        registry.register_action(
            "wait",
            "Wait for a condition (element, text, url, stability) or fixed seconds",
            None,
        );

        registry.register_action(
            "send_keys",
            "Send keyboard keys (Enter, Escape, Tab, etc.)",
            None,
        );

        registry.register_action(
            "evaluate",
            "Execute JavaScript code on the page",
            None,
        );

        registry.register_action(
            "find_text",
            "Scroll to specific text on page",
            None,
        );

        registry.register_action(
            "dropdown_options",
            "Get dropdown option values",
            None,
        );

        registry.register_action(
            "select_dropdown",
            "Select dropdown options",
            None,
        );

        registry.register_action(
            "upload_file",
            "Upload files to file inputs",
            None,
        );

        registry.register_action(
            "extract",
            "Extract structured data from page content. Use when: on right page, know what to extract, haven't called before on same page+query.",
            None,
        );

        registry.register_action(
            "write_file",
            "Write content to a file on disk",
            None,
        );

        registry.register_action(
            "read_file",
            "Read content from a file on disk",
            None,
        );

        registry.register_action(
            "replace_file",
            "Replace text within a file on disk",
            None,
        );
    }

    /// Executes an action (core browser automation, no AI required)
    pub async fn act(
        &self,
        action: ActionModel,
        browser_session: &mut dyn BrowserClient,
        selector_map: Option<
            &std::collections::HashMap<u32, crate::dom::views::DOMInteractedElement>,
        >,
        memory: Option<&mut AgentMemory>,
        recorder: Option<&mut SessionRecorder>,
        dom_snapshot: Option<String>,
    ) -> Result<ActionResult> {
        let start = std::time::Instant::now();
        let action_type = action.action_type.clone();
        let params = action.params.clone();
        let url = browser_session.get_current_url().await.ok();

        let result = self
            ._dispatch(action, browser_session, selector_map, memory, dom_snapshot.clone())
            .await;

        if let Some(rec) = recorder {
            let duration_ms = start.elapsed().as_millis() as u64;
            let step_result = match &result {
                Ok(r) => StepResult::success(r.extracted_content.clone()),
                Err(e) => StepResult::failure(format!("{e}")),
            };
            rec.record_action(
                rec.len(),
                &action_type,
                params,
                url,
                dom_snapshot,
                step_result,
                duration_ms,
            );
        }

        result
    }

    async fn _dispatch(
        &self,
        action: ActionModel,
        browser_session: &mut dyn BrowserClient,
        selector_map: Option<
            &std::collections::HashMap<u32, crate::dom::views::DOMInteractedElement>,
        >,
        memory: Option<&mut AgentMemory>,
        dom_snapshot: Option<String>,
    ) -> Result<ActionResult> {
        let action_type = action.action_type.as_str();

        // Check if this is a custom action with a handler
        if let Some(handler) = self.registry.get_handler(action_type) {
            let params =
                ActionParams::new(&action.params).with_action_type(action.action_type.clone());
            let mut context = ActionContext {
                browser: browser_session,
                selector_map,
                memory,
                recorder: None,
                dom_snapshot,
            };
            return handler.execute(&params, &mut context).await;
        }

        // Use new handler-based dispatch for built-in actions
        let params = ActionParams::new(&action.params).with_action_type(action.action_type.clone());
        let mut context = ActionContext {
            browser: browser_session,
            selector_map,
            memory,
            recorder: None,
            dom_snapshot,
        };

        match action_type {
            // Navigation actions
            "search" | "navigate" | "go_back" | "go_forward" | "reload" => NavigationHandler.handle(&params, &mut context).await,
            // Interaction actions
            "click" | "input" | "send_keys" => {
                InteractionHandler.handle(&params, &mut context).await
            }
            // Tab actions
            "switch" | "close" => TabsHandler.handle(&params, &mut context).await,
            // Content actions
            "scroll" | "find_text" | "dropdown_options" | "select_dropdown" => {
                ContentHandler.handle(&params, &mut context).await
            }
            // Advanced actions
            "done" | "evaluate" | "upload_file" | "wait" => {
                AdvancedHandler.handle(&params, &mut context).await
            }
            // File actions
            "write_file" | "read_file" | "replace_file" => FileHandler.handle(&params, &mut context).await,
            "extract" => {
                crate::tools::handlers::extract::handle_extract(action, browser_session).await
            }
            _ => Err(BrowsingError::Tool(format!(
                "Unknown action type: {action_type}"
            ))),
        }
    }

    /// Register a custom action
    pub fn register_custom_action<H: crate::tools::views::ActionHandler + 'static>(
        &mut self,
        name: String,
        description: String,
        domains: Option<Vec<String>>,
        handler: H,
    ) {
        self.registry
            .register_custom_action(name, description, domains, handler);
    }
}

impl Default for Tools {
    fn default() -> Self {
        Self::new(vec![])
    }
}