use crate::config_ref::WebSearchBackendConfigRef;
pub const MAX_SEARCH_RESULTS: usize = 100;
pub fn resolve_api_key(cfg: &WebSearchBackendConfigRef, env_keys: &[&str]) -> Option<String> {
cfg.api_key
.as_ref()
.map(|v| v.trim())
.filter(|v| !v.is_empty())
.map(ToString::to_string)
.or_else(|| {
env_keys.iter().find_map(|key| {
std::env::var(key)
.ok()
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty())
})
})
}
pub fn resolve_endpoint(cfg: &WebSearchBackendConfigRef, env_keys: &[&str]) -> Option<String> {
cfg.endpoint
.as_ref()
.map(|v| v.trim().trim_end_matches('/').to_string())
.filter(|v| !v.is_empty())
.or_else(|| {
env_keys.iter().find_map(|key| {
std::env::var(key)
.ok()
.map(|v| v.trim().trim_end_matches('/').to_string())
.filter(|v| !v.is_empty())
})
})
}
pub fn resolve_timeout_secs(cfg: &WebSearchBackendConfigRef, chain_default: u64) -> u64 {
cfg.timeout_secs.unwrap_or(chain_default).max(1)
}
pub fn lookup_backend_config(
backends: &std::collections::HashMap<String, WebSearchBackendConfigRef>,
name: &str,
) -> WebSearchBackendConfigRef {
let key = name.trim().to_ascii_lowercase();
if let Some(cfg) = backends.get(&key) {
return cfg.clone();
}
let alias = match key.as_str() {
"brave" => "brave-free",
"brave-free" => "brave",
"ddg" | "duckduckgo" => "ddgs",
_ => return WebSearchBackendConfigRef::default(),
};
backends.get(alias).cloned().unwrap_or_default()
}
pub fn backend_is_configured(name: &str, cfg: &WebSearchBackendConfigRef) -> bool {
match normalize_backend_name(name).as_str() {
"searxng" => resolve_endpoint(cfg, &["SEARXNG_URL"]).is_some(),
"brave" => resolve_api_key(cfg, &["BRAVE_API_KEY", "BRAVE_SEARCH_API_KEY"]).is_some(),
"tavily" => resolve_api_key(cfg, &["TAVILY_API_KEY"]).is_some(),
"firecrawl" => resolve_api_key(cfg, &["FIRECRAWL_API_KEY"]).is_some(),
"exa" => resolve_api_key(cfg, &["EXA_API_KEY"]).is_some(),
"parallel" => resolve_api_key(cfg, &["PARALLEL_API_KEY"]).is_some(),
"xai" => resolve_api_key(cfg, &["XAI_API_KEY"]).is_some(),
"ddgs" => true,
_ => true,
}
}
pub fn require_backend_configured(
backend: &str,
cfg: &WebSearchBackendConfigRef,
) -> Result<(), crate::tools::web::search::error::SearchError> {
if backend_is_configured(backend, cfg) {
Ok(())
} else {
Err(crate::tools::web::search::error::SearchError::not_configured(backend))
}
}
pub fn require_api_key(
backend: &str,
cfg: &WebSearchBackendConfigRef,
env_keys: &[&str],
) -> Result<String, crate::tools::web::search::error::SearchError> {
resolve_api_key(cfg, env_keys)
.ok_or_else(|| crate::tools::web::search::error::SearchError::not_configured(backend))
}
pub fn require_endpoint(
backend: &str,
cfg: &WebSearchBackendConfigRef,
env_keys: &[&str],
) -> Result<String, crate::tools::web::search::error::SearchError> {
resolve_endpoint(cfg, env_keys)
.ok_or_else(|| crate::tools::web::search::error::SearchError::not_configured(backend))
}
pub fn not_configured_message(name: &str) -> String {
match normalize_backend_name(name).as_str() {
"searxng" => {
"SEARXNG_URL is not set. Set web_search.backends.searxng.endpoint or SEARXNG_URL."
.into()
}
"brave" => {
"BRAVE_API_KEY is not set. Set web_search.backends.brave.api_key or BRAVE_API_KEY."
.into()
}
"tavily" => {
"TAVILY_API_KEY is not set. Set web_search.backends.tavily.api_key or TAVILY_API_KEY."
.into()
}
"firecrawl" => {
"FIRECRAWL_API_KEY is not set. Set web_search.backends.firecrawl.api_key or FIRECRAWL_API_KEY."
.into()
}
"exa" => {
"EXA_API_KEY is not set. Set web_search.backends.exa.api_key or EXA_API_KEY."
.into()
}
"parallel" => {
"PARALLEL_API_KEY is not set. Set web_search.backends.parallel.api_key or PARALLEL_API_KEY."
.into()
}
"xai" => {
"XAI_API_KEY is not set. Set web_search.backends.xai.api_key or XAI_API_KEY."
.into()
}
other => format!("Web search backend '{other}' is not configured."),
}
}
pub fn backend_is_enabled_in_lingshu_home(name: &str, cfg: &WebSearchBackendConfigRef) -> bool {
let name = normalize_backend_name(name);
if name == "ddgs" {
return true;
}
if cfg
.api_key
.as_ref()
.map(|v| !v.trim().is_empty())
.unwrap_or(false)
{
return true;
}
if name == "searxng"
&& cfg
.endpoint
.as_ref()
.map(|v| !v.trim().is_empty())
.unwrap_or(false)
{
return true;
}
let dotenv = lingshu_dotenv_var_names();
env_keys_for_backend(&name)
.iter()
.any(|key| dotenv.contains(*key))
}
fn env_keys_for_backend(name: &str) -> &'static [&'static str] {
match name {
"searxng" => &["SEARXNG_URL"],
"brave" => &["BRAVE_API_KEY", "BRAVE_SEARCH_API_KEY"],
"tavily" => &["TAVILY_API_KEY"],
"firecrawl" => &["FIRECRAWL_API_KEY"],
"exa" => &["EXA_API_KEY"],
"parallel" => &["PARALLEL_API_KEY"],
"xai" => &["XAI_API_KEY"],
_ => &[],
}
}
pub fn lingshu_dotenv_var_names() -> std::collections::HashSet<String> {
use std::collections::HashSet;
let path = crate::config_ref::resolve_lingshu_home().join(".env");
let Ok(content) = std::fs::read_to_string(&path) else {
return HashSet::new();
};
content
.lines()
.map(str::trim)
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.filter_map(|line| line.split_once('=').map(|(k, _)| k.trim().to_string()))
.filter(|k| !k.is_empty())
.collect()
}
pub fn normalize_backend_name(name: &str) -> String {
match name.trim().to_ascii_lowercase().as_str() {
"brave-free" => "brave".into(),
"duckduckgo" | "ddg" => "ddgs".into(),
other => other.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_api_key_overrides_env() {
let _lock = crate::tools::web::search::registry::test_registry_lock();
let prev = std::env::var("TAVILY_API_KEY").ok();
unsafe { std::env::set_var("TAVILY_API_KEY", "env-key") };
let cfg = WebSearchBackendConfigRef {
api_key: Some("cfg-key".into()),
..Default::default()
};
assert_eq!(
resolve_api_key(&cfg, &["TAVILY_API_KEY"]).as_deref(),
Some("cfg-key")
);
unsafe { std::env::remove_var("TAVILY_API_KEY") };
if let Some(v) = prev {
unsafe { std::env::set_var("TAVILY_API_KEY", v) };
}
}
#[test]
fn config_endpoint_overrides_env() {
let prev = std::env::var("SEARXNG_URL").ok();
unsafe { std::env::set_var("SEARXNG_URL", "http://env.example") };
let cfg = WebSearchBackendConfigRef {
endpoint: Some("http://cfg.example/".into()),
..Default::default()
};
assert_eq!(
resolve_endpoint(&cfg, &["SEARXNG_URL"]).as_deref(),
Some("http://cfg.example")
);
unsafe { std::env::remove_var("SEARXNG_URL") };
if let Some(v) = prev {
unsafe { std::env::set_var("SEARXNG_URL", v) };
}
}
#[test]
fn brave_free_alias_finds_brave_config() {
let mut map = std::collections::HashMap::new();
map.insert(
"brave-free".into(),
WebSearchBackendConfigRef {
api_key: Some("free-key".into()),
..Default::default()
},
);
let cfg = lookup_backend_config(&map, "brave");
assert_eq!(cfg.api_key.as_deref(), Some("free-key"));
}
#[test]
fn dotenv_key_counts_as_lingshu_home_enabled() {
let _lock = crate::tools::web::search::test_isolation::web_config_test_lock();
let dir = tempfile::TempDir::new().expect("tempdir");
let prev = std::env::var("EDGECRAB_HOME").ok();
unsafe { std::env::set_var("EDGECRAB_HOME", dir.path()) };
std::fs::write(
dir.path().join(".env"),
"FIRECRAWL_API_KEY=fc-from-dotenv\n",
)
.expect("write env");
assert!(backend_is_enabled_in_lingshu_home(
"firecrawl",
&WebSearchBackendConfigRef::default()
));
unsafe { std::env::remove_var("EDGECRAB_HOME") };
if let Some(v) = prev {
unsafe { std::env::set_var("EDGECRAB_HOME", v) };
}
}
#[test]
fn process_env_alone_does_not_count_for_auto_chain() {
let _lock = crate::tools::web::search::test_isolation::web_config_test_lock();
let _registry = crate::tools::web::search::registry::test_registry_lock();
let dir = tempfile::TempDir::new().expect("tempdir");
let prev_home = std::env::var("EDGECRAB_HOME").ok();
let prev_fc = std::env::var("FIRECRAWL_API_KEY").ok();
unsafe { std::env::set_var("EDGECRAB_HOME", dir.path()) };
unsafe { std::env::set_var("FIRECRAWL_API_KEY", "only-in-process") };
assert!(!backend_is_enabled_in_lingshu_home(
"firecrawl",
&WebSearchBackendConfigRef::default()
));
unsafe { std::env::remove_var("FIRECRAWL_API_KEY") };
unsafe { std::env::remove_var("EDGECRAB_HOME") };
if let Some(v) = prev_fc {
unsafe { std::env::set_var("FIRECRAWL_API_KEY", v) };
}
if let Some(v) = prev_home {
unsafe { std::env::set_var("EDGECRAB_HOME", v) };
}
}
}