Suunil-Dabral commited on
Commit
1129f23
·
verified ·
1 Parent(s): ad31e1c

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +113 -442
src/streamlit_app.py CHANGED
@@ -19,92 +19,55 @@ st.set_page_config(
19
  st.markdown(
20
  """
21
  <style>
22
- .main {
23
- max-width: 480px;
24
- margin: 0 auto;
25
- }
26
  .foodhub-card {
27
  background: linear-gradient(135deg, #fff3e0, #ffe0cc);
28
- padding: 16px 20px;
29
- border-radius: 18px;
30
- border: 1px solid #f5c28c;
31
- margin-bottom: 16px;
32
  }
33
  .foodhub-header-title {
34
- margin-bottom: 4px;
35
- color: #e65c2b;
36
- font-size: 26px;
37
- font-weight: 800;
38
- }
39
- .foodhub-header-subtitle {
40
- margin: 0;
41
- color: #444;
42
- font-size: 14px;
43
  }
 
44
  .foodhub-assistant-pill {
45
- display: inline-flex;
46
- align-items: center;
47
- gap: 6px;
48
- background: #ffffff;
49
- border-radius: 999px;
50
- padding: 6px 10px;
51
- font-size: 11px;
52
- color: #555;
53
- border: 1px solid #ffd3a3;
54
- margin-top: 8px;
55
- }
56
- .status-dot {
57
- width: 8px;
58
- height: 8px;
59
- border-radius: 50%;
60
- background: #2ecc71;
61
  }
 
62
  .quick-actions-title {
63
- font-size: 13px;
64
- font-weight: 600;
65
- color: #555;
66
- margin-top: 4px;
67
- margin-bottom: 4px;
68
  }
69
  .bot-bubble {
70
- background: #eef7f2;
71
- border-radius: 12px;
72
- padding: 10px 12px;
73
- font-size: 14px;
74
- margin-top: 12px;
75
- }
76
- .user-label {
77
- font-size: 12px;
78
- color: #777;
79
- margin-top: 8px;
80
- margin-bottom: 2px;
81
- }
82
- .bot-label {
83
- font-size: 12px;
84
- color: #777;
85
- margin-top: 12px;
86
- margin-bottom: 2px;
87
  }
 
 
88
  </style>
89
  """,
90
  unsafe_allow_html=True,
91
  )
92
 
93
- # ================== DB CONFIG ==================
94
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
95
- CLEAN_DB_PATH = os.path.join(BASE_DIR, "orders_clean.db") # cleaned DB
96
 
97
- # Optional startup sanity check – helps catch wrong DB uploads early
98
  try:
99
  with sqlite3.connect(CLEAN_DB_PATH) as _conn:
100
  _cur = _conn.cursor()
101
  _cur.execute("SELECT name FROM sqlite_master WHERE type='table';")
102
  _tables = [r[0] for r in _cur.fetchall()]
 
103
  if "orders_clean" not in _tables:
104
  st.error(
105
  f"Connected to {CLEAN_DB_PATH!r}, but table 'orders_clean' was not found.\n\n"
106
- "Please ensure df_clean was saved to this DB with table name 'orders_clean' "
107
- "and that the correct orders_clean.db file is present next to app.py."
108
  )
109
  st.stop()
110
  except Exception as e:
@@ -120,15 +83,6 @@ if not groq_api_key:
120
  client = Groq(api_key=groq_api_key)
121
 
122
  # ================== SCHEMA (for LLM prompt only) ==================
123
- # ==============================================================================
124
- # ORDERS_SCHEMA — FINAL, CLEANED & FEATURE-ENGINEERED
125
- # ------------------------------------------------------------------------------
126
- # Reference schema for the fully cleaned + feature-engineered orders table
127
- # stored inside orders_clean.db
128
- # ------------------------------------------------------------------------------
129
- # Table Name: orders_clean
130
- # ==============================================================================
131
-
132
  ORDERS_SCHEMA = """
133
  Table: orders_clean
134
 
@@ -143,60 +97,39 @@ Columns:
143
  - delivery_eta (DATETIME/TEXT) : Estimated delivery timestamp
144
  - delivery_time (DATETIME/TEXT) : Actual delivery timestamp
145
 
146
- - order_status_std (TEXT) : Normalized order status
147
- (e.g., 'delivered', 'canceled', 'picked_up')
148
- - payment_status_std (TEXT) : Normalized payment status
149
- (e.g., 'cod', 'completed', 'canceled')
150
 
151
  - item_in_order (TEXT) : Comma-separated list of items
152
  - item_count (INTEGER) : Derived integer count of ordered items
153
 
154
  --- Feature Engineered Columns ---
155
- - prep_duration_min (REAL) : Minutes from order_time → prepared_time
156
- - delivery_duration_min (REAL) : Minutes from prepared_time → delivery_time
157
- - total_duration_min (REAL) : Minutes from order_time → delivery_time
158
-
159
- - on_time_delivery (INTEGER: 0/1) : 1 = ETA met, 0 = late or unknown
160
- - is_delivered (INTEGER: 0/1) : 1 = delivered, 0 = not delivered
161
- - is_canceled (INTEGER: 0/1) : 1 = canceled, 0 = not canceled
162
-
163
- Usage Notes:
164
- ------------
165
- ✓ Convert datetime-like TEXT columns to actual Python datetime objects before computing durations
166
- ✓ Always use LOWER(order_id) for safe matching
167
- ✓ Prefer explicit column selection — avoid SELECT * in production agents
168
- ✓ All engineered duration fields depend on successful datetime conversion
169
  """
170
 
171
  # =====================================================================
172
  # SQL SAFETY FIREWALL
173
  # =====================================================================
174
  def is_safe_sql(sql: str) -> bool:
175
- """
176
- Simple SQL firewall:
177
- - only SELECT allowed
178
- - no stacked queries
179
- - block destructive / DDL keywords
180
- - block SQL comment injections
181
- """
182
  if not isinstance(sql, str):
183
  return False
184
 
185
  sql_lower = sql.lower().strip()
186
 
187
- # Must start with SELECT (read-only)
188
  if not sql_lower.startswith("select"):
189
  return False
190
 
191
- # Block stacked queries like "SELECT ...; DROP TABLE ..."
192
  if re.search(r";\s*\S", sql_lower):
193
  return False
194
 
195
- forbidden_keywords = [
196
- "drop", "delete", "update", "insert",
197
- "alter", "truncate", "create",
198
- " hack ",
199
- ]
200
  if any(kw in sql_lower for kw in forbidden_keywords):
201
  return False
202
 
@@ -208,67 +141,44 @@ def is_safe_sql(sql: str) -> bool:
208
 
209
  # =====================================================================
210
  # run_sql_query()
211
- # (aligned with your final run_sql_query from Code 1/3)
212
  # =====================================================================
213
  def run_sql_query(sql: str) -> pd.DataFrame:
214
- """
215
- Safely execute SQL queries on the cleaned database
216
- and automatically convert timestamp fields into proper datetime types
217
- for accurate chatbot calculations (ETA, SLA, delays, durations, etc.).
218
- """
219
- # Step 1: Basic normalization
220
  if not isinstance(sql, str):
221
  return pd.DataFrame([{"message": "🚫 Invalid SQL type (expected string)."}])
222
  sql = sql.strip()
223
 
224
- # Step 2: Validate SQL using custom firewall logic
225
  if not is_safe_sql(sql):
226
  return pd.DataFrame([{"message": "🚫 Blocked unsafe or unsupported SQL."}])
227
 
228
  try:
229
- # Step 3: Execute SQL using a fresh DB connection
230
  with sqlite3.connect(CLEAN_DB_PATH) as connection:
231
  df = pd.read_sql_query(sql, connection)
232
 
233
- # Step 4: Convert all string timestamp columns back to datetime
234
  time_cols = ["order_time", "preparing_eta", "prepared_time", "delivery_eta", "delivery_time"]
235
  for col in time_cols:
236
  if col in df.columns:
237
  df[col] = pd.to_datetime(df[col], errors="coerce")
238
 
239
  return df
240
-
241
  except Exception as e:
242
- # Step 5: Fail gracefully with diagnostic message
243
  return pd.DataFrame([{"message": f"⚠️ SQL execution error: {str(e)}"}])
244
 
245
  # =====================================================================
246
- # llm_to_sql() (Code 1 / Code 2 merged)
247
  # =====================================================================
248
  def llm_to_sql(user_message: str) -> str:
249
  """
250
- Convert a natural language user query into a safe, valid SQLite SELECT query
251
  targeting the orders_clean table inside orders_clean.db.
252
  """
253
- # Normalize user text for pattern matching
254
  text = (user_message or "").lower().strip()
255
 
256
- # -------------------------------------------------------
257
- # FAST-PATH: If an order_id like O12488 exists → skip LLM
258
- # -------------------------------------------------------
259
  match = re.search(r"\b(o\d+)\b", text)
260
  if match:
261
- order_id = match.group(1) # already lowercase
262
- sql = (
263
- "SELECT * FROM orders_clean "
264
- f"WHERE LOWER(order_id) = LOWER('{order_id}')"
265
- )
266
- # Final safety gate
267
  return sql if is_safe_sql(sql) else "SELECT 'Unable to answer safely.' AS message;"
268
 
269
- # -------------------------------------------------------
270
- # LLM PATH — General business questions (no order_id found)
271
- # -------------------------------------------------------
272
  system_prompt = f"""
273
  You are an expert SQLite assistant for a food delivery company.
274
 
@@ -282,7 +192,7 @@ RULES:
282
  - Allowed operation: SELECT only
283
  - If filtering by order_id, ALWAYS use:
284
  LOWER(order_id) = LOWER('<value>')
285
- - If unsure how to answer from available data:
286
  SELECT 'Unable to answer with available data.' AS message;
287
  """
288
 
@@ -291,27 +201,26 @@ RULES:
291
  temperature=0.1,
292
  messages=[
293
  {"role": "system", "content": system_prompt},
294
- {"role": "user", "content": user_message},
295
  ],
296
  )
297
 
298
  sql = (response.choices[0].message.content or "").strip()
299
 
300
- # Clean fences if LLM returned ```sql ...```
301
  if sql.startswith("```"):
302
  sql = re.sub(r"^```sql", "", sql, flags=re.IGNORECASE).strip()
303
  sql = re.sub(r"^```", "", sql).strip()
304
  sql = sql.replace("```", "").strip()
305
 
306
- # Force table name orders_clean
307
  sql = re.sub(
308
- r"\bfrom\s+\w+\b",
309
  "FROM orders_clean",
310
  sql,
 
311
  flags=re.IGNORECASE,
312
  )
313
 
314
- # Normalize order_id equality to LOWER(...) form
315
  sql = re.sub(
316
  r"where\s+order_id\s*=\s*'([^']+)'",
317
  r"WHERE LOWER(order_id) = LOWER('\1')",
@@ -319,17 +228,15 @@ RULES:
319
  flags=re.IGNORECASE,
320
  )
321
 
322
- # Final safety gate
323
  if not is_safe_sql(sql):
324
  return "SELECT 'Unable to answer safely.' AS message;"
325
 
326
  return sql
327
 
328
  # =====================================================================
329
- # analyze_sentiment_and_escalation() (Code 3)
330
  # =====================================================================
331
  def analyze_sentiment_and_escalation(user_message: str) -> dict:
332
- # Basic normalization
333
  if user_message is None:
334
  user_message = ""
335
  user_message = str(user_message).strip()
@@ -337,36 +244,21 @@ def analyze_sentiment_and_escalation(user_message: str) -> dict:
337
  system_prompt = """
338
  You are a classifier for a food delivery chatbot.
339
 
340
- Given a single customer message, you must return a JSON object ONLY, with no extra text:
341
-
342
  {
343
  "sentiment": "calm" | "neutral" | "frustrated" | "angry",
344
  "escalate": true or false
345
  }
346
 
347
- Rules:
348
- - Use "calm" when the user is relaxed or appreciative.
349
- - Use "neutral" when they are just asking for information.
350
- - Use "frustrated" when they express dissatisfaction, confusion, or mild complaints.
351
- - Use "angry" when they use strong negative language, threats, or clear anger.
352
-
353
- Escalate = true when:
354
- - the user sounds upset, frustrated, or angry,
355
- - they mention repeated attempts with no resolution,
356
- - they demand urgent or immediate help.
357
-
358
- If you are uncertain, default to:
359
- {
360
- "sentiment": "neutral",
361
- "escalate": false
362
- }
363
  """
364
 
365
  response = client.chat.completions.create(
366
  model="llama-3.3-70b-versatile",
367
  messages=[
368
  {"role": "system", "content": system_prompt},
369
- {"role": "user", "content": user_message},
370
  ],
371
  temperature=0.0,
372
  )
@@ -378,105 +270,47 @@ If you are uncertain, default to:
378
  if not isinstance(info, dict):
379
  raise ValueError("Not a JSON object")
380
  if "sentiment" not in info or "escalate" not in info:
381
- raise ValueError("Missing required keys")
 
 
 
 
382
  except (json.JSONDecodeError, ValueError):
383
  info = {"sentiment": "neutral", "escalate": False}
384
 
385
  return info
386
 
387
  # =====================================================================
388
- # is_misuse_or_hacker_attempt() (Code 2, extended)
389
  # =====================================================================
390
  def is_misuse_or_hacker_attempt(user_message: str) -> bool:
391
- """
392
- Detect attempts to:
393
- - Hack or bypass security
394
- - Retrieve unauthorized bulk data (ALL orders / ALL customers)
395
- - Perform SQL injection or prompt injection
396
- """
397
  text = (user_message or "").lower().strip()
398
 
399
- # Direct harmful / hacking intent
400
- hacker_keywords = [
401
- "hack",
402
- "hacker",
403
- "hacking",
404
- "hacked",
405
- "i am the hacker",
406
- "i am hacker",
407
- "breach",
408
- "exploit",
409
- "bypass security",
410
- "disable firewall",
411
- "root access",
412
- "admin access",
413
  "reverse engineer",
 
 
 
 
 
 
 
 
 
414
  ]
415
 
416
- # Attempts to extract private or BULK data
417
- bulk_keywords = [
418
- "all orders",
419
- "every order",
420
- "all my orders",
421
- "all previous orders",
422
- "all past orders",
423
- "order history",
424
- "full database",
425
- "entire database",
426
- "all customers",
427
- "customer list",
428
- "download database",
429
- "dump data",
430
- "export all",
431
- "export everything",
432
- "show everything",
433
- ]
434
-
435
- # Jailbreak / prompt injection indicators
436
- jailbreak_keywords = [
437
- "ignore previous instructions",
438
- "override rules",
439
- "forget rules",
440
- "developer mode",
441
- "you are no longer restricted",
442
- "pretend you are",
443
- "act like",
444
- ]
445
-
446
- # SQL injection indicators
447
- sql_injection_keywords = [
448
- "1=1",
449
- "union select",
450
- "drop table",
451
- "delete from",
452
- "insert into",
453
- "alter table",
454
- "truncate",
455
- ]
456
-
457
- # Only flag semicolons if other SQL keywords appear (stacked SQL)
458
- if ";" in text and any(sig in text for sig in sql_injection_keywords):
459
  return True
460
 
461
- all_signals = (
462
- hacker_keywords
463
- + bulk_keywords
464
- + jailbreak_keywords
465
- + sql_injection_keywords
466
- )
467
-
468
  return any(sig in text for sig in all_signals)
469
 
470
  # =====================================================================
471
- # create_ticket() (Code 4, aligned with CLEAN_DB_PATH)
472
  # =====================================================================
473
- def create_ticket(
474
- user_message: str,
475
- sentiment: str,
476
- ticket_type: str = "general",
477
- order_id: str = None
478
- ) -> str:
479
- # Extract order_id if not given (pattern: O12345)
480
  if order_id is None:
481
  match = re.search(r"\b(o\d+)\b", (user_message or "").lower())
482
  order_id = match.group(1).upper() if match else None
@@ -498,10 +332,7 @@ def create_ticket(
498
  );
499
  """)
500
  cursor.execute("""
501
- INSERT INTO tickets (
502
- ticket_id, ticket_type, sentiment, message,
503
- order_id, status, created_at
504
- )
505
  VALUES (?, ?, ?, ?, ?, 'Open', ?);
506
  """, (ticket_id, ticket_type, sentiment, user_message, order_id, created_at))
507
  conn.commit()
@@ -509,7 +340,7 @@ def create_ticket(
509
  return ticket_id
510
 
511
  # =====================================================================
512
- # sql_result_to_response() (Code 1 / refined version)
513
  # =====================================================================
514
  def sql_result_to_response(
515
  user_message: str,
@@ -519,175 +350,94 @@ def sql_result_to_response(
519
  language: str = "auto",
520
  max_rows_to_list: int = 5
521
  ) -> str:
522
- # 0. Handle error DataFrames (from run_sql_query) directly
523
- if (
524
- isinstance(df, pd.DataFrame)
525
- and "message" in df.columns
526
- and len(df) == 1
527
- ):
528
  return str(df.iloc[0]["message"])
529
 
530
- # 1. Handle NO MATCHING DATA explicitly
531
  if df.empty:
532
  return (
533
  "I couldn’t find any orders matching the details you provided. "
534
- "Please double-check your order ID or the information you entered, "
535
- "and try again."
536
  )
537
 
538
- # 2. Prepare data (truncate)
539
  df_for_llm = df.head(max_rows_to_list).copy()
540
  result_data = df_for_llm.to_dict(orient="records")
541
 
542
- # Emojis
543
  if include_emojis:
544
- intro_line = (
545
- "Welcome to FoodHub Support! 👋 I'm your virtual assistant here to help you.\n"
546
- )
547
- emoji_instruction = (
548
- "Include a few relevant, tasteful emojis (like ✅, 🚚, ⏱️, 🍕, ℹ️, ⚠️, 💛) "
549
- "to make it friendly, but do not overuse them.\n"
550
- )
551
  else:
552
- intro_line = (
553
- "Welcome to FoodHub Support! I’m your virtual assistant — here to help you.\n"
554
- )
555
- emoji_instruction = "Do NOT use any emojis in your response.\n"
556
 
557
- # Tone
558
- if tone == "friendly-casual":
559
- tone_instruction = "Use a friendly, conversational tone while staying respectful.\n"
560
- elif tone == "formal":
561
- tone_instruction = "Use a formal, professional tone.\n"
562
- else:
563
- tone_instruction = "Use a polite, professional tone, warm but not overly casual.\n"
564
-
565
- # Language
566
- if language == "auto":
567
- language_instruction = (
568
- "Detect the user's language and respond in the SAME language. "
569
- "If unsure, default to English.\n"
570
- )
571
- else:
572
- language_instruction = f"Respond in this language: {language}.\n"
573
 
574
  system_prompt = f"""
575
  You are a polite and professional customer support chatbot for FoodHub.
576
 
577
- Always start with a short greeting like:
578
  {intro_line}
579
 
580
- VERY IMPORTANT POLICY:
581
- - If ANY matching record exists in the data I give you The order is CONFIRMED.
582
- - NEVER tell the user to re-check their order ID if matching rows exist.
583
- - NEVER say "order not found", "unable to locate order", or similar messages
584
- when there is at least one matching record.
585
- - Instead, confidently acknowledge the order and use available data.
586
-
587
- When answering:
588
- - Explain the latest known order status clearly.
589
- - If 'delivery_time' is missing but 'delivery_eta' exists:
590
- - Say the order is still in progress, and share ETA.
591
- - If both ETA and delivery_time are missing:
592
- - Apologize for limited tracking information.
593
- - If delivered and 'delivery_time' exists:
594
- - Confirm delivery and mention the exact time (in friendly wording).
595
- - If SLA columns exist:
596
- - If 'on_time_delivery' == 1 → reassure user their order was/has been on time.
597
- - If delayed → apologize sincerely and reassure support.
598
- - For multiple rows → summarize count + list top {max_rows_to_list} orders.
599
-
600
- Tone & style rules:
601
- {tone_instruction}
602
- {emoji_instruction}
603
- {language_instruction}
604
- - Keep responses concise and human-friendly.
605
- - Use natural phrasing like "around 1:00 PM" instead of "13:00:00".
606
- - End warmly, e.g.: "If you need anything else, I’m here to help! 💛"
607
  """
608
 
609
- content = (
610
- f"User question: {user_message}\n\n"
611
- f"Matching order data (up to {max_rows_to_list} rows):\n{result_data}"
612
- )
613
 
614
  response = client.chat.completions.create(
615
  model="llama-3.3-70b-versatile",
616
  temperature=0.4,
617
  messages=[
618
  {"role": "system", "content": system_prompt},
619
- {"role": "user", "content": content},
620
  ],
621
  )
622
-
623
  return response.choices[0].message.content.strip()
624
 
625
  # =====================================================================
626
- # answer_user_question() (Code 1: NL → SQL → DF → friendly reply)
627
  # =====================================================================
628
  def answer_user_question(user_message: str) -> str:
629
- """
630
- Secure natural-language answering pipeline:
631
- - Convert natural language �� SQL
632
- - Run safe SQL
633
- - Summarize result into a polite, empathetic customer-service style reply
634
- (Hacker/misuse is handled at chatbot_response level via is_misuse_or_hacker_attempt)
635
- """
636
- # Step 1 — Natural language → SQL
637
  sql = llm_to_sql(user_message)
638
  print("Generated SQL:", sql)
639
-
640
- # Step 2 — Run SQL safely
641
  df = run_sql_query(sql)
642
-
643
- # Step 3 — Convert SQL results to a friendly, human-like response
644
- reply = sql_result_to_response(
645
- user_message=user_message,
646
- df=df,
647
- tone="polite-professional",
648
- include_emojis=True,
649
- language="auto",
650
- max_rows_to_list=5,
651
- )
652
-
653
- return reply
654
 
655
  # =====================================================================
656
- # chatbot_response() – main orchestration (Code 5 + your extra intents)
657
  # =====================================================================
658
  def chatbot_response(user_message: str) -> str:
659
- # Safely normalize user input
660
  text = (user_message or "").lower().strip()
661
 
662
- # Pre-compute order ID presence once
663
  order_id_match = re.search(r"\b[oO]\d{3,}\b", text)
664
  has_order_id = order_id_match is not None
665
 
666
- # 0) Small-talk / greetings handling
667
  greetings = ["hi", "hello", "hey", "good morning", "good evening", "good afternoon"]
668
  thanks_words = ["thank you", "thanks", "thx", "thankyou"]
669
 
670
  if any(text == g or text.startswith(g + " ") for g in greetings):
671
  return (
672
  "👋 Hi there! Welcome to FoodHub Support.\n"
673
- "You can ask me about your order status, delivery, cancellation, or payment/refund issues. 💛"
674
  )
675
 
676
  if any(w in text for w in thanks_words):
677
  return (
678
  "You’re most welcome! 💛\n"
679
- "If you need any help with your order, just share your question or Order ID (like `O12488`)."
680
  )
681
 
682
- # 1) Block hacking / misuse attempts
683
  if is_misuse_or_hacker_attempt(user_message):
684
  return (
685
  "😔 Sorry, I can’t assist with that.\n"
686
  "For everyone’s safety, I can only help with your own order using a valid order ID.\n"
687
- "Try asking something like: *Where is my order O12488?* 😊"
688
  )
689
 
690
- # 2) Escalation detection for repeated unresolved concerns
691
  escalation_keywords = [
692
  "raised the query multiple times",
693
  "asked multiple times",
@@ -699,106 +449,38 @@ def chatbot_response(user_message: str) -> str:
699
  senti_info = analyze_sentiment_and_escalation(user_message)
700
  sentiment = senti_info.get("sentiment", "angry")
701
 
702
- ticket_id = create_ticket(
703
- user_message,
704
- sentiment,
705
- ticket_type="escalation",
706
- )
707
-
708
- # Empathetic escalation response
709
- return (
710
- "💛 I’m really sorry that you’ve had to follow up multiple times and still don’t have a clear update — "
711
- "I completely understand how frustrating that feels.\n\n"
712
- "I’ve now **escalated your case to a senior support agent** so it gets immediate attention.\n\n"
713
- f"📌 Your escalation ticket ID is **{ticket_id}**.\n\n"
714
- "They’ll review your case and the order history on priority and get back to you as soon as possible.\n"
715
- "If you haven’t already, please share your **Order ID** (for example: `O12488`) and any key details so we can resolve this even faster."
716
- )
717
 
718
- # 3) Order cancellation intent detection
719
- if (
720
- "cancel my order" in text
721
- or "cancel order" in text
722
- or "i want to cancel my order" in text
723
- or ("cancel" in text and "order" in text)
724
- ):
725
- match = order_id_match
726
- if match:
727
- order_id = match.group(0).upper()
728
-
729
- senti_info = analyze_sentiment_and_escalation(user_message)
730
- sentiment = senti_info.get("sentiment", "neutral")
731
-
732
- ticket_id = create_ticket(
733
- f"Cancellation request for {order_id}. Original message: {user_message}",
734
- sentiment,
735
- ticket_type="cancellation",
736
- order_id=order_id,
737
- )
738
-
739
- return (
740
- f"🙏 I understand you would like to cancel **Order {order_id}**, and I'm really sorry "
741
- f"if there was any inconvenience that led to this decision.\n\n"
742
- f"📝 Your cancellation request has been successfully recorded.\n"
743
- f"🔎 A customer support specialist will now verify and process it as soon as possible.\n\n"
744
- f"📌 **Ticket Reference ID:** `{ticket_id}`\n\n"
745
- f"Please let me know if you'd like to modify the order instead — I'm here to help 😊"
746
- )
747
-
748
- # No order ID found in a cancel intent → empathetic ask
749
  return (
750
- "💛 I’m sorry to hear you’d like to cancel your order — I completely understand and I’m here to help.\n"
751
- "Could you please share your **Order ID** (for example: `O12488`) so I can check the details and help "
752
- "process the cancellation for you?"
 
753
  )
754
 
755
- # 4) Payment / refund intent detection (needs order ID)
756
  if ("payment" in text or "refund" in text) and not has_order_id:
757
  return (
758
- "💛 I’m sorry you’re facing an issue with the payment or refund.\n"
759
- "I’ll help you with this right away.\n\n"
760
- "Could you please share your **Order ID** (for example: `O12488`) "
761
- "so I can check the payment status and update you?"
762
  )
763
 
764
- # 5) Status / details intents that require Order ID
765
  order_status_intents = [
766
- "where is my order",
767
- "track my order",
768
- "track order",
769
- "order status",
770
- "status of my order",
771
- "delivery status",
772
- "when will my order arrive",
773
- "when will it arrive",
774
- "order not delivered",
775
- "order is late",
776
- "order delayed",
777
- "delay in delivery",
778
- "order details",
779
- "details for my order",
780
- "details of my order",
781
- "show me the details for my order",
782
  ]
783
- has_status_intent = any(phrase in text for phrase in order_status_intents)
784
-
785
- if has_status_intent and not has_order_id:
786
- return (
787
- "I’d be happy to check that for you 💛\n"
788
- "Please share your **Order ID** (for example: `O12488`) so I can look up the exact status."
789
- )
790
 
791
- # 6) Normal path → delegate to SQL-based order assistant
792
  try:
793
  return answer_user_question(user_message)
794
  except Exception as e:
795
- print("[chatbot_response] Error while processing query:", e)
796
  return (
797
  "Sorry, I ran into a small issue while processing this request. 🙏\n"
798
- "Please make sure you mention a valid order ID like `O12488`."
799
  )
800
 
801
- # Simple wrapper so the UI still calls `chatbot(...)`
802
  def chatbot(msg: str) -> str:
803
  return chatbot_response(msg)
804
 
@@ -806,20 +488,16 @@ def chatbot(msg: str) -> str:
806
  if "welcome_shown" not in st.session_state:
807
  st.session_state["welcome_shown"] = False
808
 
809
- # ================== UI (unchanged) ==================
810
-
811
- # Header card (replaces logo / banner)
812
  st.markdown(
813
  """
814
  <div class="foodhub-card">
815
  <div style="display:flex; align-items:flex-start; gap:12px;">
816
  <div style="font-size:32px;">🍕</div>
817
  <div style="flex:1%;">
818
- <div class="foodhub-header-title">
819
- FoodHub AI Support
820
- </div>
821
  <p class="foodhub-header-subtitle">
822
- Ask about your order status, delivery updates, cancellations, or payment issues.
823
  </p>
824
  <div class="foodhub-assistant-pill">
825
  <span class="status-dot"></span>
@@ -832,13 +510,11 @@ st.markdown(
832
  unsafe_allow_html=True,
833
  )
834
 
835
- # One-time welcome (not repeated per message)
836
  if not st.session_state["welcome_shown"]:
837
  st.success("Welcome to FoodHub Support! I’m your virtual assistant for order, delivery, and payment queries.")
838
  st.caption("Because great food deserves great service 💛")
839
  st.session_state["welcome_shown"] = True
840
 
841
- # Quick action suggestions (mobile friendly)
842
  st.markdown('<div class="quick-actions-title">Quick help options</div>', unsafe_allow_html=True)
843
  col1, col2 = st.columns(2)
844
 
@@ -853,7 +529,6 @@ if col3.button("❌ Cancel order", use_container_width=True):
853
  if col4.button("🧾 Order details", use_container_width=True):
854
  st.session_state["chat_input"] = "Show me the details for my order."
855
 
856
- # Main chat input
857
  query = st.text_input(
858
  "💬 Type your question here:",
859
  key="chat_input",
@@ -866,14 +541,10 @@ if send_clicked:
866
  if query.strip():
867
  reply = chatbot(query.strip())
868
 
869
- # Simple conversational layout (last turn)
870
  st.markdown('<div class="user-label">You</div>', unsafe_allow_html=True)
871
  st.markdown(f"<div class='bot-bubble' style='background:#fff;'>💬 {query}</div>", unsafe_allow_html=True)
872
 
873
  st.markdown('<div class="bot-label">FoodHub Assistant</div>', unsafe_allow_html=True)
874
- st.markdown(
875
- f"<div class='bot-bubble'>🤖 {reply}</div>",
876
- unsafe_allow_html=True,
877
- )
878
  else:
879
  st.warning("Please enter a message to continue.")
 
19
  st.markdown(
20
  """
21
  <style>
22
+ .main { max-width: 480px; margin: 0 auto; }
 
 
 
23
  .foodhub-card {
24
  background: linear-gradient(135deg, #fff3e0, #ffe0cc);
25
+ padding: 16px 20px; border-radius: 18px;
26
+ border: 1px solid #f5c28c; margin-bottom: 16px;
 
 
27
  }
28
  .foodhub-header-title {
29
+ margin-bottom: 4px; color: #e65c2b;
30
+ font-size: 26px; font-weight: 800;
 
 
 
 
 
 
 
31
  }
32
+ .foodhub-header-subtitle { margin: 0; color: #444; font-size: 14px; }
33
  .foodhub-assistant-pill {
34
+ display: inline-flex; align-items: center; gap: 6px;
35
+ background: #ffffff; border-radius: 999px;
36
+ padding: 6px 10px; font-size: 11px; color: #555;
37
+ border: 1px solid #ffd3a3; margin-top: 8px;
 
 
 
 
 
 
 
 
 
 
 
 
38
  }
39
+ .status-dot { width: 8px; height: 8px; border-radius: 50%; background: #2ecc71; }
40
  .quick-actions-title {
41
+ font-size: 13px; font-weight: 600; color: #555;
42
+ margin-top: 4px; margin-bottom: 4px;
 
 
 
43
  }
44
  .bot-bubble {
45
+ background: #eef7f2; border-radius: 12px;
46
+ padding: 10px 12px; font-size: 14px; margin-top: 12px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  }
48
+ .user-label { font-size: 12px; color: #777; margin-top: 8px; margin-bottom: 2px; }
49
+ .bot-label { font-size: 12px; color: #777; margin-top: 12px; margin-bottom: 2px; }
50
  </style>
51
  """,
52
  unsafe_allow_html=True,
53
  )
54
 
55
+ # ================== DB CONFIG (ALIGNED WITH NOTEBOOK) ==================
56
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
57
+ CLEAN_DB_PATH = os.path.join(BASE_DIR, "orders_clean.db") # <-- aligned with notebook
58
 
59
+ # Startup sanity check – catches wrong DB uploads early
60
  try:
61
  with sqlite3.connect(CLEAN_DB_PATH) as _conn:
62
  _cur = _conn.cursor()
63
  _cur.execute("SELECT name FROM sqlite_master WHERE type='table';")
64
  _tables = [r[0] for r in _cur.fetchall()]
65
+
66
  if "orders_clean" not in _tables:
67
  st.error(
68
  f"Connected to {CLEAN_DB_PATH!r}, but table 'orders_clean' was not found.\n\n"
69
+ "Please ensure df_eng was saved to this DB with table name 'orders_clean' "
70
+ "(df_eng.to_sql('orders_clean', ...)) and that orders_clean.db is present next to app.py."
71
  )
72
  st.stop()
73
  except Exception as e:
 
83
  client = Groq(api_key=groq_api_key)
84
 
85
  # ================== SCHEMA (for LLM prompt only) ==================
 
 
 
 
 
 
 
 
 
86
  ORDERS_SCHEMA = """
87
  Table: orders_clean
88
 
 
97
  - delivery_eta (DATETIME/TEXT) : Estimated delivery timestamp
98
  - delivery_time (DATETIME/TEXT) : Actual delivery timestamp
99
 
100
+ - order_status_std (TEXT) : Normalized order status
101
+ - payment_status_std (TEXT) : Normalized payment status
 
 
102
 
103
  - item_in_order (TEXT) : Comma-separated list of items
104
  - item_count (INTEGER) : Derived integer count of ordered items
105
 
106
  --- Feature Engineered Columns ---
107
+ - prep_duration_min (REAL)
108
+ - delivery_duration_min (REAL)
109
+ - total_duration_min (REAL)
110
+
111
+ - on_time_delivery (INTEGER: 0/1)
112
+ - is_delivered (INTEGER: 0/1)
113
+ - is_canceled (INTEGER: 0/1)
 
 
 
 
 
 
 
114
  """
115
 
116
  # =====================================================================
117
  # SQL SAFETY FIREWALL
118
  # =====================================================================
119
  def is_safe_sql(sql: str) -> bool:
 
 
 
 
 
 
 
120
  if not isinstance(sql, str):
121
  return False
122
 
123
  sql_lower = sql.lower().strip()
124
 
 
125
  if not sql_lower.startswith("select"):
126
  return False
127
 
128
+ # block stacked queries
129
  if re.search(r";\s*\S", sql_lower):
130
  return False
131
 
132
+ forbidden_keywords = ["drop", "delete", "update", "insert", "alter", "truncate", "create"]
 
 
 
 
133
  if any(kw in sql_lower for kw in forbidden_keywords):
134
  return False
135
 
 
141
 
142
  # =====================================================================
143
  # run_sql_query()
 
144
  # =====================================================================
145
  def run_sql_query(sql: str) -> pd.DataFrame:
 
 
 
 
 
 
146
  if not isinstance(sql, str):
147
  return pd.DataFrame([{"message": "🚫 Invalid SQL type (expected string)."}])
148
  sql = sql.strip()
149
 
 
150
  if not is_safe_sql(sql):
151
  return pd.DataFrame([{"message": "🚫 Blocked unsafe or unsupported SQL."}])
152
 
153
  try:
 
154
  with sqlite3.connect(CLEAN_DB_PATH) as connection:
155
  df = pd.read_sql_query(sql, connection)
156
 
 
157
  time_cols = ["order_time", "preparing_eta", "prepared_time", "delivery_eta", "delivery_time"]
158
  for col in time_cols:
159
  if col in df.columns:
160
  df[col] = pd.to_datetime(df[col], errors="coerce")
161
 
162
  return df
 
163
  except Exception as e:
 
164
  return pd.DataFrame([{"message": f"⚠️ SQL execution error: {str(e)}"}])
165
 
166
  # =====================================================================
167
+ # llm_to_sql()
168
  # =====================================================================
169
  def llm_to_sql(user_message: str) -> str:
170
  """
171
+ Convert natural language to a safe SQLite SELECT query
172
  targeting the orders_clean table inside orders_clean.db.
173
  """
 
174
  text = (user_message or "").lower().strip()
175
 
 
 
 
176
  match = re.search(r"\b(o\d+)\b", text)
177
  if match:
178
+ order_id = match.group(1)
179
+ sql = "SELECT * FROM orders_clean " f"WHERE LOWER(order_id) = LOWER('{order_id}')"
 
 
 
 
180
  return sql if is_safe_sql(sql) else "SELECT 'Unable to answer safely.' AS message;"
181
 
 
 
 
182
  system_prompt = f"""
183
  You are an expert SQLite assistant for a food delivery company.
184
 
 
192
  - Allowed operation: SELECT only
193
  - If filtering by order_id, ALWAYS use:
194
  LOWER(order_id) = LOWER('<value>')
195
+ - If unsure:
196
  SELECT 'Unable to answer with available data.' AS message;
197
  """
198
 
 
201
  temperature=0.1,
202
  messages=[
203
  {"role": "system", "content": system_prompt},
204
+ {"role": "user", "content": user_message},
205
  ],
206
  )
207
 
208
  sql = (response.choices[0].message.content or "").strip()
209
 
 
210
  if sql.startswith("```"):
211
  sql = re.sub(r"^```sql", "", sql, flags=re.IGNORECASE).strip()
212
  sql = re.sub(r"^```", "", sql).strip()
213
  sql = sql.replace("```", "").strip()
214
 
215
+ # safer: only replace first FROM target if not already orders_clean
216
  sql = re.sub(
217
+ r"\bfrom\s+(?!orders_clean\b)([a-zA-Z_]\w*)\b",
218
  "FROM orders_clean",
219
  sql,
220
+ count=1,
221
  flags=re.IGNORECASE,
222
  )
223
 
 
224
  sql = re.sub(
225
  r"where\s+order_id\s*=\s*'([^']+)'",
226
  r"WHERE LOWER(order_id) = LOWER('\1')",
 
228
  flags=re.IGNORECASE,
229
  )
230
 
 
231
  if not is_safe_sql(sql):
232
  return "SELECT 'Unable to answer safely.' AS message;"
233
 
234
  return sql
235
 
236
  # =====================================================================
237
+ # analyze_sentiment_and_escalation()
238
  # =====================================================================
239
  def analyze_sentiment_and_escalation(user_message: str) -> dict:
 
240
  if user_message is None:
241
  user_message = ""
242
  user_message = str(user_message).strip()
 
244
  system_prompt = """
245
  You are a classifier for a food delivery chatbot.
246
 
247
+ Return ONLY JSON:
 
248
  {
249
  "sentiment": "calm" | "neutral" | "frustrated" | "angry",
250
  "escalate": true or false
251
  }
252
 
253
+ Escalate = true when user is frustrated/angry, repeats no resolution, or demands urgent help.
254
+ If uncertain, return: {"sentiment":"neutral","escalate":false}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  """
256
 
257
  response = client.chat.completions.create(
258
  model="llama-3.3-70b-versatile",
259
  messages=[
260
  {"role": "system", "content": system_prompt},
261
+ {"role": "user", "content": user_message},
262
  ],
263
  temperature=0.0,
264
  )
 
270
  if not isinstance(info, dict):
271
  raise ValueError("Not a JSON object")
272
  if "sentiment" not in info or "escalate" not in info:
273
+ raise ValueError("Missing keys")
274
+ info["sentiment"] = str(info["sentiment"]).strip().lower()
275
+ info["escalate"] = bool(info["escalate"])
276
+ if info["sentiment"] not in {"calm", "neutral", "frustrated", "angry"}:
277
+ raise ValueError("Invalid sentiment")
278
  except (json.JSONDecodeError, ValueError):
279
  info = {"sentiment": "neutral", "escalate": False}
280
 
281
  return info
282
 
283
  # =====================================================================
284
+ # is_misuse_or_hacker_attempt()
285
  # =====================================================================
286
  def is_misuse_or_hacker_attempt(user_message: str) -> bool:
 
 
 
 
 
 
287
  text = (user_message or "").lower().strip()
288
 
289
+ all_signals = [
290
+ # hacking intent
291
+ "hack", "hacker", "hacking", "hacked", "breach", "exploit",
292
+ "bypass security", "disable firewall", "root access", "admin access",
 
 
 
 
 
 
 
 
 
 
293
  "reverse engineer",
294
+ # bulk extraction
295
+ "all orders", "every order", "order history", "full database", "entire database",
296
+ "all customers", "customer list", "download database", "dump data", "export all",
297
+ "export everything", "show everything",
298
+ # jailbreak
299
+ "ignore previous instructions", "override rules", "forget rules", "developer mode",
300
+ "you are no longer restricted", "pretend you are", "act like",
301
+ # SQL injection
302
+ "1=1", "union select", "drop table", "delete from", "insert into", "alter table", "truncate",
303
  ]
304
 
305
+ if ";" in text and any(sig in text for sig in ["1=1", "union select", "drop table", "delete from", "insert into", "alter table", "truncate"]):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  return True
307
 
 
 
 
 
 
 
 
308
  return any(sig in text for sig in all_signals)
309
 
310
  # =====================================================================
311
+ # create_ticket() (writes to SAME DB file: orders_clean.db)
312
  # =====================================================================
313
+ def create_ticket(user_message: str, sentiment: str, ticket_type: str = "general", order_id: str = None) -> str:
 
 
 
 
 
 
314
  if order_id is None:
315
  match = re.search(r"\b(o\d+)\b", (user_message or "").lower())
316
  order_id = match.group(1).upper() if match else None
 
332
  );
333
  """)
334
  cursor.execute("""
335
+ INSERT INTO tickets (ticket_id, ticket_type, sentiment, message, order_id, status, created_at)
 
 
 
336
  VALUES (?, ?, ?, ?, ?, 'Open', ?);
337
  """, (ticket_id, ticket_type, sentiment, user_message, order_id, created_at))
338
  conn.commit()
 
340
  return ticket_id
341
 
342
  # =====================================================================
343
+ # sql_result_to_response()
344
  # =====================================================================
345
  def sql_result_to_response(
346
  user_message: str,
 
350
  language: str = "auto",
351
  max_rows_to_list: int = 5
352
  ) -> str:
353
+ if isinstance(df, pd.DataFrame) and "message" in df.columns and len(df) == 1:
 
 
 
 
 
354
  return str(df.iloc[0]["message"])
355
 
 
356
  if df.empty:
357
  return (
358
  "I couldn’t find any orders matching the details you provided. "
359
+ "Please double-check your order ID or the information you entered, and try again."
 
360
  )
361
 
 
362
  df_for_llm = df.head(max_rows_to_list).copy()
363
  result_data = df_for_llm.to_dict(orient="records")
364
 
 
365
  if include_emojis:
366
+ intro_line = "Welcome to FoodHub Support! 👋 I'm your virtual assistant — here to help you.\n"
367
+ emoji_instruction = "Include a few relevant, tasteful emojis (✅ 🚚 ⏱️ 🍕 ℹ️ ⚠️ 💛), but do not overuse.\n"
 
 
 
 
 
368
  else:
369
+ intro_line = "Welcome to FoodHub Support! I’m your virtual assistant — here to help you.\n"
370
+ emoji_instruction = "Do NOT use emojis.\n"
 
 
371
 
372
+ tone_instruction = "Use a polite, professional tone, warm but not overly casual.\n"
373
+ language_instruction = "Detect the user's language and respond in the SAME language. If unsure, use English.\n" if language == "auto" else f"Respond in: {language}.\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
 
375
  system_prompt = f"""
376
  You are a polite and professional customer support chatbot for FoodHub.
377
 
378
+ Always start with:
379
  {intro_line}
380
 
381
+ Policy:
382
+ - If any matching record exists order is confirmed. Do NOT say "order not found".
383
+ - Use delivery_eta if delivery_time missing.
384
+ - Keep concise and human-friendly.
385
+
386
+ {tone_instruction}{emoji_instruction}{language_instruction}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  """
388
 
389
+ content = f"User question: {user_message}\n\nMatching order data (up to {max_rows_to_list} rows):\n{result_data}"
 
 
 
390
 
391
  response = client.chat.completions.create(
392
  model="llama-3.3-70b-versatile",
393
  temperature=0.4,
394
  messages=[
395
  {"role": "system", "content": system_prompt},
396
+ {"role": "user", "content": content},
397
  ],
398
  )
 
399
  return response.choices[0].message.content.strip()
400
 
401
  # =====================================================================
402
+ # answer_user_question()
403
  # =====================================================================
404
  def answer_user_question(user_message: str) -> str:
 
 
 
 
 
 
 
 
405
  sql = llm_to_sql(user_message)
406
  print("Generated SQL:", sql)
 
 
407
  df = run_sql_query(sql)
408
+ return sql_result_to_response(user_message=user_message, df=df)
 
 
 
 
 
 
 
 
 
 
 
409
 
410
  # =====================================================================
411
+ # chatbot_response()
412
  # =====================================================================
413
  def chatbot_response(user_message: str) -> str:
 
414
  text = (user_message or "").lower().strip()
415
 
 
416
  order_id_match = re.search(r"\b[oO]\d{3,}\b", text)
417
  has_order_id = order_id_match is not None
418
 
 
419
  greetings = ["hi", "hello", "hey", "good morning", "good evening", "good afternoon"]
420
  thanks_words = ["thank you", "thanks", "thx", "thankyou"]
421
 
422
  if any(text == g or text.startswith(g + " ") for g in greetings):
423
  return (
424
  "👋 Hi there! Welcome to FoodHub Support.\n"
425
+ "You can ask about order status, delivery, cancellation, or payment/refund issues. 💛"
426
  )
427
 
428
  if any(w in text for w in thanks_words):
429
  return (
430
  "You’re most welcome! 💛\n"
431
+ "If you need help, share your question or Order ID (like `O12488`)."
432
  )
433
 
 
434
  if is_misuse_or_hacker_attempt(user_message):
435
  return (
436
  "😔 Sorry, I can’t assist with that.\n"
437
  "For everyone’s safety, I can only help with your own order using a valid order ID.\n"
438
+ "Try: *Where is my order O12488?* 😊"
439
  )
440
 
 
441
  escalation_keywords = [
442
  "raised the query multiple times",
443
  "asked multiple times",
 
449
  senti_info = analyze_sentiment_and_escalation(user_message)
450
  sentiment = senti_info.get("sentiment", "angry")
451
 
452
+ ticket_id = create_ticket(user_message, sentiment, ticket_type="escalation")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
454
  return (
455
+ "💛 I’m sorry you’ve had to follow up multiple times.\n\n"
456
+ "I’ve **escalated your case to a senior support agent**.\n\n"
457
+ f"📌 Ticket ID: **{ticket_id}**\n\n"
458
+ "Please share your **Order ID** (e.g., `O12488`) so we can resolve faster."
459
  )
460
 
 
461
  if ("payment" in text or "refund" in text) and not has_order_id:
462
  return (
463
+ "💛 I’m sorry you’re facing a payment/refund issue.\n"
464
+ "Please share your **Order ID** (e.g., `O12488`) so I can check and update you."
 
 
465
  )
466
 
 
467
  order_status_intents = [
468
+ "where is my order", "track my order", "order status", "delivery status",
469
+ "when will my order arrive", "order delayed", "delay in delivery",
470
+ "order details", "details of my order",
 
 
 
 
 
 
 
 
 
 
 
 
 
471
  ]
472
+ if any(p in text for p in order_status_intents) and not has_order_id:
473
+ return "Please share your **Order ID** (e.g., `O12488`) so I can check the exact status. 💛"
 
 
 
 
 
474
 
 
475
  try:
476
  return answer_user_question(user_message)
477
  except Exception as e:
478
+ print("[chatbot_response] Error:", e)
479
  return (
480
  "Sorry, I ran into a small issue while processing this request. 🙏\n"
481
+ "Please make sure you include a valid Order ID like `O12488`."
482
  )
483
 
 
484
  def chatbot(msg: str) -> str:
485
  return chatbot_response(msg)
486
 
 
488
  if "welcome_shown" not in st.session_state:
489
  st.session_state["welcome_shown"] = False
490
 
491
+ # ================== UI ==================
 
 
492
  st.markdown(
493
  """
494
  <div class="foodhub-card">
495
  <div style="display:flex; align-items:flex-start; gap:12px;">
496
  <div style="font-size:32px;">🍕</div>
497
  <div style="flex:1%;">
498
+ <div class="foodhub-header-title">FoodHub AI Support</div>
 
 
499
  <p class="foodhub-header-subtitle">
500
+ Ask about order status, delivery updates, cancellations, or payment issues.
501
  </p>
502
  <div class="foodhub-assistant-pill">
503
  <span class="status-dot"></span>
 
510
  unsafe_allow_html=True,
511
  )
512
 
 
513
  if not st.session_state["welcome_shown"]:
514
  st.success("Welcome to FoodHub Support! I’m your virtual assistant for order, delivery, and payment queries.")
515
  st.caption("Because great food deserves great service 💛")
516
  st.session_state["welcome_shown"] = True
517
 
 
518
  st.markdown('<div class="quick-actions-title">Quick help options</div>', unsafe_allow_html=True)
519
  col1, col2 = st.columns(2)
520
 
 
529
  if col4.button("🧾 Order details", use_container_width=True):
530
  st.session_state["chat_input"] = "Show me the details for my order."
531
 
 
532
  query = st.text_input(
533
  "💬 Type your question here:",
534
  key="chat_input",
 
541
  if query.strip():
542
  reply = chatbot(query.strip())
543
 
 
544
  st.markdown('<div class="user-label">You</div>', unsafe_allow_html=True)
545
  st.markdown(f"<div class='bot-bubble' style='background:#fff;'>💬 {query}</div>", unsafe_allow_html=True)
546
 
547
  st.markdown('<div class="bot-label">FoodHub Assistant</div>', unsafe_allow_html=True)
548
+ st.markdown(f"<div class='bot-bubble'>🤖 {reply}</div>", unsafe_allow_html=True)
 
 
 
549
  else:
550
  st.warning("Please enter a message to continue.")