""" Centralized configuration for API keys, models, and application settings """ import os import logging from typing import Optional from pathlib import Path from pydantic_settings import BaseSettings from pydantic import Field # ============================================================================ # Settings Class # ============================================================================ class Settings(BaseSettings): """ Application settings loaded from environment variables Environment variables can be set in: 1. System environment 2. .env file in project root 3. Passed explicitly when creating Settings instance """ # ======================================================================== # API Keys # ======================================================================== # Anthropic (Required) anthropic_api_key: Optional[str] = Field( None, alias="ANTHROPIC_API_KEY", description="Anthropic API key for Claude models" ) gemini_api_key: Optional[str] = Field( None, alias="GOOGLE_API_KEY", description="Google API key for Gemini models" ) # Google Maps (Required for Scout and Traffic agents) google_maps_api_key: Optional[str] = Field( None, alias="GOOGLE_MAPS_API_KEY", description="Google Maps Platform API key" ) # OpenWeatherMap (Optional, for Weather agent) openweather_api_key: Optional[str] = Field( None, alias="OPENWEATHER_API_KEY", description="OpenWeatherMap API key (optional)" ) # Mapbox (Optional, for UI visualization) mapbox_token: Optional[str] = Field( None, alias="MAPBOX_TOKEN", description="Mapbox access token for map visualization" ) # ======================================================================== # Model Configuration # ======================================================================== # Claude model for agents and team model_name: str = Field( "claude-sonnet-4-20250514", alias="MODEL_NAME", description="Claude model name to use" ) # Alternative models for cost optimization (future) planner_model: Optional[str] = Field( None, alias="PLANNER_MODEL", description="Specific model for Planner agent (optional)" ) optimizer_model: Optional[str] = Field( None, alias="OPTIMIZER_MODEL", description="Specific model for Optimizer agent (optional)" ) # ======================================================================== # Application Settings # ======================================================================== # Logging log_level: str = Field( "INFO", alias="LOG_LEVEL", description="Logging level (DEBUG/INFO/WARNING/ERROR)" ) log_file: Optional[str] = Field( None, alias="LOG_FILE", description="Log file path (optional, logs to console if not set)" ) # Search parameters poi_search_radius: int = Field( 5000, alias="POI_SEARCH_RADIUS", description="Default POI search radius in meters" ) max_poi_results: int = Field( 5, alias="MAX_POI_RESULTS", description="Maximum number of POI candidates per task" ) # Optimization parameters tsptw_time_limit: int = Field( 10, alias="TSPTW_TIME_LIMIT", description="TSPTW solver time limit in seconds" ) tsptw_verbose: bool = Field( False, alias="TSPTW_VERBOSE", description="Enable verbose output for TSPTW solver" ) # Default locations default_start_lat: float = Field( 25.0330, alias="DEFAULT_START_LAT", description="Default start location latitude (Taipei)" ) default_start_lng: float = Field( 121.5654, alias="DEFAULT_START_LNG", description="Default start location longitude (Taipei)" ) # ======================================================================== # Feature Flags # ======================================================================== enable_weather_agent: bool = Field( True, alias="ENABLE_WEATHER_AGENT", description="Enable Weather agent (requires OpenWeatherMap API key)" ) enable_traffic_agent: bool = Field( True, alias="ENABLE_TRAFFIC_AGENT", description="Enable Traffic agent (requires Google Maps API key)" ) use_mock_data: bool = Field( False, alias="USE_MOCK_DATA", description="Use mock data instead of real APIs (for testing)" ) TASK_LIST_KEY: str = Field("task_list", description="Session state key(name) for task list") # ======================================================================== # Pydantic Config # ======================================================================== class Config: env_file = str(Path(__file__).resolve().parents[2] / ".env") env_file_encoding = "utf-8" case_sensitive = False # Allow extra fields (for future extensions) extra = "ignore" # ============================================================================ # Global Settings Instance # ============================================================================ _settings: Optional[Settings] = None def get_settings() -> Settings: """ Get application settings (singleton pattern) Returns: Settings: Application settings Raises: ValueError: If required environment variables are missing """ global _settings if _settings is None: try: _settings = Settings() except Exception as e: raise ValueError( f"Failed to load settings. Please check your .env file or environment variables.\n" f"Error: {str(e)}\n\n" f"Required variables:\n" f" - ANTHROPIC_API_KEY\n" f" - GOOGLE_MAPS_API_KEY\n\n" f"Optional variables:\n" f" - OPENWEATHER_API_KEY\n" f" - MAPBOX_TOKEN\n" ) from e return _settings def reload_settings(): """ Reload settings from environment Useful for testing or when environment variables change """ global _settings _settings = None return get_settings() # ============================================================================ # Logging Configuration # ============================================================================ def setup_logging(settings: Optional[Settings] = None): """ Setup application logging Args: settings: Settings instance (uses global if not provided) """ if settings is None: settings = get_settings() # Configure logging format log_format = ( "%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) # Configure handlers handlers = [] # Console handler console_handler = logging.StreamHandler() console_handler.setFormatter(logging.Formatter(log_format)) handlers.append(console_handler) # File handler (if specified) if settings.log_file: log_path = Path(settings.log_file) log_path.parent.mkdir(parents=True, exist_ok=True) file_handler = logging.FileHandler(settings.log_file) file_handler.setFormatter(logging.Formatter(log_format)) handlers.append(file_handler) # Configure root logger logging.basicConfig( level=getattr(logging, settings.log_level.upper()), format=log_format, handlers=handlers ) # Set specific loggers logging.getLogger("agno").setLevel(logging.WARNING) logging.getLogger("anthropic").setLevel(logging.WARNING) logging.getLogger("httpx").setLevel(logging.WARNING) logger = logging.getLogger(__name__) logger.info(f"Logging configured: level={settings.log_level}") if settings.log_file: logger.info(f"Logging to file: {settings.log_file}") # ============================================================================ # Validation Helpers # ============================================================================ def validate_api_keys(settings: Optional[Settings] = None) -> dict: """ Validate that all required API keys are set Args: settings: Settings instance Returns: dict: Validation results """ if settings is None: settings = get_settings() results = { "valid": True, "missing": [], "optional_missing": [] } # Check required keys if not settings.anthropic_api_key: results["valid"] = False results["missing"].append("ANTHROPIC_API_KEY") if not settings.google_maps_api_key: results["valid"] = False results["missing"].append("GOOGLE_MAPS_API_KEY") # Check optional keys if settings.enable_weather_agent and not settings.openweather_api_key: results["optional_missing"].append("OPENWEATHER_API_KEY") if not settings.mapbox_token: results["optional_missing"].append("MAPBOX_TOKEN") return results def print_config_status(settings: Optional[Settings] = None): """ Print current configuration status Args: settings: Settings instance """ if settings is None: settings = get_settings() print("=" * 60) print("šŸ“‹ LifeFlow AI - Configuration Status") print("=" * 60) # API Keys print("\nšŸ”‘ API Keys:") print(f" Anthropic: {'āœ… Set' if settings.anthropic_api_key else 'āŒ Missing'}") print(f" Google Maps: {'āœ… Set' if settings.google_maps_api_key else 'āŒ Missing'}") print(f" OpenWeatherMap: {'āœ… Set' if settings.openweather_api_key else 'āš ļø Not set (optional)'}") print(f" Mapbox: {'āœ… Set' if settings.mapbox_token else 'āš ļø Not set (optional)'}") # Model Configuration print("\nšŸ¤– Model Configuration:") print(f" Default Model: {settings.model_name}") if settings.planner_model: print(f" Planner Model: {settings.planner_model}") if settings.optimizer_model: print(f" Optimizer Model: {settings.optimizer_model}") # Application Settings print("\nāš™ļø Application Settings:") print(f" Log Level: {settings.log_level}") print(f" Log File: {settings.log_file or 'Console only'}") print(f" Search Radius: {settings.poi_search_radius}m") print(f" Max POI Results: {settings.max_poi_results}") print(f" TSPTW Time Limit: {settings.tsptw_time_limit}s") # Feature Flags print("\nšŸŽ›ļø Feature Flags:") print(f" Weather Agent: {'āœ… Enabled' if settings.enable_weather_agent else 'āŒ Disabled'}") print(f" Traffic Agent: {'āœ… Enabled' if settings.enable_traffic_agent else 'āŒ Disabled'}") print(f" Mock Data Mode: {'āš ļø Enabled' if settings.use_mock_data else 'āœ… Disabled'}") # Validation validation = validate_api_keys(settings) print("\nāœ… Validation:") if validation["valid"]: print(" All required API keys are set") else: print(f" āŒ Missing required keys: {', '.join(validation['missing'])}") if validation["optional_missing"]: print(f" āš ļø Missing optional keys: {', '.join(validation['optional_missing'])}") print("=" * 60) # ============================================================================ # Test # ============================================================================ if __name__ == "__main__": try: settings = get_settings() print_config_status(settings) # Test validation validation = validate_api_keys(settings) if not validation["valid"]: print("\nāŒ Configuration is invalid!") print("Please set the following environment variables:") for key in validation["missing"]: print(f" - {key}") else: print("\nāœ… Configuration is valid!") except Exception as e: print(f"\nāŒ Error loading configuration:") print(f" {str(e)}") print("\nPlease create a .env file with required variables.") raise e