feat(tool/web): route searches through provider
Acceptance Criteria:\n- web.search proxies Ollama Cloud's /api/web_search via the configured provider endpoint\n- Tool is only registered when remote search is enabled and the cloud provider is active\n- Consent prompts, docs, and MCP tooling no longer reference DuckDuckGo or expose web_search_detailed Test Notes:\n- cargo check
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
use crate::config::{Config, McpResourceConfig, McpServerConfig};
|
||||
use crate::config::{
|
||||
Config, LEGACY_OLLAMA_CLOUD_API_KEY_ENV, LEGACY_OWLEN_OLLAMA_CLOUD_API_KEY_ENV,
|
||||
McpResourceConfig, McpServerConfig, OLLAMA_API_KEY_ENV, OLLAMA_CLOUD_BASE_URL,
|
||||
};
|
||||
use crate::consent::{ConsentManager, ConsentScope};
|
||||
use crate::conversation::ConversationManager;
|
||||
use crate::credentials::CredentialManager;
|
||||
use crate::encryption::{self, VaultHandle};
|
||||
use crate::formatting::MessageFormatter;
|
||||
use crate::input::InputBuffer;
|
||||
use crate::llm::ProviderConfig;
|
||||
use crate::mcp::McpToolCall;
|
||||
use crate::mcp::client::McpClient;
|
||||
use crate::mcp::factory::McpClientFactory;
|
||||
@@ -24,22 +28,154 @@ use crate::validation::{SchemaValidator, get_builtin_schemas};
|
||||
use crate::{ChatStream, Provider};
|
||||
use crate::{
|
||||
CodeExecTool, ResourcesDeleteTool, ResourcesGetTool, ResourcesListTool, ResourcesWriteTool,
|
||||
ToolRegistry, WebScrapeTool, WebSearchDetailedTool, WebSearchTool,
|
||||
ToolRegistry, WebScrapeTool, WebSearchSettings, WebSearchTool,
|
||||
};
|
||||
use crate::{Error, Result};
|
||||
use chrono::Utc;
|
||||
use log::warn;
|
||||
use reqwest::Url;
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::SystemTime;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tokio::fs;
|
||||
use tokio::sync::Mutex as TokioMutex;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn env_var_non_empty(name: &str) -> Option<String> {
|
||||
env::var(name)
|
||||
.ok()
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
fn compute_web_search_settings(
|
||||
config: &Config,
|
||||
provider_id: &str,
|
||||
) -> Result<Option<WebSearchSettings>> {
|
||||
let provider_id = provider_id.trim();
|
||||
let provider_config = match config.providers.get(provider_id) {
|
||||
Some(cfg) => cfg,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
if !provider_config.enabled {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if provider_config
|
||||
.provider_type
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("ollama")
|
||||
{
|
||||
// Local Ollama does not expose web search.
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if !provider_config
|
||||
.provider_type
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("ollama_cloud")
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let base_url = provider_config
|
||||
.base_url
|
||||
.as_deref()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or(OLLAMA_CLOUD_BASE_URL);
|
||||
|
||||
let endpoint = provider_config
|
||||
.extra
|
||||
.get("web_search_endpoint")
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or("/api/web_search");
|
||||
|
||||
let endpoint_url = build_search_url(base_url, endpoint)?;
|
||||
|
||||
let api_key = resolve_web_search_api_key(provider_config)
|
||||
.or_else(|| env_var_non_empty(OLLAMA_API_KEY_ENV))
|
||||
.or_else(|| env_var_non_empty(LEGACY_OLLAMA_CLOUD_API_KEY_ENV))
|
||||
.or_else(|| env_var_non_empty(LEGACY_OWLEN_OLLAMA_CLOUD_API_KEY_ENV));
|
||||
|
||||
let api_key = match api_key {
|
||||
Some(key) if !key.is_empty() => key,
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
||||
let settings = WebSearchSettings {
|
||||
endpoint: endpoint_url,
|
||||
api_key,
|
||||
provider_label: provider_id.to_string(),
|
||||
timeout: Duration::from_secs(20),
|
||||
};
|
||||
|
||||
Ok(Some(settings))
|
||||
}
|
||||
|
||||
fn resolve_web_search_api_key(provider_config: &ProviderConfig) -> Option<String> {
|
||||
resolve_inline_api_key(provider_config.api_key.as_deref()).or_else(|| {
|
||||
provider_config
|
||||
.api_key_env
|
||||
.as_deref()
|
||||
.and_then(|var| env_var_non_empty(var.trim()))
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_inline_api_key(value: Option<&str>) -> Option<String> {
|
||||
let raw = value?.trim();
|
||||
if raw.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(inner) = raw
|
||||
.strip_prefix("${")
|
||||
.and_then(|value| value.strip_suffix('}'))
|
||||
.map(str::trim)
|
||||
{
|
||||
return env_var_non_empty(inner);
|
||||
}
|
||||
|
||||
if let Some(inner) = raw.strip_prefix('$').map(str::trim) {
|
||||
return env_var_non_empty(inner);
|
||||
}
|
||||
|
||||
Some(raw.to_string())
|
||||
}
|
||||
|
||||
fn build_search_url(base_url: &str, endpoint: &str) -> Result<Url> {
|
||||
let endpoint = endpoint.trim();
|
||||
if let Ok(url) = Url::parse(endpoint) {
|
||||
return Ok(url);
|
||||
}
|
||||
|
||||
let trimmed_base = base_url.trim();
|
||||
let normalized_base = if trimmed_base.ends_with('/') {
|
||||
trimmed_base.to_string()
|
||||
} else {
|
||||
format!("{}/", trimmed_base)
|
||||
};
|
||||
|
||||
let base = Url::parse(&normalized_base).map_err(|err| {
|
||||
Error::Config(format!("Invalid provider base_url '{}': {}", base_url, err))
|
||||
})?;
|
||||
|
||||
if endpoint.is_empty() {
|
||||
return Ok(base);
|
||||
}
|
||||
|
||||
base.join(endpoint.trim_start_matches('/')).map_err(|err| {
|
||||
Error::Config(format!(
|
||||
"Invalid web_search_endpoint '{}': {}",
|
||||
endpoint, err
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
pub enum SessionOutcome {
|
||||
Complete(ChatResponse),
|
||||
Streaming {
|
||||
@@ -251,8 +387,8 @@ async fn build_tools(
|
||||
ui: Arc<dyn UiController>,
|
||||
enable_code_tools: bool,
|
||||
consent_manager: Arc<Mutex<ConsentManager>>,
|
||||
credential_manager: Option<Arc<CredentialManager>>,
|
||||
vault: Option<Arc<Mutex<VaultHandle>>>,
|
||||
_credential_manager: Option<Arc<CredentialManager>>,
|
||||
_vault: Option<Arc<Mutex<VaultHandle>>>,
|
||||
) -> Result<(Arc<ToolRegistry>, Arc<SchemaValidator>)> {
|
||||
let mut registry = ToolRegistry::new(config.clone(), ui);
|
||||
let mut validator = SchemaValidator::new();
|
||||
@@ -265,7 +401,9 @@ async fn build_tools(
|
||||
}
|
||||
}
|
||||
|
||||
if config_guard
|
||||
let active_provider_id = config_guard.general.default_provider.clone();
|
||||
|
||||
let web_search_settings = if config_guard
|
||||
.security
|
||||
.allowed_tools
|
||||
.iter()
|
||||
@@ -273,11 +411,19 @@ async fn build_tools(
|
||||
&& config_guard.tools.web_search.enabled
|
||||
&& config_guard.privacy.enable_remote_search
|
||||
{
|
||||
let tool = WebSearchTool::new(
|
||||
consent_manager.clone(),
|
||||
credential_manager.clone(),
|
||||
vault.clone(),
|
||||
);
|
||||
match compute_web_search_settings(&config_guard, &active_provider_id) {
|
||||
Ok(settings) => settings,
|
||||
Err(err) => {
|
||||
warn!("Skipping web_search tool: {}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(settings) = web_search_settings {
|
||||
let tool = WebSearchTool::new(consent_manager.clone(), settings);
|
||||
registry.register(tool);
|
||||
}
|
||||
|
||||
@@ -294,22 +440,6 @@ async fn build_tools(
|
||||
registry.register(tool);
|
||||
}
|
||||
|
||||
if config_guard
|
||||
.security
|
||||
.allowed_tools
|
||||
.iter()
|
||||
.any(|tool| tool == "web_search")
|
||||
&& config_guard.tools.web_search.enabled
|
||||
&& config_guard.privacy.enable_remote_search
|
||||
{
|
||||
let tool = WebSearchDetailedTool::new(
|
||||
consent_manager.clone(),
|
||||
credential_manager.clone(),
|
||||
vault.clone(),
|
||||
);
|
||||
registry.register(tool);
|
||||
}
|
||||
|
||||
if enable_code_tools
|
||||
&& config_guard
|
||||
.security
|
||||
@@ -905,9 +1035,9 @@ impl SessionController {
|
||||
seen_tools.insert(tool_call.name.clone());
|
||||
|
||||
let (data_types, endpoints) = match tool_call.name.as_str() {
|
||||
"web_search" | "web_search_detailed" => (
|
||||
"web_search" => (
|
||||
vec!["search query".to_string()],
|
||||
vec!["duckduckgo.com".to_string()],
|
||||
vec!["cloud provider".to_string()],
|
||||
),
|
||||
"code_exec" => (
|
||||
vec!["code to execute".to_string()],
|
||||
|
||||
Reference in New Issue
Block a user