Fred808 commited on
Commit
c27d999
·
verified ·
1 Parent(s): 52592c8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +152 -100
app.py CHANGED
@@ -384,53 +384,19 @@ def match_dishes(user_input: str, threshold: int = 70) -> list:
384
  matched_dishes.append(dish_name)
385
  return list(set(matched_dishes))
386
 
 
387
  async def process_order_flow(user_id: str, message: str) -> str:
 
 
 
 
388
  state = user_state.get(user_id)
389
  if state and state.is_expired():
390
  state.reset()
391
  del user_state[user_id]
392
  state = None
393
 
394
- # --- Handle quantity details if we're at step 2 ---
395
- if state and state.flow == "order" and state.step == 2:
396
- # If the user selected multiple dishes ("both"), we expect "2 for Jollof Rice and 3 for Fried Rice".
397
- if "dishes" in state.data and len(state.data["dishes"]) > 1:
398
- # Parse pairs like "(\d+) for ([a-zA-Z\s]+)"
399
- pairs = re.findall(r'(\d+)\s*for\s*([a-zA-Z\s]+)', message, flags=re.IGNORECASE)
400
- if not pairs:
401
- return ("I'm sorry, I didn't understand the quantity details. "
402
- "Please specify like '2 for Jollof Rice and 3 for Fried Rice'.")
403
- order_quantities = {}
404
- for quantity, dish_text in pairs:
405
- dish_text = dish_text.strip().lower()
406
- for candidate in state.data["dishes"]:
407
- if candidate.lower() in dish_text or dish_text in candidate.lower():
408
- order_quantities[candidate] = int(quantity)
409
- if order_quantities:
410
- state.data["orders"] = order_quantities
411
- state.step = 3
412
- summary = "\n".join([f"{q} serving(s) of {d}" for d, q in order_quantities.items()])
413
- return (f"Got it. You have ordered:\n{summary}\n"
414
- "Please provide your phone number and delivery address.")
415
- else:
416
- return ("I'm sorry, I couldn't match those dishes. "
417
- "Please try something like '2 for Jollof Rice and 3 for Fried Rice'.")
418
- else:
419
- # Single-dish scenario. For example: "2 portions"
420
- # Just look for a single number in the user message.
421
- numbers = re.findall(r'\d+', message)
422
- if not numbers:
423
- return "Please enter a valid number for the quantity (e.g., 1, 2, 3)."
424
- quantity = int(numbers[0])
425
- if quantity <= 0:
426
- return "Please enter a valid quantity (e.g., 1, 2, 3)."
427
- state.data["quantity"] = quantity
428
- state.step = 3
429
- dish = state.data.get("dish", "")
430
- return (f"Got it. {quantity} serving(s) of {dish}. "
431
- "Please provide your phone number and delivery address.")
432
-
433
- # --- Trigger initial order flow when user types "order" or "menu" ---
434
  if message.lower() in ["order", "menu"]:
435
  state = ConversationState()
436
  state.flow = "order"
@@ -441,6 +407,7 @@ async def process_order_flow(user_id: str, message: str) -> str:
441
  return "Sure! What dish would you like to order?"
442
  return ""
443
 
 
444
  if not state and "order" in message.lower():
445
  state = ConversationState()
446
  state.flow = "order"
@@ -449,103 +416,157 @@ async def process_order_flow(user_id: str, message: str) -> str:
449
  user_state[user_id] = state
450
  return "Sure! What dish would you like to order?"
451
 
452
- # --- Identify dish(es) if not already in order flow ---
453
  if not state or state.flow != "order":
454
- matched_dishes = match_dishes(message)
455
- if matched_dishes:
456
- if len(matched_dishes) > 1:
457
- # Multiple dishes found
458
  state = ConversationState()
459
  state.flow = "order"
460
  state.update_last_active()
461
- state.data["candidate_dishes"] = matched_dishes
462
  user_state[user_id] = state
463
- dish_options = ", ".join(matched_dishes)
 
464
  return (f"We found multiple dishes in your request: {dish_options}. "
465
  "Please specify which one you'd like to order or type 'both' if you'd like all.")
466
  else:
467
- # Single dish found
468
- found_dish = matched_dishes[0]
469
  state = ConversationState()
470
  state.flow = "order"
471
- state.data["dish"] = found_dish
472
  state.update_last_active()
473
  user_state[user_id] = state
474
- # Attempt to extract a quantity if present
475
- numbers = re.findall(r'\d+', message)
476
- if numbers:
477
- quantity = int(numbers[0])
478
- if quantity <= 0:
479
- return "Please enter a valid quantity (e.g., 1, 2, 3)."
480
- state.data["quantity"] = quantity
481
  state.step = 3
482
- return (f"You selected {found_dish} with {quantity} serving(s). "
483
- "Please provide your phone number and delivery address.")
484
  else:
 
485
  state.step = 2
 
 
 
 
 
 
 
 
 
486
  return f"You selected {found_dish}. How many servings would you like?"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
487
  else:
 
488
  return "I couldn't identify the dish. Please type the dish name from our menu."
489
 
490
- # --- Clarification for multiple dishes ---
491
  if state and state.flow == "order" and "candidate_dishes" in state.data:
492
- # If user typed "both", they want all candidate dishes
493
  if message.strip().lower() in ["both", "all"]:
494
  state.data["dishes"] = state.data["candidate_dishes"]
495
  del state.data["candidate_dishes"]
496
- state.step = 2
497
  dishes_str = ", ".join(state.data["dishes"])
498
  return (f"You have selected: {dishes_str}. How many servings of each would you like? "
499
  "(For example, '2 for Jollof Rice and 3 for Fried Rice')")
500
  else:
501
- # Check if user specified a single dish out of the candidates
502
  for dish in state.data["candidate_dishes"]:
503
  if dish.lower() in message.lower():
504
  state.data["dish"] = dish
505
  del state.data["candidate_dishes"]
506
  state.step = 2
507
  return f"You selected {dish}. How many servings would you like?"
 
508
  dish_options = ", ".join(state.data["candidate_dishes"])
509
  return (f"Please specify which one you'd like to order from: {dish_options} "
510
  "(or type 'both' if you'd like to order all).")
511
-
512
- # --- Step 5: Contact details extraction (for single-dish orders) ---
513
- if state.step == 3:
514
- phone_pattern = r'(\+?\d{10,15})'
515
- phone_match = re.search(phone_pattern, message)
516
- address = None
517
- if phone_match:
518
- phone_number = phone_match.group(1)
519
- address_start = phone_match.end()
520
- address = message[address_start:].strip()
521
- address = re.sub(r'^[,\s]+|[,\s]+$', '', address)
522
- if phone_match and address:
523
- state.data["phone_number"] = phone_number
524
- state.data["address"] = address
525
- asyncio.create_task(update_user_profile(user_id, phone_number, address))
526
- shipping_cost = calculate_shipping_cost(address)
527
- state.data["shipping_cost"] = shipping_cost
528
- state.step = 5
529
- return (f"Thanks! Your phone number is recorded as: {phone_number}.\n"
530
- f"Your delivery address is: {address}.\n"
531
- f"Your delivery cost is N{shipping_cost}. Would you like extras (yes/no)?")
532
- elif phone_match:
533
- state.data["phone_number"] = phone_match.group(1)
534
- asyncio.create_task(update_user_profile(user_id, phone_match.group(1)))
535
- return "Thank you. Please provide your delivery address."
536
  else:
537
- return ("Please provide both your phone number and delivery address. "
538
- "For example: '09162409591, 1, Iyana Isashi, Isashi, Ojo, Lagos'.")
539
-
540
- # Steps 4, 5, 6, and 7 remain unchanged.
541
- if state.step == 4:
542
- state.data["address"] = message
543
- asyncio.create_task(update_user_profile(user_id, address=message))
544
- shipping_cost = calculate_shipping_cost(message)
545
- state.data["shipping_cost"] = shipping_cost
546
- state.step = 5
547
- return (f"Thanks. Your delivery address is recorded as: {message}.\n"
548
- f"Your delivery cost is N{shipping_cost}. Would you like extras (yes/no)?")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
549
 
550
  if state.step == 5:
551
  if message.lower() in ["yes", "y"]:
@@ -662,6 +683,37 @@ async def process_order_flow(user_id: str, message: str) -> str:
662
 
663
  return ""
664
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
665
 
666
  async def get_or_create_user_profile(user_id: str, phone_number: str = None) -> UserProfile:
667
  async with async_session() as session:
 
384
  matched_dishes.append(dish_name)
385
  return list(set(matched_dishes))
386
 
387
+
388
  async def process_order_flow(user_id: str, message: str) -> str:
389
+ """
390
+ A unified approach to parse dish(es), quantity, phone, and address
391
+ whether the user provides them all at once or in multiple steps.
392
+ """
393
  state = user_state.get(user_id)
394
  if state and state.is_expired():
395
  state.reset()
396
  del user_state[user_id]
397
  state = None
398
 
399
+ # 1) If user says "menu" or "order", initialize the flow
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
  if message.lower() in ["order", "menu"]:
401
  state = ConversationState()
402
  state.flow = "order"
 
407
  return "Sure! What dish would you like to order?"
408
  return ""
409
 
410
+ # If user says "order" but we have no state
411
  if not state and "order" in message.lower():
412
  state = ConversationState()
413
  state.flow = "order"
 
416
  user_state[user_id] = state
417
  return "Sure! What dish would you like to order?"
418
 
419
+ # 2) If we aren't in the order flow yet, try to detect dish(es)
420
  if not state or state.flow != "order":
421
+ matched = match_dishes(message)
422
+ if matched:
423
+ # Multiple candidate dishes
424
+ if len(matched) > 1:
425
  state = ConversationState()
426
  state.flow = "order"
427
  state.update_last_active()
 
428
  user_state[user_id] = state
429
+ state.data["candidate_dishes"] = matched
430
+ dish_options = ", ".join(matched)
431
  return (f"We found multiple dishes in your request: {dish_options}. "
432
  "Please specify which one you'd like to order or type 'both' if you'd like all.")
433
  else:
434
+ # Single dish
435
+ found_dish = matched[0]
436
  state = ConversationState()
437
  state.flow = "order"
 
438
  state.update_last_active()
439
  user_state[user_id] = state
440
+ state.data["dish"] = found_dish
441
+ # Attempt to parse quantity, phone, address if user typed them all
442
+ single_dish_parse = _parse_single_dish_line(message, found_dish)
443
+ if single_dish_parse["quantity"]:
444
+ state.data["quantity"] = single_dish_parse["quantity"]
 
 
445
  state.step = 3
 
 
446
  else:
447
+ # If no quantity found, we still need quantity
448
  state.step = 2
449
+
450
+ if single_dish_parse["phone"]:
451
+ state.data["phone_number"] = single_dish_parse["phone"]
452
+ if single_dish_parse["address"]:
453
+ state.data["address"] = single_dish_parse["address"]
454
+
455
+ # Now see how much info we have:
456
+ if state.step == 2 and not state.data.get("quantity"):
457
+ # We must ask for quantity
458
  return f"You selected {found_dish}. How many servings would you like?"
459
+ elif state.step == 3:
460
+ # We have a quantity. Check if we also have phone and address
461
+ if state.data.get("phone_number") and state.data.get("address"):
462
+ # We have everything needed to skip to step 5
463
+ shipping_cost = calculate_shipping_cost(state.data["address"])
464
+ state.data["shipping_cost"] = shipping_cost
465
+ state.step = 5
466
+ return (f"Thanks! Your phone number is recorded as: {state.data['phone_number']}.\n"
467
+ f"Your delivery address is: {state.data['address']}.\n"
468
+ f"Your delivery cost is N{shipping_cost}. Would you like extras (yes/no)?")
469
+ elif state.data.get("phone_number") and not state.data.get("address"):
470
+ return "Thank you. Please provide your delivery address."
471
+ else:
472
+ return "Please provide your phone number and address."
473
  else:
474
+ # No dish matched
475
  return "I couldn't identify the dish. Please type the dish name from our menu."
476
 
477
+ # 3) If we have candidate dishes and user needs to clarify
478
  if state and state.flow == "order" and "candidate_dishes" in state.data:
479
+ # If user typed "both" => they want all candidate dishes
480
  if message.strip().lower() in ["both", "all"]:
481
  state.data["dishes"] = state.data["candidate_dishes"]
482
  del state.data["candidate_dishes"]
483
+ state.step = 2 # We'll parse "2 for Jollof Rice and 3 for Fried Rice"
484
  dishes_str = ", ".join(state.data["dishes"])
485
  return (f"You have selected: {dishes_str}. How many servings of each would you like? "
486
  "(For example, '2 for Jollof Rice and 3 for Fried Rice')")
487
  else:
488
+ # Maybe the user typed a single dish out of the candidates
489
  for dish in state.data["candidate_dishes"]:
490
  if dish.lower() in message.lower():
491
  state.data["dish"] = dish
492
  del state.data["candidate_dishes"]
493
  state.step = 2
494
  return f"You selected {dish}. How many servings would you like?"
495
+ # If still unclear
496
  dish_options = ", ".join(state.data["candidate_dishes"])
497
  return (f"Please specify which one you'd like to order from: {dish_options} "
498
  "(or type 'both' if you'd like to order all).")
499
+
500
+ # 4) If step == 2 for multiple dishes => parse "2 for Jollof Rice and 3 for Fried Rice"
501
+ if state and state.flow == "order" and state.step == 2:
502
+ if "dishes" in state.data and len(state.data["dishes"]) > 1:
503
+ pairs = re.findall(r'(\d+)\s*for\s*([a-zA-Z\s]+)', message, flags=re.IGNORECASE)
504
+ if not pairs:
505
+ return ("I'm sorry, I didn't understand the quantity details. "
506
+ "Please specify like '2 for Jollof Rice and 3 for Fried Rice'.")
507
+ order_quantities = {}
508
+ for quantity, dish_text in pairs:
509
+ dish_text = dish_text.strip().lower()
510
+ for candidate in state.data["dishes"]:
511
+ if candidate.lower() in dish_text or dish_text in candidate.lower():
512
+ order_quantities[candidate] = int(quantity)
513
+ if order_quantities:
514
+ state.data["orders"] = order_quantities
515
+ state.step = 3
516
+ summary = "\n".join([f"{q} serving(s) of {d}" for d, q in order_quantities.items()])
517
+ return (f"Got it. You have ordered:\n{summary}\n"
518
+ "Please provide your phone number and delivery address.")
 
 
 
 
 
519
  else:
520
+ return ("I'm sorry, I couldn't match those dishes. "
521
+ "Please try something like '2 for Jollof Rice and 3 for Fried Rice'.")
522
+
523
+ # Single-dish scenario at step 2 => user typed just a quantity
524
+ numbers = re.findall(r'\d+', message)
525
+ if not numbers:
526
+ return "Please enter a valid number for the quantity (e.g., 1, 2, 3)."
527
+ quantity = int(numbers[0])
528
+ if quantity <= 0:
529
+ return "Please enter a valid quantity (e.g., 1, 2, 3)."
530
+ state.data["quantity"] = quantity
531
+ state.step = 3
532
+ dish = state.data.get("dish", "")
533
+ return (f"Got it. {quantity} serving(s) of {dish}. "
534
+ "Please provide your phone number and delivery address.")
535
+
536
+ # 5) Step == 3 => parse phone & address for single or multiple dishes
537
+ if state and state.flow == "order" and state.step == 3:
538
+ phone_pattern = r'(\+?\d{10,15})'
539
+ phone_match = re.search(phone_pattern, message)
540
+ address = None
541
+ if phone_match:
542
+ phone_number = phone_match.group(1)
543
+ address_start = phone_match.end()
544
+ address = message[address_start:].strip()
545
+ address = re.sub(r'^[,\s]+|[,\s]+$', '', address)
546
+ state.data["phone_number"] = phone_number
547
+ state.data["address"] = address
548
+ asyncio.create_task(update_user_profile(user_id, phone_number, address))
549
+ else:
550
+ # Maybe the user typed an address only, or partial info
551
+ # If the phone was previously stored, we just store the address
552
+ if "phone_number" in state.data:
553
+ state.data["address"] = message.strip()
554
+ else:
555
+ # If no phone number found yet, we need it
556
+ return ("Please provide both your phone number and address. "
557
+ "For example: '09162409591, 1, Iyana Isashi, Isashi, Lagos'.")
558
+
559
+ if "address" not in state.data:
560
+ # If we only found phone but no address, ask for address
561
+ return "Thank you. Please provide your delivery address."
562
+
563
+ # If we have phone + address, move to step 5 => ask extras
564
+ shipping_cost = calculate_shipping_cost(state.data["address"])
565
+ state.data["shipping_cost"] = shipping_cost
566
+ state.step = 5
567
+ return (f"Thanks! Your phone number is recorded as: {state.data['phone_number']}.\n"
568
+ f"Your delivery address is: {state.data['address']}.\n"
569
+ f"Your delivery cost is N{shipping_cost}. Would you like extras (yes/no)?")
570
 
571
  if state.step == 5:
572
  if message.lower() in ["yes", "y"]:
 
683
 
684
  return ""
685
 
686
+
687
+
688
+ def _parse_single_dish_line(message: str, dish_name: str) -> dict:
689
+ """
690
+ Utility: if user typed everything at once for a single dish,
691
+ parse quantity, phone, address from the same line.
692
+ E.g.: "Jollof Rice, 2, 09162409591, Iyana Isashi, Isashi, Lagos"
693
+ """
694
+ result = {"quantity": None, "phone": None, "address": None}
695
+
696
+ # 1) Parse quantity
697
+ numbers = re.findall(r'\d+', message)
698
+ if numbers:
699
+ # We'll assume the first numeric is quantity if no "for" pattern is used
700
+ result["quantity"] = int(numbers[0])
701
+
702
+ # 2) Parse phone
703
+ phone_pattern = r'(\+?\d{10,15})'
704
+ phone_match = re.search(phone_pattern, message)
705
+ address = None
706
+ if phone_match:
707
+ phone_number = phone_match.group(1)
708
+ address_start = phone_match.end()
709
+ address = message[address_start:].strip()
710
+ address = re.sub(r'^[,\s]+|[,\s]+$', '', address)
711
+ result["phone"] = phone_number
712
+ if address:
713
+ result["address"] = address
714
+
715
+ return result
716
+
717
 
718
  async def get_or_create_user_profile(user_id: str, phone_number: str = None) -> UserProfile:
719
  async with async_session() as session: