""" Application Settings Design System Extractor v2 Loads configuration from environment variables and YAML files. """ import os from pathlib import Path from typing import Optional from dataclasses import dataclass, field from dotenv import load_dotenv import yaml # Load environment variables from .env file env_path = Path(__file__).parent / ".env" if env_path.exists(): load_dotenv(env_path) else: # Try loading from parent directory (for development) load_dotenv(Path(__file__).parent.parent / ".env") @dataclass class HFSettings: """Hugging Face configuration.""" hf_token: str = field(default_factory=lambda: os.getenv("HF_TOKEN", "")) hf_space_name: str = field(default_factory=lambda: os.getenv("HF_SPACE_NAME", "")) use_inference_api: bool = field(default_factory=lambda: os.getenv("USE_HF_INFERENCE_API", "true").lower() == "true") inference_timeout: int = field(default_factory=lambda: int(os.getenv("HF_INFERENCE_TIMEOUT", "120"))) max_new_tokens: int = field(default_factory=lambda: int(os.getenv("HF_MAX_NEW_TOKENS", "2048"))) temperature: float = field(default_factory=lambda: float(os.getenv("HF_TEMPERATURE", "0.3"))) @dataclass class ModelSettings: """Model configuration for each agent — Diverse providers.""" # Agent 1: Rule-based, no LLM needed # Agent 2 (Normalizer): Fast structured output # Default: Microsoft Phi (fast, great structured output) agent2_model: str = field(default_factory=lambda: os.getenv("AGENT2_MODEL", "microsoft/Phi-3.5-mini-instruct")) # Agent 3 (Advisor): Strong reasoning - MOST IMPORTANT # Default: Meta Llama 70B (excellent reasoning) agent3_model: str = field(default_factory=lambda: os.getenv("AGENT3_MODEL", "meta-llama/Llama-3.1-70B-Instruct")) # Agent 4 (Generator): Code/JSON specialist # Default: Mistral Codestral (code specialist) agent4_model: str = field(default_factory=lambda: os.getenv("AGENT4_MODEL", "mistralai/Codestral-22B-v0.1")) # Fallback fallback_model: str = field(default_factory=lambda: os.getenv("FALLBACK_MODEL", "mistralai/Mistral-7B-Instruct-v0.3")) @dataclass class APISettings: """API key configuration (optional alternatives).""" anthropic_api_key: str = field(default_factory=lambda: os.getenv("ANTHROPIC_API_KEY", "")) openai_api_key: str = field(default_factory=lambda: os.getenv("OPENAI_API_KEY", "")) @dataclass class BrowserSettings: """Playwright browser configuration.""" browser_type: str = field(default_factory=lambda: os.getenv("BROWSER_TYPE", "chromium")) headless: bool = field(default_factory=lambda: os.getenv("BROWSER_HEADLESS", "true").lower() == "true") timeout: int = field(default_factory=lambda: int(os.getenv("BROWSER_TIMEOUT", "30000"))) network_idle_timeout: int = field(default_factory=lambda: int(os.getenv("NETWORK_IDLE_TIMEOUT", "5000"))) @dataclass class CrawlSettings: """Website crawling configuration.""" max_pages: int = field(default_factory=lambda: int(os.getenv("MAX_PAGES", "20"))) min_pages: int = field(default_factory=lambda: int(os.getenv("MIN_PAGES", "10"))) crawl_delay_ms: int = field(default_factory=lambda: int(os.getenv("CRAWL_DELAY_MS", "1000"))) max_concurrent: int = field(default_factory=lambda: int(os.getenv("MAX_CONCURRENT_CRAWLS", "3"))) respect_robots_txt: bool = field(default_factory=lambda: os.getenv("RESPECT_ROBOTS_TXT", "true").lower() == "true") @dataclass class ViewportSettings: """Viewport configuration for extraction.""" desktop_width: int = 1440 desktop_height: int = 900 mobile_width: int = 375 mobile_height: int = 812 @dataclass class StorageSettings: """Persistent storage configuration.""" storage_path: str = field(default_factory=lambda: os.getenv("STORAGE_PATH", "/data")) enable_persistence: bool = field(default_factory=lambda: os.getenv("ENABLE_PERSISTENCE", "true").lower() == "true") max_versions: int = field(default_factory=lambda: int(os.getenv("MAX_VERSIONS", "10"))) @dataclass class UISettings: """UI configuration.""" server_port: int = field(default_factory=lambda: int(os.getenv("SERVER_PORT", "7860"))) share: bool = field(default_factory=lambda: os.getenv("SHARE", "false").lower() == "true") theme: str = field(default_factory=lambda: os.getenv("UI_THEME", "soft")) @dataclass class FeatureFlags: """Feature toggles.""" color_ramps: bool = field(default_factory=lambda: os.getenv("FEATURE_COLOR_RAMPS", "true").lower() == "true") type_scales: bool = field(default_factory=lambda: os.getenv("FEATURE_TYPE_SCALES", "true").lower() == "true") a11y_checks: bool = field(default_factory=lambda: os.getenv("FEATURE_A11Y_CHECKS", "true").lower() == "true") parallel_extraction: bool = field(default_factory=lambda: os.getenv("FEATURE_PARALLEL_EXTRACTION", "true").lower() == "true") @dataclass class Settings: """Main settings container.""" debug: bool = field(default_factory=lambda: os.getenv("DEBUG", "false").lower() == "true") log_level: str = field(default_factory=lambda: os.getenv("LOG_LEVEL", "INFO")) hf: HFSettings = field(default_factory=HFSettings) models: ModelSettings = field(default_factory=ModelSettings) api: APISettings = field(default_factory=APISettings) browser: BrowserSettings = field(default_factory=BrowserSettings) crawl: CrawlSettings = field(default_factory=CrawlSettings) viewport: ViewportSettings = field(default_factory=ViewportSettings) storage: StorageSettings = field(default_factory=StorageSettings) ui: UISettings = field(default_factory=UISettings) features: FeatureFlags = field(default_factory=FeatureFlags) # Agent configuration loaded from YAML agents_config: dict = field(default_factory=dict) def __post_init__(self): """Load agent configuration from YAML after initialization.""" self.load_agents_config() def load_agents_config(self): """Load agent personas and settings from YAML file.""" yaml_path = Path(__file__).parent / "agents.yaml" if yaml_path.exists(): with open(yaml_path, "r") as f: self.agents_config = yaml.safe_load(f) else: print(f"Warning: agents.yaml not found at {yaml_path}") self.agents_config = {} def get_agent_persona(self, agent_name: str) -> str: """Get persona string for an agent.""" agent_key = f"agent_{agent_name}" if agent_key in self.agents_config: return self.agents_config[agent_key].get("persona", "") return "" def get_agent_config(self, agent_name: str) -> dict: """Get full configuration for an agent.""" agent_key = f"agent_{agent_name}" return self.agents_config.get(agent_key, {}) def get_model_for_agent(self, agent_name: str) -> str: """Get the model ID for a specific agent.""" model_map = { "normalizer": self.models.agent2_model, "advisor": self.models.agent3_model, "generator": self.models.agent4_model, } return model_map.get(agent_name, self.models.fallback_model) def validate(self) -> list[str]: """Validate settings and return list of errors.""" errors = [] if not self.hf.hf_token: errors.append("HF_TOKEN is required for model inference") if self.crawl.max_pages < self.crawl.min_pages: errors.append("MAX_PAGES must be >= MIN_PAGES") return errors # Global settings instance settings = Settings() def get_settings() -> Settings: """Get the global settings instance.""" return settings def reload_settings() -> Settings: """Reload settings from environment and config files.""" global settings settings = Settings() return settings # Convenience functions def is_debug() -> bool: """Check if debug mode is enabled.""" return settings.debug def get_hf_token() -> str: """Get HuggingFace token.""" return settings.hf.hf_token def get_agent_persona(agent_name: str) -> str: """Get persona for an agent.""" return settings.get_agent_persona(agent_name) def get_model_for_agent(agent_name: str) -> str: """Get model ID for an agent.""" return settings.get_model_for_agent(agent_name)