rairo commited on
Commit
18fb538
·
verified ·
1 Parent(s): 540c3fc

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +50 -32
main.py CHANGED
@@ -1,11 +1,11 @@
1
  """
2
- main.py — Pricelyst Shopping Advisor (Jessica Edition 2026 - Upgrade v3.1)
3
 
4
  ✅ Feature: "Vernacular Engine" (Shona/Ndebele/English Input -> Native Response).
5
  ✅ Feature: "Precision Search" (Prioritizes exact phrase matches over popularity).
6
  ✅ Feature: "Concept Exploder" (Event Planning -> Shopping List).
7
  ✅ UI/UX: "Nearest Match" phrasing for substitutions.
8
- ✅ Core: Deep Vector Search + Market Matrix + Store Preferences.
9
 
10
  ENV VARS:
11
  - GOOGLE_API_KEY=...
@@ -289,13 +289,13 @@ def search_products_deep(df: pd.DataFrame, query: str, limit: int = 15) -> pd.Da
289
 
290
  def calculate_basket_optimization(item_names: List[str], preferred_retailer: str = None) -> Dict[str, Any]:
291
  """
292
- Generates a FULL MARKET MATRIX with Precision Search.
293
  """
294
  df = get_market_index()
295
  if df.empty:
296
  return {"actionable": False, "error": "No data"}
297
 
298
- found_items = []
299
  missing_global = []
300
 
301
  # 1. Resolve Items & Check Brand Fidelity
@@ -305,7 +305,7 @@ def calculate_basket_optimization(item_names: List[str], preferred_retailer: str
305
  if hits.empty:
306
  missing_global.append(item)
307
  continue
308
-
309
  best_match = hits.iloc[0]
310
 
311
  # --- Brand Fidelity Check ---
@@ -314,12 +314,10 @@ def calculate_basket_optimization(item_names: List[str], preferred_retailer: str
314
  q_tokens = q_norm.split()
315
 
316
  is_substitute = False
317
- # If query has brand/spec but result score is low-ish (not exact name match), flag it.
318
- # Using a simple heuristic for now based on token overlap vs query length
319
  found_tokens = sum(1 for t in q_tokens if t in res_norm)
320
  if len(q_tokens) > 1 and found_tokens < len(q_tokens):
321
  is_substitute = True
322
-
323
  # Aggregate all offers
324
  product_offers = hits[hits['product_name'] == best_match['product_name']].sort_values('price')
325
 
@@ -327,12 +325,17 @@ def calculate_basket_optimization(item_names: List[str], preferred_retailer: str
327
  for _, r in product_offers.iterrows():
328
  offers_list.append({"retailer": r['retailer'], "price": float(r['price'])})
329
 
 
 
 
 
330
  found_items.append({
331
  "query": item,
332
  "product_name": str(best_match['product_name']),
333
  "is_substitute": is_substitute,
334
  "offers": offers_list,
335
- "best_price": offers_list[0]['price']
 
336
  })
337
 
338
  if not found_items:
@@ -345,7 +348,7 @@ def calculate_basket_optimization(item_names: List[str], preferred_retailer: str
345
  all_involved_retailers.add(o['retailer'])
346
 
347
  store_comparison = []
348
-
349
  for retailer in all_involved_retailers:
350
  total_price = 0.0
351
  found_count = 0
@@ -368,7 +371,17 @@ def calculate_basket_optimization(item_names: List[str], preferred_retailer: str
368
  })
369
 
370
  store_comparison.sort(key=lambda x: (-x['found_count'], x['total_price']))
371
-
 
 
 
 
 
 
 
 
 
 
372
  return {
373
  "actionable": True,
374
  "is_basket": len(found_items) > 1,
@@ -380,7 +393,7 @@ def calculate_basket_optimization(item_names: List[str], preferred_retailer: str
380
  }
381
 
382
  def calculate_zesa_units(amount_usd: float) -> Dict[str, Any]:
383
- remaining = amount_usd / 1.06
384
  units = 0.0
385
 
386
  t1 = ZIM_CONTEXT["zesa_step_1"]
@@ -520,7 +533,7 @@ def gemini_chat_response(transcript: str, intent: Dict, analyst_data: Dict, chat
520
  language = intent.get("language", "English")
521
 
522
  PROMPT = f"""
523
- You are Jessica, Pricelyst's Shopping Advisor (Zimbabwe).
524
  Role: Intelligent Shopping Companion.
525
  Goal: Shortest path to value. Complete Transparency.
526
 
@@ -535,15 +548,16 @@ def gemini_chat_response(transcript: str, intent: Dict, analyst_data: Dict, chat
535
  1. **LANGUAGE**: Reply in **{language}**. If Shona, use Shona. If English, use English.
536
 
537
  2. **BASKET COMPARISON**:
538
- - If `market_matrix` has multiple stores, compare totals.
539
- - "Spar is **$6.95**, OK Mart is **$4.00** (but missing Oil)."
540
 
541
  3. **BRAND SUBSTITUTES (Phrasing)**:
542
- - If `is_substitute` is TRUE for an item, say:
543
  "I couldn't find **[Query]**, but the **nearest match is** **[Found]** ($Price)."
544
 
545
  4. **SINGLE ITEMS**:
546
- - Best price first, then others.
 
547
 
548
  5. **CASUAL**:
549
  - Reset if user says "Hi".
@@ -576,21 +590,22 @@ def gemini_generate_4step_plan(transcript: str, analyst_result: Dict) -> str:
576
 
577
  SECTIONS:
578
 
579
- 1. **In Our Catalogue ✅**
580
- (Markdown Table: | Item | Retailer | Price (USD) |)
581
-
582
  2. **Not in Catalogue (Estimates) 😔**
583
  (Markdown Table: | Item | Estimated Price (USD) |)
584
  *Fill in estimated prices for missing items based on Zimbabwe market knowledge.*
585
 
586
- 3. **Totals 💰**
587
  - Confirmed Total (Catalogue)
 
588
  - Estimated Total (Missing Items)
589
  - **Grand Total Estimate**
590
 
591
  4. **Ideas & Tips 💡**
592
  - 3 Creative ideas based on the specific event/meal (e.g. Braai tips, Cooking hacks).
593
-
594
  Tone: Warm, Professional, Zimbabwean.
595
  """
596
  try:
@@ -610,7 +625,7 @@ def health():
610
  "ok": True,
611
  "offers_indexed": len(df),
612
  "api_source": PRICE_API_BASE,
613
- "persona": "Jessica v3.1 (Babel Fish)"
614
  })
615
 
616
  @app.post("/chat")
@@ -627,7 +642,8 @@ def chat():
627
  try:
628
  docs = db.collection("pricelyst_profiles").document(pid).collection("chat_logs") \
629
  .order_by("ts", direction=firestore.Query.DESCENDING).limit(6).stream()
630
- msgs = [f"User: {d.to_dict().get('message')}\nJessica: {d.to_dict().get('response')}" for d in docs]
 
631
  if msgs: history_str = "\n".join(reversed(msgs))
632
  except: pass
633
 
@@ -635,7 +651,7 @@ def chat():
635
  intent_data = gemini_detect_intent(msg)
636
  intent_type = intent_data.get("intent", "CASUAL_CHAT")
637
  items = intent_data.get("items", [])
638
- store_pref = intent_data.get("store_preference")
639
 
640
  analyst_data = {}
641
 
@@ -647,7 +663,7 @@ def chat():
647
  analyst_data = calculate_zesa_units(amount)
648
 
649
  reply = gemini_chat_response(msg, intent_data, analyst_data, history_str)
650
-
651
  if db:
652
  db.collection("pricelyst_profiles").document(pid).collection("chat_logs").add({
653
  "message": msg,
@@ -661,7 +677,8 @@ def chat():
661
  @app.post("/api/analyze-image")
662
  def analyze_image():
663
  body = request.get_json(silent=True) or {}
664
- image_b64 = body.get("image_data")
 
665
  caption = body.get("caption", "")
666
  pid = body.get("profile_id")
667
 
@@ -692,7 +709,7 @@ def analyze_image():
692
  else: sim_msg = f"Cheapest price for {', '.join(items)}?"
693
 
694
  response_text = gemini_chat_response(sim_msg, {"intent": "STORE_DECISION"}, analyst_data, "")
695
-
696
  else:
697
  response_text = "I couldn't identify the product. Could you type the name?"
698
 
@@ -723,9 +740,9 @@ def call_briefing():
723
  doc = ref.get()
724
  if doc.exists: prof = doc.to_dict()
725
  else: ref.set({"created_at": datetime.now(timezone.utc).isoformat()})
726
-
727
- if username != "Friend" and username != prof.get("username"):
728
- if db: db.collection("pricelyst_profiles").document(pid).set({"username": username}, merge=True)
729
 
730
  # 2. Market Intelligence Generation
731
  df = get_market_index()
@@ -752,7 +769,7 @@ def call_briefing():
752
  if not hits.empty:
753
  cheapest = hits.sort_values('price').iloc[0]
754
  staple_summary.append(f"- {s}: ${cheapest['price']} @ {cheapest['retailer']}")
755
-
756
  staples_section = "\n[STAPLES - LOWEST]\n" + "\n".join(staple_summary)
757
 
758
  # C. Top 60 Catalogue
@@ -859,6 +876,7 @@ def delete_plan(plan_id):
859
  return jsonify({"ok": True})
860
  except: return jsonify({"ok": False}), 500
861
 
 
862
  if __name__ == "__main__":
863
  port = int(os.environ.get("PORT", 7860))
864
  try: get_market_index(force_refresh=True)
 
1
  """
2
+ main.py — Pricelyst Shopping Advisor (April Edition 2026 - Upgrade v3.1)
3
 
4
  ✅ Feature: "Vernacular Engine" (Shona/Ndebele/English Input -> Native Response).
5
  ✅ Feature: "Precision Search" (Prioritizes exact phrase matches over popularity).
6
  ✅ Feature: "Concept Exploder" (Event Planning -> Shopping List).
7
  ✅ UI/UX: "Nearest Match" phrasing for substitutions.
8
+ ✅ Core: Deep Vector Search + Market Matrix + Store Preferences + Savings Calculator.
9
 
10
  ENV VARS:
11
  - GOOGLE_API_KEY=...
 
289
 
290
  def calculate_basket_optimization(item_names: List[str], preferred_retailer: str = None) -> Dict[str, Any]:
291
  """
292
+ Generates a FULL MARKET MATRIX with Precision Search and Savings Calculation.
293
  """
294
  df = get_market_index()
295
  if df.empty:
296
  return {"actionable": False, "error": "No data"}
297
 
298
+ found_items = []
299
  missing_global = []
300
 
301
  # 1. Resolve Items & Check Brand Fidelity
 
305
  if hits.empty:
306
  missing_global.append(item)
307
  continue
308
+
309
  best_match = hits.iloc[0]
310
 
311
  # --- Brand Fidelity Check ---
 
314
  q_tokens = q_norm.split()
315
 
316
  is_substitute = False
 
 
317
  found_tokens = sum(1 for t in q_tokens if t in res_norm)
318
  if len(q_tokens) > 1 and found_tokens < len(q_tokens):
319
  is_substitute = True
320
+
321
  # Aggregate all offers
322
  product_offers = hits[hits['product_name'] == best_match['product_name']].sort_values('price')
323
 
 
325
  for _, r in product_offers.iterrows():
326
  offers_list.append({"retailer": r['retailer'], "price": float(r['price'])})
327
 
328
+ best_price = offers_list[0]['price']
329
+ max_price = offers_list[-1]['price']
330
+ potential_savings = max_price - best_price
331
+
332
  found_items.append({
333
  "query": item,
334
  "product_name": str(best_match['product_name']),
335
  "is_substitute": is_substitute,
336
  "offers": offers_list,
337
+ "best_price": best_price,
338
+ "potential_savings": potential_savings
339
  })
340
 
341
  if not found_items:
 
348
  all_involved_retailers.add(o['retailer'])
349
 
350
  store_comparison = []
351
+
352
  for retailer in all_involved_retailers:
353
  total_price = 0.0
354
  found_count = 0
 
371
  })
372
 
373
  store_comparison.sort(key=lambda x: (-x['found_count'], x['total_price']))
374
+
375
+ # 3. Calculate Basket-Level Savings
376
+ if len(store_comparison) > 1:
377
+ most_expensive_total = max(s['total_price'] for s in store_comparison if s['found_count'] == store_comparison[0]['found_count'])
378
+ for store in store_comparison:
379
+ # Savings calculated against the highest total for an equivalent sized basket
380
+ store['basket_savings'] = most_expensive_total - store['total_price'] if store['found_count'] == store_comparison[0]['found_count'] else 0.0
381
+ else:
382
+ for store in store_comparison:
383
+ store['basket_savings'] = 0.0
384
+
385
  return {
386
  "actionable": True,
387
  "is_basket": len(found_items) > 1,
 
393
  }
394
 
395
  def calculate_zesa_units(amount_usd: float) -> Dict[str, Any]:
396
+ remaining = amount_usd / 1.06
397
  units = 0.0
398
 
399
  t1 = ZIM_CONTEXT["zesa_step_1"]
 
533
  language = intent.get("language", "English")
534
 
535
  PROMPT = f"""
536
+ You are April, Pricelyst's Shopping Advisor (Zimbabwe).
537
  Role: Intelligent Shopping Companion.
538
  Goal: Shortest path to value. Complete Transparency.
539
 
 
548
  1. **LANGUAGE**: Reply in **{language}**. If Shona, use Shona. If English, use English.
549
 
550
  2. **BASKET COMPARISON**:
551
+ - If `market_matrix` has multiple stores, compare totals and explicitly state the savings using the pre-calculated `basket_savings`.
552
+ - Example: "Spar is **$6.95**, OK Mart is **$4.00** (but missing Oil). You save **$2.95** by getting the basket at OK Mart!"
553
 
554
  3. **BRAND SUBSTITUTES (Phrasing)**:
555
+ - If `is_substitute` is TRUE for an item, say:
556
  "I couldn't find **[Query]**, but the **nearest match is** **[Found]** ($Price)."
557
 
558
  4. **SINGLE ITEMS**:
559
+ - State the best price first, then others. Explicitly state how much is saved by choosing the cheapest option over the most expensive one based on `potential_savings`.
560
+ - Example: "The cheapest is **$2.00** at OK. You save **$0.50** compared to the most expensive store!"
561
 
562
  5. **CASUAL**:
563
  - Reset if user says "Hi".
 
590
 
591
  SECTIONS:
592
 
593
+ 1. **In Our Catalogue ✅**
594
+ (Markdown Table: | Item | Retailer | Price (USD) | Potential Savings |)
595
+
596
  2. **Not in Catalogue (Estimates) 😔**
597
  (Markdown Table: | Item | Estimated Price (USD) |)
598
  *Fill in estimated prices for missing items based on Zimbabwe market knowledge.*
599
 
600
+ 3. **Totals & Savings 💰**
601
  - Confirmed Total (Catalogue)
602
+ - Total Basket Savings (From cheapest vs most expensive store)
603
  - Estimated Total (Missing Items)
604
  - **Grand Total Estimate**
605
 
606
  4. **Ideas & Tips 💡**
607
  - 3 Creative ideas based on the specific event/meal (e.g. Braai tips, Cooking hacks).
608
+
609
  Tone: Warm, Professional, Zimbabwean.
610
  """
611
  try:
 
625
  "ok": True,
626
  "offers_indexed": len(df),
627
  "api_source": PRICE_API_BASE,
628
+ "persona": "April v3.1 (Babel Fish)"
629
  })
630
 
631
  @app.post("/chat")
 
642
  try:
643
  docs = db.collection("pricelyst_profiles").document(pid).collection("chat_logs") \
644
  .order_by("ts", direction=firestore.Query.DESCENDING).limit(6).stream()
645
+ # Persona updated to April here for context memory
646
+ msgs = [f"User: {d.to_dict().get('message')}\nApril: {d.to_dict().get('response')}" for d in docs]
647
  if msgs: history_str = "\n".join(reversed(msgs))
648
  except: pass
649
 
 
651
  intent_data = gemini_detect_intent(msg)
652
  intent_type = intent_data.get("intent", "CASUAL_CHAT")
653
  items = intent_data.get("items", [])
654
+ store_pref = intent_data.get("store_preference")
655
 
656
  analyst_data = {}
657
 
 
663
  analyst_data = calculate_zesa_units(amount)
664
 
665
  reply = gemini_chat_response(msg, intent_data, analyst_data, history_str)
666
+
667
  if db:
668
  db.collection("pricelyst_profiles").document(pid).collection("chat_logs").add({
669
  "message": msg,
 
677
  @app.post("/api/analyze-image")
678
  def analyze_image():
679
  body = request.get_json(silent=True) or {}
680
+ image_b64 = body.get("image_data")
681
+
682
  caption = body.get("caption", "")
683
  pid = body.get("profile_id")
684
 
 
709
  else: sim_msg = f"Cheapest price for {', '.join(items)}?"
710
 
711
  response_text = gemini_chat_response(sim_msg, {"intent": "STORE_DECISION"}, analyst_data, "")
712
+
713
  else:
714
  response_text = "I couldn't identify the product. Could you type the name?"
715
 
 
740
  doc = ref.get()
741
  if doc.exists: prof = doc.to_dict()
742
  else: ref.set({"created_at": datetime.now(timezone.utc).isoformat()})
743
+
744
+ if username != "Friend" and username != prof.get("username"):
745
+ if db: db.collection("pricelyst_profiles").document(pid).set({"username": username}, merge=True)
746
 
747
  # 2. Market Intelligence Generation
748
  df = get_market_index()
 
769
  if not hits.empty:
770
  cheapest = hits.sort_values('price').iloc[0]
771
  staple_summary.append(f"- {s}: ${cheapest['price']} @ {cheapest['retailer']}")
772
+
773
  staples_section = "\n[STAPLES - LOWEST]\n" + "\n".join(staple_summary)
774
 
775
  # C. Top 60 Catalogue
 
876
  return jsonify({"ok": True})
877
  except: return jsonify({"ok": False}), 500
878
 
879
+
880
  if __name__ == "__main__":
881
  port = int(os.environ.get("PORT", 7860))
882
  try: get_market_index(force_refresh=True)