Skip to content

Commit 2e0816c

Browse files
authored
feat(#589): wire WebToolsConfig to web search/fetch tools (#663)
* docs(config): update WebToolsConfig provider names - Update doc comments to match actual implementation - Document env var fallback behavior - Change provider names from brave/searxng/google to exa/kimi_search Part of: #589 * feat(tools): add from_config() to WebSearchTool - Add from_config() method that accepts Option<&WebToolsConfig> - Support provider selection via config.search_provider - Fall back to environment variables when config not provided - Extract from_env_inner() helper for code reuse - Maintain backward compatibility with existing new() and from_env() Part of: #589 * feat(tools): add from_config() to WebFetchTool - Add from_config() method that accepts Option<&WebToolsConfig> - Support fetch mode selection via config.fetch_mode - Defaults to raw when config not provided or mode not specified - Maintain backward compatibility with existing new() and with_mode() Part of: #589 * feat(tools): wire config through to web tools - Update create_default_registry signature to accept Option<&WebToolsConfig> - Use from_config() instead of new() for web tools in registry - Update main.rs to pass config.tools.web to registry creation - Both agent mode and gateway mode now respect web_tools config Part of: #589 * test(tools): add unit tests for from_config methods - Add provider_name() test helper to WebSearchTool - Add tests for WebSearchTool::from_config(): - test_web_search_from_config_exa - test_web_search_from_config_kimi - test_web_search_from_config_fallback - test_web_search_from_config_unknown_provider - Add tests for WebFetchTool::from_config(): - test_web_fetch_from_config_raw - test_web_fetch_from_config_readability - test_web_fetch_from_config_fallback - test_web_fetch_from_config_none_mode Part of: #589 * test(tools): add integration tests for config wiring - Add test_web_tools_config_wired_to_registry - Add test_registry_without_web_tools_config - Add test_all_expected_tools_registered - Verify config values flow through to tool registry Part of: #589
1 parent eb6c535 commit 2e0816c

File tree

5 files changed

+251
-6
lines changed

5 files changed

+251
-6
lines changed

crates/terraphim_tinyclaw/src/config.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -414,10 +414,15 @@ fn default_shell_timeout() -> u64 {
414414
/// Web tools configuration.
415415
#[derive(Debug, Clone, Deserialize, Serialize)]
416416
pub struct WebToolsConfig {
417-
/// Web search provider ("brave", "searxng", "google").
417+
/// Web search provider ("exa", "kimi_search").
418+
///
419+
/// If not specified, falls back to environment variables
420+
/// (EXA_API_KEY or KIMI_API_KEY).
418421
pub search_provider: Option<String>,
419422

420-
/// Web fetch mode ("readability", "raw").
423+
/// Web fetch mode ("raw", "readability").
424+
///
425+
/// Defaults to "raw" if not specified.
421426
pub fetch_mode: Option<String>,
422427
}
423428

crates/terraphim_tinyclaw/src/main.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,11 @@ async fn run_agent_mode(config: Config, system_prompt_path: Option<PathBuf>) ->
169169
let sessions = Arc::new(tokio::sync::Mutex::new(SessionManager::new(sessions_dir)));
170170

171171
// Create tool registry with session manager
172-
let tools = Arc::new(create_default_registry(Some(sessions.clone())));
172+
let web_tools_config = config.tools.web.as_ref();
173+
let tools = Arc::new(create_default_registry(
174+
Some(sessions.clone()),
175+
web_tools_config,
176+
));
173177

174178
// Create hybrid LLM router
175179
let proxy_config = ProxyClientConfig {
@@ -222,7 +226,11 @@ async fn run_gateway_mode(config: Config) -> anyhow::Result<()> {
222226
let sessions = Arc::new(tokio::sync::Mutex::new(SessionManager::new(sessions_dir)));
223227

224228
// Create tool registry with session manager
225-
let tools = Arc::new(create_default_registry(Some(sessions.clone())));
229+
let web_tools_config = config.tools.web.as_ref();
230+
let tools = Arc::new(create_default_registry(
231+
Some(sessions.clone()),
232+
web_tools_config,
233+
));
226234

227235
// Create hybrid LLM router
228236
let proxy_config = ProxyClientConfig {

crates/terraphim_tinyclaw/src/tools/mod.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,13 @@ impl Default for ToolRegistry {
149149
}
150150

151151
/// Create a standard tool registry with all default tools.
152+
///
153+
/// # Arguments
154+
/// * `sessions` - Optional session manager for session-aware tools
155+
/// * `web_tools_config` - Optional web tools configuration
152156
pub fn create_default_registry(
153157
sessions: Option<std::sync::Arc<tokio::sync::Mutex<crate::session::SessionManager>>>,
158+
web_tools_config: Option<&crate::config::WebToolsConfig>,
154159
) -> ToolRegistry {
155160
use crate::tools::edit::EditTool;
156161
use crate::tools::filesystem::FilesystemTool;
@@ -163,8 +168,8 @@ pub fn create_default_registry(
163168
registry.register(Box::new(FilesystemTool::new()));
164169
registry.register(Box::new(EditTool::new()));
165170
registry.register(Box::new(ShellTool::new()));
166-
registry.register(Box::new(WebSearchTool::new()));
167-
registry.register(Box::new(WebFetchTool::new()));
171+
registry.register(Box::new(WebSearchTool::from_config(web_tools_config)));
172+
registry.register(Box::new(WebFetchTool::from_config(web_tools_config)));
168173
registry.register(Box::new(VoiceTranscribeTool::new()));
169174

170175
// Register session tools if SessionManager is provided

crates/terraphim_tinyclaw/src/tools/web.rs

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,8 +246,47 @@ impl WebSearchTool {
246246
Self { provider }
247247
}
248248

249+
/// Get the provider name.
250+
#[cfg(test)]
251+
fn provider_name(&self) -> &str {
252+
self.provider.name()
253+
}
254+
249255
/// Create from environment variables.
250256
pub fn from_env() -> Self {
257+
Self::from_env_inner()
258+
}
259+
260+
/// Create from configuration.
261+
///
262+
/// If config specifies a search provider, uses it.
263+
/// Otherwise falls back to environment variables (same as `new()`).
264+
///
265+
/// # Arguments
266+
/// * `config` - Optional web tools configuration
267+
///
268+
/// # Supported Providers
269+
/// - "exa" - Exa search API
270+
/// - "kimi_search" - Kimi search API
271+
pub fn from_config(config: Option<&crate::config::WebToolsConfig>) -> Self {
272+
match config {
273+
Some(cfg) => match cfg.search_provider.as_deref() {
274+
Some("exa") => {
275+
let api_key = std::env::var("EXA_API_KEY").ok().filter(|k| !k.is_empty());
276+
Self::with_provider(Box::new(ExaProvider::new(api_key)))
277+
}
278+
Some("kimi_search") => {
279+
let api_key = std::env::var("KIMI_API_KEY").ok().filter(|k| !k.is_empty());
280+
Self::with_provider(Box::new(KimiSearchProvider::new(api_key)))
281+
}
282+
Some(_) | None => Self::from_env_inner(),
283+
},
284+
None => Self::from_env_inner(),
285+
}
286+
}
287+
288+
/// Internal helper to create from environment variables.
289+
fn from_env_inner() -> Self {
251290
// Check for Exa API key
252291
if let Ok(api_key) = std::env::var("EXA_API_KEY") {
253292
if !api_key.is_empty() {
@@ -345,6 +384,28 @@ impl WebFetchTool {
345384
}
346385
}
347386

387+
/// Create from configuration.
388+
///
389+
/// If config specifies a fetch mode, uses it.
390+
/// Otherwise defaults to "raw".
391+
///
392+
/// # Arguments
393+
/// * `config` - Optional web tools configuration
394+
///
395+
/// # Supported Modes
396+
/// - "raw" - Fetch raw HTML
397+
/// - "readability" - Extract readable content
398+
pub fn from_config(config: Option<&crate::config::WebToolsConfig>) -> Self {
399+
let mode = config
400+
.and_then(|c| c.fetch_mode.clone())
401+
.unwrap_or_else(|| "raw".to_string());
402+
403+
Self {
404+
client: Client::new(),
405+
mode,
406+
}
407+
}
408+
348409
/// Fetch content from a URL.
349410
async fn fetch(&self, url: &str) -> Result<String, ToolError> {
350411
log::info!("Fetching URL: {} (mode: {})", url, self.mode);
@@ -492,4 +553,92 @@ mod tests {
492553
let provider = PlaceholderProvider;
493554
assert_eq!(provider.name(), "placeholder");
494555
}
556+
557+
#[test]
558+
fn test_web_search_from_config_exa() {
559+
let config = crate::config::WebToolsConfig {
560+
search_provider: Some("exa".to_string()),
561+
fetch_mode: None,
562+
};
563+
564+
let tool = WebSearchTool::from_config(Some(&config));
565+
// The provider name should reflect the configured provider
566+
assert_eq!(tool.provider_name(), "exa");
567+
}
568+
569+
#[test]
570+
fn test_web_search_from_config_kimi() {
571+
let config = crate::config::WebToolsConfig {
572+
search_provider: Some("kimi_search".to_string()),
573+
fetch_mode: None,
574+
};
575+
576+
let tool = WebSearchTool::from_config(Some(&config));
577+
assert_eq!(tool.provider_name(), "kimi_search");
578+
}
579+
580+
#[test]
581+
fn test_web_search_from_config_fallback() {
582+
// When config is None, should fall back to env-based selection
583+
// or placeholder if no env vars are set
584+
let tool = WebSearchTool::from_config(None);
585+
// Provider name should be one of the valid options
586+
let name = tool.provider_name();
587+
assert!(name == "exa" || name == "kimi_search" || name == "placeholder");
588+
}
589+
590+
#[test]
591+
fn test_web_search_from_config_unknown_provider() {
592+
// Unknown provider should fall back to env-based selection
593+
let config = crate::config::WebToolsConfig {
594+
search_provider: Some("unknown_provider".to_string()),
595+
fetch_mode: None,
596+
};
597+
598+
let tool = WebSearchTool::from_config(Some(&config));
599+
// Should fall back to env-based or placeholder
600+
let name = tool.provider_name();
601+
assert!(name == "exa" || name == "kimi_search" || name == "placeholder");
602+
}
603+
604+
#[test]
605+
fn test_web_fetch_from_config_raw() {
606+
let config = crate::config::WebToolsConfig {
607+
search_provider: None,
608+
fetch_mode: Some("raw".to_string()),
609+
};
610+
611+
let tool = WebFetchTool::from_config(Some(&config));
612+
assert_eq!(tool.mode, "raw");
613+
}
614+
615+
#[test]
616+
fn test_web_fetch_from_config_readability() {
617+
let config = crate::config::WebToolsConfig {
618+
search_provider: None,
619+
fetch_mode: Some("readability".to_string()),
620+
};
621+
622+
let tool = WebFetchTool::from_config(Some(&config));
623+
assert_eq!(tool.mode, "readability");
624+
}
625+
626+
#[test]
627+
fn test_web_fetch_from_config_fallback() {
628+
// When config is None, should default to "raw"
629+
let tool = WebFetchTool::from_config(None);
630+
assert_eq!(tool.mode, "raw");
631+
}
632+
633+
#[test]
634+
fn test_web_fetch_from_config_none_mode() {
635+
// When config has None for fetch_mode, should default to "raw"
636+
let config = crate::config::WebToolsConfig {
637+
search_provider: Some("exa".to_string()),
638+
fetch_mode: None,
639+
};
640+
641+
let tool = WebFetchTool::from_config(Some(&config));
642+
assert_eq!(tool.mode, "raw");
643+
}
495644
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//! Integration tests for config wiring to tools.
2+
//!
3+
//! Tests that configuration values from files are properly passed to tools.
4+
5+
use terraphim_tinyclaw::config::{Config, ToolsConfig, WebToolsConfig};
6+
use terraphim_tinyclaw::tools::create_default_registry;
7+
8+
/// Test that web tools configuration is wired through to the registry.
9+
#[test]
10+
fn test_web_tools_config_wired_to_registry() {
11+
// Create a config with specific web tools settings
12+
let config = Config {
13+
tools: ToolsConfig {
14+
web: Some(WebToolsConfig {
15+
search_provider: Some("exa".to_string()),
16+
fetch_mode: Some("readability".to_string()),
17+
}),
18+
..Default::default()
19+
},
20+
..Default::default()
21+
};
22+
23+
// Create registry with the web tools config
24+
let web_tools_config = config.tools.web.as_ref();
25+
let registry = create_default_registry(None, web_tools_config);
26+
27+
// Verify web_search tool is present
28+
let web_search = registry.get("web_search");
29+
assert!(web_search.is_some(), "web_search tool should be registered");
30+
31+
// Verify web_fetch tool is present
32+
let web_fetch = registry.get("web_fetch");
33+
assert!(web_fetch.is_some(), "web_fetch tool should be registered");
34+
}
35+
36+
/// Test that registry works with no web tools config.
37+
#[test]
38+
fn test_registry_without_web_tools_config() {
39+
// Create registry without web tools config
40+
let registry = create_default_registry(None, None);
41+
42+
// Verify web_search tool is still present (with defaults)
43+
let web_search = registry.get("web_search");
44+
assert!(
45+
web_search.is_some(),
46+
"web_search tool should be registered even without config"
47+
);
48+
49+
// Verify web_fetch tool is still present (with defaults)
50+
let web_fetch = registry.get("web_fetch");
51+
assert!(
52+
web_fetch.is_some(),
53+
"web_fetch tool should be registered even without config"
54+
);
55+
}
56+
57+
/// Test that all expected tools are registered.
58+
#[test]
59+
fn test_all_expected_tools_registered() {
60+
let registry = create_default_registry(None, None);
61+
62+
let expected_tools = [
63+
"filesystem",
64+
"edit",
65+
"shell",
66+
"web_search",
67+
"web_fetch",
68+
"voice_transcribe",
69+
];
70+
71+
for tool_name in &expected_tools {
72+
assert!(
73+
registry.get(tool_name).is_some(),
74+
"Tool '{}' should be registered",
75+
tool_name
76+
);
77+
}
78+
}

0 commit comments

Comments
 (0)