Spaces:
Running
Running
Update main.py
Browse files
main.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
| 1 |
"""
|
| 2 |
-
main.py — Pricelyst Shopping Advisor (Jessica Edition 2026 - Upgrade v2.
|
| 3 |
|
| 4 |
-
✅ Fixed:
|
| 5 |
-
✅ Fixed: "
|
| 6 |
-
✅
|
| 7 |
✅ "Analyst Engine": Enhanced Basket Math, Category Context, ZESA Logic.
|
| 8 |
✅ "Visual Engine": Lists, Products, & Meal-to-Recipe recognition.
|
| 9 |
✅ Memory Logic: Short-Term Sliding Window (Last 6 messages).
|
|
@@ -303,6 +303,8 @@ def calculate_basket_optimization(item_names: List[str]) -> Dict[str, Any]:
|
|
| 303 |
"product_id": int(best_prod['product_id']),
|
| 304 |
"name": str(best_prod['product_name']),
|
| 305 |
"category": str(best_prod['category']),
|
|
|
|
|
|
|
| 306 |
"category_stats": cat_stats
|
| 307 |
})
|
| 308 |
|
|
@@ -422,23 +424,21 @@ def gemini_detect_intent(transcript: str) -> Dict[str, Any]:
|
|
| 422 |
Analyze transcript. Return STRICT JSON.
|
| 423 |
Classify intent:
|
| 424 |
- CASUAL_CHAT: Greetings, small talk, "hi", "thanks".
|
| 425 |
-
- SHOPPING_BASKET: Looking for prices,
|
| 426 |
- UTILITY_CALC: Electricity/ZESA questions.
|
| 427 |
- STORE_DECISION: "Where should I buy?", "Which store is cheapest?".
|
| 428 |
- TRUST_CHECK: "Is this expensive?", "Is this a good deal?".
|
| 429 |
|
| 430 |
Extract:
|
| 431 |
-
- items: list of products
|
| 432 |
- utility_amount: number
|
| 433 |
-
- context_tag: "budget", "speed", "quality"
|
| 434 |
|
| 435 |
JSON Schema:
|
| 436 |
{
|
| 437 |
"actionable": boolean,
|
| 438 |
"intent": "string",
|
| 439 |
"items": ["string"],
|
| 440 |
-
"utility_amount": number
|
| 441 |
-
"context_tag": "string"
|
| 442 |
}
|
| 443 |
"""
|
| 444 |
try:
|
|
@@ -495,28 +495,34 @@ def gemini_chat_response(transcript: str, intent: Dict, analyst_data: Dict, chat
|
|
| 495 |
context_str += f"ZIMBABWE CONTEXT: Fuel={ZIM_CONTEXT['fuel_petrol']}, ZESA Rate={ZIM_CONTEXT['zesa_step_1']['rate']}\n"
|
| 496 |
|
| 497 |
if analyst_data:
|
| 498 |
-
context_str += f"ANALYST DATA: {json.dumps(analyst_data, default=str)}\n"
|
| 499 |
|
| 500 |
PROMPT = f"""
|
| 501 |
You are Jessica, Pricelyst's Shopping Advisor (Zimbabwe).
|
| 502 |
-
Role:
|
|
|
|
| 503 |
|
| 504 |
INPUT: "{transcript}"
|
| 505 |
INTENT: {intent.get('intent')}
|
| 506 |
CONTEXT:
|
| 507 |
{context_str}
|
| 508 |
|
| 509 |
-
CRITICAL
|
| 510 |
-
1.
|
| 511 |
-
|
|
|
|
|
|
|
|
|
|
| 512 |
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
|
|
|
|
|
|
| 520 |
"""
|
| 521 |
|
| 522 |
try:
|
|
@@ -527,7 +533,7 @@ def gemini_chat_response(transcript: str, intent: Dict, analyst_data: Dict, chat
|
|
| 527 |
return resp.text
|
| 528 |
except Exception as e:
|
| 529 |
logger.error(f"Chat Gen Error: {e}")
|
| 530 |
-
return "I
|
| 531 |
|
| 532 |
def gemini_generate_4step_plan(transcript: str, analyst_result: Dict) -> str:
|
| 533 |
if not _gemini_client: return "# Error\nAI Offline."
|
|
@@ -563,7 +569,7 @@ def health():
|
|
| 563 |
"ok": True,
|
| 564 |
"offers_indexed": len(df),
|
| 565 |
"api_source": PRICE_API_BASE,
|
| 566 |
-
"persona": "Jessica v2.
|
| 567 |
})
|
| 568 |
|
| 569 |
@app.post("/chat")
|
|
@@ -604,7 +610,8 @@ def chat():
|
|
| 604 |
analyst_data = {}
|
| 605 |
|
| 606 |
# 3. Data Processing (The Analyst)
|
| 607 |
-
if
|
|
|
|
| 608 |
analyst_data = calculate_basket_optimization(items)
|
| 609 |
|
| 610 |
elif intent_type == "UTILITY_CALC":
|
|
@@ -651,10 +658,7 @@ def analyze_image():
|
|
| 651 |
description = vision_result.get("description", "an image")
|
| 652 |
|
| 653 |
# Fallback: If type is PRODUCT/MEAL but items is empty, try to use description as search item
|
| 654 |
-
# This catches the "Pepsi bottle" case where items was []
|
| 655 |
if (img_type in ["PRODUCT", "MEAL"]) and not items and description:
|
| 656 |
-
# Simple heuristic: treat the description as the item for fuzzy search
|
| 657 |
-
# It won't be perfect, but it prevents the "silent failure"
|
| 658 |
items = [description]
|
| 659 |
logger.info(f"🔮 Fallback: Used description '{description}' as item.")
|
| 660 |
|
|
@@ -663,7 +667,7 @@ def analyze_image():
|
|
| 663 |
|
| 664 |
# 2. Logic Branching
|
| 665 |
if img_type == "IRRELEVANT" and not items:
|
| 666 |
-
# Graceful Rejection
|
| 667 |
prompt = f"User uploaded a photo of: {description}. If it is a pet/flower/view, compliment it warmly! Then effectively explain you are a shopping bot and can't price check that."
|
| 668 |
response_text = gemini_chat_response(prompt, {"intent": "CASUAL_CHAT"}, {}, "")
|
| 669 |
|
|
@@ -671,17 +675,17 @@ def analyze_image():
|
|
| 671 |
# Run the Analyst Engine
|
| 672 |
analyst_data = calculate_basket_optimization(items)
|
| 673 |
|
| 674 |
-
# 3. DYNAMIC SIMULATED INTENT
|
| 675 |
if img_type == "MEAL":
|
| 676 |
-
simulated_user_msg = f"I want to cook {description}. I need
|
| 677 |
intent_sim = {"intent": "SHOPPING_BASKET"}
|
| 678 |
|
| 679 |
elif img_type == "LIST":
|
| 680 |
-
simulated_user_msg = f"Here is my
|
| 681 |
intent_sim = {"intent": "STORE_DECISION"}
|
| 682 |
|
| 683 |
else: # PRODUCT
|
| 684 |
-
simulated_user_msg = f"I see {description}.
|
| 685 |
intent_sim = {"intent": "STORE_DECISION"}
|
| 686 |
|
| 687 |
# Generate Response
|
|
@@ -689,11 +693,10 @@ def analyze_image():
|
|
| 689 |
simulated_user_msg,
|
| 690 |
intent_sim,
|
| 691 |
analyst_data,
|
| 692 |
-
chat_history=""
|
| 693 |
)
|
| 694 |
|
| 695 |
else:
|
| 696 |
-
# Catch-all if something really weird happens (Product type but no description/items)
|
| 697 |
response_text = "I couldn't quite identify the product in that image. Could you type the name for me?"
|
| 698 |
|
| 699 |
return jsonify({
|
|
|
|
| 1 |
"""
|
| 2 |
+
main.py — Pricelyst Shopping Advisor (Jessica Edition 2026 - Upgrade v2.5)
|
| 3 |
|
| 4 |
+
✅ Fixed: "Basket Regression" - AI now returns prices IMMEDIATELY.
|
| 5 |
+
✅ Fixed: "Bluffing" - AI explicitly states if item is found or missing.
|
| 6 |
+
✅ Optimization: Removed "Add to list" chatter. Shortest path to value.
|
| 7 |
✅ "Analyst Engine": Enhanced Basket Math, Category Context, ZESA Logic.
|
| 8 |
✅ "Visual Engine": Lists, Products, & Meal-to-Recipe recognition.
|
| 9 |
✅ Memory Logic: Short-Term Sliding Window (Last 6 messages).
|
|
|
|
| 303 |
"product_id": int(best_prod['product_id']),
|
| 304 |
"name": str(best_prod['product_name']),
|
| 305 |
"category": str(best_prod['category']),
|
| 306 |
+
"retailer": str(best_prod['retailer']), # Added explicitly for prompt access
|
| 307 |
+
"price": float(best_prod['price']), # Added explicitly for prompt access
|
| 308 |
"category_stats": cat_stats
|
| 309 |
})
|
| 310 |
|
|
|
|
| 424 |
Analyze transcript. Return STRICT JSON.
|
| 425 |
Classify intent:
|
| 426 |
- CASUAL_CHAT: Greetings, small talk, "hi", "thanks".
|
| 427 |
+
- SHOPPING_BASKET: Looking for prices, products, lists, or "cheapest X".
|
| 428 |
- UTILITY_CALC: Electricity/ZESA questions.
|
| 429 |
- STORE_DECISION: "Where should I buy?", "Which store is cheapest?".
|
| 430 |
- TRUST_CHECK: "Is this expensive?", "Is this a good deal?".
|
| 431 |
|
| 432 |
Extract:
|
| 433 |
+
- items: list of products found in the text.
|
| 434 |
- utility_amount: number
|
|
|
|
| 435 |
|
| 436 |
JSON Schema:
|
| 437 |
{
|
| 438 |
"actionable": boolean,
|
| 439 |
"intent": "string",
|
| 440 |
"items": ["string"],
|
| 441 |
+
"utility_amount": number
|
|
|
|
| 442 |
}
|
| 443 |
"""
|
| 444 |
try:
|
|
|
|
| 495 |
context_str += f"ZIMBABWE CONTEXT: Fuel={ZIM_CONTEXT['fuel_petrol']}, ZESA Rate={ZIM_CONTEXT['zesa_step_1']['rate']}\n"
|
| 496 |
|
| 497 |
if analyst_data:
|
| 498 |
+
context_str += f"ANALYST DATA (Prices/Availability): {json.dumps(analyst_data, default=str)}\n"
|
| 499 |
|
| 500 |
PROMPT = f"""
|
| 501 |
You are Jessica, Pricelyst's Shopping Advisor (Zimbabwe).
|
| 502 |
+
Role: Intelligent Shopping Companion.
|
| 503 |
+
Goal: Shortest path to value. Give answers, not promises.
|
| 504 |
|
| 505 |
INPUT: "{transcript}"
|
| 506 |
INTENT: {intent.get('intent')}
|
| 507 |
CONTEXT:
|
| 508 |
{context_str}
|
| 509 |
|
| 510 |
+
CRITICAL INSTRUCTIONS (Shortest Path Rule):
|
| 511 |
+
1. **CHECK ANALYST DATA FIRST**:
|
| 512 |
+
- If `ANALYST DATA` contains `found_items_details` or `split_strategy` with prices: **REPORT THEM IMMEDIATELY**.
|
| 513 |
+
- Say: "I found [Product] at [Retailer] for $[Price]."
|
| 514 |
+
- Do NOT say "I will add this to your list."
|
| 515 |
+
- Do NOT say "I will check for you." (You have already checked!)
|
| 516 |
|
| 517 |
+
2. **MISSING ITEMS**:
|
| 518 |
+
- If `global_missing` has items: Say "I checked, but we don't have [Item] in our current catalogue."
|
| 519 |
+
- Don't fake it. Be honest about catalogue gaps.
|
| 520 |
+
|
| 521 |
+
3. **CASUAL CHAT**:
|
| 522 |
+
- Only if no products are mentioned. "Makadii! How can I help?"
|
| 523 |
+
- Reset topic if user says "Hi" or changes subject.
|
| 524 |
+
|
| 525 |
+
TONE: Helpful, direct, Zimbabwean. Use Markdown for prices (e.g. **$3.50**).
|
| 526 |
"""
|
| 527 |
|
| 528 |
try:
|
|
|
|
| 533 |
return resp.text
|
| 534 |
except Exception as e:
|
| 535 |
logger.error(f"Chat Gen Error: {e}")
|
| 536 |
+
return "I checked the prices, but I'm having trouble displaying them right now."
|
| 537 |
|
| 538 |
def gemini_generate_4step_plan(transcript: str, analyst_result: Dict) -> str:
|
| 539 |
if not _gemini_client: return "# Error\nAI Offline."
|
|
|
|
| 569 |
"ok": True,
|
| 570 |
"offers_indexed": len(df),
|
| 571 |
"api_source": PRICE_API_BASE,
|
| 572 |
+
"persona": "Jessica v2.5 (Immediate Price Check)"
|
| 573 |
})
|
| 574 |
|
| 575 |
@app.post("/chat")
|
|
|
|
| 610 |
analyst_data = {}
|
| 611 |
|
| 612 |
# 3. Data Processing (The Analyst)
|
| 613 |
+
# Trigger Analyst if Items exist OR intent is specifically about shopping/decisions
|
| 614 |
+
if items or intent_type in ["SHOPPING_BASKET", "STORE_DECISION", "TRUST_CHECK"]:
|
| 615 |
analyst_data = calculate_basket_optimization(items)
|
| 616 |
|
| 617 |
elif intent_type == "UTILITY_CALC":
|
|
|
|
| 658 |
description = vision_result.get("description", "an image")
|
| 659 |
|
| 660 |
# Fallback: If type is PRODUCT/MEAL but items is empty, try to use description as search item
|
|
|
|
| 661 |
if (img_type in ["PRODUCT", "MEAL"]) and not items and description:
|
|
|
|
|
|
|
| 662 |
items = [description]
|
| 663 |
logger.info(f"🔮 Fallback: Used description '{description}' as item.")
|
| 664 |
|
|
|
|
| 667 |
|
| 668 |
# 2. Logic Branching
|
| 669 |
if img_type == "IRRELEVANT" and not items:
|
| 670 |
+
# Graceful Rejection
|
| 671 |
prompt = f"User uploaded a photo of: {description}. If it is a pet/flower/view, compliment it warmly! Then effectively explain you are a shopping bot and can't price check that."
|
| 672 |
response_text = gemini_chat_response(prompt, {"intent": "CASUAL_CHAT"}, {}, "")
|
| 673 |
|
|
|
|
| 675 |
# Run the Analyst Engine
|
| 676 |
analyst_data = calculate_basket_optimization(items)
|
| 677 |
|
| 678 |
+
# 3. DYNAMIC SIMULATED INTENT (Force immediate answer)
|
| 679 |
if img_type == "MEAL":
|
| 680 |
+
simulated_user_msg = f"I want to cook {description}. I need {', '.join(items)}. How much does it cost?"
|
| 681 |
intent_sim = {"intent": "SHOPPING_BASKET"}
|
| 682 |
|
| 683 |
elif img_type == "LIST":
|
| 684 |
+
simulated_user_msg = f"Here is my list: {', '.join(items)}. What are the prices?"
|
| 685 |
intent_sim = {"intent": "STORE_DECISION"}
|
| 686 |
|
| 687 |
else: # PRODUCT
|
| 688 |
+
simulated_user_msg = f"I see {description}. What is the price for {', '.join(items)}?"
|
| 689 |
intent_sim = {"intent": "STORE_DECISION"}
|
| 690 |
|
| 691 |
# Generate Response
|
|
|
|
| 693 |
simulated_user_msg,
|
| 694 |
intent_sim,
|
| 695 |
analyst_data,
|
| 696 |
+
chat_history=""
|
| 697 |
)
|
| 698 |
|
| 699 |
else:
|
|
|
|
| 700 |
response_text = "I couldn't quite identify the product in that image. Could you type the name for me?"
|
| 701 |
|
| 702 |
return jsonify({
|