"""Application configuration loaded from env, .env, and TOML file via pydantic-settings.""" from __future__ import annotations from pathlib import Path from typing import Literal from platformdirs import PlatformDirs from pydantic import Field, ValidationError, field_validator from pydantic_settings import ( BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict, TomlConfigSettingsSource, ) from .enums import ErrorClass from .errors import MyDeepAgentError _DIRS = PlatformDirs("my-deepagent", "user", roaming=False) class Config(BaseSettings): """Frozen application config. Source priority (high -> low): CLI/env, .env, TOML, defaults.""" model_config = SettingsConfigDict( env_prefix="MYDEEPAGENT_", env_file=".env", env_file_encoding="utf-8", toml_file=Path(_DIRS.user_config_dir) / "config.toml", frozen=True, extra="ignore", ) # storage database_url: str = Field( default_factory=lambda: ( f"sqlite+aiosqlite:///{Path(_DIRS.user_data_dir) / 'database.sqlite3'}" ) ) workspace_root: Path = Field(default_factory=Path.cwd) data_dir: Path = Field(default_factory=lambda: Path(_DIRS.user_data_dir)) config_dir: Path = Field(default_factory=lambda: Path(_DIRS.user_config_dir)) state_dir: Path = Field(default_factory=lambda: Path(_DIRS.user_state_dir)) # logging / i18n log_level: Literal["trace", "debug", "info", "warn", "error"] = "info" lang: Literal["ko", "en"] = "ko" # providers openrouter_api_key: str | None = None openrouter_base_url: str = "https://openrouter.ai/api/v1" # observability langsmith_tracing: bool = False langsmith_api_key: str | None = None langsmith_project: str = "my-deepagent" # budget budget_daily_usd: float = Field(default=5.0, ge=0) budget_daily_warn_usd: float = Field(default=3.0, ge=0) budget_run_usd: float = Field(default=1.0, ge=0) budget_run_warn_usd: float = Field(default=0.5, ge=0) budget_on_hit: Literal["prompt", "block", "warn_continue"] = "prompt" # defaults default_persona: str = "default-interactive" @field_validator("workspace_root", "data_dir", "config_dir", "state_dir") @classmethod def _expand(cls, v: Path) -> Path: return Path(v).expanduser().resolve() @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: # priority: init > env > dotenv > toml > defaults return ( init_settings, env_settings, dotenv_settings, TomlConfigSettingsSource(settings_cls), file_secret_settings, ) def load_config(**overrides: object) -> Config: """Load Config with optional kwargs override. Wraps pydantic ValidationError in MyDeepAgentError(fatal, config_invalid) per plan ยง18. """ try: return Config(**overrides) # type: ignore[arg-type] except ValidationError as e: raise MyDeepAgentError( ErrorClass.FATAL, "config_invalid", message=f"config validation failed: {e}", recovery_hint=( "check .env, environment variables, and ~/.config/my-deepagent/config.toml" ), cause=e, ) from e