Spaces:
Running
Running
Update main.py
Browse files
main.py
CHANGED
|
@@ -1,14 +1,11 @@
|
|
| 1 |
"""
|
| 2 |
-
main.py — Pricelyst Shopping Advisor (Jessica Edition 2026 - Upgrade v2.
|
| 3 |
|
| 4 |
-
✅
|
| 5 |
-
✅
|
| 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 |
-
✅
|
| 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 (
|
| 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 |
-
|
| 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
|
| 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,
|
| 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 |
-
-
|
| 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 |
-
|
| 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)}")
|
| 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 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 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.
|
| 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
|
| 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", ""),
|
| 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 |
|