HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1"""Runtime settings (environment variables, `AIA_` prefix). Never log `settings.model_dump()` — it contains secrets."""2from __future__ import annotations34from functools import lru_cache5from pathlib import Path67from pydantic import Field8from pydantic_settings import BaseSettings, SettingsConfigDict91011class Settings(BaseSettings):12 model_config = SettingsConfigDict(env_file=(".env",), env_file_encoding="utf-8", extra="ignore")1314 app_env: str = Field("development", alias="APP_ENV")15 site_url: str = Field("https://www.ai-atlas.co", alias="AIA_SITE_URL")1617 database_url: str = Field("postgresql+asyncpg://aiatlas:aiatlas@127.0.0.1:5432/aiatlas", alias="DATABASE_URL")18 redis_url: str = Field("redis://127.0.0.1:6379/5", alias="REDIS_URL")1920 data_dir: Path = Field(Path("./data"), alias="AIA_DATA_DIR")21 api_host: str = Field("127.0.0.1", alias="AIA_API_HOST")22 api_port: int = Field(8321, alias="AIA_API_PORT")23 admin_token: str = Field("", alias="AIA_ADMIN_TOKEN")24 log_json: bool = Field(True, alias="AIA_LOG_JSON")25 tz: str = Field("America/Toronto", alias="AIA_TZ")2627 # Crawler28 user_agent: str = Field("AIAtlasBot/0.1 (+https://www.ai-atlas.co/bot; contact@spboucher.ai)", alias="AIA_USER_AGENT")29 http_timeout_s: float = Field(45.0, alias="AIA_HTTP_TIMEOUT")30 max_body_bytes: int = Field(25 * 1024 * 1024, alias="AIA_MAX_BODY_BYTES")31 default_rate_per_min: int = Field(30, alias="AIA_DEFAULT_RATE_PER_MIN")32 respect_robots: bool = Field(True, alias="AIA_RESPECT_ROBOTS")33 fetch_concurrency: int = Field(8, alias="AIA_FETCH_CONCURRENCY")3435 # Escalation transports (optional; never required)36 scrapfly_api_key: str = Field("", alias="SCRAPFLY_API_KEY")37 firecrawl_api_key: str = Field("", alias="FIRECRAWL_API_KEY")38 browser_enabled: bool = Field(False, alias="AIA_BROWSER_ENABLED")3940 # Local LLM factory (OpenAI-compatible endpoint, e.g. MacLustr llm-api.io). Optional.41 llm_base_url: str = Field("", alias="AIA_LLM_BASE_URL")42 llm_api_key: str = Field("", alias="AIA_LLM_API_KEY")43 llm_small_model: str = Field("qwen3-4b-instruct-2507-4bit", alias="AIA_LLM_SMALL_MODEL")44 llm_medium_model: str = Field("qwen3.6-35b-a3b-4bit", alias="AIA_LLM_MEDIUM_MODEL")45 llm_large_model: str = Field("qwen3.8-27b-4bit", alias="AIA_LLM_LARGE_MODEL")46 llm_timeout_s: float = Field(600.0, alias="AIA_LLM_TIMEOUT")47 llm_enabled: bool = Field(True, alias="AIA_LLM_ENABLED")48 embedding_model: str = Field("qwen3-embedding-0.6b-8bit", alias="AIA_EMBEDDING_MODEL")49 embedding_dim: int = Field(1024, alias="AIA_EMBEDDING_DIM")5051 # Scheduler52 scheduler_tick_s: int = Field(30, alias="AIA_SCHEDULER_TICK_S")53 worker_concurrency: int = Field(4, alias="AIA_WORKER_CONCURRENCY")54 backup_cron: str = Field("40 4 * * *", alias="AIA_BACKUP_CRON")5556 @property57 def raw_dir(self) -> Path:58 return self.data_dir / "raw"5960 @property61 def text_dir(self) -> Path:62 return self.data_dir / "text"6364 @property65 def logs_dir(self) -> Path:66 return self.data_dir / "logs"6768 @property69 def backups_dir(self) -> Path:70 return self.data_dir / "backups"7172 @property73 def cache_dir(self) -> Path:74 return self.data_dir / "cache"7576 @property77 def sync_database_url(self) -> str:78 return self.database_url.replace("+asyncpg", "")7980 @property81 def llm_available(self) -> bool:82 return bool(self.llm_enabled and self.llm_base_url and self.llm_api_key)8384 def ensure_dirs(self) -> None:85 for d in (self.raw_dir, self.text_dir, self.logs_dir, self.backups_dir, self.cache_dir, self.data_dir / "seed"):86 d.mkdir(parents=True, exist_ok=True)878889@lru_cache90def get_settings() -> Settings:91 return Settings()929394settings = get_settings()95