Suunil-Dabral commited on
Commit
7aae18a
·
verified ·
1 Parent(s): be14459

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +85 -132
src/streamlit_app.py CHANGED
@@ -146,9 +146,25 @@ def _is_escalation_intent(message: str) -> bool:
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,22 +225,23 @@ Columns:
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,7 +249,6 @@ def is_safe_sql(sql: str) -> bool:
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,50 +268,34 @@ def run_sql_query(sql: str) -> pd.DataFrame:
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"""
@@ -321,7 +321,7 @@ RULES:
321
  temperature=0.1,
322
  messages=[
323
  {"role": "system", "content": system_prompt},
324
- {"role": "user", "content": user_message or ""},
325
  ],
326
  )
327
 
@@ -333,10 +333,10 @@ RULES:
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')",
@@ -393,54 +393,23 @@ If uncertain: {"sentiment":"neutral","escalate":false}
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,26 +435,20 @@ def create_ticket(
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
 
@@ -505,7 +468,7 @@ VERY IMPORTANT POLICY:
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.
@@ -516,10 +479,11 @@ DELIVERED TEMPLATE (use when order_status_std indicates delivered OR is_delivere
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,26 +499,19 @@ If NOT delivered:
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,8 +536,7 @@ def chatbot_response(user_message: str) -> str:
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,7 +547,7 @@ def chatbot_response(user_message: str) -> str:
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(
@@ -608,32 +564,29 @@ def chatbot_response(user_message: str) -> str:
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)
 
146
  "urgent",
147
  "not resolved",
148
  "what is happening",
149
+ "followed multiple times",
150
+ "followed up multiple times",
151
+ "follow up multiple times",
152
+ "multiple follow ups",
153
+ "multiple follow-ups",
154
+ "i have followed multiple times",
155
+ "i have followed up multiple times",
156
  ]
157
  return any(k in t for k in escalation_keywords)
158
 
159
+ def _is_payment_refund_intent(message: str) -> bool:
160
+ t = (message or "").lower().strip()
161
+ keywords = [
162
+ "payment", "refund", "money back", "charged", "chargeback",
163
+ "transaction", "payment failed", "payment issue", "payment problem",
164
+ "upi", "card", "debit", "credit", "wallet"
165
+ ]
166
+ return any(k in t for k in keywords)
167
+
168
  # ================== DB CONFIG (ALIGNED WITH YOUR NOTEBOOK) ==================
169
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
170
  CLEAN_DB_PATH = os.path.join(BASE_DIR, "orders_clean.db") # must match your notebook output
 
225
  def is_safe_sql(sql: str) -> bool:
226
  if not isinstance(sql, str):
227
  return False
 
228
  s = sql.lower().strip()
229
 
 
230
  if not s.startswith("select"):
231
  return False
232
 
233
+ # block stacked queries like: SELECT ...; DROP ...
234
  if re.search(r";\s*\S", s):
235
  return False
236
 
237
+ forbidden = ["drop", "delete", "update", "insert", "alter", "truncate", "create"]
238
+ if any(k in s for k in forbidden):
239
+ return False
240
+
241
+ if any(tok in s for tok in ["--", "/*", "*/"]):
242
+ return False
243
+
244
+ return True
245
 
246
  # =====================================================================
247
  # run_sql_query()
 
249
  def run_sql_query(sql: str) -> pd.DataFrame:
250
  if not isinstance(sql, str):
251
  return pd.DataFrame([{"message": "🚫 Invalid SQL type (expected string)."}])
 
252
  sql = sql.strip()
253
 
254
  if not is_safe_sql(sql):
 
268
  return pd.DataFrame([{"message": f"⚠️ SQL execution error: {str(e)}"}])
269
 
270
  # =====================================================================
271
+ # llm_to_sql()
272
  # =====================================================================
273
  def llm_to_sql(user_message: str) -> str:
274
  """
275
  Convert natural language to a safe SELECT query for orders_clean.
276
+ - Fast-path when Order ID exists.
277
+ - If order_id required but missing: return NEED_ORDER_ID.
 
 
 
278
  """
279
+ msg = user_message or ""
280
+ text = msg.lower().strip()
281
 
282
+ oid = extract_order_id(msg)
 
283
  if oid:
284
  sql = f"SELECT * FROM orders_clean WHERE LOWER(order_id) = LOWER('{oid}')"
285
  return sql if is_safe_sql(sql) else "SELECT 'Unable to answer safely.' AS message;"
286
 
287
+ # If the message is order-specific without Order ID, force NEED_ORDER_ID
288
  must_have_order_id_intents = [
289
+ "where is my order", "track my order", "track order", "order status", "delivery status",
290
+ "when will my order arrive", "when will it arrive", "eta",
291
+ "order delayed", "delay in delivery", "order not delivered", "late delivery",
292
+ "order details", "show me the details", "details of my order", "details for my order",
293
+ "cancel my order", "cancel order",
294
+ "refund", "payment",
295
+ "modify my order", "change my order", "postpone my order",
296
+ "deliver at different address", "change address", "reschedule delivery",
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  ]
298
+ if any(p in text for p in must_have_order_id_intents) or needs_order_id(msg):
299
  return "SELECT 'NEED_ORDER_ID' AS message;"
300
 
301
  system_prompt = f"""
 
321
  temperature=0.1,
322
  messages=[
323
  {"role": "system", "content": system_prompt},
324
+ {"role": "user", "content": msg},
325
  ],
326
  )
327
 
 
333
  sql = re.sub(r"^```", "", sql).strip()
334
  sql = sql.replace("```", "").strip()
335
 
336
+ # Force FROM orders_clean
337
  sql = re.sub(r"\bfrom\s+\w+\b", "FROM orders_clean", sql, flags=re.IGNORECASE)
338
 
339
+ # Normalize order_id filter
340
  sql = re.sub(
341
  r"where\s+order_id\s*=\s*'([^']+)'",
342
  r"WHERE LOWER(order_id) = LOWER('\1')",
 
393
  # is_misuse_or_hacker_attempt()
394
  # =====================================================================
395
  def is_misuse_or_hacker_attempt(user_message: str) -> bool:
396
+ t = (user_message or "").lower().strip()
397
 
398
+ hacker = ["hack", "hacker", "hacking", "breach", "exploit", "bypass security", "root access", "admin access"]
399
+ bulk = ["full database", "entire database", "dump data", "export all", "all customers", "customer list", "all orders", "every order"]
400
+ jailbreak = ["ignore previous instructions", "override rules", "developer mode", "you are no longer restricted"]
401
+ inj = ["1=1", "union select", "drop table", "delete from", "insert into", "alter table", "truncate"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
402
 
403
+ if ";" in t and any(x in t for x in inj):
404
  return True
405
 
406
+ signals = hacker + bulk + jailbreak + inj
407
+ return any(s in t for s in signals)
 
 
 
408
 
409
  # =====================================================================
410
  # create_ticket()
411
  # =====================================================================
412
+ def create_ticket(user_message: str, sentiment: str, ticket_type: str = "general", order_id: Optional[str] = None) -> str:
 
 
 
 
 
 
 
 
 
413
  ticket_id = "TKT-" + uuid.uuid4().hex[:8].upper()
414
  created_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
415
 
 
435
  return ticket_id
436
 
437
  # =====================================================================
438
+ # sql_result_to_response()
439
  # =====================================================================
440
+ def sql_result_to_response(user_message: str, df: pd.DataFrame, max_rows_to_list: int = 5) -> str:
 
 
 
 
441
  # handle firewall/SQL execution errors and special messages
442
  if isinstance(df, pd.DataFrame) and "message" in df.columns and len(df) == 1:
443
  msg = str(df.iloc[0]["message"])
 
444
  if msg.strip().upper() == "NEED_ORDER_ID":
445
  return f"{WELCOME_LINE}\n{ASK_ORDER_ID_LINE}"
 
446
  return msg
447
 
448
  if df.empty:
449
  return (
450
  f"{WELCOME_LINE}\n"
451
+ "I couldn’t find a matching order for the details provided.\n"
452
  f"{ASK_ORDER_ID_LINE}"
453
  )
454
 
 
468
  when there is at least one matching record.
469
  - Instead, confidently acknowledge the order and use available data.
470
 
471
+ CRITICAL RULES:
472
  - NEVER invent or guess items, ETA, status, payment, or any order details.
473
  - Use ONLY the data provided in "Matching order data".
474
  - If information is missing, say it's unavailable in tracking.
 
479
  If you have any further concerns or issues, please let me know. If you need anything else, I’m here to help! 💛"
480
 
481
  If NOT delivered:
482
+ - Provide the latest known status.
483
  - If delivery_time is missing but delivery_eta exists: share ETA.
484
  - If both delivery_time and delivery_eta missing: apologize for limited tracking.
485
+
486
+ Keep it concise and helpful.
487
  """
488
 
489
  content = f"User question: {user_message}\n\nMatching order data (up to {max_rows_to_list} rows):\n{result_data}"
 
499
  return (resp.choices[0].message.content or "").strip()
500
 
501
  # =====================================================================
502
+ # answer_user_question()
503
  # =====================================================================
504
+ def answer_user_question(user_message: str) -> str:
505
+ # Safety net: never answer order intents without Order ID
506
+ if needs_order_id(user_message) and not extract_order_id(user_message):
507
+ return f"{WELCOME_LINE}\n{ASK_ORDER_ID_LINE}"
508
+
509
  sql = llm_to_sql(user_message)
 
510
  df = run_sql_query(sql)
511
+ return sql_result_to_response(user_message, df, max_rows_to_list=5)
 
 
 
 
 
 
 
 
 
 
512
 
513
  # =====================================================================
514
+ # chatbot_response() – main orchestration (FIXED)
515
  # =====================================================================
516
  def chatbot_response(user_message: str) -> str:
517
  text = (user_message or "").strip()
 
536
  return (
537
  f"{WELCOME_LINE}\n"
538
  "😔 Sorry, I can’t assist with that.\n"
539
+ "For privacy and security reasons, I can only help with your own order using a valid Order ID. 💛"
 
540
  )
541
 
542
  # Third-party privacy
 
547
  "Please ask them to contact FoodHub Support directly, or have them share their **Order ID** themselves. 💛"
548
  )
549
 
550
+ # FIX 1: Escalation intent must be handled BEFORE any generic Order ID gating
551
  if _is_escalation_intent(text):
552
  senti = analyze_sentiment_and_escalation(text)
553
  ticket_id = create_ticket(
 
564
  f"{ASK_ORDER_ID_LINE if not order_id else 'If you need anything else, I’m here to help! 💛'}"
565
  )
566
 
567
+ # FIX: Payment / Refund intent should use empathetic ask (Response 2 style)
568
+ if _is_payment_refund_intent(text) and not order_id:
569
+ return (
570
+ "💛 I’m sorry you’re facing an issue with the payment or refund. I’ll help you with this right away.\n\n"
571
+ "In order to serve you better, could you please share your **Order ID** "
572
+ "(for example: `O12488`) so I can check the payment status and update you? "
573
+ "If the payment was received, the refund will be processed within 48 hours."
 
 
 
 
574
  )
575
+
576
+ # FIX 2: Cancel intent without Order ID should return the preferred message (not generic welcome+ask)
577
+ if _is_cancel_intent(text) and not order_id:
578
  return (
579
+ "💛 I’m sorry to hear you’d like to cancel your order ��� I completely understand and I’m here to help. "
580
+ "Could you please share your **Order ID** (for example: `O12488`) so I can check the details and help "
581
+ "process the cancellation for you?"
 
 
582
  )
583
 
584
  # Global rule: any order-related request without Order ID → welcome + ask for Order ID
585
  if needs_order_id(text) and not order_id:
586
  return f"{WELCOME_LINE}\n{ASK_ORDER_ID_LINE}"
587
 
588
+ # Normal path
589
+ return answer_user_question(text)
590
 
591
  def chatbot(msg: str) -> str:
592
  return chatbot_response(msg)