Fred808 commited on
Commit
83dc28b
·
verified ·
1 Parent(s): 1bdd4ff

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +187 -103
app.py CHANGED
@@ -1,6 +1,3 @@
1
- # --------------------
2
- # IMPORTS & SETUP
3
- # --------------------
4
  import re
5
  import os
6
  import time
@@ -11,23 +8,44 @@ from datetime import datetime, timedelta
11
  from typing import Dict, List, Optional
12
  from fastapi import FastAPI, Request, HTTPException, BackgroundTasks, UploadFile, File, Form
13
  from fastapi.responses import JSONResponse, StreamingResponse, RedirectResponse
14
- from sqlalchemy import select, update
 
15
  from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
16
  from sqlalchemy.orm import sessionmaker, declarative_base
17
  from sqlalchemy import Column, Integer, String, DateTime, Text, Float, Boolean
 
 
 
 
 
 
 
 
18
 
19
  # --------------------
20
- # DATABASE MODELS
21
  # --------------------
 
 
 
 
22
  Base = declarative_base()
23
 
 
 
 
24
  class UserProfile(Base):
25
  __tablename__ = "user_profiles"
26
  id = Column(Integer, primary_key=True, index=True)
27
  user_id = Column(String(36), unique=True, index=True)
28
- # ... [keep previous fields] ...
 
 
 
 
29
  ux_mode = Column(String(20), default="default")
30
  accessibility_prefs = Column(Text, default="{}")
 
31
 
32
  class UXPreferences(Base):
33
  __tablename__ = "ux_preferences"
@@ -36,6 +54,38 @@ class UXPreferences(Base):
36
  color_scheme = Column(String(20), default="light")
37
  input_preference = Column(String(20), default="buttons")
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  # --------------------
40
  # STATE MANAGEMENT
41
  # --------------------
@@ -56,10 +106,13 @@ class ConversationState:
56
  "role": role,
57
  "message": message
58
  })
59
- # Keep only last 5 messages
60
  if len(self.context) > 5:
61
  self.context = self.context[-5:]
62
 
 
 
 
63
  # --------------------
64
  # CORE UTILITIES
65
  # --------------------
@@ -97,7 +150,7 @@ async def get_or_create_user_profile(user_id: str, phone: str = None) -> UserPro
97
  class DeliveryUXManager:
98
  @staticmethod
99
  async def generate_response_template(user_id: str) -> Dict:
100
- """Generate personalized response structure"""
101
  profile = await get_or_create_user_profile(user_id)
102
  return {
103
  "meta": {
@@ -115,14 +168,14 @@ class DeliveryUXManager:
115
 
116
  @staticmethod
117
  async def handle_ux_preferences(user_id: str, preference: str):
118
- """Update UX preferences"""
119
  async with async_session() as session:
120
  prefs = await session.get(UXPreferences, user_id)
121
  if not prefs:
122
  prefs = UXPreferences(user_id=user_id)
123
  session.add(prefs)
124
 
125
- # Update specific preference
126
  if preference.startswith("color_"):
127
  prefs.color_scheme = preference.split("_")[1]
128
  elif preference.startswith("speed_"):
@@ -130,28 +183,72 @@ class DeliveryUXManager:
130
 
131
  await session.commit()
132
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  # --------------------
134
  # MAIN APPLICATION
135
  # --------------------
136
  app = FastAPI(title="Delivery Service Chatbot")
137
 
 
 
 
 
138
  @app.post("/chatbot")
139
  async def enhanced_chatbot_handler(request: Request, bg: BackgroundTasks):
140
  data = await request.json()
141
  user_id = data["user_id"]
142
  message = data.get("message", "")
143
 
144
- # Initialize state
145
  if user_id not in user_state:
146
  user_state[user_id] = ConversationState()
147
 
148
  state = user_state[user_id]
149
  state.update_last_active()
150
 
151
- # Process message
152
  response = await DeliveryUXManager.generate_response_template(user_id)
 
 
153
 
154
- # [Add conversation handling logic here]
 
155
 
156
  return JSONResponse(response)
157
 
@@ -164,69 +261,34 @@ async def update_ux_preferences(request: Request):
164
  )
165
  return {"status": "preferences updated"}
166
 
167
- # --------------------
168
- # PROACTIVE FEATURES
169
- # --------------------
170
- async def send_proactive_update(user_id: str, update_type: str):
171
- """Send unsolicited updates to users"""
172
- state = user_state.get(user_id)
173
- if not state:
174
- return # User not active
175
-
176
- response = await DeliveryUXManager.generate_response_template(user_id)
177
-
178
- if update_type == "delivery_eta":
179
- response["content"]["text"] = "📦 Your package is arriving in 15 minutes!"
180
- response["content"]["quick_replies"] = [
181
- {"title": "Track Live", "action": "track_live"},
182
- {"title": "Delay Delivery", "action": "delay_delivery"}
183
- ]
184
-
185
- return response
186
-
187
- # --------------------
188
- # ERROR HANDLING
189
- # --------------------
190
- @app.exception_handler(HTTPException)
191
- async def ux_error_handler(request: Request, exc: HTTPException):
192
- return JSONResponse({
193
- "meta": {"error": True},
194
- "content": {
195
- "text": f"⚠️ Error: {exc.detail}",
196
- "quick_replies": [{"title": "Main Menu", "action": "reset"}]
197
- }
198
- }, status_code=exc.status_code)
199
-
200
- if __name__ == "__main__":
201
- import uvicorn
202
- uvicorn.run(app, host="0.0.0.0", port=8000)
203
- # --- FastAPI Setup & Endpoints ---
204
- app = FastAPI()
205
-
206
- @app.on_event("startup")
207
- async def on_startup():
208
- await init_db()
209
-
210
-
211
- # --- Other Endpoints (Chat History, Order Details, User Profile, Analytics, Voice, Payment Callback) ---
212
  @app.get("/chat_history/{user_id}")
213
  async def get_chat_history(user_id: str):
214
  async with async_session() as session:
215
  result = await session.execute(
216
- ChatHistory.__table__.select().where(ChatHistory.user_id == user_id)
217
  )
218
- history = result.fetchall()
219
- return [dict(row) for row in history]
 
 
 
 
220
 
221
  @app.get("/order/{order_id}")
222
  async def get_order(order_id: str):
223
  async with async_session() as session:
224
  result = await session.execute(
225
- Order.__table__.select().where(Order.order_id == order_id)
226
  )
227
- order = result.fetchone()
228
  if order:
229
- return dict(order)
 
 
 
 
 
 
230
  else:
231
  raise HTTPException(status_code=404, detail="Order not found.")
232
 
@@ -239,16 +301,23 @@ async def get_user_profile(user_id: str):
239
  "name": profile.name,
240
  "email": profile.email,
241
  "preferences": profile.preferences,
242
- "last_interaction": profile.last_interaction.isoformat()
 
 
243
  }
244
 
245
  @app.get("/analytics")
246
  async def get_analytics():
247
  async with async_session() as session:
248
- msg_result = await session.execute(ChatHistory.__table__.count())
 
 
249
  total_messages = msg_result.scalar() or 0
250
- order_result = await session.execute(Order.__table__.count())
 
 
251
  total_orders = order_result.scalar() or 0
 
252
  sentiment_result = await session.execute("SELECT AVG(sentiment_score) FROM sentiment_logs")
253
  avg_sentiment = sentiment_result.scalar() or 0
254
  return {
@@ -263,10 +332,11 @@ async def process_voice(file: UploadFile = File(...)):
263
  simulated_text = "Simulated speech-to-text conversion result."
264
  return {"transcription": simulated_text}
265
 
266
- # Enhanced payment callback with security checks
267
  @app.api_route("/payment_callback", methods=["GET", "POST"])
268
  async def payment_callback(request: Request):
269
- """Handle payment gateway callbacks with HMAC verification"""
 
 
270
  try:
271
  if request.method == "POST":
272
  # Verify Paystack signature
@@ -298,7 +368,6 @@ async def payment_callback(request: Request):
298
  raise HTTPException(status_code=400, detail="Missing order reference")
299
 
300
  async with async_session() as session:
301
- # Get order with lock to prevent race conditions
302
  result = await session.execute(
303
  select(Order)
304
  .where(Order.order_id == order_id)
@@ -326,9 +395,9 @@ async def payment_callback(request: Request):
326
 
327
  await session.commit()
328
 
329
- # Send notifications
330
  await asyncio.gather(
331
- log_order_tracking(order_id, "Payment Updated", status),
332
  send_whatsapp_message(
333
  MANAGEMENT_WHATSAPP_NUMBER,
334
  f"Payment Update: Order {order_id} - {status}"
@@ -337,38 +406,35 @@ async def payment_callback(request: Request):
337
  )
338
 
339
  if request.method == "GET":
 
340
  return RedirectResponse(
341
- url=os.getenv("PAYMENT_REDIRECT_URL", "https://default-redirect.com"),
342
  status_code=303
343
  )
344
  return JSONResponse(content={"status": "success", "order_id": order_id})
345
 
346
  except HTTPException as he:
347
- raise
348
  except Exception as e:
349
  logger.error(f"Payment callback error: {str(e)}")
350
  raise HTTPException(status_code=500, detail="Internal server error")
351
 
352
- # Enhanced tracking endpoint with caching
353
  from fastapi_cache.decorator import cache
354
 
355
  @app.get("/track_order/{order_id}", response_model=List[dict])
356
  @cache(expire=60) # Cache for 1 minute
357
- async def get_order_tracking(order_id: str,
358
- page: int = 1,
359
- limit: int = 10):
360
  """
361
- Get order tracking history with pagination
362
  """
363
  try:
364
  async with async_session() as session:
365
- # Get order first to verify existence
366
  order_result = await session.execute(
367
  select(Order).where(Order.order_id == order_id)
 
368
  if not order_result.scalar_one_or_none():
369
  raise HTTPException(status_code=404, detail="Order not found")
370
 
371
- # Get tracking history
372
  tracking_result = await session.execute(
373
  select(OrderTracking)
374
  .where(OrderTracking.order_id == order_id)
@@ -387,40 +453,58 @@ async def get_order_tracking(order_id: str,
387
  "status": update.status,
388
  "message": update.message,
389
  "timestamp": update.timestamp.isoformat(),
390
- "location": jsonable_encoder(update.location) if update.location else None
391
  }
392
  for update in tracking_updates
393
  ]
394
 
395
- # Add estimated delivery time
396
  order = await get_order_details(order_id)
397
- if order.status == "shipped":
398
- response[0]["estimated_delivery"] = calculate_eta(order.last_location)
 
399
 
400
  return JSONResponse(content=response)
401
 
402
  except HTTPException as he:
403
- raise
404
  except Exception as e:
405
  logger.error(f"Tracking error: {str(e)}")
406
  raise HTTPException(status_code=500, detail="Internal server error")
407
 
408
- # Helper functions
409
- async def get_order_details(order_id: str):
410
- async with async_session() as session:
411
- result = await session.execute(
412
- select(Order).where(Order.order_id == order_id)
413
- return result.scalar_one_or_none()
 
 
 
 
 
 
 
 
 
 
 
 
 
414
 
415
- async def update_user_order_status(user_id: str, order_id: str, status: str):
416
- async with async_session() as session:
417
- await session.execute(
418
- update(UserProfile)
419
- .where(UserProfile.user_id == user_id)
420
- .values(current_order_status=status)
421
- )
422
- await session.commit()
 
 
 
 
423
 
424
- def calculate_eta(last_location: dict) -> str:
425
- # Implement actual ETA calculation logic
426
- return "30 minutes"
 
 
 
 
1
  import re
2
  import os
3
  import time
 
8
  from typing import Dict, List, Optional
9
  from fastapi import FastAPI, Request, HTTPException, BackgroundTasks, UploadFile, File, Form
10
  from fastapi.responses import JSONResponse, StreamingResponse, RedirectResponse
11
+ from fastapi.encoders import jsonable_encoder
12
+ from sqlalchemy import select, update, func
13
  from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
14
  from sqlalchemy.orm import sessionmaker, declarative_base
15
  from sqlalchemy import Column, Integer, String, DateTime, Text, Float, Boolean
16
+ import logging
17
+ import hmac
18
+ import hashlib
19
+ import json
20
+
21
+ # Configure logging
22
+ logging.basicConfig(level=logging.INFO)
23
+ logger = logging.getLogger("delivery_service")
24
 
25
  # --------------------
26
+ # DATABASE SETUP
27
  # --------------------
28
+ DATABASE_URL = os.getenv("DATABASE_URL", "sqlite+aiosqlite:///./test.db")
29
+ engine = create_async_engine(DATABASE_URL, echo=True)
30
+ async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
31
+
32
  Base = declarative_base()
33
 
34
+ # --------------------
35
+ # DATABASE MODELS
36
+ # --------------------
37
  class UserProfile(Base):
38
  __tablename__ = "user_profiles"
39
  id = Column(Integer, primary_key=True, index=True)
40
  user_id = Column(String(36), unique=True, index=True)
41
+ phone_number = Column(String(20), nullable=True)
42
+ name = Column(String(100), nullable=True)
43
+ email = Column(String(100), nullable=True)
44
+ preferences = Column(Text, default="{}")
45
+ last_interaction = Column(DateTime, default=datetime.utcnow)
46
  ux_mode = Column(String(20), default="default")
47
  accessibility_prefs = Column(Text, default="{}")
48
+ current_order_status = Column(String(50), default="None")
49
 
50
  class UXPreferences(Base):
51
  __tablename__ = "ux_preferences"
 
54
  color_scheme = Column(String(20), default="light")
55
  input_preference = Column(String(20), default="buttons")
56
 
57
+ class ChatHistory(Base):
58
+ __tablename__ = "chat_history"
59
+ id = Column(Integer, primary_key=True, index=True)
60
+ user_id = Column(String(36), index=True)
61
+ message = Column(Text)
62
+ timestamp = Column(DateTime, default=datetime.utcnow)
63
+
64
+ class Order(Base):
65
+ __tablename__ = "orders"
66
+ order_id = Column(String(36), primary_key=True, index=True)
67
+ user_id = Column(String(36), index=True)
68
+ status = Column(String(20), default="pending")
69
+ payment_reference = Column(String(100), nullable=True)
70
+ last_location = Column(Text, nullable=True) # Stored as a JSON string
71
+
72
+ class OrderTracking(Base):
73
+ __tablename__ = "order_tracking"
74
+ id = Column(Integer, primary_key=True, index=True)
75
+ order_id = Column(String(36), index=True)
76
+ status = Column(String(50))
77
+ message = Column(Text)
78
+ timestamp = Column(DateTime, default=datetime.utcnow)
79
+ location = Column(Text, nullable=True) # Stored as a JSON string
80
+
81
+ # --------------------
82
+ # INITIALIZATION
83
+ # --------------------
84
+ async def init_db():
85
+ async with engine.begin() as conn:
86
+ await conn.run_sync(Base.metadata.create_all)
87
+ logger.info("Database tables created.")
88
+
89
  # --------------------
90
  # STATE MANAGEMENT
91
  # --------------------
 
106
  "role": role,
107
  "message": message
108
  })
109
+ # Keep only the last 5 messages
110
  if len(self.context) > 5:
111
  self.context = self.context[-5:]
112
 
113
+ # Global state dictionary
114
+ user_state: Dict[str, ConversationState] = {}
115
+
116
  # --------------------
117
  # CORE UTILITIES
118
  # --------------------
 
150
  class DeliveryUXManager:
151
  @staticmethod
152
  async def generate_response_template(user_id: str) -> Dict:
153
+ """Generate a personalized response structure."""
154
  profile = await get_or_create_user_profile(user_id)
155
  return {
156
  "meta": {
 
168
 
169
  @staticmethod
170
  async def handle_ux_preferences(user_id: str, preference: str):
171
+ """Update UX preferences."""
172
  async with async_session() as session:
173
  prefs = await session.get(UXPreferences, user_id)
174
  if not prefs:
175
  prefs = UXPreferences(user_id=user_id)
176
  session.add(prefs)
177
 
178
+ # Update specific preference based on input
179
  if preference.startswith("color_"):
180
  prefs.color_scheme = preference.split("_")[1]
181
  elif preference.startswith("speed_"):
 
183
 
184
  await session.commit()
185
 
186
+ # --------------------
187
+ # HELPER FUNCTIONS FOR PAYMENT CALLBACK & TRACKING
188
+ # --------------------
189
+ async def log_order_tracking(order_id: str, status: str, message: str):
190
+ async with async_session() as session:
191
+ tracking_entry = OrderTracking(
192
+ order_id=order_id,
193
+ status=status,
194
+ message=message
195
+ )
196
+ session.add(tracking_entry)
197
+ await session.commit()
198
+
199
+ async def send_whatsapp_message(number: str, message: str):
200
+ # Dummy implementation for sending WhatsApp messages
201
+ logger.info(f"Sending WhatsApp message to {number}: {message}")
202
+
203
+ async def update_user_order_status(user_id: str, order_id: str, status: str):
204
+ async with async_session() as session:
205
+ await session.execute(
206
+ update(UserProfile)
207
+ .where(UserProfile.user_id == user_id)
208
+ .values(current_order_status=status)
209
+ )
210
+ await session.commit()
211
+
212
+ async def get_order_details(order_id: str) -> Optional[Order]:
213
+ async with async_session() as session:
214
+ result = await session.execute(
215
+ select(Order).where(Order.order_id == order_id)
216
+ )
217
+ return result.scalar_one_or_none()
218
+
219
+ def calculate_eta(last_location: dict) -> str:
220
+ # Implement actual ETA calculation logic here.
221
+ return "30 minutes"
222
+
223
  # --------------------
224
  # MAIN APPLICATION
225
  # --------------------
226
  app = FastAPI(title="Delivery Service Chatbot")
227
 
228
+ @app.on_event("startup")
229
+ async def on_startup():
230
+ await init_db()
231
+
232
  @app.post("/chatbot")
233
  async def enhanced_chatbot_handler(request: Request, bg: BackgroundTasks):
234
  data = await request.json()
235
  user_id = data["user_id"]
236
  message = data.get("message", "")
237
 
238
+ # Initialize conversation state if it doesn't exist
239
  if user_id not in user_state:
240
  user_state[user_id] = ConversationState()
241
 
242
  state = user_state[user_id]
243
  state.update_last_active()
244
 
245
+ # Dummy conversation handling logic
246
  response = await DeliveryUXManager.generate_response_template(user_id)
247
+ response["content"]["text"] = f"Received your message: {message}"
248
+ state.add_context("user", message)
249
 
250
+ # Background task to update user's last interaction in the DB
251
+ bg.add_task(update_user_last_interaction, user_id)
252
 
253
  return JSONResponse(response)
254
 
 
261
  )
262
  return {"status": "preferences updated"}
263
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  @app.get("/chat_history/{user_id}")
265
  async def get_chat_history(user_id: str):
266
  async with async_session() as session:
267
  result = await session.execute(
268
+ select(ChatHistory).where(ChatHistory.user_id == user_id)
269
  )
270
+ history = result.scalars().all()
271
+ return [jsonable_encoder({
272
+ "user_id": entry.user_id,
273
+ "message": entry.message,
274
+ "timestamp": entry.timestamp.isoformat()
275
+ }) for entry in history]
276
 
277
  @app.get("/order/{order_id}")
278
  async def get_order(order_id: str):
279
  async with async_session() as session:
280
  result = await session.execute(
281
+ select(Order).where(Order.order_id == order_id)
282
  )
283
+ order = result.scalar_one_or_none()
284
  if order:
285
+ return jsonable_encoder({
286
+ "order_id": order.order_id,
287
+ "user_id": order.user_id,
288
+ "status": order.status,
289
+ "payment_reference": order.payment_reference,
290
+ "last_location": order.last_location
291
+ })
292
  else:
293
  raise HTTPException(status_code=404, detail="Order not found.")
294
 
 
301
  "name": profile.name,
302
  "email": profile.email,
303
  "preferences": profile.preferences,
304
+ "last_interaction": profile.last_interaction.isoformat(),
305
+ "ux_mode": profile.ux_mode,
306
+ "current_order_status": profile.current_order_status
307
  }
308
 
309
  @app.get("/analytics")
310
  async def get_analytics():
311
  async with async_session() as session:
312
+ msg_result = await session.execute(
313
+ select(func.count()).select_from(ChatHistory)
314
+ )
315
  total_messages = msg_result.scalar() or 0
316
+ order_result = await session.execute(
317
+ select(func.count()).select_from(Order)
318
+ )
319
  total_orders = order_result.scalar() or 0
320
+ # This query assumes a sentiment_logs table exists with a sentiment_score column.
321
  sentiment_result = await session.execute("SELECT AVG(sentiment_score) FROM sentiment_logs")
322
  avg_sentiment = sentiment_result.scalar() or 0
323
  return {
 
332
  simulated_text = "Simulated speech-to-text conversion result."
333
  return {"transcription": simulated_text}
334
 
 
335
  @app.api_route("/payment_callback", methods=["GET", "POST"])
336
  async def payment_callback(request: Request):
337
+ PAYSTACK_SECRET_KEY = os.getenv("PAYSTACK_SECRET_KEY", "dummy_secret")
338
+ MANAGEMENT_WHATSAPP_NUMBER = os.getenv("MANAGEMENT_WHATSAPP_NUMBER", "+1234567890")
339
+
340
  try:
341
  if request.method == "POST":
342
  # Verify Paystack signature
 
368
  raise HTTPException(status_code=400, detail="Missing order reference")
369
 
370
  async with async_session() as session:
 
371
  result = await session.execute(
372
  select(Order)
373
  .where(Order.order_id == order_id)
 
395
 
396
  await session.commit()
397
 
398
+ # Send notifications concurrently
399
  await asyncio.gather(
400
+ log_order_tracking(order_id, "Payment Updated", f"Payment status changed to {status}"),
401
  send_whatsapp_message(
402
  MANAGEMENT_WHATSAPP_NUMBER,
403
  f"Payment Update: Order {order_id} - {status}"
 
406
  )
407
 
408
  if request.method == "GET":
409
+ redirect_url = os.getenv("PAYMENT_REDIRECT_URL", "https://default-redirect.com")
410
  return RedirectResponse(
411
+ url=redirect_url,
412
  status_code=303
413
  )
414
  return JSONResponse(content={"status": "success", "order_id": order_id})
415
 
416
  except HTTPException as he:
417
+ raise he
418
  except Exception as e:
419
  logger.error(f"Payment callback error: {str(e)}")
420
  raise HTTPException(status_code=500, detail="Internal server error")
421
 
 
422
  from fastapi_cache.decorator import cache
423
 
424
  @app.get("/track_order/{order_id}", response_model=List[dict])
425
  @cache(expire=60) # Cache for 1 minute
426
+ async def get_order_tracking(order_id: str, page: int = 1, limit: int = 10):
 
 
427
  """
428
+ Get order tracking history with pagination.
429
  """
430
  try:
431
  async with async_session() as session:
 
432
  order_result = await session.execute(
433
  select(Order).where(Order.order_id == order_id)
434
+ )
435
  if not order_result.scalar_one_or_none():
436
  raise HTTPException(status_code=404, detail="Order not found")
437
 
 
438
  tracking_result = await session.execute(
439
  select(OrderTracking)
440
  .where(OrderTracking.order_id == order_id)
 
453
  "status": update.status,
454
  "message": update.message,
455
  "timestamp": update.timestamp.isoformat(),
456
+ "location": json.loads(update.location) if update.location else None
457
  }
458
  for update in tracking_updates
459
  ]
460
 
461
+ # Add estimated delivery time if order is shipped
462
  order = await get_order_details(order_id)
463
+ if order and order.status.lower() == "shipped":
464
+ loc = json.loads(order.last_location) if order.last_location else {}
465
+ response[0]["estimated_delivery"] = calculate_eta(loc)
466
 
467
  return JSONResponse(content=response)
468
 
469
  except HTTPException as he:
470
+ raise he
471
  except Exception as e:
472
  logger.error(f"Tracking error: {str(e)}")
473
  raise HTTPException(status_code=500, detail="Internal server error")
474
 
475
+ # --------------------
476
+ # PROACTIVE FEATURES
477
+ # --------------------
478
+ async def send_proactive_update(user_id: str, update_type: str):
479
+ """Send unsolicited updates to users."""
480
+ state = user_state.get(user_id)
481
+ if not state:
482
+ return # User not active
483
+
484
+ response = await DeliveryUXManager.generate_response_template(user_id)
485
+
486
+ if update_type == "delivery_eta":
487
+ response["content"]["text"] = "📦 Your package is arriving in 15 minutes!"
488
+ response["content"]["quick_replies"] = [
489
+ {"title": "Track Live", "action": "track_live"},
490
+ {"title": "Delay Delivery", "action": "delay_delivery"}
491
+ ]
492
+
493
+ return response
494
 
495
+ # --------------------
496
+ # ERROR HANDLING
497
+ # --------------------
498
+ @app.exception_handler(HTTPException)
499
+ async def ux_error_handler(request: Request, exc: HTTPException):
500
+ return JSONResponse({
501
+ "meta": {"error": True},
502
+ "content": {
503
+ "text": f"⚠️ Error: {exc.detail}",
504
+ "quick_replies": [{"title": "Main Menu", "action": "reset"}]
505
+ }
506
+ }, status_code=exc.status_code)
507
 
508
+ if __name__ == "__main__":
509
+ import uvicorn
510
+ uvicorn.run(app, host="0.0.0.0", port=8000)