fomext commited on
Commit
3f28d97
Β·
verified Β·
1 Parent(s): 780afa7

Upload groq_proxy_service.py

Browse files
Files changed (1) hide show
  1. groq_proxy_service.py +67 -79
groq_proxy_service.py CHANGED
@@ -6,8 +6,15 @@ Lightweight FastAPI proxy that exposes a single POST /estimate_audience
6
  endpoint. Deployed on HuggingFace Spaces (Slot 1 and Slot 2 of the
7
  audience estimation pool in music_chart_server.py).
8
 
9
- Rate limits honoured (lightweight Groq model per account):
10
- 60 RPM / 1K RPD / 6K TPM / 500K TPD
 
 
 
 
 
 
 
11
 
12
  This service enforces a strict 1 req/s inter-request gate so that
13
  music_chart_server's own 1 req/s per-slot reservation is always met,
@@ -16,9 +23,6 @@ even if multiple upstream callers arrive simultaneously.
16
  Environment variables (injected as HF Secrets β€” never hardcoded):
17
  GROQ_API_KEY β€” the Groq API key for this Space's account (required)
18
 
19
- Model:
20
- llama-3.1-8b-instant (same lightweight model as the native slot)
21
-
22
  Usage:
23
  uvicorn groq_proxy_service:app --host 0.0.0.0 --port 7860
24
  """
@@ -31,7 +35,7 @@ import threading
31
  import time
32
  from typing import Optional
33
 
34
- from fastapi import FastAPI, HTTPException, Request
35
  from fastapi.responses import JSONResponse
36
  from groq import Groq
37
  from pydantic import BaseModel
@@ -53,14 +57,12 @@ if not GROQ_API_KEY:
53
  "All /estimate_audience calls will fail until it is configured."
54
  )
55
 
56
- # Lightweight model β€” fits easily within 6K TPM per account
57
- GROQ_MODEL = "llama-3.1-8b-instant"
58
-
59
- # Minimum seconds between consecutive Groq calls on this instance
60
- # 1.0 s ⟹ 60 RPM ceiling, matching the per-slot reservation in the pool
61
  SLOT_INTERVAL: float = float(os.environ.get("SLOT_INTERVAL", "1.0"))
62
 
63
- # Maximum tokens in Groq response β€” audience JSON is tiny
64
  MAX_TOKENS: int = int(os.environ.get("MAX_TOKENS", "256"))
65
 
66
  # ─────────────────────────── RATE-LIMIT GATE ────────────────────────────────
@@ -111,41 +113,6 @@ def _get_client() -> Groq:
111
  return _groq_client
112
 
113
 
114
- # ─────────────────────────── PROMPT ─────────────────────────────────────────
115
-
116
- _PROMPT_TEMPLATE = """\
117
- Estimate audience distribution by country for artist {ARTIST}, song "{SONG_TITLE}".
118
-
119
- Return ONLY JSON:
120
- {{
121
- "AAA": 0.00,
122
- "BBB": 0.00,
123
- "CCC": 0.00,
124
- "OTHERS": 0.00
125
- }}
126
-
127
- Rules:
128
- - ISO3 country codes
129
- - Values are decimals, sum = 1.00
130
- - Top 3 countries + OTHERS only
131
- - No country > 0.50
132
- - Highest share = primary song language country (skip if English)
133
- - If genre is globally distributed (e.g., amapiano, afrobeats, hip-hop, pop), enforce OTHERS β‰₯ 0.15
134
- - If not a global genre, OTHERS can be any value
135
- - Prefer realistic streaming distribution
136
- - If unsure, distribute evenly
137
- - No text, JSON only
138
- Non Negotiable: Return ONLY valid JSON. No text, no explanation, no reasoning.
139
- """
140
-
141
-
142
- def _build_prompt(artist: str, song_title: str) -> str:
143
- return _PROMPT_TEMPLATE.format(
144
- ARTIST=artist,
145
- SONG_TITLE=song_title or artist,
146
- )
147
-
148
-
149
  # ─────────────────────────── JSON PARSING ───────────────────────────────────
150
 
151
  def _parse_audience_json(raw: str, artist: str) -> dict:
@@ -180,21 +147,27 @@ def _parse_audience_json(raw: str, artist: str) -> dict:
180
  app = FastAPI(
181
  title="Groq Audience Proxy",
182
  description=(
183
- "Slot proxy for the music_chart_server audience estimation pool. "
184
- "Enforces 1 req/s to stay within Groq lightweight-model limits."
 
 
185
  ),
186
- version="1.0.0",
187
  )
188
 
189
 
190
  class AudienceRequest(BaseModel):
191
- artist: str
192
  song_title: str = ""
 
 
 
 
193
 
194
 
195
  class AudienceResponse(BaseModel):
196
- result: dict
197
- model: str
198
  wait_seconds: float
199
 
200
 
@@ -202,80 +175,95 @@ class AudienceResponse(BaseModel):
202
  def health():
203
  """Liveness probe used by HuggingFace Spaces and the upstream pool."""
204
  return {
205
- "status": "ok",
206
- "model": GROQ_MODEL,
207
  "slot_interval_seconds": SLOT_INTERVAL,
208
- "groq_key_configured": bool(GROQ_API_KEY),
 
209
  }
210
 
211
 
212
  @app.post("/estimate_audience", response_model=AudienceResponse)
213
  def estimate_audience(req: AudienceRequest):
214
  """
215
- Estimate audience distribution for the given artist / song.
 
216
 
217
  The gate serialises calls so that Groq receives at most 1 request per
218
- SLOT_INTERVAL seconds from this Space β€” aligning with the 60 RPM limit
219
- of the lightweight Groq model.
220
 
221
- Returns:
222
  {
223
- "result": {"USA": 0.35, "GBR": 0.25, ...},
224
- "model": "llama-3.1-8b-instant",
 
 
 
 
 
 
 
 
225
  "wait_seconds": 0.0
226
  }
227
 
228
  Error responses:
229
- 429 β€” gate is already holding a request (should not occur in normal use
230
- because the upstream pool issues at most 1 req/s to this slot)
231
  500 β€” Groq call failed or response could not be parsed
232
  """
233
- artist = req.artist.strip()
234
  song_title = req.song_title.strip()
 
 
235
 
236
  if not artist:
237
  raise HTTPException(status_code=422, detail="'artist' must not be empty.")
 
 
 
 
238
 
239
- log.info(f"estimate_audience: artist='{artist}' song='{song_title}'")
 
 
240
 
241
  # ── Acquire slot (blocks until window is clear) ───────────────────────────
242
  wait_s = _gate.acquire()
243
  if wait_s > 0:
244
- log.debug(f"Gate wait: {wait_s:.3f}s for '{artist}'")
245
 
246
- # ── Call Groq ─────────────────────────────────────────────────────────────
247
  try:
248
  client = _get_client()
249
  completion = client.chat.completions.create(
250
- model=GROQ_MODEL,
251
- messages=[
252
- {"role": "user", "content": _build_prompt(artist, song_title)}
253
- ],
254
  temperature=0.4,
255
  max_tokens=MAX_TOKENS,
256
  stream=False,
257
  )
258
  raw: str = completion.choices[0].message.content or ""
259
- log.debug(f"Groq raw response for '{artist}': {raw!r}")
260
  except Exception as exc:
261
- log.error(f"Groq API error for '{artist}': {exc}")
262
  raise HTTPException(
263
  status_code=500,
264
- detail=f"Groq API call failed: {exc}",
265
  )
266
 
267
  # ── Parse and return ──────────────────────────────────────────────────────
268
  try:
269
  result = _parse_audience_json(raw, artist)
270
  except Exception as exc:
271
- log.error(f"JSON parse error for '{artist}': {exc} raw={raw!r}")
 
 
272
  raise HTTPException(
273
  status_code=500,
274
- detail=f"Failed to parse Groq response: {exc}",
275
  )
276
 
277
- log.info(f"estimate_audience OK: '{artist}' β†’ {result}")
278
- return AudienceResponse(result=result, model=GROQ_MODEL, wait_seconds=wait_s)
279
 
280
 
281
  # ─────────────────────────── ENTRYPOINT ─────────────────────────────────────
 
6
  endpoint. Deployed on HuggingFace Spaces (Slot 1 and Slot 2 of the
7
  audience estimation pool in music_chart_server.py).
8
 
9
+ Design: dumb executor.
10
+ music_chart_server owns ALL model-selection and prompt logic. It sends
11
+ both `model` and `prompt` in every request body so that this service
12
+ simply fires whatever it receives at Groq and returns the raw result.
13
+ This keeps control centralised β€” updating model preference or prompts
14
+ requires changes in one place only (music_chart_server.py).
15
+
16
+ Rate limits honoured (same Groq free-tier limits apply to all models):
17
+ 30 RPM / varies RPD / varies TPM / varies TPD (see music_chart_server config)
18
 
19
  This service enforces a strict 1 req/s inter-request gate so that
20
  music_chart_server's own 1 req/s per-slot reservation is always met,
 
23
  Environment variables (injected as HF Secrets β€” never hardcoded):
24
  GROQ_API_KEY β€” the Groq API key for this Space's account (required)
25
 
 
 
 
26
  Usage:
27
  uvicorn groq_proxy_service:app --host 0.0.0.0 --port 7860
28
  """
 
35
  import time
36
  from typing import Optional
37
 
38
+ from fastapi import FastAPI, HTTPException
39
  from fastapi.responses import JSONResponse
40
  from groq import Groq
41
  from pydantic import BaseModel
 
57
  "All /estimate_audience calls will fail until it is configured."
58
  )
59
 
60
+ # Minimum seconds between consecutive Groq calls on this instance.
61
+ # music_chart_server already enforces 1 req/s per slot, but this gate
62
+ # protects against accidental concurrent upstream callers.
 
 
63
  SLOT_INTERVAL: float = float(os.environ.get("SLOT_INTERVAL", "1.0"))
64
 
65
+ # Maximum tokens in Groq response β€” audience JSON is tiny regardless of model
66
  MAX_TOKENS: int = int(os.environ.get("MAX_TOKENS", "256"))
67
 
68
  # ─────────────────────────── RATE-LIMIT GATE ────────────────────────────────
 
113
  return _groq_client
114
 
115
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  # ─────────────────────────── JSON PARSING ───────────────────────────────────
117
 
118
  def _parse_audience_json(raw: str, artist: str) -> dict:
 
147
  app = FastAPI(
148
  title="Groq Audience Proxy",
149
  description=(
150
+ "Dumb-executor slot proxy for the music_chart_server audience pool. "
151
+ "Model and prompt are supplied by the caller β€” this service only fires "
152
+ "the request at Groq and returns the result. "
153
+ "Enforces 1 req/s to stay within Groq rate limits."
154
  ),
155
+ version="2.0.0",
156
  )
157
 
158
 
159
  class AudienceRequest(BaseModel):
160
+ artist: str
161
  song_title: str = ""
162
+ # Centralised control fields β€” sent by music_chart_server on every request.
163
+ # The proxy uses these directly; it never picks models or builds prompts itself.
164
+ model: str = "" # e.g. "llama-3.1-8b-instant", "llama-3.3-70b-versatile"
165
+ prompt: str = "" # fully-rendered prompt string from music_chart_server
166
 
167
 
168
  class AudienceResponse(BaseModel):
169
+ result: dict
170
+ model: str
171
  wait_seconds: float
172
 
173
 
 
175
  def health():
176
  """Liveness probe used by HuggingFace Spaces and the upstream pool."""
177
  return {
178
+ "status": "ok",
 
179
  "slot_interval_seconds": SLOT_INTERVAL,
180
+ "groq_key_configured": bool(GROQ_API_KEY),
181
+ "note": "model selected by music_chart_server per request",
182
  }
183
 
184
 
185
  @app.post("/estimate_audience", response_model=AudienceResponse)
186
  def estimate_audience(req: AudienceRequest):
187
  """
188
+ Execute an audience estimation Groq call using the model and prompt
189
+ supplied by music_chart_server in the request body.
190
 
191
  The gate serialises calls so that Groq receives at most 1 request per
192
+ SLOT_INTERVAL seconds from this Space.
 
193
 
194
+ Request body:
195
  {
196
+ "artist": "Artist Name",
197
+ "song_title": "Track Title",
198
+ "model": "llama-3.1-8b-instant", <- chosen by music_chart_server
199
+ "prompt": "<full rendered prompt>" <- built by music_chart_server
200
+ }
201
+
202
+ Response:
203
+ {
204
+ "result": {"USA": 0.35, "GBR": 0.25, ...},
205
+ "model": "llama-3.1-8b-instant",
206
  "wait_seconds": 0.0
207
  }
208
 
209
  Error responses:
210
+ 422 β€” missing required fields
 
211
  500 β€” Groq call failed or response could not be parsed
212
  """
213
+ artist = req.artist.strip()
214
  song_title = req.song_title.strip()
215
+ model = req.model.strip()
216
+ prompt = req.prompt.strip()
217
 
218
  if not artist:
219
  raise HTTPException(status_code=422, detail="'artist' must not be empty.")
220
+ if not model:
221
+ raise HTTPException(status_code=422, detail="'model' must not be empty.")
222
+ if not prompt:
223
+ raise HTTPException(status_code=422, detail="'prompt' must not be empty.")
224
 
225
+ log.info(
226
+ f"estimate_audience: artist='{artist}' song='{song_title}' model='{model}'"
227
+ )
228
 
229
  # ── Acquire slot (blocks until window is clear) ───────────────────────────
230
  wait_s = _gate.acquire()
231
  if wait_s > 0:
232
+ log.debug(f"Gate wait: {wait_s:.3f}s for '{artist}' model='{model}'")
233
 
234
+ # ── Call Groq with the caller-supplied model and prompt ───────────────────
235
  try:
236
  client = _get_client()
237
  completion = client.chat.completions.create(
238
+ model=model,
239
+ messages=[{"role": "user", "content": prompt}],
 
 
240
  temperature=0.4,
241
  max_tokens=MAX_TOKENS,
242
  stream=False,
243
  )
244
  raw: str = completion.choices[0].message.content or ""
245
+ log.debug(f"Groq raw response for '{artist}' model='{model}': {raw!r}")
246
  except Exception as exc:
247
+ log.error(f"Groq API error for '{artist}' model='{model}': {exc}")
248
  raise HTTPException(
249
  status_code=500,
250
+ detail=f"Groq API call failed (model={model}): {exc}",
251
  )
252
 
253
  # ── Parse and return ──────────────────────────────────────────────────────
254
  try:
255
  result = _parse_audience_json(raw, artist)
256
  except Exception as exc:
257
+ log.error(
258
+ f"JSON parse error for '{artist}' model='{model}': {exc} raw={raw!r}"
259
+ )
260
  raise HTTPException(
261
  status_code=500,
262
+ detail=f"Failed to parse Groq response (model={model}): {exc}",
263
  )
264
 
265
+ log.info(f"estimate_audience OK: '{artist}' model='{model}' -> {result}")
266
+ return AudienceResponse(result=result, model=model, wait_seconds=wait_s)
267
 
268
 
269
  # ─────────────────────────── ENTRYPOINT ─────────────────────────────────────