mirror of
https://github.com/RightNow-AI/openfang.git
synced 2026-07-30 23:05:08 +00:00
feat: add SearXNG search provider with custom URL and JSON output
This commit is contained in:
@@ -55,6 +55,7 @@ impl WebSearchEngine {
|
||||
SearchProvider::Tavily => self.search_tavily(query, max_results).await,
|
||||
SearchProvider::Perplexity => self.search_perplexity(query).await,
|
||||
SearchProvider::DuckDuckGo => self.search_duckduckgo(query, max_results).await,
|
||||
SearchProvider::Searxng => self.search_searxng(query, max_results).await,
|
||||
SearchProvider::Auto => self.search_auto(query, max_results).await,
|
||||
};
|
||||
|
||||
@@ -67,7 +68,7 @@ impl WebSearchEngine {
|
||||
}
|
||||
|
||||
/// Auto-select provider based on available API keys.
|
||||
/// Priority: Tavily → Brave → Perplexity → DuckDuckGo
|
||||
/// Priority: Tavily → Brave → Perplexity → Searxng → DuckDuckGo
|
||||
async fn search_auto(&self, query: &str, max_results: usize) -> Result<String, String> {
|
||||
// Tavily first (AI-agent-native)
|
||||
if resolve_api_key(&self.config.tavily.api_key_env).is_some() {
|
||||
@@ -96,6 +97,15 @@ impl WebSearchEngine {
|
||||
}
|
||||
}
|
||||
|
||||
// Searxng fourth (self-hosted, no API key needed)
|
||||
if !self.config.searxng.url.is_empty() {
|
||||
debug!("Auto: trying Searxng");
|
||||
match self.search_searxng(query, max_results).await {
|
||||
Ok(result) => return Ok(result),
|
||||
Err(e) => warn!("Searxng failed, falling back: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
// DuckDuckGo always available as zero-config fallback
|
||||
debug!("Auto: falling back to DuckDuckGo");
|
||||
self.search_duckduckgo(query, max_results).await
|
||||
@@ -313,6 +323,99 @@ impl WebSearchEngine {
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Search via SearXNG self-hosted instance.
|
||||
async fn search_searxng(&self, query: &str, max_results: usize) -> Result<String, String> {
|
||||
if self.config.searxng.url.is_empty() {
|
||||
return Err("SearXNG URL is not configured".to_string());
|
||||
}
|
||||
|
||||
let limit = max_results.min(self.config.searxng.max_results);
|
||||
|
||||
debug!(query, "Searching via SearXNG");
|
||||
|
||||
let resp = self
|
||||
.client
|
||||
.get(format!("{}/search", self.config.searxng.url.trim_end_matches('/')))
|
||||
.query(&[
|
||||
("q", query),
|
||||
("format", "json"),
|
||||
("engines", "general"),
|
||||
("limit", &limit.to_string()),
|
||||
])
|
||||
.header("User-Agent", "Mozilla/5.0 (compatible; OpenFangAgent/0.1)")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("SearXNG request failed: {e}"))?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("SearXNG API returned {}", resp.status()));
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct SearxngResponse {
|
||||
results: Vec<SearxngResult>,
|
||||
query: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct SearxngResult {
|
||||
url: String,
|
||||
title: String,
|
||||
content: Option<String>,
|
||||
}
|
||||
|
||||
let data: SearxngResponse = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("SearXNG JSON parse failed: {e}"))?;
|
||||
|
||||
if data.results.is_empty() {
|
||||
return Err(format!("No results found for '{query}' (SearXNG)."));
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct OutputResult<'a> {
|
||||
title: &'a str,
|
||||
url: &'a str,
|
||||
content: &'a str,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct Output<'a> {
|
||||
query: &'a str,
|
||||
results: Vec<OutputResult<'a>>,
|
||||
}
|
||||
|
||||
let results: Vec<OutputResult> = data
|
||||
.results
|
||||
.iter()
|
||||
.map(|r| OutputResult {
|
||||
title: &r.title,
|
||||
url: &r.url,
|
||||
content: r.content.as_deref().unwrap_or(""),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let output = Output {
|
||||
query: &data.query,
|
||||
results,
|
||||
};
|
||||
|
||||
serde_json::to_string(&output)
|
||||
.map_err(|e| format!("Failed to serialize SearXNG results: {e}"))
|
||||
}
|
||||
|
||||
/// Return all non-Auto search providers (for provider listing/discovery).
|
||||
pub fn all_providers() -> Vec<SearchProvider> {
|
||||
vec![
|
||||
SearchProvider::Brave,
|
||||
SearchProvider::Tavily,
|
||||
SearchProvider::Perplexity,
|
||||
SearchProvider::DuckDuckGo,
|
||||
SearchProvider::Searxng,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -173,7 +173,9 @@ pub enum SearchProvider {
|
||||
Perplexity,
|
||||
/// DuckDuckGo HTML (no API key needed).
|
||||
DuckDuckGo,
|
||||
/// Auto-select based on available API keys (Tavily → Brave → Perplexity → DuckDuckGo).
|
||||
/// SearXNG self-hosted search (no API key needed).
|
||||
Searxng,
|
||||
/// Auto-select based on available API keys (Tavily → Brave → Perplexity → Searxng → DuckDuckGo).
|
||||
#[default]
|
||||
Auto,
|
||||
}
|
||||
@@ -192,6 +194,8 @@ pub struct WebConfig {
|
||||
pub tavily: TavilySearchConfig,
|
||||
/// Perplexity Search configuration.
|
||||
pub perplexity: PerplexitySearchConfig,
|
||||
/// SearXNG Search configuration.
|
||||
pub searxng: SearxngSearchConfig,
|
||||
/// Web fetch configuration.
|
||||
pub fetch: WebFetchConfig,
|
||||
}
|
||||
@@ -204,6 +208,7 @@ impl Default for WebConfig {
|
||||
brave: BraveSearchConfig::default(),
|
||||
tavily: TavilySearchConfig::default(),
|
||||
perplexity: PerplexitySearchConfig::default(),
|
||||
searxng: SearxngSearchConfig::default(),
|
||||
fetch: WebFetchConfig::default(),
|
||||
}
|
||||
}
|
||||
@@ -281,6 +286,25 @@ impl Default for PerplexitySearchConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// SearXNG self-hosted search configuration.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct SearxngSearchConfig {
|
||||
/// Base URL of the SearXNG instance (e.g., "https://search.example.com").
|
||||
pub url: String,
|
||||
/// Maximum results to return.
|
||||
pub max_results: usize,
|
||||
}
|
||||
|
||||
impl Default for SearxngSearchConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
url: String::new(),
|
||||
max_results: 5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Web fetch configuration.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
@@ -3588,6 +3612,13 @@ impl KernelConfig {
|
||||
));
|
||||
}
|
||||
}
|
||||
SearchProvider::Searxng => {
|
||||
if self.web.searxng.url.is_empty() {
|
||||
warnings.push(
|
||||
"Searxng search selected but searxng.url is not configured".to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
SearchProvider::DuckDuckGo | SearchProvider::Auto => {}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user