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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +155 -21
app.py CHANGED
@@ -79,8 +79,8 @@ async def init_db():
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
 
85
  menu_items = [
86
  {"name": "Jollof Rice", "description": "A spicy and flavorful rice dish", "price": 1500, "nutrition": "Calories: 300 kcal, Carbs: 50g, Protein: 10g, Fat: 5g"},
@@ -213,7 +213,7 @@ 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
@@ -232,7 +232,6 @@ def process_order_flow(user_id: str, message: str) -> str:
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.
@@ -248,7 +247,6 @@ def process_order_flow(user_id: str, message: str) -> str:
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:
@@ -259,7 +257,6 @@ def process_order_flow(user_id: str, message: str) -> str:
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:
@@ -286,8 +283,7 @@ def process_order_flow(user_id: str, message: str) -> str:
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)
@@ -307,13 +303,9 @@ def process_order_flow(user_id: str, message: str) -> str:
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]
@@ -379,6 +371,16 @@ async def chatbot_response(request: Request, background_tasks: BackgroundTasks):
379
  if not user_id:
380
  raise HTTPException(status_code=400, detail="Missing user_id in payload.")
381
 
 
 
 
 
 
 
 
 
 
 
382
  background_tasks.add_task(log_chat_to_db, user_id, "inbound", user_message)
383
  await update_user_last_interaction(user_id)
384
  await get_or_create_user_profile(user_id, phone_number)
@@ -401,11 +403,15 @@ async def chatbot_response(request: Request, background_tasks: BackgroundTasks):
401
  order_response = process_order_flow(user_id, user_message)
402
  if order_response:
403
  background_tasks.add_task(log_chat_to_db, user_id, "outbound", order_response)
 
 
 
 
 
404
  return JSONResponse(content={"response": sentiment_modifier + order_response})
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 = []
@@ -428,6 +434,11 @@ async def chatbot_response(request: Request, background_tasks: BackgroundTasks):
428
  )
429
  }
430
  background_tasks.add_task(log_chat_to_db, user_id, "outbound", str(response_payload))
 
 
 
 
 
431
  return JSONResponse(content=response_payload)
432
 
433
  # --- Dish Selection via Menu ---
@@ -444,7 +455,6 @@ async def chatbot_response(request: Request, background_tasks: BackgroundTasks):
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
@@ -453,10 +463,20 @@ async def chatbot_response(request: Request, background_tasks: BackgroundTasks):
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})
457
  else:
458
  response_text = "Sorry, I couldn't find that dish in the menu. Please try again."
459
  background_tasks.add_task(log_chat_to_db, user_id, "outbound", response_text)
 
 
 
 
 
460
  return JSONResponse(content={"response": sentiment_modifier + response_text})
461
 
462
  # --- Nutritional Facts ---
@@ -468,19 +488,133 @@ async def chatbot_response(request: Request, background_tasks: BackgroundTasks):
468
  else:
469
  response_text = f"Sorry, I couldn't find nutritional facts for {dish_name}."
470
  background_tasks.add_task(log_chat_to_db, user_id, "outbound", response_text)
 
 
 
 
 
471
  return JSONResponse(content={"response": sentiment_modifier + response_text})
472
 
473
- # --- Fallback: LLM Response Streaming ---
474
- prompt = f"User query: {user_message}\nGenerate a helpful, personalized response for a restaurant chatbot."
 
 
 
475
  def stream_response():
476
  for chunk in stream_text_completion(prompt):
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
486
- uvicorn.run(app, host="0.0.0.0", port=8000)
 
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 = {} # { user_id: [ { "timestamp": ..., "role": "user"/"bot", "message": ... }, ... ] }
83
+ proactive_timer = {}
84
 
85
  menu_items = [
86
  {"name": "Jollof Rice", "description": "A spicy and flavorful rice dish", "price": 1500, "nutrition": "Calories: 300 kcal, Carbs: 50g, Protein: 10g, Fat: 5g"},
 
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 not provided
217
  - In step 3, finalizes the order and creates a payment link
218
  """
219
  # Retrieve or initialize conversation state
 
232
  user_state[user_id] = state
233
  if message.lower() == "order":
234
  return "Sure! What dish would you like to order?"
 
235
  return ""
236
 
237
  # If no state exists but the message includes "order", start the order flow.
 
247
  state.update_last_active()
248
  # --- Step 1: Expecting Dish Selection (and optionally quantity) ---
249
  if state.step == 1:
 
250
  dish_candidates = [item["name"] for item in menu_items]
251
  found_dish = None
252
  for dish in dish_candidates:
 
257
  numbers = re.findall(r'\d+', message)
258
  if found_dish:
259
  state.data["dish"] = found_dish
 
260
  if numbers:
261
  quantity = int(numbers[0])
262
  if quantity <= 0:
 
283
  if state.step == 3:
284
  order_id = f"ORD-{int(time.time())}"
285
  state.data["order_id"] = order_id
286
+ price_per_serving = 1500 # fixed price per serving for demonstration
 
287
  quantity = state.data.get("quantity", 1)
288
  total_price = quantity * price_per_serving
289
  state.data["price"] = str(total_price)
 
303
  await session.commit()
304
  asyncio.create_task(save_order())
305
 
306
+ email = "customer@example.com" # Placeholder; retrieve from profile if available
 
307
  payment_data = create_paystack_payment_link(email, total_price * 100, order_id)
 
 
308
  dish_name = state.data.get("dish", "")
 
309
  state.reset()
310
  if user_id in user_state:
311
  del user_state[user_id]
 
371
  if not user_id:
372
  raise HTTPException(status_code=400, detail="Missing user_id in payload.")
373
 
374
+ # Initialize conversation context for the user if not present.
375
+ if user_id not in conversation_context:
376
+ conversation_context[user_id] = []
377
+ # Append the inbound message to the conversation context.
378
+ conversation_context[user_id].append({
379
+ "timestamp": datetime.utcnow().isoformat(),
380
+ "role": "user",
381
+ "message": user_message
382
+ })
383
+
384
  background_tasks.add_task(log_chat_to_db, user_id, "inbound", user_message)
385
  await update_user_last_interaction(user_id)
386
  await get_or_create_user_profile(user_id, phone_number)
 
403
  order_response = process_order_flow(user_id, user_message)
404
  if order_response:
405
  background_tasks.add_task(log_chat_to_db, user_id, "outbound", order_response)
406
+ conversation_context[user_id].append({
407
+ "timestamp": datetime.utcnow().isoformat(),
408
+ "role": "bot",
409
+ "message": order_response
410
+ })
411
  return JSONResponse(content={"response": sentiment_modifier + order_response})
412
 
413
  # --- Menu Display ---
414
  if "menu" in user_message.lower():
 
415
  if user_id in user_state:
416
  del user_state[user_id]
417
  menu_with_images = []
 
434
  )
435
  }
436
  background_tasks.add_task(log_chat_to_db, user_id, "outbound", str(response_payload))
437
+ conversation_context[user_id].append({
438
+ "timestamp": datetime.utcnow().isoformat(),
439
+ "role": "bot",
440
+ "message": response_payload["response"]
441
+ })
442
  return JSONResponse(content=response_payload)
443
 
444
  # --- Dish Selection via Menu ---
 
455
  selected_dish = item["name"]
456
  break
457
  if selected_dish:
 
458
  state = ConversationState()
459
  state.flow = "order"
460
  state.step = 1
 
463
  user_state[user_id] = state
464
  response_text = f"You selected {selected_dish}. How many servings would you like?"
465
  background_tasks.add_task(log_chat_to_db, user_id, "outbound", response_text)
466
+ conversation_context[user_id].append({
467
+ "timestamp": datetime.utcnow().isoformat(),
468
+ "role": "bot",
469
+ "message": response_text
470
+ })
471
  return JSONResponse(content={"response": sentiment_modifier + response_text})
472
  else:
473
  response_text = "Sorry, I couldn't find that dish in the menu. Please try again."
474
  background_tasks.add_task(log_chat_to_db, user_id, "outbound", response_text)
475
+ conversation_context[user_id].append({
476
+ "timestamp": datetime.utcnow().isoformat(),
477
+ "role": "bot",
478
+ "message": response_text
479
+ })
480
  return JSONResponse(content={"response": sentiment_modifier + response_text})
481
 
482
  # --- Nutritional Facts ---
 
488
  else:
489
  response_text = f"Sorry, I couldn't find nutritional facts for {dish_name}."
490
  background_tasks.add_task(log_chat_to_db, user_id, "outbound", response_text)
491
+ conversation_context[user_id].append({
492
+ "timestamp": datetime.utcnow().isoformat(),
493
+ "role": "bot",
494
+ "message": response_text
495
+ })
496
  return JSONResponse(content={"response": sentiment_modifier + response_text})
497
 
498
+ # --- Fallback: LLM Response Streaming with Conversation Context ---
499
+ # Build recent conversation history (e.g., last 5 messages)
500
+ recent_context = conversation_context.get(user_id, [])[-5:]
501
+ context_str = "\n".join([f"{entry['role'].capitalize()}: {entry['message']}" for entry in recent_context])
502
+ prompt = f"Conversation context:\n{context_str}\nUser query: {user_message}\nGenerate a helpful, personalized response for a restaurant chatbot."
503
  def stream_response():
504
  for chunk in stream_text_completion(prompt):
505
  yield chunk
506
+ fallback_log = f"LLM fallback response for prompt: {prompt}"
507
+ background_tasks.add_task(log_chat_to_db, user_id, "outbound", fallback_log)
508
+ # (Optionally, you might want to capture and add the streamed response to conversation_context)
509
  return StreamingResponse(stream_response(), media_type="text/plain")
510
 
511
+ # --- Chat History Endpoint ---
512
+ @app.get("/chat_history/{user_id}")
513
+ async def get_chat_history(user_id: str):
514
+ """
515
+ Retrieve chat history for a user.
516
+ """
517
+ async with async_session() as session:
518
+ result = await session.execute(
519
+ ChatHistory.__table__.select().where(ChatHistory.user_id == user_id)
520
+ )
521
+ history = result.fetchall()
522
+ return [dict(row) for row in history]
523
+
524
+ # --- Order Details Endpoint ---
525
+ @app.get("/order/{order_id}")
526
+ async def get_order(order_id: str):
527
+ """
528
+ Retrieve details for a specific order.
529
+ """
530
+ async with async_session() as session:
531
+ result = await session.execute(
532
+ Order.__table__.select().where(Order.order_id == order_id)
533
+ )
534
+ order = result.fetchone()
535
+ if order:
536
+ return dict(order)
537
+ else:
538
+ raise HTTPException(status_code=404, detail="Order not found.")
539
+
540
+ # --- User Profile Endpoint ---
541
+ @app.get("/user_profile/{user_id}")
542
+ async def get_user_profile(user_id: str):
543
+ """
544
+ Retrieve the user profile.
545
+ """
546
+ profile = await get_or_create_user_profile(user_id)
547
+ return {
548
+ "user_id": profile.user_id,
549
+ "phone_number": profile.phone_number,
550
+ "name": profile.name,
551
+ "email": profile.email,
552
+ "preferences": profile.preferences,
553
+ "last_interaction": profile.last_interaction.isoformat()
554
+ }
555
+
556
+ # --- Analytics Endpoint ---
557
+ @app.get("/analytics")
558
+ async def get_analytics():
559
+ """
560
+ Simple analytics dashboard endpoint.
561
+ Returns counts of messages, orders, and average sentiment.
562
+ """
563
+ async with async_session() as session:
564
+ # Total messages count
565
+ msg_result = await session.execute(ChatHistory.__table__.count())
566
+ total_messages = msg_result.scalar() or 0
567
+
568
+ # Total orders count
569
+ order_result = await session.execute(Order.__table__.count())
570
+ total_orders = order_result.scalar() or 0
571
+
572
+ # Average sentiment score
573
+ sentiment_result = await session.execute("SELECT AVG(sentiment_score) FROM sentiment_logs")
574
+ avg_sentiment = sentiment_result.scalar() or 0
575
+
576
+ return {
577
+ "total_messages": total_messages,
578
+ "total_orders": total_orders,
579
+ "average_sentiment": avg_sentiment
580
+ }
581
+
582
+ # --- Voice Integration Endpoint ---
583
+ @app.post("/voice")
584
+ async def process_voice(file: UploadFile = File(...)):
585
+ """
586
+ Accept a voice file upload, perform speech-to-text (simulated), and process the resulting text.
587
+ In production, integrate with a real STT service.
588
+ """
589
+ contents = await file.read()
590
+ # Simulated Speech-to-Text: In real implementation, send `contents` to an STT service.
591
+ simulated_text = "Simulated speech-to-text conversion result."
592
+ return {"transcription": simulated_text}
593
+
594
+ # --- Payment Callback Endpoint (Stub) ---
595
+ @app.post("/payment_callback")
596
+ async def payment_callback(request: Request):
597
+ """
598
+ Endpoint to handle payment callbacks from Paystack.
599
+ Update order status based on callback data.
600
+ """
601
+ data = await request.json()
602
+ # Extract order reference and update order status accordingly.
603
+ # In production, verify callback signature and extract data.
604
+ order_id = data.get("reference")
605
+ new_status = data.get("status", "Paid")
606
+ async with async_session() as session:
607
+ result = await session.execute(
608
+ Order.__table__.select().where(Order.order_id == order_id)
609
+ )
610
+ order = result.scalar_one_or_none()
611
+ if order:
612
+ order.status = new_status
613
+ await session.commit()
614
+ return JSONResponse(content={"message": "Order updated successfully."})
615
+ else:
616
+ raise HTTPException(status_code=404, detail="Order not found.")
617
 
618
  if __name__ == "__main__":
619
  import uvicorn
620
+ uvicorn.run(app, host="0.0.0.0", port=8000)