SPB Git

spb/uqo-chat

Public
4.8 KB · 147 lines python
1"""Application settings (env-driven). Model slugs are configuration, not code."""23from __future__ import annotations45from functools import lru_cache6from pathlib import Path78from pydantic import SecretStr, field_validator9from pydantic_settings import BaseSettings, SettingsConfigDict1011BASE_DIR = Path(__file__).resolve().parents[2]  # backend/12REPO_DIR = BASE_DIR.parent131415class Settings(BaseSettings):16    model_config = SettingsConfigDict(env_file=(REPO_DIR / ".env", BASE_DIR / ".env"), extra="ignore")1718    # --- App ---19    APP_ENV: str = "development"20    APP_NAME: str = "UQO-Chat"21    APP_URL: str = "https://www.uqo-chat.app"22    CORS_ORIGINS: str = "https://www.uqo-chat.app,http://localhost:5173,http://localhost:8190"23    LOG_LEVEL: str = "INFO"24    PORT: int = 819025    DATA_DIR: Path = REPO_DIR / "data"26    FRONTEND_DIST: Path = REPO_DIR / "frontend" / "dist"27    COURSES: str = "IMM1003,IMM1033"28    TERM_LABEL: str = "Automne 2026"2930    # --- LLM (OpenRouter) ---31    OPENROUTER_API_KEY: SecretStr = SecretStr("")32    OPENROUTER_BASE_URL: str = "https://openrouter.ai/api/v1"33    OPENROUTER_APP_NAME: str = "UQO-Chat"34    OPENROUTER_APP_URL: str = "https://www.uqo-chat.app"35    # Slugs — verify on https://openrouter.ai/models before changing (checked 2026-09-05).36    MODEL_TUTOR_PRIMARY: str = "anthropic/claude-sonnet-4.6"37    MODEL_TUTOR_FALLBACK: str = "openai/gpt-5.5"38    MODEL_REASONING: str = "anthropic/claude-opus-4.6"39    MODEL_FAST: str = "openai/gpt-5.4-nano"40    MODEL_VISION: str = "anthropic/claude-sonnet-4.6"41    # OpenRouter exposes no embedding model (2026-09): optional OpenAI-compatible endpoint.42    EMBEDDINGS_BASE_URL: str = ""43    EMBEDDINGS_API_KEY: SecretStr = SecretStr("")44    MODEL_EMBEDDINGS: str = ""45    LLM_MAX_TOOL_ITERATIONS: int = 846    LLM_TIMEOUT_SECONDS: int = 12047    LLM_MAX_OUTPUT_TOKENS: int = 409648    LLM_MONTHLY_BUDGET_USD: float = 300.049    LLM_TURN_TIMEOUT_SECONDS: int = 3005051    # --- Web search ---52    FIRECRAWL_API_KEY: SecretStr = SecretStr("")53    FIRECRAWL_BASE_URL: str = "https://api.firecrawl.dev/v1"54    WEB_SEARCH_CACHE_TTL_S: int = 6 * 36005556    # --- Data ---57    DATABASE_URL: str = ""  # default: sqlite in DATA_DIR58    REDIS_URL: str = ""  # optional; in-memory fallback5960    # --- Sandbox ---61    SANDBOX_URL: str = "http://127.0.0.1:8191"62    SANDBOX_TIMEOUT_S: int = 3063    SANDBOX_HEAVY_TIMEOUT_S: int = 6064    SANDBOX_TOKEN: SecretStr = SecretStr("")6566    # --- Auth ---67    JWT_SECRET: SecretStr = SecretStr("change-me-in-production")68    JWT_TTL_HOURS: int = 2469    REFRESH_TTL_DAYS: int = 3070    ALLOWED_EMAIL_DOMAINS: str = "uqo.ca"71    INVITED_EMAILS: str = ""72    PROFESSOR_EMAILS: str = ""73    ADMIN_EMAILS: str = ""74    ACCESS_CODE: str = ""  # course access code: lets users log in without SMTP75    SMTP_HOST: str = ""76    SMTP_PORT: int = 58777    SMTP_USER: str = ""78    SMTP_PASSWORD: SecretStr = SecretStr("")79    SMTP_FROM: str = "no-reply@uqo-chat.app"8081    # --- Limits ---82    RATE_MESSAGES_PER_HOUR: int = 6083    RATE_MESSAGES_PER_DAY: int = 40084    RATE_SANDBOX_PER_HOUR: int = 1085    RATE_WEBSEARCH_PER_DAY: int = 3086    RATE_UPLOADS_PER_DAY: int = 2087    UPLOAD_MAX_MB: int = 2088    FILE_TTL_DAYS: int = 789    FILE_PINNED_TTL_DAYS: int = 3090    MESSAGE_RETENTION_MONTHS: int = 129192    @field_validator("DATA_DIR", mode="before")93    @classmethod94    def _expand(cls, v: object) -> object:95        if isinstance(v, str):96            return Path(v).expanduser()97        return v9899    @property100    def database_url(self) -> str:101        if self.DATABASE_URL:102            return self.DATABASE_URL103        return f"sqlite+aiosqlite:///{self.DATA_DIR / 'uqochat.db'}"104105    @property106    def cors_origins(self) -> list[str]:107        return [o.strip() for o in self.CORS_ORIGINS.split(",") if o.strip()]108109    @property110    def courses(self) -> list[str]:111        return [c.strip().upper() for c in self.COURSES.split(",") if c.strip()]112113    def _csv_lower(self, value: str) -> set[str]:114        return {e.strip().lower() for e in value.split(",") if e.strip()}115116    @property117    def allowed_domains(self) -> set[str]:118        return self._csv_lower(self.ALLOWED_EMAIL_DOMAINS)119120    @property121    def invited_emails(self) -> set[str]:122        return self._csv_lower(self.INVITED_EMAILS)123124    @property125    def professor_emails(self) -> set[str]:126        return self._csv_lower(self.PROFESSOR_EMAILS)127128    @property129    def admin_emails(self) -> set[str]:130        return self._csv_lower(self.ADMIN_EMAILS)131132    @property133    def smtp_enabled(self) -> bool:134        return bool(self.SMTP_HOST)135136    @property137    def is_dev(self) -> bool:138        return self.APP_ENV != "production"139140141@lru_cache142def get_settings() -> Settings:143    s = Settings()144    s.DATA_DIR.mkdir(parents=True, exist_ok=True)145    (s.DATA_DIR / "files").mkdir(exist_ok=True)146    return s147
Files · afb5ca448565f356efc1532400a8ac8b8eba5d6a
Menu

Theme