Files
gyandoot-v2/crates/gyandoot-agent/src/browser_tools.rs
Tara Menon 8c9a32075d Slash commands (/quit, /help) + zero compiler warnings
1. /quit and /help commands restored:
   - /quit: exits the interactive loop
   - /help: shows formatted help box with available commands & tips
   - Unknown commands get a friendly error pointing to /help
   - Slash commands intercepted before LLM processing (no token waste)

2. Zero warnings across entire workspace (13 files fixed):
   - Fixed ambiguous glob re-exports in gyandoot-core
   - Added #[allow(dead_code)] on intentional dead fields
   - Fixed unused imports, variables, and macros
   - Fixed unused assignments in subagents
   - cleaned up service.rs, profile, kanban warnings

install.sh now produces a completely clean build with zero warnings.
2026-06-12 12:15:01 +05:30

309 lines
11 KiB
Rust

//! Browser tools for the agent — enables web browsing with anti-detection.
//!
//! These tools are registered with the `ToolExecutor` and provide
//! LLM-accessible functions for searching the web, browsing pages,
//! clicking elements, and extracting content.
//!
//! Uses the camoufox-browser daemon protocol (Unix socket IPC).
use serde_json;
use tracing::info;
use gyandoot_browser::BrowserClient;
use crate::tools::Tool;
/// Search macro name strings sent to the camoufox daemon.
const MACRO_GOOGLE: &str = "@google_search";
const MACRO_YOUTUBE: &str = "@youtube_search";
const MACRO_AMAZON: &str = "@amazon_search";
const MACRO_REDDIT: &str = "@reddit_search";
const MACRO_WIKIPEDIA: &str = "@wikipedia_search";
const MACRO_TWITTER: &str = "@twitter_search";
const MACRO_YELP: &str = "@yelp_search";
const MACRO_SPOTIFY: &str = "@spotify_search";
const MACRO_LINKEDIN: &str = "@linkedin_search";
const MACRO_INSTAGRAM: &str = "@instagram_search";
const MACRO_TIKTOK: &str = "@tiktok_search";
const MACRO_TWITCH: &str = "@twitch_search";
fn site_to_macro(site: &str) -> &'static str {
match site {
"google" => MACRO_GOOGLE,
"youtube" => MACRO_YOUTUBE,
"amazon" => MACRO_AMAZON,
"reddit" => MACRO_REDDIT,
"wikipedia" => MACRO_WIKIPEDIA,
"twitter" | "x" => MACRO_TWITTER,
"yelp" => MACRO_YELP,
"spotify" => MACRO_SPOTIFY,
"linkedin" => MACRO_LINKEDIN,
"instagram" => MACRO_INSTAGRAM,
"tiktok" => MACRO_TIKTOK,
"twitch" => MACRO_TWITCH,
_ => MACRO_GOOGLE,
}
}
fn site_name(site: &str) -> &'static str {
match site {
"google" => "Google",
"youtube" => "YouTube",
"amazon" => "Amazon",
"reddit" => "Reddit",
"wikipedia" => "Wikipedia",
"twitter" | "x" => "Twitter/X",
"yelp" => "Yelp",
"spotify" => "Spotify",
"linkedin" => "LinkedIn",
"instagram" => "Instagram",
"tiktok" => "TikTok",
"twitch" => "Twitch",
_ => "Google",
}
}
/// Tool for searching the web using camoufox daemon macros.
pub struct WebSearchTool {
client: BrowserClient,
}
impl WebSearchTool {
pub fn new(client: BrowserClient) -> Self {
Self { client }
}
}
#[async_trait::async_trait]
impl Tool for WebSearchTool {
fn name(&self) -> &str {
"web_search"
}
fn description(&self) -> &str {
"Search the web using Google, YouTube, Amazon, Reddit, Wikipedia, and more. \
Uses anti-detection browser to bypass bot protection. \
Returns an accessibility snapshot of search results."
}
fn parameters(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query"
},
"site": {
"type": "string",
"description": "Site to search. Options: google, youtube, amazon, reddit, wikipedia, twitter, yelp, spotify, linkedin, instagram, tiktok, twitch",
"default": "google"
}
},
"required": ["query"]
})
}
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<String> {
let query = args["query"].as_str().unwrap_or("").to_string();
let site = args["site"].as_str().unwrap_or("google").to_lowercase();
if query.is_empty() {
return Err(anyhow::anyhow!("query is required"));
}
let mname = site_to_macro(&site);
let sname = site_name(&site);
info!("web_search: searching {} for '{}'", sname, query);
// Execute search — daemon auto-launches browser on first call
let result = self.client.search(mname, &query).await
.map_err(|e| anyhow::anyhow!("Search failed: {}", e))?;
// Get snapshot of results
let snap = self.client.snapshot(true).await
.map_err(|e| anyhow::anyhow!("Failed to get snapshot: {}", e))?;
let refs = snap.extract_refs();
let response = format!(
"## {} Search Results\n\n**Query:** {}\n**Page:** {}\n**Title:** {}\n\n**Snapshot content:**\n{}\n\n**Available element refs:** {:?}\n\n*Use `web_browse` to interact with specific results.*",
sname,
query,
result.url,
result.title,
snap.snapshot,
refs,
);
Ok(response)
}
}
/// Tool for browsing a specific URL using camoufox daemon browser.
pub struct WebBrowseTool {
client: BrowserClient,
}
impl WebBrowseTool {
pub fn new(client: BrowserClient) -> Self {
Self { client }
}
}
#[async_trait::async_trait]
impl Tool for WebBrowseTool {
fn name(&self) -> &str {
"web_browse"
}
fn description(&self) -> &str {
"Browse a specific URL using an anti-detection browser. \
Returns an accessibility snapshot with element refs for interaction. \
Supports navigation, clicking, typing, and scrolling."
}
fn parameters(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "The URL to browse"
},
"action": {
"type": "string",
"description": "Action to perform: 'navigate' (default), 'click', 'type', 'scroll', 'back', 'forward', 'refresh'",
"default": "navigate"
},
"element_ref": {
"type": "string",
"description": "Element reference (e.g., 'e1', 'e2') — required for click/type actions"
},
"text": {
"type": "string",
"description": "Text to type — required for type action"
},
"press_enter": {
"type": "boolean",
"description": "Press Enter after typing",
"default": false
},
"direction": {
"type": "string",
"description": "Scroll direction: 'up' or 'down'",
"default": "down"
},
"amount": {
"type": "integer",
"description": "Scroll amount in pixels",
"default": 500
}
},
"required": ["url"]
})
}
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<String> {
let url = args["url"].as_str().unwrap_or("").to_string();
let action = args["action"].as_str().unwrap_or("navigate").to_lowercase();
let element_ref = args["element_ref"].as_str().map(|s| s.to_string());
let text = args["text"].as_str().map(|s| s.to_string());
let press_enter = args["press_enter"].as_bool().unwrap_or(false);
let direction = args["direction"].as_str().unwrap_or("down").to_string();
let amount = args["amount"].as_u64().unwrap_or(500) as u32;
if url.is_empty() {
return Err(anyhow::anyhow!("url is required"));
}
match action.as_str() {
"navigate" => {
self.client.navigate(&url).await
.map_err(|e| anyhow::anyhow!("Navigation failed: {}", e))?;
}
"click" => {
let r#ref = element_ref.as_ref()
.ok_or_else(|| anyhow::anyhow!("element_ref is required for click action"))?;
self.client.click(r#ref).await
.map_err(|e| anyhow::anyhow!("Click failed: {}", e))?;
}
"type" => {
let r#ref = element_ref.as_ref()
.ok_or_else(|| anyhow::anyhow!("element_ref is required for type action"))?;
let t = text.as_ref()
.ok_or_else(|| anyhow::anyhow!("text is required for type action"))?;
self.client.type_text(r#ref, t, press_enter).await
.map_err(|e| anyhow::anyhow!("Type failed: {}", e))?;
}
"scroll" => {
self.client.scroll(&direction, amount).await
.map_err(|e| anyhow::anyhow!("Scroll failed: {}", e))?;
}
"back" => {
self.client.go_back().await
.map_err(|e| anyhow::anyhow!("Go back failed: {}", e))?;
}
"forward" => {
self.client.go_forward().await
.map_err(|e| anyhow::anyhow!("Go forward failed: {}", e))?;
}
"refresh" => {
self.client.refresh().await
.map_err(|e| anyhow::anyhow!("Refresh failed: {}", e))?;
}
_ => {}
}
let snap = self.client.snapshot(false).await
.map_err(|e| anyhow::anyhow!("Failed to get snapshot: {}", e))?;
let refs = snap.extract_refs();
let current_url = if snap.url.is_empty() { &url } else { &snap.url };
let response = format!(
"## Page Snapshot\n\n**URL:** {}\n\n**Snapshot:**\n{}\n\n**Element refs:** {:?}\n\n*Use `web_browse` with action=\"click\" and element_ref to interact with elements.*",
current_url,
snap.snapshot,
refs,
);
Ok(response)
}
}
/// Register all browser tools with the given executor.
pub fn register_browser_tools(executor: &mut crate::tools::ToolExecutor, client: BrowserClient) {
executor.register(Box::new(WebSearchTool::new(client.clone())));
executor.register(Box::new(WebBrowseTool::new(client)));
info!("Registered browser tools: web_search, web_browse");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_web_search_tool_definition() {
let client = BrowserClient::new();
let tool = WebSearchTool::new(client);
assert_eq!(tool.name(), "web_search");
let params = tool.parameters();
assert!(params["required"].as_array().unwrap().contains(&"query".to_string().into()));
}
#[test]
fn test_web_browse_tool_definition() {
let client = BrowserClient::new();
let tool = WebBrowseTool::new(client);
assert_eq!(tool.name(), "web_browse");
let params = tool.parameters();
assert!(params["required"].as_array().unwrap().contains(&"url".to_string().into()));
}
#[test]
fn test_site_to_macro() {
assert_eq!(site_to_macro("google"), "@google_search");
assert_eq!(site_to_macro("youtube"), "@youtube_search");
assert_eq!(site_to_macro("reddit"), "@reddit_search");
}
}