Fred808 commited on
Commit
9570725
·
verified ·
1 Parent(s): 79f1ae9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +224 -71
app.py CHANGED
@@ -1,26 +1,30 @@
1
  # chatbot_api.py
 
2
  import os
3
  import time
4
  import requests
5
  import base64
6
- from datetime import datetime
7
  from bs4 import BeautifulSoup
8
 
9
- from fastapi import FastAPI, Request, HTTPException, BackgroundTasks, UploadFile, File
10
  from fastapi.responses import JSONResponse, StreamingResponse
11
 
12
  import openai
13
 
 
 
 
 
14
  from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
15
  from sqlalchemy.orm import sessionmaker, declarative_base
16
- from sqlalchemy import Column, Integer, String, DateTime, Text
17
 
18
  # --- Configuration & Environment Variables ---
19
  SPOONACULAR_API_KEY = os.getenv("SPOONACULAR_API_KEY", "815bf76e0764456293f0e96e080e8f60")
20
  PAYSTACK_SECRET_KEY = os.getenv("PAYSTACK_SECRET_KEY", "pk_test_3222fb257041f1f2fd5ef33eafd19e1db4bdb634")
21
- DATABASE_URL = os.getenv("DATABASE_URL", "postgresql+asyncpg://postgres.lgbnxplydqdymepehirg:Lovyelias5584.@aws-0-eu-central-1.pooler.supabase.com:5432/postgres")
22
- NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY", "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") # For NVIDIA LLM endpoints
23
-
24
  openai.api_key = os.getenv("OPENAI_API_KEY", "your_openai_api_key")
25
 
26
  # --- Database Setup ---
@@ -41,12 +45,29 @@ class Order(Base):
41
  user_id = Column(String, index=True)
42
  dish = Column(String)
43
  quantity = Column(String)
44
- price = Column(String, default="0") # Price as string (or use a numeric type)
45
  status = Column(String, default="Pending Payment") # e.g., Pending Payment, Paid, Completed
46
  payment_reference = Column(String, nullable=True)
47
  timestamp = Column(DateTime, default=datetime.utcnow)
48
 
49
- # Create the asynchronous engine. Make sure DATABASE_URL is configured correctly.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  engine = create_async_engine(DATABASE_URL, echo=True)
51
  async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
52
 
@@ -54,8 +75,10 @@ async def init_db():
54
  async with engine.begin() as conn:
55
  await conn.run_sync(Base.metadata.create_all)
56
 
57
- # --- Global In-Memory Stores for Ephemeral Data ---
58
- user_state = {} # Example: { user_id: { "flow": "order", "step": int, "data": dict } }
 
 
59
 
60
  # Local menu with nutritional details
61
  menu_items = [
@@ -74,14 +97,21 @@ async def log_chat_to_db(user_id: str, direction: str, message: str):
74
  session.add(entry)
75
  await session.commit()
76
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  def google_image_scrape(query: str) -> str:
78
- """
79
- Scrape Google Images using BeautifulSoup to get an image URL for the query.
80
- Note: This basic scraper may break if Google changes its markup.
81
- """
82
- headers = {
83
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
84
- }
85
  search_url = f"https://www.google.com/search?tbm=isch&q={query}"
86
  try:
87
  response = requests.get(search_url, headers=headers, timeout=5)
@@ -97,13 +127,7 @@ def google_image_scrape(query: str) -> str:
97
  return ""
98
 
99
  def create_paystack_payment_link(email: str, amount: int, reference: str) -> dict:
100
- """
101
- Call Paystack to initialize a transaction.
102
- - email: customer's email
103
- - amount: in kobo (multiply NGN amount by 100)
104
- - reference: unique order reference
105
- Returns a dict with the payment link and status.
106
- """
107
  url = "https://api.paystack.co/transaction/initialize"
108
  headers = {
109
  "Authorization": f"Bearer {PAYSTACK_SECRET_KEY}",
@@ -125,7 +149,6 @@ def create_paystack_payment_link(email: str, amount: int, reference: str) -> dic
125
  return {"status": False, "message": str(e)}
126
 
127
  # --- NVIDIA LLM Streaming Functions ---
128
-
129
  def stream_text_completion(prompt: str):
130
  """
131
  Stream text completion using NVIDIA's text-only model.
@@ -176,13 +199,13 @@ def stream_image_completion(image_b64: str):
176
  if line:
177
  yield line.decode("utf-8") + "\n"
178
 
179
- # --- Internal Flow: Order Processing & Payment Integration ---
180
- def process_internal_flow(user_id: str, message: str) -> str:
181
  """
182
- A simple two-step order flow:
183
  - Step 1: Ask for dish.
184
  - Step 2: Ask for quantity.
185
- After collecting these details, the order is saved and a payment link is generated.
186
  """
187
  if user_id in user_state:
188
  state = user_state[user_id]
@@ -198,11 +221,10 @@ def process_internal_flow(user_id: str, message: str) -> str:
198
  data["quantity"] = message
199
  order_id = f"ORD-{int(time.time())}"
200
  data["order_id"] = order_id
201
- # Price calculation example (₦1500 per serving)
202
- price_per_serving = 1500
203
  total_price = int(data["quantity"]) * price_per_serving
204
  data["price"] = str(total_price)
205
- # Save order details to the DB asynchronously.
206
  import asyncio
207
  async def save_order():
208
  async with async_session() as session:
@@ -217,10 +239,10 @@ def process_internal_flow(user_id: str, message: str) -> str:
217
  session.add(order)
218
  await session.commit()
219
  asyncio.create_task(save_order())
220
- # Clear the in-memory state.
221
  del user_state[user_id]
222
- # Assume we have the customer's email; using a placeholder.
223
- email = "customer@example.com"
224
  payment_data = create_paystack_payment_link(email, total_price * 100, order_id)
225
  if payment_data.get("status"):
226
  payment_link = payment_data["data"]["authorization_url"]
@@ -230,10 +252,44 @@ def process_internal_flow(user_id: str, message: str) -> str:
230
  return f"Your order has been placed with Order ID {order_id}, but we could not initialize payment. Please try again later."
231
  else:
232
  if "order" in message.lower():
233
- user_state[user_id] = {"flow": "order", "step": 1, "data": {}}
234
  return "Sure! What dish would you like to order?"
235
  return ""
236
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  # --- FastAPI Setup & Endpoints ---
238
  app = FastAPI()
239
 
@@ -241,15 +297,16 @@ app = FastAPI()
241
  async def on_startup():
242
  await init_db()
243
 
 
244
  @app.post("/chatbot")
245
  async def chatbot_response(request: Request, background_tasks: BackgroundTasks):
246
  """
247
  Main chatbot endpoint.
248
- Expects a JSON payload with:
 
249
  - 'user_id'
250
- - 'message' (text query)
251
- - Optionally, 'is_image': true and 'image_base64': <base64 string> for image queries.
252
- Streaming responses will be returned.
253
  """
254
  data = await request.json()
255
  user_id = data.get("user_id")
@@ -260,35 +317,50 @@ async def chatbot_response(request: Request, background_tasks: BackgroundTasks):
260
  if not user_id:
261
  raise HTTPException(status_code=400, detail="Missing user_id in payload.")
262
 
263
- # Log inbound message if it's a text query (for image queries, you might log separately).
264
- if user_message:
265
- background_tasks.add_task(log_chat_to_db, user_id, "inbound", user_message)
266
- if is_image and image_b64 is None:
267
- raise HTTPException(status_code=400, detail="is_image is true but no image_base64 provided.")
268
 
269
- # If an image is provided, use the image model.
270
  if is_image and image_b64:
271
- # Verify the image is small enough.
272
  if len(image_b64) >= 180_000:
273
- raise HTTPException(status_code=400, detail="Image too large. Use a smaller image or the assets API.")
274
- # Return a streaming response from the image-based LLM.
275
  return StreamingResponse(stream_image_completion(image_b64), media_type="text/plain")
276
-
277
- # --- Process textual queries (menu, nutritional facts, internal flows) ---
 
 
 
 
 
 
 
 
 
 
 
 
278
  if "menu" in user_message.lower():
 
279
  menu_with_images = []
280
  for item in menu_items:
281
  image_url = google_image_scrape(item["name"])
282
- menu_with_images.append({"name": item["name"], "description": item["description"], "price": item["price"], "image_url": image_url})
 
 
 
 
 
283
  response_payload = {
284
- "response": "Here’s our delicious menu:",
285
  "menu": menu_with_images,
286
- "follow_up": ("Would you like to see nutritional facts for any dish? "
287
- "Just type, for example, 'Nutritional facts for Jollof Rice'.")
288
  }
289
  background_tasks.add_task(log_chat_to_db, user_id, "outbound", str(response_payload))
290
  return JSONResponse(content=response_payload)
291
-
292
  if "nutritional facts for" in user_message.lower():
293
  dish_name = user_message.lower().replace("nutritional facts for", "").strip().title()
294
  dish = next((item for item in menu_items if item["name"].lower() == dish_name.lower()), None)
@@ -297,28 +369,30 @@ async def chatbot_response(request: Request, background_tasks: BackgroundTasks):
297
  else:
298
  response_text = f"Sorry, I couldn't find nutritional facts for {dish_name}."
299
  background_tasks.add_task(log_chat_to_db, user_id, "outbound", response_text)
300
- return JSONResponse(content={"response": response_text})
301
-
302
- internal_response = process_internal_flow(user_id, user_message)
303
- if internal_response:
304
- background_tasks.add_task(log_chat_to_db, user_id, "outbound", internal_response)
305
- return JSONResponse(content={"response": internal_response})
306
-
307
- # --- Fallback: Use NVIDIA text LLM with streaming ---
308
- prompt = f"User query: {user_message}\nGenerate a helpful response for a restaurant chatbot."
309
-
310
- # Create a streaming response using the NVIDIA text model.
 
 
311
  def stream_response():
312
  for chunk in stream_text_completion(prompt):
313
  yield chunk
314
-
315
  background_tasks.add_task(log_chat_to_db, user_id, "outbound", f"LLM fallback response for prompt: {prompt}")
316
  return StreamingResponse(stream_response(), media_type="text/plain")
317
 
 
318
  @app.get("/chat_history/{user_id}")
319
  async def get_chat_history(user_id: str):
320
  """
321
- Retrieve the chat history for a given user from the database.
322
  """
323
  async with async_session() as session:
324
  result = await session.execute(
@@ -327,10 +401,11 @@ async def get_chat_history(user_id: str):
327
  history = result.fetchall()
328
  return [dict(row) for row in history]
329
 
 
330
  @app.get("/order/{order_id}")
331
  async def get_order(order_id: str):
332
  """
333
- Retrieve details for a specific order from the database.
334
  """
335
  async with async_session() as session:
336
  result = await session.execute(
@@ -342,6 +417,84 @@ async def get_order(order_id: str):
342
  else:
343
  raise HTTPException(status_code=404, detail="Order not found.")
344
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
  if __name__ == "__main__":
346
  import uvicorn
347
- uvicorn.run(app, host="0.0.0.0", port=8000)
 
1
  # chatbot_api.py
2
+
3
  import os
4
  import time
5
  import requests
6
  import base64
7
+ from datetime import datetime, timedelta
8
  from bs4 import BeautifulSoup
9
 
10
+ from fastapi import FastAPI, Request, HTTPException, BackgroundTasks, UploadFile, File, Form
11
  from fastapi.responses import JSONResponse, StreamingResponse
12
 
13
  import openai
14
 
15
+ # For sentiment analysis using TextBlob
16
+ from textblob import TextBlob
17
+
18
+ # SQLAlchemy Imports (Async)
19
  from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
20
  from sqlalchemy.orm import sessionmaker, declarative_base
21
+ from sqlalchemy import Column, Integer, String, DateTime, Text, Float
22
 
23
  # --- Configuration & Environment Variables ---
24
  SPOONACULAR_API_KEY = os.getenv("SPOONACULAR_API_KEY", "815bf76e0764456293f0e96e080e8f60")
25
  PAYSTACK_SECRET_KEY = os.getenv("PAYSTACK_SECRET_KEY", "pk_test_3222fb257041f1f2fd5ef33eafd19e1db4bdb634")
26
+ DATABASE_URL = os.getenv("DATABASE_URL", "postgresql+asyncpg://postgres.lgbnxplydqdymepehirg:YourPassword@aws-0-eu-central-1.pooler.supabase.com:5432/postgres")
27
+ NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY", "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")
 
28
  openai.api_key = os.getenv("OPENAI_API_KEY", "your_openai_api_key")
29
 
30
  # --- Database Setup ---
 
45
  user_id = Column(String, index=True)
46
  dish = Column(String)
47
  quantity = Column(String)
48
+ price = Column(String, default="0") # Price as string (or numeric type)
49
  status = Column(String, default="Pending Payment") # e.g., Pending Payment, Paid, Completed
50
  payment_reference = Column(String, nullable=True)
51
  timestamp = Column(DateTime, default=datetime.utcnow)
52
 
53
+ class UserProfile(Base):
54
+ __tablename__ = "user_profiles"
55
+ id = Column(Integer, primary_key=True, index=True)
56
+ user_id = Column(String, unique=True, index=True)
57
+ name = Column(String, default="Valued Customer")
58
+ email = Column(String, default="unknown@example.com")
59
+ preferences = Column(Text, default="") # e.g., favorite dishes, dietary restrictions
60
+ last_interaction = Column(DateTime, default=datetime.utcnow)
61
+
62
+ class SentimentLog(Base):
63
+ __tablename__ = "sentiment_logs"
64
+ id = Column(Integer, primary_key=True, index=True)
65
+ user_id = Column(String, index=True)
66
+ timestamp = Column(DateTime, default=datetime.utcnow)
67
+ sentiment_score = Column(Float)
68
+ message = Column(Text)
69
+
70
+ # Create the asynchronous engine.
71
  engine = create_async_engine(DATABASE_URL, echo=True)
72
  async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
73
 
 
75
  async with engine.begin() as conn:
76
  await conn.run_sync(Base.metadata.create_all)
77
 
78
+ # --- Global In-Memory Stores ---
79
+ user_state = {} # For active conversation flows: { user_id: { "flow": str, "step": int, "data": dict, "last_active": datetime } }
80
+ conversation_context = {} # Optionally store extended conversation history (in-memory for demo)
81
+ proactive_timer = {} # Track last interaction times to send proactive greetings
82
 
83
  # Local menu with nutritional details
84
  menu_items = [
 
97
  session.add(entry)
98
  await session.commit()
99
 
100
+ async def log_sentiment(user_id: str, message: str, score: float):
101
+ """Store sentiment analysis results in the database."""
102
+ async with async_session() as session:
103
+ entry = SentimentLog(user_id=user_id, sentiment_score=score, message=message)
104
+ session.add(entry)
105
+ await session.commit()
106
+
107
+ def analyze_sentiment(text: str) -> float:
108
+ """Analyze text sentiment using TextBlob. Returns polarity between -1 and 1."""
109
+ blob = TextBlob(text)
110
+ return blob.sentiment.polarity
111
+
112
  def google_image_scrape(query: str) -> str:
113
+ """Scrape Google Images using BeautifulSoup to get an image URL."""
114
+ headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
 
 
 
 
 
115
  search_url = f"https://www.google.com/search?tbm=isch&q={query}"
116
  try:
117
  response = requests.get(search_url, headers=headers, timeout=5)
 
127
  return ""
128
 
129
  def create_paystack_payment_link(email: str, amount: int, reference: str) -> dict:
130
+ """Initialize a transaction via Paystack."""
 
 
 
 
 
 
131
  url = "https://api.paystack.co/transaction/initialize"
132
  headers = {
133
  "Authorization": f"Bearer {PAYSTACK_SECRET_KEY}",
 
149
  return {"status": False, "message": str(e)}
150
 
151
  # --- NVIDIA LLM Streaming Functions ---
 
152
  def stream_text_completion(prompt: str):
153
  """
154
  Stream text completion using NVIDIA's text-only model.
 
199
  if line:
200
  yield line.decode("utf-8") + "\n"
201
 
202
+ # --- Advanced Internal Flow: Order Processing & Payment Integration ---
203
+ def process_order_flow(user_id: str, message: str) -> str:
204
  """
205
+ A multi-step order process:
206
  - Step 1: Ask for dish.
207
  - Step 2: Ask for quantity.
208
+ After details are collected, save order and generate payment link.
209
  """
210
  if user_id in user_state:
211
  state = user_state[user_id]
 
221
  data["quantity"] = message
222
  order_id = f"ORD-{int(time.time())}"
223
  data["order_id"] = order_id
224
+ price_per_serving = 1500 # ₦1500 per serving
 
225
  total_price = int(data["quantity"]) * price_per_serving
226
  data["price"] = str(total_price)
227
+ # Save order asynchronously
228
  import asyncio
229
  async def save_order():
230
  async with async_session() as session:
 
239
  session.add(order)
240
  await session.commit()
241
  asyncio.create_task(save_order())
242
+ # Clear conversation state for order flow.
243
  del user_state[user_id]
244
+ # For personalization, try retrieving the user's email.
245
+ email = "customer@example.com" # In a real system, use UserProfile info.
246
  payment_data = create_paystack_payment_link(email, total_price * 100, order_id)
247
  if payment_data.get("status"):
248
  payment_link = payment_data["data"]["authorization_url"]
 
252
  return f"Your order has been placed with Order ID {order_id}, but we could not initialize payment. Please try again later."
253
  else:
254
  if "order" in message.lower():
255
+ user_state[user_id] = {"flow": "order", "step": 1, "data": {}, "last_active": datetime.utcnow()}
256
  return "Sure! What dish would you like to order?"
257
  return ""
258
 
259
+ # --- User Profile Functions ---
260
+ async def get_or_create_user_profile(user_id: str) -> UserProfile:
261
+ """Retrieve an existing profile or create a new one."""
262
+ async with async_session() as session:
263
+ result = await session.execute(
264
+ UserProfile.__table__.select().where(UserProfile.user_id == user_id)
265
+ )
266
+ profile = result.scalar_one_or_none()
267
+ if profile is None:
268
+ profile = UserProfile(user_id=user_id, last_interaction=datetime.utcnow())
269
+ session.add(profile)
270
+ await session.commit()
271
+ return profile
272
+
273
+ async def update_user_last_interaction(user_id: str):
274
+ """Update the user's last interaction timestamp."""
275
+ async with async_session() as session:
276
+ result = await session.execute(
277
+ UserProfile.__table__.select().where(UserProfile.user_id == user_id)
278
+ )
279
+ profile = result.scalar_one_or_none()
280
+ if profile:
281
+ profile.last_interaction = datetime.utcnow()
282
+ await session.commit()
283
+
284
+ # --- Proactive Engagement: Warm Greetings ---
285
+ async def send_proactive_greeting(user_id: str):
286
+ """Simulate sending a proactive greeting if the user has been inactive."""
287
+ # In a real system, you might schedule this using a job scheduler.
288
+ # Here we just simulate a warm greeting message.
289
+ greeting = "Hi again! We miss you. Would you like to see our new menu items or get personalized recommendations?"
290
+ await log_chat_to_db(user_id, "outbound", greeting)
291
+ return greeting
292
+
293
  # --- FastAPI Setup & Endpoints ---
294
  app = FastAPI()
295
 
 
297
  async def on_startup():
298
  await init_db()
299
 
300
+ # --- Chatbot Endpoint ---
301
  @app.post("/chatbot")
302
  async def chatbot_response(request: Request, background_tasks: BackgroundTasks):
303
  """
304
  Main chatbot endpoint.
305
+ Supports text queries, image queries, and advanced logic.
306
+ Expects JSON payload with:
307
  - 'user_id'
308
+ - 'message'
309
+ - Optionally, 'is_image': true and 'image_base64'
 
310
  """
311
  data = await request.json()
312
  user_id = data.get("user_id")
 
317
  if not user_id:
318
  raise HTTPException(status_code=400, detail="Missing user_id in payload.")
319
 
320
+ # Log inbound message
321
+ background_tasks.add_task(log_chat_to_db, user_id, "inbound", user_message)
322
+ # Update user last interaction and profile
323
+ await update_user_last_interaction(user_id)
324
+ await get_or_create_user_profile(user_id)
325
 
326
+ # Handle voice/image queries if applicable
327
  if is_image and image_b64:
 
328
  if len(image_b64) >= 180_000:
329
+ raise HTTPException(status_code=400, detail="Image too large.")
 
330
  return StreamingResponse(stream_image_completion(image_b64), media_type="text/plain")
331
+
332
+ # --- Advanced Textual Processing ---
333
+ # Analyze sentiment and log it
334
+ sentiment_score = analyze_sentiment(user_message)
335
+ background_tasks.add_task(log_sentiment, user_id, user_message, sentiment_score)
336
+
337
+ # Adjust response tone based on sentiment (for demonstration)
338
+ sentiment_modifier = ""
339
+ if sentiment_score < -0.3:
340
+ sentiment_modifier = "I'm sorry if you're having a tough time. "
341
+ elif sentiment_score > 0.3:
342
+ sentiment_modifier = "Great to hear from you! "
343
+
344
+ # Check for specialized commands:
345
  if "menu" in user_message.lower():
346
+ # Return menu with images
347
  menu_with_images = []
348
  for item in menu_items:
349
  image_url = google_image_scrape(item["name"])
350
+ menu_with_images.append({
351
+ "name": item["name"],
352
+ "description": item["description"],
353
+ "price": item["price"],
354
+ "image_url": image_url
355
+ })
356
  response_payload = {
357
+ "response": sentiment_modifier + "Here’s our delicious menu:",
358
  "menu": menu_with_images,
359
+ "follow_up": "Would you like nutritional facts for any dish? Just type, for example, 'Nutritional facts for Jollof Rice'."
 
360
  }
361
  background_tasks.add_task(log_chat_to_db, user_id, "outbound", str(response_payload))
362
  return JSONResponse(content=response_payload)
363
+
364
  if "nutritional facts for" in user_message.lower():
365
  dish_name = user_message.lower().replace("nutritional facts for", "").strip().title()
366
  dish = next((item for item in menu_items if item["name"].lower() == dish_name.lower()), None)
 
369
  else:
370
  response_text = f"Sorry, I couldn't find nutritional facts for {dish_name}."
371
  background_tasks.add_task(log_chat_to_db, user_id, "outbound", response_text)
372
+ return JSONResponse(content={"response": sentiment_modifier + response_text})
373
+
374
+ # Check if this is an order flow request
375
+ order_response = process_order_flow(user_id, user_message)
376
+ if order_response:
377
+ background_tasks.add_task(log_chat_to_db, user_id, "outbound", order_response)
378
+ return JSONResponse(content={"response": sentiment_modifier + order_response})
379
+
380
+ # For context-aware conversation: store conversation context
381
+ conversation_context.setdefault(user_id, []).append({"timestamp": datetime.utcnow().isoformat(), "message": user_message})
382
+
383
+ # Fallback: use NVIDIA text LLM streaming for a response
384
+ prompt = f"User query: {user_message}\nGenerate a helpful, personalized response for a restaurant chatbot."
385
  def stream_response():
386
  for chunk in stream_text_completion(prompt):
387
  yield chunk
 
388
  background_tasks.add_task(log_chat_to_db, user_id, "outbound", f"LLM fallback response for prompt: {prompt}")
389
  return StreamingResponse(stream_response(), media_type="text/plain")
390
 
391
+ # --- Chat History Endpoint ---
392
  @app.get("/chat_history/{user_id}")
393
  async def get_chat_history(user_id: str):
394
  """
395
+ Retrieve chat history for a user.
396
  """
397
  async with async_session() as session:
398
  result = await session.execute(
 
401
  history = result.fetchall()
402
  return [dict(row) for row in history]
403
 
404
+ # --- Order Details Endpoint ---
405
  @app.get("/order/{order_id}")
406
  async def get_order(order_id: str):
407
  """
408
+ Retrieve details for a specific order.
409
  """
410
  async with async_session() as session:
411
  result = await session.execute(
 
417
  else:
418
  raise HTTPException(status_code=404, detail="Order not found.")
419
 
420
+ # --- User Profile Endpoint ---
421
+ @app.get("/user_profile/{user_id}")
422
+ async def get_user_profile(user_id: str):
423
+ """
424
+ Retrieve the user profile.
425
+ """
426
+ profile = await get_or_create_user_profile(user_id)
427
+ return {
428
+ "user_id": profile.user_id,
429
+ "name": profile.name,
430
+ "email": profile.email,
431
+ "preferences": profile.preferences,
432
+ "last_interaction": profile.last_interaction.isoformat()
433
+ }
434
+
435
+ # --- Analytics Endpoint ---
436
+ @app.get("/analytics")
437
+ async def get_analytics():
438
+ """
439
+ Simple analytics dashboard endpoint.
440
+ Returns counts of messages, orders, and average sentiment.
441
+ """
442
+ async with async_session() as session:
443
+ # Total messages count
444
+ msg_result = await session.execute(ChatHistory.__table__.count())
445
+ total_messages = msg_result.scalar() or 0
446
+
447
+ # Total orders count
448
+ order_result = await session.execute(Order.__table__.count())
449
+ total_orders = order_result.scalar() or 0
450
+
451
+ # Average sentiment score
452
+ sentiment_result = await session.execute("SELECT AVG(sentiment_score) FROM sentiment_logs")
453
+ avg_sentiment = sentiment_result.scalar() or 0
454
+
455
+ return {
456
+ "total_messages": total_messages,
457
+ "total_orders": total_orders,
458
+ "average_sentiment": avg_sentiment
459
+ }
460
+
461
+ # --- Voice Integration Endpoint ---
462
+ @app.post("/voice")
463
+ async def process_voice(file: UploadFile = File(...)):
464
+ """
465
+ Accept a voice file upload, perform speech-to-text (simulated), and process the resulting text.
466
+ In production, integrate with a real STT service.
467
+ """
468
+ # Simulated Speech-to-Text: read file bytes and decode (for demo, just return a fixed string)
469
+ contents = await file.read()
470
+ # In real implementation, send `contents` to an STT service.
471
+ simulated_text = "Simulated speech-to-text conversion result."
472
+ return {"transcription": simulated_text}
473
+
474
+ # --- Payment Callback Endpoint (Stub) ---
475
+ @app.post("/payment_callback")
476
+ async def payment_callback(request: Request):
477
+ """
478
+ Endpoint to handle payment callbacks from Paystack.
479
+ Update order status based on callback data.
480
+ """
481
+ data = await request.json()
482
+ # Extract order reference and update order status accordingly.
483
+ # This is a stub – in production, verify callback signature, extract data, and update DB.
484
+ order_id = data.get("reference")
485
+ new_status = data.get("status", "Paid")
486
+ async with async_session() as session:
487
+ result = await session.execute(
488
+ Order.__table__.select().where(Order.order_id == order_id)
489
+ )
490
+ order = result.scalar_one_or_none()
491
+ if order:
492
+ order.status = new_status
493
+ await session.commit()
494
+ return JSONResponse(content={"message": "Order updated successfully."})
495
+ else:
496
+ raise HTTPException(status_code=404, detail="Order not found.")
497
+
498
  if __name__ == "__main__":
499
  import uvicorn
500
+ uvicorn.run(app, host="0.0.0.0", port=8000)