from pydantic_settings import BaseSettings class Settings(BaseSettings): groq_api_key: str = "" groq_model: str = "llama-3.1-8b-instant" groq_base_url: str = "https://api.groq.com/openai/v1" # Speech-to-text for the voice chat panel — same Groq account/credential, # different endpoint (audio/transcriptions instead of chat/completions). groq_whisper_model: str = "whisper-large-v3-turbo" # Comma-separated fallback models tried in order when primary hits rate # limit (decommissioned entries are skipped automatically at runtime) groq_fallback_models: str = ( "llama-3.3-70b-versatile," "openai/gpt-oss-20b," "openai/gpt-oss-120b," "qwen/qwen3-32b" ) # ── Optional extra LLM providers (OpenAI-compatible) ───────────────────── # Tried in order after Groq when its models are rate-limited/unavailable. # A provider is skipped entirely when its API key is empty. cerebras_api_key: str = "" cerebras_base_url: str = "https://api.cerebras.ai/v1" cerebras_models: str = "llama-3.3-70b,llama3.1-8b" gemini_api_key: str = "" gemini_base_url: str = "https://generativelanguage.googleapis.com/v1beta/openai" gemini_models: str = "gemini-2.0-flash" openrouter_api_key: str = "" openrouter_base_url: str = "https://openrouter.ai/api/v1" # All current free models on OpenRouter (ordered best → smallest). # Audio/image/safety/vision-only and sub-2B models are excluded. # Decommissioned entries are skipped automatically at runtime. openrouter_models: str = ( "openai/gpt-oss-120b:free," "nvidia/nemotron-3-ultra-550b-a55b:free," "nousresearch/hermes-3-llama-3.1-405b:free," "nvidia/nemotron-3-super-120b-a12b:free," "meta-llama/llama-3.3-70b-instruct:free," "qwen/qwen3-next-80b-a3b-instruct:free," "qwen/qwen3-coder:free," "moonshotai/kimi-k2.6:free," "google/gemma-4-31b-it:free," "google/gemma-4-26b-a4b-it:free," "nvidia/nemotron-3-nano-30b-a3b:free," "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free," "openai/gpt-oss-20b:free," "cognitivecomputations/dolphin-mistral-24b-venice-edition:free," "nvidia/nemotron-nano-9b-v2:free," "meta-llama/llama-3.2-3b-instruct:free," "poolside/laguna-m.1:free," "poolside/laguna-xs.2:free," "nex-agi/nex-n2-pro:free" ) # Comma-separated list of allowed browser origins (frontend URLs). cors_origins: str = "http://localhost:3000,http://127.0.0.1:3000" # ── Abuse protection / cost control ────────────────────────────────────── # Required to call POST /ingest over HTTP; empty = endpoint disabled # (local ingestion via scripts/ingest.py is unaffected). admin_token: str = "" rate_limit_chat_per_minute: int = 20 # per client IP rate_limit_voice_per_minute: int = 15 # per client IP, voice transcription voice_max_upload_mb: int = 8 # ~60s of webm/opus, generous ceiling email_max_per_session: int = 5 email_max_per_day: int = 50 # global, resets at UTC midnight response_cache_ttl_seconds: int = 3600 response_cache_max_entries: int = 200 session_idle_ttl_seconds: int = 6 * 3600 max_sessions: int = 1000 chroma_persist_directory: str = "./chroma_db" chroma_collection_name: str = "negoptim_knowledge" embedding_model: str = "intfloat/multilingual-e5-small" retrieval_top_k: int = 5 chunk_size: int = 700 chunk_overlap: int = 100 max_history_turns: int = 10 knowledge_base_path: str = "../knowledge" # ── Email ───────────────────────────────────────────────────────────────── # Priority: Resend (HTTP, works on any host) → SMTP (blocked on HF free tier) # Either absent → simulation mode (logs + reports success, no real delivery). resend_api_key: str = "" resend_from: str = "Negoptim AI " smtp_host: str = "smtp.gmail.com" smtp_port: int = 587 smtp_user: str = "" smtp_password: str = "" smtp_from: str = "" # defaults to smtp_user when empty smtp_from_name: str = "Negoptim AI" # All outgoing mail is force-routed here while testing (per spec). # Set to "" to deliver to the real recipient. email_override_to: str = "samirmajzoub9@gmail.com" @property def fallback_model_list(self) -> list[str]: return [m.strip() for m in self.groq_fallback_models.split(",") if m.strip()] @property def cerebras_model_list(self) -> list[str]: return [m.strip() for m in self.cerebras_models.split(",") if m.strip()] @property def gemini_model_list(self) -> list[str]: return [m.strip() for m in self.gemini_models.split(",") if m.strip()] @property def openrouter_model_list(self) -> list[str]: return [m.strip() for m in self.openrouter_models.split(",") if m.strip()] @property def cors_origin_list(self) -> list[str]: return [o.strip() for o in self.cors_origins.split(",") if o.strip()] @property def resend_configured(self) -> bool: return bool(self.resend_api_key) @property def smtp_configured(self) -> bool: return bool(self.smtp_host and self.smtp_user and self.smtp_password) @property def smtp_sender(self) -> str: return self.smtp_from or self.smtp_user class Config: env_file = ".env" settings = Settings()