""" Application configuration via Pydantic Settings. All settings loaded from environment variables or .env file. Project metadata (name, version, description) sourced from pyproject.toml. """ import tomllib from dataclasses import dataclass from functools import lru_cache from pathlib import Path from pydantic_settings import BaseSettings, SettingsConfigDict @dataclass(frozen=True) class ProjectMeta: """Project metadata from pyproject.toml (single source of truth).""" name: str version: str description: str def _load_project_meta() -> ProjectMeta: """Load project metadata from pyproject.toml.""" pyproject_path = Path(__file__).parent.parent.parent / "pyproject.toml" try: with open(pyproject_path, "rb") as f: data = tomllib.load(f) project = data.get("project", {}) return ProjectMeta( name=str(project.get("name", "webber")).title(), version=str(project.get("version", "0.0.0")), description=str(project.get("description", "")), ) except FileNotFoundError: return ProjectMeta(name="Webber", version="0.0.0", description="") PROJECT = _load_project_meta() __version__ = PROJECT.version class Settings(BaseSettings): """Application settings loaded from environment.""" # Application (from pyproject.toml) app_name: str = PROJECT.name app_version: str = PROJECT.version app_description: str = PROJECT.description debug: bool = False # Server host: str = "0.0.0.0" port: int = 8086 # Logging log_level: str = "INFO" # CORS cors_origins: list[str] = ["http://localhost:3000", "http://localhost:8080"] cors_credentials: bool = True cors_methods: list[str] = ["*"] cors_headers: list[str] = ["*"] # LLM - Ollama (always hot in VRAM on tower-of-joy) ollama_url: str = "http://192.168.86.149:11434" ollama_agent_model: str = "mistral-nemo-large:latest" ollama_embed_model: str = "nomic-embed-text:latest" # Auth - Tatlock integration tatlock_api_url: str | None = "http://tatlock:8000" internal_api_key: str | None = None # Web search - SearXNG (use SEARXNG_URL env var to override) searxng_url: str = "http://searxng:8080" searxng_timeout: int = 10 # Tool execution tool_timeout_seconds: int = 120 sandbox_enabled: bool = True allowed_paths: list[str] | None = None # Database database_url: str = "sqlite+aiosqlite:///./webber.db" # Sessions & Context session_ttl_hours: int = 24 max_context_tokens: int = 128000 summarization_threshold: float = 0.8 # Summarize at 80% of max tokens summarization_target_tokens: int = 500 # Target summary size keep_recent_messages: int = 6 # Messages to keep unsummarized (3 turns) # Retry logic retry_max_attempts: int = 3 # Max retry attempts for transient failures retry_base_delay: float = 1.0 # Base delay in seconds retry_max_delay: float = 30.0 # Maximum delay in seconds model_config = SettingsConfigDict( env_file=".env", case_sensitive=False, extra="ignore", env_parse_none_str="", # Treat empty string as None ) @property def effective_allowed_paths(self) -> list[str]: """Return allowed_paths or empty list if None.""" return self.allowed_paths or [] @lru_cache def get_settings() -> Settings: """Cached settings singleton.""" return Settings()