Heatmap / groq_client.py
Aniket2006's picture
Security: Load API keys from environment variable instead of hardcoding
fe5cad6
"""
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}")