""" Global application configuration. Following best practice of splitting config across domains. """ from enum import Enum from functools import lru_cache from pathlib import Path from pydantic import Field, HttpUrl from pydantic_settings import BaseSettings, SettingsConfigDict def _get_version_from_pyproject() -> str: """ Load version from pyproject.toml. Falls back to "unknown" if file cannot be read. """ try: # Find pyproject.toml relative to this file config_dir = Path(__file__).parent pyproject_path = config_dir.parent.parent / "pyproject.toml" if pyproject_path.exists(): content = pyproject_path.read_text() for line in content.splitlines(): if line.strip().startswith("version"): # Parse: version = "1.0.0" return line.split("=", 1)[1].strip().strip('"').strip("'") except Exception: pass return "unknown" class Environment(str, Enum): """Application environment.""" DEVELOPMENT = "development" PRODUCTION = "production" TESTING = "testing" class Config(BaseSettings): """ Global application configuration. Loads from environment variables and .env file. Domain-specific configs should be in their respective modules. """ model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", case_sensitive=True, extra="ignore", ) # Application APP_NAME: str = "OpenAI-Compatible API" APP_VERSION: str = Field(default_factory=_get_version_from_pyproject) ENVIRONMENT: Environment = Environment.DEVELOPMENT DEBUG: bool = Field(default=False, description="Debug mode") # API Configuration API_HOST: str = Field(default="0.0.0.0", description="API host") API_PORT: int = Field(default=8000, description="API port") API_PREFIX: str = Field(default="/v1", description="API route prefix") # Anthropic Configuration (Claude - preferred backend) ANTHROPIC_API_KEY: str | None = Field( default=None, description="Anthropic API key for Claude access" ) ANTHROPIC_MODEL: str = Field( default="claude-sonnet-4-20250514", description="Claude model to use" ) PREFER_CLOUD_BACKEND: bool = Field( default=True, description="Prefer Claude over Ollama when available" ) # Ollama Configuration (local fallback) OLLAMA_HOST: HttpUrl = Field( default="http://localhost:11434", description="Ollama server URL" ) OLLAMA_DEFAULT_MODEL: str = Field( default="mistral-nemo:latest", description="Default Ollama model" ) OLLAMA_TIMEOUT: int = Field( default=120, description="Ollama request timeout in seconds" ) STREAM_TIMEOUT: int = Field( default=20, description="Timeout for each streaming turn in seconds" ) # SearXNG Configuration SEARXNG_HOST: HttpUrl = Field( default="http://localhost:8087", description="SearXNG server URL" ) SEARXNG_TIMEOUT: int = Field( default=30, description="SearXNG request timeout in seconds" ) # Redis Configuration REDIS_HOST: str = Field( default="localhost", description="Redis server host" ) REDIS_PORT: int = Field( default=6379, description="Redis server port" ) REDIS_TIMEOUT: int = Field( default=5, description="Redis connection timeout in seconds" ) # Library-Desk Configuration (The Librarian backend) LIBRARY_DESK_HOST: HttpUrl = Field( default="http://localhost:8089", description="Library-Desk API URL" ) LIBRARY_DESK_API_KEY: str = Field( default="", description="API key for Library-Desk authentication" ) LIBRARY_DESK_TIMEOUT: int = Field( default=60, description="Library-Desk request timeout in seconds" ) # Core-API Configuration (The Housekeeper backend) CORE_API_HOST: HttpUrl = Field( default="http://localhost:8090", description="Core-API URL for Home Assistant integration" ) CORE_API_KEY: str = Field( default="", description="API key for Core-API authentication" ) CORE_API_TIMEOUT: int = Field( default=30, description="Core-API request timeout in seconds" ) # Qdrant Configuration (Memory vector storage) QDRANT_HOST: str = Field( default="localhost", description="Qdrant server host" ) QDRANT_PORT: int = Field( default=6333, description="Qdrant server port" ) QDRANT_EMBEDDING_DIM: int = Field( default=768, description="Embedding dimension (768 for nomic-embed-text)" ) # Ollama Embedding Configuration OLLAMA_EMBEDDING_MODEL: str = Field( default="nomic-embed-text", description="Ollama model for embeddings" ) # Redis Memory Database REDIS_MEMORY_DB: int = Field( default=1, description="Redis database number for memory cache" ) REDIS_MEMORY_TTL_HOURS: int = Field( default=24, description="TTL for session context in hours" ) # Logging LOG_LEVEL: str | None = Field( default=None, description="Logging level (auto-set based on environment if not specified)" ) # User Configuration DEFAULT_USER: str | None = Field( default=None, description="Default user for single-user setup (auto-set based on environment if not specified)" ) # CORS CORS_ORIGINS: list[str] = Field( default=["*"], description="Allowed CORS origins" ) CORS_ALLOW_CREDENTIALS: bool = True CORS_ALLOW_METHODS: list[str] = ["*"] CORS_ALLOW_HEADERS: list[str] = ["*"] @property def redis_memory_url(self) -> str: """Construct Redis connection URL for memory cache.""" return f"redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_MEMORY_DB}" @property def qdrant_url(self) -> str: """Construct Qdrant server URL.""" return f"http://{self.QDRANT_HOST}:{self.QDRANT_PORT}" @property def log_format(self) -> str: """ Determine log format based on environment. - production: JSON format for machine parsing - development/testing: Console format for human readability """ return "json" if self.ENVIRONMENT == Environment.PRODUCTION else "console" @property def effective_log_level(self) -> str: """ Get effective log level, auto-determining from environment if not set. - development: DEBUG (maximum verbosity) - production: WARNING (minimal noise) - testing: INFO """ if self.LOG_LEVEL is not None: return self.LOG_LEVEL if self.ENVIRONMENT == Environment.DEVELOPMENT: return "DEBUG" if self.ENVIRONMENT == Environment.PRODUCTION: return "WARNING" return "INFO" @property def effective_default_user(self) -> str: """ Get effective default user, auto-determining from environment if not set. - development/testing: llm_tester (isolated test scope) - production: jpmschweitzer (real user) """ if self.DEFAULT_USER is not None: return self.DEFAULT_USER if self.ENVIRONMENT == Environment.PRODUCTION: return "jpmschweitzer" return "llm_tester" @lru_cache def get_config() -> Config: """ Get cached configuration instance. Uses lru_cache to ensure config is loaded once and reused. """ return Config() # Global config instance config = get_config()