GitHub Actions commited on
Commit
23455d9
·
1 Parent(s): 94dd7b2

Sync from GitHub · 2026-09-09 19:46 UTC

Browse files
Files changed (2) hide show
  1. routes/bff.py +31 -8
  2. utils/groq_client.py +11 -1
routes/bff.py CHANGED
@@ -50,8 +50,15 @@ def _get_bff_data(user_id: str, business_id: str) -> dict:
50
 
51
 
52
  def _call_groq_json(system_prompt: str, user_prompt: str) -> dict:
53
- """Thin wrapper — delegates to the key-rotating Groq client utility."""
54
- return call_groq_json(system_prompt, user_prompt, model=HEAVY_MODEL, max_tokens=1500)
 
 
 
 
 
 
 
55
 
56
 
57
  # ─── Route: Generate follow-up questions ─────────────────────────────────────
@@ -270,19 +277,23 @@ each containing a list of 2-4 concise bullet points:
270
  }
271
 
272
  call_plan = []
 
273
  for key, prompt_fn in all_outputs.items():
274
  existing = biz_ref_check.collection("profile_output").document(key).get()
275
  if existing.exists:
276
  print(f"[bff] Skipping {key} — already exists in Firestore")
 
277
  else:
278
  call_plan.append((key, prompt_fn))
279
 
280
  results: dict = {}
281
  errors: list = []
282
 
283
- # Sequential calls with 15s gap to stay within 12,000 TPM free-tier limit.
284
- # With 1,500 max_tokens per call: ~2,500 tokens/call × 4 = ~10,000 total.
285
- # 15s gap × 200 tokens/sec refill = 3,000 tokens recovered between calls.
 
 
286
  for i, (key, prompt_fn) in enumerate(call_plan):
287
  if i > 0:
288
  time.sleep(15) # 15s gap: ~3,000 tokens recovered at 200 tok/s
@@ -314,10 +325,22 @@ each containing a list of 2-4 concise bullet points:
314
  {**data, "generatedAt": SERVER_TIMESTAMP}
315
  )
316
 
317
- # Mark BFF as complete
318
- biz_ref.update({"bffCompleted": True})
 
 
 
 
 
 
 
 
 
 
 
319
 
320
  return {
321
- "success": True,
322
  "outputs": list(results.keys()),
 
323
  }
 
50
 
51
 
52
  def _call_groq_json(system_prompt: str, user_prompt: str) -> dict:
53
+ """
54
+ Thin wrapper — delegates to the key-rotating Groq client utility.
55
+
56
+ max_tokens is generous (3000) because HEAVY_MODEL is a reasoning model
57
+ (openai/gpt-oss-120b) that spends part of its budget on internal
58
+ reasoning before writing the final JSON — a tight budget here truncates
59
+ the JSON output and fails to parse.
60
+ """
61
+ return call_groq_json(system_prompt, user_prompt, model=HEAVY_MODEL, max_tokens=3000)
62
 
63
 
64
  # ─── Route: Generate follow-up questions ─────────────────────────────────────
 
277
  }
278
 
279
  call_plan = []
280
+ skipped_keys: set = set()
281
  for key, prompt_fn in all_outputs.items():
282
  existing = biz_ref_check.collection("profile_output").document(key).get()
283
  if existing.exists:
284
  print(f"[bff] Skipping {key} — already exists in Firestore")
285
+ skipped_keys.add(key)
286
  else:
287
  call_plan.append((key, prompt_fn))
288
 
289
  results: dict = {}
290
  errors: list = []
291
 
292
+ # Sequential calls with 15s gap to stay within the free-tier TPM limit.
293
+ # max_tokens=3000 per call leaves headroom for HEAVY_MODEL's internal
294
+ # reasoning tokens on top of the JSON output (see _call_groq_json).
295
+ # 15s gap × 200 tokens/sec refill recovers ~3,000 tokens between calls —
296
+ # if that's not enough, the 5-key rotation pool absorbs the rest via 429s.
297
  for i, (key, prompt_fn) in enumerate(call_plan):
298
  if i > 0:
299
  time.sleep(15) # 15s gap: ~3,000 tokens recovered at 200 tok/s
 
325
  {**data, "generatedAt": SERVER_TIMESTAMP}
326
  )
327
 
328
+ # Only mark BFF as complete once EVERY output actually exists — either
329
+ # generated just now or already present from a prior partial run. Marking
330
+ # this true when an output is still missing is what previously let a
331
+ # business look "complete" with an empty/partial profile (see
332
+ # project_multibusiness_gate memory — the manual-Console-flip incident
333
+ # had the same shape of bug).
334
+ present_keys = skipped_keys | set(results.keys())
335
+ all_present = present_keys == set(all_outputs.keys())
336
+ if all_present:
337
+ biz_ref.update({"bffCompleted": True})
338
+ else:
339
+ missing = set(all_outputs.keys()) - present_keys
340
+ print(f"[bff] NOT marking bffCompleted — missing outputs: {missing}")
341
 
342
  return {
343
+ "success": all_present,
344
  "outputs": list(results.keys()),
345
+ "missing": sorted(set(all_outputs.keys()) - present_keys),
346
  }
utils/groq_client.py CHANGED
@@ -174,7 +174,17 @@ def call_groq_json(
174
  """Convenience wrapper: calls Groq in JSON mode and returns parsed dict."""
175
  raw = call_groq(system_prompt, user_prompt, model=model,
176
  json_mode=True, max_tokens=max_tokens)
177
- return json.loads(raw)
 
 
 
 
 
 
 
 
 
 
178
 
179
 
180
  def call_groq_stream(
 
174
  """Convenience wrapper: calls Groq in JSON mode and returns parsed dict."""
175
  raw = call_groq(system_prompt, user_prompt, model=model,
176
  json_mode=True, max_tokens=max_tokens)
177
+ if not raw:
178
+ # Reasoning models (e.g. openai/gpt-oss-*) can spend the token budget on
179
+ # internal reasoning and return empty content if max_tokens is too tight.
180
+ raise RuntimeError(
181
+ "Groq returned empty content — likely truncated by max_tokens "
182
+ f"(model={model}, max_tokens={max_tokens})"
183
+ )
184
+ try:
185
+ return json.loads(raw)
186
+ except json.JSONDecodeError as e:
187
+ raise RuntimeError(f"Groq returned invalid JSON (likely truncated): {e}") from e
188
 
189
 
190
  def call_groq_stream(