Fred808 commited on
Commit
7255dd1
·
verified ·
1 Parent(s): 4c38444

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +325 -0
app.py CHANGED
@@ -460,6 +460,331 @@ async def process_order_flow(user_id: str, message: str) -> str:
460
  return f"You selected {found_dish}. How many servings would you like?"
461
  else:
462
  return "I couldn't identify the dish. Please type the dish name from our menu."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
 
464
  class TranscriptionResponse(BaseModel):
465
  transcription: str
 
460
  return f"You selected {found_dish}. How many servings would you like?"
461
  else:
462
  return "I couldn't identify the dish. Please type the dish name from our menu."
463
+ if state.step == 2:
464
+ numbers = re.findall(r'\d+', message)
465
+ if not numbers:
466
+ return "Please enter a valid number for the quantity (e.g., 1, 2, 3)."
467
+ quantity = int(numbers[0])
468
+ if quantity <= 0:
469
+ return "Please enter a valid quantity (e.g., 1, 2, 3)."
470
+ state.data["quantity"] = quantity
471
+ state.step = 3
472
+ return f"Got it. {quantity} serving(s) of {state.data.get('dish')}. Please provide your phone number and delivery address."
473
+
474
+ if state.step == 3:
475
+ phone_pattern = r'(\+?\d{10,15})'
476
+ phone_match = re.search(phone_pattern, message)
477
+ address = None
478
+ if phone_match:
479
+ phone_number = phone_match.group(1)
480
+ address_start = phone_match.end()
481
+ address = message[address_start:].strip()
482
+ address = re.sub(r'^[,\s]+|[,\s]+$', '', address)
483
+ if phone_match and address:
484
+ state.data["phone_number"] = phone_number
485
+ state.data["address"] = address
486
+ asyncio.create_task(update_user_profile(user_id, phone_number, address))
487
+ shipping_cost = calculate_shipping_cost(address)
488
+ state.data["shipping_cost"] = shipping_cost
489
+ state.step = 5
490
+ return (f"Thanks! Your phone number is recorded as: {phone_number}.\n"
491
+ f"Your delivery address is: {address}.\n"
492
+ f"Your delivery cost is N{shipping_cost}. Would you like extras (yes/no)?")
493
+ elif phone_match:
494
+ state.data["phone_number"] = phone_match.group(1)
495
+ asyncio.create_task(update_user_profile(user_id, phone_number))
496
+ return "Thank you. Please provide your delivery address."
497
+ else:
498
+ return ("Please provide both your phone number and delivery address. "
499
+ "For example: '09162409591, 1, Iyana Isashi, Isashi, Ojo, Lagos'.")
500
+
501
+ # Steps 4, 5, 6, and 7 remain unchanged.
502
+ if state.step == 4:
503
+ state.data["address"] = message
504
+ asyncio.create_task(update_user_profile(user_id, address=message))
505
+ shipping_cost = calculate_shipping_cost(message)
506
+ state.data["shipping_cost"] = shipping_cost
507
+ state.step = 5
508
+ return (f"Thanks. Your delivery address is recorded as: {message}.\n"
509
+ f"Your delivery cost is N{shipping_cost}. Would you like extras (yes/no)?")
510
+
511
+ if state.step == 5:
512
+ if message.lower() in ["yes", "y"]:
513
+ state.step = 6
514
+ return "Please list the extras you'd like (e.g., drinks, sides)."
515
+ elif message.lower() in ["no", "n"]:
516
+ state.data["extras"] = ""
517
+ state.step = 7
518
+ dish = state.data.get("dish", "")
519
+ quantity = state.data.get("quantity", 1)
520
+ phone = state.data.get("phone_number", "")
521
+ address = state.data.get("address", "")
522
+ shipping_cost = state.data.get("shipping_cost", 0)
523
+ price_per_serving = 1500
524
+ total_price = (quantity * price_per_serving) + shipping_cost
525
+ summary = (f"Order Summary:\nDish: {dish}\nQuantity: {quantity}\n"
526
+ f"Phone: {phone}\nAddress: {address}\n"
527
+ f"Shipping Cost: N{shipping_cost}\n"
528
+ f"Total Price: N{total_price}\n"
529
+ f"Extras: None\nConfirm order? (yes/no)")
530
+ return summary
531
+ else:
532
+ return "Please respond with 'yes' or 'no' regarding extras."
533
+
534
+ if state.step == 6:
535
+ state.data["extras"] = message
536
+ state.step = 7
537
+ dish = state.data.get("dish", "")
538
+ quantity = state.data.get("quantity", 1)
539
+ phone = state.data.get("phone_number", "")
540
+ address = state.data.get("address", "")
541
+ shipping_cost = state.data.get("shipping_cost", 0)
542
+ extras = state.data.get("extras", "")
543
+ price_per_serving = 1500
544
+ total_price = (quantity * price_per_serving) + shipping_cost
545
+ summary = (f"Order Summary:\nDish: {dish}\nQuantity: {quantity}\n"
546
+ f"Phone: {phone}\nAddress: {address}\n"
547
+ f"Shipping Cost: N{shipping_cost}\n"
548
+ f"Total Price: N{total_price}\n"
549
+ f"Extras: {extras}\nConfirm order? (yes/no)")
550
+ return summary
551
+
552
+ if state.step == 7:
553
+ if message.lower() in ["yes", "y"]:
554
+ order_id = f"ORD-{int(time.time())}"
555
+ state.data["order_id"] = order_id
556
+ price_per_serving = 1500
557
+ quantity = state.data.get("quantity", 1)
558
+ shipping_cost = state.data.get("shipping_cost", 0)
559
+ total_price = (quantity * price_per_serving) + shipping_cost
560
+ state.data["price"] = str(total_price)
561
+
562
+ async def save_order():
563
+ async with async_session() as session:
564
+ order = Order(
565
+ order_id=order_id,
566
+ user_id=user_id,
567
+ dish=state.data["dish"],
568
+ quantity=str(quantity),
569
+ price=str(total_price),
570
+ status="Pending Payment",
571
+ delivery_address=state.data.get("address", "")
572
+ )
573
+ session.add(order)
574
+ await session.commit()
575
+ asyncio.create_task(save_order())
576
+ asyncio.create_task(log_order_tracking(order_id, "Order Placed", "Order placed and awaiting payment."))
577
+
578
+ async def notify_management_order(order_details: dict):
579
+ message_body = (
580
+ f"New Order Received:\n"
581
+ f"Order ID: {order_details['order_id']}\n"
582
+ f"Dish: {order_details['dish']}\n"
583
+ f"Quantity: {order_details['quantity']}\n"
584
+ f"Total Price: {order_details['price']}\n"
585
+ f"Phone: {state.data.get('phone_number', '')}\n"
586
+ f"Delivery Address: {order_details.get('address', 'Not Provided')}\n"
587
+ f"Extras: {state.data.get('extras', 'None')}\n"
588
+ f"Status: Pending Payment"
589
+ )
590
+ await asyncio.to_thread(send_whatsapp_message, MANAGEMENT_WHATSAPP_NUMBER, message_body)
591
+ order_details = {
592
+ "order_id": order_id,
593
+ "dish": state.data["dish"],
594
+ "quantity": state.data["quantity"],
595
+ "price": state.data["price"],
596
+ "address": state.data.get("address", "")
597
+ }
598
+ asyncio.create_task(notify_management_order(order_details))
599
+
600
+ email = "customer@example.com"
601
+ payment_data = create_paystack_payment_link(email, total_price * 100, order_id)
602
+ dish_name = state.data.get("dish", "")
603
+ state.reset()
604
+ if user_id in user_state:
605
+ del user_state[user_id]
606
+ if payment_data.get("status"):
607
+ payment_link = payment_data["data"]["authorization_url"]
608
+ return (f"Thank you for your order of {quantity} serving(s) of {dish_name}! "
609
+ f"Your Order ID is {order_id}.\nPlease complete payment here: {payment_link}\n"
610
+ "You can track your order status using your Order ID.\n"
611
+ "Is there anything else you'd like to order?")
612
+ else:
613
+ return (f"Your order has been placed with Order ID {order_id}, "
614
+ "but we could not initialize payment. Please try again later.")
615
+ else:
616
+ state.reset()
617
+ if user_id in user_state:
618
+ del user_state[user_id]
619
+ return "Order canceled. Let me know if you'd like to try again."
620
+ return ""
621
+
622
+
623
+ async def get_or_create_user_profile(user_id: str, phone_number: str = None) -> UserProfile:
624
+ async with async_session() as session:
625
+ result = await session.execute(
626
+ select(UserProfile).where(UserProfile.user_id == user_id)
627
+ )
628
+ profile = result.scalars().first()
629
+ if profile is None:
630
+ profile = UserProfile(
631
+ user_id=user_id,
632
+ phone_number=phone_number,
633
+ last_interaction=datetime.utcnow()
634
+ )
635
+ session.add(profile)
636
+ await session.commit()
637
+ return profile
638
+
639
+ async def update_user_last_interaction(user_id: str):
640
+ async with async_session() as session:
641
+ result = await session.execute(
642
+ select(UserProfile).where(UserProfile.user_id == user_id)
643
+ )
644
+ profile = result.scalars().first()
645
+ if profile:
646
+ profile.last_interaction = datetime.utcnow()
647
+ await session.commit()
648
+
649
+ async def send_proactive_greeting(user_id: str):
650
+ greeting = "Hi again! We miss you. Would you like to see our new menu items or get personalized recommendations?"
651
+ await log_chat_to_db(user_id, "outbound", greeting)
652
+ return greeting
653
+
654
+ app = FastAPI()
655
+
656
+ @app.on_event("startup")
657
+ async def on_startup():
658
+ await init_db()
659
+
660
+ @app.post("/chatbot")
661
+ async def chatbot_response(request: Request, background_tasks: BackgroundTasks):
662
+ data = await request.json()
663
+ user_id = data.get("user_id")
664
+ user_message = data.get("message", "").strip()
665
+
666
+ if user_id not in conversation_context:
667
+ conversation_context[user_id] = []
668
+
669
+ conversation_context[user_id].append({
670
+ "timestamp": datetime.utcnow().isoformat(),
671
+ "role": "user",
672
+ "message": user_message
673
+ })
674
+
675
+ background_tasks.add_task(log_chat_to_db, user_id, "inbound", user_message)
676
+
677
+ sentiment_score = analyze_sentiment(user_message)
678
+ background_tasks.add_task(log_sentiment, user_id, user_message, sentiment_score)
679
+ sentiment_modifier = "Great to hear from you! " if sentiment_score > 0.3 else ""
680
+
681
+ if user_message.strip() == "1" or "menu" in user_message.lower():
682
+ if user_id in user_state:
683
+ del user_state[user_id]
684
+ menu_with_images = []
685
+ for index, item in enumerate(menu_items, start=1):
686
+ image_url = google_image_scrape(item["name"])
687
+ menu_with_images.append({
688
+ "number": index,
689
+ "name": item["name"],
690
+ "description": item["description"],
691
+ "price": item["price"],
692
+ "image_url": image_url
693
+ })
694
+ response_payload = {
695
+ "response": sentiment_modifier + "Here’s our delicious menu:",
696
+ "menu": menu_with_images,
697
+ "follow_up": (
698
+ "To order, type the *number* or *name* of the dish you'd like. "
699
+ "For example, type '1' or 'Jollof Rice' to order Jollof Rice.\n\n"
700
+ "You can also ask for nutritional facts by typing, for example, 'Nutritional facts for Jollof Rice'."
701
+ )
702
+ }
703
+ background_tasks.add_task(log_chat_to_db, user_id, "outbound", str(response_payload))
704
+ conversation_context[user_id].append({
705
+ "timestamp": datetime.utcnow().isoformat(),
706
+ "role": "bot",
707
+ "message": response_payload["response"]
708
+ })
709
+ return JSONResponse(content=response_payload)
710
+
711
+ if is_order_intent(user_message) or (user_id in user_state and user_state[user_id].flow == "order"):
712
+ order_response = await process_order_flow(user_id, user_message)
713
+ if order_response:
714
+ background_tasks.add_task(log_chat_to_db, user_id, "outbound", order_response)
715
+ conversation_context[user_id].append({
716
+ "timestamp": datetime.utcnow().isoformat(),
717
+ "role": "bot",
718
+ "message": order_response
719
+ })
720
+ return JSONResponse(content={"response": sentiment_modifier + order_response})
721
+
722
+ # Instead of calling the LLM fallback, use a default response:
723
+ default_response = "I'm sorry, I didn't understand that. Please type 'menu' to see our options or 'order' to place an order."
724
+ background_tasks.add_task(log_chat_to_db, user_id, "outbound", default_response)
725
+ conversation_context[user_id].append({
726
+ "timestamp": datetime.utcnow().isoformat(),
727
+ "role": "bot",
728
+ "message": default_response
729
+ })
730
+ return JSONResponse(content={"response": sentiment_modifier + default_response})
731
+
732
+
733
+ @app.get("/chat_history/{user_id}")
734
+ async def get_chat_history(user_id: str):
735
+ async with async_session() as session:
736
+ result = await session.execute(
737
+ ChatHistory.__table__.select().where(ChatHistory.user_id == user_id)
738
+ )
739
+ history = result.fetchall()
740
+ return [dict(row) for row in history]
741
+
742
+ @app.get("/order/{order_id}")
743
+ async def get_order(order_id: str):
744
+ async with async_session() as session:
745
+ result = await session.execute(
746
+ Order.__table__.select().where(Order.order_id == order_id)
747
+ )
748
+ order = result.fetchone()
749
+ if order:
750
+ return dict(order)
751
+ else:
752
+ raise HTTPException(status_code=404, detail="Order not found.")
753
+
754
+ @app.get("/user_profile/{user_id}")
755
+ async def get_user_profile(user_id: str):
756
+ profile = await get_or_create_user_profile(user_id)
757
+ return {
758
+ "user_id": profile.user_id,
759
+ "phone_number": profile.phone_number,
760
+ "name": profile.name,
761
+ "email": profile.email,
762
+ "preferences": profile.preferences,
763
+ "last_interaction": profile.last_interaction.isoformat(),
764
+ "order_ids": profile.order_ids
765
+ }
766
+
767
+ @app.get("/analytics")
768
+ async def get_analytics():
769
+ async with async_session() as session:
770
+ msg_result = await session.execute(ChatHistory.__table__.count())
771
+ total_messages = msg_result.scalar() or 0
772
+ order_result = await session.execute(Order.__table__.count())
773
+ total_orders = order_result.scalar() or 0
774
+ sentiment_result = await session.execute("SELECT AVG(sentiment_score) FROM sentiment_logs")
775
+ avg_sentiment = sentiment_result.scalar() or 0
776
+ return {
777
+ "total_messages": total_messages,
778
+ "total_orders": total_orders,
779
+ "average_sentiment": avg_sentiment
780
+ }
781
+
782
+ HUGGING_FACE_API_TOKEN = os.getenv("HUGGING_FACE_API_TOKEN")
783
+ if not HUGGING_FACE_API_TOKEN:
784
+ raise ValueError("Hugging Face API token not found in environment variables.")
785
+
786
+ WHISPER_API_URL = "https://router.huggingface.co/fal-ai"
787
+ WHISPER_API_HEADERS = {"Authorization": f"Bearer {HUGGING_FACE_API_TOKEN}"}
788
 
789
  class TranscriptionResponse(BaseModel):
790
  transcription: str