Spaces:
Running
Running
| """Configuration management for the project. | |
| Loads settings from environment variables and .env files, with support for | |
| different environments (dev/prod). See README.md for configuration options. | |
| """ | |
| import os | |
| import logging | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| from dotenv import load_dotenv | |
| class ColoredFormatter(logging.Formatter): | |
| """Custom formatter with colored log levels""" | |
| # ANSI color codes as class-level constants | |
| # ALL_CAPS naming convention indicates these are constants | |
| ANSI_COLORS = { | |
| "DEBUG": "\033[36m", # Cyan | |
| "INFO": "\033[32m", # Green | |
| "WARNING": "\033[33m", # Yellow | |
| "ERROR": "\033[31m", # Red | |
| "CRITICAL": "\033[35m", # Magenta | |
| "RESET": "\033[0m", # Reset | |
| } | |
| def __init__(self, fmt=None, datefmt=None): | |
| super().__init__(fmt, datefmt) | |
| def format(self, record): | |
| # Add color to levelname | |
| if record.levelname in self.ANSI_COLORS: | |
| record.levelname = f"{self.ANSI_COLORS[record.levelname]}{record.levelname}{self.ANSI_COLORS['RESET']}" | |
| return super().format(record) | |
| class EnvironmentConfig: | |
| """Configuration for environment settings.""" | |
| name: str | |
| file: str | |
| class LoggingConfig: | |
| """Configuration for logging settings.""" | |
| level: str | |
| format: str | |
| class APIConfig: | |
| """Configuration for API settings.""" | |
| title: str | |
| host: str | |
| port: int | |
| debug: bool | |
| class Config: | |
| """Manages application configuration and logging setup. | |
| Provides typed access to configuration through dataclasses and | |
| automatically sets up logging based on the environment. | |
| """ | |
| def __init__(self, environment: str = None): | |
| # Initialize environment config first | |
| self._init_environment(environment) | |
| # Load environment variables | |
| self._load_env_file() | |
| # Load all other configs | |
| self._load_config() | |
| # Set up logging AFTER loading config | |
| self._setup_logging() | |
| # Now we can log safely | |
| self.logger.info("Configuration loaded for environment: %s", self.env.name) | |
| if os.path.exists(self.env.file): | |
| self.logger.info("Using environment file: %s", self.env.file) | |
| else: | |
| self.logger.warning( | |
| "Environment file %s not found, using default .env", self.env.file | |
| ) | |
| def _init_environment(self, environment: str = None): | |
| """Initialize environment configuration""" | |
| env_name = ( | |
| environment | |
| or os.getenv("APP_ENV") | |
| or os.getenv("ENVIRONMENT", "development") | |
| ) | |
| self.env = EnvironmentConfig(name=env_name, file=f".env.{env_name}") | |
| def _load_env_file(self): | |
| """Load environment variables from the appropriate file | |
| NOTE: override=False means Docker/system environment variables take precedence | |
| This is critical for containerized deployments where docker-compose sets DATABASE_URL, etc. | |
| Order of precedence (highest to lowest): | |
| 1. System/Docker environment variables (e.g., from docker-compose) | |
| 2. .env files (loaded here as defaults only) | |
| """ | |
| if os.path.exists(self.env.file): | |
| load_dotenv(self.env.file, override=False) | |
| else: | |
| # Fall back to default .env | |
| load_dotenv(".env", override=False) | |
| def _load_config(self): | |
| """Load all configuration values from environment variables""" | |
| # API Configuration | |
| self.api = APIConfig( | |
| title=os.getenv("API_TITLE", "Smart Chatbot API"), | |
| host=os.getenv("API_HOST", "127.0.0.1"), | |
| port=int(os.getenv("API_PORT", "8000")), | |
| debug=os.getenv("API_DEBUG", "false").lower() == "true", | |
| ) | |
| # CORS Configuration | |
| cors_origins = os.getenv("CORS_ORIGINS", "*") | |
| # Middleware Configuration | |
| self.middleware = { | |
| "enable_request_logging": os.getenv( | |
| "ENABLE_REQUEST_LOGGING", "false" | |
| ).lower() | |
| == "true", # Request Logging Configuration | |
| "cors_origins": [origin.strip() for origin in cors_origins.split(",")], | |
| } | |
| # Frontend Configuration | |
| self.frontend = { | |
| "backend_url": os.getenv("BACKEND_API_URL", "http://127.0.0.1:8000") | |
| } | |
| # NLP Configuration | |
| self.nlp = { | |
| "confidence_threshold": float(os.getenv("CONFIDENCE_THRESHOLD", "0.5")), | |
| "max_history": int(os.getenv("MAX_CONVERSATION_HISTORY", "50")), | |
| "enable_debug": os.getenv("ENABLE_DEBUG_INFO", "false").lower() == "true", | |
| } | |
| # Response Configuration | |
| self.response = { | |
| "default_language": os.getenv("DEFAULT_LANGUAGE", "en"), | |
| "enable_fallback": os.getenv("ENABLE_FALLBACK_RESPONSES", "true").lower() | |
| == "true", | |
| "delay": float(os.getenv("RESPONSE_DELAY", "0")), | |
| } | |
| # Logging Configuration | |
| self.logging = LoggingConfig( | |
| level=os.getenv("LOG_LEVEL", "INFO").upper(), | |
| format=( | |
| "%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s" | |
| if self.env.name == "development" | |
| else "%(asctime)s - %(levelname)s - %(message)s" | |
| ), | |
| ) | |
| # Application Credential Configuration | |
| self.llm_api_key = os.getenv("GROQ_API_KEY", "") | |
| self.demo_api_key = os.getenv("DEMO_API_KEY", "") | |
| # LLM Configuration | |
| self.system_prompt = os.getenv("SYSTEM_PROMPT", "") | |
| # Database Configuration | |
| self.database_url = os.getenv("DATABASE_URL", "") | |
| def _setup_logging(self): | |
| """Configure logging for the entire application""" | |
| # Create logger | |
| self.logger = logging.getLogger("chatbot") | |
| # Prevent duplicate handles if config is reloaded | |
| if self.logger.handlers: | |
| self.logger.handlers.clear() | |
| # Set log level | |
| log_level = getattr(logging, self.logging.level, logging.INFO) | |
| self.logger.setLevel(log_level) | |
| # Create console handler | |
| console_handler = logging.StreamHandler() | |
| console_handler.setLevel(log_level) | |
| # Create formatter | |
| formatter = ( | |
| ColoredFormatter(self.logging.format) | |
| if self.env.name == "development" | |
| else logging.Formatter(self.logging.format) | |
| ) | |
| console_handler.setFormatter(formatter) | |
| self.logger.addHandler(console_handler) | |
| # Configure root logger to avoid duplicate logs | |
| root_logger = logging.getLogger() | |
| root_logger.setLevel(log_level) | |
| # Remove default handlers to avoid duplicates | |
| for handler in root_logger.handlers[:]: | |
| root_logger.removeHandler(handler) | |
| def get_logger(self, name: str = None): | |
| """Get a logger instance for use in other modules""" | |
| if name: | |
| return logging.getLogger(f"chatbot.{name}") | |
| return logging.getLogger("chatbot") | |
| class ConfigManager: | |
| """Singleton configuration manager""" | |
| def __init__(self): | |
| self.config = None | |
| def get_config(self, environment: str = None) -> Config: | |
| """Get the configuration instance (creates if doesn't exist)""" | |
| if self.config is None: | |
| self.config = Config(environment) | |
| return self.config | |
| def get_logger(self, name: str = None): | |
| """Get a logger instance from the configuration""" | |
| return self.get_config().get_logger(name) | |
| # Create single instance of ConfigManager | |
| config_manager = ConfigManager() | |
| # Convenience functions for easier imports | |
| def get_config(environment: str = None) -> Config: | |
| """Get the global configuration instance""" | |
| return config_manager.get_config(environment) | |
| def get_logger(name: str = None): | |
| """Get a logger instance from anywhere in the app""" | |
| return config_manager.get_logger(name) | |
| def find_project_root() -> Path: | |
| """Find the project root directory by walking up until we find pyproject.toml | |
| This works regardless of where the code is executed from: | |
| - If running from /app/main.py → walks up to / | |
| - If running from /tests/ → walks up to / | |
| - If file structure changes → still finds root | |
| Returns: | |
| Path: Absolute path to project root directory | |
| Raises: | |
| FileNotFoundError: If pyproject.toml cannot be found | |
| """ | |
| current = Path(__file__).resolve() # Start from this file's location | |
| # Walk up parent directories | |
| for parent in [current, *current.parents]: | |
| if (parent / "pyproject.toml").exists(): | |
| return parent | |
| raise FileNotFoundError("Could not find pyproject.toml in any parent directory") | |
| # Cache version to avoid reading file repeatedly | |
| _cached_version = None | |
| def get_version() -> str: | |
| """Get application version from pyproject.toml | |
| Uses caching so the file is only read once during runtime. | |
| Can be called from anywhere in the application. | |
| Returns: | |
| str: Version string (e.g., "0.1.0") | |
| Example: | |
| >>> from app.utils.config import get_version | |
| >>> version = get_version() | |
| >>> print(version) # "0.1.0" | |
| """ | |
| global _cached_version | |
| if _cached_version is not None: | |
| return _cached_version | |
| # Find project root and read pyproject.toml | |
| try: | |
| import tomllib # Built into Python 3.11+ | |
| except ImportError: | |
| import tomli as tomllib # Fallback for Python < 3.11 | |
| root = find_project_root() | |
| pyproject_path = root / "pyproject.toml" | |
| with open(pyproject_path, "rb") as f: | |
| data = tomllib.load(f) | |
| _cached_version = data["project"]["version"] | |
| return _cached_version | |