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

Sync from GitHub · 2026-09-13 10:42 UTC

Browse files
Files changed (1) hide show
  1. 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}