""" Centralized configuration for Grant Analyst. All runtime configuration loaded from environment variables with sensible defaults. Supports OpenAI as the primary LLM provider with provider validation. ENV VARS -------- # Environment ENV=dev|staging|prod # default: dev # Database MONGO_URI= # MongoDB connection string MONGO_DB=grant_analyst # Database name REDIS_URL= # Redis connection URL (optional) # LLM Provider LLM_PROVIDER=openai # Only OpenAI supported LLM_MODEL=gpt-5-mini # Fallback model LLM_MODEL_ROUTER=gpt-5-nano # Fast routing/classification LLM_MODEL_QA=gpt-5-mini # QA and analysis LLM_MODEL_TRANSLATE=gpt-5-mini # Translations # Auth OPENAI_API_KEY= # Required for OpenAI # Security ALLOWED_ORIGINS= # Comma-separated CORS origins # Behavior MAX_QUERY_CHARS=4000 # Max query length LLM_TEMPERATURE=0.2 # 0..2 LLM_MAX_OUTPUT_TOKENS=800 # model-dependent cap LLM_TIMEOUT_S=30 # HTTP timeout (seconds) LLM_DISABLE=0 # 1 disables external calls # Logging LOG_LEVEL=INFO # DEBUG|INFO|WARNING|ERROR """ from __future__ import annotations import os from functools import lru_cache from typing import List, Optional class Settings: """Centralized settings loaded from environment variables.""" # Environment ENV: str # Database MONGO_URI: Optional[str] MONGO_DB: str REDIS_URL: Optional[str] # LLM Configuration LLM_PROVIDER: str LLM_MODEL_ROUTER: str LLM_MODEL_QA: str LLM_MODEL_TRANSLATE: str # Auth OPENAI_API_KEY: Optional[str] ANTHROPIC_API_KEY: Optional[str] # Security ALLOWED_ORIGINS: List[str] MAX_QUERY_CHARS: int # LLM Behavior TEMPERATURE: float MAX_OUTPUT_TOKENS: int TIMEOUT_S: float DISABLE_LLM: bool # Logging LOG_LEVEL: str def __init__(self) -> None: """Load all settings from environment variables.""" self.ENV = os.getenv("ENV", "dev") # Database self.MONGO_URI = os.getenv("MONGO_URI") self.MONGO_DB = os.getenv("MONGO_DB", os.getenv("MONGO_DB_NAME", "grant_analyst")) self.REDIS_URL = os.getenv("REDIS_URL") # LLM Provider & Models self.LLM_PROVIDER = os.getenv("LLM_PROVIDER", "openai") base_model = os.getenv("LLM_MODEL", "gpt-5-mini") self.LLM_MODEL_ROUTER = os.getenv("LLM_MODEL_ROUTER", os.getenv("LLM_MODEL", "gpt-5-nano")) self.LLM_MODEL_QA = os.getenv("LLM_MODEL_QA", base_model) self.LLM_MODEL_TRANSLATE = os.getenv("LLM_MODEL_TRANSLATE", base_model) # Auth self.OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") self.ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY") # Security - CORS allowed = os.getenv("ALLOWED_ORIGINS", "") if allowed: self.ALLOWED_ORIGINS = [o.strip() for o in allowed.split(",") if o.strip()] else: # Dev-safe default; override in prod self.ALLOWED_ORIGINS = ["*"] if self.ENV == "dev" else [] # Security - Limits self.MAX_QUERY_CHARS = int(os.getenv("MAX_QUERY_CHARS", "4000")) # LLM Behavior self.TEMPERATURE = float(os.getenv("LLM_TEMPERATURE", "0.2")) self.MAX_OUTPUT_TOKENS = int(os.getenv("LLM_MAX_OUTPUT_TOKENS", "800")) self.TIMEOUT_S = float(os.getenv("LLM_TIMEOUT_S", "30")) self.DISABLE_LLM = os.getenv("LLM_DISABLE", "0") == "1" # Logging self.LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO") # Validate critical settings self._validate() def _validate(self) -> None: """Validate critical configuration.""" # Only OpenAI is fully supported if self.LLM_PROVIDER != "openai": raise ValueError( f"Unsupported LLM_PROVIDER: {self.LLM_PROVIDER}. " "Only 'openai' is currently supported." ) # Auth validation (skip if LLM is disabled) if not self.DISABLE_LLM: if self.LLM_PROVIDER == "openai" and not self.OPENAI_API_KEY: raise ValueError("OPENAI_API_KEY is required when LLM_PROVIDER=openai") if self.LLM_PROVIDER == "anthropic" and not self.ANTHROPIC_API_KEY: raise ValueError("ANTHROPIC_API_KEY is required when LLM_PROVIDER=anthropic") # Range validation if not (0.0 <= self.TEMPERATURE <= 2.0): raise ValueError("LLM_TEMPERATURE must be between 0 and 2") if self.MAX_OUTPUT_TOKENS <= 0: raise ValueError("LLM_MAX_OUTPUT_TOKENS must be > 0") if self.TIMEOUT_S <= 0: raise ValueError("LLM_TIMEOUT_S must be > 0") if self.MAX_QUERY_CHARS <= 0: raise ValueError("MAX_QUERY_CHARS must be > 0") # Security warning for production if self.ENV != "dev" and not self.ALLOWED_ORIGINS: import logging logging.warning( "ALLOWED_ORIGINS not set in production environment. " "CORS will be disabled for security." ) @lru_cache(maxsize=1) def get_settings() -> Settings: """Get cached settings instance (singleton pattern).""" return Settings() # Legacy compatibility: load_config() for existing code def load_config(): """ Legacy function for backward compatibility. Returns a dict-like object with config values. DEPRECATED: Use get_settings() instead. """ import warnings warnings.warn( "load_config() is deprecated. Use get_settings() instead.", DeprecationWarning, stacklevel=2 ) settings = get_settings() # Create a simple namespace object that acts like the old Config dataclass class LegacyConfig: def __init__(self, s: Settings): self.provider = s.LLM_PROVIDER self.model = s.LLM_MODEL_QA self.openai_api_key = s.OPENAI_API_KEY self.anthropic_api_key = s.ANTHROPIC_API_KEY self.model_router = s.LLM_MODEL_ROUTER self.model_translator = s.LLM_MODEL_TRANSLATE self.model_analyzer = s.LLM_MODEL_QA # Use QA model for analysis self.temperature = s.TEMPERATURE self.max_output_tokens = s.MAX_OUTPUT_TOKENS self.timeout_s = s.TIMEOUT_S self.disable_llm = s.DISABLE_LLM self.log_level = s.LOG_LEVEL return LegacyConfig(settings) # Quick self-test when run directly if __name__ == "__main__": try: s = get_settings() print("Settings loaded successfully:") print(f" ENV: {s.ENV}") print(f" LLM_PROVIDER: {s.LLM_PROVIDER}") print(f" LLM_MODEL_QA: {s.LLM_MODEL_QA}") print(f" MONGO_DB: {s.MONGO_DB}") print(f" ALLOWED_ORIGINS: {s.ALLOWED_ORIGINS}") except Exception as e: print(f"Config error: {e}")