pub use crate::browser::profile::{BrowserProfile, ProxyConfig};
use crate::error::BrowsingError;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use tracing::warn;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LlmConfig {
pub api_key: Option<String>,
pub model: Option<String>,
pub temperature: Option<f64>,
pub max_tokens: Option<u32>,
}
impl Default for LlmConfig {
fn default() -> Self {
Self {
api_key: None,
model: None,
temperature: None,
max_tokens: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentConfig {
pub max_steps: Option<u32>,
pub use_vision: Option<bool>,
pub system_prompt: Option<String>,
}
impl Default for AgentConfig {
fn default() -> Self {
Self {
max_steps: Some(100),
use_vision: None,
system_prompt: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub browser_profile: BrowserProfile,
pub llm: LlmConfig,
pub agent: AgentConfig,
}
impl Default for Config {
fn default() -> Self {
Self {
browser_profile: BrowserProfile::default(),
llm: LlmConfig::default(),
agent: AgentConfig::default(),
}
}
}
impl Config {
pub fn validate(&self) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
if let Some(ref api_key) = self.llm.api_key {
if api_key.is_empty() {
errors.push("LLM API key is empty".to_string());
} else if api_key.len() < 10 {
errors.push(format!(
"LLM API key is too short ({} characters, expected at least 10)",
api_key.len()
));
}
}
if let Some(ref model) = self.llm.model {
if model.is_empty() {
errors.push("LLM model name is empty".to_string());
}
}
if let Some(temperature) = self.llm.temperature {
if temperature < 0.0 || temperature > 2.0 {
errors.push(format!(
"LLM temperature out of range ({}), must be between 0.0 and 2.0",
temperature
));
}
}
if let Some(max_tokens) = self.llm.max_tokens {
if max_tokens == 0 {
errors.push("LLM max_tokens cannot be zero".to_string());
} else if max_tokens > 128000 {
errors.push(format!(
"LLM max_tokens too large ({}), maximum is 128000",
max_tokens
));
}
}
if let Some(max_steps) = self.agent.max_steps {
if max_steps == 0 {
errors.push("Agent max_steps cannot be zero".to_string());
} else if max_steps > 1000 {
errors.push(format!(
"Agent max_steps too large ({}), maximum is 1000",
max_steps
));
}
}
if let Some(ref allowed_domains) = self.browser_profile.allowed_domains {
if allowed_domains.is_empty() {
errors.push(
"Browser allowed_domains list is empty, which would block all domains"
.to_string(),
);
} else {
for domain in allowed_domains {
if domain.is_empty() {
errors.push("Browser allowed_domains contains empty domain".to_string());
} else if !domain.contains('.') && !domain.is_empty() {
errors.push(format!(
"Invalid domain format: '{}', domains should contain at least one dot",
domain
));
}
}
}
}
if let Some(ref proxy) = self.browser_profile.proxy {
if proxy.server.is_empty() {
errors.push("Proxy server URL is empty".to_string());
} else if !proxy.server.starts_with("http://")
&& !proxy.server.starts_with("https://")
&& !proxy.server.starts_with("socks://")
{
errors.push(format!(
"Invalid proxy URL scheme: '{}', must be http://, https://, or socks://",
proxy.server
));
}
if let Some(ref bypass) = proxy.bypass {
if bypass.is_empty() {
errors.push("Proxy bypass list is empty".to_string());
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
pub fn validate_or_error(&self) -> Result<(), BrowsingError> {
match self.validate() {
Ok(()) => Ok(()),
Err(errors) => {
let error_msg = if errors.len() == 1 {
format!("Configuration error: {}", errors[0])
} else {
format!(
"Configuration errors ({}):\n {}",
errors.len(),
errors.join("\n ")
)
};
Err(BrowsingError::Config(error_msg))
}
}
}
}
pub fn parse_proxy_config_from_env() -> Option<ProxyConfig> {
let server = std::env::var("HTTP_PROXY")
.or_else(|_| std::env::var("http_proxy"))
.or_else(|_| std::env::var("HTTPS_PROXY"))
.or_else(|_| std::env::var("https_proxy"))
.ok()?;
let bypass = std::env::var("NO_PROXY")
.or_else(|_| std::env::var("no_proxy"))
.ok()
.filter(|s| !s.is_empty());
let username = std::env::var("PROXY_USERNAME")
.or_else(|_| std::env::var("proxy_username"))
.ok()
.filter(|s| !s.is_empty());
let password = std::env::var("PROXY_PASSWORD")
.or_else(|_| std::env::var("proxy_password"))
.ok()
.filter(|s| !s.is_empty());
Some(ProxyConfig {
server,
bypass,
username,
password,
})
}
impl Config {
pub fn from_env() -> Self {
let _ = dotenv::dotenv();
Self {
browser_profile: BrowserProfile {
headless: std::env::var("BROWSER_USE_HEADLESS")
.ok()
.and_then(|v| v.parse().ok()),
user_data_dir: std::env::var("BROWSER_USE_USER_DATA_DIR")
.ok()
.map(PathBuf::from),
allowed_domains: std::env::var("BROWSER_USE_ALLOWED_DOMAINS")
.ok()
.map(|s| s.split(',').map(|s| s.trim().to_string()).collect()),
downloads_path: std::env::var("BROWSER_USE_DOWNLOADS_PATH")
.ok()
.map(PathBuf::from),
proxy: parse_proxy_config_from_env(),
},
llm: LlmConfig {
api_key: std::env::var("LLM_API_KEY").ok(),
model: std::env::var("LLM_MODEL").ok(),
temperature: std::env::var("LLM_TEMPERATURE")
.ok()
.and_then(|v| v.parse().ok()),
max_tokens: std::env::var("LLM_MAX_TOKENS")
.ok()
.and_then(|v| v.parse().ok()),
},
agent: AgentConfig {
max_steps: std::env::var("BROWSER_USE_MAX_STEPS")
.ok()
.and_then(|v| v.parse().ok())
.or(Some(100)),
use_vision: std::env::var("BROWSER_USE_VISION")
.ok()
.and_then(|v| v.parse().ok()),
system_prompt: None,
},
}
}
pub fn load_from_file<P: AsRef<Path>>(path: P) -> anyhow::Result<Self> {
if !path.as_ref().exists() {
warn!("Config file not found, using defaults");
return Ok(Self::from_env());
}
let content = std::fs::read_to_string(path)?;
let config: Config = serde_json::from_str(&content)?;
Ok(config)
}
}