masadonline commited on
Commit
a29e958
·
verified ·
1 Parent(s): ef85737

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +396 -133
app.py CHANGED
@@ -1,171 +1,434 @@
 
1
  import os
 
 
2
  import time
3
- import json
4
  import threading
5
- from datetime import datetime
6
-
7
- import streamlit as st
8
- from twilio.rest import Client
 
 
 
 
 
9
  from langchain.embeddings import HuggingFaceEmbeddings
10
- from langchain.vectorstores import FAISS
11
- from langchain.document_loaders import PyMuPDFLoader, TextLoader, UnstructuredExcelLoader, UnstructuredWordDocumentLoader
12
- from langchain.text_splitter import RecursiveCharacterTextSplitter
13
- from langchain.llms import OpenAI
 
 
14
 
15
- # Config
 
 
 
 
16
  DOCS_DIR = "docs"
17
  EMBEDDING_MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2"
18
- MONITOR_INTERVAL_SECONDS = 30
19
- APP_START_TIME = datetime.utcnow()
20
-
21
- # Helper functions
22
-
23
- def load_and_process_documents(folder_path):
24
- loaders = {
25
- ".pdf": PyMuPDFLoader,
26
- ".txt": TextLoader,
27
- ".xlsx": UnstructuredExcelLoader,
28
- ".docx": UnstructuredWordDocumentLoader
29
- }
30
- docs = []
31
- for filename in os.listdir(folder_path):
32
- file_path = os.path.join(folder_path, filename)
33
- ext = os.path.splitext(filename)[-1].lower()
34
- loader_cls = loaders.get(ext)
35
- if loader_cls:
36
- try:
37
- loader = loader_cls(file_path)
38
- docs.extend(loader.load())
39
- except Exception as e:
40
- print(f"❌ Failed to load {filename}: {e}")
41
- if not docs:
42
- st.error("No documents loaded.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  return []
44
- text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
45
- return text_splitter.split_documents(docs)
46
 
47
- def create_vector_store(docs, model_name):
48
- embeddings = HuggingFaceEmbeddings(model_name=model_name)
49
- return FAISS.from_documents(docs, embeddings)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
- def get_llm(api_key):
52
- os.environ["OPENAI_API_KEY"] = api_key
53
- return OpenAI(model_name="gpt-3.5-turbo", temperature=0)
54
 
55
  def fetch_latest_incoming_message(client, conversation_sid):
 
56
  try:
57
- messages = client.conversations.v1.conversations(conversation_sid).messages.list(limit=5)
58
- for msg in reversed(messages):
59
- if msg.direction == "inbound" and msg.author and msg.body:
60
  return {
 
61
  "author": msg.author,
62
- "body": msg.body.strip(),
63
  "timestamp": msg.date_created
64
  }
 
65
  except Exception as e:
66
- print(f"❌ Error fetching messages: {e}")
67
- return None
68
-
69
- def retrieve_chunks(query, index, embed_model, text_chunks, k=4):
70
- query_embedding = embed_model.embed_query(query)
71
- docs_and_scores = index.similarity_search_by_vector(query_embedding, k=k)
72
- return [doc.page_content for doc in docs_and_scores]
73
-
74
- def generate_answer_with_groq(question, context):
75
- from groq import Groq
76
- client = Groq(api_key=os.getenv("GROQ_API_KEY"))
77
- chat_completion = client.chat.completions.create(
78
- model="llama3-8b-8192",
79
- messages=[
80
- {"role": "system", "content": "You are a helpful assistant for a toy shop. Respond to customer queries based on provided order and product info."},
81
- {"role": "user", "content": f"Context: {context}\n\nQuestion: {question}"}
82
- ]
83
- )
84
- return chat_completion.choices[0].message.content.strip()
85
 
86
- def send_twilio_message(client, conversation_sid, reply):
87
- try:
88
- client.conversations.v1.conversations(conversation_sid).messages.create(body=reply)
89
- except Exception as e:
90
- print(f"❌ Failed to send message: {e}")
91
 
92
- def start_conversation_monitor(client, index, embed_model, text_chunks):
93
- processed_convos = set()
94
  last_processed_timestamp = {}
95
-
96
- def poll_conversation(convo_sid):
97
- while True:
98
- try:
99
- latest_msg = fetch_latest_incoming_message(client, convo_sid)
100
- if latest_msg:
101
- msg_time = latest_msg["timestamp"]
102
- if convo_sid not in last_processed_timestamp or msg_time > last_processed_timestamp[convo_sid]:
103
- last_processed_timestamp[convo_sid] = msg_time
104
- question = latest_msg["body"]
105
- sender = latest_msg["author"]
106
- print(f"\n📥 New message from {sender} in {convo_sid}: {question}")
107
- context = "\n\n".join(retrieve_chunks(question, index, embed_model, text_chunks))
108
- answer = generate_answer_with_groq(question, context)
 
 
 
 
 
109
  send_twilio_message(client, convo_sid, answer)
110
  print(f"📤 Replied to {sender}: {answer}")
111
- time.sleep(3)
112
- except Exception as e:
113
- print(f" Error in convo {convo_sid} polling:", e)
114
- time.sleep(5)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
- def poll_new_conversations():
117
- print("➡️ Monitoring for new WhatsApp conversations...")
118
- while True:
119
- try:
120
- conversations = client.conversations.v1.conversations.list(limit=20)
121
- for convo in conversations:
122
- convo_full = client.conversations.v1.conversations(convo.sid).fetch()
123
- if convo.sid not in processed_convos and convo_full.date_created > APP_START_TIME:
124
- participants = client.conversations.v1.conversations(convo.sid).participants.list()
125
- for p in participants:
126
- address = p.messaging_binding.get("address", "") if p.messaging_binding else ""
127
- if address.startswith("whatsapp:"):
128
- print(f"🆕 New WhatsApp convo found: {convo.sid}")
129
- processed_convos.add(convo.sid)
130
- threading.Thread(target=poll_conversation, args=(convo.sid,), daemon=True).start()
131
- except Exception as e:
132
- print("❌ Error polling conversations:", e)
133
- time.sleep(MONITOR_INTERVAL_SECONDS)
134
 
135
- threading.Thread(target=poll_new_conversations, daemon=True).start()
 
 
 
 
 
136
 
137
- # Main Streamlit UI
 
138
  def main():
139
- st.set_page_config(page_title="ToyShop Assistant", layout="wide")
140
- st.title("🧸 ToyShop Assistant WhatsApp Chatbot (RAG + Twilio)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
142
- if st.button("🚀 Start"):
143
- with st.spinner("Loading and processing documents..."):
144
- docs = load_and_process_documents(DOCS_DIR)
145
- if not docs:
146
- return
147
 
148
- with st.spinner("Creating vector store..."):
149
- vector_store = create_vector_store(docs, EMBEDDING_MODEL_NAME)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  if not vector_store:
151
- return
 
152
 
153
- with st.spinner("Initializing LLM..."):
154
- llm = get_llm(os.getenv("OPENAI_API_KEY"))
155
  if not llm:
156
- return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
- account_sid = os.getenv("TWILIO_ACCOUNT_SID")
159
- auth_token = os.getenv("TWILIO_AUTH_TOKEN")
160
- if not account_sid or not auth_token:
161
- st.error("Twilio credentials not found in environment variables.")
162
- return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
 
164
- client = Client(account_sid, auth_token)
165
 
166
- st.success("✅ Setup complete. Monitoring WhatsApp conversations...")
167
- start_conversation_monitor(client, vector_store, HuggingFaceEmbeddings(model_name=EMBEDDING_MODEL_NAME), docs)
168
- st.info(f"📡 Watching for messages every {MONITOR_INTERVAL_SECONDS} seconds...")
169
 
170
  if __name__ == "__main__":
171
  main()
 
 
1
+ import streamlit as st
2
  import os
3
+ import glob
4
+ from dotenv import load_dotenv
5
  import time
 
6
  import threading
7
+ from twilio.rest import Client # Import Twilio client
8
+ from langchain_community.document_loaders import (
9
+ PyPDFLoader,
10
+ Docx2txtLoader,
11
+ UnstructuredExcelLoader,
12
+ JSONLoader,
13
+ UnstructuredFileLoader # Generic loader, good for tables
14
+ )
15
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
16
  from langchain.embeddings import HuggingFaceEmbeddings
17
+ from langchain_community.vectorstores import FAISS
18
+ from langchain_groq import ChatGroq
19
+ from langchain.chains import RetrievalQA
20
+ from langchain.prompts import PromptTemplate
21
+ from langchain.schema.runnable import RunnablePassthrough
22
+ from langchain.schema.output_parser import StrOutputParser
23
 
24
+ # --- Configuration ---
25
+ # --- Moved groq_api_key here ---
26
+ load_dotenv()
27
+ groq_api_key = os.getenv("GROQ_API_KEY")
28
+ # groq_api_key = ""
29
  DOCS_DIR = "docs"
30
  EMBEDDING_MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2"
31
+ CACHE_DIR = ".streamlit_cache"
32
+ GENERAL_QA_PROMPT = """
33
+ You are an AI assistant for our internal knowledge base.
34
+ Your goal is to provide accurate and concise answers based ONLY on the provided context.
35
+ Do not make up information. If the answer is not found in the context, state that clearly.
36
+ Ensure your answers are directly supported by the text.
37
+ Accuracy is paramount.
38
+
39
+ Context:
40
+ {context}
41
+
42
+ Question: {question}
43
+
44
+ Answer:
45
+ """
46
+ ORDER_STATUS_PROMPT = """
47
+ You are an AI assistant helping with customer order inquiries.
48
+ Based ONLY on the following retrieved information from our order system and policies:
49
+ {context}
50
+
51
+ The customer's query is: {question}
52
+
53
+ Please perform the following steps:
54
+ 1. Carefully analyze the context for any order details (Order ID, Customer Name, Status, Items, Dates, etc.).
55
+ 2. If an order matching the query (or related to a name in the query) is found in the context:
56
+ - Address the customer by their name if available in the order details (e.g., "Hello [Customer Name],").
57
+ - Provide ALL available information about their order, including Order ID, status, items, dates, and any other relevant details found in the context.
58
+ - Be comprehensive and clear.
59
+ 3. If no specific order details are found in the context that match the query, politely state that you couldn't find the specific order information in the provided documents and suggest they contact support for further assistance.
60
+ 4. Do NOT invent or infer any information not explicitly present in the context.
61
+
62
+ Answer:
63
+ """
64
+ MONITOR_INTERVAL_SECONDS = 30 # Add the constant for the monitoring interval
65
+ APP_START_TIME = time.time()
66
+
67
+ # Twilio Configuration (Add your Twilio credentials here)
68
+ account_sid = os.getenv("TWILIO_ACCOUNT_SID")
69
+ auth_token = os.getenv("TWILIO_AUTH_TOKEN")
70
+ twilio_number = os.getenv("TWILIO_PHONE_NUMBER") # Your Twilio phone number
71
+
72
+
73
+ # Create docs and cache directory if they don't exist
74
+ if not os.path.exists(DOCS_DIR):
75
+ os.makedirs(DOCS_DIR)
76
+ if not os.path.exists(CACHE_DIR):
77
+ os.makedirs(CACHE_DIR)
78
+
79
+
80
+ # --- Helper Function for Document Loading ---
81
+ def get_loader(file_path):
82
+ """Detects file type and returns appropriate Langchain loader."""
83
+ _, ext = os.path.splitext(file_path)
84
+ ext = ext.lower()
85
+ # Prioritize UnstructuredFileLoader for robust table and content extraction
86
+ # UnstructuredFileLoader can handle many types, but we can specify if needed
87
+ if ext in ['.pdf', '.docx', '.doc', '.xlsx', '.xls', '.json', '.txt', '.md', '.html', '.xml', '.eml', '.msg']:
88
+ return UnstructuredFileLoader(file_path, mode="elements", strategy="fast") # "elements" is good for tables
89
+ # Fallback or specific loaders if UnstructuredFileLoader has issues with a particular file
90
+ # elif ext == ".pdf":
91
+ # return PyPDFLoader(file_path) # Basic PDF loader
92
+ # elif ext in [".docx", ".doc"]:
93
+ # return Docx2txtLoader(file_path) # Basic DOCX loader
94
+ # elif ext in [".xlsx", ".xls"]:
95
+ # return UnstructuredExcelLoader(file_path, mode="elements") # Unstructured for Excel
96
+ # elif ext == ".json":
97
+ # return JSONLoader(file_path, jq_schema='.[]', text_content=False) # Adjust jq_schema as needed
98
+ else:
99
+ st.warning(f"Unsupported file type: {ext}. Skipping {os.path.basename(file_path)}")
100
+ return None
101
+
102
+
103
+ # --- Caching Functions ---
104
+ @st.cache_resource(show_spinner=False) # Disable spinner during initial load
105
+ def load_and_process_documents(docs_path: str):
106
+ """
107
+ Loads documents from the specified path, processes them, and splits into chunks.
108
+ Uses UnstructuredFileLoader for potentially better table extraction.
109
+ """
110
+ documents = []
111
+ doc_files = []
112
+ for ext in ["*.pdf", "*.docx", "*.xlsx", "*.json", "*.txt", "*.md"]:
113
+ doc_files.extend(glob.glob(os.path.join(docs_path, ext)))
114
+
115
+ if not doc_files:
116
+ st.error(f"No documents found in the '{docs_path}' directory. Please add some documents.")
117
+ st.info("Supported formats: .pdf, .docx, .xlsx, .json, .txt, .md")
118
  return []
 
 
119
 
120
+ for file_path in doc_files:
121
+ try:
122
+ print(f"Processing: {os.path.basename(file_path)}...") # Show progress
123
+ loader = get_loader(file_path)
124
+ if loader:
125
+ loaded_docs = loader.load()
126
+ # Add source metadata to each document for better traceability
127
+ for doc in loaded_docs:
128
+ doc.metadata["source"] = os.path.basename(file_path)
129
+ documents.extend(loaded_docs)
130
+ except Exception as e:
131
+ st.error(f"Error loading {os.path.basename(file_path)}: {e}")
132
+ st.warning(f"Skipping file {os.path.basename(file_path)} due to error.")
133
+
134
+ if not documents:
135
+ st.error("No documents were successfully loaded or processed.")
136
+ return []
137
+
138
+ text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
139
+ chunked_documents = text_splitter.split_documents(documents)
140
+
141
+ if not chunked_documents:
142
+ st.error("Document processing resulted in no text chunks. Check document content and parsing.")
143
+ return []
144
+
145
+ st.success(f"Successfully loaded and processed {len(doc_files)} documents into {len(chunked_documents)} chunks.")
146
+ return chunked_documents
147
+
148
+
149
+ @st.cache_resource(show_spinner=False) # Disable spinner during initial load
150
+ def create_vector_store(_documents, _embedding_model_name: str):
151
+ """Creates a FAISS vector store from the given documents and embedding model."""
152
+ if not _documents:
153
+ st.warning("Cannot create vector store: No documents processed.")
154
+ return None
155
+ try:
156
+ embeddings = HuggingFaceEmbeddings(model_name=_embedding_model_name)
157
+ vector_store = FAISS.from_documents(_documents, embedding=embeddings)
158
+ st.success("Vector Store created successfully!")
159
+ return vector_store
160
+ except Exception as e:
161
+ st.error(f"Error creating vector store: {e}")
162
+ # Return an empty FAISS instance instead of None to prevent the AttributeError.
163
+ embeddings = HuggingFaceEmbeddings(model_name=_embedding_model_name) # Initialize embeddings
164
+ vector_store = FAISS.from_documents([], embeddings) # Changed from None to FAISS.from_documents
165
+ return vector_store
166
+
167
+
168
+ @st.cache_resource(show_spinner=False) # Disable spinner during initial load
169
+ def get_llm(api_key: str, model_name: str = "llama3-8b-8192"): # UPDATED MODEL
170
+ """Initializes the Groq LLM."""
171
+ if not api_key:
172
+ st.error("GROQ_API_KEY not found! Please set it in your environment variables or a .env file.")
173
+ return None
174
+ try:
175
+ # Available models (check Groq documentation for the latest):
176
+ # "llama3-8b-8192" (good balance of speed and capability)
177
+ # "llama3-70b-8192" (more powerful, potentially slower)
178
+ # "gemma-7b-it"
179
+ llm = ChatGroq(temperature=0, groq_api_key=api_key, model_name=model_name)
180
+ st.sidebar.info(f"LLM Initialized: {model_name}") # Add info about which model is used
181
+ return llm
182
+ except Exception as e:
183
+ st.error(f"Error initializing Groq LLM: {e}")
184
+ return None
185
+
186
+
187
+ # --- RAG Chain Setup ---
188
+ def get_rag_chain(llm, retriever, prompt_template):
189
+ """Creates the Retrieval QA chain."""
190
+ prompt = PromptTemplate.from_template(prompt_template)
191
+ rag_chain = (
192
+ {"context": retriever, "question": RunnablePassthrough()}
193
+ | prompt
194
+ | llm
195
+ | StrOutputParser()
196
+ )
197
+ return rag_chain
198
+
199
+
200
+ # --- Twilio Helper Functions ---
201
+ def send_twilio_message(client, conversation_sid, message):
202
+ """Sends a message to a Twilio Conversation."""
203
+ try:
204
+ client.conversations.v1.conversations(conversation_sid).messages.create(
205
+ author="Internal Knowledge Base AI", # Or some identifier
206
+ body=message,
207
+ )
208
+ except Exception as e:
209
+ print(f"❌ Error sending Twilio message: {e}")
210
 
 
 
 
211
 
212
  def fetch_latest_incoming_message(client, conversation_sid):
213
+ """Fetches the latest incoming message from a conversation."""
214
  try:
215
+ messages = client.conversations.v1.conversations(conversation_sid).messages.list(limit=1)
216
+ for msg in messages:
217
+ if msg.direction == "inbound":
218
  return {
219
+ "body": msg.body,
220
  "author": msg.author,
 
221
  "timestamp": msg.date_created
222
  }
223
+ return None # No incoming message found
224
  except Exception as e:
225
+ print(f"❌ Error fetching latest message: {e}")
226
+ return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
 
 
 
 
 
 
228
 
229
+ def poll_conversation(convo_sid, rag_chain, client):
230
+ """Polls a single conversation for new messages and responds."""
231
  last_processed_timestamp = {}
232
+ while True:
233
+ try:
234
+ latest_msg = fetch_latest_incoming_message(client, convo_sid)
235
+ if latest_msg:
236
+ msg_time = latest_msg["timestamp"]
237
+ if convo_sid not in last_processed_timestamp or msg_time > last_processed_timestamp[convo_sid]:
238
+ last_processed_timestamp[convo_sid] = msg_time
239
+ question = latest_msg["body"]
240
+ sender = latest_msg["author"]
241
+ print(f"\n📥 New message from {sender} in {convo_sid}: {question}")
242
+ # context = "\n\n".join(retrieve_chunks(question, index, embed_model, text_chunks))
243
+ # answer = generate_answer_with_groq(question, context)
244
+ try:
245
+ if "order" in question.lower() and (
246
+ "status" in question.lower() or "track" in question.lower() or "update" in question.lower() or any(
247
+ name_part.lower() in question.lower() for name_part in ["customer", "client", "name"])):
248
+ answer = rag_chain.invoke(question, config={'prompt': ORDER_STATUS_PROMPT})
249
+ else:
250
+ answer = rag_chain.invoke(question, config={'prompt': GENERAL_QA_PROMPT})
251
  send_twilio_message(client, convo_sid, answer)
252
  print(f"📤 Replied to {sender}: {answer}")
253
+ except Exception as e:
254
+ print(f"❌ Error during RAG chain invocation: {e}")
255
+ answer = "Sorry, I encountered an error while processing your request."
256
+ send_twilio_message(client, convo_sid, answer)
257
+ time.sleep(3) # Reduced polling interval
258
+ except Exception as e:
259
+ print(f"❌ Error in convo {convo_sid} polling:", e)
260
+ time.sleep(5)
261
+
262
+
263
+ def poll_new_conversations(client, vector_store, llm):
264
+ """Polls for new conversations and starts monitoring them."""
265
+ processed_convos = set()
266
+ print("➡️ Monitoring for new WhatsApp conversations...")
267
+ rag_chain = get_rag_chain(llm, vector_store.as_retriever(search_kwargs={"k": 5}), GENERAL_QA_PROMPT)
268
+ while True:
269
+ try:
270
+ conversations = client.conversations.v1.conversations.list(limit=20) # Adjust limit as needed
271
+ for convo in conversations:
272
+ convo_full = client.conversations.v1.conversations(convo.sid).fetch()
273
+ if convo.sid not in processed_convos and convo_full.date_created > APP_START_TIME:
274
+ participants = client.conversations.v1.conversations(convo.sid).participants.list()
275
+ for p in participants:
276
+ address = p.messaging_binding.get("address", "") if p.messaging_binding else ""
277
+ if address.startswith("whatsapp:"):
278
+ print(f"🆕 New WhatsApp convo found: {convo.sid}")
279
+ processed_convos.add(convo.sid)
280
+ threading.Thread(target=poll_conversation, args=(convo.sid, rag_chain, client),
281
+ daemon=True).start()
282
+ except Exception as e:
283
+ print("❌ Error polling conversations:", e)
284
+ time.sleep(MONITOR_INTERVAL_SECONDS) # Use the defined interval
285
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
 
287
+ def start_conversation_monitor(client, vector_store, llm):
288
+ """Starts the conversation monitoring process."""
289
+ # Launch the new conversation polling in a separate thread
290
+ threading.Thread(target=poll_new_conversations, args=(client, vector_store, llm),
291
+ daemon=True).start()
292
+ print("🟢 WhatsApp monitoring started.") # Add a message
293
 
294
+
295
+ # --- Main Application Logic ---
296
  def main():
297
+ # --- UI Setup ---
298
+ st.set_page_config(page_title="Internal Knowledge Base AI", layout="wide", initial_sidebar_state="expanded")
299
+
300
+ # Custom CSS (remains the same)
301
+ st.markdown("""
302
+ <style>
303
+ .reportview-container .main .block-container{
304
+ padding-top: 2rem;
305
+ padding-bottom: 2rem;
306
+ }
307
+ .st-emotion-cache-z5fcl4 {
308
+ padding-top: 1rem;
309
+ }
310
+ .response-area {
311
+ background-color: #f0f2f6;
312
+ padding: 15px;
313
+ border-radius: 5px;
314
+ margin-top: 10px;
315
+ }
316
+ </style>
317
+ """, unsafe_allow_html=True)
318
+
319
+ st.title("📚 Internal Knowledge Base AI 💡")
320
+
321
+ st.sidebar.header("System Status")
322
+ status_placeholder = st.sidebar.empty()
323
+ status_placeholder.info("Initializing...")
324
 
325
+ if not groq_api_key:
326
+ status_placeholder.error("GROQ API Key not configured. Application cannot start.")
327
+ st.stop()
 
 
328
 
329
+ # --- Initialize session state ---
330
+ if "app_initialized" not in st.session_state:
331
+ st.session_state.app_initialized = False
332
+
333
+ # --- Start Button ---
334
+ if not st.session_state.app_initialized:
335
+ if st.button("Start"): # Create a start button
336
+ st.session_state.app_initialized = True # set the session state to true
337
+ st.rerun() # Rerun the app to trigger the knowledge base loading
338
+
339
+ # --- Knowledge Base Loading and LLM Initialization ---
340
+ if st.session_state.app_initialized: # only run if the app has been initialized
341
+ with st.spinner("Knowledge Base is loading... Please wait."):
342
+ start_time = time.time()
343
+ processed_documents = load_and_process_documents(DOCS_DIR)
344
+ if not processed_documents:
345
+ status_placeholder.error("Failed to load or process documents. Check logs and `docs` folder.")
346
+ st.stop()
347
+
348
+ vector_store = create_vector_store(processed_documents, EMBEDDING_MODEL_NAME)
349
  if not vector_store:
350
+ status_placeholder.error("Failed to create vector store. Application cannot proceed.")
351
+ st.stop()
352
 
353
+ # Pass the selected model to get_llm
354
+ llm = get_llm(groq_api_key, model_name="llama3-8b-8192") # Hardcoded to use llama3-8b-8192
355
  if not llm:
356
+ # Error is already shown by get_llm, but update status_placeholder too
357
+ status_placeholder.error("Failed to initialize LLM. Application cannot proceed.")
358
+ st.stop()
359
+
360
+ end_time = time.time()
361
+ # status_placeholder is updated by get_llm or on success below
362
+ status_placeholder.success(f"Application Ready! (Loaded in {end_time - start_time:.2f}s)")
363
+
364
+ # --- Initialize Twilio Client and Start Monitoring ---
365
+ if account_sid and auth_token and twilio_number:
366
+ try:
367
+ client = Client(account_sid, auth_token)
368
+ # Start the conversation monitor in a separate thread
369
+ start_conversation_monitor(client, vector_store, llm)
370
+ st.success("🟢 Monitoring new WhatsApp conversations...")
371
+ st.info("⏳ Waiting for new messages...")
372
+ except Exception as e:
373
+ status_placeholder.error(f"Failed to initialize Twilio: {e}. Check your credentials and network.")
374
+ st.stop()
375
+ else:
376
+ st.warning("Twilio credentials not fully configured. WhatsApp monitoring is disabled.")
377
+
378
+ # --- Query Input and Response ---
379
+ st.markdown("---")
380
+ st.subheader("Ask a question about our documents:")
381
+
382
+ if "messages" not in st.session_state:
383
+ st.session_state.messages = []
384
 
385
+ query = st.text_input("Enter your question:", key="query_input",
386
+ placeholder="e.g., 'What is the return policy?' or 'Status of order for John Doe?'")
387
+
388
+ if st.button("Submit", key="submit_button"):
389
+ if query:
390
+ st.session_state.messages.append({"role": "user", "content": query})
391
+
392
+ current_model_info = st.sidebar.empty() # Placeholder for current mode info
393
+
394
+ if "order" in query.lower() and (
395
+ "status" in query.lower() or "track" in query.lower() or "update" in query.lower() or any(
396
+ name_part.lower() in query.lower() for name_part in ["customer", "client", "name"])):
397
+ active_prompt_template = ORDER_STATUS_PROMPT
398
+ current_model_info.info("Mode: Order Status Query")
399
+ else:
400
+ active_prompt_template = GENERAL_QA_PROMPT
401
+ current_model_info.info("Mode: General Query")
402
+
403
+ rag_chain = get_rag_chain(llm, vector_store.as_retriever(search_kwargs={"k": 5}), active_prompt_template)
404
+
405
+ with st.spinner("Thinking..."):
406
+ try:
407
+ response = rag_chain.invoke(query)
408
+ st.session_state.messages.append({"role": "assistant", "content": response})
409
+ except Exception as e:
410
+ st.error(f"Error during RAG chain invocation: {e}")
411
+ response = "Sorry, I encountered an error while processing your request."
412
+ st.session_state.messages.append({"role": "assistant", "content": response})
413
+ else:
414
+ st.warning("Please enter a question.")
415
+
416
+ st.markdown("---")
417
+ st.subheader("Response:")
418
+ response_area = st.container()
419
+ # Ensure response_area is robust against empty messages or incorrect last role
420
+ last_assistant_message = "Ask a question to see the answer here."
421
+ if st.session_state.messages and st.session_state.messages[-1]['role'] == 'assistant':
422
+ last_assistant_message = st.session_state.messages[-1]['content']
423
+
424
+ response_area.markdown(f"<div class='response-area'>{last_assistant_message}</div>",
425
+ unsafe_allow_html=True)
426
+
427
+ st.sidebar.markdown("---")
428
+ st.sidebar.markdown("Built with ❤️ using Streamlit & Langchain & Groq")
429
 
 
430
 
 
 
 
431
 
432
  if __name__ == "__main__":
433
  main()
434
+