Fred808 commited on
Commit
15f067b
·
verified ·
1 Parent(s): 4697437

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +49 -137
app.py CHANGED
@@ -6,7 +6,7 @@ import base64
6
  import asyncio
7
  from datetime import datetime, timedelta
8
  from bs4 import BeautifulSoup
9
- from sqlalchemy import select, text # make sure to import text for SQL commands
10
  from pydantic import BaseModel
11
 
12
  from fastapi import FastAPI, Request, HTTPException, BackgroundTasks, UploadFile, File, Form
@@ -25,10 +25,9 @@ from sqlalchemy import Column, Integer, String, DateTime, Text, Float
25
  # --- Environment Variables and API Keys ---
26
  SPOONACULAR_API_KEY = os.getenv("SPOONACULAR_API_KEY", "default_fallback_value")
27
  PAYSTACK_SECRET_KEY = os.getenv("PAYSTACK_SECRET_KEY", "default_fallback_value")
28
- DATABASE_URL = os.getenv("DATABASE_URL", "postgresql+asyncpg://user:password@localhost/dbname") # Update as needed
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")
32
 
33
  # WhatsApp Business API credentials (Cloud API)
34
  WHATSAPP_PHONE_NUMBER_ID = os.getenv("WHATSAPP_PHONE_NUMBER_ID", "default_value")
@@ -78,7 +77,6 @@ class UserProfile(Base):
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="") # New column
82
 
83
  class SentimentLog(Base):
84
  __tablename__ = "sentiment_logs"
@@ -88,6 +86,7 @@ class SentimentLog(Base):
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)
@@ -100,31 +99,11 @@ class OrderTracking(Base):
100
  engine = create_async_engine(DATABASE_URL, echo=True)
101
  async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
102
 
103
- # --- Migration Functions ---
104
- async def add_order_ids_column():
105
- async with engine.connect() as conn:
106
- # Ensure the column exists (PostgreSQL 9.6+ supports IF NOT EXISTS)
107
- await conn.execute(
108
- text("ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS order_ids TEXT DEFAULT ''")
109
- )
110
- await conn.commit()
111
- print("Column 'order_ids' ensured on user_profiles.")
112
-
113
  async def init_db():
114
  async with engine.begin() as conn:
115
  await conn.run_sync(Base.metadata.create_all)
116
- print("Database initialized.")
117
-
118
- # --- FastAPI Application Setup ---
119
- app = FastAPI()
120
-
121
- # Run migrations at startup
122
- @app.on_event("startup")
123
- async def startup_event():
124
- await add_order_ids_column()
125
- await init_db()
126
 
127
- # --- Global In-Memory Stores and Other Code ---
128
  user_state = {} # e.g., { user_id: ConversationState }
129
  conversation_context = {} # { user_id: [ { "timestamp": ..., "role": "user"/"bot", "message": ... }, ... ] }
130
  proactive_timer = {}
@@ -136,6 +115,7 @@ menu_items = [
136
  {"name": "Egusi Soup", "description": "A rich and hearty soup made with melon seeds", "price": 1000, "nutrition": "Calories: 250 kcal, Carbs: 15g, Protein: 8g, Fat: 10g"}
137
  ]
138
 
 
139
  SESSION_TIMEOUT = timedelta(minutes=5)
140
 
141
  class ConversationState:
@@ -296,104 +276,41 @@ def calculate_shipping_cost(address: str) -> int:
296
  return cost
297
  return TOWN_SHIPPING_COSTS["default"] # Default shipping cost for unknown areas
298
 
299
- # --- Google Maps ETA Calculation ---
300
- def calculate_eta(destination: str) -> str:
301
- """
302
- Calculate ETA from the restaurant address to the customer's delivery address using Google Maps API.
303
- """
304
- if not GOOGLE_MAPS_API_KEY:
305
- return "ETA unavailable (Google Maps API key missing)."
306
-
307
- origin = "Plot 13 Isashi Road, Iyana Isashi, Off Lagos - Badagry Expy, Lagos"
308
- url = f"https://maps.googleapis.com/maps/api/directions/json?origin={origin}&destination={destination}&key={GOOGLE_MAPS_API_KEY}"
309
-
310
- try:
311
- response = requests.get(url, timeout=10)
312
- if response.status_code == 200:
313
- data = response.json()
314
- if data.get("routes"):
315
- duration = data["routes"][0]["legs"][0]["duration"]["text"]
316
- return f"Estimated delivery time: {duration}"
317
- else:
318
- return "ETA unavailable (no route found)."
319
- else:
320
- return "ETA unavailable (API error)."
321
- except Exception as e:
322
- return f"ETA unavailable (error: {str(e)})."
323
 
324
- # --- Contextual Order Intent Detection ---
325
- def is_order_intent(message: str) -> bool:
326
- """
327
- Check if the user's message indicates an intent to place an order.
328
  """
329
- order_keywords = ["order", "menu", "dish", "food", "deliver", "hungry"]
330
- order_phrases = ["I want to order", "Can I order", "I'd like to order", "get food", "place an order"]
331
-
332
- message_lower = message.lower()
333
- for phrase in order_phrases:
334
- if phrase in message_lower:
335
- return True
336
- for keyword in order_keywords:
337
- if keyword in message_lower:
338
- # Ensure the keyword is not part of another word (e.g., "border")
339
- if re.search(rf"\b{keyword}\b", message_lower):
340
- return True
341
- return False
342
-
343
- # --- Order Tracking Functionality ---
344
- async def track_order(user_id: str, order_id: str) -> str:
345
  """
346
- Track an order and return its status and ETA.
347
- """
348
- async with async_session() as session:
349
- # Fetch order details
350
- order_result = await session.execute(
351
- select(Order).where(Order.order_id == order_id)
352
- )
353
- order = order_result.scalars().first()
354
- if not order:
355
- return "Order not found. Please check your order ID."
356
-
357
- # Fetch tracking updates
358
- tracking_result = await session.execute(
359
- select(OrderTracking)
360
- .where(OrderTracking.order_id == order_id)
361
- .order_by(OrderTracking.timestamp)
362
- )
363
- tracking_updates = tracking_result.scalars().all()
364
-
365
- # Calculate ETA
366
- eta = calculate_eta(order.delivery_address)
367
-
368
- # Prepare response
369
- response = f"Order ID: {order_id}\nStatus: {order.status}\n"
370
- if tracking_updates:
371
- response += "Tracking Updates:\n"
372
- for update in tracking_updates:
373
- response += f"- {update.status} ({update.timestamp}): {update.message or 'No details'}\n"
374
- response += f"\n{eta}"
375
- return response
376
-
377
- # --- Update User Profile with Order IDs ---
378
- async def update_user_profile_with_order(user_id: str, order_id: str):
379
  async with async_session() as session:
380
  result = await session.execute(
381
  select(UserProfile).where(UserProfile.user_id == user_id)
382
  )
383
  profile = result.scalars().first()
384
- if profile:
385
- if profile.order_ids:
386
- profile.order_ids += f",{order_id}"
387
- else:
388
- profile.order_ids = order_id
389
- await session.commit()
 
 
 
 
 
 
 
 
 
 
390
 
391
  # ... (rest of your imports and setup)
392
 
393
- async def process_order_flow(user_id: str, message: str) -> str:
394
  """
395
  Implements an FSM-based order flow with shipping cost calculation.
396
  """
 
397
  # Retrieve or initialize conversation state
398
  state = user_state.get(user_id)
399
  if state and state.is_expired():
@@ -460,7 +377,7 @@ async def process_order_flow(user_id: str, message: str) -> str:
460
  state.data["phone_number"] = phone_number
461
  state.data["address"] = address
462
  # Save phone number and address to the user's profile
463
- await update_user_profile(user_id, phone_number, address)
464
  # Calculate shipping cost based on the address
465
  shipping_cost = calculate_shipping_cost(address)
466
  state.data["shipping_cost"] = shipping_cost
@@ -471,7 +388,7 @@ async def process_order_flow(user_id: str, message: str) -> str:
471
  elif phone_match:
472
  state.data["phone_number"] = phone_match.group(1)
473
  # Save phone number to the user's profile
474
- await update_user_profile(user_id, phone_number)
475
  return "Thank you. Please provide your delivery address."
476
  else:
477
  return "Please provide both your phone number and delivery address. For example: '09162409591, 1, Iyana Isashi, Isashi, Ojo, Lagos'."
@@ -534,7 +451,7 @@ async def process_order_flow(user_id: str, message: str) -> str:
534
  state.data["phone_number"] = phone_number
535
  state.data["address"] = address
536
  # Save phone number and address to the user's profile
537
- await update_user_profile(user_id, phone_number, address)
538
  # Calculate shipping cost based on the address
539
  shipping_cost = calculate_shipping_cost(address)
540
  state.data["shipping_cost"] = shipping_cost
@@ -545,7 +462,7 @@ async def process_order_flow(user_id: str, message: str) -> str:
545
  elif phone_match:
546
  state.data["phone_number"] = phone_match.group(1)
547
  # Save phone number to the user's profile
548
- await update_user_profile(user_id, phone_number)
549
  return "Thank you. Please provide your delivery address."
550
  else:
551
  return "Please provide both your phone number and delivery address. For example: '09162409591, 1, Iyana Isashi, Isashi, Ojo, Lagos'."
@@ -554,7 +471,7 @@ async def process_order_flow(user_id: str, message: str) -> str:
554
  if state.step == 4:
555
  state.data["address"] = message
556
  # Save address to the user's profile
557
- await update_user_profile(user_id, address=message)
558
  # Calculate shipping cost based on the address
559
  shipping_cost = calculate_shipping_cost(message)
560
  state.data["shipping_cost"] = shipping_cost
@@ -634,7 +551,7 @@ async def process_order_flow(user_id: str, message: str) -> str:
634
  asyncio.create_task(save_order())
635
 
636
  # Record the initial tracking update: Order Placed
637
- await log_order_tracking(order_id, "Order Placed", "Order placed and awaiting payment.")
638
 
639
  # Notify management of the new order via WhatsApp
640
  async def notify_management_order(order_details: dict):
@@ -659,10 +576,6 @@ async def process_order_flow(user_id: str, message: str) -> str:
659
  }
660
  asyncio.create_task(notify_management_order(order_details))
661
 
662
- # Update user profile with the new order ID
663
- await update_user_profile_with_order(user_id, order_id)
664
-
665
- # Generate payment link
666
  email = "customer@example.com" # Placeholder; retrieve from profile if available
667
  payment_data = create_paystack_payment_link(email, total_price * 100, order_id)
668
  dish_name = state.data.get("dish", "")
@@ -725,7 +638,6 @@ app = FastAPI()
725
  async def on_startup():
726
  await init_db()
727
 
728
-
729
  @app.post("/chatbot")
730
  async def chatbot_response(request: Request, background_tasks: BackgroundTasks):
731
  data = await request.json()
@@ -762,31 +674,31 @@ async def chatbot_response(request: Request, background_tasks: BackgroundTasks):
762
  background_tasks.add_task(log_sentiment, user_id, user_message, sentiment_score)
763
  sentiment_modifier = ""
764
  if sentiment_score < -0.3:
765
- sentiment_modifier = "I'm sorry if you're having a tough time. "
766
  elif sentiment_score > 0.3:
767
  sentiment_modifier = "Great to hear from you! "
768
 
769
- # --- Order Tracking Handling ---
770
- order_id_match = re.search(r"ORD-\d+", user_message.upper())
771
  if order_id_match:
772
  order_id = order_id_match.group(0)
773
  try:
774
- tracking_response = await track_order(user_id, order_id)
 
775
  return JSONResponse(content={"response": tracking_response})
776
  except HTTPException as e:
777
  return JSONResponse(content={"response": f"⚠️ {e.detail}"})
778
 
779
  # --- Order Flow Handling ---
780
- if is_order_intent(user_message):
781
- order_response = process_order_flow(user_id, user_message)
782
- if order_response:
783
- background_tasks.add_task(log_chat_to_db, user_id, "outbound", order_response)
784
- conversation_context[user_id].append({
785
- "timestamp": datetime.utcnow().isoformat(),
786
- "role": "bot",
787
- "message": order_response
788
- })
789
- return JSONResponse(content={"response": sentiment_modifier + order_response})
790
 
791
  # --- Menu Display ---
792
  if "menu" in user_message.lower():
@@ -916,8 +828,7 @@ async def get_user_profile(user_id: str):
916
  "name": profile.name,
917
  "email": profile.email,
918
  "preferences": profile.preferences,
919
- "last_interaction": profile.last_interaction.isoformat(),
920
- "order_ids": profile.order_ids
921
  }
922
 
923
  @app.get("/analytics")
@@ -986,6 +897,7 @@ async def process_voice(file: UploadFile = File(...)):
986
 
987
  # --- Payment Callback Endpoint with Payment Tracking and Redirection ---
988
  @app.api_route("/payment_callback", methods=["GET", "POST"])
 
989
  async def payment_callback(request: Request):
990
  # GET: User redirection after payment
991
  if request.method == "GET":
@@ -1035,7 +947,7 @@ async def payment_callback(request: Request):
1035
  return JSONResponse(content={"message": "Order updated successfully."})
1036
  else:
1037
  raise HTTPException(status_code=404, detail="Order not found.")
1038
-
1039
  @app.get("/track_order/{order_id}")
1040
  async def track_order(order_id: str):
1041
  """
 
6
  import asyncio
7
  from datetime import datetime, timedelta
8
  from bs4 import BeautifulSoup
9
+ from sqlalchemy import select
10
  from pydantic import BaseModel
11
 
12
  from fastapi import FastAPI, Request, HTTPException, BackgroundTasks, UploadFile, File, Form
 
25
  # --- Environment Variables and API Keys ---
26
  SPOONACULAR_API_KEY = os.getenv("SPOONACULAR_API_KEY", "default_fallback_value")
27
  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
  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
  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)
 
99
  engine = create_async_engine(DATABASE_URL, echo=True)
100
  async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
101
 
 
 
 
 
 
 
 
 
 
 
102
  async def init_db():
103
  async with engine.begin() as conn:
104
  await conn.run_sync(Base.metadata.create_all)
 
 
 
 
 
 
 
 
 
 
105
 
106
+ # --- Global In-Memory Stores ---
107
  user_state = {} # e.g., { user_id: ConversationState }
108
  conversation_context = {} # { user_id: [ { "timestamp": ..., "role": "user"/"bot", "message": ... }, ... ] }
109
  proactive_timer = {}
 
115
  {"name": "Egusi Soup", "description": "A rich and hearty soup made with melon seeds", "price": 1000, "nutrition": "Calories: 250 kcal, Carbs: 15g, Protein: 8g, Fat: 10g"}
116
  ]
117
 
118
+ # --- Conversation State Management ---
119
  SESSION_TIMEOUT = timedelta(minutes=5)
120
 
121
  class ConversationState:
 
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():
 
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
  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
  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
  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
  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
  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
  }
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
  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
  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
  "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
 
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
  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
  """