Spaces:
Running
Running
GitHub Actions commited on
Commit ·
020705e
1
Parent(s): 889e86b
Sync from GitHub · 2026-09-26 23:46 UTC
Browse files- routes/bff.py +4 -4
- routes/chat.py +1 -0
- routes/consultation.py +2 -2
- routes/documents.py +2 -2
- routes/planning.py +4 -1
- routes/transcribe.py +1 -0
- tests/fixtures/README.md +9 -0
- tests/fixtures/fake-service-account.json +13 -0
- tests/fixtures/groq/businessIdentity.json +10 -0
- tests/fixtures/groq/businessModel.json +11 -0
- tests/fixtures/groq/business_plan.json +11 -0
- tests/fixtures/groq/chat_response.txt +1 -0
- tests/fixtures/groq/consultation_report.json +27 -0
- tests/fixtures/groq/consultation_start.json +10 -0
- tests/fixtures/groq/customerProfile.json +17 -0
- tests/fixtures/groq/followups.json +27 -0
- tests/fixtures/groq/marketData.json +20 -0
- tests/fixtures/groq/pitch_deck.json +30 -0
- tests/fixtures/groq/planning_milestones.json +9 -0
- tests/fixtures/groq/planning_tasks.json +9 -0
- tests/fixtures/groq/transcript.json +3 -0
- tests/test_groq_fixture_mode.py +58 -0
- utils/groq_client.py +70 -0
routes/bff.py
CHANGED
|
@@ -109,7 +109,7 @@ def _stage_guidance(stage: str) -> str:
|
|
| 109 |
return f"\n\nBUSINESS STAGE: {label}\n{rule}"
|
| 110 |
|
| 111 |
|
| 112 |
-
def _call_groq_json(system_prompt: str, user_prompt: str) -> dict:
|
| 113 |
"""
|
| 114 |
Thin wrapper — delegates to the key-rotating Groq client utility.
|
| 115 |
|
|
@@ -118,7 +118,7 @@ def _call_groq_json(system_prompt: str, user_prompt: str) -> dict:
|
|
| 118 |
reasoning before writing the final JSON — a tight budget here truncates
|
| 119 |
the JSON output and fails to parse.
|
| 120 |
"""
|
| 121 |
-
return call_groq_json(system_prompt, user_prompt, model=HEAVY_MODEL, max_tokens=3000)
|
| 122 |
|
| 123 |
|
| 124 |
# ─── Route: Generate follow-up questions ─────────────────────────────────────
|
|
@@ -185,7 +185,7 @@ Question types:
|
|
| 185 |
"""
|
| 186 |
|
| 187 |
try:
|
| 188 |
-
data = _call_groq_json(system_prompt, user_prompt)
|
| 189 |
return data
|
| 190 |
except Exception as e:
|
| 191 |
raise HTTPException(status_code=500, detail=f"Follow-up generation failed: {str(e)}")
|
|
@@ -370,7 +370,7 @@ each containing a list of 2-4 concise bullet points:
|
|
| 370 |
if i > 0:
|
| 371 |
time.sleep(15) # 15s gap: ~3,000 tokens recovered at 200 tok/s
|
| 372 |
try:
|
| 373 |
-
results[key] = _call_groq_json(base_system, prompt_fn())
|
| 374 |
except Exception as e:
|
| 375 |
errors.append(f"{key}: {str(e)}")
|
| 376 |
print(f"[bff] Failed to generate {key}: {e}")
|
|
|
|
| 109 |
return f"\n\nBUSINESS STAGE: {label}\n{rule}"
|
| 110 |
|
| 111 |
|
| 112 |
+
def _call_groq_json(system_prompt: str, user_prompt: str, fixture: str | None = None) -> dict:
|
| 113 |
"""
|
| 114 |
Thin wrapper — delegates to the key-rotating Groq client utility.
|
| 115 |
|
|
|
|
| 118 |
reasoning before writing the final JSON — a tight budget here truncates
|
| 119 |
the JSON output and fails to parse.
|
| 120 |
"""
|
| 121 |
+
return call_groq_json(system_prompt, user_prompt, model=HEAVY_MODEL, max_tokens=3000, fixture=fixture)
|
| 122 |
|
| 123 |
|
| 124 |
# ─── Route: Generate follow-up questions ─────────────────────────────────────
|
|
|
|
| 185 |
"""
|
| 186 |
|
| 187 |
try:
|
| 188 |
+
data = _call_groq_json(system_prompt, user_prompt, fixture="followups")
|
| 189 |
return data
|
| 190 |
except Exception as e:
|
| 191 |
raise HTTPException(status_code=500, detail=f"Follow-up generation failed: {str(e)}")
|
|
|
|
| 370 |
if i > 0:
|
| 371 |
time.sleep(15) # 15s gap: ~3,000 tokens recovered at 200 tok/s
|
| 372 |
try:
|
| 373 |
+
results[key] = _call_groq_json(base_system, prompt_fn(), fixture=key)
|
| 374 |
except Exception as e:
|
| 375 |
errors.append(f"{key}: {str(e)}")
|
| 376 |
print(f"[bff] Failed to generate {key}: {e}")
|
routes/chat.py
CHANGED
|
@@ -113,6 +113,7 @@ RULES:
|
|
| 113 |
user_prompt=body.message,
|
| 114 |
model=model,
|
| 115 |
max_tokens=512,
|
|
|
|
| 116 |
)
|
| 117 |
for chunk in stream_response:
|
| 118 |
token = chunk.choices[0].delta.content
|
|
|
|
| 113 |
user_prompt=body.message,
|
| 114 |
model=model,
|
| 115 |
max_tokens=512,
|
| 116 |
+
fixture="chat_response",
|
| 117 |
)
|
| 118 |
for chunk in stream_response:
|
| 119 |
token = chunk.choices[0].delta.content
|
routes/consultation.py
CHANGED
|
@@ -106,7 +106,7 @@ Return JSON:
|
|
| 106 |
}}"""
|
| 107 |
|
| 108 |
try:
|
| 109 |
-
result = call_groq_json(system, prompt, model=HEAVY_MODEL, max_tokens=800)
|
| 110 |
return {
|
| 111 |
"openingMessage": result.get("openingMessage", ""),
|
| 112 |
"focusAreas": result.get("focusAreas", []),
|
|
@@ -180,7 +180,7 @@ Write a formal consultation report. Return JSON:
|
|
| 180 |
}}"""
|
| 181 |
|
| 182 |
try:
|
| 183 |
-
result = call_groq_json(system, prompt, model=HEAVY_MODEL, max_tokens=2000)
|
| 184 |
return {"report": result}
|
| 185 |
except Exception as e:
|
| 186 |
raise HTTPException(status_code=500, detail=f"Report generation failed: {str(e)}")
|
|
|
|
| 106 |
}}"""
|
| 107 |
|
| 108 |
try:
|
| 109 |
+
result = call_groq_json(system, prompt, model=HEAVY_MODEL, max_tokens=800, fixture="consultation_start")
|
| 110 |
return {
|
| 111 |
"openingMessage": result.get("openingMessage", ""),
|
| 112 |
"focusAreas": result.get("focusAreas", []),
|
|
|
|
| 180 |
}}"""
|
| 181 |
|
| 182 |
try:
|
| 183 |
+
result = call_groq_json(system, prompt, model=HEAVY_MODEL, max_tokens=2000, fixture="consultation_report")
|
| 184 |
return {"report": result}
|
| 185 |
except Exception as e:
|
| 186 |
raise HTTPException(status_code=500, detail=f"Report generation failed: {str(e)}")
|
routes/documents.py
CHANGED
|
@@ -82,7 +82,7 @@ Generate a comprehensive Business Plan. Return JSON:
|
|
| 82 |
"financialSummary": "Startup costs, expected revenue, break-even estimate",
|
| 83 |
"implementationTimeline": "Key milestones for the first 12 months"
|
| 84 |
}}"""
|
| 85 |
-
return call_groq_json(system, prompt, model=HEAVY_MODEL, max_tokens=2500)
|
| 86 |
|
| 87 |
|
| 88 |
# ─── Pitch Deck ────────────────────────────────────────────────────────────────
|
|
@@ -130,7 +130,7 @@ Generate a 7-slide Pitch Deck. Return JSON where each key is a slide:
|
|
| 130 |
"bullets": ["Seeking: ...", "Use of funds: ...", "Expected outcome: ..."]
|
| 131 |
}}
|
| 132 |
}}"""
|
| 133 |
-
return call_groq_json(system, prompt, model=HEAVY_MODEL, max_tokens=2000)
|
| 134 |
|
| 135 |
|
| 136 |
# ─── Route ────────────────────────────────────────────────────────────────────
|
|
|
|
| 82 |
"financialSummary": "Startup costs, expected revenue, break-even estimate",
|
| 83 |
"implementationTimeline": "Key milestones for the first 12 months"
|
| 84 |
}}"""
|
| 85 |
+
return call_groq_json(system, prompt, model=HEAVY_MODEL, max_tokens=2500, fixture="business_plan")
|
| 86 |
|
| 87 |
|
| 88 |
# ─── Pitch Deck ────────────────────────────────────────────────────────────────
|
|
|
|
| 130 |
"bullets": ["Seeking: ...", "Use of funds: ...", "Expected outcome: ..."]
|
| 131 |
}}
|
| 132 |
}}"""
|
| 133 |
+
return call_groq_json(system, prompt, model=HEAVY_MODEL, max_tokens=2000, fixture="pitch_deck")
|
| 134 |
|
| 135 |
|
| 136 |
# ─── Route ────────────────────────────────────────────────────────────────────
|
routes/planning.py
CHANGED
|
@@ -130,7 +130,10 @@ Return JSON:
|
|
| 130 |
"""
|
| 131 |
|
| 132 |
try:
|
| 133 |
-
result = call_groq_json(
|
|
|
|
|
|
|
|
|
|
| 134 |
suggestions = result.get("suggestions", [])
|
| 135 |
if not suggestions:
|
| 136 |
raise ValueError("No suggestions returned")
|
|
|
|
| 130 |
"""
|
| 131 |
|
| 132 |
try:
|
| 133 |
+
result = call_groq_json(
|
| 134 |
+
system, prompt, model=HEAVY_MODEL, max_tokens=800,
|
| 135 |
+
fixture=f"planning_{body.type}",
|
| 136 |
+
)
|
| 137 |
suggestions = result.get("suggestions", [])
|
| 138 |
if not suggestions:
|
| 139 |
raise ValueError("No suggestions returned")
|
routes/transcribe.py
CHANGED
|
@@ -51,6 +51,7 @@ async def transcribe_audio(
|
|
| 51 |
file=(audio.filename or "recording.webm", audio_bytes, content_type),
|
| 52 |
response_format="json",
|
| 53 |
language="en",
|
|
|
|
| 54 |
)
|
| 55 |
return {"transcript": transcription.text}
|
| 56 |
|
|
|
|
| 51 |
file=(audio.filename or "recording.webm", audio_bytes, content_type),
|
| 52 |
response_format="json",
|
| 53 |
language="en",
|
| 54 |
+
fixture="transcript",
|
| 55 |
)
|
| 56 |
return {"transcript": transcription.text}
|
| 57 |
|
tests/fixtures/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Test fixtures
|
| 2 |
+
|
| 3 |
+
- `groq/` — canned Groq responses for `E2E_FIXTURE_MODE` (see `utils/groq_client.py`, ADR-0013).
|
| 4 |
+
- `fake-service-account.json` — **not a real credential.** A throwaway RSA key generated locally
|
| 5 |
+
(`openssl genpkey`) and never registered with any Google Cloud project. It exists only so
|
| 6 |
+
`firebase_admin.credentials.Certificate()` has a syntactically valid key to parse when the
|
| 7 |
+
backend initializes against the local Firebase Emulator Suite in E2E tests
|
| 8 |
+
(`playwright.config.ts`) — the emulators never validate it against real Google infrastructure.
|
| 9 |
+
If a secret scanner flags this file: it's a known, intentional fixture, not a leaked key.
|
tests/fixtures/fake-service-account.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"type": "service_account",
|
| 3 |
+
"project_id": "venthrive-e2e-demo",
|
| 4 |
+
"private_key_id": "e2e-fake-key-id",
|
| 5 |
+
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC47ouHuvQe0Irn\nix5oJ7DZDhfrgot0D23dYwV6cdmlpvTKyeDUWwrgHbPmTUJ5ouo3UvwG7GeiEM3t\n0cDWNL/WkeW+FS5RodY9GsfFCZJNBGUfIdjLLyWN3LMT92njkMu7pLRvuC/Cvi/i\nOKzRCwE8bY+HfDDAVCwNnacVlM6c/sHY23QJ2rKVbOcbdXa18rtSEHwHLjMfFyr/\nVokMc21dyPTyqt0QERC1qMgFoaCDj2gEHmZRq06ZqSsqJoY0hH/HvH/bqqqYzuZN\n8w8QU2Ft/4Yl3C5yPg/Hyjar/92giMf0+j9w+HxKpMK73NXLOPGEq33jkUiy0OXF\niv37vFPTAgMBAAECggEAJqpljQL3QKwcps3NFTzbOn66XJjmXGVPdL7b6d59AagX\nPYEZG31PmE1NjGTx9ams69VTcYUjI9z8mXFGLuNHiu7lSKl4DkRWnV8IjrdyGd1F\n4Uz73Xulj/Qpkw6jWpuvJTQKL+OkRMTsERIFfA2ffAlyPYMnVEclENG6GgaLboJw\nLQv56tcD+LDuqDPmP+AWKvNeLurOgS+UjcF1s7ImGJq6iA1IscuXwCFlCUHIw+yW\nTrjh1vgzra5kDM1HbFK4cc6TbQ7PFt9L9BdR4HRBKj93tG8oWT/3tggGMqmw10j+\nRCBrNp0looDXMQWk9ZPzCUzjDFeH6FWGsGkWnhNgeQKBgQDsOuMCkxec7/AasRwv\nOLJAob+frXTBkgMHASQ/smPMmvLRIOXmjPz3aosZfaE++RbfAtfgF7D9rcceAaDF\n5n5l8DJvyaMpnkTJm7KPzr96EoDau1nb0E9fxb2busbtTV74C/UmCj7dI1jdBt5S\n7SHjRBlar8lTkVI47gaOtoi4+QKBgQDIaJ5+9M5o6VXpyp6ojz8TxjYaxgJ3aIPX\nEeWo/r4g5EzudNEjtDyaIngOauDkEFg6ecjrUkrmuL9ChZpdBdvwOcXORtdKUai+\n5HdLS11zYmVFdPlV9bWJri/8bNt/P4WD7OSkF4NQ0skfupCQUKBOYEIYHn//EpPx\n7CNLFpHSKwKBgQCYPasncZx6K7fVu+aqGCPou1U/lw3z4iHJVdcCpfjjV8i9NjJz\nztEIssVKfEh6yRUQTEjLxvjtxBzbHbl7UPyMvawDlzNsEu/J7mm5fEiyZ1JbpPpY\nS3ucAVgdENY7AO3/ddVb2qaA5dLTPyXw05pPWQfuBfP9w132CpB3UPP0IQKBgQCv\nNtirwrjb/HVCrAW+9U5+qWMQUWJCp7vswxb5g48TDsc8DCH1h1UcpVo1sLfibW/V\n4DgLla0qvwdoz4pAHilGhBwu1rzPm0lfbDvoNoIDqVlFOpnrz6DiOMp3dW0Keazv\nxKO521zRwxhYhzJxGiytmXbvw2rAxatXbc8FjTv8dQKBgGf1WUk/596vqjmhGyKa\nqQuyTTGXKUCAEXduM7OedAIFoBIzHTKvFtBEDRpisDLOtwNd42RaTlgl4m5CyvJ9\nAA0aDtcPcVcVihf2QkQRy+7wb6psQMIHVL7K4MookW1Uklxy6Fdm6cQhmoWauw5n\n459M3aPoqVx/aqViD82UwLOf\n-----END PRIVATE KEY-----\n",
|
| 6 |
+
"client_email": "e2e-fake@venthrive-e2e-demo.iam.gserviceaccount.com",
|
| 7 |
+
"client_id": "000000000000000000000",
|
| 8 |
+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
| 9 |
+
"token_uri": "https://oauth2.googleapis.com/token",
|
| 10 |
+
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
| 11 |
+
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/e2e-fake%40venthrive-e2e-demo.iam.gserviceaccount.com",
|
| 12 |
+
"universe_domain": "googleapis.com"
|
| 13 |
+
}
|
tests/fixtures/groq/businessIdentity.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"businessName": "Ada's Fresh Kitchen",
|
| 3 |
+
"sector": "Food & Beverage",
|
| 4 |
+
"subsector": "Meal Prep & Catering",
|
| 5 |
+
"niche": "Healthy lunch delivery for office workers",
|
| 6 |
+
"summary": "Ada's Fresh Kitchen prepares and delivers healthy, affordable lunches to office workers in Lagos who don't have time to cook. Every meal is made fresh that morning using locally sourced ingredients.",
|
| 7 |
+
"valueProposition": "Fresh, healthy lunch delivered to your desk before 12pm, every workday.",
|
| 8 |
+
"brandVoice": "warm, reliable, no-nonsense",
|
| 9 |
+
"tagline": "Fresh food, fast desk."
|
| 10 |
+
}
|
tests/fixtures/groq/businessModel.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"keyPartners": ["Local vegetable and grain suppliers", "Dispatch rider network (Gokada, Max.ng)", "Office building facility managers"],
|
| 3 |
+
"keyActivities": ["Daily meal prep and cooking", "Order-taking and route planning", "Quality control and packaging"],
|
| 4 |
+
"keyResources": ["Commercial kitchen space", "Delivery riders", "Recurring customer subscription list"],
|
| 5 |
+
"valuePropositions": ["Fresh, healthy lunch delivered before 12pm", "Consistent quality and pricing", "Subscription convenience — no daily ordering hassle"],
|
| 6 |
+
"customerRelationships": ["WhatsApp broadcast list for weekly menus", "Subscription plans with loyalty discounts", "Direct feedback line for complaints"],
|
| 7 |
+
"channels": ["WhatsApp ordering", "Office cluster word-of-mouth", "Instagram for menu showcases"],
|
| 8 |
+
"customerSegments": ["Office workers in Lagos mainland business districts", "Small companies ordering team lunches"],
|
| 9 |
+
"costStructure": ["Ingredient sourcing", "Rider delivery fees", "Kitchen rent and utilities", "Packaging"],
|
| 10 |
+
"revenueStreams": ["Weekly/monthly lunch subscriptions", "One-off bulk office orders", "Corporate catering for events"]
|
| 11 |
+
}
|
tests/fixtures/groq/business_plan.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"executiveSummary": "Ada's Fresh Kitchen delivers fresh, healthy lunches to office workers in Lagos who lack the time to cook and are wary of street food. By preparing meals fresh each morning and delivering directly to office clusters, the business aims to become the trusted daily lunch option for busy professionals. The goal for the next 12 months is to grow from a single-cook operation to a small team serving 300+ weekly subscribers across three office clusters.",
|
| 3 |
+
"businessOpportunity": "Office workers in Lagos mainland business districts spend significant time and money on inconsistent lunch options — either unreliable street food or slow, expensive delivery apps. There is a clear gap for a trusted, healthy, consistently-timed lunch service.",
|
| 4 |
+
"companyDescription": "Ada's Fresh Kitchen is a sole-proprietor meal-prep and delivery business based in Lagos, operating out of a small commercial kitchen and delivering via contracted riders.",
|
| 5 |
+
"targetMarket": "Office workers aged 25-40 in Ikeja, Yaba, and Maryland business districts, middle-income, health-conscious, and time-constrained.",
|
| 6 |
+
"productsServices": "Daily fresh lunch plates and weekly subscription plans, with menu rotation across 5 core meals. Pricing targets ₦1,500-₦2,500 per meal, with a subscription discount for weekly commitments.",
|
| 7 |
+
"marketingStrategy": "WhatsApp-based ordering and weekly menu broadcasts, office-cluster word-of-mouth referrals, and Instagram content showcasing daily meals.",
|
| 8 |
+
"operationsPlan": "Meals are prepared each morning in a rented commercial kitchen, packaged, and dispatched via contracted riders on fixed delivery routes timed to arrive before 12pm.",
|
| 9 |
+
"financialSummary": "Estimated startup costs of ₦800,000 (kitchen equipment, initial ingredient stock, packaging). Break-even is projected around month 5 at approximately 150 weekly subscribers.",
|
| 10 |
+
"implementationTimeline": "Month 1-2: formalize subscription plan and onboard first 50 subscribers. Month 3-6: expand to a second office cluster. Month 7-12: hire a second cook and add a third delivery route."
|
| 11 |
+
}
|
tests/fixtures/groq/chat_response.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
That's a great question. For a lunch delivery business like yours, the fastest way to build repeat customers is a weekly subscription — it gives you predictable income and gives them one less decision to make each morning. Start with a simple 5-day plan at a slight discount versus ordering daily, and promote it directly to the office clusters you already deliver to. Track who renews each week so you know which offices are your strongest base.
|
tests/fixtures/groq/consultation_report.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"date": "September 26, 2026",
|
| 3 |
+
"sessionSummary": "We discussed customer acquisition and retention for Ada's Fresh Kitchen, focusing on how to convert one-off lunch orders into a reliable weekly subscription base.",
|
| 4 |
+
"keyFindings": [
|
| 5 |
+
"Most current orders are one-off rather than recurring, leaving revenue unpredictable week to week.",
|
| 6 |
+
"Word-of-mouth within office clusters is the strongest existing acquisition channel.",
|
| 7 |
+
"There is no simple way for customers to commit to a recurring order today."
|
| 8 |
+
],
|
| 9 |
+
"recommendations": [
|
| 10 |
+
{
|
| 11 |
+
"action": "Launch a 5-day weekly subscription plan at a small discount versus daily ordering",
|
| 12 |
+
"rationale": "Gives Ada's Fresh Kitchen predictable weekly revenue and removes the daily ordering decision for customers",
|
| 13 |
+
"timeline": "This week"
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
"action": "Ask every satisfied customer to refer one colleague in their office",
|
| 17 |
+
"rationale": "Office clusters are already the strongest channel — formalizing referrals compounds it",
|
| 18 |
+
"timeline": "Within 30 days"
|
| 19 |
+
}
|
| 20 |
+
],
|
| 21 |
+
"nextSteps": [
|
| 22 |
+
"Draft the subscription pricing and share it with 10 existing customers for feedback",
|
| 23 |
+
"Set up a simple WhatsApp broadcast list for weekly menu announcements",
|
| 24 |
+
"Track subscription renewal rate weekly for the first month"
|
| 25 |
+
],
|
| 26 |
+
"closingNote": "Ada's Fresh Kitchen already has loyal customers — the subscription model just needs to make it easy for them to keep coming back."
|
| 27 |
+
}
|
tests/fixtures/groq/consultation_start.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"openingMessage": "Hi! I've reviewed the Business Footing Framework profile for Ada's Fresh Kitchen — a fresh, healthy lunch delivery service for Lagos office workers. It's a solid foundation with a clear value proposition. Today, I'd like you to pick one area to dig into together:\n\n1. Strategy & Market Position\n2. Customer Acquisition & Retention\n3. Financial Planning & Pricing\n4. Operations & Standard Processes\n5. Investor Readiness & Growth\n\nWhich one would be most useful to focus on right now?",
|
| 3 |
+
"focusAreas": [
|
| 4 |
+
"Strategy & Market Position",
|
| 5 |
+
"Customer Acquisition & Retention",
|
| 6 |
+
"Financial Planning & Pricing",
|
| 7 |
+
"Operations & Standard Processes",
|
| 8 |
+
"Investor Readiness & Growth"
|
| 9 |
+
]
|
| 10 |
+
}
|
tests/fixtures/groq/customerProfile.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"demographics": {
|
| 3 |
+
"ageRange": "25-40",
|
| 4 |
+
"gender": "mixed, slightly more female",
|
| 5 |
+
"location": "Lagos mainland office districts (Ikeja, Yaba, Maryland)",
|
| 6 |
+
"incomeLevel": "middle",
|
| 7 |
+
"educationLevel": "mostly tertiary-educated professionals",
|
| 8 |
+
"employmentStatus": "full-time office employees"
|
| 9 |
+
},
|
| 10 |
+
"psychographics": {
|
| 11 |
+
"values": ["convenience", "health", "value for money", "reliability", "time"],
|
| 12 |
+
"lifestyle": "Busy weekday routine, limited time for meal prep, health-conscious but budget-aware.",
|
| 13 |
+
"painPoints": ["No time to cook during the week", "Street food feels unhealthy", "Restaurant delivery is slow and expensive"],
|
| 14 |
+
"motivations": ["Wants to eat healthy without the effort", "Values getting lunch on time so they don't lose their break", "Prefers a trustworthy, consistent vendor over trying something new every day"]
|
| 15 |
+
},
|
| 16 |
+
"buyerPersona": "Meet Amaka, a 29-year-old marketing executive in Ikeja. She's at her desk by 8am and rarely leaves for lunch. She used to grab suya from a roadside vendor but started worrying about hygiene and wants something that fits her fitness goals. She'd happily pay a bit more for a lunch she trusts, as long as it shows up on time."
|
| 17 |
+
}
|
tests/fixtures/groq/followups.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"questions": [
|
| 3 |
+
{
|
| 4 |
+
"id": "q1",
|
| 5 |
+
"question": "How many meals do you currently make in a typical day?",
|
| 6 |
+
"type": "text",
|
| 7 |
+
"placeholder": "e.g. around 30 meals"
|
| 8 |
+
},
|
| 9 |
+
{
|
| 10 |
+
"id": "q2",
|
| 11 |
+
"question": "Do you deliver the food yourself, or does someone else handle delivery?",
|
| 12 |
+
"type": "select",
|
| 13 |
+
"options": ["I deliver myself", "I have a rider", "Customers pick up"]
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
"id": "q3",
|
| 17 |
+
"question": "Have you had any repeat customers who order from you every week?",
|
| 18 |
+
"type": "boolean"
|
| 19 |
+
},
|
| 20 |
+
{
|
| 21 |
+
"id": "q4",
|
| 22 |
+
"question": "What's the biggest challenge you're facing right now?",
|
| 23 |
+
"type": "text",
|
| 24 |
+
"placeholder": "e.g. finding reliable delivery riders"
|
| 25 |
+
}
|
| 26 |
+
]
|
| 27 |
+
}
|
tests/fixtures/groq/marketData.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"tam": "The Nigerian food delivery and meal-prep market is estimated at over ₦500 billion annually, driven by urban office workers.",
|
| 3 |
+
"sam": "Office-worker lunch delivery within Lagos mainland business districts — roughly ₦15 billion annually.",
|
| 4 |
+
"som": "A realistic first-year target is 200-400 regular subscribers across 2-3 office clusters, roughly ₦40-60 million in annual revenue.",
|
| 5 |
+
"swot": {
|
| 6 |
+
"strengths": ["Fresh, made-to-order meals", "Direct relationships with office clusters", "Low overhead (no dine-in space needed)"],
|
| 7 |
+
"weaknesses": ["Limited delivery radius without more riders", "Dependent on consistent ingredient sourcing", "Brand still unknown outside initial customer base"],
|
| 8 |
+
"opportunities": ["Growing health-consciousness among young professionals", "Rise of corporate wellness budgets that could subsidize lunch", "Partnerships with office building management"],
|
| 9 |
+
"threats": ["Established food delivery apps (Chowdeck, Glovo) entering the same space", "Fuel and ingredient price volatility raising delivery costs", "Traffic delays affecting on-time delivery reputation"]
|
| 10 |
+
},
|
| 11 |
+
"pestle": {
|
| 12 |
+
"political": "Lagos State's food safety regulations are becoming stricter — proactive NAFDAC-aligned hygiene practices are a differentiator.",
|
| 13 |
+
"economic": "Inflation pressures ingredient costs; subscription pricing must be revisited quarterly.",
|
| 14 |
+
"social": "Increasing dual-income households and long commutes are pushing more workers toward pre-made meals over home cooking.",
|
| 15 |
+
"technological": "WhatsApp-based ordering and mobile payments (Moniepoint, OPay) make low-tech order-taking viable without a full app.",
|
| 16 |
+
"legal": "Food handling permits and NAFDAC registration required for packaged meal sales.",
|
| 17 |
+
"environmental": "Packaging waste is a growing concern; biodegradable containers could become a selling point."
|
| 18 |
+
},
|
| 19 |
+
"competitiveLandscape": "Competes with informal roadside food vendors on price and with app-based delivery (Chowdeck, Glovo) on convenience. Differentiation comes from a curated healthy menu and direct office-cluster relationships rather than trying to out-scale the delivery apps."
|
| 20 |
+
}
|
tests/fixtures/groq/pitch_deck.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"slide1_problem": {
|
| 3 |
+
"title": "The Problem",
|
| 4 |
+
"bullets": ["Office workers waste time and money on unreliable lunch options", "Street food raises hygiene concerns", "Delivery apps are slow and expensive for daily use"]
|
| 5 |
+
},
|
| 6 |
+
"slide2_solution": {
|
| 7 |
+
"title": "Our Solution",
|
| 8 |
+
"bullets": ["Fresh, healthy lunch delivered before 12pm", "Direct relationships with office clusters", "Simple WhatsApp-based subscription ordering"]
|
| 9 |
+
},
|
| 10 |
+
"slide3_market": {
|
| 11 |
+
"title": "Market Opportunity",
|
| 12 |
+
"bullets": ["TAM: ₦500B+ Nigerian food delivery market", "SAM: ₦15B Lagos office-lunch segment", "SOM: 200-400 subscribers in year one, ~₦40-60M revenue"]
|
| 13 |
+
},
|
| 14 |
+
"slide4_business_model": {
|
| 15 |
+
"title": "How We Make Money",
|
| 16 |
+
"bullets": ["Weekly/monthly subscription plans", "One-off bulk office orders", "Corporate event catering"]
|
| 17 |
+
},
|
| 18 |
+
"slide5_traction": {
|
| 19 |
+
"title": "Traction & Milestones",
|
| 20 |
+
"bullets": ["Serving 3 office buildings today", "Growing repeat-customer base via word of mouth", "Planning first formal subscription launch"]
|
| 21 |
+
},
|
| 22 |
+
"slide6_strategy": {
|
| 23 |
+
"title": "Go-To-Market Strategy",
|
| 24 |
+
"bullets": ["Expand cluster by cluster, not city-wide", "Referral incentives within existing offices", "WhatsApp and Instagram over paid ads"]
|
| 25 |
+
},
|
| 26 |
+
"slide7_ask": {
|
| 27 |
+
"title": "The Ask",
|
| 28 |
+
"bullets": ["Seeking: ₦2,000,000 working capital", "Use of funds: second kitchen hire, delivery bikes, packaging", "Expected outcome: 3 office clusters served within 12 months"]
|
| 29 |
+
}
|
| 30 |
+
}
|
tests/fixtures/groq/planning_milestones.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"suggestions": [
|
| 3 |
+
{"title": "Launch first formal weekly subscription plan", "targetDate": "2026-10-26"},
|
| 4 |
+
{"title": "Reach 100 active weekly subscribers", "targetDate": "2026-11-25"},
|
| 5 |
+
{"title": "Expand delivery to a second office cluster", "targetDate": "2026-12-25"},
|
| 6 |
+
{"title": "Hire a second cook to support kitchen capacity", "targetDate": "2027-03-25"},
|
| 7 |
+
{"title": "Reach 300 active weekly subscribers across three clusters", "targetDate": "2027-09-26"}
|
| 8 |
+
]
|
| 9 |
+
}
|
tests/fixtures/groq/planning_tasks.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"suggestions": [
|
| 3 |
+
{"title": "Draft a weekly subscription pricing sheet", "description": "Gives Ada's Fresh Kitchen a concrete offer to test with existing customers this week", "column": "todo"},
|
| 4 |
+
{"title": "Call 10 regular customers to pitch the subscription plan", "description": "Direct feedback validates pricing before a wider launch", "column": "todo"},
|
| 5 |
+
{"title": "Set up a WhatsApp broadcast list for weekly menus", "description": "Low-cost channel to keep customers engaged between orders", "column": "todo"},
|
| 6 |
+
{"title": "Confirm rider availability for a second delivery route", "description": "Needed before expanding to a second office cluster", "column": "todo"},
|
| 7 |
+
{"title": "Order packaging supplies for the next two weeks", "description": "Avoids running out mid-week during a busy order period", "column": "in_progress"}
|
| 8 |
+
]
|
| 9 |
+
}
|
tests/fixtures/groq/transcript.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"text": "I make about thirty meals a day right now and I deliver most of them myself on my motorbike."
|
| 3 |
+
}
|
tests/test_groq_fixture_mode.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Unit tests for the E2E fixture-mode switch in utils/groq_client.py.
|
| 3 |
+
|
| 4 |
+
Scope: fixture mode is what lets the (not-yet-built) E2E/crawler suite walk
|
| 5 |
+
the app without spending real Groq tokens — see docs/qa/test-strategy.md and
|
| 6 |
+
the project-e2e-testing-design memory. These tests only verify the switch
|
| 7 |
+
itself behaves correctly; they don't touch real Groq (ADR-0009 keeps that
|
| 8 |
+
out of scope for automation).
|
| 9 |
+
"""
|
| 10 |
+
import importlib
|
| 11 |
+
import os
|
| 12 |
+
|
| 13 |
+
import pytest
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@pytest.fixture
|
| 17 |
+
def fixture_mode(monkeypatch):
|
| 18 |
+
"""Reloads utils.groq_client with E2E_FIXTURE_MODE=1, then reloads it back
|
| 19 |
+
off afterward so other tests aren't affected by module-level state."""
|
| 20 |
+
monkeypatch.setenv("E2E_FIXTURE_MODE", "1")
|
| 21 |
+
import utils.groq_client as gc
|
| 22 |
+
importlib.reload(gc)
|
| 23 |
+
yield gc
|
| 24 |
+
monkeypatch.delenv("E2E_FIXTURE_MODE", raising=False)
|
| 25 |
+
importlib.reload(gc)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def test_call_groq_json_returns_fixture_without_network(fixture_mode):
|
| 29 |
+
data = fixture_mode.call_groq_json("system", "user", fixture="businessIdentity")
|
| 30 |
+
assert data["businessName"] == "Ada's Fresh Kitchen"
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def test_call_groq_json_without_fixture_key_raises(fixture_mode):
|
| 34 |
+
with pytest.raises(RuntimeError, match="no fixture key"):
|
| 35 |
+
fixture_mode.call_groq_json("system", "user")
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def test_call_groq_json_missing_fixture_file_raises(fixture_mode):
|
| 39 |
+
with pytest.raises(RuntimeError, match="no fixture file"):
|
| 40 |
+
fixture_mode.call_groq_json("system", "user", fixture="does-not-exist")
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def test_call_groq_stream_yields_chunks_shaped_like_the_real_sdk(fixture_mode):
|
| 44 |
+
chunks = list(fixture_mode.call_groq_stream("system", "user", fixture="chat_response"))
|
| 45 |
+
assert len(chunks) > 1
|
| 46 |
+
rebuilt = "".join(c.choices[0].delta.content for c in chunks)
|
| 47 |
+
assert "subscription" in rebuilt
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def test_call_groq_transcribe_returns_object_with_text(fixture_mode):
|
| 51 |
+
result = fixture_mode.call_groq_transcribe(file=("a.webm", b"", "audio/webm"), fixture="transcript")
|
| 52 |
+
assert "meals a day" in result.text
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def test_fixture_mode_is_off_by_default():
|
| 56 |
+
import utils.groq_client as gc
|
| 57 |
+
importlib.reload(gc)
|
| 58 |
+
assert gc.FIXTURE_MODE is False
|
utils/groq_client.py
CHANGED
|
@@ -19,8 +19,42 @@ import os
|
|
| 19 |
import json
|
| 20 |
import time
|
| 21 |
import threading
|
|
|
|
|
|
|
| 22 |
from groq import Groq
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
# ─── Load all keys ────────────────────────────────────────────────────────────
|
| 25 |
# Accepts: GROQ_API_KEY, GROQ_API_KEY_1 ... GROQ_API_KEY_N (any suffix)
|
| 26 |
_KEYS: list[str] = []
|
|
@@ -129,6 +163,7 @@ def call_groq_transcribe(
|
|
| 129 |
model: str = TRANSCRIBE_MODEL,
|
| 130 |
response_format: str = "json",
|
| 131 |
language: str | None = None,
|
|
|
|
| 132 |
):
|
| 133 |
"""
|
| 134 |
Calls Groq's Whisper transcription endpoint with the same key-rotation
|
|
@@ -136,6 +171,15 @@ def call_groq_transcribe(
|
|
| 136 |
|
| 137 |
`file` is the (filename, bytes, content_type) tuple the Groq SDK expects.
|
| 138 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
last_error: Exception | None = None
|
| 140 |
|
| 141 |
for attempt in range(len(_KEYS) + 1):
|
|
@@ -170,8 +214,17 @@ def call_groq_json(
|
|
| 170 |
user_prompt: str,
|
| 171 |
model: str = HEAVY_MODEL,
|
| 172 |
max_tokens: int = 1500,
|
|
|
|
| 173 |
) -> dict:
|
| 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:
|
|
@@ -187,16 +240,33 @@ def call_groq_json(
|
|
| 187 |
raise RuntimeError(f"Groq returned invalid JSON (likely truncated): {e}") from e
|
| 188 |
|
| 189 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
def call_groq_stream(
|
| 191 |
system_prompt: str,
|
| 192 |
user_prompt: str,
|
| 193 |
model: str = LIGHT_MODEL,
|
| 194 |
max_tokens: int = 512,
|
|
|
|
| 195 |
):
|
| 196 |
"""
|
| 197 |
Returns a Groq streaming completion object.
|
| 198 |
Rotates key on 429 like all other calls.
|
| 199 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
last_error: Exception | None = None
|
| 201 |
for attempt in range(len(_KEYS) + 1):
|
| 202 |
try:
|
|
|
|
| 19 |
import json
|
| 20 |
import time
|
| 21 |
import threading
|
| 22 |
+
from pathlib import Path
|
| 23 |
+
from types import SimpleNamespace
|
| 24 |
from groq import Groq
|
| 25 |
|
| 26 |
+
# ─── E2E fixture mode ─────────────────────────────────────────────────────────
|
| 27 |
+
# When E2E_FIXTURE_MODE=1, every call below returns a canned fixture instead of
|
| 28 |
+
# hitting the real Groq API — no cost, deterministic, works offline. Used by
|
| 29 |
+
# the E2E/surface-discovery test suite (see docs/qa/test-strategy.md and the
|
| 30 |
+
# project-e2e-testing-design memory), never by production or by the unit tests
|
| 31 |
+
# in backend/tests/ (those stay pure-function tests with no I/O).
|
| 32 |
+
# Each call site passes a `fixture` key identifying which canned response it
|
| 33 |
+
# wants; fixture mode with no key given is a bug at the call site, not a
|
| 34 |
+
# silent fallback, so it raises loudly instead of guessing.
|
| 35 |
+
FIXTURE_MODE = os.environ.get("E2E_FIXTURE_MODE") == "1"
|
| 36 |
+
_FIXTURES_DIR = Path(__file__).resolve().parent.parent / "tests" / "fixtures" / "groq"
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _load_json_fixture(name: str) -> dict:
|
| 40 |
+
path = _FIXTURES_DIR / f"{name}.json"
|
| 41 |
+
if not path.exists():
|
| 42 |
+
raise RuntimeError(
|
| 43 |
+
f"[E2E_FIXTURE_MODE] no fixture file for '{name}' at {path} — "
|
| 44 |
+
"add one under backend/tests/fixtures/groq/"
|
| 45 |
+
)
|
| 46 |
+
return json.loads(path.read_text(encoding="utf-8"))
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _load_text_fixture(name: str) -> str:
|
| 50 |
+
path = _FIXTURES_DIR / f"{name}.txt"
|
| 51 |
+
if not path.exists():
|
| 52 |
+
raise RuntimeError(
|
| 53 |
+
f"[E2E_FIXTURE_MODE] no fixture file for '{name}' at {path} — "
|
| 54 |
+
"add one under backend/tests/fixtures/groq/"
|
| 55 |
+
)
|
| 56 |
+
return path.read_text(encoding="utf-8")
|
| 57 |
+
|
| 58 |
# ─── Load all keys ────────────────────────────────────────────────────────────
|
| 59 |
# Accepts: GROQ_API_KEY, GROQ_API_KEY_1 ... GROQ_API_KEY_N (any suffix)
|
| 60 |
_KEYS: list[str] = []
|
|
|
|
| 163 |
model: str = TRANSCRIBE_MODEL,
|
| 164 |
response_format: str = "json",
|
| 165 |
language: str | None = None,
|
| 166 |
+
fixture: str | None = None,
|
| 167 |
):
|
| 168 |
"""
|
| 169 |
Calls Groq's Whisper transcription endpoint with the same key-rotation
|
|
|
|
| 171 |
|
| 172 |
`file` is the (filename, bytes, content_type) tuple the Groq SDK expects.
|
| 173 |
"""
|
| 174 |
+
if FIXTURE_MODE:
|
| 175 |
+
if not fixture:
|
| 176 |
+
raise RuntimeError(
|
| 177 |
+
"[E2E_FIXTURE_MODE] call_groq_transcribe was called with no fixture key — "
|
| 178 |
+
"every call site must pass one when fixture mode is on"
|
| 179 |
+
)
|
| 180 |
+
data = _load_json_fixture(fixture)
|
| 181 |
+
return SimpleNamespace(text=data.get("text", ""))
|
| 182 |
+
|
| 183 |
last_error: Exception | None = None
|
| 184 |
|
| 185 |
for attempt in range(len(_KEYS) + 1):
|
|
|
|
| 214 |
user_prompt: str,
|
| 215 |
model: str = HEAVY_MODEL,
|
| 216 |
max_tokens: int = 1500,
|
| 217 |
+
fixture: str | None = None,
|
| 218 |
) -> dict:
|
| 219 |
"""Convenience wrapper: calls Groq in JSON mode and returns parsed dict."""
|
| 220 |
+
if FIXTURE_MODE:
|
| 221 |
+
if not fixture:
|
| 222 |
+
raise RuntimeError(
|
| 223 |
+
"[E2E_FIXTURE_MODE] call_groq_json was called with no fixture key — "
|
| 224 |
+
"every call site must pass one when fixture mode is on"
|
| 225 |
+
)
|
| 226 |
+
return _load_json_fixture(fixture)
|
| 227 |
+
|
| 228 |
raw = call_groq(system_prompt, user_prompt, model=model,
|
| 229 |
json_mode=True, max_tokens=max_tokens)
|
| 230 |
if not raw:
|
|
|
|
| 240 |
raise RuntimeError(f"Groq returned invalid JSON (likely truncated): {e}") from e
|
| 241 |
|
| 242 |
|
| 243 |
+
def _fixture_stream(text: str):
|
| 244 |
+
"""Yields fake chunks shaped like the Groq SDK's streaming response —
|
| 245 |
+
chunk.choices[0].delta.content — so callers don't need to know they're
|
| 246 |
+
in fixture mode."""
|
| 247 |
+
for word in text.split(" "):
|
| 248 |
+
yield SimpleNamespace(choices=[SimpleNamespace(delta=SimpleNamespace(content=word + " "))])
|
| 249 |
+
|
| 250 |
+
|
| 251 |
def call_groq_stream(
|
| 252 |
system_prompt: str,
|
| 253 |
user_prompt: str,
|
| 254 |
model: str = LIGHT_MODEL,
|
| 255 |
max_tokens: int = 512,
|
| 256 |
+
fixture: str | None = None,
|
| 257 |
):
|
| 258 |
"""
|
| 259 |
Returns a Groq streaming completion object.
|
| 260 |
Rotates key on 429 like all other calls.
|
| 261 |
"""
|
| 262 |
+
if FIXTURE_MODE:
|
| 263 |
+
if not fixture:
|
| 264 |
+
raise RuntimeError(
|
| 265 |
+
"[E2E_FIXTURE_MODE] call_groq_stream was called with no fixture key — "
|
| 266 |
+
"every call site must pass one when fixture mode is on"
|
| 267 |
+
)
|
| 268 |
+
return _fixture_stream(_load_text_fixture(fixture))
|
| 269 |
+
|
| 270 |
last_error: Exception | None = None
|
| 271 |
for attempt in range(len(_KEYS) + 1):
|
| 272 |
try:
|