Spaces:
Running
Running
GitHub Actions commited on
Commit ·
5026675
1
Parent(s): 23455d9
Sync from GitHub · 2026-09-13 10:42 UTC
Browse files- routes/bff.py +122 -3
routes/bff.py
CHANGED
|
@@ -20,6 +20,11 @@ class GenerateFollowupsRequest(BaseModel):
|
|
| 20 |
class GenerateProfileRequest(BaseModel):
|
| 21 |
userId: str
|
| 22 |
businessId: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
|
| 25 |
# ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
@@ -49,6 +54,60 @@ def _get_bff_data(user_id: str, business_id: str) -> dict:
|
|
| 49 |
return sections
|
| 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.
|
|
@@ -82,12 +141,17 @@ def generate_followups(
|
|
| 82 |
bff_query = f"Business Footing Framework section {body.section} {json.dumps(body.sectionData)[:200]}"
|
| 83 |
book_ctx = query_book_context(bff_query, top_k=2)
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
system_prompt = (
|
| 86 |
"You are a business consultant helping a Nigerian entrepreneur understand their business better. "
|
| 87 |
"Generate follow-up questions based on what they have already shared. "
|
| 88 |
"Questions MUST use simple, everyday English — no business jargon. "
|
| 89 |
"Think: how would a friendly, experienced mentor ask? "
|
| 90 |
"Return ONLY valid JSON."
|
|
|
|
| 91 |
+ (f"\n\n{book_ctx}" if book_ctx else "")
|
| 92 |
)
|
| 93 |
|
|
@@ -166,6 +230,7 @@ def generate_profile(
|
|
| 166 |
"Write in plain English that ANY entrepreneur can understand. "
|
| 167 |
"Ground all analysis in the Nigerian market context. "
|
| 168 |
"Return ONLY valid JSON."
|
|
|
|
| 169 |
+ (f"\n\n{book_ctx}" if book_ctx else "")
|
| 170 |
)
|
| 171 |
|
|
@@ -276,11 +341,17 @@ each containing a list of 2-4 concise bullet points:
|
|
| 276 |
"businessModel": prompt_bmc,
|
| 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:
|
|
@@ -331,10 +402,17 @@ each containing a list of 2-4 concise bullet points:
|
|
| 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}")
|
|
@@ -344,3 +422,44 @@ each containing a list of 2-4 concise bullet points:
|
|
| 344 |
"outputs": list(results.keys()),
|
| 345 |
"missing": sorted(set(all_outputs.keys()) - present_keys),
|
| 346 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
class GenerateProfileRequest(BaseModel):
|
| 21 |
userId: str
|
| 22 |
businessId: str
|
| 23 |
+
force: bool = False # regenerate outputs even if they already exist (Settings edits)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class ReopenFollowupsRequest(BaseModel):
|
| 27 |
+
businessId: str
|
| 28 |
|
| 29 |
|
| 30 |
# ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
|
|
| 54 |
return sections
|
| 55 |
|
| 56 |
|
| 57 |
+
# ─── Business stage ───────────────────────────────────────────────────────────
|
| 58 |
+
# Keep ids in sync with data/business-stages.ts on the frontend.
|
| 59 |
+
|
| 60 |
+
BUSINESS_STAGES = {
|
| 61 |
+
"idea": "Just an idea — has not started anything yet (Ideation stage)",
|
| 62 |
+
"validation": "Checking if the idea will work — researching and talking to possible customers (Validation / Planning stage)",
|
| 63 |
+
"prelaunch": "Getting ready to launch — registering, raising money, setting up (Pre-launch stage)",
|
| 64 |
+
"launched": "Just started — selling for less than 1 year (Launch / Startup stage)",
|
| 65 |
+
"early": "Running for 1–2 years (Early stage)",
|
| 66 |
+
"growth": "Running for 3–5 years (Growth stage)",
|
| 67 |
+
"established": "Running for 6–10 years (Established / Expansion stage)",
|
| 68 |
+
"mature": "Running for over 10 years (Mature stage)",
|
| 69 |
+
"restarting": "Restarting or changing direction — has run a business before and is starting again or pivoting",
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
PRE_LAUNCH_STAGES = {"idea", "validation", "prelaunch"}
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _get_business_stage(user_id: str, business_id: str) -> str:
|
| 76 |
+
"""Reads the stage saved on the WHAT step. Returns '' if not set."""
|
| 77 |
+
snap = (
|
| 78 |
+
_db().collection("user_data").document(user_id)
|
| 79 |
+
.collection("businesses").document(business_id)
|
| 80 |
+
.collection("bff").document("section1").get()
|
| 81 |
+
)
|
| 82 |
+
return (snap.to_dict() or {}).get("businessStage", "") if snap.exists else ""
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _stage_guidance(stage: str) -> str:
|
| 86 |
+
"""Prompt instructions that keep the AI's tense and assumptions matched to the stage."""
|
| 87 |
+
if stage not in BUSINESS_STAGES:
|
| 88 |
+
return ""
|
| 89 |
+
label = BUSINESS_STAGES[stage]
|
| 90 |
+
if stage in PRE_LAUNCH_STAGES:
|
| 91 |
+
rule = (
|
| 92 |
+
"This business has NOT launched yet. Never assume it has customers, sales, staff, "
|
| 93 |
+
"revenue or a track record. Phrase everything around plans, expectations and "
|
| 94 |
+
"assumptions (e.g. 'Who do you expect to buy this?' not 'Who buys from you?')."
|
| 95 |
+
)
|
| 96 |
+
elif stage == "restarting":
|
| 97 |
+
rule = (
|
| 98 |
+
"The entrepreneur has run a business before and is starting again or changing direction. "
|
| 99 |
+
"Treat the new direction as not yet proven: phrase it around plans, but draw on their "
|
| 100 |
+
"past experience (what worked, what didn't, what they are changing and why)."
|
| 101 |
+
)
|
| 102 |
+
else:
|
| 103 |
+
rule = (
|
| 104 |
+
"This business is already operating. Ask about real experience so far — actual "
|
| 105 |
+
"customers, what sells, what has worked and what hasn't — at a depth that suits "
|
| 106 |
+
"how long it has been running."
|
| 107 |
+
)
|
| 108 |
+
return f"\n\nBUSINESS STAGE: {label}\n{rule}"
|
| 109 |
+
|
| 110 |
+
|
| 111 |
def _call_groq_json(system_prompt: str, user_prompt: str) -> dict:
|
| 112 |
"""
|
| 113 |
Thin wrapper — delegates to the key-rotating Groq client utility.
|
|
|
|
| 141 |
bff_query = f"Business Footing Framework section {body.section} {json.dumps(body.sectionData)[:200]}"
|
| 142 |
book_ctx = query_book_context(bff_query, top_k=2)
|
| 143 |
|
| 144 |
+
# Stage is read server-side so WHY/HOW follow-ups get it too, not just WHAT
|
| 145 |
+
stage = body.sectionData.get("businessStage") if body.section == 1 else None
|
| 146 |
+
stage = stage or _get_business_stage(user["uid"], body.businessId)
|
| 147 |
+
|
| 148 |
system_prompt = (
|
| 149 |
"You are a business consultant helping a Nigerian entrepreneur understand their business better. "
|
| 150 |
"Generate follow-up questions based on what they have already shared. "
|
| 151 |
"Questions MUST use simple, everyday English — no business jargon. "
|
| 152 |
"Think: how would a friendly, experienced mentor ask? "
|
| 153 |
"Return ONLY valid JSON."
|
| 154 |
+
+ _stage_guidance(stage)
|
| 155 |
+ (f"\n\n{book_ctx}" if book_ctx else "")
|
| 156 |
)
|
| 157 |
|
|
|
|
| 230 |
"Write in plain English that ANY entrepreneur can understand. "
|
| 231 |
"Ground all analysis in the Nigerian market context. "
|
| 232 |
"Return ONLY valid JSON."
|
| 233 |
+
+ _stage_guidance(bff.get("section1", {}).get("businessStage", ""))
|
| 234 |
+ (f"\n\n{book_ctx}" if book_ctx else "")
|
| 235 |
)
|
| 236 |
|
|
|
|
| 341 |
"businessModel": prompt_bmc,
|
| 342 |
}
|
| 343 |
|
| 344 |
+
# profileStale is set by /reopen-followups: the old profile was built without
|
| 345 |
+
# follow-up answers, so it must be rebuilt even if the outputs already exist.
|
| 346 |
+
# Read server-side so it survives the user leaving and resuming later.
|
| 347 |
+
biz_snap = biz_ref_check.get()
|
| 348 |
+
force = body.force or bool((biz_snap.to_dict() or {}).get("profileStale"))
|
| 349 |
+
|
| 350 |
call_plan = []
|
| 351 |
skipped_keys: set = set()
|
| 352 |
for key, prompt_fn in all_outputs.items():
|
| 353 |
+
existing = None if force else biz_ref_check.collection("profile_output").document(key).get()
|
| 354 |
+
if existing is not None and existing.exists:
|
| 355 |
print(f"[bff] Skipping {key} — already exists in Firestore")
|
| 356 |
skipped_keys.add(key)
|
| 357 |
else:
|
|
|
|
| 402 |
# business look "complete" with an empty/partial profile (see
|
| 403 |
# project_multibusiness_gate memory — the manual-Console-flip incident
|
| 404 |
# had the same shape of bug).
|
| 405 |
+
# On a forced run, outputs that failed to regenerate still exist from before,
|
| 406 |
+
# so count them as present rather than blocking completion.
|
| 407 |
+
if force:
|
| 408 |
+
skipped_keys = {
|
| 409 |
+
k for k in all_outputs
|
| 410 |
+
if k not in results and biz_ref.collection("profile_output").document(k).get().exists
|
| 411 |
+
}
|
| 412 |
present_keys = skipped_keys | set(results.keys())
|
| 413 |
all_present = present_keys == set(all_outputs.keys())
|
| 414 |
if all_present:
|
| 415 |
+
biz_ref.update({"bffCompleted": True, "profileStale": False})
|
| 416 |
else:
|
| 417 |
missing = set(all_outputs.keys()) - present_keys
|
| 418 |
print(f"[bff] NOT marking bffCompleted — missing outputs: {missing}")
|
|
|
|
| 422 |
"outputs": list(results.keys()),
|
| 423 |
"missing": sorted(set(all_outputs.keys()) - present_keys),
|
| 424 |
}
|
| 425 |
+
|
| 426 |
+
|
| 427 |
+
# ─── Route: Reopen onboarding for missing follow-ups ─────────────────────────
|
| 428 |
+
|
| 429 |
+
def _missing_followup_sections(user_id: str, business_id: str) -> list[int]:
|
| 430 |
+
"""Sections (1-3) whose follow-up questions were never generated/saved."""
|
| 431 |
+
bff_ref = (
|
| 432 |
+
_db().collection("user_data").document(user_id)
|
| 433 |
+
.collection("businesses").document(business_id)
|
| 434 |
+
.collection("bff")
|
| 435 |
+
)
|
| 436 |
+
missing = []
|
| 437 |
+
for section in (1, 2, 3):
|
| 438 |
+
snap = bff_ref.document(f"section{section}_followups").get()
|
| 439 |
+
if not snap.exists or not (snap.to_dict() or {}).get("questions"):
|
| 440 |
+
missing.append(section)
|
| 441 |
+
return missing
|
| 442 |
+
|
| 443 |
+
|
| 444 |
+
@router.post("/reopen-followups")
|
| 445 |
+
def reopen_followups(
|
| 446 |
+
body: ReopenFollowupsRequest,
|
| 447 |
+
user: dict = Depends(verify_firebase_token),
|
| 448 |
+
):
|
| 449 |
+
"""
|
| 450 |
+
A profile was generated while some follow-up steps failed or were skipped.
|
| 451 |
+
Marks the business incomplete (bffCompleted is backend-only per Firestore
|
| 452 |
+
rules) and flags the profile stale so the next generate-profile rebuilds it
|
| 453 |
+
with the follow-up answers included.
|
| 454 |
+
"""
|
| 455 |
+
biz_ref = (
|
| 456 |
+
_db().collection("user_data").document(user["uid"])
|
| 457 |
+
.collection("businesses").document(body.businessId)
|
| 458 |
+
)
|
| 459 |
+
if not biz_ref.get().exists:
|
| 460 |
+
raise HTTPException(status_code=404, detail="Business not found.")
|
| 461 |
+
|
| 462 |
+
missing = _missing_followup_sections(user["uid"], body.businessId)
|
| 463 |
+
if missing:
|
| 464 |
+
biz_ref.update({"bffCompleted": False, "profileStale": True})
|
| 465 |
+
return {"missing": missing}
|