Fred808 commited on
Commit
928a7cf
·
verified ·
1 Parent(s): 18d8b56

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +142 -71
app.py CHANGED
@@ -3,6 +3,7 @@ import os
3
  import time
4
  import requests
5
  import base64
 
6
  from datetime import datetime, timedelta
7
  from bs4 import BeautifulSoup
8
  from sqlalchemy import select
@@ -76,7 +77,8 @@ async def init_db():
76
  await conn.run_sync(Base.metadata.create_all)
77
 
78
  # --- Global In-Memory Stores ---
79
- user_state = {} # e.g., { user_id: { "flow": str, "step": int, "data": dict, "last_active": datetime } }
 
80
  conversation_context = {}
81
  proactive_timer = {}
82
 
@@ -87,6 +89,28 @@ menu_items = [
87
  {"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"}
88
  ]
89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  # --- Utility Functions ---
91
  async def log_chat_to_db(user_id: str, direction: str, message: str):
92
  async with async_session() as session:
@@ -186,79 +210,121 @@ def stream_image_completion(image_b64: str):
186
 
187
  # --- Advanced Internal Flow: Order Processing & Payment Integration ---
188
  def process_order_flow(user_id: str, message: str) -> str:
189
- # If user types a command that should override any active order flow:
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  if message.lower() in ["order", "menu"]:
191
- if user_id in user_state:
192
- del user_state[user_id]
 
 
 
193
  if message.lower() == "order":
194
- user_state[user_id] = {"flow": "order", "step": 1, "data": {}, "last_active": datetime.utcnow()}
195
  return "Sure! What dish would you like to order?"
196
- # For "menu", we return empty to allow the menu-handling code to run.
197
  return ""
198
 
199
- if user_id in user_state:
200
- state = user_state[user_id]
201
- flow = state.get("flow")
202
- step = state.get("step")
203
- data = state.get("data", {})
204
- if flow == "order":
205
- if step == 1:
206
- # Validate dish name (exact match is used)
207
- dish_name = message.title()
208
- dish = next((item for item in menu_items if item["name"].lower() == dish_name.lower()), None)
209
- if dish:
210
- data["dish"] = dish_name
211
- state["step"] = 2
212
- return f"You selected {data['dish']}. How many servings would you like?"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  else:
214
- return f"Sorry, we don't have {dish_name} on the menu. Please choose another dish."
215
- elif step == 2:
216
- # Extract the first number from the message (e.g., '1 servings' becomes 1)
217
- numbers = re.findall(r'\d+', message)
218
- if not numbers:
219
- return "Please enter a valid number for the quantity (e.g., 1, 2, 3)."
220
- quantity = int(numbers[0])
221
- if quantity <= 0:
222
- return "Please enter a valid quantity (e.g., 1, 2, 3)."
223
- data["quantity"] = quantity
224
- order_id = f"ORD-{int(time.time())}"
225
- data["order_id"] = order_id
226
- price_per_serving = 1500 # ₦1500 per serving
227
- total_price = quantity * price_per_serving
228
- data["price"] = str(total_price)
229
-
230
- # Save order asynchronously
231
- import asyncio
232
- async def save_order():
233
- async with async_session() as session:
234
- order = Order(
235
- order_id=order_id,
236
- user_id=user_id,
237
- dish=data["dish"],
238
- quantity=str(quantity),
239
- price=str(total_price),
240
- status="Pending Payment"
241
- )
242
- session.add(order)
243
- await session.commit()
244
- asyncio.create_task(save_order())
245
-
246
- # Clear conversation state for order flow.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  del user_state[user_id]
248
 
249
- # Retrieve email from user profile if available (using a placeholder here)
250
- email = "customer@example.com"
251
- payment_data = create_paystack_payment_link(email, total_price * 100, order_id)
252
- if payment_data.get("status"):
253
- payment_link = payment_data["data"]["authorization_url"]
254
- return (f"Thank you for your order of {data['quantity']} serving(s) of {data['dish']}! "
255
- f"Your Order ID is {order_id}.\nPlease complete payment here: {payment_link}")
256
- else:
257
- return f"Your order has been placed with Order ID {order_id}, but we could not initialize payment. Please try again later."
258
- else:
259
- if "order" in message.lower():
260
- user_state[user_id] = {"flow": "order", "step": 1, "data": {}, "last_active": datetime.utcnow()}
261
- return "Sure! What dish would you like to order?"
262
  return ""
263
 
264
  # --- User Profile Functions ---
@@ -339,7 +405,7 @@ async def chatbot_response(request: Request, background_tasks: BackgroundTasks):
339
 
340
  # --- Menu Display ---
341
  if "menu" in user_message.lower():
342
- # Clear any active order flow if user explicitly asks for menu.
343
  if user_id in user_state:
344
  del user_state[user_id]
345
  menu_with_images = []
@@ -378,8 +444,13 @@ async def chatbot_response(request: Request, background_tasks: BackgroundTasks):
378
  selected_dish = item["name"]
379
  break
380
  if selected_dish:
381
- # Trigger a new order flow
382
- user_state[user_id] = {"flow": "order", "step": 1, "data": {"dish": selected_dish}, "last_active": datetime.utcnow()}
 
 
 
 
 
383
  response_text = f"You selected {selected_dish}. How many servings would you like?"
384
  background_tasks.add_task(log_chat_to_db, user_id, "outbound", response_text)
385
  return JSONResponse(content={"response": sentiment_modifier + response_text})
@@ -406,9 +477,9 @@ async def chatbot_response(request: Request, background_tasks: BackgroundTasks):
406
  yield chunk
407
  background_tasks.add_task(log_chat_to_db, user_id, "outbound", f"LLM fallback response for prompt: {prompt}")
408
  return StreamingResponse(stream_response(), media_type="text/plain")
409
-
410
  # --- Other Endpoints (Chat History, Order Details, User Profile, Analytics, Voice, Payment Callback) ---
411
- # ... (unchanged for brevity) ...
412
 
413
  if __name__ == "__main__":
414
  import uvicorn
 
3
  import time
4
  import requests
5
  import base64
6
+ import asyncio
7
  from datetime import datetime, timedelta
8
  from bs4 import BeautifulSoup
9
  from sqlalchemy import select
 
77
  await conn.run_sync(Base.metadata.create_all)
78
 
79
  # --- Global In-Memory Stores ---
80
+ # Instead of a plain dict for conversation context, we use a dedicated class below.
81
+ user_state = {} # e.g., { user_id: ConversationState }
82
  conversation_context = {}
83
  proactive_timer = {}
84
 
 
89
  {"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"}
90
  ]
91
 
92
+ # --- Conversation State Management ---
93
+ SESSION_TIMEOUT = timedelta(minutes=5)
94
+
95
+ class ConversationState:
96
+ def __init__(self):
97
+ self.flow = None # e.g., "order"
98
+ self.step = 0
99
+ self.data = {}
100
+ self.last_active = datetime.utcnow()
101
+
102
+ def update_last_active(self):
103
+ self.last_active = datetime.utcnow()
104
+
105
+ def is_expired(self):
106
+ return datetime.utcnow() - self.last_active > SESSION_TIMEOUT
107
+
108
+ def reset(self):
109
+ self.flow = None
110
+ self.step = 0
111
+ self.data = {}
112
+ self.last_active = datetime.utcnow()
113
+
114
  # --- Utility Functions ---
115
  async def log_chat_to_db(user_id: str, direction: str, message: str):
116
  async with async_session() as session:
 
210
 
211
  # --- Advanced Internal Flow: Order Processing & Payment Integration ---
212
  def process_order_flow(user_id: str, message: str) -> str:
213
+ """
214
+ Implements an FSM-based order flow that:
215
+ - In step 1, expects the user to mention a dish name (optionally with quantity)
216
+ - In step 2, asks for quantity if it was not provided
217
+ - In step 3, finalizes the order and creates a payment link
218
+ """
219
+ # Retrieve or initialize conversation state
220
+ state = user_state.get(user_id)
221
+ if state and state.is_expired():
222
+ state.reset()
223
+ del user_state[user_id]
224
+ state = None
225
+
226
+ # If the user explicitly types "order" or "menu", (re)start the order flow.
227
  if message.lower() in ["order", "menu"]:
228
+ state = ConversationState()
229
+ state.flow = "order"
230
+ state.step = 1
231
+ state.update_last_active()
232
+ user_state[user_id] = state
233
  if message.lower() == "order":
 
234
  return "Sure! What dish would you like to order?"
235
+ # For "menu", let the menu handler run by returning empty.
236
  return ""
237
 
238
+ # If no state exists but the message includes "order", start the order flow.
239
+ if not state and "order" in message.lower():
240
+ state = ConversationState()
241
+ state.flow = "order"
242
+ state.step = 1
243
+ state.update_last_active()
244
+ user_state[user_id] = state
245
+ return "Sure! What dish would you like to order?"
246
+
247
+ if state and state.flow == "order":
248
+ state.update_last_active()
249
+ # --- Step 1: Expecting Dish Selection (and optionally quantity) ---
250
+ if state.step == 1:
251
+ # Look for a dish name in the message
252
+ dish_candidates = [item["name"] for item in menu_items]
253
+ found_dish = None
254
+ for dish in dish_candidates:
255
+ if dish.lower() in message.lower():
256
+ found_dish = dish
257
+ break
258
+
259
+ numbers = re.findall(r'\d+', message)
260
+ if found_dish:
261
+ state.data["dish"] = found_dish
262
+ # If a quantity is provided, move to finalizing the order directly.
263
+ if numbers:
264
+ quantity = int(numbers[0])
265
+ if quantity <= 0:
266
+ return "Please enter a valid quantity (e.g., 1, 2, 3)."
267
+ state.data["quantity"] = quantity
268
+ state.step = 3 # ready to finalize the order
269
  else:
270
+ state.step = 2 # ask for quantity
271
+ else:
272
+ return "I couldn't identify the dish. Please type the dish name from our menu."
273
+
274
+ # --- Step 2: Asking for Quantity ---
275
+ if state.step == 2:
276
+ numbers = re.findall(r'\d+', message)
277
+ if not numbers:
278
+ return "Please enter a valid number for the quantity (e.g., 1, 2, 3)."
279
+ quantity = int(numbers[0])
280
+ if quantity <= 0:
281
+ return "Please enter a valid quantity (e.g., 1, 2, 3)."
282
+ state.data["quantity"] = quantity
283
+ state.step = 3
284
+
285
+ # --- Step 3: Finalize Order ---
286
+ if state.step == 3:
287
+ order_id = f"ORD-{int(time.time())}"
288
+ state.data["order_id"] = order_id
289
+ # Use a fixed price per serving for demonstration purposes.
290
+ price_per_serving = 1500
291
+ quantity = state.data.get("quantity", 1)
292
+ total_price = quantity * price_per_serving
293
+ state.data["price"] = str(total_price)
294
+
295
+ # Save the order asynchronously
296
+ async def save_order():
297
+ async with async_session() as session:
298
+ order = Order(
299
+ order_id=order_id,
300
+ user_id=user_id,
301
+ dish=state.data["dish"],
302
+ quantity=str(quantity),
303
+ price=str(total_price),
304
+ status="Pending Payment"
305
+ )
306
+ session.add(order)
307
+ await session.commit()
308
+ asyncio.create_task(save_order())
309
+
310
+ # Retrieve email from user profile if available (placeholder used here)
311
+ email = "customer@example.com"
312
+ payment_data = create_paystack_payment_link(email, total_price * 100, order_id)
313
+
314
+ # Before returning, capture the dish name for the response
315
+ dish_name = state.data.get("dish", "")
316
+ # Clear the conversation state after finalizing the order.
317
+ state.reset()
318
+ if user_id in user_state:
319
  del user_state[user_id]
320
 
321
+ if payment_data.get("status"):
322
+ payment_link = payment_data["data"]["authorization_url"]
323
+ return (f"Thank you for your order of {quantity} serving(s) of {dish_name}! "
324
+ f"Your Order ID is {order_id}.\nPlease complete payment here: {payment_link}")
325
+ else:
326
+ return f"Your order has been placed with Order ID {order_id}, but we could not initialize payment. Please try again later."
327
+
 
 
 
 
 
 
328
  return ""
329
 
330
  # --- User Profile Functions ---
 
405
 
406
  # --- Menu Display ---
407
  if "menu" in user_message.lower():
408
+ # Clear any active order flow if user explicitly asks for the menu.
409
  if user_id in user_state:
410
  del user_state[user_id]
411
  menu_with_images = []
 
444
  selected_dish = item["name"]
445
  break
446
  if selected_dish:
447
+ # Start a new order flow with the selected dish.
448
+ state = ConversationState()
449
+ state.flow = "order"
450
+ state.step = 1
451
+ state.data["dish"] = selected_dish
452
+ state.update_last_active()
453
+ user_state[user_id] = state
454
  response_text = f"You selected {selected_dish}. How many servings would you like?"
455
  background_tasks.add_task(log_chat_to_db, user_id, "outbound", response_text)
456
  return JSONResponse(content={"response": sentiment_modifier + response_text})
 
477
  yield chunk
478
  background_tasks.add_task(log_chat_to_db, user_id, "outbound", f"LLM fallback response for prompt: {prompt}")
479
  return StreamingResponse(stream_response(), media_type="text/plain")
480
+
481
  # --- Other Endpoints (Chat History, Order Details, User Profile, Analytics, Voice, Payment Callback) ---
482
+ # ... (Implement other endpoints as needed) ...
483
 
484
  if __name__ == "__main__":
485
  import uvicorn