Fred808 commited on
Commit
59aa4b6
·
verified ·
1 Parent(s): dd7a262

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +698 -812
app.py CHANGED
@@ -14,15 +14,12 @@ from fastapi.responses import JSONResponse, StreamingResponse, RedirectResponse
14
 
15
  import openai
16
 
17
- # For sentiment analysis using TextBlob
18
  from textblob import TextBlob
19
 
20
- # SQLAlchemy Imports (Async)
21
  from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
22
  from sqlalchemy.orm import sessionmaker, declarative_base
23
  from sqlalchemy import Column, Integer, String, DateTime, Text, Float
24
 
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")
@@ -30,917 +27,806 @@ 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")
35
  WHATSAPP_ACCESS_TOKEN = os.getenv("WHATSAPP_ACCESS_TOKEN", "default_value")
36
  MANAGEMENT_WHATSAPP_NUMBER = os.getenv("MANAGEMENT_WHATSAPP_NUMBER", "default_value")
37
 
38
- # Synthetic town prices for shipping costs
39
  TOWN_SHIPPING_COSTS = {
40
-     "lasu gate": 1000,  # N1,000 for LASU Gate
41
-     "ojo": 800,         # N800 for Ojo
42
-     "ajangbadi": 1200,  # N1,200 for Ajangbadi
43
-     "iba": 900,         # N900 for Iba
44
-     "okokomaiko": 1500, # N1,500 for Okokomaiko
45
-     "default": 1000     # Default shipping cost for unknown areas
46
  }
47
 
48
- # --- Database Setup ---
49
  Base = declarative_base()
50
 
51
  class ChatHistory(Base):
52
-     __tablename__ = "chat_history"
53
-     id = Column(Integer, primary_key=True, index=True)
54
-     user_id = Column(String, index=True)
55
-     timestamp = Column(DateTime, default=datetime.utcnow)
56
-     direction = Column(String)  # 'inbound' or 'outbound'
57
-     message = Column(Text)
58
 
59
  class Order(Base):
60
-     __tablename__ = "orders"
61
-     id = Column(Integer, primary_key=True, index=True)
62
-     order_id = Column(String, unique=True, index=True)
63
-     user_id = Column(String, index=True)
64
-     dish = Column(String)
65
-     quantity = Column(String)
66
-     price = Column(String, default="0")
67
-     status = Column(String, default="Pending Payment")
68
-     payment_reference = Column(String, nullable=True)
69
-     delivery_address = Column(String, default="")  # New field for address
70
-     timestamp = Column(DateTime, default=datetime.utcnow)
71
 
72
  class UserProfile(Base):
73
-     __tablename__ = "user_profiles"
74
-     id = Column(Integer, primary_key=True, index=True)
75
-     user_id = Column(String, unique=True, index=True)
76
-     phone_number = Column(String, unique=True, index=True, nullable=True)
77
-     name = Column(String, default="Valued Customer")
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"
85
-     id = Column(Integer, primary_key=True, index=True)
86
-     user_id = Column(String, index=True)
87
-     timestamp = Column(DateTime, default=datetime.utcnow)
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)
94
-     order_id = Column(String, index=True)
95
-     status = Column(String)  # e.g., "Order Placed", "Payment Confirmed", etc.
96
-     message = Column(Text, nullable=True)  # Optional additional details
97
-     timestamp = Column(DateTime, default=datetime.utcnow)
98
-
99
- # --- Create Engine and Session ---
100
  engine = create_async_engine(DATABASE_URL, echo=True)
101
  async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
102
 
103
  async def init_db():
104
-     async with engine.begin() as conn:
105
-         await conn.run_sync(Base.metadata.create_all)
106
 
107
- # --- Global In-Memory Stores ---
108
- user_state = {}       # e.g., { user_id: ConversationState }
109
- conversation_context = {}  # { user_id: [ { "timestamp": ..., "role": "user"/"bot", "message": ... }, ... ] }
110
  proactive_timer = {}
111
 
112
  menu_items = [
113
-     {"name": "Jollof Rice", "description": "A spicy and flavorful rice dish", "price": 1500, "nutrition": "Calories: 300 kcal, Carbs: 50g, Protein: 10g, Fat: 5g"},
114
-     {"name": "Fried Rice", "description": "A savory rice dish with vegetables and meat", "price": 1200, "nutrition": "Calories: 350 kcal, Carbs: 55g, Protein: 12g, Fat: 8g"},
115
-     {"name": "Chicken Wings", "description": "Crispy fried chicken wings", "price": 2000, "nutrition": "Calories: 400 kcal, Carbs: 20g, Protein: 25g, Fat: 15g"},
116
-     {"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"}
117
  ]
118
 
119
- # --- Conversation State Management ---
120
- SESSION_TIMEOUT = timedelta(minutes=5)
121
-
122
  class ConversationState:
123
-     def __init__(self):
124
-         self.flow = None  # e.g., "order"
125
-         self.step = 0
126
-         self.data = {}
127
-         self.last_active = datetime.utcnow()
128
 
129
-     def update_last_active(self):
130
-         self.last_active = datetime.utcnow()
131
 
132
-     def is_expired(self):
133
-         return datetime.utcnow() - self.last_active > SESSION_TIMEOUT
134
 
135
-     def reset(self):
136
-         self.flow = None
137
-         self.step = 0
138
-         self.data = {}
139
-         self.last_active = datetime.utcnow()
 
 
140
 
141
- # --- Utility Functions ---
142
  async def log_chat_to_db(user_id: str, direction: str, message: str):
143
-     async with async_session() as session:
144
-         entry = ChatHistory(user_id=user_id, direction=direction, message=message)
145
-         session.add(entry)
146
-         await session.commit()
147
 
148
  async def log_sentiment(user_id: str, message: str, score: float):
149
-     async with async_session() as session:
150
-         entry = SentimentLog(user_id=user_id, sentiment_score=score, message=message)
151
-         session.add(entry)
152
-         await session.commit()
153
 
154
  def analyze_sentiment(text: str) -> float:
155
-     blob = TextBlob(text)
156
-     return blob.sentiment.polarity
157
 
158
  def google_image_scrape(query: str) -> str:
159
-     headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
160
-     search_url = f"https://www.google.com/search?tbm=isch&q={query}"
161
-     try:
162
-         response = requests.get(search_url, headers=headers, timeout=5)
163
-     except Exception:
164
-         return ""
165
-     if response.status_code == 200:
166
-         soup = BeautifulSoup(response.text, "html.parser")
167
-         img_tags = soup.find_all("img")
168
-         for img in img_tags:
169
-             src = img.get("src")
170
-             if src and src.startswith("http"):
171
-                 return src
172
-     return ""
173
 
174
  def create_paystack_payment_link(email: str, amount: int, reference: str) -> dict:
175
-     url = "https://api.paystack.co/transaction/initialize"
176
-     headers = {
177
-         "Authorization": f"Bearer {PAYSTACK_SECRET_KEY}",
178
-         "Content-Type": "application/json",
179
-     }
180
-     data = {
181
-         "email": email,
182
-         "amount": amount,
183
-         "reference": reference,
184
-         "callback_url": "https://custy-bot.vercel.app/payment_callback"
185
-     }
186
-     try:
187
-         response = requests.post(url, json=data, headers=headers, timeout=10)
188
-         if response.status_code == 200:
189
-             return response.json()
190
-         else:
191
-             return {"status": False, "message": "Failed to initialize payment."}
192
-     except Exception as e:
193
-         return {"status": False, "message": str(e)}
194
 
195
- # --- WhatsApp Business API Helper ---
196
  def send_whatsapp_message(recipient: str, message_body: str) -> dict:
197
-     """
198
-     Sends a WhatsApp text message using the WhatsApp Cloud API.
199
-     `recipient` should be in international format, e.g., "15551234567".
200
-     """
201
-     url = f"https://graph.facebook.com/v15.0/{WHATSAPP_PHONE_NUMBER_ID}/messages"
202
-     headers = {
203
-         "Authorization": f"Bearer {WHATSAPP_ACCESS_TOKEN}",
204
-         "Content-Type": "application/json"
205
-     }
206
-     payload = {
207
-         "messaging_product": "whatsapp",
208
-         "to": recipient,
209
-         "type": "text",
210
-         "text": {"body": message_body}
211
-     }
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):
263
-     async with async_session() as session:
264
-         tracking_entry = OrderTracking(
265
-             order_id=order_id,
266
-             status=status,
267
-             message=message
268
-         )
269
-         session.add(tracking_entry)
270
-         await session.commit()
271
 
272
- # --- Advanced Internal Flow: Order Processing & Payment Integration ---
273
  def calculate_shipping_cost(address: str) -> int:
274
-     """
275
-     Calculate shipping cost based on the user's address.
276
-     """
277
-     address_lower = address.lower()
278
-     for area, cost in TOWN_SHIPPING_COSTS.items():
279
-         if area in address_lower:
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
391
-         if message.lower() == "order":
392
-             return "Sure! What dish would you like to order?"
393
-         return ""
394
-
395
-     # If no state exists but the message includes "order", start the order flow.
396
-     if not state and "order" in message.lower():
397
-         state = ConversationState()
398
-         state.flow = "order"
399
-         state.step = 1
400
-         state.update_last_active()
401
-         user_state[user_id] = state
402
-         return "Sure! What dish would you like to order?"
403
-
404
-     # --- New Logic: Parse Dish, Quantity, Phone Number, and Address in a Single Message ---
405
-     if not state or state.flow != "order":
406
-         # Check if the message contains a dish and quantity
407
-         dish_candidates = [item["name"] for item in menu_items]
408
-         found_dish = None
409
-         for dish in dish_candidates:
410
-             if dish.lower() in message.lower():
411
-                 found_dish = dish
412
-                 break
413
-         numbers = re.findall(r'\d+', message)
414
-         if found_dish and numbers:
415
-             quantity = int(numbers[0])
416
-             if quantity <= 0:
417
-                 return "Please enter a valid quantity (e.g., 1, 2, 3)."
418
-             # Initialize state and skip to step 3 (phone number)
419
-             state = ConversationState()
420
-             state.flow = "order"
421
-             state.step = 3
422
-             state.data["dish"] = found_dish
423
-             state.data["quantity"] = quantity
424
-             state.update_last_active()
425
-             user_state[user_id] = state
426
-
427
-             # Extract phone number and address from the message
428
-             phone_pattern = r'(\+?\d{10,15})'
429
-             phone_match = re.search(phone_pattern, message)
430
-             address = None
431
-             if phone_match:
432
-                 phone_number = phone_match.group(1)
433
-                 # Assume the address starts after the phone number
434
-                 address_start = phone_match.end()
435
-                 address = message[address_start:].strip()
436
-                 # Remove any leading/trailing commas or spaces
437
-                 address = re.sub(r'^[,\s]+|[,\s]+$', '', address)
438
-            
439
-             if phone_match and address:
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
447
-                 state.step = 5
448
-                 return (f"Thanks! Your phone number is recorded as: {phone_number}.\n"
449
-                         f"Your delivery address is: {address}.\n"
450
-                         f"Your delivery cost is N{shipping_cost}. Would you like to add any extras such as sides or drinks? (yes/no)")
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'."
458
-
459
-     if state and state.flow == "order":
460
-         state.update_last_active()
461
-         # --- Step 1: Expecting Dish Selection (and optionally quantity) ---
462
-         if state.step == 1:
463
-             dish_candidates = [item["name"] for item in menu_items]
464
-             found_dish = None
465
-             for dish in dish_candidates:
466
-                 if dish.lower() in message.lower():
467
-                     found_dish = dish
468
-                     break
469
-             numbers = re.findall(r'\d+', message)
470
-             if found_dish:
471
-                 state.data["dish"] = found_dish
472
-                 if numbers:
473
-                     quantity = int(numbers[0])
474
-                     if quantity <= 0:
475
-                         return "Please enter a valid quantity (e.g., 1, 2, 3)."
476
-                     state.data["quantity"] = quantity
477
-                     state.step = 3  # Move to phone number step
478
-                     return f"You selected {found_dish} with {quantity} serving(s). Please provide your phone number and delivery address."
479
-                 else:
480
-                     state.step = 2  # Ask for quantity
481
-                     return f"You selected {found_dish}. How many servings would you like?"
482
-             else:
483
-                 return "I couldn't identify the dish. Please type the dish name from our menu."
484
-
485
-         # --- Step 2: Asking for Quantity ---
486
-         if state.step == 2:
487
-             numbers = re.findall(r'\d+', message)
488
-             if not numbers:
489
-                 return "Please enter a valid number for the quantity (e.g., 1, 2, 3)."
490
-             quantity = int(numbers[0])
491
-             if quantity <= 0:
492
-                 return "Please enter a valid quantity (e.g., 1, 2, 3)."
493
-             state.data["quantity"] = quantity
494
-             state.step = 3
495
-             return f"Got it. {quantity} serving(s) of {state.data.get('dish')}. Please provide your phone number and delivery address."
496
-
497
-         # --- Step 3: Parse Phone Number and Address from Message ---
498
-         if state.step == 3:
499
-             # Extract phone number using regex
500
-             phone_pattern = r'(\+?\d{10,15})'
501
-             phone_match = re.search(phone_pattern, message)
502
-            
503
-             # Extract address (assume it follows the phone number, separated by a comma)
504
-             address = None
505
-             if phone_match:
506
-                 phone_number = phone_match.group(1)
507
-                 # Assume the address starts after the phone number
508
-                 address_start = phone_match.end()
509
-                 address = message[address_start:].strip()
510
-                 # Remove any leading/trailing commas or spaces
511
-                 address = re.sub(r'^[,\s]+|[,\s]+$', '', address)
512
-            
513
-             if phone_match and address:
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
521
-                 state.step = 5
522
-                 return (f"Thanks! Your phone number is recorded as: {phone_number}.\n"
523
-                         f"Your delivery address is: {address}.\n"
524
-                         f"Your delivery cost is N{shipping_cost}. Would you like to add any extras such as sides or drinks? (yes/no)")
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'."
532
-
533
-         # --- Step 4: Requesting Delivery Address ---
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
541
-             state.step = 5
542
-             return (f"Thanks. Your delivery address is recorded as: {message}.\n"
543
-                     f"Your delivery cost is N{shipping_cost}. Would you like to add any extras such as sides or drinks? (yes/no)")
544
-
545
-         # --- Step 5: Asking if They Want Extras ---
546
-         if state.step == 5:
547
-             if message.lower() in ["yes", "y"]:
548
-                 state.step = 6
549
-                 return "Please list the extras you would like to add (e.g., drinks, sides, etc.)."
550
-             elif message.lower() in ["no", "n"]:
551
-                 state.data["extras"] = ""
552
-                 state.step = 7
553
-                 dish = state.data.get("dish", "")
554
-                 quantity = state.data.get("quantity", 1)
555
-                 phone = state.data.get("phone_number", "")
556
-                 address = state.data.get("address", "")
557
-                 shipping_cost = state.data.get("shipping_cost", 0)
558
-                 price_per_serving = 1500  # Fixed price per serving
559
-                 total_price = (quantity * price_per_serving) + shipping_cost
560
-                 summary = (f"Order Summary:\nDish: {dish}\nQuantity: {quantity}\n"
561
-                            f"Phone: {phone}\nAddress: {address}\n"
562
-                            f"Shipping Cost: N{shipping_cost}\n"
563
-                            f"Total Price: N{total_price}\n"
564
-                            f"Extras: None\nConfirm order? (yes/no)")
565
-                 return summary
566
-             else:
567
-                 return "Please respond with 'yes' or 'no'. Would you like to add any extras to your order? (yes/no)"
568
-
569
-         # --- Step 6: Capturing Extras Details ---
570
-         if state.step == 6:
571
-             state.data["extras"] = message
572
-             state.step = 7
573
-             dish = state.data.get("dish", "")
574
-             quantity = state.data.get("quantity", 1)
575
-             phone = state.data.get("phone_number", "")
576
-             address = state.data.get("address", "")
577
-             shipping_cost = state.data.get("shipping_cost", 0)
578
-             extras = state.data.get("extras", "")
579
-             price_per_serving = 1500  # Fixed price per serving
580
-             total_price = (quantity * price_per_serving) + shipping_cost
581
-             summary = (f"Order Summary:\nDish: {dish}\nQuantity: {quantity}\n"
582
-                        f"Phone: {phone}\nAddress: {address}\n"
583
-                        f"Shipping Cost: N{shipping_cost}\n"
584
-                        f"Total Price: N{total_price}\n"
585
-                        f"Extras: {extras}\nConfirm order? (yes/no)")
586
-             return summary
587
-
588
-         # --- Step 7: Order Confirmation & Finalization ---
589
-         if state.step == 7:
590
-             if message.lower() in ["yes", "y"]:
591
-                 order_id = f"ORD-{int(time.time())}"
592
-                 state.data["order_id"] = order_id
593
-                 price_per_serving = 1500  # Fixed price per serving
594
-                 quantity = state.data.get("quantity", 1)
595
-                 shipping_cost = state.data.get("shipping_cost", 0)
596
-                 total_price = (quantity * price_per_serving) + shipping_cost
597
-                 state.data["price"] = str(total_price)
598
-
599
-                 # Save the order asynchronously (including delivery_address, phone, extras, shipping_cost)
600
-                 async def save_order():
601
-                     async with async_session() as session:
602
-                         order = Order(
603
-                             order_id=order_id,
604
-                             user_id=user_id,
605
-                             dish=state.data["dish"],
606
-                             quantity=str(quantity),
607
-                             price=str(total_price),
608
-                             status="Pending Payment",
609
-                             delivery_address=state.data.get("address", ""),
610
-                             shipping_cost=str(shipping_cost)  # Save shipping cost
611
-                         )
612
-                         session.add(order)
613
-                         await session.commit()
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):
621
-                     message_body = (
622
-                         f"New Order Received:\n"
623
-                         f"Order ID: {order_details['order_id']}\n"
624
-                         f"Dish: {order_details['dish']}\n"
625
-                         f"Quantity: {order_details['quantity']}\n"
626
-                         f"Total Price: {order_details['price']}\n"
627
-                         f"Phone: {state.data.get('phone_number', '')}\n"
628
-                         f"Delivery Address: {order_details.get('address', 'Not Provided')}\n"
629
-                         f"Extras: {state.data.get('extras', 'None')}\n"
630
-                         f"Status: Pending Payment"
631
-                     )
632
-                     await asyncio.to_thread(send_whatsapp_message, MANAGEMENT_WHATSAPP_NUMBER, message_body)
633
-                 order_details = {
634
-                     "order_id": order_id,
635
-                     "dish": state.data["dish"],
636
-                     "quantity": state.data["quantity"],
637
-                     "price": state.data["price"],
638
-                     "address": state.data.get("address", "")
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", "")
649
-                 state.reset()
650
-                 if user_id in user_state:
651
-                     del user_state[user_id]
652
-                 if payment_data.get("status"):
653
-                     payment_link = payment_data["data"]["authorization_url"]
654
-                     return (f"Thank you for your order of {quantity} serving(s) of {dish_name}! "
655
-                             f"Your Order ID is {order_id}.\nPlease complete payment here: {payment_link}\n"
656
-                             "You can track your order status using your Order ID.\n"
657
-                             "Is there anything else you'd like to order?")
658
-                 else:
659
-                     return f"Your order has been placed with Order ID {order_id}, but we could not initialize payment. Please try again later."
660
-             else:
661
-                 state.reset()
662
-                 if user_id in user_state:
663
-                     del user_state[user_id]
664
-                 return "Order canceled. Let me know if you'd like to try again."
665
-
666
-     return ""
667
-    
668
- # --- User Profile Functions ---
669
  async def get_or_create_user_profile(user_id: str, phone_number: str = None) -> UserProfile:
670
-     async with async_session() as session:
671
-         result = await session.execute(
672
-             select(UserProfile).where(UserProfile.user_id == user_id)
673
-         )
674
-         profile = result.scalars().first()
675
-         if profile is None:
676
-             profile = UserProfile(
677
-                 user_id=user_id,
678
-                 phone_number=phone_number,
679
-                 last_interaction=datetime.utcnow()
680
-             )
681
-             session.add(profile)
682
-             await session.commit()
683
-         return profile
684
 
685
  async def update_user_last_interaction(user_id: str):
686
-     async with async_session() as session:
687
-         result = await session.execute(
688
-             select(UserProfile).where(UserProfile.user_id == user_id)
689
-         )
690
-         profile = result.scalars().first()
691
-         if profile:
692
-             profile.last_interaction = datetime.utcnow()
693
-             await session.commit()
694
 
695
- # --- Proactive Engagement: Warm Greetings ---
696
  async def send_proactive_greeting(user_id: str):
697
-     greeting = "Hi again! We miss you. Would you like to see our new menu items or get personalized recommendations?"
698
-     await log_chat_to_db(user_id, "outbound", greeting)
699
-     return greeting
700
 
701
- # --- FastAPI Setup & Endpoints ---
702
  app = FastAPI()
703
 
704
  @app.on_event("startup")
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"])
741
-             menu_with_images.append({
742
-                 "number": index,
743
-                 "name": item["name"],
744
-                 "description": item["description"],
745
-                 "price": item["price"],
746
-                 "image_url": image_url
747
-             })
748
-         response_payload = {
749
-             "response": sentiment_modifier + "Here’s our delicious menu:",
750
-             "menu": menu_with_images,
751
-             "follow_up": (
752
-                 "To order, type the *number* or *name* of the dish you'd like. "
753
-                 "For example, type '1' or 'Jollof Rice' to order Jollof Rice.\n\n"
754
-                 "You can also ask for nutritional facts by typing, for example, 'Nutritional facts for Jollof Rice'."
755
-             )
756
-         }
757
-         background_tasks.add_task(log_chat_to_db, user_id, "outbound", str(response_payload))
758
-         conversation_context[user_id].append({
759
-             "timestamp": datetime.utcnow().isoformat(),
760
-             "role": "bot",
761
-             "message": response_payload["response"]
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}")
796
  async def get_chat_history(user_id: str):
797
-     async with async_session() as session:
798
-         result = await session.execute(
799
-             ChatHistory.__table__.select().where(ChatHistory.user_id == user_id)
800
-         )
801
-         history = result.fetchall()
802
-         return [dict(row) for row in history]
803
 
804
  @app.get("/order/{order_id}")
805
  async def get_order(order_id: str):
806
-     async with async_session() as session:
807
-         result = await session.execute(
808
-             Order.__table__.select().where(Order.order_id == order_id)
809
-         )
810
-         order = result.fetchone()
811
-         if order:
812
-             return dict(order)
813
-         else:
814
-             raise HTTPException(status_code=404, detail="Order not found.")
815
 
816
  @app.get("/user_profile/{user_id}")
817
  async def get_user_profile(user_id: str):
818
-     profile = await get_or_create_user_profile(user_id)
819
-     return {
820
-         "user_id": profile.user_id,
821
-         "phone_number": profile.phone_number,
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")
830
  async def get_analytics():
831
-     async with async_session() as session:
832
-         msg_result = await session.execute(ChatHistory.__table__.count())
833
-         total_messages = msg_result.scalar() or 0
834
-         order_result = await session.execute(Order.__table__.count())
835
-         total_orders = order_result.scalar() or 0
836
-         sentiment_result = await session.execute("SELECT AVG(sentiment_score) FROM sentiment_logs")
837
-         avg_sentiment = sentiment_result.scalar() or 0
838
-     return {
839
-         "total_messages": total_messages,
840
-         "total_orders": total_orders,
841
-         "average_sentiment": avg_sentiment
842
-     }
843
-
844
- # Load the Hugging Face API token from environment variables
845
  HUGGING_FACE_API_TOKEN = os.getenv("HUGGING_FACE_API_TOKEN")
846
  if not HUGGING_FACE_API_TOKEN:
847
-     raise ValueError("Hugging Face API token not found in environment variables.")
848
 
849
- # Hugging Face Whisper API configuration
850
  WHISPER_API_URL = "https://router.huggingface.co/fal-ai"
851
  WHISPER_API_HEADERS = {"Authorization": f"Bearer {HUGGING_FACE_API_TOKEN}"}
852
 
853
  class TranscriptionResponse(BaseModel):
854
-     transcription: str
855
 
856
  @app.post("/voice", response_model=TranscriptionResponse)
857
  async def process_voice(file: UploadFile = File(...)):
858
-     """
859
-     Endpoint to process voice notes and transcribe them using OpenAI Whisper via Hugging Face.
860
-     """
861
-     try:
862
-         # Read the uploaded file
863
-         contents = await file.read()
864
-
865
-         # Save the file temporarily (optional, depending on how the API expects the input)
866
-         temp_file_path = f"temp_{file.filename}"
867
-         with open(temp_file_path, "wb") as temp_file:
868
-             temp_file.write(contents)
869
 
870
-         # Call the Hugging Face Whisper API
871
-         with open(temp_file_path, "rb") as audio_file:
872
-             response = requests.post(
873
-                 WHISPER_API_URL,
874
-                 headers=WHISPER_API_HEADERS,
875
-                 files={"file": audio_file}
876
-             )
877
 
878
-         # Clean up the temporary file
879
-         os.remove(temp_file_path)
880
 
881
-         # Check if the API call was successful
882
-         if response.status_code != 200:
883
-             raise HTTPException(status_code=response.status_code, detail="Failed to transcribe audio.")
884
 
885
-         # Extract the transcription from the API response
886
-         transcription = response.json().get("text", "")
887
 
888
-         return {"transcription": transcription}
 
889
 
890
-     except Exception as e:
891
-         raise HTTPException(status_code=500, detail=f"An error occurred: {str(e)}")
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":
898
-         params = request.query_params
899
-         order_id = params.get("reference")
900
-         status = params.get("status", "Paid")
901
-         if not order_id:
902
-             raise HTTPException(status_code=400, detail="Missing order reference in callback.")
903
-         async with async_session() as session:
904
-             result = await session.execute(
905
-                 Order.__table__.select().where(Order.order_id == order_id)
906
-             )
907
-             order = result.scalar_one_or_none()
908
-             if order:
909
-                 order.status = status
910
-                 await session.commit()
911
-             else:
912
-                 raise HTTPException(status_code=404, detail="Order not found.")
913
-         # Record payment confirmation tracking update
914
-         await log_order_tracking(order_id, "Payment Confirmed", f"Payment status updated to {status}.")
915
-         # Notify management via WhatsApp about the payment update
916
-         await asyncio.to_thread(send_whatsapp_message, MANAGEMENT_WHATSAPP_NUMBER,
917
-             f"Payment Update:\nOrder ID: {order_id} is now {status}."
918
-         )
919
-         # Redirect user back to the chat interface (adjust URL as needed)
920
-         redirect_url = f"https://wa.link/am87s2"
921
-         return RedirectResponse(url=redirect_url)
922
-     # POST: Server-to-server callback from Paystack
923
-     else:
924
-         data = await request.json()
925
-         order_id = data.get("reference")
926
-         new_status = data.get("status", "Paid")
927
-         if not order_id:
928
-             raise HTTPException(status_code=400, detail="Missing order reference in callback.")
929
-         async with async_session() as session:
930
-             result = await session.execute(
931
-                 Order.__table__.select().where(Order.order_id == order_id)
932
-             )
933
-             order = result.scalar_one_or_none()
934
-             if order:
935
-                 order.status = new_status
936
-                 await session.commit()
937
-                 await log_order_tracking(order_id, "Payment Confirmed", f"Payment status updated to {new_status}.")
938
-                 await asyncio.to_thread(send_whatsapp_message, MANAGEMENT_WHATSAPP_NUMBER,
939
-                     f"Payment Update:\nOrder ID: {order_id} is now {new_status}."
940
-                 )
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):
 
14
 
15
  import openai
16
 
 
17
  from textblob import TextBlob
18
 
 
19
  from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
20
  from sqlalchemy.orm import sessionmaker, declarative_base
21
  from sqlalchemy import Column, Integer, String, DateTime, Text, Float
22
 
 
23
  SPOONACULAR_API_KEY = os.getenv("SPOONACULAR_API_KEY", "default_fallback_value")
24
  PAYSTACK_SECRET_KEY = os.getenv("PAYSTACK_SECRET_KEY", "default_fallback_value")
25
  DATABASE_URL = os.getenv("DATABASE_URL", "default_fallback_value")
 
27
  openai.api_key = os.getenv("OPENAI_API_KEY", "default_fallback_value")
28
  GOOGLE_MAPS_API_KEY = os.getenv("GOOGLE_MAPS_API_KEY", "default_fallback_value")
29
 
 
30
  WHATSAPP_PHONE_NUMBER_ID = os.getenv("WHATSAPP_PHONE_NUMBER_ID", "default_value")
31
  WHATSAPP_ACCESS_TOKEN = os.getenv("WHATSAPP_ACCESS_TOKEN", "default_value")
32
  MANAGEMENT_WHATSAPP_NUMBER = os.getenv("MANAGEMENT_WHATSAPP_NUMBER", "default_value")
33
 
 
34
  TOWN_SHIPPING_COSTS = {
35
+ "lasu gate": 1000,
36
+ "ojo": 800,
37
+ "ajangbadi": 1200,
38
+ "iba": 900,
39
+ "okokomaiko": 1500,
40
+ "default": 1000
41
  }
42
 
 
43
  Base = declarative_base()
44
 
45
  class ChatHistory(Base):
46
+ __tablename__ = "chat_history"
47
+ id = Column(Integer, primary_key=True, index=True)
48
+ user_id = Column(String, index=True)
49
+ timestamp = Column(DateTime, default=datetime.utcnow)
50
+ direction = Column(String)
51
+ message = Column(Text)
52
 
53
  class Order(Base):
54
+ __tablename__ = "orders"
55
+ id = Column(Integer, primary_key=True, index=True)
56
+ order_id = Column(String, unique=True, index=True)
57
+ user_id = Column(String, index=True)
58
+ dish = Column(String)
59
+ quantity = Column(String)
60
+ price = Column(String, default="0")
61
+ status = Column(String, default="Pending Payment")
62
+ payment_reference = Column(String, nullable=True)
63
+ delivery_address = Column(String, default="")
64
+ timestamp = Column(DateTime, default=datetime.utcnow)
65
 
66
  class UserProfile(Base):
67
+ __tablename__ = "user_profiles"
68
+ id = Column(Integer, primary_key=True, index=True)
69
+ user_id = Column(String, unique=True, index=True)
70
+ phone_number = Column(String, unique=True, index=True, nullable=True)
71
+ name = Column(String, default="Valued Customer")
72
+ email = Column(String, default="unknown@example.com")
73
+ preferences = Column(Text, default="")
74
+ last_interaction = Column(DateTime, default=datetime.utcnow)
75
+ order_ids = Column(Text, default="")
76
 
77
  class SentimentLog(Base):
78
+ __tablename__ = "sentiment_logs"
79
+ id = Column(Integer, primary_key=True, index=True)
80
+ user_id = Column(String, index=True)
81
+ timestamp = Column(DateTime, default=datetime.utcnow)
82
+ sentiment_score = Column(Float)
83
+ message = Column(Text)
84
 
85
  class OrderTracking(Base):
86
+ __tablename__ = "order_tracking"
87
+ id = Column(Integer, primary_key=True, index=True)
88
+ order_id = Column(String, index=True)
89
+ status = Column(String)
90
+ message = Column(Text, nullable=True)
91
+ timestamp = Column(DateTime, default=datetime.utcnow)
92
+
 
93
  engine = create_async_engine(DATABASE_URL, echo=True)
94
  async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
95
 
96
  async def init_db():
97
+ async with engine.begin() as conn:
98
+ await conn.run_sync(Base.metadata.create_all)
99
 
100
+ user_state = {}
101
+ conversation_context = {}
 
102
  proactive_timer = {}
103
 
104
  menu_items = [
105
+ {"name": "Jollof Rice", "description": "A spicy and flavorful rice dish", "price": 1500, "nutrition": "Calories: 300 kcal, Carbs: 50g, Protein: 10g, Fat: 5g"},
106
+ {"name": "Fried Rice", "description": "A savory rice dish with vegetables and meat", "price": 1200, "nutrition": "Calories: 350 kcal, Carbs: 55g, Protein: 12g, Fat: 8g"},
107
+ {"name": "Chicken Wings", "description": "Crispy fried chicken wings", "price": 2000, "nutrition": "Calories: 400 kcal, Carbs: 20g, Protein: 25g, Fat: 15g"},
108
+ {"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"}
109
  ]
110
 
 
 
 
111
  class ConversationState:
112
+ def __init__(self):
113
+ self.flow = None
114
+ self.step = 0
115
+ self.data = {}
116
+ self.last_active = datetime.utcnow()
117
 
118
+ def update_last_active(self):
119
+ self.last_active = datetime.utcnow()
120
 
121
+ def is_expired(self):
122
+ return datetime.utcnow() - self.last_active > SESSION_TIMEOUT
123
 
124
+ def reset(self):
125
+ self.flow = None
126
+ self.step = 0
127
+ self.data = {}
128
+ self.last_active = datetime.utcnow()
129
+
130
+ SESSION_TIMEOUT = timedelta(minutes=5)
131
 
 
132
  async def log_chat_to_db(user_id: str, direction: str, message: str):
133
+ async with async_session() as session:
134
+ entry = ChatHistory(user_id=user_id, direction=direction, message=message)
135
+ session.add(entry)
136
+ await session.commit()
137
 
138
  async def log_sentiment(user_id: str, message: str, score: float):
139
+ async with async_session() as session:
140
+ entry = SentimentLog(user_id=user_id, sentiment_score=score, message=message)
141
+ session.add(entry)
142
+ await session.commit()
143
 
144
  def analyze_sentiment(text: str) -> float:
145
+ blob = TextBlob(text)
146
+ return blob.sentiment.polarity
147
 
148
  def google_image_scrape(query: str) -> str:
149
+ headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
150
+ search_url = f"https://www.google.com/search?tbm=isch&q={query}"
151
+ try:
152
+ response = requests.get(search_url, headers=headers, timeout=5)
153
+ except Exception:
154
+ return ""
155
+ if response.status_code == 200:
156
+ soup = BeautifulSoup(response.text, "html.parser")
157
+ img_tags = soup.find_all("img")
158
+ for img in img_tags:
159
+ src = img.get("src")
160
+ if src and src.startswith("http"):
161
+ return src
162
+ return ""
163
 
164
  def create_paystack_payment_link(email: str, amount: int, reference: str) -> dict:
165
+ url = "https://api.paystack.co/transaction/initialize"
166
+ headers = {
167
+ "Authorization": f"Bearer {PAYSTACK_SECRET_KEY}",
168
+ "Content-Type": "application/json",
169
+ }
170
+ data = {
171
+ "email": email,
172
+ "amount": amount,
173
+ "reference": reference,
174
+ "callback_url": "https://custy-bot.vercel.app/payment_callback"
175
+ }
176
+ try:
177
+ response = requests.post(url, json=data, headers=headers, timeout=10)
178
+ if response.status_code == 200:
179
+ return response.json()
180
+ else:
181
+ return {"status": False, "message": "Failed to initialize payment."}
182
+ except Exception as e:
183
+ return {"status": False, "message": str(e)}
184
 
 
185
  def send_whatsapp_message(recipient: str, message_body: str) -> dict:
186
+ url = f"https://graph.facebook.com/v15.0/{WHATSAPP_PHONE_NUMBER_ID}/messages"
187
+ headers = {
188
+ "Authorization": f"Bearer {WHATSAPP_ACCESS_TOKEN}",
189
+ "Content-Type": "application/json"
190
+ }
191
+ payload = {
192
+ "messaging_product": "whatsapp",
193
+ "to": recipient,
194
+ "type": "text",
195
+ "text": {"body": message_body}
196
+ }
197
+ response = requests.post(url, headers=headers, json=payload)
198
+ return response.json()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
 
200
  def stream_text_completion(prompt: str):
201
+ from openai import OpenAI
202
+ client = OpenAI(
203
+ base_url="https://integrate.api.nvidia.com/v1",
204
+ api_key=NVIDIA_API_KEY
205
+ )
206
+ try:
207
+ completion = client.chat.completions.create(
208
+ model="meta/llama-3.1-405b-instruct",
209
+ messages=[{"role": "user", "content": prompt}],
210
+ temperature=0.2,
211
+ top_p=0.7,
212
+ max_tokens=1024,
213
+ stream=True
214
+ )
215
+ for chunk in completion:
216
+ if chunk.choices[0].delta.content is not None:
217
+ yield chunk.choices[0].delta.content
218
+ except Exception as e:
219
+ yield f"Error: {str(e)}"
220
+
221
+ def stream_image_completion(image_b64: str):
222
+ invoke_url = "https://ai.api.nvidia.com/v1/gr/meta/llama-3.2-90b-vision-instruct/chat/completions"
223
+ headers = {
224
+ "Authorization": f"Bearer {NVIDIA_API_KEY}",
225
+ "Accept": "text/event-stream"
226
+ }
227
+ payload = {
228
+ "model": "meta/llama-3.2-90b-vision-instruct",
229
+ "messages": [
230
+ {
231
+ "role": "user",
232
+ "content": f'What is in this image? <img src="data:image/png;base64,{image_b64}" />'
233
+ }
234
+ ],
235
+ "max_tokens": 512,
236
+ "temperature": 1.00,
237
+ "top_p": 1.00,
238
+ "stream": True
239
+ }
240
+ response = requests.post(invoke_url, headers=headers, json=payload, stream=True)
241
+ for line in response.iter_lines():
242
+ if line:
243
+ yield line.decode("utf-8") + "\n"
244
 
 
245
  async def log_order_tracking(order_id: str, status: str, message: str = None):
246
+ async with async_session() as session:
247
+ tracking_entry = OrderTracking(
248
+ order_id=order_id,
249
+ status=status,
250
+ message=message
251
+ )
252
+ session.add(tracking_entry)
253
+ await session.commit()
254
 
 
255
  def calculate_shipping_cost(address: str) -> int:
256
+ address_lower = address.lower()
257
+ for area, cost in TOWN_SHIPPING_COSTS.items():
258
+ if area in address_lower:
259
+ return cost
260
+ return TOWN_SHIPPING_COSTS["default"]
 
 
 
261
 
 
262
  def calculate_eta(destination: str) -> str:
263
+ if not GOOGLE_MAPS_API_KEY:
264
+ return "ETA unavailable (Google Maps API key missing)."
265
+
266
+ origin = "Plot 13 Isashi Road, Iyana Isashi, Off Lagos - Badagry Expy, Lagos"
267
+ url = f"https://maps.googleapis.com/maps/api/directions/json?origin={origin}&destination={destination}&key={GOOGLE_MAPS_API_KEY}"
268
+
269
+ try:
270
+ response = requests.get(url, timeout=10)
271
+ if response.status_code == 200:
272
+ data = response.json()
273
+ if data.get("routes"):
274
+ duration = data["routes"][0]["legs"][0]["duration"]["text"]
275
+ return f"Estimated delivery time: {duration}"
276
+ else:
277
+ return "ETA unavailable (no route found)."
278
+ else:
279
+ return "ETA unavailable (API error)."
280
+ except Exception as e:
281
+ return f"ETA unavailable (error: {str(e)})."
 
 
 
282
 
 
283
  def is_order_intent(message: str) -> bool:
284
+ order_keywords = ["order", "menu", "dish", "food", "deliver", "hungry"]
285
+ order_phrases = ["I want to order", "Can I order", "I'd like to order", "get food", "place an order"]
286
+
287
+ message_lower = message.lower()
288
+ for phrase in order_phrases:
289
+ if phrase in message_lower:
290
+ return True
291
+ for keyword in order_keywords:
292
+ if keyword in message_lower:
293
+ if re.search(rf"\b{keyword}\b", message_lower):
294
+ return True
295
+ return False
 
 
 
 
 
 
 
 
296
 
297
  async def track_order(user_id: str, order_id: str) -> str:
298
+ async with async_session() as session:
299
+ order_result = await session.execute(
300
+ select(Order).where(Order.order_id == order_id)
301
+ )
302
+ order = order_result.scalars().first()
303
+ if not order:
304
+ return "Order not found. Please check your order ID."
305
+
306
+ tracking_result = await session.execute(
307
+ select(OrderTracking)
308
+ .where(OrderTracking.order_id == order_id)
309
+ .order_by(OrderTracking.timestamp)
310
+ )
311
+ tracking_updates = tracking_result.scalars().all()
312
+
313
+ eta = calculate_eta(order.delivery_address)
314
+
315
+ response = f"Order ID: {order_id}\nStatus: {order.status}\n"
316
+ if tracking_updates:
317
+ response += "Tracking Updates:\n"
318
+ for update in tracking_updates:
319
+ response += f"- {update.status} ({update.timestamp}): {update.message or 'No details'}\n"
320
+ response += f"\n{eta}"
321
+ return response
322
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  async def update_user_profile_with_order(user_id: str, order_id: str):
324
+ async with async_session() as session:
325
+ result = await session.execute(
326
+ select(UserProfile).where(UserProfile.user_id == user_id)
327
+ )
328
+ profile = result.scalars().first()
329
+ if profile:
330
+ if profile.order_ids:
331
+ profile.order_ids += f",{order_id}"
332
+ else:
333
+ profile.order_ids = order_id
334
+ await session.commit()
335
 
336
  async def process_order_flow(user_id: str, message: str) -> str:
337
+ state = user_state.get(user_id)
338
+ if state and state.is_expired():
339
+ state.reset()
340
+ state.step = 1
341
+ state.update_last_active()
342
+ user_state[user_id] = state
343
+ if message.lower() == "order":
344
+ return "Sure! What dish would you like to order?"
345
+ return ""
346
+
347
+ if not state and "order" in message.lower():
348
+ state = ConversationState()
349
+ state.flow = "order"
350
+ state.step = 1
351
+ state.update_last_active()
352
+ user_state[user_id] = state
353
+ return "Sure! What dish would you like to order?"
354
+
355
+ if not state or state.flow != "order":
356
+ dish_candidates = [item["name"] for item in menu_items]
357
+ found_dish = None
358
+ for dish in dish_candidates:
359
+ if dish.lower() in message.lower():
360
+ found_dish = dish
361
+ break
362
+ numbers = re.findall(r'\d+', message)
363
+ if found_dish and numbers:
364
+ quantity = int(numbers[0])
365
+ if quantity <= 0:
366
+ return "Please enter a valid quantity (e.g., 1, 2, 3)."
367
+ state = ConversationState()
368
+ state.flow = "order"
369
+ state.step = 3
370
+ state.data["dish"] = found_dish
371
+ state.data["quantity"] = quantity
372
+ state.update_last_active()
373
+ user_state[user_id] = state
374
+
375
+ phone_pattern = r'(\+?\d{10,15})'
376
+ phone_match = re.search(phone_pattern, message)
377
+ address = None
378
+ if phone_match:
379
+ phone_number = phone_match.group(1)
380
+ address_start = phone_match.end()
381
+ address = message[address_start:].strip()
382
+ address = re.sub(r'^[,\s]+|[,\s]+$', '', address)
383
+
384
+ if phone_match and address:
385
+ state.data["phone_number"] = phone_number
386
+ state.data["address"] = address
387
+ await update_user_profile(user_id, phone_number, address)
388
+ shipping_cost = calculate_shipping_cost(address)
389
+ state.data["shipping_cost"] = shipping_cost
390
+ state.step = 5
391
+ return (f"Thanks! Your phone number is recorded as: {phone_number}.\n"
392
+ f"Your delivery address is: {address}.\n"
393
+ f"Your delivery cost is N{shipping_cost}. Would you like to add any extras such as sides or drinks? (yes/no)")
394
+ elif phone_match:
395
+ state.data["phone_number"] = phone_match.group(1)
396
+ await update_user_profile(user_id, phone_number)
397
+ return "Thank you. Please provide your delivery address."
398
+ else:
399
+ return "Please provide both your phone number and delivery address. For example: '09162409591, 1, Iyana Isashi, Isashi, Ojo, Lagos'."
400
+
401
+ if state and state.flow == "order":
402
+ state.update_last_active()
403
+ if state.step == 1:
404
+ dish_candidates = [item["name"] for item in menu_items]
405
+ found_dish = None
406
+ for dish in dish_candidates:
407
+ if dish.lower() in message.lower():
408
+ found_dish = dish
409
+ break
410
+ numbers = re.findall(r'\d+', message)
411
+ if found_dish:
412
+ state.data["dish"] = found_dish
413
+ if numbers:
414
+ quantity = int(numbers[0])
415
+ if quantity <= 0:
416
+ return "Please enter a valid quantity (e.g., 1, 2, 3)."
417
+ state.data["quantity"] = quantity
418
+ state.step = 3
419
+ return f"You selected {found_dish} with {quantity} serving(s). Please provide your phone number and delivery address."
420
+ else:
421
+ state.step = 2
422
+ return f"You selected {found_dish}. How many servings would you like?"
423
+ else:
424
+ return "I couldn't identify the dish. Please type the dish name from our menu."
425
+
426
+ if state.step == 2:
427
+ numbers = re.findall(r'\d+', message)
428
+ if not numbers:
429
+ return "Please enter a valid number for the quantity (e.g., 1, 2, 3)."
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
+ return f"Got it. {quantity} serving(s) of {state.data.get('dish')}. Please provide your phone number and delivery address."
436
+
437
+ if state.step == 3:
438
+ phone_pattern = r'(\+?\d{10,15})'
439
+ phone_match = re.search(phone_pattern, message)
440
+ address = None
441
+ if phone_match:
442
+ phone_number = phone_match.group(1)
443
+ address_start = phone_match.end()
444
+ address = message[address_start:].strip()
445
+ address = re.sub(r'^[,\s]+|[,\s]+$', '', address)
446
+
447
+ if phone_match and address:
448
+ state.data["phone_number"] = phone_number
449
+ state.data["address"] = address
450
+ await update_user_profile(user_id, phone_number, address)
451
+ shipping_cost = calculate_shipping_cost(address)
452
+ state.data["shipping_cost"] = shipping_cost
453
+ state.step = 5
454
+ return (f"Thanks! Your phone number is recorded as: {phone_number}.\n"
455
+ f"Your delivery address is: {address}.\n"
456
+ f"Your delivery cost is N{shipping_cost}. Would you like to add any extras such as sides or drinks? (yes/no)")
457
+ elif phone_match:
458
+ state.data["phone_number"] = phone_match.group(1)
459
+ await update_user_profile(user_id, phone_number)
460
+ return "Thank you. Please provide your delivery address."
461
+ else:
462
+ return "Please provide both your phone number and delivery address. For example: '09162409591, 1, Iyana Isashi, Isashi, Ojo, Lagos'."
463
+
464
+ if state.step == 4:
465
+ state.data["address"] = message
466
+ await update_user_profile(user_id, address=message)
467
+ shipping_cost = calculate_shipping_cost(message)
468
+ state.data["shipping_cost"] = shipping_cost
469
+ state.step = 5
470
+ return (f"Thanks. Your delivery address is recorded as: {message}.\n"
471
+ f"Your delivery cost is N{shipping_cost}. Would you like to add any extras such as sides or drinks? (yes/no)")
472
+
473
+ if state.step == 5:
474
+ if message.lower() in ["yes", "y"]:
475
+ state.step = 6
476
+ return "Please list the extras you would like to add (e.g., drinks, sides, etc.)."
477
+ elif message.lower() in ["no", "n"]:
478
+ state.data["extras"] = ""
479
+ state.step = 7
480
+ dish = state.data.get("dish", "")
481
+ quantity = state.data.get("quantity", 1)
482
+ phone = state.data.get("phone_number", "")
483
+ address = state.data.get("address", "")
484
+ shipping_cost = state.data.get("shipping_cost", 0)
485
+ price_per_serving = 1500
486
+ total_price = (quantity * price_per_serving) + shipping_cost
487
+ summary = (f"Order Summary:\nDish: {dish}\nQuantity: {quantity}\n"
488
+ f"Phone: {phone}\nAddress: {address}\n"
489
+ f"Shipping Cost: N{shipping_cost}\n"
490
+ f"Total Price: N{total_price}\n"
491
+ f"Extras: None\nConfirm order? (yes/no)")
492
+ return summary
493
+ else:
494
+ return "Please respond with 'yes' or 'no'. Would you like to add any extras to your order? (yes/no)"
495
+
496
+ if state.step == 6:
497
+ state.data["extras"] = message
498
+ state.step = 7
499
+ dish = state.data.get("dish", "")
500
+ quantity = state.data.get("quantity", 1)
501
+ phone = state.data.get("phone_number", "")
502
+ address = state.data.get("address", "")
503
+ shipping_cost = state.data.get("shipping_cost", 0)
504
+ extras = state.data.get("extras", "")
505
+ price_per_serving = 1500
506
+ total_price = (quantity * price_per_serving) + shipping_cost
507
+ summary = (f"Order Summary:\nDish: {dish}\nQuantity: {quantity}\n"
508
+ f"Phone: {phone}\nAddress: {address}\n"
509
+ f"Shipping Cost: N{shipping_cost}\n"
510
+ f"Total Price: N{total_price}\n"
511
+ f"Extras: {extras}\nConfirm order? (yes/no)")
512
+ return summary
513
+
514
+ if state.step == 7:
515
+ if message.lower() in ["yes", "y"]:
516
+ order_id = f"ORD-{int(time.time())}"
517
+ state.data["order_id"] = order_id
518
+ price_per_serving = 1500
519
+ quantity = state.data.get("quantity", 1)
520
+ shipping_cost = state.data.get("shipping_cost", 0)
521
+ total_price = (quantity * price_per_serving) + shipping_cost
522
+ state.data["price"] = str(total_price)
523
+
524
+ async def save_order():
525
+ async with async_session() as session:
526
+ order = Order(
527
+ order_id=order_id,
528
+ user_id=user_id,
529
+ dish=state.data["dish"],
530
+ quantity=str(quantity),
531
+ price=str(total_price),
532
+ status="Pending Payment",
533
+ delivery_address=state.data.get("address", ""),
534
+ shipping_cost=str(shipping_cost)
535
+ )
536
+ session.add(order)
537
+ await session.commit()
538
+ asyncio.create_task(save_order())
539
+
540
+ await log_order_tracking(order_id, "Order Placed", "Order placed and awaiting payment.")
541
+
542
+ async def notify_management_order(order_details: dict):
543
+ message_body = (
544
+ f"New Order Received:\n"
545
+ f"Order ID: {order_details['order_id']}\n"
546
+ f"Dish: {order_details['dish']}\n"
547
+ f"Quantity: {order_details['quantity']}\n"
548
+ f"Total Price: {order_details['price']}\n"
549
+ f"Phone: {state.data.get('phone_number', '')}\n"
550
+ f"Delivery Address: {order_details.get('address', 'Not Provided')}\n"
551
+ f"Extras: {state.data.get('extras', 'None')}\n"
552
+ f"Status: Pending Payment"
553
+ )
554
+ await asyncio.to_thread(send_whatsapp_message, MANAGEMENT_WHATSAPP_NUMBER, message_body)
555
+ order_details = {
556
+ "order_id": order_id,
557
+ "dish": state.data["dish"],
558
+ "quantity": state.data["quantity"],
559
+ "price": state.data["price"],
560
+ "address": state.data.get("address", "")
561
+ }
562
+ asyncio.create_task(notify_management_order(order_details))
563
+
564
+ await update_user_profile_with_order(user_id, order_id)
565
+
566
+ email = "customer@example.com"
567
+ payment_data = create_paystack_payment_link(email, total_price * 100, order_id)
568
+ dish_name = state.data.get("dish", "")
569
+ state.reset()
570
+ if user_id in user_state:
571
+ del user_state[user_id]
572
+ if payment_data.get("status"):
573
+ payment_link = payment_data["data"]["authorization_url"]
574
+ return (f"Thank you for your order of {quantity} serving(s) of {dish_name}! "
575
+ f"Your Order ID is {order_id}.\nPlease complete payment here: {payment_link}\n"
576
+ "You can track your order status using your Order ID.\n"
577
+ "Is there anything else you'd like to order?")
578
+ else:
579
+ return f"Your order has been placed with Order ID {order_id}, but we could not initialize payment. Please try again later."
580
+ else:
581
+ state.reset()
582
+ if user_id in user_state:
583
+ del user_state[user_id]
584
+ return "Order canceled. Let me know if you'd like to try again."
585
+
586
+ return ""
587
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
588
  async def get_or_create_user_profile(user_id: str, phone_number: str = None) -> UserProfile:
589
+ async with async_session() as session:
590
+ result = await session.execute(
591
+ select(UserProfile).where(UserProfile.user_id == user_id)
592
+ profile = result.scalars().first()
593
+ if profile is None:
594
+ profile = UserProfile(
595
+ user_id=user_id,
596
+ phone_number=phone_number,
597
+ last_interaction=datetime.utcnow()
598
+ )
599
+ session.add(profile)
600
+ await session.commit()
601
+ return profile
 
602
 
603
  async def update_user_last_interaction(user_id: str):
604
+ async with async_session() as session:
605
+ result = await session.execute(
606
+ select(UserProfile).where(UserProfile.user_id == user_id)
607
+ profile = result.scalars().first()
608
+ if profile:
609
+ profile.last_interaction = datetime.utcnow()
610
+ await session.commit()
 
611
 
 
612
  async def send_proactive_greeting(user_id: str):
613
+ greeting = "Hi again! We miss you. Would you like to see our new menu items or get personalized recommendations?"
614
+ await log_chat_to_db(user_id, "outbound", greeting)
615
+ return greeting
616
 
 
617
  app = FastAPI()
618
 
619
  @app.on_event("startup")
620
  async def on_startup():
621
+ await init_db()
622
 
 
623
  @app.post("/chatbot")
624
  async def chatbot_response(request: Request, background_tasks: BackgroundTasks):
625
+ data = await request.json()
626
+ user_id = data.get("user_id")
627
+ user_message = data.get("message", "").strip()
628
+
629
+ if user_id not in conversation_context:
630
+ conversation_context[user_id] = []
631
+
632
+ conversation_context[user_id].append({
633
+ "timestamp": datetime.utcnow().isoformat(),
634
+ "role": "user",
635
+ "message": user_message
636
+ })
637
+
638
+ background_tasks.add_task(log_chat_to_db, user_id, "inbound", user_message)
639
+
640
+ sentiment_score = analyze_sentiment(user_message)
641
+ background_tasks.add_task(log_sentiment, user_id, user_message, sentiment_score)
642
+ sentiment_modifier = "Great to hear from you! " if sentiment_score > 0.3 else ""
643
+
644
+ if user_message.strip() == "1" or "menu" in user_message.lower():
645
+ if user_id in user_state:
646
+ del user_state[user_id]
647
+ menu_with_images = []
648
+ for index, item in enumerate(menu_items, start=1):
649
+ image_url = google_image_scrape(item["name"])
650
+ menu_with_images.append({
651
+ "number": index,
652
+ "name": item["name"],
653
+ "description": item["description"],
654
+ "price": item["price"],
655
+ "image_url": image_url
656
+ })
657
+ response_payload = {
658
+ "response": sentiment_modifier + "Here’s our delicious menu:",
659
+ "menu": menu_with_images,
660
+ "follow_up": (
661
+ "To order, type the *number* or *name* of the dish you'd like. "
662
+ "For example, type '1' or 'Jollof Rice' to order Jollof Rice.\n\n"
663
+ "You can also ask for nutritional facts by typing, for example, 'Nutritional facts for Jollof Rice'."
664
+ )
665
+ }
666
+ background_tasks.add_task(log_chat_to_db, user_id, "outbound", str(response_payload))
667
+ conversation_context[user_id].append({
668
+ "timestamp": datetime.utcnow().isoformat(),
669
+ "role": "bot",
670
+ "message": response_payload["response"]
671
+ })
672
+ return JSONResponse(content=response_payload)
673
+
674
+ if is_order_intent(user_message) or (user_id in user_state and user_state[user_id].flow == "order"):
675
+ order_response = await process_order_flow(user_id, user_message)
676
+ if order_response:
677
+ background_tasks.add_task(log_chat_to_db, user_id, "outbound", order_response)
678
+ conversation_context[user_id].append({
679
+ "timestamp": datetime.utcnow().isoformat(),
680
+ "role": "bot",
681
+ "message": order_response
682
+ })
683
+ return JSONResponse(content={"response": sentiment_modifier + order_response})
684
+
685
+ recent_context = conversation_context.get(user_id, [])[-5:]
686
+ context_str = "\n".join([f"{entry['role'].capitalize()}: {entry['message']}" for entry in recent_context])
687
+ prompt = f"Conversation context:\n{context_str}\nUser query: {user_message}\nGenerate a helpful, personalized response for a restaurant chatbot."
688
+ response_stream = stream_text_completion(prompt)
689
+ fallback_response = "".join([chunk for chunk in response_stream])
690
+
691
+ background_tasks.add_task(log_chat_to_db, user_id, "outbound", fallback_response)
692
+ conversation_context[user_id].append({
693
+ "timestamp": datetime.utcnow().isoformat(),
694
+ "role": "bot",
695
+ "message": fallback_response
696
+ })
697
+
698
+ return JSONResponse(content={"response": sentiment_modifier + fallback_response})
699
+
 
 
 
 
 
 
 
 
 
700
  @app.get("/chat_history/{user_id}")
701
  async def get_chat_history(user_id: str):
702
+ async with async_session() as session:
703
+ result = await session.execute(
704
+ ChatHistory.__table__.select().where(ChatHistory.user_id == user_id)
705
+ )
706
+ history = result.fetchall()
707
+ return [dict(row) for row in history]
708
 
709
  @app.get("/order/{order_id}")
710
  async def get_order(order_id: str):
711
+ async with async_session() as session:
712
+ result = await session.execute(
713
+ Order.__table__.select().where(Order.order_id == order_id)
714
+ )
715
+ order = result.fetchone()
716
+ if order:
717
+ return dict(order)
718
+ else:
719
+ raise HTTPException(status_code=404, detail="Order not found.")
720
 
721
  @app.get("/user_profile/{user_id}")
722
  async def get_user_profile(user_id: str):
723
+ profile = await get_or_create_user_profile(user_id)
724
+ return {
725
+ "user_id": profile.user_id,
726
+ "phone_number": profile.phone_number,
727
+ "name": profile.name,
728
+ "email": profile.email,
729
+ "preferences": profile.preferences,
730
+ "last_interaction": profile.last_interaction.isoformat(),
731
+ "order_ids": profile.order_ids
732
+ }
733
 
734
  @app.get("/analytics")
735
  async def get_analytics():
736
+ async with async_session() as session:
737
+ msg_result = await session.execute(ChatHistory.__table__.count())
738
+ total_messages = msg_result.scalar() or 0
739
+ order_result = await session.execute(Order.__table__.count())
740
+ total_orders = order_result.scalar() or 0
741
+ sentiment_result = await session.execute("SELECT AVG(sentiment_score) FROM sentiment_logs")
742
+ avg_sentiment = sentiment_result.scalar() or 0
743
+ return {
744
+ "total_messages": total_messages,
745
+ "total_orders": total_orders,
746
+ "average_sentiment": avg_sentiment
747
+ }
748
+
 
749
  HUGGING_FACE_API_TOKEN = os.getenv("HUGGING_FACE_API_TOKEN")
750
  if not HUGGING_FACE_API_TOKEN:
751
+ raise ValueError("Hugging Face API token not found in environment variables.")
752
 
 
753
  WHISPER_API_URL = "https://router.huggingface.co/fal-ai"
754
  WHISPER_API_HEADERS = {"Authorization": f"Bearer {HUGGING_FACE_API_TOKEN}"}
755
 
756
  class TranscriptionResponse(BaseModel):
757
+ transcription: str
758
 
759
  @app.post("/voice", response_model=TranscriptionResponse)
760
  async def process_voice(file: UploadFile = File(...)):
761
+ try:
762
+ contents = await file.read()
763
+ temp_file_path = f"temp_{file.filename}"
764
+ with open(temp_file_path, "wb") as temp_file:
765
+ temp_file.write(contents)
 
 
 
 
 
 
766
 
767
+ with open(temp_file_path, "rb") as audio_file:
768
+ response = requests.post(
769
+ WHISPER_API_URL,
770
+ headers=WHISPER_API_HEADERS,
771
+ files={"file": audio_file}
772
+ )
 
773
 
774
+ os.remove(temp_file_path)
 
775
 
776
+ if response.status_code != 200:
777
+ raise HTTPException(status_code=response.status_code, detail="Failed to transcribe audio.")
 
778
 
779
+ transcription = response.json().get("text", "")
780
+ return {"transcription": transcription}
781
 
782
+ except Exception as e:
783
+ raise HTTPException(status_code=500, detail=f"An error occurred: {str(e)}")
784
 
 
 
 
 
785
  @app.api_route("/payment_callback", methods=["GET", "POST"])
786
  async def payment_callback(request: Request):
787
+ if request.method == "GET":
788
+ params = request.query_params
789
+ order_id = params.get("reference")
790
+ status = params.get("status", "Paid")
791
+ if not order_id:
792
+ raise HTTPException(status_code=400, detail="Missing order reference in callback.")
793
+ async with async_session() as session:
794
+ result = await session.execute(
795
+ Order.__table__.select().where(Order.order_id == order_id)
796
+ )
797
+ order = result.scalar_one_or_none()
798
+ if order:
799
+ order.status = status
800
+ await session.commit()
801
+ else:
802
+ raise HTTPException(status_code=404, detail="Order not found.")
803
+ await log_order_tracking(order_id, "Payment Confirmed", f"Payment status updated to {status}.")
804
+ await asyncio.to_thread(send_whatsapp_message, MANAGEMENT_WHATSAPP_NUMBER,
805
+ f"Payment Update:\nOrder ID: {order_id} is now {status}."
806
+ )
807
+ redirect_url = f"https://wa.link/am87s2"
808
+ return RedirectResponse(url=redirect_url)
809
+ else:
810
+ data = await request.json()
811
+ order_id = data.get("reference")
812
+ new_status = data.get("status", "Paid")
813
+ if not order_id:
814
+ raise HTTPException(status_code=400, detail="Missing order reference in callback.")
815
+ async with async_session() as session:
816
+ result = await session.execute(
817
+ Order.__table__.select().where(Order.order_id == order_id)
818
+ )
819
+ order = result.scalar_one_or_none()
820
+ if order:
821
+ order.status = new_status
822
+ await session.commit()
823
+ await log_order_tracking(order_id, "Payment Confirmed", f"Payment status updated to {new_status}.")
824
+ await asyncio.to_thread(send_whatsapp_message, MANAGEMENT_WHATSAPP_NUMBER,
825
+ f"Payment Update:\nOrder ID: {order_id} is now {new_status}."
826
+ )
827
+ return JSONResponse(content={"message": "Order updated successfully."})
828
+ else:
829
+ raise HTTPException(status_code=404, detail="Order not found.")
 
 
 
 
 
830
 
831
  @app.get("/track_order/{order_id}")
832
  async def track_order(order_id: str):