use crate::tool::{Tool, ToolBox, ToolError, ToolResult, toolbox};
use reqwest::Client;
use serde_json::Value;
const BRAVE_API_URL: &str = "https://api.search.brave.com/res/v1/web/search";
pub struct WebSearchToolBox {
client: Client,
api_key: String,
}
#[toolbox]
impl WebSearchToolBox {
pub fn new(api_key: &str) -> Self {
Self {
client: Client::default(),
api_key: api_key.to_string(),
}
}
#[tool]
pub async fn web_search(
&self,
#[doc = "The search terms or keywords to be used by the search engine for retrieving relevant results."]
query: String,
) -> ToolResult {
let params = [
("q", query.as_str()),
("count", "5"),
("result_filter", "web"),
];
if self.api_key.trim().is_empty() {
return Err(ToolError::Configuration(
"the Brave Search API key is empty".to_string(),
));
}
let response = self
.client
.get(BRAVE_API_URL)
.query(¶ms)
.header("X-Subscription-Token", self.api_key.clone())
.send()
.await
.map_err(|e| ToolError::Failed(format!("Web search request failed: {e}")))?;
let status = response.status();
if matches!(status.as_u16(), 401 | 403) {
return Err(ToolError::Configuration(format!(
"the Brave Search API rejected the API key (status {status})"
)));
}
if !status.is_success() {
return Err(ToolError::Failed(format!(
"Web search failed with status: {status}"
)));
}
let json: Value = response
.json()
.await
.map_err(|e| ToolError::Failed(format!("Invalid web search response: {e}")))?;
let mut results: Vec<String> = vec![];
let Some(response) = json["web"]["results"].as_array() else {
return Ok("No results found".to_string());
};
for item in response {
let field = |name: &str| {
item[name].as_str().ok_or_else(|| {
ToolError::Failed(format!("Web search result has no string field '{name}'"))
})
};
let title = field("title")?;
let description = field("description")?;
let url = field("url")?;
results.push(format!(
"Title: {title}\nDescription: {description}\nURL: {url}"
));
}
Ok(results.join("\n\n"))
}
}
pub struct WebFetchToolBox {
client: Client,
}
impl Default for WebFetchToolBox {
fn default() -> Self {
Self::new()
}
}
#[toolbox]
impl WebFetchToolBox {
pub fn new() -> Self {
Self {
client: Client::default(),
}
}
#[allow(rustdoc::bare_urls)]
#[tool]
pub async fn web_fetch(
&self,
#[doc = "The full URL of the web page to fetch, including the protocol (e.g., https://)."]
url: String,
) -> ToolResult {
let response = self
.client
.get(&url)
.send()
.await
.map_err(|e| ToolError::Failed(format!("Request to {url} failed: {e}")))?;
if !response.status().is_success() {
return Err(ToolError::Failed(format!(
"Request to {} failed with status: {}",
url,
response.status()
)));
}
let body = response
.text()
.await
.map_err(|e| ToolError::Failed(format!("Reading the response of {url} failed: {e}")))?;
Ok(body)
}
}