Fred808 commited on
Commit
9e42313
·
verified ·
1 Parent(s): 2d25b24

Update app.py

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