"""
Centralized configuration management using Pydantic Settings.
This module provides type-safe configuration loaded from environment variables.
All settings are validated at startup, preventing runtime configuration errors.
Key Benefits:
- Type safety with Pydantic validation
- Environment-specific overrides
- Clear documentation of all configuration options
- Fail-fast on missing required values
"""
from __future__ import annotations
from typing import Literal, Optional
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""
Application settings loaded from environment variables.
All values can be overridden via .env file or environment variables.
See .env.example for all available options.
"""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore", # Ignore unknown env vars
)
# =========================================================================
# Laravel API Configuration
# =========================================================================
laravel_api_url: str = Field(
default="http://localhost:8000",
description="Base URL for Laravel API endpoints"
)
# =========================================================================
# MCP Server Configuration
# =========================================================================
mcp_server_name: str = Field(
default="Updation MCP Server",
description="Display name for the MCP server"
)
mcp_server_host: str = Field(
default="0.0.0.0",
description="Host to bind MCP server"
)
mcp_server_port: int = Field(
default=8050,
ge=1024,
le=65535,
description="Port for MCP server"
)
mcp_transport: Literal["streamable-http", "stdio"] = Field(
default="streamable-http",
description="MCP transport protocol"
)
mcp_path: str = Field(
default="/mcp",
description="HTTP path for MCP endpoint"
)
# =========================================================================
# LLM Provider Configuration
# =========================================================================
llm_provider: Literal["openai", "anthropic", "google", "azure_openai"] = Field(
default="openai",
description="Active LLM provider"
)
# OpenAI
openai_api_key: Optional[str] = Field(
default=None,
description="OpenAI API key"
)
openai_model: str = Field(
default="gpt-4o",
description="OpenAI model name"
)
openai_temperature: float = Field(
default=0.0,
ge=0.0,
le=2.0,
description="OpenAI temperature"
)
openai_max_tokens: int = Field(
default=4096,
ge=1,
description="OpenAI max output tokens"
)
# Anthropic (Claude)
anthropic_api_key: Optional[str] = Field(
default=None,
description="Anthropic API key"
)
anthropic_model: str = Field(
default="claude-3-5-sonnet-20241022",
description="Anthropic model name"
)
anthropic_max_tokens: int = Field(
default=4096,
ge=1,
description="Anthropic max output tokens"
)
# Google (Gemini)
google_api_key: Optional[str] = Field(
default=None,
description="Google API key"
)
google_model: str = Field(
default="gemini-2.0-flash-exp",
description="Google model name"
)
google_max_tokens: int = Field(
default=4096,
ge=1,
description="Google max output tokens"
)
# Azure OpenAI
azure_openai_api_key: Optional[str] = Field(
default=None,
description="Azure OpenAI API key"
)
azure_openai_endpoint: Optional[str] = Field(
default=None,
description="Azure OpenAI endpoint URL"
)
azure_openai_deployment: Optional[str] = Field(
default=None,
description="Azure OpenAI deployment name"
)
azure_openai_api_version: str = Field(
default="2024-02-15-preview",
description="Azure OpenAI API version"
)
# =========================================================================
# Backend API Configuration (Updation Laravel API)
# =========================================================================
updation_api_base_url: str = Field(
default="http://127.0.0.1:8000/api",
description="Base URL for Updation API"
)
updation_api_timeout: float = Field(
default=15.0,
ge=1.0,
description="HTTP timeout for Updation API calls"
)
updation_api_user_agent: str = Field(
default="Updation-MCP/1.0",
description="User agent for API requests"
)
# =========================================================================
# Redis Configuration
# =========================================================================
redis_url: str = Field(
default="redis://localhost:6379/0",
description="Redis connection URL"
)
redis_enabled: bool = Field(
default=True,
description="Enable Redis for distributed state"
)
redis_ttl_conversations: int = Field(
default=1800,
ge=60,
description="TTL for conversation history (seconds)"
)
redis_ttl_cache: int = Field(
default=600,
ge=10,
description="TTL for general cache (seconds)"
)
# =========================================================================
# Web Chat API Configuration
# =========================================================================
web_chat_host: str = Field(
default="0.0.0.0",
description="Host to bind web chat API"
)
web_chat_port: int = Field(
default=8002,
ge=1024,
le=65535,
description="Port for web chat API"
)
web_chat_reload: bool = Field(
default=False,
description="Enable auto-reload for development"
)
cors_origins: list[str] = Field(
default=["http://localhost:3000"],
description="Allowed CORS origins"
)
# =========================================================================
# Observability
# =========================================================================
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = Field(
default="INFO",
description="Logging level"
)
log_format: Literal["json", "console"] = Field(
default="json",
description="Log output format"
)
log_file: Optional[str] = Field(
default=None,
description="Log file path (optional)"
)
metrics_enabled: bool = Field(
default=True,
description="Enable Prometheus metrics"
)
metrics_port: int = Field(
default=9090,
ge=1024,
le=65535,
description="Port for metrics endpoint"
)
sentry_dsn: Optional[str] = Field(
default=None,
description="Sentry DSN for error tracking"
)
sentry_environment: str = Field(
default="development",
description="Sentry environment name"
)
# =========================================================================
# Security
# =========================================================================
rate_limit_enabled: bool = Field(
default=True,
description="Enable rate limiting"
)
rate_limit_per_minute: int = Field(
default=60,
ge=1,
description="Max requests per minute per user"
)
rate_limit_per_hour: int = Field(
default=1000,
ge=1,
description="Max requests per hour per user"
)
jwt_secret: str = Field(
default="change-this-in-production",
min_length=32,
description="JWT secret for internal auth"
)
# =========================================================================
# Feature Flags
# =========================================================================
enable_rag: bool = Field(
default=False,
description="Enable RAG (Retrieval Augmented Generation)"
)
enable_caching: bool = Field(
default=True,
description="Enable response caching"
)
enable_conversation_memory: bool = Field(
default=True,
description="Enable conversation history"
)
enable_sampling: bool = Field(
default=True,
description="Enable MCP sampling for client-side formatting"
)
# =========================================================================
# MCP Sampling Configuration
# =========================================================================
sampling_max_tokens: int = Field(
default=2000,
ge=100,
le=8000,
description="Maximum tokens for sampling responses"
)
sampling_temperature: float = Field(
default=0.0,
ge=0.0,
le=1.0,
description="Default temperature for sampling (0.0 = deterministic)"
)
sampling_default_format: str = Field(
default="markdown",
description="Default format for sampling responses"
)
# =========================================================================
# Environment
# =========================================================================
debug: bool = Field(
default=False,
description="Enable debug mode"
)
environment: Literal["development", "staging", "production"] = Field(
default="development",
description="Deployment environment"
)
# =========================================================================
# Validators
# =========================================================================
@field_validator("cors_origins", mode="before")
@classmethod
def parse_cors_origins(cls, v):
"""Parse comma-separated CORS origins from env var."""
if isinstance(v, str):
return [origin.strip() for origin in v.split(",") if origin.strip()]
return v
@field_validator("openai_api_key", "anthropic_api_key", "google_api_key")
@classmethod
def validate_api_key(cls, v, info):
"""Validate that API key is set for the active provider."""
# This runs after all fields are set, so we can't check llm_provider here
# We'll do provider-specific validation in a separate method
return v
def validate_provider_config(self) -> None:
"""
Validate that the active LLM provider has required configuration.
Call this after settings are loaded to ensure provider is properly configured.
Raises:
ValueError: If required configuration is missing for active provider
"""
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")
if self.llm_provider == "google" and not self.google_api_key:
raise ValueError("GOOGLE_API_KEY is required when LLM_PROVIDER=google")
if self.llm_provider == "azure_openai":
if not all([self.azure_openai_api_key, self.azure_openai_endpoint, self.azure_openai_deployment]):
raise ValueError(
"AZURE_OPENAI_API_KEY, AZURE_OPENAI_ENDPOINT, and AZURE_OPENAI_DEPLOYMENT "
"are required when LLM_PROVIDER=azure_openai"
)
@property
def is_production(self) -> bool:
"""Check if running in production environment."""
return self.environment == "production"
@property
def is_development(self) -> bool:
"""Check if running in development environment."""
return self.environment == "development"
# ============================================================================
# Global Settings Instance
# ============================================================================
# Create singleton settings instance
# This will be imported throughout the application
settings = Settings()
# Validate provider configuration at startup
try:
settings.validate_provider_config()
except ValueError as e:
# Log error but don't crash - allow app to start for health checks
import sys
print(f"⚠️ Configuration Warning: {e}", file=sys.stderr)
print("⚠️ Some features may not work correctly.", file=sys.stderr)