Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions crates/terraphim_tinyclaw/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -414,10 +414,15 @@ fn default_shell_timeout() -> u64 {
/// Web tools configuration.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct WebToolsConfig {
/// Web search provider ("brave", "searxng", "google").
/// Web search provider ("exa", "kimi_search").
///
/// If not specified, falls back to environment variables
/// (EXA_API_KEY or KIMI_API_KEY).
pub search_provider: Option<String>,

/// Web fetch mode ("readability", "raw").
/// Web fetch mode ("raw", "readability").
///
/// Defaults to "raw" if not specified.
pub fetch_mode: Option<String>,
}

Expand Down
12 changes: 10 additions & 2 deletions crates/terraphim_tinyclaw/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,11 @@ async fn run_agent_mode(config: Config, system_prompt_path: Option<PathBuf>) ->
let sessions = Arc::new(tokio::sync::Mutex::new(SessionManager::new(sessions_dir)));

// Create tool registry with session manager
let tools = Arc::new(create_default_registry(Some(sessions.clone())));
let web_tools_config = config.tools.web.as_ref();
let tools = Arc::new(create_default_registry(
Some(sessions.clone()),
web_tools_config,
));

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

// Create tool registry with session manager
let tools = Arc::new(create_default_registry(Some(sessions.clone())));
let web_tools_config = config.tools.web.as_ref();
let tools = Arc::new(create_default_registry(
Some(sessions.clone()),
web_tools_config,
));

// Create hybrid LLM router
let proxy_config = ProxyClientConfig {
Expand Down
9 changes: 7 additions & 2 deletions crates/terraphim_tinyclaw/src/tools/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,13 @@ impl Default for ToolRegistry {
}

/// Create a standard tool registry with all default tools.
///
/// # Arguments
/// * `sessions` - Optional session manager for session-aware tools
/// * `web_tools_config` - Optional web tools configuration
pub fn create_default_registry(
sessions: Option<std::sync::Arc<tokio::sync::Mutex<crate::session::SessionManager>>>,
web_tools_config: Option<&crate::config::WebToolsConfig>,
) -> ToolRegistry {
use crate::tools::edit::EditTool;
use crate::tools::filesystem::FilesystemTool;
Expand All @@ -163,8 +168,8 @@ pub fn create_default_registry(
registry.register(Box::new(FilesystemTool::new()));
registry.register(Box::new(EditTool::new()));
registry.register(Box::new(ShellTool::new()));
registry.register(Box::new(WebSearchTool::new()));
registry.register(Box::new(WebFetchTool::new()));
registry.register(Box::new(WebSearchTool::from_config(web_tools_config)));
registry.register(Box::new(WebFetchTool::from_config(web_tools_config)));
registry.register(Box::new(VoiceTranscribeTool::new()));

// Register session tools if SessionManager is provided
Expand Down
149 changes: 149 additions & 0 deletions crates/terraphim_tinyclaw/src/tools/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,47 @@ impl WebSearchTool {
Self { provider }
}

/// Get the provider name.
#[cfg(test)]
fn provider_name(&self) -> &str {
self.provider.name()
}

/// Create from environment variables.
pub fn from_env() -> Self {
Self::from_env_inner()
}

/// Create from configuration.
///
/// If config specifies a search provider, uses it.
/// Otherwise falls back to environment variables (same as `new()`).
///
/// # Arguments
/// * `config` - Optional web tools configuration
///
/// # Supported Providers
/// - "exa" - Exa search API
/// - "kimi_search" - Kimi search API
pub fn from_config(config: Option<&crate::config::WebToolsConfig>) -> Self {
match config {
Some(cfg) => match cfg.search_provider.as_deref() {
Some("exa") => {
let api_key = std::env::var("EXA_API_KEY").ok().filter(|k| !k.is_empty());
Self::with_provider(Box::new(ExaProvider::new(api_key)))
}
Some("kimi_search") => {
let api_key = std::env::var("KIMI_API_KEY").ok().filter(|k| !k.is_empty());
Self::with_provider(Box::new(KimiSearchProvider::new(api_key)))
}
Some(_) | None => Self::from_env_inner(),
},
None => Self::from_env_inner(),
}
}

/// Internal helper to create from environment variables.
fn from_env_inner() -> Self {
// Check for Exa API key
if let Ok(api_key) = std::env::var("EXA_API_KEY") {
if !api_key.is_empty() {
Expand Down Expand Up @@ -345,6 +384,28 @@ impl WebFetchTool {
}
}

/// Create from configuration.
///
/// If config specifies a fetch mode, uses it.
/// Otherwise defaults to "raw".
///
/// # Arguments
/// * `config` - Optional web tools configuration
///
/// # Supported Modes
/// - "raw" - Fetch raw HTML
/// - "readability" - Extract readable content
pub fn from_config(config: Option<&crate::config::WebToolsConfig>) -> Self {
let mode = config
.and_then(|c| c.fetch_mode.clone())
.unwrap_or_else(|| "raw".to_string());

Self {
client: Client::new(),
mode,
}
}

/// Fetch content from a URL.
async fn fetch(&self, url: &str) -> Result<String, ToolError> {
log::info!("Fetching URL: {} (mode: {})", url, self.mode);
Expand Down Expand Up @@ -492,4 +553,92 @@ mod tests {
let provider = PlaceholderProvider;
assert_eq!(provider.name(), "placeholder");
}

#[test]
fn test_web_search_from_config_exa() {
let config = crate::config::WebToolsConfig {
search_provider: Some("exa".to_string()),
fetch_mode: None,
};

let tool = WebSearchTool::from_config(Some(&config));
// The provider name should reflect the configured provider
assert_eq!(tool.provider_name(), "exa");
}

#[test]
fn test_web_search_from_config_kimi() {
let config = crate::config::WebToolsConfig {
search_provider: Some("kimi_search".to_string()),
fetch_mode: None,
};

let tool = WebSearchTool::from_config(Some(&config));
assert_eq!(tool.provider_name(), "kimi_search");
}

#[test]
fn test_web_search_from_config_fallback() {
// When config is None, should fall back to env-based selection
// or placeholder if no env vars are set
let tool = WebSearchTool::from_config(None);
// Provider name should be one of the valid options
let name = tool.provider_name();
assert!(name == "exa" || name == "kimi_search" || name == "placeholder");
}

#[test]
fn test_web_search_from_config_unknown_provider() {
// Unknown provider should fall back to env-based selection
let config = crate::config::WebToolsConfig {
search_provider: Some("unknown_provider".to_string()),
fetch_mode: None,
};

let tool = WebSearchTool::from_config(Some(&config));
// Should fall back to env-based or placeholder
let name = tool.provider_name();
assert!(name == "exa" || name == "kimi_search" || name == "placeholder");
}

#[test]
fn test_web_fetch_from_config_raw() {
let config = crate::config::WebToolsConfig {
search_provider: None,
fetch_mode: Some("raw".to_string()),
};

let tool = WebFetchTool::from_config(Some(&config));
assert_eq!(tool.mode, "raw");
}

#[test]
fn test_web_fetch_from_config_readability() {
let config = crate::config::WebToolsConfig {
search_provider: None,
fetch_mode: Some("readability".to_string()),
};

let tool = WebFetchTool::from_config(Some(&config));
assert_eq!(tool.mode, "readability");
}

#[test]
fn test_web_fetch_from_config_fallback() {
// When config is None, should default to "raw"
let tool = WebFetchTool::from_config(None);
assert_eq!(tool.mode, "raw");
}

#[test]
fn test_web_fetch_from_config_none_mode() {
// When config has None for fetch_mode, should default to "raw"
let config = crate::config::WebToolsConfig {
search_provider: Some("exa".to_string()),
fetch_mode: None,
};

let tool = WebFetchTool::from_config(Some(&config));
assert_eq!(tool.mode, "raw");
}
}
78 changes: 78 additions & 0 deletions crates/terraphim_tinyclaw/tests/config_wiring.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//! Integration tests for config wiring to tools.
//!
//! Tests that configuration values from files are properly passed to tools.

use terraphim_tinyclaw::config::{Config, ToolsConfig, WebToolsConfig};
use terraphim_tinyclaw::tools::create_default_registry;

/// Test that web tools configuration is wired through to the registry.
#[test]
fn test_web_tools_config_wired_to_registry() {
// Create a config with specific web tools settings
let config = Config {
tools: ToolsConfig {
web: Some(WebToolsConfig {
search_provider: Some("exa".to_string()),
fetch_mode: Some("readability".to_string()),
}),
..Default::default()
},
..Default::default()
};

// Create registry with the web tools config
let web_tools_config = config.tools.web.as_ref();
let registry = create_default_registry(None, web_tools_config);

// Verify web_search tool is present
let web_search = registry.get("web_search");
assert!(web_search.is_some(), "web_search tool should be registered");

// Verify web_fetch tool is present
let web_fetch = registry.get("web_fetch");
assert!(web_fetch.is_some(), "web_fetch tool should be registered");
}

/// Test that registry works with no web tools config.
#[test]
fn test_registry_without_web_tools_config() {
// Create registry without web tools config
let registry = create_default_registry(None, None);

// Verify web_search tool is still present (with defaults)
let web_search = registry.get("web_search");
assert!(
web_search.is_some(),
"web_search tool should be registered even without config"
);

// Verify web_fetch tool is still present (with defaults)
let web_fetch = registry.get("web_fetch");
assert!(
web_fetch.is_some(),
"web_fetch tool should be registered even without config"
);
}

/// Test that all expected tools are registered.
#[test]
fn test_all_expected_tools_registered() {
let registry = create_default_registry(None, None);

let expected_tools = [
"filesystem",
"edit",
"shell",
"web_search",
"web_fetch",
"voice_transcribe",
];

for tool_name in &expected_tools {
assert!(
registry.get(tool_name).is_some(),
"Tool '{}' should be registered",
tool_name
);
}
}
Loading