""" Application configuration using Pydantic Settings. Loads configuration from environment variables and .env files. """ import os from pathlib import Path from pydantic import Field, field_validator from pydantic_settings import BaseSettings, SettingsConfigDict def get_backend_dir() -> Path: """Get the backend directory path.""" # Try to get from environment first if env_path := os.getenv("BACKEND_DIR"): return Path(env_path) # Fallback to script location return Path(__file__).parent.parent class Settings(BaseSettings): """Application settings loaded from environment variables.""" # Server Configuration host: str = Field(default="127.0.0.1", alias="HOST") port: int = Field(default=3002, alias="PORT") # CORS Configuration frontend_url: str = Field(default="http://localhost:3000", alias="FRONTEND_URL") frontend_urls: str = Field( default="http://localhost:3000", alias="FRONTEND_URLS", ) # SSE Configuration sse_flush_ms: int = Field(default=50, alias="SSE_FLUSH_MS") sse_heartbeat_ms: int = Field(default=10000, alias="SSE_HEARTBEAT_MS") # Supabase Configuration supabase_project_name: str = Field(default="", alias="SUPABASE_PROJECT_NAME") supabase_url: str = Field(default="", alias="SUPABASE_URL") supabase_password: str = Field(default="", alias="SUPABASE_PASSWORD") supabase_service_role_key: str = Field(default="", alias="SUPABASE_SERVICE_ROLE_KEY") supabase_db_url: str = Field(default="", alias="SUPABASE_DB_URL") # Tavily API (for web search) tavily_api_key: str = Field(default="", alias="TAVILY_API_KEY") # Jina AI (for webpage reading) jina_api_key: str = Field(default="", alias="JINA_API_KEY") # Debug Flags debug_stream: bool = Field(default=False, alias="DEBUG_STREAM") debug_tools: bool = Field(default=False, alias="DEBUG_TOOLS") debug_sources: bool = Field(default=False, alias="DEBUG_SOURCES") # Context Message Limit context_message_limit: int = Field(default=50, alias="CONTEXT_MESSAGE_LIMIT") # Database Providers (backend-managed) database_providers_json: str = Field(default="", alias="DATABASE_PROVIDERS") db_access_key: str = Field(default="", alias="DB_PROVIDER_ACCESS_KEY") database_provider: str = Field(default="", alias="DATABASE_PROVIDER") database_url: str = Field(default="", alias="DATABASE_URL") database_path: str = Field(default="", alias="DATABASE_PATH") database_label: str = Field(default="", alias="DATABASE_LABEL") # Session Summary Configuration summary_lite_provider: str = Field(default="openai", alias="SUMMARY_LITE_PROVIDER") summary_lite_model: str = Field(default="gpt-4o-mini", alias="SUMMARY_LITE_MODEL") summary_agent_api_key: str = Field(default="", alias="SUMMARY_AGENT_API_KEY") summary_lite_base_url: str | None = Field(default=None, alias="SUMMARY_LITE_BASE_URL") # Memory Lite Model Configuration (Legacy / Long Term Memory) memory_lite_provider: str = Field(default="openai", alias="MEMORY_LITE_PROVIDER") memory_lite_model: str = Field(default="gpt-4o-mini", alias="MEMORY_LITE_MODEL") memory_agent_api_key: str = Field(default="", alias="MEMORY_AGENT_API_KEY") memory_lite_base_url: str | None = Field(default=None, alias="MEMORY_LITE_BASE_URL") # Model configuration @property def allowed_origins(self) -> list[str]: """Parse frontend_urls into a list of allowed origins.""" return [origin.strip() for origin in self.frontend_urls.split(",") if origin.strip()] model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", env_ignore_empty=True, extra="ignore", ) @field_validator("sse_flush_ms", "sse_heartbeat_ms") @classmethod def validate_positive(cls, v: int) -> int: """Validate that numeric values are non-negative.""" return max(0, v) # Global settings instance _settings: Settings | None = None def get_settings() -> Settings: """Get the global settings instance (singleton).""" global _settings if _settings is None: if os.getenv("QURIO_ELECTRON", "0") == "1": _settings = Settings(_env_file=None) return _settings # Search paths for env files (priority order) src_dir = Path(__file__).parent backend_dir = src_dir.parent candidates = [ src_dir / ".env.local", backend_dir / ".env.local", src_dir / ".env", backend_dir / ".env", ] env_file = None for candidate in candidates: if candidate.exists(): env_file = str(candidate) break _settings = Settings(_env_file=env_file) return _settings def reload_settings() -> Settings: """Reload settings from environment variables.""" global _settings _settings = None return get_settings()