""" AGROW Centralized Groq Client ============================= Shared LLM client with key rotation and fallback for all AGROW backend services. Features: - Centralized API key pool loaded from environment - Automatic key rotation on rate limits (429) - Exponential backoff per key - Detailed logging for debugging """ import os import time import logging from typing import Optional # ============================================================================ # LOGGING # ============================================================================ logger = logging.getLogger("GroqClient") # ============================================================================ # GROQ API CONFIGURATION # ============================================================================ # Load API keys from environment variable (comma-separated) GROQ_API_KEYS_ENV = os.environ.get("GROQ_API_KEYS", "") GROQ_API_KEYS = [key.strip() for key in GROQ_API_KEYS_ENV.split(",") if key.strip()] if not GROQ_API_KEYS: logger.warning("[GroqClient] GROQ_API_KEYS environment variable not set or empty!") GROQ_MODEL = "llama-3.3-70b-versatile" logger.info(f"[GroqClient] Initialized with {len(GROQ_API_KEYS)} API keys") def call_groq( prompt: str, system_prompt: str = "You are an expert agricultural AI analyst. Always respond with valid JSON only, no markdown code blocks.", max_tokens: int = 2048, temperature: float = 0.7, ) -> str: """ Call Groq API with automatic key rotation and fallback. Args: prompt: User prompt to send system_prompt: System message for the LLM max_tokens: Maximum tokens in response temperature: Sampling temperature Returns: Response text from the LLM Raises: ValueError: If all API keys fail """ from groq import Groq if not GROQ_API_KEYS: raise ValueError("No GROQ_API_KEYS configured") max_attempts_per_key = 3 base_backoff = 1.0 last_error = None # Iterate through all available keys for key_idx, api_key in enumerate(GROQ_API_KEYS): try: client = Groq(api_key=api_key) logger.info(f"[GroqClient] Trying key {key_idx+1}/{len(GROQ_API_KEYS)}") # Retry logic per key (for network/timeout issues) for attempt in range(1, max_attempts_per_key + 1): try: chat_completion = client.chat.completions.create( messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": prompt} ], model=GROQ_MODEL, temperature=temperature, max_tokens=max_tokens, ) response_text = chat_completion.choices[0].message.content logger.info(f"[GroqClient] Success with key {key_idx+1}! Response: {len(response_text)} chars") return response_text except Exception as e: error_str = str(e) logger.warning(f"[GroqClient] Key {key_idx+1} attempt {attempt} failed: {error_str[:100]}") # Rate limit (429) - immediately switch to next key if "rate_limit" in error_str.lower() or "429" in error_str: logger.info(f"[GroqClient] Rate limited. Switching to next key...") break # Other errors - backoff and retry same key if attempt < max_attempts_per_key: wait = base_backoff * (2 ** (attempt - 1)) logger.info(f"[GroqClient] Backing off {wait}s...") time.sleep(wait) else: # Move to next key raise e except Exception as e: last_error = str(e) logger.warning(f"[GroqClient] Key {key_idx+1} failed completely: {str(e)[:100]}") continue raise ValueError(f"All {len(GROQ_API_KEYS)} Groq API keys failed. Last error: {last_error}") # Backward compatibility alias def call_gemini_with_fallback(prompt: str, keys=None, url=None) -> str: """Backward compatible wrapper - now uses Groq.""" return call_groq(prompt) def call_groq_whisper(audio_filename: str) -> str: """ Transcribe audio using Groq Whisper model. Uses centralized key rotation. """ from groq import Groq if not GROQ_API_KEYS: raise ValueError("No GROQ_API_KEYS configured") last_error = None # Iterate through keys for key_idx, api_key in enumerate(GROQ_API_KEYS): try: client = Groq(api_key=api_key) logger.info(f"[GroqClient-Whisper] Trying key {key_idx+1}/{len(GROQ_API_KEYS)}") with open(audio_filename, "rb") as file: transcription = client.audio.transcriptions.create( file=(audio_filename, file.read()), model="whisper-large-v3", response_format="json", language="en", temperature=0.0 ) logger.info(f"[GroqClient-Whisper] Success with key {key_idx+1}") return transcription.text except Exception as e: error_str = str(e) logger.warning(f"[GroqClient-Whisper] Key {key_idx+1} failed: {error_str[:100]}") # If rate limit, try next key immediately if "rate_limit" in error_str.lower() or "429" in error_str: continue last_error = str(e) continue raise ValueError(f"All keys failed for Whisper. Last error: {last_error}")