sahsan commited on
Commit
3af0227
·
1 Parent(s): 6680d53

Initial commit publishing streamlit chatbot

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ docs/original_cat.pdf filter=lfs diff=lfs merge=lfs -text
Dockerfile CHANGED
@@ -1,20 +1,36 @@
1
- FROM python:3.13.5-slim
2
-
3
- WORKDIR /app
4
 
5
  RUN apt-get update && apt-get install -y \
6
  build-essential \
7
  curl \
 
8
  git \
9
  && rm -rf /var/lib/apt/lists/*
10
 
11
- COPY requirements.txt ./
12
- COPY src/ ./src/
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
- RUN pip3 install -r requirements.txt
 
15
 
16
  EXPOSE 8501
17
 
18
  HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
19
 
20
- ENTRYPOINT ["streamlit", "run", "src/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
 
1
+ FROM python:3.11-slim
 
 
2
 
3
  RUN apt-get update && apt-get install -y \
4
  build-essential \
5
  curl \
6
+ software-properties-common \
7
  git \
8
  && rm -rf /var/lib/apt/lists/*
9
 
10
+ WORKDIR /code
11
+
12
+ COPY ./requirements.txt /code/requirements.txt
13
+
14
+ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
15
+
16
+ # Set up a new user named "user" with user ID 1000
17
+ RUN useradd -m -u 1000 user
18
+
19
+ # Switch to the "user" user
20
+ USER user
21
+
22
+ # Set home to the user's home directory
23
+ ENV HOME=/home/user \
24
+ PATH=/home/user/.local/bin:$PATH
25
+
26
+ # Set the working directory to the user's home directory
27
+ WORKDIR $HOME/app
28
 
29
+ # Copy the current directory contents into the container at $HOME/app setting the owner to the user
30
+ COPY --chown=user . $HOME/app
31
 
32
  EXPOSE 8501
33
 
34
  HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
35
 
36
+ ENTRYPOINT ["streamlit", "run", "src/app.py", "--server.port=8501", "--server.address=0.0.0.0"]
docs/original_cat.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:967231334d0e7d21935aa15d6db1101bd2a4505818ead14ee32f391d30f0b4ae
3
+ size 421618
docs/original_vector/index.faiss ADDED
Binary file (12.3 kB). View file
 
docs/original_vector/index.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d536b8316f55166be494cc12bbab562c5d38165003dc55d68e31cb4fe361ce7e
3
+ size 13518
requirements.txt CHANGED
Binary files a/requirements.txt and b/requirements.txt differ
 
src/app.py ADDED
@@ -0,0 +1,1231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from chains import rag_chain
3
+ from history import get_session_history
4
+ from langchain_core.runnables.history import RunnableWithMessageHistory
5
+ from sklearn.feature_extraction.text import CountVectorizer
6
+ from sklearn.metrics.pairwise import cosine_similarity
7
+ import pandas as pd
8
+ import numpy as np
9
+ import time
10
+ import os
11
+ from dotenv import load_dotenv
12
+ import json
13
+ from openai import OpenAI
14
+ import sqlite3
15
+ import hashlib
16
+ import secrets
17
+ from datetime import datetime, timedelta
18
+
19
+ # Load environment variables
20
+ load_dotenv()
21
+
22
+ # Database setup
23
+ database_path='./docs/users.db'
24
+
25
+ def init_database():
26
+ """Initialize SQLite database with users table"""
27
+ try:
28
+ conn = sqlite3.connect(database_path)
29
+ cursor = conn.cursor()
30
+
31
+ # Create users table
32
+ cursor.execute('''
33
+ CREATE TABLE IF NOT EXISTS users (
34
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
35
+ username TEXT UNIQUE NOT NULL,
36
+ email TEXT UNIQUE NOT NULL,
37
+ password_hash TEXT NOT NULL,
38
+ salt TEXT NOT NULL,
39
+ api_key TEXT,
40
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
41
+ last_login TIMESTAMP,
42
+ is_active BOOLEAN DEFAULT 1
43
+ )
44
+ ''')
45
+
46
+ # Create indexes for better performance
47
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_username ON users(username)')
48
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_email ON users(email)')
49
+
50
+ conn.commit()
51
+ conn.close()
52
+ print("Database initialized successfully")
53
+ return True
54
+ except Exception as e:
55
+ print(f"Error initializing database: {e}")
56
+ if 'st' in globals():
57
+ st.error(f"Database initialization failed: {e}")
58
+ return False
59
+
60
+ def hash_password(password, salt=None):
61
+ """Hash password with salt using SHA-256"""
62
+ if salt is None:
63
+ salt = secrets.token_hex(16)
64
+
65
+ # Combine password and salt
66
+ password_salt = password + salt
67
+ # Hash using SHA-256
68
+ password_hash = hashlib.sha256(password_salt.encode()).hexdigest()
69
+
70
+ return password_hash, salt
71
+
72
+ def verify_password(password, stored_hash, salt):
73
+ """Verify password against stored hash"""
74
+ password_hash, _ = hash_password(password, salt)
75
+ return password_hash == stored_hash
76
+
77
+ def validate_password_strength(password):
78
+ """Validate password strength"""
79
+ if len(password) < 8:
80
+ return False, "Password must be at least 8 characters long"
81
+
82
+ if not any(c.isupper() for c in password):
83
+ return False, "Password must contain at least one uppercase letter"
84
+
85
+ if not any(c.islower() for c in password):
86
+ return False, "Password must contain at least one lowercase letter"
87
+
88
+ if not any(c.isdigit() for c in password):
89
+ return False, "Password must contain at least one number"
90
+
91
+ return True, "Password is strong"
92
+
93
+ def register_user(username, email, password):
94
+ """Register a new user"""
95
+ try:
96
+ # Validate password strength
97
+ is_strong, strength_message = validate_password_strength(password)
98
+ if not is_strong:
99
+ return False, strength_message
100
+
101
+ # Validate username and email format
102
+ if len(username) < 3:
103
+ return False, "Username must be at least 3 characters long"
104
+
105
+ if '@' not in email or '.' not in email:
106
+ return False, "Please enter a valid email address"
107
+
108
+ conn = sqlite3.connect(database_path)
109
+ cursor = conn.cursor()
110
+
111
+ # Check if username or email already exists
112
+ cursor.execute('SELECT id FROM users WHERE username = ? OR email = ?', (username, email))
113
+ if cursor.fetchone():
114
+ conn.close()
115
+ return False, "Username or email already exists"
116
+
117
+ # Hash password
118
+ password_hash, salt = hash_password(password)
119
+
120
+ # Insert new user
121
+ cursor.execute('''
122
+ INSERT INTO users (username, email, password_hash, salt)
123
+ VALUES (?, ?, ?, ?)
124
+ ''', (username, email, password_hash, salt))
125
+
126
+ conn.commit()
127
+ conn.close()
128
+ return True, "User registered successfully"
129
+
130
+ except sqlite3.IntegrityError as e:
131
+ return False, "Username or email already exists"
132
+ except sqlite3.Error as e:
133
+ return False, f"Database error: {str(e)}"
134
+ except Exception as e:
135
+ return False, f"Registration failed: {str(e)}"
136
+
137
+ def authenticate_user(username, password):
138
+ """Authenticate user login"""
139
+ try:
140
+ conn = sqlite3.connect(database_path)
141
+ cursor = conn.cursor()
142
+
143
+ # Get user by username
144
+ cursor.execute('SELECT id, username, password_hash, salt, api_key FROM users WHERE username = ? AND is_active = 1', (username,))
145
+ user = cursor.fetchone()
146
+
147
+ if user and verify_password(password, user[2], user[3]):
148
+ # Update last login
149
+ cursor.execute('UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?', (user[0],))
150
+ conn.commit()
151
+
152
+ user_data = {
153
+ 'id': user[0],
154
+ 'username': user[1],
155
+ 'api_key': user[4]
156
+ }
157
+ conn.close()
158
+ return True, user_data
159
+ else:
160
+ conn.close()
161
+ return False, "Invalid username or password"
162
+
163
+ except sqlite3.Error as e:
164
+ return False, f"Database error: {str(e)}"
165
+ except Exception as e:
166
+ return False, f"Authentication failed: {str(e)}"
167
+
168
+ def change_password(user_id, current_password, new_password):
169
+ """Change user password"""
170
+ try:
171
+ conn = sqlite3.connect(database_path)
172
+ cursor = conn.cursor()
173
+
174
+ # Get current user data
175
+ cursor.execute('SELECT password_hash, salt FROM users WHERE id = ?', (user_id,))
176
+ user = cursor.fetchone()
177
+
178
+ if not user:
179
+ conn.close()
180
+ return False, "User not found"
181
+
182
+ # Verify current password
183
+ if not verify_password(current_password, user[0], user[1]):
184
+ conn.close()
185
+ return False, "Current password is incorrect"
186
+
187
+ # Validate new password strength
188
+ is_strong, strength_message = validate_password_strength(new_password)
189
+ if not is_strong:
190
+ conn.close()
191
+ return False, strength_message
192
+
193
+ # Hash new password
194
+ new_password_hash, new_salt = hash_password(new_password)
195
+
196
+ # Update password
197
+ cursor.execute('UPDATE users SET password_hash = ?, salt = ? WHERE id = ?',
198
+ (new_password_hash, new_salt, user_id))
199
+ conn.commit()
200
+ conn.close()
201
+
202
+ return True, "Password changed successfully"
203
+
204
+ except Exception as e:
205
+ return False, f"Failed to change password: {str(e)}"
206
+
207
+ def update_user_api_key(user_id, api_key):
208
+ """Update user's API key"""
209
+ try:
210
+ conn = sqlite3.connect(database_path)
211
+ cursor = conn.cursor()
212
+
213
+ cursor.execute('UPDATE users SET api_key = ? WHERE id = ?', (api_key, user_id))
214
+ conn.commit()
215
+ conn.close()
216
+ return True, "API key updated successfully"
217
+
218
+ except Exception as e:
219
+ return False, f"Failed to update API key: {str(e)}"
220
+
221
+ def deactivate_user_account(user_id):
222
+ """Deactivate user account"""
223
+ try:
224
+ conn = sqlite3.connect(database_path)
225
+ cursor = conn.cursor()
226
+
227
+ cursor.execute('UPDATE users SET is_active = 0 WHERE id = ?', (user_id,))
228
+ conn.commit()
229
+ conn.close()
230
+ return True, "Account deactivated successfully"
231
+
232
+ except Exception as e:
233
+ return False, f"Failed to deactivate account: {str(e)}"
234
+
235
+ def get_user_profile(user_id):
236
+ """Get user profile information"""
237
+ try:
238
+ conn = sqlite3.connect(database_path)
239
+ cursor = conn.cursor()
240
+
241
+ cursor.execute('''
242
+ SELECT username, email, created_at, last_login, api_key
243
+ FROM users WHERE id = ? AND is_active = 1
244
+ ''', (user_id,))
245
+
246
+ user = cursor.fetchone()
247
+ conn.close()
248
+
249
+ if user:
250
+ return {
251
+ 'username': user[0],
252
+ 'email': user[1],
253
+ 'created_at': user[2],
254
+ 'last_login': user[3],
255
+ 'has_api_key': bool(user[4])
256
+ }
257
+ return None
258
+
259
+ except Exception as e:
260
+ return None
261
+
262
+ def get_user_by_email(email):
263
+ """Get user by email address"""
264
+ try:
265
+ conn = sqlite3.connect(database_path)
266
+ cursor = conn.cursor()
267
+
268
+ cursor.execute('SELECT id, username FROM users WHERE email = ? AND is_active = 1', (email,))
269
+ user = cursor.fetchone()
270
+ conn.close()
271
+
272
+ if user:
273
+ return {'id': user[0], 'username': user[1]}
274
+ return None
275
+
276
+ except Exception as e:
277
+ return None
278
+
279
+ def reset_password_request(email):
280
+ """Request password reset (placeholder for future email functionality)"""
281
+ user = get_user_by_email(email)
282
+ if user:
283
+ # In a real application, this would send an email with a reset link
284
+ # For now, we'll just return success
285
+ return True, f"Password reset instructions sent to {email}"
286
+ else:
287
+ return False, "Email address not found"
288
+
289
+ def get_user_api_key(user_id):
290
+ """Get user's API key"""
291
+ try:
292
+ conn = sqlite3.connect(database_path)
293
+ cursor = conn.cursor()
294
+
295
+ cursor.execute('SELECT api_key FROM users WHERE id = ?', (user_id,))
296
+ result = cursor.fetchone()
297
+ conn.close()
298
+
299
+ return result[0] if result else None
300
+
301
+ except Exception as e:
302
+ return None
303
+
304
+ def create_default_admin():
305
+ """Create a default admin user if no users exist"""
306
+ try:
307
+ conn = sqlite3.connect(database_path)
308
+ cursor = conn.cursor()
309
+
310
+ # Check if any users exist
311
+ cursor.execute('SELECT COUNT(*) FROM users')
312
+ user_count = cursor.fetchone()[0]
313
+
314
+ if user_count == 0:
315
+ # Create default admin user with strong password
316
+ admin_username = "admin"
317
+ admin_email = "[email protected]"
318
+ admin_password = "Admin123!" # Strong password
319
+
320
+ password_hash, salt = hash_password(admin_password)
321
+
322
+ cursor.execute('''
323
+ INSERT INTO users (username, email, password_hash, salt)
324
+ VALUES (?, ?, ?, ?)
325
+ ''', (admin_username, admin_email, password_hash, salt))
326
+
327
+ conn.commit()
328
+ print("Default admin user created: username='admin', password='Admin123!'")
329
+ print("IMPORTANT: Change this password after first login!")
330
+
331
+ conn.close()
332
+ except Exception as e:
333
+ print(f"Error creating default admin: {e}")
334
+
335
+ # Initialize database and create default admin user
336
+ if __name__ == "__main__":
337
+ # Only initialize once when the script is run directly
338
+ init_database()
339
+ create_default_admin()
340
+ else:
341
+ # When imported as a module, just initialize database
342
+ init_database()
343
+
344
+ def validate_api_key(api_key):
345
+ """Validate the API key format and test it"""
346
+ if not api_key:
347
+ return False, "API key cannot be empty"
348
+
349
+ # Check if it starts with 'sk-' and has appropriate length
350
+ if not api_key.startswith('sk-') or len(api_key) < 20:
351
+ return False, "Invalid API key format. OpenAI API keys start with 'sk-' and are at least 20 characters long."
352
+
353
+ try:
354
+ # Test the API key with a simple request
355
+ client = OpenAI(api_key=api_key)
356
+ client.models.list() # This will fail if the API key is invalid
357
+ return True, "API key is valid"
358
+ except Exception as e:
359
+ return False, f"Invalid API key: {str(e)}"
360
+
361
+ def get_api_key_for_use():
362
+ """Get the API key to use for API calls"""
363
+ # Always prioritize the current user's stored API key
364
+ if st.session_state.authenticated and st.session_state.current_user:
365
+ user_api_key = get_user_api_key(st.session_state.current_user['id'])
366
+ if user_api_key:
367
+ return user_api_key
368
+
369
+ # If no user API key, return None (don't fall back to env)
370
+ return None
371
+
372
+ def set_api_key_for_request(api_key):
373
+ """Set API key for a single request and return a cleanup function"""
374
+ if api_key:
375
+ os.environ["OPENAI_API_KEY"] = api_key
376
+ return lambda: os.environ.pop("OPENAI_API_KEY", None)
377
+ return lambda: None
378
+
379
+ # Initialize API key in session state (but don't use env var as default)
380
+ if "api_key" not in st.session_state:
381
+ st.session_state.api_key = ""
382
+
383
+ def update_api_key(new_api_key, user_id=None):
384
+ """Update the API key in session state and database after validation"""
385
+ is_valid, message = validate_api_key(new_api_key)
386
+ if is_valid:
387
+ # Update user's API key in database if user_id is provided
388
+ if user_id:
389
+ success, db_message = update_user_api_key(user_id, new_api_key)
390
+ if not success:
391
+ return False, f"API key validated but failed to save: {db_message}"
392
+
393
+ # Update session state
394
+ st.session_state.api_key = new_api_key
395
+
396
+ return True, message
397
+ return False, message
398
+
399
+ def get_current_user_api_key():
400
+ """Get the current user's API key from database"""
401
+ if st.session_state.authenticated and st.session_state.current_user:
402
+ return get_user_api_key(st.session_state.current_user['id'])
403
+ return None
404
+
405
+ def check_api_key_validity(api_key):
406
+ """Check if the stored API key is still valid"""
407
+ if not api_key:
408
+ return False, "No API key provided"
409
+
410
+ try:
411
+ client = OpenAI(api_key=api_key)
412
+ client.models.list()
413
+ return True, "API key is valid"
414
+ except Exception as e:
415
+ return False, f"API key validation failed: {str(e)}"
416
+
417
+ def get_valid_api_key():
418
+ """Get a valid API key for the current user"""
419
+ user_api_key = get_current_user_api_key()
420
+ if user_api_key:
421
+ is_valid, message = check_api_key_validity(user_api_key)
422
+ if is_valid:
423
+ return user_api_key
424
+ else:
425
+ # API key is invalid, remove it from database
426
+ if st.session_state.current_user:
427
+ update_user_api_key(st.session_state.current_user['id'], None)
428
+ return None
429
+ return None
430
+
431
+ def login_user(username, password):
432
+ """Authenticate and login user"""
433
+ try:
434
+ success, result = authenticate_user(username, password)
435
+ if success:
436
+ st.session_state.authenticated = True
437
+ st.session_state.current_user = result
438
+ st.session_state.show_login = False
439
+ st.session_state.show_register = False
440
+ st.session_state.login_time = datetime.now()
441
+
442
+ # Set user's API key if available
443
+ if result and 'api_key' in result and result['api_key']:
444
+ st.session_state.api_key = result['api_key']
445
+
446
+ return True, "Login successful"
447
+ else:
448
+ return False, result
449
+ except Exception as e:
450
+ return False, f"Login error: {str(e)}"
451
+
452
+ def check_session_timeout():
453
+ """Check if the current session has timed out"""
454
+ if not st.session_state.authenticated or not st.session_state.login_time:
455
+ return False
456
+
457
+ current_time = datetime.now()
458
+ time_diff = current_time - st.session_state.login_time
459
+ timeout_seconds = st.session_state.session_timeout
460
+
461
+ if time_diff.total_seconds() > timeout_seconds:
462
+ logout_user()
463
+ return True
464
+
465
+ return False
466
+
467
+ def logout_user():
468
+ """Logout current user"""
469
+ st.session_state.authenticated = False
470
+ st.session_state.current_user = None
471
+ st.session_state.show_login = True
472
+ st.session_state.show_register = False
473
+ st.session_state.api_key = ""
474
+ st.session_state.messages = []
475
+ st.session_state.session_id = "default_session"
476
+
477
+ def switch_to_register():
478
+ """Switch to registration form"""
479
+ st.session_state.show_login = False
480
+ st.session_state.show_register = True
481
+
482
+ def switch_to_login():
483
+ """Switch to login form"""
484
+ st.session_state.show_login = True
485
+ st.session_state.show_register = False
486
+
487
+ def show_auth_forms():
488
+ """Display authentication forms (login/register)"""
489
+ st.markdown("""
490
+ <div style="display: flex; justify-content: center; width: 100%; margin: 0 auto;">
491
+ <div style="display: inline-block; text-align: center; padding: 4px 20px;
492
+ background: linear-gradient(135deg, #4f46e5, #3b82f6);
493
+ border-radius: 6px; margin: 4px auto;
494
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
495
+ border: 1px solid rgba(255, 255, 255, 0.1);
496
+ backdrop-filter: blur(10px);
497
+ max-width: fit-content;">
498
+ <div style="display: flex; flex-direction: column; gap: 2px;">
499
+ <h1 style="color: white; font-size: 18px; font-weight: 600; margin: 0; padding: 0;
500
+ font-family: 'Arial', sans-serif; line-height: 1.2;
501
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);">
502
+ Academic Advisement Bot
503
+ </h1>
504
+ <p style="color: rgba(255, 255, 255, 0.95); font-size: 11px; font-weight: 400;
505
+ margin: 0; padding: 0; line-height: 1.2;
506
+ letter-spacing: 0.3px;">
507
+ Please login or register to continue
508
+ </p>
509
+ </div>
510
+ </div>
511
+ </div>
512
+ """, unsafe_allow_html=True)
513
+
514
+ # Create two columns for login and register forms
515
+ col1, col2 = st.columns(2)
516
+
517
+ with col1:
518
+ st.subheader("Login")
519
+ with st.form("login_form"):
520
+ login_username = st.text_input("Username", key="login_username")
521
+ login_password = st.text_input("Password", type="password", key="login_password")
522
+ login_submitted = st.form_submit_button("Login")
523
+
524
+ if login_submitted:
525
+ if login_username and login_password:
526
+ success, message = login_user(login_username, login_password)
527
+ if success:
528
+ st.success(message)
529
+ st.rerun()
530
+ else:
531
+ st.error(message)
532
+ else:
533
+ st.error("Please fill in all fields")
534
+
535
+ if st.button("Don't have an account? Register"):
536
+ switch_to_register()
537
+
538
+
539
+
540
+ with col2:
541
+ st.subheader("Register")
542
+ with st.form("register_form"):
543
+ reg_username = st.text_input("Username", key="reg_username")
544
+ reg_email = st.text_input("Email", key="reg_email")
545
+ reg_password = st.text_input("Password", type="password", key="reg_password")
546
+ reg_confirm_password = st.text_input("Confirm Password", type="password", key="reg_confirm_password")
547
+ register_submitted = st.form_submit_button("Register")
548
+
549
+ if register_submitted:
550
+ if reg_username and reg_email and reg_password and reg_confirm_password:
551
+ if reg_password == reg_confirm_password:
552
+ success, message = register_user(reg_username, reg_email, reg_password)
553
+ if success:
554
+ st.success(message)
555
+ st.info("Please login with your new account")
556
+ switch_to_login()
557
+ st.rerun()
558
+ else:
559
+ st.error(message)
560
+ else:
561
+ st.error("Passwords do not match")
562
+ else:
563
+ st.error("Please fill in all fields")
564
+
565
+ if st.button("Already have an account? Login"):
566
+ switch_to_login()
567
+
568
+ # Original RAG chain setup
569
+ conversational_rag_chain = RunnableWithMessageHistory(
570
+ rag_chain,
571
+ get_session_history,
572
+ input_messages_key="input",
573
+ history_messages_key="chat_history",
574
+ output_messages_key="answer",
575
+ )
576
+
577
+ # List of predefined Q&A pairs for evaluation
578
+ questions_answers = [
579
+ # Easy
580
+ ("Which courses can I take first semester?",
581
+ "You can take CET 1100, CET 1111, CET 1120, CET 1150, ENG 1101, MAT 1275."),
582
+ ("Can I take CET1100 after completing ENG1101?",
583
+ "Yes, you can take CET 1100 after completing ENG 1101 because CET 1100 requires no prerequisite."),
584
+ ("What courses should I take after CET1111??",
585
+ "CET 1120 (1 credit), CET 1150 (3 credits), CET 1211 (2 credits), MAT 1375 (4 credits), ENG 1121 (3 credits)."),
586
+ ("Can I overload credits this semester?",
587
+ " It's best to consult with your academic advisor or the registrar's office at your institution to understand the specific requirements and process for requesting a credit overload."),
588
+ ("I just completed CET1111 and MAT1275. What courses should I register for next semester?",
589
+ "Based on the courses you've completed (CET 1111 and MAT 1275), you can consider registering for the following courses next semester: CET 1120 (1 credit) if you haven't taken it yet, CET 1150 (3 credits), CET 1211 (2 credits), MAT 1375 (4 credits), ENG 1101 (3 credits)."),
590
+ ("Can I take CET2305 if I haven’t finished CET1150 yet?",
591
+ "No you can not take CET2350 before taing CET1150 because CET1150 is prerequisite of CET1250, and CET1250 is prerequisite of CET2350. Since you havent completed CET1150, I assume you have not finished CET2350 as well, therefore you cant not take CET2350."),
592
+ ("I registered late and some CET classes are full. What can I do?",
593
+ "Talk to your advisor."),
594
+ ("I failed CET2450. Can I still register for upper-level CET classes?",
595
+ "CET2450 is not prerequired for any higher level CET classes so even if you failed this class you can still take upper level CET classes."),
596
+ # Medium
597
+ ("The courses of the CET department have been changed to EMT to CET. I have failed in EMT1255, which class I should take as an equivalent class to EMT 1255.",
598
+ "If you failed EMT 1255, you should take CET 2350, as EMT 1255 is equivalent to CET 2350."),
599
+ ("I have completed CET1111, CET1150, and ENG1101. What courses can I take next?",
600
+ "You can take CET 1100, CET 1120, MAT 1275, CET 1211, CET 1250 in the next semester."),
601
+ ("Can you list all prerequisites and corequisites for CET 3615?",
602
+ "Prerequisites of CET 3615 are MAT 1575, CET 3525, PHY 1434 or PHY 1442."),
603
+ ("Which general education courses are required for graduation?",
604
+ """For degree in Computer Engineering Technology (CET), the General Education requirements typically include courses in English, Mathematics, and Flex Core electives. Here are the general education courses you need to complete:
605
+ ENG 1101 (3 credits), ENG 1121 (3 credits), MAT 1275 (4 credits), Flex Core 1 (3 credits), Flex Core 2 (3 credits), Flex core 2, Flex core 4, MAT 1375 (4 credits), MAT 1475 (4 credits), PHY 1433(4 credits), MAT 2680, MAT 2580, ID Course."""
606
+ ),
607
+ ("How many credits can I take if I want to overload?",
608
+ "Generally, students are allowed to take a standard full-time course load, which is often around 12-18 credits per semester. To find out the specific number of credits you can take when overloading, and the process to request an overload, you should Consult with your Academic Advisor."),
609
+ ("Can I take an internship while I'm still completing my last two CET courses?",
610
+ "Yes you can take an internship even if you havent completed last two CET courses since the internship lasses has no prerequisite."),
611
+ ("What are the General Education requirements for my AAS degree in CET?",
612
+ "For an AAS degree in Computer Engineering Technology (CET), the general education requirements are ENG 1101 (3 credits) - English Composition 1, ENG 1121 (3 credits), MAT 1275 (4 credits), MAT 1375 (4 credits) - The next math course after MAT 1275, PHY 1433 (4 credits), Flex Core 1 (3 credits), Flex Core 2 (3 credits), ID Course."),
613
+ ("I transferred from another college and completed Calculus I. Do I need to retake it here?",
614
+ "Calculus I is equivalent to MAT 1475 at your current institute. If you have completed Calculus I at another college, you do not need to retake it."),
615
+ ("I'm interested in switching from AAS to BTech after finishing my AAS. What are the requirements?",
616
+ "Talk to your advisor."),
617
+ ("I need help choosing between CET3525 and CET3625 next semester. What should I consider?",
618
+ "You can not take CET3625 before taking CET3525, so first cosider taking CET3525 then in the next semester take CET3625."),
619
+ # Hard
620
+ ("Given my completed courses (CET1111, CET1150, ENG1101), provide a custom-made step-by-step plan for the remaining semesters.",
621
+ """Based on the courses you've completed (CET 1111, CET 1150, ENG 1101), here's a step-by-step plan for the remaining semesters:
622
+ You've already completed first semester.
623
+ Second Semester: CET 1120 (2 credits) - No prerequisites.
624
+ CET 1100 (2 credits) - No prerequisites.
625
+ MAT 1275 (4 credits), ENG 1121 (3 credits), CET 1250(3 credits), CET 1211
626
+ Third Semester: CET 2312 (4 credits) - Prerequisite: CET 1120; Corequisite: PHY 1433.
627
+ CET 2350 (4 credits) - Prerequisite/Corequisite: CET 1250 and MAT 1375.
628
+ CET 2370 (2 credits) - Prerequisite: CET 1250.
629
+ CET 2390 (1 credit) - Prerequisite/Corequisite: CET 2370.
630
+ PHY 1433 (4 credits) - Prerequisite: MAT 1275.
631
+ MAT 1375(4 credits)
632
+ Fourth Semester: CET 2450 (3 credits) - Prerequisite: CET 2350.
633
+ CET 2455 (2 credits) - Prerequisite: CET 2370.
634
+ CET 2461 (2 credits) - Prerequisite/Corequisite: CET 2455, MAT 1475.
635
+ Technical elective, MAT 1475 (4 credits), Flex core 1 (3 credits)
636
+ Fifth Semester: CET 3510 (4 credits) - Prerequisite/Corequisite: CET 2411 and MAT 1575.
637
+ CET 3525 (4 credits) - Prerequisite/Corequisite: MAT 1575.
638
+ MAT 1575 (4 credits) - Prerequisite: MAT 1475.
639
+ PHY 1434 (3 credits), Flex core 2
640
+ Sixth Semester: MAT 2680 (3 credits) - Prerequisite: MAT 1575.
641
+ CET 3615 (4 credits) - Prerequisites: MAT 1575, CET 3525, and PHYS 1434 or PHYS 1442.
642
+ CET 3625 (1 credit) - Prerequisite: CET 3525; Corequisite: MAT 2680.
643
+ CET 3640 (3 credits) - Prerequisites: CET 2411 and CET 3510.
644
+ FLEX CORE 3, COM 1330 (3 credits)
645
+ Seventh Semester: CET 4711 (2 credits) - Prerequisite: CET 3640 and CET 4705.
646
+ MAT 2580 (3 credits) - Prerequisite/Corequisite: MAT 1575.
647
+ CET 4705 (2 credits) - Prerequisite: CET 3625 with a grade of C or better.
648
+ CET 4773 (4 credits) - Prerequisite: CET 3510.
649
+ Technical Elective 1 (4 credits), FLEX CORE 4
650
+ Eighth Semester: Technical Elective 2 (3 credits), CET 4811 (2 credits) - Prerequisites: CET 3640, CET 4711.
651
+ CET 4805 (2 credits) - Prerequisite: CET 4705.
652
+ CET 4864 (4 credits) - Prerequisites: CET 3625, MAT 2580.
653
+ ID Course."""
654
+ ),
655
+ ("I want to know the courses I can take in the 2nd semester. I've completed CET1120, CET 1150 but I haven't completed all the first-semester courses yet. Can you recommend some courses?",
656
+ "For the 2nd semester, you can take CET 1100, CET 1111, MAT 1275, CET 1250, ENG 1100, Flex Core 1."),
657
+ ("If I want to graduate in six/seven semesters instead of eight, how should I plan my courses?",
658
+ """1st semester: CET 1100, CET 1111, CET 1120, CET 1150, ENG 1100, MAT 1275
659
+ 2nd semester: CET 1211, CET 1250, CET 2312, CET 2350, MAT 1375, PHY 1433
660
+ 3rd semester: MAT 1475, PHY 1434, CET 2370, CET 2390, CET 2450, Technical Elective
661
+ 4th semester: CET 2455, CET 2461, CET 3510, CET 3525, Flex Core 2, MAT 1575
662
+ 5th semester: CET 3615, CET 3625, MAT 2680, CET 3640, Flex Core 3, CET 4773
663
+ 6th semester: CET 4705, CET 4711, Technical Elective 1, Flex Core 4, ID, MAT 2580, COM 1330
664
+ 7th semester: CET 4805, CET 4811, CET 4864, Technical Elective 2, Flex Core 1, ENG 1121"""
665
+ ),
666
+ # Long Answer
667
+ ("My catalog year is 2023, but I took a break. Should I follow the new 2025 curriculum now?",
668
+ "Yes you have to follow 2025 curriculum."),
669
+ ("List all courses till the eighth semester.",
670
+ """1st semester: CET 1100, CET 1111, CET 1120, CET 1150, ENG 1100, MAT 1275
671
+ 2nd semester: CET 1211, MAT 1375, CET 1250, ENG 1121, PHY 1433
672
+ 3rd semester: CET 2312, MAT 1475, PHY 1434, CET 2350, CET 2370, CET 2390
673
+ 4th semester: CET 2450, CET 2455, CET 2461, Technical Elective, MAT 1575
674
+ 5th semester: Flex Core 1, CET 3510, CET 3525, MAT 2680, ID
675
+ 6th semester: Flex Core 2, CET 3615, CET 3625, CET 3640, Technical Elective 1
676
+ 7th semester: CET 4705, CET 4711, CET 4773, Flex Core 3, Technical Elective 2
677
+ 8th semester: CET 4811, CET 4864, CET 4805, COM 1330, Flex Core 4"""
678
+ )
679
+ ]
680
+
681
+ def calculate_cosine_similarity(text1, text2):
682
+ """Calculate cosine similarity between two text strings"""
683
+ if isinstance(text1, list):
684
+ text1 = " ".join(text1)
685
+ if isinstance(text2, list):
686
+ text2 = " ".join(text2)
687
+
688
+ # Create vectorizer and transform texts
689
+ vectorizer = CountVectorizer().fit_transform([str(text1), str(text2)]) # Ensure inputs are strings
690
+ vectors = vectorizer.toarray()
691
+
692
+ # Calculate cosine similarity
693
+ cosine_sim = cosine_similarity(vectors)[0, 1]
694
+ return cosine_sim
695
+
696
+ def init_session_state():
697
+ """Initialize session state variables"""
698
+ if "messages" not in st.session_state:
699
+ st.session_state.messages = []
700
+ if "session_id" not in st.session_state:
701
+ st.session_state.session_id = "default_session"
702
+ if "last_input" not in st.session_state:
703
+ st.session_state.last_input = None
704
+ if "evaluation_results" not in st.session_state:
705
+ st.session_state.evaluation_results = []
706
+ if "evaluation_complete" not in st.session_state:
707
+ st.session_state.evaluation_complete = False
708
+ if "current_question_index" not in st.session_state:
709
+ st.session_state.current_question_index = 0
710
+ if "in_evaluation_mode" not in st.session_state:
711
+ st.session_state.in_evaluation_mode = False
712
+ if "api_key" not in st.session_state:
713
+ st.session_state.api_key = ""
714
+ if "authenticated" not in st.session_state:
715
+ st.session_state.authenticated = False
716
+ if "current_user" not in st.session_state:
717
+ st.session_state.current_user = None
718
+ if "show_login" not in st.session_state:
719
+ st.session_state.show_login = True
720
+ if "show_register" not in st.session_state:
721
+ st.session_state.show_register = False
722
+ if "login_time" not in st.session_state:
723
+ st.session_state.login_time = None
724
+ if "session_timeout" not in st.session_state:
725
+ st.session_state.session_timeout = 3600 # 1 hour in seconds
726
+
727
+
728
+ def format_message(text, is_user=False):
729
+ """Format chat messages with styling"""
730
+ if is_user:
731
+ align = "right"
732
+ bg_color = "linear-gradient(135deg, #6366f1, #4f46e5)"
733
+ border_radius = "20px 20px 5px 20px"
734
+ else:
735
+ align = "left"
736
+ bg_color = "linear-gradient(135deg, #1e1e38, #242447)"
737
+ border_radius = "20px 20px 20px 5px"
738
+
739
+ return f"""
740
+ <div style="display: flex; justify-content: {align}; margin: 15px 0;">
741
+ <div style="background: {bg_color};
742
+ padding: 15px 20px;
743
+ border-radius: {border_radius};
744
+ max-width: 80%;
745
+ font-size: 16px;
746
+ line-height: 1.6;
747
+ color: white;
748
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
749
+ animation: fadeIn 0.5s ease-out;
750
+ border: 1px solid rgba(255, 255, 255, 0.1);">
751
+ {text}
752
+ </div>
753
+ </div>
754
+ """
755
+
756
+ def custom_css():
757
+ """Define custom CSS for the app"""
758
+ return """
759
+ <style>
760
+ @keyframes fadeIn {
761
+ from { opacity: 0; transform: translateY(10px); }
762
+ to { opacity: 1; transform: translateY(0); }
763
+ }
764
+
765
+ .stTextInput > div > div > input {
766
+ background-color: #f8f9fa;
767
+ border: 2px solid #e9ecef;
768
+ border-radius: 15px;
769
+ padding: 15px 20px;
770
+ font-size: 16px;
771
+ transition: all 0.3s ease;
772
+ color: black; /* Ensure input text is black */
773
+ }
774
+
775
+ .stButton > button {
776
+ background: linear-gradient(135deg, #6366f1, #4f46e5);
777
+ color: white;
778
+ border: none;
779
+ border-radius: 15px;
780
+ padding: 15px 30px;
781
+ font-weight: 600;
782
+ transition: all 0.3s ease;
783
+ box-shadow: 0 4px 10px rgba(79, 70, 229, 0.2);
784
+ }
785
+
786
+ .stButton > button:hover {
787
+ transform: translateY(-2px);
788
+ box-shadow: 0 6px 15px rgba(79, 70, 229, 0.3);
789
+ }
790
+
791
+ .main {
792
+ background: linear-gradient(135deg, #f8fafc, #eef2ff);
793
+ }
794
+ </style>
795
+ """
796
+
797
+ def handle_submit():
798
+ """Handle user input submission"""
799
+ if st.session_state.user_input and len(st.session_state.user_input.strip()) > 0:
800
+ user_message = st.session_state.user_input.strip()
801
+
802
+ # Prevent duplicate messages
803
+ if st.session_state.last_input != user_message:
804
+ st.session_state.last_input = user_message
805
+
806
+ # Add user message to chat
807
+ st.session_state.messages.append({
808
+ "role": "user",
809
+ "content": user_message
810
+ })
811
+
812
+ # Get response from the chain
813
+ try:
814
+ # Get the user's API key for this request
815
+ user_api_key = get_api_key_for_use()
816
+ if not user_api_key:
817
+ result = "Error: No API key set. Please set your OpenAI API key in the User Panel."
818
+ else:
819
+ # Set the API key for this request and get cleanup function
820
+ cleanup = set_api_key_for_request(user_api_key)
821
+ try:
822
+ result = conversational_rag_chain.invoke(
823
+ {"input": user_message},
824
+ config={"configurable": {"session_id": st.session_state.session_id}}
825
+ )["answer"]
826
+ finally:
827
+ # Always cleanup the environment variable
828
+ cleanup()
829
+ except Exception as e:
830
+ result = f"Error communicating with the model: {str(e)}. Please check your API key and network."
831
+ st.error(result)
832
+
833
+
834
+ # Add assistant response to chat
835
+ st.session_state.messages.append({
836
+ "role": "assistant",
837
+ "content": result
838
+ })
839
+
840
+ # Reset input box
841
+ st.session_state.user_input = ""
842
+
843
+ def run_automated_evaluation():
844
+ """Run the automated evaluation process"""
845
+ st.session_state.in_evaluation_mode = True
846
+ st.session_state.evaluation_results = []
847
+ st.session_state.messages = [] # Clear messages for evaluation display
848
+
849
+ # Check if user has API key set
850
+ user_api_key = get_api_key_for_use()
851
+ if not user_api_key:
852
+ st.error("OpenAI API Key is not set. Please set it in the User Panel.")
853
+ st.session_state.in_evaluation_mode = False # Exit evaluation mode
854
+ return
855
+
856
+ progress_bar = st.progress(0)
857
+ status_text = st.empty()
858
+
859
+ for i, (question, expected_answer) in enumerate(questions_answers):
860
+ progress = (i) / len(questions_answers)
861
+ progress_bar.progress(progress)
862
+ status_text.text(f"Processing question {i+1}/{len(questions_answers)}")
863
+
864
+ st.session_state.session_id = f"eval_session_{i}" # Unique session for each question
865
+
866
+ actual_answer = "Error: Could not get response." # Default error message
867
+ try:
868
+ # Get the user's API key for this request
869
+ user_api_key = get_api_key_for_use()
870
+ if not user_api_key:
871
+ actual_answer = "Error: No API key set. Please set your OpenAI API key in the User Panel."
872
+ else:
873
+ # Set the API key for this request and get cleanup function
874
+ cleanup = set_api_key_for_request(user_api_key)
875
+ try:
876
+ actual_answer = conversational_rag_chain.invoke(
877
+ {"input": question},
878
+ config={"configurable": {"session_id": st.session_state.session_id}}
879
+ )["answer"]
880
+ finally:
881
+ # Always cleanup the environment variable
882
+ cleanup()
883
+ except Exception as e:
884
+ actual_answer = f"Error invoking RAG chain: {str(e)}"
885
+ st.warning(f"Error processing question '{question[:50]}...': {str(e)}")
886
+
887
+
888
+ similarity = calculate_cosine_similarity(str(actual_answer), str(expected_answer))
889
+
890
+ st.session_state.evaluation_results.append({
891
+ "question": question,
892
+ "expected_answer": str(expected_answer), # Ensure it's a string
893
+ "actual_answer": str(actual_answer), # Ensure it's a string
894
+ "similarity": similarity
895
+ })
896
+
897
+ time.sleep(0.5) # Small delay
898
+
899
+ progress_bar.progress(1.0)
900
+ status_text.text("Evaluation completed!")
901
+
902
+ save_results_to_csv()
903
+ st.session_state.evaluation_complete = True
904
+
905
+ def save_results_to_csv():
906
+ """Save evaluation results to a CSV file"""
907
+ if st.session_state.evaluation_results:
908
+ df = pd.DataFrame(st.session_state.evaluation_results)
909
+ df.to_csv("qa_evaluation_results.csv", index=False)
910
+ return df
911
+ return pd.DataFrame() # Return empty DataFrame if no results
912
+
913
+ def display_evaluation_results(location="main"):
914
+ """Display the evaluation results in the app"""
915
+ if not st.session_state.evaluation_results:
916
+ if location == "main": # Only show this prominent message in the main area
917
+ st.info("No evaluation results to display. Run the evaluation from the Admin Panel.")
918
+ return
919
+
920
+ df = pd.DataFrame(st.session_state.evaluation_results)
921
+
922
+ avg_similarity = df['similarity'].mean() if not df.empty else 0
923
+ st.metric("Average Cosine Similarity", f"{avg_similarity:.4f}")
924
+
925
+ st.write("Detailed Results:")
926
+ tab1, tab2 = st.tabs(["Detailed View", "Summary Chart"])
927
+
928
+ with tab1:
929
+ for idx, row in df.iterrows():
930
+ with st.expander(f"Question {idx + 1}: {row['question'][:100]}..."):
931
+ st.write("**Question:**")
932
+ st.write(row['question'])
933
+
934
+ st.write("**LLM Answer:**")
935
+ st.write(row['actual_answer'])
936
+
937
+ st.write("**Expected Answer:**")
938
+ st.write(row['expected_answer'])
939
+
940
+ st.write("**Cosine Similarity:**")
941
+ st.write(f"{row['similarity']:.4f}")
942
+
943
+ if row['similarity'] >= 0.8:
944
+ st.success(f"High similarity: {row['similarity']:.4f}")
945
+ elif row['similarity'] >= 0.6:
946
+ st.warning(f"Medium similarity: {row['similarity']:.4f}")
947
+ else:
948
+ st.error(f"Low similarity: {row['similarity']:.4f}")
949
+
950
+ with tab2:
951
+ st.write("Similarity Scores Chart")
952
+ if not df.empty:
953
+ chart_data = pd.DataFrame({
954
+ 'Question': [f"Q{i+1}" for i in range(len(df))],
955
+ 'Similarity': df['similarity']
956
+ }).set_index('Question')
957
+ st.bar_chart(chart_data)
958
+ else:
959
+ st.write("No data for chart.")
960
+
961
+
962
+ if not df.empty:
963
+ csv = df.to_csv(index=False)
964
+ st.download_button(
965
+ label="Download Results CSV",
966
+ data=csv,
967
+ file_name="qa_evaluation_results.csv",
968
+ mime="text/csv",
969
+ key=f"download_results_csv_{location}"
970
+ )
971
+
972
+ def main():
973
+ st.set_page_config(page_title="Academic Advisement Bot", layout="wide")
974
+
975
+ # Initialize database first
976
+ if not init_database():
977
+ st.error("Failed to initialize database. Please check the console for errors.")
978
+ st.stop()
979
+
980
+ init_session_state()
981
+
982
+ st.markdown(custom_css(), unsafe_allow_html=True)
983
+
984
+ # Check authentication status
985
+ if not st.session_state.authenticated:
986
+ show_auth_forms()
987
+ return
988
+
989
+ # Check session timeout
990
+ if check_session_timeout():
991
+ st.warning("Session expired. Please login again.")
992
+ st.rerun()
993
+
994
+ # Additional safety check for current_user
995
+ if not st.session_state.current_user:
996
+ st.error("User session data is missing. Please login again.")
997
+ logout_user()
998
+ st.rerun()
999
+
1000
+ # User is authenticated, show main app
1001
+ welcome_message = f"Welcome, {st.session_state.current_user['username']}! Ask questions about your course material!"
1002
+
1003
+ st.markdown(f"""
1004
+ <div style="display: flex; justify-content: center; width: 100%; margin: 0 auto;">
1005
+ <div style="display: inline-block; text-align: center; padding: 4px 20px;
1006
+ background: linear-gradient(135deg, #4f46e5, #3b82f6);
1007
+ border-radius: 6px; margin: 4px auto;
1008
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
1009
+ border: 1px solid rgba(255, 255, 255, 0.1);
1010
+ backdrop-filter: blur(10px);
1011
+ max-width: fit-content;">
1012
+ <div style="display: flex; flex-direction: column; gap: 2px;">
1013
+ <h1 style="color: white; font-size: 18px; font-weight: 600; margin: 0; padding: 0;
1014
+ font-family: 'Arial', sans-serif; line-height: 1.2;
1015
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);">
1016
+ Academic Advisement Bot
1017
+ </h1>
1018
+ <p style="color: rgba(255, 255, 255, 0.95); font-size: 11px; font-weight: 400;
1019
+ margin: 0; padding: 0; line-height: 1.2;
1020
+ letter-spacing: 0.3px;">
1021
+ {welcome_message}
1022
+ </p>
1023
+ </div>
1024
+ </div>
1025
+ </div>
1026
+ <style>
1027
+ div[data-testid="stVerticalBlock"] > div:first-child {{
1028
+ text-align: center;
1029
+ }}
1030
+ </style>
1031
+ """, unsafe_allow_html=True)
1032
+
1033
+ with st.sidebar:
1034
+ st.header("User Panel")
1035
+
1036
+ # Additional safety check for current_user in sidebar
1037
+ if not st.session_state.current_user:
1038
+ st.error("User session data is missing. Please login again.")
1039
+ if st.button("Return to Login"):
1040
+ logout_user()
1041
+ st.rerun()
1042
+ return
1043
+
1044
+ # User info and logout
1045
+ st.subheader(f"Welcome, {st.session_state.current_user['username']}!")
1046
+
1047
+ # User profile information
1048
+ try:
1049
+ user_profile = get_user_profile(st.session_state.current_user['id'])
1050
+ if user_profile:
1051
+ with st.expander("Profile Information"):
1052
+ st.write(f"**Username:** {user_profile['username']}")
1053
+ st.write(f"**Email:** {user_profile['email']}")
1054
+ st.write(f"**Member since:** {user_profile['created_at']}")
1055
+ if user_profile['last_login']:
1056
+ st.write(f"**Last login:** {user_profile['last_login']}")
1057
+ st.write(f"**API Key:** {'Set' if user_profile['has_api_key'] else 'Not set'}")
1058
+ except Exception as e:
1059
+ st.error(f"Error loading profile: {str(e)}")
1060
+
1061
+ if st.button("Logout", type="secondary"):
1062
+ logout_user()
1063
+ st.rerun()
1064
+
1065
+ st.divider()
1066
+
1067
+ # Account management section
1068
+ with st.expander("Account Management"):
1069
+ # Password change
1070
+ st.subheader("Change Password")
1071
+ with st.form("change_password_form"):
1072
+ current_password = st.text_input("Current Password", type="password", key="current_password")
1073
+ new_password = st.text_input("New Password", type="password", key="new_password")
1074
+ confirm_new_password = st.text_input("Confirm New Password", type="password", key="confirm_new_password")
1075
+ change_submitted = st.form_submit_button("Change Password")
1076
+
1077
+ if change_submitted:
1078
+ if current_password and new_password and confirm_new_password:
1079
+ if new_password == confirm_new_password:
1080
+ success, message = change_password(st.session_state.current_user['id'], current_password, new_password)
1081
+ if success:
1082
+ st.success(message)
1083
+ st.rerun()
1084
+ else:
1085
+ st.error(message)
1086
+ else:
1087
+ st.error("New passwords do not match")
1088
+ else:
1089
+ st.error("Please fill in all fields")
1090
+
1091
+ st.divider()
1092
+
1093
+ # Account deactivation
1094
+ st.subheader("Deactivate Account")
1095
+ st.warning("This action cannot be undone. You will need to contact support to reactivate your account.")
1096
+ if st.button("Deactivate Account", type="secondary"):
1097
+ success, message = deactivate_user_account(st.session_state.current_user['id'])
1098
+ if success:
1099
+ st.success(message)
1100
+ st.info("You will be logged out in 5 seconds...")
1101
+ time.sleep(5)
1102
+ logout_user()
1103
+ st.rerun()
1104
+ else:
1105
+ st.error(message)
1106
+
1107
+
1108
+
1109
+ st.subheader("API Key Management")
1110
+ try:
1111
+ # Get the current user's API key from database
1112
+ current_user_api_key = get_api_key_for_use()
1113
+
1114
+ if current_user_api_key:
1115
+ masked_api_key = current_user_api_key[:4] + "..." + current_user_api_key[-4:] if len(current_user_api_key) > 8 else "Invalid"
1116
+ st.text(f"Current API Key: {masked_api_key}")
1117
+ st.success("✅ API key is set and ready to use")
1118
+ else:
1119
+ st.text("Current API Key: Not set")
1120
+ st.warning("⚠️ Please set your OpenAI API key to use the chatbot")
1121
+
1122
+ st.divider()
1123
+
1124
+ new_api_key_input = st.text_input("Set/Update API Key", type="password", key="new_api_key_input_field")
1125
+
1126
+ col1, col2 = st.columns(2)
1127
+ with col1:
1128
+ if st.button("Update API Key"):
1129
+ if new_api_key_input:
1130
+ success, message = update_api_key(new_api_key_input, st.session_state.current_user['id'])
1131
+ if success:
1132
+ st.success(message)
1133
+ st.rerun()
1134
+ else:
1135
+ st.error(message)
1136
+ else:
1137
+ st.warning("Please enter a new API key.")
1138
+
1139
+ with col2:
1140
+ if st.button("Test Current API Key"):
1141
+ if current_user_api_key:
1142
+ is_valid, message = check_api_key_validity(current_user_api_key)
1143
+ if is_valid:
1144
+ st.success("✅ API key is valid and working!")
1145
+ else:
1146
+ st.error(f"❌ API key validation failed: {message}")
1147
+ st.warning("Please update your API key")
1148
+ else:
1149
+ st.warning("No API key to test")
1150
+
1151
+ # Add option to clear API key
1152
+ if current_user_api_key:
1153
+ if st.button("Clear API Key", type="secondary"):
1154
+ success, message = update_user_api_key(st.session_state.current_user['id'], None)
1155
+ if success:
1156
+ st.success("API key cleared successfully")
1157
+ st.rerun()
1158
+ else:
1159
+ st.error(f"Failed to clear API key: {message}")
1160
+ except Exception as e:
1161
+ st.error(f"Error in API key management: {str(e)}")
1162
+
1163
+ st.divider()
1164
+
1165
+ try:
1166
+ if st.button("Run Automated Evaluation"):
1167
+ run_automated_evaluation()
1168
+ except Exception as e:
1169
+ st.error(f"Error running evaluation: {str(e)}")
1170
+
1171
+ try:
1172
+ if st.session_state.evaluation_complete and st.session_state.in_evaluation_mode: # Show in sidebar only during/after eval
1173
+ st.success("Evaluation completed! Results saved to qa_evaluation_results.csv")
1174
+ display_evaluation_results(location="sidebar")
1175
+ except Exception as e:
1176
+ st.error(f"Error displaying evaluation results: {str(e)}")
1177
+
1178
+
1179
+ if not st.session_state.in_evaluation_mode:
1180
+ # Check if user has API key set
1181
+ user_api_key = get_api_key_for_use()
1182
+ if not user_api_key:
1183
+ st.warning("⚠️ **No API Key Set** - Please set your OpenAI API key in the User Panel to start chatting!")
1184
+ st.info("Go to the sidebar → API Key Management → Set your API key")
1185
+ else:
1186
+ st.success("✅ **API Key Ready** - You can now ask questions!")
1187
+
1188
+ chat_container = st.container()
1189
+ with chat_container:
1190
+ for message in st.session_state.messages:
1191
+ st.markdown(
1192
+ format_message(
1193
+ message["content"],
1194
+ message["role"] == "user"
1195
+ ),
1196
+ unsafe_allow_html=True
1197
+ )
1198
+
1199
+ st.markdown("<div style='position: fixed; bottom: 0; left: 0; right: 0; padding: 20px; background: white; box-shadow: 0 -4px 15px rgba(0, 0, 0, 0.1); z-index: 999;'>", unsafe_allow_html=True)
1200
+ cols = st.columns([7, 1])
1201
+ with cols[0]:
1202
+ st.text_input(
1203
+ "Message",
1204
+ key="user_input",
1205
+ placeholder="Ask about courses..." if user_api_key else "Set API key first...",
1206
+ on_change=handle_submit if user_api_key else None,
1207
+ label_visibility="collapsed",
1208
+ disabled=not user_api_key
1209
+ )
1210
+ with cols[1]:
1211
+ st.button("Send", on_click=handle_submit, use_container_width=True, disabled=not user_api_key)
1212
+ st.markdown("</div>", unsafe_allow_html=True)
1213
+ else: # Evaluation mode display in main area
1214
+ if st.session_state.evaluation_complete:
1215
+ st.subheader("Evaluation Results (Main View)")
1216
+ display_evaluation_results(location="main_eval_results") # Unique key for download button
1217
+ if st.button("Return to Chat Mode"):
1218
+ st.session_state.in_evaluation_mode = False
1219
+ st.session_state.messages = [] # Clear eval messages
1220
+ st.rerun()
1221
+ else:
1222
+ st.info("Evaluation in progress... Please wait. Results will appear here and in the sidebar once complete.")
1223
+
1224
+
1225
+ st.markdown("""
1226
+ <div style="text-align: center; padding: 20px 0; color: #6b7280; font-size: 14px; padding-bottom: 70px;"> Built with ❤️ to help students succeed
1227
+ </div>
1228
+ """, unsafe_allow_html=True)
1229
+
1230
+ if __name__ == "__main__":
1231
+ main()
src/chains.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain.chains import create_retrieval_chain
2
+ from langchain.chains.combine_documents import create_stuff_documents_chain
3
+ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
4
+ from langchain_community.vectorstores import FAISS
5
+ from embeddings import chat_model, embeddings
6
+ from langchain.chains import create_history_aware_retriever
7
+ import os
8
+ from pathlib import Path
9
+
10
+ system_prompt = (
11
+ "find the courses that match the student query and answer according to the question. if a student ask you to list all courses till eighth semester then list all the courses till 8th semester from the given datatset. Do not miss any class for god sake."
12
+ "Consider the prerequisites and corequisites for each course when making recommendations."
13
+ "Pre-requisite: Classes you must take prior to the specific class."
14
+ "Co-requisite: Classes you can take along with the desired class if you have not taken them before."
15
+ "For example, suppose someone took CET1111, CET1150, ENG1101 so far."
16
+ "As they have not completed all classes needed for starting second-semester classes,"
17
+ "they cannot take any classes which requires classes other than the completed ones, so first, they can take CET 1120, CET1100 and some other classes which does not require prerequisite of other than the completed ones."
18
+ "Have a user-friendly but straightforward conversation and do not use unnecessary sentences.Provide a detailed and accurate list of all courses a student needs to take from the first semester "
19
+ "to the eighth semester to graduate. Include each course's name, prerequisites, corequisites, and semester. "
20
+ "Base your answer on the program's curriculum guidelines and ensure no course is omitted. If a student asks for "
21
+ "graduation requirements, confirm you cover all required general education and major-specific courses"
22
+ "{context}"
23
+ )
24
+
25
+ # Get the current file's directory
26
+ current_dir = Path(__file__).parent
27
+
28
+ vector = FAISS.load_local(r".\docs\original_vector", embeddings, allow_dangerous_deserialization=True)
29
+
30
+ # Create retriever and retrieval chain
31
+ retriever = vector.as_retriever()
32
+
33
+ # Create history-aware retriever
34
+ history_aware_retriever = create_history_aware_retriever(
35
+ chat_model, retriever,
36
+ ChatPromptTemplate.from_messages(
37
+ [
38
+ ("system", "Given a chat history and the latest user question..."),
39
+ MessagesPlaceholder(variable_name="chat_history"),
40
+ ("human", "{input}"),
41
+ ]
42
+ )
43
+ )
44
+
45
+ # Create final question-answering chain
46
+ qa_prompt = ChatPromptTemplate.from_messages(
47
+ [
48
+ ("system", system_prompt),
49
+ MessagesPlaceholder("chat_history"),
50
+ ("human", "{input}"),
51
+ ]
52
+ )
53
+ question_answer_chain = create_stuff_documents_chain(chat_model, qa_prompt)
54
+
55
+ # Create RAG chain
56
+ rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain)
57
+
58
+
59
+
src/embeddings.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_google_genai import GoogleGenerativeAIEmbeddings, ChatGoogleGenerativeAI
2
+ from dotenv import load_dotenv
3
+ import getpass
4
+ import os
5
+ from langchain_openai import ChatOpenAI
6
+ from langchain_openai import OpenAIEmbeddings
7
+ load_dotenv()
8
+
9
+ # Fetch API keys directly from .env
10
+ GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
11
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
12
+
13
+ #AI model for creating embeddings of query(google)
14
+ embeddings = GoogleGenerativeAIEmbeddings(
15
+ model="models/embedding-001",
16
+ google_api_key=GOOGLE_API_KEY
17
+ )
18
+
19
+ #AI model for generating ans (chat gpt)
20
+ chat_model = ChatOpenAI(
21
+ model="gpt-4o-mini",
22
+ temperature=0,
23
+ max_tokens=None,
24
+ timeout=None,
25
+ max_retries=2,
26
+ api_key=OPENAI_API_KEY
27
+ )
src/history.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_community.chat_message_histories import ChatMessageHistory
2
+ from langchain_core.chat_history import BaseChatMessageHistory
3
+
4
+ # Store for managing session-based histories
5
+ store = {}
6
+
7
+ def get_session_history(session_id: str) -> BaseChatMessageHistory:
8
+ """Retrieve or create chat history for a given session ID."""
9
+ if session_id not in store:
10
+ store[session_id] = ChatMessageHistory()
11
+ return store[session_id]