Skip to main content

a3s_code_core/config/
search.rs

1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4// ============================================================================
5// Search / Browser / Document Configuration
6// ============================================================================
7
8/// Search engine configuration (a3s-search integration)
9#[derive(Debug, Clone, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase")]
11pub struct SearchConfig {
12    /// Default timeout in seconds for all engines
13    #[serde(default = "default_search_timeout")]
14    pub timeout: u64,
15
16    /// Health monitor configuration
17    #[serde(default, skip_serializing_if = "Option::is_none")]
18    pub health: Option<SearchHealthConfig>,
19
20    /// Engine configurations
21    #[serde(default, rename = "engine")]
22    pub engines: std::collections::HashMap<String, SearchEngineConfig>,
23
24    /// Headless browser configuration for JS-rendered engines (Google, Baidu,
25    /// Bing, and Brave). When omitted, Moli is provisioned lazily from the
26    /// bundled sidecar or the shared per-user cache.
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub headless: Option<HeadlessConfig>,
29}
30
31/// Browser backend for JS-rendered search engines.
32#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "lowercase")]
34pub enum BrowserBackend {
35    /// Moli's standalone, JavaScript-capable headless browser and the default.
36    #[default]
37    Moli,
38    /// Chrome/Chromium headless browser for explicit compatibility use.
39    #[serde(alias = "chromium")]
40    Chrome,
41    /// Explicit Lightpanda backend (native Linux/macOS; WSL2 on Windows).
42    Lightpanda,
43}
44
45/// Headless browser configuration for JS-rendered search engines.
46///
47/// Moli is the default backend. When it is selected and no executable is
48/// configured or packaged, A3S Code downloads the pinned, digest-verified
49/// runtime into the user cache on first use.
50#[derive(Debug, Clone, Serialize, Deserialize)]
51#[serde(rename_all = "camelCase")]
52pub struct HeadlessConfig {
53    /// Browser backend to use.
54    #[serde(default)]
55    pub backend: BrowserBackend,
56
57    /// Maximum number of concurrent browser tabs.
58    #[serde(default = "default_headless_max_tabs")]
59    pub max_tabs: usize,
60
61    /// Path to the browser executable. If None, Moli is discovered/downloaded
62    /// or an explicit Chrome/Lightpanda runtime is discovered.
63    #[serde(
64        default,
65        alias = "chromePath",
66        alias = "lightpandaPath",
67        alias = "obscuraPath",
68        alias = "playwrightPath",
69        skip_serializing_if = "Option::is_none"
70    )]
71    pub browser_path: Option<String>,
72
73    /// Download Moli automatically when the selected backend is Moli and no
74    /// usable executable is already available.
75    #[serde(default = "default_auto_download_moli")]
76    pub auto_download_moli: bool,
77
78    /// Optional pinned Moli release version. Defaults to the version bundled
79    /// by this A3S Code release.
80    #[serde(default, skip_serializing_if = "Option::is_none")]
81    pub moli_version: Option<String>,
82
83    /// Optional SHA-256 digest for the pinned Moli archive. Supplying a version
84    /// without its digest is rejected by the runtime manager.
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub moli_sha256: Option<String>,
87
88    /// Optional cache directory for the managed Moli runtime.
89    #[serde(default, skip_serializing_if = "Option::is_none")]
90    pub moli_cache_dir: Option<PathBuf>,
91
92    /// Maximum time in seconds spent provisioning a missing Moli runtime.
93    /// This budget is separate from the per-request web-search timeout so a
94    /// first-use download does not make the default search path unusable.
95    #[serde(default = "default_moli_download_timeout_secs")]
96    pub moli_download_timeout_secs: u64,
97
98    /// Additional browser launch arguments.
99    #[serde(default, skip_serializing_if = "Vec::is_empty")]
100    pub launch_args: Vec<String>,
101
102    /// Proxy URL for the browser to use.
103    #[serde(default, skip_serializing_if = "Option::is_none")]
104    pub proxy_url: Option<String>,
105}
106
107impl BrowserBackend {
108    pub fn is_moli(self) -> bool {
109        matches!(self, Self::Moli)
110    }
111
112    pub fn is_lightpanda(self) -> bool {
113        matches!(self, Self::Lightpanda)
114    }
115}
116
117impl Default for HeadlessConfig {
118    fn default() -> Self {
119        Self {
120            backend: BrowserBackend::Moli,
121            max_tabs: 4,
122            browser_path: None,
123            auto_download_moli: true,
124            moli_version: None,
125            moli_sha256: None,
126            moli_cache_dir: None,
127            moli_download_timeout_secs: default_moli_download_timeout_secs(),
128            launch_args: Vec::new(),
129            proxy_url: None,
130        }
131    }
132}
133
134/// Default configuration for built-in document context extraction.
135#[derive(Debug, Clone, Serialize, Deserialize)]
136#[serde(rename_all = "camelCase")]
137pub struct DocumentParserConfig {
138    /// Whether the default document extraction stack is registered in the parser registry.
139    #[serde(default = "default_enabled")]
140    pub enabled: bool,
141
142    /// Maximum file size accepted by the parser, in MiB.
143    #[serde(default = "default_document_parser_max_file_size_mb")]
144    pub max_file_size_mb: u64,
145
146    /// Optional cache settings for parsed / normalized document context.
147    #[serde(default, skip_serializing_if = "Option::is_none")]
148    pub cache: Option<DocumentCacheConfig>,
149}
150
151impl Default for DocumentParserConfig {
152    fn default() -> Self {
153        Self {
154            enabled: true,
155            max_file_size_mb: default_document_parser_max_file_size_mb(),
156            cache: Some(DocumentCacheConfig::default()),
157        }
158    }
159}
160
161impl DocumentParserConfig {
162    pub fn normalized(&self) -> Self {
163        Self {
164            enabled: self.enabled,
165            max_file_size_mb: self.max_file_size_mb.clamp(1, 1024),
166            cache: self.cache.as_ref().map(DocumentCacheConfig::normalized),
167        }
168    }
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct DocumentCacheConfig {
174    #[serde(default = "default_enabled")]
175    pub enabled: bool,
176
177    #[serde(default, skip_serializing_if = "Option::is_none")]
178    pub directory: Option<PathBuf>,
179}
180
181impl Default for DocumentCacheConfig {
182    fn default() -> Self {
183        Self {
184            enabled: true,
185            directory: None,
186        }
187    }
188}
189
190impl DocumentCacheConfig {
191    pub fn normalized(&self) -> Self {
192        Self {
193            enabled: self.enabled,
194            directory: self.directory.clone(),
195        }
196    }
197}
198
199/// Search health monitor configuration
200#[derive(Debug, Clone, Serialize, Deserialize)]
201#[serde(rename_all = "camelCase")]
202pub struct SearchHealthConfig {
203    /// Number of consecutive failures before suspending
204    #[serde(default = "default_max_failures")]
205    pub max_failures: u32,
206
207    /// Suspension duration in seconds
208    #[serde(default = "default_suspend_seconds")]
209    pub suspend_seconds: u64,
210}
211
212/// Per-engine search configuration
213#[derive(Debug, Clone, Serialize, Deserialize)]
214#[serde(rename_all = "camelCase")]
215pub struct SearchEngineConfig {
216    /// Whether the engine is enabled
217    #[serde(default = "default_enabled")]
218    pub enabled: bool,
219
220    /// Weight for ranking (higher = more influence)
221    #[serde(default = "default_weight")]
222    pub weight: f64,
223
224    /// Per-engine timeout override in seconds
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub timeout: Option<u64>,
227}
228
229pub(crate) fn default_search_timeout() -> u64 {
230    20
231}
232
233pub(crate) fn default_headless_max_tabs() -> usize {
234    4
235}
236
237fn default_auto_download_moli() -> bool {
238    true
239}
240
241pub(crate) fn default_moli_download_timeout_secs() -> u64 {
242    120
243}
244
245fn default_max_failures() -> u32 {
246    3
247}
248
249fn default_suspend_seconds() -> u64 {
250    60
251}
252
253pub(crate) fn default_enabled() -> bool {
254    true
255}
256
257fn default_weight() -> f64 {
258    1.0
259}
260
261pub(crate) fn default_document_parser_max_file_size_mb() -> u64 {
262    50
263}