rairo commited on
Commit
ae97bd4
·
verified ·
1 Parent(s): 3f86912

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +27 -41
main.py CHANGED
@@ -1,12 +1,12 @@
1
  """
2
- main.py — Pricelyst Shopping Advisor (Jessica 2026 Edition)
3
 
4
  ✅ Flask API
5
  ✅ Firebase Admin Persistence
6
- ✅ Gemini 2.5 Flash (Target Model)
7
  ✅ "Analyst Engine": Python Math for Baskets & ZESA
8
- ✅ "Creative Engine": 4-Step Hybrid Plans (Catalogue -> Estimates -> Totals -> Context Ideas)
9
- Jessica Context: Live Mini-Catalogue Injection
10
 
11
  ENV VARS:
12
  - GOOGLE_API_KEY=...
@@ -48,7 +48,6 @@ except Exception as e:
48
  logger.error("google-genai not installed. pip install google-genai. Error=%s", e)
49
 
50
  GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY", "")
51
- # Updated to 2.5 Flash as requested for 2026 timeline
52
  GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-2.5-flash")
53
 
54
  _gemini_client = None
@@ -168,18 +167,18 @@ def fetch_and_flatten_data() -> pd.DataFrame:
168
  rows = []
169
  for p in all_products:
170
  try:
171
- p_id = p.get("id")
172
- p_name = p.get("name") or "Unknown"
173
  clean_name = _norm(p_name)
174
 
175
  cat_obj = p.get("category") or {}
176
- cat_name = cat_obj.get("name") or "General"
177
 
178
  brand_obj = p.get("brand") or {}
179
- brand_name = brand_obj.get("brand_name") or ""
180
 
181
  views = int(p.get("view_count") or 0)
182
- image = p.get("thumbnail") or p.get("image")
183
 
184
  prices = p.get("prices") or []
185
 
@@ -200,7 +199,7 @@ def fetch_and_flatten_data() -> pd.DataFrame:
200
 
201
  for offer in prices:
202
  retailer = offer.get("retailer") or {}
203
- r_name = retailer.get("name") or "Unknown Store"
204
  price_val = _coerce_price(offer.get("price"))
205
 
206
  if price_val > 0:
@@ -268,6 +267,7 @@ def search_products_fuzzy(df: pd.DataFrame, query: str, limit: int = 10) -> pd.D
268
  def calculate_basket_optimization(item_names: List[str]) -> Dict[str, Any]:
269
  """
270
  Determines the best store for a list of items.
 
271
  """
272
  df = get_market_index()
273
  if df.empty:
@@ -289,12 +289,11 @@ def calculate_basket_optimization(item_names: List[str]) -> Dict[str, Any]:
289
  # Best match based on popularity
290
  best_prod = hits.iloc[0]
291
  found_items.append({
292
- "query": item,
293
- "product_id": best_prod['product_id'],
294
- "name": best_prod['product_name']
295
  })
296
 
297
- # Note: We return actionable=True even if products missing, so Gemini can estimate
298
  if not found_items:
299
  return {
300
  "actionable": True,
@@ -319,10 +318,10 @@ def calculate_basket_optimization(item_names: List[str]) -> Dict[str, Any]:
319
  found_names = [x['name'] for x in found_items if x['product_id'] in retailer_pids]
320
 
321
  retailer_stats.append({
322
- "retailer": retailer,
323
- "total_price": float(total_price),
324
- "item_count": found_count,
325
- "coverage_percent": (found_count / len(found_items)) * 100,
326
  "found_items": found_names
327
  })
328
 
@@ -373,13 +372,13 @@ def calculate_zesa_units(amount_usd: float) -> Dict[str, Any]:
373
  breakdown.append(f"All {bought:.1f}u @ ${t1['rate']}")
374
 
375
  return {
376
- "amount_usd": amount_usd,
377
- "est_units_kwh": round(units, 1),
378
  "breakdown": breakdown
379
  }
380
 
381
  # =========================
382
- # 3. Gemini Helpers (Detect + Creative)
383
  # =========================
384
 
385
  def gemini_detect_intent(transcript: str) -> Dict[str, Any]:
@@ -423,7 +422,7 @@ def gemini_generate_4step_plan(transcript: str, analyst_result: Dict) -> str:
423
  USER CONTEXT: "{transcript}"
424
 
425
  ANALYST DATA (REAL PRICES):
426
- {json.dumps(analyst_result, indent=2)}
427
 
428
  REQUIRED SECTIONS (Do not deviate):
429
 
@@ -432,7 +431,7 @@ def gemini_generate_4step_plan(transcript: str, analyst_result: Dict) -> str:
432
  - Show product name, retailer, and exact price from data.
433
 
434
  2. **Not in Catalogue (Estimates) 😔**
435
- - List items from 'global_missing' or items in the user list but not in 'found_items'.
436
  - Provide a realistic USD estimate for Zimbabwe.
437
 
438
  3. **Totals 💰**
@@ -471,7 +470,7 @@ def health():
471
 
472
  @app.post("/chat")
473
  def chat():
474
- """Text Chat - Uses simple summary."""
475
  body = request.get_json(silent=True) or {}
476
  msg = body.get("message", "")
477
  pid = body.get("profile_id")
@@ -489,13 +488,12 @@ def chat():
489
  elif intent_data["intent"] == "PRODUCT_SEARCH" and intent_data.get("items"):
490
  analyst_data = calculate_basket_optimization(intent_data["items"])
491
 
492
- # Quick chat summary
493
  reply = "I'm processing that for you."
494
  if analyst_data:
495
  try:
496
  resp = _gemini_client.models.generate_content(
497
  model=GEMINI_MODEL,
498
- contents=f"Summarize this shopping data for chat:\n{json.dumps(analyst_data)}"
499
  )
500
  reply = resp.text
501
  except: pass
@@ -541,7 +539,7 @@ def call_briefing():
541
  top = df[df['is_offer']].sort_values('views', ascending=False).drop_duplicates('product_name').head(60)
542
  lines = []
543
  for _, r in top.iterrows():
544
- lines.append(f"{r['product_name']} (${r['price']:.2f})")
545
  catalogue_str = ", ".join(lines)
546
 
547
  kpi_snapshot = {
@@ -558,12 +556,7 @@ def call_briefing():
558
  @app.post("/api/log-call-usage")
559
  def log_call_usage():
560
  """
561
- The Post-Call Orchestrator.
562
- 1. Update Memory.
563
- 2. Detect Intent.
564
- 3. Run Analyst.
565
- 4. Generate 4-Step Creative Plan.
566
- 5. Save.
567
  """
568
  body = request.get_json(silent=True) or {}
569
  pid = body.get("profile_id")
@@ -573,7 +566,6 @@ def log_call_usage():
573
 
574
  logger.info(f"Log Call: Processing {pid}. Transcript Len: {len(transcript)}")
575
 
576
- # 1. Update Memory
577
  if len(transcript) > 20 and db:
578
  try:
579
  curr_mem = db.collection("pricelyst_profiles").document(pid).get().to_dict().get("memory_summary", "")
@@ -583,21 +575,16 @@ def log_call_usage():
583
  except Exception as e:
584
  logger.error(f"Memory Update Error: {e}")
585
 
586
- # 2. Intent Detection
587
  intent_data = gemini_detect_intent(transcript)
588
  logger.info(f"Intent: {intent_data.get('intent')}")
589
 
590
  plan_data = {}
591
 
592
- # 3. Actionable Logic
593
  if intent_data.get("actionable"):
594
 
595
- # Run Analyst
596
  if intent_data.get("items"):
597
  analyst_result = calculate_basket_optimization(intent_data["items"])
598
 
599
- # 4. Generate Creative Plan (4-Step)
600
- # We generate if we have actionable basket results OR missing items
601
  if analyst_result.get("actionable"):
602
  md_content = gemini_generate_4step_plan(transcript, analyst_result)
603
 
@@ -615,7 +602,6 @@ def log_call_usage():
615
  doc_ref.set(plan_data)
616
  logger.info(f"Plan Saved: {doc_ref.id}")
617
 
618
- # 5. Log Call
619
  if db:
620
  db.collection("pricelyst_profiles").document(pid).collection("call_logs").add({
621
  "transcript": transcript,
 
1
  """
2
+ main.py — Pricelyst Shopping Advisor (Jessica Edition 2026)
3
 
4
  ✅ Flask API
5
  ✅ Firebase Admin Persistence
6
+ ✅ Gemini 2.5 Flash
7
  ✅ "Analyst Engine": Python Math for Baskets & ZESA
8
+ ✅ "Creative Engine": 4-Step Hybrid Plans
9
+ Type Safety: Explicit Casting for JSON Serialization (Fixes int64 error)
10
 
11
  ENV VARS:
12
  - GOOGLE_API_KEY=...
 
48
  logger.error("google-genai not installed. pip install google-genai. Error=%s", e)
49
 
50
  GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY", "")
 
51
  GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-2.5-flash")
52
 
53
  _gemini_client = None
 
167
  rows = []
168
  for p in all_products:
169
  try:
170
+ p_id = int(p.get("id") or 0)
171
+ p_name = str(p.get("name") or "Unknown")
172
  clean_name = _norm(p_name)
173
 
174
  cat_obj = p.get("category") or {}
175
+ cat_name = str(cat_obj.get("name") or "General")
176
 
177
  brand_obj = p.get("brand") or {}
178
+ brand_name = str(brand_obj.get("brand_name") or "")
179
 
180
  views = int(p.get("view_count") or 0)
181
+ image = str(p.get("thumbnail") or p.get("image") or "")
182
 
183
  prices = p.get("prices") or []
184
 
 
199
 
200
  for offer in prices:
201
  retailer = offer.get("retailer") or {}
202
+ r_name = str(retailer.get("name") or "Unknown Store")
203
  price_val = _coerce_price(offer.get("price"))
204
 
205
  if price_val > 0:
 
267
  def calculate_basket_optimization(item_names: List[str]) -> Dict[str, Any]:
268
  """
269
  Determines the best store for a list of items.
270
+ Explicitly casts int64/float64 to native Python types for JSON safety.
271
  """
272
  df = get_market_index()
273
  if df.empty:
 
289
  # Best match based on popularity
290
  best_prod = hits.iloc[0]
291
  found_items.append({
292
+ "query": str(item),
293
+ "product_id": int(best_prod['product_id']), # Explicit int cast
294
+ "name": str(best_prod['product_name']) # Explicit str cast
295
  })
296
 
 
297
  if not found_items:
298
  return {
299
  "actionable": True,
 
318
  found_names = [x['name'] for x in found_items if x['product_id'] in retailer_pids]
319
 
320
  retailer_stats.append({
321
+ "retailer": str(retailer),
322
+ "total_price": float(total_price), # Explicit float cast
323
+ "item_count": int(found_count), # Explicit int cast
324
+ "coverage_percent": float((found_count / len(found_items)) * 100),
325
  "found_items": found_names
326
  })
327
 
 
372
  breakdown.append(f"All {bought:.1f}u @ ${t1['rate']}")
373
 
374
  return {
375
+ "amount_usd": float(amount_usd),
376
+ "est_units_kwh": float(round(units, 1)),
377
  "breakdown": breakdown
378
  }
379
 
380
  # =========================
381
+ # 3. Gemini Helpers
382
  # =========================
383
 
384
  def gemini_detect_intent(transcript: str) -> Dict[str, Any]:
 
422
  USER CONTEXT: "{transcript}"
423
 
424
  ANALYST DATA (REAL PRICES):
425
+ {json.dumps(analyst_result, indent=2, default=str)}
426
 
427
  REQUIRED SECTIONS (Do not deviate):
428
 
 
431
  - Show product name, retailer, and exact price from data.
432
 
433
  2. **Not in Catalogue (Estimates) 😔**
434
+ - List items from 'global_missing'.
435
  - Provide a realistic USD estimate for Zimbabwe.
436
 
437
  3. **Totals 💰**
 
470
 
471
  @app.post("/chat")
472
  def chat():
473
+ """Text Chat."""
474
  body = request.get_json(silent=True) or {}
475
  msg = body.get("message", "")
476
  pid = body.get("profile_id")
 
488
  elif intent_data["intent"] == "PRODUCT_SEARCH" and intent_data.get("items"):
489
  analyst_data = calculate_basket_optimization(intent_data["items"])
490
 
 
491
  reply = "I'm processing that for you."
492
  if analyst_data:
493
  try:
494
  resp = _gemini_client.models.generate_content(
495
  model=GEMINI_MODEL,
496
+ contents=f"Summarize this shopping data for chat:\n{json.dumps(analyst_data, default=str)}"
497
  )
498
  reply = resp.text
499
  except: pass
 
539
  top = df[df['is_offer']].sort_values('views', ascending=False).drop_duplicates('product_name').head(60)
540
  lines = []
541
  for _, r in top.iterrows():
542
+ lines.append(f"{r['product_name']} (~${r['price']:.2f})")
543
  catalogue_str = ", ".join(lines)
544
 
545
  kpi_snapshot = {
 
556
  @app.post("/api/log-call-usage")
557
  def log_call_usage():
558
  """
559
+ Post-Call Orchestrator.
 
 
 
 
 
560
  """
561
  body = request.get_json(silent=True) or {}
562
  pid = body.get("profile_id")
 
566
 
567
  logger.info(f"Log Call: Processing {pid}. Transcript Len: {len(transcript)}")
568
 
 
569
  if len(transcript) > 20 and db:
570
  try:
571
  curr_mem = db.collection("pricelyst_profiles").document(pid).get().to_dict().get("memory_summary", "")
 
575
  except Exception as e:
576
  logger.error(f"Memory Update Error: {e}")
577
 
 
578
  intent_data = gemini_detect_intent(transcript)
579
  logger.info(f"Intent: {intent_data.get('intent')}")
580
 
581
  plan_data = {}
582
 
 
583
  if intent_data.get("actionable"):
584
 
 
585
  if intent_data.get("items"):
586
  analyst_result = calculate_basket_optimization(intent_data["items"])
587
 
 
 
588
  if analyst_result.get("actionable"):
589
  md_content = gemini_generate_4step_plan(transcript, analyst_result)
590
 
 
602
  doc_ref.set(plan_data)
603
  logger.info(f"Plan Saved: {doc_ref.id}")
604
 
 
605
  if db:
606
  db.collection("pricelyst_profiles").document(pid).collection("call_logs").add({
607
  "transcript": transcript,