Fred808 commited on
Commit
6517c39
·
verified ·
1 Parent(s): 8ae843e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +172 -178
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)
@@ -211,48 +212,51 @@ def send_whatsapp_message(recipient: str, message_body: str) -> dict:
211
      response = requests.post(url, headers=headers, json=payload)
212
      return response.json()
213
 
214
- # --- NVIDIA LLM Streaming Functions ---
215
  def stream_text_completion(prompt: str):
216
      from openai import OpenAI
217
      client = OpenAI(
218
          base_url="https://integrate.api.nvidia.com/v1",
219
          api_key=NVIDIA_API_KEY
220
      )
221
-     completion = client.chat.completions.create(
222
-         model="meta/llama-3.1-405b-instruct",
223
-         messages=[{"role": "user", "content": prompt}],
224
-         temperature=0.2,
225
-         top_p=0.7,
226
-         max_tokens=1024,
227
-         stream=True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
      )
229
-     for chunk in completion:
230
-         if chunk.choices[0].delta.content is not None:
231
-             yield chunk.choices[0].delta.content
232
 
233
- def stream_image_completion(image_b64: str):
234
-     invoke_url = "https://ai.api.nvidia.com/v1/gr/meta/llama-3.2-90b-vision-instruct/chat/completions"
235
-     headers = {
236
-         "Authorization": f"Bearer {NVIDIA_API_KEY}",
237
-         "Accept": "text/event-stream"
238
-     }
239
-     payload = {
240
-         "model": "meta/llama-3.2-90b-vision-instruct",
241
-         "messages": [
242
-             {
243
-                 "role": "user",
244
-                 "content": f'What is in this image? <img src="data:image/png;base64,{image_b64}" />'
245
-             }
246
-         ],
247
-         "max_tokens": 512,
248
-         "temperature": 1.00,
249
-         "top_p": 1.00,
250
-         "stream": True
251
-     }
252
-     response = requests.post(invoke_url, headers=headers, json=payload, stream=True)
253
-     for line in response.iter_lines():
254
-         if line:
255
-             yield line.decode("utf-8") + "\n"
256
 
257
  # --- Helper Function for Order Tracking ---
258
  async def log_order_tracking(order_id: str, status: str, message: str = None):
@@ -276,52 +280,111 @@ 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
 
309
- def process_order_flow(user_id: str, message: str) -> str:
310
      """
311
      Implements an FSM-based order flow with shipping cost calculation.
312
      """
313
-            
314
      # Retrieve or initialize conversation state
315
      state = user_state.get(user_id)
316
      if state and state.is_expired():
317
          state.reset()
318
-         del user_state[user_id]
319
-         state = None
320
-
321
-     # Start a new order flow if the user explicitly types "order" or "menu"
322
-     if message.lower() in ["order", "menu"]:
323
-         state = ConversationState()
324
-         state.flow = "order"
325
          state.step = 1
326
          state.update_last_active()
327
          user_state[user_id] = state
@@ -377,7 +440,7 @@ def process_order_flow(user_id: str, message: str) -> str:
377
                  state.data["phone_number"] = phone_number
378
                  state.data["address"] = address
379
                  # Save phone number and address to the user's profile
380
-                 asyncio.create_task(update_user_profile(user_id, phone_number, address))
381
                  # Calculate shipping cost based on the address
382
                  shipping_cost = calculate_shipping_cost(address)
383
                  state.data["shipping_cost"] = shipping_cost
@@ -388,7 +451,7 @@ def process_order_flow(user_id: str, message: str) -> str:
388
              elif phone_match:
389
                  state.data["phone_number"] = phone_match.group(1)
390
                  # Save phone number to the user's profile
391
-                 asyncio.create_task(update_user_profile(user_id, phone_number))
392
                  return "Thank you. Please provide your delivery address."
393
              else:
394
                  return "Please provide both your phone number and delivery address. For example: '09162409591, 1, Iyana Isashi, Isashi, Ojo, Lagos'."
@@ -451,7 +514,7 @@ def process_order_flow(user_id: str, message: str) -> str:
451
                  state.data["phone_number"] = phone_number
452
                  state.data["address"] = address
453
                  # Save phone number and address to the user's profile
454
-                 asyncio.create_task(update_user_profile(user_id, phone_number, address))
455
                  # Calculate shipping cost based on the address
456
                  shipping_cost = calculate_shipping_cost(address)
457
                  state.data["shipping_cost"] = shipping_cost
@@ -462,7 +525,7 @@ def process_order_flow(user_id: str, message: str) -> str:
462
              elif phone_match:
463
                  state.data["phone_number"] = phone_match.group(1)
464
                  # Save phone number to the user's profile
465
-                 asyncio.create_task(update_user_profile(user_id, phone_number))
466
                  return "Thank you. Please provide your delivery address."
467
              else:
468
                  return "Please provide both your phone number and delivery address. For example: '09162409591, 1, Iyana Isashi, Isashi, Ojo, Lagos'."
@@ -471,7 +534,7 @@ def process_order_flow(user_id: str, message: str) -> str:
471
          if state.step == 4:
472
              state.data["address"] = message
473
              # Save address to the user's profile
474
-             asyncio.create_task(update_user_profile(user_id, address=message))
475
              # Calculate shipping cost based on the address
476
              shipping_cost = calculate_shipping_cost(message)
477
              state.data["shipping_cost"] = shipping_cost
@@ -551,7 +614,7 @@ def process_order_flow(user_id: str, message: str) -> str:
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):
@@ -576,6 +639,10 @@ def process_order_flow(user_id: str, message: str) -> str:
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", "")
@@ -638,72 +705,36 @@ 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()
644
      user_id = data.get("user_id")
645
-     phone_number = data.get("phone_number")
646
      user_message = data.get("message", "").strip()
647
-     is_image = data.get("is_image", False)
648
-     image_b64 = data.get("image_base64", None)
649
 
650
-     if not user_id:
651
-         raise HTTPException(status_code=400, detail="Missing user_id in payload.")
652
-
653
-     # Initialize conversation context for the user if not present.
654
      if user_id not in conversation_context:
655
          conversation_context[user_id] = []
656
-     # Append the inbound message to the conversation context.
 
657
      conversation_context[user_id].append({
658
          "timestamp": datetime.utcnow().isoformat(),
659
          "role": "user",
660
          "message": user_message
661
      })
662
 
 
663
      background_tasks.add_task(log_chat_to_db, user_id, "inbound", user_message)
664
-     await update_user_last_interaction(user_id)
665
-     await get_or_create_user_profile(user_id, phone_number)
666
-
667
-     # Handle image queries
668
-     if is_image and image_b64:
669
-         if len(image_b64) >= 180_000:
670
-             raise HTTPException(status_code=400, detail="Image too large.")
671
-         return StreamingResponse(stream_image_completion(image_b64), media_type="text/plain")
672
 
 
673
      sentiment_score = analyze_sentiment(user_message)
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():
705
          if user_id in user_state:
706
-             del user_state[user_id]
707
          menu_with_images = []
708
          for index, item in enumerate(menu_items, start=1):
709
              image_url = google_image_scrape(item["name"])
@@ -731,71 +762,34 @@ async def chatbot_response(request: Request, background_tasks: BackgroundTasks):
731
          })
732
          return JSONResponse(content=response_payload)
733
 
734
-     # --- Dish Selection via Menu ---
735
-     if any(item["name"].lower() in user_message.lower() for item in menu_items) or \
736
-        any(str(index) == user_message.strip() for index, item in enumerate(menu_items, start=1)):
737
-         selected_dish = None
738
-         if user_message.strip().isdigit():
739
-             dish_number = int(user_message.strip())
740
-             if 1 <= dish_number <= len(menu_items):
741
-                 selected_dish = menu_items[dish_number - 1]["name"]
742
-         else:
743
-             for item in menu_items:
744
-                 if item["name"].lower() in user_message.lower():
745
-                     selected_dish = item["name"]
746
-                     break
747
-         if selected_dish:
748
-             state = ConversationState()
749
-             state.flow = "order"
750
-             # Set step to 2 since the dish is already selected
751
-             state.step = 2  
752
-             state.data["dish"] = selected_dish
753
-             state.update_last_active()
754
-             user_state[user_id] = state
755
-             response_text = f"You selected {selected_dish}. How many servings would you like?"
756
-             background_tasks.add_task(log_chat_to_db, user_id, "outbound", response_text)
757
-             conversation_context[user_id].append({
758
-                 "timestamp": datetime.utcnow().isoformat(),
759
-                 "role": "bot",
760
-                 "message": response_text
761
-             })
762
-             return JSONResponse(content={"response": sentiment_modifier + response_text})
763
-         else:
764
-             response_text = "Sorry, I couldn't find that dish in the menu. Please try again."
765
-             background_tasks.add_task(log_chat_to_db, user_id, "outbound", response_text)
766
              conversation_context[user_id].append({
767
                  "timestamp": datetime.utcnow().isoformat(),
768
                  "role": "bot",
769
-                 "message": response_text
770
              })
771
-             return JSONResponse(content={"response": sentiment_modifier + response_text})
772
-
773
-     # --- Nutritional Facts ---
774
-     if "nutritional facts for" in user_message.lower():
775
-         dish_name = user_message.lower().replace("nutritional facts for", "").strip().title()
776
-         dish = next((item for item in menu_items if item["name"].lower() == dish_name.lower()), None)
777
-         if dish:
778
-             response_text = f"Nutritional facts for {dish['name']}:\n{dish['nutrition']}"
779
-         else:
780
-             response_text = f"Sorry, I couldn't find nutritional facts for {dish_name}."
781
-         background_tasks.add_task(log_chat_to_db, user_id, "outbound", response_text)
782
-         conversation_context[user_id].append({
783
-             "timestamp": datetime.utcnow().isoformat(),
784
-             "role": "bot",
785
-             "message": response_text
786
-         })
787
-         return JSONResponse(content={"response": sentiment_modifier + response_text})
788
 
789
-     # --- Fallback: LLM Response Streaming with Conversation Context ---
790
-     recent_context = conversation_context.get(user_id, [])[-5:]
791
      context_str = "\n".join([f"{entry['role'].capitalize()}: {entry['message']}" for entry in recent_context])
792
      prompt = f"Conversation context:\n{context_str}\nUser query: {user_message}\nGenerate a helpful, personalized response for a restaurant chatbot."
793
-     def stream_response():
794
-         for chunk in stream_text_completion(prompt):
795
-             yield chunk
796
-     fallback_log = f"LLM fallback response for prompt: {prompt}"
797
-     background_tasks.add_task(log_chat_to_db, user_id, "outbound", fallback_log)
798
-     return StreamingResponse(stream_response(), media_type="text/plain")
 
 
 
 
 
 
799
 
800
  # --- Other Endpoints (Chat History, Order Details, User Profile, Analytics, Voice, Payment Callback) ---
801
  @app.get("/chat_history/{user_id}")
@@ -828,7 +822,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 +892,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 +941,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
      """
 
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)
 
212
      response = requests.post(url, headers=headers, json=payload)
213
      return response.json()
214
 
 
215
  def stream_text_completion(prompt: str):
216
      from openai import OpenAI
217
      client = OpenAI(
218
          base_url="https://integrate.api.nvidia.com/v1",
219
          api_key=NVIDIA_API_KEY
220
      )
221
+     print(f"Using NVIDIA API Key: {NVIDIA_API_KEY}")  # Debugging
222
+
223
+     try:
224
+         completion = client.chat.completions.create(
225
+             model="meta/llama-3.1-405b-instruct",
226
+             messages=[{"role": "user", "content": prompt}],
227
+             temperature=0.2,
228
+             top_p=0.7,
229
+             max_tokens=1024,
230
+             stream=True
231
+         )
232
+         for chunk in completion:
233
+             if chunk.choices[0].delta.content is not None:
234
+                 yield chunk.choices[0].delta.content
235
+     except Exception as e:
236
+         yield f"Error: {str(e)}"  # Handle errors gracefully
237
+
238
+ def stream_text_completion(prompt: str):
239
+     from openai import OpenAI
240
+     client = OpenAI(
241
+         base_url="https://integrate.api.nvidia.com/v1",
242
+         api_key=NVIDIA_API_KEY
243
      )
244
+     print(f"Using NVIDIA API Key: {NVIDIA_API_KEY}")  # Debugging
 
 
245
 
246
+     try:
247
+         completion = client.chat.completions.create(
248
+             model="meta/llama-3.1-405b-instruct",
249
+             messages=[{"role": "user", "content": prompt}],
250
+             temperature=0.2,
251
+             top_p=0.7,
252
+             max_tokens=1024,
253
+             stream=True
254
+         )
255
+         for chunk in completion:
256
+             if chunk.choices[0].delta.content is not None:
257
+                 yield chunk.choices[0].delta.content
258
+     except Exception as e:
259
+         yield f"Error: {str(e)}"  # Handle errors gracefully
 
 
 
 
 
 
 
 
 
260
 
261
  # --- Helper Function for Order Tracking ---
262
  async def log_order_tracking(order_id: str, status: str, message: str = None):
 
280
              return cost
281
      return TOWN_SHIPPING_COSTS["default"]  # Default shipping cost for unknown areas
282
 
283
+ # --- Google Maps ETA Calculation ---
284
+ def calculate_eta(destination: str) -> str:
285
+     """
286
+     Calculate ETA from the restaurant address to the customer's delivery address using Google Maps API.
287
+     """
288
+     if not GOOGLE_MAPS_API_KEY:
289
+         return "ETA unavailable (Google Maps API key missing)."
290
 
291
+     origin = "Plot 13 Isashi Road, Iyana Isashi, Off Lagos - Badagry Expy, Lagos"
292
+     url = f"https://maps.googleapis.com/maps/api/directions/json?origin={origin}&destination={destination}&key={GOOGLE_MAPS_API_KEY}"
293
+    
294
+     try:
295
+         response = requests.get(url, timeout=10)
296
+         if response.status_code == 200:
297
+             data = response.json()
298
+             if data.get("routes"):
299
+                 duration = data["routes"][0]["legs"][0]["duration"]["text"]
300
+                 return f"Estimated delivery time: {duration}"
301
+             else:
302
+                 return "ETA unavailable (no route found)."
303
+         else:
304
+             return "ETA unavailable (API error)."
305
+     except Exception as e:
306
+         return f"ETA unavailable (error: {str(e)})."
307
+
308
+ # --- Contextual Order Intent Detection ---
309
+ def is_order_intent(message: str) -> bool:
310
+     """
311
+     Check if the user's message indicates an intent to place an order.
312
+     """
313
+     order_keywords = ["order", "menu", "dish", "food", "deliver", "hungry"]
314
+     order_phrases = ["I want to order", "Can I order", "I'd like to order", "get food", "place an order"]
315
+
316
+     message_lower = message.lower()
317
+     for phrase in order_phrases:
318
+         if phrase in message_lower:
319
+             return True
320
+     for keyword in order_keywords:
321
+         if keyword in message_lower:
322
+             # Ensure the keyword is not part of another word (e.g., "border")
323
+             if re.search(rf"\b{keyword}\b", message_lower):
324
+                 return True
325
+     return False
326
+
327
+    
328
+
329
+ # ... (previous code remains unchanged)
330
+
331
+ async def track_order(user_id: str, order_id: str) -> str:
332
      """
333
+     Track an order and return its status and ETA.
334
      """
335
      async with async_session() as session:
336
+         # Fetch order details
337
+         order_result = await session.execute(
338
+             select(Order).where(Order.order_id == order_id)
339
+         )
340
+         order = order_result.scalars().first()
341
+         if not order:
342
+             return "Order not found. Please check your order ID."
343
+
344
+         # Fetch tracking updates
345
+         tracking_result = await session.execute(
346
+             select(OrderTracking)
347
+             .where(OrderTracking.order_id == order_id)
348
+             .order_by(OrderTracking.timestamp)
349
+         )
350
+         tracking_updates = tracking_result.scalars().all()
351
+
352
+         # Calculate ETA
353
+         eta = calculate_eta(order.delivery_address)
354
+
355
+         # Prepare response
356
+         response = f"Order ID: {order_id}\nStatus: {order.status}\n"
357
+         if tracking_updates:
358
+             response += "Tracking Updates:\n"
359
+             for update in tracking_updates:
360
+                 response += f"- {update.status} ({update.timestamp}): {update.message or 'No details'}\n"
361
+         response += f"\n{eta}"
362
+         return response
363
+
364
+ # ... (rest of the code remains unchanged)
365
+
366
+ # --- Update User Profile with Order IDs ---
367
+ async def update_user_profile_with_order(user_id: str, order_id: str):
368
+     async with async_session() as session:
369
          result = await session.execute(
370
              select(UserProfile).where(UserProfile.user_id == user_id)
371
          )
372
          profile = result.scalars().first()
373
+         if profile:
374
+             if profile.order_ids:
375
+                 profile.order_ids += f",{order_id}"
376
+             else:
377
+                 profile.order_ids = order_id
378
+             await session.commit()
 
 
 
 
 
 
 
 
 
 
 
 
379
 
380
+ async def process_order_flow(user_id: str, message: str) -> str:
381
      """
382
      Implements an FSM-based order flow with shipping cost calculation.
383
      """
 
384
      # Retrieve or initialize conversation state
385
      state = user_state.get(user_id)
386
      if state and state.is_expired():
387
          state.reset()
 
 
 
 
 
 
 
388
          state.step = 1
389
          state.update_last_active()
390
          user_state[user_id] = state
 
440
                  state.data["phone_number"] = phone_number
441
                  state.data["address"] = address
442
                  # Save phone number and address to the user's profile
443
+                 await update_user_profile(user_id, phone_number, address)
444
                  # Calculate shipping cost based on the address
445
                  shipping_cost = calculate_shipping_cost(address)
446
                  state.data["shipping_cost"] = shipping_cost
 
451
              elif phone_match:
452
                  state.data["phone_number"] = phone_match.group(1)
453
                  # Save phone number to the user's profile
454
+                 await update_user_profile(user_id, phone_number)
455
                  return "Thank you. Please provide your delivery address."
456
              else:
457
                  return "Please provide both your phone number and delivery address. For example: '09162409591, 1, Iyana Isashi, Isashi, Ojo, Lagos'."
 
514
                  state.data["phone_number"] = phone_number
515
                  state.data["address"] = address
516
                  # Save phone number and address to the user's profile
517
+                 await update_user_profile(user_id, phone_number, address)
518
                  # Calculate shipping cost based on the address
519
                  shipping_cost = calculate_shipping_cost(address)
520
                  state.data["shipping_cost"] = shipping_cost
 
525
              elif phone_match:
526
                  state.data["phone_number"] = phone_match.group(1)
527
                  # Save phone number to the user's profile
528
+                 await update_user_profile(user_id, phone_number)
529
                  return "Thank you. Please provide your delivery address."
530
              else:
531
                  return "Please provide both your phone number and delivery address. For example: '09162409591, 1, Iyana Isashi, Isashi, Ojo, Lagos'."
 
534
          if state.step == 4:
535
              state.data["address"] = message
536
              # Save address to the user's profile
537
+             await update_user_profile(user_id, address=message)
538
              # Calculate shipping cost based on the address
539
              shipping_cost = calculate_shipping_cost(message)
540
              state.data["shipping_cost"] = shipping_cost
 
614
                  asyncio.create_task(save_order())
615
 
616
                  # Record the initial tracking update: Order Placed
617
+                 await log_order_tracking(order_id, "Order Placed", "Order placed and awaiting payment.")
618
 
619
                  # Notify management of the new order via WhatsApp
620
                  async def notify_management_order(order_details: dict):
 
639
                  }
640
                  asyncio.create_task(notify_management_order(order_details))
641
 
642
+                 # Update user profile with the new order ID
643
+                 await update_user_profile_with_order(user_id, order_id)
644
+
645
+                 # Generate payment link
646
                  email = "customer@example.com"  # Placeholder; retrieve from profile if available
647
                  payment_data = create_paystack_payment_link(email, total_price * 100, order_id)
648
                  dish_name = state.data.get("dish", "")
 
705
  async def on_startup():
706
      await init_db()
707
 
708
+ @app.post("/chatbot")
709
  @app.post("/chatbot")
710
  async def chatbot_response(request: Request, background_tasks: BackgroundTasks):
711
      data = await request.json()
712
      user_id = data.get("user_id")
 
713
      user_message = data.get("message", "").strip()
 
 
714
 
715
+     # Initialize conversation context for the user if not present
 
 
 
716
      if user_id not in conversation_context:
717
          conversation_context[user_id] = []
718
+
719
+     # Append the user's message to the conversation context
720
      conversation_context[user_id].append({
721
          "timestamp": datetime.utcnow().isoformat(),
722
          "role": "user",
723
          "message": user_message
724
      })
725
 
726
+     # Log the user's message
727
      background_tasks.add_task(log_chat_to_db, user_id, "inbound", user_message)
 
 
 
 
 
 
 
 
728
 
729
+     # Analyze sentiment
730
      sentiment_score = analyze_sentiment(user_message)
731
      background_tasks.add_task(log_sentiment, user_id, user_message, sentiment_score)
732
+     sentiment_modifier = "Great to hear from you! " if sentiment_score > 0.3 else ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
733
 
734
+     # --- Handle Menu Selection ---
735
+     if user_message.strip() == "1" or "menu" in user_message.lower():
736
          if user_id in user_state:
737
+             del user_state[user_id]  # Clear any existing state
738
          menu_with_images = []
739
          for index, item in enumerate(menu_items, start=1):
740
              image_url = google_image_scrape(item["name"])
 
762
          })
763
          return JSONResponse(content=response_payload)
764
 
765
+     # --- Order Flow Handling ---
766
+     if is_order_intent(user_message) or (user_id in user_state and user_state[user_id].flow == "order"):
767
+         order_response = await 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
+     # --- Fallback to NVIDIA LLM ---
778
+     recent_context = conversation_context.get(user_id, [])[-5:]  # Get the last 5 messages
779
      context_str = "\n".join([f"{entry['role'].capitalize()}: {entry['message']}" for entry in recent_context])
780
      prompt = f"Conversation context:\n{context_str}\nUser query: {user_message}\nGenerate a helpful, personalized response for a restaurant chatbot."
781
+     response_stream = stream_text_completion(prompt)  # Pass only the prompt
782
+     fallback_response = "".join([chunk for chunk in response_stream])
783
+
784
+     # Log the bot's response
785
+     background_tasks.add_task(log_chat_to_db, user_id, "outbound", fallback_response)
786
+     conversation_context[user_id].append({
787
+         "timestamp": datetime.utcnow().isoformat(),
788
+         "role": "bot",
789
+         "message": fallback_response
790
+     })
791
+
792
+     return JSONResponse(content={"response": sentiment_modifier + fallback_response})
793
 
794
  # --- Other Endpoints (Chat History, Order Details, User Profile, Analytics, Voice, Payment Callback) ---
795
  @app.get("/chat_history/{user_id}")
 
822
          "name": profile.name,
823
          "email": profile.email,
824
          "preferences": profile.preferences,
825
+         "last_interaction": profile.last_interaction.isoformat(),
826
+         "order_ids": profile.order_ids
827
      }
828
 
829
  @app.get("/analytics")
 
892
 
893
  # --- Payment Callback Endpoint with Payment Tracking and Redirection ---
894
  @app.api_route("/payment_callback", methods=["GET", "POST"])
 
895
  async def payment_callback(request: Request):
896
      # GET: User redirection after payment
897
      if request.method == "GET":
 
941
                  return JSONResponse(content={"message": "Order updated successfully."})
942
              else:
943
                  raise HTTPException(status_code=404, detail="Order not found.")
944
+
945
  @app.get("/track_order/{order_id}")
946
  async def track_order(order_id: str):
947
      """