pub mod click;
pub mod close;
pub mod close_tab;
pub mod evaluate;
pub mod extract;
pub mod go_back;
pub mod go_forward;
pub mod hover;
pub mod html_to_markdown;
pub mod input;
pub mod markdown;
pub mod navigate;
pub mod new_tab;
pub mod press_key;
pub mod read_links;
pub mod readability_script;
pub mod screenshot;
pub mod scroll;
pub mod select;
pub mod snapshot;
pub mod switch_tab;
pub mod tab_list;
mod utils;
pub mod wait;
pub use click::ClickParams;
pub use close::CloseParams;
pub use close_tab::CloseTabParams;
pub use evaluate::EvaluateParams;
pub use extract::ExtractParams;
pub use go_back::GoBackParams;
pub use go_forward::GoForwardParams;
pub use hover::HoverParams;
pub use input::InputParams;
pub use markdown::GetMarkdownParams;
pub use navigate::NavigateParams;
pub use new_tab::NewTabParams;
pub use press_key::PressKeyParams;
pub use read_links::ReadLinksParams;
pub use screenshot::ScreenshotParams;
pub use scroll::ScrollParams;
pub use select::SelectParams;
pub use snapshot::SnapshotParams;
pub use switch_tab::SwitchTabParams;
pub use tab_list::TabListParams;
pub use wait::WaitParams;
use crate::browser::BrowserSession;
use crate::dom::DomTree;
use crate::error::Result;
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
pub struct ToolContext<'a> {
pub session: &'a BrowserSession,
pub dom_tree: Option<DomTree>,
}
impl<'a> ToolContext<'a> {
pub fn new(session: &'a BrowserSession) -> Self {
Self {
session,
dom_tree: None,
}
}
pub fn with_dom(session: &'a BrowserSession, dom_tree: DomTree) -> Self {
Self {
session,
dom_tree: Some(dom_tree),
}
}
pub fn get_dom(&mut self) -> Result<&DomTree> {
if self.dom_tree.is_none() {
self.dom_tree = Some(self.session.extract_dom()?);
}
Ok(self.dom_tree.as_ref().unwrap())
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ToolResult {
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, Value>,
}
impl ToolResult {
pub fn success(data: Option<Value>) -> Self {
Self {
success: true,
data,
error: None,
metadata: HashMap::new(),
}
}
pub fn success_with<T: serde::Serialize>(data: T) -> Self {
Self {
success: true,
data: serde_json::to_value(data).ok(),
error: None,
metadata: HashMap::new(),
}
}
pub fn failure(error: impl Into<String>) -> Self {
Self {
success: false,
data: None,
error: Some(error.into()),
metadata: HashMap::new(),
}
}
pub fn with_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
self.metadata.insert(key.into(), value);
self
}
}
pub trait Tool: Send + Sync + Default {
type Params: serde::Serialize + for<'de> serde::Deserialize<'de> + schemars::JsonSchema;
fn name(&self) -> &str;
fn parameters_schema(&self) -> Value {
serde_json::to_value(schemars::schema_for!(Self::Params)).unwrap_or_default()
}
fn execute_typed(&self, params: Self::Params, context: &mut ToolContext) -> Result<ToolResult>;
fn execute(&self, params: Value, context: &mut ToolContext) -> Result<ToolResult> {
let typed_params: Self::Params = serde_json::from_value(params).map_err(|e| {
crate::error::BrowserError::InvalidArgument(format!("Invalid parameters: {}", e))
})?;
self.execute_typed(typed_params, context)
}
}
pub trait DynTool: Send + Sync {
fn name(&self) -> &str;
fn parameters_schema(&self) -> Value;
fn execute(&self, params: Value, context: &mut ToolContext) -> Result<ToolResult>;
}
impl<T: Tool> DynTool for T {
fn name(&self) -> &str {
Tool::name(self)
}
fn parameters_schema(&self) -> Value {
Tool::parameters_schema(self)
}
fn execute(&self, params: Value, context: &mut ToolContext) -> Result<ToolResult> {
Tool::execute(self, params, context)
}
}
pub struct ToolRegistry {
tools: HashMap<String, Arc<dyn DynTool>>,
}
impl ToolRegistry {
pub fn new() -> Self {
Self {
tools: HashMap::new(),
}
}
pub fn with_defaults() -> Self {
let mut registry = Self::new();
registry.register(navigate::NavigateTool);
registry.register(go_back::GoBackTool);
registry.register(go_forward::GoForwardTool);
registry.register(wait::WaitTool);
registry.register(click::ClickTool);
registry.register(input::InputTool);
registry.register(select::SelectTool);
registry.register(hover::HoverTool);
registry.register(press_key::PressKeyTool);
registry.register(scroll::ScrollTool);
registry.register(new_tab::NewTabTool);
registry.register(tab_list::TabListTool);
registry.register(switch_tab::SwitchTabTool);
registry.register(close_tab::CloseTabTool);
registry.register(extract::ExtractContentTool);
registry.register(markdown::GetMarkdownTool);
registry.register(read_links::ReadLinksTool);
registry.register(snapshot::SnapshotTool);
registry.register(screenshot::ScreenshotTool);
registry.register(evaluate::EvaluateTool);
registry.register(close::CloseTool);
registry
}
pub fn register<T: Tool + 'static>(&mut self, tool: T) {
let name = tool.name().to_string();
self.tools.insert(name, Arc::new(tool));
}
pub fn get(&self, name: &str) -> Option<&Arc<dyn DynTool>> {
self.tools.get(name)
}
pub fn has(&self, name: &str) -> bool {
self.tools.contains_key(name)
}
pub fn list_names(&self) -> Vec<String> {
self.tools.keys().cloned().collect()
}
pub fn all_tools(&self) -> Vec<Arc<dyn DynTool>> {
self.tools.values().cloned().collect()
}
pub fn execute(
&self,
name: &str,
params: Value,
context: &mut ToolContext,
) -> Result<ToolResult> {
match self.get(name) {
Some(tool) => tool.execute(params, context),
None => Ok(ToolResult::failure(format!("Tool '{}' not found", name))),
}
}
pub fn count(&self) -> usize {
self.tools.len()
}
}
impl Default for ToolRegistry {
fn default() -> Self {
Self::with_defaults()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tool_result_success() {
let result = ToolResult::success(Some(serde_json::json!({"url": "https://example.com"})));
assert!(result.success);
assert!(result.data.is_some());
assert!(result.error.is_none());
}
#[test]
fn test_tool_result_failure() {
let result = ToolResult::failure("Test error");
assert!(!result.success);
assert!(result.data.is_none());
assert_eq!(result.error, Some("Test error".to_string()));
}
#[test]
fn test_tool_result_with_metadata() {
let result = ToolResult::success(None).with_metadata("duration_ms", serde_json::json!(100));
assert!(result.metadata.contains_key("duration_ms"));
}
}