masadonline commited on
Commit
6d5efc5
·
verified ·
1 Parent(s): 30c292a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +234 -123
app.py CHANGED
@@ -20,7 +20,7 @@ DEFAULT_TWILIO_AUTH_TOKEN_FALLBACK = "" # Fallback if secret "TWILIO_TOKEN" is n
20
  DEFAULT_GROQ_API_KEY_FALLBACK = "" # Fallback if secret "GROQ_API_KEY" is not found
21
 
22
  DEFAULT_TWILIO_CONVERSATION_SERVICE_SID = ""
23
- DEFAULT_TWILIO_BOT_WHATSAPP_IDENTITY = st.secrets.get("TWILIO_PHONE_NUMBER")#"whatsapp:+14155238886" # Twilio Sandbox default
24
  DEFAULT_EMBEDDING_MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2"
25
  DEFAULT_POLLING_INTERVAL_S = 30
26
  DOCS_FOLDER = "docs/"
@@ -66,7 +66,7 @@ def load_pdf_data(file_path):
66
  for page_num in range(len(reader.pages)):
67
  page = reader.pages[page_num]
68
  text_pages.append(page.extract_text() or "")
69
- return text_pages
70
  except FileNotFoundError:
71
  st.error(f"Error: PDF file not found at {file_path}")
72
  return []
@@ -87,7 +87,7 @@ def chunk_text(text_pages, chunk_size=1000, chunk_overlap=200):
87
  if end >= len(full_text):
88
  break
89
  start += (chunk_size - chunk_overlap)
90
- if start >= len(full_text):
91
  break
92
  return [chunk for chunk in chunks if chunk.strip()]
93
 
@@ -113,7 +113,7 @@ def create_faiss_index(_text_chunks, _embedding_model):
113
  st.warning("No valid text chunks to embed for FAISS index.")
114
  return None, []
115
  embeddings = _embedding_model.encode(valid_chunks, convert_to_tensor=False)
116
- if embeddings.ndim == 1:
117
  embeddings = embeddings.reshape(1, -1)
118
  if embeddings.shape[0] == 0:
119
  st.warning("No embeddings were generated for FAISS index.")
@@ -132,8 +132,8 @@ def search_faiss_index(index, query_text, embedding_model, indexed_chunks, k=3):
132
  return []
133
  try:
134
  query_embedding = embedding_model.encode([query_text], convert_to_tensor=False)
135
- if query_embedding.ndim == 1:
136
- query_embedding = query_embedding.reshape(1, -1)
137
  distances, indices = index.search(np.array(query_embedding, dtype=np.float32), k)
138
  results = []
139
  for i in range(len(indices[0])):
@@ -150,37 +150,66 @@ def get_order_details(order_id, customer_orders_data):
150
  if not customer_orders_data:
151
  return "Customer order data is not loaded."
152
  for order in customer_orders_data:
153
- if order.get("order_id") == order_id:
154
  return json.dumps(order, indent=2)
155
  return f"No order found with ID: {order_id}."
156
 
157
  def get_product_info(query, products_data):
158
- """Retrieves product information based on a query."""
 
 
159
  if not products_data:
 
160
  return "Product data is not loaded."
 
161
  query_lower = query.lower()
162
  found_products = []
 
163
  for product in products_data:
164
- # Changed keys to match Products.json
165
- if query_lower in (product.get("Product_Name", "").lower()) or \
166
- query_lower in (product.get("Product_Type", "").lower()) or \
167
- query_lower == (product.get("Product_ID", "").lower()):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  found_products.append(product)
 
169
  if found_products:
170
  return json.dumps(found_products, indent=2)
171
  return f"No product information found matching your query: '{query}'."
172
 
173
  # --- LLM Operations ---
174
- @st.cache_data(show_spinner="Generating response with LLaMA3...")
175
  def generate_response_groq(_groq_client, query, context, model="llama3-8b-8192"):
176
  """Generates a response using GROQ LLaMA3 API."""
177
  if not _groq_client:
178
  return "GROQ client not initialized. Please check API key."
179
  if not query:
180
  return "Query is empty."
 
 
181
  prompt = f"""You are a helpful customer support assistant.
182
  Use the following context to answer the user's question.
183
- If the context doesn't contain the answer, state that you don't have enough information.
184
  Do not make up information. Be concise and polite.
185
 
186
  Context:
@@ -206,7 +235,7 @@ Assistant Answer:
206
 
207
  def initialize_groq_client(api_key_val):
208
  """Initializes the GROQ client."""
209
- if not api_key_val: # Changed parameter name to avoid conflict
210
  st.warning("GROQ API Key is missing.")
211
  return None
212
  try:
@@ -217,7 +246,7 @@ def initialize_groq_client(api_key_val):
217
  return None
218
 
219
  # --- Twilio Operations ---
220
- def initialize_twilio_client(acc_sid, auth_tkn): # Changed parameter names
221
  """Initializes the Twilio client."""
222
  if not acc_sid or not auth_tkn:
223
  st.warning("Twilio Account SID or Auth Token is missing.")
@@ -229,8 +258,8 @@ def initialize_twilio_client(acc_sid, auth_tkn): # Changed parameter names
229
  st.error(f"Failed to initialize Twilio client: {e}")
230
  return None
231
 
232
- def get_new_whatsapp_messages(twilio_client, conversation_service_sid_val, bot_start_time_utc, # Renamed
233
- processed_message_sids, bot_whatsapp_identity_val): # Renamed
234
  """Fetches new, unanswered WhatsApp messages from Twilio Conversations."""
235
  if not twilio_client:
236
  st.warning("Twilio client not initialized.")
@@ -238,25 +267,34 @@ def get_new_whatsapp_messages(twilio_client, conversation_service_sid_val, bot_s
238
  if not conversation_service_sid_val:
239
  st.warning("Twilio Conversation Service SID not provided.")
240
  return []
 
 
 
 
241
 
242
  new_messages_to_process = []
243
  try:
 
244
  conversations = twilio_client.conversations.v1 \
245
  .services(conversation_service_sid_val) \
246
  .conversations \
247
- .list(limit=50)
248
 
249
  for conv in conversations:
 
250
  if conv.date_updated and conv.date_updated > bot_start_time_utc:
 
251
  messages = twilio_client.conversations.v1 \
252
  .services(conversation_service_sid_val) \
253
  .conversations(conv.sid) \
254
  .messages \
255
- .list(order='desc', limit=10)
256
 
257
  for msg in messages:
258
  if msg.sid in processed_message_sids:
259
- continue
 
 
260
  if msg.author and msg.author.lower() != bot_whatsapp_identity_val.lower() and \
261
  msg.date_created and msg.date_created > bot_start_time_utc:
262
  new_messages_to_process.append({
@@ -264,12 +302,15 @@ def get_new_whatsapp_messages(twilio_client, conversation_service_sid_val, bot_s
264
  "author_identity": msg.author, "message_body": msg.body,
265
  "timestamp_utc": msg.date_created
266
  })
267
- break
 
 
 
268
  except Exception as e:
269
  st.error(f"Error fetching Twilio messages: {e}")
270
- return sorted(new_messages_to_process, key=lambda m: m['timestamp_utc'])
271
 
272
- def send_whatsapp_message(twilio_client, conversation_service_sid_val, conversation_sid, message_body, bot_identity_val): # Renamed
273
  """Sends a message to a Twilio Conversation from the bot's identity."""
274
  if not twilio_client:
275
  st.error("Twilio client not initialized for sending message.")
@@ -304,14 +345,14 @@ if APP_TWILIO_ACCOUNT_SID:
304
  st.sidebar.text_input("Twilio Account SID (from Secrets)", value="********" + APP_TWILIO_ACCOUNT_SID[-4:] if len(APP_TWILIO_ACCOUNT_SID) > 4 else "********", disabled=True)
305
  twilio_account_sid_to_use = APP_TWILIO_ACCOUNT_SID
306
  else:
307
- st.sidebar.warning("Secret 'TWILIO_SID' not found.")
308
  twilio_account_sid_to_use = st.sidebar.text_input("Twilio Account SID (Enter Manually)", value=DEFAULT_TWILIO_ACCOUNT_SID_FALLBACK, type="password")
309
 
310
  if APP_TWILIO_AUTH_TOKEN:
311
  st.sidebar.text_input("Twilio Auth Token (from Secrets)", value="********", disabled=True)
312
  twilio_auth_token_to_use = APP_TWILIO_AUTH_TOKEN
313
  else:
314
- st.sidebar.warning("Secret 'TWILIO_TOKEN' not found.")
315
  twilio_auth_token_to_use = st.sidebar.text_input("Twilio Auth Token (Enter Manually)", value=DEFAULT_TWILIO_AUTH_TOKEN_FALLBACK, type="password")
316
 
317
  if APP_GROQ_API_KEY:
@@ -325,7 +366,7 @@ else:
325
  twilio_conversation_service_sid_to_use = st.sidebar.text_input(
326
  "Twilio Conversation Service SID (IS...)",
327
  value=APP_TWILIO_CONVERSATION_SERVICE_SID_SECRET or DEFAULT_TWILIO_CONVERSATION_SERVICE_SID,
328
- type="password",
329
  help="The SID of your Twilio Conversations Service. Can be set by 'TWILIO_CONVERSATION_SERVICE_SID' secret."
330
  )
331
  twilio_bot_whatsapp_identity_to_use = st.sidebar.text_input(
@@ -333,11 +374,11 @@ twilio_bot_whatsapp_identity_to_use = st.sidebar.text_input(
333
  value=APP_TWILIO_BOT_WHATSAPP_IDENTITY_SECRET or DEFAULT_TWILIO_BOT_WHATSAPP_IDENTITY,
334
  help="e.g., 'whatsapp:+1234567890'. Can be set by 'TWILIO_BOT_WHATSAPP_IDENTITY' secret."
335
  )
336
- embedding_model_name_to_use = st.sidebar.text_input( # Renamed
337
  "Embedding Model Name",
338
  value=DEFAULT_EMBEDDING_MODEL_NAME
339
  )
340
- polling_interval_to_use = st.sidebar.number_input( # Renamed
341
  "Twilio Polling Interval (seconds)",
342
  min_value=10, max_value=300,
343
  value=DEFAULT_POLLING_INTERVAL_S,
@@ -356,27 +397,40 @@ if "manual_chat_history" not in st.session_state: st.session_state.manual_chat_h
356
  # --- Helper: Simple Intent Classifier ---
357
  def simple_intent_classifier(query):
358
  query_lower = query.lower()
359
- if any(k in query_lower for k in ["order", "status", "track", "delivery"]):
360
- # More specific regex to find 'ORD' followed by digits (assuming order IDs are like ORD1001)
361
- match = re.search(r'\b(ord\d{3,})\b', query_lower) # Matches 'ord' followed by at least 3 digits, as a whole word
362
- if match:
363
- return "ORDER_STATUS", match.group(1).upper() # Return intent and extracted ID
364
- # Fallback if specific order ID not found but still an order-related query
365
- return "ORDER_STATUS", None # Indicate order status intent but no specific ID found yet
366
-
367
- if any(k in query_lower for k in ["product", "item", "buy", "price", "feature", "stock"]): return "PRODUCT_INFO", None
368
- if any(k in query_lower for k in ["return", "policy", "refund", "exchange", "faq", "question", "how to", "support"]): return "GENERAL_POLICY_FAQ", None
369
- return "UNKNOWN", None # Return intent and None for ID if unknown
 
 
 
 
 
 
 
 
 
 
 
 
 
370
 
371
  # --- Main Application Controls ---
372
  col1, col2, col3, col4 = st.columns(4)
373
  with col1:
374
  if st.button("🚀 Start App", disabled=st.session_state.app_started, use_container_width=True):
375
- if not groq_api_key_to_use: # Use the correct variable
376
  st.error("GROQ API Key is required.")
377
  else:
378
  with st.spinner("Initializing RAG pipeline..."):
379
- st.session_state.embedding_model = initialize_embedding_model(embedding_model_name_to_use) # Use correct var
380
  st.session_state.customer_orders_data = load_json_data(CUSTOMER_ORDERS_FILE)
381
  st.session_state.products_data = load_json_data(PRODUCTS_FILE)
382
  policy_pdf_pages = load_pdf_data(POLICY_PDF_FILE)
@@ -389,18 +443,28 @@ with col1:
389
  create_faiss_index(st.session_state.pdf_text_chunks_raw, st.session_state.embedding_model)
390
  else:
391
  st.session_state.faiss_index_pdfs, st.session_state.indexed_pdf_chunks = None, []
392
- st.warning("FAISS index for PDFs could not be created.")
393
 
394
- st.session_state.groq_client = initialize_groq_client(groq_api_key_to_use) # Use correct var
395
-
396
- if st.session_state.embedding_model and st.session_state.groq_client and \
397
- st.session_state.customer_orders_data and st.session_state.products_data:
 
 
 
 
398
  st.session_state.rag_pipeline_ready = True
399
  st.session_state.app_started = True
400
  st.success("RAG Application Started!")
401
  st.rerun()
402
  else:
403
- st.error("Failed to initialize RAG pipeline. Check configurations and ensure all data files are present in 'docs/'.")
 
 
 
 
 
 
404
  st.session_state.app_started = False
405
  with col2:
406
  if st.button("🛑 Stop App", disabled=not st.session_state.app_started, use_container_width=True):
@@ -410,52 +474,61 @@ with col2:
410
  "bot_start_time_utc", "processed_message_sids", "manual_chat_history"]
411
  for key in keys_to_reset:
412
  if key in st.session_state: del st.session_state[key]
 
413
  st.session_state.app_started = False
414
  st.session_state.bot_started = False
415
  st.session_state.rag_pipeline_ready = False
416
  st.session_state.processed_message_sids = set()
417
  st.session_state.manual_chat_history = []
418
  st.success("Application Stopped.")
 
 
 
 
419
  st.rerun()
420
  with col3:
421
  if st.button("💬 Start WhatsApp Bot", disabled=not st.session_state.app_started or st.session_state.bot_started, use_container_width=True):
422
- if not all([twilio_account_sid_to_use, twilio_auth_token_to_use, twilio_conversation_service_sid_to_use, twilio_bot_whatsapp_identity_to_use]): # Use correct vars
423
- st.error("Twilio credentials, Service SID, and Bot Identity are required.")
424
  else:
425
- st.session_state.twilio_client = initialize_twilio_client(twilio_account_sid_to_use, twilio_auth_token_to_use) # Use correct vars
426
  if st.session_state.twilio_client:
427
  st.session_state.bot_started = True
428
  st.session_state.bot_start_time_utc = datetime.now(timezone.utc)
429
- st.session_state.processed_message_sids = set()
430
- st.session_state.last_twilio_poll_time = time.time() - polling_interval_to_use -1 # Use correct var
431
  st.success("WhatsApp Bot Started!")
432
  st.rerun()
433
  else:
434
- st.error("Failed to initialize Twilio client.")
435
  with col4:
436
  if st.button("🔕 Stop WhatsApp Bot", disabled=not st.session_state.bot_started, use_container_width=True):
437
  st.session_state.bot_started = False
438
  st.info("WhatsApp Bot Stopped.")
 
 
439
  st.rerun()
440
  st.divider()
441
 
442
  # --- Manual Query Interface ---
443
  if st.session_state.get("app_started") and st.session_state.get("rag_pipeline_ready"):
444
  st.subheader("💬 Manual Query")
 
445
  for chat_entry in st.session_state.manual_chat_history:
446
  with st.chat_message(chat_entry["role"]):
447
  st.markdown(chat_entry["content"])
448
- if "context" in chat_entry and chat_entry["context"]:
449
  with st.expander("Retrieved Context"):
450
  try:
451
  # Attempt to parse as JSON only if it looks like a JSON string
452
- if isinstance(chat_entry["context"], str) and (chat_entry["context"].strip().startswith('{') or chat_entry["context"].strip().startswith('[')):
 
453
  st.json(json.loads(chat_entry["context"]))
454
- else:
455
- # Otherwise, display as plain text
 
456
  st.text(str(chat_entry["context"]))
457
- except (json.JSONDecodeError, TypeError):
458
- # Fallback for any other parsing errors
459
  st.text(str(chat_entry["context"]))
460
 
461
  user_query_manual = st.chat_input("Ask a question:")
@@ -464,121 +537,159 @@ if st.session_state.get("app_started") and st.session_state.get("rag_pipeline_re
464
  with st.chat_message("user"): st.markdown(user_query_manual)
465
 
466
  with st.spinner("Thinking..."):
467
- intent_result = simple_intent_classifier(user_query_manual) # Get both intent and potential_id
468
  intent = intent_result[0]
469
- potential_oid_from_intent = intent_result[1] # This is the extracted ID if any
470
 
471
- context_for_llm, raw_context_data = "No specific context.", None
472
 
473
  if intent == "ORDER_STATUS":
474
  order_id_to_check = None
475
- if potential_oid_from_intent:
476
  order_id_to_check = potential_oid_from_intent
477
- else:
478
- # Fallback for edge cases, though the regex should catch most
479
- words = user_query_manual.upper().split()
480
- # This regex specifically looks for 'ORD' followed by digits
481
- possible_match = next((w for w in words if re.match(r'ORD\d+', w)), None)
482
- if possible_match:
483
- order_id_to_check = possible_match
484
-
485
-
486
  if order_id_to_check:
487
- raw_context_data = get_order_details(order_id_to_check.upper(), st.session_state.customer_orders_data)
488
- context_for_llm = f"Order Details: {raw_context_data}"
489
  else:
490
- context_for_llm = "Please provide a valid Order ID (e.g., ORD1234)."
491
- raw_context_data = {"message": "Order ID needed."}
 
492
  elif intent == "PRODUCT_INFO":
493
  raw_context_data = get_product_info(user_query_manual, st.session_state.products_data)
494
- context_for_llm = f"Product Information: {raw_context_data}"
495
- elif intent == "GENERAL_POLICY_FAQ" or intent == "UNKNOWN":
496
- # ... (rest of your existing logic for these intents) ...
497
- if st.session_state.faiss_index_pdfs and st.session_state.embedding_model:
498
- k_val = 2 if intent == "GENERAL_POLICY_FAQ" else 1
499
  retrieved_chunks = search_faiss_index(st.session_state.faiss_index_pdfs, user_query_manual,
500
  st.session_state.embedding_model, st.session_state.indexed_pdf_chunks, k=k_val)
501
  if retrieved_chunks:
502
- context_for_llm = "\n\n".join(retrieved_chunks)
503
- raw_context_data = retrieved_chunks
504
  else:
505
- context_for_llm = "No specific policy/FAQ info found." if intent == "GENERAL_POLICY_FAQ" else "Could not find relevant info."
506
  raw_context_data = {"message": "No relevant PDF chunks found."}
507
  else:
508
- context_for_llm = "Policy/FAQ documents unavailable."
509
- raw_context_data = {"message": "PDF index not ready."}
510
 
511
  llm_response = generate_response_groq(st.session_state.groq_client, user_query_manual, context_for_llm)
 
512
  with st.chat_message("assistant"):
513
  st.markdown(llm_response)
514
- if raw_context_data:
515
- with st.expander("Retrieved Context"):
516
  try:
517
- if isinstance(raw_context_data, str) and (raw_context_data.strip().startswith('{') or raw_context_data.strip().startswith('[')):
 
518
  st.json(json.loads(raw_context_data))
 
 
519
  else:
520
  st.text(str(raw_context_data))
521
  except (json.JSONDecodeError, TypeError):
522
  st.text(str(raw_context_data))
523
  st.session_state.manual_chat_history.append({"role": "assistant", "content": llm_response, "context": raw_context_data})
 
524
 
525
  # --- Twilio Bot Polling Logic ---
526
  if st.session_state.get("bot_started") and st.session_state.get("rag_pipeline_ready"):
527
  current_time = time.time()
528
- if (current_time - st.session_state.get("last_twilio_poll_time", 0)) > polling_interval_to_use: # Use correct var
 
 
 
 
529
  st.session_state.last_twilio_poll_time = current_time
530
- with st.spinner("Checking WhatsApp messages..."):
531
- if not st.session_state.get("twilio_client") or not twilio_conversation_service_sid_to_use or not twilio_bot_whatsapp_identity_to_use: # Use correct vars
532
- st.warning("Twilio client/config missing for polling.")
533
- else:
534
- new_messages = get_new_whatsapp_messages(st.session_state.twilio_client, twilio_conversation_service_sid_to_use,
535
- st.session_state.bot_start_time_utc, st.session_state.processed_message_sids,
536
- twilio_bot_whatsapp_identity_to_use) # Use correct vars
 
 
 
 
 
 
 
537
  if new_messages:
538
- st.info(f"Found {len(new_messages)} new WhatsApp message(s).")
539
  for msg_data in new_messages:
540
- user_query_whatsapp, conv_sid, msg_sid, author_id = msg_data["message_body"], msg_data["conversation_sid"], msg_data["message_sid"], msg_data["author_identity"]
541
- st.write(f"Processing from {author_id} in {conv_sid}: '{user_query_whatsapp}'")
 
 
 
542
 
543
- intent_result_whatsapp = simple_intent_classifier(user_query_whatsapp) # Use the updated classifier
 
544
  intent_whatsapp = intent_result_whatsapp[0]
545
- potential_oid_whatsapp = intent_result_whatsapp[1] # Extracted ID from intent classifier
546
 
547
- context_whatsapp = "No specific context."
 
548
  if intent_whatsapp == "ORDER_STATUS":
549
  order_id_to_check_whatsapp = None
550
  if potential_oid_whatsapp:
551
  order_id_to_check_whatsapp = potential_oid_whatsapp
552
  else:
553
- words_whatsapp = user_query_whatsapp.upper().split()
554
- possible_match_whatsapp = next((w for w in words_whatsapp if re.match(r'ORD\d+', w)), None)
555
- if possible_match_whatsapp:
556
- order_id_to_check_whatsapp = possible_match_whatsapp
557
 
558
  if order_id_to_check_whatsapp:
559
- context_whatsapp = f"Order Details: {get_order_details(order_id_to_check_whatsapp.upper(), st.session_state.customer_orders_data)}"
 
560
  else:
561
- context_whatsapp = "Please provide a valid Order ID."
 
562
  elif intent_whatsapp == "PRODUCT_INFO":
563
- context_whatsapp = f"Product Info: {get_product_info(user_query_whatsapp, st.session_state.products_data)}"
 
 
564
  elif intent_whatsapp == "GENERAL_POLICY_FAQ" or intent_whatsapp == "UNKNOWN":
565
- if st.session_state.faiss_index_pdfs and st.session_state.embedding_model:
566
- k_val = 2 if intent_whatsapp == "GENERAL_POLICY_FAQ" else 1
567
- chunks = search_faiss_index(st.session_state.faiss_index_pdfs, user_query_whatsapp, st.session_state.embedding_model, st.session_state.indexed_pdf_chunks, k=k_val)
568
- context_whatsapp = "\n\n".join(chunks) if chunks else ("No policy/FAQ info." if intent_whatsapp == "GENERAL_POLICY_FAQ" else "No relevant info.")
569
- else: context_whatsapp = "Policy/FAQ docs unavailable."
 
 
 
 
 
 
 
 
570
 
571
- response_whatsapp = generate_response_groq(st.session_state.groq_client, user_query_whatsapp, context_whatsapp)
572
- if send_whatsapp_message(st.session_state.twilio_client, twilio_conversation_service_sid_to_use, conv_sid, response_whatsapp, twilio_bot_whatsapp_identity_to_use): # Use correct vars
573
  st.session_state.processed_message_sids.add(msg_sid)
574
- st.success(f"Responded to {msg_sid} from {author_id}")
575
- else: st.error(f"Failed to send response for {msg_sid}")
576
- st.experimental_rerun()
 
 
 
 
577
 
578
  # --- Footer & Status ---
579
  st.sidebar.markdown("---")
580
  st.sidebar.info("Ensure all keys and SIDs are correctly configured. Primary API keys (Twilio SID/Token, GROQ Key) are loaded from secrets if available.")
581
  if st.session_state.get("app_started"):
582
- st.sidebar.success(f"App RUNNING. WhatsApp Bot {'RUNNING' if st.session_state.get('bot_started') else 'STOPPED'}.")
 
 
 
 
583
  else:
584
- st.sidebar.warning("App is STOPPED.")
 
 
20
  DEFAULT_GROQ_API_KEY_FALLBACK = "" # Fallback if secret "GROQ_API_KEY" is not found
21
 
22
  DEFAULT_TWILIO_CONVERSATION_SERVICE_SID = ""
23
+ DEFAULT_TWILIO_BOT_WHATSAPP_IDENTITY = st.secrets.get("TWILIO_PHONE_NUMBER", "whatsapp:+14155238886") # Twilio Sandbox default
24
  DEFAULT_EMBEDDING_MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2"
25
  DEFAULT_POLLING_INTERVAL_S = 30
26
  DOCS_FOLDER = "docs/"
 
66
  for page_num in range(len(reader.pages)):
67
  page = reader.pages[page_num]
68
  text_pages.append(page.extract_text() or "")
69
+ return text_pages
70
  except FileNotFoundError:
71
  st.error(f"Error: PDF file not found at {file_path}")
72
  return []
 
87
  if end >= len(full_text):
88
  break
89
  start += (chunk_size - chunk_overlap)
90
+ if start >= len(full_text): # Should be `start > len(full_text) - chunk_overlap` or similar to avoid empty last chunk
91
  break
92
  return [chunk for chunk in chunks if chunk.strip()]
93
 
 
113
  st.warning("No valid text chunks to embed for FAISS index.")
114
  return None, []
115
  embeddings = _embedding_model.encode(valid_chunks, convert_to_tensor=False)
116
+ if embeddings.ndim == 1: # Handle single chunk case
117
  embeddings = embeddings.reshape(1, -1)
118
  if embeddings.shape[0] == 0:
119
  st.warning("No embeddings were generated for FAISS index.")
 
132
  return []
133
  try:
134
  query_embedding = embedding_model.encode([query_text], convert_to_tensor=False)
135
+ if query_embedding.ndim == 1: # Handle single query embedding
136
+ query_embedding = query_embedding.reshape(1, -1)
137
  distances, indices = index.search(np.array(query_embedding, dtype=np.float32), k)
138
  results = []
139
  for i in range(len(indices[0])):
 
150
  if not customer_orders_data:
151
  return "Customer order data is not loaded."
152
  for order in customer_orders_data:
153
+ if order.get("order_id") == order_id: # Assuming order_id is stored as uppercase in JSON or matches case
154
  return json.dumps(order, indent=2)
155
  return f"No order found with ID: {order_id}."
156
 
157
  def get_product_info(query, products_data):
158
+ """Retrieves product information based on a query.
159
+ This function is updated to correctly find products by ID, name, or type within the query.
160
+ """
161
  if not products_data:
162
+ st.warning("Product data is not loaded or is empty in get_product_info.")
163
  return "Product data is not loaded."
164
+
165
  query_lower = query.lower()
166
  found_products = []
167
+
168
  for product in products_data:
169
+ if not isinstance(product, dict): # Skip if product entry is not a dictionary
170
+ continue
171
+
172
+ product_id_lower = str(product.get("Product_ID", "")).lower()
173
+ product_name_lower = str(product.get("Product_Name", "")).lower()
174
+ product_type_lower = str(product.get("Product_Type", "")).lower()
175
+
176
+ match = False
177
+ # 1. Check if the Product ID is mentioned in the query
178
+ if product_id_lower and product_id_lower in query_lower:
179
+ match = True
180
+
181
+ # 2. If no match by ID, check for Product Name
182
+ # - If the query (e.g., "rattle") is a substring of the product name (e.g., "soft rattle set")
183
+ # - OR if the product name (e.g., "soft rattle set") is a substring of the query (e.g., "info on soft rattle set")
184
+ if not match and product_name_lower:
185
+ if query_lower in product_name_lower or product_name_lower in query_lower:
186
+ match = True
187
+
188
+ # 3. If no match yet, check for Product Type similarly
189
+ if not match and product_type_lower:
190
+ if query_lower in product_type_lower or product_type_lower in query_lower:
191
+ match = True
192
+
193
+ if match:
194
  found_products.append(product)
195
+
196
  if found_products:
197
  return json.dumps(found_products, indent=2)
198
  return f"No product information found matching your query: '{query}'."
199
 
200
  # --- LLM Operations ---
201
+ @st.cache_data(show_spinner="Generating response with LLaMA3...") # Consider disabling caching if context changes frequently or add more granular cache invalidation
202
  def generate_response_groq(_groq_client, query, context, model="llama3-8b-8192"):
203
  """Generates a response using GROQ LLaMA3 API."""
204
  if not _groq_client:
205
  return "GROQ client not initialized. Please check API key."
206
  if not query:
207
  return "Query is empty."
208
+
209
+ # Basic prompt, can be enhanced
210
  prompt = f"""You are a helpful customer support assistant.
211
  Use the following context to answer the user's question.
212
+ If the context doesn't contain the answer, state that you don't have enough information or ask clarifying questions.
213
  Do not make up information. Be concise and polite.
214
 
215
  Context:
 
235
 
236
  def initialize_groq_client(api_key_val):
237
  """Initializes the GROQ client."""
238
+ if not api_key_val:
239
  st.warning("GROQ API Key is missing.")
240
  return None
241
  try:
 
246
  return None
247
 
248
  # --- Twilio Operations ---
249
+ def initialize_twilio_client(acc_sid, auth_tkn):
250
  """Initializes the Twilio client."""
251
  if not acc_sid or not auth_tkn:
252
  st.warning("Twilio Account SID or Auth Token is missing.")
 
258
  st.error(f"Failed to initialize Twilio client: {e}")
259
  return None
260
 
261
+ def get_new_whatsapp_messages(twilio_client, conversation_service_sid_val, bot_start_time_utc,
262
+ processed_message_sids, bot_whatsapp_identity_val):
263
  """Fetches new, unanswered WhatsApp messages from Twilio Conversations."""
264
  if not twilio_client:
265
  st.warning("Twilio client not initialized.")
 
267
  if not conversation_service_sid_val:
268
  st.warning("Twilio Conversation Service SID not provided.")
269
  return []
270
+ if not bot_whatsapp_identity_val: # Added check
271
+ st.warning("Twilio Bot WhatsApp Identity not provided.")
272
+ return []
273
+
274
 
275
  new_messages_to_process = []
276
  try:
277
+ # Fetch conversations updated since the bot started or a reasonable window
278
  conversations = twilio_client.conversations.v1 \
279
  .services(conversation_service_sid_val) \
280
  .conversations \
281
+ .list(limit=50) # Consider filtering by date_updated if API supports
282
 
283
  for conv in conversations:
284
+ # Check if conversation was updated after bot start time
285
  if conv.date_updated and conv.date_updated > bot_start_time_utc:
286
+ # Fetch recent messages from this conversation
287
  messages = twilio_client.conversations.v1 \
288
  .services(conversation_service_sid_val) \
289
  .conversations(conv.sid) \
290
  .messages \
291
+ .list(order='desc', limit=10) # Get latest messages first
292
 
293
  for msg in messages:
294
  if msg.sid in processed_message_sids:
295
+ continue # Skip already processed messages
296
+
297
+ # Check if message is from a user (not the bot) and is new
298
  if msg.author and msg.author.lower() != bot_whatsapp_identity_val.lower() and \
299
  msg.date_created and msg.date_created > bot_start_time_utc:
300
  new_messages_to_process.append({
 
302
  "author_identity": msg.author, "message_body": msg.body,
303
  "timestamp_utc": msg.date_created
304
  })
305
+ # Assuming we only process the latest unread message per conversation polling cycle
306
+ # If multiple new messages from the same user in one convo need processing, this break might be too early.
307
+ # For simplicity, processing one latest unread message per conversation poll.
308
+ break
309
  except Exception as e:
310
  st.error(f"Error fetching Twilio messages: {e}")
311
+ return sorted(new_messages_to_process, key=lambda m: m['timestamp_utc']) # Process in chronological order
312
 
313
+ def send_whatsapp_message(twilio_client, conversation_service_sid_val, conversation_sid, message_body, bot_identity_val):
314
  """Sends a message to a Twilio Conversation from the bot's identity."""
315
  if not twilio_client:
316
  st.error("Twilio client not initialized for sending message.")
 
345
  st.sidebar.text_input("Twilio Account SID (from Secrets)", value="********" + APP_TWILIO_ACCOUNT_SID[-4:] if len(APP_TWILIO_ACCOUNT_SID) > 4 else "********", disabled=True)
346
  twilio_account_sid_to_use = APP_TWILIO_ACCOUNT_SID
347
  else:
348
+ st.sidebar.warning("Secret 'TWILIO_ACCOUNT_SID' not found.") # Corrected secret name from TWILIO_SID
349
  twilio_account_sid_to_use = st.sidebar.text_input("Twilio Account SID (Enter Manually)", value=DEFAULT_TWILIO_ACCOUNT_SID_FALLBACK, type="password")
350
 
351
  if APP_TWILIO_AUTH_TOKEN:
352
  st.sidebar.text_input("Twilio Auth Token (from Secrets)", value="********", disabled=True)
353
  twilio_auth_token_to_use = APP_TWILIO_AUTH_TOKEN
354
  else:
355
+ st.sidebar.warning("Secret 'TWILIO_AUTH_TOKEN' not found.") # Corrected secret name from TWILIO_TOKEN
356
  twilio_auth_token_to_use = st.sidebar.text_input("Twilio Auth Token (Enter Manually)", value=DEFAULT_TWILIO_AUTH_TOKEN_FALLBACK, type="password")
357
 
358
  if APP_GROQ_API_KEY:
 
366
  twilio_conversation_service_sid_to_use = st.sidebar.text_input(
367
  "Twilio Conversation Service SID (IS...)",
368
  value=APP_TWILIO_CONVERSATION_SERVICE_SID_SECRET or DEFAULT_TWILIO_CONVERSATION_SERVICE_SID,
369
+ type="password", # Keep as password if sensitive
370
  help="The SID of your Twilio Conversations Service. Can be set by 'TWILIO_CONVERSATION_SERVICE_SID' secret."
371
  )
372
  twilio_bot_whatsapp_identity_to_use = st.sidebar.text_input(
 
374
  value=APP_TWILIO_BOT_WHATSAPP_IDENTITY_SECRET or DEFAULT_TWILIO_BOT_WHATSAPP_IDENTITY,
375
  help="e.g., 'whatsapp:+1234567890'. Can be set by 'TWILIO_BOT_WHATSAPP_IDENTITY' secret."
376
  )
377
+ embedding_model_name_to_use = st.sidebar.text_input(
378
  "Embedding Model Name",
379
  value=DEFAULT_EMBEDDING_MODEL_NAME
380
  )
381
+ polling_interval_to_use = st.sidebar.number_input(
382
  "Twilio Polling Interval (seconds)",
383
  min_value=10, max_value=300,
384
  value=DEFAULT_POLLING_INTERVAL_S,
 
397
  # --- Helper: Simple Intent Classifier ---
398
  def simple_intent_classifier(query):
399
  query_lower = query.lower()
400
+ # Order status: look for 'order', 'status', 'track', 'delivery' AND an order ID pattern
401
+ order_keywords = ["order", "status", "track", "delivery"]
402
+ # Regex for 'ORD' followed by 3 or more digits (case insensitive for 'ord')
403
+ order_id_match = re.search(r'\b(ord\d{3,})\b', query_lower, re.IGNORECASE)
404
+
405
+ if any(k in query_lower for k in order_keywords):
406
+ if order_id_match:
407
+ return "ORDER_STATUS", order_id_match.group(1).upper() # Return intent and extracted ID
408
+ return "ORDER_STATUS", None # Order-related query but no specific ID found yet
409
+
410
+ # Product info: look for 'product', 'item', 'buy', 'price', 'feature', 'stock' OR a product ID pattern
411
+ product_keywords = ["product", "item", "buy", "price", "feature", "stock"]
412
+ # Regex for 'PRD' followed by 3 or more digits (case insensitive for 'prd')
413
+ # This is an example, adjust if your product IDs have a different format
414
+ product_id_match = re.search(r'\b(prd\d{3,})\b', query_lower, re.IGNORECASE)
415
+ if any(k in query_lower for k in product_keywords) or product_id_match:
416
+ # If a PRD ID is explicitly found, we can pass it, though get_product_info also searches the query
417
+ # For simplicity, product_info intent doesn't pass an ID here, get_product_info handles it.
418
+ return "PRODUCT_INFO", None
419
+
420
+ if any(k in query_lower for k in ["return", "policy", "refund", "exchange", "faq", "question", "how to", "support"]):
421
+ return "GENERAL_POLICY_FAQ", None
422
+
423
+ return "UNKNOWN", None
424
 
425
  # --- Main Application Controls ---
426
  col1, col2, col3, col4 = st.columns(4)
427
  with col1:
428
  if st.button("🚀 Start App", disabled=st.session_state.app_started, use_container_width=True):
429
+ if not groq_api_key_to_use:
430
  st.error("GROQ API Key is required.")
431
  else:
432
  with st.spinner("Initializing RAG pipeline..."):
433
+ st.session_state.embedding_model = initialize_embedding_model(embedding_model_name_to_use)
434
  st.session_state.customer_orders_data = load_json_data(CUSTOMER_ORDERS_FILE)
435
  st.session_state.products_data = load_json_data(PRODUCTS_FILE)
436
  policy_pdf_pages = load_pdf_data(POLICY_PDF_FILE)
 
443
  create_faiss_index(st.session_state.pdf_text_chunks_raw, st.session_state.embedding_model)
444
  else:
445
  st.session_state.faiss_index_pdfs, st.session_state.indexed_pdf_chunks = None, []
446
+ st.warning("FAISS index for PDFs could not be created (model or chunks missing).")
447
 
448
+ st.session_state.groq_client = initialize_groq_client(groq_api_key_to_use)
449
+
450
+ # Check all critical components for RAG readiness
451
+ if st.session_state.embedding_model and \
452
+ st.session_state.groq_client and \
453
+ st.session_state.customer_orders_data is not None and \
454
+ st.session_state.products_data is not None and \
455
+ (st.session_state.faiss_index_pdfs is not None or not all_pdf_text_pages): # Index needed if PDFs exist
456
  st.session_state.rag_pipeline_ready = True
457
  st.session_state.app_started = True
458
  st.success("RAG Application Started!")
459
  st.rerun()
460
  else:
461
+ error_messages = []
462
+ if not st.session_state.embedding_model: error_messages.append("Embedding model failed to initialize.")
463
+ if not st.session_state.groq_client: error_messages.append("GROQ client failed to initialize.")
464
+ if st.session_state.customer_orders_data is None: error_messages.append(f"CustomerOrders.json ({CUSTOMER_ORDERS_FILE}) failed to load.")
465
+ if st.session_state.products_data is None: error_messages.append(f"Products.json ({PRODUCTS_FILE}) failed to load.")
466
+ if all_pdf_text_pages and st.session_state.faiss_index_pdfs is None: error_messages.append("PDF FAISS index failed to create.")
467
+ st.error("Failed to initialize RAG pipeline. Issues:\n- " + "\n- ".join(error_messages) + "\nCheck configurations and ensure all data files are present in 'docs/'.")
468
  st.session_state.app_started = False
469
  with col2:
470
  if st.button("🛑 Stop App", disabled=not st.session_state.app_started, use_container_width=True):
 
474
  "bot_start_time_utc", "processed_message_sids", "manual_chat_history"]
475
  for key in keys_to_reset:
476
  if key in st.session_state: del st.session_state[key]
477
+ # Explicitly reset to default states
478
  st.session_state.app_started = False
479
  st.session_state.bot_started = False
480
  st.session_state.rag_pipeline_ready = False
481
  st.session_state.processed_message_sids = set()
482
  st.session_state.manual_chat_history = []
483
  st.success("Application Stopped.")
484
+ # Clear cached resources if desired (or let Streamlit manage them)
485
+ # initialize_embedding_model.clear()
486
+ # create_faiss_index.clear()
487
+ # generate_response_groq.clear()
488
  st.rerun()
489
  with col3:
490
  if st.button("💬 Start WhatsApp Bot", disabled=not st.session_state.app_started or st.session_state.bot_started, use_container_width=True):
491
+ if not all([twilio_account_sid_to_use, twilio_auth_token_to_use, twilio_conversation_service_sid_to_use, twilio_bot_whatsapp_identity_to_use]):
492
+ st.error("Twilio Account SID, Auth Token, Conversation Service SID, and Bot WhatsApp Identity are all required.")
493
  else:
494
+ st.session_state.twilio_client = initialize_twilio_client(twilio_account_sid_to_use, twilio_auth_token_to_use)
495
  if st.session_state.twilio_client:
496
  st.session_state.bot_started = True
497
  st.session_state.bot_start_time_utc = datetime.now(timezone.utc)
498
+ st.session_state.processed_message_sids = set() # Reset processed messages on bot start
499
+ st.session_state.last_twilio_poll_time = time.time() - polling_interval_to_use - 1 # Ensure immediate first poll
500
  st.success("WhatsApp Bot Started!")
501
  st.rerun()
502
  else:
503
+ st.error("Failed to initialize Twilio client. WhatsApp Bot not started.")
504
  with col4:
505
  if st.button("🔕 Stop WhatsApp Bot", disabled=not st.session_state.bot_started, use_container_width=True):
506
  st.session_state.bot_started = False
507
  st.info("WhatsApp Bot Stopped.")
508
+ # Optionally clear twilio_client from session state if desired
509
+ # if "twilio_client" in st.session_state: del st.session_state.twilio_client
510
  st.rerun()
511
  st.divider()
512
 
513
  # --- Manual Query Interface ---
514
  if st.session_state.get("app_started") and st.session_state.get("rag_pipeline_ready"):
515
  st.subheader("💬 Manual Query")
516
+ # Display chat history
517
  for chat_entry in st.session_state.manual_chat_history:
518
  with st.chat_message(chat_entry["role"]):
519
  st.markdown(chat_entry["content"])
520
+ if "context" in chat_entry and chat_entry["context"]: # Check if context exists and is not None/empty
521
  with st.expander("Retrieved Context"):
522
  try:
523
  # Attempt to parse as JSON only if it looks like a JSON string
524
+ if isinstance(chat_entry["context"], str) and \
525
+ (chat_entry["context"].strip().startswith('{') or chat_entry["context"].strip().startswith('[')):
526
  st.json(json.loads(chat_entry["context"]))
527
+ elif isinstance(chat_entry["context"], list): # Handle if context is already a list (e.g. PDF chunks)
528
+ st.json(chat_entry["context"]) # Or st.text for list of strings
529
+ else: # Otherwise, display as plain text
530
  st.text(str(chat_entry["context"]))
531
+ except (json.JSONDecodeError, TypeError): # Fallback for any other parsing errors
 
532
  st.text(str(chat_entry["context"]))
533
 
534
  user_query_manual = st.chat_input("Ask a question:")
 
537
  with st.chat_message("user"): st.markdown(user_query_manual)
538
 
539
  with st.spinner("Thinking..."):
540
+ intent_result = simple_intent_classifier(user_query_manual)
541
  intent = intent_result[0]
542
+ potential_oid_from_intent = intent_result[1] # This is the extracted Order ID if any
543
 
544
+ context_for_llm, raw_context_data = "No specific context could be retrieved.", None # Default
545
 
546
  if intent == "ORDER_STATUS":
547
  order_id_to_check = None
548
+ if potential_oid_from_intent: # ID from intent classifier (preferred)
549
  order_id_to_check = potential_oid_from_intent
550
+ else: # Fallback: try to find any 'ORDxxx' in the query if intent classifier missed it (less likely with current regex)
551
+ match_manual = re.search(r'\b(ord\d{3,})\b', user_query_manual.lower(), re.IGNORECASE)
552
+ if match_manual:
553
+ order_id_to_check = match_manual.group(1).upper()
554
+
 
 
 
 
555
  if order_id_to_check:
556
+ raw_context_data = get_order_details(order_id_to_check, st.session_state.customer_orders_data)
557
+ context_for_llm = f"Order Details for {order_id_to_check}: {raw_context_data}"
558
  else:
559
+ context_for_llm = "To check an order status, please provide a valid Order ID (e.g., ORD123)."
560
+ raw_context_data = {"message": "Order ID needed or not found in query."}
561
+
562
  elif intent == "PRODUCT_INFO":
563
  raw_context_data = get_product_info(user_query_manual, st.session_state.products_data)
564
+ context_for_llm = f"Product Information related to '{user_query_manual}': {raw_context_data}"
565
+
566
+ elif intent == "GENERAL_POLICY_FAQ" or intent == "UNKNOWN": # Consolidate for PDF search
567
+ if st.session_state.faiss_index_pdfs and st.session_state.embedding_model and st.session_state.indexed_pdf_chunks:
568
+ k_val = 3 if intent == "GENERAL_POLICY_FAQ" else 2 # Retrieve more for specific FAQ, less for UNKNOWN
569
  retrieved_chunks = search_faiss_index(st.session_state.faiss_index_pdfs, user_query_manual,
570
  st.session_state.embedding_model, st.session_state.indexed_pdf_chunks, k=k_val)
571
  if retrieved_chunks:
572
+ context_for_llm = "Relevant information from documents:\n\n" + "\n\n---\n\n".join(retrieved_chunks)
573
+ raw_context_data = retrieved_chunks # Store the list of chunks
574
  else:
575
+ context_for_llm = "I couldn't find specific information in our policy or FAQ documents regarding your query."
576
  raw_context_data = {"message": "No relevant PDF chunks found."}
577
  else:
578
+ context_for_llm = "Our policy and FAQ documents are currently unavailable for search."
579
+ raw_context_data = {"message": "PDF index or embedding model not ready."}
580
 
581
  llm_response = generate_response_groq(st.session_state.groq_client, user_query_manual, context_for_llm)
582
+
583
  with st.chat_message("assistant"):
584
  st.markdown(llm_response)
585
+ if raw_context_data: # Display context if it was retrieved
586
+ with st.expander("Retrieved Context For Assistant"): # Changed label for clarity
587
  try:
588
+ if isinstance(raw_context_data, str) and \
589
+ (raw_context_data.strip().startswith('{') or raw_context_data.strip().startswith('[')):
590
  st.json(json.loads(raw_context_data))
591
+ elif isinstance(raw_context_data, list):
592
+ st.json(raw_context_data) # Display list of strings (chunks) as JSON array
593
  else:
594
  st.text(str(raw_context_data))
595
  except (json.JSONDecodeError, TypeError):
596
  st.text(str(raw_context_data))
597
  st.session_state.manual_chat_history.append({"role": "assistant", "content": llm_response, "context": raw_context_data})
598
+ st.rerun() # Rerun to update chat display immediately
599
 
600
  # --- Twilio Bot Polling Logic ---
601
  if st.session_state.get("bot_started") and st.session_state.get("rag_pipeline_ready"):
602
  current_time = time.time()
603
+ # Ensure last_twilio_poll_time is initialized
604
+ if "last_twilio_poll_time" not in st.session_state:
605
+ st.session_state.last_twilio_poll_time = current_time - polling_interval_to_use - 1
606
+
607
+ if (current_time - st.session_state.last_twilio_poll_time) > polling_interval_to_use:
608
  st.session_state.last_twilio_poll_time = current_time
609
+
610
+ # Check if Twilio client and necessary configs are available
611
+ if not st.session_state.get("twilio_client") or \
612
+ not twilio_conversation_service_sid_to_use or \
613
+ not twilio_bot_whatsapp_identity_to_use or \
614
+ not st.session_state.get("bot_start_time_utc"):
615
+ st.warning("Twilio client/config missing for polling. Ensure bot is started and SIDs are set.")
616
+ else:
617
+ with st.spinner(f"Checking WhatsApp messages (last poll: {datetime.fromtimestamp(st.session_state.last_twilio_poll_time).strftime('%H:%M:%S')})..."):
618
+ new_messages = get_new_whatsapp_messages(st.session_state.twilio_client,
619
+ twilio_conversation_service_sid_to_use,
620
+ st.session_state.bot_start_time_utc,
621
+ st.session_state.processed_message_sids,
622
+ twilio_bot_whatsapp_identity_to_use)
623
  if new_messages:
624
+ st.info(f"Found {len(new_messages)} new WhatsApp message(s) to process.")
625
  for msg_data in new_messages:
626
+ user_query_whatsapp, conv_sid, msg_sid, author_id = \
627
+ msg_data["message_body"], msg_data["conversation_sid"], \
628
+ msg_data["message_sid"], msg_data["author_identity"]
629
+
630
+ st.write(f"Processing WhatsApp message from {author_id} in conversation {conv_sid}: '{user_query_whatsapp}' (SID: {msg_sid})")
631
 
632
+ # --- (Identical RAG logic as manual query, adapted for WhatsApp context) ---
633
+ intent_result_whatsapp = simple_intent_classifier(user_query_whatsapp)
634
  intent_whatsapp = intent_result_whatsapp[0]
635
+ potential_oid_whatsapp = intent_result_whatsapp[1]
636
 
637
+ context_for_llm_whatsapp = "No specific context could be retrieved."
638
+
639
  if intent_whatsapp == "ORDER_STATUS":
640
  order_id_to_check_whatsapp = None
641
  if potential_oid_whatsapp:
642
  order_id_to_check_whatsapp = potential_oid_whatsapp
643
  else:
644
+ match_whatsapp = re.search(r'\b(ord\d{3,})\b', user_query_whatsapp.lower(), re.IGNORECASE)
645
+ if match_whatsapp:
646
+ order_id_to_check_whatsapp = match_whatsapp.group(1).upper()
 
647
 
648
  if order_id_to_check_whatsapp:
649
+ order_details_whatsapp = get_order_details(order_id_to_check_whatsapp, st.session_state.customer_orders_data)
650
+ context_for_llm_whatsapp = f"Order Details for {order_id_to_check_whatsapp}: {order_details_whatsapp}"
651
  else:
652
+ context_for_llm_whatsapp = "To check an order status, please provide a valid Order ID (e.g., ORD123)."
653
+
654
  elif intent_whatsapp == "PRODUCT_INFO":
655
+ product_info_whatsapp = get_product_info(user_query_whatsapp, st.session_state.products_data)
656
+ context_for_llm_whatsapp = f"Product Information related to '{user_query_whatsapp}': {product_info_whatsapp}"
657
+
658
  elif intent_whatsapp == "GENERAL_POLICY_FAQ" or intent_whatsapp == "UNKNOWN":
659
+ if st.session_state.faiss_index_pdfs and st.session_state.embedding_model and st.session_state.indexed_pdf_chunks:
660
+ k_val_whatsapp = 3 if intent_whatsapp == "GENERAL_POLICY_FAQ" else 2
661
+ chunks_whatsapp = search_faiss_index(st.session_state.faiss_index_pdfs, user_query_whatsapp,
662
+ st.session_state.embedding_model, st.session_state.indexed_pdf_chunks, k=k_val_whatsapp)
663
+ if chunks_whatsapp:
664
+ context_for_llm_whatsapp = "Relevant information from documents:\n\n" + "\n\n---\n\n".join(chunks_whatsapp)
665
+ else:
666
+ context_for_llm_whatsapp = "I couldn't find specific information in our policy or FAQ documents regarding your query."
667
+ else:
668
+ context_for_llm_whatsapp = "Our policy and FAQ documents are currently unavailable for search."
669
+ # --- End of RAG logic for WhatsApp ---
670
+
671
+ response_whatsapp = generate_response_groq(st.session_state.groq_client, user_query_whatsapp, context_for_llm_whatsapp)
672
 
673
+ if send_whatsapp_message(st.session_state.twilio_client, twilio_conversation_service_sid_to_use,
674
+ conv_sid, response_whatsapp, twilio_bot_whatsapp_identity_to_use):
675
  st.session_state.processed_message_sids.add(msg_sid)
676
+ st.success(f"Successfully responded to WhatsApp message SID {msg_sid} from {author_id}.")
677
+ else:
678
+ st.error(f"Failed to send WhatsApp response for message SID {msg_sid} from {author_id}.")
679
+ st.experimental_rerun() # Rerun to clear spinner and update UI if messages were processed
680
+ # else:
681
+ # st.write(f"No new WhatsApp messages since last poll at {datetime.fromtimestamp(st.session_state.last_twilio_poll_time).strftime('%H:%M:%S')}.")
682
+
683
 
684
  # --- Footer & Status ---
685
  st.sidebar.markdown("---")
686
  st.sidebar.info("Ensure all keys and SIDs are correctly configured. Primary API keys (Twilio SID/Token, GROQ Key) are loaded from secrets if available.")
687
  if st.session_state.get("app_started"):
688
+ status_color = "green" if st.session_state.get("rag_pipeline_ready") else "orange"
689
+ app_status_text = "App RUNNING" if st.session_state.get("rag_pipeline_ready") else "App Initializing/Error"
690
+ bot_status_text = "WhatsApp Bot RUNNING" if st.session_state.get("bot_started") else "WhatsApp Bot STOPPED"
691
+ st.sidebar.markdown(f"<span style='color:{status_color};'>{app_status_text}</span>. {bot_status_text}.", unsafe_allow_html=True)
692
+
693
  else:
694
+ st.sidebar.warning("App is STOPPED.")
695
+