Spaces:
Paused
Paused
Update app.py
Browse files
app.py
CHANGED
|
@@ -7,8 +7,6 @@ import asyncio
|
|
| 7 |
from datetime import datetime, timedelta
|
| 8 |
from bs4 import BeautifulSoup
|
| 9 |
from sqlalchemy import select
|
| 10 |
-
import ssl
|
| 11 |
-
from sqlalchemy.ext.asyncio import create_async_engine
|
| 12 |
|
| 13 |
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks, UploadFile, File, Form
|
| 14 |
from fastapi.responses import JSONResponse, StreamingResponse, RedirectResponse
|
|
@@ -23,10 +21,10 @@ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
| 23 |
from sqlalchemy.orm import sessionmaker, declarative_base
|
| 24 |
from sqlalchemy import Column, Integer, String, DateTime, Text, Float
|
| 25 |
|
| 26 |
-
# Environment Variables
|
| 27 |
SPOONACULAR_API_KEY = os.getenv("SPOONACULAR_API_KEY", "default_fallback_value")
|
| 28 |
PAYSTACK_SECRET_KEY = os.getenv("PAYSTACK_SECRET_KEY", "default_fallback_value")
|
| 29 |
-
DATABASE_URL = os.getenv("DATABASE_URL", "default_fallback_value")
|
| 30 |
NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY", "default_fallback_value")
|
| 31 |
openai.api_key = os.getenv("OPENAI_API_KEY", "default_fallback_value")
|
| 32 |
|
|
@@ -35,6 +33,7 @@ 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 |
Base = declarative_base()
|
| 39 |
|
| 40 |
class ChatHistory(Base):
|
|
@@ -50,10 +49,12 @@ class Order(Base):
|
|
| 50 |
id = Column(Integer, primary_key=True, index=True)
|
| 51 |
order_id = Column(String, unique=True, index=True)
|
| 52 |
user_id = Column(String, index=True)
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
status = Column(String, default="Pending")
|
|
|
|
|
|
|
| 57 |
timestamp = Column(DateTime, default=datetime.utcnow)
|
| 58 |
|
| 59 |
class UserProfile(Base):
|
|
@@ -65,6 +66,8 @@ class UserProfile(Base):
|
|
| 65 |
email = Column(String, default="unknown@example.com")
|
| 66 |
preferences = Column(Text, default="")
|
| 67 |
last_interaction = Column(DateTime, default=datetime.utcnow)
|
|
|
|
|
|
|
| 68 |
|
| 69 |
class SentimentLog(Base):
|
| 70 |
__tablename__ = "sentiment_logs"
|
|
@@ -74,7 +77,23 @@ class SentimentLog(Base):
|
|
| 74 |
sentiment_score = Column(Float)
|
| 75 |
message = Column(Text)
|
| 76 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
|
|
|
| 78 |
engine = create_async_engine(DATABASE_URL, echo=True)
|
| 79 |
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
| 80 |
|
|
@@ -83,30 +102,9 @@ async def init_db():
|
|
| 83 |
await conn.run_sync(Base.metadata.create_all)
|
| 84 |
|
| 85 |
# --- Global In-Memory Stores ---
|
| 86 |
-
user_state = {}
|
| 87 |
conversation_context = {} # { user_id: [ { "timestamp": ..., "role": "user"/"bot", "message": ... }, ... ] }
|
| 88 |
-
|
| 89 |
-
# --- Conversation State Management ---
|
| 90 |
-
SESSION_TIMEOUT = timedelta(minutes=5)
|
| 91 |
-
|
| 92 |
-
class ConversationState:
|
| 93 |
-
def __init__(self):
|
| 94 |
-
self.flow = None # e.g., "track_order", "schedule_delivery"
|
| 95 |
-
self.step = 0
|
| 96 |
-
self.data = {}
|
| 97 |
-
self.last_active = datetime.utcnow()
|
| 98 |
-
|
| 99 |
-
def update_last_active(self):
|
| 100 |
-
self.last_active = datetime.utcnow()
|
| 101 |
-
|
| 102 |
-
def is_expired(self):
|
| 103 |
-
return datetime.utcnow() - self.last_active > SESSION_TIMEOUT
|
| 104 |
-
|
| 105 |
-
def reset(self):
|
| 106 |
-
self.flow = None
|
| 107 |
-
self.step = 0
|
| 108 |
-
self.data = {}
|
| 109 |
-
self.last_active = datetime.utcnow()
|
| 110 |
|
| 111 |
# --- Utility Functions ---
|
| 112 |
async def log_chat_to_db(user_id: str, direction: str, message: str):
|
|
@@ -125,84 +123,137 @@ def analyze_sentiment(text: str) -> float:
|
|
| 125 |
blob = TextBlob(text)
|
| 126 |
return blob.sentiment.polarity
|
| 127 |
|
| 128 |
-
# ---
|
| 129 |
-
def
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
else:
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
""
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
""
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
|
| 207 |
# --- FastAPI Setup & Endpoints ---
|
| 208 |
app = FastAPI()
|
|
@@ -215,53 +266,160 @@ async def on_startup():
|
|
| 215 |
async def chatbot_response(request: Request, background_tasks: BackgroundTasks):
|
| 216 |
data = await request.json()
|
| 217 |
user_id = data.get("user_id")
|
|
|
|
| 218 |
user_message = data.get("message", "").strip()
|
|
|
|
|
|
|
| 219 |
|
| 220 |
if not user_id:
|
| 221 |
raise HTTPException(status_code=400, detail="Missing user_id in payload.")
|
| 222 |
|
| 223 |
-
# Initialize conversation context if not present
|
| 224 |
if user_id not in conversation_context:
|
| 225 |
conversation_context[user_id] = []
|
| 226 |
-
|
| 227 |
-
# Append the inbound message to the conversation context
|
| 228 |
conversation_context[user_id].append({
|
| 229 |
"timestamp": datetime.utcnow().isoformat(),
|
| 230 |
"role": "user",
|
| 231 |
"message": user_message
|
| 232 |
})
|
| 233 |
|
| 234 |
-
# Log the chat to the database
|
| 235 |
background_tasks.add_task(log_chat_to_db, user_id, "inbound", user_message)
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
elif
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
|
| 266 |
# --- Other Endpoints (Chat History, Order Details, User Profile, Analytics, Voice, Payment Callback) ---
|
| 267 |
@app.get("/chat_history/{user_id}")
|
|
@@ -338,12 +496,14 @@ async def payment_callback(request: Request):
|
|
| 338 |
await session.commit()
|
| 339 |
else:
|
| 340 |
raise HTTPException(status_code=404, detail="Order not found.")
|
|
|
|
|
|
|
| 341 |
# Notify management via WhatsApp about the payment update
|
| 342 |
await asyncio.to_thread(send_whatsapp_message, MANAGEMENT_WHATSAPP_NUMBER,
|
| 343 |
f"Payment Update:\nOrder ID: {order_id} is now {status}."
|
| 344 |
)
|
| 345 |
# Redirect user back to the chat interface (adjust URL as needed)
|
| 346 |
-
redirect_url = f"https://
|
| 347 |
return RedirectResponse(url=redirect_url)
|
| 348 |
# POST: Server-to-server callback from Paystack
|
| 349 |
else:
|
|
@@ -360,12 +520,37 @@ async def payment_callback(request: Request):
|
|
| 360 |
if order:
|
| 361 |
order.status = new_status
|
| 362 |
await session.commit()
|
|
|
|
| 363 |
await asyncio.to_thread(send_whatsapp_message, MANAGEMENT_WHATSAPP_NUMBER,
|
| 364 |
f"Payment Update:\nOrder ID: {order_id} is now {new_status}."
|
| 365 |
)
|
| 366 |
return JSONResponse(content={"message": "Order updated successfully."})
|
| 367 |
else:
|
| 368 |
raise HTTPException(status_code=404, detail="Order not found.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
|
| 370 |
if __name__ == "__main__":
|
| 371 |
import uvicorn
|
|
|
|
| 7 |
from datetime import datetime, timedelta
|
| 8 |
from bs4 import BeautifulSoup
|
| 9 |
from sqlalchemy import select
|
|
|
|
|
|
|
| 10 |
|
| 11 |
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks, UploadFile, File, Form
|
| 12 |
from fastapi.responses import JSONResponse, StreamingResponse, RedirectResponse
|
|
|
|
| 21 |
from sqlalchemy.orm import sessionmaker, declarative_base
|
| 22 |
from sqlalchemy import Column, Integer, String, DateTime, Text, Float
|
| 23 |
|
| 24 |
+
# --- Environment Variables and API Keys ---
|
| 25 |
SPOONACULAR_API_KEY = os.getenv("SPOONACULAR_API_KEY", "default_fallback_value")
|
| 26 |
PAYSTACK_SECRET_KEY = os.getenv("PAYSTACK_SECRET_KEY", "default_fallback_value")
|
| 27 |
+
DATABASE_URL = os.getenv("DATABASE_URL", "default_fallback_value") # Example using SQLite
|
| 28 |
NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY", "default_fallback_value")
|
| 29 |
openai.api_key = os.getenv("OPENAI_API_KEY", "default_fallback_value")
|
| 30 |
|
|
|
|
| 33 |
WHATSAPP_ACCESS_TOKEN = os.getenv("WHATSAPP_ACCESS_TOKEN", "default_value")
|
| 34 |
MANAGEMENT_WHATSAPP_NUMBER = os.getenv("MANAGEMENT_WHATSAPP_NUMBER", "default_value")
|
| 35 |
|
| 36 |
+
# --- Database Setup ---
|
| 37 |
Base = declarative_base()
|
| 38 |
|
| 39 |
class ChatHistory(Base):
|
|
|
|
| 49 |
id = Column(Integer, primary_key=True, index=True)
|
| 50 |
order_id = Column(String, unique=True, index=True)
|
| 51 |
user_id = Column(String, index=True)
|
| 52 |
+
dish = Column(String)
|
| 53 |
+
quantity = Column(String)
|
| 54 |
+
price = Column(String, default="0")
|
| 55 |
+
status = Column(String, default="Pending Payment")
|
| 56 |
+
payment_reference = Column(String, nullable=True)
|
| 57 |
+
delivery_address = Column(String, default="") # New field for address
|
| 58 |
timestamp = Column(DateTime, default=datetime.utcnow)
|
| 59 |
|
| 60 |
class UserProfile(Base):
|
|
|
|
| 66 |
email = Column(String, default="unknown@example.com")
|
| 67 |
preferences = Column(Text, default="")
|
| 68 |
last_interaction = Column(DateTime, default=datetime.utcnow)
|
| 69 |
+
loyalty_points = Column(Integer, default=0) # New field for loyalty points
|
| 70 |
+
preferred_language = Column(String, default="English") # New field for language preference
|
| 71 |
|
| 72 |
class SentimentLog(Base):
|
| 73 |
__tablename__ = "sentiment_logs"
|
|
|
|
| 77 |
sentiment_score = Column(Float)
|
| 78 |
message = Column(Text)
|
| 79 |
|
| 80 |
+
class OrderTracking(Base):
|
| 81 |
+
__tablename__ = "order_tracking"
|
| 82 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 83 |
+
order_id = Column(String, index=True)
|
| 84 |
+
status = Column(String) # e.g., "Order Placed", "Payment Confirmed", etc.
|
| 85 |
+
message = Column(Text, nullable=True) # Optional additional details
|
| 86 |
+
timestamp = Column(DateTime, default=datetime.utcnow)
|
| 87 |
+
|
| 88 |
+
class Feedback(Base):
|
| 89 |
+
__tablename__ = "feedback"
|
| 90 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 91 |
+
user_id = Column(String, index=True)
|
| 92 |
+
rating = Column(Integer)
|
| 93 |
+
comment = Column(Text, nullable=True)
|
| 94 |
+
timestamp = Column(DateTime, default=datetime.utcnow)
|
| 95 |
|
| 96 |
+
# --- Create Engine and Session ---
|
| 97 |
engine = create_async_engine(DATABASE_URL, echo=True)
|
| 98 |
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
| 99 |
|
|
|
|
| 102 |
await conn.run_sync(Base.metadata.create_all)
|
| 103 |
|
| 104 |
# --- Global In-Memory Stores ---
|
| 105 |
+
user_state = {} # e.g., { user_id: ConversationState }
|
| 106 |
conversation_context = {} # { user_id: [ { "timestamp": ..., "role": "user"/"bot", "message": ... }, ... ] }
|
| 107 |
+
proactive_timer = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
# --- Utility Functions ---
|
| 110 |
async def log_chat_to_db(user_id: str, direction: str, message: str):
|
|
|
|
| 123 |
blob = TextBlob(text)
|
| 124 |
return blob.sentiment.polarity
|
| 125 |
|
| 126 |
+
# --- New Features Implementation ---
|
| 127 |
+
async def send_main_menu(user_id: str):
|
| 128 |
+
menu_message = (
|
| 129 |
+
"Hi there! 👋 Welcome to [Delivery Service Co.]. I’m here to help with your deliveries. "
|
| 130 |
+
"What would you like to do today?"
|
| 131 |
+
)
|
| 132 |
+
quick_replies = [
|
| 133 |
+
{"title": "Track an Order", "payload": "track_order"},
|
| 134 |
+
{"title": "Schedule a Delivery", "payload": "schedule_delivery"},
|
| 135 |
+
{"title": "FAQs & Support", "payload": "faqs"},
|
| 136 |
+
{"title": "Loyalty Points", "payload": "loyalty_points"},
|
| 137 |
+
{"title": "Talk to an Agent", "payload": "live_agent"},
|
| 138 |
+
]
|
| 139 |
+
await log_chat_to_db(user_id, "outbound", menu_message)
|
| 140 |
+
return {"response": menu_message, "quick_replies": quick_replies}
|
| 141 |
+
|
| 142 |
+
async def track_order(user_id: str, order_id: str):
|
| 143 |
+
# Simulate fetching real-time tracking data
|
| 144 |
+
tracking_data = {
|
| 145 |
+
"status": "On the way",
|
| 146 |
+
"estimated_time": "30 minutes",
|
| 147 |
+
"driver_location": "https://maps.google.com/?q=6.5244,3.3792", # Example location
|
| 148 |
+
}
|
| 149 |
+
tracking_message = (
|
| 150 |
+
f"🚚 Your order ({order_id}) is currently {tracking_data['status']} and is expected to arrive in {tracking_data['estimated_time']}. "
|
| 151 |
+
f"Tap below to track your package in real-time."
|
| 152 |
+
)
|
| 153 |
+
quick_replies = [
|
| 154 |
+
{"title": "Track on Map", "url": tracking_data["driver_location"]},
|
| 155 |
+
{"title": "Back to Menu", "payload": "main_menu"},
|
| 156 |
+
]
|
| 157 |
+
await log_chat_to_db(user_id, "outbound", tracking_message)
|
| 158 |
+
return {"response": tracking_message, "quick_replies": quick_replies}
|
| 159 |
+
|
| 160 |
+
async def recommend_package(user_id: str, package_description: str):
|
| 161 |
+
# Simulate AI analysis
|
| 162 |
+
package_size = "Medium"
|
| 163 |
+
price = 2500
|
| 164 |
+
recommendation_message = (
|
| 165 |
+
f"Based on your description, we recommend a {package_size} package for ₦{price}. "
|
| 166 |
+
"Does this sound right?"
|
| 167 |
+
)
|
| 168 |
+
quick_replies = [
|
| 169 |
+
{"title": "Yes, proceed", "payload": f"confirm_package:{package_size}:{price}"},
|
| 170 |
+
{"title": "No, adjust size", "payload": "adjust_package"},
|
| 171 |
+
]
|
| 172 |
+
await log_chat_to_db(user_id, "outbound", recommendation_message)
|
| 173 |
+
return {"response": recommendation_message, "quick_replies": quick_replies}
|
| 174 |
+
|
| 175 |
+
async def check_loyalty_points(user_id: str):
|
| 176 |
+
# Simulate fetching loyalty points
|
| 177 |
+
points = 200
|
| 178 |
+
discount = 500
|
| 179 |
+
loyalty_message = (
|
| 180 |
+
f"🎉 You’ve earned 50 points for this delivery! You now have {points} points. "
|
| 181 |
+
f"Redeem them for a ₦{discount} discount on your next order."
|
| 182 |
+
)
|
| 183 |
+
quick_replies = [
|
| 184 |
+
{"title": "Redeem Points", "payload": "redeem_points"},
|
| 185 |
+
{"title": "Back to Menu", "payload": "main_menu"},
|
| 186 |
+
]
|
| 187 |
+
await log_chat_to_db(user_id, "outbound", loyalty_message)
|
| 188 |
+
return {"response": loyalty_message, "quick_replies": quick_replies}
|
| 189 |
+
|
| 190 |
+
async def send_proactive_update(user_id: str, order_id: str, status: str):
|
| 191 |
+
if status == "picked_up":
|
| 192 |
+
message = f"🚚 Your order ({order_id}) has been picked up and is on the way!"
|
| 193 |
+
elif status == "nearby":
|
| 194 |
+
message = f"🚚 Your driver is 10 minutes away! Please ensure someone is available to receive the package."
|
| 195 |
+
await log_chat_to_db(user_id, "outbound", message)
|
| 196 |
+
return {"response": message}
|
| 197 |
+
|
| 198 |
+
async def set_language(user_id: str, language: str):
|
| 199 |
+
supported_languages = ["English", "Français", "Español"]
|
| 200 |
+
if language in supported_languages:
|
| 201 |
+
user_state[user_id]["language"] = language
|
| 202 |
+
message = f"Language set to {language}. How can I assist you today?"
|
| 203 |
else:
|
| 204 |
+
message = "Sorry, that language is not supported. Please choose from: English, Français, Español."
|
| 205 |
+
quick_replies = [{"title": lang, "payload": f"set_language:{lang}"} for lang in supported_languages]
|
| 206 |
+
await log_chat_to_db(user_id, "outbound", message)
|
| 207 |
+
return {"response": message, "quick_replies": quick_replies}
|
| 208 |
+
|
| 209 |
+
async def request_feedback(user_id: str):
|
| 210 |
+
feedback_message = "How was your delivery experience? Tap to rate:"
|
| 211 |
+
quick_replies = [
|
| 212 |
+
{"title": "⭐️⭐️⭐️⭐️⭐️", "payload": "rate:5"},
|
| 213 |
+
{"title": "⭐️⭐️⭐️⭐️", "payload": "rate:4"},
|
| 214 |
+
{"title": "⭐️⭐️⭐️", "payload": "rate:3"},
|
| 215 |
+
{"title": "⭐️⭐️", "payload": "rate:2"},
|
| 216 |
+
{"title": "⭐️", "payload": "rate:1"},
|
| 217 |
+
]
|
| 218 |
+
await log_chat_to_db(user_id, "outbound", feedback_message)
|
| 219 |
+
return {"response": feedback_message, "quick_replies": quick_replies}
|
| 220 |
+
|
| 221 |
+
async def show_environmental_impact(user_id: str):
|
| 222 |
+
impact_message = "🌍 Your delivery saved 2kg of CO2 emissions! Thank you for choosing eco-friendly shipping."
|
| 223 |
+
await log_chat_to_db(user_id, "outbound", impact_message)
|
| 224 |
+
return {"response": impact_message}
|
| 225 |
+
|
| 226 |
+
async def start_onboarding(user_id: str):
|
| 227 |
+
tutorial_message = (
|
| 228 |
+
"Let me guide you through how to schedule a delivery. Tap ‘Next’ to continue."
|
| 229 |
+
)
|
| 230 |
+
quick_replies = [
|
| 231 |
+
{"title": "Next", "payload": "tutorial_step_1"},
|
| 232 |
+
{"title": "Skip Tutorial", "payload": "main_menu"},
|
| 233 |
+
]
|
| 234 |
+
await log_chat_to_db(user_id, "outbound", tutorial_message)
|
| 235 |
+
return {"response": tutorial_message, "quick_replies": quick_replies}
|
| 236 |
+
|
| 237 |
+
async def suggest_faqs(user_id: str, user_input: str):
|
| 238 |
+
# Simulate AI-powered FAQ suggestions
|
| 239 |
+
suggested_faqs = [
|
| 240 |
+
"How long does delivery take?",
|
| 241 |
+
"Can I change my delivery time?",
|
| 242 |
+
"What are your pricing options?",
|
| 243 |
+
]
|
| 244 |
+
faq_message = (
|
| 245 |
+
f"It looks like you’re asking about delivery times. Here are some related FAQs:"
|
| 246 |
+
)
|
| 247 |
+
quick_replies = [{"title": faq, "payload": f"faq:{faq}"} for faq in suggested_faqs]
|
| 248 |
+
await log_chat_to_db(user_id, "outbound", faq_message)
|
| 249 |
+
return {"response": faq_message, "quick_replies": quick_replies}
|
| 250 |
+
|
| 251 |
+
async def schedule_offline(user_id: str):
|
| 252 |
+
offline_message = (
|
| 253 |
+
"You’re offline. Your delivery has been scheduled and will be confirmed once you’re back online."
|
| 254 |
+
)
|
| 255 |
+
await log_chat_to_db(user_id, "outbound", offline_message)
|
| 256 |
+
return {"response": offline_message}
|
| 257 |
|
| 258 |
# --- FastAPI Setup & Endpoints ---
|
| 259 |
app = FastAPI()
|
|
|
|
| 266 |
async def chatbot_response(request: Request, background_tasks: BackgroundTasks):
|
| 267 |
data = await request.json()
|
| 268 |
user_id = data.get("user_id")
|
| 269 |
+
phone_number = data.get("phone_number")
|
| 270 |
user_message = data.get("message", "").strip()
|
| 271 |
+
is_image = data.get("is_image", False)
|
| 272 |
+
image_b64 = data.get("image_base64", None)
|
| 273 |
|
| 274 |
if not user_id:
|
| 275 |
raise HTTPException(status_code=400, detail="Missing user_id in payload.")
|
| 276 |
|
| 277 |
+
# Initialize conversation context for the user if not present.
|
| 278 |
if user_id not in conversation_context:
|
| 279 |
conversation_context[user_id] = []
|
| 280 |
+
# Append the inbound message to the conversation context.
|
|
|
|
| 281 |
conversation_context[user_id].append({
|
| 282 |
"timestamp": datetime.utcnow().isoformat(),
|
| 283 |
"role": "user",
|
| 284 |
"message": user_message
|
| 285 |
})
|
| 286 |
|
|
|
|
| 287 |
background_tasks.add_task(log_chat_to_db, user_id, "inbound", user_message)
|
| 288 |
+
await update_user_last_interaction(user_id)
|
| 289 |
+
await get_or_create_user_profile(user_id, phone_number)
|
| 290 |
+
|
| 291 |
+
# Handle image queries
|
| 292 |
+
if is_image and image_b64:
|
| 293 |
+
if len(image_b64) >= 180_000:
|
| 294 |
+
raise HTTPException(status_code=400, detail="Image too large.")
|
| 295 |
+
return StreamingResponse(stream_image_completion(image_b64), media_type="text/plain")
|
| 296 |
+
|
| 297 |
+
sentiment_score = analyze_sentiment(user_message)
|
| 298 |
+
background_tasks.add_task(log_sentiment, user_id, user_message, sentiment_score)
|
| 299 |
+
sentiment_modifier = ""
|
| 300 |
+
if sentiment_score < -0.3:
|
| 301 |
+
sentiment_modifier = "I'm sorry if you're having a tough time. "
|
| 302 |
+
elif sentiment_score > 0.3:
|
| 303 |
+
sentiment_modifier = "Great to hear from you! "
|
| 304 |
+
|
| 305 |
+
# --- Order Tracking Handling ---
|
| 306 |
+
order_id_match = re.search(r"ord-\d+", user_message.lower())
|
| 307 |
+
if order_id_match:
|
| 308 |
+
order_id = order_id_match.group(0)
|
| 309 |
+
try:
|
| 310 |
+
# Call the /track_order endpoint
|
| 311 |
+
tracking_response = await track_order(order_id)
|
| 312 |
+
return JSONResponse(content={"response": tracking_response})
|
| 313 |
+
except HTTPException as e:
|
| 314 |
+
return JSONResponse(content={"response": f"⚠️ {e.detail}"})
|
| 315 |
+
|
| 316 |
+
# --- Order Flow Handling ---
|
| 317 |
+
order_response = process_order_flow(user_id, user_message)
|
| 318 |
+
if order_response:
|
| 319 |
+
background_tasks.add_task(log_chat_to_db, user_id, "outbound", order_response)
|
| 320 |
+
conversation_context[user_id].append({
|
| 321 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 322 |
+
"role": "bot",
|
| 323 |
+
"message": order_response
|
| 324 |
+
})
|
| 325 |
+
return JSONResponse(content={"response": sentiment_modifier + order_response})
|
| 326 |
+
|
| 327 |
+
# --- Menu Display ---
|
| 328 |
+
if "menu" in user_message.lower():
|
| 329 |
+
if user_id in user_state:
|
| 330 |
+
del user_state[user_id]
|
| 331 |
+
menu_with_images = []
|
| 332 |
+
for index, item in enumerate(menu_items, start=1):
|
| 333 |
+
image_url = google_image_scrape(item["name"])
|
| 334 |
+
menu_with_images.append({
|
| 335 |
+
"number": index,
|
| 336 |
+
"name": item["name"],
|
| 337 |
+
"description": item["description"],
|
| 338 |
+
"price": item["price"],
|
| 339 |
+
"image_url": image_url
|
| 340 |
+
})
|
| 341 |
+
response_payload = {
|
| 342 |
+
"response": sentiment_modifier + "Here’s our delicious menu:",
|
| 343 |
+
"menu": menu_with_images,
|
| 344 |
+
"follow_up": (
|
| 345 |
+
"To order, type the *number* or *name* of the dish you'd like. "
|
| 346 |
+
"For example, type '1' or 'Jollof Rice' to order Jollof Rice.\n\n"
|
| 347 |
+
"You can also ask for nutritional facts by typing, for example, 'Nutritional facts for Jollof Rice'."
|
| 348 |
+
)
|
| 349 |
+
}
|
| 350 |
+
background_tasks.add_task(log_chat_to_db, user_id, "outbound", str(response_payload))
|
| 351 |
+
conversation_context[user_id].append({
|
| 352 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 353 |
+
"role": "bot",
|
| 354 |
+
"message": response_payload["response"]
|
| 355 |
+
})
|
| 356 |
+
return JSONResponse(content=response_payload)
|
| 357 |
+
|
| 358 |
+
# --- Dish Selection via Menu ---
|
| 359 |
+
if any(item["name"].lower() in user_message.lower() for item in menu_items) or \
|
| 360 |
+
any(str(index) == user_message.strip() for index, item in enumerate(menu_items, start=1)):
|
| 361 |
+
selected_dish = None
|
| 362 |
+
if user_message.strip().isdigit():
|
| 363 |
+
dish_number = int(user_message.strip())
|
| 364 |
+
if 1 <= dish_number <= len(menu_items):
|
| 365 |
+
selected_dish = menu_items[dish_number - 1]["name"]
|
| 366 |
+
else:
|
| 367 |
+
for item in menu_items:
|
| 368 |
+
if item["name"].lower() in user_message.lower():
|
| 369 |
+
selected_dish = item["name"]
|
| 370 |
+
break
|
| 371 |
+
if selected_dish:
|
| 372 |
+
state = ConversationState()
|
| 373 |
+
state.flow = "order"
|
| 374 |
+
# Set step to 2 since the dish is already selected
|
| 375 |
+
state.step = 2
|
| 376 |
+
state.data["dish"] = selected_dish
|
| 377 |
+
state.update_last_active()
|
| 378 |
+
user_state[user_id] = state
|
| 379 |
+
response_text = f"You selected {selected_dish}. How many servings would you like?"
|
| 380 |
+
background_tasks.add_task(log_chat_to_db, user_id, "outbound", response_text)
|
| 381 |
+
conversation_context[user_id].append({
|
| 382 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 383 |
+
"role": "bot",
|
| 384 |
+
"message": response_text
|
| 385 |
+
})
|
| 386 |
+
return JSONResponse(content={"response": sentiment_modifier + response_text})
|
| 387 |
+
else:
|
| 388 |
+
response_text = "Sorry, I couldn't find that dish in the menu. Please try again."
|
| 389 |
+
background_tasks.add_task(log_chat_to_db, user_id, "outbound", response_text)
|
| 390 |
+
conversation_context[user_id].append({
|
| 391 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 392 |
+
"role": "bot",
|
| 393 |
+
"message": response_text
|
| 394 |
+
})
|
| 395 |
+
return JSONResponse(content={"response": sentiment_modifier + response_text})
|
| 396 |
+
|
| 397 |
+
# --- Nutritional Facts ---
|
| 398 |
+
if "nutritional facts for" in user_message.lower():
|
| 399 |
+
dish_name = user_message.lower().replace("nutritional facts for", "").strip().title()
|
| 400 |
+
dish = next((item for item in menu_items if item["name"].lower() == dish_name.lower()), None)
|
| 401 |
+
if dish:
|
| 402 |
+
response_text = f"Nutritional facts for {dish['name']}:\n{dish['nutrition']}"
|
| 403 |
+
else:
|
| 404 |
+
response_text = f"Sorry, I couldn't find nutritional facts for {dish_name}."
|
| 405 |
+
background_tasks.add_task(log_chat_to_db, user_id, "outbound", response_text)
|
| 406 |
+
conversation_context[user_id].append({
|
| 407 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 408 |
+
"role": "bot",
|
| 409 |
+
"message": response_text
|
| 410 |
+
})
|
| 411 |
+
return JSONResponse(content={"response": sentiment_modifier + response_text})
|
| 412 |
+
|
| 413 |
+
# --- Fallback: LLM Response Streaming with Conversation Context ---
|
| 414 |
+
recent_context = conversation_context.get(user_id, [])[-5:]
|
| 415 |
+
context_str = "\n".join([f"{entry['role'].capitalize()}: {entry['message']}" for entry in recent_context])
|
| 416 |
+
prompt = f"Conversation context:\n{context_str}\nUser query: {user_message}\nGenerate a helpful, personalized response for a restaurant chatbot."
|
| 417 |
+
def stream_response():
|
| 418 |
+
for chunk in stream_text_completion(prompt):
|
| 419 |
+
yield chunk
|
| 420 |
+
fallback_log = f"LLM fallback response for prompt: {prompt}"
|
| 421 |
+
background_tasks.add_task(log_chat_to_db, user_id, "outbound", fallback_log)
|
| 422 |
+
return StreamingResponse(stream_response(), media_type="text/plain")
|
| 423 |
|
| 424 |
# --- Other Endpoints (Chat History, Order Details, User Profile, Analytics, Voice, Payment Callback) ---
|
| 425 |
@app.get("/chat_history/{user_id}")
|
|
|
|
| 496 |
await session.commit()
|
| 497 |
else:
|
| 498 |
raise HTTPException(status_code=404, detail="Order not found.")
|
| 499 |
+
# Record payment confirmation tracking update
|
| 500 |
+
await log_order_tracking(order_id, "Payment Confirmed", f"Payment status updated to {status}.")
|
| 501 |
# Notify management via WhatsApp about the payment update
|
| 502 |
await asyncio.to_thread(send_whatsapp_message, MANAGEMENT_WHATSAPP_NUMBER,
|
| 503 |
f"Payment Update:\nOrder ID: {order_id} is now {status}."
|
| 504 |
)
|
| 505 |
# Redirect user back to the chat interface (adjust URL as needed)
|
| 506 |
+
redirect_url = f"https://wa.link/am87s2"
|
| 507 |
return RedirectResponse(url=redirect_url)
|
| 508 |
# POST: Server-to-server callback from Paystack
|
| 509 |
else:
|
|
|
|
| 520 |
if order:
|
| 521 |
order.status = new_status
|
| 522 |
await session.commit()
|
| 523 |
+
await log_order_tracking(order_id, "Payment Confirmed", f"Payment status updated to {new_status}.")
|
| 524 |
await asyncio.to_thread(send_whatsapp_message, MANAGEMENT_WHATSAPP_NUMBER,
|
| 525 |
f"Payment Update:\nOrder ID: {order_id} is now {new_status}."
|
| 526 |
)
|
| 527 |
return JSONResponse(content={"message": "Order updated successfully."})
|
| 528 |
else:
|
| 529 |
raise HTTPException(status_code=404, detail="Order not found.")
|
| 530 |
+
|
| 531 |
+
@app.get("/track_order/{order_id}")
|
| 532 |
+
async def track_order(order_id: str):
|
| 533 |
+
"""
|
| 534 |
+
Fetch order tracking details for a given order ID.
|
| 535 |
+
"""
|
| 536 |
+
async with async_session() as session:
|
| 537 |
+
result = await session.execute(
|
| 538 |
+
select(OrderTracking)
|
| 539 |
+
.where(OrderTracking.order_id == order_id)
|
| 540 |
+
.order_by(OrderTracking.timestamp)
|
| 541 |
+
)
|
| 542 |
+
tracking_updates = result.scalars().all()
|
| 543 |
+
if tracking_updates:
|
| 544 |
+
response = []
|
| 545 |
+
for update in tracking_updates:
|
| 546 |
+
response.append({
|
| 547 |
+
"status": update.status,
|
| 548 |
+
"message": update.message,
|
| 549 |
+
"timestamp": update.timestamp.isoformat(),
|
| 550 |
+
})
|
| 551 |
+
return JSONResponse(content=response)
|
| 552 |
+
else:
|
| 553 |
+
raise HTTPException(status_code=404, detail="No tracking information found for this order.")
|
| 554 |
|
| 555 |
if __name__ == "__main__":
|
| 556 |
import uvicorn
|