Spaces:
Sleeping
Sleeping
File size: 6,862 Bytes
454f118 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | """
src/generation/groq_client.py
==============================
Groq LLM client with retry logic, streaming support, and JSON mode.
Groq specifics:
- Runs on custom LPU hardware → ~10x faster than OpenAI (500+ tokens/sec)
- Free tier has generous rate limits (14,400 requests/day)
- Model: llama-3.3-70b-versatile (128K context window)
- JSON mode: pass response_format={"type": "json_object"} for structured output
Rate limit handling:
- Groq free tier: 30 req/min, 14,400 req/day
- We use tenacity for exponential backoff retry on rate limit errors
"""
from __future__ import annotations
import json
import os
import time
from typing import Optional, Generator
from groq import Groq
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
from config.settings import settings
from src.utils.logger import logger
class GroqClient:
"""
Wrapper around the Groq API client.
Handles retries, streaming, and JSON-mode generation.
"""
def __init__(self):
api_key = settings.groq_api_key
if not api_key:
# Try environment directly as fallback
api_key = os.environ.get("GROQ_API_KEY", "")
if not api_key:
raise ValueError(
"GROQ_API_KEY not set! Add it to your .env file.\n"
"Get a free key at: https://console.groq.com"
)
self.client = Groq(api_key=api_key)
self.model = settings.groq_model
self.temperature = settings.llm_temperature
self.max_tokens = settings.llm_max_tokens
logger.info(f"GroqClient initialized | model: {self.model}")
# ---------------------------------------------------------------- #
# Standard Generation
# ---------------------------------------------------------------- #
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=2, max=30),
retry=retry_if_exception_type(Exception),
reraise=True,
)
def generate(
self,
system_prompt: str,
user_message: str,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
json_mode: bool = False,
) -> str:
"""
Generate a response from Groq LLM.
Args:
system_prompt: System instruction
user_message: User question + context
temperature: Override default temperature
max_tokens: Override default max tokens
json_mode: If True, forces JSON output (for metric extraction)
Returns:
Generated text response
"""
kwargs = {
"model": self.model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message},
],
"temperature": temperature or self.temperature,
"max_tokens": max_tokens or self.max_tokens,
}
if json_mode:
kwargs["response_format"] = {"type": "json_object"}
start_time = time.time()
response = self.client.chat.completions.create(**kwargs)
elapsed = time.time() - start_time
content = response.choices[0].message.content or ""
tokens_used = response.usage.total_tokens if response.usage else 0
logger.debug(f"Groq response: {tokens_used} tokens in {elapsed:.2f}s")
return content
# ---------------------------------------------------------------- #
# Streaming Generation
# ---------------------------------------------------------------- #
def generate_stream(
self,
system_prompt: str,
user_message: str,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
) -> Generator[str, None, None]:
"""
Generate a streaming response (for real-time UI display).
Yields text chunks as they arrive from Groq.
Perfect for Streamlit's st.write_stream().
Args:
system_prompt: System instruction
user_message: User question + context
Yields:
String chunks of the response as they stream in
"""
stream = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message},
],
temperature=temperature or self.temperature,
max_tokens=max_tokens or self.max_tokens,
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta
if delta and delta.content:
yield delta.content
# ---------------------------------------------------------------- #
# JSON Extraction
# ---------------------------------------------------------------- #
def generate_json(
self,
system_prompt: str,
user_message: str,
) -> dict:
"""
Generate structured JSON output.
Uses Groq's JSON mode to guarantee valid JSON response.
Args:
system_prompt: System instruction (should specify JSON schema)
user_message: User message with context
Returns:
Parsed Python dict from the JSON response
"""
raw = self.generate(
system_prompt=system_prompt,
user_message=user_message,
json_mode=True,
temperature=0.0, # zero temperature for deterministic JSON
)
try:
return json.loads(raw)
except json.JSONDecodeError as e:
logger.error(f"Failed to parse JSON from Groq response: {e}\nRaw: {raw[:200]}")
return {}
# ---------------------------------------------------------------- #
# Query Expansion (LLM-based)
# ---------------------------------------------------------------- #
def expand_query(self, query: str) -> list[str]:
"""
Use the LLM to generate paraphrased query variants.
Falls back to empty list on failure.
"""
from src.generation.prompts import build_query_expansion_prompt
system_prompt, user_message = build_query_expansion_prompt(query)
try:
result = self.generate_json(system_prompt, user_message)
if isinstance(result, list):
return [q for q in result if isinstance(q, str)]
# Some models return {"queries": [...]}
if isinstance(result, dict):
for v in result.values():
if isinstance(v, list):
return [q for q in v if isinstance(q, str)]
except Exception as e:
logger.warning(f"Query expansion failed: {e}")
return []
|