# app/config.py - Application configuration (Production Hardened) from pydantic_settings import BaseSettings, SettingsConfigDict from typing import Optional from functools import lru_cache from dotenv import load_dotenv import os # ════════════════════════════════════════════════════════════════════════════ # FIX 1: HF-SAFE DOTENV LOADING # On HuggingFace, secrets are injected at runtime. Don't override them. # ════════════════════════════════════════════════════════════════════════════ if os.getenv("HF_SPACE_ID") is None: load_dotenv(override=True) # Local dev → .env else: load_dotenv(override=False) # HF prod → Secrets only class Settings(BaseSettings): """Application settings loaded from environment variables.""" # Application APP_NAME: str = "Scam Honeypot API" VERSION: str = "2.5.0" DEBUG: bool = False GUVI_API_KEY: str = "" # Must be set via Environment Variable (HF Secrets) GUVI_CALLBACK_URL: str = "https://hackathon.guvi.in/api/updateHoneyPotFinalResult" # SOC Hardening (SIEM Integration) SYSLOG_ENABLED: bool = False SYSLOG_HOST: str = "localhost" SYSLOG_PORT: int = 514 # LLM Configuration LLM_PROVIDER: str = "groq" OPENAI_API_KEY: Optional[str] = None ANTHROPIC_API_KEY: Optional[str] = None GROQ_API_KEY: Optional[str] = None OPENROUTER_API_KEY: Optional[str] = None # Local HF (Offline / Free-Tier) Inference # FIX: Enable by default as crash-proof fallback when Groq fails USE_LOCAL_HF_MODEL: bool = True # Auto-fallback when API fails HF_LOCAL_MODEL_NAME: str = "facebook/opt-125m" # Tiny model, runs on CPU HF_LOCAL_MAX_TOKENS: int = 80 # Short responses like real SMS/chat HF_LOCAL_DEVICE: str = "cpu" # Explicit so HF Spaces & local dev behave consistently # ════════════════════════════════════════════════════════════════════════ # FIX 2: EXPLICIT MODEL DEFAULTS (No None = No Surprises) # Aligned with Groq limits and capability-aware fallback chains # ════════════════════════════════════════════════════════════════════════ GPT_MODEL: str = "gpt-4-turbo-preview" CLAUDE_MODEL: str = "claude-3-sonnet-20240229" GROQ_MODEL: str = "llama-3.3-70b-versatile" # Per-task model routing (Production Grade) MAX_RETRIES: int = 2 # ════════════════════════════════════════════════════════════════════════ # FIX: HARD LLM CALL LIMITS (Prevent rate limit exhaustion) # OPTIMIZED for GUVI's 30s timeout & Groq free tier rate limits # ════════════════════════════════════════════════════════════════════════ MAX_LLM_CALLS_PER_REQUEST: int = 6 # Reduced from 10 - fits within 20s timeout MAX_LLM_CALLS_PER_SESSION: int = 20 # Reduced from 30 - prevents key exhaustion # Per-task model routing (Production Grade) # FIX: llama-3.3-70b fails JSON_SCHEMA mode with 400. Use qwen3-32b for structured. GROQ_FAST_MODEL: str = "llama-3.1-8b-instant" # Blazing fast (~300 tokens/sec) GROQ_SMART_MODEL: str = "qwen/qwen3-32b" # Native reasoning, 60 RPM GROQ_NATURAL_MODEL: str = "moonshotai/kimi-k2-instruct-0905" GROQ_STRUCTURED_MODEL: str = "qwen/qwen3-32b" # FIX: Supports JSON_SCHEMA properly GROQ_SAFETY_MODEL: str = "meta-llama/llama-guard-4-12b" GROQ_SAFEGUARD_MODEL: str = "openai/gpt-oss-safeguard-20b" OPENROUTER_MODEL: str = "meta-llama/llama-3.1-70b-instruct" # LLM parameters LLM_TEMPERATURE: float = 0.7 LLM_MAX_TOKENS: int = 500 # Conversation MAX_CONVERSATION_LENGTH: int = 50 CONVERSATION_TTL_HOURS: int = 24 # Rate Limiting RATE_LIMIT_PER_MINUTE: int = 60 # Feature Flags ENABLE_LLM_DETECTION: bool = True ENABLE_LLM_RESPONSES: bool = True ENABLE_THREAT_INTELLIGENCE: bool = True ENABLE_LAW_ENFORCEMENT_API: bool = False # Disabled for hackathon ENABLE_ENGAGEMENT_DELAY: bool = False # Forensic Clinic (Compound Systems) ENABLE_MATH_FORENSICS: bool = False # 🧮 Claim Verifier (Compound-Mini) ENABLE_VISUAL_EVIDENCE: bool = False # 📊 Chart Lab (Compound) # Database (SQLite default, PostgreSQL/Supabase via env) DATABASE_URL: str = "sqlite+aiosqlite:///./data/honeypot.db" # Compliance SANDBOX_MODE: bool = False ANONYMIZE_LOGS: bool = True SYNTHETIC_DATA_ONLY: bool = False model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", case_sensitive=True, extra="allow" ) @lru_cache() def get_settings() -> Settings: return Settings() settings = get_settings() # ════════════════════════════════════════════════════════════════════════════ # FIX 3 & 4: STARTUP VALIDATION (Fail Fast) # ════════════════════════════════════════════════════════════════════════════ def validate_production_config(): """Validates critical config at startup. Raises RuntimeError if invalid.""" errors = [] # FIX 3: GUVI_API_KEY must be set for scoring if not settings.GUVI_API_KEY: errors.append("GUVI_API_KEY missing — scoring impossible") # FIX 4: Exactly ONE *external* LLM provider key must be set # EXCEPTION: When USE_LOCAL_HF_MODEL=True we allow zero external keys active_keys = [ ("GROQ_API_KEY", settings.GROQ_API_KEY), ("OPENAI_API_KEY", settings.OPENAI_API_KEY), ("ANTHROPIC_API_KEY", settings.ANTHROPIC_API_KEY), ("OPENROUTER_API_KEY", settings.OPENROUTER_API_KEY), ] set_keys = [(name, key) for name, key in active_keys if key] if len(set_keys) == 0 and not settings.USE_LOCAL_HF_MODEL: errors.append("No LLM API key set — system cannot function (set USE_LOCAL_HF_MODEL=True to enable offline mode)") elif len(set_keys) > 1: key_names = [name for name, _ in set_keys] errors.append(f"Multiple LLM API keys set ({', '.join(key_names)}) — please use exactly one") # In production, fail hard if errors and not settings.DEBUG: raise RuntimeError("🚨 CONFIG VALIDATION FAILED:\n - " + "\n - ".join(errors)) elif errors: # In debug mode, just warn import warnings for err in errors: warnings.warn(f"⚠️ CONFIG WARNING: {err}") # Run validation at import time (fail fast) validate_production_config()