LifeFlow-AI / src /infra /config.py
Marco310's picture
Fix: specific API keys are now optional to prevent startup crash when env vars are missing
5f40fec
"""
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