mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
Implement web_search tool using Brave Search API
Replace the placeholder web_search tool with a real implementation backed by the Brave Search API. Register it in all three profiles (Anthropic, OpenAI, Gemini) so every provider has web search capability. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3253589193
commit
29b08fc6c5
6 changed files with 147 additions and 13 deletions
|
|
@ -1,4 +1,5 @@
|
|||
export ANTHROPIC_API_KEY=
|
||||
export OPENAI_API_KEY=
|
||||
export GEMINI_API_KEY=
|
||||
export DAYTONA_API_KEY=
|
||||
export DAYTONA_API_KEY=
|
||||
export BRAVE_SEARCH_API_KEY=
|
||||
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -19,6 +19,7 @@ dependencies = [
|
|||
"libc",
|
||||
"llm",
|
||||
"paste",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tar",
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ futures.workspace = true
|
|||
async-trait.workspace = true
|
||||
jsonschema.workspace = true
|
||||
chrono.workspace = true
|
||||
reqwest.workspace = true
|
||||
tokio-util.workspace = true
|
||||
dirs = "6"
|
||||
glob = "0.3"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use crate::skills::Skill;
|
|||
use crate::tool_registry::ToolRegistry;
|
||||
use crate::tools::{
|
||||
make_edit_file_tool, make_glob_tool, make_grep_tool, make_read_file_tool,
|
||||
make_shell_tool_with_config, make_write_file_tool,
|
||||
make_shell_tool_with_config, make_web_search_tool, make_write_file_tool,
|
||||
};
|
||||
|
||||
use super::EnvContext;
|
||||
|
|
@ -31,6 +31,7 @@ impl AnthropicProfile {
|
|||
registry.register(make_shell_tool_with_config(&config));
|
||||
registry.register(make_grep_tool());
|
||||
registry.register(make_glob_tool());
|
||||
registry.register(make_web_search_tool());
|
||||
|
||||
Self {
|
||||
base: BaseProfile {
|
||||
|
|
@ -131,6 +132,9 @@ Use this for searching the content of files rather than using shell grep or rg.
|
|||
Find files by name pattern. Results sorted by modification time (newest first). Use this for \
|
||||
finding files rather than using shell find or ls commands.
|
||||
|
||||
## web_search
|
||||
Search the web using Brave Search. Returns titles, URLs, and descriptions.
|
||||
|
||||
# Coding Best Practices
|
||||
|
||||
Write clean, maintainable code. Handle errors appropriately. Follow existing code conventions \
|
||||
|
|
@ -236,6 +240,10 @@ mod tests {
|
|||
prompt.contains("Write clean, maintainable code"),
|
||||
"prompt should contain coding best practices"
|
||||
);
|
||||
assert!(
|
||||
prompt.contains("web_search"),
|
||||
"prompt should contain web_search guidance"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -283,13 +291,14 @@ mod tests {
|
|||
fn anthropic_tools_registered() {
|
||||
let profile = AnthropicProfile::new("claude-sonnet-4-20250514");
|
||||
let names = profile.tool_registry().names();
|
||||
assert_eq!(names.len(), 6);
|
||||
assert_eq!(names.len(), 7);
|
||||
assert!(names.contains(&"read_file".to_string()));
|
||||
assert!(names.contains(&"write_file".to_string()));
|
||||
assert!(names.contains(&"edit_file".to_string()));
|
||||
assert!(names.contains(&"shell".to_string()));
|
||||
assert!(names.contains(&"grep".to_string()));
|
||||
assert!(names.contains(&"glob".to_string()));
|
||||
assert!(names.contains(&"web_search".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -339,7 +348,7 @@ mod tests {
|
|||
use std::sync::Arc;
|
||||
|
||||
let mut profile = AnthropicProfile::new("claude-sonnet-4-20250514");
|
||||
assert_eq!(profile.tool_registry().names().len(), 6);
|
||||
assert_eq!(profile.tool_registry().names().len(), 7);
|
||||
|
||||
let manager = Arc::new(tokio::sync::Mutex::new(SubAgentManager::new(3)));
|
||||
let factory: SessionFactory = Arc::new(|| {
|
||||
|
|
@ -349,7 +358,7 @@ mod tests {
|
|||
profile.register_subagent_tools(manager, factory, 0);
|
||||
|
||||
let names = profile.tool_registry().names();
|
||||
assert_eq!(names.len(), 10, "should have 6 base + 4 subagent tools");
|
||||
assert_eq!(names.len(), 11, "should have 7 base + 4 subagent tools");
|
||||
assert!(names.contains(&"spawn_agent".to_string()));
|
||||
assert!(names.contains(&"send_input".to_string()));
|
||||
assert!(names.contains(&"wait".to_string()));
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ use crate::skills::Skill;
|
|||
use crate::tool_registry::{RegisteredTool, ToolRegistry};
|
||||
use llm::types::ToolDefinition;
|
||||
use crate::tools::{
|
||||
make_glob_tool, make_grep_tool, make_read_file_tool, make_shell_tool, make_write_file_tool,
|
||||
make_glob_tool, make_grep_tool, make_read_file_tool, make_shell_tool, make_web_search_tool,
|
||||
make_write_file_tool,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
|
|
@ -28,6 +29,7 @@ impl OpenAiProfile {
|
|||
registry.register(make_grep_tool());
|
||||
registry.register(make_glob_tool());
|
||||
registry.register(make_apply_patch_tool());
|
||||
registry.register(make_web_search_tool());
|
||||
|
||||
Self {
|
||||
base: BaseProfile {
|
||||
|
|
@ -144,6 +146,9 @@ Search file contents with regex. Use glob_filter to narrow results.
|
|||
## glob
|
||||
Find files by name pattern.
|
||||
|
||||
## web_search
|
||||
Search the web using Brave Search. Returns titles, URLs, and descriptions.
|
||||
|
||||
# Coding Best Practices
|
||||
|
||||
Write clean, maintainable code. Handle errors appropriately. Follow existing code conventions \
|
||||
|
|
@ -532,27 +537,28 @@ mod tests {
|
|||
use crate::subagent::SessionFactory;
|
||||
|
||||
let mut profile = OpenAiProfile::new("o3-mini");
|
||||
assert_eq!(profile.tool_registry().names().len(), 6);
|
||||
assert_eq!(profile.tool_registry().names().len(), 7);
|
||||
|
||||
let manager = Arc::new(tokio::sync::Mutex::new(SubAgentManager::new(3)));
|
||||
let factory: SessionFactory = Arc::new(|| {
|
||||
panic!("should not be called in test")
|
||||
});
|
||||
profile.register_subagent_tools(manager, factory, 0);
|
||||
assert_eq!(profile.tool_registry().names().len(), 10);
|
||||
assert_eq!(profile.tool_registry().names().len(), 11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_tools_registered() {
|
||||
let profile = OpenAiProfile::new("o3-mini");
|
||||
let names = profile.tool_registry().names();
|
||||
assert_eq!(names.len(), 6);
|
||||
assert_eq!(names.len(), 7);
|
||||
assert!(names.contains(&"read_file".to_string()));
|
||||
assert!(names.contains(&"write_file".to_string()));
|
||||
assert!(names.contains(&"shell".to_string()));
|
||||
assert!(names.contains(&"grep".to_string()));
|
||||
assert!(names.contains(&"glob".to_string()));
|
||||
assert!(names.contains(&"apply_patch".to_string()));
|
||||
assert!(names.contains(&"web_search".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -344,24 +344,79 @@ pub(crate) fn make_list_dir_tool() -> RegisteredTool {
|
|||
}
|
||||
}
|
||||
|
||||
fn format_brave_results(body: &serde_json::Value) -> String {
|
||||
let results = body
|
||||
.get("web")
|
||||
.and_then(|w| w.get("results"))
|
||||
.and_then(serde_json::Value::as_array);
|
||||
|
||||
let Some(results) = results else {
|
||||
return "No results found.".to_string();
|
||||
};
|
||||
|
||||
let mut output = String::new();
|
||||
for (i, result) in results.iter().enumerate() {
|
||||
let title = result.get("title").and_then(serde_json::Value::as_str).unwrap_or("(no title)");
|
||||
let url = result.get("url").and_then(serde_json::Value::as_str).unwrap_or("(no url)");
|
||||
let description = result.get("description").and_then(serde_json::Value::as_str).unwrap_or("");
|
||||
let _ = write!(output, "{}. {}\n {}\n {}\n\n", i + 1, title, url, description);
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn make_web_search_tool() -> RegisteredTool {
|
||||
make_web_search_tool_with_api_key(std::env::var("BRAVE_SEARCH_API_KEY").ok())
|
||||
}
|
||||
|
||||
fn make_web_search_tool_with_api_key(api_key: Option<String>) -> RegisteredTool {
|
||||
let client = reqwest::Client::new();
|
||||
RegisteredTool {
|
||||
definition: ToolDefinition {
|
||||
name: "web_search".into(),
|
||||
description: "Search the web".into(),
|
||||
description: "Search the web using Brave Search".into(),
|
||||
parameters: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "Search query"},
|
||||
"max_results": {"type": "integer", "description": "Maximum number of results"}
|
||||
"max_results": {"type": "integer", "description": "Maximum number of results (default 5, max 20)"}
|
||||
},
|
||||
"required": ["query"]
|
||||
}),
|
||||
},
|
||||
executor: Arc::new(|_args, _env, _cancel| {
|
||||
executor: Arc::new(move |args, _env, _cancel| {
|
||||
let client = client.clone();
|
||||
let api_key = api_key.clone();
|
||||
Box::pin(async move {
|
||||
Ok("Web search is not configured. This is a placeholder tool.".to_string())
|
||||
let api_key = api_key
|
||||
.ok_or_else(|| "BRAVE_SEARCH_API_KEY environment variable is not set".to_string())?;
|
||||
|
||||
let query = required_str(&args, "query")?;
|
||||
let count = args
|
||||
.get("max_results")
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.unwrap_or(5)
|
||||
.min(20);
|
||||
|
||||
let resp = client
|
||||
.get("https://api.search.brave.com/res/v1/web/search")
|
||||
.header("X-Subscription-Token", &api_key)
|
||||
.header("Accept", "application/json")
|
||||
.query(&[("q", query), ("count", &count.to_string())])
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("HTTP request failed: {e}"))?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("Brave Search API returned status {}", resp.status()));
|
||||
}
|
||||
|
||||
let body: serde_json::Value = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to parse response: {e}"))?;
|
||||
|
||||
Ok(format_brave_results(&body))
|
||||
})
|
||||
}),
|
||||
}
|
||||
|
|
@ -645,4 +700,65 @@ mod tests {
|
|||
assert!(output.contains("src/main.rs"));
|
||||
assert!(output.contains("src/lib.rs"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn web_search_missing_api_key_returns_error() {
|
||||
let tool = make_web_search_tool_with_api_key(None);
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment::default());
|
||||
let result = (tool.executor)(serde_json::json!({"query": "test"}), env, CancellationToken::new()).await;
|
||||
let err = result.unwrap_err();
|
||||
assert!(err.contains("BRAVE_SEARCH_API_KEY"), "error should mention BRAVE_SEARCH_API_KEY, got: {err}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn web_search_missing_query_returns_error() {
|
||||
let tool = make_web_search_tool_with_api_key(Some("fake-key".into()));
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment::default());
|
||||
let result = (tool.executor)(serde_json::json!({}), env, CancellationToken::new()).await;
|
||||
let err = result.unwrap_err();
|
||||
assert!(err.contains("query"), "error should mention missing query, got: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_brave_results_formats_results() {
|
||||
let body = serde_json::json!({
|
||||
"web": {
|
||||
"results": [
|
||||
{"title": "Rust Lang", "url": "https://rust-lang.org", "description": "A systems language"},
|
||||
{"title": "Rust Book", "url": "https://doc.rust-lang.org/book", "description": "The Rust book"}
|
||||
]
|
||||
}
|
||||
});
|
||||
let output = format_brave_results(&body);
|
||||
assert!(output.contains("1. Rust Lang"));
|
||||
assert!(output.contains("https://rust-lang.org"));
|
||||
assert!(output.contains("A systems language"));
|
||||
assert!(output.contains("2. Rust Book"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_brave_results_no_results() {
|
||||
let body = serde_json::json!({"web": {}});
|
||||
assert_eq!(format_brave_results(&body), "No results found.");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Requires BRAVE_SEARCH_API_KEY env var
|
||||
async fn web_search_returns_results() {
|
||||
let api_key = std::env::var("BRAVE_SEARCH_API_KEY")
|
||||
.expect("BRAVE_SEARCH_API_KEY must be set to run this test");
|
||||
let tool = make_web_search_tool_with_api_key(Some(api_key));
|
||||
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment::default());
|
||||
let result = (tool.executor)(
|
||||
serde_json::json!({"query": "rust programming language"}),
|
||||
env,
|
||||
CancellationToken::new(),
|
||||
)
|
||||
.await;
|
||||
let output = result.expect("web search should succeed with valid API key");
|
||||
assert!(
|
||||
output.to_lowercase().contains("rust"),
|
||||
"results should mention rust, got: {output}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue