Fred808 commited on
Commit
d1ffc17
·
verified ·
1 Parent(s): 793cb51

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +185 -111
app.py CHANGED
@@ -28,6 +28,7 @@ PAYSTACK_SECRET_KEY = os.getenv("PAYSTACK_SECRET_KEY", "default_fallback_value")
28
  DATABASE_URL = os.getenv("DATABASE_URL", "default_fallback_value") # Example using SQLite
29
  NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY", "default_fallback_value")
30
  openai.api_key = os.getenv("OPENAI_API_KEY", "default_fallback_value")
 
31
 
32
  # WhatsApp Business API credentials (Cloud API)
33
  WHATSAPP_PHONE_NUMBER_ID = os.getenv("WHATSAPP_PHONE_NUMBER_ID", "default_value")
@@ -77,6 +78,7 @@ class UserProfile(Base):
77
  email = Column(String, default="unknown@example.com")
78
  preferences = Column(Text, default="")
79
  last_interaction = Column(DateTime, default=datetime.utcnow)
 
80
 
81
  class SentimentLog(Base):
82
  __tablename__ = "sentiment_logs"
@@ -86,7 +88,6 @@ class SentimentLog(Base):
86
  sentiment_score = Column(Float)
87
  message = Column(Text)
88
 
89
- # --- New Model: Order Tracking ---
90
  class OrderTracking(Base):
91
  __tablename__ = "order_tracking"
92
  id = Column(Integer, primary_key=True, index=True)
@@ -276,33 +277,96 @@ def calculate_shipping_cost(address: str) -> int:
276
  return cost
277
  return TOWN_SHIPPING_COSTS["default"] # Default shipping cost for unknown areas
278
 
279
- # ... (rest of your imports and setup)
 
 
 
 
 
 
280
 
281
- async def update_user_profile(user_id: str, phone_number: str = None, address: str = None) -> UserProfile:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  """
283
- Update or create a user profile with the provided phone number and address.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  async with async_session() as session:
286
  result = await session.execute(
287
  select(UserProfile).where(UserProfile.user_id == user_id)
288
  )
289
  profile = result.scalars().first()
290
- if profile is None:
291
- profile = UserProfile(
292
- user_id=user_id,
293
- phone_number=phone_number,
294
- address=address,
295
- last_interaction=datetime.utcnow()
296
- )
297
- else:
298
- if phone_number:
299
- profile.phone_number = phone_number
300
- if address:
301
- profile.address = address
302
- profile.last_interaction = datetime.utcnow()
303
- session.add(profile)
304
- await session.commit()
305
- return profile
306
 
307
  # ... (rest of your imports and setup)
308
 
@@ -522,79 +586,88 @@ def process_order_flow(user_id: str, message: str) -> str:
522
  f"Extras: {extras}\nConfirm order? (yes/no)")
523
  return summary
524
 
525
- # --- Step 7: Order Confirmation & Finalization ---
526
- if state.step == 7:
527
- if message.lower() in ["yes", "y"]:
528
- order_id = f"ORD-{int(time.time())}"
529
- state.data["order_id"] = order_id
530
- price_per_serving = 1500 # Fixed price per serving
531
- quantity = state.data.get("quantity", 1)
532
- shipping_cost = state.data.get("shipping_cost", 0)
533
- total_price = (quantity * price_per_serving) + shipping_cost
534
- state.data["price"] = str(total_price)
535
-
536
- # Save the order asynchronously (including delivery_address, phone, extras, shipping_cost)
537
- async def save_order():
538
- async with async_session() as session:
539
- order = Order(
540
- order_id=order_id,
541
- user_id=user_id,
542
- dish=state.data["dish"],
543
- quantity=str(quantity),
544
- price=str(total_price),
545
- status="Pending Payment",
546
- delivery_address=state.data.get("address", ""),
547
- shipping_cost=str(shipping_cost) # Save shipping cost
548
- )
549
- session.add(order)
550
- await session.commit()
551
- asyncio.create_task(save_order())
552
-
553
- # Record the initial tracking update: Order Placed
554
- asyncio.create_task(log_order_tracking(order_id, "Order Placed", "Order placed and awaiting payment."))
555
-
556
- # Notify management of the new order via WhatsApp
557
- async def notify_management_order(order_details: dict):
558
- message_body = (
559
- f"New Order Received:\n"
560
- f"Order ID: {order_details['order_id']}\n"
561
- f"Dish: {order_details['dish']}\n"
562
- f"Quantity: {order_details['quantity']}\n"
563
- f"Total Price: {order_details['price']}\n"
564
- f"Phone: {state.data.get('phone_number', '')}\n"
565
- f"Delivery Address: {order_details.get('address', 'Not Provided')}\n"
566
- f"Extras: {state.data.get('extras', 'None')}\n"
567
- f"Status: Pending Payment"
568
- )
569
- await asyncio.to_thread(send_whatsapp_message, MANAGEMENT_WHATSAPP_NUMBER, message_body)
570
- order_details = {
571
- "order_id": order_id,
572
- "dish": state.data["dish"],
573
- "quantity": state.data["quantity"],
574
- "price": state.data["price"],
575
- "address": state.data.get("address", "")
576
- }
577
- asyncio.create_task(notify_management_order(order_details))
578
-
579
- email = "customer@example.com" # Placeholder; retrieve from profile if available
580
- payment_data = create_paystack_payment_link(email, total_price * 100, order_id)
581
- dish_name = state.data.get("dish", "")
582
- state.reset()
583
- if user_id in user_state:
584
- del user_state[user_id]
585
- if payment_data.get("status"):
586
- payment_link = payment_data["data"]["authorization_url"]
587
- return (f"Thank you for your order of {quantity} serving(s) of {dish_name}! "
588
- f"Your Order ID is {order_id}.\nPlease complete payment here: {payment_link}\n"
589
- "You can track your order status using your Order ID.\n"
590
- "Is there anything else you'd like to order?")
591
- else:
592
- return f"Your order has been placed with Order ID {order_id}, but we could not initialize payment. Please try again later."
593
- else:
594
- state.reset()
595
- if user_id in user_state:
596
- del user_state[user_id]
597
- return "Order canceled. Let me know if you'd like to try again."
 
 
 
 
 
 
 
 
 
598
 
599
  return ""
600
 
@@ -638,6 +711,7 @@ app = FastAPI()
638
  async def on_startup():
639
  await init_db()
640
 
 
641
  @app.post("/chatbot")
642
  async def chatbot_response(request: Request, background_tasks: BackgroundTasks):
643
  data = await request.json()
@@ -674,31 +748,31 @@ async def chatbot_response(request: Request, background_tasks: BackgroundTasks):
674
  background_tasks.add_task(log_sentiment, user_id, user_message, sentiment_score)
675
  sentiment_modifier = ""
676
  if sentiment_score < -0.3:
677
- sentiment_modifier = "I'm sorry if you're having a tough time. "
678
  elif sentiment_score > 0.3:
679
  sentiment_modifier = "Great to hear from you! "
680
 
681
- # --- Order Tracking Handling ---
682
- order_id_match = re.search(r"ord-\d+", user_message.lower())
683
  if order_id_match:
684
  order_id = order_id_match.group(0)
685
  try:
686
- # Call the /track_order endpoint
687
- tracking_response = await track_order(order_id)
688
  return JSONResponse(content={"response": tracking_response})
689
  except HTTPException as e:
690
  return JSONResponse(content={"response": f"⚠️ {e.detail}"})
691
 
692
  # --- Order Flow Handling ---
693
- order_response = process_order_flow(user_id, user_message)
694
- if order_response:
695
- background_tasks.add_task(log_chat_to_db, user_id, "outbound", order_response)
696
- conversation_context[user_id].append({
697
- "timestamp": datetime.utcnow().isoformat(),
698
- "role": "bot",
699
- "message": order_response
700
- })
701
- return JSONResponse(content={"response": sentiment_modifier + order_response})
 
702
 
703
  # --- Menu Display ---
704
  if "menu" in user_message.lower():
@@ -828,7 +902,8 @@ async def get_user_profile(user_id: str):
828
  "name": profile.name,
829
  "email": profile.email,
830
  "preferences": profile.preferences,
831
- "last_interaction": profile.last_interaction.isoformat()
 
832
  }
833
 
834
  @app.get("/analytics")
@@ -897,7 +972,6 @@ async def process_voice(file: UploadFile = File(...)):
897
 
898
  # --- Payment Callback Endpoint with Payment Tracking and Redirection ---
899
  @app.api_route("/payment_callback", methods=["GET", "POST"])
900
-
901
  async def payment_callback(request: Request):
902
  # GET: User redirection after payment
903
  if request.method == "GET":
@@ -947,7 +1021,7 @@ async def payment_callback(request: Request):
947
  return JSONResponse(content={"message": "Order updated successfully."})
948
  else:
949
  raise HTTPException(status_code=404, detail="Order not found.")
950
-
951
  @app.get("/track_order/{order_id}")
952
  async def track_order(order_id: str):
953
  """
@@ -974,4 +1048,4 @@ async def track_order(order_id: str):
974
 
975
  if __name__ == "__main__":
976
  import uvicorn
977
- uvicorn.run(app, host="0.0.0.0", port=8000)
 
28
  DATABASE_URL = os.getenv("DATABASE_URL", "default_fallback_value") # Example using SQLite
29
  NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY", "default_fallback_value")
30
  openai.api_key = os.getenv("OPENAI_API_KEY", "default_fallback_value")
31
+ GOOGLE_MAPS_API_KEY = os.getenv("GOOGLE_MAPS_API_KEY", "default_fallback_value") # Add Google Maps API key
32
 
33
  # WhatsApp Business API credentials (Cloud API)
34
  WHATSAPP_PHONE_NUMBER_ID = os.getenv("WHATSAPP_PHONE_NUMBER_ID", "default_value")
 
78
  email = Column(String, default="unknown@example.com")
79
  preferences = Column(Text, default="")
80
  last_interaction = Column(DateTime, default=datetime.utcnow)
81
+ order_ids = Column(Text, default="") # Store order IDs as a comma-separated string
82
 
83
  class SentimentLog(Base):
84
  __tablename__ = "sentiment_logs"
 
88
  sentiment_score = Column(Float)
89
  message = Column(Text)
90
 
 
91
  class OrderTracking(Base):
92
  __tablename__ = "order_tracking"
93
  id = Column(Integer, primary_key=True, index=True)
 
277
  return cost
278
  return TOWN_SHIPPING_COSTS["default"] # Default shipping cost for unknown areas
279
 
280
+ # --- Google Maps ETA Calculation ---
281
+ def calculate_eta(destination: str) -> str:
282
+ """
283
+ Calculate ETA from the restaurant address to the customer's delivery address using Google Maps API.
284
+ """
285
+ if not GOOGLE_MAPS_API_KEY:
286
+ return "ETA unavailable (Google Maps API key missing)."
287
 
288
+ origin = "Plot 13 Isashi Road, Iyana Isashi, Off Lagos - Badagry Expy, Lagos"
289
+ url = f"https://maps.googleapis.com/maps/api/directions/json?origin={origin}&destination={destination}&key={GOOGLE_MAPS_API_KEY}"
290
+
291
+ try:
292
+ response = requests.get(url, timeout=10)
293
+ if response.status_code == 200:
294
+ data = response.json()
295
+ if data.get("routes"):
296
+ duration = data["routes"][0]["legs"][0]["duration"]["text"]
297
+ return f"Estimated delivery time: {duration}"
298
+ else:
299
+ return "ETA unavailable (no route found)."
300
+ else:
301
+ return "ETA unavailable (API error)."
302
+ except Exception as e:
303
+ return f"ETA unavailable (error: {str(e)})."
304
+
305
+ # --- Contextual Order Intent Detection ---
306
+ def is_order_intent(message: str) -> bool:
307
+ """
308
+ Check if the user's message indicates an intent to place an order.
309
  """
310
+ order_keywords = ["order", "menu", "dish", "food", "deliver", "hungry"]
311
+ order_phrases = ["I want to order", "Can I order", "I'd like to order", "get food", "place an order"]
312
+
313
+ message_lower = message.lower()
314
+ for phrase in order_phrases:
315
+ if phrase in message_lower:
316
+ return True
317
+ for keyword in order_keywords:
318
+ if keyword in message_lower:
319
+ # Ensure the keyword is not part of another word (e.g., "border")
320
+ if re.search(rf"\b{keyword}\b", message_lower):
321
+ return True
322
+ return False
323
+
324
+ # --- Order Tracking Functionality ---
325
+ async def track_order(user_id: str, order_id: str) -> str:
326
  """
327
+ Track an order and return its status and ETA.
328
+ """
329
+ async with async_session() as session:
330
+ # Fetch order details
331
+ order_result = await session.execute(
332
+ select(Order).where(Order.order_id == order_id)
333
+ order = order_result.scalars().first()
334
+ if not order:
335
+ return "Order not found. Please check your order ID."
336
+
337
+ # Fetch tracking updates
338
+ tracking_result = await session.execute(
339
+ select(OrderTracking)
340
+ .where(OrderTracking.order_id == order_id)
341
+ .order_by(OrderTracking.timestamp)
342
+ )
343
+ tracking_updates = tracking_result.scalars().all()
344
+
345
+ # Calculate ETA
346
+ eta = calculate_eta(order.delivery_address)
347
+
348
+ # Prepare response
349
+ response = f"Order ID: {order_id}\nStatus: {order.status}\n"
350
+ if tracking_updates:
351
+ response += "Tracking Updates:\n"
352
+ for update in tracking_updates:
353
+ response += f"- {update.status} ({update.timestamp}): {update.message or 'No details'}\n"
354
+ response += f"\n{eta}"
355
+ return response
356
+
357
+ # --- Update User Profile with Order IDs ---
358
+ async def update_user_profile_with_order(user_id: str, order_id: str):
359
  async with async_session() as session:
360
  result = await session.execute(
361
  select(UserProfile).where(UserProfile.user_id == user_id)
362
  )
363
  profile = result.scalars().first()
364
+ if profile:
365
+ if profile.order_ids:
366
+ profile.order_ids += f",{order_id}"
367
+ else:
368
+ profile.order_ids = order_id
369
+ await session.commit()
 
 
 
 
 
 
 
 
 
 
370
 
371
  # ... (rest of your imports and setup)
372
 
 
586
  f"Extras: {extras}\nConfirm order? (yes/no)")
587
  return summary
588
 
589
+ # Inside the order confirmation step (step 7)
590
+ if state.step == 7:
591
+ if message.lower() in ["yes", "y"]:
592
+ order_id = f"ORD-{int(time.time())}"
593
+ state.data["order_id"] = order_id
594
+ price_per_serving = 1500 # Fixed price per serving
595
+ quantity = state.data.get("quantity", 1)
596
+ shipping_cost = state.data.get("shipping_cost", 0)
597
+ total_price = (quantity * price_per_serving) + shipping_cost
598
+ state.data["price"] = str(total_price)
599
+
600
+ # Calculate ETA
601
+ delivery_address = state.data.get("address", "")
602
+ eta = calculate_eta(delivery_address)
603
+
604
+ # Save the order asynchronously
605
+ async def save_order():
606
+ async with async_session() as session:
607
+ order = Order(
608
+ order_id=order_id,
609
+ user_id=user_id,
610
+ dish=state.data["dish"],
611
+ quantity=str(quantity),
612
+ price=str(total_price),
613
+ status="Pending Payment",
614
+ delivery_address=delivery_address,
615
+ shipping_cost=str(shipping_cost)
616
+ )
617
+ session.add(order)
618
+ await session.commit()
619
+ asyncio.create_task(save_order())
620
+
621
+ # Record the initial tracking update: Order Placed
622
+ await log_order_tracking(order_id, "Order Placed", "Order placed and awaiting payment.")
623
+
624
+ # Notify management via WhatsApp about the new order
625
+ async def notify_management_order(order_details: dict):
626
+ message_body = (
627
+ f"New Order Received:\n"
628
+ f"Order ID: {order_details['order_id']}\n"
629
+ f"Dish: {order_details['dish']}\n"
630
+ f"Quantity: {order_details['quantity']}\n"
631
+ f"Total Price: {order_details['price']}\n"
632
+ f"Phone: {state.data.get('phone_number', '')}\n"
633
+ f"Delivery Address: {order_details.get('address', 'Not Provided')}\n"
634
+ f"Extras: {state.data.get('extras', 'None')}\n"
635
+ f"Status: Pending Payment"
636
+ )
637
+ await asyncio.to_thread(send_whatsapp_message, MANAGEMENT_WHATSAPP_NUMBER, message_body)
638
+ order_details = {
639
+ "order_id": order_id,
640
+ "dish": state.data["dish"],
641
+ "quantity": state.data["quantity"],
642
+ "price": state.data["price"],
643
+ "address": state.data.get("address", "")
644
+ }
645
+ asyncio.create_task(notify_management_order(order_details))
646
+
647
+ # Update user profile with the new order ID
648
+ asyncio.create_task(update_user_profile_with_order(user_id, order_id))
649
+
650
+ # Generate payment link
651
+ email = "customer@example.com" # Placeholder; retrieve from profile if available
652
+ payment_data = create_paystack_payment_link(email, total_price * 100, order_id)
653
+ dish_name = state.data.get("dish", "")
654
+ state.reset()
655
+ if user_id in user_state:
656
+ del user_state[user_id]
657
+ if payment_data.get("status"):
658
+ payment_link = payment_data["data"]["authorization_url"]
659
+ return (f"Thank you for your order of {quantity} serving(s) of {dish_name}! "
660
+ f"Your Order ID is {order_id}.\nPlease complete payment here: {payment_link}\n"
661
+ f"Estimated delivery time: {eta}\n"
662
+ "You can track your order status using your Order ID.\n"
663
+ "Is there anything else you'd like to order?")
664
+ else:
665
+ return f"Your order has been placed with Order ID {order_id}, but we could not initialize payment. Please try again later."
666
+ else:
667
+ state.reset()
668
+ if user_id in user_state:
669
+ del user_state[user_id]
670
+ return "Order canceled. Let me know if you'd like to try again."
671
 
672
  return ""
673
 
 
711
  async def on_startup():
712
  await init_db()
713
 
714
+
715
  @app.post("/chatbot")
716
  async def chatbot_response(request: Request, background_tasks: BackgroundTasks):
717
  data = await request.json()
 
748
  background_tasks.add_task(log_sentiment, user_id, user_message, sentiment_score)
749
  sentiment_modifier = ""
750
  if sentiment_score < -0.3:
751
+ sentiment_modifier = "I'm sorry if you're having a tough time. "
752
  elif sentiment_score > 0.3:
753
  sentiment_modifier = "Great to hear from you! "
754
 
755
+ # --- Order Tracking Handling ---
756
+ order_id_match = re.search(r"ORD-\d+", user_message.upper())
757
  if order_id_match:
758
  order_id = order_id_match.group(0)
759
  try:
760
+ tracking_response = await track_order(user_id, order_id)
 
761
  return JSONResponse(content={"response": tracking_response})
762
  except HTTPException as e:
763
  return JSONResponse(content={"response": f"⚠️ {e.detail}"})
764
 
765
  # --- Order Flow Handling ---
766
+ if is_order_intent(user_message):
767
+ order_response = process_order_flow(user_id, user_message)
768
+ if order_response:
769
+ background_tasks.add_task(log_chat_to_db, user_id, "outbound", order_response)
770
+ conversation_context[user_id].append({
771
+ "timestamp": datetime.utcnow().isoformat(),
772
+ "role": "bot",
773
+ "message": order_response
774
+ })
775
+ return JSONResponse(content={"response": sentiment_modifier + order_response})
776
 
777
  # --- Menu Display ---
778
  if "menu" in user_message.lower():
 
902
  "name": profile.name,
903
  "email": profile.email,
904
  "preferences": profile.preferences,
905
+ "last_interaction": profile.last_interaction.isoformat(),
906
+ "order_ids": profile.order_ids
907
  }
908
 
909
  @app.get("/analytics")
 
972
 
973
  # --- Payment Callback Endpoint with Payment Tracking and Redirection ---
974
  @app.api_route("/payment_callback", methods=["GET", "POST"])
 
975
  async def payment_callback(request: Request):
976
  # GET: User redirection after payment
977
  if request.method == "GET":
 
1021
  return JSONResponse(content={"message": "Order updated successfully."})
1022
  else:
1023
  raise HTTPException(status_code=404, detail="Order not found.")
1024
+
1025
  @app.get("/track_order/{order_id}")
1026
  async def track_order(order_id: str):
1027
  """
 
1048
 
1049
  if __name__ == "__main__":
1050
  import uvicorn
1051
+ uvicorn.run(app, host="0.0.0.0", port=8000)