Fred808 commited on
Commit
3f14bfe
·
verified ·
1 Parent(s): 4c7fe53

Create app.js

Browse files
Files changed (1) hide show
  1. app.js +371 -0
app.js ADDED
@@ -0,0 +1,371 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import os
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
10
+
11
+ from fastapi import FastAPI, Request, HTTPException, BackgroundTasks, UploadFile, File, Form
12
+ from fastapi.responses import JSONResponse, StreamingResponse, RedirectResponse
13
+
14
+ import openai
15
+
16
+ # For sentiment analysis using TextBlob
17
+ from textblob import TextBlob
18
+
19
+ # SQLAlchemy Imports (Async)
20
+ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
21
+ from sqlalchemy.orm import sessionmaker, declarative_base
22
+ from sqlalchemy import Column, Integer, String, DateTime, Text, Float
23
+
24
+ # Environment Variables
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")
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
+
31
+ # WhatsApp Business API credentials (Cloud API)
32
+ WHATSAPP_PHONE_NUMBER_ID = os.getenv("WHATSAPP_PHONE_NUMBER_ID", "default_value")
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):
40
+ __tablename__ = "chat_history"
41
+ id = Column(Integer, primary_key=True, index=True)
42
+ user_id = Column(String, index=True)
43
+ timestamp = Column(DateTime, default=datetime.utcnow)
44
+ direction = Column(String) # 'inbound' or 'outbound'
45
+ message = Column(Text)
46
+
47
+ class Order(Base):
48
+ __tablename__ = "orders"
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
+ pickup_location = Column(String)
53
+ dropoff_location = Column(String)
54
+ package_details = Column(String)
55
+ status = Column(String, default="Pending")
56
+ timestamp = Column(DateTime, default=datetime.utcnow)
57
+
58
+ class UserProfile(Base):
59
+ __tablename__ = "user_profiles"
60
+ id = Column(Integer, primary_key=True, index=True)
61
+ user_id = Column(String, unique=True, index=True)
62
+ phone_number = Column(String, unique=True, index=True, nullable=True)
63
+ name = Column(String, default="Valued Customer")
64
+ email = Column(String, default="[email protected]")
65
+ preferences = Column(Text, default="")
66
+ last_interaction = Column(DateTime, default=datetime.utcnow)
67
+
68
+ class SentimentLog(Base):
69
+ __tablename__ = "sentiment_logs"
70
+ id = Column(Integer, primary_key=True, index=True)
71
+ user_id = Column(String, index=True)
72
+ timestamp = Column(DateTime, default=datetime.utcnow)
73
+ sentiment_score = Column(Float)
74
+ message = Column(Text)
75
+
76
+ # Initialize Database
77
+ engine = create_async_engine(DATABASE_URL, echo=True)
78
+ async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
79
+
80
+ async def init_db():
81
+ async with engine.begin() as conn:
82
+ await conn.run_sync(Base.metadata.create_all)
83
+
84
+ # --- Global In-Memory Stores ---
85
+ user_state = {} # { user_id: ConversationState }
86
+ conversation_context = {} # { user_id: [ { "timestamp": ..., "role": "user"/"bot", "message": ... }, ... ] }
87
+
88
+ # --- Conversation State Management ---
89
+ SESSION_TIMEOUT = timedelta(minutes=5)
90
+
91
+ class ConversationState:
92
+ def __init__(self):
93
+ self.flow = None # e.g., "track_order", "schedule_delivery"
94
+ self.step = 0
95
+ self.data = {}
96
+ self.last_active = datetime.utcnow()
97
+
98
+ def update_last_active(self):
99
+ self.last_active = datetime.utcnow()
100
+
101
+ def is_expired(self):
102
+ return datetime.utcnow() - self.last_active > SESSION_TIMEOUT
103
+
104
+ def reset(self):
105
+ self.flow = None
106
+ self.step = 0
107
+ self.data = {}
108
+ self.last_active = datetime.utcnow()
109
+
110
+ # --- Utility Functions ---
111
+ async def log_chat_to_db(user_id: str, direction: str, message: str):
112
+ async with async_session() as session:
113
+ entry = ChatHistory(user_id=user_id, direction=direction, message=message)
114
+ session.add(entry)
115
+ await session.commit()
116
+
117
+ async def log_sentiment(user_id: str, message: str, score: float):
118
+ async with async_session() as session:
119
+ entry = SentimentLog(user_id=user_id, sentiment_score=score, message=message)
120
+ session.add(entry)
121
+ await session.commit()
122
+
123
+ def analyze_sentiment(text: str) -> float:
124
+ blob = TextBlob(text)
125
+ return blob.sentiment.polarity
126
+
127
+ # --- Delivery Service UX Functions ---
128
+ def generate_main_menu() -> str:
129
+ """Generate the main menu with quick reply options."""
130
+ menu_text = "Hi there! 👋 Welcome to [Delivery Service Co.]. I’m here to help with your deliveries. What would you like to do today?\n\n"
131
+ menu_text += "1. Track an Order\n"
132
+ menu_text += "2. Schedule a Delivery\n"
133
+ menu_text += "3. FAQs & Support\n"
134
+ menu_text += "4. Talk to an Agent\n"
135
+ menu_text += "\nPlease reply with the number of your choice."
136
+ return menu_text
137
+
138
+ def generate_faq_menu() -> str:
139
+ """Generate the FAQ menu with quick reply options."""
140
+ faq_text = "What do you need help with? Choose a category below:\n\n"
141
+ faq_text += "1. Pricing & Fees\n"
142
+ faq_text += "2. Delivery Times\n"
143
+ faq_text += "3. Order Cancellations\n"
144
+ faq_text += "4. Other Questions\n"
145
+ faq_text += "\nPlease reply with the number of your choice."
146
+ return faq_text
147
+
148
+ def handle_faq_response(choice: str) -> str:
149
+ """Provide detailed answers based on FAQ category."""
150
+ if choice == "1":
151
+ return "Our pricing is based on distance, package size, and weight. For more details, visit [Link] or let me know if you have a specific question."
152
+ elif choice == "2":
153
+ return "Standard delivery times are between 9 AM and 6 PM. Express delivery is available for an additional fee."
154
+ elif choice == "3":
155
+ return "You can cancel your order up to 1 hour before the scheduled pickup time. Refunds are processed within 3-5 business days."
156
+ elif choice == "4":
157
+ return "Please type your question, and I’ll do my best to assist. If you’d like to speak with a live agent, just type 'agent'."
158
+ else:
159
+ return "I didn’t quite catch that. Please choose a valid option from the list."
160
+
161
+ async def track_order_flow(user_id: str, order_id: str) -> str:
162
+ """Handle the order tracking flow."""
163
+ async with async_session() as session:
164
+ result = await session.execute(
165
+ select(Order).where(Order.order_id == order_id)
166
+ )
167
+ order = result.scalars().first()
168
+ if order:
169
+ return f"Your order (ID: {order_id}) is currently {order.status} and is expected to arrive by {order.timestamp + timedelta(hours=2)}."
170
+ else:
171
+ return "Hmm, that order ID doesn’t seem right. Please check and try again or type 'help' for assistance."
172
+
173
+ async def schedule_delivery_flow(user_id: str, step: int, user_input: str = None) -> str:
174
+ """Handle the delivery scheduling flow."""
175
+ state = user_state.get(user_id, ConversationState())
176
+ if step == 1:
177
+ state.flow = "schedule_delivery"
178
+ state.step = 1
179
+ state.data = {}
180
+ user_state[user_id] = state
181
+ return "Great! Let’s schedule your delivery. Please share your pickup and drop-off locations. You can type in the addresses or share your location."
182
+ elif step == 2:
183
+ if user_input:
184
+ state.data["locations"] = user_input
185
+ state.step = 2
186
+ return "Please provide details about your package (size, weight, and any special instructions)."
187
+ else:
188
+ return "I didn’t catch that. Please share your pickup and drop-off locations."
189
+ elif step == 3:
190
+ if user_input:
191
+ state.data["package_details"] = user_input
192
+ state.step = 3
193
+ return "Thank you! Your delivery is scheduled. We will confirm the pickup time shortly. Would you like to receive updates via WhatsApp? (Yes/No)"
194
+ else:
195
+ return "Please provide package details (size, weight, and any special instructions)."
196
+ elif step == 4:
197
+ if user_input.lower() in ["yes", "y"]:
198
+ state.data["updates"] = True
199
+ state.reset()
200
+ return "Awesome! You’ll receive updates on your delivery. Type 'menu' to return to the main menu."
201
+ else:
202
+ state.data["updates"] = False
203
+ state.reset()
204
+ return "Got it. You won’t receive updates. Type 'menu' to return to the main menu."
205
+
206
+ # --- FastAPI Setup & Endpoints ---
207
+ app = FastAPI()
208
+
209
+ @app.on_event("startup")
210
+ async def on_startup():
211
+ await init_db()
212
+
213
+ @app.post("/chatbot")
214
+ async def chatbot_response(request: Request, background_tasks: BackgroundTasks):
215
+ data = await request.json()
216
+ user_id = data.get("user_id")
217
+ user_message = data.get("message", "").strip()
218
+
219
+ if not user_id:
220
+ raise HTTPException(status_code=400, detail="Missing user_id in payload.")
221
+
222
+ # Initialize conversation context if not present
223
+ if user_id not in conversation_context:
224
+ conversation_context[user_id] = []
225
+
226
+ # Append the inbound message to the conversation context
227
+ conversation_context[user_id].append({
228
+ "timestamp": datetime.utcnow().isoformat(),
229
+ "role": "user",
230
+ "message": user_message
231
+ })
232
+
233
+ # Log the chat to the database
234
+ background_tasks.add_task(log_chat_to_db, user_id, "inbound", user_message)
235
+
236
+ # Handle main menu options
237
+ if user_message.lower() in ["menu", "hi", "hello"]:
238
+ response_text = generate_main_menu()
239
+ elif user_message == "1": # Track an Order
240
+ response_text = "Please enter your Order ID, or type 'help' if you need assistance."
241
+ elif user_message == "2": # Schedule a Delivery
242
+ response_text = await schedule_delivery_flow(user_id, step=1)
243
+ elif user_message == "3": # FAQs & Support
244
+ response_text = generate_faq_menu()
245
+ elif user_message == "4": # Talk to an Agent
246
+ response_text = "Please hold on while I connect you to one of our agents."
247
+ elif user_message.isdigit() and len(user_message) == 1 and user_message in ["1", "2", "3", "4"]: # FAQ Submenu
248
+ response_text = handle_faq_response(user_message)
249
+ elif "track" in user_message.lower(): # Order Tracking
250
+ order_id = user_message.replace("track", "").strip()
251
+ response_text = await track_order_flow(user_id, order_id)
252
+ else:
253
+ response_text = "I didn’t quite catch that. Please choose a valid option or type 'menu' to see the main menu."
254
+
255
+ # Log the outbound response
256
+ background_tasks.add_task(log_chat_to_db, user_id, "outbound", response_text)
257
+ conversation_context[user_id].append({
258
+ "timestamp": datetime.utcnow().isoformat(),
259
+ "role": "bot",
260
+ "message": response_text
261
+ })
262
+
263
+ return JSONResponse(content={"response": response_text})
264
+
265
+ # --- Other Endpoints (Chat History, Order Details, User Profile, Analytics, Voice, Payment Callback) ---
266
+ @app.get("/chat_history/{user_id}")
267
+ async def get_chat_history(user_id: str):
268
+ async with async_session() as session:
269
+ result = await session.execute(
270
+ ChatHistory.__table__.select().where(ChatHistory.user_id == user_id)
271
+ )
272
+ history = result.fetchall()
273
+ return [dict(row) for row in history]
274
+
275
+ @app.get("/order/{order_id}")
276
+ async def get_order(order_id: str):
277
+ async with async_session() as session:
278
+ result = await session.execute(
279
+ Order.__table__.select().where(Order.order_id == order_id)
280
+ )
281
+ order = result.fetchone()
282
+ if order:
283
+ return dict(order)
284
+ else:
285
+ raise HTTPException(status_code=404, detail="Order not found.")
286
+
287
+ @app.get("/user_profile/{user_id}")
288
+ async def get_user_profile(user_id: str):
289
+ profile = await get_or_create_user_profile(user_id)
290
+ return {
291
+ "user_id": profile.user_id,
292
+ "phone_number": profile.phone_number,
293
+ "name": profile.name,
294
+ "email": profile.email,
295
+ "preferences": profile.preferences,
296
+ "last_interaction": profile.last_interaction.isoformat()
297
+ }
298
+
299
+ @app.get("/analytics")
300
+ async def get_analytics():
301
+ async with async_session() as session:
302
+ msg_result = await session.execute(ChatHistory.__table__.count())
303
+ total_messages = msg_result.scalar() or 0
304
+ order_result = await session.execute(Order.__table__.count())
305
+ total_orders = order_result.scalar() or 0
306
+ sentiment_result = await session.execute("SELECT AVG(sentiment_score) FROM sentiment_logs")
307
+ avg_sentiment = sentiment_result.scalar() or 0
308
+ return {
309
+ "total_messages": total_messages,
310
+ "total_orders": total_orders,
311
+ "average_sentiment": avg_sentiment
312
+ }
313
+
314
+ @app.post("/voice")
315
+ async def process_voice(file: UploadFile = File(...)):
316
+ contents = await file.read()
317
+ simulated_text = "Simulated speech-to-text conversion result."
318
+ return {"transcription": simulated_text}
319
+
320
+ # --- Payment Callback Endpoint with Payment Tracking and Redirection ---
321
+ @app.api_route("/payment_callback", methods=["GET", "POST"])
322
+ async def payment_callback(request: Request):
323
+ # GET: User redirection after payment
324
+ if request.method == "GET":
325
+ params = request.query_params
326
+ order_id = params.get("reference")
327
+ status = params.get("status", "Paid")
328
+ if not order_id:
329
+ raise HTTPException(status_code=400, detail="Missing order reference in callback.")
330
+ async with async_session() as session:
331
+ result = await session.execute(
332
+ Order.__table__.select().where(Order.order_id == order_id)
333
+ )
334
+ order = result.scalar_one_or_none()
335
+ if order:
336
+ order.status = status
337
+ await session.commit()
338
+ else:
339
+ raise HTTPException(status_code=404, detail="Order not found.")
340
+ # Notify management via WhatsApp about the payment update
341
+ await asyncio.to_thread(send_whatsapp_message, MANAGEMENT_WHATSAPP_NUMBER,
342
+ f"Payment Update:\nOrder ID: {order_id} is now {status}."
343
+ )
344
+ # Redirect user back to the chat interface (adjust URL as needed)
345
+ redirect_url = f"https://yourdomain.com/chat?order_id={order_id}&status=success"
346
+ return RedirectResponse(url=redirect_url)
347
+ # POST: Server-to-server callback from Paystack
348
+ else:
349
+ data = await request.json()
350
+ order_id = data.get("reference")
351
+ new_status = data.get("status", "Paid")
352
+ if not order_id:
353
+ raise HTTPException(status_code=400, detail="Missing order reference in callback.")
354
+ async with async_session() as session:
355
+ result = await session.execute(
356
+ Order.__table__.select().where(Order.order_id == order_id)
357
+ )
358
+ order = result.scalar_one_or_none()
359
+ if order:
360
+ order.status = new_status
361
+ await session.commit()
362
+ await asyncio.to_thread(send_whatsapp_message, MANAGEMENT_WHATSAPP_NUMBER,
363
+ f"Payment Update:\nOrder ID: {order_id} is now {new_status}."
364
+ )
365
+ return JSONResponse(content={"message": "Order updated successfully."})
366
+ else:
367
+ raise HTTPException(status_code=404, detail="Order not found.")
368
+
369
+ if __name__ == "__main__":
370
+ import uvicorn
371
+ uvicorn.run(app, host="0.0.0.0", port=8000)