Suunil-Dabral commited on
Commit
be14459
·
verified ·
1 Parent(s): 8df776a

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +189 -66
src/streamlit_app.py CHANGED
@@ -131,6 +131,24 @@ def is_third_party_order_request(message: str) -> bool:
131
  order_words = ["order", "delivery", "track", "status", "items", "details", "cancel", "refund", "payment", "address", "modify", "postpone"]
132
  return any(tp in t for tp in third_party) and any(w in t for w in order_words)
133
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  # ================== DB CONFIG (ALIGNED WITH YOUR NOTEBOOK) ==================
135
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
136
  CLEAN_DB_PATH = os.path.join(BASE_DIR, "orders_clean.db") # must match your notebook output
@@ -191,23 +209,22 @@ Columns:
191
  def is_safe_sql(sql: str) -> bool:
192
  if not isinstance(sql, str):
193
  return False
 
194
  s = sql.lower().strip()
195
 
 
196
  if not s.startswith("select"):
197
  return False
198
 
199
- # block stacked queries like: SELECT ...; DROP ...
200
  if re.search(r";\s*\S", s):
201
  return False
202
 
203
- forbidden = ["drop", "delete", "update", "insert", "alter", "truncate", "create"]
204
- if any(k in s for k in forbidden):
205
- return False
206
-
207
- if any(tok in s for tok in ["--", "/*", "*/"]):
208
- return False
209
-
210
- return True
211
 
212
  # =====================================================================
213
  # run_sql_query()
@@ -215,6 +232,7 @@ def is_safe_sql(sql: str) -> bool:
215
  def run_sql_query(sql: str) -> pd.DataFrame:
216
  if not isinstance(sql, str):
217
  return pd.DataFrame([{"message": "🚫 Invalid SQL type (expected string)."}])
 
218
  sql = sql.strip()
219
 
220
  if not is_safe_sql(sql):
@@ -234,21 +252,52 @@ def run_sql_query(sql: str) -> pd.DataFrame:
234
  return pd.DataFrame([{"message": f"⚠️ SQL execution error: {str(e)}"}])
235
 
236
  # =====================================================================
237
- # llm_to_sql()
238
  # =====================================================================
239
  def llm_to_sql(user_message: str) -> str:
240
  """
241
  Convert natural language to a safe SELECT query for orders_clean.
242
- NOTE: This should only be called when Order ID is present OR the question is general analytics.
 
 
 
 
243
  """
244
- msg = user_message or ""
245
 
246
  # Fast path: if Order ID exists, bypass LLM completely
247
- oid = extract_order_id(msg)
248
  if oid:
249
  sql = f"SELECT * FROM orders_clean WHERE LOWER(order_id) = LOWER('{oid}')"
250
  return sql if is_safe_sql(sql) else "SELECT 'Unable to answer safely.' AS message;"
251
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  system_prompt = f"""
253
  You are an expert SQLite assistant for a food delivery company.
254
 
@@ -260,6 +309,9 @@ RULES:
260
  - Output ONLY the SQL query (no markdown, no comments, no explanation).
261
  - Allowed table name: orders_clean
262
  - Allowed operation: SELECT only
 
 
 
263
  - If unsure:
264
  SELECT 'Unable to answer with available data.' AS message;
265
  """
@@ -269,7 +321,7 @@ RULES:
269
  temperature=0.1,
270
  messages=[
271
  {"role": "system", "content": system_prompt},
272
- {"role": "user", "content": msg},
273
  ],
274
  )
275
 
@@ -281,12 +333,14 @@ RULES:
281
  sql = re.sub(r"^```", "", sql).strip()
282
  sql = sql.replace("```", "").strip()
283
 
284
- # safer table forcing: only first FROM target if not already orders_clean
 
 
 
285
  sql = re.sub(
286
- r"\bfrom\s+(?!orders_clean\b)([a-zA-Z_]\w*)\b",
287
- "FROM orders_clean",
288
  sql,
289
- count=1,
290
  flags=re.IGNORECASE,
291
  )
292
 
@@ -339,23 +393,54 @@ If uncertain: {"sentiment":"neutral","escalate":false}
339
  # is_misuse_or_hacker_attempt()
340
  # =====================================================================
341
  def is_misuse_or_hacker_attempt(user_message: str) -> bool:
342
- t = (user_message or "").lower().strip()
343
 
344
- hacker = ["hack", "hacker", "hacking", "breach", "exploit", "bypass security", "root access", "admin access"]
345
- bulk = ["full database", "entire database", "dump data", "export all", "all customers", "customer list", "all orders", "every order"]
346
- jailbreak = ["ignore previous instructions", "override rules", "developer mode", "you are no longer restricted"]
347
- inj = ["1=1", "union select", "drop table", "delete from", "insert into", "alter table", "truncate"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
 
349
- if ";" in t and any(x in t for x in inj):
350
  return True
351
 
352
- signals = hacker + bulk + jailbreak + inj
353
- return any(s in t for s in signals)
 
 
 
354
 
355
  # =====================================================================
356
  # create_ticket()
357
  # =====================================================================
358
- def create_ticket(user_message: str, sentiment: str, ticket_type: str = "general", order_id: Optional[str] = None) -> str:
 
 
 
 
 
 
 
 
 
359
  ticket_id = "TKT-" + uuid.uuid4().hex[:8].upper()
360
  created_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
361
 
@@ -381,18 +466,27 @@ def create_ticket(user_message: str, sentiment: str, ticket_type: str = "general
381
  return ticket_id
382
 
383
  # =====================================================================
384
- # sql_result_to_response()
385
  # =====================================================================
386
- def sql_result_to_response(user_message: str, df: pd.DataFrame, max_rows_to_list: int = 5) -> str:
387
- # handle firewall/SQL execution errors
 
 
 
 
388
  if isinstance(df, pd.DataFrame) and "message" in df.columns and len(df) == 1:
389
- return str(df.iloc[0]["message"])
 
 
 
 
 
390
 
391
  if df.empty:
392
  return (
393
  f"{WELCOME_LINE}\n"
394
- "I couldn’t find a matching order for that Order ID.\n"
395
- "Please double-check the Order ID and try again. 💛"
396
  )
397
 
398
  df_for_llm = df.head(max_rows_to_list).copy()
@@ -401,26 +495,31 @@ def sql_result_to_response(user_message: str, df: pd.DataFrame, max_rows_to_list
401
  system_prompt = f"""
402
  You are a polite and professional customer support chatbot for FoodHub.
403
 
404
- CRITICAL RULES (must follow):
 
 
 
 
 
 
 
 
 
 
405
  - NEVER invent or guess items, ETA, status, payment, or any order details.
406
  - Use ONLY the data provided in "Matching order data".
407
  - If information is missing, say it's unavailable in tracking.
408
 
409
- STYLE REQUIREMENT:
410
- - Always start with EXACTLY:
411
- "{WELCOME_LINE}"
412
-
413
  DELIVERED TEMPLATE (use when order_status_std indicates delivered OR is_delivered == 1):
414
- - You MUST write a message in this exact style (same meaning, you may include the order id if available):
415
  "I've checked on your order, and I'm happy to inform you that it has been delivered ✅. You should have received your order.
416
  If you have any further concerns or issues, please let me know. If you need anything else, I’m here to help! 💛"
417
 
418
  If NOT delivered:
419
- - Provide the latest known status.
420
  - If delivery_time is missing but delivery_eta exists: share ETA.
421
  - If both delivery_time and delivery_eta missing: apologize for limited tracking.
422
-
423
- Keep it concise and helpful.
424
  """
425
 
426
  content = f"User question: {user_message}\n\nMatching order data (up to {max_rows_to_list} rows):\n{result_data}"
@@ -436,19 +535,26 @@ Keep it concise and helpful.
436
  return (resp.choices[0].message.content or "").strip()
437
 
438
  # =====================================================================
439
- # answer_user_question()
440
  # =====================================================================
441
- def answer_user_question(user_message: str) -> str:
442
- # Safety net: never answer order intents without Order ID
443
- if needs_order_id(user_message) and not extract_order_id(user_message):
444
- return f"{WELCOME_LINE}\n{ASK_ORDER_ID_LINE}"
445
-
446
  sql = llm_to_sql(user_message)
 
447
  df = run_sql_query(sql)
448
- return sql_result_to_response(user_message, df, max_rows_to_list=5)
 
 
 
 
 
 
 
 
 
 
449
 
450
  # =====================================================================
451
- # chatbot_response() – main orchestration
452
  # =====================================================================
453
  def chatbot_response(user_message: str) -> str:
454
  text = (user_message or "").strip()
@@ -473,7 +579,8 @@ def chatbot_response(user_message: str) -> str:
473
  return (
474
  f"{WELCOME_LINE}\n"
475
  "😔 Sorry, I can’t assist with that.\n"
476
- "For privacy and security reasons, I can only help with your own order using a valid Order ID. 💛"
 
477
  )
478
 
479
  # Third-party privacy
@@ -484,16 +591,8 @@ def chatbot_response(user_message: str) -> str:
484
  "Please ask them to contact FoodHub Support directly, or have them share their **Order ID** themselves. 💛"
485
  )
486
 
487
- # Your global rule: any order-related request without Order ID → welcome + ask for Order ID
488
- if needs_order_id(text) and not order_id:
489
- return f"{WELCOME_LINE}\n{ASK_ORDER_ID_LINE}"
490
-
491
- # Escalation path (still asks for Order ID if missing)
492
- escalation_keywords = [
493
- "raised the query multiple times", "asked multiple times",
494
- "no resolution", "still not resolved", "immediate response"
495
- ]
496
- if any(k in t for k in escalation_keywords):
497
  senti = analyze_sentiment_and_escalation(text)
498
  ticket_id = create_ticket(
499
  user_message=text,
@@ -503,14 +602,38 @@ def chatbot_response(user_message: str) -> str:
503
  )
504
  return (
505
  f"{WELCOME_LINE}\n"
506
- "💛 I’m sorry you’ve had to follow up multiple times.\n\n"
507
- "I’ve escalated this to a senior support agent.\n"
508
  f"📌 Ticket ID: **{ticket_id}**\n\n"
509
  f"{ASK_ORDER_ID_LINE if not order_id else 'If you need anything else, I’m here to help! 💛'}"
510
  )
511
 
512
- # Normal path
513
- return answer_user_question(text)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
514
 
515
  def chatbot(msg: str) -> str:
516
  return chatbot_response(msg)
@@ -581,4 +704,4 @@ if send_clicked:
581
  st.markdown('<div class="bot-label">FoodHub Assistant</div>', unsafe_allow_html=True)
582
  st.markdown(f"<div class='bot-bubble'>{bot_html}</div>", unsafe_allow_html=True)
583
  else:
584
- st.warning("Please enter a message to continue.")
 
131
  order_words = ["order", "delivery", "track", "status", "items", "details", "cancel", "refund", "payment", "address", "modify", "postpone"]
132
  return any(tp in t for tp in third_party) and any(w in t for w in order_words)
133
 
134
+ def _is_cancel_intent(message: str) -> bool:
135
+ t = (message or "").lower().strip()
136
+ return ("cancel" in t and "order" in t) or ("cancellation" in t)
137
+
138
+ def _is_escalation_intent(message: str) -> bool:
139
+ t = (message or "").lower().strip()
140
+ escalation_keywords = [
141
+ "raised the query multiple times",
142
+ "asked multiple times",
143
+ "no resolution",
144
+ "still not resolved",
145
+ "immediate response",
146
+ "urgent",
147
+ "not resolved",
148
+ "what is happening",
149
+ ]
150
+ return any(k in t for k in escalation_keywords)
151
+
152
  # ================== DB CONFIG (ALIGNED WITH YOUR NOTEBOOK) ==================
153
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
154
  CLEAN_DB_PATH = os.path.join(BASE_DIR, "orders_clean.db") # must match your notebook output
 
209
  def is_safe_sql(sql: str) -> bool:
210
  if not isinstance(sql, str):
211
  return False
212
+
213
  s = sql.lower().strip()
214
 
215
+ # Must start with SELECT
216
  if not s.startswith("select"):
217
  return False
218
 
219
+ # Block stacked queries
220
  if re.search(r";\s*\S", s):
221
  return False
222
 
223
+ forbidden = [
224
+ "hack", " drop ", " delete ", " update ", " insert ", " alter ",
225
+ " truncate ", " create ", "--", "/*", "*/"
226
+ ]
227
+ return not any(word in s for word in forbidden)
 
 
 
228
 
229
  # =====================================================================
230
  # run_sql_query()
 
232
  def run_sql_query(sql: str) -> pd.DataFrame:
233
  if not isinstance(sql, str):
234
  return pd.DataFrame([{"message": "🚫 Invalid SQL type (expected string)."}])
235
+
236
  sql = sql.strip()
237
 
238
  if not is_safe_sql(sql):
 
252
  return pd.DataFrame([{"message": f"⚠️ SQL execution error: {str(e)}"}])
253
 
254
  # =====================================================================
255
+ # llm_to_sql() (aligned with latest tool behavior)
256
  # =====================================================================
257
  def llm_to_sql(user_message: str) -> str:
258
  """
259
  Convert natural language to a safe SELECT query for orders_clean.
260
+
261
+ Key behavior:
262
+ - If Order ID is present: bypass LLM entirely.
263
+ - If user intent requires Order ID but it's missing: return NEED_ORDER_ID.
264
+ - Otherwise: allow LLM to answer general/non-order-specific questions using SELECT only.
265
  """
266
+ text = (user_message or "").lower().strip()
267
 
268
  # Fast path: if Order ID exists, bypass LLM completely
269
+ oid = extract_order_id(user_message or "")
270
  if oid:
271
  sql = f"SELECT * FROM orders_clean WHERE LOWER(order_id) = LOWER('{oid}')"
272
  return sql if is_safe_sql(sql) else "SELECT 'Unable to answer safely.' AS message;"
273
 
274
+ # If intent requires Order ID, do NOT hallucinate placeholders
275
+ must_have_order_id_intents = [
276
+ "where is my order",
277
+ "track my order",
278
+ "track order",
279
+ "order status",
280
+ "delivery status",
281
+ "when will my order arrive",
282
+ "order details",
283
+ "show me the details",
284
+ "my order is delayed",
285
+ "order delayed",
286
+ "order is late",
287
+ "refund",
288
+ "payment",
289
+ "cancel my order",
290
+ "cancel order",
291
+ "modify my order",
292
+ "change my order",
293
+ "postpone my order",
294
+ "deliver at different address",
295
+ "change address",
296
+ "reschedule delivery",
297
+ ]
298
+ if any(p in text for p in must_have_order_id_intents) or needs_order_id(user_message or ""):
299
+ return "SELECT 'NEED_ORDER_ID' AS message;"
300
+
301
  system_prompt = f"""
302
  You are an expert SQLite assistant for a food delivery company.
303
 
 
309
  - Output ONLY the SQL query (no markdown, no comments, no explanation).
310
  - Allowed table name: orders_clean
311
  - Allowed operation: SELECT only
312
+ - NEVER invent placeholders like 'your_order_id'.
313
+ - If the query requires an order_id but it is missing, return exactly:
314
+ SELECT 'NEED_ORDER_ID' AS message;
315
  - If unsure:
316
  SELECT 'Unable to answer with available data.' AS message;
317
  """
 
321
  temperature=0.1,
322
  messages=[
323
  {"role": "system", "content": system_prompt},
324
+ {"role": "user", "content": user_message or ""},
325
  ],
326
  )
327
 
 
333
  sql = re.sub(r"^```", "", sql).strip()
334
  sql = sql.replace("```", "").strip()
335
 
336
+ # Force FROM orders_clean (first FROM only)
337
+ sql = re.sub(r"\bfrom\s+\w+\b", "FROM orders_clean", sql, flags=re.IGNORECASE)
338
+
339
+ # Normalize order_id filter if produced
340
  sql = re.sub(
341
+ r"where\s+order_id\s*=\s*'([^']+)'",
342
+ r"WHERE LOWER(order_id) = LOWER('\1')",
343
  sql,
 
344
  flags=re.IGNORECASE,
345
  )
346
 
 
393
  # is_misuse_or_hacker_attempt()
394
  # =====================================================================
395
  def is_misuse_or_hacker_attempt(user_message: str) -> bool:
396
+ text = (user_message or "").lower().strip()
397
 
398
+ hacker_keywords = [
399
+ r"\bhack\b", r"\bhacking\b", r"\bhacked\b",
400
+ "breach", "exploit", "bypass security",
401
+ "disable firewall", "root access", "admin access",
402
+ "reverse engineer"
403
+ ]
404
+ bulk_keywords = [
405
+ "full database", "entire database",
406
+ "all customers", "customer list",
407
+ "download database", "dump data", "export all",
408
+ "all orders", "every order", "order history", "export everything", "show everything"
409
+ ]
410
+ jailbreak_keywords = [
411
+ "ignore previous instructions",
412
+ "override rules", "forget rules",
413
+ "developer mode", "you are no longer restricted",
414
+ "pretend you are", "act like"
415
+ ]
416
+ sql_injection_keywords = [
417
+ "1=1", "union select",
418
+ "drop table", "delete from", "insert into",
419
+ "alter table", "truncate"
420
+ ]
421
 
422
+ if ";" in text and any(k in text for k in sql_injection_keywords):
423
  return True
424
 
425
+ return any(
426
+ re.search(pattern, text) for pattern in (
427
+ hacker_keywords + bulk_keywords + jailbreak_keywords + sql_injection_keywords
428
+ )
429
+ )
430
 
431
  # =====================================================================
432
  # create_ticket()
433
  # =====================================================================
434
+ def create_ticket(
435
+ user_message: str,
436
+ sentiment: str,
437
+ ticket_type: str = "general",
438
+ order_id: Optional[str] = None
439
+ ) -> str:
440
+ if order_id is None:
441
+ m = re.search(ORDER_ID_REGEX, user_message or "")
442
+ order_id = m.group(0).upper() if m else None
443
+
444
  ticket_id = "TKT-" + uuid.uuid4().hex[:8].upper()
445
  created_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
446
 
 
466
  return ticket_id
467
 
468
  # =====================================================================
469
+ # sql_result_to_response() (aligned with latest “NEED_ORDER_ID” behavior)
470
  # =====================================================================
471
+ def sql_result_to_response(
472
+ user_message: str,
473
+ df: pd.DataFrame,
474
+ max_rows_to_list: int = 5
475
+ ) -> str:
476
+ # handle firewall/SQL execution errors and special messages
477
  if isinstance(df, pd.DataFrame) and "message" in df.columns and len(df) == 1:
478
+ msg = str(df.iloc[0]["message"])
479
+
480
+ if msg.strip().upper() == "NEED_ORDER_ID":
481
+ return f"{WELCOME_LINE}\n{ASK_ORDER_ID_LINE}"
482
+
483
+ return msg
484
 
485
  if df.empty:
486
  return (
487
  f"{WELCOME_LINE}\n"
488
+ "I couldn’t find any matching order details for the information provided.\n"
489
+ f"{ASK_ORDER_ID_LINE}"
490
  )
491
 
492
  df_for_llm = df.head(max_rows_to_list).copy()
 
495
  system_prompt = f"""
496
  You are a polite and professional customer support chatbot for FoodHub.
497
 
498
+ Always start with EXACTLY:
499
+ "{WELCOME_LINE}"
500
+
501
+ VERY IMPORTANT POLICY:
502
+ - If ANY matching record exists in the data I give you → The order is CONFIRMED.
503
+ - NEVER tell the user to re-check their order ID if matching rows exist.
504
+ - NEVER say "order not found", "unable to locate order", or similar messages
505
+ when there is at least one matching record.
506
+ - Instead, confidently acknowledge the order and use available data.
507
+
508
+ CRITICAL DATA RULES:
509
  - NEVER invent or guess items, ETA, status, payment, or any order details.
510
  - Use ONLY the data provided in "Matching order data".
511
  - If information is missing, say it's unavailable in tracking.
512
 
 
 
 
 
513
  DELIVERED TEMPLATE (use when order_status_std indicates delivered OR is_delivered == 1):
514
+ - Use this meaning and style (you may include the order id if available):
515
  "I've checked on your order, and I'm happy to inform you that it has been delivered ✅. You should have received your order.
516
  If you have any further concerns or issues, please let me know. If you need anything else, I’m here to help! 💛"
517
 
518
  If NOT delivered:
519
+ - Provide the latest known status clearly.
520
  - If delivery_time is missing but delivery_eta exists: share ETA.
521
  - If both delivery_time and delivery_eta missing: apologize for limited tracking.
522
+ - Keep it concise, warm, and helpful.
 
523
  """
524
 
525
  content = f"User question: {user_message}\n\nMatching order data (up to {max_rows_to_list} rows):\n{result_data}"
 
535
  return (resp.choices[0].message.content or "").strip()
536
 
537
  # =====================================================================
538
+ # TOOL LAYER (aligned with your latest notebook structure)
539
  # =====================================================================
540
+ def order_query_tool(user_message: str) -> pd.DataFrame:
 
 
 
 
541
  sql = llm_to_sql(user_message)
542
+ print("[OrderQueryTool] Generated SQL:", sql)
543
  df = run_sql_query(sql)
544
+ print(f"[OrderQueryTool] Rows fetched: {len(df)}")
545
+ return df
546
+
547
+ def answer_tool(user_message: str, df: pd.DataFrame) -> str:
548
+ reply = sql_result_to_response(user_message, df, max_rows_to_list=5)
549
+ print("[AnswerTool] Generated reply length:", len(reply))
550
+ return reply
551
+
552
+ def order_support_tool(user_message: str) -> str:
553
+ df = order_query_tool(user_message)
554
+ return answer_tool(user_message, df)
555
 
556
  # =====================================================================
557
+ # chatbot_response() – main orchestration (updated)
558
  # =====================================================================
559
  def chatbot_response(user_message: str) -> str:
560
  text = (user_message or "").strip()
 
579
  return (
580
  f"{WELCOME_LINE}\n"
581
  "😔 Sorry, I can’t assist with that.\n"
582
+ "For everyone’s safety and privacy, I can only help with your own order using a valid Order ID.\n"
583
+ "Try asking something like: *Where is my order O12488?* 💛"
584
  )
585
 
586
  # Third-party privacy
 
591
  "Please ask them to contact FoodHub Support directly, or have them share their **Order ID** themselves. 💛"
592
  )
593
 
594
+ # Escalation (ticket)
595
+ if _is_escalation_intent(text):
 
 
 
 
 
 
 
 
596
  senti = analyze_sentiment_and_escalation(text)
597
  ticket_id = create_ticket(
598
  user_message=text,
 
602
  )
603
  return (
604
  f"{WELCOME_LINE}\n"
605
+ "💛 I’m really sorry you’ve had to follow up multiple times.\n\n"
606
+ "I’ve escalated this to a senior support agent so it gets immediate attention.\n"
607
  f"📌 Ticket ID: **{ticket_id}**\n\n"
608
  f"{ASK_ORDER_ID_LINE if not order_id else 'If you need anything else, I’m here to help! 💛'}"
609
  )
610
 
611
+ # Cancellation (ticket)
612
+ if _is_cancel_intent(text):
613
+ if not order_id:
614
+ return f"{WELCOME_LINE}\n{ASK_ORDER_ID_LINE}"
615
+
616
+ senti = analyze_sentiment_and_escalation(text)
617
+ ticket_id = create_ticket(
618
+ user_message=f"Cancellation request for {order_id}. Original message: {text}",
619
+ sentiment=senti.get("sentiment", "neutral"),
620
+ ticket_type="cancellation",
621
+ order_id=order_id
622
+ )
623
+ return (
624
+ f"{WELCOME_LINE}\n"
625
+ f"🙏 I understand you would like to cancel **Order {order_id}**, and I’m sorry for the inconvenience.\n\n"
626
+ f"📝 Your cancellation request has been recorded.\n"
627
+ f"📌 **Ticket Reference ID:** `{ticket_id}`\n\n"
628
+ "A support specialist will verify and process it as soon as possible. 💛"
629
+ )
630
+
631
+ # Global rule: any order-related request without Order ID → welcome + ask for Order ID
632
+ if needs_order_id(text) and not order_id:
633
+ return f"{WELCOME_LINE}\n{ASK_ORDER_ID_LINE}"
634
+
635
+ # Normal order support pipeline (tools)
636
+ return order_support_tool(text)
637
 
638
  def chatbot(msg: str) -> str:
639
  return chatbot_response(msg)
 
704
  st.markdown('<div class="bot-label">FoodHub Assistant</div>', unsafe_allow_html=True)
705
  st.markdown(f"<div class='bot-bubble'>{bot_html}</div>", unsafe_allow_html=True)
706
  else:
707
+ st.warning("Please enter a message to continue.")