Spaces:
Sleeping
Sleeping
Initial commit publishing streamlit chatbot
Browse files- .gitattributes +1 -0
- Dockerfile +23 -7
- docs/original_cat.pdf +3 -0
- docs/original_vector/index.faiss +0 -0
- docs/original_vector/index.pkl +3 -0
- requirements.txt +0 -0
- src/app.py +1231 -0
- src/chains.py +59 -0
- src/embeddings.py +27 -0
- src/history.py +11 -0
.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.
|
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 |
-
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
|
14 |
-
|
|
|
15 |
|
16 |
EXPOSE 8501
|
17 |
|
18 |
HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
|
19 |
|
20 |
-
ENTRYPOINT ["streamlit", "run", "src/
|
|
|
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]
|