""" config.py — Centralised, validated configuration using Pydantic Settings. All values are read from environment variables (or .env file). Import the singleton `settings` anywhere in the project. """ from functools import lru_cache from typing import Literal from pydantic import Field, field_validator from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="ignore", ) # ── LLM ────────────────────────────────────────────────────────────── groq_api_key: str = Field(..., description="Groq API key (free tier)") llm_model: str = Field("llama-3.3-70b-versatile", description="Groq chat model") stt_model: str = Field("whisper-large-v3-turbo", description="Groq Whisper STT model") tts_voice: str = Field("en-US-JennyNeural", description="Edge-TTS voice") # ── RAG ─────────────────────────────────────────────────────────────── chroma_persist_dir: str = Field("./data/chroma_db") embedding_model: str = Field("all-MiniLM-L6-v2") docs_dir: str = Field("./data/sample_files") chunk_size: int = Field(500, ge=100, le=4000) chunk_overlap: int = Field(50, ge=0, le=500) # ── MCP Server ──────────────────────────────────────────────────────── mcp_server_host: str = Field("0.0.0.0") mcp_server_port: int = Field(8000, ge=1024, le=65535) mcp_transport: Literal["sse", "stdio"] = Field("sse") # ── SQLite ──────────────────────────────────────────────────────────── sqlite_db_path: str = Field("./data/database.db") # ── Audio ───────────────────────────────────────────────────────────── audio_sample_rate: int = Field(16000) audio_channels: int = Field(1) audio_silence_threshold: int = Field(500) audio_silence_duration_sec: float = Field(1.5) audio_min_speech_duration_sec: float = Field(0.5) # ── App ─────────────────────────────────────────────────────────────── app_env: Literal["development", "production"] = Field("development") log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = Field("INFO") log_format: Literal["json", "console"] = Field("console") @field_validator("chunk_overlap") @classmethod def overlap_less_than_chunk(cls, v: int, info) -> int: chunk_size = info.data.get("chunk_size", 500) if v >= chunk_size: raise ValueError("chunk_overlap must be less than chunk_size") return v # NEW — works both locally and on HF Spaces @property def mcp_server_url(self) -> str: if self.is_production: return f"https://{self.mcp_server_host}/sse" return f"http://localhost:{self.mcp_server_port}/sse" @property def is_production(self) -> bool: return self.app_env == "production" @lru_cache(maxsize=1) def get_settings() -> Settings: """Return the cached Settings singleton.""" return Settings() # type: ignore[call-arg] # Convenience singleton — import this everywhere settings = get_settings()