Spaces:
Running
Running
GitHub Actions commited on
Commit ·
23455d9
1
Parent(s): 94dd7b2
Sync from GitHub · 2026-09-09 19:46 UTC
Browse files- routes/bff.py +31 -8
- 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 |
-
"""
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 284 |
-
#
|
| 285 |
-
#
|
|
|
|
|
|
|
| 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 |
-
#
|
| 318 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
|
| 320 |
return {
|
| 321 |
-
"success":
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|