rairo commited on
Commit
b1fa9bd
·
verified ·
1 Parent(s): 486c74d

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +24 -34
main.py CHANGED
@@ -1,14 +1,11 @@
1
  """
2
- main.py — Pricelyst Shopping Advisor (Jessica Edition 2026 - Upgrade v2.1)
3
 
4
- Flask API
5
- Firebase Admin Persistence
6
- ✅ Gemini 2.5 Flash (Multimodal)
7
  ✅ "Analyst Engine": Enhanced Basket Math, Category Context, ZESA Logic
8
- ✅ "Jessica Engine": Natural Conversation, Chit-Chat, & Advisory
9
  ✅ "Visual Engine": Lists, Products, & Meal-to-Recipe recognition
10
- Type Safety: Explicit Casting for JSON Serialization
11
- ✅ Memory Logic: Separated Deep Memory (Calls) from Short-Term Context (Chat)
12
 
13
  ENV VARS:
14
  - GOOGLE_API_KEY=...
@@ -236,7 +233,7 @@ def get_market_index(force_refresh: bool = False) -> pd.DataFrame:
236
  return _data_cache["df"]
237
 
238
  # =========================
239
- # 2. Analyst Engine (Enhanced Math Logic)
240
  # =========================
241
 
242
  def search_products_fuzzy(df: pd.DataFrame, query: str, limit: int = 10) -> pd.DataFrame:
@@ -268,7 +265,6 @@ def search_products_fuzzy(df: pd.DataFrame, query: str, limit: int = 10) -> pd.D
268
  return matches.head(limit)
269
 
270
  def get_category_stats(df: pd.DataFrame, category_name: str) -> Dict[str, Any]:
271
- """Returns min, max, avg price for a category to determine value."""
272
  if df.empty: return {}
273
  cat_df = df[df['category'].str.lower().str.contains(category_name.lower()) & df['is_offer']]
274
  if cat_df.empty:
@@ -285,9 +281,6 @@ def get_category_stats(df: pd.DataFrame, category_name: str) -> Dict[str, Any]:
285
  }
286
 
287
  def calculate_basket_optimization(item_names: List[str]) -> Dict[str, Any]:
288
- """
289
- Optimizes for: 1. Best Single Store, 2. Cheapest Split Mix.
290
- """
291
  df = get_market_index()
292
  if df.empty:
293
  return {"actionable": False, "error": "No data"}
@@ -377,8 +370,7 @@ def calculate_basket_optimization(item_names: List[str]) -> Dict[str, Any]:
377
  }
378
 
379
  def calculate_zesa_units(amount_usd: float) -> Dict[str, Any]:
380
- """Calculates ZESA units with tiered logic explanation."""
381
- remaining = amount_usd / 1.06 # Remove 6% REA Levy approx
382
  units = 0.0
383
  breakdown = []
384
 
@@ -419,7 +411,7 @@ def calculate_zesa_units(amount_usd: float) -> Dict[str, Any]:
419
  }
420
 
421
  # =========================
422
- # 3. Gemini Helpers (Persona & Vision)
423
  # =========================
424
 
425
  def gemini_detect_intent(transcript: str) -> Dict[str, Any]:
@@ -428,7 +420,7 @@ def gemini_detect_intent(transcript: str) -> Dict[str, Any]:
428
  PROMPT = """
429
  Analyze transcript. Return STRICT JSON.
430
  Classify intent:
431
- - CASUAL_CHAT: Greetings, small talk, no shopping data needed.
432
  - SHOPPING_BASKET: Looking for prices, creating a list.
433
  - UTILITY_CALC: Electricity/ZESA questions.
434
  - STORE_DECISION: "Where should I buy?", "Which store is cheapest?".
@@ -437,7 +429,7 @@ def gemini_detect_intent(transcript: str) -> Dict[str, Any]:
437
  Extract:
438
  - items: list of products
439
  - utility_amount: number
440
- - context: "budget", "speed", "quality"
441
 
442
  JSON Schema:
443
  {
@@ -477,17 +469,18 @@ def gemini_analyze_image(image_b64: str, caption: str = "") -> Dict[str, Any]:
477
  }}
478
  """
479
  try:
 
480
  image_bytes = base64.b64decode(image_b64)
481
  resp = _gemini_client.models.generate_content(
482
  model=GEMINI_MODEL,
483
  contents=[
484
- types.Part.from_text(PROMPT),
485
  types.Part.from_bytes(data=image_bytes, mime_type="image/jpeg")
486
  ],
487
  config=types.GenerateContentConfig(response_mime_type="application/json")
488
  )
489
  result = _safe_json_loads(resp.text, {"type": "IRRELEVANT", "items": []})
490
- logger.info(f"🔮 VISION RAW: {json.dumps(result)}") # Debug Logging
491
  return result
492
  except Exception as e:
493
  logger.error(f"Vision Error: {e}")
@@ -496,7 +489,7 @@ def gemini_analyze_image(image_b64: str, caption: str = "") -> Dict[str, Any]:
496
  def gemini_chat_response(transcript: str, intent: Dict, analyst_data: Dict, chat_history: str = "") -> str:
497
  if not _gemini_client: return "I'm having trouble connecting to my brain right now."
498
 
499
- context_str = f"RECENT CHAT HISTORY:\n{chat_history}\n" if chat_history else ""
500
  context_str += f"ZIMBABWE CONTEXT: Fuel={ZIM_CONTEXT['fuel_petrol']}, ZESA Rate={ZIM_CONTEXT['zesa_step_1']['rate']}\n"
501
 
502
  if analyst_data:
@@ -511,19 +504,17 @@ def gemini_chat_response(transcript: str, intent: Dict, analyst_data: Dict, chat
511
  CONTEXT:
512
  {context_str}
513
 
 
 
 
 
514
  INSTRUCTIONS:
515
- 1. **Casual Chat**: Use the chat history to reply naturally. If history is empty, be warm. "Makadii! How can I help?"
516
- 2. **Shopping Advice**:
517
- - If data exists, guide them. "I found XYZ at Store A for $5."
518
- - If 'best_store' exists, recommend it explicitly based on coverage.
519
- - If 'split_strategy' saves money (>10%), suggest splitting the shop.
520
- 3. **Trust & Budget**:
521
- - If user asks "Is this expensive?", check 'category_stats' in data.
522
- - If price is > avg, say "That's a bit pricey, average is $X."
523
- 4. **ZESA**: Explain the units naturally using the breakdown provided.
524
 
525
  TONE: Conversational, Zimbabwean English.
526
- Do NOT dump JSON. Write a natural message. Use Markdown for lists/prices.
527
  """
528
 
529
  try:
@@ -570,7 +561,7 @@ def health():
570
  "ok": True,
571
  "offers_indexed": len(df),
572
  "api_source": PRICE_API_BASE,
573
- "persona": "Jessica v2.1 (Memory Firewall)"
574
  })
575
 
576
  @app.post("/chat")
@@ -589,11 +580,10 @@ def chat():
589
  history_str = ""
590
  if db:
591
  try:
592
- # Get last 6 messages, ordered by time descending (newest first)
593
  docs = db.collection("pricelyst_profiles").document(pid).collection("chat_logs") \
594
  .order_by("ts", direction=firestore.Query.DESCENDING).limit(6).stream()
595
 
596
- # Reverse to Chronological order for the LLM
597
  msgs = []
598
  for d in docs:
599
  data = d.to_dict()
@@ -721,7 +711,7 @@ def call_briefing():
721
 
722
  return jsonify({
723
  "ok": True,
724
- "memory_summary": prof.get("memory_summary", ""), # Keep Long Term memory here
725
  "kpi_snapshot": json.dumps(kpi_snapshot)
726
  })
727
 
 
1
  """
2
+ main.py — Pricelyst Shopping Advisor (Jessica Edition 2026 - Upgrade v2.2)
3
 
4
+ Fixed: Vision SDK Crash (Part.from_text removed)
5
+ Fixed: "Clingy" Memory (Topic Reset Rule added)
 
6
  ✅ "Analyst Engine": Enhanced Basket Math, Category Context, ZESA Logic
 
7
  ✅ "Visual Engine": Lists, Products, & Meal-to-Recipe recognition
8
+ Memory Logic: Short-Term Sliding Window (Last 6 messages)
 
9
 
10
  ENV VARS:
11
  - GOOGLE_API_KEY=...
 
233
  return _data_cache["df"]
234
 
235
  # =========================
236
+ # 2. Analyst Engine (Math Logic)
237
  # =========================
238
 
239
  def search_products_fuzzy(df: pd.DataFrame, query: str, limit: int = 10) -> pd.DataFrame:
 
265
  return matches.head(limit)
266
 
267
  def get_category_stats(df: pd.DataFrame, category_name: str) -> Dict[str, Any]:
 
268
  if df.empty: return {}
269
  cat_df = df[df['category'].str.lower().str.contains(category_name.lower()) & df['is_offer']]
270
  if cat_df.empty:
 
281
  }
282
 
283
  def calculate_basket_optimization(item_names: List[str]) -> Dict[str, Any]:
 
 
 
284
  df = get_market_index()
285
  if df.empty:
286
  return {"actionable": False, "error": "No data"}
 
370
  }
371
 
372
  def calculate_zesa_units(amount_usd: float) -> Dict[str, Any]:
373
+ remaining = amount_usd / 1.06
 
374
  units = 0.0
375
  breakdown = []
376
 
 
411
  }
412
 
413
  # =========================
414
+ # 3. Gemini Helpers
415
  # =========================
416
 
417
  def gemini_detect_intent(transcript: str) -> Dict[str, Any]:
 
420
  PROMPT = """
421
  Analyze transcript. Return STRICT JSON.
422
  Classify intent:
423
+ - CASUAL_CHAT: Greetings, small talk, "hi", "thanks".
424
  - SHOPPING_BASKET: Looking for prices, creating a list.
425
  - UTILITY_CALC: Electricity/ZESA questions.
426
  - STORE_DECISION: "Where should I buy?", "Which store is cheapest?".
 
429
  Extract:
430
  - items: list of products
431
  - utility_amount: number
432
+ - context_tag: "budget", "speed", "quality"
433
 
434
  JSON Schema:
435
  {
 
469
  }}
470
  """
471
  try:
472
+ # FIX: Remove types.Part.from_text wrapping. Pass string directly.
473
  image_bytes = base64.b64decode(image_b64)
474
  resp = _gemini_client.models.generate_content(
475
  model=GEMINI_MODEL,
476
  contents=[
477
+ PROMPT, # Pass text as string directly
478
  types.Part.from_bytes(data=image_bytes, mime_type="image/jpeg")
479
  ],
480
  config=types.GenerateContentConfig(response_mime_type="application/json")
481
  )
482
  result = _safe_json_loads(resp.text, {"type": "IRRELEVANT", "items": []})
483
+ logger.info(f"🔮 VISION RAW: {json.dumps(result)}")
484
  return result
485
  except Exception as e:
486
  logger.error(f"Vision Error: {e}")
 
489
  def gemini_chat_response(transcript: str, intent: Dict, analyst_data: Dict, chat_history: str = "") -> str:
490
  if not _gemini_client: return "I'm having trouble connecting to my brain right now."
491
 
492
+ context_str = f"RECENT CHAT HISTORY (Last 6 messages):\n{chat_history}\n" if chat_history else ""
493
  context_str += f"ZIMBABWE CONTEXT: Fuel={ZIM_CONTEXT['fuel_petrol']}, ZESA Rate={ZIM_CONTEXT['zesa_step_1']['rate']}\n"
494
 
495
  if analyst_data:
 
504
  CONTEXT:
505
  {context_str}
506
 
507
+ CRITICAL RULES FOR HISTORY:
508
+ 1. Use 'RECENT CHAT HISTORY' ONLY to resolve references (e.g. "how much is *it*?").
509
+ 2. **TOPIC RESET**: If the user says "Hi", "Hello", or asks about a completely NEW topic, IGNORE the previous history items. Do not bring up old shopping lists unless asked.
510
+
511
  INSTRUCTIONS:
512
+ - **Casual**: Warm greeting. If they just said "Hi", reply "Makadii! How can I help you save today?". Don't summarize past chats.
513
+ - **Advice**: If data exists, guide them. "I found XYZ at Store A."
514
+ - **Trust**: If user asks "Is this expensive?", check 'category_stats'.
515
+ - **ZESA**: Explain units clearly.
 
 
 
 
 
516
 
517
  TONE: Conversational, Zimbabwean English.
 
518
  """
519
 
520
  try:
 
561
  "ok": True,
562
  "offers_indexed": len(df),
563
  "api_source": PRICE_API_BASE,
564
+ "persona": "Jessica v2.2"
565
  })
566
 
567
  @app.post("/chat")
 
580
  history_str = ""
581
  if db:
582
  try:
583
+ # Get last 6 messages
584
  docs = db.collection("pricelyst_profiles").document(pid).collection("chat_logs") \
585
  .order_by("ts", direction=firestore.Query.DESCENDING).limit(6).stream()
586
 
 
587
  msgs = []
588
  for d in docs:
589
  data = d.to_dict()
 
711
 
712
  return jsonify({
713
  "ok": True,
714
+ "memory_summary": prof.get("memory_summary", ""),
715
  "kpi_snapshot": json.dumps(kpi_snapshot)
716
  })
717