Skip to main content

web_search/providers/
registry.rs

1//! Typed provider registry.
2//!
3//! A single source of truth describing every search provider this library can
4//! use, grouped into the four categories that `formal-ai` consumes (`search`,
5//! `knowledge`, `papers`, `code`). The registry powers provider discovery
6//! (CLI/server/`/providers`) and is the factory that instantiates the correct
7//! provider implementation for each id. Mirrors the JavaScript
8//! `src/providers/registry.js` (issue #3 parity requirement).
9
10use serde::Serialize;
11
12use super::base::SearchProvider;
13use super::bing::{BingConfig, BingProvider};
14use super::duckduckgo::DuckDuckGoProvider;
15use super::engines::{access_for, all_descriptor_engines, EngineDescriptor};
16use super::generic::GenericProvider;
17use super::google::{GoogleConfig, GoogleProvider};
18use super::web_capture::{WebCaptureProvider, SUPPORTED_PROVIDERS};
19
20/// Provider categories, mirroring `formal-ai`'s `web_search_core` registry.
21pub const CATEGORIES: [&str; 4] = ["search", "knowledge", "papers", "code"];
22
23/// Public metadata describing a single registered provider.
24#[derive(Debug, Clone, Serialize)]
25#[serde(rename_all = "camelCase")]
26pub struct RegistryEntry {
27    /// Stable provider id.
28    pub id: String,
29    /// Human-readable label.
30    pub label: String,
31    /// Provider category (one of [`CATEGORIES`]).
32    pub category: String,
33    /// Whether the endpoint is browser-CORS readable.
34    pub cors_readable: bool,
35    /// Whether this is its category's default provider.
36    pub default_for_category: bool,
37    /// How results are obtained (`api`, `html`, `hybrid`, `component`, ...).
38    pub access: String,
39}
40
41/// Engine configuration used to instantiate providers.
42#[derive(Debug, Clone, Default)]
43pub struct BuildConfig {
44    /// Google Custom Search API key.
45    pub google_api_key: Option<String>,
46    /// Google Custom Search Engine ID.
47    pub google_cx: Option<String>,
48    /// Bing Search API key.
49    pub bing_api_key: Option<String>,
50}
51
52/// Metadata for a class-based provider (google/bing/duckduckgo) that predates
53/// the descriptor catalog and keeps its dedicated API + scraping logic.
54struct ClassEngine {
55    id: &'static str,
56    label: &'static str,
57    category: &'static str,
58    cors_readable: bool,
59    default_for_category: bool,
60    access: &'static str,
61}
62
63const CLASS_ENGINES: [ClassEngine; 3] = [
64    ClassEngine {
65        id: "google",
66        label: "Google",
67        category: "search",
68        cors_readable: false,
69        default_for_category: false,
70        access: "hybrid",
71    },
72    ClassEngine {
73        id: "bing",
74        label: "Bing",
75        category: "search",
76        cors_readable: false,
77        default_for_category: false,
78        access: "hybrid",
79    },
80    ClassEngine {
81        id: "duckduckgo",
82        label: "DuckDuckGo",
83        category: "search",
84        cors_readable: false,
85        default_for_category: true,
86        access: "html",
87    },
88];
89
90fn descriptor_entry(d: &EngineDescriptor) -> RegistryEntry {
91    RegistryEntry {
92        id: d.id.to_string(),
93        label: d.label.to_string(),
94        category: d.category.to_string(),
95        cors_readable: d.cors_readable,
96        default_for_category: d.default_for_category,
97        access: access_for(d.kind).to_string(),
98    }
99}
100
101/// Build the full registry of provider entries, in catalog order
102/// (class engines, descriptor engines, then web-capture engines).
103pub fn get_registry() -> Vec<RegistryEntry> {
104    let mut entries = Vec::new();
105
106    for e in &CLASS_ENGINES {
107        entries.push(RegistryEntry {
108            id: e.id.to_string(),
109            label: e.label.to_string(),
110            category: e.category.to_string(),
111            cors_readable: e.cors_readable,
112            default_for_category: e.default_for_category,
113            access: e.access.to_string(),
114        });
115    }
116    for d in all_descriptor_engines() {
117        entries.push(descriptor_entry(&d));
118    }
119    for engine in SUPPORTED_PROVIDERS {
120        entries.push(RegistryEntry {
121            id: format!("wc:{engine}"),
122            label: format!("web-capture ({engine})"),
123            category: "search".to_string(),
124            cors_readable: engine == "wikipedia",
125            default_for_category: false,
126            access: "component".to_string(),
127        });
128    }
129
130    entries
131}
132
133/// Get all provider ids, optionally filtered by category.
134pub fn get_provider_ids(category: Option<&str>) -> Vec<String> {
135    get_registry()
136        .into_iter()
137        .filter(|e| category.is_none_or(|c| e.category == c))
138        .map(|e| e.id)
139        .collect()
140}
141
142/// Get the default provider ids used when the caller does not specify providers.
143///
144/// Mirrors FormalAI's live default plan (`WEB_SEARCH_PROVIDERS`): a
145/// DuckDuckGo-first, CORS-readable knowledge sweep across the Wikimedia family
146/// and the Internet Archive (issue #5 parity requirement).
147pub fn get_default_provider_ids() -> Vec<String> {
148    [
149        "duckduckgo",
150        "internet-archive",
151        "wikipedia",
152        "wikidata",
153        "wiktionary",
154        "wikinews",
155    ]
156    .iter()
157    .map(|s| s.to_string())
158    .collect()
159}
160
161/// Whether `category` is a known category.
162pub fn is_known_category(category: &str) -> bool {
163    CATEGORIES.contains(&category)
164}
165
166/// Instantiate every registered provider, keyed by id, in catalog order.
167pub fn build_providers(config: &BuildConfig) -> Vec<(String, Box<dyn SearchProvider>)> {
168    let mut providers: Vec<(String, Box<dyn SearchProvider>)> = Vec::new();
169
170    providers.push((
171        "google".to_string(),
172        Box::new(GoogleProvider::new(GoogleConfig {
173            api_key: config.google_api_key.clone(),
174            search_engine_id: config.google_cx.clone(),
175        })),
176    ));
177    providers.push((
178        "bing".to_string(),
179        Box::new(BingProvider::new(BingConfig {
180            api_key: config.bing_api_key.clone(),
181        })),
182    ));
183    providers.push((
184        "duckduckgo".to_string(),
185        Box::new(DuckDuckGoProvider::new()),
186    ));
187
188    for d in all_descriptor_engines() {
189        providers.push((d.id.to_string(), Box::new(GenericProvider::new(d))));
190    }
191
192    for engine in SUPPORTED_PROVIDERS {
193        providers.push((
194            format!("wc:{engine}"),
195            Box::new(WebCaptureProvider::new(engine)),
196        ));
197    }
198
199    providers
200}