"""Bot configuration. All settings can be overridden with environment variables prefixed with ``PROCUREMENT_BOT_`` (e.g. ``PROCUREMENT_BOT_BOT_TOKEN=...``) or a local ``.env`` file. Docker/compose passes them in directly (same convention as the api service's ``PROCUREMENT_*`` prefix). """ from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict( env_prefix="PROCUREMENT_BOT_", env_file=".env", env_file_encoding="utf-8", extra="ignore", ) # Bale Bot API — Telegram-compatible clone hosted at tapi.bale.ai. BALE_API_BASE: str = "https://tapi.bale.ai" BOT_TOKEN: str = "" # from Bale's BotFather POLL_TIMEOUT_SECONDS: int = 25 # getUpdates long-poll window POLL_ERROR_SLEEP_SECONDS: float = 3.0 # ERP API (in-network service URL inside Docker). API_BASE_URL: str = "http://localhost:8000/api/v1" API_TIMEOUT_SECONDS: float = 30.0 # Optional bearer for the day procurement endpoints start enforcing auth. SERVICE_TOKEN: str = "" # Frontend URL (for opening Wizard from Bale bot) FRONTEND_URL: str = "http://localhost:8081" # Security token for standalone wizard review URL DRAFT_TOKEN_SECRET: str = "procurement-draft-secret-change-in-prod-v7" DRAFT_TOKEN_EXPIRE_SECONDS: int = 7200 # 2 hours # Persistent storage directory for draft attachments (proformas) ATTACHMENTS_DIR: str = "data/attachments/proformas" # OpenAI-compatible AI provider (LLM), e.g. OpenAI / GLM / DeepSeek / OpenRouter. AI_BASE_URL: str = "" # e.g. https://api.openai.com/v1 AI_API_KEY: str = "" LLM_MODEL: str = "gpt-4o-mini" LLM_TIMEOUT_SECONDS: float = 60.0 # STT providers — support multiple models/APIs in parallel. # 1. JSON list: [{"name": "m1", "base_url": "...", "api_key": "...", "model": "..."}, ...] STT_PROVIDERS_JSON: str = "" # 2. Individual provider variables (up to 3 distinct STT models) STT_PROVIDER_1_NAME: str = "model-1" STT_PROVIDER_1_BASE_URL: str = "" STT_PROVIDER_1_API_KEY: str = "" STT_PROVIDER_1_MODEL: str = "" STT_PROVIDER_1_LANGUAGE: str = "fa" STT_PROVIDER_2_NAME: str = "model-2" STT_PROVIDER_2_BASE_URL: str = "" STT_PROVIDER_2_API_KEY: str = "" STT_PROVIDER_2_MODEL: str = "" STT_PROVIDER_2_LANGUAGE: str = "fa" STT_PROVIDER_3_NAME: str = "model-3" STT_PROVIDER_3_BASE_URL: str = "" STT_PROVIDER_3_API_KEY: str = "" STT_PROVIDER_3_MODEL: str = "" STT_PROVIDER_3_LANGUAGE: str = "fa" # Legacy single STT provider fallback: STT_BASE_URL: str = "" STT_API_KEY: str = "" STT_MODEL: str = "whisper-1" STT_LANGUAGE: str = "fa" @property def stt_base_url(self) -> str: return self.STT_BASE_URL or self.AI_BASE_URL @property def stt_api_key(self) -> str: return self.STT_API_KEY or self.AI_API_KEY @property def stt_providers(self) -> list[dict]: import json if self.STT_PROVIDERS_JSON: try: data = json.loads(self.STT_PROVIDERS_JSON) if isinstance(data, list) and len(data) > 0: return data except Exception: pass # Check explicit STT_PROVIDER_1 / 2 / 3 custom_list: list[dict] = [] for idx in (1, 2, 3): b_url = getattr(self, f"STT_PROVIDER_{idx}_BASE_URL") key = getattr(self, f"STT_PROVIDER_{idx}_API_KEY") model = getattr(self, f"STT_PROVIDER_{idx}_MODEL") name = getattr(self, f"STT_PROVIDER_{idx}_NAME") lang = getattr(self, f"STT_PROVIDER_{idx}_LANGUAGE") or "fa" if b_url and key: custom_list.append({ "name": name or f"model-{idx}", "base_url": b_url, "api_key": key, "model": model or self.STT_MODEL, "language": lang, "timeout": 30.0, }) if custom_list: return custom_list # Fallback to single legacy STT provider if set if self.stt_base_url and self.stt_api_key: return [{ "name": "default", "base_url": self.stt_base_url, "api_key": self.stt_api_key, "model": self.STT_MODEL, "language": self.STT_LANGUAGE, "timeout": 30.0, }] return [] # PostgreSQL — same instance/database as the api service. DATABASE_URL: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/procurement" # Chats that may use the bot without Casdoor linking (comma separated). ADMIN_CHAT_IDS: str = "" # Drafts older than this are marked expired by the sweeper loop. DRAFT_TTL_HOURS: int = 24 LOG_LEVEL: str = "INFO" @property def admin_chat_ids(self) -> set[int]: out: set[int] = set() for part in self.ADMIN_CHAT_IDS.replace(";", ",").split(","): part = part.strip() if part.isdigit(): out.add(int(part)) return out settings = Settings()