SPB Git forge

spb/uqo-chat

Public
14commits 1branches 0releases
1.4 MBsize
maindefault branch
17 days agolast push
Python 64.6% TypeScript 33.7% CSS 0.8%
5.3 KB · 161 lines python
Raw Blame History
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-fable-5.1"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 = 1246    LLM_TIMEOUT_SECONDS: int = 12047    LLM_MAX_OUTPUT_TOKENS: int = 1600048    LLM_MONTHLY_BUDGET_USD: float = 2000.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    INVITE_TTL_DAYS: int = 14  # "choose your password" link sent to newly added students75    RESET_TTL_MINUTES: int = 60  # "forgot my password" link76    # --- Mail (Resend first; SMTP kept as fallback) ---77    RESEND_API_KEY: SecretStr = SecretStr("")78    RESEND_BASE_URL: str = "https://api.resend.com"79    MAIL_FROM: str = "UQO-Chat <no-reply@uqo-chat.app>"80    MAIL_REPLY_TO: str = ""81    SMTP_HOST: str = ""82    SMTP_PORT: int = 58783    SMTP_USER: str = ""84    SMTP_PASSWORD: SecretStr = SecretStr("")85    SMTP_FROM: str = "no-reply@uqo-chat.app"8687    # --- Limits ---88    RATE_MESSAGES_PER_HOUR: int = 6089    RATE_MESSAGES_PER_DAY: int = 40090    RATE_SANDBOX_PER_HOUR: int = 1091    RATE_WEBSEARCH_PER_DAY: int = 3092    RATE_UPLOADS_PER_DAY: int = 2093    UPLOAD_MAX_MB: int = 2094    FILE_TTL_DAYS: int = 795    FILE_PINNED_TTL_DAYS: int = 3096    MESSAGE_RETENTION_MONTHS: int = 129798    @field_validator("DATA_DIR", mode="before")99    @classmethod100    def _expand(cls, v: object) -> object:101        if isinstance(v, str):102            return Path(v).expanduser()103        return v104105    @property106    def database_url(self) -> str:107        if self.DATABASE_URL:108            return self.DATABASE_URL109        return f"sqlite+aiosqlite:///{self.DATA_DIR / 'uqochat.db'}"110111    @property112    def cors_origins(self) -> list[str]:113        return [o.strip() for o in self.CORS_ORIGINS.split(",") if o.strip()]114115    @property116    def courses(self) -> list[str]:117        return [c.strip().upper() for c in self.COURSES.split(",") if c.strip()]118119    def _csv_lower(self, value: str) -> set[str]:120        return {e.strip().lower() for e in value.split(",") if e.strip()}121122    @property123    def allowed_domains(self) -> set[str]:124        return self._csv_lower(self.ALLOWED_EMAIL_DOMAINS)125126    @property127    def invited_emails(self) -> set[str]:128        return self._csv_lower(self.INVITED_EMAILS)129130    @property131    def professor_emails(self) -> set[str]:132        return self._csv_lower(self.PROFESSOR_EMAILS)133134    @property135    def admin_emails(self) -> set[str]:136        return self._csv_lower(self.ADMIN_EMAILS)137138    @property139    def smtp_enabled(self) -> bool:140        return bool(self.SMTP_HOST)141142    @property143    def resend_enabled(self) -> bool:144        return bool(self.RESEND_API_KEY.get_secret_value())145146    @property147    def mail_enabled(self) -> bool:148        return self.resend_enabled or self.smtp_enabled149150    @property151    def is_dev(self) -> bool:152        return self.APP_ENV != "production"153154155@lru_cache156def get_settings() -> Settings:157    s = Settings()158    s.DATA_DIR.mkdir(parents=True, exist_ok=True)159    (s.DATA_DIR / "files").mkdir(exist_ok=True)160    return s161