Aniq-63 commited on
Commit
7b1350c
·
verified ·
1 Parent(s): c0b489e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +399 -832
app.py CHANGED
@@ -1,951 +1,518 @@
 
1
  import os
2
  import sqlite3
3
  import streamlit as st
4
  from werkzeug.security import generate_password_hash, check_password_hash
5
- import logging # Import logging
6
 
7
- # Langchain specific imports
8
  from langchain_groq import ChatGroq
9
  from langchain_huggingface import HuggingFaceEmbeddings
10
- from langchain_community.document_loaders.csv_loader import CSVLoader # Although not used directly for DB, keep if needed later
11
  from langchain_text_splitters import RecursiveCharacterTextSplitter
12
- from langchain_community.vectorstores import FAISS # Using FAISS for persistence/efficiency
13
- # Or use InMemoryVectorStore if preferred and persistence isn't critical:
14
- # from langchain_core.vectorstores import InMemoryVectorStore
15
- from langchain_core.vectorstores import VectorStore # Base class for type hinting
16
  from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
17
  from langchain.tools import Tool
18
  from langchain.agents import AgentExecutor, create_tool_calling_agent
19
  from langchain_core.messages import HumanMessage, AIMessage
20
  from langchain.docstore.document import Document
21
 
22
- # --- Logging Setup ---
23
- # Configure logging for better debugging, especially for agent actions
24
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
25
- logger = logging.getLogger(__name__)
26
-
27
  # --- Database Setup ---
28
- # Use a constant for the database file name
29
- DB_FILE = 'users_and_products.db'
30
-
31
- @st.cache_resource(show_spinner="Initializing Database...")
32
  def init_db():
33
- """Initializes the SQLite database and returns a connection."""
34
- try:
35
- conn = sqlite3.connect(DB_FILE, check_same_thread=False)
36
- c = conn.cursor()
37
-
38
- # Users table
39
- c.execute('''CREATE TABLE IF NOT EXISTS users
40
- (id INTEGER PRIMARY KEY AUTOINCREMENT,
41
- username TEXT UNIQUE NOT NULL,
42
- password TEXT NOT NULL,
43
- previous_chat_history TEXT,
44
- previous_products_bought TEXT)''')
45
-
46
- # Company settings table
47
- c.execute('''CREATE TABLE IF NOT EXISTS company_settings
48
- (id INTEGER PRIMARY KEY DEFAULT 1, -- Ensure only one row
49
- name TEXT NOT NULL,
50
- business TEXT NOT NULL,
51
- agent_name TEXT NOT NULL,
52
- key_features TEXT NOT NULL)''')
53
-
54
- # Products table with inventory
55
- c.execute('''CREATE TABLE IF NOT EXISTS products
56
- (id INTEGER PRIMARY KEY AUTOINCREMENT,
57
- name TEXT NOT NULL,
58
- category TEXT NOT NULL,
59
- price REAL NOT NULL,
60
- description TEXT NOT NULL,
61
- features TEXT NOT NULL,
62
- stock INTEGER NOT NULL DEFAULT 0 CHECK(stock >= 0))''') # Ensure stock >= 0
63
-
64
- # Check and update products schema if 'stock' column is missing (for backward compatibility)
65
- c.execute("PRAGMA table_info(products)")
66
- columns = [column[1] for column in c.fetchall()]
67
- if 'stock' not in columns:
68
- logger.warning("Adding missing 'stock' column to products table.")
69
- c.execute('ALTER TABLE products ADD COLUMN stock INTEGER NOT NULL DEFAULT 0 CHECK(stock >= 0)')
70
-
71
- # Insert default company settings if empty
72
- c.execute('SELECT COUNT(*) FROM company_settings')
73
- if c.fetchone()[0] == 0:
74
- logger.info("Inserting default company settings.")
75
- c.execute('''INSERT OR IGNORE INTO company_settings
76
- (id, name, business, agent_name, key_features)
77
- VALUES (?, ?, ?, ?, ?)''',
78
- (1, # Explicitly set ID to 1
79
- 'TechElectronics',
80
- 'Consumer Electronics Retailer',
81
- 'Alex',
82
- 'Cutting-edge technology, Competitive pricing, Excellent customer service'))
83
-
84
- conn.commit()
85
- logger.info("Database initialized successfully.")
86
- return conn
87
- except sqlite3.Error as e:
88
- logger.error(f"Database initialization failed: {e}")
89
- st.error(f"Fatal Error: Could not initialize the database. Please check logs. Error: {e}")
90
- st.stop() # Stop execution if DB fails
91
- # No need to return conn here as it's handled by cache_resource
92
-
93
- # Get a connection (will be cached after first call)
94
- # Note: It's generally better practice to open/close connections per transaction/request
95
- # in web apps, but for Streamlit's model, caching the connection object is common,
96
- # though requires `check_same_thread=False`. Be mindful of potential concurrency issues.
97
- # A safer approach might involve a connection pool or context managers within functions.
98
- # For simplicity in this example, we'll use the cached connection but wrap DB ops in try/finally.
99
-
100
- def get_db_conn():
101
- """Gets a connection to the database."""
102
- try:
103
- return sqlite3.connect(DB_FILE, check_same_thread=False)
104
- except sqlite3.Error as e:
105
- logger.error(f"Failed to connect to database: {e}")
106
- st.error(f"Error connecting to the database: {e}")
107
- return None
108
 
109
  # --- Admin Classes ---
110
 
111
  class Company:
112
  @staticmethod
113
  def get_settings():
114
- """Retrieves company settings from the database."""
115
- conn = get_db_conn()
116
- if not conn: return None
117
- try:
118
- c = conn.cursor()
119
- c.execute('SELECT name, business, agent_name, key_features FROM company_settings WHERE id = 1')
120
- settings = c.fetchone()
121
- # Return a default tuple if no settings found, preventing downstream errors
122
- return settings if settings else ('Default Company', 'Default Business', 'AI Assistant', 'Default Features')
123
- except sqlite3.Error as e:
124
- logger.error(f"Error getting company settings: {e}")
125
- return None # Indicate error
126
- finally:
127
- if conn: conn.close()
128
 
129
  @staticmethod
130
  def update_settings(name, business, agent_name, key_features):
131
- """Updates company settings in the database."""
132
- conn = get_db_conn()
133
- if not conn: return False
134
- try:
135
- c = conn.cursor()
136
- # Use INSERT OR REPLACE to handle the case where the row might not exist yet (though init_db should prevent this)
137
- c.execute('''INSERT OR REPLACE INTO company_settings
138
- (id, name, business, agent_name, key_features)
139
- VALUES (1, ?, ?, ?, ?)''',
140
- (name, business, agent_name, key_features))
141
- conn.commit()
142
- logger.info("Company settings updated.")
143
- return True
144
- except sqlite3.Error as e:
145
- logger.error(f"Error updating company settings: {e}")
146
- conn.rollback()
147
- return False
148
- finally:
149
- if conn: conn.close()
150
 
151
  class Product:
152
  @staticmethod
153
  def get_all():
154
- """Retrieves all products from the database."""
155
- conn = get_db_conn()
156
- if not conn: return []
157
- try:
158
- c = conn.cursor()
159
- c.execute('SELECT id, name, category, price, description, features, stock FROM products ORDER BY name')
160
- return c.fetchall()
161
- except sqlite3.Error as e:
162
- logger.error(f"Error getting all products: {e}")
163
- return [] # Return empty list on error
164
- finally:
165
- if conn: conn.close()
166
 
167
  @staticmethod
168
  def get_by_id(product_id):
169
- """Retrieves a single product by its ID."""
170
- conn = get_db_conn()
171
- if not conn: return None
172
- try:
173
- c = conn.cursor()
174
- c.execute('SELECT id, name, category, price, description, features, stock FROM products WHERE id = ?', (product_id,))
175
- return c.fetchone()
176
- except sqlite3.Error as e:
177
- logger.error(f"Error getting product by ID {product_id}: {e}")
178
- return None
179
- finally:
180
- if conn: conn.close()
181
 
182
  @staticmethod
183
  def add(name, category, price, description, features, stock):
184
- """Adds a new product to the database."""
185
- if not all([name, category, description, features]) or price < 0 or stock < 0:
186
- st.error("Invalid product data. Please check all fields.")
187
- return False
188
- conn = get_db_conn()
189
- if not conn: return False
190
- try:
191
- c = conn.cursor()
192
- c.execute('''INSERT INTO products
193
- (name, category, price, description, features, stock)
194
- VALUES (?, ?, ?, ?, ?, ?)''',
195
- (name, category, price, description, features, stock))
196
- conn.commit()
197
- logger.info(f"Product '{name}' added successfully.")
198
- # Clear cache after adding product
199
- load_vector_store.clear()
200
- return True
201
- except sqlite3.IntegrityError:
202
- logger.error(f"Product '{name}' might already exist or violates constraints.")
203
- st.error(f"Product '{name}' might already exist.")
204
- conn.rollback()
205
- return False
206
- except sqlite3.Error as e:
207
- logger.error(f"Error adding product '{name}': {e}")
208
- st.error(f"Database error adding product: {e}")
209
- conn.rollback()
210
- return False
211
- finally:
212
- if conn: conn.close()
213
 
214
  @staticmethod
215
  def delete(product_id):
216
- """Deletes a product from the database by ID."""
217
- conn = get_db_conn()
218
- if not conn: return False
219
- try:
220
- c = conn.cursor()
221
- c.execute('DELETE FROM products WHERE id=?', (product_id,))
222
- conn.commit()
223
- logger.info(f"Product ID {product_id} deleted.")
224
- # Clear cache after deleting product
225
- load_vector_store.clear()
226
- return True
227
- except sqlite3.Error as e:
228
- logger.error(f"Error deleting product ID {product_id}: {e}")
229
- conn.rollback()
230
- return False
231
- finally:
232
- if conn: conn.close()
233
 
234
  @staticmethod
235
  def update_stock(product_id, new_stock):
236
- """Updates the stock level for a specific product."""
237
- if new_stock < 0:
238
- logger.warning(f"Attempted to set negative stock ({new_stock}) for product ID {product_id}.")
239
- st.warning("Stock cannot be negative.")
240
- return False
241
- conn = get_db_conn()
242
- if not conn: return False
243
- try:
244
- c = conn.cursor()
245
- c.execute('UPDATE products SET stock=? WHERE id=?', (new_stock, product_id))
246
- conn.commit()
247
- logger.debug(f"Stock for product ID {product_id} updated to {new_stock}.")
248
- # Clear cache after updating stock
249
- load_vector_store.clear()
250
- return True
251
- except sqlite3.Error as e:
252
- logger.error(f"Error updating stock for product ID {product_id}: {e}")
253
- conn.rollback()
254
- return False
255
- finally:
256
- if conn: conn.close()
257
 
258
  # --- User Class ---
259
  class User:
260
- def __init__(self, id, username, password_hash, chat_history_str=None, products_bought_str=None):
261
  self.id = id
262
  self.username = username
263
- self.password_hash = password_hash # Store the hash directly
264
-
265
- # Safely evaluate stored string representations of lists
266
- try:
267
- self.chat_history = eval(chat_history_str) if chat_history_str else []
268
- if not isinstance(self.chat_history, list): self.chat_history = []
269
- except Exception as e:
270
- logger.warning(f"Error parsing chat history for user {username}: {e}. Resetting to empty list.")
271
- self.chat_history = []
272
-
273
- try:
274
- self.products_bought = eval(products_bought_str) if products_bought_str else []
275
- if not isinstance(self.products_bought, list): self.products_bought = []
276
- except Exception as e:
277
- logger.warning(f"Error parsing products bought for user {username}: {e}. Resetting to empty list.")
278
- self.products_bought = []
279
-
280
 
281
  @classmethod
282
  def create(cls, username, password):
283
- """Creates a new user, hashes the password, and saves to DB."""
284
- if not username or not password:
285
- st.error("Username and password cannot be empty.")
286
- return None
287
  hashed_pw = generate_password_hash(password)
288
- conn = get_db_conn()
289
- if not conn: return None
290
- try:
291
- c = conn.cursor()
292
- c.execute('INSERT INTO users (username, password, previous_chat_history, previous_products_bought) VALUES (?, ?, ?, ?)',
293
- (username, hashed_pw, '[]', '[]')) # Initialize with empty lists as strings
294
- user_id = c.lastrowid
295
- conn.commit()
296
- logger.info(f"User '{username}' created successfully with ID {user_id}.")
297
- return cls(user_id, username, hashed_pw) # Return new User instance
298
- except sqlite3.IntegrityError:
299
- logger.warning(f"Attempted to create user with existing username: {username}")
300
- st.error("Username already exists. Please choose a different one.")
301
- conn.rollback()
302
- return None
303
- except sqlite3.Error as e:
304
- logger.error(f"Database error creating user '{username}': {e}")
305
- st.error(f"Database error during registration: {e}")
306
- conn.rollback()
307
- return None
308
- finally:
309
- if conn: conn.close()
310
 
311
  @classmethod
312
  def get_by_username(cls, username):
313
- """Retrieves a user by username from the database."""
314
- conn = get_db_conn()
315
- if not conn: return None
316
- try:
317
- c = conn.cursor()
318
- c.execute('SELECT id, username, password, previous_chat_history, previous_products_bought FROM users WHERE username = ?', (username,))
319
- user_data = c.fetchone()
320
- if user_data:
321
- # Pass the raw string data to the constructor for parsing
322
- return cls(user_data[0], user_data[1], user_data[2], user_data[3], user_data[4])
323
- else:
324
- return None # User not found
325
- except sqlite3.Error as e:
326
- logger.error(f"Database error retrieving user '{username}': {e}")
327
- return None
328
- finally:
329
- if conn: conn.close()
330
-
331
- def _update_field(self, field_name, updated_value_list):
332
- """Helper method to update a list field (chat history or products) in the DB."""
333
- conn = get_db_conn()
334
- if not conn: return False
335
- try:
336
- c = conn.cursor()
337
- # Store the list as its string representation
338
- c.execute(f'UPDATE users SET {field_name} = ? WHERE id = ?',
339
- (str(updated_value_list), self.id))
340
- conn.commit()
341
- logger.debug(f"User {self.id}'s {field_name} updated in DB.")
342
- return True
343
- except sqlite3.Error as e:
344
- logger.error(f"Database error updating {field_name} for user {self.id}: {e}")
345
- conn.rollback()
346
- return False
347
- finally:
348
- if conn: conn.close()
349
 
350
  def update_chat_history(self, new_messages):
351
- """Appends new messages to the user's chat history and updates the DB."""
352
- # Ensure new_messages is a list
353
- if not isinstance(new_messages, list):
354
- logger.error("Invalid format for new_messages. Expected a list.")
355
- return
356
-
357
- # Append new messages to the existing list
358
  updated_history = self.chat_history + new_messages
359
- if self._update_field('previous_chat_history', updated_history):
360
- self.chat_history = updated_history # Update in-memory object only on successful DB update
361
-
362
- def update_products_bought(self, new_product_name):
363
- """Adds a new product name to the user's bought list and updates the DB."""
364
- # Ensure new_product_name is a string
365
- if not isinstance(new_product_name, str):
366
- logger.error("Invalid format for new_product_name. Expected a string.")
367
- return
368
 
369
- updated_products = self.products_bought + [new_product_name] # Append the new product name
370
- if self._update_field('previous_products_bought', updated_products):
371
- self.products_bought = updated_products # Update in-memory object
 
 
 
 
 
 
372
 
373
  # --- AI Setup ---
374
- # Load API Key from secrets
375
- try:
376
- GROQ_API_KEY = st.secrets["GROQ_API_KEY"]
377
- ADMIN_PIN = st.secrets["ADMIN_PIN"]
378
- os.environ["GROQ_API_KEY"] = GROQ_API_KEY
379
- except KeyError as e:
380
- st.error(f"Missing secret: {e}. Please configure GROQ_API_KEY and ADMIN_PIN in .streamlit/secrets.toml")
381
- st.stop()
382
-
383
- # Initialize LLM
384
- # Consider adding error handling for LLM initialization
385
- try:
386
- llm = ChatGroq(
387
- temperature=0.1, # Low temperature for more factual responses
388
- model_name="llama3-8b-8192", # Or other suitable model
389
- # api_key=GROQ_API_KEY, # Handled by environment variable
390
- )
391
- except Exception as e:
392
- st.error(f"Failed to initialize the Language Model: {e}")
393
- logger.error(f"LLM Initialization failed: {e}")
394
- st.stop()
395
-
396
-
397
- # Initialize Embeddings Model
398
- # Using a smaller, faster model suitable for in-memory vector stores
399
- try:
400
- embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2",
401
- model_kwargs={'device': 'cpu'}) # Explicitly use CPU if GPU issues arise
402
- except Exception as e:
403
- st.error(f"Failed to initialize the Embeddings Model: {e}")
404
- logger.error(f"Embeddings Initialization failed: {e}")
405
- st.stop()
406
-
407
- # --- Vector Store and Retriever ---
408
- # Path for FAISS index persistence
409
- FAISS_INDEX_PATH = "faiss_product_index"
410
-
411
- @st.cache_resource(show_spinner="Loading Product Knowledge...")
412
- def load_vector_store() -> VectorStore:
413
- """Loads products from DB, creates documents, splits them, and builds/loads a FAISS vector store."""
414
- logger.info("Attempting to load or build vector store.")
415
- products = Product.get_all()
416
- if not products:
417
- logger.warning("No products found in database to build vector store.")
418
- # Return an empty store or handle appropriately
419
- # For simplicity, creating an empty one here, but might need better handling
420
- return FAISS.from_texts(["No products available"], embeddings) # Create dummy store
421
 
 
 
 
 
 
422
  docs = []
423
  for p in products:
424
- # Create detailed content string for each product
425
- content = (f"Product Name: {p[1]}\n"
426
- f"Category: {p[2]}\n"
427
- f"Price: ${p[3]:.2f}\n" # Format price
428
- f"Stock: {p[6]} units available\n" # Crucial: Include current stock
429
- f"Description: {p[4]}\n"
430
- f"Features: {p[5]}")
431
- # Metadata includes ID, name, category, price, and stock for potential filtering/retrieval checks
432
  metadata = {"id": p[0], "name": p[1], "category": p[2], "price": p[3], "stock": p[6]}
433
  docs.append(Document(page_content=content, metadata=metadata))
 
 
 
 
 
434
 
435
- if not docs:
436
- logger.warning("No documents created from products.")
437
- return FAISS.from_texts(["No products available"], embeddings)
438
 
439
- # Split documents into smaller chunks
440
- text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50) # Adjusted chunk size
441
- splits = text_splitter.split_documents(docs)
 
442
 
443
- # Create or load FAISS index
444
- # Note: FAISS persistence is basic. For production, consider more robust vector DBs.
445
- if os.path.exists(FAISS_INDEX_PATH):
446
- try:
447
- logger.info(f"Loading existing FAISS index from {FAISS_INDEX_PATH}")
448
- vectorstore = FAISS.load_local(FAISS_INDEX_PATH, embeddings, allow_dangerous_deserialization=True) # Required for FAISS loading
449
- # Optional: Update the index if products changed significantly?
450
- # vectorstore.add_documents(splits) # Could lead to duplicates if not managed carefully
451
- logger.info("FAISS index loaded.")
452
- except Exception as e:
453
- logger.error(f"Error loading FAISS index: {e}. Rebuilding index.")
454
- vectorstore = FAISS.from_documents(documents=splits, embedding=embeddings)
455
- vectorstore.save_local(FAISS_INDEX_PATH)
456
- logger.info("New FAISS index built and saved.")
457
- else:
458
- logger.info("Building new FAISS index.")
459
- vectorstore = FAISS.from_documents(documents=splits, embedding=embeddings)
460
- vectorstore.save_local(FAISS_INDEX_PATH)
461
- logger.info(f"New FAISS index built and saved to {FAISS_INDEX_PATH}.")
462
-
463
- return vectorstore
464
-
465
- # Load the vector store (cached)
466
- vector_store = load_vector_store()
467
- retriever = vector_store.as_retriever(search_kwargs={"k": 5}) # Retrieve top 5 relevant chunks
468
-
469
- # --- Tool Definition ---
470
- def retrieve_query(query: str) -> list[Document]:
471
- """Retrieves relevant product documents based on the query using the vector store."""
472
- logger.info(f"Retrieving documents for query: '{query}'")
473
- try:
474
- # Use the globally loaded retriever
475
- docs = retriever.invoke(query)
476
- # Log retrieved docs for debugging (optional)
477
- # logger.debug(f"Retrieved {len(docs)} documents: {[doc.metadata for doc in docs]}")
478
- # Store last retrieved docs in session state IF NEEDED for purchase logic (alternative to re-querying)
479
- # st.session_state.last_retrieved_docs = docs
480
- return docs
481
- except Exception as e:
482
- logger.error(f"Error during document retrieval for query '{query}': {e}")
483
- return [] # Return empty list on error
484
-
485
- # Create the Langchain Tool
486
- product_retriever_tool = Tool(
487
  name="product_retriever",
488
  func=retrieve_query,
489
- description="Mandatory tool to get CURRENT product information. Use it to find products by name or category, check features, price, and MOST IMPORTANTLY, current stock levels BEFORE recommending or confirming anything to the customer."
490
  )
491
 
492
  # --- Admin Dashboard ---
493
  def admin_dashboard():
494
- """Renders the admin dashboard for managing settings and products."""
495
  st.header("Admin Dashboard")
496
-
497
- # Logout Button
498
- if st.sidebar.button("Exit Admin Mode"):
499
- st.session_state.admin_mode = False
500
- st.rerun()
501
-
502
- # Company Settings Management
503
- with st.expander("Company Settings", expanded=True):
504
  current_settings = Company.get_settings()
505
- if current_settings is None:
506
- st.error("Could not load company settings.")
507
- return # Stop rendering if settings failed to load
508
-
509
  with st.form("Company Settings Form"):
510
- # Use values from fetched settings or defaults if None
511
- name = st.text_input("Company Name", value=current_settings[0])
512
- business = st.text_input("Business", value=current_settings[1])
513
- agent_name = st.text_input("Agent Name", value=current_settings[2])
514
- key_features = st.text_area("Key Features", value=current_settings[3])
515
-
516
- submitted = st.form_submit_button("Update Settings")
517
- if submitted:
518
- if Company.update_settings(name, business, agent_name, key_features):
519
- st.success("Settings updated successfully!")
520
- # No need to clear cache here unless settings affect prompts drastically
521
- else:
522
- st.error("Failed to update settings.")
523
-
524
- # Product Management
525
- with st.expander("Product Management", expanded=True):
526
- # Add New Product Form
527
- with st.form("Add Product Form"):
528
  st.subheader("Add New Product")
529
- p_name = st.text_input("Product Name*")
530
- p_category = st.text_input("Category*")
531
- p_price = st.number_input("Price*", min_value=0.0, format="%.2f")
532
- p_description = st.text_area("Description*")
533
- p_features = st.text_area("Features*")
534
- p_stock = st.number_input("Initial Stock*", min_value=0, value=0, step=1)
535
-
536
- add_submitted = st.form_submit_button("Add Product")
537
- if add_submitted:
538
- if not all([p_name, p_category, p_description, p_features]):
539
- st.error("Please fill in all required fields marked with *.")
540
- elif Product.add(p_name, p_category, p_price, p_description, p_features, p_stock):
541
- st.success(f"Product '{p_name}' added!")
542
- # load_vector_store.clear() # Cache is cleared inside Product.add
543
- st.rerun() # Rerun to refresh the product list below
544
- else:
545
- # Error message displayed within Product.add
546
- pass
547
-
548
- # Manage Existing Products / Inventory
549
  st.subheader("Manage Inventory")
550
  products = Product.get_all()
551
- if not products:
552
- st.info("No products found in the database. Add products using the form above.")
553
- else:
554
- st.markdown("""---""")
555
  for p in products:
556
- # Use columns for better layout
557
- col1, col2, col3, col4 = st.columns([3, 2, 2, 1]) # Adjust ratios as needed
558
- with col1:
559
- st.write(f"**{p[1]}**") # Name
560
- st.caption(f"Category: {p[2]}") # Category
561
- with col2:
562
- st.write(f"Price: ${p[3]:.2f}") # Price
563
- with col3:
564
- # Use a unique key for each number input
565
- new_stock = st.number_input(
566
- "Stock",
567
- min_value=0,
568
- value=p[6], # Current stock (index 6)
569
- key=f"stock_{p[0]}", # Unique key using product ID (index 0)
570
- step=1,
571
- label_visibility="collapsed" # Hide label for cleaner look
572
- )
573
- # Update stock if value changes
574
- if new_stock != p[6]:
575
- if Product.update_stock(p[0], new_stock):
576
- st.toast(f"Stock for '{p[1]}' updated to {new_stock}.")
577
- # load_vector_store.clear() # Cache cleared inside update_stock
578
- st.rerun() # Rerun to reflect change immediately
579
- else:
580
- st.toast(f"Failed to update stock for '{p[1]}'.", icon="❌")
581
-
582
-
583
- with col4:
584
- # Use a unique key for each delete button
585
- if st.button("❌ Delete", key=f"del_{p[0]}", help=f"Delete {p[1]}"):
586
- if Product.delete(p[0]):
587
- st.success(f"Product '{p[1]}' deleted.")
588
- # load_vector_store.clear() # Cache cleared inside delete
589
- st.rerun() # Rerun to refresh list
590
- else:
591
- st.error(f"Failed to delete product '{p[1]}'.")
592
- st.markdown("""---""") # Separator
593
 
594
- # --- Main App Logic ---
595
  def main():
596
- """Main function to run the Streamlit application."""
597
- # Initialize Database (cached)
598
- init_db()
599
 
600
- # Initialize session state variables if they don't exist
 
601
  if 'user' not in st.session_state:
602
- st.session_state.user = None # Stores the logged-in User object
603
- if 'chat_history' not in st.session_state:
604
- # This will be populated from the user object upon login
605
- # It primarily serves as the display buffer for the current session UI
606
  st.session_state.chat_history = []
607
- if 'admin_mode' not in st.session_state:
608
  st.session_state.admin_mode = False
609
- # 'last_retrieved_docs' could be used but re-querying before purchase is safer
610
- # if 'last_retrieved_docs' not in st.session_state:
611
- # st.session_state.last_retrieved_docs = []
612
-
613
- # --- Page Title ---
614
- # Fetch company name for title (handle potential None)
615
- company_settings = Company.get_settings()
616
- company_name = company_settings[0] if company_settings else "Sales Assistant"
617
- st.title(f"{company_name} AI Sales Assistant 🤖")
618
-
619
-
620
- # --- Authentication / Mode Handling ---
621
- if st.session_state.admin_mode:
622
- admin_dashboard() # Show admin view
623
- elif not st.session_state.user:
624
- # Show Login/Register/Admin Access view
625
- st.header("Login / Register / Admin Access")
626
- login_tab, register_tab, admin_tab = st.tabs(["Login", "Register", "Admin Login"])
627
-
628
- with login_tab:
629
- with st.form("Login Form"):
630
  username = st.text_input("Username")
631
  password = st.text_input("Password", type="password")
632
- login_submitted = st.form_submit_button("Login")
633
- if login_submitted:
634
  user = User.get_by_username(username)
635
- if user and check_password_hash(user.password_hash, password):
636
  st.session_state.user = user
637
- # Load user's history into session state for display
638
  st.session_state.chat_history = user.chat_history
639
- logger.info(f"User '{username}' logged in.")
640
  st.rerun()
641
  else:
642
- st.error("Invalid username or password.")
643
-
644
- with register_tab:
645
- with st.form("Register Form"):
646
- new_username = st.text_input("Choose Username")
647
- new_password = st.text_input("Choose Password", type="password")
648
- register_submitted = st.form_submit_button("Register")
649
- if register_submitted:
650
- user = User.create(new_username, new_password)
651
- if user:
 
652
  st.session_state.user = user
653
- st.session_state.chat_history = [] # Start with empty history for new user
654
- logger.info(f"User '{new_username}' registered and logged in.")
655
- st.success("Registration successful! You are now logged in.")
656
  st.rerun()
657
- # Error messages handled within User.create
658
-
659
- with admin_tab:
660
- with st.form("Admin Login Form"):
661
- admin_pin_input = st.text_input("Admin PIN", type="password")
662
- admin_login_submitted = st.form_submit_button("Admin Login")
663
- if admin_login_submitted:
664
- # Compare with the PIN loaded from secrets
665
- if admin_pin_input == ADMIN_PIN:
666
  st.session_state.admin_mode = True
667
- logger.info("Admin mode activated.")
668
  st.rerun()
669
  else:
670
- st.error("Invalid Admin PIN.")
671
- else:
672
- # --- Logged-in User Chat Interface ---
673
- user: User = st.session_state.user # Get the logged-in user object
674
- company_settings = Company.get_settings() # Fetch fresh settings
675
- if not company_settings:
676
- st.error("Critical Error: Could not load company settings for the chat.")
677
- st.stop()
678
-
679
- company_name = company_settings[0]
680
- agent_name = company_settings[2]
681
-
682
- st.sidebar.header(f"Welcome, {user.username}!")
683
- if st.sidebar.button("Logout"):
684
- logger.info(f"User '{user.username}' logged out.")
685
- # Optionally save final chat history before logging out if needed
686
- # user.update_chat_history([]) # Example: Save current session history (already done per message)
687
- st.session_state.user = None
688
- st.session_state.chat_history = []
689
  st.session_state.admin_mode = False
690
  st.rerun()
691
 
692
- st.sidebar.markdown("---")
693
- st.sidebar.subheader("Purchase History")
694
- if user.products_bought:
695
- for item in user.products_bought:
696
- st.sidebar.markdown(f"- {item}")
697
- else:
698
- st.sidebar.info("No purchases recorded yet.")
699
-
700
-
701
- st.subheader(f"Chat with {agent_name}")
702
 
703
- # Display Chat History from session state
704
  for msg in st.session_state.chat_history:
705
- role = "user" if msg["type"] == "human" else "assistant"
706
- with st.chat_message(role):
707
- st.write(msg["content"])
 
 
 
708
 
709
- # --- Define System Prompt (Using the improved version) ---
 
 
710
  system_prompt = f"""
711
- **Your Identity:**
712
- You are {company_settings[2]}, a friendly and highly professional AI Sales Assistant for {company_settings[0]}.
713
- - **Company:** {company_settings[0]}
714
- - **Business:** {company_settings[1]} (Consumer Electronics Retailer)
715
- - **Key Strengths:** {company_settings[3]}
716
-
717
- **Your Primary Goal:**
718
- Assist customers effectively by understanding their needs, providing accurate product information based *only* on available inventory, guiding them through the sales process, and ensuring a positive customer experience.
719
-
720
- **Core Operating Principles:**
721
-
722
- 1. **Tool Reliance (Mandatory):**
723
- - You MUST use the `product_retriever` tool to get **current and accurate** information about products, including name, description, features, price, and **especially stock levels**.
724
- - **When to use the tool:**
725
- - When asked about specific products or product categories.
726
- - Before recommending ANY product to confirm its availability (stock > 0).
727
- - Before confirming the price or stock level of a product.
728
- - Before initiating the purchase process for a specific item.
729
- - If the tool returns no relevant products for a query, state that you don't have information on that specific item or category currently available.
730
- - If the tool fails unexpectedly, apologize and mention a temporary inability to fetch product details.
731
-
732
- 2. **Inventory-Driven Recommendations:**
733
- - **Only recommend products confirmed to be IN STOCK (stock > 0) via the `product_retriever` tool.**
734
- - **Never** suggest, discuss, or confirm availability for products with stock <= 0.
735
- - **Never** discuss or recommend products not part of {company_settings[0]}'s inventory (i.e., not returnable by the tool).
736
-
737
- 3. **Stock Level Communication:**
738
- - If a retrieved product has low stock (e.g., stock <= 3 BUT > 0), inform the customer using the "Low stock items" template.
739
- - If a customer asks about a product that the tool confirms is out of stock (stock = 0), use the "Out-of-stock items" template and suggest relevant, *in-stock* alternatives retrieved via the tool.
740
-
741
- 4. **User Context Awareness (Optional but Recommended):**
742
- - You have access to the user's chat history. Use it to understand the context of the current conversation.
743
- - The user object may contain `previous_products_bought`. If available ({user.products_bought}), you *can* use this information to tailor recommendations (e.g., accessories for past purchases), but **always prioritize current stock availability** confirmed via the tool.
744
-
745
- **Sales Conversation Flow:**
746
-
747
- 1. **Greeting & Needs Assessment:** Start with a warm, professional welcome. Ask open-ended questions to understand the customer's requirements, budget, and preferences. Actively listen.
748
- 2. **Product Consultation & Recommendation:**
749
- - Based on needs, query the `product_retriever` tool.
750
- - Recommend **only suitable, in-stock** products.
751
- - Clearly state product name, key features/benefits, and **tool-retrieved price and stock status**.
752
- - If comparing products, use tool-retrieved data for both.
753
- 3. **Handling Questions & Objections:** Address customer queries and concerns politely and factually, using information obtained from the `product_retriever` tool where applicable. If a requested product is unavailable or unsuitable, explain why and suggest appropriate, *in-stock* alternatives.
754
- 4. **Purchase Process Guidance:**
755
- - When a customer expresses clear intent to buy a *specific* product:
756
- a. **Re-verify stock** using the `product_retriever` tool one last time. Crucially check the **stock** field in the retrieved document metadata.
757
- b. If retrieved stock > 0, confirm the product name and **tool-retrieved price**.
758
- c. Ask if they wish to proceed using the "For payment process" template.
759
- d. If they agree, provide the pre-defined payment link: `https://www.example.com/payment`
760
- e. **Crucially state:** "Please note: Stock is confirmed now, and inventory will be updated once you complete the process via the link."
761
- - If retrieved stock is 0 upon re-verification, apologize and inform them it just went out of stock, suggesting alternatives if possible.
762
-
763
- **Communication Style:**
764
- - Tone: Professional, friendly, helpful, empathetic, patient.
765
- - Language: Clear, concise, easy-to-understand. Avoid jargon unless explained.
766
- - Branding: Consistently represent {company_settings[0]}.
767
- - Formatting: Use bullet points or short paragraphs for readability.
768
- - Emojis: Sparingly (max 1-2 per message) and appropriately. 😊
769
-
770
- **Response Templates (Adapt as needed):**
771
-
772
- * **Out-of-stock items:** "I apologize, but the [product name retrieved by tool] is currently out of stock (Tool shows 0 units). However, based on your needs, I found the [alternative in-stock product name retrieved by tool] which is available (Tool shows [stock] units) and has similar features like [mention 1-2 key features]. Would you like to know more about it?"
773
- * **Low stock items:** "Just letting you know, we have limited availability for the [product name retrieved by tool]. The tool shows only [X retrieved stock] units left in stock. I'd recommend deciding soon if you're interested!"
774
- * **Product not carried/found:** "I checked our inventory using the product tool, but I couldn't find [queried item] or anything similar in the [category if specified]. As a {company_settings[1]}, we specialize in [mention core categories]. Perhaps I can help you find something else from our available electronics?"
775
- * **For payment process:** "Great choice! I've just double-checked with the tool, and the [product name retrieved by tool] is currently in stock. The price is $[price retrieved by tool]. Are you ready to proceed with the purchase? I can provide the secure payment link."
776
- * **After providing payment link:** "Okay, here is the secure link to complete your purchase for the [product name]: https://www.example.com/payment. Please note: Stock is confirmed now, and inventory will be updated once you complete the process via the link."
777
-
778
- **Key Constraints - Adhere Strictly:**
779
- - **ALWAYS use the `product_retriever` tool for product details/stock BEFORE presenting information to the user.** Do not rely on memory or previous turns for stock levels. Check the 'stock' value in the retrieved document metadata.
780
- - **Stick to the product inventory of {company_settings[0]}.** Do not discuss competitors or unavailable items unless suggesting in-stock alternatives.
781
- - **Follow the purchase process precisely, especially the final stock check using the tool before offering the payment link.**
782
- - **Prioritize accuracy and helpfulness.**
 
 
 
783
  """
784
 
785
- # --- Agent Setup ---
786
- prompt_template = ChatPromptTemplate.from_messages([
787
  ("system", system_prompt),
788
  MessagesPlaceholder(variable_name="chat_history"),
789
  ("human", "{input}"),
790
- MessagesPlaceholder(variable_name="agent_scratchpad") # For tool usage intermediate steps
791
  ])
792
 
793
- tools = [product_retriever_tool]
794
- agent = create_tool_calling_agent(llm, tools, prompt_template)
795
- agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True, handle_parsing_errors=True) # Add verbose and error handling
796
-
797
-
798
- # --- Handle Chat Input ---
799
- if user_input := st.chat_input(f"Ask {agent_name} about our products..."):
800
- # Add user message to session state chat history (for UI display)
801
- st.session_state.chat_history.append({"type": "human", "content": user_input})
802
 
803
- # Display user message in chat message container
804
  with st.chat_message("user"):
805
- st.write(user_input)
806
-
807
- # Prepare chat history for the agent (Langchain format)
808
- formatted_history = []
809
- # Use the user object's history which is the ground truth from DB
810
- for msg in user.chat_history:
811
- if msg["type"] == "human":
812
- formatted_history.append(HumanMessage(content=msg["content"]))
813
- elif msg["type"] == "ai":
814
- formatted_history.append(AIMessage(content=msg["content"]))
815
- # else: ignore potentially malformed history entries
816
-
817
- # Display AI response in chat message container
818
  with st.chat_message("assistant"):
819
- response_container = st.empty() # Placeholder for potential streaming later
820
- ai_response_content = "" # Initialize response content
821
-
822
- try:
823
- with st.spinner(f"{agent_name} is thinking..."):
824
- # Invoke the agent
825
- response = agent_executor.invoke({
826
- "input": user_input,
827
- "chat_history": formatted_history
828
- # agent_scratchpad is handled internally by the executor
829
- })
830
- ai_response_content = response.get("output", "Sorry, I couldn't generate a response.")
831
- response_container.write(ai_response_content)
832
-
833
- except Exception as e:
834
- logger.error(f"Error invoking agent executor: {e}", exc_info=True) # Log stack trace
835
- ai_response_content = f"I apologize, I encountered an technical issue processing your request. Please try again shortly. (Error: {e})"
836
- response_container.error(ai_response_content) # Show error in the chat
837
-
838
- # Add AI response to session state chat history (for UI display)
839
- st.session_state.chat_history.append({"type": "ai", "content": ai_response_content})
840
-
841
- # Update user's persistent chat history in DB using the User object method
842
- # We pass the latest user message and the AI response
843
- new_messages_for_db = [
844
- {"type": "human", "content": user_input},
845
- {"type": "ai", "content": ai_response_content}
846
- ]
847
- user.update_chat_history(new_messages_for_db)
848
-
849
-
850
- # --- Purchase Handling Logic ---
851
- # Check if the AI response contains the payment link trigger phrase
852
- payment_link = "https://www.example.com/payment"
853
- if payment_link in ai_response_content:
854
- logger.info(f"Payment link detected in response for user {user.username}.")
855
- # Attempt to extract the product name reliably
856
- # Look for patterns like "purchase for the [product name]:" or similar
857
- product_name = None
858
- try:
859
- # Try different patterns to extract the name
860
- patterns = [
861
- r"purchase for the (.*?):",
862
- r"purchase of (.*?)\?",
863
- r"interested in (.*?)\." # Less reliable, add more if needed
864
- ]
865
- import re
866
- for pattern in patterns:
867
- match = re.search(pattern, ai_response_content, re.IGNORECASE)
868
- if match:
869
- product_name = match.group(1).strip().rstrip('.') # Clean up extracted name
870
- logger.info(f"Extracted product name '{product_name}' for purchase.")
871
- break # Stop after first match
872
- if not product_name:
873
- logger.warning("Payment link found, but failed to extract product name from response.")
874
- st.warning("Purchase link mentioned, but couldn't confirm the specific product name from the chat context. Stock not updated.")
875
-
876
- except Exception as extraction_error:
877
- logger.error(f"Error extracting product name: {extraction_error}")
878
- st.warning("Error processing product name for purchase. Stock not updated.")
879
  product_name = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
880
 
 
 
 
 
 
 
881
 
882
- # If product name was extracted, proceed with stock update simulation
883
- if product_name:
884
- conn = None # Ensure conn is defined for finally block
885
- try:
886
- conn = get_db_conn()
887
- if not conn: raise sqlite3.Error("Failed to get DB connection for purchase.")
888
- c = conn.cursor()
889
-
890
- # **Crucial Final Check:** Get current stock directly from DB
891
- c.execute('SELECT id, stock FROM products WHERE LOWER(name) = LOWER(?)', (product_name,))
892
- product_data = c.fetchone()
893
-
894
- if product_data and product_data[1] > 0: # Check if product exists and stock > 0
895
- product_id = product_data[0]
896
- current_stock = product_data[1]
897
- new_stock = current_stock - 1
898
-
899
- logger.info(f"Attempting to update stock for '{product_name}' (ID: {product_id}) from {current_stock} to {new_stock}.")
900
-
901
- # Perform updates in a transaction
902
- c.execute('UPDATE products SET stock = ? WHERE id = ?', (new_stock, product_id))
903
- logger.info(f"Product stock updated in DB for ID {product_id}.")
904
-
905
- # Update user's purchase history (using the User object method is safer)
906
- # This appends the product name and saves the updated list to DB
907
- user.update_products_bought(product_name)
908
- logger.info(f"User {user.username}'s purchase history updated with '{product_name}'.")
909
-
910
- conn.commit() # Commit transaction
911
- logger.info(f"Purchase transaction committed for product '{product_name}'.")
912
-
913
- # Clear the vector store cache AFTER successful commit
914
- load_vector_store.clear()
915
- logger.info("Vector store cache cleared due to stock update.")
916
-
917
- # Inform user via chat (can't write directly to chat history here easily)
918
- # Use st.success/warning/info below the chat area
919
- st.success(f"Purchase link provided for {product_name}! Inventory updated. Remaining stock: {new_stock}.")
920
- if new_stock <= 3 and new_stock > 0:
921
- st.warning(f"Low stock alert: Only {new_stock} units of {product_name} remaining!")
922
- elif new_stock == 0:
923
- st.warning(f"{product_name} is now out of stock.")
924
-
925
- # Rerun to potentially update sidebar purchase history display
926
- st.rerun()
927
-
928
-
929
- elif product_data and product_data[1] <= 0:
930
- logger.warning(f"Product '{product_name}' (ID: {product_data[0]}) is out of stock (DB check). Purchase aborted.")
931
- st.error(f"Sorry, {product_name} just went out of stock before the transaction could be completed. Please choose another item.")
932
- else:
933
- logger.error(f"Product '{product_name}' not found in database during final purchase check.")
934
- st.error(f"Error: Could not find product '{product_name}' to complete the purchase simulation.")
935
-
936
- except sqlite3.Error as db_err:
937
- logger.error(f"Database error during purchase update for '{product_name}': {db_err}", exc_info=True)
938
- st.error(f"A database error occurred while processing the purchase for {product_name}. Please try again.")
939
- if conn: conn.rollback() # Rollback on error
940
- except Exception as final_err:
941
- logger.error(f"Unexpected error during purchase update for '{product_name}': {final_err}", exc_info=True)
942
- st.error(f"An unexpected error occurred processing the purchase for {product_name}.")
943
- if conn: conn.rollback()
944
- finally:
945
- if conn: conn.close() # Ensure connection is closed
946
-
947
-
948
- # --- Run the App ---
949
  if __name__ == "__main__":
950
- main()
951
-
 
1
+
2
  import os
3
  import sqlite3
4
  import streamlit as st
5
  from werkzeug.security import generate_password_hash, check_password_hash
 
6
 
 
7
  from langchain_groq import ChatGroq
8
  from langchain_huggingface import HuggingFaceEmbeddings
9
+ from langchain_community.document_loaders.csv_loader import CSVLoader
10
  from langchain_text_splitters import RecursiveCharacterTextSplitter
11
+ from langchain_core.vectorstores import InMemoryVectorStore
 
 
 
12
  from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
13
  from langchain.tools import Tool
14
  from langchain.agents import AgentExecutor, create_tool_calling_agent
15
  from langchain_core.messages import HumanMessage, AIMessage
16
  from langchain.docstore.document import Document
17
 
 
 
 
 
 
18
  # --- Database Setup ---
19
+ @st.cache_resource
 
 
 
20
  def init_db():
21
+ conn = sqlite3.connect('users.db', check_same_thread=False)
22
+ c = conn.cursor()
23
+
24
+ # Users table
25
+ c.execute('''CREATE TABLE IF NOT EXISTS users
26
+ (id INTEGER PRIMARY KEY AUTOINCREMENT,
27
+ username TEXT UNIQUE NOT NULL,
28
+ password TEXT NOT NULL,
29
+ previous_chat_history TEXT,
30
+ previous_products_bought TEXT)''')
31
+
32
+ # Company settings table
33
+ c.execute('''CREATE TABLE IF NOT EXISTS company_settings
34
+ (id INTEGER PRIMARY KEY AUTOINCREMENT,
35
+ name TEXT NOT NULL,
36
+ business TEXT NOT NULL,
37
+ agent_name TEXT NOT NULL,
38
+ key_features TEXT NOT NULL)''')
39
+
40
+ # Products table with inventory
41
+ c.execute('''CREATE TABLE IF NOT EXISTS products
42
+ (id INTEGER PRIMARY KEY AUTOINCREMENT,
43
+ name TEXT NOT NULL,
44
+ category TEXT NOT NULL,
45
+ price REAL NOT NULL,
46
+ description TEXT NOT NULL,
47
+ features TEXT NOT NULL,
48
+ stock INTEGER NOT NULL DEFAULT 0)''')
49
+
50
+ # Check and update schema if needed
51
+ c.execute("PRAGMA table_info(products)")
52
+ columns = [column[1] for column in c.fetchall()]
53
+ if 'stock' not in columns:
54
+ c.execute('ALTER TABLE products ADD COLUMN stock INTEGER NOT NULL DEFAULT 0')
55
+
56
+ # Insert default company settings if empty
57
+ c.execute('SELECT COUNT(*) FROM company_settings')
58
+ if c.fetchone()[0] == 0:
59
+ c.execute('''INSERT INTO company_settings
60
+ (name, business, agent_name, key_features)
61
+ VALUES (?, ?, ?, ?)''',
62
+ ('TechElectronics',
63
+ 'Consumer Electronics Retailer',
64
+ 'Alex',
65
+ 'Cutting-edge technology, Competitive pricing, Excellent customer service'))
66
+
67
+ conn.commit()
68
+ return conn
69
+
70
+ conn = init_db()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
  # --- Admin Classes ---
73
 
74
  class Company:
75
  @staticmethod
76
  def get_settings():
77
+ c = conn.cursor()
78
+ c.execute('SELECT * FROM company_settings LIMIT 1')
79
+ return c.fetchone()
 
 
 
 
 
 
 
 
 
 
 
80
 
81
  @staticmethod
82
  def update_settings(name, business, agent_name, key_features):
83
+ c = conn.cursor()
84
+ c.execute('''UPDATE company_settings
85
+ SET name=?, business=?, agent_name=?, key_features=?
86
+ WHERE id=1''',
87
+ (name, business, agent_name, key_features))
88
+ conn.commit()
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
  class Product:
91
  @staticmethod
92
  def get_all():
93
+ c = conn.cursor()
94
+ c.execute('SELECT * FROM products')
95
+ return c.fetchall()
 
 
 
 
 
 
 
 
 
96
 
97
  @staticmethod
98
  def get_by_id(product_id):
99
+ c = conn.cursor()
100
+ c.execute('SELECT * FROM products WHERE id = ?', (product_id,))
101
+ return c.fetchone()
 
 
 
 
 
 
 
 
 
102
 
103
  @staticmethod
104
  def add(name, category, price, description, features, stock):
105
+ c = conn.cursor()
106
+ c.execute('''INSERT INTO products
107
+ (name, category, price, description, features, stock)
108
+ VALUES (?, ?, ?, ?, ?, ?)''',
109
+ (name, category, price, description, features, stock))
110
+ conn.commit()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
  @staticmethod
113
  def delete(product_id):
114
+ c = conn.cursor()
115
+ c.execute('DELETE FROM products WHERE id=?', (product_id,))
116
+ conn.commit()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
  @staticmethod
119
  def update_stock(product_id, new_stock):
120
+ c = conn.cursor()
121
+ c.execute('UPDATE products SET stock=? WHERE id=?', (new_stock, product_id))
122
+ conn.commit()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
  # --- User Class ---
125
  class User:
126
+ def __init__(self, id, username, password, chat_history=None, products_bought=None):
127
  self.id = id
128
  self.username = username
129
+ self.password = password
130
+ self.chat_history = chat_history or []
131
+ self.products_bought = products_bought or []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
  @classmethod
134
  def create(cls, username, password):
 
 
 
 
135
  hashed_pw = generate_password_hash(password)
136
+ conn = sqlite3.connect('users.db')
137
+ c = conn.cursor()
138
+ c.execute('INSERT INTO users (username, password) VALUES (?, ?)', (username, hashed_pw))
139
+ user_id = c.lastrowid
140
+ conn.commit()
141
+ conn.close()
142
+ return cls(user_id, username, hashed_pw)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
 
144
  @classmethod
145
  def get_by_username(cls, username):
146
+ conn = sqlite3.connect('users.db')
147
+ c = conn.cursor()
148
+ c.execute('SELECT * FROM users WHERE username = ?', (username,))
149
+ user = c.fetchone()
150
+ conn.close()
151
+ if user:
152
+ return cls(user[0], user[1], user[2],
153
+ eval(user[3]) if user[3] else [],
154
+ eval(user[4]) if user[4] else [])
155
+ return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
 
157
  def update_chat_history(self, new_messages):
 
 
 
 
 
 
 
158
  updated_history = self.chat_history + new_messages
159
+ conn = sqlite3.connect('users.db')
160
+ c = conn.cursor()
161
+ c.execute('UPDATE users SET previous_chat_history = ? WHERE id = ?',
162
+ (str(updated_history), self.id))
163
+ conn.commit()
164
+ conn.close()
165
+ self.chat_history = updated_history # Update in-memory
 
 
166
 
167
+ def update_products_bought(self, new_products):
168
+ updated_products = self.products_bought + new_products
169
+ conn = sqlite3.connect('users.db')
170
+ c = conn.cursor()
171
+ c.execute('UPDATE users SET previous_products_bought = ? WHERE id = ?',
172
+ (str(updated_products), self.id))
173
+ conn.commit()
174
+ conn.close()
175
+ self.products_bought = updated_products # Update in-memory
176
 
177
  # --- AI Setup ---
178
+ os.environ["GROQ_API_KEY"] = st.secrets["GROQ_API_KEY"]
179
+ llm = ChatGroq(
180
+ temperature=0.1,
181
+ model_name="llama3-8b-8192",
182
+ api_key=st.secrets["GROQ_API_KEY"],
183
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
+ embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
186
+
187
+ @st.cache_resource(show_spinner=False)
188
+ def load_data():
189
+ products = Product.get_all()
190
  docs = []
191
  for p in products:
192
+ content = f"Name: {p[1]}\nCategory: {p[2]}\nPrice: {p[3]}\nDescription: {p[4]}\nFeatures: {p[5]}\nStock: {p[6]}"
 
 
 
 
 
 
 
193
  metadata = {"id": p[0], "name": p[1], "category": p[2], "price": p[3], "stock": p[6]}
194
  docs.append(Document(page_content=content, metadata=metadata))
195
+
196
+ text_splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=20)
197
+ splits = text_splitter.split_documents(docs)
198
+ vectorstore = InMemoryVectorStore.from_documents(documents=splits, embedding=embeddings)
199
+ return vectorstore.as_retriever()
200
 
201
+ retriever = load_data()
 
 
202
 
203
+ def retrieve_query(query: str):
204
+ docs = retriever.get_relevant_documents(query)
205
+ st.session_state.last_retrieved_docs = docs
206
+ return docs
207
 
208
+ tool = Tool(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  name="product_retriever",
210
  func=retrieve_query,
211
+ description="Useful for retrieving product information including current stock levels"
212
  )
213
 
214
  # --- Admin Dashboard ---
215
  def admin_dashboard():
 
216
  st.header("Admin Dashboard")
217
+
218
+ with st.expander("Company Settings"):
 
 
 
 
 
 
219
  current_settings = Company.get_settings()
 
 
 
 
220
  with st.form("Company Settings Form"):
221
+ name = st.text_input("Company Name", value=current_settings[1])
222
+ business = st.text_input("Business", value=current_settings[2])
223
+ agent_name = st.text_input("Agent Name", value=current_settings[3])
224
+ key_features = st.text_area("Key Features", value=current_settings[4])
225
+
226
+ if st.form_submit_button("Update Settings"):
227
+ Company.update_settings(name, business, agent_name, key_features)
228
+ st.success("Settings updated!")
229
+
230
+ with st.expander("Product Management"):
231
+ with st.form("Add Product"):
 
 
 
 
 
 
 
232
  st.subheader("Add New Product")
233
+ name = st.text_input("Product Name")
234
+ category = st.text_input("Category")
235
+ price = st.number_input("Price", min_value=0.0)
236
+ description = st.text_area("Description")
237
+ features = st.text_area("Features")
238
+ stock = st.number_input("Initial Stock", min_value=0, value=0)
239
+
240
+ if st.form_submit_button("Add Product"):
241
+ Product.add(name, category, price, description, features, stock)
242
+ st.success("Product added!")
243
+ load_data.clear()
244
+
 
 
 
 
 
 
 
 
245
  st.subheader("Manage Inventory")
246
  products = Product.get_all()
247
+ if products:
 
 
 
248
  for p in products:
249
+ cols = st.columns([3,2,2,1])
250
+ cols[0].write(f"**{p[1]}** ({p[2]})")
251
+ cols[1].write(f"Price: ${p[3]}")
252
+ new_stock = cols[2].number_input(
253
+ "Stock",
254
+ min_value=0,
255
+ value=p[6],
256
+ key=f"stock_{p[0]}"
257
+ )
258
+ if new_stock != p[6]:
259
+ Product.update_stock(p[0], new_stock)
260
+ st.rerun()
261
+ if cols[3].button("❌", key=f"del_{p[0]}"):
262
+ Product.delete(p[0])
263
+ st.rerun()
264
+ else:
265
+ st.info("No products found in database")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
 
267
+ # --- Main App ---
268
  def main():
269
+ company_settings = Company.get_settings()
270
+ company_name = company_settings[1]
 
271
 
272
+ st.title("AI Sales Assistant 🤖")
273
+
274
  if 'user' not in st.session_state:
275
+ st.session_state.user = None
 
 
 
276
  st.session_state.chat_history = []
 
277
  st.session_state.admin_mode = False
278
+ st.session_state.last_retrieved_docs = []
279
+
280
+ # Authentication
281
+ if not st.session_state.user and not st.session_state.admin_mode:
282
+ st.header("Login/Register Admin")
283
+ tab1, tab2, tab3 = st.tabs(["Login", "Register", "Admin"])
284
+
285
+ with tab1:
286
+ with st.form("Login"):
 
 
 
 
 
 
 
 
 
 
 
 
287
  username = st.text_input("Username")
288
  password = st.text_input("Password", type="password")
289
+ if st.form_submit_button("Login"):
 
290
  user = User.get_by_username(username)
291
+ if user and check_password_hash(user.password, password):
292
  st.session_state.user = user
 
293
  st.session_state.chat_history = user.chat_history
 
294
  st.rerun()
295
  else:
296
+ st.error("Invalid credentials")
297
+
298
+ with tab2:
299
+ with st.form("Register"):
300
+ new_user = st.text_input("New Username")
301
+ new_pass = st.text_input("New Password", type="password")
302
+ if st.form_submit_button("Register"):
303
+ if User.get_by_username(new_user):
304
+ st.error("Username already exists")
305
+ else:
306
+ user = User.create(new_user, new_pass)
307
  st.session_state.user = user
308
+ st.session_state.chat_history = []
 
 
309
  st.rerun()
310
+
311
+ with tab3:
312
+ with st.form("Admin Login"):
313
+ admin_pin = st.text_input("Admin PIN", type="password")
314
+ if st.form_submit_button("Admin Login"):
315
+ if admin_pin == st.secrets["ADMIN_PIN"]:
 
 
 
316
  st.session_state.admin_mode = True
 
317
  st.rerun()
318
  else:
319
+ st.error("Invalid Admin PIN")
320
+
321
+ elif st.session_state.admin_mode:
322
+ admin_dashboard()
323
+ if st.button("Exit Admin Mode"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  st.session_state.admin_mode = False
325
  st.rerun()
326
 
327
+ else:
328
+ # Chat Interface
329
+ st.header(f"Welcome to {company_name}, {st.session_state.user.username} 😊!")
330
+ st.subheader("Chat with our AI Sales Assistant")
 
 
 
 
 
 
331
 
332
+ # Display Chat History
333
  for msg in st.session_state.chat_history:
334
+ if msg["type"] == "human":
335
+ with st.chat_message("user"):
336
+ st.write(msg["content"])
337
+ else:
338
+ with st.chat_message("assistant"):
339
+ st.write(msg["content"])
340
 
341
+ # Enhanced System Prompt
342
+ company_settings = Company.get_settings()
343
+ current_products = "\n".join([f"- {p[1]} (${p[3]}, Stock: {p[6]})" for p in Product.get_all()])
344
  system_prompt = f"""
345
+ You are {company_settings[3]}, the AI Sales Assistant for {company_settings[1]} ({company_settings[2]}).
346
+
347
+ Company Profile:
348
+ - Company Name: {company_settings[1]}
349
+ - Business: {company_settings[2]}
350
+ - Key Features: {company_settings[4]}
351
+
352
+ Product Policy & Inventory Management:
353
+ 1. Product Recommendations:
354
+ - Only recommend products with available stock (stock > 0)
355
+ - Always check current stock levels before making recommendations
356
+ - For out-of-stock items, suggest similar in-stock alternatives
357
+ - Never promote or discuss products not in our inventory
358
+
359
+ 2. Stock Management:
360
+ - Track real-time inventory levels for all products
361
+ - Update stock automatically after successful purchases
362
+ - Alert customers about low stock items (stock 3)
363
+ - Inform when items are temporarily out of stock
364
+
365
+ Current Inventory:
366
+ {current_products}
367
+
368
+ Sales Process & Conversation Flow:
369
+ 1. Greeting & Need Assessment
370
+ - Warm, professional welcome
371
+ - Ask relevant questions to understand customer needs
372
+ - Listen actively and acknowledge customer preferences
373
+
374
+ 2. Product Consultation
375
+ - Recommend suitable products based on customer needs
376
+ - Highlight relevant features and benefits
377
+ - Provide accurate pricing and stock information
378
+ - Compare products when appropriate
379
+
380
+ 3. Objection Handling
381
+ - Address concerns professionally
382
+ - Provide factual information
383
+ - Offer alternatives when necessary
384
+ - Focus on value proposition
385
+
386
+ 4. Purchase Process
387
+ - Clear explanation of payment process
388
+ - Generate payment link when customer is ready [https://www.example.com/payment]
389
+ - Confirm stock availability before purchase
390
+ - Update inventory after successful transaction
391
+
392
+ Communication Style:
393
+ - Professional yet friendly tone
394
+ - Clear and concise explanations
395
+ - Empathetic to customer needs
396
+ - Use simple, non-technical language
397
+ - Maintain consistent branding
398
+ - Limited emoji use (max 1-2 per message)
399
+
400
+ Response Templates:
401
+
402
+ For out-of-stock items:
403
+ "I apologize, but [product] is currently out of stock. However, I can recommend [alternative product] which has similar features and is available now. Would you like to learn more about it?"
404
+
405
+ For low stock items:
406
+ "Just to let you know, we only have [X] units of [product] remaining in stock. I'd recommend making your decision soon if you're interested."
407
+
408
+ For unrelated products:
409
+ "I apologize, but as a {company_settings[2]}, we don't carry that item. However, I can suggest [relevant alternative] from our current inventory that might meet your needs. Would you like to learn more?"
410
+
411
+ For payment process:
412
+ "I'm glad you're interested in [product]. I can generate a secure payment link for you to complete your purchase. The current price is $[price]. Would you like to proceed with the purchase of [product]?"
413
+
414
+ Remember to:
415
+ 1. Always verify stock before recommending products
416
+ 2. Keep track of customer preferences
417
+ 3. Provide accurate product information
418
+ 4. Update inventory after successful purchases
419
+ 5. Maintain professional communication
420
  """
421
 
422
+ prompt = ChatPromptTemplate.from_messages([
 
423
  ("system", system_prompt),
424
  MessagesPlaceholder(variable_name="chat_history"),
425
  ("human", "{input}"),
426
+ MessagesPlaceholder(variable_name="agent_scratchpad")
427
  ])
428
 
429
+ tools = [tool]
430
+ agent = create_tool_calling_agent(llm, tools, prompt)
431
+ agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
 
 
 
 
 
 
432
 
433
+ if prompt_input := st.chat_input("Type your message here..."):
434
  with st.chat_message("user"):
435
+ st.write(prompt_input)
436
+
 
 
 
 
 
 
 
 
 
 
 
437
  with st.chat_message("assistant"):
438
+ response = agent_executor.invoke({
439
+ "input": prompt_input,
440
+ "chat_history": [HumanMessage(content=msg["content"]) if msg["type"] == "human" else AIMessage(content=msg["content"])
441
+ for msg in st.session_state.chat_history]
442
+ })["output"]
443
+ st.write(response)
444
+
445
+ # Handle inventory update on purchase
446
+ if "https://www.example.com/payment" in response:
447
+ # Extract product name from the agent's response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
448
  product_name = None
449
+ if "proceed with the purchase of" in response:
450
+ start = response.find("proceed with the purchase of") + len("proceed with the purchase of")
451
+ end = response.find("?", start)
452
+ product_name = response[start:end].strip()
453
+
454
+ if product_name:
455
+ # Retrieve current product details
456
+ docs = retrieve_query(product_name)
457
+ if docs:
458
+ product_doc = docs[0]
459
+ product_id = product_doc.metadata.get("id")
460
+ current_stock = product_doc.metadata.get("stock")
461
+
462
+ if product_id and current_stock > 0:
463
+ try:
464
+ conn = sqlite3.connect('users.db')
465
+ c = conn.cursor()
466
+
467
+ # Verify current stock
468
+ c.execute('SELECT stock FROM products WHERE id = ?', (product_id,))
469
+ actual_stock = c.fetchone()[0]
470
+
471
+ if actual_stock > 0:
472
+ # Update product stock
473
+ c.execute('UPDATE products SET stock = ? WHERE id = ?',
474
+ (actual_stock - 1, product_id))
475
+
476
+ # Update user's purchase history
477
+ c.execute('SELECT previous_products_bought FROM users WHERE id = ?',
478
+ (st.session_state.user.id,))
479
+ current_purchases = c.fetchone()[0]
480
+
481
+ updated_purchases = eval(current_purchases) + [product_name] if current_purchases else [product_name]
482
+
483
+ c.execute('UPDATE users SET previous_products_bought = ? WHERE id = ?',
484
+ (str(updated_purchases), st.session_state.user.id))
485
+
486
+ conn.commit()
487
+ st.session_state.user.products_bought.append(product_name)
488
+ st.success(f"Successfully purchased {product_name}! Stock updated.")
489
+ load_data.clear()
490
+
491
+ # Check for low stock
492
+ if (actual_stock - 1) <= 3 and (actual_stock - 1) > 0:
493
+ st.warning(f"Low stock alert: Only {actual_stock - 1} units of {product_name} remaining!")
494
+ else:
495
+ st.error(f"Sorry, {product_name} is now out of stock!")
496
+ except Exception as e:
497
+ st.error(f"Error processing purchase: {str(e)}")
498
+ if conn:
499
+ conn.rollback()
500
+ finally:
501
+ if conn:
502
+ conn.close()
503
+ else:
504
+ st.error("Product is out of stock or not found in the inventory.")
505
+ else:
506
+ st.error("Could not retrieve product details. Please try again.")
507
+ else:
508
+ st.error("Please specify which product you'd like to purchase.")
509
 
510
+ new_messages = [
511
+ {"type": "human", "content": prompt_input},
512
+ {"type": "ai", "content": response}
513
+ ]
514
+ st.session_state.user.update_chat_history(new_messages)
515
+ st.session_state.chat_history += new_messages
516
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
517
  if __name__ == "__main__":
518
+ main()